diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..28c5cd2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# http://editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +max_line_length = 80 +trim_trailing_whitespace = true + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..0fceb13 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,14 @@ +__fixtures__ +__mocks__ +dist +node_modules +.yarn +.history +build +coverage +jest.config.js +jest.transform.js +docusaurus.config.ts +sidebars.js +*.md +lib diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..3551072 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,15 @@ +module.exports = { + parserOptions: { + ecmaVersion: 7, + sourceType: 'module', + }, + plugins: ['@docusaurus', '@typescript-eslint'], + extends: [ + 'plugin:@docusaurus/recommended', + 'plugin:@typescript-eslint/recommended', + ], + rules: { + '@typescript-eslint/no-unused-vars': 'off', + "@typescript-eslint/no-explicit-any": "error" + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f4ce7ee --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI + +on: + push: + branches: + - main + +jobs: + build-and-deploy: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest] # macos-latest, windows-latest + node: [18] + + steps: + - uses: actions/checkout@v4 + + - name: Set node version to ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - run: corepack enable + + - name: Setup + run: npm i -g @antfu/ni + + - name: Install + run: nci + + - name: Build + run: nr build + + - name: SSH Deploy + uses: easingthemes/ssh-deploy@v4.1.10 + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + ARGS: "-avzr --delete" + SOURCE: "build" + REMOTE_HOST: ${{ secrets.REMOTE_HOST }} + REMOTE_USER: "root" + TARGET: "/opt/1panel/apps/openresty/openresty/www/sites/kuizuo.cn/index" diff --git a/.github/workflows/docsearch.yml.bak b/.github/workflows/docsearch.yml.bak new file mode 100644 index 0000000..06ec9d8 --- /dev/null +++ b/.github/workflows/docsearch.yml.bak @@ -0,0 +1,28 @@ +name: docsearch + +on: + push: + branches: + - main + +jobs: + algolia: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Get the content of docsearch.json as config + id: algolia_config + run: echo "::set-output name=config::$(cat docsearch.json | jq -r tostring)" + + - name: Run algolia/docsearch-scraper image + env: + ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }} + ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }} + CONFIG: ${{ steps.algolia_config.outputs.config }} + run: | + docker run \ + --env APPLICATION_ID=${ALGOLIA_APP_ID} \ + --env API_KEY=${ALGOLIA_API_KEY} \ + --env "CONFIG=${CONFIG}" \ + algolia/docsearch-scraper diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..11c81c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Dependencies +node_modules + +# Production +/build +/drafts +/tmp +cloudbaserc.json + +# Generated files +.docusaurus +.cache-loader + +# Misc +.DS_Store +.env + +# Packages +/packages + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-error.log* + +.vercel diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..cf04042 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +shamefully-hoist=true +strict-peer-dependencies=false diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..a65b417 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +lib diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..efe2e44 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,14 @@ +{ + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "semi": false, + "singleQuote": true, + "quoteProps": "as-needed", + "jsxSingleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "jsxBracketSameLine": false, + "arrowParens": "avoid", + "proseWrap": "never" +} diff --git a/.stylelintrc.js b/.stylelintrc.js new file mode 100644 index 0000000..cbce6ad --- /dev/null +++ b/.stylelintrc.js @@ -0,0 +1,19 @@ +module.exports = { + extends: ['stylelint-config-standard-scss', 'stylelint-config-prettier-scss'], + rules: { + 'selector-pseudo-class-no-unknown': [ + true, + { + // :global is a CSS modules feature to escape from class name hashing + ignorePseudoClasses: ['global'], + }, + ], + 'selector-class-pattern': null, + 'custom-property-empty-line-before': null, + 'selector-id-pattern': null, + 'declaration-empty-line-before': null, + 'no-descending-specificity': null, + 'comment-empty-line-before': null, + 'value-keyword-case': ['lower', { camelCaseSvgKeywords: true }], + }, +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2f1d248 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,14 @@ +{ + "prettier.enable": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "never" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "stylelint.validate": ["css", "less", "postcss", "scss", "sass"] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..448703c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 kuizuo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/blog/authors.yml b/blog/authors.yml new file mode 100644 index 0000000..8027bfa --- /dev/null +++ b/blog/authors.yml @@ -0,0 +1,5 @@ +kuizuo: + name: 愧怍 + title: 全栈 typescripter / 即将毕业的学生 + url: https://github.com/kuizuo + image_url: /img/logo.webp diff --git "a/blog/develop/AutoHotkey\351\224\256\347\233\230\346\230\240\345\260\204.md" "b/blog/develop/AutoHotkey\351\224\256\347\233\230\346\230\240\345\260\204.md" new file mode 100644 index 0000000..677f549 --- /dev/null +++ "b/blog/develop/AutoHotkey\351\224\256\347\233\230\346\230\240\345\260\204.md" @@ -0,0 +1,49 @@ +--- +slug: autohotkey +title: AutoHotkey键盘映射 +date: 2022-07-08 +authors: kuizuo +tags: [工具, keyMap] +keywords: [工具, keyMap] +--- + +当我使用笔记本的时候,每次移动光标,都要大费周章,同时由于笔记本的缘故,导致键入Home与End都需要搭配Fn功能键来实现。所以我希望在任何情况下(敲代码,写文章)都可以将某些组合键绑定为上下左右键,在代码编辑器上有键盘映射可以设置,但脱离代码编辑器就不起作用了,在window下有个神器 [AutoHotkey](https://www.autohotkey.com/) 可以实现我想要的功能。 + + + +## 安装 + +打开[官网](https://www.autohotkey.com/),点击Download,安装即可。 + +## 使用 + +安装完成后,右键新建会AutoHotKey Srcipt后缀为ahk。例如创建demo.ahk,其内容如下 + +```ahk +<+Shift + Ctrl + [HIJKL;] 就可以看到光标上下左右移动。 + +这里对上面语法进行讲解 + +| 键名 | 热键标识 | +| ----- | -------- | +| Ctrl | ^ | +| Shift | + | +| Alt | ! | +| Win | # | + +如果要针对左右Ctrl或Shfit只需要在前面添加`<` `>` 。`::`则作为映射关系,左边的按键作用于何种指令,而右侧则是左侧按键所对应的指令,这里的指令相对简单,只是发送键盘上下左右的关系,指令还可以实现信息框MsgBox 启动应用等等。具体还有更多键盘与鼠标热键详情可在AutoHotkey Help手册中查看,非常详细,不过是英文。 + +具体要映射的快捷键可自行发挥,但要切记不建议与常用快捷键冲突,例如上面为何是IJKL而不是WASD,其原因会导致快捷键冲突。 + +此外AutoHotkey不仅能做键盘映射,实现宏定义,一键启动任务也不成问题,篇幅有限,就不做过多演示,有兴趣可自行研究。 + + + diff --git "a/blog/develop/HTTP\350\257\267\346\261\202\344\271\213Content-Type.md" "b/blog/develop/HTTP\350\257\267\346\261\202\344\271\213Content-Type.md" new file mode 100644 index 0000000..852fa4d --- /dev/null +++ "b/blog/develop/HTTP\350\257\267\346\261\202\344\271\213Content-Type.md" @@ -0,0 +1,109 @@ +--- +slug: content-type-of-http-request +title: HTTP请求之Content-Type +date: 2020-12-12 +authors: kuizuo +tags: [http] +keywords: [http] +--- + + + +## Content-type + +先看一条 HTTP 请求 + +```http +POST https://xxx.kuizuo.cn/v2/login HTTP/1.1 +Host: xxx.kuizuo.cn +Connection: keep-alive +Content-Length: 121 +User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 +// Content-Type: application/json;charset=UTF-8 +Accept: application/json, text/plain, */* + +{"username":"kuizuo","password":"a12345678"} +``` + +上面那个请求发送到我的服务器,服务器却接收到的是这样一串值 + +```json +{ "{\"username\":\"kuizuo\",\"password\":\"a12345678\"}": "" } +``` + +很显然,它把 json 格式解析成了 x-www-form-urlencoded。 + +一个很简单的登录请求,注意一个协议头`Content-Type`,它决定了你的数据发送到服务端上会是什么格式。 + +``` +类型格式:type/subtype(;parameter)? +type 主类型,任意的字符串,如text,如果是*号代表所有; +subtype 子类型,任意的字符串,如html,如果是*号代表所有; +parameter 可选,一些参数,如Accept请求头的q参数, Content-Type的charset参数。 +``` + +常见的媒体格式类型如下: + +- text/html : HTML 格式 + +- text/plain :纯文本格式 + +- text/xml : XML 格式 + +- image/gif :gif 图片格式 + +- image/jpeg :jpg 图片格式 + +- image/png:png 图片格式 + + 以 application 开头的媒体格式类型: + +- application/xhtml+xml :XHTML 格式 + +- application/xml : XML 数据格式 + +- application/atom+xml :Atom XML 聚合格式 + +- application/json : JSON 数据格式 + +- application/pdf :pdf 格式 + +- application/msword : Word 文档格式 + +- application/octet-stream : 二进制流数据(如常见的文件下载) + +- application/x-www-form-urlencoded : form 表单数据被编码为 key/value 格式(通过=与&拼接)发送到服务器(表单默认的提交数据的格式)格式如: username=kuizuo&password=a12345678 + + 另外一种常见的媒体格式是上传文件之时使用的: + +- multipart/form-data : 需要在表单中进行文件上传时,就需要使用该格式 + +实际上遇到最多的也就是 text/html,text/plain,application/json,application/x-www-form-urlencoded 这几个。 + +> 参考资料 [Http 请求中 Content-Type](https://www.cnblogs.com/klb561/p/10090540.html) + +### 说说我那时候的情况 + +这是在我帮别人分析登录算法的时候,由于协议头中少添加了一个`Content-Type`,导致我发送的数据,服务端解析不了,收到了这样的响应 + +```json +{ + "code": 500001, + "message": "亲,我们的系统目前忙碌中或无法回应,请将此问题回报给我们的客服人员。 错误代碼(68523)", + "data": null +} +``` + +然而实际响应应该是这样的 + +```json +{ "code": 400020, "message": "密码错误", "data": "验证码错误" } +``` + +原因就是因为协议头没有添加`Content-Type: application/json;charset=UTF-8`所导致的。因为这个,坑了我近一个小时,还一直以为是数据错误,没想到仅仅只是少加了一些协议头导致的请求数据格式错误。 + +一个印象很深刻的教训,模拟 HTTP 请求的时候,一定不要吝啬补全协议头,不然坑的就是自己了。我已经给坑过两次了,所以在特意想借此记录一下,免得下次又是一番折腾。写个注意,醒目一点。 + +:::danger 发送的是 JSON 格式数据,切记一定要添加上协议头`Content-Type: application/json;charset=UTF-8` + +::: diff --git "a/blog/develop/HTTP\350\257\267\346\261\202\351\205\215\347\275\256\345\256\242\346\210\267\347\253\257SSL\350\257\201\344\271\246.md" "b/blog/develop/HTTP\350\257\267\346\261\202\351\205\215\347\275\256\345\256\242\346\210\267\347\253\257SSL\350\257\201\344\271\246.md" new file mode 100644 index 0000000..1a24af8 --- /dev/null +++ "b/blog/develop/HTTP\350\257\267\346\261\202\351\205\215\347\275\256\345\256\242\346\210\267\347\253\257SSL\350\257\201\344\271\246.md" @@ -0,0 +1,92 @@ +--- +slug: http-config-client-ssl-certificate +title: HTTP请求配置客户端SSL证书 +date: 2022-02-17 +authors: kuizuo +tags: [http, ssl] +keywords: [http, ssl] +--- + +在学习安卓逆向的时候,遇到一个 APP,服务端检测请求的 SSL 证书,需要提交 SSL 证书上去才能正常发送请求。而在开启抓包和协议复现的时候,请求是能正常发出去,但是服务器会返回 400 错误。于是便有了这篇文章来记录下。 + + + +## 说明 + +由于是服务端效验客户端发送的证书,所以使用代理服务器(FD,Charles 等)抓包是会替换本地证书,当服务器效验客户端发送的证书与服务器内的证书不一致,那么就直接返回 400 错误,实际上请求还是能够发送出去,只是被服务器给拒绝了。俗称**双向认证** + +所以解决办法就是在请求的时候,将正确的证书也一同发送过去,这样服务端效验时就会将正常的响应结果返回给客户端,也就是**配置自定义证书**。 + +### 例子 + +APP 例子:隐约 + +具体如何拉取证书,就是安卓逆向相关的部分了,这里我也只提供证书文件,不提供 app。 + +贴上下载地址及密码 + +证书: https://img.kuizuo.cn/cert.p12 + +密码: `xinghekeji888.x` + +### 证书转化 + +[证书格式转换 (myssl.com)](https://myssl.com/cert_convert.html) + +[SSL 在线工具-在线证书格式转换-证书在线合并-p12、pfx、jks 证书在线合成解析-SSLeye 官网](https://www.ssleye.com/ssltool/jks_pkcs12.html) + +也可使用 OpenSSL 工具来进行转化证书 + +## HTTP 发送请求 + +### node 的 axios + +```javascript +const axios = require('axios').default +const fs = require('fs') +const https = require('https') + +axios + .post( + `https://app.yyueapp.com/api/passLogin`, + { + mobile: '15212345678', + password: 'a123456', + }, + { + httpsAgent: new https.Agent({ + cert: fs.readFileSync('./cert.cer'), + key: fs.readFileSync('./cert.key'), + // pfx: fs.readFileSync('./cert.p12'), + // passphrase: 'xinghekeji888.x, + }), + }, + ) + .then((res) => { + console.log(res.data) + }) + .catch((error) => { + console.log(error.response.data) + }) +``` + +如果没有配置 httpsAgent,也就是没有配置证书,那么返回 400 错误 `400 No required SSL certificate was sent`。 + +配置成功将会得到正确的响应结果 + +```javascript +{ code: 998, msg: '系统维护中...', data: null } +``` + +### python 的 requests + +requests 不支持 p12 格式的证书,所以需要使用其他的证书格式,如下 + +```python +import requests + +r = requests.post('https://app.yyueapp.com/api/passLogin', data={ + 'mobile': '15212345678', 'password': 'a123456'}, cert=('./cert.cer', './cert.key')) +print(r.status_code) +print(r.text) +``` diff --git "a/blog/develop/JS\344\273\243\347\240\201\344\271\213\346\267\267\346\267\206.md" "b/blog/develop/JS\344\273\243\347\240\201\344\271\213\346\267\267\346\267\206.md" new file mode 100644 index 0000000..7e71717 --- /dev/null +++ "b/blog/develop/JS\344\273\243\347\240\201\344\271\213\346\267\267\346\267\206.md" @@ -0,0 +1,1240 @@ +--- +slug: js-code-obfuscator +title: JS代码之混淆 +date: 2021-12-21 +authors: kuizuo +tags: [javascript, ast, reverse, project] +keywords: [javascript, ast, reverse, project] +--- + + + +[JS deobfuscator](http://js-deobfuscator.kuizuo.cn/) + +## 什么是 AST + +抽象语法树(Abstract Syntax Tree),简称 AST,初识 AST 是在一门网页逆向的课程,该课程讲述了 js 代码中混淆与还原的对抗,而所使用的技术便是 AST,通过 AST 能很轻松的将 js 源代码混淆成难以辨别的代码。同样的,也可以通过 AST 将其混淆的代码 还原成执行逻辑相对正常的代码。 + +例如下面的代码(目的是当天时间格式化) + +```javascript +Date.prototype.format = function (formatStr) { + var str = formatStr + var Week = ['日', '一', '二', '三', '四', '五', '六'] + str = str.replace(/yyyy|YYYY/, this.getFullYear()) + str = str.replace(/MM/, (this.getMonth() + 1).toString().padStart(2, '0')) + str = str.replace(/dd|DD/, this.getDate().toString().padStart(2, '0')) + return str +} +console.log(new Date().format('yyyy-MM-dd')) +``` + +通过 AST 混淆的结果为 + +```javascript +const OOOOOO = [ + 'eXl5eS1NTS1kZA==', + 'RGF0ZQ==', + 'cHJvdG90eXBl', + 'Zm9ybWF0', + '5pel', + '5LiA', + '5LqM', + '5LiJ', + '5Zub', + '5LqU', + '5YWt', + 'cmVwbGFjZQ==', + 'Z2V0RnVsbFllYXI=', + 'Z2V0TW9udGg=', + 'dG9TdHJpbmc=', + 'cGFkU3RhcnQ=', + 'MA==', + 'Z2V0RGF0ZQ==', + 'bG9n', +] + +;(function (OOOOOO, OOOOO0) { + var OOOOOo = function (OOOOO0) { + while (--OOOOO0) { + OOOOOO.push(OOOOOO.shift()) + } + } + + OOOOOo(++OOOOO0) +})(OOOOOO, 115918 ^ 115930) + +window[atob(OOOOOO[694578 ^ 694578])][atob(OOOOOO[873625 ^ 873624])][ + atob(OOOOOO[219685 ^ 219687]) +] = function (OOOOO0) { + function OOOO00(OOOOOO, OOOOO0) { + return OOOOOO + OOOOO0 + } + + var OOOOOo = OOOOO0 + var OOOO0O = [ + atob(OOOOOO[945965 ^ 945966]), + atob(OOOOOO[298561 ^ 298565]), + atob(OOOOOO[535455 ^ 535450]), + atob(OOOOOO[193006 ^ 193000]), + atob(OOOOOO[577975 ^ 577968]), + atob(OOOOOO[428905 ^ 428897]), + atob(OOOOOO[629582 ^ 629575]), + ] + OOOOOo = OOOOOo[atob(OOOOOO[607437 ^ 607431])](/yyyy|YYYY/, this[atob(OOOOOO[799010 ^ 799017])]()) + OOOOOo = OOOOOo[atob(OOOOOO[518363 ^ 518353])]( + /MM/, + OOOO00(this[atob(OOOOOO[862531 ^ 862543])](), 671347 ^ 671346) + [atob(OOOOOO[822457 ^ 822452])]() + [atob(OOOOOO[974597 ^ 974603])](741860 ^ 741862, atob(OOOOOO[544174 ^ 544161])), + ) + OOOOOo = OOOOOo[atob(OOOOOO[406915 ^ 406921])]( + /dd|DD/, + this[atob(OOOOOO[596004 ^ 596020])]() + [atob(OOOOOO[705321 ^ 705316])]() + [atob(OOOOOO[419232 ^ 419246])](318456 ^ 318458, atob(OOOOOO[662337 ^ 662350])), + ) + return OOOOOo +} + +console[atob(OOOOOO[490983 ^ 490998])]( + new window[atob(OOOOOO[116866 ^ 116866])]()[atob(OOOOOO[386287 ^ 386285])]( + atob(OOOOOO[530189 ^ 530207]), + ), +) +``` + +将上述代码复制到浏览器控制台内执行,将会输出当天的年月日。 + +### AST 有什么用 + +除了上述的混淆代码,很多文本编辑器中也会使用到,例如: + +- 编辑器的错误提示、代码格式化、代码高亮、代码自动补全; +- `elint`、`pretiier` 对代码错误或风格的检查; +- `webpack` 通过 `babel` 转译 `javascript` 语法; + +不过本篇并非介绍 AST 的基本概念,看本篇你只需要知道**如何通过 babel 编译器生成 AST 并完成上述的混淆操作**即可。 + +### 有必要学 AST 吗 + +如果作为 JS 开发者并且想要深入了解 V8 编译,那么 AST 基本是必修课之一,像 Vue,React 主流的前端框架都使用到 AST 对代码进行编译,在 ast 学习中定能让你对 JS 语法有一个更深入的了解。 + +### AST 误区 + +AST 本质上是静态分析,静态分析是在不需要执行代码的前提下对代码进行分析的处理过程,与动态分析不同,静态分析的目的是多种多样的, 它可用于语法检查,编译,代码高亮,代码转换,优化,压缩等等场景。即便你的程序也许在运行时报错,但都不会影响 AST 解析(除非语法错误),在 js 逆向中,通过静态分析还原出相对容易看的出的代码有对于代码分析,而对于一些需要知道某一变量执行后的结果静态分析是做不到的。 + +## 环境安装 + +首先需要 Node 环境,这就不介绍了,其次工具 Babel 编译器可通过 npm 安装 + +```bash +npm i @babel/core -S-D +``` + +安装代码提示 + +```bash +npm i @types/node @types/babel__traverse @types/babel__generator -D +``` + +新建 js 文件,导入相关模块(也可使用 ES module 导入),大致代码如下 + +```javascript +const fs = require('fs') +const parser = require('@babel/parser') +const traverse = require('@babel/traverse').default +const t = require('@babel/types') +const generator = require('@babel/generator').default + +let jscode = fs.readFileSync(__dirname + "/demo.js", { + encoding: "utf-8" +}) + +// 解析为AST +let ast = parser.parse(jscode) + +// 转化特征代码 +traverse(ast, { + ... +}) + +// 生成转化后的代码 +let code = generator(ast).code +``` + +babel 的编译过程主要有三个阶段 + +1. 解析(Parse): 将输入字符流解析为 AST 抽象语法树 +2. 转化(Transform): 对抽象语法树进一步转化 +3. 生成(Generate): 根据转化后的语法树生成目标代码 + +## AST 的 API + +在进行编译前,首先需要了解 Babel 的一些相关 API,这边所选择的是 babel/parser 库作为解析,还有一个在线 ast 解析网站[AST explorer](https://astexplorer.net/) 能帮助我们有效的了解 AST 中的树结构。 + +同时 Babel 手册(中文版) [babel-handbook](https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md)强烈建议反复阅读,官方的例子远比我所描述来的详细。 + +![image-20211212151620278](https://img.kuizuo.cn/image-20211212151620278.png) + +### 例子 + +这边就举一个非常简单的例子,混淆变量名(或说标识符混淆)感受一下。引用网站代码例子 + +```javascript +/** + * Paste or drop some JavaScript here and explore + * the syntax tree created by chosen parser. + * You can use all the cool new features from ES6 + * and even more. Enjoy! + */ + +let tips = [ + "Click on any AST node with a '+' to expand it", + + 'Hovering over a node highlights the \ + corresponding location in the source code', + + 'Shift click on an AST node to expand the whole subtree', +] + +function printTips() { + tips.forEach((tip, i) => console.log(`Tip ${i}:` + tip)) +} +``` + +比如说,我要将这个 tips 标识符更改为`_0xabcdef` ,那么肯定是需要找到这个要 tips,在 Babel 中要找到这个则可以通过遍历特部位(如函数表达式,变量声明等等)。 + +鼠标点击这个 tips 查看 tips 变量在树节点中的节点。 + +![image-20211212170832228](https://img.kuizuo.cn/image-20211212170832228.png) + +这边可以看到有两个蓝色标记的节点,分别是`VariableDeclaration`和`VariabelDeclarator`,翻译过来便是变量声明与变量说明符,很显然整个`let tips = [ ]` 是`VariableDeclaration`,而`tips`则是`VariabelDeclarator`。 + +所以要将`tips`更改为`_0xabcdef`就需要遍历`VariabelDeclarator`并判断属性`name`是否为`tips`,大致代码如下。**(后文代码将会省略模块引入、js 代码读取、解析与生成的代码)** + +```javascript +const fs = require('fs') +const parser = require('@babel/parser') +const traverse = require('@babel/traverse').default +const t = require('@babel/types') +const generator = require('@babel/generator').default + +let jscode = fs.readFileSync(__dirname + '/demo.js', { encoding: 'utf-8' }) +let ast = parser.parse(jscode) + +traverse(ast, { + VariableDeclarator(path) { + let name = path.node.id.name + if (name === 'tips') { + let binding = path.scope.getOwnBinding(name) + binding.scope.rename(name, '_0xabcdef') + } + }, +}) +let code = generator(ast).code +``` + +生成的代码如下,成功的将`tips`更改为`_0xabcdef`,并且是`tips`的所有作用域(printTips 函数下)都成功替换了。 + +```javascript +/** + * Paste or drop some JavaScript here and explore + * the syntax tree created by chosen parser. + * You can use all the cool new features from ES6 + * and even more. Enjoy! + */ +let _0xabcdef = ["Click on any AST node with a '+' to expand it", "Hovering over a node highlights the \ + corresponding location in the source code", "Shift click on an AST node to +expand the whole subtree"]; + +function printTips() { + _0xabcdef.forEach((tip, i) => console.log(`Tip ${i}:` + tip)); +} +``` + +简单描述下上述代码的过程 + +1、遍历所有`VariableDeclarator`节点,也就是`tips`变量说明符(标识符) + +2、获取当前遍历到的标识符的 name,也就是`path.node.id.name`,在树节点是对应的也是`id.name` + +3、判断 name 是否等于 tips,是的话,通过`path.scope.getOwnBinding(name)`,获取当前标识符(tips)的作用域,scope 的意思就是作用域,如果只是赋值操作的话如`path.node.id.name = '_0xabcdef'`,那只修改的`let tips =` 的 tips,而后面的对 tips 进行`forEach`操作的 tips 并不会更改,所以这里才需要使用`binding`来获取 tips 的作用域,并调用提供好的`rename`方法来进行更改。 + +4、调用`binding.scope.rename(name, '_0xabcdef')`,将旧名字 name(tips)更改为\_0xabcdef,就此整个遍历就结束,此时的 ast 已经发生了变化,所以只需要根据遍历过的 ast 生成代码便可得到修改后的代码。 + +如果在仔细观察的话,其实`Identifier`(标识符)也是蓝色表示的,说明`Identifier`也同样可以遍历,甚至比上面的效果更好(后续替换所有的标识符也是遍历这个) + +```javascript {3-4} +traverse(ast, { + Identifier(path) { + let name = path.node.name + console.log(name) + if (name === 'tips') { + let binding = path.scope.getOwnBinding(name) + binding.scope.rename(name, '_0xabcdef') + } + }, +}) +``` + +并尝试输出所有的标识符,输出的 name 结果为 + +``` +tips +printTips +_0xabcdef +forEach +tip +i +console +log +i +tip +``` + +这个例子也许有点啰嗦,但我认为是有必要的,同时想说的是某种混淆(还原)的实现往往可以有好几种方法遍历,会懂得融会贯通,AST 混淆与还原才能精通。 + +### parser 与 generator + +前者用于将 js 代码解析成 AST,后者则是将 AST 转为 js 代码,两者的具体参数可通过 babel 手册查看,这就不做过多介绍了。 + +[babel-handbook #babel-parser](https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md#babel-parser) + +[babel-handbook #babel-generator](https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md#babel-generator) + +### traverse 与 visitor + +整个 ast 混淆还原最关键的操作就是遍历,而 visitor 则是根据特定标识(函数声明,变量订阅)来进行遍历各个节点,而非无意义的全部遍历。 + +traverse 一共有两个参数,第一个就是 ast,第二个是 visitor,而 visitor 本质是一个对象如下(分别有 JavaScript 和 TypeScript 版本,区别就是在于这样定义的 visitor 是否有代码提示) + +import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; + + + + +```javascript +const visitor = { + FunctionDeclaration(path) { + console.log(path.node.id.name) // 输出函数名 + }, +} +``` + + + + +```tsx +let visitor: Visitor = { + FunctionDeclaration(path) { + console.log(path.node.id.name) // 输出函数名 + }, +} +``` + + + + +一般来说,都是直接写到写到 traverse 内。个人推荐这种写法,因为能有 js 的代码提示,如果是 TypeScript 效果也一样。 + +```javascript +traverse(ast, { + FunctionDeclaration(path) { + console.log(path.node.id.name) // 输出函数名 + }, +}) +``` + +如果我想遍历函数声明与二项式表达式的话,还可以这么写 + +```javascript +traverse(ast, { + 'FunctionDeclaration|BinaryExpression'(path) { + let node = path.node + if (t.isFunctionDeclaration(node)) { + console.log(node.id.name) // 输出函数名 printTips + } else if (t.isBinaryExpression(node)) { + console.log(node.operator) // 输出操作符 + + } + }, +}) +``` + +不过要遍历不同类型的代码,那么对应的 node 属性肯定大不相同,其中这里使用了 t(也就是`@babel/types`库)来进行判断 node 节点是否为该属性,来进行不同的操作,后文会提到 types。 + +上述操作将会输出 `printTips` 与 `+` 因为 printTips 函数中代码有 `Tip ${i}: + tip` ,这就是一个二项式表达式。 + +此外 visitor 中的属性中,还对应两个生命周期函数 enter(进入节点)和 exit(退出节点),可以在这两个周期内进行不同的处理操作,演示代码如下。 + +```javascript +traverse(ast, { + FunctionDeclaration: { + enter(path) { + console.log('进入函数声明') + }, + exit(path) { + console.log('退出函数声明') + }, + }, +}) +``` + +其中 enter 与 exit 还可以是一个数组(当然基本没怎么会用到),比如 + +```javascript +traverse(ast, { + FunctionDeclaration: { + enter: [ + path => { + console.log('1') + }, + path => { + console.log('2') + }, + ], + }, +}) +``` + +path 对象下还有一种方法,针对当前 path 进行遍历 `path.traverse`,比如下面代码中,我遍历到了 printTips,我想输出函数内的箭头函数中的参数,那么就可以使用这种遍历。 + +```javascript +function printTips() { + tips.forEach((tip, i) => console.log(`Tip ${i}:` + tip)) +} +``` + +此时的 path.traverse 的第一个参数便不是 ast 对象了,而是一个 visitor 对象 + +```javascript +traverse(ast, { + FunctionDeclaration(path) { + path.traverse({ + ArrowFunctionExpression(path) { + console.log(path.node.params) + }, + }) + }, +}) +``` + +输出的结果如下 + +``` +[ + Node { + type: 'Identifier', + start: 40, + end: 43, + loc: SourceLocation { + start: [Position], + end: [Position], + filename: undefined, + identifierName: 'tip' + }, + name: 'tip' + }, + Node { + type: 'Identifier', + start: 45, + end: 46, + loc: SourceLocation { + start: [Position], + end: [Position], + filename: undefined, + identifierName: 'i' + }, + name: 'i' + } +] +``` + +### types + +该库主要的作用是判断节点类型与生成新的节点。判断节点类型上面已经演示过了,比如判断 node 节点是否是为标识符`t.isIdentifier(path.node)`,等同于`path.node.type === "Identifier"` + +判断节点类型是很重要的一个环节,有时候混淆需要针对很多节点进行操作,但并不是每个节点都有相同的属性,判断节点才不会导致获取到的节点属性出错,甚至可以写下面的代码(将输出所有函数声明与箭头函数的参数)。 + +``` +traverse(ast, { + enter(path) { + t.isFunctionDeclaration(path.node) && console.log(path.node.params) + t.isArrowFunctionExpression(path.node) && console.log(path.node.params) + } +}) +``` + +types 的主要用途还是构造节点,或者说写一个 Builders(构建器),例如我要生成 `let a = 100` 这样的变量声明原始代码,通过 types 能轻松帮我们生成。 + +不过先别急着敲代码,把`let a = 100`代码进行 ast 解析,看看每个代码的节点对应的 type 都是什么,这样才有助于生成该代码。 + +![image-20211216131627955](https://img.kuizuo.cn/image-20211216131627955.png) + +body 内的第一个节点便是我们整条的代码,输入`t.variableDeclaration()`,鼠标悬停在 variableDeclaration 上,或者按 Ctrl 跳转只.d.ts 类型声明文件 查看该方法所需几个参数 + +```ts +declare function variableDeclaration( + kind: 'var' | 'let' | 'const', + declarations: Array, +): VariableDeclaration +``` + +可以看到第一个参数就是关键字,而第二个则一个数组,其中节点为`VariableDeclarator`,关于`variableDeclaration`与 `VariableDeclarator` 在前面已经提及过一次了,就不在赘述了。由于我们这里只是声明一个变量 a,所有数组成员只给一个便可,如果要生成 b,c 这些变量,就传入对应的`VariableDeclarator`即可 + +这时候在查看下 VariableDeclarator 方法参数 + +```ts +declare function variableDeclarator(id: LVal, init?: Expression | null): VariableDeclarator +``` + +第一个参数 id 很显然就是标识符了,不过这里的 id 不能简简单单传入一个字符串 a,而需要通过`t.identifier('a')`生成该节点,在上图中 id 就是对应`Identifier`节点。然后就是第二个参数了,一个表达式,其中这个`Expression`是 ts 中的联合类型(Union Types),可以看到有很多表达式 + +```ts +declare type Expression = + | ArrayExpression + | AssignmentExpression + | BinaryExpression + | CallExpression + | ConditionalExpression + | FunctionExpression + | Identifier + | StringLiteral + | NumericLiteral + | NullLiteral + | BooleanLiteral + | RegExpLiteral + | LogicalExpression + | MemberExpression + | NewExpression + | ObjectExpression + | SequenceExpression + | ParenthesizedExpression + | ThisExpression + | UnaryExpression + | UpdateExpression + | ArrowFunctionExpression + | ClassExpression + | MetaProperty + | Super + | TaggedTemplateExpression + | TemplateLiteral + | YieldExpression + | AwaitExpression + | Import + | BigIntLiteral + | OptionalMemberExpression + | OptionalCallExpression + | TypeCastExpression + | JSXElement + | JSXFragment + | BindExpression + | DoExpression + | RecordExpression + | TupleExpression + | DecimalLiteral + | ModuleExpression + | TopicReference + | PipelineTopicExpression + | PipelineBareFunction + | PipelinePrimaryTopicReference + | TSAsExpression + | TSTypeAssertion + | TSNonNullExpression +``` + +其中我们所要赋值的数值 100,对应的节点类型`NumericLiteral`也在其中。在查看 numericLiteral 中的参数,就只给一个数值,那么便传入 100。 + +``` +declare function numericLiteral(value: number): NumericLiteral; +``` + +最后整个代码如下,将 t.variableDeclaration 结果赋值为一个变量`var_a`,这里的 var_a 便是一个 ast 对象,通过 generator(var_a).code 就可以获取到该 ast 的代码,也就是 `let a = 100;`,默认还会帮你添加分号 + +```javascript +let var_a = t.variableDeclaration('let', [ + t.variableDeclarator(t.identifier('a'), t.numericLiteral(100)), +]) + +let code = generator(var_a).code +// let a = 100; +``` + +这边再列举一个生成函数声明代码的例子(不做解读),要生成的代码如下 + +```javascript +function b(x, y) { + return x + y +} +``` + +types 操作 + +```javascript +let param_x = t.identifier('x') +let param_y = t.identifier('y') +let func_b = t.functionDeclaration( + t.identifier('b'), + [param_x, param_y], + t.blockStatement([t.returnStatement(t.binaryExpression('+', param_x, param_y))]), +) + +let code = generator(func_b).code +``` + +大致步骤可以总结成一下几点 + +1、将要生成的 js 代码进行 ast Explorer 查看树结构,理清所要构造的代码节点(很重要) + +2、找到最顶层的结果,如 variableDeclaration,查看该代码所对应的参数 + +3、进一步的分析内层节点结构,构造出最终的原始代码。 + +types 还有一个方法`valueToNode`,先看演示 + +```javascript +let arr_c = t.valueToNode([1, 2, 3, 4, 5]) +console.log(arr_c) + +{ + type: 'ArrayExpression', + elements: [ + { type: 'NumericLiteral', value: 1 }, + { type: 'NumericLiteral', value: 2 }, + { type: 'NumericLiteral', value: 3 }, + { type: 'NumericLiteral', value: 4 }, + { type: 'NumericLiteral', value: 5 } + ] +} +``` + +如果使用`numericLiteral`来生成这些字面量的话那要写的话代码可能就要像下面这样 + +```javascript +let arr_c = t.arrayExpression([ + t.numericLiteral(1), + t.numericLiteral(2), + t.numericLiteral(3), + t.numericLiteral(4), + t.numericLiteral(5), +]) +``` + +而`valueToNode`能很方便地生成各种基本类型,甚至是一些对象类型(RegExp,Object 等)。不过像函数这种就不行。 + +```javascript +t.valueToNode(function b(x, y) { + return x + y +}) +// throw new Error("don't know how to turn this value into a node"); +``` + +写到着,其实不难发现,每个 node 节点其实就是一个 json 对象,而 types 只是将其封装好方法,供使用者调用,像下面这样方式定义 arr_c,同样也能生成数组 [1, 2, 3, 4, 5] + +```javascript +let arr_c = { + type: 'ArrayExpression', + elements: [ + { type: 'NumericLiteral', value: 1 }, + { type: 'NumericLiteral', value: 2 }, + { type: 'NumericLiteral', value: 3 }, + { type: 'NumericLiteral', value: 4 }, + { type: 'NumericLiteral', value: 5 }, + ], +} +let code = generator(arr_c).code +``` + +至于生成其他的语句,原理与上述一致,篇幅有限不在做其他例子演示了,Babel 中的 API 很多,最主要的是懂得善用手册与代码提示,没有什么生成不了的语句,更没有还原不了的代码。 + +### Path + +上述讲了基本的库操作,不难发现,使用到最多的还是 traverse,并且都会传入一个参数 path,并且`path.node`使用到的频率很多,能理解请两个的区别(Node 与 NodePath),基本上你想遍历到的地方就没有遍历不到的。 + +先说说 path 能干嘛,能停止遍历当前节点 (`path.stop`),能跳过当前节点(`path.skip`),还可以获取父级 path(`path.parentPath` ),替换当前节点(`path.replaceWith`),移除当前节点(`path.remove`)等等。 + +#### 获取 Node 节点属性 + +**`path.node`** 也就是当前节点所在的 Node 对象,比如`loc`、`id`、`init`,`param`、`name`等,这些都是在 node 对象下都是能直接获取到的。 + +不过获取到的是 node 对象,就无法使用 path 对象的方法了,如果要获取该属性的 path,就可以使用`path.get('name')`,获取到的就是 path 对象。不过对于一些特定的属性(name,operator)获取 path 对象就多此一举了。 + +一共有两种类型 `Node` 与 `NodePath`,记住有`Path`则是`path`,如`path`就属于`NodePath`,而`path.node` 属于`Node`。 + +![image-20211213021420326](https://img.kuizuo.cn/image-20211213021420326.png) + +#### 将节点转为代码 + +有时候遍历到一系列的代码,想输出一下原始代码,那么有以下两种方式。 + +```javascript +traverse(ast, { + FunctionDeclaration(path) { + console.log(generator(path.node).code) + console.log(path.toString()) + }, +}) +``` + +#### 替换节点属性 + +与获取节点属性相同,比如我需要修改函数的第一个参数,那么我只要获取到第一个参数,并且将值赋值为我想修改值(node 对象)便可。 + +```javascript +traverse(ast, { + FunctionDeclaration(path) { + path.node.params[0] = t.identifier('x') + }, +}) +``` + +#### 替换整个节点 + +替换的相关方法有 + +`replaceWith` 一对一替换当前节点,且严格替换。 + +```javascript +path.replaceWith(t.valueToNode('kuizuo')) +``` + +`replaceWithMultiple` 则是一对多,将多个节点替换到一个节点上。 + +```javascript +traverse(ast, { + ReturnStatement(path) { + path.replaceWithMultiple([ + t.expressionStatement( + t.callExpression(t.memberExpression(t.identifier('console'), t.identifier('log')), [ + t.stringLiteral('kuizuo'), + ]), + ), + t.returnStatement(), + ]) + path.stop() + }, +}) +``` + +要注意的是,替换节点要非常谨慎,就比如上述代码,如果我遍历 return 语句,同时我又替换成了 return 语句,替换后的节点同样是可以进入到遍历里,如果不进行停止,将会造成死循环,所以这里才使用了`path.stop`完全停止当前遍历,直到下一条 return 语句。 + +`path.skip()`跳过遍历当前路径的子路径。`path.stop()`完全停止当前遍历 + +`relaceInline` 接收一个参数,如果不为数组相当于`replaceWith`,如果是数组相当于`replaceWithMultiple` + +`replaceWithSoureString` 该方式将字符串源码与节点进行替换,例如 + +```javascript +// 要替换的函数 +function add(a, b) { + return a + b +} + +traverse(ast, { + FunctionDeclaration(path) { + path.replaceWithSourceString(`function mult(a, b){ + return a * b + }`) + path.stop() + }, +}) + +// 替换后的结果 +// (function mult(a, b) { +// return a * b; +// }); +``` + +#### 删除节点 + +```javascript +traverse(ast, { + EmptyStatement(path) { + path.remove() + }, +}) +``` + +`EmptyStatement`指空语句,也就是多余的分号。 + +#### 插入节点 + +`insertBefore`与`insertAfter`分别在当前节点前后插入语句 + +```javascript +traverse(ast, { + ReturnStatement(path) { + path.insertBefore(t.expressionStatement(t.stringLiteral('before'))) + path.insertAfter(t.expressionStatement(t.stringLiteral('after'))) + }, +}) +``` + +#### 父级 path + +**`path.parent`** 表示父级的 node + +**`path.parentPath`** 表示父级的 path,也就是 NodePath + +`path.parentPath.node` === `path.parent` 两者效果一样,都是获取 Node 对象。 + +此外还有一些方法也可以获取父级 Path + +**`path.findParent`** 向上遍历每一个父级 Path 并根据条件返回,与数组 find 方式类型。 + +```javascript +traverse(ast, { + BinaryExpression(path) { + let parent = path.findParent(p => p.isFunctionDeclaration()) + console.log(parent.toString()) + }, +}) +``` + +**`path.find`** 与 findParent 方式类似,不过 find 方法不包括当前节点,而 findParent 不包括。 + +**`path.getFunctionParent`** 向上查找与当前节点最接近的父函数,返回的是 Path 对象。 + +**`path.getStatementParent`** 遍历语法树,直到找到语句节点(带有 Statement),如 return 语句(ReturnStatement),if 语句(IfStatement),块级语句(BlockStatement) + +#### 同级 path + +path 有一个属性 container,表示当前节点所处于的那个节点下,共有那些同级节点,而 listKey 表示容器名。key 表示索引或是是容器对象的属性名 + +```javascript +traverse(ast, { + ReturnStatement(path) { + console.log(path.key) + console.log(path.listKey) + console.log(path.container) + }, +}) +``` + +```javascript +// 输出结果 +0 +body +[ + Node { + type: 'ReturnStatement', + start: 24, + end: 36, + loc: SourceLocation { + start: [Position], + end: [Position], + filename: undefined, + identifierName: undefined + }, + argument: Node { + type: 'BinaryExpression', + start: 31, + end: 36, + loc: [SourceLocation], + left: [Node], + operator: '+', + right: [Node] + } + } +] +``` + +在 ast 树结构中框中所表示 + +![image-20211216200502122](https://img.kuizuo.cn/image-20211216200502122.png) + +也并不是说所有节点都有同级节点,也并不是所有的 container 都是一个数组,例如下面这个例子 + +```javascript +let obj = { + name: 'kuizuo', +} +``` + +```javascript + +init +undefined +Node { + type: 'VariableDeclarator', + start: 4, + end: 30, + loc: SourceLocation { + start: Position { line: 1, column: 4 }, + end: Position { line: 3, column: 1 }, + filename: undefined, + identifierName: undefined + }, + id: Node { + type: 'Identifier', + start: 4, + end: 7, + loc: SourceLocation { + start: [Position], + end: [Position], + filename: undefined, + identifierName: 'obj' + }, + name: 'obj' + }, + init: Node { + type: 'ObjectExpression', + start: 10, + end: 30, + loc: SourceLocation { + start: [Position], + end: [Position], + filename: undefined, + identifierName: undefined + }, + properties: [ [Node] ] + } +} +``` + +对应 AST 树结构中所框选 + +![image-20211216201242257](https://img.kuizuo.cn/image-20211216201242257.png) + +也就是说该节点并没有同级节点 + +其中关于同级节点有以下几种方法。 + +`path.inList` 判断 container 属性是否为数组 + +`path.getSibling(index)` 获取当前节点所在容器中索引对应的同级节点,index 可通过 path.key 获取。 + +其中还有`unshiftContainer`与`pushContainer`,在容器前与后添加节点,与`Array.unshift`和`Array.push`方法类似,不过基本没怎么用过,便不做实例了。 + +### Scope + +**`path.scope`** 字面名意思为作用域,可以方便查找标识符的引用。如当前变量的哪里被调用了,标识符为参数还是变量。 + +演示代码 + +```javascript +function test() { + let obj = { + name: 'kuizuo', + } + return obj +} +``` + +#### 获取标识符代码块 + +`scope.block` 返回 Node 对象,使用方法分为两种情况,变量与函数。 + +```javascript +traverse(ast, { + ObjectExpression(path) { + let block = path.scope.block + console.log(generator(block).code) + }, +}) + +// function test() { +// let obj = { +// name: 'kuizuo' +// }; +// return obj; +// } +``` + +返回的是整个函数体代码 + +```javascript +traverse(ast, { + ObjectExpression(path) { + let block = path.scope.block + console.log(generator(block).code) + }, +}) + +// function test() { +// let obj = { +// name: 'kuizuo' +// }; +// return obj; +// } +``` + +由于`scope.block`返回的是 Node 对象,将就无法使用 path.toString()转为原始代码了。 + +#### binding + +**`scope.getBinding()`** 接收一个参数,可用于获取标识符的绑定,这里的 binding 可能会有些抽象,在一开始的例子中初次接触到 + +```javascript +traverse(ast, { + VariableDeclarator(path) { + let name = path.node.id.name + if (name === 'tips') { + let binding = path.scope.getOwnBinding(name) + console.log(binding) + binding.scope.rename(name, '_0xabcdef') + } + }, +}) +``` + +其中这里的 binding 是属性相对较多,下面会一一介绍 + +```javascript +Binding { + identifier: Node {type: 'Identifier', name: 'tips'}, + scope: Scope { + path: NodePath {...}, + kind: 'let', + constantViolations: [], + constant: true, + referencePaths: [ + NodePath {...} + ], + referenced: true, + references: 1, + hasDeoptedValue: false, + hasValue: false, + value: null +} +``` + +要注意的是,getBinding 中传的值必须是当前节点能够引用到的标识符,如果当前标识符不存在,那么返回 undefined。 + +identifier 是标识符 tips 的 Node 的对象,path 则是标识符 Path 对象,constant 为布尔值,表示当前标识符是否为常量,referenced 表示当前节点是否被引用。references 表示引用的次数。 + +binding 中的 scope 等同于 path 中的 scope,作用域范围相同。 + +**`scope.getOwnBinding()`** 获取当前节点下的绑定,不包含其他父级中定义的标识符,会包含子函数中定义的标识符绑定。 + +#### referencePaths 与 constantViolations + +假如标识符被引用,referencePaths 中会存放所有引用该标识的 path 对象数组。像下面这样 + +```javascript +referencePaths: [ + NodePath { + contexts: [], + state: [Object], + opts: [Object], + _traverseFlags: 0, + skipKeys: null, + parentPath: [NodePath], + container: [Node], + listKey: undefined, + key: 'object', + node: [Node], + type: 'Identifier', + parent: [Node], + hub: undefined, + data: null, + context: [TraversalContext], + scope: [Scope] + } + ], +``` + +而 constantViolations 则是存放所有修改标识符的 Path 对象。 + +#### 标识符重命名 + +这在一开始的例子中就简单介绍过了,使用的是 rename 方法,能将该标识符中所有引用的地方重命名,不过上面的例子只是重命名 tips,想要重命名所有标识符的话,就需要遍历 Identifier。不过重命名标识符不能都重命名为相同字符,有一个 api `path.scope.generateUidIdentifier` 用于生成唯一不重复标识符。 + +```javascript +traverse(ast, { + Identifier(path) { + path.scope.rename(path.node.name, path.scope.generateUidIdentifier('_0xabcdef').name) + }, +}) +``` + +最终生成的代码如下 + +```javascript +** + * Paste or drop some JavaScript here and explore + * the syntax tree created by chosen parser. + * You can use all the cool new features from ES6 + * and even more. Enjoy! + */ +let _0xabcdef11 = ["Click on any AST node with a '+' to expand it", "Hovering over a node highlights the \ + corresponding location in the source code", "Shift click on an AST node to expand the whole subtree"]; + +function _0xabcdef2() { + _0xabcdef11.forEach((_0xabcdef10, _0xabcdef9) => console.log(`Tip ${_0xabcdef9}:` + _0xabcdef10)); +} +``` + +`scope.hasBinding('a')` 查询是否有标识符 a 的绑定 + +`scope.getAllBindings()` 获取当前节点下所有绑定,返回一个对象,以标识符名作为属性名,值为 binding。 + +`scope.hasReference('a')` 查询当前节点是否有标识符 a 的引用。 + +当然大部分的 api 还需要自行翻阅文档,或通过代码提示与动态调试查看方法,举一反三,来达到所想要的目的。 + +## 混淆实战 + +关于混淆实战的代码都已贴到 Github[kuizuo/AST-obfuscator](https://github.com/kuizuo/AST-obfuscator),在`src/obfuscated`中便可看到完整的混淆程序。其中也包括一些实战还原的例子,大部分的写法都采用了 ES6 的类来写,方便编写理解。 + +大部分混淆的例子在这本书《反爬虫 AST 原理与还原混淆实战》中都有,例如常量混淆,数组混淆与乱序,标识符混淆等等就不细说了,上传的代码中有,不过书中有一些 es6 的代码是没提及到的。 + +### 模板字符串 + +与`StringLiteral`不同,模板字符串的 type 是`TemplateLiteral`,所以是遍历不到模板字符串的。下文将用代码来实现将模板字符串转为字符串拼接 + +演示代码 + +```javascript +let a = 'kuizuo' +;`${a}nb${12}3${'456'}` +``` + +分析 AST 树结构 + +![image-20211217161958075](https://img.kuizuo.cn/image-20211217161958075.png) + +不难观察出,parser 将其成两部分`expressions`与`quasis`。而所要转为的最终代码应该是`'' + a + 'nb' + 12 + '3' + '456'+ ''`,并且`quasis`成员个数始终比`expressions`多一位,所以只需要将`expressions`插入置`quasis`成员内,然后通过 binaryExpression 进行拼接即可。大致的思路有了,那么就开始用代码来进行拼接。 + +```javascript +traverse(ast, { + TemplateLiteral(path) { + let { expressions, quasis } = path.node + // 将expressions节点逐个插入到quasis节点上 + for (const i in expressions) { + let e = expressions[i] + quasis.splice(i * 2 + 1, 0, e) + } + let newExpressions = quasis + + // 循环新的表达式节点构造出二项式表达式 + let binary + for (let i = 0; i < newExpressions.length; i++) { + let left = binary + let right = newExpressions[i] + if (i === 0) { + left = t.valueToNode(right.value.raw) + binary = left + continue + } + + if (t.isTemplateElement(right)) { + // if (right.value.raw === '') continue + right = t.valueToNode(right.value.raw) + } + binary = t.binaryExpression('+', left, right) + } + path.replaceWith(binary) + }, +}) +``` + +最终输出 `"" + a + "nb" + 12 + "3" + "456" + ""` + +### 类声明 + +同样,类名与类方法名同样也是可以混淆的,演示代码如下 + +```javascript +class Test { + age = 20 + constructor(name) { + this.name = name + } + + run() { + return this.name + this.age + } +} + +let test = new Test('kuizuo') +console.log(test.run()) +``` + +复制上述代码,观察 AST 树结构(图就不放了) + +不难发现,其实就是 type `ClassDeclaration`、`ClassProperty`、`ClassMethod`,通过标识符混淆的方法`renameIdentifier`,将`Program|FunctionExpression|FunctionDeclaration`新增这两个 type 即可 + +```javascript +traverse(ast, { + 'Program|FunctionExpression|FunctionDeclaration|ClassDeclaration|ClassProperty|ClassMethod'( + path, + ) { + renameOwnBinding(path) + }, +}) +``` + +但混淆完的代码并没有把属性名与方法名给混淆到 + +```javascript +class OOOOO0 { + age = 399100 ^ 399080 + + constructor(OOOOO0) { + this[atob(OOOOOO[226019 ^ 226019])] = OOOOO0 + } + + run() { + return this[atob(OOOOOO[255772 ^ 255772])] + this[atob(OOOOOO[982314 ^ 982315])] + } +} +``` + +不过这样混淆肯定远远不够的,方法可是类中很重要的属性,同时类方法与属性还能这么编写(constructor 不行),然后将下面的代码通过混淆程序执行一遍就能成功混淆变量名。 + +```javascript +class Test { + ['age'] = 20 + constructor(name) { + this.name = name + } + + ['run']() { + return this.name + this.age + } +} +``` + +所以将`run()` 转为`[‘run’]()`便成为了关键。而实现起来也相对简单(与改变对象访问方式一样) + +```javascript +traverse(ast, { + 'ClassProperty|ClassMethod'(path) { + if (t.isIdentifier(path.node.key)) { + let name = path.node.key.name + if (name === 'constructor') return + path.node.key = t.stringLiteral(name) + } + path.node.computed = true + }, +}) +``` + +最终运行混淆程序,执行混淆后的代码,成功输出`kuizuo20` + +--- + +后续有时间再补充。。。 + +## 混淆心得 + +### 混淆前提 + +**不改变原有代码的执行过程与结果**,并不是随便混淆都行了,比如`let c = a + b` ,总不能混淆成 `let OO = Oo - oO`吧。其次要懂得利用 js 语法的特性来进行混淆,比如高阶函数,函数传参,jsfuck 等等。 + +### 混淆并非万能 + +混淆始终是混淆,只是将代码相对变得难以阅读,但不代表不可阅读。只要程序能运行,那么我就能调试,能调试还能有什么解决不了的(毕竟 bug 都是调试出来)。如果真想保全你的代码,那我的建议是编译成二进制文件,或采用远程调用的形式将执行后的结果返回。 + +### 代码执行效率 + +通常来说,混淆会使你的代码数量增大至 2,3 倍,与加密壳同理,但程序的执行速度也会稍慢下,当然只要不是特别 ex 的混淆,如将函数调用封装至,3,4 层的调用导致调用堆栈过大,那么这种执行效率基本可以忽略不计。 + +### 有混淆就有还原 + +既然混淆是通过 AST 来进行混淆的,那么还原也同样可以,不过还原就不可能还原出原始开发者所编写的,就如同一些打包工具打包后的代码,比如将 name 压缩成 n,age 压缩成 a,那么就无法推断出 n 为 name,a 为 age,而混淆也是同理,像代码`let OOOOOO = atob('a3VpenVv')`,能还原的也只能是`let OOOOOO = ‘kuizuo’`或者是将标识符重新命名`let _0x123456 = ‘kuizuo’`,相对好看些。大部分的还原工作都只是将代码变得好读一些,比如`atob('a3VpenVv')`就可以变为`‘kuizuo’`,这便是基本的还原之一,关于还原还会另出一篇文章来记录,就不在这多废笔舌了。 + +整个混淆的过程来看,无非就是多了门技能,对 js 有了更进一步的了解,略懂 js 编译过程中的语法分析,此外也感叹 Babel 提供如此强大的 api。同时也能尝试使用最新的 ECMAScript 语法特性,无需考虑兼容问题,babel 统统都能处理。就如同 babel 官网所说的: + +**现在就开始使用下一代 JavaScript 语法吧**。 diff --git "a/blog/develop/JS\344\273\243\347\240\201\344\271\213\350\277\230\345\216\237.md" "b/blog/develop/JS\344\273\243\347\240\201\344\271\213\350\277\230\345\216\237.md" new file mode 100644 index 0000000..7657677 --- /dev/null +++ "b/blog/develop/JS\344\273\243\347\240\201\344\271\213\350\277\230\345\216\237.md" @@ -0,0 +1,769 @@ +--- +slug: js-code-deobfuscator +title: JS代码之还原 +date: 2021-12-25 +authors: kuizuo +tags: [javascript, ast, reverse, project] +keywords: [javascript, ast, reverse, project] +--- + +基于 Babel 对 JS 代码进行混淆与还原操作的网站 [JS 代码混淆与还原 (kuizuo.cn)](http://deobfuscator.kuizuo.cn/) + +![js-de-obfuscator](https://github.com/kuizuo/js-deobfuscator/blob/main/images/1.png) + + + +## 还原前言 + +AST 能做为逆向分析的利器,可以将还原出来的代码替换原来的代码,以便更好的动态分析找出相关点。在还原时,并不是所有的代码都能还原成一眼就识破代码执行逻辑的,ast 也并非万能,如果你拥有强大的 js 逆向能力,有时候动态调试甚至比 AST 静态分析来的事半功倍。 + +### 还原不出最原始的代码 + +标识符是可以随便定义的,只要变量不冲突,我可以随意定义,那么就已经决定我们还原不出源代码的变量名,所以能还原的只有一些花指令,使其代码变好看,方便调试。 + +### 还原也不是万能的 + +混淆的方式有很多,与之对应还原的方式也有很多,上面那套混淆的还原可能只针对那一套混淆的代码,如果拿另一份混淆过的代码,然后执行这个还原程序的话,那程序多半有可能报错。所以绝对没有万能的还原代码,所有的还原程序,都需要针对不同的混淆手段来进行处理的。 + +**我只是将我所遇到的混淆手段整合到一套代码上,而非所有的混淆手段都能进行还原处理的。** + +**同时也别过于追求还原,因为还原很容易破坏原有代码,导致一些未知 bug。** + +## 例子 + +下文将会针对主流的一些混淆手段(至少是在我遇到的混淆中相对比较好还原的),并会附上对应代码供参考(不放置代码出处)。 + +接下来我将要演示一个混淆代码是如何还原的,这个例子是我第一次接触混淆的例子,也可以说是我玩的最溜的一次还原了,反正折腾了也有 4,5 来次。 + +贴上代码 git 地址 [js-de-obfuscator/example/deobfuscator/cx](https://github.com/kuizuo/js-de-obfuscator/blob/main/example/deobfuscator/cx/code.js) + +> 注:该 js 文件是通过工具[JavaScript Obfuscator Tool](https://www.obfuscator.io/)进行混淆处理的。 + +### 分析 AST + +首先一定一定要将混淆的代码解析成 AST 树结构,任何混淆的还原都是如此。首先简单随便看看代码,不难发现这些代码中都有`'\x6a\x4b\x71\x4b'`这样的十六进制编码字符,可以使用现成的工具,格式化便会限制编码前的结果,不过这边使用 ast 来进行操作 + +通过 AST 查看 node 节点,可以发现`value`正是我们想要的数据,但这里确显示的是`extra.raw`,实际上只需要遍历到相应的节点,然后 extra 属性给删除即可,同样的 Unicode 编码也是按上述方式显示。 + +![image-20211224202108279](https://img.kuizuo.cn/image-20211224202108279.png) + +具体遍历的代码如下 + +```javascript +// 将所有十六进制编码与Unicode编码转为正常字符 +hexUnicodeToString() { + traverse(this.ast, { + StringLiteral(path) { + var curNode = path.node; + delete curNode.extra; + }, + NumericLiteral(path) { + var curNode = path.node; + delete curNode.extra; + } + }) + } +``` + +然后将遍历后处理过的代码与 demo.js 替换一下,方便接下来的还原处理。不过处理完还是有大部分未知的字符串需要解密,当然也有一些没处理过的代码。 + +### 找解密函数 + +如果你尝试过静态分析该代码,会发现一些参数都通过\_0x3028 来调用,像这样 + +```javascript +_0x3028['nfkbEK'] +_0x3028('0x0', 'jKqK') +_0x3028('0x1', ')bls') +``` + +不过认真查看会发现像成员表达式`MemberExpression`语句`_0x3028["nfkbEK"]`,但在第三条语句却定义函数`_0x3028`。其实是 js 的特性,比方说下面的代码就可以给函数添加一个自定义属性 + +```javascript +let add = function (a, b) { + add['abc'] = 123 + return a + b +} + +console.log(add(1, 2)) +console.log(add['abc']) + +// 3 +// 123 +``` + +不过不影响,这里只是提一嘴,并不是代码的问题。而其中**`_0x3028`就是解密函数**,且遍历`_0x3028`调用表达式,且参数为两个的 CallExpression。 + +那么接下来就要着重查看前三个语句,因为这三条语句便是这套混淆的关键所在。 + +```javascript title="demo.js" {1,3-7} +var _0x34ba = ["JcOFw4ITY8KX", "EHrDoHNfwrDCosO6Rkw=",...] +(function(_0x2684bf, _0x5d23f1) { + // 这里只是定义了一个数组乱序的函数,但是调用是在后面 + var _0x20d0a1 = function(_0x17cf70) { + while (--_0x17cf70) { + _0x2684bf['push'](_0x2684bf['shift']()); + } + }; + var _0x1b4e1d = function() { + var _0x3dfe79 = { + 'data': { + 'key': 'cookie', + 'value': 'timeout' + }, + "setCookie": function (_0x41fad3, _0x155a1e, _0x2003ae, _0x48bb02) { + ... + }, + "removeCookie": function () { + return "dev"; + }, + "getCookie": function (_0x23cc41, _0x5ea286) { + _0x23cc41 = _0x23cc41 || function (_0x20a5ee) { + return _0x20a5ee; + }; + + // 在这里定义了一个花指令函数调用来调用 + var _0x267892 = function (_0x51e60d, _0x57f223) { + _0x51e60d(++_0x57f223); + }; + // 实际调用的地方 + _0x267892(_0x20d0a1, _0x5d23f1); + + return _0x1c1cc3 ? decodeURIComponent(_0x1c1cc3[1]) : undefined; + } + } + }; + }; + _0x1b4e1d(); +}(_0x34ba, 296)); +var _0x3028 = function (_0x2308a4, _0x573528) { + _0x2308a4 = _0x2308a4 - 0; + var _0x29a1e7 = _0x34ba[_0x2308a4]; + + // 省略百行代码... + + return _0x29a1e7; +}; +``` + +其中省略的代码没必要细读,因为后续都只将这三条语句写入到 node 内存中(eval),然后来调用。接下来分析每一个语句都是干嘛的。 + +#### 大数组 + +基本 99%的混淆**第一条语句都是一个大数组**,存放这所有加密过的字符串,而我们要做的就是找到所有加密过的字符串,将其还原。 + +#### 数组乱序 + +然后接着**第二条语句一般都是自调用函数**,将大数组与数组乱序数量作为参数,其中的作用是将数组进行乱序,也就是上面代码中加亮的地方,但这里只是定义了一个函数`_0x20d0a1`,而实际调用的地方 `_0x1b4e1d` 中`_0x3dfe79`.`getCookie`中调用的,上述代码中有注释。如果你是正常一步步分析还真不一定的分析的出来,这就是混淆恶心的地方。 + +不吹混淆了,总之只要知道第二条语句是用作数组乱序,而具体无论怎么混淆,我们都可以通过 eval 来调用一遍,详看后文代码。 + +#### 解密函数 + +第三条语句就是加密函数,实际上就是传入大数组的索引,然后返回数组对应的成员,只是这边将其封装成函数,相当于原本 `_0x34ba[0]` 变为`_0x3028("0x0", "jKqK")` 形式来获取原本的字符串(这里只是举例,实际还涉及到第二个参数)。 + +### 还原字符串 + +上面说了那么多,实际上具体混淆逻辑其实根本没必要去理解,像传入的第二个参数做了啥根本无需了解,因为我们最终的目的是将 `_0x3028("0x0", "jKqK")`转为原本字符串,然后替换的当前节点的。所有只需要遍历到`_0x3028("0x0", "jKqK")`,然后**执行一遍解密函数**得到解密后的结果,然后替换即可。所以如何执行解密函数便是重点了。 + +#### 将解密函数添加到内存中 + +首先要将三条语句运行一遍,js 中要在运行时运行字符串的代码,就可以使用 eval,但 eval 有作用域的问题,eval 运行的代码作用范围都是局部的,如果脱离当前作用域,eval 运行的代码就相当于无效了,所有可以使用`window.eval`或`global.eval`,将其写入置全局作用域下,由于这里是 node 环境,便用`global.eval`。 + +截取前三条语句,使用 eval 写入内存 + +```javascript +// 拿到解密函数所在节点 +let stringDecryptFuncAst = this.ast.program.body[2] +// 拿到解密函数的名字 也就是_0x3028 +let DecryptFuncName = stringDecryptFuncAst.declarations[0].id.name + +let newAst = parser.parse('') +newAst.program.body.push(this.ast.program.body[0]) +newAst.program.body.push(this.ast.program.body[1]) +newAst.program.body.push(stringDecryptFuncAst) +// 把这三部分的代码转为字符串,由于存在格式化检测,需要指定选项,来压缩代码 +let stringDecryptFunc = generator(newAst, { compact: true }).code +// 将字符串形式的代码执行,这样就可以在 nodejs 中运行解密函数了 +global.eval(stringDecryptFunc) +``` + +#### 调用解密函数 + +这时候,就可以使用`_0x3028("0x0", "jKqK")` 来输出解密后的结果,不过要一个个手动输入还是太麻烦了,完全可以找到`_0x3028`调用的所有地方,然后判断是否为调用表达式 CallExpression,然后使用`eval('_0x3028("0x0", "jKqK")')` 获取解密结果。这边就举一个遍历的例子。 + +```javascript +traverse(this.ast, { + VariableDeclarator(path) { + // 当变量名与解密函数名相同 + if (path.node.id.name == DecryptFuncName) { + let binding = path.scope.getBinding(DecryptFuncName) + // 通过referencePaths可以获取所有引用的地方 + binding && + binding.referencePaths.map(p => { + // 判断父节点是调用表达式,且参数为两个 + if (p.parentPath.isCallExpression()) { + // 输出参数与解密后的结果 + let args = p.parentPath.node.arguments.map(a => a.value).join(' ') + let str = eval(p.parentPath.toString()) + console.log(args, str) + p.parentPath.replaceWith(t.stringLiteral(str)) + } + }) + } + }, +}) +``` + +在混淆的时候就提及到 binding 可以获取当前变量的作用域,而`binding.referencePaths`就可以获取到所有调用的地方,那么只需要判断是否为调用表达式,且参数是两个的情况下,然后通过 eval 执行一遍整个节点,也就是`eval('_0x3028("0x0", "jKqK")')`,然后通过 replaceWith,替换节点即可。传入的参数与加密后的结果大致展示如下,可自行运行一遍程序中`decStringArr()` + +``` +0x0 jKqK PdAlB +0x1 )bls jtvLV +0x2 M10H SjQMk +0x3 2Q@E length +0x4 [YLR length +0x5 QvlS charCodeAt +0x6 YvHw IrwYd +0x7 iLkl ClOby +0x8 DSlT console +... +``` + +#### 两者代码对比 + +原先代码与处理后的代码对比(部分) + +```javascript +var _0x505b30 = (function () { + if (_0x3028('0x0', 'jKqK') !== _0x3028('0x1', ')bls')) { + var _0x104ede = !![] + + return function (_0x3d32a2, _0x35fd15) { + if ('bKNqX' === _0x3028('0x2', 'M10H')) { + var _0x46992c, + _0x1efd4e = 0, + _0x5cae2b = d(f) + + if (0 === _0xb2c58f[_0x3028('0x3', '2Q@E')]) return _0x1efd4e + + for ( + _0x46992c = 0; + _0x46992c < _0xb2c58f[_0x3028('0x4', '[YLR')]; + _0x46992c++ + ) + (_0x1efd4e = + (_0x1efd4e << (_0x5cae2b ? 5 : 16)) - + _0x1efd4e + + _0xb2c58f[_0x3028('0x5', 'QvlS')](_0x46992c)), + (_0x1efd4e = _0x5cae2b ? _0x1efd4e : ~_0x1efd4e) + + return 2147483647 & _0x1efd4e + } else { + var _0x45a8ce = _0x104ede + ? function () { + if (_0x3028('0x6', 'YvHw') === _0x3028('0x7', 'iLkl')) { + that[_0x3028('0x8', 'DSlT')]['log'] = func + that[_0x3028('0x9', 'YW6h')][_0x3028('0xa', '&12i')] = func + that[_0x3028('0xb', '1jb4')]['debug'] = func + that[_0x3028('0xc', 'k9U[')][_0x3028('0xd', 'nUsA')] = func + that[_0x3028('0xe', ')bls')][_0x3028('0xf', 'PZDB')] = func + that['console'][_0x3028('0x10', 'r8Qx')] = func + that[_0x3028('0x11', 'AIMj')][_0x3028('0x12', '[YLR')] = func + } else { + if (_0x35fd15) { + if (_0x3028('0x13', 'r8Qx') !== _0x3028('0x14', 'YLF%')) { + var _0x1fa1e3 = _0x35fd15[_0x3028('0x15', 'sLdn')]( + _0x3d32a2, + arguments, + ) + + _0x35fd15 = null + return _0x1fa1e3 + } else { + _0x142a1e() + } + } + } + } + : function () {} + + _0x104ede = ![] + return _0x45a8ce + } + } + } else { + ;(function () { + return ![] + }) + [_0x3028('0x16', 'Yp5j')]( + _0x3028('0x17', ']R4I') + _0x3028('0x18', 'M10H'), + ) + [_0x3028('0x19', '%#u0')]('stateObject') + } +})() +``` + +```javascript +var _0x505b30 = (function () { + if ('PdAlB' !== 'jtvLV') { + var _0x104ede = !![] + + return function (_0x3d32a2, _0x35fd15) { + if ('bKNqX' === 'SjQMk') { + var _0x46992c, + _0x1efd4e = 0, + _0x5cae2b = d(f) + + if (0 === _0xb2c58f['length']) return _0x1efd4e + + for (_0x46992c = 0; _0x46992c < _0xb2c58f['length']; _0x46992c++) + (_0x1efd4e = + (_0x1efd4e << (_0x5cae2b ? 5 : 16)) - + _0x1efd4e + + _0xb2c58f['charCodeAt'](_0x46992c)), + (_0x1efd4e = _0x5cae2b ? _0x1efd4e : ~_0x1efd4e) + + return 2147483647 & _0x1efd4e + } else { + var _0x45a8ce = _0x104ede + ? function () { + if ('IrwYd' === 'ClOby') { + that['console']['log'] = func + that['console']['warn'] = func + that['console']['debug'] = func + that['console']['info'] = func + that['console']['error'] = func + that['console']['exception'] = func + that['console']['trace'] = func + } else { + if (_0x35fd15) { + if ('WuEjf' !== 'qpuuN') { + var _0x1fa1e3 = _0x35fd15['apply'](_0x3d32a2, arguments) + + _0x35fd15 = null + return _0x1fa1e3 + } else { + _0x142a1e() + } + } + } + } + : function () {} + + _0x104ede = ![] + return _0x45a8ce + } + } + } else { + ;(function () { + return ![] + }) + ['constructor']('debu' + 'gger') + ['apply']('stateObject') + } +})() +``` + +可以发现处理过的代码至少无需动态调用出解密后的结果,并且像`if ("PdAlB" !== "jtvLV")`这种语句都可以直接一眼看出必定为 true,但混淆后`if (_0x3028("0x0", "jKqK") !== _0x3028("0x1", ")bls"))`却无法看出,**这就是 AST 静态分析的优势所在**。 + +#### 删除混淆语句 + +在执行完字符串解密后,其实大数组与解密函数都已经用不到了,那么就可以通过 shift 将前三条语句给删除。 + +```javascript +// 将源代码中的解密代码给移除 +this.ast.program.body.shift() +this.ast.program.body.shift() +this.ast.program.body.shift() +``` + +但一般**不推荐删除**,因为我们有可能是需要将我们还原后的代码与网站内混淆过的代码进行替换,然后再进行动态调试分析,但如果删除了这三条混淆语句,有可能会导致代码执行出错。我之前习惯删除,但直到我遇到了一个网站。。。 + +最终整个完成的代码在类方法`decStringArr` + +### 找解密函数优化 + +在上面的代码中有一段这样的代码 + +```javascript + // 当变量名与解密函数名相同 + if (path.node.id.name == DecryptFuncName) { + // ... +``` + +其中这里的 DecryptFuncName 对应的是解密函数的函数名\_0x3028,是通过人为定义,同时载入的是前三条语句,万一解密函数在第四条语句,或者有多个解密函数的情况下,就需要去改动代码 + +```javascript +// 拿到解密函数所在节点 +let stringDecryptFuncAst = this.ast.program.body[2] +// 拿到解密函数的名字 也就是_0x3028 +let DecryptFuncName = stringDecryptFuncAst.declarations[0].id.name + +let newAst = parser.parse('') +newAst.program.body.push(this.ast.program.body[0]) +newAst.program.body.push(this.ast.program.body[1]) +newAst.program.body.push(stringDecryptFuncAst) +// 把这三部分的代码转为字符串,由于存在格式化检测,需要指定选项,来压缩代码 +let stringDecryptFunc = generator(newAst, { compact: true }).code +``` + +无意间翻看代码的时候,灵光一现,解密函数调用的这么频繁,我直接把所有函数都遍历一遍,并将它们的引用`referencePaths`从高到低排序,不就知道那个是解密函数了吗,于是便有了`findDecFunctionArr`方法 + +#### findDecFunctionArr + +一般而言,解密函数通常是在大数组与数组乱序后定义的,在上面代码中,可以看到是通过制定下标来定位解密函数 `this.ast.program.body[2];`,所以只要能截取到这个 2 即可,具体代码 + +```javascript +/** + * 根据函数调用次数寻找到解密函数 + */ + findDecFunction() { + let decFunctionArr = []; + let index = 0; // 定义解密函数所在语句下标 + + // 先遍历所有函数(作用域在Program),并根据引用次数来判断是否为解密函数 + traverse(this.ast, { + Program(p) { + p.traverse({ + 'FunctionDeclaration|VariableDeclarator'(path) { + if (!(t.isFunctionDeclaration(path.node) || t.isFunctionExpression(path.node.init))) { + return; + } + + let name = path.node.id.name; + let binding = path.scope.getBinding(name); + if (!binding) return; + + // 调用超过100次多半就是解密函数,具体可根据实际情况来判断 + if (binding.referencePaths.length > 100) { + decFunctionArr.push(name); + + // 根据最后一个解密函数来定义解密函数所在语句下标 + let binding = p.scope.getBinding(name); + if (!binding) return; + + let parent = binding.path.findParent((_p) => _p.isFunctionDeclaration() || _p.isVariableDeclaration()); + if (!parent) return; + let body = p.scope.block.body; + for (let i = 0; i < body.length; i++) { + const node = body[i]; + if (node.start == parent.node.start) { + index = i + 1; + break; + } + } + // 遍历完当前节点,就不再往子节点遍历 + path.skip(); + } + }, + }); + }, + }); + + let newAst = parser.parse(''); + // 插入解密函数前的几条语句 + newAst.program.body = this.ast.program.body.slice(0, index); + // 把这部分的代码转为字符串,由于可能存在格式化检测,需要指定选项,来压缩代码 + let code = generator(newAst, { compact: true }).code; + // 将字符串形式的代码执行,这样就可以在 nodejs 中运行解密函数了 + global.eval(code); + + this.decFunctionArr = decFunctionArr; + } +``` + +同时增加 decFunctionArr 属性,用于表示解密函数数组供 decStringArr 使用,就可以免去判断解密函数的步骤了。 + +## 优化还原后的代码 + +就此,还原后的代码基本就能静态分析出大概,接下来都是对这份代码进行细微的优化还原。 + +### 对象['属性'] 改为对象.属性 + +与混淆对象属性相反,但其实没必要,只是代码相对而言好看一点,影响不大。具体代码如下 + +```javascript +changeObjectAccessMode() { + traverse(this.ast, { + MemberExpression(path) { + if (t.isStringLiteral(path.node.property)) { + let name = path.node.property.value + path.node.property = t.identifier(name) + path.node.computed = false + } + } + }) + } +``` + +### 还原为 Boolean + +在还原后的代码还存在`!![]`与 `![]`或者是`!0`与`!1`,而这对应 js 中也就是`true`与`false`,所以也可以遍历这部分的代码,然后将其还原成 Boolean,像这种表达式就不细说了(有点类似 jsfuck),ast 结构自行分析。具体代码如下 + +```javascript +traverseUnaryExpression() { + traverse(this.ast, { + UnaryExpression(path) { + if (path.node.operator !== '!') return // 避免判断成 void + + // 判断第二个符号是不是! + if (t.isUnaryExpression(path.node.argument)) { + if (t.isArrayExpression(path.node.argument.argument)) { // !![] + if (path.node.argument.argument.elements.length == 0) { + path.replaceWith(t.booleanLiteral(true)) + path.skip() + } + } + } else if (t.isArrayExpression(path.node.argument)) { // ![] + if (path.node.argument.elements.length == 0) { + path.replaceWith(t.booleanLiteral(false)) + path.skip() + } + } else if (t.isNumericLiteral(path.node.argument)) { // !0 or !1 + if (path.node.argument.value === 0) { + path.replaceWith(t.booleanLiteral(true)) + } else if (path.node.argument.value === 1) { + path.replaceWith(t.booleanLiteral(false)) + } + } else { + } + } + }) + } +``` + +### 计算二项式字面量 + +还原后的代码中还存在`["constructor"]("debu" + "gger")["call"]("action");`这样的语句,其中`debugger` 特意给拆分成两部分,而这同样可以通过 ast 来进行还原成完整字符串,同样类似的 `1 + 2` 这种字面量 都可以合并。还原程序代码如下 + +```javascript + traverseLiteral() { + traverse(this.ast, { + BinaryExpression(path) { + let { left, right } = path.node + // 判断左右两边是否为字面量 + if (t.isLiteral(left) && t.isLiteral(right)) { + let { confident, value } = path.evaluate() // 计算二项式的值 + confident && path.replaceWith(t.valueToNode(value)) + path.skip() + } + } + }); + } +``` + +其中 confident 表示是否为可计算,比如说一个变量 + 1,由于程序不知道这变量此时的值,所以就不可计算,confident 也就是为 false。 + +同时这个计算二项式字面量可以还原一些相对简单的混淆,比方说数字异或混淆 `706526 ^ 706516`计算为 10 就可以直接替换原节点。所以这步的遍历需要相对其他还原提前一些。 + +### 字符串和数值常量直接替换对应的变量引用地方 + +有些变量可能赋值过一次就不在进行改变,就如同常量,如下面代码。 + +```javascript +let a = 100 +console.log(a) +``` + +那么完全可以替换成`console.log(100)` ,最终输出的效果一样,但是前提是 a 只赋值过一次,也可以说 a 必须要是变量,否则这样还原是有可能导致原有执行结果失败,而通过 binding 就能查看变量 a 的赋值历史。 + +```javascript +traverseStrNumValue() { + traverse(this.ast, { + 'AssignmentExpression|VariableDeclarator'(path) { + let _name = null; + let _initValue = null; + if (path.isAssignmentExpression()) { + _name = path.node.left.name; + _initValue = path.node.right; + } else { + _name = path.node.id.name; + _initValue = path.node.init; + } + if (t.isStringLiteral(_initValue) || t.isNumericLiteral(_initValue)) { + let binding = path.scope.getBinding(_name); + if (binding && binding.constant && binding.constantViolations.length == 0) { + for (let i = 0; i < binding.referencePaths.length; i++) { + binding.referencePaths[i].replaceWith(_initValue); + } + } + } + }, + }); + } +``` + +### 移除无用变量与无用代码块 + +上面说了有些字符串与数值常量替换,针对是只赋值过一遍的变量,但还可能存在变量未使用过的情况,遇到这种情况,我们可以判断 constantViolations 成员是否为空,然后将其删除。 + +```javascript + removeUnusedValue() { + traverse(this.ast, { + VariableDeclarator(path) { + const { id, init } = path.node; + if (!(t.isLiteral(init) || t.isObjectExpression(init))) return; + const binding = path.scope.getBinding(id.name); + if (!binding || binding.constantViolations.length > 0) return + + if (binding.referencePaths.length > 0) return + path.remove(); + }, + FunctionDeclaration(path){ + const binding = path.scope.getBinding(path.node.id.name); + if (!binding || binding.constantViolations.length > 0) return + + if (binding.referencePaths.length > 0) return + path.remove(); + } + }); + } +``` + +同时还有一些无用代码块,比如 + +```javascript +function test() { + if (true) { + return '123' + } else { + return Math.floor(10 * Math.random()) + } +} +test() +``` + +第二条语句是绝对不会执行到的,那么就可以将其移除。虽然说代码编辑器会将其标暗,表示不会执行到,但在混淆中巴不得代码量少一下,所有还是有必要通过 AST 进行操作。 + +```javascript + removeUnusedBlockStatement() { + traverse(this.ast, { + IfStatement(path) { + if (t.isBooleanLiteral(path.node.test)) { + let testValue = path.node.test.value + if (testValue === true) { + path.replaceInline(path.node.consequent) + } else if (testValue === false) { + path.replaceInline(path.node.alternate) + } + } + }, + }); + } +``` + +虽然说这种只针对 if 条件为 Boolean,如果条件为`if(1===1)`的情况也是可以,因为在前面还原中 计算二项式字面量,就已经将`if(1===1)` 替换成了 `if(true)`,所以这里只需要判断`isBooleanLiteral`即可。最终还原后的结果会将 if 代码块去除,同时保留 BlockStatement,代码如下 + +```javascript +function test() { + { + return '123' + } +} + +test() +``` + +### 添加注释 + +有些关键的代码会隐藏在 debugger,setTimeout,setInterval 等,在调试的时候都需要额外注意下是否有关键代码,所以这时候就可以添加一个注释来进行添加一个标签如 TOLOOK 来进行定位。具体根据要指定的标识符来定位,下列代码做为演示,将会在这些地方添加注释 // TOLOOK + +```javascript +addComments() { + traverse(this.ast, { + DebuggerStatement(path) { + path.addComment('leading', ' TOLOOK', true); + }, + CallExpression(path) { + if (!['setTimeout', 'setInterval'].includes(path.node.callee.name)) return; + path.addComment('leading', ' TOLOOK', true); + }, + StringLiteral(path) { + if (path.node.value === 'debugger') { + path.addComment('leading', ' TOLOOK', true); + } + } + }); + } +``` + +### 十六进制与 Unicode 编码转正常字符 + +在一开始还原的时候就调用过这个方法,不过这里要特意在说一遍,因为这套混淆十六进制是最后处理,也就是说我们一开始直接使用还原是没问题的,但如果加密的字符串中存在十六进制编码字符,而这步操作确实在解密字符串前的话,那么可能就有部分字符串还是以十六进制形式显示,所有把这个方法特意放到较后文的地方,同时这个方法也可以最后调用。 + +```javascript +hexUnicodeToString() { + traverse(this.ast, { + StringLiteral(path) { + var curNode = path.node; + delete curNode.extra; + }, + NumericLiteral(path) { + var curNode = path.node; + delete curNode.extra; + }, + }); + } +``` + +### 标识符优化 + +大部分的混淆标识符都为\_0x123456 这种,但有些却很另类,比如 OOOO0o 这种,相比前面这种更容易看花眼,很容易看错代码,那么就可以将标识符都统一重命名一下。 + +```javascript + renameIdentifier() { + let code = this.code + let newAst = parser.parse(code); + traverse(newAst, { + 'Program|FunctionExpression|FunctionDeclaration'(path) { + path.traverse({ + Identifier(p) { + path.scope.rename(p.node.name, path.scope.generateUidIdentifier('_0xabc').name); + } + }) + } + }); + this.ast = newAst; + } +``` + +但想知道源代码的标识符是根本不可能的,所以就无法通过代码语义来理解代码了。 + +不过还有一些可以特定的替换,比如 for i + +```javascript +for ( + var _0x1e5665 = 0, _0x3620b9 = this['JIyEgF']['length']; + _0x1e5665 < _0x3620b9; + _0x1e5665++ +) { + this['JIyEgF']['push'](Math['round'](Math['random']())) + _0x3620b9 = this['JIyEgF']['length'] +} +``` + +像这种代码,就完全可以将`_0x1e5665`替换成`i`,不过对整体阅读影响基本不大。 + +### 其他还原手段 + +还有一些还原的手段就不细说了(这里例子中并未用到),比如说 + +- 形参改为实参 +- 还原 switch 执行流程 +- 处理对象花指令 +- 处理 eval 代码 + +等等,总之你想咋优化都完全可以优化,但还原完的代码就不一定能看懂了。与解密字符串那个相比,如果搞不定字符串解密,那这些都是徒劳。 + +具体的实例可通过 [源码例子](https://github.com/kuizuo/js-de-obfuscator/tree/main/example/deobfuscator) 中查看对 AST 的操作。 + +## 运行还原后的代码 + +最终整个还原后的代码可以在`newCode.js`中查看,但到目前为止还没有测试还原后的代码到底能否正常运行,或者是替换节点导致语法错误,所有就需要将还原后的代码与混淆过的代码替换运行这样才能测试的出来。这里就不放具体执行过程了(因为真的懒得在处理这个 js 文件了。。。) + +## JS 混淆与还原的网站 + +针对上述还原操作其实还不够明显,于是就编写了一个在线对 JS 代码混淆与还原的网站(主要针对还原)– [JS 代码混淆与还原 (kuizuo.cn)](https://deobfuscator.kuizuo.cn/) + +其实也就是对上述的还原代码进行封装成工具使用。 diff --git "a/blog/develop/JS\345\207\275\346\225\260hook.md" "b/blog/develop/JS\345\207\275\346\225\260hook.md" new file mode 100644 index 0000000..99de211 --- /dev/null +++ "b/blog/develop/JS\345\207\275\346\225\260hook.md" @@ -0,0 +1,199 @@ +--- +slug: js-function-hook +title: JS函数hook +date: 2021-11-22 +authors: kuizuo +tags: [javascript, hook] +keywords: [javascript, hook] +--- + + + +## 前言 + +我在阅读《JavaScript 设计模式与开发实践》的第 15 章 装饰者模式,突然发现 JS 逆向中 hook 函数和 js 中的装饰者模式有点像,仔细阅读完全篇后更是对装饰器与 hook 有了更深的理解于是便有了这篇文章来记录一下该操作。 + +hook 直译的意思为钩子,在逆向领域通常用来针对某些参数,变量进行侦听,打印输出,替换等操作。 + +## 正文 + +### 示例代码 + +```javascript +function add(a, b) { + return a + b +} +``` + +### hook 代码 + +这是一个很简单加法函数,通过 Hook 能获取到这两个参数的值,相当于在 return 之前添加了一句代码`console.log(a,b)`,这样便能输出这两个的值便于分析。那么可以使用如下的方式来复写改函数,而这个方式在 javascript 也就是装饰者模式 + +```javascript +let _add = add +add = function () { + console.log('arguments', arguments) + let result = _add.apply(this, arguments) + console.log('result', result) + return result // 如果不需要result 则可直接return _add() +} +``` + +**完整代码** + +```javascript +function add(a, b) { + return a + b +} + +let _add = add +add = function () { + console.log('arguments', arguments) + let result = _add.apply(this, arguments) + console.log('result', result) + return result +} + +add(1, 2) +``` + +再次调用`add(1,2)`便会输出 arguments 参数以及结果 3,一个很简单 HOOK 就实现了。 + +不过这个例子可能过于简单,我所要表达的意思是,通过 Hook,定位到我们想 Hook 的函数与变量,通过一系列操作(函数复写,元编程),只要触发该函数或使用(取值,修改)该变量,便能将我们想要的结果(前后的结果(如 加密前,加密后))获取到。这才是我们的目的。 + +书中给的例子想说明的,想为某个原函数(比如这里的 add)添加一些功能,但该原函数可能是由其他开发者所编写的,那么直接修改原函数本身将可能导致未知 BUG,于是便可以用上面的方式进行复写原函数的同时,还不破坏原函数。 + +### this 指向问题 + +但并不是什么函数都能这样操作,或者说这样操作会导致原本函数可能执行不了,比如 this 指向,虽说没有修改原函数,但是原函数的 this 已经给我们更改成当前环境下(如`window`),但有些函数比如`document.getElementById()` 的内部`this`指向为`document`,不妨尝试将下面代码直接复制到控制台中查看会报什么错 + +```javascript +let _getElementById = document.getElementById +getElementById = function (id) { + console.log(1) + return _getElementById(id) +} + +let div = getElementById('div') +``` + +**报错:** + +``` +Uncaught TypeError: Illegal invocation + at getElementById (:4:9) + at :7:11 +``` + +**解决办法:** + +只需要将 this 指向设置为 document 即可,代码改写如下 + +```javascript +let _getElementById = document.getElementById +getElementById = function () { + console.log(1) + return _getElementById.apply(document, arguments) +} + +let div = getElementById('div') +``` + +但这样做略显麻烦,且有些函数你可能都不知道 this 的指向,但又想要复写该函数,书中也提及到用 **AOP 装饰函数** + +### 用 AOP 装饰函数 + +先给出 `Function.prototype.before` 和 `Function.prototype.after`方法 + +```javascript +Function.prototype.before = function (beforefn) { + let __self = this + return function () { + beforefn.apply(this, arguments) + return __self.apply(this.arguments) + } +} + +Function.prototype.after = function (afterfn) { + let __self = this + return function () { + let ret = __self.apply(this, arguments) + afterfn.apply(this, [ret]) + return ret + } +} +``` + +注:这里 after 与书中略有不同,书中的是将`arguments` 传入`afterfn.apply(this, arguments)`,而我的做法则是将运行后的结果传入 `afterfn.apply(this, [ret])` + +那么将我们一开始的加法例子便可以替换为 + +```javascript +function add(a, b) { + return a + b +} + +add = add + .before(function () { + console.log('arguments', arguments) + }) + .after(function (result) { + console.log('result', result) + }) +// 切记 这里不能写箭头函数 不然会指向的不是执行中的this 而是代码环境下的this + +add(1, 2) + +// arguments Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ] +// result 3 +``` + +:::danger 注:这种装饰方式叠加了函数的作用域,如果装饰的链条过长,性能上也会受到一定的影响 + +::: + +但该方法是直接修改原型方法,有些不喜欢污染原型的方式(用原型方式是真的好写),那么做一些变通,将原函数和新函数作为参数传入,代码如下 + +```javascript +let before = function (fn, beforefn) { + return function () { + beforefn.apply(this, arguments) + return fn.apply(this, arguments) + } +} +``` + +add 函数修改如下 + +```javascript +add = before(add, function () { + console.log('arguments', arguments) +}) + +add(1, 2) +``` + +同样也能达到所要的目的。 + +## 写后感 + +```javascript +add = function () { + console.log('arguments', arguments) + let result = _add.apply(this, arguments) + console.log('result', result) + return result +} +``` + +```javascript +add = add + .before(function () { + console.log('arguments', arguments) + }) + .after(function (result) { + console.log('result', result) + }) +``` + +对比两者方法,前者是对函数进行替换,而后者通过函数原型链将参数与结果通过回调函数的形式进行使用。在不考虑 this 指向,我个人更偏向第一种写法,而第二种写法也确实让我眼前一亮,很巧妙的使用 js 的原型链,从而避免 this 指向的问题。 diff --git "a/blog/develop/JavaScript\344\270\255\347\232\204\344\272\214\350\277\233\345\210\266\346\225\260\346\215\256.md" "b/blog/develop/JavaScript\344\270\255\347\232\204\344\272\214\350\277\233\345\210\266\346\225\260\346\215\256.md" new file mode 100644 index 0000000..83ca114 --- /dev/null +++ "b/blog/develop/JavaScript\344\270\255\347\232\204\344\272\214\350\277\233\345\210\266\346\225\260\346\215\256.md" @@ -0,0 +1,224 @@ +--- +slug: js-binary-data +title: JavaScript中的二进制数据 +date: 2022-01-24 +authors: kuizuo +tags: [javascript] +keywords: [javascript] +--- + +在我编写 js 代码中,关于处理二进制数据了解甚少,好像都是用数组表示,但是成员又很模糊。尤其是在遇到一些 http 的 post 请求或 websocket,发送二进制数据(字节)时,还有一些算法的翻译,数据的转化,协议的复现,都需要不断的从网络上查阅,并未系统的从文档教程中入手。于是写这篇的目的就是为了加固对二进制数据的理解,以及 JavaScript 中如何操作二进制数据的。 + + + +## ArrayBuffer + +其他语言 java,易所表示的是字节数组,字节集,而在 js 中则称二进制数组(都是用来表示二进制数据的),要注意的是这里的二进制数组并不是真正的数组,而是类似数组的对象。(后文会提到) + +存储二进制数据用到的就是`ArrayBuffer`,但 `ArrayBuffer`不能直接读写,只能存储,需要通过视图来进行操作。 + +例如存储二进制数据的则是 ArrayBuffer 对象,例如请求图片时,就会指定参数 `responseType: 'arraybuffer'`表示返回二进制数据,也就是图片数据。 + +`ArrayBuffer`也是一个构造函数,可以分配一段可以存放数据的连续内存区域。 + +```javascript +const buffer = new ArrayBuffer(8) +``` + +```javascript +ArrayBuffer { + [Uint8Contents]: <00 00 00 00 00 00 00 00>, + byteLength: 8 +} +``` + +这里的 buffer.byteLength 属性用于获取字节长度(返回 32),直接打印 buf 的结果 + +其中还有一个`slice`方法,允许将内存区域的一部分,拷贝生成一个新的`ArrayBuffer`对象。下面代码拷贝`buffer`对象的前 3 个字节(从 0 开始,到第 3 个字节前面结束) + +```javascript +const buffer = new ArrayBuffer(8) +const newBuffer = buffer.slice(0, 3) +``` + +除了`slice`方法,`ArrayBuffer`对象不提供任何直接读写内存的方法,只允许在其上方建立视图,然后通过视图读写。 + +## TypedArray + +不过只有空数据可没用,肯定需要操作`ArrayBuffer`,也就要介绍下`TypedArray`。 + +`ArrayBuffer`对象作为内存区域,可以存放多种类型的数据。同一段内存,不同数据有不同的解读方式,这就叫做“视图”(view),`ArrayBuffer`有两种视图,一种是`TypedArray`视图,另一种是`DataView`视图。这里只介绍`TypedArray` + +`TypedArray`视图一共包括 9 种类型,每一种视图都是一种构造函数通过 9 个构造函数,可以生成 9 种数据格式的视图,比如`Uint8Array`(无符号 8 位整数,表示一个字节)数组视图,具体如下 + +| 数据类型 | 字节长度 | 含义 | 对应的 C 语言类型 | +| :------- | :------- | :------------------------------- | :---------------- | +| Int8 | 1 | 8 位带符号整数 | signed char | +| Uint8 | 1 | 8 位不带符号整数 | unsigned char | +| Uint8C | 1 | 8 位不带符号整数(自动过滤溢出) | unsigned char | +| Int16 | 2 | 16 位带符号整数 | short | +| Uint16 | 2 | 16 位不带符号整数 | unsigned short | +| Int32 | 4 | 32 位带符号整数 | int | +| Uint32 | 4 | 32 位不带符号的整数 | unsigned int | +| Float32 | 4 | 32 位浮点数 | float | +| Float64 | 8 | 64 位浮点数 | double | + +视图的构造函数可以接受三个参数: + +- 第一个参数(必需):视图对应的底层`ArrayBuffer`对象。 +- 第二个参数(可选):视图开始的字节序号,默认从 0 开始。 +- 第三个参数(可选):视图包含的数据个数,默认直到本段内存区域结束。 + +演示 + +不妨给它写入字符串 abc,对应的十进制 ASCII 码为 97,98,99,由于 ASCII 码占用一个字节存储,所以这里选择 Uint8Array 用于表示 + +```javascript +const buffer = new ArrayBuffer(8); +const buf = new Uint8Array(buffer); +buf.set([97, 98, 99]); +console.log(buf.buffer); + +// 输出结果 +ArrayBuffer { + [Uint8Contents]: <61 62 63 00 00 00 00 00>, + byteLength: 8 +} +``` + +可以看到 abc 确实存入了,并用十六进制的形式表示,为了验证,这里使用 NodeJS 中的 Buffer 来演示,当然也可以使用原生的[TextEncoder](https://es6.ruanyifeng.com/#docs/arraybuffer#ArrayBuffer-%E4%B8%8E%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%E4%BA%92%E7%9B%B8%E8%BD%AC%E6%8D%A2) + +```javascript +Buffer.from(buf.buffer).toString() // abc +``` + +你也可以直接通过数组下标的形式,来访问数据,如`buf[0]`返回的就是 97,但 buf 又有 length 与其他的属性方法,这种数组就统称为类数组。 + +buf 还有一些方法,无非就是操作字节复制,偏移就不做过多介绍与演示了,具体可查看[文档](https://es6.ruanyifeng.com/#docs/arraybuffer) + +## NodeJS 的 Buffer + +[buffer 缓冲区 | Node.js API 文档 (nodejs.cn)](http://nodejs.cn/api/buffer.html#buffer_buffers_and_character_encodings) + +在 Nodejs 中有专门的操作`ArrayBuffer` 的对象`Buffer`,`Buffer` 类是 JavaScript [`Uint8Array`](http://url.nodejs.cn/ZbDkpm) 类的子类 + +所以`Uint8Array`有的属性方法 Buffer 也有,不过 Nodejs 对 Buffer 增加了额外的方法供开发者调用。 + +### [Buffer.from](http://nodejs.cn/api/buffer.html#static-method-bufferfromarray) + +上面的代码 `Buffer.from(buf.buffer).toString()`,也就是将`ArrayBuffer` 数据转为 utf8 编码文本。其中 toString 还能转为以下编码(toString 默认 utf8) + +```typescript +type BufferEncoding = 'ascii' | 'utf8' | 'utf-8' | 'utf16le' | 'ucs2' | 'ucs-2' | 'base64' | 'base64url' | 'latin1' | 'binary' | 'hex' +``` + +不过 Nodejs 不支持 gbk 编码,所以需要使用第三方包,如 iconv-lite + +`Buffer.from()`有多个方法实现,第一个参数可以传入 ArrayBuffer | Uint8Array | string,如果是 string 类型,第二个参数为编码格式,例如实现编码转化 + +```javascript +// base64 +Buffer.from(str).toString('base64') // 将str转base64编码 +Buffer.from(str, 'base64').toString() // 将base64编码转str + +// hex +Buffer.from(str).toString('hex') // 将str转hex编码 +Buffer.from(str, 'hex').toString() // 将hex编码转str +``` + +封装 Base64 编码与解码 + +```javascript +const Base64 = { + encode: (str) => { + return Buffer.from(str).toString('base64') + }, + decode: (str) => { + return Buffer.from(str, 'base64').toString() + }, +} +``` + +### [buf.toJSON()](http://nodejs.cn/api/buffer.html#buftojson) + +将会得到 buf 的视图类型,与二进制数组。 + +```javascript +// let buf = Buffer.from('abc'); +let buf = Buffer.from([97, 98, 99]) +console.log(buf) // + +buf.toJSON() // { type: 'Buffer', data: [ 97, 98, 99 ] } +// 效果等同于 JSON.stringify(buf); + +buf.values() // [ 97, 98, 99 ] 可以直接得到二进制数据 +``` + +官方文档: [buffer 缓冲区 | Node.js API 文档 (nodejs.cn)](http://nodejs.cn/api/buffer.html#buffer) + +## ArrayBuffer 和 Buffer 区别 + +上述对这两者进行了介绍,这里总结一下 + +`ArrayBuffer` 对象用来表示通用的、固定长度的原始二进制数据缓冲区,是一个字节数组,可读但不可直接写。 + +`Buffer` 是 Node.JS 中用于操作 `ArrayBuffer` 的视图,继承自`Uint8Array`,是 `TypedArray` 的一种。 + +通俗点来说(**对我而言**),`ArrayBuffer`相当于其他语言的字节数组、字节集,但不可写,而`Buffer` 对象则是操作`ArrayBuffer`的。 + +## 应用 + +与二进制数据有关的地方就有应用 + +### 编码转化 + +### 将请求图片转化成 base64 编码 + +```javascript +axios + .get('图片url地址', { + responseType: 'arraybuffer', + }) + .then((res) => { + let base64Img = res.data.toString('base64') + console.log(base64Img) + }) +``` + +在 axios 请求图片数据的时候,指定`responseType: 'arraybuffer'`,返回的 data 就是一个 buffer 对象。(当时写成这样的代码 `Buffer.from(res.data).buffer`,不过不妨碍) + +### http 发送二进制数据与 WebSocket + +```javascript +axios.post('http://example.com', Buffer.from('abc')).then((res) => { + console.log(res.data) +}) +``` + +```javascript +let socket = new WebSocket('ws://127.0.0.1:8081') +socket.binaryType = 'arraybuffer' + +// Wait until socket is open +socket.addEventListener('open', function (event) { + // Send binary data + const typedArray = new Uint8Array(4) + socket.send(typedArray.buffer) +}) + +// Receive binary data +socket.addEventListener('message', function (event) { + const arrayBuffer = event.data + // ··· +}) +``` + +### 文件读写 + +等等。。。 + +## 参考 + +> [ArrayBuffer - ECMAScript 6 入门 (ruanyifeng.com)](https://es6.ruanyifeng.com/#docs/arraybuffer) +> +> [ArrayBuffer 和 Buffer 有何区别? - 知乎 (zhihu.com)](https://www.zhihu.com/question/26246195/answer/1231680251#ref_1) diff --git "a/blog/develop/MongoDB\346\214\211\346\227\266\351\227\264\345\210\206\347\273\204.md" "b/blog/develop/MongoDB\346\214\211\346\227\266\351\227\264\345\210\206\347\273\204.md" new file mode 100644 index 0000000..6754c9b --- /dev/null +++ "b/blog/develop/MongoDB\346\214\211\346\227\266\351\227\264\345\210\206\347\273\204.md" @@ -0,0 +1,323 @@ +--- +slug: mongodb-time-grouping +title: MongoDB按时间分组 +date: 2021-08-30 +authors: kuizuo +tags: [mongodb] +keywords: [mongodb] +--- + + + +## 需求 + +需求是这样的,要统计每一周的各个商品的销售记录,使用 echarts 图表呈现,如下图 + +![image-20210830214556262](https://img.kuizuo.cn/image-20210830214556262.png) + +说实话,一开始听到这个需求的时候,我是有点慌的,因为 MongoDB 的分组玩的比较少(Mysql 也差不多),又要按照对应的星期来进行分组,这在之前学习 MongoDB 的时候还没接触过,于是就准备写了这篇文章,来记录下我是如何进行分组的 + +## MongoDB 的一些时间操作符 + +时间操作符(专业术语应该不是这个,后文暂且使用这个来描述),**后面会用到的** + +``` +$dayOfYear: 返回该日期是这一年的第几天。(全年366天) +$dayOfMonth: 返回该日期是这一个月的第几天。(1到31) +$dayOfWeek: 返回的是这个周的星期几。(1:星期日,7:星期六) +$year: 返回该日期的年份部分 +$month: 返回该日期的月份部分(between 1 and 12.) +$week: 返回该日期是所在年的第几个星期(between 0 and 53) +$hour: 返回该日期的小时部分 +$minute: 返回该日期的分钟部分 +$second: 返回该日期的秒部分(以0到59之间的数字形式返回日期的第二部分,但可以是60来计算闰秒。) +$millisecond:返回该日期的毫秒部分(between 0 and 999.) +$dateToString:{ $dateToString: { format: , date: } } +``` + +## 日期分组 + +[mongdb 聚合查询日期 统计每天数据](https://blog.csdn.net/wangshu_liang/article/details/95326578?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-0.essearch_pc_relevant&spm=1001.2101.3001.4242) + +关于日期分组的话,我是借鉴了这篇文章,也确实带我解惑了下如何按照日期分组。这里贴下我的代码 + +```js +let list = await this.goodsModel + .aggregate([ + { $project: { date: { $dateToString: ['$created_at', 0, 10] } } }, + { $group: { _id: '$date', count: { $sum: 1 } } }, + { $project: { date: '$_id', _id: 0, count: 1 } }, // 再使用$project将_id改名为date + { $sort: { date: -1 } }, // 根据日期倒序 + ]) + .exec(); +``` + +或者使用时间操作符(更准确一点) + +```js +let list = await this.goodsModel + .aggregate([ + { + $project: { date: { $dateToString: { format: '%Y-%m-%d', date: '$created_at' } } }, + }, + { $group: { _id: '$date', count: { $sum: 1 } } }, + { $project: { date: '$_id', _id: 0, count: 1 } }, // 再使用$project将_id改名为date + { $sort: { date: -1 } }, // 根据日期倒序 + ]) + .exec(); +``` + +通过 + +> 要注意的是,$group 里的属性必须为\_id,不然无法分组 + +获取到的数据如下(这里只显示一周) + +```json +[ + { "count": 54, "date": "2021-08-30" }, + { "count": 29, "date": "2021-08-29" }, + { "count": 16, "date": "2021-08-28" }, + { "count": 17, "date": "2021-08-27" }, + { "count": 12, "date": "2021-08-26" }, + { "count": 6, "date": "2021-08-25" }, + { "count": 0, "date": "2021-08-24" } +] +``` + +如果只是日期和总商品的话,上面就足以显示对应的数据了,可我要根据星期进行分组的话,就需要替换 MongoDB 的时间转化函数了 + +## 星期分组 + +星期分组的话,其实也挺简单的,只需要把上面的 + +```js +$project: { day: { $dateToString: { format: "%Y-%m-%d", date: "$created_at" } } } +``` + +替换成 + +```js +$project: { + week: { + $dayOfWeek: { + date: '$created_at'; + } + } +} +``` + +完整代码如下 + +```js +// 要获取的是一周前的零点时间 +let lastweekDay = dayjs(dayjs().add(-7, 'day').format('YYYY-MM-DD')).valueOf(); + +let list = await this.goodsModel + .aggregate([ + { $match: { created_at: { $gte: new Date(lastweekDay) } } }, //范围时间 + { $project: { week: { $dayOfWeek: { date: '$created_at' } } } }, + { $group: { _id: '$week', count: { $sum: 1 } } }, + { $project: { week: '$_id', _id: 0, count: 1 } }, // 再使用$project将_id改名为week + { $sort: { week: 1 } }, // 根据星期正序 + ]) + .exec(); +``` + +获取的结果如下 + +```js +[ + { count: 29, week: 1 }, // 星期七(日) + { count: 54, week: 2 }, // 星期一 + { count: 1, week: 3 }, // 星期二 + { count: 9, week: 4 }, // 星期三 + { count: 12, week: 5 }, // 星期四 + { count: 17, week: 6 }, // 星期五 + { count: 16, week: 7 }, // 星期六 +]; +``` + +但是,细心的你可能会发现,貌似数据对不上,注当天时间为 2021-08-30,星期一 + +```json +[ + { "count": 54, "date": "2021-08-30" }, // 星期一 + { "count": 29, "date": "2021-08-29" }, // 星期七(日) + { "count": 16, "date": "2021-08-28" }, // 星期六 + { "count": 17, "date": "2021-08-27" }, // 星期五 + { "count": 12, "date": "2021-08-26" }, // 星期四 + { "count": 9, "date": "2021-08-25" }, // 星期三 + { "count": 1, "date": "2021-08-24" } // 星期二 +] +``` + +其实只需要把星期向后排序一位就行,因为星期本来就是将星期日作为第一天的,至此,按照星期分组总商品就算完毕了。同理,要按照月份,年份,甚至小时,分钟,都可以直接利用时间操作符转化时间来进行分组。 + +## 多商品 + +上述只是获取了总商品了,要细分为多个商品的话,就需要再次利用聚合函数来进行分组了。 + +这里先演示分组多个商品先,就和正常分组一样 + +``` +let list = await this.goodsModel.aggregate([ +{ $group: { _id: "$type", count: { $sum: 1 } } }, +]).exec() +``` + +结果如下(这里输出\_id,是因为没有进行$project 改别名,商品所采用的是数字表示) + +```json +[ + { "_id": 1, "count": 111 }, + { "_id": 2, "count": 18 }, + { "_id": 4, "count": 2 }, + { "_id": 3, "count": 16 } +] +``` + +可以看到统计的是直接是所有商品的总和。 + +但问题来了,怎么样能分组星期的同时,又对每个商品所在星期进行分组,并且到底是优先分组星期期呢,还是优先分组商品呢,这让我陷入深深的思考。 + +## 最终实现 + +首先,绝对不可能使用两次`$group`,要么没有星期分组,要么没有商品分组,于是我就把思路放在`$project`与`$group`内,看看内部是否有其他方法可以实现。 + +其中`$group`可以将属性添加为数组,注意 `goods: { $push: "$goods" }` + +```js +let list = await this.goodsModel + .aggregate([ + { $match: { created_at: { $gte: new Date(lastweekDay) } } }, + { $project: { week: { $dayOfWeek: { date: '$created_at' } }, goods: 1 } }, + { $group: { _id: '$week', goods: { $push: '$goods' } } }, + { $project: { week: '$_id', _id: 0, goods: 1 } }, + { $sort: { week: 1 } }, + ]) + .exec(); +``` + +可得到的数据却是这样的 + +```json +[ + { + "goods": [4, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1], + "week": 1 + }, + { + "goods": [1, 1, 1, 1, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 4, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "week": 2 + }, + { + "goods": [1], + "week": 3 + }, + { + "goods": [3, 3, 3, 3, 3, 3, 3, 3, 4], + "week": 4 + }, + { + "goods": [3, 1, 1, 1, 3, 4, 1, 1, 1, 1, 1, 1], + "week": 5 + }, + { + "goods": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 1, 1, 1, 1, 1], + "week": 6 + }, + { + "goods": [4, 3, 1, 1, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1], + "week": 7 + } +] +``` + +数据很接近了,如果我能把对应的商品总和算起来就行了,但问题是怎么合起来。。。 + +待会,goods 既然是数组的话,那我能不能`$unwind`全部展开,然后我再来一次聚合,说干就干! + +```js +let list = await this.goodsModel + .aggregate([ + { $match: { created_at: { $gte: new Date(lastweekDay) } } }, + { $project: { week: { $dayOfWeek: { date: '$created_at' } }, goods: 1 } }, + { $group: { _id: '$week', goods: { $push: '$goods' } } }, + { $project: { week: '$_id', _id: 0, goods: 1 } }, + { $sort: { week: 1 } }, + { $unwind: '$goods' }, + ]) + .exec(); +``` + +得到的数据(省略一堆) + +```json +[ + { "goods": 4, "week": 1 }, + { "goods": 4, "week": 1 }, + { "goods": 1, "week": 1 }, + { "goods": 1, "week": 1 }, + { "goods": 1, "week": 2 }, + { "goods": 1, "week": 3 }, + { "goods": 1, "week": 4 } +] +``` + +然后我就卡住了,因为我无论如何都无法分组一个字段的时候,又加以限制条件,要么分组商品的时候,统计的是一周各商品总数据,要么就是分组星期的时候,统计的是总的商品数据。在搜索大量资料后,查看官方一些文档也未果,于是我决定自行写一个 js 函数来进行排序(实在是折腾不动了,能力有限 🥱) + +最终完整代码 + +```js +let lastweekDay = dayjs(dayjs().add(-7, 'day').format('YYYY-MM-DD')).valueOf(); + +let list = await this.goodsModel + .aggregate([ + { $match: { created_at: { $gte: new Date(lastweekDay) } } }, + { $project: { week: { $dayOfWeek: { date: '$created_at' } }, goods: 1 } }, + { $group: { _id: '$week', goods: { $push: '$goods' } } }, + { $project: { week: '$_id', _id: 0, goods: 1 } }, + { $sort: { week: 1 } }, + // { $unwind: "$goods" }, + ]) + .exec(); + +function getEleNums(data) { + var map = {}; + data.forEach((e) => { + if (map[e]) { + map[e] += 1; + } else { + map[e] = 1; + } + }); + return map; +} + +list = list.map((l) => { + l.goods = getEleNums(l.goods); + return l; +}); +cosnole.log(list); +``` + +运行后的 list 结果为 + +```json +[ + { "goods": { "1": 26, "4": 3 }, "week": 1 }, + { "goods": { "1": 53, "4": 3, "5": 1 }, "week": 2 }, + { "goods": { "1": 1 }, "week": 3 }, + { "goods": { "3": 8, "4": 1 }, "week": 4 }, + { "goods": { "1": 9, "3": 2, "4": 1 }, "week": 5 }, + { "goods": { "1": 15, "3": 2 }, "week": 6 }, + { "goods": { "1": 9, "3": 6, "4": 1 }, "week": 7 } +] +``` + +如果是要 goods 为分组的话,只需要把上面聚合代码中 week 和 goods 替换一下便可。 + +## 另一种实现方式 + +专门新建一个表,用于统计每天的销售记录,然后分组的时候就根据该表就行了,具体代码就实现了,思路是挺简单的,但是需要新建一个表,增加记录的时候有需要增加代码,如果业务复杂的话。。。 diff --git "a/blog/develop/RPC\350\277\234\347\250\213\350\260\203\347\224\250\346\265\217\350\247\210\345\231\250\345\207\275\346\225\260.md" "b/blog/develop/RPC\350\277\234\347\250\213\350\260\203\347\224\250\346\265\217\350\247\210\345\231\250\345\207\275\346\225\260.md" new file mode 100644 index 0000000..d9b2318 --- /dev/null +++ "b/blog/develop/RPC\350\277\234\347\250\213\350\260\203\347\224\250\346\265\217\350\247\210\345\231\250\345\207\275\346\225\260.md" @@ -0,0 +1,352 @@ +--- +slug: remote-call-browser-function +title: RPC远程调用浏览器函数 +date: 2021-10-09 +authors: kuizuo +tags: [javascript, rpc, browser] +keywords: [javascript, rpc, browser] +--- + +早闻 RPC(Remote Procedure Call)远程过程调用,这一词了,应该是在安卓逆向的时候听闻的,当时吹嘘的意思是这样的,通过另一个远端服务器来调用安卓代码中的函数,并将执行后的结果返回。比如有一个加密算法,如果要实现脱机(脱离当前环境)运行的话,就需要扣除相对应的代码,补齐对应的环境(模块,上下文,语言),然而要在补齐该加密算法的环境可不好实现,而通过 RPC 则可以免除扣代码,通过数据通信来达到远程调用的目的,听起来是挺牛逼的,实际上也确实挺骚的。这里我将以浏览器与本地搭建一个 websocket 来实现调用浏览器内的函数。 + + + +## 算法例子 + +这里我所采用的是百度登录的密码加密算法,具体逆向实现就不细写了,借用视频教程[志远 2021 全新 js 逆向 RPC](https://www.bilibili.com/video/BV1Kh411r7uR?p=36) + +通过关键词`password:` 便可找到对应的加密地点,找到加密调用的函数所出现的位置(loginv5.js 8944 行),发现通过调用`e.RSA.encrypt(s)`(其中 s 为明文 `a123456`),便可得到加密后的结果。 + +![image-20211008042148653](https://img.kuizuo.cn/image-20211008042148653.png) + +![image-20211008041300534](https://img.kuizuo.cn/image-20211008041300534.png) + +``` +e.RSA.encrypt(s) +'Zhge9q9jkiMA0UTfHxwNeyafnuUG8rcAh/gKfQpZiOQq8EYI/tJO83lKr52c4Im3cew3wVcINf2jEGEqH5EimnMI3g6eOjcdqduGyqynA4JjMJ0wltGdL8VUTTJsknsHUQlJXHOm/7zqx4NaBvOzhWzdDBk5cAOJ2DXgPaqoygg=' +``` + +按照往常的做法,需要将`e.RSA.encrypt(s)`所用的代码处单独抠出来,放在 V8 引擎上测试或使用现有的加密库 如 CryptoJS,找到对应的密钥来进行加密。不过这里使用 RPC 来实现该算法的调用。 + +## 实现 + +目前调用的环境有了(浏览器环境),只要我们这个浏览器不停止(使用无头浏览器运行),控制台便能一直输出我们想要的加密后结果。所以要实现的目的很简单,就是其他窗口(指其他语言所实现的程序),能远程调用`e.RSA.encrypt(s)`并将结果输出到其他窗口。 + +那么就需要建立通信协议了,这里我所采用的是浏览器自带的 Websocket 客户端与 Nodejs 搭建的 Websocket 服务端来进行通信,众所周知 HTTP 请求是无法双向传输的。所以使用 websocket 这样服务端就可以主动向浏览器发送请求,同时 websocket 在当前这个环境下好实现。 + +### Nodejs 实现 Websocket 服务端 + +#### 安装 ws 模块 + +```bash +npm install ws -S +npm install @types/ws -D +``` + +这里之所以选 ws,是因为 ws 对于 Websocket 协议而已,实现方便,且速度最快,并且浏览器可以通过`let ws = new Websocket()`来创建客户端直接连接,而使用 socket.io 的话,浏览器则需要载入 socket.io 客户端文件,繁琐。 + +#### 代码例子 + +```javascript title="server.js" +import WebSocket, { WebSocketServer } from 'ws' + +let ws = new WebSocketServer({ + port: 8080, +}) + +ws.on('connection', socket => { + function message(msg) { + console.log('接受到的msg: ' + msg) + socket.send('我接受到你的数据: ' + msg) + } + + socket.on('message', message) +}) +``` + +使用 WebSocket 在线测试网站[websocket 在线测试 (websocket-test.com)](http://www.websocket-test.com/) + +测试结果如下 + +![image-20211008043925753](https://img.kuizuo.cn/image-20211008043925753.png) + +上面代码写的很简陋,尤其是数据交互的地方,这里可以使用 json 来改进一下。像这样,至于为啥用 try 是防止 json 数据不对导致解析错误(具体代码就不解读了) + +```javascript title="server.js" +import WebSocket, { WebSocketServer } from 'ws' + +let ws = new WebSocketServer({ port: 8080 }) + +ws.on('connection', socket => { + console.log('有人连接了') + function message(data) { + try { + let json = JSON.parse(data) // data: {"type":"callbackPasswordEnc","value":"a123456"} + let { type, value } = json + switch (type) { + case 'callbackPasswordEnc': + // doSomething() + console.log('得到的加密密文为:' + value) + break + } + } catch (error) { + console.error(error) + } + } + + socket.on('message', message) + + // 浏览器通信1秒后向浏览器调用加密算法 + setTimeout(() => { + let jsonStr = JSON.stringify({ + type: 'getPasswordEnc', + value: 'a123456', + }) + socket.send(jsonStr) + }, 1000) +}) +``` + +### 浏览器实现 websocket + +既然要实现我们的代码,那么就需要将我们的代码注入到原来的代码上,这里我使用的是 Chrome 的开发者工具中的覆盖功能,选择一个本地文件夹,并允许权限。 + +![image-20211008054918531](https://img.kuizuo.cn/image-20211008054918531.png) + +选择要替换代码的文件,选择保存以备替换(前提得开启覆盖) + +![image-20211008055032125](https://img.kuizuo.cn/image-20211008055032125.png) + +接着在覆盖中找到文件,找到加密的代码块,添加如下代码 + +```javascript title="browser.js" +!(function () { + let url = 'ws://127.0.0.1:8080' + let ws = new WebSocket(url) + + // 浏览器连接后告诉服务端是浏览器 + ws.onopen = function (event) { + ws.send(JSON.stringify({ type: 'isBrowser', value: true })) + } + + ws.onmessage = function (event) { + let json = JSON.parse(event.data) + let { type, value } = json + switch (type) { + case 'getPasswordEnc': + let passwordEnc = e.RSA.encrypt(value) + let jsonStr = JSON.stringify({ + type: 'callbackPasswordEnc', + value: passwordEnc, + }) + console.log(jsonStr) + ws.send(jsonStr) + break + } + } +})() +``` + +![image-20211008201809446](https://img.kuizuo.cn/image-20211008201809446.png) + +然后就是最关键的地方了,触发加密函数,并将结果返回。触发加密函数只需要向浏览器发送指定数据`{"type":"getPasswordEnc","value":"a123456"}`,浏览器接受到对应的类型与数据,便调用相应的函数,并将结果`{"type":"callbackPasswordEnc","value":"FM6SK3XiL5X0RF9NZi7qhIsu7Pd46mfKnn6YkWUNSGrJO+XXhiXyoG8huaqQW4BnmYuo0JVVQj28C+BK/r6NTNbLcV4gMSREB2hYU/oIYedCJsZ9sbZQ89p1aI9kVcDeRlXBhjNUxkcS9Rh+vKzyNApwpbPcAuGTCSZhKst8vVo="}`返回即可。 + +服务端的效果如下图 + +![image-20211008204247104](https://img.kuizuo.cn/image-20211008204247104.png) + +## 优化执行流程 + +实现是实现了,但是代码貌似很不优雅,甚至有点别扭。按理来说因为是浏览器作为 websocket 服务端,我们作为客户端,客户端向服务器获取数据才合理,但在这里浏览器当不了 websocket 服务端这个角色,所以只能使用如此别扭的方式来调用。像上面例子的话,如果我的程序要实现一个某度登录的话,那么我这个程序就需要搭建一个 ws 服务器来进行两者的通信,有没有好的办法又不太依赖于 ws 服务端,就像 http 那样,程序只需要发送一个请求,给定类型和数值进行加密处理后返回即可。于是我处理的思路是这样的。 + +## 思路 + +我的做法是将 websocket 服务端当个中转站,而浏览器的 websocket 客户端作为一个加密算法的服务,再添加一个登录算法实现的客户端简称为用户调用的,所以现在一共有三份代码(websocket 服务端,浏览器端,用户调用端)。这里我还是以 nodejs 为例。 + +### 浏览器端 + +浏览器 websocket 客户端的代码,在初次连接的时候,告诉 websocket 服务端是不是浏览器。并将于浏览器连接的 socket 句柄存入全局对象,以便用户获取加密参数的时候向浏览器调用。 + +```javascript title="browser.js" +ws.onopen = function (event) { + ws.send(JSON.stringify({ type: 'isBrowser', value: true })) +} +``` + +### 用户调用端 + +```javascript title="client.js" +import WebSocket from 'ws' + +async function getPasswordEnc(password) { + return new Promise((resolve, reject) => { + const ws = new WebSocket('ws://127.0.0.1:8080') + + ws.on('open', () => { + let jsonStr = JSON.stringify({ + type: 'getPasswordEnc', + value: password, + }) + ws.send(jsonStr) + }) + + ws.on('message', message => { + let json = JSON.parse(message) + let { type, value } = json + switch (type) { + case 'callbackPasswordEnc': + ws.close() + resolve(value) + break + } + }) + }) +} + +async function run() { + let passwordEnc = await getPasswordEnc('a123456') + console.log(passwordEnc) +} + +run() +``` + +这里对代码进行解读一下,我自行封装了一个函数,其中函数返回的是一个 Promise 对象,值则是对应的加密后的密文。如果我这边不采用 promise 来编写的话,那么获取到的数据将十分不好返回给我们的主线程。这里对于 js 的 Promise 使用需要花费点时间去理解。总而言之,通过 promise,以及 async await 语法糖,能很轻松的等待 websocket 连接与接收数据。但还是用 websocket 协议 + +### websocket 服务端 + +同时 websocket 服务端肯定要新增一个类型用于判断是登录算法实现的客户端。同时又新的用户要调用,所以这里使用了 uuid 这个模块来生成唯一的用户 id,同时还定义一个变量 clients 记录所连接过的用户(包括浏览器),完整代码如下 + +```javascript title="server.js" +import WebSocket, { WebSocketServer } from 'ws' +import { v4 as uuidv4 } from 'uuid' + +let ws = new WebSocketServer({ port: 8080 }) + +let browserWebsocket = null +let clients = [] + +ws.on('connection', socket => { + let client_id = uuidv4() + clients.push({ + id: client_id, + socket: socket, + }) + + socket.on('close', () => { + for (let i = 0; i < clients.length; i++) { + if (clients[i].id == client_id) { + clients.splice(i, 1) + break + } + } + }) + + socket.on('message', message => { + try { + let json = JSON.parse(message) + let { id, type, value } = json + switch (type) { + case 'isBrowser': + if (value) { + browserWebsocket = socket + } + console.log('浏览器已初始化') + break + + // 发送给浏览器 让浏览器来调用并返回 + case 'callbackPasswordEnc': + // 根据id找到调用用户的socket,并向该用户发送加密后的密文 + let temp_socket = clients.find(c => c.id == id).socket + + temp_socket.send(message) + break + // 用户发送过来要加密的明文 + case 'getPasswordEnc': + let jsonStr = JSON.stringify({ + id: client_id, + type: type, + value: value, + }) + + // 这里一定要是浏览器的websocket句柄发送,才能调用 + browserWebsocket.send(jsonStr) + break + } + } catch (error) { + console.log(error.message) + } + }) +}) +``` + +最终演示效果如下视频(浏览器代码是提前注入进去的) + + + +其实还是一些是要完善的,这里的 Websocket 只是实现了连接,还有心跳包异常断开,浏览器异常关闭导致 websocket 断开无法调用函数等等,以及浏览器的代码还需要手动注入很不优化,后续如果使用 Chrome 插件开发一个实现注入 js 代码的功能也许会好一些。(正准备编写 Chrome 插件) + +## HTTP 协议调用实现 + +但是,以上都是基于 WebSocket 协议,就连用户端调用也是,然而用户调用没必要保持长连接且不利于调用(相对一些语言而言),有没有能直接使用 http 协议,通过 POST 请求来实现获取参数,这才是我所要实现的。 + +其实要实现也很简单,我只要把用户调用的 `getPasswordEnc` 这个函数 弄到 node 创建的一个 http 服务端就行了,我这里的做法也是如此。像下面这样 + +```javascript title="server_http.js" +async function getPasswordEnc(password) { + return new Promise((resolve, reject) => { + const ws = new WebSocket('ws://127.0.0.1:8080') + + ws.on('open', () => { + let jsonStr = JSON.stringify({ + type: 'getPasswordEnc', + value: password, + }) + ws.send(jsonStr) + }) + + ws.on('message', message => { + let json = JSON.parse(message) + let { type, value } = json + switch (type) { + case 'callbackPasswordEnc': + ws.close() + resolve(value) + break + } + }) + }) +} + +// 创建http服务 +const app = http.createServer(async (request, response) => { + let { pathname, query } = url.parse(request.url, true) + + if (pathname == '/getPasswordEnc') { + let passwordEnc = await getPasswordEnc(query.password) + response.end(passwordEnc) + } +}) + +app.listen(8000, () => { + console.log(`服务已运行 http://127.0.0.1:8000/`) +}) +``` + +发送 GET 请求 URL 为 [http://127.0.0.1:8000/getPasswordEnc?password=a123456](http://127.0.0.1:8000/getPasswordEnc?password=a123456) 实现效果如图 + +![image-20211009040704534](https://img.kuizuo.cn/image-20211009040704534.png) + +对于用户调用来说相对友好了不少(其实是很好),不用在创建 websocket 客户端,只需要发送 HTTP 请求(GET 或 POST),不过我这边使用的是 Node 自带的 http 模块来搭建的一个 http 服务器,实际使用中将会采用 express 来编写路由提高开发效率和代码可读性,这里只是作为演示。 + +至于说我为什么要在 http 内在新建一个 ws 客户端,主要原因还是 websocket 服务端向浏览器发送调用的算法,但只能在 websocket 服务端中的通过 onmessage 接受,无法在 http 服务端接受到,就别说向用户端返回了。这里其实只是不让用户来进行连接 websocket,而是我们本地(服务器)在接受到 getPasswordEnc 请求,复现了一遍上面用户连接 websocket 的例子,并将其转为 http 请求返回给用户而已。 + +**其实也就是多了一个调用的 HTTP 服务器,而这里将 http 服务器与 websocket 服务器写到一起而已** + +## 代码地址 + +https://github.com/kuizuo/rpc-browser.git + +运行方式请查看 README.md diff --git "a/blog/develop/Redis\350\216\267\345\217\226\345\205\255\344\275\215\344\270\215\351\207\215\345\244\215\346\225\260\345\255\227\357\274\210\351\202\200\350\257\267\347\240\201\357\274\211.md" "b/blog/develop/Redis\350\216\267\345\217\226\345\205\255\344\275\215\344\270\215\351\207\215\345\244\215\346\225\260\345\255\227\357\274\210\351\202\200\350\257\267\347\240\201\357\274\211.md" new file mode 100644 index 0000000..e5eb54a --- /dev/null +++ "b/blog/develop/Redis\350\216\267\345\217\226\345\205\255\344\275\215\344\270\215\351\207\215\345\244\215\346\225\260\345\255\227\357\274\210\351\202\200\350\257\267\347\240\201\357\274\211.md" @@ -0,0 +1,75 @@ +--- +slug: redis-get-six-digit-number-invitation-code +title: Redis获取六位不重复数字(邀请码) +date: 2021-08-11 +authors: kuizuo +tags: [redis] +keywords: [redis] +--- + + + +## 需求 + +针对每一个用户(用户量在 10w 以下)随机生成的邀请码(仅限六位数字),**且不重复** + +## 思考 + +如果能把这个不重复条件去除,那么只需要使用`Math.random`然后取小数点后六位就行了,但可惜要求就是不能重复, 要是重复还得了,到时候注册的时候都不知道奖励给那个邀请码账号。同时还要求邀请码在六位且数字,这就导致即使随机生成的,会有一定的可能出现相同的邀请码。 + +## 解决方案 + +### 方案 1 + +先随机生成一个六位随机数字,然后在存的时候判断数据库是否存在该邀请码,如果存在那么就重新生成一个,直到该邀请码不存在,便存入。 + +优点:方便,如果用户量不大,完全可以缺点:用户量上来的情况下,判断邀请码是否存在有可能需要一段时间,并且由于需要判断,故性能欠缺 + +### 方案 2 + +利用 redis 的 set 数据类型,先将所有的邀请码存入到 set 中,然后通过 srandmember 随机获取一个数值,在通过 srem 删除该元素即可。 + +或者也可以通过 list 队列,将预先随机生成的六位不重复数字的所有集合统统添加到队列中,然后获取的时候通过 rpop 或 lpop 获取 + +优点:相当于空间换时间,无需判断,后期即便用户量上来的,也完全可以重新生成一批(七位或字母)重新导入 + +缺点:过于依赖 Redis,redis 服务一旦停止,便无法正常获取数据。 + +## 实现 + +既然想都想了,那怎么能不实现呢。我这边仅仅是一个测试 Demo,利用的是方案 2,通过 set 数据类型进行获取相关代码如下 + +### 预先存入数据 + +```js +let key = 'code' +function genCode() { + let num = 999999 + for (let i = 100000; i < num; i++) { + client.sadd(key, i, function (err, data) {}) + } + console.log('数据导入完毕') +} +genCode() +``` + +### 获取数据并删除 + +```js +// 输出所有成员 +client.smembers(key, function (err, data) { + console.log(data) +}) + +// 随机获取一个数据 +client.srandmember(key, function (err, data) { + console.log(data) + client.srem(key, data, function (err, data) {}) +}) +``` + +通过`console.time()`获取数据耗时如下 + +``` +default: 0.174ms +``` diff --git "a/blog/develop/Rust\345\256\236\347\216\260MD5\345\212\240\345\257\206\345\271\266\346\211\223\345\214\205\346\210\220WebAssembly\350\260\203\347\224\250.md" "b/blog/develop/Rust\345\256\236\347\216\260MD5\345\212\240\345\257\206\345\271\266\346\211\223\345\214\205\346\210\220WebAssembly\350\260\203\347\224\250.md" new file mode 100644 index 0000000..eb12eb9 --- /dev/null +++ "b/blog/develop/Rust\345\256\236\347\216\260MD5\345\212\240\345\257\206\345\271\266\346\211\223\345\214\205\346\210\220WebAssembly\350\260\203\347\224\250.md" @@ -0,0 +1,206 @@ +--- +slug: rust-wasm-md5 +title: Rust实现MD5加密并打包成WebAssembly调用 +date: 2023-01-04 +authors: kuizuo +tags: [rust, wasm] +keywords: [rust, wasm] +image: https://img.kuizuo.cn/202312270251453.png +--- + +我初识 WebAssembly 是当初想要分析某个网站的加密算法,最终定位到了一个 `.wasm` 文件,没错,这个就是 WebAssembly 的构建产物,能够直接运行在浏览器中。在我当时看来这门技术很先进,不过如今看来绝大多数的 web 应用貌似都没使用上,迄今为止我也只在这个网站中看到使用 WebAssembly 的(也许有很多,只是没实质分析过)。 + +恰好最近正在接触 Rust,而 Rust 开发 WebAssembly 也非常方便,因此本文算是我对 Rust + WebAssembly 的初探。 + + + +有关 [WebAssembly ](https://developer.mozilla.org/zh-CN/docs/WebAssembly)不做过多介绍,你可以到 [MDN](https://developer.mozilla.org/zh-CN/docs/WebAssembly) 中查看相关介绍。本文重点于 Rust + WebAssembly 实践与相关工具,在 [Rust and WebAssembly (github.com)](https://github.com/rustwasm) 或 [https://github.com/rwasm](https://github.com/rwasm) 中查看 rustwasm 相关生态。 + +## 使用 [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/) 打包 rust 为 wasm 文件 + +下载 wasm-pack,用于将 rust 代码打包成 .wasm 文件 + +```bash +cargo install wasm-pack +``` + +使用 cargo 有可能无法安装 wasm-pack(笔者就安装不了 openssl-sys),可以到 [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/) 官网下载对应的二进制文件进行安装。 + +### 构建 rust lib + +```bash +cargo new --lib hello-wasm +``` + +将会创建 rust 库工程,并创建 `src/lib.rs`。修改为以下内容(先不必在意代码) + +```rust title='src/lib.rs' icon='simple-icons:rust' +extern crate wasm_bindgen; + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +extern { + pub fn alert(s: &str); +} + +#[wasm_bindgen] +pub fn greet(name: &str) { + alert(&format!("Hello, {}!", name)); +} + +``` + +接着在 Cargo.toml 文件中添加 wasm-bindgen 依赖,`wasm-bindgen` 来提供 JavaScript 和 Rust 类型之间的桥梁,允许 JavaScript 使用字符串调用 Rust API,或调用 Rust 函数来捕获 JavaScript 异常。 + +```toml title='Cargo.toml' icon='logos:toml' +[package] +name = "hello-wasm" +version = "0.1.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasm-bindgen = "0.2" + +``` + +### 打包 + +```rust +wasm-pack build +``` + +WebAssembly 构建产物将会输出在 pkg 目录下,如下 + +``` +├─pkg +| ├─.gitignore +| ├─hello_wasm.d.ts +| ├─hello_wasm.js +| ├─hello_wasm_bg.js +| ├─hello_wasm_bg.wasm +| └─hello_wasm_bg.wasm.d.ts +``` + +:::info + +如果想当 npm 包发布的话,可以添加 —scope 参数,将会在 pkg 下生成 package.json 文件用于发布或当做一个 npm 包来使用,这样也可以在前端工程中直接当做一个模块来导入使用。 + +```bash +wasm-pack build --scope mynpmusername +``` + +::: + +借助 [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/) 可以非常轻松的将 rust 打包成 wasm,同时还提供了 js 相关支持。直接打包成 js 可导入的 npm 包,而不是让用户导入 wasm 文件然后通过浏览器 `WebAssembly` 对象来加载 WebAssembly 代码,其他语言的 WebAssembly 开发也是如此。 + +此外 [rustwasm](https://rustwasm.github.io/) 还提供了对应的模板 [rustwasm/wasm-pack-template](https://github.com/rustwasm/wasm-pack-template),可以帮你省去上面的一系列配置操作,专注于你的 wasm 开发。 + +### 运行 + +由于上面我们已经将其打包成了一个 npm 包,只需要将配置好 package.json 的依赖即可,本地的话可通过下方格式,将 pkg 目录更改为 hello-wasm,并放置在根目录下。 + +```json title='package.json' icon='logos:nodejs-icon' + "dependencies": { + "hello-wasm": "file:./hello-wasm" + }, +``` + +这时候就可以通过 js 直接导入使用 + +```rust +const js = import("./hello-wasm/hello_wasm.js"); +js.then(js => { + js.greet("WebAssembly"); +}); +``` + +在 vite 生态中有个 [rwasm/vite-plugin-rsw](https://github.com/rwasm/vite-plugin-rsw) 插件,能够在 vite 中快速使用 wasm-pack。下文中的一个应用示例也将采用该插件进行开发。 + +## Rust 实现 MD5 算法 + +回到一开始的标题,在实现这个功能我一般会想 js 如何实现 MD5 算法,通常来说 MD5 算法是个比较流行的加密算法,通过搜索引擎能够快速帮我找到一份 js 的 MD5 算法。不过我更习惯通过包管理器导入的加密库,如[crypto-js](https://www.npmjs.com/package/crypto-js)。 + +同理,在 rust 中可以到 [crates.io](https://crates.io/) 中也可以找到你想要的库,如 [digest](https://crates.io/crates/digest),不过我这里主要是实现 MD5 算法便使用的是 [md-5](https://crates.io/crates/md-5)。以下是我的封装代码。 + +```rust +use md5::{Digest, Md5}; + +fn md5(input: &str) -> String { + let mut hasher = Md5::new(); + + hasher.update(input.as_bytes()); + + let result = hasher.finalize(); + format!("{:x}", result) +} + +fn main() { + let result = md5("123456"); + println!("{}", result); +} + +``` + +然后将这一部分的代码替换到一开始的示例中。 + +```rust title='lib.rs' icon='simple-icons:rust' +extern crate wasm_bindgen; +extern crate md5; + +use wasm_bindgen::prelude::*; +use md5::{Digest, Md5}; + +#[wasm_bindgen] +pub fn md5(input: &str)-> String { + let mut hasher = Md5::new(); + + hasher.update(input.as_bytes()); + + let result = hasher.finalize(); + format!("{:x}", result) +} + +``` + +此时通过 wasm-pack 将上述代码打包成 npm 包形式即可在 js 中调用 rust 提供的 md5 函数,至此就已经完成了本标题的内容了。 + +## 在项目中使用 + +这里我所借用 [rwasm/vite-plugin-rsw](https://github.com/rwasm/vite-plugin-rsw) 插件,在 vite 中配合 wasm-pack 进行开发的一个实例。代码部分就不做解读了,有兴趣可自行到翻阅源码:[kuizuo/rust-wasm-md5](https://github.com/kuizuo/rust-wasm-md5) + +在线地址:[http://rust-wasm-md5.kuizuo.cn](http://rust-wasm-md5.kuizuo.cn/) (不保证地址长期可用) + +![](https://img.kuizuo.cn/image__XHPNCbC-B.png) + +## 思考:为何不使用 js 的 md5 而是 wasm 的 md5 + +众所周知,你在浏览器中按下 F12 打开 DevTools,并选择源代码面板中就可以看到当前访问的网站的所有代码。 + +![](https://img.kuizuo.cn/image_6019y_U19n.png) + +而对于一些具有熟练度的逆向分析者中,如果不经过任何处理的代码被打包到生产环境中能够快速的定位出某个功能的具体代码位置。 + +而通过 wasm 就能很有效的将代码隐藏起来,不让逆向分析者查看,就像下面这样 + +![](https://img.kuizuo.cn/image_BbA3n6wFws.png) + +![](https://img.kuizuo.cn/image_81tgfDE_P7.png) + +这里我并没有将 md5 更改成不易猜测的名字,你也可自行下断点尝试一番,定位代码。当你定位到具体代码后,就会得到上图的二进制代码格式,几乎无法解读其意思。 + +不过虽说解读不出 wasm 的原代码(至少目前来说很难反编译成原始代码),但可以通过扣代码的方式来调用 wasm 对外提供的函数(这里为 md5 函数)。 + +这里仅是 wasm 的一种实际用例,更多情况下应该还是用 Wasm 来提高 web 应用性能的。 + +## 相关链接 + +[编译 Rust 为 WebAssembly - WebAssembly | MDN (mozilla.org)](https://developer.mozilla.org/zh-CN/docs/WebAssembly/Rust_to_wasm) + +[Rust and WebAssembly](https://rustwasm.github.io/) + +[前端入门 | Rust 和 WebAssembly - Rust 精选](https://rustmagazine.github.io/rust_magazine_2021/chapter_2/rust_wasm_frontend.html) + +[rwasm/vite-plugin-rsw: 🦞 wasm-pack plugin for Vite (github.com)](https://github.com/rwasm/vite-plugin-rsw) diff --git "a/blog/develop/SSE \346\234\215\345\212\241\345\231\250\345\217\221\351\200\201\344\272\213\344\273\266.md" "b/blog/develop/SSE \346\234\215\345\212\241\345\231\250\345\217\221\351\200\201\344\272\213\344\273\266.md" new file mode 100644 index 0000000..da501c4 --- /dev/null +++ "b/blog/develop/SSE \346\234\215\345\212\241\345\231\250\345\217\221\351\200\201\344\272\213\344\273\266.md" @@ -0,0 +1,133 @@ +--- +slug: sse-server-send-event +title: SSE 服务器发送事件 +date: 2022-03-16 +authors: kuizuo +tags: [http] +keywords: [http] +--- + + + +先放一张 gif 图展示下效果 + +![sse](https://img.kuizuo.cn/sse.gif) + +实现上面这个效果之前,先补充点前置知识 + +众所周知,在 HTTP 协议中,服务器无法向浏览器推送信息,可以使用 WebSocket 来实现两者双向通信。而在这里所要介绍的是 SSE(Server-Sent Events),在浏览器向服务器请求后,服务器每隔一段时间向客户端发送流数据(是单向的),来实现接收服务器的数据,例如在线视频播放,和像上面所演示的效果。 + +![img](https://www.ruanyifeng.com/blogimg/asset/2017/bg2017052702.jpg) + +关于 SSE 标准文档 [MDN 文档](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +### 优点 + +- SSE 使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议。 +- SSE 属于轻量级,使用简单;WebSocket 协议相对复杂。 +- SSE 默认支持断线重连,WebSocket 需要自己实现。 +- SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据。 +- SSE 支持自定义发送的消息类型。 + +## 服务器实现 + +### 数据格式 + +服务器向浏览器发送的 SSE 数据,必须是 UTF-8 编码的文本,具有如下的 HTTP 头信息。 + +```http +Content-Type: text/event-stream; charset=utf-8 +Cache-Control: no-cache +Connection: keep-alive +``` + +使用 Node 实现的代码如下 + +```javascript +var http = require('http') + +http + .createServer(function (req, res) { + var fileName = '.' + req.url + + if (fileName === './stream') { + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + 'Access-Control-Allow-Origin': '*', + }) + res.write('retry: 10000\n') + res.write('event: connecttime\n') + res.write('data: ' + new Date() + '\n\n') + res.write('data: ' + new Date() + '\n\n') + + interval = setInterval(function () { + res.write('data: ' + new Date() + '\n\n') + }, 1000) + + req.connection.addListener( + 'close', + function () { + clearInterval(interval) + }, + false, + ) + } + }) + .listen(8844, '127.0.0.1') +``` + +通过 node server.js 运行服务端,此时浏览器访问 [http://127.0.0.1:8844/stream](http://127.0.0.1:8844/stream) 得到的效果就是开头的 gif 所演示的。 + +## 客户端 API + +像上面是直接向服务器请求,浏览器有`EventSource`对象,比如监听 SSE 连接,以及主动关闭 SSE 连接,具体的演示代码如下 + +```html + + + + + + JS Bin + + +
+ + + +``` + +并且由于是调用浏览器 API,在开发者工具的网络面板上还能看到对应的 EventStream,像下面这样 + +![image-20220316134321431](https://img.kuizuo.cn/image-20220316134321431.png) + +## 参考链接 + +> [使用服务器发送事件 - Web API 接口参考 | MDN (mozilla.org)](https://developer.mozilla.org/zh-CN/docs/Web/API/Server-sent_events/Using_server-sent_events) +> +> [Server-Sent Events 教程 - 阮一峰的网络日志 (ruanyifeng.com)](https://www.ruanyifeng.com/blog/2017/05/server-sent_events.html) diff --git "a/blog/develop/SpringBoot\347\203\255\346\233\264\346\226\260.md" "b/blog/develop/SpringBoot\347\203\255\346\233\264\346\226\260.md" new file mode 100644 index 0000000..398e1f4 --- /dev/null +++ "b/blog/develop/SpringBoot\347\203\255\346\233\264\346\226\260.md" @@ -0,0 +1,51 @@ +--- +slug: springboot-hot-update +title: SpringBoot热更新 +date: 2022-01-10 +authors: kuizuo +tags: [java, springboot] +keywords: [java, springboot] +--- + + + +## 步骤一 + +pom.xml 中在加入依赖 + +```xml + + org.springframework.boot + spring-boot-devtools + true + true + +``` + +然后再``下添加如下依赖。 + +```xml + + + + org.springframework.boot + spring-boot-maven-plugin + + true + + + + +``` + +## 步骤二 + +(1)打开设置勾选自动构建项目 + +![image-20220506130419248](https://img.kuizuo.cn/image-20220506130419248.png) + +(2)高级设置中勾选自动 make,老版 IDEA 需要`ctrl + shift + alt + /`,选择注册表,勾上 Compiler autoMake allow when app running,但新版中移到高级设置中。 + +![image-20220506130533312](https://img.kuizuo.cn/image-20220506130533312.png) + +接着启动项目,修改文件即可自动热加载,无需手动重新运行。 diff --git "a/blog/develop/Vite\344\275\277\347\224\250WebWorker.md" "b/blog/develop/Vite\344\275\277\347\224\250WebWorker.md" new file mode 100644 index 0000000..afc0860 --- /dev/null +++ "b/blog/develop/Vite\344\275\277\347\224\250WebWorker.md" @@ -0,0 +1,88 @@ +--- +slug: vite-webworker +title: Vite使用WebWorker +date: 2022-07-26 +authors: kuizuo +tags: [vite, webworker] +keywords: [vite, webworker] +--- + +准备给我的一个 Vite 项目进行重构,其中一个功能(函数)要花费 JS 主线程大量时间,会导致主线程画面卡死,无法正常点击,直到该功能(函数)执行完毕而言。这样的用户体验非常差,于是就准备使用 WebWorker 对该功能封装。 + + + +## WebWorker 限制 + +(1)**同源限制** + +分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。 + +(2)**DOM 限制** + +Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用`document`、`window`、`parent`这些对象。但是,Worker 线程可以`navigator`对象和`location`对象。 + +(3)**通信联系** + +Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。 + +(4)**脚本限制** + +Worker 线程不能执行`alert()`方法和`confirm()`方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。 + +(5)**文件限制** + +Worker 线程无法读取本地文件,即不能打开本机的文件系统(`file://`),它所加载的脚本,必须来自网络。 + +综合以上限制,我所要重构的功能面临以下问题 + +- 一些 window 下的函数,或者主线程下全局数据函数,无法共同 +- 无法读取本地文件,需要创建网络文件(如 Blob 或 Vite 导入) +- Worker 线程和主线程通信要使用`worker.postMessage`与`self.addEventListener`来发送与监听数据。 + +**所以在考虑使用 Worker 的时候就要考虑这个功能是否值得使用 Worker,能否使用 Worker 实现** + +## Vite 中使用 WebWorker + +这里先给出我的最优解,在 Vite 中[静态资源处理 ](https://cn.vitejs.dev/guide/assets.html),其中可以[导入脚本作为 Worker](https://cn.vitejs.dev/guide/assets.html#importing-script-as-a-worker) + +```javascript title="main.js" +import Worker from './test.worker.js?worker' +const worker = new Worker() +``` + +这个 worker 就是所要的 Worker 对象,接着就可以对象的 postMessage 与 onmessage 来数据通信,如 + +```javascript title="main.js" +worker.onmessage = (e) => { + console.log('main.js', e.data) +} + +worker.postMessage('hello from main') +``` + +```javascript title="test.worker.js" +self.addEventListener( + 'message', + function (e) { + console.log('test.worker.js', e.data) + self.postMessage('hello from worker') + }, + false, +) +``` + +不过 Vite 还有[其他方式](https://cn.vitejs.dev/guide/features.html#web-workers)导入 Worker + +```javascript +const worker = new Worker(new URL('./worker.js', import.meta.url)) +``` + +这种方式相对更加标准,但是如果worker并不是一个js文件,而是ts文件,并且还夹杂一些第三方的包,这种方式是有可能会失败,本人测试是这样的,所以推荐一开始的方式,也就是带有查询后缀的导入。 + +在打包的时候将其实所用到引入的依赖合并成一个文件,如果打开开发者工具,可以在源代码面板的右侧线程中看到主线程,以及worker线程。 + +## 参考文章 + +[使用 Web Workers - Web API 接口参考 | MDN (mozilla.org)](https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Using_web_workers) + +[Web Worker 使用教程 - 阮一峰的网络日志 (ruanyifeng.com)](https://www.ruanyifeng.com/blog/2018/07/web-worker.html) diff --git "a/blog/develop/nest.js \346\267\273\345\212\240 swagger \345\223\215\345\272\224\346\225\260\346\215\256\346\226\207\346\241\243.md" "b/blog/develop/nest.js \346\267\273\345\212\240 swagger \345\223\215\345\272\224\346\225\260\346\215\256\346\226\207\346\241\243.md" new file mode 100644 index 0000000..30912d4 --- /dev/null +++ "b/blog/develop/nest.js \346\267\273\345\212\240 swagger \345\223\215\345\272\224\346\225\260\346\215\256\346\226\207\346\241\243.md" @@ -0,0 +1,235 @@ +--- +slug: /nest-swagger-response-data +title: nest.js 添加 swagger 响应数据文档 +date: 2023-07-18 +authors: kuizuo +tags: [nest, swagger] +keywords: [nest, swagger] +description: nest.js 添加 swagger 响应数据文档 +image: https://img.kuizuo.cn/202307180126751.png +--- + + + +## 基本使用 + +通常情况下,在 nest.js 的 swagger 页面文档中的响应数据文档默认如下 + +![](https://img.kuizuo.cn/202307180105813.png) + +此时要为这个控制器添加响应数据文档的话,只需要先声明 数据的类型,然后通过@ApiResponse 装饰器添加到该控制器上即可,举例说明 + +```typescript title="todo.entity.ts" icon='logos:nestjs' +@Entity('todo') +export class TodoEntity { + @Column() + @ApiProperty({ description: 'todo' }) + value: string + + @ApiProperty({ description: 'todo' }) + @Column({ default: false }) + status: boolean +} +``` + +```typescript title="todo.controller.ts" icon='logos:nestjs' + @Get() + @ApiOperation({ summary: '获取Todo详情' }) + @ApiResponse({ type: [TodoEntity] }) + async list(): Promise { + return this.todoService.list(); + } + + + @Get(':id') + @ApiOperation({ summary: '获取Todo详情' }) + @ApiResponse({ type: TodoEntity }) + async info(@IdParam() id: number): Promise { + return this.todoService.detail(id); + } +``` + +此时对应的文档数据如下显示 + +![image-20230718012234692](https://img.kuizuo.cn/202307180122728.png) + +如果你想要自定义返回的数据,而不是用 entity 对象的话,可以按照如下定义 + +```typescript title="todo.model.ts" icon='logos:nestjs' +export class Todo { + @ApiProperty({ description: 'todo' }) + value: string + + @ApiProperty({ description: 'todo' }) + status: boolean +} +``` + +然后将 `@ApiResponse({ type: TodoEntity })` 中的 `TodoEntity` 替换 `Todo` 即可。 + +## 自定义返回数据 + +然而通常情况下,都会对返回数据进行一层包装,如 + +```json +{ + "data": [ + { + "name": "string" + } + ], + "code": 200, + "message": "success" +} +``` + +其中 data 数据就是原始数据。要实现这种数据结构字段,首先定义一个自定义类用于包装,如 + +```typescript title="res.model.ts" +export class ResOp { + @ApiProperty({ type: 'object' }) + data?: T + + @ApiProperty({ type: 'number', default: 200 }) + code: number + + @ApiProperty({ type: 'string', default: 'success' }) + message: string + + constructor(code: number, data: T, message = 'success') { + this.code = code + this.data = data + this.message = message + } +} +``` + +接着在定义一个拦截器,将 data 数据用 ResOp 包装,如下拦截器代码如下 + +```typescript title="transform.interceptor.ts" icon='logos:nestjs' +export class TransformInterceptor implements NestInterceptor { + constructor(private readonly reflector: Reflector) {} + + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe( + map(data => { + const response = context.switchToHttp().getResponse() + response.header('Content-Type', 'application/json; charset=utf-8') + return new ResOp(HttpStatus.OK, data ?? null) + }), + ) + } +} +``` + +此时返回的数据都会转换为 `{ "data": { }, "code": 200, "message": "success" }` 的形式,这部分不为就本文重点,就不赘述了。 + +回到 Swagger 文档中,只需将 `@ApiResponse({ type: TodoEntity })` 改写成 `@ApiResponse({ type: ResOp })`,就可以实现下图需求。 + +![image-20230718012618710](https://img.kuizuo.cn/202307180126751.png) + +## 自定义 Api 装饰器 + +然而对于庞大的业务而言,使用 `@ApiResponse({ type: ResOp })`的写法,肯定不如 `@ApiResponse({ type: TodoEntity })`来的高效,有没有什么办法能够用后者的写法,却能达到前者的效果,答案是肯定有的。 + +这里需要先自定义一个装饰器,命名为 `ApiResult`,完整代码如下 + +```typescript title="api-result.decorator.ts" icon='logos:nestjs' +import { Type, applyDecorators, HttpStatus } from '@nestjs/common' +import { ApiExtraModels, ApiResponse, getSchemaPath } from '@nestjs/swagger' + +import { ResOp } from '@/common/model/response.model' + +const baseTypeNames = ['String', 'Number', 'Boolean'] + +/** + * @description: 生成返回结果装饰器 + */ +export const ApiResult = >({ + type, + isPage, + status, +}: { + type?: TModel | TModel[] + isPage?: boolean + status?: HttpStatus +}) => { + let prop = null + + if (Array.isArray(type)) { + if (isPage) { + prop = { + type: 'object', + properties: { + items: { + type: 'array', + items: { $ref: getSchemaPath(type[0]) }, + }, + meta: { + type: 'object', + properties: { + itemCount: { type: 'number', default: 0 }, + totalItems: { type: 'number', default: 0 }, + itemsPerPage: { type: 'number', default: 0 }, + totalPages: { type: 'number', default: 0 }, + currentPage: { type: 'number', default: 0 }, + }, + }, + }, + } + } else { + prop = { + type: 'array', + items: { $ref: getSchemaPath(type[0]) }, + } + } + } else if (type) { + if (type && baseTypeNames.includes(type.name)) { + prop = { type: type.name.toLocaleLowerCase() } + } else { + prop = { $ref: getSchemaPath(type) } + } + } else { + prop = { type: 'null', default: null } + } + + const model = Array.isArray(type) ? type[0] : type + + return applyDecorators( + ApiExtraModels(model), + ApiResponse({ + status, + schema: { + allOf: [ + { $ref: getSchemaPath(ResOp) }, + { + properties: { + data: prop, + }, + }, + ], + }, + }), + ) +} +``` + +其核心代码就是在 `@ApiResponse` 上进行扩展,这一部分代码在官方文档: [advanced-generic-apiresponse](https://docs.nestjs.com/openapi/operations#advanced-generic-apiresponse) 中提供相关示例,这里我简单说明下: + +`{ $ref: getSchemaPath(ResOp) }` 表示原始数据,要被“塞”到那个类下,而第二个参数 `properties: { data: prop }` 则表示 `ResOp` 的 `data` 属性要如何替换,替换的部分则由 `prop` 变量决定,只需要根据实际需求构造相应的字段结构。 + +由于有些类没有被任何控制器直接引用, SwaggerModule `SwaggerModule` 还无法生成相应的模型定义,所以需要 `@ApiExtraModels(model)` 将其额外导入。 + +此时只需要将 `@ApiResponse({ type: TodoEntity })` 改写为 `@ApiResult({ type: TodoEntity })`,就可达到最终目的。 + +不过我还对其进行扩展,使其能够返回分页数据格式,具体根据实际数据而定,演示效果如下图: + +![image-20230718023729609](https://img.kuizuo.cn/202307180237658.png) + +## 导入第三方接口管理工具 + +通过上述的操作后,此时记下项目的 swagger-ui 地址,例如 [http://127.0.0.1:5001/api-docs](http://127.0.0.1:5001/api-docs), 此时再后面添加`-json`,即 [http://127.0.0.1:5001/api-docs-json ](http://127.0.0.1:5001/api-docs-json) 所得到的数据便可导入到第三方的接口管理工具,就能够很好的第三方的接口协同,接口测试等功能。 + +![image-20230718022612215](https://img.kuizuo.cn/202307180226265.png) + +![image-20230718022446188](https://img.kuizuo.cn/202307180224284.png) diff --git "a/blog/develop/node\344\270\216\346\265\217\350\247\210\345\231\250\344\270\255\347\232\204cookie.md" "b/blog/develop/node\344\270\216\346\265\217\350\247\210\345\231\250\344\270\255\347\232\204cookie.md" new file mode 100644 index 0000000..af6a7f8 --- /dev/null +++ "b/blog/develop/node\344\270\216\346\265\217\350\247\210\345\231\250\344\270\255\347\232\204cookie.md" @@ -0,0 +1,393 @@ +--- +slug: cookie-of-node-and-browser +title: node与浏览器中的cookie +date: 2020-12-10 +authors: kuizuo +tags: [node, axios, cookie] +keywords: [node, axios, cookie] +--- + + + +## 前言 + +记录一下自己在 nodejs 中使用 http 请求库 axios 中的一些坑(针对 Cookie 操作) + +不敢说和别人封装的 axios 相比有多好,但绝对是你能收获到 axios 的一些知识,话不多说,开始 + +## 封装 + +一般而言,很少有裸装使用 axios 的,就我涉及的项目来说,我都会将 axios 的 request 封装成一个函数使用,接着在 api 目录下,引用该文件。项目结构一般是这样的: + +``` +|-- src + |-- api + |-- user.js + |-- goods.js + |-- utils + |-- request.js +``` + +#### request.js + +```js +import axios from 'axios' + +var instance = axios.create({ + baseURL: process.env.API, // node环境变量获取的Api地址 + withCredentials: true, // 跨域携带Cookies + timeout: 5000, +}) +// 设置请求拦截器 +instance.interceptors.request.use( + config => { + // 在config可以添加自定义协议头 例如token + config.headers['x-token'] = 'xxxxxxxx' + + return config + }, + error => { + Promise.error(error) + }, +) + +instance.interceptors.response.use( + response => { + const res = response.data + // 根据对应的业务代码 对返回数据进行处理 + + return res + }, + error => { + const { response } = error + // 状态码为4或5开头则会报错 + // 根据根据对应的错误,反馈给前端显示 + if (response) { + if (response.status == 404) { + console.log('请求资源路径不存在') + } + return Promise.reject(response) + } else { + // 断网...... + } + }, +) + +export default instance +``` + +实际上,上面那样的封装就够了,相对于的业务代码就不补充了,如果你的宿主环境是浏览器的话,很多东西你就没必要在折腾的,甚至下面的文章都没必要看(不过还是推荐你看看,会有帮助的)。不过没完,再看看 api 里怎么使用的 + +#### api/user.js + +```js +import request from '@/utils/request' + +export function login(data) { + return request({ + url: '/user/login', + method: 'post', + data, + }) +} + +export function info() { + return request({ + url: '/user/info', + method: 'get', + }) +} + +export function logout() { + return request({ + url: '/user/logout', + method: 'post', + }) +} +``` + +看来很简单,没错,就是这么简单,由于是运行在浏览器内的,所以像 cookies,headers 等等都没必要设置,浏览器会自行携带该有的设置,其实想设置也设置不了,主要就是浏览器内置跨域问题。[XMLHttpRequest](https://fetch.spec.whatwg.org/#concept-header-name) + +就这?感觉你写的跟别人没什么区别啊 + +别急,下面才是重头戏。也是我为啥标题只写 axios,而不写 vue-axios 或者 axios 封装的原因。 + +## 踩坑 Cookies 获取与设置 + +### 浏览器 + +运行环境在浏览器中,axios 是无法设置与获取 cookie,获取不到 set-cookies 这个协议头的(即使服务器设置了也没用),先看代码与输出 + +```js +instance.interceptors.request.use(config => { + config.headers['cookie'] = 'cookie=this_is_cookies;username=kuizuo;' + console.log('config.headers', config.headers) + return config +}) + +instance.interceptors.response.use(response => { + console.log('response.headers', response.headers) + return res +}) +``` + +控制台结果: + +![image-20201210060704240](https://img.kuizuo.cn/image-20201210060704240.png) + +首先,就是圈的这个,浏览器是不许允许设置一些不安全的协议头,例如 Cookie,Orgin,Referer 等等,即便你看到控制台 config.headers 确实有刚刚设置 cookie,但我们输出的也只是 headers 对象,在 Network 中找到这个请求,也同样看不到 Cookie 设置的(这就不放图了)。 + +同样的,通过响应拦截器中输出的 headers 中也没有 set-cookies 这个字样。网络上很多都是说,添加这么一行代码 `withCredentials: true`,确实,但是没说到重点,都没讲述到怎么获取 cookies 的,因为在**浏览器环境中 axios 压根就获取不到 set-cookies 这个协议头**,实际上 axios 就没必要,因为浏览器会自行帮你获取服务器返回的 Cookies,并将其写入在 Storage 里的 Cookies 中,再下次请求的时候根据同源策略携带上对应的 Cookie。 + +![image-20201210061627824](https://img.kuizuo.cn/image-20201210061627824.png) + +要获取也很简单,vue 中通过`js-cookie`模块即可,而在 electron 中通过`const { session } = require('electron').remote` (electron 可以设置允许跨域,好用)有关更多可以自行查看文档。 + +那我就是想要设置 Cookies,来跳过登录等等咋办,我的建议是别用浏览器来伪装 http 请求。跨域是浏览器内不可少的一部分,并且要允许跨域过于麻烦。有关跨域,我推一篇文章[10 种跨域解决方案(附终极大招)](https://juejin.cn/post/6844904126246027278) + +
+ 完整封装代码 + +```js +import axios from 'axios' +import { MessageBox, Message } from 'element-ui' +import store from '@/store' +import { getToken } from '@/utils/auth' + +const service = axios.create({ + baseURL: process.env.VUE_APP_BASE_API, + withCredentials: true, + timeout: 5000, +}) + +service.interceptors.request.use( + config => { + if (store.getters.token) { + config.headers['x-token'] = getToken() + } + + return config + }, + error => { + Message.error(error) + return Promise.reject(error) + }, +) + +service.interceptors.response.use( + response => { + const res = response.data + if (res.code !== 200) { + Message.error(res.msg || 'Error') + + return Promise.reject(new Error(res.msg || '未知错误')) + } else { + return res + } + }, + error => { + if (error.response) { + let res = error.response + switch (res.status) { + case 400: + Message.error(res.msg || '非法请求') + break + case 401: + MessageBox.alert('当前登录已过期,请重新登录', '提示', { + confirmButtonText: '重新登录', + type: 'warning', + }).then(() => { + store.dispatch('user/logout').then(() => { + location.reload() + }) + }) + case 403: + Message.error(res.msg || '非法请求') + router.push('/401') + case 404: + Message.error(res.msg || '请求资源不存在') + break + case 500: + Message.error(res.msg || '服务器开小差啦') + break + default: + Message.error(res.msg || res.statusText) + } + } else { + Message.error(res.msg || '请检查网络连接状态') + } + + return Promise.reject(error) + }, +) + +export default service +``` + +
+ +### Nodejs + +作为 nodejs 的主流 http 框架怎么能只用在浏览器上,nodejs 自然可以,不过 nodejs 需要配置的可就多了,在 nodejs 环境中,自然没有浏览器的同源策略,像上面设置不了的 Cookie,现在随便设置,先看看我是怎么封装的: + +```js +import axios from 'axios' +import * as http from 'http' +import * as https from 'https' + +export async function request(opt) { + let { url, method = 'get', headers = {}, cookies, data = null } = opt + + headers['User-Agent'] = + 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36' + headers['Referer'] = url + + if (typeof cookies === 'object') { + headers['Cookie'] = Object.keys(cookies) + .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(cookies[k])) + .join('; ') + } else if (typeof cookies === 'string') { + headers['Cookie'] = cookies + } + + let options = { + url: url, + method: method, + headers: headers, + data: queryString.stringify(data), + httpAgent: new http.Agent({ keepAlive: true }), + httpsAgent: new https.Agent({ + keepAlive: true, + rejectUnauthorized: false, + }), + timeout: 5000, + } + + try { + let res = await this.axios.request(options) + + return res + } catch (e) { + console.log(e) + return e.message + } +} +``` + +```js +// test.js +const request = require('./request'); + +function test() { + let url = 'https://passport2.chaoxing.com/fanyalogin'; + let data = { + fid: '-1', + uname: '15212345678', + password: 'a12345678', + refer: 'http%253A%252F%252Fi.mooc.chaoxing.com', + t: 'true', + }; + let headers = {}; + let cookies = 'username=kuizuo;uid=123;'; + let res = await request({ + url: url, + data, + headers, + cookies, + }); + console.log('test -> res.headers', res.headers); + return res.data; +} + +test(); +``` + +测试一下,顺便抓一下包,看看请求包 + +```http +GET /fanyalogin HTTP/1.1 +Accept: application/json, text/plain, */* +User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36 +Referer: https://passport2.chaoxing.com/fanyalogin +Cookie: username=kuizuo;uid=123; +Host: passport2.chaoxing.com +Connection: keep-alive +Content-Length: 100 + +.... +``` + +有我们自定义的 Cookie,在看看响应的协议头 + +```js +test -> res.headers { + server: 'Tengine', + date: 'Thu, 10 Dec 2020 00:24:15 GMT', + 'content-type': 'text/html', + 'content-length': '1852', + connection: 'keep-alive', + vary: 'Accept-Encoding', + 'set-cookie': [ + 'JSESSIONID=4365A6B9FD8E0CBADDBDD7E7DA468F7E; Path=/; HttpOnly', + 'route=b2eda164bddd148142a54809ef404926;Path=/' + ], + 'accept-ranges': 'bytes', + etag: 'W/"1852-1606444212000"', +} +``` + +同样能获取到 set-cookie,设置与获取都是这么 so easy ,不同于上面浏览器的配置。 + +这里我要说明一些东西,在封装代码中有个 httpAgent 与 httpsAgent,你可以字面翻译就是 http 代理,设置它用来干嘛呢,其中有这么个属性 `keepAlive: true` ,如果设置了协议头中的将会有 `Connection: keep-alive`,而不设置则 `Connection: close`,这里也不想过多说明 http 相关知识,如果只是请求一次,那么两者没有太大区别 + +然而如果我请求一次,过一会(几秒内)又要请求了,那么 keep-alive 一次连接就可以处理多个请求,而 close 则是一次请求后就断开,下次就需要再次连接。说白了就是快一点,而 close 需要不断连接,断开,自然而然就慢。一般来说设置 keep-alive 就对了。 + +其中在 httpsAgent 中,还有一个属性`rejectUnauthorized: false`,说简单点,就是不抛出验证错误,在抓 nodejs 包的时候,如果不通过设置代理服务器(Fiddler,Charles),而是通过网卡(HTTP Analyzer,Wireshark)就会抛出异常,一般就会出现这种错误。 + +``` +Error: unable to verify the first certificate +``` + +然而问题就来了,服务端的返回的 set-cookie 该怎么保存。如果只是涉及客户端层面的,想写一个模拟 http 请求的,直接将获取到的 cookies 与原有的 cookie 合并即可。我那时候的代码就是这样: + +```js +let newCookie = res.header['set-cookie'] + ? res.header['set-cookie'] + .map(a => { + return a.split(';')[0] + }) + .join('; ') + : '' + +// mergeCookie 就是将两者cookie 拼接而成 +let newCookies = mergeCookie(cookies, newCookie) + +res[cookie] = newCookies +return res +``` + +然后返回响应中携带 res.cookies 即可,下次请求的时候再将其在带上。 + +如果只是,利用 nodejs 来实现类似爬虫,模拟登录,然后利用登录后的 cookie,来获取用户信息。如果不希望手动处理 cookies 的话,我其实还是推荐一个 http 模块,superagent,做一些小爬虫和模拟请求挺好用的,就不做过多介绍了。不过由于 nestjs 中自带 axios 模块,加上需要转发 http 请求,于是我就自行封装了一个 axios。 + +## 总结 + +实际上,axios 会根据当前环境,来创建 xhr 对象(浏览器)还是 http 对象(nodejs),在我那时候都以为 axios 是两个共用的,初学 electron 的时候,一直卡在 http 请求的配置 + +``` + // `adapter` allows custom handling of requests which makes testing easier. + // Return a promise and supply a valid response (see lib/adapters/README.md). + adapter: function (config) { + /* ... */ + }, +``` + +在 axios 中也有这么一段配置,翻看了 lib/adapters 下目录我才瞬间醒悟过来,两者环境是不同的。 + +![image-20201210214055696](https://img.kuizuo.cn/image-20201210214055696.png) + +就我使用而言,在浏览器环境下 axios 处理的特别好,允许设置拦截器处理请求与响应,但在 nodejs 下在处理模拟请求确实不如 Python 的 request 模块,奈何 axios 最大的便携就是能直接在浏览器中,尤大推荐的 http 请求库也是 axios。 + +实际上还涉及到了 nodejs 中转发请求的,再给自己留一个坑。 + +在写这篇文章的时候,我其实都没读过 axios 的源码,说实话,那时候遇到问题,就不应该愚昧的去搜索,去不断尝试,有时候直接通过翻看底层代码,可以一目了然自己所面临问题的解决方式。 diff --git "a/blog/develop/pnpm monorepo\345\256\236\350\267\265.md" "b/blog/develop/pnpm monorepo\345\256\236\350\267\265.md" new file mode 100644 index 0000000..c8a299c --- /dev/null +++ "b/blog/develop/pnpm monorepo\345\256\236\350\267\265.md" @@ -0,0 +1,177 @@ +--- +slug: pnpm-monorepo-practice +title: pnpm monorepo实践 +date: 2022-08-29 +authors: kuizuo +tags: [pnpm, monorepo] +keywords: [pnpm, monorepo] +description: 使用 pnpm monorepo 实践 +--- + +老早老早之前就听过 monorepo(单一代码库) 这个名词,也大致了解其出现的意义与功能。但奈何自己的一些小项目中暂时还用不上多项目存储库,所以迟迟没有尝试使用。 + +但随着越来越多的开源项目使用 monorepo,现在不实践到时候也肯定是要实践的,这次实践也算是为以后的技能先做个铺垫了。 + + + +## 介绍 + +前言铺垫这么多,就举个例子介绍下 monorepo 的应用场景,比如现在有个 UI 组件库的开源项目。 + +既然是组件库,首先肯定要有组件库的代码吧,此外可能还有脚手架(CLI)或是工具库(utils)或者是插件要作为 npm 包发布,等等。 + +如果是传统的开发,每个项目都作为单独的 npm 项目来发布引用,就需要创建多个代码仓库,即**多代码库(multirepos)**。很显然这样在开发以及代码仓库的协同上肯定有弊端,而 monorepo 正是解决这种问题,**将所有的项目在一个代码仓库中,即单一代码库(monorepos)**。 + +这只是 monorepo 的一个应用场景例子,这里有一个更好的例子 [前端工程化:如何使用 monorepo 进行多项目的高效管理](https://juejin.cn/post/7043990636751503390),更多可以参考使用 monorepo 的开源项目来了解。在 [这里](https://pnpm.io/zh/workspaces#%E4%BD%BF%E7%94%A8%E7%A4%BA%E4%BE%8B) 可查看使用了 pnpm 工作空间功能的最受欢迎的开源项目。 + +有篇文章推荐阅读 [5 分钟搞懂 Monorepo - 简书 (jianshu.com)](https://www.jianshu.com/p/c10d0b8c5581) + +这里还有份手册可供阅读 [What is a Monorepo? | Turborepo](https://turborepo.org/docs/handbook/what-is-a-monorepo) + +## 上手实践 + +你可以 clone [https://github.com/kuizuo/monorepo-demo](https://github.com/kuizuo/monorepo-demo) 来查看本文示例代码仓库 + +这里使用 pnpm 的 [workspace](https://pnpm.io/zh/workspaces) 来创建 monorepo 代码仓库,此外目前主流的还有 yarn workspace + [lerna](https://lerna.js.org/),[nx](https://nx.dev/),[turborepo](https://turborepo.org/)等等。 + +### 项目结构 + +pnpm 内置了对单一存储库(也称为多包存储库、多项目存储库或单体存储库)的支持, 你可以创建一个 workspace 以将多个项目合并到一个仓库中。 + +pnpm 要使用 monorepo 的话,需要创建 pnpm-workspace.yaml 文件,其内容如下 + +```YAML +packages: + - 'packages/*' +``` + +其中 packages 为多项目的存放路径(一般为公共代码),pnpm 将 packages 下的子目录都视为一个项目。此外如果项目还有文档或在线演示的项目(这些不作为核心库),放在 packages 有些许不妥,就可以像下面这样来配置 workspace + +```YAML +packages: + - packages/* + - docs + - play +``` + +像一开始所举例的代码仓库的项目结构如下 + +```bash +monorepo-demo +├── package.json +├── packages +│ ├── components # 组件库 +│ │ ├── index.js +│ │ └── package.json +│ ├── cli # CLI +│ │ ├── index.js +│ │ └── package.json +│ ├── plugins # 插件 +│ │ ├── index.js +│ │ └── package.json +│ ├── utils # 工具 +│ │ ├── index.js +│ │ └── package.json +├── docs # 文档 +│ │ ├── index.js +│ │ └── package.json +├── play # 在线演示 +│ │ ├── index.js +│ │ └── package.json +├── pnpm-lock.yaml +└── pnpm-workspace.yaml +``` + +其中 packages 下存放的就是多个项目代码库,假设项目就叫 demo(因为到时候这些包是有可能要发布的,而名字就要保证唯一),那么项目的 package.json 如下演示: + +```json +{ + "name": "@demo/components", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "type": "module", + "license": "ISC", + "dependencies": { + "@packages/utils": "workspace:^1.0.0" + } +} +``` + +### 安装依赖 + +执行`pnpm install` 会自动安装所有依赖(包括 packages 下),所以我们肯定不会傻傻 cd 到每个目录下,然后执行`pnpm install` 来一个个安装依赖。 + +假设现在我要为某个项目添加依赖,例如为 utils 模块添加 lodash 的话,按之前可能会 cd 到 utils 目录执行`pnpm add loadsh` ,其实完全不用,pnpm 提供 `--filter` 选项来指定包安装依赖,命令如下 + +```bash +pnpm --filter +``` + +例如: + +```bash +pnpm -F @demo/utils add lodash +``` + +> `-F`等价于`--filter` + +假设现在写好了 utils 模块,`@demo/components`准备使用 utils 模块,可以按照如下操作 + +```bash +pnpm -F @demo/components add @demo/utils@* +``` + +这个命令表示在`@demo/components`安装`@demo/utils`,其中的`@*`表示默认同步最新版本,省去每次都要同步最新版本的问题。 + +### 启动项目 + +使用**node packages/component** (默认执行 index.js 文件) + +```bash +node packages/components + +``` + +更好的选择是编写 npm scripts 就像下面这样: + +```json + "scripts": { + "test": "vitest", + "dev": "pnpm -C play dev", + "docs:dev": "pnpm run -C docs dev", + "docs:build": "pnpm run -C docs build", + "docs:serve": "pnpm run -C docs serve", + }, +``` + +其中[ -C \](https://pnpm.io/pnpm-cli#-c-path---dir-path) 表示 在 path 下运行 npm 脚本 而不是在当前工作路径下。例如根目录下执行 `npm run docs:dev` 便会执行 `docs/package.json` `dev`脚本,同理`build`和`serve`也是一样。 + +此外更多的可能会在根目录下创建 script 脚本,然后编写(编译,发布)脚本。 + +## Turborepo + +在上面只是介绍了使用 pnpm workspace 来搭建一个 monorepo 的仓库,但很多时候还需要搭配适当的工具来扩展 monorepo, Turborepo 就是其中之一,利用先进的构建技术和思想来加速开发,构建了无需配置复杂的工作。 + +这里就不做介绍,这篇 [🚀Turborepo:发布当月就激增 3.8k Star,这款超神的新兴 Monorepo 方案,你不打算尝试下吗? - 掘金 (juejin.cn)](https://juejin.cn/post/7129267782515949575) 就非常值得推荐阅读。 + +## 总结 + +搭建一个 monorepo 的仓库其实挺简单的,但也并不是什么项目使用 monorepo 就好,想想看,所有项目和依赖都堆积在一起,那么项目启动速度必然不如单项目启动来的快。就比如一个博客项目,就完全不至于将博客细分为文章/评论/搜索等等划分,还不如统一将代码都直接写到 src 目录下。 + +可以说当使用 monorepo 作为项目管理时,每个模块就相当于按照一个 npm 包发布的方式创建,而不是像 src/utils 那么随便了,而开源项目大部分都是要作为 npm 包的方式发布的,使用 monorepo 来管理多个项目自然也就再合适不过了。 + +## 相关文章 + +[5 分钟搞懂 Monorepo - 简书 (jianshu.com)](https://www.jianshu.com/p/c10d0b8c5581) + +[前端工程化:如何使用 monorepo 进行多项目的高效管理](https://juejin.cn/post/7043990636751503390) + +[pnpm workspace](https://pnpm.io/zh/workspaces) + +[🚀Turborepo:发布当月就激增 3.8k Star,这款超神的新兴 Monorepo 方案,你不打算尝试下吗? - 掘金 (juejin.cn)](https://juejin.cn/post/7129267782515949575) diff --git "a/blog/develop/rollup.js\345\210\235\344\275\223\351\252\214.md" "b/blog/develop/rollup.js\345\210\235\344\275\223\351\252\214.md" new file mode 100644 index 0000000..66d2407 --- /dev/null +++ "b/blog/develop/rollup.js\345\210\235\344\275\223\351\252\214.md" @@ -0,0 +1,182 @@ +--- +slug: rollup-js-experience +title: rollup.js 初体验 +date: 2022-10-18 +authors: kuizuo +tags: [rollup, webpack, utils] +keywords: [rollup, webpack, utils] +image: https://img.kuizuo.cn/202312270253535.pnghttps://img.kuizuo.cn/202312270253535.png +--- + +# rollup.js 初体验 + +近期准备写一个工具包 [@kuizuo/utils](https://github.com/kuizuo/utils '@kuizuo/utils'),由于要将其发布到npm上,必然就要兼容不同模块(例如 CommonJS 和 ESModule),通过打包器可以很轻松的将代码分别编译成这不同模块格式。 + +恰好 [rollup 3](https://github.com/rollup/rollup/releases/tag/v3.0.0 'rollup 3') 正式发布,也算是来体验一下。 + + + +### 为什么不是Webpack? + +`rollup` 的特色是 `ES6` 模块和代码 `Tree-shaking`,这些 `webpack` 同样支持,除此之外 `webpack` 还支持热模块替换、代码分割、静态资源导入等更多功能。 + +当开发应用时当然优先选择的是 `webpack`,但是若你项目只需要打包出一个简单的 `bundle` 包,并是基于 `ES6` 模块开发的,可以考虑使用 `rollup`。 + +**`rollup` 相比 `webpack`,它更少的功能和更简单的 api,是我们在打包类库时选择它的原因。**例如本次要编写的工具包就是这类项目。 + +## 支持打包的模块格式 + +目前常见的模块规范有: + +- IFFE:使用立即执行函数实现模块化 例:`(function(){})()` + +- CJS:基于 CommonJS 标准的模块化 + +- AMD:使用 Require 编写 + +- ESM:ES 标准的模块化方案 ( ES6 标准提出 ) + +- UMD:兼容 CJS 与 AMD、IFFE 规范 + +以上 Rollup 都是支持的。 + +## 使用 + +官方有一篇文章 [创建你的第一个bundle](https://rollupjs.org/guide/en/#creating-your-first-bundle '创建你的第一个bundle') ,不过英文文档比较难啃,同时通过命令方式+选项的方式来打包肯定不是工程化想要的。 + +### 配置文件 + +所以这里所演示的是通过 `rollup.config.js` 文件,通过`rollup -c` 来打包。 + +一个示例文件如下 + +```javascript title='rollup.config.js' icon='logos:rollupjs' +export default { + input: 'src/main.js', + output: { + file: 'bundle.js', + format: 'cjs', + }, +} +``` + +执行 `rollup -c` 就会将`main.js` 中所引用到的js代码,通过`commonjs`的方式编写到`bundle.js`,就像这样。 + +```javascript title='bundle.js' icon='logos:javascript' +'use strict' + +var foo = 'hello world!' + +function main() { + console.log(foo) +} + +module.exports = main +``` + +但是更多的情况下,是需要同时打包多个模块格式的包,就可以在output传入数组,例如 + +```javascript title='rollup.config.js' icon='logos:rollupjs' +export default { + input: 'src/main.js', + output: [ + { + file: 'bundle.cjs', + format: 'cjs', + }, + { + file: 'bundle.mjs', + format: 'esm', + }, + ], +} +``` + +便会生成 `bundle.cjs`, `bundle.mjs` 两种不同的模块格式的文件。同时在 `package.json` 中,指定对应模块路径,在引入时,便会根据当前的项目环境去选择导入哪个模块。 + +```javascript title='package.json' icon='logos:nodejs-icon' +{ + "main": "bundle.cjs", + "module": "bundle.mjs" +} +``` + +### 结合rollup插件使用 + +不过更多情况下,rollup需要配置插件来使用。官方插件地址:[rollup/plugins: 🍣 The one-stop shop for official Rollup plugins (github.com)](https://github.com/rollup/plugins 'rollup/plugins: 🍣 The one-stop shop for official Rollup plugins (github.com)') + +比如使用 [rollup-plugin-esbuild](https://github.com/egoist/rollup-plugin-esbuild 'rollup-plugin-esbuild') 插件来使用[esbuild](https://esbuild.docschina.org/ 'esbuild')(也是一个打包器,并且构建非常快)来加快打包速度。可以使用 [@rollup/plugin-babel](https://github.com/rollup/plugins/tree/master/packages/babel '@rollup/plugin-babel') 借助babel,编译成兼容性更强的js代码或者代码转换等等。 + +以下是rollup+插件的配置示例,来源 [antfu/utils/rollup.config.js](https://github.com/antfu/utils/blob/main/rollup.config.js 'antfu/utils/rollup.config.js') ,也作为本次工具包的配置。 + +```javascript title='rollup.config.js' icon='logos:rollupjs' +import esbuild from 'rollup-plugin-esbuild' +import dts from 'rollup-plugin-dts' +import resolve from '@rollup/plugin-node-resolve' +import commonjs from '@rollup/plugin-commonjs' +import json from '@rollup/plugin-json' +import alias from '@rollup/plugin-alias' + +const entries = ['src/index.ts'] + +const plugins = [ + alias({ + entries: [{ find: /^node:(.+)$/, replacement: '$1' }], + }), + resolve({ + preferBuiltins: true, + }), + json(), + commonjs(), + esbuild({ + target: 'node14', + }), +] + +export default [ + ...entries.map(input => ({ + input, + output: [ + { + file: input.replace('src/', 'dist/').replace('.ts', '.mjs'), + format: 'esm', + }, + { + file: input.replace('src/', 'dist/').replace('.ts', '.cjs'), + format: 'cjs', + }, + ], + external: [], + plugins, + })), + ...entries.map(input => ({ + input, + output: { + file: input.replace('src/', '').replace('.ts', '.d.ts'), + format: 'esm', + }, + external: [], + plugins: [dts({ respectExternal: true })], + })), +] +``` + +以下是对应的npm 安装命令 + +```bash +pnpm i -D rollup @rollup/plugin-alias @rollup/plugin-commonjs @rollup/plugin-json @rollup/plugin-node-resolve rollup-plugin-esbuild rollup-plugin-dts +``` + +关于rollup更多使用,不妨参见 [rollup官方文档](https://rollupjs.org/ 'rollup官方文档'),以及一些使用 rollup 来打包的开源项目。 + +## 类似工具 + +类似的工具还有 [webpack.js](https://webpack.js.org/ 'webpack.js'), [esbuild](https://esbuild.github.io/ 'esbuild'), [parceljs](https://parceljs.org/ 'parceljs') + +不过就打包类库而言,并不要求过强的性能,有个相对简单的配置就足以,而 [rollup](https://rollupjs.org/ 'rollup') 正是这样的打包工具。 + +## 相关文章 + +[【实战篇】最详细的Rollup打包项目教程](https://juejin.cn/post/7145090564801691684 '【实战篇】最详细的Rollup打包项目教程') + +[一文带你快速上手Rollup](https://zhuanlan.zhihu.com/p/221968604 '一文带你快速上手Rollup') diff --git "a/blog/develop/\344\273\243\347\240\201\345\244\207\344\273\275\346\226\271\346\241\210.md" "b/blog/develop/\344\273\243\347\240\201\345\244\207\344\273\275\346\226\271\346\241\210.md" new file mode 100644 index 0000000..f58fd1f --- /dev/null +++ "b/blog/develop/\344\273\243\347\240\201\345\244\207\344\273\275\346\226\271\346\241\210.md" @@ -0,0 +1,92 @@ +--- +slug: code-backup +title: 代码备份方案 +date: 2022-05-02 +authors: kuizuo +tags: [随笔, code, backup] +keywords: [随笔, code, backup] +--- + +前段时间因为笔记本不在身边,导致一些本地磁盘代码数据没法直接同步过来。于是就准备把这些年写的代码重新整理一下,谈谈常用备份手段以及我的[最佳实现](#最佳实现) + + + +## 备份手段 + +### 本地硬盘 + +大多数代码的存储方式,方便存取,我通常会新建一个驱动器 **代码 (F:)** 来将所有代码放在此处,可能还会自备一个移动硬盘来存储代码。 + +#### 优点 + +无需网络,保存时间久,**读写数据快**。 + +#### 缺点 + +数据同步不方便,难以做到跨端跨设备共享。万一硬盘出了点问题,代码将难以恢复。 + +### 网盘 + +例如某度网盘,Onedrive 等等,这类备份通常有一定的限制,例如下载限速严重,空间限制,保存期限等等,具体以实际使用网盘为例。 + +在某度网盘中,选择你的想备份的文件夹,是可以做到文件夹自动备份,但对于你项目中的依赖文件(例如 Node 的 node_module)那不小的空间也将备份,但有文件夹数量以及大小限制,如下图,这里只是简单一提,不作为备用手段。 + +![image-20220502153823417](https://img.kuizuo.cn/image-20220502153823417.png) + +#### 优点 + +相比本地硬盘而已,**网盘更易分享**,在其他设备中只需要登录网盘账号或访问网盘所分享的链接。 + +#### 缺点 + +有些免费的网盘,必然有一定的限制,例如空间限制,下载限制,远不如本地硬盘。如果不付费的话,体验效果堪忧。 + +### 代码托管平台 + +代码托管平台有很多,例如 Github、Gitee 等等,甚至可以自建一个像 GItLab 的代码托管平台。 + +#### 优点 + +**代码实时性强**,并且基于 Git 版本管理工具可以很方便查看代码的历史操作,对于项目类的而言非常方便。 + +#### 缺点 + +对于一些公有项目而言,一些私密信息(密码)不易于上传,在其他设备拉取代码就不存在这部分数据。同时**对于大量代码就束手无策**。 + +## 最佳实现 + +介绍完上面的几种代码备份手段,各自都有优缺点,至于如何选择就因人而异,这里就说说我是如何备份代码的。 + +### 全部代码 + +对于全部代码而言,肯定是多备份远优于不备份的。所以我通常会在本地电脑硬盘中备份两份代码,一份就正常放在固态硬盘上,另一份则放在机械硬盘,同时再备份一份代码在网盘上。 + +但机械硬盘与网盘的备份时间一致,都是定期或阶段性的备份(甚至可能会忘记备份),所以这种对代码的实时性要求不高,通常这类代码为学习代码以及工具类相关代码。 + +### 项目代码 + +对于项目代码而言,我是毫不犹豫的选择代码托管平台,使用到版本管理工具 Git,可以很好的查看代码的全部历史记录以及修改追踪能力。易于维护的同时,代码分享与同步也比网盘来的高效。设置好.gitignore 也不会将非必要的文件(依赖文件,打包后的文件,生产环境下的配置文件)上传上去。而 Github 便是我最好的选择,里面存放了或多或少的开源与私有项目,每次在其他设备上只需要登录 github,然后 clone 项目,便可开始 coding。 + +**通常来说备份项目代码就已经足够了,毕竟这类代码往往会有一定的价值性。** + +### 文章笔记备份 + +除了代码备份外,此外笔记也十分重要,毕竟有时候自己写的代码,自己甚至都不一定明白。对于文章数据以及笔记,我通常会使用云端协作平台,这类产品有 notion,wolai,语雀等等,不过我个人还是相对倾向于使用 [notion](https://www.notion.so/),曾经 notion 在国内网络的体验下并不友好,我那时会选择 wolai 平替,但现在 notion 发展的越来越好,尤其对开发者而言,你完全可以在 notion 编写内容,通过 api 方式将文章展示给他人访问, + +此外一些博客文章,就会同步在[个人博客](https://kuizuo.cn/)以及[掘金](https://juejin.im/user/1565318510545901/activities)上,当然博客的静态站点的代码仓库也是存放在[Github](https://github.com/kuizuo/blog)上。 + +### 不必要的代码不要备份 + +其实对于很多代码都没备份的必要,例如我在安卓学习的时候,涉及到的刷机包(少说 2 个 g),以及各种 apk。完全可以直接备份其下载地址,而不是选择备份。 + +像临时用脚手架创建的工程文件或是下载别人的代码,这类通常就临时使用(甚至不会再打开第二次),完全没有必要备份理由。 + +### 请压缩后在备份 + +如果不压缩文件夹,备份时将逐个读取文件特别耗时,同时压缩完代码还可以节省一定的空间。也许在一开始备份时会相对麻烦,但在上传与下载以及多次备份时就一举两便。 + +## 最后 + +**永远不要嫌备份麻烦,当你辛辛苦苦写的代码丢失时,那才是真正的麻烦。** + +养成定期备份代码的习惯,因为你永远不知道什么突发情况会导致代码丢失。 diff --git "a/blog/develop/\344\275\277\347\224\250Github Action\350\207\252\345\212\250\345\214\226\351\203\250\347\275\262.md" "b/blog/develop/\344\275\277\347\224\250Github Action\350\207\252\345\212\250\345\214\226\351\203\250\347\275\262.md" new file mode 100644 index 0000000..7c84cab --- /dev/null +++ "b/blog/develop/\344\275\277\347\224\250Github Action\350\207\252\345\212\250\345\214\226\351\203\250\347\275\262.md" @@ -0,0 +1,208 @@ +--- +slug: use-github-action-to-auto-deploy +title: 使用Github Action自动化部署 +date: 2022-05-11 +authors: kuizuo +tags: [github, git] +keywords: [github, git] +--- + +如果有写过项目的经历,就免不了将代码上传到服务器上,安装依赖,然后输入启动命令的步骤。但是有的项目往往需要经常性的改动,如果还是照着上面的方式进行部署的话。先不说这样操作的效率,操作个几次就想罢工了。并且上面这样操作的往往容易误操作。而 Github Actions 正是该问题的良药。 + + + +## 介绍 + +Github Actions 是 Github 提供的免费自动化构建实现,特别适用于持续集成和持续交付的场景,它具备自动化完成许多不同任务的能力,例如构建、测试和部署等等。 + +## 概念 + +在进行操作前,先对 Github Actions 基础知识进行补充,具体可查看 [GitHub Actions 入门教程 阮一峰](https://www.ruanyifeng.com/blog/2019/09/getting-started-with-github-actions.html) + +可以在 [GitHub Marketplace · Actions to improve your workflow](https://github.com/marketplace?type=actions) 中找到所有的 Actions。 + +## 实例:将 VIte 项目发布到 GitHub Pages + +第一步:创建一个 Vite 工程,可在[官网](https://cn.vitejs.dev/guide/#scaffolding-your-first-vite-project)中查看如何安装 + +``` +pnpm create vite +``` + +选择对应的项目名(vite-project)与模板(vue-ts) + +第二步:打开`package.json`文件,加一个`homepage`字段,表示该应用发布后的根目录(参见[官方文档](https://create-react-app.dev/docs/deployment#building-for-relative-paths))。 + +``` +"homepage": "https://[username].github.io/vite-project", +``` + +上面代码中,将`[username]`替换成你的 GitHub 用户名。 + +第三步:在这个仓库的`.github/workflows`目录,生成一个 workflow 文件,名字可以随便取,这个示例是`ci.yml`。 + +workflow 文件如下 + +```yml +name: Build and Deploy +on: + push: + branches: + - master +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install and Build + run: | + yarn install + yarn run build + + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + with: + personal_token: ${{ secrets.ACCESS_TOKEN }} + publish_dir: ./dist +``` + +上面这个 workflow 文件的要点如下 + +1. 整个流程在`master`分支发生`push`事件时触发。 +2. 只有一个`job`,运行在虚拟机环境`ubuntu-latest`。 +3. 第一步是获取源码,使用的 action 是`actions/checkout`。 +4. 第二步是安装依赖与构建,`yarn install`和`yarn run build` +5. 第三步是部署到 Github Page 上,使用的 action 是 [peaceiris/actions-gh-pages@v3](https://github.com/marketplace/actions/github-pages-action)。其中需要设置 secrets.ACCESS_TOKEN + +第四步:将项目上传置 Github 仓库中, + +该 peaceiris/actions-gh-pages 支持三种 Token,这里使用 personal_token,其生成教程在[官方文档](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token)中有详细图文,这里就不贴其生成的图了。**不过记得权限过期以及勾选上 workflow** + +:::tip Tip + +token 只会在生成的时候显示一次,如需要再次显示,则可以点击,但使用此令牌的任何脚本或应用程序都需要更新! + +::: + +然后在**Settings -> Secrets -> Actions 中 New repository secret**中便可添加 secret。 + +![image-20220511122017247](https://img.kuizuo.cn/image-20220511122017247.png) + +这时候只要一调用 git push,就会触发对应的 workflows 文件配置。点击 Actions 便可看到 jobs 工作。 + +![image-20220511122420135](https://img.kuizuo.cn/image-20220511122420135.png) + +此时访问https://kuizuo.github.io/vite-project就可呈现vite项目(不过我已经把仓库给关闭了),但进入会白屏,控制台提示 + +![image-20220511122914534](https://img.kuizuo.cn/image-20220511122914534.png) + +很显然,需要静态资源请求的路径错了,正确的应该是https://kuizuo.github.io/vite-project/assets/index.2435d274.js,根据Vite中的[构建生产版本](https://www.vitejs.net/guide/build.html#public-base-path) 通过命令行参数 `--base=/vite-project/` + +稍加操作在 Install and Build 加上 base 参数 + +``` + - name: Install and Build + run: | + yarn install + yarn run build --base=/vite-project/ +``` + +git push 后,稍等片刻再次访问便可得到如下页面 + +![image-20220511125536189](https://img.kuizuo.cn/image-20220511125536189.png) + +## FTP发布到自有服务器上 + +那么现在在 Github Page 上搭建好了,但还要将编译后的文件还可以通过 FTP 协议添加自己的服务器上,这里我就以我的博客为例。 + +在服务器中开启 FTP,并添加一个用户名,密码以及根目录(这里我问选择为项目目录) + +workflow 要做的就是新建一个 steps,这里选用 [FTP-Deploy-Action](https://github.com/SamKirkland/FTP-Deploy-Action),以下是我的完整配置内容 + +```yml +name: FTP Deploy + +on: [push] + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Use Node.js 16 + uses: actions/setup-node@v3 + with: + node-version: '16.x' + + - name: Build Project + run: | + yarn install + yarn run build + + - name: FTP Deploy + uses: SamKirkland/FTP-Deploy-Action@4.0.0 + with: + server: ${{ secrets.ftp_server }} + username: ${{ secrets.ftp_user }} + password: ${{ secrets.ftp_pwd }} + local-dir: ./build/ + server-dir: ./ +``` + +相信第一个实例中的 workflow 应该已经明白了,其中 ftp_server,ftp_user,ftp_pwd 都是私密信息,所以需要 New repository secret 设置这三个变量。 + +但由于 build 下存在大量文件夹与文件,所以 FTP 速度上传速度堪忧,最终耗时 17 minutes 38.4 seconds。这里只是作为 FTP 演示。 + +## SCP发布到自有服务器上 + +FTP 传输文件着实过慢,所以可以通过 SCP 的方式来传输文件,这里用到了[ssh deploy · Actions](https://github.com/marketplace/actions/ssh-deploy),以下是示例 + +```yaml +name: ci + +on: + push: + branches: + - main + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Use Node.js 16 + uses: actions/setup-node@v3 + with: + node-version: '16.x' + + - name: Build Project + run: | + yarn install + yarn run build + + - name: SSH Deploy + uses: easingthemes/ssh-deploy@v2.2.11 + env: + SSH_PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} + ARGS: '-avzr --delete' + SOURCE: 'build' + REMOTE_HOST: ${{ secrets.REMOTE_HOST }} + REMOTE_USER: 'root' + TARGET: '/www/wwwroot/blog' +``` + +其中 **PRIVATE_KEY** 为服务器SSH登录的私钥,**REMOTE_HOST** 就是服务器的ip地址。当然,这些参数也都作为私密信息,也是要通过New repository secret来设置的。 + +## 总结 + +从上面的演示便可看出 Github Actions 的强大,但其实我挺早之前就了解到它能做这些事情,但迟迟没有动手尝试一番,因为这些自动化操作用人工也是能完成的。也许当时的我认为,用人工所花费的时间远比自动化操作的学习时间来的长,可又随着自己的个人应用增加,每次都需要手动发布,而此时前者的时间已远远大于后者,所以才会想去学习。 + +明知该技术是一定会接触的,为何不趁现在去了解学习呢? diff --git "a/blog/develop/\344\275\277\347\224\250JSONPath\350\247\243\346\236\220json\346\225\260\346\215\256.md" "b/blog/develop/\344\275\277\347\224\250JSONPath\350\247\243\346\236\220json\346\225\260\346\215\256.md" new file mode 100644 index 0000000..fcd9df6 --- /dev/null +++ "b/blog/develop/\344\275\277\347\224\250JSONPath\350\247\243\346\236\220json\346\225\260\346\215\256.md" @@ -0,0 +1,453 @@ +--- +slug: use-jsonpath-to-parse-json-data +title: 使用JSONPath解析json数据 +date: 2021-09-20 +authors: kuizuo +tags: [javascript, json, node] +keywords: [javascript, json, node] +description: jsonpath 能够帮助我们快速的从json数据中提取想要的数据 +image: /img/blog/jsonpath.png +sticky: 3 +--- + +之前学习爬虫的时候,如果是 HTML 的数据,通过 xpath 或是 css 选择器,就能很快的获取我们想要的数据,如果是 json 有没有类似 xpath 这种,能够直接根据条件定位数据,而不需要自行 json 解析在遍历获取。答案是有的,也就是 JSONPath。 + + + +在线测试网址 [JSONPath 在线验证](https://www.jsonpath.cn/) + +所选用的环境是 Node + JavaScript,用到 jsonpath 这个包 [jsonpath - npm (npmjs.com)](https://www.npmjs.com/package/jsonpath) + +> 参考链接 [JsonPath - 根据表达式路径解析 Json - 简书 (jianshu.com)](https://www.jianshu.com/p/8c0ade82891b) + +## 基本语法 + +### 过滤器表达式 + +通常的表达式格式为:`[?(@.age > 18)]` 表示当前节点属性 age 大于 18 + +| 操作符 | 描述 | +| ------- | ---------------------------------------------------------------- | +| `==` | 等于符号,但数字 1 不等于字符 1(note that 1 is not equal to ‘1’) | +| `!=` | 不等于符号 | +| `<` | 小于符号 | +| `<=` | 小于等于符号 | +| `>` | 大于符号 | +| `>=` | 大于等于符号 | +| `=~` | 判断是否符合正则表达式,例如[?(@.name =~ /foo.*?/i)] | +| `in` | 所属符号,例如[?(@.size in [‘S’, ‘M’])] | +| `nin` | 排除符号 | +| `size` | size of left (array or string) should match right | +| `empty` | 判空 Null 符号 | + +语法就这些,不过单单有语法,不实践肯定是不够的。下面就是一些官方简单例子操作,还有一个终极实战 + +## 代码演示 + +```js +var jp = require('jsonpath') + +var cities = [ + { name: 'London', population: 8615246 }, + { name: 'Berlin', population: 3517424 }, + { name: 'Madrid', population: 3165235 }, + { name: 'Rome', population: 2870528 }, +] + +var names = jp.query(cities, '$..name') + +// [ "London", "Berlin", "Madrid", "Rome" ] +``` + +如果使用 js 来遍历的话,也简单 + +```js +let names = cities.map(c => c.name) +``` + +这个数据可能还没那么复杂,在看看下面这个例子,代码来源于https://goessner.net/articles/JsonPath + +```json +{ + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 19.95 + } + } +} +``` + +| JsonPath | Result | +| --- | --- | +| `$.store.book[*].author` | 所有 book 的 author 节点 | +| `$..author` | 所有 author 节点 | +| `$.store.*` | store 下的所有节点,book 数组和 bicycle 节点 | +| `$.store..price` | store 下的所有 price 节点 | +| `$..book[2]` | 匹配第 3 个 book 节点 | +| `$..book[(@.length-1)]`,或 `$..book[-1:]` | 匹配倒数第 1 个 book 节点 | +| `$..book[0,1]`,或 `$..book[:2]` | 匹配前两个 book 节点 | +| `$..book[?(@.isbn)]` | 过滤含 isbn 字段的节点 | +| `$..book[?(@.price<10)]` | 过滤`price<10`的节点 | +| `$..*` | 递归匹配所有子节点 | + +对应的语法可直接到在 JSONPath 在线验证网站上进行测试。要提一点的是,jsonpath 是支持使用 || 与 && 进行过滤的,比如上面要获取 category 为 fiction,price 大于 10 的语法为`$..book[?(@.price>10 && @.category=="fiction")]` 结果如下 + +```json +[ + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } +] +``` + +## 终极实战 + +也许你会觉得上面的例子太过简单了,可能没达到你预期所想要的效果,甚至还不如使用 json 遍历呢,下面我列举一个是我实战中遇到的例子(实际上这样的例子特别多),我先把部分数据展示出来(删除部分没用到的参数,实际参数远比这多),然后通过 js 遍历,以及 jsonpath 来获取我想要的数据。 + +### 结构 + +![image-20210919194116296](https://img.kuizuo.cn/image-20210919194116296.png) + +### 数据 + +```json +{ + "role": "unit", + "children": [ + { + "role": "section", + "children": [ + { + "role": "node", + "children": [ + { + "summary": "{\"indexMap\": {}, \"questionsList\": []}", + "role": "group", + "tab_type": "text", + "name": "Learning objectives", + "scoreDetail": [], + "id": "u1g2", + "url": "u1g2", + "tags": [] + } + ], + "suggestedDuration": "0", + "name": "1-1 Learning objectives", + "block_id": "90ed499f91084e2aa1b7032d2e4ecd76", + "url": "u1g1", + "tags": [] + } + ], + "name": "Learning objectives", + "tags": [] + }, + { + "role": "section", + "children": [ + { + "role": "node", + "children": [ + { + "role": "node", + "children": [ + { + "role": "group", + "tab_type": "task", + "name": "Practice-1", + "scoreDetail": [0], + "id": "u1g6", + "url": "u1g6", + "tags": [] + }, + { + "role": "group", + "tab_type": "task", + "name": "Practice-2", + "scoreDetail": [1, 1, 1, 1], + "id": "u1g7", + "url": "u1g7", + "tags": [] + }, + { + "role": "group", + "tab_type": "task", + "name": "Practice-3", + "scoreDetail": [1, 1, 1, 1, 1], + "id": "u1g544", + "url": "u1g544", + "tags": [] + }, + { + "role": "group", + "tab_type": "task", + "name": "Practice-4", + "scoreDetail": [1, 1, 1, 1, 1], + "id": "u1g9", + "url": "u1g9", + "tags": [] + } + ], + "name": "Practice", + "block_id": "f6768dc9474746b9ba071e7f211534d9", + "url": "u1g5", + "tags": [] + } + ], + "suggestedDuration": "0", + "name": "1-2 Sharing", + "block_id": "1c97a87a9feb4a8aa7d6ed39482d866d", + "url": "u1g3", + "tags": [] + }, + { + "role": "node", + "children": [ + { + "role": "group", + "tab_type": "video", + "name": "Get the skills", + "scoreDetail": [], + "id": "u1g16", + "url": "u1g16", + "tags": [] + }, + { + "role": "node", + "children": [ + { + "role": "group", + "tab_type": "task", + "name": "Use the skills-1", + "scoreDetail": [0, 0], + "id": "u1g615", + "url": "u1g615", + "tags": [] + }, + { + "role": "group", + "tab_type": "task", + "name": "Use the skills-2", + "scoreDetail": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "id": "u1g18", + "url": "u1g18", + "tags": [] + }, + { + "role": "group", + "tab_type": "task", + "name": "Use the skills-3", + "scoreDetail": [0, 0, 0, 0, 0, 0, 0], + "id": "u1g19", + "url": "u1g19", + "tags": [] + } + ], + "name": "Use the skills", + "block_id": "2d8a81799bcc44ccab2646b613557b2b", + "url": "u1g17", + "tags": [] + }, + { + "role": "node", + "children": [ + { + "role": "group", + "tab_type": "task", + "name": "Think and speak", + "scoreDetail": [0], + "id": "u1g21", + "url": "u1g21", + "tags": [] + } + ], + "name": "Think and speak", + "block_id": "5833925c8c5e4ddab7a114b15d610983", + "url": "u1g20", + "tags": [] + } + ], + "suggestedDuration": "0", + "name": "1-3 Listening", + "block_id": "681817aaf75845468e464e1a8d82f2c8", + "url": "u1g14", + "tags": [] + }, + { + "role": "node", + "children": [ + { + "role": "node", + "children": [ + { + "role": "group", + "tab_type": "task", + "name": "Get a clue", + "scoreDetail": [0, 0, 0], + "id": "u1g25", + "url": "u1g25", + "tags": [] + } + ], + "name": "Get a clue", + "block_id": "b332335ab3554dffb92afcae5f815b5a", + "url": "u1g24", + "tags": [] + }, + { + "role": "node", + "children": [ + { + "role": "group", + "tab_type": "task", + "name": "View it-1", + "scoreDetail": [1, 1, 1, 1], + "id": "u1g27", + "url": "u1g27", + "tags": [] + }, + { + "role": "group", + "tab_type": "task", + "name": "View it-2", + "scoreDetail": [1, 1, 1, 1, 1, 1, 1], + "id": "u1g545", + "url": "u1g545", + "tags": [] + }, + { + "role": "group", + "tab_type": "task", + "name": "View it-3", + "scoreDetail": [1, 1, 1, 1, 1, 1, 1, 1], + "id": "u1g29", + "url": "u1g29", + "tags": [] + } + ], + "name": "View it", + "block_id": "a2ecf6464d5f480e98242ebe4431a73b", + "url": "u1g26", + "tags": [] + }, + { + "role": "node", + "children": [ + { + "role": "group", + "tab_type": "task", + "name": "Think and speak", + "scoreDetail": [0, 0], + "id": "u1g31", + "url": "u1g31", + "tags": [] + } + ], + "name": "Think and speak", + "block_id": "120d0784e63c414793f5e648c416144b", + "url": "u1g30", + "tags": [] + } + ], + "suggestedDuration": "0", + "name": "1-4 Viewing", + "block_id": "765a1be83ac5437aaca8fa150ad5af2e", + "url": "u1g22", + "tags": [] + } + ], + "name": "Listening to the world", + "tags": [] + } + ] +} +``` + +### 需求 + +可以看到数据比一开始的例子复杂了可不是一点,不过先别管这些数据是干啥的,说说需求,从结构上也能看出来,是有很多`children`嵌套的,而需求就是**获取`role`为`group`的`children`节点数据** + +### js 实现遍历 + +先说说 js 如何实现的,我贴一下对应的代码(当时项目的代码,稍微修改的一点),可自己粘贴运行一下。 + +```js +let groupList = [] +for (const node of json.children ?? []) { + if (node.role == 'group') groupList.push({ ...node }) + + for (const group of node.children ?? []) { + if (group.role == 'group') groupList.push({ ...group }) + + for (const child of group.children ?? []) { + if (child.role == 'group') groupList.push({ ...child }) + + let children4 = child.children ?? [] + for (const child of children4) { + if (child.role == 'group') groupList.push({ ...child }) + } + } + } +} +console.log(groupList) +``` + +因为这些数据中,是存在不确定性的,也就是在当前节点下,二级节点可能有`children`,而其他节点下的二级很可能没有 `children`,所以我在这边就加上 `?? []` (Typescript 中的`??`语法,你可以把 `??` 当做 `||` )来判断是否有`children`节点,有些读者可能会思考,为啥不用递归呢。说的是挺轻松的,但是递归是很容易出问题的,万一爬取到后台数据进行了一些修改,很有可能对于的递归算法将失效,甚至导致堆栈溢出,所以我这边值循环 4 级`chilren`节点(实际遇到的貌似也只有 4 级,谁又能保证爬取到数据就一定只有 4 级呢)。 + +### jsonpath 获取 + +于是了解到 jsonpath 后,我第一个时间就开始分析这样的数据,果不其然,得到了我想要的结果 ⬇️ + +![image-20210919200826079](https://img.kuizuo.cn/image-20210919200826079.png) + +语法:`$..children[?(@.role=="group")]` + +语法意思很明确,根节点下遍历所有`children`节点,同时`role`等于`group`,呈现的效果如上图。 + +而回到需求,**就是获取`role`为`group`的`children`节点数据**,而 jsonpath 就帮我轻松实现我想要的效果。 + +## 最终思考 + +实际上这样的需求我已经不止遇到一次,二次了,然而我寻求百度与群友的时候,给我的结果都不尽人意。但都没有提及到 jsonpath 来进行获取。也许是我的搜索方式有问题,但千篇一律都是 js 如何解析多层 json,以及遍历所有的子元素,虽然这些办法确实能解决我的问题,但每次遇到这种数据,都需要花上长时间去编写对应的逻辑。 + +在回想起当时爬取 HTML 页面数据的时候(数据与上面展示的差不多,都是树结构多层),而我只接触到了正则表达式,没了解过 CSS 选择器与 xpath。怎么办,为了实现目的,只好用现有的技术去实现,于是编写一个正则表达式就花费了近一个下午的时间,而使用 CSS 选择器 10 分钟不到就达到目的。没想到竟然有这么好用的方法,早知道多去了解点技术了。可能现在的心情和当时一样,只不过 HTML 换成了 JSON,编辑器还是那个编辑器,而我依旧还是我 + +也许这就是编程,也许这就是人生。 diff --git "a/blog/develop/\345\211\215\347\253\257\346\265\213\350\257\225.md" "b/blog/develop/\345\211\215\347\253\257\346\265\213\350\257\225.md" new file mode 100644 index 0000000..495a8b4 --- /dev/null +++ "b/blog/develop/\345\211\215\347\253\257\346\265\213\350\257\225.md" @@ -0,0 +1,121 @@ +--- +slug: frontend-automated-testing +title: 前端测试 +date: 2022-10-06 +authors: kuizuo +tags: [frontend, test] +keywords: [frontend, test] +image: https://img.kuizuo.cn/v2-45d641f2191559d4eff581d0607efd61_1440w.jpg +--- + +如果你的项目要长期使用并维护的话,那么代码自动测试就非常有必要使用。因为没人能保证在修改代码后,不会引发其他额外 bug(功能失效,渲染失败),而在修改完代码后,跑一遍测试就能很大程度让开发者发现自己所修改的代码是否存在问题,是否会导致原有功能失效。 + +尤其是在其他人接手这个项目时,诱发 bug 的概率自然也就更高(因为他有很大的可能不知道这部分代码的上下文的功能用途),所以这也就是为什么很多开源项目与大型企业的公司都会使用自动化测试,以及要求一定的代码覆盖率。 + +当然如果项目不是长期维护的,那么完全没必要编写测试代码,这么做无疑是在浪费开发者的时间。 + + + +## 适合引入自动化测试的场景 + +提前简单总结下**适合引入自动化测试的场景(优点)**: + +- 中长期项目迭代/重构(需要频繁的修改代码) + +- 准确定位代码问题,提高代码质量 + +- 引用了不可控的第三方依赖,极易发生 bug(例:beta 版相关的包) + +测试的目的在于,**及时发现错误,提高代码质量和开发效率,避免存在 BUG 的代码发布上线造成损失**。 + +自动化测试要注意的点 + +- 并不是所有项目都适合引入自动化测试,反而会增加一定代码成本 + +- 如果项目开发阶段还不稳定,那么手动测试效率会比自动化测试更好 + +- 有些代码可能这辈子都不会在碰第二次,就没有编写自动化测试的意义 + +**在代码编写阶段,建议只对重点功能进行测试,没必要一定追求过高的测试覆盖率**。注意,是编写阶段 + +## 测试思想 + +### TDD:Test-Driven Development(测试驱动开发) + +- TDD:Test-Driven Development(测试驱动开发):TDD 则要求在编写某个功能的代码之前先编写测试代码,然后只编写使测试通过的功能代码,通过测试来推动整个开发的进行 + +### BDD:Behavior-Driven Development(行为驱动开发) + +- BDD:Behavior-Driven Development(行为驱动开发):BDD 可以让项目成员(甚至是不懂编程的)使用自然语言来描述系统功能和业务逻辑,从而根据这些描述步骤进行系统自动化的测试 + +## 自动化测试类型 + +测试类型有以下几种: + +- **单元测试(Unit Testing)** + + 代码中多个组件共用的工具类库、多个组件共用的子组件等。通常情况下,在公共函数/组件中一定要有单元测试来保证代码能够正常工作。单元测试也应该是项目中数量最多、覆盖率最高的。 + +- **集成测试(Integration Testing)** + + 测试经过单元测试后的各个模块组合在一起是否能正常工作。会对组合之后的代码整体暴露在外接口进行测试,查看组合后的代码工作是否符合预期。集成测试是安全感较高的测试,能很大程度提升开发者的信心,集成测试用例设计合理且测试都通过能够很大程度保证产品符合预期。 + +- **UI 测试 (UI Testing)** + + 对于前端的测试,是脱离真实后端环境的,仅仅只是将前端放在真实环境中运行,而后端和数据都应该使用 Mock 的。 + +- **端对端测试(End-to-End Testing)** + + 将整个应用放到真实的环境中运行,包括数据在内也是需要使用真实的。 + +关于测试框架,我主要使用 [Vitest](https://vitest.dev/) 与 [Cypress](https://cypress.io/)。这两个作为测试框架都相对比较新,并且性能与开发上会比 [Jest](https://jestjs.io/),[Puppeteer](https://pptr.dev/) 来的好。本文的一些测试示例也是基于这两类框架之上。 + +:::note 其实还有个接口测试,不过这就不是前端要关心的内容了,所以就没列举在这上面。 + +::: + +## 自动化测试的误区 + +自动化测试和普通说的测试是有些不大一样的,有很多测试,其实都不能归类为前端自动化测试。这里我会举个例子来说明一下。 + +在自动化测试来说有个要求:**自动化测试要的不是某次测试执行的是否通过,而是每次执行都必须通过。** + +怎么理解这句话呢:比方说我要测试获取博客列表的函数,假设实际的接口失效了,那么就会导致结果与预期不一致,就会导致代码测试不通过。既然不通过,那我就要去查看为什么不通过。当我点击这个单元测试的时,发现原来是后端接口失效了。可万一哪天这个接口突然好了,又或者发现刚刚原来没插网线导致的请求失败导致测试不通过。像这些 **不稳定因素** 在前端自动化测试中就会使用 mock 的方式,强制返回一定格式的数据给测试框架。到这里你可能会好奇,为什么要这么做? + +想想看,如果因为接口失效导致测试失败,是因为测试代码的问题吗?那跟测试代码有毛关系,明显是后端或者服务器的问题。我们要测试的是**获取博客列表的函数,而不是在测试接口(接口自动化测试)**。测试接口不应该是前端要做的事情。确保后端返回正确的响应结果,前端能够对这些数据进行处理渲染,这才是我们要做的。 + +**每次测试都存在不可控的因素,就会导致每次测试结果都有可能不同,这就违背测试的意义了。** 所以这也就是为什么要数据 mock 的原因了。 + +**给测试输入的值,在经过测试后,要保证输出的值与我们预期想要结果的值相同。** + +## 自动化测试到底在测试什么? + +其实目前前端有个尴尬的点,目前绝大部分实际业务项目里,前端的单元测试都没啥鸟用,UI 自动测试又太难搞。 + +这就导致很多开发者不清楚到底要测试什么,导致对测试特别不重视,包括我一开始也是如此。看到很多文章都是在演示测试 1+1 =2,介绍测试框架,很少从实际项目中出发进行测试。不过原因无非就是实际项目写的少,就别说测试代码了。再不然就是写过的代码都不怎么维护(重构,阅读),自然的就不会去写测试了。 + +不过确实没什么好举例的,因为太多东西可以写成单元测试了,比方说`formatTime.test.ts`, `param2Obj.test.ts`,`validate.test.ts`,从文件名就知道在测试什么了,就看开发者想不想写的问题了。 + +可以到 [vitest-dev/vitest](https://github.com/vitest-dev/vitest 'vitest-dev/vitest') / [facebook/jest](https://github.com/facebook/jest) 等测试框架中的 example 中查看测试案例。 + +关于 UI 测试和 e2e 测试,我非常推荐看看 cypress 的[Todo 示例](https://example.cypress.io/todo 'Todo示例'),测试的特别清楚,这里放张官方测试结果供参考。 + +![](https://img.kuizuo.cn/image_a_B5FPFfJI.png) + +这里补充一句,vitest 是能做 UI 测试的,可以通过 [vuejs/test-utils](https://github.com/vuejs/test-utils 'vuejs/test-utils') 库来实现,但是 vitest 的运行环境是 nodejs,通过 jsdom 等库来模拟浏览器环境,而 cypress 是实实在在的运行在浏览器上,而且有可视化页面操作。这两者的区别也就是运行时环境的区别,有些实际场景对真实环境是有需求的,所以针对 UI 测试更多会选择像 cypress 这种直接运行在浏览器的测试框架。 + +## 为何我开始重视起测试? + +在之前我根本不会在意测试,就连已有的测试代码我都不会尝试运行。就在前段时间我正重构我的一个项目时,但当我写了一大部分的代码后,我尝试运行发现有些功能失效了。于是我进一步的排查,终于找到我修改代码并还原成我原来的代码。 + +假设一开始有份完整的测试代码,当我修改一部分代码后,跑一遍测试查看测试情况。发现没问题后,再开始下一步的代码工作,反复测试,直到最终重构完毕。**与其浪费代码的时间,不如将这些时间去用来完善测试代码**。不仅自己后续使用需要,到时候项目交付给别人的时,别人也不至于修改你的代码时兢兢业业。 + +**究其原因是为了保证代码质量**。 + +当然,虽说重视,但我也不会立马为已有的项目增加测试.耗时且费力不讨好。更多时候只会在准备重构的项目,或者是新项目上去增加测试代码。 + +编写这篇文章主要解惑我自己对往常对测试的看法,也借此机会养成 TDD 模式的开发的习惯。 + +## 参考文章 + +[试试前端自动化测试!(基础篇) - 掘金 (juejin.cn)](https://juejin.cn/post/6844904194600599560) diff --git "a/blog/develop/\345\237\272\344\272\216Axios\345\260\201\350\243\205HTTP\347\261\273\345\272\223.md" "b/blog/develop/\345\237\272\344\272\216Axios\345\260\201\350\243\205HTTP\347\261\273\345\272\223.md" new file mode 100644 index 0000000..efb4e6e --- /dev/null +++ "b/blog/develop/\345\237\272\344\272\216Axios\345\260\201\350\243\205HTTP\347\261\273\345\272\223.md" @@ -0,0 +1,206 @@ +--- +slug: axios-http-class-library +title: 基于Axios封装HTTP类库 +date: 2021-08-26 +authors: kuizuo +tags: [node, http, axios] +keywords: [node, http, axios] +description: 基于 Axios 封装 HTTP 类库,并发布到 npm 仓库中 +--- + + + +一个基于 Axios 封装 HTTP 类库 + +源代码 [kz-http](https://github.com/kuizuo/kz-http) + +## 使用方法 + +npm 安装 + +```bash +npm i kz-http -S +``` + +### 请求 + +```javascript +import Http from 'kz-http' + +let http = new Http() + +http.get('https://www.example.com').then(res => { + console.log(res) +}) +``` + +## 能解决什么 + +axios 明明那么好用,为啥又要基于 axios 重新造一个轮子。首先不得否认的是 axios 确实好用,Github 能斩获近 90k 的 star,且基本已成为前端作为数据交互的必备工具。但是它对我所使用的环境下还是存在一定的问题,也就是我为什么要重新造一个轮子。 + +### Node 环境下无法自动封装 Set-Cookie + +如果 axios 是运行在浏览器那还好说,就算你无论怎么请求,浏览器都会自动将你的所有请求中的响应包含 set-cookie 参数,提供给下一次同域下的请求。但是,Node 环境并不是浏览器环境,在 Node 环境中运行并不会自动保存 Cookie,还需要手动保存,并将 Cookie 添加至协议头给下一个请求。(如果是 Python 的话,request 有个 session 方法可以自动保存 cookie,十分方便) + +一开始我是自行封装,将响应中的 set-cookie 全都存在实例对象 http.cookies 上,但封装的不彻底,如果有的网站 + +间请求存在跨域,那么会将携带不该属于该域下的 Cookies。于是乎,我在 github 仓库找到了一个库可达到我的目的 + +[3846masa/axios-cookiejar-support: Add tough-cookie support to axios. (github.com)](https://github.com/3846masa/axios-cookiejar-support) + +具体安装可以直接点击链接查看,这里贴下我**之前**的封装代码 + +```javascript +const tough = require('tough-cookie'); +const axiosCookieJarSupport = require('axios-cookiejar-support').default; +axiosCookieJarSupport(axios); + +class Http { + public cookieJar; + public instance: AxiosInstance; + construction() { + this.cookieJar = new tough.CookieJar(null, { allowSpecialUseDomain: true }); + this.instance = axios.create({ + jar: this.cookieJar, + ignoreCookieErrors: false, + withCredentials: true, + }); + } +} +``` + +这样 axios 就会自动将响应中的 set-cookie 封装起来,供下次使用 + +但是正是由于导入了这个包,导致每次请求都需要处理,就会导致请求速度变慢,实测大约是在 100ms 左右,同时导入这个包之后,实例化的对象都将会携带对应 cookies,想要删除又得对应 Url,于是决定自行封装相关代码可查看 request 方法,实测下来大约有 10ms 左右的差距(前提都通过创建实例来请求),不过有个缺陷,我封装的代码是不进行同源判断的,如何你当前站点请求的是 api1.test.com,获取到 cookie1,那么请求 api2.test.com 的时候也会将 cookie1 携带,这边不做判断是不想在请求的时候耗费时间,比如网页与手机协议,一般这种情况建议实例化两个对象,如 + +```javascript +let http_api1 = new Http() +let http_api2 = new Http() +``` + +### 请求失败无法自动重试 + +在高并发的情况下,偶尔会出现请求超时,请求拒绝的情况,但是默认下 axios 是不支持自动重试请求的,不过可以借助插件`axios-retry`来达到这个目的 + +```javascript +const axiosRetry = require('axios-retry') + +class Http { + constructor(retryConfig?) { + this.instance = axios.create() + + if (retryConfig) { + axiosRetry(this.instance, { + retries: retryConfig.retry, // 设置自动发送请求次数 + retryDelay: (retryCount) => { + return retryCount * retryConfig.delay // 重复请求延迟 + }, + shouldResetTimeout: true, // 重置超时时间 + retryCondition: (error) => { + if (axiosRetry.isNetworkOrIdempotentRequestError(error)) { + return true + } + + if (error.code == 'ECONNABORTED' && error.message.indexOf('timeout') != -1) { + return true + } + if (['ECONNRESET', 'ETIMEDOUT'].includes(error.code)) { + // , 'ENOTFOUND' + return true + } + return false + }, + }) + } + } +} +``` + +这边判断重新发送请求条件是连接拒绝,连接重置,和连接超时的情况。 + +### 配置拦截器 + +有时候一个网站的协议是这样的,每一条 Post 都自动将所有参数进行拼接,然后进行 MD5 加密,并添加为 sign 参数,于是,不得不给每一条请求都进行这样的操作,那么有没有什么能在每次请求的时候,都自动的对参数进行 MD5 加密。如果使用过 axios 来配置过 JWT 效验,那自然就会熟悉给每条请求协议头都携带 JWT 数值。同样的,这里的加密例子同样使用,具体配置实例对象 http 的请求拦截器即可,如 + +```javascript +let http = new Http() + +// axios实例instance是公开的 +http.instance.interceptors.request.use( + config => { + // 执行每条请求都要处理的操作 + return config + }, + error => {}, +) +``` + +同样的,响应拦截器也同理,例如请求返回的响应都进行加密处理,那么就可以通过响应拦截器进行统一解密,这里就不做过多描述,具体场景具体分析。 + +### 封装一些常用方法 + +比如设置伪造 IP(setFakeIP),自动补全 referer 和 orgin 参数,禁止重定向等等,更详细的查看源码便可 + +## 发布 npm 包 + +如果要让别人使用的话,总不可能让他去下载源码然后编译吧,这里就借助 npm。 + +:::tip + +在使用 npm 之前,请先使用`npm install -g npm@latest`升级为最新版,否则可能会提示 **ERR! 426 Upgrade Required**。原文 [The npm registry is deprecating TLS 1.0 and TLS 1.1 | The GitHub Blog](https://github.blog/2021-08-23-npm-registry-deprecating-tls-1-0-tls-1-1/) + +::: + +创建 npm 账号,创建 package.json + +```json title="package.json" +{ + "name": "kz-http", + "version": "0.1.0", + "description": "An HTTP class library based on axios", + "main": "dist/index.js", + "scripts": { + "build": "tsc" + }, + "author": "kuizuo", + "license": "ISC", + "dependencies": { + "axios": "^0.21.1", + "axios-retry": "^3.1.9" + }, + "devDependencies": { + "typescript": "^4.3.5" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/kuizuo/kz-http.git" + }, + "keywords": ["node", "axios", "http"] +} +``` + +然后通过`npm login`登录 npm 账号,接着输入`npm publish --access public`发布即可 + +发布的是要注意以下几点 + +- 如果 npm 镜像必须是官方的,否则无法登录,镜像还原 + + ```bash + npm config set registry https://registry.npmjs.org/ + ``` + + 查看镜像配置地址 + + ```bash + npm get registry + ``` + +- 如果包有重名,那么就无法发布,就必须要要改名 + +- 邮箱必须要验证(会接受一条下图邮箱),不然就会发布失败 ![image-20210826212258752](https://img.kuizuo.cn/image-20210826212258752.png) + +- **请勿随意删包,否则同名的包将需要 24 小时后才能发布(亲测)** + + > npm ERR! 403 403 Forbidden - PUT http://registry.npmjs.org/kz-http - kz-http cannot be republished until 24 hours have passed. + +发布完成后,别人只需要通过`npm i kz-http`就可成功将模块下载至本地 node_modules 文件夹下 diff --git "a/blog/develop/\345\274\200\346\272\220\350\256\270\345\217\257\350\257\201.md" "b/blog/develop/\345\274\200\346\272\220\350\256\270\345\217\257\350\257\201.md" new file mode 100644 index 0000000..ea92a54 --- /dev/null +++ "b/blog/develop/\345\274\200\346\272\220\350\256\270\345\217\257\350\257\201.md" @@ -0,0 +1,158 @@ +--- +slug: about-open-source-license +title: 关于开源许可证 +date: 2022-05-05 +authors: kuizuo +tags: [open-source, git] +keywords: [open-source, git] +--- + +虽然知道开源有个许可证 LICENSE,但一直没给自己写的一些开源项目选择开源许可证。于是准备系统了解一下开源许可证,以及如何为 Github 项目添加 LICENSE。 + + + +### OSI(Open Source Initiative) + +即开发源代码组织,是一个旨在推动开源软件发展的非盈利组织。可以在 [Open Source Initiative](https://opensource.org/licenses/alphabetical) 中查看所有的开源协议。 + +## 开源许可证 + +关于开源许可证的简单区别 + +![img](https://www.ruanyifeng.com/blogimg/asset/201105/bg2011050101.png) + +至于如何选择,下图更加通俗易懂 + +![快速选择协议](https://img.kuizuo.cn/2019-04-29-072557.png) + +其中开源许可证可分为两大类 + +### 宽松式(permissive)许可证 + +宽松式许可证(permissive license)是最基本的类型,对用户几乎没有限制。用户可以修改代码后闭源。 + +它有三个基本特点。 + +**(1)没有使用限制** + +用户可以使用代码,做任何想做的事情。 + +**(2)没有担保** + +不保证代码质量,用户自担风险。 + +**(3)披露要求(notice requirement)** + +用户必须披露原始作者。 + +#### 常见许可证 + +常见的宽松式许可证有四种。它们都允许用户任意使用代码,区别在于要求用户遵守的条件不同。 + +**(1)BSD(二条款版)** + +分发软件时,必须保留原始的许可证声明。 + +**(2) BSD(三条款版)** + +分发软件时,必须保留原始的许可证声明。不得使用原始作者的名字为软件促销。 + +**(3)MIT** + +分发软件时,必须保留原始的许可证声明,与 BSD(二条款版)基本一致。 + +**(4)Apache 2** + +分发软件时,必须保留原始的许可证声明。凡是修改过的文件,必须向用户说明该文件修改过;没有修改过的文件,必须保持许可证不变。 + +不难看出这类许可证要求相对宽松,市面上大部分的开源项目主要以 MIT 和 Apache 两者为主。使用 MIT 协议开源项目如 vue,react,bootstrap,vscode,electron,axios,terminal 等等,作为大多数开发者而言,MIT 无法是最好的选择。 + +### Copyleft 许可证 + +Copyleft 是[理查德·斯托曼](https://www.ruanyifeng.com/blog/2005/03/post_112.html)发明的一个词,作为 Copyright (版权)的反义词。 + +Copyright 直译是"复制权",这是版权制度的核心,意为不经许可,用户无权复制。作为反义词,Copyleft 的含义是不经许可,用户可以随意复制。 + +但是,它带有前提条件,比宽松式许可证的限制要多。 + +> - 如果分发二进制格式,必须提供源码 +> - 修改后的源码,必须与修改前保持许可证一致 +> - 不得在原始许可证以外,附加其他限制 + +上面三个条件的核心就是:修改后的 Copyleft 代码不得闭源。 + +#### 常见许可证 + +常见的 Copyleft 许可证也有四种(对用户的限制从最强到最弱排序)。 + +**(1)Affero GPL (AGPL)** + +如果云服务(即 SAAS)用到的代码是该许可证,那么云服务的代码也必须开源。 + +**(2)GPL** + +如果项目包含了 GPL 许可证的代码,那么整个项目都必须使用 GPL 许可证。 + +**(3)LGPL** + +如果项目采用动态链接调用该许可证的库,项目可以不用开源。 + +**(4)Mozilla(MPL)** + +只要该许可证的代码在单独的文件中,新增的其他文件可以不用开源。 + +> 参考文章 +> +> [开源许可证教程 - 阮一峰的网络日志 (ruanyifeng.com)](https://www.ruanyifeng.com/blog/2017/10/open-source-license-tutorial.html) +> +> [如何选择开源许可证? - 阮一峰的网络日志 (ruanyifeng.com)](https://www.ruanyifeng.com/blog/2011/05/how_to_choose_free_software_licenses.html) +> +> [五分钟看懂开源协议](https://juejin.cn/post/6844903925863153672) + +## Github 项目添加 LICENSE + +Github 官方专门制作了一个网站 [Choose a License](https://choosealicense.com/) 帮助大家选择合适的开源,License。中文版也有 [选择一个开源许可证](https://choosealicense.rustwiki.org/)。不过我更推荐下面在 Github 仓库页中来新增 LICENSE。 + +在仓库页中,Add file-> Create new file + +![image-20220505190634653](https://img.kuizuo.cn/image-20220505190634653.png) + +输入 LICENSE(建议大写),右侧将会弹出 Choose a license template,这里我选择 MIT 协议 + +![image-20220505190758791](https://img.kuizuo.cn/image-20220505190758791.png) + +![image-20220505191409696](https://img.kuizuo.cn/image-20220505191409696.png) + +点击 Review and submit,此时就会回到添加文件的地方,并且自动为你填写好 Message,接着点击 Commit new file 即可 + +![image-20220505200951047](https://img.kuizuo.cn/image-20220505200951047.png) + +整个许可证内容如下 + +``` +MIT License + +Copyright (c) 2022 kuizuo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +再次刷新便可看到效果 + +![image-20220505201138476](https://img.kuizuo.cn/image-20220505201138476.png) diff --git "a/blog/develop/\346\234\211\344\272\206 Prisma \345\260\261\345\210\253\347\224\250 TypeORM \344\272\206.md" "b/blog/develop/\346\234\211\344\272\206 Prisma \345\260\261\345\210\253\347\224\250 TypeORM \344\272\206.md" new file mode 100644 index 0000000..54af26c --- /dev/null +++ "b/blog/develop/\346\234\211\344\272\206 Prisma \345\260\261\345\210\253\347\224\250 TypeORM \344\272\206.md" @@ -0,0 +1,445 @@ +--- +slug: with-prisma-dont-use-typeorm +title: 有了 Prisma 就别用 TypeORM 了 +date: 2024-01-13 +authors: kuizuo +tags: [orm, prisma, typeorm] +keywords: [orm, prisma, typeorm] +image: https://img.kuizuo.cn/2024/0113174834-202401131748137.png +--- + +要说 2024 年 Node.js 的 ORM 框架应该选择哪个?毫无疑问选 Prisma。至于为何,请听我细细道来。 + + + +本文面向的对象是饱受 TypeORM 折磨的资深用户(说的便是我自己)。只对这两个 ORM 框架从开发体验上进行对比,你也可以到 [这里](https://www.prisma.io/docs/orm/more/comparisons/prisma-and-typeorm) 查看 Prisma 官方对这两个 ORM 框架的对比。 + +## 整体对比 + +### 更新频率 & 下载量 + +TypeORM 距离上次更新已经过去半年之久了(下图截取自 24 年 1 月 1 日,没想到年初竟然还复活了) + +![Untitled](https://img.kuizuo.cn/2024/0113165614-Untitled.png) + +从下载量以及 star 数来看,如今 Prisma 已经超过 TypeORM,这很大一部分的功劳归功于像 Next.js、Nuxt.js 这样的全栈框架。 + +![Untitled](https://img.kuizuo.cn/2024/0113165632-Untitled%201.png) + +上图来源 [https://npmtrends.com/prisma-vs-typeorm](https://npmtrends.com/prisma-vs-typeorm) + +而在 Nest.js 的 [Discord 社区](https://discord.com/channels/520622812742811698/1156124199874732033) 讨论之中,Prisma 也成为诸多 Nest.js 开发者首选的 ORM 框架,因为它有着更好的开发体验。 + +在大势所趋之下相信你内心已经有一份属于自己的答案。 + +### 文档 & 生态 + +从文档的细致程度上 Prisma 比 TypeORM 要清晰详尽。在 [Get started](https://www.prisma.io/docs/getting-started) 花个数十分钟了解 Prisma 基本使用,到 [playground.prisma.io](https://playground.prisma.io/) 中在线尝试,到 [learn](https://www.prisma.io/learn) 查看官方所提供的免费教程。 + +此外 Prisma 不仅支持 js/ts 生态,还支持其他语言。丰富的[生态](https://www.prisma.io/ecosystem)下,加之 Prisma 开发团队的背后是由商业公司维护,无需担心需求得不到解决。 + +![Untitled](https://img.kuizuo.cn/2024/0113165658-Untitled%202.png) + +## 开发体验对比 + +在从开发体验上对比之前,我想先说说 TypeORM 都有哪些坑(不足)。 + +### findOne(undefined) 所查询到的却是第一条记录 + +首先 TypeORM 有个天坑,你可以在 这个 [Issue](https://github.com/typeorm/typeorm/issues/2500) 中查看详情或查看 [这篇文章](https://pietrzakadrian.com/blog/how-to-hack-your-nodejs-application-which-uses-typeorsm) 是如何破解使用 TypeORM 的 Node.js 应用。 + +当你使用 `userRepository.findOne({ where: { id: null } })` 时,从开发者的预期来看所返回的结果应该为 null 才对,但结果却是大跌眼镜,结果所返回的是 user 表中的第一个数据记录! + +你可能会说,这不是 bug 吗?为何官方还不修。事实上确实是 bug,而事实上官方到目前也还没修复该 bug。再结合上文提到的更新频率,哦,那没事了。 + +目前解决方法则是用 `createQueryBuilder().where({ id }).getOne()` 平替上一条语句或者确保查询参数不为 undefined。从这也可以看的出,TypeORM 在现今或许并不是一个很好的选择。 + +### synchronize: true 导致数据丢失 + +`synchronize` 表示数据库的结构是否和代码保持同步,官方提及到请不要在生产环境中使用,但在开发阶段这也并不是一个很好的做法。举个例子,有这么一个实体 + +```ts title='user.entity.ts' icon='logos:nestjs' +@Entity() +export class User { + @PrimaryGeneratedColumn() + id: number + + @Column() + name: string +} +``` + +当开启了 `synchronize: true`,并且将 `name` 更改为 `title` 时,一旦运行 nest 服务后就会发现原有 `name` 下的数据全都丢失了!如图所示 + +![Untitled](https://img.kuizuo.cn/2024/0113165658-Untitled%203.png) + +因为 TypeORM 针对上述操作的 sql 语句是这样的 + +```sql +ALTER TABLE `user` CHANGE `name` `title` varchar(255) NOT NULL +ALTER TABLE `user` DROP COLUMN `title` +ALTER TABLE `user` ADD `title` varchar(255) NOT NULL +``` + +也就是说,当你在开发环境中,修改某个字段(包括名字,属性)时,该字段原有的数据便会清空。 + +因此针对数据库更新的操作最正确的做法是使用迁移(migrate)。 + +### 接入成本 + +在 Nest 项目中,Prisma 的接入成本远比 TypeORM 来的容易许多。 + +相信你一定有在 `xxx.module.ts` 中在 imports 中导入 `TypeOrmModule.forFeature([xxxEntity])` 的经历。就像下面代码这样: + +```ts title='xxx.module.ts' icon='logos:nestjs' +@Module({ + imports: [TypeOrmModule.forFeature([UserEntity])], + controllers: [UserController], + providers: [UserService], + exports: [TypeOrmModule, UserService], +}) +export class UserModule {} +``` + +对于初学者而言,很大程度上会忘记导入 `xxxEntity`,就会出现这样的报错 + +```bash +Potential solutions: + - Is UserModule a valid NestJS module? + - If "UserEntityRepository" is a provider, is it part of the current UserModule? + - If "UserEntityRepository" is exported from a separate @Module, is that module imported within UserModule? + @Module({ + imports: [ /* the Module containing "UserEntityRepository" */ ] + }) + +Error: Nest can't resolve dependencies of the userService (?). Please make sure that the argument "UserEntityRepository" at index [0] is available in the UserModule context. +``` + +此外这还不是最繁琐的,你还需要再各个 service 中,通过下面的代码来注入 userRepository。 + +```ts title='user.service.ts' icon='logos:nestjs' +@InjectRepository(UserEntity) +private readonly userRepository: Repository +``` + +一旦实体一多,要注入的 Repository 也就更多,无疑不是对开发者心智负担的加深。 + +再来看看 Prisma 是怎么导入的,你可以使用 [nestjs-prisma](https://nestjs-prisma.dev/docs/basic-usage/) 或者按照官方文档中[创建 PrismaService](https://docs.nestjs.com/recipes/prisma#use-prisma-client-in-your-nestjs-services)。 + +然后在 service 上,注入 PrismaService 后,就可以通过 `this.prisma[model]` 来调用模型(实体) ,就像这样 + +```ts title='app.service.ts' icon='logos:nestjs' +import { Injectable } from '@nestjs/common' +import { PrismaService } from 'nestjs-prisma' + +@Injectable() +export class AppService { + constructor(private prisma: PrismaService) {} + + users() { + return this.prisma.user.findMany() + } + + user(userId: string) { + return this.prisma.user.findUnique({ + where: { id: userId }, + }) + } +} +``` + +哪怕创建其他新的实体,只需要重新生成 PrismaClient,都无需再导入额外服务,this.prisma 便能操作所有与数据库相关的 api。 + +### 更好的类型安全 + +Prisma 的贡献者中有 [ts-toolbelt](https://github.com/millsp/ts-toolbelt) 的作者,正因此 Prisma 的类型推导十分强大,能够自动生成几乎所有的类型。 + +而反观 TypeORM 虽说使用 Typescript 所编写,但它的类型推导真是一言难尽。我举几个例子: + +在 TypeORM 中,你需要 select 选择某个实体的几个字段,你可以这么写 + +![Untitled](https://img.kuizuo.cn/2024/0113165658-Untitled%204.png) + +你会发现 post 对象的类型提示依旧还是 postEntity,没有任何变化。但从开发者的体验角度而言,**既然我选择查询 id 和 title 两个字段,那么你所返回的 post 类型应该也只有 id 与 title 才更符合预期**而后续代码中由于允许 post 有 body 属性提示,那么 post.body 为 null 这样不必要的结果。 + +再来看看 Prisma,你就会发现 post 对象的类型提示信息才符合开发者的预期。像这样的细节在 Prisma 有非常多。 + +![Untitled](https://img.kuizuo.cn/2024/0113165658-Untitled%205.png) + +这还不是最关键的,TypeORM 通常需要使用 `createQueryBuilder` 方法来构造 sql 语句来满足开发者所要查询的预期。而当你使用了该方法,你就会发现你所编写的代码与 js 无疑,我贴几张图给大伙看看。 + +![Untitled](https://img.kuizuo.cn/2024/0113165658-Untitled%206.png) + +![Untitled](https://img.kuizuo.cn/2024/0113165658-Untitled%207.png) + +![Untitled](https://img.kuizuo.cn/2024/0113165658-Untitled%208.png) + +这无疑会诱发一些潜在 bug,我就多次因为要 select 某表中的某个字段,却因拼写错误导致查询失败。 + +### 创建实体 + +在 TypeORM 中,假设你要新增一条 User 记录,你通常需要这么做 + +```ts +const newUser = new User() +newUser.name = 'kuizuo' +newUser.email = 'hi@kuizuo.cn' +const user = userRepository.save(newUser) +``` + +当然你可以对 User 实体中做点手脚,像下面这样加一个构造函数 + +```ts title='user.entity.ts' icon='logos:nestjs' +@Entity() +export class User { + @PrimaryGeneratedColumn() + id: number + + @Column({ unique: true }) + username: string + + @Column() + email: string + + constructor(partial?: Partial) { + Object.assign(this, partial) + } +} +``` + +```ts +const newUser = new User({ + name: 'kuizuo', + email: 'hi@kuizuo.cn', +}) +const user = userRepository.save(newUser) +``` + +于是你就可以传递一个 js 对象到 User 实体,而不是 newUser.xxx = xxx 像 Java 版的写法。 + +而要是涉及到多个关联的数据,往往需要先查询到关联数据,然后再像上面这样赋值+保存。这里就不展开了,使用过 TypeORM 的应该深有体会。 + +而在 Prisma 中,绝大多数的操作你都只需要一条代码语句外加一个对象结构,像上述 TypeORM 的操作对应 Prisma 的代码语句如下 + +```ts +const user = await prisma.user.create({ + data: { + name: 'kuizuo', + email: 'hi@kuizuo.cn', + }, +}) +``` + +### 根据条件来创建还是更新 + +在数据库中操作经常需要判断数据库中是否有某条记录,以此来决定是更改该记录还是创建新的一条记录,而在 Prisma 中,完全可以使用 upsert,就像下面这样 + +```ts +const user = await prisma.user.upsert({ + where: { id: 1 }, + update: { email: 'example@prisma.io' }, + create: { email: 'example@prisma.io' }, +}) +``` + +### 聚合函数 + +在 TypeORM 中,假设你需要使用聚合函数来查询的话,通常会这么写 + +```ts +const raw = await this.userRepository + .createQueryBuilder('user') + .select('SUM(user.id)', 'sum') + .getRawOne() + +const sum = raw.sum +``` + +如果只是像上面这样,单纯查询 sum,那么 raw 的值是 `{ sum: 1 }` , 但最要命的就是 `select` 配合 `getRawOne` 还要额外查询 user 实体的属性,所得到的结果就像这样 + +```ts +const raw = await this.userRepository + .createQueryBuilder('user') + .select('SUM(user.id)', 'sum') + .addSelect('user') + .where('user.id = :id', { id: 1 }) + .getRawOne() +``` + +```ts +{ + user_id: 1, + user_name: 'kuizuo', + user_email: 'hi@kuizuo.cn', + sum: '1' +} +``` + +所有 user 的属性都会带有 `user_` 前缀,这看上去有点不是那么合理,但如果考虑要联表查询的情况下,就会存在相同名称的字段,通过添加表名(别名)前缀就可以避免这种情况,这样来看貌似又有点合理了。 + +但还是回到熟悉的类型安全,这里的所返回的 raw 对象是个 any 类型,一样不会有任何提示。 + +而在 Prisma 中,提供了 专门用于聚合的方法 [aggregate](https://www.prisma.io/docs/orm/reference/prisma-client-reference#aggregate),可以特别轻松的实现聚合函数查询。 + +```ts +const minMaxAge = await prisma.user.aggregate({ + _count: { + _all: true, + }, + _max: { + profileViews: true, + }, + _min: { + profileViews: true, + }, +}) +``` + +```ts +{ + _count: { _all: 29 }, + _max: { profileViews: 90 }, + _min: { profileViews: 0 } +} +``` + +--- + +看到这里,你若是长期使用 TypeORM 的用户必定会感同身受如此糟糕的体验。那种开发体验真的是无法用言语来形容的。 + +## Prisma 生态 + +### 分页 + +在 Prisma 你要实现分页,只需要在 prismaClient 继承 [prisma-extension-pagination](https://github.com/deptyped/prisma-extension-pagination) 这个库。就可像下面这样,便可在 model 中使用paginate方法来实现分页,如下代码。 + +```ts +import { PrismaClient } from '@prisma/client' +import { pagination } from 'prisma-extension-pagination' + +const prisma = new PrismaClient().$extends(pagination()) +``` + +```ts +const [users, meta] = prisma.user + .paginate() + .withPages({ + limit: 10, + page: 2, + includePageCount: true, + }); + +// meta contains the following +{ + currentPage: 2, + isFirstPage: false, + isLastPage: false, + previousPage: 1, + nextPage: 3, + pageCount: 10, // the number of pages is calculated + totalCount: 100, // the total number of results is calculated +} +``` + +支持页数(page)或光标(cursor)。 + +:::tip 两种分页的使用场景 + +按页查询: 用于传统分页,例如翻页 + +光标查询: 根据游标进行查询,例如无限滚动 + +::: + +而在 TypeORM 你通常需要自己封装一个 paginate方法,就如下面代码所示(以下写法借用 [nestjs-typeorm-paginate](https://www.npmjs.com/package/nestjs-typeorm-paginate)) + +```ts +async function paginate( + queryBuilder: SelectQueryBuilder, + options: IPaginationOptions, +): Promise> { + const { page, limit } = options + + queryBuilder.take(limit).skip((page - 1) * limit) + + const [items, total] = await queryBuilder.getManyAndCount() + + return createPaginationObject({ + items, + totalItems: total, + currentPage: page, + limit, + }) +} + +// example +const queryBuilder = userRepository.createQueryBuilder('user') +const { items, meta } = paginate(queryBuilder, { page, limit }) +``` + +当然也可以自定义userRepository,为其添加 paginate 方法,支持链式调用。但这无疑增添了开发成本。 + +### 根据 Schema 自动生成数据验证 + +得益于 Prisma 强大的数据建模 dsl,通过 [generators](https://www.prisma.io/docs/orm/prisma-schema/overview/generators) 生成我们所需要的内容(文档,类型),比如可以使用 [zod-prisma-types](https://github.com/chrishoermann/zod-prisma-types) 根据 Schema 生成 [zod](https://github.com/colinhacks/zod) 验证器**。** + +举个例子,可以为 schema.prisma 添加一条 generator,长下面这样 + +```prisma title='prisma.schema' icon='logos:prisma' +generator client { + provider = "prisma-client-js" + output = "./client" +} + +generator zod { + provider = "zod-prisma-types" + output = "./zod" + createModelTypes = true + // ...rest of config +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(uuid()) + email String @unique + name String? +} +``` + +执行构建命令后,这将会自动生成 zod/index.ts 文件,将包含 UserSchema 信息,其中片段代码如下 + +```ts title='zod/index.ts' icon='logos:typescript-icon' +export const UserSchema = z.object({ + id: z.string().uuid(), + email: z.string(), + name: z.string().nullable(), +}) + +export type User = z.infer +``` + +再通过 createZodDto,将 zod 验证器转化为 dto 类,就像下面这样 + +![Untitled](https://img.kuizuo.cn/2024/0113165658-Untitled%209.png) + +当然你可能并不想在 nestjs 项目中使用 zod,而是希望使用传统的 [class-validator](https://www.npmjs.com/package/class-validator) 来编写 dto。可以使用社区提供的 [prisma-class-generator](https://github.com/kimjbstar/prisma-class-generator) 根据已有 model 生成 dto。 + +--- + +合理来说,Prisma 并不是一个传统的 ORM,它的工作原理并不是将表映射到编程语言中的模型类,为处理关系数据库提供了一种面向对象的方式。而是在 Prisma Schema 中定义模型。在应用程序代码中,您可以使用 Prisma Client 以类型安全的方式读取和写入数据库中的数据,而无需管理复杂模型实例的开销。 + +![](https://img.kuizuo.cn/2024/0113171541-202401131715135.png) + +总而言之,你若想要[更好的类型](https://www.prisma.io/docs/orm/prisma-client/type-safety),简洁的[实体声明语法](https://www.prisma.io/docs/orm/prisma-schema/data-model/database-mapping#prismas-default-naming-conventions-for-indexes-and-constraints),况且带有[可视化桌面端应用](https://www.prisma.io/studio),以及更好的[生态完备](https://www.prisma.io/ecosystem),那么你就应该选 Prisma。 + +## 总结 + +在写这篇文章时,我也是彻底的将 Nestjs 项目中由 TypeORM 迁移到 Prisma ,这期间给我最大的变化就是在极少的代码量却又能实现强大的功能。许多涉及多表的 CRUD操作可以通过一条简洁的表达式来完成,而在使用 TypeORM 时,常常需要编写繁琐臃肿的 queryBuilder。 + +TypeORM 有种被 nestjs 深度绑定的模样,一提到 TypeORM,想必第一印象就是 Nestjs 中所用到的 ORM 框架。然而,Prisma 却不同,是一个全能通用的选择,可以在任何的 js/ts 框架中使用。 + +从开发体验的角度不接受任何选择 TypeORM 的反驳,有了更优优秀的选择,便不愿意也不可能在回去了。如果你还未尝试过 Prisma,我强烈建议你亲身体验一番。 diff --git "a/blog/develop/\346\234\215\345\212\241\345\231\250\344\270\216\345\237\237\345\220\215\345\244\207\346\241\210.md" "b/blog/develop/\346\234\215\345\212\241\345\231\250\344\270\216\345\237\237\345\220\215\345\244\207\346\241\210.md" new file mode 100644 index 0000000..ddcff7a --- /dev/null +++ "b/blog/develop/\346\234\215\345\212\241\345\231\250\344\270\216\345\237\237\345\220\215\345\244\207\346\241\210.md" @@ -0,0 +1,117 @@ +--- +slug: server-and-domain-beian +title: 服务器与域名备案 +date: 2020-11-17 +authors: kuizuo +tags: [server, cloud] +keywords: [develop, cloud] +--- + + +## 云服务器 + +服务器说白就是全天 24 小时不停歇的运行一台电脑,同时分配一个公网 IP 给这个电脑,你只需要把你要的资源放置到这台电脑上,别人通过访问该 IP 就能访问到这台电脑的资源,比如你放一个网页部署在这台服务器上,别人访问 这个IP就能访问到网页的内容。 + +你可以根据需求来安装服务器的系统,这些在云服务厂商中都是可以选择的。 + +### 服务器的配置 + +一般来说,一些提供云服务器的厂商都会给新用户一个新用户价,差不多也就是 100 左右一年的云服务器,配置一般都是 2g 内存,1 核,1m 带宽,50g 硬盘说实话对于没有特殊需求的服务器够用了。而不是新用户的价格,这样的配置一个月差不多 70 元,算一下一年打折后 700 元,这还算便宜的了,服务器贵这很正常,全天不间断运行,电费,网费和一些服务费用,其实就已经非常值了。 + +其他几个配置没什么可说的,如果要说服务器哪个配置最贵的话,无疑就是带宽了。1M 的带宽理论上传速度为 128kB/s,也就是我从服务器中下载一个 10m 的软件,需要 80 秒,这还只是理论速度,我实测过平均速度不到 100kB/s。如果搭建网站的话 1m 有些慢,虽说一个页面一般都不会超过 100kb,但背后加载的图片 css js资源可就不只是kb大小了,用户访问网页就需要花费一定时间等待加载,体验非常不好。个人建议带宽5m起步,否则就不建议购买。 + +### 云服务器和轻量应用服务 + +关于服务器的选择很多人不知道云服务器和轻量应用服务的区别,这里两种服务器我都买过,且目前都在役。主要区别和优势请参考下表(腾讯云文档) + +![](https://img.kuizuo.cn/image_CO_V_ghsyo.png) + +更具体的可以查看对应云服务商的介绍 + +[阿里云ECS云服务器和轻量应用服务有什么区别及选择方法-阿里云开发者社区 (aliyun.com)](https://developer.aliyun.com/article/1023850?spm=5176.21213303.J_6704733920.7.432353c9DbykNf\&scm=20140722.S_community@@文章@@1023850._.ID_community@@文章@@1023850-RL_阿里云ecs云服务器和轻量应用服务有什么区别及选择方法-LOC_main-OR_ser-V_2-P0_0 "阿里云ECS云服务器和轻量应用服务有什么区别及选择方法-阿里云开发者社区 (aliyun.com)") + +[轻量应用服务器 与云服务器 CVM 对比-产品简介-文档中心-腾讯云 (tencent.com)](https://cloud.tencent.com/document/product/1207/49819 "轻量应用服务器 与云服务器 CVM 对比-产品简介-文档中心-腾讯云 (tencent.com)") + +**总结:买轻量应用服务器是最实惠的** + +### github学生认证送服务器 + +如果你不想花钱买一个服务器的话,可以考试github学生认证,会送你一个服务器。具体可到官方中查看 [https://education.github.com/experiences/virtual\_event\_kit](https://education.github.com/experiences/virtual_event_kit "https://education.github.com/experiences/virtual_event_kit") + +![](https://img.kuizuo.cn/image_Rsh8Y_TBfe.png) + +### windows 和 Linus 服务器的区别 + +这里可能会说的不对,毕竟我接触服务器相关等配置也没太多时间,但是我觉得有必要说一下,我那时候用 windows 服务器的时候,只要通过 windows**专业版**(一定要专业版才能远程连接别人的电脑)自带的远程桌面(cmd 中输入`mstsc`即可),然后输入 ip 地址,接着在输入相应的账号密码即可,但有可能无法连接,原因是防火墙和 ip 白名单没有配置好,服务器不允许连接。 + +而对于 Linux,用的最多的就是宝塔面板了,但是连接不是通过 windows 远程桌面,而是通过像终端那样连接登录,我一般是用 Xshell 来连接。但是连接完就开始输入命令安装宝塔面板,然后会有对应的面板地址和登录宝塔面板的账号密码,只要访问给定的面板地址加上用户密码即可登录。像对应的界面如下 + +![image-20200918141519472](https://img.kuizuo.cn/image-20200918141519472.png "image-20200918141519472") + +## 选择哪家云服务器厂商 + +目前市场上主要有阿里云和腾讯云的服务器,这两者的服务器质量和操作体验上都属于大厂级别。不分上下,都可以选择购买,不过最好有个原则,你域名在哪购买,服务器就买哪一家的,因为到时候备案是需要服务器才能备案的。 + +# 域名 + +正常来说你访问你一个网站肯定不是访问一个ip地址,而是一个域名,比方说访问kuizuo.cn,baidu.com。但其实访问域名就相当于访问这个ip,过程如下:首先访问域名会经过DNS解析,DNS(域名系统)会找到你要访问的域名所解析的ip,然后访问这个ip。 + +但有遗憾的地方就是很多时候想要注册自己想要的域名非常难,因为已经被别人现行注册了。比如我的现在的kuizuo.cn这个域名是从他人手里购买而来的。所以域名这东西请优先准备好,备案解析啥的完全不急,一年也就是几十块钱,但是你不先买就很有可能给别人先注册,到时候想买都没得买,要不然就要花大价格购买,因此也有很多人去做域名买卖的生意。 + +一般一个域名够用了,需要的话在域名解析中去添加域名的子域名(二级,三级等等),比如我的一些个人项目就是使用二级域名来访问的,这里我也就不在列举了。 + +### 为啥要备案 + +首先备案要提交负责人的身份信息(身份证正反,手持,人脸,手机号,住址等),记录你这个域名内的网站的负责人和单位,主要为了防止在网上从事非法的网站经营活动,打击不良互联网信息的传播,能给予警告和封禁。(网站备案只针对国内服务器) + +总之不要去搞违规违法行为,天网恢恢疏而不漏,网警要找总归有方法。 + +### ICP 备案 + +9 月 3 号购买的腾讯云与域名,然后进行初步的服务器简单部署配置,第二天开始域名实名认证,接着实名后需要 3 天时间才可以进行开始域名备案,等了 3 天开始域名备案提交网站的用途信息等等,然后拍身份证,人脸,手持,接着到 9 月 9 日腾讯云服务器的客服打电话给我要我修改一些信息,比如网站详情写的不行,资料不全,过不了备案等等,然后修改重新提交一次,最终等待收到腾讯云助手的通知,直到 9 月 18 号,如下结果 + +![image-20200918113449612](https://img.kuizuo.cn/image-20200918113449612.png "image-20200918113449612") + +至此 ICP 备案就搞定了,ICP 还算轻松,身份信息真实,来访电话及时接听,等就行了。 + +只要ICP 备案,就可以通过域名 [kuizuo.cn](https://kuizuo.cn "kuizuo.cn") 访问到我的个人博客。(当然前提需要到域名管理中的DNS解析添加) + +![](https://img.kuizuo.cn/image_nuVA2RTh_b.png) + +### 公安备案 + +但还有一个公安备案,虽说不是强制的,但一般都是建议去公安备案一下。我之前的域名kzcode.cn就有公安备案,但现在的kuizuo.cn 并没有。主要原因还是太过于繁琐,比ICP备案复杂多了。 + +首先需要登录 [http://www.beian.gov.cn](http://www.beian.gov.cn "http://www.beian.gov.cn") 注册并且登录填写的信息也比上面的多,如下图 + +![image-20201119191204222](https://img.kuizuo.cn/image-20201119191204222.png "image-20201119191204222") + +填写资料折腾了半小时左右的时间,才提交上去,而且过了快两周后,我收到了如下的短信 + +> 【公安网站服务平台】尊敬的用户:您开办的网站(互联网技术文章分享:kzcode.cn)审核未通过,原因:网站信息检查有误,审核不通过,请尽快登录www\.beian.gov.cn网站,在草稿中修改并重新提交网站备案申请,如有疑问可在工作日(上午8:30-11:30、下午14:00-17:00)联系网警,联系电话:059xxxxxxxx,谢谢您的配合。 + +工作人员电话和我联系是说我户籍转了,需要在我转入地去申请,于是又重新提交改数据,反正就是照着腾讯云的帮助文档填写自己的个人信息,中途有几次就这么磨着磨着到了 11 月 17 号,短信收到了这一条 + +> 【公安网站服务平台】尊敬的用户:您的开办主体已经审核通过,如果存在自动关联的待备案或待认领网站,请尽快核对归属,进行新网站备案以及已备案网站认领的申请。如有疑问可在工作时间(周一至周五上午 9:00-12:00、下午 14:00-17:00)联系网警,联系电话:059xxxxxxxx,谢谢您的配合。 + +然而,我登录了网站却没有看到已备案的网站,于是联系工作人员 ,然后叫我重新提交一次,这次不到半小时就搞定,最终短信结果如下 + +> 【公安网站服务平台】尊敬的用户:您开办的网站(互联网技术文章分享:kzcode.cn)已经审核通过,请尽快登录www\.beian.gov.cn网站,下载备案号码,附在网站底部,如有疑问可在工作日(上午9:00-12:00、下午14:00-17:00)联系网警,联系电话:059xxxxxxxx,谢谢您的配合。 + +然后登录公安备案网站,查看备案结果如下图,帅的不谈! + +![image-20201117152619474](https://img.kuizuo.cn/image-20201117152619474.png "image-20201117152619474") + +### 悬挂备案号 + +如果上面都已经弄好了也别太高兴,要在网页源代码将公安联网备案信息放置在网页底部。简单的说完成两项备案后都需要在网站页面底部显示备案号,并指明转到链接。在我的个人博客页面最下方,就会看到如下图这样。 + +![image-20201117153209037](https://img.kuizuo.cn/image-20201117153209037.png "image-20201117153209037") + +[工信部原文](http://www.gov.cn/gongbao/content/2005/content_93018.htm "工信部原文") 第十三条和第二十五条 + +> **第十三条** 非经营性互联网信息服务提供者应当在其网站开通时在主页底部的中央位置标明其备案编号,并在备案编号下方按要求链接信息产业部备案管理系统网址,供公众查询核对。 + +> **第二十五条** 违反本办法第十三条的规定,未在其备案编号下方链接信息产业部备案管理系统网址的,或未将备案电子验证标识放置在其网站指定目录下的,由住所所在地省通信管理局责令改正,并处5千元以上1万元以下罚款。 + +当然也有很多网站并没有这么做,具体还是需要看网站的性质,是否经营性网站。像个人博客这种,只需要悬挂一个ICP备案号即可,但对于绝大多数的国内网站是肯定悬挂公安备案,并且除了备案信息外,还有一堆相关证件,如营业执照,许可证,资格证等等,国内的网站监管非常严格的。 \ No newline at end of file diff --git "a/blog/develop/\346\250\241\346\213\237\350\257\267\346\261\202 \345\215\217\350\256\256\345\244\215\347\216\260\346\226\271\346\241\210.md" "b/blog/develop/\346\250\241\346\213\237\350\257\267\346\261\202 \345\215\217\350\256\256\345\244\215\347\216\260\346\226\271\346\241\210.md" new file mode 100644 index 0000000..e3d1c44 --- /dev/null +++ "b/blog/develop/\346\250\241\346\213\237\350\257\267\346\261\202 \345\215\217\350\256\256\345\244\215\347\216\260\346\226\271\346\241\210.md" @@ -0,0 +1,249 @@ +--- +slug: request-protocol-scheme +title: 模拟请求|协议复现方案 +date: 2022-09-25 +authors: kuizuo +tags: [http, protocol] +keywords: [http, protocol] +description: 模拟请求/协议复现 最佳实践方案 +--- + +前段时间看了别人的一个写了羊了个羊刷次数网页版,但是 js 代码做了混淆,然后我的那个解混淆的工具还没适配上,短时间内还原不了。但由于是网页版,所以抓包数据还是能看到的,于是就准备复刻了一个。 + +可在此体验:[7y8y.vercel.app](https://7y8y.vercel.app) (当然由于官方改动,现在功能已经失效了,但看看页面到不成问题,可能需要科学上网) + +原本我是不考虑写的,但是这背后所涉及到的技术以及技术框架我是特别想聊聊,加之以后我也有很大的可能会再写一个类似的刷 xx 的网页版,所以就考虑写一个类似的模板以便后续应用需求。 + +与此同时,我也快有半年的时间没碰 **协议复现**(网络通信协议重新实现,后文都简称协议复现)。我更喜欢说这个词,也有的人会说**模拟请求**,对应的关键词可能有 post 请求,抓包,发包,爬虫等等,但大致的意思是**抓取请求数据包,然后脱离宿主机(浏览器,手机),将抓取的数据包重新发送一遍**。 + +你也可以理解成爬虫,但和爬虫相比,要做的不只是爬取数据,而是要基于某些请求包(或者说调用他人不提供的 api 接口,即爬取),来实现一定的功能。比如登录协议,签到协议,抢购协议,游戏封包等等,然后不依靠宿主机(即不用登录浏览器或者应用设备)就能实现诸如登录,签到等功能(在后台记录是有的)。因为这些都是基于网络通信协议的,只要抓包(抓取数据包),然后使用编程提供的网络请求模块来模拟请求,达到重新发包,重新请求的目的。在网页中有 http 协议,websocket 协议,而游戏中有相应的与游戏服务器对应的协议,邮件短信文件又是不同的协议(这里的协议都叫网络通信协议),所以我个人更倾向于称之为协议复现。 + +所以要做协议复现,那基本上有一定的逆向功底和爬虫能力,还有网络通信协议相关的知识了。此次的开发也算是回顾下这些相关技术了。 + + + +## 小区开门应用 + +在这里容我多废话几句,讲一个我之前的一次开发经历,可以说这次的开发经历算是这篇文章的由来。 + +### 应用需求 + +在之前住的一个小区,有个门禁系统,需要安装一个开门的 app(后文都称开门 app),然后注册一个账号到物业那边登记为户主或家庭成员。 + +每次开门的时候,都需要打开这个开门 app,然后点击你要打开的门,接着门就打开了。或者叫保安开个门,总之就是特别麻烦,还不提供创建应用快捷方式。 + +于是我想的是将接口数据“偷”了过来,将大门列表展示在前端上,然后点击对应的大门,然后将大门 id 转发给原 app 的服务器,就实现了开门的效果,也就是这个小区开门的网页版的核心逻辑。 + +当时设计的界面大致如下,展示小区的大门,点击即可远程开门。 + +![](https://img.kuizuo.cn/image_Zv0hXU8h1j.png) + +因为是网页版的,所以只需要在浏览器打开对应的网址,点击对应的大门按钮即可。而开发的初衷是这个 app 不提供桌面快捷方式,点击这个 app 还需要观看首屏广告,此外他人也不用到物业登记,就能开门,对于一些朋友或者住户来说,省去了物业登记的繁琐。 + +不过这个软件还是有挺多要注意的点:首先就是鉴权了,由于我当时主要目的是为了我自己和身边朋友,网站也没有特意发布到互联网上,所以就没做鉴权相关的,不然正常情况下是一定要做鉴权和调用记录的,以及 ip 白名单的。否则搞不好登录原 app 的账号直接因为调用过于频繁直接给禁用了;最主要的安全问题,这里的安全可不只是网站的安全,而是现实的安全。想想如果有一个可以随意进出小区大门的程序,那么任何人都可以进入这个小区,小区的公共设施,业主生活质量安全等等谁来保障?而且最主要所绑定的账号还是我的,万一小区真出了事,那么我的责任将会非常大。 + +综合考量,这个应用是绝对不可能大肆发布到网上的。个人自用问题还是不大,因为这种调用量对服务器几乎没有什么压力。 + +在当时我甚至想基于手机的 GPS 定位,来实现靠近小区自动开门。真羡慕当时我的一堆想法,但也遗憾当时没有去尝试实现这一个想法。 + +### 开发 + +这个应用的起源就说到这了,接下来我要说说其开发形态了,这也就是本文说要的重点内容了。下面是我当时的项目结构: + +![](https://img.kuizuo.cn/image_7J7PhsdaQy.png) + +不难看出,这是一个前后端分离的项目,其中前端使用 uniapp 来开发一套代码多端运行,并且使用的是 Hbuilder 编辑器来开发。而后端就是常规的 Node 后端服务,使用的是 Express 框架。 + +技术栈就介绍完毕,这里我要介绍整个开门实现流程。 + +就说说获取大门列表和开门的两个接口请求: + +#### 获取大门列表 + +后端接口:`http://localhost:3000/api/list` + +这个接口主要的作用就是获取原开门 app 的大门列表,这里简单介绍下代码 + +```javascript +router.get('/list', async (req, res, next) => { + // 模拟请求获取所有大门数据 + let url = `https://xxx.com/api/getDoorList` + let data = { + xxx: {}, + } + + let json = await (await axios.post(url, data)).data + + return json // [{...},{...},{...}] +}) +``` + +然后前端请求后,将列表数据渲染到页面上。 + +#### 开门请求 + +后端接口:`http://localhost:3000/api/open` + +```javascript +router.get('/open', async (req, res, next) => { + let { id } = req.query + + // 模拟请求开门 + let url = `https://xxx.com/api/openDoorControl` + let data = { + id: id, + } + + let json = await (await axios.post(url, data)).data + + return json // { "code": 0 ,"msg": "success" } +}) +``` + +这里的代码也仅仅只是作为演示,实际代码可不止这么简单,因为还需要涉及到登录,加密等等环节。 + +我的前端页面访问地址是 [http://localhost:5000](http://localhost:5000),我需要访问后端接口 [http://localhost:3000/api/list](http://localhost:3000/api/list) 和 [http://localhost:3000/api/openDoor](http://localhost:3000/api/openDoor)。 + +对于不了解 web 开发的人员可能会问为啥要后端服务,不直接在前端向开门 app 的服务器发送请求,然后将响应直接渲染到前端上。比如直接在前端代码中写 openopenDoor 函数 + +```javascript +async function openDoor(id) { + + // 模拟请求开门 + let url = `https://xxx.com/api/openDoorControl` + let data = { + id: id + } + + let json = await (await axios.post(url, data)).data + + return json // { "code": 0 ,"msg": "success" } +}) +``` + +这个疑惑在我初次想使用 web 端来实现协议复现的时候也考虑过,但浏览器为了安全考虑而不支持。这也是我下面所要说的 + +### 同源策略 跨域 + +一般用户的浏览器是有非常强的页面安全策略的,这里要说的就是同源策略,更细分点就是跨域。比如说 kuizuo.cn 这个站点,想要向 baidu.com 发送请求,请求是能够正常发送过去的,但是 kuizuo.cn 这个站点是接收不到任何数据。因为 kuizuo.cn 和 baidu.com 根本不是同一个网址,专业点说就是不同源,这种不同源的请求在浏览器,称为跨域请求。 + +![](https://img.kuizuo.cn/image_QLEJFPTkb6.png) + +跨域请求如果请求的服务端不允许跨域,即响应协议头没有如下内容 + +```text +access-control-allow-credentials: true +access-control-allow-headers: Content-Type, Authorization, X-Requested-With +access-control-allow-methods: GET, POST, PUT, DELETE, OPTIONS +access-control-allow-origin: * +``` + +浏览器会直接拒绝接收响应,但**浏览器确实将请求发送给了服务端**,并且你打开控制台中的网络是看不到该请求的响应结果的。 + +跨域限制只存在于浏览器端,在其他环境下是不存在,请求都是能够发送出去,并且是可以接收到的。所以说为什么不在前端直接向原应用程序的服务器发送请求,罪魁祸首也就是**同源策略**。 + +### 不支持修改协议头 + +像 origin,reference,user-agents 等协议头在浏览器是无法修改的 + +```text +origin: https://xxx.com +referer: https://xxx.com/api/test +user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 Edg/105.0.1343.42 +``` + +而有些请求是会效验你的设备信息,来源地址,这些内容在浏览器中都写在协议头中,且浏览器不支持修改。使用浏览器来发送数据,无疑就是告诉服务器我是浏览器发送的。服务器判断来源不是自家的域名,那就直接拒绝响应,像防盗链就是检测 referer 。 + +## 方案 + +### 桌面端应用开发 + +正是因为同源策略的限制,导致做协议复现时往往都会选择在本地中直接运行,比如使用易语言,python 等语言,将应用打包成 exe,然后跑在 window 系统上。 + +![](https://img.kuizuo.cn/image_hyjd395YGI.png) + +这样的应用会有以下几点缺陷: + +- **易破解**:由于你的大部分核心逻辑最终都会进行编译打包成 exe,如果会些破解技术,恰好你不做任何防护手段,要破解你的程序非常容易。所以也就为什么很多 exe 程序(尤其是易语言)会带有 `.vmp.exe` 后缀也就是 vmp 加壳,让程序很难被分析与破解。并且我就可以开启系统抓包,就可以看到应用程序模拟发送的请求数据包是什么。 + +- **不易跨平台**:大多数的协议复现都是 exe 桌面应用程序,虽然也有安卓,但一般比较少。对于非 windows 用户或者说手头不方便用电脑的用户就很难体验到,并且还要特意安装一个应用,应用程序更新也需要重新安装。 + +其实我说的这些,也算是绝大部分都是桌面应用程序的一个“通病”,但也不是没有优点,这在后面介绍后端应用开发的会做一个比较。 + +### 后端应用开发 + +另一种方式就是我自行搭建一个后端服务,然后将我要模拟的请求封装成一个接口供外部调用。只要我的这个后端服务允许跨域请求,那么我在浏览器或者在桌面端应用都能调用该接口。这样做调用者根本不可能知到你这个接口返回的数据的核心代码(除非他能渗透你的服务器)。 + +![](https://img.kuizuo.cn/image_pVEHwQk1AJ.png) + +别人像要复现一个相同的协议请求的话,就必须自行抓包分析原站点的数据。而原站点可能做了一定的防护手段,例如验证码 浏览器指纹等风控手段。对于一般人而言,非常难破解。但是假设你能复现该请求的话,又非常不想让别人复现,那么自行搭建一个后端服务封装是最好的手段。而在此基础上,你可以做一些限制,比如接口封装,调用收费等等。 + +这里我就不细说太多了,但也不是说没有缺点,甚至可以说这个缺点不比桌面端应用好到哪里: + +- **部署后端服务**:由于搭建了一个后端服务,那么就需要将后端服务部署到服务器上,部署后端服务是小事(但其实也很麻烦,有些写协议复现也不一定会后端开发),但是需要考虑用户的访问量,可能并发量大,那么请求就可能会阻塞导致响应速度变慢。 + +- **请求限制**:从上流程图也不难看出,由于后端应用是部署在自己的服务器上,同时需要承载多个接口请求,然后模拟的请求都是由**自己后端应用服务器发送**的,这和桌面端应用不同。桌面端模拟的请求发送是用户自己的电脑,即用户自己电脑的 ip 地址,而后端应用服务器是服务器的 ip。一旦发送的请求多了,必然是会限制请求的,说白了就是将 ip 黑了,无法访问。要解决的最有效的办法就是换 ip,使用一些 ip 代理服务商,在请求 xx 服务器的时候使用动态 ip 来请求,检测被黑 ip 之后就换另一个 ip 来请求,但是这样就需要额外支付一些 ip 的费用。 + +如果你需要**跨平台**(web 端,桌面端,手机端)并且想要**保护好你的模拟请求的代码**,那么就要考虑选择后端应用开发方案。 + +像我一开始所介绍的小区开门网页版就是属于这一范围,这里就不再赘述其实现过程了。 + +### 前端应用开发/反向代理(可行) + +假设你手头已经有了某个网站的大部分协议复现的代码接口,但是**不想搭建一个后端应用,却想要在前端中使用**。有没有解决方案,这也是有的。 + +一种就是通过浏览器插件来允许任何请求跨域,或者本地开启 http 响应替换,将允许跨域的协议头加到响应中。但这些手段都需要使用者有一定的开发能力,对于普通用户而言就无能为力。 + +目前绝大多数的网站应该都属于前后端分离的形式,后端只提供服务与接口的,提供的接口一般都带有 `/api/` 或 `/v1/` 等请求前缀。那么就可以将前端的请求,通过反向代理,转发到原应用服务器。 + +反向代理其实也需要服务器,但是和后端应用相比,只需要配置一个 nginx,例如 + +```text +location /api/ { +    proxy_set_header Host $host; +    proxy_set_header X-Real-IP $remote_addr; +    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +    proxy_pass http://要转发的服务器域名/api/; +    index index.html index.htm index.jsp; +} +``` + +举个例子,原后端应用通过 `http://target.com/api/user/me` 来获取目标服务器用户信息,如果我的前端应用 `http://example.com` 想要请求该接口必然会有跨域限制。但是通过反向代理,我的前端应用可以请求 `http://example.com/api/user/methods`,nginx 会判断 `url` 是否为 `/api/` 前缀,将请求转发给 `http://target.com/API`。说白了请求 `http://example.com/api/user/me` 就是在请求 `http://target.com/api`,同时还不会触发跨域,反向代理也是解决跨域的常用方法之一。 + +![](https://img.kuizuo.cn/image_PO82fF6A5E.png) + +由于请求还是通过服务器发送的,所以后端应用开发有的限制,在反向代理也同样是有的。还有就是对于限制设备请求的服务器,使用这种方案就不行。。。 + +同时这种开发方案对于多域名的应用来说可能就不是那么友好,因为就需要配置比较多的反向代理,同时只是将接口转发,接口的定制化就不是那么好。**只不过你可以原封不动的将原有的请求迁移到前端**,假设有了某个网站的大部分协议复现的代码接口,那么这样迁移将会特别方便。 + +:::info 提一嘴,如果使用跨平台开发的,如使用前端技术栈去编写桌面端应用(electron),编写安卓,小程序(uniapp,taro)的话,**除了小程序外,其他都没有跨域限制的**(具体还要看相应跨平台技术的限制)。 + +::: + +### 方案选择 + +最终选择那种开发方式还是取决对应的应用场景,没有绝对的方案,具体考虑哪种方案是需要考虑用户,代码安全,请求量,是否维护来考量了。 + +写到这,可能对大多数人而言还是不懂,也很正常,因为这些内容都不算属于传统开发的范畴,甚至可以说是做 hui 产的利器。 + +## 全栈框架 + +我非常希望使用到浏览器的跨平台性,即多端运行,用户的设备只需要有一个浏览器能打开网页就能体验到。(这其实也算是我为什么学 web 开发的初衷了) + +但是在一开始所介绍的小区开门应用中,这样的开发体验其实并不友好。因为我既要编写前端应用还要编写后端服务,相当于两个项目。同时部署应用和传统部署没有特别大的区别,都需要一台服务器,很多时候都是浪费在部署上。 + +而全栈框架可以算是后端应用开发和前端应用开发结合。能很好的解决上述存在的问题,并且也易于部署,下面我会细细道来。 + +这里我选用的 [Nuxt](https://nuxt.com) 框架,这是一个基于 Vue 前端框架实现的服务端渲染框架,羊了个羊刷次数网页版就是基于 Nuxt3 框架来开发的,并且使用 vercel 来进行部署。我手头还写过一个项目 [api-service](https://github.com/kuizuo/api-service)。 + +首先在全栈框架,是有对应的后端服务引擎。像 Nuxt 使用的是 [Nitro](https://nitro.unjs.io/),而 Next.js 使用的是 koa。都提供了后端服务 API 的解决方案,同时这些都是服务都算是 `serverless function`(无服务函数),所以在编写与调用非常方便。 + +此外基于 [Netlify](https://www.netlify.com/) 和 [Vercel](https://vercel.com) 这些 `serverless development` 平台,可以非常方便的部署全栈框架。同时内置 CI/CD,只需要提交 git commit 就能实现自动构建自动部署。 + +最主要是的我恰好使用 Node.js 来做爬虫与 api 接口,因此后端复现接口也使用 js 来实现。 + +为此我特意编写了一个 [Protocol 协议复现模板](/blog/protocol-template) ,这里我就不在过多介绍该模板。 + +## 总结 + +协议复现能写非常多的程序,因为协议复现的大多数案例都是基于已有的应用服务上去实现的,而很多人的日常生活都使用这些已有的服务上。有些已有的服务可能对于一些人而言,体验不好,或者是有其他限制。因此就有人对这些已有的服务来进行“扩展”,来实现自己所定制化的需求。 diff --git "a/blog/develop/\346\265\205\350\260\210HTTP.md" "b/blog/develop/\346\265\205\350\260\210HTTP.md" new file mode 100644 index 0000000..72397d5 --- /dev/null +++ "b/blog/develop/\346\265\205\350\260\210HTTP.md" @@ -0,0 +1,154 @@ +--- +slug: brief-talk-http +title: 浅谈HTTP +date: 2020-09-29 +authors: kuizuo +tags: [http] +keywords: [http] +description: 记录 git 操作失误导致代码丢失与找回的过程 +--- + + + +关于 HTTP 我不讲理论,只讲一下具体的用途。 + +## GET 请求之发送验证码 + +首先我举一个例子,收过短信验证码吧,一般来说在你注册账号的时候就会用到,会有一个点击发送验证码的按钮,这里以 网址 [114 预约挂号](https://www.114yygh.com/) 为例 + +![image-20200928234944932](https://img.kuizuo.cn/image-20200928234944932.png) + +输入完手机号,点击获取验证码就能收到验证码,但这背后的原理又是啥,服务器那边怎么知道我要验证码,并且我输入正确的验证码就进入,错误的就不行。而这正是网络协议 HTTP(关于 HTTP 相关的这里不做过多讲述,希望读者能自行百度了解),我先说下点击了获取验证码发生了什么,通过抓包工具可以获取到如下请求 + +```http +GET https://www.114yygh.com/web/common/verify-code/get?_time=1601308153790&mobile=15212345678&smsKey=LOGIN HTTP/1.1 +Host: www.114yygh.com +Connection: keep-alive +Accept: application/json, text/plain, */* +Request-Source: PC +User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3775.400 QQBrowser/10.6.4208.400 +Content-Type: application/json;charset=UTF-8 +Referer: https://www.114yygh.com/ +Accept-Encoding: gzip, deflate, br +Accept-Language: zh-CN,zh;q=0.9 +``` + +首先我们向服务器发送了一个如上的请求,这是一个 GET 请求,同时请求的链接(url)为`https://www.114yygh.com/web/common/verify-code/get?_time=1601308153790&mobile=15212345678&smsKey=LOGIN` + +如果你会点英文的话,可能会理解其中的含义,主要就这几个参数`verify-code`验证码,`_time=1601308153790`时间戳(时间戳是一个记录时间的东西,用当前时间减去`1970-01-01 08:00:00`即可得到,你可以通过这个工具[时间戳在线转化](https://tool.lu/timestamp/),这里的`1601308153790`所对应的时间为`2020-09-28 23:58:06`)还有一个`mobile=15212345678`,这个`15212345678`是我刚刚输入的手机号。这是向服务器请求的数据,那在来看看服务器返回给我们的是什么 + +```http +HTTP/1.1 200 +Date: Mon, 28 Sep 2020 15:49:15 GMT +Content-Type: application/json;charset=UTF-8 +Content-Length: 57 +Connection: keep-alive +Set-Cookie: hyde_session=Kd10cra3X4yNBePaaQTKUkuYgX9J6Hfx_5337693 +Set-Cookie: hyde_session_tm=1601308154470; Domain=.114yygh.com; Path=/; HttpOnly +Content-Security-Policy: : default-src *.114yygh.com *.qq.com *.baidu.com; font-src * data: +X-Content-Type-Options: nosniff +X-XSS-Protection: 1; mode=block +X-Frame-Options: SAMEORIGIN +X-Via-JSL: d8c5e31,- +X-Cache: bypass + +{"resCode":0,"msg":null,"data":{"endMilliseconds":59997}} +``` + +只需要关注最后一行即可,其中 resCode 为 0,同时手机号`15212345678`也收到了验证码,貌似 resCode 为 0 就决定了服务器是否有给手机号发送短信,事实上也是的,那么说了这么多,有什么用呢,用处可大了。 + +既然这样,我知道了发送上面的那个请求服务器就能给对应的手机号发送验证码,那么我能不能将上面那个请求的手机号给改一下,改成`15287654321`,事实上是完全没问题的,这里我就放一张 HTTP 测试工具的截图。 + +![image-20200929001306474](https://img.kuizuo.cn/image-20200929001306474.png) + +那么是不是我多请求这样像服务器请求,我就能源源不断的收到验证码,现实很美好,人家服务器也不傻,我再一次向服务器发送请求,服务器给我的结果是 + +``` +{"resCode":10000,"msg":"请58秒后重试","data":null} +``` + +没错,就需要等,而且这里的 resCode 也不为 0,那么既然要等一分钟的话,我能不能写个定时脚本,每隔一分钟发送一次,人家服务器也不傻,一般来说,一个手机号最多也就收 5 次验证码,多了就会提示明天再重试,或者今天收到的验证码过多等等。而外面的炸则是通过收集几百个这样的请求,然后将手机号替换成要轰炸的,即可实现多平台验证码轰炸一个手机号。 + +现在你可能已经知道了初步了解 HTTP 请求,但一般的网站都不会像这个这么简单的,明文标码,通常都会进行效验,例如图片验证码,滑块,点字,点图等等,并且还会进行加密操作处理,而这才算真正的难点。 + +## POST 请求之登录 + +既然发验证码是这样,那如果是登录呢,下面就用网站 [万创帮](https://m.wcbchina.com/) 为例,首先进入登录界面 + +![image-20200929003119277](https://img.kuizuo.cn/image-20200929003119277.png) + +输入手机号和密码,点击登录,同样的我们可以通过抓包工具获取到对应的 HTTP 请求,如下 + +```http +POST https://m.wcbchina.com/api/login/login?rnd=0.6463111465399551 HTTP/1.1 +Host: m.wcbchina.com +Connection: keep-alive +Content-Length: 149 +Pragma: no-cache +Cache-Control: no-cache +Accept: application/json, text/javascript, */*; q=0.01 +Origin: https://m.wcbchina.com +X-Requested-With: XMLHttpRequest +User-Agent: Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Mobile Safari/537.36 +Content-Type: application/json +Accept-Encoding: gzip, deflate, br +Accept-Language: zh-CN,zh;q=0.9 + +{"auth":{"timestamp":1601312429287,"sign":"777473FB2A1838DBD64BA7A11C98911B"},"username":"15212345678","password":"E9BC0E13A8A16CBB07B175D92A113126"} +``` + +貌似比上面那个发验证码的复杂,确实,这是一个 POST 请求,你在链接上看不出什么有效信息,在最后一行才是关键。这里的`timestamp`也就是时间戳,记录时间的,username 是我们输入的手机号(账号)没什么问题,这里的 sign 和 password 的内容又是啥?这就是加密,让你不能简单单纯的通过替换文本来实现登录,这就来分析一下他到底怎么加密的。 + +通过在浏览器按下 F12 键,打开控制台面板,接着点击 Network,这里会将我们发送的请求记录下来 + +![image-20200929004247182](https://img.kuizuo.cn/image-20200929004247182.png) + +同时鼠标停在 Initiator 上有如下结果 + +![image-20200929004449660](https://img.kuizuo.cn/image-20200929004449660.png) + +不管三七二十一,点击跳转到对应的代码先,然后在左边下一个断点 + +![image-20200929004628978](https://img.kuizuo.cn/image-20200929004628978.png) + +这时候我们在点击登录按钮看看 + +![image-20200929004713733](https://img.kuizuo.cn/image-20200929004713733.png) + +没错,浏览器这时候停了下来,停在了我下断点的地方,通过函数名也可以猜到这个是发送的,对应的肯定在上面,通过右边的 Call Stack 函数调用栈即可追随上一函数 + +![image-20200929004943674](https://img.kuizuo.cn/image-20200929004943674.png) + +在这里我看到了原文的信息,这是通过 Jquery 通过 id 获取元素的值,也就是这里的手机号和密码,在这里还都是原文,点到下一个函数则变成了密文,那么肯定是上一个函数做了手脚。 + +认真观察,N 这个是我们的密码,但对 N 进行了一个操作也就是 `a.hex_md5(N)`,没错,这就是 md5 加密。有关加密的可以看看我写过的 [浅谈加密算法](/docs/brief-talk-encryption-algorithm) + +那么通过加密工具将 md5 加密是否能得到我们要的加密结果,如下 + +![image-20200929005711500](https://img.kuizuo.cn/image-20200929005711500.png) + +`E9BC0E13A8A16CBB07B175D92A113126`在看看 Password 的值,一模一样,看来已经解决了一个参数,那么还有一个`sign`呢。貌似右边的函数调用栈都不好使,我试试搜索字符串 sign 看看 + +![image-20200929005911119](https://img.kuizuo.cn/image-20200929005911119.png) + +好家伙,直接定位到了,那么同样的在这里下一个断点,查看一下到底发生了什么(实际上 js 静态分析就完事了,这个网站太简单了) + +这里的 N 看来就决定了 sign,而 N 也是通过 md5 加密的,不过原文我还不知道,让代码执行到这一行看看结果 + +![image-20200929010417525](https://img.kuizuo.cn/image-20200929010417525.png) + +这里的 c 就是时间戳,而 token 和 password 都是未定义,那么就好办了。这里明文也就是 c `1601312429287`,那么用加密工具即可得到`777473FB2A1838DBD64BA7A11C98911B`,那么参数都搞定好了, 只需要替换一下账号,然后将正确的密码通过加密算法(这里为 md5)生成,同时对 sign 也生成出来,然后提交给服务器就能收到我们登录的请求,就认定为我们登录了,记录为在线用户。 + +如果我用的加密算法错了,或者我分析错了,提交给服务器会是怎么样的 + +![image-20200929011055451](https://img.kuizuo.cn/image-20200929011055451.png) + +例如我这里的 sign 算法是错了(将结尾的 B 改成了 A),发送给服务器,服务器返回给我们则是失败的结果,原因很简单,就是为了防止别人恶意登录所添加的效验,提交的数据伪成败,就决定了服务器给我们结果是成败。 + +## 总结 + +通过上面的一些例子,只要能伪造请求,发送给服务器,就能获取我们想要的结果或者目的,事实上也是如此,但伪造数据的难易则要由所对应的网站而定,有的网站压根就没没什么难度,而有的你搞一天都未必能搞的出来。如今的网站在这方面也都下足了功夫,想要轻松的伪造请求可不是件容易的事情了。 + +也正是因为我学了 HTTP 请求与 JS 逆向分析,我能做的也就更多,而正是基于 HTTP 协议下,其中一个就是有关于超星刷课软件的例子,如果我没有学过这些,我就不可能写出来。 + +后续会有关 HTTP 请求这方面都会放在逆向这个分类下,比如一些网站的加密算法和常见的坑等等。 diff --git "a/blog/develop/\350\256\260\344\270\200\346\254\241git\344\270\242\345\244\261\344\273\243\347\240\201\346\211\276\345\233\236.md" "b/blog/develop/\350\256\260\344\270\200\346\254\241git\344\270\242\345\244\261\344\273\243\347\240\201\346\211\276\345\233\236.md" new file mode 100644 index 0000000..1de2172 --- /dev/null +++ "b/blog/develop/\350\256\260\344\270\200\346\254\241git\344\270\242\345\244\261\344\273\243\347\240\201\346\211\276\345\233\236.md" @@ -0,0 +1,43 @@ +--- +slug: lost-code-find-by-git +title: 记一次git丢失代码找回 +date: 2021-08-15 +authors: kuizuo +tags: [git, code] +keywords: [git, code] +description: 记录 git 操作失误导致代码丢失与找回的过程 +--- + + + +## 场景复现 + +今晚,我和往常一样对着电脑撸着代码,这时候我灵光一现,想到了一个好的功能,于是乎我就开始增加代码文件,更改之前已有的问题,当我实现完这个功能的时候,觉得可有可无,我想通过 Git 直接回退到我没有这个新功能的版本,把新增的文件和更改的文件全都给还原回去,然而在编写新功能的时候我忘记 Commit 了!!!(正常操作应该是新建一个分支,在新分支编写新功能),于是乎我点了如图操作(这里仅作为事件发生展示,并不为实际丢失个数) + +![image-20210815141808996](https://img.kuizuo.cn/image-20210815141808996.png) + +没错,清空所有更改过的代码。导致这些文件直接丢失(并不在回收站),包括写新功能前的代码和写新功能后的代码全都丢失了 😭!!! + +## 找回前提 + +庆幸的时候,写新功能前的代码我成功 add 到了暂存区,只是未 Commit 而已,那么就能找回对应的文件(仅仅只是文件,并且没有文件名,项目结构都无法还原 )。如果有 Commit 的话非常好找回,直接回退上一个版本即可,如果连 add 操作都没有的话,除非像 VScode 插件 Local History 或一些 IDE 有记录本地文件,不然恐怕是真的找不回了。。。 + +## 开始找回 + +故,此次目的是找回 add 过而未 commit 的文件,首先打开 git bash 输入 + +```bash +git fsck --lost-found +``` + +![image-20210815150520759](https://img.kuizuo.cn/image-20210815150520759.png) + +进入`.git\lost-found\other` + +![image-20210815153556495](https://img.kuizuo.cn/image-20210815153556495.png) + +然后通过文本编辑器打开即可,如果是代码的话重命名对应的后缀,如果是图片这些就得对应删除前所对应的文件链接。名字是找不回来了,只能手动重命名。 + +## 事后回想 + +可能这次丢失的仅仅只是几十个文件,下次丢失的可能就是一个项目了。所以在每次更改代码前做好备份才是首要做的,同时也感谢 git 这么好用的版本控制系统,不然这篇博客可能也不存在。 diff --git "a/blog/lifestyle/MacBook \344\275\223\351\252\214\346\234\211\346\204\237.md" "b/blog/lifestyle/MacBook \344\275\223\351\252\214\346\234\211\346\204\237.md" new file mode 100644 index 0000000..0227af3 --- /dev/null +++ "b/blog/lifestyle/MacBook \344\275\223\351\252\214\346\234\211\346\204\237.md" @@ -0,0 +1,128 @@ +--- +slug: /macbook-experience +title: MacBook 体验有感 +date: 2023-05-05 +authors: kuizuo +tags: [macOS, MacBook, 记录, 使用体验] +keywords: [macOS, MacBook, 记录, 使用体验] +description: 作者是一位从来没用过苹果产品的程序员,但在使用了一周的 MacBook Pro 14 寸后,便爱不释手。 +image: https://img.kuizuo.cn/202312270236590.png +--- + +首先我不是 iphone 用户,甚至是果黑(苹果的小黑子,合理来说是苹果手机的小黑子),所以我一向从内心就很摈弃苹果的产品。因此我从来没体验过 MacOS 系统,用了近 4 年 window,不过由于我的那台 window 本 (21 年小新 pro14) 给我的体验非常差,虽然说续航勉勉强强足够支撑我一个下午的开发,但 intel 的 i5 cpu 我就没打算将其作为主力机开发(根本做不了),更多是使用向日葵远程桌面软件来远程连接到我的台式电脑,远程操控来进行开发。然而由于屏幕分辨率不同以及网络延迟,这样的体验长期下去必然会崩溃。因此**更换自己的移动办公设备已经成了我当下的刚需。** + +见识到诸多程序员大神都将 mac 作为主力开发机器,同时又被安利过很多次 MacBook,但我一直对 macOS 保持观察的态度,自己从未亲自体验过,最多也就看别人用用,在 window 上这些同样也能实现,何必要多此一举再去了解一个新的系统,新的操作逻辑。但直到我真正接触并体验过 macOS 后,我便爱不释手。 + +在写这篇文章时,我已经用了近一周的 macbook,因此想分享个人的购买流程、选购建议、使用感悟,或许对于某些想要尝试 MacOS 但又保有迟疑态度的用户有所帮助,也算是给曾经的我对苹果的偏见的转变。 + +> 拖更了近两个月的博文了,摆了一整子,说来也确实有些许惭愧。不过目前生产力工具已就绪,也要开始步入正轨。 + + + +## 购买流程 + +我是在线下 apple 授权店买的,五一假期的前一天晚上逛商场的时候正好经过,于是进去与店员交谈了一整子,又思考了一晚上,最终决定第二天直接现货 购买了丐版(标准版)的 macbook m2 pro 14 寸 16g + 512g。 + +至于选择 14 寸还是 16 寸,就因人(钱)而异,去线下给我最大的感受就是 16 寸是真的大,且厚重(14 寸也挺重的有 1.6kg),通常我在室内我就会拓展外接显示器加上偶尔有床上办公的需求所以在看到第一眼后便毫不犹豫的选择 14 寸作为我的目标尺寸。 + +至于说选择 m2 pro 还是 m2 max,这条[链接](https://www.apple.com.cn/macbook-pro-14-and-16/)与下图能告诉你答案。 + +![Untitled](https://img.kuizuo.cn/202305050428893.png) + +其次就是选配方面,在之前我是打算购买 32g + 1t 的。但在如今一堆 electron 应用(一个就要吃至少 100m 的内存),加上我本身的会有多开几个 vscode 以及多个浏览器标签, 16g 内存在 window 下对于开发从事者而言已经不够使用了,在 mac 上 从我的事实也证明 16g 内存 在两个正在运行中的 node 与 的 10 来个浏览器标签,加上一些常用软件(微信、QQ、飞书、)是有些不够用了,以下对应的活动监视图(window 中的任务管理器) + +![Untitled](https://img.kuizuo.cn/202305050429190.png) + +虽说有 swap,表面上的 16g 物理内存实际上运行内存可能会更多,但最主要还是看内存压力。不过即使是这样,系统也没有出现过任何的卡顿,这要是换 windows,恐怕已经蓝屏了。等哪天内存压力变红时或者出现卡顿现象,再来汇报相关进程。(新买的机子,不舍得压力测试折腾她) + +而 m2 pro 的 512g 相比 1t 读写速度减少一半(看下图你便懂了,单通道的速度和两个 512g 组双通道相比),事实上在之前的丐版都存在这种问题,可以说苹果是巴不得你加购硬盘容量,不然硬盘速度缩半。 + +![Untitled](https://img.kuizuo.cn/202305050428895.png) + +而 1t 及以上容量自然是无该问题,何况不加配的 mac 能叫 mac 吗。我其实是很想加配的,但无奈无现货,并且官网定制这个配置(m2 pro 32g + 1t)的还需要等待 2 个星期,据店员说“这只是预计,实际可能会更久“,不过这里不排除店员这么说诱导我在购买丐版现货。 + +![Untitled](https://img.kuizuo.cn/202305050428896.jpeg) + +不过最终能让我购买的很大原因是在五一期间我实在不想用那破 window 本,因此第二天再次联系店员决定直接付款拿丐版现货。因为考虑两年后大概率也会更换电子设备(距离我上次更换电子设备也过去两年),所以综合考虑当前的配置在这两年应该是足以使用(这句话也许说的有点早)。 + +实体店与线上购买没有本质的区别,**价格上都是一致的**,也是有教育优惠的。只是人家会帮你激活设备,在你的眼皮子底下看看有无问题,确认无误后,交钱走人便可。此外可能还会赠送一些额外的一些配件,如键盘膜,屏幕膜,清洁套装,拓展坞(没有手提包),不过这些对我来说都非刚需,只要 MacBook Pro 没问题即可。 + +不过这里想要提一下,人家是挺极力推荐我购买 apple care 的(顺带有个配件券),据我了解,貌似是有一个 apple care 的指标,需要达到多少这样。不过我个人不喜欢买保险,因此便没有购买。 + +## 刚到手的 Mac 该如何处理 + +### 不要贴膜 + +**不要贴膜,不要贴膜,不要贴膜!** + +我本是不想贴膜,喜欢裸机的感觉,但由于附送一个屏幕膜,我便勉为其难的贴一下,然而当我贴完后我随即将辛辛苦苦贴好的膜又给撕了下来了,并不是因为我贴的不好,而是**贴膜简直就是负提升**,前后对比是肉眼可见明显,这里我用相机拍不出肉眼那种效果,如果说喝枸杞是养生,那看 macbook 屏幕说是养眼可一点不为过。毕竟维修一个 Macbook 屏幕就需要 5000 左右,让我萌生一丝购买 apple care 的想法。 + +其次就是网上都有流传 macbook 的 B 面(屏幕)与 C 面(键盘)之间的间隔特别薄,贴屏幕膜或键盘膜可能都会让这层素质极高的屏幕受到一些损害。还有贴屏幕膜后,在下一次更换屏幕膜的时候,可能会导致屏幕涂层脱落,而贴键盘膜的话,时间久了会导致合盖的时候键盘膜印在屏幕上。总之,基本都是建议裸机。 + +### 熟悉 mac 操作以及相关软件 + +这里我推荐自己我自己看过的几个 mac 相关指南,能够帮助小白速度上手 mac。 + +[Mac 云课堂的个人空间\_哔哩哔哩\_bilibili](https://space.bilibili.com/41062266) + +[【看完秒懂】Mac 苹果电脑超详细上手入门指南!建议做笔记!up 良心制作,用一集视频包你熟练上手 Mac\_哔哩哔哩\_bilibili](https://www.bilibili.com/video/BV1PF411E7LG/) + +下面这个会比较针对与程序开发 + +[2022 我用 MacBook Pro 整一年 【感想 与 踩坑指南】 - 掘金 (juejin.cn)](https://juejin.cn/post/7181274704659873850) + +由于我使用时间较短,因此软件方面我不好做出评价与推荐,这里我只附上一张我已安装应用截图 + +![Untitled](https://img.kuizuo.cn/202305050428897.png) + +### 熟悉触控板与应用全屏,提升效率 + +如果要说使用的这段期间对笔记本电脑的体验变化无意有两点,一是颠覆我对笔记本触控板难用的想法,二是应用全屏(配合台前调度)却能够有如此高效的体验。 + +这一部分我认为不必过多吹捧,亲自到线下实体店感受才最为真实,相信我,你会爱上触摸板了。 + +### 养成保养电池的习惯 + +以下内容为使用 8 个月后的补充。 + +由于内存的不足加上重度开发,现今我的电池容量已达到 93% 了,说实话是有点小心痛的😭。 + +![](https://img.kuizuo.cn/2024/0114155525-202401141555937.png) + +这与我一些不好的使用习惯还是有关的,我通常会打开好几个标签页,好几个 应用而不关闭,这就导致内存时常处于高负载状态下,不断的从硬盘中拿内存,这样不仅伤硬盘同时也伤电池。此外我通常会用 Type-c 一线连接外接显示器(既可以充电又可以显示),加上我没有拔电以及关机的习惯(可能要过好几周才会重关一次),这就导致 8 个月的时间内电池最大容量缩减。 + +现在来看或许当初买个 apple care 还能免费换个电池似乎还划得来。 + +## 选 windows 还是 macOS ? + +现在可以毫不犹豫的说,我会选择 macOS,下一台笔电也会选择 macOS 系统。但并不是说什么场景,macOS 都是最优选,就比如说游戏需求,我想没人会买台 mac 来作为自己的游戏机。mac 上几乎玩不到什么 3A 大作,甚至在 m2 芯片上,你可能都无法下载 wegame 来玩上一把英雄联盟。 + +![Untitled](https://img.kuizuo.cn/202305050428898.png) + +如果你有桌面游戏的需求,建议拉黑 mac。此外还有一些 window 的专业软件,你在 mac 上可能找不到与之对应或平替的软件,尤其是在大学课程中,老师几乎不可能给学生发个 dmg 文件,如果你在大学期间买 mac,又要兼顾学校的课程软件需求,又不得不安装 window 虚拟机,与其如此折腾不如一开始就选用相对便宜的 win 本,还能减少一些经济压力。不过我觉得大学老师上课所说的一些软件都没必要安装,反而占用一些不必要的空间,(vc++、eclipse 等等),如果你们老师提到了 Vscode 那当我没说。 + +但出色的系统、高素质的屏幕,注定能让 MacBook 能够成为某部分群体的生产力工具,挣钱的机器。选用 macbook 的用户想必都希望在它任职期间产生数十倍的价值,当然排除我这个买来尝鲜的。 + +## 开发上的体验提升 + +目前手头的三台电脑设备对应的 CPU(性能从高到低)M2 Pro > AMD 5900x > i5-11300H + +![Untitled](https://img.kuizuo.cn/202305050428899.png) + +![Untitled](https://img.kuizuo.cn/202305050428900.png) + +这里我没找到比较好的前端 benchmark 项目,但就从我个人直观的体验与在这三台机器启动同一个前端项目启动打包来看,在冷启动上,m2 pro 耗费 1.7s, 5900x 耗费 2.8s,i5-11300H 我都不想拉项目,去年的暑假靠这台 win 本进行开发,别提体验有多差,每次都需要干巴巴的干等项目完全启动就需要等个 2、3 分钟(不夸张),有时候可能因为某些特殊原因需要重启服务,好的,又浪费个 2、3 分钟。影响你效率的可能不只是环境,还有你的机器。 + +冷启动都能有近 1s 的优势,就别提热加载和打包速度上,这里直接给出我打包一个 Nuxt 项目的打包时间输出耗时,m2 pro 耗时 27s、5900x 耗时 116s(数据真实有效),快的让我有点感觉我是不是少写了某部分代码,还是说多注释了些代码。 + +性能优势可能不是最大的优势,但编程环境上 Mac 绝对比 Window 来的好,一个 [Homebrew](https://brew.sh/) 就已经能解决百分之 90 的编程语言环境,而这换到 Window 上则有诸多的安装方式。至少你不必像 Window 那样还需要打开设置面板配置环境变量。而 MacOS 与 Linux 又非常相似,都可以在命令行中运行 Unix、bash/zsh、以及其他 shell 命令. 所以至少从 `代码` 开发方面, Mac 绝对比 Window 来的好,这也是多数开发人员选择 Mac 的原因。 + +## 结语 + +相对遗憾的是购买的还是相对匆忙,就是没有加配 32g,虽说目前来说 16g 勉强能够应付绝大部分场景,但免不了后续爆内存,又无能为力的情况。但想到自己仿佛挖到了一个新世界的宝藏,这种担忧就显得不足为惧。 + +在写完这篇稿子时,回头用起 win 时,都习惯性的按下 `Alt + C` 键位,殊不知 `Ctrl + C` 才是 win 的复制。适应也许只需要几天的时间,但回去也许可能大半辈子都不再回去。 + +从被别人安利到用 mac,再到自己安利别人用 mac,这种对 macOS 系统相见恨晚的感受,也许只有使用过 macOS 的人才能理解。**很多东西只有自己用过才知道,只有尝试过,才知道适不适合自己。不尝试并不会丢失什么,但尝试过后往往能够收获意想不到的东西。** + +如果你还没有尝试过 macOS 系统,那么你或许真的错过了很多。 diff --git "a/blog/lifestyle/\344\270\200\344\275\215\346\234\252\346\233\276\346\266\211\350\266\263\347\256\227\346\263\225\347\232\204\345\210\235\345\255\246\350\200\205\346\224\266\350\216\267.md" "b/blog/lifestyle/\344\270\200\344\275\215\346\234\252\346\233\276\346\266\211\350\266\263\347\256\227\346\263\225\347\232\204\345\210\235\345\255\246\350\200\205\346\224\266\350\216\267.md" new file mode 100644 index 0000000..53f7c6f --- /dev/null +++ "b/blog/lifestyle/\344\270\200\344\275\215\346\234\252\346\233\276\346\266\211\350\266\263\347\256\227\346\263\225\347\232\204\345\210\235\345\255\246\350\200\205\346\224\266\350\216\267.md" @@ -0,0 +1,236 @@ +--- +slug: discoveries-of-an-algorithm-neophyte +title: 一位未曾涉足算法的初学者收获 +date: 2023-09-16 +authors: kuizuo +tags: [算法] +keywords: [算法] +--- + +正如标题所言,在我四年的编程经历中就没刷过一道算法题,这可能与我所编写的应用有关,算法对我而言提升不是特别大。加上我几乎都是**在需求中学习,而非系统性的学习**。所以像算法这种基础知识我自然就不是很熟悉。 + +## 那我为何会接触算法呢? + +我在今年暑假期间有一个面试,当时面试官想考察下我的算法能力,而我直接明摆了说我不行(指算法上的不行),但面试官还是想继续考察,于是就出了道斐波那契数列作为考题。 + +但我毕竟也接触了 4 年的代码,虽说不刷算法,但起码也看过许多文章和代码,斐波那契数列使用递归实现的代码也有印象,于是很快我就写出了下面的代码作为我的答案。 + +```typescript +function fib(n) { + if (n <= 1) return n + + return fib(n - 1) + fib(n - 2) +} +``` + +面试官问我还有没有更好的答案,我便摇了摇头表示这 5 行不到的代码难道不是最优解? + +> 事实上这份代码看起来很简洁,实际却是耗时最慢的解法 + +毫无疑问,在算法这关我肯定是挂了的,不过好在项目经验及后续的项目实践考核较为顺利,不然结局就是回去等通知了。最后面试接近尾声时,面试官友情提醒我加强基础知识(算法),强调各种应用框架不断更新迭代,但计算机的底层基础知识是不变的。于是在面试官的建议下,便有了本文。 + + + +好吧,我承认我是为了面试才去学算法的。 + +### 对上述代码进行优化 + +在介绍我是从何处学习算法以及从中学到了什么,不妨先来看看上题的最优答案是什么。 + +对于有接触过算法的同学而言,不难看出时间复杂度为 O(n²),而指数阶属于爆炸式增长,当 n 非常大时执行效果缓慢,且可能会出现函数调用堆栈溢出。 + +如果仔细观察一下,会发现这其中进行了非常多的重复计算,我们不妨将设置一个 res 变量来输出一下结果 + +```tsx +function fib(n) { + if (n <= 1) { + return n + } + + const res = fib(n - 1) + fib(n - 2) + console.log(res) + return res +} +``` + +当 n=7 时,所输出的结果如下 + +![Untitled](https://img.kuizuo.cn/202309162220346.png) + +这还只是在 n=7 的情况下,便有这么多输出结果。而在算法中要避免的就是重复计算,这能够高效的节省执行时间,因此不妨定义一个缓存变量,在递归时将缓存变量也传递进去,如果缓存变量中存在则说明已计算过,直接返回计算结果即可。 + +```typescript +function fib(n, mem = []) { + if (n <= 1) { + return n + } + + if (mem[n]) { + return mem[n] + } + + const res = fib(n - 1, mem) + fib(n - 2, mem) + console.log(res) + mem[n] = res + return res +} +``` + +此时所输出的结果可以很明显的发现没有过多的重复计算,执行时间也有显著降低。 + +![Untitled](https://img.kuizuo.cn/202309162220348.png) + +这便是**记忆化搜索**,时间复杂度被优化至 O(n)。 + +可这还是免不了递归调用出现堆栈溢出的情况(如 n=10000 时)。 + +![Untitled](https://img.kuizuo.cn/202309162220349.png) + +从上面的解法来看,都是从”**从顶至底**”,比方说 n=7,会先求得 n=6 的结果, 而 n=6 又要求得 n=5 的结果,依次类推直至得到底层 n=1 的结果。 + +事实上我们可以换一种思路,先求得 n=1,n=2 的结果,然后依次类推上去,最终得到 n=6,n=7 的结果,也就是“**从底至顶”**,而这就是**动态规划**的方法。 + +从代码上来分析,因此我们可以初始化一个 dp 数组,用于存放数据状态。 + +```tsx +function fib(n) { + const dp = [0, 1] + + for (let i = 2; i <= n; i++) { + dp[i] = dp[i - 1] + dp[i - 2] + } + + return dp[n] +} +``` + +最终 dp 数组的最后一个成员便是原问题的解。此时输出 dp 数组结果。 + +![Untitled](https://img.kuizuo.cn/202309162220350.png) + +且由于不存在递归调用,因此你当 n=10000 时也不在会出现堆栈溢出的情况(只不过最终的结果必定超出了 JS 数值可表示范围,所以只会输出 Infinity) + +对于上述代码而言,在空间复杂度上能够从 O(n) 优化到 O(1),至于实现可以参考 [空间优化](https://www.hello-algo.com/chapter_dynamic_programming/intro_to_dynamic_programming#1414),这里便不再赘述。 + +我想至少从这里你就能看出算法的魅力所在,**这里我强烈推荐 [hello-algo](https://www.hello-algo.com/) 这本数据结构与算法入门书**,我的算法之旅的起点便是从这本书开始,同时激发起我对算法的兴趣。 + +## 两数之和 + +于是在看完了这本算法书后,我便打开了大名鼎鼎的刷题网站 [LeetCode](https://leetcode.cn),同时打开了究极经典题目的[两数之和](https://leetcode.cn/problems/two-sum)。 + +> 有人相爱,有人夜里开车看海,有人 leetcode 第一题都做不出来。 + +题干: + +> 给定一个整数数组 `nums`  和一个整数目标值 `target`,请你在该数组中找出和为目标值 `target` 的那 **两个** 整数,并返回它们的数组下标。 +> +> 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。 +> +> 你可以按任意顺序返回答案。 + +以下代码将会采用 JavaScript 代码作为演示。 + +### 暴力枚举 + +我初次接触该题也只会暴力解法,遇事不决,暴力解决。也很验证了那句话:不论多久过去,我首先还是想到两个 for。 + +```tsx +var twoSum = function (nums, target) { + const n = nums.length + + for (let i = 0; i < n; i++) { + for (let j = 0; j < n; j++) { + if (nums[i] + nums[j] === target && i !== j) { + return [i, j] + } + } + } +} +``` + +当然针对上述 for 循环优化部分,比如说让 `j = i + 1` ,这样就可以有效避免重复数字的循环以及 `i ≠ j` 的判断。由于用到了两次循环,很显然时间复杂度为 O(n²),并不高效。 + +### 哈希表 + +我们不妨将每个数字通过 hash 表缓存起来,将值 `nums[i]` 作为 key,将 `i` 作为 value。由于题目的条件则是 `x + y = target`,也就是 `target - x = y`,这样判断的条件就可以由 `nums[i]+ nums[j] === target` 变为 `map.has(target - nums[i])` 。如果 map 表中有 y 索引,那么显然 `target - nums[i] = y`,取出 y 的索引以及当前 i 索引就能够得到答案。代码如下 + +```tsx +var twoSum = function (nums, target) { + const map = new Map() + + for (let i = 0; i < nums.length; i++) { + if (map.has(target - nums[i])) { + return [map.get(target - nums[i]), i] + } + map.set(nums[i], i) + } +} +``` + +而这样由于只有一次循环,时间复杂度为 O(N)。 + +### 双指针算法(特殊情况) + +假如理想情况下,题目所给定的 nums 是**有序的情况**,那么就可以考虑使用双指针解法。先说原理,假设给定的 nums 为 `[2,3,5,6,8]`,而目标的解为 9。在上面的做法中都是从索引 0 开始枚举,也就是 2,3,5…依次类推,如果没找到与 2 相加的元素则从 3 开始 3,5,6…依次类推。 + +此时我们不妨从**最小的数**和**最大的数**开始,在这个例子中也就是 2 和 8,很显然 `2 + 8 > 9`,说明什么?说明 8 和中间所有数都大于 9 即 3+8 ,5+8 肯定都大于 9,所以 8 的下标必然不是最终结果,那么我们就可以把 8 排除,从 `[2,3,5,6]` 中找出结果,同样的从最小和最大的数开始,`2 + 6 < 9` ,这又说明什么?说明 2 和中间这些数相加肯定都下雨 9 即 2+3,2+5 肯定都小于 9,因此 2 也应该排除,然后从 `[3,5,6]` 中找出结果。就这样依次类推,直到找到最终两个数 `3 + 6 = 9`,返回 3 与 6 的下标即可。 + +由于此解法相当于有两个坐标(指针)不断地向中间移动,因此这种解法也叫**双指针算法**。当然,要使用该方式的前提是输入的**数组有序**,否则无法使用。 + +用代码的方式来实现: + +1. 定义两个坐标(指针)分别指向数组成员最左边与最右边,命名为 left 与 right。 +2. 使用 while 循环,循环条件为 left < right。 +3. 判断 `nums[left] + nums[right]` 与 `target` 的大小关系,如果相等则说明找到目标(答案),如果大于则 右指针减 1 `right—-`,小于则左指针加 1 `left++`。 + +```tsx +function twoSum(nums, target) { + let left = 0 + let right = nums.length - 1 + + while (left < right) { + const sum = nums[left] + nums[right] + if (sum === target) { + return [left, right] + } + + if (sum > target) { + right-- + } else if (sum < target) { + left++ + } + } +} +``` + +--- + +针对上述两道算法题浅浅的做个分享,毕竟我还只是一名初入算法的小白。对我而言,我的算法刷题之旅还有很长的一段时间。且看样子这条路可能不会太平坦。 + +## 算法对我有用吗? + +在我刷算法之前,我在网上看到鼓吹算法无用论的人,也能看到学算法却不知如何应用的人。 + +这也不禁让我思考 🤔,算法对我所开发的应用是否真的有用呢? + +在我的开发过程中,往往面临着各种功能需求,而通常情况下我会以尽可能快的速度去实现该功能,至于说这个功能耗时 1ms,还是 100 ms,并不在乎。因为对我来说,这种微小的速度变化并不会被感知到,或者说绝大多数情况下,处理的数据规模都处在 n = 1 的情况下,此时我们还会在意 n² 大还是 2ⁿ 大吗? + +但如果说到了用户感知到卡顿的情况下,那么此时才会关注性能优化,否则,过度的优化可能会成为一种徒劳的努力。 + +或许正是因为我都没有用到算法解决实际问题的经历,所以很难说服自己算法对我的工作有多大帮助。但不可否认的是,算法对我当前而言是一种思维上的拓宽。让我意识到一道(实际)问题的解法通常不只有一种,如何规划设计出一个高效的解决方案才是值得我们思考的地方。 + +## 结语 + +借 MIT 教授 Erik Demaine 的一句话 + +> If you want to become a good programmer, you can spend 10 years programming, or spend 2 years programming and learning algorithms. + +如果你想成为一名优秀的程序员,你可以花 10 年时间编程,或者花 2 年时间编程和学习算法。 + +这或许就是学习算法的真正意义。 + +## 参考文章 + +[初探动态规划](https://www.hello-algo.com/chapter_dynamic_programming/intro_to_dynamic_programming) + +[学习算法重要吗?](https://www.zhihu.com/question/335097718) diff --git "a/blog/lifestyle/\344\270\272\344\273\200\344\271\210\346\210\221\344\270\215\347\224\250\344\270\203\347\211\233\344\272\221.md" "b/blog/lifestyle/\344\270\272\344\273\200\344\271\210\346\210\221\344\270\215\347\224\250\344\270\203\347\211\233\344\272\221.md" new file mode 100644 index 0000000..8a70564 --- /dev/null +++ "b/blog/lifestyle/\344\270\272\344\273\200\344\271\210\346\210\221\344\270\215\347\224\250\344\270\203\347\211\233\344\272\221.md" @@ -0,0 +1,62 @@ +--- +slug: why-i-dont-use-qiniu-cloud +title: 为什么我不用七牛云 +date: 2020-12-23 +authors: kuizuo +tags: [随笔, cloud-service] +keywords: [随笔, cloud-service] +--- + +七牛云是国内鲜有的免费提供对象存储服务的一个云服务商,和腾讯云,阿里云一样,但这两者收费,而七牛云不收费,当然也不是绝对免费,对象存储免费空间 10g,每个月还有 10g 的 cdn 加速服务(多数人基本用不完),超出部分额外收费,此外 https 收费。 + + + +我**之前**使用七牛云的主要原因,就是业内太多人推荐了,免费还带加速,存储博客图片好的不行,然而发生了如下的事情: + +## 事情经过 + +让我不用七牛云的罪魁祸首其实是 Chrome 浏览器,先看一张图片。 + +![image-20201214211848873](https://img.kuizuo.cn/image-20201214211848873.png) + +What?图片呢?第一时间毫不犹豫打开控制台查看问题所在,有这样几行报错 + +![image-20201214212056058](https://img.kuizuo.cn/image-20201214212056058.png) + +关注第一行 + +``` +Mixed Content: The page at '' was loaded over HTTPS, but requested an insecure element ''. This request was automatically upgraded to HTTPS, For more information see +``` + +翻译过来就是:网页混合内容:页面是通过 HTTPS 加载,但却请求一个不安全元素 HTTP。该请求被自动升级为 HTTPS,更多信息请参见。 + +管他啥意思,先百度,然后才了解到,在 Chrome 浏览器高版本中(具体多少忘了),如果当前站点是 https,那么会自动将页面请求的 http 升级为 https,也就是说,我当前站点是 https 协议,访问不了 http 的资源,然而这可坑惨我了,我的图片全都放在七牛云上,然而七牛云的 HTTPS 是收费的,那时候我也抱着白嫖的心态,去嫖了七牛云的对象存储来做图床,现在我将http://kzcode.cn 升级为https://kzcode.cn 的时候,就意味这我不能白嫖了?也就是出现了如上画面,然后又去相关了一些百度相关的知道,看看有没有解决办法,如下 + +- 使用其他浏览器 + +这个问题只有 Chrome 浏览器内才有,在 https 站点会将 http 请求自动升级为 https,在其他浏览器不会,上面的图片也会正常显示。 + +- 要么都用 HTTP,要么都用 HTTPS + +http 站点去请求 https 资源会不安全,而 https 站点去请求 http 会自动升级为 https,而这没有很好的有效方法去让两者兼容。 + +## 选择 + +既然搜到解决办法后,我心想,这网站怎么能不上 HTTPS,怎么能让左上角的锁不安全呢。用了 HTTPS 是不可能回去的了,同时怎么能保证别人用的浏览器不是 Chrome 浏览器呢,于是毅然决然的将所有将图片升级为 HTTPS 了,然而在对比后各家的对象存储服务,我选择了腾讯云,先放一张比对图: + +| 运营商 | 价格(元/GB) | 活动 | +| ------ | ------------------------------------------------ | ---------------------------------------------------------------------- | +| 阿里云 | HTTP 0.24
HTTPS 需要额外加一点请求费 | 无 | +| 腾讯云 | HTTP 和 HTTPS **统一 0.21** | 前 6 个月每个月送 20GB 国内流量 | +| 七牛云 | HTTP **0.24** (超出免费 10GB)
HTTPS **0.28** | 每个月送 **10GB HTTP 流量** (国内外均可使用) 以及 5 万次动态加速请求数 | + +可以看到如果你每个月的请求流量都在 10GB 以内,且不用 HTTPS,七牛云肯定是最好的选择,然而我目前的站点是需要 HTTPS 的,并且七牛的 HTTPS 费用高出其他两家,甚至可以说,用了 HTTPS,就没必要选择七牛云!一没优势,速度优化不到哪里去,二是你完全可以相信大厂,三是服务费用还比两者贵。同时我云服务器也是腾讯云的,肯定优先选择腾讯,于是把对象存储换为了腾讯云,早知道就一开始就直接用腾讯的对象存储服务,果然还是花钱实在,白嫖太麻烦了。 + +> 参考链接 [阿里云、腾讯云、七牛云 CDN 对比](https://blog.txzhou.com/website/compare-cdn.html) + +## 最后 + +本文的标题并不是说七牛云不好,而是我所遇到的情形让我放弃了使用七牛云,相信你看完了上面所说的,能对你的网站有个存储有个明确的配置,你在哪买服务器了,还是在哪去买其他相关的业务,没必要花费时间去折腾,说到底还是花钱实在。并且价格实际上对一个小网站来说,已经是可以非常低了(当然还是有些人会想着白嫖) + +总结下来其实就是,如果你的网站不准备挂 SSL 证书,也就是通过 http 请求访问,那么白嫖七牛云,没问题,好用,但如果你的网站一旦挂了 SSL 证书,我的建议是直接删了七牛云的对象存储。 diff --git "a/blog/lifestyle/\344\270\272\344\275\225\346\210\221\351\200\211\346\213\251\346\227\245\345\244\234\351\242\240\345\200\222\346\225\262\344\273\243\347\240\201.md" "b/blog/lifestyle/\344\270\272\344\275\225\346\210\221\351\200\211\346\213\251\346\227\245\345\244\234\351\242\240\345\200\222\346\225\262\344\273\243\347\240\201.md" new file mode 100644 index 0000000..4a6ff14 --- /dev/null +++ "b/blog/lifestyle/\344\270\272\344\275\225\346\210\221\351\200\211\346\213\251\346\227\245\345\244\234\351\242\240\345\200\222\346\225\262\344\273\243\347\240\201.md" @@ -0,0 +1,78 @@ +--- +slug: why-i-turn-night-into-day-to-code +title: 为何我选择日夜颠倒敲代码 +date: 2022-01-03 +authors: kuizuo +tags: [随笔] +keywords: [随笔] +--- + +![](https://img.kuizuo.cn/20230308001404.png) + +![](https://img.kuizuo.cn/202307220734551.png) + +上图是我两段假期的睡眠周期,你可以发现每日的时间都在不断延后,意味着我熬的夜越来越长,甚至达到了颠倒生物钟的程度。 + +其实我很早就想写这个话题了。因为我是一个熬夜大户,经常动不动就是 3,4 点睡觉,甚至有时候是日夜颠倒的作息习惯,如同过美国时间般,在 0 点到 6 点这时间段几乎是我的 coding time。但与常人想法不同,我巴不得在保持这种作息状态。我会分以下几点来阐述我这一不正常的行为,相对于我而言的好点。 + + + +## 对我而言的好处 + +### 安静 + +夜生活过的人应该最清楚了,那种静可以算上是幽静了,加上黑夜的承托与屏幕前的微光,如果恰好此时耳机还播放着音乐,好了,直接进入自我境界。 + +这种安静与图书馆的安静是根本无法相比的,图书馆可能还有走动声,而凌晨你唯独只能听见自己的脚步声(不排除有上厕所的室友,有时候可能被其吓得一跳),最主要还是黑,你的视野中除了屏幕的信息外几乎很难有其他东西去干涉你。 + +你问我用的什么键盘?程序员不都用静电容的键盘吗?机械键盘的声音在晚上敲怕不是给室友去世器。(tips: 室友包括我) + +### 琐事 + +安静也许远远还不够,想要达到与世隔绝的地步,在互联网这个时间段最简单的办法就是断网。如果事情都与你毫不相干,那也差不多已经与这个世界说拜拜了,但在白天你还是有一定的存在感,会有一些莫名的琐事干涉你当前正在进行的状态。 + +我就说一个最简单的例子,假如你正在编写一个功能的时候,此时手机正好弹出一条信息(通常为广告),此时大多数人基本上就去看手机去了,意志力好一点的可能看完直接回到手头的任务上,差一点的可能就是几十分钟的短视频,也许很夸张,但确实如此。 + +所以我一般都会在 0~6 点开启勿扰模式(睡眠模式),但在白天却又不能错过很多重要的信息。 + +### 思考 + +可能对我而言,灵感通常是在睡前(准确说是夜深时)产生的,同时脑子里想的东西也会比白天而言多,想的多自然写的东西也就多,所以也就是我为什么很多代码 Commit 与博客编写时间都是在 0~6 点这个时间,就连这篇文章也都是在夜间写的,白天实在是想不出什么点来写。 + +也许与静也有相关,白天普遍嘈杂的声音,说实话很难让人能静下来去冥想,但在夜晚,静确实它唯一的资本。 + +**唯有夜深人静,才有自我深思。** + +### 工作效率 + +在我亲身体验下来,可以毫不夸张的说,我在夜晚的工作效率可以是白天的 3,4 倍,没错就是这么恐怖。 + +至于原因,大概率(99%)是由上面几点导致的,所以也就是为什么我巴不得在夜深人静的时候开始 doing,导致这异于常人的作息规律。尤其在夜晚,是很难感受到时间的存在,有时候可能天亮了才知道。这时才发觉自己又熬了一个晚上。 + +但回想 1 小时抵 3 小时,怎么想都是赚,换做你,会考虑这样的作息吗? + +## 对我而言的坏处 + +不过导致这种规律的原因,与当天任务未完成,要到凌晨加班加点完成。或者是一时上头,忘记了时间的存在,手头的任务打消了睡意。久而久之,作息时间点不断延后,就导致现在现在的生物钟。 + +其实坏处都不用写,是个正常作息的一眼就能看出来,我也不细说了。 + +但往往事与愿违,上面的这种作息行为都有点压榨白天的时间,因为白天总要上学,上班,周末又不可能一次性就调整成这样的作息,所以我这种情况一般也都发生在长假期,例如寒暑假(包括这次的寒假)。但其实日常生活中大多时间还都是夜间做事,只是假期的夜间时间相对延后那么亿点。 + +与好处相比,除了猝死其他都还能接受。 + +:::tip 时隔一年后的补充 + +事实上,这种作息方式对身体的危害不言而喻,现在的我能明显的感觉到身体的持续能力不足(说人话就是虚了),虽说本就瘦小的身体,在经历这种生物钟的摧残下也变得更加消瘦。可即便在我意识到的情况下,我对此也很难做出改变,也许只有那天大病将临,或者才会好好爱惜这副躯体。 + +::: + +## 总结 + +原本标题我是起为什么喜欢熬夜,但后来我并不认为我是在熬夜,只是时间相对错乱,每天睡觉的时间与正常作息并没有差别,唯一的差别估计就是天亮了我睡了,天暗了我醒了。主要还是强调作息,所以便有了这个标题。 + +当然我写这篇文章也不是想要倡导夜间做事有多好,每个人都有属于自己适应的时间段,比方说有些人就喜欢早晨背单词,而有些选择在睡前,道理都是同样的。我只是觉得我这种习惯比较异类,也许也有人的作息习惯和我臭味相投。 + +:::danger ⚠️ 非专业人士,请勿模仿! + +::: diff --git "a/blog/lifestyle/\345\205\263\344\272\216 restful api \350\267\257\345\276\204\345\256\232\344\271\211\347\232\204\346\200\235\350\200\203.md" "b/blog/lifestyle/\345\205\263\344\272\216 restful api \350\267\257\345\276\204\345\256\232\344\271\211\347\232\204\346\200\235\350\200\203.md" new file mode 100644 index 0000000..ace9c5b --- /dev/null +++ "b/blog/lifestyle/\345\205\263\344\272\216 restful api \350\267\257\345\276\204\345\256\232\344\271\211\347\232\204\346\200\235\350\200\203.md" @@ -0,0 +1,155 @@ +--- +slug: restful-api-url-definition +title: 关于 restful api 路径定义的思考 +date: 2023-11-30 +authors: kuizuo +tags: [杂谈, restful] +keywords: [杂谈, restful] +--- + +关于 restful api 想必不用多说,已经有很多文章都阐述过它的设计原则,但遵循这个原则可以让你的 API 接口更加规范吗?以下是我对 restful api 风格的一些思考🤔。 + + + +## 思考 + +此时不妨思考一个问题,现在以下几个接口,你会怎么去设计 url 路径? + +- 查询文章 +- 查看文章详情 +- 创建文章 +- 更新文章 +- 删除文章 +- 查看我的文章 +- 查看他人的文章 + +前 5 个接口想必不难设计,这边就给出标准答案。 + +- 查询文章 `GET /articles` +- 查看某篇文章详情 `GET /articles/:id` +- 创建文章 `POST /articles/` +- 更新文章 `PUT /articles/:id` +- 删除文章 `DELETE /articles/:id` + +当然,我相信肯定也有`GET /article—list` `POST /add-article` 这样的答案,不过这些不在 restful api 风格的范畴,就不考虑了。 + +而这时 查看我的文章 或许就需要稍加思考,或许你会有以下几种方式 + +- `GET /my-articles` 从资源角度来看肯定不好,因为此时在 url 不能很直观地体现请求资源,同时在控制器文件(controller) 就与 article 分离了,并且还占用了 / 下路径。 +- `GET /articles/mine` 则又不那么遵循 restful api 风格,挺违和的。 + +那么这时不妨遵循 **资源从属关系**,在这里 文章所属的对象就用户,因此查看他人的文章可以这么设计`GET /users/:userId/articles` 获取特定用户(userId)的文章列表。 + +而 查看我的文章 同样也可用此 URL,只需将 userId 更改为自己的便可。从 api 的 URL 来看是很舒服了,但是从代码开发的角度上问题又有了问题了。。。 + +对于 user 资源,是不是也有查询,创建,更新,删除等接口,即 查询用户 `GET /users`,创建用户`POST /users/` 等等。。 + +我是不是就需要在 user 这么重要的资源控制器上去添加一些其他方法,所对应的代码就如下所示 + +```jsx +@Controller('users') +export class UserController { + constructor(private userService: UserService, private articleService: ArticleService) {} + + @Get() + async list(@Query() dto: UserQueryDto) { + return this.userService.findAll(dto) + } + + @Get(':id') + async info(@Param('id') id: number) { + return this.userService.findOne(id) + } + + @Post() + async create(@Body() dto: UserCreateDto) { + await this.userService.create(dto) + } + + // 省略有关 User 部分接口,以下是其他 user 下的资源接口 + + @Get(':userId/articles') + async articles(@Param('userId') userId: number) { + return this.userService.findAll(userId, articlesId) + } + + @Get(':userId/articles/:articlesId') + async articles(@Param('userId') userId: number, @Param('articlesId') articlesId: number) { + return this.articleService.find(userId, articlesId) + } +} +``` + +换做是我,肯定不会希望将用户的代码与文章的代码混杂在一起。解决办法也是有的,可以额外创建一个新的 UserController 文件,专门用于获取用户下的资源(这里指 article),这样可以 即与原有针对 user 资源进行解耦,有可以有比较清晰接口分类。 + +:::warning + +不过针对这种情况我可能的解决办法是下会额外 **起一个别名**,例如 author,将 `/users/:id/articles`转为 `/authors/:id/articles`,不过在这里指向的是用户 id,而不是新建一个 author 实体(资源)。 + +这里的 id 会根据情况而定,假设业务中需要创建 author 实体的情况下,对 author(作者)这一身份有一些操作,如普通用户变成一个作者,获取所有作者,那么这么做就再适合不过了。 + +在比如说一个更鲜明的例子 商店(store) 与 商品(product)。 + +::: + +业务再稍微复杂一下,现在要为业务增加以下几个功能,你又会如何设计 + +- 收藏他人文章 +- 获取我收藏的文章 + +答案应该会有两种,即 `POST /articles/:articleId/collections` 与 `POST /collections` + +而这就令我特别头疼,因为这两个都符合 restful api 风格,也确实都能很好的满足业务功能。于是在我尝试抓包拥有相关的网站后,我发现几乎都是后者的 url。后来一想,前者更像是获取某种资源,而不是用于创建资源。后者确实更能胜任多数场景,比如说现在我需要收藏某个专栏,那么我用 `POST /collections` 足以胜任,只需要传递 条目id与条目类型,后端根据这两个条件找到对应条目数据便可。假设后续业务多一个资源需要收藏也不成问题。但换做前者的话,就得再多写一个重复性接口。 + +## 抽象资源 + +restful 更多是针对实际存储的资源,核心是名词,对于增删改查的业务可以说非常适合,但现实情况下不只有增删改查,就例如上述的收藏功能。 + +对于一些个别接口需要另外表达,如 登录 `POST /login`、获取个人信息 `GET /profile` + +对于一些非增删改查的操作,还是使用 RPC 式的 API 更为实在,即 **`POST /命名空间/资源类型/动作`**,至少不用再为某个操作决定 PATCH/PUT 还是 POST/DELETE。 + +## 针对同一实体,区分不同用户 + +问题还没结束,不妨碍继续使用上述文章的例子,针对 文章 这一实体,又要怎么定义(区分)用户与作者或管理员路径呢? + +管理员所看到的数据肯定远比用户来的多,如果使用同一个接口(如 `/articles`),那么业务代码必然会十分复杂。 + +使用不同的端点(end point) 是个解决方法,例如管理员在请求前添加 manage 或 admin,如 `/manage/articles` 或 `/articles/manage` 这样只需要多一步判断请求用户是否拥有管理的权限。 + +但对我个人而言,我一般都会以在一个命名空间下(这里指 `/articles`)编写,像前面的 `/manage/articles` 我是直接 pass 的。 + +在设计接口的原则就优先以拥有者的身份来设计,在去设计其他用户获取这个资源的接口。就比如说上述 `article` 为例, 针对增删改查而言,都是用于这个资源的拥有者可操作的,那么所获取到的数据就是尽可能符合拥有者需求的。而这时如果要将资源给其他角色请求,就会根据情况设计,如 + +- `GET /articles` 获取我的文章列表(针对拥有者) +- `GET /articles/query` 查询文章(针对所有用户) + +## 权限区分 + +在 restful 中有两个概念:resources 与 action,因此只需要定义好权限标识码便可,还是以文章举例,如 `article:read` `article:create` `article:update` `article:delete` ,这里的 resources 对应的就是 article ,action 则是 read,create 等。将这些权限码分配给不同的控制器方法,在某个请求的时候判断用户是否拥有这个权限码便可。 + +## 资源粒度问题 + +但是复杂的实际业务中,仅仅单靠 restful API,往往需要发送多条请求,例如获取某篇文章数据与作者数据 + +``` +GET /articles/1 + +GET /articles/1/author +``` + +要么两条请求获取相应数据,要么为调用方“定制”一个接口,如`GET /getArticleInfo`,这样只需一条请求便可得到想要的数据。但这个就破坏了 restful API 接口风格,并且在复杂的业务中,比如说还要获取博文的评论等等,后端就要额外提供一个接口,可以说是非常繁琐了。相比之下 [GraphQL](https://graphql.org/) 就更为灵活了。 + +## 写到最后 + +在我写这篇文章之前,我尝试抓包看过很多网站的请求 url,见识到各式各样的 url 路径,基本上很难找到遵循 restful api 风格的网站,绝大多数的操作除了获取外用 GET,其余全用 POST 。对于复杂的业务,restful api 风格实在过于难以胜任。 + +如果说变量命名是编程最大的痛苦,那么写接口最大的痛苦我想就是定义 url 路径了。 + +## 相关文章 + +[RESTful API 对于同一实体,如何定义管理员和用户的路径?](https://www.v2ex.com/t/482682) + +[RESTful API设计经验总结](https://blog.51cto.com/LiatscBookshelf/5427906) + +[为什么很多后端写接口都不按照 restful 规范?](https://www.zhihu.com/question/438825740) diff --git "a/blog/lifestyle/\346\255\243\350\242\253\346\266\210\347\243\250\346\256\206\345\260\275\347\232\204\350\200\220\345\277\203.md" "b/blog/lifestyle/\346\255\243\350\242\253\346\266\210\347\243\250\346\256\206\345\260\275\347\232\204\350\200\220\345\277\203.md" new file mode 100644 index 0000000..c9f5a58 --- /dev/null +++ "b/blog/lifestyle/\346\255\243\350\242\253\346\266\210\347\243\250\346\256\206\345\260\275\347\232\204\350\200\220\345\277\203.md" @@ -0,0 +1,62 @@ +--- +slug: patience-wearing-out +title: 正被消磨殆尽的耐心 +date: 2022-08-15 +authors: kuizuo +tags: [随笔, 感悟] +keywords: [随笔, 感悟] +--- + +在以前,我可以花费 4,5 个小时的时间专注于一件事情上,并且丝毫没有不耐烦的意思。而且这种状态能持续个好几天。例如拼个积木,拼个拼图,写个代码等等。专注个半天根本不成问题。 + +当然有很大一部分是因为兴趣,要让我做一件乏味的事,别说坚持了,开始可能都成一个问题。但现在的我对很多东西都难以提起兴趣,要是放在以往,都能让我兴奋个数天。可现在,很难。 + + + +不知为何,感觉专注一件事变得特别困难,总沉不下心来。总感觉一切东西不咋那么美好,都很乏味,难以提起内心仅留的一丝兴趣,总感觉自我内心好像变了个特别无趣的人。这也间接导致我的耐心也不在像之前那样,现在的我遇到一些繁琐的事情,比如写报告,办手续,做核酸等等,我都会感到十分厌烦。 + +像之前如果有人问我代码相关的问题,即便是我没接触过的知识方面,我都愿意去尝试了解学习为他解答。可现在,问我不知道的东西,我张口就是不知道。就算我知道,我的回复也往往是丢给他一个相关链接,让他自己研究琢磨,仿佛在告诉他我极不耐烦的样子(当然也有可能是这类问题问的太多了)。之前打车哪怕师傅晚个 10 分钟来接送我都能接受,现在 3 分钟我就已经有点坐立不安了。 + +对于这种耐心的转变,我自己都认为有点不太思议。我不知道是不是由于之前太过于耐心,然后种种环境下不断磨练中,导致耐心值不断的下降,最终导致在很多事情都感觉不耐烦,难以平静。 + +## **耐心值下降** + +自我分析了耐心值下降的可能原因: + +### **提不起兴趣** + +说到兴趣,这可就不得不提起 5 年级开始就接触[手部极限运动](https://baike.baidu.com/item/手部极限运动/2431307)中的转笔,那也算是我第一个对其感兴趣的东西。期间不断在尝试其他的手指极限运动并持续坚持了整整 8 年,直到高考结束便退坑了。至于为何不再坚持下去,有很大原因是因为 QQ 被永久冻结导致一些社交圈失联,再加上当时假期一直在学习编程。可以说没有这些意外,我依旧会坚持我这持续这份爱好,并且会将其爱好当做一个我的吃饭的本事。 + +目前仅余的手指极限运动也就只有转手机了。同样的对于游戏也不再有新鲜感去尝试,偶尔还会下载英雄联盟玩上熟悉的英雄回忆当年的高(下)光(饭)时刻(当然游戏方面也和 QQ 冻结连同游戏账号一并冻结有关)。 + +在回到目前主业的编程在我接触了进 3 年内,期间踩的无数的坑,做着重复的业务需求,尤其是所感兴趣,但到头来却又不可完成。渐渐地不再有当初以往的热情,不再乐此不疲。即便完成了一个自我认为成就感爆满的事情,也容易在接下来几天的时间内消磨下去。 + +当所感兴趣的东西太多,对于新事物就不再容易提起兴趣。 + +人的耐心都是被一些自己感兴趣,但是又不可能完成的事,一点一点消磨殆尽的。 + +### **过快的生活节奏** + +现在技术发展是真的快,有很多新的突破和发现。生活质量也在不断变好,但这也间接导致生活节奏过快,社会更加内卷。变化太快。现在的生活总让我感觉一天好像什么事情都没干,一从座位起来就已经是第二天的凌晨。前一秒还在想着吃什么晚饭,后一秒就到了吃夜宵的时刻。看视频也不再是原速播放,而是更快的 1.5 倍,甚至 2 倍速。原本可能要 2 个小时完成的东西,硬是被压缩到 1 个小时内完成。 + +在这样的快节奏生活工作下,耐心值怎能不下降?又怎能静的下心来去完成任务? + +### **走神** + +在写代码任务的时候,有时候一个准备实现的功能好像之前实现过,然后去翻看之前的代码,然后就发现写的还不够好,虽然能用,但总归少些什么,于是就开始重构,然后慢慢的就偏离原本的工作流,导致大部分的时间都在重构该功能,而不是在原本的任务。不过这样也确实完善了该部分的代码,至少不是一次无意义的重构。 + +有时候看到一些推荐类的文章或者视频,可能看到中间有一个之前没遇到过的新鲜玩意,然后就开始打开官网,然后去看相关示例。刷着刷着浏览器网页打开多了,哪里还会在意每个标签页的标题,早都给慢慢的标签挤的只剩 logo 了,这时候我大概率是直接整个浏览器关闭,在打开新窗口,而一开始的所推荐的内容,可能早已忘得一干二净了。 + +但这样也确实加深了我中途对这玩意的印象和认知,但却让我也忘了一开始的本意。不过一生中要接触许许多多的新的美食,普通人哪里有精力和时间去一遍遍的尝试,要做的只是将自己能够吃到吃进嘴里,体会其中的美味这就足够了。对于任何新鲜事物的尝试也应如此。(有点扯远了) + +所以久而久之,往往越想专注的时候,越容易被细微的东西吸引注意力。专注不下去了,耐心也随之消磨下去。 + +## **消磨耐心** + +可能是因为经历太多耐心的过程,耐心在无形之间被不断磨灭,最终在不断消磨下导致原本不厌其烦 🙂 的样子变的极其厌烦 😣。 + +在这过程中,耐心也实实在在的告诉我正在成长,也在警示我不该松懈。即便内心再怎么烦躁,所表现的也应心照不宣。 + +消磨耐心的过程不妨是在打磨工具,将一个钝器打磨成一个利器。绝大多数人一开始的耐心与容忍程度都是非常高的,但随之不断的经历。而多数人的耐心自然也在不断地下降,往往成长的也就越多。 + +**慢慢磨练一个人的耐心,才更能考验一个人的心智。** diff --git "a/blog/lifestyle/\346\265\205\350\260\210\344\270\252\344\272\272\345\255\246\344\271\240\346\226\271\345\274\217.md" "b/blog/lifestyle/\346\265\205\350\260\210\344\270\252\344\272\272\345\255\246\344\271\240\346\226\271\345\274\217.md" new file mode 100644 index 0000000..efd5389 --- /dev/null +++ "b/blog/lifestyle/\346\265\205\350\260\210\344\270\252\344\272\272\345\255\246\344\271\240\346\226\271\345\274\217.md" @@ -0,0 +1,60 @@ +--- +slug: learning-style +title: 浅谈个人学习方式 +date: 2022-06-10 +authors: kuizuo +tags: [杂谈] +keywords: [杂谈] +--- + +临近考试周,又要开始准备复习,顺带总结下自己平常的学习方式与一些感慨。 + + + +我个人主要的两种学习方式,**主动学习**与**被动学习** + +## 主动学习 ✍ + +通常来说,主动学习往往是一件痛苦的事情。就如考试周,平常的课该去在课堂上睡觉,该不去的在宿舍睡觉,到考试周,一学期的课没听,但一想到不复习就有可能面临挂科,或多或少都会复习。考试摆烂,挂科惨淡。 + +除了上面是会导致坏结果而去学习,那么还有一种则是与之相反,也就是所需,所感兴趣的去学习。 + +例如我想要实现一个酷炫功能,或者是一个需求时,而其中需要的知识点便会去了解与学习。 + +对于前者而言,其过程并不舒服,甚至可以说是逼自己学习不喜欢的东西,通常我是不如不学的,即便学了在以后也难以记起,除非不学的结果比较严重(如挂科)。而后者情况便不同,兴趣是最好的老师,甚至都不需要借助外界因素去激励,便能有一个很好的学习成果。 + +如果没有兴趣或需求的情况下,那么主动学习一定要定制一系列的目标,如果没有明确的目标,学习将会显得十分迷茫。我有很多时候便是这样,想去主动去学习诸多技术栈,不知从何下手。 + +## 被动学习 📘 + +其实可以说是利用业余时间学习的一个方式,首先我会想我一天主要做的事情(通常是与学习无关),比如刷各个平台(b 站,抖音)的视频,刷知乎,qq 群闲聊等等,这里只是以我一天的大部分日常举例。 + +此事不妨关注一些与学习类的账号,包括但不限于加入一些技术交流群, up 主,公众号,博主等等,当你每次刷这类平台时,系统很自然就会给你推送相关内容,比方说 b 站,我日常会在上面刷一些鬼畜视频,生活娱乐视频,但也会关注一些技术类的账号,在娱乐的同时,还能不经意间刷到一些编程知识(知乎,微信公众号同理),这送上门的知识不香吗? + +再比如 watch 一些开源项目,订阅一些博客文章,定时推送 Hacker News 周刊等等,在他们发布一些重要内容的时候能及时通知到你。虽然这样每天邮箱时常处于未读状态,但如果打开某一条邮箱查看其中的内容,说不定又是一次不错的收获。 + +当然,这样学习肯定有一定弊端。首先所摄取的知识过于笼统,一般都是由别人推荐的,甚至有可能会浪费你人生中的几分钟,因为这些知识点可能你已经掌握了,或者这些知识确实没什么干货。其次所获取到的知识往往是整个知识面的冰上一角,或者是某些新型技术,但想要深入去学习还是得按上面的主动学习。 + +被动学习主要的作用能让你在不学习的状态下,还能获取相对应的知识。听起来可能觉得很卷,无时无刻的在学习,但我认为在娱乐中学习往往不会显得像主动学习那般有种疲惫感。 + +我在被动学习中就间接了解到许多的前沿的技术栈,以及了解到许多之前没使用过的语言、框架特性,而这些都是直接送上门的知识点,而我要做的只是关注一些账号,订阅一些文章或站点,而不用自己去茫茫知识海洋中去寻求。 + +## 两者的权衡 + +我时常处于这类被动学习状态,因为我对很多技术点都很感兴趣,但又不知道从何下手,而当我刷到我所感兴趣的东西时,我才会开始转为主动学习。之所以会出现这样的学习状态转型,有很大一部分是因为**兴趣感已经没有一开始所学的那么强烈,甚至可以说有些许乏味**。 + +在一开始接触时,我是抱着强烈的兴趣感去学的,处于一种非常积极的主动学习状态,当时所获取的知识是无法用现在同等时间所堪比。然而在兴趣感消散,这种状态都为浮云。主动学习的频率减弱,每天都在做着与学习无关的事情,这期间与知识没有任何的往来。 + +直到将每天所要做的事情,间接的转为被动学习的方式,至少有在学习,而不是在停步,即便是行走 1 厘米,那也是米。 + +## 总结 + +最好是对感兴趣的方面学习,这样即有动力去学习,有不会感到厌烦,并且兴趣是能陪伴很久的,也往往能坚持下去。 + +无论是任何事情在任何时候下都应该保持的是一种不断坚持的状态,打游戏也好,学习也好。 + +一段时间不打游戏,手感生疏,再次接触就没状态。一段时间不学知识,容易忘记,再次使用就没印象。 + +大部分人都是如此,可谁又不希望保持一种不断坚持的状态。毕竟不是每个人都有充裕的个人资源与时间资源,很多时候这些资源并不是由自己分配的,有可能是亲朋好友,也有可能是上司老板,最有可能是生活所迫。。。 + +**唯愿此生,岁月静好** diff --git "a/blog/lifestyle/\346\267\261\350\260\210\344\270\252\344\272\272\345\257\271\346\226\260\346\212\200\346\234\257\347\232\204\347\234\213\346\263\225.md" "b/blog/lifestyle/\346\267\261\350\260\210\344\270\252\344\272\272\345\257\271\346\226\260\346\212\200\346\234\257\347\232\204\347\234\213\346\263\225.md" new file mode 100644 index 0000000..7699f1e --- /dev/null +++ "b/blog/lifestyle/\346\267\261\350\260\210\344\270\252\344\272\272\345\257\271\346\226\260\346\212\200\346\234\257\347\232\204\347\234\213\346\263\225.md" @@ -0,0 +1,149 @@ +--- +slug: talk-new-technologies-opinion +title: 深谈个人对新技术的看法 +date: 2022-10-15 +authors: kuizuo +tags: [杂谈] +keywords: [杂谈] +--- + +目前技术圈的发展速度可谓是有目共睹,尤其是前端,每隔一段时间就出新的技术,可以说让很多初学者非常畏惧,没有一个明确的方向不知道从何学起。 + +同时也有很多人,只局限于使用手头已掌握的技术,而不愿去尝试新技术。举个例子,如今 vue3 都已经正式发布,但仍还有停留在 vue2 不愿尝试 vue3 的开发者。而 java 都发布 18 版本了,可还有很多人都还使用着 java8,这种现象可以说是非常常见了。 + +这里说说我个人编码经验与看法,**仅作为个人观点,没别的意思**。 + + + +熟悉我的人应该都知道,我对很多新鲜的技术有一种难以用言语表达的情感,又哭又笑。属于是那种看到感兴趣的就会开始尝试,在之前也许更强烈。 + +驱使我去接触的原因无非就以下几点: + +- 开发体验、性能提升 +- 对已有技术的厌倦感和对新鲜事物的好奇心 +- 更多机会与方向 +- 对未来技术趋势有更好的了解 + +## 写不完的代码 + +首先要知道一点,在任何的软件开发迭代中都没有最终形态的代码。说白话就是代码都是不断更新的,永远写不出最好的代码。 + +你能看到如今很多开源项目或者商业项目都在不断新增代码或者功能,除非作者不维护了,不然这份代码可以说写到世界末日。 + +究其原因还是因为社会不断在发展,硬件升级,性能提升,不断的业务需求。毕竟人都在进步,社会难道还不能进步。所以必然会有新的技术出现,只是出现的时间快慢,与技术难点突破。 + +## 开发体验、性能提升 + +毋庸置疑,在购买方面,肯定是买新不买旧,同样的在技术(软件更新)方面也是则从用新不用旧。驱使软件和框架更新的原因也就是新增了某些功能(特性),对用户(开发者)的体验有所提升。一般而言比较少的会存在反向更新的操作,这里除了某些国产软件~~(如某信,某 Q 等等)~~ + +就我对此的看法也是如此,假设一个开发工具启动花费了 5s 钟启动,而在它的最新版只需要 1s 钟就能够启动,你会选择更新尝试吗?。再比如一个框架原先的代码需要 10 行代码才能实现的功能,由于新版本提供一个语言级别的语法糖,使该功能只需一行。 + +并且我对开发体验非常在意,尤其是不好用,或者不好配置的东西,我基本秉持能不用就不用的原则,像 vue2 与 webpack 就是这样,我跟愿意使用 vue3 或 vite。诸如此类的替换有非常多,便不一一列举。 + +要我肯定毫不犹豫的更新去使用,但有些人可能对此提升不是很在意,又或者是升级的成本相对较高,也可能是因为这个开发工具(框架)他用的比较少,更新的意义自然就不大。 + +## 对代码厌倦和对新事物的好奇心 + +我写代码时常处于三分钟热度的状态,有些东西可能也就一开始的时候感觉比较新奇,然后就不了了之了。我对此的看法主要还是容易对代码产生厌倦感,不想写代码,不愿意学习。当厌倦感产生了,自然而然就放弃编写,也就是三分钟热度的状态。不过也能侧重体验一点,那就是肯定我对此不是那么感兴趣,既然不是那么感兴趣的东西,又何必在写下去呢? + +接着过段时间又遇到了一个新的技术,冲击了我的好奇心,开始尝试。 如此重复,就会发现啥都学了一点,但实际是啥都没学到。但至少,让我肯去学习,而不是在原地踏步。而从心底里就想接触的新鲜事物,每次接触到就能满足自己内心的好奇心,就这一点我便知足了。 + +### 重构的艺术 + +如果回头看看自己 1,2 年前写的代码,会发现原来自己也曾写过丑陋不堪的代码,也成为过自己所讨厌的样子。如果这份代码我将来还会用到,那么我就尝试去重构,也许在当时还不支持某种特性,代码就无法简化。基于现有的水平,便会发现很多代码都有可改善的地方,可以化繁为简。代码重构属于将杂乱摆放的东西,收拾的整整齐齐的样子。重构是在提升观赏度和舒适度的同时,还减少 bug 的诱发概率。 + +> 在编程语言级别水平上,也就是我为什么会想去使用最新的版本(ES2022,TypeScript 4.9),即便是兼容性的问题,我也会去使用,就是因为能够满足我对代码的舒适度,这便足以。 + +### 生态与解决问题方面 + +我是很感谢新技术的出现,他实实在在的解决了一些我已有的痛点,提升了我的开发体验。当然它也让我踩了无数的坑,也折磨过我。但不可否认的是,我的自我解决问题的能力也在不断提升,如果我学的是一门比较流行的技术,那么我所遇到的问题,很有可能别人也遇到过,并将他的解决方案分享出来。而我就很容易根据报错描述找他的解决方案来解决我的问题。但在新技术下,用的人自然而然就少了,所分享的问题解决方案也就少了,所以在这种环境下,我就需要自行翻看源码,查阅文档,提出 issues 才能够解决问题。自然而然解决问题的能力也就有所提升。 + +像流行框架能有这么有问题解决方案,就是因为强大的生态,同时这也是生态好处之一。 + +> 因此也有很多人顾忌使用新技术,就是因为遇到问题不知如何解决。包括我也是,但通常我会观察一段时间,等成熟了我才去尝试,而不是直接上手,避免踩一些不必要且耗时的坑。 + +### 总是活在舒适圈 + +在圈内有着熟悉的环境,与认识的人相处,做自己会做的事,所以会感到很轻松、很自在。但是当踏出这个圈子的界限的时候,就马上会面对不熟悉的变化与挑战,因而感到不舒适,很自然的想要退回到舒适圈内。 + +我在阅读英文博客的时候,我也时常感到不舒适,阅读不下去。我也很想回到舒适圈,使用翻译软件来翻译但是这样就会导致我非常依赖翻译软件,就间接失去了一次英文环境与英语能力的提升。 + +长时间待在舒适圈,会让自己过得很舒服,但是却很难提升自己。不过想想也是,**提升的过程不就是苦尽甘来**。 + +以目前来看有一种这样的学习趋势,别人学什么,当下什么技术火,就去学什么。我其实特别反感这种现象,也不推崇这种学习理念。我会做出我的解释: + +首先,什么技术会火就学什么,这固然没什么问题,如果一门技术没有热度,没有生态,那么学了的意义不大,一是难有长久稳定的技术发展,二是不能将技术变现。而绝大多数人之所以选择火的技术,有很大一点是因为有前人给他铺了很多“路”,如学习指南,思维导图,视频教程仿佛跟着学就能成为编程大牛似的。可一旦没有这些,就不知道该如何下手。始终都是跟着别人步伐学习,思维很难扩散出去。 + +并且这种现象必然会导致内卷,首先看看国内的技术,Vue 和 Spring boot 的可以说 10 个 web 开发程序员中有 8 个技术栈是这套,比麻花还是卷了,可薪资呢? + +这里我并不想贴相关的薪资图片,你完全可以自行去了解,但是我可以肯定且直接告诉你,React 的薪资普遍会比 Vue 高上一截,而 Java 后端开发,如果技术只停留在 CRUD 的层面,工资普遍也高不了多少。 + +**如果你不去拓展自身的技术栈,不多去了解一些未来的可能会火的技术,还停留在当下,活在舒适圈。那么薪资大概率不变,并且自身会有很大被劝退的概率。** + +与时俱进,这是我认为不断学习新技术,提升自身技术栈,非常重要的一点。**过得舒服,反而过得难受** + +## 更多的机会与方向 + +技术更新迭代越来,也带来越多的机会,这对于接触前端的我感到尤为明显。假设当下又出了某某技术,那么必然会引起软件开发者的关注,于此同时就带来了维护者,贡献者,甚至是一些金主投资商。像 [Tailwind CSS](https://tailwindcss.com) 与 [Vercel](https://vercel.com/) 就是一个很好的例子,两个前端明星项目,有兴趣可以了解它们的故事。 + +**不过这种机会在国内不太多见,反而在国外特别普遍。** + +但必须要承认的一个事实,如今技术发展过于迅猛,加上目前就业行情不容乐观,当别人了解过的东西,你却不了解,那么别人所能遇到的机会自然就比你多。说的难听点也就是没有对技术提升的想法,今后项目迭代的过程中使用到一些前沿技术就难以胜任。 + +### 没有目标的学习,等同于乱学 + +**没有一个明确的目标,学任何(新)技术都是乱学,充其量也就只比不学好一点。** + +这在我初学阶段尤为明显,我一开始也不清楚我以后会从事什么行业,可以说是什么都乱学一顿。在我的一篇年终总结 [2019.7-2020.7 编程年记](/blog/2020-year-end-summary) 中可以说是尤为明显,尤其是在 [定一下明年的目标](/blog/2020-year-end-summary#定一下明年的目标) 的段落中,最后我真正深入学习的也就是只有 Web 开发。 + +我相信很多初学者也会遇到类似的问题,不知道学什么,想学好找工作的但是薪资不高,想学感兴趣的但又不知如何下手。说实话,要我回到当初,我也难以抉择。也有可能处于摆烂状态或是乱学一同,到头来啥都会一点点,但是又好像啥也不会的样子。 + +这里我是奉劝先定一个短期的目标,为了这个目标我要去学习哪些技术知识。这里就说我未来一年的目标为例:我未来一年想写开源项目,为开源社区做一份微薄贡献,乃至从事开源行业。那么我就需要了解写开源我需要那些预备知识,例如 Github 的使用,项目规范,英文交流等对应开源项目的技能知识,这才是我所该学的,并且能够实实在在用到的,且对我未来有用的。 + +## 对未来技术趋势有更好的了解 + +**当你了解的技术越多,你就越能知道自己适合哪些技术。**并且当你去尝试过后,更能加深你对某个技术的信仰。 + +在未来技术只会越来越多,因为当下要解决太多问题,有太多的业务需求开发。只要不断有需求,就不断会有技术更新。但技术更新必然是朝着好的方向去发展,即技术趋势方向。而了解的越多,能看到技术趋势也就更远,方向就更难偏移。 + +像我目前就比较看好未来 js/ts 的发展,这也是使我从逆向和爬虫转到 Web 开发行业上,并且将会长期发展下去。 + +但很多程序员就缺乏这种对**技术的认可**,甚至眼光比较浅薄,认为自己当下所学的就足以,可没却从未到真正的”外面”去看过。 + +当有了对未来技术趋势的了解,自身就有相对明确的目标学习,而不是漫无目的学习,跟风学习。 + +## 我是如何了解到这些技术的? + +也许有些人并不在意新技术是否学习,而是好奇我是如何知道这些技术的。这个问题非常好,我自己简单总结通过那些途径来获取到这些相关技术的新闻。 + +主要有以下几种来源: + +- 多加技术群,不定时看群聊 +- **多刷技术大佬文章(推特),或者是技术公众号和掘金(最多的也是最有效的)** +- 订阅一些技术周刊,或订阅某个项目 + +没啥技巧,就靠刷技术文章,自然而然的了解也就越来越多了。 + +尤其是第二点,也是我了解这些新技术的最直接途径。与其自己去主动了解新的技术,将刷抖音的时间改成刷技术文章,了解新技术就是分分钟的事情。可以说我写博客是因为这个契机,记录自己用到的技术的开发过程,并分享个人的开发体验,让更多人了解到这些新技术。 + +## 面对新技术该怎么学? + +其实更多时候是比较在意如何去学一门新技术,而不是找一门新技术,当阅历多了,技术自然就了解的多。这里我分享下我对于新技术是如何起步与学习的。 + +首先我会列举出我近期感兴趣的技术,这一步很关键,我当然不可能每个技术都去尝试一遍,时间精力根本不够。通常在我了解到这个技术的时候,比如文章与视频中,都会介绍到这个的优点与用法,这就足以了。 + +但想要进一步的学习,还是得依靠实战项目(至少我都是通过实战项目来学习的),这时候我会看看手头的项目,看看有没有能够基于上面所列举出的新技术升级的想法,如果有的话,那正好就当重构与新技术的学习,这是最好的,也是相对比较节省时间的。 + +但如果没有的话,我通常是会考虑另写一个项目,而这个项目可能是某个灵感的实现,也可能是久违想写的项目,或者是复刻某个感兴趣的站点,总之从上面所列举出来的技术中去选择一个来进行实践。在项目实践中去尝试使用这些新技术,哪怕只是实现一个简单的 demo,也总比单纯的刷文档,看代码来的有效。 + +**在项目实践中学习,永远是最直接也是最有效的**。回想你编写课设或者工作的项目,是不是在项目开发中进步的最快?如果这时候还有点时间紧迫感,进步反而会更快。(当然焦虑和压力也会随之提升) + +## 最后感悟 + +关于本文,必然有引来一些不同看法与见解,每个人都有对不同事物的理解,我只是将我对新技术的看法,以文章的方式输出出来。本文并未带有任何的技术的偏见,我对任何技术都保持一视同仁,并且愿意去尝试学习。 + +不必抱怨新技术发展的过快,自己来不及学,学不完。或者担心自己学的东西在未来将会淘汰,等同于白学。学习过程就是一个非常好的经验总结,当你回顾整个学习过程,其实都没有白学。反而多一次的学习过程,在未来学习新的东西时,学习的成效也会显著提升。保持不断学习,就永远来的及学习。 + +更多时候,不应该只学如何使用,而是该想想这东西是在什么样的契机下如何被创造出来的,解决了什么问题。而这个问题在未来有没有什么更好的解决方案可替代,如果有更好的解决方案,那么必将又将发展出新的技术来更好的解决这个问题。这在我曾经的学习中,我是从未考虑到的,只专注于学习,而没去了解为什么。 + +我是希望越来越多的新技术出现,无论它是为了解决什么,必然能解决某些人的一些需求,那么它的出现就很有意义。至于未来该技术和相关生态发展如何,不得而知。也没人敢笃定未来这个技术必定会火,就去学这门技术。绝大大多情况下都是比较看好这门技术,认为未来可期,同时又感兴趣,就开始学习并使用。 diff --git "a/blog/lifestyle/\350\256\260 Github \345\255\246\347\224\237\350\256\244\350\257\201.md" "b/blog/lifestyle/\350\256\260 Github \345\255\246\347\224\237\350\256\244\350\257\201.md" new file mode 100644 index 0000000..35eec81 --- /dev/null +++ "b/blog/lifestyle/\350\256\260 Github \345\255\246\347\224\237\350\256\244\350\257\201.md" @@ -0,0 +1,125 @@ +--- +slug: github-student-certification +title: 记 Github 学生认证 +date: 2022-09-06 +authors: kuizuo +tags: [记录, github] +keywords: [记录, github] +description: 记录 Github 学生认证艰辛过程与经验分享。 +image: https://img.kuizuo.cn/202312270150041.png +sticky: 1 +--- + +我个人是非常讨厌这些认证提交手续的,例如疫情健康报告,请假申请表等等,当然也包括这次 Github 学生认证。 + +这也就是我为什么迟迟不认证 Github 学生的原因,其实说白了就是没必要。但就在前段时间 [github copilot](https://github.com/features/copilot/ 'github copilot') 不是内测结束了,然后要开始收费了,收费标准 一个月 $10 / 一年 $100。这费用对于我本不富裕的生活雪上加霜。而 coplot 对教育认证有免费资格使用,于是乎就有了此次较为艰辛的 github 学生认证。 + + + +## 开始认证 + +介绍完故事背景后,就要开始认证了。 + +能看到这篇的估计也是想要学生认证的,这里就将我的认证过程总结出来。 + +### 1、不要科学上网 + +如果开启科学上网的话,提交时 github 会根据 ip 来判断所提交的学校位置和 ip 地址是否相近,如果差的很远的话是直接认证失败,并提示 + +> You appear not to be near any campus location for the school you have selected. If you are a distance learner then your school-provided academic affiliation documentation must state so. + +大致意思:您没有出现在您所选择的学校的任何校园附近。如果你是远程学习者,那么你的学校提供的学术联系文件必须说明这一点。 + +也就是这一点,让我放弃我在老家认证学生认证的想法,而到开学才重新认证 + +但如果不开启科学上网就有可能获取不了 Google 地图与最终提交,我的做法是修改 host,然后需要 Google 地图的时候开启科学上网,然后获取定位信息后再关闭,最后提交的时候没开启科学上网。 + +### 2、学生认证资料 + +#### 教育邮箱 + +有的大学是没有教育邮箱的,就比如我的大学。但不用教育邮箱也是能认证成功的。(当然有的话反而更好通过) + +#### 学生证 + +学生证学生卡这些都可以作为学生 ID 来认证的,不过在拍学生证之前一定要保证照片清晰,看情况决定时间水印,因为有可能会提示如下信息 + +> Your document does not appear to include a date demonstrating current academic affiliation. For countries utilizing non-standard calendars, you may need to capture the original document beside one with a converted date. You may include multiple documents in your image, so long as they are legible. + +大致意思就是提交的资料没有当前时间认证,所以加个时间水印主要是为了这个。 + +但不过我有个同学是新号,5 月 github 注册的时候提示要他学生认证,然后他就随手拍了一下学生证的照片提交上去就认证通过了。据他回忆当时认证的信息填的很随意,然后第一次就通过了。而反倒是我提交了好多次学生证都失败了,怎么说呢,可能看账号吧。 + +#### 学信网在线验证报告 + +假设你拍照提交学生证一直失败(我就是这样),那么还可以通过 [学信网](https://account.chsi.com.cn/passport/login '学信网') 的学信档案 [申请教育部学籍在线验证报告](https://my.chsi.com.cn/archive/bab/xj/show.action '申请教育部学籍在线验证报告') + +这个报告默认是中文的,但是 github 不一定认中文的,所以会拒绝。这时候就需要翻译成英文,但是在学信网申请英文在线报告需要额外 30 元,有效期 1 年。当然如果不想花这些钱,就想着是学生认证白嫖的话,也可以使用网页在线翻译,将内容翻译成英文,就得到了一份英文版的在线验证报告。而这个份报告是能通过的,我就是这样操作的。 + +每次提交的文件都要求不同,因为 github 后台会对文件做认证,所以就需要多拍照,多截图,做到图片相似,但不相同。 + +### 3、修改 github 个人信息 + +如果你按照上面的操作提交了,但还是不通过,并且只有下面一条提示信息的话 + +> You are significantly more likely to be verified if you have completed your [GitHub user profile](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-github-profile/customizing-your-profile/personalizing-your-profile 'GitHub user profile') with your full name and a short bio. + +大致意思是,完善你的 github 个人账号信息(头像,昵称,简介),像我做的就是把昵称改成了我的真实姓名,简介就写我来自什么学校,热爱开源。就差最后大招把头像改成我的自拍照,背景是学校门口。当然 github 还算仁慈,最终还是没让我放出“大招”。 + +然后我修改了个人信息,并又提交了几次后,就终于成功了! + +所以只出现了上面的一条提示,那么说明已经快要成功了,只不过 github 还要考核你的坚持程度,看你会不会放弃(我猜的) + +## 我的认证过程 + +按照以上的步骤,我将演示一遍我的认证过程。 + +1、登录 [github education](https://education.github.com/benefits) ,选择学生那个按钮。 + +![image-20221010134753749](https://img.kuizuo.cn/image-20221010134753749.png) + +2、首次表单填写邮箱,学校,以及使用 Github 的目的。**表单所提交内容全都要使用英文** + +![image-20221010134942952](https://img.kuizuo.cn/image-20221010134942952.png) + +3、再次填写一个表单,首先是照片证明,也就是学生认证资料。这里是使用的是**学信网的在线证明英文翻译**,Proof Type 选择 Other (Example: Screenshot of school portal),备注内容填写证明来源,例如:**这份证明来自中国高等教育学生信息网(学信网),以下是在线证明地址。。。** + +![image-20221010135500357](https://img.kuizuo.cn/image-20221010135500357.png) + +其次第二个表单,根据你的学校信息填写即可。**切记到这一步的时候请不要使用科学上课,最好使用学校的网络来提交。** + +![image-20221010135949606](https://img.kuizuo.cn/image-20221010135949606.png) + +4、点击 Process my application 提交,等待结果即可。 + +最终 Github 在今早发送邮箱告知我认证成功了! + +![](https://img.kuizuo.cn/github_eduction_success.jpg) + +只要你提供的学生信息真实有效,不断提交最终肯定是会成功的。在这认证期间我一共提交了 11 次请求。 + +![](https://img.kuizuo.cn/image_n3x8Cm8kMv.png) + +期间收到的 Gtihub Education 邮箱信息如下: + +![](https://img.kuizuo.cn/github_eduction_eamil.jpg) + +最终也不负众望,在收到 github 通知的时候的,我就立马编写了这篇文章,记录了自己 github 学生认证的过程。 + +如果你有幸看到这篇文章,并想要认证 github 学生资格,希望这篇文章有帮到你。 + +## 认证到期 + +![](https://img.kuizuo.cn/202307210745506.png) + +时隔一年,我的 github 学生认证到期了,然而就当我准备按照上述方式续期的时候。发现我的学校认证方式变得严格了起来, + +![](https://img.kuizuo.cn/202307210748905.png) + +由于我之前的信息(学信网在线认证)已经使用过了,告知我换个文件,我猜测是因为 审核流程 检测到页面相似度过高,就 Reject 了。还有一点在这次提交的时候,明确告诉需要我校的教育邮箱来进行认证,然而我并未申请,并且申请也不是那么容易,所以便暂时放弃。 + +不过目前来看,我使用最多的还是 github copilot,但学生认证过期后并不影响使用,所以我就不急着续期了。但 Jetbrains 的产品估计是跑不了的。 + +## 感谢 + +最终也是要感谢 Github 为广大开发者提供平台,让一群志同道合的人在上面分享并创造想法,同时也感谢这些默默为开源做出贡献的前人,不断为这个世界增添一丝色彩。 diff --git "a/blog/lifestyle/\350\256\260\344\270\200\346\254\241Github\346\217\220\344\272\244PR\350\277\207\347\250\213.md" "b/blog/lifestyle/\350\256\260\344\270\200\346\254\241Github\346\217\220\344\272\244PR\350\277\207\347\250\213.md" new file mode 100644 index 0000000..7727db1 --- /dev/null +++ "b/blog/lifestyle/\350\256\260\344\270\200\346\254\241Github\346\217\220\344\272\244PR\350\277\207\347\250\213.md" @@ -0,0 +1,96 @@ +--- +slug: github-pr-experience +title: 记一次Github提交PR过程 +date: 2022-01-25 +authors: kuizuo +tags: [记录, github] +keywords: [记录, github] +--- + +## 故事起因 + +博客正准备写一个项目展示的功能,其中 Docusaurus 中的[案例展示](https://docusaurus.io/zh-CN/showcase)就很适合改写成项目展示页面,然后无意间刷到我当时搭建博客所参考的博主[峰华](https://zxuqian.cn/)的博客也在展示页面。 + +![image-20220124214558772](https://img.kuizuo.cn/20220124214558.png) + +于是脑海中就想:要不然提交一下我的博客试试看?然后便有了下文的故事 + + + +## 故事过程 + +当时具体提交的[Pull requests](https://github.com/facebook/docusaurus/pull/6458) + +展示页面中有个很明显的按钮 Please add your site,点击后就跳转到 Github 的编辑页面了,不过浏览器不方便操作代码,所以我就 clone 了项目,根据提示,修改了两份代码(一个是添加背景图片,一个是添加博客的 json 数据)提交了 PR(Pull requests)。 + +![image-20220124215841410](https://img.kuizuo.cn/20220124215841.png) + +一开始我是怀着尝试的态度去提交的,所以我不小心将代码格式化(也就是第 10 行 sortBy 两边的空格,原本代码风格是没有的),直到我已经提交上去的时候才发现 😂,甚至提交的时候我连 _description_ 都没写(所以我当时真是怀着尝试的态度去提交的)。虽然这是我第二次提交 PR,但也告诉我以后 commit 提交,一定一定一定要比对前后代码变动的地方,不然就会像上面这样。 + +提交完之后,很快就有机器人给我回复 + +![image-20220124220731250](https://img.kuizuo.cn/20220124220731.png) + +大致的意思:首先很感谢你为社区提交请求,但是呢,为了合并你的代码,我们必须要贡献者签署我们的贡献者许可协议 + +很显然我并没有签署过,于是它就把解决方案也告诉了我,叫我访问https://code.facebook.com/cla,去签署CLA签名(贡献者许可协议),像下面这样,点击Submit就可以提交。 + +![image-20220124221203894](https://img.kuizuo.cn/20220124221203.png) + +当时我看签署完毕后,返回 PR 页面还是提交要签署,所以我打算关掉这个 pr,准备重新提交一个新的 PR。(这种做法是真的愚蠢,尤其是在一个大型的开源项目) + +就正当我关闭 pr 的时候,这时 Reviewers(审核人)给我回复了一条信息 + +![image-20220124221555614](https://img.kuizuo.cn/20220124221555.png) + +> Hey, please don't close your PR if just because of the CLA. The bot will update your status soon after you signed it. + +意思就是:请不要在签署 CLA 签名前关闭 PR,机器人会自动在你签署后自动为你更改状态 + +然后我就灰溜溜的重新开放 PR,那时候感觉我是真小白,太尴尬了 😅。 + +然后等待了差不多有半个小时左右,机器人给了回复 + +![image-20220124222432479](https://img.kuizuo.cn/20220124222432.png) + +> Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! + +意思:感谢您签署我们的贡献者许可协议。我们现在可以接受您的代码为这个(和任何)元开放源码项目。谢谢! + +然后审核人为我的错误 commit 标题进行了修改~~docs: Add Kuizuo's Personal Website to showcase page~~ docs: add Kuizuo's Personal Website to showcase,**第一个单词 Add 不应该首字母大写**,不符合规范。 + +然后为我提交的代码做了一些小调整 minor tweaks,也就是上面所提到的 sortBy 空格,然后为我提供的展示图裁剪成标准尺寸。 + +![image-20220124222739483](https://img.kuizuo.cn/20220124222739.png) + +审核人批准了我这两项修改,然后等待系统审核,具体审核的图我当时没截,现在没显示了,把已提交后的代码重新部署到 preview(预览)下,整个过程大约 5 分钟这样,接着审核人对我回复了一句 Great site, thanks! (很好的网站,谢谢),然后这个 PR 状态就变成了 merged(合并)状态。 + +然后我犹豫了几分钟,不知道该怎么回复了,加上我英文表达不行,所以我原本中文是 + +谢谢,希望 Docusaurus 做的更好,一起努力 用软件翻译后 Thank you, Hope Docusaurus can do better. Let's go + +![image-20220124222926032](https://img.kuizuo.cn/20220124222926.png) + +虽然才过去两个小时,但是我现在回想起来都感觉贼丢人。 + +首先,我这个回复不是指定为他回复,而是相当于全体评论,贼不礼貌,然后这个蹩脚的英文翻译,我真像把 Let‘s go 改成 Let's work together,就算改了,感觉这个回复也太不礼貌了,这就已经不是英文表达能力,而是中文的表达能力了。 + +总之最后的结果是好的,我提交的 PR 已经成功合并到了 main 分支上,并且在下一个发布的版本中,案例展示中将会有我的博客显示在上面,现在访问[preview 网站](https://deploy-preview-6458--docusaurus-2.netlify.app/showcase/?name=kuizuo),搜索 kuizuo 也能看到(B 格瞬间就上来了) + +![image-20220124223506489](https://img.kuizuo.cn/20220124223506.png) + +## 事后思考 + +整个过程下来,审核员给我的印象太好了,我这小白式的 PR,现在回看下来都感觉太丢人了。然后我一看审核员的[Github 账号](https://github.com/Josh-Cena),好家伙,竟然是一名在中国上海的高中生!还是团队的核心人员!太牛了! + +![image-20220124225625869](https://img.kuizuo.cn/20220124225625.png) + +![image-20220124225830338](https://img.kuizuo.cn/20220124225830.png) + +![image-20220124231207662](https://img.kuizuo.cn/20220124231207.png) + +很难想象的到一位高中生竟能为默默的为开源项目做出贡献,而我的这次 PR 能提交成功,也与这位热心的国内学生有很大关系。(再次对我一开始报着尝试提交 PR 的态度表示抱歉) + +但又回到我这边,这次提交 PR 的经过也让我学到了很多,commit 时一定要仔细对比更改前后的代码,提交的 commit 标题的规范,不必要的 closed,以及最重要的开源精神,让我看到一个实实在在开源者的样子,也是我梦寐以求的样子。 + +最后也祝 Docusaurus 能越做越好,也感谢这些默默为开源做出贡献的人们,正因为有你们世界才会变得更好。 diff --git "a/blog/lifestyle/\350\256\260\344\270\200\346\254\241\345\211\215\347\253\257\351\235\242\350\257\225\350\277\207\347\250\213.md" "b/blog/lifestyle/\350\256\260\344\270\200\346\254\241\345\211\215\347\253\257\351\235\242\350\257\225\350\277\207\347\250\213.md" new file mode 100644 index 0000000..860b4bc --- /dev/null +++ "b/blog/lifestyle/\350\256\260\344\270\200\346\254\241\345\211\215\347\253\257\351\235\242\350\257\225\350\277\207\347\250\213.md" @@ -0,0 +1,74 @@ +--- +slug: frontend-interview-experience +title: 记一次前端面试过程 +date: 2022-06-25 +authors: kuizuo +tags: [记录, 面试] +keywords: [记录, 面试] +description: 记录一次前端面试过程 +draft: true +--- + +考试终于结束了,也将近一个月没怎么写代码和文章了,准备调整下自身状态,开始进一步的学习。然后在前段时间我的一个同校同学拿到了 xx 公司的 offer,然后该公司正好有招前端开发实习,就问我有没有兴趣尝试一下,但一开始我内心其实不是很想工作的,想着暑假去闭关学其他技术,同时加上我目前的身份**准大三(休学一年)**实习期可能也就 2 个月,毕竟这行业不同于打工,是需要长期工作和维护。不过一想毕竟没面试过,所以就蛮去尝试一下,至于最终的结果,阅读全文吧。 + + + +## 简历 + +我的那位同学给我推了面试官的微信,于是加完微信,简单的几句招呼便直接开始找我要了一份**简历**,也就是整个面试过程中最重要的物件,即**面试的前提**。 + +很显然我压根就没有准备过这玩意,然后面试官叫我去准备一下(期间聊天强调过几次有空准备下简历),于是就去网上搜寻了关于简历说明、编写建议与简历模板,也了解到简历对面试者的一个重要性。 + +**简历是面试官了解面试者最快方式**,甚至可以说对于大部分人群是非要不可。于是我花费了一天的时间去准备了一份属于自己的简历,主要内容为个人的基本信息、技术特长、个人描述、教育背景、工作经验、项目经验等,其内容一定要真实,同时也要在精简的同时,让面试官一眼就能看到你的亮点,以及将来你到贵公司工作能为其提供的帮助,这些在外面的大部分简历编写建议中也有提及,这里我也仅是简单总结。 + +关于我的简历就不展示了(毕竟暂时比较烂),不过关于简历模板的话,我倒是可以推荐下。我所使用的是 [木及简历](https://www.mujicv.com/),此外还有 [Markdown 简历排版工具 (mdnice.com)](https://resume.mdnice.com/) 与 [简历自动生成 (sugarat.top)](https://resume.sugarat.top/) ,这些都是我在搜寻资料中认为不错的在线简历模板。 + +最主要是丰富自身的阅历与技术,这样在简历编写时,其内容是一定能吸引到面试官的注意。要是什么技能都不会,什么项目都没有,一份不起眼的简历,作为面试官是很难给你一个面试的机会。 + +如果你已经看到这里了,并且你未来有找工作的需求,那么现在就可以着笔简历,而不是等到要找工作的时候在准备。同时在日常开发与学习中,也可以不断去完善自身简历。**总之,越早写简历越好**。 + +将简历提交给面试官后,如果叫你准备下面试,那么恭喜,已经进入第二步------**面试** + +## 面试 + +这次预定为周六早上 10 点(即本文发布日期)**线上面试**(无屏幕共享,只单纯聊聊技术栈相关),正常来说知道自己要被面的话,应该来说会提前做些准备。然而我就不一样了,我**没刷过任何面试题**(八股文),所以我想看看我的知识储备能否回答出这些问题,结果也确实如此,这次面试大多数问题回答不出来或者回答的半知半解的。 + +### 问技术 + +关于所问的问题,和外面主流的前端面试大致,这些就是考验基本功的问题,例如 HTML5 的新特性,JS 的一些语法等等,这些在八股文在面试过程中是必须要背的,因为这些决定计算机的基本功。此外面试官在了解你的大致技术栈后,必然会针对你简历上的内容,提出一些针对性的问题。比方说我在简历上写了一个后台管理系统 [KzAdmin](https://admin.kuizuo.cn) 其中就有问到像 JWT,axios 封装,前端菜单展示等问题,然后和一个[JavaScript 的混淆与还原](https://deobfuscator.kuizuo.cn/),就问这个主要功能,以及其中所涉及的一些技术栈相关的。 + +至于整个面试所关于技术的问题与细节就不一一列举了,不过令我意外的是,所问题的竟然没有算法题,不过事后想想也对线上面试还无共享屏幕的前提下,咋可能问算法题呢,最多也就是问问你对你的技术栈的了解程度与理解情况,整个过程与我预期所想符合。 + +### 闲谈 + +最后就是表明他们的项目可能是需要长期的,然后我目前的情况又相对比较特殊,问我下学期课程的安排啥的。相对面试者比较在意的就是薪资,但在这次谈论期间是没有关于薪资的,我和面试官都没提,我自然是不敢提的,毕竟问题都回答的那么烂了,哪里还好意思提,同时这是实习岗位,自然薪资不会高到哪里去的。 + +当然了,整个面试过程最后一句话是**回去等通知** + +### 等待面试结果 + +通常面试都有以下三种结果。 + +第一、现场录用。第二、当场拒用。第三、回去等通知 + +大多数面试结果都是第三条,我自然不例外。至于有没有二面啥的,自我感觉有个 7 成左右。为何不敢说百分百呢?从整个面试的过程来看,其实我准备时间是充分的,但根本没怎么准备,甚至可以说全程是种摆烂的态度。再从回答问题上来看,我整个回答情况就表明告诉面试官我没准备好这次面试,加上一开始没准备好简历,就相当于我就是来面着玩的 😂。 + +公司面对这类面试者(求职意向不是很明确,情况特殊),大概率就是直接刷掉了。当然,这只是我的个人对此次面试的看法,如果有二面的话,我一定好好刷面试题,顺带锻炼下表达能力(现在在回想整个回答过程,我甚至都不好意思说我是学前端的了)。如果没有的话,算了不去想了。 + +## 面试结果 + +承接上文,直到考试结束任然没收到任何通知,即挂了。其原因其实我也在上文中说到,我的面试态度相对比较差,加上实习期限的原因。不过后话来说,即便我收到了这份 offer 我也不一定会去,当然这里说这种话确实有那么一点不好,但首先我是工作过(虽然不算一个体系的公司,只能算作工作室),而且给我分配的时间充裕,而且薪资还不低,并且当时涉及的业务还是我相对比较强项的协议复现和逆向(虽然我已经快半年没碰,而且大概率会在持续半年的时间不接触逆向相关的)。但每天的任务就是等待产品经理的需求,这时就需要停下手头的事情,来完成任务,大部分的时间都是在测试和维护。虽说和与搬砖打工有点区别,但本质无异,都是枯燥任务。同时我本身是挺反感重复任务与中断的,所以上一份的工作给我的感受就是(时间)充实但又夹杂着(任务)枯燥,但很多时候就不得不妥协,因为生活所迫,这里就不做感慨了。 + +总之,这次的面试结果也并不让我意外,甚至可以说是情理之中。 + +## 总结与建议 + +对整个面试的过程对我而言就是要锻炼自身表达能力,在除了没刷八股文外,像一些问题,我很难用口语去告诉面试官我的意思,但使用屏幕共享加代码展示就不一样,但当时的面试官没提加上我也没主动申请,这是我认为整个过程比较可惜的一点。不过整个过程下来,我认为面试也没什么的,无非就是问问题,平常多刷面试题。 + +关于一些建议的话: + +首先就是个人简历非常重要,最好添写个人[Github](https://github.com/)与[博客](https://kuizuo.cn),绝对是面试的加分项。尤其是博客,在上面所分享的内容,也可能会吸引到一些需要招聘你的 HR。至少告诉面试官你有在学习,并且懂得记录。其次就是简历上一定要有能亮眼的地方同时自己又很掌握的项目经验,这样面试官针对性的问题,对自身也有很好的作答。如果自己掌握程度不是很大的情况下,还是不建议编写,即便学过,但忘了,那就是不会(说的不就是我吗)。 + +一个好的简历谁都会写,但面试不是一纸千金,工作更不是纸上谈兵。简历是了解一个人的基本资料,而面试主要是考察一个人的工作能力与综合素质,即便你工作能力再强大,但你工作态度属于摸鱼的状态,同样是无法胜任此工作的。所以既然选择要工作,那就应该做足准备,拿出最好的工作态度给面试官。 + +所以我一开始的目的到底是什么呢?准备实习工作呢?还是闭关学习呢?又或者是... diff --git "a/blog/lifestyle/\350\260\210\346\230\223\350\257\255\350\250\200.md" "b/blog/lifestyle/\350\260\210\346\230\223\350\257\255\350\250\200.md" new file mode 100644 index 0000000..0c6f54c --- /dev/null +++ "b/blog/lifestyle/\350\260\210\346\230\223\350\257\255\350\250\200.md" @@ -0,0 +1,178 @@ +--- +slug: easy-language +title: 谈易语言 +date: 2020-10-08 +authors: kuizuo +tags: [杂谈, 易语言] +keywords: [杂谈, 易语言] +description: 谈谈易语言其优缺点以及我对易语言的看法 +--- + +好歹自己学习易语言也快有一年了,也用易语言写了一些软件,特此记录一下以及对易语言的个人看法。 + +该文章有可能过于啰嗦,可吐槽的点太多了,也正有感悟才能写的这么多。 + +:::note 补 + +2021 年 3 月是我最后一次打开易语言,至此我已经很久不写 exe 项目了。 + +::: + + + +## 易语言介绍 + +还是简单介绍一些易语言吧,毕竟肯定有很多即使学过编程也没听过易语言的,易语言是一门以**中文**作为程序代码编程语言,简称 E 语言(EPL),创始人[吴涛](https://www.baike.com/wikiid/3464184696167845391?view_id=2nxgpp3bv6k000),2000 年一个人独立开发易语言。 + +有关易语言的特点如下: + +### 易语言不开源 + +不像主流的编程语言 C,Java,Python 等是开源的,易语言是一款纯正的商业编程软件,易语言正版加密狗 618 元,不过有破解版,不然多数人都不会去接触易语言了。但不开源就已经注定了易语言的在整个生态就不行,并且易语言已不在维护了,也就是很久很久没更新过,或者说不会再更新了,作者也已不再管易语言了,目前也就一些易友去开发一些相关的插件模块库这些。 + +### 全中文界面,可视化 UI,填表式的声明 + +我这里放几张图展示一下 + +![image-20200914010759872](https://img.kuizuo.cn/image-20200914010759872.png) + +![image-20200914011112087](https://img.kuizuo.cn/image-20200914011112087.png) + +首先要吐槽一下,2000 年的页面与 2020 年的页面可以说是完全一模一样的。开发界面是真的丑,但有一点是,页面的语言命令都是全中文的,比如`if`所对应的的就是`如果`,`MessageBox`所对的就是`信息框`,很多命令都中文化就再举例了,并且每个函数都是以表格似的填写,也就是代码的格式都定死死的了,如果你学过其他的编程语言在来和易语言比对,你多半会学的够呛,很难理解为啥要这样。不过也正是填表式声明,导致易语言过于简单,后文也会提及。 + +在比对一些 C#的开发界面 + +![image-20200924125428023](https://img.kuizuo.cn/20200924135324.png) + +![image-20200924125907216](https://img.kuizuo.cn/20200924135325.png) + +可以看到页面肯定比易语言好看 100 倍,但是随之而来的就是难度的提升,先不说好写与不好写,你让一个没学过编程的看,多半看的云里雾里,这时候就会劝退很多人瞬间不想学了,相信很多学编程的都有这样的经历。 + +#### 上手容易,可以做到极速开发 + +接着再来说一下上手学习,正是由于有上面那个前提,易语言可以做到上手特别快,可以说会用电脑,有逻辑,会识中文,易语言好学的一批,基本上学个几天自行写个软件完全没问题。对于国人一点编程基础都没有的新手,并且英语还不好的话来说,易语言可能是真的好上手,我当初学易就有一部分就是给英文劝退了。 + +首先我要提的是可视化界面设计,你只需要将旁边的组件拖拽至窗口页面上即可,相对于的属性,例如内容,宽高,颜色在旁边显而易见,要修改只需要点击修改对应的数值即可,而对于其他的 IDE 来说,如果英文不咋好,并且还是第一次用,找可能都要找几分钟。而正是这个可视化界面,让我当初有信心学下去易语言,如果你学过 C 或者其他编程语言,一开始都是在那黑不溜秋的控制台显示,我就只是想写个软件用用,你给我讲那么多理论知识,甚至我还听不懂的那种有个嘚用。 + +同时还可以直接打包成 exe 文件,直接在 windows 上运行,发给别人也能运行,哇,瞬间感觉到写软件的牛逼之处了,直接小有成就一波。这里我放几张我当初学易语言写的一些界面吧: + +例如写一个骗骗小学生的 2020 年最新刷 Q 币软件(用到了浏览器的填表功能改了 q 币的值) + +![demo](https://img.kuizuo.cn/20200924135326.gif) + +在比如做一个音乐播放器(是有声音的,只是我录制的是 gif) + +![demo1](https://img.kuizuo.cn/20200927031909.gif) + +在比如一些自动添加好友的 + +![image-20200924191526221](https://img.kuizuo.cn/20200927031910.png) + +在比如写一个注册机模板 + +![image-20200924192403210](https://img.kuizuo.cn/20200927031911.png) + +网络验证 + +![image-20210819233054879](https://img.kuizuo.cn/image-20210819233054879.png) + +![image-20210819232928124](https://img.kuizuo.cn/image-20210819232928124.png) + +还有特别特别多的例子我就不举例了,这些用其他的编程语言肯定能写,但是与之对应的就是学习成本,很多人学其他编程语言,甚至还没学到界面设计就开始放弃了,原因很简单,没兴趣学呗,易语言界面好设计,但是基本都是原生 windows 组件,对于新手来说这完全足够设计出自己的软件了。 + +**要是没能在最想学习的时候,满足自我的成就感,那很有可能就会学不下去**。 + +我当初学易语言也是这样的,暑假学了两个月,其中第一个月学基础到还没什么,也就开始学习易语言的基本语法和编写一些程序来玩玩,但这些说实话没什么可看的,或者说没什么可用的,就想上面那个骗骗小学生的刷 q 币软件有用吗,没用呗。初学的一个月就都开始写这些可以说毫无软用的东西,直到我照着视频一个字一个字的模仿着敲一遍扫雷一键秒杀的代码,没错,就是这个激起了我对编程,让我感受到编程的魅力。放上一张 gif 图片。 + +![demo2](https://img.kuizuo.cn/20200927031912.gif) + +当初照着视频一步一步来最终完成了该软件,但那时候的我其实根本不知道为什么可以这样,直到后续了解到汇编与游戏内存相关的知识,我才算真正懂的当初扫雷外挂的原理。 + +也正是因这个扫雷的外挂,让我接下来的几个天疯狂的学习,去写其他的游戏外挂,比如连连看的一键秒杀,消消乐,植物大战僵尸等等。这里我也放一张图吧(还特意去下载 qq 游戏) + +![demo3](https://img.kuizuo.cn/20200927031913.gif) + +不过后面就没怎么学习游戏外挂相关的,一是所看的教程是 11 年的,中途没更新了,二是目前热门游戏以我目前能力写不出来,只要加上了检测,就过不了,并且容易封号(注:我 QQ 可不是开外挂给搞封了,就算开外挂最多也只是封游戏账号),最近接触的也就是 CF 越南服的外挂,有教程于是就学了点皮毛,不过教程又教到一半,就没深入去学习。这里提醒一句,写游戏外挂并销售是可是会给抓的。 + +不过有点扯远了,就凭这一手界面设计,易语言其实就足以容易上手写出个软件出来。在叙述几点易语言容易上手的地方,自带提示,全中文文档,比如下图 + +![image-20200924220413124](https://img.kuizuo.cn/20200927031914.png) + +只要你鼠标选到对应的函数上,按下 F1 或者点击提示,就有对应的函数提示,对应其他语言也有,但是纯英文的,门槛就高一个档次。 + +#### 精易模块 + +如果没有这个模块也就易语言跟其他语言的区别可能就是一个是中文一个是英文了。我就举我用的最多的一个命令`文本_取出中间文本` + +![image-20200924220908502](https://img.kuizuo.cn/20200927031915.png) + +而对于其他的编程语言,这类语言还需要自行编写一个函数来调用,而精易模块则是直接封装好好的供你使用。你都没必要去了解底层的函数,直接把门槛降了一个大档次。 + +至于相关的程序编写我也不多概述,下面就是易语言的缺点。 + +### 易语言的缺点 + +我说说我用易语言的缺点,也是我最不推荐别人学易语言的了 + +我上面也说到过 2000 年的页面与 2020 年的页面可以说是完全一模一样的。虽然有易友开发了仿 VS 界面的,但启动起来影响运行速度,我就没安装了。虽说我不是强迫症,但用多了 vscode 与其他的 IDE 相比,看到易语言就能想到是几年前的软件了。 + +#### 占用空间与运行 + +易语言毕竟还是一种封装过的语言,带来的方便,同时也牺牲了性能空间,与原生的桌面级开发相比易语言是无法比的。就比如用 C#开发的所占用空间肯定比易语言少,相关的性能优化更好,这里我就不放图了。 + +#### 软件报毒,即使没毒也会给杀毒软件报 + +这里我有必要说说关于易语言的一段故事,这里我放几个链接,可以去了解一番 [刷枪改图强登游戏 CF 外挂](https://www.bilibili.com/video/BV1WE411E7mQ/?spm_id_from=333.788.videocard.0) + +[为什么多数外挂都用易语言?](https://www.zhihu.com/question/20690643?sort=created) + +如果你在 2010 年左右接触过网络游戏,你肯定遇到过各种各样的外挂,而这类外挂多数都是出自易语言之手,甚至你现在在外面遇到的很多游戏脚本外挂,易语言也能占据多数。你随便百度一些易语言,相关的评论都是有关外挂这些。但事实上你只会易语言是根本不够写外挂的,我学过相关外挂制作,虽然学的浅,但至少学过。是需要汇编这类基础,但又为什么会很多外挂是用易语言写,,而且都是些水平不是特别高的人,原因很简单,因为那些写挂的很多都不会真正写挂,只是调用别人封装好了的库,甚至就连易语言自身都带了外挂库这些。让他们写一款新游戏的外挂,他们多半是写不出来了,原因就是他们不懂汇编这些,但是调用写好的库就 6 的飞起。当然这其中还是有些利益相关的方面,我也不多提了。 + +如我上面所的我一个初中同学,要不是我接触了编写外挂这些,我还真信了他当初能写的,实际上都是修改外面的源码,或者是直接调用写好的库,直接偷源码用。 + +因为外挂行业的崛起,导致一些厂商不得不进行一定的处理。总之,目前易语言写过的项目,多数是会报毒的,即便没毒,也已经给杀毒软件的厂商给拉入黑名单了,所以可以说没公司要易语言的程序员,即使软件没毒,但是还是报毒,你是信杀毒软件还是易语言? + +#### 说说我用到的一些坑 + +我在做一些网页数据获取的时候,竟然连个 DOM 对象都没有提供,当时没接触前端,不知道有 DOM 对象,还是用正则去匹配,那时候是真的 nc。接触了前端,发现易语言竟然没提供 DOM 对象操作,我还是用别人封装的 DOM 类,并且还有可能出现匹配不到情况。 + +其次是调试的时候,对于变量值长度过长竟然无法直接查看,还需要保存为文本才能查看,并且我调试的时候常常崩溃,导致我每次找一个 bug 的时候都需要重启易语言好几遍才行。 + +由于是类似表格式的填写变量,参数与类型,也就导致了无法在其他编辑器上进行编写易语言代码比如我复制一个函数,给我的结果是 + +``` +.版本 2 + +.子程序 子程序1, 整数型 +.参数 参数1, 文本型 +.局部变量 变量1, 整数型 + +变量1 = 到整数 (参数1) +返回 (变量1) +``` + +而在易语言所对的是 + +![image-20200924222319462](https://img.kuizuo.cn/20200927031916.png) + +在易语言中的引号`""`,只能通过常量`#引号`,或者通过常量表,就比如下面这个 jsoin 字符串 `{"a"="123","b"="321"}`,而易语言的写法就是,`"{”+#引号+“a”+#引号+“=”+#引号+“123”+#引号+“,”+#引号+“b”+#引号+“=”+#引号+“321”+#引号+“}"`,一个个通过字符串来拼接,巨麻烦,也是我最想吐槽易语言的,不过也可以通过常量表来替换,但依旧很麻烦。 + +还有易语言自身是不支持 utf-8 编码显示,原因很简单,当初只是为了给国人用,gbk 显然是更好的选择。但有时需要 utf-8 的,这时候就莫得办法。 + +#### 没公司要易语言程序员 + +几乎没有公司招聘易语言程序员,实际上上面所说的就足以证明易语言不行了。并且很多人都不看好易语言,黑易语言,至于为什么黑,百度或者知乎想必会有更好的答案,这里我也就不再赘述了。 + +### 小总结 + +写到这,我其实有点想把介绍易语言的一部分给删了, 我不推荐新手去学易语言,因为易语言相比于其他语言,它还是太弱了。但如果没有这半年的易语言学习,让我天真的以为编程的简单,又怎么鼓舞真正入坑,让我去学习更多更深奥的知识。 + +不过就目前而已,我已经很少用易语言写东西了,但如果要写桌面级软件,我还是会首选易语言(因为只会易语言),毕竟写了也有半年了,开发效率也高。如果有机会的话,会深入学一下 C#还有 Qt,不过也不知道是什么时候才会有机会。 + +关于英语方面的话,我是挺惧怕英语的,我高中英语就没怎么及格过,甚至我大学英语还挂科了,但易语言给我带来了编程希望。就我目前学习来说,编程还真的不怎么吃英语,看不懂英语文档,翻译成中文文档不就完事,而且学多了就会发现太多都是死代码,需要的时候翻阅文档直接 Ctrl C V 使用即可。英语对编程来说只是为辅,英语好并不能提高编程的上限,同时也决定不了下限,就这么说吧,你让一个学英语专业的人来看一份几百行的代码,基本注释写得在详细,他没学过编程,能看的懂吗?但有很多开源的项目都是英文的,会英文固然是好,但不会就不行了吗,看不懂英文文档,我翻译还不行吗?说这些,就是希望别用自己的短处来阻劝自己的目标,很多时候都是学了才知道这个有没有用,没学有锤子用! + +能接触易语言的,多半是没什么英文基础、编程基础。不能说易语言适合新手的入门语言,但绝对是能让中文普通用户眼前一亮,产生编程好感的一门语言。 + +易语言是我接触过的第一款编程语言。那时候曾是我最喜欢的编程语言,也是最能让我感到成就感的编程语言,不过在这行学多了,还是不得不放弃易语言开发,原因就是因为易语言不够强大,但也莫得办法,如今易语言的生态就是如此。 + +**如果易语言不是我的第一门编程语言,那么其他编程语言就是最后一门。** diff --git "a/blog/lifestyle/\350\267\235\347\246\273\346\210\221\344\270\212\347\257\207\347\254\224\350\256\260\350\277\230\346\230\257\345\234\250.md" "b/blog/lifestyle/\350\267\235\347\246\273\346\210\221\344\270\212\347\257\207\347\254\224\350\256\260\350\277\230\346\230\257\345\234\250.md" new file mode 100644 index 0000000..564a3ad --- /dev/null +++ "b/blog/lifestyle/\350\267\235\347\246\273\346\210\221\344\270\212\347\257\207\347\254\224\350\256\260\350\277\230\346\230\257\345\234\250.md" @@ -0,0 +1,78 @@ +--- +slug: why-i-dont-write-notes +title: 距离我上篇笔记还是在? +date: 2023-03-13 +authors: kuizuo +tags: [杂谈] +keywords: [杂谈] +--- + +当我起这个标题时,其实我已经很久没更新(翻看)过笔记了,甚至我都不记得我的博客还有笔记这个东西。 + +当我翻阅 git 记录,寻找上一次在笔记文件夹的 commit 提交记录,还是在去年的 10 月 1 号。 + +![image_U0EDw0PkAf1](https://img.kuizuo.cn/image_U0EDw0PkAf1.png) + +然而并不是我的技术栈没更新,而是我实实在在没去为这些技术栈编写过笔记。仅有的只是博文来记录自己所用的过程。 + +因此我想思考下我为何不记录笔记。 + + + +## 笔记的意义 + +首先不妨回答一下我所认为的笔记意义。 + +### 检索高效 + +当我需要回忆我曾经学习过的某个知识点时,笔记可以说是最直接有效的办法。与其再次使用搜索引擎搜寻答案,不妨直接从答案中找答案。 + +> 正如我笔记简介所述:**做到即查即用,能复制粘贴解决的,就绝不百度。** + +### 巩固理解 + +相比绝大多数笔记内容都是初学者去记录自己所学,在这个阶段,你对知识的掌握程度是比较浅显的,而笔记无非能加快你的理解,同时也是是成本最低的,翻看自己的内容远比理解他人的内容来的简单。 + +## 学习方式 + +很多初学者除了通过视频教程来学习,当然不乏有些人是通过别人的学习经验(即笔记)来进行学习,包括我一开始也是通过刷别人的笔记来学习某个知识。 + +不过相对于视频而言,视频通常很啰嗦且时长感人(动辄可能数天的时长)。很多人不喜欢铺垫,不喜欢废话,就想知道某个知识点怎么用,结果如何。同时视频没有搜索功能,如果视频创作者没有对视频进行分集,当你需要回看的时候时,你往往需要从数十分钟的视频中不断的拖动,甚至到最后才发现原来我看的不是这个视频。 + +而文档则不会,那你可以通过 Ctrl + F 搜索你想要的关键字,相比视频而言,检索的效率将会大大提升。 + +这也是我为何从视频学习转成文档学习很重要的原因。我也很推荐如今的学习者从文档学习,而不是视频学习。 + +## 为何不写笔记 + +### 笔记应该记录哪些知识点? + +笔记无疑是耗时的,你可能会花费数个小时的时间,去总结一个可能你职业生涯中都用不到几次的知识。 + +为什么这么说,因为我发现我有很多笔记就是这种情况,你也可以回想一下你所记录的笔记,有多少是经常使用到的。 + +尤其是当你使用的足够多的情况下,你甚至都无需翻阅笔记。脑海中自然就会复现出你需要的东西,此时为何还需要看笔记。为了验证脑海中的正确性吗? + +想想看你会为怎样的知识点大费周章的记录,是一个自己都不怎么用的知识点,还是用到滚瓜烂熟的知识点? + +当我们经常使用某些知识点时,自然而然就会记住它们,这时笔记就没有太大的意义了;而对于使用频率较低的知识点,记录下来的意义也不是很大。 + +### 搜索大于记录 + +究其原因,还是因为我翻看笔记的频率变少了许多。如今的搜索引擎很智能,只要你用的不是百度,就能很快速的搜寻到答案,为何还需要记录一下,换成自己熟悉的口吻,到最后就变成上面所说的那样。而在 ChatGPT ,new bing 的诞生下,更加剧了我这一行为。 + +**当搜索大于记录时,笔记就显得弱不禁风。** + +### 官方文档与学习笔记 + +说实话,当你看到别人的笔记的时候,你会认真的从头到尾看一遍吗?我想不会,更多情况我们只利用搜索功能去获取我们想要的知识点。 + +但如果此时有两份文档,一份是官方的技术文档,一份是他人的学习笔记文档。你会选择哪一份?是我肯定毫不犹豫的选择前者,他人的学习笔记并不会及时更新,但官方文档只要还在维护,那么必定处于常更新的状态。假设我照着他人的学习笔记学习,此时正好有个函数的使用方法更新了,那么我必定会踩坑,导致不必要的 bug。而官方文档则不会,官方一旦有破坏性更新,通常会有显眼的提示,和 changelog 可供我参考,这就能及时有效的帮我排除不必要的坑。 + +这也是我为什么会宁愿去看官方文档,哪怕是英文的,也绝不愿去看第三方中文翻译后的文档,我有太多的坑就是因为更新不及时,存在信息差导致的。所以无论如何,能获取一手的信息,就别用二手的,甚至你也不知道人家会加工成什么样子。 + +## 最后 + +综上,我想我已经把我为何不写笔记的原因讲述的比较清楚了。 + +当然我并不是说笔记就一无是处,因为每个人,每个阶段的学习方式不同。曾经我也是笔记学习的追崇者,但如今的我能够不借助视频教程,不借助笔记来进行独立学习,所以为何不选择一个对我而言更高效的方式。 diff --git "a/blog/program/Chrome\346\217\222\344\273\266\345\274\200\345\217\221.md" "b/blog/program/Chrome\346\217\222\344\273\266\345\274\200\345\217\221.md" new file mode 100644 index 0000000..9eff5c4 --- /dev/null +++ "b/blog/program/Chrome\346\217\222\344\273\266\345\274\200\345\217\221.md" @@ -0,0 +1,523 @@ +--- +slug: chrome-plugin-development +title: Chrome插件开发 +date: 2020-09-28 +authors: kuizuo +tags: [chrome, plugin, develop] +keywords: [chrome, plugin, develop] +--- + + + +:::warning Chrome v3 已发布,而本文基于 v2 编写,故本文内容不再具有时效性。 + +::: + +## 前言 + +相关文章 [谷歌官方文档](https://developer.chrome.com/extensions/manifest) (需翻墙) + +[Chrome 插件开发全攻略](http://blog.haoji.me/chrome-plugin-develop.html) (强烈推荐看这一篇!) + +你只需要看完上面那篇文章和掌握一些前端开发基础,就足以自行编写一个 Chrome 插件。本文也是基于上面文章加上自己之前写的插件所记。 + +### 什么是 Chrome 插件 + +如果你用过 Chrome 浏览器的话,也许会用到过一些插件,其中比较知名的就是油猴插件,通过这些插件能够帮你例如自动完成一些功能,屏蔽广告,相当于一个浏览器内置的脚本。应该来说这是 Chrome 扩展开发,不过说 Chrome 插件更顺口,后文也会说成 Chrome 插件。 + +### 安装 Chrome 插件 + +首先打开 Chrome,如下图即可进入插件的管理页面 + +![image-20200922225606159](https://img.kuizuo.cn/image-20200922225606159.png) + +这时候记得把右上角的开发者模式给勾上,如果不勾上的话你无法直接将文件夹拖入 Chrome 进行安装,就只能安装`.crx`格式的文件。Chrome 要求插件必须从它的 Chrome 应用商店(需要翻墙)安装,其它任何网站下载的都无法直接安装,所以可以把`crx`文件解压,然后通过开发者模式直接加载。 + +然后将写好的 Chrome 插件文件夹拖入到刚刚打开的插件管理页面即可。 + +## Chrome 插件知识 + +### manifest.json + +是`manifest.json`切记不要英文单词打错字,一定要有这个文件,且需要放在根目录上,否则就会出现未能成功加载扩展程序的错误。 + +### background.html 和 background.js + +可以理解为后台,同时这个页面会一直常驻在浏览器中,而主要 background 权限非常高,几乎可以调用所有的 Chrome 扩展 API(除了 devtools),基本很多操作都是放在 background 执行,返回给 content,而且它可以**无限制跨域**,也就是可以跨域访问任何网站而无需要求对方设置`CORS`。这对我们后面要在 content 中发送跨域请求至关重要! + +我习惯的做法是通过`”page”:"background.html"`来导入`background.js`或其他 js 代码,如下 + +```json +// manifest.json + "background": { + "page": "background.html", + }, +``` + +```html + + + + + 背景页 + + + + + + + + +``` + +如果是 scripts 方式导入 js 文件则需要反复修改`manifest.json`文件。 + +#### 关于乱码 + +有时候你在编写代码中出现了中文可能会出现了如下的乱码, + +![image-20200923214834081](https://img.kuizuo.cn/image-20200923214834081.png) + +我遇到的原因是就是我原先的`background.html`代码写成如下的情况 + +```html + + +``` + +没错,就只写了这两个行,就出现乱码(将 UTF-8 的编码变为了 windows1252),而只需要把 background.html 代码修改成正常的 HTML 结构,也就是上上面的那个代码即可解决该乱码情况。 + +### content.js + +我们主要的向页面注入脚本就依靠这个文件,相当于给页面添加了一个 js 文件,但是`content`和原始页面**共享 DOM**,但是不共享 JS,如要**访问页面 JS(例如某个 JS 变量)**,只能通过`injected js`来实现(后文会提到)。并且`content`不能访问绝大部分`chrome.xxx.api`,除了下面这 4 种: + +- chrome.extension(getURL , inIncognitoContext , lastError , onRequest , sendRequest) +- chrome.i18n +- chrome.runtime(connect , getManifest , getURL , id , onConnect , onMessage , sendMessage) +- chrome.storage + +这些 API 绝大部分时候都够用了,非要调用其它 API 的话,你还可以通过通信来实现让 background 来帮你调用。 + +### inject.js + +上文也说到了`content`是**无法访问页面中的 JS**,可以操作 DOM,但是 DOM 却不能调用它,也就是无法在 DOM 中通过绑定事件的方式调用`content`中的代码(包括直接写`onclick`和`addEventListener`2 种方式都不行),但是,**在页面上添加一个按钮并调用插件的扩展 API**是一个很常见的需求,那该怎么办呢?这时候就需要注入 inject.js 这个文件 + +```js +document.addEventListener('DOMContentLoaded', function () { + injectCustomJs() +}) + +// 向页面注入JS +function injectCustomJs(jsPath) { + jsPath = jsPath || 'js/inject.js' + var temp = document.createElement('script') + temp.setAttribute('type', 'text/javascript') + // 获得的地址类似:chrome-extension://ihcokhadfjfchaeagdoclpnjdiokfakg/js/inject.js + temp.src = chrome.extension.getURL(jsPath) + temp.onload = function () { + // 放在页面不好看,执行完后移除掉 + this.parentNode.removeChild(this) + } + document.head.appendChild(temp) +} +``` + +还没有完,因为注入有权限,所以需要在 manifest.json 声明一下这个文件。也就是下面的这行代码 + +```js +{ + // 普通页面能够直接访问的插件资源列表,如果不设置是无法直接访问的 + "web_accessible_resources": ["js/inject.js"], +} +``` + +这样你就能调用 + +### 关于消息通信 + +Chrome 插件主要就 4 个部分组成,injected,content,popup,background,但这 4 个部分所对应的权限,应用都有可能各自不一,这时候就需要通过消息通信,将对应的数据发送到对应的文件,主要也就如下四种通信方式: + +#### popup 和 background + +popup 可以直接调用 background 中的 JS 方法,也可以直接访问 background 的 DOM: + +```javascript +// background.js +function test() { + alert('我是background!') +} + +// popup.js +var bg = chrome.extension.getBackgroundPage() +bg.test() // 访问bg的函数 +alert(bg.document.body.innerHTML) // 访问bg的DOM +``` + +`background`访问`popup`如下(前提是`popup`已经打开): + +```javascript +var views = chrome.extension.getViews({ type: 'popup' }) +if (views.length > 0) { + console.log(views[0].location.href) +} +``` + +#### popup 或 bg 与 content + +##### popup 或 bg 向 content 发送请求 + +```js +//background.js或popup.js: +function sendMessageToContentScript(message, callback) { + chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { + chrome.tabs.sendMessage(tabs[0].id, message, function (response) { + if (callback) callback(response) + }) + }) +} + +sendMessageToContentScript({ cmd: 'test', value: '你好,我是popup!' }, function (response) { + console.log('来自content的回复:' + response) +}) +``` + +`content.js`通过监听事件接收: + +```js +chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { + // console.log(sender.tab ?"from a content script:" + sender.tab.url :"from the extension"); + if (request.cmd == 'test') alert(request.value) + sendResponse('我收到了你的消息!') +}) +``` + +##### content 向 popup 或 bg + +```js +// content.js +chrome.runtime.sendMessage( + { greeting: '你好,我是content呀,我主动发消息给后台!' }, + function (response) { + console.log('收到来自后台的回复:' + response) + }, +) +``` + +```js +//background.js 或 popup.js: +// 监听来自content的消息 +chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { + console.log('收到来自content的消息:') + console.log(request, sender, sendResponse) + sendResponse('我是后台,我已收到你的消息:' + JSON.stringify(request)) +}) +``` + +注意: + +- content_scripts 向`popup`主动发消息的前提是 popup 必须打开!否则需要利用 background 作中转; +- 如果 background 和 popup 同时监听,那么它们都可以同时收到消息,但是只有一个可以 sendResponse,一个先发送了,那么另外一个再发送就无效; + +#### injected 和 content + +主要就是`injected`向`content`发送,`injected`无需监听。 + +`content`和页面内的脚本(`injected`自然也属于页面内的脚本)之间唯一共享的东西就是页面的 DOM 元素,有 2 种方法可以实现二者通讯,: + +1. 可以通过`window.postMessage`和`window.addEventListener`来实现二者消息通讯;(推荐) +2. 通过自定义 DOM 事件来实现(我就懒得写了,没怎么用到); + +`injected`中: + +```js +window.postMessage({ test: '你好!' }, '*') +``` + +`content script中`: + +```js +window.addEventListener( + 'message', + function (e) { + console.log(e.data) + }, + false, +) +``` + +#### injected 与 popup + +`injected`无法直接和`popup`通信,必须借助`content`作为中间人。不过一般这种都少,直接和 bg 通信即可。 + +## 我的模板 + +关于 Chrome 的主要内容也就这些,实际开发如果有个模板就能大大方便开发,在原文章中该作者已经分享了有对应的源代码,这里放上我自写的 Chrome 模板编写过程。 + +![image-20210820004414785](https://img.kuizuo.cn/image-20210820004414785.png) + +当然,这里需要提几点地方: + +### 配置项与 storage + +首先是配置方面,有时候插件的内的选项是要记录,以便下一次在启动插件的时候还是上一次的配置。先看代码 + +```html + +
+ + +
+
+ + +
+``` + +```js +// popup.js +$(function () { + let configs = document.getElementsByClassName('configs') + for (let i = 0; i < configs.length; i++) { + let type = configs[i].type + if (type == 'checkbox') { + configs[i].onchange = function () { + chrome.storage.sync.set({ + [this.id]: this.checked, + }) + } + chrome.storage.sync.get(configs[i].id, function (items) { + configs[i].checked = items[configs[i].id] || false + }) + } else if (type == 'text' || type == 'password') { + configs[i].onblur = function () { + chrome.storage.sync.set({ [this.id]: this.value }) + } + chrome.storage.sync.get(configs[i].id, function (items) { + configs[i].value = items[configs[i].id] || '' + }) + } + } +}) +``` + +可能需要多花点时间才能理解上面代码的意思,首先我在需要记录配置的地方添加了一个类`configs`,然后通过 js 代码遍历类名为`configs`,接着判断是多选框,还是输入框,input 的 id 为键名,value 为键值,来 set 或 get `chrome.storage`的值,然后进行事件绑定为修改配置后在记录一下配置。这里需要注意一下,写配置的时候`{ [this.id]: this.value }`这里的`this.id`是加了中括号的,原因就是这个 this.id 是变量,如果不加的话默认为字符串,但在这里有.所以是会报错的。 + +强烈不建议用 localStorage,我当初第一遍学的时候没学明白,我还通过消息通信将配置信息发给`content`,然后还用 localStorage 记录一遍,现在才发现`chrome.storage`是针对插件全局的,即使你在`background`或者`popup中`保存的数据,在`content`也能获取到。 + +当然这种读写配置的也算麻烦了,不像桌面级开发的读写配置。 + +### 悬浮窗 + +首先,一般对于网页端的插件,能提供的页面最好方式就是悬浮窗了,这里我也是通过 DOM 创建元素生成对象。而这个悬浮窗是针对页面的,而不是像 popup 那样。相关的页面初始化代码如下, + +```js +var view = { + show: true, + cache: { + count: 0, + type: 0, + mouse_x: -1, + mouse_y: -1, + }, +} + +function initView() { + view.float = $(` +
+
+ +
+
日志
+ +
+
+
+
+ + +
+

+
+
+
+ `) + view.info = view.float.find('#info') + view.kz_title = view.float.find('#kz_title') + view.kz_main = view.float.find('#kz_main') + view.float.appendTo('body').delegate('button', 'click', function (e) { + e.stopImmediatePropagation() + e.stopPropagation() + e.preventDefault() + let name = $(this).attr('name') + if (name == 'show') { + $(this).html(view.show ? '+' : '-') + view.show = !view.show + view.kz_main.slideToggle() + } + }) + addViewMouseListener() + log('日志输出1') + log('日志输出2') + log('日志输出3') +} + +function addViewMouseListener() { + view.float.bind('mousedown', function (event) { + view.cache.x = $(this).position().left + view.cache.y = $(this).position().top + view.cache.mouse_x = event.originalEvent.clientX + view.cache.mouse_y = event.originalEvent.clientY + //console.log(view.cache.mouse_x, view.cache.mouse_y, view.cache.x, view.cache.y) + }) + $(document).bind('mousemove', function (event) { + //计算出现在的位置是多少 + if (view.cache.mouse_x == -1) return + if (view.cache.mouse_y - view.cache.y > view.kz_title.height()) return + let new_position_left = event.originalEvent.clientX - view.cache.mouse_x + view.cache.x, + new_position_top = event.originalEvent.clientY - view.cache.mouse_y + view.cache.y + //加上边界限制 + if (new_position_top < 0) { + //当上边的偏移量小于0的时候,就是上边的临界点,就让新的位置为0 + new_position_top = 0 + } + //如果向下的偏移量大于文档对象的高度减去自身的高度,就让它等于这个高度 + if ( + new_position_top > $(document).height() - view.float.height() && + $(document).height() - view.float.height() > 0 + ) { + new_position_top = $(document).height() - view.float.height() + } + //右限制 + if (new_position_left > $(document).width() - view.float.width()) { + new_position_left = $(document).width() - view.float.width() + } + if (new_position_left < 0) { + //左边的偏移量小于0的时候设置 左边的位置为0 + new_position_left = 0 + } + view.float.css({ + left: new_position_left + 'px', + top: new_position_top + 'px', + }) + }) + $(document).bind('mouseup', function (event) { + view.cache.mouse_x = -1 + view.cache.mouse_y = -1 + }) +} + +function log(msg, color) { + let date = new Date() + let t = date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds() + msg = t + ' ' + msg + let div = $('
').css({ + 'border-color': 'rgba(121, 187, 255, 0.2)', + 'background-color': 'rgba(121, 187, 255, 0.2)', + }) + let log = $('

' + msg + '

') + + if ($('.log').length > 15) { + for (let i = 0; $('.log').length - 15; i++) { + $('.log')[i].remove() + } + } + $('#logList').append(div.append(log)) +} +``` + +然后在 content.js 内容的对页面 url 判断是否需要初始化悬浮窗即可 + +```js +document.addEventListener('DOMContentLoaded', function () { + if (location.host.indexOf('chaoxing') != -1) { + initView() + } +}) +``` + +如何发挥就看各位了。 + +### 跨域请求 + +关于跨域请求,我当初在学习 Chrome 插件的时候,就是卡在了跨域这个地方,那时候前端学的浅,对跨域都不知道处理,然后放弃学习了 Chrome 插件一段时间,后来有时间了,想在补一补之前没写完的 Chrome 扩展搞完。然而跨域请求非常简单,而我那时候之所以卡住就是因为没好好看文档,搞不定的地方就多看几遍说不准就搞定了。 + +首先要使 Chrome 插件访问跨域资源,需要在 manifest.json 文件中声明要访问的域如下: + +```json +{ + "permissions": [ + "http://www.google.com/", + "http://*.google.com/", + "https://*.google.com/", + "http://*/" + ] +} +``` + +建议直接直接暴力点写上 + +```json +{ + "permissions": ["http://*/*", "https://*/*"] +} +``` + +然后封装一下对应的 ajax 请求,因为在 content 内进行 ajax 请求,是会在控制台输出跨域请求拦截,或者是 HTTPS 访问 HTTP 不安全等问题,这时候就需要通过消息通信,将 content 要发送的请求发送给 bg,让 bg 请求,然后等 bg 请求完毕,再将数据返回到 content 即可。下面是我对应的封装代码 + +```js +// background.js +chrome.runtime.onMessage.addListener(function (req, sender, sendResponse) { + console.log(req, sender, sendResponse) + if (req.cmd == 'ajax') { + $.ajax({ + url: req.url, + type: req.type, + data: req.data, + async: false, + success: function (res) { + sendResponse(res) + }, + }) + } +}) +``` + +```js +// content.js +function sendAjaxToBg(url, type, data, callback) { + chrome.runtime.sendMessage( + { cmd: 'ajax', url: url, type: type, data: data }, + function (response) { + callback(response) + }, + ) +} +``` + +这里的话我通信发送的是 js 对象,其中 cmd 决定了我要的操作,后台通过判断 cmd 来执行对应的操作。比较不好理解的是回调函数,由于 JS 自身语言的因素与浏览器的问题,很多事件都是先挂着,后做完在回调,所以我这里就封装成这种形式,例如 + +```js +sendAjaxToBg("http://...", "GET", null, function(response){ + console.log(response) + ...code +}) +``` + +这只是一个简单的 http 封装发送,如果要更复杂的话还可以添加协议头和 cookies,这里就不在补充了。 + +### 一些自写 Chrome 插件 + +实际上已经写过一些 Chrome 插件了,奈何写的比较烂或没搞完,也就暂时先不发,有时间会再整理一下自己所写的。 + +一个验证码识别,有时候在登录的时候需要输入验证码是件非常痛苦的事情。于是乎我就通过调用打码 Api 接口写了个自动识别验证码并填写的。也提供了非常方便的右键识别验证码的功能。具体效果如图(实际上还是得第一次先确认要识别的图片框与输入框,下次加载的时候需要手动点击验证码才会自动生效,还是不够智能的,不过成就感十足) + +![image-20210820001938051](https://img.kuizuo.cn/image-20210820001938051.png) + +![wydm](https://img.kuizuo.cn/wydm.gif) + +另一个是基于某布大佬的 WebHook 工具,所更改的,不过一直停滞着,有空将其完善一下。 diff --git "a/blog/program/Deno\344\270\215\345\217\252\346\230\257\344\270\252Javascript\350\277\220\350\241\214\346\227\266.md" "b/blog/program/Deno\344\270\215\345\217\252\346\230\257\344\270\252Javascript\350\277\220\350\241\214\346\227\266.md" new file mode 100644 index 0000000..b05a90a --- /dev/null +++ "b/blog/program/Deno\344\270\215\345\217\252\346\230\257\344\270\252Javascript\350\277\220\350\241\214\346\227\266.md" @@ -0,0 +1,263 @@ +--- +slug: deno-is-not-only-a-javascript-runtime +title: Deno 不只是个 Javascript 运行时 +date: 2023-01-20 +authors: kuizuo +tags: [deno, node, javascript, typescript] +keywords: [deno, node, javascript, typescript] +image: https://img.kuizuo.cn/202312270248371.png +--- + +Deno 是一个安全的 JavaScript 和 TypeScript 运行时,作者是 Ryan Dahl(也是 Node.js 的原作者)。Deno 的诞生之初是为了[解决 2009 年首次设计 Node.js 时的一些疏忽](https://link.juejin.cn?target=https://www.youtube.com/watch?v=M3BM9TB-8yA)。我认为这种改造动机很有道理,因为**我相信每个程序员都希望有机会能重写他们已有 10 年历史的代码。** + +deno 刚出的时候就听闻了,传言 deno 是下一代 node.js。不过如今看来,还革不了 node.js 的命。如果要说两者字面上的区别,Deno 的来源是 Node 的字母重新组合(Node = no + de),表示"拆除 Node.js"(de = destroy, no = Node.js)。 + +趁着假期学了一段时间的 deno(指[文档](https://deno.land/manual@v1.29.3/introduction '文档')刷了一遍),想分享本人作为 node 开发者在学习 deno 时认为的一些亮点,以及个人对 deno 与 node 见解。 + + + +### 开发环境 + +[Installation | Manual | Deno](https://deno.land/manual@v1.29.2/getting_started/installation 'Installation | Manual | Deno') + +默认情况下 deno 会根据不同的系统,选择相应的安装目录,以及依赖目录,你可以[配置环境变量](https://deno.land/manual@v1.29.3/getting_started/setup_your_environment#environment-variables '配置环境变量')来改变 deno 的默认行为。 + +这里我选用 vscode 进行开发,安装[deno 官方插件](https://marketplace.visualstudio.com/items?itemName=denoland.vscode-deno 'deno官方插件')。此时创建一个项目工程文件夹,打开 vscode,并创建 `.vscode/settings.json` 内容如下 + +```json title='.vscode/settings.json' icon='logos:visual-studio-code' +{ + "deno.enable": true, + "deno.lint": true, + "editor.formatOnSave": true, + "[typescript]": { "editor.defaultFormatter": "denoland.vscode-deno" } +} +``` + +在 vscode 中默认会将 ts 代码认为是 node 运行时环境,因此需要在项目工程下手动配置并启用 deno,让 vscode 以 deno 运行时环境来语法解析 ts 代码。 + +## deno 的一些亮点💡 + +因为 deno 与 node 一样,都是 javascript 运行时(deno 合理来说是 typescript 运行时)。所以在 javascript 的部分就没什么好说的了,主要对比 deno 相比与 node 的优势,或说我个人觉得一些使用亮点。 + +### 官方所介绍的亮点 + +以下是官方所介绍的[亮点](https://deno.land/manual@v1.29.3/introduction#feature-highlights '亮点'),我对其做了翻译 + +- 提供[web 平台功能](https://deno.land/manual@v1.29.3/runtime/web_platform_apis 'web平台功能'),采用网络平台标准。例如,使用 ES 模块、Web worker 和支持 `fetch()`。 +- 默认安全。除非显式启用,否则无法访问文件、网络或环境。 + +- 支持开箱即用的 [TypeScript](https://deno.land/manual@v1.29.3/advanced/typescript 'TypeScript')。 +- 提供单个可执行文件 (`deno`)。 +- 为编辑器提供内置的开发工具,如代码格式化程序 ([deno fmt](https://deno.land/manual@v1.29.3/tools/formatter 'deno fmt'))、linter ([deno lint](https://deno.land/manual@v1.29.3/tools/linter 'deno lint'))、测试运行程序([deno test](https://deno.land/manual@v1.29.3/basics/testing 'deno test'))和[语言服务器](https://deno.land/manual@v1.29.3/getting_started/setup_your_environment.md#using-an-editoride '语言服务器')。 +- 拥有[一组经过审查(审核)的标准模块](https://deno.land/std@0.172.0 '一组经过审查(审核)的标准模块'),保证与 Deno 一起使用。 +- 可以将脚本[捆绑](https://deno.land/manual@v1.29.3/tools/bundler '捆绑')到单个 JavaScript 文件或[可执行文件](https://deno.land/manual@v1.29.3/tools/compiler '可执行文件')中。 +- 支持使用现有的 npm 模块 + +以下会针对部分亮点,进行个人的见解。 + +### 自带实用工具 + +deno 则是自带代码格式化(`deno fmt`)、代码风格(`deno lint`)、代码测试(`deno test`)、依赖检查器(`deno info`)等等的功能。而这些在 node 中,你需要通过第三方的库,如 eslint,jest 才能实现。 + +你可以在项目工程中添加配置文件 [deno.json](https://deno.land/manual@v1.29.2/getting_started/configuration_file 'deno.json')来定制化代码风格(rust 中也有类似的功能),但在 node 中必须要借助第三方的库,或是 IDE 才能实现。 + +不过也能理解,在当时的编程环境背景下,javascript 还主要作为前端的脚本语言使用,又怎能让 node 来做相关规范呢?(这句话可能有点不妥) + +**这点我认为对开发者是否选用你这门语言的一个加分项**,并且这些功能也应该作为编程语言所自带的,有官方的背书(保证),对代码风格才更有所保障。 + +这里有份 [官方小抄](https://deno.land/manual@v1.29.4/references/cheatsheet#nodejs---deno-cheatsheet '官方小抄') 可以知道通过`deno xxx`等命令能够做到 node 原本需要通过第三方库才能实现的功能。 + +| Node.js | Deno | +| -------------------------------------- | ---------------------------------------------- | +| `node file.js` | `deno run file.js` | +| `ts-node file.ts` | `deno run file.ts` | +| `npm i -g` | `deno install` | +| `npm i` / `npm install` | _n/a_ | +| `npm run` | `deno task` | +| `eslint` | `deno lint` | +| `prettier` | `deno fmt` | +| `rollup` / `webpack` / etc | `deno bundle` | +| `package.json` | `deno.json` / `deno.jsonc` / `import_map.json` | +| `tsc` | `deno check` | +| `typedoc` | `deno doc` | +| `jest` / `ava` / `mocha` / `tap` / etc | `deno test` | +| `nodemon` | `deno run/lint/test --watch` | +| `nexe` / `pkg` | `deno compile` | +| `npm explain` | `deno info` | +| `nvm` / `n` / `fnm` | `deno upgrade` | +| `tsserver` | `deno lsp` | +| `nyc` / `c8` / `istanbul` | `deno coverage` | +| `benchmarks` | `deno bench` | + +### [远程导入](https://deno.land/manual@v1.29.3/basics/modules#remote-import '远程导入') + +与 node 不同,使用 node 通常需要从 npm 官方包来下载并导,有 npm 这样的包管理器来统一管理这些包(package),我们通常称这种为中心化,而 deno 与 go 的做法很像,你可以将你的封装好的代码定义成一个包,并将其放在任何网络可访问的地方,比如 github,或是私有地址,然后通过网络读取文件的方式来导入,这种称为去中心化。 + +:::tip node 也不一定要用 npm 来下载模块,也可以本地模块或者私有模块。 + +::: + +关于中心化与去中心化管理,各有优缺,这里不做细致讨论。 + +以下是 deno 官方远程导入的代码示例: + +```typescript title='remote.ts' icon="logos:typescript-icon" +import { add, multiply } from 'https://x.nest.land/ramda@0.27.0/source/index.js' + +function totalCost(outbound: number, inbound: number, tax: number): number { + return multiply(add(outbound, inbound), tax) +} + +console.log(totalCost(19, 31, 1.2)) +console.log(totalCost(45, 27, 1.15)) + +/** + * Output + * + * 60 + * 82.8 + */ +``` + +而这里的 `https://x.nest.land/ramda@0.27.0/source/index.js` 可以替换成任何 ES module 特性(import/export)的模块。 + +### http 的方式运行代码 + +既然都能通过 http(cdn)远程导入模块,那远程运行文件自然也不成大问题。有时候像快捷体验一下别人的代码,或是想要在浏览器中运行一下代码,这时候就可以通过 http 的方式来运行代码。 + +这里我准备了一段代码,并部署到我的站点上,你可以通过如下命令得到该代码的执行结果(如果你有安装 deno 的话),放心这段代码并无危害,就是一段简单的 console.log 输出。 + +```bash +deno run https://deno.kuizuo.cn/main.ts +``` + +在第一次使用时下载并缓存代码,你可以通过 + +```bash +deno info http://deno.kuizuo.cn/main.ts +``` + +来查看文件信息,如下 + +![](https://img.kuizuo.cn/image_deb0_lGYRA.png) + +deno info 还可以查看 deno 的相关配置,默认缓存都设置在 C 盘,你也可以设置**DENO_DIR** 环境变量来更改 deno 目录,可以到 [Set Up Your Environment](https://deno.land/manual@v1.29.3/getting_started/setup_your_environment#environment-variables 'Set Up Your Environment') 查看 deno 相关环境变量。 + +### 依赖管理 + +经常使用 node 的开发者应该对 node 的依赖感到无比厌烦,关于这部分强烈建议看 [node_modules 困境](https://juejin.cn/post/6914508615969669127),你就能知道 node 的 node_modules 设计的是有多少问题。看完你也就能知道为啥越来越多的 node 项目都使用 [pnpm](https://pnpm.io) 作为包管理。 + +虽然 node 有了 pnpm 包管理器这种情况会好一些,但本质在项目目录还是需要 node_modules 文件。也许你用过其他语言的包管理器,你会发现基本都是将所有用到的依赖全局缓存起来,当不同的项目工程需要用到依赖时,直接去全局缓存中找,而不是像 npm 一样,下载到项目工程目录下,存放在 node_modules 里。 + +而 deno 也是采用这种这种方式,`no npm install`,`no package.json`,`no node_modules/` ,[使用 npm 包](https://deno.land/manual@v1.29.3/node/npm_specifiers#using-npm-packages-with-npm-specifiers '使用npm包')可以像下面这样,当你使用 deno run 时便会下载好依赖置全局缓存中。 + +```typescript title="app.ts" {2} icon='logos:typescript-icon' +// @deno-types="npm:@types/express@^4.17" +import express from 'npm:express@^4.17' +const app = express() + +app.get('/', (req, res) => { + res.send('Hello World') +}) + +app.listen(3000) +console.log('listening on http://localhost:3000/') +``` + +deno 刚发布的时候,甚至还不支持 NPM 软件包,这无非是要告诉用户 deno 社区没有轮子,要求用户自己去造一个。不过 deno 团队还是做出了比较正确的选择,支持 npm 软件包,并且还非常友好。 + +不过如果你在 deno 中使用了 npm 包,可能会存在一些兼容性问题,万一遇到了,也可以通过添加 `--node-modules-dir` 标识,在当前运行目录下创建 `node_modules` 文件夹。详见 [--node-modules-dir flag](https://deno.land/manual@v1.29.4/node/npm_specifiers#--node-modules-dir-flag '--node-modules-dir flag') + +### 安全 + +[Permissions](https://deno.land/manual@v1.29.4/basics/permissions 'Permissions') + +在 2022 年 npm 出现过一些恶性的库,如 lodash-utils, faker.js, chalk-next。万一你不小心安装了上面,轻则项目无法运行,输出无意义乱码,重则删除本地文件。 + +又因为 npm 几乎没有代码审计的机制,任何开发者只需要有一个 npm 的账号就能在上面随意发布他想发布的包。通常来说电脑病毒都是通过随意读取与写入本地文件来达到病毒的目的,但在 deno 中,代码如果尝试写入与读入文件,都需要询问开发者是否允许操作。并且在 linux 系统,你可以指定像 /usr /etc 这样非 root 角色来操作该文件,避免真是病毒文件导致删除不该删除的文件。 + +此外像命令执行,网络访问,环境变量这些极易危害电脑的权限,deno 都会检测到,并做出提示告诫开发者是否允许执行。总之你能想到的电脑安全隐患,deno 都为你做好了。 + +### 内置浏览器环境(运行时) + +这是我认为 deno 最大的亮点。 + +总所周知,浏览器的 js 代码有很大概率是无法直接在 node 中跑起来的,原因就是 node 的全局对象中没有浏览器的对象,如 window,document,甚至连`localStorage` 都有! + +这说明什么,往常如果你从别的网站扣了一段代码下来,想在 node 运行会发现什么 window is not defined,xxx is not defined。如果想在 node 运行,你必须需要补齐浏览器的环境,此外可以借助 js-dom,happy-dom 等 npm 包。而 window,xxx 这些全局只有浏览器才定义的全局对象在 deno 的运行时同样定义了,可以到[这里](https://deno.land/manual@v1.29.3/runtime/web_platform_apis#using-web-platform-apis '这里')查看支持的 Web 平台 API。 + +虽说与真实浏览器全局对象有些许差异,但这也足够让开发者少做很多工作。比如 Web 逆向者通常要扣取浏览器的 js 代码,并补齐环境使其能够在 node 中运行,而有了 deno 这将变得非常轻松! + +**与其说是 javascript/typescript 运行时,我更愿意说是浏览器运行时!** + +### Web 框架 + +你可以在 [Web Frameworks](https://deno.land/manual@v1.29.2/getting_started/web_frameworks 'Web Frameworks') 中看到 deno 官方所推荐的 Web 框架,其中 [Fresh](https://deno.land/manual@v1.29.2/getting_started/web_frameworks#fresh 'Fresh') 也是最为推荐使用的(后续我也会尝试使用该框架)。 + +而在 node 社区中,你会看到像 express,koa,nestjs 等等这种非 Node 官方或大背景的 web 框架(而且还很多),而这时对于初学者而言,就有点不知道该如何做出抉择。 + +而像 java 中你完全可以不用担心该学什么,说学 spring 就是在学 java 这可一点都不为过。可能这也是国内 java,尤其是 spring 的开发者尤为诸多的原因。 + +吐槽归吐槽,但我想表明的是在有官方的支持下,用户和开发者能够统一使用某个框架,一起维护与使用一个更好的框架。而不是个个 Web 框架的都有各自的优缺点,让使用者去选择,搞得这个框架是另一个框架的轮子一般。 + +所以我认为这种支持是很有必要。 + +### 公共托管服务 + +[Project - Deploy (deno.com)](https://dash.deno.com/ 'Project - Deploy (deno.com)') + +deno 像 vercel/netfily 一样提供了一个代码托管服务,可以将你的 deno 应用部署上去。对,目前来看还无法部署前端应用,因为要指明一个入门文件(main.ts)。 + +你可以通过 [https://kuizuo.deno.dev/](https://kuizuo.deno.dev/ 'https://kuizuo.deno.dev/') 来访问我使用 deno Deploy 所创建的一个在线项目。将会输出一个`Hello World!` 的页面。 + +提供一个免费的线上环境体验,对开发者而言尤为重要,尤其是在将自己的项目成果分享给他人展示时,成就感油然而生。 + +## node 转 deno 开发的一些帮助 + +deno 相关的亮点我也差不多介绍完了,也许你对 deno 已经有一丝兴趣想尝试一番,以下我整理的对你也许有所帮助。 + +- 如果你是一个 Node 用户,考虑切换到 Deno,这里有一个[官方小抄](https://link.juejin.cn/?target=https://deno.land/manual/node/cheatsheet '官方小抄')来帮助你。 + +- 如果你不想刷 deno 文档,想快速上手 deno 的话,这里我建议推荐看看 deno 官方所推荐的[deno 代码例子 ](https://deno.land/manual@v1.29.4/examples 'deno代码例子 '),能够非常快速有效了直接了解 deno 标准库以及依赖导入导出。 + +- deno 是集成了 node 与 npm 的,也就是说允许直接使用 npm 包与 node 标准库,如果你想用 deno 来写 node,也行,详看[Interoperating with Node.js and npm](https://deno.land/manual@v1.29.4/node#interoperating-with-nodejs-and-npm 'Interoperating with Node.js and npm')。 + +- 想要在 deno 中连接数据库,可看[Connecting to Databases](https://deno.land/manual@v1.29.4/basics/connecting_to_databases#connecting-to-databases 'Connecting to Databases')。 + +- 如果想看 deno 如何使用 deno 生态的 Web 框架创建一个 Web 服务,推荐[fresh](https://fresh.deno.dev/ 'fresh')框架,并查看该例子[fresh/examples/counter](https://github.com/denoland/fresh/tree/main/examples/counter 'fresh/examples/counter') + +## node 火吗? + +关于 deno 就暂且落下笔墨,不妨思考一个问题,node 火吗。 + +作为 node 开发者,我肯定会说 node 火,不过更多是对 javascript 来说火。 + +如今 typescript 大势所趋,说 javascript 就等同于说 typescript,而 javascript 和 node 绑定已成事实,而前端也与 javascript 所绑定,如今的前端工程师要是不会 node,都不好意思说自己是个前端工程师。就现阶段看,没了 nodejs,前端技术得倒退十年(不夸张)。 + +如果是在 Web 前端,Node 确实已经火的一塌糊涂了,然而它的诞生并不是为了 Web 前端,而是希望将 javascript 作为服务器端语言发展。只是后来没有想到的是 Node.js 在前端领域却大放异彩,造就了如今大前端的盛世。 + +所以在 Web 后端的领域,Node 确实是不温不火,更多的公司都宁可选主流的后端开发语言,而不是优先考虑 Node。不过倒是在 Serverless 领域中,Node 有着一席之地。 + +所以我想 deno 的出现,不仅是针对 Node.js 的缺陷,更是针对 Node.js 后端开发的不足。至于说 deno 能否完成原先 node 的使命,只有时间能给我们答案。 + +## 总结 + +从上述看来,你应该会发现 deno 并不和 node 一样是一个纯运行时环境。因为他不仅仅做了 javascript/typescript 运行时环境,还做了很多开发者好评的功能,一个为 javascript/typescript 提供更好的开发支持的产品。 + +但好评并不能直接决定销量,这些功能看似可有可无,没有激起用户从 Node.js 切换过来的杰出之处。就我体验完发现,好像 deno 能做的东西 node 大部分也能做,只是相对繁琐重复一些而已。**但人们更倾向于做一件繁琐重复的事情,而不是做一个新的事情。** + +扪心自问,我真的很希望 deno 能火,就开发体验而言,比 node 好用太多了,但好用的东西代表不了用的人就多,这个领域中,生态尤为重要。想要让 node 用户转到 deno 开发还有很长一段路要走。 + +再来反问自己,我现在会将 deno 作为 node 替代品吗,我想我和多数 node 开发者一样,都不会将 deno 作为主力语言(因为有很多项目都已经使用node来进行开发与推动)。但作为个人开发者,尤其是 node 开发者,我认为还是非常有必要去尝试一番 deno,亲手目睹"下一代Node"。 + +希望本文能对你了解 deno 有所帮助。 + +## 相关推荐文章 + +[Deno vs. Node.js 哪个更好 - 掘金 (juejin.cn)](https://juejin.cn/post/7168383367602241550 'Deno vs. Node.js哪个更好 - 掘金 (juejin.cn)') + +[为什么 Deno 没有众望所归?超越 Node.js 还要做些什么? - 掘金 (juejin.cn)](https://juejin.cn/post/6956461134299955213 '为什么 Deno 没有众望所归?超越 Node.js 还要做些什么? - 掘金 (juejin.cn)') + +[连发明人都抛弃 node.js 了,还有前途吗? - 知乎 (zhihu.com)](https://www.zhihu.com/question/327534747 '连发明人都抛弃node.js了,还有前途吗? - 知乎 (zhihu.com)') + +[已经 2022 年了 Deno 现在怎么样了? - 知乎 (zhihu.com)](https://www.zhihu.com/question/517617266 '已经 2022 年了 Deno 现在怎么样了? - 知乎 (zhihu.com)') diff --git "a/blog/program/Gitea \344\270\216 Drone \345\256\236\350\267\265.md" "b/blog/program/Gitea \344\270\216 Drone \345\256\236\350\267\265.md" new file mode 100644 index 0000000..28749ac --- /dev/null +++ "b/blog/program/Gitea \344\270\216 Drone \345\256\236\350\267\265.md" @@ -0,0 +1,227 @@ +--- +slug: gitea-drone-practice +title: Gitea 与 Drone 实践 +date: 2022-09-28 +authors: kuizuo +tags: [git, gitea, drone] +keywords: [git, gitea, drone] +description: 使用 Gitea 搭建一个轻量级 git 私有仓库,并配置 Drone CI 来实现自动构建与部署。 +--- + +之前搭建过 Gitlab,但是就只是搭建而已,并未实际使用,因为我大部分的代码还是存放在 [Github](https://github.com/kuizuo?tab=repositories) 上。 + +并且大部分项目都是在 [Vercel](https://vercel.com) 上运行的(Vercel 是真好用),但是最近国内访问 vercel 情况不容乐观,貌似被墙了呜呜。然后 Gitlab 的资源占用非常严重,几乎占用了一半的服务器性能,可 [点我](https://kuizuo.cn/gitlab-code-management-environment#运行状态) 查看运行状态。与此同时,随着很多私有项目越来越多,使用 git 私有仓库以及 Vercel 部署,肯定不如自建私有 git 服务和自有服务器部署使用体验来好。 + +于是就想搭建一个轻量级仓库,同时支持 CI/CD。经过一番的调研,决定使用 Gitea 和 Drone 作为解决方案。 + + + +## Gitea + +[Gitea](https://gitea.io/zh-cn/ 'Gitea') 是一个开源社区驱动的轻量级代码托管解决方案,后端采用 [Go](https://golang.org/ 'Go') 编写,采用 [MIT](https://github.com/go-gitea/gitea/blob/master/LICENSE 'MIT') 许可证. + +你可以在 [横向对比 Gitea 与其它 Git 托管工具](https://docs.gitea.io/zh-cn/comparison/#横向对比-gitea-与其它-git-托管工具 '横向对比 Gitea 与其它 Git 托管工具') 查看 gitea 与其他 git 工具的优势与缺陷。 + +### 安装 + +这里我选用 Docker 进行安装,安装文档可在[官方文档](https://docs.gitea.io/zh-cn/ '官方文档')中查看其他安装方式 + +```yaml title='docker-compose.yml' icon='logos:docker-icon' +version: '3' + +networks: + gitea: + external: false + +volumes: + gitea: + driver: local + +services: + server: + image: gitea/gitea:1.17.1 + container_name: gitea + environment: + - USER_UID=1000 + - USER_GID=1000 + restart: always + networks: + - gitea + volumes: + - gitea:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - '10800:3000' + - '2221:22' +``` + +根据自身需求配置 docker-compose.yml 内容。运行 `docker-compose up` 等待部署 + +服务器防火墙与云服务安全组都需要开放端口才可访问,`服务器ip:10800`,将会出现如下界面 + +![](https://img.kuizuo.cn/image_8ix-AMvt3t.png) + +**因为修改配置相对比较麻烦,所以在首次安装的时候,请根据实际需求进行配置安装。** + +### 修改配置 + +假设要修改其中的配置的话,gitea 的后台管理面板是无法直接修改的。需要到 `/data/gitea/conf/app.ini` 中修改,具体修改的配置 参阅 [自定义 Gitea 配置 - Docs](https://docs.gitea.io/zh-cn/customizing-gitea/ '自定义 Gitea 配置 - Docs') + +:::warning 必须完全重启 Gitea 以使配置生效。 + +::: + +### 迁移仓库 + +从其他第三方 git 仓库迁移到 gitea,可以访问[https://git.kuizuo.cn/repo/migrate](https://git.kuizuo.cn/repo/migrate 'https://git.kuizuo.cn/repo/migrate') 来迁移仓库 + +![](https://img.kuizuo.cn/image_sRQV5hAKUh.png) + +稍等片刻,取决于访问 github 仓库的速度。有可能还会迁移失败,就像下面这样。 + +![](https://img.kuizuo.cn/image_X9IpG2q36n.png) + +所以可以申请访问令牌(Access Token),在 [New Personal Access Token](https://github.com/settings/tokens/new 'New Personal Access Token') 处创建。迁移成功后,如下图所示 + +![](https://img.kuizuo.cn/image_Rug0AmD8GE.png) + +### 镜像仓库 + +很大部分时间,gitea 只能作为我的副仓库,或者说 github 的镜像仓库。 + +gitea 也提供镜像仓库的方案,官方文档[Repository Mirror](https://docs.gitea.io/en-us/repo-mirror/ 'Repository Mirror') + +![](https://img.kuizuo.cn/image_Q5IaHnKCYJ.png) + +## Drone + +由于 Gitea 并没有内置 CI/CD(持续集成/持续部署) 的解决方案,所以需要配置第三方的,这里推荐使用 Drone CI。 + +Drone 是面向繁忙开发团队的自助服务持续集成平台。相对于常见的 Jenkins,选中 Drone 的原因在于它非常简洁,不像 Jenkins 那样复杂,同时它拥有可以满足基本需求的能力,并且提供了许多实用的[插件](https://plugins.drone.io/),如 GitHub,Email,微信,钉钉等 + +### 安装 + +由于我们使用了 gitea,所以 drone 中选择 gitea 来安装,这是官方文档 [Gitea | Drone](https://docs.drone.io/server/provider/gitea/ 'Gitea | Drone'),照着操作即可。 + +需要安装 Server 和 Runner,一个是 Drone 的服务,另一个用于检测 Git 记录,以重新构建项目。 + +这里贴下 drone 的 docker 配置(根据文档和自己部署的 git 服务配置来替换)。 + +```yaml title='server' +docker run \ --volume=/var/lib/drone:/data \ --env=DRONE_GITEA_SERVER=https://try.gitea.io \ --env=DRONE_GITEA_CLIENT_ID=05136e57d80189bef462 \ --env=DRONE_GITEA_CLIENT_SECRET=7c229228a77d2cbddaa61ddc78d45e \ --env=DRONE_RPC_SECRET=super-duper-secret \ --env=DRONE_SERVER_HOST=drone.company.com \ --env=DRONE_SERVER_PROTO=https \ --publish=80:80 \ --publish=443:443 \ --restart=always \ --detach=true \ --name=drone \ drone/drone:2 +``` + +```yaml title='runner' +docker run --detach \ --volume=/var/run/docker.sock:/var/run/docker.sock \ --env=DRONE_RPC_PROTO=https \ --env=DRONE_RPC_HOST=drone.company.com \ --env=DRONE_RPC_SECRET=super-duper-secret \ --env=DRONE_RUNNER_CAPACITY=2 \ --env=DRONE_RUNNER_NAME=my-first-runner \ --publish=3000:3000 \ --restart=always \ --name=runner \ drone/drone-runner-docker:1 +``` + +查看连接情况 + +```bash +docker logs runner +``` + +执行完毕后,然后访问线上的 drone 服务,点击 CONTINUE 将会跳转到你的 Git 授权页面 + +![](https://img.kuizuo.cn/image_rUdNHPlB73.png) + +点击应用授权,再次回到 drone,此时页面 Dashboard 列出了 gitea 的所有仓库(如果没有的话,可以点击右上角的 SYNC 来同步)。 + +![](https://img.kuizuo.cn/image_TXWZgDOhrQ.png) + +## 实战 + +上述只是安装了,我们还需要编写 `.drone.yml` 配置文件来告诉 drone 我们要做什么,编写过程与 Github Action 类似。相关文档: [Pipeline | Drone](https://docs.drone.io/pipeline/overview/ 'Overview | Drone') + +### 部署前端项目 + +这里就选用 [antfu/vitesse](https://github.com/antfu/vitesse 'antfu/vitesse') 作为演示。这里省略 clone 仓库的步骤。进入到自己的 gitea 仓库,然后添加 `.drone.yml` 文件,内容如下: + +```yaml title='.drone.yml' icon='logos:drone-icon' +kind: pipeline +type: docker +name: ci + +steps: + - name: install & build + image: node + commands: + - npm config set registry http://mirrors.cloud.tencent.com/npm/ + - npm i -g pnpm + - pnpm i + - pnpm run build + + - name: upload + image: appleboy/drone-scp + settings: + host: + from_secret: host + username: + from_secret: username + password: + from_secret: password + port: 22 + command_timeout: 2m + target: /www/wwwroot/${DRONE_REPO_OWNER}/${DRONE_REPO_NAME} + source: + - ./dist +``` + +这里对 `.drone.yml` 配置进行详解: + +其中 build 这个不用多说,与 node 构建相关的,不过多介绍。 + +upload 则使用[appleboy/drone-scp](https://plugins.drone.io/plugins/scp 'appleboy/drone-scp')插件,可以将构建出来的文件通过发送到服务器指定位置。在这里 source 对应就是构建的文件,target 则是要移动的位置,这里的 `/www/wwwroot/${DRONE_REPO_OWNER}/${DRONE_REPO_NAME}` 对应本项目为 `/www/wwwroot/kuizuo/vitesse`。此外 ssh 的 host,username,password 或 key,都作为环境变量(私有变量的方式传递,这在 drone 的控制台中可以设置)。 + +由于每次构建可能需要删除原有的已部署的资源文件,那么可以使用 [appleboy/drone-ssh](https://plugins.drone.io/plugins/ssh) 插件来执行终端命令来删除,例如 + +```yaml title='.drone.yml' icon='logos:drone-icon' +kind: pipeline +name: default + +steps: + - name: deploy + image: appleboy/drone-ssh + environment: + DEPLOY_PATH: + from_secret: /www/wwwroot/${DRONE_REPO_OWNER}/${DRONE_REPO_NAME} + settings: + host: + from_secret: host + username: + from_secret: username + password: + from_secret: password + port: 22 + command_timeout: 2m + envs: [DEPLOY_PATH] + script: + - rm -rf $${DEPLOY_PATH} +``` + +具体就因人而异了,这里我仅作为演示。 + +大致介绍完毕(其实已经介绍差不多了),有关更多插件可以参阅 [drone 插件](https://plugins.drone.io 'drone 插件')。这里开始演示,进入 drone 页面,找到仓库,默认情况下,所有仓库都处于未激活状态。 + +![](https://img.kuizuo.cn/image_6XBrsAY8VE.png) + +点击 `ACTIVATE REPOSITORY` 根据选项选择,点击右上角的`NEW BUILD`选择分支,添加 drone 环境变量(私有变量),即上面的 from_secret 后面的内容(host,username,password),即可开始运行。 + +![](https://img.kuizuo.cn/image_PAM6QQS1V_.png) + +静等 PIPELINE 执行完毕,结果如下 + +![image-20220928152635955](https://img.kuizuo.cn/image-20220928152635955.png) + +此时打开宝塔,跳转到指定目录下,就可以看到构建的内容都已经放到指定位置了 + +![image-20220928152725853](https://img.kuizuo.cn/image-20220928152725853.png) + +这时候只需要配置下 nginx,就能将页面展示到公网上,这里就不在这里赘述。当完成上述配置完毕后,每次只需要 pull request,drone 就会自动拉取 gitea 的代码,并开始执行`.drone.yml`中的任务。 + +## 参考文章 + +[【CI/CD】搭建 drone 服务,构建前端 cicd 工作流,实现博客的自动化打包并部署 - 掘金 (juejin.cn)](https://juejin.cn/post/7073380337766072350 '【CI/CD】搭建drone服务,构建前端cicd工作流,实现博客的自动化打包并部署 - 掘金 (juejin.cn)') + +[单机部署 CI/CD 进阶版:宝塔+gitea+drone | Laravel China 社区 (learnku.com)](https://learnku.com/articles/71333) diff --git "a/blog/program/GraphQL\345\256\236\350\267\265.md" "b/blog/program/GraphQL\345\256\236\350\267\265.md" new file mode 100644 index 0000000..bcf5e3d --- /dev/null +++ "b/blog/program/GraphQL\345\256\236\350\267\265.md" @@ -0,0 +1,532 @@ +--- +slug: graphql-practice +title: GraphQL 实践与服务搭建 +date: 2022-11-24 +authors: kuizuo +tags: [api, graphql, nest, strapi] +keywords: [api, graphql, nest, strapi] +description: 有关 GraphQL 介绍及上手实践,并在 Nest.js 和 Strapi 中搭建 GraphQL 服务 +image: https://img.kuizuo.cn/320f3e5a66900d68e93de38154989948.png +--- + +> GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。 + +大概率你听说过 GraphQL,知道它是一种与 Rest API 架构属于 API 接口的查询语言。但大概率你也与我一样没有尝试过 GraphQL。 + +事实上从 2012 年 Facebook 首次将 GraphQL 应用于移动应用,到 GraphQL 规范于 2015 年实现开源。可如今现状是 GraphQL 不温不火,时不时又有新的文章介绍,不知道的还以为是什么新技术。 + +:::tip 目标 + +本文将上手使用 GraphQL,并用 Nestjs 与 Strapi 这两个 Node 框架搭建 GraphQL 服务。 + +::: + + + +关于 GraphQL 介绍,详见官网 [GraphQL | A query language for your API](https://graphql.cn/ 'GraphQL | A query language for your API') 或相关介绍视频 [GraphQL 速览:React/Vue 的最佳搭档](https://www.bilibili.com/video/BV1fM4y1A7U1/ 'GraphQL 速览:React/Vue 的最佳搭档') + +## GraphQL 与 Restful API 相比 + +![](https://img.kuizuo.cn/9a7412200a062646b729c8419be28b35.jpeg) + +### Restful API + +Restful 架构的设计范式侧重于分配 HTTP 请求方法(GET、POST、PUT、PA TCH、DELETE)和 URL 端点之间的关系。如下图 + +![](https://img.kuizuo.cn/17fc41e2de8d829dc2d41e31a0775df3.png) + +但是实际复杂的业务中,单靠 Restful 接口,需要发送多条请求,例如获取博客中某篇博文数据与作者数据 + +```http +GET /blog/1 + +GET /blog/1/author +``` + +要么单独另写一个接口,如`getBlogAndAuthor`,这样直接为调用方“定制”一个接口,请求一条就得到就调用方想要的数据。但是另写一个`getBlogAndAuthor` 就破坏了 Restful API 接口风格,并且在复杂的业务中,比如说还要获取博文的评论等等,后端就要额外提供一个接口,可以说非常繁琐了。 + +有没有这样一个功能,将这些接口做一下聚合,然后**将结果的集合返回给前端**呢?在目前比较流行微服务架构体系下,有一个专门的中间层专门来处理这个事情,这个中间层叫 BFF(Backend For Frontend)。可以参阅 [BFF——服务于前端的后端](https://blog.csdn.net/qianduan666a/article/details/107271974 'BFF——服务于前端的后端') + +![](https://img.kuizuo.cn/image_Y4u9tNpZwR.png) + +但这些接口一般来说都比较重,里面有很多当前页面并不需要的字段,那还有没有一种请求:**客户端只需要发送一次请求就能获取所需要的字段** + +有,也就是接下来要说的 GraphQL + +### GraphQL + +![](https://img.kuizuo.cn/8a141ec5fa73781d66fb2e1b60f9b49d.jpg) + +REST API 构建在请求方法(method)和端点(endpoint)之间的连接上,而 GraphQL API 被设计为只通过一个端点,即 `/graphql`,始终使用 POST 请求进行查询,其集中的 API 如 http://localhost:3000/graphql,所有的操作都通过这个接口来执行,这会在后面的操作中在展示到。 + +:::info + +但是想要一条请求就能得到客户端想要的数据字段,那么服务端必然要做比较多的任务 😟(想想也是,后端啥都不干,前端就啥都能获取,怎么可能嘛)。 + +而服务端要做的就是搭建一个 GraphQL 服务,这在后面也会操作到,也算是本文的重点。 + +::: + +接下来便会在客户端中体验下 GraphQL,看看 GraphQL 究竟有多好用。 + +## **在线体验 GraphQL** + +可以到 [官网](https://graphql.cn/learn/ '官网') 中简单尝试入门一下,在 [Studio](https://studio.apollographql.com/sandbox/explorer 'Studio (apollographql.com)') 可在线体验 GraphQL,也可以到 [SWAPI GraphQL API]( 'SWAPI GraphQL API (swapi-graphql.netlify.app)') 中体验。 + +下面以 `apollographql` 为例,并查询 People 对象。 + +### query + +查询所有 People 并且只获取 `name`、`gender`、`height` 字段 + +![](https://img.kuizuo.cn/image_kvWUNtlUbf.png) + +查询 personID 为 1 的 Person 并且只获取 `name`,`gender`,`height` 字段 + +![](https://img.kuizuo.cn/image_Msg9xwWFrl.png) + +查询 personID 为 2 的 Person 并且只获取 `name`,`eyeColor`、`skinColor`、`hairColor` 字段 + +![](https://img.kuizuo.cn/image_hX0l36Acme.png) + +从上面查询案例中其实就可以发现,我只需要在 person 中写上想要获取的字段,GraphQL 便会返回带有该字段的数据。避免了返回结果中不必要的数据字段。 + +```graphql +{ + person{ + # 写上想获取的字段 + } +} +``` + +如果你不想要 person 数据或者想要其他其他的数据,不用像 Restful API 那样请求多条接口,依旧请求`/graphql`,如 + +![](https://img.kuizuo.cn/image_Z0b6ya-auG.png) + +:::info + +**无论你想要什么数据,一次请求便可满足。** + +::: + +### mutation + +GraphQL 的大部分讨论集中在数据获取(也是它的强项),但是任何完整的数据平台也都需要一个改变服务端数据的方法。即 CRUD。 + +GraphQL 提供了 [变更(Mutations)](https://graphql.cn/learn/queries/#mutations '变更(Mutations)') 用于改变服务端数据,不过 `apollographql` 在线示例中并没有如 `createPeople` 字段支持 。这个片段在线体验中就无法体验到,后在后文中展示到。这里你只需要知道 GraphQL 能够执行基本的 CRUD 即可。 + +### fragmen 和 subscribtion + +此外还有 `fragment ` 与 `subscription` 就不做介绍。 + +### 小结 + +尝试完上面这些操作后,可以非常明显的感受到 GraphQL 的优势与便利,本来是需要请求不同的 url,现在只需要请求 `/graphql`,对调用方(前端)来说非常友好,香是真的香。 + +可目前只是使用了别人配置好的 GraphQL 服务,让前端开发用了特别友好的 API。但是,对于后端开发而言,想要提供 GraphQL 服务可就不那么友善了。因为它不像传统的 restful 请求,需要专门配置 GraphQL 服务,而整个过程是需要花费一定的工作量(定义 Schema,Mutations 等等),前面也提到想要一条请求就能得到客户端想要的数据字段,那服务端必然需要额外的工作量。 + +不仅需要在后端中配置 GraphQL 服务,用于接收 GraphQL 查询并验证和执行,此外前端通常需要 GraphQL 客户端,来方便使用 GraphQL 获取数据,目前实用比较多的是[Apollo Graph](https://www.apollographql.com/platform/ 'Apollo Graph'),不过本文侧重搭建 GraphQL 服务,因此前端暂不演示如何使用 GraphQL。 + +你可能听过一句话是,**graphq​l 大部分时间在折磨后端**,并且要求比较严格的数据字段,但是好处都是前端。把工作量基本都丢给了后端,所以在遇到使用这门技术的公司,尤其是后端岗位就需要考虑有没有加班的可能了。 + +以下便会开始实际搭建 GraphQL 服务,这里会用 Nest.js 与 Strapi 分别实践演示。 + +## Nest.js + +官方文档:[GraphQL + TypeScript | NestJS](https://docs.nestjs.com/graphql/quick-start 'GraphQL + TypeScript | NestJS') + +模块:[nestjs/graphql](https://github.com/nestjs/graphql 'nestjs/graphql') + +仓库本文实例代码仓库: [kuizuo/nest-graphql-demo](https://github.com/kuizuo/nest-graphql-demo 'kuizuo/nest-graphql-demo') + +**创建项目** + +```bash +nest new nest-graphql-demo +``` + +**安装依赖** + +```bash +npm i @nestjs/graphql @nestjs/apollo graphql apollo-server-express +``` + +**修改 app.module.ts** + +```typescript title='app.module.ts' icon='logos:nestjs' +import { Module } from '@nestjs/common' +import { GraphQLModule } from '@nestjs/graphql' +import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo' + +@Module({ + imports: [ + GraphQLModule.forRoot({ + driver: ApolloDriver, + autoSchemaFile: true, + }), + ], +}) +export class AppModule {} +``` + +### resolver + +设置了`autoSchemaFile: true` ,nest.js 将会自动搜索整个项目所有以 `.resolver.ts` 为后缀的文件,将其解析为 `schema.gql` 比如说创建`app.resolver.ts` + +```typescript title='app.resolver.ts' icon='logos:nestjs' +import { Resolver, Query } from '@nestjs/graphql' + +@Resolver() +export class AppResolver { + @Query(() => String) // 定义一个查询,并且返回字符类型 + hello() { + return 'hello world' + } +} +``` + +在 `graphql` 中 `resolver` 叫解析器,与 `service` 类似(也需要在 `@Module` 中通过 `providers` 导入)。`resolver`主要包括`query`(查询数据)、`mutation`(增、删、改数据)、`subscription`(订阅,有点类型 `socket`),在 `graphql` 项目中我们用 `resolver` 替换了之前的控制器。 + +这时候打开[http://127.0.0.1:3000/graphql](http://127.0.0.1:3000/graphql 'http://127.0.0.1:3000/graphql'),可以在右侧中看到自动生成的 Schema,这个 Schema 非常关键,决定了你客户端能够请求到什么数据。 + +尝试输入 GraphQL 的 query 查询(可以按 Ctrl + i 触发代码建议(Trigger Suggest),与 vscode 同理) + +![](https://img.kuizuo.cn/image_a3yl4oVtSU.png) + +此时点击执行,可以得到右侧结果,即`app.resolver.ts` 中 `hello` 函数所定义的返回体。 + +![](https://img.kuizuo.cn/image_bK9bvZ3QMm.png) + +### [Code first](https://docs.nestjs.com/graphql/quick-start#code-first) 与 [Schema first](https://docs.nestjs.com/graphql/quick-start#schema-first) + +在 nestjs 中有 [Code first](https://docs.nestjs.com/graphql/quick-start#code-first) 与 [Schema first](https://docs.nestjs.com/graphql/quick-start#schema-first) 两种方式来生成上面的 Schema,从名字上来看,前者是优先定义代码会自动生成 Schema,而后者是传统方式先定义 Schema。 + +在上面一开始的例子中是 Code First 方式,通常使用该方式即可,无需关心 Schema 是如何生成的。下文也会以 Code First 方式来编写 GraphQL 服务。 + +也可到官方示例仓库中 [nest/sample/31-graphql-federation-code-first](https://github.com/nestjs/nest/tree/master/sample/31-graphql-federation-code-first) 和 [nest/sample/32-graphql-federation-schema-first](https://github.com/nestjs/nest/tree/master/sample/32-graphql-federation-schema-first) 查看两者代码上的区别。 + +### 快速生成 GraphQL 模块 + +nest 提供 cli 的方式来快速生成 GraphQL 模块 + +```typescript +nest g resource +``` + +![](https://img.kuizuo.cn/image_L9yYAn78Dw.png) + +比如创建一个 blog 模块 + +```bash +nest g resource blog --no-spec +? What transport layer do you use? GraphQL (code first) +? Would you like to generate CRUD entry points? Yes +CREATE src/blog/blog.module.ts (217 bytes) +CREATE src/blog/blog.resolver.ts (1098 bytes) +CREATE src/blog/blog.resolver.spec.ts (515 bytes) +CREATE src/blog/blog.service.ts (623 bytes) +CREATE src/blog/blog.service.spec.ts (446 bytes) +CREATE src/blog/dto/create-blog.input.ts (196 bytes) +CREATE src/blog/dto/update-blog.input.ts (243 bytes) +CREATE src/blog/entities/blog.entity.ts (187 bytes) +UPDATE src/app.module.ts (643 bytes) +``` + +便会生成如下文件 + +![](https://img.kuizuo.cn/image_XemqTcfz_D.png) + +```typescript title='blog.resolver.ts' icon='logos:nestjs' +import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql' +import { BlogService } from './blog.service' +import { Blog } from './entities/blog.entity' +import { CreateBlogInput } from './dto/create-blog.input' +import { UpdateBlogInput } from './dto/update-blog.input' + +@Resolver(() => Blog) +export class BlogResolver { + constructor(private readonly blogService: BlogService) {} + + @Mutation(() => Blog) + createBlog(@Args('createBlogInput') createBlogInput: CreateBlogInput) { + return this.blogService.create(createBlogInput) + } + + @Query(() => [Blog], { name: 'blogs' }) + findAll() { + return this.blogService.findAll() + } + + @Query(() => Blog, { name: 'blog' }) + findOne(@Args('id', { type: () => Int }) id: number) { + return this.blogService.findOne(id) + } + + @Mutation(() => Blog) + updateBlog(@Args('updateBlogInput') updateBlogInput: UpdateBlogInput) { + return this.blogService.update(updateBlogInput.id, updateBlogInput) + } + + @Mutation(() => Blog) + removeBlog(@Args('id', { type: () => Int }) id: number) { + return this.blogService.remove(id) + } +} +``` + +此时 Schema 如下 + +![](https://img.kuizuo.cn/image_sJCQpllOXK.png) + +不过`nest cli`创建的`blog.service.ts` 只是示例代码,并没有实际业务的代码。 + +此外`blog.entity.ts`也不为数据库实体类,因此这里引入`typeorm`,并使用`sqlite3` + +### 集成 Typeorm + +安装依赖 + +```bash +pnpm install @nestjs/typeorm typeorm sqlite3 +``` + +```typescript title='app.module.ts' icon='logos:nestjs' +import { Module } from '@nestjs/common' +import { AppController } from './app.controller' +import { AppService } from './app.service' +import { GraphQLModule } from '@nestjs/graphql' +import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo' +import { AppResolver } from './app.resolver' +import { BlogModule } from './blog/blog.module' +import { TypeOrmModule } from '@nestjs/typeorm' + +@Module({ + imports: [ + TypeOrmModule.forRoot({ + type: 'sqlite', + database: 'db.sqlite3', + entities: [__dirname + '/**/*.entity{.ts,.js}'], + synchronize: true, + }), + GraphQLModule.forRoot({ + driver: ApolloDriver, + autoSchemaFile: true, + playground: true, + }), + AppModule, + BlogModule, + ], + controllers: [AppController], + providers: [AppService, AppResolver], +}) +export class AppModule {} +``` + +将 `blog.entity.ts` 改成实体类,代码为 + +```typescript title='blog.entity.ts' icon='logos:nestjs' +import { ObjectType, Field } from '@nestjs/graphql' +import { Column, Entity, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm' + +@ObjectType() +@Entity() +export class Blog { + @Field(() => Int) + @PrimaryGeneratedColumn() + id: number + + @Field() + @Column() + title: string + + @Field() + @Column({ type: 'text' }) + content: string + + @Field() + @CreateDateColumn({ name: 'created_at', comment: '创建时间' }) + createdAt: Date + + @Field() + @UpdateDateColumn({ name: 'updated_at', comment: '更新时间' }) + updatedAt: Date +} +``` + +其中 `@ObjectType()` 装饰器让 `@nestjs/graphql` 自动让其视为一个 `type Blog` + +而 `@Field()` 则是作为可展示的字段,比如 `password` 字段无需返回,就不必要加该装饰器。 + +:::tip + +如果你认为 添加 `@Field()` 是件繁琐的事情(nest 官方自然也想到),于是提供了 [GraphQL + TypeScript - CLI Plugin ](https://docs.nestjs.com/graphql/cli-plugin) 用于省略 `@Field()` 等其他操作。(类似于语法糖) + +借用官方的话: + +> Thus, you won't have to struggle with @Field decorators scattered throughout the code. + +因此,您不必为分散在代码中的 `@Field` 装饰符而烦恼。 + +::: + +`@nestjs/graphql` 会将 typescript 的 number 类型视为 Float,所以需要转成 Int 类型,即 `@Field(() => Int)` + +在 BlogService 编写 CRUD 数据库业务代码,并在 dto 编写参数效验代码,这里简单暂时部分代码。 + +```typescript title='blog.service.ts' icon='logos:nestjs' +import { Injectable } from '@nestjs/common' +import { InjectRepository } from '@nestjs/typeorm' +import { Repository } from 'typeorm' +import { CreateBlogInput } from './dto/create-blog.input' +import { UpdateBlogInput } from './dto/update-blog.input' +import { Blog } from './entities/blog.entity' + +@Injectable() +export class BlogService { + constructor( + @InjectRepository(Blog) + private blogRepository: Repository, + ) {} + + create(createBlogInput: CreateBlogInput) { + return this.blogRepository.save(createBlogInput) + } + + findAll() { + return this.blogRepository.find() + } + + findOne(id: number) { + return this.blogRepository.findOneBy({ id }) + } + + async update(id: number, updateBlogInput: UpdateBlogInput) { + const blog = await this.blogRepository.findOneBy({ id }) + const item = { ...blog, ...updateBlogInput } + return this.blogRepository.save(item) + } + + remove(id: number) { + return this.blogRepository.delete(id) + } +} +``` + +```typescript title='create-blog.input.ts' icon='logos:nestjs' +import { InputType, Field } from '@nestjs/graphql' + +@InputType() +export class CreateBlogInput { + @Field() + title: string + + @Field() + content: string +} +``` + +此时 + +![](https://img.kuizuo.cn/image_7-twN56Aym.png) + +### CRUD + +下面将演示 graphql 的 Mutation。 + +#### 新增 + +![](https://img.kuizuo.cn/image_NPqShDN3Pl.png) + +#### 修改 + +![](https://img.kuizuo.cn/image_c4ycwRs-po.png) + +#### 删除 + +![](https://img.kuizuo.cn/image_xpkHhpS1-K.png) + +Query 就不在演示。 + +### 小结 + +至此,在 Nest.js 中配置 GraphQL 服务的就演示到此,从这里来看,Nest.js 配置 GraphQL 服务还算比较轻松,但是做了比较多的工作量,创建 resolver,创建 modal(或在已有实体添加装饰器),不过本文案例中只演示了基本的 CRUD 操作,实际业务中还需要涉及鉴权,限流等等。 + +## Strapi + +Strapi 官方提供 [GraphQL 插件](https://market.strapi.io/plugins/@strapi-plugin-graphql 'GraphQL插件') 免去了配置的繁琐。更具体的配置参见 [GraphQL - Strapi Developer Documentation](https://docs.strapi.io/developer-docs/latest/development/plugins/graphql.html 'GraphQL - Strapi Developer Documentation') + +这里我就选用 [kuizuo/vitesse-nuxt-strapi](https://github.com/kuizuo/vitesse-nuxt-strapi 'kuizuo/vitesse-nuxt-strapi') 作为演示,并为其提供 graphQL 支持。 + +strapi 安装 + +```bash +npm install @strapi/plugin-graphql +``` + +接着启动 strapi 项目,并在浏览器打开 graphql 控制台 [http://localhost:1337/graphql](http://localhost:1337/graphql 'http://localhost:1337/graphql'),以下将演示几个应用场景。 + +### 例子 + +#### 查询所有 todo + +![](https://img.kuizuo.cn/image_4GFUs8CmQJ.png) + +#### 查询 id 为 2 的 todo + +![](https://img.kuizuo.cn/image_NMM4e3L_y8.png) + +#### 查询 id 为 2 的 todo 并只返回 value 属性 + +![](https://img.kuizuo.cn/image_E1eWrzjaEs.png) + +#### 新增 todo + +![](https://img.kuizuo.cn/image_pclR7Zb6TE.png) + +#### 更新 todo + +![](https://img.kuizuo.cn/image_g3RJL7RQWR.png) + +#### 删除 todo + +![](https://img.kuizuo.cn/image_m7s17q2TG0.png) + +由于 [Nuxt Strapi](https://strapi.nuxtjs.org/ 'Nuxt Strapi') 提供 [useStrapiGraphQL](https://strapi.nuxtjs.org/usage#usestrapigraphql 'useStrapiGraphQL') 可以非常方便是在客户端调用 GraphQL 服务。 + +```vue + +``` + +### 小结 + +对于 Strapi 来说,搭建 GraphQL 服务基本没有配置的负担,安装一个插件,即可配合 Strapi 的 content-type 来提供 GraphQL 服务。 + +## 总结 + +**GraphQL** 翻译过来为 **图表 Query Language**,我所理解的理念是通过 json 数据格式的方式去写 SQL,而且有种前端人员在写 sql 语句。在我看来 GraphQL 更多是业务数据特别复制的情况下使用,往往能够事半功倍。但对于本文中示例的代码而言,GraphQL 反倒有点过于先进了。 + +如今看来,GraphQL 还处于不温不火的状态,目前更多的站点主流还是使用 Restful API 架构。我不过我猜测,主要还是大多数业务没有 API 架构的升级的需求,原有的 Restful API 虽说不够优雅,但是也能够满足业务的需求,反而 GraphQL 是一个新项目 API 架构的选择,但不是一个必须的选择。 + +至于如何选择,可以参阅官方 [GraphQL 最佳实践](https://graphql.cn/learn/best-practices/),至于说有没有必要学 GraphQL,这篇文章 [都快 2022 年了 GraphQL 还值得学吗](https://blog.csdn.net/kevin_tech/article/details/120735500) 能给你答案。我的建议是了解即可,新项目可以考虑使用,就别想着用 GraphQL 来重构原有的 API 接口,工作量将会十分巨大,并且还可能是费力不讨好的事。反正我认为这门技术不像 Git 这种属于必学的技能,我的五星评分是 ⭐⭐ + +但多了解一门技术,就是工作面试的资本。回想我为何尝试 GraphQL,就是因为我无意间看到了一份 ts 全栈的远程面试招聘,在这份招聘单中写到 【会 graphql 编写是加分项】。所以抱着这样的态度去尝试了一番,说不准未来就是因为 graphql 让我拿到该 offer。当然也是因为很早之前就听闻 GraphQL,想亲手目睹下是否有所谓的那么神奇。 diff --git "a/blog/program/Nest grpc \345\256\236\350\267\265\344\271\213\350\260\203\347\224\250 python ddddocr \345\272\223.md" "b/blog/program/Nest grpc \345\256\236\350\267\265\344\271\213\350\260\203\347\224\250 python ddddocr \345\272\223.md" new file mode 100644 index 0000000..e3a645d --- /dev/null +++ "b/blog/program/Nest grpc \345\256\236\350\267\265\344\271\213\350\260\203\347\224\250 python ddddocr \345\272\223.md" @@ -0,0 +1,325 @@ +--- +slug: nest-grpc-ocr +title: Nest grpc 实践之调用 python ddddocr 库 +date: 2023-07-29 +authors: kuizuo +tags: [nest, grpc, python, ddddocr] +keywords: [nest, grpc, python, ddddocr] +description: 本文将使用 nest 通过 grpc 的方式来调用 python 的 ddddocr 库来识别验证码。 +--- + +我曾经写过一个项目 [ddddocr_server](https://github.com/kuizuo/ddddocr_server),使用 fastapi 提供 http 接口,以此来调用 [ddddocr](https://github.com/sml2h3/ddddocr) 库。 + +其他语言想要调用的话,则是通过 http 协议的方式来调用。然而 http 协议的开销不小,而 Websocket 调用又不灵活,此时针对这种应用场景的最佳选择就是 rpc(Remote Procedure Call 远程过程调用),而这次所要用的技术便是 grpc。 + +早闻 [gRPC](https://grpc.io/) 大名,所以这次将使用 nest 通过 grpc 的方式来调用 python 的 ddddocr 库来识别验证码。 + + + +## 效果图 + +![Untitled](https://img.kuizuo.cn/202307290823586.png) + +本文源码 [nest-ocr](https://github.com/kuizuo/nest-ocr) + +## 简单熟悉下 grpc + +由于我们的调用方是 nest,因此就很有必要熟悉一下 nest 要如何创建 + +官方提供了一个 [样例](https://github.com/nestjs/nest/tree/master/sample/04-grpc),本文便在此基础上进行更改。 + +首先,在 nest 中 grpc 是以微服务的方式启动的,从代码上也就 3 行便可实现。 + +```typescript title='main.ts' icon='logos:nestjs' +const app = await NestFactory.create(AppModule) + +app.connectMicroservice < + MicroserviceOptions > + { + transport: Transport.GRPC, + options: { + package: 'hero', + protoPath: join(__dirname, './hero/hero.proto'), + }, + } + +await app.startAllMicroservices() +``` + +既然服务有了,那么要如何调用呢?或者说有没有像 http 接口调试工具能够调用 grpc 服务,有很多种 grpc 客户端工具,但这里选择 Postman。 + +![Untitled](https://img.kuizuo.cn/202307290823587.png) + +### 创建 API + +不过这里先别急着调用,为了后续调试,建议先到工作区的 APIs 中添加一个 API,然后将样例中的 hero.proto 中导入进来 + +![Untitled](https://img.kuizuo.cn/202307290823588.png) + +导入完毕后将显示如下页面 + +![Untitled](https://img.kuizuo.cn/202307290823589.png) + +### 创建 gRPC 客户端 + +点击工作区旁边的 New 按钮(不是 + 按钮),选择 gRPC + +![Untitled](https://img.kuizuo.cn/202307290823590.png) + +在 Enter URL 输入框填写 [localhost:5000](http://localhost:5000) (nest grpc 默认地址),这里你也可以选择第一个官方的 gRPC 测试服务,用于看看效果。 + +![Untitled](https://img.kuizuo.cn/202307290823591.png) + +填写完毕后,你会发现在右侧 Select a method 中并没有看到所定义的两个方法:FindOne,FindMang,这时候我们需要将 hero.proto 文件导入进来,如果你完成了 创建 API 那一步骤,你在右侧便能看到那两个方法 + +![Untitled](https://img.kuizuo.cn/202307290823592.png) + +此时不妨选择一下 FindOne,然后点击下方 Use Example Message,将 id 填为 1,点击 Invoke,得到的效果图如下。 + +![Untitled](https://img.kuizuo.cn/202307290823593.png) + +到这里我们就已经搞定了如何调用 grpc 服务,接下来就要自己去实现标题的需求。 + +## Protobuf 消息编码 + +**在 grpc 中,数据传输部分通过 Protobuf(Protocol Buffers)定义** + +因为从上面服务调用来看,貌似与 http 协议调用不相上下。 + +其实不然,protobuf 不同于 JSON、XML 数据,是以二进制数据流传输,数据在经 protobuf 序列化后的消息体积很小(传输内容少,传输相对就快)。同时在加上 HTTP/2 协议的加持(底层传输协议,可替换为其他协议),使得 gRPC 的传输性能要优于传统 Restful。 + +protobuf 对于数据传输的优点有很多,如 **支持流式传输,不过这就不是本文所述的内容了。总之你只要知道 grpc 性能高的原因就是因为 protobuf。** + +```protobuf title='hero.proto' icon='vscode-icons:file-type-protobuf' +syntax = "proto3"; + +package hero; + +service HeroService { + rpc FindOne (HeroById) returns (Hero); + rpc FindMany (stream HeroById) returns (stream Hero); +} + +message HeroById { + int32 id = 1; +} + +message Hero { + int32 id = 1; + string name = 2; +} +``` + +不难看出,package 定义包名,service 定义服务,而 message 则是定义数据传输的类型。 + +客户端与服务端将根据 protobuf 来生成双方交互方式,其中包名决定了双方传输的作用域,service 下的函数就是双方之间的预先定义好要以什么样的数据发送,又以什么样的数据返回。 + +我个人是觉得没什么特别重点的部分,根据自己的需求然后修改基本数据结构便可。 + +## 实践 + +首先,要**明确谁是客户端,谁是服务端。** + +从 标题 上来看,不难看出是 js(client) ⇒ python(server),也就是 nest 调用 ddddocr 这个库,那么 nest 就应该作为客户端,而 python 作为服务端。 + +先将整个流程先捋一遍,如图下图示意。 + +![Untitled](https://img.kuizuo.cn/202307290823594.png) + +用户想要调用 ddddocr 库,最理想的肯定是让用户直接和 python 打交道,但应用(这里指 Web)通常不会使用 python 进行编写,而其他语言(js)想要跨语言调用,这时 rpc 就再适合不过了。 + +可能会有人说这么操作多此一举,我只能说根据性能和业务为主。相比将 nest 后端服务迁移到 python 上,和在 nest 与 python 之间多层 grpc,在两者的工作量之下我肯定毫不疑问的选择后者。 + +### protobuf 定义 + +```protobuf title='ocr.proto' icon='vscode-icons:file-type-protobuf' +syntax = "proto3"; + +package ocr; + +service OCR { + rpc Character (CharacterBody) returns (CharacterReply) {} + + // TODO: Add other type, e.g. select, slide, etc. +} + +message CharacterBody { + bytes image = 1; +} + +message CharacterReply { + string result = 1; + int32 consumedTime = 2; +} +``` + +这部分没什么特别好说的,就图片数据以字节数组的方式传递。 + +### nest 部分 + +由于 nest 作为客户端,事实上示例部分的很多代码都无关了,就比如 main.ts 中用于启动 gRPC 服务的代码,都可以注释掉,因为在这里我们并不打算将 nest 作为服务端。 + +```typescript title='main.ts' icon='logos:nestjs' +// app.connectMicroservice(grpcClientOptions); +// await app.startAllMicroservices(); +``` + +最核心的代码,就是定义 client, 如下 + +```typescript +@Client({ + transport: Transport.GRPC, + options: { + package: ['ocr'], + protoPath: join(__dirname, './ocr.proto'), + url: 'localhost:50051', // 这里所定义的是 grpc 服务端地址 + }, +}) +client: ClientGrpc +``` + +> 这一部分也可以通过构造函数的方式注入,因人而异。 `constructor(@Inject('OCR_PACKAGE') private readonly client: ClientGrpc) {}` + +有了这个 client 就能够获取 ocrService 了,完整 ocr.controller.ts 代码如下 + +```typescript title='ocr.controller.ts' icon='logos:nestjs' +import { Body, Controller, OnModuleInit, Post } from '@nestjs/common' +import { Client, ClientGrpc } from '@nestjs/microservices' +import { Observable } from 'rxjs' +import { Character } from './interfaces/character.interface' +import { Reply } from './interfaces/reply.interface' +import { grpcClientOptions } from 'src/grpc-client.options' +import { CharacterDto } from './dtos/character.dto' + +interface OCRService { + Character(image: Character): Observable + + // TODO: Add other type, e.g. select, slide, etc. +} + +@Controller('ocr') +export class OcrController implements OnModuleInit { + private ocrService: OCRService + + @Client(grpcClientOptions) + client: ClientGrpc + + onModuleInit() { + this.ocrService = this.client.getService('OCR') + } + + @Post('character') + character(@Body() dto: CharacterDto): Observable { + // 这里多一步 Base64 将文本解码成图片的操作 + // 主要是根据接口易用性而定,最佳的做法肯定是类似上传文件,直接得到图片二进制数据,省去数据操作步骤 + const buffer = Buffer.from(dto.image, 'base64') + + return this.ocrService.Character({ image: buffer }) + } + + // TODO: Add other type, e.g. select, slide, etc. +} +``` + +而在之前 http 的方式实现的话,这里 `this.ocrService.Character({ image: dto.image });` 所对应的就是例如 `fetch(’http://localhost:3002/ocr/character’)` ,这里 3002 端口对应的是 python 的 http 服务。 + +### python 部分 + +服务端部分其实还稍微有些复杂,可能是因为我太久没写 python 的缘故。 + +在之前是通过 python 来启动一个 http 服务来供其他语言调用,现在有了 gRPC 就完全没必要启动 http 服务。 + +可以在 [这里](https://grpc.io/docs/languages/python/quickstart/#download-the-example) 下载官方的 python 示例。 + +先安装 grpc_tools + +```bash +python3 -m pip install grpcio-tools +``` + +接着执行下方指令 + +```bash +python3 -m grpc_tools.protoc -I . --python_out=. --grpc_python_out=. ocr.proto +``` + +它将会在下方根据 `ocr.proto` 生成 `ocr_pb2.py` 与 `ocr_pb2_grpc.py` 两个文件,事实上这两个文件都无需改动,你只需要每次修改 .proto 文件后再重新执行上方代码将新的内容复写到文件上便可。 + +不过要搞清流程,还要是在意这些文件便可。其中在 `ocr_pb2_grpc.py` 文件中,你会找到 OCRServicer 类的接口定义。 + +```python title='ocr_pb2_grpc.py' icon='logos:python' +class OCRServicer(object): + """Missing associated documentation comment in .proto file.""" + + def Character(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') +``` + +很显然这是一个接口类,因此我们需要实现它。 + +而 ocr_pb2.py 内容就不必细看,但后续也需要用到,主要通过 `ocr_pb2.CharacterReply` 将数据封装返回给客户端。 + +最终完整的 [server.py](http://server.py) 内容如下 + +```python title='server.py' icon='logos:python' +from concurrent import futures +import time + +import grpc +import ocr_pb2 +import ocr_pb2_grpc + +import ddddocr + +ocr = ddddocr.DdddOcr(beta=True) + +class OCRServicer(ocr_pb2_grpc.OCRServicer): + + # 这里实现 英数验证码 识别 + def Character(self, request, context): + + t = time.perf_counter() + + result = ocr.classification(request.image) + consumed_time = int((time.perf_counter() - t)*1000) + + print({'result': result, 'consumedTime': consumed_time}) + + # 根据 ocr.proto 的 message CharacterReply 生成的类 + response = ocr_pb2.CharacterReply( + result=result, consumedTime=consumed_time) + return response + +def serve(): + port = '50051' + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + ocr_pb2_grpc.add_OCRServicer_to_server(OCRServicer(), server) + server.add_insecure_port('[::]:' + port) + server.start() + print("Server started, listening on " + port) + server.wait_for_termination() + +if __name__ == '__main__': + serve() +``` + +此时整个代码的核心流程就已经搞通了,你可以到 [nest-ocr](https://github.com/kuizuo/nest-ocr) 查看源码,先看看用 postman grpc 方式调用,这里 image 为 字节数组(图片的二进制数据) + +![Untitled](https://img.kuizuo.cn/202307290823596.png) + +用户以 http 方式访问的效果。 + +![Untitled](https://img.kuizuo.cn/202307290823595.png) + +## 结语 + +时间因素,因此本文最终代码都仅实现 **英数字符识别**,ddddocr 还支持点选、滑块,如有时间再补充相关代码。 + +从 http 方式转到 gRPC 无非就是围绕 protobuf 展开,预先定义好 protobuf,然后在此基础上去编写 grpc 客户端(调用方)与服务端(提供方) 的代码。虽然引入了一丝复杂性,但可以有效提高性能。 + +有时候,为了优化性能,又不想增加硬件开销,我们不得不在代码层面做出一些改进,更换高性能框架便是其中之一。然而事实上,提高性能最快捷的方式就是升级硬件。并发数不足,增加服务器数量是最直接有效的办法。 + +为了偏薄的性能提升,开发者总能想出诸多的解决方案。 diff --git "a/blog/program/Next.js\351\241\271\347\233\256\346\220\255\345\273\272\344\270\216\351\203\250\347\275\262.md" "b/blog/program/Next.js\351\241\271\347\233\256\346\220\255\345\273\272\344\270\216\351\203\250\347\275\262.md" new file mode 100644 index 0000000..26063be --- /dev/null +++ "b/blog/program/Next.js\351\241\271\347\233\256\346\220\255\345\273\272\344\270\216\351\203\250\347\275\262.md" @@ -0,0 +1,325 @@ +--- +slug: next.js-build-and-deploy +title: Next.js项目搭建与部署 +date: 2022-07-13 +authors: kuizuo +tags: [next, react, ssr, vercel] +keywords: [next, react, ssr, vercel] +draft: false +--- + + + +官方文档 [Getting Started | Next.js (nextjs.org)](https://nextjs.org/docs/getting-started) + +## [安装](https://nextjs.org/docs/getting-started#automatic-setup) + +```bash +npx create-next-app@latest --ts +# or +yarn create next-app --typescript +# or +pnpm create next-app --ts +``` + +运行 + +``` +npm run dev +``` + +访问 http://localhost:3000 + +## 项目结构 + +![image-20220712030637300](https://img.kuizuo.cn/image-20220712030637300.png) + +| 文件 | 内容 | +| -------------- | -------------------- | +| pages | 页面文件 | +| pages/api | api 数据接口 | +| public | 静态资源文件 | +| styles | 样式文件 | +| next-env.d.ts | 确保 typescript 支持 | +| next.config.ts | next 配置文件 | + +## 路由 + +nextjs 有一个基于页面概念的文件系统路由器,存放在 pages 下`.js`, `.jsx`, `.ts`, `.tsx` 文件都将作为组件,即**文件路径 → 页面路由**,例如这里的 index.tsx 映射为 index,`pages/about.js` 将映射为 `/about`。 + +同时还支持动态路由,创建`pages/user/[id].tsx`文件,然后访问`user/1`,`user/2` + +```tsx title="[id].tsx" +import { useRouter } from 'next/router' + +const User = () => { + const router = useRouter() + const { id } = router.query + + return
User id:{id}
+} + +export default User +``` + +此时访问 http://localhost:3000/user/1 便可得到 `User ID: 1` + +在 router 对象下没有 param 属性,都是存放在 query 参数中,例如访问 user/1?username=kuizuo,此时的 query 值为 `{username: 'kuizuo', id: '2'}` + +:::tip + +不过这里有个比较有意思的点,如果你在上方代码中使用 console.log 打印 query 的话,在 vscode 中会打印出空对象`{}`,而在浏览器中会打印一次空对象,一次真实的 query 对象(并且打印两遍) + +![image-20220712191356587](https://img.kuizuo.cn/image-20220712191356587.png) + +::: + +## 数据渲染 + +如果你打开控制台,查看所返回的页面,你会发现响应中只有 User id:,这不就和 react 的 CSR(客户端)渲染没有区别吗,是的,确实是这样。因为上一部分的代码,并且从输出 query 也可以看的出来而不是 SSR(服务端)渲染。首先我要展示一下两者渲染的代码 + +### CSR 客户端渲染 + +```tsx title="[id].tsx" +import { useEffect, useState } from 'react' +import { useRouter } from 'next/router' + +const User = () => { + const router = useRouter() + const { id } = router.query + + const [data, setData] = useState({ + username: '', + email: '', + }) + + useEffect(() => { + fetch(`https://jsonplaceholder.typicode.com/users/${id}`) + .then(res => res.json()) + .then(data => { + setData(data) + }) + .catch(err => {}) + }, [id]) + + return ( +
+

username:{data.username}

+

email:{data.email}

+
+ ) +} + +export default User +``` + +经常写 react 的肯定对上面的代码不陌生,前端向后端发送数据请求,接受到数据后赋值给 data,然后渲染出来。因为请求数据是需要耗时的,所以在页面显示完之后,会停顿一会在显示出数据(主要是我这边没写 loadding),并且由于 id 并不是第一时间获取到的(从上面的 id)。 + +![image-20220712193009186](https://img.kuizuo.cn/image-20220712193009186.png) + +从这里来看,客户端渲染不仅要获取页面组件,还要请求数据,最终再通过 js 渲染出来 + +### SSR 服务端渲染 + +next 中服务端渲染需要用到 getServerSideProps 函数,而后端的数据获取都是在该函数内来获取,并通过 prop 传入给前端组件中,来看实际代码 + +```tsx title="[id].tsx" +const User = ({ data }: { data: any }) => { + return ( +
+

username:{data.username}

+

email:{data.email}

+
+ ) +} + +export default User + +export async function getServerSideProps(context: { query: { id: any } }) { + const { id } = context.query // 这里context.param也能获取到id + + const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`) + + const data = await res.json() + + return { + props: { + data, + }, + } +} +``` + +如果从页面显示来看,确实没什么区别,但如果打开控制台就能发现诸多不同。 + +首先就是请求的页面,是直接包含数据,相当于返回你一个页面,而在客户端渲染则是返回一个组件,需要自己去请求数据来展示。 + +![image-20220712192713634](https://img.kuizuo.cn/image-20220712192713634.png) + +同时查看控制台中的 Fetch/XHR 的是看不到请求的数据,因为这些数据并不是由前端发送的,而是由后端发送的(故不受跨域请求的限制)。 + +从这就能看出客户端渲染与服务端渲染的的区别了。 + +### SSG 静态生成 + +不过还没完,还有一个静态生成,先来看看代码。 + +```tsx title="[id].tsx" +const User = ({ data }: { data: any }) => { + return ( +
+

username:{data.username}

+

email:{data.email}

+
+ ) +} + +export default User + +export async function getStaticProps(context: { params: { id: any } }) { + const { id } = context.params + + const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`) + + const data = await res.json() + + return { + props: { + data, + }, + } +} + +export async function getStaticPaths() { + return { + paths: new Array(20).fill(0).map((a, i) => ({ params: { id: String(i + 1) } })), + fallback: 'blocking', + } +} +``` + +主要是 getServerSideProps 替换成 getStaticProps,同时增加了一个 getStaticPaths 用于生成静态页面的,而上面的 getStaticPaths 表示生成 id 1 到 20 的页面,那假设如果我访问 id 为 21 的 user 呢?由于这里设置`fallback: 'blocking'`,所以还是会走服务端渲染的那一部分。但如果设置`fallback: fasle`,访问 user/21 就会提示 404。 + +通俗点来说就就是生成一系列静态页面,不需要服务端处理,所以返回的速度更快,其缺点其实也比较明显,数据的任何更改都需要在服务端重新构建,而服务端渲染则是可以动态处理数据,不需要完全重建。 + +### ISR 增量式静态生成 + +不做过多介绍,详看文档 [Data Fetching: Incremental Static Regeneration | Next.js (nextjs.org)](https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration) + +## api 接口 + +上面的数据都是调用 [JSONPlaceholder](http://jsonplaceholder.typicode.com/) 所提供的虚拟数据,在 next 中要提供数据接口的话,只需要在 pages/api 下编写即可,生成的路由规则和组件一样。例如 pages/api/hello.ts 映射为 api/hello,浏览器访问[http://localhost:3000/api/hello](http://localhost:3000/api/hello) 就可以得到`{"name": "John Doe"}` + +```tsx title="hello.ts" +import type { NextApiRequest, NextApiResponse } from 'next' + +type Data = { + name: string +} + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + res.status(200).json({ name: 'John Doe' }) +} +``` + +这里的 req、res 就是同大部分 node 后端框架一样,而这里的写法与 serverless 一致(这里应该就是 serverless)。 + +上述是 get 请求,那 post 请求呢?无论什么 http 请求方法都将在 handler 处理,通过 req.method 来获取请求方法,要区分的话可以通过如下代码。 + +```tsx +export default function handler(req, res) { + if (req.method === 'POST') { + // Process a POST request + } else { + // Handle any other HTTP method + } +} +``` + +### 写一个简单的 CRUD + +既然知道了上述的一些作用,不妨来个熟悉的 CRUD。这里以文章 post 为例 + +这里数据端使用的时 sqlite,配置不做展示,只展示主要核心功能 + +```typescript title="api/post/index.ts" +import type { NextApiRequest, NextApiResponse } from 'next' +import db from '../../../lib/db' + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + try { + switch (req.method) { + case 'GET': + db.all(`select * from post`, (err, rows) => { + res.status(200).json(rows) + }) + break + case 'POST': + const { title, content } = req.body + + db.get(`insert into post(title, content) values(?, ?)`, [title, content], (err, rows) => { + res.status(200).json(rows) + }) + break + } + } catch (error) { + res.status(500).end() + } +} +``` + +```typescript title="api/post/[id].ts" +import type { NextApiRequest, NextApiResponse } from 'next' +import db from '../../../lib/db' + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + const { id } = req.query + const { title, content } = req.body + + try { + switch (req.method) { + case 'GET': + db.get(`select * from post where id=$id`, { $id: id }, (err, rows) => { + res.status(200).json(rows) + }) + break + case 'Put': + db.get( + `update post set title=?,content=? where id=?`, + [title, content, id], + (err, rows) => { + res.status(200).json(rows) + }, + ) + case 'DELETE': + db.get(`delete from post where id=$id`, { $id: id }, (err, rows) => { + res.status(200).json(rows) + }) + } + } catch (error) { + res.status(500).end() + } +} +``` + +这里为了符合 RESTFUL 风格,所以 post 下编写了两个文件,这时候请求[http://localhost:3000/api/post](http://localhost:3000/api/post/2) 就能获取到所有文章数据,基本的 CRUD 也就实现了。 + +这里写 sql 是真滴繁琐,没使用 str 或是 typeorm 主要是不想把这个 demo 搞得太复杂,实际项目还是用上比较好。 + +当然这里只是作为后端 api 接口的演示,至于前端的展示与编写就和普通前端开发没啥大的区别。基本后端框架能做的,next 能做后端很多事情,更多的使用还是作为接口转发,中间件等,毕竟 Next 主要的强项还是服务端渲染的能力。 + +## 打包部署 + +既然说到部署,那肯定离不开 nextjs 的母公司[Vercel](https://vercel.com)了,关于 Vercel 之前也写过相关文章,关于 Vercel 就不过多介绍。 + +nextjs 部署到 vercel 实在简单,将项目推送到 github 仓库中,然后在 vercel 中 New Project,接着选择 nextjs 的仓库,点击 Deploy,静等部署即可。关于部署可以看这篇文章 [Vercel 部署个人博客](https://kuizuo.cn/develop/Vercel部署个人博客) + +现在你可以通过访问 [kz-next-app-demo.vercel.app](https://kz-next-app-demo.vercel.app/) 来访问该项目,并尝试访问`/api/post`,`user/1`来看看。 + +只能说不愧是母公司。 + +至于其他部署?既然都用 nextjs 了,还考虑自建服务器来部署吗? + +## 总结 + +这次的整体过程比较简单,后续应该会使用 nextjs 编写一个完整的项目(~~也有可能是 nuxt.js~~)。 diff --git "a/blog/program/SpringBoot\351\241\271\347\233\256\347\273\223\346\236\204.md" "b/blog/program/SpringBoot\351\241\271\347\233\256\347\273\223\346\236\204.md" new file mode 100644 index 0000000..0d7ab8d --- /dev/null +++ "b/blog/program/SpringBoot\351\241\271\347\233\256\347\273\223\346\236\204.md" @@ -0,0 +1,267 @@ +--- +slug: springboot-project-structure +title: SpringBoot项目结构 +date: 2022-01-08 +authors: kuizuo +tags: [java, springboot, develop] +keywords: [java, springboot, develop] +--- + + + +演示代码地址:[kuizuo/spring-boot-demo (github.com)](https://github.com/kuizuo/spring-boot-demo) + +## 目录结构展示图 + +![](https://img.kuizuo.cn/20220108011921.png) + +### controller + +controller 目录下对应的也就是控制器,用于接收用户的请求(get,post 等),如下面代码 + +```java title="controller/UserController.java" +@RestController +@RequestMapping("/user") +public class UserController { + + @Resource + private UserService userService; + + @GetMapping("list") + public List list() { + return userService.findAll(); + } +} +``` + +用户请求[http://127.0.0.1:8080/user/list](http://127.0.0.1:8080/users/list) 将会调用 userService.findAll 方法,当然这个方法事先定义好,用于获取所有用户。 + +### model(service) + +这里数据库连接方式以 JPA(一个 ORM 框架)为例,可以安装一个 IDEA 插件 JPA Buddy 新建文件时可以直接创建 Entity(实体)或 Repository(仓库) + +![image-20220506115207717](https://img.kuizuo.cn/image-20220506115207717.png) + +#### entity 类 + +在 domain 目录下创建实体类,大致如下(lombok 因人而异选择使用,相对不展示 get 与 set 会好一些) + +```java title="domain/User.java" +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.*; + +@Entity +@Getter +@Setter +@Table(name = "user") +public class User implements Serializable { + @Id + @GeneratedValue + @ApiModelProperty(value = "ID", hidden = true) + private Long id; + + @Column(nullable = false, unique = true) + private String username; + @Column(nullable = false) + private String password; + @Column(nullable = false) + private String email; +} +``` + +User.java 用于定义 user 实体,在 ORM 中,数据库表中的字段都可以通过实体类中的属性来定义的,如果定义好 user 实体,并且在 resources/application.yml 中设置了`spring.jpa.hibernate.ddl-auto: update` 那么启动项目后,数据库将会自动创建 user 表且其表中字段自动为`@Column`注解的字段。 + +#### repository 类 + +创建完实体后,还需要定义数据接口访问层 DAO,在 JPA 中则是在 repository 目录下创建。 + +```java title="repository/UserRepository.java" +public interface UserRepository extends JpaRepository , JpaSpecificationExecutor { + User findByUsername(String username); +} +``` + +一般情况下该接口无需定义额外方法,如有需要还可以定义属于自己的查询语句,比如上面的 findByUsername,这时候就注入后的 userRepository 对象就可以使用`userRepository.findByUsername("kuizuo");` ,将会返回数据库中该用户名的数据。 + +#### UserService 类 + +```java title="service/UserService.java" +@Service +public class UserService { + @Autowired + private UserRepository userRepository; + + public List findAll(){ + return userRepository.findAll(); + } +} +``` + +**@Autowired 可能不建议使用字段注入**,可以在类添加@RequiredArgsConstructor 注解,表明 userRepository 不为空,总之目的就是将 userRepository 注入,供服务可用。 + +```java title="service/UserService.java" +import com.kuizuo.demo.domain.User; +import com.kuizuo.demo.repository.UserRepository; +import com.kuizuo.demo.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class UserService { + private final UserRepository userRepository; + + @Override + public List findAll() { + return userRepository.findAll(); + } +} +``` + +接着就可以使用 userRepository 下的方法,如 `userRepository.findAll`命令相当于 `select * from user`,返回所有的用户列表。 + +#### service 接口实现 + +此外 service 服务还可以有另一种写法,在 service 中添加一个 impl 目录,通过对 userService **接口**进行实现的服务。在上面所写的 UserService 是一个类,这边将其改为一个接口,代码如下 + +```java title="service/UserService.java" +public interface UserService { + List findAll(); + User findOne(Long id); +} +``` + +同时只保留 UserService 所要提供的方法,然后在 service/impl 中创建文件 UserServiceImpl.java,具体代码如下 + +```java title="service/impl/UserServiceImpl.java" +@Service +@RequiredArgsConstructor +public class UserServiceImpl implements UserService { + private final UserRepository userRepository; + + @Override + public List findAll() { + return userRepository.findAll(); + } + + + @Override + public User findOne(Long id) { + return userRepository.findById(id).orElseThrow(() -> new BadRequestException("用户不存在")); + } +} +``` + +调用并无差异,对 service 进一步的封装,相对更规范些(我看外面都这么写的,所以就这么写了)。 + +#### 数据接口 + +[POJO、PO、DTO、DAO、BO、VO 需要搞清楚的概念](https://developer.aliyun.com/article/694418) 此外还可能对不同层的数据进行命令 + +- 数据实体(entity)类`PO` : + - jpa 项目: domain 目录 + - mybatis 项目: entity 目录 +- 数据接口访问层`DAO`: + - jpa 项目: repository 目录 + - mybatis 项目: mapper 目录 +- 数据传输对象`DTO`:dto 目录 +- 视图对象`VO`:vo 目录 + +其中前两种在上文中 jpa 的例子中已经介绍了,简单介绍下后两者 + +`DTO` 经过处理后的 PO,在传输数据对象中可能增加或者减少 PO 的属性 + +`VO` 在控制层与视图层进行传输交换 + +对于后两者而言,可能还需要提供 Mapper 类用于数据转化,如 DTO 转 PO,PO 转 DTO。 + +##### modelMapper + +```xml + + org.modelmapper + modelmapper + 2.3.5 + +``` + +同时在启动类下配置为一个 Bean 才能被注入使用 + +```java +@SpringBootApplication +public class DemoApplication { + + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } + + @Bean + public ModelMapper modelMapper() { + return new ModelMapper(); + } +} + +``` + +##### po 与 dto 转化 + +还是上面那个 user 实体,但是返回的数据中不需要将 user 的 password 展示出来。在 service/dto 中创建一个 UserDTO + +```java title="service/dto/UserDto.java" +@Getter +@Setter +public class UserDto { + private Long id; + private String username; + private String email; +} +``` + +如果要转化,通常要一个个字段转化,如下 + +```java {5-8} + @Override + public UserDto findOne(Long id) { + User user = userRepository.findById(id).orElseThrow(() -> new BadRequestException("用户不存在")); + + UserDto userDto = new UserDto(); + userDto.setId(user.getId()); + userDto.setUsername(user.getUsername()); + userDto.setEmail(user.getEmail()); + return userDto; + } +``` + +结果肯定是没问题的,但是代码写的很丑陋且不易于维护。就可以使用 modelMapper 来转化(前提已经注入) + +```java {5} + private final ModelMapper modelMapper; + + @Override + public UserDto findOne(Long id) { + User user = userRepository.findById(id).orElseThrow(() -> new BadRequestException("用户不存在")); + + UserDto userDto = modelMapper.map(user, UserDto.class); + return userDto; + } +``` + +不过这样使用可能还是不大规范,同时还需要手动传入对象及其 Class 对象。所以可能还会创建 service/mapstruct,然后创建 UserMapper,这里就不举例了。 + +### view + +此外还有个文件 resources/templates/user.html 用于返回页面,不过这些都属于模板语言的内容,就不细说了(针对前后端分离的项目而言,后端主要提供数据便可) + +### 整体流程 + +大致的流程便可总结为 Controller 接收请求 → 调用 service 服务 → 调用数据接口服务 dao 提供数据 → 将数据(页面)返回给用户 + +**此外,该目录结构仅仅本人所选用的 springboot 项目结构,实际情况还需额外考虑。** + +## 总结 + +回到开头,其中提供业务服务(数据)的也就是 service 所做的事情,控制接口的则是 controller,还有一个视图层 view 介绍的比较少(反正就是返回数据或页面)。其中最为复杂的也就是 service 所提供的服务,相对 controller 和 view 而言会繁琐许多。 diff --git "a/blog/program/Strapi \345\256\236\347\216\260\347\224\250\346\210\267\346\263\250\345\206\214\344\270\216\347\231\273\345\275\225.md" "b/blog/program/Strapi \345\256\236\347\216\260\347\224\250\346\210\267\346\263\250\345\206\214\344\270\216\347\231\273\345\275\225.md" new file mode 100644 index 0000000..f833c91 --- /dev/null +++ "b/blog/program/Strapi \345\256\236\347\216\260\347\224\250\346\210\267\346\263\250\345\206\214\344\270\216\347\231\273\345\275\225.md" @@ -0,0 +1,150 @@ +--- +slug: strapi-user-register-and-login +title: Strapi 实现用户注册与登录 +date: 2022-09-03 +authors: kuizuo +tags: [strapi, nuxt, next] +keywords: [strapi, nuxt, next] +description: Strapi 实现用户注册与登录 +image: https://img.kuizuo.cn/202312270300876.png +--- + +在官方博客 [Registration and Login (Authentication) with Vue.js and Strapi](https://strapi.io/blog/registration-and-login-authentication-with-vue-js-and-strapi-1) 中演示如何实现注册与登录。实际重点部分是 Strapi 的[角色和权限插件](https://docs.strapi.io/developer-docs/latest/plugins/users-permissions.html),可以说这个插件让开发者不用再为项目考虑的用户登录注册与鉴权相关。 + +此外这里有个在线示例可供体验:[Vitesse Nuxt 3 Strapi](https://vitesse-nuxt3-strapi.vercel.app) + + + +## 创建 Strapi 项目 + +这里省略创建 strapi 项目创建过程,具体可到 [Quick Start Guide](https://docs.strapi.io/developer-docs/latest/getting-started/quick-start.html) 中查看。创建完项目,并注册管理员账号后,打开管理面板,根据自己需求创建数据。下面会介绍下管理面板的一些操作(以下针对中文面板) + +### 角色列表 + +打开 **设置 => 用户及权限插件 => 角色列表** + +![image-20220825131929320](https://img.kuizuo.cn/image-20220825131929320.png) + +默认有两个角色 Authenticated 与 Pubilc,都不可删除,其中还有一个 Admin 是我自己创建的角色,用于分配管理员的权限。 + +Authenticated 对应的也就是登录后的角色,即携带 **Authorization** 协议头携带 jwt 的用户。 + +另一个 Pubilc 则是未授权用户,默认权限如下 + +![image-20220825132235027](https://img.kuizuo.cn/image-20220825132235027.png) + +### 权限分配 + +双击角色可以到权限分配页面,比方说我想给 Authenticated 角色分配 Restaurant 表中查询数据,就可以按照如下选项中勾选,并且勾选其中一个权限(增删改查)可以在右侧看到对应的请求 api 接口(路由) + +![image-20220825132716257](https://img.kuizuo.cn/image-20220825132716257.png) + +### 默认角色 + +可以在 **设置 => 用户及权限插件 => 高级设置** 中分配默认角色,此外这里还可以配置注册,重置密码等操作。对于这些功能而言,传统开发就需要编写相当多的代码了,而 Strapi 的 [角色和权限](https://docs.strapi.io/developer-docs/latest/plugins/users-permissions.html) 插件能省去开发这一部分功能的时间。 + +![image-20220825132948740](https://img.kuizuo.cn/image-20220825132948740.png) + +### 管理员权限 + +在 **设置 => 管理员权限** 也可以看到角色列表与用户列表,不过这个只针对登录 strapi 仪表盘的用户,与实际业务的用户毫不相干。通俗点说就是数据库系统的用户与后台管理系统用户的区别。 + +一开始登录面板创建的用户在 **设置 => 管理员权限 => 用户列表** 中可以看到,而通过api http://localhost:1337/api/auth/local/register 注册的用户则是在 **内容管理 => User** 中查看。 + +## 使用 HTTP 请求用户操作(通用) + +这里先给出官方提供的注册和登录地址,分别是: + +[http://localhost:1337/api/auth/local/register](http://localhost:1337/api/auth/local/register) + +[http://localhost:1337/api/auth/local](http://localhost:1337/api/auth/local) + +分别可在 [Login](https://docs.strapi.io/developer-docs/latest/plugins/users-permissions.html#login) 与 [Register](https://docs.strapi.io/developer-docs/latest/plugins/users-permissions.html#registration) 中查看官方演示例子,例如 + +import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; + +```mdx-code-block + + +``` + +```js {4} +import axios from 'axios' + +// Request API. +axios + .post('http://localhost:1337/api/auth/local', { + identifier: 'user@strapi.io', + password: 'strapiPassword', + }) + .then(response => { + // Handle success. + console.log('Well done!') + console.log('User profile', response.data.user) + console.log('User token', response.data.jwt) + }) + .catch(error => { + // Handle error. + console.log('An error occurred:', error.response) + }) +``` + +```mdx-code-block + + +``` + +```js {4} +import axios from 'axios' + +// Request API. +axios + .post('http://localhost:1337/api/auth/local/register', { + username: 'Strapi user', + email: 'user@strapi.io', + password: 'strapiPassword', + }) + .then(response => { + // Handle success. + console.log('Well done!') + console.log('User profile', response.data.user) + console.log('User token', response.data.jwt) + }) + .catch(error => { + // Handle error. + console.log('An error occurred:', error.response) + }) +``` + +```mdx-code-block + + +``` + +除了登录外,还有几个api可能还会用到如获取个人信息,重置密码,修改密码,发送邮箱验证等等。更多可到 [Roles & Permissions](https://docs.strapi.io/developer-docs/latest/plugins/users-permissions.html#authentication) 中查看 + +通过 HTTP 这种方案可以说是最通用的了,不过有些框架还提供相应的模块来调用 Strapi。 + +## Nuxt + +官方 Nuxt3 提供了 hooks 方案使用 Strapi。具体可看 [Nuxt Strapi Module](https://strapi.nuxtjs.org/)。Nuxt2 可看[这里](https://strapi-v0.nuxtjs.org/hooks) + +通过相应的 hooks 就可以实现登录注册以及数据增删改查的功能,演示例子可看 [Usage](https://strapi.nuxtjs.org/usage) + +这里有一份我创建的预设模板 [kuizuo/vitesse-nuxt3-strapi](https://github.com/kuizuo/vitesse-nuxt3-strapi),一开始的示例也是基于这个模板来搭建的。不过目前 Strapi 对 TypeScript 支持不是那么友好,尤其在 window 下会出现无法运行的情况,详看这个 [pr](https://github.com/strapi/strapi/pull/14088)。所以目前 backend 使用 js 创建,然后增加 ts 相关支持的,所以有些 ts 支持可能不是那么友好。 + +:::note + +原本我考虑的是使用 starter 方式来创建nuxt3 strapi项目,但是就在我创建完 starter 与 template 准备使用 `yarn create strapi-starter strapi-nuxt3 https://github.com/kuizuo/strapi-starter-nuxt3` 下载模板时,不出意外又出意外的报错了,由于这个报错也不好排查就暂时放弃了。 + +总之又是一趟白折腾的经过。 + +::: + +## Next + +Next 我暂未找到相关库可以像 Nuxt 提供 Strapi 的服务。不过 Strapi 官方有提供 [sdk](https://github.com/strapi/strapi-sdk-javascript)的方案来调用 strapi 服务,而不用发送 http 请求的形式来调用,具体可以到官方提供的 [sdk](https://github.com/strapi/strapi-sdk-javascript) 查看如何使用,这里不做演示。有如下两个SDK可供选择: + +[strapi/strapi-sdk-javascript](https://github.com/strapi/strapi-sdk-javascript) 官网 + +[Strapi SDK (strapi-sdk-js.netlify.app)](https://strapi-sdk-js.netlify.app/) 社区 diff --git "a/blog/program/Typescript \345\205\250\346\240\210\346\234\200\345\200\274\345\276\227\345\255\246\344\271\240\347\232\204\346\212\200\346\234\257\346\240\210 TRPC.md" "b/blog/program/Typescript \345\205\250\346\240\210\346\234\200\345\200\274\345\276\227\345\255\246\344\271\240\347\232\204\346\212\200\346\234\257\346\240\210 TRPC.md" new file mode 100644 index 0000000..561ff96 --- /dev/null +++ "b/blog/program/Typescript \345\205\250\346\240\210\346\234\200\345\200\274\345\276\227\345\255\246\344\271\240\347\232\204\346\212\200\346\234\257\346\240\210 TRPC.md" @@ -0,0 +1,576 @@ +--- +slug: typescript-full-stack-technology-trpc +title: Typescript 全栈最值得学习的技术栈 TRPC +date: 2023-03-07 +authors: kuizuo +tags: [trpc, next, prisma, zod, auth.js] +keywords: [trpc, next, prisma, zod, auth.js] +description: 本文介绍了 tRPC 技术以及它与传统 RESTful API 的区别。同时 tRPC 可以帮助人们更快地开发全栈 TypeScript 应用程序,同时无需传统的 API 层,并保证应用程序在快速迭代时的稳定性。 +image: https://img.kuizuo.cn/trpc-banner.png +toc_max_heading_level: 3 +--- + +如果你想成为一个 **Typescript 全栈工程师**,那么你可能需要关注一下 [tRPC](https://trpc.io/) 框架。 + +本文总共会接触到以下主要技术栈。 + +- [Next.js](https://nextjs.org/ 'Next.js') +- [TRPC](https://trpc.io/ 'TRPC') +- [Prisma](https://www.prisma.io/ 'Prisma') +- [Zod](https://github.com/vriad/zod 'Zod') +- [Auth.js](https://authjs.dev/ 'Auth.js') + +不是介绍 tRPC 吗,怎么突然出现这么多技术栈。好吧,主要这些技术栈都与 typescript 相关,并且在 trpc 的示例应用中都或多或少使用到,因此也是有必要了解一下。 + +在线体验地址:[TRPC demo](https://trpc.kuizuo.cn/) + + + +## End-to-end typesafe APIs(端到端类型安全) + +在介绍相关技术前,不妨思考一个问题。 + +> 当进行网络请求和 API 调用时,你是否知道本次请求的参数类型以及返回的响应数据类型?知道了请求的数据类型与响应的数据类型,会为得到的 json 数据定义 type/interface,使其有更好的类型提示?还是会在 any 类型下获取属性,但由于没有类型提示,导致写错个单词,最终提示 Cannot read properties of undefined (reading 'xxx')? + +对于大部分前端应用而言,类型往往常被忽略的,这就导致不知道这个请求的提交参数、响应结果有什么数据字段。举个 axios 发送 post 请求的例子 + +![image-20230308142331808](https://img.kuizuo.cn/image-20230308142331808.png) + +这是一个 post 请求用于实现登录的,但是这个响应数据 data 没有任何具体提示(这里的提示是 vscode 记录用户最近输入的提示),这时候如果一旦对象属性拼写错误,就会导致某个数据没拿到,从而诱发 bug。同理提交的请求体 body 不做约束,万一这个请求还有验证码 code 参数,但是我没写上,那请求就会失败,这是就需要通过调试输出,甚至需要抓包比对原始数据包,其过程可想而知。 + +最主要的是没有类型约束的情况下,非常容易出现访问某个对象属性不存在,js 开发者肯定经常遇到如下错误提示。 + +```typescript +Cannot read properties of undefined (reading 'xxx') +``` + +有太多时候就是因为没有类型,无形间诱发 bug,这也是很多做 api 接口都常常忽视的一点。 + +> 因此我个人所认为的未来 Web 框架形态是要满足的前提就是前后端类型统一,即可以将后端的类型无缝的给前端使用,反之同理。而像 Next、Nuxt 这样的全栈框架便是趋势所向。 + +当然 axios 是可以通过泛型的方式拿到 data 的数据类型提示,就如下图所示。 + +![image-20230308142452678](https://img.kuizuo.cn/image-20230308142452678.png) + +但这样为了更好的类型提示,无形之间又增加了工作量,我需要定义每个接口的 Response 与 Body 类型,就极易造成开发疲惫,不愿维护代码。而本次所要介绍的技术栈 tRPC 就能够帮你省去重复的类型定义的一个 web 全栈框架。 + +## [tRPC](https://github.com/trpc/trpc) + +tRPC 是一个基于 TypeScript 的远程过程调用框架,旨在简化客户端与服务端之间的通信过程,并提供高效的类型安全。它允许您使用类似本地函数调用的方式来调用远程函数,同时自动处理序列化和反序列化、错误处理和通信协议等底层细节。 + +借官方 Feature + +- Automatic type-safety(自动类型安全) +- Snappy DX(敏捷高效的开发者体验) +- Is framework agnostic (不依赖于特定框架) +- Amazing autocompletion(出色的自动补全功能) +- Light bundle size(轻量级打包大小) + +### 什么时候该使用 tRPC + +这个问题非常好,因为我在了解到 tRPC,并参阅了一些基本示例与实践一段时间后发现 trpc 和 http 的应用场景可以说非常相似,完全可以使用 trpc 来替代 http,只不过写法上从 **发送 http 请求 ⇒ 调用本地函数**(这在后面会演示到)。 + +而 trpc 又以类型安全与高效著称,如果你的 Web 应用的程序是基于 typescript,并且需要有高效的性能,那么 tRPC 就是一个很好的选择。 + +tRPC 可以作为 REST/GraphQL 的替代品,如果前端与后端共享代码的 TypeScript monorepo,trpc 则可以无需任何类型转换,也不太会有心智负担。 + +**请记住,tRPC 只有当您在诸如 Next、Nuxt、SvelteKit、SolidStart 等全栈项目中使用 TypeScript 时,tRPC 才会发挥其优势。** + +## tRPC 如何进行接口调用 + + + +一图胜千言,你可以点击 [这里](https://trpc.io/#try-it-out '这里') 在线体验一下 tRPC,并且查看其目录结构,以及调用方式。下面我一步步讲解如何进行接口调用。 + +### 定义服务端 + +这里以 Next.js 的目录结构而定。创建 `server/trpc.ts`,如下代码。分别导出 router, middleware, procedure + +```typescript title='server/trpc.ts' icon='logos:nextjs-icon' +import { initTRPC } from '@trpc/server' + +const t = initTRPC.create() + +export const router = t.router +export const middleware = t.middleware +export const publicProcedure = t.procedure +``` + +创建项目(根)路由文件 `pages/api/trpc/[trpc].ts` + +```typescript title='server/trpc.ts' icon='logos:nextjs-icon' +import * as trpc from '@trpc/server' +import { publicProcedure, router } from './trpc' + +const appRouter = router({ + greeting: publicProcedure.query(() => 'hello tRPC!'), +}) + +export type AppRouter = typeof appRouter +``` + +此时已经定义好了一个路由地址 `api/trpc/[trpc].ts`(这里 endpoint(端点)会在客户端中使用到),以及 `greeting` 函数,服务端的工作就暂且完毕。 + +### 创建客户端 + +创建 `utils/trpc.ts` 文件,代码如下 + +```typescript title='utils/trpc.ts' icon='logos:nextjs-icon' +import { httpBatchLink } from '@trpc/client' +import { createTRPCNext } from '@trpc/next' +import type { AppRouter } from '../pages/api/trpc/[trpc]' + +function getBaseUrl() { + if (typeof window !== 'undefined') { + // In the browser, we return a relative URL + return '' + } + // When rendering on the server, we return an absolute URL + + // reference for vercel.com + if (process.env.VERCEL_URL) { + return `https://${process.env.VERCEL_URL}` + } + + // assume localhost + return `http://localhost:${process.env.PORT ?? 3000}` +} + +export const trpc = createTRPCNext({ + config() { + return { + links: [ + httpBatchLink({ + url: getBaseUrl() + '/api/trpc', + }), + ], + } + }, +}) +``` + +在 `_app.tsx` 包装一下 + +```typescript title='_app.tsx' icon='logos:nextjs-icon' +import type { AppType } from 'next/app' +import { trpc } from '../utils/trpc' + +const MyApp: AppType = ({ Component, pageProps }) => { + return +} + +export default trpc.withTRPC(MyApp) +``` + +有了这个对象后,我们就可以开始尽情调用服务端所定义好了函数了。 + +当你导入 trpc 并输入 `trpc.` 时,将会提示出服务端定义好的 `greeting` 函数,如下图所示。 + +![](https://img.kuizuo.cn/image_YDKc7TixQA.png) + +此时通过 `const result = trpc.greeting.useQuery()` 便可调用 `greeting` 函数,其中 `result.data` 便可拿到 `'hello tRPC!'` 信息。 + +### 这个过程发生了什么? + +> 文档: [useQuery() | tRPC](https://trpc.io/docs/useQuery 'useQuery() | tRPC') + +不妨此时打开控制台面板,看看请求 + +![](https://img.kuizuo.cn/image_WfW8ehqUKz.png) + +![](https://img.kuizuo.cn/image_qicvoGjshx.png) + +不难看出,调用 greeting 函数本质是向 `/api/trpc/greeting` 发送了 http 请求,并且携带参数 batch 和 input,虽然我们暂时还没有传。默认 input 为 {}。 + +要支持传递参数,首先需要在服务端定义传递参数的类型(会有 Zod 对参数效验),这样客户端才有对应的类型提示。然后调用 greeting 函数时,通过通过函数参数的形式来传递请求参数。 + +举例说明,比如说我们将 appRouter 改写成这样,通过 input 参数指定了 `useQuery` 需要传递一个 `name` 为字符串且不为空的对象。 + +```typescript +import z from 'zod' + +const appRouter = router({ + greeting: publicProcedure + .input( + z.object({ + name: z.string().nullish(), + }), + ) + .query(({ input }) => { + return { + text: `hello ${input?.name ?? 'world'}`, + } + }), +}) +``` + +调用 `trpc.greeting.useQuery({ name: 'kuizuo' })` 发送的请求的 query 参数则变为 + +![](https://img.kuizuo.cn/20230307214659.png) + +不仅于此,你如果同时调用了多次 greeting 函数,如 + +```typescript title='pages/index.tsx' +const result1 = trpc.greeting.useQuery({ name: 'kuizuo1' }) +const result2 = trpc.greeting.useQuery({ name: 'kuizuo2' }) +const result3 = trpc.greeting.useQuery({ name: 'kuizuo3' }) +``` + +tRPC 会将这三次函数调用合并成一次 http 请求,并且得到的响应本文也是以多条数据的形式返回 + +![](https://img.kuizuo.cn/image_ufrhaugaIj.png) + +![](https://img.kuizuo.cn/image_cvlDJjhwPl.png) + +分别输出三者 result 也没有任何问题。 + +![](https://img.kuizuo.cn/image_hbL8So_RzB.png) + +这是 tRPC 的一个特性:**请求批处理,将同时发出的请求(调用)可以自动组合成一个请求。** + +#### [useMutation() | tRPC](https://trpc.io/docs/useMutation 'useMutation() | tRPC') + +tRPC 同样也支持 post 请求,例如 + +服务端代码 + +```typescript title='server/trpc.ts' icon='logos:nextjs-icon' +const appRouter = router({ + createUser: publicProcedure.input(z.object({ name: z.string() })).mutation(req => { + const user: User = { + name: req.input.name, + } + + return user + }), +}) +``` + +客户端代码 + +```typescript title='pages/index.tsx' icon='logos:nextjs-icon' +export default function IndexPage() { + const mutation = trpc.createUser.useMutation() + + // ERROR! + // mutation.mutate({ name: 'kuizuo' }); + + const handleCreate = () => { + mutation.mutate({ name: 'kuizuo' }) + } + + return ( +
+ + {mutation.error &&

Something went wrong! {mutation.error.message}

} +
+ ) +} +``` + +:::danger + +这里需要注意 `mutate` 方法无法在外层直接调用,否则将会提示 + +```typescript +Unhandled Runtime Error +Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops. +``` + +主要防止这个组件被其他组件调用,此时自动调用 mutate 函数,导致不可控且循环调用的情况,因此需要通过一个事件(比如点击事件)来触发。 + +::: + +此时请求变为 post 请求,并且携带的参数也以 body 形式传递。 + +![](https://img.kuizuo.cn/image_-qEI8jR1uM.png) + +![](https://img.kuizuo.cn/image_RTdWJn_55p.png) + +通过 useQuery 和 useMutation 就能够用 tRPC 实现最基本的 CRUD。此外还有 useInfiniteQuery 可以用作类似无限下拉查询,类似 [SWR 无限加载](https://swr.bootcss.com/examples/infinite-loading)。useQueries 批量查询,使用 [Subscriptions](https://trpc.io/docs/subscriptions) 进行订阅 WebSocket 等等。 + +tRPC 针对 react 项目的查询主要依赖于 [@tanstack/react-query](https://tanstack.com/query/v4/docs/react/adapters/react-query '@tanstack/react-query'),你也可以到 [tRPC React Query documentation](https://trpc.io/docs/react-query 'tRPC React Query documentation') 查看相关 hook。 + +从上述例子中你就会发现,tRPC 将 http 请求给我们包装成了函数形式调用,即上文所说的,调用服务端接口的形式由 **发送 http 请求 ⇒ 调用本地函数**。 + +### 不足 + +不过也并非没有缺点(个人认为)。 + +首先不如传统的 RESTFUL 来的直观,假设我现在在服务端定义了一个服务,那么我只能通过`@trpc/client` 创建客户端进行调用。虽然也能用 http 的形式,但调用的很不优雅。 + +在我印象中,RPC 框架通常是可以跨语言进行调用的,比如 gRPC 框架,然而**tRPC 目前只能在 Typescript 项目中进行调用**,我倒是希望能向 gRPC 那个方向发展,不过不同语言间的类型安全又是个大麻烦。 + +学习成本与项目成本偏高,tRPC 对整个全栈项目的技术要求比较高,并且限定于 typescript,如果你~~想~~将你的项目从传统的 Restful 迁移到 tRPC 上,无疑是个工程量大,且不讨好的事。 + +## 创建工程 + +这里选用 [Create T3 App](https://create.t3.gg/ 'Create T3 App') 用于创建应用(也可以选择 [trpc/examples-next-prisma-starter](https://github.com/trpc/examples-next-prisma-starter 'trpc/examples-next-prisma-starter')),Create T3 App 集成了诸多有关 TypeScript full-stack 相关的技术栈,其中就包括了本文所要介绍的几个技术栈。 + +![](https://img.kuizuo.cn/image_8BUcBPK8In.png) + +```bash +pnpm create t3-app@latest +``` + +安装过程如下 + +![](https://img.kuizuo.cn/image_ERGzEt2Tq8.png) + +### prisma + +此时安装完先别急着 pnpm run dev 启动项目,首先执行 + +```bash +npx prisma db push +``` + +运行结果如下 + +```bash +Environment variables loaded from .env +Prisma schema loaded from prisma schema.prisma +Datasource "db": SQLite database "db.sqlite" at "file:./db.sqlite" + +SQLite database db.sqlite created at file:./db.sqlite + +Your database is now in sync with your Prisma schema. Done in 81ms +``` + +这会将数据库与 prisma 的 schema 同步,说人话就是将数据库的表与 `schema.prisma` 文件中的 model 对应。 + +
+ +schema.prisma + +```prisma title='prisma/schema.prisma' +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +model Example { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// Necessary for Next auth +model Account { + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? // @db.Text + access_token String? // @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? // @db.Text + session_state String? + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) +} + +model Session { + id String @id @default(cuid()) + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model User { + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + image String? + accounts Account[] + sessions Session[] +} + +model VerificationToken { + identifier String + token String @unique + expires DateTime + + @@unique([identifier, token]) +} + +``` + +
+ +create-t3-app 默认使用的 sqlite 数据库,优点就是你无需安装任何数据库的环境,将会在 prisma 目录下创建 `db.sqlite` 文件来存放数据。但是缺点很明显,性能与部署方面是远不如主流服务级别的数据库。尤其是部署,这在后面会说。 + +将会创建 `Account` `Example` `Session` `User` `Verification Token` 表,这里需要教你一个命令 + +```bash +npx prisma studio +``` + +此时访问 localhost:5555 将会得到一个 prisma 面板,即项目的所有 model 。 + +![](https://img.kuizuo.cn/image_QBXnHdoewh.png) + +关于 prisma 更多命令请参考 [Prisma CLI Command Reference](https://www.prisma.io/docs/reference/api-reference/command-reference 'Prisma CLI Command Reference') + +prisma 在线体验:[Prisma Playground | Learn the Prisma ORM in your browser](https://playground.prisma.io/) + +由于 create-t3-app 已经封装好了[数据库的操作](https://create.t3.gg/en/usage/prisma),并且导出 prisma 对象,所以你只需要配置好环境变量便可。 + +主要代码如下 + +```typescript title='server/db.ts' +import { PrismaClient } from '@prisma/client' + +export const prisma = new PrismaClient() +``` + +#### 类型提示 + +在上面所定义的 model,都会被 prisma client 创建对应的 typescript 类型(在`node_modules/.prisma/index.d.ts`),你就可以直接通过 prisma.modelName 来操作 model,例如 Example(这里就不做注释了) + +```typescript +import { prisma } from '~/server/db' + +prisma.post.findUnique({ where: { id: 1 } }) + +prisma.post.create({ data: {} }) + +prisma.post.update(id, { data: {} }) + +prisma.post.delete(id) + +prisma.post.count() +``` + +#### 数据迁移 + +我之前如果做数据库备份的话,我通常会在数据库管理软件(Navicat)将整个数据库转储为 SQL 文件,然后要用的时候在运行该 SQL 文件。而这样做呢虽然方便,但是数据都比较死,而且版本多了 sql 文件也多,导入繁琐。 + +此时就可以使用 [Migrate](https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch/relational-databases/using-prisma-migrate-typescript-postgres),通过命令的方式自动为我们生成当前版本下的 sql 文件,而需要用到的也通过命令的形式运行 sql 文件。 + +#### 数据生成 + +你可以编写一个 [seed 脚本](https://www.prisma.io/docs/guides/database/seed-database#example-seed-scripts),用于插种(生成)自定义数据。 + +--- + +prisma 不是本文重点,篇幅略少,但是作为 Typeorm 的长期使用者而言,我认为 prisma 会比 typeorm 友善一些,至少从文档上来说 prisma 大胜一筹,而且很多 node 的 web 框架都优先 prisma 作为 orm 框架(除了 nest.js),但不过这两个仓库的 issues 数量有点惨不忍睹。。。 + +### next-auth + +我想先简单介绍一下 next-auth(背后由[Auth.js](https://authjs.dev/ 'Auth.js') 提供)。 + +从名字来看也不难猜出,这是一个 next.js 的 auth 库。该库提供了多种身份验证策略,如基于密码的身份验证,OAuth 等等。并且你只需要简单的几行代码,提供好相关信息便可启用身份验证和授权功能。 + +你可以到这个网站 [NextAuth.js Example](https://next-auth-example.vercel.app/ 'NextAuth.js Example')体验一番。下面是一些代码演示 + +由于 create-t3-app 默认是 Discord OAuth,因此我这边替换成使用者更多的 Github。(至于如何创建 Github OAuth Apps,在我之前的文章以及外面诸多文章中都有介绍到,这里不在演示了,附上配置图) + +![](https://img.kuizuo.cn/image__B1RYeiFze.png) + +首先在 + +server/auth.ts 中 导入 + +```typescript title='server/auth.ts' icon='logos:nextjs-icon' +import CredentialsProvider from 'next-auth/providers/credentials' +import GithubProvider from 'next-auth/providers/github' +``` + +并在 options 中设置好 providers,如下 + +```typescript title='server/auth.ts' icon='logos:nextjs-icon' +export const authOptions: NextAuthOptions = { + callbacks: { + session({ session, user }) { + if (session.user) { + session.user.id = user.id + // session.user.role = user.role; <-- put other properties on the session here + } + return session + }, + }, + adapter: PrismaAdapter(prisma), + providers: [ + CredentialsProvider({ + name: 'Credentials', + credentials: { + username: { label: 'Username', type: 'text', placeholder: 'kuizuo' }, + password: { label: 'Password', type: 'password' }, + }, + async authorize(credentials, req) { + // Add logic here to look up the user from the credentials supplied + const user = { id: '1', name: 'kuizuo', email: 'hi@kuizuo.cn' } + + if (user) { + return user + } else { + return null + } + }, + }), + GithubProvider({ + clientId: env.GITHUB_CLIENT_ID, + clientSecret: env.GITHUB_CLIENT_SECRET, + }), + ], +} +``` + +不过此时会提示 env 对象没有 GITHUB_CLIENT_ID 属性,需要在 env.mjs 定义好 GITHUB_CLIENT_ID 与 GITHUB_CLIENT_SECRET。类型安全嘛,你可不想 GITHUB 不小心输成 ~~GAYHUB~~ 导致找不到这个值把。 + +当上述在设置完毕后,点击 Sign in 按钮便可跳转到 next-auth 所提供的简单登录表单。 + +![](https://img.kuizuo.cn/image_9eowvvnwU2.png) + +如果你想自定义修改登录页面,可以参考该视频[Create your own next-auth Login Pages - YouTube](https://www.youtube.com/watch?v=kB6YNYZ63fw 'Create your own next-auth Login Pages - YouTube') + +## 部署 tRPC + +通常来说 tRPC 会配合全栈框架使用,因此可以非常轻松的部署在 Vercel,Netlify 上。如今 Vercel 应该也已经家喻户晓了,因此这里就不演示如何部署,可到 [Vercel • Create T3 App](https://create.t3.gg/en/deployment/vercel 'Vercel • Create T3 App') 中查看相关步骤。 + +:::warning + +不过要注意,Vercel 并不提供文件读写操作,即无法实现数据存储,因此你如果需要提供数据读取的操作,那么普通需要一个远程的数据库服务,将 DATABASE_URL 环境变量替换成线上地址。如 + +```title='env' +DATABASE_URL=postgresql://myuser:mypassword@localhost:5432/mydb +``` + +这里推荐 [railway](https://railway.app/ 'railway') 与 [supabase](https://supabase.com/ 'supabase') 都提供远程数据服务,且有免费额度。(不过我比较好奇为啥好多远程数据服务多数都是 postgresql) + +如果你执意要使用 vercel 部署,当你触发数据库服务时便会报错,以下是相关截图。 + +![](https://img.kuizuo.cn/image_7_XKmbuK87.png) + +::: + +至于说自行部署的话,create t3 app 提供了 docker 相关镜像,你可以直接使用 docker 部署,具体步骤可参考 [Docker • Create T3 App](https://create.t3.gg/en/deployment/docker)。 + +## 示例 + +这里我提供了一个简单的示例,你可以 [点我](https://trpc.kuizuo.cn) 访问体验一下(项目部署在 Vercel,而数据库服务在腾讯云,登录服务又依赖 Github,所以项目会稍微有那么慢)。整个项目结构大致如下 + +![](https://img.kuizuo.cn/image_z_YaR-RnSu.png) + +你可以在 [Example Apps | tRPC](https://trpc.io/docs/example-apps 'Example Apps | tRPC') 查看 trpc 的示例应用。 + +## 结语 + +如果你是用 Next,Nuxt 等这样的全栈框架,并且你的后端服务使用 Typescript 编写,不妨试试 trpc,你会惊喜地发现,它颠覆了传统的 API 交互,使你的 typescript 全栈应用程序的开发变得更加高效和流畅。 + +从 JavaScript 到 TypeScript 的演变,全栈应用的端到端类型安全,TypeScript 目前正在逐渐成为前端开发中不可或缺的一部分,也许未来的某一天当人们说起前端三件套时,不再是 HTML,CSS,JavaScript,而是 HTML,CSS,TypeScript。 + +再说到我为何会去尝试 tRPC,有很大的原因是因为厌倦了传统后端开发,厌倦了 nest.js 开发。然而现实生活中,你所厌倦的,往往是能为你提供收入的。人们总是做着自己不愿做的事,但生活所迫,谁又愿意呢。 diff --git "a/blog/program/Vercel\351\203\250\347\275\262Serverless.md" "b/blog/program/Vercel\351\203\250\347\275\262Serverless.md" new file mode 100644 index 0000000..5368ab6 --- /dev/null +++ "b/blog/program/Vercel\351\203\250\347\275\262Serverless.md" @@ -0,0 +1,191 @@ +--- +slug: vercel-deploy-serverless +title: Vercel部署Serverless +date: 2022-05-12 +authors: kuizuo +tags: [vercel, serverless] +keywords: [vercel, serverless] +description: 使用 Vercel 部署 serverless 过程记录 +--- + +Vercel 除了能部署静态站点外,还能运行 Serverless Functions,也是本次的主题 + + + +## 创建接口 + +> To deploy Serverless Functions without any additional configuration, you can put files with extensions matching [supported languages](https://vercel.com/docs/concepts/functions/supported-languages) and exported functions in the `/api` directory at your project's root. + +vercel 约定在目录下 api 下创建接口路径,这里创建 api/hello.js 文件,当然也支持 ts 以及 ESmodule 写法 + +```javascript title='api/hello.js' +export default function handler(request, response) { + const { name } = request.query + response.status(200).send(`Hello ${name}!`) +} +``` + +此时通过`vc --prod`生产环境部署后,在浏览器请求 vercel 提供的二级域名/api/hello?name=vercel 便可得到文本`Hello vercel`,而其函数写法与 express 类似 + +接口信息可以在 Functions 中查看 + +![image-20220512155341109](https://img.kuizuo.cn/image-20220512155341109.png) + +### 使用 typescript + +不过上面是使用 js 写法,vercel 更推荐[使用 TypeScript](https://vercel.com/docs/concepts/functions/serverless-functions/supported-languages#using-typescript) + +安装 `@vercel/node` + +``` +npm i -D @vercel/node +``` + +将上面的 hello.js 改为 hello.ts,内容为 + +```typescript title='api/hello.ts' +import type { VercelRequest, VercelResponse } from '@vercel/node' + +export default (request: VercelRequest, response: VercelResponse) => { + const { name } = request.query + response.status(200).send(`Hello ${name}!`) +} +``` + +此外还可以使用其他语言,这里为 Vercel 所支持的[语言](https://vercel.com/docs/concepts/functions/serverless-functions/supported-languages#supported-languages:) + +### 开发环境 + +上面创建的例子是在生产环境下进行的,vercel 官方非常贴心的提供了 vercel dev 来用于开发环境(本地调试)。 + +``` +vercel dev +``` + +执行后,将会默认开启 3000 端口来启动服务,此时访问 http://localhost:3000/api/hello 就可调用该接口 + +## vercel.json + +在根目录创建[vercel.json](https://vercel.com/docs/project-configuration),用于设置 Vercel 项目配置 ,其配置结构与 Nextjs 的 next.config.js 大体一致。 + +### headers + +vercel 允许响应携带自定义的协议头,例如设置允许跨域的协议头。 + +```json title='vercel.json' icon='logos:vercel-icon' +{ + "headers": [ + { + "source": "/(.*)", + "headers": [ + { + "key": "Access-Control-Allow-Origin", + "value": "*" + }, + { + "key": "Access-Control-Allow-Headers", + "value": "content-type" + }, + { + "key": "Access-Control-Allow-Methods", + "value": "DELETE,PUT,POST,GET,OPTIONS" + } + ] + } + ] +} +``` + +### rewrites + +Vercel 支持路由重写功能,因此我们可以实现反向代理。 + +例如将前缀为/proxy 的所有请求都代理到 ,其写法如下 + +```json title='vercel.json' icon='logos:vercel-icon' +{ + "rewrites": [ + { + "source": "/proxy/:match*", + "destination": "http://127.0.0.1:5000/:match*" + } + ] +} +``` + +请求`/proxy/hello` 将会请求到 `http://127.0.0.1:5000/hello`(不带有`/proxy`) + +:::warning 注意:无法代理前缀为 `/api` 的接口,即使设置了也无效。 + +::: + +#### redirects 和 rewrites 区别 + +除了 rewrites 还有一个 redirects,也就是重定向,response 返回 3xx 的状态码和 location 头信息。 + +而 rewrites 重写内部转发了请求,地址栏不会发生改变,并且状态码由转发的请求决定。 + +并且 redirects 是先被调用的,而 rewrites 是后被调用的。 + +### functions + +可以设置指定接口分配的内存以及最大执行时间。默认下 + +- Memory: 1024 MB (1 GB) +- Maximum Execution Duration: 5s (Hobby), 15s (Pro), or 30s (Enterprise) + +个人用户接口超时时间最长为 5 秒。 + +## 部署 Node 项目 + +可以使用 vercel.json 配置来覆盖 vercel 默认行为,也就能使用 Vercel 部署 Node 项目。 + +假设要部署一个 Express 项目,则配置如下 + +```json title='vercel.json' icon='logos:vercel-icon' +{ + "builds": [ + { + "src": "app.js", + "use": "@vercel/node" + } + ] +} +``` + +安装 `@vercel/node`包 + +```bash +npm i @vercel/node -D +``` + +然后运行 vercel,而不是~~vercel --prod~~ + +### 部署 Nest.js + +这里有个部署 Nest.js 项目的教程 [基于 Vercel+Github Action 部署 Nest.js 项目 - 掘金 (juejin.cn)](https://juejin.cn/post/7023690214803505166) + +其 vercel.json 如下 + +```json title='vercel.json' icon='logos:vercel-icon' +{ + "builds": [ + { + "src": "dist/main.js", + "use": "@vercel/node" + } + ], + "routes": [ + { + "src": "/(.*)", + "dest": "dist/main.js" + } + ] +} +``` + +然后执行 vercel --prod(因为 nest 项目需要 build 打包) + +## 最后 + +Vercel 十分良心,为个人用户提供了免费的爱好者计划,每个月提供 100G 流量,构建时间是 100 小时,50 个根域名绑定。 diff --git "a/blog/program/Vercel\351\203\250\347\275\262\344\270\252\344\272\272\345\215\232\345\256\242.md" "b/blog/program/Vercel\351\203\250\347\275\262\344\270\252\344\272\272\345\215\232\345\256\242.md" new file mode 100644 index 0000000..2791c44 --- /dev/null +++ "b/blog/program/Vercel\351\203\250\347\275\262\344\270\252\344\272\272\345\215\232\345\256\242.md" @@ -0,0 +1,172 @@ +--- +slug: vercel-deploy-blog +title: Vercel部署个人博客 +date: 2022-05-11 +authors: kuizuo +tags: [vercel, blog] +keywords: [vercel, blog] +description: 使用 Vercel 部署个人博客过程记录,简单方便、访问快、免费部署。 +image: https://img.kuizuo.cn/image-20220511170700075.png +--- + +![image-20220511170700075](https://img.kuizuo.cn/image-20220511170700075.png) + +:::tip 观前提醒 + +[Vercel](https://vercel.com/) 部署静态资源网站极其**简单方便**,并且有可观的**访问速度**,最主要的是**免费部署**。 + +如果你还没有尝试的话,强烈建议去使用一下。 + +::: + +[vercel 介绍](https://zhuanlan.zhihu.com/p/452654619) + +与之相似的产品 [Netfily](https://netlify.com),如果你想部署私有化,推荐 [Coolify](https://coolify.io) + +如果你想搭建一个类似这样的站点,不妨参考我的 [Docusaurus 主题魔改](/docs/docusaurus-guides) + +:::danger DNS 污染 + +由于某些原因,vercel.app 被 DNS 污染(即被墙),目前在国内已经无法打开,除非你有自己的域名,通过 CNAME 解析访问你的域名。 + +**因此想要在国内访问,建议不要使用 Vercel 部署了,最好选用 Netlify。** + +::: + + + +## 注册账号 + +进入 [Vercel](https://vercel.com) 官网,先去注册一个账号,建议注册一个 [Github](https://github.com/) 账号后,使用 Github 账号来登录 Vercel。 + +## 部署网站 + +进入 [Dashboard](https://vercel.com/dashboard) + +![image-20220511170233559](https://img.kuizuo.cn/image-20220511170233559.png) + +点击 [New Project](https://vercel.com/new) + +![image-20220511165902993](https://img.kuizuo.cn/image-20220511165902993.png) + +这里可以从已有的 git repository 中导入,也可以选择一个模板。 + +这里登录我的 Github 账号选择仓库,然后点击 blog 仓库旁的 Import 即可。当然,你也可以直接拉取我的仓库,仓库地址:[kuizuo/blog](https://github.com/kuizuo/blog) + +![image-20220511165513526](https://img.kuizuo.cn/image-20220511165513526.png) + +点击 Deploy,然后静等网站安装依赖以及部署,稍后将会出现下方页面。 + +![image-20220511170700075](https://img.kuizuo.cn/image-20220511170700075.png) + +此时网站已经成功搭建完毕了,点击图片即可跳转到 vercel 所提供的二级域名访问。 + +是不是极其简单?**甚至不需要你输入任何命令,便可访问构建好的网站。** + +## 自定义域名 + +如果有自己的域名,还可以在 vercel 中进行设置。 + +首先进入 blog 的控制台,在 Settings -> Domains 添加域名。 + +![image-20220511171144240](https://img.kuizuo.cn/image-20220511171144240.png) + +接着提示域名需要 DNS 解析到 vercel 提供的记录值 + +![image-20220511171359148](https://img.kuizuo.cn/image-20220511171359148.png) + +登录所在的域名服务商,根据 Vercel 提供的记录值 cname.vercel-dns.com,添加两条记录 + +![image-20220511172741663](https://img.kuizuo.cn/image-20220511172741663.png) + +此时回到 Vercel,可以看到记录值成功生效。 + +![image-20220511172027570](https://img.kuizuo.cn/image-20220511172027570.png) + +此时访问自己的域名,同样也能访问到页面,同时还有可观的访问速度。 + +### 自动颁发 SSL 证书 + +默认状态下,Vercel 将会颁发并自动更新 SSL 证书。(着实方便,不用自己手动去申请证书,配置证书) + +![image-20220511172240999](https://img.kuizuo.cn/image-20220511172240999.png) + +## 持续集成(CI)/持续部署(CD) + +> To update your Production Deployment, push to the "main" branch. + +当主分支有代码被推送,Vercel 将会重新拉取代码,并重新构建进行单元测试与部署(构建速度可观) + +![image-20220511173442694](https://img.kuizuo.cn/image-20220511173442694.png) + +## Serverless + +同时 vercel 还支持 serverless,也就是说,不仅能部署静态站点,还能部署后端服务,不过肯定有一定的限制。 + +[Vercel 部署 Serverless](/blog/vercel-deploy-serverless) + +## Edge Functions + +翻译过来叫边缘函数,你可以理解为在 Vercel 的 CDN 上运行的函数,可以在 Vercel 的 CDN 上运行代码,而不需要在服务器上运行。 + +由于这类函数和静态资源一样,都通过 CDN 分发,因此它们的执行速度非常快。 + +官网介绍:[Edge Functions](https://vercel.com/docs/concepts/functions/edge-functions) + +## Vercel CLI + +有时候并不想登录网页,然后新建项目,选择仓库,拉取部署,而是希望直接在项目下输入命令来完成部署。vercel 自然肯定提供相对应的脚手架 **[CLI](https://vercel.com/docs/cli)** 供开发者使用。 + +安装 + +``` +npm i -g vercel +``` + +在项目根目录中输入 + +``` +vercel --prod +``` + +第一次将进行登录授权,选择对应平台,将会自动打开浏览器完成授权,接着将会确认一些信息,一般默认回车即可,下为执行结果 + +``` +Vercel CLI 24.2.1 +? Set up and deploy “F:\Project\React\online-tools”? [Y/n] y +? Which scope do you want to deploy to? kuizuo +? Link to existing project? [y/N] n +? What’s your project’s name? online-tools +? In which directory is your code located? ./ +Auto-detected Project Settings (Create React App): +- Build Command: react-scripts build +- Output Directory: build +- Development Command: react-scripts start +? Want to override the settings? [y/N] n +🔗 Linked to kuizuo12/online-tools (created .vercel and added it to .gitignore) +🔍 Inspect: https://vercel.com/kuizuo12/online-tools/6t8Vt8rG3waGVHTKU7ZzJuGc6Hoq [2s] +✅ Production: https://online-tools-phi.vercel.app [copied to clipboard] [2m] +📝 Deployed to production. Run `vercel --prod` to overwrite later (https://vercel.link/2F). +💡 To change the domain or build command, go to https://vercel.com/kuizuo12/online-tools/settings +``` + +执行完毕后,将会在根目录创建.vercel 文件夹,其中 project.json 中存放 orgId 和 projectId,下面将会用到。此时在[dashboard](https://vercel.com/dashboard)中也能看到该项目被部署了。 + +不过这样部署上去的代码,并不会连接 git 仓库,需要到控制台中选择仓库即可。 + +如果想在 github actions 中使用,则新建一个 steps,设置好对应的变量。 + +``` + - name: Deploy to Vercel +       run: npx vercel --token ${{VERCEL_TOKEN}} --prod +       env: +           VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} +           VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} +           VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} +``` + +还有一个 VERCEL_TOKEN 需要到 [Vercel Settings Tokens](https://vercel.com/account/tokens) 新建一个 Token。 + +## 总结 + +没什么好总结,直接上手使用,相信你会爱上 Vercel,以及他旗下的产品,[Next.js](https://github.com/vercel/next.js) 和 [Turbo](https://github.com/vercel/turbo) 等等。 diff --git "a/blog/program/vite+vue3\346\220\255\345\273\272uniapp\345\274\200\345\217\221\347\216\257\345\242\203.md" "b/blog/program/vite+vue3\346\220\255\345\273\272uniapp\345\274\200\345\217\221\347\216\257\345\242\203.md" new file mode 100644 index 0000000..9d5c76b --- /dev/null +++ "b/blog/program/vite+vue3\346\220\255\345\273\272uniapp\345\274\200\345\217\221\347\216\257\345\242\203.md" @@ -0,0 +1,196 @@ +--- +slug: vite-vue3-build-uniapp-environment +title: vite+vue3搭建uniapp开发环境 +date: 2022-03-27 +authors: kuizuo +tags: [vue, vite, uniapp, develop] +keywords: [vue, vite, uniapp, develop] +description: 使用 vite vue3 搭建 uniapp 开发环境 +--- + +![uniapp](https://img.kuizuo.cn/uniapp.png) + +最近想搞个移动端或小程序的 Vue3 项目,所以选择跨端开发平台就显得十分重要。在业内主要有两个跨端开发平台,Taro 与 uniapp,但 uniapp 貌似对 vue3 的支持不是特别友好。所以让我在 Taro 和 uniapp 之间抉择了一段时间,最终还是尝试选择相对熟悉的 uniapp 来进行开发。 + +:::warning 前排提醒:目前 uniapp 对 Vue3 的支持还处于 alpha 版,即开发阶段,大概率是会遇到很多问题的。 + +::: + + + +## 开发环境搭建 + +建议安装 HBuilderX,主要是 uni cli 在 APP 平台仅支持生成离线打包的 wgt 资源包,不支持云端打包生成 apk/ipa,并且也不便配置一些打包后的参数。 + +这里建议安装 Alpha 版,后文会说明缘由。 + +:::warning 注意 + +在 HBuilderX 正式版中是无法直接创建 Vue3 项目的,而 Alpha 版有 Vue2 和 3 可供选择,但创建的自带的模板大部分的写法还是 vue2 的写法(无 setup 语法糖),所以这时候要么改代码自建,要么使用官方所提供的 [Vue3 模板](https://uniapp.dcloud.io/worktile/CLI.html#%E5%88%9B%E5%BB%BA%E5%B7%A5%E7%A8%8B) + +![image-20220327000608783](https://img.kuizuo.cn/image-20220327000608783.png) + +::: + +```bash +# 创建以 javascript 开发的工程 +npx degit dcloudio/uni-preset-vue#vite my-vue3-project + +# 创建以 typescript 开发的工程 +npx degit dcloudio/uni-preset-vue#vite-ts my-vue3-project +``` + +当然,有可能会下载失败,可以直接访问 [gitee](https://gitee.com/dcloud/uni-preset-vue/repository/archive/vite-ts.zip)下载模板。 + +## 项目结构 + +``` +|-- src + |-- App.vue + |-- env.d.ts + |-- main.ts + |-- manifest.json + |-- pages.json + |-- uni.scss + |-- pages + | |-- index + | |-- index.vue + |-- static + |-- logo.png +|-- index.html +|-- package-lock.json +|-- package.json +|-- postcss.config.js +|-- tsconfig.json +|-- vite.config.ts +``` + +下载完毕,开始安装依赖,接着就可以开始测试了。 + +## 运行编译 + +在运行之前,首先将**vuex**包给移除,不然将会有如下提示,总之就是不推荐使用的意思,而且要使用状态管理也推荐使用 pinia。所以执行 `yarn remove vuex` 吧 + +``` +(node:26968) [DEP0148] DeprecationWarning: Use of deprecated folder mapping "./" in the "exports" field module resolution of the package at F:\Uniapp\my-vue3-project1\node_modules\vuex\package.json. +Update this package.json to use a subpath pattern like "./*". +``` + +### H5 + +运行编译都正常 + +### APP + +使用`npm run dev:app`后就会发现,终端一直卡在如下界面无法继续。(后面测试发现,除了 H5 能正常运行,其他都会卡住) + +``` +编译器版本:3.4.3(vue3) +请注意运行模式下,因日志输出、sourcemap 以及未压缩源码等原因,性能和包体积,均不及发行模式 +。 +正在编译中... +vite v2.8.6 building for development... +DONE Build complete. Watching for changes... +ready in 1554ms. +``` + +然后呢??? + +算了,就用 HBuilderX 的 cli 先运行到手机或模拟器,然后后打开 app 的时候提示如下错误,点击忽略后发现应用无法正常运行。 + +![image-20220326224649953](https://img.kuizuo.cn/image-20220326224649953.png) + +查看了下我本地的 HBuilderX 版本是正式版 v3.3.13,而该 Vue3 的模板的 Alpha 版 v3.4.3 + +![image-20220326225748608](https://img.kuizuo.cn/image-20220326225748608.png) + +好家伙,官方提供的模板都直接使用 Alpha 版,无奈只好点击 [查看详情](https://ask.dcloud.net.cn/article/35627) 后问题解决办法。最终测试后,建议是使用最新版,即 Alpha 版本,于是替换了本地正式版的 HbuilderX,应用便能正常运行了。 + +既然开发环境下能正常运行,那就试下打包。由于 uniapp 打包安卓应用只能打包成 APP 资源,要打包成 apk,要么创建一个 Android Studio 工程,然后将 APP 资源放入并打包成 apk,要么使用云打包(而云打包又是只有 HBuilder 才有的功能)。如果本地没有 Android Studio 相关环境,建议还是使用云打包(简单方便),这里就不演示下打包过程了。 + +### 小程序 + +这里只测试了微信小程序,在上面 app 的处理完之后,微信小程序也是正常运行,不过至于与上面 Vue3 模板和 HbuilderX 正式版有无关系我就不得而知了,也懒得重装测试了。不过猜测应该与上面无关,毕竟是与手机的 SDK 有关。 + +## 组件库 + +uniapp 官方中提供了一个 uni-ui 的组件库,但有一个 uniapp 相对知名的组件库 uview,并且相对前者来说更易上手实用,但当我尝试用 HBuilderX 导入时,却出现下方提示。 + +![image-20220327002827115](https://img.kuizuo.cn/image-20220327002827115.png) + +很显然,uview 并不支持 vue3,但在社区中找到了份同时支持 Vue3.0 和 Vue2.0 的[uView](https://ext.dcloud.net.cn/plugin?name=vk-uview-ui),但测试后最终已失败告终。 + +在社区中也搜到了 [ThorUI 组件库](https://ext.dcloud.net.cn/plugin?id=556) 但貌似需要会员收费,果断放弃且没有测试。 + +然后想到 Taro 中还有 nutui,于是我便开始尝试了一下,不出所料,支持 Vue3 组件库,肯定是支持的。演示如下 + +![image-20220327005629618](https://img.kuizuo.cn/image-20220327005629618.png) + +但很遗憾,这里的支持也只是局限于 h5 开发。官方也有声明只能开发 h5 + +> @nutui/nutui@next 基于 Vue3 视觉风格 JD APP 10.0 规范 ,只能开发 h5 @nutui/nutui-taro 基于 Vue3 视觉风格 JD APP 10.0 规范 ,必须基于 taro + vue3 框架 进行开发多端(多端指一套代码 部署多端环境 微信小程序 h5、等第三方小程序) + +而且想要多端开发,也必须基于 taro + vue3 框架,所以在 uniapp 上的 app 与小程序上自然无法运行(已测试) + +所以说一开始在 uniapp 和 taro 中的选择中,为啥不使用 Taro 呢?而且还支持 Vue3(相比 uniapp 而言)? + +最终组件库的选择是 uniapp 官方的 uni-ui。 + +## 使用 VSCode 开发 + +HBuilder 给我代码编写体验并不友好,所以将 uniapp 的项目转 vscode 进行开发,并且使用到 npm 包。 + +首先创建一个 vite+vue3 项目(或者使用一开始介绍的官方提供的 Vue3 模板,主要是有 cli,需要自行在安装),然后将原 src 目录给删除,替换成 uniapp 创建的项目根目录。但还需要做以下操作 + +### 安装 sass + +vite 要支持 sass 只需要安装 sass 的依赖即可 + +```bash +npm install sass +``` + +### 允许 js 文件 + +由于使用了 ts,如果项目中存在 js 文件,将会警告,可以在 tsconfig.json 中添加`"allowJs": true`即可 + +### 组件语法提示 + +```bash +npm i @dcloudio/uni-helper-json @types/uni-app @types/html5plus -D +``` + +但发现对于 uni-ui 组件库的代码提示并不友好,大概率是需要局部引用组件,我这里并未使用[npm 包](https://www.npmjs.com/package/@dcloudio/uni-ui)的方式导入,而是采用官方的 uni_modules,不过组件库的代码提示的问题不是很大,查阅文档即可解决。 + +### 导入代码块 + +[uni-app 代码块(vscode) (github.com)](https://github.com/zhetengbiji/uniapp-snippets-vscode) + +### 找不到模块“./App.vue”或其相应的类型声明 + +在 src 目录下创建`env.d.ts`文件,填入以下内容即可 + +```typescript +/// + +declare module '*.vue' { + import { DefineComponent } from 'vue' + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types + const component: DefineComponent<{}, {}, any> + export default component +} +``` + +然后就是把一些`#ifndef VUE3`不是 vue3 的代码块,以及部分 js 文件改写成 ts 文件即可。这里把我修改后的模板上传到 github 上,有需要的可自行下载:[kuizuo/vite-vue3-uniapp (github.com)](https://github.com/kuizuo/vite-vue3-uniapp) + +如果不想使用官方的 vue3 模板,这里也有篇文章介绍如何迁移 + +[迁移 HbuilderX 的 uniapp 项目到主流的前端 IDE 开发(支持 VS Code 等编辑器/IDE)](https://zhuanlan.zhihu.com/p/268206071) + +不过最终如果要在 app 或小程序端运行,还是得打开 HBuilder。 + +## 总结 + +整个过程下来,其实还是 uniapp 对 Vue3 支持不够友好,加上生态没能及时更新。并且官方提供的 Vue3 模板也存在一定问题。 + +但最终还是使用 uniapp 来进行开发,一是对 Vue3 足够了解加上使用过 uniapp,二是 Taro 对 Vue3 是支持了,但是又该如何编译成 App 这是我主要需求的,最主要还是不想踩一遍 Taro 的坑了。 diff --git "a/blog/program/\344\275\277\347\224\250 Fresh \346\241\206\346\236\266\346\236\204\345\273\272 Web \345\272\224\347\224\250.md" "b/blog/program/\344\275\277\347\224\250 Fresh \346\241\206\346\236\266\346\236\204\345\273\272 Web \345\272\224\347\224\250.md" new file mode 100644 index 0000000..d9d338e --- /dev/null +++ "b/blog/program/\344\275\277\347\224\250 Fresh \346\241\206\346\236\266\346\236\204\345\273\272 Web \345\272\224\347\224\250.md" @@ -0,0 +1,198 @@ +--- +slug: use-fresh-build-web-applicatioin +title: 🍋 使用 Fresh 框架构建Web 应用 +date: 2023-02-15 +authors: kuizuo +tags: [deno, fresh, web, project] +keywords: [deno, fresh, web, project] +description: 使用 Fresh 框架构建Web 应用,用于将链接转换为卡片样式的预览效果图。 +image: https://img.kuizuo.cn/link-maker.png +--- + +这篇文章将使用 deno 的 web 框架 Fresh,一个简单的 Web 应用 [Link Maker](https://link-maker.deno.dev/ 'Link Maker'),一个用于将链接转换成卡片样式的预览效果。 + +这个项目也放在了 fresh 的 [Showcase](https://fresh.deno.dev/showcase 'Showcase'),感兴趣的可以查看一番。 + + + +## 什么是 fresh? + +[fresh](https://fresh.deno.dev/) 自称是下一代 web 开发框架(这句话怎么这么熟悉?),是一个基于 Deno 的 Web 框架。它提供了许多用于构建 Web 应用程序和 API 的工具和功能。Fresh 框架特别强调简单性和灵活性,并着重于提供最佳的性能和开发体验。它支持 TypeScript,并且不需要任何配置或构建步骤。这些特性使得 Fresh 框架成为构建高效和现代 Web 应用程序的理想选择。 + +:::warning Fresh 的前端渲染层由 Preact 完成,包括 Islands 架构的实现也是基于 Preact。如果你想在 Fresh 中使用其他主流前端框架,目前来说有点无能为力。 + +::: + +## 创建 fresh 项目 + +[Create a project | fresh docs](https://fresh.deno.dev/docs/getting-started/create-a-project 'Create a project | fresh docs') + +deno 提供了非常友好的创建 fresh 项目的命令,运行: + +```bash +deno run -A -r https://fresh.deno.dev my-project +cd my-project +deno task start +``` + +根据你的喜好进行配置,如下 + +![](https://img.kuizuo.cn/image_jSRfPu966v.png) + +此时会创建如下文件 + +```bash +my-project +├── components # 组件 +│ └── Button.tsx # 按钮组件 +├── deno.json # deno配置文件 +├── dev.ts # +├── fresh.gen.ts # +├── import_map.json # 依赖导入映射 +├── islands # 群岛(组件群岛) +│ └── Counter.tsx +├── main.ts # 入口文件 +├── routes # 路由 +│ ├── [name].tsx +│ ├── api +│ │ └── joke.ts +│ └── index.tsx +├── static # 静态资源 +│ ├── favicon.ico +│ └── logo.svg +└── twind.config.ts # twind配置文件 +``` + +介绍几个文件: + +- **`dev.ts`**: 项目开发模式的匹配文件,假设你需要区分生产环境和开发环境,就可以通过 dev.ts,prod.ts 命令来指明入口 +- **`main.ts`**: 入口文件,会用于链接 [Deno Deploy](https://deno.com/deploy)。 +- **`fresh.gen.ts`**: 这个清单文件会基于`routes/` 和 `islands/` 文件夹自动生成。包含项目的 route 和 island 信息。 +- **`import_map.json`**: 这是用于管理项目的依赖项的导入映射。这允许轻松地导入和更新依赖项。 + +其中最主要的两个目录,这里会细说。 + +### routes + +**`routes/`**: 存放项目中的所有路由。文件即路由,每个文件的名称对应于访问该页的路径。注:此文件夹中的代码永远不会直接发送到客户端. + +其中 routes/api 通常存放一些 api 接口,这这里你完全可以将其当做一个 deno 的服务端,可以做后端能做的事情,通常来说就是提供一个可请求的 api 接口。 + +而其他文件就相当于一个可访问的页面组件,同样是文件路由系统,也可以在这里进行 SSR、中间件操作。 + +### islands + +**`Islands/`**: 群岛,Fresh 中我并未看到对这一词的解释,你可以到 [astro 群岛](https://docs.astro.build/zh-cn/concepts/islands/) 看看新的 Web 架构模式,主要作用就是用于存放交互式组件(服务端组件),可以在客户端和服务端运行。有点类似与 next.js 的服务端组件,同样有两种状态(服务端,浏览器端)。 + +这一部分会有点难理解,你只要知道 IsLands 存放的组件有两种状态(服务端,浏览器端),下文称服务端组件,不同于 components 下的组件,服务端组件有一些优势,例如说 + +- 可以直接访问服务端相关资源 +- 避免了不必要的客户端和服务端之间的交互,因此性能更快 +- 允许一些类库可以直接运行在服务端,因此减小了客户端包文件的大小 + +**想要真正理解服务端组件,就不得不将其与 SSR 拿来对比了。** + +SSR 通常是将数据通过服务端的前端框架渲染成 HTML,直接将 HTML 返回给客户端就可以省去 xhr/fetch 请求的过程,只需要首次请求就能得到数据。此时页面交互,数据更新与传统的前端应用没有任何区别,**通俗点说 SSR 就是省去 xhr/fetch 请求的过程**。 + +而服务端组件会在服务端完成渲染,然后通过自定义的协议发送到客户端。前端应用会将新的 UI 整体(服务端组件)的合并到客户端 UI 树里面(也有叫 hydration 水合),此过程不会对客户端其他状态产生影响,还能达到保持客户端状态的目的,极大的增强了用户体验。 + +如果你仔细查看控制面板的网络请求输出,可以看到服务器端组件是可以请求的。(这里用的后面实战的截图作为展示) + +![](https://img.kuizuo.cn/image_v73eXB47yI.png) + +不过既然服务端组件也有很多限制,就比如说服务端状态下,是无法使用 Web 相关 Api 的,数据传输(通过 props)是有前提的,要 JSON 可序列化,也就是说只能传递基本类型、基本对象、数组,像 Date,自定义类,函数等复制对象是无法传递的。 + +## 实战 + +项目还是相对比较简单的,将链接转化为一个卡片样式的预览效果(包含链接的标题,图片,描述)。 + +核心代码在 [`routes\api\link.ts`](https://github.com/kuizuo/link-maker/blob/main/routes/api/link.ts) 下,将会生成 `/api/link` 接口,例如访问 [https://link-maker.deno.dev/api/link?q=https://kuizuo.cn](https://link-maker.deno.dev/api/link?q=https://kuizuo.cn 'https://link-maker.deno.dev/api/link?q=https://kuizuo.cn') 你就可以得到如下 json 数据 + +```json +{ + "title": "愧怍的小站", + "description": "Blog", + "image": "https://kuizuo.cn/img/logo.png", + "url": "https://kuizuo.cn" +} +``` + +原理就是通过 fetch 请求目标 url,通常来说得到的是一个 html 页面,这时使用 [deno-dom](https://deno.land/x/deno_dom@v0.1.36-alpha/deno-dom-wasm.ts 'deno-dom') 解析成 Dom 对象,通过 css 选择器选取所要的数据,并整合返回给调用方。 + +有了这个接口,剩下的前端工作就相对比较轻松了,主要也就是细节话的问题。 + +## 坑点/不足 + +下面我会说说,在我编写该应用的时候,有哪些开发体验上的不足之处,如果你恰好有使用 Fresh 框架编写 Web 应用的话,最好需要注意下。 + +### vscode 下对 deno 项目重构并不友好 + +当我移动项目 .ts/.tsx 文件的时候,vscode 会将该文件与其他引用该文件的路径更改为 .js/.jsx,这就比较蛋疼了,所以每当要移动文件的时候都要尤为小心。 + +还有就是文件的依赖关系不是那么准确,尤其是在首次进入项目工程的时候,比如说在 routes/test.tsx 中 导入了 `components/Button.tsx` 组件,当你在 tsx 中写了`` ,vscode 并不会有任何的引入提示,当你打开 `components/Button.tsx` 文件后就又有了,搞得我都怀疑是不是没有该组件。 + +### 无法直接通过上下文获取 query 参数 + +fresh 的 handler 提供两个参数,一般来都会写成下面这种形式,可以区分 Get,Post 请求 + +```typescript +export const handler = { + async GET(req: Request, ctx: HandlerContext): Promise {}, + async POST(req: Request, ctx: HandlerContext): Promise {}, +} +``` + +假设当前的请求是 /api/test?q=123,我想要获取 query 参数的 q,我得这么做 + +```typescript +const url = new URL(req.url) +const q = url.searchParams.get('q') +``` + +当时我尝试用 ctx.query 和 req.query 来获取 q 参数,然而 ctx 与 req 并没有 query 属性,在翻阅文档与源码,才得知 fresh 并没有将 query 参数解析到 req 或 ctx 下。 + +至于说为何要用 query 而不是用 param,主要是因为 url 的缘故,比如说 `/api/link/https://kuizuo.cn` 这个链接,这时 param 是解析不出 `https://kuizuo.cn` 完整 url 的,除非 url 编码,但这对使用者来说就不是很好,于是就舍弃了 param 参数的方案。 + +### 有些 npm 包在 fresh 无法正常使用 + +在这个应用中我所使用到了 [html2canvas](https://www.npmjs.com/package/html2canvas 'html2canvas') 库用于将页面的 div 元素转成 canvas,以便转成图片的形式并下载。然后在我导入的时候,要么提示找不到该包(大概率是因为 Commonjs),要么就是 html2canvas 不存在,最终无奈我只好将 html2canvas.min.js 存放在 static 下,并在页面中通过 `` 方式导入,这样全局有了 html2canvas 就可使用。 + +### islands 下的组件要时刻注意 Web Api 调用 + +我在 islands 下的组件中用到了 localStorage 用于持久化数据,然而在我尝试部署到服务器上的时候发现网站无法访问,并在错误日志中提示 localStorage is not defined。 + +其实这在很多 hydration 框架中都有这一个问题,在 islands 下的组件有两种状态(浏览器端,服务端),后文就称为客户端组件和服务端组件。也正是如此,服务端组件是没有客户端的运行时环境,就比如说你想要在组件中使用 localStorage 对象用来持久化数据,在两种状态下,首先会在服务端执行一遍,然而服务端并没有 localStorage 对象,此时就会提示 localStorage is not defined。 + +通常的做法是判断组件当前的状态,可以用如下方式来判断是否为浏览器环境。 + +```typescript +import { IS_BROWSER } from '$fresh/runtime.ts' +``` + +然后将 localStorage 等 Web 相关 API 的调用放在 IS_BROWSER 的判断中。 + +有篇相关文件非常值得阅读,或许对组件的 hydration 有更好的理解 + +[💧 Hydration and Server-side Rendering – somewhat abstract](https://blog.somewhatabstract.com/2020/03/16/hydration-and-server-side-rendering/ '💧 Hydration and Server-side Rendering – somewhat abstract') + +## 前端框架比较局限 + +在前面也说过,Fresh 的前端渲染层由 Preact 完成。如果用户要用 React/Vue 那为何不选择生态更好的 next.js/nuxt.js 呢?所以目前来看,Fresh 还是有些无能为力。但可以肯定的是,fresh 的方向与 next.js/nuxt.js 的一致。 + +## 部署 + +[deno Deploy](https://dash.deno.com/ 'deno Deploy') 可以非常轻松的部署 fresh 应用,使用 Github 账号登录后,[New Project](https://dash.deno.com/new 'New Project'),从 github 仓库中拉取项目点击 Link 即可部署完毕。 + +![](https://img.kuizuo.cn/image_CYOAgv6IGe.png) + +这里的项目名为 link-maker,那么就会生成 专属访问链接 [https://link-maker.deno.dev](https://link-maker.deno.dev/ 'https://link-maker.deno.dev')(也许要梯子才能访问) + +## 结语 + +最后,在我编写完该应用后,我对其做一个评价吧。收回一开始的一句话,~~fresh 自称是下一代 web 开发框架~~。 + +如果要让我在 next.js 和 fresh 两个相似的产品中做个选择的话,我肯定毫不犹豫的选择 next.js。一个以一己之力推动了前端的发展,到至今已有越来越多的项目使用 next.js ,我想作为任何一个前端学习者肯定会毫不犹豫的选择 next.js 去编写 web 应用。 + +就从用户的开发体验而言,就已经很难让我再次选择 fresh,更何况还有像 next.js/nuxt.js 这样的全栈框架。作为一个开发体验(Developer experience)优先的程序员角度来看,如果一个框架想要让别人广泛使用,一定要满足其开发过程,只有沉浸于此,才能不断思考,编写出高质量代码。即便无负担的配置,高性能编译,轻便的部署,这些在他人看来可选择的点(也是 fresh 的点),在我看来却显得很微不足道。 + +而为什么我会选择尝试 fresh,其实也就想看看能不能找到一个令我眼前一亮的一个全栈 Web 框架,然而目前来看,fresh 还有很长一段距离。 diff --git "a/blog/program/\344\275\277\347\224\250Vue\345\274\200\345\217\221Chrome\346\217\222\344\273\266.md" "b/blog/program/\344\275\277\347\224\250Vue\345\274\200\345\217\221Chrome\346\217\222\344\273\266.md" new file mode 100644 index 0000000..647601a --- /dev/null +++ "b/blog/program/\344\275\277\347\224\250Vue\345\274\200\345\217\221Chrome\346\217\222\344\273\266.md" @@ -0,0 +1,593 @@ +--- +slug: vue-chrome-extension +title: 使用Vue开发Chrome插件 +date: 2021-09-18 +authors: kuizuo +tags: [chrome, plugin, vue, develop] +keywords: [chrome, plugin, vue, develop] +description: 使用 Vue2 开发一个 Chrome 插件 +image: /img/blog/vue-chrome-extension.png +--- + +我当时学习开发 Chrome 插件的时候,还不会 Vue,更别说 Webpack 了,所以使用的都是原生的 html 开发,效率就不提了,而这次就准备使用 vue-cli 来进行编写一个某 B 站获取视频信息,评论的功能(原本是打算做自动回复的),顺便巩固下 chrome 开发(快一年没碰脚本类相关技术了),顺便写套模板供自己后续编写 Chrome 插件做铺垫。 + + + +## 环境搭建 + +[Vue Web-Extension - A Web-Extension preset for VueJS (vue-web-extension.netlify.app)](https://vue-web-extension.netlify.app/) + +```bash +npm install -g @vue/cli +npm install -g @vue/cli-init +vue create --preset kocal/vue-web-extension my-extension +cd my-extension +npm run server +``` + +会提供几个选项,如 Eslint,background.js,tab 页,axios,如下图 + +![image-20210916142751129](https://img.kuizuo.cn/image-20210916142751129.png) + +选择完后,将会自动下载依赖,通过 npm run server 将会在根目录生成 dist 文件夹,将该文件拖至 Chrome 插件管理便可安装,由于使用了 webpack,所以更改代码将会热更新,不用反复的编译导入。 + +### 项目结构 + +```tree +├─src +| ├─App.vue +| ├─background.js +| ├─main.js +| ├─manifest.json +| ├─views +| | ├─About.vue +| | └Home.vue +| ├─store +| | └index.js +| ├─standalone +| | ├─App.vue +| | └main.js +| ├─router +| | └index.js +| ├─popup +| | ├─App.vue +| | └main.js +| ├─override +| | ├─App.vue +| | └main.js +| ├─options +| | ├─App.vue +| | └main.js +| ├─devtools +| | ├─App.vue +| | └main.js +| ├─content-scripts +| | └content-script.js +| ├─components +| | └HelloWorld.vue +| ├─assets +| | └logo.png +├─public +├─.browserslistrc +├─.eslintrc.js +├─.gitignore +├─babel.config.js +├─package.json +├─vue.config.js +├─yarn.lock +``` + +根据所选的页面,并在 src 与 vue.config.js 中配置页面信息编译后 dist 目录结构如下 + +``` +├─devtools.html +├─favicon.ico +├─index.html +├─manifest.json +├─options.html +├─override.html +├─popup.html +├─_locales +├─js +├─icons +├─css +``` + +### 安装组件库 + +#### 安装 elementUI + +整体的开发和 vue2 开发基本上没太大的区别,不过既然是用 vue 来开发的话,那肯定少不了组件库了。 + +要导入 Element-ui 也十分简单,`Vue.use(ElementUI); `Vue2 中怎么导入 element,便怎么导入。演示如下 + +![image-20210916150154078](https://img.kuizuo.cn/image-20210916150154078.png) + +不过我没有使用 babel-plugin-component 来按需引入,按需引入一个按钮打包后大约 1.6m,而全量引入则是 5.5 左右。至于为什么不用,因为我需要在 content-scripts.js 中引入 element 组件,如果使用 babel-plugin-component 将无法按需导入组件以及样式(应该是只支持 vue 文件按需引入,总之就是折腾了我一个晚上的时间) + +#### 安装 tailwindcss + +不过官方提供了如何使用 TailwindCSS,这里就演示一下 + +[在 Vue 3 和 Vite 安装 Tailwind CSS - Tailwind CSS 中文文档](https://www.tailwindcss.cn/docs/guides/vue-3-vite) + +推荐安装低版本,最新版有兼容性问题 + +```bash +npm install tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9 +``` + +创建 postcss.config.js 文件 + +```js title="postcss.config.js" +// postcss.config.js +module.exports = { + plugins: [ + // ... + require('tailwindcss'), + require('autoprefixer'), // if you have installed `autoprefixer` + // ... + ], +} +``` + +创建 tailwind.config.js 文件 + +```js title="tailwind.config.js" +// tailwind.config.js +module.exports = { + purge: { + // Specify the paths to all of the template files in your project + content: ['src/**/*.vue'], + + // Whitelist selectors by using regular expression + whitelistPatterns: [ + /-(leave|enter|appear)(|-(to|from|active))$/, // transitions + /data-v-.*/, // scoped css + ], + }, + // ... +} +``` + +在 src/popup/App.vue 中导入样式,或在新建 style.css 在 mian.js 中`import "../style.css";` + +```vue title="src/popup/App.vue" + +``` + +从官方例子导入一个登陆表单,效果如下 + +![image-20210916152633247](https://img.kuizuo.cn/image-20210916152633247.png) + +## 项目搭建 + +### 页面搭建 + +页面搭建就没什么好说的了,因为使用的是 element-ui,所以页面很快就搭建完毕了,效果如图 + +![image-20210918115438700](https://img.kuizuo.cn/image-20210918115438700.png) + +### 悬浮窗 + +悬浮窗其实可有可无,不过之前写 Chrome 插件的时候就写了悬浮窗,所以 vue 版的也顺带写一份。 + +要注意的是悬浮窗是内嵌到网页的(且在 document 加载前载入,也就是`"run_at": "document_start"`),所以需要通过 content-scripts.js 才能操作页面 Dom 元素,首先在配置清单 manifest.json 与 vue.confing.js 中匹配要添加的网站,以及注入的 js 代码,如下 + +```json title="manifest.json" + "content_scripts": [ + { + "matches": ["https://www.bilibili.com/video/*"], + "js": ["js/jquery.js", "js/content-script.js"], + "css": ["css/index.css"], + "run_at": "document_start" + }, + { + "matches": ["https://www.bilibili.com/video/*"], + "js": ["js/jquery.js", "js/bilibili.js"], + "run_at": "document_end" + } + ] +``` + +```js title="vue.config.js" + contentScripts: { + entries: { + 'content-script': ['src/content-scripts/content-script.js'], + bilibili: ['src/content-scripts/bilibili.js'], + }, + }, +``` + +由于是用 Vue,但又要在 js 中生成组件,就使用`document.createElement`来进行创建元素,Vue 组件如下(可拖拽) + +![image-20210917142340863](https://img.kuizuo.cn/image-20210917142340863.png) + +:::warning + +如果使用`babel-plugin-component`按需引入,组件的样式将无法载入,同时自定义组件如果编写了 style 标签,那么也同样无法载入,报错:Cannot read properties of undefined (reading 'appendChild') + +大致就是 css-loader 无法加载对应的 css 代码,如果执意要写 css 的话,直接在 manifest.json 中注入 css 即可 + +::: + +
+ 完整代码 + +```js title="content-script.js" +// 注意,这里引入的vue是运行时的模块,因为content是插入到目标页面,对组件的渲染需要运行时的vue, 而不是编译环境的vue (我也不知道我在说啥,反正大概意思就是这样) +import Vue from 'vue/dist/vue.esm.js' +import ElementUI, { Message } from 'element-ui' +Vue.use(ElementUI) + +// 注意,必须设置了run_at=document_start此段代码才会生效 +document.addEventListener('DOMContentLoaded', function () { + console.log('vue-chrome扩展已载入') + + insertFloat() +}) + +// 在target页面中新建一个带有id的dom元素,将vue对象挂载到这个dom上。 +function insertFloat() { + let element = document.createElement('div') + let attr = document.createAttribute('id') + attr.value = 'appPlugin' + element.setAttributeNode(attr) + document.getElementsByTagName('body')[0].appendChild(element) + + let link = document.createElement('link') + let linkAttr = document.createAttribute('rel') + linkAttr.value = 'stylesheet' + let linkHref = document.createAttribute('href') + linkHref.value = 'https://unpkg.com/element-ui/lib/theme-chalk/index.css' + link.setAttributeNode(linkAttr) + link.setAttributeNode(linkHref) + document.getElementsByTagName('head')[0].appendChild(link) + + let left = 0 + let top = 0 + let mx = 0 + let my = 0 + let onDrag = false + + var drag = { + inserted: function (el) { + ;(el.onmousedown = function (e) { + left = el.offsetLeft + top = el.offsetTop + mx = e.clientX + my = e.clientY + if (my - top > 40) return + + onDrag = true + }), + (window.onmousemove = function (e) { + if (onDrag) { + let nx = e.clientX - mx + left + let ny = e.clientY - my + top + let width = el.clientWidth + let height = el.clientHeight + let bodyWidth = window.document.body.clientWidth + let bodyHeight = window.document.body.clientHeight + + if (nx < 0) nx = 0 + if (ny < 0) ny = 0 + + if (ny > bodyHeight - height && bodyHeight - height > 0) { + ny = bodyHeight - height + } + + if (nx > bodyWidth - width) { + nx = bodyWidth - width + } + + el.style.left = nx + 'px' + el.style.top = ny + 'px' + } + }), + (el.onmouseup = function (e) { + if (onDrag) { + onDrag = false + } + }) + }, + } + + window.kz_vm = new Vue({ + el: '#appPlugin', + directives: { + drag: drag, + }, + template: ` +
+ +
+ 悬浮窗 + {{ show ? '收起' : '展开'}} +
+ +
+ {{user}} +
+
+
+
+ `, + data: function () { + return { + show: true, + list: [], + user: { + username: '', + follow: 0, + title: '', + view: 0, + }, + } + }, + mounted() {}, + methods: { + toggle() { + this.show = !this.show + }, + }, + }) +} +``` + +
+ +因为只能在 js 中编写 vue 组件,所以得用 template 模板,同时使用了 directives,给组件添加了拖拽的功能(尤其是`window.onmousemove`,如果是元素绑定他自身的鼠标移动事件,那么拖拽鼠标将会十分卡顿),还使用了 transition 来进行缓慢动画效果其中注入的 css 代码如下 + +```css +.float-page { + width: 400px; + border-radius: 8px; + position: fixed; + left: 50%; + top: 25%; + z-index: 1000001; +} + +.el-card__header { + padding: 10px 15px !important; +} + +.ul-box { + height: 200px; + overflow: hidden; +} + +.ul-enter-active, +.ul-leave-active { + transition: all 0.5s; +} +.ul-enter, +.ul-leave-to { + height: 0; +} +``` + +相关逻辑可自行观看,这里不在赘述了,并不复杂。 + +也顺带是复习一下 HTML 中鼠标事件和 vue 自定义命令了 + +### 功能实现 + +主要功能 + +- 检测视频页面,输出对应 up 主,关注数以及视频标题播放(参数过多就不一一显示了) + +- 监控关键词根据内容判断是否点赞,例如文本出现了下次一定,那么就点赞。 + +#### 输出相关信息 + +这个其实只要接触过一丢丢爬虫的肯定都会知道如何实现,通过右键审查元素,像这样 + +![image-20210918104907148](https://img.kuizuo.cn/image-20210918104907148.png) + +然后使用 dom 操作,选择对应的元素,输出便可 + +```js +> document.querySelector("#v_upinfo > div.up-info_right > div.name > a.username").innerText +< '老番茄' +``` + +当然使用 JQuery 效果也是一样的。后续我都会使用 JQuery 来进行操作 + +在 src/content-script/bilibili.js 中写下如下代码 + +```js +window.onload = function () { + console.log('加载完毕') + + function getInfo() { + let username = $('#v_upinfo > div.up-info_right > div.name > a.username').text() + let follow = $( + `#v_upinfo > div.up-info_right > div.btn-panel > div.default-btn.follow-btn.btn-transition.b-gz.following > span > span > span`, + ).text() + let title = $(`#viewbox_report > h1 > span`).text() + let view = $('#viewbox_report > div > span.view').attr('title') + + console.log(username, follow, title, view) + } + + getInfo() +} +``` + +重新加载插件,然后输出查看结果 + +``` +加载完毕 +bilibili.js:19 老番茄 1606.0万 顶级画质 总播放数2368406 +``` + +这些数据肯定单纯的输出肯定是没什么作用的,要能显示到内嵌悬浮窗口,或者是 popup 页面上(甚至发送 ajax 请求到远程服务器上保存) + +对上面代码微改一下 + +```js +window.onload = function () { + console.log('加载完毕') + + function getInfo() { + let username = $('#v_upinfo > div.up-info_right > div.name > a.username').text().trim() + let follow = $( + `#v_upinfo > div.up-info_right > div.btn-panel > div.default-btn.follow-btn.btn-transition.b-gz.following > span > span > span`, + ).text() + let title = $(`#viewbox_report > h1 > span`).text() + let view = $('#viewbox_report > div > span.view').attr('title') + + //console.log(username, follow, title, view); + window.kz_vm.user = { + username, + follow, + title, + view, + } + } + getInfo() +} +``` + +其中`window.kz_vm`是通过`window.kz_vm = new Vue()` 初始化的,方便我们操作 vm 对象,就需要通过 jquery 选择元素在添加属性了。如果你想的话也可以直接在 content-script.js 上编写代码,这样就无需使用 window 对象,但这样导致一些业务逻辑都堆在一个文件里,所以我习惯分成 bilibili.js 然后注入方式为 document_end,然后在操作 dom 元素吗,实现效果如下 + +![image-20210918110958104](https://img.kuizuo.cn/image-20210918110958104.png) + +如果像显示到 popup 页面只需要通过页面通信就行了,不过前提得先 popup 打开才行,所以一般都是通过 background 来进行中转,一般来说很少 content –> popup(因为操作 popup 的前提都是 popup 要打开),相对更多的是 content –> background 或 popup –> content + +[content-script 主动发消息给后台 我是小茗同学 - 博客园 (cnblogs.com)](https://www.cnblogs.com/liuxianan/p/chrome-plugin-develop.html#content-script主动发消息给后台) + +#### 实现评论 + +这边简单编写了一下页面,通过 popup 给 content,让 content 输入评论内容,与点击发送,先看效果 + +![bilibili_comment](https://img.kuizuo.cn/bilibili_comment.gif) + +同样的,找到对应元素位置 + +```js +// 评论文本框 +$('#comment > div > div.comment > div > div.comment-send > div.textarea-container > textarea').val( + '要回复的内容', +) +// 评论按钮 +$('#comment > div > div.comment > div > div.comment-send > div.textarea-container > button').click() +``` + +接着就是写页面通信的了,可以看到是 popup 向 content 发送请求 + +```js title="src/content-script/bilibili.js" +window.onload = function () { + console.log('content加载完毕') + + function comment() { + chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { + let { cmd, message } = request + if (cmd === 'addComment') { + $( + '#comment > div > div.comment > div > div.comment-send > div.textarea-container > textarea', + ).val(message) + $( + '#comment > div > div.comment > div > div.comment-send > div.textarea-container > button', + ).click() + } + + sendResponse('我收到了你的消息!') + }) + } + + comment() +} +``` + +```html title="src/popup/App.vue" + + + +``` + +代码就不解读了,调用 sendMessageToContentScript 方法即可。相关源码可自行下载查看 + +实现类似点赞功能也是同理的。 + +## 相关模板 + +[vitesse-webext](https://github.com/antfu/vitesse-webext) + +[plasmo](https://www.plasmo.com/) + +## 整体体验 + +当时写 Chrome 插件的效率不能说慢,反正不快就是了,像一些 tips,都得自行封装。用过 Vue 的都知道写网页很方便,写 Chrome 插件未尝不是编写一个网页,当时的我在接触了 Vue 后就萌发了使用 vue 来编写 Chrome 的想法,当然肯定不止我一个这么想过,所以我在 github 上就能搜索到相应的源码,于是就有了这篇文章。 + +如果有涉及到爬取数据相关的,我肯定是首选使用 HTTP 协议,如果在搞不定我会选择使用 puppeteerjs,不过 Chrome 插件主要还是增强页面功能的,可以实现原本页面不具备的功能。 + +本文仅仅只是初步体验,简单编写了个小项目,后期有可能会实现一个百度网盘一键填写提取码,Js 自吐 Hooke 相关的。(原本是打算做 pdd 商家自动回复的,客户说要用客户端而不是网页端(客户端可以多号登陆),无奈,这篇博客就拿 B 站来演示了) diff --git "a/blog/program/\345\260\206 Supabase \344\275\234\344\270\272\344\270\213\344\270\200\344\270\252\345\220\216\347\253\257\346\234\215\345\212\241.md" "b/blog/program/\345\260\206 Supabase \344\275\234\344\270\272\344\270\213\344\270\200\344\270\252\345\220\216\347\253\257\346\234\215\345\212\241.md" new file mode 100644 index 0000000..778a9c6 --- /dev/null +++ "b/blog/program/\345\260\206 Supabase \344\275\234\344\270\272\344\270\213\344\270\200\344\270\252\345\220\216\347\253\257\346\234\215\345\212\241.md" @@ -0,0 +1,262 @@ +--- +slug: use-supabase-as-backend-service +title: 将 Supabase 作为下一个后端服务 +date: 2023-02-18 +authors: kuizuo +tags: [supabase, nuxt, project] +keywords: [supabase, nuxt, project] +description: 本文介绍了如何使用 Supabase 作为后端服务,使开发人员可以更快地构建和部署应用程序,无需配置数据库或编写复杂的身份验证代码。将使用 Nuxt.js 和 Supabase,以实现一个图床网站为例,来演示如何在前端中使用 Supabase API 和 Storage 服务。 +image: https://img.kuizuo.cn/213727234-cda046d6-28c6-491a-b284-b86c5cede25d.png +toc_max_heading_level: 3 +--- + +对于想快速实现一个产品而言,如果使用传统开发,又要兼顾前端开发,同时又要花费时间构建后端服务。然而有这么一个平台(Baas Backend as a service)后端即服务,能够让开发人员可以专注于前端开发,而无需花费大量时间和精力来构建和维护后端基础设施。 + +对于只会前端的人来说,这是一个非常好的选择。后端即服务的平台使得开发人员能够快速构建应用程序,更快地将其推向市场。当然了,你可以将你的后端应用接入 Baas,这样你就无需配置数据库,编写复杂的身份效验。 + +如果你想了解 Baas,我想这篇文章或许对你有所帮助。 + + + +## 什么是 [Supabase](https://supabase.com/ 'Supabase')? + +在摘要部分也介绍到名词 BaaS (Backend as a Service) ,意思为**后端即服务**。这个概念是在我接触 Serverless 的时候了解到的,更准确来说是腾讯云开发。当时在编写小程序的时候,只需要专注与应用业务逻辑,而不用编写数据存储,身份验证,文件存储等后端服务,这些统统由 BaaS 平台所提供。 通常会配合 Serverless 函数使用,通常也叫 FaaS(Function as a Service)。通常来说,FaaS 会依赖于 BaaS 平台。 + +而 Supabase 便是 BaaS 的平台之一。Supabase 是一个开源的 Firebase 替代品。使用 Postgres 数据库、身份验证、即时 API、边缘函数、实时订阅和存储启动项目。 + +你也许听过 Firebase,由 Google 提供的私有云服务,但开发者无法修改和扩展其底层代码。而 Supabase 是开源的,提供了类似 Firebase 的功能,且定价灵活,并且官方自称为 [Firebase](https://link.juejin.cn/?target=https://firebase.google.com/ 'Firebase')的替代品。 + +## BaaS 与 CMS 有何不同? + +BaaS 通常只专注于应用的后端服务,而 CMS 则是专注与内容管理。不过 BaaS 比较依赖云服务,而 CMS 通常只依赖于 web 后端技术。如果你想搭建一个内容站点(视频,音频,文章),并且作为网站管理员,那么 CMS 就是一个很好的选择,并且有相当多的主题模板。反之,不想搭建后端服务,减少运营程序,那么毫不犹豫的选择 BaaS。 + +## 注册 Supabase + +进入 [supabase 登录界面](https://app.supabase.com/sign-in) 选择 Continue With Github + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_2yiQ9NHv21.png) + +输入 Github 账号密码进入[主页面](https://app.supabase.com/projects '主页面'),新建一个项目 + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_0eoOyP8DM2.png) + +为该项目起名,设置数据库密码,以及分配地区。 + +:::warning + +创建 supabase 项目对密码要求非常严格,像 a123456 这种根本无法通过,像 ●●●●●●●●●● 密码就可以。 + +地区方面优先就近原则,而最近的也就是日本与韩国,很无奈 supabase 在大陆和港澳台并未设立服务器。 + +::: + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_N5CQnx8cnU.png) + +等待片刻,你将拥有一个免费的后端服务! + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_Z33n9aUOC7.png) + +supabase 会提供一个二级域名供开发者访问,也就是这里 Project Configuration 的 URL,对应的这个二级域名 azlbliyjwcxxxxx 也就是你这个项目的唯一标识 Reference ID(下文称 项目 id)。你可以到 [https://app.supabase.com/project/你的项目 id/settings/api](https://app.supabase.com/project/azlbliyjwcemojkwazto/settings/api 'https://app.supabase.com/project/你的项目id/settings/api') 中查看相关配置。 + +## 体验一下 + +这里参考到了官方文档 [Serverless APIs](https://supabase.com/docs/guides/database/api 'Serverless APIs')。 + +首先,创建一个 todos 表,并新增字段(列)task 为 varchar 类型,Save 保存。 + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_Do9LHoUsYo.png) + +Insert row 添加一行记录,id 为 1,task 为 code。 + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_R9PEyH-spd.png) + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_MLm6_i1Pb-.png) + +现在有了数据后,正常来说我们应该做什么?请求一下数据看看?不不不,应该是设置数据的权限。 + +打开到下图界面,我们要为 todos 数据新增一个 policy 策略。 + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_MEKk1-qQFl.png) + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_W-C-pGNh1o.png) + +supabase 针对不同的场景提供了相应的策略方案模板,你也可以根据你的需求进行设置,这里作为演示不考虑太复杂,选择第一个允许任何人都可以请求到 todos 数据。 + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_Oa_424N4gz.png) + +接着下一步即可 + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_wV_MqXQXcK.png) + +此时就新增了一个所有用户都可查询的 todo 的策略,同样的你还可以添加只有授权用户才能够创建更新删除 todo,更新与删除只能操作属于自己的 todo 资源。 + +这时候设置好了数据的权限后,就可以尝试去请求了,打开下图页面,将 URL 与 apikey 复制下来。 + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_GDEeyFCI2E.png) + +选择你一个 http 请求工具,这里我选用 [hoppscotch](https://hoppscotch.io/ 'hoppscotch'),将信息填写上去,请求将会得到一开始所创建的 todo 数据。 + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_aSbRfmlwb9.png) + +除了 restful api 风格,还支持 graphql 风格,可查阅文档 [Using the API](https://supabase.com/docs/guides/database/api#using-the-api 'Using the API') + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_R0HtkYmznS.png) + +### 使用类库 + +正常情况肯定不会像上面那样去使用,而是通过代码的方式进行登录,CRUD。这里使用 [Javascript Client Library](https://supabase.com/docs/reference/javascript/installing 'Javascript Client Library'),替我们封装好了 supabase 的功能。 + +首先,安装依赖 + +```bash +npm install @supabase/supabase-js +``` + +创建 客户端实例 + +```typescript +import { createClient } from '@supabase/supabase-js' +``` + +此时准备好上述的 URL 与 apikey,用于创建 supabase 实例,不过 supabase 还提供 [type 类型支持](https://supabase.com/docs/reference/javascript/typescript-support),可以将生成的 `database.types.ts` 导入到实例中,如 + +```typescript +import { createClient } from '@supabase/supabase-js' +import { Database } from 'lib/database.types' + +const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY) +``` + +此时有了 supabse 对象后,就能够请求数据了,像上述通过 http 的方式获取 todos 数据,在这里对应的代码为 + +```typescript +const { data, error } = await supabase.from('todos').select() +``` + +[官方的演示例子](https://supabase.com/docs/reference/javascript/select) 非常清晰,这里就不在演示新增更新等示例。 + +![image-20230218182910913](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image-20230218182910913.png) + +## [Supabase 主要功能](https://supabase.com/docs) + +### Database 数据库 + +supabase 基于 PostgreSQL 数据库,因此当你创建完项目后,就自动为你分配好了一个可访问的 PostgreSQL 数据库,你完全可以将其当做一个远程的 PostgreSQL 数据主机。 + +可以在如下页面中查看到有关数据库连接的信息,当然你看不到密码。 + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_6uCHh3qrlE.png) + +测试连接,结果如下,并无问题 + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_8-JOTiLI0G.png) + +### Authentication 身份验证 + +[Auth | Supabase Docs](https://supabase.com/docs/guides/auth/overview 'Auth | Supabase Docs') + +supabase 令我感兴趣的是 [Row Level Security](https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security 'Row Level Security'),supabase 使用 Postgres 的 Row-Level-Security(行级安全)策略,可以限制不同用户对同一张表的不同数据行的访问权限。这种安全机制可以确保只有授权用户才能访问其所需要的数据行,保护敏感数据免受未授权的访问和操作。 + +在传统的访问控制模型中,用户通常只有对整个表的访问权限,无法限制他们对表中特定数据行的访问。而行级安全技术则通过将访问权限授予到特定的数据行,从而让不同的用户只能访问他们被授权的行。这种行级安全有一个很经典应用场景-多租户系统:允许不同的客户在同一张表中存储数据,但每个客户只能访问其自己的数据行。 + +这对于传统后端开发而言,如果不借用一些安全框架,实现起来十分棘手,要么业务代码与安全代码逻辑混杂不堪。 + +权限细分方面,无需担心,supabase 已经为你做好了准备,就等你来进行开发。 + +#### 第三方登录 + +对于想要提供第三方登录,supabse 集成多数平台(除了国内),只需要提供 Clinet ID, Client Secret, Redirect URL 便可完成第三方登录。 + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_OvBRJ_elZR.png) + +这里演示下如何使用 Github,首先到打开[New OAuth Application (github.com)](https://github.com/settings/applications/new 'New OAuth Application (github.com)') 创建一个 Oauth Apps,其中 Authorization callback URL 由 supabase 提供,如下图。 + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_QVspy-oxQK.png) + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_jyaUMSDed2.png) + +当你创建完后,会提供 Client ID,与 Client secret,将这两个值填写到 supabase 中,并启用。 + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_QpRRxpR5o5.png) + +此时打开如下页面,将 Site URL 替换成开发环境,或是线上环境,在 Github 登录后将会跳转到这个地址上 + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_zmfXC85ayC.png) + +此时 supabase 支持 github 登录就已经配置完毕,当你在前端触发登录按钮后,借助[supabase 的 js 库](https://supabase.com/docs/reference/javascript/auth-signinwithoauth 'supabase 的js库'),如 + +```typescript +const { data, error } = await supabase.auth.signInWithOAuth({ + provider: 'github', +}) +``` + +便可完成 Github 第三方登录。 + +### Bucket 存储桶 + +接触过对象存储的开发者对 Bucket 应该不陌生,相当于给你一个云盘,这里演示如何使用。 + +打开如下界面,这里选择公开存储桶,比如说用于图床。 + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_2Is4Bfwf8f.png) + +点击右上角的 upload files,选择你要上传的图片。你可以为此生成一个访问 URL + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_vkuzeZZVJ_.png) + +你可以访问 [1.png](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/publilc/1.png) 来查看这张图片。如果是公开的话 一般都是类似https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/new-bucket/1.png + +而私有的为 https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/sign/new-bucket/1.png?token=eyJhbGciOiJIUzI1NiIsInR5cCIxxxxxxxxxxxxxxxxx 路径稍微变化了下,还有就是多了个 token,如果不携带 token 则访问不了图片。 + +你可以到[Supabase Storage API](https://supabase.github.io/storage-api/ 'Supabase Storage API') 查看 storage 相关 api。 + +:::tip 现学现用 + +本文中的所有图片数据都来源于 supabase bucket。 + +::: + +### Edge Functions 边缘函数 + +边缘函数可以分布在全球的接近您的用户各个地方,类似与 CDN,但 CDN 主要服务于静态资源,而 Edge Functions 可以将你的后端应用接口,像 CDN 那样部署到全球各地。 + +有兴趣可自行了解。 + +## **使用 Supabase 编写一个简易图床** + +如果只单纯看看 supabase 文档,不去动手实践接入一下,总觉得还是差点意思。于是我准备使用 Nuxt 作为前端框架接入 supabase,官方模块 [Nuxt Supabase](https://supabase.nuxtjs.org/ 'Nuxt Supabase') 去编写一个应用。 + +原本我是打算写个 Todo List 的(恼,怎么又是 Todo List),但是看到 [官方示例](https://supabase.com/docs/guides/resources/examples#official-examples '官方示例')(一堆 Todo List)后我瞬间就没了兴致 🥀。 + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_1polvJf0q0.png) + +思来想去,不妨就简单做个图床吧。项目地址:[https://image.kuizuo.cn](https://image.kuizuo.cn) 有兴趣可自行阅读[源码](https://github.com/kuizuo/image-hosting)。(**写的相对匆忙,仅作为演示,随时有可能删除,请勿将此站作为永久图床!**) + +## 一些你可能比较好奇的问题 + +### 资源 + +可以到 https://app.supabase.com/project/项目id/settings/billing/usage 中查看相关资源使用情况,这里我就将截图放出来了。 + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_Bllhp6XlFz.png) + +说实话,对于个人独立开发者的项目都绰绰有余了。 + +### 费用 + +在 [资费标准](https://supabase.com/pricing '资费标准') 中可以看到,免费版**最多 2 个项目**,不过在上述的资源,其实已经非常香了,毕竟只需要一个 GIthub 账号就能免费使用,还要啥自行车。 + +![](https://azlbliyjwcemojkwazto.supabase.co/storage/v1/object/public/public/image_MNtdzsdJ2t.png) + +### 网速 + +国内因为没有 supabase 的服务器节点,然后且有防火墙的存在,所以请求速度偏慢。不过体验下来至少不用梯子,速度慢点但也还在可接受范围。 + +### 域名 + +用过 vercel 的你应该会想是不是也能自定义域名呢? 当然,不过这是 supabase pro 版才支持,一个月$25(美刀),算了算了,再一眼 azlbliyjwcxxxxx.supabase.co~~就会爆炸~~感觉也蛮好记的。 + +## 结语 + +说句实话,真心感觉 supabase 不错,尤其是对个人/独立开发者而言,没必要自行去购买服务器,去搭建后端服务,很多时候我们只想专注于应用程序的开发和功能实现,而不是花费大量时间和精力在服务器和后端服务的部署和管理上。 diff --git "a/blog/program/\346\220\255\345\273\272Electron+Vue3\345\274\200\345\217\221\347\216\257\345\242\203.md" "b/blog/program/\346\220\255\345\273\272Electron+Vue3\345\274\200\345\217\221\347\216\257\345\242\203.md" new file mode 100644 index 0000000..cbc36ea --- /dev/null +++ "b/blog/program/\346\220\255\345\273\272Electron+Vue3\345\274\200\345\217\221\347\216\257\345\242\203.md" @@ -0,0 +1,116 @@ +--- +slug: electron-vue3-development-environment +title: 搭建Electron+Vue3开发环境 +date: 2022-03-17 +authors: kuizuo +tags: [electron, vue, vite] +keywords: [electron, vue, vite] +description: 搭建 Electron Vue3 的开发环境,用于编写跨平台应用 +--- + +之前用 electron-vue 写过一个半成品的桌面端应用,但是是基于 Vue2 的,最近又想重写点桌面端应用,想要上 Vue3+TypeScript,于是便有了这篇文章总结下具体的搭建过程。 + + + +## Vue Cli + +Vue CLI 有一个插件`vue-cli-plugin-electron-builder`,可以非常方便的搭建 electron 环境。 + +```bash +npm i @vue/cli -g +``` + +```bash +vue create my-app +``` + +根据自己项目的需求选择对应的依赖(例如 Babel,TS,Vuex 等等) + +```bash +Vue CLI v5.0.3 +? Please pick a preset: Manually select features +? Check the features needed for your project: Babel, TS, Vuex, CSS Pre-processors, Linter +? Choose a version of Vue.js that you want to start the project with 3.x +? Use class-style component syntax? Yes +? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes +? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with dart-sass) +? Pick a linter / formatter config: Prettier +? Pick additional lint features: Lint on save +? Where do you prefer placing config for Babel, ESLint, etc.? In package.json +? Save this as a preset for future projects? No + + +Vue CLI v5.0.3 +✨ Creating project in F:\Electron\my-app. +🗃 Initializing git repository... +⚙️ Installing CLI plugins. This might take a while... +``` + +### 安装 vue-cli-plugin-electron-builder + +[Vue CLI Plugin Electron Builder (nklayman.github.io)](https://nklayman.github.io/vue-cli-plugin-electron-builder/) + +```bash +cd my-app +vue add electron-builder +``` + +安装过程中会提示你选择 Electron 的版本,选择最新版本即可 + +### 启动项目 + +```bash +npm run electron:serve +``` + +参考文章:[Electron + Vue3 开发跨平台桌面应用【从项目搭建到打包完整过程】 - 掘金 (juejin.cn)](https://juejin.cn/post/6983843979133468708) + +### 坑 + +``` +error in ./src/background.ts + +Module build failed (from ./node_modules/ts-loader/index.js): +TypeError: loaderContext.getOptions is not a function +``` + +我测试的时候,`@vue/cli-plugin-typescript`版本为`~5.0.0`,就会导致编译类型出错,将 package.json 中改为`"@vue/cli-plugin-typescript": "~4.5.15"`,即可正常运行(但还是会有 DeprecationWarning) + +## Vite + +上面是使用 Vue Cli 脚手架进行开发,如果想上 Vite 的话,就需要用 Vite 来构建项目,然后安装 electron 的相关依赖。 + +这个不是作为重点,因为很多大佬都已经写了现成的模板,完全可以自行借鉴学习,就贴几个阅读过的几篇文章 + +[Vite + Vue 3 + electron + TypeScript - DEV Community](https://dev.to/brojenuel/vite-vue-3-electron-5h4o) + +[2021 年最前卫的跨平台开发选择!vue3 + vite + electron - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/424202065) + +### 现成的模板 + +均可在 github 上搜索到 + +- [vite-react-electron](https://github.com/caoxiemeihao/vite-react-electron) (推荐) + +- [electron-vue-vite](https://github.com/caoxiemeihao/electron-vue-vite) (推荐) +- [vite-electron-builder](https://github.com/cawa-93/vite-electron-builder) + +### electron-vite 脚手架(推荐) + +当然也可以使用脚手架,可选择 React 与 Vue,实际上也就是创建上面的前两个模板 + +```bash +npm create electron-vite +``` + +## 现有项目使用 electron + +TODO... + +## 总结 + +因为 Electron 本质上还是一个浏览器,无论是 Vue 还是 React 开发也好,在传统网页开发的时候都有对应的调试地址,如 [http://127.0.0.1:3000](http://127.0.0.1:3000),而 electron 的做法无非就是开启一个浏览器,然后和正常的网页开发一样,并提供桌面端的 api 使用。 + +目前社区两大 Vue+Electron 的脚手架主要是[electron-vue](https://github.com/SimulatedGREG/electron-vue)和[vue-cli-plugin-electron-builder](https://github.com/nklayman/vue-cli-plugin-electron-builder),更多 electron 的开源项目都遵循着前者的项目结构,像上面的模板也就是。 + +以上就是我所使用 Vue3 来开发 Electron 的环境搭建过程,总体来说从 Electron 除了应用体积过大,对于前端开发者来说是非常友好的,既然环境配置完,那么现在就可以开始好好的编写桌面端应用了。 diff --git "a/blog/program/\346\220\255\345\273\272GitLab\344\273\243\347\240\201\347\256\241\347\220\206\344\273\223\345\272\223.md" "b/blog/program/\346\220\255\345\273\272GitLab\344\273\243\347\240\201\347\256\241\347\220\206\344\273\223\345\272\223.md" new file mode 100644 index 0000000..450f2d5 --- /dev/null +++ "b/blog/program/\346\220\255\345\273\272GitLab\344\273\243\347\240\201\347\256\241\347\220\206\344\273\223\345\272\223.md" @@ -0,0 +1,255 @@ +--- +slug: gitlab-code-management-environment +title: 搭建GitLab代码管理仓库 +date: 2022-04-15 +authors: kuizuo +tags: [git, gitlab] +keywords: [git, gitlab] +description: 搭建 GitLab 代码管理仓库,用于管理代码 +--- + +![image-20220414235645607](https://img.kuizuo.cn/image-20220414235645607.png) + +我只要有代码的项目,都会放到 Github 上,无论公开还是私有项目。一是相当于在云端备份了一份代码,二是可以很方便的分享给别人。但对于私有项目而言存放在别人那总归不好,而且 Github 时常会出现无法访问的情况(即使搭了梯子)。所以就打算搭建一个私有的仓库,基于[GitLab](https://gitlab.com/)。 + +可以访问 [kuizuo · GitLab](https://gitlab.kuizuo.cn/kuizuo) 来查看搭建效果。 + + + +## 页面概览 + +![image-20220415013028002](https://img.kuizuo.cn/image-20220415013028002.png) + +## 前提 + +一台服务器,系统 Linux,内存 >=4g + +我的轻量应用服务器配置如下 + +![image-20220414210129510](https://img.kuizuo.cn/image-20220414210129510.png) + +## 搭建 + +服务器我选择安装[宝塔面板](https://www.bt.cn/new/index.html),对于个人项目,还是很推荐安装的,集成了一些软件商店,包括本次的主角,同时提供可视化页面操作,能省下很多敲命令的时间,~~同时也会增加忘记命令的记忆~~。 + +### 安装 GitLab + +进入宝塔面板,点击软件商店,找到**GitLab 最新社区版**,点击安装 + +![image-20220414204808143](https://img.kuizuo.cn/image-20220414204808143.png) + +实测等了 8 分钟,安装完毕即可查看 GitLab 的访问地址,账号密码。默认端口号 8099,记得在防火墙开放下该端口 + +![image-20220414213002293](https://img.kuizuo.cn/image-20220414213002293.png) + +进入访问地址就可以看到 GitLab 的登录页面了。 + +### 修改密码 + +[Reset a user's password | GitLab](https://docs.gitlab.com/ee/security/reset_user_password.html#reset-the-root-password) + +进入控制台(进入要稍等一段时间) + +```bash +sudo gitlab-rails console +``` + +显示页面如下 + +``` +[root@VM-4-5-centos ~]# sudo gitlab-rails console +-------------------------------------------------------------------------------- + Ruby: ruby 2.7.5p203 (2021-11-24 revision f69aeb8314) [x86_64-linux] + GitLab: 14.9.3 (ec11aba56f1) FOSS + GitLab Shell: 13.24.0 + PostgreSQL: 12.7 +------------------------------------------------------------[ booted in 29.71s ] +Loading production environment (Rails 6.1.4.6) +irb(main):001:0> +``` + +输入如下代码 + +```bash +u=User.find(1) +u.password='a12345678' +u.password_confirmation = 'a12345678' +u.save! +``` + +输出结果 + +```bash +irb(main):001:0> u=User.find(1) +=> # +irb(main):002:0> u.password='a12345678' +=> "a12345678" +irb(main):003:0> u.password_confirmation = 'a12345678' +=> "a12345678" +irb(main):004:0> u.save! +=> true +irb(main):005:0> +``` + +最后输入`exit`退出控制台,然后输入下方代码重启 gitlab,密码就设置完毕了 + +```bash +gitlab-ctl restart +``` + +:::info 若重启或修改端口等操作后出现 502 错误,您可能需要等待 3-5 分钟才能正常访问 GitLab + +::: + +### 修改语言 + +点击右上角的头像->Preferences 进入到设置,找到语言设置为简体中文,然后点击左小角的 Save changes。刷新网页语言就设置完毕了 + +![image-20220414215528543](https://img.kuizuo.cn/image-20220414215528543.png) + +### 配置 HTTPS + +gitlab 内部集成了 letsencrypt,因此,这里只需要启用 letsencrypt,并进行一些必要的配置 + +打开/opt/gitlab/etc/gitlab.rb.template,修改以下内容 + +1. 在 32 行左右,将 external_url 前面的#删掉,并在单引号中填写 gitlab 服务器的 https 地址,例如[https://gitlab.kuizuo.cn](https://gitlab.kuizuo.cn) + + ``` + external_url 'https://gitlab.kuizuo.cn' + ``` + +2. gitlab 默认占用 nginx80 端口,所以需要更改下 + + ``` + nginx['listen_port'] = 8100 + ``` + +3. 在 2434 行左右(可通过搜索 letsencrypt 定位),修改下面几项 + + ``` + letsencrypt['enable'] = true #删除前面的#号,并将值修改为true + letsencrypt['contact_emails'] = ['kuizuo12@gmail.com'] #删除前面的#号,修改为自己的邮箱 + letsencrypt['auto_renew'] = true #删除前面的#号 自动更新 + ``` + +然后重载配置(需要一点时间) + +``` +gitlab-ctl reconfigure +``` + +然后重启 gitlab 使配置生效 + +``` +gitlab-ctl restart +``` + +gitlab 就会通过 letsencrypt 自动签发免费的 HTTPS 证书,等证书签发成功,就可以通过上面指定的域名访问代码仓库了。 + +**其实也可以在 nginx 创建一个站点,然后该站点配置 ssl,通过反向代理到 127.0.0.1:8099 也是能实现配置 HTTPS 的。(推荐)** + +:::danger + +如果上面的操作的话,可能会导致 gitlab 的 nginx 无法启动(原因应该是修改了 gitlab 自带的 nginx 服务,或者与自带的冲突)。修改`/opt/gitlab/sv/nginx/run` + +```bash +exec chpst -P /opt/gitlab/embedded/sbin/nginx -p /var/opt/gitlab/nginx +# 改为 +exec chpst -P /opt/gitlab/embedded/sbin/gitlab-web -p /var/opt/gitlab/nginx +``` + +重启 gitlab + +```bash +gitlab-ctl start +``` + +::: + +## 管理中心 + +点击左上角的菜单选择管理员,可在管理中心设置 GitLab 的相关设置。例如 + +### 禁止注册 + +在设置->通用->注册限制,取消勾选 **已启动注册功能**,这样就可以禁止注册(页面无注册按钮)。当然也可以允许,然后需要批准以及确认邮箱。 + +![image-20220415004207174](https://img.kuizuo.cn/image-20220415004207174.png) + +在概览->用户中可以查看相关用户信息。 + +![image-20220415012817311](https://img.kuizuo.cn/image-20220415012817311.png) + +至于其他设置自行研究了。 + +## 创建项目 + +点击新建项目,这里就导入我的 blog 项目。 + +![image-20220414220221480](https://img.kuizuo.cn/image-20220414220221480.png) + +选择 Github 后,会提示使用 GitHub 身份验证,这里需要拿到 Github 的[Token](https://github.com/settings/tokens) + +![image-20220414220333437](https://img.kuizuo.cn/image-20220414220333437.png) + +访问https://github.com/settings/tokens,新建一个Token,选择token有效期,以及相关权限(我这边选择全选,token不过期) + +![image-20220414220507016](https://img.kuizuo.cn/image-20220414220507016.png) + +![image-20220414220738714](https://img.kuizuo.cn/image-20220414220738714.png) + +生成完毕后复制该 Token 到 GitLab 上,就可以看到该 Github 账号下的所有仓库了,这里我选择 blog 进行导入(导入需要一点时间)。 + +![image-20220414220858379](https://img.kuizuo.cn/image-20220414220858379.png) + +导入完毕后与原仓库无特别区别 + +![image-20220414224639573](https://img.kuizuo.cn/image-20220414224639573.png) + +### 自动同步项目 + +点击项目中设置->仓库,找到镜像仓库。在 Git 仓库 URL 中填写格式如下 + +```js +// 原仓库git +https://github.com/kuizuo/blog +// 在https://后加上username@ +https://kuizuo@github.com/kuizuo/blog +``` + +密码为上面的 Token(如果忘记的话,可以在 Github 的 Token 页中 Regenerate token),如下图所示 + +![image-20220414232028397](https://img.kuizuo.cn/image-20220414232028397.png) + +--- + +基本上 github 能实现的操作 gitlab 也都能实现。 + +## 其他功能 + +### Web IDE(在线编辑代码) + +![image-20220415001914123](https://img.kuizuo.cn/image-20220415001914123.png) + +## 运行状态 + +放几张图 + +![image-20220414233435739](https://img.kuizuo.cn/image-20220414233435739.png) + +输入 top 命令,按 M 按内存排序。 + +![image-20220414233416223](https://img.kuizuo.cn/image-20220414233416223.png) + +还是挺吃内存的,毕竟安装的时候就要求 4g 内存以上。 + +有个轻量级的项目管理器 [gitea](https://github.com/go-gitea/gitea) 不妨也是一种选择,但功能上没有 Gitlab 这么丰富。 + +对于自建 git 服务的选择,这里有篇文章推荐阅读 [自建 Git 服务器:Gitea 与 Gitlab 部署踩坑经历与对比总结 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/486410391) + +## 总结 + +其实回到一开始的问题,既然 Github 有可能访问不了,为啥不要迁移到国内的[Gitee](https://gitee.com/)上。 + +~~除了瞎玩瞎折腾外~~,对于一些公司而言,他们不一定会使用这类开源的代码托管平台,而是自建一个像 GitLab 这样的代码仓库管理系统。此外别人的东西,多半都会有一定的限制,例如项目成员数量等等,所以才会有这次的尝试,整体体验感觉可玩性很大。 diff --git "a/blog/program/\347\254\254\344\270\200\344\270\252\345\215\232\345\256\242\346\220\255\345\273\272\344\271\213Vuepress.md" "b/blog/program/\347\254\254\344\270\200\344\270\252\345\215\232\345\256\242\346\220\255\345\273\272\344\271\213Vuepress.md" new file mode 100644 index 0000000..cd1c1b4 --- /dev/null +++ "b/blog/program/\347\254\254\344\270\200\344\270\252\345\215\232\345\256\242\346\220\255\345\273\272\344\271\213Vuepress.md" @@ -0,0 +1,313 @@ +--- +slug: first-blog-is-vuepress +title: 第一个博客搭建之Vuepress +date: 2020-08-30 +authors: kuizuo +tags: [blog, vuepress, project] +keywords: [blog, vuepress, project] +--- + +感谢 [vuepress-theme-reco](https://vuepress-theme-reco.recoluan.com/)主题与一篇博客使用文章[使用 vuepress 构建个人博客](https://lookroot.cn/views/article/vuepress.html#reco%E4%B8%BB%E9%A2%98) + +在写这篇文章前,本人非前端专业人士,只是一时兴起想开始搭建一个博客,在该博客上记录与分享一下自己所学的一切内容。(然后现在都在往前端这方向走了) + + + +## 补充 + +不打算破坏之前所写的一些内容,将一些补充的内容写在开头,目前访问 kzcode.cn 依旧能访问到该站,不过文章都保留原有未变动,图片都是直接跟随 MD 文档下,加上服务器又比较拉,所以访问会稍带卡顿。这篇合理来说也算我的第一篇博客,所以还是有必要记录一下的。 + +## 为什么写博客 + +在接触软件行业的这一年里,也学到了很多很多很多的知识,也让我感受到代码的魅力与强大。在这学习过程中,百度到了各种相关学习文章,而这些正式前辈们所分享的学习经历,倘若没有这些,我可能早已停下学习的步伐。一时兴起,萌生了搭建博客的想法,然后开始搜索搭建博客的知识,于是乎就有了这篇文章。 + +写博客是为了记录自己,记录自己学习中的过程,知识,遇到的坑,写成一篇文章,也许过了几个月后的自己脑子不好使,忘记是如何解决的,回过头来看,瞬间焕然大悟。同时也能巩固自己所学的,在如今这个时代,技术更新换代是真的快,而要求学的东西也就越来越多。有时候学过的一项技术,过了几个月真的说忘就忘,不时常记录一下当初写的笔记,翻看之前写过的代码,那真的就和重新学习没什么两样了。 + +在此期间曾遇到许多坑,而解决最好的办法就是百度。在百度搜索过程中看到了许许多多的学习者分享自己那时候与我遇到一样的问题,说明他是如何解决的,并写文章。有时运气好可能他的解决办法同样也是我的解决办法,但往往总是不尽人意,这时需要再看下一个相关搜索或者下下个才能解决我的问题,这在学习过程中是必不可少的一个环节。而他们所分享的内容,就是一篇篇的博客。正是这一篇篇博客解惑学习者学习中的问题,让他们有自信再去学习下去! + +于是就萌生的一种想法,利用自己所学的 Web 知识开始搭建属于自己的个人博客,分享自己所遇到的坑,希望能解决遇到同样问题的人。 + +### 初步搭建博客界面 + +要展示给别人看,就必须得搞前端 UI 界面,同时为了快速开发,我又百度了相关的前端 UI 框架,其中决定用 layui,界面风格布局可以接受,于是乎在搜索用过 layui 框架搭建的个人博客,成功找到一篇博主[燕十三一个人的江湖](https://www.yanshisan.cn/)分享的模板源码,然后开始大改,最终花了几个小时修改了大致页面 + +![image-20200901012343086](https://img.kuizuo.cn/image-20200901012343086.png) + +然后问题来了,前端大致界面设计好了,我最关心的文章要怎么写。。。而这份源码是没有给后端这些的(此时的我刚入前端,后端毛也不会),就一个纯前端页面,连文章模板都没有,可能是真的没什么可整理的,于是就放弃了自己手动搭建,主要还是那时候太菜了。 + +就在想用纯静态页面,还是用动态页面,对于动态页面获取文章数据的技能并不熟练。然后发现这种想法并不行,还是得借助外界提供现有的博客系统来写,于是乎停滞了一段时间,去搜索一系列相关的博客系统(合理来说应该是静态文件生成器),如 Hexo 或 WordPress,不过为啥选择 vuepress,因为我那时候正好在学 Vue,于是乎又开始新的一番折腾。不过也好,如果以后写技术文档,vuepress 也是一个非常推荐的选择。后续的话可能会去接触一下 Hexo 的 butterfly 主题,希望学到点东西,能给自己的博客在增添几份美感。 + +我所用的主题是[vuepress-theme-reco](https://vuepress-theme-reco.recoluan.com/) 也非常推荐用这个主题来写博客,下面会简单介绍这款主题 + +## reco 主题 + +> 一款简洁而优雅的 vuepress 博客 & 文档 主题。官方文档[立即前往](https://vuepress-theme-reco.recoluan.com/) + +![image-20200515152702435](https://img.kuizuo.cn/152702-539475.png) + +### **安装** + +```bash +#全局安装vuepress-reco +npm install @vuepress-reco/theme-cli -g + +# 初始化 (blog改成你要的文件名) 然后填写项目标题等等 +theme-cli init blog + +# 进入项目目录 +cd blog + +#安装依赖包 +npm install + +# 运行 +npm run dev + +# 编译 +npm run build +``` + +执行完`npm run dev`运行后,点击控制台的对应地址 你就能看到 + +![image-20200901191643031](https://img.kuizuo.cn/image-20200901191643031.png) + +当然,可能标题和一些会不一样,因为我更改了两处地方一处是`blog`下的`README.md`文件,文件结构如下 + +```markdown +--- +home: true +heroText: 愧怍的个人空间 +... +``` + +这个是决定首页的样式,具体要什么背景,内容就因人而异。 + +另一处是`.vuepress\config.js`里的内容,内容有点多,我依次来讲 + +首先是开头几行的,title 决定你网站的标题,description 则是一开始出场界面的描述 + +```js {2-3} +module.exports = { + title: '愧怍的小站', + description: '如果代码都解决不了的话,那可能真的解决不了', +} +``` + +随后你要关注的就是 themeConfig 下的 author,也就是作者名,改成你的名字就行 + +```js {1,5,6} + "logo": "/logo.png", + "search": true, + "searchMaxSuggestions": 10, + "lastUpdated": "Last Updated", + "author": "愧怍", + "authorAvatar": "/logo.png", + "record": "xxxx", + "startYear": "2017" +``` + +其余的一些,比如`logo.png`与`avatar.png`啊,你换成你的想要的头像就行,他们都存放在.`vuepress\public`下,然后就是修改标题栏,他们都放在`themeConfig`下的`nav`里,这里你想修改哪个导航栏,就改哪个导航栏与标题,文末我会放上我的全部代码。 + +```js +"themeConfig": { + "nav": [ + { + "text": "Home", + "link": "/", + "icon": "reco-home" + }, + { + "text": "TimeLine", + "link": "/timeline/", + "icon": "reco-date" + }, + { + "text": "Docs", + "icon": "reco-message", + "items": [ + { + "text": "vuepress-reco", + "link": "/docs/theme-reco/" + } + ] + }, + { + "text": "Contact", + "icon": "reco-message", + "items": [ + { + "text": "GitHub", + "link": "https://github.com/recoluan", + "icon": "reco-github" + } + ] + } + ], +``` + +还有一个是主题自带的导航栏配置,这里你只需要更改 text 与 location 即可,其余不建议更改,你到时候写的文章都在依靠这两个 + +```js +"blogConfig": { + "category": { + "location": 2, + "text": "Category" + }, + "tag": { + "location": 3, + "text": "Tag" + } + }, +``` + +zhuyi 你每次修改修改 config 的内容,就需要重新`npm run dev` vuepress 不支持热更新,也就是文件内容给修改了你需要重新编译运行,这是初学接触会遇到的坑。 + +### 编写文章 + +现在有了一个页面风格不错,同时还是响应式页面,就差文章了。这时候你就需要了解 vuepress 的[Markdown 拓展](https://vuepress.vuejs.org/zh/guide/markdown.html#front-matter),我这里简单叙述一下,你该怎么写文章,下面是你要写文章的模板,你只需要关注几个内容就行了, + +```markdown +--- +title: 笔记模板 +date: 2020-08-21 +tags: + - 笔记 +categories: + - 个人学习笔记 +author: 愧怍 +keys: + - 'e9bc0e13a8a16cbb07b175d92a113126' +publish: false +isShowComments: false +--- + +::: tip + +这是 tip + +::: + + + +## 这是你的文章内容 + +正文内容 +``` + +`---` 所包裹的内容就文章简述像下面这样 + +![image-20200901034715126](https://img.kuizuo.cn/image-20200901034715126.png) + +要更改标题,日期外,你还需要更改的是分类 categories 和标签 tags,举个例子,现在我想写一篇文章,标题是 ES6 语法,那么我可以这么写 + +```yaml +tags: + - ES6 + - javascript + - js +categories: + - JavaScript +``` + +分类只写一个,可以写`JavaScript`(分类建议大写),标签写多个,然后你把你写的这篇文章,切记放在`blogs`目录下(以后写的博客都放在这里),同时建一个文件夹名为`JavaScript`,的然后把文章放在这个目录下,文章名随意,建议和标题一样,如 ES6 语法.md。便于你以后分类,请按这样的方式归类文章。 + +万一我不小心`- JavaScript` 写成了 `- Java`,而并没有文件夹是`Java`的,没关系,也就是你在分类上看到 Java,文章分类不取决于文件夹名,而取决于`categories` 只是文件夹名和`categories`名一致便于分类罢了。 + +在标签页上,就能看到 ES6,javascript,与 js 的标签,方便定位相关文章 + +接着把要写的文章内容全都在写在下即可,这里要注意一下,正文内容的标题,从二级标题开始,一级标题就已经是 title 了,在写也没用。 + +其余相关的 key 和 publish 等相关参数还请读者查看 reco 主题的官方文档[立即前往](https://vuepress-theme-reco.recoluan.com/)与 vuepress 官网[立即前往](https://vuepress.vuejs.org/zh/) + +### 样式修改 + +可能你会局限于 reco 主题的默认样式,这里就说下如何修改样式。如果你会点前端,这应该来说非常简单。 + +1. 先参考这篇文章 [个人向优化](https://vuepress-theme-reco.recoluan.com/views/other/reco-optimization.html),我这里简单说明一下,首先一定要把`node_modules`里的`vuepress-theme-reco`这个主题文件夹整个放在`.vuepress\theme\`下,因为有些时候我们是要修改源码来更改样式的,如果你不这样做的话,而是直接修改`node_modules`里面的文件,你`npm install`就会覆盖你修改后的,所以要这一步操作。 +2. 当然你已经觉得 reco 主题都很完美了,不需要更改源码,那么你只需要在 `.vuepress/styles/` 来创建文件`index.styl`来方便地添加额外样式(还有一个默认样式,不推荐修改),然后把你要修改的样式代码写在`index.styl`文件里即可,例如我要修改首页的字体颜色,右键检查找到对应的 css 选择器,然后在`index.styl`添加就行,如 + +```css +.home-blog .hero h1 { + color: #fff; +} +``` + +### 部署到服务器上 + +关于部署到服务器上,如果只是为了让别人能看到你搭建的博客,而不是要购买域名和服务器这些,直接参考文章[使用 vuepress 构建个人博客](https://lookroot.cn/views/article/vuepress.html#reco%E4%B8%BB%E9%A2%98)即可,如果有服务器和域名我这里简单说下怎么个部署法。 + +```bash +npm run bulid +``` + +首先执行上一行代码,然后在目录下会生成`public`文件夹,这个文件夹就是你所有的网站静态文件,这时候你需要你的服务器开启一个 web 服务,我这里用的是腾讯云 CentOS 与宝塔面板(至于这两个怎么搞,外面教程太多了),这里我就用 Nginx。然后如下图添加站点 + +![image-20200918194540550](https://img.kuizuo.cn/image-20200918194540550.png) + +因个人情况填写域名,FTP,数据库等等,然后通过 ftp 工具直接传文件至站点对应的目录下,然后访问服务器对应的 ip 地址或者个人域名解析就行了。 + +不过这个还要手动部署特别麻烦,有没有什么命令能一键部署的,有,这里我推荐一篇文章[一键部署到服务器](https://reinness.com/views/technology-sharing/vuepress/auto_deploy.html#index-js),解决了我当初一直用 ftp 的痛点。不过有个更简单的自动部署脚本,scp2,有兴趣可以自行查阅。 + +## 自己搭建遇到的坑 + +### 图片路径 + +首先就是 markdown 图片相对路径的坑,在写文章的话,如果涉及的本地图片引入,那么默认不操作的,也就是需要配置一下,默认在当前同级文件下,在创建一个文件名相同的文件夹来存放图片,我这里就以 Typora 为例,如图 + +![image-20200901180754412](https://img.kuizuo.cn/image-20200901180754412.png) + +其次,Typora 的路径是不带`./`,在 vuepress 会被编译成绝对路径。需要在前面添加上`./`,不过主题内已自带插件`markdown-it`,这个问题无需担心。 + +但常常我们的 md 文件名是中文的,这时候相对路径带有中文,但是 vuepress 会将中文路径进行 url 编码, + +不会将你的这些图片编译到静态文件上,所以需要做一些操作 + +#### 解决方法 + +1. 安装 markdown-it-disable-url-encode + +```bash +npm i markdown-it-disable-url-encode +``` + +2. 在.vuepress/config.js 中配置如下 + +```js + markdown: { + extendMarkdown: md => { + md.use(require("markdown-it-disable-url-encode")); + } + }, +``` + +现在你用 Typora 就引用本地图片就可以在 vuepress 中完美显示了。 + +> 参考 [Vuepress 图片资源中文路径问题](https://segmentfault.com/a/1190000022275001) 完美解决上述问题 + +### 引入 UI 组件库报错 + +如果你在该主题使用其他 UI 组件库,如 element,ant design,那么你很有可能会编译失败,官方解释 + +![image-20201223042921876](https://img.kuizuo.cn/image-20201223042921876.png) + +解决办法很简单,先删除 node_modules,然后**再安装 ui 组件库**依赖后,再安装其他依赖就行了。 + +## 放一些链接 + +放一些自己搭建这个博客过程中用到的一些链接地址,主要针对插件安装这些 + +- [VuePress 官网](https://vuepress.vuejs.org/zh/) +- [VuePress 社区](https://vuepress.github.io/) +- [awesome-vuepress](https://github.com/vuepress/awesome-vuepress) +- [reco 主题](https://vuepress-theme-reco.recoluan.com/) +- [一个非常详细的搭建教程](https://blog.csdn.net/sudadaipeng1/article/details/102971008#%e6%b7%bb%e5%8a%a0svg-label%e6%a0%87%e7%ad%be) + +## 总结 + +就此,就可以好好的编写文章,主题固然方便,快捷搭建博客同时也别光顾这美化博客,注重分享文章,这才是博客的真正意义。reco 的主题也是希望帮助更多的人花更多的时间在内容创作上,而不是博客搭建上。 + +在使用 Vuepress 的一段时间,发现他更适合写的是文档,写博客可以,但花里胡哨的点少,比较简约,对于我这种又爱折腾的人来说,后续有可能会借鉴 Hexo 博客的一款主题 butterfly,将其源码复制到目前这个博客上,顺便巩固下自己的前端设计基础。 + +但还是要说的,要看自己到底要不要搭建博客,记录与分享文章,别盲目跟从。同时如果搭建博客,请把重心放在创作和笔记上,反复去美化主题对技术的提升远不如一篇有技术性的文章总结。 + +最后,希望我所分享的所有内容,正是你目前所遇到的难题,能为你排坑,便足矣。 diff --git "a/blog/program/\347\254\254\344\272\214\344\270\252\345\215\232\345\256\242\346\220\255\345\273\272\344\271\213Docusaurus.md" "b/blog/program/\347\254\254\344\272\214\344\270\252\345\215\232\345\256\242\346\220\255\345\273\272\344\271\213Docusaurus.md" new file mode 100644 index 0000000..08acb99 --- /dev/null +++ "b/blog/program/\347\254\254\344\272\214\344\270\252\345\215\232\345\256\242\346\220\255\345\273\272\344\271\213Docusaurus.md" @@ -0,0 +1,78 @@ +--- +slug: second-blog-is-docusaurus +title: 第二个博客搭建之Docusaurus +date: 2021-08-20 +authors: kuizuo +tags: [blog, docusaurus, project] +keywords: [blog, docusaurus, project] +description: 使用 docusaurus 搭建个人博客,并对其主题进行魔改 +image: /img/project/blog.png +sticky: 5 +--- + +博客地址: [愧怍的小站](https://kuizuo.cn/) + +时隔近半年没好好整理文章,博客也写的不像个人样。:joy: + +大半年没更新博客,一直忙着写项目(写到手软的那种),然后无意间在 B 站看到一个 Up 主 [峰华前端工程师](https://zxuqian.cn/) 基于 React 驱动的静态网站生成器搭建的个人博客。第一眼看到该站点的时候惊艳到我了,于是我在其基础上并魔改了一些页面功能,作为个人站点使用。 + +> 不过国内 docusaurus 的使用者是真的少,Vuepress 都快烂大街了... + + + +## 安装 + +如果你想搭建一个类似的博客,可以 [fork 本项目](https://github.com/kuizuo/blog/fork),修改个人信息,并将文章迁移过来。这里推荐使用 [Vercel 部署个人博客](https://kuizuo.cn/vercel-deploy-blog),以下是本地安装示例。 + +```bash +git clone https://github.com/kuizuo/blog +cd blog +yarn +yarn start +``` + +关于主题魔改可以看 [Docusaurus 主题魔改](https://kuizuo.cn/docs/docusaurus-guides) + +## 一些页面 + +### [博客页](/blog/) + +![image-20230221120937768](https://img.kuizuo.cn/image-20230221120937768.png) + +- 支持 3 种博文信息展示 +- 博客个人信息卡片 +- 可根据 `sticky` 字段对文章进行置顶推荐 + +### [归档页](/blog/archive) + +![image-20220804052418993](https://img.kuizuo.cn/image-20220804052418993.png) + +### [资源导航](/resources) + +![image-20220804052016538](https://img.kuizuo.cn/image-20220804052016538.png) + +- 在此分享所收藏的一些好用、实用网站。 + +### 评论 + +![image-20220804052746803](https://img.kuizuo.cn/image-20220804052746803.png) + +- 接入 [giscus](https://giscus.app) 作为评论系统,支持 GitHub 登录。 + +### [项目](/project) + +![image-20220804052117492](https://img.kuizuo.cn/image-20220804052117492.png) + +- 存放你的项目,或是当做一个作品集用于展示。 + +## 部署 + +按传统的方式,你编写好一篇文章后,需要重新打包成静态文件(.html),然后将静态文件上传到服务器(需要自己准备)上,然后通过 nginx 配置域名访问。如今有了自动化部署,你只需要将代码 push 到 Github 上,然后通过 CI/CD 自动化部署到服务器上。可以参考 [ci.yml](https://github.com/kuizuo/blog/blob/main/.github/workflows/ci.yml) 配置文件。 + +这里推荐使用 [Vercel 部署个人博客](/blog/vercel-deploy-blog),部署十分简单,你甚至不需要服务器,只需要有个 Github 账号,将你的博客项目添加为一个仓库中即可(也许需要科学上网)。 + +## 最后 + +博客的意义在于记录,记录自己的成长,记录自己的所思所想,记录自己的所学所得。希望更多的时间用在创作内容上,而不是在搭建博客上。 + +也就不浪费口舌了,博客搭建完毕,应该好好的去编写有意义的文章,才能够吸引他人的阅读。 diff --git "a/blog/program/\350\256\260ThinkPHP\351\241\271\347\233\256\351\203\250\347\275\262.md" "b/blog/program/\350\256\260ThinkPHP\351\241\271\347\233\256\351\203\250\347\275\262.md" new file mode 100644 index 0000000..e75794a --- /dev/null +++ "b/blog/program/\350\256\260ThinkPHP\351\241\271\347\233\256\351\203\250\347\275\262.md" @@ -0,0 +1,111 @@ +--- +slug: thinkphp-deploy +title: 记 ThinkPHP 项目部署 +date: 2021-09-25 +authors: kuizuo +tags: [php, develop] +keywords: [php, develop] +--- + + + +## 事情背景 + +用户花了几百块购买了一份 ThinkPHP 一个后台管理的网站源码,要求更换下部分失效接口,或是重写一个类似这样的网站。我想既然都有源码了,我改改不就完事了,这不比重写一个来的省事。虽说我不是主学 PHP 的,但至少我学过一丢丢的 PHP 语法,接触过 ThinkPHP 项目。不过层面都是局限在本地,部署到生产环境与本地还是有比较大的差别的,于是便有了这篇文章来记录一下自己部署 ThinkPHP 所遇到的一些坑。 + +## Windows 部署 + +也可理解为本地部署,本地部署就相对比较简单的了。不过需要一个工具,PHPStudy,来帮助我们配置本地的环境(Apache、Nginx、PHP、Mysql) + +[小皮面板(phpstudy) - 让天下没有难配的服务器环境! (xp.cn)](https://www.xp.cn/) + +下载安装打开界面,选择网站,创建网站 + +![image-20210925143601530](https://img.kuizuo.cn/image-20210925143601530.png) + +由于是本机,所以域名就填写 localhost 或 127.0.0.1,端口的话这边所填写的是 4200,别和其他端口冲突即可。 + +由于 ThinkPHP 的根目录要选择的是根目录下的 public 目录,不然找不到 index.php 这个文件,所以这里根目录自己指定一下源码的位置,点击确认即可。 + +### 初次启动 Not Found + +这时候访问 http://localhost:4200 提示如下 + +![image-20210925143752775](https://img.kuizuo.cn/image-20210925143752775.png) + +本着不会就百度的原则,很快就找到了解决办法 + +[ThinkPHP 报错 The requested URL /index/index/xxx.html was not found on this server](https://blog.csdn.net/qq_42940241/article/details/112461625) + +在入口文件夹 public 下查看.htaccess 是否存在。不存在则新建,存在的话,那内容替换为下面这串代码 就可以解决 Not Fund 问题 + +```xml +# +# Options +FollowSymlinks -Multiviews +# RewriteEngine On +# +# RewriteCond %{REQUEST_FILENAME} !-d +# RewriteCond %{REQUEST_FILENAME} !-f +# RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L] +# + +RewriteEngine on +RewriteCond %{REQUEST_FILENAME} !-d +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule ^(.*)$ index.php?/$1 [QSA,PT,L] + +``` + +### 页面报错 开启 Debug + +上面配置完毕后,再次打开出现如下提示。 + +![image-20210925144143248](https://img.kuizuo.cn/image-20210925144143248.png) + +遇到错误是很正常的,现在要做的就是输出报错信息,而不是简短的文字。到根目录下 config/app.php 中,将调试更改为 true(切记,生产环境中一定要更改为 false,不然用户就能查看报错详情以及对应代码) + +![image-20210925144424361](https://img.kuizuo.cn/image-20210925144424361.png) + +### 配置数据库 + +再次访问页面提示 + +**![image-20210925144620953](https://img.kuizuo.cn/image-20210925144620953.png)** + +报错信息倒是很全,不过要关注的是报错行和提示,大致意思就是没有定义数据库用户名 ml 以及密码,毕竟数据库啥的都好像没配置,要是能启动起来那估计就真是一个 bug 了,那就先找到配置文件,看看原本的账号密码是多少,数据库配置文件位置`config/database.php` + +![image-20210925145740851](https://img.kuizuo.cn/image-20210925145740851.png) + +不过 PHPstudy 用户名和密码长度都要在 6 位以上(Linux 倒是不用),所以勉为其难,把用户名和密码都改成 ml1234,接着 Mysql 导入源码给定的数据库文件(sql 文件),什么,你说源码没有给数据库文件,那我建议直接删源码,并且接下来的内容也可以不用看了。 + +数据库导入完毕后,再次访问便能看到正常的首页了,就此就算部署完毕了,这里就不放首页图了。 + +## Linux 部署 + +Linux 部署和 Windows 部署是有一丢丢差别的,这里我也列举一下,环境是 CentOS 7.6,安装了宝塔面板 + +在宝塔面板出网站,添加网站,如同 PHPstudy,配置大致相同。 + +![image-20210926050508693](https://img.kuizuo.cn/image-20210926050508693.png) + +### 关闭防跨站攻击 + +情况 1,如图 + +![image-20210925155027023](https://img.kuizuo.cn/image-20210925155027023.png) + +解决办法:点击网站,设置,将防跨站攻击关闭并保持,如下 + +![image-20210925155445084](https://img.kuizuo.cn/image-20210925155445084.png) + +### 设置伪静态 + +接着再次访问网站会出现 404 页面不存在报错,在设置中找到伪静态,添加一个 thinkphp 的配置,如下 + +![image-20210925155705573](https://img.kuizuo.cn/image-20210925155705573.png) + +再次访问后,出现的就是数据库配置的问题,配置一下数据库,导入数据,然后再次访问便可。 + +:::warning 再次提醒,生产环境下请将`app_debug`设置为 false,不然非法用户可以通过人为试错,查询对应报错代码。 + +::: diff --git "a/blog/project/API-Service\346\216\245\345\217\243\346\234\215\345\212\241.md" "b/blog/project/API-Service\346\216\245\345\217\243\346\234\215\345\212\241.md" new file mode 100644 index 0000000..6938ee1 --- /dev/null +++ "b/blog/project/API-Service\346\216\245\345\217\243\346\234\215\345\212\241.md" @@ -0,0 +1,525 @@ +--- +slug: use-nuxt3-build-api-server +title: api-service 接口服务 +date: 2022-07-20 +authors: kuizuo +tags: [nuxt, vite, vue, ssr] +keywords: [nuxt, vite, vue, ssr] +description: 基于 Nuxt3 的 API 接口服务网站,易于封装,调用,部署。 +image: https://img.kuizuo.cn/202312270328599.png +sticky: 2 +--- + +挺早之前就想写个 api 接口服务,封装下自己收集的一些 api 接口,以便调用,正好最近在接触 SSR 框架,所以就使用 [Nuxt3](https://v3.nuxtjs.org/) 来编写该项目。 + +在线地址: [API-Service](https://api.kuizuo.cn) + +开源地址: [kuizuo/api-service](https://github.com/kuizuo/api-service) + + + +如果你已经了解过 Nuxt3 与运行过程,那么可以直接跳转至 [实战](#实战) + +[Quick Start](https://v3.nuxtjs.org/getting-started/quick-start#new-project) + +``` +npx nuxi init nuxt3-app +``` + +可能会安装不上 会提示 `could not fetch remote https://github.com/nuxt/starter`,大概率就是本地电脑无法访问 github,这时候科学上网都不一定好使,这里建议尝试更换下网络或设置 host 文件。 + +安装完毕后,根据提示安装依赖与启动项目 + +![image-20220714005704602](https://img.kuizuo.cn/image-20220714005704602.png) + +初始的 nuxt3 项目及其简单,甚至没有 page、components、assets 等目录。 + +![image-20220714003726413](https://img.kuizuo.cn/image-20220714003726413.png) + +关于 nuxt3 本文不做过多介绍,本文只对部分进行介绍。nuxt 已经发布快 1 年了,相信外面很多相关介绍文章。 + +## Nuxt3 介绍 + +[What is Nuxt? ](https://v3.nuxtjs.org/guide/concepts/introduction#why-nuxt) + +### 自动导入 + +nuxt.js 与 next.js 极其相像,但 nuxt 却精简许多,这归功于 nuxt 的[自动导入](https://v3.nuxtjs.org/guide/concepts/auto-imports),这可以让你无需导入像 vue 中的 ref 等等函数,导入组件等操作,不过前提是代码文件位置要符合 nuxt 规范。如果你尝试使用过 vite 的一些自动导入插件,其效果是一样的,只不过 nuxt 都已经配置好,开箱即用。 + +### 文件路由 + +pages 为 nuxt 中页面所存放的位置,会将 pages 目录下的文件(`.vue`, `.js`, `.jsx`, `.ts` or `.tsx`) 与路由映射,像`pages/index.vue` 映射为 `/`,然后在 app.vue 中通过`` 来展示 pages。 + +要注意,**pages 下的文件一定要有根节点**,不然在路由切换的时候可能会出现问题(事实上建议所以的 vue 组件都有根节点,虽说 vue3 允许多个根节点,但或多或少存在一定问题) + +至于[动态路由与嵌套路由](https://v3.nuxtjs.org/guide/directory-structure/pages),文档说明的比较详细了,这里就不费口舌了 + +### 服务引擎 + +Nuxt3 中的的 api 接口服务引擎使用的是[⚗️ Nitro](https://nitro.unjs.io/) 的 JavaScript 服务,使用的是[h3](https://github.com/unjs/h3)的 http 框架(相当于 hook 版的 http 框架),不过文档不是特别详细,很多东西都要琢磨。(这个框架是真的相对冷门,之前都未曾听闻过) + +关于 Nuxt3 的服务具体可以看 [Nuxt 3 - Server Routes](https://v3.nuxtjs.org/guide/features/server-routes/),这里演示部分代码 + +创建一个服务,创建文件`server/api/hello.ts` + +```typescript title="server/api/helloWord.ts" +export default defineEventHandler(event => { + return 'hello nuxt' +}) +``` + +请求 http://localhost/api/hello 便可得到`hello nuxt`,在 event 可以得到 req 与 res 对象。不过在 req 身上是获取不到 query 和 body 的,这里需要使用 h3 提供的 hooks,如`useMethod()`,`useQuery()`,`useBody()`来获取,例如。 + +```typescript +export default eventHandler(async event => { + const body = await useBody(event) + + return `User updated!` +}) +``` + +这与传统的 node 的 http 框架不同点就是 query,body 这些参数不是从函数的上下文(context)取,而是通过 hook 来获取,所以这就是我为什么我说这相当于 hook 版的框架。关于这些 api,可以[点我查看](https://www.jsdocs.io/package/h3#package-functions) + +### 数据获取 + +定义完了接口,那必然是要获取数据的,nuxt.js 有四种方式来获取数据,不过主要就二种`useFetch`与`useAsyncData`,另外两种是其懒加载形式。 + +像上面定义了 helloworld 接口就可以像下面这样使用 + +```vue + + + +``` + +useAsyncData + +```vue + + + +``` + +至于 useAsyncData 与 useFetch 有什么区别的话,如果请求的是 url 资源,那么建议使用 useFetch,如果请求的是其他来源的资源,就使用 useAsyncData。可以说在请求 url 资源时,两者是等价的,如下 + +``` +useFetch(url) <==> useAsyncData(url, () => $fetch(url)) +``` + +那么如何 SSR(服务端渲染)呢? `nuxt3` 默认是全 `SSR` 的渲染模式,也就是说在上面的数据请求后就是 SSR 渲染,客户端接受到的也就是带有数据页面。 + +如果要使用传统的客户端渲染只需要填加一个 options 的 server 参数为 false 即可,如 + +```typescript +const { data } = await useFetch('/api/hello', { server: false }) +``` + +自己尝试下将 server 切换,然后打开控制台->网络中查看 Fetch/XHR 中是否有和数据相关的请求便可知道是在服务端发送的请求数据,还是客户端发送的数据。 + +## 实战 + +### 模板 + +这个项目所使用的模板是 [Vitesse for Nuxt 3](https://github.com/antfu/vitesse-nuxt3) + +![vitesse-nuxt3](https://img.kuizuo.cn/vitesse-nuxt3.png) + +该模板中集成了一些 vue 生态的相关模块(vueuse, pinia, unocss),开发者可以不必自行封装这些模块。 + +### 页面设计 + +页面设计的话其实没啥好说的,主要使用到了原子类的一个框架[unocss](https://github.com/unocss/unocss)。 + +### 接口转发 + +这里我会以通过[每日一言](https://v1.hitokoto.cn/)的 api 例子来给你演示其功能实现,请求该 api 可以得到 + +```json +{ + "id": 5233, + "uuid": "9504a2a2-bab7-4c7d-b643-a6642ed5c55e", + "hitokoto": "人间没有单纯的快乐,快乐总夹带着烦恼和忧虑。", + "type": "d", + "from": "杨绛", + "from_who": "我们仨", + "creator": "a632079", + "creator_uid": 1044, + "reviewer": 4756, + "commit_from": "web", + "created_at": "1583786494", + "length": 22 +} +``` + +这里创建`server/api/one.ts`文件 + +```typescript title="server/api/one.ts" +export default defineEventHandler(async (event) => { + const { type = 'text' } = useQuery(event) + + const data = await (await fetch('https://v1.hitokoto.cn/')).json() + if (type = 'json') { + return data + } + else { + event.res.setHeader('Content-Type', 'text/html;charset=utf-8') + return data.hitokoto + } +} +``` + +这样,这个接口就已经定义完毕了,此时访问 [/api/one](http://localhost:3000/api/one) 所得到的就是一句短语。默认状态下返回文本,如需要 json 数据等额外信息,则可添加`type=json`。例请求`/api/one?type=json`,得到的完整数据如下 + +```json +{ + "id": 7173, + "uuid": "49eff9ca-7145-4c5f-8e62-d3dca63537fa", + "hitokoto": "即使人生是一场悲剧,也应该笑着把人生演完。", + "type": "k", + "from": "查拉图斯特如是说", + "from_who": "尼采", + "creator": "Kyanite", + "creator_uid": 8042, + "reviewer": 1, + "commit_from": "web", + "created_at": "1614946509", + "length": 21 +} +``` + +而这整个过程也就是其实也就是接口转发,将访问 `/api/one` 的请求转发给目标 url https://v1.hitokoto.cn/ 的过程,然后对其数据进行抽取和封装,最终展示给调用方。 + +然而这只是完成了接口的转发,那么接口的文档又该如何实现呢? + +### 接口文档 + +要存储接口文档的数据,就需要使用 CMS(内容管理系统)或者 Database(数据库),一开始我原本打算使用`strapi`来作为 CMS,毕竟没尝试过`strapi`,而且 SSR 框架也会搭配`strapi`来使用,不需再自建后端。但就在我刷[官方模块](https://modules.nuxtjs.org/?category=CMS&version=3.x)的时候,无意间发现个官方模块 [content](https://content.nuxtjs.org/)。简单了解了一下,发现这个模块有点意思,并且能很简单的满足我当下的需求,于是就选择使用它。也可以使用官方提供的[codesandbox](https://codesandbox.io/s/github/nuxt/starter/tree/content)来尝试 + +不过`content`能实现的功能比较有限,没有`strapi`那么丰富,有多有限呢,基本的 CURD 只能实现查,无法增删改(至少官方文档是没有提供相应的函数)。不过`content`也不用像`strapi`那样自建一个服务,可以说是贼简洁了。 + +这里省略模块的导入的步骤,在根目录下创建 content 目录,目录下的文件可以是`markdonw`,`json`,`yaml`,`csv`。和 pages 一样,这里的文件都会映射对应的路由,不过这里需要映射的路由前缀是`/api/_content/query/`。举个例子 + +import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; + + + + +```json +{ + "title": "Hello Content v2!", + "description": "The writing experience for Nuxt 3", + "category": "announcement" +} +``` + + + + +```js +{ + _path: '/hello', + _draft: false, + _partial: false, + title: 'Hello Content v2!', + description: 'The writing experience for Nuxt 3', + category: 'announcement', + _id: 'content:hello.json', + _type: 'json', + _source: 'content', + _file: 'hello.json', + _extension: 'json' +} + +``` + + + + + +访问`/api/_content/query/hello`所得到的就是 output 的内容。 + +这里只演示 json 数据,是因为该项目主要用到 json 数据来渲染,如果是 markdown 的话,还有一些自带的组件 ContentDoc 来展示 markdown 数据。所提供的功能可以说非常适合用于文档类,或者博客类的站点。 + +回到该实战本身,来说明实际数据及其如何请求,上面的例子所对应的 api 文档数据如下 + +```json +{ + "id": "one", + "name": "一言", + "desc": "一言指的就是一句话,可以是动漫中的台词,也可以是网络上的各种小段子", + "path": "/api/one", + "method": "GET", + "params": [ + { + "name": "type", + "value": "json", + "type": "string", + "desc": "数据格式(text,json,img)", + "required": false + } + ], + "dataType": "text", + "example": "/api/one" +} +``` + +然后这些数据通过 content 提供的[queryContent()](https://content.nuxtjs.org/api/composables/query-content)来获取,这里来看其渲染页面`pages/apidoc/[id].vue`的部分代码 + +```vue title="pages/apidoc/[id].vue" + +``` + +获取到数据,然后渲染到 vue 上,这些就不过多叙述了。 + +### 接口限流 + +假设现在上线了这些接口,但是不做任何限制,那么调用方就可以无限次调用获取接口,这对服务器压力来说是十分巨大的,所以就需要对接口进行限流。 + +一般要做限流操作都需要涉及到中间件,在 Nuxt 中有[路由中间件](https://v3.nuxtjs.org/guide/directory-structure/middleware),和[服务中间件](https://v3.nuxtjs.org/guide/features/server-routes#server-middleware) ,这里由于是要处理后端接口的,所以就需要使用服务中间。 + +创建`server/middleware/limit.ts` 文件 + +```typescript title="server/middleware/limit.ts" +export default defineEventHandler(async event => { + console.log(`limit`) +}) +``` + +这时候,只要是 Fetch 请求都将打印`limit`,既然请求能拦截到,那限流就简单了(其实并不简单,因为这个 h3 的文档与相关库实在是少的可怜)。 + +不过由于没有使用到用户鉴权等功能(在这个项目中也没打算上),所以限流的操作只有从 IP 的手段下手。这里我选用的是[node-rate-limiter-flexible](https://github.com/animir/node-rate-limiter-flexible)这个库,下面是实现代码 + +```typescript title="server/middleware/limit.ts" +import { RLWrapperBlackAndWhite, RateLimiterMemory } from 'rate-limiter-flexible' + +const rateLimiter = new RLWrapperBlackAndWhite({ + limiter: new RateLimiterMemory({ + points: 1, + duration: 1, + }), +}) + +function getIP(req) { + return ( + (req.headers['x-forwarded-for'] as string) || (req.socket?.remoteAddress as string) + ).replace('::ffff:', '') +} + +export default defineEventHandler(async event => { + const { req, res } = event + + if (/^\/api\/[A-Za-z0-9].*/.test(req.url || '')) { + const ip = getIP(req) + + try { + await rateLimiter.consume(ip) + } catch (error) { + res.statusCode = 429 + return { statusCode: 429, statusMessage: '请求太快了,请稍后再试' } + } + } +}) +``` + +自行阅读代码即可,设置的限制是 1 秒内只能请求 1 条接口。 + +### 接口缓存 + +除了接口限流外,对于实时性不高的接口可以开启缓存,这样可以防止过度调用导致接口匮乏。并且对于重复调用的接口响应速度更快,性能更佳。 + +可 nuxt 的中间件好像只能拦截用户端发送的请求数据,而服务端发送的给用户端的数据貌似无法拦截,也就无法在中间件中获取到数据或者处理数据了? + +是的,nuxt 的服务层并不像[nest](https://nestjs.com/)有 Middleware(中间件),Guards(守卫),Interceptors(拦截器),而这里所要拦截的部分也就是 nest 中的 Interceptors。 + +![img](https://docs.nestjs.com/assets/Interceptors_1.png) + +不过 nuxt 只提供了中间件,这够实现接口缓存的功能了,不过需要一些“技巧”,关于这个技巧我写过的一篇文章 [JS 函数 hook](/blog/js-function-hook) 比较详细的介绍该技巧,这里简单说下。 + +假设有个 add 函数,我并不想破坏 add 的参数与内部代码结果,但是我又像在调用 add 函数时,查看传入的参数,以及计算的结果,那该如何做?来看下面代码 + +```javascript +function add(a, b) { + return a + b +} + +let original_add = add +add = function () { + console.log('arguments', arguments) + let result = original_add.apply(this, arguments) + console.log('result', result) + return result +} +``` + +首先重新定义了与 add 相同作用的函数,名为 original_add,然后将 add 修改,同时修改像成上面的代码。这时尝试调用 add 就可以发现输出了传入的参数及计算后的结果。 + +知道了这个修改 add 函数的技巧,要拦截 nuxt 的服务端数据也就不难了。只需要将这里的 add 函数替换成 http 框架的返回函数即可,也就是`res.end()`。大致逻辑如下 + +```typescript title="server/middleware/cache.ts" +export default defineEventHandler(async event => { + const { req, res } = event + + const original_res_end = res.end + res.end = function (...args: any) { + // 这里的args的第一个参数就是 res.end 调用的参数,即返回给客户端的数据 + console.log(args) + + // 最后可别忘了调用原始的 res.end,不然客户端一直处于等待状态 + return original_res_end.apply(this, args) + } +}) +``` + +这里所使用到的缓存库是[lru-cache](https://github.com/isaacs/node-lru-cache),其实现代码如下 + +```typescript title="server/middleware/cache.ts" +import type { ServerResponse } from 'h3' +import { defaultContentType } from 'h3' + +import LRU from 'lru-cache' + +const options = { + max: 500, + ttl: 1000 * 60 * 1, // 缓存1min + allowStale: false, + updateAgeOnGet: false, + updateAgeOnHas: false, +} +const cache = new LRU(options) + +export default defineEventHandler(async event => { + defaultContentType(event, 'text/plain; charset=utf-8') + + const { req, res } = event + if (/^\/api\/[A-Za-z0-9].*/.test(req.url || '')) { + const key = req.url + const cached = cache.get(key) + if (cached) return res.end(cached) + + const original_res_end = res.end + res.end = function (...args: any): ServerResponse { + const data = args?.[0] + if (data) { + cache.set(key, data) + } + + return original_res_end.apply(this, args) + } + } +}) +``` + +现在缓存是实现了,但所有的接口都被强行缓存 1 分钟,像有些接口(如随机图片)要是也这样设定,那就失去了这个接口的实时性了(我明明要随机,你却偏偏返回都是同一张图片)。所以就要对不同的接口进行不同的接口缓存处理,这里就可以使用到上下文 context。 + +定义接口代码 + +```typescript {2} title="server/api/test.ts" +export default defineEventHandler(async event => { + event.context.cache = { ttl: 1000 * 5 } // 缓存5s + + // ... 其他代码 ... +}) +``` + +定义缓存代码 + +```typescript title="server/middleware/cache.ts" +// ... 其他代码 ... +if (data) { + if (context.cache) { + const options = context.cache + + cache.set(key, data, options) + } else { + cache.set(key, data) + } +} +``` + +这样就可以为不同的接口,设置不同的缓存配置。(不过这样还是不够优雅,其实可以上装饰器的,但是想了想这也非 AOP 设计,于是就没尝试了) + +### 异常捕获 + +这个功能主要用途是有些接口可能失效了,就需要捕获这些异常接口信息然后停止或者修改该接口。如果要在每个接口上都定义 try catch,接口数量一多将难以维护,所以需要一个服务端全局异常捕获。 + +不过目前 Nuxt3 还不支持捕获服务端的异常,这里是[官网说明](https://v3.nuxtjs.org/guide/features/error-handling/#errors-during-api-or-nitro-server-lifecycle)。所以该功能暂时未实现,后续也有可能通过 Test 来测试接口可靠性,而不是全局捕获异常接口。 + +不过 Nuxt3 对客户端的错误处理做得比较好,有个[演示示例](https://v3.nuxtjs.org/examples/app/error-handling#error-handling)。 + +### 后续功能 + +由于 content 模块,以及 Nuxt3 后端服务的一些限制,导致一些功能就暂未实现,后续再考虑引入其他方案来实现 + +- [ ] 接口计次 +- [ ] 接口分类 +- [ ] 代码示例 +- [ ] ip 白名单 + +### 收集接口 + +就此整个项目的核心功能就已经实现完毕了,接下来要做的就是收集 api 接口,写 api 文档了。然而这部分也是最头疼的部分,因为在互联网上很难有免费的资源。 + +像大部分的 api 接口,如果数据来源不是自己的,名义上“免费”的,那大概率就是有限制,例如一天只能 100 条,1 分钟只能请求几次等等,而且这类接口多半是需要填写一个 app_ey 的参数。而需要登录才能获取,当然,你可以选择加钱来增加限额,那么就不再是免费的了。总之就是各种不方便 + +如果真想实现免费无限制,那么数据来源只能在自己身上,至于数据来源如何转化成自己的,懂得都懂好吧。 + +所以在本项目仅可能的收集一手文档的资源接口或是自行封装的功能接口,但也会存在一些调用别人封装过的接口,服务端的接口信息可自行在`server/api`中查看,由于一些接口的安全性而言,线上的部分接口代码并未公布,这很正常,因为我并不想泄露一些关键数据。 + +如果接口调用有涉及侵权相关的还请直接联系作者删除。 + +## 部署项目 + +本地打包 + +```bash +npm run build +``` + +等待打包完毕,将打包后生成的.output 文件夹放到服务器上(依赖都无需安装,.output 文件里有 node_modules),执行 + +```bash +node .output/server/index.mjs +``` + +即可运行项目,或者也可以使用 pm2,总之和常见的 node 部署没什么差异。 + +此外也可部署到云提供商,像 AWS,Netlify,Vercel 等,所支持的[服务商](https://v3.nuxtjs.org/guide/deploy/presets#supported-hosting-providers) + +## 坑点 + +### 打包失败 + +cherrio 中的 parse5 包无法打包至生成环境,提示如下 + +``` +WARN Could not resolve import "parse5/lib/parser/index.js" in ~\.pnpm\hast-util-raw@7.2.1\node_modules\hast-util-raw\lib\index.js using exports defined in ~\parse5\package.json. +``` + +我猜测是因为 hast-util-raw 包和 cheerio 的 parse5 冲突,而 nuxt 服务端的 nitro 在用 rollup 打包时没有将两者冲突部分合并,而是选择前者,这就导致生产环境下 cheerio 无法使用。我尝试搜索没有得到一个很好结果,而我的解决方案是降级 cherrio 版本至 0.22.0,因为这个版本中没有引入 parse5。 + +### 版本切换 + +在我最终准备上线的时候,发现 nuxt 又有新版本了,于是我将项目从 rc.4 升级到 rc.6,然后再次测试的时候,发现在动态路由页面切换的时候,无法正常的向后端发送请求,甚至都监听不到路由变化,相当于页面被缓存了。 + +其实这也侧面说明了,目前 Nuxt3 的兼容性是比较差的。 + +实际上还有一些,不过解决相对比较迅速,就没写上。 + +## 总结 + +体验了一周的 Nuxt3,整个的开发过程不敢说特别顺利,因为存在一定的兼容和 Bug。目前 Nuxt3 的目前还处于 rc 版,实际项目还得考虑上线。不过个人还是非常推荐 Nuxt 这个框架,在代码编写与开发体验上实在是太香了,不出意外后续的 web 项目都会采用 Nuxt3 来构建,期待正式版的发布。 diff --git "a/blog/project/Protocol \345\215\217\350\256\256\345\244\215\347\216\260\346\250\241\346\235\277.md" "b/blog/project/Protocol \345\215\217\350\256\256\345\244\215\347\216\260\346\250\241\346\235\277.md" new file mode 100644 index 0000000..f008c7a --- /dev/null +++ "b/blog/project/Protocol \345\215\217\350\256\256\345\244\215\347\216\260\346\250\241\346\235\277.md" @@ -0,0 +1,485 @@ +--- +slug: protocol-template +title: Protocol 协议复现模板 +date: 2022-10-30 +authors: kuizuo +tags: [project, protocol, template, nuxt] +keywords: [project, protocol, template, nuxt] +description: 一个用于快速复现请求协议的 Web 开发模板。基于 Nuxt 开发,并集成了NaiveUI,Unocss,等相关工具库封装。 +image: /img/project/protocol.png +--- + + + +## 为什么我要写这样的一个模板(网站) + +我曾经是做 API 请求的,经常要模拟某些请求(协议复现)。所以写过比较多的 api 请求代码,在此期间尝试编写过许多代码方式和软件形态。但都不令我满意,不是过于繁琐,就是开发太慢,都达不到我想要的预期。尤其是开发体验,可以说是苦不堪言。 + +就在前段时间接触了 SSR 框架(Nuxt3)与 Serverless Function,并用其写了一些项目,如 [api-service](https://github.com/kuizuo/api-service 'api-service') 。而[某了个羊刷次数的网站](https://7y8y.vercel.app)的实现,则让我意外发现这貌似就是我理想中的的协议复现最佳实现?于是我立马开启了 VSCode,将我的这一想法用代码的方式来实现出来,在经历了两周几乎不间断的开发,最终达到了我的预期效果! + +在 [模拟请求|协议复现方案](https://kuizuo.cn/request-protocol-scheme '模拟请求|协议复现方案') 这篇文章中我对协议复现的一些方案总结,而这篇就是对 SSR 框架方案的一个具体实践。 + +## 技术栈 + +这个模板基于[Nuxt3](https://v3.nuxtjs.org/)开发的,该框架拥有全栈开发能力(即全栈框架),并有诸多[模块](https://modules.nuxtjs.org/),即装即用。同时由于采用`Serverless Function` 方式来定义 api 接口,可以轻易地部署在自有服务器或[Vercel](https://vercel.com 'Vercel'), [Netlify](https://www.netlify.com/ 'Netlify')这样的平台上。由于要做到敏捷开发,该模板集成了[Naive UI](https://www.naiveui.com/ 'Naive UI') 组件库,组件库的质量足够胜任常规前端业务开发。此外还封装了一些我个人的所用到的工具库以提高开发效率。 + +为此我给这个模板起名 Protocol,即协议,也可以认为是礼仪。**一个用于快速复现请求协议的 Web 开发模板。** + +废话不多数,就正式来介绍下 [Protocol](https://github.com/kuizuo/protocol 'Protocol')。 + +## 目录结构 + +```bash +protocol +├── assets # 前端静态资源文件 +├── components # 组件 +├── composables # 组合式API +├── content # content 模块 +│ ├── changelog.md # 更新日志 +│ └── help.md # 帮助说明 +├── data # 持久化数据 +│ └── db +├── layouts # 布局 +├── nuxt.config.ts # nuxt 配置文件 +├── package.json # 依赖包 +├── pages # 页面 +├── plugins # 插件 +├── public # 服务端静态资源文件 +│ └── logo.svg +├── server # 服务端文件 +│ ├── api # 后端接口 +│ └── protocol # 协议请求逻辑代理 +├── stores # pinia 状态管理 +│ └── user.ts # 用户状态 +├── types # 类型定义 +│ └── user.d.ts # 用户类型声明文件 +├── ecosystem.config.js # pm2 配置文件 +├── nitro.config.ts # nitro 配置文件 +├── app.config.ts # 前端配置文件 +└── app.vue # 入口文件 + +``` + +从这个项目的目录结构中其实就可以看出,本项目是集成了**全栈**能力,并且使用 Vue 与 Node 来编写前端与后端,并**不会产生前后端分离的分割感**,只需要打开一个项目即可开始工作。这得益于[Nuxt3](https://v3.nuxtjs.org/ 'Nuxt3') 与 [Nitro](https://nitro.unjs.io/ 'Nitro')。 + +由于是基于 Nuxt3 开发的,所以使用该项目是需要一些 SSR 开发经验。如果你还没有接触 SSR,可以根据你熟悉的前端框架选择对应的 SSR 框架来尝试体验一番。~~都要 2023 年了,不会还有前端程序员没用过 SSR 框架吧。~~ + +## **基本功能** + +### 全栈开发 + +这里我不想过多介绍 Nuxt3 的基本功能与使用,在我的一个 [基于 Nuxt3 的 API 接口服务网站](https://kuizuo.cn/use-nuxt3-build-api-server#nuxt3-介绍 '基于Nuxt3的API接口服务网站') 的项目中,有简单介绍过 Nuxt3,有兴趣可以去看看。 + +这里你只需要知道 Nuxt3 具有全栈开发的能力,如果你想,完成可以基于 Nuxt3 这个技术栈来实现 Web 开发的前端后端工作。 + +### 类型提示 + +首先,最重要的就是类型提示,对于大多数 api 请求而言,类型往往常被忽略。这就导致不知道这个请求的提交参数、响应结果有什么数据字段。举个例子 + +![](https://img.kuizuo.cn/image_75GsdEZuLK.png) + +这是一个 post 请求用于实现登录的,但是这个响应数据 data 没有任何具体提示(这里的提示是 vscode 记录用户常输入的提示),这时候如果一旦拼接错误,就会导致某个数据没拿到,从而诱发 bug。同理提交的请求体 body 不做约束,万一这个请求还有验证码 code 参数,但是我没写上,那请求就会失败,这是就需要通过调试输出,甚至需要抓包比对原始数据包才能得知。 + +最主要的是没有类型约束的情况下,非常容易出现出现访问的对象属性不存在,做 js 开发的肯定经常遇到如下错误提示。 + +```javascript +Uncaught TypeError: Cannot read properties of undefined (reading 'data') +``` + +有太多很多时候就是因为没有类型,无形间诱发 bug。就极易造成开发疲惫,不愿维护代码,这也是很多做 api 接口都常常忽视的一点。包括我之前也是同样如此。 + +对于 js 而言,上述情况自然是解决不了,但这种场景对于 ts 来说在适合不过了。所以 Protocol 自然是集成了 ts,并且有良好的类型提示。下面展示几张开发时的截图就能体会到,当然你前提是得会 ts 或者看的懂 ts。 + +![](https://img.kuizuo.cn/image_VbEuizLRfz.png) + +上面的类型提示演示代码仅仅作为体现类型的好处,将类型定义(interface,type 等)和核心逻辑都在同一个文件自然不好,容易造成代码冗余。实际开发中,更多使用命名空间,将类型写到 ts 声明文件.d.ts 中。比如将上面的改写后如下 + +![](https://img.kuizuo.cn/image_48-YSpYd1g.png) + +![](https://img.kuizuo.cn/image_9b9ns2BM67.png) + +就在我写这篇文章做代码演示的时候,又发生了拼写错误,如下图。由于使用 ts 类型与 eslint,所以在开发时的问题我就能立马发现,而不是到了运行时才提示错误。 + +![](https://img.kuizuo.cn/image_PfpxCKZomB.png) + +**有了类型提示能非常有效的避免上述问题**。同时 ts 并不像 java 那样的强类型语言,你完全可以选择是否编写 ts 的类型(type 或 interfere),这由你决定,你乐意都可以将 typescript 写成 anyscript,因为确实有些人确实不喜欢写类型。 + +ts 的类型提示仅是其次,此外还配置了 eslint 对代码检查,让代码在 2 个空格缩进,无分号,单引号等代码规范下。保证代码质量,而不会出现这边一个分号,那边来个双引号的情况。 + +### 工具库 + +要想在实际项目中使用,还需要做很多功课,例如数据格式转换,编码,加解密,cookie 存储,IP 代理等等。这段时间也特此对常用工具封装成 npm 包,也就是 [@kuizuo/http](https://www.npmjs.com/package/@kuizuo/http) 与 [@kuizuo/utils](https://www.npmjs.com/package/@kuizuo/utils)。 + +大部分的代码我都会采用最新的 ECMAScript 标准来编写,目的也是为了简化代码,减少不必要的负担。 + +### 数据库 + +既然是全栈框架,那么必然少不了数据库的存取,[nitro](https://nitro.unjs.io/guide/introduction/storage 'nitro') 自然是提供了数据存储选择,即 [unjs/unstorage](https://github.com/unjs/unstorage#http-universal 'unjs/unstorage')。使用特别简单: + +```javascript +await useStorage().setItem('test:foo', { hello: 'world' }) +await useStorage().getItem('test:foo') +``` + +不指定则使用内存,当然了想要持久化配置,[nitro](https://nitro.unjs.io/guide/introduction/storage#defining-mountpoints 'nitro') 也提供了相关配置 + +```javascript title='nitro.config.ts' icon="logos:nuxt-icon" +// nitro.config.ts +import { defineNitroConfig } from 'nitropack' +export default defineNitroConfig({ + storage: { + redis: { + driver: 'redis', + /* redis connector options */ + }, + db: { + driver: 'fs', + base: './data/db', + }, + }, +}) +``` + +并根据不同前缀(根据 nitro.config.ts 中的 storage 对象的属性)存储在不同存储位置,如 + +```javascript +// 存内存缓存中 +await useStorage().setItem('cache:foo', { hello: 'world' }) +await useStorage().getItem('cache:foo') + +// 存db中 +await useStorage().setItem('db:foo', { hello: 'world' }) +await useStorage().getItem('db:foo') + +// 存redis中 +await useStorage().setItem('redis:foo', { hello: 'world' }) +await useStorage().getItem('redis:foo') +``` + +从目前来看,[unjs/unstorage](https://github.com/unjs/unstorage#http-universal 'unjs/unstorage')并没有提供 sql 数据库的方案。不过对于这类项目而言,似乎也没有上 sql 数据库的必要,文件和 redis 就足以了。如果需要也可以[自定义 drivers](https://github.com/unjs/unstorage#making-custom-drivers '自定义 drivers')。 + +:::warning 注意 + +由于 Vercel 是不支持文件读写的,所以想要文件方式数据存储功能就行不通,需要更换存储方案,比如远程 redis 数据库。 + +如果是部署到自由的服务器(通常是 Linux 系统),则还需要分配相应的读写权限。 + +::: + +### 用户凭证存储 + +通常来说,有两种用户凭证,Cookie 和 Token,有了上述数据存储的方案,存取用户凭证并不是什么难题。不过用户凭证更多的是用来鉴权的,这时候就需要配置前端[Middleware](https://v3.nuxtjs.org/guide/directory-structure/middleware#middleware-directory) 和后端 [Middleware](https://v3.nuxtjs.org/guide/directory-structure/server#server-middleware),至于选择哪种,根据实际网站情况来选择即可。 + +### 更新日志与帮助说明 + +我提供了两个 md 页面,更新日志(ChangeLog)和帮助说明(Usage),如果需要更新内容,在根目录下 `content` 文件夹中找到对应文件修改即可。 + +如果你想在创建新的 md 页面只需要在 content 中新建一个文件(如 test.md),在页面路由创建同名 vue 文件(test.vue),将下方的 path 修改相应文件名即可。 + +```vue title='pages/test.vue' icon='logos:vuejs' + + + +``` + +### 打包与部署 + +传统的 node 后端框架,通常需要将原文件或者打包后的文件放到服务器上,执行 `npm i` 下载 `package.json` 里的依赖文件,然后执行运行命令启动。这一步骤的下载依赖就尤为致命,因为通常下载依赖将会特别耗时。 + +但 Nuxt3 则是会将前后端的资源文件,打包到 `.output` 文件夹下,以本项目为例,打包的大小为 14.6MB,gzip 压缩为 3.11MB(写本章时的记录),如果不使用[Content](https://content.nuxtjs.org/) 模块体积将会更小。打包完成提示如下 + +```bash +Σ Total size: 14.6 MB (3.11 MB gzip) +√ You can preview this build using node .output/server/index.mjs +``` + +然后你只需要将 `.output` 整个文件夹放到服务器上,并且安装好 node 环境,输入 `node .output/server/index.mjs` 即可启动项目,默认端口为 3000。当然也可以通过 pm2 的配置文件来启动,配置文件如下 + +```javascript title='ecosystem.config.js' icon="logos:pm2-icon" +module.exports = { + apps: [ + { + name: 'Protocol', + exec_mode: 'cluster', + instances: '1', + env: { + NITRO_PORT: 8010, + NITRO_HOST: 'localhost', + NODE_ENV: 'production', + }, + script: './.output/server/index.mjs', + }, + ], +} +``` + +接着执行 `pm2 start ecosystem.config.js --env production` 即可运行。相比传统需要手动下载依赖的方式,Nuxt3 则是直接将 web 项目实际所需要的依赖都打包在一起,只需要在有 node 环境下机器中就可以立马运行,无需等待依赖下载。 + +如果部署在 Vercel 或 Netlify 就更轻松了,根据官方的步骤即可做到一键部署。 + +## **开发流程(形态)** + +介绍完工具库,如果不介绍下开发流程,很多人都不知道该如何起手,这里我会用 Github 的 api 作为案例演示,也就是模板源代码中所演示的那样。当然,后续我会根据一些实战项目考虑弄个案例展示(在写中),以来方便使用与完善该模板。毕竟如果开发者自己都不愿意用,又怎么去说服他人来使用呢。 + +### 修改内容 + +如何修改某文字内容或某图标,这里就不再赘述了,Ctrl + Shift + F 搜索你想修改的内容并修改即可。大部分能修改的配置都写在了 `app.config.ts` 下。 + +```javascript title='app.config.ts' icon="logos:nuxt-icon" +export default defineAppConfig({ + title: 'Protocol', + description: '一个用于快速复现请求协议的Web开发模板。', + author: { + name: 'kuizuo', + link: 'https://github.com/kuizuo', + qq: 'https://im.qq.com/', + wx: 'https://wx.qq.com/', + }, +}) +``` + +通过 `const appConfig = useAppConfig()` 获取配置对象数据。 + +### **定义协议复现逻辑代码(重要)** + +这里以调用 Github 的 api 为例,因为业务相对简单,所以使用的是静态方法来调用,简单展示一下代码 + +```javascript title='server/protocol/github/index.ts' +import { AHttp } from '@kuizuo/http' + +const http = new AHttp({ baseURL: 'https://api.github.com' }) + +export class Github { + + static async getUser(username: string) { + const { data } = await http.get(`/users/${username}`) + return data + } + + static async getRepos(username: string) { + const { data } = await http.get(`/users/${username}/repos`) + + return data + } +} + +``` + +我个人是习惯也喜欢将逻辑部分用 [class](https://es6.ruanyifeng.com/#docs/class) 的方式来编写,也推荐用这种去定义这些业务逻辑代码。这里我举个例子来说明,假设现在有一个博客网站,有登陆、获取博文列表、评论等功能。那么我会这么写 + +```javascript +import { AHttp } from '@kuizuo/http' + +interface User { + username: string + password: string +} + +export class Blog { + public http: AHttp + public user: User + + constructor(user: User) { + this.http = new AHttp({ baseURL: 'https://blog.kuizuo.cn' }) + this.user = User + } + + async login() { + // login logic code + } + + async getBlogList() { + // getBlogList logic code + } + + async comment(id: number) { + // comment logic code + } +} + +``` + +定义完这些后,我只需要实例化一个对象 account,调用 login 方法即可登录,后续的获取博文列表与评论操作我只需要拿这个 account 来操作即可。 + +```javascript +const account = new Blog({ username: 'kuizuo', password: '123456' }) +await account.login() + +const blogList = await account.getBlogList() + +await account.comment(1) +``` + +如果想换一个账号操作,就需要重新按照上面的方式实例化一个新的对象,拿这个对象操作即可。 + +并且这种方式在迁移代码的时候尤为方便,可以直接将这份代码放到不同的 Node 项目中来运行。 + +通常也是在这一流程中,会编写大量的类型代码,来完善整个项目,保证代码的健壮。通常我会在同文件下或者在 types 下定义 `.d.ts` 声明文件,通过声明文件与命名空间,无需导入即可全局使用类型。 + +### 定义后端数据接口 + +定义完复现协议的逻辑代码后,那么就到前后端数据交互部分了,首先定义后端的接口,由于上面我们已经定义好了协议复现逻辑代码,这边只需要导入使用即可。就像下面这样 + +```javascript title='server/api/uesr/[username].ts' +import { Github } from '~~/server/protocol/github' +import { ResOp } from '~~/server/utils' + +export default defineEventHandler(async event => { + const { username } = event.context.params + + const user = await Github.getUser(username) + + if (!user.login) return ResOp.error(404, user.message ?? 'User not found') + + await useStorage().setItem(`db:github:user:${username}`, user) + + return ResOp.success(user) +}) +``` + +这一部分的代码建立在 Serverless Function 上,每一个接口都是以函数的方式对外暴露出去。这些代码会根据文件位置生成对应的路由,比如说上面的文件为 `server/api/user/[username].ts`,就映射为 `/api/user/:username`,前端请求 `/api/user/kuizuo` 通过`event.context.params.username` 便可以拿到 username 的值为 kuizuo。 + +至此后端部分就暂以告告落。 + +### 定义前端状态管理 + +对于前端而言,肯定是需要全局管理一些数据状态的,这样能够在不同的组件间共享数据,并且需要持久化这些数据,以保证下次用户再次打开网页的时候无需向后台请求数据,pinia 持久化使用到了 [pinia-plugin-persistedstate](https://github.com/prazdevs/pinia-plugin-persistedstate) 插件。 + +同时在状态管理中,会定义一些方法来调用后端接口。如下演示 + +```javascript title='stores/user.ts' +import { useMessage } from 'naive-ui' + +export const useUserStore = definePiniaStore('user', () => { + const user = ref(null) + const repos = ref([]) + const message = useMessage() + + async function getUser(username: string) { + const { data } = await http.get(`/api/user/${username}`) + + if (data.login) { + user.value = data + message.success('获取成功') + } + else { + message.error(data.message) + } + } + + async function getRepos() { + const username = user.value?.login + const { data } = await http.get(`/api/repo/${username}`) + repos.value = data + } + + async function reset() { + user.value = null + repos.value = [] + } + + return { + user, + repos, + getUser, + getRepos, + reset, + } +}, { + persist: { + key: 'user', + }, +}) + +``` + +这里的 http 是经过封装的,因为返回数据格式如:`{"code":200,"data":{},"message":"success"}` ,但对于业务逻辑而言,我们通常只需要关注 `data` 里面的数据,而请求的状态 code 与信息 message 则不是所要着重关系的对象。 + +至于想要返回原数据,还是带有 code, message 的数据,因人而异,我更喜欢后者将数据格式规范化,这样我就能知道本次请求的状态结果,在响应拦截器中就能够进行预先处理。 + +在 vue 组件中只需要使用演示如下 + +```vue title='components\Demo.vue' + +``` + +### 编写前端页面与组件 + +这一部分自由发挥即可了,这里我是集成了 NaiveUI 与 Unocss,足够应对大部分的前端开发需求。没什么过多要说的了。 + +### 流程总结 + +整个开发流程就是这样的,如果我想要添加一个功能,用于获取 Github 用户已点的 Star 项目列表,那么按照上面流程将会清晰的实现出来。 + +这里仅举调用 Github api 为例,想调用其他第三方的 api 都不成问题,本模板只提供一个这样的开发流程(形态)能够帮助快速实现 Web 站点开发,同时极易部署,做到敏捷开发。 + +对比传统前后端分离的开发流程,这种开发流程可以说更加清晰,更加规范,更加高效。 + +## 一些问题 + +### 遇到图片防盗链怎么办? + +我的做法相对比较简单粗暴,直接在图片中添加`referrerpolicy='no-referrer'` 就像下面这样。 + +```html + +``` + +如果你想要集成到 HTML 或者 CSS ,可以直接在 `` 标签下添加如下代码. + +```html + +``` + +参阅[Referrer-Policy - HTTP | MDN (mozilla.org)](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Referrer-Policy 'Referrer-Policy - HTTP | MDN (mozilla.org)') + +### 跨域问题 + +几乎不会遇到跨域问题,因为所有的接口都相当于转发过一遍,不是由前端直接发送,而是后端接收到后,通过服务器来进行发送,然后将数据在返还给前端。 + +## 考虑做的 + +### 编写一个后台管理系统 + +这个模板如果要实现鉴权是相对比较简单的,前后端配置[Middleware](https://v3.nuxtjs.org/guide/directory-structure/middleware 'Middleware') 即可实现。使用 cookie 和 token 都随意,甚至第三方的登录。 + +但这时数据多了,难免需要去管理数据,不如专门为此编写一个后台管理系统,同时提供一个鉴权相关的功能。主要还是借助 ntrio 来开发,毕竟提供全栈开发能力,要实现只是时间开发的问题。 + +### 使用 tauri 编译跨平台程序 + +编译成跨平台程序有一个好处,就是所有的流量请求与接收都是存放在用户的机器中,就相当于传统的桌面应用开发。而部署在 Web 端请求流量的压力都将会来到服务器上,就避免不了用户量大,导致请求缓慢,甚至 ip 被封禁的问题。 + +由于我暂且还不会 tauri 开发,也还不会 rust,所以这个功能估计得到寒假才有可能去实现了。electron 占用比较大的资源空间,不作为跨平台框架优先选择。 + +## 写在最后 + +这种开发形态自打我接触协议复现到前端开发我就考虑过,但奈何在没接触 ssr 框架之前,这种开发形态多半是需要前后端分离,要么使用模板语言,这样接口交互方面将会十分繁琐,开发效率过于低效。 + +因此当我发觉 ssr 框架的可行性后,我几乎整整花费了两周的时间在不断的探索与完善中,希望将其编写成一个我日后随时都会用到的模板,即写即用,极速上线。因为这样的开发场景对我来说太过于常见了,而很多时间就是因为没有一个相应的模板与工具库,代码总是东凑西凑,后续维护与测试总是花费不小的时间去解决。 + +目前这种方案已有初步雏形,由于一些特殊的因素,我并未将已经写过的站点作为案例放在这上面作为演示,而将 Github api 作为演示,后续大概率会弄个案例展示供参考学习。 + +后续我还是会不断去完善与维护该项目,并基于该项目去重构我的一些项目。 diff --git "a/blog/project/nest-vben-admin\345\220\216\345\217\260\347\256\241\347\220\206\347\263\273\347\273\237.md" "b/blog/project/nest-vben-admin\345\220\216\345\217\260\347\256\241\347\220\206\347\263\273\347\273\237.md" new file mode 100644 index 0000000..cf27447 --- /dev/null +++ "b/blog/project/nest-vben-admin\345\220\216\345\217\260\347\256\241\347\220\206\347\263\273\347\273\237.md" @@ -0,0 +1,293 @@ +--- +slug: nest-vben-admin +title: nest-vben-admin后台管理系统 +date: 2022-05-08 +authors: kuizuo +tags: [project, admin, vue, nest] +keywords: [project, admin, vue, nest] +description: 一款基于 NestJs + TypeScript + TypeORM + Redis + MySql + Vben Admin 编写的一款前后端分离的权限管理系统 +image: /img/project/nest-vben-admin.png +--- + +当时初学 Web 开发的时候,除了写一个网页博客外,第二个选择无非就是一个后台管理系统,可以应用于多种需要数据管理类项目中。 + +基于**NestJs + TypeScript + TypeORM + Redis + MySql + Vben Admin**编写的一款前后端分离的权限管理系统 + +演示地址:[KzAdmin](https://admin.kuizuo.cn) 管理员账号:admin 密码:123456 + + + +![image-20220505171231754](https://img.kuizuo.cn/image-20220505171231754.png) + +## 前端 + +**基于[Vben Admin](https://vvbin.cn/doc-next/)开发,使用 Vue3、Vite、TypeScript 等最新技术栈,内置常用功能组件、权限验证、动态路由。** + +仓库地址:https://github.com/kuizuo/kz-vue-admin + +### [项目结构](https://vvbin.cn/doc-next/guide/#%E7%9B%AE%E5%BD%95%E8%AF%B4%E6%98%8E) + +```bash +├── build # 打包脚本相关 +│ ├── config # 配置文件 +│ ├── generate # 生成器 +│ ├── script # 脚本 +│ └── vite # vite配置 +├── mock # mock文件夹 +├── public # 公共静态资源目录 +├── src # 主目录 +│ ├── api # 接口文件 +│ ├── assets # 资源文件 +│ │ ├── icons # icon sprite 图标文件夹 +│ │ ├── images # 项目存放图片的文件夹 +│ │ └── svg # 项目存放svg图片的文件夹 +│ ├── components # 公共组件 +│ ├── design # 样式文件 +│ ├── directives # 指令 +│ ├── enums # 枚举/常量 +│ ├── hooks # hook +│ │ ├── component # 组件相关hook +│ │ ├── core # 基础hook +│ │ ├── event # 事件相关hook +│ │ ├── setting # 配置相关hook +│ │ └── web # web相关hook +│ ├── layouts # 布局文件 +│ │ ├── default # 默认布局 +│ │ ├── iframe # iframe布局 +│ │ └── page # 页面布局 +│ ├── locales # 多语言 +│ ├── logics # 逻辑 +│ ├── main.ts # 主入口 +│ ├── router # 路由配置 +│ ├── settings # 项目配置 +│ │ ├── componentSetting.ts # 组件配置 +│ │ ├── designSetting.ts # 样式配置 +│ │ ├── encryptionSetting.ts # 加密配置 +│ │ ├── localeSetting.ts # 多语言配置 +│ │ ├── projectSetting.ts # 项目配置 +│ │ └── siteSetting.ts # 站点配置 +│ ├── store # 数据仓库 +│ ├── utils # 工具类 +│ └── views # 页面 +├── test # 测试 +│ └── server # 测试用到的服务 +│ ├── api # 测试服务器 +│ ├── upload # 测试上传服务器 +│ └── websocket # 测试ws服务器 +├── types # 类型文件 +├── vite.config.ts # vite配置文件 +└── windi.config.ts # windcss配置文件 +``` + +### 启动项目 + +建议使用 pnpm 包管理器来管理 node 项目,使用`npm install -g pnpm`即可安装。 + +```bash +pnpm install + +pnpm run dev +``` + +运行结果 + +```bash + vite v2.9.5 dev server running at: + + > Network: https://192.168.184.1:3100/ + > Local: https://localhost:3100/ + + ready in 5057ms. +``` + +> 注: 开发环境下首次载入项目会稍慢(Vite 在动态解析依赖) + +更多关于前端项目规范可直接参考 [Vben Admin 文档 ](https://vvbin.cn/doc-next/guide/introduction.html),非常详细了。 + +## 后端 + +**基于 NestJs + TypeScript + TypeORM + Redis + MySql 编写的前后端分离权限管理系统** + +仓库地址:https://github.com/kuizuo/kz-nest-admin + +### [项目结构](https://blog.si-yee.com/sf-admin-cli/nest/usage.html#%E7%9B%AE%E5%BD%95%E7%BB%93%E6%9E%84%E8%AF%B4%E6%98%8E) + +```bash +|─setup-swagger.ts # Swaager文档配置 +|─main.ts # 主入口 +|─config # 配置文件 +|─shared +| |─redis # redisModule +| | |─redis.module.ts +| | |─redis.interface.ts +| | |─redis.constants.ts +| |─shared.module.ts +| |─services # 全局通用Provider +|─app.module.ts +|─mission +| |─mission.module.ts +| |─mission.decorator.ts # 任务装饰器,所有任务都需要定义该装饰器,否则无法运行 +| |─jobs # 后台定时任务定义 +|─common # 系统通用定义 +| |─dto # 通用DTO定义 +| |─contants +| | |─error-code.contants.ts # 系统错误码定义 +| | |─decorator.contants.ts # 装饰器常量 +| |─filters # 通用过滤器定义 +| |─interceptors # 通用拦截器定义 +| |─decorators # 通用装饰器定义 +| |─exceptions # 系统内置通用异常定义 +| |─class # Class Model 不使用Interface定义,使用Interface无法让Swagger识别 +|─modules +| |─admin +| | |─core # 核心功能 +| | | |─interceptors # 后台管理拦截器定义 +| | | |─decorators # 后台管理注解定义 +| | | |─provider # 后台管理提供者定义 +| | | |─guards # 后台管理守卫定义 +| | |─system # 系统模块定义 +| | |─account # 用户账户模块定义 +| | |─login # 登录模块定义 +| | |─admin.module.ts # 后台管理模块 +| | |─admin.constants.ts # 后台管理模块通用常量 +| | |─admin.interface.ts # Admin通用interface定义 +| |─ws # Socket模块 +|─entities # TypeORM 实体文件定义 +``` + +### 启动项目 + +依赖安装与执行打包命令前端与后端一致,但需要提前修改.env.development 中数据库相关配置,并执行 sql/init.sql 来初始化数据。 + +### 实现 + +项目中大部分的目录结构设计参照与[sf-nest-admin](https://github.com/hackycy/sf-nest-admin),但主要为了贴合自我的代码风格修改部分数据字段名,接口方法,接口响应格式等等。 + +同时对于大部分这类后台管理的 demo,通常都会定义用户,角色,菜单,部门。而我将部门相关代码删除,因为对于我后续项目大概率用不上这些部分,然后删了一些不相关的模块,主要写的这套模板还是用作自己后续的管理类项目。 + +#### 用户-角色-权限 + +这套系统中最为重要的一部分便是权限管理,不过在这套后台管理系统中这里的权限与菜单共用,前端路由渲染菜单,后端鉴权。后文的菜单表也就作为权限表而言。 + +在这三张表中关系如下(这里使用外键与数据库模型为例,实际项目并未用到外键,也不推荐使用) + +![image-20220508235534026](https://img.kuizuo.cn/image-20220508235534026.png) + +用户-角色 与 角色-权限都采用的多对多的关系,即新创建一个表用于映射两表关系。这些都属于 mysql 基础,不做过多赘述。 + +在权限管理中,最为重要的便是权限表了,由于这套后台管理系统中还涉及到前端的左侧菜单,所以将这里的 permission 表替换为 menu 表,字段 permission 表示权限值。数据库中的 menu 表如下 + +![image-20220508234343594](https://img.kuizuo.cn/image-20220508234343594.png) + +对于主要字段介绍: + +- **parent**:对于有父子关系的表,会创建一个 parent_id(这里为 parent)字段用于表示父节点,无则为顶级节点。 + +- **permission**:权限标识,根据后端接口而定,比如新增用户的 url 为`sys/user/add`,那么权限标识通常将/替换成:,也就是`sys:user:add`(主要防止和接口的 url 混淆)。 +- **type**:0 目录 1 菜单(前端组件) 2 权限,由于是菜单与权限混用,所以用 type 来区分。 +- **icon**:左侧菜单图标 +- **order_no**:左侧菜单排序 +- **component**:组件,目录为 LAYOUT,菜单则为对应组件,权限则无 + +有了这些数据,要做的是将他们拼接为**前端菜单管理**,**根据角色获取所有菜单**,**根据用户的所有权限**的树结构数据。 + +##### 前端菜单管理 + +获取所有的菜单列表数据,通过递归生成对应的菜单树结构,具体递归代码在`src/modules/core/permission/index.ts`中的`generatorMenu`方法中。 + +具体拼接数据过多,可自行打开控制台(F12)->网络 到菜单管理页中获取数据可得,这里便不做展示(后文拼接数据同理)。 + +##### 根据角色获取所有菜单 + +首先根据用户 id 找到该用户的所有角色 id,然后通过联表找到角色 id 所对应的菜单数据。 + +```typescript + /** + * 根据角色获取所有菜单 + */ + async getMenus(uid: number): Promise { + const roleIds = await this.roleService.getRoleIdByUser(uid); + let menus: SysMenu[] = []; + if (includes(roleIds, this.rootRoleId)) { + menus = await this.menuRepository.find({ order: { orderNo: 'ASC' } }); + } else { + menus = await this.menuRepository + .createQueryBuilder('menu') + .innerJoinAndSelect('sys_role_menu', 'role_menu', 'menu.id = role_menu.menu_id') + .andWhere('role_menu.role_id IN (:...roldIds)', { roldIds: roleIds }) + .orderBy('menu.order_no', 'ASC') + .getMany(); + } + + const menuList = generatorRouters(menus); + return menuList; + } +``` + +同样`generatorRouters`函数也在`src/modules/core/permission/index.ts`中。 + +##### 根据用户的所有权限 + +与上例一样,不过这里主要获取的是 permission 字段,所以在条件上添加了`menu.type = 2`与`menu.permission IS NOT NULL`,将 permission 拼接为一个数组。 + +```typescript + /** + * 获取当前用户的所有权限 + */ + async getPerms(uid: number): Promise { + const roleIds = await this.roleService.getRoleIdByUser(uid); + let permission: any[] = []; + let result: any = null; + if (includes(roleIds, this.rootRoleId)) { + result = await this.menuRepository.find({ + permission: Not(IsNull()), + type: 2, + }); + } else { + result = await this.menuRepository + .createQueryBuilder('menu') + .innerJoinAndSelect('sys_role_menu', 'role_menu', 'menu.id = role_menu.menu_id') + .andWhere('role_menu.role_id IN (:...roldIds)', { roldIds: roleIds }) + .andWhere('menu.type = 2') + .andWhere('menu.permission IS NOT NULL') + .getMany(); + } + if (!isEmpty(result)) { + result.forEach((e) => { + permission = concat(permission, e.permission.split(',')); + }); + permission = uniq(permission); + } + return permission; + } +``` + +permission 的值如 + +```json +["sys:user:add", "sys:user:delete", "sys:user:update", "sys:user:list", "sys:user:info"] +``` + +然后在 auth.guard.ts 守卫中获取 permission,然后每次请求需要鉴权的接口时,将权限标识转为接口 url,判断是否包含该 url,不包含则无访问权限。 + +在[菜单管理页](https://admin.kuizuo.cn/#/system/menu)中可操作菜单,具体可自测。 + +至此,菜单表的数据被拆分为这 3 部分数据,以实现权限管理,动态路由的目的。 + +#### 其他文档 + +你可以访问 [https://admin.kuizuo.cn/swagger-ui](https://admin.kuizuo.cn/swagger-ui 'https://admin.kuizuo.cn/swagger-ui') 来查看 nest-vben-admin 的 Swagger 文档 + +json 格式为 [https://admin.kuizuo.cn/swagger-ui/json](https://admin.kuizuo.cn/swagger-ui/json 'https://admin.kuizuo.cn/swagger-ui/json'),用于导入 ApiFox 中。 + +ApiFox 在线链接: [https://www.apifox.cn/apidoc/shared-7a07def2-5b82-4c71-bf57-915514f61f25](https://www.apifox.cn/apidoc/shared-7a07def2-5b82-4c71-bf57-915514f61f25 'https://www.apifox.cn/apidoc/shared-7a07def2-5b82-4c71-bf57-915514f61f25') 访问密码: nest-vben-admin + +## 写后感 + +其实一年多前我就想写一套相对完善的后台管理系统的模板,供自己后续的一些项目中使用。然而迟迟没有动手写套模板,而是不断根据业务需求,修修改改写了一套乱七八糟的代码,以至于维护的时候究极痛苦。就在不久前正好也用到,然而也是把之前写的屎山一样的代码拿来修改。 + +**我所遇到的问题:项目结构乱,代码风格乱,维护代码极其折磨** + +所以今年寒假于是准备完善这套模板,然而当时只是创建完工程结构,到现在才正式把功能实现以及测试相关,部署搞定。说真的,非常拖延,甚至都快让我放弃写这个模板的打算。但拖也对我有一定的好处,为什么这么说?因为当时有这个想法时,市面上关于这套技术栈的实现还很少,而等我寒假再去搜索相关实现的时候,却有相关开源的代码,而这便可供我学习,使项目更加完善。 + +回顾整体项目的编写过程,所花费的时间可能一个月不到,甚至更少,但往往就是各种各样的拖延导致项目逾期,或者是学习某个技术栈。难以将精力集中起来完成任务,至于原因,也许是目标过于庞大,或许是日常生活中的各种琐事,不过我想多半是自我的懒惰。 diff --git "a/blog/project/\345\206\231\344\270\200\344\270\252VSCode\346\211\251\345\261\225.md" "b/blog/project/\345\206\231\344\270\200\344\270\252VSCode\346\211\251\345\261\225.md" new file mode 100644 index 0000000..0957e22 --- /dev/null +++ "b/blog/project/\345\206\231\344\270\200\344\270\252VSCode\346\211\251\345\261\225.md" @@ -0,0 +1,745 @@ +--- +slug: vscode-extension +title: 写一个 VSCode 扩展 +date: 2022-07-11 +authors: kuizuo +tags: [vscode, plugin, extension, develop] +keywords: [vscode, plugin, extension, develop] +description: 编写个人定制化的 VSCode 扩展,并将其发布到应用商店中。 +image: /img/project/vscode-extension.png +--- + +自从使用过 VSCode 后就再也离不开 VSCode,其轻量的代码编辑器与诸多插件让多数开发者爱不释手。同样我也不例外,一年前的我甚至还特意买本《Visual Studio Code 权威指南》的书籍,来更进一步了解与使用。 + +在购买这本书时就想写一个 vscode 插件(扩展),奈何当时事务繁忙加之不知做何功能,就迟迟未能动手。如今有时间了,就顺带体验下 vscode 扩展开发,并记录整个开发过程。 + +扩展地址:[VSCode-extension](https://marketplace.visualstudio.com/items?itemName=kuizuo.vscode-extension-sample 'VSCode-extension') + +开源地址:[kuizuo/vscode-extension (github.com)](https://github.com/kuizuo/vscode-extension) + + + +## Vscode 相关 + +[vscode 应用商店](https://marketplace.visualstudio.com/vscode 'vscode应用商店') + +[vscode 插件官方文档](https://code.visualstudio.com/api 'vscode插件官方文档') + +[vscode 官方插件例子](https://github.com/microsoft/vscode-extension-samples 'vscode 官方插件例子') + +关于 Vscode 及其插件就不过多介绍,相信这篇文章 [VSCode 插件开发全攻略(一)概览 - 我是小茗同学 - 博客园](https://www.cnblogs.com/liuxianan/p/vscode-plugin-overview.html 'VSCode插件开发全攻略(一)概览 - 我是小茗同学 - 博客园')能告诉你 Vscode 插件的作用。 + +## 工具准备 + +:::tip + +**在开发前,建议关闭所有功能性扩展,以防止部分日志输出与调试效率**。 + +::: + +### vscode 插件脚手架 + +vscode 提供插件开发的脚手架 [vscode-generator-code](https://github.com/Microsoft/vscode-generator-code 'vscode-generator-code') 来生成项目结构,选择要生成的类型 + +```bash +? ========================================================================== +We're constantly looking for ways to make yo better! +May we anonymously report usage statistics to improve the tool over time? +More info: https://github.com/yeoman/insight & http://yeoman.io +========================================================================== Yes + + _-----_ ╭──────────────────────────╮ + | | │ Welcome to the Visual │ + |--(o)--| │ Studio Code Extension │ + `---------´ │ generator! │ + ( _´U`_ ) ╰──────────────────────────╯ + /___A___\ / + | ~ | + __'.___.'__ + ´ ` |° ´ Y ` + +? What type of extension do you want to create? (Use arrow keys) +> New Extension (TypeScript) + New Extension (JavaScript) + New Color Theme + New Language Support + New Code Snippets + New Keymap + New Extension Pack + New Language Pack (Localization) + New Web Extension (TypeScript) + New Notebook Renderer (TypeScript) +``` + +根据指示一步步选择,这里省略勾选过程,最终生成的项目结果如下 + +![](https://img.kuizuo.cn/image_StiMqQrFCi.png) + +### 运行 vscode 插件 + +既然创建好了工程,那必然是要运行的。由于我这里选择的 ts + webpack 进行开发(视情况勾选 webpack),所以是需要打包,同时脚手架已经生成好了对应.vscode 的设置。只需要按下 F5 即可开始调试,这时会打开一个新的 vscode 窗口,`Ctrl+Shift+P`打开命令行,输入`Hello World`,右下角弹出提示框`Hello World from kuizuo-plugin!` + +:::warning + +注意: 由于是 webpack 开发,在调用堆栈中可以看到有两个进程,一个是 webpack,另一个是新开的插件窗口的,同时在该调试窗口也能查看调试输出信息。 + +![](https://img.kuizuo.cn/image_Yv4X32qLE5.png) + +**切记一定要等到第二个调试进程加载完毕**(时间根据电脑性能而定),再打开命令行输入 Hello World 才会有命令,否则会提示 没有匹配命令。 + +::: + +至此,一个 vscode 的开发环境就已经搭建完毕,接下来就是了解项目结构,以及 vscode 插件的 api 了。 + +### 代码解读 + +```typescript title="extension.ts" +import * as vscode from 'vscode' + +export function activate(context: vscode.ExtensionContext) { + let disposable = vscode.commands.registerCommand('kuizuo-plugin.helloWorld', () => { + vscode.window.showInformationMessage('Hello World from kuizuo-plugin!') + }) + + context.subscriptions.push(disposable) +} + +export function deactivate() {} +``` + +`vscode.commands.registerCommand`用于注册命令,`kuizuo-plugin.helloWorld` 为命令 ID,在后续`package.json`中要与之匹配。第二个参数为一个回调函数,当触发该命令时,弹出提示框。 + +在 package.json 中关注 activationEvents 与 contributes + +```json title="package.json" +{ + "activationEvents": ["onCommand:kuizuo-plugin.helloWorld"], + "contributes": { + "commands": [ + { + "command": "kuizuo-plugin.helloWorld", + "title": "Hello World" + } + ] + } +} +``` + +activationEvents 激活事件,`onCommand:kuizuo-plugin.helloWorld`中`kuizuo-plugin`是插件 ID 要与 extension.ts 中的注册命令匹配,`helloWorld`则是命令标识,而 onCommand 则是监听的类型,此外还有`onView`、`onUri`、`onLanguage`等等。 + +contributes 则是配置那些地方来显示命令,像官方的例子中,就是在 Ctrl + Shift + P 命令行中输入 Hello World 来调用`kuizuo-plugin.helloWorld` 命令。此外还可以设置按键与菜单 + +```json title="package.json" +"keybindings": [ + { + "command": "kuizuo-plugin.helloWorld", + "key": "ctrl+f10", + "mac": "cmd+f10", + "when": "editorTextFocus" + } + ], + "menus": { + "editor/context": [ + { + "when": "editorFocus", + "command": "kuizuo-plugin.helloWorld", + "group": "navigation" + } + ] + } +``` + +设置完毕后,可以按 Ctrl + Alt + O 或者命令行中键入 reload 来重启 vscode + +:::danger + +这里也要注意,如果重启后并无生效,请查看 package.json 是否配置正确(多一个逗号都不行),或者尝试重新调试。如果还不行,那么很有可能就是代码报错,但日志输出并没有,那么在弹出的新窗口中打开开发人员工具(Ctrl+Alt+I 或帮助 → 切换开发人员工具),这里有报错相关的提示信息。 + +建议查看[VSCode 插件开发全攻略(六)开发调试技巧](https://www.cnblogs.com/liuxianan/p/vscode-plugin-develop-tips.html 'VSCode插件开发全攻略(六)开发调试技巧') + +::: + +## 功能 + +### 首次启动弹窗与配置项 + +先说首次启动弹窗的实现,要实现该功能,肯定要保证插件在 VSCode 一打开就运行,而这取决于 vscode 触发插件的时机,也就是 activationEvents,所以`activationEvents`需要设置成`onStartupFinished`。想要更高的优先级,可以选择 `*` (但官方不建议,除非其他事件无法实现的前提下),这里为了演示就使用`*`。 + +其实现代码主要调用 `vscode.window.showInformationMessage` 函数如下 + +```typescript title="extension.ts" +import * as vscode from 'vscode' +import { exec } from 'child_process' + +export function activate(context: vscode.ExtensionContext) { + vscode.window + .showInformationMessage('是否要打开愧怍的小站?', '是', '否', '不再提示') + .then(result => { + if (result === '是') { + exec(`start 'https://kuizuo.cn'`) + } else if (result === '不再提示') { + // 其他操作 后文会说 + } + }) +} +``` + +此时重启窗口,就会有如下弹窗显示 + +![](https://img.kuizuo.cn/image_9oqLzZl-wE.png) + +但如果你是 mac 用户的话,你会发现无法打开,其原因是 window 下打开链接的指令是 start,而 mac 则是 open,所以需要区分不同的系统。要区分系统就可以使用 node 中的 os 模块的 platform 方法获取系统,如下(省略部分代码) + +```typescript +import * as os from 'os' + +const commandLine = os.platform() === 'win32' ? `start https://kuizuo.cn` : `open https://kuizuo.cn` +exec(commandLine) +``` + +当然了,当用户选择不再提示的时候,下次再打开 vscode 就别提示了,不然大概率就是卸载插件了。这里就需要设置全局参数了,在 package.json 中 contributes 设置 configuration,具体如下,注意`kuizuoPlugin.showTip` 为全局参数之一 + +```json title="package.json" +"contributes": { + "configuration": { + "title": "kuizuo-plugin", + "properties": { + "kuizuoPlugin.showTip": { + "type": "boolean", + "default": true, + "description": "是否在每次启动时显示欢迎提示!" + } + } + } +} +``` + +该参数可以在设置 → 扩展中找到`kuizuo-plugin`插件来手动选择,也可以是通过 api 来修改 + +![](https://img.kuizuo.cn/image_teNrxe9D9O.png) + +然后读取`vscode.workspace.getConfiguration().get(key)`和设置该参数`vscode.workspace.getConfiguration().update(key, value)` + +```typescript title="extension.ts" +export async function activate(context: vscode.ExtensionContext) { + const key = 'kuizuoPlugin.showTip' + const showTip = vscode.workspace.getConfiguration().get(key) + if (showTip) { + const result = await vscode.window.showInformationMessage( + '是否要打开愧怍的小站?', + '是', + '否', + '不再提示', + ) + if (result === '是') { + const commandLine = + os.platform() === 'win32' ? `start https://kuizuo.cn` : `open https://kuizuo.cn` + exec(commandLine) + } else if (result === '不再提示') { + //最后一个参数,为true时表示写入全局配置,为false或不传时则只写入工作区配置 + await vscode.workspace.getConfiguration().update(key, false, true) + } + } +} +``` + +即便是调试状态下,重启也不会影响全局参数。最终封装完整代码查看源码,这里不再做展示了。 + +### 右键资源管理器(快捷键)新建测试文件 + +我日常开发中写的最多的文件就是 js/ts 了,有时候就会在目录下创建 demo.js 来简单测试编写 js 代码,那么我就要点击资源管理器,然后右键新建文件,输入 demo.js。于是我想的是将该功能**封装成快捷键**的方式,当然右键也有**新建测试文件**这一选项。 + +![](https://img.kuizuo.cn/image_3SRybBGaF1.png) + +功能其实挺鸡肋的,也挺高不了多少效率,这里可以说**为了演示和测试这个功能而实现**。 + +总之前面这么多废话相当于铺垫了,具体还是看功能实现吧。 + +首先就是注册命令,具体就不解读代码了,其逻辑就是获取调用`vscode.window.showQuickPick`弹出选择框选择 js 还是 ts 文件(自定义),接着获取到其目录,判断文件是否存在,创建文件等操作。 + +```typescript title="extension.ts" +import * as vscode from 'vscode' +import * as fs from 'fs' + +export async function activate(context: vscode.ExtensionContext) { + let disposable = vscode.commands.registerCommand('kuizuo-plugin.newFile', (uri: vscode.Uri) => { + vscode.window.showQuickPick(['js', 'ts'], {}).then(async item => { + if (!uri?.fsPath) { + return + } + + const filename = `${uri.fsPath}/demo.${item}` + if (fs.existsSync(filename)) { + vscode.window.showErrorMessage(`文件${filename}已存在`) + return + } + + fs.writeFile(filename, '', () => { + vscode.window.showInformationMessage(`demo.${item}已创建`) + vscode.window.showTextDocument(vscode.Uri.file(filename), { + viewColumn: vscode.ViewColumn.Two, // 显示在第二个编辑器窗口 + }) + }) + }) + }) + + context.subscriptions.push(disposable) +} + +export function deactivate() {} +``` + +然后再 keybindins 中添加一条 + +```json title="package.json" +"keybindings": [ + { + "command": "kuizuo-plugin.newFile", + "key": "shift+alt+n", + } +], +``` + +然后就当我实现完功能的时候,我在想**自带的新建文件是不是就是个 command?只是没有绑定快捷键?** 于是我到键盘快捷方式中找到答案 + +![](https://img.kuizuo.cn/image_nQu3Y8DWSw.png) + +图中的`explorer.newFile`就是资源管理器右键新建文件的命令,只是没有键绑定。所以我只需要简单的加上`shift + alt + n`即可实现我一开始想要的快捷键功能,此时再次右键资源管理器新建文件右侧就有对应的快捷键。 + +此时的我不知该哭该笑,折腾半天的功能其实只是设置个快捷键的事情。 + +:::note + +这些命令在 vscode 中作为内置命令[Built-in Commands](https://code.visualstudio.com/api/references/commands 'Built-in Commands')。要查看 vscode 所有命令的话,也可以通过`vscode.commands.getCommands` 来获取所有命令 ID,要在插件中执行也只需要调用`vscode.commands.executeCommand(id)` + +::: + +### 键盘快捷键(光标移动) + +接着我就在想,既然很多 vscode 功能都是命令的形式,那是不是在插件级别就能做键盘映射,而不用让用户在 vscode 设置,很显然是可以的。只需要在 package.json 中 contributes 的 keybindings 中设置,就可以实现组合键来进行光标的移动。下面是我给出的答案 + +```json title="package.json" +"keybindings": [ + { + "command": "cursorUp", + "key": "shift+alt+i", + "when": "textInputFocus" + }, + { + "command": "cursorDown", + "key": "shift+alt+k", + "when": "textInputFocus" + }, + { + "command": "cursorLeft", + "key": "shift+alt+j", + "when": "textInputFocus" + }, + { + "command": "cursorRight", + "key": "shift+alt+l", + "when": "textInputFocus" + }, + { + "command": "cursorHome", + "key": "shift+alt+h", + "when": "textInputFocus" + }, + { + "command": "cursorEnd", + "key": "shift+alt+;", + "when": "textInputFocus" + } + ] +``` + +![](https://img.kuizuo.cn/image_SnnPUABJN5.png) + +仔细看右侧来源就可以知道是没问题的,第一个为我之前设置的,而扩展则是通过上面的方法。 + +### 自定义扩展工作台 + +在 vscode 中有几个地方可以用于扩展,具体可看[Extending Workbench | Visual Studio Code Extension API](https://code.visualstudio.com/api/extension-capabilities/extending-workbench#status-bar-item 'Extending Workbench | Visual Studio Code Extension API') + +![](https://code.visualstudio.com/assets/api/extension-capabilities/extending-workbench/workbench-contribution.png) + +- 左侧图标(活动栏):主要有资源管理器、搜索、调试、源代码管理、插件 + +- 编辑器右上角:代码分栏、code runner 的运行图标 + +- 底部(状态栏):git、消息、编码等等 + +在 contributes 添加 viewsContainers 与 views,注意,views 的属性要与 viewsContainers 的 id 对应。 + +```json title="package.json" +"viewsContainers": { + "activitybar": [ + { + "id": "demo", + "title": "愧怍", + "icon": "public/lollipop.svg" + } + ] +}, +"views": { + "demo": [ + { + "id": "view1", + "name": "视图1" + }, + { + "id": "view2", + "name": "视图2" + } + ] +} +``` + +编辑器右上角是在 menus 中设置 editor/title,图标则是对应命令下设置,不然就是显示文字 + +```json title="package.json" +"commands": [ + { + "command": "kuizuo-plugin.helloWorld", + "title": "Hello World", + "icon": { + "light": "public/lollipop.svg", + "dark": "public/lollipop.svg" + } + } +], +"menus": { + "editor/title": [ + { + "when": "resourceLangId == javascript", + "command": "kuizuo-plugin.helloWorld", + "group": "navigation" + } + ], +} +``` + +至于底部状态栏,这里借用官方例子[vscode-extension-samples/statusbar-sample at main · microsoft/vscode-extension-samples (github.com)](https://github.com/microsoft/vscode-extension-samples/tree/main/statusbar-sample 'vscode-extension-samples/statusbar-sample at main · microsoft/vscode-extension-samples (github.com)'),最终效果如下 + +![](https://img.kuizuo.cn/image_yQRsMkT6f5.png) + +那个 🍭 就是所添加的图标,不过并不实际功能,这里只是作为展示。 + +### 自定义颜色、图标主题 + +在 vscode 中分别有三部分的主题可以设置 + +| 主题 | 范围 | 推荐 | +| --- | --- | --- | +| 文件图标主题 | 资源管理器内的文件前的图标 | [Material Icon Theme](https://marketplace.visualstudio.com/items?itemName=PKief.material-icon-theme) | +| 颜色主题 | 代码编辑器以及整体颜色主题 | [One Dark Pro](https://marketplace.visualstudio.com/items?itemName=zhuangtongfa.Material-theme) | +| 产品图标主题 | 左侧的图标 | [Carbon Product Icons](https://marketplace.visualstudio.com/items?itemName=antfu.icons-carbon) | + +不过关于主题美化就不做深入研究,上面所推荐的就已经足够好看,个人目前也在使用。 + +### 代码片段 + +代码片段,也叫`snippets`,相信大家都不陌生,就是输入一个很简单的单词然后一回车带出来很多代码。平时大家也可以直接在 vscode 中创建属于自己的`snippets` + +代码片段相对比较简单,这里就简单跳过了 + +### xxx.log → console.log(xxx)包装 + +功能描述:在一个变量后使用.log,即可转化为 console.log(变量)的形式就像 `xxx.log => console.log('xxx', xxx)` 有点像 idea 中的`.sout` + +这里我把 [jaluik/dot-log](https://github.com/jaluik/dot-log) 这个插件的实现逻辑给简化了,这里先给出基本雏形 + +```typescript title="extension.ts" +import * as vscode from 'vscode' + +class MyCompletionItemProvider implements vscode.CompletionItemProvider { + constructor() {} + + // 提供代码提示的候选项 + public provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) { + const snippetCompletion = new vscode.CompletionItem('log', vscode.CompletionItemKind.Operator) + snippetCompletion.documentation = new vscode.MarkdownString('quick console.log result') + + return [snippetCompletion] + } + + // 光标选中当前自动补全item时触发动作 + public resolveCompletionItem(item: vscode.CompletionItem) { + return null + } +} + +export function activate(context: vscode.ExtensionContext) { + const disposable = vscode.languages.registerCompletionItemProvider( + ['html', 'javascript', 'javascriptreact', 'typescript', 'typescriptreact', 'vue'], + new MyCompletionItemProvider(), + '.', // 注册代码建议提示,只有当按下“.”时才触发 + ) + + context.subscriptions.push(disposable) +} +``` + +在 vscode 插件中通过`vscode.languages.registerCompletionItemProvider`提供像补全,代码提示等功能,第一个参数为所支持的语言,第二个参数为提供的服务`vscode.CompletionItemProvider` 这里只是封装成类的形式,目的是为了保存一些属性,例如光标位置 position,也可以传递对象形式 `{ provideCompletionItems, resolveCompletionItem }` ,第三个参数则是触发的时机。 + +`provideCompletionItems` 需返回一个数组,成员类型为`vscode.CompletionItem`,可通过`new vscode.CompletionItem()`来创建。 + +当你尝试运行上述代码时,会发现在任何值后面输入`.`都会有`log`提示。 + +![](https://img.kuizuo.cn/image_-ZCy88xVyq.png) + +但是点击后只是满足了代码补全的功能,而选择 log 选项后所要执行的操作则是在 `resolveCompletionItem` 中实现,这里仅仅只是返回一个 null,即只有简单的补全功能,这里对整个过程进行描述(可以自行下个断点调试查看):。 + +1. 当输入`.`时,程序进入到`provideCompletionItems` 函数内,这里可以获取到当前正在编辑的代码文档(文件名,代码内容)对应第一个参数,以及光标所在位置也就是第二个参数。还有其他参数,但这里用不到。具体可看[CompletionItemProvider](https://code.visualstudio.com/api/references/vscode-api#CompletionItemProvider%3CT%3E 'CompletionItemProvider') + +2. 选择完毕后,便会进入到 resolveCompletionItem 里面,这里可以获取到用户所选的选项内容,然后执行一系列的操作。 + +要做代码替换的话就需要注册文本编辑命令`vscode.commands.registerTextEditorCommand` ,内容如下 + +```typescript title="extension.ts" +const commandId = 'kuizuo-plugin.log' +const commandHandler = ( + editor: vscode.TextEditor, + edit: vscode.TextEditorEdit, + position: vscode.Position, +) => { + const lineText = editor.document.lineAt(position.line).text + // match case name.log etc. + const matchVarReg = new RegExp(`\(\[^\\s\]*\[^\'\"\`\]\).${'log'}$`) + // match case 'name'.log etc. /(['"`])([^'"])\1.log/ + const matchStrReg = new RegExp(`\(\[\'\"\`\]\)\(\[^\'\"\`\]*\)\\1\.${'log'}$`) + let matchFlag: 'var' | 'str' = 'var' + let text, + key, + quote = "'", + insertVal = '' + ;[text, key] = lineText.match(matchVarReg) || [] + if (!key) { + ;[text, quote, key] = lineText.match(matchStrReg) || [] + matchFlag = 'str' + } + // if matched + if (key) { + const index = lineText.indexOf(text) + edit.delete( + new vscode.Range( + position.with(undefined, index), + position.with(undefined, index + text.length), + ), + ) + + if (matchFlag === 'var' && key.includes("'")) { + quote = '"' + } + // format like console.log("xxx", xxx) + if (matchFlag === 'var') { + // only console.log(xxx) + insertVal = `${'console.log'}(${key})` + } + // if key is string format like console.log("xxx") + if (matchFlag === 'str') { + insertVal = `${'console.log'}(${quote}${key}${quote})` + } + + edit.insert(position.with(undefined, index), insertVal) + } + + return Promise.resolve([]) +} +context.subscriptions.push(vscode.commands.registerTextEditorCommand(commandId, commandHandler)) +``` + +`registerTextEditorCommand`不同于`registerCommand`,它只针对编辑器的命令,例如可以删除代码中的某个片段,增加代码等等。上面的代码就是为了找到.log 前(包括.log)匹配的代码,进行正则提取,然后调用 edit.delete 删除指定范围,再调用 edit.insert 来插入要替换的代码,以此达到替换的效果。 + +命令注册完毕了就需要调用了,也就到了 resolveCompletionItem 的时机 + +```typescript title="extension.ts" + public resolveCompletionItem(item: vscode.CompletionItem) { + const label = item.label + if (this.position && typeof label === 'string') { + item.command = { + command: 'kuizuo-plugin.log', + title: 'refactor', + arguments: [this.position.translate(0, label.length + 1)], // 这里可以传递参数给该命令 + } + } + + return item + } +``` + +将命令赋值给 item.command,会自动调用其 command 命令,同时把 arguments 参数传入给 command。最终达到替换的效果。 + +#### Position + +这里要说下 vscode 编辑器中的 Position,了解这个对代码替换、代码定位、代码高亮有很大帮助。 + +position 有两个属性`line`和`character`,对应的也就是行号和列号(后文以`line`和`character` 为称),和 **都是从 0 开始算起,而在 vscode 自带的状态栏提示中则是从 1 开始算起**,这两者可别混淆了。 + +其中 position 有如下几个方法 + +**position.translate** + +根据当前坐标计算,例如当前 position 的 line 0,character1。`position.translate(1, 1)` 得到 line 1,character 2,这不会改变远 position,这很好理解。但如果计算后得到的 line 与 character 有一个为负数则直接报错。 + +**position.with** + +从自身创建一个新的 postion 对象 + +#### Range + +知道了坐标信息,那么就可以获取范围了。可以通过 new vscode.Range() 来截取两个 position 之间的内容,得到的是一个 对象,有 start 与 end 属性,分别是传入的两个 position。 + +同样的 Range 和 Postion 方法都一致,这里就不多叙述了,可查看其声明文件。知道范围就可以通过 editor 来获取范围内的代码或是 edit 来删除代码等操作。 + +知道了这些内容,再看上面的代码也不难理解了。 + +### 选中变量并打印 console.log + +这里在补充一个功能:选中一个变量的时候,按下快捷键在下方添加`console.log(变量)`,相关插件 [Turbo Console Log](https://marketplace.visualstudio.com/items?itemName=ChakrounAnas.turbo-console-log 'Turbo Console Log') + +补:只有编辑器有光标的情况下会传入当前光标属性 position,选中状态下是不会传入 postion 属性,而是要通过`editor.selection`来获取选中内容,是一个 Selection 对象。 + +```typescript title="extension.ts" +context.subscriptions.push( + vscode.commands.registerTextEditorCommand( + 'kuizuo-plugin.insertLog', + (editor: vscode.TextEditor, edit: vscode.TextEditorEdit) => { + // 获取选中代码 在其下方插入 console.log(xxx) + const { selection, selections } = editor + // 选中多个代码时 + if (selections.length > 1) { + return + } + + // 如果不是当行代码 + if (!selection.isSingleLine) { + return + } + + const value = editor.document.getText(selection) + const insertVal = `${os.EOL}${'console.log'}('${value}', ${value})` + + edit.insert(editor.selection.end, insertVal) + editor.selection = new vscode.Selection(editor.selection.end, editor.selection.end) // 重置选中区域 + return Promise.resolve([]) + }, + ), +) +``` + +### 悬停提示 + +这里也一笔带过,具体可看 hover.ts 中的代码。只要在 json 文件中,将鼠标悬停在`kuizuo`这个词中即可触发,试试看看。 + +![](https://img.kuizuo.cn/image_RUIjdDI90l.png) + +### WebView + +使用 webView 可以在 vscode 内显示自定义的网页内容,丰富 vscode 功能,但所消耗的性能是肯定有的,就有可能影响 vscode 的运行速度。官方给出的建议是: + +- 这个功能真的需要放在`VSCode`中吗?作为单独的应用程序或网站会不会更好呢? + +- webview 是实现这个功能的唯一方法吗?可以使用常规 VS Code API 吗? + +- 您的 webview 是否会带来足够的用户价值以证明其高资源成本? + +不过这里还只是作为一个演示,点击右上角的 logo 图标便可在 vscode 中打开网页。 + +![](https://img.kuizuo.cn/image_nVO_YmRit4.png) + +不过要注意一点。新开的 webview 的背景是对应主题颜色的背景,如果网站有黑白模式的话,那么可能会导致颜色不对,故这里设置了 webview 的背景为白色。 + +至于消息通信就不尝试了。 + +## 发布 + +大部分常用的 vscode 插件实现就此完毕,实际上有很多 api 还没尝试过,篇幅有限,就不一一列举了,后续若有开发实际作用插件再研究。具体可自行安装尝试一番,既然要让别人安装,这里就需要介绍发布了。 + +这里在打包前重构下命令 ID,从 kuizuo-plugin → vscode-extension,同时把 package.json 的 name 改成了 vscode-extension-sample,因为发布的时候这个 id 必须唯一,不能与已有重名,到时候生成的为 kuizuo.vscode-extension-sample。(demo 给取了,不然我也不想起名为 sample) + +### 本地打包 + +无论是本地打包还是发布到应用市场都需要借助`vsce`这个工具。 + +安装 + +```bash +npm i vsce -g +``` + +打包成`vsix`文件: + +```bash +vsce package +``` + +:::warning 如果使用 pnpm 的话,有可能会打包失败,提示:npm ERR! missing: xxxxxx + +::: + +在打包时会提示一些信息,例如修改 README.md ,添加 LICENSE 等等,根据提示来操作即可。 + +生成好的 vsix 文件不能直接拖入安装,只能从扩展的右上角选择`Install from VSIX`安装: + +### 发布到应用市场 + +**1、注册账号获取 token** + +因为 Visual Studio Code 使用 [Azure DevOps](https://azure.microsoft.com/services/devops/)作为其 Marketplace 服务。所以需要登录一下[Azure](https://dev.azure.com/ 'Azure')。登录后,如果之前没用过的话会要求创建一个组织,默认为邮箱前缀,这里如下点击 + +![](https://img.kuizuo.cn/token1_JNXknLPQyJ.png) + +**2、新建一个 token** + +![image-20220831152146541](https://img.kuizuo.cn/image-20220831152146541.png) + +根据图片选择,注意其中 `Organization` 选择 `All aaccessible organizations`,`Scopes` 选择:`Full access`,否则登录会失败。生成后会得到一个 token,保存它,当你关闭时便不再显示。 + +**3、创建一个发布者** + +先使用网页版创建发布账号:[https://marketplace.visualstudio.com/manage](https://marketplace.visualstudio.com/manage 'https://marketplace.visualstudio.com/manage')填写一些基本信息,然后在使用 + +```bash +vsce login +``` + +这里的 `publisher name` 根据 package.json 中的 `publisher`,会要求你输入 `Personal Access Token`,把刚刚创建的 `token` 的值粘贴过来即可 + +提示 `The Personal Access Token verification succeeded for the publisher 'kuizuo'.` 就说明验证成功 + +**4、发布应用** + +```bash +vsce publish +``` + +:::warning + +这里要保证 package.json 的 name 在插件市场中唯一,否则会提示 The Extension Id already exist in the Marketplace. Please use the different Id。 + +::: + +运行完毕后,最终提示`Published kuizuo.vscode-extension-sample v1.0.0.` 就说明发布完毕,发布和 npm 包一样,都无需审核,但要求包名唯一。 + +可以在 [Manage Extensions | Visual Studio Marketplace](https://marketplace.visualstudio.com/manage/publishers/kuizuo 'Manage Extensions | Visual Studio Marketplace') 中管理已发布的插件 + +![](https://img.kuizuo.cn/image_HssaMdar8f.png) + +这时在 vscode 扩展商店中搜索 `vscode-extension-sample`就能找到该插件[VSCode-extension](https://marketplace.visualstudio.com/items?itemName=kuizuo.vscode-extension-sample 'VSCode-extension'),也可以通过`publisher:"kuizuo"`来找到我的所有 vscode 插件。 + +![vscode-extension](https://img.kuizuo.cn/image-20220711195038039.png) + +## 总结 + +整个开发过程的体验还是非常不错的,调试和代码提示都做得特别到位。不过有一点体验不好的,是大部分的配置信息都要写在 package.json 中,而在这里就不像 ts 那样有没有很好的代码提示了。不过当你填错命令 id 的时,vscode 还会提示命令 id 不存在,而不是不知道报错点。 + +浅浅吐槽下:说真的 vscode 插件开发相关的文章与教程少之又少,有时候一个功能的一个 api 实现只能去查阅文档,而不像 chrome 插件,通过搜索引擎就能很快得出结果,而 vscode 插件往往得到的是推荐...但这也说明 chrome 插件开发的人远多于 vscode 插件,或者说远多于 IDE 插件的开发,也很正常,大部分编程好用的功能,已有大牛实现了对应的插件,多数开发者没有一些特别的需求完全就没必要接触 vscode 插件开发。就如我一年前就想写 vscode 插件,但却迟迟拖到现在,其原因可能就这。 + +不过这类应用本就如此,就是不断翻阅文档,阅读前人的代码实现,再结合自身思路以完成最终目标。 + +## 参考文章 + +[VSCode 插件开发全攻略(一)概览 - 我是小茗同学 - 博客园 (cnblogs.com)](https://www.cnblogs.com/liuxianan/p/vscode-plugin-overview.html 'VSCode插件开发全攻略(一)概览 - 我是小茗同学 - 博客园 (cnblogs.com)') + +[Extension API | Visual Studio Code Extension API](https://code.visualstudio.com/api) diff --git "a/blog/project/\346\237\220\344\271\240\351\200\232\345\260\217\345\212\251\346\211\213.md" "b/blog/project/\346\237\220\344\271\240\351\200\232\345\260\217\345\212\251\346\211\213.md" new file mode 100644 index 0000000..3f8782d --- /dev/null +++ "b/blog/project/\346\237\220\344\271\240\351\200\232\345\260\217\345\212\251\346\211\213.md" @@ -0,0 +1,604 @@ +--- +slug: chaoxing-helper +title: 某习通小助手 +date: 2021-01-02 +authors: kuizuo +tags: [project, 易语言] +keywords: [project, 易语言] +description: 使用易语言开发的某星某习通小助手,助你的网课学习不再枯燥。 +image: /img/project/chaoxing-helper.png +--- + + + +## 前言 + +**声明,本文与该软件仅用于学习技术交流,请勿将软件用于非法途径,本作者不承担一切法律责任,望使用者须知。若影响到贵公司正常运行,请联系本人删除。** + +:::info 注:本文适用于有编程基础的,会 Http 请求那更好。 + +::: + +## 使用语言 + +有人会很好奇这个软件到底是怎么运行的,为啥可以实现自动完成视频,作业。借此写个完整的该软件开发过程,供各位学习,整个开发过程真不算的难,听我慢慢道来(尽可能详细),~~但你看完后,写不出来,大概率也不会想写~~。 + +既然是写软件,那怎么能不说说编程语言,首先这个软件是基于易语言开发的,初学易语言的三个月所写的练手项目,本是写来给我自用的,不过确实好用,那为啥不分享出去呢。 + +实际上我也考虑过开源,奈何多次审核不通过,累了,就懒得开源了。 + +首先,说说为啥会选择易语言,有一部分原因是因为我那时候正好在学易语言,哪怕现在如果要开发一个类似于这样的软件,我也会优选易语言(在不考虑兼容与报毒情况下)。原因其实非常简单,好写,太好写了,我记得那时候的第一版超星刷课只花了一周时间,实现了自动完成视频,那还只是我没什么开发经验的前提下(借鉴了外面的一份源码)。再者也是最主要的一部分,写的软件是基于什么平台,很显然,桌面级应用 Windows 平台。那就少不了交互界面了,而正是这个交互界面,让我劝退了 javascrpit 与 python。不是说他们不行,而是写起来绝对比易语言复杂。如果你有接触过这两者相关的估计会知道,尤其还是实现自动完成任务的功能,基本上是不提供界面而言。也就让代码的可操作性少了非常多,这还不是最致命的,致命的是使用者不是人人都学程序了,即使发你一个 python 文件,但他大概率是不会听你大费周章的安装,输入,原因就是麻烦(如果这份代码好用的话当我没说过)。 + +说这些都不如直接来一个 exe 可执行文件,让用户去点击操作,然后通过一个日志输出显示给用户,告知用户当前程序执行进度。可能有人又会问,那为啥不用 C#,VB.net,QT 等,我 tm 要是会的话,也不会用易语言来写了,易语言敲代码体验很差,如果用过其他的文本编辑器,就特别不想用易语言(反正我是这样,真的难用),毕竟易语言都是 20 年前的产物了,能活到现在就不错了。但不得不说,易语言是真的好写,好用,好上手。 + +语言不分贵贱,能写出好的程序都是好语言,所以本文都是以本人从易语言开发角度来讲述,如果你恰好有程序开发经验,或有想接触的,本文或许能给予你一些帮助。 + +## 找源码 + +既然介绍完所用语言,那么就开始编写代码吧,不过在此之前先别急,这一步尤为关键,能极大的节省你所开发的效率,那就是搜索是否的相关源码或者软件。最好是与自己所开发的语言一致。 + +这是我当时编写软件前在吾爱破解论坛上搜索到的相关源码,如下:![image-20200925173801271](https://img.kuizuo.cn/image-20200925173801271.png) + +看看软件源码的界面![image-20200925173832002](https://img.kuizuo.cn/image-20200925173832002.png) + +在比对一下我的修改了数十次的。 + +![image-20201220072514909](https://img.kuizuo.cn/image-20201220072514909.png) + +没错,我就是基于这个软件改的,还是有点相似之处的。但事实上这个原作者的代码在我翻阅到时就已经不能用了,并且还有很多弊端,例如还需要输入学校名称,输入验证码,这对用户体验来说的是非常烦人的。 + +同时在这份源码上只能说是一份临时品,几乎没有维护可言(虽然易语言写的软件多半都不好维护),不过有一个核心加密算法,也就是最终提交视频的一个核心算法,让我省去 JS 逆向分析的时间(后文会说到,不过以我那时候来看,这个 JS 自行解决也不成问题) + +那时候搜索到的还有其他的相关脚本,例如大多数人都了解过的油猴插件。后文有简单讲述到,因为和本文涉及到的不相关。 + +## 执行流程 + +找到相关源码或软件,就已经离项目完成快了一半了,接着只需要在该软件上进行修改,已达到自己的目的。当然,如果要补充一些功能还需要花费很多时间的。 + +### 页面设计 + +我优先做的就是修改 UI 界面,做到竟可能的不丑,且符合个人风格。而这部分就是拖拽组件,移动组件,微调组件,平行垂直等操作,没啥可言的。我所用的都是 windows 自带的组件,加上我不会自绘组件,只好借助皮肤模块来美化界面了。美化的效果如下图 + +![image-20201226062612827](https://img.kuizuo.cn/image-20201226062612827.png) + +实际上,页面设计相关就到此结束了,我能做的也只是尽量不丑,毕竟不会自绘组件,用原生自带的组件就这样了。当然,后面关于怎么数据渲染到组件这些会写到的。 + +### 登录 + +接着就是要说实现原理,首先回想一下,我们如果手动去一个个看视频,答题,需要干嘛,那肯定是登录了,不登录学习通那边怎么知道是你,那么在浏览器中,登录只是输入下账号,密码,然后点击登录按钮就完事了。然而实际原理不只是点击按钮这么简单,实则是发送一个 http 请求给后端,后端进行效验结果比对,返回结果,我简单叙述一下,放 js 代码来看看: + +![image-20201226063607140](https://img.kuizuo.cn/image-20201226063607140.png) + +具体看图片 + +![image-20201226064003045](https://img.kuizuo.cn/image-20201226064003045.png) + +完整关键代码如下:(已删除不必要代码) + +```js +//手机号+密码登录 +function loginByPhoneAndPwd() { + var phone = $('#phone').val().trim() + var pwd = $('#pwd').val() + var fid = $('#fid').val() + var refer = $('#refer').val() + if (util.isEmpty(phone)) { + util.showMsg(true, 'phoneMsg', '请输入手机号', true) + return + } + if (util.isEmpty(pwd)) { + util.showMsg(true, 'pwdMsg', '请输入密码', true) + return + } + var t = $('#t').val() + if (t == 'true') { + pwd = $.base64.btoa(pwd, 'UTF-8') + } + // -------------------------------------------------------- + $.ajax({ + url: '/fanyalogin', + type: 'post', + dataType: 'json', + data: { + fid: fid, + uname: phone, + password: pwd, + refer: refer, + t: t, + }, + success: function (data) { + if (data.status) { + var url = '' + if (data.tochaoxing) { + var path = window.location.protocol + '//' + window.location.host + url = + path + + '/towriteother?name=' + + encodeURIComponent(data.name) + + '&pwd=' + + encodeURIComponent(data.pwd) + + '&refer=' + + data.url + } else { + url = decodeURIComponent(data.url) + } + + if (top.location != self.location && $('#_blank').val() == '1') { + top.location = url + } else { + window.location = url + } + } else { + if (data.weakpwd) { + window.location = + '/v11/updateweakpwd?uid=' + + data.uid + + '&oldpwd=' + + encodeURIComponent($('#pwd').val()) + + '&refer=' + + refer + } else { + var msg = util.isEmpty(data.msg2) ? '登录失败' : data.msg2 + msg = '密码错误' == msg || '用户名或密码错误' == msg ? '手机号或密码错误' : msg + util.showMsg(true, 'err-txt', msg) + } + } + }, + }) +} +``` + +代码并不长,一个很简单的 post 登录,这里我会一一进行分析 + +```js +var phone = $('#phone').val().trim() +var pwd = $('#pwd').val() +var fid = $('#fid').val() +var refer = $('#refer').val() +if (util.isEmpty(phone)) { + util.showMsg(true, 'phoneMsg', '请输入手机号', true) + return +} +if (util.isEmpty(pwd)) { + util.showMsg(true, 'pwdMsg', '请输入密码', true) + return +} +var t = $('#t').val() +if (t == 'true') { + pwd = $.base64.btoa(pwd, 'UTF-8') +} +``` + +在分割符的前一部分,获取我们表单中的手机号(phone),密码(pwd),学校 id(fid),以及不重要的 refer,同时判断手机号,密码是否为空,并给出相应提示,同时这里的 pwd 还进行了 base64.btoa,也就是 Base64 编码处理过。这里我就模拟一下这些数据 + +```js +phone = '15212345678' +pwd = 'a123456' +pwd = 'YTEyMzQ1Ng==' // Base64编码后的结果 +fid = '12345' // 也可以不指定学校 填-1 +refer = 'http://passport2.chaoxing.com' +``` + +然后再看剩余的一部分,主要就关注这些: + +```js +$.ajax({ + url: '/fanyalogin', + type: 'post', + dataType: 'json', + data: { + fid: fid, + uname: phone, + password: pwd, + refer: refer, + t: t, + }, + success: function (data) { + //.... + } +} +``` + +也正是因为这几行代码,将我们的数据发送给了学习通的服务端,并将数据返回给我们,这里我抓个数据包看看数据是怎么样的 + +```http +POST /fanyalogin HTTP/1.1 +Connection: Keep-Alive +Content-Type: application/x-www-form-urlencoded; Charset=UTF-8 +Accept: */* +Referer: https://passport2.chaoxing.com/login?&newversion=true +User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36 +Origin: https://passport2.chaoxing.com +x-requested-with: XMLHttpRequest +Host: passport2.chaoxing.com +Content-Length: 94 + +fid=12345&uname=15212345678&password=YTEyMzQ1Ng==&refer=http://passport2.chaoxing.com&t=true +``` + +认真看上面最后一行,有没有发现这些数据不就是我们刚刚上面模拟的数据。再来看返回数据 + +```http +HTTP/1.1 200 OK +Server: Tengine +Date: Fri, 25 Sep 2020 11:50:30 GMT +Content-Type: text/html;charset=utf-8 +Connection: keep-alive +Set-Cookie: JSESSIONID=625EBEBB8C9A307910975B8A6306EE13; Path=/; HttpOnly +Set-Cookie: lv=3; Domain=.chaoxing.com; Expires=Sun, 25-Oct-2020 11:50:30 GMT; Path=/ +Set-Cookie: uf=b2d2c93beefa90dcc0dd308bdb4e3ac7c15d612bf3f08318fdb57793f3e0b0e8e06d6354b86e5f8c6733e63a87a57410913b662843f1f4ad6d92e371d7fdf644cb407fe2f4a1b7e3102289339c6dea121471850d8bf7e34cbde8ab62ef4efbfc29d661c57520821b; Domain=.chaoxing.com; Expires=Sun, 25-Oct-2020 11:50:30 GMT; Path=/ +Set-Cookie: _d=1601034630583; Domain=.chaoxing.com; Expires=Sun, 25-Oct-2020 11:50:30 GMT; Path=/ +Set-Cookie: vc=D117659BD1295E4489AED8ED14E8A8D8; Domain=.chaoxing.com; Expires=Sun, 25-Oct-2020 11:50:30 GMT; Path=/; HttpOnly +Set-Cookie: vc2=5B73047EF8C636D19D282B878FC42D4A; Domain=.chaoxing.com; Expires=Sun, 25-Oct-2020 11:50:30 GMT; Path=/; HttpOnly +Set-Cookie: vc3=Lptknj6gO2EVnnAWOW0A1O0d0RGWzgO1jVDtKiGtxlqX7dH5uAz84KoWDf2Y9v%2Biw2V3RyKd2gNXf%2BMVKt2HKmJzYK1vt%2BBHu%2B%2BXwG3NtJAWvXygxxcRYlSwCt%2BDv0r8JkrhqgJxJQV2VkMVMon8PABIuJdJKudVTQR%2FP6u2pfY%3D2dd5ab18abc4e7b47fdeb363a13a7c64; Domain=.chaoxing.com; Expires=Sun, 25-Oct-2020 11:50:30 GMT; Path=/; HttpOnly +Set-Cookie: xxtenc=0fd26095d768e519a53edd2f4ba4c9e9; Domain=.chaoxing.com; Expires=Sun, 25-Oct-2020 11:50:30 GMT; Path=/ +Set-Cookie: DSSTASH_LOG=C_38-UN_9502-US_42736002-T_1601034630584; Domain=.chaoxing.com; Expires=Sun, 25-Oct-2020 11:50:30 GMT; Path=/ +Set-Cookie: route=1b37a788fe3a8c39de935217be0d9f7a;Path=/ +Content-Length: 56 + +{"url":"http%3A%2F%2Fi.mooc.chaoxing.com","status":true} +``` + +**现在只要注意最后一行**,这是登录成功的返回结果,那如果密码错误呢,返回的结果如下 + +```json +{ "msg2": "用户名或密码错误", "status": false } +``` + +那么既然简单了明白了一下基本的登录实现原理,能不能模拟一下这样的请求,替换一下其他人的账号密码,然后发送给服务端,当然可以,看看用易语言代码是怎么实现的这样的登录功能。 + +![](https://img.kuizuo.cn/image-20201226070501146.png) + +注意我划线的两个部分,实际上在 POST 请求中,要做的也就是替换一系列的数据,已达到模拟请求,并接受服务端接收的数据。 + +不过这里我还得小提一句,看到上面的返回响应中,有好几个 Set-Cookie,这里 Cookie 是服务端返回的,并且加密处理了,而正是这个 Cookie,保存了我们的登录凭证,使得下次我们请求的时候会将这些 Cookie 携带上去,这样服务端才能知道哪个请求是谁发送的。这样才能获取到哪个学生的信息,如课程信息,作业信息等等。 + +凭什么我发送一个请求给服务端,然后就能登录成功,就可以不用借用浏览器,来实现登录账号?或者说本质上,浏览器数据交互也是通过 HTTP 协议,而 HTTP 协议就是这样,至于实现原理,这里我不涉及太多相关的,你只需要知道可以就行,但有时候并不像上面这么简单(请接着看下文例子)。 + +从上面的例子中,~~也许~~你已经能知道,只要发送一个请求的事情,就可以登录,就能获取对应的信息,实际上,你只要知道下面这一句话: + +**浏览器本质的操作,就是向服务器发送请求,而软件所做的就是模拟请求,已达到获取数据。** + +而操作流程,先抓包获取到发送的数据,然后再模拟数据发送给服务端,就可以达到相对应的操作后文都将以这样的操作为例,以至于实现原理,就涉及到相关专业的知识了,同时模拟请求,能绕过浏览器自身的限制(JS 的限制). + +### 获取课程列表 + +这里我就以获取课程列表数据,并通过 DOM 解析,并将其显示在软件上来演示。 + +现在已经登录成功了,那么我现在就向服务器发送获取课程信息的请求,在这里我封装成一个易语言的函数,如下几个部分: + +``` +url = “http://mooc1-1.chaoxing.com/visit/courses/study?isAjax=true&debug=false” +http.Open (“GET”, url) +http.Send () +response = http.GetResponseTextU2A () +``` + +此时 response 为 html 文件文本,像这样的 + +```html +
    + +
  • + + + + +
    +

    + 创业基础 + +

    +

    王艳茹

    +

    中国青年政治学院

    +

    默认班级

    +

    课程时间:2020年11月12日-2021年01月10日

    + +
    +
  • +
  • +
    + +
    +
  • +
+``` + +可以看到课程信息以及相关链接,接下来要做的就是根据 DOM 对象,提取这些课程数据,下为我那时候解析的代码: + +![image-20210102035545981](https://img.kuizuo.cn/image-20210102035545981.png) + +根据 CSS 类名 courseItem,获取课程单元块以及课程数,然后通过遍历 courseItemList,同时通过 CSS 选择器选择到对应的 HTML 标签,获取到我们想要的数据,通过一个自定义数据类型(这里非对象),将其存在课程列表数组内,最后将这些数据通过超级列表框设置到页面上。也就是如下图这样 + +![image-20210102035927028](https://img.kuizuo.cn/image-20210102035927028.png) + +同样的获取章节列表,作业列表,考试列表,甚至是一些评论列表,也都是通过 DOM 解析,获取其数据,存储到数组内,然后根据章节名或者 id 来获取数组成员,已达到指定课程完成任务。 + +### 开始刷课(重点) + +如果只是获取数据那怎么能够,而刷课才是软件的主要目的,首先要刷课,就必须要指定课程,这里指定课程也就是 列表框中选中即可,此时点击开始刷课便能开始任务,这里来看看刷课的代码 + +![image-20210102040540378](https://img.kuizuo.cn/image-20210102040540378.png) + +就是判断用户有无登录,有无任务在执行,有无选课,然后将配置写入到配置文件,方便下次打开还是上次配置,同时设置模式,是要完成那一部分任务,最后将按钮设置为禁止,不可点击(所以为啥我不一开始就直接禁止开始刷课为假呢),最后启动一个线程来执行,在主线程执行会导致窗口卡顿等现象。然后下面才是正在的执行逻辑了。 + +#### 获取章节列表 + +![image-20210102041501299](https://img.kuizuo.cn/image-20210102041501299.png) + +添加了注释,就懒得在打字一个个说明了,执行逻辑并不难,也就是判断,然后执行,接着要到刷视频和题这个方法,因为最主要还是这个代码在干什么。(后面也会将写上代码注释,方便大家理解) + +#### 开始刷视频和题 + +![image-20210102044334946](https://img.kuizuo.cn/image-20210102044334946.png) + +开始循环访问选择夹,接下来代码有点多,执行流程也就是循环,判断,我简化了很多,认真看 + +![image-20210102050722756](https://img.kuizuo.cn/image-20210102050722756.png) + +这里我要提一下,上面的 mArg 数据是什么,是一段 JSON 数据文本,长下面这样 + +```json +{ + "attachments": [ + { + "headOffset": 663000, + "jobid": "1596706035431101", + "otherInfo": "nodeId_349140314-cpi_159793445", + "isPassed": true, + "property": { + "jobid": "1596706035431101", + "switchwindow": "true", + "size": 443706433, + "fastforward": "true", + "hsize": "423.15 MB", + "module": "insertvideo", + "name": "12.13.mp4", + "mid": "8562913227181596706035139", + "type": ".mp4", + "doublespeed": 1, + "objectid": "902ca19c673c7fa256702b6211c9df07", + "_jobid": "1596706035431101" + }, + "mid": "8562913227181596706035139", + "playTime": 663000, + "type": "video", + "aid": 600168224, + "objectId": "902ca19c673c7fa256702b6211c9df07" + } + ], + "defaults": { + "fid": "1617", + "ktoken": "ac0308fc1f7a84019740f1bebfd0b733", + "mtEnc": "a70ce1e7dac8d0d2e6082e2a95d002a3", + "isFiled": 0, + "ignoreVideoCtrl": 0, + "reportUrl": "https://mooc1-2.chaoxing.com/multimedia/log/a/159793445", + "chapterCapture": 0, + "userid": "157041903", + "reportTimeInterval": 60, + "initdataUrl": "https://mooc1-2.chaoxing.com/richvideo/initdatawithviewer", + "knowledgeid": 349140314, + "schooldoublespeed": 0, + "qnenc": "b2a727eed024085321062c005680e1ef", + "defenc": "bca5e389669154f0fd1fb0208b2ad655", + "clazzId": 34189060, + "cardid": 311180449, + "imageUrl": "https://p.ananas.chaoxing.com/star3/270_169c/f01bc30632e023f83b3e8879cdeea2c7.jpg", + "lastmodifytime": 1609462354000, + "state": 0, + "courseid": 215403857, + "cpi": 159793445, + "subtitleUrl": "https://mooc1-2.chaoxing.com/richvideo/subtitle" + }, + "control": true +} +``` + +通过 JSON 解析工具,可以获取到章节下的课件内容信息。比如视频时长,视频通过状态,视频 id,等等 + +![image-20210102074018606](https://img.kuizuo.cn/image-20210102074018606.png) + +取到我们想要的数据,并存为变量即可,接着才是关键所在,获取到了课件信息,同时判断课件类型为视频,并且视频的通过状态为 false,那么接下来就是要提交视频了。相关代码如下 + +#### 提交视频 + +![image-20210102052457877](https://img.kuizuo.cn/image-20210102052457877.png) + +其中这里提交视频就一个请求,也就是这个请求,服务端才知道你视频看了多少,并且将你的观看时长记录到数据库中,最终拼接的 url 比如这样的 `https://mooc1-2.chaoxing.com/multimedia/log/a/159793445/66ee5f706ccc9e58ab0ea383a83e665c?clazzId=34189060&playingTime=935&duration=935&clipTime=0_935&objectId=34ad66ae9778a00c6bfde810f12431ac&otherInfo=nodeId_349140316-cpi_159793445&jobid=1595816983931186&userid=257041903&isdrag=4&view=pc&enc=f26c618c0e0af1147fe2f4ce7b5e8f95&rt=0.9&dtype=Video&_t=160954150532` + +当然这个请求没这么好伪装出来,你在上面这几行就可以看到这些参数的复杂了,并且还有相关的加密,如果你随便发送一个请求,服务器鸟都不会鸟你的。伪装还算简单,照葫芦画瓢就完事了,而加密你就需要了解对应的加密算法和 JS 逆向了。这里如果我改掉其中一个必要的请求,那么将会出现如下界面 + +```jsx live + + + + + +403 + + + +
+

>_< 很抱歉,您没有权限访问这个页面! (403)

+

112.48.28.255
mooc-2166199849-8f1c1

+
+ + +``` + +所以,POST 请求最主要的之一,就是拼接参数,去模拟请求,在说到这里的加密 enc,先看看原文本长啥样 + +``` +[34189060][157041903][1595816983931186][34ad66ae9778a00c6bfde810f12431ac][935000][d_yHJ!$pdA~5][935000][0_935] + +将上面这段数据通过MD5加密即可获取enc为 +f26c618c0e0af1147fe2f4ce7b5e8f95 + +通过易语言拼接如下 +web参.enc = 取数据摘要 (到字节集 (“[” + web参.classId + “]” + “[” + web参.userid + “]” + “[” + web参.jobid + “]” + “[” + web参.objectId + “]” + “[” + 到文本 (web参.playingTime × 1000) + “]” + “[d_yHJ!$pdA~5]” + “[” + web参.duration + “000]” + “[0_” + web参.duration + “]”)) +``` + +至于我怎么知道加密点的,就涉及到 JS 逆向,就需要看网页内的 JS 文件,这里就不在赘述了。 + +然后我们每提交一次视频的请求,只要将 playingTime 播放时长,改为视频时长,就能实现秒刷,原理就是通过协议发送请求。但也有问题所在,我提交一次,服务端记录一次,并且间隔为 1 分钟,我 1 分钟内在提交一次视频完成的请求都将不会记录时长,所以这里的秒刷也只是将视频章节内的视频秒刷了,实际上要挂时长,还得每隔一分钟提交一次才行。(学习通是这样的) + +顺便抓个提交测验的请求的包,看看模拟请求有多烦躁。 + +``` +// url (请求地址) +https://mooc1-2.chaoxing.com/work/addStudentWorkNewWeb?_classId=34189060&courseid=215403857&token=5219a574b7571639ea6b9770bbc23da2&totalQuestionNum=cd8c29480d9c16f5a675e8c2c459245c&ua=pc&formType=post&saveStatus=1&pos=841bf6c56a6d8179e801f5743b&rd=0.5328632906746906&value=(285|546)&wid=11020928&_edt=1609545107317285&version=1 + +// post的data数据(拼接题号,选项,课程id,班级id,测验id等等......) +pyFlag=&courseId=215403857&classId=34189060&api=1&workAnswerId=50407707&totalQuestionNum=cd8c29480d9c16f5a675e8c2c459245c&fullScore=100.0&knowledgeid=349140316&oldSchoolId=&oldWorkId=62df20e345ff464d9bbe4ea021025cc5&jobid=work-62df20e345ff464d9bbe4ea021025cc5&workRelationId=11020928&enc=&enc_work=5219a574b7571639ea6b9770bbc23da2&userId=157041903&answercheck209790637=A&answercheck209790637=B&answer209790637=AB&answertype209790637=1&answer209790638=false&answertype209790638=3&answer209790639=true&answertype209790639=3&answerwqbid=209790637%2C209790638%2C209790639%2C + +// 最后提交请求的响应文本(根据status 是否为true来判断是否提交成功,msg为返回结果文本) +{"msg":"success!","stuStatus":4,"backUrl":"","url":"/api/work?courseid=215403857&workId=ca62b882b279427b9d24876daba4e2ba&clazzId=34189060&knowledgeid=349140316&ut=s&type=&submit=true&jobid=work-62df20e345ff464d9bbe4ea021025cc5&enc=061df255135b657c3aef35c5afc711c4&ktoken=c09603936670a079289c0d1488ab0f63","status":true} +``` + +想这样的自动完成任务软件(协议),要做的就是抓包(截包),分析数据,模拟数据,然后在通过代码方式生成出来,最终提交给服务器。 + +通过这样的流程,就能实现自动刷视频,类似的章节测验与考试无非也就是判断,然后执行发送请求。对这个软件而言,就封装了好几个方法了 + +![image-20210102073628331](https://img.kuizuo.cn/image-20210102073628331.png) + +而这还只是超星提交的操作,软件界面相关的我就不多演示了。总之,整体执行的流程就是像上面那样,软件怎么编写,就看开发人员了。 + +## 关于协议与脚本 + +上面说了一大堆,相信还是有大多数人迷迷糊糊的,正常,不过听不听得懂无所谓,了解即可,这里我需要说一下,协议与脚本的一些区别,实际上,简单比较下也能很明显的感受到两者的区别,甚至可以说,这两者的本质毫不相干。 + +- 脚本 + +需要依托在宿主上(浏览器),不然也无法执行代码(js 或填表),来实现点击提交操作。脚本要做的,也就是将人手动操作的,通过自动化方式来操作。 + +- 协议 + +则是基于 HTTP,只需要发送请求,就可以做到脱机(浏览器),以达到高效执行,而这是脚本做不到的,同时编写难度也是高于脚本。 + +发送完成视频的请求,就能绕过浏览器内置的拖拽视频进度条,倍数等限制,而浏览器本身也是基于协议来实现的,也就是将这些提交请求的代码(JS)放在浏览器客户端,然后判断执行。而你的所有操作最终都将通过 HTTP 请求来发送,来达到数据交互的目的。 + +实际上协议能做的远非如此,我这里简单举几个例子吧,例如抢购,机器人,等等,而这些用脚本都是执行不了,要么执行效率贼差。 + +实际上在一开始写这款软件时(学易语言和 JS 三个月左右),我是知道脚本与协议的区别,并且一开始想使用脚本来写,奈何,那时候的技术,只停留在使用按键精灵或大漠插件,来实现 PC 端操作,而要操作浏览器内置 DOM 元素,则需要网页填表以及一些前端基础,也正是因为不会这些相关的,但又想写个练手项目,于是就选择使用协议去完成,并且最终成功写成。 + +现在想想,也正应该感谢这款软件的开发经历,对我技术提升以及后续的学习兴趣至关重要。可以说没有这个软件给我带来的成就感,也许就不会有这篇文章了。(貌似有点提早感慨了) + +## 关于风险 + +这也是很多人可能会担心的,毕竟软件自动执行嘛,我咋知道安全不安全,会不会导致我账号异常等等。不过这实际上要看情况的。 + +就先这么说,你在 10 秒内,同时请求了 20 条视频,而后端是知道你请求了 20 条视频,因为你获取了后端的资源,然后后端一看,这丫的不对劲啊,手速这么快,有可能吗?后端不会觉得你手速快,而只会觉得,你像上面代码那样来短时间内批量提交视频的。所以就会给记录异常 + +而且这也要看平台的 + +- 学习通 + +学习通目前提交过快,就会出现验证码,并未有使账号异常冻结等操作。 + +- 职教云 + +视频提交间隔要在 5 秒以上(我目前测试情况下是这样),课件等无限制。 + +- 其他平台(没具体了解过,目前手头就写了这两个) + +那如果是正常速度提交,尽可能的模拟人为的操作呢,那也未必就没风险,只要他们后端想,修改一下接口,就能知道你操作是否异常了,比如我更改了加密算法,导致了你提交请求中和服务端效验失败,那我就可以认为你是通过外来手段篡改了请求数据,也就非法操作,就认为你不是非人为(非浏览器)操作。 + +然而这种情况很少,一般来说,网站部署运行了,除非特别大的改动,基本上是不会频繁的更换源码,需要不断测试。基本上也就是看平台了 + +我一开始只写了刷视频的,后面对接了题库,就开始写刷题的,然后有验证码就开始过验证码,最终还为软件添加一个网络验证,这每一步的过程都是深夜在电脑前,望着别人完全看不懂的代码,想,改,不过我庆幸我学了这些技术,因为它确实让我目前能写很多能用的项目。从这个超星学习通助手还学到的其他技术,下面一一列举 + +## 一些相关技术 + +### 完美验证系统 + +这个是用于验证码识别的,我当初为了解决超星的登录与提交题目时出现的验证码,就必须要识别出该验证码,于是我找到了完美验证码系统,我先放一张图 + +首先说下识别的实现原理:获取到验证码的图片,比如下面这张 + +![ABCWF](https://img.kuizuo.cn/ABCWF.jpg) + +那么我先把干扰线去掉,并且二值化处理一下,变成下面这样 + +![image-20200924232042428](https://img.kuizuo.cn/image-20200924232042428.png) + +然后这时候开始抠图,抠出 A,B,C,W,F 字符,比如抠出 A 字符 + +![image-20200924232357263](https://img.kuizuo.cn/image-20200924232357263.png) + +接着,做几百张这样的抠图图片,如 + +![demo4](https://img.kuizuo.cn/demo4.gif) + +然后交给系统识别就行了,他会比对你做的字模,然后进行图片相似度比对,最终将识别结果返回给你,看似很简单,的确也很简单,但是我扣这些图,扣了一周,最终识别效果也就只有百分之 50 这样,真的吐了。但是没办法,那时候学的浅,哪里还知道深度学习和 ocr 的识别,就这样坚持硬着头皮扣了一周,然后将这些字模全部导出置识别库用于调用。最终整体识别效果,如下图 + +![image-20200924233820001](https://img.kuizuo.cn/image-20200924233820001.png) + +甚至还看了一个图象识别的教程手写一个类似这样的识别系统,最终效果如下图。 + +![demo5](https://img.kuizuo.cn/demo5.gif) + +我要是当时会深度识别和调用 OCR,我用的着这么麻烦吗,这个是真的浪费我太多没用的时间。 + +### 自写网络验证 + +作为软件开发商,肯定不希望自己的辛辛苦苦写的软件,给别人一破解,修改了版权,并借此牟利,于是就不得不对软件进行一个操作,一般来说外面都有专门的对软件进行保护的厂商,但要钱的嘛,与其如此不如自己写一个,虽然防御上面可能没别人好,但至少一些破解小白肯定没那么轻松搞定。 + +一般的网络验证,你会看到如下界面 + +![image-20200926161606528](https://img.kuizuo.cn/image-20200926161606528.png) + +你需要注册一个账号使用,如果时间到期了就需要充值卡密使用,但你如果不注册的话,你就无法使用这个软件,能有效的防止破解者,提高破解门槛(该破解都能破解,就看想不想了),这是一个客户端,对应的肯定有服务端 + +![image-20200926161958435](https://img.kuizuo.cn/image-20200926161958435.png) + +这里只记录了一些用的上的,实际上可以记录更多,只是我懒得再记录了,通过这个网络验证,可以有效的防止软件被篡改,同时也能利用这个来获取用户引流等等,至于网络验证相关代码就不提及了。 + +实际上我防破解意识很浅,原因也很简单,这个软件都不收费,别人闲着没事破解啥,原本是加了网络验证,但是由于太麻烦,我就懒得加了。 + +实际上超星这个软件从头到尾就没主动收取用户费用。那时候也是本着写来自用,并未想过去接单,帮别人啥的,对我来说没必要。软件写来不就是分享给更多的用户吗,话说的绝对,并不是所有开发者都有时间和精力去免费维护一个软件,我之所以能免费分享,主要还是我学校正好就是超星学习通的,加上也是我的练手项目,于是不分享白不分享,到时候一些其他相关软件的合作者也能找我(已经在合作了),所以这个软件对我来说有必要收费吗,反正我是没打算过。但后续平台发展了,就不得不停了该软件,没办法,为了平台而着想。 + +### 浏览器插件 + +我在搜索的超星刷课源码的同时也搜索到一个浏览器插件,油猴插件。相信学校是超星的肯定被同学安利过这样的一款插件,在这个插件你能看到很多脚本,其中你一搜就能搜到有关超星学习通和其他的,并且免费使用,也就是这个原因,我就没打算在我自写的超星学习通助手上进行收费。但实际上它们只是脚本,我当初准备写超星刷课的时候一开始是想写网页自动操作脚本的,但是随便一搜就有了相关的,并且别人的还有一些打码收费什么的,于是就放弃用脚本,而是通过 POST 请求(其实就是我那时候压根就不会浏览器的脚本,搜了一些网页填表的没学明白,而 js 那时候只会分析算法,ES6 语法都不会,于是就选择了协议),然后就有了上面的超星学习通助手,在疫情期间深入学习 web 方面就接触到了两个浏览器脚本,一个是 Puppeteer 与 selenium,另一个是 Chrome 插件开发,而这里的油猴插件就是基于 Chrome 插件开发出来的。 + +但我没学过油猴插件,而是直接学了 Chrome 插件开发,有关 Chrome 插件开发可以看我写过一篇 Chrome 插件开发的文章,这里的话我就放一个网络上开源的插件 [超星慕课小工具](https://cx.icodef.com/) 毕竟我的调用题库接口还是基于这个插件的。(现转储为我自己的服务器) + +## 总结 + +作为作者,能看到更多的人使用自己所写的软件,非常欣慰,估计认识我的一部分也是因为这个软件原因,由于要搞平台,所以就要停滞该项目了。 + +在一开始编写时,都未曾想到能写的出来,并且优化成这样,在经历了这段编写过程,让我感受的编程的强大,也正是如此,让我会去尝试新鲜的技术,去编写新鲜的项目。不过还要说一些相关的问题 + +### 说说这软件一些问题 + +- 报毒 :易语言的自身的原因 +- 无法做到适配:有的 win7 系统是获取不到课程,别问,问就是易语言。 +- 无法热更新:每次更新都需要在对应的下载链接下载,到现在我还不会热更新,哭了哭了。 +- 题库不全:有概率是搜不到题目的,所以提供了随机选项。 + +从写这个软件开始,我能感受到易语言带来的便携,但也看到了易语言的不足之处。以至于我曾最喜欢的编程语言,也渐渐的开始放弃。(补: 我已不再从事易语言开发) + +### 不断更新,维护 + +没有软件一上来就是完美无瑕的,都是经过多次的修改,更新,最终展现给用户,这个也不例外,从我 去年 10 月 10 号开始一直到现在,中间陆陆续续修改了几十遍,从 1.0 版本更新到 4.1.0(最终版,已不再更新) + +### 感慨 + +因为这款软件,因为易语言让我感觉到编程是多么无敌的感觉,算是我目前为止还能拿的出手的软件之一,真的完全可以说,没有易语言,我也写不出来这样软件,更别说接下来的学习了。成就感与自信心油然而生,随后的学习更是得心应手。 + +网络上也很少有类似这种文章,道理想必都懂,最后还是要说下 + +**本文仅作为技术交流,希望更多的人利用技术去方便自己,而不是利用技术从事违规行为** + +**请勿利用本文相关技术与软件从事违法行为,否则后果自负!** diff --git "a/blog/project/\351\242\230\345\260\217\344\276\240.md" "b/blog/project/\351\242\230\345\260\217\344\276\240.md" new file mode 100644 index 0000000..d2c0d8b --- /dev/null +++ "b/blog/project/\351\242\230\345\260\217\344\276\240.md" @@ -0,0 +1,332 @@ +--- +slug: question-man +title: 题小侠 +date: 2022-04-06 +authors: kuizuo +tags: [project, vue, miniprogram] +keywords: [project, vue, miniprogram] +description: 作者通过Taro + Vue3 + NutUI技术栈开发了一个搜题小程序,并记录了开发和发布过程。总体评价Taro开发体验较好,但小程序的发布并不是一件容易的事情。 +--- + +很早就了解与学习过微信小程序开发相关的技术栈与框架,小程序的账号也都已经申请过。但写过的 demo 项目也迟迟没有发布到小程序上。这主要的原因还是觉得不值得发布,加上各种审核相关的。而这次准备写一个搜题相关的小程序,也是时候实战发布一下,顺带记录下整个开发与发布过程。 + +在线体验:扫下图小程序二维码 + +![itopic](https://img.kuizuo.cn/itopic.jpg) + +小程序的源码地址:[https://github.com/kuizuo/question-man](https://github.com/kuizuo/question-man) + + + +## 技术栈 + +小程序所采用的是 Taro + Vue3 + NutUI,之所以选这套技术栈,主要是想上 Vue3,而 uniapp 对 Vue3 的支持并不友好,在我的上篇文章中也有说明到,同时支持 uniapp 的 vue3 屈指可数。所以便选用了这套技术栈来进行开发。 + +## 页面设计 + +![image-20220405213930313](https://img.kuizuo.cn/image-20220405213930313.png) + +## 项目配置 + +### 项目搭建 + +[安装及使用 | Taro 文档 (jd.com)](https://taro-docs.jd.com/taro/docs/GETTING-STARTED) + +``` +taro init myApp +``` + +配置如下 + +![image-20220405214126617](https://img.kuizuo.cn/image-20220405214126617.png) + +安装完依赖,使用`npm run dev:weapp`,在打开微信开发者工具,导入项目即可。 + +### axios 封装 + +web 端 http 请求使用最多的库就是 axios 了,但是在小程序中使用 axios 会提示 adapter 未定义,原因是小程序不能解析 package.json 中的 browser module 等字段。 + +要使用 axios 的话可以安装 axios-miniprogram 或者 taro-axios 库(我选择后者,但前者稍小 5kb),也就是会适配小程序的 axios 的 adapter,引入和使用与 axios 并不特别大的差异。以下是我封装后的代码 + +```typescript title=“utils/http.ts” +import axios from 'taro-axios' +import Taro from '@tarojs/taro' +import { useAuthStore } from '@/stores/modules/auth' + +const showErrorToast = msg => { + Taro.showToast({ + title: msg, + icon: 'none', + }) +} + +const instance = axios.create({ + baseURL: process.env.BASE_URL, +}) + +instance.interceptors.request.use( + config => { + Taro.showLoading({ + title: '加载中', + mask: true, + }) + let token = Taro.getStorageSync('token') + if (typeof token == 'undefined') { + token = '' + } + config.headers = { + 'Content-Type': 'application/json;charset=utf-8', + Authorization: token, + } + return config + }, + error => { + console.log(error) + return Promise.reject(error) + }, +) + +// respone拦截器 +instance.interceptors.response.use( + (response: any) => { + Taro.hideLoading() + if (response.data.isError) { + showErrorToast(response.data.error.message) + } else { + return response + } + }, + error => { + if (error.response) { + Taro.hideLoading() + console.log('err', error) + + let res = error.response.data + switch (res.code) { + case 400: + showErrorToast(res.message || '非法请求') + break + case 401: + const authStore = useAuthStore() + authStore.login() + // showErrorToast(res.message || '当前登录已过期,请重新登录') + // Taro.navigateTo({ + // url: '/pages/login/index' + // }) + break + case 403: + showErrorToast(res.message || '非法请求') + break + case 404: + showErrorToast(res.message || '请求资源不存在') + break + case 500: + case 502: + showErrorToast(res.message || '服务器开小差啦') + break + default: + showErrorToast(res.message || res.statusText) + } + } else { + console.log(error) + showErrorToast('请检查网络连接状态') + } + + return Promise.reject(error) + }, +) + +export default instance +``` + +没什么好说的,和网页端基本一致。主要是加了个 wx 的 Loading 与 Toast。然后在 token 失效的时候,应该是要跳转到登录页面,但我并没有编写登录页面,而是重新调用一遍 login,达到静默登录的效果。 + +### 获取用户唯一标识(openid) + +借助微信小程序能十分方便的获取到微信用户。在微信中,为了识别用户,每个用户针对每个公众号或小程序等应用会产生一个安全的 openid,开发者可以通过这个标识识别出用户。 + +要获取 openid 有以下几种方法(这里以 Taro 为例子,而为 wx 官方文档),具体代码可在官方文档中查看到。 + +[Taro.login(option) | Taro 文档 (jd.com)](https://taro-docs.jd.com/taro/docs/apis/open-api/login/) + +首先调用`Taro.login()` 获取 5 分钟时长的 code,然向 api.weixin.qq.com 获取 openid 代码如下 + +```javascript +Taro.login({ + success(res) { + let code = res.code + let appId = '小程序->开发管理->开发设置->开发者ID获取' + let appSecret = '小程序->开发管理->开发设置->开发者ID获取' + Taro.request({ + url: 'https://api.weixin.qq.com/sns/jscode2session', + data: { + appid: appId, + secret: appSecret, + js_code: res.code, + grant_type: 'authorization_code', + }, + method: 'GET', + success(res) { + console.log('openid', res.data.openid) // 得到openid + console.log('session_key', res.data.session_key) // 得到 session_key + }, + }) + }, +}) +``` + +但现在小程序是无法添加 api.weixin.qq.com 域名,所以上面的方案在小程序端失效,只能转到后端上。小程序官方有张实践图 + +![img](https://mmbiz.qpic.cn/mmbiz_jpg/JpltBJ31poWJDK7SWLI8Y52j3eL3jVicRyXKjo60OsUwcHb3BGm2YKvOF45TC4yVIWMT28pJO3YBvsiaGkGdEJDQ/640?wx_fmt=jpeg) + +整个步骤 + +1、调用**wx.login 获取 code**,此时也可调用 wx.getUserInfo 来获取用户数据(昵称,头像) + +2、由于**小程序后台授权域名无法授权微信的域名**,所以需要**将 code 与用户数据传入到自己的服务器**上。 + +3、服务器后台接收到 code,**后台将 appid+appsecret+code 向微信 api 服务获取用户的登录态信息(openid 与 session_key)**,服务器对这些登录态信息进行封装,如 session 或者 jwt 的 token 都可以(这里以 token 为例),返回给小程序端 + +4、小程序接收到 token 时,将其保存到本地存储上(wx.setStorage),每次携带该 token 请求向服务器发送请求。 + +不过如果使用云开发可以免去很多鉴权相关的,但由于数据库存储相关的,所以我是采用自建后台服务器,而后端暂不考虑开源,故具体逻辑代码就不演示了(考虑安全为主),自行编写后端 login 接口。 + +### 获取手机号 + +**注意:只有企业小程序才可以获取用户手机号,个人小程序没有办法获取的。** + +在这篇文章中有说明到如何获取 [5 行代码获取小程序用户手机号 | 微信开放社区 (qq.com)](https://developers.weixin.qq.com/community/develop/article/doc/000c462925c610ecc899b11d751013) + +[获取手机号 | 微信开放文档 (qq.com)](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html) + +所以就没有然后了(因为我肯定是个人账号)。 + +### 获取用户信息 + +要获取用户信息的需要调用 getUserProfile,演示代码如下 + +```javascript +Taro.getUserProfile({ + desc: '获取用户个人信息', + success: function(res) { + const userInfo = res.userInfo + console.log(res.userInfo) + } +} +``` + +要注意的是 getUserProfile 必须通过按钮来触发,而不能通过生命周期的形式,弹出授权窗口,用户可接受与拒绝。说白了就是开发者无法在用户不知情的情况下获取到用户信息(考虑到用户隐私相关的) + +同时有一个小坑,getUserInfo 获取到微信用户与灰色头像 + +由于小程序官方调整了接口,导致 getUserInfo 无法获取正确的用户信息。详情可看 [小程序登录、用户信息相关接口调整说明 | 微信开放社区 (qq.com)](https://developers.weixin.qq.com/community/develop/doc/000cacfa20ce88df04cb468bc52801?highLine=login) + +一个正常的登录流程: + +按理来说一般是要提供一个专门的登录页面,哪怕登录页面只有一个按钮,按钮名为一键登录。然后用户点击触发 getUserProfile 获取用户信息,如果用户拒绝则不允许使用软件,允许则进入主页,然后将用户信息保存置服务器上,以便下次用户访问时无需再次调用 getUserProfile 接口,直接从服务器上取。 + +而我的做法相对比较粗略,我是直接允许用户进入首页,但当他**使用搜索功能时**,则判断服务器是否存有该用户的信息,如果没有,调用 getUserProfile,弹出授权框给用户选择。同时在首页的时候通过 checkSession 来判断 session 是否过期,过期则调用 wx.login 静默登录,更新登录态。(不过如果后端不判断该用户信息是否完善的话,那么是有办法直接绕过这种方式来进行调用接口了)。 + +### 数据库搭建 + +实际上这个小程序最主要的依赖就是数据库了,而这个数据库与传统的关系型(Mysql)和文档型(MongoDB)不同,要做到搜索引擎式的搜索。举个例子,搜索“李白”,能得到 【李白的诗风是】【李白的称呼】有关李白的字样,并且要求返回的速度快。像上面举到的两种数据库实现起来不方便,如果涉及到千万级别的数据库,将是按秒,甚至数十秒的响应速度。 + +而我所选择的数据库是[Elasticsearch](https://www.elastic.co/cn/elasticsearch/),能很轻松的实现上面的效果,并且有个很客观的响应速度。(具体实现自行了解,后端代码暂不考虑开源) + +## 上传发布 + +当本地开发完毕时,点击右上角的上传,填写版本号相关以及项目备注,然后上传成功如下图 + +![image-20220403125339679](https://img.kuizuo.cn/image-20220403125339679.png) + +在版本管理中的开发版本可以看到刚刚上传的代码 + +![image-20220403133323838](https://img.kuizuo.cn/image-20220403133323838.png) + +点击提交审核后,会提示确保项目不是测试版与 Demo(否则将受到相应处罚)如下图 + +![image-20220403133707429](https://img.kuizuo.cn/image-20220403133707429.png) + +### 测试版 + +或者选择体验版(只有管理员与体验者的账号才可以访问),扫描体验版的二维码,即可使用微信访问项目。 + +### 审核版本 + +如果是要发布正式版的话,还需要填写如下表格,接着静等 1-7 天即可(可选择加急,一年一次) + +![image-20220406003544712](https://img.kuizuo.cn/image-20220406003544712.png) + +等待审核完毕,实测一天内,审核版本如下 + +![image-20220406123345533](https://img.kuizuo.cn/image-20220406123345533.png) + +### 线上版本 + +最终提交发布,线上版本如下 + +![image-20220406125311142](https://img.kuizuo.cn/image-20220406125311142.png) + +至此,用户即可打开微信,通过小程序访问到该应用。 + +### 关于改名 + +可能有些人(这个人就是我)会担心自己上线小程序后,期间能否改名,有何影响。 + +首先是可以改名的,小程序的标识是 APPID,而不是一个名称。但**改名会直接影响到用户对小程序的认知,从而影响小程序用户流失**。同时改名要求很苛刻,会检索是否有相似同名小程序,以及是否有商标证明等等,并且一年只能修改两次名字,所以起名与改名都要慎重。 + +## 一些开发时的注意事项(坑) + +### request:fail url not in domain list(跨域) + +浏览器最烦躁之一跨域,在小程序上也不一意外,但小程序更加苛刻,即便后端允许跨域,小程序请求也会提示标题所示错误。这时候一般就如下几种做法。 + +[网络 | 微信开放文档 (qq.com)](https://developers.weixin.qq.com/miniprogram/dev/framework/ability/network.html) + +首先要确保是 https,同时开发时为了方便调试,一般会在本地设置中勾选,不效验合法域名选项,如下 + +![image-20220403012958069](https://img.kuizuo.cn/image-20220403012958069.png) + +这时候开发环境下就能正常发送请求相关了(我这里后端是直接允许跨域了,不然需要在 config/index.js 中设置 devServer.proxy 的反向代理)。 + +但在生成环境下就需要在[小程序](https://mp.weixin.qq.com)中的开发管理中配置服务器域名了 + +![image-20220403013733314](https://img.kuizuo.cn/image-20220403013733314.png) + +点击开始配置会提示身份认证相关,扫完码后将会到如下配置 + +![image-20220403013828824](https://img.kuizuo.cn/image-20220403013828824.png) + +这里就是填写生产环境下要请求资源的域名了,并且是需要开启 https 的。 + +设置完毕,重启微信开发者工具或刷新项目配置,这里需使用小程序账号的 AppID 进行登录,测试号无效,然后项目配置就会设置好 request 合法域名,再次请求便能正常响应。 + +![image-20220403014345645](https://img.kuizuo.cn/image-20220403014345645.png) + +### pinia 持久化 + +pinia 有个插件`pinia-plugin-persist`,可以对 store 状态进行持久化操作,在小程序中引入时则会提示 sessionStorage is not defined,原因是小程序中并无 sessionStorage 与 localStorag。所以还是得使用原生的数据缓存方法(getStorage,setStorage)来解决,或者将数据存至后端。 + +### 第三方组件修改样式 + +在 vue3 中要修改第三方组件库中的组件样式的话,需要使用 `:deep(css选择器)`,同时一般会在 style 加上 scoped,但如果在小程序中使用会发现子组件并不生效,而编译成 h5 却生效。我在这个 issues [taro 3.0 + Vue 中 scoped 在 h5 下生效,在微信小程序中无效](https://github.com/NervJS/taro/issues/6662) 下找到了解决问题 + +在 h5 模式下 scoped 会生成**[data-v-xxx]** 的属性,但是在小程序下则不会有,所以使用 scopd 在小程序中是无用的。就可以使用 cssModules,前提是需要在 config/index.js 中添加 cssModules 的支持,在上面 issues 中提到过,具体也就这样。 + +然后在 style 中添加 module 即可生效,都不用使用`:deep` 。 + +### invalid code, rid: 6249d588-48af462b-xxxxxxx + +服务器将 wx.login 获取到的 code 向小程序 API 获取 openid 的时候,如果提示该错误,那么大概率是 APPID 有问题,使用了测试号或者填写了错误 APPID。 + +## 总结 + +由于小程序(h5 手机端)应用写的少,所以在页面布局方便写的相对简陋,但本质与前端开发无特别大致区别,无非就是自定义了些相关的标签与 api,遇到时在查阅即可。 + +但相比传统 Web 开发而言,网站是无需审核只需要有个公网服务器就能访问,而小程序必须要经过审核才可发布,同时对小程序名称有严格的审核。主要方便在于微信用户的获取,同时提供完备的开发以及部署环境(开发者工具,云开发),加上用户数据分析等等。 + +在我发布完以及写完本文章后,我的建议是: + +> 如非必要,不建议上传小程序供他人访问,尤其对于个人开发者而言,网页版也许是个更好的选择。 + +在回到开发者的角度,Taro 对 Vue3 的体验远比 Uniapp 来的好(至少目前是这样的,个人感受),Uniapp 太依赖于自家的 Hbuilder,但目前 Taro 的 Vue3 无法编译成安卓(React Native),所以如果考虑安卓端与小程序的话,还是 Uniapp 略胜一筹,H5 两者区别不大。但如果只是写小程序,我还是会毫不犹豫使用的使用 Taro。 diff --git "a/blog/reference/2020 \302\267 \347\274\226\347\250\213\344\271\213\346\227\205-\350\265\267\347\202\271.md" "b/blog/reference/2020 \302\267 \347\274\226\347\250\213\344\271\213\346\227\205-\350\265\267\347\202\271.md" new file mode 100644 index 0000000..bc96ad3 --- /dev/null +++ "b/blog/reference/2020 \302\267 \347\274\226\347\250\213\344\271\213\346\227\205-\350\265\267\347\202\271.md" @@ -0,0 +1,356 @@ +--- +slug: 2020-year-end-summary +title: 2020 · 编程之旅-起点 +date: 2020-12-30 +authors: kuizuo +tags: [年终总结] +keywords: [年终总结] +--- + +写篇年记,记录一下自己这一年的所学。 + +能有幸在这个行业有两个关键因素 + +1. QQ 永久冻结 + +2. 易语言 + + + +### QQ 永久冻结 + +有些认识我的人可能会知道我的 QQ804493238 给永久冻结了,可以说这个号码是不可能再搞回来的。一个幸幸苦苦养了十年的 QQ ,说没就没的那种,与之相对应的就是游戏账号没了,没了游戏能干嘛,当然不能干嘛,生活还是得过的,但又要有个东西来打发时间,没错就是编程。于是高中毕业后的暑假,就开始了学习编程。不过这里要先介绍一下易语言,作为我的第一门编程语言。 + +### 初识易语言 + +初识易语言的时候还是在初中,那时候有个同学给我讲诉了他用易语言刷 CF 永久枪,用易语言写游戏外挂的故事,也是那时候我也才刚接触网络游戏,一把永久枪 888 还免费刷,别说有多牛逼了好吧。可以说从那时候开始,下了个目标,以后有时间一定要学易语言!(不过那时候没条件学或者说是给游戏耽搁了)果不其然,在高中毕业后,就开始了易语言的学习(最主要的原因就是号没了,完全没有心思再玩游戏)。 + +有关易语言的详细介绍我划分在另一篇文章 [易语言](/blog/easy-language) + +### HTTP 请求 + +这里我需要简单说一下这门技术,就是因为这门技术才能让我能写上软件,并且是有实质性用处的。有关这个介绍可以点击文章 [浅谈 HTTP](/blog/brief-talk-http/) + +在开学军训一个月期间,也没有放弃学习易语言,不过那一个月应该不算学易语言这个语言,而是在学一个网络协议 HTTP,在这行的术语应该叫 POST 与 JS 逆向(基于易语言),这里我需要放一个我当时学习的链接 [零基础易语言 POST 入门到精通](https://www.zygx8.com/thread-7162-1-6.html),导师教的非常好,是真心推荐,我从他的课程学到了非常多的知识,就凭我听了他的这一期课,就能自行写出超星刷课软件,我觉得这就足够我去报他的班。 + +### qq 机器人   + +非常可惜之前写了一段时间的 qq 机器人代码无法使用了,相关文章 [纪念 QQ 机器人业黑暗的一天](https://www.bilibili.com/read/mobile/7009209?share_medium=iphone&share_plat=ios&share_source=QQ&share_tag=s_i×tamp=1596387202&unique_k=vdFqN2) + +我用的是酷 Q 框架,用易语言写的代码,花了也有半个月的时间去写![image-20200926164358137](https://img.kuizuo.cn/image-20200926164358137.png) + +那时候封装了好几个功能,最终就因为腾讯的封杀,导致自己辛辛苦苦写了半个月的代码灰飞烟灭。这时候的心情与当初 QQ 被永久冻结一样,不过现在也看淡了,就算回来说实话也高兴不到哪里去,也就在以前群在多吹吹牛逼罢了。(泽宇是我之前玩网的一个艺名) + +这个 qq 机器人算是我手机端和电脑端一个变通的交互方式,以下是一些相关的菜单图,有些功能不方便展示,仅作为个人使用。 + +![image-20220517010245349](https://img.kuizuo.cn/image-20220517010245349.png) + +1. 对接网络验证服务端,购买卡密,实现购买卡密 + +![image-20200928204705890](https://img.kuizuo.cn/image-20200928204705890.png) + +2. 实现一些注册,例如一些软件注册给新用户多少时间使用,就不必在通过电脑,而是直接通过机器人发送命令来注册即可。 +3. 群监控,监控群里的一些不良信息进行撤回,监控刷屏进行禁言操作。 + +只要你想,然后给上对应的代码就行了,那时候也是沉迷于 qq 机器人花了很多时间写这些接口。这里的话我说说那时候我用机器人来写一个学校的用手机进行超星线下考试。 + +事情是这样的,这个考试是**用手机考试**的,但只能带一部手机,同时老师提供了题库,**允许带资料**,差不多就是开卷考。而且用于考试的软件(超星学习通)是**不允许切换的后台**来进行搜题的(或者说切换到后台会扣分),有些手机是无法分屏的(但是有悬浮窗)。这时候该怎么办,难不成真的去把题库打印一遍?还真有,十几张来着,先不说好不好,找一题都要找半天,有没有更有效的办法,有,我那时候就是通过机器人。 + +首先,将老师发的题库,存入文件(那时候的我还不会数据库,就只好读文件),然后通过则匹配,将对应的题目,将答案全部都记录到数组里面去。接着在通过给机器人发送对应的命令如 查题+关键词即可搜到相关的题目。这里就放一个我当初录制的一个视频,(其他人操作也就是通过悬浮窗来) + +![demo6](https://img.kuizuo.cn/demo6.gif) + +即便眼睛再也好,也比不过可靠的搜索,搜索可靠也不及关键词筛选,当初考试就是通过这样方式来通过这场用手机的考试,但是也有缺点,只能说当初写软意识不好,没考虑周全,像这个搜题我还要再打一遍【查题】这个关键词,很傻,而不是发送【搜题模式】,然后直接发送题目获取就行,再发送【退出搜题】(那时候花了一天时间去写)。并且对于这样的搜题还要切换特别麻烦,好一点的办法,有,自写安卓悬浮窗,不过现在也没这样的考试了,也是我后面学了点安卓后随手写的,悬浮窗大致如下。 + +![搜题悬浮窗](https://img.kuizuo.cn/floating.png) + +qq 机器人算是我特别想写的一个东西,但很可惜腾讯封杀外面大部分 qq 机器人框架,我使用的同样框架无疑避免,同时腾讯自己的机器人又不给开发者提供合适的开发接口,这就是腾讯吗,这本来就是腾讯的作风。 + +至于后续如果有时间,或者要发展 qq 群的话,肯定会重新再写一份 qq 机器人,到时候想要实现功能可就多了。 + +## 疫情期间,也是进步最快的时候 + +上一阶段学习期间,从 7 月到下半年 1 个月,这一阶段主要就是易语言与脚本开发,相关也就是上述了,而下一阶段,也就是从 1 月中旬到开学(5 月 23 号),也就是差不多这期间,开始了逆向初步学习和 Web 开发方面,而这段时间,可以说除了编程,就只有编程了。 + +先说下生物钟,晚上 6 点左右起来,然后早上 9 点左右睡觉,没错,这 4 个月基本上是这么熬过来的。(其中期间调整了两次作息习惯),因为疫情的因素,开不了学,又不方便出去,加上我本来也不喜欢出去,所以这阶段对我来说无疑是最好不过的,而这一阶段,也是我学习最多的时候,见识最多的时候,让我再一次感觉到编程的魅力,但同时让我感受到真正的编程和难。下面则会按时间顺序简单介绍下我学了什么。 + +### 资源共享吧 + +首先要提一个这个学习论坛,因为我在这个论坛上找到的很多教程,可以说没有这个论坛,我视频教程都不好找,先放个论坛链接 [资源共享吧](https://www.zygx8.com/forum.php),首先这个论坛从名字上应该可以知道是资源共享的,是关于编程相关技术方面的资源,可不是那啥,我先放一张图片,看看到底都有啥资源。 + +![image-20201005025855844](https://img.kuizuo.cn/image-20201005025855844.png) + +别说,基本上有关编程的你在这都能看到,当然肯定不是免费了,是需要交 VIP 的,但只要 199 元,终身高级 VIP 会员,别提有多值了,你知道外面一套培训有多贵吗,这我就不提了,自己搜一搜就知道了,我在写超星刷课不是提过一个讲师,我报了它的班,4000 安卓 VIP+3000 网页逆向 VIP 来着,而这里你只需要 199 元,并且在该论坛你也能看到他的一些相关课程。当然,和培训相比还是有一定的区别,但在这里的教程真不差。 + +关于付费学习,可能有些人不解,为啥要收费,没为啥,就是你听付费的课程,能比别人学的快,能少走点坑路,很多免费课程要么就是为了推荐他的付费课程,要么就是为了推荐他写的书,总之,免费之中必有付费,单纯的免费课程能学,但想要走个捷径,付费应该是最快捷的方法。 + +是真心推荐这个论坛,一点广告费都没收,因为在这个论坛上我下载了特别多有关编程相关的知识,奈何时间不允许,不然我真的都学了。正是因为我在这个论坛上学习到特别多的知识,这就是我推荐的理由之一。 + +下面的大多数学习都是基于这个论坛上的视频教程。 + +### 安卓逆向 + +这上半年,我也只会网页端的数据分析与 JS 逆向,很多时候并没有网页版的,只有安卓应用,这时候想要偷其中的 api 接口,找到对应的加密点,该咋办,学呗。就必须要会安卓逆向,并且这个不比网页端简单。 + +在我开始提笔写的时候,已经有半年没怎么碰过安卓方面的了,我都快忘记了我安卓逆向的好多知识,而且当初还没有写笔记的习惯,就连我一开始怎么入门的都没什么印象了,总之就是看了教程,然后一步步照抄,视频教程怎做就怎么仿,就完事了。 + +同时也正是因为安卓,花了 10 天左右用 2.5 倍速度把毕向东的 java25 天速成教程看完了,而 java 才算是我真正第一门主流的编程语言,之前的 javascript 我是连 ES6 语法都不会的,甚至很多基本的语法我都不知道。但学完了 Java 的基础语法,但对于安卓逆向或者开发来说还是差太多了,虽说对于当时的我看的明白,但实际上整个安卓的项目结构我依旧不明白,不会点开发去搞逆向是真的折腾。 + +合理来说我安卓逆向压根就没学完,或者说我只学到了 java 层的源码分析(java 是真的好反编译了),我还没什么能拿的出手的东西,没有破解过安卓软件,只是分析了跟网页端差不多的 HTTP 请求,差不多的加密算法,在这方面我还真的不知道该说写什么,即使说了,很多没了解过安卓逆向人也不懂,后续的话会再学安卓这方面,从开发到逆向,到时候会这方面的知识在进行一个分类总结。(主要是我真的忘了太多了) + +### Auto.js + +我先简单介绍一下这个是什么,这个也就是专门针对安卓端的无 root 脚本操作,看到后缀名你应该能想到 js,正是用 JavaScript 作为脚本语言。可以说用这个开发工具也能开发些安卓软件,但主要还是针对脚本操作,比如做一个 qq 自动点赞的, 贴吧签到脚本,抖音自动刷视频,双十一用过淘宝叠过猫猫吧,用 Auto.js 也能写个自动浏览商品,刷金币的,此外有太多例子了。 + +在之前的脚本操作,我也只会电脑端的,而对手机端无奈只好投屏到电脑,通过电脑的鼠标操作来实现脚本,而现在有了这个软件,则就不用在连接电脑,直接将写好的脚本打包成安装包安装,点击运行即可。但对比原生安卓开发,这个开发工具还是略显下风,不过对于安卓的自动化操作已经足够了,我也只说说我用这个写了个什么软件。 + +#### 钉钉签到脚本 + +像抖音自动刷视频和贴吧签到这些我就不多举例,主要还是这个软件,听名字就知道是钉钉签到的,有些在疫情期间,学校老师又要求同学使用钉钉,并且签到,但是有的同学就是会忘记签到或者没起来(说的是我),怎么办,记旷课?这不写个脚挂在那边时间到了自动签到呗。 + +![ddqd1](https://img.kuizuo.cn/ddqd1.png) + +这是我当时写的页面,只需要填写对应的课程名和开始的时间即可,时间一到,手机自动亮屏,开始签到。主要的代码就下面这一个函数 + +```js +function ddSign(courseName) { + launchApp('钉钉') + waitForActivity('android.widget.FrameLayout') + + let course = text(courseName).findOne() + if (course.parent() != null) { + course.parent().parent().click() + } + + let sign = text('群签到').findOne() + if (sign.parent() != null) { + sign.parent().parent().click() + } + sleep(3000) + + if (desc('群签到')) { + sleep(3000) + let btn_sign = className('android.view.View').desc('签到').findOne() + let result = btn_sign.click() + Log('签到结果' + result) + } else { + toastLog('不在群签到页面') + } +} +``` + +启动钉钉,等待钉钉启动完毕,找到对应的课程名,点击课程名,找到群签到按钮,点击群签到按钮,进入群签到找签到按钮,点击签到,签到成功。就这么完事了,脚本就是这样的。 + +不过最终有个缺点,对于一些没有 root 的手机,需要每次运行就要不断的打开无障碍服务,特别繁琐,但没办法,这是安卓的机制问题。 + +这里要提及的一句是为啥不用 HTTP 发送请求要来签到,而是要这种脚本方式,对比一下你就会发现,用脚本写基本无压力,就是简单判断一些字或者图在哪,然后点击对应的坐标,而通过 HTTP 请求的话,一是要过钉钉登录,二是要处理各种加密算法。不过钉钉登录算法难不难我就不知道了,我也懒得分析,加上正好学了 Auto.js,索性就写一个这样的签到脚本得了。但说实话签到就不应该这样用这种定时脚本,而是应该选择协议更好。 + +### 深度学习之图像识别 + +可以去了解一些深度学习,颠覆我对机器的认知,至少让我又觉得编程的强大,重拾学下去的信心。首先,先看张图片 + +![QQ图片20201004030419](https://img.kuizuo.cn/QQ%E5%9B%BE%E7%89%8720201004030419.png) + +看图也能看明白,这个就是识别一个缺口的图片软件,可能对没接触过这行业的人觉得这并没有什么软用,这个滑块的意义主要还是防止人为操作和机器操作。对于人而已,自然而然知道缺口的位置,但是对于机器而言要怎么知道这个缺口的位置,就针对上面这类图片,可以通过图片颜色深度来定位到缺口的地方,同时也可以使用深度学习,简单来说深度学习就是 AI,不过这里的 AI 是用来让它识别这个缺口,至于怎么让它识别和对应的算法我就没过多了解了,我接触这个主要还是用现成的模型来训练识别的。说一下我是怎么让机器训练的。 + +这个过程其实就跟教小孩一样,现在有一个小孩,他不知道这个缺口的位置,这时候我告诉它缺口的位置在那,对应的操作也就是标注,如下 + +![image-20201004031417625](https://img.kuizuo.cn/image-20201004031417625.png) + +我把缺口的地方标注一下,并记录对应的坐标,然后告诉这个小孩,缺口是我标注的地方,你下次遇到的时候记得是缺口这样的,但是小孩毕竟是小孩,这是我换一张类似的图片,这时候他可能就蒙了,所以就需要不断的给他标注好的缺口图片,让这个小孩一直看,一直记,直到下一次看到一张陌生的图片,但是它已经把之前训练的给记住了,很快他就能找到缺口的位置,这整个过程其实就是告诉小孩,然后让小孩一直训练,这就达到了我们想要的目的,这个小孩也会知道缺口的位置了。现在把这个小孩换成机器,那么这就是深度学习,并且机器是机器,可以封装的“记忆”远比人类可比,人是会感到疲惫的,而机器不会。再比如下面这张图片 + +![image-20201004033445666](https://img.kuizuo.cn/image-20201004033445666.png) + +对于我们来说显示屏,键盘等等这些在常见不过了,深度学习就可以做到识别图片对应的物体分别是什么,不过这要的训练量就比上面那个滑块大多了。我这里简单说说主要的通途,现在我想拍一张人脸照,还有风景照,但是时间旧了,我想找可能就要一定的时间,这时候就可以通过识别图片,进行分类,例如头像照,风景照,食物照等等,现在大多数相册都有上述图片分类的功能,不止如此,通过训练还可以识别文字,快递单号,车牌号,识别人脸等等,总之想训练的东西,都能训练,不过就是吃训练量和显卡,一般来说都是用现成的模型直接用就完事了。对我目前而言,我深度学习学的是非常浅了,也只会用用模型,跑个显卡训练训练。 + +此外,既然图片训练是这样的,能不能训练其他的,当然,语音识别,模拟一些明星的声音,游戏 AI,甚至让机器自己去学习怎么打游戏,游戏内的人机也就如此,通过训练满分作文,实现写一篇条理清晰的作文等等,太多例子都能看到人工智能。 + +关于这方面我也不敢做过多讲述,毕竟也还没开始从事这方面的真正学习。但确实,惊艳到我了,也让我想去学习这方面的知识。想自己写一个属于自己的语音 AI 助手,训练出一个能自己玩小游戏的模型,总之想学的太多了。 + +### Chrome 扩展开发 + +关于这个的话可以查看我写过的一篇文章 [Chrome 插件开发](/blog/chrome-plugin-development),我总结了一下我那时候学习的插件,和自写的一些模板,不在这做过多赘述了。 + +疫情这期间说句闭关学习应该不成问题,不过学的东西远不止于此,主要其他的没写出啥玩意,而且学的很浅很浅,比如汇编基础(不过应该已经忘得差不多了),又深入学习了编写游戏外挂,TCP 协议(和 HTTP 一样),还有就是一些开发工具的使用。期间也写过一些东西,例如疫情填表,网课签到等等,不过都是临时写来自用的,也懒得放图了,原理基本和超星刷课一样,有时间再整理一下。 + +## Web 开发 + +在进入了这个阶段,我就很久没再用易语言开发新的软件了。到目前为止也真的快半年没怎么动易语言的代码了(也快半年没怎么写项目了),就那个超星刷课而言,没想到半年后还能用。而这一阶段主要接触的也就是 Web 开发方面。 + +实际上,我在接触易语言就已经接触了一些 Web 方面,因为要针对网页的一些操作就不得不接触这些,比如 HTTP 协议,DOM 解析,CSS 选择器,JS。不过那时候这对真正的 Web 开发远远不够。当然要说一下为什么会想学习 Web,在学 Web 之前,实际上还接触了一些 js 高端操作,例如 AST 语法树(混淆 js 代码),WebSocket,但发现有点没学明白,用都有点没用明白,然后就是想搞明白这些操作,于是进阶学习 js。 + +其实决定性的也就是一门编程语言——JavaScript,那时候感觉到易语言的不足,恰好手头在学的一门编程也就是 JavaScript,于是就开始了 JavaScript 的学习,可以说不是 JavaScript,Web 想都没带想的。js 作为我目前的主力语言,就目前而言,基本屏幕上,必有 vscode 的痕迹,而且还是 js。js 吸引我的一个地方就是其他语言都能调用,加上语法清晰,不过要我硬说 js 哪里好,我也说不清楚,可能这就是对一件事物的热爱吧。 + +于是改变了当初原有的想法——搞安卓开发与深度学习,转行到 Web 开发去了,和学习其他技术一样,找教程,跟教程做一遍,自己在举一反三一波才不多就明了。 + +Web 学习确实比逆向轻松,并且学完 Web 能写的东西也算比较多。也算可以自己动手设计出一个界面或者功能出来,上手特别容易。这里我就不献丑了,之前前端写的那些代码真没脸丢出来。就连当初的笔记我都不敢看,写的是什么玩意,竟然还是在 js 文件上用注释来记录笔记。 + +就这样学习了几周,然后收到了开学通知,没错是开学通知,疫情的时候,开学一个月,于是就停滞了学习近一个月了,前面也说到,生物钟的问题,开学了就不得不强行调整生物钟,把我学习节奏给打乱了。其次突然开学,心态是真崩了,开学各种繁琐的事情搞得压根学不进去。原本不出意外在保持这样的学习状态一直学下去,暑假结束前就能把 Web 大部分知识都能搞完,然而却拖到现在。 + +不过也不能说开学不好,从 1 月 10 号放假,到 5 月 23 号开学,在家好像也就出门了 2 次,要是再这样拖到暑假估计身体可真要出问题。停滞了学习,但保住了身体,是赚是亏,对我来说血亏吧。 + +然而暑假由于学车的因素,加上外出玩了一波,整个暑假并没有像去年那样全身心投入去学习,不过业余时间会去摄取有关这行的知识与科普,也就是刷刷知乎,公众号,博客文章等等。而也就是在这段了解到了 Nodejs 与 Vue,暑假开始学习。 + +### Nodejs + +不过学前端,Nodejs 基本是必接触,基本可以说只要是个前端开发者,就肯定会 Nodejs,关于 Nodejs 不做过多介绍,我能推荐的也只是一些 npm 包,总之在这期间 也就是只学了点 Nodejs 的基本使用,还有 npm (Node 包管理器)。 + +主要接触两个 Web 框架 Puppeteer 与 Express,关于这两个我在 Node 的那个目录内有简单说明到,这里也就不放链接了。这里简单说说 Express + +### Express + +学 node 大多数就是为了搞服务端,那么肯定也有对应的服务端框架,Express 就是其中之一。 + +在此之前,我是写过点后端的,也就是一个网络验证的系统,不过那时候是基于易语言和 HP-Socket 的库,但有一个很大缺陷,就是我这个后端服务器编译出来的是 exe 文件,也就是只能在 windows 上运行,这对 Linux 系统服务器很不友好,于是乎就想搞一个基于 Express 的网络验证服务端。 + +而这期间才算学会用 Mysql,之前易语言写的网络验证时连 Sql 语句都不会,还都是用易语言自带命令帮我封装好来进行增删改查。也简单的将易语言写的网络验证接口全都搬运至 Express 中,然而有个问题来了,我要怎么查看数据?难不成直接打开数据库查,怎么可能,而这就开始了 vue-element-admin 开发,后文会说到,因为这里要涉及到一个新的技能——Vue。 + +### Vue + +可以说没有 Vue,我多半不会去继续学习前端(怎么感觉这句话好像说过了),和大多数人学 Web 一样,都是从基本的 HTML CSS JS 这前端三件套开始学起,然而在学到一半的时候,想写一写 Web 小项目的时候,就需要在一个 HTML 上写上大量代码,并且页面还不好设计,并且在用了 Express 一段时间后,发现如果要做动态网站的时候,就需要将数据渲染到对应的模板语言上(如 ejs),于是想知道有没有啥能解决这上述问题,于是便开始搜索有关前端的一些相关知识扩展,果不其然,困惑了我一段时间是否继续学习前端,因 Vue 而解惑。 + +如果要我评价一句 Vue,我只能说后悔学 Vue,后悔学晚了。我花了挺多时间在原生的前端开发,在了解到 Vue 时,还在用着 Jquery。Vue 让我继续学前端并不是数据交互方面,而是模块化,在我接触原生前端开发的时候,这个问题尤为困扰我,每次写一个页面都需要重复大量相同代码,即便是复制粘贴也觉得烦躁,而 Vue 组件化就不一样。例如我当前的博客页面,你会看到导航栏,侧边栏,与正文,评论系统,乃至打赏按钮都是一个个组件拼接而成,只需要在单个 Vue 文件中,引入对应的组件,然后想 html 标签那样直接写在视图层上即可显示,当然,相关代码也展示了,有关 Vue 的我也都放在 Vue 这个目录下。 + +然后就是基于 Vue 而开发的组件库,在 Web 开发难免不了页面设计,然而并非所有程序员又兼设计师,想要设计出一个好看的按钮或其他组件并非容易的事情,而组件库便提供了一个好看的组件供开发者使用,例如通过引入 element 的样式与组件库 + +```html + + + + +``` + +然后在官网中找到需要的组件,如下按钮 + +主要按钮 + +而对于的代码也就是 + +```html +主要按钮 +``` + +只需要简单修改一下参数就能生成相应的组件。哪里还需要花费大量时间去写 css,不断改参数,上手即用,快速成型。 + +Vue 模板语法也是,显示后端传递的数据也简单,比如下面这行 + +```html +

标题: {{ title }}

+``` + +只要接收到后端发送来的标题,即可渲染在`{{title}}`内,此外还有数据的双向绑定,关于这些等等不在这细说了,需要的话可以自行翻阅 Vue 文档。 + +而且就这么说吧,前端框架如果不学 Vue 和 React 其中之一,面试都不给你面试的。甚至可以说,Vue 和 React 是前端开发必学之一,不学就跟不学前端没两样。 + +同时 Vue 还是国人尤大大(尤雨溪)开发的,在国内生态好,社区活跃高。基本上目前国内的大部分 Web 项目都有 Vue 的身影,同时 Vue 不止于前端,小程序的开发等等也有它的身影,如 uniapp 就是使用 Vue.js 所开发的。 + +Vue 能做的太多了,如果你恰好准备学 Web 方面,尤其是前端,毫不犹豫,直接学 Vue,Vue 在 github 上排名第三,star 高达 175k,凭这,就足以去学习一番。 + +### Vuepress + +Vuepress 一个 Vue 驱动的静态网站生成器,可以用 Vuepress 来写文档,写博客,而我目前这个博客便是通过 Vuepress 实现而成。关于这个博客搭建过程就不在这花费口舌了。我有一篇文章专门讲述这个博客的搭建过程。 + +### 对 Web 开发做个小总结 + +可以说没有前半年易语言学习所接触到的 Web 知识,就别说 Web 开发了,JS 估计都不会了解到,也因 JS,误打误撞闯进了 Web 开发的坑,让我领略到更多知识。 + +我算是作为 JS 语言的粉丝,听说过一句名言:凡是能用 JS 实现的最终都将用 JS 实现。即便不理解这句话的意思,但依旧这句话很喜欢,也许这就是对 JS 语言的一种爱吧。 + +![javascript](https://img.kuizuo.cn/javascript.jpg) + +写到这,其实 Web 开发真就要告一段落了,实在是不想在折腾 Web 了,主要就两点 + +- 不会设计,不知道该设计什么,实现什么功能 +- 后端数据接口写的非常痛苦,Express 折磨了我一个月 + +在页面设计方面,有组件库很方便,但是不知道要写什么好,就那个网络验证而言,我已经绞尽脑汁想添加写功能,但是奈何后端写的折磨,很多我都写不下去,就比如用户-角色-权限 这三者关系,我只写了用户-角色 还有权限没写,还有积分,与 nodejs 模拟数据这些写的都特别鸡肋,实在是写不动了(甚至说实在不想写了)。想转型写点其他的,后续有时间才把这个在做过多优化。 + +目前把博客和网络验证基本搭建好,现在脑子只想好好写写博客,把这一年要学过的东西好好巩固一番,尤其是这一段 Vue 的学习,收获到太多知识,都有点来不及消化。后续会将这些都部署在该博客上(不妨点个收藏收藏个书签呗) + +不过我目前的 Web 开发还有很多没学到,而关于 Web 后续的学习的话,以目前的话应该是不会在花费大量时间去学了,学了也有半年了,自我感觉 Web 这方面还是不适合我,但确实这半年的 Web 学习挺充实的,不会接着深入学习,会用已学过的知识去写一些东西出来。(主要还是太想学习其他技术了) + +## 总结这一年学习经历 + +这一年学的确实多,相比于刚接触这行一年的人来说的话,我挺知足这一年能花费大量心思在这上面,同时还能坚持学下去,并爱上编程,于是乎从数学系转到了计算机系,但学还是自己学自己的,在我看来学校老师能教的,不如我课下自己学习的,况且编程单靠别人教是教不出什么玩意的,还真没听说过什么大神是另一个大神教出来了。 + +不过正是因为这种心态,也导致如今我连学校的课基本也都不会去上,能旷的课尽可能旷,旷课补觉,或者是宅在宿舍,坐在电脑前,盯着这赏心悦目的代码,和平时分相比它不香吗。既然旷课,那怎么能不挂科,虽然都是公共课,不过挺无所谓了。说件好玩事情,我大学英语挂了,没错,我英语说实在话就是不行,甚至我都没打算过四级考试,但这影响编写代码吗?不言而喻。 + +专业课的话,有的老师一般会认可我的能力,允许不去上课只考试就行,或者是不去也不会记名字,甚至和老师说一声不去就完事了。至少在我看来有时间在学校拿个文凭,不如直接用自己写过的项目去面试,至于看学历还是看能力,面试官一般都不看,因为只看简历呗。目前的话除了有关编程的事啥也不想干,估摸着在学一段时间就说不定真就辍学了呗,不,说不准还是给学校开除。 + +### 身体状态 + +早已习惯了凌晨晚睡晚起,早上的课基本也都是旷了的那种,就已经习惯了旷课。再看看下午的课,不出意外的话,一般就是坐在电脑前到,到点了吃个饭,接着到凌晨 3,4 点,每天反复这样。我都不敢说我在这方面有天赋,我只觉得我每天花费在这方面的时间是别人的几倍罢了。 + +**哪有什么天生,一切不过是摧残出来罢了。** + +说实在话,挺享受整天在电脑前办公的生活(相比于躺在床上玩游戏而言),不过熬夜,久坐,与长时间盯着屏幕,甚至是掉发,身体难免有些遭不住,并且我是能感受到,身体一天不如一天了。但它改变了我原有枯燥的生活,成天的游戏与娱乐,迷茫到不知所措, 如果没有 QQ 冻结,还有易语言,JS 这些,说真的,我现在极有可能还在某个床角玩着游戏,还对着屏幕傻笑(现在就特别想笑)。即使都是宅在家的生活,但编程能成为我养家糊口的资本,而游戏,它不行!甚至还可能成为我颓废的资本。 + +### 事实告诉我,我还有很多要学 + +就在 9 月初,参加了一场网络安防(CTF)的比赛,真的是小巫见大巫,瞬间觉得自己还有很多要学的,虽然其他选手都很强,但也印衬了自己在这方面就是技不如人。说白就是自己菜,就是学的不够多,要是能在多学一点,说不定就多解一道题,多拿一点分,说不准就苟一波,吃个烂分。而事实是这是场没拿奖的比赛,而我是这场比赛的失败者。 + +不过有参与必有收获,确实让我学到了 Web 的一些渗透知识与一些工具的使用,也让我更加觉得外面的大佬是真的多,自己离他们还需要更加努力一把,这场比赛值了。 + +### 给别人的一些建议 + +我也仅能给别人一些我学习上的建议,也仅仅是当前,毕竟我接触这样也才一年多。我其实都不推荐去学编程这玩意,但我常常和别人说多花点时间在这上面,能写出些东西来。话是这么说,但真正肯去学的又有几个,即便学了坚持下去的又有几个,即便花费口舌去和他们介绍,但多数觉得难,但你说这个东西难吗,很难,但你说学不进去吗,也不至于,如今网络上生态这么好,学习编程的人越来越多,相互帮助的人也多,只要肯学,肯下功夫,随便一百度就有的结果,怎么可能学不会。 + +#### 多搜索,少问人 + +我这一年学习中,基本上就是看视频跟着视频教程一步一步照做,我一开始学习的时候也会在群里问过别人,也私下问过别人,但是有用吗?压根就没用,基本就没什么人会去鸟你的。百度它解决不了吗,如今强大搜索引擎会是你解决问题的最好选择(甚至我帮别人解决问题还是我将百度结果链接给别人)。有两句话,百度 5 分钟,问人 2 小时。百度能解决你百分之 99 的问题,但问人并不一定解决其中的百分之 1。 + +![006ARE9vgy1fwntelg0mlj30b40b4gm1](https://img.kuizuo.cn/006ARE9vgy1fwntelg0mlj30b40b4gm1.jpg) + +**没点自行解决问题的能力真心劝别碰这行。** + +我现在都有点反感那些没自行尝试,就直接问人的那种,有什么好问的,要么你学的不够多看不懂呗,要么就是你那里少做了什么步骤,大不了重新卸载安装重启来一遍嘛。主要没点自行解决问题的能力,到时候项目一来,出了一个 bug,或者开发环境与生产环境有问题,你问谁去?问了会回复你吗?能不能先自行看看是不是哪里少改的,或者版本的问题。说太多就是想强调一点,自行解决很重要,决定你适不适合干这行。 + +话也不能说的这么绝对,有时候搜索引擎确实不能有效的给到你想要的答案,比如我所用过的一个博客主题,是有交流群的,但有时候我百度了半天就是得不到我想要的结果,然后一问群,**可能**会一些**热心伙伴**会给你**正确**的答案,但往往是在你**实在解决不了**的情况问。毕竟,问了不一定答,但是不问一定不答。 + +#### 复制粘贴也是一门技术 + +别觉得偷别人代码有什么丢脸了,有的人甚至连偷都不会偷,而你早早就把项目给搞定了,别人会认可你的过程还是认可你的结果,必定是结果好吧,而且干这行的,基本上你见不到一个程序员是真的每一行每一行开始写起,而用的最多的三个键 Ctrl C V,不会真有程序员一行一行的敲吧,不会吧,不会吧。 + +![7280a4701a0daf98](https://img.kuizuo.cn/7280a4701a0daf98.jpg) + +当然,上面也只是说笑了,只是现在都是模块化编程,什么意思呢,就比如我要实现一个自动发送邮件的功能,怎么办,直接搜索,比如 nodejs 中实现发送邮箱,于是就搜索到了 nodemailer 这个模块,二话不说,直接运行官方的 demo 案例,发现可以运行,能达到我想要的目的,接着就是自己简单的封装一下,直接给项目来使用,这不就成了吗? + +然而要从 0 开始写一个发送邮件的服务端,别有这种想法,大概率不会想写的。就我目前而言吧,除了一开始学习新的技术外,会跟着敲一遍磨磨手感,到后面基本上都是从网络上偷各种源码,然后自己看一遍主要的地方是怎么实现的,然后对源码进行修改来达到我需要的目的。如今网络环境这么好,想要的功能随便一搜就有了,如果你能做到改别人的代码,那么别人这份代码你肯定也学进去了不少。为啥还要自己造轮子,尽可能利用别人已经写好的代码,而不是自己在手动写一份类似功能。 + +## 定一下明年的目标 + +到目前为止,我还有好多想写的东西没写,好多好多都写了一半由于一些某某原因没继续写下去。同时还有好多想学的技术,好多都是之前学到一半卡着就没接着学下去了。至于能学多少,尽力而为吧,希望明年的这个时候,在回头看看自己所写的一些代码,都能感到一些惬意。 + +### 进阶 Web 开发 + +Vue 才学了点皮毛,很多 HTML5 特性都没玩明白,CSS 也只会基本样式,就别说设计网页特效啥的,能看的过去就不错了,如果有时间的话一定会去接触写 React,毕竟和 Vue 一样,都属于人气爆棚的前端框架,很多大厂都使用 React 作为前端框架使用。 + +像 Webpack,Eslint 等等前端工具库,再如 JavaScript 的超集 Typescript,这些肯定都会去学习一番。 + +### 进阶 Python + +学过点 Python,但并非精通,有特别多的库都还没用过。主要说说为什么想学 Python,最主要的就是深度学习这方面,在前面我也写过,深度识别之图象识别,其次就是模拟数据请求这些,如果知道 Python,说到爬虫自然不陌生,不过爬虫对我来说并不敢兴趣,主要是 Python 好做模拟请求,http 协议这些,在易语言这些操作都是乱写,基本没啥难度,当然,http 协议难得不是模拟数据请求,而是在逆向分析数据加密,风控算法,所以网页逆向的基本功还是要有的。 + +当然,如果有可能还会接触一些 Python 的后端框架 Djongo 和 Flask,看看会不会符合自己的编程风格,再考虑是否要重写一些后端接口,当然最主要的还是想学习 tensorflow,我本地环境都搭建好了,然而却没有花大量时间去在这方面学习! + +### 安卓逆向与安卓开发 + +干这行前,我就是个重度手机使用者,或者说是玩机者。对于手机特别感兴趣,刷过机,拆修过手机,手头目前就有 5 台手机,对于电子设备可以说是又爱又恨了。用过的 APP 多,然而有些 APP 并不能达到我想要的目的,或者说有限制,于是就想尝试利用逆向技术去修改,奈何那时学了皮毛的安装逆向,根本不够用,也感到困难,于是就没继续学习,一开始的本心还是想搞安卓这方面。 + +如果可以的话,我更希望的是当一个安卓开发人员或者逆向分析工程师,而不是一个 Web 开发人员或者网络安全工程师。至于最后能如何,谁又能说的准呢。 + +### github + +就目前而言,git 还有很多没玩明白,主要手头也没啥好开源的玩意,明年,一定多刷 github,当然最想的还是搞一个开源项目,还能在 github 上小有知名,那干这行真就无悔了。有点小野心,但以目前能力估计是够呛,想想就好,到时候技术上来的指不定就真成了呢。 + +这不才学了一年,才 20 出头,留给我的时间还多着很,在学个十年不成问题吧。但话是这么说,自己心里很清楚,已经有点学不进去了,就我写 Web 项目的这段时间,写着写着就去搞其他东西去了,实在是写的憋屈。总之已经没有一开始那么想学,那么有想法与灵感,那么肯于去学习新的技术,估摸着在学几年也许就会停滞于学习。 + +本以为年轻有的是精力,还曾妄想把好几门技术都学进去,但事实告诉我,能精通一个就不错了,学那么多意义又在哪,只是为了满足当初高中不认真学习的而又渴望学习的心吧。 + +总之,能学就尽量学,明年,理应更强。 + +## 感谢 + +最后还是要感谢互联网的前辈们,与当下互联网的环境,不然的话我还可能在迷茫中四处摸索人生的真正意图,耗费着年轻所带来的资本。往着曾经写过的代码,眼角不禁也会湿润起来(绝不是熬夜太困),感叹当初为啥会去学这东西呢,也许这就是编程所带来的魅力吧。 + +

写于2020年11月18日    By 愧怍

diff --git "a/blog/reference/2021 \302\267 \344\274\221\345\255\246\344\270\200\345\271\264.md" "b/blog/reference/2021 \302\267 \344\274\221\345\255\246\344\270\200\345\271\264.md" new file mode 100644 index 0000000..30d8160 --- /dev/null +++ "b/blog/reference/2021 \302\267 \344\274\221\345\255\246\344\270\200\345\271\264.md" @@ -0,0 +1,214 @@ +--- +slug: 2021-year-end-summary +title: 2021 · 休学一年 +date: 2021-12-31 +authors: kuizuo +tags: [年终总结] +keywords: [年终总结] +toc_max_heading_level: 3 +--- + +当写这篇年终时,都已过于数月了。今年 (2021 年)休学出去工作(创业);加上 2022 年 1 月闭关安卓逆向学习,所以便没有抽出时间来完善年终总结。 + +所以说要写年终前一定要趁早,平常也要时刻保持记录的习惯,这样年终总结的时候思路才清醒,看到平时记录的点滴就能一时刻地回忆起所有细节。 + +每次写年终总结时不时也会潸然泪下,写的时候就需要不断的回忆过去,而往往过去的某些时刻的做法会让自己觉得是不是有个更优解?常常会回忆起过去这一年所经历的往往,难以忘怀,不知从何写起。 + + + +## Web 开发 + +在这过去的中时间内,我已经从一位逆向爱好者的转到 Web 开发行业上。在上次的记录中我也仅仅只是搭建了一个博客,还是基于 [Vuepress](https://vuepress.vuejs.org/zh/),不过由于拖更过久,于是就索性使用 [Docusaurus](https://docusaurus.io/) 做为未来的博客。像一些主流前端框架 [Vue](https://v3.cn.vuejs.org/),[React](https://reactjs.org/) 以及 [Vite](https://cn.vitejs.dev/) 和 [webpack](https://webpack.js.org/) 构建工具使用过,期间也不断尝试新的技术栈,了解其新特性,所以实现些基本的前端页面或功能倒是不成问题。 + +但 Web 开发可不仅仅只是由前端页面构成的,虽然对于上面静态站点的博客而言,那确实够。不过想要做到一些页面阅读量,以及评论相关的,就必须涉及到数据交互,也就是后端服务。后端服务所选用的语言可就多了,例如 nodejs、java、php、python、go 等等,虽说都有接触过(尝试搭建过后端服务与部署),但 node 还是我的后端开发首选, JavaScript/TypeScript 是我目前用的最多的编程语言,这其中就使用到[Nest.js](https://nestjs.com/)这类 Node.js 版的 Spring 框架,同时也接触到 [TypeORM](https://typeorm.io/) 这个 ORM 框架,操作数据就如操作对象一样,可以不用写 sql 就能完成基本的 CRUD。同时也接触到 Java 的 [Spring](https://spring.io/)、Php 的 [ThinkPHP](https://www.thinkphp.cn/)、Go 的 [Gin](https://gin-gonic.com/zh-cn/)、Python 的 [FastAPI](https://fastapi.tiangolo.com/zh/),这些语言中的后端框架。同时数据库方面也学习与使用到 [Mysql](https://www.mysql.com/)、[MongoDB](https://www.mongodb.com/)、[Redis](https://redis.io/)、[Elasticsearch](https://www.elastic.co/cn/elasticsearch/),这些 SQL 与 NoSQL 数据库以及搜索和数据分析引擎。 + +可以肯定是的未来的编程日子里有一大段时间估计也与 Web 开发息息相关。 + +不过在年终这并不想介绍在学习期间所涉及的项目,因为这太啰嗦了,导致有挺大一部分时间都是在介绍,而不是在总结,违背年终总结的意义。**(学习的)过程往往不是人们所在意的,人们往往在意的是所导致的结果。** + +## 看书 + +也是在今天开始去看书/看文章,加固对已有技术的理解,基本上每天凌晨 0-2 点的时间段都在看技术相关的书籍。主要都是针对 JS 相关的书籍与一些其他书籍,以下是我今年看过的书 + +- 《JavaScript 高级程序设计》(第 4 版,简称红宝书) +- 《重构 改善既有代码的设计》(第 2 版,JS) +- 《JavaScript 设计模式与开发实践》 +- 《深入浅出 Node.js》 +- 《Visual Studio Code 权威指南》 +- 《深入浅出 Vue.js》 +- 《Vue.js 设计与实现》 + +## 经历总结 + +相比学习记录而言,我反倒是想总结个人初入社会中的一些机遇与所做(缺点与不足),以及未来遇到这类情景能否有改进的地方。 + +### 深思熟虑 + +反思我当时面对这种情况,我当时的做法是否合理?是否为最优解?是否有考虑他人感受?是否有考虑这么做未来有什么不利?是否...? + +事情发生后能否做到不后悔?能否想过如何挽回?能否总结下次遇到这种情况又该如何做?...(语塞片刻) 还能有下一次吗? + +很显然,在这一年当中我并没有很好的反思与总结,而是直到出现一些严重性的结果,我才会开始考虑此后果与弥补。 + +而休学便是我当时面对情景中的做法之一。 + +当时的我没有思考我休学后所会给我带来的不利,如后续学校课程的变化能否顺便完成毕业,是否想过休学后的生活,复学手续的办理,等等太多要思考的了,然而当时的我只思考到学校课程的无聊,不如出去工作闯闯,这可不比学校每天枯燥的生活来的丰富。我甚至还幻想着我休学出去,技术特别厉害,是不是回去就能直接免修课程,直接当大三来读。然而这种想法的天真程度不亚于一个三岁小孩问父母我为什么不能像鸟儿一样在天空中遨游,而现实生活是你只能在地面上爬行。 + +在学校跟着校方的课程,修满学分,遵守学校规定,完成日常内务卫生,方可毕业。而休学就是休学,休学期间学校只保留你的学籍,你在休学期间外面所发生的一切都与学校无关,如果在休学期结束后还未办理休学手续,则视为退学处理。 + +到了复学的日子,当时的早上我回到学校办理复学手续,准备重归校园生活。但由于疫情的缘故,学校是不让正常进出,需要使用学校 app 上申报进出码,在当时的我无论怎么申请都无法通过,提示找不到辅导员,因为我休学期间的又重新分配了新的辅导员,也就导致我无法申报进出码,当时的保安无论如何都要学生凭进出码才可进出学校,哪怕我把学生证,以及我休学时的手续,保安与辅导员的沟通,都不允许我进去学校。即便我家属陪同的情况下,依旧等了越 10 来分钟才方可放我进入学校。 + +现在回想当时如果我的家属没有陪同,也许这一天这个保安都可能不放我进去,保安估计是因为不希望家属在门口等候太久,同时也因为我复学的情况,所以破例放我进入学校,办理复学手续。 + +扯了这么多,也该说说休学的原因了。 + +当时(2020 年 12 月),我写了一个软件,并将其发布到我的 QQ 空间上供他人免费使用。有人加了我微信,简单咨询下,机缘很巧,他们的工作地点离我学校仅有 5 公里,于是线下交谈了下,问我有没有兴趣开发一个软件,并提供了一些想法和规划,当然,功能和需求与我所编写的大致。加之那段时间我已经厌烦学校所教的课程,与他们不谋而合,于是伴着辍学的心态办理休学手续,在 2021 这一整年“大展身手”。 + +这也就是为什么我 2021 年上半年没继续学习前端,没写博客的原因。在这期间,我基本上都是在忙着对该软件的更新维护。至于说为何要休学,明明离校那么近,可以边远程边线下办公。这主要还是与我的生活态度有关,我不希望我在做任何事情的时候,突然有其他的事情来打断我现有的阶段任务,哪怕只是一点小事,都有可能导致我难以进入工作状态。我忌惮的是在同时兼顾学业与工作,到头来很有可能两者都干得一塌糊涂(不过最主要我当时的内心是非常不情愿在上课的)。于是在学业与工作上两者无法兼顾到于是就休学专心工作。 + +说这么多,最主要是当时的我确实不是很想在校园里呆在,与其听着学校老师教的,不如自己出去社会闯荡一番来的实在。但最终的事实告诉我,还是出来太早了,社会的经验是不断磨练学习,而不是凭自己短暂实践与猜想的,只有**切身体会才能悟出真谛**。 + +又有点扯远了,总之简单交代下休学的缘由。而在今后的未来也会有一大堆这样的例子,而所发生过的甚至能写一天。**能做的仅有是保持一颗善良的心,与权衡自己内心的真实** + +#### 如何考虑诸多结果 + +有很多种结果,并不是由自己所能考虑到的,哪有该如何是好。 + +如果之前的话,我确实不知道该如何,但现在我多半会把我所遇到的情况告诉身边的人(父母、朋友),让他们帮我出谋划策。 + +他们所能给的也是建议,最终的决定权还是归咎于自己。事情的最终发展走向,也是看自己的表现。 + +### 人情世故 + +在没休学前,我的情商可以说是低的离谱,一点人情世故都不会有的那种。遇到一些情况我都很直面的揭穿,没有留有一定的台阶给他人下,没有思考这样做的情况对长期利益下的影响。这里我有一个亲身例子。 + +当时在工作中有位用户 A 反馈了一个网站的充值系统的 bug,这个 bug 可以导致随意给任意用户充值任意金额(没听错,这个 bug 的严重程度就是如此。不过当然不是我写的,这部分核心是由当时的一个外包公司搞得)。用户 A 交代了这个 bug 是由其他用户 B 告诉他的,并没有指名 B 的真实身份,原本 B 是想告诉 A 一起薅这个网站的漏洞,但 A 觉得不妥,想和我们长期合作,并将这个 bug 反馈给了我们。如果换做你的话,你又该如何处理这种事情。 + +当时的我第一想法就是找到漏洞,并将该漏洞修复了,但是是否有想过,在 B 告诉 A 没多久后,网站就把这个漏洞修复了,这不就明摆告诉 B,A 把这个 bug 反馈给了网站所有者,并且出卖了他。到时候 A 和 B 的关系又会如何发展?A 是否又会认为这个网站很没有格局?最终不打算长期合作? + +而这些显然不是我当时所能想到的,而是我的同事所告诉我的。接着再来说说最终这件事情是如何处理的。首先我们第一时间肯定不是修复这个 bug,而是尝试去查询一些异常数据,例如大额充值,余额异常,对比真实流水与网站流水,尽快的确保找到这个 B 用户。不过这个 B 用户做事非常小心,这些数据与真实的几乎难以排查。于是我们尝试给这个 bug 加个暗桩,只要有用户触发这个 bug 就会将数据上报,最终找到 B 用户,也就是在赌这个 B 用户还会利用这个漏洞来进行充值。 + +果不其然,这个 B 用户还是抱着侥幸心理触发了这个 bug,然后项目的负责人直接联系到 B,然后暗示 B 用户,说他最近账号的活跃度这么高,金额有点异常。人总是有做贼心虚的时候,和 B 用户说了一大堆道理啥的,也说明 B 的这种行为是直接利用网站漏洞来盈利,其行为是有可能构成犯罪的。可能是把 B 说怕了,也是让 B 自己说出自己利用过 Bug,也说自己把这个 Bug 告诉了几个同伙 A,C 等人。最终,修复了这个 Bug,并一一联系这些并将其获利的金额给补齐,也就没在追究。(但实际上是有一定的损失的,只要这个 bug 被发现,能做的也只有弥补损失) + +现在一回想,如果不是 A 主动告诉我们,也许这 Bug 可能在长时间都无法发现。最终这个 A 也成为了我们网站的合伙人。如果当时直接修复这个 bug,失去的金额是有可能找的回来的,但 A 这个用户却再也成为不了合作伙伴。但要说 B 会知道是 A 说的吗,他也许不知道,因为自始至终都未提及过 A,但他们之间的关系会有影响吗?与直接修复 bug 的相比,我看微不足道。 + +身边有太多为人处世的方法、道理和经验。 + +- 麻烦他人签字盖个凭证,如果态度不是很友好,并且无感激之言,会让签字的人所感厌烦,下次的签字是否会顺利? +- 遇到他人能否做到打个招呼,即便不是那么熟的情况下? +- 如果麻烦他人帮忙处理点事情,顺带带点吃的、送点礼物,那么他是否就会帮忙处理一下? +- 学会聆听长辈的建议,长辈所经历的远非后辈可言,都有长辈一定的道理,如果你只会觉得厌烦这些道理,那么很有可能一些长辈的经验只有自己吃过亏了才懂。 +- 人都爱听赞美之词,能否在别人取得一定成果的时,给予一定的夸奖,对于他的心情是否也会变好? +- …… + +有些人说他不想拘泥于生活的小节,想活得无拘无束。然而绝大多数情况下是不可能的,就像上面所举的例子而言,很多小节就有很大程度决定事情最终的发展。 + +**人活着务必要懂的人情世故,合理的为人处世往往会名利兼收,而不是狼狈不堪。** + +### 行事低调 + +人在外面,要尽可能保持低调,不要展现自己过多的储备与能力。 + +就当我而言,我复学回学校正常上课的期间,很多学校老师所教的与所问的我都悉知,但我并不会第一时间回答,甚至是不回答。因为我知道如果我一旦回答,就会显得我很专业,他人会认为我的知识面比较广,可能就会来问我问题,交流技术。这本质是好的,一是能给同学一个相对好的印象,二是对班级同学学习氛围都有一定影响。但如果我不回答,对我有害吗?并不会。但回答就有优吗?也不见得。因为随着越来越多人询问,就可能会导致本该属于我的时间,却因为这些问答所耽搁。 + +至于是否回答与导致的结果,就需要权衡自身当下的言语行为是否对你有利,就比如对于新室友而言,我反倒第一时间说明自己有一定的技术水平,也在外面工作过,并给他们展示一些个人项目。因为我知道他们身边有个大佬,一些技术问题就有个大腿可抱,对于人际关系与求人办事来说都是对我非常有利。 + +不要泄露自身的“底”,这里的“底”对于上面的例子就是技术而言,而实际生活中,“底”有可能财富,背景,智慧等等。要知道社会处处是勾心斗角,越是让他人知道你的情况,往往是对自己不利。即便此刻的他对你所述的并无兴趣,但当他需要的时候,第一时间想到的就很有可能就是你。 + +“财不外露”是古人总结的经验教训,低调就是最好的自我保护。这是社会上的生存法则。 + +**一时的得意洋洋会换来以后的肠子悔青,一时的众星捧月会招来以后的众人愤恨。** + +### 言多必失 + +我是属于那种说几句话,就容易滔滔不绝的那种。平常聊天中,也许对方只是想问下我银行卡号是多少,而我可能已经把银行卡密码告诉他(夸张点)。直白点意思就是话很多,非常容易说出一些本不应该说的东西。像以前如果有个人偷偷告诉我一个秘密,叫我千万不要说出去,而有时候不经意间就成为了告密者。然而我本意并不想泄密,但就是因为交际中,过度不必要的话语就往往。 + +当然,如果说的是自己的一些事情的话,对于他人来说,可能就听听就忘了。但有很多时候说着说着就说到他人去了,这时候一些过多的言语,就可能导致一些不恰当的词语加在他人身上。 + +我也许做不到**沉默是金**的实践,但我一定明白存在的意义。不善言,那就不必言。我之前就很喜欢给别人安利些东西,具体点如技术框架这些,但大部分情况下没成功安利,哪怕我对他当下的情况进行一定的分析,并告诉他使用了对自己的提升等等。而结果往往是将我的话语当做耳边风一般,也许是我的言语没有那么说服力,又或者是他不愿尝试新事物。所以这也就是为什么我现在不喜欢安利,不想多说些什么。即便说得再多,对我来说几乎没有任何利益可言,反倒是不说,没有任何亏损。 + +**能沉默寡言就不必口若悬河** + +### 得意忘形 + +当一个人突然对你特别好,那么这时候就需要谨慎了,很有可能自己**沉浸在好处之中**,而**忘记所在的风险**。 + +就比如陌生人给小孩子糖果的例子,小孩子往往容易沉浸在得到甜味之中,而不会去想陌生人会将自己处于何种地步,当然这也和小孩子没有明确的自我辨别善恶的能力有关。但换到成人年身上不妨是同一道理,只是将这里的糖果换成了其他的好处而已。 + +这里我想强调的是,人一旦处于高兴的状态下就容易忘记一些行为。就如酒后吐真言,在喝酒兴奋的状态,是难以有任何的危险警觉,在喝醉的情况下,可能会把自己的一切说出去,而醒后根本难以想到当时兴奋的时候竟说过这般话语。 + +而多数人往往容易处于这种状态,对我来说可能就是别人夸我几句,我感觉非常得意,然后又和对方说起无关该话题的内容。然后这就回到上一个口若悬河话题。 + +也许可能会说,为啥要如此盯防这些潜在危险,因为坏人多数都是利用这种得意忘形的状态去坑害受害者。而能做的只有时刻预防危险,因为你永远不清楚危险的来临,当危险真正来临之时,就以措手不及。 + +### 急于求成 + +就如这次休学,我就巴不得早点工作,早点实现财富自由。但**过早的发育,往往会迷失方向**。 + +我日常编写代码的时候也是如此,有时候就是为了快点实现功能需求,就会去寻找相关功能库,就容易忽视底层实现逻辑。久而久之就成为了 CV(复制粘贴)工程师,导致一些学习本该了解的知识点,就因此忽视,直到别人的库无法实现自己的功能的,到自己实现起来可谓是愁眉苦脸的。 + +所以我现在心态也相对以前平坦了许多,没有之前的那种激情劲,或者说更加稳重,走一步都要稳一步。 + +有时候有些东西就应该顺从时间的发展,强行去改变它的发展方向有可能就得不偿失。还有,为何要急于去创造成功,而不是成功去找寻你呢? + +**顺其自然,不失一个生活态度。** + +### 虚假欺诈 + +[楚门的世界](https://movie.douban.com/subject/1292064/)中有一句台词对我印象特别深刻。 + +**外面的世界跟我给你的世界一样的虚假,有一样的谎言一样的欺诈**。 + +外面的世界即现实,亦或者是社会。确实,这里存在太多虚伪的内容,多数人对他人的表现与自身内心存在极大差异。举个例子,一个非常普通的人,面对领导时展现积极主动,面对朋友时展现情同手足,面对亲人时展现情同骨肉,对不同的人,都有不同的表现形式,而真实的他却只有自己最清楚。可以说人本就是很虚伪的,只是这种形式在交际中被放大。 + +除了虚伪外,很多的还是谎言。这里举一个不那么黑暗的谎言,多数人都喜欢装逼,吹嘘自己如何如何的。将自己包装得有多么厉害,地位多么显赫。其目的也只有自己最清楚,在被这种表面所影响下,就认为他事情都相对可靠,不然的。 + +所以言语上,并不能以百分之百的确信。再好比目前互联网的新闻内容或者是短视频,我都不会去对其真实性保持绝对,至少肯定不会是完整的。因为太清楚这些内容多数是以博人眼球为目的,也许会歪曲一定的事实,营造一个绝对火爆的效果。没置身于此地,又怎敢下一断言呢。 + +**俗话说得好眼见为实,不要轻信传闻,看到的才是事实。** + +不要轻易的他言,越是真诚的人,越是容易被欺骗。 + +## 自我保护 + +这些就是我这一年的大部分社会感悟,然而事实上这仅仅只是皮毛中的皮毛,有很多不是那么友好的面我并没有用言语去展现,也难以展示。 + +但最终想说的是出门在外,保护好自己才是最重要的。无论是身体还是精力,起码活着是为自己而活。 + +同时时刻**当心**生活中的风险,无论大火小火最终都有可能酿成不可挽回的伤害。能做的也只有提防,警惕。 + +与社会相比,校园生活也确实安全多了。与你相龄的同学,很多都没有经过社会,做事处事都不会搞得如此复杂,但或多或少肯定还是存在的,只是相比社会而言没那么复杂。倒是可以说大学校园就是步入社会的一个缓冲区,缓冲区有多大,就决定在社会中你的发展效率。 + +## 回到校园 + +办理完复学手续,接触了新室友,新班级,依旧还是熟悉的校园,但此时此刻却感受不到校园的气息。内心也许还停留在休学一年的风光,可身体可却要老老实实在待上两年。 + +每日起床,打开屏幕,查看邮件信息,吃饭,能旷的课就旷,不能旷的课就尽量去,坐下敲代码,一天的日子就这样过去了。这半年里至少有百分之 80 的日子是处于这种状态。 + +要说枯燥,也有一些乐趣,要说充实,也会摸点鱼。不过,这不就是大部分人的生活方式吗?以混日子的方式做着重复的事,在重复中寻求一丝不同。 + +在学校也没啥特殊的要求,别挂科,在读两年拿到毕业证与学位证即可。还有两年的时间发展,也许这两年是程序生涯中仅存可自由分配的时间。 + +## 总结 + +要说我这一年的总结,**一切行为都一定要留后路**。 + +人生要做太多的后退路,才能够走出自己的路。过去的自己的很多决定都是不留活路,可以说是大部分的情况连后悔的机会都没有。当时的我为何要做得如此绝呢?仅仅只是鲁莽吗。不,所突显的是一个真诚的态度。如果做得任何事情都留有后路,只能说你是一个警惕的人,但不是一个真诚的人。但这个社会往往就是不需要真诚老实的人,在结果面前,这些都一文不值。越是真诚,越容易被欺骗。到头来,伤的还是真诚的人。 + +我高中因为一些特殊原因换过班,换完之后有一段时间非常后悔,失去(永别)日常课上课下与要好的同学。可结果已经发生了,很多东西就难以还原成最初的模样。上了大学第一学期结束,我就从数学专业转到软件工程专业,只为毕业时有个科班的资格,而现在科班在我看来一文不值,还要面临补修原专业没有的课程。但相关手续都已经办理完毕,即便撕毁手续,也无济于事。每一次的选择,都总让我觉得不值,失去的远比得到的多。 + +不过话也不能说的这么绝对,我高中换班让我远离那些特意”针对”我的老师(我高中学习表现特别差,属于那种差生不爱学习的,虽然现在也差不多)。我转专业也认识到一些志同道合都热爱编程的伙伴,我要是不转专业也许在这个学校里可能都难以有交集。同样的休学也让我收获了不少,社会经验与做人处事有了个质的提升。 + +所以高中的我特别渴求大学,在大学里不断的提升自我,也就是为什么高考一结束,我就沉浸在电脑面前,不是游戏电影,而是可创造一切的代码上。在上一学期后,想去软件工程软件。并且当我有了一定的代码基础水平,我觉得有能力去应付实际任务需求,也就办理休学手续,去完成一番“事业”。现在回到校园,同样的,我也非常渴求毕业,只为能够更早做出一番事情来。 + +如果有个选择让你在 40 岁前平平无奇,但在 40 岁后一鸣惊人成就一番事业,我想我肯定不会选择。我已经经历了比同龄人多的多的经历,我完全认为 30 岁就能达到 40 岁时的目标。有一个梦想叫别人 30 岁有的东西我 20 岁就要,这也是我的梦想。 + +为什么会休学,想必已经有了一个很明确的答案。而且我也很清楚,谁都劝不动当年的我。即便休学让我晚毕业的一年,同时还带来了各种琐事。在明知道结果的前提我依然还是会决定。 + +做技术写程序的不应该只有技术上的进步,更应该在友好的交互方面,这里的交互不单单是人与机的交互,而是生活中人与人,人与事之间的交互。 + +一个应用程序的实用性如何,很大一部分取决于交互。生活中圈子同样与他人的交互有很大牵连。代码写的再好,功能再强大,但是没有一个好的交互,用户同样不会使用。生活中,即使能力再强大,但对他人不友好、对工作不上进,也难被认可。 + +愿来年提升自我的同时,能够辨别是非,待人处世。修正自身缺点,发挥自有特长。不要高傲自大,不要妄自菲薄。不要只沉浸做事,而不去做人。保持原有的心态,不被大起大落所失衡。不断经历,不断收获。 + +愿一切安好,愿前程似锦。 diff --git "a/blog/reference/2022 \302\267 \351\200\206\345\220\221\345\210\260Web\345\274\200\345\217\221.md" "b/blog/reference/2022 \302\267 \351\200\206\345\220\221\345\210\260Web\345\274\200\345\217\221.md" new file mode 100644 index 0000000..1545b8a --- /dev/null +++ "b/blog/reference/2022 \302\267 \351\200\206\345\220\221\345\210\260Web\345\274\200\345\217\221.md" @@ -0,0 +1,247 @@ +--- +slug: 2022-year-end-summary +title: 2022 · 逆向到Web开发 +date: 2022-12-22 +authors: kuizuo +tags: [年终总结] +keywords: [年终总结] +toc_max_heading_level: 3 +--- + +距离上一篇博文有一个月之久,距离上次编写代码也有一周之久,由于疫情封控全面放开,加上福建省教育厅通知的提前返乡,反而让我感到有些不适。往常这个时间点我忙于期末考试,会把代码的事情放一边,等到彻底放假后,开始闭关潜心学习。然而剩余的半个月变成了线上形式,课还是要上,考试还是要考,虽说身在家乡,但心不在焉的。 + +12 月已过半,也是时候该写年终总结了。迄今为止,我已学习了 3 年半的编程(还好不是两年半)。当下的技术不再是当初只会易语言的小伙了。只是当下已没有当初如此强烈的热情与精力了,我称之为老了。 + +今年主要总结我为何从逆向转 Web 开发,并明确我未来所要走的方向,也是本文的主题。**仅作为个人感悟,不作为建议参考。** + + + +## 为什么要写年终总结? + +我从高中开始,每年都会记录这一年所发生的比较有印象的事情并写下自己的感悟。所以写年终算是个人习惯,并且我希望能够一直坚持下去,写到不能写为止。 + +写年终总结是个非常好的自我总结与反思的方式,总结这一年自身的变化,有哪些精彩与满足,有哪些遗憾和不足。同时定制明年的规划,以该目标不断前行,而不是漫无目的地活着,就失去了很多人生的意义。 + +同时也算是自我评价与建议,很多时候我们会收到很多别人有关自己的评价与建议,可人总会对他人有莫名的排斥感,就难以虚心听从他人的建议,从而犯错许多(说的是我就对了)。年终总结还有个作用就是弥补自己对某件事情未来可能要发生的情况,要如何做到不犯错,预先有个明确的预防意识。 + +即便可能要花数天的周期去回顾与总结,我也很愿意去总结,我常说回顾过去,就是在仰望未来。写年终所做的也就是这个过程。 + +写年终总结也算是种分享,分享自己的开发经历,当他人阅读时或许有所启发。 + +**过去的,就过去了,别将当下的遗憾留到未来,这就是年终的最大意义。** + +## 为什么是 Web 开发?而不是逆向 + +今年大部分的开发时间都花在 Web 开发上,在此期间也接触到许多技术,并通过博客笔记的方式记录下来。 + +我很庆幸我的 Web 开发从一个 Beginner 到 Intermediate,现在回忆整个学习路程,一路学得都很野很随意,从未系统学习过,总是学到一半,就自认为已经掌握了,便开始进行实战项目,可以说很多知识都是在实战探索中了解的。 + +虽然很多人都直称我为大佬,但我离 Advanced 还有一大段的路途要走,而这段路途是无比的艰难与漫长。不过好在路不歪,只要肯走终会到达终点。 + +而带我入门的逆向技术,在今年没有丝毫的长进,说得过分点,就是弃坑了。也正如标题所说,至于缘由,细看下文。 + +### 逆向 + +熟悉我的人应该知道我之前是做爬虫与逆向分析,但是为何今年的技术栈彻底转型到 Web 开发上。 + +我常常和别人说起我的技术栈转型(从逆向转到开发),不过比较多的都会比较好奇我为什么不继续深造下去。 + +在此我也回顾了我**从逆向转变到 Web 开发**的过程,顺带也回答这个问题。不过在这里先说说我个人对逆向的看法: + +#### 逆向需要的技术知识面比较广。 + +就我接触逆向的过程来说,接触了易语言,JavaScript,Python,Java,Php 等等编程语言。 + +使用过了一堆的逆向工具,如 Frida、IDA、JEB、jadx 等等(我目前能想到比较有名的)。此外还有一堆知识,包括但不限于以下技术:自动化脚本、TCP/HTTP 协议、抓包、爬虫、加密学、图像识别(验证码、滑块位置)、汇编、反编译、AST 反混淆。 + +**要我说我在逆向中学到的不是如何使用这些工具或掌握某个技能,而是锻炼出一定的阅读与分析代码能力。**工具与技能总是瞬息万变,但阅读与分析代码能力却是实实在在,一成不变的,也是逆向中最值得学习的。 + +有接触过 [CTF](https://baike.baidu.com/item/ctf/9548546) 想必再熟悉不过逆向工程的技术面广了。 + +#### 越来越多的网站或应用程序不断加强安全防护,未来只会越来越难逆向。 + +逆向分析,说白话就是去看别人的代码,进行一些修改手段,达到自己想要的目的。比如修改某个软件的标题或作者信息、将别人的代码“偷”过来用、爬取某些网站或软件的数据。 + +但是随着现在越来越多潜在的安全问题,很多框架底层,服务厂商,都会对一些可能有安全问题的代码进行警告或者底层处理。比如使用 ORM 框架能够有效防止 SQL 注入,前端框架中涉及 XSS 攻击也会有相应的错误提示与处理,再如浏览器跨域以及跨站点 cookie 不共享,都是为了用户的安全而去考虑的。这样的例子有太多了。 + +简单说说安卓逆向的过程:拿到一个 Apk,发现有加壳(给代码加固让逆向者不易于看到源代码),这时候就需要通过脱壳才能查看到源代码;此时就算脱完了壳,接下来可能将面对经过混淆过的代码,这份代码难以用人眼去阅读,不调试运行,单靠静态分析很难分析出东西,这时候可能就要借助 AST 对代码进行还原;还原完了配合动静分析将代码给扣出来,而在分析的时候可能又有各种检测,比如抓包,反调试以阻碍逆向进度;最后就算逆向工作都做完了,代码也扣下来了,还要尝试运行扣完的代码,这时候极大可能还不一定能运行,然后又要回头看看到底那一步做错了。 + +可以说逆向的工作就是不断地调试,不断分析,最终拿到想要的结果。整个过程可以说非常耗时且折磨人,心智如果不够强大,真的容易劝退(我当下耐心也早不如当初了)。但是得到最终的目的,将非常爽,成就感爆棚,相当于一个解了几小时的题,最终被攻克的感觉。 + +前阵子在逆向圈中,看到过一张图,大致也把我的逆向学习流程也表达了出来,最终我的逆向学习也确实止步在 Flutter 上。 + +![](https://img.kuizuo.cn/1cd67d5812e3061e_zy9LIkXWIM.jpg) + +上面所说到的逆向技术中,例如加壳,代码混淆,反调试,风控等等,都是阻碍逆向手段。同样对于爬虫而已,通常会采取反爬措施,包括但不限于封 IP、封账号、JS 参数加密、代码混淆、浏览器指纹、TLS 指纹、验证等。而且防护手段可以说是越来越多,逆向的难度也就越来越大。并且在服务开发那边只需要修改一点东西,逆向可能就需要从头再来一遍。 + +**未来逆向的难度只增不减,但薪资可不一定保证这趋势。** + +#### 风险 + +从上面也不难看出逆向常常与安全挂钩,如果一个网站或软件不是那么好被逆向,那么间接说明安全性是比较高的。可一旦涉及到安全,同时又是互联网,就免不了网络安全与法律相关的风险。 + +在大多数逆向的对象(网站,软件)中,多数都是他人的劳动产物,当你未经他人允许的情况下,去爬取他人的数据,或是修改一些版权信息再次发布出来,就属于非法行为。本质和未经他人允许,偷人家的东西性质是一个样的。并且在有防护措施的情况下,绕过网站防护措施获取数据属于违背权利人意愿读取、收集数据,将有较大可能被认定为对计算机信息系统的 “侵入”。 + +要知道在逆向的行业中,有很多都是擦边灰产,至少我所在的逆向圈是这样的,抱着【仅供学习为参考,请勿用于非法用途】的想法做逆向工程。路子极易走歪,很少能够正常去走安全岗位的。相信你应该能看到许多类似的案件,如【某某程序员因非法侵入 xx 网站,获刑 x 年】。这不是危言耸听,目前国家对网络安全以及非法数据获取的打击力度,也将决定了这些案件将会越来越多,爬虫逆向也将会越来越邢。 + +### 自身因素 + +简单介绍完我认为爬虫和逆向的看法后,再来说说自身因素。 + +#### 更想写代码,而不是看代码 + +我是很享受写代码的过程,一份高质量的代码会让人赏心悦目。我为此特意学习下设计模式、重构技巧以及 TDD 测试。这些在逆向中基本难以涉及,但是这些对开发的体验和代码的健壮性都是非常重要的,也是让我个人觉得技术有所提升的技能点。 + +在逆向分析中,大部分时间都不是在写代码而是在看(分析)代码,而反编译出来的代码,很有可能是带有混淆过的代码,而你想要分析这一块代码的作用,只能去一步步调试,将代码啃下来,从而推断运行时某变量的值,或是某个函数的作用。 + +虽然说开发岗位中,有很多情况下也是在看代码的日子中度过的,但相比反编译阅读代码而言,至少不至于那么晦涩难懂。 + +#### 更想碰新技术,而不是旧技术 + +其次,在逆向分析中,遇到的网站或软件所用的技术一般都是较为稳定成熟的技术,而这些技术往往不是很新,因此需要去了解很多旧的技术,但这些旧技术仅仅只对逆向分析有用,甚至过段时间很有可能就会遗忘。并且要了解的旧技术还不少,学得将会特别杂。 + +而我又是一个喜新厌旧的人,对任何新鲜事物都抱有好奇尝试的态度。我终将认为旧的技术被淘汰是迟早的问题,新技术的出现肯定不是无缘无故的出现,必然是为了解决某些问题而诞生的,如性能,开发体验,安全等等。 + +这里有篇我对[新技术的看法](https://kuizuo.cn/talk-new-technologies-opinion),推荐阅读一番。 + +#### 更想开源,而不是闭源 + +在逆向开发,不,是在灰产开发中,有很多代码是别指望开源出来的,这背后会涉及到商业利益或是版权等问题。一旦代码放出来,将意味着你的代码将有很大的可能被别人利用做坏事,最终甚至祸及到自己。所以大多数情况下,你多半只能将自己的应用发布出去,而不是将源码开源,通常也就带有商业化的性质。 + +我想任何开发者肯定是希望自己的代码走的更远,走的正规,而不是被拿来做违背自身意愿的事情。 + +开源不仅能为自己提升一些技术知名度,展现自身技术的一面,同时代码被他人使用与认可,这番成就感就足够继续坚持开源下去。因为我有很多技术都是通过开源项目中学到的,所以我能感受到开源带来的魅力,也是对于前人的崇拜,想走开源的原因。 + +**因为淋过雨,所以很想为别人撑把伞** + +### 小结 + +其实在今年出头我还特意复习了一波安卓逆向,为了更深入了解了更底层的知识与工具(当然现在忘得也差不多了),因一些特殊原因我的电子设备不在了,别问,问就是坏了到现在还没修好。就导致我的编程语言环境,逆向工具,虚拟机配置,代码等数据直接灰飞烟灭,也让我停滞了 1 个月的学习。 + +我想这才是我从逆向转 Web 开发的最重要的理由了。 + +逆向的学习对我开发有很大的帮助。例如开发中的安全问题,我在开发中都会时刻考虑去考虑该问题。如加壳、SQL 注入、代码混淆、接口限流、接口幂等性(Fiddle R 包)等等安全性问题。 + +我庆幸我学过逆向,让我学到很多在开发中比较难学到的技能,如调试,阅读源码,而这些也是绝大多数 Web 开发者都欠缺的能力。 + +逆向应该就暂以告终,未来会有很长的一段时间,甚至以后不再接触深入专研逆向。 + +每当回想,【当时那么难的加密算法都能搞得定,开发一个功能还能有多难】,保持着这种心态,也让我保持着开发。 + +也是逆向激起我对编程的兴趣,可以说没有接触逆向开发,我也不太可能会接触到 Web 开发。 + +## 但 Web 开发就一定好吗? + +上面对逆向的看法仅个人分析而言,但是 Web 开发就一定好吗?我不敢下绝对的肯定,不同人不一定适合相同的技术,**但是我特别看好 Web 技术(尤其是 JavaScript,偏前端向)**,以下是我看好的点。 + +### 应用性广 + +目前仍有很多人还停留在 JavaScript 只能编写前端页面的水平,自从 Node.js 问世,目前绝大多数的应用都可以使用 JavaScript 进行编写。 + +换句话说,只要你会 JavaScript 就可以编写很多应用了。有个在国外经常被应用的一条和 JavaScript 有关系的著名定律: + +**Atwood 定律:任何可以用 JavaScript 编写的应用程序,最终都会用 JavaScript 编写** + +Web 前端开发不用多说,目前还得用 JavaScript 来编写。而后端开发通过 Node.js 也有一己之力。近几年特别流行跨平台开发,也就是一份代码,多端运行。 + +使用 React Native 或是 Uniapp 这样的前端应用框架,顺带去了解一下相应平台的 API,就能够编写出安卓或 IOS 应用,做过小程序开发肯定知道小程序的技术栈就使用到前端开发的技术栈,如果你会前端开发,就能很轻松的上手小程序开发了。 + +并且越来越多的软件都是网页版优先,然后再通过 Electron 这样的跨平台解决方案,实现不同平台间的程序。诸如 Vscode,Typeorm 等等便是其中的佼佼者。 + +不过本质上都是在不同应用的平台上套浏览器的壳,然后接入一些该平台的接口,因此普遍应用的体积与内存都相对比较大,与原生相比自然是略逊与原生,不过这点性能上的差异在如今硬件升级如此普遍的物质生活也显得微不足道了。 + +但借助浏览器自带的跨平台性,你所编写的应用能够非常轻松的让他人访问,只需要用户有个浏览器,并且通过上述跨平台的解决方案,也能够有效的将你的 web 应用转成其他平台的应用,而不是在耗费时间与精力去编写另一套代码。 + +在一些技术文档上,会有 API/SDK 的支持,而绝大多数都支持使用 JavaScript/Node.js,也许该功能本身并不是使用 JavaScript 来编写的,但完全可以通过 JavaScript 来轻松调用该功能。并且如今 JavaScript 生态如此庞大,你不必担心库/包的问题,npm 作为世界上最大的开放源代码的生态系统(包管理器),在这里你几乎找得到你所想要的库/包。 + +此外有越来越多的第三方服务平台(通常称 Fass,函数即服务),专门提供数据接口与用户鉴权,如 firebase/supabase 或是国产的云开发产品(如小程序)。前端程序员只需要专注与业务代码,借助第三方开发平台,就能够编写一个高可用的应用。反而也映衬了,Web 开发好像可以没有后端,但必须要有前端的观念。 + +### 工作量将越来越多 + +这句话的意思可能有点加班那味道,但我所要表明的是可以做的事情多了,意味着你的工作量增加了,同时你的编码价值也就提升了,不过实际价值(工资)的话不一定提升,因为这主要看老板和地区。 + +怎么理解呢,我举几个例子。 + +现在大部分的 web 应用都采用前后端分离的形式,但在曾经则是由后端通过模板语言渲染成 HTML 直接返回,通过前后端分离的形式,前端只需要专注页面交互的编写,而后端只需要保证接口可靠性。并且前后端分离也有一个特别显著的特点,能非常有效的节省服务器的资源,原本服务器的渲染动作迁移至客户端来做。也许有人会说,那这样 SEO 优化该怎么办,这不就有了 Next.js、Nuxt.js 这样的 SSR(服务端渲染) 框架,而这些工作,也是前端要做的。 + +并且越到后面你越能感觉到客户端(前端)远比服务端(后端)做的东西来的多,比如开发阶段的数据 Mock,前端国际化,设计原型等等。之所以会有这种感觉,也许是因为我的大部分开发都是前端,这里你也可以回想当下的工作量与往年相比是多了还是不变。 + +### 技术更新快 + +前端真的是每隔一段时间,必定会出一些新的技术,也不由让不少前端学习者感叹真的学不动。但伴随着新的技术出现,必然会有新的岗位与机会,这也是我看好前端的一个点。 + +**不过这种机会在国内不太多见,反而在国外特别普遍。** 多的不说,自行体会。 + +忘记从哪看到的一句话:“前端工程师的一大焦虑:永远能(且必须要)看到大量前沿的技术,但自己手上的活儿跟不上,导致眼高手低。” + +不只是前端,做技术这行的,必须要跟着技术发展的角度,并且要时刻关注最新,主流的技术。 + +--- + +有关 Web 开发的一些想法与感悟就写到这,其实还有蛮多可写的,比如选择 Vue 与 React?为什么是 JavaScript?有太多自我审问的话题了,不过由于当下时间相对紧迫,有机会的话再续写吧。 + +综上,也就是我看好 Web 技术的几个点,也回答了我未来的方向。希望这些能够帮助一些不知抉择方向的伙伴。 + +## 在实践中学习 + +回顾整个技术栈,基本都是在项目实践中不断学习。也许是因为逆向的缘故,因为逆向基本上都是靠实战出来的,导致我的学习路线也趋于实践。 + +不过在我看来,写项目是最直接能体现出所想学的技术。我在学习一门新技术时,我通常会用项目或者博文的形式来总结我的学习过程。我也乐意花时间在这上面,并将其分享出来。当有其他人也在学习这门技术时,看到该项目或博文,我就认为非常有意义。 + +**我在学任何一门技术,会使用该技术写点东西;换言之是为了写点东西,而去学点不一样技术。** + +## 技术心态的变化 + +如今来看,确实没那么想写代码,尤其是那种工作量大,重复性强的代码,而编写这些代码,其实与搬砖无任何区别,本质也是重复性与劳动性的任务。 + +曾经可能是因为接触得比较少,对于很多软件背后的原理及实现一概不知,所以看到啥就都想写写看,最终很多项目都成为半成品。 + +而随着越深入的学习,反而自己所想实现的东西,前人都已经帮我们实现好了。完全可以借用前人的代码,在此基础上学习与使用,而不必从头再来,耗费很多不必要的时间。 + +到最后为了实现一开始所想实现的东西,不用再费尽心思,去开源社区搜寻一番,总能找到与自己所要实现的类似的产品,此时只需要会看,会改,总能达到自己最终要实现的东西,有时候自己也称为了别人口中的 CV 工程师。 + +也正因如此,写代码的欲望就不再那么强烈。很多自认为无意义的代码或者是以后都不一定用得上的代码就少写了许多。于是乎缺少了很多自我思考与专研的时间,虽说也确实节省了很多不必要的时间,加快最终功能的实现,但这就是自己想要的编程生活吗?或者说这就是绝大多数人的编程生活。 + +## 何来的自驱力 + +现在回想,是什么驱使我学习,我心中的答案是无能。 + +当你什么都不会时,或者是目前的能力还不足够将某件事情做好,你就会不断地焦虑,犹如热锅上的蚂蚁,想做但又怕做不好。 + +我有很多社会技能是欠缺的,比如人际交往,谈话技巧,合作行为等等,并且我本身也不愿去学习这些技能。正因如此,只有不断发挥自身长处,用自己所擅长的领域弥补自己的短板,以提升自我在社会的竞争力,以至于不那么容易被淘汰。 + +要让自己保持每天都处于学习状态真的太难了 😩,生活总有源源不断的琐事打扰着你。在忙碌的时候总是感觉时间不够用,哪怕有时一天花费了数十个小时,也总感觉做的事情太少了;哪怕每天计划都规划得好好的,但总是有一半还未完成。生活中大量的碎片化时间,而编程学习最不需要的就是这些碎片化时间。 + +回顾整个编程生涯有太多可感慨的,整个旅途几乎是一个人走完的,期间遇到的坎坷就只得依靠搜索引擎来解决,搜索引擎是我再生父母都不为过。没有人给我指引明确的道路,只依靠心中对技术的憧憬不断前行。走过低谷,登过山峰,而如今站稳身子就足矣。 + +## 返校的一年 + +今年也是我重归学校的一年,倘若,去年没有休学一年的话,如今我可能已经在外实习了,转瞬一年就过去了,如今的我还沉浸在当初休学和别人创业工作的日子。 + +也是因为当时休学急迫,后事做得并未完善,学校的一些课程并未申请缓考,最终视为缺考,即挂科。然后此前对大学的课程也是抱着可有可无的心态,也挂了几门课程,如果不是辅导员告知,我还有 34 学分要补(20 学分为严重预警,40 学分为退学或降级处理),我都不曾了解自己在学校挂了这么多科 😂。最离谱的是有个同班同学正好就被降级处理了,不过好在今年上半年并未挂科,加上下半年也重修了一些课程,还不至于离本科毕业证书越走越远。 + +回到学校后,就感觉如同坐牢一般,只不过环境相对舒适一番,尤其是在疫情当下更是如此,我这一年出校玩乐的次数好像不超过 10 次。当我仔细回想一下大学的真正意义是什么?貌似就是混个日子,混个证书。我也想不出大学能够有什么实质性的作用,也许是因为我所处的学校不行,换个好一点的大学或许都不是这样了。 + +不过今年回学校反而去参加了曾经都不怎么看上的社团(虽然现在也差不了多少),给社团写了些项目,主要也为了给自己重返学校带点个人知名度。但因为学校不是那么有名(臭名可能有),所以这里就不便放上相关信息。 + +### 考研 + +明年的话我就大三下了,面对我的有条熟知的考研路。说实话,从目前来看,我对考研不抱有太大的希望,我是属于实践派的那种,我坚信没有什么是试不出来。背理论,刷八股,我很反感。 + +不知道由于什么原因,总感觉一段时间不写代码,真的就不会写代码。仿佛与自己写的代码成为了陌生人一般(所以写注释的重要性就体现出来了) + +倘若读研的话,必然将会有数个月的时间重心不在代码上,到时候回来编写代码时,又发现自己好像又重新学了一遍似的。加之万一上岸失败的心理落差,会让自我觉得这几个月的努力都白费。而我又恰好是一位结果论者,即只在意结果,不在意过程如何。在别人的眼中,是难以看到你考研备战努力的过程,只会在意你上没上岸。这便是我不想考研因素之一。 + +但今年在互联网上所认识伙伴中挺多给我过于高的评价,都认为我作为一个学生,能有这水平是不是在某某牛逼的高校读书。然而并不是,我甚至都不好意思述说自己的高校,说是我的污点也不为过。也正因此身份上,这不得让我萌生读研或是留学的一丝丝想法,也不至于死绝。 + +自我认为离真正的大佬还差很长的一段距离,我只是靠着三分钟热度才维系下去,而那些真正的大佬是肯花费大量时间去编写他们所认为有意义的代码。 + +## 结语 + +今年的年终就告一段落,与之前的年终总结相比,篇幅缩减了许多,主要感觉之前写的挺多流水账的,废话挺多的。也有一点是因为时间相对紧迫,其实 12 月我还没放假,都处于线上上课,线上考试的状态,而线上考试的方式我就不得吐槽了,提前打印给定的答题纸,题目以电脑的方式展示,最终将答案手写到答题纸上并通过手机扫描(扫描全能王)成 PDF 的格式提交,当然摄像头必然也是有的。总之麻烦事还是有的,还没到彻底闲下来闭关学习的时候。 + +按往常可能还有明年的规划,但我现在认为很多时候都难以依照自身意愿做事,而规划正好自己所设想美好的计划,很容易事与愿违。因为回顾过往的规划,有太多美好的计划,但在实际分析下也将变得不堪一击。不止项目需要可行性分析,人生计划同样需要,一些天马行空幻想,其实就不再有意义去记录了。 + +**坚持做自己想做的事情,而不是逼自己做不愿做的事情**。这是我编程学习中座右铭,也是给予他人编程学习的建议。 + +

写于2022年12月22日 By 愧怍

diff --git "a/blog/reference/2023 \302\267 \350\260\210\350\260\210\350\201\214\344\270\232\350\247\204\345\210\222.md" "b/blog/reference/2023 \302\267 \350\260\210\350\260\210\350\201\214\344\270\232\350\247\204\345\210\222.md" new file mode 100644 index 0000000..ea17fea --- /dev/null +++ "b/blog/reference/2023 \302\267 \350\260\210\350\260\210\350\201\214\344\270\232\350\247\204\345\210\222.md" @@ -0,0 +1,180 @@ +--- +slug: 2023-year-end-summary +title: 2023 · 谈谈职业规划 +date: 2023-12-25 +authors: kuizuo +tags: [年终总结] +keywords: [年终总结] +--- + +又到了年底写年终总结的时候了,说实话今年感觉没什么内容可写。上半年我已经写了 [叙一名转专业+休学的大学生经历](/blog/narrate-a-college-student),而下半年我忙于学校课程 + 课程重修,加上处于“监视”下,过得其实还有点浑浑噩噩。 + +不过如今都大四了,也确实是要考虑实习的事了。我想结合我自身情况,谈谈我是怎么看待工作或者往远点说职业规划方面的想法。 + + + +## 一些经历 + +### 工作经历 + +我目前一共有 3 段工作经历 + +1. 休学一年在厦门本地某工作室(共4人)与他人创业。 2021.1 ~ 2022.1 +2. 在北京的一家公司(规模不大)远程实习。 2022.8 ~ 2022.10 +3. 在 [3R 教室](https://3rcd.com) 兼任助教与开发组成员。 2023.1 ~ 2023.12 + +事实上,对我而言也仅有休学的那一段才能算是工作,而其他两者从年限与工作性质来看不能算是一份真正的工作。但也不能说不是,至少也替别人做过事、签过合同、领过工资的。 + +不过我想稍微提提我是怎么找到这三个工作的,或者说这三份工作是怎么找到我的 -> **全靠分享** + +我不止说过一次分享的重要性,我的第一份工作就是在网络分享了[一个大学生自动完成视频、作业的程序](/blog/chaoxing-helper),恰好被我当地一个工作室的同事看到,寻求我能否改进功能一同合作,于是一拍即可,便开始[休学](/blog/narrate-a-college-student#休学)。 + +剩余的两个工作同样也是,一个契机是我当时正好在研究 [Strapi](https://strapi.io) 编写了一篇文章,这家公司恰好用到这门技术;而 3R教室则是因为创始人所用的与我博客相同的网站生成器 [Docusaurus](https://docusaurus.io/zh-CN) 在这个机缘下认识的,后来创始人创业开了3R教室,我也就利用业余时间在其中扮演助教身份,赚个零花钱。(其实我很热心肠的,哪怕我不当助教,只要时间允许的情况下,你有问题问我也会尽数回答) + +### 接单经历 + +同样的,分享也能很大程度上提升你接单的资本。 + +我并非想要炫耀什么,而是分享实实在在给我带来了很多好处,我希望你能够分享一些内容,一些见解心得,哪怕是一些笔记、对他人问题的回复,都可能给你带来一些意想不到的好处。 + +我就有一个同学将自己的笔记、课设以及每次专业课期末考试的文档放到 CSDN 上(没错就是那个 IT 界的毒瘤),现在这不到期末了嘛,他的私信就有一堆人找他写课设啥的。 + +回归到正题,我接过单不多也不少,绝大多数是那种对我而言挺简单的问题,但对于一些人而言就比较困难。例如前几天一个例子,我编写过 [js-deobfuscator](https://github.com/kuizuo/js-deobfuscator) 一个 js 混淆还原的工具,正好有人需要将一份混淆代码还原出来,于是寻求我的帮助,发了个红包给我。有时哪怕只是回答别人一个问题,甚至都可能会收到来自他人的红包。想想看,你在技术群里是不是有过这个现象。 + +还有一个网站的单子我印象很深,是一个用于销售流量、虚拟会员等商品的网站,不同与普通的商城系统,充值是通过卡密,使用卡密到特定的网站上使用。当时一个网站全套 5000,客户也算熟人了,还帮我推荐几个人购买,相当于这一套网站(模版),帮我赚了几 w,还省去我很多开发成本,后续我都无偿给他提供技术服务。 + +事实上至少现在为止我并不是很喜欢接单,也很少主动接单。小的单(也可叫零活)通常以一顿夜宵作为回报或是看在人情的份上;而大的单(通常为外包单),通常要求交付时间快,很多时候只为了更快的完成功能,而不考虑代码质量。并且通常没有维护性可言,就更别说写测试和重构了。写久了,代码虽然写的是快了,但堆屎山的速度都堪比屎壳郎了。代码能力的提升反而不是很大。 + +在我看来当因某种目的收了他的钱财时,往往就要花费时间精力去完成这个目的,而从我内心上很难平衡这点。所以只要我不收钱,我就可以不做事了(bushi + +### 赚钱经历 + +我赚的第一桶金其实还不是我工作,在上大学前我已经赚过对我两桶金了,还全都是依靠网络上的资源,而非现实打工。一次是初三当时向别人学习如何刷钻以及购买钻卡来接单帮别人刷钻,另一个是作为线报群群主(现如今更多的称之为羊毛群,但我更愿称撸界),发布一些活动和教程通过拉人头引流赚钱。而也是再次契机之下,学会了开发(定制)软件。 + +这并非本文重点,因此我并不想过多介绍,但或许这会作为后面的铺垫。 + +不过我还是想说:**赚钱不易**。我现在回看我过去的一些经历,只能说运气成分很大,而又恰好在风口之中。在如今的互联网环境下,想要复刻曾经的路, 无疑是死路一条。 + +## 为何不实习? + +背景介绍完了,那就来说说实习的事情,为何我都大四了,还不找实习? + +不是不找,而是很难找到满意的。我在今年暑期的时候有尝试找过,投过几家大厂,无一例外,了无音讯。这对我当时而言,打击还蛮大的。因为三流学校出身,确实很难过(双关),加之没人内推,海投基本上是没希望的。 + +如今前端的职位卷之又卷,僧多粥少,在厦门一些中小公司所提供的前端实习待遇(薪资大约在 3k~4k),况且对自身的提升并不是特别大,在我不是一笔性价比划得来的买卖。 + +反观学校的情况那可就更头疼了,你敢相信在大四上最该实习的时候,极为不合理的教案却还要给学生安排课程,还是专业课的那种,以下我的课表。 + +![](https://img.kuizuo.cn/2023/1231064437.png) + +此外,我还需要重修当初因休学而没去考试的几门课程(还挺多的,重修费还交了我不少😢),导致我最后一场考试时间是在 12月中旬。还是强调那一点,我不希望工作与学业这两者同时进行。关于这学校的诸多不满,待我毕业后我一定要说说,感受什么叫中国的私立大学。 + +此外还有一点,也是我不想提及的一点,这段期间我是处于“监视”状态,内心总悬着一个不知何时爆炸的炸弹,生怕突然爆炸将会打乱我的行程。 + +![image-20231226224325326](https://img.kuizuo.cn/202312271649106.png) + +好在如今期限已到,心里悬着的一块石头终于放下来了,如释重负。 + +于是在上述的因素下,今年下半年我就不想找实习了。 + +## 为何又想实习了? + +最主要的一点就是以我目前学生(应届生)的身份,是可以有机会争取到一份好的实习的,乃至是大厂的实习。 + +其次是我并没有一个很好的项目演进经历,看完我的一些工作经历不难发现我所待的公司/工作室的体量都不大,甚至我到现在都没真正体验过打卡上班(当然我也希望不要有)。而这就是小厂或者初创公司的同病,各个流程所要负责的任务很模糊。我当时休学和他人创业工作,基本上都是我一人负责项目开发(当时说我是技术 Leader 都不为过);第二个远程实习也是我当时主动退出的,因为工作形式与接单无疑;第三个就不用多说了。 + +尤其是在技术层面的团队协作之中,还缺乏相当多的实践经验。所以在自我分析下,为了学习某些只在公司才能学到的东西,就非常有必要到大公司去一趟。 + +我大概率以后是不太可能再考虑坐班,所以这或许是我仅有为数不多的上班经历了。不过我想既便真正要开始打卡上班,要开始适应国内 996,未来我铁定会后悔有过这段历程。 + +![1703021156782.png](https://img.kuizuo.cn/202312250547469.png) + +### 那么该怎么找呢? + +那么在如今就业形势如此严峻的时代,我又该如何找工作? + +关于这点我其实并没有什么很好的经验分享,这也算是我首次主动找工作。不过我可以肯定的一点是,只单靠海投与某些招聘软件,想要拿到一份心意的 offer 很难。若是能通过一些关系,牵一条线(走内推),才是最佳选择,所以多积攒人脉交际圈是很有必要的。 + +这一部分我想待我后续找到工作后,再来做个心得分享也不为迟。(不说了,我去准备项目与简历去了) + +## 远程工作 + +[电鸭](https://eleduck.com/)的sologen:**只工作,不上班**。很好表明远程工作的意图。 + +远程工作(remote work)是我当下认为最具性价比的工作形式,你可以过着四五线城市的生活水平而不用考虑一线城市的房租与消费,却赚取一线城市的薪资。往大点说就是挣美元花人民币。 + +但是远程工作并非多数人想的那么轻松的,很多人对远程工作有个误区,就是可以自由决定上班时间,很自由。其实不是的,不用上下班通勤,那就把通勤的时间拿来工作。没有额外房补开销,那就拿来压低工资。而该开的会还是得开,说白了就是换个地方上班,懒床是睡不了一点的,好一点的是冬天起来不用遭受寒风的洗礼罢了。 + +有的远程工作还会要求你记录每个时间段(细到每小时)你都做了哪些工作,我暑期的就有一份我老师推荐的远程工作(实习)就是这样,我就没干了。要论自由,接单/自由职业是最自由的,项目进度、时间都由你自己把握。 + +此外还有一点就是技术栈的因素。我并没有跟随国内的主流的 Web 开发技术栈去学习什么 Java,也庆幸还好当初认为 Java 这门语言繁琐的要死,让我转变使用 JS/TS 来进行 Web 全栈的开发,而如今的重心也是在 JS/TS。 + +而这套技术栈在国外的远程工作的岗位占比很大,一些初创公司也会采用这套技术栈。而我本身学的就是 JS/TS,自身优势反而更能体现出来。但反观在国内绝大多数公司我也就是老老实实做前端的那种。况且如果只想靠国内的主流传统的技术栈而去争取一个远程岗位,我认为当下还是很难实现的。就国内的职场环境下,如今工作都难找了,就别说本身岗位就少的远程工作了。 + +## 正常坐班 + +退而其次,那就正常上下班通勤,而这又有的抉择了。就以我自身举例把,我老家福建宁德且我在厦门某三流大学读书,意味着我只要在福建本地找工作,就可以过的相对来说舒服,人脉资源、衣食住行都不用过多考虑。但倘若选择在外打拼,意味着可能要适应陌生的城市生活,建立起新的人际网络,面对未知的困难和挑战。而这其中所能利用的、所要遭受的,都得由自个儿来承担。 + +今年冬至的时候我与家里人商讨过这个问题,家里人的意见更多偏向于留在本省,只求我稳稳当当,脚踏实地。因为我从小到大独立性很差,加上我确实有那么亿点宅,到外面恐怕是吃不消。所以在他们眼里哪怕在本省薪资不是令我那么满意,但至少可以过一种相对舒适的生活。而我自己则认为有必要去外面见见世面,有时候很多苦只有自己吃过才知道有苦。 + +不过最终如何选择,还得取决于我自己。这对于多数人也是值得花时间思考的。 + +最终如果选择重归故里,那么一开始就身在其中是否会更好?又何必在外漂泊,独自承受着一切。 + +### 我对打工/上班的看法 + +我经常拿我高考结束的那个暑假来做例子,我的一些高中同学去当外卖骑手、餐厅服务员,将他们的假期时间拿来打工赚钱,而我却埋头苦干的[学习编程(易语言)](/blog/2020-year-end-summary),以兴趣驱使我学习一门技术。 + +或许是因为有过几段[赚钱经历](#赚钱经历),加上家境和个人意愿,所以至今为止我都不可能会去从事这种回报率不高,或者说没有“未来”的工作。与其打工/上班,尤其是耗费大量时间与体力劳动的,不如将时间拿来提升自己综合能力。(个人观点,或许有些极端) + +可能是由于受自由的影响加上年轻有精力折腾,同时自己又是那么不走”寻常路“、不遵循“规则”的人。所以像那种安稳平淡,重复劳动的工作,如体制/编制内的职业就不太那么感冒。 + +但从现实是没人想打工/上班,却依旧有人从事这些工作。绝大多数打工人别无选择,因生活所迫,只得忍受和抱怨打工/上班的痛苦,却又寄人篱下。 + +## 期望的工作 + +要说不工作是不可能的(但打工是不可能的),或许是因为这几年的一些工作经历有关,导致我有点习惯了远程,加上网间传闻 996 的压力下,我现在对坐班甚至还有些厌恶。对我最理想的工作环境就是能够自我在决定在某个时间点是否工作,例如在工作日放假,在节假日上班。 + +接单虽说也是,那这毕竟不是一份正经的工作,更多意义上来说是份兼职。更多是作为一时之需,接单又不可能接一辈子(搞得工作能工作一辈子似得)。 + +而我想说的是做个自由职业者,在细一点也就是独立开发者。但是现在来看这并非易事,不仅需要一定的技术能力和营销水平,最关键的想法就已经让绝大多数人对这份职业扼杀在摇篮中。 + +## 现状 + +首先我对自己的生活方式定义是:**偏向于自发的能量爆发,而不是有条理的持续努力。** + +今年我的 github contributions 出现过了几次长时间的空缺,那段期间我基本上没有在写代码,而是将心思用在其他事情上。例如只打游戏(好在游戏如今也是戒了)、只想摆烂(好在是摆的有点多了)、只想复习(好在该考的试都已经考了)。**这就导致很多时候我会将全部精力专注在某件事情上,而抛弃其他与之无关的事情**。 + +这也就导致了我很难养成一个良好的习惯,我的生物钟也因此而发生巨变。贴一张极为离谱的作息 + +![Untitled](https://img.kuizuo.cn/202312250547470.png) + +总之持之以恒在我这还不存在过,**多数的开始是因一腔热血,而最后结束时却草草了事**。 + +我想是是过度的自由造就了这般现状,想要解决这个问题,坐班可能还真可以。 + +--- + +或许是因为比同龄人提前踏入过社会,有过几段工作经验,所以在就业形势严峻的时代,其实我反倒不是那么慌张。 + +何况目前经济状态也还算正常,哪怕不找工作也足够养活我自己好一整子。至少相比同龄人下,我已经挺富裕了。也没必要搞个打赏在自己的网站或项目上放置收款二维码等无关项目信息。对我而言,编写这些的目的不是为了接某个单,赚个打赏费,更多是一时兴起,分享给有需要的人,希望让其更为纯粹一些,仅此而已。 + +我对赚钱的态度也很简单,悦己便可(做点让自己愉悦的事情),数额能够维持自身生存便足够了。不过现在我还没开始到还贷的境地,或许只有压迫感来临时,才会让我激起对金钱的渴望,改变对金钱的态度。 + +**取悦自己,生活最好的心态。** + +## 感慨 + +接触 web 开发算下整整两年半的时间,在这期间我并没有很好地扩张自己的技术面。在折腾方面确实不如以往,学习主动性也欠缺许多。不同于一开始所学习那样,喜欢瞎捣鼓,看到某个东西就会想尝试安装。 + +明年也就 24 岁了,面对即将到来的本命年,下一个阶段的走向其实很迷茫,面对我的是留学还是工作,我到现在都没有定数… + +虽说我现在大四还在读,但其实我已经比同龄人晚毕业 1-2 年了(此时怀着感慨的泪水不禁流下)。心智上还保留着校园少年的青雉 ,同时也多了份成熟的稳重。 + +有过这些经历让我收获了更为珍贵的经验和独特的成长路径,我想未来的日子必定是丰富多彩的。 + +## 往年回顾 + +- [2022 · 逆向到Web开发](/blog/2022-year-end-summary) +- [2021 · 休学一年](/blog/2021-year-end-summary) +- [2020 · 编程之旅-起点](/blog/2020-year-end-summary) diff --git "a/blog/reference/\345\217\231\344\270\200\345\220\215\350\275\254\344\270\223\344\270\232+\344\274\221\345\255\246\347\232\204\345\244\247\345\255\246\347\224\237\347\273\217\345\216\206.md" "b/blog/reference/\345\217\231\344\270\200\345\220\215\350\275\254\344\270\223\344\270\232+\344\274\221\345\255\246\347\232\204\345\244\247\345\255\246\347\224\237\347\273\217\345\216\206.md" new file mode 100644 index 0000000..a165579 --- /dev/null +++ "b/blog/reference/\345\217\231\344\270\200\345\220\215\350\275\254\344\270\223\344\270\232+\344\274\221\345\255\246\347\232\204\345\244\247\345\255\246\347\224\237\347\273\217\345\216\206.md" @@ -0,0 +1,286 @@ +--- +slug: narrate-a-college-student +title: 叙一名转专业+休学的大学生经历 +date: 2023-07-11 +authors: kuizuo +tags: [年中总结, 人生感悟] +keywords: [年中总结, 人生感悟] +description: 叙述一名转专业+休学的大学生经历 +image: https://img.kuizuo.cn/202312270109542.png +--- + +我一般很少做年中总结,但是这上半年发生在我事情比较多,加上毕业季,万千感慨涌上心头。 + +过得很快,本该在这个时间段毕业的我,因一意孤行申请休学一年,导致我比原同一届的人晚毕业一年。也正是这个决定改变了我的人生轨迹,让我成长了许多。 + +如今的我作为一名准大四的大学生 👨‍🎓,且经历过转专业和休学的大学生,来叙述自己的经历。 + + + +## 前言 + +如果你有注意到我的 [github contributions](https://github.com/kuizuo?tab=overview&from=2023-07-01&to=2023-07-19),你会发现我有整整一段时间(约 2 个月)没怎么编写代码和记录博客了。在这期间整个人的状态很差,十分消沉,迟迟到现在为止。 + +因为在这期间我被**公安传唤**了。说难听点,我当时的身份是犯罪嫌疑人。当然现在应该也算,处于取保候审状态,除了不能出国外,目前还算自由。 + +:::info 补充: 暑假期间还想去香港旅游,去办理港澳通行证的时候被拒绝了 🤡。还想着去尝试办理 visa 卡(可境外金额交易),现在看样子结果已经毋庸置疑了 😔。 + +::: + +至于案情我不方便细说,总之与互联网(技术)相关且在我休学期间出的事情。一开始关于这个话题我并不想提及,毕竟说出来肯定对自身有所不好,因此本文便迟迟都处于草稿状态。但结合自身经历,我认为非常有必要把这些内容记录下来,避免重蹈覆辙。 + +## 转专业+休学 + +我想大学内有这两项经历的人应该是在少数,我会把缘由交代明细。 + +### 转专业 + +我大一时的专业是 《信息与科学计算》,所属数学系,这个并不是计算机专业(虽然涉及到一些计算机相关的课程,但本质还是理学,不是工学),所以我转专业的原因很简单,就是要**科班出身**。当时就我看来科班出身能为我以后工作带来很大的帮助(当时的想法很天真,没有考虑转专业后所要付出的代价),但在这些年的经历以及行内的各个大神的能力告诉我,实际上并不会带来多大的优势。 + +我转专业并不难,因为我暑假就已经接触编程,并且在大一的时候,每天不是在写代码,就是在看代码,那段时间可谓是我人生编程学习最快乐的时光。所以转专业的考试对我来说特别轻松加上我数学天赋还不错,原专业的成绩也 OK,屁颠屁颠地将转专业填报单提交了上去,下学期便分配到新的班级中修学新专业——软件工程。 + +然而就在我转完专业,我便开始后悔转专业了。我发现学校老师所教的是什么牛马?真的就是会念 PPT 就便会教课,而且所教的多数内容,所发放的题材都是相对过时甚至被淘汰的东西。难怪说学生会对编程失去兴趣,要是我一开始跟着老师这么学,现在那有愧怍二字。 + +课程教学质量差也就算了,课程设计的要求还与学生课堂的内容还不同,我很难想象这学期只教 Web 前端(ES5 时代),却要求课程设计实现一个带后端接口服务以及数据库服务的程序。要不是我当时的基础还算好,恐怕连项目买来都不知道怎么跑起来。 + +不过这也是国内绝大多数高校现状,课程内容老旧,教案设计不合理,在越垃圾的大学中,这种情况反而更明显,恰好我就读于垃圾中的垃圾。与其抱怨教学质量,不如自己潜心学习,也正是因如此,自我学习能力才能有所大提升。 + +也不能说转专业对我没有一点帮助,毕竟自身有了一定的编程基础知识,在专业课上回答问题上我还是能说上一点的。并且每次课程设计与考试的时候总有人会找我来报个大腿,老师也见识到我的专业能力,我这一小组成员都能轻松通过课设答辩。(主要还是归功于我吹牛逼的能力) + +关于转专业,还有一点就是补修。比如我大一是数学系的,当时的课是叫数学分析,而在软件工程专业就是传统的高等数学,运气还算好,这两门类别相同,可以做学分替换。但往往没那么幸运,就需要额外花时间去修之前的软件工程大一的课程,在跨度较大的转专业中甚至还会更多。 + +:::warning 警示 + +最后我想告诫一些有想转专业的同学,如果你能接受转专业的麻烦,并且真的认为转专业对你带来帮助,那可以转。但如果只是为了换个班级换个室友什么的,转后的代价或许比你与同学间不友好相处四年还要负重。**总之,转与不转,最终目的是以最快捷轻松的方式拿毕业证为主。** + +::: + +### 休学 + +然后在我大二上的时候发生了一个契机,我当时编写过一个易语言软件能够自动完成大学生网课视频、作业的[程序](/blog/chaoxing-helper),并将其发布在网络上免费使用。 + +挺巧的是不久后,厦门当地(距离我学校也就 5 公里)的一个工作室(算我有 4 个人)恰好看到了这个软件,问我能否在此基础上实现的一些功能,也说明了他们的目的,想要一同合作,我主要负责技术,他们负责销售推广。我思考了下可行性,于是结合转专业的懊悔之下,我义无反顾地办理休学手续,开始了我的休学之旅,准备大干一场! + +[](https://img.kuizuo.cn/202312250425022.jpeg) + +现在回想当时我的做法太过于任性,当时是 12 月份的时候,也就是临近考试周时,我便不在宿舍复习,甚至考试我都是直接没去的,而是直接到工作室里开始改写我的程序。以下是我当时学期的成绩单(有点惨不忍睹) + +![Untitled](https://img.kuizuo.cn/202307110122716.png) + +我当时的想法特别天真,认为只要技术在身,天下我有。~~甚至当时都考虑花点时间学习渗透技术来入侵教务系统来改分(好在目前为止我都没真正接触过渗透技术,不然就真的是太刑啦)~~ + +还有就是我本想的是休学带辍学,也就是休学一年期间 搞的好就辍学,搞不好就复学,当然前者是比较多的,不然也不至于连考试都没去考。(现在回想太亏了,因为这些课程缺课就相当于挂科,我还要办理重修,但没办法,当时的我可不想着以后的事情) + +至于说为何有辍学的想法,甚至对本科证都抱着一脸嫌弃。一部分来源于技术成就的膨胀,还有一部分来自校园生活。反观学校和身边的同学,基本处于无所事事的状态(混日子),不是打游戏就是刷短视频,虽说很听规矩,要上课去上课,要开会就开会,无一缺席,日复一日,年复一年,可没点规划,就想着如何不劳而获。说真的,就这种 B 状态,谁毕业不失业?谁毕业不打工? + +**什么样的环境造就什么样的人**。一个地方的土壤会决定植物生长的是枝繁叶茂还是弱不禁风。至少现在我知道为何那些大厂都会优先面向 985 和 211 的学生。 + +因此我要办理休学的决心非常强烈,我父母与我姐不断给我劝告,告诉我要赚钱,要工作什么时候都来的级的以后都来得及,然而这并不是赚钱和工作的原因,而是受身边人的因素。如果要我与其同那些大学的同学这样混四年,那我不如出来实打实的干一年。(这话说的会难听,但当时我就是这么想的)父母犟不过我,我也和父母表明我肯定会拿毕业证。既然孩子有想法,那就让孩子试试。 + +--- + +在确定好正式合作之后,我便到工作室布置好自己工位,以下是我曾经的一张正在跑项目的图 + +![101688581490_.pic.jpg](https://img.kuizuo.cn/202307110122808.jpg) + +现在回想,当时动不动就加班到半夜,但是我对此没有怨言,甚至很享受这段时间。做自己愿意做的事情,我认为这就足够有意义了。不过这段时间持续不长,主要开发阶段也就是上半年的事情,而下半年由于本地疫情原因加上我身处老家,迟迟未能回到工位。而平常更多的任务是维护,兴致也没有一开始从 0 到 1 的热情。要知道维护是件非常枯燥且重复的事情,运营人员一有问题,就反馈到我这边,而当时写的项目又是协议项目,基本上每隔一阵子,我就要重新抓包,重新分析参数,重新部署。加上当时易语言的项目又没有 CI/CD,于是我维护完之后,就又要将易语言的项目丢到这些服务器上,我当时非常想写一个脚本,奈何我们的做单服务器基本上隔一段时间换一批,而这些服务器又是物理机(一台真实的远程机子),而非腾讯云、阿里云这种服务器,更没有批量备份,系统镜像这一功能。因此在安装环境和部署上可以说花费了我很多没必要的时间。(也可以说因此契机我彻彻底底的放弃了易语言,对于一个大型应用而言,它有数不清的缺点) + +不过最终我还是复学了,因为在这个项目上,我只看到不断重复的日子,且项目并不是一个长久项目,在未来的某个时刻必定会被制裁(只是我没想到会这么快),最终随着一年的休学期限到来,在做了项目移交后,我便退出了该团队。在此期间自然挣了点小钱(不多,也就相当于阿里 P6 级别薪酬),足够我快活潇洒好些年了。 + +可好景不长,往往一个人在巅峰后的一段时间,必然要经历一次落差。 + +### 复学 + +办理休学手续非常容易,只需要家长知情并和学校说一下,学生提交材料,学校会给你保留学籍等待复学(期限为休学期间)。但办理复学手续可就不那么容易了,首先当时处于疫情下,学校是没那么容易进出了,需要通过学校的某个软件来申请,可由于我休学了,学院那边并没有将我的信息录入进去,导致软件压根无法证明我的身份。门口的保安可不管你进出校门做的事情,没证明就别指望进出学校。无奈,我只好联系我当时的辅导员,可保安也不认老师(真实情况就是这样),就这样耗了 10 来分钟,保安看不下我母亲与我姐在旁边等候,才肯让我进去处理完手续。说真的,假设当天没有我家属陪同,复学恐怕就变成了辍学了。 + +进出大门的事情解决了,复学找老师签字可又没那么顺利了,具体细节我就不细说了,总之就是各种找老师签字,而有的老师今天在,有的不在,来来回回折腾了 3、4 天才把整个手续处理完,最终学是复了,书是也可继续读了,此刻的心还处在小孩子不愿上学的时代。 + +但仅仅只是一年,却正好让我赶上了学费和教案的变化。 + +休学是保留你当前的学籍,在你复学的时候,给你降级处理,将你插入到新的班级里,我原本是 2019 级的,我复学后变成了 2020 级。比较不幸的是,2020 级比 2019 级的学费还贵上 1000,我认了,毕竟这学校还有啥事情做不出来,涨点学费算什么(又不得不吐槽现在 2023 级的新生学费竟然还更贵!也确实,现在多数大学学费都上涨了)。这还不算啥,最主要是我们系的教授正好换人了,人换了也就算了,教案也跟着换,这就导致也就是我要比 19 级的学生还要多上几门课,并且有些课我还需要重新补修。妈的,什么坏事都让我遇上是吧。唉,谁叫我要休学呢。 + +而复学后学校发生的一些变数也是在我休学前未曾考虑到的,变相的让我毕业的难度增加。 + +:::info 补充 + +你可能会比较好奇,为何不边上学边工作,而是要休学。我当时有两种想法,一种前面也说了,就是抱着辍学的心态,这里也就不在赘述。另一种就和我个人做事比较有关了,我做任何一件事情的前提就是不希望有其他事物来打扰,尤其是学校的因素,我学校是比较极端,规定的特别死,你想要实习只能在大四这个阶段(并且大四上还得上半个学期的课),需要将课程修完才可。因此就有了休学。 + +::: + +**如果仅仅是转专业+休学的话,`其实`就没什么好看的了,不过需要这些的铺垫才有后面一言难尽的故事。** + +## 事件之 👮🔍💧 + +在复学的第一个学期,在 2022 年 5 月 x 号的 22 点时,我工作室负责其他业务的同事突然打电话告诉我(语气还略带急促,我猜测是因为项目与他无关就放了),说我原先待的工作室的人都被抓了,说和我之前写的项目有关,叫我把相关代码全都删除了,这段时间不要联系他们。挂断电话后,我二话不说直接把我台式电脑搬到了其他宿舍,然后把我的另一台备用笔记本电脑放在桌上,我生怕 👮‍♂️ 第二天直接来我寝室来拿电脑。就这样提心吊胆几天,我内心暗自窃喜以为没事,毕竟我已经金盆洗手不做了,应该不会再追究吧,果不其然,越担心的事越会发生。就在几天后的周六中午突然来了一通电话,我一看 0xxxxx110 显示 xxx 公安局,完了看来是逃不掉了,但我还是抱着侥幸的心理,我选择了拒接就当我睡觉没看见(我当时确实因为这个电话而醒)。打了两通后没回应,又过了一段时间后,突然我姐给我打了电话,说我学校当地的派出所打电话给她,说联系不上我,然后叫我姐联系我看能不能联系上,顺便问我发生了什么事,我并没有告诉他,然而我心里很清楚,就是和我原本工作室的同伴有关。(至于说为什么 👮‍♂️ 有我姐的联系方式,我只能说我傻逼,每次学校填写什么表格的时候,有个家长联系人,我都是填写我姐的电话。也就是从这一阶段开始,所有要我填报的信息都是虚的) + +然后我就回了公安局一个电话,和我确认完身份后,叫我带上 🆔  和 📱 叫我到学校门口,有 🚓  接~~送~~。我一上车后,就直接夺走了我的 📱(真),在车上就问我:”知道我们找你来是干什么的吗?“ 我默不作声,一直到了公安局后,接下来的安排懂的都懂,从中午做 📝 一直到晚上 8 点,从醒来到回校的期间别说吃东西了,连 💧 都 tm 没得喝。这一整个阶段就是要我交代当时写这个项目的负责了什么、盈利了多少、收款方式有什么,然后打开我手机的支付宝,微信,银行卡查我账户里的 💰,总之先把我号里的 💰 转到他们的卡里先,我当时和他们说微信里的 💰 我父母给我的生活费,也是我平常主要的消费方式,我也把相关的账单记录给他们看,确信后才没有将我微信的 💰 转过去。(这么一说似乎还挺良心) + +然后 📝  做完之后,👮‍♂️ 说他们局长本来应该把我带到他们当地公安局一趟,可由于疫情的因素,就为我进行取保候审,(当然,这句话肯定是有吓唬成分),接着身份信息采集,打印账单记录,签字就不在话下了。至此在这个案件没结束之前,我都将作为一个嫌疑人处理。整个过程弄完之后叫我过几天再来一趟,主要就是二次笔录,在进一步确认哪些是“非法所得”。恰好我回去的时候还下起来大雨,还得用身体去遮挡着 👮‍♂️ 给我的那几份回执单,通知书。 + +![Untitled](https://img.kuizuo.cn/202307110122809.jpeg) + +:::note + +你可能会好奇 👮‍♂️ 为何没有没收我的”作案工具“,首先我很明确的一点是我已经半年没碰这个项目了,其次我的“同伙”已经做好了相关笔录,也把大致的流程交代完毕,那么 👮‍♂️ 大概是已经掌握我做的部分了,再者我不像我的”同伙“第一时间被“逮捕”,距离第一时间已经过去了几天,我该处理的数据也都处理的差不多了,这时候再没收的意义也不大,得不到实质信息。 + +::: + +这也是我第一次体验到被任人处置的感觉,很不是滋味,这与学生时代被老师批评罚站或者被家长训斥不同,讲不清楚的感觉。在这期间,到目前现在印象还非常深刻的一句话:**不如实交代的话,让你连书都读不了!** + +不过现在回想都是吓唬话,一种侦查的手段,只为了不择目的的获取更多的信息。因为 👮‍♂️ 从头到尾都没有直接通知我校,而是直接联系我本人,假设人家真的想让我读不了,直接联系学校,说明这个学生的行为,那么我必定会被开除(用开除学籍来保留学校的声誉)。不过最终学校还是知道了这件事,放到后面再说。 + +回到宿舍我便将台式搬回到原本的宿舍,坐在椅子上沉思了许久,殊不知我已经一天没吃饭了,然而此时的我毫无饿意,复杂的心绪让我发了条朋友圈: + +![Untitled](https://img.kuizuo.cn/202307110122810.png) + +至此,事实告诉我白白浪费了一整年的时间,并且此后还将给我带来了诸多麻烦。 + +但如今我回想这个事件,我都已经退出这个团伙近半年了,已经金盆洗手了,却依旧有所关联。👮‍♂️ 可不管你的解释,只要你参与了,获利了,势必要追究下来。如果我当时能够劝阻他们不做,或许如今就没有这么多麻烦事,可这仅存在于如果。 + +![Untitled](https://img.kuizuo.cn/202307110122812.png) + +回到这个项目而言,本质上确实不是很正规很传统,但也不至于黑产那种,更多的称呼是灰色产业。👮‍♂️ 给定性为 “**提供侵入、非法控制计算机信息系统程序、工具罪”**,但事实上这个程序并没有非法侵入服务器,我只是将用户的正常操作转变为电脑程序操作,将正常的数据包给模拟出来,可以省去人为操作的一个过程,而不是入侵对方的服务器,通过数据库的方式直接修改。在笔录期间,我还特意举例了游戏外挂和游戏脚本的区别,前者是实实在在的篡改数据,后者则是程序来模拟人为的情况,而我所做的部分就是游戏脚本的部分,根本不能算作非法控制。但 👮‍♂️ 不会按照我的理解,更别说检察院了,他们都只会认为这是在”破坏公平性“,那么就归属同一“恶劣”性质,就可以归属这个罪。总之我说的再多,说的再好,也都不及别人的一句反对。 + +### 时隔一年 + +这件事情过去快一年后,也就是取保时间要到之际(前段时间),👮‍♂️ 传唤几个当事人去他们当地的派出所一趟。对我而言,这段期间正好在读书阶段,此次之行大概率短时间内是回不来的,无奈之举,只能把自身的情况和辅导员说明清楚后,便踏上了不知用何形容词修饰的路。 + +在外地待了整整两周,住了整个两周的酒店。至于说过程也没啥特别的,**一切都以流程为主**,再次用 📝  核实了一下情况,从公安阶段移交到检察院阶段(此时一般就没 👮‍♂️ 的事情了),将材料都归递到检察院,等待公安这边解除取保状态(钱保),再到检察院这边开具监视居住书,然后告诉我们可以回去等通知了。 + +所以情况就是这么个情况,抛开公安这边,回到学校的第一时间我就与辅导员汇报了情况,此时就不得不向学院解释我的情况了。 + +事实上学校是知道我这个事情的,因为当时当地的公安局的一位 👮‍♂️ 在与外地的 👮‍♂️ 配合,而这个当地的 👮‍♂️ 正好和我校学安处的老师比较熟,没过多久就找我谈了话喝 🍵(真),我将情况和他们汇报,但当时认为还没审判阶段,还只是嫌疑状态,所以就暂时不做处理。 + +直到了这次传唤之后,等我再次回到学校后,没过几天就被辅导员叫去和院长谈话了,在这谈话期间,我都一五一十的交代清楚,但听完我的叙述后,院长给我的态度其实并不友好。因为在他眼里可能也认为我编写的程序也不至于被公安传唤,再到检察院的阶段,便觉得我没有如实交代,说话的语气都带有警告之意,总之各种吓唬的口吻,院长也和 👮‍♂️ 说了几乎同样的话:“如果被我们查到事实和你所说的不符,那么我们这边会直接开除你”。最搞笑的是他还提了一下,如果被开除的话,也不要想不开什么的。就让我很无语,到底是安慰呢,还是警告。 + +![441703600436_.pic](https://img.kuizuo.cn/202312262235233.png) + +至于为何要这么说,我想有很大原因是因为学生的负面因素会影响学校的名誉(挣钱),我想读过大学的人应该很清楚。但在谈话的期间,我表明了我是在休学期间做的事情,只是在校读书期间被叫去了(在校期间都没接触这项目),并且 👮‍♂️ 是没有直接通过校方来找我(我想是 👮‍♂️ 也不想麻烦学校),可以这么说,如果当地的 👮‍♂️ 与我校老师没有交识,校方可能都不知道我这个情况。但无论我如何解释,校方还是会以你是这个学校的学生身份来对你进行处理。 + +![Untitled](https://img.kuizuo.cn/202307110122813.png) + +当然,我可以比较肯定的是,辅导员也不希望我出事,毕竟现在大学生如果出事,辅导员也是要背点锅的,但院长的话我就不清楚了,但从与他的谈话中,总感觉他不见得我好的样子。 + +不过要说学生身份是否对我有利的话,那答案是肯定有的。比如说开具学生在校证明,酌情处理等等。但不是说大学生就是免死金牌,这要是换成未成年人,那还说不准真是。 + +## 我对此事件的看法 + +我想大致的剧情就交代了差不多,至于这一年期间找律师询问,找关系,操作什么的,我就不细说了,篇幅来的太长,且谈话内容很复杂,现实很残酷。我不想将这些负面,非正能量的东西传递下去,我本还是相信会社会会变得越来越善良的。但惟有一点我明白的是,没有办不成的事,只有到不到位是事。自我领悟这段话的意思。 + +回到这件事情,在来说说我的看法(补充)。 + +### 利益 + +因为当时这个项目的收益都不是以公司的名义,而是通过多个下级使用私下转账到某个总账号(个人号),如果金额不是很大,那大概率也没什么事情,但金额一旦大,又没有缴税,这种做法无法就是非法经营。只不过项目涉及网络因素,且又有点灰产的性质,所以嘛。。。 + +从一开始这个收入形式就已经决定了 💰  的合法性,再加之利益摆在那,自然而然就被盯上。假设这个项目都没产生任何利益,没有破坏他人的利益,哪怕只是个灰产项目(开源),我认为 👮‍♂️  不会折腾到去找你的麻烦。可以说没有利益与负面影响,就不会这么多麻烦事祸降到头上。 + +### 不一定什么都要交代 + +在笔录阶段,👮‍♂️ 的手段都会比较极端,希望当事人能够积极配合,透露出更多的信息,还告诉你能够酌情处理。但很多情况下,交代的越多反而不是什么好处,尤其在诱导性提问会让你偏向本不是你本意的回答。举个例子(算是真实例子,我听别人说的): + +某人 A 入职一个普通公司,就当 A 做了半年员工时,突然有一天警察突击这个公司,叫所有人双手抱头蹲下,A 不知道发送了什么,到了做笔录的时候,👮‍♂️  告诉 A ,这家公司是一家诈骗公司,这时 A 开始思考,在一开始入职的时候确实不知道是诈骗公司,但 A 做了有半年的事情,慢慢也**意识**到自己做的就是诈骗相关的,但待遇不错,他也不当回事,也没有离职退出什么的。接着 👮‍♂️ 问他,你知不知道你做的是诈骗,这里假设 A 有几种说法: + +1. A 是个老实人回答道直接回答道:我知道。 +2. A 心里知道,且在与 👮‍♂️ 多次回答中交代自己的行为在他的认知下不是诈骗。 +3. A 心里知道,可 A 嘴比较硬,死活不承认,甚至连诈骗字样都没说,一副装死的样子。且有一段这么回答:什么?这是诈骗?你要说是诈骗的话我肯定不干,谁会去干这东西。 + +假设你是 👮‍♂️ 你会抓谁,毫无疑问,1、2 是肯定要抓的,因为从回答和行为上都已经承认了是诈骗,哪怕只是一点都算。即便 A 想要翻供,可警察却说以第一次笔录上说准,说 A 当时已经都回答了,可 A 不知要如何反驳,甚至无法反驳。 + +但 3 的情况也许就不一定了,因为从 A 的笔录证词上,确实表明 A 主观不认为是诈骗,且是员工身份,雇佣关系,即便事实发生,只要诈骗的 💰A 没拿多少,而是工资形式,除非 👮‍♂️ 真的有 A 的诈骗实质性证据,那么 A 大概率是无事发生。 + +例子可能不是很好懂,但我想要表达的是:不要被 👮‍♂️ 的话语所带入,一切回答都要以不是、不知道为主,且不要模棱两可、含糊不清,这种回答在 👮‍♂️ 认为就是”肯定“,意味着是知法犯法。一定把自己装的什么都不懂,哪怕自己内心什么都知道,也要装成哑巴一般。 + +这就不禁让我想说出但不记得是从哪里看到的一句话: **坦白从宽,牢底坐穿;抗拒从严,回家过年。** + +### 技术岗位或许是个高危职业 + +试想一下,在上面的案例中,这个诈骗公司的技术人员与客服人员对比,你认为那么最后的结局会比较惨。再比如某技术人员不小心写错了代码,并且将其部署到线上环境,导致公司损失重大,你说责任在谁? + +这样例子有太多了,很多情况下出了事情,技术人员要占据非常大的责任。但伴随这份责任的风险,也伴随着巨大的收益,风浪越大,鱼越贵。 + +## 后悔? + +如果说没有这些变故,那我毫无疑问是无悔的,因为挣到了本不属于我这年龄段所拥有的资本。 + +论事实,这些资本化为乌有,同时耗费了一年的时间的情况下呢?那必然是会后悔一部分,我后悔的是没有听从我父母的劝导,在合适的时间做合适的事情,给家里人带来了许多的麻烦;而不后悔的是这段经历,让我成长了太多,让我如此膨胀的内心收敛的甚许,也让我在做任何事情都要三思而后虑,而不再一意孤行,固执己见。 + +提前发现自身的错误,及时纠正,以免重蹈覆辙。有很多道理也是在我经历之后才深刻明白的,只不过这次学习的“学费”比较贵重。 + +**年轻难免犯错,但也正是从错误中成长。** + +故事的经历就告一段落,未来的路还很长,要考虑的事还有很多,有机会在慢慢叙述。 + +## 一些题外话 + +我想听完上面的叙述应该能解释的通这近两个月的状态情况。 + +我本以为奖励自己搞台 MacBook 能够将状态调整回来,然而不到 1 周的时间变又开始萎靡不振,对一切新鲜事物失去了好奇感。已没有当初纯粹的兴趣,留有的是对生活的无奈。找寻不到花费一晚上解决一个 bug 的成就感,留有的只剩错中复杂的需求与枯燥乏味的工单。 + +我开始思考问题的所在,如何长期保持某种状态。最关键的因素莫过于情绪,曾经的我能够保持不断学习的状态正是因为内心无忧无虑。可时至今日,伴随我的是焦虑、压力、不安、迷茫,**总觉得心里悬着什么东西放不下**,可至今我也没能找到很好的解决方案,也许等案子结束,一切都将会从容自如。 + +我曾收到一本书,名为《谁的青春不迷茫》,到现在为止我也没看过,为什么呢?就书名而言,在我收到这本书时,我不认为我有何迷茫的地方。可现在我又想重新拾起这本书,却不知这本书被我放置何处。 + +### 对我来说的本科 + +在对这个社会没有认知的前提下(即休学前),我自认为能力是完全能够胜过一本的证书。外界都说没有本科不好找工作,现如今怕是本科都不好找工作咯。就我事实而说,我的几份工作都不是本科给我的,而是我的项目经验,所以也因此我更加坚信我自己对本科证的蔑视看法。 + +但有两段经历(谈话),改变了我对此的看法。 + +1. 在我与一位 40 岁的技术人询问过一些问题,其中一个问题是问国内读研还有必要吗,人家没有直接否定,而是直接告诉我,很多公司现在阅读简历会更偏重于看第一学历(本科的学历),也就是看你本科学校好不好再看你其他学历,至于为何想必不用多说,高考考进清华和研究生考进清华的难度便知。 + + 而另一个问题就是问他本科有没有必要,而就举了他曾经项目投资的例子,说如果投资人发现要投资的项目的负责人没有本科学历,人家可能就不会投资了,大致意思是这样。 + +2. 一位国外华人的 hr,看到我写的一个作品,并且他公司正好急招一名相关的开发者(远程开发),便于我交谈了起来,然而他没想到的是我竟还未毕业,而他们招聘的最低要求便是本科生。然后又进行了一番简单交谈下,得出的结论是:以我目前的身份只能开实习生或高中学历的薪资(薪资差距在 4 倍左右,国外的待遇我不说具体的薪资你大概也能猜出有多少),hr 给的建议是对我来说就是拿着点薪资就有点浪费时间,于是便错过了这个 offer。 + +我觉得没有什么能比收益更具有说服力,因此也确确实实改变了我的看法,至少在中国是这样的。 + +就单从社会展示的数据上也可以知道,自考成人本科的人是越来越多,很多公司招聘都是本科起步。没经历只看数据对我而言是意识不到其重要性的。 + +本科固然重要,可反观现在中国的大学还是用来学知识培养人才的吗?我更倾向于说是用金钱消耗时光最终换来一本证书,至于这笔交易值不值这个价,我想多数大学生的内心已经有了答案。 + +如果要说大学对我来说最大的作用的一件事,无非就是白嫖各种大学生认证优惠(如苹果的教育优惠,github 学生认证,学生票等等),确实给我省下了不少的钱。除此之外,我很难再说出第二个实质性的作用,对我而言是更多的负面作用,例如高学费、断电断网以及学校一些系列蛮不讲理的行为,这种作风会在私立大学之间不断扩大。 + +### 感悟 + +在之前我是比较浮躁的,总想着如何一步登天,并且我与很多平常人不同,我是不走”寻常路“,不遵循“规则”的人。绝大多数人愿安安稳稳,但我可不,我宁可去冒险一番,却又不曾思考过失败的后果。 + +我很讨厌被安排,**被安排的人生,活着有什么意义?** 没有自己的主见想法,不断被灌输他人的思想,这便失去了我对生活的意义。不能做自己想做的事情,那真的是一件极其可悲的事! + +但现在让我重新做一个选择我肯定和我原先的同学一样是安安稳稳(混日子)拿个毕业证,求稳已经变为了我当下一个很重要的标识,只要主观上有风险的事,哪怕利润再高,我也不会尝试一番,我经不起风险,至少在我没有绝对”保险”的前提下。 + +可如今的现状又怎么走的了宽敞,平坦的道路。也许生来就不应该平淡度过,最终要身处何处才能对的自己颠沛流离的一生。 + +--- + +人总是不愿听闻他人的建议,尤其这个建议还是长辈给予的。可多数人总会认为自己就是最正确的,自己的做法就是最优解,过度的自信很难看清自己几斤几两。长辈的建议是用他的人生阅历换来的,而你的想法是凭空而造,不切实际。(说的便是我自己) + +很多建议仅仅只是传达给他人,他人即便意识到有道理,但也通常不会有所改变。只有自己亲身经历过后,回想起他人的建议是多么的宝贵。所以我也渐渐不怎么再给别人安利,反而是一种浪费口舌的行为。 + +--- + +曾经我是一个特别念旧的人,时常会怀念过去的巅峰、美好。因为在落魄之时,也能想起曾经所拥有的,让我重拾信心。能让自己不断前行的事物只有自己所经历的过去,而记录便成了我仅有回忆方式。 + +可现实却是不断打击我曾有的美好,好像上天就巴不得我好的样子。可没有什么上帝,任何事物所发展的结果都将有始有终,自己做的事情没有人比自己清楚,我曾怪罪自己运气不好,现在我意识到是努力不足。没有不幸之人,只有懒惰之徒。在绝对的实力面前,运气再差的非酋都能化身变为欧皇。 + +曾经有个人告诉我,凡事都要往前看,**只迷恋于过去只会让自己活得越加困难**。可当时的我怎么可能懂得这个道理,活在过去的美好之中,忘却了当下的目标。 + +不知道自己当下要做什么,是因为不知道未来要干什么,没有目标的未来,犹如没有罗盘的航行,在茫茫大海之中无处漂泊。 + +借阿甘正传的一句名言: + +> _You got to put the past behind before you can move on._(你得把过去抛在脑后才能向前看。) + +## 四年的编程 + +```jsx +const timeline = ['易语言', '逆向', 'JavaScript/TypeScript', 'Web 全栈'] + +timeline.forEach(time => console.log(time)) +``` + +回到技术角度,随着我的 4 年编程经历就这么过去了,和曾所预期的技术要求还差之甚远,没能给自己一个很好的技术交代,没能在这四年结束之际进行编码,没能给这段经历一个完美的句号。 + +倘若没有编程,我甚至都不知道自己应当从事何种职业,可以说编程是我的再生父母,没有 4 年前那场偶遇,便没有如今的我。 + +**道阻且长,行则将至;行而不辍,未来可期。** diff --git a/data/features.tsx b/data/features.tsx new file mode 100644 index 0000000..c7822f4 --- /dev/null +++ b/data/features.tsx @@ -0,0 +1,52 @@ +import WebDeveloperSvg from '@site/static/svg/undraw_web_developer.svg' +import OpenSourceSvg from '@site/static/svg/undraw_open_source.svg' +import SpiderSvg from '@site/static/svg/undraw_spider.svg' +import Translate, { translate } from '@docusaurus/Translate' + +export type FeatureItem = { + title: string + text: JSX.Element + Svg: React.ComponentType> +} + +const FEATURES: FeatureItem[] = [ + { + title: translate({ + id: 'homepage.feature.developer', + message: 'Python', + }), + text: ( + + 作为一名 TypeScript 全栈工程师,秉着能用 TS 绝不用 JS + 的原则,为项目提供类型安全的保障,提高代码质量和开发效率。 + + ), + Svg: WebDeveloperSvg, + }, + { + title: translate({ + id: 'homepage.feature.spider', + message: '深度学习', + }), + text: ( + + 作为一名曾学习与实践逆向工程两年半的开发者,对于逆向工程有着浓厚的兴趣,同时造就了超凡的阅读代码能力。没有看不懂的代码,只有不想看的代码。 + + ), + Svg: SpiderSvg, + }, + { + title: translate({ + id: 'homepage.feature.enthusiast', + message: '3D Vision', + }), + text: ( + + 作为一名开源爱好者,积极参与开源社区,为开源项目贡献代码,希望有生之年能够构建出一个知名的开源项目。 + + ), + Svg: OpenSourceSvg, + }, +] + +export default FEATURES diff --git a/data/friends.tsx b/data/friends.tsx new file mode 100644 index 0000000..248bba6 --- /dev/null +++ b/data/friends.tsx @@ -0,0 +1,93 @@ +export const Friends: Friend[] = [ + { + title: 'Hongyi Li', + description: '台湾大学 李宏毅', + website: 'https://speech.ee.ntu.edu.tw/~hylee/index.php', + avatar: '/img/friend/zxuqian.png', + }, + { + title: 'Mas0n', + description: '梦想是咸鱼', + website: 'https://mas0n.cn', + avatar: '/img/friend/mas0n.png', + }, + { + title: 'Jetzihan', + description: 'A bug maker.', + website: 'https://jetlab.live', + avatar: '/img/friend/jetzihan.png', + }, + { + title: 'Pincman', + description: '中年老码农,专注于全栈开发与教学', + website: 'https://pincman.com', + avatar: '/img/friend/pincman.png', + }, + { + title: 'Opacity', + description: '助力每一个梦想', + website: 'https://www.opacity.ink', + avatar: '/img/friend/opacity.png', + }, + { + title: '静かな森', + description: '致虚极,守静笃', + website: 'https://innei.in', + avatar: '/img/friend/innei.png', + }, + { + title: 'Simon He', + description: 'Front-end development, Open source', + website: 'https://simonme.netlify.app', + avatar: '/img/friend/simonme.png', + }, + { + title: '前端老怪兽', + description: '一只会敲代码的怪兽', + website: 'https://zswei.xyz', + avatar: '/img/friend/old_monster.png', + }, + { + title: 'Meoo', + description: '一杯茶,一根网线,一台电脑', + website: 'https://cxorz.com', + avatar: '/img/friend/meoo.png', + }, + { + title: '尚宇', + description: '心怀理想,仰望星空,埋头苦干', + website: 'https://www.disnox.top', + avatar: '/img/friend/disnox.png', + }, + { + title: 'CWorld Blog', + description: '求知若愚,虚怀若谷', + website: 'https://blog.cworld.top', + avatar: '/img/friend/cworld.png', + }, + { + title: 'Shake', + description: '世界继续热闹,愿你不变模样,勇敢且自由😃', + website: 'https://www.shaking.site', + avatar: '/img/friend/shake.png', + }, + { + title: 'Alan', + description: '此刻想举重若轻,之前必要负重前行', + website: 'https://www.alanwang.site', + avatar: '/img/friend/alan.png', + }, + { + title: '鲸落', + description: '心中无女人,代码自然神', + website: 'http://www.xiaojunnan.cn', + avatar: '/img/friend/xiaojunnan.png', + }, +] + +export type Friend = { + title: string + description: string + website: string + avatar?: string +} diff --git a/data/projects.tsx b/data/projects.tsx new file mode 100644 index 0000000..ba72f1f --- /dev/null +++ b/data/projects.tsx @@ -0,0 +1,246 @@ +export const projects: Project[] = [ + { + title: '愧怍的小站', + description: '🦖 基于 Docusaurus 静态网站生成器实现个人博客', + preview: '/img/project/blog.png', + website: 'https://kuizuo.cn', + source: 'https://github.com/kuizuo/blog', + tags: ['opensource', 'design', 'favorite'], + type: 'web', + }, + { + title: 'JS代码反混淆', + description: '基于 Babel 对 JavaScript 混淆代码还原的工具', + preview: '/img/project/js-deobfuscator.png', + website: 'https://js-deobfuscator.vercel.app', + source: 'https://github.com/kuizuo/js-deobfuscator', + tags: ['opensource', 'favorite'], + type: 'web', + }, + { + title: 'nest-vben-admin', + description: ' NestJs + Vben Admin 编写的一款前后端分离的权限管理系统', + preview: '/img/project/nest-vben-admin.png', + website: 'https://admin.kuizuo.cn', + source: 'https://github.com/kuizuo/nest-vben-admin', + tags: ['opensource', 'favorite', 'product', 'large'], + type: 'web', + }, + { + title: 'api-server', + description: '🔗 基于 Nuxt 搭建的 API 接口服务网站', + preview: '/img/project/kz-api.png', + website: 'https://api.kuizuo.cn', + source: 'https://github.com/kuizuo/api-service', + tags: ['opensource', 'favorite', 'product'], + type: 'web', + }, + // toy + { + title: 'Chaoxing-sign', + description: '🌟 超星学习通在线签到,摆脱客户端繁琐的签到流程,让签到不再是你的烦恼。', + preview: '/img/project/chaoxing-sign.png', + website: 'https://cx.kuizuo.cn', + source: 'https://github.com/kuizuo/chaoxing-sign', + tags: ['opensource', 'favorite'], + type: 'toy', + }, + { + title: 'Hoppx', + description: '👽 仿 Hoppscotch 风格的网站模板', + preview: '/img/project/hoppx.png', + website: 'https://hoppx.vercel.app', + source: 'https://github.com/kuizuo/hoppx', + tags: ['opensource'], + type: 'toy', + }, + { + title: 'Link Maker', + description: '🍋 一个用于将链接转换为卡片样式的预览网站', + preview: '/img/project/link-maker.png', + website: 'https://link-maker.deno.dev', + source: 'https://github.com/kuizuo/link-maker', + tags: ['opensource'], + type: 'toy', + }, + { + title: 'Nuxt-Naive-Admin', + description: '🎁 一站式管理系统,融合 Nuxt、Naive UI 和 Supabase', + preview: '/img/project/nuxt-naive-admin.png', + website: 'https://nuxt-naive-admin.vercel.app', + source: 'https://github.com/kuizuo/nuxt-naive-admin', + tags: ['opensource'], + type: 'toy', + }, + // { + // title: 'Image Hosting', + // description: '🖼️ 使用 Supabase 搭建一个简易图床', + // preview: '/img/project/image-hosting.png', + // website: 'https://image.kuizuo.cn', + // source: 'https://github.com/kuizuo/image-hosting', + // tags: ['opensource'], + // type: 'web', + // }, + // { + // title: 'Vitesse Nuxt3 Strapi', + // description: '一个 Vitesse Nuxt3 Strapi 的模板,灵感来源 Vitesse', + // preview: '/img/project/vitesse-nuxt3-strapi.png', + // website: 'https://vitesse-nuxt3-strapi.vercel.app', + // source: 'https://github.com/kuizuo/vitesse-nuxt3-strapi', + // tags: ['opensource'], + // type: 'web', + // }, + // personal + { + title: 'vscode-extension', + description: 'vscode 插件的样品', + preview: '/img/project/vscode-extension.png', + website: 'https://marketplace.visualstudio.com/items?itemName=kuizuo.vscode-extension-sample', + source: 'https://github.com/kuizuo/vscode-extension', + tags: ['opensource'], + type: 'personal', + }, + { + title: '前端示例代码库', + description: '📦 整理前端样式和功能的实现代码,可以用来寻找灵感或直接使用示例中的代码', + preview: '/img/project/example-website.png', + website: 'https://example.kuizuo.cn', + source: 'https://github.com/kuizuo/example', + tags: ['opensource', 'design'], + type: 'personal', + }, + // { + // title: '@kuizuo/utils', + // description: '整理 JavaScript / TypeScript 的相关工具函数', + // website: 'https://www.npmjs.com/package/@kuizuo/utils', + // tags: ['opensource', 'personal'], + // type: 'personal', + // }, + // { + // title: '@kuizuo/eslint-config', + // description: '来自 antfu 的 ESLint 配置文件', + // website: 'https://github.com/kuizuo/eslint-config', + // tags: ['opensource', 'personal'], + // type: 'personal', + // }, + // commerce + // { + // title: 'link-admin', + // description: '基于 nest-vben-admin 编写的一次性充值链接销售系统', + // preview: '/img/project/link-admin.png', + // website: 'http://link.kuizuo.cn', + // tags: ['product', 'large'], + // type: 'commerce', + // }, + // { + // title: 'youni', + // description: '基于 nest-vben-admin 编写的一次性充值链接销售系统', + // preview: '/img/project/link-admin.png', + // website: 'http://link.kuizuo.cn', + // tags: ['product', 'large'], + // type: 'commerce', + // }, + // other + { + title: '@kuizuo/http', + description: '基于 Axios 封装的 HTTP 类库', + website: 'https://www.npmjs.com/package/@kuizuo/http', + tags: ['opensource', 'personal'], + type: 'other', + }, + { + title: 'browser-rpc', + description: 'WebSocket 远程调用浏览器函数', + website: 'https://github.com/kuizuo/rpc-browser', + tags: ['opensource'], + type: 'other', + }, + { + title: 'ocr-server', + description: '使用 nestjs 通过 grpc 与 python ddddocr 库搭建的验证码图像识别服务', + website: 'https://github.com/kuizuo/ocr-server', + tags: ['opensource'], + type: 'other', + }, + { + title: 'rust-wasm-md5', + description: '🦀 Rust + WebAssembly 实现的 MD5 加密', + website: 'https://github.com/kuizuo/rust-wasm-md5', + tags: ['opensource'], + type: 'other', + }, +] + +export type Tag = { + label: string + description: string + color: string +} + +export type TagType = 'favorite' | 'opensource' | 'product' | 'design' | 'large' | 'personal' + +export type ProjectType = 'web' | 'app' | 'commerce' | 'personal' | 'toy' | 'other' + +export const projectTypeMap = { + web: '网站', + app: '应用', + commerce: '商业项目', + personal: '个人', + toy: '玩具', + other: '其他', +} + +export type Project = { + title: string + description: string + preview?: string + website: string + source?: string | null + tags: TagType[] + type: ProjectType +} + +export const Tags: Record = { + favorite: { + label: '喜爱', + description: '我最喜欢的网站,一定要去看看!', + color: '#e9669e', + }, + opensource: { + label: '开源', + description: '开源项目可以提供灵感!', + color: '#39ca30', + }, + product: { + label: '产品', + description: '与产品相关的项目!', + color: '#dfd545', + }, + design: { + label: '设计', + description: '设计漂亮的网站!', + color: '#a44fb7', + }, + large: { + label: '大型', + description: '大型项目,原多于平均数的页面', + color: '#8c2f00', + }, + personal: { + label: '个人', + description: '个人项目', + color: '#12affa', + }, +} + +export const TagList = Object.keys(Tags) as TagType[] + +export const groupByProjects = projects.reduce( + (group, project) => { + const { type } = project + group[type] = group[type] ?? [] + group[type].push(project) + return group + }, + {} as Record, +) diff --git a/data/resources.tsx b/data/resources.tsx new file mode 100644 index 0000000..fc4ca8a --- /dev/null +++ b/data/resources.tsx @@ -0,0 +1,1156 @@ +import { Friends } from './friends' + +export interface Resource { + name: string + logo: string + desc: string + href: string + tags?: string[] +} + +export interface ResourceCategory { + name: string + resources: Resource[] +} + +const friends: Resource[] = Friends.map(f => { + return { + ...f, + name: f.title, + desc: f.description, + logo: f.avatar!, + href: f.website, + } +}) + +export const resourceData: ResourceCategory[] = [ + { + name: '友链 👨‍💻', + resources: friends, + }, + { + name: '每周必刷🔥', + resources: [ + { + name: '稀土掘金', + desc: '稀土掘金是一个技术博客平台,是程序员发布自己的技术文章、分享知识的地方', + logo: '/img/resource/juejin.png', + href: 'https://juejin.cn/', + }, + { + name: 'OSS Insight', + desc: 'Open Source Software Insight', + logo: '/img/resource/ossinsight.png', + href: 'https://ossinsight.io/', + }, + { + name: 'Javascript Weekly', + desc: 'A newsletter of JavaScript articles, news and cool projects', + logo: '/img/resource/javascript.svg', + href: 'https://javascriptweekly.com/', + }, + { + name: 'State of JavaScript', + desc: 'JavaScript 生态系统的年度开发人员调查', + logo: '/img/resource/stateofjs.svg', + href: 'https://stateofjs.com', + }, + { + name: '前端食堂', + desc: '周周尝鲜,人工筛选前端圈每周最新资讯。—— 由 童欧巴 创作', + logo: '/img/resource/zhubai.png', + href: 'https://hungryturbo.zhubai.love/', + }, + ], + }, + { + name: '站点 🖥️', + resources: [ + { + name: 'Developer Roadmap', + desc: 'Roadmap to becoming a web developer.', + logo: '/img/resource/roadmap.png', + href: 'https://roadmap.sh/', + }, + { + name: 'JS delivr', + desc: '一个免费的CDN开源项目', + logo: '/img/resource/jsdelivr.webp', + href: 'https://www.jsdelivr.com/', + }, + { + name: 'Shields.io', + desc: '为你的开源项目生成高质量小徽章图标', + logo: '/img/resource/shields.png', + href: 'https://shields.io/', + tags: ['图标', '首页'], + }, + { + name: 'namae', + desc: 'namae可让您给您的应用程序、Web服务或组织起一个好名字', + logo: '/img/resource/namae.png', + href: 'https://namae.dev/', + tags: ['起名'], + }, + { + name: 'Quick Reference', + desc: '为开发人员分享快速参考备忘清单【速查表】', + logo: '/img/resource/quick reference.svg', + href: 'https://jaywcjlove.github.io/reference', + tags: ['手册'], + }, + { + name: 'Can I use', + desc: '对浏览器支持的 API 兼容性查询', + logo: 'https://caniuse.com/img/favicon-128.png', + href: 'https://caniuse.com', + tags: ['手册'], + }, + { + name: 'NGINX 配置', + desc: '配置高性能、安全、稳定的NGINX服务器的最简单方法', + logo: '/img/resource/digitalocean.png', + href: 'https://www.digitalocean.com/community/tools/nginx', + tags: ['nginx'], + }, + { + name: 'BootCDN', + desc: '稳定、快速、免费的前端开源项目 CDN 加速服务', + logo: '/img/resource/bootcdn.png', + href: 'https://www.bootcdn.cn/', + tags: ['cdn'], + }, + { + name: '那些免费的砖', + desc: '发现免费可商用的资源', + logo: 'https://img.thosefree.com/static/logo.png', + href: 'https://www.thosefree.com/', + tags: [''], + }, + { + name: '正则大全', + desc: '🦕 常用正则大全, 支持web / vscode / idea / Alfred Workflow多平台', + logo: '/img/resource/any-rule.ico', + href: 'https://any-rule.vercel.app/', + tags: [''], + }, + ], + }, + { + name: '文档 📘', + resources: [ + { + name: 'MDN', + desc: '从2005年开始记录网络技术,包括 CSS、 HTML 和 JavaScript。', + logo: '/img/resource/mdn.png', + href: 'https://developer.mozilla.org/zh-CN/', + tags: ['Css', '教程'], + }, + { + name: 'ES6 入门教程', + desc: '《ECMAScript 6 入门教程》是一本开源的 JavaScript 语言教程,全面介绍 ECMAScript 6 新引入的语法特性', + logo: '/img/resource/es6.png', + href: 'https://es6.ruanyifeng.com/', + tags: ['文档'], + }, + { + name: '深入理解 TypeScript', + desc: '《TypeScript Deep Dive》 是一本很好的开源书,从基础到深入,很全面的阐述了 TypeScript 的各种魔法,不管你是新手,还是老鸟,它都将适应你', + logo: '/img/resource/typescript.png', + href: 'https://jkchao.github.io/typescript-book-chinese/', + tags: ['文档'], + }, + { + name: 'Rust语言圣经', + desc: '一份高质量 Rust 教程', + logo: '/img/resource/rust.svg', + href: 'https://course.rs', + tags: ['文档'], + }, + ], + }, + { + name: '工具 🛠️', + resources: [ + { + name: '在线工具', + desc: '在线工具,开发人员工具,代码格式化、压缩、加密、解密,下载链接转换,ico图标制作,字帖生成', + logo: 'https://tool.lu/favicon.ico', + href: 'https://tool.lu/', + tags: ['工具'], + }, + { + name: '菜鸟工具', + desc: '菜鸟工具,为开发设计人员提供在线工具,提供在线PHP、Python、 CSS、JS 调试,中文简繁体转换,进制转换等工具', + logo: '/img/resource/runoob.png', + href: 'https://c.runoob.com/', + tags: ['工具'], + }, + { + name: 'ProcessOn', + desc: '免费在线流程图思维导图', + logo: 'https://processon.com/favicon.ico', + href: 'https://processon.com/', + tags: ['工具', '思维导图'], + }, + { + name: 'Terminal Gif Maker', + desc: '在线生成 Terminal GIF', + logo: 'https://www.terminalgif.com/favicon.ico', + href: 'https://www.terminalgif.com', + tags: [], + }, + + { + name: 'AST Explorer', + desc: '一个 Web 工具,用于探索由各种解析器生成的 AST 语法树', + logo: 'https://astexplorer.net/favicon.png', + href: 'https://astexplorer.net/', + tags: ['工具', '格式转换'], + }, + { + name: 'transform', + desc: '各类数据格式与对象转换', + logo: 'https://transform.tools/static/favicon.png', + href: 'https://transform.tools', + tags: ['工具', '格式转换'], + }, + { + name: 'Hoppscotch', + desc: '开源 API 开发生态系统', + logo: '/img/resource/hoppscotch.png', + href: 'https://hoppscotch.io/', + tags: ['api'], + }, + { + name: 'JsonT.run', + desc: '一个简洁的在线 JSON 解析器', + logo: 'https://www.jsont.run/favicon.ico', + href: 'https://www.jsont.run/', + tags: ['工具'], + }, + { + name: 'Apifox', + desc: 'API 文档、API 调试、API Mock、API 自动化测试', + logo: '/img/resource/apifox.png', + href: 'https://www.apifox.cn/', + tags: ['工具'], + }, + ], + }, + { + name: '代码托管', + resources: [ + { + name: 'GitHub', + desc: '全球最大的软件项目托管平台,发现优质开源项目', + logo: 'https://github.githubassets.com/favicons/favicon.svg', + href: 'https://github.com/', + tags: ['GitHub', '代码托管'], + }, + { + name: 'Gitee', + desc: 'Gitee 是中国领先的基于 Git 的代码托管平台,类似于全球知名的 GitHub', + logo: '/img/resource/gitee.ico', + href: 'https://gitee.com/', + tags: ['代码托管'], + }, + { + name: 'Gitlab', + desc: '更快地交付安全代码,部署到任何云,并推动业务成果', + logo: 'https://gitlab.com/uploads/-/system/group/avatar/6543/logo-extra-whitespace.png?width=64', + href: 'https://gitlab.com/', + tags: ['代码托管'], + }, + { + name: 'Gitea', + desc: 'Gitea 是一个开源社区驱动的轻量级代码托管解决方案,后端采用 Go 编写,采用 MIT 许可证.', + logo: 'https://gitea.io/images/favicon.png', + href: 'https://gitea.io/', + tags: ['代码托管'], + }, + { + name: 'Coding', + desc: '提供一站式研发管理平台及云原生开发工具,让软件研发如同工业生产般简单高效,助力企业提升研发管理效能', + logo: '/img/resource/coding.png', + href: 'https://coding.net/', + tags: ['代码托管'], + }, + ], + }, + { + name: '网站托管', + resources: [ + { + name: 'Vercel', + desc: 'Vercel将最好的开发人员体验与对最终用户性能的执着关注相结合', + logo: 'https://assets.vercel.com/image/upload/q_auto/front/favicon/vercel/57x57.png', + href: 'https://vercel.com', + tags: ['网站托管'], + }, + { + name: 'Netlify', + desc: 'Netlify 是一家提供静态网站托管的云平台,支持从 Github, GitLab, Bitbucket 等代码仓库中自动拉取代码 然后进行项目打包和部署等功能', + logo: '/img/resource/netlify.png', + href: 'https://www.netlify.com', + tags: ['网站托管'], + }, + { + name: 'Coolify', + desc: '一个开源和自我托管的 Heroku/Netlify 替代品', + logo: '/img/resource/coolify.png', + href: 'https://coolify.io', + tags: ['网站托管'], + }, + { + name: 'GitHub Codespace', + desc: '全球最大的软件项目托管平台,发现优质开源项目', + logo: 'https://github.githubassets.com/favicons/favicon.svg', + href: 'https://github.com/codespaces', + tags: ['网站托管'], + }, + { + name: 'Railway', + desc: '带上你的代码,剩下交给我们 ', + logo: '/img/resource/railway.png', + href: 'https://railway.app/', + tags: ['网站托管'], + }, + { + name: 'Supabase', + desc: 'Supabase 是一个开源的后端即服务(BaaS)平台,它可以帮助开发者快速构建应用程序,无需编写后端代码。', + logo: '/img/resource/supabase.png', + href: 'https://supabase.com/', + tags: ['BaaS'], + }, + ], + }, + { + name: '在线代码', + resources: [ + { + name: 'CodesandBox', + desc: 'CodeSandbox是一个在线代码编辑器和原型工具,可以更快地创建和共享web应用程序', + logo: 'https://codesandbox.io/favicon.ico', + href: 'https://codesandbox.io/', + tags: ['在线代码'], + }, + { + name: 'CodePen', + desc: '是构建、测试和发现前端代码的最佳场所', + logo: 'https://cpwebassets.codepen.io/assets/favicon/favicon-aec34940fbc1a6e787974dcd360f2c6b63348d4b1f4e06c77743096d55480f33.ico', + href: 'https://codepen.io/', + tags: ['在线代码'], + }, + { + name: 'Stackblitz', + desc: 'Stackblitz在流程中保持即时的开发体验。没有更多的小时储存/拉/安装本地-只需点击,并开始编码', + logo: '/img/resource/stackblitz.png', + href: 'https://stackblitz.com/', + tags: ['在线代码'], + }, + { + name: 'vscode.dev', + desc: 'vscode官方提供在线Web版vscode代码编写网站', + logo: 'https://vscode.dev/static/stable/favicon.ico', + href: 'https://vscode.dev/', + tags: ['在线代码'], + }, + { + name: 'Sandpack', + desc: '用于创建实时运行的代码编辑经验', + logo: 'https://sandpack.codesandbox.io/favicon.ico', + href: 'https://sandpack.codesandbox.io/', + tags: ['在线代码'], + }, + ], + }, + { + name: 'Vue 生态', + resources: [ + { + name: 'Vue.js', + desc: '渐进式 JavaScript 框架', + logo: 'https://vuejs.org/logo.svg', + href: 'https://vuejs.org', + tags: ['前端', 'Vue', '框架'], + }, + { + name: 'Vue Router', + desc: '为 Vue.js 提供富有表现力、可配置的、方便的路由', + logo: 'https://vuejs.org/logo.svg', + href: 'https://router.vuejs.org', + tags: ['前端', 'Vue'], + }, + { + name: 'Nuxt', + desc: '使用 Nuxt 自信地构建您的下一个 Vue.js 应用程序。使 Web 开发简单而强大。', + logo: '/img/resource/nuxt.svg', + href: 'https://nuxt.com/', + tags: ['前端', 'Vue', '文档', '框架'], + }, + { + name: 'Pinia', + desc: '您将会喜欢使用的 Vue 状态管理', + logo: 'https://pinia.vuejs.org/logo.svg', + href: 'https://pinia.vuejs.org/', + tags: ['前端', 'Vue', '文档', '框架'], + }, + { + name: 'VueUse', + desc: '基本 Vue 合成实用程序的集合', + logo: 'https://vueuse.org/favicon.ico', + href: 'https://vueuse.org/', + tags: ['前端', 'Vue', '文档', '框架'], + }, + { + name: 'Vitest', + desc: '一个 Vite 原生单元测试框架。它很快!', + logo: 'https://vitest.dev/favicon.ico', + href: 'https://cn.vitest.dev/', + tags: ['前端', 'Vue', '框架'], + }, + ], + }, + { + name: 'React 生态', + resources: [ + { + name: 'React', + desc: '用于构建用户界面的 JavaScript 库', + logo: 'https://react.dev/favicon.ico', + href: 'hhttps://react.dev/', + tags: ['前端', 'React'], + }, + { + name: 'Next.js', + desc: 'Next.js 为您提供生产环境所需的所有功能以及最佳的开发体验:包括静态及服务器端融合渲染、 支持 TypeScript、智能化打包、 路由预取等功能 无需任何配置', + logo: 'https://nextjs.org/static/favicon/favicon.ico', + href: 'https://nextjs.org/', + tags: ['前端', 'React', '框架'], + }, + { + name: 'zustand', + desc: '一种小型、快速且可扩展的 Bearbones 状态管理解决方案,使用简化的通量原理。拥有基于钩子的舒适 API,不是样板文件或固执己见。', + logo: '/img/resource/zustand.png', + href: 'https://docs.pmnd.rs/zustand/', + tags: ['前端', 'React'], + }, + { + name: 'react-use', + desc: '一个强大的 React Hooks 库', + logo: 'https://reactjs.org/favicon.ico', + href: 'https://github.com/streamich/react-use', + tags: ['前端', 'React'], + }, + { + name: 'SWR', + desc: '用于数据请求的 React Hooks 库', + logo: '/img/resource/swr.png', + href: 'https://swr.vercel.app/', + tags: ['前端', 'React'], + }, + { + name: 'TanStack Query', + desc: '适用于 TS/JS、React、Solid、Vue 和 Svelte 的强大异步状态管理', + logo: 'https://tanstack.com/favicons/apple-touch-icon.png', + href: 'https://tanstack.com/query/latest/', + tags: ['前端', 'React'], + }, + { + name: 'framer-motion', + desc: 'Framer Motion是一个用于React的开源动画库,提供简单易用的API来创建流畅、高性能的动画效果,使Web应用程序和界面变得更加生动和吸引人。', + logo: 'https://www.framer.com/images/favicons/favicon.png', + href: 'https://www.framer.com/motion', + tags: ['前端', 'React', '动画'], + }, + { + name: 'UmiJS', + desc: '用 Umi 构建你的下一个应用,带给你简单而愉悦的 Web 开发体验', + logo: 'https://img.alicdn.com/tfs/TB1YHEpwUT1gK0jSZFhXXaAtVXa-28-27.svg', + href: 'https://umijs.org', + tags: ['前端', 'React', '脚手架'], + }, + ], + }, + { + name: 'CSS', + resources: [ + { + name: 'TailwindCSS', + desc: 'Tailwind CSS 是一个功能类优先的 CSS 框架,它集成了诸如 flex, pt-4, text-center 和 rotate-90 这样的的类,它们能直接在脚本标记语言中组合起来,构建出任何设计', + logo: 'https://www.tailwindcss.cn/favicon-32x32.png', + href: 'https://www.tailwindcss.cn', + tags: ['Css', '框架'], + }, + { + name: 'WindiCSS', + desc: 'Windi CSS 是下一代工具优先的 CSS 框架', + logo: 'https://windicss.org/assets/logo.svg', + href: 'https://windicss.org', + tags: ['Css', '框架'], + }, + { + name: 'Twind', + desc: '现存最小、最快、功能最齐全的完整 Tailwind-in-JS 解决方案', + logo: '/img/resource/twind.svg', + href: 'https://github.com/tw-in-js/twind', + tags: ['Css', '框架'], + }, + { + name: 'UnoCSS', + desc: '即时按需原子 CSS 引擎', + logo: 'https://uno.antfu.me//favicon.svg', + href: 'https://uno.antfu.me/', + tags: ['Css', '框架'], + }, + { + name: 'Bootstrap', + desc: 'Bootstrap 是全球最受欢迎的前端开源工具库,它支持 Sass 变量和 mixin、响应式栅格系统、自带大量组件和众多强大的 JavaScript 插件。基于 Bootstrap 提供的强大功能,能够让你快速设计并定制你的网站', + logo: 'https://img.kuizuo.cn/20210907055816.png', + href: 'https://v5.bootcss.com/', + tags: ['Css', '框架'], + }, + { + name: 'w3schools Css 教程', + desc: 'w3schools 从基础到高级的CSS教程', + logo: 'https://www.w3schools.com/favicon.ico', + href: 'https://www.w3schools.com/css', + tags: ['Css', '样式'], + }, + { + name: 'CSS-Inspiration', + desc: 'CSS灵感', + logo: '/img/resource/css-inspiration.png', + href: 'https://csscoco.com/inspiration', + tags: ['Css', '样式'], + }, + { + name: 'CSS常用样式', + desc: 'CSS常用样式', + logo: 'https://tse1-mm.cn.bing.net/th?id=OIP-C.EgSPriuEnAtlIWJV8R_E1QHaGs&w=107&h=100&c=8&rs=1&qlt=90&o=6&pid=3.1&rm=2', + href: 'https://github.com/QiShaoXuan/css_tricks', + tags: ['Css', '样式'], + }, + { + name: 'CSSFX', + desc: '一个精心制作的集合设计的重点是流动性,简单性和易用性。使用最小标记的 CSS 支持', + logo: '/img/resource/cssfx.png', + href: 'https://cssfx.netlify.app/', + tags: ['Css', '样式'], + }, + { + name: 'NES.css', + desc: '一个像素风格的CSS框架', + logo: 'https://nostalgic-css.github.io/NES.css/favicon.png', + href: 'https://nostalgic-css.github.io/NES.css/', + tags: ['Css', '框架'], + }, + { + name: 'clay.css', + desc: 'claymorphism 泥陶态风格CSS', + logo: 'https://codeadrian.github.io/clay.css/apple-touch-icon.png', + href: 'https://codeadrian.github.io/clay.css/', + tags: ['Css', '框架'], + }, + { + name: 'loading.io', + desc: 'Animation Made Easy', + logo: '/img/resource/loading.ico', + href: 'https://loading.io/', + tags: ['Css'], + }, + { + name: '神奇UI样式', + desc: '我们赋予任何人创建、分享和使用用 CSS 和 HTML 制作的漂亮自定义元素的权力。', + logo: '/img/resource/uiverse.png', + href: 'https://uiverse.io', + tags: ['Css'], + }, + { + name: 'HYPE4', + desc: '透明玻璃态生成器', + logo: 'https://hype4.academy/_next/static/media/logorwd@2x.b40bc92c.png', + href: 'https://hype4.academy/tools/glassmorphism-generator', + tags: ['Css'], + }, + { + name: 'Omatsuri', + desc: '收集不同的发电机,让您的生活更轻松。', + logo: 'https://omatsuri.app/assets/favicon.ico', + href: 'https://omatsuri.app', + tags: ['Css'], + }, + { + name: 'smooth shadow', + desc: '快速轻松地实现基于 CSS 阴影的绝佳工具。您只需要指定一些阴影设置,代码就在您的路上。', + logo: 'https://shadows.brumm.af/favicon.svg', + href: 'https://shadows.brumm.af/', + tags: ['Css'], + }, + { + name: 'FANCY-BORDER-RADIUS', + desc: '花式边界半径,有助于创建 CSS 花式边框。', + logo: 'https://9elements.github.io/fancy-border-radius/favicon-32x32.png', + href: 'https://9elements.github.io/fancy-border-radius/', + tags: ['Css'], + }, + { + name: 'Coolors', + desc: '创建调色板', + logo: 'img/resource/coolors.png', + href: 'https://coolors.co/', + tags: ['Css'], + }, + ], + }, + { + name: '组件库', + resources: [ + { + name: 'Element Plus', + desc: '基于 Vue 3,面向设计师和开发者的组件库', + logo: 'https://element-plus.gitee.io/images/element-plus-logo-small.svg', + href: 'https://element-plus.gitee.io/', + tags: ['前端', 'Vue', '组件库'], + }, + { + name: 'Naive UI', + desc: '一个 Vue 3 组件库比较完整,主题可调,使用 TypeScript,快 有点意思', + logo: '/img/resource/naiveUI.svg', + href: 'https://www.naiveui.com/', + tags: ['组件库', 'vue'], + }, + { + name: 'Ant Design', + desc: '一套企业级 UI设计语言和 React 组件库', + logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg', + href: 'https://ant.design', + tags: ['前端', 'React', '组件库'], + }, + { + name: 'shadcn/ui', + desc: '设计精美的组件,您可以将其复制并粘贴到您的应用程序中。无障碍。可定制。开源。', + logo: 'https://ui.shadcn.com/favicon.ico', + href: 'https://ui.shadcn.com/', + tags: ['组件库', 'react', 'tailwindcss'], + }, + { + name: 'TDesign', + desc: 'TDesign 是腾讯各业务团队在服务业务过程中沉淀的一套企业级设计体系', + logo: 'https://tdesign.tencent.com/favicon.ico', + href: 'https://tdesign.tencent.com/', + tags: ['组件库', 'react'], + }, + { + name: 'Arco Design', + desc: '字节跳动出品的企业级设计系统', + logo: 'https://unpkg.byted-static.com/latest/byted/arco-config/assets/favicon.ico', + href: 'https://arco.design/', + tags: ['组件库', 'react'], + }, + { + name: 'Vuetify', + desc: 'Vuetify 是一个 Vue UI 库,包含手工制作的精美材料组件。不需要设计技能 - 创建令人惊叹的应用程序所需的一切都触手可及', + logo: 'https://vuetify.cn/favicon.ico', + href: 'https://vuetify.cn/', + tags: ['组件库', 'react'], + }, + { + name: 'MUI', + desc: '当下流行的 React UI 框架', + logo: 'https://mui.com/static/favicon.ico', + href: 'https://mui.com', + tags: ['前端', 'React', '组件库'], + }, + { + name: 'VbenAdmin', + desc: 'Vben是一个基于Vue3、Vite、TypeScript等最新技术栈开发的后台管理框架', + logo: '/img/resource/vben-admin.png', + href: 'https://vvbin.cn/doc-next/', + tags: ['前端', 'Vue', '后台', '项目'], + }, + ], + }, + { + name: 'Frontend', + resources: [ + { + name: 'Component party', + desc: '前端框架开Party🎉,Web 组件 JS 框架通过其语法和特性进行概述', + logo: '/img/resource/component party.svg', + href: 'https://component-party.dev/', + tags: ['前端', 'css', '动画'], + }, + { + name: 'Lodash', + desc: '一个 JavaScript 的实用工具库, 表现一致性, 模块化, 高性能, 以及可扩展', + logo: 'https://lodash.com/icons/favicon-32x32.png', + href: 'https://lodash.net', + tags: ['Nodejs'], + }, + { + name: 'WebAssembly', + desc: 'wasm 是一个可移植、体积小、加载快并且兼容 Web 的全新格式', + logo: 'https://www.wasm.com.cn/favicon.ico', + href: 'https://www.wasm.com.cn', + tags: ['Nodejs'], + }, + { + name: 'Greensock', + desc: '超强大h5动画库', + logo: 'https://greensock.com/favicon.ico', + href: 'https://greensock.com/docs/', + tags: ['前端', 'css', '动画'], + }, + { + name: 'Threejs', + desc: '强大的3D-Js库', + logo: 'https://threejs.org/favicon.ico', + href: 'https://threejs.org/', + tags: ['前端', 'JavaScript', '3D'], + }, + { + name: 'Jest', + desc: 'Jest 是一个令人愉快的 JavaScript 测试框架,注重简单性。', + logo: '/img/resource/jest.png', + href: 'https://jestjs.io/', + tags: ['自动化测试'], + }, + { + name: 'Cypress', + desc: '对任何在浏览器中运行的东西进行快速、简单和可靠的测试。', + logo: '/img/resource/cypress.png', + href: 'https://www.cypress.io/', + tags: ['自动化测试'], + }, + { + name: 'Playwright', + desc: 'Playwright 为现代网络应用程序提供了可靠的端到端测试。', + logo: '/img/resource/playwright.svg', + href: 'https://playwright.dev/', + tags: ['自动化测试'], + }, + ], + }, + { + name: 'Node/Deno', + resources: [ + { + name: 'Node', + desc: 'Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时', + logo: 'https://nodejs.org/static/images/favicons/apple-touch-icon.png', + href: 'http://nodejs.cn/', + tags: ['后端', 'Nodejs', '文档'], + }, + { + name: 'Deno', + desc: '一个现代的JavaScript和TypeScript运行时', + logo: 'https://deno.land/logo.svg', + href: 'https://bun.sh/', + tags: ['Nodejs', 'Deno', 'JavaScript', 'TypeScript'], + }, + { + name: 'Bun', + desc: 'Bun 是一个快速的一体化 JavaScript 运行时', + logo: '/img/resource/bun.svg', + href: 'https://bun.sh', + tags: ['Nodejs', 'Deno', 'JavaScript', 'TypeScript'], + }, + { + name: 'NPM', + desc: 'NPM是世界上最大的包管理器', + logo: 'https://static.npmjs.com/58a19602036db1daee0d7863c94673a4.png', + href: 'https://www.npmjs.com', + tags: ['Nodejs', '包管理', '文档'], + }, + { + name: 'Yarn', + desc: 'Yarn 是一个软件包管理器,还可以作为项目管理工具。无论你是小型项目还是大型单体仓库(monorepos),无论是业余爱好者还是企业用户,Yarn 都能满足你的需求', + logo: 'https://www.yarnpkg.cn/favicon-32x32.png', + href: 'https://www.yarnpkg.cn', + tags: ['Nodejs', '包管理', '文档'], + }, + { + name: 'Pnpm', + desc: '速度快、节省磁盘空间的软件包管理器', + logo: 'https://www.pnpm.cn/img/favicon.png', + href: 'https://pnpm.io', + tags: ['Nodejs', '包管理', '文档'], + }, + { + name: 'Node.js技术栈', + desc: '“Nodejs技术栈” 是作者 @五月君 从事 Node.js 开发以来的学习历程,希望这些分享能帮助到正在学习、使用 Node.js 的朋友们', + logo: 'https://nodejsred.oss-cn-shanghai.aliyuncs.com/nodejs_roadmap-logo.jpeg?x-oss-process=style/may', + href: 'https://www.nodejs.red/', + tags: ['Nodejs', '笔记', '教程'], + }, + { + name: 'Axios', + desc: 'Axios 是一个基于 promise 的网络请求库,可以用于浏览器和 node.js', + logo: '/img/resource/axios.ico', + href: 'https://axios-http.cn/', + tags: ['Nodejs', 'HTTP'], + }, + { + name: 'Expressjs', + desc: '基于 Node.js 平台,快速、开放、极简的 Web 开发框架', + logo: 'https://www.expressjs.com.cn/images/favicon.png', + href: 'https://www.expressjs.com.cn/', + tags: ['Nodejs', '后端', '框架'], + }, + { + name: 'Nest.js', + desc: '用于构建高效且可伸缩的服务端应用程序的渐进式 Node.js 框架', + logo: 'https://docs.nestjs.cn/_media/icon.svg', + href: 'https://docs.nestjs.cn/', + tags: ['后端', 'Nodejs', '框架'], + }, + { + name: 'Fresh', + desc: 'Deno 下一代 Web 框架,专注于速度、可靠性和简单性的构建。', + logo: '/img/resource/fresh.ico', + href: 'https://fresh.deno.dev/', + tags: ['Nodejs'], + }, + { + name: 'Socket.io', + desc: 'Socket.IO 是一个可以在浏览器与服务器之间实现实时、双向、基于事件的通信的工具库', + logo: 'https://socket.io/images/favicon.png', + href: 'https://socketio.bootcss.com', + tags: ['Nodejs', 'socket'], + }, + { + name: 'tRPC', + desc: 'tRPC 是一个轻量级的、类型安全的远程过程调用框架,它使用 TypeScript 进行开发,可以帮助开发者轻松地编写和部署高性能的分布式应用程序。', + logo: 'https://trpc.io/img/logo.svg', + href: 'https://trpc.io/', + tags: ['Nodejs'], + }, + { + name: 'Strapi', + desc: 'Socket.IO 是一个可以在浏览器与服务器之间实现实时、双向、基于事件的通信的工具库', + logo: '/img/resource/strapi.png', + href: 'https://strapi.io/', + tags: ['Nodejs', 'CMS'], + }, + { + name: 'TypeORM', + desc: 'TypeORM 是一个 ORM 框架,它可以运行在 NodeJS、Browser、Cordova、PhoneGap、Ionic、React Native、Expo 和 Electron 平台上,可以与 TypeScript 和 JavaScript (ES5,ES6,ES7,ES8)一起使用', + logo: '/img/resource/typeorm.ico', + href: 'https://typeorm.bootcss.com', + tags: ['Nodejs', 'ORM'], + }, + { + name: 'Prisma', + desc: 'Prisma 下一代 Node.js 和 TypeScript 的ORM框架', + logo: '/img/resource/prisma.png', + href: 'https://prisma.io/', + tags: ['Nodejs', 'ORM'], + }, + { + name: 'GraphQL', + desc: 'GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时', + logo: '/img/resource/graphQL.svg', + href: 'https://graphql.cn', + tags: ['Nodejs', 'GraphQL'], + }, + { + name: 'ECharts', + desc: '一个基于 JavaScript 的开源可视化图表库', + logo: 'https://echarts.apache.org/zh/images/favicon.png', + href: 'https://echarts.apache.org/', + tags: ['图表', '可视化'], + }, + { + name: 'AntV', + desc: '蚂蚁集团全新一代数据可视化解决方案,让数据栩栩如生', + logo: '/img/resource/antv.png', + href: 'https://antv.vision/', + tags: ['图表', '可视化'], + }, + ], + }, + { + name: '构建工具', + resources: [ + { + name: 'Webpack', + desc: 'webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle', + logo: '/img/resource/webpack.png', + href: 'https://www.webpackjs.com', + tags: ['构建工具'], + }, + { + name: 'Rollup.js', + desc: 'Rollup 是 JavaScript 的模块打包器,它将小段代码编译成更大、更复杂的代码,例如库或应用程序', + logo: 'https://rollupjs.org/favicon.png', + href: 'https://rollupjs.org', + tags: ['构建工具'], + }, + { + name: 'Vite', + desc: '下一代的前端工具链,为开发提供极速响应', + logo: '/img/resource/vite.svg', + href: 'https://cn.vitejs.dev', + tags: ['构建工具'], + }, + { + name: 'Turborepo', + desc: 'Turborepo 是一个用于 JavaScript 和 TypeScript 代码库的高性能构建系统。', + logo: '/img/resource/turborepo.svg', + href: 'https://turbo.build/repo', + tags: ['构建工具'], + }, + { + name: 'Turbopack', + desc: 'Turbopack 是一个用 Rust 编写的针对 JavaScript 和 TypeScript 优化的增量式捆绑包。', + logo: '/img/resource/turbopack.svg', + href: 'https://turbo.build/pack', + tags: ['构建工具'], + }, + { + name: 'SWC', + desc: 'SWC 是下一代快速开发工具的可扩展的基于 Rust 的平台。', + logo: '/img/resource/swc.png', + href: 'https://swc.rs/', + tags: ['构建工具'], + }, + ], + }, + { + name: '设计', + resources: [ + { + name: 'Mastergo', + desc: '面向团队的专业 UI/UX 设计工具,多人同时编辑、随时在线评审、设计一键交付,让想法更快实现', + logo: 'https://mastergo.com/favicon.ico', + href: 'https://mastergo.com/', + tags: ['设计'], + }, + { + name: '即时设计', + desc: '可云端编辑的专业级 UI 设计工具,为中国设计师量身打造,Windows 也能用的「协作版 Sketch」', + logo: 'https://img.js.design/assets/webImg/favicon.ico', + href: 'https://js.design/', + tags: ['设计'], + }, + { + name: 'Figma', + desc: 'Figma 是为 UI 设计而生的设计工具,除了有和 Sketch 一样基本的操作和功能,还有许多专为 UI 设计而生的强大功能。', + logo: '/img/resource/figma.png', + href: 'https://www.figma.com/', + tags: ['设计'], + }, + { + name: 'Pixso', + desc: '一站式完成原型、设计、交互与交付,为数字化团队协作提效', + logo: 'https://cms.pixso.cn/images/logo.svg', + href: 'https://pixso.cn/', + tags: ['设计'], + }, + ], + }, + { + name: '字体图标', + resources: [ + { + name: 'iconify', + desc: '数千个图标,一个统一的框架', + logo: 'https://icon-sets.iconify.design/favicon.ico', + href: 'https://icon-sets.iconify.design/', + tags: ['图标'], + }, + { + name: 'icones', + desc: 'Icon Explorer with Instant searching, powered by Iconify', + logo: 'https://icones.js.org/favicon.svg', + href: 'https://icones.js.org/', + tags: ['图标'], + }, + { + name: 'iconfont', + desc: 'iconfont-国内功能很强大且图标内容很丰富的矢量图标库,提供矢量图标下载、在线存储、格式转换等功能', + logo: 'https://img.alicdn.com/imgextra/i4/O1CN01EYTRnJ297D6vehehJ_!!6000000008020-55-tps-64-64.svg', + href: 'https://www.iconfont.cn/', + tags: ['图标'], + }, + { + name: 'feathericons', + desc: '简单美丽的开源图标', + logo: 'https://feathericons.com/favicon.ico', + href: 'https://feathericons.com/', + tags: ['图标'], + }, + { + name: 'undraw', + desc: '一个不断更新的设计项目与美丽的SVG图像,使用完全免费', + logo: 'https://undraw.co/apple-touch-icon.png', + href: 'https://undraw.co/', + tags: ['插画', 'svg'], + }, + { + name: 'igoutu', + desc: '图标、插图、照片、音乐和设计工具', + logo: '/img/resource/igoutu.png', + href: 'https://igoutu.cn/', + tags: ['插画', 'svg'], + }, + { + name: 'Emojiall', + desc: 'Emoji表情大全', + logo: 'https://www.emojiall.com/apple-touch-icon.png', + href: 'https://www.emojiall.com/zh-hans', + tags: ['图标', 'emoji'], + }, + { + name: '渐变色网站', + desc: '数百万个自动生成的渐变的网站', + logo: 'https://gradihunt.com/favicon.ico', + href: 'https://gradihunt.com/', + tags: ['配色', '背景'], + }, + { + name: '谷歌字体', + desc: '一个生成渐变色背景的网站', + logo: '/img/resource/google_fonts.ico', + href: 'https://googlefonts.cn/', + tags: ['字体'], + }, + { + name: 'Typing SVG', + desc: '一个动态生成的可自定义 SVG 打字效果', + logo: '/img/resource/typing-svg.png', + href: 'https://readme-typing-svg.herokuapp.com/demo/', + tags: ['字体'], + }, + ], + }, + { + name: '跨平台', + resources: [ + { + name: 'Electron', + desc: '使用 JavaScript,HTML 和 CSS 构建跨平台的桌面应用程序', + logo: '/img/resource/electron.ico', + href: 'https://www.electronjs.org/', + tags: ['跨平台', 'Nodejs'], + }, + { + name: 'Tauri', + desc: 'Tauri是一个框架,用于为所有主要桌面平台构建小巧、快速的二进制文件', + logo: 'https://tauri.app/meta/favicon-96x96.png', + href: 'https://tauri.app/', + tags: ['跨平台', 'Rust'], + }, + { + name: 'Flutter', + desc: 'Flutter 是 Google 开源的应用开发框架,仅通过一套代码库,就能构建精美的、原生平台编译的多平台应用', + logo: 'https://flutter.cn/assets/images/cn/flutter-icon.png', + href: 'https://flutter.cn/', + tags: ['跨平台', 'Rust'], + }, + { + name: 'Uni-app', + desc: 'uni-app 是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/百度/头条/QQ/快手/钉钉/淘宝)、快应用等多个平台', + logo: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-a90b5f95-90ba-4d30-a6a7-cd4d057327db/d23e842c-58fc-4574-998d-17fdc7811cc3.png', + href: 'https://uniapp.dcloud.io/', + tags: ['Vue', '小程序'], + }, + { + name: 'Taro', + desc: 'Taro 是一个开放式跨端跨框架解决方案,支持使用 React/Vue/Nerv 等框架来开发 微信 / 京东 / 百度 / 支付宝 / 字节跳动 / QQ / 飞书 小程序 / H5 / RN 等应用', + logo: '/img/resource/taro.png', + href: 'https://taro.jd.com', + tags: ['前端', 'React', '小程序'], + }, + ], + }, + { + name: '站点生成', + resources: [ + { + name: 'VitePress', + desc: 'Vue 驱动并使用Vite构建的静态网站生成器', + logo: 'https://vuepress.vuejs.org/hero.png', + href: 'https://vitepress.vuejs.org', + tags: ['前端', 'Vue', '静态站点'], + }, + { + name: 'VuePress', + desc: 'Vue 驱动的静态网站生成器', + logo: 'https://vuepress.vuejs.org/hero.png', + href: 'https://vuepress.vuejs.org', + tags: ['前端', 'Vue', '静态站点'], + }, + { + name: 'Docusaurus', + desc: '快速构建以内容为核心的最佳网站', + logo: '/img/resource/docusaurus.svg', + href: 'https://docusaurus.io', + tags: ['前端', 'React', '静态站点'], + }, + { + name: 'Hexo', + desc: '快速、简洁且高效的博客框架', + logo: 'https://hexo.io/favicon.ico', + href: 'https://hexo.io', + tags: ['前端', '静态站点'], + }, + { + name: 'GitBook', + desc: 'GitBook帮助您为用户发布漂亮的文档,并集中您的团队的知识进行高级协作', + logo: 'https://assets-global.website-files.com/600ead1452cf056d0e52dbed/6246d2036225eac4d74cff27_Favicon_Blue.png', + href: 'https://www.gitbook.com/', + tags: ['前端', '静态站点'], + }, + { + name: 'Docsify', + desc: 'docsify 可以快速帮你生成文档网站', + logo: 'https://docsify.js.org/_media/icon.svg', + href: 'https://docsify.js.org', + tags: ['前端', '静态站点'], + }, + { + name: 'WordPress', + desc: 'WordPress是一款能让您建立出色网站、博客或应用程序的开源软件', + logo: 'https://s.w.org/images/wmark.png', + href: 'https://cn.wordpress.org/', + tags: ['前端', '站点'], + }, + ], + }, + { + name: 'Github', + resources: [ + { + name: 'Gitstar Ranking', + desc: '针对用户、组织和存储库的非官方 GitHub 星级排名', + logo: '/img/resource/github.ico', + href: 'https://gitstar-ranking.com/', + tags: [], + }, + { + name: 'Metrics', + desc: 'Create your own metrics', + logo: '/img/resource/github.ico', + href: 'https://metrics.lecoq.io/', + tags: [], + }, + { + name: 'Github主页 README 生成器', + desc: '一个Github 个人主页 README 生成器', + logo: '/img/resource/github.ico', + href: 'https://rahuldkjain.github.io/gh-profile-readme-generator/', + tags: [], + }, + { + name: 'Github 统计生成器', + desc: 'Github 在你的 README 中获取动态生成的 GitHub 统计信息!', + logo: '/img/resource/github.ico', + href: 'https://github.com/anuraghazra/github-readme-stats', + tags: [], + }, + ], + }, +] diff --git a/data/skills.tsx b/data/skills.tsx new file mode 100644 index 0000000..d08a501 --- /dev/null +++ b/data/skills.tsx @@ -0,0 +1,63 @@ +import { IconProps } from '@iconify/react' + +const SKILLS: IconProps[] = [ + { + icon: 'logos:vue', + style: { left: '1%', top: '1%' }, + }, + { + icon: 'logos:nuxt-icon', + style: { left: '4%', top: '5%' }, + }, + + { + icon: 'logos:react', + style: { right: '2%', top: '11%' }, + }, + { + icon: 'logos:nextjs-icon', + style: { right: '8%', top: '14%' }, + }, + + { + icon: 'logos:javascript', + style: { top: '5%', left: '54%' }, + }, + { + icon: 'logos:typescript-icon', + style: { top: '9%', left: '60%' }, + }, + + { + icon: 'logos:nodejs-icon-alt', + style: { top: '14%', left: '30%' }, + }, + { + icon: 'logos:nestjs', + style: { top: '19%', left: '38%' }, + }, + { + icon: 'logos:prisma', + style: { top: '24%', left: '50%' }, + }, + { + icon: 'logos:postgresql', + style: { top: '26%', left: '60%' }, + }, + + { + icon: 'logos:tailwindcss-icon', + style: { top: '30%', left: '90%' }, + }, + + { + icon: 'logos:visual-studio-code', + style: { bottom: '25%', right: '5%' }, + }, + { + icon: 'logos:docusaurus', + style: { bottom: '1%', left: '1%' }, + }, +] + +export default SKILLS diff --git a/data/social.ts b/data/social.ts new file mode 100644 index 0000000..35d4aa2 --- /dev/null +++ b/data/social.ts @@ -0,0 +1,97 @@ +export type Social = { + github?: string + twitter?: string + juejin?: string + csdn?: string + qq?: string + wx?: string + cloudmusic?: string + zhihu?: string + email?: string + discord?: string +} + +type SocialValue = { + href?: string + title: string + icon: string + color: string +} + +const social: Social = { + github: 'https://github.com/jiatianzhi', + twitter: 'https://twitter.com/kuizuo', + juejin: 'https://juejin.cn/user/1565318510545901', + csdn: 'https://blog.csdn.net/kuizuo12', + qq: 'https://img.kuizuo.cn/qq.png', + wx: 'https://img.kuizuo.cn/wechat.png', + zhihu: 'https://www.zhihu.com/people/jiatianzhi', + // cloudmusic: 'https://music.163.com/#/user/home?id=1333010742', + email: 'mailto:jiatianzhi@bjtu.edu.cn', + discord: 'https://discord.gg/M8cVcjDxkz', +} + +const socialSet: Record = { + github: { + href: social.github, + title: 'GitHub', + icon: 'ri:github-line', + color: '#010409', + }, + zhihu: { + href: social.zhihu, + title: '知乎', + icon: 'ri:zhihu-line', + color: '#1772F6', + }, + juejin: { + href: social.juejin, + title: '掘金', + icon: 'simple-icons:juejin', + color: '#1E81FF', + }, + twitter: { + href: social.twitter, + title: 'Twitter', + icon: 'ri:twitter-line', + color: '#1da1f2', + }, + discord: { + href: social.discord, + title: 'Discord', + icon: 'ri:discord-line', + color: '#5A65F6', + }, + qq: { + href: social.qq, + title: 'QQ', + icon: 'ri:qq-line', + color: '#1296db', + }, + wx: { + href: social.wx, + title: '微信', + icon: 'ri:wechat-2-line', + color: '#07c160', + }, + email: { + href: social.email, + title: '邮箱', + icon: 'ri:mail-line', + color: '#D44638', + }, + cloudmusic: { + href: social.cloudmusic, + title: '网易云', + icon: 'ri:netease-cloud-music-line', + color: '#C20C0C', + }, + rss: { + href: '/blog/rss.xml', + title: 'RSS', + icon: 'ri:rss-line', + color: '#FFA501', + }, +} + +export default socialSet \ No newline at end of file diff --git a/docs/skill/algorithm/0.introduction.mdx b/docs/skill/algorithm/0.introduction.mdx new file mode 100644 index 0000000..4f8db92 --- /dev/null +++ b/docs/skill/algorithm/0.introduction.mdx @@ -0,0 +1,15 @@ +--- +id: algorithm-introduction +slug: /algorithm +title: 简介 +authors: kuizuo +keywords: ['algorithm-introduction'] +--- + +整理常用算法, 大部分题型来源于 [面试经典 150 题](https://leetcode.cn/studyplan/top-interview-150/) + +```mdx-code-block +import DocCardList from '@theme/DocCardList'; + + +``` diff --git "a/docs/skill/algorithm/1.\344\270\244\346\225\260\344\271\213\345\222\214.md" "b/docs/skill/algorithm/1.\344\270\244\346\225\260\344\271\213\345\222\214.md" new file mode 100644 index 0000000..901bc94 --- /dev/null +++ "b/docs/skill/algorithm/1.\344\270\244\346\225\260\344\271\213\345\222\214.md" @@ -0,0 +1,39 @@ +--- +id: two-sum +slug: /algorithm/two-sum +title: 两数之和 +authors: kuizuo +tags: [algorithm] +keywords: [algorithm] +--- + +## 暴力枚举 + +```js +var twoSum = function (nums, target) { + const n = nums.length + + for (let i = 0; i < n; i++) { + for (let j = 0; j < n; j++) { + if (nums[i] + nums[j] === target && i !== j) { + return [i, j] + } + } + } +} +``` + +## 哈希表 + +```js +var twoSum = function (nums, target) { + const map = new Map() + + for (let i = 0; i < nums.length; i++) { + if (map.has(target - nums[i])) { + return [map.get(target - nums[i]), i] + } + map.set(nums[i], i) + } +} +``` diff --git "a/docs/skill/algorithm/2.\344\270\211\346\225\260\344\271\213\345\222\214.md" "b/docs/skill/algorithm/2.\344\270\211\346\225\260\344\271\213\345\222\214.md" new file mode 100644 index 0000000..d567fca --- /dev/null +++ "b/docs/skill/algorithm/2.\344\270\211\346\225\260\344\271\213\345\222\214.md" @@ -0,0 +1,59 @@ +--- +id: three-sum +slug: /algorithm/three-sum +title: 三数之和 +authors: kuizuo +tags: [algorithm] +keywords: [algorithm] +--- + +## 排序+双指针 + +```js +/** + * @param {number[]} nums + * @return {number[][]} + */ +var threeSum = function (nums) { + nums.sort((a, b) => a - b) + + const ans = [] + const n = nums.length + + for (let i = 0; i < n - 2; i++) { + // 当前元素不等于上一个元素 + if (i > 0 && nums[i] == nums[i - 1]) continue + + // 优化 + if (nums[i] + nums[i + 1] + nums[i + 2] > 0) break + if (nums[i] + nums[n - 1] + nums[n - 2] < 0) continue + + let j = i + 1 + let k = n - 1 + + while (j < k) { + const sum = nums[i] + nums[j] + nums[k] + + if (sum > 0) { + k-- + } else if (sum < 0) { + j++ + } else { + ans.push([nums[i], nums[j], nums[k]]) + + j++ + while (j < k && nums[j] === nums[j - 1]) { + j++ + } + + k-- + while (j < k && nums[k] === nums[k + 1]) { + k-- + } + } + } + } + + return ans +} +``` diff --git "a/docs/skill/algorithm/3.\346\273\221\345\212\250\347\252\227\345\217\243.md" "b/docs/skill/algorithm/3.\346\273\221\345\212\250\347\252\227\345\217\243.md" new file mode 100644 index 0000000..bf65bdb --- /dev/null +++ "b/docs/skill/algorithm/3.\346\273\221\345\212\250\347\252\227\345\217\243.md" @@ -0,0 +1,231 @@ +--- +id: sliding-window +slug: /algorithm/sliding-window +title: 滑动窗口 +authors: kuizuo +tags: [algorithm, sliding-window] +keywords: [algorithm, sliding-window] +--- + +## [无重复字符的最长子串](https://leetcode.cn/problems/longest-substring-without-repeating-characters/) + +> 给定一个字符串 `s` ,请你找出其中不含有重复字符的 **最长子串** 的长度。 +> +> **示例 1:** +> +> ``` +> 输入: s = "abcabcbb" +> 输出: 3 +> 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 +> ``` +> +> **示例 2:** +> +> ``` +> 输入: s = "bbbbb" +> 输出: 1 +> 解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。 +> ``` +> +> **示例 3:** +> +> ``` +> 输入: s = "pwwkew" +> 输出: 3 +> 解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。 +> 请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。 +> ``` + +```js +var lengthOfLongestSubstring = function (s) { + const counter = {} + let ans = 0 + let left = 0 + + for (let right in s) { + counter[s[right]] = (counter[s[right]] || 0) + 1 + + while (counter[s[right]] > 1) { + counter[s[left]] -= 1 + left++ + } + + ans = Math.max(ans, right - left + 1) + } + + return ans +} +``` + +时间复杂度:O(N),其中 N 是字符串的长度。左指针和右指针分别会遍历整个字符串一次。 + +空间复杂度:O(∣Σ∣),其中 Σ 表示字符集(即字符串中可以出现的字符)。 + +## [长度最小的子数组](https://leetcode.cn/problems/minimum-size-subarray-sum) + +> 给定一个含有 `n` 个正整数的数组和一个正整数 `target` **。** +> +> 找出该数组中满足其总和大于等于 `target` 的长度最小的 **连续子数组** `[numsl, numsl+1, ..., numsr-1, numsr]` ,并返回其长度**。**如果不存在符合条件的子数组,返回 `0` 。 +> +> **示例 1:** +> +> ``` +> 输入:target = 7, nums = [2,3,1,2,4,3] +> 输出:2 +> 解释:子数组 [4,3] 是该条件下的长度最小的子数组。 +> ``` +> +> **示例 2:** +> +> ``` +> 输入:target = 4, nums = [1,4,4] +> 输出:1 +> ``` +> +> **示例 3:** +> +> ``` +> 输入:target = 11, nums = [1,1,1,1,1,1,1,1] +> 输出:0 +> ``` + +```js +var minSubArrayLen = function (target, nums) { + let ans = Infinity + let sum = 0 + let left = 0 + + for (let right in nums) { + sum += nums[right] + + while (sum >= target) { + ans = Math.min(ans, right - left + 1) + sum -= nums[left] + left++ + } + } + + return ans <= nums.length ? ans : 0 +} +``` + +## [乘积小于 K 的子数组](https://leetcode.cn/problems/subarray-product-less-than-k) + +> 给你一个整数数组 `nums` 和一个整数 `k` ,请你返回子数组内所有元素的乘积严格小于 `k` 的连续子数组的数目。 +> +> **示例 1:** +> +> ``` +> 输入:nums = [10,5,2,6], k = 100 +> 输出:8 +> 解释:8 个乘积小于 100 的子数组分别为:[10]、[5]、[2],、[6]、[10,5]、[5,2]、[2,6]、[5,2,6]。 +> 需要注意的是 [10,5,2] 并不是乘积小于 100 的子数组。 +> ``` +> +> **示例 2:** +> +> ``` +> 输入:nums = [1,2,3], k = 0 +> 输出:0 +> ``` + +```js +var numSubarrayProductLessThanK = function (nums, k) { + if (k <= 1) return 0 + + let left = 0 + let count = 0 + let product = 1 + + for (let right = 0; right < nums.length; right++) { + product *= nums[right] + + while (product >= k) { + product /= nums[left] + left++ + } + + count += right - left + 1 + } + + return count +}; +``` + +## [最小覆盖子串](https://leetcode.cn/problems/minimum-window-substring) + +> 给你一个字符串 `s` 、一个字符串 `t` 。返回 `s` 中涵盖 `t` 所有字符的最小子串。如果 `s` 中不存在涵盖 `t` 所有字符的子串,则返回空字符串 `""` 。 +> +> **注意:** +> +> - 对于 `t` 中重复字符,我们寻找的子字符串中该字符数量必须不少于 `t` 中该字符数量。 +> - 如果 `s` 中存在这样的子串,我们保证它是唯一的答案。 +> +> **示例 1:** +> +> ``` +> 输入:s = "ADOBECODEBANC", t = "ABC" +> 输出:"BANC" +> 解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。 +> ``` +> +> **示例 2:** +> +> ``` +> 输入:s = "a", t = "a" +> 输出:"a" +> 解释:整个字符串 s 是最小覆盖子串。 +> ``` +> +> **示例 3:** +> +> ``` +> 输入: s = "a", t = "aa" +> 输出: "" +> 解释: t 中两个字符 'a' 均应包含在 s 的子串中, +> 因此没有符合条件的子字符串,返回空字符串。 +> ``` + +```js +function minWindow(s, t) { + const charCount = new Map() + let left = 0, + minLen = Infinity, + minWindowStart = 0, + charsToMatch = t.length + + // 初始化字符计数 + for (const char of t) { + charCount.set(char, (charCount.get(char) || 0) + 1) + } + + for (let right = 0; right < s.length; right++) { + const rightChar = s[right] + + if (charCount.has(rightChar) && charCount.get(rightChar) > 0) { + charsToMatch-- + } + + charCount.set(rightChar, (charCount.get(rightChar) || 0) - 1) + + // 缩小左窗口 + while (charsToMatch === 0) { + if (right - left + 1 < minLen) { + minLen = right - left + 1 + minWindowStart = left + } + + const leftChar = s[left] + charCount.set(leftChar, (charCount.get(leftChar) || 0) + 1) + + if (charCount.get(leftChar) > 0) { + charsToMatch++ + } + + left++ + } + } + + return minLen === Infinity ? '' : s.substr(minWindowStart, minLen) +} +``` diff --git "a/docs/skill/algorithm/4.\345\217\214\346\214\207\351\222\210.md" "b/docs/skill/algorithm/4.\345\217\214\346\214\207\351\222\210.md" new file mode 100644 index 0000000..bda5f6d --- /dev/null +++ "b/docs/skill/algorithm/4.\345\217\214\346\214\207\351\222\210.md" @@ -0,0 +1,197 @@ +--- +id: double-pointer +slug: /algorithm/double-pointer +title: 双指针 +authors: kuizuo +tags: [algorithm, double-pointer] +keywords: [algorithm, double-pointer] +--- + +以下是有关双指针相关算法的题目与题解。 + +## [移除元素](https://leetcode.cn/problems/remove-element) + +> 给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。 +> +> 不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。 +> +> 元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。 + +### 我的解法 + +```js +var removeElement = function (nums, val) { + for (let i = 0; i < nums.length; i++) { + if (nums[i] === val) { + nums.splice(i, 1) + i-- + } + } + + return nums.length +} +``` + +然而 splice 操作时间复杂度为 O(n),并且**每次删除一个元素都会导致数组的重新排序**。因此在算法解题中应尽量避免使用数组自带方法操作 + +### 方法: 双指针 + +```js +var removeElement = function (nums, val) { + let left = 0, + right = nums.length + while (left < right) { + if (nums[left] === val) { + nums[left] = nums[right - 1] + right-- + } else { + left++ + } + } + return left +} +``` + +如果题目有要求排序的话,不如将符合条件(`nums[i] !== val`)的元素移到数组头部,这样就不需要排序了。 + +```js +var removeElement = function (nums, val) { + const n = nums.length + let left = 0 + + for (let right = 0; right < n; right++) { + if (nums[right] !== val) { + nums[left] = nums[right] + left++ + } + } + + return left +} +``` + +## [删除有序数组中的重复项](https://leetcode.cn/problems/remove-duplicates-from-sorted-array) + +> 给你一个 升序排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。 +> +> 考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过: +> +> 更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。返回 k 。 + +### 我的解法 + +```js +var removeDuplicates = function (nums) { + const counter = {} + let count = 0 + for (let i = 0; i < nums.length; i++) { + counter[nums[i]] = (counter[nums[i]] || 0) + 1 + + if (counter[nums[i]] > 2) { + continue + } else { + nums[count] = nums[i] + count++ + } + } + + return count +} +``` + +思路很清晰,就是用一个计数对象来计数,然后遍历数组,如果计数大于 1,就跳过,否则就赋值。不过我这里引入了 counter 对象,因此空间复杂度为 O(k),其中 k 表示不同元素的数量。 + +如果以我这个思路去解决 [删除有序数组中的重复项 II](https://leetcode.cn/problems/remove-duplicates-from-sorted-array-ii) 解决题目就会特别容易,只需要将 > 1 换成 1 即可。不过题目要求 不使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。 + +并且通常哈希计数针对无序而言,而题目所给定的数据又有序,因此想要解得好就需要采用其他方案。 + +### 方法: 双指针 + +由于题目声明输入 **升序排列** 的数组,因此采用快慢双指针来判断后一个元素是否等于前一个,如果不相等则说明是不同的元素,慢指针(不同数字次数)加一,快指针继续遍历。 + +```js +var removeDuplicates = function (nums) { + const n = nums.length + + let fast = 1, + slow = 1 + while (fast < n) { + if (nums[fast] !== nums[fast - 1]) { + nums[slow] = nums[fast] + ++slow + } + ++fast + } + return slow +} +``` + +## [判断子序列](https://leetcode.cn/problems/is-subsequence) + +### 我的解法(失败) + +既然判断子序列,那我就把无关元素删除,然后判断最后元素是否包含即可。 + +```js +var isSubsequence = function (s, t) { + t = t.replace(new RegExp(`[^${s}]`, 'g'), '') + return t.includes(s) +} +``` + +然而假设输入数据为 + +``` +s = "leetcode" +t = "yyyyyleeeytcode" +``` + +处理后数据为 + +``` +s = "leetcode" +t = "leeetcode" +``` + +很显然这里 includes 无法判断子序列,因此这种思路不可行。 + +### 方法: 双指针 + +```js +var isSubsequence = function (s, t) { + let i = 0, + j = 0 + while (i < s.length && j < t.length) { + if (s[i] === t[j]) { + i++ + } + j++ + } + + return i === s.length +} +``` + +## [两数之和 II - 输入有序数组](https://leetcode.cn/problems/two-sum-ii-input-array-is-sorted) + +### 方法: 双指针 + +```js +function twoSum(nums, target) { + let left = 0 + let right = nums.length - 1 + + while (left < right) { + const sum = nums[left] + nums[right] + if (sum === target) { + return [left, right] + } + + if (sum > target) { + right-- + } else if (sum < target) { + left++ + } + } +} +``` diff --git "a/docs/skill/algorithm/5.\345\223\210\345\270\214\350\241\250.md" "b/docs/skill/algorithm/5.\345\223\210\345\270\214\350\241\250.md" new file mode 100644 index 0000000..b471654 --- /dev/null +++ "b/docs/skill/algorithm/5.\345\223\210\345\270\214\350\241\250.md" @@ -0,0 +1,393 @@ +--- +id: hash-table +slug: /algorithm/hash-table +title: 哈希表 +authors: kuizuo +tags: [algorithm, hash-table] +keywords: [algorithm, hash-table] +--- + +## [快乐数](https://leetcode.cn/problems/happy-number) + +> 编写一个算法来判断一个数 `n` 是不是快乐数。 +> +> **「快乐数」** 定义为: +> +> - 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。 +> - 然后重复这个过程直到这个数变为 1,也可能是 **无限循环** 但始终变不到 1。 +> - 如果这个过程 **结果为** 1,那么这个数就是快乐数。 +> +> 如果 `n` 是 _快乐数_ 就返回 `true` ;不是,则返回 `false` 。 +> +> **示例 1:** +> +> ``` +> 输入:n = 19 +> 输出:true +> 解释: +> 1² + 9² = 82 +> 8² + 2² = 68 +> 6² + 8² = 100 +> 1² + 0² + 02 = 1 +> ``` + +### 我的解法 + +```js +var isHappy = function (n) { + let deep = 0 + while (n !== 1 && deep < 10) { + const digits = n.toString().split('') + + n = 0 + digits.forEach(num => { + n += Math.pow(num, 2) + }) + + deep++ + } + + return n === 1 +} +``` + +这个解法其实很粗糙,通过肯定是能通过的,但有几个很明显的问题。 + +1. 首先在**数位分离**我采用 `n.toString().split('')` ,不过这是借用到了 js 的特性,要是换其他语言肯定不行。 +2. 重复计算,比如 19 第一次计算 1² + 9² = 82,第二次计算 8² + 2² = 68,第三次计算 6² + 8² = 100 ...,这里的 8² 就可以将其结果存起来,避免重复计算。 +3. 因为存在某些数(如 2)会无限循环,所以需要设置一个深度限制 deep,不然会死循环。 + +### 改进 + +在数位分离上可以依次使用 `num % 10` 来得到 个十百...位, 通过下方代码则可以得到所有位上的数字。 + +```js +function getDigist(nums) { + let digits = [] + while (nums > 0) { + digits.unshift(nums % 10) + nums = Math.floor(nums / 10) + } + + return digits +} + +getDigist(1234) +``` + +而要避免重复运算最简单就是使用哈希集合,将计算过的结果存起来。 + +```js +const cache = {} + +digits.forEach(num => { + if (!cache[num]) { + cache[num] = Math.pow(num, 2) + } + + n += cache[num] +}) +``` + +然而现在还有一个问题,就是如何处理 deep 的问题,由于官方题解中对于这部分给出[分析](https://leetcode.cn/problems/happy-number/solutions/224894/kuai-le-shu-by-leetcode-solution/),这里便不赘述了。如果一个数不是快乐数,那么它一定存在一个循环,可以将生成循环链的数字存入哈希集合中来判断是否处于无限循环中。最终代码如下 + +```js +var isHappy = function (n) { + function getDigist(nums) { + let digits = [] + while (nums > 0) { + digits.unshift(nums % 10) + nums = Math.floor(nums / 10) + } + return digits + } + + const cache = {} + const set = new Set() + + while (n !== 1 && !set.has(n)) { + const digits = getDigist(n) + set.add(n) + + n = 0 + digits.forEach(num => { + if (!cache[num]) { + cache[num] = Math.pow(num, 2) + } + + n += cache[num] + }) + } + + return n === 1 +} +``` + +## [最长连续序列](https://leetcode.cn/problems/longest-consecutive-sequence) + +> 给定一个未排序的整数数组 `nums` ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。 +> +> 请你设计并实现时间复杂度为 `O(n)` 的算法解决此问题。 +> +> **示例 1:** +> +> ``` +> 输入:nums = [100,4,200,1,3,2] +> 输出:4 +> 解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。 +> ``` +> +> **示例 2:** +> +> ``` +> 输入:nums = [0,3,7,2,5,8,4,6,0,1] +> 输出:9 +> ``` + +我的解法 + +1. 对数组去重并排序 +2. 判断 `nums[i]` === `nums[i+1] - 1`,为真则缓存当前最长序列的长度 +3. 不符合条件则重新计算当前最长序列的长度,取 maxLen 和 currentLen 最大值作为返回值 + +```js +var longestConsecutive = function (nums) { + if (nums.length === 0) return 0 + + nums = [...new Set(nums)] + nums = nums.sort((a, b) => a - b) + + const n = nums.length + + let maxLen = 0 + let currentLen = 0 + + for (let i = 0; i < n - 1; i++) { + if (nums[i] === nums[i + 1] - 1) { + currentLen++ + } else { + maxLen = Math.max(maxLen, currentLen) + currentLen = 0 + } + } + + maxLen = Math.max(maxLen, currentLen) + 1 + + return maxLen +} +``` + +### 哈希表 + +```js +var longestConsecutive = function (nums) { + let numSet = new Set(nums) + + let maxLen = 0 + + for (let num of numSet) { + if (!numSet.has(num - 1)) { + let currentNum = num + let currentLen = 1 + + while (numSet.has(currentNum + 1)) { + currentNum++ + currentLen++ + } + + maxLen = Math.max(maxLen, currentLen) + } + } + + return maxLen +} +``` + +解释: 只有当一个数是连续序列的第一个数的情况下才会进入内层循环 `if (!numSet.has(num - 1))`,然后在内层循环中匹配连续序列中的数,因此数组中的每个数只会进入内层循环一次。 + +## [单词规律](https://leetcode.cn/problems/word-pattern) + +> 给定一种规律 `pattern` 和一个字符串 `s` ,判断 `s` 是否遵循相同的规律。 +> +> 这里的 **遵循** 指完全匹配,例如, `pattern` 里的每个字母和字符串 `s` 中的每个非空单词之间存在着双向连接的对应规律。 +> +> **示例1:** +> +> ``` +> 输入: pattern = "abba", s = "dog cat cat dog" +> 输出: true +> ``` +> +> **示例 2:** +> +> ``` +> 输入:pattern = "abba", s = "dog cat cat fish" +> 输出: false +> ``` +> +> **示例 3:** +> +> ``` +> 输入: pattern = "aaaa", s = "dog cat cat dog" +> 输出: false +> ``` + +### 我的解法 + +```js +var wordPattern = function (pattern, s) { + // 提取 pattern 的索引 { a: [0,3], b:[1,2] } + const rules = {} + pattern.split('').forEach((p, i) => { + rules[p] ||= [] + rules[p].push(i) + }) + + const words = s.match(/[a-z]+\b/g) + + for (let value of Object.values(rules)) { + const group = new Set() + + for (let v of value) { + if (group.size === 0) { + group.add(words[v]) + } + + if (!group.has(words[v])) { + return false + } + + group.add(words[v]) + } + } + return true +}; +``` + +而对于下列数据会返回 true,显然是不符合条件的。 + +> pattern = "abba" +> +> s = "dog dog dog dog" + +原因就在于 ab 要不同,因此就需要改进验证组 group。 + +于是就想到既然提取 pattern 的索引,不如也提取 words 索引,然后判断两者值 + +```js +var wordPattern = function (pattern, s) { + function convertArray(arr) { + const result = []; + let charMap = {}; + + for (let i = 0; i < arr.length; i++) { + const char = arr[i]; + if (charMap[char] === undefined) { + charMap[char] = [i]; + } else { + charMap[char].push(i); + } + } + + for (const char in charMap) { + result.push(charMap[char]); + } + + return result; + } + + // "abba" => [[0, 3], [1, 2]] + const patternValues = convertArray(pattern) + // "dog cat cat dog" => [[0, 3], [1, 2]] + const wordValues = convertArray(s.match(/[a-z]+\b/g)) + + for (let i in patternValues) { + if (JSON.stringify(patternValues[i]) !== JSON.stringify(wordValues[i])) { + return false + } + } + + return true +}; +``` + +然而我没想到,但有字母为 **constructor** 时,`wordsRules[w]` 就会变成 `wordsRules[constructor]` 相当于下图所示。 + +![image-20230921010005890](https://img.kuizuo.cn/202309210100004.png) + +不过也好解决,因为这里的 key `a,b,dog,cat` 事实上我们都没用到。只需要把`{ a: [0,3], b: [1,2] }` 转成 `[[0,3], [1,2]]` 即可。 + +然而我发现 将 `"abba"` 转成 `[[0,3], [1,2]]` 在不借助 `Object.values()` 情况下是一件很困难的事情。于是既然 `{}[constructor]` 不行,那么我就用 `Map.get()` 来解决便可。贴上 `convertArray` 函数代码 + +```js +function convertArray(arr) { + const result = []; + const charMap = new Map(); + + for (let i = 0; i < arr.length; i++) { + const char = arr[i]; + if (charMap.has(char)) { + charMap.get(char).push(i); + } else { + charMap.set(char, [i]); + } + } + + charMap.forEach((indices) => result.push(indices)); + + return result; + } +``` + +很显然,上述的解法缺陷很多,这里就不一一列举了,直接来看正确答案。 + +### 正确答案 + +```js +var wordPattern = function (pattern, s) { + const word2ch = new Map(); + const ch2word = new Map(); + const words = s.split(' '); + if (pattern.length !== words.length) { + return false; + } + + for (const [i, word] of words.entries()) { + const ch = pattern[i]; + if (word2ch.has(word) && word2ch.get(word) != ch || ch2word.has(ch) && ch2word.get(ch) !== word) { + return false; + } + word2ch.set(word, ch); + ch2word.set(ch, word); + } + return true; +};Ï +``` + +要判断一个集合与另一个集合是否相同的关系叫「双射」,利用 两个哈希集合的 key 与 value 来判断便可。 + + ### [同构字符串](https://leetcode.cn/problems/isomorphic-strings) + +有了上题解题思路,这题就容易许多了。 + +```js +var isIsomorphic = function(s, t) { + const s2t = {}; + const t2s = {}; + + for (let i = 0; i < s.length; ++i) { + const x = s[i], y = t[i]; + if ((s2t[x] && s2t[x] !== y) || (t2s[y] && t2s[y] !== x)) { + return false; + } + s2t[x] = y; + t2s[y] = x; + } + return true; +}; +``` + + + + + diff --git a/docs/skill/code-specification/editorconfig.md b/docs/skill/code-specification/editorconfig.md new file mode 100644 index 0000000..61b2cc2 --- /dev/null +++ b/docs/skill/code-specification/editorconfig.md @@ -0,0 +1,41 @@ +--- +id: editorconfig +slug: /editorconfig +title: editorconfig +authors: kuizuo +keywords: ['code-specification', 'editorconfig'] +--- + +[Editorconfig](https://editorconfig.org/) 有助于跨各种编辑器和 IDE 为处理同一项目的多个开发人员维护一致的编码样式。 + +## 使用 ESLint 做代码 lint,那么为什么还要使用 .editorconfig 呢? + +- ESLint 确实包含 .editorconfig 中的一些属性,如缩进等,但并不全部包含,如 .editorconfig 中的 insert_final_newline 属性 Eslint 就没有。Eslint 更偏向于对语法的提示,如定义了一个变量但是没有使用时应该给予提醒。而 .editorconfig 更偏向于代码风格,如缩进等。 +- ESLint 仅仅支持对 js 文件的校验,而 .editorconfig 不光可以检验 js 文件的代码风格,还可以对 .py(python 文件)、.md(markdown 文件)进行代码风格控制。 + +> 根据项目需要,Eslint 和 .editorconfig 并不冲突,同时配合使用可以使代码风格更加优雅。 + +## 安装 EditorConfig + +[EditorConfig for VS Code](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) + +创建 `.editorconfig`,示例内容如下 + +```editorconfig title='.editorconfig' icon='logos:editorconfig' +# http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +quote_type = single + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false +``` diff --git a/docs/skill/code-specification/eslint.md b/docs/skill/code-specification/eslint.md new file mode 100644 index 0000000..57aa40d --- /dev/null +++ b/docs/skill/code-specification/eslint.md @@ -0,0 +1,57 @@ +--- +id: eslint +slug: /eslint +title: eslint +authors: kuizuo +keywords: ['code-specification', 'eslint'] +--- + +ESLint 是一种用于识别和报告 ECMAScript/JavaScript 代码中发现的模式的工具,目的是使代码更加一致并避免错误。 + +[Getting Started with ESLint](https://eslint.org/docs/latest/user-guide/getting-started) + +## eslint-config + +这里强烈推荐 [antfu/eslint-config](https://github.com/antfu/eslint-config),以及大佬的文章 [Why I don't use Prettier (antfu.me)](https://antfu.me/posts/why-not-prettier) + +这份 eslint 配置对于 ts 与 vue 已经足够完整,如果还有其他需求,可自行添加 rule 或使用[overrides](https://eslint.org/docs/latest/user-guide/configuring/configuration-files#how-do-overrides-work)。 + +## 在 Vscode 中集成 ESlint 插件 + +- 在 VScode 插件市场安装 [ESLint 插件](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + +- 开启代码保存时自动执行 ESLint 修复功能(全局设置) + +```json title='.vscode/settings.json' icon='logos:visual-studio-code' + "editor.codeActionsOnSave": { + "source.fixAll": false, + "source.fixAll.eslint": true, + "source.organizeImports": false + }, +``` + +- 工作区示例如下 + +```json title='.vscode/settings.json' icon='logos:eslint' +{ + "prettier.enable": false, + "editor.formatOnSave": false, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + } +} +``` + +## 在 WebStorm 中集成 ESLint 插件 + +> 由于 WebStorm 自动集成 ESLint,所以我们无需安装 + +- 进入 WebStorm 配置 ESLint 自动修复 + +![image-20220701081021965](https://tva1.sinaimg.cn/large/e6c9d24egy1h3r3vxs790j215p0u00vk.jpg) + +## 注意事项 + +由于 eslint 配置相对繁琐,所以很多时候编辑器的 eslint 可能都没有生效,具体看编辑器下方状态栏或者日志输出查看ESLint状态。如果为警告(黄色感叹号)或者错误(红色),那么ESLint就是没配置好,可能缺少某些依赖文件或是配置文件写错了。 + +![](https://img.kuizuo.cn/image-20221002163239434.png) diff --git a/docs/skill/code-specification/guides.mdx b/docs/skill/code-specification/guides.mdx new file mode 100644 index 0000000..2b9f708 --- /dev/null +++ b/docs/skill/code-specification/guides.mdx @@ -0,0 +1,19 @@ +--- +id: code-specification-guides +slug: /code-specification +title: 代码规范 +authors: kuizuo +keywords: ['code-specification'] +--- + +杂乱不堪如同屎山般的代码,会让开发者头皮发麻,无从下手,往往更容易诱发 bug。而一个良好的代码规范,能够修复团队的各个成员间代码格式不统一,有利于维护与审查。 + +这里主要不是介绍具体的代码规范标准,这些在对应的官方文档中的风格指南可查看。本文主要利用插件工具,在保存代码与上传代码时,根据配置规则来规范代码。 + +> 本栏主要针对前端项目的代码规范配置,使用 VSCode 文本编辑器及其插件配置。 + +```mdx-code-block +import DocCardList from '@theme/DocCardList'; + + +``` diff --git a/docs/skill/code-specification/husky.md b/docs/skill/code-specification/husky.md new file mode 100644 index 0000000..f22fa0e --- /dev/null +++ b/docs/skill/code-specification/husky.md @@ -0,0 +1,105 @@ +--- +id: husky +slug: /husky +title: husky +authors: kuizuo +keywords: ['code-style', 'husky'] +--- + +为了确保只有合格的代码才能够提交到仓库。需要配置自动化脚本,确保代码在提交前通过了代码验证工具的检验。 + +实际上 git 本身就设计了生命周期钩子来完成这个任务。但是设置过程比较复杂。所以通常情况下会使用 husky 来简化配置。 + +[Husky](https://typicode.github.io/husky/#/) + +[Git - githooks](https://git-scm.com/docs/githooks) + +```bash +pnpm i husky -D +``` + +会创建一个 npm script + +``` +npm set-script prepare "husky install" +``` + +## githooks + +### 在 commit 提交前执行 lint 代码校验 + +执行下方命令,以添加生命周期钩子: + +```sql +npx husky add .husky/pre-commit "pnpm lint" +``` + +会创建 `.husky/pre-commit` 文件,其内容如下 + +```bash title='.husky/pre-commit' +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +pnpm lint +``` + +在每次提交时,都将会执行 lint 脚本来检查代码。 + +### 在 push 之前通过单元测试 + +不过更多的做法都是用 **github action** 配置 CI 在虚拟机上跑测试,而不是本地测试。(故这步可省略) + +执行下方命令,以添加生命周期钩子: + +```bash +npx husky add .husky/pre-push "pnpm test" +``` + +### 提交时自动检查 commit 信息是否符合要求 + +[commitlint - Lint commit messages](https://commitlint.js.org/#/?id=getting-started) + +安装 + +```bash +pnpm i -g @commitlint/cli @commitlint/config-conventional +``` + +```bash +echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js +``` + +:::warning 注意 + +windows 系统请勿使用上行命令,否则会导致编码不是 UTF-8。建议直接复制文本内容到 `commitlint.config.js` + +```javascript title='commitlint.config.js' +module.exports = { extends: ['@commitlint/config-conventional'] } +``` + +::: + +将 commitlint 脚本添加到 githooks 中, 让每次提交前都验证信息是否正常。 + +```bash +npx husky add .husky/commit-msg "npx --no-install commitlint --edit "$1"" +``` + +其内容如下 + +```bash title='.husky/commit-msg' +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx --no-install commitlint --edit "$1" +``` + +测试 commit 提交 `echo 'foo: bar' | commitlint` 将会报错,不符合 commit msg 规范。 + +``` +echo 'foo: bar' | commitlint +⧗ input: foo: bar✖ type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] [type-enum] + +✖ found 1 problems, 0 warnings +ⓘ Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint +``` diff --git a/docs/skill/code-specification/npmrc.md b/docs/skill/code-specification/npmrc.md new file mode 100644 index 0000000..44df478 --- /dev/null +++ b/docs/skill/code-specification/npmrc.md @@ -0,0 +1,17 @@ +--- +id: npmrc +slug: /npmrc +title: npmrc +authors: kuizuo +keywords: ['code-specification', 'npmrc'] +--- + +对于 pnpm 项目,通常会有一个 `.npmrc` 文件,用于配置npm的一些参数,比如使用pnpm的严格模式等,其内容如下。 + +```properties title='.npmrc' icon='logos:npm-icon' +shamefully-hoist=true +strict-peer-dependencies=false +shell-emulator=true +``` + +此外,配置仓库镜像源,node版本等等。更多配置可看 [.npmrc](https://pnpm.io/npmrc)。 diff --git a/docs/skill/code-specification/prettier.md b/docs/skill/code-specification/prettier.md new file mode 100644 index 0000000..cc1c8e0 --- /dev/null +++ b/docs/skill/code-specification/prettier.md @@ -0,0 +1,32 @@ +--- +id: prettier +slug: /prettier +title: prettier +authors: kuizuo +keywords: ['code-specification', 'prettier'] +--- + +Prettier 是一个固执己见的代码格式化程序。 + +[Install · Prettier](https://prettier.io/docs/en/install.html) + +## 集成在 ESlint 中 + +ESlint 与 Prettier 可能会冲突,故需做如下设置: + +```js +//1. 安装 eslint-config-prettier 插件 +npm i -D eslint-config-prettier +//2. 在 eslint 的配置文件中写入以下内容 +extends: ['plugin:prettier/recommended'], // 避免与 prettier 冲突 +``` + +## prettier 与 eslint 如何选择 + +prettier 只需要按照一个 vscode 插件,几乎没有任何门槛,按下 Ctrl + Alt + F 就可以美化你的代码。而 eslint 需要配合代码编辑器与相关规则,通过保存文件或者执行 eslint 命令才能格式化代码。但往往也是因为过少的配置,使 prettier 对代码的约束不如 eslint。 + +可以看看 Antfu 大佬的博客 [Why I don't use Prettier (antfu.me)](https://antfu.me/posts/why-not-prettier),阐述了他为何不使用 Prettier。 + +这两个我都有在使用,在临时编写 demo 代码的时候,肯定优先使用 prettier。 + +但是在实际项目中,如果不使用 eslint 的话,每次保存代码都需要手动格式化,还是比较繁琐的。 diff --git a/docs/skill/code-specification/stylelint.md b/docs/skill/code-specification/stylelint.md new file mode 100644 index 0000000..82b3f68 --- /dev/null +++ b/docs/skill/code-specification/stylelint.md @@ -0,0 +1,26 @@ +--- +id: stylelint +slug: /stylelint +title: stylelint +authors: kuizuo +keywords: ['code-specification', 'stylelint'] +--- + +stylelint 主要针对 css 样式进行格式化(包括css预处理器),同时对一些属性拼写进行检查。 + +[Getting started | Stylelint](https://stylelint.io/user-guide/get-started) + +配置文件示例: + +```json title='.stylelintrc.json' icon='logos:stylelint' +{ + "extends": ["stylelint-config-recommended", "stylelint-config-standard"], + "rules": { + "indentation": 4 + } +} +``` + +## 配合 prettier + +[prettier/stylelint-config-prettier](https://github.com/prettier/stylelint-config-prettier) diff --git a/docs/skill/database/elasticsearch/index.md b/docs/skill/database/elasticsearch/index.md new file mode 100644 index 0000000..25f368b --- /dev/null +++ b/docs/skill/database/elasticsearch/index.md @@ -0,0 +1,449 @@ +--- +id: elasticsearch-note +slug: /skill/database/elasticsearch +title: elasticsearch笔记 +date: 2021-03-15 +tags: [elasticsearch, database] +keywords: [elasticsearch, database] +--- + +[Elasticsearch Clients | Elastic 官方文档](https://www.elastic.co/guide/en/elasticsearch/client/index.html) + +## 安装 + +下载地址:[Elasticsearch, Kibana, and the Elastic Stack | Elastic](https://www.elastic.co/cn/start) + +### window + +解压,双击 bin 目录下的 `elasticsearch.bat` 即可启动,kibana 也是同理。 + +启动后输入 http://localhost:9200 与 http://localhost:5601/ 显示正常说明两者都安装成功 + +### linux + +同 windows 不过多叙述了 + +### docker + +当然上面那些安装都过于麻烦,docker 一步到位 + +#### elasticsearch + +[elasticsearch (docker.com)](https://hub.docker.com/_/elasticsearch) + +``` +# 创建自定义网络与kibana通信 +docker network create esnet + +# 挂载目录 端口映射 +docker run -d --name elasticsearch --net esnet -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -v /data/elasticsearch:/usr/share/elasticsearch/data -v /data/elasticsearch/plugins:/usr/share/elasticsearch/plugins elasticsearch:tag + +``` + +参数详解 + +``` +docker run 创建并启动容器 +-d 后台运行 +--name elasticsearch 指定容器唯一的名称,方便管理 +-p 9200:9200 -p 9300:9300 映射容器端口到宿主机上 +-e "discovery.type=single-node" 环境变量配置单机模式 +-v /data/elasticsearch:/usr/share/elasticsearch/data 持久化数据存储 +-v /data/elasticsearch/plugins:/usr/share/elasticsearch/plugins +elasticsearch:tag 镜像名称及版本 tag最新 +``` + +#### kibana + +``` +docker run -d --name kibana --net esnet -p 5601:5601 kibana:tag +``` + +#### ik 分词器 + +```bash +cd /usr/share/elasticsearch/plugins/ +elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.2.0/elasticsearch-analysis-ik-7.2.0.zip +exit +docker restart elasticsearch +``` + +或 + +```bash +docker exec -it 容器id /bin/bash +cd /usr/share/elasticsearch/plugins/ +mkdir ik +cd ik +wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.6.2/elasticsearch-analysis-ik-7.6.2.zip +yum install unzip +unzip elasticsearch-analysis-ik-7.6.2.zip +exit +docker restart elasticsearch +``` + +然后可以在 kibana 界面的`dev tools`中验证是否安装成功; + +``` +POST test/_analyze +{ + "analyzer": "ik_max_word", + "text": "你好我是愧怍" +} +``` + +#### 设置密码 + +[ElasticSearch 设置账户密码](https://blog.csdn.net/qq_43188744/article/details/108096394) + +进入 es 容器 + +``` +docker exec -it elasticsearch bash + +cd config +vi elasticsearch.yml +``` + +添加如下代码 + +``` +http.cors.enabled: true +http.cors.allow-origin: "*" +http.cors.allow-headers: Authorization +xpack.security.enabled: true +``` + +重启后,重新进入容器,输入 + +``` +elasticsearch-setup-passwords interactive +``` + +按 y 确认后即可设置密码 + +进入 kibana 容器 + +``` +docker exec -it kibana bash + +cd config +vi kibana.yml +``` + +添加如下代码 + +``` +elasticsearch.username: "kibana" +elasticsearch.password: "a123456" +``` + +顺便在加这几行代码,后续如果导出数据过大的话也导的出来 + +``` +xpack.reporting.csv.maxSizeBytes: 409715200 +xpack.reporting.queue.timeout: 2800000 +``` + +登录 Kibana 的账户就是 kibana,elasticsearch 的账户为 elastic. + +## docker-compose + +创建 volume 挂载目录,并修改目录用户和用户组。由于 elasticsearch6 之后不允许使用 root 启用,所以需要修改 + +``` +/usr/share/elasticsearch/data的权限为1000 +mkdir -pv /usr/share/elasticsearch/data +chown 1000:1000 /usr/share/elasticsearch/data +``` + +部署文件 + +``` +mkdir /usr/local/elasticsearch-kibana +cd elasticsearch-kibana/ +vim docker-compose.yml +``` + +docker-compose.yml + +```yaml +version: '3.9' +services: + elasticsearch: + image: elasticsearch:7.2.0 + container_name: elasticsearch + volumes: + - /usr/share/elasticsearch/data:/usr/share/elasticsearch/data + - ./elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml + ports: + - 9200:9200 + - 9300:9300 + networks: + - esnet + restart: always + kibana: + image: kibana:7.2.0 + container_name: kibana + ports: + - 5601:5601 + networks: + - esnet + depends_on: + - elasticsearch + restart: always + +networks: + esnet: +``` + +vim elasticsearch.yml + +``` +#集群名 +cluster.name: "elasticsearch" +# 允许外部网络访问 +network.host: 0.0.0.0 +#支持跨域 +http.cors.enabled: true +#支持所有域名 +http.cors.allow-origin: "*" +# 开启xpack安全校验,在kibana中使用需要输入账号密码 +xpack.security.enabled: true +xpack.security.transport.ssl.enabled: true + +``` + +启动 docker-compose `docker-compose up -d` + +至此有关 elasticSearch 安装与配置就告一段落 + +## 数据迁移 + +### elasticdump + +[elasticsearch-dump/elasticsearch-dump](https://github.com/elasticsearch-dump/elasticsearch-dump) + +这里使用 elasticdump (因为只会这个) + +#### 安装 + +```bash +npm install elasticdump -g +elasticdump +``` + +#### 命令 + +``` +elasticdump --input SOURCE --output DESTINATION [OPTIONS] +``` + +##### 参数 + +- limit + + 每个操作要批量移动多少对象,Limit 是文件流的近似值 默认:100 + +- type + + 导出类型 默认 data [settings, analyzer, data, mapping, policy, alias, template, component_template, index_template] + +- 其他参数看文档,暂时都用不上 + +例: + +```bash +# 将es数据导入另一台es数据 +elasticdump --input=http://production.es.com:9200/my_index --output=http://staging.es.com:9200/my_index --all=true --limit=2000 + +# 或 +elasticdump \ + --input=http://production.es.com:9200/my_index \ + --output=http://staging.es.com:9200/my_index \ + --type=analyzer +elasticdump \ + --input=http://production.es.com:9200/my_index \ + --output=http://staging.es.com:9200/my_index \ + --type=mapping +elasticdump \ + --input=http://production.es.com:9200/my_index \ + --output=http://staging.es.com:9200/my_index \ + --type=data + +# 备份文件到本地 +elasticdump \ + --input=http://production.es.com:9200/my_index \ + --output=/data/my_index_mapping.json \ + --type=mapping +elasticdump \ + --input=http://production.es.com:9200/my_index \ + --output=/data/my_index.json \ + --type=data + +``` + +#### docker 安装 + +``` +docker pull elasticdump/elasticsearch-dump +``` + +例: + +``` +# Copy an index from production to staging with mappings: +docker run --rm -ti elasticdump/elasticsearch-dump \ + --input=http://production.es.com:9200/my_index \ + --output=http://staging.es.com:9200/my_index \ + --type=mapping +docker run --rm -ti elasticdump/elasticsearch-dump \ + --input=http://production.es.com:9200/my_index \ + --output=http://staging.es.com:9200/my_index \ + --type=data + +# Backup index data to a file: +docker run --rm -ti -v /data:/tmp elasticdump/elasticsearch-dump \ + --input=http://production.es.com:9200/my_index \ + --output=/tmp/my_index_mapping.json \ + --type=data +``` + +## 常用命令 + +### 查询并删除匹配文档 + +正常查询对应的代码 + +``` +GET answer/_search +{ + "query": { + "match_phrase": { + "topic": "测试" + } + } +} +``` + +要删除 topic 为“测试”,只需要将`_search`替换为`_delete_by_query`即可。 + +--- + +暂时只用到这些 TODO。。。 + +## 注意事项 + +### elasticsearch 默认输出一万条 + +elasticsearch 默认输出最多一万条,查询第 10001 条数据就会报错 + +解决方案: + +1、修改 elasticsearch 输出默认限制条数 + +``` +PUT 索引名称/_settings?preserve_existing=true +{ + "max_result_window": "1000000" +} +``` + +2、创建索引时设置 + +``` +"settings":{ + "index":{ + "max_result_window":1000000 +   } +} +``` + +3、在请求的时候附加参数`"track_total_hits":true` + +### elasticsearch 默认分配内容为 1g + +elasticsearch 默认分配内容为 1g,在`jvm.options`配置如下 + +``` +################################################################ +## IMPORTANT: JVM heap size +################################################################ +## +## The heap size is automatically configured by Elasticsearch +## based on the available memory in your system and the roles +## each node is configured to fulfill. If specifying heap is +## required, it should be done through a file in jvm.options.d, +## and the min and max should be set to the same value. For +## example, to set the heap to 4 GB, create a new file in the +## jvm.options.d directory containing these lines: +## +## -Xms4g +## -Xmx4g +## +## See https://www.elastic.co/guide/en/elasticsearch/reference/current/heap-size.html +## for more information +## +################################################################ + +-Xms1g +-Xmx1g +``` + +将其更改为服务器可分配的的内存,比如 32g,就分配个 16g 即可 + +``` +-Xms16g +-Xmx16g +``` + +重启 elasticsearch 生效。 + +### kibana 设置导出 csv 大小 + +kibana 默认导出的 csv 有文件大小限制,默认是 10M,数据量大于 10M,那么 csv 只会下载 10M 大小的数据 + +并且导出 CSV 报告 Kibana 是放入队列中执行的,有一个处理超时时间,默认是 12000 毫秒,也就是 2 分钟 + +解决方案: 通过修改配置可以更改限制大小 + +`vim kibana.yml` + +``` +# csv文件大小200MB,默认为10485760(10MB) +xpack.reporting.csv.maxSizeBytes: 209715200 +# 超时时间-30分钟,默认是120000(2分钟) +xpack.reporting.queue.timeout: 1800000 +``` + +**修改后,重启 kibana 即可生效** + +> 参考 [Kibana 7.X 导出 CSV 报告](https://blog.csdn.net/qq_25646191/article/details/108641758) + +### Kibana server is not ready yet + +访问 Elasticsearch 的 9200 端口,能正常访问,但访问 Kibana 的 5601 端口就提示 + +``` +Kibana server is not ready yet +``` + +**解决办法** + +将配置文件 kibana.yml 中的 elasticsearch.url 改为正确的链接,默认为: [http://elasticsearch:9200](http://elasticsearch:9200),改为 http://自己的 IP 地址:9200 + +``` +# Default Kibana configuration for docker target +server.name: kibana +server.host: "0" +elasticsearch.hosts: [ "http://elasticsearch:9200" ] +xpack.monitoring.ui.container.elasticsearch.enabled: true +``` + +然后重启 kibana 即可,记得防火墙开放 5601 端口 + +#### 出问题不知道怎么解决,查看日志输出才是关键 + +``` +docker logs 容器id(容器名) +``` diff --git a/docs/skill/database/mongo/index.md b/docs/skill/database/mongo/index.md new file mode 100644 index 0000000..5d8e883 --- /dev/null +++ b/docs/skill/database/mongo/index.md @@ -0,0 +1,585 @@ +--- +id: mongodb-note +slug: /skill/database/mongodb +title: MongoDB笔记 +date: 2021-06-20 +tags: [mongodb, database] +keywords: [mongodb, database] +--- + +## 安装 + +### Windows + +官网下载[MongoDB Community Download](https://www.mongodb.com/try/download/community) + +可安装 MongoDB Compass 的数据库管理工具 + +打开 bin/mongo.exe 即可连接 MongoDB + +### Linux + +推荐直接宝塔面板,然后在软件商店点击安装 MongoDB 即可 + +### Docker + +[`mongo (docker.com)`](https://hub.docker.com/_/mongo) + +```bash +docker pull mongo:latest + +mkdir /home/mongo/ # 创建本地数据库文件夹 + +docker run -itd --name mongo --restart=always --privileged -p 27017:27017 -v /home/mongo/data:/data/db -v /home/mongo/conf:/data/configdb -v /home/mongo/logs:/data/log/ mongo:latest --config /data/configdb/mongod.conf --bind_ip_all +# -v 指定配置文件启动 +# --bind_ip_all 允许所有IP访问 +# ----restart=always Docker服务重启容器也启动 +# --privileged 拥有真正的root权限 + +docker exec -it mongo bash +# 进入容器 + +root@71351dc5b914:/# mongo +# 进入 mongo + +# 后文有 +``` + +### 配置远程连接 + +首先按照上面步骤创建用户,后续都是以这个用户来进行连接数据库 + +找到配置文件 + +windows `\bin\mongod.cfg` + +linux `/etc/mongo/mongod.conf` + +``` +bind_ip = 0.0.0.0 + +security: + authorization:enabled +# 注意 两个空 +``` + +注意: mongodb Compass 有可能会连接不上提示**Authentication failed**,但使用代码即可。 + +配置文件如下 + +```bash +# 数据库文件存储位置 +dbpath = /data/db/ +# log文件存储位置 +logpath = /data/log/mongodb/master/mongodb.log +# 使用追加的方式写日志 +logappend = true +# 是否以守护进程方式运行 +# fork = true +# 端口号 +port = 27017 +# 是否启用认证 +auth = true +# 设置oplog的大小(MB) +oplogSize=2048 +``` + +- 开启防火墙 systemctl start firewall + +- 防火墙放端口 + + firewall-cmd --zone=public --add-port=27010/tcp --permanent + +- 重启防火墙 + + firewall-cmd --reload + +## 基本命令 + +```bash +# 创建数据库 +use 数据库名 + +show databases + +show users +``` + +### 增删改查 + +原生的 mongodb CRUD 命令没啥好说的,Nodejs 主要配合 Mongoose 来使用,这边就直接不列举了 + +### 索引 + +```js +// 创建索引 +db.user.ensureIndex({"username":1},{"name":"usernameIndex"}) // 1是升序 一般用降序 可查最新的账号 第二个参数可指定索引名称 + +// 获取索引 +db.user.getIndexes() + +// 删除索引 +db.user.dropIndex({"username":1}) + +// 唯一索引 +db.user.ensureIndex({"userId":1},{"unique",true}) +// 再次插入userId重复的文档 mongodb将会报错 提示插入重复键 同时有重复文档也无法创建唯一索引 +``` + +### 账户权限配置管理 + +#### 1.创建用户 + +```js +use admin + +# root 超级管理员 +db.createUser({ user:'admin', pwd:'123456',roles:[ { role:'root', db: 'admin'} ] }); +db.auth('admin', '123456') + +# 创建有可读写权限的用户. 对于一个特定的数据库, 比如 my,添加用户 user1,角色:dbOwner +db.createUser({user:"user1",pwd:"pwd",roles:[{role:"dbOwner",db:"my"}]}) +``` + +一些角色权限 命令 + +### 角色命令 + +```js +show users // 查看当前数据库下角色 + +db.updateUser("admin",pwd:"password") + +db.auth("admin","password") + +// 或者 直接通过Url 来连接 +const url = 'mongodb://admin:a123456.@localhost:27027/'; +``` + +### 聚合管道 + +```js +// $projext 限制字段 +db.order.aggregate([ +{$projext:{no:1,all_price:1}} +]) + +// $match 过滤文档 类似于find 方法中的参数 +db.order.aggregate([ + {$projext:{no:1,all_price:1}}, + {$match:{"all_price":{$get:90}} +]) + +// $group 分组 +db.order.aggregate([ + { + $group:{_id:"$order_id",total:{$sum: "$price"}} + }, +]) + +// 可加 $sort $skip + +// $lookup 表关联 +db.order.aggregate([ + { + $lookup:{ + from:'order_item', + localField:"order_id", + foreignField:"order_id", + as:"items" + } + }, +]) +``` + +## Mongoose + +### 连接 + +```js +const mongoose = require('mongoose') +let url = 'mongodb://localhost:27017/kuizuo' +mongoose.connect(url, { useNewUrlParser: true }, function (err) {}) +``` + +### 定义 Schema + +```js +import * as mongoose from 'mongoose' + +let UserSchema = mongoose.Schema({ + username: { + type: String, + trim: true, + unique: true, // 唯一索引 index:true 是普通索引 + }, + password: String, + age: { + type: Number, + get(params) { + return params + '岁' + }, // get不建议使用 因为不是获取的时候添加 而是实例化的时候取的时候添加 + }, + status: Number, + headImg: { + type: String, + set(params) { + if (!params.includes('https://') || !params.includes('http://')) { + return 'http://' + params + } + return params + }, + }, +}) +``` + +### 定义模型 + +```js +// let User = mongoose.model('User', UserSchema) // 首字母大写 默认users表 +let User = mongoose.model('User', UserSchema, 'user') // 指定user表 + +User.find({}, (err, doc) => { + console.log(doc) +}) + +// 增加数据 +// 实例化对象 +let user = new User({ + username: 'kuizuo', + password: 'a12345678', +}) + +user.save() +``` + +### 自定义封装方法(一般很少使用) + +```js +// 静态方法 实在Schema上扩展 +UserSchema.statics.findByUsername = function(username,cb){ + this.find({'username',username},function(err,data){ + cb(err,data) + }) +} + +// 实例方法 没多大用 +UserSchema.methods.print = function(){ + console.log("实例方法") +} +``` + +### 数据效验 + +```js +let UserSchema = mongoose.Schema({ + username: { + type: String, + trim: true, + require:true // 必须传入 + }, + password: String, + mobile:{ + match: /^1((34[0-8]\d{7})|((3[0-3|5-9])|(4[5-7|9])|(5[0-3|5-9])|(6[0-9])|(7[0-3|5-8])|(8[0-9])|(9[1|5|8|9]))\d{8})$/, + }, + age: { + type: Number, + max: 200, + min: 0 + }, + status: { + type:String, + default:"success", + enum:["success",'error] //用在String类型 + }, + headImg: { + type: String, + set(params) { + if (!params.includes("https://") || !params.includes("http://")) { + return 'http://' + params + } + return params + } + } +}) +``` + +## Mongoose 命令 + +### 查询指定时间范围 + +``` +let filter = { + timestamp: { + '$gte': 123456789, + '$lte': 987654321, + } +} +``` + +1. `(>)` 大于 - $gt +2. `(<)` 小于 - $lt +3. `(>=)` 大于等于 - $gte +4. `(<=)` 小于等于 - $lte + +如果时间日期格式是 ISO,则需用使用 ISODate 函数转为一下 + +``` +ISODate("2020-01-01T00:00:00Z") +``` + +### 去除 mongodb \_\_v 字段 + +[去除 mongodb 下划线\_\_v 字段](https://blog.csdn.net/a1059526327/article/details/106893186) + +去除\_\_v 字段,可以在定义 schema 规则的时候通过设置`versionKey:false`去除这个字段: + +```js +var userSchema = new mongoose.Schema( + { + username: { + type: String, + required: true, + }, + password: { + type: String, + required: true, + select: false, + }, + }, + { versionKey: false }, +) +``` + +如果在数据库中扔向保留这个字段,只是在查询的时候不想返回 `**v` 字段,可以通过设置 `{ **v: 0}` 在返回结果中过滤掉这一字段 + +```js +UserModel.findOne({username, password}, {__v: 0}, function (err, user){ +} +``` + +### 查询内嵌数组 + +原始数据如下 + +```json +{ + "_id" : ObjectId("5aab3460353df3bd352e0e15"), + "username": "15212345678" + "tags" : [ + { + "name" : "前端", + }, + { + "name" : "后端", + }, + ] +} +``` + +查询 username=15212345678 and tags.name="前端" (**不希望出现这个后端**) + +想要的数据为 + +```json +{ + "_id" : ObjectId("5aab3460353df3bd352e0e15"), + "username": "15212345678" + "tags" : [ + { + "name" : "前端", + } + ] +} +``` + +但通过 find 查询会将整个文档都给返回,这是我们不希望的,有两种方法可以实现 + +#### $elemMatch + +```js +Model.find({ username: '15212345678', name: { $elemMatch: { name: '前端' } } }) +``` + +要注意的是:**对于数组中只有一个返回元素,我们可以使用$elemMatch来查询,但是对于多个元素$elemMatch 是不适应。** + +#### aggregation + +[Aggregation Pipeline](https://docs.mongodb.com/manual/reference/operator/aggregation-pipeline/) + +一共有三个参数 + +- $unwind: 将数组中的每一个元素转为每一条文档 + 使用$unwind 可以将指定内嵌数组中的每个数据都被分解成一个文档,并且除了指定的值不同外,其他的值都是相同的 +- $match: 简单的过滤文档,条件查询。query +- $project: 修改输入文档的结构,例如别名,字段显示 [mongoose聚合—$project](https://www.cnblogs.com/ellen-mylife/p/14794284.html) + +例: + +```json +Model.aggregate([{ "$unwind": "$tags" }, { "$match": { "tags.name": "前端" } }, { "$project": { "tags": 1 } }]) +``` + +但显示的效果为 + +```json +{ + "_id" : ObjectId("5aab3460353df3bd352e0e15"), + "username": "15212345678" + "tags" : { + "name" : "前端", + } +} +``` + +tags 直接有**数组转为文档**了,因为添加了$unwind这个参数,将会拆分为多条数据,比如我不加$match 那么还将输出 tags 为后端单独一个文档,这肯定也不是想要的数据,就是想要这个 tags 为数组,那么有如下两种操作方式 + +#### $group + +方法一:使用$unwind将tags数组打散,获取结果集后用$match 筛选符合条件的数据,最后使用$group 进行聚合获取最终结果集。 + +```json +db.getCollection("user").aggregate([ + { "$unwind": "$tagss" }, + { "$match": { "tags.name": "前端" } }, + { + "$group": { + "_id": "$uid", + "username": { "$first": "$username" }, + "tags": { "$push": "$tags" } + } + } +]) +``` + +不过要注意的是,要显示其他字段的话,可以通过$first来显示,如`“username”: { $first: "$username" }` + +方法二:使用$match过滤符合条件的根文档结果集,然后使用$project 返回对应字段的同时,在 tags 数组中使用$filter 进行内部过滤,返回最终结果集 + +```js +db.getCollection('user').aggregate([ + { $match: {} }, + { + $project: { + uid: 1, + username: 1, + tags: { + $filter: { + input: '$tags', + as: 'item', + cond: { $eq: ['$$item.name', '前端'] }, + }, + }, + }, + }, +]) +``` + +相比 group 而言,filter 比较直接,但通过 group 可以直接统计对应的数量啥的,毕竟分组聚合才是关键精髓。 + +## 数据份与恢复 + +[MongoDB 备份与恢复](https://zhuanlan.zhihu.com/p/163255094) + +### 备份 + +``` +mongodump -h dbhost -d dbname -o dbdirectory +``` + +-h:MongoDB 所在服务器地址,例如:127.0.0.1,当然也可以指定端口号:127.0.0.1:27017 + +-d:需要备份的数据库实例,例如:test + +-o:备份的数据存放位置,例如:c:\data\dump,当然该目录需要提前建立,在备份完成后,系统自动在 dump 目录下建立一个 test 目录,这个目录里面存放该数据库实例的备份数据。 + +--gzip:压缩格式 gzip + +mongodump 命令可选参数列表如下所示: + +| 语法 | 描述 | 实例 | +| :-- | :-- | :-- | +| mongodump --host HOST_NAME --port PORT_NUMBER | 该命令将备份所有 MongoDB 数据 | mongodump --host runoob.com --port 27017 | +| mongodump --dbpath DB_PATH --out BACKUP_DIRECTORY | 指定备份数据库位置 | mongodump --dbpath /data/db/ --out /data/backup/ | +| mongodump --collection COLLECTION --db DB_NAME | 该命令将备份指定数据库的集合。 | mongodump --collection mycol --db test | + +例: 备份 test 数据库 + +``` +mongodump --port 27017 -u test -p 123456 --authenticationDatabase test -o back +``` + +### 恢复 + +mongorestore 命令脚本语法如下: + +``` +mongorestore -h <:port> -d dbname +``` + +- `--host <:port>, -h <:port>` :MongoDB 所在服务器地址,默认为:localhost:27017 + +- `--db , -d` :需要恢复的数据库实例,例如:test,当然这个名称也可以和备份时候的不一样,比如 test2 + +- `--drop`:恢复的时候,遇到重复值先删除当前数据,然后恢复备份的数据。就是说,恢复后,备份后添加修改的数据都会被删除,慎用哦! + +- `\`:mongorestore 最后的一个参数,设置备份数据所在位置,例如:c:\data\dump\test。 + + 你不能同时指定 `\` 和 --dir 选项,--dir 也可以设置备份目录。 + +- --dir:指定备份的目录 + + 你不能同时指定 `\` 和 --dir 选项。 + +例: + +``` +mongorestore --port 27017 -u test -p 123456 --authenticationDatabase test +``` + +### 将 mysql 中的数据导入 mongo + +1、mysql 开启安全路径 + +`vim /etc/my.cnf` + +```text +#添加以下配置 +secure-file-priv=/tmp +``` + +重启数据库生效 + +```text +/etc/init.d/mysqld restart +``` + +2、mysql ⾃定义分隔符导出成 csv 格式 + +```text +select * from test.t100w limit 10 into outfile '/tmp/100w.csv' fields terminated by ','; +``` + +PS:mysql 导出 csv + +fields terminated by ','    ------字段间以,号分隔 + +optionally enclosed by '"'   ------字段用"号括起 + +escaped by '"'    ------字段中使用的转义符为" + +lines terminated by '\r\n';  ------行以\r\n 结束 + +PS:mysql 导入 csv + +```text +load data infile '/tmp/2.csv' +into table t1 +fields terminated by ',' ; +``` + +3、在 mongodb 中导入备份 + +```text +mongoimport -u root -p root123 --port 27017 --authenticationDatabase admin -d test -c t100w --type=csv -f id,num,k1,k2,dt --file /tmp/100w.csv +``` diff --git "a/docs/skill/database/mysql/Mysql\346\233\277\346\215\242\345\207\275\346\225\260replace.md" "b/docs/skill/database/mysql/Mysql\346\233\277\346\215\242\345\207\275\346\225\260replace.md" new file mode 100644 index 0000000..a433c92 --- /dev/null +++ "b/docs/skill/database/mysql/Mysql\346\233\277\346\215\242\345\207\275\346\225\260replace.md" @@ -0,0 +1,30 @@ +--- +id: mysql-replace-function +slug: /mysql-replace-function +title: mysql替换函数replace +date: 2021-01-07 +tags: [mysql, database] +keywords: [mysql, database] +--- + +## 1、前言 + +当初设计题库数据库时,没考虑周全,存在多题目,题目不标准,比如下面这样 + +![image-20210107044832103](https://img.kuizuo.cn/image-20210107044832103.png) + +题目前面的【单选题】【判断题】怎么能忍,于是就百度 mysql 文本替换 第一篇文章就解决了我的问题,于是我也顺手记录一下,以防下次使用 + +> 参考链接 [mysql 替换函数 replace()实现 mysql 替换指定字段中的字符串](https://blog.csdn.net/qq_36663951/article/details/78791138) + +## 2、替换函数 replace() + +最关键的也就是这个函数了,先看看我的 SQL 语句是怎么写的 + +```sql +UPDATE `kz_answer` SET `topic` = replace (`topic`,'【单选题】','') WHERE `topic` LIKE '%【单选题】%' +``` + +其实也就是 UPDATE 更新语句,然后通过 WHERE 子句与 LIKE 模糊判断,最后将字段给修改了。会点 MySQL 的上面代码一眼就懂,不写了,还要折腾题库接口和题库存储。 + +该函数是多字节安全的,也就是说你不用考虑是中文字符还是英文字符。 diff --git "a/docs/skill/database/mysql/Mysql\346\250\241\347\263\212\346\237\245\350\257\242like\344\274\230\345\214\226.md" "b/docs/skill/database/mysql/Mysql\346\250\241\347\263\212\346\237\245\350\257\242like\344\274\230\345\214\226.md" new file mode 100644 index 0000000..00a5537 --- /dev/null +++ "b/docs/skill/database/mysql/Mysql\346\250\241\347\263\212\346\237\245\350\257\242like\344\274\230\345\214\226.md" @@ -0,0 +1,42 @@ +--- +id: mysql-like-optimization +slug: /mysql-like-optimization +title: mysql模糊查询like优化 +date: 2021-01-07 +tags: [mysql, database] +keywords: [mysql, database] +--- + +## 1、前言 + +在我存储题库的时候,搜题肯定要用模糊搜索题目,但一般情况下 like 模糊查询的写法为(field 已建立索引) + +```sql +SELECT `column` FROM `table` WHERE `field` LIKE '%keyword%'; +``` + +但是问题来了,因为是模糊搜索,一旦数据过大,查询速度将会非常慢,同时请求过多还会导致服务器负载(我的题库 API 接口就是这样),宝塔面板如下 + +![image-20210116000628122](https://img.kuizuo.cn/image-20210116000628122.png) + +所以,要保证多并发查题查题的同时,有能快速搜索到对应的题目,数据库提速就显得尤为重要了,在翻看相关文章解决了我这一问题。 + +> 参考链接 [MySql 模糊查询 LIKE 优化](https://www.imooc.com/article/300874) + +## 2、LIKE '%keyword%' + +在没怎么了解 LIKE 模糊查询前,一直以为 LIKE 会用到索引,搜索了相关资料才发现,%keyword% 对应这种的模糊搜索,用不到索引,而是全表扫描,也就导致查询速度特别慢。 + +## 3、添加前缀 + +上面写到 %keyword% 用不到索引,但如果给字段添加一个前缀文本,比如我这里添加为 KZTK\_(愧怍题库),然后拼接为 KZTK\_%keyword% + +## 4、给字段添加前缀 + +```sql +UPDATE kz_answer SET `topic` = CONCAT('KZTK_',topic) +``` + +## 然而。。。 + +然而上面的那些操作对百万级别的数据来说几乎没有任何速度的提升,因为 Like 搜索本来就很慢。上面所说的需求其实更应该换一个数据库,也就是 elasticsearch。想制作一个搜索引擎似的数据库,并且有高效的查询速度,并且可针对关键词,模糊搜索,正好就符合这个场景。 diff --git a/docs/skill/database/mysql/index.md b/docs/skill/database/mysql/index.md new file mode 100644 index 0000000..fa16c4d --- /dev/null +++ b/docs/skill/database/mysql/index.md @@ -0,0 +1,824 @@ +--- +id: mysql-note +slug: /skill/database/mysql +title: MySql笔记 +date: 2020-12-30 +tags: [mysql, database] +keywords: [mysql, database] +--- + +## 1、前言 + +比较少写这种文章,主要还是我 mysql 没系统化学习过,在写这篇前也只会 CRUD​,​ 也不会 ​ 数据 ​ 库 ​ 设计:pensive:,加上期末考正好要考 mysql,正好借这个机会重学一遍,顺便来记录一下这段学习中的一些 mysql 的操作。 + +## 2、操作数据库 + +==mysql 关键字 不区分大小写==(个人习惯,喜欢大写,方便区分),下文例子数据库以 kzsoft 为名。 + +表名与字段是**关键字**请带上反引号` + +### 2.1、简单操作数据库 + +#### 1、创建数据库 + +```sql +CREATE DATABASE IF NOT EXISTS kzsoft; +``` + +#### 2、删除数据库 + +```sql +DROP DATABASE IF EXISTS kzsoft +``` + +#### 3、使用数据库 + +```sql +USE kzsoft +``` + +#### 4、查看数据库 + +```sql +SHOW DATABASES --查看所有的数据库 有s +``` + +### 2.2、数据库的数据类型 + +#### 1、数值 + +| 类型 | 描述 | 所占字节 | 用途 | +| --------- | ------------------- | ------------ | ------------------------------- | +| tinyint | 十分小的数据 | 1 个字节 | 一般用来当布尔值用 | +| smallint | 较小的数据 | 2 个字节 | 少用 | +| mediumint | 中等的数据 | 3 个字节 | 少用 | +| **int** | **标准整数** | **4 个字节** | **常用,一般都用 int** | +| bigint | 较大的整数 | 8 个字节 | 少用 | +| float | 单浮点数/单精度小数 | 4 个字节 | 少用 | +| double | 双浮点数/双精度小数 | 4 个字节 | 少用 有精度问题 | +| decimal | 字符串形式的浮点数 | 不一定 | 精度要求高用 decimal (金融计算) | + +#### 2、字符串 + +| 类型 | 描述 | 用途 | +| ----------- | -------------------------- | ----------------------- | +| char | 固定大小 0~255,不可变长度 | 存手机号等固定长度 | +| **varchar** | **可变字符串 0~65535** | **存可变字符串 存变量** | +| tinytext | 微型文本 2^8-1 | 能用 text 就别用这个 | +| **text** | **文本串 2^16-1** | **保存大文本** | + +#### 3、时间日期 + +| 类型 | 描述 | 用途 | +| ------------ | ------------------------------------------ | -------------------- | +| date | YYYY-MM-DD 日期 | 存日期 | +| time | HH:mm:ss 时间 | 存 | +| **datetime** | **YYYY-MM-DD HH:mm:ss** | **最常用的时间格式** | +| timestamp | 时间戳形式 1970.1.1 8:00:00 到现在的毫秒数 | 但会有 2038 年问题 | + +#### 4、NULL + +不要用 NULL 进行运算,结果为 NULL + +### 2.3、字段类型 + +| 字段类似 | 描述 | 用途 | | +| -------- | ------------------------------------------- | ---------------------- | --- | +| Unsigned | 无符号整数 | 该列不能声明为负数 | | +| zerofill | 用 0 填充 | 不足的位数 用 0 来填充 | | +| 自增 | 自动在上一条记录+1 (默认,可设置自增大小) | 设置唯一的主键 如 id | | +| 非空 | not null | 该字段不能为 NULL | | +| 默认 | 默认值 | 不指定 则默认值 | | + +以下字段 是未来做项目用的,表示一个记录的存在意义 + +``` +id 主键 +`version` 乐观锁 +is_delete 伪删除 +createAt 创建时间 +updateAt 修改时间 +``` + +### 2.4、 操作表 + +**表名与字段,尽量用``括起来(你永远不知道,你的字段名会不会和关键字重名!)** + +字符串 通过单引号括起来 + +所有语句后面加,除了最后一行 + +PRIMARY KEY 主键一张表只有唯一的主键 + +#### 1、创建表 + +```sql +CREATE TABLE IF NOT EXISTS `user` ( + `id` INT(10) NOT NULL AUTO_INCREMENT COMMENT '用户id', + `username` VARCHAR(30) NOT NULL COMMENT '用户名', + `password` VARCHAR(30) NOT NULL COMMENT '密码', + PRIMARY KEY(`id`) +)ENGINE=INNODB DEFAULT CHARSET=utf8; +``` + +格式如下 + +```sql +CREATE TABLE [IF NOT EXISTS] `表名` ( + `字段名` 列类型 [属性] [索引] [注释], + `字段名` 列类型 [属性] [索引] [注释], + `字段名` 列类型 [属性] [索引] [注释], + PRIMARY KEY(` `) +)[表类型] [字符集设置] [注释] + +``` + +通过上面的手动通过 sql 语句创建表,对已创建的表可通过 + +- `SHOW CREATE DATABASE 数据库名` 查看数据库的定义语句,也就是输出创建数据库的 sql 语句 + +- `SHOW CREATE TABLE 表名` 查看表的定义语句,也就是输出创建表的 sql 语句 +- `DESC 表名` –显示表的结构 (desc 是 describe 的缩写) + + 2.5、数据库引擎 + +| | MYISM | INNODB | +| ------------ | :-------------- | -------------------------------- | +| 事务支持 | 不支持 | 支持 | +| 数据行锁定 | 不支持 | 支持 | +| 外键约束 | 不支持 | 支持 | +| 全文索引 | 支持 | 不支持 | +| 表空间的大小 | 较小 | 较大,约为前者 2 倍 | +| 各自优点 | 节省空间,速度快 | 安全性高,事务处理,多表多用户操作 | + +MySQL 引擎在物理文件上的区别 + +- INNODB 在数据库表中只有一个 \*.frm 文件(表结构定义文件) 以及上级目录下的 ibdata1 文件 +- MYISM 对应文件 有*.frm 文件 *.MYD 文件(数据文件) \*.MYI 文件(索引文件) + +设置数据库表的字符集编码 + +``` +CHARSET=utf8 +``` + +**不设置的话,会是 mysql 的默认字符集编码 Latin1(不支持中文!)** + +在配置文件 my.ini 中配置默认编码 `character-set-server=utf8` + +#### 2、修改表 + +关键字 ALTER + +```sql + --将表名user修改为account +ALTER TABLE user RENAME AS account +--添加字段 age +ALTER TABLE user ADD age INT(10) +--修改字段 (修改类型与约束) +ALTER TABLE user MODIFY age VARCHAR(10) +--修改字段 (修改字段名) +ALTER TABLE user CHANGE age age1 INT(10) +--删除字段 +ALTER TABLE user DROP age +``` + +#### 3、删除表 + +```sql +DROP TABLE IF RXISTS user +``` + +## 3、MySQL 数据管理 + +### 3.1、外键(极少用) + +定义外键 key + +添加约束(执行引用) references 引用 + +这里创建一个角色表 role,字段有 roleid,rolename,下为创建表时添加外键例子 + +``` +KEY `FK_roleid` (`roleid`), +CONSTRAINT `FK_roleid` FOREIGN KEY(`roleid`) REFEREBCES `role`(`roleid`) +``` + +删除带有外键关系表时,必须先删除引用别的表(从表),再删除被引用的表(主表) + +上面用户与角色关系表中,角色表就是无法直接删除,需删除用户表才可删除角色表。 + +可直接用 ALTER 添加外键关系 + +``` +ALTER TABLE `user` +ADD CONSTRAINT `FK_roleid` FOREIGN KEY(`roleid`) REFEREBCES `role`(`roleid`) + +ALTER TABLE `表名` +ADD CONSTRAINT 约束名 FOREIGN KEY(`外键`) REFEREBCES `引用表`(`引用字段`) +``` + +以上操作都是物理外键,数据库级别的外键,不建议使用!(虽然能保证数据完整性,但是寻找以及删除都是特别麻烦的,在数据量大的时候,异常痛苦,用过外键的都说坏) + +### 3.2、DML 语言 + +数据库意义:数据存储,数据管理 + +DML:数据库操作语言 + +- insert +- update +- delete + +#### 3.2.1、添加(insert) + +```sql +--语法 +INSERT INTO 表名([字段1,字段2,字段3]) VALUES ('值1'),('值2'),('值3') + +-- 插入数据 +INSERT INTO `role`(`rolename`) VALUES ('管理员') + +-- 插入多个数据 +INSERT INTO `role`(`rolename`) +VALUES ('管理员'),('代理'),('用户') +``` + +#### 3.2.2、更新(update) + +**注: 更新一定要带条件,不然就是所有数据都会更新!** + +```sql +--语法 +UPDATE 表名 SET `字段1`='值1' WHERE 条件1 + +-- 修改用户名与命名 +UPDATE `user` SET `username`='kuizuo',`password`='a12345678` WHERE id = 1 +``` + +#### 3.2.3、删除(delete) + +##### DELETE 命令 + +```sql +--删除数据 条件 +DELETE FROM `user` WHERE id = 1; --少了条件,直接全删 +--删除全部数据 +DELETE FROM `user` +``` + +##### TRUNCATE 命令(要全部删除用这个命令) + +作用:完全清空一个数据库表,表的结构和索引约束不会变。 + +使用:`TRUNCATE 表名` 即可 + +好处:删除后,会刷新自增值(置为 0),而 DELETE 不影响自增值,为上一自增值 + +### 3.3、DQL 语言 + +(Data Query Language:数据查询语言) + +数据库中最核心的语言,使用频率最高的语句。 + +完整语法 + +```sql +SELECT + [ALL | DISTINCT | DISTINCTROW ] + [HIGH_PRIORITY] + [STRAIGHT_JOIN] + [SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT] + [SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS] + select_expr [, select_expr ...] + [ + + FROM table_references + [PARTITION partition_list] + [WHERE where_condition] + [GROUP BY {col_name | expr | position} + [ASC | DESC], ... [WITH ROLLUP]] + [HAVING where_condition] + [ORDER BY {col_name | expr | position} + [ASC | DESC], ...] + [LIMIT {[offset,] row_count | row_count OFFSET offset}] + [PROCEDURE procedure_name(argument_list)] + [INTO OUTFILE 'file_name' + [CHARACTER SET charset_name] + export_options + | INTO DUMPFILE 'file_name' + | INTO var_name [, var_name]] + [FOR UPDATE | LOCK IN SHARE MODE] + ] +``` + +#### 3.3.1、指定查询 + +==注: 字段名也不区分大小写== + +```sql +-- 查询所有字段 +SELECT * FROM `user` + +-- 查询指定字段 +SELECT `username` FROM `user` + +-- 使用别名 +SELECT `username` AS 用户名 FROM `user` AS u + +-- 使用函数 concat(a,b) 拼接两者字符串 +SELECT CONCAT('用户名: ',`username`) AS 新用户名 FROM `user` AS u + +-- 去重 distinct +SELECT DISTINCT `StudentNo` AS 学号 FROM result --去重重复数据 只显示一条 +``` + +#### 3.3.2、表达式 + +数据库中的表达式: 文本值,列,NULL,函数,计算表达式,系统变量… + +```sql +SELECT VERSION() --查询系统版本(函数) +SELECT 100*2-123 AS 结果 --用于计算(计算表达式) +SELECT @@auto_increment_increment --查询自增的步长 变量 +SELECT now() --查询当前时间 +``` + +#### 3.3.3、where 条件子句 + +==尽量使用英文符号== + +基本运算符 + +| 运算符 | 语法 | 描述 | +| ------- | -------------- | ---- | +| and && | a and b a && b | 与 | +| or \|\| | a orb a \|\| b | 或 | +| Not ! | not a ! a | 非 | + +模糊查询 + +| 运算符 | 语法 | 描述 | +| ----------- | --------------- | --------------------------- | +| BETWEEN | between … and … | 在两者之间 | +| IS NULL | a is null | 如果为 null,结果为真 | +| IS NOT NULL | a is not null | 如果不为 null,结果为真 | +| **Like** | **a like b** | **a 匹配到 b,结果为真** | +| In | a in (a1,a2,a3) | 匹配 a 在 a1,a2,a3 其中之一 | + +其中 like 还搭配了 %(0 到任意个字符) \_(一个字符) 使用 + +#### 3.3.4、联表查询(重点) + +一共有 7 中 JOIN 查询 + +![img](https://img.kuizuo.cn/20201009150524563.png) + +实际上用的最多的也就是以下三种,区别如下 + +| 操作 | 描述 | +| ---------- | -------------------------------------- | +| inner join | 如果表中至少有一个匹配,就返回该行 | +| left join | 会返回左表中所有数据,即使右表没有匹配 | +| right join | 会返回右表中所有数据,即使左表没有匹配 | + +```sql + +-- 查询用户所属角色 +SELECT u.*,r.role +FROM `user` u + LEFT JOIN user_role ur ON u.id = ur.user_id + LEFT JOIN role r ON r.id = ur.user_id +WHERE + u.id = 1 + +-- 查询登录日志 +SELECT l.login_time +FROM kz_user u + LEFT JOIN kz_login_log l ON l.user_id = u.id + +``` + +#### 3.3.5、自连接查询 + +自己的表和自己的表连接,核心:**将一张表拆分为两张一样的表**,本质还是同一张表 + +一张表中对应了子表,父表,并通过 pid 来标注,下为相关表结构 + +| menu_id | pid | menu_name | +| ------- | --- | ------------ | +| 1 | 0 | 首页 | +| 2 | 0 | 用户管理 | +| 3 | 2 | 用户列表 | +| 4 | 2 | 角色管理 | +| 5 | 2 | 用户角色管理 | +| 6 | 0 | 卡密管理 | +| 7 | 6 | 卡密列表 | +| 8 | 6 | 卡密购买 | + +所查询的 sql 语句 + +```sql +SELECT a.`menu_name` AS 主菜单, b.`menu_name` AS 子菜单 +FROM `menu` AS a, `menu` AS b +WHERE a.`menu_id` = b.`pid` +``` + +查询结果如下 + +| 主菜单 | 子菜单 | +| -------- | ------------ | +| 用户管理 | 用户列表 | +| 用户管理 | 角色管理 | +| 用户管理 | 用户角色管理 | +| 卡密管理 | 卡密列表 | +| 卡密管理 | 卡密购买 | + +#### 3.3.6、分页和排序 + +关键字 `limit` 和 `order by`,注:limit 最后使用 + +排序语法: ORDER BY 字段 排序类型 + +升序 ASC 降序 DESC + +分页语法:LIMIT 起始值,页面大小 + +假设当前页面需展示 10 条数据(变量 pageSize),那么 + +第一页数据 LiMIT 0,10 (1-1)\*10 + +第二页数据 LiMIT 10,10 (2-1)\*10 + +第三页数据 LiMIT 20,10 (3-1)\*10 + +**第 N 页数据 LIMIT (N-1)\*pageSize,pageSize** + +基于这样的原理,即可实现分页,大致过程如下 + +首先,接收到前端发送的分页请求,page 与 pageSize,那么与之对应的数据库查询语句为 + +``` +SELECT * FROM user +LIMIT (page-1)*pageSize,pageSize +``` + +总页数 = 数据总数/页面大小 + +#### 3.3.7、子查询 + +在 where 中,条件为固定的,想根据查询当前表的结果赋值到 where 条件中,则为子查询,注:子查询多数下查询速度较慢 + +**本质:在 where 语句中嵌套子查询语句** + +子查询用的少,联表查询用的多。 + +```sql +SELECT * FROM kz_user +WHERE id +IN (SELECT user_id FROM kz_login_log WHERE login_time<=1609104740976) + +-- 查询登录时间小于1609104740976 的用户 +``` + +#### 3.3.8、分组查询 + +关键字 group by + +**注:group by 所要分组的字段,必须要在 select 中所选,且常搭配聚合函数所使用** + +```sql +select is_used ,count(*) as 数量 from kz_card group by is_used +-- 根据is_used 卡密是否使用分组 结果如 +``` + +| 是否使用 | 数量 | +| -------- | ---- | +| 0 | 10 | +| 1 | 3 | + +## 4、MySQL 函数 + +[官网地址](https://dev.mysql.com/doc/refman/8.0/en/functions.html) + +### 4.1、常用函数 + +数学运算 + +```sql +SELECT RAND() --返回0~1之间的随机数 +``` + +字符串 + +```sql +SELECT CHAR_LENGTH('这是一串文本') --返回字符串长度 +SELECT CONCAT('JavaScript','是世界上最好用的语言') --拼接字符串 +SELECT LOWER('Kuizuo') --到小写 +SELECT UPPER('Kuizuo') --到大写 +``` + +时间日期 + +```sql +SELECT CURRENT_DATE() --获取当前日期 +SELECT CURDATE() --获取当前时间 与上面等价 +SELECT NOW() --获取当前时间 +SELECT LOCALTIME() --本地时间 +SELECT SYSDATE() --系统时间 +``` + +系统 + +``` +SELECT SYSTEM_USER() -- 获取当前用户 +SELECT USER() -- 获取当前用户 root@localhost +SELECT VERSION() --获取当前版本 8.0.21 +``` + +### 4.2、聚合函数(用的多) + +| 函数名 | 描述 | +| --------- | -------- | +| **COUNT** | **计数** | +| SUM | 求和 | +| AVG | 平均值 | +| MAX | 最大值 | +| MIN | 最小值 | +| … | … | + +COUNT(列) —指定列,当值为 Null 不计数 + +COUNT(\*) —获取全部计数结果,不会忽略 NULL 值 + +COUNT(1) —忽略所有列,用 1 代表代码行,不会忽略 NULL 值 + +执行效率上: +列名为主键,count(列名)会比 count(1)快 +列名不为主键,count(1)会比 count(列名)快 +如果表多个列并且没有主键,则 count(1) 的执行效率优于 count(\*) +如果有主键,则 select count(主键)的执行效率是最优的 +如果表只有一个字段,则 select count(\*)最优。 + +> 参考链接 [count(1)、count(\*)与 count(列名)的执行区别](https://www.cnblogs.com/Memories-off/p/10435558.html) + +使用聚合函数,常常与分组 GROUP BY 和 HAVING 结合使用。 + +## 5、事务(Transaction) + +将一组 SQL 语句放在一个批次中去执行 + +### 5.1、事务原则 + +**ACID 原则 原子性(Atomicity),一致性(Consistency),隔离性(Isolation),持久性(Durability)** + +原子性:要么都成功,要么都失败 + +一致性:最终一致性,操作前与操作后的状态一致 + +隔离性:针对多个用户同时操作,主要排除其他事务对本次事务的影响 + +持久性:事务没有提交,恢复原状,事务已提交,持久化到数据库中,已提交就不可逆。 + +隔离所导致的一些问题: + +脏读:指一个事务读取了另外一个事务未提交的数据。 + +不可重复读:在一个事务内读取表中的某一行数据,多次读取结果不同。(这个不一定是错误,只是某些场合不对) + +虚读(幻读):是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。 + +> 参考链接 [事务 ACID 理解](https://blog.csdn.net/dengjili/article/details/82468576) + +### 5.2、MySQL 事务操作 + +mysql 是默认开始事务自动提交的,可通过下方设置开启关闭 + +```sql +SET autocommit = 0 —- 关闭 +SET autocommit =1 —- 开启(默认) + +-- 事务开启 +START TRANSACTION --之后的sql都在同一个事务中 + +INSERT xxx +UPDATE xxx + +-- 提交: 持久化(成功) +COMMIT +-- 回滚: 回到原来的样子(失败) +ROLLBACK + +-- 事务结束 + +-- 了解 +SAVEPOINT 保存点名 --设置一个事务的保存点 +ROLLBACK TO SAVEPOINT 保存点名 -- 回滚到保存点 +RELEASE SAVEPOINT 保存点名 -- 释放保存点 + +``` + +## 6、索引 + +索引(Index)是帮助 MySQL 高效获取数据的数据结构。 + +提取句子主干,就可也得到索引的本质:索引是数据结构 + +### 6.1、索引分类 + +一个表中,主键索引只能有一个,唯一索引可以有多个 + +- 主键索引(PRIMARY KEY) + + - 唯一的标识,主键不可重复,只能有一个列作为主键 + +- 唯一索引(UNIQUE KEY) + + - 避免重复的列出现,唯一索引可以重复,多个列,都可以标识为 唯一索引 + +- 常规索引(KEY/INDEX) + + - 默认的,index,key 关键字来设置 + +- 全文索引 (FULLTEXT) + - 在特定的数据库引擎下才有 + +### 6.2、索引的使用 + +``` +-- 显示所有的索引信息 +SHOW INDEX FROM 表名 + +-- 添加一个全文索引 索引名 字段名 +ALTER TABLE 表名 ADD FULLTEXT INDEX 索引名(字段名) + +-- EXPLAIN 分析sql执行的状况 +EXPLAIN SELECT * FROM student; -- 非全文索引 + +``` + +### 6.3、测试索引 + +插入 100 万数据,编写 mysql 函数 + +不过 mysql 的默认是不允许创建函数 + +在此之前需要执行一下 SET GLOBAL log_bin_trust_function_creators = 1; + +```sql +DELIMITER $$ -- 写函数之前必须要写,标志 +CREATE FUNCTION mock_data() +RETURNS INT +BEGIN + DECLARE num INT DEFAULT 1000; + DECLARE i INT DEFAULT 0; + + WHILE i物理磁盘位置 + + 导入 + + 登录情况下,USE 选择数据库 + + source D:/backup.sql + + 或者 + + mysql -u 用户名 –p 密码 库名 < 备份文件 + +## 9、数据库设计 + +当数据库比较复杂的时候,数据库设计显得尤为重要。 + +软件开发中,关于数据库的设计 + +1. 分析需求:分析业务和需要处理的数据库需求 +2. 概要设计:设计关系图 E-R 图 + +**设计数据库的步骤(个人博客为例):** + +1. 收集信息,分析需求 + 1. 用户表(用户登录注销,用户的个人信息) + 2. 分类表(文章分类) + 3. 文章表(文章信息,作者) + 4. 评论表 + 5. 友链表(友链信息) + 6. 自定义表(系统信息,某个关键的子,或者一些主字段) key:value +2. 标识实体之间的关系 + +### 9.1、三大范式 + +**第一范式(1NF):要求数据库表的每一列都是不可分割的原子数据项。** + +不然获取数据时,不好处理 + +**第二范式(2NF):在 1NF 的基础上,非码属性必须完全依赖于候选码(在 1NF 基础上消除非主属性对主码的部分函数依赖)** + +第二范式需要确保数据库表中的每一列都和主键相关,而不能只与主键的某一部分相关(主要针对联合主键而言)。 + +每张表只描述一件事情 + +**第三范式(3NF):在 2NF 基础上,任何非主属性不依赖于其它非主属性(在 2NF 基础上消除传递依赖)** + +第三范式需要确保数据表中的每一列数据都和主键直接相关(属性依赖主键),而不能间接相关。 + +[关系型数据库设计:三大范式的通俗理解](https://www.cnblogs.com/wsg25/p/9615100.html) + +**规范与性能问题** + +关联查询的表不得超过三张表 + +- 在考虑规范成本与用户体验上,数据库的性能更加重要 +- 故意给某些表添加一下冗余的字段,是多表查询变为单表查询。 + +## 10、数据库模型 + +在 Navicat 中,右键数据库,可逆向数据库到模型,模型的结果图如下 + +![image-20210102213536190](https://img.kuizuo.cn/image-20210102213536190.png) + +通过数据库模型,可以方便的分析该数据库中的关系,同时也可添加相应的数据等。 + +## 11、总结 + +简单花了两个晚上的时间刷一遍数据库的教程,并将其写成笔记总结,整体来说收获到的东西确实多,但是也有太多理论性的,例如三大范式,在考试的时候就考到过,然后我没背,但是我知道该如何规范,但就是不好表述。。。后续学习到 MongoDB 和 Redis 估计也要花点时间像这样子系统化的写个笔记,对知识巩固确实有帮助。 diff --git a/docs/skill/database/redis/index.md b/docs/skill/database/redis/index.md new file mode 100644 index 0000000..8adf082 --- /dev/null +++ b/docs/skill/database/redis/index.md @@ -0,0 +1,816 @@ +--- +id: redis-note +slug: /skill/database/redis +title: Redis笔记 +date: 2021-05-21 +tags: [redis, database] +keywords: [redis, database] +--- + +[redis 中文官方网站](http://www.redis.cn/) + +## 安装 + +官方推荐使用 Linux 去开发使用! + +### window + +下载地址:https://github.com/tporadowski/redis/releases + +下载 **Redis-x64-xxx.zip**压缩包 解压为 redis + +在 redis 目录下,打开 CMD 输入 或者双击运行 redis-server.exe + +```bash +redis-server.exe redis.windows.conf +``` + +在打开一个输入 + +```bash +redis-cli.exe -h 127.0.0.1 -p 6379 +``` + +即可连接 + +### Linux + +[redis 下载](http://redis.io/download) + +`redis-6.0.8.tar.gz` + +```bash +# wget http://download.redis.io/releases/redis-6.0.8.tar.gz +# tar xzf redis-6.0.8.tar.gz +# cd redis-6.0.8 + +# 安装gcc-c++ 编译 +yum instatll gcc-c++ +# make +``` + +执行完 **make** 命令后,redis-6.0.8 的 **src** 目录下会出现编译后的 redis 服务程序 redis-server,还有用于测试的客户端程序 redis-cli + +下面启动 redis 服务 + +```bash +# cd src +# ./redis-server +``` + +注意这种方式启动 redis 使用的是默认配置。也可以通过启动参数告诉 redis 使用指定配置文件使用下面命令启动。 + +```bash +# cd src +# ./redis-server ../redis.conf +``` + +redis 默认安装路径 `/usr/local/bin` + +### Docker + +#### 拉取镜像 + +```bash +docker pull redis +``` + +#### 启动 Redis + +```bash +docker run -d -v $PWD/data:/data --name redis -p 6379:6379 redis redis-server --requirepass "123456" --appendonly yes +``` + +启动命令说明: + +- `$PWD/data:/data` : 映射 redis 的 data 目录到当前目录下的 data 目录 +- `--requirepass` : 是设置 redis 的密码 +- `--appendonly yes` : 启用持久化存储 + +例如: + +```bash +docker run -d -v /home/app/redis/data:/data --name redis -p 6379:6379 redis redis-server --requirepass "123456" --appendonly yes +``` + +如果需要使用配置文件,则需要做个文件映射;注意所在目录下必须要有 redis.conf 这个文件,否则将启动失败。 + +```bash +docker run -d -v /home/app/redis/data:/data -v /home/app/redis/conf:/usr/local/etc/redis --name redis -p 6379:6379 redis redis-server /usr/local/etc/redis/redis.conf +``` + +> redis 的这个配置文件可以到官方的这个地址上去获取 http://download.redis.io/redis-stable + +更多: [Docker 上安装 Redis](https://www.cnblogs.com/vchar/p/14347260.html) + +## 基本命令 + +[Redis 命令中心(Redis commands)](http://www.redis.cn/commands.html) + +Redis 不区分大小写 一般推荐大写(与 Mysql 一样) + +```bash +set key value + +get key + +keys * # 查看所有key + +EXISTS key # 判断key 是否存在 +type key # 查看key的value类型 + +EXPIRE key second # 设置key的过期时间,单位是秒 + +ttl key # 查看当前key 的剩余时间 +``` + +## 五大数据类型 + +Redis 支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及 zset(sorted set:有序集合)。 + +### String + +``` +APPEND key '123' # 给key后面追加字符串123 如果key不存在 则为set 返回字符串长度 + +STRLEN key # 获取字符串长度 + +incr key # 自增1 + +decr key # 自减1 + +INCRBY key 10 # 递增10 指定增量 + +DECRBY key 10 # 递减10 + +GETRANGE key 0 3 # 截取字符串 0-3 包括3 +GETRANGE key 0 -1 # 截取所有字符串 + +SETRANGE key 1 xxx # 替换指定位置的字符串 + +########################################################################### +setex (set with expire) # 设置过期时间 +setnx (set if not exist) # 不存在在设置 (分布式锁中会常常使用) + +setex key3 30 'hello' # 设置key3的为hello,30秒后过期 + +########################################################################### +mset mget msetex # 批量设置与批量获取 + +mset k1 v1 k2 v2 k3 v3 + +msetex k1 v1 k4 v4 # 原子性的操作 要么一起成功 要么一起失败 + +getset key value # 先取后设置 不存在则返回nil 如果存在,则获取,并赋为新值 +############################################################################ +# 对象 +set user:1 {name:kuizuo,age:20} # 设置user为一个对象 +# or +set user:1:name kuizuo +# user:{id}:{filed} value + +get user:1 +# or +get user:1:name + +``` + +String 类似的使用场景: value 除了是字符串还可以是数字 或者对象 + +### List + +redis 里 List 可以充当栈,队列,阻塞队列 + +**所有 list 命令用 l 开头** + +```bash +LPUSH list value # 将value 将一个值或多个值插入列表头部(左) + +RPUSH list value # 将value 将一个值或多个值插入列表底部(右) + +LRANGE list 0 -1 # 获取所有list元素 + +LPOP list # 移除list的第一个元素(左) + +RPOP list # 移除list的最后一个元素(右) + +Lindex list 1 # 通过下标获取list中的某一个值 + +Lset list 0 item # 如果不存在列表 去更新就会报错 + +Llen list # 取列表的长度 + +Lrem list 1 one # 移除指定的值 例:移除一个为one的 + +Ltrim list 1 2 # 截取1-2 包括2 + +Linsert list before "world" "new" # 在world 前面插入new 后面则用after + + +rpoplpush list1 list2 # 移除列表最后一个元素,将他移动到新的列表 +``` + +列表实际上就是一个链表 + +可以实现消息队列 (Lpush Rpop),栈(Lpush Lpop) + +### Set + +set 中的值是无法重复的,无序不重复集合 + +**set 命令用 s 开头** + +```bash +sadd myset "hello" # set集合中添加元素 + +scard myset # 获取set集合中的内容元素个数 + +smembers myset # 查看指定set的所有值 + +sismember myset hello # 判断某一个值是不是在set集合中 + +SRANDMEMBER myset # 随机抽选出一个元素 +SRANDMEMBER myset 2 # 随机抽选出指定个数元素 +##################################################################### +# 获取set中的差集 +SDIFF set1 set2 + +# 获取set中的交集 +SINTER set1 set2 + +# 获取set中的并集 +SUNION set1 set2 +``` + +例如:共同好友就可以使用 set 交集来实现 + +### Hash + +Map 集合,key-map(key-value) + +**set 命令用 h 开头** + +```bash +hset myhash field1 kuizuo + +hget myhash field1 + +hgetall myhash + +hdel myhash + +hlen myhash # 获取hash表的字段数量 + +HEXISTS myhash field1 # 判断hash中 指定字段是否存在 + +Hkeys myhash # 只获得所有field + +Hvals myhash # 只获得所有value +``` + +hash 可变更数据 比如 user 信息,更适合对象的存储 + +### Zset + +有序集合,在 set 的基础上增加了一个值 score + +**zset 命令用 z 开头** + +```bash +zadd myset 1 one + +zadd myset 2 two 3 three + +# ZRANGEBYSCORE key min max 一定要从小到大 +ZRANGEBYSCORE myset -inf +inf # 根据score排序 + +ZREVERANGE myset 0 -1 # 从大到小进行排序! + +Zrem myset item # 移除有序集合中的指定元素 + +Zcard myset # 获取有序集合中元素的个数 + + +``` + +案例:set 排序 班级成绩表,工资表排序 + +普通消息 1 重要消息 2 带权重进行判断 + +排行榜应用实现,取 TOP N + +## 三种特殊数据类型 + +### geospatial + +地址位置,**geospatial 命令用 geo 开头** + +**GEO 底层的实现原理就是 Zset,所以可以使用 Zset 命令来操作 Geo!** + +应用: 推算地理位置的信息,两地之间的距离,方圆几里的人 + +```bash +# 规则: 两极无法直接添加,一般都是直接下载城市数据,直接通过程序一次性读入 +# 参数: key (经度,纬度、名称) 切记不可反! 经纬度 +# 有效经度-180度到180度 有效纬度-85.05112878到85.05112878 +GEOADD china:city 116.40 39.90 beijin # 设置北京的经纬度 + +GEOPOS china:city beijing # 获取北京的经纬度 + +GEODIST china:city beijing shanghai unit # 获取两地之间的距离 默认单位m + +GEORADIS china:city 110 30 1000 km # 以110,30 这个点范围1000km的 地理位置 +GEORADIS china:city 110 30 500 km withdist withcoord count 10 # 以110,30 这个点范围500km的 获取10个 带直线距离和经纬度 + +GEORADIUSBYMEMBER chaina:city beijing 1000m # 以北京周围1000km的 地理位置 + +GEOHASH china:city beijing # 将二维的地址位置转为一位11位字符串,如果两个字符串越接近,则距离越近 + +ZRANGE chaina:city 0 -1 # 查看地图中全部元素 + +ZREM chaina:city beijing # 移除指定元素 +``` + +### Hyperloglog + +Redis Hyperloglog 基数统计的算法 + +基数(不重复的元素),会有误差!0.81 的错误率,但使用场景是可以接受的 + +统计网页的 UV (一个人访问一个网站多次,但是还是算作一个人),传统的方式,用 set 保存用户的 id,然后统计 set 中的元素数量来作为标准判断。 + +**Hyperloglog 命令 使用 PF 开头** + +```bash +PFadd mykey1 a b c d e f g h i j +PFadd mykey2 i j k l m n o + +PFMERGE mykey3 mykey1 mykey2 # 获取并集 并生成新的组 + +PFCOUNT mykey # 获取元素的数量 +``` + +允许容错,一定可以使用 Hyperloglog + +不允许容错,就使用 set 与自己的数据类型即可 + +### Bitmaps + +位存储 + +统计用户信息,活跃,不活跃;登录,不登录,打卡;两个状态的都可以使用 Bitmaps + +Bitmaps 位图,数据结构,都是操作二进制为来进行记录,就只有 0 和 1 两个状态! + +``` +# 记录周一到周日的打卡 +setbit sign 0 1 +setbit sign 1 1 +setbit sign 2 1 +setbit sign 3 1 +setbit sign 4 1 +setbit sign 5 0 +setbit sign 6 0 + +# 查看某一天是否有打开 +getbit sign 3 + +# 统计打卡的天数 +bitcount sign +``` + +## 事务 + +Redis 单条命令式保存原子性的,但是事务不保证原子性! + +Redis 事务本质: 一组命令的集合!一个事务中的所有命令都会被序列化,会安卓顺序执行 + +一次性、顺序性、排他性!执行一些列的命令! + +Redis 事务没有隔离级别的概念! + +所有命令在事务中并没有直接呗执行!而只有发起执行命令的时候才会执行!Exec + +- 开始事务(multi) +- 命令入队(...) +- 执行事务(exec) + +``` +multi + +set k1 v1 +set k2 v2 + +get k1 + +exec # 执行事务 + +DISCARD # 取消事务 事务队列中的命令都不会被执行 + +# 代码有问题,命令有错,事务中所有命令都不会被执行 +# 运行中异常,执行其他命令正常,错误命令抛出异常 +``` + +### 监控 Watch + +悲观锁:很悲观,认为什么时候都会出问题,无论做什么都会加锁 + +乐观锁:很乐观,认为什么时候都不会出问题,所以不会上锁!更新数据的时候判断一下,在此期间是否有人修改过该数据 + +``` +set money 100 +set out 0 + +watch money + +multi + +DECRBY money 20 +InCRBY money 20 + +# 如果这时用户充钱了 那么exec就无法执行 +exec + +# 解除监控,并重新监控最新的值 +unwatch money +watch money + +``` + +## Redis.conf + +配置文件 unit 单位对大小写不敏感 + +##### 网络 + +``` +bind 127.0.0.1 # 绑定的IP + +protected-mode yes # 保护模式 + +port 6379 # 端口设置 +``` + +##### GENERAL 通用 + +``` +daemonize yes # 以守护进程方式的运行,默认是no,需自己开启yes + +pidfile /var/run/redis_6379.pid # 以后台方式运行,就需要指定一个pid文件 + +# Specify the server verbosity level. +# This can be one of: +# debug (a lot of information, useful for development/testing) +# verbose (many rarely useful info, but not a mess like the debug level) +# notice (moderately verbose, what you want in production probably) +# warning (only very important / critical messages are logged) +loglevel notice + +logfile # 日志文件位置名 + +database 16 # 数据库数量 默认16个 +alaways-show-logo yes # 是否总是显示LOGO +``` + +##### 快照 + +持久化,在规定的时间内,执行了多少次操作,则会持久化到文件 .rdb .aof + +``` +stop-writes-on-bgsave-error yessave 900 1 # 在900s内,如果至少有一个key修改 则持久化操作 +save 300 10 # 在300s内,如果至少有10个key进行修改 则持久化操作 +save 60 10000 + +stop-writes-on-bgsave-error yes # 持久化如果出错 是否继续工作 + +rdbcompression yes # 是否压缩rdb文件,会消耗一些cpu资源 +rdbchecksum yes # 保存rdb文件的时候,是否效验 + +dir ./ # rdb保存的目录 + +dbfilename dump.rdb # 保存的文件名 +``` + +##### REPLICATION 主从复制 + +``` +slaveof # 设置主机的端口和ip +``` + +##### SECURITY 密码 + +requirepass 密码 + +``` +config get requirepass + +config set requirepass "123456" + +auth 123456 +``` + +##### CLIENTS 客户端 + +``` +maxclients 10000 # 默认客户端连接数 + +maxmemory # redis 配置最大的内存容量 + +maxmemory-policy noeviction # 内存达到上限后的处理策略 # 移除一些过期的key + +noeviction: 不删除策略, 达到最大内存限制时, 如果需要更多内存, 直接返回错误信息。(默认值) +allkeys-lru: 所有key通用; 优先删除最近最少使用(less recently used ,LRU) 的 key。 +volatile-lru: 只限于设置了 expire 的部分; 优先删除最近最少使用(less recently used ,LRU) 的 key。 +allkeys-random: 所有key通用; 随机删除一部分 key。 +volatile-random: 只限于设置了 expire 的部分; 随机删除一部分 key。 +volatile-ttl: 只限于设置了 expire 的部分; 优先删除剩余时间(time to live,TTL) 短的key。 +``` + +##### APPEND ONLY MODE aof 配置 + +``` +appendonly no # 默认不开启aof模式,默认使用rdb方式持久化,在大部分情况,rdb够用 +appendfilename "appendonly.aof" # 持久化文件名 + +# appendfsync always # 每次修改都会 sync 消耗性能 +appendfsync everysec # 美妙执行一次 sync,可能会丢失这1s的数据! +# appendfsync no # 不执行sync,这个时候操作系统自己同步数据,速度最快 + +``` + +## Redis 持久化 + +redis 是内存数据库,单如果不将内存中的数据库状态保存的磁盘,一旦服务器进程退出,服务器中的数据库状态也会丢失,所以 redis 提供了持久化的功能 + +### RDB(Redis Database) + +在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是 Snapshot 快照,恢复时直接将快照文件读到内存 + +Redis 会单独创建 ( fork )一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何 IO 操作的。这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效。RDB 的缺点是最后一次持久化后的数据可能丢失。我们默认的就是 RDB,一般情况下不需要修改这个配置! + +**rdb 保存的文件是 dump.rdb** + +dbfilename dump.rdb + +#### 触发机制 + +1、save 的规则满足的情况下,会自动触发 rdb 规则 + +2、执行 flushall 命令,也会触发我们的 rdb 规则! + +3、退出 redis,也会产生 rdb 文件! + +就会自动生成一个 dump.rdb,有时候还有备份一份 + +#### 恢复 rdb 文件 + +只需要将 rdb 文件放在 redis 启动目录下就可以了,redis 启动的时候就会自动检查 dump.rdb 文件 + +``` +config get dir +"dir" +"/usr/local/bin" # 如果在这个目录下存在dump.rdb 启动就会自动恢复其中的数据 +``` + +优点: + +1、适合大规模的数据恢复 + +2、对数据的完整性要求不高 + +缺点: + +1、需要一定的时间间隔进程操作,如果 redis 意外宕机了,这个最后一次修改数据就没有了 + +2、fork 进程的时候,会占用一定的内存空间 + +### AOF(Append Only File) + +将我们的所有命令都记录下来,history,恢复的时候就把这个文件内的命令全部在执行一遍 + +默认是不开启的,我们需要手动进行配置!我们只需要将 appendonly 改为 yes 就开启了 aof !重启 + +redis 就可以生效了 + +如果这个 aof 文件有错误,这时候 redis 是启动不起来的,我们需要修复这个 aof 文件,redis 给我们提供了一个工具 redis-check-aof --fix + +优点 + +可指定修改都同步还是每秒都同步,文件完整性会更好 + +缺点: + +相对于数据文件,aof 远远大于 rdb,修复的速度也不如 rdb + +Aof 的运行效率也要比 rdb 慢。 + +### 小结 + +只做缓存,如果你只希望你的数据在服务器运行的时候存在,也可以不使用任何持久化 + +## Redis 发布订阅 + +Redis 发布订阅(publsub)是一种消息通信模式 ∶ 发送者(pub)发送消息,订阅者(sub)接收消息。Redis 客户端可以订阅任意数量的频道。订阅/发布消息图︰ + +第一个:消息发布者,第二个频道(消息队列),第三个:消息订阅者 + +![查看源图像](http://cdn.kuizuo.cn/blogR50ea35ec36a3e4ea16cb132637477df0) + +### 测试 + +订阅端 + +``` +SUBSCRIBE kuizuo # 创建频道 + + + +``` + +发送端 + +``` +PUBLISH kuizuo ‘hello‘ + + +``` + +### 原理 + +Redis 是使用 C 实现的,通过分析 Redis 源码里的 pubsub.c 文件,了解发布和订阅机制的底层实现,籍此加深对 Redis 的理解。Redis 通过 PUBLISH、SUBSCRIBE 和 PSUBSCRIBE 等命令实现发布和订阅功能。微信 ∶ 通过 SUBSCRIBE 命令订阅某频道后,redis-server 里维护了一个字典,字典的键就是一个个频道!,而字典的值则是一个链表,链表中保存了所有订阅这个 channel 的客户端。SUBSCRIBE 命令的关键,就是将客户端添加到给定 channel 的订阅链表中。通过 PUBLSH 命令向订阅者发送消息,redis-server 会使用给定的频道作为键,在它所维护的 channel 字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。Pub/Sub 从字面上理解就是发布( Publish)与订阅( Subscribe ),在 Redis 中,你可以设定对某一个 key 值进行消息发布及消息订阅,当一个 key 值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。 + +使用场景: + +1、实时消息系统,公告 + +2、实时聊天(将频道当做聊天室,将信息回显给所有人) + +3、订阅,关注系统都是可以的 + +## Redis 主从复制 + +### 概念 + +主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为主节点(master/leader),后者称为从节点(slave/follower);数据的复制是单向的,只能由主节点到从节点。Master 以写为主,Slave 以读为主。默认情况下,每台 Redis 服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。 + +### 主从复制的作用主要包括︰ + +1、数据冗余 ∶ 主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。2、故障恢复︰当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。3、负载均衡︰在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写 Redis 数据时应用连接主节点,读 Redis 数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。4、高可用基石 ∶ 除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是 Redis 高可用的基础。 + +一般来说,要将 Redis 运用于工程项目中,只使用一台 Redis 是万万不能的 + +原因如下︰1、从结构上,单个 Redis 服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大; 2、从容量上,单个 Redis 服务器内存容量有限,就算一台 Redis 服务器内存容量为 256G,也不能将所有内存用作 Redis 存储内存,一般来说,**单台 Redis 最大使用内存不应该超过 20G。** + +主从复制,读写分离!80%的情况下都是在进行读操作,就可以减缓服务器压力,架构中经常使用,一主二从 + +### 环境配置 + +``` +redis:0>info replication # 查看当前库信息 +"# Replication +role:master # 角色 +connected_slaves:0 # 没有从机 +master_replid:d6950e2fdb86b591f42e8725279034303e8cb6ee +master_replid2:0000000000000000000000000000000000000000 +master_repl_offset:0 +second_repl_offset:-1 +repl_backlog_active:0 +repl_backlog_size:1048576 +repl_backlog_first_byte_offset:0 +repl_backlog_histlen:0 + +``` + +需要修改 配置文件 + +端口 pid 名字 log 文件名 dump.rdb 名 + +### 一主二从 + +默认情况下,每台 Redis 服务器都是主节点;我们一般情况下只用配置从机就好了!认老大!一主( 79)二从( 80,81 ) + +``` +SLAVEOF 127.0.0.1 6379 # 找谁为主机 +``` + +**主机可以写,从机不能写只能读!**主机中的所有数据都会被从机保存 + +主机断开连接,从机依旧可以连接到主机,并且主机回来,从机依旧可以直接获取数据 + +从机重新连接会主机,主机的任何操作立马同步到从机上 + +### 复制原理 + +slave 启动成功连接到 master 后会发送一个 sync 命令 Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,**master 将传送整个数据文件到 slave,并完成一次完全同步。** + +全量复制:slave 服务在接收到数据库文件数据后,将其存盘并加载到内存中。 + +增量复制:Master 继续将新的所有收集到的修改命令依次传给 slave,完成同步但是只要是重新连接 master,一次完全同步(全量复制)将被自动执行 + +### 层层链路 + +上一个 M 链接下一个 S + +## 哨兵模式 + +[Redis 哨兵(Sentinel)模式](https://www.jianshu.com/p/06ab9daf921d) + +### 概述 + +如果主机宕机了,从机要当主机,通过命令`SLAVEOF no one` 从机变主机,但如果这时候主机恢复了,那么就需要重新配置了,十分麻烦 + +主从切换技术的方法是 ∶ 当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis 从 2.8 开始正式提供了 Sentinel (哨兵)架构来解决这个问题。 + +后台能够监控主机是否故障,如果故障了根据投票数自动将从机变为主机 + +### 实现 + +哨兵模式是一种特殊的模式,首先 Redis 提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是**哨兵通过发送命令,等待 Redis 服务器响应,从而监控运行的多个 Redis 实例。** + +![img](http://cdn.kuizuo.cn/blog11320039-57a77ca2757d0924.png) + +### 配置 + +1、哨兵配置文件 sentinel.conf + +``` +sentinel monitor myredis 127.0.0.1 6379 1 +``` + +后面的这个数字 1,代表主机挂了,slave 投票看让谁当主机,票数多的就会成为主机 + +2、启动哨兵 + +``` +redis-sentinel config/sentinel.conf +``` + +3、主机挂了,从机当主机了,但是如果原主机恢复了,也只能乖乖当新主机的从机 + +优点: 1、哨兵集群,基于主从复制模式,所有的主从配置优点,它全有 + +2、主从可以切换,故障可以转移,系统的可用性就会更好 + +3、哨兵模式就是主从模式的升级,手动到自动,更加健壮 + +缺点: + +1、Redis 不好啊在线扩容的,集群容量一旦到达上限,在线扩容就十分麻烦 + +2、实现哨兵模式的配置其实是很麻烦的,里面有很多选择 + +## Redis 缓存穿透和雪崩 + +**服务器高可用的问题** + +Redis 缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,那么就不能使用缓存。另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界也都有比较流行的解决方案。 + +### 缓存穿透 + +#### 概念 + +缓存查不到,导致数据都查数据库 + +缓存穿透的概念很简单,用户想要查询一个数据,发现 redis 内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中(秒杀!),于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。| + +#### 解决方案 + +##### 布隆过滤器 + +布隆过滤器是一种数据结构,对所有可能查询的参数以 hash 形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力 + +##### 缓存空对象 + +当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源; + +但是这种方法会存在两个问题: 1、如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键; 2、即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。 + +### 缓存击穿 + +#### 概述 + +全都查缓存,此时缓存恰好过期,导致量过大 + +这里需要注意和缓存击穿的区别,缓存击穿,是指一个 key 非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个 key 在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。当某个 key 在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。 + +#### 解决方案 + +##### 设置热点数据永不过期 + +从缓存层面来看,没有设置过期时间,所以不会出现热点 key 过期后产生的问题。 + +##### 加互斥锁 + +分布式锁 ∶ 使用分布式锁,保证对于每个 key 同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。 + +### 缓存雪崩 + +#### 概述 + +指在某一个时间段,缓存集中过期失效。Redis 宕机! + +产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。 + +### 基本解决方案 + +redis 高可用这个思想的含义是,既然 redis 有可能挂掉,那我多增设几台 redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。(异地多活!) 限流降级(在 SpringCloud 讲解过!) 这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存,其他线程等待。数据预热数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的 key,设置不同的过期时间,让缓存失效的时间点尽量均匀。 diff --git a/docs/skill/docker/Docker Compose.md b/docs/skill/docker/Docker Compose.md new file mode 100644 index 0000000..128e3fd --- /dev/null +++ b/docs/skill/docker/Docker Compose.md @@ -0,0 +1,77 @@ +--- +id: docker-compose +slug: /docker-compose +title: Docker Compose +date: 2021-05-26 +authors: kuizuo +tags: [docker] +keywords: [docker] +--- + + + +## 简介 + +dockerfile 能让程序在任何地方运行 比如 web 服务 redis mysql nginx 但需要启动多个容器 并且都需要 run 一下 ,而通过 Docker Compose 则可以一键完成上面任务 实现自动化部署 + +**一句话:将多个容器融合在一起** + +## 安装 + +前提需要安装 docker + +1、下载 + +```bash +sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + +# 上为官方的地址 可能有些慢 下为daocloud +sudo curl -L https://get.daocloud.io/docker/compose/releases/download/1.25.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose + +``` + +2、授权文件权限 + +```bash +sudo chmod +x /usr/local/bin/docker-compose +``` + +3、测试安装结果 + +``` +docker-compose --version +``` + +## 使用 + +``` +docker-compose up +``` + +### YAML 规则 + +[Compose file version 3 reference | Docker Documentation](https://docs.docker.com/compose/compose-file/compose-file-v3/#compose-file-structure-and-examples) + +`Compose` 中有两个重要的概念: + +- 服务 (`service`):一个应用的容器,实际上可以包括若干运行相同镜像的容器实例。 +- 项目 (`project`):由一组关联的应用容器组成的一个完整业务单元,在 `docker-compose.yml` 文件中定义。 + +一个简单的 YAML 配置文件就像下面这样。 + +```yaml +version: '3' # compose版本 根据docker的版本来匹配 + +services: # 服务 + 服务1: + # 服务配置 + images: + build: + ports: + network: + environment: + depends_on: + 服务2: +networks: +volumes: +``` diff --git a/docs/skill/docker/Docker.md b/docs/skill/docker/Docker.md new file mode 100644 index 0000000..7136ad1 --- /dev/null +++ b/docs/skill/docker/Docker.md @@ -0,0 +1,363 @@ +--- +id: docker +slug: /docker +title: Docker笔记 +date: 2021-05-26 +authors: kuizuo +tags: [docker] +keywords: [docker] +--- + + + +[官方文档](https://docs.docker.com/engine/install/centos/) + +[Docker — 从入门到实践 (gitbook.io)](https://yeasy.gitbook.io/docker_practice/) + +## 安装 + +```bash +# 删除旧的版本 +yum remove docker \ + docker-client \ + docker-client-latest \ + docker-common \ + docker-latest \ + docker-latest-logrotate \ + docker-logrotate \ + docker-engine + +# 需要的安装包 +yum install -y yum-utils + +# 设置镜像仓库 下面为阿里云的 +yum-config-manager \ + --add-repo \ + https://download.docker.com/linux/centos/docker-ce.repo #默认是国外的 + +yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo + +# 更新yum软件包索引 +yum makecache fast + +# 安装docker 引擎 +yum install docker-ce docker-ce-cli containerd.io + +# 启动docker +systemctl start docker + +docker version #查看版本是否安装成功 + +docker run hello-world #运行该镜像 如果没有将会拉去官方镜像 + +docker images # 查看已有镜像 + +# 卸载docker +# 卸载依赖 +yum remove docker-ce docker-ce-cli containerd.io + +#删除资源 +sudo rm -rf /var/lib/docker +sudo rm -rf /var/lib/containerd +``` + +/var/liv/docker docker 在宿主机的默认工作路径 + +## 配置阿里云镜像加速 + +登录阿里云 找到容器镜像服务,按照下图命令复制粘贴即可 + +![](https://img.kuizuo.cn/image-20210527011655512.png) + +## Docker 的命令 + +![](https://img.kuizuo.cn/v2-820aee2a33654099d87cdd2b7a1ce741_r.jpg) + +```bash +docker info # 显示docker 系统信息 +docker stats # 显示docker 所占用的资源 +docker --help # 查看帮助 +``` + +### 镜像命令 + +```bash +#查看本地主机上的镜像 +docker images + +[root@localhost ~]# docker images +REPOSITORY TAG IMAGE ID CREATED SIZE +hello-world latest d1165f221234 3 weeks ago 13.3kB + +REPOSITORY 仓库源 +TAG 标签 一般为版本号 +IMAGE ID id +CREATED 创建时间 +SIZE 大小 + +-a 显示全部 +-q 只显示id + +docker search 镜像名 # 搜索镜像 +docker pull 镜像名:[TAG] # 下载镜像 +docker rmi 镜像ID # 删除镜像 -f 强制删除 +docker rmi 镜像ID1 镜像ID2 镜像ID3 # 删除多个镜像 通过空格 + +docker rmi -f $(docker images -aq) # 删除全部镜像 +``` + +### 容器命令 + +得先有了镜像才可创建容器 + +安装一个 centos 容器 docker pull centos + +#### 启动容器 + +```bash +docker run [参数] image +# 参数说明 +--name="名字" 指定容器名字 +-d 后台方式运行 +-it 交互方式运行,可进入容器查看内容 +-p 指定容器的端口 +-p 主机端口:容器端口 +-v 宿主机路径:容器内路径 数据卷 +``` + +#### 查看容器 + +```bash +docker ps 命令 + +-a #所有+历史运行过的容器 +-n=? #最近创建的容器 +-q #只显示容器的编号 + +``` + +#### 退出容器 + +```bash +exit #直接停止并退出 +Ctrl + P + Q #不停止退出 +``` + +#### 删除容器 + +注意 没有`i` + +```bash +docker rm 容器id #删除指定的容器 +docker rm -f $(docker ps -aq) # 删除所有的容器 +``` + +#### 启动和停止容器的操作 + +```bash +docker start 容器id +docker restart 容器id +docker stop 容器id +docker kill 容器id +``` + +#### 进入当前正在运行的容器 + +```bash +docker exec -it 容器id /bin/bash #进入后开启新的终端 可在里面操作(常用) +docker attach 容器id # 不会启动新的进程 单单只是进入容器的终端 +``` + +#### 后台启动容器 + +``` +docker run -d 容器 +docker run -d centos +docker ps +# 没有容器的数据 发现centos 停止了 + +``` + +常见的坑 docker 容器使用后台运行 就必须要有一个前台应用,否则将会自动停止 nginx 容器启动后 发现自己没有提供服务 就会立刻停止 **就是没有程序了** + +#### 查看容器内的进程信息 + +```bash +docker top 容器id +``` + +#### 查看容器的元数据 + +``` +docker inspect 容器id +``` + +#### 从容器内拷贝文件到宿主机上 + +```bash +docker cp 容器id:容器内路径 宿主机路径 +``` + +### 自定义网络 + +```bash +docker network ls #查看所有的docker 网络 + +docker network create --driver bridge mynet + +创建容器通过 `--net` 默认为 --net bridge + +docker network connect # 连通网络 +``` + +### 容器数据卷 + +一句话 容器的持久化和同步操作! 容器间 也是可以数据共享的 + +#### 使用数据卷 + +```bash +docker run -it -v 主机目录:容器目录 +``` + +#### 指定路径挂载 + +注意 路径前有`/` 为绝对路径 + +#### 匿名挂载 + +只指定容器内的名字 + +```bash +docker run -d -P --name nginx -v /ect/nginx nginx + +通过 docker volume ls 即可查看 +为local ..... +``` + +#### 具名挂载 + +```bash +docker run -d -P --name nginx -v mynginx:/ect/nginx nginx + +# mynginx 为卷名 + +docker volume inspect mynginx 可查看挂载位置 +没指定目录 都是在 /var/lib/docker/volumes/卷名/_data + +docker volume ls +local mynginx +``` + +区别 + +```bash +-v 容器内路径 #匿名挂载 +-v 卷名:容器内路径 #具名 +-v /宿主机路径:容器内路径 #指定路径 +``` + +拓展 + +``` +-v 容器内路径:ro rw +ro 表示只读 readonly 只可外部改变 只可宿主机改变 +rw 可读写 readwirte 默认rw +``` + +#### 例子 + +安装Mysql + +```bash +docker run -d -p 3307:3306 --privileged=true -v /data/mysql/log:/var/log/mysql -v /data/mysql/data:/var/lib/mysql -v /opt/docker/mysql/conf:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=123456 --name mysql mysql:5.7 +``` + +安装Redis + +``` +docker run -d -p 6379:6379 --privileged=true -v /app/redis/redis.conf:/etc/redis/redis.conf -v /app/redis/data:/data -e MYSQL_ROOT_PASSWORD=123456 --name mysql mysql:5.7 redos-server /etc/redis/redis.conf +``` + +## DockerFile + +![](https://img.kuizuo.cn/OIP.p3NmHHlewBvLwukFPGudFgHaFV.jpg) + +所有命令大小写不敏感(但推荐大写) + +构建镜像命令 + +```bash +docker build -t 自定镜像名 . +``` + +例:创建一个属于自己的 centos 镜像 + +```dockerfile +FROM cetnos +MAINTAINER kuizuo<911993023@qq.com> + +ENV MYPATH /usr/local +WORKDIR $MYPATH + +RUN yum -y install vim +RUN yum -y install net-tools + +EXPOSE 80 +CMD /bin/bash +``` + +通过 docker history 镜像 ID 可以查看 镜像的变更历史 + +CMD 和 ENTRYPOINT 区别 + +``` +CMD # 指定这个容器启动的时候要运行的命令,只有最后一个会生效,可被替代 +ENTRYPOINT # 指定这个容器启动的时候要运行的命令,可以追加命令 直接拼接的形 +``` + +## 发布镜像 + +### Commit 镜像 + +```dockerfile +# 命令和git原理很像 +docker commit -m="描述信息" -a="作者" 容器id 自定镜像名:[TAG] + +即可在本地生成一个属于自己的镜像文件 +``` + +### 发布 + +1、登录[Docker Hub](https://hub.docker.com/) 注册一个账号 + +2、docker login -u kuizuo + +3、输入密码 + +``` +[root@localhost ~]# docker login -u kuizuo +Password: +Error response from daemon: Get https://registry-1.docker.io/v2/: unauthorized: incorrect username or password +[root@localhost ~]# docker login -u kuizuo +Password: +WARNING! Your password will be stored unencrypted in /root/.docker/config.json. +Configure a credential helper to remove this warning. See +https://docs.docker.com/engine/reference/commandline/login/#credentials-store + +Login Succeeded +``` + +4、docker push 镜像 ID 镜像名:[Tag] + +5、有可能提交不上 需要修改下属 docker tag 镜像名 + +### 部署到阿里云容器服务 + +1、登录阿里云,在容器镜像服务 创建个人实例 + +2、创建命名空间,不然无法创建镜像仓库,且 只可创建 3 个 + +3、创建镜像仓库,然后选择本地仓库 + +4、点击管理可查看基本信息,操作指南写的非常详细 diff --git "a/docs/skill/docker/Docker\345\256\271\345\231\250\346\227\245\345\277\227\350\277\207\345\244\247\346\270\205\347\220\206.md" "b/docs/skill/docker/Docker\345\256\271\345\231\250\346\227\245\345\277\227\350\277\207\345\244\247\346\270\205\347\220\206.md" new file mode 100644 index 0000000..408d26e --- /dev/null +++ "b/docs/skill/docker/Docker\345\256\271\345\231\250\346\227\245\345\277\227\350\277\207\345\244\247\346\270\205\347\220\206.md" @@ -0,0 +1,135 @@ +--- +id: docker-container-log-clean +slug: /docker-container-log-clean +title: Docker容器日志过大清理 +date: 2021-10-16 +authors: kuizuo +tags: [docker] +keywords: [docker] +--- + + + +在我以 docker 容器部署了 elasticsearch 服务后的 3 个月时间,发现硬盘会不断的增大,一开始时没在意,直到硬盘报黄,就像下图这样 + +![image-20211016180014693](https://img.kuizuo.cn/image-20211016180014693.png) + +于是就准备找找是什么原因导致硬盘空间不断增大。 + +## linux 查找最大占用空间的文件 + +```bash +# 进入根目录 +cd / +# 查看根目录下每个文件夹的大小 +du -sh * +``` + +进入占用空间比较大的文件夹,再通过 `du -sh *` 找到最大的文件夹,如此反复便可找到最大 + +或使用下列命令(会稍微需要一些时间,建议先使用上面命令来缩小目录范围) + +```bash +# Linux中查找当前目录下占用空间最大的前10个文件或文件夹 +du -am | sort -nr | head -n 10 +``` + +搜寻的结果如下,一眼就能看的出那个文件夹与文件 + +``` +134938 . +125920 ./var +125315 ./var/lib +125229 ./var/lib/docker +94888 ./var/lib/docker/containers +94297 ./var/lib/docker/containers/f603a98f79874bca0e075ec1fcb0ec6866555832a4678631e7dffa7f34297281/f603a98f79874bca0e075ec1fcb0ec6866555832a4678631e7dffa7f34297281-json.log +94297 ./var/lib/docker/containers/f603a98f79874bca0e075ec1fcb0ec6866555832a4678631e7dffa7f34297281 +30335 ./var/lib/docker/overlay2 +27291 ./var/lib/docker/overlay2/f43f485f7707293cda3319786debbbdede5d940c7706c0c4b5464f57eeed7bdb +14012 ./var/lib/docker/overlay2/f43f485f7707293cda3319786debbbdede5d940c7706c0c4b5464f57eeed7bdb/merged +``` + +最终定位到文件夹`/var/lib/docker/containers`,输出当前文件夹下的文件大小 + +``` +du -d1 -h /var/lib/docker/containers | sort -h +``` + +结果如下 + +```bash +[root@localhost /]# du -d1 -h /var/lib/docker/containers | sort -h +93G /var/lib/docker/containers +93G /var/lib/docker/containers/f603a98f79874bca0e075ec1fcb0ec6866555832a4678631e7dffa7f34297281 +``` + +成功找到这个文件`f603a98f79874bca0e075ec1fcb0ec6866555832a4678631e7dffa7f34297281-json.log`,近 93GB(反正我是没敢尝试打开,生怕直接把服务器干宕机了) + +### 问题 + +[docker](https://so.csdn.net/so/search?q=docker)容器日志导致主机磁盘空间满了。elasticsearch 的 log 很占用空间,完全可以清理掉了。 + +### 清理 Docker 容器日志 + +如果 docker 容器正在运行,那么使用 rm -rf 方式删除日志后,那么删除后会发现磁盘空间并没有释放。原因是在 Linux 或者 Unix 系统中,通过 rm -rf 或者文件管理器删除文件,将会从文件系统的目录结构上解除链接(unlink)。如果文件是被打开的(有一个进程正在使用),那么进程将仍然可以读取该文件,磁盘空间也一直被占用。删除后重启 docker。 + +#### 日志清理脚本 clean_docker_log.sh + +```bash +#!/bin/sh +echo "======== start clean docker containers logs ========" + +logs=$(find /var/lib/docker/containers/ -name *-json.log) + +for log in $logs + do + echo "clean logs : $log" + cat /dev/null > $log + done + +echo "======== end clean docker containers logs ========" + +# chmod +x clean_docker_log.sh + +# ./clean_docker_log.sh +``` + +### 设置 Docker 容器日志大小 + +上述方法,日志文件迟早又会涨回来。要从根本上解决问题,需要限制容器服务的日志大小上限。这个通过配置容器 docker-compose 的 max-size 选项来实现 + +```yaml +nginx: + image: nginx:1.12.1 + restart: always + logging: + driver: “json-file” + options: + max-size: “5g” +``` + +### 全局设置 + +新建/etc/docker/daemon.json,若有就不用新建了。添加 log-dirver 和 log-opts 参数,样例如下: + +```bash +# vim /etc/docker/daemon.json + +{ + "log-driver":"json-file", + "log-opts": {"max-size":"500m", "max-file":"3"} +} + +``` + +max-size=500m,意味着一个容器日志大小上限是 500M,max-file=3,意味着一个容器有三个日志,分别是 id+.json、id+1.json、id+2.json。 + +```bash +# 重启docker守护进程 + +systemctl daemon-reload + +systemctl restart docker +``` + +> 参考文章: [Docker 容器日志占用空间过大解决办法](https://blog.csdn.net/gdsfga/article/details/90599131) diff --git "a/docs/skill/docker/Docker\350\256\277\351\227\256\345\256\277\344\270\273\346\234\272\344\270\212\346\234\215\345\212\241.md" "b/docs/skill/docker/Docker\350\256\277\351\227\256\345\256\277\344\270\273\346\234\272\344\270\212\346\234\215\345\212\241.md" new file mode 100644 index 0000000..ce929d5 --- /dev/null +++ "b/docs/skill/docker/Docker\350\256\277\351\227\256\345\256\277\344\270\273\346\234\272\344\270\212\346\234\215\345\212\241.md" @@ -0,0 +1,53 @@ +--- +id: docker-accesses-host-service +slug: /docker-accesses-host-service +title: Docker访问宿主机上服务 +date: 2022-05-25 +authors: kuizuo +tags: [docker] +keywords: [docker] +--- + + + +如果尝试过部署 docker 容器应用,并且该应用需要访问宿主机的服务,如 Mysql,Redis。会发现应用可能无法连接,其本质的原因的就是 docker 容器内的 localhost 与宿主机的 localhost 并不是同一个东西。所以连接地址不能用 localhost 和 127.0.0.1。 + +**宿主机是可以直接访问 docker 容器内的应用。** + +## 解决办法 + +### 使用 host 模式(常用) + +docker 运行容器时使用的[桥接](https://so.csdn.net/so/search?q=桥接&spm=1001.2101.3001.7020)模式(默认),如果使用 host 模式就可以访问,所以需要将 docker 的网络模式设置为 host 模式。 + +通过`docker run` 启动容器时加入`–net=host` 参数,或在 compose 文件中指定`network_mode: “host”`,便可以 host 模式运行容器 + +该参数指定该容器使用 host 网络模式,因此也无需映射端口(不然会报警告)。 + +#### mac 和 windows + +需要 env 配置中的 127.0.0.1 替换为**host.docker.internal** + +#### linux + +在启动 docker 时,加入如下语句 + +```bash +--add-host=host.docker.internal:host-gateway +``` + +而在 container 内,可以直接请求 host.docker.internal:PORT,来获取宿主机上提供的各种服务 +如果使用了 Docker Compose,则应该将下面的句子加入 container 的声明中: + +```yaml +extra_hosts: + - 'host.docker.internal:host-gateway' +``` + +### 使用 docker0 网络的默认网关地址 + +在默认的 bridge 模式下,docker0 网络的默认网关即是宿主机。在 Linux(Windows)下,docker0 网络通常会分配一个 172.17.0.0/16 的网段,其网关通常为**172.17.0.1**;macOS 下的网段则为 192.168.65.0/24,网关为**192.168.65.1**。在容器中使用该 IP 地址即可访问宿主机上的各种服务。 + +需要注意的是,这种情况下,经由 docker0 网桥而来的流量不经过宿主机的本地回环,因此需要将宿主机上的应用(MySQL,Redis 等)配置为监听 0.0.0.0。 + +但此 IP 并不一定完全固定,可能会因系统及配置而发生变化,应用也需要更改。 diff --git "a/docs/skill/docker/Docker\351\203\250\347\275\262Node\351\241\271\347\233\256.md" "b/docs/skill/docker/Docker\351\203\250\347\275\262Node\351\241\271\347\233\256.md" new file mode 100644 index 0000000..211c4a9 --- /dev/null +++ "b/docs/skill/docker/Docker\351\203\250\347\275\262Node\351\241\271\347\233\256.md" @@ -0,0 +1,146 @@ +--- +id: docker-deploy-node-project +slug: /docker-deploy-node-project +title: Docker部署Node项目 +date: 2022-05-25 +authors: kuizuo +tags: [docker, node] +keywords: [docker, node] +--- + + + +[把一个 Node.js web 应用程序给 Docker 化 | Node.js (nodejs.org)](https://nodejs.org/zh-cn/docs/guides/nodejs-docker-webapp/) + +## 部署 Express 项目 + +前提:准备一个 Express 项目以及 Docker 环境 + +在 Express 项目根目录下创建 Dockerfile 文件,内容如下 + +```dockerfile title="Dockerfile" +FROM node:alpine as builder + +WORKDIR /app + +COPY . . + +RUN npm install + +EXPOSE 3000 + +CMD ["npm", "run", "start"] +``` + +上述代码的大致意思如下 + +1. 下载 node 环境 +2. 设置 RUN CMD COPY ADD 指令的工作目录 +3. 拷贝宿主机(当前运行终端的位置)的文件到容器中的 app 目录中 +4. 安装 npm 包 +5. 暴露 3000 端口 +6. 执行`npm run start`脚本命令 + +在执行命令前,还需要创建.dockerignore,将一些不必要的文件排除(其作用于.gitignore 一致) + +```dockerfile title=".dockerignore" +/dist +/node_modules +package-lock.json +yarn.lock +``` + +此时打开终端,输入 + +```bash +docker build -t my-app . +``` + +将会执行 Dockerfile 命令,待所有命令执行完毕后,将会创建 my-app 的镜像 + +执行启动容器命令,将服务启动。 + +```bash +docker run --name my-app -p 3000:3000 my-app +``` + +此时访问对应机器的 3000 端口即可访问 express 项目。 + +如果想打开容器内的终端,有以下两种选择 + +``` +docker exec -it 容器ID/名 /bin/bash # 进入后开启新的终端 可在里面操作(常用) 或者为/bin/sh +docker attach 容器ID/名 # 不会启动新的进程 单单只是进入容器的终端 +``` + +## 部署 Express+MongoDB+Redis + +假设我现在要部署 Express + MongoDB+Redis 的服务的话,可以使用 docker-compose.yml 来自动化部署多个容器。 + +创建 docker-compose.yml 文件,内容如下 + +```yaml title="docker-compose.yml" +version: '3.9' + +services: + mongodb: + image: mongo:4.4.6 + restart: always + ports: + - 27017:27017 + networks: + - backend + environment: + MONGO_INITDB_ROOT_USERNAME: root + MONGO_INITDB_ROOT_PASSWORD: 123 + volumes: + - db-data:/data/db + + redis: + image: redis:latest + command: + - /bin/bash + - -c + - redis-server --appendonly yes + ports: + - 6379:6379 + networks: + - backend + + web: + build: . + restart: always + environment: + - NODE_ENV=development + - MYSQL_HOST=mysql + - REDIS_HOST=redis + ports: + - 5000:5000 + networks: + - backend + depends_on: + - mongodb + - redis + +networks: + backend: + +volumes: + db-data: +``` + +运行命令,**重新部署的话可以添加--build 参数** + +``` +docker-compose up -d +``` + +web 后端项目中涉及到,数据库的连接地址(Host)要以 docker-compose.yml 中的 service 名一致。例如上面所定义的 environment 中 + +MYSQL_HOST=mysql + +REDIS_HOST=redis + +而不能为 localhost,因为**docker 容器内的 localhost 与宿主机的 localhost 并不是同一个地址**。 + +或者是在配置中将 localhost 修改为 docker 网络的 ip,一般为 172.17.0.1,具体根据 docker 实际网络而定。 diff --git "a/docs/skill/docker/\344\277\256\346\224\271\346\227\240\346\263\225\345\220\257\345\212\250\347\232\204Docker\345\256\271\345\231\250\351\205\215\347\275\256\346\226\207\344\273\266.md" "b/docs/skill/docker/\344\277\256\346\224\271\346\227\240\346\263\225\345\220\257\345\212\250\347\232\204Docker\345\256\271\345\231\250\351\205\215\347\275\256\346\226\207\344\273\266.md" new file mode 100644 index 0000000..bb06488 --- /dev/null +++ "b/docs/skill/docker/\344\277\256\346\224\271\346\227\240\346\263\225\345\220\257\345\212\250\347\232\204Docker\345\256\271\345\231\250\351\205\215\347\275\256\346\226\207\344\273\266.md" @@ -0,0 +1,61 @@ +--- +id: fix-docker-config-that-cannot-start-up +slug: /fix-docker-config-that-cannot-start-up +title: 修改无法启动的Docker容器配置文件 +date: 2021-08-17 +authors: kuizuo +tags: [docker, elasticsearch] +keywords: [docker, elasticsearch] +--- + + + +## 前因 + +事情是这样的 + +我想给我的 elasticsearch 扩充一下内存,默认配置的内存太少了,机器 32g,连 16g 都没占用上,有好几次的时候同时并发几千条服务就挂了。。。 + +于是,进入 elasticsearch 容器,找到`elasticsearch.yml`(注意这个文件) + +![image-20210817142200704](https://img.kuizuo.cn/image-20210817142200704.png) + +添加了下列两个参数 + +-Xms16g -Xmx16g + +然后重启容器,就发现容器怎么也启动不了,然后咋一看,配置文件搞错了,设置内存的应该是`jvm.options`这个配置文件 + +### 解决办法 + +所以目标很明确,只需要更改回原来配置文件即可正常启动。但容器只要一重启就会立马挂掉,都启动不了,又怎么通过`docker exec -it elasticsearch /bin/bash`进入容器,然后通过 vim 修改配置呢。 + +我当时的想法是这样的,容器一启动肯定不会立马挂掉,至少会有个几秒,我能不能通过一系列的命令进入容器然后立马修改文件,想法是挺好,可当 vim 编辑文件的,我又改怎么通过进入编辑,保存退出编辑。于是就果断放弃,翻看自己之前写过的 Docker 笔记 ,发现。有一个命令`docker cp 容器id:容器内路径 宿主机路径`从容器内拷贝文件到宿主机上,于是找到 elasticsearch 的配置文件路径`/usr/share/elasticsearch/config`,我的容器名字 + +```bash +docker cp elasticsearch:/usr/share/elasticsearch/config/elasticsearch.yml . +vi elasticsearch.yml +# 编辑文件 +docker cp elasticsearch.yml :/usr/share/elasticsearch/config/elasticsearch.yml +docker start elasticsearch +``` + +然后重启 elasticsearch 容器即可正常运行 + +## 后果 + +回到最开始的目的,那么要如何更改 elasticsearch 内存呢 + +如果要新建一个容器的话 附带这个参数即可`-e ES_JAVA_OPTS="-Xms64m -Xmx512m"` + +如果已经新建过容器的话,找到**jvm.options**这个文件 + +```bash +[root@localhost /]# find /var/lib/docker/ -name jvm.options +/var/lib/docker/overlay2/1f06b1e87d0fd473cc910d8689add0638f1ac36676d92f92dc03b17e65bf7dae/diff/usr/share/elasticsearch/config/jvm.options +/var/lib/docker/overlay2/d20c175dffdc80467dbce39d4a5bc6dc9f7ff239564a8ee1ac8c4bcfdd9a461e/merged/usr/share/elasticsearch/config/jvm.options +``` + +![image-20210817145633786](https://img.kuizuo.cn/image-20210817145633786.png) + +如图,设置对应的内存大小即可,重启 elasticsearch 容器即可 diff --git a/docs/skill/docusaurus/comment.md b/docs/skill/docusaurus/comment.md new file mode 100644 index 0000000..1c6b85b --- /dev/null +++ b/docs/skill/docusaurus/comment.md @@ -0,0 +1,70 @@ +--- +id: docusaurus-comment +slug: /docusaurus-comment +title: 评论服务 +authors: kuizuo +--- + +这里推荐两种评论服务 + +Giscus:基于GitHub Discussions,对程序员相对友好,评论信息提示通过github邮箱发送。 + +Waline:需要搭建后端服务与数据库服务,提供评论与浏览量服务,可拓展性强。 + +## [giscus](https://giscus.app) + +之前的评论使用的是 gitalk,但是那个是基于 github issue 的,并且 issue 不能关闭,每次打开仓库的时候都会看到几十个 issue,特别不友好。 + +所以后面就考虑换成 [giscus](https://giscus.app/zh-CN),由 [GitHub Discussions](https://docs.github.com/en/discussions) 驱动的评论系统。首先要确保以下几点: + +1. **此仓库是[公开的](https://docs.github.com/en/github/administering-a-repository/managing-repository-settings/setting-repository-visibility#making-a-repository-public)**,否则访客将无法查看 discussion(并不需要一定是博客的项目,随便一个仓库都可以)。 +2. **[giscus](https://github.com/apps/giscus) app 已安装**否则访客将无法评论和回应。 +3. **Discussions** 功能已[在你的仓库中启用](https://docs.github.com/en/github/administering-a-repository/managing-repository-settings/enabling-or-disabling-github-discussions-for-a-repository)。 + +本博客已经内置好评论组件 [src/component/Comment](https://github.com/kuizuo/blog/blob/main/src/components/Comment/index.tsx),所以只需要在 `docusaurus.config.ts` 中设置 giscus 的配置即可。 + +### 配置 giscus + +打开 [giscus](https://giscus.app/) 官网,填写完对应的信息后,可以得到一个已经配置好的` +``` + +由于我在 `src/component/Comment` 组件中做了配置合并,并且支持主题变化、国际化。因此,你只需要复制 `data-repo`, `data-repo-id`, `data-category` 和 `data-category-id` 填写到 `docusaurus.config.ts` 中即可,以下是我的配置文件。 + +```javascript title='docusaurus.config.ts' icon='logos:docusaurus' +giscus: { + repo: 'kuizuo/blog', + repoId: 'MDEwOlJlcG9zaXRvcnkzOTc2MjU2MTI=', + category: 'General', + categoryId: 'DIC_kwDOF7NJDM4CPK95', + theme: 'light', + darkTheme: 'dark', +} +``` + +:::info 如果不替换的话,评论的信息都将会在我的 Discussions 下😂 + +::: + +## [waline](https://github.com/walinejs/waline) + +目前比较流行的博客评论系统还有 waline,它可以提供评论与浏览量服务,由于需要搭配后端服务与数据库服务,所以在配置方面会比 giscus 来的麻烦,但它无需 github Discussions,所以也是绝大多数博客作者的标配。 + +关于如何配置,参见官方 [快速上手 | Waline](https://waline.js.org/guide/get-started.html) diff --git a/docs/skill/docusaurus/component.md b/docs/skill/docusaurus/component.md new file mode 100644 index 0000000..19ea2fc --- /dev/null +++ b/docs/skill/docusaurus/component.md @@ -0,0 +1,64 @@ +--- +id: docusaurus-component +slug: /docusaurus-component +title: 自定义组件 +authors: kuizuo +description: 介绍如何自定义 docusaurus 组件 +--- + +初始化的一个 docusaurus 项目就已经有预留好的的组件,例如博客布局,标签页归档页等等。但是这些组件的样式可能不满你的审美,或者是想增加在这些主题组件中增加点东西。那么就需要用到 [Swizzle](https://docusaurus.io/zh-CN/docs/swizzling) + +## 主题组件 + +在 docusaurus 中的主题组件存放在 **@docusaurus/theme-classic/theme** 下,如果想要覆盖某个组件的话可以在 src/theme 目录下创建与之对应文件路径结构相同的文件。 + +像下面这样 + +``` +website +├── node_modules +│ └── @docusaurus/theme-classic +│ └── theme +│ └── Navbar.js +└── src + └── theme + └── Navbar.js +``` + +每当导入 `@theme/Navbar` 时,`website/src/theme/Navbar.js` 都会被优先载入。 + +关于*分层架构*可看[客户端架构 | Docusaurus](https://docusaurus.io/zh-CN/docs/advanced/client) + +## swizzle 组件 + +要输出所有 `@docusaurus/theme-classic` 组件的总览,可以运行: + +```bash +yarn run swizzle @docusaurus/theme-classic -- --list +``` + +不过我更倾向于直接在 `node_modules/@docusaurus/theme-classic/src/theme` 查看所有组件。 + +这里以归档页举例,官方的归档页面组件是 `theme/BlogArchivePage` + +有两种方式可以完成自定义组件:[弹出组件](https://docusaurus.io/zh-CN/docs/swizzling#ejecting)或者[包装组件](https://docusaurus.io/zh-CN/docs/swizzling#wrapping) + +例如弹出组件,可以执行以下[命令](https://docusaurus.io/zh-CN/docs/cli#docusaurus-swizzle): + +```bash +yarn run swizzle @docusaurus/theme-classic BlogArchivePage -- --eject --typescript +``` + +这样会创建 `src/theme/BlogArchivePage/index.tsx`,也就是归档页面的代码,而要做的就是修改代码,实现自己所需的样式与功能。 + +不过这样获取到的只是 index.tsx 文件,有可能还存在子组件。所有我一般的做法是在 `node_modules/@docusaurus/theme-classic/src/theme` 中找到组件所在文件夹,然后将整个文件夹复制到 `src/theme` 下。这样能得到就是最原始的 ts 文件,同时所能修改的地方也就越多,更方便的个性化。 + +:::warning + +**但是**,在使用自定义组件的时候,有些主题组件可能会存在一定**风险**。尤其是在升级 Docusaurus 变得更困难,因为如果接收的属性发生变化,或内部使用的主题 API 发生变化,有可能就会导致页面渲染失败。 + +就比如我在将 docusaurus 升级到 2.0.0 正式版的时候就出现页面错误,原因是 [plugin-content-blog](https://docusaurus.io/zh-CN/docs/api/plugins/@docusaurus/plugin-content-blog) 在传递给组件的数据发生了变动,导致数据无法解析,自然而然页面就渲染失败。 + +::: + +如果不升级依赖也确实不会有问题,但谁能保证新版本的一些特性不吸引使用者去升级呢?所以在自定义组件的时候,升级依赖后就可能需要维护一定的代码。要做的是重新 swizzle 一份最新的文件,然后去比对变化,最终排查问题。 diff --git a/docs/skill/docusaurus/config.md b/docs/skill/docusaurus/config.md new file mode 100644 index 0000000..e65d9f7 --- /dev/null +++ b/docs/skill/docusaurus/config.md @@ -0,0 +1,85 @@ +--- +id: docusaurus-config +slug: /docusaurus-config +title: 配置文件 +authors: kuizuo +--- + +## docusaurus.config.ts + +`docusaurus.config.ts` 位于你的网站的根目录,包含了你的站点的配置信息。 + +在这里可以修改 logo,站点名(title),作者名,顶部的公告(announcementBar),导航栏(navbar),底部导航(footer)等等。 + +```typescript title='docusaurus.config.ts' icon='logos:docusaurus' +const config: Config = { + title: '愧怍的小站', + url: 'https://kuizuo.cn', + baseUrl: '/', + favicon: 'img/favicon.ico', + organizationName: 'kuizuo', + projectName: 'blog', + themeConfig: { + image: 'img/logo.png', + metadata: [ + { + name: 'keywords', + content: '愧怍, kuizuo, blog, javascript, typescript, node, react, vue, web, 前端, 后端', + }, + ], + // ... +} + +export default config +``` + +同时绝大部分的配置信息都可以放在这里,例如搜索(algolia),评论(giscus),社交链接(socials)等等。这些配置都可以通过docusaurus内置的hook(useThemeConfig、useDocusaurusContext)来获取。 + +完整的配置信息说明 [docusaurus.config.ts | Docusaurus](https://docusaurus.io/zh-CN/docs/api/docusaurus-config) + +## sidebars.js + +用于配置文档的侧边栏,例如本博客中的[技术笔记](/docs/skill/),[工具推荐](/docs/tools/)。侧边栏对应的每一项都是一个 markdown 文件,同时这些文件都存放在 docs 目录下,方便管理。 + +[侧边栏 | Docusaurus](https://docusaurus.io/zh-CN/docs/sidebar) + +## 相关信息 + +### 基本信息 + +站点名和作者名只需要搜索 **愧怍** 便能找到关键位置 + +### 关于我 + +具体可在 `src/pages/about.mdx` 中查看与修改。 + +其中技术栈的图标使用 [Shields.io](https://shields.io/) 生成,github 的状态信息使用[GitHub Profile Summary Cards](https://github-profile-summary-cards.vercel.app/demo.html)生成 + +所要做的就是将 username 替换成你的 github 名即可。 + +### 社交链接 + +只需要在 `docusaurus.config.ts` 中修改 socials 属性,替换成你的即可。 + +```typescript title='docusaurus.config.ts' icon='logos:docusaurus' +socials: { + github: 'https://github.com/kuizuo', + twitter: 'https://twitter.com/kuizuo', + juejin: 'https://juejin.cn/user/1565318510545901', + csdn: 'https://blog.csdn.net/kuizuo12', + qq: 'https://wpa.qq.com/msgrd?v=3&uin=911993023&site=qq', + cloudmusic: 'https://music.163.com/#/user/home?id=1333010742', +} +``` + +如果你还有其他社交链接,可以在这里添加对应的链接,然后在 `src/components/Hero.index.tsx` 中的 SocialLinks 组件中来配置新增或者删除社交链接图标。 + +### 友链、导航、项目 + +这里你需要关注数据部分,如果想了解页面的实现可以看[自定义页面](/docs/docusaurus-style#自定义页面) + +数据部分存放在根目录 `/data` 下,并使用 ts 用作类型提示。这些数据最终会在这些页面中渲染,你只需要根据符合的类型定义所要展示的数据,访问对应页面就能查看到效果。 + +## 其他配置 + +可能还需要配置下 giscus 评论,搜索,站点统计等等,这些会放在[插件](/docs/docusaurus-plugin)中细讲。 diff --git a/docs/skill/docusaurus/deploy.md b/docs/skill/docusaurus/deploy.md new file mode 100644 index 0000000..49f1580 --- /dev/null +++ b/docs/skill/docusaurus/deploy.md @@ -0,0 +1,87 @@ +--- +id: docusaurus-deploy +slug: /docusaurus-deploy +title: 部署 +authors: kuizuo +--- + +我之前使用 [Vercel](https://vercel.com) 一把梭,无需任何配置。这样我就只需要专注输出内容即可。这是我当时使用 Vercel 部署的文章 [Vercel 部署个人博客](/blog/vercel-deploy-blog) + +但如今,`vercel.app` 被 DNS 污染,即被墙了,导致国内无法访问,虽然使用有自己的域名解析到 Vercel 上也可能访问,但被墙了,也就意味着国内 DNS 的解析速度必然有所下降,导致站点访问速度有所下降。 + +加上我想有更好的访客体验,于是我决定采用国内国外不同的解析方式来加快访问。 + +首先在线路类型中,分别针对境内和境外做了不同的记录值,境内使用国内的 CDN 服务,而境外就使用 Vercel。 + +![image-20221204161431863](https://img.kuizuo.cn/image-20221204161431863.png) + +这样我国内访问就是访问国内的 CDN,访问国外访问就是 Vercel 的 CDN,这样针对不同的地区的网络都能有一个不错的访问速度,可以到 [Ping.cn:网站测速-ping 检测](https://www.ping.cn/) 中测试测试你的站点访问速度如何。 + +以下是我的网站测速结果,也可通过访问 [kuizuo.cn 在全国各地区网络速度测试情况-Ping.cn](https://www.ping.cn/http/kuizuo.cn) 在线查看 + +![image-20221204161146327](https://img.kuizuo.cn/image-20221204161146327.png) + +果然,花钱了就是不一样。 + +## 持续集成 + +由于 Vercel 能够自动拉取仓库代码,并自行构建部署,因此通常什么配置都不需要。 + +由于代码提交到代码仓库(github),则需要借用 CI 服务来帮助我们完成这些任务,这里我使用了 [Github Action](https://github.com/marketplace) 来帮助我构建,构建记录可以在 [Actions · kuizuo/blog](https://github.com/kuizuo/blog/actions) 中查看。以下是我的配置文件 + +```yaml title='.github/workflows/ci.yml' icon='logos:github-actions' +name: CI + +on: + push: + branches: + - main + +jobs: + build-and-deploy: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest] # macos-latest, windows-latest + node: [18] + + steps: + - uses: actions/checkout@v4 + + - name: Set node version to ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - run: corepack enable + + - name: Setup + run: npm i -g @antfu/ni + + - name: Install + run: nci + + - name: Build + run: nr build + + - name: SSH Deploy + uses: easingthemes/ssh-deploy@v4.1.10 + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + ARGS: '-avzr --delete' + SOURCE: 'build' + REMOTE_HOST: ${{ secrets.REMOTE_HOST }} + REMOTE_USER: 'root' + TARGET: '/opt/1panel/apps/openresty/openresty/www/sites/kuizuo.cn/index' +``` + +等待 CI 将最终构建的产物通过 rsync 放到自己的服务器上,便完成了整套部署的流程。 + +当一切都配置好了之后,我只需要将代码推送到远程仓库上,Github Action 与 Vercel 分别完成它们所该做的任务。等待片刻,再次访问站点,刚刚提交的代码就成功生效了。 + +## 没有域名和服务器该怎么部署? + +当然了上述只是我的配置方案,有许多伙伴可能没有自己的域名或者自己的服务器,就想着白嫖,那么这里目前我只能推荐 [Netlify](https://www.netlify.com/),然后通过 netlify 的二级域名如 kuizuo-blog.netlify.app 来进行访问。 + +我个人还是非常建议去弄一个属于自己的域名,通过 Vercel 的自定义域名就可以访问。并且由于自己的域名解析的不是大陆的服务器(Vercel 的服务器就不是国内大陆的),因此无需备案这一更繁琐的步骤。 diff --git a/docs/skill/docusaurus/guides.mdx b/docs/skill/docusaurus/guides.mdx new file mode 100644 index 0000000..e4aee89 --- /dev/null +++ b/docs/skill/docusaurus/guides.mdx @@ -0,0 +1,19 @@ +--- +id: docusaurus-guides +slug: /docusaurus-guides +title: Docusaurus 主题魔改 +authors: kuizuo +keywords: ['guides', 'docusaurus', 'docusaurus-guides'] +--- + +这里是本人对 [Docusaurus](https://docusaurus.io/) 的魔改指南,帮助使用者更好使用 Docusaurus。 + +同时 [Docusaurus 2.0](https://docusaurus.io/zh-CN/blog/2022/08/01/announcing-docusaurus-2.0) 也正式发布了,顺带升级依赖与重构项目使其易懂易用。 + +也欢迎你使用本主题,如果你有任何问题,欢迎在 [GitHub Discussions](https://github.com/kuizuo/blog/discussions) 提出。 + +```mdx-code-block +import DocCardList from '@theme/DocCardList'; + + +``` diff --git a/docs/skill/docusaurus/plugin.md b/docs/skill/docusaurus/plugin.md new file mode 100644 index 0000000..8b21312 --- /dev/null +++ b/docs/skill/docusaurus/plugin.md @@ -0,0 +1,95 @@ +--- +id: docusaurus-plugin +slug: /docusaurus-plugin +title: 插件 +authors: kuizuo +--- + +在 `docusaurus.config.ts` 下的 plugins,可以看到所有插件以及插件配置。 + +```typescript title='docusaurus.config.ts' icon='logos:docusaurus' +plugins: [ + 'docusaurus-plugin-image-zoom', + 'docusaurus-plugin-sass', + path.resolve(__dirname, './src/plugin/plugin-baidu-tongji'), + path.resolve(__dirname, './src/plugin/plugin-baidu-push'), + [ + path.resolve(__dirname, './src/plugin/plugin-content-blog'), + { + path: 'blog', + routeBasePath: '/', + editUrl: ({ locale, blogDirPath, blogPath, permalink }) => + `https://github.com/kuizuo/blog/edit/main/${blogDirPath}/${blogPath}`, + editLocalizedFiles: false, + blogSidebarCount: 10, + postsPerPage: 10, + showReadingTime: true, + readingTime: ({ content, frontMatter, defaultReadingTime }) => + defaultReadingTime({ content, options: { wordsPerMinute: 300 } }), + feedOptions: { + type: 'all', + title: '愧怍', + copyright: `Copyright © ${new Date().getFullYear()} 愧怍 Built with Docusaurus.

${beian}

`, + }, + }, + ], + // ... +] +``` + +这里我会列举我所用到的插件(包括自定义),更多插件可看[社区精选 | Docusaurus](https://docusaurus.io/zh-CN/community/resources#community-plugins) + +## plugin-baidu-tongji + +[百度统计](https://tongji.baidu.com/web/welcome/login) + +这样你就能看到你的站点访客主要都在看哪些页面,以及行为记录,如下图所示。![image-20221204153015256](https://img.kuizuo.cn/image-20221204153015256.png) + +## plugin-baidu-push + +[百度收录](https://ziyuan.baidu.com/dailysubmit/index) + +主动推送代码,用于网站收录,这部分代码无需变动。 + +```javascript title="src/plugins/plugin-baidu-push/index.js" +;(function () { + var bp = document.createElement('script') + var curProtocol = window.location.protocol.split(':')[0] + if (curProtocol === 'https') { + bp.src = 'https://zz.bdstatic.com/linksubmit/push.js' + } else { + bp.src = 'http://push.zhanzhang.baidu.com/push.js' + } + bp.defer = true + var s = document.getElementsByTagName('script')[0] + s.parentNode.insertBefore(bp, s) +})() +``` + +## plugin-matomo(弃用) + +[Matomo Analytics](https://matomo.org/) 站点统计,分析用户行为,停留时间。 + +与百度统计类似,不过这个需要自行部署 matomo 服务,不需要的可直接删除。 + +## [plugin-pwa](https://docusaurus.io/zh-CN/docs/api/plugins/@docusaurus/plugin-pwa) + +创建支持离线模式和应用安装的 PWA 文档站点,就像下图这样。 + +![image-20221204153401244](https://img.kuizuo.cn/image-20221204153401244.png) + +## [plugin-image-zoom](https://github.com/flexanalytics/plugin-image-zoom) + +适用于 Docusaurus 的图像缩放插件。 + +## plugin-sass + +支持 sass 预处理器 + +## plugin-content-blog + +由于官方的 [plugin-content-blog](https://docusaurus.io/zh-CN/docs/api/plugins/@docusaurus/plugin-content-blog) 插件没有将博客的所有**标签**数据传递给博客列表组件,也就是导致博客列表页面 `BlogListPage` 获取不到全局标签信息,所以这里对 `plugin-content-blog` 进行魔改,将 blog 信息添加至全局数据中,可在任意页面中都访问到所有博文的信息。 + +:::warning 这些数据可能会占据一定的空间,查看详情 [点我](https://github.com/facebook/docusaurus/pull/7163#issuecomment-1096780257)。 + +::: diff --git a/docs/skill/docusaurus/search.md b/docs/skill/docusaurus/search.md new file mode 100644 index 0000000..1749ea9 --- /dev/null +++ b/docs/skill/docusaurus/search.md @@ -0,0 +1,189 @@ +--- +id: docusaurus-search +slug: /docusaurus-search +title: 搜索 +authors: kuizuo +--- + +> [搜索 | Docusaurus](https://docusaurus.io/zh-CN/docs/search) + +## [algolia](https://www.algolia.com/) + +有两种方案来配置 algolia。 + +1. 让 Docsearch(准确来说是 [Algolia Crawler](https://crawler.algolia.com/)) 每周一次爬取你的网站(也可自行爬取),**前提是项目开源,否则收费**,好处是无需额外配置,申请比较繁琐(本博客目前采用的方式) + +2. 自己运行 DocSearch 爬虫,可以随时爬取,但需要自行去注册账号和搭建爬虫环境,或者使用 Github Actions 来帮我们爬取。 + +### 方案1 + +关于申请 Algolia DocSearch 在文档中有详细介绍,主要是要申请麻烦,需要等待邮箱,并且还需要回复内容给对方进行确认。所以免费托管的 DocSearch 条件是,比较苛刻的,但申请完几乎是一劳永逸,也是我非常推荐的。如果申请成功后就可以在 [Crawler Admin Console](https://crawler.algolia.com/admin/crawlers) 中查看 + +![image-20220627232545640](https://img.kuizuo.cn/image-20220627232545640.png) + +然后将得到 algolia 的 appId,apiKey,indexName 填写到 `docusaurus.config.ts` 中即可。 + +```javascript title='docusaurus.config.ts' +algolia: { + appId: 'GV6YN1ODMO', + apiKey: '50303937b0e4630bec4a20a14e3b7872', + indexName: 'kuizuo', +} +``` + +爬取完毕后还会定时发送到你邮箱 + +![image-20230219144035031](https://img.kuizuo.cn/image-20230219144035031.png) + +### 方案2 + +[Run your own | DocSearch (algolia.com)](https://docsearch.algolia.com/docs/run-your-own) + +因为方案1是真的难申请,极大概率会失败,无奈只能采用方案2。 + +首先去申请 [Algolia](https://www.algolia.com/) 账号,然后在左侧 indices 创建索引,在 API Keys 中获取 Application ID 和 API Key(注意,有两个 API KEY) + +![image-20210821230135749](https://img.kuizuo.cn/image-20210821230135749.png) + +![image-20210821230232837](https://img.kuizuo.cn/image-20210821230232837.png) + +填入到 `docusaurus.config.ts` 中的 API KEY 是 **Search-Only API Key** + +```js +themeConfig: { + algolia: { + apiKey: "xxxxxxxxxxx", + appId: "xxxxxxxxxxx", + indexName: "kuizuo", + }, +} +``` + +系统我选用的是 Linux,在 Docker 的环境下运行爬虫代码。不过要先 [安装 jq](https://github.com/stedolan/jq/wiki/Installation#zero-install) 我这里选择的是 0install 进行安装(安装可能稍慢),具体可以查看文档,然后在控制台查看安装结果 + +``` +[root@kzserver kuizuo.cn]# jq --version +jq-1.6 +``` + +接着在任意目录中创建 `.env` 文件,填入对应的 APPID 和 API KEY(这里是`Admin API Key`,当时我还一直以为是 Search API Key 坑了我半天 😭) + +```js +APPLICATION_ID = YOUR_APP_ID +API_KEY = YOUR_API_KEY +``` + +然后创建 `docsearch.json` 文件到项目目录下,其内容可以参考如下(将高亮部分替换成你的网站) + +```json title='docsearch.json' {2-4} +{ + "index_name": "xxxx", + "start_urls": ["https://example.com"], + "sitemap_urls": ["https://example.com"], + "selectors": { + "lvl0": { + "selector": "(//ul[contains(@class,'menu__list')]//a[contains(@class, 'menu__link menu__link--sublist menu__link--active')]/text() | //nav[contains(@class, 'navbar')]//a[contains(@class, 'navbar__link--active')]/text())[last()]", + "type": "xpath", + "global": true, + "default_value": "Documentation" + }, + "lvl1": "header h1, article h1", + "lvl2": "article h2", + "lvl3": "article h3", + "lvl4": "article h4", + "lvl5": "article h5, article td:first-child", + "lvl6": "article h6", + "text": "article p, article li, article td:last-child" + }, + "custom_settings": { + "attributesForFaceting": ["type", "lang", "language", "version", "docusaurus_tag"], + "attributesToRetrieve": ["hierarchy", "content", "anchor", "url", "url_without_anchor", "type"], + "attributesToHighlight": ["hierarchy", "content"], + "attributesToSnippet": ["content:10"], + "camelCaseAttributes": ["hierarchy", "content"], + "searchableAttributes": [ + "unordered(hierarchy.lvl0)", + "unordered(hierarchy.lvl1)", + "unordered(hierarchy.lvl2)", + "unordered(hierarchy.lvl3)", + "unordered(hierarchy.lvl4)", + "unordered(hierarchy.lvl5)", + "unordered(hierarchy.lvl6)", + "content" + ], + "distinct": true, + "attributeForDistinct": "url", + "customRanking": ["desc(weight.pageRank)", "desc(weight.level)", "asc(weight.position)"], + "ranking": ["words", "filters", "typo", "attribute", "proximity", "exact", "custom"], + "highlightPreTag": "", + "highlightPostTag": "", + "minWordSizefor1Typo": 3, + "minWordSizefor2Typos": 7, + "allowTyposOnNumericTokens": false, + "minProximity": 1, + "ignorePlurals": true, + "advancedSyntax": true, + "attributeCriteriaComputedByMinProximity": true, + "removeWordsIfNoResults": "allOptional", + "separatorsToIndex": "_", + "synonyms": [ + ["js", "javascript"], + ["ts", "typescript"] + ] + } +} +``` + +运行 docker 命令 + +```bash +docker run -it --env-file=.env -e "CONFIG=$(cat docsearch.json | jq -r tostring)" algolia/docsearch-scraper +``` + +接着等待容器运行,爬取你的网站即可。最终打开 algolia 控制台提示如下页面则表示成功 + +![image-20210821225934002](https://img.kuizuo.cn/image-20210821225934002.png) + +因为要确保项目成功部署后才触发,如果采用 vercel 部署可以按照如下触发条件。 + +```yaml title='.github/workflows/docsearch.yml' +name: docsearch + +on: deployment + +jobs: + algolia: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Get the content of docsearch.json as config + id: algolia_config + run: echo "::set-output name=config::$(cat docsearch.json | jq -r tostring)" + + - name: Run algolia/docsearch-scraper image + env: + ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }} + ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }} + CONFIG: ${{ steps.algolia_config.outputs.config }} + run: | + docker run \ + --env APPLICATION_ID=${ALGOLIA_APP_ID} \ + --env API_KEY=${ALGOLIA_API_KEY} \ + --env "CONFIG=${CONFIG}" \ + algolia/docsearch-scraper +``` + +添加 [secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) 到你的 Github 仓库中,提交代码便可触发爬虫规则。 + +## [orama](https://docs.oramasearch.com/open-source/plugins/plugin-docusaurus) + +配置 algolia 的过程有稍许的复杂,这里你可以在 docusaurus 中集成 [orama](https://docs.oramasearch.com/open-source/plugins/plugin-docusaurus),这是一个在浏览器、服务器和边缘运行全文、矢量和混合搜索查询服务。最终实现的效果如图所示 + +![](https://img.kuizuo.cn/2024/0118082834-202401180828818.png) + +## 本地搜索 + +如果你嫌 algolia 申请比较麻烦,docusaurus 也提供本地搜索,不过搜索上肯定会比全文搜索来的差一些。 + +本地搜索插件:[docusaurus-search-local](https://github.com/cmfcmf/docusaurus-search-local) diff --git a/docs/skill/docusaurus/style.md b/docs/skill/docusaurus/style.md new file mode 100644 index 0000000..d5ab782 --- /dev/null +++ b/docs/skill/docusaurus/style.md @@ -0,0 +1,28 @@ +--- +id: docusaurus-style +slug: /docusaurus-style +title: 样式与页面 +authors: kuizuo +--- + +## [样式和布局](https://docusaurus.io/zh-CN/docs/styling-layout#styling-your-site-with-infima) + +Docusaurus 网站是一个 React 单页应用。 你可以像一般的 React 应用一样给网站提供样式,像 [tailwindcss](https://tailwindcss.com/) 与组件库都是支持的。不过引入这些会带来一定的代码体积,因此在这套主题中我所使用的都是全局样式与 css 模块。 + +## 修改主题色 + +可以在 [这里](https://docusaurus.io/zh-CN/docs/styling-layout#styling-your-site-with-infima) 设置主色调与背景色来查看浅色与深色模式下的效果,例如我的主题色是 #12AFFA + +`@docusaurus/preset-classic` 用 [Infima](https://infima.dev/) 作为底层样式框架。 Infima 提供了灵活的布局,以及常见的 UI 组件样式,适用于以内容为中心的网站(博客、文档、首页)。想要了解更多详情,请查看 [Infima 网站](https://infima.dev/)。 + +## 主页 + +因为设置了[仅博客模式](https://docusaurus.io/zh-CN/docs/blog#blog-only-mode),没有专门编写的首页,而是将博文列表设置为首页。需要将 `src/pages/index.tsx` 文件给删除(或者改个名),否则会导致首页路径冲突。当然你也可以专门搞一个主页,就像 docusaurus 那样,然后跳转至博文列表页。 + +所以博客页面,也就是首页。但仅仅只有博客是远远不够的,所以便添加了 Hero 组件,也就是首次访问博客的样子。 + +主页右侧 SVG 背景文件地址: `src/components/Hero/img/hero_main.svg`, 插画来源于 [unDraw](https://undraw.co/illustrations),在这个网站可以找到这类插画风格的背景。或者你可以找专门设计插画的人为你设计。 + +## 自定义页面 + +[归档](/blog/archive)、[友链](/friends)、[导航](/resources)、[项目](/project)以及[关于我](/about)页面都在 `src/pages` 目录下定义,根据文件名映射对应路由。页面的创建可以查看 [创建页面 | Docusaurus](https://docusaurus.io/zh-CN/docs/creating-pages) diff --git "a/docs/skill/git/git commit \350\247\204\350\214\203.md" "b/docs/skill/git/git commit \350\247\204\350\214\203.md" new file mode 100644 index 0000000..ba3200c --- /dev/null +++ "b/docs/skill/git/git commit \350\247\204\350\214\203.md" @@ -0,0 +1,64 @@ +--- +id: git-conmit-specification +slug: git-conmit-specification +title: git commit 规范 +date: 2021-08-31 +authors: kuizuo +tags: [git, commit] +keywords: [git, commit] +--- + + + +提交规范主要是为了让开发者提交完整的更新信息,方便查阅。 + +目前最为流行的提交信息规范来自于 Angular 团队。 + +规范中,主要就是要求提交内容要进行分类并填写内容,更为严格的规定是要求标注开发模块,整个语法如下 + +```bash +type(scope?): subject #scope is optional; multiple scopes are supported (current delimiter options: "/", "\" and ",") +``` + +| type | commit 的类型 | +| -------- | -------------------------------------------------------- | +| feat | 新功能、新特性 | +| fix | 修改 bug | +| perf | 更改代码,以提高性能 | +| refactor | 代码重构(重构,在不影响代码内部行为、功能下的代码修改) | +| docs | 文档修改 | +| style | 代码格式修改, 注意不是 css 修改(例如分号修改) | +| test | 测试用例新增、修改 | +| build | 影响项目构建或依赖项修改 | +| revert | 恢复上一次提交 | +| ci | 持续集成相关文件修改 | +| chore | 其他修改(不在上述类型中的修改) | +| release | 发布新版本 | +| workflow | 工作流相关文件修改 | + +以下是一些示例: + +| commit message | 描述 | +| ---------------------------------- | ------------------------- | +| chore: init | 初始化项目 | +| chore: update deps | 更新依赖 | +| chore: wording | 调整文字(措词) | +| chore: fix typos | 修复拼写错误 | +| chore: release v1.0.0 | 发布 1.0.0 版本 | +| fix: icon size | 修复图标大小 | +| fix: value.length -> values.length | value 变量调整为 values | +| feat(blog): add comment section | blog 新增评论部分 | +| feat: support typescript | 新增 typescript 支持 | +| feat: improve xxx types | 改善 xxx 类型 | +| style(component): code | 调整 component 代码样式 | +| refactor: xxx | 重构 xxx | +| perf(utils): random function | 优化 utils 的 random 函数 | +| docs: xxx.md | 添加 xxx.md 文章 | + +更多示例可以参考主流开源项目的 commit。 + +## 检查 commit 规范 + +要检查 commit message 是否符合要求,可以使用 [commitlint](https://github.com/conventional-changelog/commitlint) 工具,并配合 [husky](https://github.com/typicode/husky) 对每次提交的 commit 进行检查。 + +当然规范不是强求,但 commit message 一定要能简要说明本次代码的改动主要部分,有利于他人与自己后期查看代码记录。 diff --git "a/docs/skill/git/git \344\277\256\346\224\271\351\273\230\350\256\244\345\210\206\346\224\257main.md" "b/docs/skill/git/git \344\277\256\346\224\271\351\273\230\350\256\244\345\210\206\346\224\257main.md" new file mode 100644 index 0000000..f13e4b0 --- /dev/null +++ "b/docs/skill/git/git \344\277\256\346\224\271\351\273\230\350\256\244\345\210\206\346\224\257main.md" @@ -0,0 +1,45 @@ +--- +id: git-change-default-branch +slug: git-change-default-branch +title: git 修改默认分支main +date: 2021-08-04 +authors: kuizuo +tags: [git] +keywords: [git] +--- + + + +## 前言 + +GitHub 官方表示,从 2020 年 10 月 1 日起,在该平台上创建的所有新的源代码仓库将默认被命名为 "main",而不是原先的"master"。值得注意的是,现有的存储库不会受到此更改影响。 + +也就是现在从 github 初始化的项目都是 main 分支,然而在此之前安装的 git 默认分支为 master,本地使用 git 创建项目都是 master,通过如下命令可更改默认分支的名字 + +## 命令 + +- 修改默认分支为 `main` 分支 + +```bash +git config --global init.defaultBranch main +``` + +- 修改当前项目的分支为 `main` + +```bash +git branch -M main +``` + +要更改为其他名字 只需把 main 替换即可 + +## 要求 + +Git 版本为 **v2.28** 或更高 查看版本 `git --version` + +## 其他 + +#### 禁止忽略大小写 + +``` +git config core.ignorecase false +``` diff --git "a/docs/skill/git/git \346\216\250\351\200\201\345\244\232\344\270\252\350\277\234\347\250\213\344\273\223\345\272\223.md" "b/docs/skill/git/git \346\216\250\351\200\201\345\244\232\344\270\252\350\277\234\347\250\213\344\273\223\345\272\223.md" new file mode 100644 index 0000000..cd3750a --- /dev/null +++ "b/docs/skill/git/git \346\216\250\351\200\201\345\244\232\344\270\252\350\277\234\347\250\213\344\273\223\345\272\223.md" @@ -0,0 +1,32 @@ +--- +id: git-push-multiple-remote-repos +slug: git-push +title: git 推送多个远程仓库 +date: 2023-11-09 +authors: kuizuo +tags: [git] +keywords: [git] +--- + + + +核心命令 + +```bash +git remote set-url --add origin 远程仓库地址 +``` + +如:`git remote set-url --add origin https://git.kuizuo.cn/kuizuo/blog.git` + +此时打开 `.git/config`,可以看到这样的配置 + +```bash {4} +[remote "origin"] + url = https://github.com/kuizuo/blog.git + fetch = +refs/heads/*:refs/remotes/origin/* + url = https://git.kuizuo.cn/kuizuo/blog.git +``` + +上述命令 git 配置添加新的 url 记录。也可手动添加、修改。 + +`git push origin --all` 推送至所有远程仓库 diff --git "a/docs/skill/git/github actions \347\244\272\344\276\213.md" "b/docs/skill/git/github actions \347\244\272\344\276\213.md" new file mode 100644 index 0000000..26c0567 --- /dev/null +++ "b/docs/skill/git/github actions \347\244\272\344\276\213.md" @@ -0,0 +1,296 @@ +--- +id: github-actions-example +slug: github-actions-example +title: github actions示例 +date: 2021-10-01 +authors: kuizuo +tags: [github, action] +keywords: [github, action] +--- + + + +[GitHub Marketplace · Actions to improve your workflow](https://github.com/marketplace?type=actions) + + +## 测试 输出 + +[Environment variables - GitHub Docs](https://docs.github.com/cn/actions/learn-github-actions/environment-variables) + +[Contexts - GitHub Docs](https://docs.github.com/cn/actions/learn-github-actions/contexts#github-context) + +```yaml title='print.yml' +name: Print +on: push + +jobs: + print-job: + name: Print Job + runs-on: ubuntu-latest + steps: + - name: Print a greeting + env: + MY_VAR: Hi there! My name is + NAME: Kuizuo + run: | + echo $MY_VAR $NAME. + + - name: Print github info + run: | + echo github owner: ${{ github.repository_owner }} + echo github repository: ${{ github.repository }} + echo github workspace ${{ github.workspace }} + +``` + +## 前端项目代码 lint 与 test + +[Setup Node.js environment · Actions · GitHub Marketplace](https://github.com/marketplace/actions/setup-node-js-environment) + +```yaml title='lint.yml' +name: Lint + +on: + push: + branches: + - main + + pull_request: + branches: + - main + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Install pnpm + uses: pnpm/action-setup@v2 + + - name: Set node + uses: actions/setup-node@v3 + with: + node-version: 16.x + cache: pnpm + + - name: Setup + run: npm i -g @antfu/ni + + - name: Install + run: nci + + - name: Lint + run: nr lint +``` + +```yaml title='test.yml' +name: Test + +on: + push: + branches: + - main + + pull_request: + branches: + - main + +jobs: + test: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + node: [14.x, 16.x] + os: [ubuntu-latest] + fail-fast: false + + steps: + - uses: actions/checkout@v3 + + - name: Install pnpm + uses: pnpm/action-setup@v2 + + - name: Set node ${{ matrix.node }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + cache: pnpm + + - run: corepack enable + + - name: Setup + run: npm i -g @antfu/ni + + - name: Install + run: nci + + - name: Build + run: nr build + + - name: Test + run: nr test + + - name: Typecheck + run: nr typecheck +``` + +也可将 jobs 整合在一个文件内 + +## 发布到 GitHub Pages + +[GitHub Pages action](https://github.com/marketplace/actions/github-pages-action) + +```yaml +name: Build and Deploy +on: + push: + branches: + - main +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install and Build + run: | + yarn install + yarn run build + + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + with: + personal_token: ${{ secrets.ACCESS_TOKEN }} + publish_dir: ./dist +``` + +publish_dir 为打包后的文件夹. + +## ssh 部署 + +[ssh deploy · Actions · GitHub Marketplace](https://github.com/marketplace/actions/ssh-deploy) + +```yaml +name: ci + +on: + push: + branches: + - main + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Use Node.js 16 + uses: actions/setup-node@v3 + with: + node-version: '16.x' + + - name: Build Project + run: | + yarn install + yarn run build + + - name: SSH Deploy + uses: easingthemes/ssh-deploy@v2.2.11 + env: + SSH_PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} + ARGS: '-avzr --delete' + SOURCE: 'build' + REMOTE_HOST: ${{ secrets.REMOTE_HOST }} + REMOTE_USER: 'root' + TARGET: '/www/wwwroot/blog' +``` + +SSH_PRIVATE_KEY 是 SSH 密钥,可通过 `ssh-keygen` (生成位置/root/.ssh)或通过服务器管理面板的来生成密钥。后者的话需要绑定服务器实例,并且需要关机,我个人推荐使用后者。 + +## ftp 文件传输 + +```yaml + - name: FTP Deploy + uses: SamKirkland/FTP-Deploy-Action@4.0.0 + with: + server: ${{ secrets.ftp_server }} + username: ${{ secrets.ftp_user }} + password: ${{ secrets.ftp_pwd }} + local-dir: ./build/ + server-dir: ./ +``` + +## 发布 release / npm 包 + +[changesets/action (github.com)](https://github.com/changesets/action) + +```yaml title='release.yml' +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Install pnpm + uses: pnpm/action-setup@v2 + + - name: Set node + uses: actions/setup-node@v3 + with: + node-version: 16.x + cache: pnpm + registry-url: 'https://registry.npmjs.org' + + - run: npx changelogithub + continue-on-error: true + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + + - name: Install Dependencies + run: pnpm i + + - name: PNPM build + run: pnpm run build + + - name: Publish to NPM + run: pnpm -r publish --access public --no-git-checks + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + + - name: Publish to VSCE & OVSX + run: npm run publish + working-directory: ./packages/vscode + env: + VSCE_TOKEN: ${{secrets.VSCE_TOKEN}} + OVSX_TOKEN: ${{secrets.OVSX_TOKEN}} +``` + +## 添加状态徽章 status badge + +[Adding a workflow status badge - GitHub Docs](https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/adding-a-workflow-status-badge) + +创建一个工作流会自动生成状态徽章,地址如下 + +``` +https://github.com///actions/workflows//badge.svg +``` + +示例: + +``` +https://github.com/kuizuo/github-action-example/actions/workflows/ci.yml/badge.svg +``` \ No newline at end of file diff --git "a/docs/skill/git/github apps \347\244\272\344\276\213.md" "b/docs/skill/git/github apps \347\244\272\344\276\213.md" new file mode 100644 index 0000000..c8efa5c --- /dev/null +++ "b/docs/skill/git/github apps \347\244\272\344\276\213.md" @@ -0,0 +1,49 @@ +--- +id: github-apps-example +slug: github-apps-example +title: github apps示例 +date: 2021-10-01 +authors: kuizuo +tags: [github, app] +keywords: [github, app] +--- + + + +### Github Dependabot + +介绍:[About Dependabot security updates - GitHub Docs](https://docs.github.com/cn/code-security/dependabot/dependabot-security-updates/about-dependabot-security-updates) + +简单说就是一个能够自动更新项目依赖,确保仓库代码依赖的包和应用程序一直处于最新版本的机器人。 + +将 `dependabot.yml` 配置文件放入仓库的 `.github` 目录中即可开启。当然也可以到 `Insights` => `Dependency graph` => `Dependabot` 中开启。如下图 + +![image-20221001171946879](https://img.kuizuo.cn/image-20221001171946879.png) + +然后创建你的配置文件,默认内容如下 + +![image-20221001172149673](https://img.kuizuo.cn/image-20221001172149673.png) + +其中要修改 package-ecosystem 配置,也就是包管理器,比如node就用npm,python就用pip。可以在 [About Dependabot version updates - GitHub Docs](https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/about-dependabot-version-updates#supported-repositories-and-ecosystems) 中查看。 + +然后配置完毕后,根据时间周期,就会自动检测依赖更新,并创建一个pull request 请求,仓库拥有者根据实际需要合并即可。 + +### [Stale](https://github.com/marketplace/stale) + +可在一段时间不活动后关闭废弃的问题。即**关闭长时间未回复的issues**。 + +### [Imgbot](https://github.com/marketplace/imgbot) + +Imgbot是一个友好的机器人,可以优化您的图像并节省您的时间。优化的图像意味着在不牺牲质量的情况下减小文件大小。 + +### [giscus](https://github.com/marketplace/giscus) + +由 GitHub 讨论提供支持的评论系统。让访问者通过GitHub在您的网站上发表评论和反应! + +### [WakaTime](https://github.com/marketplace/wakatime) + +从编程活动中自动生成的生产力指标、见解和时间跟踪。 + +### [wxwork](https://github.com/marketplace/wxwork-github-webhook) + +Github 企业微信群机器人,无需配置轻松集成 Github 与 企业微信。 \ No newline at end of file diff --git "a/docs/skill/git/github \345\205\266\344\273\226\346\226\207\344\273\266.md" "b/docs/skill/git/github \345\205\266\344\273\226\346\226\207\344\273\266.md" new file mode 100644 index 0000000..9fb5533 --- /dev/null +++ "b/docs/skill/git/github \345\205\266\344\273\226\346\226\207\344\273\266.md" @@ -0,0 +1,37 @@ +--- +id: github-other +slug: github-other +title: .github 其他文件 +date: 2021-10-01 +authors: kuizuo +tags: [github] +keywords: [github] +--- + + + +### ISSUE_TEMPLATE + +**issues 默认模版** + +[Configuring issue templates for your repository - GitHub Docs](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository) + +### CODEOWNERS + +**仓库代码拥有者。用于合并请求批准** + +``` +# https://help.github.com/articles/about-codeowners/ + +* @kuizuo +``` + +### FUNDING.yml + +**配置赞助者按钮** + +[在仓库中显示赞助者按钮 - GitHub Docs](https://docs.github.com/cn/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository#about-funding-files) + +### CONTRIBUTING.md + +**贡献指南** \ No newline at end of file diff --git a/docs/skill/introduction.md b/docs/skill/introduction.md new file mode 100644 index 0000000..54e3b90 --- /dev/null +++ b/docs/skill/introduction.md @@ -0,0 +1,29 @@ +--- +id: introduction +slug: /skill +title: 技术笔记 +keywords: + - 前端 + - 后端 + - Vue + - React + - JavaScript + - Typescript + - 逆向 + - HTTP + - 算法 +--- + +本页面为个人学习中所涉及相关技术栈的笔记汇总,包含但不限于 + +- Web +- 前端 +- 后端 +- Vue +- React +- JavaScript & TypeScript +- Node +- 安卓 +- 逆向 +- HTTP +- 算法 diff --git "a/docs/skill/js&ts/JS\345\246\202\344\275\225\350\216\267\345\217\226\345\275\223\345\244\251\351\233\266\347\202\271\346\227\266\351\227\264\346\210\263.md" "b/docs/skill/js&ts/JS\345\246\202\344\275\225\350\216\267\345\217\226\345\275\223\345\244\251\351\233\266\347\202\271\346\227\266\351\227\264\346\210\263.md" new file mode 100644 index 0000000..537f66b --- /dev/null +++ "b/docs/skill/js&ts/JS\345\246\202\344\275\225\350\216\267\345\217\226\345\275\223\345\244\251\351\233\266\347\202\271\346\227\266\351\227\264\346\210\263.md" @@ -0,0 +1,43 @@ +--- +id: how-does-js-get-today-zero-timestamp +slug: /how-does-js-get-today-zero-timestamp +title: JS如何获取当天零点时间戳 +date: 2021-08-18 +authors: kuizuo +tags: [javascript] +keywords: [javascript] +--- + + + +## 需求 + +准备做一个签到系统,所以当天的 0 点就成为了判断是否签到过的关键点,那 Js 又如何获取对应的时间戳呢? + +## 实现 + +我一开始是这么实现的,利用到的 js 时间库,moment 或者 dayjs 都行,这里选择 dayjs(因为轻量)。 + +代码如下 + +```js +dayjs(dayjs().format('YYYY-MM-DD')).valueOf() +``` + +moment 的话,只需要将 dayjs 替换成 moment 即可。 + +中间部分取出来的时间为 `“2021-08-18”`,然后再通过 dayjs 转为 Dayjs 对象,并通过 valueOf(),就可获取到当天的零点的时间戳。 + +思路很明确,就是要先获取到当前日期,然后通过日期在转为时间戳即可 + +对应的原生 Js 代码也就很明显了 + +```js +new Date(new Date().toLocaleDateString()).getTime() +``` + +但要我选择我依旧毫不犹豫选择使用 js 时间库,一些复杂的时间计算,如时间格式化,计算两者时间秒/天数差,给指定时间增加/减少天数,这些如果使用原生 Js 代码,不如直接使用已有的库,何必造个轮子呢。 + +有关 dayjs 的具体使用就不做介绍了,贴个官方文档,要用的时候查阅一下便可。 + +[Day.js · 中文文档 - 2kB 大小的 JavaScript 时间日期库](https://day.js.org/zh-CN/) diff --git "a/docs/skill/js&ts/JS\345\256\236\347\216\260\345\207\275\346\225\260\347\274\223\345\255\230.md" "b/docs/skill/js&ts/JS\345\256\236\347\216\260\345\207\275\346\225\260\347\274\223\345\255\230.md" new file mode 100644 index 0000000..752d5ed --- /dev/null +++ "b/docs/skill/js&ts/JS\345\256\236\347\216\260\345\207\275\346\225\260\347\274\223\345\255\230.md" @@ -0,0 +1,117 @@ +--- +id: js-implement-function-cache +slug: /js-implement-function-cache +title: JS实现函数缓存 +date: 2021-11-22 +authors: kuizuo +tags: [javascript] +keywords: [javascript] +--- + + + +## 原理 + +- 闭包 +- 柯里化 + +- 高阶函数 + +## 例子:求和 + +正常的循环累加代码 + +```javascript +function add() { + let sum = 0 + for (let i = 0; i < arguments.length; i++) { + sum += arguments[i] + } + return sum +} +``` + +使用数组的 reduce 方法 + +```javascript +function add() { + var arr = Array.prototype.slice.call(arguments) + return arr.reduce(function (prev, cur) { + return prev + cur + }, 0) +} +``` + +但多次传入同样的参数 如 `add(1, 2, 3)` 都将执行运算对应的次数,将会耗费一定的性能。 + +### 使用函数缓存 + +使用闭包,将每次运算的参数与结果存入置 cache 对象中,如果 cache 中有,便直接获取,来达到缓存的目的 + +```javascript +let add = (function () { + let cache = {} + + return function () { + let args = Array.prototype.join.call(arguments, ',') + if (cache[args]) { + return cache[args] + } + let sum = 0 + for (let i = 0; i < arguments.length; i++) { + sum += arguments[i] + } + return (cache[args] = sum) + } +})() + +add(1, 2, 3) // 输出6 +add(1, 2, 3) // 直接从cache中获取 +``` + +已经达到缓存的目的了,但这时我想将乘法也想实现缓存的目的,那么又得写一大行这样的代码,同时原本求和的代码又想单独分离出来,就可以使用代理模式,具体演示如下 + +### 代理模式 + +#### 创建缓存代理的工厂 + +```javascript +let memoize = function (fn) { + let cache = {} + return function () { + let args = Array.prototype.join.call(arguments, ',') + if (args in cache) { + return cache[args] + } + return (cache[args] = fn.apply(this.arguments)) + } +} +``` + +那么通过`memoize` 就能将函数运行后的结果给缓存起来,如 + +```javascript +let add1 = memoize(add) + +add1(1, 2, 3) // 输出6 +add1(1, 2, 3) // 直接从cache中获取 +``` + +我们只需要编写我们正常的业务逻辑(加法,乘法等),然后通过 memoize 调用 便可达到缓存的目的 + +同理乘法 + +```javascript +function mult() { + let a = 0 + for (let i = 0; i < arguments.length; i++) { + a *= arguments[i] + } + return a +} + +let mult1 = memoize(mult) + +mult1(1, 2, 3) // 输出6 +mult1(1, 2, 3) // 直接从cache中获取 +``` diff --git "a/docs/skill/js&ts/JS\346\211\223\345\215\260\345\207\275\346\225\260\350\260\203\347\224\250\346\240\210.md" "b/docs/skill/js&ts/JS\346\211\223\345\215\260\345\207\275\346\225\260\350\260\203\347\224\250\346\240\210.md" new file mode 100644 index 0000000..ca98256 --- /dev/null +++ "b/docs/skill/js&ts/JS\346\211\223\345\215\260\345\207\275\346\225\260\350\260\203\347\224\250\346\240\210.md" @@ -0,0 +1,272 @@ +--- +id: js-print-stack-of-function +slug: /js-print-stack-of-function +title: JS输出函数调用栈 +date: 2021-10-15 +authors: kuizuo +tags: [javascript, callstack] +keywords: [javascript, callstack] +--- + + + +最近在编写 JS 逆向 hook 类插件,然后需要获取当前代码执行时所在的位置,方便代码定位,于是就总结下 JavaScript 如何输出函数调用栈。 + +## 演示代码 + +```javascript +function main() { + let a = fun('hello world') + console.log(a) +} + +function fun(a) { + return a +} + +main() +``` + +## 方法 + +### console.trace() + +使用如下 + +```javascript {7} +function main() { + let a = fun('hello world') + console.log(a) +} + +function fun(a) { + console.trace('fun') + return a +} + +main() +``` + +输出结果为 + +``` +Trace: fun + at fun (c:\Users\zeyu\Desktop\demo\main.js:7:11) + at main (c:\Users\zeyu\Desktop\demo\main.js:2:11) + at Object. (c:\Users\zeyu\Desktop\demo\main.js:11:1) at Module._compile (node:internal/modules/cjs/loader:1095:14) + at Object.Module._extensions..js (node:internal/modules/cjs/loader:1124:10) + at Module.load (node:internal/modules/cjs/loader:975:32) + at Function.Module._load (node:internal/modules/cjs/loader:816:12) + at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:79:12) + at node:internal/main/run_main_module:17:47 +hello world +``` + +其中`console.trace()`可以传入参数,最终都将直接输出在 Trace 后面,如这里的 fun,但只能在控制台中输出 + +不过 IE6 并不支持,不过应该也没人用了吧 + +### arguments.callee.caller + +在**非严格模式**下,可以直接输出`arguments`,便会打印出所调用的参数,以及调用的函数,使用如下 + +```javascript {7-10} +function main() { + let a = fun('hello world') + console.log(a) +} + +function fun(a) { + console.log(fun.caller.toString()) + console.log(arguments) + console.log(arguments.callee.toString()) + console.log(arguments.callee.caller.toString()) + + return a +} + +main() +``` + +输出结果为 + +``` +function main() { + let a = fun('hello world') + console.log(a) +} +[Arguments] { '0': 'hello world' } +function fun(a) { + console.log(fun.caller.toString()) + console.log(arguments) + console.log(arguments.callee.toString()) + console.log(arguments.callee.caller.toString()) + + return a +} +function main() { + let a = fun('hello world') + console.log(a) +} +hello world +``` + +成功的将我们当前运行的函数给打印了出来(这里使用 toString 方便将函数打印出来),而上级的函数的话通过`fun.caller`和`arguments.callee.caller`都能得到。 + +![image-20211015094231693](https://img.kuizuo.cn/image-20211015094231693.png) + +`caller`便是调用的上层函数,也就是这里的 main 函数,不难发现每个 caller 对象下都有一个 caller 属性,也就是`caller`的上层函数,由于我这里是 node 环境,所以这里的 caller 的 caller 我也不知道是个什么玩意。。。反正这不是所要关注的重点,重点是**`fun.caller`和``arguments.callee.caller`便可以打印出上层函数**,直到 caller 为空 + +另外圈的`[[FunctionLocation]]`便是函数所在位置,不过可惜是,这个并不是 caller 的属性,仅供 js 引擎使用的,所以无法输出。 + +总结下来: + +**fun.caller == arguments.callee.caller 代表 fun 的执行环境 (上层函数)** + +**arguments.callee 代表的是正在执行的 fun** + +**前提: 非严格模式下** + +### new Error().stack + +众所周知,程序一旦出错 W,便会直接停止运行,同时输出报错信息,而这里的报错信息就包括调用的函数以及具体位置,相对于上面的方法而言,这个能直接在执行环境中输出,而不是单纯的在控制台显示。 + +同样还是上面的代码 + +```javascript {7,11-14} +function main() { + let a = fun('hello world') + console.log(a) +} + +function fun(a) { + printStack() + return a +} + +function printStack() { + let stack = new Error().stack + console.log(stack) +} + +main() +``` + +输出的结果为一串字符串,如下 + +``` +Error + at printStack (c:\Users\zeyu\Desktop\demo\main.js:12:16) + at fun (c:\Users\zeyu\Desktop\demo\main.js:7:3) + at main (c:\Users\zeyu\Desktop\demo\main.js:2:11) + at Object. (c:\Users\zeyu\Desktop\demo\main.js:16:1) at Module._compile (node:internal/modules/cjs/loader:1095:14) + at Object.Module._extensions..js (node:internal/modules/cjs/loader:1124:10) + at Module.load (node:internal/modules/cjs/loader:975:32) + at Function.Module._load (node:internal/modules/cjs/loader:816:12) + at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:79:12) + at node:internal/main/run_main_module:17:47 +hello world +``` + +由于结果是个字符串,所以通过 split 分割一下,便能得到调用的函数(fun)以及调用位置(c:\Users\zeyu\Desktop\demo\main.js:7:3),稍加处理一下,如下 + +```javascript {7} +function main() { + let a = fun('hello world') + console.log(a) +} + +function fun(a) { + printStack() + return a +} + +main() + +function printStack() { + const callstack = new Error().stack.split('\n') + callstack.forEach((s) => { + let matchArray = s.match(/at (.+?) \((.+?)\)/) + if (!matchArray) return + + let name = matchArray[1] + let location = matchArray[2] + console.log(name, location) + }) +} +``` + +输出结果如下(由于是 Node 环境,所以会输出一些有关模块 modules 的东西) + +``` +printStack c:\Users\zeyu\Desktop\demo\main.js:14:21 +fun c:\Users\zeyu\Desktop\demo\main.js:7:3 +main c:\Users\zeyu\Desktop\demo\main.js:2:11 +Object. c:\Users\zeyu\Desktop\demo\main.js:11:1 +Module._compile node:internal/modules/cjs/loader:1095:14 +Object.Module._extensions..js node:internal/modules/cjs/loader:1124:10 +Module.load node:internal/modules/cjs/loader:975:32 +Function.Module._load node:internal/modules/cjs/loader:816:12 +Function.executeUserEntryPoint [as runMain] node:internal/modules/run_main:79:12 +hello world +``` + +### Error.captureStackTrace + +Error 中有一个静态方法,同样用于获取调用栈。演示代码如下 + +```js +function main() { + let a = fun('hello world') + console.log(a) +} + +function fun(a) { + let stack = stackTrace() + console.log(stack) + + return a +} + +function stackTrace() { + const obj = {} + Error.captureStackTrace(obj, stackTrace) + return obj.stack +} + +main() +``` + +效果和`new Error().stack`一样,只不过少了一行~~at printStack (c:\Users\zeyu\Desktop\demo\main.js:12:16)~~ 的输出。 + +不过一般用法如下 + +```js +function MyError() { + Error.captureStackTrace(this, MyError) +} + +// 如果没有向captureStackTrace传递MyError参数,则在访问.stack属性时,MyError及其内部信息将会出现在堆栈信息中。当传递MyError参数时,这些信息会被忽略。 +new MyError().stack +``` + +其中`Error.captureStackTrace()`源自[V8 引擎的 Stack Trace API](https://link.segmentfault.com/?enc=u3YSqa2uqpuK4qOK1mcE%2BQ%3D%3D.S7z7nzmOapoEFtq3WEZcXOIYfU79dXMyMCaHOU3pUVILksNiqpAhLEXacnQs0fHN),在自定义 Error 类的内部经常会使用该函数,用以在 error 对象上添加合理的 stack 属性。上文中的 MyError 类即是一个最简单的例子。 + +```js +function MyError() { + Error.captureStackTrace(this, MyError) +} + +// 如果没有向captureStackTrace传递MyError参数,则在访问.stack属性时,MyError及其内部信息将会出现在堆栈信息中。当传递MyError参数时,这些信息会被忽略。 +new MyError().stack +``` + +[关于 Error.captureStackTrace - SegmentFault 思否](https://segmentfault.com/a/1190000007076507) + +## 总结 + +如果是作为调试阶段,想输出调用栈的话,那么`console.trace()`肯定是个最好的选择,不过只能在控制台显示,无法在运行环境中使用 + +而`arguments.callee.caller`使用的前提是非严格模式下,所以要使用的话,则需要删除`"use strict";`代码, 但能直接打印出完整的函数,以及调用所传入的参数。 + +`new Error().stack` 相当于主动报错,由于报错会自动打印报错所在的调用信息,所以能精确的定位到代码的函数名和代码行与列,对于后续要定位代码位置而言优先选择。 diff --git "a/docs/skill/js&ts/JS\346\225\260\347\273\204\345\257\271\350\261\241\345\216\273\351\207\215.md" "b/docs/skill/js&ts/JS\346\225\260\347\273\204\345\257\271\350\261\241\345\216\273\351\207\215.md" new file mode 100644 index 0000000..aa901cb --- /dev/null +++ "b/docs/skill/js&ts/JS\346\225\260\347\273\204\345\257\271\350\261\241\345\216\273\351\207\215.md" @@ -0,0 +1,68 @@ +--- +id: js-array-object-unique +slug: /js-array-object-unique +title: JS数组对象去重 +date: 2021-07-05 +authors: kuizuo +tags: [javascript] +keywords: [javascript] +--- + + + +参考 [数组对象去重](https://www.nodejs.red/#/javascript/base?id=数组去重的三种实现方式) + +数据如下: + +```js +[{ name: 'zs', age: 15 }, { name: 'lisi' }, { name: 'zs' }] +``` + +想要将 name 为 zs 的数据去重,优先保留第一条相同数据 + +## 解决方法 + +### reduce 去重 + +```js +let hash = {} + +function unique(arr, initialValue) { + return arr.reduce(function (previousValue, currentValue, index, array) { + hash[currentValue.name] ? '' : (hash[currentValue.name] = true && previousValue.push(currentValue)) + + return previousValue + }, initialValue) +} + +const uniqueArr = unique([{ name: 'zs', age: 15 }, { name: 'lisi' }, { name: 'zs' }], []) + +console.log(uniqueArr) // uniqueArr.length == 2 +``` + +### lodash 工具库去重 + +[Lodash Documentation](https://lodash.com/docs/4.17.15#uniqBy) + +```js +_.uniqBy([{ x: 1 }, { x: 2 }, { x: 1 }], 'x') + +// => [{ 'x': 1 }, { 'x': 2 }] + +// 指定条件 +_.uniqBy([2.1, 1.2, 2.3], Math.floor) +// => [2.1, 1.2] +``` + +想要所有对象属性都一样才去重也简单 + +```js +var objects = [ + { x: 1, y: 2 }, + { x: 2, y: 1 }, + { x: 1, y: 2 }, +] + +_.uniqWith(objects, _.isEqual) +// => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }] +``` diff --git "a/docs/skill/js&ts/TypeScript\351\253\230\347\272\247\350\257\255\346\263\225.md" "b/docs/skill/js&ts/TypeScript\351\253\230\347\272\247\350\257\255\346\263\225.md" new file mode 100644 index 0000000..6db1d3d --- /dev/null +++ "b/docs/skill/js&ts/TypeScript\351\253\230\347\272\247\350\257\255\346\263\225.md" @@ -0,0 +1,404 @@ +--- +id: typescript-advanced-grammar +slug: /typescript-advanced-grammar +title: TypeScript高级语法 +date: 2022-06-25 +authors: kuizuo +tags: [typescript] +keywords: [typescript] +--- + + + +在线运行 TypeScript [https://www.typescriptlang.org/play](https://www.typescriptlang.org/play) + +## typeof + +```typescript +typeof val +``` + +获取对象的类型 + +1. 已知一个 javascript 变量,通过 typeof 就能直接获取其类型 + +```typescript +const str = 'foo' +typeof str === 'string' // true + +const user = { + name: 'kuizuo', + age: 12, + address: { + province: '福建', + city: '厦门', + }, +} + +type User = typeof user +// { +// name: string; +// age: number; +// address: { +// province: string; +// city: string; +// }; +// } + +type Address = (typeof user)['address'] +// { +// province: string; +// city: string; +// } +``` + +2. 获取函数的类型(参数类型与返回值类型) + +```typescript +function add(a: number, b: number): number { + return a + b +} + +type AddType = typeof add +// (a: number, b: number) => number + +type AddReturnType = ReturnType +// number + +type AddParameterType = Parameters +// [a: number, b: number] +``` + +## keyof + +```typescript +keyof T +``` + +获取 T 类型中的所有 key,类似与 Object.keys(object) + +根据 key 获取对象其属性的例子 + +```typescript +function getProperty(obj: T, key: K) { + return obj[key] +} +``` + +上面代码有很好的代码提示,并且如果获取的 key 不在其对象中,将会直接报错。 + +对于一些常用类型的 keyof 值 + +```typescript +type K0 = keyof string +// number | typeof Symbol.iterator | "toString" | "charAt" | ... more +type K1 = keyof boolean +// "valueOf" +type K2 = keyof number +// "toString" | "valueOf" | "toFixed" | "toExponential" | "toPrecision" | "toLocaleString" +type K3 = keyof any +// string | number | symbol +``` + +## 交叉类型 + +& 交叉运算符 + +类似集合中的交集,满足以下特性 + +1. 唯一性:A & A 等价于 A +2. 满足交换律:A & B +3. 满足结合律:(A & B) & C 等价于 A & (B & C) +4. 父类型收敛:如果 B 是 A 的父类型,那么 A & B 将收敛为 A 类型. + +任何与 never 交叉的类型都是 nerver,any 交叉的类型为 any(除了 nerver) + +```typescript +type A0 = any & 1 // any +type A1 = any & boolean // any +type A2 = any & never // never + +type A3 = string & number // never +``` + +## 映射类型 + +```typescript +{ [P in K]: T } +``` + +其中 in 类似与 for ...in 语句,而 T 类型表示任意类型。遍历 K 类型的所有 key,生成 P : T,例如 + +```typescript +interface Todo { + title: string + description: string + completed: boolean +} + +type Demo = { [P in keyof T]: T } +type Todo1 = Demo +// { +// title: string +// description: string +// completed: boolean +// } +``` + +上面代码看似没有任何映射关系,因为在映射类型中可以给对其添加`readonly `和 `?` 只读与可选修饰符,以及`+` `-` 增加与删除修饰符(默认为+)例如 + +```typescript +{ [ P in K] :T } +{ [ P in K] ?:T } +{ [ P in K] -?:T } + +{ readonly [ P in K] :T } +{ readonly [ P in K] ?:T } +{ -readonly [ P in K] ?:T } + +``` + +就可以实现一些 TypeScript 的内置工具类(给对象属性只读,可选等等) + +```typescript +type MyPick = { + [P in K]: T[P] +} + +type MyPartial = { + [P in keyof T]?: T[P] +} + +type MyRequired = { + [P in keyof T]-?: T[P]; +} + +type MyReadonly = { + readonly [P in keyof T]: T[P] +} + +... +``` + +## 条件类型 + +```typescript +T extends U ? X : Y +``` + +其代码语法类似与三元运算符, + +1. 如果 T 和 U 都为基本类型两侧相同,则 extends 在语义上可以理解为 === + +```typescript +type Demo1 = 'foo' extends 'bar' ? true : false // false +type Demo2 = 'c' extends 'c' ? true : false // true +``` + +2. 若位于 extends 右侧的类型包含位于 extends 左侧的类型(即**狭窄类型 extends 宽泛类型**)时,结果为 true,反之为 false。 + +```typescript +type Demo3 = string extends string | number ? true : false // true +``` + +3. 当 extends 作用于**对象**时,若在对象中指定的 key 越多,则其类型定义的范围越狭窄。 + +```typescript +type Demo4 = { a: true; b: false } extends { a: true } ? true : false // true +``` + +4. 作用于联合类型中,且 T 为**裸类型参数**(无`T[] [T] Promise` 等类型包装过),那么则为**分布式条件类型**,对于该类型来说,当 T 为联合类型时,运算过程会被分解为多个分支(类似于乘法分配律),那么返回的类型也将是多个类型。 + +分布式条件类型的特点:“裸”类型、类型参数、联合类型参数会触发分支。 + +```typescript +type Demo5 = T extends U ? never : T +type Demo6 = Demo5<'a' | 'b' | 'c' | 'd', 'c' | 'd'> // 'a' | 'b' +``` + +例如上面定义的 Demo5,其实也就是 TypeScript 内置工具类的[`Exclude`](https://www.typescriptlang.org/docs/handbook/utility-types.html#excludeuniontype-excludedmembers)的实现,所返回的结果是 `'a' | 'b'`,其内部的实现相当于 + +```typescript +'a' extends 'c' | 'd' ? never : 'a' // 'a' +'b' extends 'c' | 'd' ? never : 'b' // 'b' +'c' extends 'c' | 'd' ? never : 'c' // never +'d' extends 'c' | 'd' ? never : 'd' // never +// 执行四次条件类型,最终合并得到 'a' | 'b' + +``` + +但如果 T 不能**裸类型参数**类型,那么便不会做**分布式条件类型**,返回的结果便只有一个。 + +## 类型推断 + +```typescript +type Demo = T extends (infer U)[] ? U : T +``` + +如果 T 为`string[]`类型,那么 infer 可以推导出 U 为 string 类型 + +注:infer 只能在**条件类型的`extends`子句**中才允许`infer`声明,且只能**在条件分支中 true 中**使用 + +下列语句都将报错 + +```typescript +type Wrong1 = T[0] + +type Wrong2 = (infer U)[] extends T ? U : T + +type Wrong3 = T extends (infer U)[] ? T : U +``` + +一些例子 + +```typescript +type Unpacked = T extends (infer U)[] + ? U + : T extends (...args: any[]) => infer U + ? U + : T extends Promise + ? U + : T + +type T0 = Unpacked // string +type T1 = Unpacked // string +type T2 = Unpacked<() => string> // string +type T3 = Unpacked> // string +type T4 = Unpacked[]> // Promise +type T5 = Unpacked | string> // string | Promise +``` + +通过 infer 就可以推导出函数的参数类型与返回值类型 + +```typescript +const fn = (v: boolean) => { + if (v) return 1 + else return 2 +} + +type MyReturnType = T extends (...args: any[]) => infer R ? R : any + +type MyParameterType = T extends (...args: infer P) => any ? P : any + +type FnReturnType = MyReturnType +// 1 | 2 +type FnParameterType = MyParameterType +// [v: boolean] +``` + +## 声明文件 + +我个人习惯会在根目录创建 types 文件夹,里面存放 d.ts 声明文件,同时 tsconfig.json 中配置 `"include": ["src/**/*.ts", "types/**/*.d.ts"]` + +创建一个全局声明文件`global.d.ts`,使用 declare 关键字来声明 + +```typescript title="global.d.ts" +declare module 'foo' { + export var bar: number +} +``` + +此时就可以在其他文件中`import * as foo from 'foo'`,即便没有安装 foo 模块,但是 foo 依然有 bar 属性提示,这在一些第三方使用 js 所编写的库中经常遇到。在例如我想给我的 axios 封装些自己定义的代码,同时还带有类型提示,那么就可以使用声明文件,如下 + +```typescript title="global.d.ts" +import * as axios from 'axios' + +declare module 'axios' { + export interface Axios { + myget: (url: string, config?: AxiosRequestConfig) => Promise + } +} +``` + +```typescript title="demo.ts" +import axios, { AxiosRequestConfig } from 'axios' + +axios.myget = async (url: string, config?: AxiosRequestConfig) => { + console.log(url) + return axios.get(url, config) +} +``` + +## type 和 interface 区别 + +### 相同点 + +1. 都可以用来描述对象或函数 + +2. 类型别名和接口都支持扩展 + +```typescript +type User = { + name: string +} + +type User1 = User & { age: number } +``` + +```typescript +interface User { + name: string +} + +interface User1 extends User { + age: number +} +``` + +### 不同点 + +1. 同名接口会自动合并,而类型别名不会 + +```typescript +interface User { + name: string +} + +interface User { + age: number +} + +const user: User = { + name: 'kuizuo', + age: 20, +} +``` + +```typescript +type User = { + name: string +} + +type User = { + age: number +} +// 标识符“User”重复。 +``` + +### 使用场景 + +#### type 的使用场景 + +- 定义基本类型 +- 定义元组类型 +- 定义函数类型 +- 定义联合类型 +- 定义映射类型 + +#### interface 的使用场景 + +- 利用接口自动合并特性,在第三方库中可以对其进行接口扩展 + +- 定义对象类型且无需使用 type 时 + +## TypeScript 内置工具类 + +[TypeScript: Documentation - Utility Types (typescriptlang.org)](https://www.typescriptlang.org/docs/handbook/utility-types.html) + +## 相关文档与练习 + +[TypeScript: JavaScript With Syntax For Types. (typescriptlang.org)](https://www.typescriptlang.org/) + +[深入理解 TypeScript | 深入理解 TypeScript (jkchao.github.io)](https://jkchao.github.io/typescript-book-chinese/) + +[type-challenges/type-challenges: Collection of TypeScript type challenges with online judge (github.com)](https://github.com/type-challenges/type-challenges) diff --git "a/docs/skill/js&ts/\345\270\270\347\224\250util.js.md" "b/docs/skill/js&ts/\345\270\270\347\224\250util.js.md" new file mode 100644 index 0000000..8ee24fb --- /dev/null +++ "b/docs/skill/js&ts/\345\270\270\347\224\250util.js.md" @@ -0,0 +1,171 @@ +--- +id: commonly-used-util.js +slug: /commonly-used-util.js +title: 常用util.js +date: 2020-10-21 +authors: kuizuo +tags: [js, util] +keywords: [js, util] +--- + +记录一下自己在 js 学习中常用到的一些方法,进行封装使用 + + + +## 1.时间格式解析 + +首当其冲的就是这个时间格式解析了,js 的 Date 中有一个方法`toLocaleString()` 返回的结果为本地时间,如`new Date().toLocaleString()`返回为`2020/10/21 上午1:03:17`,好像看着并没有什么问题,但是我如果要将`2020/10/21 上午1:03:17`转为时间戳的话,也就是执行`new Date("2020/10/21 上午5:03:17").getTime()`,然而它却返回`NaN`,不合理啊,时间格式难道不是这样的吗,时间格式还真不是这样,上面只是显示为本地的时间,然而对于 js 而言,它只识别`yyyy-MM-dd HH:mm:ss`这样的时间格式。于是就需要对返回的时间格式进行操作了。 + +可以通过下方的解析函数,并带上对应的时间格式返回给我对应的时间,代码就不分析了,我也是借鉴网络上的一些格式化时间代码,修改而来的。 + +```js +function parseTime(time, cFormat) { + if (arguments.length === 0 || !time) { + return null + } + const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' + let date + if (typeof time === 'object') { + date = time + } else { + if (typeof time === 'string') { + if (/^[0-9]+$/.test(time)) { + time = parseInt(time) + } else { + time = time.replace(new RegExp(/-/gm), '/') + } + } + + if (typeof time === 'number' && time.toString().length === 10) { + time = time * 1000 + } + date = new Date(time) + } + const formatObj = { + y: date.getFullYear(), + m: date.getMonth() + 1, + d: date.getDate(), + h: date.getHours(), + i: date.getMinutes(), + s: date.getSeconds(), + a: date.getDay(), + } + const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => { + const value = formatObj[key] + if (key === 'a') { + return ['日', '一', '二', '三', '四', '五', '六'][value] + } + return value.toString().padStart(2, '0') + }) + return time_str +} +``` + +## 2.计算过去时间距离现在时间差 + +上面说到的是时间结构的解析,但有时候需要计算过去时间与现在的时间差,比如计算评论发布的时间。这个我也放一个对应的相关代码 + +```js +function formatTime(time, option) { + if (('' + time).length === 10) { + time = parseInt(time) * 1000 + } else { + time = +time + } + const d = new Date(time) + const now = Date.now() + + const diff = (now - d) / 1000 + + if (diff < 30) { + return '刚刚' + } else if (diff < 3600) { + // less 1 hour + return Math.ceil(diff / 60) + '分钟前' + } else if (diff < 3600 * 24) { + return Math.ceil(diff / 3600) + '小时前' + } else if (diff < 3600 * 24 * 2) { + return '1天前' + } + if (option) { + return parseTime(time, option) + } else { + return d.getFullYear() + '年' + (d.getMonth() + 1) + '月' + d.getDate() + '日' + d.getHours() + '时' + d.getMinutes() + '分' + } +} +``` + +这里提一下`moment.js`,一个 js 日期处理的类库,有兴趣的可以去了解一下 [moment.js](http://momentjs.cn/) + +## 3.取随机数,字母 + +js 提供了获取随机数的方法`Math.random()` ,但返回的是一个获取 0-1 之间的随机数,如`0.8790767725487598`,当然,这肯定不是我们想要的,我要的只是一个 0-9 数字,很简单,只需要将上面获取到的随机数乘 10,然后取个位数不就成了。对应的也就是 + +`parseInt(Math.random() * 10)` + +有时候肯定不只是要 0-9 之间,可能是要 0-100 的,原理一样,对应的换算公式如下 + +获取 N-M 的随机数 `parseInt(Math.random() * (M - N + 1) + N)` + +封装成如下对应代码 + +```js +function ranNum(min, max) { + if (arguments.length === 0) { + return parseInt(Math.random() * 10) + } + return parseInt(Math.random() * (max - min + 1) + min) +} +``` + +对应的获取随机字母也简单,只要通过 ASCII 码 A 为 65,Z 为 90,然后获取随机数 0-25,通过`String.fromCharCode`传入对应的 ASCII 码即可,如下 + +```js +function ranChar() { + return String.fromCharCode(65 + parseInt(Math.random() * 25)) +} +``` + +## 4.查询字符串与 json 互转 + +这里我在我的另一篇文章 [查询字符串与 JSON 互转](./查询字符串与JSON互转.md) 中有写到了,这里就不在做过多叙述了 + +## 5.提取 url 中的 Query 对象 + +```js +function getQueryObject(url) { + url = url == null ? window.location.href : url + const search = url.substring(url.lastIndexOf('?') + 1) + const obj = {} + const reg = /([^?&=]+)=([^?&=]*)/g + search.replace(reg, (rs, $1, $2) => { + const name = decodeURIComponent($1) + let val = decodeURIComponent($2) + val = String(val) + obj[name] = val + return rs + }) + return obj +} +``` + +## 6.深拷贝 + +浅拷贝就不说了,`Object.assign`就能解决了,有关 js 对象拷贝这里也不做过多的赘述,随便一搜就有各种相关的。这里就贴一个深拷贝的相关代码。 + +```js +function deepClone(source) { + if (!source && typeof source !== 'object') { + throw new Error('error arguments', 'deepClone') + } + const targetObj = source.constructor === Array ? [] : {} + Object.keys(source).forEach((keys) => { + if (source[keys] && typeof source[keys] === 'object') { + targetObj[keys] = deepClone(source[keys]) + } else { + targetObj[keys] = source[keys] + } + }) + return targetObj +} +``` diff --git "a/docs/skill/js&ts/\346\237\245\350\257\242\345\255\227\347\254\246\344\270\262\344\270\216JSON\344\272\222\350\275\254.md" "b/docs/skill/js&ts/\346\237\245\350\257\242\345\255\227\347\254\246\344\270\262\344\270\216JSON\344\272\222\350\275\254.md" new file mode 100644 index 0000000..9349a46 --- /dev/null +++ "b/docs/skill/js&ts/\346\237\245\350\257\242\345\255\227\347\254\246\344\270\262\344\270\216JSON\344\272\222\350\275\254.md" @@ -0,0 +1,179 @@ +--- +id: querystring-and-json-convert +slug: /querystring-and-json-convert +title: 查询字符串与JSON互转 +date: 2022-03-15 +authors: kuizuo +tags: [http, javascript] +keywords: [http, javascript] +--- + + + +## 查询字符串与 JSON 互转 + +在发送 HTTP 请求的时候,要模拟一个登录请求的包,而抓到得包如下 + +```http +POST https://xxx.xxx.com/xxx/login HTTP/1.1 +Host: xxx.xxx.com +Connection: keep-alive +Upgrade-Insecure-Requests: 1 +User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3775.400 QQBrowser/10.6.4208.400 +Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 +Content-Type: application/x-www-form-urlencoded + +username=kuizuo&password=a12345678 +``` + +但是我要模拟这样的请求就要写成如下方式 + +```javascript +let url = 'https://xxx.xxx.com/xxx/login' +let username = 'kuizuo' +let password = 'a12345678' + +let data = 'username=' + username + '&password=' + password +// or +// let data = `username=${username}&password=${password}` + +axios.post(url, data).then(function (res) { + console.log(res.data) +}) +``` + +像这种 `username=kuizuo&password=a12345678`就称之为查询字符串。显而易见,如果涉及到的参数一多修改显得十分不可靠(**极易改错**)。 + +所以一般的做法都是将 data 用 js 对象或者用 json 格式表示,像下面这样 + +```javascript +let username = 'kuizuo' +let password = 'a12345678' +let data = { + username: username, + password: password, +} +``` + +不过请求头是`Content-Type: application/x-www-form-urlencoded`,那么就需要使用工具将其转化为查询字符串了。比方说 node 中自带的 [querystring](http://nodejs.cn/api/querystring.html) 库。 + +### querystring + +```javascript +const qs = require('querystring') + +let obj = { + username: 'kuizuo', + password: 'a12345678', +} +let data = qs.stringify(obj) +// username=kuizuo&password=a12345678 +``` + +```javascript +const qs = require('querystring') + +let data = 'username=kuizuo&password=a12345678' +let json = qs.parse(data) +// { username: 'kuizuo', password: 'a12345678' } +``` + +### 使用正则与 array.reduce + +除了借用 querystring 库之外,实际还可以通过正则匹配与`array.reduce()`,将查询字符串 js 对象。这里就放一下对应的代码: + +```javascript +function qs2Json(str) { + return (str.match(/([^=&]+)(=([^&]*))/g) || []).reduce((a, val) => ((a[val.slice(0, val.indexOf('='))] = val.slice(val.indexOf('=') + 1)), a), {}) +} +``` + +js 对象转查询字符串就相对简单许多了,只需要对 js 对象遍历,然后使用使用&拼接即可。具体转化代码 + +```javascript +function json2Qs(obj) { + return Object.keys(obj) + .map((key) => { + return key + '=' + obj[key] + }) + .join('&') +} +``` + +不过这里遍历的时候还可以添加一些判断的,比如`if (obj[key] === undefined) return ''`,如果键值未定义就返回空字符串,或者清除数组一些为空字符串或 null 等值,这里我就不做过多判断了。 + +至于要转成 json 格式字符串还是解析 通过`JSON.stringify` 与 `JSON.parse`即可,这里就不在演示了。 + +最终两者的执行效果 + +```javascript +let obj = qs2Json('username=kuizuo&password=a12345678') +// {username: "kuizuo", password: "a12345678"} + +let param = json2Qs({ username: 'kuizuo', password: 'a12345678' }) +// username=kuizuo&password=a12345678 +``` + +### URLSearchParams + +除了 querystring,实际上还有一个更好的库 [URLSearchParams](http://nodejs.cn/api/url.html#class-urlsearchparams),具体的使用如下 + +```javascript +const params = new URLSearchParams({ + user: 'abc', + query: 'xyz', +}) +console.log(params.toString()) +// 'user=abc&query=xyz' +``` + +```javascript +let params = new URLSearchParams('user=abc&query=xyz') +let json = {} +for (const [key, value] of newSearchParams) { + json[key] = value +} +console.log(json) +// { user: 'abc', query: 'xyz' } +``` + +关于`URLSearchParams`更多的可以去官方查看,主要是针对 url 的一个操作,不过我个人更倾向于使用`querystring`,主要原因还是`URLSearchParams`对中文使用的是 js 中的`encodeURIComponent`与`decodeURIComponent`,也就是`UTF8`编码,如果是`GBK`编码就会编码错误。而`querystring`可以指定编码(针对 gbk 的 url 编解码有个[gbk-nice](https://www.npmjs.com/package/gbk-nice)的库 也就是 gbk 版的`encodeURIComponent`) + +## Cookie 与 JSON 互转 + +除了查询字符串需要互转,cookie 数据也可能需要互转。 + +```javascript +Cookie: _uuid=E4842E42-D3DC-2425-C598-231821AB344B39943infoc; buvid3=C844F66D-EC25-4712-8FF3-A0B65DF172C6155806infoc; sid=cvzaog1s; DedeUserID=35745471; DedeUserID__ckMd5=24ac8c69051043f3; SESSDATA=fc469231%2C1608969153%2C1bd79*61; +``` + +要转化为下面的方式 + +```json +{ + "_uuid": "E4842E42-D3DC-2425-C598-231821AB344B39943infoc", + "buvid3": "C844F66D-EC25-4712-8FF3-A0B65DF172C6155806infoc", + "sid": "cvzaog1s", + "DedeUserID": "35745471", + "DedeUserID__ckMd5": "24ac8c69051043f3", + "SESSDATA": "fc469231%2C1608969153%2C1bd79*61" +} +``` + +主要是修改 qs2Json 与 json2Qs 这两个方法,放上对应的 js 代码。 + +```javascript +function cookies2Obj(cookies) { + return cookies.split('; ').reduce((a, val) => ((a[val.slice(0, val.indexOf('=')).trim()] = val.slice(val.indexOf('=') + 1).trim()), a), {}) +} + +function obj2Cookies(obj) { + return Object.keys(obj) + .map((key) => { + return key + '=' + obj[key] + }) + .join('; ') +} +``` + +效果就请读者自行尝试了。 diff --git "a/docs/skill/js&ts/\351\207\215\346\236\204\344\271\213\345\244\232\346\200\201\345\217\226\344\273\243\346\235\241\344\273\266\345\210\206\346\224\257.md" "b/docs/skill/js&ts/\351\207\215\346\236\204\344\271\213\345\244\232\346\200\201\345\217\226\344\273\243\346\235\241\344\273\266\345\210\206\346\224\257.md" new file mode 100644 index 0000000..19e3f10 --- /dev/null +++ "b/docs/skill/js&ts/\351\207\215\346\236\204\344\271\213\345\244\232\346\200\201\345\217\226\344\273\243\346\235\241\344\273\266\345\210\206\346\224\257.md" @@ -0,0 +1,312 @@ +--- +id: polymorphic-elimination-conditional-branching-refactor +slug: /polymorphic-elimination-conditional-branching-refactor +title: 重构之多态消除条件分支 +date: 2021-12-07 +authors: kuizuo +tags: [javascript, refactor] +keywords: [javascript, refactor] +--- + + + +最近翻看之前写过一个项目,其中用到了大量的 switch-case 分支语句,大致的代码结构如下 + +## 代码演示 + +```javascript +class A { + run() { + console.log(A.name, 'run1!'); + } +} + +class B { + run() { + console.log(B.name, 'run2!'); + } +} + +class C { + run() { + console.log(C.name, 'run3!'); + } +} + +function fun1(type) { + let temp; + switch (type) { + case 1: + temp = new A(); + break; + case 2: + temp = new B(); + break; + case 3: + temp = new C(); + break; + default: + throw new Error('Unsupported types'); + break; + } + temp.run(); +} + +fun1(1); +``` + +可以发现每个分支都有个共同的特点,通过构造函数实例化对象 a,调用`a.task`方法 + +``` +temp = new A() +``` + +但现在有一个需求 给 A,B,C 三个类分别加上`getUser`方法,用来获取传入的 user,代码如下 + +```javascript +class A { + constructor(user) { + this.user = user; + } + getUser() { + return this.user; + } + run() { + console.log(A.name, 'run1!'); + } +} + +class B { + constructor(user) { + this.user = user; + } + getUser() { + return this.user; + } + run() { + console.log(B.name, 'run2!'); + } +} + +class C { + constructor(user) { + this.user = user; + } + getUser() { + return this.user; + } + run() { + console.log(C.name, 'run3!'); + } +} + +function fun2(type) { + let temp; + switch (type) { + case 1: + temp = new A({ username: 'aaa' }); + break; + case 2: + temp = new B({ username: 'bbb' }); + break; + case 3: + temp = new C({ username: 'ccc' }); + break; + default: + throw new Error('Unsupported types'); + break; + } + let user = temp.getUser(); + return user; +} + +let user = fun2(1); +console.log(user); +``` + +从这你也能看的出来,如果我要在添加一个需求的话,我就要分别给这三个类添加方法(当然这个是避免不了的),同时又要定义一个`fun3`来指定功能,没错,需求和写着代码的都是我。于是我决定重构一些这一部分的代码,为以后方便我后续的修改操作。 + +## 重构代码 + +首先 我们也能到两个部分都有 switch 分支,并且都夹杂着 break 语句,说实话,看的不是很入眼。同时定义了一个 temp 临时参数用于调用,不妨封装成一个函数,专门用来获取该类的**实例对象**。 + +```javascript +function getClass(type) { + switch (type) { + case 1: + return new A({ username: 'aaa' }); + case 2: + return new B({ username: 'bbb' }); + case 3: + return new C({ username: 'ccc' }); + default: + throw new Error('Unsupported types'); + } +} +``` + +```javascript +function fun2(type) { + let temp = getClass(type); + let user = temp.getUser(); + return user; +} +``` + +这样 就能把那个共有的`switch-case`代码封装成一个工厂函数用于获取。 + +## 利用多态来消除分支 + +但是上面这么写的话,其实是会一个很隐患的问题,比如 C 类忘记写`getUser`方法了,但是我却调用了`temp.getUser()`,那么 js 运行到该代码的时候就会报错`temp.getUser is not a function`,这是 js 特性,并不能通过文本编辑器找到该 bug。但如果是 TypeScript,上面的代码就会提示 + +``` +类型“A | B | C”上不存在属性“getUser”。 + 类型“C”上不存在属性“getUser”。ts(2339) +``` + +虽说 ES6 可以通过继承,实现对象的多态性,但 ES6 并没有抽象类的。加上我的项目是基于 TypeScript 的,所以用到的也是 TypeScript 的类(也强烈建议使用 TypeScript),故以下的代码也都是基于 TypeScript 所编写的。 + +```typescript +interface User { + username: string; +} + +abstract class S { + protected user: User; + constructor(user) { + this.user = user; + } + + abstract getUser(); +} + +class A extends S { + getUser(): User { + console.log('A'); + return this.user; + } +} + +class B extends S { + getUser(): User { + console.log('B'); + return this.user; + } +} + +class C extends S { + getUser(): User { + console.log('C'); + return this.user; + } +} + +function getClass(type): S { + switch (type) { + case 1: + return new A({ username: 'aaa' }); + case 2: + return new B({ username: 'bbb' }); + case 3: + return new C({ username: 'ccc' }); + default: + throw new Error('Unsupported types'); + } +} + +function fun3(type) { + let temp = getClass(type); + let user = temp.getUser(); + console.log(user); + return user; +} + +fun3(3); +``` + +这样,ABC 都是继承与 S 类,并且必须复写 getUser 方法,否则编辑器将会报错,编译出来的 js 代码如下(ES2020 标准,其实也就正常的 JS 继承) + +```javascript +var __extends = + (this && this.__extends) || + (function () { + var extendStatics = function (d, b) { + extendStatics = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function (d, b) { + d.__proto__ = b; + }) || + function (d, b) { + for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; + }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== 'function' && b !== null) throw new TypeError('Class extends value ' + String(b) + ' is not a constructor or null'); + extendStatics(d, b); + function __() { + this.constructor = d; + } + d.prototype = b === null ? Object.create(b) : ((__.prototype = b.prototype), new __()); + }; + })(); +var S = /** @class */ (function () { + function S(user) { + this.user = user; + } + return S; +})(); +var A = /** @class */ (function (_super) { + __extends(A, _super); + function A() { + return (_super !== null && _super.apply(this, arguments)) || this; + } + A.prototype.getUser = function () { + console.log('A'); + return this.user; + }; + return A; +})(S); +var B = /** @class */ (function (_super) { + __extends(B, _super); + function B() { + return (_super !== null && _super.apply(this, arguments)) || this; + } + B.prototype.getUser = function () { + console.log('B'); + return this.user; + }; + return B; +})(S); +var C = /** @class */ (function (_super) { + __extends(C, _super); + function C() { + return (_super !== null && _super.apply(this, arguments)) || this; + } + C.prototype.getUser = function () { + console.log('C'); + return this.user; + }; + return C; +})(S); +function getClass(type) { + switch (type) { + case 1: + return new A({ username: 'aaa' }); + case 2: + return new B({ username: 'bbb' }); + case 3: + return new C({ username: 'ccc' }); + default: + throw new Error('Unsupported types'); + } +} +function fun2(type) { + var temp = getClass(type); + var user = temp.getUser(); + console.log(user); + return user; +} +fun2(3); +``` diff --git "a/docs/skill/js&ts/\351\207\215\346\236\204\344\271\213\345\257\271\350\261\241\346\230\240\345\260\204\347\261\273\345\236\213.md" "b/docs/skill/js&ts/\351\207\215\346\236\204\344\271\213\345\257\271\350\261\241\346\230\240\345\260\204\347\261\273\345\236\213.md" new file mode 100644 index 0000000..9bf74e1 --- /dev/null +++ "b/docs/skill/js&ts/\351\207\215\346\236\204\344\271\213\345\257\271\350\261\241\346\230\240\345\260\204\347\261\273\345\236\213.md" @@ -0,0 +1,218 @@ +--- +id: type-of-object-map-refactor +slug: /type-of-object-map-refactor +title: 重构之对象映射类型 +date: 2021-12-07 +authors: kuizuo +tags: [javascript, refactor] +keywords: [javascript, refactor] +--- + +写代码的时候,遇到多重条件分支的时候,使用`if else if`肯定不如 switch 好用,但 switch 又避免不了 break 语句。但如果只是将数据转化的话,不妨使用对象映射的形式来替换 + + + +## 需求 + +**题型对应的数字转化 将单选 0 多选 1 填空 2 判断 3 简答 4 其他类型-1 转化为 单选 1 多选 2 判断 3 填空 4 简答 5 其他类型-1** + +目的:前者是其他来源的题目题型标记,而后者是存入数据库的题目标记。 + +## 代码 + +**if else if 语句** + +```javascript +let ti = { + title: '题目', + answer: '答案', + type: null, +}; + +let type = 0; +if (type === 0) { + ti.type = 1; +} else if (type === 1) { + ti.type = 2; +} else if (type === 2) { + ti.type = 3; +} else if (type === 3) { + ti.type = 4; +} else if (type === 4) { + ti.type = 5; +} else { + ti.type = -1; +} + +console.log(ti.type); // 1 +``` + +**swtich 语句** + +```javascript +let ti = { + title: '题目', + answer: '答案', + type: null, +}; + +let type = 0; +switch (type) { + case 0: + ti.type = 1; + break; + case 1: + ti.type = 2; + break; + case 2: + ti.type = 3; + break; + case 3: + ti.type = 4; + break; + case 4: + ti.type = 5; + break; + default: + ti.type = -1; + break; +} + +console.log(ti.type); // 1 +``` + +显而易见 上面的代码写的很臃肿,并且可读性很差,万一这时候有道题型对应数字的发生了改变,就很容易改错。 + +不妨定义一个对象,用于映射不同的题目题型,像下面这样 + +```javascript +let ti = { + title: '题目', + answer: '答案', + type: null, +}; + +let type = 0; + +const typeMap = { + 0: 1, // 单选 + 1: 2, // 多选 + 2: 3, // 判断 + 3: 4, // 填空 + 4: 5, // 简答 +}; + +ti.type = typeMap[type] ?? -1; // typeMap[type]为0的话 为假值 使用||将会赋值为-1 + +console.log(ti.type); // 1 +``` + +像这样的例子还有状态映射。 + +```javascript +let status = 1 +const statusMap = { + 0: '未使用' + 1: '已使用', +} +let statusStr = statusMap[status] +``` + +这样已经很好了,这里的 1 2 3 4 5 后都添加了注释,增加了一定的可读性,但是还不够,有时候在引用的话或记混了都有可能把填空题判断成简答题,比如后续使用的代码 + +```javascript +switch (ti.type) { + case 1: // 单选 + // ... + break; + case 2: // 多选 + // ... + break; + case 3: // 判断 + // ... + break; + case 4: // 填空 + // ... + break; + case 5: // 简答 + // ... + break; + default: + break; +} +``` + +又需要加一遍注释,如果不看`typeMap`的话,就很大的可能会写错。于是一个好的命名就至关重要了 + +## enum(枚举) + +如果使用的是 Typescript,那么可以直接使用 enum(枚举) + +```typescript +enum Qtype { + RADIO = 0, + MULT = 1, + BLANK = 2, + JUDGE = 3, + SHORT = 4, + UNKNOWN = -1, +} +``` + +此时的整个代码就可以写成这样 + +```javascript +let ti = { + title: '题目', + answer: '答案', + type: null, +}; + +let type = 0; +const typeMap = { + 0: Qtype.RADIO, // 单选 + 1: Qtype.CHECK, // 多选 + 2: Qtype.JUDGE, // 判断 + 3: Qtype.BLANK, // 填空 + 4: Qtype.SHORT, // 简答 +}; + +ti.type = typeMap[type] ?? Qtype.UNKNOWN; +console.log(ti.type); // 1 + +switch (ti.type) { + case Qtype.RADIO: + // ... + break; + case Qtype.CHECK: // 多选 + // ... + break; + case Qtype.JUDGE: // 判断 + // ... + break; + case Qtype.BLANK: // 填空 + // ... + break; + case Qtype.SHORT: // 简答 + // ... + break; + default: + break; +} +``` + +如果不新建一个typeMap变量,还可以直接这样操作 + +```javascript +ti.type = { + 0: Qtype.RADIO, // 单选 + 1: Qtype.CHECK, // 多选 + 2: Qtype.JUDGE, // 判断 + 3: Qtype.BLANK, // 填空 + 4: Qtype.SHORT, // 简答 +}[type] ?? Qtype.UNKNOWN; +``` + +## 后续重构 + +如果这时候需求发生了变化,比如判断题与填空题的数字要交换一下,那么也只需要修改`Qtype`,极大的提高了开发的效率与 bug 的发生。 diff --git "a/docs/skill/misc/\346\237\245\347\234\213\347\253\257\345\217\243\345\215\240\347\224\250\345\217\212\347\273\223\346\235\237\350\277\233\347\250\213.md" "b/docs/skill/misc/\346\237\245\347\234\213\347\253\257\345\217\243\345\215\240\347\224\250\345\217\212\347\273\223\346\235\237\350\277\233\347\250\213.md" new file mode 100644 index 0000000..a9608c6 --- /dev/null +++ "b/docs/skill/misc/\346\237\245\347\234\213\347\253\257\345\217\243\345\215\240\347\224\250\345\217\212\347\273\223\346\235\237\350\277\233\347\250\213.md" @@ -0,0 +1,109 @@ +--- +id: look-up-port-and-kill-process +slug: /look-up-port-and-kill-process +title: 查看端口占用及结束进程 +date: 2022-05-09 +authors: kuizuo +tags: [system] +keywords: [system] +--- + +## Linux + +### 查看端口占用情况 + +```bash +lsof -i:端口号 +``` + +#### 实例 + +```bash +[root@VM-4-5-centos]# lsof -i:5002 +COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME +node 15196 www 25u IPv6 63810147 0t0 TCP *:rfe (LISTEN) +``` + +更多 lsof 的命令如下: + +```bash +lsof -i:8080:查看8080端口占用 +lsof abc.txt:显示开启文件abc.txt的进程 +lsof -c abc:显示abc进程现在打开的文件 +lsof -c -p 1234:列出进程号为1234的进程所打开的文件 +lsof -g gid:显示归属gid的进程情况 +lsof +d /usr/local/:显示目录下被进程开启的文件 +lsof +D /usr/local/:同上,但是会搜索目录下的目录,时间较长 +lsof -d 4:显示使用fd为4的进程 +lsof -i -U:显示所有打开的端口和UNIX domain文件 +``` + +### netstat + +**netstat -tunlp** 用于显示 tcp,udp 的端口和进程等相关情况。 + +netstat 查看端口占用语法格式: + +``` +netstat -tunlp | grep 端口号 +``` + +- -t (tcp) 仅显示 tcp 相关选项 +- -u (udp)仅显示 udp 相关选项 +- -n 拒绝显示别名,能显示数字的全部转化为数字 +- -l 仅列出在 Listen(监听)的服务状态 +- -p 显示建立相关链接的程序名 + +### 结束进程 + +```bash +kill -9 PID +``` + +[Linux 查看端口占用情况 | 菜鸟教程 (runoob.com)](https://www.runoob.com/w3cnote/linux-check-port-usage.html) + +## Windows + +### 查看端口占用的 PID + +```bash +netstat -ano | findstr "端口号" +``` + +例 + +```bash + netstat -ano | findstr "8080" + TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 18180 + TCP 192.168.123.210:14075 115.236.121.240:8080 ESTABLISHED 14060 + TCP [::]:8080 [::]:0 LISTENING 18180 +``` + +### 查看指定 PID 的进程 + +如果想看占用进程,可以继续输入命令: + +```bash +tasklist|findstr "PID" +``` + +例 + +```bash +tasklist|findstr "18180" +java.exe 18180 Console 1 852,996 K +``` + +### 结束进程 + +```bash +taskkill /T /F /PID PID +``` + +例 + +```bash +taskkill /T /F /PID 8080 +``` + +强制(/F 参数)杀死 pid 为 8080 的所有进程包括子进程(/T 参数) diff --git "a/docs/skill/node/axios\350\257\267\346\261\202gbk\351\241\265\351\235\242\344\271\261\347\240\201\350\247\243\345\206\263.md" "b/docs/skill/node/axios\350\257\267\346\261\202gbk\351\241\265\351\235\242\344\271\261\347\240\201\350\247\243\345\206\263.md" new file mode 100644 index 0000000..e4a02b7 --- /dev/null +++ "b/docs/skill/node/axios\350\257\267\346\261\202gbk\351\241\265\351\235\242\344\271\261\347\240\201\350\247\243\345\206\263.md" @@ -0,0 +1,43 @@ +--- +id: axios-request-gbk-page-encoding-solution +slug: /axios-request-gbk-page-encoding-solution +title: axios请求gbk页面乱码解决 +date: 2021-09-19 +authors: kuizuo +tags: [node, axios, encode] +keywords: [node, axios, encode] +--- + + + +使用 axios 请求 gbk 编码的网站,将会出现乱码,原因很简单,node 默认字符编码为 utf8,如果要正常显示 gbk 数据的话就需要将 gbk 转 utf8 格式。 + +## 解决办法 + +借助`iconv-lite`,不让 axios 自动处理响应数据,添加`responseType`和`transformResponse`参数,演示代码如下 + +```js +import axios from 'axios' +import * as iconv from 'iconv-lite' + +axios + .get(`https://www.ip138.com/`, { + responseType: 'arraybuffer', + transformResponse: [ + function (data) { + return iconv.decode(data, 'gbk') + }, + ], + }) + .then((res) => { + console.log(res.data) + }) +``` + +或者不使用`transformResponse`,在响应结束后使用`iconv.decode(res.data, 'gbk')`,使用`transformResponse`相对优雅一点。 + +如果返回的是 json 格式的话,可以直接`JSON.parse`转为 json 对象(前提得确保是 json 格式,不然解析报错) + +```js +return JSON.parse(iconv.decode(data, 'gbk')) +``` diff --git "a/docs/skill/node/npkill\345\277\253\351\200\237\345\210\240\351\231\244node_modules\346\226\207\344\273\266.md" "b/docs/skill/node/npkill\345\277\253\351\200\237\345\210\240\351\231\244node_modules\346\226\207\344\273\266.md" new file mode 100644 index 0000000..3a26bd8 --- /dev/null +++ "b/docs/skill/node/npkill\345\277\253\351\200\237\345\210\240\351\231\244node_modules\346\226\207\344\273\266.md" @@ -0,0 +1,35 @@ +--- +id: npkill-quickly-move-node-modules +slug: /npkill-quickly-move-node-modules +title: npkill快速删除node_modules文件 +date: 2022-03-17 +authors: kuizuo +tags: [node, npm] +keywords: [node, npm] +--- + +官网地址 [npkill](https://npkill.js.org/) + +### 安装 + +``` +npm i -g npkill +``` + +无需安装也可通过`npx npkill`来运行 + +### 使用 + +在指定目录下,输入`npkill`,如下结果 + +![image-20220610112331413](https://img.kuizuo.cn/image-20220610112331413.png) + +提示该目录共存在一个 node_modules,占用空间为 584.50 MB,如果有多个 node_modules,可以通过上下键来选择。按下 SPACE(空格键)后,便会删除该 node_modules。删除结果如下 + +![image-20220610112447316](https://img.kuizuo.cn/image-20220610112447316.png) + +该库最强大的一点是可以直接遍历你指定目录下所有的 node_modules,其效果如下。 + +![image-20220610113241441](https://img.kuizuo.cn/image-20220610113241441.png) + +更多详细配置见官方文档。 diff --git "a/docs/skill/node/npm\351\225\234\345\203\217\351\205\215\347\275\256.md" "b/docs/skill/node/npm\351\225\234\345\203\217\351\205\215\347\275\256.md" new file mode 100644 index 0000000..af5f620 --- /dev/null +++ "b/docs/skill/node/npm\351\225\234\345\203\217\351\205\215\347\275\256.md" @@ -0,0 +1,81 @@ +--- +id: npm-mirror-config +slug: /npm-mirror-config +title: npm镜像配置 +date: 2022-03-17 +authors: kuizuo +tags: [node, npm, electron] +keywords: [node, npm, electron] +--- + + + +由于原淘宝 npm 域名(**http://npm.taobao.org 和 http://registry.npm.taobao.org**)将于 **2022.06.30 号正式下线和停止 DNS 解析**,不妨提前修改镜像的地址,以免受到影响。 + +域名切换规则: + +- http://npm.taobao.org => http://npmmirror.com +- http://registry.npm.taobao.org => http://registry.npmmirror.com + +同时不推荐使用镜像下载依赖,因为有可能会导致与官方包不同步(亲测,就因为下载依赖折腾了一晚上,还以为是电脑问题),但有时候开启科学上网(或者没有),下载也不见得特别快,所以这时候才会使用国内镜像。 + +## 镜像站点 + +[npmmirror 中国镜像站](https://www.npmmirror.com/) + +http://registry.npmjs.org + +## 单次使用镜像 + +```bash +npm install [name] --registry=https://registry.npmmirror.com +``` + +## 永久配置镜像 + +```bash +npm config set registry https://registry.npmmirror.com +``` + +## 查看镜像 + +``` +npm get registry +``` + +## nrm镜像管理工具 + +``` +npm install nrm -g +``` + +### nrm ls 查看所有镜像 + +``` + npm ---------- https://registry.npmjs.org/ + yarn --------- https://registry.yarnpkg.com/ + tencent ------ https://mirrors.cloud.tencent.com/npm/ + cnpm --------- https://r.cnpmjs.org/ + taobao ------- https://registry.npmmirror.com/ + npmMirror ---- https://skimdb.npmjs.com/registry/ +``` + +### nrm use 镜像 切换镜像 + +``` +nrm use taobao +``` + +## 清除 npm 缓存 + +```bash +npm cache clean --force +``` + +## 配置 electron 镜像 + +```bash +npm config set ELECTRON_MIRROR https://npmmirror.com/mirrors/electron/ + +npm config set ELECTRON_BUILDER_BINARIES_MIRROR https://npmmirror.com/mirrors/electron-builder-binaries/ +``` diff --git "a/docs/skill/node/\344\275\277\347\224\250 require.context \345\256\236\347\216\260\346\250\241\345\235\227\350\207\252\345\212\250\345\257\274\345\205\245.md" "b/docs/skill/node/\344\275\277\347\224\250 require.context \345\256\236\347\216\260\346\250\241\345\235\227\350\207\252\345\212\250\345\257\274\345\205\245.md" new file mode 100644 index 0000000..08ca755 --- /dev/null +++ "b/docs/skill/node/\344\275\277\347\224\250 require.context \345\256\236\347\216\260\346\250\241\345\235\227\350\207\252\345\212\250\345\257\274\345\205\245.md" @@ -0,0 +1,91 @@ +--- +id: use-require.context-to-auto-import-modules +slug: /use-require.context-to-auto-import-modules +title: 使用 require.context 实现模块自动导入 +date: 2021-09-12 +authors: kuizuo +tags: [node, webpack] +keywords: [node, webpack] +--- + + + +## 前言 + +在写资源导航的时候,我在将资源分类为一个文件的时候,发现如果我每定义一个分类,那我就需要创建一个文件,然后又要通过`import form`导入,就很烦躁。 + +![image-20210912080353288](https://img.kuizuo.cn/image-20210912080353288.png) + +突然想到貌似 vue-element-admin 中的路由好像也是这样的,而 store 貌似定义完就无需再次导入,于是就开始研究代码,果不其然,发现了`require.context` + +![image-20210912080429237](https://img.kuizuo.cn/image-20210912080429237.png) + +[依赖管理 | webpack 中文文档 (docschina.org)](https://webpack.docschina.org/guides/dependency-management/) + +## 实现 + +require.context:是一个 webpack 提供的 api,通过执行 require.context 函数遍历获取到指定文件夹(及其下子文件夹)内的指定文件,然后自动导入。 + +语法:`require.context(directory, useSubdirectories = false, regExp = /^.//)` + +- directory 指定文件 +- useSubdirectories 是否遍历目录的子目录 +- regExp 匹配文件的正则表达式,即文件类型 + +而上图代码中对应的代码也明确表达要指定`./modules`目录下的,所有 js 文件 + +```js +const modulesFiles = require.context('./modules', true, /\.js$/) +``` + +输出一下看看 modulesFiles 到底是什么(console.dir 输出) + +![image-20210912081146031](https://img.kuizuo.cn/image-20210912081146031.png) + +返回一个函数,但该函数包含三个属性 resolve()、keys()、id + +其中`modulesFiles.keys()`则是指定目录下文件名数组 + +``` + ['./app.js', './permission.js','./settings.js', './tagsView.js', './user.js'] +``` + +接着看下 vue-element-admin 中的下一行代码 + +```js +const modules = modulesFiles.keys().reduce((modules, modulePath) => { + // set './app.js' => 'app' + const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1') + const value = modulesFiles(modulePath) + modules[moduleName] = value.default + return modules +}, {}) +``` + +这边先输出一下 modules,看下结果是什么 + +![image-20210912081553729](https://img.kuizuo.cn/image-20210912081553729.png) + +没错,正对应着 modules 下的所有文件,以及所导出的对象 + +其中在循环体中还调用了`const value = modulesFiles(modulePath)`,其中 value 是 Module 对象,有个属性`default`,通过`value.default`便可获取到对应模块所导出的内容。 + +就此便可以实现自动导入模块。不过由于导出的是 store 对象,所封装的代码也有点过于复杂,这边我贴下我是如何自动导入数组对象的 + +```typescript +const modulesFiles = require.context('./modules', true, /\.ts$/) + +let allData: any[] = [] + +modulesFiles.keys().forEach((modulePath) => { + const value = modulesFiles(modulePath) + let data = value.default + + if (!data) return + allData.push(...value.default) +}) +``` + +## 参考链接 + +> [前端优化之 -- 使用 require.context 让项目实现路由自动导入 - 沐浴点阳光 - 博客园 (cnblogs.com)](https://www.cnblogs.com/garfieldzhong/p/12585280.html) diff --git "a/docs/skill/programming-languages/go/Gin\346\241\206\346\236\266\345\210\235\344\275\223\351\252\214.md" "b/docs/skill/programming-languages/go/Gin\346\241\206\346\236\266\345\210\235\344\275\223\351\252\214.md" new file mode 100644 index 0000000..5f6daf9 --- /dev/null +++ "b/docs/skill/programming-languages/go/Gin\346\241\206\346\236\266\345\210\235\344\275\223\351\252\214.md" @@ -0,0 +1,489 @@ +--- +id: try-gin-framework +slug: /try-gin-framework +title: Gin框架初体验 +date: 2021-09-01 +authors: kuizuo +tags: [go, gin] +keywords: [go, gin] +--- + + + +## 安装 Gin + +[文档 | Gin Web Framework (gin-gonic.com)](https://gin-gonic.com/zh-cn/docs/) + +打开命令行窗口,输入 + +```bash +go get -u github.com/gin-gonic/gin +``` + +大概率可能安装不上,一般这里就需要配置 Go 代理 + +## 使用 + +创建文件夹 GinTest,进入目录输入命令`go mod init GinTest`来管理项目的包 + +创建文件 main.go 内容为 + +```go title="main.go" +package main + +import "github.com/gin-gonic/gin" + +func main() { + r := gin.Default() + + r.GET("/", func(c *gin.Context) { + c.String(200, "你好,gin") + }) + + r.Run() +} +``` + +通过`go run "f:\GO\GinTest\main.go"`即可运行 go 服务。 + +![image-20210831045351327](https://img.kuizuo.cn/image-20210831045351327.png) + +通过浏览器访问`http:127.0.0.1:8080`便可输出`你好,gin` + +### 热加载 + +由于每次更改代码后都需要重新启动,通过热加载可以省去每次手动编译的过程 + +### Fresh + +这边使用的是 fresh,还有其他的热加载工具,例如 Air,bee,gin 等等 + +```bash +go get github.com/pilu/fresh +``` + +接着输入 fresh 即可 + +![image-20210831061629685](https://img.kuizuo.cn/image-20210831061629685.png) + +同时还会在当前目录下创建 tmp 文件夹,有个编译好的可执行文件。 + +### 返回数据格式 + +上面代码所演示的`c.String()` 返回的是文本格式,但有时候要返回的可能是一个 JSON 类型,或者是一个 HTML 或 XML 格式。这时候的话就需要使用其他方法了 + +### JSON + +```go title="main.go" +r.GET("/json", func(c *gin.Context) { + c.JSON(200, map[string]interface{}{ + "code": 200, + "msg": "成功", + }) +}) +``` + +浏览器访问http://127.0.0.1:8080/json显示如下数据 + +```json +{ "code": 200, "msg": "成功" } +``` + +> 注: msg 属性后,必须要有,号 + +其中`map[string]interface{}`可以简写为`gin.H` + +也可通过定义结构体 + +```go title="main.go" +type Article struct { + Title string `json:"title"` + Desc string `json:"desc"` + Content string `json:"content"` +} + +r.GET("/json3", func(c *gin.Context) { + a := &Article{ + Title: "这是标题", + Desc: "描述", + Content: "测试内容", + } + + c.JSON(200, a) +}) +``` + +得到数据 + +```json +{ "title": "这是标题", "desc": "描述", "content": "测试内容" } +``` + +JSONP 与 XML 数据就不做介绍,顺便提一下,这年头还有人用 JSONP 来跨域吗? + +### HTML + +要发送 HTML 的话,首先在根目录下创建文件夹`templates`,再创建一个文件`test.html`,其中``内容为 + +```html title="/templates/test.html" + +

{{.title}}

+ +``` + +接着在 main.go 中配置 Html 模板文件,如下 + +```go title="main.go" +r := gin.Default() +r.LoadHTMLFiles("templates/*") +``` + +重启下服务,然后就可以在路由中返回 HTML 文件,如 + +```go title="main.go" +r.GET("/html", func(c *gin.Context) { + + c.HTML(200, "test.html", gin.H{ + "title": "一个标题而已", + }) +}) +``` + +结果就不放图了,就是将`一个标题而已`填入至 h2 标签处 + +### 配置静态 Web 目录 + +和配置 html 模板一样,先在根目录下创建一个静态 web 目录 static,然后添加 + +```go title="main.go" +r.Static("/static", "./static") +``` + +访问 `http://127.0.0.1:8080/static` 就能访问静态文件夹下的资源 + +### 获取 Query 参数 + +```go title="main.go" +r.GET("/query", func(c *gin.Context) { + username := c.Query("username") + page := c.DefaultQuery("page", "1") + + c.String(200, username+page) +}) +``` + +浏览器请求 `http://127.0.0.1:8080/query?username=kuizuo` 便可输出 `kuizuo1` + +### 获取 Post 数据 + +```go title="main.go" +r.POST("/add", func(c *gin.Context) { + username := c.PostForm("username") + password := c.PostForm("password") + + c.String(200, username+password) +}) +``` + +使用 api 请求工具发送 post 数据便可输出相应数据 + +### Post 传值绑定到结构体 + +```go title="main.go" +type UserInfo struct { + Username string `json:"username" form:"username"` + Password string `json:"password" form:"password"` +} + +r.POST("/add1", func(c *gin.Context) { + user := &UserInfo{} + + if err := c.ShouldBind(&user); err == nil { + c.JSON(200, user) + } else { + c.JSON(400,gin.H{ + "err":err.Error() + }) + } +}) +``` + +同样使用 api 请求工具,发送 post 数据,就可直接通过 user 获取信息 + +### 动态路由传值 + +```go title="main.go" +r.GET("/list/:id", func(c *gin.Context) { + id := c.Param("id") + c.String(200, id) +}) +``` + +浏览器请求http://127.0.0.1:8080/list/123 id 便可赋值为 123 + +### 路由分组 + +在根目录下创建文件夹`routers`,里面创建路由文件,如`apiRouters.go`,内容如下 + +```go title="/routers/apiRouters.go" +package routers + +import "github.com/gin-gonic/gin" + +func ApiRoutersInit(r *gin.Engine) { + apiRouters := r.Group("/api") + { + apiRouters.GET("/json", func(c *gin.Context) { + c.JSON(200, gin.H{ + "code": 200, + "msg": "成功", + }) + }) + } +} +``` + +接着在`main.go`文件中,导入 routers + +```go title="main.go" +import ( + "GinTest/routers" + "github.com/gin-gonic/gin" +) +``` + +同时输入 + +```go title="main.go" +r := gin.Default() + +routers.ApiRoutersInit(r) +``` + +访问 [http://127.0.0.1:8080/api/json](http://127.0.0.1:8080/api/json),显示`{"code":200,"msg":"成功"}` + +### 控制器 + +在根目录下创建文件夹`controllers`,里面创建控制器文件,如`userController.go`,内容如下 + +```go title="/controllers/user/userController.go" +package user + +import "github.com/gin-gonic/gin" + +type UserController + +func UserList(c *gin.Context) { + c.String(200, "用户列表") +} + +func UserAdd(c *gin.Context) { + c.String(200, "添加用户") +} + +func UserEdit(c *gin.Context) { + c.String(200, "编辑用户") +} +``` + +```go title="/routers/userRouters.go" +package routers + +import ( + "GinTest/controllers/user" + "github.com/gin-gonic/gin" +) + +func UserRoutersInit(r *gin.Engine) { + userRouters := r.Group("/user") + { + userRouters.GET("/list", user.UserList) + userRouters.GET("/add", user.UserAdd) + userRouters.GET("/edit", user.UserEdit) + } +} +``` + +分别访问对应三个路由,都可得到对应结果 + +也可以通过控制器结构体优化成如下 + +```go title="/controllers/user/userController.go" +package user + +import "github.com/gin-gonic/gin" + +type UserController struct { +} + +func (con UserController) List(c *gin.Context) { + c.String(200, "用户列表") +} + +func (con UserController) Add(c *gin.Context) { + c.String(200, "添加用户") +} + +func (con UserController) Edit(c *gin.Context) { + c.String(200, "编辑用户") +} +``` + +```go title="/routers/userRouters.go" +package routers + +import ( + "GinTest/controllers/user" + + "github.com/gin-gonic/gin" +) + +func UserRoutersInit(r *gin.Engine) { + userRouters := r.Group("/user") + { + userRouters.GET("/list", user.UserController{}.List) + userRouters.GET("/add", user.UserController{}.Add) + userRouters.GET("/edit", user.UserController{}.Edit) + } +} +``` + +### 中间件 + +中间件本质上就是一个函数,路由执行的时候可以在对应的地方添加中间件执行,如 + +#### 局部中间件 + +```go title="main.go" +func initMiddleware(c *gin.Context) { + fmt.Println("1-中间件") + + c.Next() + + fmt.Println("2-中间件") +} + +r.GET("/", initMiddleware, func(c *gin.Context) { + c.String(200, "你好,gin") +}) +``` + +访问[http://127.0.0.1:8080](http://127.0.0.1:8080) 便会输出 `1-中间件` `2-中间件` + +#### 全局中间件 + +```go title="main.go" +r.Use(initMiddleware) +``` + +这样就需要给每个路由添加中间件配置,所有路由请求后都将会输出。 + +#### 分组中间件 + +与全局中间件使用一样,如 + +```go title="/routers/apiRouters.go" +apiRouters := r.Group("/api",initMiddleware) + +// 或 +apiRouters := r.Group("/api") +apiRouters.Use(initMiddleware) +``` + +可以创建中间件目录`middlewares`,创建文件`init.go`,内容 + +```go title="/middleware/init.go" +package middlewares + +import ( + "fmt" + "github.com/gin-gonic/gin" +) + +func InitMiddleware(c *gin.Context) { + fmt.Println("1-中间件") + + c.Next() + + fmt.Println("2-中间件") +} +``` + +使用如下(前提需要导入中间件的包) + +```go title="/routers/apiRouters.go" +apiRouters.Use(middlewares.InitMiddleware) +``` + +#### 取消默认中间件 + +gin.Default()默认使用了 Logger 和 Recovery 中间件 + +```go +// Default returns an Engine instance with the Logger and Recovery middleware already attached. +func Default() *Engine { + debugPrintWARNINGDefault() + engine := New() + engine.Use(Logger(), Recovery()) + return engine +} +``` + +如果需要上面两个默认的中间件,可以使用 gin.New()新建一个没有任何中间件的路由 + +#### 中间件中使用 goroutine 协程 + +```go title="/middleware/init.go" +func InitMiddleware(c *gin.Context) { + fmt.Println("1-中间件") + + cCp := c.Copy() + go func() { + time.Sleep(2 * time.Second) + fmt.Println("path: " + cCp.Request.URL.Path) + }() + + c.Next() + + fmt.Println("2-中间件") +} +``` + +请求完成两秒后,将会打印`path /` + +### 文件上传 + +```go title="main.go" +r.MaxMultipartMemory = 8 << 20 // 8 MiB +r.POST("/upload", func(c *gin.Context) { + // 单文件 + file, _ := c.FormFile("file") + log.Println(file.Filename) + + // 上传文件至指定目录 + dst := path.Join("./static/upload", file.Filename) + c.SaveUploadedFile(file, dst) + + c.String(200, fmt.Sprintf("'%s' uploaded!", file.Filename)) +}) +``` + +使用 curl,即可上传文件 + +```bash +curl -X POST http://localhost:8080/upload \ + -F "file=@/Users/appleboy/test.zip" \ + -H "Content-Type: multipart/form-data" +``` + +## 最终项目结构 + +![image-20210901033059576](https://img.kuizuo.cn/image-20210901033059576.png) + +## 整体感受 + +说实话,我已经快一年没真正接触一门新的语言了,写 Js 和 Ts 代码也写了快一年了,初次体验 Gin 框架整体感受还算不错,大部分的后端框架路由基本都是这么写的,体验过 Express,Flask 路由写法大致相同。 + +仅仅只是初步体验,后续估计会考虑尝试上手 gin-vue-admin 项目 + +[自动化全栈后台管理系统 | Gin-Vue-Admin](https://www.gin-vue-admin.com/) diff --git "a/docs/skill/programming-languages/go/Go\345\217\221\351\200\201http\350\257\267\346\261\202.md" "b/docs/skill/programming-languages/go/Go\345\217\221\351\200\201http\350\257\267\346\261\202.md" new file mode 100644 index 0000000..f4f1a84 --- /dev/null +++ "b/docs/skill/programming-languages/go/Go\345\217\221\351\200\201http\350\257\267\346\261\202.md" @@ -0,0 +1,216 @@ +--- +id: go-send-http-request +slug: /go-send-http-request +title: Go发送http请求 +date: 2022-05-22 +authors: kuizuo +tags: [go, http] +keywords: [go, http] +--- + + + +## Get 请求 + +```go +import ( + "fmt" + "io/ioutil" + "net/http" +) + +func main() { + resp, err := http.Get("http://127.0.0.1:5000/api/test") + + if err != nil { + panic(err) + + } + defer resp.Body.Close() + + s, _ := ioutil.ReadAll(resp.Body) + + fmt.Println(resp.StatusCode) + fmt.Println(string(s)) +} +``` + +可以发现上面的例子中还需要对**响应体**进行读取,如果每条请求都需要如此操作的话,代码逻辑将会十分臃肿,一般都需要自行封装。而事实上大部分的编程语言的 http 请求库(包)都不会过度封装,一般都需要用户自行封装或使用第三方请求库。 + +当然这里肯定毫不犹豫的选择第三方库,后文会推荐几个,以及一些使用代码,这里还需要使用原生 http 库发送 Post 请求 + +## Post 请求 + +### 发送 querystring + +```go +import ( + "fmt" + "io/ioutil" + "net/http" + "strings" +) + +func main() { + payload := strings.NewReader("foo=1&bar=2") + + resp, err := http.Post("http://127.0.0.1:5000/api/test", "application/x-www-form-urlencoded", payload) + + if err != nil { + panic(err) + } + defer resp.Body.Close() + + s, _ := ioutil.ReadAll(resp.Body) + + fmt.Println(resp.StatusCode) + fmt.Println(string(s)) +} + +``` + +此外还可以使用 http.PostForm(省略读取响应代码) + +```go +import ( + "net/http" + "net/url" +) + +func main() { + payload := url.Values{"foo": {"1"}, "bar": {"2"}} + + resp, err := http.PostForm("http://127.0.0.1:5000/api/test", payload) +} + +``` + +### 发送 json + +```go +import ( + "fmt" + "net/http" + "strings" +) + +func main() { + payload := strings.NewReader(`{"name":"kuizuo"}`) + + req, _ := http.NewRequest("POST", "http://127.0.0.1:5000/api/test", payload) + + req.Header.Add("Content-Type", "application/json") + + res, _ := http.DefaultClient.Do(req) + fmt.Println(res) +} + +``` + +至于其他方法就不做演示,不对其封装将十分难用,日常开发主要还是使用第三方 http 请求库。 + +## HTTP 请求库 + +[valyala/fasthttp: Fast HTTP package for Go. Tuned for high performance. Zero memory allocations in hot paths. Up to 10x faster than net/http (github.com)](https://github.com/valyala/fasthttp) + +[go-resty/resty: Simple HTTP and REST client library for Go (github.com)](https://github.com/go-resty/resty) + +[imroc/req: Simple Go HTTP client with Black Magic (github.com)](https://github.com/imroc/req) + +[levigross/grequests: A Go "clone" of the great and famous Requests library (github.com)](https://github.com/levigross/grequests) + +整合了几个 Github 上所开源的 http 请求库,更多 http 库可在[http-client · GitHub Topics](https://github.com/topics/http-client?l=go)上查看 ,这里对其进行简单优点介绍,以及个人的选择。 + +- [fasthttp](https://github.com/valyala/fasthttp) 号称比 net/http 快 10 倍的 http 包,并且 star 数最多的。 +- [resty](https://github.com/go-resty/resty#usage) 一个链式调用的请求库。 +- [Req](https://req.cool/) 与 resty 使用相似,并且提供非常友好的使用文档(有中文)。 +- [grequests](https://github.com/levigross/grequests) go 语言版的 python requests。 + +如果使用过 python requests 库,那么可以毫不犹豫的选择 grequests。很显然,我使用过 requests,所以也就毫不犹豫的选择 grequests。 + +## grequests + +这里写一个模拟登录的例子 + +```go + +import ( + "fmt" + + "github.com/levigross/grequests" +) + +type Demo struct { + Session *grequests.Session + User User +} + +type User struct { + Username string + Password string +} + +type Result struct { + Code int `json:"code"` + Msg string `json:"msg"` +} + +func (dm *Demo) login() string { + + resp, err := dm.Session.Post("http://127.0.0.1:5000/api/login", + &grequests.RequestOptions{ + Data: map[string]string{ + "username": dm.User.Username, + "password": dm.User.Password, + }, + Headers: map[string]string{ + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36", + }, + }) + + var result Result + resp.JSON(&result) + + if err == nil && result.Code == 200 { + return "登录成功" + } + + return result.Msg +} + +func main() { + var session = grequests.NewSession(nil) + dm := Demo{ + Session: session, + User: User{ + Username: "kuizuo", + Password: "a123456", + }, + } + loginResult := dm.login() + fmt.Println(loginResult) + // TODO: +} + +``` + +因为 go 中没有类的概念,所以想要实现“类”,就得在 `func` 和方法名之间添加方法所属的类型声明(有的地方将其称之为接收者声明) + +也就是`func (dm *Demo) login() string {` 中的`(dm *Demo)` 其中这里的 Demo 根据实际需求进行更换,并且前面的 dm 无法更名为 this 或 self。 + +如果想发送 json 请求的话,grequests 写法也挺简单的,只需要将 Data 替换为 JSON(协议头会自动添加 Content-Type: application/json),如下 + +```go + resp, err := dm.Session.Post("http://127.0.0.1:5000/api/login", + &grequests.RequestOptions{ + JSON: map[string]string{ + "username": dm.User.Username, + "password": dm.User.Password, + }, + }) +``` + +## 总结 + +相比 js 和 python 来写 http 请求,由于 go 中没有类的概念(即无 class 关键字),所以只能利用**自定义结构体**来实现这样功能,并且在代码写法上也不算优雅。综合考虑的情况下,还是优选 js 和 python 来复现 http 协议。 diff --git "a/docs/skill/programming-languages/go/Go\345\271\266\345\217\221.md" "b/docs/skill/programming-languages/go/Go\345\271\266\345\217\221.md" new file mode 100644 index 0000000..2fe425c --- /dev/null +++ "b/docs/skill/programming-languages/go/Go\345\271\266\345\217\221.md" @@ -0,0 +1,257 @@ +--- +id: go-concurrent +slug: /go-concurrent +title: Go并发 +date: 2022-05-22 +authors: kuizuo +tags: [go] +keywords: [go] +--- + +Go 语言的并发是基于 `goroutine` 的,`goroutine` 类似于线程,但并非线程。可以将 `goroutine` 理解为一种虚拟线程。Go 语言运行时会参与调度 `goroutine`,并将 `goroutine` 合理地分配到每个 CPU 中,最大限度地使用 CPU 性能。开启一个 goroutine 的消耗非常小(大约 2KB 的内存),你可以轻松创建数百万个`goroutine`。 + + + +## goroutine + +goroutine 语法格式: + +```text +go 函数名( 参数列表 ) +``` + +演示代码如下 + +```go +import ( + "fmt" + "time" +) + +func say(s string) { + for i := 0; i < 3; i++ { + time.Sleep(100 * time.Millisecond) + fmt.Println(s) + } +} + +func main() { + go say("world") + say("hello") + fmt.Println("over!") +} +``` + +执行上面代码将会输出 + +```go +hello +world +world +hello +hello +over! +``` + +其中 hello 与 world 每次执行顺序都不一致,甚至有时候 world 会少输出一遍,或是 world 将会在 over! 后输出。因为此时的`go say("world")` 不在是主线程中执行,而是创建一个 goroutine 去执行。可以认为`go say("world")`就相当于 js 中的`await say("world")` 但 js 是单线程基于事件循环机制来实现的,所以两者还是有着一定的区别。 + +## 等待 goroutine 执行完成 + +可以发现 say("hello") 实际上也是在等待执行,如果将 say("hello")注释掉,再次执行,将只会输出 over!。原因很简单,因为主线程已经结束了,程序自然就结束了,goroutine 的执行也就不是程序的重点。 + +所以有时候需要等待 goroutine 执行完成,最直接的方法就是通过 time.Sleep 函数,或执行时间较长的函数来等待,但实际执行中并不知道应该等待多长时间,很显然这种方式并不是特别好。 + +## sync 包 + +Golang 官方在 sync 包中提供了 WaitGroup 类型来解决这个问题,下面是其简单的演示例子。 + +```go +import ( + "fmt" + "sync" + "time" +) + +func say(s string, wg *sync.WaitGroup) { + defer wg.Done() + + for i := 0; i < 3; i++ { + time.Sleep(100 * time.Millisecond) + fmt.Println(s) + } +} + +func main() { + var wg sync.WaitGroup + wg.Add(2) + say("hello", &wg) + say("world", &wg) + + wg.Wait() + fmt.Println("over!") +} +``` + +将会像同步输出一样,输出结果 + +```go +hello +hello +hello +world +world +world +over! +``` + +使用方法可以总结为下面几点: + +1. 创建一个 WaitGroup 实例,比如名称为:wg +2. 调用 wg.Add(n),其中 n 是等待的 goroutine 的数量 +3. 在每个 goroutine 运行的函数中执行 defer wg.Done() +4. 调用 wg.Wait() 阻塞主逻辑 + +## 通道(channel) + +如果说 goroutine 是 Go 语言程序的并发体的话,那么 channels 则是它们之间的通信机制。一个 channel 是一个通信机制,它可以让一个 goroutine 通过它给另一个 goroutine 发送值信息。每个 channel 都有一个特殊的类型,也就是 channels 可发送数据的类型。一个可以发送 int 类型数据的 channel 一般写为 chan int。 + +使用内置的 make 函数,我们可以创建一个 channel: + +```go +ch := make(chan 元素类型, [缓冲大小]) +``` + +先展示一个简单的代码例子 + +```go +import "fmt" + +func sum(s []int, c chan int) { + sum := 0 + for _, v := range s { + sum += v + } + c <- sum // 把 sum 发送到通道 c +} + +func main() { + s := []int{1, 2, 3, 4, 5} + + c := make(chan int) + go sum(s[:len(s)/2], c) + go sum(s[len(s)/2:], c) + x, y := <-c, <-c // 从通道 c 中接收 + + close(c) // 关闭通道 + + fmt.Println(x, y, x+y) +} +``` + +将会输出 12 3 15 + +关闭通道不是必须的,但关闭后的通道有以下特点: + +1.对一个关闭的通道再发送值就会导致 panic。 + +2.对一个关闭的通道进行接收会一直获取值直到通道为空。 + +3.对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。 + +4.关闭一个已经关闭的通道会导致 panic。 + +## 实例 + +一个深度遍历的代码例子,具体可看 [示例: 并发的 Web 爬虫 · Go 语言圣经 (studygolang.com)](https://books.studygolang.com/gopl-zh/ch8/ch8-06.html) + +```go +import ( + "fmt" + "log" + "net/http" + + "golang.org/x/net/html" +) + +func crawl(url string) []string { + fmt.Println(url) + list, err := Extract(url) + if err != nil { + log.Print(err) + } + return list +} + +func Extract(url string) ([]string, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, fmt.Errorf("getting %s: %s", url, resp.Status) + } + + doc, err := html.Parse(resp.Body) + resp.Body.Close() + if err != nil { + return nil, fmt.Errorf("parsing %s as HTML: %v", url, err) + } + + var links []string + visitNode := func(n *html.Node) { + if n.Type == html.ElementNode && n.Data == "a" { + for _, a := range n.Attr { + if a.Key != "href" { + continue + } + link, err := resp.Request.URL.Parse(a.Val) + if err != nil { + continue // ignore bad URLs + } + links = append(links, link.String()) + } + } + } + forEachNode(doc, visitNode, nil) + return links, nil +} + +func forEachNode(n *html.Node, pre, post func(n *html.Node)) { + if pre != nil { + pre(n) + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + forEachNode(c, pre, post) + } + if post != nil { + post(n) + } +} + +func main() { + worklist := make(chan []string) + + go func() { worklist <- []string{"http://gopl.io/"} }() + + // Crawl the web concurrently. + seen := make(map[string]bool) + for list := range worklist { + for _, link := range list { + if !seen[link] { + seen[link] = true + go func(link string) { + worklist <- crawl(link) + }(link) + } + } + } +} + +``` + +## 参考文章 + +[Goroutines 和 Channels · Go 语言圣经 (studygolang.com)](https://books.studygolang.com/gopl-zh/ch8/ch8.html) + +[https://www.cnblogs.com/sparkdev/p/10917536.html](https://www.cnblogs.com/sparkdev/p/10917536.html) diff --git "a/docs/skill/programming-languages/go/Go\347\216\257\345\242\203\345\256\211\350\243\205.md" "b/docs/skill/programming-languages/go/Go\347\216\257\345\242\203\345\256\211\350\243\205.md" new file mode 100644 index 0000000..facbfca --- /dev/null +++ "b/docs/skill/programming-languages/go/Go\347\216\257\345\242\203\345\256\211\350\243\205.md" @@ -0,0 +1,97 @@ +--- +id: go-environment-install +slug: /go-environment-install +title: Go环境安装 +date: 2021-09-01 +authors: kuizuo +tags: [go] +keywords: [go] +--- + + + +## 安装 Go + +[golang.org](https://golang.org/) + +[go 下载地址](https://studygolang.com/dl) + +[GoLand](https://www.jetbrains.com/go/download/download-thanks.html) + +下载安装包,选择路径,默认下一步即可 + +## 配置环境变量 + +**GOROOT 即为 GO 的安装目录。**设置为 `E:\Go` + +**GOPATH 即为存储 Go 工具依赖的路径**,可以自己进行设值,我放在了 GoWorks 自己建的,里面需要包含 src、pkg、bin 三个目录。 设置为 `E:\GoWork` + +## 配置 Go 代理 + +[GOPROXY.IO - 一个全球代理 为 Go 模块而生](https://goproxy.io/zh/) + +windows + +```bash +# 设置goproxy.io代理 +go env -w GOPROXY="https://proxy.golang.com.cn,direct" +# 设置GO111MOUDLE +go env -w GO111MODULE="on" + +``` + +临时设置(不推荐) + +```bash +# 配置 GOPROXY 环境变量 +$env:GOPROXY = "https://proxy.golang.com.cn,direct" +# 还可以设置不走 proxy 的私有仓库或组,多个用逗号相隔(可选) +$env:GOPRIVATE = "git.mycompany.com,github.com/my/private" +``` + +mac/linux 下 + +```bash +# 配置 GOPROXY 环境变量 +export GOPROXY=https://proxy.golang.com.cn,direct +# 还可以设置不走 proxy 的私有仓库或组,多个用逗号相隔(可选) +export GOPRIVATE=git.mycompany.com,github.com/my/private +``` + +### 常用的 go 代理 + +- goproxy [https://goproxy.io/zh/](https://goproxy.io/zh/) +- 阿里云 [https://mirrors.aliyun.com/goproxy/](https://mirrors.aliyun.com/goproxy/) +- 七牛云 [https://goproxy.cn](https://goproxy.cn) + +可输出 go env 查看环境 + +## 配置 VSCode 开发环境 + +[VsCode 中 Golang Tools 使用 · 语雀 (yuque.com)](https://www.yuque.com/flipped-aurora/gqbcfk/lidsv6) + +这里使用的是 VSCode 进行开发,在扩展程序中安装 Go 插件,输入 + +- `command` + `shift` + `p` 输入 Go:Show All Commands 选择 Go:Install/Update Tools,选择所有工具,并确定安装。 + +![image-20210901044224765](https://img.kuizuo.cn/image-20210901044224765.png) + +控制台输出安装结果 + +![image-20210901044323709](https://img.kuizuo.cn/image-20210901044323709.png) + +或者打开命令提示符(以管理员身份打开)输入 + +```bash +go get -v github.com/mdempsky/gocode +go get -v github.com/uudashr/gopkgs/v2/cmd/gopkgs +go get -v github.com/rogpeppe/godef +go get -u github.com/ramya-rao-a/go-outline +go get -v github.com/sqs/goreturns +``` + +安装 go 的开发依赖,比如语法提示,包提示等等。安装完成后,就此配置完成 Vscode 的 Go 开发环境。 + +## Goland + +没什么好说的,大部分配置无需操作即可使用,不过个人还是倾向于使用 VSCode。 diff --git "a/docs/skill/programming-languages/go/Go\350\257\255\350\250\200\344\271\213json\344\275\277\347\224\250.md" "b/docs/skill/programming-languages/go/Go\350\257\255\350\250\200\344\271\213json\344\275\277\347\224\250.md" new file mode 100644 index 0000000..e0aec1f --- /dev/null +++ "b/docs/skill/programming-languages/go/Go\350\257\255\350\250\200\344\271\213json\344\275\277\347\224\250.md" @@ -0,0 +1,223 @@ +--- +id: go-json-usage +slug: /go-json-usage +title: Go语言之json使用 +date: 2022-05-20 +authors: kuizuo +tags: [go, json] +keywords: [go, json] +--- + + + +Go 语言中,官方提供了一个专门的包 `encoding/json` + +## json 反序列(解析) + +假设有这样一个 json 数据,我要将其解析为 go 结构体 + +```json +{ "name": "kuizuo", "age": 20 } +``` + +首先需要定义结构体,通常可以使用 json 转 go 结构体的[在线工具](https://mholt.github.io/json-to-go/),如下图 + +![json-to-go.png (1582×248) (kuizuo.cn)](https://img.kuizuo.cn/json-to-go.png) + +其转化代码如下 + +```go +import ( + "encoding/json" + "fmt" +) + +type Person struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + var p Person + + jsonString := `{"name": "kuizuo", "age" : 20}` + + err := json.Unmarshal([]byte(jsonString), &p) + + if err == nil { + fmt.Println(p.Name) + fmt.Println(p.Age) + } else { + fmt.Println(err) + } +} + +``` + +**主要步骤** + +- 定义结构体(一般结构体的每个字段第一个字母大写),并新建**结构体变量** +- 核心代码`err := json.Unmarshal([]byte(jsonString), &p)`,如果解析有效那么 err 为 nil,并将 p 赋值其 json 数据。 +- 访问结构体 p 的成员 + +### 解析 json 数组 + +如果要解析 json 数组,其步骤同上,演示代码如下 + +```go +import ( + "encoding/json" + "fmt" +) + +type Person struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + var persons []Person + + jsonString := `[{"name": "kuizuo", "age" : 20},{"name": "愧怍", "age" : 22}]` + + err := json.Unmarshal([]byte(jsonString), &persons) + + if err == nil { + for _, p := range persons { + fmt.Print("\t\n", p.Name) + fmt.Print("\t", p.Age) + } + } else { + fmt.Println(err) + } +} + +``` + +哪怕对于再复杂的 json 数据,都要先将 json 转为 go 结构体,然后执行`json.Unmarshal`。 + +### 自定义属性名称的映射 + +假设 json 的 key 值存在空格(一般情况下不可能,以我多年读写 json 的经历都没看到过),由于 go 中无法将空格做为变量标识符(貌似没有语言支持空格当标识符),而 json-to-go 工具会将空格清楚,并将下个单词首字母大写。 + +```json +{ + "user name": "kuizuo" +} +``` + +```go +type AutoGenerated struct { + UserName string `json:"user name"` +} +``` + +当然,这里的 UserName 可以随便命名 go 变量规范的名字,只要`json:"user name"`不变,结构体映射的还是`user name`属性。 + +### map[string]interface{} + +对于未知属性,并且不想定义结构体的话,go 有个很典型的解决方法是采用 `map[string]interface{}` ,在一些情况(仅一层 json 数据)下确实会比较方便,但也会存在 json 属性不存在导致读取错误的情况。 + +```go +import ( + "encoding/json" + "fmt" +) + +func main() { + jsonString := ` + { + "code": 200, + "data": { + "username": "kuizuo", + "age": 20 + } + }` + + var result map[string]interface{} + err := json.Unmarshal([]byte(jsonString), &result) + if err == nil { + fmt.Println(result["code"]) + username := result["data"].(map[string]interface{})["username"] + fmt.Println(username) + } else { + fmt.Println(err) + } +} +``` + +:::tip + +在 Go1.18 更新中,any 作为一个新的关键字出现。**any 本质上是 interface{} 的别名** + +```go +type any = interface{} +``` + +`map[string]interface{}` 也可写为 `map[string]any` + +::: + +## json 序列化 + +既然前面是 json 转 go 结构体,那这肯定是将 go 结构体转为 json。 + +同样的,既然想要生成 json 数据,那么也可将要生成的 json 数据放到 json-to-go 里转为 go 结构体。如 + +```json +{ + "id": 1, + "username": "kuizuo", + "hobby": ["敲代码", "吃饭", "睡觉"] +} +``` + +```go +type AutoGenerated struct { + ID int `json:"id"` + Username string `json:"username"` + Hobby []string `json:"hobby"` +} +``` + +对应的 go 代码如下 + +```go +import ( + "encoding/json" + "fmt" +) + +type User struct { + ID int `json:"id"` + Username string `json:"username"` + Hobby []string `json:"hobby"` +} + +func main() { + user := &User{ + ID: 1, + Username: "kuizuo", + Hobby: []string{"敲代码", "吃饭", "睡觉"}, + } + + result, _ := json.Marshal(user) + fmt.Println(string(result)) +} +``` + +**主要步骤** + +- 定义结构体,并新建**结构体指针**,同时赋值 +- 调用`result, _ := json.Marshal(user)`,此时的 result 为字节数组。 +- `string(result)` 将其转为 json 字符串 + +json 序列化没什么过多补充的,上面这里例子实际中已经足够使用了。 + +## 相关 json 库 + +[tidwall/gjson: Get JSON values quickly - JSON parser for Go (github.com)](https://github.com/tidwall/gjson) 强烈推荐(还支持 jsonpath 语法) + +## 总结 + +和 js 相比,go 对 json 的操作可以说是比较繁琐,但这并不是只有 go 这样,静态类型的语言都相对繁琐。在 go 是定义结构体,而在 java 则是定义类,每种语言都有数据格式的规范。不过 json 序列化与反序列化还是 js 最为方便,毕竟 json(JavaScript Object Notation)本身就作为 js 的对象数据表达形式。 diff --git "a/docs/skill/programming-languages/go/Go\350\260\203\347\224\250js\344\273\243\347\240\201.md" "b/docs/skill/programming-languages/go/Go\350\260\203\347\224\250js\344\273\243\347\240\201.md" new file mode 100644 index 0000000..28b4292 --- /dev/null +++ "b/docs/skill/programming-languages/go/Go\350\260\203\347\224\250js\344\273\243\347\240\201.md" @@ -0,0 +1,97 @@ +--- +id: go-call-js +slug: /go-call-js +title: Go调用js代码 +date: 2022-05-22 +authors: kuizuo +tags: [go, javascript] +keywords: [go, javascript] +--- + + + +## 运行 js 代码 + +```go +import ( + "fmt" + + "github.com/robertkrimen/otto" +) + +func main() { + vm := otto.New() + result, _ := vm.Run(` + foo = 1 + 2 + console.log(foo) + result = foo; + `) + fmt.Println(result) // 4 +} +``` + +## 调用函数 + +```go +func main() { + vm := otto.New() + vm.Run(` + function hello(name){ + console.log('hello ' + name) + return 'OK' + } +`) + + ret, _ := vm.Call("hello", nil, "kuizuo") + fmt.Println(ret) +} + +``` + +这里以 go 调用 js 的 CryptoJS 来实现加密演示。 + +```go +func main() { + bytes, _ := ioutil.ReadFile("md5.js") + vm := otto.New() + vm.Run(string(bytes)) + + ret, _ := vm.Call("MD5", nil, "a123456") + fmt.Println(ret) +} +``` + +## 封装成 go 函数 + +不过这样写法不方便,可以将其封装为一个 go 函数来调用。 + +```go +import ( + "fmt" + "io/ioutil" + + "github.com/robertkrimen/otto" +) + +var vm = otto.New() + +func initJs() { + bytes, _ := ioutil.ReadFile("md5.js") + vm.Run(string(bytes)) +} + +func md5(content string) string { + ret, err := vm.Call("MD5", nil, content) + if err != nil { + return "" + } + return ret.String() +} + +func main() { + initJs() + result := md5("a123456") + fmt.Println(result) +} + +``` diff --git "a/docs/skill/programming-languages/java/Java\345\217\215\345\260\204.md" "b/docs/skill/programming-languages/java/Java\345\217\215\345\260\204.md" new file mode 100644 index 0000000..b0e7c55 --- /dev/null +++ "b/docs/skill/programming-languages/java/Java\345\217\215\345\260\204.md" @@ -0,0 +1,225 @@ +--- +slug: java-reflect +title: java反射 +date: 2022-01-16 +authors: kuizuo +tags: [java] +keywords: [java] +--- + + + +## 什么是反射?   + +Java 反射(Reflection)就是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;并且能改变它的属性。(摘自网络) + +## 反射能做什么? + +由于我们能够知道这个类的所有属性和方法,并且可以调用其方法与属性,那么我们就可以在外部,不通过修改类的形式来给类增加额外自定义功能。 + +在一些框架开发中,可以更灵活的编写代码,在运行时装配无需针对原类进行大幅度改动,降低代码耦合度。 + +在安卓逆向中,反射的主要作用就是寻找到某个类,去注入我们的代码,以便查看调用前后的参数与结果,也称之为 hook。 + +## 反射常用 API + +### 获取 Class 对象 + +在反射中,要获取一个类或调用一个类的方法,我们首先需要获取到该类的 Class 对象,获取 Class 类对象以下方法: + +**1、使用 Class.forName 静态方法。当你知道该类的全路径名时,你可以使用该方法获取 Class 类对象。** + +```java +Class cls = Class.forName("java.lang.String"); +``` + +**2、使用 .class 方法。** + +这种方法只适合在编译前就知道操作的 Class。 + +```java +Class cls = String.class; +``` + +**3、使用类对象的 getClass() 方法。** + +```java +String str = new String("Hello"); +Class cls = str.getClass(); +``` + +**4、ClassLoader.loadClass()** + +前提:已经获取到 ClassLoader 的情况下(Person 是定义好的类,其中`String.class.getClassLoader()`获取到得为 null) + +```java +ClassLoader clsl = Person.class.getClassLoader(); +Class cls = clsl.loadClass("Person"); +``` + +基本数据类型的类对象获取 `int.class` `Integer.TYPE` 得到`int` + +包装类的 Class 类对象获取 `Integer.class` 得到 `class java.lang.Integer` + +#### 哪些有 Class 对象 + +并非所有 java 对象都有 Class 对象,获取方式如上 + +- 外部类 +- 内部类 + +内部类的获取方式通过$连接外部类与内部类,多个内部类也可通过$1,$2 依次获取 + +```java +Class cls1 = Class.forName("OutClass$InnerClass"); +Class cls2 = Class.forName("OutClass$1"); +``` + +- 接口 =>`interface DemoI` +- 数组 => `class [Ljava.lang.String;` +- 枚举 enum +- Thread.State +- 注解 anntation +- 基本数据类型 +- 包装类 +- void + +### 创建类对象(与获取构造函数) + +**1、通过 Class 对象的 newInstance 方法**(无法传参) + +```java +Person p = Person.class.newInstance(); +// 相当于 Person p = new Person(); +``` + +**2、通过 Constructor 对象的 newInstance() 方法** (可传参数) + +可以传参数,但需要知道传入参数类型,以确定哪个构造函数。 + +```java +Constructor constructor = Person.class.getConstructor(String.class); +Person p = constructor.newInstance("kuizuo"); +``` + +如果构造函数是私有方法,则通过`getDeclaredConstructor`获取 Constructor + +同时设置是否访问 `constructor.setAccessible(true)` 才可访问 + +```java +Constructor constructor = Person.class.getDeclaredConstructor(String.class); +constructor.setAccessible(true); +Person p = constructor.newInstance("kuizuo"); +``` + +**getParamerTypes** 获取参数类型数组(Class []) + +**要获取私有属性,私有方法或私有构造器,则必须使用有 declared 关键字的方法。** + +### 获取类属性 + +- **getField **只可获取公有属性 + +```java +Field nameField = Person.class.getField("name"); +String name =(String) nameField.get(p); +``` + +设置属性值 + +```java +Field nameField = Person.class.getField("name"); +nameField.set(p,"kuizuo12"); +``` + +设置静态属性值 set 第一个参数给 null 即可 + +```java +Field nameField = Person.class.getField("name"); +nameField.set(null,"kuizuo12"); +``` + +- **getDeclaredField **只可获取所有属性 +- **getFields** 获取所有共有属性 +- **getDeclaredFields** 获取所有属性 + +```java + Field[] fields = Person.class.getDeclaredFields(); + for (Field field : fields) { + System.out.println(field.getName()); + } +``` + +### 获取类方法 + +- **getMethod** 获取 + +参数一为方法名,其余参数为参数类型 + +调用通过`method.invoke`调用,参数一为对象,其余参数为实参 + +```java +Method method = Person.class.getMethod("say", String.class); +method.invoke(p, "hello"); + +``` + +**如果是静态方法,invoke 第一个参数可传入 null** + +- **getDeclaredMethod** 可获取私有方法 (也需要 setAccessible) + +```java +Method method = Person.class.getDeclaredMethod("say", String.class); +method.setAccessible(true); +method.invoke(p, "hello"); +``` + +- **getMethods** 获取所有公有方法 +- **getDeclaredMethods**获取所有方法 + +```java +Method[] methods = Person.class.getDeclaredMethods(); +for (Method method : methods) { + System.out.println(method.getName()); +} +``` + +### 获取父类 + +- **getSuperclass** + +接口无父类 + +### 获取内部类 + +- **getClasses** + +```java +Class[] classes = Person.class.getClasses(); +System.out.println(classes[0]); +``` + +- **getDeclaredClasses** 获取所有内部类(包括私有) + +### 获取接口 + +前提:实现(implements)一个接口 + +```java +Class[] interfaces = Person.class.getInterfaces(); +System.out.println(interfaces.length); +``` + +### 其他方法 + +官方文档 [Class (Java Platform SE 8 ) (oracle.com)](https://docs.oracle.com/javase/8/docs/api/) + +大致常用的方法如上,其余的 Class 类的方法还有 + +- getName 获取全类名 +- getSimpleName 获取简单类名 +- getModifiers 获取标识符 +- getAnnotations 获取注解 +- getPackage 获取包名 + +具体代码就不演示了。 diff --git "a/docs/skill/programming-languages/python/Python\344\270\255\347\232\204cv2\344\275\277\347\224\250.md" "b/docs/skill/programming-languages/python/Python\344\270\255\347\232\204cv2\344\275\277\347\224\250.md" new file mode 100644 index 0000000..fd49d6b --- /dev/null +++ "b/docs/skill/programming-languages/python/Python\344\270\255\347\232\204cv2\344\275\277\347\224\250.md" @@ -0,0 +1,152 @@ +--- +id: python-cv2-usage +slug: /python-cv2-usage +title: Python中的cv2使用 +date: 2022-03-06 +authors: kuizuo +tags: [python] +keywords: [python] +--- + + + +[模块 cv2 的用法 - 陨落&新生 - 博客园 (cnblogs.com)](https://www.cnblogs.com/shizhengwen/p/8719062.html) + +[Python-OpenCV 基本操作 cv2 - 菜鸟程序猿\_python - 博客园 (cnblogs.com)](https://www.cnblogs.com/zlel/p/9267629.html) + +## 常用方法 + +### 读取图像 + +cv2.imread(filepath,flags) + +- filepath:要读入图片的完整路径 + +- flags:读入图片的标志 + +- - cv2.IMREAD_COLOR:默认参数(3),读入一副彩色图片,忽略 alpha 通道 + - cv2.IMREAD_GRAYSCALE:读入灰度图片 + - cv2.IMREAD_UNCHANGED:顾名思义,读入完整图片,包括 alpha 通道 + +### 写入图像 + +cv2.imwrite(filepath, img, flags) + +- filepath: 要保存图像的文件名 +- img: 要保存的图像 +- flags: 可选的第三个参数,它针对特定的格式:对于 JPEG,其表示的是图像的质量,用 0 - 100 的整数表示,默认 95;对于 png ,第三个参数表示的是压缩级别。默认为 3. + +cv2.IMWRITE_JPEG_QUALITY 类型为 long ,必须转换成 int + +cv2.IMWRITE_PNG_COMPRESSION, 从 0 到 9 压缩级别越高图像越小。 + +```python +cv2.imwrite('1.png',img, [int(cv2.IMWRITE_JPEG_QUALITY), 95]) +cv2.imwrite('1.png',img, [int(cv2.IMWRITE_PNG_COMPRESSION), 9]) +``` + +### 显示图像 + +演示代码如下 + +```python +import cv2 + +img = cv2.imread('temp.jpg') +cv2.imwrite('save.jpg', img) +cv2.imshow('img', img) +cv2.waitKey(0) +cv2.destroyAllWindow() +``` + +### img 的一些属性 + +```python +img.shape # (1200, 1920, 3) 宽、高、通道数 +img.size # 像素个数 +img.dtype # uint8 +``` + +### 颜色转化 + +由于 cv2 得到的图片是 BGR 格式,而非传统的 RGB 格式,因此需要进行转化。 + +有以下三种方法 + +```python +im_bgr = cv2.imread('temp.jpg') + +im_rgb = im_bgr[:, :, [2, 1, 0]] +im_rgb = im_bgr[:, :, ::-1] +im_rgb = cv2.cvtColor(im_bgr, cv2.COLOR_BGR2RGB) +``` + +还有一些颜色空间转化 + +```python +#彩色图像转为灰度图像 +img2 = cv2.cvtColor(img,cv2.COLOR_RGB2GRAY) +#灰度图像转为彩色图像 +img3 = cv2.cvtColor(img,cv2.COLOR_GRAY2RGB) +# cv2.COLOR_X2Y,其中X,Y = RGB, BGR, GRAY, HSV, YCrCb, XYZ, Lab, Luv, HLS +``` + +### cv 图片对象与二进制图片转化 + +```python +def bytes2cv(im): + return cv2.imdecode(np.array(bytearray(im), dtype='uint8'), cv2.IMREAD_UNCHANGED) + +def cv2bytes(im): + return np.array(cv2.imencode('.png', im)[1]).tobytes() +``` + +### 添加边框 + +```python +import cv2 + +poses = [[111, 46, 151, 86], [177, 46, 212, 80], + [246, 89, 286, 128], [240, 18, 280, 56]] + +img = cv2.imread("1.jpg") + +for box in poses: + x1, y1, x2, y2 = box + img = cv2.rectangle(img, (x1, y1), (x2, y2), color=(0, 0, 255), thickness=2) + +cv2.imwrite("result.jpg", img) +``` + +![result](https://img.kuizuo.cn/result.png) + +### 添加文本 + +```python +import cv2 + +img = cv2.imread('temp.jpg') +# 图片对象、文本、像素、字体、字体大小、颜色、字体粗细 +img_text = cv2.putText(img, "kuizuo", (50, 50), + cv2.FONT_HERSHEY_DUPLEX, 5.5, (35, 175, 255), 2) +cv2.imwrite("result.jpg", img_text) +``` + +效果如下 + +![image-20220306203918438](https://img.kuizuo.cn/image-20220306203918438.png) + +### 图片缩放 + +```python +import cv2 + +img = cv2.imread("1.png") +cv2.imshow("img", img) + +img1 = cv2.resize(img, (200, 100)) + +cv2.imshow("img1", img1) + +cv2.waitKey() +``` diff --git "a/docs/skill/programming-languages/python/Python\346\214\207\345\256\232\347\211\210\346\234\254\350\277\220\350\241\214.md" "b/docs/skill/programming-languages/python/Python\346\214\207\345\256\232\347\211\210\346\234\254\350\277\220\350\241\214.md" new file mode 100644 index 0000000..fc0b235 --- /dev/null +++ "b/docs/skill/programming-languages/python/Python\346\214\207\345\256\232\347\211\210\346\234\254\350\277\220\350\241\214.md" @@ -0,0 +1,66 @@ +--- +id: python-specified-versiton-run +slug: /python-specified-versiton-run +title: Python指定版本运行 +date: 2020-09-11 +authors: kuizuo +tags: [python] +keywords: [python] +--- + + + +## 前言 + +在用一些开源的 python 脚本的时候,而原作者是用`python2.7`写的,但学过 python 的应该会知道 python 每个版本之间存在兼容性,python2 的代码用 python3 是会可能运行不了的,一些现有的框架在 python3.6 可以运行而 python3.7 就报错。通常这时候我想执行 python2 代码的解决办法: + +- 安装 python2,并且就算安装了还要重新配置环境变量这些(麻烦) +- 通过虚拟环境,来安装 python2,在虚拟环境中运行 python2 代码(麻烦) +- python3(>=3.3)其实自带了 python2 的代码,就没必要像上面那么麻烦 + +### 具体实现步骤 + +其实在安装 Python3(>=3.3)时,Python 的安装包实际上在系统中安装了一个启动器`py.exe`,默认放置在文件夹`C:\Windows\`下面。这个启动器允许我们指定使用 Python2 还是 Python3 来运行代码。 + +![image-20200912224056257](https://img.kuizuo.cn/image-20200912224056257.png) + +例如: + +#### 运行 python2 + +```bash +py -2 demo.py +``` + +![image-20200912225223752](https://img.kuizuo.cn/image-20200912225223752.png) + +#### 运行 python3 + +```bash +py -3 demo.py +``` + +![image-20200912225250066](https://img.kuizuo.cn/image-20200912225250066.png) + +只要把命令行的 python 的改成 py -2 就能以 python2 来执行。 但是,每次运行都要加入参数-2 和-3 还是比较麻烦,于是所以 py.exe 这个启动器允许你在代码中加入说明,表明这个文件应该是由 python2 或 3 来解释运行。只需要在代码文件的最开始加入一行,**一定要放到文件第一行**,编码可以放在第二行,如 + +```py +#!py -2 +# -*- coding: utf-8 -*- + +...code +``` + +### pip 安装 + +#### python2 下安装 + +```bash +py -2 -m pip install XXXX +``` + +#### python3 下安装 + +```bash +py -3 -m pip install XXXX +``` diff --git "a/docs/skill/programming-languages/python/Python\347\210\254\350\231\253\346\200\273\347\273\223.md" "b/docs/skill/programming-languages/python/Python\347\210\254\350\231\253\346\200\273\347\273\223.md" new file mode 100644 index 0000000..825f357 --- /dev/null +++ "b/docs/skill/programming-languages/python/Python\347\210\254\350\231\253\346\200\273\347\273\223.md" @@ -0,0 +1,458 @@ +--- +id: python-spider-summary +slug: /python-spider-summary +title: Python爬虫总结 +date: 2022-03-03 +authors: kuizuo +tags: [python, node, http] +keywords: [python, node, http] +--- + +最近临时写了个 python 爬虫的例子(核心代码不开源),总结下这次编写过程中的一些相关知识点与注意事项,以一个用 nodejs 写爬虫的开发者的角度去看待与比对。 + + + +## 编码 + +在抓包与协议复现的时候,出现中文以及特殊符号免不了 url 编码,python 的编码可以使用内置库 urllib,同时也能指定编码格式。 + +gbk 编码中文是占 2 个字节,utf8 编码中文占 3 个字节 + +### url 编码 + +```python +from urllib.parse import urlencode, parse_qs, quote, unquote + +quote("愧怍", encoding="gbk") +# %C0%A2%E2%F4 +``` + +quot 还有一个 safe 参数,可以指定那个字符不进行 url 编码 + +```python +quote("?", safe=";/?:@&=+$,", encoding="utf8") +# ? 加了safe +# %3F 不加safe +``` + +解码操作与编码同理 + +```python +unquote("%C0%A2%E2%F4", encoding="gbk") +# 愧怍 +``` + +如果编码格式错误,比如 gbk 编码用 utf8 解码将会变成不可见字符 ����,而用 utf8 编码用 gbk 解码,存在一个字节差,会输出成其他字符串,比如 `你好` 就会变成 `浣犲ソ`,代码 `unquote(quote("你好",encoding='utf8'), encoding="gbk")` + +### URL 查询字符串 + +如果想构造一个 `a=1&b=2`的 url 查询字符串,使用文本拼接很不现实。urllib 提供 urlencode 与 parse_qs 可以在查询字符串与字典中切换 + +```python +urlencode({ + "username": '愧怍', + "password": 'a123456' +}) +# username=%E6%84%A7%E6%80%8D&password=a123456 +``` + +也有 encoding 与 safe 参数,配置同 quote,就不演示了。 + +```python +parse_qs('a=1&a=2&b=2') +# {'a': ['1', '2'], 'b': ['3']} +``` + +将查询字符串转为 python 字典的话,值都是列表(应该是考虑可能会多个相同参数才这么设计) + +小提一下,nodejs 中有个 querystring,方法 parse 与 stringify 与效果同理。 + +## 解构赋值 + +```python +a,b = [1,2] +print(a,b) + +user = { + "username": "kuizuo", + "password": "a123456" +} +username, password = user.values() +print(username, password) + +print(user.keys()) +# dict_keys(['username', 'password']) +print(user.values()) +# dict_values(['kuizuo', 'a123456']) +``` + +解构赋值没什么好说的,和 js 类似,只不过对字典的解构赋值的话,要取值则要调用 values(),取 key 的话默认不填,但是也可以调用 keys() + +## 模板字符串 + +```python +user = 'kuizuo' +print(f'username: {user} age: {20+1}') +# username: kuizuo age: 21 +``` + +同样{}中可以编写表达式,与 js 的模板字符串类似 + +如果是 python3.6 之前的话,则是用使用 string.format 方法(不常用,也不好用) + +```python +"username: {} age: {}".format("愧怍", 18) +``` + +而 js 中的模板字符串则是使用反引号`和${},像下面这样 + +```javascript +user = 'kuizuo' +console.log(`username: ${user} age: ${20+1}`) +# username: kuizuo age: 21 +``` + +## 字典 + +python 的字典与 js 的对象有些许相像,个人总体感觉没有 js 的对象灵活,演示如下 + +```python +user = { 'username':'kuizuo','password':'a123456' } +print(user['username']) +``` + +想要获取字典中的值,就需要写成`user['username']`,如果习惯了 js 的写法(比如我),就会习惯的写成`user.username`,这在 python 中将会报错,`AttributeError: 'dict' object has no attribute 'username'`,并且字典的 key 还需要使用引号进行包裹,如果是 js 的话,代码如下 + +```javascript +user = { username: 'kuizuo', password:'a123456' +console.log(user.username) +``` + +如果想在 key 中包裹引号也是可以的,省略引号相当于代码简洁,同时取值也可以像 python 中的`user['username']`来进行取值,相对灵活。 + +假设我想取 user 的 age 属性,但是 user 没有 age 属性,python 则是直接报错`KeyError: 'age'`,可以使用`user.get('age',20)`,如果没有 age 属性,则默认 20。而 js 是不会报错,则是会返回`undefiend`,如果想要默认值的话可以像这样,`user.age || 20`。毕竟 js 调用类的方法属性都是可以直接 `对象.属性` `对象.方法`,而 python 中是 `对象["属性"]` `对象.方法`,只能说各有各的优劣吧 。 + +不过 js 不确定是否有该属性的话,可以使用`?.`,比方`user?.age`,这样返回的`null`,而不是`undefiend`。 + +:::note 易错小结:获取字典属性使用 `字典['属性值']` 获取,key 需用引号包裹 + +::: + +## 类 + +在写爬虫时,我都会将其封装成类,把一些核心的方法封装成类方法,比如登录,获取图片验证码等等 + +```python +class Demo(): + + def __init__(self, user): + self.user = user + + def get_img_code(self): + pass + + def login(self): + pass + + def get_xxx(self): + pass +``` + +同样的,像 requests 的 session 也会将其封装在类属性下,但是我一开始的写法是 + +```python +class Demo(): + session = requests.Session() + + def __init__(self, user): + self.user = user + +``` + +导致我创建多个实例时 + +```python +demo1 = Demo() +demo2 = Demo() +``` + +demo1 与 demo2 的的 session 是相等的,经过百度,了解到这样定义的类属性相当于是共有属性,每个实例下获取到的都是同一个 session,如果将 session 放置在`__init__`下,每个实例的 session 就不相同 + +```python +class Demo(): + def __init__(self, user): + self.session = requests.Session() + self.user = user +``` + +其中 `__init__`相当于 js 中的 constructor,也就是构造函数了。 + +不过 python 的方法第一个参数都要是 self,像 js 或者 java 等一些面向对象的语言,不用特意声明 this,就可以直接使用 this 来调用自身属性与方法。而 python 则需要显式的声明 self。 + +[Python 为什么要保留显式的 self ? - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/84546388) + +:::note 易错小结:类共有属性与实例属性区别 + +::: + +## 线程 + +python3 中线程操作可以使用 threading + +```python +import threading + +def func(name, sec): + print('---开始---', name, '时间', ctime()) + sleep(sec) + print('***结束***', name, '时间', ctime()) + +# 创建 Thread 实例 +t1 = Thread(target=func, args=('第一个线程', 1)) +t2 = Thread(target=func, args=('第二个线程', 2)) + +# 启动线程运行 +t1.start() +t2.start() + +# 等待所有线程执行完毕 +t1.join() # join() 等待线程终止,要不然一直挂起 +t2.join() +``` + +### 多线程 + +如果要实现多线程的话,需要将 Thread 实例(线程句柄),保存到列表中,然后调用 join + +```python +l = [] + +for i in range(10): # 开启10个线程 + t = threading.Thread(target=func, args=('第'+str(i)+'个线程', i)) + t.start() + l.append(t) + +# 等待线程运行结束 +for i in l: + i.join() +``` + +### 锁 + +说到多线程,怎么可能不提到锁呢。 + +```python +import threading +from time import sleep + +def func(): + global num + sleep(1) + lock.acquire() # 获取锁 + num = num+1 + print(num) + lock.release() # 释放锁 + + +lock = threading.Lock() +num = 0 + +for i in range(10): + t = threading.Thread(target=func, args=()) + t.start() + +``` + +获取与释放锁的操作可以使用 with 关键字来操作 + +```python +with lock: + num = num+1 + print(num) +``` + +## 时间 + +### 计算两者时间间隔 + +```python +duringtime = datetime.datetime.strptime('2022-03-02 16:16:16', "%Y-%m-%d %H:%M:%S") - datetime.datetime.now() +seconds = duringtime.seconds +``` + +### 定时任务 + +[8 种 Python 定时任务的解决方案 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/410388979) + +## http 请求库 + +python 较为知名的 http 请求库无非就是 requests 了,但是 requests 不支持异步,在某些情况下,就只能等待上条请求结束,而异步请求则可以在发起一次请求后,在等待网站返回结果的时间里,可以继续发送更多请求。 + +此外还有 aiohttp、httpx,由于 httpx 又可以发送同步,也可以发送异步请求,号称新下一代网络请求库,并且基本与 requests 的代码重合度高,只需要改点对应关键词即可,这里所使用的时 httpx,并着重针对两者的区别进行测试。 + +### cookies + +在 requests 中想要在下次使用上次响应中返回 cookies 十分简单,只需要设置实例化一个 session,然后使用 session 来发送后续的请求。在 requests 中是`session = requests.Session()`,而 httpx 则是`client = httpx.Client()`来代替 + +不过 httpx 则是有同步客户端与异步客户端,下面就是异步请求对的演示代码 + +```python +import asyncio +import httpx + +async def main(): + async with httpx.AsyncClient() as client: # 创建一个异步client + r = await client.get('https://www.example.com/') + print(r) + +if __name__ == '__main__': + asyncio.run(main()) +``` + +获取请求的 cookies 也比较简单 + +request + +```python +cookies_dict = requests.utils.dict_from_cookiejar(session.cookies) +``` + +httpx + +```python +cookies_dict = dict(self.client.cookies) +``` + +### 协议头 + +在 http 请求中,少不了协议头的检测,比如说 Referer 检测来源链接是否符合要求,Content-Type 的请求体格式等等。但是如果在每条请求下都添加 headers 就略显代码繁杂,而且像很多公用的协议头 Origin,User-Agent 在全部的请求都是不变的,就可以使用`client.headers.update`设置成全局的协议头 + +```python +client.headers.update({ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36 Edg/98.0.1108.62", +}) +``` + +如果不设置的话,默认全局协议头如下 + +```python +Headers({'host': 'example.com', 'accept': '*/*', 'accept-encoding': 'gzip, deflate', 'connection': 'keep-alive', 'user-agent': 'python-httpx/0.22.0'}) +``` + +### post 请求 + +post 请求主要有两种格式一个是查询字符串 `a=1&b=2`,另一个是 json 格式 `{"a": 1, "b": 2}`,下面为代码演示 + +查询字符串 + +```python +import httpx +data = { + "username": "kuizuo", + "password": "a123456" +} + +httpx.post( + url='http://example.com', data=data) +# 请求体 username=kuizuo&password=a123456 +``` + +json + +```python +import httpx +data = { + "username": "kuizuo", + "password": "a123456" +} + +httpx.post( + url='http://example.com', json=data) +# 请求体 {"username": "kuizuo", "password": "a123456"} +``` + +请求库将会自动将根据你所传入的字典,转成对应的格式,同时会携带对应`Content-Type`协议头`Content-Type: application/x-www-form-urlencoded` 与 `Content-Type: application/json`。所以就不需要使用 + +:::warning 要注意一点的时,如果 data 不是字典,而是字符串 `a=1&b=2` ,那么请求时不会携带`Content-Type`,如果网站有对`Content-Type`的判断的话,那么这次的请求很有可能报错。 + +::: + +:::note 易错小结:请求库默认使用 utf8 编码,如果想要发送 gbk 编码的话,就需要使用 urlencode,然后设置对应的协议头。(相对还是比较麻烦的,暂时没找到比较有效的方法) + +::: + +### 重定向 + +requests 默认情况下是允许重定向请求的,而 httpx 则是默认不允许重定向,所以,如果项目中涉及到重定向的请求的话,是需要改点代码 + +如果要禁止重定向设置为 False,允许则为 True + +requests 的参数是`allow_redirects`,而 httpx 则是`follow_redirects`,如果想要在 httpx 设置允许重定向的话,可以在 client 中设置,之后的请求都将进行重定向 + +```python +client.follow_redirects = True +``` + +不过在正常协议复现的情况下,是不建议允许重定向的,因为有可能重定向的那个请求有必要关键参数可能会在后续中使用到,而重定向就会直接跳过。 + +:::note 易错小结:requests 是 allow_redirects,httpx 是 follow_redirects + +::: + +### 拦截器(hook) + +http 拦截器主要用途在请求时附带一些参数(比方说 post 请求对 body 进行加密,添加 authorization 协议头),在返回响应的时候作何处理(如请求重试,ip 异常更换 ip,对响应结果进行统一处理) + +在 node 的请求库 axios 中的叫拦截器,而在 requests 中则是叫 hook,httpx 则是 event_hooks,下面对两者拦截器进行简单演示 + +requests + +```python +import requests + +def log_response(r, *args, **kwargs): + request = r.request + print( + f"Response event hook: {request.method} {request.url} - Status {r.status_code}") + + +requests.get(url='http://example.com', + hooks=dict(response=[log_response])) +``` + +httpx + +```python +import httpx + +def log_request(request): + print( + f"Request event hook: {request.method} {request.url} - Waiting for response") + + +def log_response(response): + request = response.request + print( + f"Response event hook: {request.method} {request.url} - Status {response.status_code}") + + +client = httpx.Client( + event_hooks={'request': [log_request], 'response': [log_response]}) + +r = client.get( + url='http://example.com') +``` + +requests 只支持响应后处理,还不支持请求发送前处理,而 httpx 则是都支持,所以更推荐使用 httpx。 + +## OCR + +python 有一个 ocr 的识别库 [ddddocr](https://github.com/sml2h3/ddddocr) + +主要是用于识别验证码,不过前提环境要求 python,通过`pip install ddddocr`进行安装,具体演示代码在官网文档上也有,这里就不做演示了。 + +还有一个搭建 api 服务的 [sml2h3/ocr_api_server: 使用 ddddocr 的最简 api 搭建项目,支持 docker (github.com)](https://github.com/sml2h3/ocr_api_server) + +## 总结 + +主要是这个爬虫项目中所使用到了 OCR 识别验证码,加上太久没有编写 python 爬虫的项目,就打算编写一个 demo 例子,顺带巩固下 python 的一些语法特性。整体体验其实与 node 相差不大,但是 python 对异步的支持不如 js 的异步,并且 js 编写 json 数据更加灵活,最主要是 node 的三大特性**单线程、非阻塞 I/O、事件驱动**,如果不是特殊必要,我都会首选 node 的 axios 库来进行编写 http 请求。 diff --git "a/docs/skill/programming-languages/python/pyautogui\350\207\252\345\212\250\345\214\226\346\223\215\344\275\234\350\204\232\346\234\254.md" "b/docs/skill/programming-languages/python/pyautogui\350\207\252\345\212\250\345\214\226\346\223\215\344\275\234\350\204\232\346\234\254.md" new file mode 100644 index 0000000..5b5d056 --- /dev/null +++ "b/docs/skill/programming-languages/python/pyautogui\350\207\252\345\212\250\345\214\226\346\223\215\344\275\234\350\204\232\346\234\254.md" @@ -0,0 +1,160 @@ +--- +id: pyautogui +slug: /pyautogui +title: pyautogui自动化操作脚本 +date: 2022-02-18 +authors: kuizuo +tags: [python, script, auto] +keywords: [python, script, auto] +--- + + + +说实话,貌似有一年没写过啥脚本类的代码了 + +之前针对加密视频播放编写了一个自动答题的脚本(使用易语言 大漠插件所编写的) + +![image-20220207045652164](https://img.kuizuo.cn/20220207045652.png) + +还有商户自动话术回复的(也是易语言+大漠插件) + +![image-20220207050011838](https://img.kuizuo.cn/20220207050011.png) + +还有使用 autojs 所编写的一个针对安卓端钉钉的自动签到 + +![image-20220207045811771](https://img.kuizuo.cn/20220207045811.png) + +还有一个某宝领喵币类的,这里就不放截图了 + +甚至是一些网页类的脚本,例如油猴,Chrome 拓展之类的,都可以算作是脚本开发。 + +通常对这类代码称 RPA(机器人流程自动化),不过自从玩了网络协议后,貌似就没在怎么碰过自动化操作脚本类的东西了(协议 脱机是真香,并且效率还高,不过需要一定的逆向能力),但对于一些需要自动化的东西,就只能靠脚本了。 + +## 使用 + +pyautogui 无就是一个 python 版的针对 windows 的 API 的封装操作,而这类操作主要功能就是找到窗口,找到鼠标位置,控制鼠标点击移动,还有键盘信息输入,进行一系列流程控制来达到想要的目的。所以必然会提供相关的 API 供调用,这里有一篇文章 [PyAutoGUI 超全介绍|基于 python 的自动化控制|工作自动化](https://www.zhaoyabo.com/?p=7033#i-15) 就不做 api 的介绍了。 + +## 例子 + +就简单写一个打开微信窗口并自动寻找关键人物头像发送你好的例子,顺便来说明下编写一个自动化脚本的各个流程。 + +### 第一步:寻找窗口 + +如果要写一个自动化脚本,首先范围是一定要确认好,这样能避免不必要的区域搜索以及效率的提升,在这里例子的范围就是整个微信窗口,通过一些窗口检测工具(这里使用精易编程助手),可以得到窗口标题与窗口类名,用于定位窗口(**窗口句柄**)。 + +![image-20220218065420228](https://img.kuizuo.cn/20220218065420.png) + +可以通过如下代码获取窗口句柄 + +```python +def findWindow(): + windows = pyautogui.getWindowsWithTitle('微信') + if len(windows) == 0: + raise Exception("微信窗口未找到") + return windows[0] + + +wxWindow = findWindow() +wxWindow.activate() # 激活窗口,将窗口最前化 +``` + +### 第二步:找图点击 + +要找到对应联系人,就需要找到该联系人的相关特征,例如头像、昵称,这里就以头像作为演示。 + +既然要以头像作为特征,那么就需要提前将头像保存起来,然后利用 api 找到图片所在的坐标 + +```python +def clickAvatar(): + try: + location = pyautogui.locateOnScreen('avatar.png') + print(location) # Box(left=293, top=402, width=40, height=40) + pyautogui.click(location) + # pyautogui.click('avatar.png') # 坐标用不到的话,可使用该命令 识图+点击 + except: + print('头像未找到') +``` + +要注意的是:头像最好截取的完整(小而准),因为要完全匹配(所有像素及分辨率)。 + +### 第三步:输入内容 + +经过上面两步操作,就可以正常打开对应联系人与其聊天,现在就需要将内容输入到聊天框,然后和第二步一样找到发送按钮并点击。 + +```python +import pyperclip + + +def paste(content): + pyperclip.copy(content) + pyautogui.hotkey('ctrl', 'v') + + +content = u'你好' +paste(content) +``` + +由于我们要输入的内容是包含中文的,而在一般键盘指令是无法直接输入中文,所以需要变通一下,将所需要输入的内置剪辑版,然后使用组合键 ctrl + V 粘贴至窗口,具体代码如上演示(需要引入 pyperclip) + +然后同第二步,找到发送按钮,并点击 + +```python +def clickSend(): + pyautogui.click('send.png') +``` + +### 完整代码 + +```python +import pyperclip +import pyautogui + +pyautogui.PAUSE = 1 # 调用在执行动作后暂停的秒数,只能在执行一些pyautogui动作后才能使用,建议用time.sleep + +def findWindow(): + windows = pyautogui.getWindowsWithTitle('微信') + if len(windows) == 0: + raise Exception("微信窗口未找到") + return windows[0] + + +def clickAvatar(): + try: + location = pyautogui.locateOnScreen('avatar.png') + print(location) # Box(left=293, top=402, width=40, height=40) + pyautogui.click(location) + # pyautogui.click('avatar.png') # 坐标用不到的话,可使用该命令 + except: + print('头像未找到') + +def clickSend(): + pyautogui.click('send.png') + + +def paste(content): + pyperclip.copy(content) + pyautogui.hotkey('ctrl', 'v') + + +if __name__ == '__main__': + wxWindow = findWindow() + wxWindow.activate() # 激活窗口,将窗口最前化 + clickAvatar() + + content = '你好' + paste(content) + + clickSend() +``` + +## 演示效果 + +![wxauto](https://img.kuizuo.cn/wxauto.gif) + +## 体验感受 + +不过还有很多地方需要改进,例如多个微信窗口的情况下呢,针对窗口的操作更推荐使用 win32gui,其次在找图的时候,使用的是全屏找图,但都已经找到图片所在的区域是微信窗口的大小,可以将范围搜下,以便搜到更快。 + +上面也仅仅只是一个简单的例子,事实上自动化所需要考虑的东西挺多的,比方我当时编写的视频自动答题的,就需要定时(1 秒)监控是否弹出答题窗口,然后判断题目内容,从现有题库中获取题库。而不是像上面这个看似毫无意义,实际也确实毫无意义,但如果对其加强,比方说判断微信图标是否闪烁(有人发消息),然后对对方的聊天内容进行判断是否有关键词进行回复,事实上就能做一个简单的机器人客服聊天了(对于一些平台不支持自动回复的话,自动化脚本有显得很有用了)。不过具体使用场景还需要另行考虑,本文所展示的例子看看就行了。 + +不过整体体验下来,该说不说,比易语言好太多了,如果再让我写 window 窗口自动化操作的话,我肯定毫不犹豫的选择 python 来编写。 diff --git "a/docs/skill/reverse/android/frida/Frida Python\345\272\223\344\275\277\347\224\250.md" "b/docs/skill/reverse/android/frida/Frida Python\345\272\223\344\275\277\347\224\250.md" new file mode 100644 index 0000000..ce65885 --- /dev/null +++ "b/docs/skill/reverse/android/frida/Frida Python\345\272\223\344\275\277\347\224\250.md" @@ -0,0 +1,200 @@ +--- +id: frida-python-usage +slug: /frida-python-usage +title: Frida Python库使用 +date: 2021-02-10 +authors: kuizuo +tags: [frida, app, hook] +keywords: [frida, app, hook] +--- + + + +## 启动 Frida 服务 + +### 包名附加 + +```python +import frida, sys + +jsCode = """ ...... """ +process = frida.get_usb_device().attach('com.dodonew.online') +script = process.create_script(jsCode) +script.load() +sys.stdin.read() +``` + +### pid 附加 + +```python +process = frida.get_usb_device().attach(1234) # 1234 pid +``` + +### spawn 方式启动 + +```python +device = frida.get_usb_device() +pid = device.spawn(["com.dodonew.online"]) # 以挂起方式创建进程 +process = device.attach(pid) +script = process.create_script(jsCode) +script.load() +device.resume(pid) # 加载完脚本, 恢复进程运行 +sys.stdin.read() +``` + +### 连接非标准端口 + +```python +process = frida.get_device_manager().add_remote_device('192.168.3.68:8888').attach('com.dodonew.online') +``` + +### 连接多个设备 + +```python +process = frida.get_device_manager().add_remote_device('192.168.3.68:8888').attach('com.dodonew.online') +script = process.create_script(jsCode) +script.load() +process1 = frida.get_device_manager().add_remote_device('192.168.3.69:8888').attach('com.dodonew.online') +script1 = process.create_script(jsCode) +script1.load() +sys.stdin.read() +``` + +## frida 与 Python 的交互 + +```python {7-12,17} +# -*- coding: UTF-8 -*- +import frida, sys + +jsCode = """""" + + +def onMessage(message, data): + # print(message) + # {'type': 'send', 'payload':'some strings'} + if message["type"] == 'send': + print(u"[*] {0}".format(message['payload'])) + else: + print(message) + + +process = frida.get_usb_device().attach('com.dodonew.online') +script = process.create_script(jsCode) +script.on('message', onMessage) +script.load() +sys.stdin.read() +``` + +在 jscode 中可以使用`send(data)`,将数据传入到 onMessage 回调函数中处理。 + +### recv 与 script.post + +在 js 端中可以通过 send 向 python 发送数据,而 python 要向 js 发送数据则需要使用 script.post,js 中使用 recv 来接收,演示代码如下 + +```python {8-11,23-24} +jsCode = """ + Java.perform(function(){ + var Utils = Java.use('com.dodonew.online.util.Utils'); + Utils.md5.implementation = function(a){ + console.log('MD5 string: ', a); + var retval = this.md5(a); + send(retval); + recv(function(obj){ + console.log(JSON.stringify(obj)); + retval = obj.data; + }).wait(); + return retval; + } + }); +""" + + +def onMessage(message, data): + print(message) + if message["type"] == 'send': + print(u"[*] {0}".format(message['payload'])) + time.sleep(10) + script.post({"data": "a123456"}) + else: + print(message) +``` + +## 算法转发 + +### rpc.exports 与 script.exports + +js 端:`rpc.exports = { func: func}` + +python 端:`script.exports.func()/script.exports.FUNC()` + +注: 如果 js 导出函数中包含驼峰命名,则 python 需要将大写替换成\_小写,如 getUser => get_user + +```python + +jsCode = """ + function md5(data){ + var result = ""; + Java.perform(function(){ + result = Java.use('com.dodonew.online.util.Utils').md5(data); + }); + return result; + } + + rpc.exports = { + md5: md5 + }; +""" + + +result = script.exports.md5('a123456') +print(result) +``` + +### 使用 fastapi 搭建接口 + +```python +from fastapi import FastAPI +import uvicorn +import frida + +jsCode = """ + function enc(data){ + var result; + Java.perform(function(){ + // 主动调用难以复现的加密算法,将结果返回 + result = "a123456" + data; + }); + return result; + } + rpc.exports = { + enc: enc + }; +""" + +process = frida.get_device_manager().add_remote_device('192.168.3.68:27042').attach("com.dodonew.online") +script = process.create_script(jsCode) +script.load() + + +app = FastAPI() + +@app.get("/getEnc") +async def getEnc(username=None, password=None): + result = script.exports.enc({username: username, password: password}) + return {"result": result} + +class Item(BaseModel): + username: str = None + password: str = None + +@app.post("/getEnc") +async def getEncPost(postData: Item): + result = script.exports.enc(postData) + return {"result": result} + +if __name__ == '__main__': + uvicorn.run(app, port = 8080) + +``` + +http 发送 get 请求 如 [http://127.0.0.1:8080/getEnc?username=kuizuo&password=a123456](http://127.0.0.1:8080/getEnc?username=kuizuo&password=a123456),即可得到 enc 调用后的结果,post 请求同理 diff --git "a/docs/skill/reverse/android/frida/Frida java\345\261\202\350\207\252\345\220\220\345\212\240\345\257\206\347\256\227\346\263\225.md" "b/docs/skill/reverse/android/frida/Frida java\345\261\202\350\207\252\345\220\220\345\212\240\345\257\206\347\256\227\346\263\225.md" new file mode 100644 index 0000000..bfac592 --- /dev/null +++ "b/docs/skill/reverse/android/frida/Frida java\345\261\202\350\207\252\345\220\220\345\212\240\345\257\206\347\256\227\346\263\225.md" @@ -0,0 +1,238 @@ +--- +id: frida-java-encryption-algorithm +slug: /frida-java-encryption-algorithm +title: Frida java层自吐加密算法 +date: 2021-02-10 +authors: kuizuo +tags: [frida, app, hook] +keywords: [frida, app, hook] +--- + + + +## 代码 + +针对 java 层加密算法,能 hook 到 java 自带的加密函数库 + +```javascript +const config = { + showStacks: false, + showDivider: true, +} + +Java.perform(function () { + // console.log('frida 已启动'); + function showStacks(name = '') { + if (config.showStacks) { + console.log(Java.use('android.util.Log').getStackTraceString(Java.use('java.lang.Throwable').$new(name))) + } + } + + function showDivider(name = '') { + if (config.showDivider) { + console.log(`==============================${name}==============================`) + } + } + + function showArguments() { + console.log('arguments: ', ...arguments) + } + + const ByteString = Java.use('com.android.okhttp.okio.ByteString') + const Encode = { + toBase64(tag, data) { + console.log(tag + ' Base64: ', ByteString.of(data).base64()) + // console.log(tag + ' Base64: ', bytesToBase64(data)); + }, + toHex(tag, data) { + console.log(tag + ' Hex: ', ByteString.of(data).hex()) + // console.log(tag + ' Hex: ', bytesToHex(data)); + }, + toUtf8(tag, data) { + console.log(tag + ' Utf8: ', ByteString.of(data).utf8()) + // console.log(tag + ' Utf8: ', bytesToString(data)); + }, + toAll(tag, data) { + Encode.toUtf8(tag, data) + Encode.toHex(tag, data) + Encode.toBase64(tag, data) + }, + toResult(tag, data) { + Encode.toHex(tag, data) + Encode.toBase64(tag, data) + }, + } + + const MessageDigest = Java.use('java.security.MessageDigest') + { + let overloads_update = MessageDigest.update.overloads + for (const overload of overloads_update) { + overload.implementation = function () { + const algorithm = this.getAlgorithm() + showDivider(algorithm) + showStacks(algorithm) + Encode.toAll(`${algorithm} update data`, arguments[0]) + return this.update(...arguments) + } + } + + let overloads_digest = MessageDigest.digest.overloads + for (const overload of overloads_digest) { + overload.implementation = function () { + const algorithm = this.getAlgorithm() + showDivider(algorithm) + showStacks(algorithm) + const result = this.digest(...arguments) + if (arguments.length === 1) { + Encode.toAll(`${algorithm} update data`, arguments[0]) + } else if (arguments.length === 3) { + Encode.toAll(`${algorithm} update data`, arguments[0]) + } + + Encode.toResult(`${algorithm} digest result`, result) + return result + } + } + } + + const Mac = Java.use('javax.crypto.Mac') + { + Mac.init.overload('java.security.Key', 'java.security.spec.AlgorithmParameterSpec').implementation = function (key, AlgorithmParameterSpec) { + return this.init(key, AlgorithmParameterSpec) + } + Mac.init.overload('java.security.Key').implementation = function (key) { + const algorithm = this.getAlgorithm() + showDivider(algorithm) + showStacks(algorithm) + const keyBytes = key.getEncoded() + Encode.toAll(`${algorithm} init Key`, keyBytes) + return this.init(...arguments) + } + + // let overloads_update = Mac.update.overloads; + // for (const overload of overloads_update) { + // overload.implementation = function () { + // const algorithm = this.getAlgorithm(); + // showDivider(algorithm); + // showStacks(algorithm); + // Encode.toAll(`${algorithm} update data`, arguments[0]); + // return this.update(...arguments); + // }; + // } + + let overloads_doFinal = Mac.doFinal.overloads + for (const overload of overloads_doFinal) { + overload.implementation = function () { + const algorithm = this.getAlgorithm() + showDivider(algorithm) + showStacks(algorithm) + const result = this.doFinal(...arguments) + if (arguments.length === 1) { + Encode.toAll(`${algorithm} update data`, arguments[0]) + } else if (arguments.length === 3) { + Encode.toAll(`${algorithm} update data`, arguments[0]) + } + + Encode.toResult(`${algorithm} doFinal result`, result) + return result + } + } + } + + const Cipher = Java.use('javax.crypto.Cipher') + { + let overloads_init = Cipher.init.overloads + for (const overload of overloads_init) { + overload.implementation = function () { + const algorithm = this.getAlgorithm() + showDivider(algorithm) + showStacks(algorithm) + + if (arguments[0]) { + const mode = arguments[0] + console.log(`${algorithm} init mode`, mode) + } + + if (arguments[1]) { + const className = JSON.stringify(arguments[1]) + // 安卓10以上私钥是有可能输出不了的 + if (className.includes('OpenSSLRSAPrivateKey')) { + // const keyBytes = arguments[1]; + // console.log(`${algorithm} init key`, keyBytes); + } else { + const keyBytes = arguments[1].getEncoded() + Encode.toAll(`${algorithm} init key`, keyBytes) + } + } + + if (arguments[2]) { + const className = JSON.stringify(arguments[2]) + if (className.includes('javax.crypto.spec.IvParameterSpec')) { + const iv = Java.cast(arguments[2], Java.use('javax.crypto.spec.IvParameterSpec')) + const ivBytes = iv.getIV() + Encode.toAll(`${algorithm} init iv`, ivBytes) + } else if (className.includes('java.security.SecureRandom')) { + } + } + + return this.init(...arguments) + } + } + + // let overloads_update = Cipher.update.overloads; + // for (const overload of overloads_update) { + // overload.implementation = function () { + // const algorithm = this.getAlgorithm(); + // showDivider(algorithm); + // showStacks(algorithm); + // Encode.toAll(`${algorithm} update data`, arguments[0]); + // return this.update(...arguments); + // }; + // } + + let overloads_doFinal = Cipher.doFinal.overloads + for (const overload of overloads_doFinal) { + overload.implementation = function () { + const algorithm = this.getAlgorithm() + showDivider(algorithm) + showStacks(algorithm) + const result = this.doFinal(...arguments) + if (arguments.length === 1) { + Encode.toAll(`${algorithm} update data`, arguments[0]) + } else if (arguments.length === 3) { + Encode.toAll(`${algorithm} update data`, arguments[0]) + } + + Encode.toResult(`${algorithm} doFinal result`, result) + return result + } + } + } + + const Signature = Java.use('java.security.Signature') + { + let overloads_update = Signature.update.overloads + for (const overload of overloads_update) { + overload.implementation = function () { + const algorithm = this.getAlgorithm() + showDivider(algorithm) + showStacks(algorithm) + Encode.toAll(`${algorithm} update data`, arguments[0]) + return this.update(...arguments) + } + } + + let overloads_sign = Signature.sign.overloads + for (const overload of overloads_sign) { + overload.implementation = function () { + const algorithm = this.getAlgorithm() + showDivider(algorithm) + showStacks(algorithm) + const result = this.sign() + Encode.toResult(`${algorithm} sign result`, result) + return this.sign(...arguments) + } + } + } +}) +``` diff --git "a/docs/skill/reverse/android/frida/Frida so\345\261\202\344\270\255\347\232\204hook.md" "b/docs/skill/reverse/android/frida/Frida so\345\261\202\344\270\255\347\232\204hook.md" new file mode 100644 index 0000000..8c591ef --- /dev/null +++ "b/docs/skill/reverse/android/frida/Frida so\345\261\202\344\270\255\347\232\204hook.md" @@ -0,0 +1,1071 @@ +--- +id: frida-so-hook +slug: /frida-so-hook +title: Frida so层中的hook +date: 2021-02-10 +authors: kuizuo +tags: [frida, app, hook] +keywords: [frida, app, hook] +--- + + + +## 前言 + +so 中会接触到的东西:系统库函数、加密算法、jni 调用、系统调用、自定义算法 + +## 如何 hook + +so hook 只需要得到一个地址,有**函数地址就能 hook 与主动调用**,与 java 层的 hook 一致。 + +### 得到函数地址的方式 + +1. 通过 frida 提供的 api 来得到,该函数必须有符号的才可以 +2. 通过计算得到地址:so 基址+函数在 so 中的偏移[+1] + +### 演示代码如下 + +```javascript +const moduleName = 'libnative-lib.so' +let baseAddr = Module.findBaseAddress(moduleName) +let sub_99C0 = baseAddr.add(0x99c0 + 1) +Interceptor.attach(funcPtr, { + onEnter: function (args) { + // ... + }, + onLeave: function (retval) { + // ... + }, +}) +``` + +## API + +### 枚举导入表 + +```javascript +const improts = Module.enumerateImports('libencryptlib.so') +for (const iterator of improts) { + console.log(JSON.stringify(iterator)) + // {"type":"function","name":"__cxa_atexit","module":"/apex/com.android.runtime/lib64/bionic/libc.so","address":"0x778957bd34"} +} +``` + +### 枚举导出表 + +```javascript +const exports = Module.enumerateExports('libencryptlib.so') +for (const iterator of exports) { + console.log(JSON.stringify(iterator)) + // {"type":"letiable","name":"_ZTSx","address":"0x74d594b1c0"} +} +``` + +### 枚举符号表 + +```javascript +const symbols = Module.enumerateSymbols('libencryptlib.so') +for (const iterator of symbols) { + console.log(JSON.stringify(iterator)) + // {"isGlobal":true,"type":"function","name":"pthread_getspecific","address":"0x0","size":0 +} +``` + +### 枚举进程中已加载的模块 + +```javascript +const modules = Process.enumerateModules() +console.log(JSON.stringify(modules[0].enumerateExports()[0])) +``` + +### findExportByName + +注: **函数名以汇编中出现的为准** + +```javascript +const funcAddr = Module.findExportByName('libencryptlib.so', '_ZN7MD5_CTX11MakePassMD5EPhjS0_') +// 返回的是函数地址 第二个参数根据汇编中为准 +console.log(funcAddr) + +// 通过Interceptor.attach来对函数进行hook +Interceptor.attach(funcAddr, { + onEnter: function (args) { + console.log('args[1]: ', hexdump(args[1])) // 打印参数的地址 通过hexdump打印16进制 + console.log(this.context.x1) // 打印寄存器内容 + console.log('args[2]: ', args[2].toInt32()) // 默认显示16进制,这里转为10进制 + this.args3 = args[3] // 将args[3]值保存到this上 + }, + onLeave: function (retval) { + console.log('args[3]: ', hexdump(this.args3)) + }, +}) +``` + +### 模块基址获取方式 + +如果在导入表、导出表、符号表里找不到的函数,那么函数地址需要自己计算 + +计算公式:**so 基址+函数在 so 中的偏移[+1]** + +| 安卓位数 | 指令 | 计算方式 | +| -------- | ----- | -------------------------------- | +| 32 位 | thumb | so 基址 + 函数在 so 中的偏移 + 1 | +| 64 位 | arm | so 基址 + 函数在 so 中的偏移 | + +也可通过显示汇编指令对应的 opcode bytes,来判断 + +IDA -> Options -> General -> Number of opcode bytes (non-graph) 改为 4 + +![image-20220206042920297](https://img.kuizuo.cn/20220206042927.png) + +arm 指令为 4 个字节,如果函数中有些指令是两个字节,那么函数地址计算需要 + 1 + +**不清楚的话,+1 和不+1 都试一遍即可** + +所以获取基址就显得尤为重要 + +#### Process.findModuleByName + +通过模块名找到模块 + +```javascript +const module = Process.findModuleByName('libencryptlib.so') +console.log(JSON.stringify(module)) +// {"name":"libencryptlib.so","base":"0x74d5934000","size":303104,"path":"/data/app/~~Nzn4SQ_RZn1-PYH7TbX7Ig==/com.pocket.snh48.activity-Muxx7c_dtplxjFPY2SGF0A==/lib/arm64/libencryptlib.so"} +// base为基址 +``` + +#### Process.getModuleByName + +同 findModuleByName + +#### Module.findBaseAddress()(常用) + +直接获得模块基址 + +```javascript +const baseAddr = Module.findBaseAddress('libencryptlib.so') +console.log(baseAddr) +// 0x74d5934000 +``` + +#### Process.findModuleByAddress(address) + +通过基址来找到模块 + +#### Process.getModuleByAddress(address) + +同 findModuleByAddress + +#### 测试 hook 任意函数 + +```javascript +const baseAddr = Module.findBaseAddress('libencryptlib.so') +// const so = 0x77ab999000; +// console.log(ptr(so).add(0x1FA38)); // ptr 是 new NativePointer()的简写 +const funcAddr = baseAddr.add(0x1fa38) // 0x1FA38 是IDA中函数定义的地址 +Interceptor.attach(funcAddr, {}) +``` + +#### 打印参数 + +```javascript +function print_arg(addr) { + const module = Process.findRangeByAddress(addr) + // 判断传入的参数是否为地址 + if (module !== null) return hexdump(addr) + '\n' + return ptr(addr) + '\n' +} +``` + +#### 参数的方法 + +```javascript +// args[0] 是一个内存地址 +hexdump(args[0]) // 打印参数的所在内存区域的字节数据 +args[0].readCString() // 读取参数所对应的C字符串 (前提: 参数是一个可见字符串) +args[0].readPointer() // 用读地址方式去读取参数所对应的值 (如果参数是一个指针的话可能就需要使用) +``` + +### 修改函数数值参数和返回值 + +#### 修改数值 + +```javascript +Interceptor.attach(helloAddr, { + onEnter: function (args) { + args[2] = ptr(1000) // 直接将第三个参数修改为1000 + console.log(args[2].toInt32()) + }, + onLeave: function (retval) { + retval.replace(20000) // 通过replace 修改成20000 + console.log('retval', retval.toInt32()) + }, +}) +``` + +### 修改字符串 + +hex 与 string 转化封装函数(中文无法转化) + +```javascript +function stringToBytes(str) { + return hexToBytes(stringToHex(str)) +} + +function stringToHex(str) { + return str + .split('') + .map(function (c) { + return ('0' + c.charCodeAt(0).toString(16)).slice(-2) + }) + .join('') +} + +function hexToBytes(hex) { + for (let bytes = [], c = 0; c < hex.length; c += 2) bytes.push(parseInt(hex.substr(c, 2), 16)) + return bytes +} + +function hexToString(hexStr) { + let hex = hexStr.toString() + let str = '' + for (let i = 0; i < hex.length; i += 2) str += String.fromCharCode(parseInt(hex.substr(i, 2), 16)) + return str +} +``` + +#### 将指向的字符串修改成新的字符串(新字符串不宜超过原有字符串长度) + +```javascript +Interceptor.attach(funcAddr, { + onEnter: function (args) { + let newStr = 'some strings' + // 需要写入字节数组的方式来写入字符串 + args[1].writeByteArray(hexToBytes(stringToHex(newStr) + '00')) // c语言字符串结尾为0字节 + console.log(hexdump(args[1])) + args[2] = ptr(newStr.length) + console.log(args[2].toInt32()) + }, + onLeave: function (retval) {}, +}) +``` + +:::danger + +有缺陷,如果字符串长度大于原字符串长度,有可能导致内存中其他区域被修改,导致不可预知的 BUG + +::: + +#### 将 so 层中已有的字符串传给函数(字符串地址替换) + +```javascript +Interceptor.attach(funcAddr, { + onEnter: function (args) { + args[1] = baseAddr.add(0x38a1) // 0x38a1 为IDA中所对应的字符串地址 + console.log(hexdump(args[1])) + args[2] = ptr(baseAddr.add(0x38a1).readCString().length) // 读取字符串长度 + console.log(args[2].toInt32()) + }, + onLeave: function (retval) {}, +}) +``` + +#### 替换函数(建议使用) + +```javascript +cosnt newStr = "some strings"; +cosnt newStrAddr = Memory.allocUtf8String(newStr); // 使用Frida的Memory来申请内存区域 返回的是一个指针 + +Interceptor.attach(funcAddr, { + onEnter: function (args) { + // cosnt newStrAddr = Memory.allocUtf8String(newStr); // 如果在这里申请的话,到onLeave将会回收 可以在全局定义或使用this.newStrAddr 附加到自身 + args[1] = newStrAddr + console.log(hexdump(args[1])) + args[2] = ptr(newStr.length) + console.log(args[2].toInt32()) + }, + onLeave: function (retval) {}, +}) + + +``` + +### 内存读写 + +```javascript +// 1. 读取指定地址的字符串 +let baseAddr = Module.findBaseAddress('libxiaojianbang.so') +console.log(baseAddr.add(0x2c00).readCString()) + +// 2. dump指定地址的内存 +console.log(hexdump(baseAddr.add(0x2c00))) + +// 3. 读指定地址的内存 +console.log(baseAddr.add(0x2c00).readByteArray(16)) +console.log(Memory.readByteArray(baseAddr.add(0x2c00), 16)) //原先的API + +// 4. 写指定地址的内存 +baseAddr.add(0x2c00).writeByteArray(stringToBytes('xiaojianbang')) +console.log(hexdump(baseAddr.add(0x2c00))) + +// 5. 申请新内存写入 +Memory.alloc() +Memory.allocUtf8String() + +// 6. 修改内存权限 +Memory.protect(ptr(libso.base), libso.size, 'rwx') +``` + +### 修改 so 函数代码(需了解 ARM 汇编相关知识) + +```javascript +// 1. 修改地址对应的指令 +let baseAddr = Module.findBaseAddress("libxiaojianbang.so"); +baseAddr.add(0x1684).writeByteArray(hexToBytes("0001094B")); +ARM与Hex在线转换 https://armconverter.com/ + +// 2. 将对应地址的指令解析成汇编 +let ins = Instruction.parse(baseAddr.add(0x1684)); +console.log(ins.toString()); + +// 3. 利用frida提供的api来写汇编代码 +new Arm64Writer(baseAddr.add(0x167C)).putNop(); +console.log(Instruction.parse(baseAddr.add(0x167C)).toString()); + +// 4. 利用frida提供的api来写汇编代码 +let codeAddr = baseAddr.add(0x167C); +Memory.patchCode(codeAddr, 8, function (code) { + let Writer = new Arm64Writer(code, {pc: codeAddr}); + Writer.putBytes(hexToBytes("0001094B")); + Writer.putBytes(hexToBytes("FF830091")); + Writer.putRet(); + Writer.flush(); +}); +``` + +### 主动调用任意函数 + +1. 声明函数指针 + + 文档:https://frida.re/docs/javascript-api/#NativeFunction 语法:`new NativeFunction(address, returnType, argTypes[, abi])` + +2. 支持的 returnType 和 argTypes + + void、pointer、int、uint、long、ulong、char、uchar、float、double int8、uint8、int16、uint16、int32、uint32、int64、uint64、bool size_t、ssize_t + +3. 代码示例 + + ```javascript + Java.perform(function () { + //拿到函数地址 + let funcAddr = Module.findBaseAddress('libxiaojianbang.so').add(0x23f4) + //声明函数指针 + let func = new NativeFunction(funcAddr, 'pointer', ['pointer', 'pointer']) + let env = Java.vm.tryGetEnv() // 获取JNIEnv + console.log('env: ', JSON.stringify(env)) + // {"handle":"0xb400007911df2c10","vm":{"handle":"0xb400007921d5f710"}} + if (env != null) { + // 创建java字符串 (jstr是一个地址) + let jstr = env.newStringUtf('some strings') + let cstr = func(env, jstr) + console.log(cstr.readCString()) + console.log(hexdump(cstr)) + } + }) + ``` + +### hook libc.so 读写文件 + +```javascript +// 找到C中操作文件的api +let fopenAddr = Module.findExportByName('libc.so', 'fopen') +let fputsAddr = Module.findExportByName('libc.so', 'fputs') +let fcloseAddr = Module.findExportByName('libc.so', 'fclose') +console.log(fopenAddr, fputsAddr, fcloseAddr) + +let fopen = new NativeFunction(fopenAddr, 'pointer', ['pointer', 'pointer']) +let fputs = new NativeFunction(fputsAddr, 'int', ['pointer', 'pointer']) +let fclose = new NativeFunction(fcloseAddr, 'int', ['pointer']) + +// 需要申请内存地址 (由于需要传入指针) +let fileName = Memory.allocUtf8String('/data/data/com.xiaojianbang.app/xiaojianbang.txt') +let openMode = Memory.allocUtf8String('w') +let data = Memory.allocUtf8String('QQ24358757\n') + +let file = fopen(fileName, openMode) +console.log(file) +fputs(data, file) +fclose(file) +``` + +### hook jni 函数 + +libart.so 存放 jni 函数 + +**jni 文档可在 jni.h 头文件中查看** + +安卓 10 以下 `/system/lib` 或 `/system/lib64` + +安卓 10 以后 `/system/apex/com.android.runtime.release/lib64/libart.so` + +例如 hook env->NewStringUTF()方法 + +```javascript +// 找到 env->NewStringUTF(a1, str) 函数 +function findNewStringUtfAddr() { + let artSym = Module.enumerateSymbols('libart.so') + for (const sym of artSym) { + if (!sym.name.includes('CheckJNI') && sym.name.includes('NewStringUTF')) { + // console.log(JSON.stringify(sym)); + return sym.address + } + } + return null +} + +function hookNewStringUTF() { + const NewStringUTFAddr = findNewStringUtfAddr() + // console.log('NewStringUTFAddr', NewStringUTFAddr); + if (NewStringUTFAddr !== null) { + Interceptor.attach(NewStringUTFAddr, { + onEnter: function (args) { + console.log(args[1].readCString()) + }, + onLeave: function (retval) {}, + }) + } +} +hookNewStringUTF() +``` + +计算地址方式(了解) + +```javascript +Java.perform(function () { + console.log('Process.arch: ', Process.arch) + let envAddr = ptr(Java.vm.tryGetEnv().handle).readPointer() + // 获取到的是JNINativeInterface 结构体 + + // 0x538 是结构体偏移的指针 需要计算 + let newStringUtfAddr = envAddr.add(0x538).readPointer() + console.log('newStringUtfAddr', newStringUtfAddr) + if (newStringUtfAddr != null) { + Interceptor.attach(newStringUtfAddr, { + onEnter: function (args) { + console.log(args[1].readCString()) + }, + onLeave: function (retval) {}, + }) + } +}) +``` + +### 主动调用 JNI 函数 + +#### 使用 frida 封装的函数来调用 jni + +```javascript +let funcAddr = Module.findExportByName('libxiaojianbang.so', 'helloFromC') +console.log(funcAddr) +if (funcAddr != null) { + Interceptor.attach(funcAddr, { + onEnter: function (args) {}, + onLeave: function (retval) { + let env = Java.vm.tryGetEnv() + let jstr = env.newStringUtf('bbs.125.la') //主动调用jni函数 cstr转jstr + retval.replace(jstr) + + let cstr = env.getStringUtfChars(jstr) //主动调用 jstr转cstr + console.log(cstr.readCString()) + console.log(hexdump(cstr)) + }, + }) +} +``` + +#### NativeFunction 方式主动调用 + +```javascript +let symbols = Process.getModuleByName('libart.so').enumerateSymbols() +let newStringUtf = null +for (let i = 0; i < symbols.length; i++) { + let symbol = symbols[i] + if (symbol.name.indexOf('CheckJNI') == -1 && symbol.name.indexOf('NewStringUTF') != -1) { + console.log(symbol.name, symbol.address) + newStringUtf = symbol.address + } +} +let newStringUtf_func = new NativeFunction(newStringUtf, 'pointer', ['pointer', 'pointer']) +let jstring = newStringUtf_func(Java.vm.tryGetEnv().handle, Memory.allocUtf8String('xiaojianbang')) +console.log(jstring) + +let envAddr = Java.vm.tryGetEnv().handle.readPointer() +let GetStringUTFChars = envAddr.add(0x548).readPointer() +let GetStringUTFChars_func = new NativeFunction(GetStringUTFChars, 'pointer', [ + 'pointer', + 'pointer', + 'pointer', +]) +let cstr = GetStringUTFChars_func(Java.vm.tryGetEnv().handle, jstring, ptr(0)) +console.log(cstr.readCString()) +``` + +### 打印函数调用堆栈 + +```javascript +console.log( + Thread.backtrace(this.context, Backtracer.FUZZY).map(DebugSymbol.fromAddress).join('\n') + '\n', +) +``` + +### frida trace + IDA 插件 trace-natives 打印函数调用流程 + +github 地址: https://github.com/Pr0214/trace_natives + +IDA -> Edit -> Plugins -> traceNatives,将会对当前 so 文件中所有函数进行 hook + +使用 + +```bash +frida-trace -UF -O C:\Users\zeyu\Desktop\libmfw_1644263290.txt +``` + +会生成 `__handlers__/libdemo.so`的文件夹,里面存放对所有函数的 hook 脚本 + +结果如下 + +```bash + /* TID 0x4da4 */ + 11249 ms sub_1e3c() + 11250 ms | sub_15fc() + 11250 ms | | sub_1794() + 11250 ms | | sub_17cc() + 11250 ms | | sub_1804() + 11250 ms | | sub_184c() + 11255 ms | | sub_194c() + 11255 ms | | sub_1984() + 11255 ms | | sub_19c4() + 11255 ms | sub_2140() + 11255 ms | sub_21b0() + 11255 ms | sub_3988() + 11255 ms | | sub_3a84() + 11255 ms | | sub_21b0() + 11255 ms | | sub_21b0() + 11255 ms | | | sub_2428() + 11255 ms | | | | sub_3bc0() + 11255 ms | | sub_3a84() + 11255 ms | sub_2004() +``` + +### 确认 native 函数在哪个 so + +静态分析查看静态代码块中加载的 so,但并不靠谱,因为 native 函数声明在一个类中,so 加载可以在其他的类中此外还可以在另外的类中,一次性加载所有的 so + +hook 系统函数来得到绑定的 native 函数地址,然后再得到 so 地址 + +| 注册方式 | hook 点 | +| ---------------- | -------------------- | +| jni 函数动态注册 | hook RegisterNatives | +| jni 函数静态注册 | hook dlsym | + +#### hook_RegisterNatives + +```javascript +function hook_RegisterNatives() { + let RegisterNatives_addr = null + let symbols = Process.findModuleByName('libart.so').enumerateSymbols() + for (let i = 0; i < symbols.length; i++) { + let symbol = symbols[i].name + if (symbol.indexOf('CheckJNI') == -1 && symbol.indexOf('JNI') >= 0) { + if (symbol.indexOf('RegisterNatives') >= 0) { + RegisterNatives_addr = symbols[i].address + console.log('RegisterNatives_addr: ', RegisterNatives_addr) + } + } + } + Interceptor.attach(RegisterNatives_addr, { + onEnter: function (args) { + let env = args[0] + let jclass = args[1] + let class_name = Java.vm.tryGetEnv().getClassName(jclass) + let methods_ptr = ptr(args[2]) + let method_count = args[3].toInt32() + console.log('RegisterNatives method counts: ', method_count) + for (let i = 0; i < method_count; i++) { + let name = methods_ptr + .add(i * Process.pointerSize * 3) + .readPointer() + .readCString() + let sig = methods_ptr + .add(i * Process.pointerSize * 3 + Process.pointerSize) + .readPointer() + .readCString() + let fnPtr_ptr = methods_ptr + .add(i * Process.pointerSize * 3 + Process.pointerSize * 2) + .readPointer() + let find_module = Process.findModuleByAddress(fnPtr_ptr) + console.log( + 'RegisterNatives java_class: ', + class_name, + 'name: ', + name, + 'sig: ', + sig, + 'fnPtr: ', + fnPtr_ptr, + 'module_name: ', + find_module.name, + 'module_base: ', + find_module.base, + 'offset: ', + ptr(fnPtr_ptr).sub(find_module.base), + ) + } + }, + onLeave: function (retval) {}, + }) +} +``` + +#### hook_dlsym + +```javascript +function hook_dlsym() { + let dlsymAddr = Module.findExportByName('libdl.so', 'dlsym') + console.log(dlsymAddr) + Interceptor.attach(dlsymAddr, { + onEnter: function (args) { + this.args1 = args[1] + }, + onLeave: function (retval) { + let module = Process.findModuleByAddress(retval) + if (module == null) return + console.log(this.args1.readCString(), module.name, retval, retval.sub(module.base)) + }, + }) +} +``` + +### inlineHook(针对寄存器的值) + +```javascript +function inlineHook() { + // var nativePointer = Module.findBaseAddress("libxiaojianbang.so"); + // var hookAddr = nativePointer.add(0x17BC); + // Interceptor.attach(hookAddr, { + // onEnter: function (args) { + // console.log("onEnter: ", this.context.x8); + // }, onLeave: function (retval) { + // console.log("onLeave: ", this.context.x8.toInt32()); + // console.log(this.context.x8 & 7); + // } + // }); + + var nativePointer = Module.findBaseAddress('libxiaojianbang.so') + var hookAddr = nativePointer.add(0x1b70) + Interceptor.attach(hookAddr, { + onEnter: function (args) { + console.log('onEnter: ', this.context.x1) + console.log('onEnter: ', hexdump(this.context.x1)) + }, + onLeave: function (retval) {}, + }) +} +``` + +### hook_dlopen + +有些函数在 so 首次加载的时候执行,而 so 没加载之前又不能去 hook,那么要 hook 这些函数,就必须监控 so 何时被加载,因此,需要 hook dlopen 等系统函数,当 so 加载完毕,立刻 hook + +```javascript +//hook_dlopen +function hook_dlopen(addr, soName, callback) { + Interceptor.attach(addr, { + onEnter: function (args) { + let soPath = args[0].readCString() + if (soPath.indexOf(soName) != -1) this.hook = true + }, + onLeave: function (retval) { + if (this.hook) callback() + }, + }) +} + +function hook_func() { + let baseAddr = Module.findBaseAddress('libxiaojianbang.so') + console.log('baseAddr', baseAddr) + let MD5Final = baseAddr.add(0x3540) + Interceptor.attach(MD5Final, { + onEnter: function (args) { + this.args1 = args[1] + }, + onLeave: function (retval) { + console.log(hexdump(this.args1)) + }, + }) +} + +let dlopen = Module.findExportByName('libdl.so', 'dlopen') // 低版本安卓系统 +let android_dlopen_ext = Module.findExportByName('libdl.so', 'android_dlopen_ext') // 高版本安卓系统 +//console.log(JSON.stringify(Process.getModuleByAddress(dlopen))); +hook_dlopen(dlopen, 'libxiaojianbang.so', hook_func) +hook_dlopen(android_dlopen_ext, 'libxiaojianbang.so', hook_func) +``` + +### hook_initarray + +```javascript +function main() { + function hook_dlopen(addr, soName, callback) { + Interceptor.attach(addr, { + onEnter: function (args) { + var soPath = args[0].readCString() + if (soPath.indexOf(soName) != -1) hook_call_constructors() + }, + onLeave: function (retval) {}, + }) + } + var dlopen = Module.findExportByName('libdl.so', 'dlopen') + var android_dlopen_ext = Module.findExportByName('libdl.so', 'android_dlopen_ext') + hook_dlopen(dlopen, 'libxiaojianbang.so', inlineHook) + hook_dlopen(android_dlopen_ext, 'libxiaojianbang.so', inlineHook) + + var isHooked = false + function hook_call_constructors() { + var symbols = Process.getModuleByName('linker64').enumerateSymbols() + var call_constructors_addr = null + for (let i = 0; i < symbols.length; i++) { + var symbol = symbols[i] + // initarray 在__dl__ZN6soinfo17call_constructorsEv中被调用的 + if (symbol.name.indexOf('__dl__ZN6soinfo17call_constructorsEv') != -1) { + call_constructors_addr = symbol.address + } + } + console.log('call_constructors_addr: ', call_constructors_addr) + Interceptor.attach(call_constructors_addr, { + onEnter: function (args) { + if (!isHooked) { + hook_initarray() + isHooked = true + } + }, + onLeave: function (retval) {}, + }) + } + + function hook_initarray() { + var xiaojianbangAddr = Module.findBaseAddress('libxiaojianbang.so') + var func1_addr = xiaojianbangAddr.add(0x1c54) + var func2_addr = xiaojianbangAddr.add(0x1c7c) + + Interceptor.replace( + func1_addr, + new NativeCallback( + function () { + console.log('func1 is replaced!!!') + }, + 'void', + [], + ), + ) + + Interceptor.replace( + func2_addr, + new NativeCallback( + function () { + console.log('func2 is replaced!!!') + }, + 'void', + [], + ), + ) + } +} +main() +``` + +### hook_JNIOnload + +```javascript +hook_dlopen(dlopen, 'libxiaojianbang.so', hook_JNIOnload) +hook_dlopen(android_dlopen_ext, 'libxiaojianbang.so', hook_JNIOnload) + +function hook_JNIOnload() { + var xiaojianbangAddr = Module.findBaseAddress('libxiaojianbang.so') + // 0x1CCC JNIOnload的地址 + var funcAddr = xiaojianbangAddr.add(0x1ccc) + Interceptor.replace( + funcAddr, + new NativeCallback( + function () { + console.log('this func is replaced !') + }, + 'void', + [], + ), + ) +} +``` + +### hook_pthread_create + +创建子线程的相关函数 + +```javascript +function hook_pthread_create() { + var pthread_create_addr = Module.findExportByName('libc.so', 'pthread_create') + console.log('pthread_create_addr: ', pthread_create_addr) + Interceptor.attach(pthread_create_addr, { + onEnter: function (args) { + console.log(args[0], args[1], args[2], args[3]) + var Module = Process.findModuleByAddress(args[2]) + if (Module != null) console.log(Module.name, args[2].sub(Module.base)) + }, + onLeave: function (retval) {}, + }) +} +hook_pthread_create() +``` + +## 封装 so 中常用 hook 函数 + +```javascript +function showStacks() { + console.log( + Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + + '\n', + ) +} + +function findJNIFunc(func) { + if (!func) return null + let artSym = Module.enumerateSymbols('libart.so') + for (const sym of artSym) { + if (!sym.name.includes('CheckJNI') && sym.name.includes(func)) { + // console.log(JSON.stringify(sym)); + return sym.address + } + } + return null +} + +function hookNewStringUTF() { + // 找到 env->NewStringUTF(a1, str) 函数 + const NewStringUTFAddr = findJNIFunc('NewStringUTF') + // console.log('NewStringUTFAddr', NewStringUTFAddr); + if (NewStringUTFAddr !== null) { + Interceptor.attach(NewStringUTFAddr, { + onEnter: function (args) { + showStacks.call(this) + console.log(args[1].readCString()) + }, + onLeave: function (retval) {}, + }) + } +} + +function hookNewByteArray() { + const NewByteArrayAddr = findJNIFunc('NewByteArray') + console.log('NewByteArrayAddr', NewByteArrayAddr) + if (NewByteArrayAddr !== null) { + Interceptor.attach(NewByteArrayAddr, { + onEnter: function (args) { + showStacks.call(this) + console.log(args[1].toInt32()) + }, + onLeave: function (retval) { + // retval 返回的是一个java对象,不可直接读取,需要将其转为c中的指针 + // 得到是一个 NativePointer + // let retPointer = Java.vm.tryGetEnv().getByteArrayElements(retval); + // console.log(retPointer.readByteArray(32)); + }, + }) + } +} + +function print_arg(addr) { + var module = Process.findRangeByAddress(addr) + if (module != null) return '\n' + hexdump(addr) + return ptr(addr) +} + +function hook_native_addr(funcPtr, params = [], result = {}) { + var module = Process.findModuleByAddress(funcPtr) + Interceptor.attach(funcPtr, { + onEnter: function (args) { + this.logs = [] + this.args = [] + this.logs.push('call ' + module.name + '!' + ptr(funcPtr).sub(module.base)) + for (let i = 0; i < params.length; i++) { + let param = params[i] + this.args.push(args[i]) + if (param.type) { + this.logs.push(`a${i + 1} onEnter:` + args[i][param.type]()) + } else { + this.logs.push(`a${i + 1} onEnter:` + print_arg(args[i])) + } + } + }, + onLeave: function (retval) { + for (let i = 0; i < params.length; i++) { + let param = params[i] + if (param.type) { + this.logs.push(`a${i + 1} onLeave:` + this.args[i][param.type]()) + } else { + this.logs.push(`a${i + 1} onLeave:` + print_arg(this.args[i])) + } + } + if (result.type) { + this.logs.push('retval onLeave: ' + retval[result.type]()) + } else { + this.logs.push('retval onLeave: ' + print_arg(retval)) + } + console.log(this.logs.join('\n')) + }, + }) +} + +// ================================================================================ + +// hookNewStringUTF() // 用于定位NewStringUTF +// hookNewByteArray(); // 用于定位NewByteArray + +const moduleName = 'libnative-lib.so' +let baseAddr = Module.findBaseAddress(moduleName) + +// let sub_1234 = baseAddr.add(0x1234 + 1); +// hook_native_addr(sub_1234, Array(3).fill({})); +``` + +## JNItrace + +so 中会应用很多的 jni 函数,比如:Java 的字符串到 C,需要先使用 GetStringUtfChars 来转成 C 语言字符串。而加密后的结果,如果要转成 jstring,又需要用到 NewStringUtf,所以可以通过 hook 这些 jni 函数,来可以定位关键代码,也可以大体上了解函数的代码逻辑。 + +**jnitrace 就是 hook 一系列的 jni 函数** + +github 地址:https://github.com/chame1eon/jnitrace + +版本: jnitrace-3.3.0 + +### 安装(进入到 frida 环境) + +```bash +pip install jnitrace +``` + +### 使用 + +```bash +jnitrace -m attach -l <模块.so> <包名> +``` + +`-m ` 附加方式去运行 + +`-o path/output.json` 将结果输出到文件上 + +## ollvm 字符串解密 + +找到加密的字符串地址(基址+变量偏移地址),通过 hexdump 可以直接打印出内存中解密后的状态 + +使用 JNItrace,但是前提只能查看 jni 相关函数 + +从内存中 dump 整个 so,获取所有解密后的字符串,但是需要修复 + +分析 so 中字符串解密函数,然后还原(同 js 混淆解密函数) + +### dump_so.js + +```javascript +function dump_so(so_name) { + Java.perform(function () { + let currentApplication = Java.use('android.app.ActivityThread').currentApplication() + let dir = currentApplication.getApplicationContext().getFilesDir().getPath() + let libso = Process.getModuleByName(so_name) + console.log('[name]:', libso.name) + console.log('[base]:', libso.base) + console.log('[size]:', ptr(libso.size)) + console.log('[path]:', libso.path) + let file_path = dir + '/' + libso.name + '_' + libso.base + '_' + ptr(libso.size) + '.so' + let file_handle = new File(file_path, 'wb') + if (file_handle && file_handle != null) { + Memory.protect(ptr(libso.base), libso.size, 'rwx') + let libso_buffer = ptr(libso.base).readByteArray(libso.size) + file_handle.write(libso_buffer) + file_handle.flush() + file_handle.close() + console.log('[dump]:', file_path) + } + }) +} +``` + +使用 dump_so(so_name)将保存的文件拉去到桌面上(需要先从私有目录移动到权限大的目录下再移动到桌面)。 + +此时的 so 文件直接通过 IDA 打开会报错,需要修复,使用的工具是[SoFixer](https://github.com/F8LEFT/SoFixer)。 + +### SoFixer + +github 地址:[F8LEFT/SoFixer (github.com)](https://github.com/F8LEFT/SoFixer) + +使用方式详看 README + +注: 修复后的 so 文件无法重新打包动态分析,只可静态分析使用 + +## Frida 检测 + +[翻译多种特征检测 Frida-外文翻译-看雪论坛-安全社区|安全招聘|bbs.pediy.com](https://bbs.pediy.com/thread-217482.htm) + +#### ptrace 占坑 + +ptrace(0, 0 ,0 ,0); 开启一个子进程附加父进程,通常有一下几种 + +- 守护进程 +- 子进程附加父进程 目的是不让别人附加 +- 普通的多进程 + +就只好使用 frida -f 包名 spawn 方式启动 + +#### 进程名检测 + +遍历运行的进程列表,检测 frida-server 是否运行 + +#### 端口检测 + +检测 frida-server 默认端口 27042 是否开放 + +#### D-Bus 协议通信 + +app 运行时,会创建/proc/进程 pid 的文件夹 + +Frida 使用 D-Bus 协议通信,可以遍历/proc/net/tcp 文件,或者直接从 0-65535 向每个开放的端口发送 D-Bus 认证消息,哪个端口回复了 REJECT,就是 frida-server + +#### 扫描 maps 文件 + +cat maps + +maps 文件用于显示当前 app 中加载的依赖库 Frida 在运行时会先确定路径下是否有 re.frida.server 文件夹若没有则创建该文件夹并存放 frida-agent.so 等文件,该 so 会出现在 maps 文件中 + +#### 扫描 task 目录 + +扫描目录下所有/task/pid/status 中的 Name 字段寻找是否存在 frida 注入的特征具体线程名为 gmain、gdbus、gum-js-loop、pool-frida 等 + +#### 通过 readlink + +查看/proc/self/fd、/proc/self/task/pid/fd 下所有打开的文件,检测是否有 Frida 相关文件 + +#### 常见用于检测的系统函数 + +strstr、strcmp、open、read、fread、readlink + +扫描内存中是否有 Frida 库特征出现,例如字符串 LIBFRIDA + +#### 通常比较会被检测的文件 + +riru 的特征文件 /system/lib/libmemtrack.so /system/lib/libmemtrack_real.so cmdline 检测进程名,防重打包 status 检测进程是否被附加 stat 检测进程是否被附加 task/xxx/cmdline 检测进程名,防重打包 task/xxx/stat 检测进程是否被附加 task/xxx/status 检测线程 name 是否包含 Frida 关键字 fd/xxx 检测 app 是否打开的 Frida 相关文件 maps 检测 app 是否加载的依赖库里是否有 Frida net/tcp 检测 app 打开的端口 + +huluda-server 处理了 re.frida.server 文件夹以及该文件夹下的文件的名字 + +使用这个 server,不放在/data/local/tmp 目录下,基本可以不用关心 fd 和 maps 的检测 + +frida-gadget https://bbs.pediy.com/thread-269866.htm diff --git "a/docs/skill/reverse/android/frida/Frida\347\254\224\350\256\260.md" "b/docs/skill/reverse/android/frida/Frida\347\254\224\350\256\260.md" new file mode 100644 index 0000000..7d6dd2a --- /dev/null +++ "b/docs/skill/reverse/android/frida/Frida\347\254\224\350\256\260.md" @@ -0,0 +1,742 @@ +--- +id: frida-note +slug: /frida-note +title: Frida笔记 +date: 2021-02-10 +authors: kuizuo +tags: [frida, app, hook] +keywords: [frida, app, hook] +--- + +## 虚拟环境安装 + +由于 Python 版本兼容性问题,建议是安装虚拟环境 + +#### 安装 virtualenvwrapper + +```bash +pip install virtualenvwrapper-win -i https://pypi.tuna.tsinghua.edu.cn/simple +``` + +#### 添加虚拟环境变量 + +添加一个 `WORKON_HOME` 为 `E:\Envs` (虚拟环境的路径) + +#### 创建虚拟环境 + +`mkvirtualenv --python=python版本路径 环境名` + +```bash +mkvirtualenv --python=E:\Python37\python.exe py37 +mkvirtualenv --python=E:\Python38\python.exe frida +``` + +默认是用户路径下 `C:\Users\{username}\Envs\` + +#### 进入虚拟环境 + +```bash +workon #列出所有虚拟环境 + +workon 环境名 #进入对应名字下的虚拟环境 +``` + +#### 退出虚拟环境 + +```bash +deactivate +``` + +### 删除虚拟环境(必须先退出虚拟环境内部才能删除当前虚拟环境) + +```bash +rmvirtualenv 虚拟环境名称 +``` + +### pip 相关指令 + +#### 查看虚拟环境中安装的包: + +```bash +pip freeze + +pip list +``` + +#### 收集当前环境中安装的包及其版本: + +```bash +pip freeze > requirements.txt +``` + +#### 在部署项目的服务器中安装项目使用的模块: + +```bash +pip install -r requirements.txt +``` + +## Frida + +github 下载地址: [frida/frida](https://github.com/frida/frida) + +文档: [Welcome | Frida • A world-class dynamic instrumentation framework](https://frida.re/docs/home/) + +### 安装 + +```bash +pip install frida-tools # 会自动帮你下载Frida 最新版 +``` + +#### 安装指定版本 + +```bash +pip install frida==版本号 +``` + +#### 查看 frida-tools 版本 + +因为 一个 frida-tools 会对应多个 frida 版,所以安装指定版本不能直接安装最新版,需查看对应版本号 + +访问 https://github.com/frida/frida/releases/tag/ + frida 版本号,找到 python3-frida-tools-版本号,即 frida-tools 版本号 + +```bash +pip install frida-tools==【frida-tools版本号】 # 无【】 +``` + +#### 查看版本号,验证是否安装成功 + +```bash +frida --v +``` + +上述安装,有可能无法下载,建议科学上网,或使用 whl 离线安装 + +``` +pip install frida-15.1.14-cp38-cp38-win_amd64.whl +pip install frida_tools-10.4.1-py3-none-any.whl +``` + +#### frida 代码提示 + +``` +npm i @types/frida-gum +``` + +### frida 版本与 Android 版本与 Python 版本 + +| frida | Android | Python | +| ------ | ------- | ------ | +| 12.3.6 | 5-6 | 3.7 | +| 12.8.0 | 7-8 | 3.8 | +| 14+ | 9+ | 3.8 | + +### fridaserver + +#### 安装 + +fridaserver 与 frida 版本需要匹配,和 frida-tools 一样,访问 https://github.com/frida/frida/releases/tag/ + frida 版本号,可以找到对应的 fridaserver 版本。 + +文件名的格式为:`frida-server-(version)-(platform)-(cpu).xz`,需要下载的安卓的也就是`frida-server-15.1.14-android-arm64.xz`, **解压后**将文件 push 到手机内`/data/local/tmp/`下,并重命名 fsarm64 + +```bash +adb push C:\Users\kuizuo\Desktop\frida-server-15.1.14-android-arm64 /data/local/tmp/fsarm64 + +adb shell +su +cd data/local/tmp +chmod 777 fsarm64 + +./fsarm64 +``` + +#### 使用 + +```bash +# CMD 手机端 +adb shell +su +./data/local/tmp/fsarm64 # 启动fs服务 +# 可添加参数 -l 0.0.0.0:9000 指定端口为9000(默认27042),用于frida -H连接多个设备 + +# CMD 电脑端 +workon frida #进入frida环境 +frida -H -U -l hook.js +``` + +**新版本 fridaserver 无需端口转发**,旧版可能还需要新开一个 CMD 窗口执行`adb forward tcp:27042 tcp:27042` + +## Frida 命令 + +Hook 前提: 在 hook 时,要保证参数类型执行流程与原代码保持一致,必要的调用与结果返回不可省略,否则将有可能导致程序崩溃。 + +`frida -help` 查看帮助,常用选项如下 + +| 选项 | 功能 | +| ------------------------------- | -------------------------------------------------- | +| -U,–usb | 连接 USB 设备 | +| -F, --attach-frontmost | app 最前显示的应用 | +| -H HOST, --host=HOST | 通过端口连接 frida-server 默认监听 局域网 ip:27042 | +| -f FILE, --file=FILE spawn FILE | 以包名方式,自动启动 app 用%resume 恢复主线程 | +| -l SCRIPT, --load=SCRIPT | 以 js 脚本方式注入 | +| -n NAME, --attach-name=NAME | 以包名附加 | +| -p PID, --attach-pid=PID | 以 PID 附加 | +| -o LOGFILE, --output=LOGFILE | 将结果输出到文件上 | +| --debug | 附加到 Node.js 进行调试 | +| --no-pause | 启动后,自动运行主线程 可省略%resume | + +### 简单 Hook 脚本演示 + +**注:Frida 老版本不支持 es6 语法。** + +代码如下 + +```js title="hook.js" +// java层的代码 都需要在perform下执行 +Java.perform(function () { + // Java.use() // 选择对应的类名 返回实例化的对象 可直接调用类下方法(反编译后查看) + var Util = Java.use('com.dodonew.online.util.Utils') + // 调用类下的md5方法 同时实现方法改为新函数 + Util.md5.implementation = function (a) { + console.log('a: ', a) + var ret = this.md5(a) + console.log('ret: ', ret) + return ret + } +}) +``` + +运行 `frida -U -F -l hook.js`,触发 hook 的函数,便可打印出参。 + +### 获取类 + +```javascript +// Java.use(类名) + +let J_String = Java.use('java.lang.String') +let HashMap = Java.use('java.util.HashMap') +let Utils = Java.use('com.kuizuo.app.Utils') +``` + +### 静态方法与实例方法 + +```js +类.方法.implementation = function () { + this.方法() +} + +// 如果有返回值则需要将返回值返回 + +Util.md5.implementation = function (a) { + console.log('a: ', a) + let ret = this.md5(a) + console.log('ret: ', ret) + return ret +} + +const HashMap = Java.use('java.util.HashMap') +HashMap.put.implementation = function (key, value) { + console.log(JSON.stringify({ key: key.toString(), value: value.toString() })) + let ret = this.put(key, value) // 如果不修改的话,则需要原封不动的传入。 + return ret +} +``` + +### 重载方法 + +如果方法有重载,需要使用`.overload('java.lang.String')` 给定参数个数与类型,如果有重载,但是不使用 overload,frida 将会报错 + +```javascript +Util.test.overload('java.lang.String').implementation = function (a) { + let ret = this.test(a) + return ret +} + +Util.test.overload('int').implementation = function (a) { + let ret = this.test(a) + return ret +} +``` + +#### hook 所有重载方法 + +像上述两个重载方法,就需要编写两份代码,如果重载方法过多,代码不能很好的复用,就可以使用获取类下的所有重载方法 + +```javascript +类.方法.overloads // 返回所有重载方法,依次为每个成员实现implementation方法即可hook多个重载方法 + +let overloads = RequestUtil.encodeDesMap.overloads +for (const overload of overloads) { + overload.implementation = function () { + // console.log(Array.from(arguments)); + console.log([...arguments]) + // 两者都是打印参数,将类数组转真实数组 + + return this.encodeDesMap(...arguments) + } +} +``` + +### 构造方法 + +```javascript +类.$init.implementation = function () { + this.$init() +} +``` + +### 实例化对象 + +```javascript +类.$new() // 等同于 new 类() +``` + +### 主动调用类方法 + +**以下的“类”,是通过`Java.use()`返回的值。** + +#### 静态方法 + +```javascript +类.方法() +``` + +#### 实例方法 实例化对象 + +```javascript +let obj = 类.$new() +obj.方法() +``` + +#### 实例方法 获取已有对象(Java.choose) + +内存中遍历,找到**所有**符合条件的对象。 + +```javascript +Java.choose('类路径', { + onMatch: function (obj) { + obj.方法() + }, + onComplete: function () { + console.log('内存中的对象搜索完毕') + }, +}) +``` + +这样调用不优雅,会陷入回调地狱,所以可以封装成一个外部函数,来调用。(留个伏笔 TODO…) + +### 修改函数参数与返回值 + +```javascript +Utils.md5.implementation = function (a) { + let b = '随便设置的参数值' + let result = this.md5(b) // 直接修改成b + return '随便设置的返回值' // frida会将字符串包装成java的String对象。 + // return J_String.$new("随便设置的返回值"); +} +``` + +### 获取与修改类字段(成员变量) + +#### 静态字段 + +```javascript +类.字段.value // 获取类的属性值 +类.字段.value = '新的值' // 修改类的值 +``` + +#### 实例字段 实例化对象 + +```javascript +let obj = 类.$new() +obj.字段.value +``` + +#### 实例字段 获取已有对象(Java.choose) + +```javascript +Java.choose('类路径', { + onMatch: function (obj) { + console.log(obj.字段.value) + }, + onComplete: function () {}, +}) +``` + +:::tip + +**注: 如果字段名与方法名一样,则需要给字段名前加下划线\_,否则获取到的是方法** + +::: + +### 内部类与匿名类 + +内部类 + +```javascript +const 外部类$内部类 = Java.use('外部类$内部类') // 变量命名随意 +const 外部类$1 = Java.use('外部类$1') // 获取第一个内部类 +``` + +匿名类 + +匿名类是根据内存生成,没有具体的内部类名,通过 smali 代码来判断,获取到的可能像下面这样 + +```javascript +const $1 = Java.use('包名.MainActivity$1') +``` + +### 枚举类 + +```javascript +Java.choose("枚举类" { + onMatch: function (obj) { + console.log(obj.ordinal()); // 输出枚举的键 + }, onComplete: function () { + + } +}) +console.log(Java.use("枚举类").values()); // 输出值 +``` + +### 获取所有类 + +```javascript +Java.enumerateLoadedClassesSync() // 同步获取已加载所有类,返回一个数组 +Java.enumerateLoadedClasses() // 异步 +``` + +#### 加载类下所有方法,属性 + +使用到 Java 的反射 + +```javascript +const Utils = Java.use('com.kuizuo.app.Utils') +const methods = Utils.class.getDeclaredMethods() // 方法 +const constructors = Utils.class.getDeclaredConstructors() // 构造函数 +const fields = Utils.class.getDeclaredFields() // 字段 +const classes = Utils.class.getDeclaredClasses() // 内部类 +const superClass = Utils.class.getSuperclass() // 父类(抽象类) +const interfaces = Utils.class.getInterfaces() // 所有接口 + +// 遍历输出 +for (const method of methods) { + console.log(method.getName()) +} +// ... + +for (const class$ of classes) { + // class$ 为类的字节码,无需.class + let fields = class$.getDeclaredFields() + for (const field of fields) { + console.log(field.getName()) + } +} +``` + +### 函数堆栈的打印 + +```javascript +function showStack() { + Java.perform(function () { + console.log( + Java.use('android.util.Log').getStackTraceString(Java.use('java.lang.Throwable').$new()), + ) + }) +} +``` + +### HashMap 的打印 + +```javascript +RequestUtil.paraMap.overload('java.util.Map').implementation = function (a) { + // // a是一个HashMap对象 + let key = a.keySet() + let it = key.iterator() + let obj = {} + while (it.hasNext()) { + let keystr = it.next() + let valuestr = a.get(keystr) + // keystr 与 valuestr 都是Java的对象,需要使用toString转成文本 + // 直接打印结果为 + obj[keystr.toString()] = valuestr.toString() + } + console.log('obj: ', JSON.stringify(obj)) // 将打印成js的对象 + var result = this.paraMap(a) + return result +} +``` + +### 安卓关键代码类 + +| 类名 | 方法 | 作用 | +| ----------------------- | ----------------- | ---------------------- | +| android.widget.Toast | show | 弹窗提示 | +| android.widget.EditText | getText | 获取编辑框文本 | +| java.lang.StringBuilder | toString | 字符串获取与拼接 | +| java.lang.String | toString/getBytes | 获取字符串与字符串字节 | + +### 写文件 + +写文件如果写入的不是私有空间的话,需要获取内部存储空间权限 + +私有空间 `/data/data/包名`、`/storage/emulated/0/Android/data/包名` + +```javascript +let current_application = Java.use('android.app.ActivityThread').currentApplication() +let context = current_application.getApplicationContext() +let path = Java.use('android.content.ContextWrapper') + .$new(context) + .getExternalFilesDir('Download') + .toString() +console.log(path) // 获取app的私有空间 /storage/emulated/0/Android/data/包名/files/Download +let file = new File(path + '/test.txt', 'w') +file.write('内容') +file.flush() +file.close() +``` + +### 修改类型 + +Java.cast + +```javascript +utils.shufferMap2.implementation = function (map) { + console.log('map: ', map) // 传入的是HashMap对象,但是会向上转型为Map对象 输出[object Object] + var hashMap = Java.cast(map, Java.use('java.util.HashMap')) + console.log('hashMap: ', hashMap) + return this.shufferMap2(hashMap) +} +``` + +### 构建 Java 数组 + +```javascript +// 普通字符串数组 +let arr = Java.array('Ljava.lang.String;', ['字符串1', '字符串2', '字符串3']) + +// 对象数组 +let integer = Java.use('java.lang.Integer') +let boolean = Java.use('java.lang.Boolean') +let objarr = Java.array('Ljava.lang.Object;', ['字符串1', integer.$new(10), boolean.$new(true)]) + +// arrayList +var arrayList = Java.use('java.util.ArrayList').$new() +var integer = Java.use('java.lang.Integer') +var boolean = Java.use('java.lang.Boolean') +var Person = Java.use('com.kuizuo.app.Person') +var person = Person.$new('kuizuo', 20) +arrayList.add('kuizuo') +arrayList.add(integer.$new(10)) +arrayList.add(boolean.$new(true)) +arrayList.add(person) +``` + +注: 第一个参数类型给的是`Ljava.lang.String;` 而不是 `[Ljava.lang.String;` + +#### 指定函数下 hook(取消 hook) + +`HashMap.put.implementation = null` 取消对 HashMap.put 方法的 hook + +```javascript +const HashMap = Java.use('java.util.HashMap') +RequestUtil.paraMap.overload('java.util.Map').implementation = function (a) { + // a是一个HashMap对象 + HashMap.put.implementation = function (key, value) { + // 只在RequestUtil.paraMap方法调用的时候才会打印HashMap传入的参数 + console.log(JSON.stringify({ key: key.toString(), value: value.toString() })) + let ret = this.put(key, value) + return ret + } + var result = this.paraMap(a) + HashMap.put.implementation = null + return result +} +``` + +### dex 加载 + +#### 注入一个类 registerClass + +[JavaScript API | Frida • A world-class dynamic instrumentation framework](https://frida.re/docs/javascript-api/#java-cast) + +通常是加载某个类,复写某些方法,达到绕过的目的,如证书效验 + +但此方法相对繁琐,不如直接编写 java 代码编译成 dex 直接注入来的方便,也就有了 dex 的动态加载。 + +#### DexClassLoader + +```javascript +Java.perform(function () { + // console.log(Java.enumerateLoadedClassesSync().join("\n")); + // var dynamic = Java.use("com.xiaojianbang.app.Dynamic"); + // console.log(dynamic); + + // Java.enumerateClassLoaders({ + // onMatch: function (loader){ + // try { + // Java.classFactory.loader = loader; + // var dynamic = Java.use("com.xiaojianbang.app.Dynamic"); + // console.log("dynamic: ", dynamic); + // //console.log(dynamic.$new().sayHello()); + // dynamic.sayHello.implementation = function () { + // console.log("hook dynamic.sayHello is run!"); + // return "xiaojianbang"; + // } + // }catch (e) { + // console.log(loader); + // } + // }, onComplete: function () { + // + // } + // }); + + var dexClassLoader = Java.use('dalvik.system.DexClassLoader') + dexClassLoader.loadClass.overload('java.lang.String').implementation = function (className) { + //console.log(className); + var result = this.loadClass(className) + //console.log("class: ", result); + //console.log("class.class: ", result.class); + //console.log("xxxxxxxx: ", result.getDeclaredMethods()); + if ('com.xiaojianbang.app.Dynamic' === className) { + Java.classFactory.loader = this + var dynamic = Java.use('com.xiaojianbang.app.Dynamic') + console.log('dynamic: ', dynamic) + //var clazz = dynamic.class; + //console.log("xxxxxxxx: ", clazz.getDeclaredMethods()[0].invoke(clazz.newInstance(), [])); + //console.log(dynamic.$new().sayHello()); + dynamic.sayHello.implementation = function () { + console.log('dynamic.sayHello is called') + return 'xiaojianbang' + } + console.log(dynamic.$new().sayHello()) + } + return result + } +}) +``` + +### dx + +bat: android\SDK\build-tools\sdk 版本\dx.bat + +jar 包: android\SDK\build-tools\sdk 版本\lib\dx.jar + +#### 使用 + +```bash +dx --dex --output=C:\Users\zeyu\Desktop\com\output.dex C:\Users\zeyu\Desktop\com\* +``` + +`C:\Users\zeyu\Desktop\com\*`下存放 java 代码编译后的.class 将其转为 dex 文件,也可指定.class 文件 + +注: `C:\Users\zeyu\Desktop\com\*` 绝对路径可能会报错,可使用相对路径。 + +#### baksmali 与 smali + +github: [JesusFreke/smali: smali/baksmali (github.com)](https://github.com/JesusFreke/smali) + +下载地址: [JesusFreke / smali / Downloads — Bitbucket](https://bitbucket.org/JesusFreke/smali/downloads/) + +baksmali 将 dex 编译成 smali + +smali 将 smali 编译成 dex + +##### 使用 + +反编译 dex + +```bash +java -jar baksmali-2.5.2.jar d classes.dex # 将会生成out的文件夹 +``` + +回编译 dex + +```bash +java -jar smali-2.5.2.jar a out # 将会生成out.dex文件 +``` + +#### apktool + +[iBotPeaches/Apktool: A tool for reverse engineering Android apk files (github.com)](https://github.com/iBotPeaches/Apktool) + +安装文档: [Apktool - How to Install (ibotpeaches.github.io)](https://ibotpeaches.github.io/Apktool/install/) + +#### apksigner + +jar 包: android\SDK\build-tools\sdk 版本\lib\apksigner.jar + +``` +apksigner sign --ks xxx.jks xxx.apk +Keystore password for signer #1: +# +``` + +#### frida 注入 dex 文件 + +``` +Java.openClassFile("/data/local/tmp/xxx.dex").load(); + +// 就可以在内存中使用加载后的类 +``` + +## 脱离 PC 使用 frida + +### Termux + +使用 Termux 终端,补齐 python,node 环境,相当于手机端运行电脑端的 frida,本质上与电脑端相同。 + +### frida-inject + +同 fridaserver,下载 frida-inject 移动到手机上, + +``` +adb push C:\Users\kuizuo\Desktop\frida-inject-15.1.14-android-arm64 /data/local/tmp/fiarm64 + +adb shell +su +cd data/local/tmp +chmod 777 fiarm64 +``` + +##### 使用 + +前提,hook 的 js 脚本也移动到 fiarm64 相同路径或指定路径。 + +```bash +./fiarm64 -n 包名 -s 脚本.js +./fiarm64 -p pid -s 脚本.js # ps -A 可查看pid +``` + +可以加-e,–eternalize 使其在后台运行。 + +### frida-gadget.so + +**免 root 使用 frida**,但需要重打包 app,比较稳定。可通过魔改系统,让系统帮我们注入 so,免去重打包的繁琐 + +##### 环境 + +abd、aapt、jarsigner、apksigner、apktool(这些都需要添加到环境变量中) + +##### 使用 + +使用到 objection patchapk 命令,选项如下 + +| 选项 | 例子 | 功能 | +| --- | --- | --- | +| -s xxx.apk | -s xxx.apk | 指定 apk 文件 | +| -a so 版本 | -a arm64-v8a | 指定安卓 so 版本 | +| -V frida-gadget 版本号 | -V 15.1.14 | 指定 frida-gadget 版本号,默认最新 | +| -d, –enable-debug | -d | 是否允许调试 | +| -c, –gadget-config TEXT | -c config.txt | 加载[配置](https://frida.re/docs/gadget/#script)方式打包 | + +frida-gadget 可能会下载失败,去 github 下载[frida-gadget-15.1.14-android-arm64.so.xz](https://github.com/frida/frida/releases/download/15.1.14/frida-gadget-15.1.14-android-arm64.so.xz),解压后将 gadget 文件更名`libfrida-gadget.so`为存放到`C:\Users\zeyu\.objection\android\arm64-v8a` + +执行 + +``` +objection patchapk -a arm64-v8a -V 15.1.14 -s xxx.apk +``` + +将会生成 xxx.objection.apk 文件,卸载原 app(与原 apk 签名不一样,无法覆盖安装),重新安装 + +重新打开将会进入白屏,正常现象,等待 frida 去连接,相当于 apk 中运行了一个 frida-server。 diff --git "a/docs/skill/reverse/android/frida/objection\347\254\224\350\256\260.md" "b/docs/skill/reverse/android/frida/objection\347\254\224\350\256\260.md" new file mode 100644 index 0000000..9cb2c0d --- /dev/null +++ "b/docs/skill/reverse/android/frida/objection\347\254\224\350\256\260.md" @@ -0,0 +1,149 @@ +--- +id: objection-note +slug: /objection-note +title: objection笔记 +date: 2021-02-10 +authors: kuizuo +tags: [frida, app, hook] +keywords: [frida, app, hook] +--- + +## objection + +Frida 只是提供了各种 API 供我们调用,在此基础之上可以实现具体的功能,比如禁用证书绑定之类的脚本,就是使用 Frida 的各种 API 来组合编写而成。于是有大佬将各种常见、常用的功能整合进一个工具,供我们直接在命令行中使用,这个工具便是[objection](https://github.com/sensepost/objection)。 + +objection 功能强大,命令众多,而且不用写一行代码,便可实现诸如内存搜索、类和模块搜索、方法 hook 打印参数返回值调用栈等常用功能,是一个非常方便的逆向必备、内存漫游神器。 + +### 安装 + +```bash +pip install objection +``` + +### 使用 + +```bash +objection -g <包名> explore +objection -N -h <手机ip地址> -p <端口> -g <包名> explore # 指定ip与端口连接 +``` + +#### 选项 + +| 选项 | 功能 | +| --------------------------------- | ------------------ | +| -s, --startup-command “hook 命令” | 启动前注入 | +| -c, –file-commands FILENAME | 通过文件命令来运行 | +| --dump-args | 打印参数 | +| --dump-return | 打印返回值 | +| --dump-backtrace | 打印堆栈信息 | + +objection log 文件位置: `C:\Users\zeyu\.objection\objection.log` + +### 常用命令 + +| 命令 | 功能 | +| --------- | --------------------- | +| frida | 显示 frida 版本信息 | +| env | 显示 app 相关环境变量 | +| help 命令 | 查看命令帮助 | + +#### hook 命令 + +| 命令 | 功能 | +| --- | --- | +| `android hooking list classes` | 列出所有已加载的类 | +| `android hooking search classes ` | 搜索特定关键字的类 | +| `android hooking list class_methods <路径.类名>` | 列出类下所有方法 | +| `android hooking watch class <路径.类名>` | hook 类的所有方法(不包括构造方法) | +| `android hooking watch class_method <路径.类名.方法名>` | hook 类的方法(所有重载方法) | +| `android hooking watch class_method <路径.类名.方法名> "<参数类型>"` | hook 单个重载方法,需指定参数类型 | + +#### 查看 hook 列表 + +``` +jobs list +``` + +#### 取消 hook + +``` +jobs kill +``` + +#### 关闭 ssl 效验 + +``` +android sslpinning disable +``` + +#### 关闭 root 检测 + +``` +android root disable +``` + +### 界面跳转 + +#### 查看当前 app 的 activity + +``` +android hooking list activities +``` + +#### 尝试跳转到对应的 activity + +``` +android intent launch_activity +``` + +### 插件 + +:::note 注: 在 Window 下插件路径需要使用两个//或使用\,不然会报 Missing `__init__.py`错误 + +::: + +#### 加载插件 + +```bash +objection -g com.app.name explore -P <插件路径> +``` + +or + +```bash +objection -g com.app.name explore +plugin load <插件路径> +``` + +#### [Wallbreaker](https://github.com/hluwa/Wallbreaker) + +从内存中搜索对象或类,并漂亮地可视化目标的真实结构。 + +``` +objection -g com.app.name explore -P F:\\Frida\\objection-plugin\\Wallbreaker\\wallbreaker +// or +plugin load F:\\Frida\\objection-plugin\\Wallbreaker\\wallbreaker +``` + +##### 使用 + +``` +plugin wallbreaker classsearch # 搜索类 +plugin wallbreaker objectsearch # 搜索类的实例对象 +plugin wallbreaker classdump [--fullname] # 输出类结构, 打印数据中类的完整包名 +plugin wallbreaker objectdump [--fullname] # 输出指定对象的每个字段值 + +``` + +#### [FRIDA-DEXDump](https://github.com/hluwa/FRIDA-DEXDump) + +进入 objection,加载插件 `plugin load <插件路径> [指定插件名字]` + +``` +plugin load F:\\Frida\\objection-plugin\\FRIDA-DEXDump\\frida_dexdump + +# 加载完插件后就可以使用插件命令了 + +plugin dexdump dump +plugin dexdump search +``` diff --git "a/docs/skill/reverse/android/\345\210\267\346\234\272/\345\210\267\345\205\245Magisk.md" "b/docs/skill/reverse/android/\345\210\267\346\234\272/\345\210\267\345\205\245Magisk.md" new file mode 100644 index 0000000..02fa0aa --- /dev/null +++ "b/docs/skill/reverse/android/\345\210\267\346\234\272/\345\210\267\345\205\245Magisk.md" @@ -0,0 +1,71 @@ +--- +id: brush-magisk +slug: /brush-magisk +title: 刷入Magisk +date: 2021-12-09 +authors: kuizuo +tags: [android, magisk, 刷机] +keywords: [android, magisk, 刷机] +--- + + + +> 相关链接: [小胡子的干货铺——Pixel 4 XL 刷入 Magisk、Root - 少数派 (sspai.com)](https://sspai.com/post/57923#!) + +### **1.下载官方镜像包** + +[Factory Images for Nexus and Pixel Devices | Google Play services | Google Developers](https://developers.google.com/android/images#coral) + +在手机**设置**—**关于手机**—**版本号**查看自己手机系统的版本号**下载自己手机对应的版本!**,比如我这里的版本号是 + +**SP1A.210812.015** + +那么我就要在 Version 中找到对应的版本下载 + +![image-20211209142333912](https://img.kuizuo.cn/image-20211209142333912.png) + +解压缩后,找到后缀为.zip 的文件再解压缩,找到 boot.img 文件,将其单独复制出来。 + +### 2.下载 Magisk Manager + +这里我选择的是最新版的 + +[Releases · topjohnwu/Magisk (github.com)](https://github.com/topjohnwu/Magisk/releases) + +![image-20211209150313229](https://img.kuizuo.cn/image-20211209150313229.png) + +### 3.将复制文件置手机 + +将上述 boot.img 和 MagiskManager-v7.4.0.apk 两个文件传到手机里备用 + +### 4.安装 Magisk + +在手机上安装 Magisk Manager 后并打开,点击安装 Magisk,**选择安装方法**—**选择并修补一个文件**,找到刚才传到手机中的 boot.img 并选中。 + +![image-20211209152217621](https://img.kuizuo.cn/image-20211209152217621.png) + +这时会出现下方图二的修补过程,修补完成后**不要重启**。 + +![image-20211209152501019](https://img.kuizuo.cn/image-20211209152501019.png) + +修补后会文件夹下生成一个**magisk_patched-23000_woltm.img**文件(每次生成的文件名都不一样) + +上图框选的即为文件位置,**将该文件复制到 platform-tools 文件夹下**。 + +### 5.进入 bootloader 模式 + +关机后,同时长按**电源键**和**音量减键**,进入 bootloader 界面。(`adb reboot bootloader`)通过 USB 线将手机连接到电脑。 + +### 6.打开 platform-tools 文件夹,打开 CMD 窗口 输入下行命令 + +``` +fastboot flash boot magisk_patched-23000_woltm.img +``` + +``` +fastboot reboot +``` + +手机重启后,便成功刷入 Magisk,并拥有 Root 权限。 + +![image-20211209153202961](https://img.kuizuo.cn/image-20211209153202961.png) diff --git "a/docs/skill/reverse/android/\345\210\267\346\234\272/\345\256\211\350\243\205LSPosed.md" "b/docs/skill/reverse/android/\345\210\267\346\234\272/\345\256\211\350\243\205LSPosed.md" new file mode 100644 index 0000000..5701a28 --- /dev/null +++ "b/docs/skill/reverse/android/\345\210\267\346\234\272/\345\256\211\350\243\205LSPosed.md" @@ -0,0 +1,54 @@ +--- +id: install-lsposed +slug: /install-lsposed +title: 安装LSPosed +date: 2021-12-09 +authors: kuizuo +tags: [android, 刷机] +keywords: [android, 刷机] +--- + + + +## 高版本安装 Xposed + +熟悉安卓逆向都清楚,Xposed 不支持高安卓版本,自从 android 7.0 之后 xposed 的开发者 rovo89 基本就不维护了,针对 android 8.0 的版本草草发布了一个测试版本撒手不管了。如今安卓手机遍地安卓 9 安卓 10 的(本篇写的时候甚至安卓 12 都已发布大半年了),于是便有了一个替代品----Edxposed 框架,Edxposed 全称 Elder driver Xposed Framework,简称 edxp,我的前一部手机 Pixel 便是安装 Edxposed 的,但有个更好的替代品 Lsposed + +## 什么是 Lsposed + +LSPosed 是一款开源在 GitHub 上的 Xposed 框架,全称:LSPosed Xposed Framework。LSPosed 框架基于 Rirud 的 ART 挂钩框架(最初为 Android Pie)提供与原版 Xposed 相同的 API,利用 YAHFA 挂钩框架,支持 Android 12,在 Android 高权限模式下运行的框架服务,可以在不修改 APK 文件的情况下修改程序的运行,基于它可以制作出许多功能强大的 Xposed 模块,且在功能不冲突的情况下同时运作。 + +## 为什么是 Lsposed + +1️⃣ Edxposed 面临着停更的风险,且稳定性欠佳, Lsposed 则可以保证长期更新,并会持续加入新的功能。 +2️⃣ Lsposed 修复了 Edxoosed 的一系列 bug(比如偶尔软重启),并提升了其稳定性和性能。由于 Lsposed 默认开启白名单模式,模块只受用于需要的应用,系统资源的消耗被大大减少,耗电量也有所改善。 +3️⃣ 而且,对于绝大多数模块而言,Lsposed 只需重启该应用即可激活,而无需重启整个系统(部分涉及系统框架的模块除外) +4️⃣ 此外,Lsposed 的默认白名单设定使得用户的个人隐私得到保障,进一步加强了系统安全性。借助 Magisk Hide 功能,Lsposed 可以很好地隐藏自己,避免被部分重要应用识别 + +## 安装 + +github 地址:[LSPosed/LSPosed: LSPosed Framework (github.com)](https://github.com/LSPosed/LSPosed) + +手机配置: + +版本: 安卓 12 + +手机型号: Pixel 4XL + +安装 Lsposed 前提是安装 Magisk 模块,这边安装的是最新版即 Magisk v23,关于 Magisk 的安装在上一篇已经呈现了。 + +### 安装 Riru + +又由于我的安卓版本是 12,所以需要下载 Riru v25+ + +Riru 模块地址:[Releases · RikkaApps/Riru (github.com)](https://github.com/RikkaApps/Riru/releases) + +或者直接在 Magisk 模块仓库中搜索 Riru,并安装 + +### 安装 Lsposed + +同样的,安装 Riru 后在 Magisk 模块仓库中搜索 LSPosed(一般在在线首页中就有),然后点击安装后重启便可。![image-20211209162111421](https://img.kuizuo.cn/image-20211209162111421.png) + +这时候桌面便会有 LSPosed 应用,这时候就可以愉快去下载 Xposed 模块了。 + +![image-20211210015027929](https://img.kuizuo.cn/image-20211210015027929.png) diff --git "a/docs/skill/reverse/android/\345\210\267\346\234\272/\346\212\223\345\214\205.md" "b/docs/skill/reverse/android/\345\210\267\346\234\272/\346\212\223\345\214\205.md" new file mode 100644 index 0000000..7b82254 --- /dev/null +++ "b/docs/skill/reverse/android/\345\210\267\346\234\272/\346\212\223\345\214\205.md" @@ -0,0 +1,133 @@ +--- +id: intercepting-requests +slug: /intercepting-requests +title: 抓包 +date: 2020-02-02 +authors: kuizuo +tags: [android, http, 抓包] +keywords: [android, http, 抓包] +--- + +## Charles + +### 1、安装 + +下载地址: [Download a Free Trial of Charles • Charles Web Debugging Proxy (charlesproxy.com)](https://www.charlesproxy.com/download/) + +激活码地址: [Charles 破解工具 (zzzmode.com)](https://www.zzzmode.com/mytools/charles/) + +### 2、设置 socket 代理 + +Proxy -> Proxy Setting 如图 + +![image-20210202045815609](https://img.kuizuo.cn/image-20210202045815609.png) + +用 Charles 针对抓安卓的包,所以在 Windows 下就不设置代理。同时使用 Socket 代理 而不是 http 代理,配置端口(这里为 8999)即可 + +### 3、抓取 HTTPS 包 + +Proxy -> SSL Proxying Settings… + +![image-20220119144140665](https://img.kuizuo.cn/20220119144147.png) + +匹配所有地址与端口 `*:*` ,对于双向验证的证书也可在这里设置。 + +![image-20220119144353270](https://img.kuizuo.cn/20220119144353.png) + +### 3、Denying access from address not on ACL + +要在 charles 允许设备,需要如下设置 + +![image-20210517020819625](https://img.kuizuo.cn/image-20210517020819625.png) + +然后添加一个 0.0.0.0/0 的 ip 即可抓取所有设备 + +![image-20210517020904361](https://img.kuizuo.cn/image-20210517020904361.png) + +## Postern + +### 1、安装 postern + +下载地址: https://github.com/postern-overwal/postern-stuff/blob/master/Postern-3.1.2.apk + +### 2、配置代理 + +![image-20210202050134094](https://img.kuizuo.cn/image-20210202050134094.png) + +用户名与密码加密类型可不填 + +注: 有个小坑,要保证电脑与手机连接的是同一个 Wifi 网络,点击 菜单 -> Help -> Local IP 可查看当前网络下 IP 如图,一般为 Wireless(笔记本),具体都要尝试一遍 + +![image-20210202050422857](https://img.kuizuo.cn/image-20210202050422857.png) + +### 3、配置规则 + +![image-20210202050513312](https://img.kuizuo.cn/image-20210202050513312.png) + +这里也要注意,在**第一次**配置的时候,点击保存后,Charles 会弹出对话框,**点击右边 Allow** 即可,如果没有出现,那么多半是代理 IP 没有配置好,这时候尝试多开关几次 VPN 与设置 Local IP 中的 IP 即可。 + +![image-20210202051719212](https://img.kuizuo.cn/image-20210202051719212.png) + +### 4、配置 SSL 证书 + +此时可以抓包,但抓取 HTTPS 则是 unknown,即未解密的,这时候就要配置 SSL 证书 + +#### 安装方式 1 + +![image-20210202051440984](https://img.kuizuo.cn/image-20210202051440984.png) + +弹出安装提示,并非直接安装 + +![image-20210202051610186](https://img.kuizuo.cn/image-20210202051610186.png) + +访问 chls.pro/ssl 下载 安装(与 fd 类似) + +#### 安装方式 2 + +但要注意,在 Socket 代理下 可能无法下载证书,这时候 切换至 HTTPS 代理(同 FD 配置),然后下载证书安装也有可能会失败,则选择 Save Charles Root Certifcate,将证书推送(adb pull)到手机上,然后点击安装,或者到安全->加密与凭证 -> 从存储设备安装证书 -> CA 证书,选择导入到手机的证书即可。 + +### 2、电脑端也要安装证书,如图 + +![image-20210517023044653](https://img.kuizuo.cn/image-20210517023044653.png) + +点击 然后下一步即可 + +### 检测证书 + +由于 fd 与 charles 都是替换证书的,安装的证书都是用户下的,而非系统下(7.0 以上),一些 app 会检测证书,从而无法发送请求,这时候就需要将用户证书移动到系统证书下 + +系统证书路径 `/etc/security/cacerts` + +用户证书路径 `/data/misc/user/0/cacerts-added` + +命令 + +```bash +#挂载为可读写 将根路径挂载为可读写 +mount -o rw,remount / + +# 将当前目录下所有文件移动置系统证书路径下 +cp * /etc/security/cacerts +``` + +不执行 `mount -o rw,remount /` 则会报 cp: /etc/security/cacerts/03f1f1d0.0: Read-only file system + +或用 Root Explorer 将用户证书移动到系统证书路径下即可 + +:::tip + +补:有时候目录无法挂载为可读写(比如安卓 10 以上系统分区/system 挂载为只读),就可以使用 Magisk 的 Move Certificates 模块,将用户证书移动至系统证书路径下 + +::: + +### 大功告成 + +这时候就可以正常的抓到安卓对应的包了。 + +## 对比 FD 配置代理 与 Charles 和 Postern 组合 + +首先配置代理属于会话层,很容易获取到代理的 ip 与端口,检测到是否代理,从而限制 app 使用, + +而挂了 VPN 则是将在网络层中,不易被检测,同时能获取到应用层(HTTP)与传输层(TCP)等数据。 + +同时 FD 需要来回配置代理特别麻烦,而 Postern 只需要开启 VPN 与关闭即可。所以在 wifi 中就无需配置代理。 diff --git "a/docs/skill/reverse/android/\345\210\267\346\234\272/\350\247\243Bootloader\351\224\201.md" "b/docs/skill/reverse/android/\345\210\267\346\234\272/\350\247\243Bootloader\351\224\201.md" new file mode 100644 index 0000000..be0712a --- /dev/null +++ "b/docs/skill/reverse/android/\345\210\267\346\234\272/\350\247\243Bootloader\351\224\201.md" @@ -0,0 +1,109 @@ +--- +id: solution-of-bootloader-lock +slug: /solution-of-bootloader-lock +title: 解Bootloader锁 +date: 2021-12-09 +authors: kuizuo +tags: [android, bootloader, 刷机] +keywords: [android, bootloader, 刷机] +--- + + + +最近准备重学安卓逆向与开发,自然工具是肯定少不了,最为重要的便是手机。之前的手机是 Pixel,但是手机性能不太好,使用起来一卡一卡的(程序安装的有点多)。于是就准备换台 Pixel 4XL 欧版的来作为新设备。 + +于是便记录下手机的配置逆向环境的过程,而这篇就是刷机最主要的一步,解 bl 锁,不然就没有后续刷面具,root 等等操作了。 + +相关文章 [小胡子的干货铺——Pixel 4 XL 解锁 Bootloader - 少数派 (sspai.com)](https://sspai.com/post/57922) + +## 开始解锁 + +:::danger **前文提示:解锁后,手机上所有数据将被清除重置,请备份重要数据!** + +::: + +### 下载工具包 + +**需要科学上网才能下载** + +1、platform-tools.zip: (解压到一个文件夹下) + +https://developer.android.com/studio/releases/platform-tools.html + +2、USB 驱动.zip:(放同文件夹不需要解压) + +https://developer.android.com/studio/run/oem-usb.html#InstallingDriver + +### OEM 解锁 + +1、**设置**—**关于手机**—**版本号**连按七下 + +2、返回上一级(**设置**)—**系统**—**高级**—**开发者选项**—打开“**OEM 解锁**”,后续按提示操作 + +3、**开发者选项**—打开“**USB 调试**”(备用) + +### 更新驱动 + +1、电脑开始菜单旁边搜索“**设备管理器**”并打开 + +2、通过 USB 线将手机连接到电脑 + +3、找到新出现的设备就是你的手机,右击更新驱动—自动搜索更新驱动或者手动在桌面搜索刚才下载的驱动并安装 + +![img](https://img.kuizuo.cn/f49f1e5afc077dafab5d74a72965f8ba.png) + +如下图提示则说明安装成,接着就要开始解锁了 + +![image-20211209133458792](https://img.kuizuo.cn/image-20211209133458792.png) + +### Bootloader 解锁 + +1、**关机**后,同时按住**电源键**和**音量减键**,进入 Bootloader 界面。 + +![image-20211209135203559](https://img.kuizuo.cn/image-20211209135203559.png) + +可以看到**Device-State: locked** 表明为加锁状态 + +2、通过 USB 线将手机连接到电脑。 + +3、打开桌面 platform-tools 文件夹,在当前文件夹下打开 CMD 窗口(不可打开 PowerShell,不然命令不可用) + +4、键入以下命令检查 fastboot 连接: + +```bash +fastboot devices +``` + +回车后应该显示你的设备序列号,如果不是,你需要确保你的驱动程序已正确安装。 + +5、确认 fastboot 连接没问题,即可运行解锁 bootloader 命令: + +```bash +fastboot flashing unlock +``` + +你现在应该在手机上看到一个操作界面,要求你确认此操作。使用音量键选择(按一下音量键下即可),使用电源键确认(选择 Unlock the bootloader 并确认)。确认该过程完成,然后键入此命令: + +```bash +fastboot reboot +``` + +手机重启,完成解锁。此时手机界面就会显示 Google 解锁的提示动画。 + +**解锁后,手机上所有数据被清除重置,如需执行后续工作,须重新开启开发者选项、USB 调试** + +## 解除 WiFi 网络受限 + +由于国内网络访问谷歌服务器会被墙,导致 wifi 网络受限。通过下面操作可以解除 WiFi 网络受限 + +1、手机开机状态下,通过 USB 线将手机连接到电脑。 + +2、打开桌面 platform-tools 文件夹,打开 CMD 窗口 + +3、输入命令 + +``` +adb shell settings put global captive_portal_https_url https://www.google.cn/generate_204 +``` + +4、打开飞行模式再关闭,查看是否已解除受限。 diff --git "a/docs/skill/reverse/crypto/\346\265\205\350\260\210\345\212\240\345\257\206\347\256\227\346\263\225.md" "b/docs/skill/reverse/crypto/\346\265\205\350\260\210\345\212\240\345\257\206\347\256\227\346\263\225.md" new file mode 100644 index 0000000..a012522 --- /dev/null +++ "b/docs/skill/reverse/crypto/\346\265\205\350\260\210\345\212\240\345\257\206\347\256\227\346\263\225.md" @@ -0,0 +1,1107 @@ +--- +id: brief-talk-encryption-algorithm +slug: /brief-talk-encryption-algorithm +title: 浅谈加密算法 +date: 2020-09-02 +authors: kuizuo +tags: [cipher, reverse] +keywords: [cipher, reverse] +--- + + + +## 前言 + +本文只涉及加密算法认识与使用,不涉及加密算法的源码分析与加密原理。(因为本人自己也看不懂源码,但是会用真就足够了,就算让我写一个这样的算法,给我源码也不会写,何况还是开源的) + +本人并非密码学专家,但接触过 JS 逆向和安卓 Java 层,对一些加密算法也有所了解,借此来分享一下自己所接触过的常见加密算法与使用。 + +涉及到的常用的加密算法有 + +- 消息摘要算法 + - MD5 + - SHA1,SHA3,SHA256... + - HmacMD5,HmacSHA1,HmacSHA256... +- 对称加密算法 + - DES + - 3DES(也称 TripleDES,DESede) + - AES +- 非对称加密算法 + - RSA + +## 编码 + +涉及到加密算法,必须要涉及到编码格式,主要涉及到的编码方式编码有以下几种 + +### UTF-8 + +针对 Unicode 的一种可变长度字符编码。它可以用来表示 Unicode 标准中的任何字符,而且其编码中的第一个字节仍与[ASCII](https://baike.baidu.com/item/ASCII/309296)相容,使得原来处理 ASCII 字符的软件无须或只进行少部份修改后,便可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。 + +### GBK (gb2312) + +GBK 即“国标” ,汉字编码的标准编码字库。 + +#### 上述两者的区别 + +- 表示中文的所占字节不同 + + 同样表示一个中文字符,gbk 所占 2 字节,而 utf8 占 3 字节,通俗点就是如果你的项目代码涉及的都是中文这些,不会有希腊文,韩文等等,那么优先 gbk 编码,因为字节少,占用空间少。但如果涉及到更广的语言,那么 uf8 无疑是首选的。一般来说 Unicode 标准中 utf8 已经够用了,在编写代码中多数环境也是再 utf8 的标准上。总之基本 utf8 就完事了。 + +如需更深入了解可自行百度相关编码知识,本文只做与加密算法相关。 + +### Base64 + +1. 所有的数据都能被编码为只用 65 个字符就能表示的文本。标准的 Base64 每行为 76 个字符,每行末尾添加一个回车换行符(\r\n)。base 是可以互相转化的 + +2. 65 字符:A~Z a~z 0~9 + / = + + 在 URL Base64 算法中,为了安全,会把 + 替换成 - ,把 / 替换成 \_ + + = 有时候用 ~ 或 . 代替(了解即可) + +3. Base64 的应用 + + 密钥、密文、图片、数据简单加密或者预处理 + + 例如下面这些数据 通过链接 [base64 图片在线转化](http://tool.chinaz.com/tools/imgtobase/)即可 base64 编码数据与图片互相转化 + +``` +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAMbSURBVDhPARAD7/wAbcLpc8Poc7nbcbbVecPggrzQyN3g9/z1+P7yx9rUlr7Ipc/fwNHh8vn/+///3eLeAGy84W+73aPl/6Th/YK80IWvu+b18v3989/h04uXi3aTkb7a3tnj7L3Axe3z8cPJxQB/xON5u9ub2PWczOKDoKSNmJCkopWxqpiyq5izsZqtsJWPkn2al5CSkY3N0sz1//cAu+r8q9jri7TGlrO7q7Oov7qnv7ekyMCrzMSvz8iuycOjqqOHX1lNNTItMzgyYGlkAPT//9/r57jBvMLEuczHs8rCq87Gsc3Gs7q1oqKdioJ/bHVzZF5fWS4wLSIkIystLADe39HW1MXUzbvDu6jKwq29tqOmoJJ8eGx+fG+AfnKTk4dpbGOfpJ6HhYYmFR1ONkMAubutvLqropuLqKGRko6DcXBrPDw6PT07kZKMkpOL6+7l0NPKo6SftqWrfEhesm2KAJaajDg6LWdjV4SAdYWEgJCSkYuPkldbXKWnpuPl4tXa1K+xrJiXk4ZzeaZyiNmUswAyPDNLUkqio52ko56uramZmpVvdHCssa2oqKiRkZOHiIqChIONjIq1qq7Fsbz/9P8AKiQklZGQ///9///9/v37+fj2foB70NLNk5OTrq6wnp6gqqqs4+Lgz87MmpibvbvAAD8ZJodsdf/6/P7////9///9//v5+t/e3PT08vz7+f/+//78/f7/+19hXBkZGTMzNQCbdYqHbHvCvMD3+Pr59/zx7PPs5+v//P3///37+/nw7+3s6+lxc24RExAlJigyMjQA19jt0tHhqqmvycnH6Ofj9fHw+PL28+7029/gyM7M5+flsbCuKCYpISAlIyInMTI2AMDX6eT2//f//+js7evs5+no5Obi4/79//z//97k5P7//////6imqQIBBhARFSMkKADP7fjW8v3d9Pzi8fb7///+//3q7+vq7u3V2drEyMvm5ujQ0NKio6VlaWp1d3aGiIcA0fP8xebvu9jg1Ojv9Pz/+//+8vj02N7a19vc6+zw2djd1NXX+Pz9+///9/n27e/sqToTd5KpZ2kAAAAASUVORK5CYII= +``` + +4. 浏览器内置 Base64 编码(btoa) 解码(atob) + + ![image-20200828054526066](https://img.kuizuo.cn/image-20200828054526066.png) + +### Hex + +**二进制数据**最常用的一种表示方式。用 0-9 a-f 16 个字符表示。每个十六进制字符代表 4bit。也就是**2**个十六进制字符代表一个字节。如`a12345678`用 md5 加密的结果为 32 位 0-9a-f 字符`e9bc0e13a8a16cbb07b175d92a113126` 每 2 个十六进制字符为一个字节,32 位字符也就是 16 个字节。 + +在实际应用中,尤其在密钥初始化的时候,一定要分清楚自己传进去的密钥是哪种方式编码的,采用对应方式解析,才能得到正确的结果。 + +## 单向散列函数(消息摘要算法,哈希算法) + +- MD5 +- SHA1,SHA3,SHA256... +- HmacMD5,HmacSHA1,HmacSHA256... + +先说最简单的也是用的最多的算法,性质如下 + +- 不管明文多长,散列后的密文定长 +- **明文不一样,散列后结果一定不一样** +- **散列后的密文不可逆** +- 一般用于校验数据完整性、签名、sign + +你只需要需要性质就行,下文会举实例。 + +由于密文不可逆,所以后台无法还原,也就是说他要验证,会在后台以跟前台一样的方式去重新签名一遍。也就是说他会把源数据和签名后的值一起提交到后台。常用于校验数据完整性、签名、sign + +比如我有一篇毕业论文,我写的差不多了,然后去厕所回来,看到我的一个室友坐在我电脑前,我该如何知道他是否有更改过我的毕业论文。这是消息摘要算法就能解决这个问题,在你走之前将论文取 MD5(后面例子也都以 MD5 为例),然后去厕所完,再取一次 MD5 的值,将两者一比对,只要修改了一个字符或者添加了一个空格,两者的 MD5 值都完全不一样,基本差别巨大。也就可以知道你的论文有没有被改了,但是被改了你也没有办法还原回去,然后你就毕业不了了。 + +由于固定原文加密后的密文是固定的,理论上只要我一个一个字符试过去,将结果与密文对比,相同的话就可以知道原文。那么可以将这些原文和密文存入对应的数据库里,在查加密后的密文后去数据库找原文,如彩虹表就是专门暴力破解这种算法的(相关链接 [什么是彩虹表](https://www.zhihu.com/question/19790488))。防止通过彩虹表破解的话就需要对原文在做一次操作----加盐。加盐可以理解为就是添加了一些字符串,例如上面说到`kuizuo`这个字符串 通过 MD5 算法后得到的结果是`ff1fa96799ded9ee89d0f764b3e9ff54` 这就是不加盐 MD5 返回的结果,万一我在`kuizuo`后面加一个`!`后呢,结果为`7e74121af78b9555241fdf6538e2f22b`,可以看到两者完全天壤之别,这就是这个算法妙的地方,我不加`!`的彩虹表所对应的`kuizuo`密文可能在彩虹表里都有了,但是我这样处理,在`kuizuo`这个字符串前或后,都加一个随机的字符串(这些随机的字符串可要记得,不然你自己到时候数据效验的时候也不知道原文对不对了),然后进行拼接在取 MD5 的值,这样他的彩虹表就废了,就需要猜测出我的加的盐,然后在重新生成密文与原文对应的数据库。 + +在涉及 HTTP 请求的时候,用到最多的还是 sign,如一段 post 的 data 数据为 + +```json +{"phone":"15212345678”,"timestamp":"1598567732417" , sign:"41785be6d13c5e3a0112c79255607f3a"} +``` + +timestamp 是时间戳,可以知道 1598567732417 所对应的时间(年月日时分秒毫秒的那种) + +这段数据用于发送手机验证码注册的,前端发送这段 post 请求给后端,后端要如果验证这个算法是否是伪造的,只要就需要将前端发送的原文与我的 sign 比对,是否正常来效验数据。比方说我上面的 sign 是通过 js 文件 将 phone 的值`15280326573`加上 timestamp 的值`1598567732417` 再加上盐`kuizuo` 然后进行 MD5 得到的值。拼接后也就是`152123456781598567732417kuizuo`加密后的结果。试想一下如果不加这个 sign 的值,那么我只要发送`{"phone":"15212345678”,"timestamp":"1598567732417}`给后端就能收到验证码了?那可也太轻松了,所以一般网络都会添加这个 sign。但由于是浏览器,浏览器内访问的 web 所有文件都是可见的,也就是我们能看到这些源文件的代码,也就是能找到对应加密代码,就能找到是将 phone 的值与 timestamp 的值加上 kuizuo 拼接后的取 MD5 的,同样也能伪装,只不过你得会看的懂 JS 逆向,这里就不多涉及了。 + +![image-20200828071854891](https://img.kuizuo.cn/image-20200828071854891.png) + +那 MD5 SHA1 SHA3 HmacMD5 HmacSHA1。。。又有啥区别 + +可以说没多大区别,都是消息摘要算法,加密的结果都是不可逆 加密后的长度固定 但各不同,如 md5 加密后为 32 个字符(hex 表示)而 sha1 则是 40 个字符,sha256 则是 64 位字符,加密后长度越长安全更好,但是加密速度也会限制。但是 sha 的输出结果还可以为 Base64 编码,此外加密后结果就没什么区别了,实现原理相不相同我可就不知道了。 + +HMAC 倒是有点区别,就是它需要一个密钥 Key,其余的也就和上面那些没区别了 + +## 对称加密算法 + +- DES +- 3DES(也称 TripleDES,DESede) +- AES 根据密钥长度不同又分为 AES-128 AES-192 AES-256 其中 AES-192 AES-256 在 Java 中使用需获取无政策限制权限文件 + +这种算法是对称的,分组加密,也就是加密和解密是可逆的,那么就肯定有东西用来加密和解密,就是密钥 Key,并且这个密钥我加密可以用,解密也可以用,此乃居家旅行之必备啊。但是他与 MD5 等不同,他需要的参数就多了,如图 + +![image-20200828074126613](https://img.kuizuo.cn/image-20200828074126613.png) + +模式 Mode,填充方式 Padding,一个 Key,一个 IV + +简要概述一下这里用 AES 来举例,(AES 算是 DES 的加强版,一般都是用 AES) + +CryptoJS 提供 ECB,CBC,CFB,OFB,CTR 五种模式,但常见的 Mode 模式也就 ECB,CBC(其他几个我实战还真没见过) + +填充方式提供 NoPadding ZeroPadding Pkcs7(Pkcs5) Iso10126 Iso97971 AnsiX923 + +CBC 模式 是需要 IV 向量的 **最常用的就是它** + +而 EBC 是不需要 IV 向量的 (由于不需要 Iv 向量,容易遭到字典攻击,不推荐) + +填充可以理解每次是对固定大小的分组数据进行处理。但是大多数需要加密的数据并不是固定大小的倍数长度。例如 AES 数据块为 128 位,也就是 16 字节长度,而需要加密的长度可能为 15、26 等等。为了解决这个问题,我们就需要对数据进行填补操作,将数据补齐至对应块长度。 + +而 Padding 可以说就决定了加密结果是否固定,如 Pkcs7 (我遇到的多),加密结果就是固定不变的 + +PKCS7 在填充时首先获取需要填充的字节长度 = (块长度 - (数据长度 % 块长度)), 在填充字节序列中所有字节填充为`需要填充的字节长度值`例: + +```js +假定块长度为 8,数据长度为 3,则填充字节数等于 5,数据等于 FF FF FF : +| FF FF FF 05 05 05 05 05 | +``` + +ZeroPadding 则是填充 0x00 + +```js +假定块长度为 8,数据长度为 2,则填充字节数等于 6,数据等于 FF FF : +| FF FF 00 00 00 00 00 00 | +``` + +ISO10126 在填充时首先获取需要填充的字节长度 = (块长度 - (数据长度 % 块长度)), 在填充字节序列中最后一个字节填充为`需要填充的字节长度值`, 填充字节中其余字节均填充随机数值. 例: + +```ruby +假定块长度为 16,数据长度为 9,则填充字节数等于 7,数据等于 FF FF FF FF FF FF FF FF FF : +| FF FF FF FF FF FF FF FF FF 73 68 C4 81 A6 23 07 | +``` + +也就是说 Iso10126 的 加密结果是不固定的,每次都会随机,但是通过同样的密钥和 IV 都能得到正确的明文 + +特定的,为了使算法可以逆向去除多余的填充字符,所以当数据长度恰好等于块长度的时候,需要补足块长度的字节.例如块长度为 8,数据长度为 8,则填充字节数等于 8. + +上面的 mode 和 Padding 没必要看懂,你只需要知道 CBC 模式 是需要 IV 向量的,而 EBC 是不需要 IV 向量的 + +Iso10126 填充 加密结果是随机的 + +但 。。。是,不同填充的加密结果,在特定的情况下的密文 在通过不同的填充方式是能的到正常的明文,你可以自行试试。 + +key 的长度 决定是 AES-几 如 key 的长度为 128 位,也就是 16 字节 如`0123456789abcdef` 就是 AES-128 + +注意,这里的 0123456789abcdef 是每一位为一字节 不是 MD5 的两位为一字节 key 的长度为 192 位 则是 24 字节 256 位 则是 32 字节 + +而 Iv 的话 可以理解为密钥偏移量,此外没了 + +DES 的话 密钥为 56 加 8 后 密钥长度为 64 位 (因为每 7 比特位会设置错误效验位)也就是 8 字节 密钥长度为 8 位字符(超出部分不管)密钥如 `12345678` 其他的大致都同 AES + +3DES 也就是 3 个 DES 密钥长度为 24 字符,密钥如 `123456781234567812345678` + +说这么多,不如使用工具使用工具试试。具体的可以自行下载 WT-JS,然后点击 CryptoJS 里测试一番 + +### WT-JS 使用 + +工具下载地址 [WT-JS](https://wwe.lanzous.com/iEEbmg4mr2b) + +这个工具可以直接生成对应的 CryptoJS 代码,直接调用即可获取对应的加密结果。然后点击 CryptoJS + +![image-20200828073118836](https://img.kuizuo.cn/image-20200828073118836.png) + +会弹出一个窗口,上面是选择对应的 js 文件,下面是测试加密用的,自己输入原文点击加密就能得到密文 + +![image-20200828073206109](https://img.kuizuo.cn/image-20200828073206109.png) + +此外,还可以用 node 的 crypto-js 模块来直接调用,不过需要导入 crypto-js,具体可查看文档 [crypto-js](https://www.npmjs.com/package/crypto-js) 这里就不提及了。 + +这里就以 WT-JS 的 AES 为例 + +```js +var CryptoJS = + CryptoJS || + (function (Math, undefined) { + // 此处省略几百行代码... + })() + +var key = CryptoJS.enc.Utf8.parse('0123456789abcdef') +var iv = CryptoJS.enc.Utf8.parse('0123456789abcdef') + +function AES_Encrypt(word) { + var srcs = CryptoJS.enc.Utf8.parse(word) + var encrypted = CryptoJS.AES.encrypt(srcs, key, { + iv: iv, + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7, + }) + return encrypted.toString() +} + +function AES_Decrypt(word) { + var srcs = word + var decrypt = CryptoJS.AES.decrypt(srcs, key, { + iv: iv, + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7, + }) + return decrypt.toString(CryptoJS.enc.Utf8) +} +``` + +通过 js 调试工具,调用`AES_Encrypt`与`AES_Decrypt`两个函数,即可实现加密解密。 + +## 非对称加密算法 + +常见也就是 RSA 了,性质如下 + +1. 使用公钥加密,使用私钥解密 + +2. 公钥是公开的,私钥保密 + +3. 加密处理安全,但是性能极差,单次加密长度有限制 + + pkcs1padding 明文最大字节数为密钥字节数-11 密文与密钥等长 + + NoPadding 明文最大字节数为密钥字节数 密文与密钥等长 + +4. RSA 既可用于数据交换,也可用于数据校验 + + 数据校验通常结合消息摘要算法 MD5withRSA 等 + +这个加密算法就牛逼了,因为它涉及到一个数学难题,将一个**大数**分解为 2 个**大素数**之积。这个大数有几百位长度的十进制数那么大。 + +#### 密钥对 + +公钥和密钥是不能乱写的,需要通过工具生成密钥对 [在线链接](http://web.chacuo.net/netrsakeypair) 如下例 + +公钥 + +``` +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4nxVUPQRp7oisWPe0FtFzqnaQ +U7cWuaIXp+EBBt8NWUwFUJfsIw1E4QaDKX0UXR3dZixHRzfHbR4ozojfJdUD5Y4j +lx0ChkfBxmIQuwO9yKoBteKkuDN3pwi15iUoVR6INHTS1zQNQCnwA7ucpMpE5leP +4mDmaKqeIoQzKR7/AwIDAQAB +-----END PUBLIC KEY----- +``` + +私钥 + +``` +-----BEGIN PRIVATE KEY----- +MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALifFVQ9BGnuiKxY +97QW0XOqdpBTtxa5ohen4QEG3w1ZTAVQl+wjDUThBoMpfRRdHd1mLEdHN8dtHijO +iN8l1QPljiOXHQKGR8HGYhC7A73IqgG14qS4M3enCLXmJShVHog0dNLXNA1AKfAD +u5ykykTmV4/iYOZoqp4ihDMpHv8DAgMBAAECgYEApESn7bP84WRkJzVh8NL8ujXK +GNDj70xsdS/ie89pV69EfNYg1vK5M7gk2z9nE19m2z+11hYAA2mLlDNwhVxcEv8o +9lyRZG3sJ4YNBnDs7tA3puIcG2pkOqHEnjr5l2FwYCGhVE6WHFzApBNxy3iXfesh +UotkajhtyzmgNZMe14ECQQDtUambf7WWLhaimvM1smxxue7GMHcUJLQvLzKlgcr6 +7euXL94em98FPC9pPZGmPiM+sHthV5CNWy4pE/PI+E2ZAkEAxyd8GdrmLPaO7XXK +HczOR2u9jGUHA43qiC81ftbFpkyPwEdsakxXK1AWtAjz8bcObpVsml0TR8PiTBBn +nDB6+wJBANCT9X21wOM9nqdLiHapWqaZxEJsVjxeBf9yfBD7AmuIsIcwiwhb9qej +PghBFMIH2vI+KjJjw6h5exifcKQxmAECQFWpMSL52bmLT8zptkb9Gdj0ibJCnjK0 +LyXmkG7/OEKgedBtqD9MmM3jg/BqTWsxnr6H/Q+kay+aHNM01ywCWlMCQFFI3Vk9 +QlK0C6SHTVLaHqcP/pWkcKb5ulZu1EsWrXXsnf1Rm40BuyPZEDcR1PE81/dZAukJ +jBQYikQQtBjNcEs= +-----END PRIVATE KEY----- +``` + +公钥是需要公开的,因为加密就只需要这个公钥就行,但是解密你没有私钥你是解不出明文的,目前要找出私钥的话,无解。在私钥里其实是包含公钥的,也就是可以通过私钥提取公钥(私钥长有长的理由)。 + +然而搞逆向的,你只需要找到能找到公钥加密的代码就行,因为我们只需要将加密的结果发送给服务器就行了,解密的东西就不用管,后台收到数据会自行解密就行了 + +但也不是说 RSA 就无敌了,他加密的数据非常有限,并且加密速度与对称加密算法是无法相比。 + +#### 在知道公钥的前提下加密 + +知道了公钥怎么加密?这里可以用 jsencrypt 自行导入 + +```bash +npm i jsencrypt +``` + +然后调用 jsencrypt 即可 + +```js +import { JSEncrypt } from 'jsencrypt' + +//var pub_key = MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDC7kw8r6tq43pwApYvkJ5laljaN9BZb21TAIfT/vexbobzH7Q8SUdP5uDPXEBKzOjx2L28y7Xs1d9v3tdPfKI2LR7PAzWBmDMn8riHrDDNpUpJnlAGUqJG9ooPn8j7YNpcxCa1iybOlc2kEhmJn5uwoanQq+CA6agNkqly2H4j6wIDAQAB + +function jsencrypt(pwd, key) { + var RSA = new JSEncrypt() + RSA.setPublicKey(key) + return RSA.encrypt(pwd) +} +``` + +或 RSA.min.js + +:::details 展开代码 + +```js +function BarrettMu(m) { + this.modulus = biCopy(m) + this.k = biHighIndex(this.modulus) + 1 + var b2k = new BigInt() + b2k.digits[2 * this.k] = 1 + this.mu = biDivide(b2k, this.modulus) + this.bkplus1 = new BigInt() + this.bkplus1.digits[this.k + 1] = 1 + this.modulo = BarrettMu_modulo + this.multiplyMod = BarrettMu_multiplyMod + this.powMod = BarrettMu_powMod +} +function BarrettMu_modulo(x) { + var q1 = biDivideByRadixPower(x, this.k - 1) + var q2 = biMultiply(q1, this.mu) + var q3 = biDivideByRadixPower(q2, this.k + 1) + var r1 = biModuloByRadixPower(x, this.k + 1) + var r2term = biMultiply(q3, this.modulus) + var r2 = biModuloByRadixPower(r2term, this.k + 1) + var r = biSubtract(r1, r2) + if (r.isNeg) { + r = biAdd(r, this.bkplus1) + } + var rgtem = biCompare(r, this.modulus) >= 0 + while (rgtem) { + r = biSubtract(r, this.modulus) + rgtem = biCompare(r, this.modulus) >= 0 + } + return r +} +function BarrettMu_multiplyMod(x, y) { + var xy = biMultiply(x, y) + return this.modulo(xy) +} +function BarrettMu_powMod(x, y) { + var result = new BigInt() + result.digits[0] = 1 + var a = x + var k = y + while (true) { + if ((k.digits[0] & 1) != 0) result = this.multiplyMod(result, a) + k = biShiftRight(k, 1) + if (k.digits[0] == 0 && biHighIndex(k) == 0) break + a = this.multiplyMod(a, a) + } + return result +} +var biRadixBase = 2 +var biRadixBits = 16 +var bitsPerDigit = biRadixBits +var biRadix = 1 << 16 +var biHalfRadix = biRadix >>> 1 +var biRadixSquared = biRadix * biRadix +var maxDigitVal = biRadix - 1 +var maxInteger = 9999999999999998 +var maxDigits +var ZERO_ARRAY +var bigZero, bigOne +function setMaxDigits(value) { + maxDigits = value + ZERO_ARRAY = new Array(maxDigits) + for (var iza = 0; iza < ZERO_ARRAY.length; iza++) ZERO_ARRAY[iza] = 0 + bigZero = new BigInt() + bigOne = new BigInt() + bigOne.digits[0] = 1 +} +setMaxDigits(20) +var dpl10 = 15 +var lr10 = biFromNumber(1000000000000000) +function BigInt(flag) { + if (typeof flag == 'boolean' && flag == true) { + this.digits = null + } else { + this.digits = ZERO_ARRAY.slice(0) + } + this.isNeg = false +} +function biFromDecimal(s) { + var isNeg = s.charAt(0) == '-' + var i = isNeg ? 1 : 0 + var result + while (i < s.length && s.charAt(i) == '0') ++i + if (i == s.length) { + result = new BigInt() + } else { + var digitCount = s.length - i + var fgl = digitCount % dpl10 + if (fgl == 0) fgl = dpl10 + result = biFromNumber(Number(s.substr(i, fgl))) + i += fgl + while (i < s.length) { + result = biAdd(biMultiply(result, lr10), biFromNumber(Number(s.substr(i, dpl10)))) + i += dpl10 + } + result.isNeg = isNeg + } + return result +} +function biCopy(bi) { + var result = new BigInt(true) + result.digits = bi.digits.slice(0) + result.isNeg = bi.isNeg + return result +} +function biFromNumber(i) { + var result = new BigInt() + result.isNeg = i < 0 + i = Math.abs(i) + var j = 0 + while (i > 0) { + result.digits[j++] = i & maxDigitVal + i >>= biRadixBits + } + return result +} +function reverseStr(s) { + var result = '' + for (var i = s.length - 1; i > -1; --i) { + result += s.charAt(i) + } + return result +} +var hexatrigesimalToChar = new Array( + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i', + 'j', + 'k', + 'l', + 'm', + 'n', + 'o', + 'p', + 'q', + 'r', + 's', + 't', + 'u', + 'v', + 'w', + 'x', + 'y', + 'z', +) +function biToString(x, radix) { + var b = new BigInt() + b.digits[0] = radix + var qr = biDivideModulo(x, b) + var result = hexatrigesimalToChar[qr[1].digits[0]] + while (biCompare(qr[0], bigZero) == 1) { + qr = biDivideModulo(qr[0], b) + digit = qr[1].digits[0] + result += hexatrigesimalToChar[qr[1].digits[0]] + } + return (x.isNeg ? '-' : '') + reverseStr(result) +} +function biToDecimal(x) { + var b = new BigInt() + b.digits[0] = 10 + var qr = biDivideModulo(x, b) + var result = String(qr[1].digits[0]) + while (biCompare(qr[0], bigZero) == 1) { + qr = biDivideModulo(qr[0], b) + result += String(qr[1].digits[0]) + } + return (x.isNeg ? '-' : '') + reverseStr(result) +} +var hexToChar = new Array( + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', +) +function digitToHex(n) { + var mask = 0xf + var result = '' + for (i = 0; i < 4; ++i) { + result += hexToChar[n & mask] + n >>>= 4 + } + return reverseStr(result) +} +function biToHex(x) { + var result = '' + var n = biHighIndex(x) + for (var i = biHighIndex(x); i > -1; --i) { + result += digitToHex(x.digits[i]) + } + return result +} +function charToHex(c) { + var ZERO = 48 + var NINE = ZERO + 9 + var littleA = 97 + var littleZ = littleA + 25 + var bigA = 65 + var bigZ = 65 + 25 + var result + if (c >= ZERO && c <= NINE) { + result = c - ZERO + } else if (c >= bigA && c <= bigZ) { + result = 10 + c - bigA + } else if (c >= littleA && c <= littleZ) { + result = 10 + c - littleA + } else { + result = 0 + } + return result +} +function hexToDigit(s) { + var result = 0 + var sl = Math.min(s.length, 4) + for (var i = 0; i < sl; ++i) { + result <<= 4 + result |= charToHex(s.charCodeAt(i)) + } + return result +} +function biFromHex(s) { + var result = new BigInt() + var sl = s.length + for (var i = sl, j = 0; i > 0; i -= 4, ++j) { + result.digits[j] = hexToDigit(s.substr(Math.max(i - 4, 0), Math.min(i, 4))) + } + return result +} +function biFromString(s, radix) { + var isNeg = s.charAt(0) == '-' + var istop = isNeg ? 1 : 0 + var result = new BigInt() + var place = new BigInt() + place.digits[0] = 1 + for (var i = s.length - 1; i >= istop; i--) { + var c = s.charCodeAt(i) + var digit = charToHex(c) + var biDigit = biMultiplyDigit(place, digit) + result = biAdd(result, biDigit) + place = biMultiplyDigit(place, radix) + } + result.isNeg = isNeg + return result +} +function biToBytes(x) { + var result = '' + for (var i = biHighIndex(x); i > -1; --i) { + result += digitToBytes(x.digits[i]) + } + return result +} +function digitToBytes(n) { + var c1 = String.fromCharCode(n & 0xff) + n >>>= 8 + var c2 = String.fromCharCode(n & 0xff) + return c2 + c1 +} +function biDump(b) { + return (b.isNeg ? '-' : '') + b.digits.join(' ') +} +function biAdd(x, y) { + var result + if (x.isNeg != y.isNeg) { + y.isNeg = !y.isNeg + result = biSubtract(x, y) + y.isNeg = !y.isNeg + } else { + result = new BigInt() + var c = 0 + var n + for (var i = 0; i < x.digits.length; ++i) { + n = x.digits[i] + y.digits[i] + c + result.digits[i] = n & 0xffff + c = Number(n >= biRadix) + } + result.isNeg = x.isNeg + } + return result +} +function biSubtract(x, y) { + var result + if (x.isNeg != y.isNeg) { + y.isNeg = !y.isNeg + result = biAdd(x, y) + y.isNeg = !y.isNeg + } else { + result = new BigInt() + var n, c + c = 0 + for (var i = 0; i < x.digits.length; ++i) { + n = x.digits[i] - y.digits[i] + c + result.digits[i] = n & 0xffff + if (result.digits[i] < 0) result.digits[i] += biRadix + c = 0 - Number(n < 0) + } + if (c == -1) { + c = 0 + for (var i = 0; i < x.digits.length; ++i) { + n = 0 - result.digits[i] + c + result.digits[i] = n & 0xffff + if (result.digits[i] < 0) result.digits[i] += biRadix + c = 0 - Number(n < 0) + } + result.isNeg = !x.isNeg + } else { + result.isNeg = x.isNeg + } + } + return result +} +function biHighIndex(x) { + var result = x.digits.length - 1 + while (result > 0 && x.digits[result] == 0) --result + return result +} +function biNumBits(x) { + var n = biHighIndex(x) + var d = x.digits[n] + var m = (n + 1) * bitsPerDigit + var result + for (result = m; result > m - bitsPerDigit; --result) { + if ((d & 0x8000) != 0) break + d <<= 1 + } + return result +} +function biMultiply(x, y) { + var result = new BigInt() + var c + var n = biHighIndex(x) + var t = biHighIndex(y) + var u, uv, k + for (var i = 0; i <= t; ++i) { + c = 0 + k = i + for (j = 0; j <= n; ++j, ++k) { + uv = result.digits[k] + x.digits[j] * y.digits[i] + c + result.digits[k] = uv & maxDigitVal + c = uv >>> biRadixBits + } + result.digits[i + n + 1] = c + } + result.isNeg = x.isNeg != y.isNeg + return result +} +function biMultiplyDigit(x, y) { + var n, c, uv + result = new BigInt() + n = biHighIndex(x) + c = 0 + for (var j = 0; j <= n; ++j) { + uv = result.digits[j] + x.digits[j] * y + c + result.digits[j] = uv & maxDigitVal + c = uv >>> biRadixBits + } + result.digits[1 + n] = c + return result +} +function arrayCopy(src, srcStart, dest, destStart, n) { + var m = Math.min(srcStart + n, src.length) + for (var i = srcStart, j = destStart; i < m; ++i, ++j) { + dest[j] = src[i] + } +} +var highBitMasks = new Array( + 0x0000, + 0x8000, + 0xc000, + 0xe000, + 0xf000, + 0xf800, + 0xfc00, + 0xfe00, + 0xff00, + 0xff80, + 0xffc0, + 0xffe0, + 0xfff0, + 0xfff8, + 0xfffc, + 0xfffe, + 0xffff, +) +function biShiftLeft(x, n) { + var digitCount = Math.floor(n / bitsPerDigit) + var result = new BigInt() + arrayCopy(x.digits, 0, result.digits, digitCount, result.digits.length - digitCount) + var bits = n % bitsPerDigit + var rightBits = bitsPerDigit - bits + for (var i = result.digits.length - 1, i1 = i - 1; i > 0; --i, --i1) { + result.digits[i] = + ((result.digits[i] << bits) & maxDigitVal) | + ((result.digits[i1] & highBitMasks[bits]) >>> rightBits) + } + result.digits[0] = (result.digits[i] << bits) & maxDigitVal + result.isNeg = x.isNeg + return result +} +var lowBitMasks = new Array( + 0x0000, + 0x0001, + 0x0003, + 0x0007, + 0x000f, + 0x001f, + 0x003f, + 0x007f, + 0x00ff, + 0x01ff, + 0x03ff, + 0x07ff, + 0x0fff, + 0x1fff, + 0x3fff, + 0x7fff, + 0xffff, +) +function biShiftRight(x, n) { + var digitCount = Math.floor(n / bitsPerDigit) + var result = new BigInt() + arrayCopy(x.digits, digitCount, result.digits, 0, x.digits.length - digitCount) + var bits = n % bitsPerDigit + var leftBits = bitsPerDigit - bits + for (var i = 0, i1 = i + 1; i < result.digits.length - 1; ++i, ++i1) { + result.digits[i] = + (result.digits[i] >>> bits) | ((result.digits[i1] & lowBitMasks[bits]) << leftBits) + } + result.digits[result.digits.length - 1] >>>= bits + result.isNeg = x.isNeg + return result +} +function biMultiplyByRadixPower(x, n) { + var result = new BigInt() + arrayCopy(x.digits, 0, result.digits, n, result.digits.length - n) + return result +} +function biDivideByRadixPower(x, n) { + var result = new BigInt() + arrayCopy(x.digits, n, result.digits, 0, result.digits.length - n) + return result +} +function biModuloByRadixPower(x, n) { + var result = new BigInt() + arrayCopy(x.digits, 0, result.digits, 0, n) + return result +} +function biCompare(x, y) { + if (x.isNeg != y.isNeg) { + return 1 - 2 * Number(x.isNeg) + } + for (var i = x.digits.length - 1; i >= 0; --i) { + if (x.digits[i] != y.digits[i]) { + if (x.isNeg) { + return 1 - 2 * Number(x.digits[i] > y.digits[i]) + } else { + return 1 - 2 * Number(x.digits[i] < y.digits[i]) + } + } + } + return 0 +} +function biDivideModulo(x, y) { + var nb = biNumBits(x) + var tb = biNumBits(y) + var origYIsNeg = y.isNeg + var q, r + if (nb < tb) { + if (x.isNeg) { + q = biCopy(bigOne) + q.isNeg = !y.isNeg + x.isNeg = false + y.isNeg = false + r = biSubtract(y, x) + x.isNeg = true + y.isNeg = origYIsNeg + } else { + q = new BigInt() + r = biCopy(x) + } + return new Array(q, r) + } + q = new BigInt() + r = x + var t = Math.ceil(tb / bitsPerDigit) - 1 + var lambda = 0 + while (y.digits[t] < biHalfRadix) { + y = biShiftLeft(y, 1) + ++lambda + ++tb + t = Math.ceil(tb / bitsPerDigit) - 1 + } + r = biShiftLeft(r, lambda) + nb += lambda + var n = Math.ceil(nb / bitsPerDigit) - 1 + var b = biMultiplyByRadixPower(y, n - t) + while (biCompare(r, b) != -1) { + ++q.digits[n - t] + r = biSubtract(r, b) + } + for (var i = n; i > t; --i) { + var ri = i >= r.digits.length ? 0 : r.digits[i] + var ri1 = i - 1 >= r.digits.length ? 0 : r.digits[i - 1] + var ri2 = i - 2 >= r.digits.length ? 0 : r.digits[i - 2] + var yt = t >= y.digits.length ? 0 : y.digits[t] + var yt1 = t - 1 >= y.digits.length ? 0 : y.digits[t - 1] + if (ri == yt) { + q.digits[i - t - 1] = maxDigitVal + } else { + q.digits[i - t - 1] = Math.floor((ri * biRadix + ri1) / yt) + } + var c1 = q.digits[i - t - 1] * (yt * biRadix + yt1) + var c2 = ri * biRadixSquared + (ri1 * biRadix + ri2) + while (c1 > c2) { + --q.digits[i - t - 1] + c1 = q.digits[i - t - 1] * ((yt * biRadix) | yt1) + c2 = ri * biRadix * biRadix + (ri1 * biRadix + ri2) + } + b = biMultiplyByRadixPower(y, i - t - 1) + r = biSubtract(r, biMultiplyDigit(b, q.digits[i - t - 1])) + if (r.isNeg) { + r = biAdd(r, b) + --q.digits[i - t - 1] + } + } + r = biShiftRight(r, lambda) + q.isNeg = x.isNeg != origYIsNeg + if (x.isNeg) { + if (origYIsNeg) { + q = biAdd(q, bigOne) + } else { + q = biSubtract(q, bigOne) + } + y = biShiftRight(y, lambda) + r = biSubtract(y, r) + } + if (r.digits[0] == 0 && biHighIndex(r) == 0) r.isNeg = false + return new Array(q, r) +} +function biDivide(x, y) { + return biDivideModulo(x, y)[0] +} +function biModulo(x, y) { + return biDivideModulo(x, y)[1] +} +function biMultiplyMod(x, y, m) { + return biModulo(biMultiply(x, y), m) +} +function biPow(x, y) { + var result = bigOne + var a = x + while (true) { + if ((y & 1) != 0) result = biMultiply(result, a) + y >>= 1 + if (y == 0) break + a = biMultiply(a, a) + } + return result +} +function biPowMod(x, y, m) { + var result = bigOne + var a = x + var k = y + while (true) { + if ((k.digits[0] & 1) != 0) result = biMultiplyMod(result, a, m) + k = biShiftRight(k, 1) + if (k.digits[0] == 0 && biHighIndex(k) == 0) break + a = biMultiplyMod(a, a, m) + } + return result +} +var RSAAPP = {} +RSAAPP.NoPadding = 'NoPadding' +RSAAPP.PKCS1Padding = 'PKCS1Padding' +RSAAPP.RawEncoding = 'RawEncoding' +RSAAPP.NumericEncoding = 'NumericEncoding' +function RSAKeyPair(encryptionExponent, decryptionExponent, modulus, keylen) { + this.e = biFromHex(encryptionExponent) + this.d = biFromHex(decryptionExponent) + this.m = biFromHex(modulus) + if (typeof keylen != 'number') { + this.chunkSize = 2 * biHighIndex(this.m) + } else { + this.chunkSize = keylen / 8 + } + this.radix = 16 + this.barrett = new BarrettMu(this.m) +} +function encryptedString(key, s, pad, encoding) { + var a = new Array() + var sl = s.length + var i, j, k + var padtype + var encodingtype + var rpad + var al + var result = '' + var block + var crypt + var text + if (typeof pad == 'string') { + if (pad == RSAAPP.NoPadding) { + padtype = 1 + } else if (pad == RSAAPP.PKCS1Padding) { + padtype = 2 + } else { + padtype = 0 + } + } else { + padtype = 0 + } + if (typeof encoding == 'string' && encoding == RSAAPP.RawEncoding) { + encodingtype = 1 + } else { + encodingtype = 0 + } + if (padtype == 1) { + if (sl > key.chunkSize) { + sl = key.chunkSize + } + } else if (padtype == 2) { + if (sl > key.chunkSize - 11) { + sl = key.chunkSize - 11 + } + } + i = 0 + if (padtype == 2) { + j = sl - 1 + } else { + j = key.chunkSize - 1 + } + while (i < sl) { + if (padtype) { + a[j] = s.charCodeAt(i) + } else { + a[i] = s.charCodeAt(i) + } + i++ + j-- + } + if (padtype == 1) { + i = 0 + } + j = key.chunkSize - (sl % key.chunkSize) + while (j > 0) { + if (padtype == 2) { + rpad = Math.floor(Math.random() * 256) + while (!rpad) { + rpad = Math.floor(Math.random() * 256) + } + a[i] = rpad + } else { + a[i] = 0 + } + i++ + j-- + } + if (padtype == 2) { + a[sl] = 0 + a[key.chunkSize - 2] = 2 + a[key.chunkSize - 1] = 0 + } + al = a.length + for (i = 0; i < al; i += key.chunkSize) { + block = new BigInt() + j = 0 + for (k = i; k < i + key.chunkSize; ++j) { + block.digits[j] = a[k++] + block.digits[j] += a[k++] << 8 + } + crypt = key.barrett.powMod(block, key.e) + if (encodingtype == 1) { + text = biToBytes(crypt) + } else { + text = key.radix == 16 ? biToHex(crypt) : biToString(crypt, key.radix) + } + result += text + } + return result +} +function decryptedString(key, c) { + var blocks = c.split(' ') + var b + var i, j + var bi + var result = '' + for (i = 0; i < blocks.length; ++i) { + if (key.radix == 16) { + bi = biFromHex(blocks[i]) + } else { + bi = biFromString(blocks[i], key.radix) + } + b = key.barrett.powMod(bi, key.d) + for (j = 0; j <= biHighIndex(b); ++j) { + result += String.fromCharCode(b.digits[j] & 255, b.digits[j] >> 8) + } + } + if (result.charCodeAt(result.length - 1) == 0) { + result = result.substring(0, result.length - 1) + } + return result +} +//ab3320e630287cb133b92c6cb4735a0f43028bb3e39cc5eec9960d493a018b61 +function getRSA(password, pubkey) { + setMaxDigits(131) + var key = new RSAKeyPair('10001', '', pubkey) + return encryptedString(key, password) +} +``` + +::: + +```js +// RSA.min.js +function getRSA(password, pubkey) { + setMaxDigits(131) + var key = new RSAKeyPair('10001', '', pubkey) + return encryptedString(key, password) +} +``` + +网上也有很多关于 RSA 的调用,可以自行百度,这里就以 JS 为例,java 与 python 就不做过多赘述 + +## 对称加密与非对称加密比较 + +对称加密的明文是没有长度限制的,并且加密速度快,而非对称加密算法加密是有长度限制的,而且速度慢。 + +### 对称加密与非对称加密结合使用 + +如上面说的,两者各有所优,各有所弊。可以延伸出一个常用的加密套路 + +1. **随机**生成 AES/DES/3DES 的密钥 +2. 这个密钥用于 AES/DES/3DES 加密数据 +3. **RSA 对密钥进行加密** +4. 将 RSA 对密钥加密后的值与 AES/DES/3DES 加密后数据的数据发送给服务器 + +首先先说下服务器能否解密,答案是肯定能的 + +假设我随机生成的密钥 key 为`0123456789abcdef`,明文是`kuizuo`。首先用 AES/EBC/PKCS5Padding 填充将密钥加密得到`gujzY/9NeeM=`,然后通过 RSA/None/NOPadding 公钥 puk 加密得到加密结果`2c9717c0a002fade7878fb3a590531aa165f7859c3663483884e3c09359e4a4360908198b45771f0a58c9e8824aac8bd8cbcea911a0444c2ddfcee9fdb8fd8258a8950ae43d24da9aaa5f97247502b9f1bc0fc42ae8f8771ba27bb2d8691ef389e829a16929e84a588fa91ecb9af1225ad026945da3628b25b9b5536509c0d28`,然后将这两串数据发送给服务器,如: + +```json +{"data" : "gujzY/9NeeM=" ,"secret" : “2c9717c0a002fade7878fb3a590531aa165f7859c3663483884e3c09359e4a4360908198b45771f0a58c9e8824aac8bd8cbcea911a0444c2ddfcee9fdb8fd8258a8950ae43d24da9aaa5f97247502b9f1bc0fc42ae8f8771ba27bb2d8691ef389e829a16929e84a588fa91ecb9af1225ad026945da3628b25b9b5536509c0d28”} +``` + +服务器这边由于知道 secret 是 AES 的加密后的密钥 Key,同时服务器这边又有公钥和私钥,自然而然能将 secret 解出值为`0123456789abcdef`,接着知道了 AES 的密钥 Key,有知道 data 加密后的数据,得到客户端发送的数据`kuizuo`就轻而易举了。 + +当然,实际过程中不会像上面那么显而易见,往往是更复杂的,这里也只是举个例子讲的清晰。 + +而在逆向中遇到这类情况的,通常有变通的方法,因为 AES 的密钥 Key 是随机生成的,但是每次发送给服务器服务器都能正常的解密出来,那么我能不能伪造这样的请求,固定随机的 Key 和 secret,每次要加密的时候就用这两个固定的值来加密发送给服务器不就行了?对的,一般来说是完全可以的,但是也不妨服务器是否会对每次加密后的的 Key 进行比对,如果有那么肯定不行。还是常规去找对应的加密点,把 js 代码扣下来用吧。 + +### 结语 + +关于密码学,实际上还有很多内容没有概述到,像 RSA 中对私钥的处理我也并没有深入去了解,只知道如何用 RSA 的公钥,然后通过调用 js 来达到加密结果发送给服务器,伪造 HTTP 请求的,不过对于逆向或者一些开发使用,会就这些的话够应付了,至于如何找加密点,也就是看逆向能力,还有熟能生巧了。 + +在我前端与后端数据交互中,我也简单了利用了上面所说的加密算法,然而也只是简单的加密,增加点模拟请求的门槛罢了。 diff --git "a/docs/skill/reverse/web/JS\344\273\243\347\240\201\346\267\267\346\267\206\344\270\216\350\277\230\345\216\237.md" "b/docs/skill/reverse/web/JS\344\273\243\347\240\201\346\267\267\346\267\206\344\270\216\350\277\230\345\216\237.md" new file mode 100644 index 0000000..6b5acc5 --- /dev/null +++ "b/docs/skill/reverse/web/JS\344\273\243\347\240\201\346\267\267\346\267\206\344\270\216\350\277\230\345\216\237.md" @@ -0,0 +1,14 @@ +--- +id: js-code-obfuscation-and-reverse +slug: /js-code-obfuscation-and-reverse +title: JS代码混淆与还原 +authors: kuizuo +tags: [javascript, deobfuscator] +keywords: [javascript, deobfuscator] +--- + + + +[JS 代码之还原](/blog/js-code-deobfuscator) + +[JS 代码之混淆](/blog/js-code-obfuscator) diff --git "a/docs/skill/reverse/web/\345\260\217\347\250\213\345\272\217\345\246\202\344\275\225\345\217\215\347\274\226\350\257\221.md" "b/docs/skill/reverse/web/\345\260\217\347\250\213\345\272\217\345\246\202\344\275\225\345\217\215\347\274\226\350\257\221.md" new file mode 100644 index 0000000..ddada8e --- /dev/null +++ "b/docs/skill/reverse/web/\345\260\217\347\250\213\345\272\217\345\246\202\344\275\225\345\217\215\347\274\226\350\257\221.md" @@ -0,0 +1,97 @@ +--- +id: how-to-decompiling-miniprogram +slug: /how-to-decompiling-miniprogram +title: 小程序如何反编译 +date: 2021-08-30 +authors: kuizuo +tags: [reverse, decompilation, miniprogram] +keywords: [reverse, decompilation, miniprogram] +--- + + + +## 环境安装 + +### 微信开发者工具 + +下载地址:[稳定版 Stable Build | 微信开放文档 (qq.com)](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html) + +只支持 windows 和 mac + +### nodejs + +[下载 | Node.js (nodejs.org)](https://nodejs.org/zh-cn/download/) + +### 模拟器 + +这里推荐用雷电模拟器,真机也行,只要能登录微信都可以 + +### 解包工具 + +[xuedingmiaojun/wxappUnpacker: 小程序反编译(支持分包) (github.com)](https://github.com/xuedingmiaojun/wxappUnpacker) + +选择一个文件夹,通过如下命令安装 + +```bash +git clone https://github.com/xuedingmiaojun/wxappUnpacker + +cd wxappUnpacker + +npm install +``` + +## 操作步骤 + +首先打开模拟器,安装下微信(获取小程序的包)和 RE 文件管理器或自带文件管理器(找到小程序的包并导出) + +登录微信,然后进入要反编译的小程序,打开 RE 文件管理器,找到路径`/data/data/com.tencent.mm/MicroMsg/{{哈希值}}/appbrand/pkg/xxxxx.wxapkg` 对应程序包。 + +如:`/data/data/com.tencent.mm/MicroMsg/a5e1a6f4438d7cad5182e77248180f50/appbrand/pkg/xxxxx.wxapkg`,具体哈希值需要根据生成文件时间来判断 + +![image-20210831222820533](https://img.kuizuo.cn/image-20210831222820533.png) + +如果是模拟器的话,可以使用 QQ 或者是模拟器自带的文件共享器,将文件导入至电脑,真机的话直接连接电脑传输文件即可。 + +:::warning 注:本人测试中 Root 过的机型是无法登录微信,要么一直转圈圈要么滑块加载不出来,也许是模拟器的问题或是之前已经安装过一些插件导致的。如果登录不上可选择关闭 Root 登录微信后,在打开 Root。 + +::: + +接着打开解包工具文件夹,打开控制台窗口输入 `node wuWxapkg.js `,运行结果如下图 + +![image-20210830034643420](https://img.kuizuo.cn/image-20210830034643420.png) + +会在 wxapkg 文件下生成与之对应的文件夹,接着打开微信开发者工具,选择导入项目,选择对应文件夹,选择测试号 + +![image-20210830034848958](https://img.kuizuo.cn/image-20210830034848958.png) + +导入即可运行,接着就可以开始分析小程序的参数和页面样式了。 + +![image-20210831222521295](https://img.kuizuo.cn/image-20210831222521295.png) + +## 一键导包工具 + +[xuedingmiaojun/mp-unpack: 基于 electron-vue 开发的跨平台微信小程序自助解包(反编译)客户端 (github.com)](https://github.com/xuedingmiaojun/mp-unpack) + +有个基于 electron-vue 开发的一键导包工具,具体的话可以查看对应源码,需要的可自行编译,不过也已经提供各平台对应的应用程序。 + +![image-20210831222006013](https://img.kuizuo.cn/image-20210831222006013.png) + +不过是因为 electron 写的,且不支持选择路径,所以 C 盘会瞬间多个 150M 左右,安装包大约 40M,除此外的话使用体验还是非常好的。(至少可以不用输入命令) + +## 一些问题 + +### Q:电脑也能运行小程序,那能不能从电脑上导包呢 + +A:能,电脑导包的路径为`C:\Users\{{电脑用户名}}\Documents\WeChat Files\Applet\{{小程序AppID}}\{{随机产生的数字}}\__APP__.wxapkg`,然后按照如上步骤,就可反编译小程序,不过电脑导包可能会出现**magic number is not correct** 的错误 (本人测试是这样的),毕竟小程序主要运行在手机上,所以还是推荐手机导包。 + +### Q:反编译后的小程序能重新打包后在发布吗 + +A: 理论上是可以的,但不能保证反编译后的小程序就一定能重编译成功,毕竟有一些插件等等,编译都不能编译,就别谈打包了。并且反编译后的代码都是经过压缩的,阅读性略差,不过如果是 uniapp 编写的话,除了 js 文件外,其余基本原封不动(前提没做混淆的情况下),所以要仿一个页面的话,完全可以新建一个 Vue 文件,然后将其 html 与 css 添加至对应模板处,然后 js 就只能扣去部分代码,至于接口的话都是别人的,所以一般分析下页面样式和参数就差不多了。 + +我写这篇的时候目的就是为了查看别人小程序的样式而已。 + +## 最后 + +小程序反编译比我想象中的简单,相对于安卓与 windows 程序反编译的话(当然也可能是我逆向玩多了),并且几乎能无缝运行,不过小程序也算是用前端技术写的了,网页 F12 打开控制台就可看到源码,小程序的话,em。。。 + +由于我是接触过 Uniapp 开发的,并且我自己所编译的小程序也是 Uniapp 开发的,所以一些相关的样式就自然熟悉不过了,将其几个页面转化为 Vue 代码也算比较轻松了 diff --git "a/docs/skill/web/browser/\346\265\217\350\247\210\345\231\250\345\244\215\345\210\266Console\351\235\242\346\235\277\350\276\223\345\207\272.md" "b/docs/skill/web/browser/\346\265\217\350\247\210\345\231\250\345\244\215\345\210\266Console\351\235\242\346\235\277\350\276\223\345\207\272.md" new file mode 100644 index 0000000..6613c1c --- /dev/null +++ "b/docs/skill/web/browser/\346\265\217\350\247\210\345\231\250\345\244\215\345\210\266Console\351\235\242\346\235\277\350\276\223\345\207\272.md" @@ -0,0 +1,58 @@ +--- +id: brower-copy-console-panel-output +slug: /brower-copy-console-panel-output +title: 浏览器复制Console面板输出 +date: 2021-12-07 +authors: kuizuo +tags: [javascript, browser, console] +keywords: [javascript, browser, console] +--- + + + +在分析一个网站的时候,要将控制台(Console 面板)中的大数组(长度大约 100)复制到本地上进行调用 + +```javascript +// 模拟生成的数据 +let data = Array.from({ length: 100 }, (v, i) => ({ index: i, value: Math.random() })) +``` + +![image-20211207122529224](https://img.kuizuo.cn/image-20211207122529224.png) + +如果直接鼠标选中复制,是得不到想要的结果的。而我之前的做法都是使用 JSON.stringify() 将其转为 json 文本格式,然后复制到剪贴板 + +![image-20211207124755461](https://img.kuizuo.cn/image-20211207124755461.png) + +很明显 这种方法缺陷很大,首先复制出的结果是一个 JSON 格式数据,其次万一数据很长,复制也很费力,也需要按 Ctrl + C 与 Ctrl + V。无意间刷到个浏览器 API,有个用于复制 js 数据方法----`copy`,使用也特别简单 + +``` +copy(data) +``` + +此时剪贴板的内容便是 data 的原生 js 对象(格式化后),像下面这样 + +```javascript +;[ + { + index: 0, + value: 0.3875488580101616, + }, + { + index: 1, + value: 0.8932296395340085, + }, + { + index: 2, + value: 0.14681203758288164, + }, + { + index: 3, + value: 0.374650909955935, + }, + // ... + { + index: 99, + value: 0.31823645771583875, + }, +] +``` diff --git "a/docs/skill/web/css/\344\270\200\344\272\233CSS\345\261\236\346\200\247.md" "b/docs/skill/web/css/\344\270\200\344\272\233CSS\345\261\236\346\200\247.md" new file mode 100644 index 0000000..88aba8d --- /dev/null +++ "b/docs/skill/web/css/\344\270\200\344\272\233CSS\345\261\236\346\200\247.md" @@ -0,0 +1,82 @@ +--- +id: css-properties +slug: /css-properties +title: 一些CSS属性 +date: 2022-08-12 +authors: kuizuo +tags: [css] +keywords: [css] +--- + +最近在写一些 CSS 样例,可以在 [前端示例代码库](https://example.kuizuo.cn/) 中查看,后续也会把一些灵感和设计放在这上面,不过这里主要介绍我之前没怎么用到过的一些 CSS 属性(奇技淫巧),通过这些特性能非常方便的实现一些需求,不会做过多使用介绍,具体可查看 [MDN](https://developer.mozilla.org/zh-CN/docs/Web/CSS) 与 [示例源代码](https://github.com/kuizuo/example)。 + +可在这个网站 [Can I use](https://caniuse.com/) 查看 CSS 兼容情况。 + + + +## [clip-path](https://developer.mozilla.org/zh-CN/docs/Web/CSS/clip-path) + +如果要实现多边形的话,之前的做法通常是使用 border 来实现的,但是用 border 来实现的是比较复杂的,最关键的是不好用。[**`clip-path`**](https://developer.mozilla.org/zh-CN/docs/Web/CSS/clip-path) CSS 属性使用裁剪方式创建元素的可显示区域。可以在这个网站 [Clippy — CSS clip-path 生成器](https://www.html.cn/tool/css-clip-path/) 勾勒出所要的图形,然后将其添加至 css 属性即可。 + +![](https://secure2.wostatic.cn/static/qs1brMUAga5NbQhpbMU5d6/image.png) + +## [linear-gradient](https://developer.mozilla.org/zh-CN/docs/Web/CSS/gradient/linear-gradient) + +线性渐变颜色,也是渐变色用到最多的一个属性,此外还有径向 [`radial-gradient`](https://developer.mozilla.org/zh-CN/docs/Web/CSS/gradient/radial-gradient)与圆锥[conic-gradient](https://developer.mozilla.org/zh-CN/docs/Web/CSS/gradient/conic-gradient) + +```css +/* 渐变轴为45度,从蓝色渐变到红色 */ +linear-gradient(45deg, blue, red); + +/* 从右下到左上、从蓝色渐变到红色 */ +linear-gradient(to left top, blue, red); + +/* 从下到上,从蓝色开始渐变、到高度 40% 位置是绿色渐变开始、最后以红色结束 */ +linear-gradient(0deg, blue, green 40%, red); +``` + +不过这个属性只适用于背景(background)颜色,如果想要在文字,边框,阴影中使用渐变颜色,通常需要先设置渐变背景颜色,然后通过一些 css 属性“裁剪”出相应的部分。 + +这里的“裁剪”主要用到 background-clip 属性,如果想要裁剪出文字可以 `background-clip: text`配合文字`color: transparent`,要裁剪出边框可以 `background-clip: content-box, border-box;`,在给背景颜色添加原背景色。 + +## [backdrop-filter](https://developer.mozilla.org/zh-CN/docs/Web/CSS/backdrop-filter) + +**`backdrop-filter`** [CSS](https://developer.mozilla.org/zh-CN/docs/Web/CSS) 属性可以让你为一个元素后面区域添加图形效果(如模糊或颜色偏移)。因为它适用于元素*背后*的所有元素,为了看到效果,必须使元素或其背景至少部分透明。 + +为背景添加滤镜,比如毛玻璃效果 `backdrop-filter: blur(5px);` 、灰度`backdrop-filter: grayscale(1);`等等。 + +再次之前要实现这类效果还需要使用[filter](https://developer.mozilla.org/zh-CN/docs/Web/CSS/filter)属性(兼容性更好),然后用伪元素双背景的方式来实现,实在过于麻烦。 + +# [-webkit-box-reflect](https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-box-reflect) + +可以实现类似水下倒影的效果,例如 + +``` +-webkit-box-reflect: below 0 linear-gradient(transparent, transparent, rgba(0, 0, 0, 0.4)); +``` + +## [aspect-ratio](https://developer.mozilla.org/zh-CN/docs/Web/CSS/aspect-ratio) + +例如 + +```css +aspect-ratio: 1 / 1; +aspect-ratio: 16 / 9; +aspect-ratio: 4 / 3; +``` + +## [gap](https://developer.mozilla.org/zh-CN/docs/Web/CSS/gap) + +这个属性我经常用到,主要**用于 flex 与 grid 布局中用于设置元素间的间隔**,原本这个属性是只有 grid 布局中才有的,后来在 flex 布局中也可以使用。 + +## [writing-mode](https://developer.mozilla.org/zh-CN/docs/Web/CSS/writing-mode) + +修改文字显示方向,例如竖行显示 `writing-mode: vertical-lr;` + +![img](https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode/screenshot_2020-02-05_21-04-30.png) + +## 总结 + +此外还有很多特性也在不断了解,每年也会有一些新的特性来帮助开发者更好的使用 css 去美化网站。 + +最直接的体验就是到 [CSS(层叠样式表) | MDN](https://developer.mozilla.org/zh-CN/docs/Web/CSS) ,在 MDN 上能查到关于前端开发技术的文档,可以说是前端的百科全书了。 diff --git "a/docs/skill/web/react/React\344\270\255Css\345\207\240\347\247\215\345\256\236\347\216\260\346\226\271\346\241\210.md" "b/docs/skill/web/react/React\344\270\255Css\345\207\240\347\247\215\345\256\236\347\216\260\346\226\271\346\241\210.md" new file mode 100644 index 0000000..455e09a --- /dev/null +++ "b/docs/skill/web/react/React\344\270\255Css\345\207\240\347\247\215\345\256\236\347\216\260\346\226\271\346\241\210.md" @@ -0,0 +1,334 @@ +--- +slug: react-css-implementation +title: React中Css几种实现方案 +date: 2022-01-14 +authors: kuizuo +tags: [react, css] +keywords: [react, css] +--- + + + +## 全局样式 + +与传统 html 标签类属性不同,react 中 class 必须编写为 className,比如 + +全局 css + +```jsx +.box { + background-color:red; + width:300px; + height:300px; +} +``` + +js + +```jsx +function Hello() { + return
hello react
+} + +ReactDOM.render(, document.getElementById('root')) +``` + +与传统在 html 标签定义 css 样式不同,因为这不是传统的 html 代码,而是 JSX,由于 class 作为关键字,无法作为标识符出现,比方说下面的代码将会报错。 + +```jsx +const { class } = { class: 'foo' } // Uncaught SyntaxError: Unexpected token } +const { className } = { className: 'foo' } +const { class: className } = { class: 'foo' } +``` + +关于官方也有对此问题回答 + +[有趣的话题,为什么 jsx 用 className 而不是 class](https://www.jackpu.com/you-qu-de-hua-ti-wei-shi-yao-jsxyong-classnameer-bu-shi-class/) + +所以把传统的 html 代码强行搬运到 react 中,如果带有 class 与 style 属性,那么将会报错。 + +## 内联样式 + +内联样式也得写成对象 key-value 形式,遇到-连字符,则需要大写,如 + +```jsx +function Hello() { + return ( +
+ hello react +
+ ) +} +``` + +CSS 的`font-size`属性要写成`fontSize`,这是 JavaScript 操作 CSS 属性的[约定](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Properties_Reference)。 + +其实{ } 可传入表达式,比方这里传入的就是`{ fontSize: "32px",textAlign: "center" }` 对象,也可以将其定义为一个变量传入。 + +但是写内联样式显得组丑陋影响阅读,并且样式不易于复用,同时伪元素与媒体查询无法实现,但是封装成类样式,又会影响到全局作用域,所以便有了局部样式`styles.module.css` 。 + +## 局部样式 CSS Modules + +Css Modules 并不是 React 专用解决方法,适用于所有使用 webpack 等打包工具的开发环境。以 webpack 为例,在 css-loader 的 options 里打开`modules:true` 选项即可使用 Css Modules。一般配置如下 + +```js +{ + loader: "css-loader", + options: { + importLoaders: 1, + modules: true, + localIdentName: "[name]__[local]___[hash:base64:5]" // 为了生成类名不是纯随机 + }, +}, + +``` + +然后通过 import 引入 + +```jsx +import styles from './styles.module.css' + +function Hello() { + return
hello react
+} +``` + +但如果是有多个局部样式,直接拼接是无效的(毕竟是个无效的表达式) + +```jsx +// 错误 +
+ +// 正确 +
+
+
+ +``` + +### classnames + +还可以通过 npm 包 classnames 来定义类名,如 + +```jsx +import classnames from 'classnames' +import styles from './styles.module.css' + +;
+``` + +最终都将编译为 + +```jsx +
+``` + +当然 classnames 还有多种方式添加,就不列举了,主要针对复杂样式,根据条件是否添加样式。 + +但是 在 Css Module 中,其实能发现挺多问题的 + +如果类名是带有-连字符`.table-size`那么就只能`styles["table-size"]` 来引用,并且都必须使用`{style.className}` 形式。 + +最主要的是,css 都写在 css 文件中,无法处理动态 css。 + +## CSS in JS + +由于 React 对 CSS 的封装非常弱,导致了一系列的第三方库,用来加强 CSS 操作,统称为 CSS in JS,有一种在 js 文件中写 css 代码的感觉,根据不完全统计,各种 CSS in JS 的库至少有[47 种](https://github.com/MicheleBertoli/css-in-js),其中比较出名的 便是[styled-components](https://link.juejin.cn/?target=https://github.com/styled-components/styled-components)。 + +```jsx +import styled from 'styled-components' + +// `` 和 () 一样可以作为js里作为函数接受参数的标志,这个做法类似于HOC,包裹一层css到h1上生成新组件Title +const Title = styled.h1` + font-size: 1.5em; + text-align: center; + color: palevioletred; + + span { + font-size: 2em; + } +` + +// 在充分使用css全部功能的同时,非常方便的实现动态css, 甚至可以直接调用props! +const Wrapper = styled.section` + padding: 4em; + background: ${(props) => props.bgColor}; +` + +const Button = styled.a` + /* This renders the buttons above... Edit me! */ + display: inline-block; + border-radius: 3px; + padding: 0.5rem 0; + margin: 0.5rem 1rem; + width: 11rem; + background: transparent; + color: white; + border: 2px solid white; + /* The GitHub button is a primary button + * edit this to target it specifically! */ + ${(props) => + props.primary && + css` + background: white; + color: palevioletred; + `} +` + +const App = () => ( + + + <span>Hello World</span>, this is my first styled component! + + + +) +``` + +像上面的 Title,Wrapper,Button 都是组件,Title 本质就是一个 h1 标签,在通过模板字符串编写局部 css 样式。 + +能直接编写子元素的样式,以及`& :hover`等 Sass 语法。 + +根据传入属性,在 css 中使用,Wrapper 传入背景颜色属性,Button 判断是否为 primary。 + +并且能方便的给暴露`className` props 的三方 UI 库上样式: + +```jsx +const StyledButton = styled(Button)` ... ` +``` + +## styled-jsx + +[vercel/styled-jsx: Full CSS support for JSX without compromises (github.com)](https://github.com/vercel/styled-jsx) + +styled-jsx 概括第一印象就是 React css 的 vue 解决。`yarn add styled-jsx` 安装后,不用`import`,而是一个 babel 插件,`.babelrc`配置: + +```JavaScript +{ + "plugins": [ + "styled-jsx/babel" + ] +} + +``` + +使用 + +```jsx + +render () { + return
+
+
A0
+
B0
+
+ +
; +} + +``` + +只会作用到同级标签作用域,可以说是一种另类的内联样式了,如果不喜欢将样式写在 render 里,styled-jsx 提供了一个 `css` 的工具函数: + +```jsx +import css from 'styled-jsx/css' + +export default () => ( +
+ + +
+) + +const button = css` + button { + color: hotpink; + } +` +``` + +补充:现在我更推荐使用 Emotion。 + +## 原子类 + +简单说,就是将常用的 css 样式都封装完,只需要在 class 中引入即可 + +这里选用当红框架 [Tailwind CSS](https://www.tailwindcss.cn/) 作为演示。 + +比方说 flex 布局的话,就需要写 `dispaly: flex;` 但是封装成类,如 + +```CSS +.flex { + dispaly: flex; +} +``` + +引用的时候直接在 class 中添加 flex 即可 + +```jsx +

tailwindcss

+``` + +贴一张官方演示图,把大部分常用的样式都封装成 class + +官方在线例子(下图) [Tailwind Play (tailwindcss.com)](https://play.tailwindcss.com/) + +![](https://img.kuizuo.cn/20220114033240.png) + +有以下几种优点: + +1. 源代码无非就是 css 的基本样式,如 class `w-auto` 对应 css `width: auto;` 等等 +2. 如果不是特别复杂的样式,甚至可以不用写一条 css 代码,开发效率杠杠的。 +3. 体积很小,更好的样式复用,并且打包后会根据所用的 class 进行打包,而非全部无用样式打包。 +4. 与 bootstrap 设计不同,完全可以定制化不同类型的组件,而不是像 `class="btn btn-danger"` 这样。 + +体验下来基本上就是在写内联样式 inline css 但是同时又不显得杂乱。 + +### 组件化中使用 + +在组件化开发中,完全可以自己实现一个 Button 按钮(上间距 `pt-4`,底部间距 `pb-10`,文字为 `text-sky-500` 天蓝色), + +```jsx +const Button = ({ children, color }) => ( + {children} +) +``` + +不过要说缺点的话: + +1. 可能之前标题只需要定义.title 类来完成全部样式,而 tailwind 需要好几个 css 原子类来实现 +2. 初学者可能不适应,需要反复的查阅文档。(不过用多了,自然就会习惯了) + +然后还有一个 WindCSS,可以看作是**按需供应的** Tailwind 替代方案。不过暂时不支持 React。 + +此外还有一篇文章非常推荐 [重新构想原子化 CSS (antfu.me)](https://antfu.me/posts/reimagine-atomic-css-zh),不多说,再刷一遍。 + +## 最佳实现? + +介绍完几种 React 中 Css 的实现(当然还有很多库没介绍,主要挑几种主流的),实际又要选择哪种呢? + +说说我目前 react 所选的操作,tailwind(原子类)+ CSS modules,写一些小项目或者 demo 甚至都没必要写 css 代码,毕竟 css 是大多数前端程序员都不是那么想写的(包括我)。而做一些自定义的小组件的话那肯定是 styled-components,而 styled-jsx,对组件代码牺牲挺大所以不怎么写。 + +不过每个人使用风格不同,我一开始接触原子类是 windicss,用久了之后习惯了常用的 class,编写起来可以说是相当的快捷了。 + +不过相比 Vue 而言,react 的 css 实现着实费劲。 + +> 参考链接: +> +> [CSS Modules 用法教程 - 阮一峰的网络日志 (ruanyifeng.com)](https://www.ruanyifeng.com/blog/2016/06/css_modules.html) +> +> [CSS in JS 简介 - 阮一峰的网络日志 (ruanyifeng.com)](https://www.ruanyifeng.com/blog/2017/04/css_in_js.html) +> +> [React 拾遗:从 10 种现在流行的 CSS 解决方案谈谈我的最爱 (下) - 掘金 (juejin.cn)](https://juejin.cn/post/6844903638289252360) diff --git "a/docs/skill/web/react/React\344\271\213hooks.md" "b/docs/skill/web/react/React\344\271\213hooks.md" new file mode 100644 index 0000000..ebeb073 --- /dev/null +++ "b/docs/skill/web/react/React\344\271\213hooks.md" @@ -0,0 +1,412 @@ +--- +id: react-hooks +slug: /react-hooks +title: React之hooks +date: 2022-09-07 +authors: kuizuo +tags: [react, hook] +keywords: [react, hook] +--- + + + +## 官方内置 hooks + +### useState + +在函数组件中管理数据状态 + +#### 基本数据类型 + +```tsx +import React from 'react' + +export function App(props) { + const [count, setCount] = React.useState(0) + + return ( +
+
{count}
+ + + +
+ ) +} +``` + +主要注意的点是 setCount 可以传入相应数值或匿名函数,如上所示的都是可以实现对 count+1 + +#### 对象 + +这里主要针对复杂类型(数组,对象),示例: + +```tsx +import * as React from 'react' + +export default function App(props) { + type User = { + name: string + age: number + } + + const [user, setUser] = React.useState({ + name: 'kuizuo', + age: 20, + }) + + return ( +
+
{user.name}
+
{user.age}
+ +
+ ) +} +``` + +#### 数组 + +```tsx +import * as React from 'react' + +export default function App(props) { + const [arr, setArr] = React.useState(['code', 'eat', 'sleep']) + + return ( +
+ {arr.map((a) => ( +
{a}
+ ))} + +
+ ) +} + +``` + +useState 对于复杂类型而言,尤其是在赋值操作是比较麻烦的。没办法,因为需要更改状态就需要调用 setState 方法,而 setState 方法需要传入最终完整的数据。 + +对于对象而言,可以考虑使用 react use 的 [useMap](https://github.com/streamich/react-use/blob/master/docs/useMap.md),对于数组而言,可以考虑使用 react use 的 [useList](https://github.com/streamich/react-use/blob/master/docs/useList.md)。(其实都是对 setState 进行一定的封装) + +### useEffect + +useEffect 可以让你在函数组件中执行副作用操作 + +副作用是指一段和当前执行结果无关的代码,常用的副作用操作如数据获取、设置订阅、手动更改 React 组件中的 DOM。 + +useEffect 可以接收两个参数,代码如下: + +```TypeScript +useEffect(callback, dependencies) +``` + +第一个参数是要执行的函数 callback,第二个参数是可选的依赖项数组 dependencies。 + +以下是一些示例: + +```tsx +import * as React,{} from 'react' + +export default function App() { + const [count, setCount] = React.useState(0) + + React.useEffect(()=>{ + console.log(count) + }) + + return
setCount(count+1)}>{count}
+} +``` + +每当 count 发生变化后,useEffect 副作用函数就会输出 count,由于没传入 dependencies 数组,则**每次 render 后执行** + +如果第二个参数给空数组的话,只会在**第一次加载组件时执行**,通常可用于首次数据请求。 + +```tsx +import * as React from 'react' + +export default function App() { + const [data, setData] = React.useState('') + + React.useEffect(() => { + async function fetchData() { + const data = await (await fetch('https://api.kuizuo.cn/api/one')).text() + console.log(data) + setData(data) + } + + fetchData() + }, []) + + return
{data}
+} + +``` + +此外 componentWillUnmount 生命周期也可在 useEffect 中执行。 + +```tsx +import * as React from 'react' + +export default function App() { + const [data, setData] = React.useState('') + + React.useEffect(() => { + // Update the document title using the browser API + document.title = `You clicked ${count} times` + + return () => { + // 可用于做清除,相当于 class 组件的 componentWillUnmount + } + + }, [count]) // 指定依赖项为 count,在 count 更新时执行该副作用 + + return
setCount(count+1)}>{count}
+} +``` + +#### 小总结 + +useEffect 提供了四种执行副作用的时机: + +- **每次 render 后执行**:不提供第二个依赖项参数。比如 `useEffect(() => {})` +- **仅第一次 render 后执行**:提供一个空数组作为依赖项。比如 `useEffect(() => {}, [])` +- **第一次以及依赖项发生变化后执行**:提供依赖项数组。比如 `useEffect(() => {}, [deps])` +- **组件 unmount 后执行**:返回一个回调函数。比如 `useEffect(() => { return () => {} }, [])` + +### useMono + +useMemo 定义的创建函数只会在某个依赖项改变时才重新计算,有助于每次渲染时**不会重复的高开销的计算**,而接收这个计算值作为属性的组件,也**不会频繁地需要重新渲染**。类似与 Vue 中的 computed + +示例: + +```tsx +const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]) +``` + +useMemo 本质上就像一个缓存,而依赖项是缓存失效策略。 + +不仅能对数据进行缓存,对于纯组件也是能够缓存的。使用`memo` 对组件进行包裹即可,例如 `export default React.memo(Children)` + +### useCallback + +useCallback 定义的回调函数只会在依赖项改变时重新声明这个回调函数,这样就保证了**组件不会创建重复的回调函数**。而接收这个回调函数作为属性的组件,也**不会频繁地需要重新渲染**。 + +useCallback 与 useMono 的作用都是一样的,只不过前者专门为函数构建的。例如下面的一个例子 + +```tsx +const handleMegaBoost = React.useMemo(() => { + return function() { + setCount((currentValue) => currentValue + 1234) + } +}, []) +``` + +有更好的方法,就是使用 useCallback,如下 + +```tsx +const handleMegaBoost = React.useCallback(() => { + setCount((currentValue) => currentValue + 1234) +}, []) +``` + +这两者的效果是完全相同的。相当于 + +```tsx +// This: +React.useCallback(function helloWorld(){}, []) +// ...Is functionally equivalent to this: +React.useMemo(() => function helloWorld(){}, []) +``` + +对于 useMono 和 useCallback 强烈推荐阅读[Understanding useMemo and useCallback (joshwcomeau.com)](https://www.joshwcomeau.com/react/usememo-and-usecallback/) + +### useRef + +useRef 返回一个 ref 对象,这个 ref 对象在组件的整个生命周期内持续存在。 + +他有 2 个用处: + +- 保存 DOM 节点的引用 +- 在多次渲染之间共享数据 + +保存 DOM 节点的引入使用示例如下: + +```tsx +function TextInputWithFocusButton() { + const inputEl = React.useRef(null) + const onButtonClick = () => { + // `current` 指向已挂载到 DOM 上的文本输入元素 + inputEl.current.focus() + } + return ( + <> + + + + ) +} +``` + +以上代码通过 useRef 创建了 ref 对象,保存了 DOM 节点的引用,可以对 ref.current 做 DOM 操作。 + +第二个用途在日常开发中没怎么用到过,useRef 主要还是为了获取 dom 属性。 + +### useContext + +useContext 用于接收一个 context 对象并返回该 context 的值,可以实现**跨层级的数据共享**。 + +```tsx +// 创建一个 context 对象 +const MyContext = React.createContext(initialValue) +function App() { + return ( + // 通过 Context.Provider 传递 context 的值 + + + + ) +} + +function Container() { + return +} + +function Test() { + // 获取 Context 的值 + const theme = useContext(MyContext) // 1 + return
+} + +``` + +更倾向的做法是将`const MyContext = React.createContext(initialValue)` 存在在`src/contexts`目录下,以便于其他组件引用 + +### useReducer + +语法:`const [state, dispatch] = useReducer(reducer, initialArg, init)` + +第一个参数 reducer 是函数 `(state, action) => newState`,接受当前的 state 和操作行为。第二个参数 initialArg 是状态初始值。第三个参数 init 是懒惰初始化函数。 + +示例: + +```tsx +import * as React from 'react' +import './style.css' + +const initialState = { count: 0 } + +function reducer(state, action) { + switch (action.type) { + case 'increment': + return { count: state.count + 1 } + case 'decrement': + return { count: state.count - 1 } + default: + throw new Error() + } +} + +export default function Counter() { + const [state, dispatch] = React.useReducer(reducer, initialState) + return ( +
+ Count: {state.count} + + +
+ ) +} +``` + +通过`useReducer `与`useContext` 就能做到代替[redux](https://cn.redux.js.org/) 来进行状态管理了。篇幅有限,这里占不做演示。 + +### useId + +这是 React18 的新特性,用于同一个组件在服务端和客户端之间确定对应的匹配关系。而确定关系的便是这个 Id。 + +当一个组件,同时会被服务端和客户端渲染时,我们就可以使用 `useId` 来创建当前组件的唯一身份。 + +```tsx +function Checkbox() { + const id = useId() + return ( + <> + + + + ) +} +``` + +如果在同一个组件中,我们需要多个 id,那么一定不要重复的使用 `useId`,而是基于一个 id 来创建不同的标识,通常的做法是添加额外不同的字符串,例如下面这样: + +```tsx +function NameFields() { + const id = useId() + return ( +
+ +
+ +
+ +
+ +
+
+ ) +} +``` + +更多 React 内置 Hook 可以参考 [Hook API](https://zh-hans.reactjs.org/docs/hooks-reference.html) + +## 自定义 hooks + +自定义 Hooks 就是函数,它有 2 个特征区分于普通函数: + +- 名称以 “use” 开头; +- 函数内部调用其他的 Hook。 + +例如: + +### useToggle + +```tsx +import * as React from 'react' +function useToggle(initialValue) { + const [value, setValue] = React.useState(initialValue) + const toggle = React.useCallback(() => { + setValue(v => !v) + }, []) + return [value, toggle] +} +``` + +等等根据实际应用场景编写相应的 hooks + +## Hooks 库 + +[react-use](https://github.com/streamich/react-use) + +[ahooks](https://ahooks.js.org/zh-CN/) + +## 参考文章 + +[React-你有完全了解 Hooks 吗](https://juejin.cn/post/7064345263061598222) diff --git a/docs/skill/web/vue/Pinia.md b/docs/skill/web/vue/Pinia.md new file mode 100644 index 0000000..1dd2f5d --- /dev/null +++ b/docs/skill/web/vue/Pinia.md @@ -0,0 +1,194 @@ +--- +id: pinia +slug: /pinia +title: Pinia +date: 2020-10-23 +authors: kuizuo +tags: [vue, pinia] +keywords: [vue, pinia] +--- + + + +> 官方文档:[Introduction | Pinia (vuejs.org)](https://pinia.vuejs.org/introduction.html) + +## 安装 + +```bash +npm install pinia +``` + +## 创建 Store + +在 src/store 中创建 index.ts,并导出 store + +```typescript title="src/store/index.ts" +import { createPinia } from 'pinia' + +const store = createPinia() + +export default store +``` + +在 main.ts 中引入并使用 + +```typescript title="main.ts" +import { createApp } from 'vue' +import App from './App.vue' +import store from './store' + +const app = createApp(App) +app.use(store) +``` + +## 创建 modules + +在 src/store 目录下创建 modules 目录,里面存放项目中所需要使用到的状态。演示代码如下 + +```typescript title="store/modules/user.ts" +import { defineStore } from 'pinia' + +interface UserState { + name: string +} + +export const useUserStore = defineStore({ + id: 'user', + state: (): UserState => { + return { + name: 'kuizuo', + } + }, + getters: { + getName(): string { + return this.name + }, + }, + actions: { + setName(name: string) { + this.name = name + }, + }, +}) +``` + +## 使用 + +### 获取 state + +```vue + + + +``` + +不过这样写法不优雅,就可以使用 computed + +```typescript +const name = computed(() => userStore.getName) // 前提定义了getters +const name = computed(() => userStore.name) +``` + +state 也可以使用解构,但使用解构会使其失去响应式,这时候可以用 pinia 的 `storeToRefs`。 + +```typescript +import { storeToRefs } from 'pinia' +const { name } = storeToRefs(userStore) +``` + +### 修改 state + +可以直接使用`userStore.name = "xxx"` 来进行修改,但不建议,而是使用 actions 来修改,在上面已经定义一个 setName 方法用来修改 state + +```typescript +userStore.setName('xxx') +``` + +## 与 vuex 对比 + +不难发现,pinia 比 vuex 少了个`mutations`,也就是变更状态的函数,而 pinia 则是将其与 action 合并在一起。 + +在 Vuex 中 mutation 是无法异步操作的,而 Action 可以包含任意异步操作。像上面要写异步操作的只需要在 actions 中正常的编写 async await 语法的异步函数即可。如 + +```typescript +export const useUserStore = defineStore({ + id: 'user', + actions: { + async login(user) { + const { data } = await api.login(user) + return data + }, + }, +}) +``` + +而 vuex 中写法与调用就不堪入目了 😂 + +## 数据持久化 + +安装 + +```bash +npm i pinia-plugin-persistedstate +``` + +使用 + +```typescript {2,5} +import { createPinia } from 'pinia' +import piniaPluginPersist from 'pinia-plugin-persistedstate' + +const store = createPinia() +store.use(piniaPluginPersist) + +export default store +``` + +在对应的 store 中开启 persist 即可,**默认情况下数据是存放在 sessionStorage(会话存储),并以 store 中的 id 作为 key** + +```typescript {8-10} +export const useUserStore = defineStore({ + id: 'user', + state: (): UserState => { + return { + name: 'kuizuo', + } + }, + persist: { + enabled: true, + }, +}) +``` + +persist 还有其他配置,例如自定义 key,存放位置改为 localStorage + +```typescript {3-8} +persist: { + enabled: true, + strategies: [ + { + key: 'my_user', + storage: localStorage + } + ] +} +``` + +还可以使用 paths 来指定那些 state 持久化,如下 + +```typescript {5} +persist: { + enabled: true, + strategies: [ + { + paths: ['name'] + } + ] +} +``` diff --git "a/docs/skill/web/vue/Vue\345\223\215\345\272\224\345\274\217\346\225\260\346\215\256\344\271\213Array.md" "b/docs/skill/web/vue/Vue\345\223\215\345\272\224\345\274\217\346\225\260\346\215\256\344\271\213Array.md" new file mode 100644 index 0000000..b4c1a5f --- /dev/null +++ "b/docs/skill/web/vue/Vue\345\223\215\345\272\224\345\274\217\346\225\260\346\215\256\344\271\213Array.md" @@ -0,0 +1,190 @@ +--- +id: vue-reactive-data-array +slug: /vue-reactive-data-array +title: Vue响应式数据之Array +date: 2022-05-12 +authors: kuizuo +tags: [vue, javascript] +keywords: [vue, javascript] +--- + + + +## 修改原型方法 + +上面所说到的是对象的响应式,但 js 中不止有对象,还有数组,数组能用 Object.defineProperty 方式来监听吗,能 + +```javascript +const original = Array.prototype.push +Array.prototype.push = function (...args) { + console.log('ADD', args) + return original.apply(this, args) +} + +const arr = [1, 2, 3] + +arr.push(4) +// 输出 ADD 4 +``` + +当然,这里修改了全局的 Array 原型,对于一些不必要的数据也会监听到,在 Vue2 中会进入 Observer 构造函数体,判断 value 是否为数组,是则对 value 原型赋值为修改后的 arrayMethods。 + +```javascript +const arrayProto = Array.prototype +const arrayMethods = Object.create(arrayProto) + +if (Array.isArray(value)) { + value.__proto__ = arrayMethods +} +``` + +至于以及其他数组方法,这里仅做代码实现,由于篇幅有限,不做细说。 + +```javascript +const arrayProto = Array.prototype +const arrayMethods = Object.create(arrayProto) + +function def(obj, key, val, enumerable) { + Object.defineProperty(obj, key, { + value: val, + enumerable: !!enumerable, + writable: true, + configurable: true, + }) +} + +;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) { + const original = arrayProto[method] + + def(arrayMethods, method, function mutator(...args) { + const result = original.apply(this, args) + let inserted + switch (method) { + case 'push': + case 'unshift': + inserted = args + break + case 'splice': + inserted = args.slice(2) + break + } + + if (inserted) { + console.log('ADD', args) + } + return result + }) +}) + +function observerArray(arr) { + arr.__proto__ = arrayMethods + return arr +} + +let arr = observerArray([1, 2, 3]) + +arr.push(4) +arr.unshift(0) + +console.log(arr) +``` + +输出如下 + +``` +ADD [ 4 ] +ADD [ 0 ] +[ 0, 1, 2, 3, 4 ] +``` + +### 缺陷 + +通过一系列原型方法修改来实现响应式也有缺陷,尤其对于数组特殊变动并没有对应原型方法。 + +1. 利用索引直接设置一个数组项时,例如:`vm.items[indexOfItem] = newValue` +2. 修改数组的长度时,例如:`vm.items.length = newLength` + +## Proxy + +但在 Vue3 也可以使用 Proxy 来监听(代理)数据,先引用监听[Object 中的最终代码](/docs/vue-reactive-data-object#最终代码),对其稍加修改一下,看看效果 + +```javascript +function log(type, index, val) { + console.log(type, index, val) +} + +function reactive(target) { + return new Proxy(target, { + get(target, key, receiver) { + const res = Reflect.get(target, key, receiver) + + if (typeof res === 'object' && res !== null) { + return reactive(res) + } + + if (Array.isArray(target) && isNaN(key)) { + return res + } + + log('GET', key, res) + return res + }, + set(target, key, newVal, receiver) { + const oldVal = target[key] + + const type = Array.isArray(target) + ? Number(key) < target.length + ? 'SET' + : 'ADD' + : Object.prototype.hasOwnProperty.call(target, key) + ? 'SET' + : 'ADD' + + const res = Reflect.set(target, key, newVal, receiver) + + if (Array.isArray(target) && key === 'length') { + // log('Length', null, target.length) + } else { + if (oldVal !== newVal) { + log(type, key, newVal) + } + } + + return res + }, + deleteProperty(target, key) { + const hadKey = Object.prototype.hasOwnProperty.call(target, key) + + const res = Reflect.deleteProperty(target, key) + + if (res && hadKey) { + log('DELETE', key, res) + } + + return res + }, + }) +} + +const target = [1, 2, 3] +const p = reactive(target) + +p[1] +p.push(4) +p[2] = 100 +p.pop() +console.log(p) +``` + +执行结果 + +``` +GET 1 2 +ADD 3 4 +SET 2 100 +GET 3 4 +DELETE 3 true +[ 1, 2, 100 ] +``` + +实际上,以上代码就已经能监听数组成员新增,修改与删除了。但对于一些特殊方法(数组遍历,寻找成员),还需要修改其原型方法,就需要像 Vue2 对原型方法那样操作。不过在监听数据变化上,用处并不是特别大,主要体现在依赖收集以及副作用函数的调用上。 diff --git "a/docs/skill/web/vue/Vue\345\223\215\345\272\224\345\274\217\346\225\260\346\215\256\344\271\213Object.md" "b/docs/skill/web/vue/Vue\345\223\215\345\272\224\345\274\217\346\225\260\346\215\256\344\271\213Object.md" new file mode 100644 index 0000000..4f5a083 --- /dev/null +++ "b/docs/skill/web/vue/Vue\345\223\215\345\272\224\345\274\217\346\225\260\346\215\256\344\271\213Object.md" @@ -0,0 +1,306 @@ +--- +id: vue-reactive-data-object +slug: /vue-reactive-data-object +title: Vue响应式数据之Object +date: 2022-05-10 +authors: kuizuo +tags: [vue, javascript] +keywords: [vue, javascript] +--- + +在阅读《深入浅出 Vue.js》与《Vue.js 设计与实现》,了解到 vue 是如何侦测数据,同时自己在接触 js 逆向时也常常会用到。于是就准备写篇 js 如何监听数据变化,这篇为监听 Object 数据。 + + + +## Object.defineproperty + +```javascript +const data = { + username: 'kuizuo', + password: 'a123456', +} + +function defineReactive(data, key, val) { + Object.defineProperty(data, key, { + enumerable: true, + configurable: true, + get() { + console.log('GET', val) + return val + }, + set(newVal) { + if (val === newVal) return + + val = newVal + console.log('SET', val) + }, + }) +} + +function observe(data) { + Object.keys(data).forEach(function (key) { + defineReactive(data, key, data[key]) + }) +} + +observe(data) + +data.username +data.username = '愧怍' +``` + +从上面的代码中就可以发现,只要取值与赋值就会进入 get 和 set 函数内,在这里面便可以实现一些功能,例如 Vue 中收集依赖,在想监听浏览器中 cookies 的取值与赋值,就可以使用如下代码 + +```javascript +!(function () { + let cookie = document.cookie + Object.defineProperty(document, 'cookie', { + get() { + console.log('cookie get', cookie) + return cookie + }, + set(newVal) { + cookie = newVal + console.log('cookie set', cookie) + }, + }) +})() +``` + +使用 object.defineproperty 能监听对象上的某个属性修改与获取,但是无法监听到对象属性的增和删。这在 es5 是无法实现的,因为还不支持[元编程](https://baike.baidu.com/item/元编程/6846171)。这也就是为什么 Vue2 中[对于对象](https://cn.vuejs.org/v2/guide/reactivity.html#对于对象)无法监听到 data 的某个属性增加与删除了 + +```javascript +var vm = new Vue({ + data: { + a: 1, + }, +}) + +// `vm.a` 是响应式的 + +vm.b = 2 +// `vm.b` 是非响应式的 +``` + +## Proxy 与 Reflect + +但在 ES6 中提供了 Proxy 可以实现元编程,同时 Vue3 也使用 Proxy 来重写[响应式系统](https://v3.cn.vuejs.org/guide/reactivity.html)。所以就很有必要去了解该 API + +```javascript +function reactive(target) { + return new Proxy(target, { + get(target, key) { + const res = target[key] + console.log('GET', key, res) + return res + }, + set(target, key, newValue) { + target[key] = newValue + console.log('SET', key, newValue) + }, + deleteProperty(target, key) { + console.log('DELETE', key) + delete target[key] + }, + }) +} +``` + +但上述写法中使用了`target[key]` 是能获取到 target 的值,但可能会存在一定隐患(如 this 问题),所以更推荐使用`Reflect`对象的方法,如下 + +```javascript +function reactive(target) { + return new Proxy(target, { + get(target, key, receiver) { + const res = Reflect.get(target, key, receiver) + console.log('GET', key, res) + return res + }, + set(target, key, newValue, receiver) { + const res = Reflect.set(target, key, newValue, receiver) + console.log('SET', key, newValue) + return res + }, + deleteProperty(target, key) { + const res = Reflect.deleteProperty(target, key) + console.log('DELETE', key) + + return res + }, + }) +} +``` + +调用如下 + +```javascript +const target = { + foo: 1, + bar: 1, +} + +let p = reactive(target) +p.foo++ +delete p.bar + +console.log(target) +``` + +输出内容如下 + +``` +GET foo 1 +SET foo 2 +DELETE bar +{ foo: 2 } +``` + +其中这里的 get,set,deleteProperty 可以拦截到对象属性的取值,赋值与删除的操作。相比 Object.defineproperty 除了好用外,可操作空间也大。 + +### [this 问题](https://es6.ruanyifeng.com/#docs/proxy#this-问题) + +如果 target 对象存在 this,那么不做任何拦截的情况下,target 的 this 所指向的是 target,而不是代理对象 proxy + +```javascript +const target = { + m: function () { + console.log(this === proxy) + }, +} +const handler = {} + +const proxy = new Proxy(target, handler) + +target.m() // false +proxy.m() // true +``` + +具体可看:[this 问题](https://es6.ruanyifeng.com/#docs/proxy#this-问题) + +### 区别增加和修改 + +对象属性增加还是修改都会触发 set,所以需要在 set 中区别增加和修改, + +```javascript {6} +function reactive(target) { + return new Proxy(target, { + set(target, key, newVal, receiver) { + const oldVal = target[key] + + const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD' + const res = Reflect.set(target, key, newVal, receiver) + + if (oldVal !== newVal) { + console.log(type, key, newValue) + } + + return res + }, + }) +} +``` + +### 深响应 + +如果数据含多层对象,像 + +```javascript +const p = reactive({ foo: { bar: 1 } }) + +// 将不会触发 +p.foo.bar = 2 +``` + +需要将 get 中包装为 + +```javascript {6-9} +function reactive(target) { + return new Proxy(target, { + get(target, key, receiver) { + const res = Reflect.get(target, key, receiver) + + if (typeof res === 'object' && res !== null) { + // 将其包装成响应式数据 + return reactive(res) + } + + console.log('GET', key, res) + return res + }, + }) +} +``` + +## 最终代码 + +在稍加对 console.log 进行封装,最终实现对 Object 代理的代码如下 + +```javascript +const target = { + foo: 1, + bar: 1, +} + +function log(type, key, val) { + console.log(type, key, val) +} + +function reactive(target) { + return new Proxy(target, { + get(target, key, receiver) { + const res = Reflect.get(target, key, receiver) + + if (typeof res === 'object' && res !== null) { + return reactive(res) + } + + log('GET', key, res) + return res + }, + set(target, key, newVal, receiver) { + const oldVal = target[key] + + const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD' + const res = Reflect.set(target, key, newVal, receiver) + + if (oldVal !== newVal) { + log(type, key, newVal) + } + + return res + }, + deleteProperty(target, key) { + const hadKey = Object.prototype.hasOwnProperty.call(target, key) + + const res = Reflect.deleteProperty(target, key) + + if (res && hadKey) { + log('DELETE', key, res) + } + + return res + }, + }) +} + +const p = reactive(target) +p.a = 1 +p.foo++ +delete p.bar + +console.log(target) +``` + +当然,可以将 log 函数的进一步的封装,如 Vue3 中 get 方法的*track*,set 方法中的*trigger*。更好的监听数据变化以及执行自定义函数等等,这里只谈论监听数据变化。 + +此外 Proxy 还不只有监听对象的属性,还可以监听对象方法等等,具体可在[MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy)中查询相对于的拦截器。 + +## 参考 + +> [Proxy - ECMAScript 6 入门 (ruanyifeng.com)](https://es6.ruanyifeng.com/#docs/proxy) +> +> [Proxy() 构造器 - JavaScript | MDN (mozilla.org)](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy) +> +> 《Vue.js 设计与实现》 +> +> 《深入浅出 Vue.js》 diff --git "a/docs/skill/web/vue/Vue\345\223\215\345\272\224\345\274\217\346\225\260\346\215\256\344\271\213\345\237\272\346\234\254\346\225\260\346\215\256\347\261\273\345\236\213.md" "b/docs/skill/web/vue/Vue\345\223\215\345\272\224\345\274\217\346\225\260\346\215\256\344\271\213\345\237\272\346\234\254\346\225\260\346\215\256\347\261\273\345\236\213.md" new file mode 100644 index 0000000..5eee546 --- /dev/null +++ "b/docs/skill/web/vue/Vue\345\223\215\345\272\224\345\274\217\346\225\260\346\215\256\344\271\213\345\237\272\346\234\254\346\225\260\346\215\256\347\261\273\345\236\213.md" @@ -0,0 +1,277 @@ +--- +id: vue-reactive-data-basic-type +slug: /vue-reactive-data-basic-type +title: Vue响应式数据之基本数据类型 +date: 2022-05-18 +authors: kuizuo +tags: [vue, javascript] +keywords: [vue, javascript] +--- + + + +学过 js 的应该都知道,基本数据类型并非引用类型,直接修改是无法直接拦截的 + +```javascript +let str = 'vue' +// 无法拦截str +str = 'vue3' +``` + +很容易想到,用非原始值“包裹”原始值,成一个对象的形式,然后对包裹对象 wrapper 进行 proxy 拦截 + +```javascript +const wrapper = { + value: 'vue', +} + +const name = reactive(wrapper) + +name.value = 'vue3' +``` + +不出意外(肯定不会出),将会输出 + +```text +SET value vue3 +``` + +不难发现,vue2 中对原始值的响应都是将其包裹在 data 函数下返回的对象,并且从上面的代码上来看。但从开发者的角度还需要创建一个包装对象,不易操作的同时,也意味不规范。于是 vue3 封装了 ref 函数,而返回的对象便是响应式的包装对象`reactive(wrapper)` + +```javascript +function ref(val) { + const wrapper = { + value: val, + } + + return reactive(wrapper) +} +``` + +上面的代码便改写为 + +```javascript +const name = ref('vue') + +name.value = 'vue3' +``` + +## 区别是否为 ref + +要区别一个数据是否为 ref,只需要在 ref 中定义一个不可枚举的属性`__v_isRef`值为 true。 + +```javascript +function ref(val) { + const wrapper = { + value: val, + } + + Object.defineProperty(wrapper, '__v_isRef', { + value: true, + }) + + return reactive(wrapper) +} +``` + +## 响应丢失问题 + +在使用解构赋值的情况下,可能会存在响应丢失的情况,例如 + +```javascript +const obj = reactive({ foo: 1, bar: 2 }) + +const user = { + ...obj, +} + +user.foo.value = 3 +``` + +可以发现,并不会输出 SET foo 3,主要由展开运算符...所导致的。上面的 user 就等价于 `{ foo: 1, bar: 2 }` + +所以 Vue 则封装了 toRef 和 toRefs 方法,将某个对象的 key 包裹为 ref + +```javascript +function toRef(obj, key) { + const wrapper = { + get value() { + return obj[key] + }, + set value(val) { + obj[key] = val + }, + } + + Object.defineProperty(wrapper, '__v_isRef', { + value: true, + }) + + return wrapper +} + +function toRefs(obj) { + const ret = {} + for (const key in obj) { + ret[key] = toRef(obj, key) + } + + return ret +} + +const obj = reactive({ foo: 1, bar: 2 }) + +const user = { + ...toRefs(obj), +} + +user.foo.value = 3 +``` + +其结果便能正常监听响应式,并输出 SET foo 3 + +## 自动脱 ref + +toRefs 是解决了响应式的问题,但同时也带来了一个新的问题。由于 toRefs 会把响应式数据第一层转为 ref,所以就必须通过 value 来访问属性,这在模板中 + +```HTML +

{{ foo.value }}

+``` + +要是我,我肯定不会使用 Vue。所以 Vue 提供自动脱 ref 的能力,通俗点就是省略.value。 + +```javascript +function proxyRefs(target) { + return new Proxy(target, { + get(target, key, receiver) { + const value = Reflect.get(target, key, receiver) + return value.__v_isRef ? value.value : value + }, + set(target, key, newValue, receiver) { + const value = target[key] + if (value.__v_isRef) { + value.value = newValue + return true + } + + return Reflect.set(target, key, newValue, receiver) + }, + }) +} +``` + +将其 user 数据传递给 proxyRefs 函数进行处理,便可省略.value + +```javascript +const user = proxyRefs({ + ...toRefs(obj), +}) + +console.log(user.foo) // 1 +``` + +实际上,在编写 Vue 组件时,setup 返回的数据便会传递给 proxyRefs 函数进行处理。 + +## 最终代码 + +```javascript +function log(type, key, val) { + console.log(type, key, val) +} + +function reactive(target) { + return new Proxy(target, { + get(target, key, receiver) { + const res = Reflect.get(target, key, receiver) + + if (typeof res === 'object' && res !== null) { + return reactive(res) + } + + log('GET', key, res) + return res + }, + set(target, key, newVal, receiver) { + const oldVal = target[key] + + const type = Object.prototype.hasOwnProperty.call(target, key) + ? 'SET' + : 'ADD' + const res = Reflect.set(target, key, newVal, receiver) + + if (oldVal !== newVal) { + log(type, key, newVal) + } + + return res + }, + deleteProperty(target, key) { + const hadKey = Object.prototype.hasOwnProperty.call(target, key) + + const res = Reflect.deleteProperty(target, key) + + if (res && hadKey) { + log('DELETE', key, res) + } + + return res + }, + }) +} + +function ref(val) { + const wrapper = { + value: val, + } + + Object.defineProperty(wrapper, '__v_isRef', { + value: true, + }) + + return reactive(wrapper) +} + +function toRef(obj, key) { + const wrapper = { + get value() { + return obj[key] + }, + set value(val) { + obj[key] = val + }, + } + + Object.defineProperty(wrapper, '__v_isRef', { + value: true, + }) + + return wrapper +} + +function toRefs(obj) { + const ret = {} + for (const key in obj) { + ret[key] = toRef(obj, key) + } + + return ret +} + +function proxyRefs(target) { + return new Proxy(target, { + get(target, key, receiver) { + const value = Reflect.get(target, key, receiver) + return value.__v_isRef ? value.value : value + }, + set(target, key, newValue, receiver) { + const value = target[key] + if (value.__v_isRef) { + value.value = newValue + return true + } + + return Reflect.set(target, key, newValue, receiver) + }, + }) +} +``` diff --git "a/docs/tools/Everything\345\277\253\351\200\237\346\220\234\347\264\242\346\234\254\345\234\260\346\226\207\344\273\266.md" "b/docs/tools/Everything\345\277\253\351\200\237\346\220\234\347\264\242\346\234\254\345\234\260\346\226\207\344\273\266.md" new file mode 100644 index 0000000..df16711 --- /dev/null +++ "b/docs/tools/Everything\345\277\253\351\200\237\346\220\234\347\264\242\346\234\254\345\234\260\346\226\207\344\273\266.md" @@ -0,0 +1,69 @@ +--- +id: everything-quick-search-local-files +slug: /everything-quick-search-local-files +title: Everything快速搜索本地文件 +date: 2020-09-08 +authors: kuizuo +tags: [工具] +keywords: [工具] +--- + +![everything-Everything](https://img.kuizuo.cn/everything-Everything.jpg) + +你有没有经历过为了找一个**本地**文件,反复点开一个个文件夹,甚至有可能还找不到,于是开始 Windows 自带的搜索引擎去搜索想要的文件,然后等你吃完饭回来,搜索的进度条竟然还没有走完。而现在有一个软件,它可以让你不必等待,一个眨眼的功夫,你想要的文件就呈现在你的面前,它就是`Everything` + + + +软件下载地址 [点我下载](https://www.voidtools.com/zh-cn/) 一路下一步,默认选项即可。 + +## 软件说明 + +可以说`Everything`是速度最快的文件搜索软件,可以瞬间搜索到你所需要的文件,并且页面简洁,体积仅有几兆,可以说你想要的优点都有了。内存占用也不过 200m(我本地文件很多),单丝毫不影响它搜索的速度。 + +## 使用方法 + +### 设置打开快捷键 + +请设置一个快捷键用于快捷的打开`Everything`,有时候你需要搜索一个文件时,不必在找到`Everything`的图标打开,只需要按下所设置的快捷键,这里我设置为`Ctrl+Shift+Alt+Q`,一是避免其他键冲突,二是按键方便。 + +设置方式如下图 + +![demo](https://img.kuizuo.cn/demo.gif) + +其余的相关快捷键感兴趣可以自行设置,这里就举我最常用的一个。 + +## 搜索文件 + +在搜索前建议你设置一下排除列表,避免文件过多,根据自己需求来设置排除的文件夹,这里我把我基本用不上的文件夹放在这。 + +![image-20200908001436884](https://img.kuizuo.cn/image-20200908001436884.png) + +这里我先放一个 gif 图,先感受一下 + +![demo1](https://img.kuizuo.cn/demo1.gif) + +我也只是简单的使用一下,实际使用过就知道这个工具有多牛逼,你就会知道我为什么会推荐了。想上面的简单使用已经够用了,还有一些针对性的操作,可以在 帮助->搜索语法 即可看到相关搜索语法帮助。 + +这里我就简单说几个常用的 比如我要打开`pycharm` 这个软件,这时候我会肯定会先输入`pycharm`,但是发现出现了特别多的结果且没有我想要的,于是我尝试补上`.exe`后缀,然后发现竟然没有!怎么可能我明明安装了,其实这个文件名是`pycharm64.exe`你输入`pycharm.exe`肯定搜不到。而遇到这种问题一般都不是这样搜索而是输入 `pycharm exe:` 意思就是我要找` 包含``pycharm `文件名同时又要是`exe`为后缀的。操作情况如下 + +![demo2](https://img.kuizuo.cn/demo2.gif) + +不过一般这种常用软件的话我还是建议你把这个文件设置一个快捷方式放在桌面,开始菜单或者任务栏。 + +### 右键搜索 Everything + +有时候我只想在指定文件夹下搜索,而不是全局搜索,那么我可以右键点击文件夹空白区域,其中你会看到`搜索 Everything…`的选项,接着会在搜索框为你补上当前的文件夹的路径,然后你在输入你想搜索的文件名即可,这里我就不在赘述和放图了。 + +## 一些注意的坑 + +### 全字匹配 + +有时候我要搜索一个文件,文件名是`demo123.txt`,但是我输入`demo`的时候下面却没有`demo123`,而当我补上`123`的时候`demo123.txt`又出现了的我的面前了。别急,你多半是不小心按下了 Ctrl+B 触发了全字匹配,从而搜索不到文件,你只要在按一下 Ctrl+B 关闭即可。 + +![image-20200908004645736](https://img.kuizuo.cn/image-20200908004645736.png) + +## 总结 + +想必你如果已经安装完,并且试用一番,你可能会忍不住给该作者捐赠。没错,Everything 谁用谁知道,windows 必装之一。 + +但如果你并没有反复打开和搜索文件的需求,而电脑只是用来刷刷剧,玩玩游戏的话,请赶快把这篇文章关掉,不适合你。但如果有,赶快把这篇文章分享给别人吧,也许你的分享能让别人少掉几根头发。 diff --git "a/docs/tools/IDEA\345\237\272\346\234\254\351\205\215\347\275\256.md" "b/docs/tools/IDEA\345\237\272\346\234\254\351\205\215\347\275\256.md" new file mode 100644 index 0000000..15a26f9 --- /dev/null +++ "b/docs/tools/IDEA\345\237\272\346\234\254\351\205\215\347\275\256.md" @@ -0,0 +1,217 @@ +--- +id: idea-config +slug: /idea-config +title: IDEA基本配置 +date: 2022-01-06 +authors: kuizuo +tags: [Jetbrains, idea, java, 工具] +keywords: [Jetbrains, idea, java, 工具] +--- + +准备系统的学习一遍 java(主要是后端与安卓),所以就免不了使用业界好评最高的 IDE 工具——IDEA。 + +同时在写这篇之前,JetBrains 全家桶就没怎么使用过,基本上我能用 vscode 我都用 codeRun 插件来运行,但对于一个大型项目,何况是开发 java 项目的话,vscode 有点难以胜任,加上后续会使用 GoLand,PyCharm 这些,所以很有必要记录下 JetBrains 全家桶的一些基本操作。 + +ps: 我本地电脑基本把大部分 JetBrains 产品给安装了一遍,而在去年 1 月 13 号淘宝上买的一个账号用于激活,到现在整整一年时间都没怎么使用 JetBrains 产品 😂 + +这里有一份我的[配置文件](https://pan.kuizuo.cn/s/Bpf0),在最后也会说明配置的导入与导出。 + + + +## 插件 + +### 主题图标 + +`Atom Material Icons` 设置文件图标 + +`Material Theme UI` 设置主题 (我一般设置 Atom One Dark Theme 这个主题) + +`Rainbow Brackets` 彩虹括号 + +--- + +说实话,IDEA 内置集成了一堆好用的功能,比如 TODO,Git,这些在 VSCode 中插件的体验甚至有不如 IDEA。(尤其是这 Git 用过都说好),后续有其他插件才进行补充。 + +## 快捷键 + +[IntelliJ IDEA 常用快捷键 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/61690346) + +### Ctrl + +- Ctrl+D 复制光标所在行 或 复制选择内容,并把复制内容插入光标位置下面 + +- Ctrl+W 递进式选择代码块。可选中光标所在的单词或段落,连续按会在原有选中的基础上再扩展选中范围 + +- Ctrl+E 显示最近打开的文件记录列表 + +- Ctrl+N 根据输入的 名/类名 查找类文件 + +- Ctrl+G 在当前文件跳转到指定行处 + +- Ctrl+F12 弹出当前文件结构层,可以在弹出的层上直接输入,进行筛选 + +- Ctrl+左键单击 在打开的文件标题上,弹出该文件路径 + +### Alt + +- Alt+1,2,3…9 显示对应数值的选项卡,其中 1 是 Project 用得最多 (必备) + +- Alt+Enter 根据光标所在问题,提供快速修复选择,光标放在的位置不同提示的结果也不同 + +- Alt+Insert 代码自动生成,如生成对象的 set / get 方法,构造函数,toString() 等 + +- Alt+左方向键 切换当前已打开的窗口中的子视图,比如 Debug 窗口中有 Output、Debugger 等子视图,用此快捷键就可以在子视图中切换 + +- Alt+右方向键 按切换当前已打开的窗口中的子视图,比如 Debug 窗口中有 Output、Debugger 等子视图,用此快捷键就可以在子视图中切换 + +- Alt+前方向键 当前光标跳转到当前文件的前一个方法名位置 + +- Alt+后方向键 当前光标跳转到当前文件的后一个方法名位置 + +### Ctrl + Alt + +- Ctrl+Alt+L 格式化代码,可以对当前文件和整个包目录使用 + +- Ctrl+Alt+T 选择代码,可以将其包裹在 if、try、while 等代码块中 + +- Ctrl+Alt+O 优化导入的类,可以对当前文件和整个包目录使用 + +- Ctrl+Alt+V 快速引进变量 + +- Ctrl+Alt+S 打开 IntelliJ IDEA 系统设置 + +- Ctrl+Alt+Enter 光标所在行上空出一行,光标定位到新行 + +- Ctrl+Alt+左方向键 退回到上一个操作的地方 + +- Ctrl+Alt+右方向键 前进到上一个操作的地方 + +- Ctrl+Alt+前方向键 在查找模式下,跳到上个查找的文件 + +- Ctrl+Alt+后方向键 在查找模式下,跳到下个查找的文件 + +### Ctrl + Shift + +- Ctrl+Shift+Z 取消撤销 + +- Ctrl+Shift+W 递进式取消选择代码块。可选中光标所在的单词或段落,连续按会在原有选中的基础上再扩展取消选中范围 + +- Ctrl+Shift+T 对当前类生成单元测试类,如果已经存在的单元测试类则可以进行选择 + +- Ctrl+Shift+C 复制当前文件磁盘路径到剪贴板 + +- Ctrl+Shift+V 弹出缓存的最近拷贝的内容管理器弹出层 + +- Ctrl+Shift+E 显示最近修改的文件列表的弹出层 + +- Ctrl+Shift+A 查找动作 / 设置 + +- Ctrl+Shift+/ 代码块注释 + +- Ctrl+Shift+[ 选中从光标所在位置到它的顶部中括号位置 + +- Ctrl+Shift+] 选中从光标所在位置到它的底部中括号位置 + +- Ctrl+Shift++ 展开所有代码 + +- Ctrl+Shift+- 折叠所有代码 + +- F3 选择下一个单词 + +- Ctrl+Shift+F7 高亮显示所有该选中文本,按 Esc 高亮消失 + +- Ctrl+Shift+Space 智能代码提示 + +- Ctrl+Shift+Enter 自动结束代码,行末自动添加分号 + +- Ctrl+Shift+ackspace 退回到上次修改的地方 + +- Ctrl+Shift+左键单击 把光标放在某个类变量上,按此快捷键可以直接定位到该类中 + +- Ctrl+Shift+左方向键 在代码文件上,光标跳转到当前单词 / 中文句的左侧开头位置,同时选中该单词 / 中文句 + +- Ctrl+Shift+右方向键 在代码文件上,光标跳转到当前单词 / 中文句的右侧开头位置,同时选中该单词 / 中文句 + +- Ctrl+Shift+前方向键 光标放在方法名上,将方法移动到上一个方法前面,调整方法排序 + +- Ctrl+Shift+后方向键 光标放在方法名上,将方法移动到下一个方法前面,调整方法排序 + +### Alt + Shift + +- Alt+Shift+前方向键 移动光标所在行向上移动 + +- Alt+Shift+后方向键 移动光标所在行向下移动 + +### 调试 + +- Shift+F9 调试模式运行 + +- F7 步入,如果当前行断点是一个方法,则进入当前方法体内 + +- F8 步过,如果当前行断点是一个方法,则不进入当前方法体内 + +- Shift+F8 跳出步入的方法体外 + +- F9 恢复程序运行,但是如果该断点下面代码还有断点则停在下一个断点上 + +### 自定义 + +在 vscode 中我通常都会设置光标键,比如 + +- Shift+Alt+J 左光标移动 +- Shift+Alt+K 下光标移动 +- Shift+Alt+L 右光标移动 +- Shift+Alt+I 上光标移动 +- Shift+Alt+; 光标移动至行尾,相当于 End 键 +- Shift+Alt+H 光标移动至行首,相当于 Home 键 + +同样的在 IDEA 肯定也要如此设置 + +在 setting 中找到 keymap,分别搜索关键字 up down left right home end 分别为其设置快捷键,如果遇到快捷键冲突,会提示 REMOVE(移除)还是 LEAVE(保留),选择 REMOVE(这些都是用处相对小的功能键,可以直接覆盖,介意者忽视) + +还有一些快捷键针对 VSCode 我这就列举一下 + +- 重命名 `rename Shift+F6` ⇒ `F2` +- 重做 `Ctrl + Shift + Z` ⇒ `Ctrl + Y` +- 打开终端 `Alt + F12` ⇒ Ctrl + `(根据终端使用率) + +如果习惯了 Vscode 快捷键方式也可以通过插件 VSCode Keymap 来对 IDEA 快捷键进行映射,相对使用成本会有所降低。 + +## 其他操作 + +### 快速生成代码 + +通过缩写或后缀的方式快速完成一些代码的补全,一般写完,按 tab 或回车即可。罗列一些比较常用的: + +| 代码 | 效果 | +| ------------ | -------------------------- | +| psvm | 自动生成 main 函数 | +| .var | 自动为对象生成声明 | +| sout / .sout | 输出:System.out.println() | +| .if | 生成 if 判断 | +| .for | 生成循环,默认是高级 for | +| .try | 生成 try … catch | + +可在 Settings ⇒ Editor ⇒ Live Templates 中 根据对应的语言生成相应的模板,也可自定义生成 + +![image-20220106052026798](https://img.kuizuo.cn/image-20220106052026798.png) + +### 修改 Maven 依赖仓库位置 + +一般 Maven 所下载的依赖都会存储在`C:\User\{user}\.m2\repository` ,通过下图位置可以将其移动到其他地方。 + +![image-20220106052100190](https://img.kuizuo.cn/image-20220106052100190.png) + +## 配置导入与导出 + +具体操作如下图,根据自己需要进行导入与导出 + +![image-20220616135757525](https://img.kuizuo.cn/image-20220616135757525.png) + +导出 + +![image-20220616135810570](https://img.kuizuo.cn/image-20220616135810570.png) + +导入 + +![image-20220616135847464](https://img.kuizuo.cn/image-20220616135847464.png) diff --git "a/docs/tools/Jetbrains\347\263\273\345\210\227\344\272\247\345\223\201\346\277\200\346\264\273\346\226\271\346\263\225.md" "b/docs/tools/Jetbrains\347\263\273\345\210\227\344\272\247\345\223\201\346\277\200\346\264\273\346\226\271\346\263\225.md" new file mode 100644 index 0000000..dd22d3e --- /dev/null +++ "b/docs/tools/Jetbrains\347\263\273\345\210\227\344\272\247\345\223\201\346\277\200\346\264\273\346\226\271\346\263\225.md" @@ -0,0 +1,131 @@ +--- +id: jetbrains-product-activation-method +slug: /jetbrains-product-activation-method +title: Jetbrains系列产品激活方法 +date: 2020-09-03 +authors: kuizuo +tags: [Jetbrains, 工具] +keywords: [Jetbrains, 工具] +--- + +![jetbrains](https://img.kuizuo.cn/jetbrains.jpg) + + + +## 前言 + +> 参考链接 [知了](https://zhile.io/2018/08/25/jetbrains-license-server-crack.html) + +无论你是什么开发者,多多少少肯定听过`Jetbrains`,或者肯定见过相关类似界面,如果真不知道问问百度。官网 [点我去下载](https://www.jetbrains.com/zh-cn)。 + +**本文内容只用于学习,请勿用于商务用途,请支持正版!** + +## 开始激活 + +需要说下 Jetbrains 产品的两种外面主流激活方式 + +- 激活码激活(一般没隔半年就要重新百度新的激活码,非常不推荐) +- `jetbrains-agent`补丁(使用期到 2089 年,但有局限,本文着重举这个方法) +- 去购买账号 20 元一年(本人已用该方法,不贵且方便(支持所有 Jetbrains 产品激活),推荐) + +### 下载激活工具 + +首先,请下载`jetbrains-agent-latest.zip`工具 [点我下载](https://wwe.lanzous.com/i3UTYjdd0mh) 解压会看到两个文件 一个`jetbrains-agent-latest.zip`不用再解压另一个安装参数(后面会用到) + +我用这个激活工具实现`PyCharm`和`WebStorm`还有`IDEA`的激活,其余类似产品的激活方式都一样。 + +**重点来了!这种激活是有前提的** + +1. **软件不要更新! 尽可能用旧版本** + + 尽管这个补丁是 2020 年 4 月 10 日的,但有可能软件更新后会用不了,所以要破解请不要更新或安装最新版(本文 9 月 8 号已测试没问题),毕竟人家软件商又不傻,能让你白嫖免费用最新的,如图下载其他版本。 + +![image-20200903064643648](https://img.kuizuo.cn/image-20200903064643648.png) + +2. **清除 hosts 文件内有关 jetbrains** + + 如果你在之前就接触过这类软件的激活使用,那么有可能别人的文章是让你这么做的 + + 添加一行`0.0.0.0 account.jetbrains.com`到`C:\Windows\System32\drivers\etc\hosts`文件中 + + 而现在,请把上面那一行删掉,没必要,甚至你都有可能都访问不了 jetbrains 官网 + +### 运行要激活的软件 + +1. 首先运行软件(这里以 IDEA2019.3 为例),如果是第一次的话会进行一些正常配置然后弹出一个如下注册框,勾选 Evaluate for free, 点击 Evaluate: + +![wps1](https://img.kuizuo.cn/wps1.jpg) + +​ 正常进入到工具编程开始界面,进入第二步 + +2. 用鼠标拖动下载完的激活工具`jetbrains-agent-latest.zip`文件到 到编程界面,或者一开始创建项目页面 + + ![image-20200903070702901](https://img.kuizuo.cn/image-20200903070702901.png) + + 提示选择 Restart 重启软件,这里就不放图了。 + +3. 重新打开 idea,激活方式默认`Activation code`,啥也别改 直接点击为 IDEA 安装即可 + +![image-20200903070849707](https://img.kuizuo.cn/image-20200903070849707.png) + +​ 补充一下,如果你是用我提供给你的补丁的话,可能会遇到如下图 + +![image-20200908111018549](https://img.kuizuo.cn/image-20200908111018549.png) + +​ 有个安装参数,你把下面的文本复制粘贴到输入框即可 + +``` +LFq51qqupnaiTNn39w6zATiOTxZI2JYuRJEBlzmUDv4zeeNlXhMgJZVb0q5QkLr+CIUrSuNB7ucifrGXawLB4qswPOXYG7+ItDNUR/9UkLTUWlnHLX07hnR1USOrWIjTmbytcIKEdaI6x0RskyotuItj84xxoSBP/iRBW2EHpOc +``` + +接着提示安装 jetbrains-agent 成功.... 选择是就对了。 + +4. 稍等片刻,这时候点击 Help->About 查看到期时间 2089 年 + +![image-20200903071235404](https://img.kuizuo.cn/image-20200903071235404.png) + +没错现在 IDEA 已经成功激活破解了,这时候再点 Help->Register 查看注册情况 + +![image-20200903071428877](https://img.kuizuo.cn/image-20200903071428877.png) + +就此 IDEA2019.3 已成功激活破解。就是这么简单。 + +### 补充几点 + +现在你已能成功破解 Jetbrains 相关的软件,但我还需要补充几点 + +首先在 Help->Edit Custom VM Options 中,你可以看到`-javaagent:C:\Users\Public\.jetbrains\jetbrains-agent-v3.2.0.de72.619`这个字样 + +![image-20200903071942873](https://img.kuizuo.cn/image-20200903071942873.png) + +也就是这个,决定了你能否运行 IDEA 的关键,现在我找到对应的目录下,把这两个文件先移走,然后重新运行 IDEA,你就会发现运行不了,同样的你若删除了这一行也是运行不了的。反正闲着没事就别管这些地方,甚至你都不用修改软件对应`bin`下的以`.exe.vmoptions`后缀文件里的内容。 + +![image-20200903072112685](https://img.kuizuo.cn/image-20200903072112685.png) + +为什么要说这个呢,因为你到时候如果是要用其他的补丁,可能要你更改的就是上面那文件的对应路径或者 bin 下对应的两个文件,你到时候根据对应的使用方式修改就行,并不难。 + +第二点,也是最坑的一点! + +你在执行完第三步的时候,重启后,发现还在`License Activation`激活界面,先不管,在点击试用,然后点开 Help->About 发现显示到期时间不是 2089 年,再点 Help->Register 查看注册情况,发现在`Activation code`中并没有内容,然后尝试把激活工具里的`Activation code.txt`里的激活码复制到上面,然后就出现如下图的情况 + +![image-20200903073459886](https://img.kuizuo.cn/image-20200903073459886.png) + +~~这里我就用 Go 来做演示了,卸载重装够折腾的了~~,Key is invalid.啥玩意? + +正常你按我上述的步骤是不会出现这的,而出现这种情况有两种原因 + +1. 你的 agent 是真的没有配置好,请把上述步骤重新做一遍,然而一般不会是这个问题。 + +2. 软件是最新版的,我上面说到,软件不宜最新,破解补丁和版本不匹配,就会遇到这种情况。 + + 这时候的解决办法 + + 1. 找最最新的破解补丁 (我提供给你的已经是最新的了,就没必须要在折腾了,像上面的 go 语言也是能成功破解不会出现`Key is invalid`的) + 2. 使用较旧版本 (直接官网就旧版本下载即可) + 3. **购买激活码或账号(有钱,任性)**,个人建议直接去淘宝购买一个账号 20 元一年,主要使用插件激活是真的折腾,账号联网激活,是可以激活最新版的但要注意的是,你需要把我上面所说的 Help->Edit Custom VM Options 将`-javaagent:C:\Users\Public\.jetbrains\jetbrains-agent-v3.2.0.de72.619` 注释掉 + +## 总结 + +最后要说一句,环境配置这些说实在话挺折腾人的,在我学习阶段中,在这些配置中就花费了大量的时间。从环境变量,到下载各种开发工具,安装各种插件,各种包,库等等,有的还需要破解。这期间每一次都至少来回反复了 4,5 遍,都经历过几天折磨,安装卸载重启都成了家常便饭了,但这又是学习中必不可少的一个阶段。同时每次成功配置完,那种感觉真的只有经历过的人才会知道,也驱使我不断前进。 + +有时候可能都忘记之前这个环境是怎么安装使用的,想更新一下软件,又怕花费太多时间,甚至在我写这篇文章的时候,我都快忘记我之前是怎么破解的(然而我离我最近一次破解也不过一个月左右),然后又百度相关了各种破解,同时也算是搞懂了`key is invalid.`这个问题和解决方式,也同时记录一下。 diff --git "a/docs/tools/VScode\347\233\270\345\205\263\351\205\215\347\275\256.md" "b/docs/tools/VScode\347\233\270\345\205\263\351\205\215\347\275\256.md" new file mode 100644 index 0000000..acd7dac --- /dev/null +++ "b/docs/tools/VScode\347\233\270\345\205\263\351\205\215\347\275\256.md" @@ -0,0 +1,324 @@ +--- +id: vscode-config +slug: /vscode-config +title: VScode相关配置 +date: 2021-08-03 +authors: kuizuo +tags: [vscode, 开发工具, 配置] +keywords: [vscode, 开发工具, 配置] +--- + +关于 vscode 介绍和安装啥的不在这浪费口舌,上号就完事了! + +![vscode上号](https://img.kuizuo.cn/vscode%E4%B8%8A%E5%8F%B7.jpg) + + + +## 前言 + +`vscode` 算是我用的最多的一款文本编辑器,也是我用过最好用的文本编辑器,这一年都在和 vscode 打交道,不得不说一句微软牛逼! + +这里我会推荐一些关于 vscode 的一些相关配置,与常用操作对我来说能提高我一定编写代码的效率(常用不写)。 + +## 插件推荐 + +### GitHub Copilot + +AI 写代码,用过都说好。 + +官网地址 [GitHub Copilot · Your AI pair programmer](https://copilot.github.com/) + +### Bracket Pair Colorizer 2 + +![image-20210817213845020](https://img.kuizuo.cn/image-20210817213845020.png) + +如果你不希望你的代码中白茫茫一片的,或者说想让括号更好看一点,那么这个插件特别推荐。此外,有时候代码写多了,要删除嵌套括号的时候,如果有颜色标识,在寻找的时候必然是轻松的一件事情。 + +现 Vscode 自带该功能,无需安装插件,在设置中搜索 Bracket Pair Colorization,勾选即可。 + +![image-20220610012923130](https://img.kuizuo.cn/image-20220610012923130.png) + +### indent-rainbow + +正如插件名,彩虹缩进,能让你的代码中不同长度的缩进呈现不同的颜色(上面的代码缩进有略微的颜色差),有时候在缩进特别多的时候尤其有效,当然,配合 VScode 快捷键`Ctrl + Shift + \`能快速定位下一个括号所在的位置 + +### Prettier + +首先要知道 vscode 代码格式化快捷键是 Shift + Alt + F,然而 vscode 自带的代码格式化对于一些文件并没有格式化操作,比如 vue,这时候你下载这个插件即可格式化 vue 代码。 + +我在用了 vscode 半年后才知道有这么好用的格式化插件,之前用的是 Beautify 但是格式化的效果,并不是我满意的,并且同样的有些文件并未能格式化。如果还在用 Beautify,果然换 Prettier 准没错。 + +如果是 Vue2 用户的话,Vetur 是必装一个插件,不仅能格式化代码,还能提供相对于的提示,如果转型为 Vue3 的话,同样也有插件 Volar 可供选择。 + +### ESLint + +前端工程化代码规范必备,无需多言。 + +### Turbo Console Log + +这个一定要安利一波,有时候测试 js 代码并不需要调试那么复杂,只是想输出一下结果是什么,然后就要反复的输入`console.log()`,而这个插件就可以一键帮你得到想要输出的结果。一键 注释 / 启用 / 删除 所有 `console.log`,这也是我最常用的一个插件之一。 + +所要用到的快捷键: + +- ctrl + alt + l 选中变量之后,使用这个快捷键生成 console.log +- alt + shift + c 注释所有 console.log +- alt + shift + u 启用所有 console.log +- alt + shift + d 删除所有 console.log + +输出的路径则是根据当前代码所在的文件,行数,作用域,变量输出一遍(前面还带有一个小火箭 🚀),如下(输出变量 a) + +`console.log('🚀 ~ file: demo.ts ~ line 111 test ~ a', a)` + +有点可惜的是该插件不支持自定义快捷键。 + +### Live Server + +安装这个插件后,右下角会出现 Go Live 的按钮,点击试试,如果你当前根目录正好是有 index.html 这个文件,那么它将会打开你浏览器开启一个本地服务器,端口默认为 5500,并浏览所写的 html 代码,如果没有则是目录文件管理。同样对文件右键也有 Open with Live Server 字样 + +要注意的时,你 vscode 打开的是一个文件夹,并非一个单文件,不然是没有 Go Live 按钮的。 这个插件用来打开一些要基于 web 服务器的才能打开的静态页面的时候异为方便。 + +### Live Share + +注意哈,和上者插件名字大不相同,功能也完全不同,这是用于多人同步的一个插件,只需要登录 Github 或 Microsoft 账号,就可以将自己的本地代码实时共享给别人看,同时也能实战显示对方这时候所指的代码位置,还能发送信息,在多人远程协作的时候无疑是一把利器。 + +### REST Client + +允许在 Vscode 中发送 http 请求的并在 Vscode 中查看响应,我个人在做协议分析的时候常常用到,有多好用呢, + +可以直接将抓包的 http 请求部分,直接 vscode 中创建临时文件并复制进去。需要的时候直接保存成.http 文件即可永久使用。右键选择`Generator Code Snippet`或快捷键`Ctrl + Alt + C`还能够直接生成不同编程语言发送 HTTP 的例子。体验效果甚至堪比一些 HTTP 请求工具(说的就是你 PostMan) + +![image-20210817221312429](https://img.kuizuo.cn/image-20210817221312429.png) + +:::tip 是点击左上角灰色的 Send Request,如果有安装 Code Runner 的用户,容易直接点成右上角的播放键. + +::: + +### Thunder Client + +![image-20221003223247386](https://img.kuizuo.cn/image-20221003223247386.png) + +要想在 Vscode 拥有 Postman 或者 ApiPost 的接口调试工具,不妨使用这个插件,支持分类,环境变量,如果仅作为个人测试,不要求接口分享,这个插件就足以满足大部分日常 api 接口调试。 + +### CSS Peek + +快速查看 CSS 定位的地方,使用也方便,直接按住 Ctrl 对准要查看的样式的类名,然后在补一个鼠标左键即可定位。按住 Ctrl 同样适用于其他定位,如函数,变量等等。。。 + +### Project Manager + +![image-20220610013640476](https://img.kuizuo.cn/image-20220610013640476.png) + +对于一些常用项目而言,可以通过该插件添加到 Vscode 中,直接在左侧项目管理器中便可直接使用 vscode 打开项目工程。 + +还有挺多使用插件没介绍到,个人建议还是直接下载对应的配置文件,将其导入即可,配置文件包含插件、主题、快捷键,布局等等。 + +## 快捷键 + +### 常用快捷键 + +一些 Ctrl + C 和 Ctrl + V 等就不做过多解释了,主要说一些有可能不知道,并且还在通过鼠标还完成的一些操作。 + +- Shift+Alt+F 代码格式化 +- Shift+Alt+R 在资源管理器中显示 (右键点文件在选择老累了) +- Shift+Alt+A 多行注释 +- Shift+Alt+向下箭头 复制当前行到下一行 +- Ctrl+D 下一个匹配的也被选中 +- Ctrl+F2 匹配所有当前选中文本 +- Ctrl+Shift+L 获取将当前所选内容的所有匹配项 方便快捷删改(上一操作的升级版) +- Ctrl+Alt+向下箭头 批量复制光标(向下也同理) +- Ctrl+~ 打开终端 +- Ctrl+Shift+[ 代码折叠 +- Ctrl+Shift+] 代码展开 +- Ctrl+K Ctrl+0 全部折叠 +- Ctrl+K Ctrl+J 展开全部 + +- Ctrl+Backspace 删除前一个单词(特别有用) + +- Ctrl+Alt+右箭头 快捷将当前文件移动到右边单独标签组 (不用在鼠标点击分页按钮) + +- Ctrl+Shift+右箭头 可以逐个选择文本,方便 + +- 如果可以 使用 Ctrl + Shift + K 删除一行 而不是通过 Ctrl +X 剪贴一行 + +以下功能,能用快捷键就别用鼠标了 + +- **Ctrl+E/P 跳转到近期文件(再次按下即可切换下一个文件,加 Shift则是上一个文件)** +- **Ctrl+Tab 切换 Tab (类比于 window Alt+Tab)在已显示的 Tab 切换比上面好用一些** +- Ctrl+G 跳转到某行(别再滚动鼠标了) +- Ctrl+Shift+O 跳转(列举)当前文件某个函数 +- **Ctrl+T 全局搜索某个函数(markdown 则是标题)** +- Ctrl+N 创建一个临时文件(别再鼠标双击 tab 栏了) +- Ctrl+W 关闭当前 Tab 页面(浏览器适用,别加 Shift,别再鼠标点击关闭按钮了) +- **Ctrl+Shift+T 打开刚刚关闭的页面(手残必备,浏览器适用)** +- Ctrl+B 切换左侧导航栏 + +以上基本就是我常用的快捷键了,可以说些快捷键,确实提升了我编写代码的效率。这里强烈建议马上打开 Vscode,在不借用鼠标的情况下,使用以上快捷键。会有意想不到的使用体验! + +### 自定义快捷键 + +同时 vscode 也支持开发者自定义快捷键使用。主要就是光标定位功能,有时候编写代码的时候,要经常移动光标到指定位置,这时候就需要右手去移动鼠标或者移动到方向键,反复这样操作,有没有什么办法不移动手的前提下移动光标,肯定有,主要也就两种。 + +- 专属定制键盘或者是可以设置宏按键的键盘,说一个键盘 HHKB 一个被神化为“程序员梦寐以求的神器”,有兴趣可以去搜一下。 +- 自定义快捷键 + +实际上很多时候都没必要自己设置快捷键,不过是为了满足一些人的需求,就比如我主要就设置了 6 个快捷键分别是: + +- Shift+Alt+J 左光标移动 +- Shift+Alt+K 下光标移动 +- Shift+Alt+L 右光标移动 +- Shift+Alt+I 上光标移动 +- Shift+Alt+; 光标移动至行尾,相当于 End 键 +- Shift+Alt+H 光标移动至行首,相当于 Home 键 +- Shift+Alt+U 选中代码片段,即可合并成一行代码。 + +设置的话也比较简单,打开设置,找到键盘快捷方式,然后找到光标移动的快捷键然后,或者是打开对应的 keybindings.json 文件,把下面代码添加即可。 + +```json +[ + { + "key": "shift+alt+j", + "command": "cursorLeft", + "when": "textInputFocus" + }, + { + "key": "shift+alt+l", + "command": "cursorRight", + "when": "textInputFocus" + }, + { + "key": "shift+alt+i", + "command": "cursorUp", + "when": "textInputFocus" + }, + { + "key": "shift+alt+k", + "command": "cursorDown", + "when": "textInputFocus" + }, + { + "key": "shift+alt+h", + "command": "cursorHome", + "when": "textInputFocus" + }, + { + "key": "shift+alt+oem_1", + "command": "cursorEnd", + "when": "textInputFocus" + }, + { + "key": "shift+alt+u", + "command": "editor.action.joinLines" + } +] +``` + +自定义快捷键也是因人而异,并非每个人都适合,键盘固然方便,但也没有鼠标来的直接。这里也只是提及一下我使用 vscode 中一些快捷键设置。 + +此外还设置了一些,例如配置语言特定,通过双击空白创建新文件的时候,默认是纯文本,想要格式为 js 或者其他的,需要点击右下小角来切换,特别麻烦,于是就给自己设置了一个快捷键 + +首先在键盘快捷方式找到配置语言特定的设置, + +设置为 Ctrl + i Ctrl + k 因人而异 + +## 代码提示 + +相信你在使用`vscode`中,肯定有过这样的问题,明明引入本地模块,但是有的时候就是没有对应的代码提示。如图 + +![image-20200901212906150](https://img.kuizuo.cn/image-20200901212906150.png) + +像导入本地模块`fs`,却没有代码提示,想要有本地模块代码提示,最快捷的方法就是通过下面一行代码 + +```bash +npm install @types/node +``` + +但是如果你像上面那样,目录下没有`package.json`文件是肯定安装不上去的,这时候是需要初始化项目结构也就是执行下面的代码 + +```bash +npm init +或 +npm init -y +``` + +然后在目录下你就能看到`node_modules`,在这个文件夹下有一个`@types`,这个目录就是存放你以后代码提示的目录,现在`@types`里面有`node`这个文件夹,也就是我们刚刚这个命令`npm install @types/node`后的 node,现在试试看确实是有代码提示了,并且还有带星推荐。 + +![image-20200901214223439](https://img.kuizuo.cn/image-20200901214223439.png) + +现在,我的代码里有`jquery`代码,但是本地已有`jquery.js`文件,又不想安装`jquery`的模块,但是又要`jquery`的代码提示,这时候你就可以输入下面代码,就能看到对应的代码。 + +```bash +npm install @types/jquery +``` + +![image-20200901214906038](https://img.kuizuo.cn/image-20200901214906038.png) + +在比如有的库安装会没带代码提示,这时候就用上面的方法同样也可以有代码提示,例如`express` + +`express`相关安装操作我就不赘述了,先看图片 + +![image-20200901215612611](https://img.kuizuo.cn/image-20200901215612611.png) + +这 app 代码提示怎么全是 js 自带的代码提示。 + +然后在看`node_modules\@types`下,怎么只有我刚刚安装的那几个? + +![image-20200901215826419](https://img.kuizuo.cn/image-20200901215826419.png) + +不妨试试 + +```bash +npm install @types/express +``` + +这时候`node_modules\@types`下,就多了几个文件夹,其中一个名为 express,那么现在代码提示肯定有了。 + +![image-20200901220225659](https://img.kuizuo.cn/image-20200901220225659.png) + +果不其然,`vscode`里也有正常的代码提示了 + +![image-20200901220329481](https://img.kuizuo.cn/image-20200901220329481.png) + +:::info + +要注意的是,如果导入的库所采用的是 TypeScript 所书写的,那么就无需引用@types/xxx。而一些远古的库所采用的 JavaScript 编写的,所以自然没有代码提示,就需要借用 typescript 官方提供的@types/xxx 包。 + +::: + +从上面的例子中,可以得出`@types`这个文件夹里存放的都是`vscode`当前工作区的代码提示文件,想要对应的代码提示就直接`npm i @types/模块名`即可,如果你当前工作区没有代码提示,那么多半是这个问题。 + +### 自定义代码提示与快捷输入 + +这里补充一下,有时候我想自己定义一个代码提示,有没有办法呢,当然有,如果你恰巧学过 java,想必每次写`System.out.println`都痛苦的要死,这时候你就可以像这样 + +1. 创建一个.vscode 文件夹,在文件夹里创建一个名为`kuizuo.code-snippets`(只要后缀是 code-snippets 就行) +2. 在这个文件内写上如下代码 + +```json +{ + "System.out.println": { + "scope": "java", + "prefix": "syso", + "body": ["System.out.println($1);"], + "description": "输出至控制台,并带上换行符" + } +} +``` + +- System.out.println 为代码块的名字,无需强制。 +- prefix:触发代码片段 +- body:按下 TAB 后触发的内容填充,注意是一个数组类型,每行都需要用双引号修饰,不能使用模板字符串 +- description:代码提示内容 +- scope: 作用的语言,可多选,如"javascript,c" +- $+数字: 为光标的定位符,有多个则 Tab 跳转下个光标位置 + +上则代码的意思就是输入 prefix 内的`syso` 然后按下 tab 键就会把 body 内的`System.out.println($1);`代码提示显示出来,其中`$1`为光标位置,如图 + +![](https://img.kuizuo.cn/syso.gif) + +但一般很少用到代码块,很多现成的插件就可以完全满足对应代码补全的需求,但有时候会方便很多。 + +像一些插件内会自带的代码提示,能不能“偷”过来使用一下呢,答案是肯定能的,这里我就已 autoj -pro 为例,(没了解过该软件可以忽视) + +1. 首先安装 autoJS_pro 插件,然后进入 C:\Users\Administrato\\.vscode\extensions\hyb1996.auto-js-pro-ext.... (Administrator 为用户名) +2. 找到以 snippets 结尾的文件,打开全选复制其中的代码。 +3. 打开 vscode,如上操作,创建一个.vscode 文件夹,后同 +4. 把复制的代码段粘贴到我们创建的 snippets 文件,卸载 auto.js-pro 插件,重启即可 diff --git "a/docs/tools/Vite\347\233\270\345\205\263\346\217\222\344\273\266.md" "b/docs/tools/Vite\347\233\270\345\205\263\346\217\222\344\273\266.md" new file mode 100644 index 0000000..662ffc0 --- /dev/null +++ "b/docs/tools/Vite\347\233\270\345\205\263\346\217\222\344\273\266.md" @@ -0,0 +1,512 @@ +--- +id: vite-plugin +slug: /vite-plugin +title: Vite相关插件 +date: 2022-04-10 +authors: kuizuo +tags: [vue, vite] +toc_max_heading_level: 2 +--- + +记录下日常使用 Vite 的一些相关插件。 + + + +## [unplugin-auto-import](https://github.com/antfu/unplugin-auto-import) + +**自动导入 vue3 相关方法,支持`vue`, `vue-router`, `vue-i18n`, `@vueuse/head`, `@vueuse/core`等自动引入** + +效果 + +```javascript +// 引入前 +import { ref, computed } from 'vue' +const count = ref(0) +const doubled = computed(() => count.value * 2) + +//引入后 +const count = ref(0) +const doubled = computed(() => count.value * 2) + +// 引入前 +import { useState } from 'react' +export function Counter() { + const [count, setCount] = useState(0) + return
{count}
+} + +//引入后 +export function Counter() { + const [count, setCount] = useState(0) + return
{count}
+} +``` + +安装 + +```bash +npm i -D unplugin-auto-import +``` + +[完整配置](https://github.com/antfu/unplugin-auto-import#configuration) + +```javascript title="vite.config.js" +import { defineConfig } from 'vite' +import AutoImport from 'unplugin-auto-import/vite' + +export default defineConfig({ + plugins: [ + AutoImport({ + imports: ['vue', 'vue-router', 'vue-i18n', '@vueuse/head', '@vueuse/core'], + dts: 'src/auto-import.d.ts', + // 可以选择auto-import.d.ts生成的位置(默认根目录),建议设置为'src/auto-import.d.ts' + }), + ], +}) +``` + +原理: 自动生成 auto-imports.d.ts 文件用于代码提示,如下 + +```typescript title="auto-imports.d.ts" +// Generated by 'unplugin-auto-import' +// We suggest you to commit this file into source control +declare global { + const ref: (typeof import('vue'))['ref'] + const reactive: (typeof import('vue'))['reactive'] + const computed: (typeof import('vue'))['computed'] + const createApp: (typeof import('vue'))['createApp'] + const watch: (typeof import('vue'))['watch'] + const customRef: (typeof import('vue'))['customRef'] + const defineAsyncComponent: (typeof import('vue'))['defineAsyncComponent'] +} +export {} +``` + +:::warning 注意:由于没有局部导入,在代码跳转查看时,就会跳转到 auto-imports.d.ts 文件,然后再跳转到原定义位置。 + +::: + +### 响应式语法糖 + +[Reactivity Transform | Vue.js (vuejs.org)](https://vuejs.org/guide/extras/reactivity-transform.html) + +使用$ref 在使用时,无需.value。演示如下 + +```javascript +import { $ref, $$ } from 'vue/macros' // $ref是vue/macros包下的 + +// bind ref as a variable +let count = $ref(0) + +// assignments are reactive +count++ + +// get the actual ref +console.log($$(count)) // { value: 1 } +``` + +- 可以用`$()`来解构响应式对象,这样就不用写`.value` +- 可以用`$$()`来获取原有的响应式对象 + +在 vite.config.ts 文件里,加上`reactivityTransform: true` + +```javascript title="vite.config.js" +plugins: [ + vue({ + reactivityTransform: true, + }), +] +``` + +可以使用 unplugin-auto-import 中的导入`vue/macros`无需 import 导入。 + +```javascript title="vite.config.js" +AutoImport({ + imports: ['vue/macros'], + dts: true, +}) +``` + +## [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components) + +**自动导入 UI 库,按需导入。很多组件库都推荐这种方式导入例如 Element Plus 、Ant Design Vue** + +安装 + +```bash +npm install unplugin-vue-components -D +``` + +```javascript title="vite.config.js" +import { defineConfig } from 'vite' +import Components from 'unplugin-vue-components/vite' +import { + ElementPlusResolver, + AntDesignVueResolver, + VantResolver, + HeadlessUiResolver, + ElementUiResolver +} from 'unplugin-vue-components/resolvers' + +export default defineConfig({ + plugins: [ + Components({ + // ui库解析器,也可以自定义 + resolvers: [ + ElementPlusResolver(), + AntDesignVueResolver(), + VantResolver(), + HeadlessUiResolver(), + ElementUiResolver() + ] + }) + ] +``` + +插件会生成一个 ui 库组件以及指令路径 components.d.ts 文件,如下 + +```typescript title="components.d.ts" +// generated by unplugin-vue-components +// We suggest you to commit this file into source control +// Read more: https://github.com/vuejs/vue-next/pull/3399 + +declare module 'vue' { + export interface GlobalComponents { + ElAside: (typeof import('element-plus/es'))['ElAside'] + ElButton: (typeof import('element-plus/es'))['ElButton'] + ElContainer: (typeof import('element-plus/es'))['ElContainer'] + ElHeader: (typeof import('element-plus/es'))['ElHeader'] + ElIcon: (typeof import('element-plus/es'))['ElIcon'] + ElMain: (typeof import('element-plus/es'))['ElMain'] + } +} + +export {} +``` + +只要你用过的组件都会自动导入,同时也可以导入自己的组件。 + +```javascript title="vite.config.js" +import { defineConfig } from 'vite' +import Components from 'unplugin-vue-components/vite' + +export default defineConfig({ + plugins: [ + Components({ + // 指定组件位置,默认是src/components + dirs: ['src/components'], + extensions: ['vue'], + // 配置文件生成位置 + dts: 'src/components.d.ts', + }), + ], +}) +``` + +## [unplugin-vue-define-options/vite](https://github.com/sxzz/unplugin-vue-define-options) + +**script setup 语法糖通过 defineOptions 定义组件 name、inheritAttrs、props、emits** + +安装 + +```bash +npm i unplugin-vue-define-options +``` + +```javascript title="vite.config.js" +import { defineConfig } from 'vite' +import DefineOptions from 'unplugin-vue-define-options/vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue(), DefineOptions()], +}) +``` + +在`tsconfig.json`设置 types,如下所示: + +```json +{ + "compilerOptions": { + "types": ["unplugin-vue-define-options"] + } +} +``` + +不然在 ts 项目中会提示 **找不到名称“defineOptions”**,具体使用如下 + +```vue + +``` + +输出 + +```vue + + + +``` + +如果只是想**单纯的设置组件名**的话,这个插件 [vite-plugin-vue-setup-extend](https://github.com/vbenjs/vite-plugin-vue-setup-extend) 可能更适合,只需要在 script 中添加一个 name 属性即可。 + +```vue + + + +``` + +## [vite-plugin-mock](https://github.com/vbenjs/vite-plugin-mock) + +**提供本地和生产模拟服务。** + +安装 + +```bash +npm i mockjs vite-plugin-mock +``` + +```javascript title="vite.config.js" +import { UserConfigExport, ConfigEnv } from 'vite' + +import { viteMockServe } from 'vite-plugin-mock' +import vue from '@vitejs/plugin-vue' + +export default ({ command }: ConfigEnv): UserConfigExport => { + return { + plugins: [ + vue(), + viteMockServe({ + // default + mockPath: 'mock', + localEnabled: command === 'serve', + }), + ], + } +} +``` + +## [vite-plugin-pages](https://github.com/hannoeru/vite-plugin-pages) + +**基于文件系统的动态路由。** + +安装 + +```bash +npm install -D vite-plugin-pages +npm install vue-router +``` + +```javascript title="vite.config.js" +import Pages from 'vite-plugin-pages' + +export default { + plugins: [ + Pages({ + dirs: 'src/views', + }), + ], +} +``` + +传统的 routes 写法 + +```javascript +// 1. 定义路由组件. +const Home = { template: '
Home
' } +const About = { template: '
About
' } + +// 2. 定义一些路由 +const routes = [ + { path: '/', component: Home }, + { path: '/about', component: About }, +] + +// 3. 创建路由实例并传递 `routes` 配置 +const router = VueRouter.createRouter({ + routes, +}) +``` + +而该插件则是导入整个 pages(views)下的 vue 文件作为路由,也有一套自定义的路由规则,类似 nuxt.js + +```main.js +import { createRouter } from 'vue-router' +import routes from '~pages' + +const router = createRouter({ + // ... + routes, +}) +``` + +## [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) + +配合`vite-plugin-pages`使用,可以在生成页面路由的基础上实现动态布局功能. + +安装 + +```bash +npm install -D vite-plugin-vue-layouts +``` + +```typescript +import Vue from '@vitejs/plugin-vue' +import Pages from 'vite-plugin-pages' +import Layouts from 'vite-plugin-vue-layouts' + +export default { + plugins: [Vue(), Pages(), Layouts()], +} +``` + +在对应的页面单文件中添加布局配置,在路由时即可按配置切换布局,将页面嵌入对应的布局文件之中. + +```vue + +meta: + layout: users + +``` + +## [vite-plugin-purge-icons](https://www.npmjs.com/package/vite-plugin-purge-icons) + +**方便的使用 [Iconify](https://icon-sets.iconify.design/) 图标。** + +安装 + +```bash + pnpm add @iconify/iconify + pnpm add vite-plugin-purge-icons @iconify/json -D +``` + +```javascript title="vite.config.js" +import PurgeIcons from 'vite-plugin-purge-icons' + +export default { + plugins: [ + PurgeIcons({ + /* PurgeIcons Options */ + }), + ], +} +``` + +在 main.js 中导入 `@purge-icons/generated` + +```javascript title="main.js" {3,4} +import { createApp } from 'vue' +import App from './App.vue' + +import '@iconify/iconify' +import '@purge-icons/generated' + +createApp(App).mount('#app') +``` + +使用 + +在 html 文件中指明 class 为 iconify,data-icon 为 iconify 对应的图标(直接复制官网的图标) + +```html + +``` + +当然,也可以自行封装一个 Icon 组件,像下面这样使用。 + +```html + +``` + +或者使用 vue3 版的 [Iconify for Vue](https://docs.iconify.design/icon-components/vue/index.html) 。 + +## [vite-plugin-windicss](https://github.com/windicss/vite-plugin-windicss) + +安装 + +```bash +npm i -D vite-plugin-windicss windicss +``` + +```javascript title="vite.config.js" +import WindiCSS from 'vite-plugin-windicss' + +export default { + plugins: [WindiCSS()], +} +``` + +之所以使用 Windi CSS,主要是[属性化模式](https://cn.windicss.org/features/attributify.html)太香了(预计会成为一个趋势),属性化默认情况下是可选的,可以在你的 windi 配置中开启。 + +```typescript title="windi.config.ts" +import { defineConfig } from 'windicss/helpers' + +export default defineConfig({ + attributify: true, +}) +``` + +并根据需要这样使用它们: + +```html + +``` + +语法 + +``` +(variant[:-]{1})*key? = "((variant:)*value)*" +``` + +## [vite-plugin-node](https://github.com/axe-me/vite-plugin-node) + +**允许您使用 vite 作为节点开发服务器。‎** + +暂时没有实际测试过,只是觉得有点意思。 + +## 总结 + +体验过一段时间的 Vite 开发,开发体验还是很满意的,这其中肯定与上面的插件有着密切联系。这次去翻看了一些项目,了解其中插件的使用。这里只是汇总了些常用的,还有更多相关插件可以去[awesome-vite](https://github.com/vitejs/awesome-vite#plugins)上查看。 + +> 参考文章: +> +> [尤大推荐的神器 unplugin-vue-components,解放双手!以后再也不用呆呆的手动引入(组件,ui(Element-ui)库,vue hooks 等) - 掘金 (juejin.cn)](https://juejin.cn/post/7012446423367024676) +> +> [vite2 常用插件篇(三)- 进阶插件 - 掘金 (juejin.cn)](https://juejin.cn/post/6996176490148659231) +> +> [Vite 之高效插件推荐 🍉 - 掘金 (juejin.cn)](https://juejin.cn/post/6998059092497399845) +> +> [vitejs/awesome-vite: ⚡️ A curated list of awesome things related to Vite.js (github.com)](https://github.com/vitejs/awesome-vite#plugins) diff --git "a/docs/tools/Wappalyzer\350\257\206\345\210\253\347\275\221\347\253\231\344\270\212\347\232\204\346\212\200\346\234\257.md" "b/docs/tools/Wappalyzer\350\257\206\345\210\253\347\275\221\347\253\231\344\270\212\347\232\204\346\212\200\346\234\257.md" new file mode 100644 index 0000000..960a683 --- /dev/null +++ "b/docs/tools/Wappalyzer\350\257\206\345\210\253\347\275\221\347\253\231\344\270\212\347\232\204\346\212\200\346\234\257.md" @@ -0,0 +1,39 @@ +--- +id: wappalyzer-recognize-technology +slug: /wappalyzer-recognize-technology +title: Wappalyzer识别网站上的技术 +date: 2021-07-20 +authors: kuizuo +tags: [chrome, 插件] +keywords: [chrome, 插件] +--- + +你是否还在为不知道心仪的网站所采用的技术栈而烦恼吗,如果是,那么这款 Chrome 插件值得你拥有 + + + +## 前言 + +作为前端开发者而言,浏览器可以说是我们的栖息地,随着大前端发展,催生出许许多多的技术栈,各式各样的网站迎刃而出。在面对一个悉知而又陌生的网站,想了解所采用的技术栈无不得从利用开发者工具或者分析 js 入手,而有一款 Chrome 插件,能轻松分析出对应网站所采用技术。 + +## 开始 + +正如标题所言,利用它可以大致分析网站所用编程语言,框架,库,Web 服务器等等。 + +这里先贴下官方链接和使用截图 + +[Find out what websites are built with - Wappalyzer](https://www.wappalyzer.com/) + +Wappalyzer 官网 + +![image-20210729074332788](https://img.kuizuo.cn/image-20210729074332788.png) + +愧怍的个人博客 + +![image-20210802131116542](https://img.kuizuo.cn/image-20210802131116542.png) + +b 站 + +![image-20210729074249566](https://img.kuizuo.cn/image-20210729074249566.png) + +本人测试过多个网站,分析结果与实际也如图所示,按理来说不会分析错,最多也就是没分析出来,不过效果可以说非常明显了,有时候看到一个心仪的网站,帮助我们去分析了解对应的技术栈,这再好不过了。 diff --git "a/docs/tools/Windows Terminal\347\276\216\345\214\226.md" "b/docs/tools/Windows Terminal\347\276\216\345\214\226.md" new file mode 100644 index 0000000..e9cff99 --- /dev/null +++ "b/docs/tools/Windows Terminal\347\276\216\345\214\226.md" @@ -0,0 +1,315 @@ +--- +id: windows-terminal-beautify +slug: /windows-terminal-beautify +title: Windows Terminal美化 +date: 2021-05-04 +authors: kuizuo +tags: [Terminal, 美化] +keywords: [Terminal, 美化] +--- + +![image-20210527065050479](https://img.kuizuo.cn/image-20210527065050479.png) + + + +其实就是美化 PowerShell 命令窗口罢了,同时可以判断当前目录下的语言环境,还有标签,时间(没错,这是我早上 6 点 40 左右写的一篇文章),等等,(顺便吐槽一句,没想到 python 都发布到了 3.9.5) + +## 安装 + +在 Microsoft Store 搜索 Windows Terminal,点击安装即可。 + +win + R 输入 `wt` 即可启动 Terminal + +或者右键文件夹空白处 `Open in Windows Terminal` + +不过默认设置背景全黑 同时 打开配置文件 + +![image-20210527070628394](https://img.kuizuo.cn/image-20210527070628394.png) + +其中在 profiles.list 下则是对应不同的终端,默认有 Windows PowerShell,Command Prompt,AzureCloud Shell, + +这边主要优化的是 Windows PowerShell + +### 更改 powershell + +如下是我的配置文件 + +``` +{ +"acrylicOpacity": 0.69999999999999996, +"commandline": "powershell.exe -nologo", +"fontFace": "JetBrainsMono NF", +"fontSize": 10, +"guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", +"hidden": false, +"name": "Windows PowerShell", +"useAcrylic": true +}, +``` + +- useAcrylic: `true` 毛玻璃效果 如果是图片的话 就别用毛玻璃的 +- acrylicOpacity: `0.7` 透明度 +- fontFace: `JetBrainsMono NF` 字体 这里我用的是 jetbrains 家的,强烈推荐 +- fontSize: `10` 字体大小 + +[字体下载](https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts) 不过估计要翻墙才能下载,下载 windows 的 .ttf 然后双击 安装即可 + +### 下载模块 + +**管理员**方式打开 PowerShell,输入如下命令 + +设置组权限,不然安装不了所需的模块 + +```bash +Set-ExecutionPolicy RemoteSigned -Scope CurrentUser +``` + +下载`oh-my-posh` 和`posh-git` 这边估计也要翻墙,不然大概率下载不了。 + +```bash +Install-Module oh-my-posh -Scope CurrentUser +Install-Module posh-git -Scope CurrentUser +Install-Module Get-ChildItemColor -Scope CurrentUser +``` + +提示输入选择 是(Y)或者 全是(A) + +### 安装主题 + +下载完毕输入下方命令 打开预览主题 + +```bash +Get-PoshThemes +``` + +![image-20210527071827101](https://img.kuizuo.cn/image-20210527071827101.png) + +貌似上面的主题混入了某个不显眼的字样 + +临时切换某个主题 + +```bash +Set-PoshPrompt jandedobbeleer +``` + +和我目前这个有点小像(因为当时是基于 jandedobbeleer 这个主题改的),不过这只是临时主题,需要更改主题文件,路径一般为 `C:\Users\用户名\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1` + +或者输入下方命令来打开 + +``` +$profile +if (!(Test-Path -Path $PROFILE )) { New-Item -Type File -Path $PROFILE -Force } +notepad $PROFILE +``` + +添加下方代码并保存,重启 Terminal 即可生效 + +```bash +Import-Module Get-ChildItemColor +$env:PYTHONIOENCODING="utf-8" +Import-Module posh-git +Import-Module oh-my-posh + +$DefaultUser = 'kuizuo' +# Set theme +Set-PoshPrompt jandedobbeleer +Set-PSReadlineKeyHandler -Key Tab -Function MenuComplete +``` + +### 修改主题 + +官方预设的主题,并不满足于我,于是就去查阅了官方文档 [Introduction | Oh my Posh](https://ohmyposh.dev/docs/) + +首先,官方的主题所在的路径 为 `C:\Users\用户名\Documents\WindowsPowerShell\Modules\oh-my-posh\3.144.0\themes` + +在`themes`目录下新建文件`xxxx.omp.json` 比如 `kuizuo.omp.json` + +我是基于主题 jandedobbeleer 所更改的,所以有些相似,这边就放一下我的主题文件配置,具体的参数 需要查看对应的官方文档,这里就不过多叙述了。 + +```json +{ + "$schema": "https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json", + "blocks": [ + { + "type": "prompt", + "alignment": "left", + "segments": [ + { + "type": "os", + "style": "diamond", + "foreground": "#ffffff", + "background": "#3A86FF", + "leading_diamond": "\uE0B6" + }, + { + "type": "session", + "style": "powerline", + "foreground": "#ffffff", + "background": "#3A86FF", + "properties": { + "postfix": " ", + "display_host": false + } + }, + { + "type": "git", + "style": "powerline", + "powerline_symbol": "", + "foreground": "#193549", + "background": "#fffb38", + "properties": { + "display_stash_count": true, + "display_upstream_icon": true, + "status_colors_enabled": true, + "local_changes_color": "#ff9248", + "ahead_and_behind_color": "#f26d50", + "behind_color": "#f17c37", + "ahead_color": "#89d1dc", + "stash_count_icon": "\uF692 " + } + }, + { + "type": "node", + "style": "powerline", + "powerline_symbol": "\uE0B0", + "foreground": "#ffffff", + "background": "#6CA35E", + "properties": { + "prefix": " \uE718 " + } + }, + { + "type": "go", + "style": "powerline", + "powerline_symbol": "", + "foreground": "#111111", + "background": "#8ED1F7", + "properties": { + "prefix": " \uE626 ", + "display_version": true + } + }, + { + "type": "python", + "style": "powerline", + "powerline_symbol": "", + "foreground": "#111111", + "background": "#FFDE57", + "properties": { + "prefix": " \uE235 ", + "display_version": true, + "display_mode": "files", + "display_virtual_env": false + } + }, + { + "type": "path", + "style": "powerline", + "powerline_symbol": "\uE0B0", + "foreground": "#ffffff", + "background": "#61AFEF", + "properties": { + "prefix": " \uE5FF ", + "style": "full" + } + }, + { + "type": "exit", + "style": "powerline", + "powerline_symbol": "\uE0B0", + "foreground": "#ffffff", + "background": "#ff8080", + "properties": { + "prefix": " \uE20F" + } + } + ] + }, + { + "type": "rprompt", + "segments": [ + { + "type": "ytm", + "style": "powerline", + "powerline_symbol": "\uE0B2", + "invert_powerline": true, + "foreground": "#111111", + "background": "#1BD760", + "properties": { + "prefix": " \uF167 ", + "paused_icon": " ", + "playing_icon": " " + } + }, + { + "type": "battery", + "style": "powerline", + "invert_powerline": true, + "powerline_symbol": "\uE0B2", + "foreground": "#ffffff", + "background": "#f36943", + "properties": { + "battery_icon": "", + "discharging_icon": " ", + "charging_icon": " ", + "charged_icon": " ", + "color_background": true, + "charged_color": "#4caf50", + "charging_color": "#40c4ff", + "discharging_color": "#ff5722", + "postfix": " " + } + }, + { + "type": "time", + "style": "diamond", + "invert_powerline": true, + "leading_diamond": "\uE0B2", + "trailing_diamond": "\uE0B4", + "background": "#2e9599", + "foreground": "#111111", + "properties": { + "time_format": "15:04:05", + "prefix": "<#000000> \uf64f " + } + } + ] + } + ], + "final_space": true +} +``` + +然后将设置主题的命令 改为 主题名 比如 + +```bash + Set-PoshPrompt kuizuo +``` + +之后你的 PowerShell 就和我这个一样了。 + +### 添加 GitBash + +这里的`E:/Git` 是我的 git 的安装路径,可根据你的自行更改 + +```json +{ + "hidden": false, + "name": "Git Bash", + "commandline": "E:/Git/bin/bash.exe -li", + "icon": "E:/Git/mingw64/share/git/git-for-windows.ico", + "startingDirectory": "%USERPROFILE%" +} +``` + +## 最后 + +当时在技术群里看到大佬秀 termnial 美化 于是自己也去折腾了一番,也折腾了一个晚上,不过好在最终效果还是比较满意的。就是不知道会不会再去折腾 Windows 桌面的美化了,算了我还是换个背景得了。 + +贴几个相关链接 + +[究极美化之 posh+termnial – 翻车鱼 (shi1011.cn)](https://blog.shi1011.cn/other/957) + +[Windows 终端概述 | Microsoft Docs](https://docs.microsoft.com/zh-cn/windows/terminal/) + +[Introduction | Oh my Posh](https://ohmyposh.dev/docs/) diff --git "a/docs/tools/Windows\350\207\252\345\256\232\344\271\211\345\217\263\351\224\256\350\217\234\345\215\225.md" "b/docs/tools/Windows\350\207\252\345\256\232\344\271\211\345\217\263\351\224\256\350\217\234\345\215\225.md" new file mode 100644 index 0000000..c3615ef --- /dev/null +++ "b/docs/tools/Windows\350\207\252\345\256\232\344\271\211\345\217\263\351\224\256\350\217\234\345\215\225.md" @@ -0,0 +1,169 @@ +--- +id: windows-custom-right-click-menu +slug: /windows-custom-right-click-menu +title: Windows自定义右键菜单 +date: 2020-09-08 +authors: kuizuo +tags: [工具] +keywords: [工具] +--- + +为什么写这篇文章呢,因为我每次都要更改鼠标右键菜单都要去百度相关的,然后在照着一步一步操作。甚至我在写这篇文章的时候也百度了相关的内容。到时候忘记了直接看我自己写的就完事了(可能写了之后就记得住了。) + + + +## 开始操作 + +### 打开注册表 + +要修改右键菜单的内容,就需要打开注册表。修改注册表的内容,来为右键菜单增添一些内容。 + +打开运行(Windows 键+ R),输入 regedit,点击确定打开注册表。 + +​ ![image-20200908114639158](https://img.kuizuo.cn/image-20200908114639158.png) + +这里建议右键 文件->导出,以防不小心误操作还原为原先配置。 + +接着可以在 编辑 -> 查找 (Ctrl+F),接着搜索对应的关键词。输入要查找的目标的值,具体操作会在后面详细说明。 + +![image-20200908154701753](https://img.kuizuo.cn/image-20200908154701753.png) + +### 右键打开 Cmd + +最终效果,右键空白处,可以使用打开 CMD,如图 + +![image-20200908152557371](https://img.kuizuo.cn/image-20200908152557371.png) + +用于你在对应的文件夹下输入 cmd 命令,免去 cd 等繁杂操作。个人建议设置一下,将下面的代码复制,然后创建一个`1.reg`文件(文件名无所谓,后缀名是 reg 就行,**注意保存为 ANSI**,不然带中文会乱码),点击运行,会有提示,放心,绝对安全。接着右键空白处,就可也看到打开 CMD 的字样,点击就能打开 cmd 窗口。 + +```bash +Windows Registry Editor Version 5.00 + +[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Directory\background\shell\cmd_here] + +@="打开 CMD" +"Icon"="cmd.exe" + +[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Directory\background\shell\cmd_here\command] + +@="\"C:\\Windows\\System32\\cmd.exe\"" + +[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Folder\shell\cmdPrompt] + +@="打开 CMD" +"Icon"="cmd.exe" + +[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Folder\shell\cmdPrompt\command] + +@="\"C:\\Windows\\System32\\cmd.exe\" \"cd %1\"" + +[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Directory\shell\cmd_here] + +@="打开 CMD" +"Icon"="cmd.exe" + +[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Directory\shell\cmd_here\command] + +@="\"C:\\Windows\\System32\\cmd.exe\"" +``` + +### 右键空白区域的菜单 + +有的时候安装了一个开发软件,不小心勾上了什么`open Folder as …`然后就出现下图情况 + +![image-20200908115455647](https://img.kuizuo.cn/image-20200908115455647.png) + +但有时候我并不需要这么长的选项,或者说我想改一下名字,让他不那么长。这时候我们同样打开注册表,首先这是*右键在文件夹空白处下*的(桌面也是一个文件夹,路径是 C:\User\用户名\Desktop)那么对应的注册表的位置是 `计算机\HKEY_CLASSES_ROOT\Directory\Background\shell` (或搜索对应的关键词),输入完定位目录为下图![image-20200908151408855](https://img.kuizuo.cn/image-20200908151408855.png) + +看到画框的 IDEA 没,那就是 IDEA 安装的时候为用户添加的右键菜单,现在我不要这个右键菜单,那就把这个文件夹整个删了就行(这里推荐你删了,太占空间了)。但这时我只想改一下右键菜单的文件名,不想让他这么长,那么你在默认哪里鼠标右键,选择修改,然后输入修改后的文件名即可。而下面的 Icon 则是图标路径,对应的也就是 exe 路径。如果图标没了,那么多半就是这里的问题。 + +然后在`IntelliJ IDEA`下还有一个 command 目录,这个目录就一个默认,对应的是执行文件的命令,你会看到一个是文件路径 可能还有一个参数是 `”%V“` ,意思就是如果你这个运行时没有传参默认就是你工作目录。可以查看 [windows 帮助](https://superuser.com/questions/136838/which-special-variables-are-available-when-writing-a-shell-command-for-a-context) + +### 右键文件夹菜单 + +![image-20200908152835802](https://img.kuizuo.cn/image-20200908152835802.png) + +本以为上面设置好删除了`Open Folder as IntelliJ IDEA Project` 这个长的要死的文件夹,没想到右键文件夹竟然也有,不管了定位在对应的位置再说,路径 `计算机\HKEY_CLASSES_ROOT\Directory\shell`,我擦,原来就在上一步操作的文件夹的下面一点。定位的结果如下 + +![image-20200908153930520](https://img.kuizuo.cn/image-20200908153930520.png) + +然后同上一步操作,这里修改一下名字就行,我就设置短点名字`Open Folder as IDEA`。结果如下 + +![image-20200908154339744](https://img.kuizuo.cn/image-20200908154339744.png) + +### 右键程序菜单 + +既然上两步的操作你都会了,那么右键程序菜单也是一样,定位到对应的路径`计算机\HKEY_CLASSES_ROOT\*\shell\` 我就放一张定位路径图。 + +![image-20200908164515142](https://img.kuizuo.cn/image-20200908164515142.png) + +### 右键手动新建 + +现在你应该知道如何定位到已有的右键菜单,并且知道了如何修改或者删除。那么现在,就手动来新建一个右键菜单。 + +作为一个`vscode`使用者,右键不设置`通过 Code 打开`怎么行,而你安装了`vscode`却没有`Open with Code`,那就是你安装时没有勾上这两项。 + +![20190530203030700](https://img.kuizuo.cn/20190530203030700.png) + +当然你也可以百度 右键菜单添加 vscode,会有相关像我提供的右键`打开 Cmd`这样的操作。这里也将对应代码贴出来,但*需要更改一下的 vscode 的路径*与右键菜单名,并且要将单反斜杠都换成双反斜杠 防止转义。例如,这里右键菜单名为`Open with Code`,而我的`Code.exe`路径为`E:\VSCode\Code.exe`那么我就要改为`E:\\VSCode\\Code.exe`,你只需要改为你的路径即可。 + +zhuyi 下面有的路径是在`Code.exe`后面有一个`\`这里是转义`“`的,不要删除。所以这么麻烦,还不如重新卸载安装勾上这两个选项。 + +按同样的存为`1.reg`文件,双击执行即可 + +```bash +Windows Registry Editor Version 5.00 + +[HKEY_CLASSES_ROOT\*\shell\VSCode] +@="Open with Code" +"Icon"="E:\\VSCode\\Code.exe" + +[HKEY_CLASSES_ROOT\*\shell\VSCode\command] +@="\"E:\\VSCode\\Code.exe" \"%1\"" + +[HKEY_CLASSES_ROOT\Directory\shell\VSCode] +@="Open with Code" +"Icon"="E:\\VSCode\\Code.exe" + +[HKEY_CLASSES_ROOT\Directory\shell\VSCode\command] +@="\"E:\\VSCode\\Code.exe\" \"%V\"" + +[HKEY_CLASSES_ROOT\Directory\Background\shell\VSCode] +@="Open with Code" +"Icon"="E:\\VSCode\\Code.exe" + +[HKEY_CLASSES_ROOT\Directory\Background\shell\VSCode\command] +@="\"E:\\VSCode\\Code.exe\" \"%V\"" +``` + +不过这里还是说下手动的操作,话不多说先看 gif 操作 + +![demo](https://img.kuizuo.cn/demo.gif) + +由于我设置过了 VSCode,这里我将 S 改成 B,对应的操作就是这样。对应的数据我在上面也已经说过了。一般来说也没必要手动操作添加,都会有对应的`.reg`文件,点击运行即可。 + +## 这里补充一个 + +右键菜单有一个自定义文件夹,这个没什么用可以通过注册表来进行删除。 + +#### 1.首先打开注册表 + +#### 2.定位到`计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer` + +#### 3.然后右键新建 DWORD,名称为`NoCustomizeWebView`,数值为 1。 + +![image-20200908163225915](https://img.kuizuo.cn/image-20200908163225915.png) + +#### 4.打开任务管理器,找到 Windows 资源管理器,右键重新启动,再次打开就会发现右键不在有自定义文件夹了。 + +![image-20200908163415434](https://img.kuizuo.cn/image-20200908163415434.png) + +## 总结 + +- 右键文件夹空白处所对对应的目录路径是`计算机\HKEY_CLASSES_ROOT\Directory\Background\shell` + +- 右键文件夹的目录路径是`计算机\HKEY_CLASSES_ROOT\Directory\shell` + +- 右键程序的目录路径是 `计算机\HKEY_CLASSES_ROOT\*\shell` + +**要添加,要修改就因人而异了。** diff --git a/docs/tools/introduction.md b/docs/tools/introduction.md new file mode 100644 index 0000000..a6b54be --- /dev/null +++ b/docs/tools/introduction.md @@ -0,0 +1,9 @@ +--- +id: introduction +slug: /tools +title: 开发工具推荐 +--- + +本页为个人开发中使用到的一些开发工具。 + +同时你也可以到上方的导航栏中的工具中,查看一些本人常用的工具,我都将其部署在自有的服务器上,加快访问速度。 \ No newline at end of file diff --git a/docsearch.json b/docsearch.json new file mode 100644 index 0000000..df2a165 --- /dev/null +++ b/docsearch.json @@ -0,0 +1,81 @@ +{ + "index_name": "kuizuo", + "start_urls": ["https://kuizuo.cn/"], + "sitemap_urls": ["https://kuizuo.cn/sitemap.xml"], + "selectors": { + "lvl0": { + "selector": "(//ul[contains(@class,'menu__list')]//a[contains(@class, 'menu__link menu__link--sublist menu__link--active')]/text() | //nav[contains(@class, 'navbar')]//a[contains(@class, 'navbar__link--active')]/text())[last()]", + "type": "xpath", + "global": true, + "default_value": "Documentation" + }, + "lvl1": "header h1, article h1", + "lvl2": "article h2", + "lvl3": "article h3", + "lvl4": "article h4", + "lvl5": "article h5, article td:first-child", + "lvl6": "article h6", + "text": "article p, article li, article td:last-child" + }, + "custom_settings": { + "attributesForFaceting": [ + "type", + "lang", + "language", + "version", + "docusaurus_tag" + ], + "attributesToRetrieve": [ + "hierarchy", + "content", + "anchor", + "url", + "url_without_anchor", + "type" + ], + "attributesToHighlight": ["hierarchy", "content"], + "attributesToSnippet": ["content:10"], + "camelCaseAttributes": ["hierarchy", "content"], + "searchableAttributes": [ + "unordered(hierarchy.lvl0)", + "unordered(hierarchy.lvl1)", + "unordered(hierarchy.lvl2)", + "unordered(hierarchy.lvl3)", + "unordered(hierarchy.lvl4)", + "unordered(hierarchy.lvl5)", + "unordered(hierarchy.lvl6)", + "content" + ], + "distinct": true, + "attributeForDistinct": "url", + "customRanking": [ + "desc(weight.pageRank)", + "desc(weight.level)", + "asc(weight.position)" + ], + "ranking": [ + "words", + "filters", + "typo", + "attribute", + "proximity", + "exact", + "custom" + ], + "highlightPreTag": "", + "highlightPostTag": "", + "minWordSizefor1Typo": 3, + "minWordSizefor2Typos": 7, + "allowTyposOnNumericTokens": false, + "minProximity": 1, + "ignorePlurals": true, + "advancedSyntax": true, + "attributeCriteriaComputedByMinProximity": true, + "removeWordsIfNoResults": "allOptional", + "separatorsToIndex": "_", + "synonyms": [ + ["js", "javascript"], + ["ts", "typescript"] + ] + } +} diff --git a/docusaurus.config.ts b/docusaurus.config.ts new file mode 100644 index 0000000..62e4cef --- /dev/null +++ b/docusaurus.config.ts @@ -0,0 +1,271 @@ +import type { Config } from '@docusaurus/types' +import type * as Preset from '@docusaurus/preset-classic' +import { themes } from 'prism-react-renderer' +import { GiscusConfig } from './src/components/Comment' +import social from './data/social' + +const beian = '闽ICP备2020017848号-2' +const beian1 = '闽公网安备35021102000847号' + +const config: Config = { + title: 'Tianzhi Jia (贾添植)', //网页标签标题 + url: 'https://jiatianzhi.xyz', + baseUrl: '/', + favicon: 'img/favicon.ico', + organizationName: 'jiatianzhi', + projectName: 'blog', + customFields: { + bio: '锦衣未加身,独在夜中行。', + description: + '是一个由愧怍创建的个人博客,主要分享编程开发知识和项目,该网站基于 React 驱动的静态网站生成器 Docusaurus 构建。', + }, + themeConfig: { + // announcementBar: { + // id: 'announcementBar-3', + // content: ``, + // }, + metadata: [ + { + name: 'keywords', + content: '愧怍, kuizuo', + }, + { + name: 'keywords', + content: 'blog, javascript, typescript, node, react, vue, web', + }, + { + name: 'keywords', + content: '编程爱好者, Web开发者, 写过爬虫, 学过逆向, 现在主攻ts全栈', + }, + ], + docs: { + sidebar: { + hideable: true, + }, + }, + navbar: { + title: 'TIANZHI JIA (贾添植)', + logo: { + alt: '愧怍', + src: 'img/logo.webp', + srcDark: 'img/logo.webp', + }, + hideOnScroll: true, + items: [ + { + label: '笔记', + position: 'right', + to: 'docs/skill', + }, + { + label: '博客', + position: 'right', + to: 'blog', + }, + { + label: '项目', + position: 'right', + to: 'project', + }, + { + label: '更多', + position: 'right', + items: [ + { label: '归档', to: 'blog/archive' }, + { label: '资源', to: 'resources' }, + { label: '友链', to: 'friends' }, + { label: '工具推荐', to: 'docs/tools' }, + ], + }, + { + type: 'localeDropdown', + position: 'left', + }, + ], + }, + footer: { + style: 'dark', + links: [ + { + title: '学习', + items: [ + { label: '博客', to: 'blog' }, + { label: '归档', to: 'blog/archive' }, + { label: '笔记', to: 'docs/skill' }, + { label: '项目', to: 'project' }, + { label: '前端示例', to: 'https://example.kuizuo.cn' }, + ], + }, + { + title: '社交媒体', + items: [ + { label: '关于我', to: '/about' }, + { label: 'GitHub', href: social.github.href }, + { label: 'Twitter', href: social.twitter.href }, + { label: '掘金', href: social.juejin.href }, + { label: 'Discord', href: social.discord.href }, + ], + }, + { + title: '更多', + items: [ + { label: '友链', position: 'right', to: 'friends' }, + { label: '导航', position: 'right', to: 'resources' }, + { + html: ` + + build with docusaurus + + `, + }, + ], + }, + ], + copyright: ` +

${beian}

+

police${beian1}

+

Copyright © 2024 Tianzhi Jia Built with Docusaurus.

+ `, + }, + algolia: { + appId: 'GV6YN1ODMO', + apiKey: '50303937b0e4630bec4a20a14e3b7872', + indexName: 'kuizuo', + }, + prism: { + theme: themes.oneLight, + darkTheme: themes.oneDark, + additionalLanguages: [ + 'bash', + 'json', + 'java', + 'python', + 'php', + 'graphql', + 'rust', + 'toml', + 'protobuf', + ], + defaultLanguage: 'javascript', + magicComments: [ + { + className: 'theme-code-block-highlighted-line', + line: 'highlight-next-line', + block: { start: 'highlight-start', end: 'highlight-end' }, + }, + { + className: 'code-block-error-line', + line: 'This will error', + }, + ], + }, + giscus: { + repo: 'kuizuo/blog', + repoId: 'MDEwOlJlcG9zaXRvcnkzOTc2MjU2MTI=', + category: 'General', + categoryId: 'DIC_kwDOF7NJDM4CPK95', + theme: 'light', + darkTheme: 'dark_dimmed', + } satisfies Partial, + tableOfContents: { + minHeadingLevel: 2, + maxHeadingLevel: 4, + }, + liveCodeBlock: { playgroundPosition: 'top' }, + zoom: { + selector: '.markdown :not(em) > img', + background: { + light: 'rgb(255, 255, 255)', + dark: 'rgb(50, 50, 50)', + }, + }, + } satisfies Preset.ThemeConfig, + presets: [ + [ + 'classic', + { + docs: { + path: 'docs', + sidebarPath: 'sidebars.ts', + }, + blog: false, + theme: { + customCss: ['./src/css/custom.scss'], + }, + sitemap: { + priority: 0.5, + }, + gtag: { + trackingID: 'G-S4SD5NXWXF', + anonymizeIP: true, + }, + debug: process.env.NODE_ENV === 'development', + } satisfies Preset.Options, + ], + ], + plugins: [ + 'docusaurus-plugin-image-zoom', + 'docusaurus-plugin-sass', + '@docusaurus/plugin-ideal-image', + ['docusaurus-plugin-baidu-tongji', { token: 'c9a3849aa75f9c4a4e65f846cd1a5155' }], + [ + '@docusaurus/plugin-pwa', + { + debug: process.env.NODE_ENV === 'development', + offlineModeActivationStrategies: ['appInstalled', 'standalone', 'queryString'], + pwaHead: [ + { tagName: 'link', rel: 'icon', href: '/img/logo.png' }, + { tagName: 'link', rel: 'manifest', href: '/manifest.json' }, + { tagName: 'meta', name: 'theme-color', content: '#12affa' }, + ], + }, + ], + [ + './src/plugin/plugin-content-blog', // 为了实现全局 blog 数据,必须改写 plugin-content-blog 插件 + { + path: 'blog', + editUrl: ({ locale, blogDirPath, blogPath, permalink }) => + `https://github.com/kuizuo/blog/edit/main/${blogDirPath}/${blogPath}`, + editLocalizedFiles: false, + blogDescription: '代码人生:编织技术与生活的博客之旅', + blogSidebarCount: 10, + blogSidebarTitle: 'Blogs', + postsPerPage: 10, + showReadingTime: true, + readingTime: ({ content, frontMatter, defaultReadingTime }) => + defaultReadingTime({ content, options: { wordsPerMinute: 300 } }), + feedOptions: { + type: 'all', + title: '愧怍', + copyright: `Copyright © ${new Date().getFullYear()} 愧怍 Built with Docusaurus.

${beian}

`, + }, + }, + ], + ], + headTags: [ + { + tagName: 'meta', + attributes: { + name: 'description', + content: '愧怍的个人博客', + }, + }, + ], + stylesheets: [ + 'https://cdn.jsdelivr.net/npm/misans@4.0.0/lib/Normal/MiSans-Normal.min.css', + 'https://cdn.jsdelivr.net/npm/misans@4.0.0/lib/Normal/MiSans-Semibold.min.css', + ], + i18n: { + defaultLocale: 'zh-CN', + locales: ['en', 'zh-CN'], + localeConfigs: { + en: { + htmlLang: 'en-GB', + }, + }, + }, +} + +export default config diff --git a/i18n/en/code.json b/i18n/en/code.json new file mode 100644 index 0000000..95c2e0a --- /dev/null +++ b/i18n/en/code.json @@ -0,0 +1,477 @@ +{ + "homepage.hero.greet": { + "message": "Hello! I'm ", + "description": "hero greet" + }, + "homepage.hero.name": { + "message": "Kuizuo", + "description": "hero name" + }, + "homepage.hero.text": { + "message": "Here I will share the problems and solutions encountered by various technology stacks, take you to understand the latest technology stacks and how to apply them in actual development, and hope that my development experience will inspire you", + "description": "hero text" + }, + "hompage.hero.introduce": { + "message": "Introduce" + }, + "homepage.blog.title": { + "message": "Recent Blogs" + }, + "homepage.lookMore": { + "message": "Look More" + }, + "homepage.project.title": { + "message": "Project Showcase" + }, + "homepage.feature.title": { + "message": "Feature" + }, + "homepage.feature.developer": { + "message": "Typescript Developer" + }, + "homepage.feature.spider": { + "message": "Know a little Reverse" + }, + "homepage.feature.enthusiast": { + "message": "Open source enthusiast" + }, + "theme.blog.archive.posts.total": { + "message": "Total {total} posts" + }, + "theme.blog.archive.posts.unit": { + "message": "posts" + }, + "theme.project.title": { + "message": "Project Showcase" + }, + "theme.project.description": { + "message": "Here is the best proof of my hard practice and application in the field of technology." + }, + "showcase.header.button": { + "message": "Go to Github to clone" + }, + "theme.blog.archive.title": { + "message": "Archive", + "description": "The page & hero title of the blog archive page" + }, + "theme.blog.archive.description": { + "message": "Archive", + "description": "The page & hero description of the blog archive page" + }, + "theme.blog.sidebar.navAriaLabel": { + "message": "Blog recent posts navigation", + "description": "The ARIA label for recent posts in the blog sidebar" + }, + "theme.blog.post.readMore": { + "message": "Read More", + "description": "The label used in blog post item excerpts to link to full blog posts" + }, + "theme.blog.post.readMoreLabel": { + "message": "Read more about {title}", + "description": "The ARIA label for the link to full blog posts from excerpts" + }, + "theme.blog.post.readingTime.plurals": { + "message": "One min read|{readingTime} min read", + "description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" + }, + "theme.blog.title.recommend": { + "message": "Recommend Blog" + }, + "theme.blog.title.new": { + "message": "New Blog" + }, + "theme.ErrorPageContent.title": { + "message": "This page crashed.", + "description": "The title of the fallback page when the page crashed" + }, + "theme.ErrorPageContent.tryAgain": { + "message": "Try again", + "description": "The label of the button to try again when the page crashed" + }, + "theme.NotFound.title": { + "message": "Page Not Found", + "description": "The title of the 404 page" + }, + "theme.NotFound.p1": { + "message": "We could not find what you were looking for.", + "description": "The first paragraph of the 404 page" + }, + "theme.NotFound.p2": { + "message": "Please contact the owner of the site that linked you to the original URL and let them know their link is broken.", + "description": "The 2nd paragraph of the 404 page" + }, + "theme.admonition.note": { + "message": "note", + "description": "The default label used for the Note admonition (:::note)" + }, + "theme.admonition.tip": { + "message": "tip", + "description": "The default label used for the Tip admonition (:::tip)" + }, + "theme.admonition.danger": { + "message": "danger", + "description": "The default label used for the Danger admonition (:::danger)" + }, + "theme.admonition.info": { + "message": "info", + "description": "The default label used for the Info admonition (:::info)" + }, + "theme.admonition.warning": { + "message": "warning", + "description": "The default label used for the Warning admonition (:::warning)" + }, + "theme.AnnouncementBar.closeButtonAriaLabel": { + "message": "Close", + "description": "The ARIA label for close button of announcement bar" + }, + "theme.BackToTopButton.buttonAriaLabel": { + "message": "Scroll back to top", + "description": "The ARIA label for the back to top button" + }, + "theme.blog.paginator.navAriaLabel": { + "message": "Blog list page navigation", + "description": "The ARIA label for the blog pagination" + }, + "theme.blog.paginator.newerEntries": { + "message": "Newer Entries", + "description": "The label used to navigate to the newer blog posts page (previous page)" + }, + "theme.blog.paginator.olderEntries": { + "message": "Older Entries", + "description": "The label used to navigate to the older blog posts page (next page)" + }, + "theme.blog.post.paginator.navAriaLabel": { + "message": "Blog post page navigation", + "description": "The ARIA label for the blog posts pagination" + }, + "theme.blog.post.paginator.newerPost": { + "message": "Newer Post", + "description": "The blog post button label to navigate to the newer/previous post" + }, + "theme.blog.post.paginator.olderPost": { + "message": "Older Post", + "description": "The blog post button label to navigate to the older/next post" + }, + "theme.blog.post.plurals": { + "message": "One post|{count} posts", + "description": "Pluralized label for \"{count} posts\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" + }, + "theme.blog.tagTitle": { + "message": "{nPosts} tagged with \"{tagName}\"", + "description": "The title of the page for a blog tag" + }, + "theme.tags.tagsPageLink": { + "message": "View All Tags", + "description": "The label of the link targeting the tag list page" + }, + "theme.colorToggle.ariaLabel": { + "message": "Switch between dark and light mode (currently {mode})", + "description": "The ARIA label for the navbar color mode toggle" + }, + "theme.colorToggle.ariaLabel.mode.dark": { + "message": "dark mode", + "description": "The name for the dark color mode" + }, + "theme.colorToggle.ariaLabel.mode.light": { + "message": "light mode", + "description": "The name for the light color mode" + }, + "theme.docs.breadcrumbs.home": { + "message": "Home page", + "description": "The ARIA label for the home page in the breadcrumbs" + }, + "theme.docs.breadcrumbs.navAriaLabel": { + "message": "Breadcrumbs", + "description": "The ARIA label for the breadcrumbs" + }, + "theme.docs.DocCard.categoryDescription": { + "message": "{count} items", + "description": "The default description for a category card in the generated index about how many items this category includes" + }, + "theme.docs.paginator.navAriaLabel": { + "message": "Docs pages navigation", + "description": "The ARIA label for the docs pagination" + }, + "theme.docs.paginator.previous": { + "message": "Previous", + "description": "The label used to navigate to the previous doc" + }, + "theme.docs.paginator.next": { + "message": "Next", + "description": "The label used to navigate to the next doc" + }, + "theme.docs.tagDocListPageTitle.nDocsTagged": { + "message": "One doc tagged|{count} docs tagged", + "description": "Pluralized label for \"{count} docs tagged\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" + }, + "theme.docs.tagDocListPageTitle": { + "message": "{nDocsTagged} with \"{tagName}\"", + "description": "The title of the page for a docs tag" + }, + "theme.docs.versionBadge.label": { + "message": "Version: {versionLabel}" + }, + "theme.common.editThisPage": { + "message": "Edit this page", + "description": "The link label to edit the current page" + }, + "theme.docs.versions.unreleasedVersionLabel": { + "message": "This is unreleased documentation for {siteTitle} {versionLabel} version.", + "description": "The label used to tell the user that he's browsing an unreleased doc version" + }, + "theme.docs.versions.unmaintainedVersionLabel": { + "message": "This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained.", + "description": "The label used to tell the user that he's browsing an unmaintained doc version" + }, + "theme.docs.versions.latestVersionSuggestionLabel": { + "message": "For up-to-date documentation, see the {latestVersionLink} ({versionLabel}).", + "description": "The label used to tell the user to check the latest version" + }, + "theme.docs.versions.latestVersionLinkLabel": { + "message": "latest version", + "description": "The label used for the latest version suggestion link label" + }, + "theme.common.headingLinkTitle": { + "message": "Direct link to heading", + "description": "Title for link to heading" + }, + "theme.lastUpdated.atDate": { + "message": " on {date}", + "description": "The words used to describe on which date a page has been last updated" + }, + "theme.lastUpdated.byUser": { + "message": " by {user}", + "description": "The words used to describe by who the page has been last updated" + }, + "theme.lastUpdated.lastUpdatedAtBy": { + "message": "Last updated{atDate}{byUser}", + "description": "The sentence used to display when a page has been last updated, and by who" + }, + "theme.navbar.mobileVersionsDropdown.label": { + "message": "Versions", + "description": "The label for the navbar versions dropdown on mobile view" + }, + "theme.common.skipToMainContent": { + "message": "Skip to main content", + "description": "The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation" + }, + "theme.tags.tagsListLabel": { + "message": "Tags:", + "description": "The label alongside a tag list" + }, + "theme.CodeBlock.copied": { + "message": "Copied", + "description": "The copied button label on code blocks" + }, + "theme.CodeBlock.copyButtonAriaLabel": { + "message": "Copy code to clipboard", + "description": "The ARIA label for copy code blocks button" + }, + "theme.CodeBlock.copy": { + "message": "Copy", + "description": "The copy button label on code blocks" + }, + "theme.CodeBlock.wordWrapToggle": { + "message": "Toggle word wrap", + "description": "The title attribute for toggle word wrapping button of code block lines" + }, + "theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": { + "message": "Toggle the collapsible sidebar category '{label}'", + "description": "The ARIA label to toggle the collapsible sidebar category" + }, + "theme.navbar.mobileLanguageDropdown.label": { + "message": "Languages", + "description": "The label for the mobile language switcher dropdown" + }, + "theme.TOCCollapsible.toggleButtonLabel": { + "message": "On this page", + "description": "The label used by the button on the collapsible TOC component" + }, + "theme.docs.sidebar.collapseButtonTitle": { + "message": "Collapse sidebar", + "description": "The title attribute for collapse button of doc sidebar" + }, + "theme.docs.sidebar.collapseButtonAriaLabel": { + "message": "Collapse sidebar", + "description": "The title attribute for collapse button of doc sidebar" + }, + "theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": { + "message": "← Back to main menu", + "description": "The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)" + }, + "theme.docs.sidebar.expandButtonTitle": { + "message": "Expand sidebar", + "description": "The ARIA label and title attribute for expand button of doc sidebar" + }, + "theme.docs.sidebar.expandButtonAriaLabel": { + "message": "Expand sidebar", + "description": "The ARIA label and title attribute for expand button of doc sidebar" + }, + "theme.SearchBar.seeAll": { + "message": "See all {count} results" + }, + "theme.SearchBar.label": { + "message": "Search", + "description": "The ARIA label and placeholder for search button" + }, + "theme.SearchModal.searchBox.resetButtonTitle": { + "message": "Clear the query", + "description": "The label and ARIA label for search box reset button" + }, + "theme.SearchModal.searchBox.cancelButtonText": { + "message": "Cancel", + "description": "The label and ARIA label for search box cancel button" + }, + "theme.SearchModal.startScreen.recentSearchesTitle": { + "message": "Recent", + "description": "The title for recent searches" + }, + "theme.SearchModal.startScreen.noRecentSearchesText": { + "message": "No recent searches", + "description": "The text when no recent searches" + }, + "theme.SearchModal.startScreen.saveRecentSearchButtonTitle": { + "message": "Save this search", + "description": "The label for save recent search button" + }, + "theme.SearchModal.startScreen.removeRecentSearchButtonTitle": { + "message": "Remove this search from history", + "description": "The label for remove recent search button" + }, + "theme.SearchModal.startScreen.favoriteSearchesTitle": { + "message": "Favorite", + "description": "The title for favorite searches" + }, + "theme.SearchModal.startScreen.removeFavoriteSearchButtonTitle": { + "message": "Remove this search from favorites", + "description": "The label for remove favorite search button" + }, + "theme.SearchModal.errorScreen.titleText": { + "message": "Unable to fetch results", + "description": "The title for error screen of search modal" + }, + "theme.SearchModal.errorScreen.helpText": { + "message": "You might want to check your network connection.", + "description": "The help text for error screen of search modal" + }, + "theme.SearchModal.footer.selectText": { + "message": "to select", + "description": "The explanatory text of the action for the enter key" + }, + "theme.SearchModal.footer.selectKeyAriaLabel": { + "message": "Enter key", + "description": "The ARIA label for the Enter key button that makes the selection" + }, + "theme.SearchModal.footer.navigateText": { + "message": "to navigate", + "description": "The explanatory text of the action for the Arrow up and Arrow down key" + }, + "theme.SearchModal.footer.navigateUpKeyAriaLabel": { + "message": "Arrow up", + "description": "The ARIA label for the Arrow up key button that makes the navigation" + }, + "theme.SearchModal.footer.navigateDownKeyAriaLabel": { + "message": "Arrow down", + "description": "The ARIA label for the Arrow down key button that makes the navigation" + }, + "theme.SearchModal.footer.closeText": { + "message": "to close", + "description": "The explanatory text of the action for Escape key" + }, + "theme.SearchModal.footer.closeKeyAriaLabel": { + "message": "Escape key", + "description": "The ARIA label for the Escape key button that close the modal" + }, + "theme.SearchModal.footer.searchByText": { + "message": "Search by", + "description": "The text explain that the search is making by Algolia" + }, + "theme.SearchModal.noResultsScreen.noResultsText": { + "message": "No results for", + "description": "The text explains that there are no results for the following search" + }, + "theme.SearchModal.noResultsScreen.suggestedQueryText": { + "message": "Try searching for", + "description": "The text for the suggested query when no results are found for the following search" + }, + "theme.SearchModal.noResultsScreen.reportMissingResultsText": { + "message": "Believe this query should return results?", + "description": "The text for the question where the user thinks there are missing results" + }, + "theme.SearchModal.noResultsScreen.reportMissingResultsLinkText": { + "message": "Let us know.", + "description": "The text for the link to report missing results" + }, + "theme.SearchModal.placeholder": { + "message": "Search docs", + "description": "The placeholder of the input of the DocSearch pop-up modal" + }, + "theme.SearchPage.documentsFound.plurals": { + "message": "One document found|{count} documents found", + "description": "Pluralized label for \"{count} documents found\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" + }, + "theme.SearchPage.existingResultsTitle": { + "message": "Search results for \"{query}\"", + "description": "The search page title for non-empty query" + }, + "theme.SearchPage.emptyResultsTitle": { + "message": "Search the documentation", + "description": "The search page title for empty query" + }, + "theme.SearchPage.inputPlaceholder": { + "message": "Type your search here", + "description": "The placeholder for search page input" + }, + "theme.SearchPage.inputLabel": { + "message": "Search", + "description": "The ARIA label for search page input" + }, + "theme.SearchPage.algoliaLabel": { + "message": "Search by Algolia", + "description": "The ARIA label for Algolia mention" + }, + "theme.SearchPage.noResultsText": { + "message": "No results were found", + "description": "The paragraph for empty search result" + }, + "theme.SearchPage.fetchingNewResults": { + "message": "Fetching new results...", + "description": "The paragraph for fetching new search results" + }, + "theme.IdealImageMessage.loading": { + "message": "Loading...", + "description": "When the full-scale image is loading" + }, + "theme.IdealImageMessage.load": { + "message": "Click to load{sizeMessage}", + "description": "To prompt users to load the full image. sizeMessage is a parenthesized size figure." + }, + "theme.IdealImageMessage.offline": { + "message": "Your browser is offline. Image not loaded", + "description": "When the user is viewing an offline document" + }, + "theme.IdealImageMessage.404error": { + "message": "404. Image not found", + "description": "When the image is not found" + }, + "theme.IdealImageMessage.error": { + "message": "Error. Click to reload", + "description": "When the image fails to load for unknown error" + }, + "theme.PwaReloadPopup.info": { + "message": "New version available", + "description": "The text for PWA reload popup" + }, + "theme.PwaReloadPopup.refreshButtonText": { + "message": "Refresh", + "description": "The text for PWA reload button" + }, + "theme.PwaReloadPopup.closeButtonAriaLabel": { + "message": "Close", + "description": "The ARIA label for close button of PWA reload popup" + }, + "showcase.card.sourceLink": { + "message": "source", + "description": "The label for the source link of a showcase card" + } +} diff --git a/i18n/en/docusaurus-plugin-content-blog/authors.yml b/i18n/en/docusaurus-plugin-content-blog/authors.yml new file mode 100644 index 0000000..a23a000 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-blog/authors.yml @@ -0,0 +1,5 @@ +kuizuo: + name: Kuizuo + title: ts full stack / Student + url: https://github.com/kuizuo + image_url: /img/logo.webp diff --git a/i18n/en/docusaurus-plugin-content-docs/current.json b/i18n/en/docusaurus-plugin-content-docs/current.json new file mode 100644 index 0000000..c1488c4 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current.json @@ -0,0 +1,106 @@ +{ + "version.label": { + "message": "Next", + "description": "The label for version current" + }, + "sidebar.skill.category.Docusaurus2 主题魔改": { + "message": "Docusaurus2 主题魔改", + "description": "The label for category Docusaurus2 主题魔改 in sidebar skill" + }, + "sidebar.skill.category.代码规范": { + "message": "代码规范", + "description": "The label for category 代码规范 in sidebar skill" + }, + "sidebar.skill.category.Vue": { + "message": "Vue", + "description": "The label for category Vue in sidebar skill" + }, + "sidebar.skill.category.React": { + "message": "React", + "description": "The label for category React in sidebar skill" + }, + "sidebar.skill.category.Web": { + "message": "Web", + "description": "The label for category Web in sidebar skill" + }, + "sidebar.skill.category.JavaScript": { + "message": "JavaScript", + "description": "The label for category JavaScript in sidebar skill" + }, + "sidebar.skill.category.Node": { + "message": "Node", + "description": "The label for category Node in sidebar skill" + }, + "sidebar.skill.category.Css": { + "message": "Css", + "description": "The label for category Css in sidebar skill" + }, + "sidebar.skill.category.Go": { + "message": "Go", + "description": "The label for category Go in sidebar skill" + }, + "sidebar.skill.category.Git": { + "message": "Git", + "description": "The label for category Git in sidebar skill" + }, + "sidebar.skill.category.算法": { + "message": "算法", + "description": "The label for category 算法 in sidebar skill" + }, + "sidebar.skill.category.逆向": { + "message": "逆向", + "description": "The label for category 逆向 in sidebar skill" + }, + "sidebar.skill.category.逆向.link.generated-index.title": { + "message": "逆向笔记", + "description": "The generated-index page title for category 逆向 in sidebar skill" + }, + "sidebar.skill.category.逆向.link.generated-index.description": { + "message": "Web逆向与安卓逆向笔记", + "description": "The generated-index page description for category 逆向 in sidebar skill" + }, + "sidebar.skill.category.安卓": { + "message": "安卓", + "description": "The label for category 安卓 in sidebar skill" + }, + "sidebar.skill.category.frida": { + "message": "frida", + "description": "The label for category frida in sidebar skill" + }, + "sidebar.skill.category.刷机": { + "message": "刷机", + "description": "The label for category 刷机 in sidebar skill" + }, + "sidebar.skill.category.密码学": { + "message": "密码学", + "description": "The label for category 密码学 in sidebar skill" + }, + "sidebar.skill.category.Docker": { + "message": "Docker", + "description": "The label for category Docker in sidebar skill" + }, + "sidebar.skill.category.数据库": { + "message": "数据库", + "description": "The label for category 数据库 in sidebar skill" + }, + "sidebar.skill.category.Mysql": { + "message": "Mysql", + "description": "The label for category Mysql in sidebar skill" + }, + "sidebar.skill.category.MongoDB": { + "message": "MongoDB", + "description": "The label for category MongoDB in sidebar skill" + }, + "sidebar.skill.category.Redis": { + "message": "Redis", + "description": "The label for category Redis in sidebar skill" + }, + "sidebar.skill.category.Elasticsearch": { + "message": "Elasticsearch", + "description": "The label for category Elasticsearch in sidebar skill" + }, + "sidebar.skill.category.杂项": { + "message": "杂项", + "description": "The label for category 杂项 in sidebar skill" + } +} diff --git a/i18n/en/docusaurus-plugin-content-pages/about.mdx b/i18n/en/docusaurus-plugin-content-pages/about.mdx new file mode 100644 index 0000000..5a2adf4 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-pages/about.mdx @@ -0,0 +1,62 @@ +--- +id: about +title: Introduction +description: Kuizuo self-introduction +hide_table_of_contents: true +--- + +import { Icon } from '@iconify/react'; + +# 👋 Hello! I'm Kuizuo + +🧑 University Student, from China + +👨‍💻 Code enthusiasts, write projects of interest, Hope to contribute to the open source community + +🌱 Keep learning, hoping to learn unlimit possibilities in a limit time + +🐛 I have written crawlers for a long time, learned the reverse of Web and Android, and now focus on the full stack of js/ts, and continue to develop for a long time. + +### 🤔 Why is it called Kuizuo + +Kuizuo, guilty, ashamed. This is also the meaning I want to express by using this name. + +I used to feel guilty and distressed by some wrong practices. I want to keep motivating myself with this name, reflect on the past, and don't want to live up to so many things I've experienced again, that's all. + +### 🛠 Tech Stack + +![JavaScript](https://img.shields.io/badge/-JavaScript-333333?style=flat&logo=javascript) +![TypeScript](https://img.shields.io/badge/-TypeScript-333333?style=flat&logo=typescript) +![Python](https://img.shields.io/badge/-Python-333333?style=flat&logo=python) +![Vue](https://img.shields.io/badge/-Vue-333333?style=flat&logo=vue.js) +![React](https://img.shields.io/badge/-React-333333?style=flat&logo=react) +![Node.js](https://img.shields.io/badge/-Node-333333?style=flat&logo=node.js) +![NestJs](https://img.shields.io/badge/-NestJs-333333?style=flat&logo=nestjs&logoColor=ea2845) +![Vite](https://img.shields.io/badge/-Vite-333333?style=flat&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MTAiIGhlaWdodD0iNDA0IiB2aWV3Qm94PSIwIDAgNDEwIDQwNCIgZmlsbD0ibm9uZSI+CjxwYXRoIGQ9Ik0zOTkuNjQxIDU5LjUyNDZMMjE1LjY0MyAzODguNTQ1QzIxMS44NDQgMzk1LjMzOCAyMDIuMDg0IDM5NS4zNzggMTk4LjIyOCAzODguNjE4TDEwLjU4MTcgNTkuNTU2M0M2LjM4MDg3IDUyLjE4OTYgMTIuNjgwMiA0My4yNjY1IDIxLjAyODEgNDQuNzU4NkwyMDUuMjIzIDc3LjY4MjRDMjA2LjM5OCA3Ny44OTI0IDIwNy42MDEgNzcuODkwNCAyMDguNzc2IDc3LjY3NjNMMzg5LjExOSA0NC44MDU4QzM5Ny40MzkgNDMuMjg5NCA0MDMuNzY4IDUyLjE0MzQgMzk5LjY0MSA1OS41MjQ2WiIgZmlsbD0idXJsKCNwYWludDBfbGluZWFyKSIvPgo8cGF0aCBkPSJNMjkyLjk2NSAxLjU3NDRMMTU2LjgwMSAyOC4yNTUyQzE1NC41NjMgMjguNjkzNyAxNTIuOTA2IDMwLjU5MDMgMTUyLjc3MSAzMi44NjY0TDE0NC4zOTUgMTc0LjMzQzE0NC4xOTggMTc3LjY2MiAxNDcuMjU4IDE4MC4yNDggMTUwLjUxIDE3OS40OThMMTg4LjQyIDE3MC43NDlDMTkxLjk2NyAxNjkuOTMxIDE5NS4xNzIgMTczLjA1NSAxOTQuNDQzIDE3Ni42MjJMMTgzLjE4IDIzMS43NzVDMTgyLjQyMiAyMzUuNDg3IDE4NS45MDcgMjM4LjY2MSAxODkuNTMyIDIzNy41NkwyMTIuOTQ3IDIzMC40NDZDMjE2LjU3NyAyMjkuMzQ0IDIyMC4wNjUgMjMyLjUyNyAyMTkuMjk3IDIzNi4yNDJMMjAxLjM5OCAzMjIuODc1QzIwMC4yNzggMzI4LjI5NCAyMDcuNDg2IDMzMS4yNDkgMjEwLjQ5MiAzMjYuNjAzTDIxMi41IDMyMy41TDMyMy40NTQgMTAyLjA3MkMzMjUuMzEyIDk4LjM2NDUgMzIyLjEwOCA5NC4xMzcgMzE4LjAzNiA5NC45MjI4TDI3OS4wMTQgMTAyLjQ1NEMyNzUuMzQ3IDEwMy4xNjEgMjcyLjIyNyA5OS43NDYgMjczLjI2MiA5Ni4xNTgzTDI5OC43MzEgNy44NjY4OUMyOTkuNzY3IDQuMjczMTQgMjk2LjYzNiAwLjg1NTE4MSAyOTIuOTY1IDEuNTc0NFoiIGZpbGw9InVybCgjcGFpbnQxX2xpbmVhcikiLz4KPGRlZnM+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQwX2xpbmVhciIgeDE9IjYuMDAwMTciIHkxPSIzMi45OTk5IiB4Mj0iMjM1IiB5Mj0iMzQ0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiM0MUQxRkYiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjQkQzNEZFIi8+CjwvbGluZWFyR3JhZGllbnQ+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQxX2xpbmVhciIgeDE9IjE5NC42NTEiIHkxPSI4LjgxODE4IiB4Mj0iMjM2LjA3NiIgeTI9IjI5Mi45ODkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iI0ZGRUE4MyIvPgo8c3RvcCBvZmZzZXQ9IjAuMDgzMzMzMyIgc3RvcC1jb2xvcj0iI0ZGREQzNSIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNGRkE4MDAiLz4KPC9saW5lYXJHcmFkaWVudD4KPC9kZWZzPgo8L3N2Zz4=) +![Nuxt](https://img.shields.io/badge/-Nuxt-333333?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAHfElEQVR4Ae3BeVCU5wEH4N/3vt+x9y6wnAuuCAILcoqCuFxyiGIiKd4K0aBIFCMatB7xShxHM+kk1v5hmqTJdFKdmsZk7LS2YzXWg1Fj4xXPKJ54S4SIy7L4bfmDndlhBFm+RZjJPg88PDw8PDw8PDw8fp0Y9BKmjaBRagS1Uos21l+aGqyNTY32NuhHGPQCwrJsTFHmxJTy4oX+0YNiGULInTM/nTj++a6tP369d/szW6sN/QRFL0iYVjBz3AeLt3oZA0Mpx3KEpazW4BcyODdlrKW+8VHdDxe+Rz9B4GbaYL+Q9KrpyzmFTIEOWIEXMpe+vsY71BCGfoLAzVIrJlR5DzKEoxMKH60+67cz16KfIHAj44i49KSScXPQBaZN9PjMiQNHJmShHyBwE8pzvLlq2jJBrVDjBViBF3LXlG8klFL0MQI3MRWaXwvNSMpBNwUlRiYnzhhbhj7Gwg3kOrVX9vI33mUFXkA3EUpp5pLS1We//W5Hc8OTx+iMnFegMGYSpgyrQIDGgEv3fsRnNb/D4Sv/gRtQuMGIeZMWR7+aOYFpAxcIaqWGIQy5euDEPtjtdnRECcX8zHdQnbcRBp0RKkEDo084siPH4fzdk7hRfwUSEUjkZQwMTaucXM0QQtADybNerfCPGRSH54kOTMTkoeXgKQ9nOrk3FuWsh5xTQCICiXLXlG+UaVU69JCgVmpGVk5ZQjmWgzOBk2FexkroVf54niFBQ1GcOBMSEUgwICV2pGlcxm+YNpDg5+t3au2iXYSz/KjXkBaWi84QhuCt7LXwUflDAoIe4uSCPP+9Nz8gLGUhweObd68f2fq3j8Rnz57BwVuhx2xzNZS8Cl3xVvpi0aj3QBiCHiLoAabNkOKcqcHJ0amQwC6K4r71n618Wt/wCA4Mw2Da8HmI8o9HdxTFz0B8cAp6iKAHlL5efplLSldDopvHztac/fa7HXAW7mvClOQ5oISiO2ScHJVZayDnFegBAhcRlrJpCyYv0YUEGCGBzWK17Fv/6cpntlYbHDjKoSytGgGaYLjCHJaLAlMxGIaBiwhc5BthNCWVFM6GBPY2p7/a8+W1mlMH4Mwcno/C2MlwFSUU87LegZ86CC4icAEnF+RZy2atk2lUWkjw5F793cObt2+CMwWvRGXWasg5BXoi1CcCpSmVoITCBQQuCM8ZXhCRn1oICeyiKB79+OvN9VfrrsBZUXwpYgKTIMWU5LmI9I+FCwi6Sa5Te5mrpi+nPMdDgntna08f/WTnFjjzUfmhatQ6sISFFFq5FyrSl0PGydFNBN2UPGv8m4akqGGQwGaxWva//8U629Pmp3BWnbMBXgo93GF0dDHM4aPRTQTd4BtpNA0rGz8PEtXuP77n4u6aXXAWZxiG8QklYBgG7kAJRdWoddCr/NENBC9AKKUp5cULNUG+BkjQ9ODn+/s2/GmVXRRFOFBCsWLMh+ApD3eK9IvFpKFzQBiCFyB4gYHpCdlxk/JmQAK7KIontu3+/P652jNw9krsVCSFjIC7MQyDKcnlCPM14QUIuiDTqnQjF0xdyivlSkjw6PKtS8c++WaLvQ0c9Cp/zM9aBcIQ9IZATTAq0peBpzy6QNCFqEJz0UBzQhYkaLW2WA9t3rap8faDOjhQQvFG2mKE+kSgtzAMg1xTEdIHj0EXCDrhZQwMHblg6lLKsRwkuH741H/P/+PgTjiL8BuCqclz0duUvApzzNXQKbzRCYLnIJTSpJLC2frBIZGQoLnxScOh32/fZG1saoSDwMkwP3MV1DItXoZ4QwrGx81AJwieIyAuPDFh2phZDCEEEpzesefLm0fOHIIDwzAwh+Ujz1SEl4WjHEpTFyBUH4HnIOiAFXghY3HJSnWATyAkeHzj7rVjn37zh9YWWwscvBV6LMxeC0ooXqYBXmEoTV0IlrDogKCDwXkpYwdlDc2DBHZRFI/+ceeWR5dvXYIDIQQTk8oQHZiIl41hGBTFTUeyMR0dEDgRNEpN+qIZK3ilXAkJ6n648P2pv/77z3ZRFOEQpo9CSeoC9BW1TIuKjBWQcwo4IXCS/Porc/2HhMVDglZri/Xgh3/Z8PRRw0M48JRHWdrb8FcHoS8NN6ZjTMxEOCFopw7QB6VWTKiiHMtBgov/qtlVu//4HjhLC89DQcxE9DWeFVCZvRreCj3aEbQzpsVlKH29/CBBc+OThkMfbdtos1gtcFAJasxOexsqQY3+IEQXijLzUrQjaCfTqLQMJRQS3Dp+7sjDS9fPw9lAnwgkBKeiv2AYBqNNRWhH0K7+at3l1uaWZkjQePvBLZvFaoEzSihYyqE/kXFytCNod73m1IHbJy8ehwTqAH0QJxfkcHbr8TVcuHca/cnh2r1oR9HOLorijSOnDw5IiTWrA3wC0QOaAL3hp73Hdv9y52EdHCwtTbj/5A4ywgsg4+ToS3a7HefunsC7/3wLDZZ6tGHQgVKv8w3PGV4QlBg1jFMICrjo/rmrZ/73xd8/tjVbm+FACEGY3oTR0cUw6AagL4h2EefunMTus1+hvukBPDw8PDw8PDw8PDx+1f4PWmgPDDI9UJ0AAAAASUVORK5CYII=) +![Git](https://img.shields.io/badge/-Git-333333?style=flat-square&logo=git) +![GitHub](https://img.shields.io/badge/-GitHub-333333?style=flat-square&logo=github) +![Docker](https://img.shields.io/badge/-Docker-333333?style=flat&logo=docker) +![Mysql](https://img.shields.io/badge/-Mysql-333333?style=flat&logo=mysql) +![MongoDB](https://img.shields.io/badge/-MongoDB-333333?style=flat&logo=mongodb) +![Redis](https://img.shields.io/badge/-Redis-333333?style=flat&logo=redis) +![Electron](https://img.shields.io/badge/-Electron-333333?style=flat&logo=electron) +![Miniprogram](https://img.shields.io/badge/-Miniprogram-333333?style=flat&logo=wechat) +![Reverse](https://img.shields.io/badge/-Reverse-333333?style=flat&logo=reverse) +![HTTP](https://img.shields.io/badge/-HTTP-333333?style=flat&logo=http) + + + + + +### ☎️ Contact me + +

RSS

+ +

Github

+ +

QQ

+ +

Twitter

+ +

hi@kuizuo.cn

diff --git a/i18n/en/docusaurus-theme-classic/footer.json b/i18n/en/docusaurus-theme-classic/footer.json new file mode 100644 index 0000000..e7a12a6 --- /dev/null +++ b/i18n/en/docusaurus-theme-classic/footer.json @@ -0,0 +1,66 @@ +{ + "link.title.学习": { + "message": "Study", + "description": "The title of the footer links column with title=学习 in the footer" + }, + "link.title.社交媒体": { + "message": "Social Media", + "description": "The title of the footer links column with title=社交媒体 in the footer" + }, + "link.title.更多": { + "message": "More", + "description": "The title of the footer links column with title=更多 in the footer" + }, + "link.item.label.博客": { + "message": "Blogs", + "description": "The label of footer link with label=博客 linking to tags" + }, + "link.item.label.标签": { + "message": "Tags", + "description": "The label of footer link with label=标签 linking to tags" + }, + "link.item.label.归档": { + "message": "Archive", + "description": "The label of footer link with label=归档 linking to archive" + }, + "link.item.label.笔记": { + "message": "Notes", + "description": "The label of footer link with label=笔记 linking to docs/skill" + }, + "link.item.label.实战项目": { + "message": "Projects", + "description": "The label of footer link with label=实战项目 linking to project" + }, + "link.item.label.前端示例": { + "message": "Frontend Examples", + "description": "The label of footer link with label=前端示例 linking to https://example.kuizuo.cn" + }, + "link.item.label.关于我": { + "message": "About Me", + "description": "The label of footer link with label=关于我 linking to /about" + }, + "link.item.label.GitHub": { + "message": "GitHub", + "description": "The label of footer link with label=GitHub linking to https://github.com/kuizuo" + }, + "link.item.label.掘金": { + "message": "Juejin", + "description": "The label of footer link with label=掘金 linking to https://juejin.cn/user/1565318510545901" + }, + "link.item.label.Discord": { + "message": "Discord", + "description": "The label of footer link with label=Discord linking to https://discord.gg/M8cVcjDxkz" + }, + "link.item.label.友链": { + "message": "Friends", + "description": "The label of footer link with label=友链 linking to friends" + }, + "link.item.label.导航": { + "message": "Links", + "description": "The label of footer link with label=导航 linking to resource" + }, + "copyright": { + "message": "

闽ICP备2020017848号-2

Copyright © 2020 – PRESENT kuizuo Built with Docusaurus.

", + "description": "The footer copyright" + } +} \ No newline at end of file diff --git a/i18n/en/docusaurus-theme-classic/navbar.json b/i18n/en/docusaurus-theme-classic/navbar.json new file mode 100644 index 0000000..05482cb --- /dev/null +++ b/i18n/en/docusaurus-theme-classic/navbar.json @@ -0,0 +1,62 @@ +{ + "title": { + "message": "kuizuo", + "description": "The title in the navbar" + }, + "item.label.博客": { + "message": "Blogs", + "description": "Navbar item with label 博客" + }, + "item.label.工具": { + "message": "Tools", + "description": "Navbar item with label 工具" + }, + "item.label.导航": { + "message": "Links", + "description": "Navbar item with label 导航" + }, + "item.label.项目": { + "message": "Projects", + "description": "Navbar item with label 项目" + }, + "item.label.标签": { + "message": "Tags", + "description": "Navbar item with label 标签" + }, + "item.label.归档": { + "message": "Archive", + "description": "Navbar item with label 归档" + }, + "item.label.笔记": { + "message": "Notes", + "description": "Navbar item with label 笔记" + }, + "item.label.工具推荐": { + "message": "Recommended Tools", + "description": "Navbar item with label 工具推荐" + }, + "item.label.前端示例": { + "message": "Frontend Example", + "description": "Navbar item with label 前端示例" + }, + "item.label.在线代码": { + "message": "Live Code", + "description": "Navbar item with label 在线代码" + }, + "item.label.API服务": { + "message": "API Service", + "description": "Navbar item with label API服务" + }, + "item.label.JS代码还原": { + "message": "JS Deobfuscate", + "description": "Navbar item with label JS代码还原" + }, + "item.label.CyberChef加密": { + "message": "CyberChef Encryption", + "description": "Navbar item with label CyberChef加密" + }, + "item.label.网盘": { + "message": "Cloudreve", + "description": "Navbar item with label 网盘" + } +} \ No newline at end of file diff --git a/i18n/zh/code.json b/i18n/zh/code.json new file mode 100644 index 0000000..fe25779 --- /dev/null +++ b/i18n/zh/code.json @@ -0,0 +1,387 @@ +{ + "playground.codesandbox.description": { + "message": "CodeSandbox is a popular playground solution. Runs Docusaurus in a remote Docker container." + }, + "playground.stackblitz.description": { + "message": "StackBlitz uses a novel {webContainersLink} technology to run Docusaurus directly in your browser." + }, + "playground.tryItButton": { + "message": "Try it now!" + }, + "Hello! 我是": { + "message": "Hello! 我是", + "description": "hero greet" + }, + "愧怍": { + "message": "贾添植", + "description": "my name" + }, + "homepage.hero.text": { + "message": "在这里我会分享各类技术栈所遇到问题与解决方案,带你了解最新的技术栈以及实际开发中如何应用,并希望我的开发经历对你有所启发。", + "description": "hero text" + }, + "homepage.hero.look": { + "message": "你可以随处逛逛,查看{note}、{project}、{link}、以及我的{idea}。", + "description": "hero look" + }, + "hompage.hero.note": { + "message": "笔记", + "description": "Note link label" + }, + "hompage.hero.project": { + "message": "实战项目", + "description": "Project link label" + }, + "hompage.hero.link": { + "message": "网址导航", + "description": "Link link label" + }, + "hompage.hero.idea": { + "message": "想法感悟", + "description": "Idea label" + }, + "homepage.qqgroup": { + "message": "QQ 群:5478458", + "description": "qq group" + }, + "自我介绍": { + "message": "自我介绍", + "description": "follow me btn text" + }, + "阅读全文": { + "message": "阅读全文", + "description": "read full text" + }, + "theme.tags.tagsPageTitle": { + "message": "标签", + "description": "The title of the tag list page" + }, + "blogtagpage.title": { + "message": "下的博客", + "description": "blog tag page title" + }, + "blogtagpage.title.alt": { + "message": "", + "description": "blog tag page title in alternate order" + }, + "blogtagpage.description": { + "message": "博客标签", + "description": "blog tag page description" + }, + "blogtagpage.count.label": { + "message": "篇", + "description": "blog page count label" + }, + "blogtagpage.seeall.label": { + "message": "查看所有标签(分类)", + "description": "blog page see all label" + }, + "theme.ErrorPageContent.title": { + "message": "页面已崩溃。", + "description": "The title of the fallback page when the page crashed" + }, + "theme.ErrorPageContent.tryAgain": { + "message": "重试", + "description": "The label of the button to try again when the page crashed" + }, + "theme.NotFound.title": { + "message": "哎呀,页面失踪了~", + "description": "The title of the 404 page" + }, + "theme.NotFound.p1": { + "message": "找不到你要访问的页面,要不点点别处看看?", + "description": "The first paragraph of the 404 page" + }, + "theme.NotFound.p2": { + "message": "可能是个 Bug🐛,也可能是作者偷懒还没写😴", + "description": "The 2nd paragraph of the 404 page" + }, + "theme.AnnouncementBar.closeButtonAriaLabel": { + "message": "关闭", + "description": "The ARIA label for close button of announcement bar" + }, + "theme.blog.archive.title": { + "message": "历史博文", + "description": "The page & hero title of the blog archive page" + }, + "theme.blog.archive.description": { + "message": "历史博文", + "description": "The page & hero description of the blog archive page" + }, + "theme.BackToTopButton.buttonAriaLabel": { + "message": "回到顶部", + "description": "The ARIA label for the back to top button" + }, + "theme.blog.paginator.navAriaLabel": { + "message": "博文列表分页导航", + "description": "The ARIA label for the blog pagination" + }, + "theme.blog.paginator.newerEntries": { + "message": "较新的博文", + "description": "The label used to navigate to the newer blog posts page (previous page)" + }, + "theme.blog.paginator.olderEntries": { + "message": "较旧的博文", + "description": "The label used to navigate to the older blog posts page (next page)" + }, + "theme.blog.post.readingTime.plurals": { + "message": "{readingTime} 分钟阅读", + "description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" + }, + "theme.blog.post.readMoreLabel": { + "message": "阅读 {title} 的全文", + "description": "The ARIA label for the link to full blog posts from excerpts" + }, + "theme.blog.post.readMore": { + "message": "阅读全文", + "description": "The label used in blog post item excerpts to link to full blog posts" + }, + "theme.blog.post.paginator.navAriaLabel": { + "message": "博文分页导航", + "description": "The ARIA label for the blog posts pagination" + }, + "theme.blog.post.paginator.newerPost": { + "message": "较新一篇", + "description": "The blog post button label to navigate to the newer/previous post" + }, + "theme.blog.post.paginator.olderPost": { + "message": "较旧一篇", + "description": "The blog post button label to navigate to the older/next post" + }, + "theme.blog.sidebar.navAriaLabel": { + "message": "最近博文导航", + "description": "The ARIA label for recent posts in the blog sidebar" + }, + "theme.blog.post.plurals": { + "message": "{count} 篇博文", + "description": "Pluralized label for \"{count} posts\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" + }, + "theme.blog.title.recommend": { + "message": "推荐阅读" + }, + "theme.blog.title.new": { + "message": "最新博客" + }, + "theme.blog.tagTitle": { + "message": "{nPosts} 含有标签「{tagName}」", + "description": "The title of the page for a blog tag" + }, + "theme.tags.tagsPageLink": { + "message": "查看所有标签", + "description": "The label of the link targeting the tag list page" + }, + "theme.colorToggle.ariaLabel": { + "message": "切换浅色/暗黑模式(当前为{mode})", + "description": "The ARIA label for the navbar color mode toggle" + }, + "theme.colorToggle.ariaLabel.mode.dark": { + "message": "暗黑模式", + "description": "The name for the dark color mode" + }, + "theme.colorToggle.ariaLabel.mode.light": { + "message": "浅色模式", + "description": "The name for the light color mode" + }, + "theme.docs.DocCard.categoryDescription": { + "message": "{count} 个项目", + "description": "The default description for a category card in the generated index about how many items this category includes" + }, + "theme.docs.sidebar.expandButtonTitle": { + "message": "展开侧边栏", + "description": "The ARIA label and title attribute for expand button of doc sidebar" + }, + "theme.docs.sidebar.expandButtonAriaLabel": { + "message": "展开侧边栏", + "description": "The ARIA label and title attribute for expand button of doc sidebar" + }, + "theme.docs.paginator.navAriaLabel": { + "message": "文档分页导航", + "description": "The ARIA label for the docs pagination" + }, + "theme.docs.paginator.previous": { + "message": "上一页", + "description": "The label used to navigate to the previous doc" + }, + "theme.docs.paginator.next": { + "message": "下一页", + "description": "The label used to navigate to the next doc" + }, + "theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": { + "message": "打开/收起侧边栏菜单「{label}」", + "description": "The ARIA label to toggle the collapsible sidebar category" + }, + "theme.docs.tagDocListPageTitle.nDocsTagged": { + "message": "{count} 篇文档带有标签", + "description": "Pluralized label for \"{count} docs tagged\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" + }, + "theme.docs.tagDocListPageTitle": { + "message": "{nDocsTagged}「{tagName}」", + "description": "The title of the page for a docs tag" + }, + "theme.docs.versionBadge.label": { + "message": "版本:{versionLabel}" + }, + "theme.docs.versions.unreleasedVersionLabel": { + "message": "此为 {siteTitle} {versionLabel} 版尚未发行的文档。", + "description": "The label used to tell the user that he's browsing an unreleased doc version" + }, + "theme.docs.versions.unmaintainedVersionLabel": { + "message": "此为 {siteTitle} {versionLabel} 版的文档,现已不再积极维护。", + "description": "The label used to tell the user that he's browsing an unmaintained doc version" + }, + "theme.docs.versions.latestVersionSuggestionLabel": { + "message": "最新的文档请参阅 {latestVersionLink} ({versionLabel})。", + "description": "The label used to tell the user to check the latest version" + }, + "theme.docs.versions.latestVersionLinkLabel": { + "message": "最新版本", + "description": "The label used for the latest version suggestion link label" + }, + "theme.common.editThisPage": { + "message": "编辑此页", + "description": "The link label to edit the current page" + }, + "theme.common.headingLinkTitle": { + "message": "标题的直接链接", + "description": "Title for link to heading" + }, + "theme.lastUpdated.atDate": { + "message": "于 {date} ", + "description": "The words used to describe on which date a page has been last updated" + }, + "theme.lastUpdated.byUser": { + "message": "由 {user} ", + "description": "The words used to describe by who the page has been last updated" + }, + "theme.lastUpdated.lastUpdatedAtBy": { + "message": "最后{byUser}{atDate}更新", + "description": "The sentence used to display when a page has been last updated, and by who" + }, + "theme.navbar.mobileVersionsDropdown.label": { + "message": "选择版本", + "description": "The label for the navbar versions dropdown on mobile view" + }, + "theme.common.skipToMainContent": { + "message": "跳到主要内容", + "description": "The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation" + }, + "theme.tags.tagsListLabel": { + "message": "标签:", + "description": "The label alongside a tag list" + }, + "theme.TOCCollapsible.toggleButtonLabel": { + "message": "本页总览", + "description": "The label used by the button on the collapsible TOC component" + }, + "theme.CodeBlock.copied": { + "message": "复制成功", + "description": "The copied button label on code blocks" + }, + "theme.CodeBlock.copyButtonAriaLabel": { + "message": "复制代码到剪贴板", + "description": "The ARIA label for copy code blocks button" + }, + "theme.CodeBlock.copy": { + "message": "复制", + "description": "The copy button label on code blocks" + }, + "theme.navbar.mobileLanguageDropdown.label": { + "message": "选择语言", + "description": "The label for the mobile language switcher dropdown" + }, + "theme.docs.sidebar.collapseButtonTitle": { + "message": "收起侧边栏", + "description": "The title attribute for collapse button of doc sidebar" + }, + "theme.docs.sidebar.collapseButtonAriaLabel": { + "message": "收起侧边栏", + "description": "The title attribute for collapse button of doc sidebar" + }, + "theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": { + "message": "← 回到主菜单", + "description": "The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)" + }, + "theme.SearchBar.seeAll": { + "message": "查看全部 {count} 个结果" + }, + "theme.SearchBar.label": { + "message": "搜索", + "description": "The ARIA label and placeholder for search button" + }, + "theme.SearchPage.documentsFound.plurals": { + "message": "找到 {count} 份文件", + "description": "Pluralized label for \"{count} documents found\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" + }, + "theme.SearchPage.existingResultsTitle": { + "message": "「{query}」的搜索结果", + "description": "The search page title for non-empty query" + }, + "theme.SearchPage.emptyResultsTitle": { + "message": "在文档中搜索", + "description": "The search page title for empty query" + }, + "theme.SearchPage.inputPlaceholder": { + "message": "在此输入搜索字词", + "description": "The placeholder for search page input" + }, + "theme.SearchPage.inputLabel": { + "message": "搜索", + "description": "The ARIA label for search page input" + }, + "theme.SearchPage.algoliaLabel": { + "message": "通过 Algolia 搜索", + "description": "The ARIA label for Algolia mention" + }, + "theme.SearchPage.noResultsText": { + "message": "未找到任何结果", + "description": "The paragraph for empty search result" + }, + "theme.SearchPage.fetchingNewResults": { + "message": "正在获取新的搜索结果...", + "description": "The paragraph for fetching new search results" + }, + "theme.IdealImageMessage.loading": { + "message": "加载中……", + "description": "When the full-scale image is loading" + }, + "theme.IdealImageMessage.load": { + "message": "点击加载图片{sizeMessage}", + "description": "To prompt users to load the full image. sizeMessage is a parenthesized size figure." + }, + "theme.IdealImageMessage.offline": { + "message": "你的浏览器处于离线状态。图片未加载", + "description": "When the user is viewing an offline document" + }, + "theme.IdealImageMessage.404error": { + "message": "未找到图片", + "description": "When the image is not found" + }, + "theme.IdealImageMessage.error": { + "message": "出现错误,点击重试", + "description": "When the image fails to load for unknown error" + }, + "theme.PwaReloadPopup.info": { + "message": "有可用的新版本", + "description": "The text for PWA reload popup" + }, + "theme.PwaReloadPopup.refreshButtonText": { + "message": "刷新", + "description": "The text for PWA reload button" + }, + "theme.PwaReloadPopup.closeButtonAriaLabel": { + "message": "关闭", + "description": "The ARIA label for close button of PWA reload popup" + }, + "theme.Playground.result": { + "message": "结果", + "description": "The result label of the live codeblocks" + }, + "theme.Playground.liveEditor": { + "message": "实时编辑器", + "description": "The live editor label of the live codeblocks" + }, + "showcase.card.sourceLink": { + "message": "源码", + "description": "The label for the source link of a showcase card" + } +} \ No newline at end of file diff --git a/i18n/zh/docusaurus-plugin-content-blog/options.json b/i18n/zh/docusaurus-plugin-content-blog/options.json new file mode 100644 index 0000000..5fda370 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-blog/options.json @@ -0,0 +1,14 @@ +{ + "title": { + "message": "Blog", + "description": "The title for the blog used in SEO" + }, + "description": { + "message": "Blog", + "description": "The description for the blog used in SEO" + }, + "sidebar.title": { + "message": "近期文章", + "description": "The label for the left sidebar" + } +} diff --git a/i18n/zh/docusaurus-plugin-content-docs/current.json b/i18n/zh/docusaurus-plugin-content-docs/current.json new file mode 100644 index 0000000..56bfe77 --- /dev/null +++ b/i18n/zh/docusaurus-plugin-content-docs/current.json @@ -0,0 +1,70 @@ +{ + "version.label": { + "message": "Next", + "description": "The label for version current" + }, + "sidebar.skill.category.Vue": { + "message": "Vue", + "description": "The label for category Vue in sidebar skill" + }, + "sidebar.skill.category.逆向": { + "message": "逆向", + "description": "The label for category 逆向 in sidebar skill" + }, + "sidebar.skill.category.逆向.link.generated-index.title": { + "message": "逆向笔记", + "description": "The generated-index page title for category 逆向 in sidebar skill" + }, + "sidebar.skill.category.逆向.link.generated-index.description": { + "message": "Web逆向与安卓逆向笔记", + "description": "The generated-index page description for category 逆向 in sidebar skill" + }, + "sidebar.skill.category.安卓": { + "message": "安卓", + "description": "The label for category 安卓 in sidebar skill" + }, + "sidebar.skill.category.frida": { + "message": "frida", + "description": "The label for category frida in sidebar skill" + }, + "sidebar.skill.category.刷机": { + "message": "刷机", + "description": "The label for category 刷机 in sidebar skill" + }, + "sidebar.skill.category.Web": { + "message": "Web", + "description": "The label for category Web in sidebar skill" + }, + "sidebar.skill.category.密码学": { + "message": "密码学", + "description": "The label for category 密码学 in sidebar skill" + }, + "sidebar.skill.category.后端": { + "message": "后端", + "description": "The label for category 后端 in sidebar skill" + }, + "sidebar.skill.category.数据库": { + "message": "数据库", + "description": "The label for category 数据库 in sidebar skill" + }, + "sidebar.skill.category.Mysql": { + "message": "Mysql", + "description": "The label for category Mysql in sidebar skill" + }, + "sidebar.skill.category.MongoDB": { + "message": "MongoDB", + "description": "The label for category MongoDB in sidebar skill" + }, + "sidebar.skill.category.Redis": { + "message": "Redis", + "description": "The label for category Redis in sidebar skill" + }, + "sidebar.skill.category.Elasticsearch": { + "message": "Elasticsearch", + "description": "The label for category Elasticsearch in sidebar skill" + }, + "sidebar.skill.link.React": { + "message": "React", + "description": "The label for link React in sidebar skill, linking to /docs/category/react" + } +} diff --git a/i18n/zh/docusaurus-theme-classic/footer.json b/i18n/zh/docusaurus-theme-classic/footer.json new file mode 100644 index 0000000..6c6c27b --- /dev/null +++ b/i18n/zh/docusaurus-theme-classic/footer.json @@ -0,0 +1,42 @@ +{ + "link.title.学习": { + "message": "学习", + "description": "The title of the footer links column with title=学习 in the footer" + }, + "link.title.社交媒体": { + "message": "社交媒体", + "description": "The title of the footer links column with title=社交媒体 in the footer" + }, + "link.title.友情链接": { + "message": "友情链接", + "description": "The title of the footer links column with title=友情链接 in the footer" + }, + "link.item.label.技术博客": { + "message": "技术博客", + "description": "The label of footer link with label=技术博客 linking to /#homepage_blogs" + }, + "link.item.label.笔记": { + "message": "笔记", + "description": "The label of footer link with label=笔记 linking to docs/skill" + }, + "link.item.label.实战项目": { + "message": "实战项目", + "description": "The label of footer link with label=实战项目 linking to project" + }, + "link.item.label.首页": { + "message": "首页", + "description": "The label of footer link with label=首页 linking to /" + }, + "link.item.label.关于我": { + "message": "关于我", + "description": "The label of footer link with label=关于我 linking to /about" + }, + "link.item.label.GitHub": { + "message": "GitHub", + "description": "The label of footer link with label=GitHub linking to https://github.com/kuizuo" + }, + "link.item.label.掘金": { + "message": "掘金", + "description": "The label of footer link with label=掘金 linking to https://juejin.cn/user/1565318510545901" + } +} diff --git a/i18n/zh/docusaurus-theme-classic/navbar.json b/i18n/zh/docusaurus-theme-classic/navbar.json new file mode 100644 index 0000000..9ffb37f --- /dev/null +++ b/i18n/zh/docusaurus-theme-classic/navbar.json @@ -0,0 +1,46 @@ +{ + "title": { + "message": "愧怍", + "description": "The title in the navbar" + }, + "item.label.标签": { + "message": "标签", + "description": "Navbar item with label 标签" + }, + "item.label.归档": { + "message": "归档", + "description": "Navbar item with label 归档" + }, + "item.label.学习": { + "message": "学习", + "description": "Navbar item with label 学习" + }, + "item.label.小工具": { + "message": "小工具", + "description": "Navbar item with label 小工具" + }, + "item.label.实战项目": { + "message": "实战项目", + "description": "Navbar item with label 实战项目" + }, + "item.label.笔记": { + "message": "笔记", + "description": "Navbar item with label 笔记" + }, + "item.label.网址导航": { + "message": "网址导航", + "description": "Navbar item with label 网址导航" + }, + "item.label.JS代码还原": { + "message": "JS代码还原", + "description": "Navbar item with label JS代码还原" + }, + "item.label.CyberChef加密": { + "message": "CyberChef加密", + "description": "Navbar item with label CyberChef加密" + }, + "item.label.网盘": { + "message": "网盘", + "description": "Navbar item with label 网盘" + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2005d56 --- /dev/null +++ b/package.json @@ -0,0 +1,77 @@ +{ + "name": "blog", + "version": "3.0.0", + "author": { + "url": "https://github.com/kuizuo", + "email": "hi@kuizuo.cn", + "name": "Kuizuo" + }, + "repository": { + "url": "https://github.com/kuizuo/blog", + "type": "git" + }, + "homepage": "https://kuizuo.cn", + "license": "MIT", + "scripts": { + "docusaurus": "docusaurus", + "start": "docusaurus start", + "start:en": "docusaurus start --locale en", + "build": "docusaurus build", + "swizzle": "docusaurus swizzle", + "deploy": "docusaurus deploy", + "clear": "docusaurus clear && rimraf changelog && rimraf _dogfooding/_swizzle_theme_tests", + "serve": "docusaurus serve", + "lint": "yarn lint:js && yarn lint:style", + "lint:js": "eslint --fix --report-unused-disable-directives \"**/*.{js,jsx,ts,tsx,mjs}\"", + "lint:style": "stylelint \"**/*.scss\"", + "lint:fix": "eslint src --fix", + "prettier:fix": "npx prettier src data --check --write", + "format": "npm run prettier:fix && npm run lint:fix", + "write-translations": "docusaurus write-translations", + "write-heading-ids": "docusaurus write-heading-ids", + "index": "docker run -it --env-file=.env -e \"CONFIG=$(cat docsearch.json | jq -r tostring)\" algolia/docsearch-scraper" + }, + "dependencies": { + "@docusaurus/core": "3.1.0", + "@docusaurus/plugin-ideal-image": "3.1.0", + "@docusaurus/plugin-pwa": "3.1.0", + "@docusaurus/preset-classic": "3.1.0", + "@docusaurus/theme-search-algolia": "3.1.0", + "@giscus/react": "^2.3.0", + "@popperjs/core": "^2.11.8", + "@vercel/analytics": "^1.1.1", + "dayjs": "^1.11.10", + "docusaurus-plugin-baidu-tongji": "0.0.0-beta.4", + "docusaurus-plugin-image-zoom": "^1.0.0", + "docusaurus-plugin-sass": "^0.2.5", + "framer-motion": "^10.16.4", + "ora": "^7.0.0", + "prism-react-renderer": "^2.3.1", + "raw-loader": "^4.0.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-popper": "^2.3.0", + "sass": "^1.64.1" + }, + "devDependencies": { + "@docusaurus/eslint-plugin": "3.1.0", + "@docusaurus/module-type-aliases": "3.1.0", + "@docusaurus/tsconfig": "^3.1.0", + "@docusaurus/types": "3.1.0", + "@iconify/react": "^4.1.1", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.53.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "prettier": "^3.1.0", + "stylelint": "^15.0.0", + "stylelint-config-prettier-scss": "^1.0.0", + "stylelint-config-standard": "^34.0.0", + "stylelint-config-standard-scss": "^11.0.0", + "typescript": "~5.3.0" + }, + "engines": { + "node": ">=18.0" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..e1c33fd --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,11462 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@docusaurus/core': + specifier: 3.1.0 + version: 3.1.0(@docusaurus/types@3.1.0)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/plugin-ideal-image': + specifier: 3.1.0 + version: 3.1.0(eslint@8.53.0)(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/plugin-pwa': + specifier: 3.1.0 + version: 3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/preset-classic': + specifier: 3.1.0 + version: 3.1.0(@algolia/client-search@4.19.1)(@types/react@18.2.21)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.8.2)(typescript@5.3.3) + '@docusaurus/theme-search-algolia': + specifier: 3.1.0 + version: 3.1.0(@algolia/client-search@4.19.1)(@docusaurus/types@3.1.0)(@types/react@18.2.21)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.8.2)(typescript@5.3.3) + '@giscus/react': + specifier: ^2.3.0 + version: 2.3.0(react-dom@18.2.0)(react@18.2.0) + '@popperjs/core': + specifier: ^2.11.8 + version: 2.11.8 + '@vercel/analytics': + specifier: ^1.1.1 + version: 1.1.1 + dayjs: + specifier: ^1.11.10 + version: 1.11.10 + docusaurus-plugin-baidu-tongji: + specifier: 0.0.0-beta.4 + version: 0.0.0-beta.4 + docusaurus-plugin-image-zoom: + specifier: ^1.0.0 + version: 1.0.1(@docusaurus/theme-classic@3.1.0) + docusaurus-plugin-sass: + specifier: ^0.2.5 + version: 0.2.5(@docusaurus/core@3.1.0)(sass@1.66.1)(webpack@5.88.2) + framer-motion: + specifier: ^10.16.4 + version: 10.16.4(react-dom@18.2.0)(react@18.2.0) + ora: + specifier: ^7.0.0 + version: 7.0.1 + prism-react-renderer: + specifier: ^2.3.1 + version: 2.3.1(react@18.2.0) + raw-loader: + specifier: ^4.0.2 + version: 4.0.2(webpack@5.88.2) + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + react-popper: + specifier: ^2.3.0 + version: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0) + sass: + specifier: ^1.64.1 + version: 1.66.1 + +devDependencies: + '@docusaurus/eslint-plugin': + specifier: 3.1.0 + version: 3.1.0(eslint@8.53.0)(typescript@5.3.3) + '@docusaurus/module-type-aliases': + specifier: 3.1.0 + version: 3.1.0(react-dom@18.2.0)(react@18.2.0) + '@docusaurus/tsconfig': + specifier: ^3.1.0 + version: 3.1.0 + '@docusaurus/types': + specifier: 3.1.0 + version: 3.1.0(react-dom@18.2.0)(react@18.2.0) + '@iconify/react': + specifier: ^4.1.1 + version: 4.1.1(react@18.2.0) + '@typescript-eslint/eslint-plugin': + specifier: ^6.0.0 + version: 6.11.0(@typescript-eslint/parser@6.11.0)(eslint@8.53.0)(typescript@5.3.3) + '@typescript-eslint/parser': + specifier: ^6.0.0 + version: 6.11.0(eslint@8.53.0)(typescript@5.3.3) + eslint: + specifier: ^8.53.0 + version: 8.53.0 + eslint-config-prettier: + specifier: ^9.0.0 + version: 9.0.0(eslint@8.53.0) + eslint-plugin-prettier: + specifier: ^5.0.0 + version: 5.0.1(eslint-config-prettier@9.0.0)(eslint@8.53.0)(prettier@3.1.0) + prettier: + specifier: ^3.1.0 + version: 3.1.0 + stylelint: + specifier: ^15.0.0 + version: 15.11.0(typescript@5.3.3) + stylelint-config-prettier-scss: + specifier: ^1.0.0 + version: 1.0.0(stylelint@15.11.0) + stylelint-config-standard: + specifier: ^34.0.0 + version: 34.0.0(stylelint@15.11.0) + stylelint-config-standard-scss: + specifier: ^11.0.0 + version: 11.1.0(postcss@8.4.29)(stylelint@15.11.0) + typescript: + specifier: ~5.3.0 + version: 5.3.3 + +packages: + + /@aashutoshrathi/word-wrap@1.2.6: + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + + /@algolia/autocomplete-core@1.9.3(@algolia/client-search@4.19.1)(algoliasearch@4.19.1)(search-insights@2.8.2): + 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.8.2) + '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.19.1)(algoliasearch@4.19.1) + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + - search-insights + dev: false + + /@algolia/autocomplete-plugin-algolia-insights@1.9.3(@algolia/client-search@4.19.1)(algoliasearch@4.19.1)(search-insights@2.8.2): + 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.8.2 + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + dev: false + + /@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: false + + /@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: false + + /@algolia/cache-browser-local-storage@4.19.1: + resolution: {integrity: sha512-FYAZWcGsFTTaSAwj9Std8UML3Bu8dyWDncM7Ls8g+58UOe4XYdlgzXWbrIgjaguP63pCCbMoExKr61B+ztK3tw==} + dependencies: + '@algolia/cache-common': 4.19.1 + dev: false + + /@algolia/cache-common@4.19.1: + resolution: {integrity: sha512-XGghi3l0qA38HiqdoUY+wvGyBsGvKZ6U3vTiMBT4hArhP3fOGLXpIINgMiiGjTe4FVlTa5a/7Zf2bwlIHfRqqg==} + dev: false + + /@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: false + + /@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: false + + /@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: false + + /@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: false + + /@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: false + + /@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: false + + /@algolia/events@4.0.1: + resolution: {integrity: sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==} + dev: false + + /@algolia/logger-common@4.19.1: + resolution: {integrity: sha512-i6pLPZW/+/YXKis8gpmSiNk1lOmYCmRI6+x6d2Qk1OdfvX051nRVdalRbEcVTpSQX6FQAoyeaui0cUfLYW5Elw==} + dev: false + + /@algolia/logger-console@4.19.1: + resolution: {integrity: sha512-jj72k9GKb9W0c7TyC3cuZtTr0CngLBLmc8trzZlXdfvQiigpUdvTi1KoWIb2ZMcRBG7Tl8hSb81zEY3zI2RlXg==} + dependencies: + '@algolia/logger-common': 4.19.1 + dev: false + + /@algolia/requester-browser-xhr@4.19.1: + resolution: {integrity: sha512-09K/+t7lptsweRTueHnSnmPqIxbHMowejAkn9XIcJMLdseS3zl8ObnS5GWea86mu3vy4+8H+ZBKkUN82Zsq/zg==} + dependencies: + '@algolia/requester-common': 4.19.1 + dev: false + + /@algolia/requester-common@4.19.1: + resolution: {integrity: sha512-BisRkcWVxrDzF1YPhAckmi2CFYK+jdMT60q10d7z3PX+w6fPPukxHRnZwooiTUrzFe50UBmLItGizWHP5bDzVQ==} + dev: false + + /@algolia/requester-node-http@4.19.1: + resolution: {integrity: sha512-6DK52DHviBHTG2BK/Vv2GIlEw7i+vxm7ypZW0Z7vybGCNDeWzADx+/TmxjkES2h15+FZOqVf/Ja677gePsVItA==} + dependencies: + '@algolia/requester-common': 4.19.1 + dev: false + + /@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: false + + /@ampproject/remapping@2.2.1: + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.19 + dev: false + + /@apideck/better-ajv-errors@0.3.6(ajv@8.12.0): + resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==} + engines: {node: '>=10'} + peerDependencies: + ajv: '>=8' + dependencies: + ajv: 8.12.0 + json-schema: 0.4.0 + jsonpointer: 5.0.1 + leven: 3.1.0 + dev: false + + /@babel/code-frame@7.22.13: + resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.22.13 + chalk: 2.4.2 + + /@babel/code-frame@7.23.5: + resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.23.4 + chalk: 2.4.2 + dev: false + + /@babel/compat-data@7.23.5: + resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==} + engines: {node: '>=6.9.0'} + dev: false + + /@babel/core@7.23.5: + resolution: {integrity: sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.5 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.5) + '@babel/helpers': 7.23.5 + '@babel/parser': 7.23.5 + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.5 + '@babel/types': 7.23.5 + convert-source-map: 2.0.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@babel/generator@7.23.5: + resolution: {integrity: sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.5 + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.19 + jsesc: 2.5.2 + dev: false + + /@babel/helper-annotate-as-pure@7.22.5: + resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.5 + dev: false + + /@babel/helper-builder-binary-assignment-operator-visitor@7.22.15: + resolution: {integrity: sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.5 + dev: false + + /@babel/helper-compilation-targets@7.22.15: + resolution: {integrity: sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.23.5 + '@babel/helper-validator-option': 7.23.5 + browserslist: 4.22.2 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: false + + /@babel/helper-create-class-features-plugin@7.22.15(@babel/core@7.23.5): + resolution: {integrity: sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-member-expression-to-functions': 7.22.15 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.5) + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + semver: 6.3.1 + dev: false + + /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.23.5): + resolution: {integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-annotate-as-pure': 7.22.5 + regexpu-core: 5.3.2 + semver: 6.3.1 + dev: false + + /@babel/helper-define-polyfill-provider@0.4.3(@babel/core@7.23.5): + resolution: {integrity: sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + debug: 4.3.4 + lodash.debounce: 4.0.8 + resolve: 1.22.4 + transitivePeerDependencies: + - supports-color + dev: false + + /@babel/helper-environment-visitor@7.22.20: + resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} + engines: {node: '>=6.9.0'} + dev: false + + /@babel/helper-function-name@7.23.0: + resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/types': 7.23.5 + dev: false + + /@babel/helper-hoist-variables@7.22.5: + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.5 + dev: false + + /@babel/helper-member-expression-to-functions@7.22.15: + resolution: {integrity: sha512-qLNsZbgrNh0fDQBCPocSL8guki1hcPvltGDv/NxvUoABwFq7GkKSu1nRXeJkVZc+wJvne2E0RKQz+2SQrz6eAA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.5 + dev: false + + /@babel/helper-module-imports@7.22.15: + resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.5 + dev: false + + /@babel/helper-module-transforms@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + dev: false + + /@babel/helper-optimise-call-expression@7.22.5: + resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.5 + dev: false + + /@babel/helper-plugin-utils@7.22.5: + resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} + engines: {node: '>=6.9.0'} + dev: false + + /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.23.5): + resolution: {integrity: sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-wrap-function': 7.22.20 + dev: false + + /@babel/helper-replace-supers@7.22.20(@babel/core@7.23.5): + resolution: {integrity: sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-member-expression-to-functions': 7.22.15 + '@babel/helper-optimise-call-expression': 7.22.5 + dev: false + + /@babel/helper-simple-access@7.22.5: + resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.5 + dev: false + + /@babel/helper-skip-transparent-expression-wrappers@7.22.5: + resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.5 + dev: false + + /@babel/helper-split-export-declaration@7.22.6: + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.5 + dev: false + + /@babel/helper-string-parser@7.23.4: + resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} + engines: {node: '>=6.9.0'} + dev: false + + /@babel/helper-validator-identifier@7.22.15: + resolution: {integrity: sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ==} + engines: {node: '>=6.9.0'} + + /@babel/helper-validator-identifier@7.22.20: + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + dev: false + + /@babel/helper-validator-option@7.23.5: + resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} + engines: {node: '>=6.9.0'} + dev: false + + /@babel/helper-wrap-function@7.22.20: + resolution: {integrity: sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-function-name': 7.23.0 + '@babel/template': 7.22.15 + '@babel/types': 7.23.5 + dev: false + + /@babel/helpers@7.23.5: + resolution: {integrity: sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.5 + '@babel/types': 7.23.5 + transitivePeerDependencies: + - supports-color + dev: false + + /@babel/highlight@7.22.13: + resolution: {integrity: sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.15 + chalk: 2.4.2 + js-tokens: 4.0.0 + + /@babel/highlight@7.23.4: + resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: false + + /@babel/parser@7.23.5: + resolution: {integrity: sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.23.5 + dev: false + + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/plugin-transform-optional-chaining': 7.23.4(@babel/core@7.23.5) + dev: false + + /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-XaJak1qcityzrX0/IU5nKHb34VaibwP3saKqG6a/tppelgllOH13LUann4ZCIBcVOeE6H18K4Vx9QKkVww3z/w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.23.5): + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + dev: false + + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.5): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.5): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.23.5): + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.23.5): + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.23.5): + resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-import-assertions@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-import-attributes@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.5): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.5): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-jsx@7.22.5(@babel/core@7.23.5): + resolution: {integrity: sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.5): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.5): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.5): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.5): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.5): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.5): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.23.5): + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.5): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-typescript@7.22.5(@babel/core@7.23.5): + resolution: {integrity: sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.23.5): + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.5) + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-arrow-functions@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-async-generator-functions@7.23.4(@babel/core@7.23.5): + resolution: {integrity: sha512-efdkfPhHYTtn0G6n2ddrESE91fgXxjlqLsnUtPWnJs4a4mZIbUaK7ffqKIIUKXSHwcDvaCVX6GXkaJJFqtX7jw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.23.5) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.5) + dev: false + + /@babel/plugin-transform-async-to-generator@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.23.5) + dev: false + + /@babel/plugin-transform-block-scoped-functions@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-block-scoping@7.23.4(@babel/core@7.23.5): + resolution: {integrity: sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-class-properties@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.5) + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-class-static-block@7.23.4(@babel/core@7.23.5): + resolution: {integrity: sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.5) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.5) + dev: false + + /@babel/plugin-transform-classes@7.23.5(@babel/core@7.23.5): + resolution: {integrity: sha512-jvOTR4nicqYC9yzOHIhXG5emiFEOpappSJAl73SDSEDcybD+Puuze8Tnpb9p9qEyYup24tq891gkaygIFvWDqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.5) + '@babel/helper-split-export-declaration': 7.22.6 + globals: 11.12.0 + dev: false + + /@babel/plugin-transform-computed-properties@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/template': 7.22.15 + dev: false + + /@babel/plugin-transform-destructuring@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-dotall-regex@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.5) + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-duplicate-keys@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-dynamic-import@7.23.4(@babel/core@7.23.5): + resolution: {integrity: sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.23.5) + dev: false + + /@babel/plugin-transform-exponentiation-operator@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-export-namespace-from@7.23.4(@babel/core@7.23.5): + resolution: {integrity: sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.23.5) + dev: false + + /@babel/plugin-transform-for-of@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-X8jSm8X1CMwxmK878qsUGJRmbysKNbdpTv/O1/v0LuY/ZkZrng5WYiekYSdg9m09OTmDDUWeEDsTE+17WYbAZw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-function-name@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-json-strings@7.23.4(@babel/core@7.23.5): + resolution: {integrity: sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.5) + dev: false + + /@babel/plugin-transform-literals@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-logical-assignment-operators@7.23.4(@babel/core@7.23.5): + resolution: {integrity: sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.5) + dev: false + + /@babel/plugin-transform-member-expression-literals@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-modules-amd@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.5) + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.5) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-simple-access': 7.22.5 + dev: false + + /@babel/plugin-transform-modules-systemjs@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.5) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-identifier': 7.22.20 + dev: false + + /@babel/plugin-transform-modules-umd@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.5) + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.23.5): + resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.5) + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-new-target@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-nullish-coalescing-operator@7.23.4(@babel/core@7.23.5): + resolution: {integrity: sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.5) + dev: false + + /@babel/plugin-transform-numeric-separator@7.23.4(@babel/core@7.23.5): + resolution: {integrity: sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.5) + dev: false + + /@babel/plugin-transform-object-rest-spread@7.23.4(@babel/core@7.23.5): + resolution: {integrity: sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.23.5 + '@babel/core': 7.23.5 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.5) + '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.23.5) + dev: false + + /@babel/plugin-transform-object-super@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.5) + dev: false + + /@babel/plugin-transform-optional-catch-binding@7.23.4(@babel/core@7.23.5): + resolution: {integrity: sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.5) + dev: false + + /@babel/plugin-transform-optional-chaining@7.23.4(@babel/core@7.23.5): + resolution: {integrity: sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.5) + dev: false + + /@babel/plugin-transform-parameters@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-private-methods@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.5) + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-private-property-in-object@7.23.4(@babel/core@7.23.5): + resolution: {integrity: sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.5) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.5) + dev: false + + /@babel/plugin-transform-property-literals@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-react-constant-elements@7.22.5(@babel/core@7.23.5): + resolution: {integrity: sha512-BF5SXoO+nX3h5OhlN78XbbDrBOffv+AxPP2ENaJOVqjWCgBDeOY3WcaUcddutGSfoap+5NEQ/q/4I3WZIvgkXA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-react-display-name@7.22.5(@babel/core@7.23.5): + resolution: {integrity: sha512-PVk3WPYudRF5z4GKMEYUrLjPl38fJSKNaEOkFuoprioowGuWN6w2RKznuFNSlJx7pzzXXStPUnNSOEO0jL5EVw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-react-jsx-development@7.22.5(@babel/core@7.23.5): + resolution: {integrity: sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.23.5) + dev: false + + /@babel/plugin-transform-react-jsx@7.22.15(@babel/core@7.23.5): + resolution: {integrity: sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.23.5) + '@babel/types': 7.23.5 + dev: false + + /@babel/plugin-transform-react-pure-annotations@7.22.5(@babel/core@7.23.5): + resolution: {integrity: sha512-gP4k85wx09q+brArVinTXhWiyzLl9UpmGva0+mWyKxk6JZequ05x3eUcIUE+FyttPKJFRRVtAvQaJ6YF9h1ZpA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-regenerator@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + regenerator-transform: 0.15.2 + dev: false + + /@babel/plugin-transform-reserved-words@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-runtime@7.22.15(@babel/core@7.23.5): + resolution: {integrity: sha512-tEVLhk8NRZSmwQ0DJtxxhTrCht1HVo8VaMzYT4w6lwyKBuHsgoioAUA7/6eT2fRfc5/23fuGdlwIxXhRVgWr4g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + babel-plugin-polyfill-corejs2: 0.4.6(@babel/core@7.23.5) + babel-plugin-polyfill-corejs3: 0.8.6(@babel/core@7.23.5) + babel-plugin-polyfill-regenerator: 0.5.3(@babel/core@7.23.5) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@babel/plugin-transform-shorthand-properties@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-spread@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + dev: false + + /@babel/plugin-transform-sticky-regex@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-template-literals@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-typeof-symbol@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-typescript@7.22.15(@babel/core@7.23.5): + resolution: {integrity: sha512-1uirS0TnijxvQLnlv5wQBwOX3E1wCFX7ITv+9pBV2wKEk4K+M5tqDaoNXnTH8tjEIYHLO98MwiTWO04Ggz4XuA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.5) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-typescript': 7.22.5(@babel/core@7.23.5) + dev: false + + /@babel/plugin-transform-unicode-escapes@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-unicode-property-regex@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.5) + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-unicode-regex@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.5) + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-transform-unicode-sets-regex@7.23.3(@babel/core@7.23.5): + resolution: {integrity: sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.5) + '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/preset-env@7.23.5(@babel/core@7.23.5): + resolution: {integrity: sha512-0d/uxVD6tFGWXGDSfyMD1p2otoaKmu6+GD+NfAx0tMaH+dxORnp7T9TaVQ6mKyya7iBtCIVxHjWT7MuzzM9z+A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.23.5 + '@babel/core': 7.23.5 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.23.5 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.23.5) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.5) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.23.5) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.5) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.23.5) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.23.5) + '@babel/plugin-syntax-import-assertions': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-syntax-import-attributes': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.23.5) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.5) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.5) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.5) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.5) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.5) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.5) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.5) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.5) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.5) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.23.5) + '@babel/plugin-transform-arrow-functions': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-async-generator-functions': 7.23.4(@babel/core@7.23.5) + '@babel/plugin-transform-async-to-generator': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-block-scoped-functions': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-block-scoping': 7.23.4(@babel/core@7.23.5) + '@babel/plugin-transform-class-properties': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-class-static-block': 7.23.4(@babel/core@7.23.5) + '@babel/plugin-transform-classes': 7.23.5(@babel/core@7.23.5) + '@babel/plugin-transform-computed-properties': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-destructuring': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-dotall-regex': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-duplicate-keys': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-dynamic-import': 7.23.4(@babel/core@7.23.5) + '@babel/plugin-transform-exponentiation-operator': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-export-namespace-from': 7.23.4(@babel/core@7.23.5) + '@babel/plugin-transform-for-of': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-function-name': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-json-strings': 7.23.4(@babel/core@7.23.5) + '@babel/plugin-transform-literals': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-logical-assignment-operators': 7.23.4(@babel/core@7.23.5) + '@babel/plugin-transform-member-expression-literals': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-modules-amd': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-modules-systemjs': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-modules-umd': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.23.5) + '@babel/plugin-transform-new-target': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-nullish-coalescing-operator': 7.23.4(@babel/core@7.23.5) + '@babel/plugin-transform-numeric-separator': 7.23.4(@babel/core@7.23.5) + '@babel/plugin-transform-object-rest-spread': 7.23.4(@babel/core@7.23.5) + '@babel/plugin-transform-object-super': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-optional-catch-binding': 7.23.4(@babel/core@7.23.5) + '@babel/plugin-transform-optional-chaining': 7.23.4(@babel/core@7.23.5) + '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-private-methods': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-private-property-in-object': 7.23.4(@babel/core@7.23.5) + '@babel/plugin-transform-property-literals': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-regenerator': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-reserved-words': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-shorthand-properties': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-spread': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-sticky-regex': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-template-literals': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-typeof-symbol': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-unicode-escapes': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-unicode-property-regex': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-unicode-regex': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-unicode-sets-regex': 7.23.3(@babel/core@7.23.5) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.23.5) + babel-plugin-polyfill-corejs2: 0.4.6(@babel/core@7.23.5) + babel-plugin-polyfill-corejs3: 0.8.6(@babel/core@7.23.5) + babel-plugin-polyfill-regenerator: 0.5.3(@babel/core@7.23.5) + core-js-compat: 3.33.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.23.5): + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/types': 7.23.5 + esutils: 2.0.3 + dev: false + + /@babel/preset-react@7.22.15(@babel/core@7.23.5): + resolution: {integrity: sha512-Csy1IJ2uEh/PecCBXXoZGAZBeCATTuePzCSB7dLYWS0vOEj6CNpjxIhW4duWwZodBNueH7QO14WbGn8YyeuN9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.23.5 + '@babel/plugin-transform-react-display-name': 7.22.5(@babel/core@7.23.5) + '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.23.5) + '@babel/plugin-transform-react-jsx-development': 7.22.5(@babel/core@7.23.5) + '@babel/plugin-transform-react-pure-annotations': 7.22.5(@babel/core@7.23.5) + dev: false + + /@babel/preset-typescript@7.22.15(@babel/core@7.23.5): + resolution: {integrity: sha512-HblhNmh6yM+cU4VwbBRpxFhxsTdfS1zsvH9W+gEjD0ARV9+8B4sNfpI6GuhePti84nuvhiwKS539jKPFHskA9A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.23.5 + '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.23.5) + '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.23.5) + '@babel/plugin-transform-typescript': 7.22.15(@babel/core@7.23.5) + dev: false + + /@babel/regjsgen@0.8.0: + resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} + dev: false + + /@babel/runtime-corejs3@7.22.15: + resolution: {integrity: sha512-SAj8oKi8UogVi6eXQXKNPu8qZ78Yzy7zawrlTr0M+IuW/g8Qe9gVDhGcF9h1S69OyACpYoLxEzpjs1M15sI5wQ==} + engines: {node: '>=6.9.0'} + dependencies: + core-js-pure: 3.32.1 + regenerator-runtime: 0.14.0 + dev: false + + /@babel/runtime@7.22.15: + resolution: {integrity: sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.0 + + /@babel/template@7.22.15: + resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/parser': 7.23.5 + '@babel/types': 7.23.5 + dev: false + + /@babel/traverse@7.23.5: + resolution: {integrity: sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.23.5 + '@babel/types': 7.23.5 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@babel/types@7.23.5: + resolution: {integrity: sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.23.4 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + dev: false + + /@colors/colors@1.5.0: + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + requiresBuild: true + dev: false + optional: true + + /@csstools/css-parser-algorithms@2.3.2(@csstools/css-tokenizer@2.2.1): + resolution: {integrity: sha512-sLYGdAdEY2x7TSw9FtmdaTrh2wFtRJO5VMbBrA8tEqEod7GEggFmxTSK9XqExib3yMuYNcvcTdCZIP6ukdjAIA==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + '@csstools/css-tokenizer': ^2.2.1 + dependencies: + '@csstools/css-tokenizer': 2.2.1 + dev: true + + /@csstools/css-tokenizer@2.2.1: + resolution: {integrity: sha512-Zmsf2f/CaEPWEVgw29odOj+WEVoiJy9s9NOv5GgNY9mZ1CZ7394By6wONrONrTsnNDv6F9hR02nvFihrGVGHBg==} + engines: {node: ^14 || ^16 || >=18} + dev: true + + /@csstools/media-query-list-parser@2.1.5(@csstools/css-parser-algorithms@2.3.2)(@csstools/css-tokenizer@2.2.1): + resolution: {integrity: sha512-IxVBdYzR8pYe89JiyXQuYk4aVVoCPhMJkz6ElRwlVysjwURTsTk/bmY/z4FfeRE+CRBMlykPwXEVUg8lThv7AQ==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + '@csstools/css-parser-algorithms': ^2.3.2 + '@csstools/css-tokenizer': ^2.2.1 + dependencies: + '@csstools/css-parser-algorithms': 2.3.2(@csstools/css-tokenizer@2.2.1) + '@csstools/css-tokenizer': 2.2.1 + dev: true + + /@csstools/selector-specificity@3.0.0(postcss-selector-parser@6.0.13): + resolution: {integrity: sha512-hBI9tfBtuPIi885ZsZ32IMEU/5nlZH/KOVYJCOh7gyMxaVLGmLedYqFN6Ui1LXkI8JlC8IsuC0rF0btcRZKd5g==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss-selector-parser: ^6.0.13 + dependencies: + postcss-selector-parser: 6.0.13 + dev: true + + /@discoveryjs/json-ext@0.5.7: + resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} + engines: {node: '>=10.0.0'} + dev: false + + /@docsearch/css@3.5.2: + resolution: {integrity: sha512-SPiDHaWKQZpwR2siD0KQUwlStvIAnEyK6tAE2h2Wuoq8ue9skzhlyVQ1ddzOxX6khULnAALDiR/isSF3bnuciA==} + dev: false + + /@docsearch/react@3.5.2(@algolia/client-search@4.19.1)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.8.2): + resolution: {integrity: sha512-9Ahcrs5z2jq/DcAvYtvlqEBHImbm4YJI8M9y0x6Tqg598P40HTEkX7hsMcIuThI+hTFxRGZ9hll0Wygm2yEjng==} + peerDependencies: + '@types/react': '>= 16.8.0 < 19.0.0' + react: '>= 16.8.0 < 19.0.0' + react-dom: '>= 16.8.0 < 19.0.0' + search-insights: '>= 1 < 3' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + react-dom: + optional: true + search-insights: + optional: true + dependencies: + '@algolia/autocomplete-core': 1.9.3(@algolia/client-search@4.19.1)(algoliasearch@4.19.1)(search-insights@2.8.2) + '@algolia/autocomplete-preset-algolia': 1.9.3(@algolia/client-search@4.19.1)(algoliasearch@4.19.1) + '@docsearch/css': 3.5.2 + '@types/react': 18.2.21 + algoliasearch: 4.19.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + search-insights: 2.8.2 + transitivePeerDependencies: + - '@algolia/client-search' + dev: false + + /@docusaurus/core@3.1.0(@docusaurus/types@3.1.0)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3): + resolution: {integrity: sha512-GWudMGYA9v26ssbAWJNfgeDZk+lrudUTclLPRsmxiknEBk7UMp7Rglonhqbsf3IKHOyHkMU4Fr5jFyg5SBx9jQ==} + engines: {node: '>=18.0'} + hasBin: true + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@babel/core': 7.23.5 + '@babel/generator': 7.23.5 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.23.5) + '@babel/plugin-transform-runtime': 7.22.15(@babel/core@7.23.5) + '@babel/preset-env': 7.23.5(@babel/core@7.23.5) + '@babel/preset-react': 7.22.15(@babel/core@7.23.5) + '@babel/preset-typescript': 7.22.15(@babel/core@7.23.5) + '@babel/runtime': 7.22.15 + '@babel/runtime-corejs3': 7.22.15 + '@babel/traverse': 7.23.5 + '@docusaurus/cssnano-preset': 3.1.0 + '@docusaurus/logger': 3.1.0 + '@docusaurus/mdx-loader': 3.1.0(@docusaurus/types@3.1.0)(react-dom@18.2.0)(react@18.2.0) + '@docusaurus/react-loadable': 5.5.2(react@18.2.0) + '@docusaurus/utils': 3.1.0(@docusaurus/types@3.1.0) + '@docusaurus/utils-common': 3.1.0(@docusaurus/types@3.1.0) + '@docusaurus/utils-validation': 3.1.0(@docusaurus/types@3.1.0) + '@slorber/static-site-generator-webpack-plugin': 4.0.7 + '@svgr/webpack': 6.5.1 + autoprefixer: 10.4.15(postcss@8.4.29) + babel-loader: 9.1.3(@babel/core@7.23.5)(webpack@5.88.2) + babel-plugin-dynamic-import-node: 2.3.3 + boxen: 6.2.1 + chalk: 4.1.2 + chokidar: 3.5.3 + clean-css: 5.3.2 + cli-table3: 0.6.3 + combine-promises: 1.2.0 + commander: 5.1.0 + copy-webpack-plugin: 11.0.0(webpack@5.88.2) + core-js: 3.32.1 + css-loader: 6.8.1(webpack@5.88.2) + css-minimizer-webpack-plugin: 4.2.2(clean-css@5.3.2)(webpack@5.88.2) + cssnano: 5.1.15(postcss@8.4.29) + del: 6.1.1 + detect-port: 1.5.1 + escape-html: 1.0.3 + eta: 2.2.0 + file-loader: 6.2.0(webpack@5.88.2) + fs-extra: 11.1.1 + html-minifier-terser: 7.2.0 + html-tags: 3.3.1 + html-webpack-plugin: 5.5.3(webpack@5.88.2) + leven: 3.1.0 + lodash: 4.17.21 + mini-css-extract-plugin: 2.7.6(webpack@5.88.2) + postcss: 8.4.29 + postcss-loader: 7.3.3(postcss@8.4.29)(typescript@5.3.3)(webpack@5.88.2) + prompts: 2.4.2 + react: 18.2.0 + react-dev-utils: 12.0.1(eslint@8.53.0)(typescript@5.3.3)(webpack@5.88.2) + react-dom: 18.2.0(react@18.2.0) + react-helmet-async: 1.3.0(react-dom@18.2.0)(react@18.2.0) + react-loadable: /@docusaurus/react-loadable@5.5.2(react@18.2.0) + react-loadable-ssr-addon-v5-slorber: 1.0.1(@docusaurus/react-loadable@5.5.2)(webpack@5.88.2) + react-router: 5.3.4(react@18.2.0) + react-router-config: 5.1.1(react-router@5.3.4)(react@18.2.0) + react-router-dom: 5.3.4(react@18.2.0) + rtl-detect: 1.0.4 + semver: 7.5.4 + serve-handler: 6.1.5 + shelljs: 0.8.5 + terser-webpack-plugin: 5.3.9(webpack@5.88.2) + tslib: 2.6.2 + update-notifier: 6.0.2 + url-loader: 4.1.1(file-loader@6.2.0)(webpack@5.88.2) + webpack: 5.88.2 + webpack-bundle-analyzer: 4.9.1 + webpack-dev-server: 4.15.1(webpack@5.88.2) + webpack-merge: 5.9.0 + webpackbar: 5.0.2(webpack@5.88.2) + transitivePeerDependencies: + - '@docusaurus/types' + - '@parcel/css' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - eslint + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - vue-template-compiler + - webpack-cli + dev: false + + /@docusaurus/cssnano-preset@3.1.0: + resolution: {integrity: sha512-ned7qsgCqSv/e7KyugFNroAfiszuxLwnvMW7gmT2Ywxb/Nyt61yIw7KHyAZCMKglOalrqnYA4gMhLUCK/mVePA==} + engines: {node: '>=18.0'} + dependencies: + cssnano-preset-advanced: 5.3.10(postcss@8.4.29) + postcss: 8.4.29 + postcss-sort-media-queries: 4.4.1(postcss@8.4.29) + tslib: 2.6.2 + dev: false + + /@docusaurus/eslint-plugin@3.1.0(eslint@8.53.0)(typescript@5.3.3): + resolution: {integrity: sha512-Zo/QNKFyHa0JG2shKwFKZTQnSc8ECXIWmnfi8y5RjTBiWLsoxusePCRcOQMqVWSRknO0bumZyOLjKEFK7Z57Ew==} + engines: {node: '>=18.0'} + peerDependencies: + eslint: '>=6' + dependencies: + '@typescript-eslint/utils': 5.62.0(eslint@8.53.0)(typescript@5.3.3) + eslint: 8.53.0 + tslib: 2.6.2 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@docusaurus/logger@3.1.0: + resolution: {integrity: sha512-p740M+HCst1VnKKzL60Hru9xfG4EUYJDarjlEC4hHeBy9+afPmY3BNPoSHx9/8zxuYfUlv/psf7I9NvRVdmdvg==} + engines: {node: '>=18.0'} + dependencies: + chalk: 4.1.2 + tslib: 2.6.2 + dev: false + + /@docusaurus/lqip-loader@3.1.0(webpack@5.88.2): + resolution: {integrity: sha512-vP7Smz7p5Xu75UvD4dA5qlkC7PnXl9dbTv6Eq0kLY8M5ZwBoKhNdB5c8u6j3MS5N8jUTtwYFEuH2wZWjr1/Fpw==} + engines: {node: '>=18.0'} + dependencies: + '@docusaurus/logger': 3.1.0 + file-loader: 6.2.0(webpack@5.88.2) + lodash: 4.17.21 + sharp: 0.32.6 + tslib: 2.6.2 + transitivePeerDependencies: + - webpack + dev: false + + /@docusaurus/mdx-loader@3.1.0(@docusaurus/types@3.1.0)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-D7onDz/3mgBonexWoQXPw3V2E5Bc4+jYRf9gGUUK+KoQwU8xMDaDkUUfsr7t6UBa/xox9p5+/3zwLuXOYMzGSg==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@babel/parser': 7.23.5 + '@babel/traverse': 7.23.5 + '@docusaurus/logger': 3.1.0 + '@docusaurus/utils': 3.1.0(@docusaurus/types@3.1.0) + '@docusaurus/utils-validation': 3.1.0(@docusaurus/types@3.1.0) + '@mdx-js/mdx': 3.0.0 + '@slorber/remark-comment': 1.0.0 + escape-html: 1.0.3 + estree-util-value-to-estree: 3.0.1 + file-loader: 6.2.0(webpack@5.88.2) + fs-extra: 11.1.1 + image-size: 1.0.2 + mdast-util-mdx: 3.0.0 + mdast-util-to-string: 4.0.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + rehype-raw: 7.0.0 + remark-directive: 3.0.0 + remark-emoji: 4.0.1 + remark-frontmatter: 5.0.0 + remark-gfm: 4.0.0 + stringify-object: 3.3.0 + tslib: 2.6.2 + unified: 11.0.4 + unist-util-visit: 5.0.0 + url-loader: 4.1.1(file-loader@6.2.0)(webpack@5.88.2) + vfile: 6.0.1 + webpack: 5.88.2 + transitivePeerDependencies: + - '@docusaurus/types' + - '@swc/core' + - esbuild + - supports-color + - uglify-js + - webpack-cli + dev: false + + /@docusaurus/module-type-aliases@3.1.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-XUl7Z4PWlKg4l6KF05JQ3iDHQxnPxbQUqTNKvviHyuHdlalOFv6qeDAm7IbzyQPJD5VA6y4dpRbTWSqP9ClwPg==} + peerDependencies: + react: '*' + react-dom: '*' + dependencies: + '@docusaurus/react-loadable': 5.5.2(react@18.2.0) + '@docusaurus/types': 3.1.0(react-dom@18.2.0)(react@18.2.0) + '@types/history': 4.7.11 + '@types/react': 18.2.21 + '@types/react-router-config': 5.0.7 + '@types/react-router-dom': 5.3.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-helmet-async: 1.3.0(react-dom@18.2.0)(react@18.2.0) + react-loadable: /@docusaurus/react-loadable@5.5.2(react@18.2.0) + transitivePeerDependencies: + - '@swc/core' + - esbuild + - supports-color + - uglify-js + - webpack-cli + + /@docusaurus/plugin-content-blog@3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3): + resolution: {integrity: sha512-iMa6WBaaEdYuxckvJtLcq/HQdlA4oEbCXf/OFfsYJCCULcDX7GDZpKxLF3X1fLsax3sSm5bmsU+CA0WD+R1g3A==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@docusaurus/core': 3.1.0(@docusaurus/types@3.1.0)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/logger': 3.1.0 + '@docusaurus/mdx-loader': 3.1.0(@docusaurus/types@3.1.0)(react-dom@18.2.0)(react@18.2.0) + '@docusaurus/types': 3.1.0(react-dom@18.2.0)(react@18.2.0) + '@docusaurus/utils': 3.1.0(@docusaurus/types@3.1.0) + '@docusaurus/utils-common': 3.1.0(@docusaurus/types@3.1.0) + '@docusaurus/utils-validation': 3.1.0(@docusaurus/types@3.1.0) + cheerio: 1.0.0-rc.12 + feed: 4.2.2 + fs-extra: 11.1.1 + lodash: 4.17.21 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + reading-time: 1.5.0 + srcset: 4.0.0 + tslib: 2.6.2 + unist-util-visit: 5.0.0 + utility-types: 3.10.0 + webpack: 5.88.2 + transitivePeerDependencies: + - '@parcel/css' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - eslint + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - vue-template-compiler + - webpack-cli + dev: false + + /@docusaurus/plugin-content-docs@3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3): + resolution: {integrity: sha512-el5GxhT8BLrsWD0qGa8Rq+Ttb/Ni6V3DGT2oAPio0qcs/mUAxeyXEAmihkvmLCnAgp6xD27Ce7dISZ5c6BXeqA==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@docusaurus/core': 3.1.0(@docusaurus/types@3.1.0)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/logger': 3.1.0 + '@docusaurus/mdx-loader': 3.1.0(@docusaurus/types@3.1.0)(react-dom@18.2.0)(react@18.2.0) + '@docusaurus/module-type-aliases': 3.1.0(react-dom@18.2.0)(react@18.2.0) + '@docusaurus/types': 3.1.0(react-dom@18.2.0)(react@18.2.0) + '@docusaurus/utils': 3.1.0(@docusaurus/types@3.1.0) + '@docusaurus/utils-validation': 3.1.0(@docusaurus/types@3.1.0) + '@types/react-router-config': 5.0.7 + combine-promises: 1.2.0 + fs-extra: 11.1.1 + js-yaml: 4.1.0 + lodash: 4.17.21 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tslib: 2.6.2 + utility-types: 3.10.0 + webpack: 5.88.2 + transitivePeerDependencies: + - '@parcel/css' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - eslint + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - vue-template-compiler + - webpack-cli + dev: false + + /@docusaurus/plugin-content-pages@3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3): + resolution: {integrity: sha512-9gntYQFpk+93+Xl7gYczJu8I9uWoyRLnRwS0+NUFcs9iZtHKsdqKWPRrONC9elfN3wJ9ORwTbcVzsTiB8jvYlg==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@docusaurus/core': 3.1.0(@docusaurus/types@3.1.0)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/mdx-loader': 3.1.0(@docusaurus/types@3.1.0)(react-dom@18.2.0)(react@18.2.0) + '@docusaurus/types': 3.1.0(react-dom@18.2.0)(react@18.2.0) + '@docusaurus/utils': 3.1.0(@docusaurus/types@3.1.0) + '@docusaurus/utils-validation': 3.1.0(@docusaurus/types@3.1.0) + fs-extra: 11.1.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tslib: 2.6.2 + webpack: 5.88.2 + transitivePeerDependencies: + - '@parcel/css' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - eslint + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - vue-template-compiler + - webpack-cli + dev: false + + /@docusaurus/plugin-debug@3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3): + resolution: {integrity: sha512-AbvJwCVRbmQ8w9d8QXbF4Iq/ui0bjPZNYFIhtducGFnm2YQRN1mraK8mCEQb0Aq0T8SqRRvSfC/far4n/s531w==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@docusaurus/core': 3.1.0(@docusaurus/types@3.1.0)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/types': 3.1.0(react-dom@18.2.0)(react@18.2.0) + '@docusaurus/utils': 3.1.0(@docusaurus/types@3.1.0) + fs-extra: 11.1.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-json-view-lite: 1.2.1(react@18.2.0) + tslib: 2.6.2 + transitivePeerDependencies: + - '@parcel/css' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - eslint + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - vue-template-compiler + - webpack-cli + dev: false + + /@docusaurus/plugin-google-analytics@3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3): + resolution: {integrity: sha512-zvUOMzu9Uhz0ciqnSbtnp/5i1zEYlzarQrOXG90P3Is3efQI43p2YLW/rzSGdLb5MfQo2HvKT6Q5+tioMO045Q==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@docusaurus/core': 3.1.0(@docusaurus/types@3.1.0)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/types': 3.1.0(react-dom@18.2.0)(react@18.2.0) + '@docusaurus/utils-validation': 3.1.0(@docusaurus/types@3.1.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tslib: 2.6.2 + transitivePeerDependencies: + - '@parcel/css' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - eslint + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - vue-template-compiler + - webpack-cli + dev: false + + /@docusaurus/plugin-google-gtag@3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3): + resolution: {integrity: sha512-0txshvaY8qIBdkk2UATdVcfiCLGq3KAUfuRQD2cRNgO39iIf4/ihQxH9NXcRTwKs4Q5d9yYHoix3xT6pFuEYOg==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@docusaurus/core': 3.1.0(@docusaurus/types@3.1.0)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/types': 3.1.0(react-dom@18.2.0)(react@18.2.0) + '@docusaurus/utils-validation': 3.1.0(@docusaurus/types@3.1.0) + '@types/gtag.js': 0.0.12 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tslib: 2.6.2 + transitivePeerDependencies: + - '@parcel/css' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - eslint + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - vue-template-compiler + - webpack-cli + dev: false + + /@docusaurus/plugin-google-tag-manager@3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3): + resolution: {integrity: sha512-zOWPEi8kMyyPtwG0vhyXrdbLs8fIZmY5vlbi9lUU+v8VsroO5iHmfR2V3SMsrsfOanw5oV/ciWqbxezY00qEZg==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@docusaurus/core': 3.1.0(@docusaurus/types@3.1.0)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/types': 3.1.0(react-dom@18.2.0)(react@18.2.0) + '@docusaurus/utils-validation': 3.1.0(@docusaurus/types@3.1.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tslib: 2.6.2 + transitivePeerDependencies: + - '@parcel/css' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - eslint + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - vue-template-compiler + - webpack-cli + dev: false + + /@docusaurus/plugin-ideal-image@3.1.0(eslint@8.53.0)(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3): + resolution: {integrity: sha512-ytT7f3hCM78yyv8Km3DIE1Myqp5GEGE8JnRz98wUWDzZq1tNroe+dhrLTqI2D4iqwOYFMcjFQkcvK0vDkLG7cw==} + engines: {node: '>=18.0'} + peerDependencies: + jimp: '*' + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + jimp: + optional: true + dependencies: + '@docusaurus/core': 3.1.0(@docusaurus/types@3.1.0)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/lqip-loader': 3.1.0(webpack@5.88.2) + '@docusaurus/responsive-loader': 1.7.0(sharp@0.32.6) + '@docusaurus/theme-translations': 3.1.0 + '@docusaurus/types': 3.1.0(react-dom@18.2.0)(react@18.2.0) + '@docusaurus/utils-validation': 3.1.0(@docusaurus/types@3.1.0) + '@slorber/react-ideal-image': 0.0.12(prop-types@15.8.1)(react-waypoint@10.3.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-waypoint: 10.3.0(react@18.2.0) + sharp: 0.32.6 + tslib: 2.6.2 + webpack: 5.88.2 + transitivePeerDependencies: + - '@parcel/css' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - eslint + - lightningcss + - prop-types + - supports-color + - typescript + - uglify-js + - utf-8-validate + - vue-template-compiler + - webpack-cli + dev: false + + /@docusaurus/plugin-pwa@3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3): + resolution: {integrity: sha512-8UXyD35x3BALl7GuWWqSbC/ldnJHW8wfk5xzMDHfi5kKK+8OxpayJNxmXFXd0FlCVGN1PxE5EAhpdYIj9SBMUQ==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@babel/core': 7.23.5 + '@babel/preset-env': 7.23.5(@babel/core@7.23.5) + '@docusaurus/core': 3.1.0(@docusaurus/types@3.1.0)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/theme-common': 3.1.0(@docusaurus/types@3.1.0)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/theme-translations': 3.1.0 + '@docusaurus/types': 3.1.0(react-dom@18.2.0)(react@18.2.0) + '@docusaurus/utils': 3.1.0(@docusaurus/types@3.1.0) + '@docusaurus/utils-validation': 3.1.0(@docusaurus/types@3.1.0) + babel-loader: 9.1.3(@babel/core@7.23.5)(webpack@5.88.2) + clsx: 2.0.0 + core-js: 3.32.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + terser-webpack-plugin: 5.3.9(webpack@5.88.2) + tslib: 2.6.2 + webpack: 5.88.2 + webpack-merge: 5.9.0 + webpackbar: 5.0.2(webpack@5.88.2) + workbox-build: 7.0.0 + workbox-precaching: 7.0.0 + workbox-window: 7.0.0 + transitivePeerDependencies: + - '@parcel/css' + - '@swc/core' + - '@swc/css' + - '@types/babel__core' + - bufferutil + - csso + - debug + - esbuild + - eslint + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - vue-template-compiler + - webpack-cli + dev: false + + /@docusaurus/plugin-sitemap@3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3): + resolution: {integrity: sha512-TkR5vGBpUooEB9SoW42thahqqwKzfHrQQhkB+JrEGERsl4bKODSuJNle4aA4h6LSkg4IyfXOW8XOI0NIPWb9Cg==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@docusaurus/core': 3.1.0(@docusaurus/types@3.1.0)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/logger': 3.1.0 + '@docusaurus/types': 3.1.0(react-dom@18.2.0)(react@18.2.0) + '@docusaurus/utils': 3.1.0(@docusaurus/types@3.1.0) + '@docusaurus/utils-common': 3.1.0(@docusaurus/types@3.1.0) + '@docusaurus/utils-validation': 3.1.0(@docusaurus/types@3.1.0) + fs-extra: 11.1.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + sitemap: 7.1.1 + tslib: 2.6.2 + transitivePeerDependencies: + - '@parcel/css' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - eslint + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - vue-template-compiler + - webpack-cli + dev: false + + /@docusaurus/preset-classic@3.1.0(@algolia/client-search@4.19.1)(@types/react@18.2.21)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.8.2)(typescript@5.3.3): + resolution: {integrity: sha512-xGLQRFmmT9IinAGUDVRYZ54Ys28USNbA3OTXQXnSJLPr1rCY7CYnHI4XoOnKWrNnDiAI4ruMzunXWyaElUYCKQ==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@docusaurus/core': 3.1.0(@docusaurus/types@3.1.0)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/plugin-content-blog': 3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/plugin-content-docs': 3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/plugin-content-pages': 3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/plugin-debug': 3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/plugin-google-analytics': 3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/plugin-google-gtag': 3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/plugin-google-tag-manager': 3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/plugin-sitemap': 3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/theme-classic': 3.1.0(@types/react@18.2.21)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/theme-common': 3.1.0(@docusaurus/types@3.1.0)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/theme-search-algolia': 3.1.0(@algolia/client-search@4.19.1)(@docusaurus/types@3.1.0)(@types/react@18.2.21)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.8.2)(typescript@5.3.3) + '@docusaurus/types': 3.1.0(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + transitivePeerDependencies: + - '@algolia/client-search' + - '@parcel/css' + - '@swc/core' + - '@swc/css' + - '@types/react' + - bufferutil + - csso + - debug + - esbuild + - eslint + - lightningcss + - search-insights + - supports-color + - typescript + - uglify-js + - utf-8-validate + - vue-template-compiler + - webpack-cli + dev: false + + /@docusaurus/react-loadable@5.5.2(react@18.2.0): + resolution: {integrity: sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==} + peerDependencies: + react: '*' + dependencies: + '@types/react': 18.2.21 + prop-types: 15.8.1 + react: 18.2.0 + + /@docusaurus/responsive-loader@1.7.0(sharp@0.32.6): + resolution: {integrity: sha512-N0cWuVqTRXRvkBxeMQcy/OF2l7GN8rmni5EzR3HpwR+iU2ckYPnziceojcxvvxQ5NqZg1QfEW0tycQgHp+e+Nw==} + engines: {node: '>=12'} + peerDependencies: + jimp: '*' + sharp: '*' + peerDependenciesMeta: + jimp: + optional: true + sharp: + optional: true + dependencies: + loader-utils: 2.0.4 + sharp: 0.32.6 + dev: false + + /@docusaurus/theme-classic@3.1.0(@types/react@18.2.21)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3): + resolution: {integrity: sha512-/+jMl2Z9O8QQxves5AtHdt91gWsEZFgOV3La/6eyKEd7QLqQUtM5fxEJ40rq9NKYjqCd1HzZ9egIMeJoWwillw==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@docusaurus/core': 3.1.0(@docusaurus/types@3.1.0)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/mdx-loader': 3.1.0(@docusaurus/types@3.1.0)(react-dom@18.2.0)(react@18.2.0) + '@docusaurus/module-type-aliases': 3.1.0(react-dom@18.2.0)(react@18.2.0) + '@docusaurus/plugin-content-blog': 3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/plugin-content-docs': 3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/plugin-content-pages': 3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/theme-common': 3.1.0(@docusaurus/types@3.1.0)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/theme-translations': 3.1.0 + '@docusaurus/types': 3.1.0(react-dom@18.2.0)(react@18.2.0) + '@docusaurus/utils': 3.1.0(@docusaurus/types@3.1.0) + '@docusaurus/utils-common': 3.1.0(@docusaurus/types@3.1.0) + '@docusaurus/utils-validation': 3.1.0(@docusaurus/types@3.1.0) + '@mdx-js/react': 3.0.0(@types/react@18.2.21)(react@18.2.0) + clsx: 2.0.0 + copy-text-to-clipboard: 3.2.0 + infima: 0.2.0-alpha.43 + lodash: 4.17.21 + nprogress: 0.2.0 + postcss: 8.4.29 + prism-react-renderer: 2.3.1(react@18.2.0) + prismjs: 1.29.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-router-dom: 5.3.4(react@18.2.0) + rtlcss: 4.1.1 + tslib: 2.6.2 + utility-types: 3.10.0 + transitivePeerDependencies: + - '@parcel/css' + - '@swc/core' + - '@swc/css' + - '@types/react' + - bufferutil + - csso + - debug + - esbuild + - eslint + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - vue-template-compiler + - webpack-cli + dev: false + + /@docusaurus/theme-common@3.1.0(@docusaurus/types@3.1.0)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3): + resolution: {integrity: sha512-YGwEFALLIbF5ocW/Fy6Ae7tFWUOugEN3iwxTx8UkLAcLqYUboDSadesYtVBmRCEB4FVA2qoP7YaW3lu3apUPPw==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@docusaurus/mdx-loader': 3.1.0(@docusaurus/types@3.1.0)(react-dom@18.2.0)(react@18.2.0) + '@docusaurus/module-type-aliases': 3.1.0(react-dom@18.2.0)(react@18.2.0) + '@docusaurus/plugin-content-blog': 3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/plugin-content-docs': 3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/plugin-content-pages': 3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/utils': 3.1.0(@docusaurus/types@3.1.0) + '@docusaurus/utils-common': 3.1.0(@docusaurus/types@3.1.0) + '@types/history': 4.7.11 + '@types/react': 18.2.21 + '@types/react-router-config': 5.0.7 + clsx: 2.0.0 + parse-numeric-range: 1.3.0 + prism-react-renderer: 2.3.1(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tslib: 2.6.2 + utility-types: 3.10.0 + transitivePeerDependencies: + - '@docusaurus/types' + - '@parcel/css' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - eslint + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - vue-template-compiler + - webpack-cli + dev: false + + /@docusaurus/theme-search-algolia@3.1.0(@algolia/client-search@4.19.1)(@docusaurus/types@3.1.0)(@types/react@18.2.21)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.8.2)(typescript@5.3.3): + resolution: {integrity: sha512-8cJH0ZhPsEDjq3jR3I+wHmWzVY2bXMQJ59v2QxUmsTZxbWA4u+IzccJMIJx4ooFl9J6iYynwYsFuHxyx/KUmfQ==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@docsearch/react': 3.5.2(@algolia/client-search@4.19.1)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.8.2) + '@docusaurus/core': 3.1.0(@docusaurus/types@3.1.0)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/logger': 3.1.0 + '@docusaurus/plugin-content-docs': 3.1.0(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/theme-common': 3.1.0(@docusaurus/types@3.1.0)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@docusaurus/theme-translations': 3.1.0 + '@docusaurus/utils': 3.1.0(@docusaurus/types@3.1.0) + '@docusaurus/utils-validation': 3.1.0(@docusaurus/types@3.1.0) + algoliasearch: 4.19.1 + algoliasearch-helper: 3.14.0(algoliasearch@4.19.1) + clsx: 2.0.0 + eta: 2.2.0 + fs-extra: 11.1.1 + lodash: 4.17.21 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tslib: 2.6.2 + utility-types: 3.10.0 + transitivePeerDependencies: + - '@algolia/client-search' + - '@docusaurus/types' + - '@parcel/css' + - '@swc/core' + - '@swc/css' + - '@types/react' + - bufferutil + - csso + - debug + - esbuild + - eslint + - lightningcss + - search-insights + - supports-color + - typescript + - uglify-js + - utf-8-validate + - vue-template-compiler + - webpack-cli + dev: false + + /@docusaurus/theme-translations@3.1.0: + resolution: {integrity: sha512-DApE4AbDI+WBajihxB54L4scWQhVGNZAochlC9fkbciPuFAgdRBD3NREb0rgfbKexDC/rioppu/WJA0u8tS+yA==} + engines: {node: '>=18.0'} + dependencies: + fs-extra: 11.1.1 + tslib: 2.6.2 + dev: false + + /@docusaurus/tsconfig@3.1.0: + resolution: {integrity: sha512-PE6fSuj5gJy5sNC1OO+bYAU1/xZH5YqddGjhrNu3/T7OAUroqkMZfVl13Tz70CjYB8no4OWcraqSkObAeNdIcQ==} + dev: true + + /@docusaurus/types@3.1.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-VaczOZf7+re8aFBIWnex1XENomwHdsSTkrdX43zyor7G/FY4OIsP6X28Xc3o0jiY0YdNuvIDyA5TNwOtpgkCVw==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@mdx-js/mdx': 3.0.0 + '@types/history': 4.7.11 + '@types/react': 18.2.21 + commander: 5.1.0 + joi: 17.10.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-helmet-async: 1.3.0(react-dom@18.2.0)(react@18.2.0) + utility-types: 3.10.0 + webpack: 5.88.2 + webpack-merge: 5.9.0 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - supports-color + - uglify-js + - webpack-cli + + /@docusaurus/utils-common@3.1.0(@docusaurus/types@3.1.0): + resolution: {integrity: sha512-SfvnRLHoZ9bwTw67knkSs7IcUR0GY2SaGkpdB/J9pChrDiGhwzKNUhcieoPyPYrOWGRPk3rVNYtoy+Bc7psPAw==} + engines: {node: '>=18.0'} + peerDependencies: + '@docusaurus/types': '*' + peerDependenciesMeta: + '@docusaurus/types': + optional: true + dependencies: + '@docusaurus/types': 3.1.0(react-dom@18.2.0)(react@18.2.0) + tslib: 2.6.2 + dev: false + + /@docusaurus/utils-validation@3.1.0(@docusaurus/types@3.1.0): + resolution: {integrity: sha512-dFxhs1NLxPOSzmcTk/eeKxLY5R+U4cua22g9MsAMiRWcwFKStZ2W3/GDY0GmnJGqNS8QAQepJrxQoyxXkJNDeg==} + engines: {node: '>=18.0'} + dependencies: + '@docusaurus/logger': 3.1.0 + '@docusaurus/utils': 3.1.0(@docusaurus/types@3.1.0) + joi: 17.10.1 + js-yaml: 4.1.0 + tslib: 2.6.2 + transitivePeerDependencies: + - '@docusaurus/types' + - '@swc/core' + - esbuild + - supports-color + - uglify-js + - webpack-cli + dev: false + + /@docusaurus/utils@3.1.0(@docusaurus/types@3.1.0): + resolution: {integrity: sha512-LgZfp0D+UBqAh7PZ//MUNSFBMavmAPku6Si9x8x3V+S318IGCNJ6hUr2O29UO0oLybEWUjD5Jnj9IUN6XyZeeg==} + engines: {node: '>=18.0'} + peerDependencies: + '@docusaurus/types': '*' + peerDependenciesMeta: + '@docusaurus/types': + optional: true + dependencies: + '@docusaurus/logger': 3.1.0 + '@docusaurus/types': 3.1.0(react-dom@18.2.0)(react@18.2.0) + '@svgr/webpack': 6.5.1 + escape-string-regexp: 4.0.0 + file-loader: 6.2.0(webpack@5.88.2) + fs-extra: 11.1.1 + github-slugger: 1.5.0 + globby: 11.1.0 + gray-matter: 4.0.3 + jiti: 1.21.0 + js-yaml: 4.1.0 + lodash: 4.17.21 + micromatch: 4.0.5 + resolve-pathname: 3.0.0 + shelljs: 0.8.5 + tslib: 2.6.2 + url-loader: 4.1.1(file-loader@6.2.0)(webpack@5.88.2) + webpack: 5.88.2 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - supports-color + - uglify-js + - webpack-cli + dev: false + + /@emotion/is-prop-valid@0.8.8: + resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} + requiresBuild: true + dependencies: + '@emotion/memoize': 0.7.4 + dev: false + optional: true + + /@emotion/memoize@0.7.4: + resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} + requiresBuild: true + dev: false + optional: true + + /@eslint-community/eslint-utils@4.4.0(eslint@8.53.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.53.0 + eslint-visitor-keys: 3.4.3 + + /@eslint-community/regexpp@4.8.0: + resolution: {integrity: sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + /@eslint/eslintrc@2.1.3: + resolution: {integrity: sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.6.1 + globals: 13.21.0 + ignore: 5.2.4 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + /@eslint/js@8.53.0: + resolution: {integrity: sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + /@giscus/react@2.3.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-tj79B+NNBfidhPdXJqWoqRm5Jhoc6CBhXMYwBR9nwTwsrdaB/spcQXmHpKcUuOdXZtlYSwMfCFcBogMNbD+gKQ==} + peerDependencies: + react: ^16 || ^17 || ^18 + react-dom: ^16 || ^17 || ^18 + dependencies: + giscus: 1.3.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@hapi/hoek@9.3.0: + resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} + + /@hapi/topo@5.1.0: + resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + dependencies: + '@hapi/hoek': 9.3.0 + + /@humanwhocodes/config-array@0.11.13: + resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 2.0.1 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + /@humanwhocodes/object-schema@2.0.1: + resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} + + /@iconify/react@4.1.1(react@18.2.0): + resolution: {integrity: sha512-jed14EjvKjee8mc0eoscGxlg7mSQRkwQG3iX3cPBCO7UlOjz0DtlvTqxqEcHUJGh+z1VJ31Yhu5B9PxfO0zbdg==} + peerDependencies: + react: '>=16' + dependencies: + '@iconify/types': 2.0.0 + react: 18.2.0 + dev: true + + /@iconify/types@2.0.0: + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + dev: true + + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: false + + /@jest/types@29.6.3: + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.4 + '@types/istanbul-reports': 3.0.1 + '@types/node': 20.5.9 + '@types/yargs': 17.0.24 + chalk: 4.1.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.19 + + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + + /@jridgewell/set-array@1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + + /@jridgewell/source-map@0.3.5: + resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.19 + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + + /@jridgewell/trace-mapping@0.3.19: + resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + + /@leichtgewicht/ip-codec@2.0.4: + resolution: {integrity: sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==} + dev: false + + /@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 + + /@mdx-js/mdx@3.0.0: + resolution: {integrity: sha512-Icm0TBKBLYqroYbNW3BPnzMGn+7mwpQOK310aZ7+fkCtiU3aqv2cdcX+nd0Ydo3wI5Rx8bX2Z2QmGb/XcAClCw==} + dependencies: + '@types/estree': 1.0.1 + '@types/estree-jsx': 1.0.0 + '@types/hast': 3.0.2 + '@types/mdx': 2.0.7 + collapse-white-space: 2.1.0 + devlop: 1.1.0 + estree-util-build-jsx: 3.0.1 + estree-util-is-identifier-name: 3.0.0 + estree-util-to-js: 2.0.0 + estree-walker: 3.0.3 + hast-util-to-estree: 3.1.0 + hast-util-to-jsx-runtime: 2.2.0 + markdown-extensions: 2.0.0 + periscopic: 3.1.0 + remark-mdx: 3.0.0 + remark-parse: 11.0.0 + remark-rehype: 11.0.0 + source-map: 0.7.4 + unified: 11.0.4 + unist-util-position-from-estree: 2.0.0 + unist-util-stringify-position: 4.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.1 + transitivePeerDependencies: + - supports-color + + /@mdx-js/react@3.0.0(@types/react@18.2.21)(react@18.2.0): + resolution: {integrity: sha512-nDctevR9KyYFyV+m+/+S4cpzCWHqj+iHDHq3QrsWezcC+B17uZdIWgCguESUkwFhM3n/56KxWVE3V6EokrmONQ==} + peerDependencies: + '@types/react': '>=16' + react: '>=16' + dependencies: + '@types/mdx': 2.0.7 + '@types/react': 18.2.21 + react: 18.2.0 + 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 + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + /@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 + + /@pkgr/utils@2.4.2: + resolution: {integrity: sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + dependencies: + cross-spawn: 7.0.3 + fast-glob: 3.3.1 + is-glob: 4.0.3 + open: 9.1.0 + picocolors: 1.0.0 + tslib: 2.6.2 + dev: true + + /@pnpm/config.env-replace@1.1.0: + resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} + engines: {node: '>=12.22.0'} + dev: false + + /@pnpm/network.ca-file@1.0.2: + resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} + engines: {node: '>=12.22.0'} + dependencies: + graceful-fs: 4.2.10 + dev: false + + /@pnpm/npm-conf@2.2.2: + resolution: {integrity: sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA==} + engines: {node: '>=12'} + dependencies: + '@pnpm/config.env-replace': 1.1.0 + '@pnpm/network.ca-file': 1.0.2 + config-chain: 1.1.13 + dev: false + + /@polka/url@1.0.0-next.21: + resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} + dev: false + + /@popperjs/core@2.11.8: + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + dev: false + + /@rollup/plugin-babel@5.3.1(@babel/core@7.23.5)(rollup@2.79.1): + resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} + engines: {node: '>= 10.0.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@types/babel__core': ^7.1.9 + rollup: ^1.20.0||^2.0.0 + peerDependenciesMeta: + '@types/babel__core': + optional: true + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-module-imports': 7.22.15 + '@rollup/pluginutils': 3.1.0(rollup@2.79.1) + rollup: 2.79.1 + dev: false + + /@rollup/plugin-node-resolve@11.2.1(rollup@2.79.1): + resolution: {integrity: sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==} + engines: {node: '>= 10.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + dependencies: + '@rollup/pluginutils': 3.1.0(rollup@2.79.1) + '@types/resolve': 1.17.1 + builtin-modules: 3.3.0 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.4 + rollup: 2.79.1 + dev: false + + /@rollup/plugin-replace@2.4.2(rollup@2.79.1): + resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==} + peerDependencies: + rollup: ^1.20.0 || ^2.0.0 + dependencies: + '@rollup/pluginutils': 3.1.0(rollup@2.79.1) + magic-string: 0.25.9 + rollup: 2.79.1 + dev: false + + /@rollup/pluginutils@3.1.0(rollup@2.79.1): + resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} + engines: {node: '>= 8.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + dependencies: + '@types/estree': 0.0.39 + estree-walker: 1.0.1 + picomatch: 2.3.1 + rollup: 2.79.1 + dev: false + + /@sideway/address@4.1.4: + resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==} + dependencies: + '@hapi/hoek': 9.3.0 + + /@sideway/formula@3.0.1: + resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} + + /@sideway/pinpoint@2.0.0: + resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} + + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: false + + /@sindresorhus/is@3.1.2: + resolution: {integrity: sha512-JiX9vxoKMmu8Y3Zr2RVathBL1Cdu4Nt4MuNWemt1Nc06A0RAin9c5FArkhGsyMBWfCu4zj+9b+GxtjAnE4qqLQ==} + engines: {node: '>=10'} + dev: false + + /@sindresorhus/is@5.6.0: + resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} + engines: {node: '>=14.16'} + dev: false + + /@slorber/react-ideal-image@0.0.12(prop-types@15.8.1)(react-waypoint@10.3.0)(react@18.2.0): + resolution: {integrity: sha512-u8KiDTEkMA7/KAeA5ywg/P7YG4zuKhWtswfVZDH8R8HXgQsFcHIYU2WaQnGuK/Du7Wdj90I+SdFmajSGFRvoKA==} + engines: {node: '>= 8.9.0', npm: '> 3'} + peerDependencies: + prop-types: '>=15' + react: '>=0.14.x' + react-waypoint: '>=9.0.2' + dependencies: + prop-types: 15.8.1 + react: 18.2.0 + react-waypoint: 10.3.0(react@18.2.0) + dev: false + + /@slorber/remark-comment@1.0.0: + resolution: {integrity: sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA==} + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + dev: false + + /@slorber/static-site-generator-webpack-plugin@4.0.7: + resolution: {integrity: sha512-Ug7x6z5lwrz0WqdnNFOMYrDQNTPAprvHLSh6+/fmml3qUiz6l5eq+2MzLKWtn/q5K5NpSiFsZTP/fck/3vjSxA==} + engines: {node: '>=14'} + dependencies: + eval: 0.1.8 + p-map: 4.0.0 + webpack-sources: 3.2.3 + dev: false + + /@surma/rollup-plugin-off-main-thread@2.2.3: + resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} + dependencies: + ejs: 3.1.9 + json5: 2.2.3 + magic-string: 0.25.9 + string.prototype.matchall: 4.0.9 + dev: false + + /@svgr/babel-plugin-add-jsx-attribute@6.5.1(@babel/core@7.23.5): + resolution: {integrity: sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ==} + engines: {node: '>=10'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + dev: false + + /@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.23.5): + resolution: {integrity: sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + dev: false + + /@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.23.5): + resolution: {integrity: sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + dev: false + + /@svgr/babel-plugin-replace-jsx-attribute-value@6.5.1(@babel/core@7.23.5): + resolution: {integrity: sha512-8DPaVVE3fd5JKuIC29dqyMB54sA6mfgki2H2+swh+zNJoynC8pMPzOkidqHOSc6Wj032fhl8Z0TVn1GiPpAiJg==} + engines: {node: '>=10'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + dev: false + + /@svgr/babel-plugin-svg-dynamic-title@6.5.1(@babel/core@7.23.5): + resolution: {integrity: sha512-FwOEi0Il72iAzlkaHrlemVurgSQRDFbk0OC8dSvD5fSBPHltNh7JtLsxmZUhjYBZo2PpcU/RJvvi6Q0l7O7ogw==} + engines: {node: '>=10'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + dev: false + + /@svgr/babel-plugin-svg-em-dimensions@6.5.1(@babel/core@7.23.5): + resolution: {integrity: sha512-gWGsiwjb4tw+ITOJ86ndY/DZZ6cuXMNE/SjcDRg+HLuCmwpcjOktwRF9WgAiycTqJD/QXqL2f8IzE2Rzh7aVXA==} + engines: {node: '>=10'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + dev: false + + /@svgr/babel-plugin-transform-react-native-svg@6.5.1(@babel/core@7.23.5): + resolution: {integrity: sha512-2jT3nTayyYP7kI6aGutkyfJ7UMGtuguD72OjeGLwVNyfPRBD8zQthlvL+fAbAKk5n9ZNcvFkp/b1lZ7VsYqVJg==} + engines: {node: '>=10'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + dev: false + + /@svgr/babel-plugin-transform-svg-component@6.5.1(@babel/core@7.23.5): + resolution: {integrity: sha512-a1p6LF5Jt33O3rZoVRBqdxL350oge54iZWHNI6LJB5tQ7EelvD/Mb1mfBiZNAan0dt4i3VArkFRjA4iObuNykQ==} + engines: {node: '>=12'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + dev: false + + /@svgr/babel-preset@6.5.1(@babel/core@7.23.5): + resolution: {integrity: sha512-6127fvO/FF2oi5EzSQOAjo1LE3OtNVh11R+/8FXa+mHx1ptAaS4cknIjnUA7e6j6fwGGJ17NzaTJFUwOV2zwCw==} + engines: {node: '>=10'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.5 + '@svgr/babel-plugin-add-jsx-attribute': 6.5.1(@babel/core@7.23.5) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.23.5) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.23.5) + '@svgr/babel-plugin-replace-jsx-attribute-value': 6.5.1(@babel/core@7.23.5) + '@svgr/babel-plugin-svg-dynamic-title': 6.5.1(@babel/core@7.23.5) + '@svgr/babel-plugin-svg-em-dimensions': 6.5.1(@babel/core@7.23.5) + '@svgr/babel-plugin-transform-react-native-svg': 6.5.1(@babel/core@7.23.5) + '@svgr/babel-plugin-transform-svg-component': 6.5.1(@babel/core@7.23.5) + dev: false + + /@svgr/core@6.5.1: + resolution: {integrity: sha512-/xdLSWxK5QkqG524ONSjvg3V/FkNyCv538OIBdQqPNaAta3AsXj/Bd2FbvR87yMbXO2hFSWiAe/Q6IkVPDw+mw==} + engines: {node: '>=10'} + dependencies: + '@babel/core': 7.23.5 + '@svgr/babel-preset': 6.5.1(@babel/core@7.23.5) + '@svgr/plugin-jsx': 6.5.1(@svgr/core@6.5.1) + camelcase: 6.3.0 + cosmiconfig: 7.1.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@svgr/hast-util-to-babel-ast@6.5.1: + resolution: {integrity: sha512-1hnUxxjd83EAxbL4a0JDJoD3Dao3hmjvyvyEV8PzWmLK3B9m9NPlW7GKjFyoWE8nM7HnXzPcmmSyOW8yOddSXw==} + engines: {node: '>=10'} + dependencies: + '@babel/types': 7.23.5 + entities: 4.5.0 + dev: false + + /@svgr/plugin-jsx@6.5.1(@svgr/core@6.5.1): + resolution: {integrity: sha512-+UdQxI3jgtSjCykNSlEMuy1jSRQlGC7pqBCPvkG/2dATdWo082zHTTK3uhnAju2/6XpE6B5mZ3z4Z8Ns01S8Gw==} + engines: {node: '>=10'} + peerDependencies: + '@svgr/core': ^6.0.0 + dependencies: + '@babel/core': 7.23.5 + '@svgr/babel-preset': 6.5.1(@babel/core@7.23.5) + '@svgr/core': 6.5.1 + '@svgr/hast-util-to-babel-ast': 6.5.1 + svg-parser: 2.0.4 + transitivePeerDependencies: + - supports-color + dev: false + + /@svgr/plugin-svgo@6.5.1(@svgr/core@6.5.1): + resolution: {integrity: sha512-omvZKf8ixP9z6GWgwbtmP9qQMPX4ODXi+wzbVZgomNFsUIlHA1sf4fThdwTWSsZGgvGAG6yE+b/F5gWUkcZ/iQ==} + engines: {node: '>=10'} + peerDependencies: + '@svgr/core': '*' + dependencies: + '@svgr/core': 6.5.1 + cosmiconfig: 7.1.0 + deepmerge: 4.3.1 + svgo: 2.8.0 + dev: false + + /@svgr/webpack@6.5.1: + resolution: {integrity: sha512-cQ/AsnBkXPkEK8cLbv4Dm7JGXq2XrumKnL1dRpJD9rIO2fTIlJI9a1uCciYG1F2aUsox/hJQyNGbt3soDxSRkA==} + engines: {node: '>=10'} + dependencies: + '@babel/core': 7.23.5 + '@babel/plugin-transform-react-constant-elements': 7.22.5(@babel/core@7.23.5) + '@babel/preset-env': 7.23.5(@babel/core@7.23.5) + '@babel/preset-react': 7.22.15(@babel/core@7.23.5) + '@babel/preset-typescript': 7.22.15(@babel/core@7.23.5) + '@svgr/core': 6.5.1 + '@svgr/plugin-jsx': 6.5.1(@svgr/core@6.5.1) + '@svgr/plugin-svgo': 6.5.1(@svgr/core@6.5.1) + transitivePeerDependencies: + - supports-color + dev: false + + /@szmarczak/http-timer@5.0.1: + resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} + engines: {node: '>=14.16'} + dependencies: + defer-to-connect: 2.0.1 + dev: false + + /@trysound/sax@0.2.0: + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + dev: false + + /@types/acorn@4.0.6: + resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} + dependencies: + '@types/estree': 1.0.1 + + /@types/body-parser@1.19.2: + resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} + dependencies: + '@types/connect': 3.4.36 + '@types/node': 20.5.9 + dev: false + + /@types/bonjour@3.5.10: + resolution: {integrity: sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==} + dependencies: + '@types/node': 20.5.9 + dev: false + + /@types/connect-history-api-fallback@1.5.1: + resolution: {integrity: sha512-iaQslNbARe8fctL5Lk+DsmgWOM83lM+7FzP0eQUJs1jd3kBE8NWqBTIT2S8SqQOJjxvt2eyIjpOuYeRXq2AdMw==} + dependencies: + '@types/express-serve-static-core': 4.17.36 + '@types/node': 20.5.9 + dev: false + + /@types/connect@3.4.36: + resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} + dependencies: + '@types/node': 20.5.9 + dev: false + + /@types/debug@4.1.8: + resolution: {integrity: sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==} + dependencies: + '@types/ms': 0.7.31 + + /@types/eslint-scope@3.7.4: + resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} + dependencies: + '@types/eslint': 8.44.2 + '@types/estree': 1.0.1 + + /@types/eslint@8.44.2: + resolution: {integrity: sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==} + dependencies: + '@types/estree': 1.0.1 + '@types/json-schema': 7.0.12 + + /@types/estree-jsx@1.0.0: + resolution: {integrity: sha512-3qvGd0z8F2ENTGr/GG1yViqfiKmRfrXVx5sJyHGFu3z7m5g5utCQtGp/g29JnjflhtQJBv1WDQukHiT58xPcYQ==} + dependencies: + '@types/estree': 1.0.1 + + /@types/estree@0.0.39: + resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} + dev: false + + /@types/estree@1.0.1: + resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} + + /@types/express-serve-static-core@4.17.36: + resolution: {integrity: sha512-zbivROJ0ZqLAtMzgzIUC4oNqDG9iF0lSsAqpOD9kbs5xcIM3dTiyuHvBc7R8MtWBp3AAWGaovJa+wzWPjLYW7Q==} + dependencies: + '@types/node': 20.5.9 + '@types/qs': 6.9.8 + '@types/range-parser': 1.2.4 + '@types/send': 0.17.1 + dev: false + + /@types/express@4.17.17: + resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==} + dependencies: + '@types/body-parser': 1.19.2 + '@types/express-serve-static-core': 4.17.36 + '@types/qs': 6.9.8 + '@types/serve-static': 1.15.2 + dev: false + + /@types/gtag.js@0.0.12: + resolution: {integrity: sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg==} + dev: false + + /@types/hast@3.0.2: + resolution: {integrity: sha512-B5hZHgHsXvfCoO3xgNJvBnX7N8p86TqQeGKXcokW4XXi+qY4vxxPSFYofytvVmpFxzPv7oxDQzjg5Un5m2/xiw==} + dependencies: + '@types/unist': 3.0.1 + + /@types/history@4.7.11: + resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} + + /@types/html-minifier-terser@6.1.0: + resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} + dev: false + + /@types/http-cache-semantics@4.0.1: + resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==} + dev: false + + /@types/http-errors@2.0.1: + resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==} + dev: false + + /@types/http-proxy@1.17.11: + resolution: {integrity: sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA==} + dependencies: + '@types/node': 20.5.9 + dev: false + + /@types/istanbul-lib-coverage@2.0.4: + resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} + dev: false + + /@types/istanbul-lib-report@3.0.0: + resolution: {integrity: sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==} + dependencies: + '@types/istanbul-lib-coverage': 2.0.4 + dev: false + + /@types/istanbul-reports@3.0.1: + resolution: {integrity: sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==} + dependencies: + '@types/istanbul-lib-report': 3.0.0 + dev: false + + /@types/json-schema@7.0.12: + resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} + + /@types/mdast@4.0.2: + resolution: {integrity: sha512-tYR83EignvhYO9iU3kDg8V28M0jqyh9zzp5GV+EO+AYnyUl3P5ltkTeJuTiFZQFz670FSb3EwT/6LQdX+UdKfw==} + dependencies: + '@types/unist': 3.0.1 + + /@types/mdx@2.0.7: + resolution: {integrity: sha512-BG4tyr+4amr3WsSEmHn/fXPqaCba/AYZ7dsaQTiavihQunHSIxk+uAtqsjvicNpyHN6cm+B9RVrUOtW9VzIKHw==} + + /@types/mime@1.3.2: + resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==} + dev: false + + /@types/mime@3.0.1: + resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} + dev: false + + /@types/minimist@1.2.2: + resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} + dev: true + + /@types/ms@0.7.31: + resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} + + /@types/node@17.0.45: + resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} + dev: false + + /@types/node@20.5.9: + resolution: {integrity: sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ==} + + /@types/normalize-package-data@2.4.1: + resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} + dev: true + + /@types/parse-json@4.0.0: + resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} + dev: false + + /@types/prismjs@1.26.2: + resolution: {integrity: sha512-/r7Cp7iUIk7gts26mHXD66geUC+2Fo26TZYjQK6Nr4LDfi6lmdRmMqM0oPwfiMhUwoBAOFe8GstKi2pf6hZvwA==} + dev: false + + /@types/prop-types@15.7.5: + resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} + + /@types/qs@6.9.8: + resolution: {integrity: sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==} + dev: false + + /@types/range-parser@1.2.4: + resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} + dev: false + + /@types/react-router-config@5.0.7: + resolution: {integrity: sha512-pFFVXUIydHlcJP6wJm7sDii5mD/bCmmAY0wQzq+M+uX7bqS95AQqHZWP1iNMKrWVQSuHIzj5qi9BvrtLX2/T4w==} + dependencies: + '@types/history': 4.7.11 + '@types/react': 18.2.21 + '@types/react-router': 5.1.20 + + /@types/react-router-dom@5.3.3: + resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} + dependencies: + '@types/history': 4.7.11 + '@types/react': 18.2.21 + '@types/react-router': 5.1.20 + + /@types/react-router@5.1.20: + resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} + dependencies: + '@types/history': 4.7.11 + '@types/react': 18.2.21 + + /@types/react@18.2.21: + resolution: {integrity: sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==} + dependencies: + '@types/prop-types': 15.7.5 + '@types/scheduler': 0.16.3 + csstype: 3.1.2 + + /@types/resolve@1.17.1: + resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} + dependencies: + '@types/node': 20.5.9 + dev: false + + /@types/retry@0.12.0: + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + dev: false + + /@types/sax@1.2.6: + resolution: {integrity: sha512-A1mpYCYu1aHFayy8XKN57ebXeAbh9oQIZ1wXcno6b1ESUAfMBDMx7mf/QGlYwcMRaFryh9YBuH03i/3FlPGDkQ==} + dependencies: + '@types/node': 20.5.9 + dev: false + + /@types/scheduler@0.16.3: + resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} + + /@types/semver@7.5.1: + resolution: {integrity: sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg==} + dev: true + + /@types/send@0.17.1: + resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==} + dependencies: + '@types/mime': 1.3.2 + '@types/node': 20.5.9 + dev: false + + /@types/serve-index@1.9.1: + resolution: {integrity: sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==} + dependencies: + '@types/express': 4.17.17 + dev: false + + /@types/serve-static@1.15.2: + resolution: {integrity: sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==} + dependencies: + '@types/http-errors': 2.0.1 + '@types/mime': 3.0.1 + '@types/node': 20.5.9 + dev: false + + /@types/sockjs@0.3.33: + resolution: {integrity: sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==} + dependencies: + '@types/node': 20.5.9 + dev: false + + /@types/trusted-types@2.0.3: + resolution: {integrity: sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==} + dev: false + + /@types/unist@2.0.8: + resolution: {integrity: sha512-d0XxK3YTObnWVp6rZuev3c49+j4Lo8g4L1ZRm9z5L0xpoZycUPshHgczK5gsUMaZOstjVYYi09p5gYvUtfChYw==} + + /@types/unist@3.0.1: + resolution: {integrity: sha512-ue/hDUpPjC85m+PM9OQDMZr3LywT+CT6mPsQq8OJtCLiERkGRcQUFvu9XASF5XWqyZFXbf15lvb3JFJ4dRLWPg==} + + /@types/ws@8.5.5: + resolution: {integrity: sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==} + dependencies: + '@types/node': 20.5.9 + dev: false + + /@types/yargs-parser@21.0.0: + resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} + dev: false + + /@types/yargs@17.0.24: + resolution: {integrity: sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==} + dependencies: + '@types/yargs-parser': 21.0.0 + dev: false + + /@typescript-eslint/eslint-plugin@6.11.0(@typescript-eslint/parser@6.11.0)(eslint@8.53.0)(typescript@5.3.3): + resolution: {integrity: sha512-uXnpZDc4VRjY4iuypDBKzW1rz9T5YBBK0snMn8MaTSNd2kMlj50LnLBABELjJiOL5YHk7ZD8hbSpI9ubzqYI0w==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.8.0 + '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 6.11.0 + '@typescript-eslint/type-utils': 6.11.0(eslint@8.53.0)(typescript@5.3.3) + '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.11.0 + debug: 4.3.4 + eslint: 8.53.0 + graphemer: 1.4.0 + ignore: 5.2.4 + natural-compare: 1.4.0 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3): + resolution: {integrity: sha512-+whEdjk+d5do5nxfxx73oanLL9ghKO3EwM9kBCkUtWMRwWuPaFv9ScuqlYfQ6pAD6ZiJhky7TZ2ZYhrMsfMxVQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.11.0 + '@typescript-eslint/types': 6.11.0 + '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.11.0 + debug: 4.3.4 + eslint: 8.53.0 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@5.62.0: + resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + dev: true + + /@typescript-eslint/scope-manager@6.11.0: + resolution: {integrity: sha512-0A8KoVvIURG4uhxAdjSaxy8RdRE//HztaZdG8KiHLP8WOXSk0vlF7Pvogv+vlJA5Rnjj/wDcFENvDaHb+gKd1A==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.11.0 + '@typescript-eslint/visitor-keys': 6.11.0 + dev: true + + /@typescript-eslint/type-utils@6.11.0(eslint@8.53.0)(typescript@5.3.3): + resolution: {integrity: sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.3.3) + '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.3.3) + debug: 4.3.4 + eslint: 8.53.0 + ts-api-utils: 1.0.3(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types@5.62.0: + resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@typescript-eslint/types@6.11.0: + resolution: {integrity: sha512-ZbEzuD4DwEJxwPqhv3QULlRj8KYTAnNsXxmfuUXFCxZmO6CF2gM/y+ugBSAQhrqaJL3M+oe4owdWunaHM6beqA==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + + /@typescript-eslint/typescript-estree@5.62.0(typescript@5.3.3): + resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + tsutils: 3.21.0(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/typescript-estree@6.11.0(typescript@5.3.3): + resolution: {integrity: sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.11.0 + '@typescript-eslint/visitor-keys': 6.11.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@5.62.0(eslint@8.53.0)(typescript@5.3.3): + resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) + '@types/json-schema': 7.0.12 + '@types/semver': 7.5.1 + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.3.3) + eslint: 8.53.0 + eslint-scope: 5.1.1 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/utils@6.11.0(eslint@8.53.0)(typescript@5.3.3): + resolution: {integrity: sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) + '@types/json-schema': 7.0.12 + '@types/semver': 7.5.1 + '@typescript-eslint/scope-manager': 6.11.0 + '@typescript-eslint/types': 6.11.0 + '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.3.3) + eslint: 8.53.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys@5.62.0: + resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.62.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@typescript-eslint/visitor-keys@6.11.0: + resolution: {integrity: sha512-+SUN/W7WjBr05uRxPggJPSzyB8zUpaYo2hByKasWbqr3PM8AXfZt8UHdNpBS1v9SA62qnSSMF3380SwDqqprgQ==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.11.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@ungap/structured-clone@1.2.0: + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + + /@vercel/analytics@1.1.1: + resolution: {integrity: sha512-+NqgNmSabg3IFfxYhrWCfB/H+RCUOCR5ExRudNG2+pcRehq628DJB5e1u1xqwpLtn4pAYii4D98w7kofORAGQA==} + dependencies: + server-only: 0.0.1 + dev: false + + /@webassemblyjs/ast@1.11.6: + resolution: {integrity: sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==} + dependencies: + '@webassemblyjs/helper-numbers': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + + /@webassemblyjs/floating-point-hex-parser@1.11.6: + resolution: {integrity: sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==} + + /@webassemblyjs/helper-api-error@1.11.6: + resolution: {integrity: sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==} + + /@webassemblyjs/helper-buffer@1.11.6: + resolution: {integrity: sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==} + + /@webassemblyjs/helper-numbers@1.11.6: + resolution: {integrity: sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==} + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.11.6 + '@webassemblyjs/helper-api-error': 1.11.6 + '@xtuc/long': 4.2.2 + + /@webassemblyjs/helper-wasm-bytecode@1.11.6: + resolution: {integrity: sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==} + + /@webassemblyjs/helper-wasm-section@1.11.6: + resolution: {integrity: sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-buffer': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/wasm-gen': 1.11.6 + + /@webassemblyjs/ieee754@1.11.6: + resolution: {integrity: sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==} + dependencies: + '@xtuc/ieee754': 1.2.0 + + /@webassemblyjs/leb128@1.11.6: + resolution: {integrity: sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==} + dependencies: + '@xtuc/long': 4.2.2 + + /@webassemblyjs/utf8@1.11.6: + resolution: {integrity: sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==} + + /@webassemblyjs/wasm-edit@1.11.6: + resolution: {integrity: sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-buffer': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/helper-wasm-section': 1.11.6 + '@webassemblyjs/wasm-gen': 1.11.6 + '@webassemblyjs/wasm-opt': 1.11.6 + '@webassemblyjs/wasm-parser': 1.11.6 + '@webassemblyjs/wast-printer': 1.11.6 + + /@webassemblyjs/wasm-gen@1.11.6: + resolution: {integrity: sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/ieee754': 1.11.6 + '@webassemblyjs/leb128': 1.11.6 + '@webassemblyjs/utf8': 1.11.6 + + /@webassemblyjs/wasm-opt@1.11.6: + resolution: {integrity: sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-buffer': 1.11.6 + '@webassemblyjs/wasm-gen': 1.11.6 + '@webassemblyjs/wasm-parser': 1.11.6 + + /@webassemblyjs/wasm-parser@1.11.6: + resolution: {integrity: sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-api-error': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/ieee754': 1.11.6 + '@webassemblyjs/leb128': 1.11.6 + '@webassemblyjs/utf8': 1.11.6 + + /@webassemblyjs/wast-printer@1.11.6: + resolution: {integrity: sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@xtuc/long': 4.2.2 + + /@xtuc/ieee754@1.2.0: + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + /@xtuc/long@4.2.2: + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + dev: false + + /acorn-import-assertions@1.9.0(acorn@8.10.0): + resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + peerDependencies: + acorn: ^8 + dependencies: + acorn: 8.10.0 + + /acorn-jsx@5.3.2(acorn@8.10.0): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.10.0 + + /acorn-walk@8.2.0: + resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} + engines: {node: '>=0.4.0'} + dev: false + + /acorn@8.10.0: + resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} + engines: {node: '>=0.4.0'} + hasBin: true + + /address@1.2.2: + resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} + engines: {node: '>= 10.0.0'} + 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: false + + /ajv-formats@2.1.1(ajv@8.12.0): + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + dependencies: + ajv: 8.12.0 + dev: false + + /ajv-keywords@3.5.2(ajv@6.12.6): + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + dependencies: + ajv: 6.12.6 + + /ajv-keywords@5.1.0(ajv@8.12.0): + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + dependencies: + ajv: 8.12.0 + fast-deep-equal: 3.1.3 + dev: false + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + /ajv@8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + + /algoliasearch-helper@3.14.0(algoliasearch@4.19.1): + resolution: {integrity: sha512-gXDXzsSS0YANn5dHr71CUXOo84cN4azhHKUbg71vAWnH+1JBiR4jf7to3t3JHXknXkbV0F7f055vUSBKrltHLQ==} + peerDependencies: + algoliasearch: '>= 3.1 < 6' + dependencies: + '@algolia/events': 4.0.1 + algoliasearch: 4.19.1 + dev: false + + /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: false + + /ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + dependencies: + string-width: 4.2.3 + dev: false + + /ansi-html-community@0.0.8: + resolution: {integrity: sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==} + engines: {'0': node >= 0.8.0} + hasBin: true + dev: false + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: false + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: false + + /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: false + + /arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: false + + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + dev: false + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + /array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.2 + is-array-buffer: 3.0.2 + dev: false + + /array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + dev: false + + /array-flatten@2.1.2: + resolution: {integrity: sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==} + dev: false + + /array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + /arraybuffer.prototype.slice@1.0.2: + resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + get-intrinsic: 1.2.1 + is-array-buffer: 3.0.2 + is-shared-array-buffer: 1.0.2 + dev: false + + /arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + dev: true + + /astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + dev: true + + /astring@1.8.6: + resolution: {integrity: sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==} + hasBin: true + + /async@3.2.4: + resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} + dev: false + + /at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + dev: false + + /autoprefixer@10.4.15(postcss@8.4.29): + resolution: {integrity: sha512-KCuPB8ZCIqFdA4HwKXsvz7j6gvSDNhDP7WnUjBleRkKjPdvCmHFuQ77ocavI8FT6NdvlBnE2UFr2H4Mycn8Vew==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + dependencies: + browserslist: 4.22.2 + caniuse-lite: 1.0.30001565 + fraction.js: 4.3.6 + normalize-range: 0.1.2 + picocolors: 1.0.0 + postcss: 8.4.29 + postcss-value-parser: 4.2.0 + dev: false + + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: false + + /b4a@1.6.4: + resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==} + dev: false + + /babel-loader@9.1.3(@babel/core@7.23.5)(webpack@5.88.2): + resolution: {integrity: sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==} + engines: {node: '>= 14.15.0'} + peerDependencies: + '@babel/core': ^7.12.0 + webpack: '>=5' + dependencies: + '@babel/core': 7.23.5 + find-cache-dir: 4.0.0 + schema-utils: 4.2.0 + webpack: 5.88.2 + dev: false + + /babel-plugin-dynamic-import-node@2.3.3: + resolution: {integrity: sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==} + dependencies: + object.assign: 4.1.4 + dev: false + + /babel-plugin-polyfill-corejs2@0.4.6(@babel/core@7.23.5): + resolution: {integrity: sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/compat-data': 7.23.5 + '@babel/core': 7.23.5 + '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.23.5) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: false + + /babel-plugin-polyfill-corejs3@0.8.6(@babel/core@7.23.5): + resolution: {integrity: sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.23.5) + core-js-compat: 3.33.3 + transitivePeerDependencies: + - supports-color + dev: false + + /babel-plugin-polyfill-regenerator@0.5.3(@babel/core@7.23.5): + resolution: {integrity: sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.23.5 + '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.23.5) + transitivePeerDependencies: + - supports-color + dev: false + + /bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + /balanced-match@2.0.0: + resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} + dev: true + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: false + + /batch@0.6.1: + resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} + dev: false + + /big-integer@1.6.51: + resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==} + engines: {node: '>=0.6'} + dev: true + + /big.js@5.2.2: + resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} + dev: false + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: false + + /bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: false + + /bl@5.1.0: + resolution: {integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==} + dependencies: + buffer: 6.0.3 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: false + + /body-parser@1.20.1: + resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.1 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /bonjour-service@1.1.1: + resolution: {integrity: sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==} + dependencies: + array-flatten: 2.1.2 + dns-equal: 1.0.0 + fast-deep-equal: 3.1.3 + multicast-dns: 7.2.5 + dev: false + + /boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + dev: false + + /boxen@6.2.1: + resolution: {integrity: sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + ansi-align: 3.0.1 + camelcase: 6.3.0 + chalk: 4.1.2 + cli-boxes: 3.0.0 + string-width: 5.1.2 + type-fest: 2.19.0 + widest-line: 4.0.1 + wrap-ansi: 8.1.0 + dev: false + + /boxen@7.1.1: + resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} + engines: {node: '>=14.16'} + dependencies: + ansi-align: 3.0.1 + camelcase: 7.0.1 + chalk: 5.3.0 + cli-boxes: 3.0.0 + string-width: 5.1.2 + type-fest: 2.19.0 + widest-line: 4.0.1 + wrap-ansi: 8.1.0 + dev: false + + /bplist-parser@0.2.0: + resolution: {integrity: sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==} + engines: {node: '>= 5.10.0'} + dependencies: + big-integer: 1.6.51 + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: false + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + + /browserslist@4.22.2: + resolution: {integrity: sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001565 + electron-to-chromium: 1.4.601 + node-releases: 2.0.14 + update-browserslist-db: 1.0.13(browserslist@4.22.2) + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + /buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + + /buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + + /builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + dev: false + + /bundle-name@3.0.0: + resolution: {integrity: sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==} + engines: {node: '>=12'} + dependencies: + run-applescript: 5.0.0 + dev: true + + /bytes@3.0.0: + resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} + engines: {node: '>= 0.8'} + dev: false + + /bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + dev: false + + /cacheable-lookup@7.0.0: + resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} + engines: {node: '>=14.16'} + dev: false + + /cacheable-request@10.2.13: + resolution: {integrity: sha512-3SD4rrMu1msNGEtNSt8Od6enwdo//U9s4ykmXfA2TD58kcLkCobtCDiby7kNyj7a/Q7lz/mAesAFI54rTdnvBA==} + engines: {node: '>=14.16'} + dependencies: + '@types/http-cache-semantics': 4.0.1 + get-stream: 6.0.1 + http-cache-semantics: 4.1.1 + keyv: 4.5.3 + mimic-response: 4.0.0 + normalize-url: 8.0.0 + responselike: 3.0.0 + dev: false + + /call-bind@1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.2.1 + dev: false + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + /camel-case@4.1.2: + resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + dependencies: + pascal-case: 3.1.2 + tslib: 2.6.2 + dev: false + + /camelcase-keys@7.0.2: + resolution: {integrity: sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==} + engines: {node: '>=12'} + dependencies: + camelcase: 6.3.0 + map-obj: 4.3.0 + quick-lru: 5.1.1 + type-fest: 1.4.0 + dev: true + + /camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + /camelcase@7.0.1: + resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} + engines: {node: '>=14.16'} + dev: false + + /caniuse-api@3.0.0: + resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} + dependencies: + browserslist: 4.22.2 + caniuse-lite: 1.0.30001565 + lodash.memoize: 4.1.2 + lodash.uniq: 4.5.0 + dev: false + + /caniuse-lite@1.0.30001565: + resolution: {integrity: sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==} + + /ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + /chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + dev: false + + /char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + dev: false + + /character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + /character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + /character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + /character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + /cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + dependencies: + boolbase: 1.0.0 + css-select: 5.1.0 + css-what: 6.1.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + dev: false + + /cheerio@1.0.0-rc.12: + resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} + engines: {node: '>= 6'} + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.1.0 + htmlparser2: 8.0.2 + parse5: 7.1.2 + parse5-htmlparser2-tree-adapter: 7.0.0 + dev: false + + /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.3 + dev: false + + /chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + dev: false + + /chrome-trace-event@1.0.3: + resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} + engines: {node: '>=6.0'} + + /ci-info@3.8.0: + resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} + engines: {node: '>=8'} + dev: false + + /clean-css@5.3.2: + resolution: {integrity: sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==} + engines: {node: '>= 10.0'} + dependencies: + source-map: 0.6.1 + dev: false + + /clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + dev: false + + /cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + dev: false + + /cli-cursor@4.0.0: + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + restore-cursor: 4.0.0 + dev: false + + /cli-spinners@2.9.0: + resolution: {integrity: sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==} + engines: {node: '>=6'} + dev: false + + /cli-table3@0.6.3: + resolution: {integrity: sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==} + engines: {node: 10.* || >= 12.*} + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + dev: false + + /clone-deep@4.0.1: + resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} + engines: {node: '>=6'} + dependencies: + is-plain-object: 2.0.4 + kind-of: 6.0.3 + shallow-clone: 3.0.1 + + /clsx@2.0.0: + resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} + engines: {node: '>=6'} + dev: false + + /collapse-white-space@2.1.0: + resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + /color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + dev: false + + /color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + dev: false + + /colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + /colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + dev: false + + /combine-promises@1.2.0: + resolution: {integrity: sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ==} + engines: {node: '>=10'} + dev: false + + /comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + /commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + dev: false + + /commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + /commander@5.1.0: + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} + engines: {node: '>= 6'} + + /commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + dev: false + + /commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + dev: false + + /common-path-prefix@3.0.0: + resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} + dev: false + + /common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + dev: false + + /compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + + /compression@1.7.4: + resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==} + engines: {node: '>= 0.8.0'} + dependencies: + accepts: 1.3.8 + bytes: 3.0.0 + compressible: 2.0.18 + debug: 2.6.9 + on-headers: 1.0.2 + safe-buffer: 5.1.2 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + /config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + dev: false + + /configstore@6.0.0: + resolution: {integrity: sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==} + engines: {node: '>=12'} + dependencies: + dot-prop: 6.0.1 + graceful-fs: 4.2.11 + unique-string: 3.0.0 + write-file-atomic: 3.0.3 + xdg-basedir: 5.1.0 + dev: false + + /connect-history-api-fallback@2.0.0: + resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} + engines: {node: '>=0.8'} + dev: false + + /consola@2.15.3: + resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} + dev: false + + /consolidated-events@2.0.2: + resolution: {integrity: sha512-2/uRVMdRypf5z/TW/ncD/66l75P5hH2vM/GR8Jf8HLc2xnfJtmina6F6du8+v4Z2vTrMo7jC+W1tmEEuuELgkQ==} + dev: false + + /content-disposition@0.5.2: + resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==} + engines: {node: '>= 0.6'} + dev: false + + /content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + dev: false + + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: false + + /cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + dev: false + + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: false + + /copy-text-to-clipboard@3.2.0: + resolution: {integrity: sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q==} + engines: {node: '>=12'} + dev: false + + /copy-webpack-plugin@11.0.0(webpack@5.88.2): + resolution: {integrity: sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==} + engines: {node: '>= 14.15.0'} + peerDependencies: + webpack: ^5.1.0 + dependencies: + fast-glob: 3.3.1 + glob-parent: 6.0.2 + globby: 13.2.2 + normalize-path: 3.0.0 + schema-utils: 4.2.0 + serialize-javascript: 6.0.1 + webpack: 5.88.2 + dev: false + + /core-js-compat@3.33.3: + resolution: {integrity: sha512-cNzGqFsh3Ot+529GIXacjTJ7kegdt5fPXxCBVS1G0iaZpuo/tBz399ymceLJveQhFFZ8qThHiP3fzuoQjKN2ow==} + dependencies: + browserslist: 4.22.2 + dev: false + + /core-js-pure@3.32.1: + resolution: {integrity: sha512-f52QZwkFVDPf7UEQZGHKx6NYxsxmVGJe5DIvbzOdRMJlmT6yv0KDjR8rmy3ngr/t5wU54c7Sp/qIJH0ppbhVpQ==} + requiresBuild: true + dev: false + + /core-js@3.32.1: + resolution: {integrity: sha512-lqufgNn9NLnESg5mQeYsxQP5w7wrViSj0jr/kv6ECQiByzQkrn1MKvV0L3acttpDqfQrHLwr2KCMgX5b8X+lyQ==} + requiresBuild: true + dev: false + + /core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + dev: false + + /cosmiconfig@6.0.0: + resolution: {integrity: sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==} + engines: {node: '>=8'} + dependencies: + '@types/parse-json': 4.0.0 + import-fresh: 3.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + dev: false + + /cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + dependencies: + '@types/parse-json': 4.0.0 + import-fresh: 3.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + dev: false + + /cosmiconfig@8.3.4(typescript@5.3.3): + resolution: {integrity: sha512-SF+2P8+o/PTV05rgsAjDzL4OFdVXAulSfC/L19VaeVT7+tpOOSscCt2QLxDZ+CLxF2WOiq6y1K5asvs8qUJT/Q==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + typescript: 5.3.3 + + /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 + + /crypto-random-string@2.0.0: + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + engines: {node: '>=8'} + dev: false + + /crypto-random-string@4.0.0: + resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} + engines: {node: '>=12'} + dependencies: + type-fest: 1.4.0 + dev: false + + /css-declaration-sorter@6.4.1(postcss@8.4.29): + resolution: {integrity: sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==} + engines: {node: ^10 || ^12 || >=14} + peerDependencies: + postcss: ^8.0.9 + dependencies: + postcss: 8.4.29 + dev: false + + /css-functions-list@3.2.1: + resolution: {integrity: sha512-Nj5YcaGgBtuUmn1D7oHqPW0c9iui7xsTsj5lIX8ZgevdfhmjFfKB3r8moHJtNJnctnYXJyYX5I1pp90HM4TPgQ==} + engines: {node: '>=12 || >=16'} + dev: true + + /css-loader@6.8.1(webpack@5.88.2): + resolution: {integrity: sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + dependencies: + icss-utils: 5.1.0(postcss@8.4.29) + postcss: 8.4.29 + postcss-modules-extract-imports: 3.0.0(postcss@8.4.29) + postcss-modules-local-by-default: 4.0.3(postcss@8.4.29) + postcss-modules-scope: 3.0.0(postcss@8.4.29) + postcss-modules-values: 4.0.0(postcss@8.4.29) + postcss-value-parser: 4.2.0 + semver: 7.5.4 + webpack: 5.88.2 + dev: false + + /css-minimizer-webpack-plugin@4.2.2(clean-css@5.3.2)(webpack@5.88.2): + resolution: {integrity: sha512-s3Of/4jKfw1Hj9CxEO1E5oXhQAxlayuHO2y/ML+C6I9sQ7FdzfEV6QgMLN3vI+qFsjJGIAFLKtQK7t8BOXAIyA==} + engines: {node: '>= 14.15.0'} + peerDependencies: + '@parcel/css': '*' + '@swc/css': '*' + clean-css: '*' + csso: '*' + esbuild: '*' + lightningcss: '*' + webpack: ^5.0.0 + peerDependenciesMeta: + '@parcel/css': + optional: true + '@swc/css': + optional: true + clean-css: + optional: true + csso: + optional: true + esbuild: + optional: true + lightningcss: + optional: true + dependencies: + clean-css: 5.3.2 + cssnano: 5.1.15(postcss@8.4.29) + jest-worker: 29.6.4 + postcss: 8.4.29 + schema-utils: 4.2.0 + serialize-javascript: 6.0.1 + source-map: 0.6.1 + webpack: 5.88.2 + dev: false + + /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: false + + /css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + dev: false + + /css-tree@1.1.3: + resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} + engines: {node: '>=8.0.0'} + dependencies: + mdn-data: 2.0.14 + source-map: 0.6.1 + dev: false + + /css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.0.2 + dev: true + + /css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + dev: false + + /cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + /cssnano-preset-advanced@5.3.10(postcss@8.4.29): + resolution: {integrity: sha512-fnYJyCS9jgMU+cmHO1rPSPf9axbQyD7iUhLO5Df6O4G+fKIOMps+ZbU0PdGFejFBBZ3Pftf18fn1eG7MAPUSWQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + autoprefixer: 10.4.15(postcss@8.4.29) + cssnano-preset-default: 5.2.14(postcss@8.4.29) + postcss: 8.4.29 + postcss-discard-unused: 5.1.0(postcss@8.4.29) + postcss-merge-idents: 5.1.1(postcss@8.4.29) + postcss-reduce-idents: 5.2.0(postcss@8.4.29) + postcss-zindex: 5.1.0(postcss@8.4.29) + dev: false + + /cssnano-preset-default@5.2.14(postcss@8.4.29): + resolution: {integrity: sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + css-declaration-sorter: 6.4.1(postcss@8.4.29) + cssnano-utils: 3.1.0(postcss@8.4.29) + postcss: 8.4.29 + postcss-calc: 8.2.4(postcss@8.4.29) + postcss-colormin: 5.3.1(postcss@8.4.29) + postcss-convert-values: 5.1.3(postcss@8.4.29) + postcss-discard-comments: 5.1.2(postcss@8.4.29) + postcss-discard-duplicates: 5.1.0(postcss@8.4.29) + postcss-discard-empty: 5.1.1(postcss@8.4.29) + postcss-discard-overridden: 5.1.0(postcss@8.4.29) + postcss-merge-longhand: 5.1.7(postcss@8.4.29) + postcss-merge-rules: 5.1.4(postcss@8.4.29) + postcss-minify-font-values: 5.1.0(postcss@8.4.29) + postcss-minify-gradients: 5.1.1(postcss@8.4.29) + postcss-minify-params: 5.1.4(postcss@8.4.29) + postcss-minify-selectors: 5.2.1(postcss@8.4.29) + postcss-normalize-charset: 5.1.0(postcss@8.4.29) + postcss-normalize-display-values: 5.1.0(postcss@8.4.29) + postcss-normalize-positions: 5.1.1(postcss@8.4.29) + postcss-normalize-repeat-style: 5.1.1(postcss@8.4.29) + postcss-normalize-string: 5.1.0(postcss@8.4.29) + postcss-normalize-timing-functions: 5.1.0(postcss@8.4.29) + postcss-normalize-unicode: 5.1.1(postcss@8.4.29) + postcss-normalize-url: 5.1.0(postcss@8.4.29) + postcss-normalize-whitespace: 5.1.1(postcss@8.4.29) + postcss-ordered-values: 5.1.3(postcss@8.4.29) + postcss-reduce-initial: 5.1.2(postcss@8.4.29) + postcss-reduce-transforms: 5.1.0(postcss@8.4.29) + postcss-svgo: 5.1.0(postcss@8.4.29) + postcss-unique-selectors: 5.1.1(postcss@8.4.29) + dev: false + + /cssnano-utils@3.1.0(postcss@8.4.29): + resolution: {integrity: sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.29 + dev: false + + /cssnano@5.1.15(postcss@8.4.29): + resolution: {integrity: sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + cssnano-preset-default: 5.2.14(postcss@8.4.29) + lilconfig: 2.1.0 + postcss: 8.4.29 + yaml: 1.10.2 + dev: false + + /csso@4.2.0: + resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} + engines: {node: '>=8.0.0'} + dependencies: + css-tree: 1.1.3 + dev: false + + /csstype@3.1.2: + resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} + + /dayjs@1.11.10: + resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} + dev: false + + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + 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 + + /decamelize-keys@1.1.1: + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} + engines: {node: '>=0.10.0'} + dependencies: + decamelize: 1.2.0 + map-obj: 1.0.1 + dev: true + + /decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + dev: true + + /decamelize@5.0.1: + resolution: {integrity: sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==} + engines: {node: '>=10'} + dev: true + + /decode-named-character-reference@1.0.2: + resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + dependencies: + character-entities: 2.0.2 + + /decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dependencies: + mimic-response: 3.1.0 + dev: false + + /deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + dev: false + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + /deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + dev: false + + /default-browser-id@3.0.0: + resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==} + engines: {node: '>=12'} + dependencies: + bplist-parser: 0.2.0 + untildify: 4.0.0 + dev: true + + /default-browser@4.0.0: + resolution: {integrity: sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==} + engines: {node: '>=14.16'} + dependencies: + bundle-name: 3.0.0 + default-browser-id: 3.0.0 + execa: 7.2.0 + titleize: 3.0.0 + dev: true + + /default-gateway@6.0.3: + resolution: {integrity: sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==} + engines: {node: '>= 10'} + dependencies: + execa: 5.1.1 + dev: false + + /defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + dev: false + + /define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + dev: false + + /define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + dev: true + + /define-properties@1.2.0: + resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} + engines: {node: '>= 0.4'} + dependencies: + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + dev: false + + /del@6.1.1: + resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==} + engines: {node: '>=10'} + dependencies: + globby: 11.1.0 + graceful-fs: 4.2.11 + is-glob: 4.0.3 + is-path-cwd: 2.2.0 + is-path-inside: 3.0.3 + p-map: 4.0.0 + rimraf: 3.0.2 + slash: 3.0.0 + dev: false + + /depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + dev: false + + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dev: false + + /dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dev: false + + /detect-libc@2.0.2: + resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} + engines: {node: '>=8'} + dev: false + + /detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + dev: false + + /detect-port-alt@1.1.6: + resolution: {integrity: sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==} + engines: {node: '>= 4.2.1'} + hasBin: true + dependencies: + address: 1.2.2 + debug: 2.6.9 + transitivePeerDependencies: + - supports-color + dev: false + + /detect-port@1.5.1: + resolution: {integrity: sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ==} + hasBin: true + dependencies: + address: 1.2.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + + /devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dependencies: + dequal: 2.0.3 + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + + /dns-equal@1.0.0: + resolution: {integrity: sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==} + dev: false + + /dns-packet@5.6.1: + resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} + engines: {node: '>=6'} + dependencies: + '@leichtgewicht/ip-codec': 2.0.4 + dev: false + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + + /docusaurus-plugin-baidu-tongji@0.0.0-beta.4: + resolution: {integrity: sha512-LePs8OLhaeHJBeZlZDDgWDbsHKERQAt7F0ELZ77X8tG3xCOkSLmNq+KTsQ+QhpsLp8xtRtyOqGtYiEaQPFMdBw==} + dev: false + + /docusaurus-plugin-image-zoom@1.0.1(@docusaurus/theme-classic@3.1.0): + resolution: {integrity: sha512-96IpSKUx2RWy3db9aZ0s673OQo5DWgV9UVWouS+CPOSIVEdCWh6HKmWf6tB9rsoaiIF3oNn9keiyv6neEyKb1Q==} + peerDependencies: + '@docusaurus/theme-classic': '>=2.2.0' + dependencies: + '@docusaurus/theme-classic': 3.1.0(@types/react@18.2.21)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + medium-zoom: 1.0.8 + validate-peer-dependencies: 2.2.0 + dev: false + + /docusaurus-plugin-sass@0.2.5(@docusaurus/core@3.1.0)(sass@1.66.1)(webpack@5.88.2): + resolution: {integrity: sha512-Z+D0fLFUKcFpM+bqSUmqKIU+vO+YF1xoEQh5hoFreg2eMf722+siwXDD+sqtwU8E4MvVpuvsQfaHwODNlxJAEg==} + peerDependencies: + '@docusaurus/core': ^2.0.0-beta || ^3.0.0-alpha + sass: ^1.30.0 + dependencies: + '@docusaurus/core': 3.1.0(@docusaurus/types@3.1.0)(eslint@8.53.0)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + sass: 1.66.1 + sass-loader: 10.4.1(sass@1.66.1)(webpack@5.88.2) + transitivePeerDependencies: + - fibers + - node-sass + - webpack + dev: false + + /dom-converter@0.2.0: + resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} + dependencies: + utila: 0.4.0 + dev: false + + /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: false + + /dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + dev: false + + /domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: false + + /domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: false + + /domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: false + + /domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + dev: false + + /domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dev: false + + /dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + dependencies: + no-case: 3.0.4 + tslib: 2.6.2 + dev: false + + /dot-prop@6.0.1: + resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} + engines: {node: '>=10'} + dependencies: + is-obj: 2.0.0 + dev: false + + /duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + dev: false + + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: false + + /ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + dev: false + + /ejs@3.1.9: + resolution: {integrity: sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + jake: 10.8.7 + dev: false + + /electron-to-chromium@1.4.601: + resolution: {integrity: sha512-SpwUMDWe9tQu8JX5QCO1+p/hChAi9AE9UpoC3rcHVc+gdCGlbT3SGb5I1klgb952HRIyvt9wZhSz9bNBYz9swA==} + + /emoji-regex@10.3.0: + resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} + dev: false + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: false + + /emojilib@2.4.0: + resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} + dev: false + + /emojis-list@3.0.0: + resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} + engines: {node: '>= 4'} + dev: false + + /emoticon@4.0.1: + resolution: {integrity: sha512-dqx7eA9YaqyvYtUhJwT4rC1HIp82j5ybS1/vQ42ur+jBe17dJMwZE4+gvL1XadSFfxaPFFGt3Xsw+Y8akThDlw==} + dev: false + + /encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + dev: false + + /end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + dev: false + + /enhanced-resolve@5.15.0: + resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} + engines: {node: '>=10.13.0'} + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + + /entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + dev: false + + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: false + + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + + /es-abstract@1.22.1: + resolution: {integrity: sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + arraybuffer.prototype.slice: 1.0.2 + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + es-set-tostringtag: 2.0.1 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.1 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has: 1.0.3 + has-property-descriptors: 1.0.0 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.12 + is-weakref: 1.0.2 + object-inspect: 1.12.3 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.0 + safe-array-concat: 1.0.1 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.7 + string.prototype.trimend: 1.0.6 + string.prototype.trimstart: 1.0.7 + typed-array-buffer: 1.0.0 + typed-array-byte-length: 1.0.0 + typed-array-byte-offset: 1.0.0 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.11 + dev: false + + /es-module-lexer@1.3.0: + resolution: {integrity: sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==} + + /es-set-tostringtag@2.0.1: + resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + has-tostringtag: 1.0.0 + dev: false + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: false + + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + + /escape-goat@4.0.0: + resolution: {integrity: sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==} + engines: {node: '>=12'} + dev: false + + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: false + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + /escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + dev: false + + /eslint-config-prettier@9.0.0(eslint@8.53.0): + resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.53.0 + dev: true + + /eslint-plugin-prettier@5.0.1(eslint-config-prettier@9.0.0)(eslint@8.53.0)(prettier@3.1.0): + resolution: {integrity: sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '*' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + dependencies: + eslint: 8.53.0 + eslint-config-prettier: 9.0.0(eslint@8.53.0) + prettier: 3.1.0 + prettier-linter-helpers: 1.0.0 + synckit: 0.8.5 + dev: true + + /eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + /eslint@8.53.0: + resolution: {integrity: sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) + '@eslint-community/regexpp': 4.8.0 + '@eslint/eslintrc': 2.1.3 + '@eslint/js': 8.53.0 + '@humanwhocodes/config-array': 0.11.13 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.21.0 + graphemer: 1.4.0 + ignore: 5.2.4 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.10.0 + acorn-jsx: 5.3.2(acorn@8.10.0) + eslint-visitor-keys: 3.4.3 + + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + + /estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + /estree-util-attach-comments@3.0.0: + resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} + dependencies: + '@types/estree': 1.0.1 + + /estree-util-build-jsx@3.0.1: + resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==} + dependencies: + '@types/estree-jsx': 1.0.0 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-walker: 3.0.3 + + /estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + /estree-util-to-js@2.0.0: + resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} + dependencies: + '@types/estree-jsx': 1.0.0 + astring: 1.8.6 + source-map: 0.7.4 + + /estree-util-value-to-estree@3.0.1: + resolution: {integrity: sha512-b2tdzTurEIbwRh+mKrEcaWfu1wgb8J1hVsgREg7FFiecWwK/PhO8X0kyc+0bIcKNtD4sqxIdNoRy6/p/TvECEA==} + engines: {node: '>=16.0.0'} + dependencies: + '@types/estree': 1.0.1 + is-plain-obj: 4.1.0 + dev: false + + /estree-util-visit@2.0.0: + resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} + dependencies: + '@types/estree-jsx': 1.0.0 + '@types/unist': 3.0.1 + + /estree-walker@1.0.1: + resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} + dev: false + + /estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + dependencies: + '@types/estree': 1.0.1 + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + /eta@2.2.0: + resolution: {integrity: sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==} + engines: {node: '>=6.0.0'} + dev: false + + /etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + dev: false + + /eval@0.1.8: + resolution: {integrity: sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==} + engines: {node: '>= 0.8'} + dependencies: + '@types/node': 20.5.9 + require-like: 0.1.2 + dev: false + + /eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + dev: false + + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + /execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + /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 + + /expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + dev: false + + /express@4.18.2: + resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.1 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.5.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.2.0 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: 2.0.7 + qs: 6.11.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0 + serve-static: 1.15.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + dependencies: + is-extendable: 0.1.1 + dev: false + + /extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + /fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + dev: true + + /fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + dev: false + + /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 + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + /fast-url-parser@1.1.3: + resolution: {integrity: sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==} + dependencies: + punycode: 1.4.1 + dev: false + + /fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + dev: true + + /fastq@1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + dependencies: + reusify: 1.0.4 + + /fault@2.0.1: + resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} + dependencies: + format: 0.2.2 + dev: false + + /faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + dependencies: + websocket-driver: 0.7.4 + dev: false + + /feed@4.2.2: + resolution: {integrity: sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==} + engines: {node: '>=0.4.0'} + dependencies: + xml-js: 1.6.11 + dev: false + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.1.0 + + /file-entry-cache@7.0.2: + resolution: {integrity: sha512-TfW7/1iI4Cy7Y8L6iqNdZQVvdXn0f8B4QcIXmkIbtTIe/Okm/nSlHb4IwGzRVOd3WfSieCgvf5cMzEfySAIl0g==} + engines: {node: '>=12.0.0'} + dependencies: + flat-cache: 3.2.0 + dev: true + + /file-loader@6.2.0(webpack@5.88.2): + resolution: {integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + dependencies: + loader-utils: 2.0.4 + schema-utils: 3.3.0 + webpack: 5.88.2 + dev: false + + /filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + dependencies: + minimatch: 5.1.6 + dev: false + + /filesize@8.0.7: + resolution: {integrity: sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==} + engines: {node: '>= 0.4.0'} + dev: false + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + + /finalhandler@1.2.0: + resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /find-cache-dir@4.0.0: + resolution: {integrity: sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==} + engines: {node: '>=14.16'} + dependencies: + common-path-prefix: 3.0.0 + pkg-dir: 7.0.0 + dev: false + + /find-up@3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + dependencies: + locate-path: 3.0.0 + dev: false + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + /find-up@6.3.0: + resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + dev: false + + /flat-cache@3.1.0: + resolution: {integrity: sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==} + engines: {node: '>=12.0.0'} + dependencies: + flatted: 3.2.7 + keyv: 4.5.3 + rimraf: 3.0.2 + + /flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.2.9 + keyv: 4.5.3 + rimraf: 3.0.2 + dev: true + + /flatted@3.2.7: + resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} + + /flatted@3.2.9: + resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + dev: true + + /follow-redirects@1.15.2: + resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: false + + /fork-ts-checker-webpack-plugin@6.5.3(eslint@8.53.0)(typescript@5.3.3)(webpack@5.88.2): + resolution: {integrity: sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==} + engines: {node: '>=10', yarn: '>=1.0.0'} + peerDependencies: + eslint: '>= 6' + typescript: '>= 2.7' + vue-template-compiler: '*' + webpack: '>= 4' + peerDependenciesMeta: + eslint: + optional: true + vue-template-compiler: + optional: true + dependencies: + '@babel/code-frame': 7.23.5 + '@types/json-schema': 7.0.12 + chalk: 4.1.2 + chokidar: 3.5.3 + cosmiconfig: 6.0.0 + deepmerge: 4.3.1 + eslint: 8.53.0 + fs-extra: 9.1.0 + glob: 7.2.3 + memfs: 3.5.3 + minimatch: 3.1.2 + schema-utils: 2.7.0 + semver: 7.5.4 + tapable: 1.1.3 + typescript: 5.3.3 + webpack: 5.88.2 + dev: false + + /form-data-encoder@2.1.4: + resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} + engines: {node: '>= 14.17'} + dev: false + + /format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + dev: false + + /forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + dev: false + + /fraction.js@4.3.6: + resolution: {integrity: sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==} + dev: false + + /framer-motion@10.16.4(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-p9V9nGomS3m6/CALXqv6nFGMuFOxbWsmaOrdmhyQimMIlLl3LC7h7l86wge/Js/8cRu5ktutS/zlzgR7eBOtFA==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tslib: 2.6.2 + optionalDependencies: + '@emotion/is-prop-valid': 0.8.8 + dev: false + + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + dev: false + + /fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + dev: false + + /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: false + + /fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: false + + /fs-monkey@1.0.4: + resolution: {integrity: sha512-INM/fWAxMICjttnD0DX1rBvinKskj5G1w+oy/pnm9u/tSlnBrzFonJMcalKJ30P8RRsPzKcCG7Q8l0jx5Fh9YQ==} + dev: false + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + functions-have-names: 1.2.3 + dev: false + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: false + + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + dev: false + + /get-intrinsic@1.2.1: + resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-proto: 1.0.1 + has-symbols: 1.0.3 + dev: false + + /get-own-enumerable-property-symbols@3.0.2: + resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} + dev: false + + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + /get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + dev: false + + /giscus@1.3.0: + resolution: {integrity: sha512-A3tVLgSmpnh2sX9uGjo9MbzmTTEJirSyFUPRvkipvy37y9rhxUYDoh9kO37QVrP7Sc7QuJ+gihB6apkO0yDyTw==} + dependencies: + lit: 2.8.0 + dev: false + + /github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + dev: false + + /github-slugger@1.5.0: + resolution: {integrity: sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==} + dev: false + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + + /glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + 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 + + /global-dirs@3.0.1: + resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} + engines: {node: '>=10'} + dependencies: + ini: 2.0.0 + dev: false + + /global-modules@2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + dependencies: + global-prefix: 3.0.0 + + /global-prefix@3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + dev: false + + /globals@13.21.0: + resolution: {integrity: sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.0 + dev: false + + /globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + 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 + + /globby@13.2.2: + resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + dir-glob: 3.0.1 + fast-glob: 3.3.1 + ignore: 5.2.4 + merge2: 1.4.1 + slash: 4.0.0 + dev: false + + /globjoin@0.1.4: + resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} + dev: true + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.1 + dev: false + + /got@12.6.1: + resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==} + engines: {node: '>=14.16'} + dependencies: + '@sindresorhus/is': 5.6.0 + '@szmarczak/http-timer': 5.0.1 + cacheable-lookup: 7.0.0 + cacheable-request: 10.2.13 + decompress-response: 6.0.0 + form-data-encoder: 2.1.4 + get-stream: 6.0.1 + http2-wrapper: 2.2.0 + lowercase-keys: 3.0.0 + p-cancelable: 3.0.0 + responselike: 3.0.0 + dev: false + + /graceful-fs@4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + dev: false + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + /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: false + + /gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + dependencies: + duplexer: 0.1.2 + dev: false + + /handle-thing@2.0.1: + resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} + dev: false + + /hard-rejection@2.1.0: + resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} + engines: {node: '>=6'} + dev: true + + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: false + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + /has-property-descriptors@1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.2.1 + dev: false + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: false + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: false + + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: false + + /has-yarn@3.0.0: + resolution: {integrity: sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + + /has@1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + + /hast-util-from-parse5@8.0.1: + resolution: {integrity: sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==} + dependencies: + '@types/hast': 3.0.2 + '@types/unist': 3.0.1 + devlop: 1.1.0 + hastscript: 8.0.0 + property-information: 6.3.0 + vfile: 6.0.1 + vfile-location: 5.0.2 + web-namespaces: 2.0.1 + dev: false + + /hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + dependencies: + '@types/hast': 3.0.2 + dev: false + + /hast-util-raw@9.0.1: + resolution: {integrity: sha512-5m1gmba658Q+lO5uqL5YNGQWeh1MYWZbZmWrM5lncdcuiXuo5E2HT/CIOp0rLF8ksfSwiCVJ3twlgVRyTGThGA==} + dependencies: + '@types/hast': 3.0.2 + '@types/unist': 3.0.1 + '@ungap/structured-clone': 1.2.0 + hast-util-from-parse5: 8.0.1 + hast-util-to-parse5: 8.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.0.2 + parse5: 7.1.2 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.1 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + dev: false + + /hast-util-to-estree@3.1.0: + resolution: {integrity: sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw==} + dependencies: + '@types/estree': 1.0.1 + '@types/estree-jsx': 1.0.0 + '@types/hast': 3.0.2 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-attach-comments: 3.0.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.0 + mdast-util-mdx-jsx: 3.0.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 6.3.0 + space-separated-tokens: 2.0.2 + style-to-object: 0.4.2 + unist-util-position: 5.0.0 + zwitch: 2.0.4 + transitivePeerDependencies: + - supports-color + + /hast-util-to-jsx-runtime@2.2.0: + resolution: {integrity: sha512-wSlp23N45CMjDg/BPW8zvhEi3R+8eRE1qFbjEyAUzMCzu2l1Wzwakq+Tlia9nkCtEl5mDxa7nKHsvYJ6Gfn21A==} + dependencies: + '@types/hast': 3.0.2 + '@types/unist': 3.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + property-information: 6.3.0 + space-separated-tokens: 2.0.2 + style-to-object: 0.4.2 + unist-util-position: 5.0.0 + vfile-message: 4.0.2 + + /hast-util-to-parse5@8.0.0: + resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} + dependencies: + '@types/hast': 3.0.2 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 6.3.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + dev: false + + /hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + dependencies: + '@types/hast': 3.0.2 + + /hastscript@8.0.0: + resolution: {integrity: sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==} + dependencies: + '@types/hast': 3.0.2 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 6.3.0 + space-separated-tokens: 2.0.2 + dev: false + + /he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + dev: false + + /history@4.10.1: + resolution: {integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==} + dependencies: + '@babel/runtime': 7.22.15 + loose-envify: 1.4.0 + resolve-pathname: 3.0.0 + tiny-invariant: 1.3.1 + tiny-warning: 1.0.3 + value-equal: 1.0.1 + dev: false + + /hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + dependencies: + react-is: 16.13.1 + dev: false + + /hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + dependencies: + lru-cache: 6.0.0 + dev: true + + /hpack.js@2.1.6: + resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==} + dependencies: + inherits: 2.0.4 + obuf: 1.1.2 + readable-stream: 2.3.8 + wbuf: 1.7.3 + dev: false + + /html-entities@2.4.0: + resolution: {integrity: sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==} + dev: false + + /html-minifier-terser@6.1.0: + resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==} + engines: {node: '>=12'} + hasBin: true + dependencies: + camel-case: 4.1.2 + clean-css: 5.3.2 + commander: 8.3.0 + he: 1.2.0 + param-case: 3.0.4 + relateurl: 0.2.7 + terser: 5.19.4 + dev: false + + /html-minifier-terser@7.2.0: + resolution: {integrity: sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==} + engines: {node: ^14.13.1 || >=16.0.0} + hasBin: true + dependencies: + camel-case: 4.1.2 + clean-css: 5.3.2 + commander: 10.0.1 + entities: 4.5.0 + param-case: 3.0.4 + relateurl: 0.2.7 + terser: 5.19.4 + dev: false + + /html-tags@3.3.1: + resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} + engines: {node: '>=8'} + + /html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + dev: false + + /html-webpack-plugin@5.5.3(webpack@5.88.2): + resolution: {integrity: sha512-6YrDKTuqaP/TquFH7h4srYWsZx+x6k6+FbsTm0ziCwGHDP78Unr1r9F/H4+sGmMbX08GQcJ+K64x55b+7VM/jg==} + engines: {node: '>=10.13.0'} + peerDependencies: + webpack: ^5.20.0 + dependencies: + '@types/html-minifier-terser': 6.1.0 + html-minifier-terser: 6.1.0 + lodash: 4.17.21 + pretty-error: 4.0.0 + tapable: 2.2.1 + webpack: 5.88.2 + dev: false + + /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: false + + /htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + dev: false + + /http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + dev: false + + /http-deceiver@1.2.7: + resolution: {integrity: sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==} + dev: false + + /http-errors@1.6.3: + resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} + engines: {node: '>= 0.6'} + dependencies: + depd: 1.1.2 + inherits: 2.0.3 + setprototypeof: 1.1.0 + statuses: 1.5.0 + dev: false + + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + dev: false + + /http-parser-js@0.5.8: + resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==} + dev: false + + /http-proxy-middleware@2.0.6(@types/express@4.17.17): + resolution: {integrity: sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/express': ^4.17.13 + peerDependenciesMeta: + '@types/express': + optional: true + dependencies: + '@types/express': 4.17.17 + '@types/http-proxy': 1.17.11 + http-proxy: 1.18.1 + is-glob: 4.0.3 + is-plain-obj: 3.0.0 + micromatch: 4.0.5 + transitivePeerDependencies: + - debug + dev: false + + /http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.15.2 + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + dev: false + + /http2-wrapper@2.2.0: + resolution: {integrity: sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==} + engines: {node: '>=10.19.0'} + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + dev: false + + /human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + /human-signals@4.3.1: + resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} + engines: {node: '>=14.18.0'} + dev: true + + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + + /icss-utils@5.1.0(postcss@8.4.29): + resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.4.29 + dev: false + + /idb@7.1.1: + resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} + dev: false + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: false + + /ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + + /image-size@1.0.2: + resolution: {integrity: sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + queue: 6.0.2 + dev: false + + /immer@9.0.21: + resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} + dev: false + + /immutable@4.3.4: + resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==} + dev: false + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + /import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + /indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + dev: false + + /indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + dev: true + + /infima@0.2.0-alpha.43: + resolution: {integrity: sha512-2uw57LvUqW0rK/SWYnd/2rRfxNA5DDNOh33jxF7fy46VWoNhGxiUQyVZHbBMjQ33mQem0cjdDVwgWVAmlRfgyQ==} + engines: {node: '>=12'} + dev: false + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + /inherits@2.0.3: + resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} + dev: false + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + /ini@2.0.0: + resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} + engines: {node: '>=10'} + dev: false + + /inline-style-parser@0.1.1: + resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} + + /internal-slot@1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + side-channel: 1.0.4 + dev: false + + /interpret@1.4.0: + resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} + engines: {node: '>= 0.10'} + dev: false + + /invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + dependencies: + loose-envify: 1.4.0 + + /ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + dev: false + + /ipaddr.js@2.1.0: + resolution: {integrity: sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==} + engines: {node: '>= 10'} + dev: false + + /is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + /is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + dev: false + + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + /is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + dev: false + + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: false + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: false + + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: false + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: false + + /is-ci@3.0.1: + resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} + hasBin: true + dependencies: + ci-info: 3.8.0 + dev: false + + /is-core-module@2.13.0: + resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} + dependencies: + has: 1.0.3 + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + /is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + /is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + dev: true + + /is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + dev: false + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + + /is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + /is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + dependencies: + is-docker: 3.0.0 + dev: true + + /is-installed-globally@0.4.0: + resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} + engines: {node: '>=10'} + dependencies: + global-dirs: 3.0.1 + is-path-inside: 3.0.3 + dev: false + + /is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + dev: false + + /is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + dev: false + + /is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: false + + /is-npm@6.0.0: + resolution: {integrity: sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + /is-obj@1.0.1: + resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} + engines: {node: '>=0.10.0'} + dev: false + + /is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + dev: false + + /is-path-cwd@2.2.0: + resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} + engines: {node: '>=6'} + dev: false + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + /is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + dev: true + + /is-plain-obj@3.0.0: + resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} + engines: {node: '>=10'} + dev: false + + /is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + /is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + dependencies: + isobject: 3.0.1 + + /is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + /is-reference@3.0.1: + resolution: {integrity: sha512-baJJdQLiYaJdvFbJqXrcGv3WU3QCzBlUcI5QhbesIm6/xPsvmO+2CDoi/GMOFBQEQm+PXkwOPrp9KK5ozZsp2w==} + dependencies: + '@types/estree': 1.0.1 + + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: false + + /is-regexp@1.0.0: + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} + engines: {node: '>=0.10.0'} + dev: false + + /is-root@2.1.0: + resolution: {integrity: sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==} + engines: {node: '>=6'} + dev: false + + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.2 + dev: false + + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + /is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: false + + /is-typed-array@1.1.12: + resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.11 + dev: false + + /is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + dev: false + + /is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + dev: false + + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.2 + dev: false + + /is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + dependencies: + is-docker: 2.2.1 + + /is-yarn-global@0.4.1: + resolution: {integrity: sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==} + engines: {node: '>=12'} + dev: false + + /isarray@0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + dev: false + + /isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + dev: false + + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: false + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + /isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + + /jake@10.8.7: + resolution: {integrity: sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==} + engines: {node: '>=10'} + hasBin: true + dependencies: + async: 3.2.4 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + dev: false + + /jest-util@29.6.3: + resolution: {integrity: sha512-QUjna/xSy4B32fzcKTSz1w7YYzgiHrjjJjevdRf61HYk998R5vVMMNmrHESYZVDS5DSWs+1srPLPKxXPkeSDOA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.5.9 + chalk: 4.1.2 + ci-info: 3.8.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + dev: false + + /jest-worker@26.6.2: + resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/node': 20.5.9 + merge-stream: 2.0.0 + supports-color: 7.2.0 + dev: false + + /jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/node': 20.5.9 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + /jest-worker@29.6.4: + resolution: {integrity: sha512-6dpvFV4WjcWbDVGgHTWo/aupl8/LbBx2NSKfiwqf79xC/yeJjKHT1+StcKy/2KTmW16hE68ccKVOtXf+WZGz7Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@types/node': 20.5.9 + jest-util: 29.6.3 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: false + + /jiti@1.21.0: + resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} + hasBin: true + dev: false + + /joi@17.10.1: + resolution: {integrity: sha512-vIiDxQKmRidUVp8KngT8MZSOcmRVm2zV7jbMjNYWuHcJWI0bUck3nRTGQjhpPlQenIQIBC5Vp9AhcnHbWQqafw==} + dependencies: + '@hapi/hoek': 9.3.0 + '@hapi/topo': 5.1.0 + '@sideway/address': 4.1.4 + '@sideway/formula': 3.0.1 + '@sideway/pinpoint': 2.0.0 + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + /js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + dev: false + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + + /jsesc@0.5.0: + resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} + hasBin: true + dev: false + + /jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + /json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + /json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + dev: false + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: false + + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.0 + optionalDependencies: + graceful-fs: 4.2.11 + dev: false + + /jsonpointer@5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + dev: false + + /keyv@4.5.3: + resolution: {integrity: sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==} + dependencies: + json-buffer: 3.0.1 + + /kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + /kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + dev: false + + /klona@2.0.6: + resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} + engines: {node: '>= 8'} + dev: false + + /known-css-properties@0.29.0: + resolution: {integrity: sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==} + dev: true + + /latest-version@7.0.0: + resolution: {integrity: sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==} + engines: {node: '>=14.16'} + dependencies: + package-json: 8.1.1 + dev: false + + /launch-editor@2.6.0: + resolution: {integrity: sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==} + dependencies: + picocolors: 1.0.0 + shell-quote: 1.8.1 + dev: false + + /leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + dev: false + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + /lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + dev: false + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + /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 + + /loader-runner@4.3.0: + resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + engines: {node: '>=6.11.5'} + + /loader-utils@2.0.4: + resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} + engines: {node: '>=8.9.0'} + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 2.2.3 + dev: false + + /loader-utils@3.2.1: + resolution: {integrity: sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==} + engines: {node: '>= 12.13.0'} + dev: false + + /locate-path@3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} + dependencies: + p-locate: 3.0.0 + path-exists: 3.0.0 + dev: false + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + + /locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-locate: 6.0.0 + dev: false + + /lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + dev: false + + /lodash.escape@4.0.1: + resolution: {integrity: sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw==} + dev: false + + /lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + dev: false + + /lodash.invokemap@4.6.0: + resolution: {integrity: sha512-CfkycNtMqgUlfjfdh2BhKO/ZXrP8ePOX5lEU/g0R3ItJcnuxWDwokMGKx1hWcfOikmyOVx6X9IwWnDGlgKl61w==} + dev: false + + /lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + dev: false + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + /lodash.pullall@4.2.0: + resolution: {integrity: sha512-VhqxBKH0ZxPpLhiu68YD1KnHmbhQJQctcipvmFnqIBDYzcIHzf3Zpu0tpeOKtR4x76p9yohc506eGdOjTmyIBg==} + dev: false + + /lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + dev: false + + /lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + dev: true + + /lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + dev: false + + /lodash.uniqby@4.7.0: + resolution: {integrity: sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==} + dev: false + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: false + + /log-symbols@5.1.0: + resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==} + engines: {node: '>=12'} + dependencies: + chalk: 5.3.0 + is-unicode-supported: 1.3.0 + dev: false + + /longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + + /lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + dependencies: + tslib: 2.6.2 + dev: false + + /lowercase-keys@3.0.0: + resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: false + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + + /magic-string@0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + dependencies: + sourcemap-codec: 1.4.8 + dev: false + + /map-obj@1.0.1: + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} + engines: {node: '>=0.10.0'} + dev: true + + /map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + dev: true + + /markdown-extensions@2.0.0: + resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} + engines: {node: '>=16'} + + /markdown-table@3.0.3: + resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==} + dev: false + + /mathml-tag-names@2.1.3: + resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} + dev: true + + /mdast-util-directive@3.0.0: + resolution: {integrity: sha512-JUpYOqKI4mM3sZcNxmF/ox04XYFFkNwr0CFlrQIkCwbvH0xzMCqkMqAde9wRd80VAhaUrwFwKm2nxretdT1h7Q==} + dependencies: + '@types/mdast': 4.0.2 + '@types/unist': 3.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.0 + mdast-util-to-markdown: 2.1.0 + parse-entities: 4.0.1 + stringify-entities: 4.0.3 + unist-util-visit-parents: 6.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /mdast-util-find-and-replace@3.0.1: + resolution: {integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==} + dependencies: + '@types/mdast': 4.0.2 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + dev: false + + /mdast-util-from-markdown@2.0.0: + resolution: {integrity: sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA==} + dependencies: + '@types/mdast': 4.0.2 + '@types/unist': 3.0.1 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.0 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-decode-string: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + /mdast-util-frontmatter@2.0.1: + resolution: {integrity: sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==} + dependencies: + '@types/mdast': 4.0.2 + devlop: 1.1.0 + escape-string-regexp: 5.0.0 + mdast-util-from-markdown: 2.0.0 + mdast-util-to-markdown: 2.1.0 + micromark-extension-frontmatter: 2.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /mdast-util-gfm-autolink-literal@2.0.0: + resolution: {integrity: sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg==} + dependencies: + '@types/mdast': 4.0.2 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.1 + micromark-util-character: 2.0.1 + dev: false + + /mdast-util-gfm-footnote@2.0.0: + resolution: {integrity: sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==} + dependencies: + '@types/mdast': 4.0.2 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.0 + mdast-util-to-markdown: 2.1.0 + micromark-util-normalize-identifier: 2.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + dependencies: + '@types/mdast': 4.0.2 + mdast-util-from-markdown: 2.0.0 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + dev: false + + /mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + dependencies: + '@types/mdast': 4.0.2 + devlop: 1.1.0 + markdown-table: 3.0.3 + mdast-util-from-markdown: 2.0.0 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + dev: false + + /mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + dependencies: + '@types/mdast': 4.0.2 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.0 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + dev: false + + /mdast-util-gfm@3.0.0: + resolution: {integrity: sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==} + dependencies: + mdast-util-from-markdown: 2.0.0 + mdast-util-gfm-autolink-literal: 2.0.0 + mdast-util-gfm-footnote: 2.0.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + dev: false + + /mdast-util-mdx-expression@2.0.0: + resolution: {integrity: sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw==} + dependencies: + '@types/estree-jsx': 1.0.0 + '@types/hast': 3.0.2 + '@types/mdast': 4.0.2 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.0 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + /mdast-util-mdx-jsx@3.0.0: + resolution: {integrity: sha512-XZuPPzQNBPAlaqsTTgRrcJnyFbSOBovSadFgbFu8SnuNgm+6Bdx1K+IWoitsmj6Lq6MNtI+ytOqwN70n//NaBA==} + dependencies: + '@types/estree-jsx': 1.0.0 + '@types/hast': 3.0.2 + '@types/mdast': 4.0.2 + '@types/unist': 3.0.1 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.0 + mdast-util-to-markdown: 2.1.0 + parse-entities: 4.0.1 + stringify-entities: 4.0.3 + unist-util-remove-position: 5.0.0 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + /mdast-util-mdx@3.0.0: + resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==} + dependencies: + mdast-util-from-markdown: 2.0.0 + mdast-util-mdx-expression: 2.0.0 + mdast-util-mdx-jsx: 3.0.0 + mdast-util-mdxjs-esm: 2.0.1 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + /mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + dependencies: + '@types/estree-jsx': 1.0.0 + '@types/hast': 3.0.2 + '@types/mdast': 4.0.2 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.0 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + /mdast-util-phrasing@4.0.0: + resolution: {integrity: sha512-xadSsJayQIucJ9n053dfQwVu1kuXg7jCTdYsMK8rqzKZh52nLfSH/k0sAxE0u+pj/zKZX+o5wB+ML5mRayOxFA==} + dependencies: + '@types/mdast': 4.0.2 + unist-util-is: 6.0.0 + + /mdast-util-to-hast@13.0.2: + resolution: {integrity: sha512-U5I+500EOOw9e3ZrclN3Is3fRpw8c19SMyNZlZ2IS+7vLsNzb2Om11VpIVOR+/0137GhZsFEF6YiKD5+0Hr2Og==} + dependencies: + '@types/hast': 3.0.2 + '@types/mdast': 4.0.2 + '@ungap/structured-clone': 1.2.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.0 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + + /mdast-util-to-markdown@2.1.0: + resolution: {integrity: sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==} + dependencies: + '@types/mdast': 4.0.2 + '@types/unist': 3.0.1 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.0.0 + mdast-util-to-string: 4.0.0 + micromark-util-decode-string: 2.0.0 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + /mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + dependencies: + '@types/mdast': 4.0.2 + + /mdn-data@2.0.14: + resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + dev: false + + /mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + dev: true + + /media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + dev: false + + /medium-zoom@1.0.8: + resolution: {integrity: sha512-CjFVuFq/IfrdqesAXfg+hzlDKu6A2n80ZIq0Kl9kWjoHh9j1N9Uvk5X0/MmN0hOfm5F9YBswlClhcwnmtwz7gA==} + dev: false + + /memfs@3.5.3: + resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} + engines: {node: '>= 4.0.0'} + dependencies: + fs-monkey: 1.0.4 + dev: false + + /meow@10.1.5: + resolution: {integrity: sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + '@types/minimist': 1.2.2 + camelcase-keys: 7.0.2 + decamelize: 5.0.1 + decamelize-keys: 1.1.1 + hard-rejection: 2.1.0 + minimist-options: 4.1.0 + normalize-package-data: 3.0.3 + read-pkg-up: 8.0.0 + redent: 4.0.0 + trim-newlines: 4.1.1 + type-fest: 1.4.0 + yargs-parser: 20.2.9 + dev: true + + /merge-descriptors@1.0.1: + resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + dev: false + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + /methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + dev: false + + /micromark-core-commonmark@2.0.0: + resolution: {integrity: sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA==} + dependencies: + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-factory-destination: 2.0.0 + micromark-factory-label: 2.0.0 + micromark-factory-space: 2.0.0 + micromark-factory-title: 2.0.0 + micromark-factory-whitespace: 2.0.0 + micromark-util-character: 2.0.1 + micromark-util-chunked: 2.0.0 + micromark-util-classify-character: 2.0.0 + micromark-util-html-tag-name: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-subtokenize: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + /micromark-extension-directive@3.0.0: + resolution: {integrity: sha512-61OI07qpQrERc+0wEysLHMvoiO3s2R56x5u7glHq2Yqq6EHbH4dW25G9GfDdGCDYqA21KE6DWgNSzxSwHc2hSg==} + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.0 + micromark-factory-whitespace: 2.0.0 + micromark-util-character: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + parse-entities: 4.0.1 + dev: false + + /micromark-extension-frontmatter@2.0.0: + resolution: {integrity: sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==} + dependencies: + fault: 2.0.1 + micromark-util-character: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + dev: false + + /micromark-extension-gfm-autolink-literal@2.0.0: + resolution: {integrity: sha512-rTHfnpt/Q7dEAK1Y5ii0W8bhfJlVJFnJMHIPisfPK3gpVNuOP0VnRl96+YJ3RYWV/P4gFeQoGKNlT3RhuvpqAg==} + dependencies: + micromark-util-character: 2.0.1 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + dev: false + + /micromark-extension-gfm-footnote@2.0.0: + resolution: {integrity: sha512-6Rzu0CYRKDv3BfLAUnZsSlzx3ak6HAoI85KTiijuKIz5UxZxbUI+pD6oHgw+6UtQuiRwnGRhzMmPRv4smcz0fg==} + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.0 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.0.1 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + dev: false + + /micromark-extension-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-c3BR1ClMp5fxxmwP6AoOY2fXO9U8uFMKs4ADD66ahLTNcwzSCyRVU4k7LPV5Nxo/VJiR4TdzxRQY2v3qIUceCw==} + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-classify-character: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + dev: false + + /micromark-extension-gfm-table@2.0.0: + resolution: {integrity: sha512-PoHlhypg1ItIucOaHmKE8fbin3vTLpDOUg8KAr8gRCF1MOZI9Nquq2i/44wFvviM4WuxJzc3demT8Y3dkfvYrw==} + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + dev: false + + /micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + dependencies: + micromark-util-types: 2.0.0 + dev: false + + /micromark-extension-gfm-task-list-item@2.0.1: + resolution: {integrity: sha512-cY5PzGcnULaN5O7T+cOzfMoHjBW7j+T9D2sucA5d/KbsBTPcYdebm9zUd9zzdgJGCwahV+/W78Z3nbulBYVbTw==} + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + dev: false + + /micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + dependencies: + micromark-extension-gfm-autolink-literal: 2.0.0 + micromark-extension-gfm-footnote: 2.0.0 + micromark-extension-gfm-strikethrough: 2.0.0 + micromark-extension-gfm-table: 2.0.0 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.0.1 + micromark-util-combine-extensions: 2.0.0 + micromark-util-types: 2.0.0 + dev: false + + /micromark-extension-mdx-expression@3.0.0: + resolution: {integrity: sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ==} + dependencies: + '@types/estree': 1.0.1 + devlop: 1.1.0 + micromark-factory-mdx-expression: 2.0.1 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.0.1 + micromark-util-events-to-acorn: 2.0.2 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + /micromark-extension-mdx-jsx@3.0.0: + resolution: {integrity: sha512-uvhhss8OGuzR4/N17L1JwvmJIpPhAd8oByMawEKx6NVdBCbesjH4t+vjEp3ZXft9DwvlKSD07fCeI44/N0Vf2w==} + dependencies: + '@types/acorn': 4.0.6 + '@types/estree': 1.0.1 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + micromark-factory-mdx-expression: 2.0.1 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + vfile-message: 4.0.2 + + /micromark-extension-mdx-md@2.0.0: + resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==} + dependencies: + micromark-util-types: 2.0.0 + + /micromark-extension-mdxjs-esm@3.0.0: + resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==} + dependencies: + '@types/estree': 1.0.1 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.0 + micromark-util-character: 2.0.1 + micromark-util-events-to-acorn: 2.0.2 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.2 + + /micromark-extension-mdxjs@3.0.0: + resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==} + dependencies: + acorn: 8.10.0 + acorn-jsx: 5.3.2(acorn@8.10.0) + micromark-extension-mdx-expression: 3.0.0 + micromark-extension-mdx-jsx: 3.0.0 + micromark-extension-mdx-md: 2.0.0 + micromark-extension-mdxjs-esm: 3.0.0 + micromark-util-combine-extensions: 2.0.0 + micromark-util-types: 2.0.0 + + /micromark-factory-destination@2.0.0: + resolution: {integrity: sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==} + dependencies: + micromark-util-character: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + /micromark-factory-label@2.0.0: + resolution: {integrity: sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==} + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + /micromark-factory-mdx-expression@2.0.1: + resolution: {integrity: sha512-F0ccWIUHRLRrYp5TC9ZYXmZo+p2AM13ggbsW4T0b5CRKP8KHVRB8t4pwtBgTxtjRmwrK0Irwm7vs2JOZabHZfg==} + dependencies: + '@types/estree': 1.0.1 + devlop: 1.1.0 + micromark-util-character: 2.0.1 + micromark-util-events-to-acorn: 2.0.2 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.2 + + /micromark-factory-space@1.1.0: + resolution: {integrity: sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==} + dependencies: + micromark-util-character: 1.2.0 + micromark-util-types: 1.1.0 + dev: false + + /micromark-factory-space@2.0.0: + resolution: {integrity: sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==} + dependencies: + micromark-util-character: 2.0.1 + micromark-util-types: 2.0.0 + + /micromark-factory-title@2.0.0: + resolution: {integrity: sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==} + dependencies: + micromark-factory-space: 2.0.0 + micromark-util-character: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + /micromark-factory-whitespace@2.0.0: + resolution: {integrity: sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==} + dependencies: + micromark-factory-space: 2.0.0 + micromark-util-character: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + /micromark-util-character@1.2.0: + resolution: {integrity: sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==} + dependencies: + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + dev: false + + /micromark-util-character@2.0.1: + resolution: {integrity: sha512-3wgnrmEAJ4T+mGXAUfMvMAbxU9RDG43XmGce4j6CwPtVxB3vfwXSZ6KhFwDzZ3mZHhmPimMAXg71veiBGzeAZw==} + dependencies: + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + /micromark-util-chunked@2.0.0: + resolution: {integrity: sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==} + dependencies: + micromark-util-symbol: 2.0.0 + + /micromark-util-classify-character@2.0.0: + resolution: {integrity: sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==} + dependencies: + micromark-util-character: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + /micromark-util-combine-extensions@2.0.0: + resolution: {integrity: sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==} + dependencies: + micromark-util-chunked: 2.0.0 + micromark-util-types: 2.0.0 + + /micromark-util-decode-numeric-character-reference@2.0.1: + resolution: {integrity: sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==} + dependencies: + micromark-util-symbol: 2.0.0 + + /micromark-util-decode-string@2.0.0: + resolution: {integrity: sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==} + dependencies: + decode-named-character-reference: 1.0.2 + micromark-util-character: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-symbol: 2.0.0 + + /micromark-util-encode@2.0.0: + resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==} + + /micromark-util-events-to-acorn@2.0.2: + resolution: {integrity: sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA==} + dependencies: + '@types/acorn': 4.0.6 + '@types/estree': 1.0.1 + '@types/unist': 3.0.1 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + vfile-message: 4.0.2 + + /micromark-util-html-tag-name@2.0.0: + resolution: {integrity: sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==} + + /micromark-util-normalize-identifier@2.0.0: + resolution: {integrity: sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==} + dependencies: + micromark-util-symbol: 2.0.0 + + /micromark-util-resolve-all@2.0.0: + resolution: {integrity: sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==} + dependencies: + micromark-util-types: 2.0.0 + + /micromark-util-sanitize-uri@2.0.0: + resolution: {integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==} + dependencies: + micromark-util-character: 2.0.1 + micromark-util-encode: 2.0.0 + micromark-util-symbol: 2.0.0 + + /micromark-util-subtokenize@2.0.0: + resolution: {integrity: sha512-vc93L1t+gpR3p8jxeVdaYlbV2jTYteDje19rNSS/H5dlhxUYll5Fy6vJ2cDwP8RnsXi818yGty1ayP55y3W6fg==} + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + /micromark-util-symbol@1.1.0: + resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==} + dev: false + + /micromark-util-symbol@2.0.0: + resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==} + + /micromark-util-types@1.1.0: + resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==} + dev: false + + /micromark-util-types@2.0.0: + resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==} + + /micromark@4.0.0: + resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==} + dependencies: + '@types/debug': 4.1.8 + debug: 4.3.4 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.0 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.0.1 + micromark-util-chunked: 2.0.0 + micromark-util-combine-extensions: 2.0.0 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-encode: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-subtokenize: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + transitivePeerDependencies: + - supports-color + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + + /mime-db@1.33.0: + resolution: {integrity: sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==} + engines: {node: '>= 0.6'} + dev: false + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + /mime-types@2.1.18: + resolution: {integrity: sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.33.0 + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + /mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + dev: true + + /mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + dev: false + + /mimic-response@4.0.0: + resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + + /min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + dev: true + + /mini-css-extract-plugin@2.7.6(webpack@5.88.2): + resolution: {integrity: sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + dependencies: + schema-utils: 4.2.0 + webpack: 5.88.2 + dev: false + + /minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + dev: false + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + + /minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: false + + /minimist-options@4.1.0: + resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} + engines: {node: '>= 6'} + dependencies: + arrify: 1.0.1 + is-plain-obj: 1.1.0 + kind-of: 6.0.3 + dev: true + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: false + + /mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + dev: false + + /mrmime@1.0.1: + resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} + engines: {node: '>=10'} + dev: false + + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + dev: false + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: false + + /multicast-dns@7.2.5: + resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} + hasBin: true + dependencies: + dns-packet: 5.6.1 + thunky: 1.1.0 + dev: false + + /nanoid@3.3.6: + resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + /napi-build-utils@1.0.2: + resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + dev: false + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + dev: false + + /neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + /no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + dependencies: + lower-case: 2.0.2 + tslib: 2.6.2 + dev: false + + /node-abi@3.47.0: + resolution: {integrity: sha512-2s6B2CWZM//kPgwnuI0KrYwNjfdByE25zvAaEpq9IH4zcNsarH8Ihu/UuX6XMPEogDAxkuUFeZn60pXNHAqn3A==} + engines: {node: '>=10'} + dependencies: + semver: 7.5.4 + dev: false + + /node-addon-api@6.1.0: + resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} + dev: false + + /node-emoji@2.1.0: + resolution: {integrity: sha512-tcsBm9C6FmPN5Wo7OjFi9lgMyJjvkAeirmjR/ax8Ttfqy4N8PoFic26uqFTIgayHPNI5FH4ltUvfh9kHzwcK9A==} + dependencies: + '@sindresorhus/is': 3.1.2 + char-regex: 1.0.2 + emojilib: 2.4.0 + skin-tone: 2.0.0 + dev: false + + /node-forge@1.3.1: + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + engines: {node: '>= 6.13.0'} + dev: false + + /node-releases@2.0.14: + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + + /normalize-package-data@3.0.3: + resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} + engines: {node: '>=10'} + dependencies: + hosted-git-info: 4.1.0 + is-core-module: 2.13.0 + semver: 7.5.4 + validate-npm-package-license: 3.0.4 + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + /normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + dev: false + + /normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + dev: false + + /normalize-url@8.0.0: + resolution: {integrity: sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==} + engines: {node: '>=14.16'} + dev: false + + /npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + dependencies: + path-key: 3.1.1 + + /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 + + /nprogress@0.2.0: + resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} + dev: false + + /nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + dependencies: + boolbase: 1.0.0 + dev: false + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + /object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + dev: false + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: false + + /object.assign@4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: false + + /obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + dev: false + + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: false + + /on-headers@1.0.2: + resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} + engines: {node: '>= 0.8'} + dev: false + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + + /onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + dependencies: + mimic-fn: 4.0.0 + dev: true + + /open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + dev: false + + /open@9.1.0: + resolution: {integrity: sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==} + engines: {node: '>=14.16'} + dependencies: + default-browser: 4.0.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + is-wsl: 2.2.0 + dev: true + + /opener@1.5.2: + resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} + hasBin: true + dev: false + + /optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + dependencies: + '@aashutoshrathi/word-wrap': 1.2.6 + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + + /ora@7.0.1: + resolution: {integrity: sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==} + engines: {node: '>=16'} + dependencies: + chalk: 5.3.0 + cli-cursor: 4.0.0 + cli-spinners: 2.9.0 + is-interactive: 2.0.0 + is-unicode-supported: 1.3.0 + log-symbols: 5.1.0 + stdin-discarder: 0.1.0 + string-width: 6.1.0 + strip-ansi: 7.1.0 + dev: false + + /p-cancelable@3.0.0: + resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} + engines: {node: '>=12.20'} + dev: false + + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + dev: false + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + + /p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + yocto-queue: 1.0.0 + dev: false + + /p-locate@3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + dependencies: + p-limit: 2.3.0 + dev: false + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + + /p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-limit: 4.0.0 + dev: false + + /p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + dependencies: + aggregate-error: 3.1.0 + dev: false + + /p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + dev: false + + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + dev: false + + /package-json@8.1.1: + resolution: {integrity: sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==} + engines: {node: '>=14.16'} + dependencies: + got: 12.6.1 + registry-auth-token: 5.0.2 + registry-url: 6.0.1 + semver: 7.5.4 + dev: false + + /param-case@3.0.4: + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + dependencies: + dot-case: 3.0.4 + tslib: 2.6.2 + dev: false + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + + /parse-entities@4.0.1: + resolution: {integrity: sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==} + dependencies: + '@types/unist': 2.0.8 + character-entities: 2.0.2 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.0.2 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.22.13 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + /parse-numeric-range@1.3.0: + resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==} + dev: false + + /parse5-htmlparser2-tree-adapter@7.0.0: + resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} + dependencies: + domhandler: 5.0.3 + parse5: 7.1.2 + dev: false + + /parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + dependencies: + entities: 4.5.0 + dev: false + + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + dev: false + + /pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + dependencies: + no-case: 3.0.4 + tslib: 2.6.2 + dev: false + + /path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + dev: false + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + /path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + /path-is-inside@1.0.2: + resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==} + dev: false + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + /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: false + + /path-root-regex@0.1.2: + resolution: {integrity: sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==} + engines: {node: '>=0.10.0'} + dev: false + + /path-root@0.1.1: + resolution: {integrity: sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==} + engines: {node: '>=0.10.0'} + dependencies: + path-root-regex: 0.1.2 + dev: false + + /path-to-regexp@0.1.7: + resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + dev: false + + /path-to-regexp@1.8.0: + resolution: {integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==} + dependencies: + isarray: 0.0.1 + dev: false + + /path-to-regexp@2.2.1: + resolution: {integrity: sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==} + dev: false + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + /periscopic@3.1.0: + resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} + dependencies: + '@types/estree': 1.0.1 + estree-walker: 3.0.3 + is-reference: 3.0.1 + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + /pkg-dir@7.0.0: + resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} + engines: {node: '>=14.16'} + dependencies: + find-up: 6.3.0 + dev: false + + /pkg-up@3.1.0: + resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} + engines: {node: '>=8'} + dependencies: + find-up: 3.0.0 + dev: false + + /postcss-calc@8.2.4(postcss@8.4.29): + resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==} + peerDependencies: + postcss: ^8.2.2 + dependencies: + postcss: 8.4.29 + postcss-selector-parser: 6.0.13 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-colormin@5.3.1(postcss@8.4.29): + resolution: {integrity: sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.22.2 + caniuse-api: 3.0.0 + colord: 2.9.3 + postcss: 8.4.29 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-convert-values@5.1.3(postcss@8.4.29): + resolution: {integrity: sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.22.2 + postcss: 8.4.29 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-discard-comments@5.1.2(postcss@8.4.29): + resolution: {integrity: sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.29 + dev: false + + /postcss-discard-duplicates@5.1.0(postcss@8.4.29): + resolution: {integrity: sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.29 + dev: false + + /postcss-discard-empty@5.1.1(postcss@8.4.29): + resolution: {integrity: sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.29 + dev: false + + /postcss-discard-overridden@5.1.0(postcss@8.4.29): + resolution: {integrity: sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.29 + dev: false + + /postcss-discard-unused@5.1.0(postcss@8.4.29): + resolution: {integrity: sha512-KwLWymI9hbwXmJa0dkrzpRbSJEh0vVUd7r8t0yOGPcfKzyJJxFM8kLyC5Ev9avji6nY95pOp1W6HqIrfT+0VGw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.29 + postcss-selector-parser: 6.0.13 + dev: false + + /postcss-loader@7.3.3(postcss@8.4.29)(typescript@5.3.3)(webpack@5.88.2): + resolution: {integrity: sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA==} + engines: {node: '>= 14.15.0'} + peerDependencies: + postcss: ^7.0.0 || ^8.0.1 + webpack: ^5.0.0 + dependencies: + cosmiconfig: 8.3.4(typescript@5.3.3) + jiti: 1.21.0 + postcss: 8.4.29 + semver: 7.5.4 + webpack: 5.88.2 + transitivePeerDependencies: + - typescript + dev: false + + /postcss-media-query-parser@0.2.3: + resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} + dev: true + + /postcss-merge-idents@5.1.1(postcss@8.4.29): + resolution: {integrity: sha512-pCijL1TREiCoog5nQp7wUe+TUonA2tC2sQ54UGeMmryK3UFGIYKqDyjnqd6RcuI4znFn9hWSLNN8xKE/vWcUQw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + cssnano-utils: 3.1.0(postcss@8.4.29) + postcss: 8.4.29 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-merge-longhand@5.1.7(postcss@8.4.29): + resolution: {integrity: sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.29 + postcss-value-parser: 4.2.0 + stylehacks: 5.1.1(postcss@8.4.29) + dev: false + + /postcss-merge-rules@5.1.4(postcss@8.4.29): + resolution: {integrity: sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.22.2 + caniuse-api: 3.0.0 + cssnano-utils: 3.1.0(postcss@8.4.29) + postcss: 8.4.29 + postcss-selector-parser: 6.0.13 + dev: false + + /postcss-minify-font-values@5.1.0(postcss@8.4.29): + resolution: {integrity: sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.29 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-minify-gradients@5.1.1(postcss@8.4.29): + resolution: {integrity: sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + colord: 2.9.3 + cssnano-utils: 3.1.0(postcss@8.4.29) + postcss: 8.4.29 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-minify-params@5.1.4(postcss@8.4.29): + resolution: {integrity: sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.22.2 + cssnano-utils: 3.1.0(postcss@8.4.29) + postcss: 8.4.29 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-minify-selectors@5.2.1(postcss@8.4.29): + resolution: {integrity: sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.29 + postcss-selector-parser: 6.0.13 + dev: false + + /postcss-modules-extract-imports@3.0.0(postcss@8.4.29): + resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.4.29 + dev: false + + /postcss-modules-local-by-default@4.0.3(postcss@8.4.29): + resolution: {integrity: sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + icss-utils: 5.1.0(postcss@8.4.29) + postcss: 8.4.29 + postcss-selector-parser: 6.0.13 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-modules-scope@3.0.0(postcss@8.4.29): + resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.4.29 + postcss-selector-parser: 6.0.13 + dev: false + + /postcss-modules-values@4.0.0(postcss@8.4.29): + resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + icss-utils: 5.1.0(postcss@8.4.29) + postcss: 8.4.29 + dev: false + + /postcss-normalize-charset@5.1.0(postcss@8.4.29): + resolution: {integrity: sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.29 + dev: false + + /postcss-normalize-display-values@5.1.0(postcss@8.4.29): + resolution: {integrity: sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.29 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-normalize-positions@5.1.1(postcss@8.4.29): + resolution: {integrity: sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.29 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-normalize-repeat-style@5.1.1(postcss@8.4.29): + resolution: {integrity: sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.29 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-normalize-string@5.1.0(postcss@8.4.29): + resolution: {integrity: sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.29 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-normalize-timing-functions@5.1.0(postcss@8.4.29): + resolution: {integrity: sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.29 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-normalize-unicode@5.1.1(postcss@8.4.29): + resolution: {integrity: sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.22.2 + postcss: 8.4.29 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-normalize-url@5.1.0(postcss@8.4.29): + resolution: {integrity: sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + normalize-url: 6.1.0 + postcss: 8.4.29 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-normalize-whitespace@5.1.1(postcss@8.4.29): + resolution: {integrity: sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.29 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-ordered-values@5.1.3(postcss@8.4.29): + resolution: {integrity: sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + cssnano-utils: 3.1.0(postcss@8.4.29) + postcss: 8.4.29 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-reduce-idents@5.2.0(postcss@8.4.29): + resolution: {integrity: sha512-BTrLjICoSB6gxbc58D5mdBK8OhXRDqud/zodYfdSi52qvDHdMwk+9kB9xsM8yJThH/sZU5A6QVSmMmaN001gIg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.29 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-reduce-initial@5.1.2(postcss@8.4.29): + resolution: {integrity: sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.22.2 + caniuse-api: 3.0.0 + postcss: 8.4.29 + dev: false + + /postcss-reduce-transforms@5.1.0(postcss@8.4.29): + resolution: {integrity: sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.29 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-resolve-nested-selector@0.1.1: + resolution: {integrity: sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==} + dev: true + + /postcss-safe-parser@6.0.0(postcss@8.4.29): + resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.3.3 + dependencies: + postcss: 8.4.29 + dev: true + + /postcss-scss@4.0.9(postcss@8.4.29): + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + dependencies: + postcss: 8.4.29 + 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 + + /postcss-sort-media-queries@4.4.1(postcss@8.4.29): + resolution: {integrity: sha512-QDESFzDDGKgpiIh4GYXsSy6sek2yAwQx1JASl5AxBtU1Lq2JfKBljIPNdil989NcSKRQX1ToiaKphImtBuhXWw==} + engines: {node: '>=10.0.0'} + peerDependencies: + postcss: ^8.4.16 + dependencies: + postcss: 8.4.29 + sort-css-media-queries: 2.1.0 + dev: false + + /postcss-svgo@5.1.0(postcss@8.4.29): + resolution: {integrity: sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.29 + postcss-value-parser: 4.2.0 + svgo: 2.8.0 + dev: false + + /postcss-unique-selectors@5.1.1(postcss@8.4.29): + resolution: {integrity: sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.29 + postcss-selector-parser: 6.0.13 + dev: false + + /postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + /postcss-zindex@5.1.0(postcss@8.4.29): + resolution: {integrity: sha512-fgFMf0OtVSBR1va1JNHYgMxYk73yhn/qb4uQDq1DLGYolz8gHCyr/sesEuGUaYs58E3ZJRcpoGuPVoB7Meiq9A==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.29 + dev: false + + /postcss@8.4.29: + resolution: {integrity: sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.6 + picocolors: 1.0.0 + source-map-js: 1.0.2 + + /prebuild-install@7.1.1: + resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + detect-libc: 2.0.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 1.0.2 + node-abi: 3.47.0 + pump: 3.0.0 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.1 + tunnel-agent: 0.6.0 + dev: false + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + /prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + dependencies: + fast-diff: 1.3.0 + dev: true + + /prettier@3.1.0: + resolution: {integrity: sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==} + engines: {node: '>=14'} + hasBin: true + dev: true + + /pretty-bytes@5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + dev: false + + /pretty-error@4.0.0: + resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==} + dependencies: + lodash: 4.17.21 + renderkid: 3.0.0 + dev: false + + /pretty-time@1.1.0: + resolution: {integrity: sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==} + engines: {node: '>=4'} + dev: false + + /prism-react-renderer@2.3.1(react@18.2.0): + resolution: {integrity: sha512-Rdf+HzBLR7KYjzpJ1rSoxT9ioO85nZngQEoFIhL07XhtJHlCU3SOz0GJ6+qvMyQe0Se+BV3qpe6Yd/NmQF5Juw==} + peerDependencies: + react: '>=16.0.0' + dependencies: + '@types/prismjs': 1.26.2 + clsx: 2.0.0 + react: 18.2.0 + dev: false + + /prismjs@1.29.0: + resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} + engines: {node: '>=6'} + dev: false + + /process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + dev: false + + /prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + dev: false + + /prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + /property-information@6.3.0: + resolution: {integrity: sha512-gVNZ74nqhRMiIUYWGQdosYetaKc83x8oT41a0LlV3AAFCAZwCpg4vmGkq8t34+cUhp3cnM4XDiU/7xlgK7HGrg==} + + /proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + dev: false + + /proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + dev: false + + /pump@3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: false + + /punycode@1.4.1: + resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} + dev: false + + /punycode@2.3.0: + resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} + engines: {node: '>=6'} + + /pupa@3.1.0: + resolution: {integrity: sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==} + engines: {node: '>=12.20'} + dependencies: + escape-goat: 4.0.0 + dev: false + + /qs@6.11.0: + resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: false + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + /queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + dev: false + + /queue@6.0.2: + resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + dependencies: + inherits: 2.0.4 + dev: false + + /quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + /randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + + /range-parser@1.2.0: + resolution: {integrity: sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==} + engines: {node: '>= 0.6'} + dev: false + + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + dev: false + + /raw-body@2.5.1: + resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + + /raw-loader@4.0.2(webpack@5.88.2): + resolution: {integrity: sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + dependencies: + loader-utils: 2.0.4 + schema-utils: 3.3.0 + webpack: 5.88.2 + dev: false + + /rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + dev: false + + /react-dev-utils@12.0.1(eslint@8.53.0)(typescript@5.3.3)(webpack@5.88.2): + resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=2.7' + webpack: '>=4' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@babel/code-frame': 7.23.5 + address: 1.2.2 + browserslist: 4.22.2 + chalk: 4.1.2 + cross-spawn: 7.0.3 + detect-port-alt: 1.1.6 + escape-string-regexp: 4.0.0 + filesize: 8.0.7 + find-up: 5.0.0 + fork-ts-checker-webpack-plugin: 6.5.3(eslint@8.53.0)(typescript@5.3.3)(webpack@5.88.2) + global-modules: 2.0.0 + globby: 11.1.0 + gzip-size: 6.0.0 + immer: 9.0.21 + is-root: 2.1.0 + loader-utils: 3.2.1 + open: 8.4.2 + pkg-up: 3.1.0 + prompts: 2.4.2 + react-error-overlay: 6.0.11 + recursive-readdir: 2.2.3 + shell-quote: 1.8.1 + strip-ansi: 6.0.1 + text-table: 0.2.0 + typescript: 5.3.3 + webpack: 5.88.2 + transitivePeerDependencies: + - eslint + - supports-color + - vue-template-compiler + dev: false + + /react-dom@18.2.0(react@18.2.0): + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.23.0 + + /react-error-overlay@6.0.11: + resolution: {integrity: sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==} + dev: false + + /react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + + /react-helmet-async@1.3.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg==} + peerDependencies: + react: ^16.6.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.6.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.22.15 + invariant: 2.2.4 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-fast-compare: 3.2.2 + shallowequal: 1.1.0 + + /react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + /react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + dev: false + + /react-json-view-lite@1.2.1(react@18.2.0): + resolution: {integrity: sha512-Itc0g86fytOmKZoIoJyGgvNqohWSbh3NXIKNgH6W6FT9PC1ck4xas1tT3Rr/b3UlFXyA9Jjaw9QSXdZy2JwGMQ==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + + /react-loadable-ssr-addon-v5-slorber@1.0.1(@docusaurus/react-loadable@5.5.2)(webpack@5.88.2): + resolution: {integrity: sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A==} + engines: {node: '>=10.13.0'} + peerDependencies: + react-loadable: '*' + webpack: '>=4.41.1 || 5.x' + dependencies: + '@babel/runtime': 7.22.15 + react-loadable: /@docusaurus/react-loadable@5.5.2(react@18.2.0) + webpack: 5.88.2 + dev: false + + /react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==} + peerDependencies: + '@popperjs/core': ^2.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + dependencies: + '@popperjs/core': 2.11.8 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-fast-compare: 3.2.2 + warning: 4.0.3 + dev: false + + /react-router-config@5.1.1(react-router@5.3.4)(react@18.2.0): + resolution: {integrity: sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==} + peerDependencies: + react: '>=15' + react-router: '>=5' + dependencies: + '@babel/runtime': 7.22.15 + react: 18.2.0 + react-router: 5.3.4(react@18.2.0) + dev: false + + /react-router-dom@5.3.4(react@18.2.0): + resolution: {integrity: sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==} + peerDependencies: + react: '>=15' + dependencies: + '@babel/runtime': 7.22.15 + history: 4.10.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-router: 5.3.4(react@18.2.0) + tiny-invariant: 1.3.1 + tiny-warning: 1.0.3 + dev: false + + /react-router@5.3.4(react@18.2.0): + resolution: {integrity: sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==} + peerDependencies: + react: '>=15' + dependencies: + '@babel/runtime': 7.22.15 + history: 4.10.1 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + path-to-regexp: 1.8.0 + prop-types: 15.8.1 + react: 18.2.0 + react-is: 16.13.1 + tiny-invariant: 1.3.1 + tiny-warning: 1.0.3 + dev: false + + /react-waypoint@10.3.0(react@18.2.0): + resolution: {integrity: sha512-iF1y2c1BsoXuEGz08NoahaLFIGI9gTUAAOKip96HUmylRT6DUtpgoBPjk/Y8dfcFVmfVDvUzWjNXpZyKTOV0SQ==} + peerDependencies: + react: ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.22.15 + consolidated-events: 2.0.2 + prop-types: 15.8.1 + react: 18.2.0 + react-is: 18.2.0 + dev: false + + /react@18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + + /read-pkg-up@8.0.0: + resolution: {integrity: sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==} + engines: {node: '>=12'} + dependencies: + find-up: 5.0.0 + read-pkg: 6.0.0 + type-fest: 1.4.0 + dev: true + + /read-pkg@6.0.0: + resolution: {integrity: sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==} + engines: {node: '>=12'} + dependencies: + '@types/normalize-package-data': 2.4.1 + normalize-package-data: 3.0.3 + parse-json: 5.2.0 + type-fest: 1.4.0 + dev: true + + /readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + dev: false + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: false + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: false + + /reading-time@1.5.0: + resolution: {integrity: sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==} + dev: false + + /rechoir@0.6.2: + resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} + engines: {node: '>= 0.10'} + dependencies: + resolve: 1.22.4 + dev: false + + /recursive-readdir@2.2.3: + resolution: {integrity: sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==} + engines: {node: '>=6.0.0'} + dependencies: + minimatch: 3.1.2 + dev: false + + /redent@4.0.0: + resolution: {integrity: sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==} + engines: {node: '>=12'} + dependencies: + indent-string: 5.0.0 + strip-indent: 4.0.0 + dev: true + + /regenerate-unicode-properties@10.1.0: + resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==} + engines: {node: '>=4'} + dependencies: + regenerate: 1.4.2 + dev: false + + /regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + dev: false + + /regenerator-runtime@0.14.0: + resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} + + /regenerator-transform@0.15.2: + resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} + dependencies: + '@babel/runtime': 7.22.15 + dev: false + + /regexp.prototype.flags@1.5.0: + resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + functions-have-names: 1.2.3 + dev: false + + /regexpu-core@5.3.2: + resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} + engines: {node: '>=4'} + dependencies: + '@babel/regjsgen': 0.8.0 + regenerate: 1.4.2 + regenerate-unicode-properties: 10.1.0 + regjsparser: 0.9.1 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.1.0 + dev: false + + /registry-auth-token@5.0.2: + resolution: {integrity: sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==} + engines: {node: '>=14'} + dependencies: + '@pnpm/npm-conf': 2.2.2 + dev: false + + /registry-url@6.0.1: + resolution: {integrity: sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==} + engines: {node: '>=12'} + dependencies: + rc: 1.2.8 + dev: false + + /regjsparser@0.9.1: + resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} + hasBin: true + dependencies: + jsesc: 0.5.0 + dev: false + + /rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + dependencies: + '@types/hast': 3.0.2 + hast-util-raw: 9.0.1 + vfile: 6.0.1 + dev: false + + /relateurl@0.2.7: + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} + dev: false + + /remark-directive@3.0.0: + resolution: {integrity: sha512-l1UyWJ6Eg1VPU7Hm/9tt0zKtReJQNOA4+iDMAxTyZNWnJnFlbS/7zhiel/rogTLQ2vMYwDzSJa4BiVNqGlqIMA==} + dependencies: + '@types/mdast': 4.0.2 + mdast-util-directive: 3.0.0 + micromark-extension-directive: 3.0.0 + unified: 11.0.4 + transitivePeerDependencies: + - supports-color + dev: false + + /remark-emoji@4.0.1: + resolution: {integrity: sha512-fHdvsTR1dHkWKev9eNyhTo4EFwbUvJ8ka9SgeWkMPYFX4WoI7ViVBms3PjlQYgw5TLvNQso3GUB/b/8t3yo+dg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + '@types/mdast': 4.0.2 + emoticon: 4.0.1 + mdast-util-find-and-replace: 3.0.1 + node-emoji: 2.1.0 + unified: 11.0.4 + dev: false + + /remark-frontmatter@5.0.0: + resolution: {integrity: sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==} + dependencies: + '@types/mdast': 4.0.2 + mdast-util-frontmatter: 2.0.1 + micromark-extension-frontmatter: 2.0.0 + unified: 11.0.4 + transitivePeerDependencies: + - supports-color + dev: false + + /remark-gfm@4.0.0: + resolution: {integrity: sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==} + dependencies: + '@types/mdast': 4.0.2 + mdast-util-gfm: 3.0.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.4 + transitivePeerDependencies: + - supports-color + dev: false + + /remark-mdx@3.0.0: + resolution: {integrity: sha512-O7yfjuC6ra3NHPbRVxfflafAj3LTwx3b73aBvkEFU5z4PsD6FD4vrqJAkE5iNGLz71GdjXfgRqm3SQ0h0VuE7g==} + dependencies: + mdast-util-mdx: 3.0.0 + micromark-extension-mdxjs: 3.0.0 + transitivePeerDependencies: + - supports-color + + /remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + dependencies: + '@types/mdast': 4.0.2 + mdast-util-from-markdown: 2.0.0 + micromark-util-types: 2.0.0 + unified: 11.0.4 + transitivePeerDependencies: + - supports-color + + /remark-rehype@11.0.0: + resolution: {integrity: sha512-vx8x2MDMcxuE4lBmQ46zYUDfcFMmvg80WYX+UNLeG6ixjdCCLcw1lrgAukwBTuOFsS78eoAedHGn9sNM0w7TPw==} + dependencies: + '@types/hast': 3.0.2 + '@types/mdast': 4.0.2 + mdast-util-to-hast: 13.0.2 + unified: 11.0.4 + vfile: 6.0.1 + + /remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + dependencies: + '@types/mdast': 4.0.2 + mdast-util-to-markdown: 2.1.0 + unified: 11.0.4 + dev: false + + /renderkid@3.0.0: + resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==} + dependencies: + css-select: 4.3.0 + dom-converter: 0.2.0 + htmlparser2: 6.1.0 + lodash: 4.17.21 + strip-ansi: 6.0.1 + dev: false + + /require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + /require-like@0.1.2: + resolution: {integrity: sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==} + dev: false + + /requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: false + + /resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + dev: false + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + dev: true + + /resolve-package-path@4.0.3: + resolution: {integrity: sha512-SRpNAPW4kewOaNUt8VPqhJ0UMxawMwzJD8V7m1cJfdSTK9ieZwS6K7Dabsm4bmLFM96Z5Y/UznrpG5kt1im8yA==} + engines: {node: '>= 12'} + dependencies: + path-root: 0.1.1 + dev: false + + /resolve-pathname@3.0.0: + resolution: {integrity: sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==} + dev: false + + /resolve@1.22.4: + resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==} + hasBin: true + dependencies: + is-core-module: 2.13.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: false + + /responselike@3.0.0: + resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} + engines: {node: '>=14.16'} + dependencies: + lowercase-keys: 3.0.0 + dev: false + + /restore-cursor@4.0.0: + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + dev: false + + /retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + dev: false + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + + /rollup-plugin-terser@7.0.2(rollup@2.79.1): + resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==} + deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser + peerDependencies: + rollup: ^2.0.0 + dependencies: + '@babel/code-frame': 7.23.5 + jest-worker: 26.6.2 + rollup: 2.79.1 + serialize-javascript: 4.0.0 + terser: 5.19.4 + dev: false + + /rollup@2.79.1: + resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==} + engines: {node: '>=10.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.3 + dev: false + + /rtl-detect@1.0.4: + resolution: {integrity: sha512-EBR4I2VDSSYr7PkBmFy04uhycIpDKp+21p/jARYXlCSjQksTBQcJ0HFUPOO79EPPH5JS6VAhiIQbycf0O3JAxQ==} + dev: false + + /rtlcss@4.1.1: + resolution: {integrity: sha512-/oVHgBtnPNcggP2aVXQjSy6N1mMAfHg4GSag0QtZBlD5bdDgAHwr4pydqJGd+SUCu9260+Pjqbjwtvu7EMH1KQ==} + engines: {node: '>=12.0.0'} + hasBin: true + dependencies: + escalade: 3.1.1 + picocolors: 1.0.0 + postcss: 8.4.29 + strip-json-comments: 3.1.1 + dev: false + + /run-applescript@5.0.0: + resolution: {integrity: sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==} + engines: {node: '>=12'} + dependencies: + execa: 5.1.1 + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + + /safe-array-concat@1.0.1: + resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: false + + /safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: false + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + /safe-regex-test@1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-regex: 1.1.4 + dev: false + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + + /sass-loader@10.4.1(sass@1.66.1)(webpack@5.88.2): + resolution: {integrity: sha512-aX/iJZTTpNUNx/OSYzo2KsjIUQHqvWsAhhUijFjAPdZTEhstjZI9zTNvkTTwsx+uNUJqUwOw5gacxQMx4hJxGQ==} + engines: {node: '>= 10.13.0'} + peerDependencies: + fibers: '>= 3.1.0' + node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + sass: ^1.3.0 + webpack: ^4.36.0 || ^5.0.0 + peerDependenciesMeta: + fibers: + optional: true + node-sass: + optional: true + sass: + optional: true + dependencies: + klona: 2.0.6 + loader-utils: 2.0.4 + neo-async: 2.6.2 + sass: 1.66.1 + schema-utils: 3.3.0 + semver: 7.5.4 + webpack: 5.88.2 + dev: false + + /sass@1.66.1: + resolution: {integrity: sha512-50c+zTsZOJVgFfTgwwEzkjA3/QACgdNsKueWPyAR0mRINIvLAStVQBbPg14iuqEQ74NPDbXzJARJ/O4SI1zftA==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + chokidar: 3.5.3 + immutable: 4.3.4 + source-map-js: 1.0.2 + dev: false + + /sax@1.2.4: + resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} + dev: false + + /scheduler@0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + dependencies: + loose-envify: 1.4.0 + + /schema-utils@2.7.0: + resolution: {integrity: sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==} + engines: {node: '>= 8.9.0'} + dependencies: + '@types/json-schema': 7.0.12 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + dev: false + + /schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/json-schema': 7.0.12 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + + /schema-utils@4.2.0: + resolution: {integrity: sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==} + engines: {node: '>= 12.13.0'} + dependencies: + '@types/json-schema': 7.0.12 + ajv: 8.12.0 + ajv-formats: 2.1.1(ajv@8.12.0) + ajv-keywords: 5.1.0(ajv@8.12.0) + dev: false + + /search-insights@2.8.2: + resolution: {integrity: sha512-PxA9M5Q2bpBelVvJ3oDZR8nuY00Z6qwOxL53wNpgzV28M/D6u9WUbImDckjLSILBF8F1hn/mgyuUaOPtjow4Qw==} + dev: false + + /section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + dev: false + + /select-hose@2.0.0: + resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==} + dev: false + + /selfsigned@2.1.1: + resolution: {integrity: sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==} + engines: {node: '>=10'} + dependencies: + node-forge: 1.3.1 + dev: false + + /semver-diff@4.0.0: + resolution: {integrity: sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==} + engines: {node: '>=12'} + dependencies: + semver: 7.5.4 + dev: false + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: false + + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + + /send@0.18.0: + resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /serialize-javascript@4.0.0: + resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==} + dependencies: + randombytes: 2.1.0 + dev: false + + /serialize-javascript@6.0.1: + resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} + dependencies: + randombytes: 2.1.0 + + /serve-handler@6.1.5: + resolution: {integrity: sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg==} + dependencies: + bytes: 3.0.0 + content-disposition: 0.5.2 + fast-url-parser: 1.1.3 + mime-types: 2.1.18 + minimatch: 3.1.2 + path-is-inside: 1.0.2 + path-to-regexp: 2.2.1 + range-parser: 1.2.0 + dev: false + + /serve-index@1.9.1: + resolution: {integrity: sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==} + engines: {node: '>= 0.8.0'} + dependencies: + accepts: 1.3.8 + batch: 0.6.1 + debug: 2.6.9 + escape-html: 1.0.3 + http-errors: 1.6.3 + mime-types: 2.1.35 + parseurl: 1.3.3 + transitivePeerDependencies: + - supports-color + dev: false + + /serve-static@1.15.0: + resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 1.0.2 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.18.0 + transitivePeerDependencies: + - supports-color + dev: false + + /server-only@0.0.1: + resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + dev: false + + /setprototypeof@1.1.0: + resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} + dev: false + + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + dev: false + + /shallow-clone@3.0.1: + resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} + engines: {node: '>=8'} + dependencies: + kind-of: 6.0.3 + + /shallowequal@1.1.0: + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + + /sharp@0.32.6: + resolution: {integrity: sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==} + engines: {node: '>=14.15.0'} + requiresBuild: true + dependencies: + color: 4.2.3 + detect-libc: 2.0.2 + node-addon-api: 6.1.0 + prebuild-install: 7.1.1 + semver: 7.5.4 + simple-get: 4.0.1 + tar-fs: 3.0.4 + tunnel-agent: 0.6.0 + dev: false + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + /shell-quote@1.8.1: + resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} + dev: false + + /shelljs@0.8.5: + resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} + engines: {node: '>=4'} + hasBin: true + dependencies: + glob: 7.2.3 + interpret: 1.4.0 + rechoir: 0.6.2 + dev: false + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + object-inspect: 1.12.3 + dev: false + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + + /simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + dev: false + + /simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + dev: false + + /simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + dependencies: + is-arrayish: 0.3.2 + dev: false + + /sirv@2.0.3: + resolution: {integrity: sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==} + engines: {node: '>= 10'} + dependencies: + '@polka/url': 1.0.0-next.21 + mrmime: 1.0.1 + totalist: 3.0.1 + dev: false + + /sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + dev: false + + /sitemap@7.1.1: + resolution: {integrity: sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg==} + engines: {node: '>=12.0.0', npm: '>=5.6.0'} + hasBin: true + dependencies: + '@types/node': 17.0.45 + '@types/sax': 1.2.6 + arg: 5.0.2 + sax: 1.2.4 + dev: false + + /skin-tone@2.0.0: + resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} + engines: {node: '>=8'} + dependencies: + unicode-emoji-modifier-base: 1.0.0 + dev: false + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + /slash@4.0.0: + resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} + engines: {node: '>=12'} + dev: false + + /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 + + /sockjs@0.3.24: + resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==} + dependencies: + faye-websocket: 0.11.4 + uuid: 8.3.2 + websocket-driver: 0.7.4 + dev: false + + /sort-css-media-queries@2.1.0: + resolution: {integrity: sha512-IeWvo8NkNiY2vVYdPa27MCQiR0MN0M80johAYFVxWWXQ44KU84WNxjslwBHmc/7ZL2ccwkM7/e6S5aiKZXm7jA==} + engines: {node: '>= 6.3.0'} + dev: false + + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + /source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + /source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + dependencies: + whatwg-url: 7.1.0 + dev: false + + /sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + dev: false + + /space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + /spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.13 + dev: true + + /spdx-exceptions@2.3.0: + resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} + dev: true + + /spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + dependencies: + spdx-exceptions: 2.3.0 + spdx-license-ids: 3.0.13 + dev: true + + /spdx-license-ids@3.0.13: + resolution: {integrity: sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==} + dev: true + + /spdy-transport@3.0.0: + resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==} + dependencies: + debug: 4.3.4 + detect-node: 2.1.0 + hpack.js: 2.1.6 + obuf: 1.1.2 + readable-stream: 3.6.2 + wbuf: 1.7.3 + transitivePeerDependencies: + - supports-color + dev: false + + /spdy@4.0.2: + resolution: {integrity: sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==} + engines: {node: '>=6.0.0'} + dependencies: + debug: 4.3.4 + handle-thing: 2.0.1 + http-deceiver: 1.2.7 + select-hose: 2.0.0 + spdy-transport: 3.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: false + + /srcset@4.0.0: + resolution: {integrity: sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==} + engines: {node: '>=12'} + dev: false + + /stable@0.1.8: + resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} + deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' + dev: false + + /statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + dev: false + + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: false + + /std-env@3.4.3: + resolution: {integrity: sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==} + dev: false + + /stdin-discarder@0.1.0: + resolution: {integrity: sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + bl: 5.1.0 + dev: false + + /streamx@2.15.2: + resolution: {integrity: sha512-b62pAV/aeMjUoRN2C/9F0n+G8AfcJjNC0zw/ZmOHeFsIe4m4GzjVW9m6VHXVjk536NbdU9JRwKMJRfkc+zUFTg==} + dependencies: + fast-fifo: 1.3.2 + queue-tick: 1.0.1 + dev: false + + /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 + + /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: false + + /string-width@6.1.0: + resolution: {integrity: sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==} + engines: {node: '>=16'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 10.3.0 + strip-ansi: 7.1.0 + dev: false + + /string.prototype.matchall@4.0.9: + resolution: {integrity: sha512-6i5hL3MqG/K2G43mWXWgP+qizFW/QH/7kCNN13JrJS5q48FN5IKksLDscexKP3dnmB6cdm9jlNgAsWNLpSykmA==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + regexp.prototype.flags: 1.5.0 + side-channel: 1.0.4 + dev: false + + /string.prototype.trim@1.2.7: + resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: false + + /string.prototype.trimend@1.0.6: + resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: false + + /string.prototype.trimstart@1.0.7: + resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: false + + /string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + dependencies: + safe-buffer: 5.1.2 + dev: false + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /stringify-entities@4.0.3: + resolution: {integrity: sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==} + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + /stringify-object@3.3.0: + resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} + engines: {node: '>=4'} + dependencies: + get-own-enumerable-property-symbols: 3.0.2 + is-obj: 1.0.1 + is-regexp: 1.0.0 + dev: false + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: false + + /strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + dev: false + + /strip-comments@2.0.1: + resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} + engines: {node: '>=10'} + dev: false + + /strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + /strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + dev: true + + /strip-indent@4.0.0: + resolution: {integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==} + engines: {node: '>=12'} + dependencies: + min-indent: 1.0.1 + dev: true + + /strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: false + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + /style-search@0.1.0: + resolution: {integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==} + dev: true + + /style-to-object@0.4.2: + resolution: {integrity: sha512-1JGpfPB3lo42ZX8cuPrheZbfQ6kqPPnPHlKMyeRYtfKD+0jG+QsXgXN57O/dvJlzlB2elI6dGmrPnl5VPQFPaA==} + dependencies: + inline-style-parser: 0.1.1 + + /stylehacks@5.1.1(postcss@8.4.29): + resolution: {integrity: sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.22.2 + postcss: 8.4.29 + postcss-selector-parser: 6.0.13 + dev: false + + /stylelint-config-prettier-scss@1.0.0(stylelint@15.11.0): + resolution: {integrity: sha512-Gr2qLiyvJGKeDk0E/+awNTrZB/UtNVPLqCDOr07na/sLekZwm26Br6yYIeBYz3ulsEcQgs5j+2IIMXCC+wsaQA==} + engines: {node: 14.* || 16.* || >= 18} + hasBin: true + peerDependencies: + stylelint: '>=15.0.0' + dependencies: + stylelint: 15.11.0(typescript@5.3.3) + dev: true + + /stylelint-config-recommended-scss@13.1.0(postcss@8.4.29)(stylelint@15.11.0): + resolution: {integrity: sha512-8L5nDfd+YH6AOoBGKmhH8pLWF1dpfY816JtGMePcBqqSsLU+Ysawx44fQSlMOJ2xTfI9yTGpup5JU77c17w1Ww==} + peerDependencies: + postcss: ^8.3.3 + stylelint: ^15.10.0 + peerDependenciesMeta: + postcss: + optional: true + dependencies: + postcss: 8.4.29 + postcss-scss: 4.0.9(postcss@8.4.29) + stylelint: 15.11.0(typescript@5.3.3) + stylelint-config-recommended: 13.0.0(stylelint@15.11.0) + stylelint-scss: 5.3.1(stylelint@15.11.0) + dev: true + + /stylelint-config-recommended@13.0.0(stylelint@15.11.0): + resolution: {integrity: sha512-EH+yRj6h3GAe/fRiyaoO2F9l9Tgg50AOFhaszyfov9v6ayXJ1IkSHwTxd7lB48FmOeSGDPLjatjO11fJpmarkQ==} + engines: {node: ^14.13.1 || >=16.0.0} + peerDependencies: + stylelint: ^15.10.0 + dependencies: + stylelint: 15.11.0(typescript@5.3.3) + dev: true + + /stylelint-config-standard-scss@11.1.0(postcss@8.4.29)(stylelint@15.11.0): + resolution: {integrity: sha512-5gnBgeNTgRVdchMwiFQPuBOtj9QefYtfXiddrOMJA2pI22zxt6ddI2s+e5Oh7/6QYl7QLJujGnaUR5YyGq72ow==} + peerDependencies: + postcss: ^8.3.3 + stylelint: ^15.10.0 + peerDependenciesMeta: + postcss: + optional: true + dependencies: + postcss: 8.4.29 + stylelint: 15.11.0(typescript@5.3.3) + stylelint-config-recommended-scss: 13.1.0(postcss@8.4.29)(stylelint@15.11.0) + stylelint-config-standard: 34.0.0(stylelint@15.11.0) + dev: true + + /stylelint-config-standard@34.0.0(stylelint@15.11.0): + resolution: {integrity: sha512-u0VSZnVyW9VSryBG2LSO+OQTjN7zF9XJaAJRX/4EwkmU0R2jYwmBSN10acqZisDitS0CLiEiGjX7+Hrq8TAhfQ==} + engines: {node: ^14.13.1 || >=16.0.0} + peerDependencies: + stylelint: ^15.10.0 + dependencies: + stylelint: 15.11.0(typescript@5.3.3) + stylelint-config-recommended: 13.0.0(stylelint@15.11.0) + dev: true + + /stylelint-scss@5.3.1(stylelint@15.11.0): + resolution: {integrity: sha512-5I9ZDIm77BZrjOccma5WyW2nJEKjXDd4Ca8Kk+oBapSO4pewSlno3n+OyimcyVJJujQZkBN2D+xuMkIamSc6hA==} + peerDependencies: + stylelint: ^14.5.1 || ^15.0.0 + dependencies: + known-css-properties: 0.29.0 + postcss-media-query-parser: 0.2.3 + postcss-resolve-nested-selector: 0.1.1 + postcss-selector-parser: 6.0.13 + postcss-value-parser: 4.2.0 + stylelint: 15.11.0(typescript@5.3.3) + dev: true + + /stylelint@15.11.0(typescript@5.3.3): + resolution: {integrity: sha512-78O4c6IswZ9TzpcIiQJIN49K3qNoXTM8zEJzhaTE/xRTCZswaovSEVIa/uwbOltZrk16X4jAxjaOhzz/hTm1Kw==} + engines: {node: ^14.13.1 || >=16.0.0} + hasBin: true + dependencies: + '@csstools/css-parser-algorithms': 2.3.2(@csstools/css-tokenizer@2.2.1) + '@csstools/css-tokenizer': 2.2.1 + '@csstools/media-query-list-parser': 2.1.5(@csstools/css-parser-algorithms@2.3.2)(@csstools/css-tokenizer@2.2.1) + '@csstools/selector-specificity': 3.0.0(postcss-selector-parser@6.0.13) + balanced-match: 2.0.0 + colord: 2.9.3 + cosmiconfig: 8.3.4(typescript@5.3.3) + css-functions-list: 3.2.1 + css-tree: 2.3.1 + debug: 4.3.4 + fast-glob: 3.3.1 + fastest-levenshtein: 1.0.16 + file-entry-cache: 7.0.2 + global-modules: 2.0.0 + globby: 11.1.0 + globjoin: 0.1.4 + html-tags: 3.3.1 + ignore: 5.2.4 + import-lazy: 4.0.0 + imurmurhash: 0.1.4 + is-plain-object: 5.0.0 + known-css-properties: 0.29.0 + mathml-tag-names: 2.1.3 + meow: 10.1.5 + micromatch: 4.0.5 + normalize-path: 3.0.0 + picocolors: 1.0.0 + postcss: 8.4.29 + postcss-resolve-nested-selector: 0.1.1 + postcss-safe-parser: 6.0.0(postcss@8.4.29) + postcss-selector-parser: 6.0.13 + postcss-value-parser: 4.2.0 + resolve-from: 5.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + style-search: 0.1.0 + supports-hyperlinks: 3.0.0 + svg-tags: 1.0.0 + table: 6.8.1 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + + /supports-hyperlinks@3.0.0: + resolution: {integrity: sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA==} + engines: {node: '>=14.18'} + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: false + + /svg-parser@2.0.4: + resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} + dev: false + + /svg-tags@1.0.0: + resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + dev: true + + /svgo@2.8.0: + resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==} + engines: {node: '>=10.13.0'} + hasBin: true + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 4.3.0 + css-tree: 1.1.3 + csso: 4.2.0 + picocolors: 1.0.0 + stable: 0.1.8 + dev: false + + /synckit@0.8.5: + resolution: {integrity: sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==} + engines: {node: ^14.18.0 || >=16.0.0} + dependencies: + '@pkgr/utils': 2.4.2 + tslib: 2.6.2 + dev: true + + /table@6.8.1: + resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==} + engines: {node: '>=10.0.0'} + dependencies: + ajv: 8.12.0 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /tapable@1.1.3: + resolution: {integrity: sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==} + engines: {node: '>=6'} + dev: false + + /tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + + /tar-fs@2.1.1: + resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 2.2.0 + dev: false + + /tar-fs@3.0.4: + resolution: {integrity: sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==} + dependencies: + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 3.1.6 + dev: false + + /tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: false + + /tar-stream@3.1.6: + resolution: {integrity: sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==} + dependencies: + b4a: 1.6.4 + fast-fifo: 1.3.2 + streamx: 2.15.2 + dev: false + + /temp-dir@2.0.0: + resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} + engines: {node: '>=8'} + dev: false + + /tempy@0.6.0: + resolution: {integrity: sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==} + engines: {node: '>=10'} + dependencies: + is-stream: 2.0.1 + temp-dir: 2.0.0 + type-fest: 0.16.0 + unique-string: 2.0.0 + dev: false + + /terser-webpack-plugin@5.3.9(webpack@5.88.2): + resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + dependencies: + '@jridgewell/trace-mapping': 0.3.19 + jest-worker: 27.5.1 + schema-utils: 3.3.0 + serialize-javascript: 6.0.1 + terser: 5.19.4 + webpack: 5.88.2 + + /terser@5.19.4: + resolution: {integrity: sha512-6p1DjHeuluwxDXcuT9VR8p64klWJKo1ILiy19s6C9+0Bh2+NWTX6nD9EPppiER4ICkHDVB1RkVpin/YW2nQn/g==} + engines: {node: '>=10'} + hasBin: true + dependencies: + '@jridgewell/source-map': 0.3.5 + acorn: 8.10.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + /thunky@1.1.0: + resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} + dev: false + + /tiny-invariant@1.3.1: + resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} + dev: false + + /tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + dev: false + + /titleize@3.0.0: + resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} + engines: {node: '>=12'} + dev: true + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + dev: false + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + dev: false + + /totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + dev: false + + /tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + dependencies: + punycode: 2.3.0 + dev: false + + /trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + /trim-newlines@4.1.1: + resolution: {integrity: sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==} + engines: {node: '>=12'} + dev: true + + /trough@2.1.0: + resolution: {integrity: sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==} + + /ts-api-utils@1.0.3(typescript@5.3.3): + resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} + engines: {node: '>=16.13.0'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.3.3 + dev: true + + /tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + dev: true + + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + + /tsutils@3.21.0(typescript@5.3.3): + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + dependencies: + tslib: 1.14.1 + typescript: 5.3.3 + dev: true + + /tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + + /type-fest@0.16.0: + resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} + engines: {node: '>=10'} + dev: false + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + /type-fest@1.4.0: + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} + + /type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + dev: false + + /type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + dev: false + + /typed-array-buffer@1.0.0: + resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + dev: false + + /typed-array-byte-length@1.0.0: + resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: false + + /typed-array-byte-offset@1.0.0: + resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: false + + /typed-array-length@1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + is-typed-array: 1.1.12 + dev: false + + /typedarray-to-buffer@3.1.5: + resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + dependencies: + is-typedarray: 1.0.0 + dev: false + + /typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + engines: {node: '>=14.17'} + hasBin: true + + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.2 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: false + + /unicode-canonical-property-names-ecmascript@2.0.0: + resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} + engines: {node: '>=4'} + dev: false + + /unicode-emoji-modifier-base@1.0.0: + resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} + engines: {node: '>=4'} + dev: false + + /unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.0 + unicode-property-aliases-ecmascript: 2.1.0 + dev: false + + /unicode-match-property-value-ecmascript@2.1.0: + resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} + engines: {node: '>=4'} + dev: false + + /unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + dev: false + + /unified@11.0.4: + resolution: {integrity: sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==} + dependencies: + '@types/unist': 3.0.1 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.1.0 + vfile: 6.0.1 + + /unique-string@2.0.0: + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + engines: {node: '>=8'} + dependencies: + crypto-random-string: 2.0.0 + dev: false + + /unique-string@3.0.0: + resolution: {integrity: sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==} + engines: {node: '>=12'} + dependencies: + crypto-random-string: 4.0.0 + dev: false + + /unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + dependencies: + '@types/unist': 3.0.1 + + /unist-util-position-from-estree@2.0.0: + resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} + dependencies: + '@types/unist': 3.0.1 + + /unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + dependencies: + '@types/unist': 3.0.1 + + /unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + dependencies: + '@types/unist': 3.0.1 + unist-util-visit: 5.0.0 + + /unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + dependencies: + '@types/unist': 3.0.1 + + /unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + dependencies: + '@types/unist': 3.0.1 + unist-util-is: 6.0.0 + + /unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + dependencies: + '@types/unist': 3.0.1 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + /universalify@2.0.0: + resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} + engines: {node: '>= 10.0.0'} + dev: false + + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + dev: false + + /untildify@4.0.0: + resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} + engines: {node: '>=8'} + dev: true + + /upath@1.2.0: + resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} + engines: {node: '>=4'} + dev: false + + /update-browserslist-db@1.0.13(browserslist@4.22.2): + resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.22.2 + escalade: 3.1.1 + picocolors: 1.0.0 + + /update-notifier@6.0.2: + resolution: {integrity: sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==} + engines: {node: '>=14.16'} + dependencies: + boxen: 7.1.1 + chalk: 5.3.0 + configstore: 6.0.0 + has-yarn: 3.0.0 + import-lazy: 4.0.0 + is-ci: 3.0.1 + is-installed-globally: 0.4.0 + is-npm: 6.0.0 + is-yarn-global: 0.4.1 + latest-version: 7.0.0 + pupa: 3.1.0 + semver: 7.5.4 + semver-diff: 4.0.0 + xdg-basedir: 5.1.0 + dev: false + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.0 + + /url-loader@4.1.1(file-loader@6.2.0)(webpack@5.88.2): + resolution: {integrity: sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==} + engines: {node: '>= 10.13.0'} + peerDependencies: + file-loader: '*' + webpack: ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + file-loader: + optional: true + dependencies: + file-loader: 6.2.0(webpack@5.88.2) + loader-utils: 2.0.4 + mime-types: 2.1.35 + schema-utils: 3.3.0 + webpack: 5.88.2 + dev: false + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + /utila@0.4.0: + resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} + dev: false + + /utility-types@3.10.0: + resolution: {integrity: sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==} + engines: {node: '>= 4'} + + /utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + dev: false + + /uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + dev: false + + /validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + dev: true + + /validate-peer-dependencies@2.2.0: + resolution: {integrity: sha512-8X1OWlERjiUY6P6tdeU9E0EwO8RA3bahoOVG7ulOZT5MqgNDUO/BQoVjYiHPcNe+v8glsboZRIw9iToMAA2zAA==} + engines: {node: '>= 12'} + dependencies: + resolve-package-path: 4.0.3 + semver: 7.5.4 + dev: false + + /value-equal@1.0.1: + resolution: {integrity: sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==} + dev: false + + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + dev: false + + /vfile-location@5.0.2: + resolution: {integrity: sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==} + dependencies: + '@types/unist': 3.0.1 + vfile: 6.0.1 + dev: false + + /vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + dependencies: + '@types/unist': 3.0.1 + unist-util-stringify-position: 4.0.0 + + /vfile@6.0.1: + resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==} + dependencies: + '@types/unist': 3.0.1 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 + + /warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + dependencies: + loose-envify: 1.4.0 + dev: false + + /watchpack@2.4.0: + resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} + engines: {node: '>=10.13.0'} + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + + /wbuf@1.7.3: + resolution: {integrity: sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==} + dependencies: + minimalistic-assert: 1.0.1 + dev: false + + /web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + dev: false + + /webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + dev: false + + /webpack-bundle-analyzer@4.9.1: + resolution: {integrity: sha512-jnd6EoYrf9yMxCyYDPj8eutJvtjQNp8PHmni/e/ulydHBWhT5J3menXt3HEkScsu9YqMAcG4CfFjs3rj5pVU1w==} + engines: {node: '>= 10.13.0'} + hasBin: true + dependencies: + '@discoveryjs/json-ext': 0.5.7 + acorn: 8.10.0 + acorn-walk: 8.2.0 + commander: 7.2.0 + escape-string-regexp: 4.0.0 + gzip-size: 6.0.0 + is-plain-object: 5.0.0 + lodash.debounce: 4.0.8 + lodash.escape: 4.0.1 + lodash.flatten: 4.4.0 + lodash.invokemap: 4.6.0 + lodash.pullall: 4.2.0 + lodash.uniqby: 4.7.0 + opener: 1.5.2 + picocolors: 1.0.0 + sirv: 2.0.3 + ws: 7.5.9 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + + /webpack-dev-middleware@5.3.3(webpack@5.88.2): + resolution: {integrity: sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + dependencies: + colorette: 2.0.20 + memfs: 3.5.3 + mime-types: 2.1.35 + range-parser: 1.2.1 + schema-utils: 4.2.0 + webpack: 5.88.2 + dev: false + + /webpack-dev-server@4.15.1(webpack@5.88.2): + resolution: {integrity: sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==} + engines: {node: '>= 12.13.0'} + hasBin: true + peerDependencies: + webpack: ^4.37.0 || ^5.0.0 + webpack-cli: '*' + peerDependenciesMeta: + webpack: + optional: true + webpack-cli: + optional: true + dependencies: + '@types/bonjour': 3.5.10 + '@types/connect-history-api-fallback': 1.5.1 + '@types/express': 4.17.17 + '@types/serve-index': 1.9.1 + '@types/serve-static': 1.15.2 + '@types/sockjs': 0.3.33 + '@types/ws': 8.5.5 + ansi-html-community: 0.0.8 + bonjour-service: 1.1.1 + chokidar: 3.5.3 + colorette: 2.0.20 + compression: 1.7.4 + connect-history-api-fallback: 2.0.0 + default-gateway: 6.0.3 + express: 4.18.2 + graceful-fs: 4.2.11 + html-entities: 2.4.0 + http-proxy-middleware: 2.0.6(@types/express@4.17.17) + ipaddr.js: 2.1.0 + launch-editor: 2.6.0 + open: 8.4.2 + p-retry: 4.6.2 + rimraf: 3.0.2 + schema-utils: 4.2.0 + selfsigned: 2.1.1 + serve-index: 1.9.1 + sockjs: 0.3.24 + spdy: 4.0.2 + webpack: 5.88.2 + webpack-dev-middleware: 5.3.3(webpack@5.88.2) + ws: 8.13.0 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + dev: false + + /webpack-merge@5.9.0: + resolution: {integrity: sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==} + engines: {node: '>=10.0.0'} + dependencies: + clone-deep: 4.0.1 + wildcard: 2.0.1 + + /webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + + /webpack@5.88.2: + resolution: {integrity: sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + dependencies: + '@types/eslint-scope': 3.7.4 + '@types/estree': 1.0.1 + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/wasm-edit': 1.11.6 + '@webassemblyjs/wasm-parser': 1.11.6 + acorn: 8.10.0 + acorn-import-assertions: 1.9.0(acorn@8.10.0) + browserslist: 4.22.2 + chrome-trace-event: 1.0.3 + enhanced-resolve: 5.15.0 + es-module-lexer: 1.3.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.9(webpack@5.88.2) + watchpack: 2.4.0 + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + + /webpackbar@5.0.2(webpack@5.88.2): + resolution: {integrity: sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==} + engines: {node: '>=12'} + peerDependencies: + webpack: 3 || 4 || 5 + dependencies: + chalk: 4.1.2 + consola: 2.15.3 + pretty-time: 1.1.0 + std-env: 3.4.3 + webpack: 5.88.2 + dev: false + + /websocket-driver@0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} + dependencies: + http-parser-js: 0.5.8 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + dev: false + + /websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + dev: false + + /whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + dev: false + + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: false + + /which-typed-array@1.1.11: + resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: false + + /which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + dependencies: + isexe: 2.0.0 + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + + /widest-line@4.0.1: + resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + dev: false + + /wildcard@2.0.1: + resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} + + /workbox-background-sync@7.0.0: + resolution: {integrity: sha512-S+m1+84gjdueM+jIKZ+I0Lx0BDHkk5Nu6a3kTVxP4fdj3gKouRNmhO8H290ybnJTOPfBDtTMXSQA/QLTvr7PeA==} + dependencies: + idb: 7.1.1 + workbox-core: 7.0.0 + dev: false + + /workbox-broadcast-update@7.0.0: + resolution: {integrity: sha512-oUuh4jzZrLySOo0tC0WoKiSg90bVAcnE98uW7F8GFiSOXnhogfNDGZelPJa+6KpGBO5+Qelv04Hqx2UD+BJqNQ==} + dependencies: + workbox-core: 7.0.0 + dev: false + + /workbox-build@7.0.0: + resolution: {integrity: sha512-CttE7WCYW9sZC+nUYhQg3WzzGPr4IHmrPnjKiu3AMXsiNQKx+l4hHl63WTrnicLmKEKHScWDH8xsGBdrYgtBzg==} + engines: {node: '>=16.0.0'} + dependencies: + '@apideck/better-ajv-errors': 0.3.6(ajv@8.12.0) + '@babel/core': 7.23.5 + '@babel/preset-env': 7.23.5(@babel/core@7.23.5) + '@babel/runtime': 7.22.15 + '@rollup/plugin-babel': 5.3.1(@babel/core@7.23.5)(rollup@2.79.1) + '@rollup/plugin-node-resolve': 11.2.1(rollup@2.79.1) + '@rollup/plugin-replace': 2.4.2(rollup@2.79.1) + '@surma/rollup-plugin-off-main-thread': 2.2.3 + ajv: 8.12.0 + common-tags: 1.8.2 + fast-json-stable-stringify: 2.1.0 + fs-extra: 9.1.0 + glob: 7.2.3 + lodash: 4.17.21 + pretty-bytes: 5.6.0 + rollup: 2.79.1 + rollup-plugin-terser: 7.0.2(rollup@2.79.1) + source-map: 0.8.0-beta.0 + stringify-object: 3.3.0 + strip-comments: 2.0.1 + tempy: 0.6.0 + upath: 1.2.0 + workbox-background-sync: 7.0.0 + workbox-broadcast-update: 7.0.0 + workbox-cacheable-response: 7.0.0 + workbox-core: 7.0.0 + workbox-expiration: 7.0.0 + workbox-google-analytics: 7.0.0 + workbox-navigation-preload: 7.0.0 + workbox-precaching: 7.0.0 + workbox-range-requests: 7.0.0 + workbox-recipes: 7.0.0 + workbox-routing: 7.0.0 + workbox-strategies: 7.0.0 + workbox-streams: 7.0.0 + workbox-sw: 7.0.0 + workbox-window: 7.0.0 + transitivePeerDependencies: + - '@types/babel__core' + - supports-color + dev: false + + /workbox-cacheable-response@7.0.0: + resolution: {integrity: sha512-0lrtyGHn/LH8kKAJVOQfSu3/80WDc9Ma8ng0p2i/5HuUndGttH+mGMSvOskjOdFImLs2XZIimErp7tSOPmu/6g==} + dependencies: + workbox-core: 7.0.0 + dev: false + + /workbox-core@7.0.0: + resolution: {integrity: sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ==} + dev: false + + /workbox-expiration@7.0.0: + resolution: {integrity: sha512-MLK+fogW+pC3IWU9SFE+FRStvDVutwJMR5if1g7oBJx3qwmO69BNoJQVaMXq41R0gg3MzxVfwOGKx3i9P6sOLQ==} + dependencies: + idb: 7.1.1 + workbox-core: 7.0.0 + dev: false + + /workbox-google-analytics@7.0.0: + resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==} + dependencies: + workbox-background-sync: 7.0.0 + workbox-core: 7.0.0 + workbox-routing: 7.0.0 + workbox-strategies: 7.0.0 + dev: false + + /workbox-navigation-preload@7.0.0: + resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==} + dependencies: + workbox-core: 7.0.0 + dev: false + + /workbox-precaching@7.0.0: + resolution: {integrity: sha512-EC0vol623LJqTJo1mkhD9DZmMP604vHqni3EohhQVwhJlTgyKyOkMrZNy5/QHfOby+39xqC01gv4LjOm4HSfnA==} + dependencies: + workbox-core: 7.0.0 + workbox-routing: 7.0.0 + workbox-strategies: 7.0.0 + dev: false + + /workbox-range-requests@7.0.0: + resolution: {integrity: sha512-SxAzoVl9j/zRU9OT5+IQs7pbJBOUOlriB8Gn9YMvi38BNZRbM+RvkujHMo8FOe9IWrqqwYgDFBfv6sk76I1yaQ==} + dependencies: + workbox-core: 7.0.0 + dev: false + + /workbox-recipes@7.0.0: + resolution: {integrity: sha512-DntcK9wuG3rYQOONWC0PejxYYIDHyWWZB/ueTbOUDQgefaeIj1kJ7pdP3LZV2lfrj8XXXBWt+JDRSw1lLLOnww==} + dependencies: + workbox-cacheable-response: 7.0.0 + workbox-core: 7.0.0 + workbox-expiration: 7.0.0 + workbox-precaching: 7.0.0 + workbox-routing: 7.0.0 + workbox-strategies: 7.0.0 + dev: false + + /workbox-routing@7.0.0: + resolution: {integrity: sha512-8YxLr3xvqidnbVeGyRGkaV4YdlKkn5qZ1LfEePW3dq+ydE73hUUJJuLmGEykW3fMX8x8mNdL0XrWgotcuZjIvA==} + dependencies: + workbox-core: 7.0.0 + dev: false + + /workbox-strategies@7.0.0: + resolution: {integrity: sha512-dg3qJU7tR/Gcd/XXOOo7x9QoCI9nk74JopaJaYAQ+ugLi57gPsXycVdBnYbayVj34m6Y8ppPwIuecrzkpBVwbA==} + dependencies: + workbox-core: 7.0.0 + dev: false + + /workbox-streams@7.0.0: + resolution: {integrity: sha512-moVsh+5to//l6IERWceYKGiftc+prNnqOp2sgALJJFbnNVpTXzKISlTIsrWY+ogMqt+x1oMazIdHj25kBSq/HQ==} + dependencies: + workbox-core: 7.0.0 + workbox-routing: 7.0.0 + dev: false + + /workbox-sw@7.0.0: + resolution: {integrity: sha512-SWfEouQfjRiZ7GNABzHUKUyj8pCoe+RwjfOIajcx6J5mtgKkN+t8UToHnpaJL5UVVOf5YhJh+OHhbVNIHe+LVA==} + dev: false + + /workbox-window@7.0.0: + resolution: {integrity: sha512-j7P/bsAWE/a7sxqTzXo3P2ALb1reTfZdvVp6OJ/uLr/C2kZAMvjeWGm8V4htQhor7DOvYg0sSbFN2+flT5U0qA==} + dependencies: + '@types/trusted-types': 2.0.3 + workbox-core: 7.0.0 + dev: false + + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: false + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + /write-file-atomic@3.0.3: + resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} + dependencies: + imurmurhash: 0.1.4 + is-typedarray: 1.0.0 + signal-exit: 3.0.7 + typedarray-to-buffer: 3.1.5 + dev: false + + /write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + dev: true + + /ws@7.5.9: + resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + + /ws@8.13.0: + resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + + /xdg-basedir@5.1.0: + resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} + engines: {node: '>=12'} + dev: false + + /xml-js@1.6.11: + resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} + hasBin: true + dependencies: + sax: 1.2.4 + dev: false + + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: false + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + /yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + dev: false + + /yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + dev: true + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + /yocto-queue@1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + dev: false + + /zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..39a2b6e --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ] +} diff --git a/sidebars.ts b/sidebars.ts new file mode 100644 index 0000000..735bf72 --- /dev/null +++ b/sidebars.ts @@ -0,0 +1,325 @@ +import type { SidebarsConfig } from '@docusaurus/plugin-content-docs' + +const sidebars: SidebarsConfig = { + skill: [ + 'skill/introduction', + { + label: 'Docusaurus 主题魔改', + type: 'category', + link: { + type: 'doc', + id: 'skill/docusaurus/docusaurus-guides', + }, + items: [ + 'skill/docusaurus/docusaurus-config', + 'skill/docusaurus/docusaurus-style', + 'skill/docusaurus/docusaurus-component', + 'skill/docusaurus/docusaurus-plugin', + 'skill/docusaurus/docusaurus-search', + 'skill/docusaurus/docusaurus-comment', + 'skill/docusaurus/docusaurus-deploy', + ], + }, + { + label: '代码规范', + type: 'category', + link: { + type: 'doc', + id: 'skill/code-specification/code-specification-guides', + }, + items: [ + 'skill/code-specification/eslint', + 'skill/code-specification/prettier', + 'skill/code-specification/stylelint', + 'skill/code-specification/editorconfig', + 'skill/code-specification/husky', + 'skill/code-specification/npmrc', + ], + }, + { + label: 'Web', + type: 'category', + link: { type: 'generated-index' }, + items: [ + { + label: 'Vue', + type: 'category', + link: { type: 'generated-index' }, + items: [ + 'skill/web/vue/vue-reactive-data-object', + 'skill/web/vue/vue-reactive-data-array', + 'skill/web/vue/vue-reactive-data-basic-type', + 'skill/web/vue/pinia', + ], + }, + { + label: 'React', + type: 'category', + link: { type: 'generated-index' }, + items: [ + { + type: 'autogenerated', + dirName: 'skill/web/react', + }, + ], + }, + { + label: 'Css', + type: 'category', + link: { type: 'generated-index' }, + items: [ + { + type: 'autogenerated', + dirName: 'skill/web/css', + }, + ], + }, + { + label: 'Browser', + type: 'category', + link: { type: 'generated-index' }, + items: [ + { + type: 'autogenerated', + dirName: 'skill/web/browser', + }, + ], + }, + ], + }, + { + label: 'JavaScript/Typescript', + type: 'category', + link: { type: 'generated-index' }, + items: [ + { + type: 'autogenerated', + dirName: 'skill/js&ts', + }, + ], + }, + { + label: 'Node', + type: 'category', + link: { type: 'generated-index' }, + items: [ + { + type: 'autogenerated', + dirName: 'skill/node', + }, + ], + }, + { + label: '编程语言', + type: 'category', + link: { type: 'generated-index' }, + items: [ + { + label: 'Java', + type: 'category', + link: { type: 'generated-index' }, + items: [ + { + type: 'autogenerated', + dirName: 'skill/programming-languages/java', + }, + ], + }, + { + label: 'Python', + type: 'category', + link: { type: 'generated-index' }, + items: [ + { + type: 'autogenerated', + dirName: 'skill/programming-languages/python', + }, + ], + }, + { + label: 'Go', + type: 'category', + link: { type: 'generated-index' }, + items: [ + 'skill/programming-languages/go/go-environment-install', + 'skill/programming-languages/go/go-json-usage', + 'skill/programming-languages/go/go-send-http-request', + 'skill/programming-languages/go/go-call-js', + 'skill/programming-languages/go/go-concurrent', + 'skill/programming-languages/go/try-gin-framework', + ], + }, + ], + }, + { + label: 'Git', + type: 'category', + link: { type: 'generated-index' }, + items: [ + { + type: 'autogenerated', + dirName: 'skill/git', + }, + ], + }, + { + label: '算法', + type: 'category', + link: { + type: 'doc', + id: 'skill/algorithm/algorithm-introduction', + }, + items: [ + 'skill/algorithm/two-sum', + 'skill/algorithm/three-sum', + 'skill/algorithm/sliding-window', + 'skill/algorithm/double-pointer', + ], + }, + { + label: '逆向', + type: 'category', + link: { + title: '逆向笔记', + description: 'Web逆向与安卓逆向笔记', + type: 'generated-index', + keywords: ['reverse', 'web', 'android', 'frida'], + }, + items: [ + { + label: '安卓', + type: 'category', + link: { type: 'generated-index' }, + items: [ + { + type: 'autogenerated', + dirName: 'skill/reverse/android', + }, + ], + }, + { + label: 'Web', + type: 'category', + items: [ + { + type: 'autogenerated', + dirName: 'skill/reverse/web', + }, + ], + }, + { + label: '密码学', + type: 'category', + items: [ + { + type: 'autogenerated', + dirName: 'skill/reverse/crypto', + }, + ], + }, + ], + }, + { + label: 'Docker', + type: 'category', + link: { type: 'generated-index' }, + items: [ + { + type: 'autogenerated', + dirName: 'skill/docker', + }, + ], + }, + { + label: '数据库', + type: 'category', + link: { + // title: '', + // description: '', + type: 'generated-index', + keywords: ['database', 'mysql', 'mongodb', 'redis', 'elasticsearch'], + }, + items: [ + { + label: 'Mysql', + type: 'category', + link: { + type: 'doc', + id: 'skill/database/mysql/mysql-note', + }, + items: [ + { + type: 'autogenerated', + dirName: 'skill/database/mysql', + }, + ], + }, + { + label: 'MongoDB', + type: 'category', + link: { + type: 'doc', + id: 'skill/database/mongo/mongodb-note', + }, + items: [ + { + type: 'autogenerated', + dirName: 'skill/database/mongo', + }, + ], + }, + { + label: 'Redis', + type: 'category', + link: { + type: 'doc', + id: 'skill/database/redis/redis-note', + }, + items: [ + { + type: 'autogenerated', + dirName: 'skill/database/redis', + }, + ], + }, + { + label: 'Elasticsearch', + type: 'category', + link: { + type: 'doc', + id: 'skill/database/elasticsearch/elasticsearch-note', + }, + items: [ + { + type: 'autogenerated', + dirName: 'skill/database/elasticsearch', + }, + ], + }, + ], + }, + { + label: '杂项', + type: 'category', + link: { type: 'generated-index' }, + items: [ + { + type: 'autogenerated', + dirName: 'skill/misc', + }, + ], + }, + ], + tools: [ + 'tools/introduction', + 'tools/everything-quick-search-local-files', + 'tools/wappalyzer-recognize-technology', + 'tools/windows-custom-right-click-menu', + 'tools/vscode-config', + 'tools/idea-config', + 'tools/vite-plugin', + 'tools/jetbrains-product-activation-method', + ], +} + +module.exports = sidebars diff --git a/src/components/BrowserWindow/index.tsx b/src/components/BrowserWindow/index.tsx new file mode 100644 index 0000000..240ffa3 --- /dev/null +++ b/src/components/BrowserWindow/index.tsx @@ -0,0 +1,29 @@ +import React from 'react' + +import styles from './styles.module.css' + +function BrowserWindow({ children, minHeight, url }) { + return ( +
+
+
+ + + +
+
{url}
+
+
+ + + +
+
+
+ +
{children}
+
+ ) +} + +export default BrowserWindow diff --git a/src/components/BrowserWindow/styles.module.css b/src/components/BrowserWindow/styles.module.css new file mode 100644 index 0000000..667587f --- /dev/null +++ b/src/components/BrowserWindow/styles.module.css @@ -0,0 +1,65 @@ +.browserWindow { + border: 3px solid var(--ifm-color-emphasis-200); + border-top-left-radius: var(--ifm-global-radius); + border-top-right-radius: var(--ifm-global-radius); + margin-bottom: 10px; +} + +.browserWindowHeader { + align-items: center; + background: var(--ifm-color-emphasis-200); + display: flex; + padding: 0.5rem 1rem; +} + +.row:after { + content: ''; + display: table; + clear: both; +} + +.buttons { + white-space: nowrap; +} + +.right { + align-self: center; + width: 10%; +} + +.browserWindowAddressBar { + flex: 1 0; + margin: 0 1rem 0 0.5rem; + border-radius: 12.5px; + background-color: #fff; + color: #666; + padding: 5px 15px; + font: 400 13px Arial; + user-select: none; +} + +.dot { + margin-right: 6px; + margin-top: 4px; + height: 12px; + width: 12px; + background-color: #bbb; + border-radius: 50%; + display: inline-block; +} + +.browserWindowMenuIcon { + margin-left: auto; +} + +.bar { + width: 17px; + height: 3px; + background-color: #aaa; + margin: 3px 0; + display: block; +} + +.browserWindowBody { + padding: 1rem; +} diff --git a/src/components/CodeSandBox/index.tsx b/src/components/CodeSandBox/index.tsx new file mode 100644 index 0000000..930ac24 --- /dev/null +++ b/src/components/CodeSandBox/index.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import { useColorMode } from '@docusaurus/theme-common' + +function index({ slug, title, height = '600px' }) { + const { isDarkTheme } = useColorMode() + const themedSrc = `https://codesandbox.io/embed/${slug}?fontsize=14&hidenavigation=1&view=preview&theme=${ + isDarkTheme ? 'dark' : 'light' + }` + return ( +
+ +
+ ) +} + +export default index diff --git a/src/components/Comment/index.tsx b/src/components/Comment/index.tsx new file mode 100644 index 0000000..4405fc4 --- /dev/null +++ b/src/components/Comment/index.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import { useThemeConfig, useColorMode } from '@docusaurus/theme-common' +import useDocusaurusContext from '@docusaurus/useDocusaurusContext' +import { ThemeConfig } from '@docusaurus/preset-classic' +import BrowserOnly from '@docusaurus/BrowserOnly' +import Giscus, { GiscusProps, Theme } from '@giscus/react' + +export type GiscusConfig = GiscusProps & { darkTheme: Theme } + +const defaultConfig: Partial & { darkTheme: string } = { + id: 'comments', + mapping: 'title', + reactionsEnabled: '1', + emitMetadata: '0', + inputPosition: 'top', + lang: 'zh-CN', + theme: 'light', + darkTheme: 'dark_dimmed', +} + +export default function Comment(): JSX.Element { + const themeConfig = useThemeConfig() as ThemeConfig & { giscus: GiscusConfig } + const { i18n } = useDocusaurusContext() + + // merge default config + const giscus = { ...defaultConfig, ...themeConfig.giscus } + + if (!giscus.repo || !giscus.repoId || !giscus.categoryId) { + throw new Error('You must provide `repo`, `repoId`, and `categoryId` to `themeConfig.giscus`.') + } + + giscus.theme = useColorMode().colorMode === 'dark' ? giscus.darkTheme : giscus.theme + giscus.lang = i18n.currentLocale + + return ( + Loading Comments...
}> + {() => } + + ) +} diff --git a/src/components/SocialLinks/index.tsx b/src/components/SocialLinks/index.tsx new file mode 100644 index 0000000..6e67db5 --- /dev/null +++ b/src/components/SocialLinks/index.tsx @@ -0,0 +1,55 @@ +import React from 'react' +import Tooltip from '@site/src/components/Tooltip' +import { Icon } from '@iconify/react' +import social from '@site/data/social' +import styles from './styles.module.scss' + +export type Social = { + github?: string + twitter?: string + juejin?: string + csdn?: string + qq?: string + wx?: string + cloudmusic?: string + zhihu?: string + email?: string +} + +interface Props { + href: string + title: string + color?: string + icon: string | JSX.Element + [key: string]: unknown +} + +function SocialLink({ href, icon, title, color, ...prop }: Props) { + return ( + + + {typeof icon === 'string' ? : icon} + + + ) +} + +export default function SocialLinks({ ...prop }) { + return ( +
+ {Object.entries(social).map(([key, { href, icon, title, color }]) => { + if (!href) return <> + + return ( + + ) + })} +
+ ) +} diff --git a/src/components/SocialLinks/styles.module.scss b/src/components/SocialLinks/styles.module.scss new file mode 100644 index 0000000..05cb11b --- /dev/null +++ b/src/components/SocialLinks/styles.module.scss @@ -0,0 +1,59 @@ +.socialLinks { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + max-width: min-content; + gap: 1rem; + padding: 0.5em 0; + z-index: 100; + position: relative; + + a { + display: inline-flex; + box-sizing: content-box; + width: 2rem; + height: 2rem; + align-items: center; + justify-content: center; + transition: all 0.3s ease-in-out; + transition-property: all; + transition-duration: 0.3s; + transition-delay: 0s; + + border-radius: 50%; + padding: 0.25rem; + + &:hover { + background-color: var(--color); + + > svg path { + color: white; + } + } + } + + .dropdown { + display: flex; + align-items: center; + } + + .dropdown span { + margin-left: 6px; + /* font-weight: 700; */ + font-size: 0.9em; + } + + .dropdown__menu { + max-width: initial; + right: 0; + top: 120%; + } +} + +.socialLinks svg, +.socialLinks svg path { + width: 24px; + height: 24px; + transition: all 0.3s ease-in-out; +} diff --git a/src/components/Svg/index.tsx b/src/components/Svg/index.tsx new file mode 100644 index 0000000..0bfe317 --- /dev/null +++ b/src/components/Svg/index.tsx @@ -0,0 +1,36 @@ +import React, { type ReactNode, type ComponentProps } from 'react' +import clsx from 'clsx' +import styles from './styles.module.css' + +export interface SvgIconProps extends ComponentProps<'svg'> { + viewBox?: string + size?: 'inherit' | 'small' | 'medium' | 'large' + color?: 'inherit' | 'primary' | 'secondary' | 'success' | 'error' | 'warning' + svgClass?: string // Class attribute on the child + colorAttr?: string // Applies a color attribute to the SVG element. + children: ReactNode // Node passed into the SVG element. +} + +export default function Svg(props: SvgIconProps): JSX.Element { + const { + svgClass, + colorAttr, + children, + color = 'inherit', + size = 'medium', + viewBox = '0 0 24 24', + ...rest + } = props + + return ( + + {children} + + ) +} diff --git a/src/components/Svg/styles.module.css b/src/components/Svg/styles.module.css new file mode 100644 index 0000000..80b8765 --- /dev/null +++ b/src/components/Svg/styles.module.css @@ -0,0 +1,47 @@ +.svgIcon { + user-select: none; + width: 1em; + height: 1em; + display: inline-block; + fill: currentColor; + flex-shrink: 0; + color: inherit; +} + +/* font-size */ +.small { + font-size: 1.25rem; +} + +.medium { + font-size: 1.5rem; +} + +.large { + font-size: 2.185rem; +} + +/* colors */ +.primary { + color: var(--ifm-color-primary); +} + +.secondary { + color: var(--ifm-color-secondary); +} + +.success { + color: var(--ifm-color-success); +} + +.error { + color: var(--ifm-color-error); +} + +.warning { + color: var(--ifm-color-warning); +} + +.inherit { + color: inherit; +} diff --git a/src/components/Tooltip/index.tsx b/src/components/Tooltip/index.tsx new file mode 100644 index 0000000..7391119 --- /dev/null +++ b/src/components/Tooltip/index.tsx @@ -0,0 +1,129 @@ +import React, { useEffect, useState, useRef } from 'react' +import ReactDOM from 'react-dom' +import { usePopper } from 'react-popper' +import styles from './styles.module.css' + +interface Props { + anchorEl?: HTMLElement | string + id: string + text: string + delay?: number + children: React.ReactElement +} + +export default function Tooltip({ children, id, anchorEl, text, delay }: Props): JSX.Element { + const [open, setOpen] = useState(false) + const [referenceElement, setReferenceElement] = useState(null) + const [popperElement, setPopperElement] = useState(null) + const [arrowElement, setArrowElement] = useState(null) + const [container, setContainer] = useState(null) + const { styles: popperStyles, attributes } = usePopper(referenceElement, popperElement, { + modifiers: [ + { + name: 'arrow', + options: { + element: arrowElement, + }, + }, + { + name: 'offset', + options: { + offset: [0, 8], + }, + }, + ], + }) + + const timeout = useRef(null) + const tooltipId = `${id}_tooltip` + + useEffect(() => { + if (anchorEl) { + if (typeof anchorEl === 'string') { + setContainer(document.querySelector(anchorEl)) + } else { + setContainer(anchorEl) + } + } else { + setContainer(document.body) + } + }, [container, anchorEl]) + + useEffect(() => { + const showEvents = ['mouseenter', 'focus'] + const hideEvents = ['mouseleave', 'blur'] + + const handleOpen = () => { + // There is no point in displaying an empty tooltip. + if (text === '') { + return + } + + // Remove the title ahead of time to avoid displaying + // two tooltips at the same time (native + this one). + referenceElement?.removeAttribute('title') + + timeout.current = window.setTimeout(() => { + setOpen(true) + }, delay || 300) + } + + const handleClose = () => { + clearInterval(timeout.current!) + setOpen(false) + } + + if (referenceElement) { + showEvents.forEach(event => { + referenceElement.addEventListener(event, handleOpen) + }) + + hideEvents.forEach(event => { + referenceElement.addEventListener(event, handleClose) + }) + } + + return () => { + if (referenceElement) { + showEvents.forEach(event => { + referenceElement.removeEventListener(event, handleOpen) + }) + + hideEvents.forEach(event => { + referenceElement.removeEventListener(event, handleClose) + }) + } + } + }, [referenceElement, text, delay]) + + return ( + <> + {React.cloneElement(children, { + ref: setReferenceElement, + 'aria-describedby': open ? tooltipId : undefined, + })} + {container + ? ReactDOM.createPortal( + open && ( + + ), + container, + ) + : container} + + ) +} diff --git a/src/components/Tooltip/styles.module.css b/src/components/Tooltip/styles.module.css new file mode 100644 index 0000000..59b1b93 --- /dev/null +++ b/src/components/Tooltip/styles.module.css @@ -0,0 +1,30 @@ +.tooltip { + border-radius: 4px; + padding: 4px 8px; + color: var(--site-color-tooltip); + background: var(--site-color-tooltip-background); + font-size: 0.8rem; + z-index: 500; + line-height: 1.4; + font-weight: 500; + max-width: 300px; + opacity: 0.92; +} + +.tooltipArrow { + visibility: hidden; +} + +.tooltipArrow::before { + visibility: visible; + content: ''; + transform: rotate(45deg); +} + +.tooltip[data-popper-placement^='top'] > .tooltipArrow { + bottom: -4px; +} + +.tooltip[data-popper-placement^='bottom'] > .tooltipArrow { + top: -4px; +} diff --git a/src/components/UserCard/index.tsx b/src/components/UserCard/index.tsx new file mode 100644 index 0000000..287f7ef --- /dev/null +++ b/src/components/UserCard/index.tsx @@ -0,0 +1,91 @@ +import React from 'react' +import clsx from 'clsx' +import { usePluginData } from '@docusaurus/useGlobalData' +import type { BlogPost } from '@docusaurus/plugin-content-blog' +import Link from '@docusaurus/Link' +import { Icon } from '@iconify/react' +import SocialLinks from '@site/src/components/SocialLinks' +import { useThemeConfig } from '@docusaurus/theme-common' +import useBaseUrl from '@docusaurus/useBaseUrl' +import useDocusaurusContext from '@docusaurus/useDocusaurusContext' + +import { projects } from '@site/data/projects' + +import styles from './styles.module.scss' + +type Count = { + blog: number + tag: number + doc: number + project: number +} + +export default function UserCard({ isNavbar = false }: { isNavbar?: boolean }) { + const { + siteConfig: { customFields }, + } = useDocusaurusContext() + const { bio } = customFields as { bio: string } + + const { + navbar: { title, logo = { src: '' } }, + } = useThemeConfig() + + const logoLink = useBaseUrl(logo.src || '/') + + const blogData = usePluginData('docusaurus-plugin-content-blog') as { + posts: BlogPost[] + postNum: number + tagNum: number + } + const docData = ( + usePluginData('docusaurus-plugin-content-docs') as { versions: { docs: BlogPost[] } } + )?.versions[0].docs + + const count: Count = { + blog: blogData.postNum, + tag: blogData.tagNum ?? 0, + doc: docData?.length ?? 0, + project: projects?.length ?? 0, + } + + return ( +
+ + logo + +
+ + {title} + +
+
{bio}
+
+ + + {count.blog} + + + + {count.tag} + + + + {count.doc} + + + + {count.project} + +
+ +
+ ) +} diff --git a/src/components/UserCard/styles.module.scss b/src/components/UserCard/styles.module.scss new file mode 100644 index 0000000..b10e494 --- /dev/null +++ b/src/components/UserCard/styles.module.scss @@ -0,0 +1,106 @@ +.userCard { + border-radius: 12px; + background: var(--blog-item-background-color); + box-shadow: var(--blog-item-shadow); + margin: 0; + position: relative; + display: block; + text-align: center; + + a { + text-decoration: none; + } +} + +.userCardNavbar { + margin-top: 1rem; + position: relative; + display: block; + text-align: center; +} + +.cardImg { + width: 6rem; + height: 6rem; + max-width: 100%; + border-radius: 50%; + padding: 4px; + background-color: #fff; + box-shadow: 0 0 10px rgb(0 0 0 / 20%); + transition: 0.4s; + + &:hover { + box-shadow: 0 0 30px rgb(0 120 231 / 20%); + } +} + +.name { + color: var(--ifm-text-color); + font-family: var(--ifm-font-family-name); + font-weight: 900; + cursor: pointer; + margin: 0 auto; +} + +.bio { + margin: 0.5rem auto; + color: var(--ifm-secondary-text-color); + font-size: 0.8rem; +} + +.num { + display: flex; + justify-content: center; + align-items: center; + position: relative; + font-size: 1rem; + font-weight: 500; + gap: 2px; +} + +.numItem { + transform: all 0.3s ease-in-out; + display: flex; + flex-shrink: 0; + gap: 5px; + padding: 0 10px; + align-items: center; + color: var(--ifm-text-color); + border-left: 1px solid #999; + + &:hover { + text-decoration: none; + color: var(--ifm-color-primary); + cursor: pointer; + } + + &:nth-child(1) { + border: none; + } + + /* &:after { + content: attr(data-tips); + position: absolute; + top: 0; + margin: 0 auto; + white-space: nowrap; + opacity: 0; + transform: translateY(-150%); + transition: 0.2s; +} + +.&:hover::after { + opacity: 1; + transform: translateY(-100%); +} */ +} + +.tags { + margin-top: 0.5rem; + display: flex; + flex-wrap: wrap; + + > a { + padding: 2px 5px; + } +} diff --git a/src/components/svgIcons/FavoriteIcon/index.tsx b/src/components/svgIcons/FavoriteIcon/index.tsx new file mode 100644 index 0000000..8874ab2 --- /dev/null +++ b/src/components/svgIcons/FavoriteIcon/index.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import Svg, { type SvgIconProps } from '@site/src/components/Svg' + +export default function FavoriteIcon( + props: Omit, +): JSX.Element { + return ( + + + + ) +} diff --git a/src/css/custom.scss b/src/css/custom.scss new file mode 100644 index 0000000..189ae2c --- /dev/null +++ b/src/css/custom.scss @@ -0,0 +1,335 @@ +:root { + --ifm-color-primary: #3cad6e; + --ifm-color-primary-dark: #359962; + --ifm-color-primary-darker: #33925d; + --ifm-color-primary-darkest: #2e8555; + --ifm-color-primary-light: #29784c; + --ifm-color-primary-lighter: #277148; + --ifm-color-primary-lightest: #205d3b; + --ifm-code-font-size: 95%; + --ifm-font-family-base: misans, ui-sans-serif, system-ui, -apple-system; + + --ifm-heading-font-family: ui-sans-serif, system-ui, -apple-system; + + --ifm-navbar-shadow: 0 4px 28px rgb(0 0 0 / 10%); + + --ifm-menu-color: #0d203a; + + /* 代码块背景 */ + --ifm-code-background: #12affa1a; + --ifm-code-color: var(--ifm-color-primary); + --prism-background-color: #f6f8fa; + + --ifm-text-color: #333; + --ifm-secondary-text-color: #555; + + --site-primary-hue-saturation: 167 68%; + --site-primary-hue-saturation-light: 167 56%; + + --site-color-favorite-background: #f6fdfd; + --site-color-tooltip: #fff; + --site-color-tooltip-background: #353738; + --site-color-svg-icon-favorite: #e9669e; + --site-color-checkbox-checked-bg: hsl(167deg 56% 73% / 25%); + + --ifm-container-width: 1024px; + + --ifm-heading-color: hsl(214deg 78% 17%); + --ifm-heading-font-weight: 500; + --ifm-font-weight-bold: 520; + --ifm-toc-border-color: #f1f5f9; + + --content-background-color: #f8fafc; + + --blog-item-background-color: linear-gradient(180deg, #fcfcfc, #fff); + --blog-item-shadow: 0 10px 20px #f1f5f9dd, 0 0 10px 0 #e4e4e7dd; + --blog-item-shade: #f4f4f5; + + -webkit-font-smoothing: unset; + + color: hsl(214deg 37% 25%); + + --docusaurus-highlighted-code-line-bg: #d1d5db; +} + +html[data-theme='dark'] { + --ifm-color-primary: hsl(214deg 100% 60%); + --ifm-color-primary-light: hsl(214deg 100% 75%); + --ifm-heading-color: hsl(0deg 0% 100%); + --ifm-menu-color: #eceef1; + --ifm-text-color: var(--ifm-menu-color); + --ifm-secondary-text-color: #eee; + --ifm-toc-border-color: #313131; + + --content-background-color: #18181b; + --blog-item-background-color: linear-gradient(180deg, #171717, #18181b); + --blog-item-shadow: 0 12px 24px rgb(37 55 72 / 20%), 0 0 8px rgb(37 55 72 / 40%); + --blog-item-shade: #27272a; + + color: hsl(214deg 15% 85%); + + --docusaurus-highlighted-code-line-bg: #4b5563; +} + +body { + font-family: + misans, + system-ui, + -apple-system, + 'PingFang SC', + 'Microsoft YaHei'; +} + +html, +body { + scroll-behavior: smooth; +} + +.theme-code-block { + --prism-background-color: #f6f8fa !important; +} + +html[data-theme='dark'] .theme-code-block { + --prism-background-color: #1e1e1e !important; +} + +article { + .markdown { + a:not(.hash-link) { + text-decoration: none; + font-weight: inherit; + border-bottom: 1px solid rgb(125 125 125 / 30%); + transition: border 0.3s ease-in-out; + + &:hover, + &:focus { + border-bottom: 1px solid var(--ifm-color-primary-light); + } + } + + code { + border: 0.1rem solid transparent; + } + + .alert { + border: 2px solid var(--ifm-alert-border-color); + } + + img { + border-radius: 10px; + display: flex; + margin: 0 auto; + + // box-shadow: 0 0 25px rgb(132 167 156 / 10%); + } + + > h2 { + font-size: 1.8em; + } + + > h3 { + font-size: 1.5em; + } + + > h4 { + font-size: 1.2em; + } + + .markdown-body a:hover::before { + width: 100%; + } + + .a-icon { + display: none; + } + + p > span { + > .a-icon { + display: block; + } + + > a { + line-height: 1.5rem; + } + } + } +} + +:where(html[data-theme='dark']) article .markdown strong { + color: #fff; +} + +.navbar { + box-shadow: none; + background-color: transparent; + + // background-image: radial-gradient(transparent 1px, #fff 1px); + // background-size: 3px 3px; + // backdrop-filter: saturate(50%) blur(4px); + + .navbar__title { + margin-left: 12px; + color: var(--ifm-color-primary); + font-family: var(--ifm-font-family-name); + } +} + +.navbar__link, +.dropdown, +.navbar__title, +.menu { + font-weight: 400; +} + +.pagination-nav__link { + border: 2px solid var(--ifm-link-color); + + &:hover { + background-color: #a1d8f71b; + } +} + +@media (width <= 570px) { + h1 { + font-size: 1.6em; + } + + .markdown { + > h2 { + font-size: 1.4em; + } + + > h3 { + font-size: 1.2em; + } + } +} + +/* 导航收缩相应尺寸调大 */ +@media (width <= 1100px) { + .navbar > .container, + .navbar > .container-fluid { + padding: 0; + } + + .navbar__toggle { + display: inherit; + } + + .navbar__item { + display: none; + } + + .navbar__search-input { + width: 9rem; + } + + .navbar-sidebar { + display: block; + } +} + +code { + color: var(--ifm-code-color); +} + +div[class^='announcementBar_'] { + background: repeating-linear-gradient( + -35deg, + var(--ifm-color-primary-lighter), + var(--ifm-color-primary-lighter) 20px, + var(--ifm-color-primary-lightest) 10px, + var(--ifm-color-primary-lightest) 40px + ); + font-weight: 700; +} + +.screen-reader-only { + border: 0; + clip: rect(0 0 0 0); + clip-path: polygon(0 0, 0 0, 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + white-space: nowrap; +} + +.code-block-error-line { + background-color: #ff000020; + display: block; + margin: 0 calc(-1 * var(--ifm-pre-padding)); + padding: 0 var(--ifm-pre-padding); + border-left: 3px solid #ff000080; +} + +.readMore { + display: flex; + flex: 1 1 auto; + justify-content: flex-end; + align-items: center; + gap: 2px; + + opacity: 0; + + transition: 0.2s; + + color: var(--ifm-link-color); + font-weight: 500; + + a:hover { + text-decoration: none; + } +} + +.blog-card { + border-radius: var(--ifm-pagination-nav-border-radius); + margin-top: 0; + + background: var(--blog-item-background-color); + box-shadow: var(--blog-item-shadow); + padding: 1em 1.25em 0.75em; + + &:hover .readMore { + opacity: 1; + } +} + +.container-wrapper { + background: var(--content-background-color); +} + +.gsc-comments textarea { + background: var(--content-background-color); +} + +.tag { + &:hover { + color: currentColor !important; + } + + &::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 0; + height: 2px; + background: var(--ifm-color-primary); + visibility: hidden; + transition: all 0.3s linear; + } + + &:hover::after { + visibility: visible; + width: 100%; + } +} + +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/src/hooks/useReadPercent.ts b/src/hooks/useReadPercent.ts new file mode 100644 index 0000000..c6fe126 --- /dev/null +++ b/src/hooks/useReadPercent.ts @@ -0,0 +1,24 @@ +import { useMotionValueEvent, useScroll } from 'framer-motion' +import { useLayoutEffect, useMemo, useRef, useState } from 'react' + +export const useReadPercent = () => { + const [scrollProgress, setScrollProgress] = useState(0) + const postRef = useRef(null) + const { scrollYProgress } = useScroll({ container: postRef }) + + useLayoutEffect(() => { + postRef.current = document.getElementById('__blog-post-container') + }, []) + + useMotionValueEvent(scrollYProgress, 'change', latest => { + setScrollProgress(latest) + }) + + const readPercent = useMemo(() => { + return Math.round(scrollProgress * 100) + }, [scrollProgress]) + + return { + readPercent, + } +} diff --git a/src/hooks/useViewType.ts b/src/hooks/useViewType.ts new file mode 100644 index 0000000..933faa1 --- /dev/null +++ b/src/hooks/useViewType.ts @@ -0,0 +1,21 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' + +export type ViewType = 'list' | 'grid' + +export function useViewType() { + const [viewType, setViewType] = useState('list') + + useEffect(() => { + setViewType((localStorage.getItem('viewType') as ViewType) || 'list') + }, []) + + const toggleViewType = useCallback((newViewType: ViewType) => { + setViewType(newViewType) + localStorage.setItem('viewType', newViewType) + }, []) + + return { + viewType, + toggleViewType, + } +} diff --git a/src/hooks/useWindowSize.ts b/src/hooks/useWindowSize.ts new file mode 100644 index 0000000..8112507 --- /dev/null +++ b/src/hooks/useWindowSize.ts @@ -0,0 +1,26 @@ +import React, { useState, useEffect } from 'react' + +export function useWindowSize() { + const [windowSize, setWindowSize] = useState({ + width: window.innerWidth, + height: window.innerHeight, + }) + + useEffect(() => { + function handleResize() { + setWindowSize({ + width: window.innerWidth, + height: window.innerHeight, + }) + } + + window.addEventListener('resize', handleResize) + + // 清理函数,在组件卸载时移除事件监听 + return () => { + window.removeEventListener('resize', handleResize) + } + }, []) // 空数组作为第二个参数,只在组件挂载和卸载时执行一次 + + return windowSize +} diff --git a/src/pages/_components/BlogRecommend/index.tsx b/src/pages/_components/BlogRecommend/index.tsx new file mode 100644 index 0000000..1b8c023 --- /dev/null +++ b/src/pages/_components/BlogRecommend/index.tsx @@ -0,0 +1,64 @@ +import React from 'react' +import clsx from 'clsx' +import { motion } from 'framer-motion' +import { BlogPost } from '@docusaurus/plugin-content-blog' +import { usePluginData } from '@docusaurus/useGlobalData' +import Translate from '@docusaurus/Translate' +import Link from '@docusaurus/Link' +import Image from '@theme/IdealImage' + +import styles from './styles.module.scss' + +export default function BlogRecommend(): JSX.Element { + const blogData = usePluginData('docusaurus-plugin-content-blog') as { + posts: BlogPost[] + postNum: number + tagNum: number + } + const recommendedPosts = blogData.posts + .filter(b => (b.metadata.frontMatter.sticky as number) > 0) + .map(b => b.metadata) + .sort((a, b) => (a.frontMatter.sticky as number) - (b.frontMatter.sticky as number)) + .slice(0, 8) + + if (recommendedPosts.length === 0) { + return <> + } + + return ( +
+

+ 推荐阅读 +

+
+
+
+
    + {recommendedPosts.map(post => ( + + {post.frontMatter.image && ( +
    + {post.title} +
    + )} +
    +

    + {post.title} +

    +

    {post.description}

    +
    +
    + ))} +
+
+
+
+
+ ) +} diff --git a/src/pages/_components/BlogRecommend/styles.module.scss b/src/pages/_components/BlogRecommend/styles.module.scss new file mode 100644 index 0000000..7b52ffe --- /dev/null +++ b/src/pages/_components/BlogRecommend/styles.module.scss @@ -0,0 +1,65 @@ +.blog__recommend { + display: grid; + grid-template-columns: repeat(4, 1fr); + justify-content: center; + gap: 24px; + padding: 0; + + li { + border-radius: 5px; + background: var(--blog-item-background-color); + box-shadow: var(--blog-item-shadow); + } + + .card__image { + overflow: hidden; + height: 150px; + border-bottom: 2px solid var(--ifm-color-emphasis-200); + } + + p { + font-size: smaller; + } + + h4 { + font-size: 1.1rem; + + a { + position: relative; + + &:hover { + text-decoration: none; + } + + &:hover::after { + visibility: visible; + transform: scaleX(1); + } + + &::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 2px; + background: var(--ifm-color-primary); + visibility: hidden; + transition: all 0.3s linear; + transform: scaleX(0); + } + } + } +} + +@media (width <= 1024px) { + .blog__recommend { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (width <= 768px) { + .blog__recommend { + grid-template-columns: repeat(1, 1fr); + } +} diff --git a/src/pages/_components/BlogSection/index.tsx b/src/pages/_components/BlogSection/index.tsx new file mode 100644 index 0000000..fc48aab --- /dev/null +++ b/src/pages/_components/BlogSection/index.tsx @@ -0,0 +1,88 @@ +import React from 'react' +import clsx from 'clsx' +import { motion, useScroll, useTransform } from 'framer-motion' +import { BlogPost } from '@docusaurus/plugin-content-blog' +import { usePluginData } from '@docusaurus/useGlobalData' +import Translate from '@docusaurus/Translate' +import Link from '@docusaurus/Link' +import Image from '@theme/IdealImage' + +import styles from './styles.module.scss' +import SectionTitle from '../SectionTitle' + +const chunk = (arr, size) => + Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => + arr.slice(i * size, i * size + size), + ) + +const BLOG_POSTS_COUNT = 6 +const BLOG_POSTS_PER_ROW = 2 + +export function BlogItem({ post }: { post: BlogPost }) { + const { + metadata: { permalink, frontMatter, title, description }, + } = post + + return ( + + {frontMatter.image && ( + + {title} + + )} +
+

+ {title} +

+

{description}

+
+
+ ) +} + +export default function BlogSection(): JSX.Element { + const blogData = usePluginData('docusaurus-plugin-content-blog') as { + posts: BlogPost[] + postNum: number + tagNum: number + } + + const posts = chunk(blogData.posts.slice(0, BLOG_POSTS_COUNT), BLOG_POSTS_PER_ROW) + + const ref = React.useRef(null) + + const { scrollYProgress } = useScroll() + const y = useTransform(scrollYProgress, [0, 0.5, 1], [20, 0, -20], { + clamp: false, + }) + + if (blogData.postNum === 0) { + return <>作者还没有写过博客哦 + } + + return ( +
+ + 近期博客 + +
+ {posts.map((postGroup, index) => ( +
+ {postGroup.map((post, i) => ( + + + + ))} +
+ ))} +
+
+ ) +} diff --git a/src/pages/_components/BlogSection/styles.module.scss b/src/pages/_components/BlogSection/styles.module.scss new file mode 100644 index 0000000..83a2ea8 --- /dev/null +++ b/src/pages/_components/BlogSection/styles.module.scss @@ -0,0 +1,55 @@ +.blogContainer { + max-width: 1280px; +} + +.list { + margin: 0; + padding: 0.5rem 0; + overflow: hidden; + max-height: 640px; + border-radius: var(--ifm-card-border-radius); + + li { + display: flex; + width: 100%; + background: var(--blog-item-background-color); + box-shadow: var(--blog-item-shadow); + } + + .image { + overflow: hidden; + object-fit: cover; + max-height: 240px; + cursor: pointer; + + img { + width: 100%; + } + } + + p { + font-size: smaller; + } + + h4 { + font-size: 1rem; + + a { + position: relative; + + &:hover { + text-decoration: none; + } + } + } +} + +@media (width <= 996px) { + .blogContainer { + max-width: 768px; + } + + .list { + max-height: none; + } +} diff --git a/src/pages/_components/FeaturesSection/index.tsx b/src/pages/_components/FeaturesSection/index.tsx new file mode 100644 index 0000000..ba757f2 --- /dev/null +++ b/src/pages/_components/FeaturesSection/index.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import clsx from 'clsx' +import Translate from '@docusaurus/Translate' + +import styles from './styles.module.scss' +import features, { type FeatureItem } from '@site/data/features' +import SectionTitle from '../SectionTitle' + +function Feature({ title, Svg, text }: FeatureItem) { + return ( +
+
+ +
+
+

{title}

+

{text}

+
+
+ ) +} + +export default function FeaturesSection(): JSX.Element { + return ( +
+ + 个人特点 + +
+ {features.map((props, idx) => ( + + ))} +
+
+ ) +} diff --git a/src/pages/_components/FeaturesSection/styles.module.scss b/src/pages/_components/FeaturesSection/styles.module.scss new file mode 100644 index 0000000..da6ddb4 --- /dev/null +++ b/src/pages/_components/FeaturesSection/styles.module.scss @@ -0,0 +1,44 @@ +.featureContainer { + max-width: 1280px; + padding: 2rem 1rem; +} + +.features { + display: grid; + grid-template-columns: repeat(3, minmax(260px, 1fr)); + gap: 1.5rem; + margin: 0; + padding: 0.5rem 0; +} + +.feature { + position: relative; + background: transparent; + border-radius: 0.375rem; + width: 100%; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.5rem; + transition: all 0.3s linear; +} + +.featureSvg { + height: 150px; + width: 100%; +} + +.feature:hover { + box-shadow: 0 0 10px 10px rgb(221 221 221 / 20%); +} + +html[data-theme='dark'] .feature:hover { + box-shadow: 0 0 10px 10px rgb(71 71 71 / 20%); +} + +@media (width <= 768px) { + .features { + grid-template-columns: repeat(1, 1fr); + gap: 0.75rem; + } +} diff --git a/src/pages/_components/Hero/img/hero_main.svg b/src/pages/_components/Hero/img/hero_main.svg new file mode 100644 index 0000000..4f1cceb --- /dev/null +++ b/src/pages/_components/Hero/img/hero_main.svg @@ -0,0 +1 @@ +{ : }</> \ No newline at end of file diff --git a/src/pages/_components/Hero/img/hero_main1.svg b/src/pages/_components/Hero/img/hero_main1.svg new file mode 100644 index 0000000..2423634 --- /dev/null +++ b/src/pages/_components/Hero/img/hero_main1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/pages/_components/Hero/index.tsx b/src/pages/_components/Hero/index.tsx new file mode 100644 index 0000000..bd077d6 --- /dev/null +++ b/src/pages/_components/Hero/index.tsx @@ -0,0 +1,139 @@ +import React from 'react' +import { Variants, motion, useScroll, useTransform } from 'framer-motion' // Import motion from framer-motion + +import Translate from '@docusaurus/Translate' + +import HeroMain from './img/hero_main.svg' + +import styles from './styles.module.scss' +import SocialLinks from '@site/src/components/SocialLinks' +import skills from '@site/data/skills' + +import { Icon } from '@iconify/react' + +const variants: Variants = { + visible: i => ({ + opacity: 1, + y: 0, + transition: { + type: 'spring', + damping: 25, + stiffness: 100, + duration: 0.3, + delay: i * 0.3, + }, + }), + hidden: { opacity: 0, y: 30 }, +} + +function Skills() { + const { scrollYProgress } = useScroll() + + // 往下滚动 元素向上移动 + const y1 = useTransform(scrollYProgress, [0, 1], ['0%', '-500%'], { + clamp: false, + }) + + // 往下滚动 元素向下移动 + const y2 = useTransform(scrollYProgress, [0, 1], ['0%', '500%'], { + clamp: false, + }) + + return ( + <> + {skills.map((skill, index) => { + const yValue = index % 2 === 0 ? y1 : y2 + + return ( + + + + ) + })} + + ) +} + +function Circle() { + return
+} + +function Name() { + return ( + { + e.currentTarget.style.setProperty('--x', `${e.clientX}px`) + e.currentTarget.style.setProperty('--y', `${e.clientY}px`) + }} + > + 你好! 我是 + { + const bounding = e.currentTarget.getBoundingClientRect() + e.currentTarget.style.setProperty('--mouse-x', `${bounding.x}px`) + e.currentTarget.style.setProperty('--mouse-y', `${bounding.y}px`) + }} + > + 贾添植 + + 👋 + + ) +} + +export default function Hero() { + return ( + +
+ + + + {`锦衣未加身,独在夜中行。`} + + + + + + + + + + + + + + + ) +} diff --git a/src/pages/_components/Hero/styles.module.scss b/src/pages/_components/Hero/styles.module.scss new file mode 100644 index 0000000..65b7ad6 --- /dev/null +++ b/src/pages/_components/Hero/styles.module.scss @@ -0,0 +1,289 @@ +/* hero */ +.hero { + height: calc(100vh - 60px); + width: 100vw; + max-width: 100%; + margin: 0; + display: grid; + grid-template-columns: 8fr 11fr; + align-items: center; + position: relative; + + letter-spacing: 0.04em; + padding: 0; +} + +.intro { + padding: 1em; + padding-left: 4em; + position: relative; + z-index: 10; +} + +.intro > p { + margin: 24px 0; + color: hsl(215deg 19% 48%); + text-shadow: 0 0 #8c99ab; + font-size: 1rem; + text-align: justify; + letter-spacing: -0.04em; + line-height: 32px; +} + +.hero_text { + font-size: calc(1.5em + 1.2vw); +} + +.name { + --lighting-size: 300px; + --lighting-color: var(--ifm-color-primary); + --lighting-highlight-color: var(--ifm-color-primary-lightest); + + background-image: radial-gradient( + var(--lighting-highlight-color), + var(--lighting-color), + var(--lighting-color) + ); + background-size: var(--lighting-size) var(--lighting-size); + background-repeat: no-repeat; + + background-position-x: calc(var(--x) - var(--mouse-x) - calc(var(--lighting-size) / 2)); + background-position-y: calc(var(--y) - var(--mouse-y) - calc(var(--lighting-size) / 2)); + + background-color: var(--lighting-color); + + color: transparent; + background-clip: text; +} + +.wave { + margin-left: 2px; +} + +.background { + position: relative; + + width: 100%; + height: 90%; + z-index: 5; + place-items: center center; + align-self: flex-start; +} + +.background svg { + width: 100%; + height: auto; +} + +.circle { + position: absolute; + top: 0; + width: 100%; + height: 100%; + background: linear-gradient(90deg, rgb(150 255 244 / 81.3%) 0%, rgb(0 71 252 / 80.6%) 100%); + border-radius: 50%; + opacity: 0.3; + + // animation: heartbeat 10s infinite; + filter: blur(80px); + z-index: -1; +} + +.box { + position: absolute; + + display: inline-flex; + justify-content: center; + align-items: center; + background-color: transparent; + color: transparent; + + backdrop-filter: blur(2px); + + box-shadow: + inset 1px 1px 5px rgb(255 255 255 / 30%), + 0 0 5px rgb(0 0 0 / 20%); + + border-radius: 8px; + + padding: 0.5rem; + width: 3.5rem; + height: 3.5rem; +} + +@keyframes surround { + 0% { + transform: translateY(-25%) translateX(40%) rotate(0deg); + } + + 30% { + transform: translateY(0) translateX(0) rotate(90deg) scaleX(0.7); + } + + 50% { + transform: translateY(-25%) translateX(-40%) rotate(180deg); + } + + 70% { + transform: translateY(0) translateX(0) rotate(270deg) scaleX(0.7); + } + + 100% { + transform: translateY(-25%) translateX(40%) rotate(1turn); + } +} + +@keyframes heartbeat { + 0% { + transform: rotate(45deg); + opacity: 0.1; + } + + 50% { + transform: rotate(45deg); + opacity: 0.3; + } + + 100% { + transform: rotate(45deg); + opacity: 0.1; + } +} + +.buttonGroup { + display: flex; + gap: 0.5rem; + + margin-top: 1rem; +} + +.outer { + position: relative; + overflow: hidden; + padding: 2px; + width: max-content; + border-radius: 16px; + + transform: translateZ(0); +} + +.gradient { + position: absolute; + inset: 0; + height: 100%; + border-radius: 16px; + animation: surround -0.64s linear 4s infinite; + background: conic-gradient( + transparent 50deg, + var(--ifm-color-primary-light) 80deg, + transparent 100deg + ); + + filter: blur(8px); + transform-origin: center; + will-change: transform; +} + +.button { + position: relative; + z-index: 1; + align-items: center; + + text-align: center; + display: inline-block; + padding: 0.75em 1.5em; + font-weight: 600; + border: 1px solid hsl(0deg 0% 16% / 10%); + + background-color: #f9fafb; + border-radius: 16px; + + &:hover { + text-decoration: none; + } +} + +html[data-theme='dark'] { + .gradient { + background: conic-gradient( + transparent 50deg, + var(--ifm-color-primary-darker) 80deg, + transparent 100deg + ); + } + + .button { + border: 1px solid hsl(0deg 0% 100% / 10%); + background-color: #262626; + } +} + +@media (width <= 1000px) { + .hero { + grid-template-columns: 1fr; + grid-template-rows: max-content minmax(0, max-content); + align-items: start; + height: auto; + /* background-position: center bottom; + background-size: 70vh; */ + } + + .intro { + padding: 0 var(--ifm-spacing-horizontal); + padding-top: 4em; + display: flex; + flex-direction: column; + align-items: center; + } + + .background { + width: 100%; + justify-self: center; + padding-top: 4em; + height: 100%; + display: grid; + place-items: center; + } + + .background svg { + width: 90%; + height: auto; + } + + .box { + width: 3rem; + height: 3rem; + } + + .outer { + width: 200px; + } + + .button { + width: 100%; + } +} + +@media (width <= 570px) { + .hero { + height: auto; + } + + .background { + padding-top: 2em; + } + + .background svg { + width: 100%; + height: auto; + } + + .box { + width: 2rem; + height: 2rem; + } + + .intro { + padding-top: 2em; + } +} diff --git a/src/pages/_components/ProjectSection/index.tsx b/src/pages/_components/ProjectSection/index.tsx new file mode 100644 index 0000000..095b848 --- /dev/null +++ b/src/pages/_components/ProjectSection/index.tsx @@ -0,0 +1,117 @@ +import React, { useLayoutEffect, useRef } from 'react' +import clsx from 'clsx' +import { Project, projects } from '@site/data/projects' +import Translate from '@docusaurus/Translate' +import styles from './styles.module.scss' +import { + motion, + useAnimationFrame, + useMotionValue, + useScroll, + useSpring, + useTransform, + useVelocity, + wrap, +} from 'framer-motion' +import SectionTitle from '../SectionTitle' + +const removeHttp = (url: string) => { + return url.replace(/(^\w+:|^)\/\//, '') +} + +const defaultVelocity = 0.4 +const showProjects = projects.filter(i => i.preview) + +const Slider = ({ items }: { items: Project[] }) => { + // 初始速度 + let baseVelocity = -defaultVelocity + // 移动方向 + const directionFactor = useRef(1) + + const baseX = useMotionValue(0) + const { scrollY } = useScroll() + const scrollVelocity = useVelocity(scrollY) + const smoothVelocity = useSpring(scrollVelocity, { + damping: 50, + stiffness: 400, + }) + const velocityFactor = useTransform(smoothVelocity, [0, 1000], [0, 6], { + clamp: false, + }) + + useLayoutEffect(() => { + baseX.set(6) + }) + + const x = useTransform(baseX, v => `${wrap(10, -3 * showProjects.length, v)}%`) + + useAnimationFrame((time, delta) => { + let moveBy = directionFactor.current * baseVelocity * (delta / 1000) + + // if (velocityFactor.get() < 0) { + // directionFactor.current = -1 + // } else if (velocityFactor.get() > 0) { + // directionFactor.current = 1 + // } + + moveBy += directionFactor.current * moveBy * velocityFactor.get() + + // 重置进度 + if (baseX.get() <= -3 * showProjects.length) { + baseX.set(11) + } + + baseX.set(baseX.get() + moveBy) + }) + + const handleHoverStart = () => { + baseX.stop() + baseVelocity = 0 + } + + const handleHoverEnd = () => { + baseVelocity = -defaultVelocity + } + + return ( +
+ + {items.map((item, index) => { + return ( + + ) + })} + +
+ ) +} + +export default function ProjectSection() { + return ( +
+ + 项目展示 + +
+
+ +
+
+
+
+
+ ) +} diff --git a/src/pages/_components/ProjectSection/styles.module.scss b/src/pages/_components/ProjectSection/styles.module.scss new file mode 100644 index 0000000..cc36ff0 --- /dev/null +++ b/src/pages/_components/ProjectSection/styles.module.scss @@ -0,0 +1,139 @@ +.projectContainer { + max-width: 1280px; +} + +.content { + position: relative; + border-radius: var(--ifm-card-border-radius); +} + +.slider { + height: 180px; + margin: auto; + padding: 0.5rem 0; + position: relative; +} + +.slide-track { + display: flex; + width: 100%; + margin-bottom: 0.5rem; +} + +.slide { + height: 100px; + width: 200px; + margin: 0 0.5rem; + + .image { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 1rem; + padding: 0 1rem; + } + + a:hover { + text-decoration: none; + } +} + +.slideBody { + padding: 0.5rem 0; + text-align: center; + width: 100%; + + .title { + font-weight: 400; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 1.25rem; + margin: 0; + } + + .website { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--ifm-color-primary); + margin: 0; + } +} + +@keyframes scroll { + 0% { + transform: translate(0); + } + + 100% { + transform: translate(-20%); + } +} + +/* Gradient Boxes */ +.gradientBox { + position: absolute; + pointer-events: none; + top: 0; + height: 100%; + width: 20px; + z-index: 1; + margin-right: -1px; + margin-left: -1px; +} + +.leftBox { + left: 0; + background-image: linear-gradient(to right, var(--content-background-color), transparent); +} + +.rightBox { + right: 0; + background-image: linear-gradient(to left, var(--content-background-color), transparent); +} + +html[data-theme='dark'] .leftBox { + background-image: linear-gradient(to right, #18181baa, transparent); +} + +html[data-theme='dark'] .rightBox { + background-image: linear-gradient(to left, #18181baa, transparent); +} + +/* Media Queries */ +@media (width >= 640px) { + .slider { + height: 250px; + } + + .slide { + height: 200px; + width: 300px; + } + + .gradientBox { + width: 50px; + } +} + +@media (width >= 768px) { + .gradientBox { + width: 100px; + } +} + +@media (width >= 1024px) { + .slider { + height: 280px; + } + + .slide { + height: 200px; + width: 400px; + } + + .gradientBox { + width: 200px; + } +} diff --git a/src/pages/_components/SectionTitle/index.tsx b/src/pages/_components/SectionTitle/index.tsx new file mode 100644 index 0000000..b048852 --- /dev/null +++ b/src/pages/_components/SectionTitle/index.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import { Icon } from '@iconify/react' +import Link from '@docusaurus/Link' +import Translate from '@docusaurus/Translate' + +import styles from './styles.module.scss' + +interface Props { + icon?: string + href?: string + children: React.ReactNode +} + +export default function SectionTitle({ children, icon, href }: Props) { + return ( +
+

+ {icon && } + {children} +

+ {href && ( + + 查看更多 + + + )} +
+ ) +} diff --git a/src/pages/_components/SectionTitle/styles.module.scss b/src/pages/_components/SectionTitle/styles.module.scss new file mode 100644 index 0000000..9a7b0d8 --- /dev/null +++ b/src/pages/_components/SectionTitle/styles.module.scss @@ -0,0 +1,30 @@ +.sectionTitle { + display: inline-flex; + justify-content: space-between; + align-items: center; + + padding: 0 var(--ifm-spacing-horizontal); + margin-top: 2rem; + margin-bottom: 1rem; + width: 100%; + + h2 { + display: inline-flex; + align-items: center; + gap: 4px; + margin: 0; + font-size: 1.1rem; + } + + .moreButton { + display: inline-flex; + align-items: center; + justify-content: center; + + font-size: 1rem; + + &:hover { + text-decoration: none; + } + } +} diff --git a/src/pages/about.mdx b/src/pages/about.mdx new file mode 100644 index 0000000..842349a --- /dev/null +++ b/src/pages/about.mdx @@ -0,0 +1,114 @@ +--- +id: about +title: 自我介绍 +description: 愧怍的自我介绍 +hide_table_of_contents: true +--- + +import { Icon } from '@iconify/react' +import Comment from '@site/src/components/Comment' +import social from '@site/data/social' + +# 👋 你好! 我是贾添植 + +🧑 一名即将毕业的大学生,来自中国 + +🌱 保持学习,希望在有限的时间内,学到无限的可能 + +🐛 曾写过比较长时间的爬虫,学过 Web 与安卓逆向,现在主攻 JS/TS 全栈,并将长期发展下去。 + +💡 我通常会将我的学习过程总结为项目或博客文章的形式,并乐意与他人分享。当其他人学习该技术时,他们可以参考我的项目或文章,我认为这非常有意义。 + +### 名字由来 + +愧怍,有愧疚/惭愧之意。也是本人使用此名所想表达的意思。 + +我曾因某些错误的做法而感到自责苦恼。希望以这个名字不断激励自我,反省过往,不想再次辜负我自己所经历的那么多事物,仅此而已。 + +### 兴趣爱好 + +- **手指极限** 入坑长达 8 年(现已退坑),你所熟知的转笔、魔方、花切等有关手指旋转的我都能够杂耍一番。 + +- **电音迷** 歌单只有电音,也只听电音。戴上耳机,沉浸在无限律动中。有生之年定要制作首电子音乐。 + +- **编程开发** 将想法付诸实践, 享受创造的乐趣。 + +### 设备 + +- MacBook Pro M2 14 + +- Xiaomi MIX Fold 2 (折叠屏不那么好用,准备换 iphone 16 了) + +- Google Pixel 4XL + +- Xiaomi Watch S2 + +- 米家智能音频眼镜 + +- 米家显示器挂灯 (买过最实用的设备) + +- Keychron K3 + +- Nuphy Air75 V2 + +- 罗技 MX Master 3 + +- ~~坚果 R2 (没收)~~ + +- ~~机械革命 X3 (2019) (没收)~~ + +- 联想小新 Pro 14 (2021) 已闲置 + +- 台式电脑 (AMD 5900X + 64G + RX 6750 GRE) + +### 我会什么 + +- 易语言程序(能写但不想写,因为开发体验很差) + +- 自动化脚本(很久没写了) + +- 爬虫/协议复现(但更愿意用 ts 而不是 python) + +- 逆向分析(网页 js 不成问题,安卓止步 so 层,ios 打扰了) + +- Chrome 扩展程序(写个高级点的 demo 应该没问题) + +- Electron (曾经写过,如今忘了差不多) + +- React Native(正学中) + +- 小程序(能写但不想写,因为开发体验很差) + +- Web 开发(只用 ts 进行全栈开发) + +### 联系方式 + +

+ + kuizuo +

+ +

+ + hi@kuizuo.cn +

+ +

+ + QQ +

+ +

+ + wx +

+ +

+ kuizuo +

+ +--- + +> **既然都看到这了,不妨留下你的评论。** + + diff --git a/src/pages/friends/_components/FriendCard/index.tsx b/src/pages/friends/_components/FriendCard/index.tsx new file mode 100644 index 0000000..c46fa5e --- /dev/null +++ b/src/pages/friends/_components/FriendCard/index.tsx @@ -0,0 +1,28 @@ +import React, { memo } from 'react' +import clsx from 'clsx' +import Link from '@docusaurus/Link' + +import styles from './styles.module.css' +import { type Friend } from '@site/data/friends' + +const FriendCard = memo(({ friend }: { friend: Friend }) => ( +
  • + {friend.title} +
    +
    +

    + + {friend.title} + +

    +
    +

    {friend.description}

    +
    +
  • +)) + +export default FriendCard diff --git a/src/pages/friends/_components/FriendCard/styles.module.css b/src/pages/friends/_components/FriendCard/styles.module.css new file mode 100644 index 0000000..1fb135e --- /dev/null +++ b/src/pages/friends/_components/FriendCard/styles.module.css @@ -0,0 +1,68 @@ +.friendCard { + background-color: var(--ifm-card-background-color); + border-radius: var(--ifm-card-border-radius); + display: flex; + flex-direction: row; + overflow: hidden; + + transition: all 0.3s; +} + +.friendCard:hover { + transform: translateY(-5px) scale(1.01, 1.01); + box-shadow: 0px 3px 10px 0 rgba(164, 190, 217, 0.3); + background-color: rgb(229, 231, 235, 0.3); +} + +.friendCardImage { + width: 100px; + height: 100px; + min-width: 100px; + border-radius: 50%; + object-fit: contain; + transition: all 0.3s; +} + +.friendCard:hover .friendCardImage { + border-radius: 30%; +} + +.friendCardHeader { + display: flex; + align-items: center; + margin-bottom: 4px; +} + +.friendCardTitle { + margin-bottom: 0; + flex: 1 1 auto; +} + +.friendCardTitle a { + text-decoration: none; + background: linear-gradient( + var(--ifm-color-primary), + var(--ifm-color-primary) + ) + 0% 100% / 0% 1px no-repeat; + transition: background-size ease-out 200ms; +} + +.friendCardTitle a:not(:focus):hover { + background-size: 100% 1px; +} + +.friendCardBody { + padding: 0.5rem var(--ifm-card-horizontal-spacing); +} + +.friendCardDesc { + font-size: smaller; + line-height: 1.66; + width: 100%; + margin: 0; + display: -webkit-box; + overflow: hidden; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} diff --git a/src/pages/friends/index.tsx b/src/pages/friends/index.tsx new file mode 100644 index 0000000..da672ac --- /dev/null +++ b/src/pages/friends/index.tsx @@ -0,0 +1,73 @@ +import React from 'react' +import Layout from '@theme/Layout' +import CodeBlock from '@theme/CodeBlock' + +import FriendCard from './_components/FriendCard' +import { Friends } from '@site/data/friends' + +import styles from './styles.module.css' +import { motion } from 'framer-motion' + +const TITLE = '友链' +const DESCRIPTION = '有很多良友,胜于有很多财富。' +const ADD_FRIEND_URL = 'https://github.com/kuizuo/blog/edit/main/data/friends.tsx' +const SITE_INFO = ` +title: '贾添植' +description: '锦衣未加身,独在夜中行。' +website: 'https://jiatianzhi.xyz' +avatar: 'https://jiatianzhi.xyz/img/logo.png' +` + +function SiteInfo() { + return ( +
    + + {SITE_INFO} + +
    + ) +} + +function FriendHeader() { + return ( +
    +

    {TITLE}

    +

    {DESCRIPTION}

    + + 🔗 申请友链 + +
    + ) +} + +function FriendCards() { + const friends = Friends + + return ( +
    +
    +
      + {friends.map(friend => ( + + ))} +
    +
    +
    + ) +} + +export default function FriendLink(): JSX.Element { + const ref = React.useRef(null) + + return ( + + + + + + + + + + ) +} diff --git a/src/pages/friends/styles.module.css b/src/pages/friends/styles.module.css new file mode 100644 index 0000000..9f76eeb --- /dev/null +++ b/src/pages/friends/styles.module.css @@ -0,0 +1,34 @@ +.friendContainer { + max-width: 1024px; + margin: 0 auto; + padding: 0.5rem 1rem; +} + +.friendList { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 24px; +} + +.dragBox { + position: sticky; + bottom: 1rem; + left: 1rem; + + display: inline-flex; + text-align: right; + cursor: move; +} + +.siteInfo { + text-align: left; + width: 400px; + font-size: 0.9rem; + border-radius: var(--ifm-pre-border-radius); + border: 0.1rem solid rgba(0, 0, 0, 0.1); + --ifm-leading: 0; +} + +.siteInfo code { + --ifm-pre-padding: 0 1rem; +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx new file mode 100644 index 0000000..449b063 --- /dev/null +++ b/src/pages/index.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import Layout from '@theme/Layout' +import Hero from './_components/Hero' +import BlogSection from './_components/BlogSection' +import FeaturesSection from './_components/FeaturesSection' +import HomepageProject from './_components/ProjectSection' +import useDocusaurusContext from '@docusaurus/useDocusaurusContext' + +export default function Home(): JSX.Element { + const { + siteConfig: { customFields, tagline }, + } = useDocusaurusContext() + const { description } = customFields as { description: string } + + return ( + +
    + +
    + + + +
    +
    +
    + ) +} diff --git a/src/pages/project/_components/ShowcaseCard/index.tsx b/src/pages/project/_components/ShowcaseCard/index.tsx new file mode 100644 index 0000000..73fa2db --- /dev/null +++ b/src/pages/project/_components/ShowcaseCard/index.tsx @@ -0,0 +1,76 @@ +import React, { memo } from 'react' +import clsx from 'clsx' +import Image from '@theme/IdealImage' +import Link from '@docusaurus/Link' +import Translate from '@docusaurus/Translate' +import styles from './styles.module.css' +import FavoriteIcon from '@site/src/components/svgIcons/FavoriteIcon' +import Tooltip from '@site/src/components/Tooltip' +import { Tags, TagList, type TagType, type Project, type Tag } from '@site/data/projects' +import { sortBy } from '@site/src/utils/jsUtils' + +const TagComp = React.forwardRef(({ label, color, description }, ref) => ( +
  • + {label.toLowerCase()} + +
  • +)) + +function ShowcaseCardTag({ tags }: { tags: TagType[] }) { + const tagObjects = tags.map(tag => ({ tag, ...Tags[tag] })) + + // Keep same order for all tags + const tagObjectsSorted = sortBy(tagObjects, tagObject => TagList.indexOf(tagObject.tag)) + + return ( + <> + {tagObjectsSorted.map((tagObject, index) => { + const id = `showcase_card_tag_${tagObject.tag}` + + return ( + + + + ) + })} + + ) +} + +const ShowcaseCard = memo(({ project }: { project: Project }) => { + return ( +
    + {project.preview && ( +
    + {project.title} +
    + )} +
    +
    +

    + + {project.title} + +

    + {project.tags.includes('favorite') && ( + + )} + {project.source && ( + + 源码 + + )} +
    +

    {project.description}

    +
    +
      + +
    +
    + ) +}) + +export default ShowcaseCard diff --git a/src/pages/project/_components/ShowcaseCard/styles.module.css b/src/pages/project/_components/ShowcaseCard/styles.module.css new file mode 100644 index 0000000..247570f --- /dev/null +++ b/src/pages/project/_components/ShowcaseCard/styles.module.css @@ -0,0 +1,109 @@ +.showcaseCard { + position: relative; + box-shadow: var(--blog-item-shadow); + transition: opacity 0.5s; + will-change: transform; + overflow: hidden; + touch-action: none; +} + +.showcaseCard::before { + display: block; + font-weight: bold; + height: 0; + overflow: hidden; + visibility: hidden; +} + +.showcaseCardImage { + overflow: hidden; + height: 150px; + border-bottom: 2px solid var(--ifm-color-emphasis-200); +} + +.showcaseCardHeader { + display: flex; + align-items: center; + margin-bottom: 12px; +} + +.showcaseCardTitle { + margin-bottom: 0; + flex: 1 1 auto; +} + +.showcaseCardTitle a { + text-decoration: none; + background: linear-gradient( + var(--ifm-color-primary), + var(--ifm-color-primary) + ) + 0% 100% / 0% 1px no-repeat; + transition: background-size ease-out 200ms; +} + +.showcaseCardTitle a:not(:focus):hover { + background-size: 100% 1px; +} + +.showcaseCardTitle, +.showcaseCardHeader .svgIconFavorite { + margin-right: 0.25rem; +} + +.showcaseCardHeader .svgIconFavorite { + color: var(--site-color-svg-icon-favorite); +} + +.showcaseCardSrcBtn { + margin-left: 6px; + padding-left: 12px; + padding-right: 12px; + border: none; +} + +.showcaseCardSrcBtn:focus-visible { + background-color: var(--ifm-color-secondary-dark); +} + +html[data-theme='dark'] .showcaseCardSrcBtn { + background-color: var(--ifm-color-emphasis-200) !important; + color: inherit; +} + +html[data-theme='dark'] .showcaseCardSrcBtn:hover { + background-color: var(--ifm-color-emphasis-300) !important; +} + +.showcaseCardBody { + font-size: smaller; + line-height: 1.66; +} + +.cardFooter { + display: flex; + flex-wrap: wrap; +} + +.tag { + font-size: 0.675rem; + border: 1px solid var(--ifm-color-secondary-darkest); + cursor: default; + margin-right: 6px; + margin-bottom: 6px !important; + border-radius: 12px; + display: inline-flex; + align-items: center; +} + +.tag .textLabel { + margin-left: 8px; +} + +.tag .colorLabel { + width: 7px; + height: 7px; + border-radius: 50%; + margin-left: 6px; + margin-right: 6px; +} diff --git a/src/pages/project/_components/ShowcaseFilterToggle/index.tsx b/src/pages/project/_components/ShowcaseFilterToggle/index.tsx new file mode 100644 index 0000000..4b53f3e --- /dev/null +++ b/src/pages/project/_components/ShowcaseFilterToggle/index.tsx @@ -0,0 +1,60 @@ +import React, { useState, useEffect, useCallback } from 'react' +import { useHistory, useLocation } from '@docusaurus/router' + +import { prepareUserState } from '../../index' + +import styles from './styles.module.css' +import clsx from 'clsx' + +export type Operator = 'OR' | 'AND' + +export const OperatorQueryKey = 'operator' + +export function readOperator(search: string): Operator { + return (new URLSearchParams(search).get(OperatorQueryKey) ?? 'OR') as Operator +} + +export default function ShowcaseFilterToggle(): JSX.Element { + const id = 'showcase_filter_toggle' + const location = useLocation() + const history = useHistory() + const [operator, setOperator] = useState(false) + useEffect(() => { + setOperator(readOperator(location.search) === 'AND') + }, [location]) + const toggleOperator = useCallback(() => { + setOperator(o => !o) + const searchParams = new URLSearchParams(location.search) + searchParams.delete(OperatorQueryKey) + if (!operator) { + searchParams.append(OperatorQueryKey, operator ? 'OR' : 'AND') + } + history.push({ + ...location, + search: searchParams.toString(), + state: prepareUserState(), + }) + }, [operator, location, history]) + + return ( +
    + { + if (e.key === 'Enter') { + toggleOperator() + } + }} + checked={operator} + /> + +
    + ) +} diff --git a/src/pages/project/_components/ShowcaseFilterToggle/styles.module.css b/src/pages/project/_components/ShowcaseFilterToggle/styles.module.css new file mode 100644 index 0000000..6bd397b --- /dev/null +++ b/src/pages/project/_components/ShowcaseFilterToggle/styles.module.css @@ -0,0 +1,50 @@ +.checkboxLabel { + --height: 25px; + --width: 80px; + --border: 2px; + display: flex; + width: var(--width); + height: var(--height); + position: relative; + border-radius: var(--height); + border: var(--border) solid var(--ifm-color-primary-darkest); + cursor: pointer; + justify-content: space-around; + opacity: 0.75; + transition: opacity var(--ifm-transition-fast) + var(--ifm-transition-timing-default); + box-shadow: var(--ifm-global-shadow-md); +} + +.checkboxLabel:hover { + opacity: 1; + box-shadow: var(--ifm-global-shadow-md), + 0 0 2px 1px var(--ifm-color-primary-dark); +} + +.checkboxLabel::after { + position: absolute; + content: ''; + inset: 0; + width: calc(var(--width) / 2); + height: 100%; + border-radius: var(--height); + background-color: var(--ifm-color-primary-darkest); + transition: transform var(--ifm-transition-fast) + var(--ifm-transition-timing-default); + transform: translateX(calc(var(--width) / 2 - var(--border))); +} + +input:focus-visible ~ .checkboxLabel::after { + outline: 2px solid currentColor; +} + +.checkboxLabel > * { + font-size: 0.8rem; + color: inherit; + transition: opacity 150ms ease-in 50ms; +} + +input:checked ~ .checkboxLabel::after { + transform: translateX(calc(-1 * var(--border))); +} diff --git a/src/pages/project/_components/ShowcaseTagSelect/index.tsx b/src/pages/project/_components/ShowcaseTagSelect/index.tsx new file mode 100644 index 0000000..18eac40 --- /dev/null +++ b/src/pages/project/_components/ShowcaseTagSelect/index.tsx @@ -0,0 +1,90 @@ +import React, { + type ComponentProps, + type ReactNode, + type ReactElement, + useCallback, + useState, + useEffect, +} from 'react' +import { useHistory, useLocation } from '@docusaurus/router' +import { toggleListItem } from '@site/src/utils/jsUtils' +import { prepareUserState } from '../../index.tsx' +import type { TagType } from '@site/data/users' + +import styles from './styles.module.css' + +interface Props extends ComponentProps<'input'> { + icon: ReactElement> + label: ReactNode + tag: TagType +} + +const TagQueryStringKey = 'tags' + +export function readSearchTags(search: string): TagType[] { + return new URLSearchParams(search).getAll(TagQueryStringKey) as TagType[] +} + +function replaceSearchTags(search: string, newTags: TagType[]) { + const searchParams = new URLSearchParams(search) + searchParams.delete(TagQueryStringKey) + newTags.forEach(tag => searchParams.append(TagQueryStringKey, tag)) + return searchParams.toString() +} + +const ShowcaseTagSelect = React.forwardRef( + ({ id, icon, label, tag, ...rest }, ref) => { + const location = useLocation() + const history = useHistory() + const [selected, setSelected] = useState(false) + useEffect(() => { + const tags = readSearchTags(location.search) + setSelected(tags.includes(tag)) + }, [tag, location]) + const toggleTag = useCallback(() => { + const tags = readSearchTags(location.search) + const newTags = toggleListItem(tags, tag) + const newSearch = replaceSearchTags(location.search, newTags) + history.push({ + ...location, + search: newSearch, + state: prepareUserState(), + }) + }, [tag, location, history]) + return ( + <> + { + if (e.key === 'Enter') { + toggleTag() + } + }} + onFocus={e => { + if (e.relatedTarget) { + e.target.nextElementSibling?.dispatchEvent( + new KeyboardEvent('focus'), + ) + } + }} + onBlur={e => { + e.target.nextElementSibling?.dispatchEvent( + new KeyboardEvent('blur'), + ) + }} + onChange={toggleTag} + checked={selected} + {...rest} + /> + + + ) + }, +) + +export default ShowcaseTagSelect diff --git a/src/pages/project/_components/ShowcaseTagSelect/styles.module.css b/src/pages/project/_components/ShowcaseTagSelect/styles.module.css new file mode 100644 index 0000000..91a6adb --- /dev/null +++ b/src/pages/project/_components/ShowcaseTagSelect/styles.module.css @@ -0,0 +1,31 @@ +.checkboxLabel:hover { + opacity: 1; + box-shadow: 0 0 2px 1px var(--ifm-color-secondary-darkest); +} + +input[type='checkbox'] + .checkboxLabel { + display: flex; + align-items: center; + cursor: pointer; + line-height: 1.5; + border-radius: 4px; + padding: 0.275rem 0.8rem; + opacity: 0.85; + transition: opacity 200ms ease-out; + border: 2px solid var(--ifm-color-secondary-darkest); +} + +input:focus-visible + .checkboxLabel { + outline: 2px solid currentColor; +} + +input:checked + .checkboxLabel { + opacity: 0.9; + background-color: var(--site-color-checkbox-checked-bg); + border: 2px solid var(--ifm-color-primary-darkest); +} + +input:checked + .checkboxLabel:hover { + opacity: 0.75; + box-shadow: 0 0 2px 1px var(--ifm-color-primary-dark); +} diff --git a/src/pages/project/index.tsx b/src/pages/project/index.tsx new file mode 100644 index 0000000..ebd867d --- /dev/null +++ b/src/pages/project/index.tsx @@ -0,0 +1,109 @@ +import React from 'react' +import clsx from 'clsx' +import { translate } from '@docusaurus/Translate' +import useDocusaurusContext from '@docusaurus/useDocusaurusContext' +import ShowcaseCard from './_components/ShowcaseCard' +import { projects, groupByProjects, projectTypeMap } from '@site/data/projects' + +import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment' + +import styles from './styles.module.css' +import MyLayout from '@site/src/theme/MyLayout' +import { upperFirst } from '@site/src/utils/jsUtils' + +const TITLE = translate({ + id: 'theme.project.title', + message: '项目', +}) +const DESCRIPTION = translate({ + id: 'theme.project.description', + message: '学而无用,不如学而用之。这里是我在技术领域中努力实践和应用的最佳证明。', +}) + +// const GITHUB_URL = 'https://github.com/kuizuo' + +type ProjectState = { + scrollTopPosition: number + focusedElementId: string | undefined +} + +export function prepareUserState(): ProjectState | undefined { + if (ExecutionEnvironment.canUseDOM) { + return { + scrollTopPosition: window.scrollY, + focusedElementId: document.activeElement?.id, + } + } + + return undefined +} + +function ShowcaseHeader() { + return ( +
    +

    {TITLE}

    +

    {DESCRIPTION}

    + {/* + 前往 Github 克隆项目 + */} +
    + ) +} + +function ShowcaseCards() { + const { i18n } = useDocusaurusContext() + const lang = i18n.currentLocale + + if (projects.length === 0) { + return ( +
    +
    +

    No result

    +
    +
    + ) + } + + return ( +
    + <> +
    +
    + + {Object.entries(groupByProjects).map(([key, value]) => { + return ( +
    +
    +

    {upperFirst(lang === 'en' ? key : projectTypeMap[key])}

    +
    +
      + {value.map(project => ( + + ))} +
    +
    + ) + })} +
    + +
    + ) +} + +function Showcase(): JSX.Element { + return ( + +
    + + +
    +
    + ) +} + +export default Showcase diff --git a/src/pages/project/styles.module.css b/src/pages/project/styles.module.css new file mode 100644 index 0000000..19b7cdd --- /dev/null +++ b/src/pages/project/styles.module.css @@ -0,0 +1,95 @@ +.filterCheckbox { + justify-content: space-between; +} + +.filterCheckbox, +.checkboxList { + display: flex; + align-items: center; +} + +.filterCheckbox > div:first-child { + display: flex; + flex: 1 1 auto; + align-items: center; +} + +.filterCheckbox > div > * { + margin-bottom: 0; + margin-right: 8px; +} + +.checkboxList { + flex-wrap: wrap; +} + +.checkboxList, +.showcaseList { + padding: 0; + list-style: none; +} + +.checkboxListItem { + user-select: none; + white-space: nowrap; + height: 32px; + font-size: 0.8rem; + margin-top: 0.5rem; + margin-right: 0.5rem; +} + +.checkboxListItem:last-child { + margin-right: 0; +} + +.searchContainer { + margin-left: auto; +} + +.searchContainer input { + height: 30px; + border-radius: 15px; + padding: 10px; + border: 1px solid gray; +} + +.showcaseList { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 24px; +} + +.showcaseFavorite { + padding-top: 2rem; + padding-bottom: 2rem; + background-color: var(--site-color-favorite-background); +} + +.showcaseFavoriteHeader { + display: flex; + justify-content: center; + align-items: center; +} + +.showcaseFavoriteHeader > h2 { + margin-bottom: 0; +} + +.showcaseFavoriteHeader > svg { + width: 30px; + height: 30px; +} + +.svgIconFavoriteXs, +.svgIconFavorite { + color: var(--site-color-svg-icon-favorite); +} + +.svgIconFavoriteXs { + margin-left: 0.625rem; + font-size: 1rem; +} + +.svgIconFavorite { + margin-left: 1rem; +} diff --git a/src/pages/resources/_components/ResourceCard/index.tsx b/src/pages/resources/_components/ResourceCard/index.tsx new file mode 100644 index 0000000..0c04a84 --- /dev/null +++ b/src/pages/resources/_components/ResourceCard/index.tsx @@ -0,0 +1,43 @@ +import React, { memo } from 'react' +import clsx from 'clsx' +import Link from '@docusaurus/Link' + +import styles from './styles.module.css' +import { type Resource } from '@site/data/resources' +import Tooltip from '@site/src/components/Tooltip' + +const ResourceCard = memo(({ resource }: { resource: Resource }) => ( +
  • + {resource.name} +
    +
    +

    + + {resource.name} + +

    +
    + +

    {resource.desc}

    +
    +
    +
  • +)) + +export default ResourceCard diff --git a/src/pages/resources/_components/ResourceCard/styles.module.css b/src/pages/resources/_components/ResourceCard/styles.module.css new file mode 100644 index 0000000..cc2b5f2 --- /dev/null +++ b/src/pages/resources/_components/ResourceCard/styles.module.css @@ -0,0 +1,70 @@ +.resourceCard { + background-color: var(--ifm-card-background-color); + border-radius: 8px; + border: 1px solid var(--ifm-toc-border-color); + height: 100%; + display: flex; + flex-direction: row; + overflow: hidden; + transition: all 0.3s ease-in-out; +} + +.resourceCard:hover { + box-shadow: 0 10px 20px -10px rgb(0 0 0 / 20%); + transform: translateY(-5px); +} + +html[data-theme='dark'] .resourceCard:hover { + box-shadow: 0 10px 20px -10px rgb(255 255 255 / 20%); + transform: translateY(-5px); +} + +.resourceCardImage { + align-self: center; + width: 64px; + height: 64px; + min-width: 64px; + object-fit: contain; +} + +.resourceCardHeader { + display: flex; + align-items: center; + margin-bottom: 4px; +} + +.resourceCardTitle { + margin-bottom: 0; + flex: 1 1 auto; +} + +.resourceCardTitle a { + text-decoration: none; + background: linear-gradient( + var(--ifm-color-primary), + var(--ifm-color-primary) + ) + 0% 100% / 0% 1px no-repeat; + transition: background-size ease-out 200ms; +} + +.resourceCardTitle a:not(:focus):hover { + background-size: 100% 1px; +} + +.resourceCardBody { + padding: 0.2rem 0 0 var(--ifm-card-horizontal-spacing); +} + +.resourceCardDesc { + cursor: pointer; + font-size: smaller; + line-height: 1.66; + width: 100%; + margin: 0; + /* stylelint-disable-next-line value-no-vendor-prefix */ + display: -webkit-box; + overflow: hidden; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} diff --git a/src/pages/resources/index.tsx b/src/pages/resources/index.tsx new file mode 100644 index 0000000..a92bab5 --- /dev/null +++ b/src/pages/resources/index.tsx @@ -0,0 +1,86 @@ +import React from 'react' +import clsx from 'clsx' +import Link from '@docusaurus/Link' +import { PageMetadata, HtmlClassNameProvider, ThemeClassNames } from '@docusaurus/theme-common' +import Layout from '@theme/Layout' +import ResourceCard from './_components/ResourceCard' +import BackToTopButton from '@theme/BackToTopButton' +import { resourceData } from '@site/data/resources' +import styles from './resource.module.css' + +function CategorySidebar() { + const sidebar = { + title: '', + items: resourceData.map(w => ({ title: w.name, permalink: `#${w.name}` })), + } + + return ( + + ) +} + +function CategoryList() { + return ( +
    + {resourceData.map(cate => ( +
    +
    +

    + {cate.name} + +

    +
    +
    +
      + {cate.resources.map(resource => ( + + ))} +
    +
    +
    + ))} +
    + ) +} + +export default function Resources() { + const title = '网址导航' + const description = '整合日常开发常用,推荐的网站导航页' + + return ( + + + +
    +
    + +
    + +
    +
    +
    + +
    +
    + ) +} diff --git a/src/pages/resources/resource.module.css b/src/pages/resources/resource.module.css new file mode 100644 index 0000000..747ce5c --- /dev/null +++ b/src/pages/resources/resource.module.css @@ -0,0 +1,80 @@ +.sidebar { + max-height: calc(100vh - (var(--ifm-navbar-height) + 2rem)); + overflow-y: auto; + overflow-x: hidden; + position: sticky; + top: calc(var(--ifm-navbar-height) + 2rem); +} + +.sidebarItemTitle { + font-size: var(--ifm-h3-font-size); + font-weight: var(--ifm-font-weight-bold); + font-family: var(--ifm-heading-font-family); +} + +.sidebarItemList { + font-size: 0.9rem; +} + +.sidebarItem { + margin-top: 0.7rem; +} + +.sidebarItemLink { + color: var(--ifm-font-color-base); + display: block; +} + +.sidebarItemLink:hover { + text-decoration: none; +} + +.sidebarItemLinkActive { + color: var(--ifm-color-primary) !important; +} + +@media (max-width: 996px) { + .sidebar { + display: none; + } +} + +.hero { + align-items: center; + background-color: var(--ifm-color-primary); + color: var(--ifm-font-color-base-inverse); + display: flex; + padding: 2rem 2rem; +} + +.heroTitle { + font-size: 2rem !important; + font-weight: 700; + line-height: 2rem; + color: #fff; +} + +.heroDesc { + margin-bottom: 0; +} + +.cateHeader { + font-size: 1rem; + font-weight: 600; +} + +.cateHeader > h2 { + font-family: sans-serif; +} + +.cateBody { + display: flex; + padding: 0 1rem; +} + +.resourceList { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 16px; + padding: 0; +} diff --git a/src/plugin/plugin-content-blog/index.js b/src/plugin/plugin-content-blog/index.js new file mode 100644 index 0000000..5dfb22a --- /dev/null +++ b/src/plugin/plugin-content-blog/index.js @@ -0,0 +1,27 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const blogPluginExports = require('@docusaurus/plugin-content-blog'); +const { default: blogPlugin } = blogPluginExports; + +async function blogPluginEnhanced(context, options) { + const blogPluginInstance = await blogPlugin(context, options); + + return { + ...blogPluginInstance, + async contentLoaded({ content, allContent, actions }) { + // Create default plugin pages + await blogPluginInstance.contentLoaded({ content, allContent, actions }); + + // Create your additional pages + const { blogPosts, blogTags } = content; + const { setGlobalData } = actions; + + setGlobalData({ + posts: blogPosts.slice(0, 10), // Only store 10 posts + postNum: blogPosts.length, + tagNum: Object.keys(blogTags).length, + }); + }, + }; +} + +module.exports = Object.assign({}, blogPluginExports, { default: blogPluginEnhanced }); diff --git a/src/style.d.ts b/src/style.d.ts new file mode 100644 index 0000000..a73d4ed --- /dev/null +++ b/src/style.d.ts @@ -0,0 +1,9 @@ +declare module '*.module.scss' { + const classes: { readonly [key: string]: string } + export default classes +} + +declare module '*.scss' { + const src: string + export default src +} diff --git a/src/sw.js b/src/sw.js new file mode 100644 index 0000000..077a8aa --- /dev/null +++ b/src/sw.js @@ -0,0 +1,19 @@ +import { registerRoute } from 'workbox-routing' +import { StaleWhileRevalidate } from 'workbox-strategies' + +export default function swCustom(params) { + if (params.debug) { + console.log('[Docusaurus-PWA][SW]: running swCustom code', params) + } + + // Cache responses from external resources + registerRoute( + context => + [ + /graph\.facebook\.com\/.*\/picture/, + /netlify\.com\/img/, + /avatars1\.githubusercontent/, + ].some(regex => context.url.href.match(regex)), + new StaleWhileRevalidate(), + ) +} diff --git a/src/theme/BlogArchivePage/index.tsx b/src/theme/BlogArchivePage/index.tsx new file mode 100644 index 0000000..6583120 --- /dev/null +++ b/src/theme/BlogArchivePage/index.tsx @@ -0,0 +1,143 @@ +import React from 'react' +import Link from '@docusaurus/Link' +import Translate, { translate } from '@docusaurus/Translate' +import clsx from 'clsx' +import { + PageMetadata, + HtmlClassNameProvider, + ThemeClassNames, +} from '@docusaurus/theme-common' +import type { ArchiveBlogPost, Props } from '@theme/BlogArchivePage' +import { Icon } from '@iconify/react' +import styles from './styles.module.css' + +import { motion, Variants } from 'framer-motion' + +import dayjs from 'dayjs' +import MyLayout from '../MyLayout' + +type YearProp = { + year: string + posts: ArchiveBlogPost[] +} + +const variants: Variants = { + from: { opacity: 0.01, y: 50 }, + to: i => ({ + opacity: 1, + y: 0, + transition: { + type: 'spring', + damping: 25, + stiffness: 100, + bounce: 0.2, + duration: 0.3, + delay: i * 0.1, + }, + }), +} + +function Year({ posts }: YearProp) { + return ( + <> +
      + {posts.map((post, i) => ( + + + + {post.metadata.title} + + + ))} +
    + + ) +} + +function YearsSection({ years }: { years: YearProp[] }) { + return ( +
    + {years.map((_props, idx) => ( + +
    +

    {_props.year}

    + + {(years[idx] as YearProp).posts.length} + + +
    + +
    + ))} +
    + ) +} + +function listPostsByYears(blogPosts: readonly ArchiveBlogPost[]): YearProp[] { + const postsByYear = blogPosts.reduceRight((posts, post) => { + const year = post.metadata.date.split('-')[0]! + const yearPosts = posts.get(year) ?? [] + return posts.set(year, [post, ...yearPosts]) + }, new Map()) + + return Array.from(postsByYear, ([year, posts]) => ({ + year, + posts, + })).reverse() +} + +export default function BlogArchive({ archive }: Props) { + const title = translate({ + id: 'theme.blog.archive.title', + message: 'Archive', + description: 'The page & hero title of the blog archive page', + }) + const description = translate({ + id: 'theme.blog.archive.description', + message: 'Archive', + description: 'The page & hero description of the blog archive page', + }) + + const years = listPostsByYears(archive.blogPosts) + return ( + + + +

    + + {title} +

    +
    + + {`共 {total} 篇文章`} + +
    + {years.length > 0 && } +
    +
    + ) +} diff --git a/src/theme/BlogArchivePage/styles.module.css b/src/theme/BlogArchivePage/styles.module.css new file mode 100644 index 0000000..8a6aba4 --- /dev/null +++ b/src/theme/BlogArchivePage/styles.module.css @@ -0,0 +1,127 @@ +.archiveTitle { + display: inline-flex; + align-items: center; +} + +.archiveCount { + text-align: right; + margin-top: -2.5rem; + font-size: 0.85rem; + opacity: 0.8; +} + +.archiveYear { + position: relative; + display: inline-flex; + justify-content: space-between; + align-items: flex-end; + margin: 1rem 0; + width: 100%; + font-weight: 400; + font-size: 1.4rem; +} + +.archiveYear span { + font-size: 0.85rem; + font-weight: 300; + color: var(--ifm-secondary-text-color); +} + +.archiveYearTitle { + display: inline-flex; + margin: 0; + font-size: 1.4rem; + font-weight: 400; + margin-right: 0.5rem; + color: var(--ifm-text-color); +} + +.archiveYearTitle::before { + margin-right: 0.5em; + width: 0.3em; + display: inline-block; + background: var(--ifm-color-primary); + color: transparent; + content: '.'; +} + +.archiveList { + padding: 0; + margin: 0; +} + +.archiveItem { + position: relative; + display: flex; + list-style: none; +} + +.archiveItem::before { + content: ''; + position: absolute; + width: 2px; + top: 0; + left: 0.1rem; + height: 100%; + background: var(--ifm-color-primary-lighter); +} + +.archiveItem:first-child::before { + height: 50%; + transform: translateY(100%); +} + +.archiveItem:last-child::before { + height: 50%; +} + +.archiveItem a { + display: flex; + align-items: center; + color: var(--ifm-text-color); + padding: 0.2rem 1rem; + font-size: 1.2rem; + font-weight: 500; + font-family: var(--ifm-font-family-base); + + text-decoration: none; + white-space: nowrap; + transition: padding 0.3s; +} + +.archiveItem a::before { + content: ''; + position: absolute; + left: 0.1rem; + top: 0.9rem; + width: 0.5rem; + height: 0.5rem; + margin-left: -4px; + border-radius: 50%; + border: 1px solid var(--ifm-color-primary); + background-color: rgb(255 255 255); + z-index: 1; + transition-duration: 0.3s; + transition-delay: 0s; + transition-property: background; +} + +.archiveItem a:hover::before { + background: var(--ifm-color-primary); +} + +.archiveTime { + opacity: 0.6; + font-size: 0.85rem; + font-weight: 400; + margin-right: 0.5rem; + width: 3rem; + position: relative; + color: var(--hty-secondary-text-color); + + font-family: 'Source Code Pro', Consolas, Monaco, SFMono-Regular, 'Ubuntu Mono', Menlo, monospace; +} + +.archiveItem a > span:hover { + color: var(--ifm-color-primary); +} diff --git a/src/theme/BlogLayout/index.tsx b/src/theme/BlogLayout/index.tsx new file mode 100644 index 0000000..14bee8b --- /dev/null +++ b/src/theme/BlogLayout/index.tsx @@ -0,0 +1,32 @@ +import React from 'react' +import clsx from 'clsx' +import Layout from '@theme/Layout' +import BlogSidebar from '@theme/BlogSidebar' + +import type { Props } from '@theme/BlogLayout' + +export default function BlogLayout(props: Props): JSX.Element { + const { sidebar, toc, children, ...layoutProps } = props + const hasSidebar = sidebar && sidebar.items.length > 0 + + return ( + +
    +
    + +
    + {children} +
    + {toc &&
    {toc}
    } +
    +
    +
    + ) +} diff --git a/src/theme/BlogListPage/index.tsx b/src/theme/BlogListPage/index.tsx new file mode 100644 index 0000000..6eadfde --- /dev/null +++ b/src/theme/BlogListPage/index.tsx @@ -0,0 +1,99 @@ +import clsx from 'clsx' +import React from 'react' +import { HtmlClassNameProvider, PageMetadata, ThemeClassNames } from '@docusaurus/theme-common' +import BackToTopButton from '@theme/BackToTopButton' +import type { Props } from '@theme/BlogListPage' +import BlogListPaginator from '@theme/BlogListPaginator' +import BlogPostItems from '@theme/BlogPostItems' +import SearchMetadata from '@theme/SearchMetadata' + +import { ViewType, useViewType } from '@site/src/hooks/useViewType' +import Translate from '@docusaurus/Translate' +import { Icon } from '@iconify/react' +import BlogPostGridItems from '../BlogPostGridItems' + +import styles from './styles.module.scss' +import MyLayout from '../MyLayout' + +function BlogListPageMetadata(props: Props): JSX.Element { + const { metadata } = props + const { blogDescription } = metadata + + return ( + <> + + + + ) +} + +function ViewTypeSwitch({ + viewType, + toggleViewType, +}: { + viewType: ViewType + toggleViewType: (viewType: ViewType) => void +}): JSX.Element { + return ( +
    + toggleViewType('list')} + color={viewType === 'list' ? 'var(--ifm-color-primary)' : '#ccc'} + /> + toggleViewType('grid')} + color={viewType === 'grid' ? 'var(--ifm-color-primary)' : '#ccc'} + /> +
    + ) +} + +function BlogListPageContent(props: Props) { + const { metadata, items } = props + + const { viewType, toggleViewType } = useViewType() + + const isListView = viewType === 'list' + const isGridView = viewType === 'grid' + + return ( + +

    + 博客 +

    +

    代码人生:编织技术与生活的博客之旅

    + +
    +
    + <> + {isListView && ( +
    + +
    + )} + {isGridView && } + + +
    +
    + +
    + ) +} + +export default function BlogListPage(props: Props): JSX.Element { + return ( + + + + + ) +} diff --git a/src/theme/BlogListPage/styles.module.scss b/src/theme/BlogListPage/styles.module.scss new file mode 100644 index 0000000..41b646a --- /dev/null +++ b/src/theme/BlogListPage/styles.module.scss @@ -0,0 +1,32 @@ +.blogTitle { + text-align: center; + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 1rem; +} + +.blogDescription { + text-align: center; + margin-bottom: 1rem; +} + +.blogList { + margin-bottom: 1rem; +} + +.blogSwithView { + text-align: center; + margin: 0 0 1em; + + svg { + cursor: pointer; + transition: 0.6s; + } +} + +@media (width <= 570px) { + h2 { + font-size: 1.3rem; + } +} diff --git a/src/theme/BlogPostGridItems/index.tsx b/src/theme/BlogPostGridItems/index.tsx new file mode 100644 index 0000000..60318e8 --- /dev/null +++ b/src/theme/BlogPostGridItems/index.tsx @@ -0,0 +1,86 @@ +import React from 'react' +import { Variants, motion, useMotionValue } from 'framer-motion' +import Link from '@docusaurus/Link' +import type { Props as BlogPostItemsProps } from '@theme/BlogPostItems' +import Tag from '@theme/Tag' + +import styles from './styles.module.scss' + +const container = { + hidden: { opacity: 1, scale: 0 }, + visible: { + opacity: 1, + scale: 1, + transition: { + delayChildren: 0.3, + staggerChildren: 0.2, + }, + }, +} + +const item = { + hidden: { y: 20, opacity: 0 }, + visible: { + y: 0, + opacity: 1, + }, +} + +export default function BlogPostGridItems({ items }: BlogPostItemsProps): JSX.Element { + return ( + + {items.map(({ content: BlogPostContent }, i) => { + const { metadata: blogMetaData, frontMatter } = BlogPostContent + const { title } = frontMatter + const { permalink, date, tags } = blogMetaData + const dateObj = new Date(date) + const dateString = `${dateObj.getFullYear()}-${('0' + (dateObj.getMonth() + 1)).slice( + -2, + )}-${('0' + dateObj.getDate()).slice(-2)}` + + return ( + { + e.currentTarget.style.setProperty('--mouse-x', `${e.clientX}px`) + e.currentTarget.style.setProperty('--mouse-y', `${e.clientY}px`) + }} + > + + {title} + +
    + {tags.length > 0 && ( + <> + + + + {tags.slice(0, 2).map(({ label, permalink: tagPermalink }, index) => { + return ( + <> + {index !== 0 && '/'} + + + ) + })} + + )} +
    +
    {dateString}
    +
    + ) + })} +
    + ) +} diff --git a/src/theme/BlogPostGridItems/styles.module.scss b/src/theme/BlogPostGridItems/styles.module.scss new file mode 100644 index 0000000..900dc8b --- /dev/null +++ b/src/theme/BlogPostGridItems/styles.module.scss @@ -0,0 +1,122 @@ +.blogGrid { + display: grid; + grid-template-columns: 1fr 1fr; + justify-content: center; + gap: 12px; + margin-bottom: 1rem; +} + +.postGridItem { + position: relative; + min-width: 24rem; + display: grid; + grid-template-columns: max-content 1fr; + grid-template-areas: + 'title title' + 'tags date'; + gap: 1em 2em; + align-items: center; + padding: 1em 1.2em; + background: var(--blog-item-background-color); + border-radius: 6px; + transition: all 0.3s; + + &:hover { + box-sizing: border-box; + box-shadow: var(--blog-item-shadow); + background: var(--blog-item-shade); + } + + .itemTitle { + color: inherit; + font-size: 1em; + text-decoration: none; + transition: 0.5s; + grid-area: title; + + &:hover { + color: var(--ifm-color-primary); + } + } + + .itemStick { + grid-area: stick; + justify-self: end; + color: #6ebdff; + position: absolute; + top: -6px; + left: 0; + display: inline-block; + font-size: 1.5rem; + + &::before { + content: '\e62b'; + } + } + + .itemTags { + grid-area: tags; + /* overflow-x: auto; */ + + display: inline-flex; + align-items: center; + white-space: nowrap; + font-size: 0.9em; + + a { + padding: 0 1px; + border: 0 !important; + color: inherit; + + &:hover { + color: var(--ifm-color-primary); + } + } + } + + .itemDate { + font-size: 0.8rem; + grid-area: date; + justify-self: end; + color: var(--ifm-color-emphasis-600); + } +} + +.spotlight { + --lighting-size: 300px; + --lighting-color: var(--ifm-color-primary); + --lighting-highlight-color: var(--ifm-color-primary-lightest); + + background-image: radial-gradient( + var(--lighting-highlight-color), + var(--lighting-color), + var(--lighting-color) + ); + background-size: var(--lighting-size) var(--lighting-size); + background-repeat: no-repeat; + + background-position-x: calc(var(--x) - var(--mouse-x) - calc(var(--lighting-size) / 2)); + background-position-y: calc(var(--y) - var(--mouse-y) - calc(var(--lighting-size) / 2)); + + background-color: var(--lighting-color); + + color: transparent; + background-clip: text; +} + +@media (width <= 768px) { + .postGridItem { + min-width: 100%; + } +} + +@media (width <= 576px) { + .blogGrid { + grid-template-columns: minmax(0, max-content); + } + + .postGridItem { + max-width: 100%; + min-width: 28rem; + } +} diff --git a/src/theme/BlogPostItem/Container/index.tsx b/src/theme/BlogPostItem/Container/index.tsx new file mode 100644 index 0000000..dc2c1b1 --- /dev/null +++ b/src/theme/BlogPostItem/Container/index.tsx @@ -0,0 +1,32 @@ +import React from 'react' +import { useBaseUrlUtils } from '@docusaurus/useBaseUrl' +import { useBlogPost } from '@docusaurus/theme-common/internal' +import type { Props } from '@theme/BlogPostItem/Container' +import clsx from 'clsx' + +import styles from './styles.module.css' + +export default function BlogPostItemContainer({ children, className }: Props): JSX.Element { + const { frontMatter, assets } = useBlogPost() + const { withBaseUrl } = useBaseUrlUtils() + const image = assets.image ?? frontMatter.image + return ( +
    + {image && ( + <> + +
    +
    +
    +
    + + )} + {children} +
    + ) +} diff --git a/src/theme/BlogPostItem/Container/styles.module.css b/src/theme/BlogPostItem/Container/styles.module.css new file mode 100644 index 0000000..7f1bf57 --- /dev/null +++ b/src/theme/BlogPostItem/Container/styles.module.css @@ -0,0 +1,34 @@ +:root { + --border-color: hsla(240, 6%, 90%, 0.7); +} + +html[data-theme='dark'] { + --border-color: #262626; +} + +.article { + position: relative; + padding: 1em 1.25em 0.75em; + /* border: 1px solid var(--border-color); */ +} + +.cover { + position: absolute; + z-index: 1; + left: 0; + right: 0; + top: 0; + height: 224px; +} + +.coverMask { + border-radius: var(--ifm-pagination-nav-border-radius); + + background-size: cover; + background-position: center; + background-repeat: no-repeat; + height: 100%; + width: 100%; + -webkit-mask-image: linear-gradient(180deg, #fff -17.19%, #00000000 92.43%); + mask-image: linear-gradient(180deg, #fff -17.19%, #00000000 92.43%); +} diff --git a/src/theme/BlogPostItem/Content/index.tsx b/src/theme/BlogPostItem/Content/index.tsx new file mode 100644 index 0000000..1c92a36 --- /dev/null +++ b/src/theme/BlogPostItem/Content/index.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import clsx from 'clsx' +import { blogPostContainerID } from '@docusaurus/utils-common' +import { useBlogPost } from '@docusaurus/theme-common/internal' +import MDXContent from '@theme/MDXContent' +import type { Props } from '@theme/BlogPostItem/Content' + +export default function BlogPostItemContent({ children, className }: Props): JSX.Element { + const { isBlogPostPage } = useBlogPost() + return ( +
    + {children} +
    + ) +} diff --git a/src/theme/BlogPostItem/Footer/ReadMoreLink/index.tsx b/src/theme/BlogPostItem/Footer/ReadMoreLink/index.tsx new file mode 100644 index 0000000..8619599 --- /dev/null +++ b/src/theme/BlogPostItem/Footer/ReadMoreLink/index.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import Translate, { translate } from '@docusaurus/Translate' +import Link from '@docusaurus/Link' +import { Icon } from '@iconify/react' +import type { Props } from '@theme/BlogPostItem/Footer/ReadMoreLink' + +function ReadMoreLabel() { + return ( + + + Read More + + + ) +} + +export default function BlogPostItemFooterReadMoreLink( + props: Props, +): JSX.Element { + const { blogPostTitle, ...linkProps } = props + return ( + + + + + ) +} diff --git a/src/theme/BlogPostItem/Footer/index.tsx b/src/theme/BlogPostItem/Footer/index.tsx new file mode 100644 index 0000000..be04007 --- /dev/null +++ b/src/theme/BlogPostItem/Footer/index.tsx @@ -0,0 +1,103 @@ +import React from 'react' +import clsx from 'clsx' +import { useBlogPost } from '@docusaurus/theme-common/internal' +import EditThisPage from '@theme/EditThisPage' +import TagsListInline from '@theme/TagsListInline' +import Tag from '@theme/Tag' +import ReadMoreLink from '@theme/BlogPostItem/Footer/ReadMoreLink' +import { Icon } from '@iconify/react' +import { ReadingTime } from '../Header/Info/index' + +import styles from './styles.module.scss' + +export default function BlogPostItemFooter(): JSX.Element | null { + const { metadata, isBlogPostPage } = useBlogPost() + const { tags, title, editUrl, hasTruncateMarker, date, formattedDate, readingTime, authors } = + metadata + + // A post is truncated if it's in the "list view" and it has a truncate marker + const truncatedPost = !isBlogPostPage && hasTruncateMarker + + const tagsExists = tags.length > 0 + const authorsExists = authors.length > 0 + + const renderFooter = isBlogPostPage + + if (!renderFooter) { + return ( +
    +
    + {/* {authorsExists && ( + <> + + {authors.map(a => ( + + + {a.name} + + + ))} + + )} */} + {date && ( + <> + + + + )} + {tagsExists && ( + <> + + + {tags.map(({ label, permalink: tagPermalink }) => ( + + ))} + + + )} + {readingTime && ( + <> + + + + + + )} + {truncatedPost && ( +
    + +
    + )} +
    +
    + ) + } + + return ( +
    + {/* {isBlogPostPage && editUrl && ( +
    + +
    + )} */} + + {truncatedPost && ( +
    + +
    + )} +
    + ) +} diff --git a/src/theme/BlogPostItem/Footer/styles.module.scss b/src/theme/BlogPostItem/Footer/styles.module.scss new file mode 100644 index 0000000..b5dc01f --- /dev/null +++ b/src/theme/BlogPostItem/Footer/styles.module.scss @@ -0,0 +1,58 @@ +.blogPostFooterDetailsFull { + flex-direction: column; +} + +.blogPostInfo { + margin-top: 0.5em; + display: flex; + flex-wrap: wrap; + align-items: center; + margin-bottom: var(--ifm-spacing-m); + gap: 4px; + color: var(--ifm-secondary-text-color); + + font-size: 0.8rem; + font-weight: 400; +} + +.blogPostInfoTags { + display: flex; + gap: 4px; + + a { + color: var(--ifm-secondary-text-color); + } + + a:hover { + color: var(--ifm-link-color); + } +} + +.blogPostInfoTags > a { + padding: 1px 4px; +} + +.blogPostAuthor { + display: inline-block; + margin: 0 2px; + color: inherit; +} + +.blogPostAuthor:hover { + text-decoration: none; +} + +.blogPostDetailsFull { + flex-direction: column; +} + +.divider { + background-color: #eaecef; + border: 0; + height: var(--ifm-hr-height); + margin: 0.25rem 0; +} + +html[data-theme='dark'] .divider { + background-color: #2f3336; +} diff --git a/src/theme/BlogPostItem/Header/Author/index.tsx b/src/theme/BlogPostItem/Header/Author/index.tsx new file mode 100644 index 0000000..d25c60a --- /dev/null +++ b/src/theme/BlogPostItem/Header/Author/index.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import clsx from 'clsx' +import Link, { type Props as LinkProps } from '@docusaurus/Link' + +import type { Props } from '@theme/BlogPostItem/Header/Author' + +function MaybeLink(props: LinkProps): JSX.Element { + if (props.href) { + return + } + return <>{props.children} +} + +export default function BlogPostItemHeaderAuthor({ + author, + className, +}: Props): JSX.Element { + const { name, title, url, imageURL, email } = author + const link = url || (email && `mailto:${email}`) || undefined + return ( +
    + {imageURL && ( + + {name} + + )} + + {name && ( + + )} +
    + ) +} diff --git a/src/theme/BlogPostItem/Header/Authors/index.tsx b/src/theme/BlogPostItem/Header/Authors/index.tsx new file mode 100644 index 0000000..bcda3f3 --- /dev/null +++ b/src/theme/BlogPostItem/Header/Authors/index.tsx @@ -0,0 +1,45 @@ +import React from 'react' +import clsx from 'clsx' +import { useBlogPost } from '@docusaurus/theme-common/internal' +import BlogPostItemHeaderAuthor from '@theme/BlogPostItem/Header/Author' +import type { Props } from '@theme/BlogPostItem/Header/Authors' +import styles from './styles.module.css' + +export default function BlogPostItemHeaderAuthors({ className }: Props): JSX.Element | null { + const { + metadata: { authors }, + assets, + } = useBlogPost() + const authorsCount = authors.length + if (authorsCount === 0) { + return null + } + const imageOnly = authors.every(({ name }) => !name) + return ( +
    + {authors.map((author, idx) => ( +
    + +
    + ))} +
    + ) +} diff --git a/src/theme/BlogPostItem/Header/Authors/styles.module.css b/src/theme/BlogPostItem/Header/Authors/styles.module.css new file mode 100644 index 0000000..c5aac4d --- /dev/null +++ b/src/theme/BlogPostItem/Header/Authors/styles.module.css @@ -0,0 +1,14 @@ +.authorCol { + max-width: inherit !important; + flex-grow: 1 !important; +} + +.imageOnlyAuthorRow { + display: flex; + flex-flow: row wrap; +} + +.imageOnlyAuthorCol { + margin-left: 0.3rem; + margin-right: 0.3rem; +} diff --git a/src/theme/BlogPostItem/Header/Info/index.tsx b/src/theme/BlogPostItem/Header/Info/index.tsx new file mode 100644 index 0000000..83af9ff --- /dev/null +++ b/src/theme/BlogPostItem/Header/Info/index.tsx @@ -0,0 +1,81 @@ +import React from 'react' +import clsx from 'clsx' +import { translate } from '@docusaurus/Translate' +import { usePluralForm } from '@docusaurus/theme-common' +import { useBlogPost } from '@docusaurus/theme-common/internal' +import type { Props } from '@theme/BlogPostItem/Header/Info' +import TagsListInline from '@theme/TagsListInline' + +import styles from './styles.module.css' +import Tag from '@site/src/theme/Tag' +import { Icon } from '@iconify/react' + +// Very simple pluralization: probably good enough for now +function useReadingTimePlural() { + const { selectMessage } = usePluralForm() + return (readingTimeFloat: number) => { + const readingTime = Math.ceil(readingTimeFloat) + return selectMessage( + readingTime, + translate( + { + id: 'theme.blog.post.readingTime.plurals', + description: + 'Pluralized label for "{readingTime} min read". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)', + message: 'One min read|{readingTime} min read', + }, + { readingTime }, + ), + ) + } +} + +export function ReadingTime({ readingTime }: { readingTime: number }) { + const readingTimePlural = useReadingTimePlural() + return {readingTimePlural(readingTime)} +} + +function Date({ date, formattedDate }: { date: string; formattedDate: string }) { + return ( + + ) +} + +export default function BlogPostItemHeaderInfo({ className }: Props): JSX.Element { + const { metadata } = useBlogPost() + const { date, tags, formattedDate, readingTime } = metadata + + const tagsExists = tags.length > 0 + + return ( +
    +
    + + +
    + {tagsExists && ( +
    + +
    + {tags.slice(0, 3).map(({ label, permalink: tagPermalink }, index) => { + return ( +
    + {index !== 0 && '/'} + +
    + ) + })} +
    +
    + )} + {typeof readingTime !== 'undefined' && ( +
    + + +
    + )} +
    + ) +} diff --git a/src/theme/BlogPostItem/Header/Info/styles.module.css b/src/theme/BlogPostItem/Header/Info/styles.module.css new file mode 100644 index 0000000..7ce6d8a --- /dev/null +++ b/src/theme/BlogPostItem/Header/Info/styles.module.css @@ -0,0 +1,39 @@ +.container { + display: inline-flex; + gap: 0.5rem; + font-size: 0.95rem; +} + +.date { + display: inline-flex; + align-items: center; + gap: 0.25rem; +} + +.read { + display: inline-flex; + align-items: center; + gap: 0.25rem; +} + +.tagInfo { + display: inline-flex; + align-items: center; + gap: 0.25rem; +} + +.tagList { + display: inline-flex; + align-items: center; +} + +.tagList a { + padding: 1px 2px; + color: var(--ifm-text-color); + + border: 0 !important; +} + +.tagList a:hover { + color: var(--ifm-color-primary); +} diff --git a/src/theme/BlogPostItem/Header/Title/index.tsx b/src/theme/BlogPostItem/Header/Title/index.tsx new file mode 100644 index 0000000..871a886 --- /dev/null +++ b/src/theme/BlogPostItem/Header/Title/index.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import clsx from 'clsx' +import Link from '@docusaurus/Link' +import { useBlogPost } from '@docusaurus/theme-common/internal' +import type { Props } from '@theme/BlogPostItem/Header/Title' + +import styles from './styles.module.css' + +export default function BlogPostItemHeaderTitle({ + className, +}: Props): JSX.Element { + const { metadata, isBlogPostPage } = useBlogPost() + const { permalink, title } = metadata + const TitleHeading = isBlogPostPage ? 'h1' : 'h2' + return ( + + {isBlogPostPage ? ( + title + ) : ( + + {title} + + )} + + ) +} diff --git a/src/theme/BlogPostItem/Header/Title/styles.module.css b/src/theme/BlogPostItem/Header/Title/styles.module.css new file mode 100644 index 0000000..9db9101 --- /dev/null +++ b/src/theme/BlogPostItem/Header/Title/styles.module.css @@ -0,0 +1,29 @@ +.titleLink { + position: relative; + font-weight: 500; + color: var(--ifm-heading-color); + transition: 0.5s; +} + +.titleLink:hover { + color: var(--ifm-link-hover-color); + text-decoration: none; +} + +.titleLink:hover::after { + visibility: visible; + transform: scaleX(1); +} + +.titleLink::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 2px; + background: var(--ifm-color-primary); + visibility: hidden; + transition: all 0.3s linear; + transform: scaleX(0); +} diff --git a/src/theme/BlogPostItem/Header/index.tsx b/src/theme/BlogPostItem/Header/index.tsx new file mode 100644 index 0000000..aa5db68 --- /dev/null +++ b/src/theme/BlogPostItem/Header/index.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import BlogPostItemHeaderTitle from '@theme/BlogPostItem/Header/Title' +import BlogPostItemHeaderInfo from '@theme/BlogPostItem/Header/Info' +import BlogPostItemHeaderAuthors from '@theme/BlogPostItem/Header/Authors' +import { useBlogPost } from '@docusaurus/theme-common/internal' + +export default function BlogPostItemHeader(): JSX.Element { + const { isBlogPostPage } = useBlogPost() + return ( +
    + + {isBlogPostPage && ( + <> + + {/* */} + + )} +
    + ) +} diff --git a/src/theme/BlogPostItem/index.tsx b/src/theme/BlogPostItem/index.tsx new file mode 100644 index 0000000..879c57a --- /dev/null +++ b/src/theme/BlogPostItem/index.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import clsx from 'clsx' +import { useBlogPost } from '@docusaurus/theme-common/internal' +import BlogPostItemContainer from '@theme/BlogPostItem/Container' +import BlogPostItemHeader from '@theme/BlogPostItem/Header' +import BlogPostItemContent from '@theme/BlogPostItem/Content' +import BlogPostItemFooter from '@theme/BlogPostItem/Footer' +import type { Props } from '@theme/BlogPostItem' + +// apply a bottom margin in list view +function useContainerClassName() { + const { isBlogPostPage } = useBlogPost() + return !isBlogPostPage ? 'blog-card margin-bottom--lg' : '' +} + +export default function BlogPostItem({ children, className }: Props): JSX.Element { + const containerClassName = useContainerClassName() + return ( + + + {children} + + + ) +} diff --git a/src/theme/BlogPostItems/index.tsx b/src/theme/BlogPostItems/index.tsx new file mode 100644 index 0000000..22af095 --- /dev/null +++ b/src/theme/BlogPostItems/index.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import { motion, Variants } from 'framer-motion' +import { BlogPostProvider } from '@docusaurus/theme-common/internal' +import BlogPostItem from '@theme/BlogPostItem' +import type { Props } from '@theme/BlogPostItems' + +const variants: Variants = { + from: { opacity: 0.01, y: 100 }, + to: i => ({ + opacity: 1, + y: 0, + transition: { + type: 'spring', + damping: 25, + stiffness: 100, + bounce: 0.2, + duration: 0.3, + delay: i * 0.2, + }, + }), +} + +export default function BlogPostItems({ + items, + component: BlogPostItemComponent = BlogPostItem, +}: Props): JSX.Element { + return ( + <> + {items.map(({ content: BlogPostContent }, i) => ( + + + + + + + + ))} + + ) +} diff --git a/src/theme/BlogPostPage/Metadata/index.tsx b/src/theme/BlogPostPage/Metadata/index.tsx new file mode 100644 index 0000000..4f340d8 --- /dev/null +++ b/src/theme/BlogPostPage/Metadata/index.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import { PageMetadata } from '@docusaurus/theme-common' +import { useBlogPost } from '@docusaurus/theme-common/internal' + +export default function BlogPostPageMetadata(): JSX.Element { + const { assets, metadata } = useBlogPost() + const { title, description, date, tags, authors, frontMatter } = metadata + + const { keywords } = frontMatter + const image = assets.image ?? frontMatter.image + return ( + + + + {/* TODO double check those article meta array syntaxes, see https://ogp.me/#array */} + {authors.some(author => author.url) && ( + author.url) + .filter(Boolean) + .join(',')} + /> + )} + {tags.length > 0 && ( + tag.label).join(',')} + /> + )} + + ) +} diff --git a/src/theme/BlogPostPage/index.tsx b/src/theme/BlogPostPage/index.tsx new file mode 100644 index 0000000..7a45354 --- /dev/null +++ b/src/theme/BlogPostPage/index.tsx @@ -0,0 +1,71 @@ +import React, { type ReactNode } from 'react' +import clsx from 'clsx' +import { HtmlClassNameProvider, ThemeClassNames } from '@docusaurus/theme-common' +import { BlogPostProvider, useBlogPost } from '@docusaurus/theme-common/internal' +import BlogLayout from '@theme/BlogLayout' +import BlogPostItem from '@theme/BlogPostItem' +import BlogPostPaginator from '@theme/BlogPostPaginator' +import BlogPostPageMetadata from '@theme/BlogPostPage/Metadata' +import BackToTopButton from '@theme/BackToTopButton' +import TOC from '@theme/TOC' +import type { Props } from '@theme/BlogPostPage' +import type { BlogSidebar } from '@docusaurus/plugin-content-blog' +import Comment from '@site/src/components/Comment' + +function BlogPostPageContent({ + sidebar, + children, +}: { + sidebar: BlogSidebar + children: ReactNode +}): JSX.Element { + const { metadata, toc } = useBlogPost() + const { nextItem, prevItem, frontMatter } = metadata + const { + hide_table_of_contents: hideTableOfContents, + toc_min_heading_level: tocMinHeadingLevel, + toc_max_heading_level: tocMaxHeadingLevel, + hide_comment: hideComment, + } = frontMatter + + return ( + 0 ? ( + + ) : undefined + } + > + {children} + + {(nextItem || prevItem) && ( +
    + +
    + )} + {!hideComment && } + +
    + ) +} + +export default function BlogPostPage(props: Props): JSX.Element { + const BlogPostContent = props.content + return ( + + + + + + + + + ) +} diff --git a/src/theme/BlogSidebar/Desktop/index.tsx b/src/theme/BlogSidebar/Desktop/index.tsx new file mode 100644 index 0000000..3df7f6d --- /dev/null +++ b/src/theme/BlogSidebar/Desktop/index.tsx @@ -0,0 +1,60 @@ +import React, { useState } from 'react' +import clsx from 'clsx' +import Link from '@docusaurus/Link' +import { Icon } from '@iconify/react' +import { translate } from '@docusaurus/Translate' +import { useBlogPost } from '@docusaurus/theme-common/internal' +import type { Props } from '@theme/BlogSidebar/Desktop' + +import styles from './styles.module.scss' + +export default function BlogSidebarDesktop({ sidebar }: Props): JSX.Element { + const { isBlogPostPage } = useBlogPost() + const [isHovered, setIsHovered] = useState(false) + + const handleBack = () => { + window.history.back() + } + + return ( + + ) +} diff --git a/src/theme/BlogSidebar/Desktop/styles.module.scss b/src/theme/BlogSidebar/Desktop/styles.module.scss new file mode 100644 index 0000000..938600a --- /dev/null +++ b/src/theme/BlogSidebar/Desktop/styles.module.scss @@ -0,0 +1,86 @@ +.sidebar { + max-height: calc(100vh - (var(--ifm-navbar-height) + 2rem)); + overflow-y: auto; + position: sticky; + top: calc(var(--ifm-navbar-height) + 2rem); + + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 0.5s; +} + +.sidebarItemTitle { + font-size: var(--ifm-h4-font-size); + font-weight: var(--ifm-font-weight-bold); + font-family: var(--ifm-heading-font-family); + + color: var(--ifm-text-color); + + &:hover { + color: var(--ifm-link-color); + text-decoration: none; + } +} + +.sidebarItemList { + font-size: 0.8rem; +} + +.sidebarItem { + margin-top: 0.7rem; +} + +.sidebarItemLink { + color: var(--ifm-font-color-secondary); + + display: block; + + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.sidebarItemLink:hover { + text-decoration: none; +} + +.sidebarItemLinkActive { + color: var(--ifm-color-primary) !important; +} + +@media (width <= 996px) { + .sidebar { + display: none; + } +} + +:root { + --back-btn-bg-color: #fafaf9; + --back-btn-bg-color-hover: #f1f5f9; +} + +html[data-theme='dark'] { + --back-btn-bg-color: #18181b; + --back-btn-bg-color-hover: #334155; +} + +.backButton { + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 2rem; + text-align: right; + float: right; + + transition: all 0.3s ease-in-out; + cursor: pointer; + + background-color: var(--back-btn-bg-color); + + &:hover { + color: var(--ifm-link-color); + background-color: var(--back-btn-bg-color-hover); + } +} diff --git a/src/theme/BlogSidebar/Mobile/index.tsx b/src/theme/BlogSidebar/Mobile/index.tsx new file mode 100644 index 0000000..d2590a9 --- /dev/null +++ b/src/theme/BlogSidebar/Mobile/index.tsx @@ -0,0 +1,32 @@ +import React from 'react' +import Link from '@docusaurus/Link' +import { NavbarSecondaryMenuFiller } from '@docusaurus/theme-common' +import type { Props } from '@theme/BlogSidebar/Mobile' + +function BlogSidebarMobileSecondaryMenu({ sidebar }: Props): JSX.Element { + return ( +
      + {sidebar.items.map(item => ( +
    • + + {item.title} + +
    • + ))} +
    + ) +} + +export default function BlogSidebarMobile(props: Props): JSX.Element { + return ( + + ) +} diff --git a/src/theme/BlogSidebar/index.tsx b/src/theme/BlogSidebar/index.tsx new file mode 100644 index 0000000..766c394 --- /dev/null +++ b/src/theme/BlogSidebar/index.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import { useWindowSize } from '@docusaurus/theme-common' +import BlogSidebarDesktop from '@theme/BlogSidebar/Desktop' +import BlogSidebarMobile from '@theme/BlogSidebar/Mobile' +import type { Props } from '@theme/BlogSidebar' + +export default function BlogSidebar({ sidebar }: Props): JSX.Element | null { + const windowSize = useWindowSize() + if (!sidebar?.items.length) { + return null + } + // Mobile sidebar doesn't need to be server-rendered + if (windowSize === 'mobile') { + return + } + return +} diff --git a/src/theme/BlogTagsListPage/index.tsx b/src/theme/BlogTagsListPage/index.tsx new file mode 100644 index 0000000..ef47c86 --- /dev/null +++ b/src/theme/BlogTagsListPage/index.tsx @@ -0,0 +1,59 @@ +import React, { useState } from 'react' +import clsx from 'clsx' +import { + PageMetadata, + HtmlClassNameProvider, + ThemeClassNames, + translateTagsPageTitle, +} from '@docusaurus/theme-common' +import TagsListByLetter from '@theme/TagsListByLetter' +import { TagsListByFlat } from '../TagsListByLetter' +import type { Props } from '@theme/BlogTagsListPage' +import SearchMetadata from '@theme/SearchMetadata' +import { Icon } from '@iconify/react' + +import MyLayout from '../MyLayout' + +export default function BlogTagsListPage({ tags, sidebar }: Props): JSX.Element { + const title = translateTagsPageTitle() + + const [type, setType] = useState<'list' | 'grid'>('list') + + return ( + + + + +
    +

    {title}

    + + setType('list')} + color={type === 'list' ? 'var(--ifm-color-primary)' : '#ccc'} + /> + setType('grid')} + color={type === 'grid' ? 'var(--ifm-color-primary)' : '#ccc'} + /> + +
    + {type === 'list' && } + {type === 'grid' && } +
    +
    + ) +} diff --git a/src/theme/BlogTagsPostsPage/index.tsx b/src/theme/BlogTagsPostsPage/index.tsx new file mode 100644 index 0000000..36c892b --- /dev/null +++ b/src/theme/BlogTagsPostsPage/index.tsx @@ -0,0 +1,101 @@ +import React from 'react' +import clsx from 'clsx' +import Translate, { translate } from '@docusaurus/Translate' +import { + PageMetadata, + HtmlClassNameProvider, + ThemeClassNames, + usePluralForm, +} from '@docusaurus/theme-common' +import Link from '@docusaurus/Link' +import BackToTopButton from '@theme/BackToTopButton' +import BlogListPaginator from '@theme/BlogListPaginator' +import SearchMetadata from '@theme/SearchMetadata' +import type { Props } from '@theme/BlogTagsPostsPage' +import BlogPostItems from '@theme/BlogPostItems' +import Unlisted from '@theme/Unlisted' +import Heading from '@theme/Heading' + +import styles from './styles.module.scss' +import MyLayout from '../MyLayout' + +// Very simple pluralization: probably good enough for now +function useBlogPostsPlural() { + const { selectMessage } = usePluralForm() + return (count: number) => + selectMessage( + count, + translate( + { + id: 'theme.blog.post.plurals', + description: + 'Pluralized label for "{count} posts". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)', + message: 'One post|{count} posts', + }, + { count }, + ), + ) +} + +function useBlogTagsPostsPageTitle(tag: Props['tag']): string { + const blogPostsPlural = useBlogPostsPlural() + return translate( + { + id: 'theme.blog.tagTitle', + description: 'The title of the page for a blog tag', + message: '{nPosts} tagged with "{tagName}"', + }, + { nPosts: blogPostsPlural(tag.count), tagName: tag.label }, + ) +} + +function BlogTagsPostsPageMetadata({ tag }: Props): JSX.Element { + const title = useBlogTagsPostsPageTitle(tag) + return ( + <> + + + + ) +} + +function BlogTagsPostsPageContent({ + tag, + items, + sidebar, + listMetadata, +}: Props): JSX.Element { + const title = useBlogTagsPostsPageTitle(tag) + return ( + + {tag.unlisted && } +
    + {title} + + + View All Tags + + +
    + + + +
    + ) +} +export default function BlogTagsPostsPage(props: Props): JSX.Element { + return ( + + + + + ) +} diff --git a/src/theme/BlogTagsPostsPage/styles.module.scss b/src/theme/BlogTagsPostsPage/styles.module.scss new file mode 100644 index 0000000..91c1c36 --- /dev/null +++ b/src/theme/BlogTagsPostsPage/styles.module.scss @@ -0,0 +1,3 @@ +.pageHeader { + margin-bottom: 1rem; +} diff --git a/src/theme/CodeBlock/Container/index.tsx b/src/theme/CodeBlock/Container/index.tsx new file mode 100644 index 0000000..99f8dcf --- /dev/null +++ b/src/theme/CodeBlock/Container/index.tsx @@ -0,0 +1,25 @@ +import React, {type ComponentProps} from 'react'; +import clsx from 'clsx'; +import {ThemeClassNames, usePrismTheme} from '@docusaurus/theme-common'; +import {getPrismCssVariables} from '@docusaurus/theme-common/internal'; +import styles from './styles.module.css'; + +export default function CodeBlockContainer({ + as: As, + ...props +}: {as: T} & ComponentProps): JSX.Element { + const prismTheme = usePrismTheme(); + const prismCssVariables = getPrismCssVariables(prismTheme); + return ( + + ); +} diff --git a/src/theme/CodeBlock/Container/styles.module.css b/src/theme/CodeBlock/Container/styles.module.css new file mode 100644 index 0000000..3c79968 --- /dev/null +++ b/src/theme/CodeBlock/Container/styles.module.css @@ -0,0 +1,7 @@ +.codeBlockContainer { + background: var(--prism-background-color); + color: var(--prism-color); + margin-bottom: var(--ifm-leading); + box-shadow: var(--ifm-global-shadow-lw); + border-radius: var(--ifm-code-border-radius); +} diff --git a/src/theme/CodeBlock/Content/Element.tsx b/src/theme/CodeBlock/Content/Element.tsx new file mode 100644 index 0000000..eef5741 --- /dev/null +++ b/src/theme/CodeBlock/Content/Element.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import clsx from 'clsx'; +import Container from '@theme/CodeBlock/Container'; +import type {Props} from '@theme/CodeBlock/Content/Element'; + +import styles from './styles.module.css'; + +//
     tags in markdown map to CodeBlocks. They may contain JSX children. When
    +// the children is not a simple string, we just return a styled block without
    +// actually highlighting.
    +export default function CodeBlockJSX({
    +  children,
    +  className,
    +}: Props): JSX.Element {
    +  return (
    +    
    +      {children}
    +    
    +  );
    +}
    diff --git a/src/theme/CodeBlock/Content/String.tsx b/src/theme/CodeBlock/Content/String.tsx
    new file mode 100644
    index 0000000..61e0c14
    --- /dev/null
    +++ b/src/theme/CodeBlock/Content/String.tsx
    @@ -0,0 +1,128 @@
    +import React from 'react'
    +import clsx from 'clsx'
    +import { useThemeConfig, usePrismTheme } from '@docusaurus/theme-common'
    +import {
    +  parseCodeBlockTitle,
    +  parseLanguage,
    +  parseLines,
    +  containsLineNumbers,
    +  useCodeWordWrap,
    +} from '@docusaurus/theme-common/internal'
    +import { Highlight, type Language } from 'prism-react-renderer'
    +import Line from '@theme/CodeBlock/Line'
    +import CopyButton from '@theme/CodeBlock/CopyButton'
    +import WordWrapButton from '@theme/CodeBlock/WordWrapButton'
    +import Container from '@theme/CodeBlock/Container'
    +import type { Props } from '@theme/CodeBlock/Content/String'
    +import { Icon } from '@iconify/react'
    +
    +import styles from './styles.module.css'
    +
    +// Prism languages are always lowercase
    +// We want to fail-safe and allow both "php" and "PHP"
    +// See https://github.com/facebook/docusaurus/issues/9012
    +function normalizeLanguage(language: string | undefined): string | undefined {
    +  return language?.toLowerCase()
    +}
    +
    +function parseIcon(metastring?: string): JSX.Element | null {
    +  const iconRegex = /icon=(?["'])(?.*?)\1/
    +
    +  const icon = metastring?.match(iconRegex)?.groups!.icon ?? ''
    +
    +  if (!icon) return null
    +
    +  return 
    +}
    +
    +export default function CodeBlockString({
    +  children,
    +  className: blockClassName = '',
    +  metastring,
    +  title: titleProp,
    +  showLineNumbers: showLineNumbersProp,
    +  language: languageProp,
    +}: Props): JSX.Element {
    +  const {
    +    prism: { defaultLanguage, magicComments },
    +  } = useThemeConfig()
    +  const language = normalizeLanguage(
    +    languageProp ?? parseLanguage(blockClassName) ?? defaultLanguage,
    +  )
    +
    +  const prismTheme = usePrismTheme()
    +  const wordWrap = useCodeWordWrap()
    +
    +  // We still parse the metastring in case we want to support more syntax in the
    +  // future. Note that MDX doesn't strip quotes when parsing metastring:
    +  // "title=\"xyz\"" => title: "\"xyz\""
    +  const title = parseCodeBlockTitle(metastring) || titleProp
    +
    +  const icon = parseIcon(metastring)
    +
    +  const { lineClassNames, code } = parseLines(children, {
    +    metastring,
    +    language,
    +    magicComments,
    +  })
    +  const showLineNumbers = showLineNumbersProp ?? containsLineNumbers(metastring)
    +
    +  return (
    +    
    +      {title && (
    +        
    + {icon} + {title} + {language} +
    + )} +
    + + {({ className, style, tokens, getLineProps, getTokenProps }) => ( +
    +              
    +                {tokens.map((line, i) => (
    +                  
    +                ))}
    +              
    +            
    + )} +
    +
    + {(wordWrap.isEnabled || wordWrap.isCodeScrollable) && ( + wordWrap.toggle()} + isEnabled={wordWrap.isEnabled} + /> + )} + +
    +
    +
    + ) +} diff --git a/src/theme/CodeBlock/Content/styles.module.css b/src/theme/CodeBlock/Content/styles.module.css new file mode 100644 index 0000000..15224fd --- /dev/null +++ b/src/theme/CodeBlock/Content/styles.module.css @@ -0,0 +1,85 @@ +.codeBlockContent { + position: relative; + /* rtl:ignore */ + direction: ltr; + border-radius: inherit; +} + +.codeBlockTitle { + display: inline-flex; + align-items: center; + gap: 0.5rem; + width: 100%; + + border-bottom: 1px solid var(--ifm-color-emphasis-300); + font-size: var(--ifm-code-font-size); + font-weight: 500; + padding: 0.75rem var(--ifm-pre-padding); + border-top-left-radius: inherit; + border-top-right-radius: inherit; +} + +.codeBlock { + --ifm-pre-background: var(--prism-background-color); + margin: 0; + padding: 0; +} + +.codeBlockTitle + .codeBlockContent .codeBlock { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.codeBlockStandalone { + padding: 0; +} + +.codeBlockLines { + font: inherit; + /* rtl:ignore */ + float: left; + min-width: 100%; + padding: var(--ifm-pre-padding); +} + +.codeBlockLinesWithNumbering { + display: table; + padding: var(--ifm-pre-padding) 0; +} + +@media print { + .codeBlockLines { + white-space: pre-wrap; + } +} + +.buttonGroup { + display: flex; + column-gap: 0.2rem; + position: absolute; + /* rtl:ignore */ + right: calc(var(--ifm-pre-padding) / 2); + top: calc(var(--ifm-pre-padding) / 2); +} + +.buttonGroup button { + display: flex; + align-items: center; + background: var(--prism-background-color); + color: var(--prism-color); + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: var(--ifm-global-radius); + padding: 0.4rem; + line-height: 0; + transition: opacity var(--ifm-transition-fast) ease-in-out; + opacity: 0; +} + +.buttonGroup button:focus-visible, +.buttonGroup button:hover { + opacity: 1 !important; +} + +:global(.theme-code-block:hover) .buttonGroup button { + opacity: 0.4; +} diff --git a/src/theme/CodeBlock/CopyButton/index.tsx b/src/theme/CodeBlock/CopyButton/index.tsx new file mode 100644 index 0000000..8a94146 --- /dev/null +++ b/src/theme/CodeBlock/CopyButton/index.tsx @@ -0,0 +1,58 @@ +import React, {useCallback, useState, useRef, useEffect} from 'react'; +import clsx from 'clsx'; +import copy from 'copy-text-to-clipboard'; +import {translate} from '@docusaurus/Translate'; +import type {Props} from '@theme/CodeBlock/CopyButton'; +import IconCopy from '@theme/Icon/Copy'; +import IconSuccess from '@theme/Icon/Success'; + +import styles from './styles.module.css'; + +export default function CopyButton({code, className}: Props): JSX.Element { + const [isCopied, setIsCopied] = useState(false); + const copyTimeout = useRef(undefined); + const handleCopyCode = useCallback(() => { + copy(code); + setIsCopied(true); + copyTimeout.current = window.setTimeout(() => { + setIsCopied(false); + }, 1000); + }, [code]); + + useEffect(() => () => window.clearTimeout(copyTimeout.current), []); + + return ( + + ); +} diff --git a/src/theme/CodeBlock/CopyButton/styles.module.css b/src/theme/CodeBlock/CopyButton/styles.module.css new file mode 100644 index 0000000..d5268e9 --- /dev/null +++ b/src/theme/CodeBlock/CopyButton/styles.module.css @@ -0,0 +1,40 @@ +:global(.theme-code-block:hover) .copyButtonCopied { + opacity: 1 !important; +} + +.copyButtonIcons { + position: relative; + width: 1.125rem; + height: 1.125rem; +} + +.copyButtonIcon, +.copyButtonSuccessIcon { + position: absolute; + top: 0; + left: 0; + fill: currentColor; + opacity: inherit; + width: inherit; + height: inherit; + transition: all var(--ifm-transition-fast) ease; +} + +.copyButtonSuccessIcon { + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0.33); + opacity: 0; + color: #00d600; +} + +.copyButtonCopied .copyButtonIcon { + transform: scale(0.33); + opacity: 0; +} + +.copyButtonCopied .copyButtonSuccessIcon { + transform: translate(-50%, -50%) scale(1); + opacity: 1; + transition-delay: 0.075s; +} diff --git a/src/theme/CodeBlock/Line/index.tsx b/src/theme/CodeBlock/Line/index.tsx new file mode 100644 index 0000000..340163e --- /dev/null +++ b/src/theme/CodeBlock/Line/index.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import clsx from 'clsx'; +import type {Props} from '@theme/CodeBlock/Line'; + +import styles from './styles.module.css'; + +export default function CodeBlockLine({ + line, + classNames, + showLineNumbers, + getLineProps, + getTokenProps, +}: Props): JSX.Element { + if (line.length === 1 && line[0]!.content === '\n') { + line[0]!.content = ''; + } + + const lineProps = getLineProps({ + line, + className: clsx(classNames, showLineNumbers && styles.codeLine), + }); + + const lineTokens = line.map((token, key) => ( + + )); + + return ( + + {showLineNumbers ? ( + <> + + {lineTokens} + + ) : ( + lineTokens + )} +
    +
    + ); +} diff --git a/src/theme/CodeBlock/Line/styles.module.css b/src/theme/CodeBlock/Line/styles.module.css new file mode 100644 index 0000000..7c28ed9 --- /dev/null +++ b/src/theme/CodeBlock/Line/styles.module.css @@ -0,0 +1,45 @@ +/* Intentionally has zero specificity, so that to be able to override +the background in custom CSS file due bug https://github.com/facebook/docusaurus/issues/3678 */ +:where(:root) { + --docusaurus-highlighted-code-line-bg: rgb(72 77 91); +} + +:where([data-theme='dark']) { + --docusaurus-highlighted-code-line-bg: rgb(100 100 100); +} + +:global(.theme-code-block-highlighted-line) { + background-color: var(--docusaurus-highlighted-code-line-bg); + display: block; + margin: 0 calc(-1 * var(--ifm-pre-padding)); + padding: 0 var(--ifm-pre-padding); +} + +.codeLine { + display: table-row; + counter-increment: line-count; +} + +.codeLineNumber { + display: table-cell; + text-align: right; + width: 1%; + position: sticky; + left: 0; + padding: 0 var(--ifm-pre-padding); + background: var(--ifm-pre-background); + overflow-wrap: normal; +} + +.codeLineNumber::before { + content: counter(line-count); + opacity: 0.4; +} + +:global(.theme-code-block-highlighted-line) .codeLineNumber::before { + opacity: 0.8; +} + +.codeLineContent { + padding-right: var(--ifm-pre-padding); +} diff --git a/src/theme/CodeBlock/WordWrapButton/index.tsx b/src/theme/CodeBlock/WordWrapButton/index.tsx new file mode 100644 index 0000000..f472d26 --- /dev/null +++ b/src/theme/CodeBlock/WordWrapButton/index.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import clsx from 'clsx'; +import {translate} from '@docusaurus/Translate'; +import type {Props} from '@theme/CodeBlock/WordWrapButton'; +import IconWordWrap from '@theme/Icon/WordWrap'; + +import styles from './styles.module.css'; + +export default function WordWrapButton({ + className, + onClick, + isEnabled, +}: Props): JSX.Element | null { + const title = translate({ + id: 'theme.CodeBlock.wordWrapToggle', + message: 'Toggle word wrap', + description: + 'The title attribute for toggle word wrapping button of code block lines', + }); + + return ( + + ); +} diff --git a/src/theme/CodeBlock/WordWrapButton/styles.module.css b/src/theme/CodeBlock/WordWrapButton/styles.module.css new file mode 100644 index 0000000..fdbc894 --- /dev/null +++ b/src/theme/CodeBlock/WordWrapButton/styles.module.css @@ -0,0 +1,8 @@ +.wordWrapButtonIcon { + width: 1.2rem; + height: 1.2rem; +} + +.wordWrapButtonEnabled .wordWrapButtonIcon { + color: var(--ifm-color-primary); +} diff --git a/src/theme/CodeBlock/index.tsx b/src/theme/CodeBlock/index.tsx new file mode 100644 index 0000000..1f1face --- /dev/null +++ b/src/theme/CodeBlock/index.tsx @@ -0,0 +1,38 @@ +import React, {isValidElement, type ReactNode} from 'react'; +import useIsBrowser from '@docusaurus/useIsBrowser'; +import ElementContent from '@theme/CodeBlock/Content/Element'; +import StringContent from '@theme/CodeBlock/Content/String'; +import type {Props} from '@theme/CodeBlock'; + +/** + * Best attempt to make the children a plain string so it is copyable. If there + * are react elements, we will not be able to copy the content, and it will + * return `children` as-is; otherwise, it concatenates the string children + * together. + */ +function maybeStringifyChildren(children: ReactNode): ReactNode { + if (React.Children.toArray(children).some((el) => isValidElement(el))) { + return children; + } + // The children is now guaranteed to be one/more plain strings + return Array.isArray(children) ? children.join('') : (children as string); +} + +export default function CodeBlock({ + children: rawChildren, + ...props +}: Props): JSX.Element { + // The Prism theme on SSR is always the default theme but the site theme can + // be in a different mode. React hydration doesn't update DOM styles that come + // from SSR. Hence force a re-render after mounting to apply the current + // relevant styles. + const isBrowser = useIsBrowser(); + const children = maybeStringifyChildren(rawChildren); + const CodeBlockComp = + typeof children === 'string' ? StringContent : ElementContent; + return ( + + {children as string} + + ); +} diff --git a/src/theme/DocTagsListPage/index.tsx b/src/theme/DocTagsListPage/index.tsx new file mode 100644 index 0000000..427bb97 --- /dev/null +++ b/src/theme/DocTagsListPage/index.tsx @@ -0,0 +1,64 @@ +import React, { useState } from 'react' +import clsx from 'clsx' +import { + PageMetadata, + HtmlClassNameProvider, + ThemeClassNames, + translateTagsPageTitle, +} from '@docusaurus/theme-common' +import TagsListByLetter from '@theme/TagsListByLetter' +import SearchMetadata from '@theme/SearchMetadata' +import type { Props } from '@theme/DocTagsListPage' +import { Icon } from '@iconify/react' + +import { TagsListByFlat } from '../TagsListByLetter' +import MyLayout from '../MyLayout' + +export default function DocTagsListPage({ tags }: Props): JSX.Element { + const title = translateTagsPageTitle() + + const [type, setType] = useState('letter') + + return ( + + + + +
    +

    {title}

    +
    +
    + setType('list')} + color={type === 'list' ? 'var(--ifm-color-primary)' : '#ccc'} + /> + setType('grid')} + color={type === 'grid' ? 'var(--ifm-color-primary)' : '#ccc'} + /> +
    +
    +
    + {type === 'letter' && } + {type === 'flat' && } +
    +
    + ) +} diff --git a/src/theme/Footer/index.tsx b/src/theme/Footer/index.tsx new file mode 100644 index 0000000..bbea70a --- /dev/null +++ b/src/theme/Footer/index.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import Footer from '@theme-original/Footer' +import { Analytics } from '@vercel/analytics/react' + +export default function FooterWrapper(props) { + return ( + <> +