diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..698b4c7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Describe the website-related issues here +title: '' +labels: bug +assignees: '' + +--- + +### Checklist + +> Please carefully check the list below and fill in `[x]` in front of the actions you have already completed. + +- [ ] Confirm there are no similar issues in the repository's Issues section +- [ ] Confirm there are no similar discussions in the repository's Discussions section +- [ ] Include screenshots/video demonstration unless you think the text is clear enough + +### Issue Description + +> Describe your issue here in text + +### Screenshots/Video Demonstration + +> Optional, demonstrate your issue in detail here + +### Expected Behavior + +> Describe what you expect to happen here + +### Additional Information + +> Got more to say? Continue here~ diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..ed7f134 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,28 @@ +--- +name: Feature request +about: Describe your needs here +title: '' +labels: feature request +assignees: '' + +--- + +### Checklist + +> Please carefully check the list below and fill in `[x]` in front of the actions you have already completed. + +- [ ] Confirm there are no similar issues in the repository's Issues section +- [ ] Confirm there are no similar discussions in the the repository's Discussions section +- [ ] Include screenshots/video demonstration unless you think the text is clear enough + +### Feature Description + +> Describe your feature request here + +### Online Demonstration + +> Optional, provide a detailed demonstration of your request here. If there is an online link or a ready-made demo, it will help the author understand your needs more clearly. + +### Additional Information + +> Got more to say? Continue here~ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b616de0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,18 @@ +on: + push: + branches: + - master + +name: Release + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: GoogleCloudPlatform/release-please-action@v3 + id: release + with: + token: ${{ secrets.RELEASE_TOKEN }} + release-type: node + package-name: standard-version + changelog-types: '[{"type": "types", "section":"Types", "hidden": false},{"type": "revert", "section":"Reverts", "hidden": false},{"type": "feat", "section": "Features", "hidden": false},{"type": "fix", "section": "Bug Fixes", "hidden": false},{"type": "improvement", "section": "Feature Improvements", "hidden": false},{"type": "docs", "section":"Docs", "hidden": false},{"type": "style", "section":"Styling", "hidden": false},{"type": "refactor", "section":"Code Refactoring", "hidden": false},{"type": "perf", "section":"Performance Improvements", "hidden": false},{"type": "test", "section":"Tests", "hidden": false},{"type": "build", "section":"Build System", "hidden": false},{"type": "ci", "section":"CI", "hidden":false}]' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ddd75c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,134 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# vitepress +docs/.vitepress/dist +cache \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d3f8d5e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,156 @@ +# Changelog + +## [1.3.7](https://github.com/Justin3go/FAV0/compare/v1.3.6...v1.3.7) (2024-06-30) + + +### Docs + +* <weekly> 持续写作的瓶颈 ([45c8bf3](https://github.com/Justin3go/FAV0/commit/45c8bf36143b251c5c1f031a39730412852789b9)) + +## [1.3.6](https://github.com/Justin3go/FAV0/compare/v1.3.5...v1.3.6) (2024-06-23) + + +### Bug Fixes + +* typo ([956c893](https://github.com/Justin3go/FAV0/commit/956c893b081d67c3300f07e8e9b0bc36a58bf6fe)) + + +### Docs + +* <weekly 004>为什么“都”在独立开发 ([204f7a7](https://github.com/Justin3go/FAV0/commit/204f7a74bac0f76180fe6da7551733a6b26932da)) +* add README in Japanese ([d5f13c3](https://github.com/Justin3go/FAV0/commit/d5f13c31c37c731ba02ab32a148bd3d0b9f4657a)) + +## [1.3.5](https://github.com/Justin3go/FAV0/compare/v1.3.4...v1.3.5) (2024-06-16) + + +### Docs + +* <weekly-003> 与GPT的信任危机 ([e19606d](https://github.com/Justin3go/FAV0/commit/e19606d9d2791b1eb145e9e4ed215c0cd27a19f6)) + +## [1.3.4](https://github.com/Justin3go/FAV0/compare/v1.3.3...v1.3.4) (2024-06-09) + + +### Docs + +* <weekly-002> 保持独立开发的动力 ([4d61e6b](https://github.com/Justin3go/FAV0/commit/4d61e6bd055dc913777f741e34e99c6934417388)) + +## [1.3.3](https://github.com/Justin3go/FAV0/compare/v1.3.2...v1.3.3) (2024-06-02) + + +### Docs + +* 优化readme以及修改协议 ([b4b0d30](https://github.com/Justin3go/FAV0/commit/b4b0d306f6238efb0ff0e859a02de14fd461d74e)) + +## [1.3.2](https://github.com/Justin3go/FAV0/compare/v1.3.1...v1.3.2) (2024-06-01) + + +### Docs + +* <FAV0周刊001期> AI内容污染搜索 ([7556917](https://github.com/Justin3go/FAV0/commit/75569177040cd08bd30dfc36822338b88c344153)) + +## [1.3.1](https://github.com/Justin3go/FAV0/compare/v1.3.0...v1.3.1) (2024-05-30) + + +### Bug Fixes + +* 英文RSS订阅链接错误 ([eba2bae](https://github.com/Justin3go/FAV0/commit/eba2bae55f091ac9d8e14e05faa8b4c8a5f1daf0)) + +## [1.3.0](https://github.com/Justin3go/FAV0/compare/v1.2.0...v1.3.0) (2024-05-30) + + +### Features + +* 支持i18n ([29f0ad4](https://github.com/Justin3go/FAV0/commit/29f0ad4381483dfb235f158355735659e1b6dfa4)) + + +### Bug Fixes + +* 搜索的国际化 ([83cc6c9](https://github.com/Justin3go/FAV0/commit/83cc6c90b79dc86882f021ccb6f17a467766d278)) +* 组件库的i18n ([8e6ddcf](https://github.com/Justin3go/FAV0/commit/8e6ddcf6b87a08abd96f47e051033f7cb6ab9c7b)) + + +### Performance Improvements + +* 预加载字体 ([3655787](https://github.com/Justin3go/FAV0/commit/365578700d41535009c9ee0b36646ff53f8b16b3)) + +## [1.2.0](https://github.com/Justin3go/FAV0/compare/v1.1.1...v1.2.0) (2024-05-29) + + +### Features + +* 更新logo ([5c7e27e](https://github.com/Justin3go/FAV0/commit/5c7e27ec7342abd22f5edbc54fde088792f69bd7)) + +## [1.1.1](https://github.com/Justin3go/FAV0/compare/v1.1.0...v1.1.1) (2024-05-28) + + +### Performance Improvements + +* 优化字体文件 ([143998d](https://github.com/Justin3go/FAV0/commit/143998d08cf02d983ea667469c4351c0d52a3986)) + +## [1.1.0](https://github.com/Justin3go/FAV0/compare/v1.0.2...v1.1.0) (2024-05-28) + + +### Features + +* initial commit ([a2ea0a0](https://github.com/Justin3go/FAV0/commit/a2ea0a0829e4ea5a2f5756208a33b021c4e981b0)) +* 修改正文字体 ([83dc3c6](https://github.com/Justin3go/FAV0/commit/83dc3c6e0a037960064069f1e9174a04415ca772)) +* 添加rss生成 ([39020af](https://github.com/Justin3go/FAV0/commit/39020af3ef93b362e4876360f5e7eec0b81136bf)) +* 添加外部链接图标以及优化代码结构 ([1e8b92b](https://github.com/Justin3go/FAV0/commit/1e8b92b1a042a465ffdc2f48e8619873d828d38d)) +* 添加推特卡片及OG信息 ([98e105c](https://github.com/Justin3go/FAV0/commit/98e105ccf24b9b3615a4372de01b3945fa630453)) + + +### Bug Fixes + +* build error ([a4efac5](https://github.com/Justin3go/FAV0/commit/a4efac5493da6d8bebc8022ca5d2cdf34e2e9eea)) +* 移动端导航栏样式 ([600303f](https://github.com/Justin3go/FAV0/commit/600303fabbfb376b49c2c2e8ea880b6ff94bd205)) + + +### Docs + +* readme ([0a4a81d](https://github.com/Justin3go/FAV0/commit/0a4a81d07f9240a344e0377ba8b2f006ca5061db)) +* readme及协议 ([c69826f](https://github.com/Justin3go/FAV0/commit/c69826fd1b221bbfc1c75f6fe50fb261f8618937)) + + +### CI + +* auto release ([55bbb73](https://github.com/Justin3go/FAV0/commit/55bbb738c6c124fa3cbd969dcda406b5fcf6add3)) + +## [1.0.2](https://github.com/Justin3go/FAV0/compare/v1.0.1...v1.0.2) (2024-05-27) + + +### Bug Fixes + +* 移动端导航栏样式 ([600303f](https://github.com/Justin3go/FAV0/commit/600303fabbfb376b49c2c2e8ea880b6ff94bd205)) + +## [1.0.2](https://github.com/Justin3go/FAV0/compare/v1.0.1...v1.0.2) (2024-05-27) + + +### Bug Fixes + +* 移动端导航栏样式 ([600303f](https://github.com/Justin3go/FAV0/commit/600303fabbfb376b49c2c2e8ea880b6ff94bd205)) + +## [1.0.1](https://github.com/Justin3go/FAV0/compare/v1.0.0...v1.0.1) (2024-05-27) + + +### Docs + +* readme及协议 ([c69826f](https://github.com/Justin3go/FAV0/commit/c69826fd1b221bbfc1c75f6fe50fb261f8618937)) + +## 1.0.0 (2024-05-27) + + +### Features + +* initial commit ([a2ea0a0](https://github.com/Justin3go/FAV0/commit/a2ea0a0829e4ea5a2f5756208a33b021c4e981b0)) +* 添加rss生成 ([39020af](https://github.com/Justin3go/FAV0/commit/39020af3ef93b362e4876360f5e7eec0b81136bf)) +* 添加推特卡片及OG信息 ([98e105c](https://github.com/Justin3go/FAV0/commit/98e105ccf24b9b3615a4372de01b3945fa630453)) + + +### Docs + +* readme ([0a4a81d](https://github.com/Justin3go/FAV0/commit/0a4a81d07f9240a344e0377ba8b2f006ca5061db)) + + +### CI + +* auto release ([55bbb73](https://github.com/Justin3go/FAV0/commit/55bbb738c6c124fa3cbd969dcda406b5fcf6add3)) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..73f8060 --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +## All files with the `.md` suffix in this repository are licensed under the following terms: + +Creative Commons Attribution 4.0 International License + +This work is licensed under a Creative Commons Attribution 4.0 International License. + +You are free to share and adapt the material under the following terms: + +- Attribution: You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. + +For detailed terms and conditions, please see https://creativecommons.org/licenses/by/4.0/legalcode. + +## All other files are under the MIT License + +Copyright (C) + +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. + +For more information, see [here](https://en.wikipedia.org/wiki/MIT_License). \ No newline at end of file diff --git a/README.ja.md b/README.ja.md new file mode 100644 index 0000000..a952fd3 --- /dev/null +++ b/README.ja.md @@ -0,0 +1,70 @@ +

英語 | 日本語

+ +
+ + + logo + + +# 《FAV0週刊》 + +![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white) +![VitePress](https://img.shields.io/badge/VitePress-646CFF?style=for-the-badge&logo=vite&logoColor=white) +![Vue-3](https://img.shields.io/badge/Vue-3-4FC08D?style=for-the-badge&logo=vue.js&logoColor=white) +![TDesign](https://img.shields.io/badge/TDesign-0052CC?style=for-the-badge&logo=tdesign&logoColor=white) +![Cloudflare Pages](https://img.shields.io/badge/Cloudflare%20Pages-F38020?style=for-the-badge&logo=cloudflare&logoColor=white) +![Giscus](https://img.shields.io/badge/Giscus-181717?style=for-the-badge&logo=github&logoColor=white) +![Support RSS](https://img.shields.io/badge/Support%20RSS-FFA500?style=for-the-badge&logo=rss&logoColor=white) +![Support I18N](https://img.shields.io/badge/Support%20I18N-0078D4?style=for-the-badge&logo=google-translate&logoColor=white) +![SEO](https://img.shields.io/badge/SEO-4285F4?style=for-the-badge&logo=google&logoColor=white) + +毎週見たり聞いたりしたことを記録し、主にフロントエンド、AI、コンピュータ関連の内容に焦点を当てています。 + +毎週土曜日/週末に更新され、更新はリリースと同期されます。最新の週刊リリースについては、スターをつけたりリリースをカスタムでウォッチしたりしてください。 + +[![changelog](https://img.shields.io/badge/changelog-→-0052CC?style=for-the-badge&logo=ReSharper&logoColor=white)](./CHANGELOG.md) + + +[![PR Welcome](https://img.shields.io/badge/PR-Welcome-EA4AAA?style=for-the-badge&logo=git&logoColor=white)](https://github.com/Justin3go/FAV0/pulls) +[![Request-Feature](https://img.shields.io/badge/Request-Feature-007BFF?style=for-the-badge&logo=github&logoColor=white)](https://github.com/Justin3go/FAV0/issues/new/choose) +[![Report-Bug](https://img.shields.io/badge/Report-Bug-red?style=for-the-badge&logo=github&logoColor=white)](https://github.com/Justin3go/FAV0/issues/new/choose) + +![demo](./images/demo.png) + +
+ +## 機能特性 + + +1. 🌓 明るいモードと暗いモードの切り替えを提供し、異なる読書環境に適応します。 +2. 🌍 中国語と英語のバイリンガルインターフェースをサポートし、異なる言語のユーザーの利便性を高めます。 +3. 📡 RSS購読機能を提供し、中国語と英語の両方の更新をサポートします。 +4. 💬 Giscusコメントシステムを統合し、ユーザーのコミュニケーションとフィードバックを容易にします。 +5. 🖼️ 高解像度の画像プレビューをサポートし、視覚体験を最適化します。 +6. 📜 フォント設定のカスタマイズを許可し、読書の快適さを向上させます。 +7. 🔍 SEO最適化を実施し、Sitemapの生成、Twitter CardとOpen Graphタグのサポートを含む、検索エンジンの可視性を向上させます。 + + +## 開発 + +```bash +git clone git@github.com:Justin3go/FAV0.git +cd FAV0 + +npm i -g pnpm # 必要な場合 +pnpm i +pnpm docs:dev +``` +1. giscusコメント設定を変更します。`.vitepress/theme/components/Comments.vue`内の`giscus`設定を変更してください。 +2. `utils`フォルダ内のサイドバー設定、RSS設定、メタ情報設定などを変更します。 +3. `config`フォルダ内の関連設定を変更します。主にタイトル、説明などです。 +4. `posts/**`および`en/posts/**`ディレクトリ内の記事の内容を自分の内容に変更します。 + +## ライセンス + +このリポジトリは、MITライセンスとCC-BY-4.0ライセンスの下でデュアルライセンスされています: + +- すべての`.md`ファイルはCC-BY-4.0ライセンスの下でライセンスされており、著作権表示を保持する必要があります。 +- その他のコードファイルはMITライセンスの下でライセンスされており、自由に使用できます。 + +詳細については、[LICENSE](./LICENSE)ファイルをご覧ください。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..e938cab --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +

English | 简体中文 | 日本語

+ +
+ + + logo + + +# FAV0 Weekly + +![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white) +![VitePress](https://img.shields.io/badge/VitePress-646CFF?style=for-the-badge&logo=vite&logoColor=white) +![Vue-3](https://img.shields.io/badge/Vue-3-4FC08D?style=for-the-badge&logo=vue.js&logoColor=white) +![TDesign](https://img.shields.io/badge/TDesign-0052CC?style=for-the-badge&logo=tdesign&logoColor=white) +![Cloudflare Pages](https://img.shields.io/badge/Cloudflare%20Pages-F38020?style=for-the-badge&logo=cloudflare&logoColor=white) +![Giscus](https://img.shields.io/badge/Giscus-181717?style=for-the-badge&logo=github&logoColor=white) +![Support RSS](https://img.shields.io/badge/Support%20RSS-FFA500?style=for-the-badge&logo=rss&logoColor=white) +![Support I18N](https://img.shields.io/badge/Support%20I18N-0078D4?style=for-the-badge&logo=google-translate&logoColor=white) +![SEO](https://img.shields.io/badge/SEO-4285F4?style=for-the-badge&logo=google&logoColor=white) + +Record what I see and hear every week, mainly focusing on front-end, AI, and computer-related content. + +Updated every Saturday/weekend, with updates synchronized with releases. Feel free to star/watch releases in custom to stay updated with the latest weekly releases. + +[![changelog](https://img.shields.io/badge/changelog-→-0052CC?style=for-the-badge&logo=ReSharper&logoColor=white)](./CHANGELOG.md) + + +[![PR Welcome](https://img.shields.io/badge/PR-Welcome-EA4AAA?style=for-the-badge&logo=git&logoColor=white)](https://github.com/Justin3go/FAV0/pulls) +[![Request-Feature](https://img.shields.io/badge/Request-Feature-007BFF?style=for-the-badge&logo=github&logoColor=white)](https://github.com/Justin3go/FAV0/issues/new/choose) +[![Report-Bug](https://img.shields.io/badge/Report-Bug-red?style=for-the-badge&logo=github&logoColor=white)](https://github.com/Justin3go/FAV0/issues/new/choose) + +![demo](./images/demo.png) + +
+ +## Features + + +1. 🌓 Provides light and dark mode switching to adapt to different reading environments. +2. 🌍 Supports bilingual interface in Chinese and English for the convenience of users of different languages. +3. 📡 Provides RSS subscription function, supporting updates in both Chinese and English. +4. 💬 Integrated Giscus comment system for user communication and feedback. +5. 🖼️ Supports high-definition image preview for optimized visual experience. +6. 📜 Allows customization of font settings for improved reading comfort. +7. 🔍 Performs SEO optimization, including Sitemap generation, Twitter Card, and Open Graph tag support to improve search engine visibility. + + +## Development + +```bash +git clone git@github.com:Justin3go/FAV0.git +cd FAV0 + +npm i -g pnpm # if needed +pnpm i +pnpm docs:dev +``` +1. Modify the giscus comment configuration in `.vitepress/theme/components/Comments.vue`, specifically the `giscus` configuration; +2. Modify the sidebar configuration, RSS configuration, metadata configuration, etc., in the `utils` folder; +3. Modify the related configurations in the `config` folder, mainly title, description, etc.; +4. Change the content of the articles in the `posts/**` and `en/posts/**` directories to your own content; + +## License + +This repository is dual-licensed under the MIT License and CC-BY-4.0 License: + +- All `.md` files are licensed under the CC-BY-4.0 License, you need to retain attribution. +- Other code files are licensed under the MIT License, you may use them freely. + +For more details, please see the [LICENSE](./LICENSE) file. diff --git a/README.zh.md b/README.zh.md new file mode 100644 index 0000000..9693b9c --- /dev/null +++ b/README.zh.md @@ -0,0 +1,70 @@ +

English | 简体中文 | 日本語

+ +
+ + + logo + + +# 《FAV0周刊》 + +![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white) +![VitePress](https://img.shields.io/badge/VitePress-646CFF?style=for-the-badge&logo=vite&logoColor=white) +![Vue-3](https://img.shields.io/badge/Vue-3-4FC08D?style=for-the-badge&logo=vue.js&logoColor=white) +![TDesign](https://img.shields.io/badge/TDesign-0052CC?style=for-the-badge&logo=tdesign&logoColor=white) +![Cloudflare Pages](https://img.shields.io/badge/Cloudflare%20Pages-F38020?style=for-the-badge&logo=cloudflare&logoColor=white) +![Giscus](https://img.shields.io/badge/Giscus-181717?style=for-the-badge&logo=github&logoColor=white) +![Support RSS](https://img.shields.io/badge/Support%20RSS-FFA500?style=for-the-badge&logo=rss&logoColor=white) +![Support I18N](https://img.shields.io/badge/Support%20I18N-0078D4?style=for-the-badge&logo=google-translate&logoColor=white) +![SEO](https://img.shields.io/badge/SEO-4285F4?style=for-the-badge&logo=google&logoColor=white) + +记录每周所见所闻,主要关注前端、AI领域以及计算机相关内容 + +每周六/周末更新,更新同步release一次,欢迎star/watch releases in custom关注最新周刊发布 + +[![changelog](https://img.shields.io/badge/changelog-→-0052CC?style=for-the-badge&logo=ReSharper&logoColor=white)](./CHANGELOG.md) + + +[![PR Welcome](https://img.shields.io/badge/PR-Welcome-EA4AAA?style=for-the-badge&logo=git&logoColor=white)](https://github.com/Justin3go/FAV0/pulls) +[![Request-Feature](https://img.shields.io/badge/Request-Feature-007BFF?style=for-the-badge&logo=github&logoColor=white)](https://github.com/Justin3go/FAV0/issues/new/choose) +[![Report-Bug](https://img.shields.io/badge/Report-Bug-red?style=for-the-badge&logo=github&logoColor=white)](https://github.com/Justin3go/FAV0/issues/new/choose) + +![demo](./images/demo.png) + +
+ +## 功能特性 + + +1. 🌓 提供明暗模式切换功能,适应不同的阅读环境。 +2. 🌍 支持中英双语界面,方便不同语言用户的使用。 +3. 📡 提供RSS订阅功能,支持中英文内容更新推送。 +4. 💬 集成Giscus评论系统,便于用户交流和反馈。 +5. 🖼️ 支持高清大图预览,优化视觉体验。 +6. 📜 允许自定义字体设置,提升阅读舒适度。 +7. 🔍 进行SEO优化,包括Sitemap生成、Twitter Card和Open Graph标签支持,提高搜索引擎可见性。 + + +## 开发 + +```bash +git clone git@github.com:Justin3go/FAV0.git +cd FAV0 + +npm i -g pnpm # 如果需要 +pnpm i +pnpm docs:dev +``` +1. 修改giscus评论配置,`.vitepress/theme/components/Comments.vue`中的`giscus`配置项; +2. 修改`utils`文件夹下的中的侧边栏配置、RSS配置、元信息配置等; +3. 修改`config`文件夹下的相关配置,主要是title、description等; +4. 修改`posts/**`与`en/posts/**`目录中的文章内容为自己的内容; + +## 协议 + +本仓库采用双协议授权,即MIT协议和CC-BY-4.0协议: + +- 所有`.md`文件采用CC-BY-4.0协议协议,你需要保留署名权 +- 其他代码文件采用MIT协议,你可以自由使用 + +具体内容请查看[LICENSE](./LICENSE)文件。 diff --git a/docs/.vitepress/config/en.ts b/docs/.vitepress/config/en.ts new file mode 100644 index 0000000..a4703bc --- /dev/null +++ b/docs/.vitepress/config/en.ts @@ -0,0 +1,31 @@ +import { defineConfig} from 'vitepress' + +import { createSideBar } from "../theme/utils/createSideBar"; + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: "FAV0 Weekly", + description: "FAV0 Weekly: Documenting Weekly Observations and Experiences, with a Focus on Front-end Development, AI, and Computer-related Topics", + lang: "en-US", //语言 + + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + + sidebar: createSideBar(), + + socialLinks: [ + { icon: 'x', link: 'https://x.com/Justin1024go' }, + { icon: 'github', link: 'https://github.com/Justin3go/FAV0' }, + { + icon: { + svg: 'RSS', + }, + link: "/feed-en.xml", + }, + ], + + editLink: { + pattern: "https://github.com/Justin3go/FAV0/edit/master/docs/:path" + }, + }, +}) diff --git a/docs/.vitepress/config/index.mts b/docs/.vitepress/config/index.mts new file mode 100644 index 0000000..8f8fbd9 --- /dev/null +++ b/docs/.vitepress/config/index.mts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vitepress' + +import shared from './shared' +import en from './en' +import zh from './zh' + +export default defineConfig({ + ...shared, + locales: { + root: { label: '简体中文', ...zh }, + en: { label: 'English', ...en }, + } +}) \ No newline at end of file diff --git a/docs/.vitepress/config/shared.ts b/docs/.vitepress/config/shared.ts new file mode 100644 index 0000000..6645680 --- /dev/null +++ b/docs/.vitepress/config/shared.ts @@ -0,0 +1,76 @@ +import { defineConfig, type SiteConfig } from 'vitepress' +// 自动导入TDesign +import AutoImport from 'unplugin-auto-import/vite'; +import Components from 'unplugin-vue-components/vite'; +import { TDesignResolver } from 'unplugin-vue-components/resolvers'; + +import { createRssFileZH, createRssFileEN } from "../utils/rss"; +import { handleHeadMeta } from "../utils/handleHeadMeta"; +import { search as zhSearch } from './zh' + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + lastUpdated: true, + cleanUrls: true, + ignoreDeadLinks: true, + sitemap: { + hostname: 'https://fav0.com' + }, + head: [ + ["script", { async: "", src: "https://www.googletagmanager.com/gtag/js?id=G-Z6HGDC7ZBL" }], + [ + "script", + {}, + `window.dataLayer = window.dataLayer || []; + function gtag(){dataLayer.push(arguments);} + gtag('js', new Date()); + gtag('config', 'G-Z6HGDC7ZBL');`, + ], + + [ + "link", + { + rel: "icon", + href: "/favicon.ico", + }, + ], + ], + // https://vitepress.dev/reference/site-config#transformhead + async transformHead(context) { + return handleHeadMeta(context) + }, + buildEnd: (config: SiteConfig) => { + createRssFileZH(config); + createRssFileEN(config); + }, + + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + outline: [2, 4], + + search: { + provider: "local", + options: { + locales: { ...zhSearch } + } + }, + + externalLinkIcon: true, + }, + + vite: { + plugins: [ + // ... + AutoImport({ + resolvers: [TDesignResolver({ + library: 'vue-next' + })], + }), + Components({ + resolvers: [TDesignResolver({ + library: 'vue-next' + })], + }), + ], + }, +}) diff --git a/docs/.vitepress/config/zh.ts b/docs/.vitepress/config/zh.ts new file mode 100644 index 0000000..0cde7b9 --- /dev/null +++ b/docs/.vitepress/config/zh.ts @@ -0,0 +1,90 @@ +import { DefaultTheme, defineConfig } from 'vitepress' + +import { createSideBar } from "../theme/utils/createSideBar"; + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: "Justin3go's Blog-🖊", + description: "《FAV0周刊》:记录每周所见所闻,主要关注前端、AI领域以及计算机相关内容", + lang: "zh-Hans", //语言 + + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + nav: [ + { text: "首页", link: "/" }, + { text: "笔记", link: "/笔记/", activeMatch: '/笔记/' }, + { text: "关于", link: "/about/", activeMatch: '/about/' }, + ], + docFooter: { + prev: '上一篇', + next: '下一篇' + }, + outlineTitle: "当前页面", + lastUpdatedText: "最近更新时间", + + sidebar: createSideBar(), + + socialLinks: [ + { icon: 'x', link: 'https://x.com/Justin1024go' }, + { icon: 'github', link: 'https://github.com/Justin3go/FAV0' }, + { + icon: { + svg: 'RSS', + }, + link: "/feed.xml", + }, + ], + + editLink: { + pattern: "https://github.com/Justin3go/FAV0/edit/master/docs/:path", + text: "在GitHub上编辑此页", + }, + returnToTopLabel: "回到顶部", + sidebarMenuLabel: "目录", + darkModeSwitchLabel: "深色模式", + }, +}) + +export const search: DefaultTheme.AlgoliaSearchOptions['locales'] = { + root: { + placeholder: '搜索文档', + translations: { + button: { + buttonText: '搜索文档', + buttonAriaLabel: '搜索文档' + }, + modal: { + searchBox: { + resetButtonTitle: '清除查询条件', + resetButtonAriaLabel: '清除查询条件', + cancelButtonText: '取消', + cancelButtonAriaLabel: '取消' + }, + startScreen: { + recentSearchesTitle: '搜索历史', + noRecentSearchesText: '没有搜索历史', + saveRecentSearchButtonTitle: '保存至搜索历史', + removeRecentSearchButtonTitle: '从搜索历史中移除', + favoriteSearchesTitle: '收藏', + removeFavoriteSearchButtonTitle: '从收藏中移除' + }, + errorScreen: { + titleText: '无法获取结果', + helpText: '你可能需要检查你的网络连接' + }, + footer: { + selectText: '选择', + navigateText: '切换', + closeText: '关闭', + searchByText: '搜索提供者' + }, + noResultsScreen: { + noResultsText: '无法找到相关结果', + suggestedQueryText: '你可以尝试查询', + reportMissingResultsText: '你认为该查询应该有结果?', + reportMissingResultsLinkText: '点击反馈' + } + } + } + } +} diff --git a/docs/.vitepress/theme/components/BlogHome.vue b/docs/.vitepress/theme/components/BlogHome.vue new file mode 100644 index 0000000..c4a59aa --- /dev/null +++ b/docs/.vitepress/theme/components/BlogHome.vue @@ -0,0 +1,96 @@ + + + diff --git a/docs/.vitepress/theme/components/Comment.vue b/docs/.vitepress/theme/components/Comment.vue new file mode 100644 index 0000000..3872a86 --- /dev/null +++ b/docs/.vitepress/theme/components/Comment.vue @@ -0,0 +1,54 @@ + + + diff --git a/docs/.vitepress/theme/components/GoBack.vue b/docs/.vitepress/theme/components/GoBack.vue new file mode 100644 index 0000000..2e47aec --- /dev/null +++ b/docs/.vitepress/theme/components/GoBack.vue @@ -0,0 +1,42 @@ + + + diff --git a/docs/.vitepress/theme/components/ImageViewer.vue b/docs/.vitepress/theme/components/ImageViewer.vue new file mode 100644 index 0000000..ea43852 --- /dev/null +++ b/docs/.vitepress/theme/components/ImageViewer.vue @@ -0,0 +1,84 @@ + + + diff --git a/docs/.vitepress/theme/components/NotesRedirect.vue b/docs/.vitepress/theme/components/NotesRedirect.vue new file mode 100644 index 0000000..b576399 --- /dev/null +++ b/docs/.vitepress/theme/components/NotesRedirect.vue @@ -0,0 +1,16 @@ + + + diff --git a/docs/.vitepress/theme/components/TDesignDark.vue b/docs/.vitepress/theme/components/TDesignDark.vue new file mode 100644 index 0000000..c2f3424 --- /dev/null +++ b/docs/.vitepress/theme/components/TDesignDark.vue @@ -0,0 +1,23 @@ + + diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts new file mode 100644 index 0000000..733b519 --- /dev/null +++ b/docs/.vitepress/theme/index.ts @@ -0,0 +1,27 @@ +// https://vitepress.dev/guide/custom-theme +import { h } from "vue"; +import Theme from 'vitepress/theme-without-fonts' // https://vitepress.dev/zh/guide/extending-default-theme#using-different-fonts +// 引入组件库的少量全局样式变量 +import 'tdesign-vue-next/es/style/index.css'; + +import "./style.css"; +import Comment from "./components/Comment.vue"; +import ImageViewer from "./components/ImageViewer.vue" +import GoBack from "./components/GoBack.vue"; + +export default { + ...Theme, + Layout: () => { + return h(Theme.Layout, null, { + // https://vitepress.dev/guide/extending-default-theme#layout-slots + "doc-after": () => h(Comment), + "doc-bottom": () => h(ImageViewer), + "aside-top": () => h(GoBack), + }); + }, + + enhanceApp({ app }) { + app.component("Comment", Comment); + }, +}; + diff --git a/docs/.vitepress/theme/posts.data.mts b/docs/.vitepress/theme/posts.data.mts new file mode 100644 index 0000000..811fb22 --- /dev/null +++ b/docs/.vitepress/theme/posts.data.mts @@ -0,0 +1,44 @@ +import { createContentLoader } from 'vitepress' + +interface Post { + title: string + url: string + date: { + time: number + string: string + } + excerpt: string | undefined +} + +export declare const data: Post[] + +export default createContentLoader('博客/**/*.md', { + excerpt: excerptFn, + transform(raw): Post[] { + return raw + .map(({ url, frontmatter, excerpt }) => ({ + title: frontmatter.title, + url, + excerpt, + date: formatDate(url.substring(4, 14) ) + })) + .sort((a, b) => b.date.time - a.date.time) + } +}) + +function excerptFn(file: { data: { [key: string]: any }; content: string; excerpt?: string }, options?: any) { + file.excerpt = file.content.split('')[1]; +} + +function formatDate(raw: string): Post['date'] { + const date = new Date(raw) + date.setUTCHours(12) + return { + time: +date, + string: date.toLocaleDateString('zh-Hans', { + year: 'numeric', + month: 'long', + day: 'numeric' + }) + } +} \ No newline at end of file diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css new file mode 100644 index 0000000..6dcaec9 --- /dev/null +++ b/docs/.vitepress/theme/style.css @@ -0,0 +1,164 @@ +/** 代码字体 */ +@font-face { + font-family: "FiraCode"; + src: url("/assets/fonts/FiraCode-VF.woff2"); +} +/** 正文字体 */ +@font-face { + font-family: "SourceHanSerifCN"; + src: local("SourceHanSerifCN"), url("/assets/fonts/SourceHanSerifCN-VF.woff2"); +} + +/* 参考 https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css */ + +/** + * Colors Base + * + * These are the pure base color presets. Most of the time, you should not be + * using these colors directly in the theme but rather use "Colors Theme" + * instead because those are "Theme (light or dark)" dependant. + * -------------------------------------------------------------------------- */ + +:root { + --vp-c-blue-1: #2949a4; + --vp-c-blue-2: #0749ff; + --vp-c-blue-3: #7494ec; + --vp-c-blue-soft: rgba(110, 156, 190, 0.14); + + --vp-c-yellow-1: #aa9100; + --vp-c-yellow-2: #d5b811; + --vp-c-yellow-3: #ecce23; + --vp-c-yellow-soft: rgba(186, 186, 186, 0.14); +} + +/** + * Colors Theme + * -------------------------------------------------------------------------- */ + +:root { + --vp-c-brand-1: var(--vp-c-blue-1); + --vp-c-brand-2: var(--vp-c-blue-2); + --vp-c-brand-3: var(--vp-c-blue-3); + --vp-c-brand-soft: var(--vp-c-blue-soft); +} + +.dark { + --vp-c-brand-1: var(--vp-c-yellow-1); + --vp-c-brand-2: var(--vp-c-yellow-2); + --vp-c-brand-3: var(--vp-c-yellow-3); + --vp-c-brand-soft: var(--vp-c-yellow-soft); +} + +/** + * Typography + * -------------------------------------------------------------------------- */ + +:root { + --vp-font-family-base: "SourceHanSerifCN"; + --vp-font-family-mono: "FiraCode"; +} + +:root { + --td-border-level-2-color: var(--vp-c-brand-soft) !important; + --td-brand-color: var(--vp-c-brand-1) !important; + --td-brand-color-light: var(--vp-c-brand-soft) !important; + /* 字体family token */ + --td-font-family: SourceHanSerifCN, PingFang SC, Microsoft YaHei, Arial Regular; + --td-font-family-medium: SourceHanSerifCN, PingFang SC, Microsoft YaHei, Arial Medium; +} + +/** + * 特殊配置 + * -------------------------------------------------------------------------- */ + +/* 侧边栏标题隐藏单行文本溢出 */ +.VPSidebar .items p { + display: inline-block; + width: calc(var(--vp-sidebar-width) - 100px); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/** 总体类 */ +::selection { + background: var(--vp-c-brand-1); + color: var(--vp-c-bg); +} + +::-moz-selection { + background: var(--vp-c-brand-1); + color: var(--vp-c-bg); +} + +::-webkit-selection { + background: var(--vp-c-brand-1); + color: var(--vp-c-bg); +} + +/** 链接样式 */ +.VPDoc a { + text-decoration: none !important; +} + +.VPDoc a:hover { + text-decoration: underline !important; +} + +.VPDoc img { + border-radius: 4px; + cursor: zoom-in; +} + +/* jupyter输出代码不换行显示 */ +main > div > div > pre > code { + white-space: normal; +} + +#app { + /* 取消移动端点击div默认高亮效果 */ + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +/** 磨砂玻璃效果设置 */ +.VPNavBar { + background-color: transparent !important; +} +.VPNav .content .content-body { + backdrop-filter: blur(36px); + background-color: linear-gradient( + to bottom, + rgba(var(--vp-nav-bg-color), 0.98), + rgba(var(--vp-nav-bg-color), 0.6) + ) !important; +} + +.VPLocalNav, +.VPLocalNav .container { + backdrop-filter: blur(36px); + background-color: linear-gradient( + to bottom, + rgba(var(--vp-nav-bg-color), 0.6), + rgba(var(--vp-nav-bg-color), 0.4) + ) !important; +} + +.VPLocalNav, +.has-aside .container { + backdrop-filter: none; + background-color: none !important; +} + +/** i18n选项不显示下拉图标 */ +.vpi-chevron-down, .text-icon { + display: none; +} + +/** 主题切换按钮样式 */ +.VPSwitchAppearance { + width: 22px !important; +} + +.VPSwitchAppearance .check { + transform: none !important; +} diff --git a/docs/.vitepress/theme/utils/createSideBar.ts b/docs/.vitepress/theme/utils/createSideBar.ts new file mode 100644 index 0000000..5bd5c2f --- /dev/null +++ b/docs/.vitepress/theme/utils/createSideBar.ts @@ -0,0 +1,212 @@ +export function createSideBar() { + return { + "/笔记/": [ + { + text: "Python基础", + collapsed: false, + items: [ + { text: "01python数据模型", link: "/笔记/Python基础/01python数据模型" }, + { text: "02序列构成的数组", link: "/笔记/Python基础/02序列构成的数组" }, + { text: "03字典与集合", link: "/笔记/Python基础/03字典与集合" }, + { text: "04文本与字节序列", link: "/笔记/Python基础/04文本与字节序列" }, + { text: "05一等函数", link: "/笔记/Python基础/05一等函数" }, + { text: "06使用一等函数实现设计模式", link: "/笔记/Python基础/06使用一等函数实现设计模式" }, + { text: "07函数装饰器与闭包", link: "/笔记/Python基础/07函数装饰器与闭包" }, + { text: "08对象引用、可变性和垃圾回收", link: "/笔记/Python基础/08对象引用、可变性和垃圾回收" }, + { text: "09符合python风格的对象", link: "/笔记/Python基础/09符合python风格的对象" }, + { text: "10序列的修改、散列和切片", link: "/笔记/Python基础/10序列的修改、散列和切片" }, + { text: "11接口:从协议到抽象基类", link: "/笔记/Python基础/11接口:从协议到抽象基类" }, + { text: "12继承的优缺点", link: "/笔记/Python基础/12继承的优缺点" }, + { text: "13正确重载运算符", link: "/笔记/Python基础/13正确重载运算符" }, + { text: "14可迭代的对象、迭代器和生成器", link: "/笔记/Python基础/14可迭代的对象、迭代器和生成器" }, + { text: "15上下文管理和else块", link: "/笔记/Python基础/15上下文管理和else块" }, + { text: "16协程", link: "/笔记/Python基础/16协程" }, + { text: "17使用future处理并发", link: "/笔记/Python基础/17使用future处理并发" }, + { text: "18使用asyncio包处理并发", link: "/笔记/Python基础/18使用asyncio包处理并发" }, + { text: "19元编程", link: "/笔记/Python基础/19元编程" }, + ], + }, + { + text: "threejs入门", + collapsed: false, + items: [ + { text: "01起步", link: "/笔记/threejs入门/01起步" }, + { text: "02一个基本的threejs应用", link: "/笔记/threejs入门/02一个基本的threejs应用" }, + { text: "03基于物理的渲染和照明", link: "/笔记/threejs入门/03基于物理的渲染和照明" }, + { text: "04变换、坐标系和场景图", link: "/笔记/threejs入门/04变换、坐标系和场景图" }, + { text: "05动画循环", link: "/笔记/threejs入门/05动画循环" }, + { text: "06纹理映射", link: "/笔记/threejs入门/06纹理映射" }, + { text: "07插件", link: "/笔记/threejs入门/07插件" }, + { text: "08环境光", link: "/笔记/threejs入门/08环境光" }, + { text: "09组织你的场景", link: "/笔记/threejs入门/09组织你的场景" }, + { text: "10内置几何体", link: "/笔记/threejs入门/10内置几何体" }, + { text: "11以gLTF格式加载3D模型", link: "/笔记/threejs入门/11以gLTF格式加载3D模型" }, + { text: "12threejs动画系统", link: "/笔记/threejs入门/12threejs动画系统" }, + ], + }, + { + text: "Rust基础学习", + collapsed: false, + items: [ + { text: "01认识Cargo", link: "/笔记/Rust基础学习/01认识Cargo" }, + { text: "02变量绑定与解构", link: "/笔记/Rust基础学习/02变量绑定与解构" }, + { text: "03基本类型", link: "/笔记/Rust基础学习/03基本类型" }, + { text: "04所有权与借用", link: "/笔记/Rust基础学习/04所有权与借用" }, + { text: "05复合类型", link: "/笔记/Rust基础学习/05复合类型" }, + { text: "06流程控制", link: "/笔记/Rust基础学习/06流程控制" }, + { text: "07模式匹配", link: "/笔记/Rust基础学习/07模式匹配" }, + { text: "08方法Method", link: "/笔记/Rust基础学习/08方法Method" }, + { text: "09泛型", link: "/笔记/Rust基础学习/09泛型" }, + { text: "10特征", link: "/笔记/Rust基础学习/10特征" }, + { text: "11特征对象", link: "/笔记/Rust基础学习/11特征对象" }, + { text: "12深入特征", link: "/笔记/Rust基础学习/12深入特征" }, + { text: "13动态数组Vector", link: "/笔记/Rust基础学习/13动态数组Vector" }, + { text: "14KV存储HashMap", link: "/笔记/Rust基础学习/14KV存储HashMap" }, + { text: "15认识生命周期", link: "/笔记/Rust基础学习/15认识生命周期" }, + { text: "16返回值和错误处理", link: "/笔记/Rust基础学习/16返回值和错误处理" }, + { text: "17包和模块", link: "/笔记/Rust基础学习/17包和模块" }, + { text: "18注释和文档", link: "/笔记/Rust基础学习/18注释和文档" }, + { text: "19格式化输出", link: "/笔记/Rust基础学习/19格式化输出" }, + { text: "20实战-文件搜索工具", link: "/笔记/Rust基础学习/20实战-文件搜索工具" }, + ], + }, + { + text: "微前端设计与实现笔记", + collapsed: false, + items: [ + { text: "01前端概览", link: "/笔记/微前端设计与实现/01前端概览" }, + { text: "02微前端原则", link: "/笔记/微前端设计与实现/02微前端原则" }, + { text: "03微前端的架构和挑战", link: "/笔记/微前端设计与实现/03微前端的架构和挑战" }, + { text: "04探索微前端架构", link: "/笔记/微前端设计与实现/04探索微前端架构" }, + { text: "05其他", link: "/笔记/微前端设计与实现/05其他" }, + ], + }, + { + text: "ChatGPT提示学习笔记", + collapsed: false, + items: [ + { text: "1_2引言—指示", link: "/笔记/ChatGPT提示学习/ChatGPT提示学习笔记1_2" }, + { text: "3迭代", link: "/笔记/ChatGPT提示学习/ChatGPT提示学习笔记3" }, + { text: "4摘要", link: "/笔记/ChatGPT提示学习/ChatGPT提示学习笔记4" }, + { text: "5推理", link: "/笔记/ChatGPT提示学习/ChatGPT提示学习笔记5" }, + { text: "6转换", link: "/笔记/ChatGPT提示学习/ChatGPT提示学习笔记6" }, + { text: "7扩展", link: "/笔记/ChatGPT提示学习/ChatGPT提示学习笔记7" }, + { text: "8聊天机器人", link: "/笔记/ChatGPT提示学习/ChatGPT提示学习笔记8" }, + ], + }, + { + text: "算法与数据结构", + collapsed: false, + items: [ + { text: "基础概念", link: "/笔记/算法与数据结构/01基础概念" }, + { text: "线性表", link: "/笔记/算法与数据结构/02线性表" }, + { text: "栈和队列", link: "/笔记/算法与数据结构/03栈和队列" }, + { text: "数组", link: "/笔记/算法与数据结构/04数组" }, + { text: "树", link: "/笔记/算法与数据结构/05树" }, + { text: "图", link: "/笔记/算法与数据结构/06图" }, + { text: "查找", link: "/笔记/算法与数据结构/07查找" }, + { text: "排序", link: "/笔记/算法与数据结构/08排序" }, + { text: "算法概述", link: "/笔记/算法与数据结构/10算法概述" }, + { text: "递归与分治", link: "/笔记/算法与数据结构/11递归与分治" }, + { text: "动态规划", link: "/笔记/算法与数据结构/12动态规划" }, + { text: "贪心算法", link: "/笔记/算法与数据结构/13贪心算法" }, + { text: "回溯与分支极限", link: "/笔记/算法与数据结构/14回溯与分支界限" }, + { text: "经典算法实现", link: "/笔记/算法与数据结构/15经典算法实现" }, + { text: "剑指Offer", link: "/笔记/算法与数据结构/16剑指Offer" }, + ], + }, + { + text: "计算机基础知识", + collapsed: false, + items: [{ text: "操作系统基础", link: "/笔记/计算机基础知识/01操作系统基础" }], + }, + { + text: "数据库一期", + collapsed: false, + items: [ + { text: "数据库系统概述", link: "/笔记/数据库01/01数据库系统概述" }, + { text: "关系数据库", link: "/笔记/数据库01/02关系数据库" }, + { text: "SQL(重点)", link: "/笔记/数据库01/03SQL(重点)" }, + { text: "数据库管理与维护(重点)", link: "/笔记/数据库01/05数据库管理与维护(重点)" }, + { text: "关系数据理论(重点)", link: "/笔记/数据库01/06关系数据理论(重点)" }, + { text: "数据库设计", link: "/笔记/数据库01/07数据库设计" }, + ], + }, + { + text: "JavaScript[待更新]", + collapsed: false, + items: [{ text: "JS常见手写面试题", link: "/笔记/JavaScript/01JS常见手写面试题" }], + }, + { + text: "CSS相关[待更新]", + collapsed: false, + items: [], + }, + { + text: "Vue相关", + collapsed: false, + items: [ + { text: "Vue3是如何运行的", link: "/笔记/Vue相关/01Vue3是如何运行的" }, + { text: "Vue3编译器", link: "/笔记/Vue相关/02Vue3编译器" }, + { text: "虚拟DOM", link: "/笔记/Vue相关/03虚拟DOM" }, + { text: "Vue3-Reactivity", link: "/笔记/Vue相关/04Vue3-Reactivity" }, + { text: "Mini-Vue", link: "/笔记/Vue相关/05Mini-Vue" }, + { text: "Vue3其他", link: "/笔记/Vue相关/06Vue3其他" }, + ], + }, + { + text: "NestJS", + collapsed: false, + items: [ + { text: "controller", link: "/笔记/NestJS/01controller" }, + { text: "service", link: "/笔记/NestJS/02service" }, + { text: "module", link: "/笔记/NestJS/03module" }, + { text: "DTO", link: "/笔记/NestJS/04DTO" }, + { text: "postgreSQL", link: "/笔记/NestJS/05postgreSQL" }, + { text: "原理细节", link: "/笔记/NestJS/06原理细节" }, + { text: "应用配置", link: "/笔记/NestJS/07应用配置" }, + { text: "更多模块", link: "/笔记/NestJS/08更多模块" }, + { text: "openAPI", link: "/笔记/NestJS/09openAPI" }, + { text: "测试", link: "/笔记/NestJS/10测试" }, + ], + }, + { + text: "前端八股文", + collapsed: false, + items: [ + { text: "HTML", link: "/笔记/前端八股文/01HTML" }, + { text: "CSS", link: "/笔记/前端八股文/02CSS" }, + { text: "JavaScript", link: "/笔记/前端八股文/03JavaScript" }, + { text: "Vue", link: "/笔记/前端八股文/04Vue" }, + { text: "计算机网络", link: "/笔记/前端八股文/05计算机网络" }, + { text: "浏览器原理", link: "/笔记/前端八股文/06浏览器原理" }, + { text: "性能优化", link: "/笔记/前端八股文/07性能优化" }, + ], + }, + { + text: "后端储备", + collapsed: false, + items: [ + { text: "Django进阶学习笔记", link: "/笔记/后端储备/01Django进阶学习笔记" }, + { text: "DRF学习笔记", link: "/笔记/后端储备/02DRF学习笔记" }, + { text: "Redis学习笔记", link: "/笔记/后端储备/03Redis学习笔记" }, + ], + }, + { + text: "Web3.0", + collapsed: false, + items: [ + { text: "Solidity8新特性", link: "/笔记/Web3.0/00Solidity8新特性" }, + { text: "Solidity8基本语法", link: "/笔记/Web3.0/01Solidity8基本语法" }, + { text: "Solidity8高级", link: "/笔记/Web3.0/02Solidity8高级" }, + { text: "Solidity8进阶", link: "/笔记/Web3.0/03Solidity8进阶" }, + ], + }, + { + text: "AI相关[待更新]", + collapsed: false, + items: [], + }, + ].map((item, i) => (!i ? item : { ...item, collapsed: true })), + } +} diff --git a/docs/.vitepress/theme/utils/mobile.ts b/docs/.vitepress/theme/utils/mobile.ts new file mode 100644 index 0000000..ff2ccfa --- /dev/null +++ b/docs/.vitepress/theme/utils/mobile.ts @@ -0,0 +1,9 @@ +import { inBrowser } from 'vitepress' + +// 判断当前页面是否为移动端 +export function isMobile() { + if (inBrowser) { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + } + return false; +} diff --git a/docs/.vitepress/utils/handleHeadMeta.ts b/docs/.vitepress/utils/handleHeadMeta.ts new file mode 100644 index 0000000..74843ab --- /dev/null +++ b/docs/.vitepress/utils/handleHeadMeta.ts @@ -0,0 +1,59 @@ +import { type HeadConfig, type TransformContext } from "vitepress"; + +// 处理每个页面的元数据 +export function handleHeadMeta(context: TransformContext) { + const { description, title, relativePath, frontmatter } = context.pageData; + + const curDesc = description || context.description; + const cover = frontmatter.cover || 'https://fav0.com/favicon-512x512.png' + const cardType = frontmatter.cover ? 'summary_large_image' : 'summary' + // 增加Twitter卡片 + const ogUrl: HeadConfig = ["meta", { property: "og:url", content: addBase(relativePath) }] + const ogTitle: HeadConfig = ["meta", { property: "og:title", content: title }] + const ogDescription: HeadConfig = ["meta", { property: "og:description", content: curDesc }] + const ogImage: HeadConfig = ["meta", { property: "og:image", content: cover }] + const twitterCard: HeadConfig = ["meta", { name: "twitter:card", content: cardType }] + const twitterImage: HeadConfig = ["meta", { name: "twitter:image:src", content: cover }] + const twitterDescription: HeadConfig = ["meta", { name: "twitter:description", content: curDesc }] + + const twitterHead: HeadConfig[] = [ + ogUrl, ogTitle, ogDescription, ogImage, + twitterCard, twitterDescription, twitterImage, + ] + + // 预加载字体 + const preloadHead: HeadConfig[] = handleFontsPreload(context) + + return [ ...twitterHead, ...preloadHead ] +} + +export function addBase(relativePath: string) { + const host = 'https://fav0.com' + if (relativePath.startsWith('/')) { + return host + relativePath + } else { + return host + '/' + relativePath + } +} + +export function handleFontsPreload({ assets }: TransformContext) { + // 只预加载正文字体,代码字体不预加载,因为可能不会使用或者很少使用 + const SourceHanSerifCN = assets.find(file => /SourceHanSerifCN-VF\.\w+\.woff2/) + + if (SourceHanSerifCN) { + return [ + [ + 'link', + { + rel: 'preload', + href: SourceHanSerifCN, + as: 'font', + type: 'font/woff2', + crossorigin: '' + } + ] + ] as HeadConfig[] + } + + return [] +} diff --git a/docs/.vitepress/utils/rss.ts b/docs/.vitepress/utils/rss.ts new file mode 100644 index 0000000..1f35845 --- /dev/null +++ b/docs/.vitepress/utils/rss.ts @@ -0,0 +1,96 @@ +import path from "node:path"; +import { writeFileSync } from "node:fs"; +import { Feed } from "feed"; +import { createContentLoader, type SiteConfig } from "vitepress"; + +const hostname = "https://fav0.com"; + +export async function createRssFileZH(config: SiteConfig) { + const feed = new Feed({ + title: "FAV0周刊", + description: "《FAV0周刊》:记录每周所见所闻,主要关注前端、AI领域以及计算机相关内容", + id: hostname, + link: hostname, + language: "zh-Hans", + image: "/favicon.png", + favicon: `/favicon.ico`, + copyright: "Copyright© 2024-present Justin3go", + }); + + const posts = await createContentLoader("posts/**/*.md", { + excerpt: true, + render: true, + }).load(); + + posts.sort((a, b) => Number(+new Date(b.frontmatter.date) - +new Date(a.frontmatter.date))); + + for (const { url, excerpt, html, frontmatter } of posts) { + // 仅保留最近5篇文章 + if (feed.items.length >= 5) { + break; + } + + feed.addItem({ + title: frontmatter.title, + id: `${hostname}${url}`, + link: `${hostname}${url}`, + description: excerpt, + content: html, + author: [ + { + name: "Justin3go", + email: "just@justin3go.com", + link: "https://justin3go.com", + }, + ], + date: frontmatter.date, + }); + } + + writeFileSync(path.join(config.outDir, "feed.xml"), feed.rss2(), "utf-8"); +} + +export async function createRssFileEN(config: SiteConfig) { + const feed = new Feed({ + title: "FAV0 Weekly", + description: "FAV0 Weekly: Documenting Weekly Observations and Experiences, with a Focus on Front-end Development, AI, and Computer-related Topics", + id: hostname, + link: hostname, + language: "en-US", + image: "/favicon.png", + favicon: `/favicon.ico`, + copyright: "Copyright© 2024-present Justin3go", + }); + + const posts = await createContentLoader("en/posts/**/*.md", { + excerpt: true, + render: true, + }).load(); + + posts.sort((a, b) => Number(+new Date(b.frontmatter.date) - +new Date(a.frontmatter.date))); + + for (const { url, excerpt, html, frontmatter } of posts) { + // 仅保留最近5篇文章 + if (feed.items.length >= 5) { + break; + } + + feed.addItem({ + title: frontmatter.title, + id: `${hostname}${url}`, + link: `${hostname}${url}`, + description: excerpt, + content: html, + author: [ + { + name: "Justin3go", + email: "just@justin3go.com", + link: "https://justin3go.com", + }, + ], + date: frontmatter.date, + }); + } + + writeFileSync(path.join(config.outDir, "feed-en.xml"), feed.rss2(), "utf-8"); +} diff --git a/docs/assets/fonts/FiraCode-VF.woff2 b/docs/assets/fonts/FiraCode-VF.woff2 new file mode 100644 index 0000000..e755a9d Binary files /dev/null and b/docs/assets/fonts/FiraCode-VF.woff2 differ diff --git a/docs/assets/fonts/SourceHanSerifCN-VF.woff2 b/docs/assets/fonts/SourceHanSerifCN-VF.woff2 new file mode 100644 index 0000000..87e20c4 Binary files /dev/null and b/docs/assets/fonts/SourceHanSerifCN-VF.woff2 differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..3c6bb04 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,15 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: doc +editLink: false +lastUpdated: false +isNoComment: true +isNoBackBtn: true +--- +# 最近发布 + + + + diff --git a/docs/public/bg.jpg b/docs/public/bg.jpg new file mode 100644 index 0000000..f32062d Binary files /dev/null and b/docs/public/bg.jpg differ diff --git a/docs/public/favicon-512x512.png b/docs/public/favicon-512x512.png new file mode 100644 index 0000000..f61ac08 Binary files /dev/null and b/docs/public/favicon-512x512.png differ diff --git a/docs/public/favicon.ico b/docs/public/favicon.ico new file mode 100644 index 0000000..2e4d09d Binary files /dev/null and b/docs/public/favicon.ico differ diff --git a/docs/public/favicon.png b/docs/public/favicon.png new file mode 100644 index 0000000..7c56603 Binary files /dev/null and b/docs/public/favicon.png differ diff --git "a/docs/\345\215\232\345\256\242/2020/06/01Java\350\277\267\345\256\253.md" "b/docs/\345\215\232\345\256\242/2020/06/01Java\350\277\267\345\256\253.md" new file mode 100644 index 0000000..a5dae8e --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2020/06/01Java\350\277\267\345\256\253.md" @@ -0,0 +1,531 @@ +# Java迷宫 + +最近这学期做了一个java迷宫的课程设计,这里代码及其算法逻辑就分享出来。 + +首先简单的说一下其中我使用的算法(自动生成地图:递归分割法、递归回溯法;寻找路径:深度优先、广度优先算法) + +递归分割法: + +地图外面一圈被墙围住,然后在空白区域生成十字墙壁,再随机选择三面墙,将其打通,这样就能保证迷宫的流动性,再分别对刚才分好的四个区域以同样的方式执行分割,一直递归下去,直到空间不足以分割就return。 + +![image-20221021150944828](https://oss.justin3go.com/blogs/image-20221021150944828.png) + +递归回溯法: + +**递归回溯法与深度优先算法在大致算法上其实差不多,具体只有一些细微的差别,都是通过判断当前点的是四个方向是否可以通过,当某个点堵住就向上退一步操作。递归回溯法具体算法如下:** + +**(****1****)初始化,建立一个所有单元格都被墙隔开的迷宫。** + +**(****2****)从起点开始,以此单元格开始打通墙壁。** + +**(****3****)以当前单元格为基准,随机选择一个方向,若此方向的邻接单元格没有被访问过,则打通这两个单元格之间的墙壁,并将此单元格作为当前单元格,重复步骤****3.** + +**(****4****)若当前单元格之间的四个邻接单元格都已经被访问过,则退回到进入当前单元格的邻接单元格,且以此单元格为当前单元格,重复步骤****3****、****4****。** + +**(****5****)直至起始点单元格被退回,则算法结束。** + +**深度优先算法和递归回溯差不太多,只是把邻接单元格变为的相邻的单元格,就直接是探寻周围是否有路可走,而不再是打通墙壁了。** + +广度优先:以步骤为主导,向四周扩散,比如第一步往四周走一格,第二步就四周的那几个单元格再往他们的四周走一格,一直下去,直到找到终点为止,这样返回的就是步骤数,同时因为这是遍历了整个地图,所以找到的一定是最短的路径。 + +![image-20221021151022288](https://oss.justin3go.com/blogs/image-20221021151022288.png) + +深度优先:以路径为主导,一直找下去,如果堵住了或者遇到已经访问过的,就返回上一格,随机另一条路继续下去,直到找到终点为止,这种方式找到的路并不是最短的,仅仅提供一条路径而已。 + +![image-20221021151038972](https://oss.justin3go.com/blogs/image-20221021151038972.png) + +**下面是递归分割法、递归回溯法以及文件加载地图实现的类map**://注意看注释,不然可能会看不懂,稍微有点乱 + +递归分割法:RandomMap1(),genMaze(),OpenADoor()//这三种方法实现,1加载的后面两种方法,2实现十字分割,3实现打开两点为一线之间的一堵墙。 + +递归回溯法:RandomMap2(),list(),digMaze()//这三种方法实现,1加载的后面两种方法,2连接两格单元格,即把中间的单元格变为通路,3实现如果往下没路可走就返回一个单元格进行继续找路。 + +文件加载地图:FileMap()方法 + +```java +package migong; + +import java.util.Random; +import java.util.Scanner; +import java.util.Stack; +import java.io.File; + + +public class Map{ + Random r = new Random(); + int l1,l2; + int x,y;//在回溯法中代表当前点 + boolean bool2 = true;//使用在getMaze()与list()方法中 + //判断是否执行了第二个if,如果都没执行,说明当前点的相邻点要么被访问过了,要么在边界之外,就需要退一步 + Map(int l1, int l2){ + this.l1 = l1; + this.l2 = l2; + } + Stack steps = new Stack<>(); + + public int[][] RandomMap2(int l1, int l2){//递归回溯法自动生成迷宫 + //规定0是墙,1是路,2是已经被探寻过的单元,也可以看做路 + int [][] map = new int[l1][l2]; + for(int i = 1;i < l1; i = i + 2) {//初始化迷宫生成所有单元都被墙隔开的迷宫 + for(int j = 1; j < l2;j = j + 2) { + map[i][j] = 1; + map[j][i] = 1; + } + } + map[1][1] = 2; + digMaze(1,1,map); + return map; + } + public boolean list(int x, int y, int[][] map) {//(x,y)代表当前单元格,初始单元格为起点 + this.x = x; + this.y = y; + int isOpen = r.nextInt(4);//0代表左边,逆时针旋转 + boolean bool1 = true; +//判断第一个if是否执行,如果四个都没执行,就递归在执行一次,因为有可能随机产生的数过大,把非边界路就已经给排除了 + + //分别判断相邻四个点(x,y-2)(x+2,y)(x,y+2)(x-2,y) + switch(isOpen) { + case 0:{ + if((this.y-2) > 0 && (this.y- 2) < l2 - 1) { + bool1 = false; + if(map[this.x][this.y-2] == 1) { + map[this.x][this.y-2] = 2;//表示这个点被访问了 + map[this.x][this.y-1] = 1;//打通墙壁 + this.y = this.y - 2;//改变当前点 + bool2 = false; + steps.push(0); + } + } + } + case 1:{ + if((this.x+2) > 0 && (this.x+2) < l1 -1) { + bool1 = false; + if(map[this.x+2][this.y] == 1) { + map[this.x+2][this.y] = 2; + map[this.x+1][this.y] = 1; + this.x = this.x + 2; + bool2 = false; + steps.push(1); + } + } + } + case 2:{ + if((this.y+2) > 0 && (this.y+2) < l2 - 1) { + bool1 = false; + if(map[this.x][this.y+2] == 1) { + map[this.x][this.y+2] = 2; + map[this.x][this.y+1] = 1; + this.y = this.y + 2; + bool2 = false; + steps.push(2); + } + } + } + case 3:{ + if((this.x-2) > 0 && (this.x-2) < l1 -1) { + bool1 = false; + if(map[this.x-2][this.y] == 1) { + map[this.x-2][this.y] = 2; + map[this.x-1][this.y] = 1; + this.x = this.x - 2; + bool2 = false; + steps.push(3); + } + } + } + default:{ + if(bool1) { + list(this.x,this.y,map); + } + } + } + return bool2; + } + public void digMaze(int x, int y, int[][] map) { + this.x = x; + this.y = y; + this.bool2 = true; + //不能将bool2定义在list方法中,因为递归调用它会让其变为true但后面switch并不会到第二层if中 + //从而这条注释下面的if就会判断失误 + + if(list(this.x,this.y,map)) { + try { + switch((int)steps.pop()) {//当当前点的下一点全都被访问了就执行退回操作 + case 0:{ + y = y + 2; + break; + } + case 1:{ + x = x -2; + break; + } + case 2:{ + y = y - 2; + break; + } + case 3:{ + x = x + 2; + } + default: + } + }catch(Exception ex) { + return; + } + } + +// if(x == l1 - 2 && y == l2 - 2){//判断是否到达终点(l1-2,l2-2) +// return; +// } +// if(map[l1-3][l2-2] == 1 && map[l1-2][l2-3] == 1) { +// return; +// } + if(steps.empty()) {//当起始点操作被退回是结束递归,这样生成的地图对比上面两种要更好些 + return; + } + digMaze(this.x,this.y,map); + } + + public int[][] RandomMap1(int l1, int l2){//递归分割法自动生成迷宫 + int [][] map = new int[l1][l2]; +//0代表墙,1代表路 + for(int i = 1; i < l1 - 1; i++) { + for(int j = 1; j < l2 - 1; j++) { + map[i][j] = 1; + } + } + + genMaze(1,1,l1,l2,map); + return map; + } + private void openAdoor(int x1, int y1, int x2, int y2, int[][] map) { + //以传参的两点为直线,打开这条线的某一点,分割的点存在于x1~(x2-1)或y1~(y2-1) + int pos;//打开的那一点 + + if(x1 == x2) { + pos = y1 + r.nextInt((int)((y2 - y1)/2 + 1))*2;//在奇数行开门 + map[x1][pos] = 1; + } + else if(y1 == y2) { + pos = x1 + r.nextInt((int)((x2 - x1)/2 + 1))*2;//在奇数列开门 + map[pos][y1] = 1; + } + else { + System.out.println("错误"); + } + } + //x,y代表要分割区域的左上点坐标,l1代表的行数,l2代表的列数 + public void genMaze(int x, int y, int l1, int l2, int[][] map) { + int Xpos, Ypos; + + if(l1 <= 3 || l2 <= 3) + return; + + //Xpos,Ypos只能取(x或y,l - 1)之间的偶数,这里是开区间 + //横着画线,在偶数位置画线, + Xpos = x + r.nextInt((int)(l1/2) - 1)*2 + 1;//Xpos,Ypos相当于两条分割线交叉点的坐标 + for(int i = y; i < y + l2 - 2;i++) { + map[Xpos][i] = 0; + } + //竖着画一条线,在偶数位置画线 + Ypos = y + r.nextInt((int)(l2/2) - 1)*2 + 1; + for(int i = x; i < x + l1 - 2;i++) { + map[i][Ypos] = 0; + } + + //随机开三扇门,左侧墙壁为1,逆时针旋转 + int isClosed = r.nextInt(4) + 1; + switch (isClosed) + { + case 1://1开234门,依次下去 + openAdoor(Xpos + 1, Ypos, x + l1 - 2, Ypos, map);// 2 + openAdoor(Xpos, Ypos + 1, Xpos, y + l2 - 2, map);// 3 + openAdoor(x, Ypos, Xpos, Ypos, map);// 4 + break; + case 2: + openAdoor(Xpos, Ypos + 1, Xpos, y + l2 - 2, map);// 3 + openAdoor(x, Ypos, Xpos, Ypos, map);// 4 + openAdoor(Xpos, y, Xpos, Ypos, map);// 1 + break; + case 3: + openAdoor(x, Ypos, Xpos, Ypos, map);// 4 + openAdoor(Xpos, y, Xpos, Ypos, map);// 1 + openAdoor(Xpos + 1, Ypos, x + l1 - 2, Ypos, map);// 2 + break; + case 4: + openAdoor(Xpos, y, Xpos, Ypos, map);// 1 + openAdoor(Xpos + 1, Ypos, x + l1 - 2, Ypos, map);// 2 + openAdoor(Xpos, Ypos + 1, Xpos, y + l2 - 2, map);// 3 + break; + default: + break; + } + //左上角 + genMaze(x, y, Xpos + 2 - x, Ypos + 2 - y, map); + //右上角 + genMaze(x, Ypos + 1, Xpos + 2 - x, l2 - Ypos, map); + //左下角 + genMaze(Xpos + 1, y, l1 - Xpos, Ypos + 2 - y, map); + //右下角 + genMaze(Xpos + 1, Ypos + 1, l1 - Xpos , l2 - Ypos, map); + } + + public static int[][] FileMap(String filename) throws Exception{//手动生成迷宫的方法 + //读取没有空格的数字方阵 + File file = new File(filename); + if(!file.exists()) { + System.out.println("文件不存在"); + } + Scanner input = new Scanner(file); + int l1 = 0, l2 = 0;//l1代表行数,l2代表列数 + String[] str = new String[1024]; + while(input.hasNext()) { + str[l1++] = input.nextLine();//获取行数同时把每一行分别赋给str数组的各个元素 + l2 = str[0].length(); + } + int [][]map = new int[l1][l2]; + for(int i = 0;i < l1;i++) { + for(int j = 0; j < l2;j++) { + map[i][j] = str[i].charAt(j) - '0';//通过两个Ascll码之差获得其数值 +// map[i][j] = Integer.parseInt(str[i].charAt(j) + ""); + } + } + input.close(); + return map; + } + + public void show(int[][] map,int l1,int l2) { + for(int i = 0; i < l1; i++) { + for(int j = 0; j < l2; j++) { + System.out.print(map[i][j] + " "); + } + System.out.println("\n"); + } + } + + public static void main(String[] args) throws Exception{ +// String filename = "C:\\Users\\21974\\Desktop\\map.txt"; +// for(int i = 0; i < 2; i++) { +// for(int j = 0; j < 4; j++) { +// System.out.print(Map.FileMap(filename)[i][j] + " "); +// } +// System.out.println("\n"); +// } + + int l1 = 15,l2 = 15;//奇数 + Map m = new Map(l1, l2); + m.show(m.RandomMap1(l1, l2),l1,l2); + } +} +``` + +![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) + +下面是深度优先与广度优先的类findpath: + + +```java +package migong; + +import java.util.LinkedList; +import java.util.Stack; + +public class findPath { + public LinkedList steps1 = new LinkedList<>(); + public Stack steps2 = new Stack<>(); + int x,y; + public boolean bool = true; + //判断是否执行了第二个if,如果都没执行,说明当前点的相邻点是墙,要么被访问过了,要么在边界之外,就需要退一步 + + public String shortestPath(int[][] map,int l1, int l2){//最优路径 + //创建一个方向数组,方向的优先级为 "下左上右" + Direction[] di = new Direction[] {new Direction(1,0),new Direction(0,-1),new Direction(-1,0),new Direction(0,1)}; + + //创建一个字符数组,其中DLUR分别表示向下、向上、向左、向右走。 + StringBuffer[] step = new StringBuffer[] {new StringBuffer("D"),new StringBuffer("L"),new StringBuffer("U"),new StringBuffer("R")}; + + //创建一个标识符,判断迷宫是否有解 + boolean b = false; + + int x=1,y=1,stepNumber=0; + String startStep = "";//代表空,没有操作 + GPS temp = new GPS(x,y,stepNumber,startStep); //将起始点的信息加入队列 + map[x][y] = 2; //将当前位置标记为已经走过 + steps1.addLast(temp); + + Loop:while(!steps1.isEmpty()) { + + temp = steps1.poll() ; //弹出队头元素进行扩展 + + for(int i=0;i<4;i++) { //按照优先级"下左上右",依次进行扩展 + int row = temp.x + di[i].inc_x; + int col = temp.y + di[i].inc_y; + StringBuffer ts = step[i]; //当前方向的字母表示//当前方向的字母表示 + + if(map[row][col] == 1) { + int tempStepNumber = temp.stepNumber+1; + String tempStepPath = temp.stb + ts; + steps1.addLast(new GPS(row,col,tempStepNumber,tempStepPath)); //符合条件的坐标加入队列 + + map[row][col] = 2; //将该结点的值设为2,扩展该结点 + + if(row == l1-2 && col == l2-2) { //判断是否到达了终点 + b = true; + break Loop; //跳出标记所在的循环 + } + } + } + } + if(b) { + return steps1.getLast().stb; + }else {return "无解";} + } + public void sMove(int x, int y, int[][] map) { + + } + + public Stack path(int x, int y, int[][] map){//深度优先自动寻路 + map[1][1] = 3; + searchMaze(x,y,map); + return this.steps2; + } + public boolean move(int x, int y,int[][] map){ + //分别判断相邻四个点(x,y-1)(x+1,y)(x,y+1)(x-1,y) + switch(0) {//0代表左,逆时针 + case 0:{ + if((this.y-1) > 0 && (this.y- 1) < map[0].length - 1) { + if(map[this.x][this.y-1] == 1 || map[this.x][this.y-1] == 2) { +//0代表墙,1代表路,2代表生成迷宫时被访问了的路,在这里也相当于路,3代表这里找路时被访问了的路 + map[this.x][this.y-1] = 3;//标明改点已经走过了 + this.y = this.y - 1;//改变当前点 + bool = false; + steps2.push(0); + break; + } + } + } + case 1:{ + if((this.x+1) > 0 && (this.x+1) < map.length -1) { + if(map[this.x+1][this.y] == 1 || map[this.x+1][this.y] == 2) { + map[this.x+1][this.y] = 3; + this.x = this.x + 1; + bool = false; + steps2.push(1); + break; + } + } + } + case 2:{ + if((this.y+1) > 0 && (this.y+1) < map[0].length - 1) { + if(map[this.x][this.y+1] == 1 || map[this.x][this.y+1] == 2) { + map[this.x][this.y+1] = 3; + this.y = this.y + 1; + bool = false; + steps2.push(2); + break; + } + } + } + case 3:{ + if((this.x-1) > 0 && (this.x-1) < map.length - 1) { + if(map[this.x-1][this.y] == 1 || map[this.x-1][this.y] == 2) { + map[this.x-1][this.y] = 3; + this.x = this.x - 1; + bool = false; + steps2.push(3); + break; + } + } + } + default: + } + return bool; + } + public void searchMaze(int x, int y, int[][] map) {//这里是空返回,以后要调用栈直接用类名加数据名 + this.x = x; + this.y = y; + this.bool = true; + if(move(this.x,this.y,map)) { + try { + switch((int)steps2.pop()) {//当当前点的下一点全都被访问了就执行退回操作 + case 0:{ + this.y = y + 1; + break; + } + case 1:{ + this.x = x - 1; + break; + } + case 2:{ + this.y = y - 1; + break; + } + case 3:{ + this.x = x + 1; + } + default: + } + }catch(Exception ex) { + return; + } + } + + if(map[map.length - 2][map[0].length - 2] == 3){//判断是否到达终点(l1-2,l2-2) + return; + } + searchMaze(this.x,this.y,map); + } + + public void show(Stack stack) { + while(!stack.empty()) { + System.out.println((int)stack.pop()); + } + } + public static void main(String[]args) { + int l1 = 5,l2 = 5; + + Map m = new Map(l1,l2); + findPath find = new findPath(); + + int[][] map = m.RandomMap1(l1, l2); +// String s = find.path(l1,l2,map); +// System.out.println(s); +// System.out.println("地图为"); +// m.show(map, l1, l2); + find.path(1,1,map); + System.out.println("路为"); + m.show(map, l1, l2); + find.show(find.steps2); + } +} +class Direction{ + int inc_x; //x方向的增量 + int inc_y; //y方向的增量 + + public Direction(int inc_x,int inc_y) { + this.inc_x = inc_x; + this.inc_y = inc_y; + } +} + +/* +GPS类,成员变量x,y表示坐标,stepNumber表示步数 +*/ +class GPS{ + int x; + int y; + int stepNumber; + String stb; //用来记录路径 + + public GPS(int x,int y,int stepNumber,String stb){ + this.x = x; + this.y = y; + this.stepNumber = stepNumber; + this.stb = stb; + } +} +``` + +![点击并拖拽以移动](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) + +能看到这里说明我的文章对你有所帮助,支持一下呗,第一次写博客有些还不够规范。 + diff --git "a/docs/\345\215\232\345\256\242/2020/06/02\344\275\277\347\224\250anaconda\344\270\255\347\232\204Prompt\351\205\215\347\275\256\350\231\232\346\213\237\347\216\257\345\242\203\347\232\204\345\270\270\347\224\250\345\221\275\344\273\244.md" "b/docs/\345\215\232\345\256\242/2020/06/02\344\275\277\347\224\250anaconda\344\270\255\347\232\204Prompt\351\205\215\347\275\256\350\231\232\346\213\237\347\216\257\345\242\203\347\232\204\345\270\270\347\224\250\345\221\275\344\273\244.md" new file mode 100644 index 0000000..e828fef --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2020/06/02\344\275\277\347\224\250anaconda\344\270\255\347\232\204Prompt\351\205\215\347\275\256\350\231\232\346\213\237\347\216\257\345\242\203\347\232\204\345\270\270\347\224\250\345\221\275\344\273\244.md" @@ -0,0 +1,77 @@ +# 使用anaconda中的Prompt配置虚拟环境的常用命令 + +因为自己目前也记不到这么多命令,每次去配环境的时候都是问度娘复制粘贴,所以就总结了一下常用的conda命令,方便用的时候直接复制; + +参考链接:1.[Windows在命令行中使用conda命令创建删除虚拟环境_我才不会害羞的博客-CSDN博客_conda环境管理](https://blog.csdn.net/qq_45855805/article/details/102979213) + +​ \2. [conda(anaconda)删除清华源,改回原源_甜甜圈Sweet Donut的博客-CSDN博客_conda 删除清华源](https://blog.csdn.net/qinglingls/article/details/89363368) + +​ 3.[conda查看及添加镜像源_血雨腥风霜的博客-CSDN博客_查看conda源](https://blog.csdn.net/weixin_41466947/article/details/107377071) + +​ 4.[conda源,添加删除 - 程序员大本营](https://www.pianshen.com/article/30971024940/) + +​ (应该还有一些,不过找不到了) + +## 1.每次都是速度限制,所以第一个就是:换源常用命令: + + 查看源:**conda config --show-sources** + + 换回默认源:conda config --remove-key channels + + 国内的一些镜像源(有时候有些源会报错或者没有一些东西,需要换一下,相互补充): + +​ 阿里云 http://mirrors.aliyun.com/pypi/simple/ + +​ 中国科技大学 [Simple Index](https://pypi.mirrors.ustc.edu.cn/simple/) + +​ 豆瓣(douban) [Simple Index](http://pypi.douban.com/simple/) + +​ 清华大学 [Simple Index](https://pypi.tuna.tsinghua.edu.cn/simple/) + +​ 中国科学技术大学 [Simple Index](http://pypi.mirrors.ustc.edu.cn/simple/) + + 这是添加清华源的方法(其他的类推): + +​ conda config --add channels [Index of /anaconda/pkgs/main/ | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror](https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/) + +​ conda config --add channels [Index of /anaconda/pkgs/free/ | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror](https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/) + +​ conda config --add channels [Error](https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/) + +​ conda config --set show_channel_urls yes + + 注:pip临时使用这些源的方法: + + **临时使用(也可以永久使用,这里就不展开了,偏题了,可自行百度):** + + 可以在使用pip的时候在后面加上-i参数,指定pip源 + + eg: pip install scrapy -i [Simple Index](https://pypi.tuna.tsinghua.edu.cn/simple) + + 删除某个源: + + conda config --remove channels ‘[main](https://repo.continuum.io/pkgs/main/)‘(删除有引号) + + 如果遇到无法删除可以尝试先执行 + + conda config --set show_channel_urls yes + + 再执行 + + conda config --remove channels ’[main](https://repo.continuum.io/pkgs/main/)‘ + +## 2.创建虚拟环境: + + 创建虚拟环境:eg:创建一个名为python37,Python版本为3.7的conda虚拟环境:conda create -n python37 python=3.7(之后会出现选择安装一些基础包的情况,输入y就可以了) + + 激活虚拟环境: conda activate (你的环境名) 这样你安装的那些东西才是安装在你所建的环境中; + + 然后你就可以尽情地使用conda或pip安装你想要的包了:conda install …… pip install …… pip install "本地下载好的包的路径whl文件" + + 搜索想要安装包的版本: conda search pytorch + + 查看你的虚拟环境:conda env list + + 在当前激活的环境中查看所安装的包:conda list + + \ No newline at end of file diff --git "a/docs/\345\215\232\345\256\242/2021/04/01scrapy\347\210\254\350\231\253\350\257\246\350\247\243.md" "b/docs/\345\215\232\345\256\242/2021/04/01scrapy\347\210\254\350\231\253\350\257\246\350\247\243.md" new file mode 100644 index 0000000..9a2b0de --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2021/04/01scrapy\347\210\254\350\231\253\350\257\246\350\247\243.md" @@ -0,0 +1,63 @@ +# scrapy爬虫详解 + +## 爬虫的原理 + +**废话**:这个月前几天才开学,事比较少,所以就自学了一下scrapy来爬取B站文章以及标题,后面感觉数据不够,就又爬了每篇文章的点赞数以及评论观看投币数等等来做NLP中的训练; + +我们先简单讲一下爬虫的原理,但不细讲,知道就行,因为后面是直接用框架,然后你遇到问题再去查印象会更深; +### 整个程序运行的过程 + + 1. 表明自己的身份,我是谁(headers),我在哪(ip地址),我要干什么,当然是爬数据了~ + 2. 有了这些信息,下一步当然就是干最重要的事了,请求数据(request)从服务器中获取数据并返回(response),你问服务器为什么要给你数据,因为你是用户啊,服务器就是给用户服务的,我们之前是伪装了用户的身份信息的,但如果你是python的身份信息,那它当然不会给你数据(解决反爬机制) + 3. 返回的信息是一大堆数据,这些东西可以通过浏览器的渲染排好版地展示再用户地面前,但是我们当然不可能每次都通过浏览器来人工地处理数据,所以需要使用一种规则来获取我们想要的数据(这里地数据每次地格式是差不多的,如果有差别需要重新设计规则)比如:正则表达式、xpath、css选择器等等,当然,这些都是针对静态数据,动态数据需要我们去浏览器手动分析,这里大概了解就行,后面会继续讲解; + 4. 最后就是忙活了这么久,收获的时候了(保存数据),选择合理的保存方法对后面处理这些数据是很关键的,但其实影响也不会太大,大多数也都可以后面相互转换,常见的方法就是txt、csv、json等格式,或者直接保存到数据库里面; + +**注:** *这里涉及到很多专有词,如果你才接触爬虫的话,对于这些是很陌生的,请不要被吓着,这些词汇百度一下一两句话你就懂了,而且后面也会在例子中尽量讲到* + +### 介绍scrapy的运行过程 +*如果你在上面对爬虫爬取的过程还有点模糊的话,那么这里就能加深你的理解了,毕竟scrapy框架也是爬虫,原理是一致的,只是功能多了不少,在这么多功能的前提下,我们仅仅只需要遵守它的编程规则就可以了* +**话不多说,看图:** +![image-20221021153529204](https://oss.justin3go.com/blogs/image-20221021153529204.png) + +#### 先介绍它的5+2结构的组件 + + 1. ENGINE:这个可以说是最重要但也最不重要的组件了,一方面是因为所有其他的组件都是通过这个引擎来进行活动的,另一方面是这个我们是不用对其进行任何操作的,真好~ + 2. SCHEDULER:爬虫的时间调度器,因为scrapy是自带多线程异步爬取的,你问我多线程是什么,一个字“快” + 3. DOWNLOADER:下载器,去服务器获取数据,相当于整个框架的前线士兵; + 4. ITEM PIPELINES:前面不是说了最后一步是保存数据码,这个地方就是scrapy框架给我们来保存数据的地方; + 5. SPIDER:设置爬虫的地方 + 6. MIDDLEWARE:中间件(自带有两个中间件,也可以自己创建中间件,需要遵循规则,这篇文章不会细讲)本来上面五个结构就能满足我们爬取数据的需求,但是由于一些反爬机制或者其他的原因,比如url本来就是没数据的等等,而导致我们的程序被终止,这显然是不具有鲁棒性的,不够灵活,所以scrapy就增加了这两个中间键让用户自定义处理一些异常等 + 7. 6包含两个组件,从图中我们可以看出这两个组件只是处理的地方不同而已,一个是在形成爬虫那里,一个是在下载数据那里 + ### 过程 + 其实图中已经很详细了,不过咋们是保姆级教程,所以再来解释一番: + 1-4:爬虫通过引擎调动时间表去网页下载内容,里面是通过requests完成的; + 5-6:返回内容并交给爬虫里面的parse方法进行解析获取自己想要的内容; + 7-8:保存内容,item是你要保存内容的对象(就是学生:姓名、学号这些) + pipline就是保存你设计的item对象为某种格式; + + **挺简单的,后面看了代码你的整体逻辑会更加清晰** + ## 实战开始~ + 如果你真的是小白小白,安装anaconda3 -> 创建环境(conda create -n <环境名> python=3.7)-> pip install scrapy -i https://pypi.tuna.tsinghua.edu.cn/simple + 好吧,现在开始实操-> + + **激活你的环境**(我的爬虫环境名为EasyTitle,这里我是在VSCODE里面使用的终端,你也可以在任何编译器里面这样做,anaconda自带很多编译器,也可以使用anaconda_promapt,同时你也要选择之后项目所在的位置,就是路径: cd <路径名>): +![image-20221021153603035](https://oss.justin3go.com/blogs/image-20221021153603035.png)) +**创建项目** 在终端输入scrapy startproject bili , 其中bili是我的项目名 +![image-20221021153619681](https://oss.justin3go.com/blogs/image-20221021153619681.png) +然后,你的该目录下就会出现一个bili的文件夹,这就是scrapy工作的地方,cd bili 进入这个文件夹(这步就不展示图了);该文件夹如下 +![image-20221021153634207](https://oss.justin3go.com/blogs/image-20221021153634207.png) +接下来就是重点了,打好精神~ +bili项目下分两个文件< bili >< scrapy.cfg >,bili里面有spider、items、middlewares、piplines、setting这些文件或文件夹(不提init) + +1. spider文件夹里面是装着你的爬虫,后面生成的爬虫都会存放在该目录 +2. items容器设计你的保存对象 +3. pipline保存内容到文件 +4. miiddleware自定义处理可能会出现的一些异常情况 +5. settings一些设置,比如启动关闭middleware,设置每次爬取的延时时间等等 + 基于上面的理解,我们我们来一步步分析: + 首先生成我们的一只爬虫: + +--- + +(后续更新)算了,懒得写了,直接看源码吧:https://github.com/Justin3go/Bili_Spider-bili_dataset- + diff --git "a/docs/\345\215\232\345\256\242/2021/04/02TFIDF\350\256\241\347\256\227\347\232\204\345\255\246\344\271\240.md" "b/docs/\345\215\232\345\256\242/2021/04/02TFIDF\350\256\241\347\256\227\347\232\204\345\255\246\344\271\240.md" new file mode 100644 index 0000000..286b6fb --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2021/04/02TFIDF\350\256\241\347\256\227\347\232\204\345\255\246\344\271\240.md" @@ -0,0 +1,402 @@ +# TFIDF计算的学习 + +## 转码 + +### 定义转码函数 + + +```python +# ! pip install codecs +# ! pip install chardet + +import codecs +import chardet + +def convert(filename, out_enc="UTF-8"): + content = codecs.open(filename, 'rb').read() + source_encoding = chardet.detect(content)['encoding'] + content = content.decode(source_encoding).encode(out_enc) + codecs.open(filename, 'wb').write(content) + +# 获取编码 +def get_encoding(file): + with open(file,'rb') as f: + return chardet.detect(f.read())['encoding'] + +``` + + ERROR: Could not find a version that satisfies the requirement codecs + ERROR: No matching distribution found for codecs + + + Requirement already satisfied: chardet in c:\users\justin3go\appdata\roaming\python\python38\site-packages (3.0.4) + + +### 读入文件并转码 + + +```python +import chardet +import codecs +import os + +# 读取文件 +file_list = [] +for root, _, files in os.walk("./实验六所用语料库"): + for file in files: + # print(os.path.join(root, file)) + file_list.append(os.path.join(root, file)) + +for file in file_list: + convert(file) + +get_encoding(file_list[0]) +``` + + + 'ascii' + +## 生成词典 + + +```python +import re +import pandas as pd +import numpy as np + +# 分词建立词典,得到词频 +dict_words = {} +files = [] +files_ = [] +for file in file_list: + with open(file, 'r', encoding='ascii') as f: + text = f.read().lower() + files_.append(text) + + text_ = re.findall('[a-z]+', text) + files.append(text_) + + for t in text_: + dict_words[t] = dict_words.get(t, 0) + 1 +``` + +## 生成TF矩阵 + + +```python +import numpy as np +words2index = {w: i for i,w in enumerate(dict_words)} +index2words = {i: w for i,w in enumerate(dict_words)} +zeros_m = np.zeros((len(files),len(words2index))) +for i, f in enumerate(files): + for t in f: + # print(t) + # print(words.index(f)) + zeros_m[i][words2index[t]] += 1 + +# tf在个文档中的矩阵 +zeros_m +``` + + + array([[1., 5., 5., ..., 0., 0., 0.], + [0., 0., 0., ..., 0., 0., 0.], + [0., 1., 0., ..., 0., 0., 0.], + ..., + [0., 4., 0., ..., 0., 0., 0.], + [1., 5., 0., ..., 0., 0., 0.], + [0., 1., 0., ..., 1., 1., 1.]]) + +## 逐步计算IDF值 + + +```python +df1 = pd.DataFrame(dict_words,index=['TF']).T +df1.head() +``` + + + .dataframe tbody tr th { + vertical-align: top; + } + + .dataframe thead th { + text-align: right; + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TF
call2
for20
presentations5
navy9
scientific6
+ +```python +# print(dict_words) +dict_words_idf = {} +for key in dict_words: + count = 0 + # files要上面那个单元运行之后存入内存才有 + for text_ in files: + if key in text_: + count += 1 + dict_words_idf[key] = count + +df2 = pd.DataFrame(dict_words_idf,index=['DF']).T +df = pd.concat([df1,df2], axis=1) +df.head(10) +``` + + + .dataframe tbody tr th { + vertical-align: top; + } + + .dataframe thead th { + text-align: right; + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TFDF
call22
for208
presentations51
navy91
scientific62
visualization94
and509
virtual51
reality51
seminar51
+ +```python +import math +# log(len(files)/df,2) + +df['IDF'] = df['DF'].apply(lambda x: math.log(len(files)/x,2)) +df.head(10) +``` + + + .dataframe tbody tr th { + vertical-align: top; + } + + .dataframe thead th { + text-align: right; + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TFDFIDF
call222.321928
for2080.321928
presentations513.321928
navy913.321928
scientific622.321928
visualization941.321928
and5090.152003
virtual513.321928
reality513.321928
seminar513.321928
+## 计算TFIDF值 + + +```python +idf = list(df['IDF']) +result = zeros_m*idf +result +``` + + + array([[ 2.32192809, 1.60964047, 16.60964047, ..., 0. , + 0. , 0. ], + [ 0. , 0. , 0. , ..., 0. , + 0. , 0. ], + [ 0. , 0.32192809, 0. , ..., 0. , + 0. , 0. ], + ..., + [ 0. , 1.28771238, 0. , ..., 0. , + 0. , 0. ], + [ 2.32192809, 1.60964047, 0. , ..., 0. , + 0. , 0. ], + [ 0. , 0.32192809, 0. , ..., 3.32192809, + 3.32192809, 3.32192809]]) + +## 使用SKlearn计算TFIDF值 + + +```python +from sklearn.feature_extraction.text import TfidfTransformer +from sklearn.feature_extraction.text import CountVectorizer + +vectorizer = CountVectorizer() +transformer = TfidfTransformer() + +tfidf = transformer.fit_transform(vectorizer.fit_transform(files_)) +word = vectorizer.get_feature_names() +print(word[40:50]) +weight = tfidf.toarray().T +print(weight) + +``` + + ['accepted', 'accessible', 'across', 'add', 'address', 'addresses', 'adresses', 'advance', 'advises', 'affiliated'] + [[0. 0.11537929 0. ... 0. 0. 0. ] + [0.03906779 0. 0. ... 0. 0. 0. ] + [0. 0. 0. ... 0. 0. 0. ] + ... + [0. 0. 0. ... 0. 0. 0. ] + [0. 0. 0.15731715 ... 0.04130626 0.09597341 0.05024117] + [0. 0. 0. ... 0. 0.11918574 0. ]] + + +## 计算余弦相似度 + + +```python +from sklearn.metrics.pairwise import cosine_similarity + +test = weight[0] # 假设其他的一篇文档就是第一篇文档 +cos_sim = [] +for i in range(len(weight)): + cos_sim.append(cosine_similarity([list(test),list(weight[i])])) + +print(cos_sim[0]) #第一行的值是a1中的第一个行向量与a2中所有的行向量之间的余弦相似度 +print(cos_sim[5]) +``` + + [[1. 1.] + [1. 1.]] + [[1. 0.] + [0. 1.]] + diff --git "a/docs/\345\215\232\345\256\242/2021/08/03\346\223\215\344\275\234\347\263\273\347\273\237\345\206\205\345\255\230\345\210\206\351\205\215\346\250\241\346\213\237\347\250\213\345\272\217.md" "b/docs/\345\215\232\345\256\242/2021/08/03\346\223\215\344\275\234\347\263\273\347\273\237\345\206\205\345\255\230\345\210\206\351\205\215\346\250\241\346\213\237\347\250\213\345\272\217.md" new file mode 100644 index 0000000..78bd910 --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2021/08/03\346\223\215\344\275\234\347\263\273\347\273\237\345\206\205\345\255\230\345\210\206\351\205\215\346\250\241\346\213\237\347\250\213\345\272\217.md" @@ -0,0 +1,603 @@ +# 操作系统内存分配模拟程序 + +## 通用配置 + + +```python +import pandas as pd + +MAX_MEMORY = 2**15 # 32kb +freeTable = pd.DataFrame(columns=['start', 'size', 'status']) +busyTable = pd.DataFrame(columns=['start', 'size', 'status']) +schedule = [] # 假设等待中的进程按照FIFO的调度策略进行调度,这里没有记录进程ID + +freeTable = freeTable.append([{'start': 0, 'size': MAX_MEMORY, 'status': 0}]) +test = [[10*2**10, 3], [2*2**10, 5], [1*2**10, 2], [12*2**10, 4], [1*2**10, 3], [25*2**10, 2], [2*2**10, 1]] + +# 回收 +# -->运行一个时间步[status--,减到0表示运行完成,空闲] +def step(): + global freeTable, busyTable, schedule + # 批量减1 + busyTable['status'] -= 1 + # 找出所有运行完[为0]的进程 + finishTable = busyTable.loc[busyTable['status'] == 0] + # 从已分配表中删除 + busyTable = busyTable.drop(busyTable[busyTable['status'] == 0].index) + # 边添加边回收 + for index, row in finishTable.iterrows(): # 开始遍历 + print('[%s %s]开始处理' % (row['start'], row['size']), '*'*50) + # 根据起始地址进行升序排序 + freeTable = freeTable.sort_values('start', ascending=True) + # 找到上下相邻-->假设小的在上,这个无所谓 + upOne = freeTable.loc[freeTable['start']+freeTable['size'] == row['start']] + downOne = freeTable.loc[row['start']+row['size'] == freeTable['start']] + # 合并 + IsMerge = 0 + if len(upOne) != 0: + # 直接将上面的size变大 + print('上相邻合并......') + mergeIndex1 = (freeTable[freeTable['start'] == int(upOne['start'])]).index[0] + freeTable.loc[mergeIndex1, 'size'] += row['size'] + IsMerge = 1 + if len(downOne) != 0: + # 修改起始地址,并变大 + print('下相邻合并......') + mergeIndex2 = (freeTable[freeTable['start'] == int(downOne['start'])]).index[0] + freeTable.loc[mergeIndex2, 'start'] = row['start'] + freeTable.loc[mergeIndex2, 'size'] += row['size'] + IsMerge = 1 + # 如何没有合并[不相邻]就直接加入 + if not IsMerge: + print('无上下相邻......') + freeTable = freeTable.append([{'start': row['start'], 'size': row['size'], 'status': row['status']}]) + # 重新编号 + busyTable.reset_index(drop=True, inplace=True) + freeTable.reset_index(drop=True, inplace=True) + +``` + +## 首次适应算法 + + +```python +# 首次适应算法-->添加一个进程 +def FirstAdapt(size, status): + global freeTable, busyTable, schedule + # 传入所需大小以及时间 + freeTable = freeTable.sort_values('start', ascending=True) # 根据起始地址进行升序排序 + IsAssigned = 0 # 标志位-->是否已分配 + for index, row in freeTable.iterrows(): # 开始遍历 + # 判断是否有剩余空间可以分配 + if row['size'] >= size: + # 记录在已分配表当中 + busyTable = busyTable.append( + [{'start': row['start'], 'size':size, 'status':status}]) + # 更新空闲分区表 + row['start'] = row['start'] + size + row['size'] = row['size'] - size + IsAssigned = 1 + print("[ %s, %s ]---已经分配成功......." % (size, status)) + break + # 重新编号 + busyTable.reset_index(drop=True, inplace=True) + + if not IsAssigned: + print("[ %s, %s ]---没有空闲空间,等待中......." % (size, status)) + schedule.append([size, status]) +``` + +## 循环首次适应 +和上面相比就添加一个变量记录每次的起始位置 + + +```python +startAddress = 0 +# 循环首次-->添加一个进程 +def LoopFirstAdapt(size, status): + global freeTable, busyTable, schedule, startAddress + # 传入所需大小以及时间 + freeTable = freeTable.sort_values('start', ascending=True) # 根据起始地址进行升序排序 + IsAssigned = 0 # 标志位-->是否已分配 + for index, row in freeTable.iterrows(): # 开始遍历 + # 判断是否有剩余空间可以分配 + if row['size'] >= size: + # 记录在已分配表当中 + busyTable = busyTable.append( + [{'start': row['start'], 'size':size, 'status':status}]) + # 更新空闲分区表 + row['start'] = row['start'] + size + row['size'] = row['size'] - size + IsAssigned = 1 + print("[ %s, %s ]---已经分配成功......." % (size, status)) + break + # 重新编号 + busyTable.reset_index(drop=True, inplace=True) + + if not IsAssigned: + print("[ %s, %s ]---没有空闲空间,等待中......." % (size, status)) + schedule.append([size, status]) +``` + +## 最佳动态分区分配 +以size进行升序排序,其余不变 + + +```python +# 首次适应算法-->添加一个进程 +def FirstAdapt(size, status): + global freeTable, busyTable, schedule + # 传入所需大小以及时间 + freeTable = freeTable.sort_values('size', ascending=True) # 根据大小进行升序排序 + IsAssigned = 0 # 标志位-->是否已分配 + for index, row in freeTable.iterrows(): # 开始遍历 + # 判断是否有剩余空间可以分配 + if row['size'] >= size: + # 记录在已分配表当中 + busyTable = busyTable.append( + [{'start': row['start'], 'size':size, 'status':status}]) + # 更新空闲分区表 + row['start'] = row['start'] + size + row['size'] = row['size'] - size + IsAssigned = 1 + print("[ %s, %s ]---已经分配成功......." % (size, status)) + break + # 重新编号 + busyTable.reset_index(drop=True, inplace=True) + + if not IsAssigned: + print("[ %s, %s ]---没有空闲空间,等待中......." % (size, status)) + schedule.append([size, status]) +``` + +## 最差动态分区分配 +以size进行降序排序,其余不变 + + +```python +# 首次适应算法-->添加一个进程 +def FirstAdapt(size, status): + global freeTable, busyTable, schedule + # 传入所需大小以及时间 + freeTable = freeTable.sort_values('size', ascending=False) # 根据大小进行降序排序 + IsAssigned = 0 # 标志位-->是否已分配 + for index, row in freeTable.iterrows(): # 开始遍历 + # 判断是否有剩余空间可以分配 + if row['size'] >= size: + # 记录在已分配表当中 + busyTable = busyTable.append( + [{'start': row['start'], 'size':size, 'status':status}]) + # 更新空闲分区表 + row['start'] = row['start'] + size + row['size'] = row['size'] - size + IsAssigned = 1 + print("[ %s, %s ]---已经分配成功......." % (size, status)) + break + # 重新编号 + busyTable.reset_index(drop=True, inplace=True) + + if not IsAssigned: + print("[ %s, %s ]---没有空闲空间,等待中......." % (size, status)) + schedule.append([size, status]) +``` + +## 测试 + +### 分配任务 + + +```python +for size, status in test: + FirstAdapt(size, status) +``` + + [ 10240, 3 ]---已经分配成功....... + [ 2048, 5 ]---已经分配成功....... + [ 1024, 2 ]---已经分配成功....... + [ 12288, 4 ]---已经分配成功....... + [ 1024, 3 ]---已经分配成功....... + [ 25600, 2 ]---没有空闲空间,等待中....... + [ 2048, 1 ]---已经分配成功....... + +```python +print(schedule) +``` + + [[25600, 2]] + +```python +freeTable +``` + + + .dataframe tbody tr th { + vertical-align: top; + } + + .dataframe thead th { + text-align: right; + } + + + + + + + + + + + + + + + + + +
startsizestatus
02867240960
+ +```python +busyTable +``` + + + .dataframe tbody tr th { + vertical-align: top; + } + + .dataframe thead th { + text-align: right; + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
startsizestatus
00102403
11024020485
21228810242
313312122884
42560010243
52662420481
+### CPU运行一个时间步 + + +```python +step() +``` + + [26624 2048]开始处理 ************************************************** + 下相邻合并...... + +```python +print(schedule) +``` + + [[25600, 2]] + +```python +busyTable +``` + + + .dataframe tbody tr th { + vertical-align: top; + } + + .dataframe thead th { + text-align: right; + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
startsizestatus
00102402
11024020484
21228810241
313312122883
42560010242
+ +```python +freeTable +``` + + + .dataframe tbody tr th { + vertical-align: top; + } + + .dataframe thead th { + text-align: right; + } + + + + + + + + + + + + + + + + + +
startsizestatus
02662461440
+ + +### CPU运行一个时间步 + + +```python +step() +``` + + [12288 1024]开始处理 ************************************************** + 无上下相邻...... + +```python +print(schedule) +``` + + [[25600, 2]] + +```python +busyTable +``` + + + .dataframe tbody tr th { + vertical-align: top; + } + + .dataframe thead th { + text-align: right; + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
startsizestatus
00102401
11024020483
213312122882
32560010241
+ + + +```python +freeTable +``` + + + .dataframe tbody tr th { + vertical-align: top; + } + + .dataframe thead th { + text-align: right; + } + + + + + + + + + + + + + + + + + + + + + + + +
startsizestatus
02662461440
11228810240
+### CPU运行一个时间步 + + +```python +step() +``` + + [0 10240]开始处理 ************************************************** + 无上下相邻...... + [25600 1024]开始处理 ************************************************** + 下相邻合并...... + +```python +print(schedule) +``` + + [[25600, 2]] + +```python +busyTable +``` + + + .dataframe tbody tr th { + vertical-align: top; + } + + .dataframe thead th { + text-align: right; + } + + + + + + + + + + + + + + + + + + + + + + + +
startsizestatus
01024020482
113312122881
+ +```python +freeTable +``` + + + .dataframe tbody tr th { + vertical-align: top; + } + + .dataframe thead th { + text-align: right; + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
startsizestatus
00102400
11228810240
22560071680
+ + diff --git "a/docs/\345\215\232\345\256\242/2021/08/04\346\225\243\345\210\227\350\241\250\345\256\236\347\216\260\346\237\245\346\211\276.md" "b/docs/\345\215\232\345\256\242/2021/08/04\346\225\243\345\210\227\350\241\250\345\256\236\347\216\260\346\237\245\346\211\276.md" new file mode 100644 index 0000000..1820f16 --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2021/08/04\346\225\243\345\210\227\350\241\250\345\256\236\347\216\260\346\237\245\346\211\276.md" @@ -0,0 +1,997 @@ +# 散列表实现查找 + +## Library + + +```python +import pandas as pd +import numpy as np +import time +``` + +## 读取数据 + + +```python +df = pd.read_excel('重庆市印刷和记录媒介复制业754.xlsx') +df.dropna(axis=0, how='any') # 去除非数 +print("表长为:", len(df)) +df.head() +``` + + 表长为: 75 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID企业名称电话企业地址
00万州区永佳路万德印刷厂15178905742重庆市万州区双河口永佳路325号
11覃彩虹13594410133重庆市万州区熊家镇古城大道296号
22重庆优得利印刷有限公司023-65903102重庆市江津区双福工业园1幢1号
33重庆市开州区森美印刷有限公司15608330060重庆市开州区云枫街道平桥社区桔乡路369号门市
44吕兴华13896015224重庆市璧山区大路街道天星街23号
+ + +## get_nms + + +```python +def get_nums(s): + ''' + 逐位相加其ASCII码 + ''' + s = str(s) + nums = 0 + for s0 in s: + if(s0 == '-'): + continue + try: + nums += ord(s0) + except: + print("error: ",s) + return nums +get_nums("重庆优得利印刷有限公司") +``` + + + 276879 + +## 平方取中法 + + +```python +df['nums_电话'] = df['电话'].apply(get_nums) +df['nums_电话^2'] = df['nums_电话'].apply(lambda x: np.square(x)) +df['nums_企业名称'] = df['企业名称'].apply(get_nums) +# df['nums_电话^4'] = df['nums_电话^2'].apply(lambda x: np.square(x)) +df.head() +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID企业名称电话企业地址nums_电话nums_电话^2nums_企业名称
00万州区永佳路万德印刷厂15178905742重庆市万州区双河口永佳路325号577332929257952
11覃彩虹13594410133重庆市万州区熊家镇古城大道296号56231584494053
22重庆优得利印刷有限公司023-65903102重庆市江津区双福工业园1幢1号559312481276879
33重庆市开州区森美印刷有限公司15608330060重庆市开州区云枫街道平桥社区桔乡路369号门市560313600364365
44吕兴华13896015224重庆市璧山区大路街道天星街23号56932376163703
+ +```python +def get_mid(x): + ''' + 取中间三位数作为地址 + ''' + return int(x/10)%1000 +``` + + +```python +df['adr_电话'] = df['nums_电话^2'].apply(get_mid) +df['adr_企业名称'] = df['nums_企业名称'].apply(get_mid) +df.head(100) +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID企业名称电话企业地址nums_电话nums_电话^2nums_企业名称adr_电话adr_企业名称
00万州区永佳路万德印刷厂15178905742重庆市万州区双河口永佳路325号577332929257952292795
11覃彩虹13594410133重庆市万州区熊家镇古城大道296号56231584494053584405
22重庆优得利印刷有限公司023-65903102重庆市江津区双福工业园1幢1号559312481276879248687
33重庆市开州区森美印刷有限公司15608330060重庆市开州区云枫街道平桥社区桔乡路369号门市560313600364365360436
44吕兴华13896015224重庆市璧山区大路街道天星街23号56932376163703376370
..............................
9595重庆涵天包装印务有限公司023-72231721重庆市涪陵区太极大道34号负一楼558311364318409136840
9696垫江县金辉印刷厂13110182246重庆市垫江县周嘉镇迎春路55731024920948424948
9797重庆凯翔包装印务有限公司13709401186重庆市永川区大安街道办事处大安工业园区568322624321198262119
9898丰都县蓝图印务有限公司15803661811重庆市丰都县三合街道商业二路117号附2号568322624284565262456
9999重庆毅然包装印刷有限公司13509402066重庆市綦江区文龙街道大田湾6号564318096323964809396
+ + + +## 除留余数法 + + +```python +df['adr_电话_1'] = df['nums_电话^2']%1007 +df['adr_企业名称_1'] = df['nums_企业名称']%1007 +df.head() +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID企业名称电话企业地址nums_电话nums_电话^2nums_企业名称adr_电话adr_企业名称adr_电话_1adr_企业名称_1
00万州区永佳路万德印刷厂15178905742重庆市万州区双河口永佳路325号577332929257952292795619160
11覃彩虹13594410133重庆市万州区熊家镇古城大道296号56231584494053584405653402
22重庆优得利印刷有限公司023-65903102重庆市江津区双福工业园1幢1号559312481276879248687311961
33重庆市开州区森美印刷有限公司15608330060重庆市开州区云枫街道平桥社区桔乡路369号门市560313600364365360436423838
44吕兴华13896015224重庆市璧山区大路街道天星街23号56932376163703376370514262
+ + +## 创建哈希表(分别使用开发地址与公共溢出区解决冲突) + + +```python +#初始化为全0 + +# 开放地址法所使用的hash +hash_map_tele = np.zeros(32000) +hash_map_name = np.zeros(8000) + +# 公共溢出区所使用的hash +hash_map_tele_1 = np.zeros(2100) +hash_map_name_1 = np.zeros(2100) + +len(hash_map_tele) +``` + + + 400000 + + + + +```python +#探测开放地址法 +def create_hash_map_tele(x, adr, df_ID): + try: + adr = int(x[adr]) + except: + print('error: ', adr) + while(hash_map_tele[adr] != 0): + adr += 800 + hash_map_tele[adr] = x[df_ID] + +def create_hash_map_name(x, adr, df_ID): + try: + adr = int(x[adr]) + except: + print('error: ', adr) + while(hash_map_name[adr] != 0): + adr += 800 + hash_map_name[adr] = x[df_ID] + +#使用公共溢出区 +count1 = 0 +count2 = 0 +def create_hash_map_tele1(x, adr, df_ID): + global count1 + if(hash_map_tele_1[x[adr]] == 0): + hash_map_tele_1[x[adr]] = x[df_ID] + else: + hash_map_tele_1[1100 + count1] = x[df_ID] + count1 += 1 + +def create_hash_map_name1(x, adr, df_ID): + global count2 + if(hash_map_name_1[x[adr]] == 0): + hash_map_name_1[x[adr]] = x[df_ID] + else: + hash_map_name_1[1100 + count2] = x[df_ID] + count2 += 1 + +``` + + +```python +df.apply(create_hash_map_tele, axis=1, args=('adr_电话', 'ID')) +df.apply(create_hash_map_name, axis=1, args=('adr_企业名称', 'ID')) + +df.apply(create_hash_map_tele1, axis=1, args=('adr_电话_1', 'ID')) +df.apply(create_hash_map_name1, axis=1, args=('adr_企业名称_1', 'ID')) + +hash_map_tele +``` + + + array([0., 0., 0., ..., 0., 0., 0.]) + +## 查找流程(平方取中+开发地址探测法)-method1 + + +```python +# 查找 +search_method = int(input("请输入你查找关键词的类型:1,电话查找;2,企业名称查找")) +if(search_method == 1): + tele = input("请输入你要查找对象的电话号码:") + tele = int(tele) + time_start = time.time() + nums = get_mid(pow(get_nums(tele), 2)) + print("-----base_nums-----\n",nums) + print("-----hash_map_tele_value-----\n", hash_map_tele[nums]) + print("-----开始查找-----") + while(df['电话'][hash_map_tele[nums]] != tele): + # print("-----tele-----\n", df['电话'][hash_map_tele[nums]]) + nums += 800 + # print('-----add_nums-----\n', nums) + if(nums >= 32000): + print('查找错误:无该信息') + break + time_end = time.time() + if(nums < 32000): + print("---------------你查找的信息如下:---------------\n", df.loc[hash_map_tele[nums]]) + + print("---------------本次查找耗时:---------------\n",time_end-time_start) +elif(search_method == 2): + name = input("请输入你要查找对象的企业名称:") + time_start = time.time() + nums = get_mid(get_nums(name)) + print("-----base_nums-----\n",nums) + print("-----hash_map_tele_value-----\n", hash_map_name[nums]) + print("-----开始查找-----") + while(df['企业名称'][hash_map_name[nums]] != name): + # print("-----tele-----\n", df['企业名称'][hash_map_name[nums]]) + nums += 800 + # print('-----add_nums-----\n', nums) + if(nums >= 8000): + print('查找错误:无该信息') + break + time_end = time.time() + if(nums < 8000): + print("---------------你查找的信息如下:---------------\n", df.loc[hash_map_name[nums]]) + + print("---------------本次查找耗时:---------------\n",time_end-time_start) +else: + print("请选择正确的查找方式!") +``` + + -----base_nums----- + 370 + -----hash_map_tele_value----- + 4.0 + -----开始查找----- + ---------------你查找的信息如下:--------------- + ID 4 + 企业名称 吕兴华 + 电话 13896015224 + 企业地址 重庆市璧山区大路街道天星街23号 + nums_电话 569 + nums_电话^2 323761 + adr_电话 376 + nums_企业名称 63703 + adr_企业名称 370 + Name: 4, dtype: object + ---------------查找耗时:--------------- + 0.0009999275207519531 + + +## 查找流程(除留余数法+公共溢出法)-method2 + + +```python +# 查找 +search_method = int(input("请输入你查找关键词的类型:1,电话查找;2,企业名称查找")) +if(search_method == 1): + tele = input("请输入你要查找对象的电话号码:") + tele = int(tele) + time_start = time.time() + nums = get_nums(tele)%1007 + print("-----base_nums-----\n",nums) + print("-----hash_map_tele_value-----\n", hash_map_tele_1[nums]) + print("-----开始查找-----") + while(df['电话'][hash_map_tele_1[nums]] != tele): + # print("-----tele-----\n", df['电话'][hash_map_tele_1[nums]]) + nums += 1 + # print('-----add_nums-----\n', nums) + if(nums >= 2100): + print('查找错误:无该信息') + break + time_end = time.time() + if(nums < 2100): + print("---------------你查找的信息如下:---------------\n", df.loc[hash_map_tele_1[nums]]) + + print("---------------本次查找耗时:---------------\n",time_end-time_start) +elif(search_method == 2): + name = input("请输入你要查找对象的企业名称:") + time_start = time.time() + nums = get_nums(name)%1007 + print("-----base_nums-----\n",nums) + print("-----hash_map_tele_value-----\n", hash_map_name_1[nums]) + print("-----开始查找-----") + while(df['企业名称'][hash_map_name_1[nums]] != name): + # print("-----tele-----\n", df['企业名称'][hash_map_name_1[nums]]) + nums += 1 + # print('-----add_nums-----\n', nums) + if(nums >= 2100): + print('查找错误:无该信息') + break + time_end = time.time() + if(nums < 2100): + print("---------------你查找的信息如下:---------------\n", df.loc[hash_map_name_1[nums]]) + + print("---------------本次查找耗时:---------------\n",time_end-time_start) +else: + print("请选择正确的查找方式!") +``` + + -----base_nums----- + 262 + -----hash_map_tele_value----- + 4.0 + -----开始查找----- + ---------------你查找的信息如下:--------------- + ID 4 + 企业名称 吕兴华 + 电话 13896015224 + 企业地址 重庆市璧山区大路街道天星街23号 + nums_电话 569 + nums_电话^2 323761 + nums_企业名称 63703 + adr_电话 376 + adr_企业名称 370 + adr_电话_1 514 + adr_企业名称_1 262 + Name: 4, dtype: object + ---------------本次查找耗时:--------------- + 0.005001544952392578 + + +## 性能分析 + + +```python +df.head(10) +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID企业名称电话企业地址nums_电话nums_电话^2nums_企业名称adr_电话adr_企业名称adr_电话_1adr_企业名称_1
00万州区永佳路万德印刷厂15178905742重庆市万州区双河口永佳路325号577332929257952292795619160
11覃彩虹13594410133重庆市万州区熊家镇古城大道296号56231584494053584405653402
22重庆优得利印刷有限公司023-65903102重庆市江津区双福工业园1幢1号559312481276879248687311961
33重庆市开州区森美印刷有限公司15608330060重庆市开州区云枫街道平桥社区桔乡路369号门市560313600364365360436423838
44吕兴华13896015224重庆市璧山区大路街道天星街23号56932376163703376370514262
55重庆海渝包装印刷有限公司13883451070重庆市璧山区奥康工业园金剑路271号568322624323605262360384358
66重庆鼎鸿印务有限公司17782279989重庆市永川区来龙四路36号597356409292462640246938432
77重庆奔速彩印有限公司023-62802235重庆市九龙坡区歇台子渝州路100号附1-5-1号561314721274268472426537364
88重庆市永川区木森印刷有限公司18580704257重庆市永川区三教镇三川路216号57533062536150262150329996
99重庆梧桐树印务有限公司023-67980980重庆市渝中区张家花园街206号58033640029136964013662346
+ +```python +print("表长为:", len(df)) +``` + + 表长为: 754 + + +### 32000与8000的来历 + + +```python +print("重复元素最多次数fen'bie为:") +for i in range(500): + flag = 0 + for j in range(800): + index = 800*i + j + if(hash_map_tele[index] != 0): + flag = 1 + if(flag == 0): + print("tele_i:", i) + break + +for i in range(500): + flag = 0 + for j in range(800): + index = 800*i + j + if(hash_map_name[index] != 0): + flag = 1 + if(flag == 0): + print("name_i:", i) + break +``` + + tele_i: 39 + name_i: 9 + + +### 计算method1的ASL + + +```python +c1 = 0 # 统计总的查找次数 +for i in range(500): + for j in range(800): + index = 800*i + j + if(hash_map_tele[index] != 0): + c1 += (i+1) +print('method1-tele-asl:', c1/754) +c2 = 0 # 统计总的查找次数 +for i in range(500): + for j in range(800): + index = 800*i + j + if(hash_map_name[index] != 0): + c2 += (i+1) +print('method1-name-asl:', c2/754) +``` + + method1-tele-asl: 12.10079575596817 + method1-name-asl: 1.6498673740053051 + + +### 计算method2的ASL + + +```python +df1 = df.adr_电话_1.value_counts() +print(df1) +``` + + 514 39 + 916 33 + 329 32 + 47 31 + 767 31 + 187 29 + 216 29 + 473 28 + 619 27 + 646 26 + 384 26 + 62 26 + 9 25 + 372 25 + 175 21 + 530 20 + 6 20 + 852 19 + 256 18 + 130 18 + 780 17 + 690 17 + 343 16 + 917 16 + 653 15 + 537 15 + 685 13 + 423 12 + 513 11 + 891 11 + 201 10 + 771 10 + 311 9 + 859 7 + 994 7 + 93 6 + 386 5 + 568 5 + 938 5 + 206 4 + 28 4 + 688 3 + 890 3 + 788 2 + 119 2 + 590 1 + 309 1 + 752 1 + 494 1 + 894 1 + 434 1 + Name: adr_电话_1, dtype: int64 + +```python +df2 = df.adr_企业名称_1.value_counts() +print(df2) +``` + + 68 6 + 90 5 + 406 5 + 231 4 + 979 4 + .. + 439 1 + 768 1 + 445 1 + 446 1 + 1006 1 + Name: adr_企业名称_1, Length: 516, dtype: int64 + +```python +print("有多少个元素在method2-tele基础表中,即只需查一次:",len(df1)) +print("有多少个元素在method2-name基础表中,即只需查一次:",len(df2)) +``` + + 有多少个元素在method2-tele基础表中,即只需查一次: 51 + 有多少个元素在method2-name基础表中,即只需查一次: 516 + +```python +# 基本表中的元素只需查一次,溢出区的元素和顺序表的查找次数是一样的,所以可以使用等差数列的计算公式进行计算,不过需要注意的就是溢出区的查找次数是从2开始的 +print("method2-tele-asl:", (51 + (703*2 + (703*702)/2))/754) +print("method2-name-asl:", (516 + (238*2 + (238*237)/2))/754) +``` + + method2-tele-asl: 329.19098143236073 + method2-name-asl: 38.720159151193634 + + +## 最终得出结果 ++ method1-tele-asl: 12.10079575596817 ++ method1-name-asl: 1.6498673740053051 ++ method2-tele-asl: 329.19098143236073 ++ method2-name-asl: 38.720159151193634 +## 分析 +总的来说,重复元素的个数越多,哈希查找的效率就相应越低而法1的开放地址探测法对冲突的解决在本题中查找效率是明显优于公共溢出法的,但相应的占据了更大的存储空间至于平方去中法以及除留余数法在本题中映射出来的地址重复个数基本一致,故不相上下。 + + diff --git "a/docs/\345\215\232\345\256\242/2022/02/01\351\203\2752022\345\271\264\344\272\206\357\274\214\350\277\230\346\230\257\345\276\227\345\255\246\345\234\243\346\235\257\345\270\203\345\261\200\344\270\216\345\217\214\351\243\236\347\277\274\345\270\203\345\261\200.md" "b/docs/\345\215\232\345\256\242/2022/02/01\351\203\2752022\345\271\264\344\272\206\357\274\214\350\277\230\346\230\257\345\276\227\345\255\246\345\234\243\346\235\257\345\270\203\345\261\200\344\270\216\345\217\214\351\243\236\347\277\274\345\270\203\345\261\200.md" new file mode 100644 index 0000000..b440fad --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2022/02/01\351\203\2752022\345\271\264\344\272\206\357\274\214\350\277\230\346\230\257\345\276\227\345\255\246\345\234\243\346\235\257\345\270\203\345\261\200\344\270\216\345\217\214\351\243\236\347\277\274\345\270\203\345\261\200.md" @@ -0,0 +1,434 @@ + +# 2022年了,还是得学圣杯布局与双飞翼布局 + +## 三列布局的其他实现 + +定义:三栏布局一般指的是页面中一共有三栏,**左右两栏宽度固定,中间自适应的布局** + +这里先介绍几种也是比较常用的,并且比较容易理解的三列布局实现方法: + +### 绝对定位 + +利用**绝对定位**,左右两栏设置为绝对定位,中间设置对应方向大小的margin的值。 + +```css +.outer { + position: relative; + height: 100px; +} + +.left { + position: absolute; + width: 100px; + height: 100px; + background: tomato; +} + +.right { + position: absolute; + top: 0; + right: 0; + width: 200px; + height: 100px; + background: gold; +} + +.center { + margin-left: 100px; + margin-right: 200px; + height: 100px; + background: lightgreen; +} +``` + +### flex布局 + +这是比较方便的一种方法了,不过存在兼容性问题,这里利用flex布局,左右两栏设置固定大小,中间一栏设置为flex:1。 + +```css +.outer { + display: flex; + height: 100px; +} + +.left { + width: 100px; + background: tomato; +} + +.right { + width: 100px; + background: gold; +} + +.center { + flex: 1; + background: lightgreen; +} +``` + +### 浮动布局 + +利用浮动,左右两栏设置固定大小,并设置对应方向的浮动。中间一栏设置左右两个方向的margin值,注意这种方式**,中间一栏必须放到最后:** + +```css +.outer { + height: 100px; +} + +.left { + float: left; + width: 100px; + height: 100px; + background: tomato; +} + +.right { + float: right; + width: 200px; + height: 100px; + background: gold; +} + +.center { + height: 100px; + margin-left: 100px; + margin-right: 200px; + background: lightgreen; +} +``` + +## 圣杯布局 + +### 首先为什么需要这个布局: + +flex布局的缺点是可能存在兼容性问题,而上方的浮动布局肯定是不存在这个问题的,那么它的缺点是什么呢?答案上面已经阐述了:中间一栏必须放到最后。这个会导致一个问题,在页面加载这个容器的时候,正文却是最后加载的,影响体验。而圣杯布局以及接下的双飞翼布局正文(center标签)会放在容器里的最前面。 + +### 如何实现 + +利用浮动和负边距来实现。父级元素设置左右的 padding,三列均设置向左浮动,中间一列放在最前面,宽度设置为父级元素的宽度,因此后面两列都被挤到了下一行,通过设置 margin 负值将其移动到上一行,再利用相对定位,定位到两边。 + +上面这句话是整个布局的实现过程,如果不理解,接下来将一步步实现: + +#### margin负边距有什么用 + +1. **负margin值可以使浮动元素重叠** + +​ 复习一下浮动的工作原理: + +- 浮动元素脱离文档流,不占据空间(引起“高度塌陷”现象) +- 浮动元素碰到包含它的边框或者其他浮动元素的边框停留 + +​ 所以这里的margin负值就是覆盖了浮动的第二条性质,下面的代码实现的效果如下下: + +```html + +
+
+
+
+
+ + +``` + +​ ![image-20220225152659616](https://oss.justin3go.com/blogs/image-20220225152659616.png) + +2. **负的margin值在页面中是如何计算的** + + 这里仅讨论margin-left与margin-right,然后这两个类似,下面就以margin-right为例: + + 正的margin-left就是使元素根据容器(或其他元素)的右边缘为基准线向左移动多少: + + ![image-20220225155944187](https://oss.justin3go.com/blogs/image-20220225155944187.png) + +​ 负的margin-left就是基准线还是一样,只是变成了向右移动: + +```css +.outer{ + width: 500px; + height: 100px; + border: 1px dotted black; + margin: auto; +} +.item{ + float: left; + margin-left: -100px; + + width: 100px; + height: 100px; + opacity: 0.4; +} +``` + +![image-20220225160411503](https://oss.justin3go.com/blogs/image-20220225160411503.png) + +到这里都是比较基础的内容,这里需要注意这个基准线,先记着这个,后面讲圣杯布局的时候会更好理解。 + +#### 讲一讲浮动 + +先看下面的布局: + +```html + +
+
+
+
+
+ + +``` + +![image-20220225161653578](https://oss.justin3go.com/blogs/image-20220225161653578.png) + +这就是浮动,这里注意是向左浮动的,简单来说就是各个元素按照排列顺序疯狂往左边挤就对了。 + +如果我们将`center`的宽度设大,那么其他两个元素会被挤下去: + +```css +.center { + float: left; + + height: 100px; + width: 500px; /* 这里 */ + background-color: blue; + opacity: 0.5; +} +``` + +![image-20220225162100150](https://oss.justin3go.com/blogs/image-20220225162100150.png) + +这里后面两个元素也是一直在往左挤的,甚至我们可以理解为下方的左边缘与上方的右边缘是一条线: + +![image-20220225162249294](https://oss.justin3go.com/blogs/image-20220225162249294.png) + +#### 浮动+margin负边距 + +结合上面的知识,我们对下方两个元素块设置负的margin,注意这里父容器以及被挤满了(`width:100%`),所以这两个元素的margin-left与margin-right都是以上面的右边缘为基准线进行移动的。 + +为left元素添加一个负边距: + +```css +.left { + float: left; + + margin-left: -150px; /* 这里 */ + height: 100px; + width: 100px; + background-color: brown; + opacity: 0.5; +} +``` + + + +![image-20220225162922076](https://oss.justin3go.com/blogs/image-20220225162922076.png) + +再次提醒:这里因为父容器被center元素给挤满了,同时center设置的是`float: left`,所以可以理解为父容器的右边缘还剩下无限小的一条线的空间,而此时为left元素设置margin的话就是以这条线为基准进行移动的。 + +补充: + +如果margin移动的距离小于自身宽度,元素是以下方的基准线来进行移动的,因为之前也讲过下方的基准线上上方的基准线其实可以理解为一条线,这也非常容易理解,并且圣杯布局并不会用到这个补充,这只是为了让你更加理解浮动配合负边距的效果: + +`margin-left: -50px`时: + +![image-20220225163444500](https://oss.justin3go.com/blogs/image-20220225163444500.png) + +更灵活一点,如果设置把所有浮动设置为right,基准线变化如下: + +```html + +
+
+
+
+
+ + +``` + +![image-20220225164217915](https://oss.justin3go.com/blogs/image-20220225164217915.png) + +#### 圣杯布局的实现 + +所以,接下来就非常简单了: + +1. 有一个BFC父容器,同时设置padding为左右两栏预留位置; +2. center在最前面; +3. center挤满整个父容器; +4. 使用margin将left与right移动一定位置; +5. 使用position相对定位调整位置; + +当然,这里父容器的宽度并不是固定的,本来就要做响应式的嘛,上面的代码固定宽度只是为了更好地作图以及方便大家理解,所以下方的代码上上面地代码还是有一些出入的: + +```css +.outer { + height: 100px; + padding-left: 100px; /* 左右两栏预留位置; */ + padding-right: 200px; +} + +.left { + position: relative; + left: -100px; + + float: left; + margin-left: -100%; + width: 100px; + height: 100px; + background-color: blue; + opacity: 0.5; +} + +.right { + position: relative; + left: 200px; + + float: right; + margin-left: -200px; + + width: 200px; + height: 100px; + background-color: brown; + opacity: 0.5; +} + +.center { + float: left; + + width: 100%; + height: 100px; + background-color: darkgoldenrod; + opacity: 0.5; +} +``` + +为了大家更好的理解,下方为**无相对定位**的效果: + +![image-20220225165039356](https://oss.justin3go.com/blogs/image-20220225165039356.png) + +添加相对定位后的效果: + +![image-20220225165116479](https://oss.justin3go.com/blogs/image-20220225165116479.png) + +## 双飞翼布局 + +双飞翼布局相对于圣杯布局来说,左右位置的保留是通过中间列的 margin 值来实现的,而不是通过父元素的 padding 来实现的。本质上来说,也是通过浮动和外边距负值来实现的。 + +这里就不过多赘述,可以查看圣杯布局的实现: + +双飞翼布局代码如下: + +```css +.outer { + height: 100px; +} + +.left { + float: left; + margin-left: -100%; + + width: 100px; + height: 100px; + background: tomato; +} + +.right { + float: left; + margin-left: -200px; + + width: 200px; + height: 100px; + background: gold; +} + +.wrapper { /* 这个作为сenter的父容器是为了方便设置宽度为100%,否则只用一个元素设为100%再加上margin会把父容器撑大 */ + float: left; + + width: 100%; + height: 100px; + background: lightgreen; +} +/* 将margin与width:100%分开写才行 */ +.center { + margin-left: 100px; + margin-right: 200px; + height: 100px; +} +``` + +最后,希望面试问到这道题[滑稽] + diff --git "a/docs/\345\215\232\345\256\242/2022/02/02TypeScript\345\205\245\351\227\250.md" "b/docs/\345\215\232\345\256\242/2022/02/02TypeScript\345\205\245\351\227\250.md" new file mode 100644 index 0000000..17a9c0a --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2022/02/02TypeScript\345\205\245\351\227\250.md" @@ -0,0 +1,687 @@ +# TypeScript入门 + +## 如何使用TS来输出你的HelloWorld呢? + +> 这一步对于很多人来说是最简单的一步,也是最难的一步,说简单是因为这确确实实仅是入门的一步,就是一个环境配置,说难则是因为很多人无法跨出这一步,当你跨出这一步之后,你会发现后面的真的学得很快很快,现在,就让我们一起跨出这一步吧~ + +step1:安装TS + +```javascript +cnpm i -g typescript +// 查看是否安装成功,能输出对应的版本号就对了 +tsc -v +``` + +step2:编写TS代码 + +```javascript +console.log('Hello TS'); +``` + +有小伙伴就问,这也是TS的代码吗?这不是JS的代码吗?当然是,TypeScript是JavaScript的超集,因为它扩展了JavaScript,有JavaScript没有的东西,那么JS有的东西,它也是有的,你就放心使用吧~ + +image-20220312205404227 + +step3:编译TS为JS + +TS在浏览器中是无法运行的,它的出现只是为了弥补开发人员在编写JS代码的痛苦,就是无类型,这个在小型demo中是无法体现的,但一上升到大项目中,你会发现JS的any类型难以维护,这也是TS出现的原因,大家都知道Vue3是全部拿TS构建了,那么Vue3+TS+...就是现在开发的一套可能是主流技术栈了,这也更加坚定了我们学习TS的原因! + +![image-20220313092140181](https://oss.justin3go.com/blogs/image-20220313092140181.png) + +![image-20220312205955891](https://oss.justin3go.com/blogs/image-20220312205955891.png) + +```javascript +tsc .\01_Hello.ts // 编译刚写的TS文件 +``` + +之后会生成同名的JS文件,好了,现在,你可以运行你的第一句TS代码吧~ + +## 项目中如何编译多个TS文件呢? + +> 上述的编译方式只能编译一个文件,多个文件难道需要我们一步步执行命令?这对于几乎成为主流的TS来说怎么可能会有如此低效率的编译方式,现在,让我们学习一下多个文件的编译方式: + +```javascript + tsc --init // 首先你需要使用这个命令初始化一个tsconfig.json文件 +``` + +之后我们就可以在这个文件里面控制对我们整个项目中多个文件的编译方式了,哈~再也不用每个文件都执行一次tsc+hello.ts了[doge] + +下面我列举了一些比较常用的tsconfig.json的配置信息,大家可以根据自己需要进行配置,也可以查看[官网文档](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html),点进官方文档之前记得给本篇文章点个赞支持一下哦,别过去了就回不来了😚; + +```json +{ + /* + 配置文件,根据该配置进行编译 + "include": []-->哪些配置文件需要被编译 + '*'表示任意文件 + '**'表示任意目录 + */ + "include": [ + "*" +, "src/*" ], + // 不包含 + "exclude": [ + + ], + // "extends": "" --> 继承于某个配置文件 + // "files": [?] //文件,不能是目录 + "compilerOptions": { + // 编译为ES的版本 + "target": "ES3", + // 指定要使用的模块化方案 + "module": "es2015", + // "lib": []-->指定项目中需要用到的库-->一般不需要设置 + "outDir": "./dist", //编译后的存放目录 + // 将代码合并到为一个文件-->如果和ing多个模块,module-->system|amd + // "outFile": "./dist/app.js" + // 是否对js文件进行编译 + "allowJs": false, + // 是否检查js代码符合规范 + "checkJs": false, + // 是否移除注释 + "removeComments": false, + // 不生成编译后的文件 + "noEmit": false, + // 当有错误的时候就不生成编译后的文件 + "noEmitOnError": false, + // 所有严格模式的总开关 + "strict": false, + // 严格模式-->性能更好 + "alwaysStrict": true, + // 不允许隐式的any类型 + "noImplicitAny": true, + // 不允许不明确类型的this + "noImplicitThis": true, + /* + function fn(this: window){ + alert(this) + } + */ + // 严格地检查空值-->比如某些获取dom元素 + "strictNullChecks": false + } +} +``` + +然后记得终端输入下方命令就可以对整个项目中的多个文件进行编译和监控了 + +```javascript +tsc -w +``` + +到这里,你已经跨出了学习TS中最难的一步了,接下来面对你的将是`康庄大道=======>success` + +![image-20220312212008138](https://oss.justin3go.com/blogs/image-20220312212008138.png) + +当然还有其他方式来控制整个项目对TS的编译方式,比如在webpack中的配置可能是这样的(这里就不过多赘述了,大家初始学习的话可以跳过这方面的知识,明白前两种编译方式就可以应对接下来的知识,后续你在编写大型项目中根据自己的需求再在网上找对应的配置就可以,毕竟知识的海洋是无限的,有些东西需要深入了解,但有些东西又得不求甚解,很多时候做一样东西你不一定会其中的技术栈,然后你再学-->使用,技术是学不完的,按需学习感觉才是这个时代的主流,当然,程序员的核心素质除外,这个挖再深都对你好处很大): + +![image-20220312213258704](https://oss.justin3go.com/blogs/image-20220312213258704.png) + +```json +// tsconfig.json中 +{ + "compilerOptions": { + "module": "ES2015", + "target": "ES2015", + "strict": true + } +} +``` + +```json +// package.json中 +{ + "devDependencies": { + "ts-loader": "^9.2.6", + "typescript": "^4.5.2", + "webpack": "^5.65.0", + "webpack-cli": "^4.9.1" + }, + "scripts": { + "build": "webpack" + } +} + +``` + +```js +// webpack.config.js中 +// 引入一个拼接路径的包 +const path = require('path'); + +// webpack中所有的配置信息 +module.exports = { + // 入口文件 + entry: "./src/index.ts", + // 指定输出文件所在目录 + output: { + // 指定目录 + path: path.resolve(__dirname, 'dist'), + // 打包以后的文件名 + filename: "bundle.js" + }, + // 指定webpack打包时要使用的模块 + module:{ + // 指定加载的规则 + rules: [{ + // test指定的是规则生效的文件 + test: /\.ts$/, + // 要使用的loader + use: 'ts-loader', + exclude:/node-modules/ + }] + } +} +``` + +好了,刚才突然就想感叹一下,接下来我们一起走这条康庄大道吧 + +![image-20220312213502936](https://oss.justin3go.com/blogs/image-20220312213502936.png) + +## 认识类型 + +> 类型是typescript中最重要的概念,毕竟人家就叫**type**script + +TS中是如何声明一个类型的呢?记住下面的格式: + +```ts +let a: number; +``` + +上面的代码又和我们平常书写的JS有什么不同的? + +JS中,我们输入如下的代码,不会报错: + +```javascript +let a; +a = 'this is string'; +a = 1234; +a = true; +``` + +但是在TS中,不同类型的值是不能相互赋值的,否则将会报错,这个报错也是我们选择TS的重要原因,JS是动态类型语言,很多时候你都不会发现你的类型搞错了,编译器也不会给你报错,当这个错误影响到项目运行的时候,你又找不到,昂~ + +![image-20220312214238578](https://oss.justin3go.com/blogs/image-20220312214238578.png) + +在初入计算机领域时,我们最怕的就是编写的代码被编译器提示报错,但是现在,我们更怕的是生产环境中报错,编译器最好多报一点错,因为编译器的提示真的太好解决了,就像你的坐姿,在小时候写字的时候是最好纠正的,但现在,害>程序员的腰酸背痛! + +下方的代码中,编译器会提示报错: + +```javascript +let a: number; +// a = 'hello' --> 报错 +``` + +```javascript +let b: string; +b = "hello"; // 这样才行 +``` + +当然你也可以这样`let a: any;`,然后你也可以像JS那样赋值,千万不要这么做,除非万不得已,否则就违背了TS的初衷,对了,如果声明对象不指定类型`let a;`也会导致隐式的`any`类型,永远记得不要这么做🈲 + +除此之外,你也可以给函数的形参和返回值做类型的声明: + +```javascript +function sum_ts(a:number, b:number):number { + return a + b; +} +``` + +回想一下,js中的函数是不考虑参数的类型以及个数的,它的个数只是表面的个数,就算你不声明形参个数,你仍然可以给JS的函数传任意的参数个数,毕竟JS函数可以使用**`arguments`** 这个类数组对象进行访问,这就导致很多时候你想编写一个万用的函数,需要考虑到是否收集剩余参数,而其他开发者在使用这个函数的时候也不能很清楚的知道这个函数的输入输出,这就完全违背了封装的思想-->拿来就用! + +所以,好好学一下TS吧~ (*^_^*) + +## 深入类型 + +### 基本类型介绍 + +就是JS中的八种类型,你还记得吗? + +**😀面试官:**说一说ES6中的八种数据类型? + +**❓我:**JavaScript共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。 + +😀**面试官:**哪两种类型是新增的?说一说你对它们的了解? + +**❓我:**其中 Symbol 和 BigInt 是ES6 中新增的数据类型: + +- Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题,可以很好地隔离用户数据与程序状态。 +- BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。 + +![image-20220312220857718](https://oss.justin3go.com/blogs/image-20220312220857718.png) + +TS中的基础类型也是这八种,写成TS的格式的话就是如下代码: + +```javascript +let str: string = "justin3go"; +let num: number = 20; +let bool: boolean = false; +let u: undefined = undefined; +let n: null = null; +let obj: object = {justin: 3}; +let big: bigint = 100n; +let sym: symbol = Symbol("justin3go"); +``` + +TS难吗,不难,难的是JS! + +![image-20220312221231737](https://oss.justin3go.com/blogs/image-20220312221231737.png) + +当然还有一些需要注意的地方,下面将详细阐述: + +1. 使用字面量形式进行类型声明: + + ```javascript + let a1: 10; + a1 = 10; + // a1 = 20 --> 报错 + ``` + + ```javascript + // 作用: + let b1: "male" | "female"; // 这样b1就只能在这两个字符串中选了 + // 记得:现在做的限制是对以后的回馈,后续写代码才会更加的方便。 + //--------------------------分界线-------------------------------------- + // 联合类型 + let c1: boolean | string; + ``` + +2. 关于TS中的任意类型: + + ```javascript + // any-->和js一样关闭类型检测 + let d1: any; + // 注:声明变量如果不指定类型,默认为隐式的any + let e1; + // 赋值 + let s1: string; + //any可以赋值给任意类型,同时导致s1也变为any类型了 + s1 = d1; + ``` + + ```javascript + // 但是unknown就不一样-->实际上就是一个类型安全的any,不能直接赋值给其他的变量 + let g1: unknown; + g1 = 10; + g1 = true; + g1 = "hello"; + // s1 = g1 --> 此时g1赋值会报错,不能赋值 + if (typeof g1 === "string") { + s1 = g1; //这种就不会报错 + } + //--------------------------分界线---------------------------------- + // 当你确认g1就是字符串,一定要赋值的话,可以这么做: + // 类型断言-->我知道它就是字符串 + s1 = g1 as string; + s1 = g1; + ``` + +3. 关于函数中的空值: + + ```javascript + // 空值 + function fn1(): void { + // return 123 报错 + return; // 对 + } + // 没有值-->表示永远不会返回结果,空也不返回(return;也不行) + function fn2(): never { + //作用:可以用来指定专门的函数报错 + throw new Error("error"); + } + ``` + +### 对象类型介绍 + +#### 定义对象的结构 + +刚才,我们一起学习了八种基本类型,其中有一种叫做`let obj: object;`,我们可以对其这样赋值: + +```javascript +// let obj: object; +obj = {}; +obj = function () {}; +``` + +所以一般也不会使用object,因为js中一切都是对象,相当于没有限制,一般这样使用,主要是声明里面的属性,之后使用结构就必须一摸一样: + +```javascript +let b2: { name: string }; +// b2 = {} --> 这种就会报错:因为需要且仅需要一个name属性 +// b2 = {name: "孙悟空", age: 18} -->error +b2 = { name: "孙悟空" }; +``` + +但是,偶尔我们也许需要部分结构是固定的,还有一些key值不确定,这时候,我们可以使用`?`来实现这种表示: + +```javascript +// ?代表这个属性有和没有都是可以的 +let c2: { name: string; age?: number }; +``` + +如果需要需要name这一个属性,其他的属性无所谓,我们可以这么做: + +```javascript +let d2: { name: string; [propName: string]: any }; // 代表必须包含name属性就可以了 +d2 = { name: "猪八戒", xixi: "xixi", haha: "haha" }; +``` + +既然对象可以提前定义其结构,那么理所当然函数也是可以提前定义其结构以及返回值的 + +![image-20220313081751269](https://oss.justin3go.com/blogs/image-20220313081751269.png) + +限制函数结构的语法: + +```javascript +let e2: (a: number, b: number) => number; +``` + +#### 数组 + +1. 我们可以使用`string[]`来表示字符串数组; + + ```ts + let l2: string[]; + l2 = ["a", "b", "c"]; + ``` + +2. 还可以使用`Array`来表示 + + ```ts + let i2: Array; + ``` + +#### 元组 + +> 元组:相当于固定长度的数组-->效率比较高 + +```ts +let h: [string, string]; +h = ["haha", "xixi"]; +``` + +#### 枚举 + +> 枚举:把所有的情况列举出来 + +```ts +enum Gender { + Male, + Feamle, +} +let j2: { name: string; gender: Gender }; +j2 = { + name: "悟空", + gender: Gender.Feamle, +}; +``` + +学一样东西必不可少的就是提问,新技术的出现必定是为了解决某一类问题,带有一定目的的,所以这种类型有什么好处吗? + +![image-20220313082623000](https://oss.justin3go.com/blogs/image-20220313082623000.png) + +其实上面的例子已经解释得非常清楚了,就是让某些标识符有语义。怎么理解这句话呢,比如如果没有该类型,我们表示男女一般会使用01来表示,那到底0是男还是1是男呢,当然01都可能是男/(ㄒoㄒ)/~~ + +![image-20220313083003251](https://oss.justin3go.com/blogs/image-20220313083003251.png) + +开个玩笑,所以枚举就是为了解决这类问题的,它让我们在编程的时候更加方便地知道性别这个类包含哪种类型,简单来说是下面几点: + +- 实际上这个值在转的时候就会转为0,1 +- 与直接0,1比较更容易辨识 +- 与直接字符串比较更节约存储 + +然后,再补充一些小知识: + +```javascript +// & 表示且 +let g: { name: string } & { age: number }; +// g = {name: "悟空"} --> 报错 +``` + +```javascript +// 类型的别名 +type myType = 1 | 2 | 3 | 4 | 5; +let k2: myType; +let h2: myType; +``` + +到这里,你几乎已经完成TS学习的一半了,是不是非常简单,接下来的内容如何小伙伴接触过java、C++这种面向对象的语言,可能理解起来非常简单,接下来的知识就是TS中另外一个非常重要的概念**类** + +![image-20220313083814386](https://oss.justin3go.com/blogs/image-20220313083814386.png) + +## 类 + +> 当然,如果你熟悉JS中类的语法,对于接下来的理解也会非常简单 + +### 简介 + +还是和之前`hello world`一样,我们先来手写定义一个类,然后实例化这个类,认识一下它,一回生,二回熟嘛,知识也是这样的: + +```javascript +// 使用class定义 +class Person { + // 实例属性 + name: string = "孙悟空"; + // 只读属性 + readonly gender:boolean = true; + // 在属性前使用static关键字可以定义类属性(静态属性) + static age: number = 18; + sayHello(params:string):void { + console.log('Hello!',params) + } +} +``` + +```javascript +const per = new Person(); +console.log(per.name); //通过实例访问(duxier) +console.log(Person.age); //通过对象名访问 +``` + +是不是真的就没什么特别之处,学习成本超低的好吗,动起来动起来! + +### 构造函数 + +这部分也可以说和JS中ES6语法的构造函数一模一样: + +```javascript +class Dog { + name: string; + age: number; + constructor(name: string, age: number) { + this.name = name; + this.age = age; + } + + bark() { + alert("汪汪汪!"); + } +} +``` + +### 继承 + +也是差不多的: + +```javascript +class animal { + name: string; + age: number; + constructor(name: string, age: number) { + this.name = name; + this.age = age; + } + + sayHello() { + console.log("动物在叫~"); + } +} +//共有的代码写在父类之中 +class Dog extends animal { + run(){ + console.log("旺仔再跑"); + } +} +class Cat extends animal {} +``` + +然后继承都来了,`super`关键字自然不会缺席 + +![image-20220313084913690](https://oss.justin3go.com/blogs/image-20220313084913690.png) + +```javascript + class Animal { + name: string; + age: number; + constructor(name: string, age: number) { + this.age = age; + this.name = name; + } + sayHello() { + console.log("动物在叫~"); + } + } + class Dog extends Animal { + sayHello(): void { + //调用父类的方法 + super.sayHello(); + } + } + // 用在添加属性后构造函数中 + class Cat extends Animal { + color:string; + constructor(name:string, age:number, color:string){ + super(name, age) + this.color = color + } + } +``` + +当然,super再在JS中是有一些限制的,这里默认是认为你们已经熟悉了JS了,还没熟悉的赶快去熟悉,来学什么TS呢?[推荐阅读红宝书的p260] + +### 抽象类 + +再回想一下,JS中有抽象类吗?没有。能实现抽象类吗?可以:通过`new.target`的判断来判断是非常容易实现的: + +```javascript +class animal { + constructor(){ + console.log(new.target); // 表示new关键字后面跟着的类型 + if(new.target === animal){ + throw new Error('xx不能被实例化') + } + } +} +``` + +此时,你就不能直接实例化这个类了,只能通过继承,然后实例化其子类是没有问题的。 + +到这里,你可能会说别人其他语言都有`abstract`关键字使用,为什么我JS还要自己手动判断呢?似乎一点也不优雅! + +![image-20220313090238262](https://oss.justin3go.com/blogs/image-20220313090238262.png) + +于是TS中引入了`abstract`关键字,可以非常方便地帮助我们定义抽象基类: + +除此之外,在JS中定义抽象方法也需要在构造器中多增加一个判断--this下是否有某方法,没有就抛出异常,而这里TS的`abstract`关键字也可以使用在抽象方法上,非常方便 + +```ts + // 不能用来创建对象,这个类就是用来被别人继承的 + // 抽象类中可以去添加抽象方法 + abstract class animal{ // 1 + name: string; + constructor(name:string){ + this.name = name + } + abstract sayHello():void; // 2 + } +``` + +### 接口 + +```ts +type myType = { + name: string; + age: number; +}; +//接口就是用来定义一个类结构的 +// 可以当作类型声明去使用 +// 区别是重新再写一个myInterface不会报错,结果是合并 +interface myInterface { + name: string; + age: number; +} +// 接口可以在定义类的时候限制结构 +// 接口中所有的属性都不能有实际的值 +// 接口只定义对象的结构而不考虑实际值 +// 所有的方法都是抽象方法 + +// 去实现一个接口 +class Mycalss implements myInterface { + name: string; + age: number; + constructor(name: string, age: number) { + this.age = age; + this.name = name; + } +} +``` + +### 属性的封装 + +再想一下,我们实现一个私有变量有多麻烦,用闭包实现仅在内部作用域能访问该变量,即私有化,然后再使用weakmap解决闭包导致的垃圾回收的问题,当然ES6中的#也可以非常方便的添加私有变量,而这里TS中的`private`似乎更符合我们在其他语言中见识的一致。 + +```javascript +//属性可以任意修改将会导致对象中的数据变得非常不安全 +class Person { + // pubulic默认值,可以在任何地方修改 + // private:只能在内部修改,当前类,子类也不行 + // protected: 只能在当前类和当前类的子类中使用 + public _name: string; +private _age: number; + +constructor(name:string, age:number){ + this._name = name; + this._age = age; +} +// getName(){ +// return this.name +// } +// setName(value:string){ +// this.name = value +// } + +//ts中设置getter方法的方式 +get name(){ + return this._name +} +} +// 其他地方就可以这样调用name属性 +const per= new Person("1",1) +console.log(per.name); +``` + +最后,在补充一个泛型的概念,这个就和C++中的泛型几乎一致,就是写函数时可以不定参数的类型,使用函数时在确定参数的类型: + +C++中是这样的`vector test`:向量中存储的类型是int,`vector test`:向量中存储的类型是string + +TS中是这样使用的: + +```ts +// 在定义函数或者是类时,如果遇到类型不明确就可以使用泛型 +function fn(a: T): T{ + return a; +} +//这样可以避免使用any,同时保证我的参数以及返回值的类型时相同的 +//使用 +//可以直接使用具有泛型的函数 +fn(10); +//指定泛型 +fn('hello') +``` + +最后最后,写作不易,支持一下哦~ + +![image-20220313092028334](https://oss.justin3go.com/blogs/image-20220313092028334.png) + + + diff --git "a/docs/\345\215\232\345\256\242/2022/02/03\350\277\231\351\201\223\351\242\230\345\216\237\346\235\245\345\217\257\344\273\245\347\224\250\345\210\260JS\350\277\231\344\271\210\345\244\232\347\237\245\350\257\206\347\202\271\357\274\201.md" "b/docs/\345\215\232\345\256\242/2022/02/03\350\277\231\351\201\223\351\242\230\345\216\237\346\235\245\345\217\257\344\273\245\347\224\250\345\210\260JS\350\277\231\344\271\210\345\244\232\347\237\245\350\257\206\347\202\271\357\274\201.md" new file mode 100644 index 0000000..ab4833f --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2022/02/03\350\277\231\351\201\223\351\242\230\345\216\237\346\235\245\345\217\257\344\273\245\347\224\250\345\210\260JS\350\277\231\344\271\210\345\244\232\347\237\245\350\257\206\347\202\271\357\274\201.md" @@ -0,0 +1,96 @@ +# 这道题原来可以用到JS这么多知识点! + +## [JZ34 二叉树中和为某一值的路径(二)](https://www.nowcoder.com/practice/b736e784e3e34731af99065031301bca?tpId=13&tqId=23276&ru=/practice/445c44d982d04483b04a54f298796288&qru=/ta/coding-interviews/question-ranking) + +### 题目描述 + +输入一颗二叉树的根节点root和一个整数expectNumber,找出二叉树中结点值的和为expectNumber的所有路径。 + +1. 该题路径定义为从树的根结点开始往下一直到叶子结点所经过的结点 +2. 叶子节点是指没有子节点的节点 +3. 路径只能从父节点到子节点,不能从子节点到父节点 +4. 总节点数目为n + +![image-20220301123709450](https://oss.justin3go.com/blogs/image-20220301123709450.png) + +```javascript +输入:{10,5,12,4,7},22 +返回值:[[10,5,7],[10,12]] +说明:返回[[10,12],[10,5,7]]也是对的 +``` + +```javascript +function TreeNode(x) { + this.val = x; + this.left = null; + this.right = null; +} +``` + +### 我的解法 + +在解这道题时,题目中有两点值得注意: + +- 要求的是根到叶子节点 +- 和为给定数 + +显然,这是一道深度优先遍历出所有满足条件的路径 + +这里,我使用了`cur`来存储当前的累加和,使用了`arr`来存储当前的路径信息; + +这两个的区别就是我们今天的重点: + +- `cur`为值变量,它是保存在栈之中的,作为参数传入函数中,函数修改了该值也不会对当前作用域的`cur`造成影响; + +- ```javascript + { + let cur = 0; + ((cur)=>{cur = 1})(cur); + console.log(cur); // 1; + } + ``` + +- `arr`作为引用变量,它是保存在堆里面,作为参数传入函数中时,它是传入的指针,所以修改的值也会影响到所有指向该对象的指针,这个对象此时只有一个; + +- ```javascript + { + const arr = [1,2,3]; + ((arr)=>{arr.push(4)})(arr); + console.log(arr); // [ 1, 2, 3, 4 ] + } + ``` + +所以我们要想`arr`在函数中修改不会影响到外部作用域,可以将其复制,然后再传入函数中,这时候对象就有两个了; + +```javascript +{ + const arr = [1,2,3]; + ((arr)=>{arr.push(4)})([...arr]); // 这里使用ES6的语法进行复制 + console.log(arr); // [ 1, 2, 3 ] +} +``` + +然后我们将其结合递归,递归其实也是内部有一个自己的函数,然后就是需要注意一下结束条件是如何返回的,下面就是该题的解法,该注释都注释了,如有问题请在评论区提出: + +```javascript +// 题解 +function FindPath(root, expectNumber){ + let res = []; // 存储符合该题条件的路径 + (function Dfs(root, cur, arr){ + if(!root) return; // root为空时 + cur += root.val; // 加上当前节点的值 + arr.push(root.val); // 保存当前节点 + if(!root.left && !root.right && cur === expectNumber){ + res.push(arr); // 符合条件的路径 + } + Dfs(root.left, cur, [...arr]); // 遍历左子树 + Dfs(root.right, cur, [...arr]); // 遍历右子树 + })(root, 0, []); + return res; +} +``` + +当存储函数栈帧时,cur和arr会在之前的情况下添加节点,当弹出栈帧时,cur和arr会在之前的情况减少一个节点,因为返回到上一作用域了。 + + + diff --git "a/docs/\345\215\232\345\256\242/2022/02/04git\345\270\270\347\224\250\346\223\215\344\275\234.md" "b/docs/\345\215\232\345\256\242/2022/02/04git\345\270\270\347\224\250\346\223\215\344\275\234.md" new file mode 100644 index 0000000..e6af45e --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2022/02/04git\345\270\270\347\224\250\346\223\215\344\275\234.md" @@ -0,0 +1,227 @@ +# git常用操作 + +## 同步master ++ 而如果feat分支有两个提交,然后直接`git rebase master`,就有可能需要处理两次冲突(假设master分支提交的与feat提交的在同一份文件中),`git add .`,`git rebase --continue`, + +## 合并多个commit ++ git log --oneline ++ git rebase -i commitHash :`commitHash`是commitID,是需要合并的commit的前一个commit节点的ID ++ git rebase -i head~2 :合并最近两次提交 ++ 最后记得使用git push -f 强制推送,而不是使用vscode的同步代码,那个会先拉取。 ++ rebase的时候,修改冲突后的提交不是使用commit命令,而是执行rebase命令指定 --continue选项。若要取消rebase,指定 --abort选项。 +## cherry-pick +它的功能是把已经存在的commit进行挑选,然后重新提交。 +(今天我记得就是我有分支被我弄乱了,因为我在开发的过程中同步拉取了远程的代码,所以顺序是我提交-->别人提交-->我提交)这时候,使用check-pick就很好的解决了合并提交记录的问题,当然,最好还是不要在开发分支的过程中同步远程master仓库。 + +例子: +在`master`的基础上,`test`进行了2次提交,`normal`进行了1次提交。现在想把`test`的第2次提交 +(仅仅是第2次提交,不包含第1次提交)和`normal`的第1次提交合并到master分支,直接merge分支是行不通的,这样会把两个分支的全部提交都合并到`master`,用`cherry-pick`即可完美的解决问题, 如果`normal`第一次提交的`SHA-1`值是`9b47dd`,`test`第二次提交的值是`dd4e49`,执行如下命令即可把这两个提交合并到`master` + +```sh +git cherry-pick 9b47dd dd4e49 +``` + +如果有冲突,则需要修改冲突文件,然后添加修改文件到暂存区,命令如下: + +```sh +git add main.js +``` + +最后执行 + +```sh +git cherry-pick --continue +``` +cherry-pick后 + +最后要说明的是: + +- 执行完`git cherry-pick --continue`后不需要commit了,该命令会自动提交 +- `git cherry-pick --abort`可以放弃本次`cherry-pick` +- `git cherry-pick 9b47dd dd4e49`和`git cherry-pick dd4e49 9b47dd`这两个的结果可能会**不一样**,**顺序很重要** + +## 其他 ++ git amend:修改同一个分支最近提交的注解和内容 ++ 在revert可以取消指定的提交内容。使用后面要提到的rebase -i或reset也可以删除提交。但是,不能随便删除已经发布的提交,这时需要通过revert创建要否定的提交。 ++ 在reset可以遗弃不再使用的提交。执行遗弃时,需要根据影响的范围而指定不同的模式,可以指定是否复原索引或工作树的内容。 ++ 在rebase指定i选项,您可以改写、替换、删除或合并提交。 + +## 优秀文章 +- [猴子都能懂的git入门](https://backlog.com/git-tutorial/cn/stepup/stepup1_5.html) +- [如何优雅解决git 中冲突](https://juejin.cn/post/7064134612129644558) +- [Git提交历史的修改删除合并](https://juejin.cn/post/6844903521993621511) +- [使用git rebase合并多次commit](https://juejin.cn/post/6844903600976576519) +- **[前端架构师的 git 功力,你有几成火候?](https://juejin.cn/post/7024043015794589727)** + +## 摘录 +总结下合并规则: + +- develop -> (merge) -> dev-* +- dev-* -> (cherry-pick) -> develop +- develop -> (rebase) -> staging +- staging -> (rebase) -> release + +### 为什么合并到 develop 必须用 cherry-pick? + +使用 merge 合并,如果有冲突,会产生分叉;`dev-*` 分支多而杂,直接 merge 到 develop 会产生错综复杂的分叉,难以理清提交进度。 + +而 cherry-pick 只将需要的 commit 合并到 develop 分支上,且不会产生分叉,使 git 提交图谱(git graph)永远保持一条直线。 + +再有,模块开发分支完成后,需要将多个 commit 合为一个 commit,再合并到 develop 分支,避免了多余的 commit,这也是不用 merge 的原因之一。 + +### 为什么合并到 staging/release 必须用 rebase? + +rebase 译为变基,合并同样不会产生分叉。当 develop 更新了许多功能,要合并到 staging 测试,不可能用 cherry-pick 一个一个把 commit 合并过去。因此要通过 rebase 一次性合并过去,并且保证了 staging 与 develop 完全同步。 + +release 也一样,测试通过后,用 rebase 一次性将 staging 合并过去,同样保证了 staging 与 release 完全同步。 +### 误操作的撤回方案 + +开发中频繁使用 git 拉取推送代码,难免会有误操作。这个时候不要慌,git 支持绝大多数场景的撤回方案,我们来总结一下。 + +撤回主要是两个命令:`reset` 和 `revert` + +#### git reset + +reset 命令的原理是根据 `commitId` 来恢复版本。因为每次提交都会生成一个 commitId,所以说 reset 可以帮你恢复到历史的任何一个版本。 + +> 这里的版本和提交是一个意思,一个 commitId 就是一个版本 + +reset 命令格式如下: + +```sh +$ git reset [option] [commitId] +复制代码 +``` + +比如,要撤回到某一次提交,命令是这样: + +```sh +$ git reset --hard cc7b5be +复制代码 +``` + +上面的命令,commitId 是如何获取的?很简单,用 `git log` 命令查看提交记录,可以看到 commitId 值,这个值很长,我们取前 7 位即可。 + +这里的 option 用的是 `--hard`,其实共有 3 个值,具体含义如下: + +- `--hard`:撤销 commit,撤销 add,删除工作区改动代码 +- `--mixed`:默认参数。撤销 commit,撤销 add,还原工作区改动代码 +- `--soft`:撤销 commit,不撤销 add,还原工作区改动代码 + +这里要格外注意 `--hard`,使用这个参数恢复会删除工作区代码。也就是说,如果你的项目中有未提交的代码,使用该参数会直接删除掉,不可恢复,慎重啊! + +除了使用 commitId 恢复,git reset 还提供了恢复到上一次提交的快捷方式: + +```sh +$ git reset --soft HEAD^ +复制代码 +``` + +`HEAD^` 表示上一个提交,可多次使用。 + +其实平日开发中最多的误操作是这样:刚刚提交完,突然发现了问题,比如提交信息没写好,或者代码更改有遗漏,这时需要撤回到上次提交,修改代码,然后重新提交。 + +这个流程大致是这样的: + +```sh +# 1. 回退到上次提交 +$ git reset HEAD^ +# 2. 修改代码... +... +# 3. 加入暂存 +$ git add . +# 4. 重新提交 +$ git commit -m 'fix: ***' +``` + +针对这个流程,git 还提供了一个更便捷的方法: + +```sh +$ git commit --amend +``` + +这个命令会直接修改当前的提交信息。如果代码有更改,先执行 `git add`,然后再执行这个命令,比上述的流程更快捷更方便。 + +reset 还有一个非常重要的特性,就是**真正的后退一个版本**。 + +什么意思呢?比如说当前提交,你已经推送到了远程仓库;现在你用 reset 撤回了一次提交,此时本地 git 仓库要落后于远程仓库一个版本。此时你再 push,远程仓库会拒绝,要求你先 pull。 + +如果你需要远程仓库也后退版本,就需要 `-f` 参数,强制推送,这时本地代码会覆盖远程代码。 + +注意,`-f` 参数非常危险!如果你对 git 原理和命令行不是非常熟悉,切记不要用这个参数。 + +那撤回上一个版本的代码,怎么同步到远程更安全呢? + +方案就是下面要说的第二个命令:`git revert` + +#### git revert + +revert 与 reset 的作用一样,都是恢复版本,但是它们两的实现方式不同。 + +简单来说,reset 直接恢复到上一个提交,工作区代码自然也是上一个提交的代码;而 revert 是新增一个提交,但是这个提交是使用上一个提交的代码。 + +因此,它们两恢复后的代码是一致的,区别是一个新增提交(revert),一个回退提交(reset)。 + +正因为 revert 永远是在新增提交,因此本地仓库版本永远不可能落后于远程仓库,可以直接推送到远程仓库,故而解决了 reset 后推送需要加 `-f` 参数的问题,提高了安全性。 + +说完了原理,我们再看一下使用方法: + +```sh +$ git revert -n [commitId] +复制代码 +``` + +掌握了原理使用就很简单,只要一个 commitId 就可以了。 + +### Tag 与生产环境 + +git 支持对于历史的某个提交,打一个 tag 标签,常用于标识重要的版本更新。 + +目前普遍的做法是,用 tag 来表示生产环境的版本。当最新的提交通过测试,准备发布之时,我们就可以创建一个 tag,表示要发布的生产环境版本。 + +比如我要发一个 `v1.2.4` 的版本: + +```sh +$ git tag -a v1.2.4 -m "my version 1.2.4" +复制代码 +``` + +然后可以查看: + +```sh +$ git show v1.2.4 + +> tag v1.2.4 +Tagger: ruims <2218466341@qq.com> +Date: Sun Sep 26 10:24:30 2021 +0800 + +my version 1.2.4 +复制代码 +``` + +最后用 git push 将 tag 推到远程: + +```sh +$ git push origin v1.2.4 +复制代码 +``` + +**这里注意**:tag 和在哪个分支创建是没有关系的,tag 只是提交的别名。因此 commit 的能力 tag 均可使用,比如上面说的 `git reset`,`git revert` 命令。 + +当生产环境出问题,需要版本回退时,可以这样: + +```sh +$ git revert [pre-tag] +# 若上一个版本是 v1.2.3,则: +$ git revert v1.2.3 +复制代码 +``` + +在频繁更新,commit 数量庞大的仓库里,用 tag 标识版本显然更清爽,可读性更佳。 + +再换一个角度思考 tag 的用处。 + +上面分支管理策略的部分说过,release 分支与生产环境代码同步。在 CI/CD(下面会讲到)持续部署的流程中,我们是监听 release 分支的推送然后触发自动构建。 + +那是不是也可以监听 tag 推送再触发自动构建,这样版本更新的直观性是不是更好? + diff --git "a/docs/\345\215\232\345\256\242/2022/05/05\345\211\215\347\253\257\347\250\213\345\272\217\345\221\230\346\220\255\345\273\272\350\207\252\345\267\261\347\232\204CodeIDE\357\274\210code-server\346\225\231\347\250\213\357\274\211.md" "b/docs/\345\215\232\345\256\242/2022/05/05\345\211\215\347\253\257\347\250\213\345\272\217\345\221\230\346\220\255\345\273\272\350\207\252\345\267\261\347\232\204CodeIDE\357\274\210code-server\346\225\231\347\250\213\357\274\211.md" new file mode 100644 index 0000000..542f76d --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2022/05/05\345\211\215\347\253\257\347\250\213\345\272\217\345\221\230\346\220\255\345\273\272\350\207\252\345\267\261\347\232\204CodeIDE\357\274\210code-server\346\225\231\347\250\213\357\274\211.md" @@ -0,0 +1,153 @@ +# 前端程序员搭建自己的CodeIDE(code-server教程) +> 偶尔不能在自己电脑上写代码时,用用浏览器敲代码也挺方便;或者用平板刷刷算法题也挺有趣;测试JavaScript某一代码片段也不用在浏览器的控制台上打印输出了; +## 安装code-server +我这里使用的是ubuntu20,大家根据自己的系统下载对应的安装包即可,当然最好跟着我的教程来,这样出错了可能都是我踩过的坑,更容易解决,不然就是自己去折腾吧 + +1. 首先下载code-server +官方地址如下:https://github.com/coder/code-server/releases 我这边根据我的需求下载的是这个: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221029220158.png) +然后两种方式,一种是直接在服务器上下载它,不过我服务器没配vpn,所以我采取的第二种方式,本地下载然后通过某些ssh工具上传服务器即可,都是一样的,结果就是服务器上多了这样一个文件就行,用自己喜欢的方式即可。 + +由于买的云服务器都是我一人使用,不用特别在乎一些用户权限等等,所以接下来的操作为了方便我都是在root用户下操作的,使用的ssh工具是finalshell。 + +不是root的话先切换为root用户 +```sh +sudo su root +``` +然后上传(或直接下载)上述的`code-server-xxx-linux-amd64.tar.gz`文件 + +我这里放在了download文件夹下面`/root/download/code-server`根据你自己的习惯存放即可 + +解压: + +```sh +tar -zxvf code-server-4.8.1-linux-amd64.tar.gz +``` + +然后其实就已经可以运行了(运行的是【code-server-4.8.1-linux-amd64(解压后的文件)/bin/code-server】) + +```sh +./bin/code-server --port 8080 --host 0.0.0.0 --auth password + +``` +`--port`:Code Server运行的端口,可以自己设置 + +`--host 0.0.0.0`:允许任意IP的访问,必须加该字段,否者默认是localhost,这样你就不能在本地访问远程运行的code-server了 + +这里先这样,后续直接在yaml文件里配置这些就不用输入后续这些一长串的参数了 + +然后在浏览器中打开对应的ip:port即可 + +当然,如果使用的云厂商的服务记得配置放行端口,并且如果ubuntu里配置了防火墙也记得放行端口或者关闭防火墙,否者无法访问(ubuntu默认是关闭防火墙的,除非你自己之前配置过,这里就不详细介绍相关命令了,大家可以自行去搜搜相关命令) + +## 额外配置 +运行了上述命令之后,会生成一个默认的config.yaml文件,你可以通过运行后的输出信息得到; + +修改其中的信息 + +```yaml +vim ~/.config/code-server/config.yaml // 一般来说都是在对应用户的这个目录下 +``` +这是我的相关配置 + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221029224316.png) + +这里我同时也配置了https访问,毕竟有些代码来回传输不加密可不行 + +简单说说证书的获取,途径很多,选择自己合适的即可,我这里使用的是阿里云的免费证书: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221029224839.png) + +然后下载其中的证书后上传到服务器中对应的文件夹即可 + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221029224927.png) + +你可以从我上面的`config.yaml`配置中看到我服务器里证书密钥的放置位置,这个完全凭喜好放置。 + +此时你就可以直接输入`./code-server`运行了,使用的就是config里面的默认参数了。 + +## pm2启动&域名解析 + +然后我们将其使用pm2管理起来,或者你直接`nohup &`挂起该进程也可以。 + +这里简单使用pm2管理如下: + +```sh +npm i -g pm2 +echo './code-server' > start_code_server.sh +pm2 start start_code_server.sh +``` +pm2的其他常用命令和其他操作这里就不一一介绍了[官网](https://pm2.keymetrics.io/docs/usage/quick-start/) + +运行后输入`pm2 list`如下 + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221029225931.png) + +之后,你就可以在浏览器中随时访问你的codeIDE了,当然,我还解析了个子域名给ip地址,这个直接在对应的云服务厂商上操作即可(这里不详细介绍域名解析操作,自己在界面点点试试就可以了): + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221029230302.png) + +然后输入ip:port或者自己的域名就可以了 + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221029230553.png) + +## 简单配置vscode +当然,初始不是上述这个界面,还需要对vscode进行一定的配置,这个真就看大家习惯了,自己喜欢什么插件就配置什么插件就行了。 + +我这里暂时只安装了这些插件: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221029231614.png) + +简单摘要几点: + +[官方FAQ](https://coder.com/docs/code-server/latest/FAQ#how-can-i-reuse-my-vs-code-configuration) +### How do I use my own extensions marketplace? + +If you own a marketplace that implements the VS Code Extension Gallery API, you can point code-server to it by setting `$EXTENSIONS_GALLERY`. This corresponds directly with the `extensionsGallery` entry in in VS Code's `product.json`. + +For example, to use the legacy Coder extensions marketplace: + +```bash +export EXTENSIONS_GALLERY='{"serviceUrl": "https://extensions.coder.com/api"}' +``` + +Though you can technically use Microsoft's marketplace in this manner, we strongly discourage you from doing so since this is [against their Terms of Use](https://coder.com/docs/code-server/latest/FAQ#why-cant-code-server-use-microsofts-extension-marketplace). + +For further information, see [this discussion](https://github.com/microsoft/vscode/issues/31168#issue-244533026) regarding the use of the Microsoft URLs in forks, as well as [VSCodium's docs](https://github.com/VSCodium/vscodium/blob/master/DOCS.md#extensions--marketplace). +### How can I reuse my VS Code configuration? + +You can use the [Settings Sync](https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync) extension for this purpose. + +Alternatively, you can also pass `--user-data-dir ~/.vscode` or copy `~/.vscode` into `~/.local/share/code-server` to reuse your existing VS Code extensions and configuration. + +## 安装JavaScript版的jupyter(ijavascript) +为了在jupyter-notebook中使用JavaScript,需要安装对应的Node.js内核,这里我使用的是[ijavascript](https://github.com/n-riesco/ijavascript) + +根据官方文档安装即可,我这里使用的是ubuntu20,node16出现了一定问题,通过该issue中的回答即可解决:https://github.com/n-riesco/ijavascript/issues/270 + +主要就是要先安装`libzmq3-dev` + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221029231956.png) + +目前我只遇到这一个问题,如果大家有其他问题,自行搜索和查看下issue中的其他回答吧 + +最后你应该就能得到我上述的页面了 + +![](https://oss.justin3go.com/blogs/demo.gif) + +## 详细视频教程 + + + diff --git "a/docs/\345\215\232\345\256\242/2022/05/06\347\216\251\350\275\254vitepress.md" "b/docs/\345\215\232\345\256\242/2022/05/06\347\216\251\350\275\254vitepress.md" new file mode 100644 index 0000000..a8241e0 --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2022/05/06\347\216\251\350\275\254vitepress.md" @@ -0,0 +1,284 @@ +# 玩转vitepress +当初1月份的时候为了后续春招求职,就使用`vitepress`搭建了一个个人网站,然后把自己本地的一些md文件整理了发布在了上面,不过当时vitepress还未发布正式版本,还是`0.22.x`这样的版本, + +所以其实有很多不满意的地方,比如侧边栏折叠之前没有,明暗模式之前没有,单篇文章的大纲好像也没有,侧边栏在不同tab下有问题,这些我不太确定,可能功能是有的,但是官方文档上没更新罢了 + +但现在我偶然发现不知不觉`vitepress`也发布了正式版本了,虽然目前仍然是`alpha`版本,但是之前诟病的地方全都没有了,文档上也清晰了非常多,我瞬间就兴奋起来了,赶紧拿之前那个旧网站试试,花了几天时间,将其升级到`1.xx`版本了 + +## 迁移 + +如果你和我一样,是从`0.xx`版本过来的,可以看这个[这里](https://vitepress.vuejs.org/guide/migration-from-vitepress-0) + +## 开始 + +你可以看[这里](https://vitepress.vuejs.org/guide/getting-started)了解最详细的开始操作,这里对于快速启动一个站点就不过多赘述,官方已经讲得很详细了,这边只讲讲我觉得重要的部分(在此之前你应该至少根据官方文档启动你的站点)。 +其中最重要的就是你需要理解文件中的`markdown`文件对应的是什么,官方文档中也有解释:每个`markdown`文件都会解析成一`Html`文件,同时文件目录对应的就是你该文件在站点上的访问路径,比如你的项目根目录就是对应根路径。 + +如: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221031092013.png) + +这是该文件在我项目中的位置,对应的它会被渲染为这样: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221031092136.png) + +所以你只需要将你本地的`markdown`文件整理上传即可,然后你就可以根据路径访问对应的文件了,不过需要注意的是,你的md文件中不能包含`dead link`就是你的链接指向的文件根本不存在,这样会导致渲染失败,通常的方法就是根据提示去删除该路径,但如果你不想一一去删除,也可以直接在构建时忽略(不推荐)[方法](https://vitepress.vuejs.org/config/app-configs#ignoredeadlinks) + +然后使用`vitepress`一个最重要的优势就是`vitepress`可以无缝编写vue组件使用,简单说说它的渲染过程,vitepress首先会将`markdown`文件渲染为html文件,然后再交给vue编译器渲染,所以目前如果你在vue里面使用`markdown`的语法是不会被渲染的,只会被渲染为纯文本(不知道未来会不会支持),目前的解决方法就是vue组件里面放置选然后的html即可,可能这样说有点晦涩,举个例子: + +如果我们使用`v-for`去批量生成某一字段时,下面这样是不可以的: + +```html +// xxx.md +--- +sidebar: false +--- + +# 图集in北京 +
+   ## {{item[0].title}} +  +
+``` + +上面的`## { {item[0].title} }`只会经过vue编译器的渲染,并不会经过`markdown`解析器的渲染,所以最终渲染结果相信你也猜到了,就是`## text`,而并不是对应的标题样式,解决方法如下: + +```html +// xxx.md +--- +sidebar: false +--- + +# 图集in北京 +

+    {{item[0].title}} +   

+
+``` + +希望以后能加个比如`v-markdown`这样的包裹标签让里面的内容再经过一次markdown渲染... + +## 生成主页 +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221031095921.png) +给你的站点来一个漂亮的主页,你可以直接使用`vitepress`给的主页模板 + +[详细配置](https://vitepress.vuejs.org/guide/theme-home-page) + +[layout的三种情况](https://vitepress.vuejs.org/guide/theme-layout) + +## 简单配置 + +上述的`markdown`文件渲染都是`vitepress`自动识别渲染的,并不需要额外的配置,所以站点的`nav`、`sidebar`、`aside`这些需要我们自己配置一下: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221031094018.png) + +接下来的配置都是在`/.vitepress/config.js`中操作的,这也是我们最重要的一个文件 + +### base + +```ts +module.exports = { +    title: "XXXX",// 网站标题 +    description: 'XXXX', //网站描述 +    // base: '/', +    lang: 'zh-CH', //语言 +    repo: 'vuejs/vitepress', +    head: [ +        // 改变title的图标 +        [ +            'link', +            { +                rel: 'icon', +                href: 'https://oss.justin3go.com/justin3goAvatar.ico', +            }, +        ] +    ], +} +``` +### nav + +```ts +module.exports = { + // ... other config +    // 主题配置 +    themeConfig: { +        //   头部导航 +        nav: [ +            { text: '首页', link: '/' }, +            { text: '知识库', link: '/知识库/' }, +            { text: '博客', link: '/博客/' }, +            { +                text: '图集', items: [ +                    { text: '重庆', link: '/图集/重庆' }, +                    { text: '北京', link: '/图集/北京' }, +                    { text: '校园', link: '/图集/校园' }, +                ] +            } +        ], + } +} +``` +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221031095315.png) +### aside + +```ts +module.exports = { + // ... other config +    // 主题配置 +    themeConfig: { +        outline: [2, 4], // 识别

-

的标题 +        outlineTitle: '大纲', // aside第一行显示的文本 + } +} +``` +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221031095332.png) +### sidebar + +```ts +module.exports = { + // ... other config +    // 主题配置 +    themeConfig: { +                //   侧边导航 + +        sidebar: { + +            '/知识库/': [], // 根据不同的路径前缀显示不同的侧边栏 + +            '/博客/': [ +                { +                    text: '2022', +                    link: '/博客/2022/', +                    collapsible: true, // 可折叠 +                    items: [ +                        { text: '都2022年了,还是得学圣杯布局与双飞翼布局', link: '/博客/2022/01都2022年了,还是得学圣杯布局与双飞翼布局' }, +                        { text: 'TypeScript入门', link: '/博客/2022/02TypeScript入门' }, +                        { text: '这道题原来可以用到JS这么多知识点!', link: '/博客/2022/03这道题原来可以用到JS这么多知识点!' }, +                        { text: 'git常用操作', link: '/博客/2022/04git常用操作' }, +                        { text: '前端程序员搭建自己的CodeIDE(code-server教程)', link: '/博客/2022/05前端程序员搭建自己的CodeIDE(code-server教程)' }, +                    ] +                }, +                { +                    text: '2021', +                    link: '/博客/2021/', +                    collapsible: true, +                    items: [ +                        { text: 'scrapy爬虫详解', link: '/博客/2021/01scrapy爬虫详解' }, +                        { text: 'TFIDF计算的学习', link: '/博客/2021/02TFIDF计算的学习' }, +                        { text: '操作系统内存分配模拟程序', link: '/博客/2021/03操作系统内存分配模拟程序' }, +                        { text: '散列表实现查找', link: '/博客/2021/04散列表实现查找' }, +                    ] +                }, +                { +                    text: '2020', +                    link: '/博客/2020/', +                    collapsible: true, +                    items: [ +                        { text: 'Java迷宫', link: '/博客/2020/01Java迷宫' }, +                        { text: '使用anaconda中的Prompt配置虚拟环境的常用命令', link: '/博客/2020/02使用anaconda中的Prompt配置虚拟环境的常用命令' }, +                    ] +                }, +            ], +        }, + } +} +``` + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221031095345.png) +### 其他 +配置项有很多,大家可以直接参考官方文档选择自己需要的配置项 + +[参考1](https://vitepress.vuejs.org/guide/theme-introduction) + +[参考2](https://vitepress.vuejs.org/config/introduction) + +常用的有这些: + +```ts +        socialLinks: [ +            { icon: 'github', link: 'https://github.com/Justin3go' }, +            { +                icon: { +                    svg: '' +                }, link: 'https://space.bilibili.com/434542518' +            }, +        ], +        footer: { +            message: 'Released under the MIT License.', +            copyright: 'Copyright© 2021-2022 Justin3go-渝ICP备2021006879号' +        }, +        docFooter: { +            prev: '上一页', +            next: '下一页' +        }, +``` +## 样式 +细心的同学应该都能发现我站点的字体是不一样的,还有一些其他小细节的修改,那么我们该如何覆盖原有的样式呢? + +`vitepress`同样也提供了相当丰富的可覆盖样式供我们自定义: + +[参考链接](https://vitepress.vuejs.org/guide/theme-introduction#extending-the-default-theme) + +## 使用vue组件 +[参考链接](https://vitepress.vuejs.org/guide/using-vue) +### many-pictures +为了存放多张图片,我自己简单编写了一个vue组件在`vitepress`中使用,你可以在[github链接](https://github.com/Justin3go/many-pictures)中看到如何在vitepress中使用vue组件的详细例子 +### gitalk +具体用法可以参考[官方文档](https://github.com/gitalk/gitalk) +简单来说它就是先要获取你github的key,即可操作你github的一些东西,然后它就是将评论数据存储你建立的某个仓库的issue里,下面是我使用`gitalk`在`vitepress`中的配置 +```vue +// docs\.vitepress\theme\components\Comment.vue + + + +``` + +```js +// .vitepress/theme/index.js +import DefaultTheme from 'vitepress/theme'; +import './custom.css'; +import Comment from "./components/Comment.vue"; +import 'many-pictures/es/style.css' +import manyPictures from 'many-pictures'; + +export default { +  ...DefaultTheme, +  enhanceApp({ app, router, siteData }) { +    // 注册组件 +    app.component("Comment", Comment); +    app.use(manyPictures); +  }, +}; +``` + +## 其他 +- 静态文件的处理同样重要,不过我都是上传了对象存储的,所以就没有对这一部分进行处理,大家如果有需要可以参考这个[链接](https://vitepress.vuejs.org/guide/asset-handling) +- vitepress中使用的`markdown`渲染器是`markdown-it`,所以你可以使用其生态下的几乎所有插件,[参考链接](https://vitepress.vuejs.org/guide/markdown#advanced-configuration) +- 部署就老生常谈了,就没单独拿出来细说,可以参考[链接](https://vitepress.vuejs.org/guide/deploying) +## 最后 +欢迎大家访问我的个人博客[jutin3go.com](https://justin3go.com/) +- 知识库:内容为整理所得,还未将本地的数据整理完,以及一些非markdown文件,如书籍笔记、手写笔记等整理工作量较大 +- 博客:偶尔花一点时间写的一篇文章,如这篇文章就是这个类别 +- 图集:满足自己拍照的爱好 + + diff --git "a/docs/\345\215\232\345\256\242/2022/05/07IntersectionObserver\345\256\236\347\216\260\346\250\252\347\253\226\346\273\232\345\212\250\350\207\252\351\200\202\345\272\224\346\207\222\345\212\240\350\275\275.md" "b/docs/\345\215\232\345\256\242/2022/05/07IntersectionObserver\345\256\236\347\216\260\346\250\252\347\253\226\346\273\232\345\212\250\350\207\252\351\200\202\345\272\224\346\207\222\345\212\240\350\275\275.md" new file mode 100644 index 0000000..70e8926 --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2022/05/07IntersectionObserver\345\256\236\347\216\260\346\250\252\347\253\226\346\273\232\345\212\250\350\207\252\351\200\202\345\272\224\346\207\222\345\212\240\350\275\275.md" @@ -0,0 +1,116 @@ +# IntersectionObserver实现横竖滚动自适应懒加载 +这几天使用`vitepress`[编写个人网站的时候](https://juejin.cn/post/7160499086271971364),编写了一个存放图片的组件,理所当然的,这个组件应该实现图片懒加载,并且由于这个组件存放的图片可以是非常多的,所以实现懒加载就显得极为重要了,但是由于我实现这个组件的方式有点特别,是用盒子的背景图来存放图片的,并且支持横向滚动,所以大致搜索了下了解到了`IntersectionObserver`这个api非常适合我用来实现这个功能(缺点就是兼容性可能差点); +## IntersectionObserver简要介绍 +直接来到[MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver) + +介绍的比较简单,当然这个api使用起来也简单,但是实现的功能却可以非常丰富,这篇文章就仅仅介绍其可以实现的一种功能就是横竖滚动懒加载。 + +这个api的原理就是观察目标元素与其祖先元素或顶级文档视窗交叉状态,所以在我的例子中竖向滚动懒加载就是观察组件容器与根元素的交叉状态,而横向滚顶就是观察组件图片项与组件容器的交叉状态,这里简单提一下思路,后续通过代码详细介绍具体实现。 + +回到这个api,要想使用这个api,首先我们得使用它提供得构造器创建一个`IntersectionObserver`对象 +```js +const io = new IntersectionObserver(callback, options) +``` +这个对象接收两个参数: +- callback: 当其监听到目标元素的可见部分穿过了一个或多个阈 (thresholds)时,会执行指定的回调函数。 +- options: 一些选项,比如指定root是谁 + +然后这个对象有如下几个方法供我们使用: + +```js +IntersectionObserver.disconnect() // 使IntersectionObserver对象停止监听工作。 +IntersectionObserver.observe() // 使IntersectionObserver开始监听一个目标元素。 +IntersectionObserver.takeRecords() // 返回所有观察目标的IntersectionObserverEntry对象数组。 +IntersectionObserver.unobserve() // 使IntersectionObserver停止监听特定目标元素。 +``` + +下面是官方给的一个示例: + +```js +// 无限滚动的功能(footer出现在了视口中就加载10个项) +var intersectionObserver = new IntersectionObserver(function(entries) { + // If intersectionRatio is 0, the target is out of view + // and we do not need to do anything. + if (entries[0].intersectionRatio <= 0) return; + + loadItems(10); + console.log('Loaded new items'); +}); +// start observing +intersectionObserver.observe(document.querySelector('.scrollerFooter')); + +``` + +其中`entries`是一个数组,每个成员都是一个[`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry)对象,我们每次`.observe(element)`时,`entries`里就会多一个对应的项,相当于如果我们要观察多个目标元素,就需要在回调函数里进行统一处理。 +介绍两个我们要用的`IntersectionObserverEntry`对象属性: +- `target`:被观察的目标元素,是一个 DOM 节点对象 +- `intersectionRatio`:目标元素的可见比例,即`intersectionRect`占`boundingClientRect`的比例,完全可见时为`1`,完全不可见时小于等于`0` + +## many-pictures组件介绍 +这是我自己编写的一个小组件,用来存放多张图片的容器,下面是它的样子 + +![demo-many-pictures](https://oss.justin3go.com/blogs/demo-many-pictures.gif) + +目前在多个容器同时使用的话动画上会有卡顿,后续优化,但不妨碍我们讲解懒加载这个功能的实现。 + +可以看到如果我们在同一个页面中使用多次这个组件,或者这个组件放在其他内容的下面,初始加载消耗的实现就会特别大,比如我们这个页面使用了100个这个容器,就需要在页面初始时加载这个用户看不见的容器,这显然是不可行的,这就是我们平常所见的图片懒加载 + +如果我们这里使用`windows.onscroll`的那种懒加载方法的话,虽然可以,但是这是个全局方法,我并不是很想在我这个局部组件中使用它,而且它还需要进行节流操作,每次还要去获取图片距离顶部的高度,视窗的高度,滚动的距离等等,比较麻烦 + +而使用这个新的api的话一切都迎刃而解了... + +其次,我们可以看到每个容器中也可以容纳多张图片,当这个容器中容纳了100张图片的时候,在这个容器加载的时候,就需要加载这个容器中包含的所有的100张图片,即使用户看不见右边没显示的图片,这显然是不合理的,所以这就是我们将要做的横向滚动懒加载 + +## 自适应懒加载实现 + +所以,到现在,我们手头上有了`IntersectionObserver`这个工具,然后需求就是实现容器的竖向懒加载和图片的横向懒加载。 + +确定思路:我们首先观察容器与视窗是否有交叉(是否显示给了用户),如果显示了就开始加载这个容器,这个加载容器的意思就是开始观察图片,然后就是观察图片与这个容器是否有交叉,如果有,就加载这个图片 +![](https://oss.justin3go.com/blogs/lazy.png) +思路有了,代码就简单了,下面贴了对应代码的实现,当然你也可以直接访问这个组件实现的github链接查看所有的代码([源码链接](https://github.com/Justin3go/many-pictures)) +```ts +onMounted(() => { +  if (props.lazy) { + // 图片 +    const ioImg = new IntersectionObserver( +      (entries) => { +        entries.forEach((entry) => { +          if (entry.intersectionRatio <= 0) return; // 是否出现在了可视区域 +          const option = entry.target; +          // 图片链接放在这个属性上的 +          const imgUrl = option.getAttribute("data-img"); +          option.setAttribute("style", `background-image: url(${imgUrl})`); +          ioImg.unobserve(option); +        }); +      }, +      { +        root: mark.value, // 横向懒加载 +      } +    ); +    // 容器 +    const ioContainer = new IntersectionObserver((entries) => { +      entries.forEach((entry) => { +        if (entry.intersectionRatio <= 0) return; +        const container = entry.target; +        const list = container.querySelectorAll(".option"); +        list.forEach((item) => { +          ioImg.observe(item); +        }); +        isLoad.value = true; +        ioContainer.unobserve(container); +      }); +    }); +    ioContainer.observe(mark.value); +  } else { +    const list: NodeListOf = mark.value.querySelectorAll(".option"); +    list.forEach((item) => { +      const imgUrl = item.getAttribute("data-img"); +      item.setAttribute("style", `background-image: url(${imgUrl})`); +    }); +  } +}); +``` +最后,效果图如下: +![demo-many-pictures](https://oss.justin3go.com/blogs/demo-many-pictures.gif) + + diff --git "a/docs/\345\215\232\345\256\242/2022/06/08\345\211\215\347\253\257\346\236\204\345\273\272\347\232\204\345\255\246\344\271\240(\345\201\217\345\220\221vite).md" "b/docs/\345\215\232\345\256\242/2022/06/08\345\211\215\347\253\257\346\236\204\345\273\272\347\232\204\345\255\246\344\271\240(\345\201\217\345\220\221vite).md" new file mode 100644 index 0000000..310e654 --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2022/06/08\345\211\215\347\253\257\346\236\204\345\273\272\347\232\204\345\255\246\344\271\240(\345\201\217\345\220\221vite).md" @@ -0,0 +1,302 @@ +# 前端工程化的学习(偏向vite构建工具) + +> 好早就听说了vite,也早就简单的使用并了解了一点,之前在公司实习团队也正在迁移webpack的项目到vite,但我自己却一直没有深入,毕竟还是初级前端工程师,功力还欠缺很多,但最近封装了一个小组件,整个项目不使用脚手架挺难受的,到处参考别人的代码希望能找到组件开发的最佳实践,整个过程举步维艰,所以开始先从vite入手学习一下前端工程化相关的东西了... + +## 为什么需要构建工具 + +摘抄一段`vite`官网对`打包`的描述: + +> 使用工具抓取、处理并将我们的源码模块串联成可以在浏览器中运行的文件 + +现阶段我们基本都不会直接编写可以浏览器上运行的文件,更多的是使用各种新的框架(Vue/React)、语法(TypeScript/less/sass),用这些工具编写出来的代码时不能直接在浏览器上直接运行的,我们需要每次都手动使用不同的解释器/编译器去将用高级语法编写的代码转换为能在浏览器中运行的代码: +![](https://oss.justin3go.com/blogs/%E6%9E%84%E5%BB%BA%E5%B7%A5%E5%85%B7.png) +**所以简单理解构建工具(打包工具)要做的就是这样一件事**:将这条工具链内置,面向开发者透明,避免开发者每次查看效果都要重复机械化地输入不同的命令,除此之外,构建工具还可以使用各种优化工具优化最终生成的文件。 +一般来说,一个构建工具会有以下功能: + +- **模块化支持**:兼容多种模块化规范写法,支持从`node_modules`中引入代码(浏览器本身只识别路径方式的模块导入,`imoprt { forEach } from 'loadsh'`这样直接以名字导入需构建工具识别) +- **框架编译/语法转换**:如:`tsc->lessc->vueComplier` +- **构建产物性能优化**:文件打包、代码压缩、code splitting、tree shaking... +- **开发体验优化**:hot module replacement、跨域解决等... +- ... + 总的来说,构建工具让我们开发人员可以更加关注代码的编写,而非代码的运行。 + +## 五花八门的构建工具 + +市面上常见的构建工具有如下(这里简单说一下各种构建工具的特点,具体展开就太多了,大家感兴趣可以直接去官网看看): + +- grunt:基于配置驱动的,开发者需要做的就是了解各种插件的功能,然后把配置整合到 Gruntfile.js 中,然后就可以自动处理一些需要反复重复的任务,例如代码压缩、编译、单元测试、linting等工作,配置复杂度较高且IO操作较多。 +- gulp:Gulp最大特点是引入了流的概念,同时提供了一系列常用的插件去处理流,流可以在插件之间传递。这使得它本身被设计的非常简单,但却拥有强大的功能,既可以单独完成构建,也可以和其他工具搭配使用 +- webpack:最主流的打包构建工具,兼容覆盖基本所有场景,前端工程化的核心,但相应带来的缺点就是配置繁琐 +- rollup:由于webpack配置繁琐,对于小型项目开发者较不友好,他们更倾向于rollup。其配置简单,易于上手,成为了目前最流行的JS库打包工具 +- esbuild:使用go语言并大量使用了其高并发的特性,速度极快。不过目前Esbuild还很年轻,没有达到1.0版本,并且其打包构建与Rollup类似更关注于JS本身,所以并不适合单独使用在前端项目的生产环境之中 +- parcel:... +- ... +- vite:开发环境基于esmodule规范按需加载,速度极快,具有极佳的开发体验,生产环境底层调用rollup,接下来主要介绍webpack与vite之间的一个对比。 + +其实官网介绍vite的优势已经非常详细了,我自身也没有额外的理解,这里就直接摘要一段官网的话: + +> 当我们开始构建越来越大型的应用时,需要处理的 JavaScript 代码量也呈指数级增长。包含数千个模块的大型项目相当普遍。基于 JavaScript 开发的工具就会开始遇到性能瓶颈,Vite 以 [原生 ESM](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) 方式提供源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理。 + +再贴两张大家可能已经很熟的对比图: +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221103210037.png) +相信大家看了上述官网的摘要差不多已经明白为什么vite在开发环境下启动速度非常快的原因了,主要就是使用了浏览器原生支持的`esmodule`规范,当然还少不了vite本身在这之上做的一些优化,比如[依赖预构建](https://cn.vitejs.dev/guide/dep-pre-bundling.html) +我在一篇文章中看到过这样一个问题:这个思路既然能解决开发启动速度上的问题,为什么webpack不能支持呢? +答: + +- webpack的设计理念就是大而全,它需要兼容不同的模块化,我们的工程既有可能跑在浏览器端,也有可能跑在服务端,所以webpack会将不同的模块化规范转换为独有的一个函数`webpack_require`进行处理,为了做到这一点,它必须一开始就要统一编译转换模块化代码,也就意味着它需要将所有的依赖全部读取一遍; +- 而我们在使用vite项目的时候,就只能使用`esmodule`规范,但项目的依赖仍然可能使用了不同的模块规范,vite会在依赖预构建中处理这一步,将依赖树转换为单个模块并缓存在`/node_modules/.vite`下方便浏览器按需加载,将打包的部分工作交给了浏览器执行,优化了开发体验。而构建交给了`rollup`同样会兼容各种模块化规范... + +总结:**webpack更多的关注兼容性,而vite关注浏览器端的开发体验**,侧重点不一样 + +## vite处理细节 + +> 自身对前端工程化的理解也比较浅,从vite官网文档中可以学到不少前端工程化相关的知识,知识点总结至vite官网,[快速入口](https://cn.vitejs.dev/guide/features.html) + +### 1. 导入路径补全 + +在处理的过程中如果说看到了有非绝对路径或者相对路径的引用, 他则会尝试开启路径补全: + +```js +import _ from "lodash" // 补全前,浏览器并不认识这种裸模块导入 +import _ from "/node_modules/.vite/lodash"; // 补全后,使用依赖预构建处理后的结果 +``` + +### 2. 依赖预构建 + +主要就是为了解网络多包传输的性能问题,官网原话: + +> 一些包将它们的 ES 模块构建作为许多单独的文件相互导入。例如,[`lodash-es` 有超过 600 个内置模块](https://unpkg.com/browse/lodash-es/)!当我们执行 `import { debounce } from 'lodash-es'` 时,浏览器同时发出 600 多个 HTTP 请求!尽管服务器在处理这些请求时没有问题,但大量的请求会在浏览器端造成网络拥塞,导致页面的加载速度相当慢。 + +> 通过**预构建 `lodash-es` 成为一个模块**,我们就只需要一个 HTTP 请求了! + +重写前: + +```js +// a.js +export default function a() {} + +``` + +```js +export { default as a } from "./a.js" +``` + +vite重写以后: + +```js +function a() {} +``` + +顺便解决了以下两个问题: + +- 不同的第三方包会有不同的导出格式,Vite 会将作为 CommonJS 或 UMD 发布的依赖项转换为 ESM +- 对路径的处理上可以直接使用.vite/deps, 方便路径重写 + +> 其他:构建这一步由 [esbuild](http://esbuild.github.io/) 执行,这使得 Vite 的冷启动时间比任何基于 JavaScript 的打包器都要快得多 + +注意:这里都是指的开发环境,生产环境会交给rollup去执行 + +### 3. vite与ts + +vite他天生就对ts支持非常良好, 因为vite在开发时态是基于esbuild, 而esbuild是天生支持对ts文件的转换的[快速入口](https://cn.vitejs.dev/guide/features.html#typescript) + +### 4. 环境变量 + +一个产品可能要经过如下环境: + +1. 开发环境 +2. 测试环境 +3. 预发布环境 +4. 灰度环境 +5. 生产环境 + 不同的环境使用的数据应该是隔离的,或者是经过处理的,比如小流量环境,很显然,不同环境在一些密钥上的设置上是不同的,环境变量在这时候就尤为重要了,vite中内置了dotenv对环境变量进行处理: + dotenv会自动读取.env文件, 并解析这个文件中的对应环境变量 并将其注入到process对象下(但是vite考虑到和其他配置的一些冲突问题, 他不会直接注入到process对象下) + 配置: + + .env # 所有情况下都会加载 + .env.local # 所有情况下都会加载,但会被 git 忽略 + .env.[mode] # 只在指定模式下加载 + .env.[mode].local # 只在指定模式下加载,但会被 git 忽略 + 然后文件里使用`VITE`前缀的命名变量`VITE_SOME_KEY=123`,可以在`vite.config.ts`中配置`envPrefix: "ENV_"`修改这个前缀 + 使用: + +```js +console.log(import.meta.env.VITE_SOME_KEY) // 123 +console.log(import.meta.env.DB_PASSWORD) // undefined +``` + +其他:为什么vite.config.js可以书写成esmodule的形式(vite明明是运行在服务端的), 这是因为vite他在读取这个vite.config.js的时候会率先node去解析文件语法, 如果发现你是esmodule规范会直接将你的esmodule规范进行替换变成commonjs规范 + +### 5. vite对css的处理 + +#### 基本流程:1. vite在读取到main.js中引用到了Index.css + +2. 直接去使用fs模块去读取index.css中文件内容 +3. 直接创建一个style标签, 将index.css中文件内容直接copy进style标签里 +4. 将style标签插入到index.html的head中 +5. 将该css文件中的内容直接替换为js脚本(方便热更新或者css模块化), 同时设置Content-Type为js 从而让浏览器以JS脚本的形式来执行该css后缀的文件 + + +#### 处理重复类名 + + + +全部都是基于node + +1. module.css (module是一种约定, 表示需要开启css模块化) +2. 他会将你的所有类名进行一定规则的替换(将footer 替换成 _footer_i22st_1) +3. 同时创建一个映射对象{ footer: "_footer_i22st_1" } +4. 将替换过后的内容塞进style标签里然后放入到head标签中 (能够读到index.html的文件内容) +5. 将componentA.module.css内容进行全部抹除, 替换成JS脚本 +6. 将创建的映射对象在脚本中进行默认导出 + +#### config参考 + +[快速入口](https://cn.vitejs.dev/config/shared-options.html#css-modules) + +```js +// 摘自https://github.com/passerecho/vite- +    css: { // 对css的行为进行配置 +        // modules配置最终会丢给postcss modules +        modules: { // 是对css模块化的默认行为进行覆盖 +            localsConvention: "camelCaseOnly", // 修改生成的配置对象的key的展示形式(驼峰还是中划线形式) +            scopeBehaviour: "local", // 配置当前的模块化行为是模块化还是全局化 (有hash就是开启了模块化的一个标志, 因为他可以保证产生不同的hash值来控制我们的样式类名不被覆盖) +            // generateScopedName: "[name]_[local]_[hash:5]" // https://github.com/webpack/loader-utils#interpolatename +            // generateScopedName: (name, filename, css) => { +            //     // name -> 代表的是你此刻css文件中的类名 +            //     // filename -> 是你当前css文件的绝对路径 +            //     // css -> 给的就是你当前样式 +            //     console.log("name", name, "filename", filename, "css", css); // 这一行会输出在哪??? 输出在node +            //     // 配置成函数以后, 返回值就决定了他最终显示的类型 +            //     return `${name}_${Math.random().toString(36).substr(3, 8) }`; +            // } +            hashPrefix: "hello", // 生成hash会根据你的类名 + 一些其他的字符串(文件名 + 他内部随机生成一个字符串)去进行生成, 如果你想要你生成hash更加的独特一点, 你可以配置hashPrefix, 你配置的这个字符串会参与到最终的hash生成, (hash: 只要你的字符串有一个字不一样, 那么生成的hash就完全不一样, 但是只要你的字符串完全一样, 生成的hash就会一样) +            globalModulePaths: ["./componentB.module.css"], // 代表你不想参与到css模块化的路径 +        }, +        preprocessorOptions: { // key + config key代表预处理器的名 +            less: { // 整个的配置对象都会最终给到less的执行参数(全局参数)中去 +                // 在webpack里就给less-loader去配置就好了 +                math: "always", +                globalVars: { // 全局变量 +                    mainColor: "red", +                } +            }, +        }, +        devSourcemap: true, +    }, +``` + +### 6. 静态资源 + +服务时引入一个静态资源会返回解析后的公共路径: + +``` +import imgUrl from './img.png' +document.getElementById('hero-img').src = imgUrl +``` + +例如,`imgUrl` 在开发时会是 `/img.png`,在生产构建后会是 `/assets/img.2d8efhg.png`。 + +行为类似于 Webpack 的 `file-loader`。区别在于导入既可以使用绝对公共路径(基于开发期间的项目根路径),也可以使用相对路径。 + +#### 为什么要使用hash + +浏览器是有一个缓存机制 静态资源名字只要不改, 那么他就会直接用缓存的 +刷新页面--> 请求的名字是不是同一个 --> 读取缓存 --> 所以我们要尽量去避免名字一致(每次开发完新代码并构建打包时) + +#### 1. 显式 URL 引入 + +未被包含在内部列表或 `assetsInclude` 中的资源,可以使用 `?url` 后缀显式导入为一个 URL。这十分有用,例如,要导入 [Houdini Paint Worklets](https://houdini.how/usage) 时: + +```js +import workletURL from 'extra-scalloped-border/worklet.js?url' +CSS.paintWorklet.addModule(workletURL) +``` + +#### 2. 将资源引入为字符串 + +资源可以使用 `?raw` 后缀声明作为字符串引入。 + +```js +import shaderString from './shader.glsl?raw' +``` + +比如svg文件如果我们以url的方式导入文件,则相当于导入一张图片,只能对其进行图片的相关操作,如果我们想要对其进行svg相关的操作,我们则需要使用`?raw`的方式导入: + +```js +import svgIcon from "./assets/svgs/fullScreen.svg?url"; // 这种是以图片的方式加载svg,无其他特殊操作 +import svgRaw from "./assets/svgs/fullScreen.svg?raw"; // 加载svg的源文件,这种方式的加载可以做到修改svg的颜色等操作 + +console.log("svgIcon", svgIcon, svgRaw); +document.body.innerHTML = svgRaw; + +const svgElement = document.getElementsByTagName("svg")[0]; + +svgElement.onmouseenter = function() { +    // 不是去改他的background 也不是color +    // 而是fill属性 +    this.style.fill = "red"; +} + +// 第一种使用svg的方式 +// const img = document.createElement("img"); +// img.src = svgIcon; + +// document.body.appendChild(img); +// 第二种加载svg的方式 +``` + +#### 3. 导入脚本作为 Worker + +脚本可以通过 `?worker` 或 `?sharedworker` 后缀导入为 web worker。 + +```js +// 在生产构建中将会分离出 chunk +import Worker from './shader.js?worker' +const worker = new Worker() +``` + +```js +// sharedworker +import SharedWorker from './shader.js?sharedworker' +const sharedWorker = new SharedWorker() +``` + +```js +// 内联为 base64 字符串 +import InlineWorker from './shader.js?worker&inline' +``` + +[快速入口](https://cn.vitejs.dev/guide/assets.html) + +## 性能优化 + +1. 代码逻辑上的优化,如: + 1. 使用`lodash`工具中的防抖、节流而非自己编写;数组数据量大时,也可以使用`lodash`中的`forEach`方法等等 + 2. `for(let i = 0; i < arr.length; i++){}`替换为`for(let i = 0, len = arr.length; i < len; i++)`这样只用通过作用域链获取一次父作用域中的`arr`变量 + 3. ... +2. 构建优化(构建工具关注的事):体积优化->压缩、treeshaking、图片资源压缩、cdn加载、分包... +3. ... + +**其中分包知识我第一次接触到,这里记录一下:** +主要是为了配合浏览器中的缓存策略 + +- 假设这样一个场景,我们使用`lodash`中的`forEach`函数编写了`console('1')`,最终打包后的代码如果不分包则会将`lodash`中的相关实现和`console('1')`合并为一个文件传给浏览器; +- 而我们的业务代码经常变化,比如`console('1')`-->`console('2')`这时候我们仍然需要将`lodash`中的相关实现和`console('1')`合并为一个文件传给浏览器; +- 但显然`lodash`中的代码实现并没有更改,浏览器直接使用以前的就可以了 +- 所以分包就是把一些不会经常更新的文件,进行单独打包处理为一个文件,[配置参考](https://cn.vitejs.dev/guide/build.html#chunking-strategy) + ![](https://oss.justin3go.com/blogs/%E5%88%86%E5%8C%85%E4%BC%98%E5%8A%BF.png) + +## 最后 + +前端工程化我也是最近开始学习,如有理解错误希望各位大佬不吝赐教 + +## 参考文章 + +- https://cn.vitejs.dev/ +- https://segmentfault.com/a/1190000040135876 +- https://juejin.cn/post/7085613927249215525#heading-15 +- https://github.com/passerecho/vite- +- https://css-tricks.com/comparing-the-new-generation-of-build-tools/ +- https://juejin.cn/post/7085613927249215525#heading-2 + diff --git "a/docs/\345\215\232\345\256\242/2022/06/09Node\346\250\241\345\235\227\350\247\204\350\214\203\345\217\212\346\250\241\345\235\227\345\212\240\350\275\275\346\234\272\345\210\266.md" "b/docs/\345\215\232\345\256\242/2022/06/09Node\346\250\241\345\235\227\350\247\204\350\214\203\345\217\212\346\250\241\345\235\227\345\212\240\350\275\275\346\234\272\345\210\266.md" new file mode 100644 index 0000000..5914337 --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2022/06/09Node\346\250\241\345\235\227\350\247\204\350\214\203\345\217\212\346\250\241\345\235\227\345\212\240\350\275\275\346\234\272\345\210\266.md" @@ -0,0 +1,174 @@ +# Node模块规范及模块加载机制 + +> 这是重新阅读《深入浅出NodeJS》的相关笔记,这次阅读发现自己依旧收获很多,而第一次阅读的东西也差不多忘记完了,所以想着这次过一遍脑子,用自己的理解输出一下,方便记忆以及以后回忆... + +历史原因,`JavaScript`以前是没有模块机制的,这对于`node`来说想要编写一个大型项目是很难的,所以`node`采用了社区提出的`CommmonJS`规范 +## 认识CommonJS + +> 这里主要介绍的是大家常见的`JavaScript`文件模块,其他的将在后续章节介绍 + +`CommonJS`对模块的定义非常简单,主要分为模块引用、模块定义和模块标识三个部分: + +比如我们有如下很常见的代码: + +```js +const math = require('math') +``` + +- 模块引用:`const math`中的`math`就是模块引用 +- 模块标识:`require('math')`中的`math`就是模块标识,必须是以小驼峰命名的字符串或者路径 +- 模块定义:简单理解就是一个文件就是一个模块,模块中 存在一个`module`对象,这个对象包含一个`exports`属性,我们只要将该文件上的方法挂载到`exports`对象,其他文件就可以引入了,而没有导出的方法/变量就会被隔离,从而避免变量污染 + +这里模块定义讲得比较粗糙,接下来将具体讲讲`node`中对于`CommonJS`规范的实现: + +## JavaScript文件模块CommonJS实现 + +刚才已经简单介绍了`node`中对于模块使用的一些语法,比如可以通过`require`引入,通过`exports`导出等等,同时,如果你不是前端领域的新手,你应该也知道我们在`node`环境中编写代码时,还可以使用`__filename`和`__dirname`这两个变量 + +但是似乎我们自己并没有定义这些对象/变量,就可以直接使用,所以这就引出了该小节将要解释的--node对于`JavaScript`文件模块的处理。 + +基础知识补充:基本上一个模块机制就是要解决作用域的问题,简单理解就是我们在编写自己的模块时,变量命名这些不会影响到其他模块。同时,要使我们编写的模块有用,我们还会导出一些出口方便其他模块使用,基本上就是一个封装的思想... 然后我们都知道函数是有自己的作用域的,函数内部的变量作用在该函数域内,所以`node`就基于此实现了该模块机制。 + +事实上,当我们执行`node test.js`的时候,也就是在编译的过程中,`node`会把获取的`JavaScript`文件内容封装到一个函数中,并且把解析该文件过程中的一些结果作为形参传入该函数,具体如下: + +```JavaScript +// test.js +console.log('用户写的一些代码逻辑') +``` + +包装后: + +```JavaScript +(function (exports, require, module, __filename, __dirname) { + console.log('用户写的一些代码逻辑') +}) +``` + +所以我们平常在`node`环境中写的代码都会经过这样一个包装,这也就是我们刚才提到的为什么可以直接使用`require`、`exports`等属性的原因,同时也就实现了各个模块文件之间的作用域隔离。 + +> 注:对于不同的文件名,`node`载入的方法也不同,`.js`的就是通过上述方法载入的,而其他的如`.node`、`.json`本篇文章不作详细介绍,除了上述这三个扩展名,其他扩展名的文件如果交给`node`执行,都会被当作`.js`文件载入。 + +接下来我将一一介绍`node`对我们代码进行包装处理的函数中的形参,相信认识了这几个参数,你就对`node`中实现的模块机制就理解的大差不差了,其中`__filename`、`__dirname`就是文件名和路径名,两个字符串就不详细介绍了,接下来主要介绍`module`、`exports`、`require`这三个参数的理解。 + +## 理解`module`参数,基本形成模块机制 + +我们可以自己尝试一下,新建一个`test.js`文件,加上`console.log(module);`这行代码,看看这个参数是什么: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221104220110.png) + +接下来详细介绍一个这个参数:`module`参数其实是`node`通过一个叫做`Module`的构造函数创建的一个实例,所以我们基本上认识这个构造函数就可以了,它的定义如下[详细介绍](https://www.nodeapp.cn/modules.html#modules_the_module_object): + +```JavaScript +function Module(id, parent) { + this.id = id; // 模块的标识符, 通常是完全解析后的文件名 + this.exports = {}; + this.parent = parent; // 最先引用该模块的模块 + if (parent && parent.children) { + parent.children.push(this); + } + this.filename = null; // 模块的完全解析后的文件名 + this.loaded = false; // 模块是否已经加载完成,或正在加载中 + this.children = []; // 被该模块引用的模块对象 + this.paths = []; // 模块的搜索路径 +} +``` + +`node`环境中每个文件都有由这个构造函数创建的唯一实例,一个文件对应一个`module`实例,我们可以把其理解为一个节点,这个节点有一些属性,如`id`、`filename`...等,然后这个节点的入度就是`children`属性,这样就可以抽象出一个模块引用图: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221104230432.png) + +可以看到上述这两个简单的测试程序,我们执行`a.js`得到上述的输出,由于`b.js`中没有引用任何模块,所以在执行`const b = require('./b.js')`时不会得到输出。由此,我们其实可以得到这样一张模块引用图: + +![](https://oss.justin3go.com/blogs/%E6%A8%A1%E5%9D%97%E5%BC%95%E7%94%A8%E9%93%BE%E6%8E%A5%E5%9B%BE.png) + +如果是大型项目,就会形成一个非常复杂的有向图了,而有了图这个数据结构,其实我们似乎就能用一些算法对模块应用进行一些分析处理,比如最简单、也是最容易想到的就是编写一个`vscode`插件来对一个`node`项目进行模块引用的分析并可视化,方便新接触项目成员快速熟悉项目,当然,要实现这个想法应该还要考虑更多,这里不深入... + +继续,由此一个文件对应一个模块的机制通过`module`参数就实现了 + +## 理解`exports`参数 + +你可能会疑惑`module`实例对象中不是已经有了`exports`属性了吗,它与`node`处理文件中传入的`exports`形参有什么关系呢?这也是我最开始接触这个模块机制的时候产生的疑惑~ + +总的来说`exports`就是`module.exports`的快捷方式 + +一般来说,我们都是直接使用`exports.hello = hello(){ console.log('hello') }`导出即可,这样也是最方便并且最好辨认的。 + +但是你需要注意的是,exports是`node`包装我们编写的js文件使用的函数中的一个形参,文章开始部分也介绍过,既然`exports`是通过形参的方式传入的,如果我们要对其直接赋值`exports = {hello: hello(){ console.log('hello')}`,会改变形参的引用,并不能修改作用域外的值,这是`JavaScript`的基础知识。 + +所以此时我们只能修改`module.exports = {hello: hello(){ console.log('hello')}`这样是可以的,但不建议这样做,多种方式的导出会使人迷惑,除非迫不得已。 + +最后,`exports`是一个对象,我们在当前函数作用域中向这个对象修改了属性,是可以反应在函数作用域外面的,因为是修改的引用对象类型。至此,我们就可以既实现作用域隔离避免变量污染,又可以暴露除该模块的功能方法,最终实现了这样一个模块机制 + +## 理解`require`是如何加载模块的 `*` + +`require()`是我们导入别的模块需要用到的一个方法,就如本篇文章中的第一个例子`const math = require('math')`,它可以使我们非常方便地导入其他模块,但是它的内部实现其实相对来说比较复杂,因为`require()`函数除了可以加载上述中`.js`结尾的文件模块,还可以加载其他扩展名结尾的文件模块,以及`node`中内置的核心模块,甚至说传给`require()`的路径参数是一个目录,也需要一定的策略去解析它。 + +总的来说,在`node`中引入模块,需要经历如下三个步骤: + +- 路径分析 +- 文件定位 +- 编译执行 + +在讲解具体的模块加载过程之前,我们先了解一下上面提到的核心模块与文件模块之间的概念: + +- 核心模块:在node源代码的编译过程中,就编译进了二进制执行文件。并且部分核心模块在`node`启动的时候就被直接加载进了内存中,所以这部分核心模块引入时,文件定位和编译执行这两步可以省略,并且路径分析中优先判断,所以其加载速度最快 +- 文件模块:之前介绍的`.js`结尾的就是文件模块中的一种,文件模块在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度相对较慢。文件模块用可以路径形式的文件模块(用户自己编写的)和自定义文件模块(通常是第三方包) + +接下来我们就用下方这个流程图来梳理一下当`require`引入一个模块标识的时候是如何判断的。基于此,我们可以对`node`的模块规范更加了解,并且可以在模块引入时做一些简单的性能优化: + +![](https://oss.justin3go.com/blogs/%E6%A8%A1%E5%9D%97%E5%8A%A0%E8%BD%BD%E8%BF%87%E7%A8%8B.png) + +模块加载流程口语描述: + + - 首先`node`会判断该模块之前是否加载过,在缓存中是否包含,如果包含,显然就可以直接从缓存中加载; + - 然后就是根据传给`require`的模块标识,判断该模块标识属于哪一类型,是模块名的字符串还是模块所在的路径 + - 之后就是如果是否属于核心模块,`node`自己心里清楚,内部存储相关的数组来记录,如果是自定义模块(就是平常我们经常见到的第三方包),就通过一个策略去查找该模块所在的路径,而这个策略是存储在`module.paths`中,你可以自行`console.log`观察一下,或者在之前介绍`module`的时候也有相关的打印信息; + - 再然后我们获得了一个路径,这个路径如果有显式的文件扩展名,就按照上述方式加载,而如果没有扩展名,就按照`.js .json .node`依次尝试,而有可能传递的是一个目录,此时`node`就会去找该目录下的`packsge.json`中的`main`属性对应的文件或者`index`文件名的文件 + - 最后,如果都不行,就包找不到该文件的错误 + +上述在通过`.js .json .node`依次尝试是什么文件的时候,需要调用fs模块同步阻塞执行,所以如果是`.node`和`.json`最后就带上扩展名,会加快一点速度; + +其他: + +> 对于核心模块的加载,涉及到一些`c++`代码,所以流程图中对其简化,这里大致讲一讲其中的流程,不感兴趣的可以略过这一部分: + +对于核心模块,`node`中也分为两种,一种是由`JavaScript`编写的模块,一种是由`C++/C`编写的模块。一般来说,`C++`模块主内完成核心,`JavaScript`主外实现封装,`Node`这种静态语言结合脚本语言的复合模式在开发体验和性能之间找到平衡点。 + +对于由`C/C++`编写的模块一般也叫做`内建模块`,我们可以通过`log`如下信息打印除`node`中包含哪些内建模块: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221105104148.png) + +**内建模块的加载**:会先创建一个`exports`空对象,然后调用`get_builtin_module()`方法去除内建模块对象,通过执行`register_func()`填充`exports`对象,最后将`exports`对象按模块名缓存,并返回给调用方完成导出。 + +一般来说,`node`并不推荐直接加载内建模块,而是通过对应封装地`JavaScript`核心模块进行加载,一个完整地核心模块加载流程如下: + +![](https://oss.justin3go.com/blogs/%E6%A0%B8%E5%BF%83%E6%A8%A1%E5%9D%97%E5%8A%A0%E8%BD%BD%E6%B5%81%E7%A8%8B.png) + +> 内建模块这块由于我缺乏实践,所以仅简单记录了一些要点,并不对其进行解释,如果你是这方面的新手,不推荐通过我这篇文章学习 + +> 一般来说,当`node`性能出现瓶颈,我们是通过编写`C++`扩展模块进行性能优化的,下面是一个简单的模块调用图 + +![](https://oss.justin3go.com/blogs/%E6%A8%A1%E5%9D%97%E4%B9%8B%E9%97%B4%E7%9A%84%E8%B0%83%E7%94%A8%E5%85%B3%E7%B3%BB.png) + +## `Module`对象其他的一些东西 +我们在前面仅仅介绍了`module`实例中有哪些属性,但其实`Module`这个对象还挂载了一些属性: + +1. `Module._extensions`:对于不同扩展名的文件的处理函数保存在这个属性上: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221105181020.png) + +我们可以在此基础上自定义一些其他文件扩展名的处理函数,不过`node`并不建议我们这样做,官方建议先将其他语言或文件编译成为`JavaScript`文件后再加载,这样做的好处是在于不用将繁琐的编译加载过程引入`node`的执行过程中 + +2. `Module._cache`:已经编译执行成功的文件模块会缓存到该对象上: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221105181457.png) + +## 碎碎念 + +作为今年的应届生,之前跨部门转正二面问到这一方面,当时这些基础知识还有点印象,但是不多!最终挂掉了,回来也没能抓住秋招的尾巴,好好复习,all in 春招了💪 + +## 参考 +- 《深入浅出NodeJS》 +- https://www.nodeapp.cn/ +- https://juejin.cn/post/6844903676922822663 + diff --git "a/docs/\345\215\232\345\256\242/2022/06/10Node\345\274\202\346\255\245\345\256\236\347\216\260\344\270\216\344\272\213\344\273\266\351\251\261\345\212\250.md" "b/docs/\345\215\232\345\256\242/2022/06/10Node\345\274\202\346\255\245\345\256\236\347\216\260\344\270\216\344\272\213\344\273\266\351\251\261\345\212\250.md" new file mode 100644 index 0000000..49144c7 --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2022/06/10Node\345\274\202\346\255\245\345\256\236\347\216\260\344\270\216\344\272\213\344\273\266\351\251\261\345\212\250.md" @@ -0,0 +1,154 @@ +# Node异步实现与事件驱动 + +> 这是重新阅读《深入浅出NodeJS》的相关笔记,这次阅读发现自己依旧收获很多,而第一次阅读的东西也差不多忘记完了,所以想着这次过一遍脑子,用自己的理解输出一下,方便记忆以及以后回忆... + +## Node的特点 + +> 计算机中的一些任务一般可以划分为两个类别,一个类别叫做IO密集型,一个叫做计算密集型;对于计算密集型的任务,只能不断榨干CPU的性能,但是对于IO密集型的任务来说,理想情况下却并不需要,只需要通知IO设备进行处理,过一段时间再来拿去数据就好了。 + +对于某些场景有一些互不相关的任务需要完成,现行的主流方法有如下两种: + +- 多线程并行完成:多线程的代价在于创建线程和执行线程上下文切换的开销较大。另外,在复杂的业务中,多线程编程经常面临锁、状态同步等问题; +- 单线程顺序执行:易于表达,但串行执行的缺点在于性能,任意一个略慢的任务都会导致后续代码被组设 + +`node`在两者之前给出了它的方案:利用单线程,远离多线程死锁、状态同步等问题;利用异步IO,让单线程远离阻塞,以更好地使用CPU + +![](https://oss.justin3go.com/blogs/%E5%BC%82%E6%AD%A5IO%E8%B0%83%E7%94%A8%E7%A4%BA%E6%84%8F%E5%9B%BE.png) + +## Node是如何实现异步的 + +> 刚才讲了`node`在多任务处理的方案,但是`node`内部想要实现却并不容易,下面介绍操作系统的几个概念,方面后续大家更好理解,后面再讲一讲异步的实现以及node的事件循环机制: + +### 阻塞IO与非阻塞IO + +- 阻塞IO:应用层面发起IO调用之后,就一直等待数据,等操作系统内核层面完成所有操作后,调用才结束; + +> 操作系统中一切皆文件,输入输出设备同样被抽象为了文件,内核在执行IO操作时,通过**文件描述符**进行管理 + +- 非阻塞IO:差别为调用后立即返回一个文件描述符,并不等待,这时候CPU的时间片就可以用来处理其他事务,之后可以通过这个文件描述符进行结果的获取; + +非阻塞IO存在的一些问题:虽然其让CPU的利用率提高了,但是由于立即返回的是一个文件描述符,我们并不知道IO操作什么时候完成,为了确认状态变更,我们只能作轮询操作 + +### 不同的轮询方法 + +- `read` :最原始、性能最低的一种,通过**重复检查IO状态**来完成完整数据的获取 +- `select`:通过对**文件描述符上的事件状态**来进行判断,相对来说消耗更少;缺点就是它采用了一个1024长度的数组来存储状态,所以它最多可以同时检查1024个文件描述符 +- `poll`:由于`select`的限制,`poll`改进为链表的存储方式,其他的基本都一致;但是当文件描述符较多的时候,它的性能还是非常低下的 +- `eopll`:该方案是`linux`下效率最高的IO事件通知机制,在进入轮询的时候如果没有检查IO事件,将会进行休眠,直到事件发生将它唤醒 +- `kqueue`:与`epoll`类似,不过仅在FreeBSD系统下存在 + +尽管`epoll`利用了事件来降低对CPU的耗用,但休眠期间CPU几乎是闲置的;我们期待的异步IO应该是应用程序发起非阻塞调用,无须通过遍历或事件唤醒等方式轮询,可以直接处理下一个任务,只需IO完成后通过信号或者回调将数据传递给应用程序即可。 + +> linux下还有中AIO方式就是通过信号或回调来传递数据的,不过只有Linux有,并且有限制无法利用系统缓存 + +### node中对于异步IO的实现 + +先说结论,`node`对异步IO的实现是通过多线程实现的。可能会混淆的地方就是`node`内部虽然是多线程的,但是我们程序员开发的`JavaScript`代码却仅仅是运行在单线程上的。 + +`node`通过部分线程进行阻塞IO或者非阻塞IO加上轮询技术来完成数据获取,让一个线程进行计算处理,通过线程之间的通信将IO得到的数据进行传递,这就轻松实现了异步IO的模拟。 + +![](https://oss.justin3go.com/blogs/node%E5%BC%82%E6%AD%A5IO.png) + +除了异步IO,计算机中的其他资源也适用,因为linux中一切皆文件,磁盘、硬件、套接字等几乎所有计算机资源都被抽象为了文件,接下来介绍对计算机资源的调用都以IO为例子。 + +### 事件循环 + +在进程启动时,`node`便会创建一个类似与`while(true)`的循环,每执行一次循环体的过程我们成为`Tick`; + +下方为`node`中事件循环流程图: + +![](https://oss.justin3go.com/blogs/%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF%E6%B5%81%E7%A8%8B%E5%9B%BE.png) + +很简单的一张图,简单解释一下:就是每次都从IO观察者里面获取执行完成的事件(是个请求对象,简单理解就是包含了请求中产生的一些数据),然后没有回调函数的话就继续取出下一个事件(请求对象),有回调就执行回调函数 + +## 异步IO细节 + +![](https://oss.justin3go.com/blogs/%E6%95%B4%E4%B8%AA%E5%BC%82%E6%AD%A5IO%E7%9A%84%E6%B5%81%E7%A8%8B.png) + +> 注:不同平台有不同的细节实现,这张图隐藏了相关平台兼容细节,比如windows下使用IOCP中的`PostQueuedCompletionStatus()`提交执行状态,通过`GetQueuedCompletionStatus`获取执行完成的请求,并且IOCP内部实现了线程池的细节,而linux等平台通过`eopll`实现这个过程,并在`libuv`下自实现了线程池 + +## `setTimtout`与`setInterval` + +除了IO等计算机资源需要异步调用之外,`node`本身还存在一些与异步IO无关的一些**其他异步API**: + +- `setTimeout` +- `setInterval` +- `setImmediate` +- `process.nextTick` + +> 该小节先讲解前面两个api + +它们的实现原理与异步IO比较类似,**只是不需要IO线程池的参与**: +- `setTimtout`与`setInterval`创建的定时器会被插入到定时器观察者内部的一个红黑树中 +- 每次`tick`执行的时候,会从该红黑树中迭代取出定时器对象,检查是否超过定时时间 +- 如果超过,就将这个事件(请求对象)推入到事件队列中,在事件循环中执行其中的回调函数 + +> 红黑树:这里简单提一下,就是一种特殊化的平衡二叉树,可以自平衡,查找效率基本上就是该二叉树的深度了$O(log_2n)$ + +你有考虑过这个问题吗,为什么定时器不需要线程池的参与了呢,如果你理解了之前章节对于异步IO实现原理的话,相信你应该能解释出来,这里简单说说原因来加深记忆: + +**`node`中的IO线程池是用来调用IO并等待数据返回(看具体实现)的一种方式,它使`JavaScript`单线程得以异步调用IO,并且不需要等待IO执行完成(因为是IO线程池做了),并且能获取到最终的数据(通过观察者模式:IO观察者从线程池获取执行完成的事件,事件循环机制执行后续的回调函数)** + +上述这段话可能有点简略,如果你还不明白,可以看下之前的那几种图~ + +## `process.nextTick`与`setImmediate` + +这两个函数都是代表立即异步执行一个函数,那为什么不用`setTimeout(() => { ... }, 0)`来完成呢? + +- 定时器精度不够 +- 定时器使用红黑树来创建定时器对象和迭代操作,浪费性能 +- 即`process.nextTick`更加轻量 + +轻量具体来说:我们在每次调用`process.nextTick`的时候,只会将回调函数放入队列中,在下一轮`Tick`时取出执行。定时器中采用红黑树的方式时$O(log_2n)$,`nextTick`为$O(1)$ + +那`process.nextTick`与`setImmediate`又有什么区别呢?毕竟它们都是将回调函数立即异步执行 + +- `process.nextTick`的回调执行优先级高于`setImmediate` +- `process.nextTick`的回调函数保存在一个数组中,每轮事件循环下全部执行,`setImmediate`的结果则是保存在链表中,每轮循环按序执行第一个回调 + +注意:之所以`process.nextTick`的回调执行优先级高于`setImmediate`,因为事件循环对观察者的检查是有顺序的,`process.nextTick`属于`idle`观察者,`setImmediate`属于`check`观察者。`iedl观察者 > IO 观察者 > check观察者` + +## 高性能服务器 + +> 对于网络套接字的处理,`node`也应用到了异步IO,网络套接字上侦听到的请求都会形成事件交给IO观察者,事件循环会不停地处理这些网络IO事件,如果我们在`JavaScrpt`层面上有传入对应的回调函数,这些回调函数就会在事件循环中执行(处理这些网络请求) + +常见的服务器模型: + +- 同步式 +- 每进程-->每请求 +- 每线程-->每请求 + +而`node`采用的是事件驱动的方式处理这些请求,无需对每个请求创建额外的对应线程,可以省略掉创建线程和销毁线程的开销,同时操作系统的调度任务因为线程较少(只有`node`内部实现的一些线程)上下文切换的代价很低。 + +经典问题--**雪崩问题**的解决: + +问题描述:服务器在刚启动时,缓存无数据,如果访问量巨大,同一条`SQL`会被发送到数据库中反复查询,影响性能。 + +解决方案: + +```js +const proxy = new events.EventEmitter(); +let status = "ready"; // 状态锁,避免反复查询 + +const select = function(callback) { + proxy.once("selected", callback); // 绑定一个只执行一次名为selected的事件 + if(status === "ready") { + status = "pending"; + // sql + db.select("SQL", (res) => { + proxy.emit("selected", res); // 触发事件,返回查询数据 + status = "ready"; + }) + } +} +``` + +使用`once`将所有请求的回调都压入了事件队列中,利用其只执行一次就会将监视器移除的特点,保证每一个回调函数只会被执行一次。对于相同的SQL语句,保证在同一个查询开始到结束的过程中永远只有一次。新到来的相同调用只需在队列中等待数据就绪即可,一旦查询到结果,得到的结果就可以被这些调用共同使用。 + +## 最后 + +基本都是参考《深入浅出NodeJS》这本书的并夹带了一些自己的理解,如果我理解有误的话,欢迎友善指出🎉 + +## 参考 +- 《深入浅出NodeJS》 + diff --git "a/docs/\345\215\232\345\256\242/2022/06/11Node\345\206\205\345\255\230\346\216\247\345\210\266.md" "b/docs/\345\215\232\345\256\242/2022/06/11Node\345\206\205\345\255\230\346\216\247\345\210\266.md" new file mode 100644 index 0000000..733fc02 --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2022/06/11Node\345\206\205\345\255\230\346\216\247\345\210\266.md" @@ -0,0 +1,105 @@ +# Node内存控制 + +> 这是重新阅读《深入浅出NodeJS》的相关笔记,这次阅读发现自己依旧收获很多,而第一次阅读的东西也差不多忘记完了,所以想着这次过一遍脑子,用自己的理解输出一下,方便记忆以及以后回忆... + +## 基本介绍 + +说到`node`对于内存的控制,可能最先想到的就是`node`是基于V8构建,因此在`node`中通过`JavaScript`使用内存时就会发现只能使用部分内存(64位系统约为1.4GB,32位系统约为0.7GB)。在这样的限制下,将会导致`node`无法直接操作大内存对象,即使你本机的物理内存有32GB也不行,这与我们传统上的认知形成了一定的差别,接下来先解释一下为什么有这样的差别: + +> 注:64位系统约为1.4GB,32位系统约为0.7GB为默认,也可以用户自定义`--max-old-space-size`和`--max-new-space-size`来调整,不过只能在启动时指定,`node`无法运行中自动扩充 + +首先,V8的设计就是在浏览器的应用场景下完成的,这套内存管理机制在浏览器下使用是绰绰有余的,只是在`node`中使用有些限制,当然,也有其他方式来解决,只是不能让开发者随心所欲地使用大内存了。 + +其实深层原因是V8的垃圾回收机制的限制,每次垃圾回收都必须让`JavaScript`线程暂停,如果垃圾回收时间过长会导致应用的性能和响应能力都会直线下降,所以V8当时的考虑直接限制堆内存是一个好的选择;这中停顿叫做“全停顿”,V8为此也还做了许多优化,这个后续章节会讲到 + +回到这一部分,介绍一个比较常见的命令:我们在`node`中输入`process.memoryUsage()`,就可以很方便地查看`node`内存使用量的相关信息: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221107183347.png) + +简单解释一下其中的含义,[详细参考](https://nodejs.org/docs/latest-v14.x/api/process.html#process_process_memoryusage) + +> 注:V8中的所有`JavaScript`对象都是通过堆来进行分配的 + +- `rss`:Resident Set Size,是该进程所占用的总空间量,包含所有`C++`和`JavaScript`对象和代码 +- `heapTotal`:已经申请到的堆内存 +- `heapUsed`:当前使用的堆内存 +- `external`:指绑定到 V8 管理的 JavaScript 对象的 C++ 对象的内存使用情况 +- `arrayBuffers`:包含的`Buffer`对象,该值包含在`external`中 + +值得注意的是:V8在上述变量中只负责了堆内存的分配,`external`包含的内存并不是通过V8管理的,所以我们在`external`中操作的东西可以不受V8的内存限 + +## 垃圾回收机制 + +总的来说,V8的垃圾回收策略是基于分代式垃圾回收机制,分为新生代和老生代。其中新生代的对象为存活时间较短的对象,老生代的对象为存活时间较长的对象,这里先介绍结论,具体原因后续讲到。 + +### 新生代 + +> 新生代中主要通过Scavenge算法进行垃圾回收,这是典型的空间换时间的算法,会牺牲一半的存储空间,但速度较快,正好与新生代中对象的特点相对应 + +该算法主要采取复制的方式进行的垃圾回收,如下图所示: + +![](https://oss.justin3go.com/blogs/%E6%96%B0%E7%94%9F%E4%BB%A3%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6.png) + +之前提到过这个算法与新生代的特点向符合,解释一下,新生代里面对象的特点就是生命周期较短,所以在下一次复制过程中存活的对象一般来说是比较少的,而这个算法是只复制存活的对象,所以时间效率上有优异的表现,同时这中将存活中的对象直接在另一半内存空间中依次排列,不会产生老生代那种算法的内存碎片问题(后续会讲到) + +### 晋升 + +其实一开始对象声明的时候,V8也不知道这个对象是否生命周期较短,那它是如何判断从而将对象区分到新生代区域和老生代区域之中的呢? + +答案就是本小节的标题,“晋升”:当一个对象经过多次复制依然存活时或者该对象To空间的占用比超出限制(一般25%),它将会被执行晋升操作,放入到老生代区域之中(注意:只要这两个条件满足一个,就会执行晋升操作,这是个“或”条件) + +### 老生代 + +结合老生代的特点,V8在老生代中主要采取了`Mark-Sweep`和`Mark-Compact`相结合的方式进行垃圾回收,就是标记清除和标记整理 + +**标记清除**: +- 标记阶段:标记活着的对象 +- 清除阶段:清除没被标记的对象 + +> 老生代的特点就是存活时间长,即失活对象的占比一般来说是比较小的,所以这里是清除死亡的对象是合理的。而不是与新生代算法一致,复制活着的对象,并且由于老生代对象占用内存较大,所以分出一半空间来说也是不合理的 + +![](https://oss.justin3go.com/blogs/%E6%A0%87%E8%AE%B0%E6%B8%85%E9%99%A4.png) + +**标记整理**:在标记清除的基础上提出来的,在对象标记为死亡后,整理的过程中,会将活着的对象往一边移动,移动完成后,直接清理掉边界外的内存。 + +注意:V8并不是直接采取标记整理的方式来管理老生代,而是通过标记清除和标记整理相结合的方式进行处理,因为它们在处理效率上有较大差别,毕竟标记清理多做了移动的操作。 + +| 回收算法 | `Mark-Sweep` | `Mark-compact` | `Scavenge` | +| ------------ | ------------ | -------------- | ------------------ | +| 速度 | 中等 | 最慢 | 最快 | +| 空间开销 | 少(有碎片) | 少(无碎片) | 双倍空间(无碎片) | +| 是否移动对象 | 否 | 是 | 是 | + +这种分层级处理方式在计算机中非常常见,最容易想到的就是比如速度上`寄存器 > 内存 > 外存`,而价格上`寄存器 < 内存 < 外存`,所以计算机的存储结构就采取了三级分层策略来平衡速度与价格;就像V8中于内存的处理是在时间和空间以及内存碎片等维度上进行平衡的。 + +所以在取舍中,V8主要使用标记清除算法,在空间不足以对新生代晋升过来的对象进行分配的时候才使用标记整理算法(如下图) + +![](https://oss.justin3go.com/blogs/%E6%A0%87%E8%AE%B0%E6%95%B4%E7%90%86.png) + +## 全停顿问题 + +上述中的三种基本垃圾回收算法都需要将应用逻辑(`JavaScript`执行线程)暂停下来,待执行完垃圾回收后再恢复执行应用逻辑,这样做是为了避免`JavaScript`应用逻辑与垃圾回收器看到的不一致的情况。这种行为就是文章前面提到的“全停顿”; + +而且老生代通常配置得较大,且活动对象较多,全堆垃圾回收得标记、清理、整理等动作造成得停顿就会比较可怕,需要优化: + +这就是“增量标记”出现的原因,具体过程就是将原本要以口气停顿完成的动作改为增量标记的方式,也就是拆分为寻多个小“步进”,每做完一个“步进”就让`JavaScript`执行一小会儿 + +同时还有延迟清理与增量式整理,让清理和整理的动作也变成增量式等一系列优化操作,这里不深入研究。 + +## 内存泄漏 + +通常造成内存泄漏的原因有这些: +- 缓存:把内存作缓存,却没有过期策略清除,导致越来越多 +- 队列消费不及时:生产速度远远大于消费速度,队列长度没做限制的话就会无限变大,导致内存泄漏 +- 作用域未释放 + +## 内存监控及内存泄漏解决方案 + +TODO,功力不够,后续来补,主要是还没实践经验,这里先挖个坑。。。 + +## 参考 + +- 《深入浅出NodeJS》 +- https://xenojoshua.com/posts/2018/01/node-memory +- https://nodejs.org/docs/latest-v14.x/api/process.html#process_process_memoryusage + diff --git "a/docs/\345\215\232\345\256\242/2022/06/12Node\350\277\233\347\250\213\345\217\212\351\233\206\347\276\244\347\233\270\345\205\263.md" "b/docs/\345\215\232\345\256\242/2022/06/12Node\350\277\233\347\250\213\345\217\212\351\233\206\347\276\244\347\233\270\345\205\263.md" new file mode 100644 index 0000000..73f01da --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2022/06/12Node\350\277\233\347\250\213\345\217\212\351\233\206\347\276\244\347\233\270\345\205\263.md" @@ -0,0 +1,279 @@ +# Node进程及集群相关 + +## 创建进程 + +相信大家耳边听烂的一句话就是“`JavaScript`是单线程的”,为了弥补面对单线程对多核使用不足的问题,`node`很方便的提供了几个创建进程的方法: + +- `spawn()`:启动一个子进程来执行命令 +- `exec()`:启动一个子进程来执行命令,与`spawn()`不同的是其接口不同,它有一个回调函数获知子进程的状况 +- `execFile()`:启动一个子进程来执行可执行文件 +- `fork()`:与`spawn()`类似,不同点在于它创建`node`的子进程只需指定要执行的`JavaScript`文件模块即可 + +`spawn()`与`exec()`、`execFile()`不同的是,后两者创建时可以指定`timeout`属性设置超时时间,一旦创建的进程超过设定的时间将会被杀死; + +`exec()`与`execFile()`不同的是,`exec()`适合执行已有的命令,`execFile()`适合执行文件; + +| 类型 | 回调/异常 | 进程类型| 执行类型 | 可设置超时时间 | +| - | - | - | - | - | +| `spawn()` | × | 任意 | 命令 | × | +| `exec()` | √ | 任意 | 命令 | 对 | +| `execFile()` | √ | 任意 | 可执行文件 | √ | +| `fork()` | × | Node | `JavaScript`文件 | × | + +> 一般来说创建多线程只是为了充分将CPU资源利用起来,在做服务端时,事件驱动的机制已经可以很好的解决大并发的问题了,注意和每请求/每进程的服务端模型区分 + +## Node多进程架构 + +经典的主从模式: + +- 主进程不负责具体的业务处理,而是负责调度或管理工作进程,它是趋于稳定的 +- 工作进程负责具体的业务处理 + +![](https://oss.justin3go.com/blogs/%E4%B8%BB%E4%BB%8E%E6%9E%B6%E6%9E%84.png) + +比如我们这里编写一个经典的代码试试: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221109123814.png) + +这里主进程中查询了当前机器cpu的核心数,根据不同的核心数复制对应数量的工作进程,从而真正利用到多核CPU的优势,当然由于我这台服务器只有单核,所以这里只开出了一个子进程,这里假设有8核的话就是如下效果: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221109124029.png) + +当然,这是个反面例子,因为我们本来就是要利用多核CPU的性能才开出多个进程,而这里在单核CPU上开出8个进程是没用的,反而调度器还要来回切换进程进行处理,因为操作系统有句经典的话就是“宏观上并发执行,微观上交替执行”,所以我这里这样做是没用的,只是为了演示。 + +## 进程通信原理 + +这里先复习一下操作系统中进程通信的方式[详细链接](https://justin3go.com/%E7%9F%A5%E8%AF%86%E5%BA%93/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/01%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%9F%BA%E7%A1%80.html#%E8%BF%9B%E7%A8%8B%E9%80%9A%E4%BF%A1) 或 [详细链接](https://justin3go.com/%E7%9F%A5%E8%AF%86%E5%BA%93/%E5%89%8D%E7%AB%AF%E5%85%AB%E8%82%A1%E6%96%87/06%E6%B5%8F%E8%A7%88%E5%99%A8%E5%8E%9F%E7%90%86.html#_4-%E8%BF%9B%E7%A8%8B%E4%B9%8B%E5%89%8D%E7%9A%84%E9%80%9A%E4%BF%A1%E6%96%B9%E5%BC%8F): + +- 共享内存通信方式 +- 消息传递通信方式(利用操作系统提供的消息传递系统实现进程通信) + - 消息缓冲通信方式:直接通信方式 + - 信箱通信方式:间接通信方式,对同步的要求没那么严格 +- 共享文件通信方式:管道通信方式 + +除此之外,还有一些其他的通信方式比如: + +- 信号量通信,传递的信息较少 +- 套接字通信 + +而`node`是通过管道技术实现的,当然这个管道与上述中的管道有所区别,是在`node`中的一个抽象层面,具体在windows中是用命名管道,linux使用`unix domain socket`实现的,最终体现出现就是进程之间创建IPC通道,通过这个通道,进程之间才能通过`message`和`send()`传递消息 + +细节上: +- 父进程在实际创建子进程之前,会创建IPC通道并监听它,然后才真正创建出子进程,并通过环境变量(NODE_CHANNEL_FD)**告诉子进程这个IPC通道的文件描述符** +- 子进程在启动的过程中,根据文件描述符去链接这个已存在的IPC通道,从而完成父子进程之前的连接(创建的子进程为`node`进程默认为遵守这个约定,而非`node`进程的要么自实现遵守约定,要么不能通信) +- IPC通道被抽象为`Stream`对象,在调用`send()`时发送数据(类似于`write()`),接收到的消息会通过`message`事件(类似于`data`)触发给应用层 + +![](https://oss.justin3go.com/blogs/%E5%88%9B%E5%BB%BAIPC%E6%AD%A5%E9%AA%A4.png) + +## 多个进程监听相同的端口 + + **1. 首先是为什么要让多个进程来监听相同的端口** + +因为我们要使用主从模式,使不同的工作线程来处理同一个应用,但是操作系统中,一般来说都是一个端口对应一个进程即一个应用 + +⚠如何不做任何处理,直接让多个进程监听同一个端口是会报错的 + +**2. 那为什么不能使用如下这种方法呢?** + +> 我们在使用主从模式的时候,可以使用主进程来监听主端口(如80),即主进程对外接收所有的网络请求,再将这些请求分别代理到不同的端口的进程上 + +答: + +通过代理,虽然可以避免端口不能被重复监听的问题,甚至可以在代理进程上做适当的负载均衡,使得每个子进程可以较为均衡地执行任务。 + +但是由于进程每接收到一个连接,就会用掉一个文件描述符,因此代理方案中客户端连接到代理进程需要消耗,代理进程连接到工作进程这两个阶段需要消耗掉两个文件描述符。而操作系统地文件描述符是有限地,代理方案浪费了一倍数量地文件描述符影响了系统的扩展能力 + +**3. 那该如何解决呢?** + +`node`中还存在发送句柄的操作,句柄是一种可以用来标识资源的引用,因此我们可以直接在主进程收到socket请求后,将这个socket通过`send()`方法发送给工作进程,而不是代理方案中的与工作进程重新建立新的socket连接,是通过IPC进程通信方式直接发送句柄,同时我们将socket发送给了子进程之后,主进程自己也可以关闭监听,之后由子进程来处理请求,这样最终效果就变为了“多个进程监听相同的端口”但不报错。 + +```js{10-11} +// 主进程 +const child = require('child_process').fork('child.js'); +const child1 = cp.fork('child.js'); +const child2 = cp.fork('child.js'); +// Open up the server object and send the handle +const server = require('net').createServer(); +server.listen(1337, function () { + child1.send('server', server); + child2.send('server', server); + // 关掉主进程的监听,让子进程来处理这些请求 + server.close(); +}); +``` + +```js +// 工作进程 +const http = require('http'); +const server = http.createServer(function (req, res) { + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.end('handled by child, pid is ' + process.pid + '\n'); +}); +process.on('message', function (m, tcp) { + if (m === 'server') { + tcp.on('connection', function (socket) { + server.emit('connection', socket); + }); + } +}); +``` + +![](https://oss.justin3go.com/blogs/%E5%8F%91%E9%80%81%E5%8F%A5%E6%9F%84%E5%85%B3%E9%97%AD%E8%AF%B7%E6%B1%82.png) + +> 最终发送到IPC通道的信息都是字符串,刚才上述代码发送的其实是句柄文件描述符,是一个整数,`send()`方法会将原始信息结合其他信息包装为一个对象并序列化为字符串传递过去,另一边再`JSON.parse()`就可以了 + +## 集群稳定 + +> 子进程的相关事件[快速入口](https://nodejs.org/dist/latest-v18.x/docs/api/child_process.html#class-childprocess) + +### 自动重启 + +通过监听子进程的`exit`事件来获知其退出的信息,然后做一些操作,比如重启一个工作进程来继续服务: + +```js{11-16} +// master.js +const fork = require('child_process').fork; +const cpus = require('os').cpus(); + +const server = require('net').createServer(); +server.listen(1337); + +const workers = {}; +const createWorker = function () { +  const worker = fork(__dirname + '/worker.js'); +  // 退出时重新启动新的进程 +  worker.on('exit', function () { +    console.log('Worker ' + worker.pid + ' exited.'); +    delete workers[worker.pid]; +    createWorker(); +  }); +  // 句柄转发 +  worker.send('server', server); +  workers[worker.pid] = worker; +  console.log('Create worker. pid: ' + worker.pid); +}; + +for (let i = 0; i < cpus.length; i++) { +  createWorker(); +} + +// 进程自己退出时,让所有工作进程退出 +process.on('exit', function () { +  for (let pid in workers) { +    workers[pid].kill(); +  } +}); +``` + +缺点,上述代码是在子进程退出后重启的新进程来处理请求,这中间有一段空白期,我们应该在子进程退出前就启动的新的工作进程从而实现**平滑重启**: + +```js{2-3,9-10} +// worker.js +process.on('uncaughtException', function (err) { +  process.send({ act: 'suicide' }); +  // 停止接收新的连接 +  worker.close(function () { +    // 所有已有连接断开后,退出进程 +    process.exit(1); +  }); +  // 设个超时自动断开,避免长连接断开需要较久的时间 +  setTimeout(function () { +   process.exit(1); +  }, 5000); +}); +``` + +```js{5-7} +const createWorker = function () { +  const worker = fork(__dirname + '/worker.js'); +  // 启动新的进程 +  worker.on('message', function (message) { +    if (message.act === 'suicide') { +      createWorker(); +    } +  }); +  worker.on('exit', function () { +    console.log('Worker ' + worker.pid + ' exited.'); +    delete workers[worker.pid]; +  }); +  worker.send('server', server); +  workers[worker.pid] = worker; +  console.log('Create worker. pid: ' + worker.pid); +}; +``` + +当业务代码本来就有严重的问题,无论重启多少次都会报错,我们应该有合适的策略放弃重启,比如单位时间内只能重启有限次数,否则就放弃(最好需要添加日志、报警等): + +```js{6,11,15,21-25} +// 重启次数 +const limit = 10; +// 时间单位 +const during = 60000; +const restart = []; +const isTooFrequently = function () { + // 记录重启时间 + const time = Date.now(); + const length = restart.push(time); + if (length > limit) { + // 取出最后10个记录 + restart = restart.slice(limit * -1); + } + // 最后一次重启到前10次重启之间的时间间隔 + return restart.length >= limit && restart[restart.length - 1] - restart[0] < during; +}; + +const workers = {}; +const createWorker = function () { + // 检查是否太过频繁 + if (isTooFrequently()) { + // 触发giveup事件后,不再重启 + process.emit('giveup', length, during); + return; + } + const worker = fork(__dirname + '/worker.js'); + worker.on('exit', function () { + console.log('Worker ' + worker.pid + ' exited.'); + delete workers[worker.pid]; + }); + // 重新启动新的进程 + worker.on('message', function (message) { + if (message.act === 'suicide') { + createWorker(); + } + }); + // 句柄转发 + worker.send('server', server); + workers[worker.pid] = worker; + console.log('Create worker. pid: ' + worker.pid); +}; +``` + +### 负载均衡 + +合理分配任务,避免单方面忙碌和单方面空闲 + +常见的策略如下: + + 1. **轮询**:每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。 + 2. **指定权重**:指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。 + 3. **IP绑定 ip_hash**:每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。 + 4. **fair(第三方)**:按后端服务器的响应时间来分配请求,响应时间短的优先分配。 + 5. **url_hash(第三方)**:按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。 + +## 状态共享 + +解决数据共享最直接、简单的方式就是通过第三方来进行数据存储,然后单独使用一个进程来轮询获取数据,并主动通知给各个工作线程 + +![](https://oss.justin3go.com/blogs/%E7%8A%B6%E6%80%81%E5%85%B1%E4%BA%AB%E7%AD%96%E7%95%A5.png) + +## Cluster模块 + +刚才进程之间的管理都是我们自己手动写的,其实`node`有很方便的`cluster`模块,用以解决多核CPU的利用率问题,同时也了较完善的API,用以处理进程的健壮性问题。 + +[快速入口](https://nodejs.org/dist/latest-v18.x/docs/api/cluster.html) + +## 参考 + +- 《深入浅出NodeJS》 +- https://zhuanlan.zhihu.com/p/89356016 + diff --git "a/docs/\345\215\232\345\256\242/2022/10/13CDN\345\256\236\350\267\265\351\205\215\347\275\256+\345\216\237\347\220\206\347\257\207.md" "b/docs/\345\215\232\345\256\242/2022/10/13CDN\345\256\236\350\267\265\351\205\215\347\275\256+\345\216\237\347\220\206\347\257\207.md" new file mode 100644 index 0000000..a59773f --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2022/10/13CDN\345\256\236\350\267\265\351\205\215\347\275\256+\345\216\237\347\220\206\347\257\207.md" @@ -0,0 +1,111 @@ +# CDN实践配置+原理篇 + +> 前几天配置了下自己在阿里云的对象存储中的CDN加速,这里记录写个教程为引入,来讲解一下CDN的相关原理及过程,希望对你有所帮助 + +## CDN概念 + +CDN(Content Delivery Network,**内容分发网络**)是指一种通过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。 + +## CDN作用 + +CDN一般会用来托管Web资源(包括文本、图片和脚本等),可供下载的资源(媒体文件、软件、文档等),应用程序(门户网站等)。使用CDN来加速这些资源的访问。 + +(1)在性能方面,引入CDN的作用在于: + +- 用户收到的内容来自最近的数据中心,延迟更低,内容加载更快 +- 部分资源请求分配给了CDN,减少了服务器的负载 + +(2)在安全方面,CDN有助于防御DDoS、MITM等网络攻击: + +- 针对DDoS:通过监控分析异常流量,限制其请求频率 +- 针对MITM:从源服务器到 CDN 节点到 ISP(Internet Service Provider),全链路 HTTPS 通信 + +除此之外,CDN作为一种基础的云服务,同样具有资源托管、按需扩展(能够应对流量高峰)等方面的优势。 + +## 直接上手配置 + +> 前提是你已经开通了对象存储,本部分教程仅介绍从对象存储→CDN加速,并以此为引,帮助回忆一下CDN的原理及过程; + +控制台中进入对象存储应该是这个样子: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221114204713.png) + +还有就是你应该在此之前准备一个域名,因为CDN本质上就是利用了DNS,后续原理篇会讲到,所以需要一个域名,同时,如果你需要HTTPS访问,你还需要准备一个SSL证书,这个本章节不详细涉及,请自行配置; + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221114211106.png) + +我这里就不重新从零配置了,直接从已经配置的地方开始讲讲,详细配置教程可以看这个[参考链接](https://help.aliyun.com/document_detail/123227.html?spm=5176.8466035.help.dexternal.6fac1450P7o0TQ#task-1937766) + +我们从上面图中箭头指向的链接进入可以看到CDN相关的配置信息,有如下两个地方值得注意: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221114212320.png) + +- CNAME:启用CDN加速服务需要将加速域名指向CNAME地址,这样访问加速域名的请求才能转发到CDN节点上,在这里就是我们的`oss.justin3go.com => CNAME` +- 原站域名:我们对象存储中阿里云自动给的一个访问域名,一般来说是非常长一串的一个地址 +- 我们自己的域名:我们想要外界通过这个来访问的一个域名,我这里就是`oss.justin3go.com` + +可以看到这里有三个地址(域名),那我们该如何进行解析呢?就是该如何进行域名映射,这就需要我们简单了解一下CDN的原理了,这时候之前记忆的八股文知识就被唤醒了... + +先说是如何映射的: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221114213906.png) + +`oss.justin3go.com => CDN加速的CNAME` + +而前面那种图(从这往上数第二张图)中也配置了原站信息`CDN加速的CANME => 原站oss域名` + +## 常见的oss配置方式 + +所以综上最后的域名映射关系就是下图中的方案四: + +![](https://oss.justin3go.com/blogs/CDN%E5%90%84%E5%9F%9F%E5%90%8D%E6%98%A0%E5%B0%84%E5%85%B3%E7%B3%BB.png) + +对于配置oss有如上如下四种方案(很基本的东西,这里简单做个对比方便大家理解): + +- 方案一:用户直接方案oss域名 +- 方案二:使用我们自己的域名对oss域名解析,方便记忆,就是我们平常域名拿来解析ip地址的作用,也是映射作用 +- 方案三:使用CDN加速,有CDN的优势,如访问速度快 +- 方案四:使用自己的域名对CDN域名进行解析,有方案二和方案三的结合优势 + +## CDN原理篇 + +这小节主要解决这个问题“为什么CDN加速需要单独一个域名” + +回答也很简单,就是CDN本质上使用了DNS解析来做到访问最近的节点资源,接下来通过下面这张图详细说一下CDN整个一个访问过程: + +![](https://oss.justin3go.com/blogs/CDN%E5%8E%9F%E7%90%86%E6%AD%A5%E9%AA%A4%E5%9B%BE.png) + +**用户未使用CDN缓存资源的过程:** + +1. 浏览器通过DNS对域名进行解析(就是基本的DNS解析过程),依次得到此域名对应的IP地址 +2. 浏览器根据得到的IP地址,向域名的服务主机发送数据请求 +3. 服务器向浏览器返回响应数据 + +**用户使用CDN缓存资源的过程:** + +1. 对于点击的数据的URL,经过本地DNS系统的解析,发现该URL对应的是一个CDN专用的DNS服务器,DNS系统就会将域名解析权交给CNAME指向的CDN专用的DNS服务器。 +2. CND专用DNS服务器将CND的全局负载均衡设备IP地址返回给用户 +3. 用户向CDN的全局负载均衡设备发起数据请求 +4. CDN的全局负载均衡设备根据用户的IP地址,以及用户请求的内容URL,选择一台用户所属区域的区域负载均衡设备,告诉用户向这台设备发起请求 +5. 区域负载均衡设备选择一台合适的缓存服务器来提供服务,将该缓存服务器的IP地址返回给全局负载均衡设备 +6. 全局负载均衡设备把服务器的IP地址返回给用户 +7. 用户向该缓存服务器发起请求,缓存服务器响应用户的请求,将用户所需内容发送至用户终端。 + +**结合刚才配置阿里云CDN的过程** + +1. 用户访问`oss.justin3go.com` +2. 本地DNS进行解析发现`oss.justin3go.com`对应的是`CDN加速域名`,于是将域名解析权交给CDN专用的DNS服务器,该专用DNS服务器返回`CDN加速域名` +3. 用户请求`CDN加速域名`,通过负载均衡返回离用户最近的一个节点资源; + +> 我们的`oss原站地址`会在CDN会定期同步资源到各个缓存服务器的时候使用,并且如果还未同步时,即缓存服务器还没我们将访问的资源时,会直接返回`oss原站地址` + +## 最后 + +有些细节部分并没有一一验证,如果我理解有误的话,欢迎友善指出🎉 + +## 参考 +- https://help.aliyun.com/document_detail/123227.html?spm=5176.8466035.help.dexternal.6fac1450P7o0TQ#task-1937766 +- https://help.aliyun.com/document_detail/27101.html?spm=5176.8466035.help.dexternal.6fac1450P7o0TQ +- https://juejin.cn/post/6987688566520299528 +- https://juejin.cn/post/6941278592215515143 + diff --git "a/docs/\345\215\232\345\256\242/2022/10/14\350\266\205\350\257\246\347\273\206\347\232\204\345\211\215\347\253\257\347\250\213\345\272\217\345\221\230git\346\214\207\345\214\227.md" "b/docs/\345\215\232\345\256\242/2022/10/14\350\266\205\350\257\246\347\273\206\347\232\204\345\211\215\347\253\257\347\250\213\345\272\217\345\221\230git\346\214\207\345\214\227.md" new file mode 100644 index 0000000..492c97c --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2022/10/14\350\266\205\350\257\246\347\273\206\347\232\204\345\211\215\347\253\257\347\250\213\345\272\217\345\221\230git\346\214\207\345\214\227.md" @@ -0,0 +1,370 @@ +# 超详细的前端程序员git指北 + +> git是团队开发必备工具之一,本期教程我们从一个开发人员开发新功能,然后合并到主分支上的一整个流程进行演示讲解,而不是仅仅告诉你这个命令的作用是什么,区别是什么,毕竟程序员始终得贯穿“学以致用”这条硬道理,最后再对不同的常见命令及逆行讲解。 + +## 一个git小demo + +这是这个例子要演示的整体节点图,接下来的demo就是按照如下流程进行演示的 + +![](https://oss.justin3go.com/blogs/git_demo%E4%BE%8B%E5%AD%90.png) + +注:`preonline => main` 这里没有画出来,理论上和`feat_login => preonline`一致也会进行一次`fast-forward merge` + +### step1:开始 + +> 为了方便理解,这里我们不深入git的原理,它是如何追踪的,它是如何在多个分支中切换的这些都不谈,本篇文章的目的也主要是带大家真正会使用git,真正成为我们手上的工具 + +首先这里我在github创建了一个仓库来演示这个过程: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221115173556.png) + +然后我们克隆这个仓库,当然前提是你已经安装了git并配置了基本信息: + +```sh +git clone git@github.com:Justin3go/test-git.git +``` + +运行效果如下: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221115191425.png) + +目前,这个项目只有`main`一个分支,但一般来说,一个线上项目至少都会还有一个`preonline`的分支,当然,具体到各个项目可能主要分支情况又不一样,这里介绍一种比较常见的情况,后续遇到不同的分支结构类比并参考`contributing.md`这类文档开发就应该没什么问题了。 + +所以我们简单创建一个基本结构,多创建一个`preonline`分支:该分支的作用就是所有其他功能分支就只能合并进入`preonline`分支,没什么问题之后才会合入`main`分支进行上线; + +```sh +git checkout -b preonline # 创建并切换分支 +``` + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221115192356.png) + +### step2:开发分支 + +接下来我们就可以开发分支了,一般来说,看大家团队自己的规范,比如要求所有开发分支必须以`dev`作为前缀,而还有些是要求以分支具体情况作为分支名,比如`feat_init`、`fix_some_bug`、`style_botton`等等,这和我们平常的`commit`信息的前缀是一致的,而具体来说有如下的前缀: + +- `build`:表示构建,发布版本可用这个 +- `ci`:更新 CI/CD 等自动化配置 +- `chore`:杂项,其他更改 +- `docs`:更新文档 +- `feat`:常用,表示新增功能 +- `fix`:常用:表示修复 bug +- `perf`:性能优化 +- `refactor`:重构 +- `revert`:代码回滚 +- `style`:样式更改 +- `test`:单元测试更改 + +比如我们现在需要新增一个登录功能,我们就创建我们自己的分支: + +```sh +git checkout main # 切换到主分支 +git checkout -b feat_login # 创建并切换到我们自己的分支 +``` + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221115194749.png) + +然后我们就可以进行开发了: + +**首先我先开发完了基本上所有的功能,下面是我新增的代码,然后准备提交** + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221115195139.png) + +基本来说,你可以直接使用vscode的可视化命令,也可以使用如下的`git`命令进行操作 + +```sh +git add . # 暂存所有更改的文件 +``` +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221115195322.png) + +```sh +git commit -m "feat: 完成登录功能" # 提交并添加提交信息 +``` + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221115195443.png) + + +**然后我们点击分布分支就会在远程仓库里面创建一个一样的分支: ** + + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221115195616.png) + +**然后走查的时候产品经理说button样式不对,所以你继续进行修改:** + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221115195821.png) + +**然后你接着和之前一样提交代码`git add .`,`git commit -m "style: login button color"`,再然后就是你就需要推送到远程分支了`git push`** + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221115200121.png) + +### step3:合并多次提交 + +前面一小节的内容几乎都是在自己的分支上操作,所以都是一些比较简单的命令,接下来就是要和团队对齐了,比如一般来说为了最后主分支上的commit信息简洁和可读性,每个功能分支上面的多个commit都需要合并成一个commit,比如我们刚才的`feat_login`分支就有两个commit,所以现在我们进行合并: + +```sh +git rebase HEAD~2 # 合并最近两次提交 +``` + +> 这里还是先将整个流程走完,某些经典命令比如这里的`rebase`的详细使用指南可以查看后续的章节 + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221115202023.png) + +然后我们修改其中的提交信息如下: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221115202308.png) +这里发现我这里的git默认使用的是nano编辑器,但我比较熟悉vim,所以设置一下git的默认编辑器: + +```sh +git config --global core.editor vim +``` + +之后修改信息为如下这个样子然后`esc wq`保存退出就可以了: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221115202858.png) + +再然后我们输入`git log --oneine`查看提交信息就可以发现修改成功了,如下图,之前的两次提交合并成为了一次提交: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221115203200.png) + +### step4:同步preonline分支 + +在团队开发时,大家基本都并行开发,每周基本都有几个需求会在同一个项目中进行修改,不可避免的就是在提交合并请求时(github叫做`pull request` ,gitlab叫做`merge request`)这里简称为`MR`,需要同步最新的代码,如果有冲突,还需要解决冲突,之后才能提交`MR` + +为了演示,我直接在github上面修改了`preonline`分支上的内容如下,假设是其他人进行了开发,并在我们之前合并到了`preonline` + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221115214947.png) + +然后我们在本地先拉取最新的`preonline`分支 + +```sh +git pull +``` + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221115215136.png) + +然后我们就需要切换到自己的分支进行同步操作了 + +```sh +git checkout feat_login +git rebase preonline # 同步最新的:将自己新开发的提交变基到最新的preonline分支上 +``` + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221115215525.png) + +很好,我们遇到冲突了,我们点击在合并编辑器中打开,你也可以直接在这里操作,不过我喜欢好看一点的合并编辑器: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221115215809.png) + +然后就需要你手动选择要合并的代码,这里你要和别人商量一下要谁的登录页,这个例子可能不太好,现实中一般不会两个人开发同一个按钮,这里仅仅为了演示。 + +然后你点击`accept`其中一个,最后点接受合并就可以了 + +继续刚才的`rebase`操作,在此之前你可能还需要`git add .`一下 + +```sh +git add . +git rebase --continue +``` + +> 如果你在处理完冲突后不想继续当前的`rebase`操作了,比如冲突处理错了等等,你可以`git rebase --abort` + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221115220146.png) + +还要记得`git push`一下,本地推送到远程,并且需要`-f`参数,因为我们修改了以前的提交的信息,注意由于这是在我们自己的分支上操作,所以`-f`是可行的。 + +```sh +git push -f +``` + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221115220451.png) + +之后我们在github上面操作,去提交`MR` + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221115220619.png) + +然后项目的负责人或者核心成员就可以对你的代码进行`CR`了(`code review`) + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221115220742.png) + +`CR`完成之后,就是项目负责人同意合并请求,你的代码就成功合并到了`preonline`,不同项目有不同的周期,等到上线当天,`preonline`里面的代码就会被项目负责人合并到`main`分支上完成上线。 + +## `git log`介绍 + +查看提交信息,一般会用`git log --oneline`简略查看之前的提交; + +[官网快速入口](https://git-scm.com/docs/git-log) + +[git log 的使用](https://www.jianshu.com/p/0805b5d5d893) + +## 撤回提交之`reset`与`revert` + +开发中频繁使用 git 拉取推送代码,难免会有误操作。这个时候不要慌,git 支持绝大多数场景的撤回方案,我们来总结一下。 + +撤回主要是两个命令:`reset` 和 `revert` + +#### git reset + +reset 命令的原理是根据 `commitId` 来恢复版本。因为每次提交都会生成一个 commitId,所以说 reset 可以帮你恢复到历史的任何一个版本。 + +> 这里的版本和提交是一个意思,一个 commitId 就是一个版本 + +reset 命令格式如下: + +```sh +git reset [option] [commitId] +``` + +比如,要撤回到某一次提交,命令是这样: + +```sh +git reset --hard cc7b5be +``` + +上面的命令,commitId 是如何获取的?通过之前介绍的`git log`命令即可查看 + +这里的 option 用的是 `--hard`,其实共有 3 个值,具体含义如下: + +- `--hard`:撤销 commit,撤销 add,删除工作区改动代码 +- `--mixed`:默认参数。撤销 commit,撤销 add,还原工作区改动代码 +- `--soft`:撤销 commit,不撤销 add,还原工作区改动代码 + +除了使用 commitId 恢复,git reset 还提供了恢复到上一次提交的快捷方式: + +```sh +git reset --soft HEAD^ +``` + +`HEAD^` 表示上一个提交,可多次使用。 + +其实平日开发中最多的误操作是这样:刚刚提交完,突然发现了问题,比如提交信息没写好,或者代码更改有遗漏,这时需要撤回到上次提交,修改代码,然后重新提交。 + +这个流程大致是这样的: + +```sh +# 1. 回退到上次提交 +git reset HEAD^ +# 2. 修改代码... +... +# 3. 加入暂存 +git add . +# 4. 重新提交 +git commit -m 'fix: ***' +``` + +针对这个流程,git 还提供了一个更便捷的方法: + +```sh +git commit --amend +``` + +这个命令会直接修改当前的提交信息。如果代码有更改,先执行 `git add`,然后再执行这个命令,比上述的流程更快捷更方便。 + +reset 还有一个非常重要的特性,就是**真正的后退一个版本**。 + +什么意思呢?比如说当前提交,你已经推送到了远程仓库;现在你用 reset 撤回了一次提交,此时本地 git 仓库要落后于远程仓库一个版本。此时你再 push,远程仓库会拒绝,要求你先 pull。 + +如果你需要远程仓库也后退版本,就需要 `-f` 参数,强制推送,这时本地代码会覆盖远程代码。 + +#### git revert + +revert 与 reset 的作用一样,都是恢复版本,但是它们两的实现方式不同。 + +简单来说,reset 直接恢复到上一个提交,工作区代码自然也是上一个提交的代码;而 revert 是新增一个提交,但是这个提交是使用上一个提交的代码。 + +因此,它们两恢复后的代码是一致的,区别是一个新增提交(revert),一个回退提交(reset)。 + +正因为 revert 永远是在新增提交,因此本地仓库版本永远不可能落后于远程仓库,可以直接推送到远程仓库,故而解决了 reset 后推送需要加 `-f` 参数的问题,提高了安全性。 + +说完了原理,我们再看一下使用方法: + +```sh +git revert -n [commitId] +``` + +## `git rebase`介绍 + +[官网快速入口](https://git-scm.com/docs/git-rebase) + +常见操作: + +- `git rebase -i head~2`:合并最近两次提交 +- `git rebase -i head~3`:合并最近三次提交 +- `git rebase -i [commitId]`:合并`[commitId]`之前的所有提交,不包括`[commitId]`对应的提交 +- `git rebase master`:同步到主分支:将我在功能分支新的提交尝试提交到最新的`master`分支上 + +基本原理: + +![](https://oss.justin3go.com/blogs/rebase%E5%8E%9F%E7%90%86%E5%9B%BE.png) + +如上图: + +- 我在`历史提交3`部分创建了功能分支并开发新增了两个提交 +- 其他人在主分支上提交了新的代码`更新提交1`与`更新提交2` +- 我们本地拉取最新的`master`分支 +- 然后我们在我们自己的功能分支上`git rebase master` 就可以得到图中下半部分 + +[逐步操作演示可参考](https://juejin.cn/post/6844903600976576519) + +## `git merge`介绍 + +将两个或多个开发历史合并在一起 + +[官网入口](https://git-scm.com/docs/git-merge) + +基本原理: + +![](https://oss.justin3go.com/blogs/merge%E5%8E%9F%E7%90%86%E5%9B%BE.png) + +`merge`会将功能提交和更新提交合并并创建一个新的提交,会有更丰富的提交信息。当然,这个“丰富”在某些情况又可以称为“杂乱”,具体用什么看大家自己的团队规范了。 + +## `git cherry-pick`介绍 + +这个命令非常好用并且简单,它的功能是把已经存在的commit进行挑选,然后重新提交 + +例子: + +在`master`的基础上,`test`进行了2次提交,`normal`进行了1次提交。现在想把`test`的第2次提交 +(仅仅是第2次提交,不包含第1次提交)和`normal`的第1次提交合并到master分支,直接merge分支是行不通的,这样会把两个分支的全部提交都合并到`master`,用`cherry-pick`即可完美的解决问题, 如果`normal`第一次提交的`SHA-1`值是`9b47dd`,`test`第二次提交的值是`dd4e49`,执行如下命令即可把这两个提交合并到`master` + +```sh +git cherry-pick 9b47dd dd4e49 +``` + +如果有冲突,则需要修改冲突文件,然后添加修改文件到暂存区,命令如下: + +```sh +git add main.js +``` + +最后执行 + +```sh +git cherry-pick --continue +``` +cherry-pick后 + +最后要说明的是: + +- 执行完`git cherry-pick --continue`后不需要commit了,该命令会自动提交 +- `git cherry-pick --abort`可以放弃本次`cherry-pick` +- `git cherry-pick 9b47dd dd4e49`和`git cherry-pick dd4e49 9b47dd`这两个的结果可能会**不一样**,**顺序很重要** + + +## 其他快捷操作 + +[通过`git alias`简化命令](https://git-scm.com/book/zh/v2/Git-%E5%9F%BA%E7%A1%80-Git-%E5%88%AB%E5%90%8D) + +https://github.com/commitizen/cz-conventional-changelog + +## 最后 + +本篇文章对原理并没有研究,仅仅演示了使用,并且对一些细节部分也没有一一演示验证,如有理解错误,欢迎友善指出🎉 + +## 参考 + +- https://git-scm.com/docs +- https://juejin.cn/post/7024043015794589727 +- https://backlog.com/git-tutorial/cn/stepup/stepup1_5.html +- https://juejin.cn/post/7064134612129644558 +- https://juejin.cn/post/6844903521993621511 +- https://juejin.cn/post/6844903600976576519 + diff --git "a/docs/\345\215\232\345\256\242/2022/10/15JavaScript\345\237\272\347\241\200-replace\346\226\271\346\263\225\347\232\204\347\254\254\344\272\214\344\270\252\345\217\202\346\225\260.md" "b/docs/\345\215\232\345\256\242/2022/10/15JavaScript\345\237\272\347\241\200-replace\346\226\271\346\263\225\347\232\204\347\254\254\344\272\214\344\270\252\345\217\202\346\225\260.md" new file mode 100644 index 0000000..95375b8 --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2022/10/15JavaScript\345\237\272\347\241\200-replace\346\226\271\346\263\225\347\232\204\347\254\254\344\272\214\344\270\252\345\217\202\346\225\260.md" @@ -0,0 +1,94 @@ +# JavaScript基础-replace方法的第二个参数 + +> 最近又重新看了下高程4,又是不同的收获,其中对`replace`方法印象较深,因为之前做的一个小功能可以用这个方法的第二个参数很轻松轻松地实现,这里简单记录一下。 + +## 基本使用 + +一般来说,用得最多的可能就是`'some string'.replace(/s/gi, 'target')`这样的了 + +其中,第一个参数可以是一个字符串`'some string'.replace('s', 'target')`,也可以是刚才提到的正则表达式,有人讨厌正则表达式,说当你认为这个问题可以用正则表达式来解决的时候,那么你就陷入了另外一个问题,不过我确挺喜欢正则的,几乎所有的字符串处理都可以用其方便地处理,这里地第一个参数没什么可讨论的,接下来就直接讨论第二个参数吧。 + +第二个参数的第一种用法就是刚才使用的也是一个字符串,作用就是替换参数一匹配的字符串,比如刚才的运行结果就是这样,平常这样使用也完全足够了: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221201164355.png) + +其实,第二个参数作为字符串还有一些高阶的用法,就是字符序列,在作为字符串的第二个参数中,`$`符号开头的某些特殊字符会被替换成其它字符,MDN上的表格是这样: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221201164712.png) +第一个变量名`$$`很好理解,就是用来转义的,当我们真正想插入一个`$`字符时,就需要使用`$$`来标识,接下来分别对下面集中变量名进行尝试,都是很好理解的,看看代码就懂了: + +1. `$&`例子:匹配`.com`字符并将其替换为10个一样的,相当于增加了9个`.com` + +```js +console.log('justin3go.com'.replace(/\.\w+/g, '$&'.repeat(10))); +``` + + justin3go.com.com.com.com.com.com.com.com.com.com + +2. 三四例子(基本一致):将目标匹配字符串替换为其左边或右边部分,直接看例子吧: + +```js +console.log('justin3go.com'.replace(/\.\w+/g, '$`')); +console.log('justin3go.com/demo'.replace(/\.\w+/g, "$'")); +``` + + justin3gojustin3go + justin3go/demo/demo + +3. `$n`,第一个匹配的组和第二个匹配的组进行交换 + +```js +console.log('justin3go.com'.replace(/(\w+)\.(\w+)/g, '$2.$1')); +``` + + com.justin3go + +## 当第二个参数为回调函数时 + +回调函数在JavaScript中广泛使用,对于用户自定以一些功能来说特别有用,比如基本的数组迭代方法都是通过回调函数进行处理的`[1,2,3].map(item => item.toString())`等等,还有比如`[2,1,3].sort((a, b) => a-b)`这类 + +这里使用的replace方法同样支持传递一个函数进行自定义操作,基本用法如下: + +MDN中介绍的参数为: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020221201171454.png) +而函数的返回值就是替换后的值 + +举个例子,自实现模板字符串,我们有如下输入: + +```js +const tpl = '欢迎参观我的博客${website},博客的作者为${writer},哈哈哈哈哈😀' +const var2str = { + website: 'justin3go.com', + writer: 'justin3go' +} +``` + +我们需要实现一个转换函数将`tpl`中的变量转换为我们对应的字符串,比如: + +```js +const res = renderTpl(tpl, var2str); +``` + +这时候用`replace`的回调函数就非常方便了,具体实现如下: + +```js +function renderTpl(tpl, var2str) { + const res = tpl.replace(/\${\w+\}/gi, (match) => { + const item = match.substring(2, match.length - 1) + if(item in var2str)return var2str[item] + else console.warn(`未找到可匹配的标识符: [${item}]`); + }) + return res +} +console.log(renderTpl(tpl, var2str)); +``` + + 欢迎参观我的博客justin3go.com,博客的作者为justin3go,哈哈哈哈哈😀 + +## 参考 + +- 《JavaScript》高级程序设计第4版 +- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/replace +- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_Expressions + diff --git "a/docs/\345\215\232\345\256\242/2022/10/16\350\216\267\345\217\226Object\347\232\204\347\254\254\344\270\200\344\270\252\345\205\203\347\264\240.md" "b/docs/\345\215\232\345\256\242/2022/10/16\350\216\267\345\217\226Object\347\232\204\347\254\254\344\270\200\344\270\252\345\205\203\347\264\240.md" new file mode 100644 index 0000000..7c678a5 --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2022/10/16\350\216\267\345\217\226Object\347\232\204\347\254\254\344\270\200\344\270\252\345\205\203\347\264\240.md" @@ -0,0 +1,59 @@ +# 获取Object的第一个元素 + +> 目前遇到个业务需要获取Object中的第一个元素,具体背景这里不详细介绍,如果将数据改为数组的形式改动量较大,需要改接口定义层面,所以这里简单偷个懒 + +## Object中的键值迭代是无序的 + +JS基础中的知识,也经常在一些八股文中看到就是`Map`和`Object`中的区别之一就是Object中的属性是无序的,而Map中的属性是有序的,那我们如何保证我们通过`Object.keys`等方法和`for in`方法迭代的第一个属性是我们预期的第一个呢? + +```ts +const sym = Symbol('foo') +const obj = { + a: '123', + b: '456', + c: '789', + 1: '111', + 2: '222', + 3: '333', +} + +console.log(Object.keys(obj)); +console.log(Object.values(obj)); +``` + [ '1', '2', '3', 'a', 'b', 'c' ] + [ '111', '222', '333', '123', '456', '789' ] + +可以看到这个顺序并不是我们实际定义的顺序,实际情况可能比上述情况更加复杂,所以一般来说都说Obect内部属性的顺序是无序的。 + +## Object中的键值迭代是有规律的 + +这就需要我们我们去确定对象迭代的内部机制是什么,这里直接说结论,具体过程可以参考[这篇文章](https://www.stefanjudis.com/today-i-learned/property-order-is-predictable-in-javascript-objects-since-es2015/)和[这篇文章](https://juejin.cn/post/6932494622661083150) + +1. 数字或者字符串类型的数字当作key时,输出是按照升序排序的 +2. 普通的字符串类型的key,就按照定义的顺序输出 +3. Symbols也是和字符串类型的规则一样 +4. 如果是三种类型的key都有,那么顺序是 1 -> 2 -> 3 + +我这里主要考虑我的业务场景,根据上述结论,也就是说我们只要key是字符串,那么其遍历顺序就是我们定义的顺序,这就符合我们的需求了 + +## 回到主题:获取第一个元素 + +最后,就是获取对象的第一个元素了,这里就不使用什么`for`循环再`break`了,这里可以直接使用解构优雅地获取: + +```ts{6} +const obj = { + a: '123', + b: '234', + c: '345', +} +const [ firstItem ] = Object.values(obj); // 这里 +console.log(firstItem); + +``` + 123 + +## 参考 + +- https://juejin.cn/post/6932494622661083150 +- https://www.stefanjudis.com/today-i-learned/property-order-is-predictable-in-javascript-objects-since-es2015/ + diff --git "a/docs/\345\215\232\345\256\242/2023/01/02JavaScript\344\270\223\351\242\230-\345\216\237\345\236\213\351\223\276.md" "b/docs/\345\215\232\345\256\242/2023/01/02JavaScript\344\270\223\351\242\230-\345\216\237\345\236\213\351\223\276.md" new file mode 100644 index 0000000..da1aec1 --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2023/01/02JavaScript\344\270\223\351\242\230-\345\216\237\345\236\213\351\223\276.md" @@ -0,0 +1,113 @@ +# JavaScript专题-原型链 + +> 此专题系列为又一次重新阅读了《高程4》后,对JavaScript重难点进行了梳理,希望能融会贯通,加深印象,更进一步... + +## 原型定义 + +> 这里通过多方面对原型进行描述,因为其实大家多多少少都接触过原型相关的知识,不理解可能只需要某句话就能点破,希望对你有所帮助。 + +**原型`prototype`其实就是每个构造函数的一个内置属性**,或者说每个函数都有这样一个属性,毕竟所有函数都可以做构造函数,当然箭头函数除外,那只是JS简化一些写法的机制,与普通函数有一定区别。任何时候我们在创建一个普通函数时,都会按照特定的规则为这个函数创建一个`prototype`属性,所以我们可以访问它。 + +```js +function Person(name) { + console.log('exec...'); + this.name = name +} + +Person.prototype.city = 'Beijing' + +Person.prototype.skill = function(){ + console.log('coding...'); +} + +``` + +**这个`prototype`属性是一个对象**,这个原型对象上的属性和方法都可以被对应的构造函数创建的实例所共享,这点也是原型最重要的性质之一。 + +```js +const p1 = new Person('Justin3go'); +const p2 = new Person('XXX'); + +console.log(p1.name, p2.name); +console.log(p1.city, p2.city); +p1.skill(); +p2.skill(); +``` + exec... + exec... + Justin3go XXX + Beijing Beijing + coding... + coding... + +> **值得注意**的是,实例并没有`prototype`属性,只有构造函数拥有该属性。如果你了解`new`操作符的过程的话可能对此比较清楚,它只是在实例化的过程中会把构造函数的`prototype`赋值给实例的一个内部特性指针`[[Prototype]]`上,然后浏览器会在每个实例对象上暴露`__proto__`执行访问操作。后续ES6才规范了`Object.getPrototype()`方法访问原型 + +我们可以把原型对象作为每个相关实例的上层作用域,通俗来说就是实例上没有的变量名,会往上层作用域找,这里就是先找的自己的原型对象里面是否包含该变量名;既然是作用域,当实例上包含和原型同名的方法或属性时,访问的就只会是实例自己定义的了,这就是常说的覆盖。 + +这个原型对象中除了自定义的属性和方法,还有就是一个特殊的属性叫做`constructor`,其指向构造函数。这样,所有的实例都可以访问该属性从而获取自己的构造函数了 + +## 深入理解原型 + +这里我们再来梳理一下这个过程: + +1. 首先我们创建了一个构造函数想要去生成一些实例对象 +2. JS会自动给这个构造函数生成一个原型对象 +3. 然后我们基于原型的特性把想要共享的属性和方法添加到了构造函数的原型上 +4. 之后我们实例化的时候会将构造函数的原型对象赋值给实例对象中的内部指针,注意赋值不是复制,只是指向,实例和构造函数的原型都是一个 +5. 然后我们访问实例对象中的属性,发现实例本身没有,就会自动去找原型上的 + +![](https://oss.justin3go.com/blogs/%E5%8E%9F%E5%9E%8B%E9%93%BEdemo%E7%9A%84%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B.png) + +然后在这个例子中,我们再来梳理一下关于构造函数、实例、原型的一个关系,下面这个图就可以非常清晰明了的表达了: + +![](https://oss.justin3go.com/blogs/%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0_%E5%AE%9E%E4%BE%8B_%E5%8E%9F%E5%9E%8B%E7%9A%84%E5%85%B3%E7%B3%BB.png) + +## 原型链 + +在JavaScript中,我们都知道每个对象都有一个`[[Prototype]]`指针指向其原型对象,而原型对象也是对象,所以原型对象也包含一个`[[Prototype]]`指针指向更上一层的原型对象。这就是形成我们常说的原型链的基础。 + +![](https://oss.justin3go.com/blogs/%E5%8E%9F%E5%9E%8B%E9%93%BE.png) + +我们再简化一下这张图,让你对链的加深一下记忆: + +![](https://oss.justin3go.com/blogs/%E5%8E%9F%E5%9E%8B%E9%93%BE%E7%AE%80%E7%89%88.png) + +## 关于原型对象中的`constructor`属性 + +这里说说我们经常见到的一个问题就是为什么不要使用对象的`constructor`属性来判断该对象属于哪类: + +```js +const arr1 = new Array(); +console.log(arr1.constructor === Array); + +function Person(){}; +const p1 = new Person(); +console.log(p1.constructor === Person); +``` + true + true + +`constructor`虽然可以拿来判断类型,但是不是百分百准确的,比如如果创建一个对象来改变它的原型,`constructor`就不能用来判断数据类型了 + +```js +function Person(){}; +Person.prototype = { + skill: function(){ + console.log('coding...'); + }, + city: "beijing" + +} +const p1 = new Person(); +console.log(p1.constructor === Person); +console.log(p1.constructor); +``` + false + [Function: Object] + +这是因为我们是以对象字面量`{}`来直接对原型进行赋值的,而之前是通过点操作符增加属性的,前者是完全覆盖,所以原型改变了,而`{}`是`Object()`的简化方式,所以此时该原型的`constructor`就等于`Object`了,所以这里就是`false`了 + +## 参考 +- 《JavaScript高级程序设计》(第4版) +- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain + diff --git "a/docs/\345\215\232\345\256\242/2023/01/08JavaScript\344\270\223\351\242\230-\347\273\247\346\211\277.md" "b/docs/\345\215\232\345\256\242/2023/01/08JavaScript\344\270\223\351\242\230-\347\273\247\346\211\277.md" new file mode 100644 index 0000000..c9318a6 --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2023/01/08JavaScript\344\270\223\351\242\230-\347\273\247\346\211\277.md" @@ -0,0 +1,268 @@ +# JavaScript专题-继承 + +> 此专题系列将对JavaScript重难点进行梳理,希望能融会贯通,加深印象,更进一步... + +本章需要你比较熟悉原型链相关的知识,如果你还不熟悉或者略有忘记,可以看看我的往期文章([JavaScript专题-原型链](https://justin3go.com/%E5%8D%9A%E5%AE%A2/2023/01/2JavaScript%E4%B8%93%E9%A2%98-%E5%8E%9F%E5%9E%8B%E9%93%BE.html) + +## 各种方法整体认识 + +我们首先梳理一下各种继承实现的方法的进化史,这样更方便我们的记忆,从上往下都是上面有一定的缺点不能忍受,由此产生了对应下方的继承实现,最终寄生组合式继承结合上述优点成为最优的一种继承实现,包括后续官方ES6的继承extends也仅仅是这种实现的语法糖; + +![](https://oss.justin3go.com/blogs/%E5%90%84%E7%A7%8D%E7%BB%A7%E6%89%BF%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F.png) + +## 1.原型链方式 + +### 继承实现 + +基本思想就是通过原型链继承多个引用类型的属性和方法,关键语句就是将父类型作为子类型的原型: + +```js{14} +function SuperType() { + this.property = true; +} + +SuperType.prototype.getSuperValue = function() { + return this.property; +}; + +function SubType() { + this.subproperty = false; +} + +// 继承SuperType +SubType.prototype = new SuperType(); + +SubType.prototype.getSubValue = function () { + return this.subproperty; +}; +let instance = new SubType(); +console.log(instance.getSuperValue()); // true +``` + +这样`SuperType`实例中可以访问的所有属性和方法也会存在于`SubType.prototype`了 + +### 判断继承关系 + +**方式一:`instanceof`** + +```js +console.log(subType instanceof SuperType) // true +``` + +**方式二:`isPrototypeOf()`** + +```js +console.log(SuperType.prototype.isPrototypeOf(subType)) // true +``` + +### 缺点 + +- 主要问题出现在原型中包含引用值的时候,原型中包含的引用值会在所有实例间共享,即通过该方式实现的继承,如果父类包含引用值,该引用值就会在子类的所有实例中共享。 +- 子类在实例化时不能给父类型的构造函数传参 + +## 2.盗用构造函数 + +### 继承实现 + +基本思路就是在子类构造函数中调用父类构造函数: + +```js{7} +function SuperType() { + this.colors = ["red", "blue", "green"]; +} + +function SubType() { + //继承SuperType + SuperType.call(this); +} + +let instance1 = new SubType(); +instance1.colors.push("black"); +console.log(instance1.colors); // "red, blue, green, black" +let instance2 = new SubType(); +console.log(instance2.colors); // "red, blue, green" + +``` + +这相当于新的`SubType`对象上运行了`SuperType()`函数中的所有初始化代码,结果就是子类的每个实例上都包含父类的属性和方法。在这里就是每个子实例都会拥有属于自己的`colors`属性,注意这与原型链实现的不同,原型链的方式是所有实例共享一个,而这里是为每个实例都新建了一个。 + +并且还有一个优点就是可以在子类构造函数中向父类构造函数传参 + +### 缺点 + +必须在构造函数中定义方法,因此函数不能重用,就是原型链的实现方式会导致我们不想要共享的属性(比如引用值)也跟着共享了,而盗用构造函数的实现方式会导致我们想要共享的通用方法也跟着都初始化了一次,就是实例化一次对象就初始化一次该方法。 + +## 3.组合式继承 + +### 继承实现 + +其实根据上述的缺点我们隐约就知道接下来的实现方式是什么样的了,就是组合式继承,上述两种继承实现方式明显就是相互补充的,所以这里结合他们从实现目的:我们可以根据需求做到有些属性或方法共享,而有些属性和方法不共享,具体哪些方法可以由我们自己决定。 + +> 这里的组合式继承就可以实现这样的效果:既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。 + +```js{6,11-12,16-17} +function SuperType(name){ + this.name = name; + this.colors = ["red", "blue", "green"]; +} + +SuperType.prototype.sayName = function() { + console.log(this.name); +}; + +function SubType(name, age){ + // 继承属性 + SuperType.call(this, name); + this.age = age; +} + +// 继承方法 +SubType.prototype = new SuperType(); +SubType.prototype.sayAge = function() { + console.log(this.age); +}; + +let instance1 = new SubType("Nicholas", 29); +instance1.colors.push("black"); +console.log(instance1.colors); // "red, blue, green, black" +instance1.sayName(); // "Nicholas"; +instance1.sayAge(); // 29 +let instance2 = new SubType("Greg", 27); +console.log(instance2.colors); // "red, blue, green" +instance2.sayName(); // "Greg"; +instance2.sayAge(); // 27 +``` + +### 缺点 + +该实现方式基本达到了我们想要的目的,但还有一个致命的缺点就是: + +调用了两次父类的构造函数: + +1. `SuperType.call(this, name);` +2. `let instance1 = new SubType("Nicholas", 29);` + +我们用白话描述一下这个过程:首先父类需要将方法写在原型上而不是作为自身的属性。然后通过盗用构造函数将所有的属性继承下来,最后通过原型链继承的方式将父类作为子类的原型,注意这里是将整个父类作为了子类的原型,并不是直接复制父类的原型。所以原型里面包含了父类的属性,即和子类的属性重复了,只不过这里是在原型,会被同名属性遮蔽而已,但也浪费了存储空间,增加了初始化的消耗 + +*关键就是我们通过原型链方式继承的时候使用的是整个父类的实例,导致子类的实例的原型上拥有了我们不需要共享的属性,这里其实就能想到一个基本的思路就是使用父类的原型赋值到子类的原型上,接下来详细讲一下这个继承实现方式。* + +## 4.原型式继承 + +### 继承实现 + +**你可以简单地将这种方式理解为`1.原型链继承`的一个封装**,下面这个函数就是这种方式的关键思想: + +```js +function object(o) { + function F() {}; + F.prototype = o; + return new F(); +} +``` + +比如在1.原型链方式中,我们是这样实现继承的: + +```js +SubType.prototype = new SuperType(); +let instance = new SubType(); +``` + +而有了这个函数,我们就可以这样实现继承: + +```js +let instance = object(new SuperType()) +``` + +这种方式将原型赋值隐藏在了函数内部,方便开发者更加灵活地操作,其中关键的操作就是`object(o)`中的`o`参数不仅仅可以传入一个父对象的实例,还可以传入任何一个对象,**比如我们可以传入父对象的原型对象**,这样我们就可以非常方便地复制父对象地原型对象,这也是我们后面解决组合式继承缺点的关键一步 + +### 扩展 + +ES5通过增加`Object.create()`方法将原型式继承的概念规范化了,这个函数可以传入两个参数,当只传入一个参数的时候,功能就和前面给的`object(o)`的代码效果相同了; + +```js +let instance = Object.create(new SuperType()) +``` + +*原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合* + +### 缺点 + +因为关键也是使用`1.原型链方式`实现的,所以缺点也是一样的,主要就是属性中包含的引用值始终会在相关对象间共享。 + +## 5.寄生式继承 + +### 继承实现 + +基本思路就是:创建一个实现继承的函数,以某种方式**增强对象**,然后返回这个对象。 + +就是比如我们使用前面的`let instance = object(new SuperType())`复制了父对象中的属性和方法,然后我们在此基础上增加一些我们需要自定义的一些新的属性和方法在`instance`上,这个操作就叫做增强这个对象,然后整个这种方式就是寄生式继承; + +```js{1,3} +function createAnother(original){ + let clone = object(original); // 通过调用函数创建一个新对象 + clone.sayHi = function() { // 以某种方式增强这个对象 + console.log("hi"); + }; + return clone; // 返回这个对象 +} + +// 使用该函数 +let person = { + name: "Nicholas", + friends: ["Shelby", "Court", "Van"] +}; +let anotherPerson = createAnother(person); +anotherPerson.sayHi(); // "hi" +``` + +### 缺点 + +注意我们在`createAnother()`函数中有一些自定义的方法,而这些方法在我们每次调用`createAnother()`函数创建一个新的实例的时候都会重新初始化一次,在这里就是上方代码中的第三行。 + +> 我们可以这样理解,通过寄生式继承这种方式的缺点其实和`2.盗用构造函数`的方式差不多,都是我们想要共享的方法也会跟着每次创建实例的时候都初始化一次。其实这里的`createAnother()`的效果和盗用构造函数是基本一致的 + +注意这里我们是在克隆对象本身上进行增强(添加方法),其实根据原型的知识,我们只需要在克隆的原型上添加我们想要共享的方法就可以了,当然具体实现稍微复杂一点,就是我们接下来要讲的终极解决方法`6.寄生式组合继承` + +## 6.寄生式组合继承 + +在组合式继承中基本能达到我们预期中继承实现效果的目标,但是组合式继承存在一定的效率问题:就是父类构造函数始终会调用两次,一次是在创建子类原型时调用,另外一次就是在子类构造函数中调用,而现在的寄生式组合继承就是结合上面寄生式继承的思想来解决这个问题的。 + +基本思路就是:不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。 + +```js +function inheritPrototype(subType, superType) { + let prototype = object(superType.prototype); // 创建父类原型副本 + prototype.constructor = subType; // 增强对象,解决由于重写原型导致默认constructor丢失的问题 + subType.prototype = prototype; // 将父类原型的副本赋值给子类原型 +} +``` + +接下来,我们再稍微修改一下组合式继承的方式(结合上面寄生式的思想)就可以解决组合式继承的缺点了: + +```js{10,13} +function SuperType(name) { + this.name = name; + this.colors = ["red", "blue", "green"]; +} +SuperType.prototype.sayName = function() { + console.log(this.name); +}; + +function SubType(name, age) { + SuperType.call(this, name); + this.age = age; +} +inheritPrototype(SubType, SuperType); +SubType.prototype.sayAge = function() { + console.log(this.age); +}; +``` + +这里就没有将整个父类赋值给子类的原型了,而仅仅是赋值了父类的原型给子类,最终这里只调用了一次`SuperType`构造函数,避免了`SubType.prototype`上不必要的属性。 + +## 参考 + +- 《JavaScript高级程序设计》(第四版) +- https://juejin.cn/post/6844903696111763470 +- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain + diff --git "a/docs/\345\215\232\345\256\242/2023/01/25\346\265\205\350\260\210NestJS\350\256\276\350\256\241\346\200\235\346\203\263.md" "b/docs/\345\215\232\345\256\242/2023/01/25\346\265\205\350\260\210NestJS\350\256\276\350\256\241\346\200\235\346\203\263.md" new file mode 100644 index 0000000..ae4a6f0 --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2023/01/25\346\265\205\350\260\210NestJS\350\256\276\350\256\241\346\200\235\346\203\263.md" @@ -0,0 +1,107 @@ +# 浅谈NestJS设计思想(分层、IOC、AOP) + +> nestJS用了有一定时间了,当初学习node后端选择的第一个web框架,这篇文章将对NestJS框架层面的几个重要概念进行梳理,希望能加深记忆,融汇贯通,更进一步,本文阅读需要对nestJS有一定使用经验。 + +## 分层 + +nestJS经常被调侃为srpingJS,所以这里参考java项目的[阿里分层规范](https://www.kancloud.cn/kanglin/java_developers_guide/539198),其架构图如下: + +> 图中默认上层依赖于下层,箭头关系表示可直接依赖,如:开放接口层可以依赖于Web层,也可以直接依赖于Service层,依此类推: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230125151727.png) + +- **开放接口层**:可直接封装Service方法暴露成RPC接口;通过Web封装成http接口;进行网关安全控制、流量控制等。 +- **终端显示层**:各个端的模板渲染并执行显示的层。当前主要是velocity渲染,JS渲染,JSP渲染,移动端展示等。 +- **Web层**:主要是对访问控制进行转发,各类基本参数校验,或者**不复用**的业务简单处理等。 +- **Service层**:相对具体的业务逻辑服务层。 +- **Manager层**:通用业务处理层,它有如下特征: + 1. 对第三方平台封装的层,预处理返回结果及转化异常信息; + 2. 对Service层通用能力的下沉,如缓存方案、中间件通用处理; + 3. 与DAO层交互,对多个DAO的组合复用。 +- **DAO层**:数据访问层,与底层MySQL、Oracle、Hbase等进行数据交互。 +- **外部接口或第三方平台**:包括其它部门RPC开放接口,基础平台,其它公司的HTTP接口。 + +不同的业务场景,不同的应用大小,程序复杂度高低,可以灵活的增删上述某些结构。无论是nest还是egg,官方demo里都没有明确提到dao层,直接在service层操作数据库了。这对于简单的业务逻辑没问题,如果业务逻辑变得复杂,service层的维护将会变得非常困难。业务一开始一般都很简单,它一定会向着复杂的方向演化,如果从长远考虑,一开始就应该保留dao层,在nestJS中并未查看到相关规定,可根据开发者场景自行考虑。如下是nestJS的分层架构图: + +![](https://oss.justin3go.com/blogs/nestjs%E5%88%86%E5%B1%82.png) + +对于Web层:在nestJS中,如果使用restful风格,就是controller;如果使用graphql规范,就是resolver...对于同一个业务逻辑,我们可以使用不同的接口方式暴露出去。 + +经常被问到和提起的问题就是**为什么需要有service层**: +- 首先service作用就是在里面编写业务逻辑代码,一般来说,都是为了增加代码复用率,实现高内聚,低耦合等... +- 体现在这里的好处就是上述提到的同一段业务代码可以使用不同的接口方式暴露出去,或者可以在一个service内调用其他service,而非在一个接口函数里面调用另外一个内部接口,这是极其不优雅的。 +- 当然,老生常谈的就是不同功能目的的代码分开写方便维护管理等等 + +> 有关nestJS实战入门可以参考我之前写的[这一系列文章](https://justin3go.com/%E7%9F%A5%E8%AF%86%E5%BA%93/NestJS/01controller.html)或者[官方链接](https://docs.nestjs.com/first-steps) + +## IOC(Inversion of Control) + +中文为控制反转,是[面向对象程序设计](https://zh.wikipedia.org/wiki/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1)的一种设计原则,下面简单认识一下为什么需要IOC,IOC有什么好处,简单来说就是减少了固定性,通过外部传参进行控制内部本身固定的一些变量,如下例子: + +在我们的代码中,经常会出现一个类依赖于另外一个类的情况,比如这样: + +```ts +class Dog {} + +class Person { + private _pet + + constructor () { + this._pet = new Dog() + } +} + +const xiaoming = new Person() +``` + +在上述例子中: +- `Person`类固定依赖于`Dog`类,如果后续`Person`想要依赖于其他宠物类,是无法轻易修改的。 +- 并且如果`Dog`类有所变化,比如其属性颜色染成了黑色,`Person`类也会直接受到影响。 + +IOC的思想就是将类的依赖动态注入,以解决上述两个问题: + +```ts +class Dog {} + +class Person { + private _pet + + constructor (pet) { + this._pet = pet + } +} + +const xiaohei = new Dog() +const xiaoming = new Bird(xiaohei) // 将实例化的 dog 传入 person 类 +``` + +这样,我们就实现了类的控制反转,同时,我们需要有一个容器来维护各个对象实例,当用户需要使用实例时,容器会自动将对象实例化给用户,这部分通常由框架处理,结合nestJS框架进行理解的话可以参考我之前写的这篇笔记--[nestJS原理细节](https://justin3go.com/%E7%9F%A5%E8%AF%86%E5%BA%93/NestJS/06%E5%8E%9F%E7%90%86%E7%BB%86%E8%8A%82.html)或者[官方文档](https://docs.nestjs.com/fundamentals/custom-providers) + +这种动态注入的思想叫做**依赖注入**(DI, Dependency Injection),它是 `IoC` 的一种应用形式。 + +## AOP(Aspect Oriented Programming) + +中文为面向切面编程。当一个请求打过来时,一般会经过 Controller(控制器)、Service(服务)、Repository(数据库访问) 的链路。当我们不使用AOP时,需要添加一些通用逻辑时(如日志记录、权限守卫、异常处理等等),就需要在每段请求逻辑中编写相关代码。 + +AOP就是在所有请求外面包裹一层切面,所有请求都会经过这个切面,然后我们就可以把上述的通用逻辑放在这个结构里,如下图: + +![](https://oss.justin3go.com/blogs/AOP.png) + +在nestJS中实现AOP的方式有很多,比如(excepion filter、pipes、guards、interceptors),相关介绍可参考我之前的这篇笔记--[更多模块](https://justin3go.com/%E7%9F%A5%E8%AF%86%E5%BA%93/NestJS/08%E6%9B%B4%E5%A4%9A%E6%A8%A1%E5%9D%97.html#%E5%9F%BA%E6%9C%AC)或者[官方文档](https://docs.nestjs.com/exception-filters) + +## 最后 + +这些思想架构都需要长期的经验体会才更深,我开发经验不足,更多是参考网上的文章和自己非常浅薄的经验进行理解,如有理解错误,欢迎友善指出... + +## 参考 +- [nestJS官方文档](https://docs.nestjs.com/) +- [nestJS原理细节](https://justin3go.com/%E7%9F%A5%E8%AF%86%E5%BA%93/NestJS/06%E5%8E%9F%E7%90%86%E7%BB%86%E8%8A%82.html) +- [什么是 AOP 和 IoC](https://hentaicracker.github.io/2020/aopioc.html) +- [Nest.js 的 AOP 架构的好处,你感受到了么?](https://juejin.cn/post/7076431946834214925#heading-8) +- [java为什么要分为service层,dao层,controller层?](https://www.zhihu.com/question/431911268) +- [mvc与三层结构终极区别](https://blog.csdn.net/csh624366188/article/details/7183872) +- [nest后端开发实战(二)——分层](https://segmentfault.com/a/1190000016992060) +- [Web开发的历史发展技术演变](https://segmentfault.com/a/1190000023740835) +- [Java 项目如何分层](https://xie.infoq.cn/article/e50e460c9723825aea4851c06) +- [阿里巴巴java开发手册](https://www.kancloud.cn/kanglin/java_developers_guide/539198) + diff --git "a/docs/\345\215\232\345\256\242/2023/01/28\344\272\206\350\247\243API\347\233\270\345\205\263\350\214\203\345\274\217(RPC\343\200\201REST\343\200\201GraphQL).md" "b/docs/\345\215\232\345\256\242/2023/01/28\344\272\206\350\247\243API\347\233\270\345\205\263\350\214\203\345\274\217(RPC\343\200\201REST\343\200\201GraphQL).md" new file mode 100644 index 0000000..cc3e054 --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2023/01/28\344\272\206\350\247\243API\347\233\270\345\205\263\350\214\203\345\274\217(RPC\343\200\201REST\343\200\201GraphQL).md" @@ -0,0 +1,301 @@ +# 了解API相关范式(RPC、REST、GraphQL) + +## 前言 + +两个独立的应用程序经常需要相互访问交谈,或则可以是同一个应用程序,但部署在不同的服务器,或者现在常用的前后端分离式架构等等需要经常相互访问交谈,因此开发人员经常搭建桥梁API(Application Programming Interfaces) + +> 关于API的定义,你可以简单看看这篇文章-- [What is an API: Definition, Types, Specifications, Documentation](https://www.altexsoft.com/blog/engineering/what-is-api-definition-types-specifications-documentation/) + +关于历史出现的API范式,我们可以参考 [Rob Crowley](https://twitter.com/robdcrowley?lang=en)的这张图: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230127165636.png) + +目前使用的较多的就是RPC、REST、GraphQL,接下来将会对这三种范式进行优缺点的讨论... + +## RPC(Remote Procedure Call) + +### 简介 + +RPC 出现的最初目的,就是**为了让计算机能够跟调用本地方法一样去调用远程方法**,我们可以简单理解为一个本地方法调用+网络通信 + +### 工作过程 + +客户端调用远程过程,将参数和附加信息序列化为消息,并将消息发送到服务器。收到消息后,服务器反序列化其内容,执行请求的操作,并将结果发送回客户端。 + +![](https://oss.justin3go.com/blogs/RPC%E5%B7%A5%E4%BD%9C%E8%BF%87%E7%A8%8B.png) + +### 相关规范 + +**RPC只是一个概念,但是这个概念有很多规范,都有具体的实现**,如:RMI(Sun/Oracle)、Thrift(Facebook/Apache)、Dubbo(阿里巴巴/Apache)、gRPC(Google)、Motan1/2(新浪)、Finagle(Twitter)、brpc(百度/Apache)、.NET Remoting(微软)、Arvo(Hadoop)、JSON-RPC 2.0(公开规范,JSON-RPC 工作组) + +参考[《凤凰架构》](http://icyfenix.cn/architect-perspective/general-architecture/api-style/rpc.html): + +> - 朝着**面向对象**发展,不满足于 RPC 将面向过程的编码方式带到分布式,希望在分布式系统中也能够进行跨进程的面向对象编程,代表为 RMI、.NET Remoting,之前的 CORBA 和 DCOM 也可以归入这类,这条线有一个别名叫做[分布式对象](https://en.wikipedia.org/wiki/Distributed_object)(Distributed Object)。 +> - 朝着**性能**发展,代表为 gRPC 和 Thrift。决定 RPC 性能的主要就两个因素:序列化效率和信息密度。序列化效率很好理解,序列化输出结果的容量越小,速度越快,效率自然越高;信息密度则取决于协议中有效荷载(Payload)所占总传输数据的比例大小,使用传输协议的层次越高,信息密度就越低,SOAP 使用 XML 拙劣的性能表现就是前车之鉴。gRPC 和 Thrift 都有自己优秀的专有序列化器,而传输协议方面,gRPC 是基于 HTTP/2 的,支持多路复用和 Header 压缩,Thrift 则直接基于传输层的 TCP 协议来实现,省去了额外应用层协议的开销。 +> - 朝着**简化**发展,代表为 JSON-RPC,说要选功能最强、速度最快的 RPC 可能会很有争议,但选功能弱的、速度慢的,JSON-RPC 肯定会候选人中之一。牺牲了功能和效率,换来的是协议的简单轻便,接口与格式都更为通用,尤其适合用于 Web 浏览器这类一般不会有额外协议支持、额外客户端支持的应用场合。 + +### 优点 + +> 实现 RPC 的可以传输协议可以直接建立在 TCP 之上,也可以建立在 HTTP 协议之上。**大部分 RPC 框架都是使用的 TCP 连接(gRPC使用了HTTP2)。** + +- 调用简单,清晰,透明,不用像 rest 一样复杂,就像调用本地方法一样简单(同样也是缺点,就是后续提到的耦合度强) +- 高效低延迟,性能高 +- **使用自定义 TCP 协议进行传输可以极大地减轻了传输数据的开销。** 这也就是为什么通常会采用自定义 TCP 协议的 RPC 来进行进行服务调用的真正原因。 +- 成熟的 RPC 框架还提供好了“服务自动注册与发现”、"智能负载均衡"、“可视化的服务治理和运维”、“运行期流量调度”等等功能减轻开发者心智负担 + +### 缺点 + +- **与底层系统紧密耦合**:API 的抽象级别有助于其可重用性。它对底层系统越紧密,对其他系统的可重用性就越低。RPC 与底层系统的紧密耦合不允许在系统功能和外部 API 之间存在抽象层。这会引发安全问题,因为很容易将有关底层系统的实现细节泄露到 API 中。RPC 的紧耦合使得可扩展性需求和松耦合团队难以实现。因此,客户端要么担心调用特定端点的任何可能的副作用,要么尝试找出要调用的端点,因为它不了解服务器如何命名其功能。 +- **各个函数可能复用率低**:创建新功能非常容易(这也可以算作优点之一)。因此,可能经常没有编辑现有的,而是创建了新的,最终得到了大量难以理解的重叠功能。 + +## REST(Representational state transfer) + +### 介绍 + +REST – REpresentational State Transfer + +> RESTful实例可以查看我之前写的Django[这个笔记](https://justin3go.com/%E7%9F%A5%E8%AF%86%E5%BA%93/%E5%90%8E%E7%AB%AF%E5%82%A8%E5%A4%87/02DRF%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0.html#restful%E4%BB%8B%E7%BB%8D)或者NestJS[这个笔记](https://justin3go.com/%E7%9F%A5%E8%AF%86%E5%BA%93/NestJS/01controller.html),如果你使用过REST,可以直接忽略这段话,继续下面的阅读。 + +下面谈谈偏理论的东西,如何理解REST: + +REST本质上是面向资源编程,这也是与RPC面向过程编程最主要的区别,需要注意的是,REST只是一种风格,不遵循它编译器也不会报错,只是不能享受到对应的一些好处罢了,需要设计者灵活考虑。 + +既然是面向资源编程,所以我们可以这样理解一个符合RESTful的接口: + +- 看URL就知道我们的目标是什么资源 +- 看方法就知道我们需要对该资源进行什么样的操作 +- 看返回码就知道操作的结果如何 + +比如我们要获取这个编号的咖啡信息 + +```sh +curl -X GET https://api.justin3go.com/coffees/1 +``` + +### REST的指导原则 + +REST的指导原则部分内容引用[该篇文档](https://restfulapi.net/)的机翻: + +- **统一接口** + + 通过将 [通用性原则](https://www.d.umn.edu/~gshute/softeng/principles.html)应用于 组件接口,我们可以简化整体系统架构并提高交互的可见性。 + + 多个体系结构约束有助于获得统一的接口并指导组件的行为。 + + 以下四个约束可以实现统一的REST接口: + + - **资源标识** ——接口必须唯一标识客户端和服务器之间交互中涉及的每个资源。 + - **通过表示操作资源** ——资源在服务器响应中应该有统一的表示。API 消费者应该使用这些表示来修改服务器中的资源状态。 + - **自描述消息** ——每个资源表示都应该携带足够的信息来描述如何处理消息。它还应提供有关客户端可以对资源执行的其他操作的信息。 + - **超媒体作为应用程序状态的引擎** ——客户端应该只有应用程序的初始 URI。客户端应用程序应使用超链接动态驱动所有其他资源和交互。 + +- **客户端服务器** + + 客户端-服务器设计模式强制 **关注点分离**,这有助于客户端和服务器组件独立发展。 + + 通过将用户界面问题(客户端)与数据存储问题(服务器)分开,我们提高了跨多个平台的用户界面的可移植性,并通过简化服务器组件提高了可扩展性。 + + 随着客户端和服务器的发展,我们必须确保客户端和服务器之间的接口/契约不会中断。 + +- **无状态** + + [无状态](https://restfulapi.net/statelessness/) 要求从客户端到服务器的每个请求都必须包含理解和完成请求所需的所有信息。 + + 服务器无法利用服务器上任何先前存储的上下文信息。 + + 为此,客户端应用程序必须完全保持会话状态。 + +-  **可缓存** + + 可 [缓存约束](https://restfulapi.net/caching/) 要求响应应隐式或显式将自身标记为可缓存或不可缓存。 + + 如果响应是可缓存的,则客户端应用程序有权在以后的等效请求和指定时间段内重用响应数据。 + + 分层系统 + + 分层系统风格允许架构通过约束组件行为由分层层组成。 + + 例如,在分层系统中,每个组件都无法看到与其交互的直接层之外的信息。 + +- 按需代码(_可选_) + + REST 还允许通过下载和执行小程序或脚本形式的代码来扩展客户端功能。 + + 下载的代码通过减少需要预先实现的功能数量来简化客户端。服务端可以将部分特性以代码的形式交付给客户端,客户端只需要执行代码即可。 + +### 优点 + +- **解耦客户端和服务器**:耦合性低,兼容性好,提高开发效率 +- 不用关心接口实现细节,相对更规范,更标准,更通用,跨语言支持 +- **缓存友好**:重用大量 HTTP 工具,REST 是唯一允许在 HTTP 级别缓存数据的样式。相比之下,任何其他 API 上的缓存实现都需要配置额外的缓存模块。 +- **多种格式支持**:支持多种格式来存储和交换数据 + +### 缺点 + +- **没有统一的REST结构**:正如之前所说,只是一种风格,有一些指导原则,所以构建REST API没有完全正确的方法。如何建模资源以及建模哪些资源仍灵活多变,取决于业务场景。**这使得REST理论上很简单但实践中较为困难**。 +- **高负载**:REST API会返回大量丰富的元数据,方便客户端仅从响应中就可以了解有关应用程序状态的所有必要信息,随之而来的就是一定的性能问题(高负载)。这个缺点和下面一个缺点也是后续GraphQL被提出的主要原因。 +- **过度获取和获取不足**:无法预估后续业务场景会如何变化,这导致了最初设计的API很难根据业务场景不断变化并且不能影响到之前的业务。 + +## GraphQL(Graph query language) + +### 介绍 + +> 如果你熟悉REST,但不熟悉GraphQL,推荐阅读这篇文章--[GraphQL vs. REST](https://www.apollographql.com/blog/graphql/basics/graphql-vs-rest/),里面有较为详细的对比与介绍 + +首先来说,它是一种查询语言,具有一定的语法规则(即学习成本--有编程基础的话较小),可以解决上述REST中的一些问题。 + +引用[官网](https://graphql.org/)的介绍: + +> GraphQL 是一种用于 API 的查询语言,也是使用现有数据完成这些查询的运行时。GraphQL 为您的 API 中的数据提供了完整且易于理解的描述,使客户能够准确地询问他们需要什么,仅此而已,随着时间的推移更容易发展 API,并启用强大的开发人员工具。 + + +### Q&A + +这里引用一下[官网的FAQ](https://graphql.org/faq/#how-does-graphql-affect-my-product-s-performance) + +**ls GraphQL a database language like SQL?** + +> No, but this is a common misconception. +> +> GraphQL is a specification typically used for remote client-server communications. Unlike SQL, GraphQL is agnostic to the data source(s) used to retrieve data and persist changes. Accessing and manipulating data is performed with arbitrary functions called [resolvers](https://graphql.org/learn/execution/). GraphQL coordinates and aggregates the data from these resolver functions, then returns the result to the client. Generally, these resolver functions should delegate to a [business logic layer](https://graphql.org/learn/thinking-in-graphs/#business-logic-layer) responsible for communicating with the various underlying data sources. These data sources could be remote APIs, databases, [local cache](https://graphql.org/learn/caching/), and nearly anything else your programming language can access. +> +> For more information on how to get GraphQL to interact with your database, check out our [documentation on resolvers](https://graphql.org/learn/execution/#root-fields-resolvers). + +**Does GraphQL replace REST?** + +> No, not necessarily. They both handle APIs and can [serve similar purposes](https://graphql.org/learn/thinking-in-graphs/#business-logic-layer) from a business perspective. GraphQL is often considered an alternative to REST, but it’s not a definitive replacement. +> +> GraphQL and REST can actually co-exist in your stack. For example, you can abstract REST APIs behind a [GraphQL server](https://www.howtographql.com/advanced/1-server/). This can be done by masking your REST endpoint into a GraphQL endpoint using [root resolvers](https://graphql.org/learn/execution/#root-fields-resolvers). +> +> For an opinionated perspective on how GraphQL compares to REST, check out [How To GraphQL](https://www.howtographql.com/basics/1-graphql-is-the-better-rest/). + + + +看到上述两个FAQ我自己蹦出了这样的想法: + +首先我想到的是一个比较荒谬的问题:既然GraphQL是一种查询语言,SQL也是一种查询语言,那为什么不直接前端传入sql直接拿数据呢? + +> 显然这是有很多问题的,最主要的问题就是这相当于无后端,全部逻辑都集中在了客户端,这对于客户端的压力是非常大的,并且也是非常不安全的,就类似于破解单机游戏一样... +> +> 高耦合的话我理解前端程序也可以进行多层抽象来解耦,比如MVC这类。但这又要重新经历一次类似的web架构演变,对现有的生态也是极大的破坏... +> +> 上述只是一些胡乱猜想,不必当真,回到这里的话GraphQL就是对后端提供的GraphQL运行时查询的语言,官方语法为SDL。而这个运行时是应用程序业务逻辑外面的一层接口暴露,我们开发人员需要对每一个接口业务字段 + + +然后与REST的区别我理解就是:二者本质都可以理解为面向资源编程 + +- GraphQL通过一个运行时,使用规定的语法可以更加精准灵活地操作资源(灵活度也是有一定限度的,只是相对来说) +- 而REST就能根据提前定义好地URL,通过不同的方法操作资源 + +这部分可能各有各的想法思考,欢迎友善讨论~ + +### 工作过程 + +> 在查询之前需要schema,客户端可以验证他们的查询以确保服务器能够响应它。在到达后端应用程序时,GraphQL 操作将针对整个schema进行解释,并使用前端应用程序的数据进行解析。向服务器发送大量查询后,API 会返回一个 JSON 响应,其中的数据形状与我们请求的数据完全相同。 + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230128011150.png) + +参考:_[_Jonas Helfer_](https://www.apollographql.com/blog/graphql-explained-5844742f195e) + +除了 RESTful CRUD 操作之外,GraphQL 还具有允许来自服务器的实时通知的[订阅](https://docs.nestjs.com/graphql/subscriptions)。 + +GraphQL需要我们对暴露出去的每一个字段规定一个函数进行处理,比如一个简单的node搭建的应用程序如下: + +```js +var express = require('express'); +var graphqlHTTP = require('express-graphql'); +var { buildSchema } = require('graph'); +// 构建schema,这里定义查询的语句和类型 +var schema = buildSchema(` + typr Account { + name: String + age: Int + sex: String + department: String + } + type Query { + hello: String + accountName: String + age: Int + account: Account + } +`) +// 定义查所对应的resolver,也就是查询对应的处理器 +var root = { + hello: () => 'Hello world', + accountName: () => 'justin3go', + age: () => 18, + account: () => ({ + name: '', + age: 18, + sex: '', + department: '' + }) +} + +var app = express(); +app.use('/graphql', graphqlHTTP({ + schema: schema, + rootValue: root, + graphiql:true +})); + +app.listen(4000) +``` + +本篇文章不做其实战介绍,可直接参考[NestJS官网搭建GraphQL教程](https://docs.nestjs.com/graphql/quick-start)以及其仓库有非常丰富并值得参考的相关代码: + +- [22-graphql-prisma](https://github.com/nestjs/nest/tree/master/sample/22-graphql-prisma) +- [23-graphql-code-first](https://github.com/nestjs/nest/tree/master/sample/23-graphql-code-first) + +> Nest 提供了两种构建 GraphQL 应用程序的方法,**代码优先**和**模式优先**方法。您应该选择最适合您的。这个 GraphQL 部分的大部分章节分为两个主要部分:一个是如果你**先采用代码**,你应该遵循,另一个是如果你先采用**模式**,则应该使用。 +> +> 在**代码优先**方法中,您使用装饰器和 TypeScript 类来生成相应的 GraphQL 模式。如果您更喜欢专门使用 TypeScript 并避免在语言语法之间切换上下文,则此方法很有用。 +> +> 在**模式优先**方法中,事实来源是 GraphQL SDL(模式定义语言)文件。SDL 是一种在不同平台之间共享模式文件的与语言无关的方式。Nest 基于 GraphQL 模式自动生成您的 TypeScript 定义(使用类或接口),以减少编写冗余样板代码的需要。 +> +> https://docs.nestjs.com/graphql/quick-start#overview + +### 优点 + +- **非常适合图形数据**:深入链接关系但不适合平面数据的数据 +- 请求的数据不多不少,按需请求,非常灵活 +- 获取多个资源,只用一个请求 +- 描述所有可能类型的系统。便于维护,根据需求平滑演进,添加或者隐藏字段(无需版本控制) + +### 缺点 + +- **性能问题**:GraphQL 以复杂性换取其强大功能。一个请求中包含太多嵌套字段会导致系统过载。因此,REST 仍然是复杂查询的更好选择。 +- **缓存复杂性**:由于 GraphQL 没有重用 HTTP 缓存语义,因此它需要自定义缓存工作。 +- **一定的学习成本**:没有足够的时间弄清楚 GraphQL生态和 SDL,许多项目决定遵循众所周知的 REST 路径。 + +## 总结 + +| | RPC|REST|GraphQL| +|-|-|-|-| +|组成|本地过程调用|6项指导原则|SDL| +|格式|JSON、XML、Protobuf、Thrift、FlatBuffers、|XML、JSON、HTML、plain text|JSON| +|学习成本|低|低|中| +|社区生态|丰富|丰富|正在丰富中| + +- RPC 适用于内部微服务,IO 密集的服务调用用 RPC,服务调用过于密集与复杂,RPC 就比较适用 +- REST 具有 API 的最高抽象和最佳建模。但它在网络上往往更重、更固定(通常为了兼容会有冗余字段) +- GraphQL在灵活获取数据上优势很大,但具有一定的学习成本。 + +## 最后 + +具体的API范式体会起来可能也是因人而异,上述的总结也可能有所偏差,欢迎友善讨论提出一些建议或者纠正一些错误 + +## 参考 + +- [究竟怎么理解restful设计风格?我喜欢这个比喻](https://su29029.github.io/2020/08/28/%E7%A9%B6%E7%AB%9F%E6%80%8E%E4%B9%88%E7%90%86%E8%A7%A3restful%E8%AE%BE%E8%AE%A1%E9%A3%8E%E6%A0%BC%EF%BC%9F%E6%88%91%E5%96%9C%E6%AC%A2%E8%BF%99%E4%B8%AA%E6%AF%94%E5%96%BB/) +- [Understanding RPC, REST and GraphQL](https://apisyouwonthate.com/blog/understanding-rpc-rest-and-graphql) +- [凤凰架构-RPC与REST](http://icyfenix.cn/architect-perspective/general-architecture/api-style/rpc.html) +- [Comparing API Architectural Styles: SOAP vs REST vs GraphQL vs RPC](https://www.altexsoft.com/blog/soap-vs-rest-vs-graphql-vs-rpc/) +- [Architectural Styles vs. Architectural Patterns vs. Design Patterns](https://herbertograca.com/2017/07/28/architectural-styles-vs-architectural-patterns-vs-design-patterns/) +- [RPC 和 REST 的优缺点、区别、如何选择](https://zhuanlan.zhihu.com/p/102760613) +- [Guiding Principles of REST](https://restfulapi.net/) +- [GraphQL vs. REST](https://www.apollographql.com/blog/graphql/basics/graphql-vs-rest/) +- [NestJS应用例子](https://github.com/nestjs/nest/tree/master/sample) + + diff --git "a/docs/\345\215\232\345\256\242/2023/02/04Vue3\347\233\270\345\205\263\345\216\237\347\220\206\346\242\263\347\220\206.md" "b/docs/\345\215\232\345\256\242/2023/02/04Vue3\347\233\270\345\205\263\345\216\237\347\220\206\346\242\263\347\220\206.md" new file mode 100644 index 0000000..d5c8784 --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2023/02/04Vue3\347\233\270\345\205\263\345\216\237\347\220\206\346\242\263\347\220\206.md" @@ -0,0 +1,640 @@ +# 一文梳理Vue3核心原理 + +## 前言 + +==**⚠万字长文warn**==本篇文章更多是以梳理的视角进行讲述,将各个原理细节串在一起,方便查漏补缺,而非为了讲懂某个原理,当然也会大致讲解。所以如果某个原理不太清楚,请自行查阅其他文章,我也会尽量给出相关的阅读推荐。 + +==本文阅读需要你有一定的vue应用程序开发经验并了解一些原理== + +> 接下来先废话一下,关注知识点的可以直接跳过前言部分 + +首先,我们先回到最初的起点是**为什么要使用Vue框架**,它为我们做了什么工作: + +1. 能开发出一个应用? +2. 性能好、构建产物轻量? +3. 对用户友好,声明式代码心智负担小? +4. 可组件化开发? +5. 社区活跃,生态丰富? +6. ... + +无论是官网介绍的优点,还是网友们提出的优点...首先这些都是毋庸置疑的,这也是一个成熟框架必备的一些属性。 + +好,我们来说但是:对于一个框架使用者,我们当然希望功能越丰富越好,但是对于一个框架初期学习者,我们则需要的是关注核心链路 + +> 就比如[``组件](https://cn.vuejs.org/guide/built-ins/keep-alive.html)原理我们前期有必要学习吗?除非你目前遇到过这样的问题,否则不建议有限的时间先学它。和玩游戏的道理一样,先做主线任务,再根据时间安排接取一些世界任务... + +这也是笔者写本文的原因,在这里我理解最主要的就是要扮演好[MVVM](https://juejin.cn/post/6844903929298288647#heading-0)中的ViewModel角色: + +ViewModel层通过**双向数据绑定**将View层和Model层连接了起来,使得View层和Model层的同步工作完全是自动的 + +这也可能为什么这是官网介绍的[第一个例子](https://cn.vuejs.org/guide/introduction.html#what-is-vue): + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230128230710.png) + +接下来就进入正题,看看框架在背后做了哪些工作👊,并且笔者会在文末给出几个扩展问题供大家思考... + +## 概括 + +把框架理解为一个封装好的函数,所以首先需要确认函数的输入以及函数的返回。在这里,用户(开发者)的输入就是单文件组件,输出就是带有一些优秀特性的可运行的代码、用户预期的结果,比如上述这个`count`例子的结果展示。 + +> 源码分支较多,细节丰富。如下图很难大而全地概括整个运行过程,仅供参考,也欢迎在评论区友善提出你的想法。 + +![](https://oss.justin3go.com/blogs/Vue%E5%8E%9F%E7%90%86%E6%A2%B3%E7%90%86.png) + +首先我们有一个按照Vue格式写的一个文件,该文件会经过如下步骤成为最终在浏览器上运行显示的文件: + +0. 将源文件通过编译手段转换为一个组件对象,其中render函数的返回值是该组件对应的虚拟DOM +1. 将组件对象交给渲染模块进行初始化 +2. 使用`ReactiveEffect()`函数对该组件对象的渲染任务进行包装,即依赖收集。 +3. 通过VNode渲染出对应的HTML挂载到页面上 +4. 用户操作页面中的响应式数据后,触发更新 +5. 响应式模块根据响应式数据找到其依赖的副作用函数并执行以更新页面、响应用户。 + +## 编译器 + +编译原理是一门复杂的学科,当时做这门课的课设时也是心态炸裂,难忘的经历。这里一两句,甚至一两篇文章都很难讲明白。但不是很影响接下来的内容,如果你对编译原理有一定的了解当然最好,没有的话你只需要理解如下非常简单的内容: + +- 假设源内容为`I am Justin3go` +- 经过编译器内部结构识别我们识别到了`(I => 主语) (am => 谓语) (Justin3go => 宾语)` +- 假设我设计的编译器功能就是主谓互换,所以结果就是`Justin3go am I`(这里忽略英语语法的问题) +- 再简单来说就是做字符串处理,根据原有结构进行文本替换增删以实现目标功能 + +*PS:编译原理对于前端来说也是一门非常重要的课程,有时间就看看书研究一下吧,这也是我后续的学习计划。* + +而回到这里无非就是识别HTML中的特殊语法、字符(如:`v-model`、双花括号等)进行转换或者打上标记等等 + +==还是那句话,编译原理部分这里不进行叙述,接下来主要对针对Vue框架特有的编译优化进行了解,一些思想还是非常值得学习的== + +**推荐链接**: + +- 你可以在官网的这个位置:[参考链接](https://cn.vuejs.org/guide/extras/rendering-mechanism.html#compiler-informed-virtual-dom)找到对应的讲解 +- 你可以在[模板编译预览器](https://template-explorer.vuejs.org/)中自行快速实践,值得注意的是:你可以在右上角Options菜单中开启相关的优化选项以验证想法 + +### Block树与动态结点收集 + +**1)为什么会有Block树的出现**: + +我们知道HTML模板在render函数中是以树状结构表示的。随着项目的复杂度增加,这个树的复杂度也随之增加,遍历并比较新旧的难度也增加。如果我们把其中某些子树分别看作整体,即作为结点,那么这颗树的复杂度会大大降低,如下图: + +![](https://oss.justin3go.com/blogs/Block%E6%A0%91.png) + +**2)为什么可以把某些子树看作整体Block**: + +因为这些Block不需要再进行遍历,因为编译器在初始化过程中会将其中的动态结点(会被更新的结点)进行了以Block为单位进行了收集,将这些动态结点以数组的形式存储在了Block上,就是算法中经典的空间换时间思想。故后续不需要遍历Block的内部结构了,因为有个扁平的数组结构存储了更新需要的必要信息。 + +**3)Block是如何进行划分的**: + +- 模板的根作为一个Block,每个Block会收集内部除了子Block包含的结点之外的所有动态结点 +- `v-if`、`v-for`等可能改变结点结构的指令对应的标签作为一个block + +值得一说的是,假设不将这些结构指令作为一个block的话: + +- `v-if`有可能让结点结构从整个树上消失,所以我们此时就不能作出安全的假设,比如条件为真时,`v-if`内部的动态结点应该被收集到上层Block的数组中;但条件为假时,就不应该被收集,所以框架将其作为一个Block就可以很好的解决这个问题:将该结构指令内的动态结点收集到该Block中,这个动态结点的信息就能随着结构的切换而出现或消失了 +- `v-for`指令同样如此,因为我们可能无法预先知道其会渲染多少个结点 + +### 静态结点优化 + +静态结点:在后续更新时永远不会改变的结点,比如一个文本结点 + +而静态优化([静态提升](https://vue-next-template-explorer.netlify.app/#eyJzcmMiOiI8ZGl2PlxuICA8ZGl2PmZvbzwvZGl2PlxuICA8ZGl2PmJhcjwvZGl2PlxuICA8ZGl2Pnt7IGR5bmFtaWMgfX08L2Rpdj5cbjwvZGl2PiIsInNzciI6ZmFsc2UsIm9wdGlvbnMiOnsiaG9pc3RTdGF0aWMiOnRydWV9fQ==))就是将这些静态结点保存到渲染函数之外,渲染函数仅引用这些静态结点,这样做的**优点**就是: + +- 避免重新创建对象,然后扔掉造成的性能损失,毕竟是静态结点不会变化 +- 在更新页面时,渲染器可以直接跳过新旧VNode这部分的比较 + +> 此外,当有足够多连续的静态元素时,它们还会再被压缩为一个“静态 vnode”,其中包含的是这些节点相应的纯 HTML 字符串。([示例](https://vue-next-template-explorer.netlify.app/#eyJzcmMiOiI8ZGl2PlxuICA8ZGl2IGNsYXNzPVwiZm9vXCI+Zm9vPC9kaXY+XG4gIDxkaXYgY2xhc3M9XCJmb29cIj5mb288L2Rpdj5cbiAgPGRpdiBjbGFzcz1cImZvb1wiPmZvbzwvZGl2PlxuICA8ZGl2IGNsYXNzPVwiZm9vXCI+Zm9vPC9kaXY+XG4gIDxkaXYgY2xhc3M9XCJmb29cIj5mb288L2Rpdj5cbiAgPGRpdj57eyBkeW5hbWljIH19PC9kaXY+XG48L2Rpdj4iLCJzc3IiOmZhbHNlLCJvcHRpb25zIjp7ImhvaXN0U3RhdGljIjp0cnVlfX0=))。这些静态节点会直接通过 `innerHTML` 来挂载。同时还会在初次挂载后缓存相应的 DOM 节点。如果这部分内容在应用中其他地方被重用,那么将会使用原生的 `cloneNode()` 方法来克隆新的 DOM 节点,这会非常高效。 + +### 内联事件优化 + +假设我们有如下模板代码: + +```html +
+
hello
+
+``` + +在不进行事件缓存优化的情况下,会被编译成如下渲染函数: + +```js{7-8} +import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" + +export function render(_ctx, _cache, $props, $setup, $data, $options) { + return (_openBlock(), _createElementBlock("div", null, [ + _createElementVNode("div", { + id: "foo", + onClick: _ctx.onClick + }, "hello", 8 /* PROPS */, ["onClick"]) + ])) +} +``` + +上述`render`函数在进行更新时,`{ id: "foo",onClick: _ctx.onClick}`必须作为一个整体进行diff,不管有多少东西在这`div`上,比如有非常多的子结点等等。 + +所以即使从模板中我们可以看到这个ID实际上是静态的,永远不会改变,我们还是会遍历整个对象,只是为了确保它不会改变,因为运行时没有足够的信息来知道这方面。 + +而开启事件缓存优化之后,如下: + +```js{7} +import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" + +export function render(_ctx, _cache, $props, $setup, $data, $options) { + return (_openBlock(), _createElementBlock("div", null, [ + _createElementVNode("div", { + id: "foo", + onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.onClick && _ctx.onClick(...args))) + }, "hello") + ])) +} +``` + +非常简单的代码,就是如果有缓存,读取缓存上的事件,如果对应位置没有缓存,则重新从上下文中获取该事件。 + +这样做的**优点**是:此时该组件对象中的所有事件都是“不变的”,都是一个指针,变化的是指向的对象,这样无论事件对应的值如何变化,该组件对象都不会因为事件变化而更新了 + +### vue3编译优化总结 + +|优化手段|简介|优点| +|-|-|-| +|Block树与动态结点收集|将根结点、结构指令结点作为Block,以Block为单位进行动态结点收集|简化HTML树结构,降低遍历难度、对比难度| +|静态提升|将静态结点的信息提升在render函数之外|不会重复渲染静态结点信息,提高性能| +|内联事件优化|render函数存储的是事件的指针,对应事件会被缓存在一个数组中|组件对象不会应该经常变化的事件而频繁更新了| + +## 渲染模块 + +> 每个组件实例的render函数会返回该组件的虚拟DOM + +渲染器的作用简单来说就是让虚拟DOM变为真实DOM + +![](https://oss.justin3go.com/blogs/%E6%B8%B2%E6%9F%93%E5%99%A8%E8%BF%87%E7%A8%8B.png) + +### 虚拟DOM概念 + +搜索和更新数千个DOM节点很明显会变慢,这就是Vue和其他类似框架的作用----[Virtual DOM](https://cn.vuejs.org/guide/extras/rendering-mechanism.html#virtual-dom) + +Virual Node是一个JavaScript对象,Vue知道如何使用这些Virtual Node并挂载待DOM上,它会更新我们在浏览器上看到的内容,**每个VNode对应一个DOM节点**,Vue通过寻找更新VNode最小的更新数量,然后再将最优策略施加到实际DOM中,从而减少DOM操作,提高性能 + +就好比在更新时对比施工图纸,找到最优的更新策略,而非把整栋楼拆了重建,或者是去楼的内部一一对比哪些不同,哪些需要更新这样耗时耗力。 + +> 除了初始化过程中Vue框架会把`HTML`模板编译为VNode,用户也可以直接使用[h函数](https://cn.vuejs.org/guide/extras/render-function.html#creating-vnodes)用于创建VNode + +优点: + +- 它让组件的渲染逻辑完全从真实DOM中解耦 +- 更直接地去重用框架的运行在其他环境中,Vue运行第三方开发人员创建自定义渲染解决方案,目标不仅仅是浏览器,也包括IOS和Android等原生环境,也可以使用API创建自定义渲染器直接渲染到WebGL,而不是DOM节点 +- 提供了以编程方式构造、检查、克隆以及操作所需的DOM操作的能力 + +### Patch函数 + +Patch函数中使用了非常多的分支结构来应对不同的情况,比如对经过编译优化打上标记的结点可以进行快速更新,而更新时也有不同的元素类型,属性,结构等都有不同的分支结构调用不同的API进行处理。 + +篇幅和精力有限,这里不可能也没必要进行完整的介绍,我理解这部分只需要认识`Patch()`函数是干嘛的就OK了,固仅对其比较经典的Diff算法进行详细介绍 + +> [源码位置](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/renderer.ts#L357),有兴趣或者想求证的可以瞧瞧.... + +如下是一个经过简化(不考虑编译信息,减少类型分支)的`patch()`函数,参考自[creating-a-patch-function](https://www.vuemastery.com/courses/vue3-deep-dive-with-evan-you/creating-a-patch-function) + +```js{6} +
+ + +``` + +### Diff算法 + +当我们遇到新旧节点都包含子节点的时候,即两个数组需要对比,又没有其他快捷更新的信息时,就不得不进行`full diff`,关于该部分Vue通过[`patchKeyedChildren()`函数](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/renderer.ts#L1751)进行了实现。 + +*注:接下来的算法讲解例子将脱离框架运行环境,抽象为纯粹的**新旧两个数组进行同步,找出最少的更新步骤**的一个算法问题。* + +顺便再提一下:之所以需要找到最优的更新策略,是因为实际DOM操作远比JS的数组比较耗时,这就是虚拟DOM作为实际DOM的设计图纸优势之一。 + +#### Key的作用 + +框架通过判断结点的key值和结点类型,从而确认新旧结点是否是可以复用的结点--"结点是否相同"。 + +值得注意的是:DOM可复用并不意味着不需要更新,比如文本结点的文本不同,只是意味着我们可以直接在同一结点上进行patch操作,而不需要移动、增、删结点。 + +> 可以参考[这部分代码实现](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/vnode.ts#L367) + +#### 1-4预处理 + +基本思想非常简单,**就是在正式处理之前,先处理头尾相同的部分**,这部分结点是相同的,可以直接进行patch操作,不需要移动结点顺序。 + +如下两个新旧数组需要进行同步,假设数字相同认为结点相同: + +```js +const oldNodes = [1,2,3,4, 5,6,7,8] +const newNodes = [1,2,3,4, 11,13,15, 5,6,7,8] +``` + +相信这个问题对大家应该问题不大,这里笔者贴合源码步骤简单实现一下供大家参考: + +1. sync from start:同步更新开始部分结点相同的 +2. sync from end:同步更新结束部分结点相同的 +3. common sequence + mount:无剩余未处理旧结点,但剩有新的结点,执行挂载操作 +4. common sequence + unmount:有剩余未处理旧结点,无剩余未处理新结点,执行卸载操作 + +```js +/** + * @param {number[]} oldNodes + * @param {number[]} newNodes + */ +function preProcess(oldNodes, newNodes){ + // * 初始化 + let i = 0; // 两个数组遍历开始的索引 + const newLen = newNodes.length; + // 由于两个数组开始位置都由i控制,但两数组可能长度不同,故这里用两个指针控制两数组的结束位置 + let oldEndPos = oldNodes.length - 1; + let newEndPos = newNodes.length - 1; + + while(i <= oldEndPos && i <= newEndPos) { + const oldNode = oldNodes[i]; + const newNode = newNodes[i]; + if(oldNode === newNode) { + console.log(`start部分执行patch(oldNode, newNode)`); + } else break; + i++; + } + + console.log(`从前往后首次两个结点不相同的坐标为:${i}`); + + while(i <= oldEndPos && i <= newEndPos) { + const oldNode = oldNodes[oldEndPos]; + const newNode = newNodes[newEndPos]; + if(oldNode === newNode) { + console.log(`end部分执行patch(oldNode, newNode)`); + } else break; + oldEndPos--; + newEndPos--; + } + + console.log(`从后往前首次两个结点不相同的坐标为 old:${oldEndPos}; new:${newEndPos}`); + + // 执行完上述操作,可能出现两种特殊情况,就是有可能新旧数组可能某一方被处理完了(无剩余结点) + // 此时就可以执行全部挂载和全部卸载的操作。简单来说就是有一方不是数组了,不需要再对两个数组进行对比了。 + + if(i > oldEndPos) { // 旧结点数组被处理完了 + if(i <= newEndPos) { // 新结点数组还有剩余 + // 执行挂载操作,新的设计图纸表明要增加,所以这里这里要在施工场地做增加操作 + console.log(`将${newNodes.slice(i, newEndPos + 1)}挂载到真实环境中`); + } + } else if (i > newEndPos) { // 新结点数组被处理完了,else表示旧结点还有剩余 + console.log(`将${oldNodes.slice(i, oldEndPos + 1)}从真实环境中卸载`); + } else { + console.log(`剩下的新旧节点数组都不为空[unknown sequence]`); + // 源码是在这里进行乱序序列处理的,不过这里为了方便分步运行演示,就抛出坐标给后续函数处理 + } + + return [i, oldEndPos, newEndPos]; +} +``` + +预处理代码非常简单,不过能在该情景想到这样的处理却是非常巧妙,下方为上方函数的运行效果,大家也可以复制上方代码自行尝试: + +```js +console.log(`预处理结果为${preProcess([1,2,3,4, 5,6,7,8], [1,2,3,4, 11,13,15, 5,6,7,8])}`); +console.log(`预处理结果为${preProcess([1,2,3,4, 11,13,15, 5,6,7,8], [1,2,3,4, 5,6,7,8])}`); +``` + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230202233312.png) + +#### 5. 处理乱序序列 + +到了这里,我们继续明确我们需要解决的问题,脱离API,抽象为一个算法问题就是:两个乱序新旧数组,如何尽可能多的复用旧数组的结点完成到新数组的更新 + +*注:回到浏览器环境中是依据新旧数组的最优更新策略操作真实的DOM数组* + +同样这里为了简化问题,以数字代表虚拟结点,**并假设数字相同认为结点相同(即key也相同)**,比如假设下方数据为预处理之后的数据: + +```js +const oldNodes = [11,12,13,14,15,16,17] +const newNodes = [14,11,12,16,13,15,18] +// 其中11,12,13,14,15,16这类结点可以直接通过移动完成更新 +// 而17在新数组未出现,故需要在真实DOM数组中卸载该结点 +// 18在旧数组未出现,故需创建该结点并挂载到真实DOM数组中 +``` + +这步相对于预处理来说比较复杂,但是再复杂的问题拆解为每一小步之后就更加容易理解了,源码部分也有关键的注释将[这步](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/renderer.ts#L1864)分为了3小步 + +##### 确定新结点位置(5.1+5.2) + +**1)做什么** + +还是经典的思路,先确定这步要做什么:构造一个新数组各结点位置在旧数组中各结点位置的一个对应关系`newIndexToOldIndexMap`。 + +*构造这个结构是为了后续方便求最长递增子序列,具体原因在那小节详细介绍。* + +```js +// old对应位置索引为[(11=>0),(12=>1),(13=>2),(14=>3),(15=>4),(16=>5),(17=>6)] +const oldNodes = [11,12,13,14,15,16,17] +``` + +然后通过判断结点key是否相同,这里假设的是数字是否相同,从而确定新结点数组在旧结点数组中的位置索引: + +![](https://oss.justin3go.com/blogs/%E7%A1%AE%E5%AE%9A%E6%96%B0%E8%99%9A%E6%8B%9F%E7%BB%93%E7%82%B9%E6%95%B0%E7%BB%84%E4%BD%8D%E7%BD%AE.png) + +该例子中的`newIndexToOldIndexMap = [3, 0, 1, 5, 2, 4, -1]` + +明白了需要做什么,接下来这步算法的实现相对来说也不难 + +**2)降低算法复杂度** + +值得注意的是:同样这里也是通过遍历这两个数组来填充`newIndexToOldIndexMap`,即暴力解法的算法复杂度为: +$$O(n^2)$$ +所以为了优化这里利用了**空间换时间**思想。使用`keyToNewIndexMap`先存储了新节点数组的关键信息: + +```js +// 伪代码表示结果如下 +const keyToNewIndexMap = {(14=>0),(11=>1),(12=>2),(16=>3),(13=>4),(15=>5),(18=>6)} +``` + +之后在遍历旧结点数组时,就可以在`O(1)`的复杂度通过旧结点的key直接获取新结点的位置,而非每次都在内部嵌套循环遍历新结点数组去重新获取对应新节点位置,最终构造`newIndexToOldIndexMap`的算法复杂度降到了 +$$O(n)$$ + +**3)代码实现** + +> `pos`变量表示:在旧 children 中寻找具有相同 key 值节点的过程中,遇到的最大索引值。如果在后续寻找的过程中,存在索引值比当前遇到的最大索引值还要小的节点,则意味着该节点需要移动`moved=true` + +```js +/** + * @param {number[]} oldNodes + * @param {number[]} newNodes + * @param {number} j 上部分传递过来的i + * @param {number} newEndPos + * @param {number} oldEndPos + */ +function buildMap(oldNodes, newNodes, j, newEndPos, oldEndPos) { + const oldStartPos = j + const newStartPos = j + // 构造keyToNewIndexMap索引表 + const keyToNewIndexMap = {} + for (let i = newStartPos; i <= newEndPos; i++) { + keyToNewIndexMap[newNodes[i]] = i + } + + // 构造 newIndexToOldIndexMap 数组 + const count = newEndPos - j + 1 + const newIndexToOldIndexMap = new Array(count).fill(-1) + + let moved = false + let pos = 0 // 记录寻找过程中遇到的最大索引值 + let patched = 0 // 表示更新过的节点数量 + for (let i = oldStartPos; i <= oldEndPos; i++) { + const oldNode = oldNodes[i] + // 如果更新过的节点数量小于等于需要更新的节点数量,则执行更新 + if (patched <= count) { + const k = keyToNewIndexMap[oldNode] // 通过旧结点的key找到新结点的位置 + if (typeof k !== 'undefined') { + const newNode = newNodes[k] + console.log(`i=${i},执行patch(${oldNode}, ${newNode})`); + // 每更新一个节点,都将 patched +1 + patched++ + newIndexToOldIndexMap[k - newStartPos] = i // 填充 + if (k < pos) { + moved = true + } else { + pos = k + } + } else { + console.log(`i=${i},没找到:unmount(${oldNode})`); + } + } else { + // 如果更新过的节点数量大于需要更新的节点数量,则卸载多余的节点 + console.log(`i=${i},卸载多余的节点:unmount(${oldNode})`); + } + } + + return [moved, newIndexToOldIndexMap]; +} +``` + +运行代码演示如下: + +```js +console.log(buildMap([11, 12, 13, 14, 15, 16, 17], [14, 11, 12, 16, 13, 15, 18], 0, 6, 6)); +``` + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230203195703.png) + +##### 最长递增子序列完成最优更新(5.3) + +这里解释一下为什么需要`newIndexToOldIndexMap`,该结构记录的时新数组各结点位置在旧数组中各结点位置的一个对应关系。这个结构中数组下标对应旧结点,数组值对应新结点。如`[3,0,1,5,2,4,-1]`中`(0=>3),(1=>0),(2=>1),(3=>5),(4=>2),(5=>4)` + +最长递增子序列:就是名字的意思,递增子序列中最长的那一个 + +而我们的总目标就是要找到最少的移动次数,而旧数组的位置索引在`newIndexToOldIndexMap`是递增的顺序,所以我们只需要找到新结点数组的最长的递增的结点们,就能找到最优的移动策略,然后进行移动就可以了,如图: + +![](https://oss.justin3go.com/blogs/%E6%9C%80%E9%95%BF%E9%80%92%E5%A2%9E%E5%AD%90%E5%BA%8F%E5%88%97%E7%A7%BB%E5%8A%A8%E7%AD%96%E7%95%A5.png) + +上图新节点的最长递增子序列的`[1,2,4]`,即图中白色节点不需要移动,所以该移动策略的移动次数为3次。 + +推荐链接: + +- [源代码链接](https://github.com/vuejs/core/blob/bef85e7975084b05af00b60ecd171c83f251c6d5/packages/runtime-core/src/renderer.ts#L2403),这是纯粹的算法实现,这里笔者就不重复实现了... +- [算法题链接](https://leetcode.cn/problems/longest-increasing-subsequence/),力扣上也是一个常规的中等题,大家也可以练练手... + +最后就是进行结点的移动了,这部分代码也没什么特别的逻辑【略】 + +### 总结 + +- 虚拟DOM:解耦真实环境,提高更新性能,提高各平台兼容性 +- Patch函数:更新节点时使用 +- Diff算法:预处理+空间换时间+最长递增子序列 + +## 响应式 + +响应式这块就我而言是看到网上文章出现频率最多的一个模块了,它就是状态数据与视图双向绑定的具体实现了。 + +==具体代码细节笔者这里不过多赘述了,有点肝不动了,主要走走响应式系统的运行过程== + +### 响应式概念 + +假设我们有一个[todoList应用](https://github.com/vuejs/core/blob/main/packages/vue/examples/composition/todomvc.html),其中的所有事项我们设置为响应式数据,当用户增删改事项时,我们作为开发者只需修改对应数据就可以了,至于后续页面事项列表的重新渲染是通过响应式模块自动触发的,这也是我们为什么要使用框架的原因之一。 + +![](https://oss.justin3go.com/blogs/%E5%93%8D%E5%BA%94%E5%BC%8F%E6%9B%B4%E6%96%B0%E8%BF%87%E7%A8%8B.png) + +接下来我们就来打开这个黑盒瞧瞧... + +### 主要工作流程 + +**1)响应式简单理解就两步:`track` + `trigger`,中文含义为跟踪+触发:** + +1. 跟踪(track):就是我们常说的依赖收集,对于响应式数据,找到依赖于该数据的[副作用函数](https://baike.baidu.com/item/%E5%87%BD%E6%95%B0%E5%89%AF%E4%BD%9C%E7%94%A8/22723425),然后使用一个方便的存储结构存储对应关系 +2. 触发(trigger):当我们监听到响应式数据变化时,就在之前收集的存储桶里面找到相关联的[副作用函数](https://baike.baidu.com/item/%E5%87%BD%E6%95%B0%E5%89%AF%E4%BD%9C%E7%94%A8/22723425),然后执行就可以了 + +**2)为了自动触发,使用了[Proxy](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy)代理响应式对象`ReactiveObject`:** + +1. 将track函数放入了[get](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/get),即使用该数据时会自动触发`track()` +2. 将trigger函数放入了[set](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/set),即修改该数据时会自动触发`trigger()` + +**3)为了避免硬编码函数名,即每次在track时都必须知道副作用函数的名字** + +- 我们使用了全局变量`activeEffect`来代替,即track函数每次收集`activeEffect`指向的函数即可 +- 然后通过`ReactiveEffect`注册传递过来的副作用函数,将该`activeEffect`变量指向该副作用函数 + +下面就介绍一下**稍微具体一点的工作链路**,如果你有一定的源码阅读经验或者学习过响应式原理,下方结合代码的图对你来说应该轻而易举,如果没有,也不用慌张,接下来会一步步介绍其工作流程。 + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230204034327.png) + +> [参考图片原文](https://juejin.cn/post/7145756356014768158) + +**工作过程:** + +0. 首先我们有一个响应式数据`ReactiveObject{}`和依赖于该数据的一个副作用函数`Effect()` +1. 将我们副作用函数传递到`ReactiveEffect`函数中 +2. 注册该副作用函数,将`activeEffect`变量指向它 +3. 执行该副作用函数 +4. 由于该副作用函数依赖于我们的响应式数据`ReactiveObject`,并且我们已经为`ReactiveObject`设置了代理拦截操作`get`,故在读取该值时会自动触发track函数 +5. track函数找到`activeEffect`变量,此时指向的正是我们需要的Effect函数 +6. 将属性值与副作用函数绑定关系并存储 +7. `activeEffect = null`方便后续使用 + +当我们更新了响应式数据中的值后,由于我们已经为`ReactiveObject`设置了代理拦截操作`set`,故会在我们设置该对象属性值时自动触发trigger函数 + +8. 找到属性值依赖的副作用函数 +9. 执行该副作用函数完成自动更新 + +核心代码实现对应图片如下: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230204050757.png) + +> [参考图片原文](https://juejin.cn/post/7145756356014768158) + +### 结合渲染模块 + +响应式模块的更新流程相信大家已经不陌生了,无非就是更改响应式数据后,被代理中的`set`等拦截,然后执行数据属性相关联的副作用函数从而完成响应式更新。 + +接下来主要结合渲染模块谈谈框架是如何让一个组件进入响应式模块的,简单来说就是**初始化过程是如何工作的**: + +*准备,首先是响应式数据已经建立,即框架已经对该数据建立了代理,并设置了各种拦截器。此时如果我们读取或者操作该响应式数据,就会自动触发相关函数* + +![](https://oss.justin3go.com/blogs/%E5%93%8D%E5%BA%94%E5%BC%8F%E7%BB%93%E5%90%88%E6%B8%B2%E6%9F%93%E6%A8%A1%E5%9D%97%E8%B0%83%E8%AF%95%E8%BF%87%E7%A8%8B.png) + +- 总的来说就是在初始化组件是会调用渲染模块 +- 然后就会调用一些(上下文+渲染模块中的函数)作为后续组件的更新函数,即副作用函数。 +- 然后传递给响应式模块进行依赖收集,看看与哪些响应式数据有关,从而建立依赖关系。 + +### 深入源码 + +篇幅有限,这部分就不详细介绍了,如下为主要函数的源码位置索引: + +- [createGetter](https://github.com/vuejs/core/blob/main/packages/reactivity/src/baseHandlers.ts#L94) +- [createSetter](https://github.com/vuejs/core/blob/main/packages/reactivity/src/baseHandlers.ts#L161) +- [track](https://github.com/vuejs/core/blob/main/packages/reactivity/src/effect.ts#L213) +- [trigger](https://github.com/vuejs/core/blob/main/packages/reactivity/src/effect.ts#L263) + +### 其他 + +关于响应式模块,除了上述主要工作流程,还有一些其他经典问题,这里进行一个基本汇总(该部分相关链接仅作参考,笔者仅粗略阅读符合问题描述即可): + +|问题标题|简述|深入阅读链接| +|-|-|-| +|effectStack|effect函数内部嵌套effect函数时,activeEffect变量会被覆盖消失,故添加一个栈进行保存|[阅读链接](https://juejin.cn/post/7100778134240231437)| +|computed|effect.lazy选项 + getter实现,可缓存,需要时执行副作用函数,而非响应式数据一变化就直接执行|[阅读链接](https://juejin.cn/post/7085524315063451684)| +|watch|effect.scheduler选项控制执行时机+保存旧值|[阅读链接](https://juejin.cn/post/7087748001208205343)| +|更全面的监测机制|使用proxy各种拦截器处理一些边界条件,比如拦截`.length`、`in`...等|[阅读链接](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/ownKeys)| +|ref|简单理解为reactive的`.value`属性|[阅读链接](https://vuejsdevelopers.com/2022/06/01/ref-vs-reactive/)| + +## 扩展问题 + +### 1. 为什么Vue3的模板根结点可以为多结点 + +- Vue2在组件中只有一个根节点。 +- Vue3在组件可以拥有多个根节点。 + +在渲染函数层面支持`Fragment`,其本身不会渲染任何内容,可以理解为在多个根节点外包裹一层`Fragment`就可以处理了。 + +[`processFragment()`](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/renderer.ts#L400) + +==欢迎补充...== + +## 最后 + +一直都想写一篇文章将所学的Vue原理部分串起来,难度确实较大,自己也一拖再拖。如何做到详略得当,如何深入浅出等等。在梳理脉络、书写文章方面自己还有很多地方值得加强。 + +- 最后在这里欢迎补充; +- 精力有限,长文难免有误,如果错误,也希望你能友善指出🤝; +- 码字不易,如果本文对你有所帮助,还望不吝👍。 + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230203012605.png) + +## 参考 + +- [Vue官网-进阶主题](https://cn.vuejs.org/guide/extras/ways-of-using-vue.html) +- [从原理和源码理解Vue3的响应式机制](https://juejin.cn/post/7145756356014768158) +- [vue3.0 响应式原理(超详细)](https://juejin.cn/post/6858899262596448270) +- [VueJS设计与实现](https://www.ituring.com.cn/book/2953) +- [Vue 3 Deep Dive with Evan You](https://www.vuemastery.com/courses/vue3-deep-dive-with-evan-you/vue3-overview) +- [Vue 文件是如何被转换并渲染到页面的?](https://juejin.cn/post/7115014433109180423) +- [Vue 3.2 源码系列:04-有点难的《最新 diff 算法详解》](https://juejin.cn/post/7190726242042118200) +- [细说 Vue.js 3.2 关于响应式部分的优化](https://juejin.cn/post/6995732683435278344) + diff --git "a/docs/\345\215\232\345\256\242/2023/02/17\350\201\212\350\201\212\346\272\220\347\255\226\347\225\245\351\231\220\345\210\266AJAX\350\257\267\346\261\202.md" "b/docs/\345\215\232\345\256\242/2023/02/17\350\201\212\350\201\212\346\272\220\347\255\226\347\225\245\351\231\220\345\210\266AJAX\350\257\267\346\261\202.md" new file mode 100644 index 0000000..2b30584 --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2023/02/17\350\201\212\350\201\212\346\272\220\347\255\226\347\225\245\351\231\220\345\210\266AJAX\350\257\267\346\261\202.md" @@ -0,0 +1,73 @@ +# 聊聊同源策略限制AJAX请求 + +## 前言 + +- 浏览器的同源策略及相关的AJAX跨域解决方案相信各位前端🐒们已经烂熟于心了,最近我脑子里突然冒出一些问题,就是标题中为什么同源策略要限制AJAX请求 +- 接下来我们来讨论下这些问题,可能某些问题对于你来说很荒谬,但希望能对你带来一些奇怪的感觉,接下来就是我的一个思考过程+资料查阅+相关解释 +- 这篇博客算是一个提问吧,非该领域的人,有些问题检索起来不知如何提问,所以这里写了一篇文章 + +## 开始 + +> 同源策略(Same Origin Policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略的基础之上的,浏览器只是针对同源策略的一种实现。 + +我之前记的笔记是--**同源策略主要限制了三个方面**: + +- 当前域下的 js 脚本不能够访问其他域下的 cookie、localStorage 和 indexDB。 +- 当前域下的 js 脚本不能够操作访问操作其他域下的 DOM。 +- 当前域下 ajax 无法发送跨域请求。 + +所以接下来我产生了下面这个问题... + +## 既然已经限制了对cookies的访问,为什么还要限制ajax请求呢? + +因为我们为了解决AJAX跨域请求的问题,实现了很多种跨域解决方案;既然脚本不能访问获取其他站点的cookies,那是否意味着这是安全的,同源策略可以放开对AJAX的请求呢,从而减少我们对跨域实现的工作量呢,虽然现在实现跨域操作已经非常方便了。 + +1)结果发现了“**cookies虽然不能跨域共享,但是似乎可以跨站点共享**”这点被我忽略的特性,如下述解释,当然你在阅读之前需要了解[CSRF](https://tech.meituan.com/2018/10/11/fe-security-csrf.html)相关内容 + +*CSRF简单来说就是根据浏览器发送请求时,自动携带与站点相关的cookies,因此恶意网站利用这点使用我们已登录重要网站的cookies达到一系列违规操作* + +> [参考](https://juejin.cn/post/6958413563799011365#heading-14)在同一浏览器,当前打开的多个Tab页网站,无论是否为同一站点,cookie都是共享可见的。这个共享不是说每个网站的脚本可以访问别的网站的cookie,而是说,**你向同一服务器发送请求时,会带上浏览器保存的对于那个服务器的所有cookie,而不管你从哪个网站发起的请求**。 + +2)从另外一方面思考,其实同源策略主要就是限制JS脚本对其他站点的操作,而ajax又处于JS脚本中,自然而然也会受到限制... + +3)在《白帽子讲Web安全》中看到这样一句话:如果XMLHttpRequest能够跨域访问资源,则可能会导致一些敏感数据泄露,比如CSRF的token,从而导致发生安全问题。 + +我们知道,CSRF的防范手段之一就是CSRF token: + +- 它的信任基础就是CSRF攻击只能利用其他网站的cookies,并不能直接窃取用户的相关信息; +- 并且攻击者需要提前猜测到接口的所有关键参数,这样加上自动携带的cookies才能请求接口成功。 + +所以CSRF token这种防御手段就是基于这两点,在用户刚进入页面的时候返回给用户一个随机token,之后请求中将该token作为参数就可以了,这样通过对比cookie中保存的token就能区别是否是来自其他站点的恶意请求了。 + +???在这里我并没有想到:假设允许AJAX跨域访问,是如何窃取CSRF token的场景的,也没有找到相关的资料,希望有小伙伴帮忙回答这个疑问... + +## 同源策略下CSRF又可以如何生效 + +刚才从同源策略聊到了CSRF,这里就从CSRF出发聊聊同源策略,我们知道CSRF的防御手段之一还有对refer头的判断,而其缺点就是refer是否可以伪造取决于第三方浏览器的实现 + +- 即使大多数浏览器的referer内容是无法改变的,但是也无法完全保证所有浏览器更改不了(虽然HTTP协议上有明确的要求 +- 但是每个浏览器对于Referer的具体实现可能有差别,并不能保证浏览器自身没有安全漏洞。 +- 使用验证 Referer 值的方法,就是把安全性都依赖于第三方(即浏览器)来保障, +- 从理论上来讲,这样并不是很安全。在部分情况下,攻击者可以隐藏,甚至修改自己请求的Referer。) + +所以即使拥有同源策略,也不能完全限制CSRF攻击,还需要一些其他手段,比如CSRF toekn + +## 那还有必要在同源策略中限制AJAX请求吗 + +兜兜转转又绕回来了,这里我想的是既然大多数场景都是直接使用CSRF token进行防范,那针对同源策略对AJAX的请求还有必要限制吗,因为最终服务器都是通过判断token是否一致来判断是否为自己的站点。 + +???当然,如果AJAX在可以随意跨域的情况下可以直接窃取CSRF token,那肯定不安全,希望有人能帮忙解释一下 + +## 最后 + +web安全技术是一个很大的领域,我这里管中窥豹,难免可能会有一些理解不到位的情况出现,如果存在相关错误,也希望各位不吝赐教,友善指出,我也会及时纠正... + +## 参考 + +- [cookie与CSRF攻击](https://juejin.cn/post/6958413563799011365#heading-14) +- [CSRF攻防实战(附JS源码)](https://juejin.cn/post/6869573026980036616) +- [前端安全系列(二):如何防止CSRF攻击?](https://tech.meituan.com/2018/10/11/fe-security-csrf.html) +- [魔法才能打败魔法:关于获取csrf-token前端技巧思考](https://xz.aliyun.com/t/7084) +- [CSRF第N次学习——搞清楚攻击为什么能成功](https://www.cnblogs.com/clwsec/p/16530449.html) +- [白帽子讲web安全](https://weread.qq.com/web/reader/7c4327b05cfd497c4eaa52fk16732dc0161679091c5aeb1) + diff --git "a/docs/\345\215\232\345\256\242/2023/02/19\346\224\276\345\274\203Cookie-Session\357\274\214\346\213\245\346\212\261JWT\357\274\237.md" "b/docs/\345\215\232\345\256\242/2023/02/19\346\224\276\345\274\203Cookie-Session\357\274\214\346\213\245\346\212\261JWT\357\274\237.md" new file mode 100644 index 0000000..230426e --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2023/02/19\346\224\276\345\274\203Cookie-Session\357\274\214\346\213\245\346\212\261JWT\357\274\237.md" @@ -0,0 +1,180 @@ +# 放弃Cookie-Session,拥抱JWT? + +## 前言 + +皮一下🐒,只是觉得这类标题挺“有趣的”,社区好多这样的标题:`放弃XXX,拥抱XXX......`😶甚至更皮一下还可以试试将`?`换成`!` + +接下来,请忽略标题的扭转事实,本文主要介绍JWT相关的知识 + +很早前写过一个搜索引擎demo,其中的登录系统包含了JWT这项技术(仅使用,未深入研究),于是乎心中埋下了一个种子。随着互联网上文章的熏陶,逐渐对其的探索欲增加,想着怎么和我之前的理解有出入。终于水过堤坝,来瞧瞧JWT到底是啥样,写下了这篇博文... + +## 安全的软件架构 + +[凤凰架构](https://icyfenix.cn/architect-perspective/general-architecture/system-security/)在架构安全性这章节中这样写道: + +> 即使只限定在“软件架构设计”这个语境下,系统安全仍然是一个很大的话题。我们谈论的计算机系统安全,不仅仅是指“防御系统被黑客攻击”这样狭隘的安全,,还至少应包括(不限于)以下这些问题的具体解决方案: +> +> - [**认证**](https://icyfenix.cn/architect-perspective/general-architecture/system-security/authentication)(Authentication):系统如何正确分辨出操作用户的真实身份? +> - [**授权**](https://icyfenix.cn/architect-perspective/general-architecture/system-security/authorization)( Authorization):系统如何控制一个用户该看到哪些数据、能操作哪些功能? +> - [**凭证**](https://icyfenix.cn/architect-perspective/general-architecture/system-security/credentials)(Credential):系统如何保证它与用户之间的承诺是双方当时真实意图的体现,是准确、完整且不可抵赖的? +> - [**保密**](https://icyfenix.cn/architect-perspective/general-architecture/system-security/confidentiality)(Confidentiality):系统如何保证敏感数据无法被包括系统管理员在内的内外部人员所窃取、滥用? +> - [**传输**](https://icyfenix.cn/architect-perspective/general-architecture/system-security/transport-security)(Transport Security):系统如何保证通过网络传输的信息无法被第三方窃听、篡改和冒充? +> - [**验证**](https://icyfenix.cn/architect-perspective/general-architecture/system-security/verification)(Verification):系统如何确保提交到每项服务中的数据是合乎规则的,不会对系统稳定性、数据一致性、正确性产生风险? + +- 在这里,假设我们需要实现一个带有用户系统的大型软件系统,无论其中的登录注册流程多么的复杂,我们都需要在每次操作资源的时候表明自己的身份,这样服务端才能识别该用户究竟是谁; +- 当然比较简单的方式是每次用户都将自己的账号密码传递,服务端验证一下就知道了你的身份,显然这是一种方式,但来回传输较为敏感的信息且让用户频繁登录并不是一个明智的选择; +- 我们可能更希望一种ID,key,凭证一样的产物,既可以证明我们的身份,又不至于类似账号密码一样“拥有它就拥有全世界~”,比如直接修改密码等等 + +其中,JWT和Cookie-Session在这里面扮演的就是一个凭证的角色,它们只是一个令牌,由用户所在系统分发,持有这个令牌可以拥有该用户身份的特定权限。比如校园学生持有学校分发的校园卡,就拥有了进出校园的权限,出入寝室的权限,食堂购买食物的权限等等。 + +## 基本登录流程 + +为了更直观的查看凭证的作用,下面介绍一个简单的登录流程: + +1. 当用户请求用户资料页时,发现没有令牌,不知道你是哪个用户 +2. 跳转登录页,用户输入账号密码或者其他登录方式 +3. 服务端验证用户身份,如果成功,返回响应分发该用户一个令牌 +4. 前端页面以某种方式保存该令牌 +5. 接下来每次请求携带该令牌,比如我们出入都要携带门禁卡一样 +6. 服务器验证该令牌,确认是否为正确的身份 + +如下用一个时序图来表示: + +![](https://oss.justin3go.com/blogs/%E9%82%93%E4%B8%BD%E8%BF%87%E7%A8%8B%E6%97%B6%E5%BA%8F%E5%9B%BE.png) + +这个过程有所简化,并不复杂,就两点: + +- 没有令牌时,输入账号密码获取令牌,保持登录态 +- 有令牌时,每次请求都携带令牌请求资源,表明自己的身份 + +## 令牌方案概述 + +到这里为止,你应该至少明白凭证(令牌)扮演着什么样的角色,有什么作用; + +接下来,我们就来瞧瞧庐山真面目,就目前来说,令牌的方案主要也就分为了标题中所描述的两种,它们的主要区别是用户信息的存放位置不同: + +- Cookie-Session方式(引用令牌):用户信息由服务器统一管理,令牌为一个随机字符串可以唯一指向详细的用户信息,这个指向关系当然也是由服务器保留 +- JWT方式(自包含令牌):用户信息经过处理直接作为令牌,相当于用户信息是保留在客户端的 + +> 如果这里你对上述描述一知半解,不用慌张,只需有一个印象即可,接下来展开介绍这两种方式,请移步下一小节... + +## Cookie-Session简述 + +通过Cookie-Session方式的认证流程这里就不赘述了,和上述介绍的基本登录流程小节是差不多的,只不过其中的凭证具体化了,是Cookie-Session方式。 + +### 描述 + +刚才提到,Cookie-Session是属于引用令牌,理解“引用”这个词的意思,和我们引用对象是一个意思,其中的变量名只是一个指针,真正的对象保留在堆栈当中; + +回到这里,这种令牌方案对用户状态信息的存储是保留在服务器中统一管理的,其中会有一个随机字符串比如叫做SessionId是指向对应的用户信息的,而分发给用户的令牌就是这个SessionId。 + +之后用户每次发送请求的时候携带这个SessionId令牌,服务器就可以检查到该令牌对应的用户信息是啥,是否有某些操作权限;或者没有携带这个令牌,就执行相关的保护逻辑或者跳转登录 + +### 优点 + +> 基本上相关优点都是围绕“引用令牌”的特性--用户相关的状态信息存储在服务器上,这一点展开的。 + +- 可以非常方便的管理在线用户:因为用户相关的状态信息都是存储在服务器的,比如统计实时在线人数,强制某些违规用户下线等等 +- 相对于在网络和客户端机器传递用户信息,一个没有任何含义的随机字符串SessionId被泄露所造成的影响更小 + +### 缺点 + +> 随着时代的进步,比如移动互联网时代的到来,分布式系统时代的到来等等,这种方式面临着以下缺点: + +- 只能在 web 场景下使用,如果是 APP 的情况,不能使用 cookie 的情况下就不能用了; +- 如果是分布式服务,需要考虑 Session 同步问题,一般在[CAP](https://en.wikipedia.org/wiki/CAP_theorem)这三角进行权衡,如下参考自[凤凰架构](https://icyfenix.cn/architect-perspective/general-architecture/system-security/credentials.html) + +> - 牺牲集群的一致性(Consistency),让均衡器采用亲和式的负载均衡算法,譬如根据用户 IP 或者 Session 来分配节点,每一个特定用户发出的所有请求都一直被分配到其中某一个节点来提供服务,每个节点都不重复地保存着一部分用户的状态,如果这个节点崩溃了,里面的用户状态便完全丢失。 +> - 牺牲集群的可用性(Availability),让各个节点之间采用复制式的 Session,每一个节点中的 Session 变动都会发送到组播地址的其他服务器上,这样某个节点崩溃了,不会中断都某个用户的服务,但 Session 之间组播复制的同步代价高昂,节点越多时,同步成本越高。 +> - 牺牲集群的分区容忍性(Partition Tolerance),让普通的服务节点中不再保留状态,将上下文集中放在一个所有服务节点都能访问到的数据节点中进行存储。此时的矛盾是数据节点就成为了单点,一旦数据节点损坏或出现网络分区,整个集群都不再能提供服务。 + +## JWT详谈 + +### 认识一下 + +无论是认识人还是认识物,外观几乎都是我们最开始接触到的,所以我们可以先看看JWT长啥样: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230219205428.png) + +可以看到,JWT分为三个部分:标头、负载、签名;详细介绍我这里就不过多赘述了,可以参考[这里](https://jwt.io/introduction) ,这里仅简单描述一下: + +- 标头:描述类型和加密算法 +- 负载:经过base64编码的数据,如用户信息 +- 签名:用于验证消息没有被更改 + +所以,可以看到,JWT里面中包含了数据如用户信息,而非前述方式引用令牌不包含用户信息,这也是后续描述JWT是如何解决Cookie-Session方式中的痛点,以及产生新的问题的主要原因,请记住这一点。 + +### 优点介绍 + +主要解决了上述Cookie-Session方式的两个痛点: + +- 并不特定依赖于Cookie方式存储 +- 用户信息包含在客户机,分布式服务中多台机器都可以认识,认证它 + +--- + +关于让多台机器验证分发下来的JWT,这里值得详细说一下,为了保证摘要不被修改,我们一般会对其进行加密,而加密算法可以分为[对称加密](https://zh.wikipedia.org/zh-hans/%E5%B0%8D%E7%A8%B1%E5%AF%86%E9%91%B0%E5%8A%A0%E5%AF%86)与[非对称加密](https://zh.wikipedia.org/wiki/%E5%85%AC%E5%BC%80%E5%AF%86%E9%92%A5%E5%8A%A0%E5%AF%86): + +- 对称加密密钥一旦泄漏,会让整个服务的基础设施遭受安全威胁 +- 而使用非对称加密方式,我们可以将其中一台服务器作为授权服务器,就是分发令牌的,由它持有私钥,而其他服务器持有公钥,这样所有服务器都可以验证请求传递过来的令牌,而最重要的私钥也仅由一台服务器持有 + +这里提到加密,还有一点值得注意:JWT中包含的信息(payload)并不是加密的,仅仅是为了方便网络传输进行了Base64编码。 + +上述加密的是摘要信息,**所以JWT仅仅保证的是其中包含的信息不会被篡改,并知道是否是己方服务分发的令牌,并不保证信息的泄露**。 + +### 说说缺点 + +JWT的特点既是它的优点,也是它的缺点 + +- **令牌分发之后,不受服务器控制**: + - 之前Cookie-Session方式由于所有用户信息统一管理在了服务端,虽然有一定的管理成本,特别是在分布式服务中成本更大,但相对来说更容易控制。 + - 而JWT是直接将用户信息经过处理: + - 通过摘要保证数据不可被修改,一旦修改,后续验证就会失效; + - 通过(非)对称加密保证这是己方服务器分发的令牌,是不可抵赖的 + - 而相关用户信息是存储在四面八方的客户机上的 + - 所以我们服务器很难控制相关用户登录状态,比如让某个用户强制下线,在线用户实时统计等等 +- **如果不使用HTTPS,更加容易遭到重放攻击**:HTTPS可以保证网络传输过程中令牌不会被泄露,但如果没有使用HTTPS或者以其他方式泄露令牌,拿到令牌的攻击方并不需要修改令牌,就可以很容易地冒充用户欺骗服务器。Cookie-Session 也是有重放攻击问题的,只是因为 Session 中的数据控制在服务端手上,应对重放攻击会相对主动一些。 + +--- + +其他的一些缺点,这些与JWT本身的特性无关,仅存在于该场景中,就要遵守该场景的规则,或者说如果该场景本来就有些问题,那么JWT也会存在一些问题: + +- **携带的数据有限**:JWT在请求过程中一般是存放在`Authorization Header`中的,而其数据的长度受限于各种服务器、浏览器的限制;就好比GET请求的长度限制首先于浏览器对URL的长度限制一样 +- **令牌存储**:存储在客户端意味着有多种选择:Cookie?Local Storage?如果放在 Cookie 中,为了安全,一般会给 Cookie 设置 `http-only` 和 `secure` 的属性。但这也会带来一定的不便性,比如客户端要读取 JWT Payload 的内容只能借助服务端 API 接口。如果将 JWT 存储至浏览器 Local Storage,虽然方便了客户端读取,但可能会带来 XSS 攻击的威胁,又需要去设置 CSP 来防御这种威胁; + +### 双token作用 + +简单来说也是为了减轻JWT被泄露而造成的影响,具体来说分为`refresh token`和`access token` + +| |access token|refresh token| +|-|-|-| +|有效时长|较短(如半小时)|较长(如一天)| +|作用|验证用户是否有操作权限|获取access token| +|什么时候使用|每次需要用户登录态时传递该token|access token失效时使用| + +这样做的好处就是: + +1. `access token`频繁传输,泄露风险较大,所以将其有效期设为较短可以有效降低泄露而造成的影响,比如此时攻击方最多伪装你半个小时; +2. `access token`存在时间较短,需要频繁获取新的,为了降低用户登录次数,提高用户体验,使用`refresh token`调用相关接口获取最新的`access token`。 +3. `refresh token`存在时间长,泄露后影响较大,所以只有在`access token`失效时才传递,所以并不会频繁传输,即泄露风险较小 + +主要就是兼顾泄露token的风险与泄露token的影响 + +## 最后 + +技术更应该是客观理性的,这也是我最开始写那个搜索引擎的误区,认为JWT是后续出现的,更加先进,所以登录技术栈就选用了它,但似乎想想,这个小系统既没有分布式,也不是APP,也许选用Cookie-Session方式更加合适,当然没有绝对~ + +> JWT 令牌与 Cookie-Session 并不是完全对等的解决方案,它只用来处理认证授权问题,充其量能携带少量非敏感的信息,只是 Cookie-Session 在认证授权问题上的替代品,而不能说 JWT 要比 Cookie-Session 更加先进,更不可能全面取代 Cookie-Session 机制。 + +如有错误,希望各位不吝赐教,友善指出... + +## 参考 + +- [凤凰架构-架构安全性](https://icyfenix.cn/architect-perspective/general-architecture/system-security/) +- [大型网站的用户登录系统是如何设计的?](https://www.zhihu.com/question/25400195) +- [JSON Web Token 入门教程](https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html) +- [JSON Web Token 简介](https://jwt.io/introduction) +- [What is a JWT? Understanding JSON Web Tokens](https://supertokens.com/blog/what-is-jwt) +- [说一说几种常用的登录认证方式,你用的哪种](https://cloud.tencent.com/developer/article/1080808) +- [使用Session和Cookie](https://www.liaoxuefeng.com/wiki/1252599548343744/1328768897515553) + diff --git "a/docs/\345\215\232\345\256\242/2023/02/23\344\275\240\345\217\257\350\203\275\345\277\275\347\225\245\347\232\20410\347\247\215JavaScript\345\277\253\344\271\220\345\206\231\346\263\225.md" "b/docs/\345\215\232\345\256\242/2023/02/23\344\275\240\345\217\257\350\203\275\345\277\275\347\225\245\347\232\20410\347\247\215JavaScript\345\277\253\344\271\220\345\206\231\346\263\225.md" new file mode 100644 index 0000000..4b87ab5 --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2023/02/23\344\275\240\345\217\257\350\203\275\345\277\275\347\225\245\347\232\20410\347\247\215JavaScript\345\277\253\344\271\220\345\206\231\346\263\225.md" @@ -0,0 +1,334 @@ +# 你可能忽略的10种JavaScript快乐写法 + +## 前言 + +- 代码的简洁、美感、可读性等等也许不影响程序的执行,但是却对人(开发者)的影响非常之大,甚至可以说是影响开发者幸福感的重要因素之一; +- 了解一些有美感的代码,不仅可以在一定程度上提高程序员们的开发效率,有些还能提高代码的性能,可谓是一举多得; + +笔者至今难以忘记最开始踏入程序员领域时接触的一段`List`内嵌`for`的Python代码: + +```python +array = [[16, 3, 7], [2, 24, 9], [4, 1, 12]] +row_min = [min(row) for row in array ] +print(row_min) +``` + +这可能就是动态语言非常优秀的一点,而JavaScript同样作为动态语言,其中包含的优秀代码片段也非常之多,比如我们通过JavaScript也可以非常轻松地实现上述的功能: + +```JavaScript +const array = [[16, 3, 7], [2, 24, 9], [4, 1, 12]] +const row_min = array.map(item => Math.min(...item)) +console.log(row_min) +``` + +能写出优秀的代码一直是笔者所追求的,以下为笔者在开发阅读过程积累的一些代码片段以及收集了互联网上一些优秀代码片段,希望对你有所帮助 + +## 概述 + +这里,考虑到有些技巧是大家见过的或者说是已经烂熟于心的,但总归有可能有些技巧没有留意过,为了让大家更加清楚的找到自己想要查阅的内容以查漏补缺,所以这里笔者贴心地为大家提供了一张本文内容的索引表,供大家翻阅以快速定位,如下: + +|应用场景标题|描述|补充1|补充2| +|-|-|-|-| +|数组去重|略|通过内置数据解构特性进行去重`[] => set => []`|通过遍历并判断是否存在进行去重`[many items].forEach(item => (item <不存在于> uniqueArr) && uniqueArr.push(item))`| +|数组的最后一个元素|获取数组中位置最后的一个元素|使用`at(-1)`|略| +|数组对象的相关转换|略|对象到数组:`Object.entries()`|数组到对象:`Obecjt.fromEntries()`| +|短路操作|通过短路操作避免后续表达式的执行|`a或b`:a真b不执行|`a且b`:a假b不执行| +|基于默认值的对象赋值|通过对象解构合并进行带有默认值的对象赋值操作|`{...defaultData, ...data}`|略| +|多重条件判断优化|单个值与多个值进行对比判断时,使用`includes`进行优化|`[404,400,403].includes`|略| +|交换两个值|通过对象解构操作进行简洁的双值交换|[a, b] = [b, a]|略| +|位运算|通过位运算提高性能和简洁程度|略|略| +|`replace()`的回调|通过传入回调进行更加细粒度的操作|略|略| +|`sort()`的回调|通过传入回调进行更加细粒度的操作|根据字母顺序排序|根据真假值进行排序| + +## 数组去重 + +这不仅是我们平常编写代码时经常会遇到的一个功能实现之一,也是许多面试官在考查JavaScript基础时喜欢考查的题目,比较常见的基本有如下两类方法: + +**1)通过内置数据结构自身特性进行去重** + +主要就是利用JavaScript内置的一些数据结构带有不包含重复值的特性,然后通过两次数据结构转换的消耗`[] => set => []`从而达到去重的效果,如下演示: + +```js +const arr = ['justin1go', 'justin2go', 'justin2go', 'justin3go', 'justin3go', 'justin3go']; +const uniqueArr = Array.from(new Set(arr)); +// const uniqueArr = [...new Set(arr)]; +``` + +**2)通过遍历并判断是否存在进行去重** + +白话描述就是:通过遍历每一项元素加入新数组,新数组存在相同的元素则放弃加入,伪代码:`[many items].forEach(item => (item <不存在于> uniqueArr) && uniqueArr.push(item))` + +至于上述的`<不存在于>`操作,可以是各种各样的方法,比如再开一个`for`循环判断新数组是否有相等的,或者说利用一些数组方法判断,如[indexOf](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf)、[includes](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/includes)、[filter](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/filter)、[reduce](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce)等等 + +```js +const arr = ['justin1go', 'justin2go', 'justin2go', 'justin3go', 'justin3go', 'justin3go']; +const uniqueArr = []; +arr.forEach(item => { + // 或者!uniqueArr.includes(item) + if(uniqueArr.indexOf(item) === -1){ + uniqueArr.push(item) + } +}) +``` + +结合`filter()`,判断正在遍历的项的index,是否是原始数组的第一个索引: + +```js +const arr = ['justin1go', 'justin2go', 'justin2go', 'justin3go', 'justin3go', 'justin3go']; +const uniqueArr = arr.filter((item, index) => { + return arr.indexOf(item, 0) === index; +}) +``` + +结合`reduce()`,prev初始设为`[]`,然后依次判断`cur`是否存在于`prev`数组,如果存在则加入,不存在则不动: + +```js +const arr = ['justin1go', 'justin2go', 'justin2go', 'justin3go', 'justin3go', 'justin3go']; +const uniqueArr = arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]); +``` + + +## 数组的最后一个元素 + +对于获取数组的最后一个元素,可能平常见得多的就是`arr[arr.length - 1]`,我们其实可以使用[`at()`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/at)方法进行获取 + +```js +const arr = ['justin1go', 'justin2go', 'justin3go']; +console.log(arr.at(-1)) // 倒数第一个值 +console.log(arr.at(-2)) // 倒数第二个值 +console.log(arr.at(0)) // 正数第一个 +console.log(arr.at(1)) // 正数第二个 +``` + +> 注:node14应该是不支持的,目前笔者并不建议使用该方法,但获取数组最后一个元素是很常用的,就应该像上述语法一样简单... + +## 数组对象的相互转换 + +- 相信大家比较熟悉的是**从对象转换为数组**的几种方法如:[`Object.keys()`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/keys)、[`Object.values()`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/values)、[`Object.entries`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/entries); +- 但其实还可以通过[`Object.fromEntries()`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries)将一个**特定数组转换回对象**: + +```js + const entryified = [ + ["key1", "justin1go"], + ["key2", "justin2go"], + ["key3", "justin3go"] + ]; + + const originalObject = Object.fromEntries(entryified); + console.log(originalObject); +``` + +## 短路操作 + +被合理运用的短路操作不仅非常的优雅,还能减少不必要的计算操作 + +**1)基本介绍** + +主要就是`||`或操作、`&&`且操作当第一个条件(左边那个)已经能完全决定整个表达式的值的时候,编译器就会跳过该表达式后续的计算 + +- 或操作`a || b`:该操作只要有一个条件为真值时,整个表达式就为真;即`a`为真时,`b`不执行; +- 且操作`a && b`:该操作只要有一个条件为假值时,整个表达式就为假;即`a`为假时,`b`不执行; + +**2)实例** + +网络传输一直是前端的性能瓶颈,所以我们在做一些判断的时候,可以通过短路操作减少请求次数: + +```js +const nextStep = isSkip || await getSecendCondition(); +if(nextStep) { + openModal(); +} +``` + +还有一个经典的代码片段: + +```js +function fn(callback) { + // some logic + callback && callback() +} +``` + +## 基于默认值的对象赋值 + +- 很多时候,我们在封装一些函数或者类时,会有一些配置参数。 +- 但这些配置参数通常来说会给出一个默认值,而这些配置参数用户是可以自定义的 +- 除此之外,还有许许多多的场景会用到的这个功能:基于默认值的对象赋值。 + +```js +function fn(setupData) { + const defaultSetup = { + email: "justin3go@qq.com", + userId: "justin3go", + skill: "code", + work: "student" + } + return { ...defaultSetup, ...setupData } +} + +const testSetData = { skill: "sing" } +console.log(fn(testSetData)) +``` + +如上`{ ...defaultSetup, ...setupData }`就是后续的值会覆盖前面`key`值相同的值。 + +## 多重条件判断优化 + +```js +if(condtion === "justin1go" || condition === "justin2go" || condition === "justin3go"){ + // some logic +} +``` + +如上,当我们对同一个值需要对比不同值的时候,我们完全可以使用如下的编码方式简化写法并降低耦合性: + +```js +const someConditions = ["justin1go", "justin2go", "justin3go"]; +if(someConditions.includes(condition)) { + // some logic +} +``` + +## 交换两个值 + +一般来说,我们可以增加一个临时变量来达到交换值的操作,在Python中是可以直接交换值的: + +```python +a = 1 +b = 2 +a, b = b, a +``` + +而在JS中,也可以通过解构操作交换值; + +```JS +let a = 1; +let b = 2; +[a, b] = [b, a] +``` + +简单理解一下: + +- 这里相当于使用了一个数组对象同时存储了a和b,该数组对象作为了临时变量 +- 之后再将该数组对象通过解构操作赋值给a和b变量即可 + +同时,还有种比较常见的操作就是交换数组中两个位置的值: + +```js +const arr = ["justin1go", "justin2go", "justin3go"]; +[arr[0], arr[2]] = [arr[2], arr[0]] +``` + +## 位运算 + +关于位运算网上的讨论参差不齐,有人说位运算性能好,简洁;也有人说位运算太过晦涩难懂,不够易读,这里笔者不发表意见,仅仅想说的是尽量在使用位运算代码的时候写好注释! + +下面为一些常见的位运算操作,[参考链接](https://juejin.cn/post/6844903568906911752) + +**1 ) 使用&运算符判断一个数的奇偶** + +```js +// 偶数 & 1 = 0 +// 奇数 & 1 = 1 +console.log(2 & 1) // 0 +console.log(3 & 1) // 1 +``` + +**2 ) 使用`~, >>, <<, >>>, |`来取整** + +```js +console.log(~~ 6.83) // 6 +console.log(6.83 >> 0) // 6 +console.log(6.83 << 0) // 6 +console.log(6.83 | 0) // 6 +// >>>不可对负数取整 +console.log(6.83 >>> 0) // 6 +``` + +**3 ) 使用`^`来完成值交换** + +```js +var a = 5 +var b = 8 +a ^= b +b ^= a +a ^= b +console.log(a) // 8 +console.log(b) // 5 +``` + +**4 ) 使用`&, >>, |`来完成rgb值和16进制颜色值之间的转换** + +```js +/** + * 16进制颜色值转RGB + * @param {String} hex 16进制颜色字符串 + * @return {String} RGB颜色字符串 + */ + function hexToRGB(hex) { + var hexx = hex.replace('#', '0x') + var r = hexx >> 16 + var g = hexx >> 8 & 0xff + var b = hexx & 0xff + return `rgb(${r}, ${g}, ${b})` +} + +/** + * RGB颜色转16进制颜色 + * @param {String} rgb RGB进制颜色字符串 + * @return {String} 16进制颜色字符串 + */ +function RGBToHex(rgb) { + var rgbArr = rgb.split(/[^\d]+/) + var color = rgbArr[1]<<16 | rgbArr[2]<<8 | rgbArr[3] + return '#'+ color.toString(16) +} +// ------------------------------------------------- +hexToRGB('#ffffff') // 'rgb(255,255,255)' +RGBToHex('rgb(255,255,255)') // '#ffffff' +``` + +## `replace()`的回调函数 + +之前写过一篇文章介绍了它,这里就不重复介绍了,[F=>传送](https://justin3go.com/%E5%8D%9A%E5%AE%A2/2022/15JavaScript%E5%9F%BA%E7%A1%80-replace%E6%96%B9%E6%B3%95%E7%9A%84%E7%AC%AC%E4%BA%8C%E4%B8%AA%E5%8F%82%E6%95%B0.html) + +## `sort()`的回调函数 + +`sort()`通过回调函数返回的正负情况来定义排序规则,由此,对于一些不同类型的数组,我们可以自定义一些排序规则以达到我们的目的: + +- 数字升序:`arr.sort((a,b)=>a-b)` +- 按字母顺序对字符串数组进行排序:`arr.sort((a, b) => a.localeCompare(b))` +- 根据真假值进行排序: + +```js +const users = [ + { "name": "john", "subscribed": false }, + { "name": "jane", "subscribed": true }, + { "name": "jean", "subscribed": false }, + { "name": "george", "subscribed": true }, + { "name": "jelly", "subscribed": true }, + { "name": "john", "subscribed": false } +]; + +const subscribedUsersFirst = users.sort((a, b) => Number(b.subscribed) - Number(a.subscribed)) +``` + +## 最后 + +- 个人能力有限,并且代码片段这类东西每个人的看法很难保持一致,不同开发者有不同的代码风格,这里仅仅整理了一些笔者自认为还不错的代码片段; +- 可能互联网上还存在着许许多多的优秀代码片段,笔者也不可能全部知道; +- 所以,如果你有一些该文章中没有包含的优秀代码片段,就不要藏着掖着了,分享出来吧~ + +同时,如本文有所错误,望不吝赐教,友善指出🤝 + +Happy Coding!🎉🎉🎉 + +## 参考 + +- [15 Killer 🗡 JS techniques you've probably never heard of 🔈🔥](https://dev.to/ironcladdev/15-killer-js-techniques-youve-probably-never-heard-of-1lgp) +- [JS奇巧淫技大杂烩(更新中)⏲👇](https://juejin.cn/post/7040359790400241694) +- [20个提升效率的JS简写技巧](https://juejin.cn/post/7041068640094912548) +- [8 techniques to write cleaner JavaScript code](https://dev.to/codewithahsan/8-techniques-to-write-cleaner-javascript-code-369e) +- [34 JavaScript Optimization Techniques to Know in 2021](https://dev.to/patelatit53/34-javascript-optimization-techniques-to-know-in-2021-57d) +- [位运算符在JS中的妙用](https://juejin.cn/post/6844903568906911752) + diff --git "a/docs/\345\215\232\345\256\242/2023/03/06\344\270\211\344\270\252\347\273\217\345\205\270\347\232\204TypeScript\346\230\223\346\267\267\346\267\206\347\202\271.md" "b/docs/\345\215\232\345\256\242/2023/03/06\344\270\211\344\270\252\347\273\217\345\205\270\347\232\204TypeScript\346\230\223\346\267\267\346\267\206\347\202\271.md" new file mode 100644 index 0000000..6b7ed46 --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2023/03/06\344\270\211\344\270\252\347\273\217\345\205\270\347\232\204TypeScript\346\230\223\346\267\267\346\267\206\347\202\271.md" @@ -0,0 +1,261 @@ +# 三个经典的TypeScript易混淆点 + +## 前言 + +- **本文会讲什么**:主要讲解TypeScript在开发过程中的易混淆点,当然也同样是面试官常考的几个题目 +- **本文不会讲什么**:本文并不是又大又全的TypeScript学习教程,不会讲那些基础知识、简单概念等,比如JS的内置类型这类。所以如果你是新手玩家,最好先去做一下新手任务出了新手村再这里 + +![](https://oss.justin3go.com/blogs/QQ%E5%9B%BE%E7%89%8720230306194345.jpg) + +## 你知道interface与type有什么区别吗 + +官网这里有[较为详细的介绍](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces),并且提到一句类似于最佳实践的话: + +> For the most part, you can choose based on personal preference, and TypeScript will tell you if it needs something to be the other kind of declaration. If you would like a heuristic, use `interface` until you need to use features from `type`. + +简单来说就是能用`interface`就用`interface`,除非Typescript提醒你了或者是`interface`根本实现不了这个功能。 + +具体来说它们有如下重要的区别 + +### 主要区别 + +`type`定义之后就不能重新添加新的属性;而`interface`则是始终可以扩展的;即仅`inyterface`支持合并类型 + +这里简单叙述一下官网中的例子: + +```ts +interface Window { + title: string +} +// 这步是OK的,`ts: TypeScriptAPI`就合并进入了之前定义的Window这个接口 +interface Window { + ts: TypeScriptAPI +} + +const src = 'const a = "Hello World"'; +window.ts.transpileModule(src, {}); +``` + +而基于已经定义的`type`继续添加新的字段就会`Error` + +```ts +type Window = { + title: string +} +type Window = { + ts: TypeScriptAPI +} + // Error: Duplicate identifier 'Window'. +``` + +### 其他区别:`interface`的限制 + +前面提到能使用`interface`的时候就使用`interface`,除了`interface`实现不了你想要的功能的时候。那本小节就描述一下`interface`又有什么限制: + +**不能直接操作基本类型(如`string`、`number`这些)** + +比如如下代码放在编译器中就会报错,因为`extends`了`string`这个基本类型: + +```ts +interface X extends string { // error + // ... +} +``` + +而`type`则可以,如下是大家可能经常使用的操作: + +```ts +type stringAlias = string; // ok +type StringOrNumber = string | number; // ok +``` + +### 本章小节 + +- `interface > type`:合并类型 +- `type > interface`:操作基本类型 + +当然,基于已有知识如`JavaScript`进行联想,你可以简单理解为`type == const`,`interface == class`。这种理解也许有点片面,不过仅仅是为了方便记忆... + +## 你知道never类型是用来干什么的吗 + +### 定义 + +故名思义,`never`是一种表示永远不会出现的类型,那什么是永远不会出现的类型呢,比如当一个函数陷入无限循环或者抛出异常时,我们就可以把这个函数的返回类型定义为`never` + +如: + +```ts +function throwError(message: string): never { + throw new Error(message); +} +``` + +注:`never` 类型仅能被赋值给另外一个 `never` + +### 应用场景1 + +对于平常开发中,`never`相对来说可能是使用的较少的了。更多人可能只是知道其定义,但不知道其场景/作用。 + +**第一个场景就是前面举例提到的定义无返回的函数的返回类型**。当然,除了上述中抛出异常会导致函数无返回,还有种形式是产生了无限循环导致代码执行不到终点: + +```ts +function infiniteLoop(): never { + while (true) { + console.log("justin3go.com"); + } +} +``` + +我们可以思考一下:**没有`never`时会导致什么坏情况出现** + +总的来说,`never`可以帮助 TypeScript 编译器更好地理解这个函数的行为,并在代码中进行类型检查。 + +例如,下面这个函数会抛出一个异常,表示输入的值不是一个有效的数字: + +```ts +function parseNumber(value: string): number { + const result = Number(value); + if (isNaN(result)) { + throw new Error(`${value} is not a valid number.`); + } + return result; +} +``` + +如果我们尝试调用上述这个 `parseNumber()` ,但是传递了一个无效的字符串参数,TypeScript 编译器无法识别这个函数会抛出一个异常(此时是假设的没有`never`类型)。而此时它会将函数的返回类型设置为 `number`,这会导致一些类型检查错误。 + +比如一个大型系统中我们调用这个通用函数时,而仅仅看到了TS的提示说这个函数返回的是一个`number`,然后你就非常笃定其返回值是一个`number`,于是就基于这个`number`类型做了许多特别的操作,哦豁,后续很可能出现偶发性bug。 + +而当有了`never`类型,我们就可以设置为这样: + +```ts +function parseNumber(value: string): never | number { + const result = Number(value); + if (isNaN(result)) { + throw new Error(`${value} is not a valid number.`); + } + return result; +} +``` + +现在,如果我们尝试调用 `parseNumber` 函数并传递一个无效的字符串参数,TypeScript 编译器会正确地推断出函数会抛出一个异常,并根据需要执行类型检查。**这可以在我们编写更安全、更健壮的代码时提供非常好的帮助。** + +### 应用场景2 + +在 TypeScript 中,`never` 类型可以**用作类型保护**。因为如果一个变量的类型为 `never`,则可以确定该变量不可能有任何值。例如下方这个经典例子: + +```ts +function assertNever(x: never): never { + throw new Error("Unexpected object: " + x); +} + +function getValue(x: string | number): string { + if (typeof x === "string") { + return x; + } else if (typeof x === "number") { + return x.toString(); + } else { + return assertNever(x); + } +} +``` + +这里,我们在最后一个分支使用`never`类型做了兜底,如果不使用`never`,这里TS检查就可能报错,因为最后一个分支没有返回与函数返回值为`string`相互冲突: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230306231122.png) + +### 与`void`的区别 + +刚才那个例子其实我们这样避免报错(当然并不推荐,这里仅仅为了引入`void`): + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230306231600.png) + +- 这样,就不会报错了,因为`void`表示该函数没有返回值,所以`string | void`1兼容了所有的分支情况,但是这里非常不推荐这么做,正确的做法还是`assertNever`那个例子 +- 原因是如果我们对这个函数按照参数类型正确传递参数,是不可能走到最后一个分支的,所以也就没必要单独在`或一个void`了,这样反而会误解这个函数的意思,增加操作; +- 此时使用抛出异常这个`never`类型就可以既避免该函数的返回值检查,又可以做一个兜底,在后续确实传参错误的时候抛出异常以避免执行后续的代码 + +所以,那`void`和`never`的区别是啥? + +- `void`:整个函数都正确执行完了,只是没有返回值 +- `never`:函数根本没有执行到返回的那一步 + +### 本章小节 + +`never`是一种表示永远不会出现的类型,主要在以下两种场景中使用: + +1. 无法执行到终点的函数的返回类型应设置为`never` +2. 可以用作类型保护 + +其中,无法执行到终点 与 在终点不返回是两个意思。这也是`never`与`void`的主要区别。 + +## 你知道unknown和any之间的区别吗 + +### 概括 + +首先你可以将`unknown`理解为TS认可的一种类型,它确确实实是TS内置的一种类型;而`any`你可以理解为它是为了兼容`JavaScript`而出现的一种类型,与其说是兼容`JavaScript`,不如说是兼容那些不太会`TypeScript`的程序员。当然,有时候项目赶工确实很着急那也没办法... + +### `unknown`简述 + +`unknown`表示一种不确定的类型,即编译器无法确定该变量的类型,因此无法对该变量执行任何操作。通常情况下,`unknown`类型的变量需要进行类型检查或者类型断言后才能使用。例如: + +```ts +let userInput: unknown; +let userName: string; + +userInput = 5; +userInput = 'hello'; + +// 需要进行类型检查 +if (typeof userInput === 'string') { + userName = userInput; +} + +// 或者需要进行类型断言 +userName = userInput as string; +``` + +### `any`简述 + +`any`表示任意类型,即该变量可以是任何类型。使用`any` 类型会关闭 TypeScript 的类型检查,因此使用 "any" 类型时需要小心,因为它会导致代码中的类型错误难以被发现。例如: + +```ts +let userInput: any; +let userName: string; + +userInput = 5; +userInput = 'hello'; + +// 没有类型检查 +userName = userInput; +``` + +### 区别呢? + +> `unknown`与`any`的最大区别是: +> +> `unknown` 是 `top type` (任何类型都是它的 `subtype`) , 而 `any` 既是 `top type`, 又是 `bottom type` (它是任何类型的 `subtype` ) , 这导致 `any` 基本上就是放弃了任何类型检查。 +> +> 因为`any`既是`top type`, 又是 `bottom type`,所以任何类型的值可以赋值给`any`,同时`any`类型的值也可以赋值给任何类型。但`unknown` 只是 `top type`,任何类型的值都可以赋值给它,但它只能赋值给`unknown`和`any`,因为只有它俩是`top type`。 + +上述话语原文:[any和unknown](https://juejin.cn/post/7003171767560716302#heading-9) + +## 最后 + +本篇文章由于是几个经典的问题,所以我结合了chatgpt与其他的一些文章进行参考,如下: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230306204709.png) + +其他两个问题也相继问了一下,有些帮助,但也仅此而已;这里疑惑的是我既然参考了它的回答,那我该不该引用它呢?如果引用它,那它的知识又来自于互联网,它自己却没有注明知识来源处... + +![](https://oss.justin3go.com/blogs/QQ%E5%9B%BE%E7%89%8720230306235244.jpg) + +最后最后,如本文有理解错误,欢迎各位友善指出。 + +## 参考 + +- [TypeScript高级用法](https://juejin.cn/post/6926794697553739784) +- [总结TypeScript在项目开发中的应用实践体会](https://juejin.cn/post/6970841540776329224) +- [重学TypeScript](https://juejin.cn/post/7003171767560716302#heading-9) +- [TS官网-differences-between-type-aliases-and-interfaces](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces) +- [Never](https://jkchao.github.io/typescript-book-chinese/typings/neverType.html#%E7%94%A8%E4%BE%8B%EF%BC%9A%E8%AF%A6%E7%BB%86%E7%9A%84%E6%A3%80%E6%9F%A5) + diff --git "a/docs/\345\215\232\345\256\242/2023/03/22\350\201\212\350\201\212\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273(\345\216\206\345\217\262\343\200\201\350\201\214\350\264\243\345\210\222\345\210\206\343\200\201\346\234\252\346\235\245\345\217\221\345\261\225).md" "b/docs/\345\215\232\345\256\242/2023/03/22\350\201\212\350\201\212\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273(\345\216\206\345\217\262\343\200\201\350\201\214\350\264\243\345\210\222\345\210\206\343\200\201\346\234\252\346\235\245\345\217\221\345\261\225).md" new file mode 100644 index 0000000..5aac18a --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2023/03/22\350\201\212\350\201\212\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273(\345\216\206\345\217\262\343\200\201\350\201\214\350\264\243\345\210\222\345\210\206\343\200\201\346\234\252\346\235\245\345\217\221\345\261\225).md" @@ -0,0 +1,163 @@ +# 聊聊前后端分离(历史、职责划分、未来发展) + +## 前言 + +3月下旬了,时间过得真快,才发觉已经有几周没写文章了😠。 + +前面写了一篇[Cookie-Session与JWT对比](https://justin3go.com/%E5%8D%9A%E5%AE%A2/2023/02/19%E6%94%BE%E5%BC%83Cookie-Session%EF%BC%8C%E6%8B%A5%E6%8A%B1JWT%EF%BC%9F.html)这样一篇文章,引发了我对未来前后端分离模式的一个思考。你可能会问,这两者能扯上什么关系?请听我慢慢道来... + +其实了解这两者区别的应该都清楚,主要就是把登录态的存储是放在前端(用户设备上)存储还是放在后端(服务器)上存储的一个区别,具体的优缺点这里不过多赘述,可以查看一下往期文章。 + +所以,这相当于就涉及到了某些业务处理既可以交给前端,又可以交给后端,甚至前端后端都需要处理一下(如权限管理,数据校验这类)。这就是前后端的一个职责划分。 + +这与边缘计算与云计算的概念是类似的。前端相当于边缘计算,把一些计算和业务放在用户设备进行处理;而后端就相当于云计算,操作在中心化服务器上进行处理的。这里解释一下边缘计算与云计算的概念: +- 边缘计算(Edge Computing)是一种分布式计算模型,它将计算和数据存储放置在接近数据源的边缘设备上,如传感器、路由器、智能手机等,以减少数据传输延迟和网络拥塞。 +- 边缘计算与云计算不同,云计算是将数据存储在云端数据中心,边缘计算则是将计算资源放在靠近数据源的设备上。这使得数据能够更快地处理和分析,以及更好地保护数据隐私和安全。 + +所以就想着梳理一下前后端分离的相关知识,以对全局更加了解,从而更好的服务于作为前端工程师的岗位😏😏😏 + +## 概述 + +总的来说,可以从以下两个层面解释前后端分离的出现: + +1. **业务**:时代的发展=>互联网的普及=>用户基数的增加=>功能业务的复杂=>前后端分离 +2. **技术**:业务的复杂=>有技术的需求=>Ajax的出现、SPA的普及等等=>前后端分离 + +关于前后端分离时代的划分,网上的文章各不相同,虽然划分的名词不同,但主要内容,脉络还是一致,这里笔者更愿意以技术名词将其划分为如下5个部分: + +1. 传统MVC架构 +2. Ajax的出现 +3. SPA的普及 +4. 微服务架构的发展 & BFF +5. Serverless 架构的兴起 + +## 1.传统MVC架构 + +其基本结构图如下: + +![](https://oss.justin3go.com/blogs/%E4%BC%A0%E7%BB%9FMVC%E6%9E%B6%E6%9E%84.png) + +这种架构下,**前后端的代码紧密耦合,难以分离** + +比如前端代码中经常嵌入后端的java代码,导致前端开发人员必须具备后端开发的知识和技能,而后端开发人员也必须了解前端的技术。 + +这种架构在开发和维护方面较为困难,随着 Web 应用的复杂性不断提高,这种架构逐渐显得力不从心。 + +## 2.Ajax的出现 + +随着 Ajax 技术的出现,前端页面可以异步获取数据,不必每次都刷新整个页面。这使得前端页面的功能和交互性大大提高,用户体验得到了显著的改善。同时,后端可以通过提供 RESTful API,为前端页面提供数据和服务,两者之间的耦合性逐渐降低。 + +具体来说,这里还是先将目光注视在上面的那张图上: + +- 之前页面更新的步骤是重新请求页面,服务器根据View中嵌入的后端动态代码生成带有数据的页面,然后返回给用户端。这种也就是整个页面全部更新。 +- 而现在,View中可以不用再嵌入后端的代码了,数据的更新只需在浏览器端通过网络请求后端提供的RESTful接口,然后使用JS操作DOM重新渲染页面即可。这种也就是页面局部更新。 + +下图就是通过Ajax请求的时序图: + +![](https://oss.justin3go.com/blogs/Ajax%E8%AF%B7%E6%B1%82%E6%97%B6%E5%BA%8F%E5%9B%BE.png) + +你可以简单理解为Ajax技术替换了前端内嵌后端代码这项技术,前端虽然不用学习后端代码或者模板语法这类技术,但是作为有得必有失,就需要学习Ajax这项技术。但总归比学习后端语法的学习成本低。 + +Ajax技术的出现是前后端分离兴起的前提条件。 + +## 3.SPA的普及 + +刚开始前端还是[MPA(Mutiple Page Application)](https://medium.com/@NeotericEU/single-page-application-vs-multiple-page-application-2591588efe58),此时虽然后续页面的局部更新不依赖于后端代码内嵌,只依赖于接口;但每个URL与前端页面的对应关系仍然还是由后端进行控制,此时前后端仍然有一定的耦合,所以有些文章将这MPA+Ajax这种情况叫做半分离时代。 + +随着技术的进步,为了更高的提高开发效率,[SPA(Single Page Application)](https://en.wikipedia.org/wiki/Single-page_application)逐渐普及,我们的前端 MV* 时代开始到来... + +如下是SPA时代下前后端分离的架构图(前端以MVVM为例): + +![](https://oss.justin3go.com/blogs/%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB%E7%9A%84SPA%E6%9E%B6%E6%9E%84.png) + +此时前后端才是所谓的分离,通过JSON进行数据交互,路由由前端自行控制,从而实现前后端的真正解耦。 + +## 4.微服务架构的发展 & BFF + +随着NodeJS的成熟,一个叫做BFF(Backend For Frontend)的技术架构出现在了开发者的视野中,BFF是为了解决微服务架构中的前端和后端之间的耦合问题而提出的,它是Web应用程序的后端和前端之间的中间层。 + +BFF模式通过将前端和后端之间的接口逻辑放在一个单独的服务中,将前端与后端之间的耦合度降低到最低。**这个服务只为前端提供特定的接口,而不是提供整个后端系统的接口**。这使得前端团队可以专注于开发他们需要的接口,而后端团队则可以专注于为不同的客户端(如Web和移动应用程序)提供最佳的服务。 + +如下是带有BFF的架构图: + +![](https://oss.justin3go.com/blogs/%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%9E%B6%E6%9E%84%E4%B8%AD%E7%9A%84BFF.png) + +在前后端中间增加了一个BFF,就相当于设计模式中的适配器模式,解耦。具体来说,有如下优势: + +1. **专注于前端需求**:BFF是为了满足前端需求而设计的,因此它可以专注于前端需要的功能和性能,而无需考虑其他方面的问题。这使得BFF能够更好地满足前端的需求,提供更好的用户体验。 +2. **灵活性**:由于BFF是一个中间层,可以自由选择技术栈,并在不影响其他层的情况下进行更改和优化。这使得BFF非常灵活,可以根据需要快速进行调整和改进。 +3. **可扩展性**:BFF可以在需要时轻松地进行扩展。当有新的前端功能需要实现时,可以向BFF添加新的服务或更改现有的服务,而无需修改后端的代码。 +4. **性能优化**:BFF可以通过将请求从前端分离出来并使用专门的服务来处理它们来提高性能。这可以使BFF在需要时缓存数据,减少网络延迟,并减轻后端的负担。 +5. **更好的安全性**:BFF可以在前端和后端之间提供额外的安全性。例如,BFF可以处理授权和身份验证,从而减少后端的安全风险。 + +总的来说,BFF架构模式提供了许多优势,包括专注于前端需求、灵活性、可扩展性、性能优化和更好的安全性。这些优势可以帮助开发团队更好地满足前端需求并提供更好的用户体验。 + +## 5.Serverless 架构的兴起 + +BFF由一般前端程序员开发,即使BFF是为前端服务的,从工作职责上区分是属于前端,但总归是一个后端服务的,意味着前端程序员也需要处理高并发、部署、负载均衡、备份冗灾、监控报警等等一系列对于前端程序员相对来说比较陌生的事物。 + +而这个问题就可以很好的被Serverless解决。 + +[Serverless架构](https://en.wikipedia.org/wiki/Serverless_computing)是一种云计算模型,它通过将代码运行环境和基础设施的管理交给云服务提供商来简化应用程序开发和部署。以下是Serverless如何促进前后端分离的几个方面: + +1. **无需管理服务器**:使用Serverless,开发人员无需考虑服务器的管理,例如配置、扩展和维护等问题。这意味着前端和后端开发人员可以更专注于自己的领域,而无需关心服务器的运行和管理。 +2. **独立部署**:Serverless架构允许独立部署每个函数或服务。这使得前端和后端开发人员可以根据需要独立地开发、测试和部署他们的代码,而不需要等待其他团队完成其工作。 +3. **适合微服务架构**:Serverless架构非常适合微服务架构,其中应用程序被拆分成多个小型服务。每个服务可以独立开发和部署,从而促进前后端分离。 +4. **按需计费**:Serverless按照每个函数的实际使用量进行计费,而不是预先支付一定量的服务器资源。这意味着前端和后端开发人员可以仅针对实际使用的资源进行支付,并根据需要进行扩展。 + +总的来说,Serverless架构模式通过简化基础设施管理、独立部署、适合微服务架构和按需计费等方式促进前后端分离。这使得开发人员可以更专注于自己的领域,而不必担心服务器管理和资源预测等问题。 + +此时前端程序员就只需在BFF中调用一下RPC/HTTP,写写JS处理一下逻辑。前后端分离得到进一步发展... + +## 一些想法(前后端职责) + +[这篇文章演示](https://2014.jsconfchina.com/slides/herman-taobaoweb/index.html#/69)提到了关于前后端的一个简单的职责划分,如下图: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230322190954.png) + +这里笔者不过多赘述,仅仅想谈的是随着技术的发展(V8、WebWorker、Webassembly、WebGL,TensorflowJS等等),浏览器端可以承受更多的业务处理和数据计算。 + +**这就意味着在浏览器端不仅仅只能操作DOM,展示数据了,同样也可以承受一定的业务处理,数据计算**。并且相对于在中心服务器进行处理,这种在用户设备上进行处理有如下优势: + +- 减少网络传输,提高响应速度 +- 降低服务器压力 +- 特定情况下可以保护用户隐私 + +举个常见的例子,比如使用TensorflowJS调用浏览器摄像头,从而识别用户的肢体动作进行交互,而识别的模型程序既可以放在中心服务器,现在也可以放在前端,并且也较为成熟了。所以该怎么选择呢? + +这个答案大家应该都比较清楚,自然放在是前端,毕竟视频的传输极其消耗网络带宽,以及摄像头是较为隐私的部分了。 + +说了这么多,现在进入正题,也就是笔者想说的想法就是:除开页面操作一般属于前端,数据库操作一般属于后端,其他的业务处理、数据计算其实前后端现在几乎都能处理。就像登录态的处理:Cookie-Session是把登录态放在中心化服务器上,JWT是将登录态分发给到各个用户设备上一样。 + +**所以怎么选择就非常重要了,而笔者这里认为能放在前端处理的数据尽量放在前端处理,除开以下特殊情况需要放在中心化服务器中进行处理**: + +- **安全**:某些计算策略、业务处理策略不能公布出来 +- **多个用户的数据处理**:用户端(前端)自然只能处理该用户的数据,所以多个用户的数据处理只能由服务器进行处理 +- **多个设备的协同**:这个肯定需要服务器的帮助 +- **复杂度特别高的计算**:虽然目前用户设备一般性能都不差,但对于一些高复杂度的处理还是只能放在服务器上运算,不能造成用户卡顿,影响用户体验 + +尽量放在前端处理的好处就是上面提到的: + +- 减少网络传输,提高响应速度 +- 降低服务器压力 +- 特定情况下可以保护用户隐私 + +这就意味着我们前端工程师就不能仅仅只懂得写页面、操作页面了,得有全栈思维、产品思维,从功能点、业务出发,站在全局思考前端问题... + +## 最后 + +上述的想法部分笔者实践经验较少,更多可能是纸上谈兵,并没有进行所谓的啥比较全面的可行性分析等等。同时也是借这个契机来梳理一下关于前后端分离模式的一个历史综述,希望对你有所帮助或者能引发你的思考。 + +如果你有一些宝贵的经验,欢迎友善评论😉 + +## 参考 + +- [前后端分离架构概述](https://blog.csdn.net/fuzhongmin05/article/details/81591072) +- [浅谈前后端分离与实践(一)](https://zhuanlan.zhihu.com/p/29996622) +- [Frontend, Backend, and the Blurring Line In-Between](https://dev.to/zenstack/frontend-backend-and-the-blurring-line-in-between-2h59) +- [淘宝前后端分离解决方案](https://2014.jsconfchina.com/slides/herman-taobaoweb/index.html#/100) +- [Web 前后端分离的意义大吗?](https://www.zhihu.com/question/28207685) +- [Confused about web app architecture and separation of frontend and backend](https://www.reddit.com/r/webdev/comments/spr2db/confused_about_web_app_architecture_and/) +- [Web开发的历史发展技术演变](https://zhuanlan.zhihu.com/p/196637639) +- [你学BFF和Serverless了吗](https://juejin.cn/post/6844904185427673095) + diff --git "a/docs/\345\215\232\345\256\242/2023/03/29\345\211\215\347\253\257\350\207\252\347\273\231\350\207\252\350\266\263UI\350\256\276\350\256\241\347\250\277.md" "b/docs/\345\215\232\345\256\242/2023/03/29\345\211\215\347\253\257\350\207\252\347\273\231\350\207\252\350\266\263UI\350\256\276\350\256\241\347\250\277.md" new file mode 100644 index 0000000..2759a1e --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2023/03/29\345\211\215\347\253\257\350\207\252\347\273\231\350\207\252\350\266\263UI\350\256\276\350\256\241\347\250\277.md" @@ -0,0 +1,160 @@ +# 前端自给自足UI设计稿?(Midjourney+MasterGo) + +## 前言 + +最近在自己做一个可能有意思的项目。对于公司的项目,一般都是有对应的UI设计稿作为参照开发的,而对于自己之前的一些小项目,更多则是套用模板,想到哪写到哪这种方式写的前端页面。 + +后者这种方式肯定是不对的,就好比我们写代码要先写技术文档,再来写代码一样。 + +笔者在做现在这个项目的时候,就吃了“想到到哪,写到哪”的亏,一个是没有统一的色调,另外一个就是页面样式调来调去,而这个调来调去是基于代码改来改去,成本较大;就好比DOM更新时一种是直接选择DOM进行比较,另外一种是使用虚拟DOM这种蓝图进行比较一样。 + +接下来就用非常火的Midjourney + 一个国产设计软件MasterGo来尝试设计一个页面,希望对你有所帮助... + +## 使用Midjourney + +首先,Midjourney使用非常简单,在[官网](https://www.midjourney.com/home/?callbackUrl=%2Fapp%2F)注册并加入discord讨论区就可以了,这里就不过多赘述了,毕竟这个已经火了很久了,相关教程也比较丰富... + +值得注意的是,我们一般可以在`newbies`新手区创建属于自己的子区,这样我们发出的消息就不会被很快刷新掉。然后就笔者自己的感受,晚上比较拥挤,早上比较流畅。 + +创建子区的入口: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230328162157.png) + +目前,我们可以使用该AI服务满足如下UI设计需求: + +1. 做UI设计稿供参考 +2. 做icon +3. 做logo +4. 做插图 + +下面笔者仅以做UI稿为例进行演示... + +## 提示Midjourney + +然后我们就可以输入`/imagine [提示词]`使用该服务器AI绘画的服务,比如: + +```txt +/imagine icon for iOS app in high resolution, burger, high quality, HQ — q2 +``` + +我们一般可以按照`关键词,关键词,关键词...`这样也可以生成比较好的效果,这样其实就不用太注意英语语法了,只需要翻译一下关键词就可以了。 + +如下是笔者的一个生成过程: + +**1)开始** + +由于是需要生成UI图,为了让AI更清楚的理解这个需求,所以笔者在这里添加了`Figma`关键词 + +输入`community study and discuss app, mobile app, user interface, Figma, HQ, 4K, clean UI — q2` + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230328170124.png) + +**2)刷新一下** + +似乎不太满意,这里点击右下角的刷新按钮,重新生成: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230328170326.png) + +**3)增加`many pages`关键词** + +然后发现其实笔者需要比较多的页面进行参考设计,所以下一条命令又添加的`many pages`的关键词: + +```txt +community study and discuss app, mobile app, user interface, many pages, Figma, HQ, 4K, clean UI — q2 +``` + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230328170942.png) + +**4)选择比较满意的版本** + +这里笔者觉得上述生成的第四个版本比较不错,所以选择V4,让AI基于此图生成风格类似的图片: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230328171101.png) + +**5)使用其中的图片** + +这里笔者觉得图一不错,想要其中的大图,所以选择上述选项中的`U1`按钮,生成如下图片: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230328171224.png) + +到此为止,基本上有一个可以供设计参考的UI设计稿了,整体风格我们就可以参考这些不同的页面。但是,如果我们想要设计其中一个页面的详情呢,比如一个个人主页这些页面中就没有包含,所以我们如果想继续设计,就还是得继续去问AI + +**6)设计个人主页** + +这里,我们在原有关键词的基础上添加了`single profile page`来生成我们预期的界面: + +``` +community study and discuss app, mobile app, user interface, single profile page, Figma, HQ, 4K, clean UI — q 2 +``` + +如下是生成的效果图: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230329085902.png) + +## 使用MasterGo + +此时,我们基本上有一定的设计灵感了,然后就可以开始画图了。至于为什么选择MasterGo:个人免费,该有的功能都有,国产对国人友好,上手简单... + +对于我这种非深度使用设计软件的人来说,这个软件刚刚好 + +对于一个程序员来说,各种软件使用的数量应该非常之多,甚至就是做这些软件的开发者。所以对于这款软件的基本使用,由于篇幅有限,我这里就不详细介绍了,相信你自己去多点点就啥都会了 + +> B站也有他们出的[官方教程](https://www.bilibili.com/video/BV1Sr4y1t7u4/?spm_id_from=333.337.search-card.all.click),也可以自己去看看,不过该教程更多是讲解软件的使用,一些UI设计的基础知识并没有详细讲解 + +接下来我们就根据参考图来设计一个UI稿 + +**1)规范主题** + +我们可以根据参考图先规范我们的主题颜色,比如背景颜色,前景色等等;规范我们的文字样式,比如标题、正文、注释都可能有不同的大小、字体,甚至于颜色等等,然后可以在MasterGo里面设置为模板,模板功能还是非常常用的: + +无论是颜色,描边,特效(阴影、模糊这些)都可以设置为模板方便后续使用: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230329095237.png) + +然后我们就可以添加和管理相应的模板了。 + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230329095346.png) + +**2)基本概念介绍** + +> 如果你学过PS、AI这类软件,这一小部分你可以跳过 + +- 页面:相当于就是一个分组 +- 容器:容纳我们设计图形的地方 +- 图层:类似于前端的z-index +- 钢笔工具:可以绘制几乎你能想到的所有曲线 + +基本快捷操作: + +- 空格拖动 +- ALT点按复制 +- ctrl+滚轮缩放 + +基本上都是其他设计软件通用的一些快捷键,所以大家可以自己按照所想试试就知道了,反正就是ctrl、alt、shift这几个键按着试呗,比如正方形我们可以按住shift拖动,从中心出发的正方形我们就可以再按住alt就可以了... + +**3)导入基本库** + +我们可以导入一些模板,比如uni-app的[uni-ui的sketch文件](https://zh.uniapp.dcloud.io/component/uniui/resource.html),或者一些其他的[社区资源](https://mastergo.com/community/)等等,方便我们直接使用... + +**4)开始绘制** + +然后我们就可以开始绘制了,绘制过程非常简单,基本上给出的三个图形: + +- 矩形 +- 圆 +- 直线 + +就能解决大部分的图形绘制需求,稍微复杂一点的用钢笔也能轻易绘制,这里也不详细演示了,大家作为前端程序员稍微上上手应该就能轻易使用该软件 + +## 最后 + +工具的使用总的来说都是非常简单的,但是UI设计师的重要素质是审美能力,这一部分并不是轻易能获得和提升的,如下是我在刚才演示过程中设计出来的两个页面: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230329102946.png) + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230329103005.png) + +感觉还是不是很满意,话不多说了,我继续去调排版和样式了 + +![](https://oss.justin3go.com/blogs/QQ%E5%9B%BE%E7%89%8720230329103231.jpg) + diff --git "a/docs/\345\215\232\345\256\242/2023/03/31\346\236\201\347\256\200\345\234\260\347\273\231\344\270\252\344\272\272\345\215\232\345\256\242\346\267\273\345\212\240\350\256\242\351\230\205\345\212\237\350\203\275.md" "b/docs/\345\215\232\345\256\242/2023/03/31\346\236\201\347\256\200\345\234\260\347\273\231\344\270\252\344\272\272\345\215\232\345\256\242\346\267\273\345\212\240\350\256\242\351\230\205\345\212\237\350\203\275.md" new file mode 100644 index 0000000..60d071e --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2023/03/31\346\236\201\347\256\200\345\234\260\347\273\231\344\270\252\344\272\272\345\215\232\345\256\242\346\267\273\345\212\240\350\256\242\351\230\205\345\212\237\350\203\275.md" @@ -0,0 +1,91 @@ +# (极简)给个人博客添加订阅功能 + +## 前言 + +今天给大家分享一种极简的给自己个人博客添加订阅功能的方式,就目前而言,各个个人博客的订阅方式以如下方式为主流: + +- RSS订阅 +- 一些邮件订阅服务 +- 自建服务(没必要) + +RSS我基本没用过,应该前些年非常火,了解了一下好像也不是很通用,如果读者没有使用RSS的习惯的话,比如笔者自己就不怎么使用这类产品。 + +而笔者基本上是通过邮件获取对于其他文章的订阅的,但如果想要在自己的博客给读者添加邮件订阅的功能,就需要去买相关的邮件服务,对这方面的花费笔者感觉不值当,并且限制也多,如下是mailchimp的费用与功能对应关系: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230331145753.png) + +然后今天在突然收到github发送的邮件之后(是一封订阅了某仓库Issue的邮件),似乎有了一定的灵感... + +## 效果演示 + +话不多说,先上效果! + +### 读者订阅 + +**1)点击订阅链接跳转**,笔者博客:[justin3go.com](https://justin3go.com) 欢迎订阅笔者的月刊。 + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230331151113.png) + +**2)在该Issue上订阅**: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230331151348.png) + +好,订阅完成,读者的操作也非常简单。而到这里,相信大家差不多也明白其中的实现方式了... + +### 通知读者 + +接下来,笔者就只需要在该Issue下进行评论,对应订阅的用户就可以接收到由github发送的相关信息以及邮件: + +**1)评论Issue** + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230331152020.png) + +**2)读者接收到信息**: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230331152130.png) + +以及邮件也会接受到信息: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230331152331.png) + +## 使用方式(一行代码) + +很简单,就是在你的个人博客某位置添加一个跳转链接就可以了,如下是我在vitepress中添加跳转链接到`footer`的方式: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230331152537.png) + +完整代码: + +```html +在github上订阅本博客月刊 +``` + +恭喜你,OK了!记得填上自己的仓库链接... + +**值得注意的是**: + +1. 我们需要锁定该Issue,避免其他人评论: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230331152822.png) + +2. 我们可以pin上该Issue,方便其他人查看仓库时查看 +3. 我们可以将该Issue的内容稍微修改一下: +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230331152939.png) + +## 缺点 + +- 依赖github,需要跳转到github +- 无法统计订阅人数 + +## 最后 + +其实最开始是想写个类似`Gitalk`的组件,但笔者也去看了下[github的开放API](https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28),好像在这部分没有找到对应的订阅Issue的API,不然其实这里就可以实现不用跳转页面即可订阅的功能了。 + +笔者较懒,这部分的调研可能并不尽善尽美,所以如果你有其他较好的方式或者想法,欢迎友善评论... + +![](https://oss.justin3go.com/blogs/QQ%E5%9B%BE%E7%89%8720230331154119.jpg) + +## 参考 + +- [有受gitalk的启发](https://gitalk.github.io/) + diff --git "a/docs/\345\215\232\345\256\242/2023/04/05\345\256\236\347\216\260\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217(uniapp)\344\270\212\344\274\240\345\244\264\345\203\217\350\207\263\351\230\277\351\207\214\344\272\221oss.md" "b/docs/\345\215\232\345\256\242/2023/04/05\345\256\236\347\216\260\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217(uniapp)\344\270\212\344\274\240\345\244\264\345\203\217\350\207\263\351\230\277\351\207\214\344\272\221oss.md" new file mode 100644 index 0000000..352e492 --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2023/04/05\345\256\236\347\216\260\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217(uniapp)\344\270\212\344\274\240\345\244\264\345\203\217\350\207\263\351\230\277\351\207\214\344\272\221oss.md" @@ -0,0 +1,354 @@ +# 小程序(uniapp)上传头像至OSS(阿里云)--保姆级 + +## 前言 + +自[微信小程序改版](https://developers.weixin.qq.com/community/develop/doc/000cacfa20ce88df04cb468bc52801)以来,现在获取用户的头像和昵称就不能直接通过[wx.getUserInfo](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/user-info/wx.getUserInfo.html)获取了。而是需要用户主动在登录后填写自己的昵称和头像,微信只是提供一个一键填写的快捷操作让用户直接使用自己已有的微信昵称或头像。 + +如果是想做一个比较完善的小程序系统,那么头像昵称的修改可谓是每个带用户的小程序开发都需要经历的。 + +昵称还好,就是一个文本字符串,但是头像的话我们就需要上传至自己的服务器或者是一些云对象存储服务,这里我选择的是阿里云OSS服务,下面开始我的**保姆级教程**。 + +## 流程概览 + +![](https://oss.justin3go.com/blogs/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F%E4%B8%8A%E4%BC%A0%E9%98%BF%E9%87%8C%E4%BA%91OSS%E6%97%B6%E5%BA%8F%E5%9B%BE.png) + +如上是整个上传头像的一个时序图,总的来说有这五步: + +1. 通过微信自带组件获取用户选择头像的临时文件路径 +2. 获取对OSS的操作授权 +3. 配置后端服务生成临时授权的服务 +4. 获取授权并上传文件至OSS +5. 将新的头像路径保存到数据库用户表中 + +好,接下来我们就以上述五步慢慢道来: + +## 1. 获取头像临时路径 + +> 据描述:是要将 [button](https://developers.weixin.qq.com/miniprogram/dev/component/button.html) 组件 `open-type` 的值设置为 `chooseAvatar`,当用户选择需要使用的头像之后,可以通过 `bindchooseavatar` 事件回调获取到头像信息的临时路径。 + +所以笔者编写了如下代码: + +```vue +// template + +``` + +```ts +// script +function onChooseAvatar(e: any) { + console.log("choose avatar: ", e.detail); + user.avatarUrl = e.detail.avatarUrl; +} +``` + +为了让`button`样式透明,所以`style`代码如下 + +```scss +.avatar-wrapper { + padding-left: 0; + padding-right: 0; + height: 50px; + line-height: 50px; + background-color: transparent; + border-color: transparent; +} + +.avatar-wrapper::after { + border: none; +} +``` + +这里笔者在`button`里面包裹了一个`icon`,因为我不想要`button`的样式,而仅仅用户点击`icon`就可以修改头像。 + +此时我们点击就可以获取头像的临时路径了: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230405100333.png) + +然后我们选择微信头像或者本地上传的图片后就可以出现如下的日志信息: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230405100436.png) + +到这里,我们这一步就已经完成了,已经获取到如上的头像临时路径了... + +## 2. 获取OSS操作授权 + +这里假设你已经开通了[OSS服务](https://oss.console.aliyun.com/overview) ,开通过程非常简单,毕竟花钱的过程一般来说都是非常简单的,一步到位🤬。这里就不过多赘述了。 + +如果你想要你的OSS服务拥有自己的专属域名以及CDN加速的话,可以查看我之间写的这篇文章--[CDN实践配置+原理篇](https://justin3go.com/%E5%8D%9A%E5%AE%A2/2022/13CDN%E5%AE%9E%E8%B7%B5%E9%85%8D%E7%BD%AE+%E5%8E%9F%E7%90%86%E7%AF%87.html#%E5%B8%B8%E8%A7%81%E7%9A%84oss%E9%85%8D%E7%BD%AE%E6%96%B9%E5%BC%8F) + +首先我们[创建一个我们阿里云账号的子用户](https://ram.console.aliyun.com/users),这个子用户我们后续会将它授权给我们的服务器,让我们的服务器操作该子用户,拥有其拥有的权限。你的就是我的🤭 + +**1)配置阿里云-访问控制** + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230405103136.png) +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230405103159.png) + +按照上述的流程点击创建后,就会出现如下页面,这里值得注意的是我们需要**保存**好其中的`accessKeyId`以及`accessKeySecret` + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230405103406.png) +拥有这个Id+Secret相当于我们就拥有了这个用户的身份,就可以操作该身份具有的资源了,但是此时我们还没有赋予其任何权限,所以这里我们赋予其操作OSS的权限,尽量不要赋予过多的权限,做好权限收敛: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230405104716.png) + +当然,如果你有多个用户有同样的权限操作,也可以使用用户组进行管理,这里不展开了。 + +点击添加权限,选择`AliyunOSSFullAccess` + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230405104844.png) + +**2)配置阿里云-OSS-细化授权** + +然后我们继续对OSS进行细化配置: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230405105255.png) + +**3)配置阿里云-OSS-跨域访问** + +如果你的OSS之前已经可以访问了,这里就不需要配置了,如果是第一次配置,那么记得还要对OSS进行跨域访问的配置,这也是很多网友出现403的主要原因。 + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230405105820.png) + +好,此时我们就拥有了一个有效的`AccesssKeyId`和`AccessKeySecret`l了 + +## 3. 配置服务器 + +这里我使用的是服务端签名,你也可以使用客户端签名临时凭证。并且我这里使用的后端技术栈是`NestJS + GraphQl`,如果你是其他技术栈,可以参考[原官方文档](https://help.aliyun.com/document_detail/92883.html)进行自定义 + +**0)安装** + +```sh +npm i crypto-js @types/crypto-js +``` + +**1)`/src/utils/oss.interface.ts`** + +```ts +export interface MpUploadOssHelperOptions { + // 阿里云账号AccessKey + accessKeyId: string; + // 阿里云账号AccessId + accessKeySecret: string; + // 限制参数的生效实践,单位为小时,默认值为1 + timeout?: number; + // 限制上传文件大小,单位为MB,默认值为10 + maxSize?: number; +} +``` + +**2)`/src/utils/oss.ts`** + +```ts +import * as crypto from 'crypto-js'; +import { MpUploadOssHelperOptions } from './oss.interface'; + +// 详见:https://help.aliyun.com/document_detail/92883.html +export class MpUploadOssHelper { + private accessKeyId: string; + private accessKeySecret: string; + private timeout: number; + private maxSize: number; + + constructor(options: MpUploadOssHelperOptions) { + this.accessKeyId = options.accessKeyId; + this.accessKeySecret = options.accessKeySecret; + // 限制参数的生效时间,单位为小时,默认值为1。 + this.timeout = options.timeout || 1; + // 限制上传文件的大小,单位为MB,默认值为10。 + this.maxSize = options.maxSize || 10; + } + + createUploadParams() { + const policy = this.getPolicyBase64(); + const signature = this.signature(policy); + return { + OSSAccessKeyId: this.accessKeyId, + policy: policy, + signature: signature, + }; + } + + getPolicyBase64() { + const date = new Date(); + // 设置policy过期时间。 + date.setHours(date.getHours() + this.timeout); + const srcT = date.toISOString(); + const policyText = { + expiration: srcT, + conditions: [ + // 限制上传文件大小。 + ['content-length-range', 0, this.maxSize * 1024 * 1024], + ], + }; + const buffer = Buffer.from(JSON.stringify(policyText)); + return buffer.toString('base64'); + } + + signature(policy) { + return crypto.enc.Base64.stringify( + crypto.HmacSHA1(policy, this.accessKeySecret) + ); + } +} +``` + + +**3)`src/oss/models/oss.model.ts`** + +```ts +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class Oss { + @Field() + OSSAccessKeyId: string; + + @Field() + policy: string; + + @Field() + signature: string; +} +``` + +**4)`src/oss/oss.module.ts`** + +```ts +import { Module } from '@nestjs/common'; +import { OssService } from './oss.service'; +import { OssResolver } from './oss.resolver'; + +@Module({ + providers: [OssService, OssResolver], +}) +export class OssModule {} +``` + +**5)`src/oss/oss.service.ts`** + +```ts +import { Injectable } from '@nestjs/common'; +import { MpUploadOssHelper } from 'src/utils/oss'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class OssService { + constructor(private readonly configService: ConfigService) {} + + getPostObjectParams() { + const mpHelper = new MpUploadOssHelper({ + accessKeyId: this.configService.get('ACCESS_KEY_ID'), + accessKeySecret: this.configService.get('ACCESS_KEY_SECRET'), + timeout: 1, + maxSize: 1, + }); + + // 生成参数 + const params = mpHelper.createUploadParams(); + + return params; + } +} +``` + +注意:这里我是通过环境变量获取的`ID`和`KEY`,环境变量的使用可参考[这篇文章](https://docs.nestjs.com/techniques/configuration#configuration) + +**6)`src/oss/oss.resolve.ts`或者`src/oss/oss.controller.ts`** + +这里就体现了service作为业务层的作用了,无论我们是想使用GraphQL,还是使用REST API暴露接口,都可以非常灵活的替换,如下是GraphQL的代码: + +```ts +import { UseGuards } from '@nestjs/common'; +import { Resolver, Query } from '@nestjs/graphql'; +import { GqlAuthGuard } from 'src/auth/gql-auth.guard'; +import { Oss } from './models/oss.model'; +import { OssService } from './oss.service'; + +@Resolver() +export class OssResolver { + constructor(private readonly ossService: OssService) {} + + @UseGuards(GqlAuthGuard) + @Query(() => Oss) + getPostObjectParams() { + return this.ossService.getPostObjectParams(); + } +} +``` + +演示: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230405112525.png) + +## 4. 上传文件至OSS + +如下代码,这里使用的是GraphQL请求的接口,如果你是使用的REST API,基本流程也是一样的: + +```ts +async function uploadAvatar(filePath: string) { + // 1. 请求服务端签名凭证 + const { execute } = useQuery({ query: getPostObjectParamsGQL }); + uni.showLoading({ title: "正在请求上传凭证中..." }); + const { error, data } = await execute(); + if (error) { + uni.showToast({ + title: `上传头像失败: ${error}`, + icon: "error", + duration: 2000, + }); + throw new Error(`上传头像失败: ${error}`); + } + + // 2. 上传图片至oss + const { OSSAccessKeyId, policy, signature } = data?.getPostObjectParams || {}; + + const imgType = filePath.split(".").pop(); + const key = `wxmp/${userData?.id}.${imgType}`; + uni.showLoading({ title: "正在上传图片中..." }); + const ossRes = await uniUploadFile({ + url: ossHost, // 开发者服务器的URL。 + filePath, + name: "file", // 必须填file。 + formData: { + key, + policy, + OSSAccessKeyId, + signature, + }, + }); + uni.hideLoading(); + + return ossHost + "/" + key; +} +``` + +上述代码我是以用户的uid重命名图片文件的,就是key这个属性值,你也可以自定义你自己的文件命名方式。 + +演示: + +![](https://oss.justin3go.com/blogs/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%A4%B4%E5%83%8F%E4%B8%8A%E4%BC%A0OSS%E6%BC%94%E7%A4%BA.gif) + +然后OSS中就可以查看到这张图片了: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230405121314.png) + +## 5. 保存数据库用户表 + +这一步其实就很简单了,也没什么参考价值,就是请求API,然后保存到数据库就可以了,具体代码就不贴了,如下是效果: + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230405121454.png) +## 最后 + +小程序的开发几乎是程序员的必备操作,自带流量,前期非常好用,不过就是开发文档确实看着不舒服,平台改版我们也需要跟着改版... + +然后用户登录以及自定义头像都是小程序们非常常见的操作,这里演示一下过程希望对你有所帮助,如果有操作不当,欢迎在评论区中友善指出.. + +![](https://oss.justin3go.com/blogs/QQ%E5%9B%BE%E7%89%8720230405121919.jpg) + +## 参考 + +- [微信开放文档-获取头像昵称](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/userProfile.html) +- [阿里云开发文档-微信小程序直传实践](https://help.aliyun.com/document_detail/92883.html) +- [NestJS官方文档-环境变量配置](https://docs.nestjs.com/techniques/configuration#configuration) + diff --git "a/docs/\345\215\232\345\256\242/2023/04/17\346\210\221\347\273\210\344\272\216\344\274\232\347\224\250Docker\344\272\206(nest+prisma+psotgresql+nginx+https).md" "b/docs/\345\215\232\345\256\242/2023/04/17\346\210\221\347\273\210\344\272\216\344\274\232\347\224\250Docker\344\272\206(nest+prisma+psotgresql+nginx+https).md" new file mode 100644 index 0000000..9787131 --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2023/04/17\346\210\221\347\273\210\344\272\216\344\274\232\347\224\250Docker\344\272\206(nest+prisma+psotgresql+nginx+https).md" @@ -0,0 +1,352 @@ +# 我终于会用Docker了(nest+prisma+psotgresql+nginx+https) + +## 前言 + +这次自己有一个NestJS后端服务需要自己部署了,之前部署node服务可能更多是pm2那一套,自己的项目可以尝试各种各样的技术,所以这次就尝试一下早就想用的docker来实际部署一下 + +本文会讲什么:对于docker的理解,docker必知必会的命令,以及最后是笔者的实战部署,和一些踩坑记录; +本文不会讲什么:docker更深层次的原理,即本文更多是一篇应用性文章,欢迎继续阅读后续章节; + +## 基本概念介绍 + +### 简介 + +如果是计算机专业的学生,那么是肯定学过操作系统的,而学这门课程有一门操作系统实验,需要自己在linux上通过模拟器再运行一个非常低版本的linux,笔者隐约记得好像是0.11那个版本,因为这个版本代码相对来说都是比较核心主要的功能,用来学习是非常不错的。 + +在操作系统运行一个可以运行其他环境的"系统环境",这不就是Docker的概念吗。或者如果你没有上述的经历,那总在本机运行过VM吧,甚至说win11中的wsl总知道吧。 + +**基本概念就是这个:在linux系统上运行一个"系统环境",官方叫做容器。** + +那这个所谓的"系统环境"可以做什么呢,当然是运行一些我们常用的东西,如node、mysql、redis以及我们自己的应用程序等。 + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230417094610.png) +[图片来源](https://www.docker.com/resources/what-container/) + +### 优点 + +好,问题又来了,为什么不直接在该linux系统上运行这些环境:这个问题用Docker的主要优势就能回答: + +**1)简化应用程序部署和依赖管理** + +Docker是将应用程序和其依赖项打包在一个容器中,避免了复杂的依赖管理和手动安装,因此可以快速部署和更新应用程序。 + +怎么理解: + +- python打包exe用过吧:它会将其依赖的所有环境一起打包最后成为一个可执行文件,形成了这样一个可执行文件,是不是用户只需要双击就能打开,不需要像程序员一样安装各种复杂的依赖 +- 前端的electron用过吧:它也将应用程序与浏览器环境打包在一起了,用户也可以直接使用你的应用程序了 +- 这么理解:有了它,我们就可以直接把这个"软件包"进行部署,而没有它,我们就需要到用户的主机,去干什么`pip install`之类的,真累~ + +**2)跨平台和环境可移植性** + +Docker容器可以在任何操作系统、云平台和虚拟化环境中运行,保证了应用程序在不同环境下的稳定性和可靠性。 + +也是第一点所讲到的,既然都打包成了一个软件包,那么就可以非常方便地运行在各个环境之中了: + +- 想想Java虚拟机JVM,Java刚出的时候主打的就是一个可移植性,其就是通过先编译成字节码,也就是那个`.class`文件,然后该文件就可以在任何安装了JVM的系统上运行了。 +- Docker也是,只要该环境安装了Docker,那么由其打包的"软件包"也就能直接运行了 + +### 一些关键词及概念 + +想入门某个领域,第一步就是得先了解该领域特有的关键热词,下面是Docker中常用的一些关键词及概念简介: + +1. **Docker镜像(Image)**: Docker镜像是一个轻量级的、可移植的、自包含的软件包,其中包含了运行某个应用程序所需的所有文件、配置和依赖项。 +2. **Docker容器(Container)**: Docker容器是由Docker镜像创建的运行实例,容器中包含了所有运行应用程序所需的文件、配置和依赖项,以及运行时环境。 + +> 镜像与容器的概念就和程序与进程的概念是一致的,一个是乐谱,另外一个就是正在演奏的音乐了 + +4. **Docker仓库(Registry)**: Docker仓库是一个用于存储和共享Docker镜像的中央存储库,Docker Hub是一个公共的Docker仓库。 +5. **Dockerfile**: Dockerfile是一个文本文件,其中包含了一组指令,用于自动化构建Docker镜像。就和C语言中的Makefile差不多。 +6. **Docker Compose**: Docker Compose是一个工具,用于定义和运行多个Docker容器组成的应用程序,并管理它们之间的交互。 +7. **Docker网络(Network)**: Docker网络是一种机制,用于在多个Docker容器之间建立网络连接,以实现容器之间的通信。 +8. **Docker数据卷(Volume)**: Docker数据卷是一种机制,用于在Docker容器和主机之间共享数据。 +9. **Docker daemon**:是Docker的核心组件之一,也称为Docker引擎。它是一个长期运行的后台进程,负责管理Docker镜像、容器、网络和存储卷等资源。 + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230417094716.png) +[图片来源](https://algodaily.com/lessons/what-is-a-container-a-docker-tutorial) + +*相关概念如果不理解也没关系,后续实战时看看这些东西到底长啥样,就自然而然就明白了* + +## 常用命令 + +这里简单索引并介绍一下笔者自己常用的一些关于docker及docker-compose相关的命令,希望对你所有帮助: + +|命令|作用|备注| +|-|-|-| +|`docker ps -n 5`|查看正在运行的前5个容器|数字代表前几个,也可以不加-n,就是所有;就和`ps -ef`查看正在运行的进程一样| +|`docker rm $(docker ps -a -q)`|删除所有已经停止的容器|无| +|`docker tag [镜像id] [新镜像名称]:[新镜像标签]`|根据id为某个镜像添加名称及标签|偶尔镜像名字显示``时极其有用| +|略|容器/镜像的导入与导出|[参考链接](https://zhuanlan.zhihu.com/p/619626619)| +| `docker-compose up -d`| 启动并后台运行|无| +|`docker exec -i -t containerId /bin/bash`|进入到容器内部,并启动一个bash shell,开始交互式操作|CTRL+D退出| + +Docker与Docker-compose:构建镜像 + +|命令|作用|备注| +|-|-|-| +|`docker build -t nest-api .`|在当前目录`.`构建镜像,默认使用Dockerfile文件|-f 指定任一Dockerfile文件;-t代表镜像名| +|`docker-compose build`|在当前目录,通过默认的docker-compose.yml文件进行构建|略| + +一般一个Dockerfile对应一个容器,如果我们要部署多个容器,就需要每次运行各个不同的Dockerfile,为了简化docker的复杂操作,就有了dcoekr-compose,它就可以在其yml文件上写多个镜像进行部署容器 + +还有就是这里有一个shell脚本,后续会使用该`setup.sh`,作用就是每次更新部署时在对应目录运行就可以了: + +```shell +#!/usr/bin/env bash +#image_version=`date +%Y%m%d%H%M`; + +# 关闭容器 +docker-compose stop || true; +# 删除容器 +docker-compose down || true; +# 构建镜像 +docker-compose build; +# 启动并后台运行 +docker-compose up -d; +# 查看日志 +docker logs nodejs; +# 对空间进行自动清理 +docker system prune -a -f +``` + +## 实战 + +现在,你已经了解了Docker的相关概念以及Docker的一些常用操作了,下面就进入实战让你练练手并加深理解。 + +我的项目文件目录如下: + +```txt +nest-api +├─ .dockerignore +├─ .eslintrc.js +├─ .github +│ └─ workflows +│ └─ ci.yml +├─ .gitignore +├─ .graphqlconfig +├─ .node-version +├─ .prettierrc.json +├─ .vscode +│ └─ extensions.json +├─ docker-compose.db.yml +├─ docker-compose.migrate.yml +├─ docker-compose.yml +├─ Dockerfile +├─ Dockerfile.alpine +├─ LICENSE +├─ nest-cli.json +├─ package-lock.json +├─ package.json +├─ pull-env.sh +├─ .env +├─ README.md +├─ run.sh +├─ setup.sh +├─ src[略] +略... +``` + +### prisma+nest应用的Dockerfile + +编写自己的NestJS应用需要的Dockerfile: + +```txt +FROM node:16-alpine AS builder + +# Create app directory +WORKDIR /app + +# A wildcard is used to ensure both package.json AND package-lock.json are copied +COPY package*.json ./ +COPY prisma ./prisma/ + +# Install app dependencies +RUN npm install + +COPY . . + +RUN npm run build + +FROM node:16-alpine + +WORKDIR /app + +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package*.json ./ +COPY --from=builder /app/dist ./dist + +EXPOSE 3000 +CMD [ "npm", "run", "start:prod" ] +``` + +这些英文单词都是一看就差不多懂得命令这里就不过多赘述了,其中`FROM node:16-alpine`代表该镜像继承自node镜像,毕竟是个node应用嘛;`apline`版本更加轻量,打包体积更小,一般来说不去编写哪些C++扩展都是够用的。 + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230417111640.png) + +[图片来源](https://juejin.cn/post/6844904006184091662#heading-6) + +### nest应用+postgresql+niginx的yml配置 + +此时相当于我们就有我们自己应用的镜像文件了,但其依赖的环境还没有,比如postgresql;还有就是我需要的nginx,这些都是有现成的,所以就不用自己构建了,直接使用;但想要将它们弄在一起,就需要编写`docker-compose.yml`文件,如下: + +```yaml +version: '3.8' +services: + nest-api: + container_name: nest-api + build: + context: . + dockerfile: Dockerfile + ports: + - '3000:3000' + depends_on: + - postgres + env_file: + - .env + + postgres: + image: postgres:13 + container_name: postgres + restart: always + ports: + - '5432:5432' + env_file: + - .env + volumes: + - postgres:/var/lib/postgresql/data + + nginx: + image: nginx:stable-alpine # 指定服务镜像 + container_name: nginx # 容器名称 + restart: always # 重启方式 + ports: # 映射端口 + - "80:80" + - "443:443" + volumes: # 挂载数据卷 + - /etc/localtime:/etc/localtime + - /home/ubuntu/work/nginx/conf.d:/etc/nginx/conf.d + - /home/ubuntu/work/nginx/logs:/var/log/nginx + - /home/ubuntu/work/nginx/cert:/etc/nginx/cert + depends_on: # 启动顺序 + - nest-api + +volumes: + postgres: + name: nest-db + +``` + +其中`volumes`的意思就是让容器中的某个文件与操作系统中的某个文件进行对应,从而做到持久化存储。 + +### 配置niginx + +上述中的: + +```yaml + - /home/ubuntu/work/nginx/conf.d:/etc/nginx/conf.d + - /home/ubuntu/work/nginx/logs:/var/log/nginx + - /home/ubuntu/work/nginx/cert:/etc/nginx/cert +``` + +冒号前面的路径是我自己在操作系统环境中创建的路径,你也可以根据自己的习惯进行创建,然后对应到容器的路径就可以了 + +**1)conf.d配置** + +```sh +vim /home/ubuntu/work/nginx/conf.d/default.conf +``` + +该文件内容如下: + +```txt +server { + listen 443 ssl http2; + server_tokens off; + + root /var/www/html; + index index.html index.htm; + + # 修改为自己的域名 + server_name api.example.com; + + client_max_body_size 4M; + # ssl证书存放位置 + ssl_certificate /etc/nginx/cert/api.example.com.pem; + ssl_certificate_key /etc/nginx/cert/api.example.com.key; + ssl_session_timeout 5m; + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_prefer_server_ciphers on; + + # 访问 / 路径时执行反向代理 + location / { + # 这里 nodejs 是 node 容器名 + proxy_pass http://nest-api:3000; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $host; + # 后端的Web服务器可以通过 X-Forwarded-For 获取用户真实 IP + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # 允许客户端请求的最大单文件字节数 + client_max_body_size 15M; + # 缓冲区代理缓冲用户端请求的最大字节数 + client_body_buffer_size 128k; + } +} +server { + listen 80; + #请填写绑定证书的域名 + server_name api.example.com; + #把http的域名请求转成https + return 301 https://$host$request_uri; +} +``` + +2)cert文件夹 + +记得放入你下载的ssl证书就可以了。 + +3)logs + +对应的日志,有问题就就`cat`一下,看里面有error不 + +### prisma的一个踩坑 + +这是prisma需要的databaseURL: + +```txt +DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DB_HOST}:${DB_PORT}/${POSTGRES_DB}?schema=${DB_SCHEMA}&sslmode=prefer +``` + +其中`DB_HOST`在nest运行在本地的时候,比如你启动了postgresql,然后`npm run start:dev`,这时候`DB_HOST=localhost` + +而当你nest运行在容器中,比如部署的时候`docker-compose up -d`,就需要改为对应容器的名字`postgres` + +我确实忘改了😭😭😭 + +## 最后 + +笔者在本文主要叙述了如下部分,也可以当作自检清单: + +1. docker是什么 +2. docker的优势,为什么要使用它 +3. docker相关的关键词及概念 +4. 常用命令 +5. 最后用了现在比较新的一些技术栈部署实践 + +本文实战部分没有每步都截图演示,假的理由是因为自己试试比啥都好,真实原因是懒得再去运行截图了... + +最后希望本文对你有所帮助,如果本文中理解有误或者操作配置不当,也希望互帮互助,在评论区中友善指出... + +## 参考 + +- [前端全栈之路 - 玩转 Docker (基础)](https://juejin.cn/post/7147483669299462174) +- [如何通过Dockerfile优化Nestjs镜像大小](https://juejin.cn/post/7132533610707419173) +- [使用 Jenkins + Docker + Nginx + MySQL + Redis 自动部署 Node 项目](https://juejin.cn/post/6844904006184091662) +- [给前端写的Docker+Node+Nginx+Mongo的本地开发+部署实战](https://juejin.cn/post/6844904004296638478) +- [使用 GitHub Actions 完成 Nest 项目自动打包并发布到服务器](https://juejin.cn/post/7169485484568084510) +- [Nestjs | 实践:如何编写生产环境下的Dockerfile?](https://juejin.cn/post/7205508171523604540) +- [Nest系列(八)一路坎坷,我实现了最简便的方式打包部署nestjs+prisma应用](https://juejin.cn/post/7175937839069134903) +- [Running docker compose up -d (strconv.Atoi: parsing "": invalid syntax)](https://github.com/docker/compose-cli/issues/1537) +- [Nginx 服务器 SSL 证书安装部署](https://cloud.tencent.com/document/product/400/35244) +- [Docker Nginx 配置安装 SSL 证书(支持 Https 访问)](https://www.quanxiaoha.com/docker/docker-nginx-install-ssl.html) +- [Prisma Migrate: Deploy Migration with Docker](https://notiz.dev/blog/prisma-migrate-deploy-with-docker) + diff --git "a/docs/\345\215\232\345\256\242/2023/04/20Vue3+TS(uniapp)\346\211\213\346\222\270\344\270\200\344\270\252\350\201\212\345\244\251\351\241\265\351\235\242.md" "b/docs/\345\215\232\345\256\242/2023/04/20Vue3+TS(uniapp)\346\211\213\346\222\270\344\270\200\344\270\252\350\201\212\345\244\251\351\241\265\351\235\242.md" new file mode 100644 index 0000000..81a4649 --- /dev/null +++ "b/docs/\345\215\232\345\256\242/2023/04/20Vue3+TS(uniapp)\346\211\213\346\222\270\344\270\200\344\270\252\350\201\212\345\244\251\351\241\265\351\235\242.md" @@ -0,0 +1,444 @@ +# Vue3+TS(uniapp)手撸一个聊天页面 + +## 前言 + +最近在自己的小程序中做了一个智能客服,API使用的是云厂商的API,然后聊天页面...嗯,找了一下关于UniApp(vite/ts)版本的好像不多,有一个官方的但其中的其他代码太多了,去看懂再删除那些对我无用的代码不如自己手撸一个,先看效果: + +![](https://oss.justin3go.com/blogs/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F%E6%99%BA%E8%83%BD%E5%AE%A2%E6%9C%8D%E6%BC%94%E7%A4%BA.gif) + +好,下面开始介绍如何一步一步实现 + +## 重难点调研 + +### 1. 如何编写气泡 + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230420162759.png) + +可以发现一般的气泡是有个“小箭头”,一般是指向用户的头像,所以这里我们的初步思路就是通过`before`与`after`伪类来放置这个小三角形,这个小三角形通过隐藏border的其余三边来实现。 + +![](https://oss.justin3go.com/blogs/Pasted%20image%2020230420163410.png) + +然后其中一个细节就是聊天气泡的最大宽度不超过对方的头像,超过就换行。这个简单,设置一个`max-width: cacl(100vw - XX)`就可以了 + +### 2. 如何编写输入框 + +考虑到用户可能输入多行文字,这里使用的是`