From 2d7679ecb5205e6f58019570be11d093c32bf6c4 Mon Sep 17 00:00:00 2001 From: storyxc Date: Wed, 21 Feb 2024 10:09:07 +0000 Subject: [PATCH] =?UTF-8?q?github=20actions=E8=87=AA=E5=8A=A8=E9=83=A8?= =?UTF-8?q?=E7=BD=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 404.html | 22 + CNAME | 1 + ...\344\275\223\345\256\236\347\216\260.html" | 673 +++ ...\351\223\276\346\250\241\345\274\217.html" | 125 + ...234\272\344\270\255\347\232\204kafka.html" | 31 + ...cker Desktop\345\256\211\350\243\205.html" | 25 + ...6\234\272\345\220\257\345\212\250WSL.html" | 29 + ...7\275\262vue\351\241\271\347\233\256.html" | 137 + ...\347\232\204\351\227\256\351\242\230.html" | 41 + ...\346\255\245\346\233\264\346\226\260.html" | 55 + ...\347\232\204\344\273\243\347\220\206.html" | 27 + ...\350\241\214\350\204\232\346\234\254.html" | 69 + ...\345\217\212\345\244\204\347\220\206.html" | 157 + ...\347\273\255\351\203\250\347\275\262.html" | 137 + actions/index.html | 25 + ...\347\241\200\350\257\255\346\263\225.html" | 123 + actions/tools/Sketch.html | 25 + ...\344\274\240\345\233\276\345\272\212.html" | 25 + ...\344\271\246\351\225\234\345\203\217.html" | 51 + ...\344\273\244\346\225\264\347\220\206.html" | 31 + ...\351\242\230\347\273\210\347\253\257.html" | 41 + ...\351\200\237\350\277\236\346\216\245.html" | 25 + ...27\264\346\234\272\345\231\250server.html" | 29 + .../powershell\347\276\216\345\214\226.html" | 43 + ...\344\270\200\350\207\264\346\200\247.html" | 25 + ...\351\235\242\347\276\216\345\214\226.html" | 1421 +++++ ...23\345\256\236\347\216\260.md.b828e5be.js" | 649 +++ ...5\256\236\347\216\260.md.b828e5be.lean.js" | 1 + ...76\346\250\241\345\274\217.md.952322e1.js" | 101 + ...6\250\241\345\274\217.md.952322e1.lean.js" | 1 + ...4\270\255\347\232\204kafka.md.375de9c2.js" | 7 + ...\255\347\232\204kafka.md.375de9c2.lean.js" | 1 + ...op\345\256\211\350\243\205.md.2d631526.js" | 1 + ...5\256\211\350\243\205.md.2d631526.lean.js" | 1 + ...345\220\257\345\212\250WSL.md.2ecc3382.js" | 5 + ...20\257\345\212\250WSL.md.2ecc3382.lean.js" | 1 + ...ue\351\241\271\347\233\256.md.b6d298c3.js" | 113 + ...1\241\271\347\233\256.md.b6d298c3.lean.js" | 1 + ...04\351\227\256\351\242\230.md.bfa9056c.js" | 17 + ...1\227\256\351\242\230.md.bfa9056c.lean.js" | 1 + ...45\346\233\264\346\226\260.md.e089c9d0.js" | 31 + ...6\233\264\346\226\260.md.e089c9d0.lean.js" | 1 + ...04\344\273\243\347\220\206.md.323e2f5e.js" | 3 + ...4\273\243\347\220\206.md.323e2f5e.lean.js" | 1 + ...14\350\204\232\346\234\254.md.6cf6a3a7.js" | 45 + ...0\204\232\346\234\254.md.6cf6a3a7.lean.js" | 1 + ...12\345\244\204\347\220\206.md.e9f9fd1d.js" | 133 + ...5\244\204\347\220\206.md.e9f9fd1d.lean.js" | 1 + ...55\351\203\250\347\275\262.md.01116c37.js" | 113 + ...1\203\250\347\275\262.md.01116c37.lean.js" | 1 + assets/actions_index.md.26d63a13.js | 1 + assets/actions_index.md.26d63a13.lean.js | 1 + ...00\350\257\255\346\263\225.md.9b7f7a0a.js" | 99 + ...0\257\255\346\263\225.md.9b7f7a0a.lean.js" | 1 + assets/actions_tools_Sketch.md.b25b7913.js | 1 + .../actions_tools_Sketch.md.b25b7913.lean.js | 1 + ...40\345\233\276\345\272\212.md.6002b22d.js" | 1 + ...5\233\276\345\272\212.md.6002b22d.lean.js" | 1 + ...46\351\225\234\345\203\217.md.c0cc3ed0.js" | 27 + ...1\225\234\345\203\217.md.c0cc3ed0.lean.js" | 1 + ...44\346\225\264\347\220\206.md.853e93cd.js" | 7 + ...6\225\264\347\220\206.md.853e93cd.lean.js" | 1 + ...30\347\273\210\347\253\257.md.f87cdce3.js" | 17 + ...7\273\210\347\253\257.md.f87cdce3.lean.js" | 1 + ...37\350\277\236\346\216\245.md.3487ddd6.js" | 1 + ...0\277\236\346\216\245.md.3487ddd6.lean.js" | 1 + ...\234\272\345\231\250server.md.3c94d4f3.js" | 5 + ...272\345\231\250server.md.3c94d4f3.lean.js" | 1 + ...ll\347\276\216\345\214\226.md.d9be26ce.js" | 19 + ...7\276\216\345\214\226.md.d9be26ce.lean.js" | 1 + ...00\350\207\264\346\200\247.md.98a2c898.js" | 1 + ...0\207\264\346\200\247.md.98a2c898.lean.js" | 1 + ...42\347\276\216\345\214\226.md.852dab4f.js" | 1397 +++++ ...7\276\216\345\214\226.md.852dab4f.lean.js" | 1 + assets/app.058f2afe.js | 1 + assets/chunks/VPAlgoliaSearchBox.bbe9601b.js | 17 + assets/chunks/framework.dc1f4608.js | 29 + assets/chunks/theme.bfdc69c3.js | 1 + ...le\350\257\255\346\263\225.md.7c5d66d7.js" | 101 + ...0\257\255\346\263\225.md.7c5d66d7.lean.js" | 1 + ...se\350\257\255\346\263\225.md.5b63270f.js" | 95 + ...0\257\255\346\263\225.md.5b63270f.lean.js" | 1 + ...273\234\344\271\213macvlan.md.15dfe39b.js" | 43 + ...34\344\271\213macvlan.md.15dfe39b.lean.js" | 1 + assets/docker_index.md.f95bad4d.js | 1 + assets/docker_index.md.f95bad4d.lean.js | 1 + ...50\346\214\207\344\273\244.md.6b637e11.js" | 11 + ...6\214\207\344\273\244.md.6b637e11.lean.js" | 1 + assets/frontend_base_CSS.md.9a8416e3.js | 391 ++ assets/frontend_base_CSS.md.9a8416e3.lean.js | 1 + assets/frontend_base_HTML.md.bd02116d.js | 1 + assets/frontend_base_HTML.md.bd02116d.lean.js | 1 + .../frontend_base_JavaScript.md.5b22eba4.js | 1461 +++++ ...ontend_base_JavaScript.md.5b22eba4.lean.js | 1 + assets/frontend_base_Nodejs.md.a3da6ed0.js | 29 + .../frontend_base_Nodejs.md.a3da6ed0.lean.js | 1 + .../frontend_base_Typescript.md.47bd8e1c.js | 1371 +++++ ...ontend_base_Typescript.md.47bd8e1c.lean.js | 1 + assets/frontend_base_Webpack.md.f49018f3.js | 141 + .../frontend_base_Webpack.md.f49018f3.lean.js | 1 + assets/frontend_base_jQuery.md.4056042a.js | 115 + .../frontend_base_jQuery.md.4056042a.lean.js | 1 + assets/frontend_framework_Vue.md.98b6e4a2.js | 923 +++ ...frontend_framework_Vue.md.98b6e4a2.lean.js | 1 + assets/frontend_index.md.01cb292c.js | 1 + assets/frontend_index.md.01cb292c.lean.js | 1 + ...01\345\210\206\346\236\220.md.6309dbd0.js" | 603 ++ ...5\210\206\346\236\220.md.6309dbd0.lean.js" | 1 + ...d_others_HackingWithSwift-1.md.355cc977.js | 853 +++ ...ers_HackingWithSwift-1.md.355cc977.lean.js | 1 + ...d_others_HackingWithSwift-2.md.85e4f87a.js | 1413 +++++ ...ers_HackingWithSwift-2.md.85e4f87a.lean.js | 1 + ...UI\345\205\245\351\227\250.md.f738d450.js" | 319 ++ ...5\205\245\351\227\250.md.f738d450.lean.js" | 1 + ...ft\350\257\255\346\263\225.md.9c4e5628.js" | 305 + ...0\257\255\346\263\225.md.9c4e5628.lean.js" | 1 + assets/golang_base_GoTemplate.md.26e53b7c.js | 391 ++ ...golang_base_GoTemplate.md.26e53b7c.lean.js | 1 + ...00\350\257\255\346\263\225.md.5d06c4a6.js" | 5003 ++++++++++++++++ ...0\257\255\346\263\225.md.5d06c4a6.lean.js" | 1 + assets/golang_cli_Cobra.md.d15fd694.js | 369 ++ assets/golang_cli_Cobra.md.d15fd694.lean.js | 1 + assets/golang_index.md.56aeaf33.js | 1 + assets/golang_index.md.56aeaf33.lean.js | 1 + .../golang_tools_redis-cleaner.md.c2849590.js | 143 + ...ng_tools_redis-cleaner.md.c2849590.lean.js | 1 + assets/golang_web_Gin.md.a83e3304.js | 491 ++ assets/golang_web_Gin.md.a83e3304.lean.js | 1 + assets/index.md.a91419bd.js | 1 + assets/index.md.a91419bd.lean.js | 1 + .../inter-italic-cyrillic-ext.33bd5a8e.woff2 | Bin 0 -> 28332 bytes assets/inter-italic-cyrillic.ea42a392.woff2 | Bin 0 -> 17824 bytes assets/inter-italic-greek-ext.4fbe9427.woff2 | Bin 0 -> 12188 bytes assets/inter-italic-greek.8f4463c4.woff2 | Bin 0 -> 23264 bytes assets/inter-italic-latin-ext.bd8920cc.woff2 | Bin 0 -> 63552 bytes assets/inter-italic-latin.bd3b6f56.woff2 | Bin 0 -> 46048 bytes assets/inter-italic-vietnamese.6ce511fb.woff2 | Bin 0 -> 8784 bytes .../inter-roman-cyrillic-ext.e75737ce.woff2 | Bin 0 -> 26600 bytes assets/inter-roman-cyrillic.5f2c6c8c.woff2 | Bin 0 -> 16780 bytes assets/inter-roman-greek-ext.ab0619bc.woff2 | Bin 0 -> 11808 bytes assets/inter-roman-greek.d5a6d92a.woff2 | Bin 0 -> 21776 bytes assets/inter-roman-latin-ext.0030eebd.woff2 | Bin 0 -> 59608 bytes assets/inter-roman-latin.2ed14f66.woff2 | Bin 0 -> 42464 bytes assets/inter-roman-vietnamese.14ce25a6.woff2 | Bin 0 -> 8492 bytes ...01\345\255\246\344\271\240.md.9d40a87c.js" | 569 ++ ...5\255\246\344\271\240.md.9d40a87c.lean.js" | 1 + ...47\345\233\236\351\241\276.md.771d4e14.js" | 133 + ...5\233\236\351\241\276.md.771d4e14.lean.js" | 1 + ...45\345\255\246\344\271\240.md.d1707e3c.js" | 89 + ...5\255\246\344\271\240.md.d1707e3c.lean.js" | 1 + ...ql\347\264\242\345\274\225.md.46f173cb.js" | 23 + ...7\264\242\345\274\225.md.46f173cb.lean.js" | 1 + ...17\345\220\214\346\255\245.md.892bc0fe.js" | 123 + ...5\220\214\346\255\245.md.892bc0fe.lean.js" | 1 + ...26\345\255\246\344\271\240.md.84dffe0e.js" | 7 + ...5\255\246\344\271\240.md.84dffe0e.lean.js" | 1 + ...75\345\221\250\346\234\237.md.e9b078b3.js" | 1 + ...5\221\250\346\234\237.md.e9b078b3.lean.js" | 1 + ...30\350\256\260\345\275\225.md.647fa7c0.js" | 229 + ...0\256\260\345\275\225.md.647fa7c0.lean.js" | 1 + ...es\346\263\250\350\247\243.md.8a9eff27.js" | 7 + ...6\263\250\350\247\243.md.8a9eff27.lean.js" | 1 + ...15\345\216\237\347\220\206.md.6a5d7c18.js" | 1 + ...5\216\237\347\220\206.md.6a5d7c18.lean.js" | 1 + ...25\345\216\206\345\217\262.md.f427bc3e.js" | 1 + ...5\216\206\345\217\262.md.f427bc3e.lean.js" | 1 + ...or\351\205\215\347\275\256.md.c35e48c8.js" | 737 +++ ...1\205\215\347\275\256.md.c35e48c8.lean.js" | 1 + ...66\346\225\260\346\215\256.md.ecde083a.js" | 1809 ++++++ ...6\225\260\346\215\256.md.ecde083a.lean.js" | 1 + ...ig\346\216\245\345\205\245.md.d19b8abd.js" | 121 + ...6\216\245\345\205\245.md.d19b8abd.lean.js" | 1 + ...01\345\210\206\346\236\220.md.ca08bf1a.js" | 617 ++ ...5\210\206\346\236\220.md.ca08bf1a.lean.js" | 1 + ...75\346\263\250\350\247\243.md.e8c07bfa.js" | 1 + ...6\263\250\350\247\243.md.e8c07bfa.lean.js" | 1 + ...66\351\205\215\347\275\256.md.ba4117be.js" | 119 + ...1\205\215\347\275\256.md.ba4117be.lean.js" | 1 + ...52\345\256\232\344\271\211.md.9e87748a.js" | 183 + ...5\256\232\344\271\211.md.9e87748a.lean.js" | 1 + ...57\345\212\237\350\203\275.md.955a4962.js" | 419 ++ ...5\212\237\350\203\275.md.955a4962.lean.js" | 1 + ...on\345\205\261\344\272\253.md.4d9b54de.js" | 103 + ...5\205\261\344\272\253.md.4d9b54de.lean.js" | 1 + ...77\346\234\215\345\212\241.md.6feadc2c.js" | 241 + ...6\234\215\345\212\241.md.6feadc2c.lean.js" | 1 + ...a_framework_swagger_knife4j.md.31965f41.js | 377 ++ ...mework_swagger_knife4j.md.31965f41.lean.js | 1 + ...5\257\274\345\207\272excel.md.6234940e.js" | 75 + ...\274\345\207\272excel.md.6234940e.lean.js" | 1 + ...60\346\240\241\351\252\214.md.eb10cc89.js" | 177 + ...6\240\241\351\252\214.md.eb10cc89.lean.js" | 1 + ...226\271\346\241\210xxl-job.md.5669afc6.js" | 1 + ...71\346\241\210xxl-job.md.5669afc6.lean.js" | 1 + ...37\351\205\215\347\275\256.md.dcf47fae.js" | 55 + ...1\205\215\347\275\256.md.dcf47fae.lean.js" | 1 + ...71\211MybatisPlusGenerator.md.cb2de2f5.js" | 813 +++ ...1MybatisPlusGenerator.md.cb2de2f5.lean.js" | 1 + assets/java_index.md.a76c7870.js | 1 + assets/java_index.md.a76c7870.lean.js | 1 + ...40\350\256\260\345\275\225.md.2b882b43.js" | 1 + ...0\256\260\345\275\225.md.2b882b43.lean.js" | 1 + ...44\350\256\260\345\275\225.md.3a75876e.js" | 1 + ...0\256\260\345\275\225.md.3a75876e.lean.js" | 1 + ...fa\345\256\236\350\267\265.md.012f67f9.js" | 305 + ...5\256\236\350\267\265.md.012f67f9.lean.js" | 1 + ...30\345\244\204\347\220\206.md.e0988449.js" | 1 + ...5\244\204\347\220\206.md.e0988449.lean.js" | 1 + ...\351\227\264\344\273\266MQ.md.68363b78.js" | 59 + ...227\264\344\273\266MQ.md.68363b78.lean.js" | 1 + ...63\346\226\271\346\241\210.md.6bed79a9.js" | 143 + ...6\226\271\346\241\210.md.6bed79a9.lean.js" | 1 + ...63\345\212\236\346\263\225.md.249c1c70.js" | 1 + ...5\212\236\346\263\225.md.249c1c70.lean.js" | 1 + ...235\221-zip file is closed.md.0fc74901.js" | 15 + ...21-zip file is closed.md.0fc74901.lean.js" | 1 + ...45\346\200\235\350\267\257.md.e4a204f9.js" | 19 + ...6\200\235\350\267\257.md.e4a204f9.lean.js" | 1 + ...30\350\256\260\345\275\225.md.f55418cc.js" | 129 + ...0\256\260\345\275\225.md.f55418cc.lean.js" | 1 + ...ce\350\270\251\345\235\221.md.aab490ac.js" | 1 + ...0\270\251\345\235\221.md.aab490ac.lean.js" | 1 + ...26\351\227\256\351\242\230.md.6a44f13e.js" | 1 + ...1\227\256\351\242\230.md.6a44f13e.lean.js" | 1 + ...al\347\232\204\345\235\221.md.98a4b036.js" | 69 + ...7\232\204\345\235\221.md.98a4b036.lean.js" | 1 + ...45\345\205\267\347\261\273.md.41297ec5.js" | 131 + ...5\205\267\347\261\273.md.41297ec5.lean.js" | 1 + ...al\351\203\250\347\275\262.md.c251b94e.js" | 637 +++ ...1\203\250\347\275\262.md.c251b94e.lean.js" | 1 + .../linux_applications_Clash.md.57ceb8e9.js | 659 +++ ...nux_applications_Clash.md.57ceb8e9.lean.js | 1 + ...eg\347\233\270\345\205\263.md.fa57cc0e.js" | 1 + ...7\233\270\345\205\263.md.fa57cc0e.lean.js" | 1 + .../linux_applications_Grafana.md.27b9670d.js | 35 + ...x_applications_Grafana.md.27b9670d.lean.js | 1 + ...75\277\347\224\250selenium.md.918cfe65.js" | 37 + ...7\347\224\250selenium.md.918cfe65.lean.js" | 1 + ...nx\351\205\215\347\275\256.md.cec7f5cb.js" | 79 + ...1\205\215\347\275\256.md.cec7f5cb.lean.js" | 1 + .../linux_applications_Rclone.md.b98100d3.js | 327 ++ ...ux_applications_Rclone.md.b98100d3.lean.js | 1 + ...linux_applications_iptables.md.4a6d73d9.js | 11 + ..._applications_iptables.md.4a6d73d9.lean.js | 1 + ...66\347\224\250\346\263\225.md.7103b3aa.js" | 13 + ...7\224\250\346\263\225.md.7103b3aa.lean.js" | 1 + ...ux\345\256\211\350\243\205.md.991f6f32.js" | 81 + ...5\256\211\350\243\205.md.991f6f32.lean.js" | 1 + ...n3\347\216\257\345\242\203.md.92d82d76.js" | 21 + ...7\216\257\345\242\203.md.92d82d76.lean.js" | 1 + ...50\346\214\207\344\273\244.md.d450f692.js" | 21 + ...6\214\207\344\273\244.md.d450f692.lean.js" | 1 + ...04\351\205\215\347\275\256.md.2e59da1d.js" | 3 + ...1\205\215\347\275\256.md.2e59da1d.lean.js" | 1 + ...\272server refused our key.md.23bf3202.js" | 15 + ...erver refused our key.md.23bf3202.lean.js" | 1 + ...ap\347\251\272\351\227\264.md.84a44d7f.js" | 1 + ...7\251\272\351\227\264.md.84a44d7f.lean.js" | 1 + ...345\210\266\344\271\213ACL.md.66efcc69.js" | 1 + ...10\266\344\271\213ACL.md.66efcc69.lean.js" | 1 + assets/linux_env_Ubuntu-server.md.b53d8eca.js | 243 + ...inux_env_Ubuntu-server.md.b53d8eca.lean.js | 1 + ...53\346\215\267\351\224\256.md.7a89f167.js" | 1 + ...6\215\267\351\224\256.md.7a89f167.lean.js" | 1 + ...31\345\221\275\344\273\244.md.15b22e4a.js" | 5 + ...5\221\275\344\273\244.md.15b22e4a.lean.js" | 1 + ...72\347\216\257\345\242\203.md.c536b8a7.js" | 489 ++ ...7\216\257\345\242\203.md.c536b8a7.lean.js" | 1 + ...01\347\231\273\345\275\225.md.8b926ddc.js" | 33 + ...7\231\273\345\275\225.md.8b926ddc.lean.js" | 1 + ...0\215(ln\343\200\201alias).md.4f5e25d9.js" | 1 + ...(ln\343\200\201alias).md.4f5e25d9.lean.js" | 1 + ...34\347\233\270\345\205\263.md.0fc149c9.js" | 65 + ...7\233\270\345\205\263.md.0fc149c9.lean.js" | 1 + assets/linux_index.md.2f33c5bb.js | 1 + assets/linux_index.md.2f33c5bb.lean.js | 1 + assets/python_base_Poetry.md.86dc5eff.js | 17 + assets/python_base_Poetry.md.86dc5eff.lean.js | 1 + ...00\350\257\255\346\263\225.md.2ec54814.js" | 751 +++ ...0\257\255\346\263\225.md.2ec54814.lean.js" | 1 + ...21\347\274\226\347\250\213.md.d5a5de0a.js" | 1023 ++++ ...7\274\226\347\250\213.md.d5a5de0a.lean.js" | 1 + ...50\346\267\261\345\205\245.md.6df2bfe7.js" | 477 ++ ...6\267\261\345\205\245.md.6df2bfe7.lean.js" | 1 + ...31\347\210\254\345\217\226.md.914da3d0.js" | 229 + ...7\210\254\345\217\226.md.914da3d0.lean.js" | 1 + ...li\350\247\206\351\242\221.md.89478ae1.js" | 133 + ...0\247\206\351\242\221.md.89478ae1.lean.js" | 1 + ...27\345\205\245\351\227\250.md.5ba42074.js" | 473 ++ ...5\205\245\351\227\250.md.5ba42074.lean.js" | 1 + ...66\345\205\245\351\227\250.md.74ea41f3.js" | 281 + ...5\205\245\351\227\250.md.74ea41f3.lean.js" | 1 + ...py\350\277\233\351\230\266.md.9a37e129.js" | 473 ++ ...0\277\233\351\230\266.md.9a37e129.lean.js" | 1 + ...um\346\250\241\345\235\227.md.5446e227.js" | 375 ++ ...6\250\241\345\235\227.md.5446e227.lean.js" | 1 + ...17\347\210\254\350\231\253.md.1722f14e.js" | 15 + ...7\210\254\350\231\253.md.1722f14e.lean.js" | 1 + ...11\344\275\234\345\223\201.md.6ec9ffd2.js" | 635 +++ ...4\275\234\345\223\201.md.6ec9ffd2.lean.js" | 1 + ...50\350\247\206\351\242\221.md.0cea7349.js" | 151 + ...0\247\206\351\242\221.md.0cea7349.lean.js" | 1 + ...46\347\210\254\350\231\253.md.ef9e7902.js" | 1 + ...7\210\254\350\231\253.md.ef9e7902.lean.js" | 1 + ...37\347\231\273\345\275\225.md.042432f0.js" | 247 + ...7\231\273\345\275\225.md.042432f0.lean.js" | 1 + assets/python_index.md.700873bd.js | 1 + assets/python_index.md.700873bd.lean.js | 1 + ...32\346\226\207\344\273\266.md.056bae34.js" | 43 + ...6\226\207\344\273\266.md.056bae34.lean.js" | 1 + ...27\345\205\245\351\227\250.md.9f5f1e90.js" | 359 ++ ...5\205\245\351\227\250.md.9f5f1e90.lean.js" | 1 + ...ython_others_youtube-upload.md.3d98cb59.js | 381 ++ ..._others_youtube-upload.md.3d98cb59.lean.js | 1 + ...56\345\244\204\347\220\206.md.fa389c23.js" | 31 + ...5\244\204\347\220\206.md.fa389c23.lean.js" | 1 + ...56\345\274\200\345\205\263.md.88db6435.js" | 29 + ...5\274\200\345\205\263.md.88db6435.lean.js" | 1 + ...60\346\225\260\346\215\256.md.3c80a264.js" | 195 + ...6\225\260\346\215\256.md.3c80a264.lean.js" | 1 + ...go\345\205\245\351\227\250.md.7b29ab40.js" | 111 + ...5\205\245\351\227\250.md.7b29ab40.lean.js" | 1 + ...ql\344\275\277\347\224\250.md.b054a10e.js" | 113 + ...4\275\277\347\224\250.md.b054a10e.lean.js" | 1 + assets/style.cb20272f.css | 1 + assets/tinker_index.md.b79e9a67.js | 1 + assets/tinker_index.md.b79e9a67.lean.js | 1 + ...21\347\251\277\351\200\217.md.7473690d.js" | 89 + ...7\251\277\351\200\217.md.7473690d.lean.js" | 1 + ...er\346\220\255\345\273\272.md.862c471d.js" | 1373 +++++ ...6\220\255\345\273\272.md.862c471d.lean.js" | 1 + ...12\351\205\215\347\275\256.md.55152baa.js" | 23 + ...1\205\215\347\275\256.md.55152baa.lean.js" | 1 + ...45\274\200\345\220\257ipv6.md.edcbc94f.js" | 1 + ...4\200\345\220\257ipv6.md.edcbc94f.lean.js" | 1 + ...75\345\205\245\351\227\250.md.0f6b065f.js" | 1 + ...5\205\245\351\227\250.md.0f6b065f.lean.js" | 1 + ...34\345\224\244\351\206\222.md.70e29fdd.js" | 49 + ...5\224\244\351\206\222.md.70e29fdd.lean.js" | 1 + ...30\345\244\204\347\220\206.md.3ad32a6e.js" | 1 + ...5\244\204\347\220\206.md.3ad32a6e.lean.js" | 1 + ...21\346\234\215\345\212\241.md.43b6af56.js" | 57 + ...6\234\215\345\212\241.md.43b6af56.lean.js" | 1 + ...50\345\205\263\346\234\272.md.a93a1b0f.js" | 55 + ...5\205\263\346\234\272.md.a93a1b0f.lean.js" | 1 + ...45\346\250\241\345\274\217.md.dd786d69.js" | 1 + ...6\250\241\345\274\217.md.dd786d69.lean.js" | 1 + ...45\345\244\204\347\220\206.md.ad24a8cd.js" | 1 + ...5\244\204\347\220\206.md.ad24a8cd.lean.js" | 1 + ...45\346\250\241\345\274\217.md.86cddc5f.js" | 1 + ...6\250\241\345\274\217.md.86cddc5f.lean.js" | 1 + ...256\211\350\243\205truenas.md.b6416835.js" | 13 + ...11\350\243\205truenas.md.b6416835.lean.js" | 1 + .../Dockerfile\350\257\255\346\263\225.html" | 125 + ...cker-compose\350\257\255\346\263\225.html" | 119 + ...5\221\347\273\234\344\271\213macvlan.html" | 67 + docker/index.html | 25 + ...\347\224\250\346\214\207\344\273\244.html" | 35 + favicon.ico | Bin 0 -> 15406 bytes frontend/base/CSS.html | 415 ++ frontend/base/HTML.html | 25 + frontend/base/JavaScript.html | 1485 +++++ frontend/base/Nodejs.html | 53 + frontend/base/Typescript.html | 1395 +++++ frontend/base/Webpack.html | 165 + frontend/base/jQuery.html | 139 + frontend/framework/Vue.html | 947 ++++ frontend/index.html | 25 + ...\347\240\201\345\210\206\346\236\220.html" | 627 ++ frontend/others/HackingWithSwift-1.html | 877 +++ frontend/others/HackingWithSwift-2.html | 1437 +++++ .../SwiftUI\345\205\245\351\227\250.html" | 343 ++ .../swift\350\257\255\346\263\225.html" | 329 ++ golang/base/GoTemplate.html | 415 ++ ...\347\241\200\350\257\255\346\263\225.html" | 5027 +++++++++++++++++ golang/cli/Cobra.html | 393 ++ golang/index.html | 25 + golang/tools/redis-cleaner.html | 167 + golang/web/Gin.html | 515 ++ hashmap.json | 1 + index.html | 25 + ...\347\240\201\345\255\246\344\271\240.html" | 593 ++ ...\346\200\247\345\233\236\351\241\276.html" | 157 + ...\345\205\245\345\255\246\344\271\240.html" | 113 + .../MySql\347\264\242\345\274\225.html" | 47 + ...\351\207\217\345\220\214\346\255\245.html" | 147 + ...\345\214\226\345\255\246\344\271\240.html" | 31 + ...\345\221\275\345\221\250\346\234\237.html" | 25 + ...\351\242\230\350\256\260\345\275\225.html" | 253 + ...onProperties\346\263\250\350\247\243.html" | 31 + ...\351\205\215\345\216\237\347\220\206.html" | 25 + ...\345\261\225\345\216\206\345\217\262.html" | 25 + ...is Generator\351\205\215\347\275\256.html" | 761 +++ ...\344\273\266\346\225\260\346\215\256.html" | 1833 ++++++ ...Cloud Config\346\216\245\345\205\245.html" | 145 + ...\347\240\201\345\210\206\346\236\220.html" | 641 +++ ...\350\275\275\346\263\250\350\247\243.html" | 25 + ...\346\227\266\351\205\215\347\275\256.html" | 143 + ...\350\207\252\345\256\232\344\271\211.html" | 207 + ...\350\256\257\345\212\237\350\203\275.html" | 443 ++ ...6\244session\345\205\261\344\272\253.html" | 127 + ...\347\272\277\346\234\215\345\212\241.html" | 265 + java/framework/swagger+knife4j.html | 401 ++ ...syExcel\345\257\274\345\207\272excel.html" | 99 + ...\346\225\260\346\240\241\351\252\214.html" | 201 + ...6\263\346\226\271\346\241\210xxl-job.html" | 25 + ...\345\237\237\351\205\215\347\275\256.html" | 79 + ...\232\344\271\211MybatisPlusGenerator.html" | 837 +++ java/index.html | 25 + ...\344\271\240\350\256\260\345\275\225.html" | 25 + ...\344\273\244\350\256\260\345\275\225.html" | 25 + .../kakfa\345\256\236\350\267\265.html" | 329 ++ ...\351\242\230\345\244\204\347\220\206.html" | 25 + ...44\270\255\351\227\264\344\273\266MQ.html" | 83 + ...\345\206\263\346\226\271\346\241\210.html" | 167 + ...\345\206\263\345\212\236\346\263\225.html" | 25 + ...0\251\345\235\221-zip file is closed.html" | 39 + ...\346\237\245\346\200\235\350\267\257.html" | 43 + ...\351\242\230\350\256\260\345\275\225.html" | 153 + ...05OpenOffice\350\270\251\345\235\221.html" | 25 + ...\345\217\226\351\227\256\351\242\230.html" | 25 + ...222\214canal\347\232\204\345\235\221.html" | 93 + ...\345\267\245\345\205\267\347\261\273.html" | 155 + .../Canal\351\203\250\347\275\262.html" | 661 +++ linux/applications/Clash.html | 683 +++ .../FFmpeg\347\233\270\345\205\263.html" | 25 + linux/applications/Grafana.html | 59 + ...\255\344\275\277\347\224\250selenium.html" | 61 + .../Nginx\351\205\215\347\275\256.html" | 103 + linux/applications/Rclone.html | 351 ++ linux/applications/iptables.html | 35 + ...\351\230\266\347\224\250\346\263\225.html" | 37 + .../ArchLinux\345\256\211\350\243\205.html" | 105 + ...3\205Python3\347\216\257\345\242\203.html" | 45 + ...\347\224\250\346\214\207\344\273\244.html" | 45 + ...\345\260\204\351\205\215\347\275\256.html" | 27 + ...20\347\244\272server refused our key.html" | 39 + ...\275\256swap\347\251\272\351\227\264.html" | 25 + ...6\216\247\345\210\266\344\271\213ACL.html" | 25 + linux/env/Ubuntu-server.html | 267 + ...\345\277\253\346\215\267\351\224\256.html" | 25 + ...\345\242\231\345\221\275\344\273\244.html" | 29 + ...\346\234\272\347\216\257\345\242\203.html" | 513 ++ ...\347\240\201\347\231\273\345\275\225.html" | 57 + ...253\345\220\215(ln\343\200\201alias).html" | 25 + ...\344\275\234\347\233\270\345\205\263.html" | 89 + linux/index.html | 25 + logo.png | Bin 0 -> 104890 bytes python/base/Poetry.html | 41 + ...\347\241\200\350\257\255\346\263\225.html" | 775 +++ ...\345\217\221\347\274\226\347\250\213.html" | 1047 ++++ ...\345\231\250\346\267\261\345\205\245.html" | 501 ++ ...\347\253\231\347\210\254\345\217\226.html" | 253 + ...\275bilibili\350\247\206\351\242\221.html" | 157 + ...\345\235\227\345\205\245\351\227\250.html" | 497 ++ ...\346\236\266\345\205\245\351\227\250.html" | 305 + .../scrapy\350\277\233\351\230\266.html" | 497 ++ .../selenium\346\250\241\345\235\227.html" | 399 ++ ...\345\274\217\347\210\254\350\231\253.html" | 39 + ...\346\234\211\344\275\234\345\223\201.html" | 659 +++ ...\351\227\250\350\247\206\351\242\221.html" | 175 + ...\344\271\246\347\210\254\350\231\253.html" | 25 + ...\346\213\237\347\231\273\345\275\225.html" | 271 + python/index.html | 25 + ...\345\256\232\346\226\207\344\273\266.html" | 67 + ...\345\235\227\345\205\245\351\227\250.html" | 383 ++ python/others/youtube-upload.html | 405 ++ ...\346\215\256\345\244\204\347\220\206.html" | 55 + ...\347\275\256\345\274\200\345\205\263.html" | 53 + ...\346\226\260\346\225\260\346\215\256.html" | 219 + .../web/Django\345\205\245\351\227\250.html" | 135 + .../web/pymysql\344\275\277\347\224\250.html" | 137 + tinker/index.html | 25 + ...\347\275\221\347\251\277\351\200\217.html" | 113 + .../home server\346\220\255\345\273\272.html" | 1397 +++++ ...\345\217\212\351\205\215\347\275\256.html" | 47 + .../openwrt\345\274\200\345\220\257ipv6.html" | 25 + ...\350\275\275\345\205\245\351\227\250.html" | 25 + ...\347\273\234\345\224\244\351\206\222.html" | 73 + ...\351\242\230\345\244\204\347\220\206.html" | 25 + ...\347\275\221\346\234\215\345\212\241.html" | 81 + ...\345\205\250\345\205\263\346\234\272.html" | 79 + ...\346\216\245\346\250\241\345\274\217.html" | 25 + ...\346\237\245\345\244\204\347\220\206.html" | 25 + ...\346\216\245\346\250\241\345\274\217.html" | 25 + ...50PVE\345\256\211\350\243\205truenas.html" | 37 + 486 files changed, 76459 insertions(+) create mode 100644 404.html create mode 100644 CNAME create mode 100644 "actions/designpattern/\347\255\226\347\225\245\346\250\241\345\274\217\347\232\204\345\205\267\344\275\223\345\256\236\347\216\260.html" create mode 100644 "actions/designpattern/\350\264\243\344\273\273\351\223\276\346\250\241\345\274\217.html" create mode 100644 "actions/env/Docker\345\256\271\345\231\250\345\206\205\350\256\277\351\227\256MacOS\345\256\277\344\270\273\346\234\272\344\270\255\347\232\204kafka.html" create mode 100644 "actions/env/Windows\344\270\213Docker Desktop\345\256\211\350\243\205.html" create mode 100644 "actions/env/Windows\345\274\200\346\234\272\345\220\257\345\212\250WSL.html" create mode 100644 "actions/env/docker+jenkins+gitee\350\207\252\345\212\250\345\214\226\351\203\250\347\275\262vue\351\241\271\347\233\256.html" create mode 100644 "actions/env/git\351\205\215\347\275\256socks5\344\273\243\347\220\206\350\247\243\345\206\263github\344\270\212down\344\273\243\347\240\201\346\205\242\347\232\204\351\227\256\351\242\230.html" create mode 100644 "actions/env/git\351\205\215\347\275\256\345\244\232ssh-key && Gitee \345\222\214 Github \345\220\214\346\255\245\346\233\264\346\226\260.html" create mode 100644 "actions/env/macOS\345\274\200\345\220\257\347\273\210\347\253\257\347\232\204\344\273\243\347\220\206.html" create mode 100644 "actions/env/macos\345\274\200\346\234\272\350\207\252\345\212\250\346\211\247\350\241\214\350\204\232\346\234\254.html" create mode 100644 "actions/env/mysql\345\220\257\345\212\250\346\212\245\351\224\231\346\216\222\346\237\245\345\217\212\345\244\204\347\220\206.html" create mode 100644 "actions/env/\344\275\277\347\224\250github actions\350\277\233\350\241\214\346\214\201\347\273\255\351\203\250\347\275\262.html" create mode 100644 actions/index.html create mode 100644 "actions/tools/Markdown\345\237\272\347\241\200\350\257\255\346\263\225.html" create mode 100644 actions/tools/Sketch.html create mode 100644 "actions/tools/Typora\343\200\201PicGo\343\200\201\344\270\203\347\211\233\344\272\221\345\256\236\347\216\260markdown\345\233\276\347\211\207\350\207\252\345\212\250\344\270\212\344\274\240\345\233\276\345\272\212.html" create mode 100644 "actions/tools/book-searcher\347\224\265\345\255\220\344\271\246\351\225\234\345\203\217.html" create mode 100644 "actions/tools/git\345\221\275\344\273\244\346\225\264\347\220\206.html" create mode 100644 "actions/tools/iterm2\351\205\215\345\220\210oh-my-zsh\351\205\215\347\275\256\344\270\252\346\200\247\344\270\273\351\242\230\347\273\210\347\253\257.html" create mode 100644 "actions/tools/iterm2\351\205\215\347\275\256ssh\345\277\253\351\200\237\350\277\236\346\216\245.html" create mode 100644 "actions/tools/linux\350\256\276\347\275\256macOS\346\227\266\351\227\264\346\234\272\345\231\250server.html" create mode 100644 "actions/tools/powershell\347\276\216\345\214\226.html" create mode 100644 "actions/tools/\345\220\204\347\263\273\347\273\237\344\270\213\346\240\241\351\252\214\346\226\207\344\273\266\344\270\200\350\207\264\346\200\247.html" create mode 100644 "actions/tools/\345\223\252\345\220\222\346\216\242\351\222\210\351\241\265\351\235\242\347\276\216\345\214\226.html" create mode 100644 "assets/actions_designpattern_\347\255\226\347\225\245\346\250\241\345\274\217\347\232\204\345\205\267\344\275\223\345\256\236\347\216\260.md.b828e5be.js" create mode 100644 "assets/actions_designpattern_\347\255\226\347\225\245\346\250\241\345\274\217\347\232\204\345\205\267\344\275\223\345\256\236\347\216\260.md.b828e5be.lean.js" create mode 100644 "assets/actions_designpattern_\350\264\243\344\273\273\351\223\276\346\250\241\345\274\217.md.952322e1.js" create mode 100644 "assets/actions_designpattern_\350\264\243\344\273\273\351\223\276\346\250\241\345\274\217.md.952322e1.lean.js" create mode 100644 "assets/actions_env_Docker\345\256\271\345\231\250\345\206\205\350\256\277\351\227\256MacOS\345\256\277\344\270\273\346\234\272\344\270\255\347\232\204kafka.md.375de9c2.js" create mode 100644 "assets/actions_env_Docker\345\256\271\345\231\250\345\206\205\350\256\277\351\227\256MacOS\345\256\277\344\270\273\346\234\272\344\270\255\347\232\204kafka.md.375de9c2.lean.js" create mode 100644 "assets/actions_env_Windows\344\270\213Docker Desktop\345\256\211\350\243\205.md.2d631526.js" create mode 100644 "assets/actions_env_Windows\344\270\213Docker Desktop\345\256\211\350\243\205.md.2d631526.lean.js" create mode 100644 "assets/actions_env_Windows\345\274\200\346\234\272\345\220\257\345\212\250WSL.md.2ecc3382.js" create mode 100644 "assets/actions_env_Windows\345\274\200\346\234\272\345\220\257\345\212\250WSL.md.2ecc3382.lean.js" create mode 100644 "assets/actions_env_docker_jenkins_gitee\350\207\252\345\212\250\345\214\226\351\203\250\347\275\262vue\351\241\271\347\233\256.md.b6d298c3.js" create mode 100644 "assets/actions_env_docker_jenkins_gitee\350\207\252\345\212\250\345\214\226\351\203\250\347\275\262vue\351\241\271\347\233\256.md.b6d298c3.lean.js" create mode 100644 "assets/actions_env_git\351\205\215\347\275\256socks5\344\273\243\347\220\206\350\247\243\345\206\263github\344\270\212down\344\273\243\347\240\201\346\205\242\347\232\204\351\227\256\351\242\230.md.bfa9056c.js" create mode 100644 "assets/actions_env_git\351\205\215\347\275\256socks5\344\273\243\347\220\206\350\247\243\345\206\263github\344\270\212down\344\273\243\347\240\201\346\205\242\347\232\204\351\227\256\351\242\230.md.bfa9056c.lean.js" create mode 100644 "assets/actions_env_git\351\205\215\347\275\256\345\244\232ssh-key __ Gitee \345\222\214 Github \345\220\214\346\255\245\346\233\264\346\226\260.md.e089c9d0.js" create mode 100644 "assets/actions_env_git\351\205\215\347\275\256\345\244\232ssh-key __ Gitee \345\222\214 Github \345\220\214\346\255\245\346\233\264\346\226\260.md.e089c9d0.lean.js" create mode 100644 "assets/actions_env_macOS\345\274\200\345\220\257\347\273\210\347\253\257\347\232\204\344\273\243\347\220\206.md.323e2f5e.js" create mode 100644 "assets/actions_env_macOS\345\274\200\345\220\257\347\273\210\347\253\257\347\232\204\344\273\243\347\220\206.md.323e2f5e.lean.js" create mode 100644 "assets/actions_env_macos\345\274\200\346\234\272\350\207\252\345\212\250\346\211\247\350\241\214\350\204\232\346\234\254.md.6cf6a3a7.js" create mode 100644 "assets/actions_env_macos\345\274\200\346\234\272\350\207\252\345\212\250\346\211\247\350\241\214\350\204\232\346\234\254.md.6cf6a3a7.lean.js" create mode 100644 "assets/actions_env_mysql\345\220\257\345\212\250\346\212\245\351\224\231\346\216\222\346\237\245\345\217\212\345\244\204\347\220\206.md.e9f9fd1d.js" create mode 100644 "assets/actions_env_mysql\345\220\257\345\212\250\346\212\245\351\224\231\346\216\222\346\237\245\345\217\212\345\244\204\347\220\206.md.e9f9fd1d.lean.js" create mode 100644 "assets/actions_env_\344\275\277\347\224\250github actions\350\277\233\350\241\214\346\214\201\347\273\255\351\203\250\347\275\262.md.01116c37.js" create mode 100644 "assets/actions_env_\344\275\277\347\224\250github actions\350\277\233\350\241\214\346\214\201\347\273\255\351\203\250\347\275\262.md.01116c37.lean.js" create mode 100644 assets/actions_index.md.26d63a13.js create mode 100644 assets/actions_index.md.26d63a13.lean.js create mode 100644 "assets/actions_tools_Markdown\345\237\272\347\241\200\350\257\255\346\263\225.md.9b7f7a0a.js" create mode 100644 "assets/actions_tools_Markdown\345\237\272\347\241\200\350\257\255\346\263\225.md.9b7f7a0a.lean.js" create mode 100644 assets/actions_tools_Sketch.md.b25b7913.js create mode 100644 assets/actions_tools_Sketch.md.b25b7913.lean.js create mode 100644 "assets/actions_tools_Typora\343\200\201PicGo\343\200\201\344\270\203\347\211\233\344\272\221\345\256\236\347\216\260markdown\345\233\276\347\211\207\350\207\252\345\212\250\344\270\212\344\274\240\345\233\276\345\272\212.md.6002b22d.js" create mode 100644 "assets/actions_tools_Typora\343\200\201PicGo\343\200\201\344\270\203\347\211\233\344\272\221\345\256\236\347\216\260markdown\345\233\276\347\211\207\350\207\252\345\212\250\344\270\212\344\274\240\345\233\276\345\272\212.md.6002b22d.lean.js" create mode 100644 "assets/actions_tools_book-searcher\347\224\265\345\255\220\344\271\246\351\225\234\345\203\217.md.c0cc3ed0.js" create mode 100644 "assets/actions_tools_book-searcher\347\224\265\345\255\220\344\271\246\351\225\234\345\203\217.md.c0cc3ed0.lean.js" create mode 100644 "assets/actions_tools_git\345\221\275\344\273\244\346\225\264\347\220\206.md.853e93cd.js" create mode 100644 "assets/actions_tools_git\345\221\275\344\273\244\346\225\264\347\220\206.md.853e93cd.lean.js" create mode 100644 "assets/actions_tools_iterm2\351\205\215\345\220\210oh-my-zsh\351\205\215\347\275\256\344\270\252\346\200\247\344\270\273\351\242\230\347\273\210\347\253\257.md.f87cdce3.js" create mode 100644 "assets/actions_tools_iterm2\351\205\215\345\220\210oh-my-zsh\351\205\215\347\275\256\344\270\252\346\200\247\344\270\273\351\242\230\347\273\210\347\253\257.md.f87cdce3.lean.js" create mode 100644 "assets/actions_tools_iterm2\351\205\215\347\275\256ssh\345\277\253\351\200\237\350\277\236\346\216\245.md.3487ddd6.js" create mode 100644 "assets/actions_tools_iterm2\351\205\215\347\275\256ssh\345\277\253\351\200\237\350\277\236\346\216\245.md.3487ddd6.lean.js" create mode 100644 "assets/actions_tools_linux\350\256\276\347\275\256macOS\346\227\266\351\227\264\346\234\272\345\231\250server.md.3c94d4f3.js" create mode 100644 "assets/actions_tools_linux\350\256\276\347\275\256macOS\346\227\266\351\227\264\346\234\272\345\231\250server.md.3c94d4f3.lean.js" create mode 100644 "assets/actions_tools_powershell\347\276\216\345\214\226.md.d9be26ce.js" create mode 100644 "assets/actions_tools_powershell\347\276\216\345\214\226.md.d9be26ce.lean.js" create mode 100644 "assets/actions_tools_\345\220\204\347\263\273\347\273\237\344\270\213\346\240\241\351\252\214\346\226\207\344\273\266\344\270\200\350\207\264\346\200\247.md.98a2c898.js" create mode 100644 "assets/actions_tools_\345\220\204\347\263\273\347\273\237\344\270\213\346\240\241\351\252\214\346\226\207\344\273\266\344\270\200\350\207\264\346\200\247.md.98a2c898.lean.js" create mode 100644 "assets/actions_tools_\345\223\252\345\220\222\346\216\242\351\222\210\351\241\265\351\235\242\347\276\216\345\214\226.md.852dab4f.js" create mode 100644 "assets/actions_tools_\345\223\252\345\220\222\346\216\242\351\222\210\351\241\265\351\235\242\347\276\216\345\214\226.md.852dab4f.lean.js" create mode 100644 assets/app.058f2afe.js create mode 100644 assets/chunks/VPAlgoliaSearchBox.bbe9601b.js create mode 100644 assets/chunks/framework.dc1f4608.js create mode 100644 assets/chunks/theme.bfdc69c3.js create mode 100644 "assets/docker_Dockerfile\350\257\255\346\263\225.md.7c5d66d7.js" create mode 100644 "assets/docker_Dockerfile\350\257\255\346\263\225.md.7c5d66d7.lean.js" create mode 100644 "assets/docker_docker-compose\350\257\255\346\263\225.md.5b63270f.js" create mode 100644 "assets/docker_docker-compose\350\257\255\346\263\225.md.5b63270f.lean.js" create mode 100644 "assets/docker_docker\347\275\221\347\273\234\344\271\213macvlan.md.15dfe39b.js" create mode 100644 "assets/docker_docker\347\275\221\347\273\234\344\271\213macvlan.md.15dfe39b.lean.js" create mode 100644 assets/docker_index.md.f95bad4d.js create mode 100644 assets/docker_index.md.f95bad4d.lean.js create mode 100644 "assets/docker_\345\270\270\347\224\250\346\214\207\344\273\244.md.6b637e11.js" create mode 100644 "assets/docker_\345\270\270\347\224\250\346\214\207\344\273\244.md.6b637e11.lean.js" create mode 100644 assets/frontend_base_CSS.md.9a8416e3.js create mode 100644 assets/frontend_base_CSS.md.9a8416e3.lean.js create mode 100644 assets/frontend_base_HTML.md.bd02116d.js create mode 100644 assets/frontend_base_HTML.md.bd02116d.lean.js create mode 100644 assets/frontend_base_JavaScript.md.5b22eba4.js create mode 100644 assets/frontend_base_JavaScript.md.5b22eba4.lean.js create mode 100644 assets/frontend_base_Nodejs.md.a3da6ed0.js create mode 100644 assets/frontend_base_Nodejs.md.a3da6ed0.lean.js create mode 100644 assets/frontend_base_Typescript.md.47bd8e1c.js create mode 100644 assets/frontend_base_Typescript.md.47bd8e1c.lean.js create mode 100644 assets/frontend_base_Webpack.md.f49018f3.js create mode 100644 assets/frontend_base_Webpack.md.f49018f3.lean.js create mode 100644 assets/frontend_base_jQuery.md.4056042a.js create mode 100644 assets/frontend_base_jQuery.md.4056042a.lean.js create mode 100644 assets/frontend_framework_Vue.md.98b6e4a2.js create mode 100644 assets/frontend_framework_Vue.md.98b6e4a2.lean.js create mode 100644 assets/frontend_index.md.01cb292c.js create mode 100644 assets/frontend_index.md.01cb292c.lean.js create mode 100644 "assets/frontend_others_ElementPlus el-upload\346\272\220\347\240\201\345\210\206\346\236\220.md.6309dbd0.js" create mode 100644 "assets/frontend_others_ElementPlus el-upload\346\272\220\347\240\201\345\210\206\346\236\220.md.6309dbd0.lean.js" create mode 100644 assets/frontend_others_HackingWithSwift-1.md.355cc977.js create mode 100644 assets/frontend_others_HackingWithSwift-1.md.355cc977.lean.js create mode 100644 assets/frontend_others_HackingWithSwift-2.md.85e4f87a.js create mode 100644 assets/frontend_others_HackingWithSwift-2.md.85e4f87a.lean.js create mode 100644 "assets/frontend_others_SwiftUI\345\205\245\351\227\250.md.f738d450.js" create mode 100644 "assets/frontend_others_SwiftUI\345\205\245\351\227\250.md.f738d450.lean.js" create mode 100644 "assets/frontend_others_swift\350\257\255\346\263\225.md.9c4e5628.js" create mode 100644 "assets/frontend_others_swift\350\257\255\346\263\225.md.9c4e5628.lean.js" create mode 100644 assets/golang_base_GoTemplate.md.26e53b7c.js create mode 100644 assets/golang_base_GoTemplate.md.26e53b7c.lean.js create mode 100644 "assets/golang_base_golang\345\237\272\347\241\200\350\257\255\346\263\225.md.5d06c4a6.js" create mode 100644 "assets/golang_base_golang\345\237\272\347\241\200\350\257\255\346\263\225.md.5d06c4a6.lean.js" create mode 100644 assets/golang_cli_Cobra.md.d15fd694.js create mode 100644 assets/golang_cli_Cobra.md.d15fd694.lean.js create mode 100644 assets/golang_index.md.56aeaf33.js create mode 100644 assets/golang_index.md.56aeaf33.lean.js create mode 100644 assets/golang_tools_redis-cleaner.md.c2849590.js create mode 100644 assets/golang_tools_redis-cleaner.md.c2849590.lean.js create mode 100644 assets/golang_web_Gin.md.a83e3304.js create mode 100644 assets/golang_web_Gin.md.a83e3304.lean.js create mode 100644 assets/index.md.a91419bd.js create mode 100644 assets/index.md.a91419bd.lean.js create mode 100644 assets/inter-italic-cyrillic-ext.33bd5a8e.woff2 create mode 100644 assets/inter-italic-cyrillic.ea42a392.woff2 create mode 100644 assets/inter-italic-greek-ext.4fbe9427.woff2 create mode 100644 assets/inter-italic-greek.8f4463c4.woff2 create mode 100644 assets/inter-italic-latin-ext.bd8920cc.woff2 create mode 100644 assets/inter-italic-latin.bd3b6f56.woff2 create mode 100644 assets/inter-italic-vietnamese.6ce511fb.woff2 create mode 100644 assets/inter-roman-cyrillic-ext.e75737ce.woff2 create mode 100644 assets/inter-roman-cyrillic.5f2c6c8c.woff2 create mode 100644 assets/inter-roman-greek-ext.ab0619bc.woff2 create mode 100644 assets/inter-roman-greek.d5a6d92a.woff2 create mode 100644 assets/inter-roman-latin-ext.0030eebd.woff2 create mode 100644 assets/inter-roman-latin.2ed14f66.woff2 create mode 100644 assets/inter-roman-vietnamese.14ce25a6.woff2 create mode 100644 "assets/java_base_JDK1.8HashMap\346\272\220\347\240\201\345\255\246\344\271\240.md.9d40a87c.js" create mode 100644 "assets/java_base_JDK1.8HashMap\346\272\220\347\240\201\345\255\246\344\271\240.md.9d40a87c.lean.js" create mode 100644 "assets/java_base_Java8\346\226\260\347\211\271\346\200\247\345\233\236\351\241\276.md.771d4e14.js" create mode 100644 "assets/java_base_Java8\346\226\260\347\211\271\346\200\247\345\233\236\351\241\276.md.771d4e14.lean.js" create mode 100644 "assets/java_base_String\347\261\273\347\232\204\346\267\261\345\205\245\345\255\246\344\271\240.md.d1707e3c.js" create mode 100644 "assets/java_base_String\347\261\273\347\232\204\346\267\261\345\205\245\345\255\246\344\271\240.md.d1707e3c.lean.js" create mode 100644 "assets/java_database_MySql\347\264\242\345\274\225.md.46f173cb.js" create mode 100644 "assets/java_database_MySql\347\264\242\345\274\225.md.46f173cb.lean.js" create mode 100644 "assets/java_database_Otter\345\256\236\347\216\260\346\225\260\346\215\256\345\205\250\351\207\217\345\242\236\351\207\217\345\220\214\346\255\245.md.892bc0fe.js" create mode 100644 "assets/java_database_Otter\345\256\236\347\216\260\346\225\260\346\215\256\345\205\250\351\207\217\345\242\236\351\207\217\345\220\214\346\255\245.md.892bc0fe.lean.js" create mode 100644 "assets/java_database_SQL\344\274\230\345\214\226\345\255\246\344\271\240.md.84dffe0e.js" create mode 100644 "assets/java_database_SQL\344\274\230\345\214\226\345\255\246\344\271\240.md.84dffe0e.lean.js" create mode 100644 "assets/java_devtool_Maven\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237.md.e9b078b3.js" create mode 100644 "assets/java_devtool_Maven\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237.md.e9b078b3.lean.js" create mode 100644 "assets/java_devtool_nexus\346\227\240\346\263\225\344\270\213\350\275\275\344\276\235\350\265\226\347\232\204\351\227\256\351\242\230\350\256\260\345\275\225.md.647fa7c0.js" create mode 100644 "assets/java_devtool_nexus\346\227\240\346\263\225\344\270\213\350\275\275\344\276\235\350\265\226\347\232\204\351\227\256\351\242\230\350\256\260\345\275\225.md.647fa7c0.lean.js" create mode 100644 "assets/java_framework_ConfigurationProperties\346\263\250\350\247\243.md.8a9eff27.js" create mode 100644 "assets/java_framework_ConfigurationProperties\346\263\250\350\247\243.md.8a9eff27.lean.js" create mode 100644 "assets/java_framework_JavaSPI\346\234\272\345\210\266\345\222\214Springboot\350\207\252\345\212\250\350\243\205\351\205\215\345\216\237\347\220\206.md.6a5d7c18.js" create mode 100644 "assets/java_framework_JavaSPI\346\234\272\345\210\266\345\222\214Springboot\350\207\252\345\212\250\350\243\205\351\205\215\345\216\237\347\220\206.md.6a5d7c18.lean.js" create mode 100644 "assets/java_framework_Java\346\227\245\345\277\227\345\217\221\345\261\225\345\216\206\345\217\262.md.f427bc3e.js" create mode 100644 "assets/java_framework_Java\346\227\245\345\277\227\345\217\221\345\261\225\345\216\206\345\217\262.md.f427bc3e.lean.js" create mode 100644 "assets/java_framework_Mybatis Generator\351\205\215\347\275\256.md.c35e48c8.js" create mode 100644 "assets/java_framework_Mybatis Generator\351\205\215\347\275\256.md.c35e48c8.lean.js" create mode 100644 "assets/java_framework_POI\344\272\213\344\273\266\346\250\241\345\274\217\350\247\243\346\236\220\345\271\266\350\257\273\345\217\226Excel\346\226\207\344\273\266\346\225\260\346\215\256.md.ecde083a.js" create mode 100644 "assets/java_framework_POI\344\272\213\344\273\266\346\250\241\345\274\217\350\247\243\346\236\220\345\271\266\350\257\273\345\217\226Excel\346\226\207\344\273\266\346\225\260\346\215\256.md.ecde083a.lean.js" create mode 100644 "assets/java_framework_Spring Cloud Config\346\216\245\345\205\245.md.d19b8abd.js" create mode 100644 "assets/java_framework_Spring Cloud Config\346\216\245\345\205\245.md.d19b8abd.lean.js" create mode 100644 "assets/java_framework_SpringMVC\347\232\204\346\211\247\350\241\214\346\265\201\347\250\213\346\272\220\347\240\201\345\210\206\346\236\220.md.ca08bf1a.js" create mode 100644 "assets/java_framework_SpringMVC\347\232\204\346\211\247\350\241\214\346\265\201\347\250\213\346\272\220\347\240\201\345\210\206\346\236\220.md.ca08bf1a.lean.js" create mode 100644 "assets/java_framework_Spring\351\205\215\347\275\256\345\222\214\346\235\241\344\273\266\345\214\226\347\273\204\344\273\266\345\212\240\350\275\275\346\263\250\350\247\243.md.e8c07bfa.js" create mode 100644 "assets/java_framework_Spring\351\205\215\347\275\256\345\222\214\346\235\241\344\273\266\345\214\226\347\273\204\344\273\266\345\212\240\350\275\275\346\263\250\350\247\243.md.e8c07bfa.lean.js" create mode 100644 "assets/java_framework_dubbo\346\216\245\345\217\243\350\266\205\346\227\266\351\205\215\347\275\256.md.ba4117be.js" create mode 100644 "assets/java_framework_dubbo\346\216\245\345\217\243\350\266\205\346\227\266\351\205\215\347\275\256.md.ba4117be.lean.js" create mode 100644 "assets/java_framework_logback\350\207\252\345\256\232\344\271\211.md.9e87748a.js" create mode 100644 "assets/java_framework_logback\350\207\252\345\256\232\344\271\211.md.9e87748a.lean.js" create mode 100644 "assets/java_framework_netty_websocket\345\256\236\347\216\260\345\215\263\346\227\266\351\200\232\350\256\257\345\212\237\350\203\275.md.955a4962.js" create mode 100644 "assets/java_framework_netty_websocket\345\256\236\347\216\260\345\215\263\346\227\266\351\200\232\350\256\257\345\212\237\350\203\275.md.955a4962.lean.js" create mode 100644 "assets/java_framework_spring-session\345\256\236\347\216\260\351\233\206\347\276\244session\345\205\261\344\272\253.md.4d9b54de.js" create mode 100644 "assets/java_framework_spring-session\345\256\236\347\216\260\351\233\206\347\276\244session\345\205\261\344\272\253.md.4d9b54de.lean.js" create mode 100644 "assets/java_framework_springcloud\344\274\230\351\233\205\344\270\213\347\272\277\346\234\215\345\212\241.md.6feadc2c.js" create mode 100644 "assets/java_framework_springcloud\344\274\230\351\233\205\344\270\213\347\272\277\346\234\215\345\212\241.md.6feadc2c.lean.js" create mode 100644 assets/java_framework_swagger_knife4j.md.31965f41.js create mode 100644 assets/java_framework_swagger_knife4j.md.31965f41.lean.js create mode 100644 "assets/java_framework_\344\275\277\347\224\250EasyExcel\345\257\274\345\207\272excel.md.6234940e.js" create mode 100644 "assets/java_framework_\344\275\277\347\224\250EasyExcel\345\257\274\345\207\272excel.md.6234940e.lean.js" create mode 100644 "assets/java_framework_\344\275\277\347\224\250spring validation\350\277\233\350\241\214\345\217\202\346\225\260\346\240\241\351\252\214.md.eb10cc89.js" create mode 100644 "assets/java_framework_\344\275\277\347\224\250spring validation\350\277\233\350\241\214\345\217\202\346\225\260\346\240\241\351\252\214.md.eb10cc89.lean.js" create mode 100644 "assets/java_framework_\345\210\206\345\270\203\345\274\217\345\256\232\346\227\266\344\273\273\345\212\241\350\247\243\345\206\263\346\226\271\346\241\210xxl-job.md.5669afc6.js" create mode 100644 "assets/java_framework_\345\210\206\345\270\203\345\274\217\345\256\232\346\227\266\344\273\273\345\212\241\350\247\243\345\206\263\346\226\271\346\241\210xxl-job.md.5669afc6.lean.js" create mode 100644 "assets/java_framework_\345\220\216\347\253\257\345\205\201\350\256\270\350\267\250\345\237\237\351\205\215\347\275\256.md.dcf47fae.js" create mode 100644 "assets/java_framework_\345\220\216\347\253\257\345\205\201\350\256\270\350\267\250\345\237\237\351\205\215\347\275\256.md.dcf47fae.lean.js" create mode 100644 "assets/java_framework_\350\207\252\345\256\232\344\271\211MybatisPlusGenerator.md.cb2de2f5.js" create mode 100644 "assets/java_framework_\350\207\252\345\256\232\344\271\211MybatisPlusGenerator.md.cb2de2f5.lean.js" create mode 100644 assets/java_index.md.a76c7870.js create mode 100644 assets/java_index.md.a76c7870.lean.js create mode 100644 "assets/java_middleware_kafka\345\255\246\344\271\240\350\256\260\345\275\225.md.2b882b43.js" create mode 100644 "assets/java_middleware_kafka\345\255\246\344\271\240\350\256\260\345\275\225.md.2b882b43.lean.js" create mode 100644 "assets/java_middleware_kafka\345\270\270\347\224\250\345\221\275\344\273\244\350\256\260\345\275\225.md.3a75876e.js" create mode 100644 "assets/java_middleware_kafka\345\270\270\347\224\250\345\221\275\344\273\244\350\256\260\345\275\225.md.3a75876e.lean.js" create mode 100644 "assets/java_middleware_kakfa\345\256\236\350\267\265.md.012f67f9.js" create mode 100644 "assets/java_middleware_kakfa\345\256\236\350\267\265.md.012f67f9.lean.js" create mode 100644 "assets/java_middleware_kakfa\351\207\215\345\244\215\346\266\210\350\264\271\351\227\256\351\242\230\345\244\204\347\220\206.md.e0988449.js" create mode 100644 "assets/java_middleware_kakfa\351\207\215\345\244\215\346\266\210\350\264\271\351\227\256\351\242\230\345\244\204\347\220\206.md.e0988449.lean.js" create mode 100644 "assets/java_middleware_\345\205\263\344\272\216\346\266\210\346\201\257\344\270\255\351\227\264\344\273\266MQ.md.68363b78.js" create mode 100644 "assets/java_middleware_\345\205\263\344\272\216\346\266\210\346\201\257\344\270\255\351\227\264\344\273\266MQ.md.68363b78.lean.js" create mode 100644 "assets/java_middleware_\345\210\206\345\270\203\345\274\217\351\224\201\350\247\243\345\206\263\346\226\271\346\241\210.md.6bed79a9.js" create mode 100644 "assets/java_middleware_\345\210\206\345\270\203\345\274\217\351\224\201\350\247\243\345\206\263\346\226\271\346\241\210.md.6bed79a9.lean.js" create mode 100644 "assets/java_others_OpenJDK\346\262\241\346\234\211jstack\347\255\211\345\221\275\344\273\244\347\232\204\350\247\243\345\206\263\345\212\236\346\263\225.md.249c1c70.js" create mode 100644 "assets/java_others_OpenJDK\346\262\241\346\234\211jstack\347\255\211\345\221\275\344\273\244\347\232\204\350\247\243\345\206\263\345\212\236\346\263\225.md.249c1c70.lean.js" create mode 100644 "assets/java_others_POI\350\270\251\345\235\221-zip file is closed.md.0fc74901.js" create mode 100644 "assets/java_others_POI\350\270\251\345\235\221-zip file is closed.md.0fc74901.lean.js" create mode 100644 "assets/java_others_cpu\345\215\240\347\224\250\347\216\207\351\253\230\346\216\222\346\237\245\346\200\235\350\267\257.md.e4a204f9.js" create mode 100644 "assets/java_others_cpu\345\215\240\347\224\250\347\216\207\351\253\230\346\216\222\346\237\245\346\200\235\350\267\257.md.e4a204f9.lean.js" create mode 100644 "assets/java_others_feign\350\257\267\346\261\202\345\257\274\350\207\264\347\232\204\347\224\250\346\210\267ip\350\216\267\345\217\226\351\227\256\351\242\230\350\256\260\345\275\225.md.f55418cc.js" create mode 100644 "assets/java_others_feign\350\257\267\346\261\202\345\257\274\350\207\264\347\232\204\347\224\250\346\210\267ip\350\216\267\345\217\226\351\227\256\351\242\230\350\256\260\345\275\225.md.f55418cc.lean.js" create mode 100644 "assets/java_others_linux\346\234\215\345\212\241\345\231\250\345\256\211\350\243\205OpenOffice\350\270\251\345\235\221.md.aab490ac.js" create mode 100644 "assets/java_others_linux\346\234\215\345\212\241\345\231\250\345\256\211\350\243\205OpenOffice\350\270\251\345\235\221.md.aab490ac.lean.js" create mode 100644 "assets/java_others_mybatis\347\232\204classpath\351\205\215\347\275\256\345\257\274\350\207\264\347\232\204jar\345\214\205\350\257\273\345\217\226\351\227\256\351\242\230.md.6a44f13e.js" create mode 100644 "assets/java_others_mybatis\347\232\204classpath\351\205\215\347\275\256\345\257\274\350\207\264\347\232\204jar\345\214\205\350\257\273\345\217\226\351\227\256\351\242\230.md.6a44f13e.lean.js" create mode 100644 "assets/java_others_ribbon\345\210\267\346\226\260\346\234\215\345\212\241\345\210\227\350\241\250\351\227\264\351\232\224\345\222\214canal\347\232\204\345\235\221.md.98a4b036.js" create mode 100644 "assets/java_others_ribbon\345\210\267\346\226\260\346\234\215\345\212\241\345\210\227\350\241\250\351\227\264\351\232\224\345\222\214canal\347\232\204\345\235\221.md.98a4b036.lean.js" create mode 100644 "assets/java_others_\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217\345\212\240\345\257\206\346\225\260\346\215\256\345\257\271\347\247\260\350\247\243\345\257\206\345\267\245\345\205\267\347\261\273.md.41297ec5.js" create mode 100644 "assets/java_others_\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217\345\212\240\345\257\206\346\225\260\346\215\256\345\257\271\347\247\260\350\247\243\345\257\206\345\267\245\345\205\267\347\261\273.md.41297ec5.lean.js" create mode 100644 "assets/linux_applications_Canal\351\203\250\347\275\262.md.c251b94e.js" create mode 100644 "assets/linux_applications_Canal\351\203\250\347\275\262.md.c251b94e.lean.js" create mode 100644 assets/linux_applications_Clash.md.57ceb8e9.js create mode 100644 assets/linux_applications_Clash.md.57ceb8e9.lean.js create mode 100644 "assets/linux_applications_FFmpeg\347\233\270\345\205\263.md.fa57cc0e.js" create mode 100644 "assets/linux_applications_FFmpeg\347\233\270\345\205\263.md.fa57cc0e.lean.js" create mode 100644 assets/linux_applications_Grafana.md.27b9670d.js create mode 100644 assets/linux_applications_Grafana.md.27b9670d.lean.js create mode 100644 "assets/linux_applications_Linux\344\270\255\344\275\277\347\224\250selenium.md.918cfe65.js" create mode 100644 "assets/linux_applications_Linux\344\270\255\344\275\277\347\224\250selenium.md.918cfe65.lean.js" create mode 100644 "assets/linux_applications_Nginx\351\205\215\347\275\256.md.cec7f5cb.js" create mode 100644 "assets/linux_applications_Nginx\351\205\215\347\275\256.md.cec7f5cb.lean.js" create mode 100644 assets/linux_applications_Rclone.md.b98100d3.js create mode 100644 assets/linux_applications_Rclone.md.b98100d3.lean.js create mode 100644 assets/linux_applications_iptables.md.4a6d73d9.js create mode 100644 assets/linux_applications_iptables.md.4a6d73d9.lean.js create mode 100644 "assets/linux_applications_screen\347\232\204\350\277\233\351\230\266\347\224\250\346\263\225.md.7103b3aa.js" create mode 100644 "assets/linux_applications_screen\347\232\204\350\277\233\351\230\266\347\224\250\346\263\225.md.7103b3aa.lean.js" create mode 100644 "assets/linux_env_ArchLinux\345\256\211\350\243\205.md.991f6f32.js" create mode 100644 "assets/linux_env_ArchLinux\345\256\211\350\243\205.md.991f6f32.lean.js" create mode 100644 "assets/linux_env_Centos7\345\256\211\350\243\205Python3\347\216\257\345\242\203.md.92d82d76.js" create mode 100644 "assets/linux_env_Centos7\345\256\211\350\243\205Python3\347\216\257\345\242\203.md.92d82d76.lean.js" create mode 100644 "assets/linux_env_Linux\345\270\270\347\224\250\346\214\207\344\273\244.md.d450f692.js" create mode 100644 "assets/linux_env_Linux\345\270\270\347\224\250\346\214\207\344\273\244.md.d450f692.lean.js" create mode 100644 "assets/linux_env_Linux\346\234\215\345\212\241\345\231\250\346\226\207\344\273\266\347\233\256\345\275\225\345\205\261\344\272\253\346\230\240\345\260\204\351\205\215\347\275\256.md.2e59da1d.js" create mode 100644 "assets/linux_env_Linux\346\234\215\345\212\241\345\231\250\346\226\207\344\273\266\347\233\256\345\275\225\345\205\261\344\272\253\346\230\240\345\260\204\351\205\215\347\275\256.md.2e59da1d.lean.js" create mode 100644 "assets/linux_env_Linux\347\247\201\351\222\245\347\231\273\351\231\206\346\217\220\347\244\272server refused our key.md.23bf3202.js" create mode 100644 "assets/linux_env_Linux\347\247\201\351\222\245\347\231\273\351\231\206\346\217\220\347\244\272server refused our key.md.23bf3202.lean.js" create mode 100644 "assets/linux_env_Linux\350\256\276\347\275\256swap\347\251\272\351\227\264.md.84a44d7f.js" create mode 100644 "assets/linux_env_Linux\350\256\276\347\275\256swap\347\251\272\351\227\264.md.84a44d7f.lean.js" create mode 100644 "assets/linux_env_Linux\350\256\277\351\227\256\346\235\203\351\231\220\346\216\247\345\210\266\344\271\213ACL.md.66efcc69.js" create mode 100644 "assets/linux_env_Linux\350\256\277\351\227\256\346\235\203\351\231\220\346\216\247\345\210\266\344\271\213ACL.md.66efcc69.lean.js" create mode 100644 assets/linux_env_Ubuntu-server.md.b53d8eca.js create mode 100644 assets/linux_env_Ubuntu-server.md.b53d8eca.lean.js create mode 100644 "assets/linux_env_bash\345\270\270\347\224\250\347\232\204\345\277\253\346\215\267\351\224\256.md.7a89f167.js" create mode 100644 "assets/linux_env_bash\345\270\270\347\224\250\347\232\204\345\277\253\346\215\267\351\224\256.md.7a89f167.lean.js" create mode 100644 "assets/linux_env_centos7\351\230\262\347\201\253\345\242\231\345\221\275\344\273\244.md.15b22e4a.js" create mode 100644 "assets/linux_env_centos7\351\230\262\347\201\253\345\242\231\345\221\275\344\273\244.md.15b22e4a.lean.js" create mode 100644 "assets/linux_env_\344\273\216\351\233\266\346\220\255\345\273\272Linux\350\231\232\346\213\237\346\234\272\347\216\257\345\242\203.md.c536b8a7.js" create mode 100644 "assets/linux_env_\344\273\216\351\233\266\346\220\255\345\273\272Linux\350\231\232\346\213\237\346\234\272\347\216\257\345\242\203.md.c536b8a7.lean.js" create mode 100644 "assets/linux_env_\346\234\215\345\212\241\345\231\250\345\220\257\347\224\250ssh\345\257\206\351\222\245\347\231\273\345\275\225\345\271\266\347\246\201\347\224\250\345\257\206\347\240\201\347\231\273\345\275\225.md.8b926ddc.js" create mode 100644 "assets/linux_env_\346\234\215\345\212\241\345\231\250\345\220\257\347\224\250ssh\345\257\206\351\222\245\347\231\273\345\275\225\345\271\266\347\246\201\347\224\250\345\257\206\347\240\201\347\231\273\345\275\225.md.8b926ddc.lean.js" create mode 100644 "assets/linux_env_\351\223\276\346\216\245\345\222\214\345\210\253\345\220\215(ln\343\200\201alias).md.4f5e25d9.js" create mode 100644 "assets/linux_env_\351\223\276\346\216\245\345\222\214\345\210\253\345\220\215(ln\343\200\201alias).md.4f5e25d9.lean.js" create mode 100644 "assets/linux_hardware_linux\347\243\201\347\233\230\346\223\215\344\275\234\347\233\270\345\205\263.md.0fc149c9.js" create mode 100644 "assets/linux_hardware_linux\347\243\201\347\233\230\346\223\215\344\275\234\347\233\270\345\205\263.md.0fc149c9.lean.js" create mode 100644 assets/linux_index.md.2f33c5bb.js create mode 100644 assets/linux_index.md.2f33c5bb.lean.js create mode 100644 assets/python_base_Poetry.md.86dc5eff.js create mode 100644 assets/python_base_Poetry.md.86dc5eff.lean.js create mode 100644 "assets/python_base_python\345\237\272\347\241\200\350\257\255\346\263\225.md.2ec54814.js" create mode 100644 "assets/python_base_python\345\237\272\347\241\200\350\257\255\346\263\225.md.2ec54814.lean.js" create mode 100644 "assets/python_base_python\345\271\266\345\217\221\347\274\226\347\250\213.md.d5a5de0a.js" create mode 100644 "assets/python_base_python\345\271\266\345\217\221\347\274\226\347\250\213.md.d5a5de0a.lean.js" create mode 100644 "assets/python_base_\350\243\205\351\245\260\345\231\250\346\267\261\345\205\245.md.6df2bfe7.js" create mode 100644 "assets/python_base_\350\243\205\351\245\260\345\231\250\346\267\261\345\205\245.md.6df2bfe7.lean.js" create mode 100644 "assets/python_crawler_CrawlSpider\345\205\250\347\253\231\347\210\254\345\217\226.md.914da3d0.js" create mode 100644 "assets/python_crawler_CrawlSpider\345\205\250\347\253\231\347\210\254\345\217\226.md.914da3d0.lean.js" create mode 100644 "assets/python_crawler_python\351\205\215\345\220\210ffmpeg\344\270\213\350\275\275bilibili\350\247\206\351\242\221.md.89478ae1.js" create mode 100644 "assets/python_crawler_python\351\205\215\345\220\210ffmpeg\344\270\213\350\275\275bilibili\350\247\206\351\242\221.md.89478ae1.lean.js" create mode 100644 "assets/python_crawler_requests\346\250\241\345\235\227\345\205\245\351\227\250.md.5ba42074.js" create mode 100644 "assets/python_crawler_requests\346\250\241\345\235\227\345\205\245\351\227\250.md.5ba42074.lean.js" create mode 100644 "assets/python_crawler_scrapy\346\241\206\346\236\266\345\205\245\351\227\250.md.74ea41f3.js" create mode 100644 "assets/python_crawler_scrapy\346\241\206\346\236\266\345\205\245\351\227\250.md.74ea41f3.lean.js" create mode 100644 "assets/python_crawler_scrapy\350\277\233\351\230\266.md.9a37e129.js" create mode 100644 "assets/python_crawler_scrapy\350\277\233\351\230\266.md.9a37e129.lean.js" create mode 100644 "assets/python_crawler_selenium\346\250\241\345\235\227.md.5446e227.js" create mode 100644 "assets/python_crawler_selenium\346\250\241\345\235\227.md.5446e227.lean.js" create mode 100644 "assets/python_crawler_\345\210\206\345\270\203\345\274\217\347\210\254\350\231\253\345\222\214\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253.md.1722f14e.js" create mode 100644 "assets/python_crawler_\345\210\206\345\270\203\345\274\217\347\210\254\350\231\253\345\222\214\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253.md.1722f14e.lean.js" create mode 100644 "assets/python_crawler_\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253\345\256\236\350\267\265\346\241\210\344\276\213 \344\270\213\350\275\275\346\214\207\345\256\232b\347\253\231up\344\270\273\347\232\204\346\211\200\346\234\211\344\275\234\345\223\201.md.6ec9ffd2.js" create mode 100644 "assets/python_crawler_\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253\345\256\236\350\267\265\346\241\210\344\276\213 \344\270\213\350\275\275\346\214\207\345\256\232b\347\253\231up\344\270\273\347\232\204\346\211\200\346\234\211\344\275\234\345\223\201.md.6ec9ffd2.lean.js" create mode 100644 "assets/python_crawler_\345\244\232\347\272\277\347\250\213\347\210\254\345\217\226\346\242\250\350\247\206\351\242\221\347\275\221\347\253\231\347\232\204\347\203\255\351\227\250\350\247\206\351\242\221.md.0cea7349.js" create mode 100644 "assets/python_crawler_\345\244\232\347\272\277\347\250\213\347\210\254\345\217\226\346\242\250\350\247\206\351\242\221\347\275\221\347\253\231\347\232\204\347\203\255\351\227\250\350\247\206\351\242\221.md.0cea7349.lean.js" create mode 100644 "assets/python_crawler_\345\260\217\347\272\242\344\271\246\347\210\254\350\231\253.md.ef9e7902.js" create mode 100644 "assets/python_crawler_\345\260\217\347\272\242\344\271\246\347\210\254\350\231\253.md.ef9e7902.lean.js" create mode 100644 "assets/python_crawler_\351\252\214\350\257\201\347\240\201\350\257\206\345\210\253\345\222\214\346\250\241\346\213\237\347\231\273\345\275\225.md.042432f0.js" create mode 100644 "assets/python_crawler_\351\252\214\350\257\201\347\240\201\350\257\206\345\210\253\345\222\214\346\250\241\346\213\237\347\231\273\345\275\225.md.042432f0.lean.js" create mode 100644 assets/python_index.md.700873bd.js create mode 100644 assets/python_index.md.700873bd.lean.js create mode 100644 "assets/python_others_Alfred\346\217\222\344\273\266-\345\277\253\351\200\237\344\275\277\347\224\250\347\274\226\350\276\221\345\231\250\346\211\223\345\274\200\346\214\207\345\256\232\346\226\207\344\273\266.md.056bae34.js" create mode 100644 "assets/python_others_Alfred\346\217\222\344\273\266-\345\277\253\351\200\237\344\275\277\347\224\250\347\274\226\350\276\221\345\231\250\346\211\223\345\274\200\346\214\207\345\256\232\346\226\207\344\273\266.md.056bae34.lean.js" create mode 100644 "assets/python_others_argparse\346\250\241\345\235\227\345\205\245\351\227\250.md.9f5f1e90.js" create mode 100644 "assets/python_others_argparse\346\250\241\345\235\227\345\205\245\351\227\250.md.9f5f1e90.lean.js" create mode 100644 assets/python_others_youtube-upload.md.3d98cb59.js create mode 100644 assets/python_others_youtube-upload.md.3d98cb59.lean.js create mode 100644 "assets/python_others_\344\275\277\347\224\250pandas\346\250\241\345\235\227\350\277\233\350\241\214\346\225\260\346\215\256\345\244\204\347\220\206.md.fa389c23.js" create mode 100644 "assets/python_others_\344\275\277\347\224\250pandas\346\250\241\345\235\227\350\277\233\350\241\214\346\225\260\346\215\256\345\244\204\347\220\206.md.fa389c23.lean.js" create mode 100644 "assets/python_others_\345\210\207\346\215\242windows\344\273\243\347\220\206\350\256\276\347\275\256\345\274\200\345\205\263.md.88db6435.js" create mode 100644 "assets/python_others_\345\210\207\346\215\242windows\344\273\243\347\220\206\350\256\276\347\275\256\345\274\200\345\205\263.md.88db6435.lean.js" create mode 100644 "assets/python_others_\350\257\273\345\217\226excel_ssh\351\200\232\351\201\223\350\277\236\346\216\245rds\346\233\264\346\226\260\346\225\260\346\215\256.md.3c80a264.js" create mode 100644 "assets/python_others_\350\257\273\345\217\226excel_ssh\351\200\232\351\201\223\350\277\236\346\216\245rds\346\233\264\346\226\260\346\225\260\346\215\256.md.3c80a264.lean.js" create mode 100644 "assets/python_web_Django\345\205\245\351\227\250.md.7b29ab40.js" create mode 100644 "assets/python_web_Django\345\205\245\351\227\250.md.7b29ab40.lean.js" create mode 100644 "assets/python_web_pymysql\344\275\277\347\224\250.md.b054a10e.js" create mode 100644 "assets/python_web_pymysql\344\275\277\347\224\250.md.b054a10e.lean.js" create mode 100644 assets/style.cb20272f.css create mode 100644 assets/tinker_index.md.b79e9a67.js create mode 100644 assets/tinker_index.md.b79e9a67.lean.js create mode 100644 "assets/tinker_network_frp\345\206\205\347\275\221\347\251\277\351\200\217.md.7473690d.js" create mode 100644 "assets/tinker_network_frp\345\206\205\347\275\221\347\251\277\351\200\217.md.7473690d.lean.js" create mode 100644 "assets/tinker_network_home server\346\220\255\345\273\272.md.862c471d.js" create mode 100644 "assets/tinker_network_home server\346\220\255\345\273\272.md.862c471d.lean.js" create mode 100644 "assets/tinker_network_openwrt\345\256\211\350\243\205\345\217\212\351\205\215\347\275\256.md.55152baa.js" create mode 100644 "assets/tinker_network_openwrt\345\256\211\350\243\205\345\217\212\351\205\215\347\275\256.md.55152baa.lean.js" create mode 100644 "assets/tinker_network_openwrt\345\274\200\345\220\257ipv6.md.edcbc94f.js" create mode 100644 "assets/tinker_network_openwrt\345\274\200\345\220\257ipv6.md.edcbc94f.lean.js" create mode 100644 "assets/tinker_network_pt\344\270\213\350\275\275\345\205\245\351\227\250.md.0f6b065f.js" create mode 100644 "assets/tinker_network_pt\344\270\213\350\275\275\345\205\245\351\227\250.md.0f6b065f.lean.js" create mode 100644 "assets/tinker_network_ubuntu-server\345\274\200\345\220\257\347\275\221\347\273\234\345\224\244\351\206\222.md.70e29fdd.js" create mode 100644 "assets/tinker_network_ubuntu-server\345\274\200\345\220\257\347\275\221\347\273\234\345\224\244\351\206\222.md.70e29fdd.lean.js" create mode 100644 "assets/tinker_network_windows\346\214\202\350\275\275webdav\347\232\204\351\227\256\351\242\230\345\244\204\347\220\206.md.3ad32a6e.js" create mode 100644 "assets/tinker_network_windows\346\214\202\350\275\275webdav\347\232\204\351\227\256\351\242\230\345\244\204\347\220\206.md.3ad32a6e.lean.js" create mode 100644 "assets/tinker_network_\344\275\277\347\224\250https\350\256\277\351\227\256\345\206\205\347\275\221\346\234\215\345\212\241.md.43b6af56.js" create mode 100644 "assets/tinker_network_\344\275\277\347\224\250https\350\256\277\351\227\256\345\206\205\347\275\221\346\234\215\345\212\241.md.43b6af56.lean.js" create mode 100644 "assets/tinker_network_\345\261\261\347\211\271ups\351\205\215\345\220\210nut\345\256\236\347\216\260\346\226\255\347\224\265\345\256\211\345\205\250\345\205\263\346\234\272.md.a93a1b0f.js" create mode 100644 "assets/tinker_network_\345\261\261\347\211\271ups\351\205\215\345\220\210nut\345\256\236\347\216\260\346\226\255\347\224\265\345\256\211\345\205\250\345\205\263\346\234\272.md.a93a1b0f.lean.js" create mode 100644 "assets/tinker_network_\347\247\273\345\212\250\345\205\211\347\214\253\346\224\271\346\241\245\346\216\245\346\250\241\345\274\217.md.dd786d69.js" create mode 100644 "assets/tinker_network_\347\247\273\345\212\250\345\205\211\347\214\253\346\224\271\346\241\245\346\216\245\346\250\241\345\274\217.md.dd786d69.lean.js" create mode 100644 "assets/tinker_vm_PVE\345\274\202\345\270\270\345\205\263\346\234\272\345\220\216\347\243\201\347\233\230\346\243\200\346\237\245\345\244\204\347\220\206.md.ad24a8cd.js" create mode 100644 "assets/tinker_vm_PVE\345\274\202\345\270\270\345\205\263\346\234\272\345\220\216\347\243\201\347\233\230\346\243\200\346\237\245\345\244\204\347\220\206.md.ad24a8cd.lean.js" create mode 100644 "assets/tinker_vm_VMWare\350\231\232\346\213\237\346\234\272\347\232\204\345\207\240\347\247\215\347\275\221\347\273\234\350\277\236\346\216\245\346\250\241\345\274\217.md.86cddc5f.js" create mode 100644 "assets/tinker_vm_VMWare\350\231\232\346\213\237\346\234\272\347\232\204\345\207\240\347\247\215\347\275\221\347\273\234\350\277\236\346\216\245\346\250\241\345\274\217.md.86cddc5f.lean.js" create mode 100644 "assets/tinker_vm_\345\256\211\350\243\205PVE\350\231\232\346\213\237\346\234\272\345\271\266\345\234\250PVE\345\256\211\350\243\205truenas.md.b6416835.js" create mode 100644 "assets/tinker_vm_\345\256\211\350\243\205PVE\350\231\232\346\213\237\346\234\272\345\271\266\345\234\250PVE\345\256\211\350\243\205truenas.md.b6416835.lean.js" create mode 100644 "docker/Dockerfile\350\257\255\346\263\225.html" create mode 100644 "docker/docker-compose\350\257\255\346\263\225.html" create mode 100644 "docker/docker\347\275\221\347\273\234\344\271\213macvlan.html" create mode 100644 docker/index.html create mode 100644 "docker/\345\270\270\347\224\250\346\214\207\344\273\244.html" create mode 100644 favicon.ico create mode 100644 frontend/base/CSS.html create mode 100644 frontend/base/HTML.html create mode 100644 frontend/base/JavaScript.html create mode 100644 frontend/base/Nodejs.html create mode 100644 frontend/base/Typescript.html create mode 100644 frontend/base/Webpack.html create mode 100644 frontend/base/jQuery.html create mode 100644 frontend/framework/Vue.html create mode 100644 frontend/index.html create mode 100644 "frontend/others/ElementPlus el-upload\346\272\220\347\240\201\345\210\206\346\236\220.html" create mode 100644 frontend/others/HackingWithSwift-1.html create mode 100644 frontend/others/HackingWithSwift-2.html create mode 100644 "frontend/others/SwiftUI\345\205\245\351\227\250.html" create mode 100644 "frontend/others/swift\350\257\255\346\263\225.html" create mode 100644 golang/base/GoTemplate.html create mode 100644 "golang/base/golang\345\237\272\347\241\200\350\257\255\346\263\225.html" create mode 100644 golang/cli/Cobra.html create mode 100644 golang/index.html create mode 100644 golang/tools/redis-cleaner.html create mode 100644 golang/web/Gin.html create mode 100644 hashmap.json create mode 100644 index.html create mode 100644 "java/base/JDK1.8HashMap\346\272\220\347\240\201\345\255\246\344\271\240.html" create mode 100644 "java/base/Java8\346\226\260\347\211\271\346\200\247\345\233\236\351\241\276.html" create mode 100644 "java/base/String\347\261\273\347\232\204\346\267\261\345\205\245\345\255\246\344\271\240.html" create mode 100644 "java/database/MySql\347\264\242\345\274\225.html" create mode 100644 "java/database/Otter\345\256\236\347\216\260\346\225\260\346\215\256\345\205\250\351\207\217\345\242\236\351\207\217\345\220\214\346\255\245.html" create mode 100644 "java/database/SQL\344\274\230\345\214\226\345\255\246\344\271\240.html" create mode 100644 "java/devtool/Maven\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237.html" create mode 100644 "java/devtool/nexus\346\227\240\346\263\225\344\270\213\350\275\275\344\276\235\350\265\226\347\232\204\351\227\256\351\242\230\350\256\260\345\275\225.html" create mode 100644 "java/framework/ConfigurationProperties\346\263\250\350\247\243.html" create mode 100644 "java/framework/JavaSPI\346\234\272\345\210\266\345\222\214Springboot\350\207\252\345\212\250\350\243\205\351\205\215\345\216\237\347\220\206.html" create mode 100644 "java/framework/Java\346\227\245\345\277\227\345\217\221\345\261\225\345\216\206\345\217\262.html" create mode 100644 "java/framework/Mybatis Generator\351\205\215\347\275\256.html" create mode 100644 "java/framework/POI\344\272\213\344\273\266\346\250\241\345\274\217\350\247\243\346\236\220\345\271\266\350\257\273\345\217\226Excel\346\226\207\344\273\266\346\225\260\346\215\256.html" create mode 100644 "java/framework/Spring Cloud Config\346\216\245\345\205\245.html" create mode 100644 "java/framework/SpringMVC\347\232\204\346\211\247\350\241\214\346\265\201\347\250\213\346\272\220\347\240\201\345\210\206\346\236\220.html" create mode 100644 "java/framework/Spring\351\205\215\347\275\256\345\222\214\346\235\241\344\273\266\345\214\226\347\273\204\344\273\266\345\212\240\350\275\275\346\263\250\350\247\243.html" create mode 100644 "java/framework/dubbo\346\216\245\345\217\243\350\266\205\346\227\266\351\205\215\347\275\256.html" create mode 100644 "java/framework/logback\350\207\252\345\256\232\344\271\211.html" create mode 100644 "java/framework/netty+websocket\345\256\236\347\216\260\345\215\263\346\227\266\351\200\232\350\256\257\345\212\237\350\203\275.html" create mode 100644 "java/framework/spring-session\345\256\236\347\216\260\351\233\206\347\276\244session\345\205\261\344\272\253.html" create mode 100644 "java/framework/springcloud\344\274\230\351\233\205\344\270\213\347\272\277\346\234\215\345\212\241.html" create mode 100644 java/framework/swagger+knife4j.html create mode 100644 "java/framework/\344\275\277\347\224\250EasyExcel\345\257\274\345\207\272excel.html" create mode 100644 "java/framework/\344\275\277\347\224\250spring validation\350\277\233\350\241\214\345\217\202\346\225\260\346\240\241\351\252\214.html" create mode 100644 "java/framework/\345\210\206\345\270\203\345\274\217\345\256\232\346\227\266\344\273\273\345\212\241\350\247\243\345\206\263\346\226\271\346\241\210xxl-job.html" create mode 100644 "java/framework/\345\220\216\347\253\257\345\205\201\350\256\270\350\267\250\345\237\237\351\205\215\347\275\256.html" create mode 100644 "java/framework/\350\207\252\345\256\232\344\271\211MybatisPlusGenerator.html" create mode 100644 java/index.html create mode 100644 "java/middleware/kafka\345\255\246\344\271\240\350\256\260\345\275\225.html" create mode 100644 "java/middleware/kafka\345\270\270\347\224\250\345\221\275\344\273\244\350\256\260\345\275\225.html" create mode 100644 "java/middleware/kakfa\345\256\236\350\267\265.html" create mode 100644 "java/middleware/kakfa\351\207\215\345\244\215\346\266\210\350\264\271\351\227\256\351\242\230\345\244\204\347\220\206.html" create mode 100644 "java/middleware/\345\205\263\344\272\216\346\266\210\346\201\257\344\270\255\351\227\264\344\273\266MQ.html" create mode 100644 "java/middleware/\345\210\206\345\270\203\345\274\217\351\224\201\350\247\243\345\206\263\346\226\271\346\241\210.html" create mode 100644 "java/others/OpenJDK\346\262\241\346\234\211jstack\347\255\211\345\221\275\344\273\244\347\232\204\350\247\243\345\206\263\345\212\236\346\263\225.html" create mode 100644 "java/others/POI\350\270\251\345\235\221-zip file is closed.html" create mode 100644 "java/others/cpu\345\215\240\347\224\250\347\216\207\351\253\230\346\216\222\346\237\245\346\200\235\350\267\257.html" create mode 100644 "java/others/feign\350\257\267\346\261\202\345\257\274\350\207\264\347\232\204\347\224\250\346\210\267ip\350\216\267\345\217\226\351\227\256\351\242\230\350\256\260\345\275\225.html" create mode 100644 "java/others/linux\346\234\215\345\212\241\345\231\250\345\256\211\350\243\205OpenOffice\350\270\251\345\235\221.html" create mode 100644 "java/others/mybatis\347\232\204classpath\351\205\215\347\275\256\345\257\274\350\207\264\347\232\204jar\345\214\205\350\257\273\345\217\226\351\227\256\351\242\230.html" create mode 100644 "java/others/ribbon\345\210\267\346\226\260\346\234\215\345\212\241\345\210\227\350\241\250\351\227\264\351\232\224\345\222\214canal\347\232\204\345\235\221.html" create mode 100644 "java/others/\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217\345\212\240\345\257\206\346\225\260\346\215\256\345\257\271\347\247\260\350\247\243\345\257\206\345\267\245\345\205\267\347\261\273.html" create mode 100644 "linux/applications/Canal\351\203\250\347\275\262.html" create mode 100644 linux/applications/Clash.html create mode 100644 "linux/applications/FFmpeg\347\233\270\345\205\263.html" create mode 100644 linux/applications/Grafana.html create mode 100644 "linux/applications/Linux\344\270\255\344\275\277\347\224\250selenium.html" create mode 100644 "linux/applications/Nginx\351\205\215\347\275\256.html" create mode 100644 linux/applications/Rclone.html create mode 100644 linux/applications/iptables.html create mode 100644 "linux/applications/screen\347\232\204\350\277\233\351\230\266\347\224\250\346\263\225.html" create mode 100644 "linux/env/ArchLinux\345\256\211\350\243\205.html" create mode 100644 "linux/env/Centos7\345\256\211\350\243\205Python3\347\216\257\345\242\203.html" create mode 100644 "linux/env/Linux\345\270\270\347\224\250\346\214\207\344\273\244.html" create mode 100644 "linux/env/Linux\346\234\215\345\212\241\345\231\250\346\226\207\344\273\266\347\233\256\345\275\225\345\205\261\344\272\253\346\230\240\345\260\204\351\205\215\347\275\256.html" create mode 100644 "linux/env/Linux\347\247\201\351\222\245\347\231\273\351\231\206\346\217\220\347\244\272server refused our key.html" create mode 100644 "linux/env/Linux\350\256\276\347\275\256swap\347\251\272\351\227\264.html" create mode 100644 "linux/env/Linux\350\256\277\351\227\256\346\235\203\351\231\220\346\216\247\345\210\266\344\271\213ACL.html" create mode 100644 linux/env/Ubuntu-server.html create mode 100644 "linux/env/bash\345\270\270\347\224\250\347\232\204\345\277\253\346\215\267\351\224\256.html" create mode 100644 "linux/env/centos7\351\230\262\347\201\253\345\242\231\345\221\275\344\273\244.html" create mode 100644 "linux/env/\344\273\216\351\233\266\346\220\255\345\273\272Linux\350\231\232\346\213\237\346\234\272\347\216\257\345\242\203.html" create mode 100644 "linux/env/\346\234\215\345\212\241\345\231\250\345\220\257\347\224\250ssh\345\257\206\351\222\245\347\231\273\345\275\225\345\271\266\347\246\201\347\224\250\345\257\206\347\240\201\347\231\273\345\275\225.html" create mode 100644 "linux/env/\351\223\276\346\216\245\345\222\214\345\210\253\345\220\215(ln\343\200\201alias).html" create mode 100644 "linux/hardware/linux\347\243\201\347\233\230\346\223\215\344\275\234\347\233\270\345\205\263.html" create mode 100644 linux/index.html create mode 100644 logo.png create mode 100644 python/base/Poetry.html create mode 100644 "python/base/python\345\237\272\347\241\200\350\257\255\346\263\225.html" create mode 100644 "python/base/python\345\271\266\345\217\221\347\274\226\347\250\213.html" create mode 100644 "python/base/\350\243\205\351\245\260\345\231\250\346\267\261\345\205\245.html" create mode 100644 "python/crawler/CrawlSpider\345\205\250\347\253\231\347\210\254\345\217\226.html" create mode 100644 "python/crawler/python\351\205\215\345\220\210ffmpeg\344\270\213\350\275\275bilibili\350\247\206\351\242\221.html" create mode 100644 "python/crawler/requests\346\250\241\345\235\227\345\205\245\351\227\250.html" create mode 100644 "python/crawler/scrapy\346\241\206\346\236\266\345\205\245\351\227\250.html" create mode 100644 "python/crawler/scrapy\350\277\233\351\230\266.html" create mode 100644 "python/crawler/selenium\346\250\241\345\235\227.html" create mode 100644 "python/crawler/\345\210\206\345\270\203\345\274\217\347\210\254\350\231\253\345\222\214\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253.html" create mode 100644 "python/crawler/\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253\345\256\236\350\267\265\346\241\210\344\276\213 \344\270\213\350\275\275\346\214\207\345\256\232b\347\253\231up\344\270\273\347\232\204\346\211\200\346\234\211\344\275\234\345\223\201.html" create mode 100644 "python/crawler/\345\244\232\347\272\277\347\250\213\347\210\254\345\217\226\346\242\250\350\247\206\351\242\221\347\275\221\347\253\231\347\232\204\347\203\255\351\227\250\350\247\206\351\242\221.html" create mode 100644 "python/crawler/\345\260\217\347\272\242\344\271\246\347\210\254\350\231\253.html" create mode 100644 "python/crawler/\351\252\214\350\257\201\347\240\201\350\257\206\345\210\253\345\222\214\346\250\241\346\213\237\347\231\273\345\275\225.html" create mode 100644 python/index.html create mode 100644 "python/others/Alfred\346\217\222\344\273\266-\345\277\253\351\200\237\344\275\277\347\224\250\347\274\226\350\276\221\345\231\250\346\211\223\345\274\200\346\214\207\345\256\232\346\226\207\344\273\266.html" create mode 100644 "python/others/argparse\346\250\241\345\235\227\345\205\245\351\227\250.html" create mode 100644 python/others/youtube-upload.html create mode 100644 "python/others/\344\275\277\347\224\250pandas\346\250\241\345\235\227\350\277\233\350\241\214\346\225\260\346\215\256\345\244\204\347\220\206.html" create mode 100644 "python/others/\345\210\207\346\215\242windows\344\273\243\347\220\206\350\256\276\347\275\256\345\274\200\345\205\263.html" create mode 100644 "python/others/\350\257\273\345\217\226excel&ssh\351\200\232\351\201\223\350\277\236\346\216\245rds\346\233\264\346\226\260\346\225\260\346\215\256.html" create mode 100644 "python/web/Django\345\205\245\351\227\250.html" create mode 100644 "python/web/pymysql\344\275\277\347\224\250.html" create mode 100644 tinker/index.html create mode 100644 "tinker/network/frp\345\206\205\347\275\221\347\251\277\351\200\217.html" create mode 100644 "tinker/network/home server\346\220\255\345\273\272.html" create mode 100644 "tinker/network/openwrt\345\256\211\350\243\205\345\217\212\351\205\215\347\275\256.html" create mode 100644 "tinker/network/openwrt\345\274\200\345\220\257ipv6.html" create mode 100644 "tinker/network/pt\344\270\213\350\275\275\345\205\245\351\227\250.html" create mode 100644 "tinker/network/ubuntu-server\345\274\200\345\220\257\347\275\221\347\273\234\345\224\244\351\206\222.html" create mode 100644 "tinker/network/windows\346\214\202\350\275\275webdav\347\232\204\351\227\256\351\242\230\345\244\204\347\220\206.html" create mode 100644 "tinker/network/\344\275\277\347\224\250https\350\256\277\351\227\256\345\206\205\347\275\221\346\234\215\345\212\241.html" create mode 100644 "tinker/network/\345\261\261\347\211\271ups\351\205\215\345\220\210nut\345\256\236\347\216\260\346\226\255\347\224\265\345\256\211\345\205\250\345\205\263\346\234\272.html" create mode 100644 "tinker/network/\347\247\273\345\212\250\345\205\211\347\214\253\346\224\271\346\241\245\346\216\245\346\250\241\345\274\217.html" create mode 100644 "tinker/vm/PVE\345\274\202\345\270\270\345\205\263\346\234\272\345\220\216\347\243\201\347\233\230\346\243\200\346\237\245\345\244\204\347\220\206.html" create mode 100644 "tinker/vm/VMWare\350\231\232\346\213\237\346\234\272\347\232\204\345\207\240\347\247\215\347\275\221\347\273\234\350\277\236\346\216\245\346\250\241\345\274\217.html" create mode 100644 "tinker/vm/\345\256\211\350\243\205PVE\350\231\232\346\213\237\346\234\272\345\271\266\345\234\250PVE\345\256\211\350\243\205truenas.html" diff --git a/404.html b/404.html new file mode 100644 index 000000000..bf661dd73 --- /dev/null +++ b/404.html @@ -0,0 +1,22 @@ + + + + + + 404 | 故事 + + + + + + + + + + + +
Skip to content

404

PAGE NOT FOUND

But if you don't change your direction, and if you keep looking, you may end up where you are heading.
+ + + + \ No newline at end of file diff --git a/CNAME b/CNAME new file mode 100644 index 000000000..15d602378 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +blog.storyxc.com diff --git "a/actions/designpattern/\347\255\226\347\225\245\346\250\241\345\274\217\347\232\204\345\205\267\344\275\223\345\256\236\347\216\260.html" "b/actions/designpattern/\347\255\226\347\225\245\346\250\241\345\274\217\347\232\204\345\205\267\344\275\223\345\256\236\347\216\260.html" new file mode 100644 index 000000000..2450dd396 --- /dev/null +++ "b/actions/designpattern/\347\255\226\347\225\245\346\250\241\345\274\217\347\232\204\345\205\267\344\275\223\345\256\236\347\216\260.html" @@ -0,0 +1,673 @@ + + + + + + 策略模式的具体实现 | 故事 + + + + + + + + + + + + + + +
Skip to content

策略模式的具体实现

背景

开发中经常有这样一种场景,一个接口需要处理的请求中的内容包含多种不同的类型。比如支付系统,订单支付的时候可能是支付宝支付,微信支付或者银联支付等。又或者是订单系统,订单可能是普通订单,可能是团购订单,也可能是秒杀订单。前阵子做的一个预览Office文件的功能也与之类似,文件的类型不同,也需要采取不同的处理方案。这时候最简单的做法就是在controller中写n多个if else:

java
if ( "excel".equals(file.getType)) {
+	//***
+} else if"word".equals(file.getType()){
+    //***
+} ……
if ( "excel".equals(file.getType)) {
+	//***
+} else if"word".equals(file.getType()){
+    //***
+} ……

如果后面再加其他的类型,那就继续加if else语句,这样代码就会变的很丑陋,而且每次都需要对controller代码进行修改,后续的扩展很麻烦。所以这种情况通常会采用策略模式来进行处理,这样我们的代码会变得更加优雅,方便后续的维护。

策略模式

策略模式是一种行为模式,主要作用是在程序运行时动态切换一个类的行为或者算法。我们需要做的就是创建一个定义行为的Strategy接口以及它的具体策略实现类,以及一个策略的上下文来动态切换策略。 无标题.png

下面将介绍具体的实现方案,以订单系统为例

环境搭建

xml
<parent>
+	<groupId>org.springframework.boot</groupId>
+	<artifactId>spring-boot-starter-parent</artifactId>
+	<version>2.1.4.RELEASE</version>
+</parent>
+<dependencies>
+	<dependency>
+		<groupId>org.projectlombok</groupId>
+		<artifactId>lombok</artifactId>
+		<version>1.18.6</version>
+	</dependency>
+	<dependency>
+		<groupId>org.springframework.boot</groupId>
+		<artifactId>spring-boot-starter-web</artifactId>
+		<version>2.1.4.RELEASE</version>
+	</dependency>
+	<dependency>
+		<groupId>org.apache.commons</groupId>
+		<artifactId>commons-lang3</artifactId>
+		<version>3.8.1</version>
+	</dependency>
+</dependencies>
<parent>
+	<groupId>org.springframework.boot</groupId>
+	<artifactId>spring-boot-starter-parent</artifactId>
+	<version>2.1.4.RELEASE</version>
+</parent>
+<dependencies>
+	<dependency>
+		<groupId>org.projectlombok</groupId>
+		<artifactId>lombok</artifactId>
+		<version>1.18.6</version>
+	</dependency>
+	<dependency>
+		<groupId>org.springframework.boot</groupId>
+		<artifactId>spring-boot-starter-web</artifactId>
+		<version>2.1.4.RELEASE</version>
+	</dependency>
+	<dependency>
+		<groupId>org.apache.commons</groupId>
+		<artifactId>commons-lang3</artifactId>
+		<version>3.8.1</version>
+	</dependency>
+</dependencies>

订单实体类

java
@Data
+public class Order {
+    private String code;
+    private BigDecimal price;
+    /**
+     * 1: 普通订单
+     * 2: 秒杀订单
+     * 3: 团购订单
+     */
+    private String type;
+}
@Data
+public class Order {
+    private String code;
+    private BigDecimal price;
+    /**
+     * 1: 普通订单
+     * 2: 秒杀订单
+     * 3: 团购订单
+     */
+    private String type;
+}

抽象策略接口

java
public interface OrderStrategy {
+    String handleOrder(Order order);
+}
public interface OrderStrategy {
+    String handleOrder(Order order);
+}

策略具体实现

java
@Component
+public class NormalHandler implements OrderStrategy {
+
+    @Override
+    public String handleOrder(Order order) {
+        return "普通订单处理完毕";
+    }
+}
+
+@Component
+public class GroupHandler implements OrderStrategy {
+    @Override
+    public String handleOrder(Order order) {
+        return "团购订单处理完毕";
+    }
+}
+
+@Component
+public class SecKillHandler implements OrderStrategy {
+    @Override
+    public String handleOrder(Order order) {
+        return "秒杀订单处理完毕";
+    }
+}
@Component
+public class NormalHandler implements OrderStrategy {
+
+    @Override
+    public String handleOrder(Order order) {
+        return "普通订单处理完毕";
+    }
+}
+
+@Component
+public class GroupHandler implements OrderStrategy {
+    @Override
+    public String handleOrder(Order order) {
+        return "团购订单处理完毕";
+    }
+}
+
+@Component
+public class SecKillHandler implements OrderStrategy {
+    @Override
+    public String handleOrder(Order order) {
+        return "秒杀订单处理完毕";
+    }
+}

SpringUtils

@Component
+public class SpringUtils implements ApplicationContextAware {
+
+    private static ApplicationContext applicationContext;
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        SpringUtils.applicationContext = applicationContext;
+    }
+
+    //获取applicationContext
+    private static ApplicationContext getApplicationContext() {
+        return applicationContext;
+    }
+
+    //通过name获取 Bean.
+    public static Object getBean(String name){
+        return getApplicationContext().getBean(name);
+    }
+
+    //通过class获取Bean.
+    public static <T> T getBean(Class<T> clazz){
+        return getApplicationContext().getBean(clazz);
+    }
+
+    //通过name,以及Clazz返回指定的Bean
+    public static <T> T getBean(String name,Class<T> clazz){
+        return getApplicationContext().getBean(name, clazz);
+    }
+
+}
@Component
+public class SpringUtils implements ApplicationContextAware {
+
+    private static ApplicationContext applicationContext;
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        SpringUtils.applicationContext = applicationContext;
+    }
+
+    //获取applicationContext
+    private static ApplicationContext getApplicationContext() {
+        return applicationContext;
+    }
+
+    //通过name获取 Bean.
+    public static Object getBean(String name){
+        return getApplicationContext().getBean(name);
+    }
+
+    //通过class获取Bean.
+    public static <T> T getBean(Class<T> clazz){
+        return getApplicationContext().getBean(clazz);
+    }
+
+    //通过name,以及Clazz返回指定的Bean
+    public static <T> T getBean(String name,Class<T> clazz){
+        return getApplicationContext().getBean(name, clazz);
+    }
+
+}

第一种实现

可以采取配置的方式,将不同类型和对应的handler的bean name配置在配置文件或者是数据库中,这样我们在context中可以直接获取配置文件中的bean name或者去数据库中查询,然后从spring容器中获取对应的bean并调用处理方法即可。

以将映射关系持久化到数据库为例,我们需要建一张表来维护类型和具体处理器之间的关系 字段为type和对应处理器的bean名称

controller

java
@RestController
+@RequestMapping("/api/order")
+public class OrderController {
+
+    @Autowired
+    private IOrderService orderService;
+
+
+    @GetMapping("/{type}")
+    public String handleOrder(@PathVariable String type){
+        return orderService.handleOrder(type);
+    }
+}
@RestController
+@RequestMapping("/api/order")
+public class OrderController {
+
+    @Autowired
+    private IOrderService orderService;
+
+
+    @GetMapping("/{type}")
+    public String handleOrder(@PathVariable String type){
+        return orderService.handleOrder(type);
+    }
+}

service

java
@Service
+public class OrderServiceImpl implements IOrderService {
+
+    @Autowired
+    private OrderStrategyContext context;
+
+    @Override
+    public String handleOrder(String type) {
+        return context.getBean(type).handleOrder(type);
+    }
+}
@Service
+public class OrderServiceImpl implements IOrderService {
+
+    @Autowired
+    private OrderStrategyContext context;
+
+    @Override
+    public String handleOrder(String type) {
+        return context.getBean(type).handleOrder(type);
+    }
+}

context

java
@Component
+public class OrderStrategyContext {
+    @Autowired
+	private StrategyMapper mapper;
+
+    public OrderStrategy getBean(String type){
+        String beanName = mapper.getBeanName(type);
+        return SpringUtils.getBean(beanName);
+    }
+
+}
@Component
+public class OrderStrategyContext {
+    @Autowired
+	private StrategyMapper mapper;
+
+    public OrderStrategy getBean(String type){
+        String beanName = mapper.getBeanName(type);
+        return SpringUtils.getBean(beanName);
+    }
+
+}

第二种实现

第一种方案相较于无尽的if else已经好很多了,但是还是需要增加配置文件或者数据库中新建表来维护类型和对应处理器的映射关系。还可以直接自定义注解来实现这个关系的对应。

自定义注解HandlerType

java
@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface HandlerType {
+    String value();
+}
@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface HandlerType {
+    String value();
+}

然后在每个具体策略类上加上注解

java
@Component
+@HandlerType("1")
+public class NormalHandler implements OrderStrategy {
+
+    @Override
+    public String handleOrder(String type) {
+        return "普通订单处理完毕";
+    }
+}
@Component
+@HandlerType("1")
+public class NormalHandler implements OrderStrategy {
+
+    @Override
+    public String handleOrder(String type) {
+        return "普通订单处理完毕";
+    }
+}
java
@Component
+@HandlerType("2")
+public class GroupHandler implements OrderStrategy {
+    @Override
+    public String handleOrder(String type) {
+        return "团购订单处理完毕";
+    }
+}
@Component
+@HandlerType("2")
+public class GroupHandler implements OrderStrategy {
+    @Override
+    public String handleOrder(String type) {
+        return "团购订单处理完毕";
+    }
+}
java
@Component
+@HandlerType("3")
+public class SecKillHandler implements OrderStrategy {
+
+    @Override
+    public String handleOrder(String type) {
+        return "秒杀订单处理完毕";
+    }
+}
@Component
+@HandlerType("3")
+public class SecKillHandler implements OrderStrategy {
+
+    @Override
+    public String handleOrder(String type) {
+        return "秒杀订单处理完毕";
+    }
+}

策略上下文context修改为

java
public class OrderStrategyContext {
+
+    private Map<String,Class> handlerMap;
+
+    public OrderStrategyContext(Map<String,Class> handlerMap){
+        this.handlerMap = handlerMap;
+    }
+
+    public OrderStrategy getBean(String type){
+        Class clazz = handlerMap.get(type);
+        if (clazz == null) {
+            throw new IllegalArgumentException("not found handler for type :" + type);
+        }
+        return (OrderStrategy) SpringUtils.getBean(clazz);
+    }
+}
public class OrderStrategyContext {
+
+    private Map<String,Class> handlerMap;
+
+    public OrderStrategyContext(Map<String,Class> handlerMap){
+        this.handlerMap = handlerMap;
+    }
+
+    public OrderStrategy getBean(String type){
+        Class clazz = handlerMap.get(type);
+        if (clazz == null) {
+            throw new IllegalArgumentException("not found handler for type :" + type);
+        }
+        return (OrderStrategy) SpringUtils.getBean(clazz);
+    }
+}

自定义注解后,我们需要将注解的value和对应策略类的bean_name放到上下文的handlerMap中,并将策略上下文对象注册到spring容器里,需要一个处理类HandlerProcessor

java
@Component
+public class HandlerProcessor implements BeanFactoryPostProcessor {
+
+    private static final String HANDLE_PACKAGE = "com.test.handler";
+
+    @Override
+    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
+        Map<String,Class> map = new HashMap<>();
+        ClassScaner.scan(HANDLE_PACKAGE, HandlerType.class).forEach(clazz -> {
+            //获取注解中对应的类型
+            String type = clazz.getAnnotation(HandlerType.class).value();
+            //注解的类型值作为key,对应的类作为value,存储在map中
+            map.put(type,clazz);
+        });
+        //初始化HandlerContext,注册到Spring容器中
+        OrderStrategyContext context = new OrderStrategyContext(map);
+        beanFactory.registerSingleton(OrderStrategyContext.class.getName(),context);
+    }
+}
@Component
+public class HandlerProcessor implements BeanFactoryPostProcessor {
+
+    private static final String HANDLE_PACKAGE = "com.test.handler";
+
+    @Override
+    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
+        Map<String,Class> map = new HashMap<>();
+        ClassScaner.scan(HANDLE_PACKAGE, HandlerType.class).forEach(clazz -> {
+            //获取注解中对应的类型
+            String type = clazz.getAnnotation(HandlerType.class).value();
+            //注解的类型值作为key,对应的类作为value,存储在map中
+            map.put(type,clazz);
+        });
+        //初始化HandlerContext,注册到Spring容器中
+        OrderStrategyContext context = new OrderStrategyContext(map);
+        beanFactory.registerSingleton(OrderStrategyContext.class.getName(),context);
+    }
+}

ClassScaner

java
public class ClassScaner implements ResourceLoaderAware {
+
+    private final List<TypeFilter> includeFilters = new LinkedList<TypeFilter>();
+    private final List<TypeFilter> excludeFilters = new LinkedList<TypeFilter>();
+
+    private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
+    private MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(this.resourcePatternResolver);
+
+    @SafeVarargs
+    public static Set<Class<?>> scan(String[] basePackages, Class<? extends Annotation>... annotations) {
+        ClassScaner cs = new ClassScaner();
+
+        if (ArrayUtils.isNotEmpty(annotations)) {
+            for (Class anno : annotations) {
+                cs.addIncludeFilter(new AnnotationTypeFilter(anno));
+            }
+        }
+
+        Set<Class<?>> classes = new HashSet<>();
+        for (String s : basePackages) {
+            classes.addAll(cs.doScan(s));
+        }
+
+        return classes;
+    }
+
+    @SafeVarargs
+    public static Set<Class<?>> scan(String basePackages, Class<? extends Annotation>... annotations) {
+        return ClassScaner.scan(StringUtils.tokenizeToStringArray(basePackages, ",; \t\n"), annotations);
+    }
+
+    public final ResourceLoader getResourceLoader() {
+        return this.resourcePatternResolver;
+    }
+
+    @Override
+    public void setResourceLoader(ResourceLoader resourceLoader) {
+        this.resourcePatternResolver = ResourcePatternUtils
+                .getResourcePatternResolver(resourceLoader);
+        this.metadataReaderFactory = new CachingMetadataReaderFactory(
+                resourceLoader);
+    }
+
+    public void addIncludeFilter(TypeFilter includeFilter) {
+        this.includeFilters.add(includeFilter);
+    }
+
+    public void addExcludeFilter(TypeFilter excludeFilter) {
+        this.excludeFilters.add(0, excludeFilter);
+    }
+
+    public void resetFilters(boolean useDefaultFilters) {
+        this.includeFilters.clear();
+        this.excludeFilters.clear();
+    }
+
+    public Set<Class<?>> doScan(String basePackage) {
+        Set<Class<?>> classes = new HashSet<>();
+        try {
+            String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
+                    + org.springframework.util.ClassUtils
+                    .convertClassNameToResourcePath(SystemPropertyUtils
+                            .resolvePlaceholders(basePackage))
+                    + "/**/*.class";
+            Resource[] resources = this.resourcePatternResolver
+                    .getResources(packageSearchPath);
+
+            for (int i = 0; i < resources.length; i++) {
+                Resource resource = resources[i];
+                if (resource.isReadable()) {
+                    MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
+                    if ((includeFilters.size() == 0 && excludeFilters.size() == 0) || matches(metadataReader)) {
+                        try {
+                            classes.add(Class.forName(metadataReader
+                                    .getClassMetadata().getClassName()));
+                        } catch (ClassNotFoundException e) {
+                            e.printStackTrace();
+                        }
+                    }
+                }
+            }
+        } catch (IOException ex) {
+            throw new BeanDefinitionStoreException(
+                    "I/O failure during classpath scanning", ex);
+        }
+        return classes;
+    }
+
+    protected boolean matches(MetadataReader metadataReader) throws IOException {
+        for (TypeFilter tf : this.excludeFilters) {
+            if (tf.match(metadataReader, this.metadataReaderFactory)) {
+                return false;
+            }
+        }
+        for (TypeFilter tf : this.includeFilters) {
+            if (tf.match(metadataReader, this.metadataReaderFactory)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
public class ClassScaner implements ResourceLoaderAware {
+
+    private final List<TypeFilter> includeFilters = new LinkedList<TypeFilter>();
+    private final List<TypeFilter> excludeFilters = new LinkedList<TypeFilter>();
+
+    private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
+    private MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(this.resourcePatternResolver);
+
+    @SafeVarargs
+    public static Set<Class<?>> scan(String[] basePackages, Class<? extends Annotation>... annotations) {
+        ClassScaner cs = new ClassScaner();
+
+        if (ArrayUtils.isNotEmpty(annotations)) {
+            for (Class anno : annotations) {
+                cs.addIncludeFilter(new AnnotationTypeFilter(anno));
+            }
+        }
+
+        Set<Class<?>> classes = new HashSet<>();
+        for (String s : basePackages) {
+            classes.addAll(cs.doScan(s));
+        }
+
+        return classes;
+    }
+
+    @SafeVarargs
+    public static Set<Class<?>> scan(String basePackages, Class<? extends Annotation>... annotations) {
+        return ClassScaner.scan(StringUtils.tokenizeToStringArray(basePackages, ",; \t\n"), annotations);
+    }
+
+    public final ResourceLoader getResourceLoader() {
+        return this.resourcePatternResolver;
+    }
+
+    @Override
+    public void setResourceLoader(ResourceLoader resourceLoader) {
+        this.resourcePatternResolver = ResourcePatternUtils
+                .getResourcePatternResolver(resourceLoader);
+        this.metadataReaderFactory = new CachingMetadataReaderFactory(
+                resourceLoader);
+    }
+
+    public void addIncludeFilter(TypeFilter includeFilter) {
+        this.includeFilters.add(includeFilter);
+    }
+
+    public void addExcludeFilter(TypeFilter excludeFilter) {
+        this.excludeFilters.add(0, excludeFilter);
+    }
+
+    public void resetFilters(boolean useDefaultFilters) {
+        this.includeFilters.clear();
+        this.excludeFilters.clear();
+    }
+
+    public Set<Class<?>> doScan(String basePackage) {
+        Set<Class<?>> classes = new HashSet<>();
+        try {
+            String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
+                    + org.springframework.util.ClassUtils
+                    .convertClassNameToResourcePath(SystemPropertyUtils
+                            .resolvePlaceholders(basePackage))
+                    + "/**/*.class";
+            Resource[] resources = this.resourcePatternResolver
+                    .getResources(packageSearchPath);
+
+            for (int i = 0; i < resources.length; i++) {
+                Resource resource = resources[i];
+                if (resource.isReadable()) {
+                    MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
+                    if ((includeFilters.size() == 0 && excludeFilters.size() == 0) || matches(metadataReader)) {
+                        try {
+                            classes.add(Class.forName(metadataReader
+                                    .getClassMetadata().getClassName()));
+                        } catch (ClassNotFoundException e) {
+                            e.printStackTrace();
+                        }
+                    }
+                }
+            }
+        } catch (IOException ex) {
+            throw new BeanDefinitionStoreException(
+                    "I/O failure during classpath scanning", ex);
+        }
+        return classes;
+    }
+
+    protected boolean matches(MetadataReader metadataReader) throws IOException {
+        for (TypeFilter tf : this.excludeFilters) {
+            if (tf.match(metadataReader, this.metadataReaderFactory)) {
+                return false;
+            }
+        }
+        for (TypeFilter tf : this.includeFilters) {
+            if (tf.match(metadataReader, this.metadataReaderFactory)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}

第三种方案

扫描指定的包还是很麻烦,还可以直接使用ioc容器来直接进行操作 将OrderStrategyContext进行修改,不再需要processor类

java
@Component
+public class OrderStrategyContext implements ApplicationContextAware, CommandLineRunner {
+
+    private Map<String,Object> handlerMap = new HashMap<>();
+
+    public OrderStrategy getInstance(String type) {
+        Object obj = handlerMap.get(type);
+        if (obj == null) {
+            throw new IllegalArgumentException("handler not found for type : " + type);
+        }
+        if (obj instanceof OrderStrategy) {
+            return (OrderStrategy) obj;
+        } else {
+            throw new IllegalArgumentException("handler not found for type : " + type);
+        }
+    }
+
+    private ApplicationContext context;
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        this.context = applicationContext;
+    }
+
+
+    @Override
+    public void run(String... args) throws Exception {
+        this.loadBean();
+    }
+
+    public void loadBean() {
+        Map<String, Object> beansWithAnnotation = context.getBeansWithAnnotation(HandlerType.class);
+        beansWithAnnotation.forEach((handlerBeanName,handlerBean)->{
+            Class<?> clazz = handlerBean.getClass();
+            HandlerType annotation = clazz.getAnnotation(HandlerType.class);
+            String annotationValue = annotation.value();
+            handlerMap.put(annotationValue,handlerBean);
+        });
+    }
+}
@Component
+public class OrderStrategyContext implements ApplicationContextAware, CommandLineRunner {
+
+    private Map<String,Object> handlerMap = new HashMap<>();
+
+    public OrderStrategy getInstance(String type) {
+        Object obj = handlerMap.get(type);
+        if (obj == null) {
+            throw new IllegalArgumentException("handler not found for type : " + type);
+        }
+        if (obj instanceof OrderStrategy) {
+            return (OrderStrategy) obj;
+        } else {
+            throw new IllegalArgumentException("handler not found for type : " + type);
+        }
+    }
+
+    private ApplicationContext context;
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        this.context = applicationContext;
+    }
+
+
+    @Override
+    public void run(String... args) throws Exception {
+        this.loadBean();
+    }
+
+    public void loadBean() {
+        Map<String, Object> beansWithAnnotation = context.getBeansWithAnnotation(HandlerType.class);
+        beansWithAnnotation.forEach((handlerBeanName,handlerBean)->{
+            Class<?> clazz = handlerBean.getClass();
+            HandlerType annotation = clazz.getAnnotation(HandlerType.class);
+            String annotationValue = annotation.value();
+            handlerMap.put(annotationValue,handlerBean);
+        });
+    }
+}

启动项目并测试

1.jpg3.jpg

+ + + + \ No newline at end of file diff --git "a/actions/designpattern/\350\264\243\344\273\273\351\223\276\346\250\241\345\274\217.html" "b/actions/designpattern/\350\264\243\344\273\273\351\223\276\346\250\241\345\274\217.html" new file mode 100644 index 000000000..6b4b8466c --- /dev/null +++ "b/actions/designpattern/\350\264\243\344\273\273\351\223\276\346\250\241\345\274\217.html" @@ -0,0 +1,125 @@ + + + + + + 责任链模式 | 故事 + + + + + + + + + + + + + + +
Skip to content

责任链模式

背景

根据不同的订单结算金额规则配置,来计算出每一条订单商品的结算金额,每条规则都有自己的匹配条件,匹配上的则应用该条规则所配置的结算金额计算公式。客户可以配置多条不同的规则,组成一条规则链。每条订单商品记录按规则的顺序依次进行条件匹配,如果匹配上则停止,匹配不上继续,直至规则链结束,没有匹配则使用默认规则进行兜底。

责任链模式

责任链模式(Chain of Responsibility Pattern)是一种软件设计模式,它可以让多个对象都有机会处理请求,从而避免了请求的发送者和接收者之间的耦合关系。在责任链模式中,每个对象都有其对应的处理请求的方法,如果一个对象不能够处理该请求,那么它会将这个请求传递给下一个对象来处理,直到找到能够处理该请求的对象为止。

责任链模式通常由以下几个角色组成:

  1. 抽象处理者(Handler):定义了处理请求的接口,同时也可以实现一些公共的处理逻辑;
  2. 具体处理者(Concrete Handler):继承自抽象处理者,实现了具体的处理方法,如果能够处理该请求,则处理请求;否则将请求转发给下一个处理者;
  3. 客户端(Client):创建请求对象,并将请求对象传递给第一个处理者;
  4. 请求对象(Request):封装了需要处理的数据和请求类型等信息。

责任链模式的优点在于它能够降低系统的耦合度,增强系统的可扩展性和灵活性。同时,由于责任链模式中的处理者之间是松散耦合的,因此可以方便地增加或删除处理者,而不会影响到其他部分的功能。

实现

上述需求,其实就是典型的责任链模式的应用场景。而责任链模式一般有两种实现:指针和集合的方式。

指针模式是最常见的责任链模式实现方式之一。在这种模式下,每个处理对象都持有一个指向下一个处理对象的引用,形成一个链表。当请求到达一个处理对象时,如果该对象无法处理该请求,则将请求传递给链表中的下一个对象,直到找到能够处理该请求的对象为止。

集合模式是另一种责任链模式实现方式。与指针模式不同的是,集合模式下,所有的处理对象被封装在一个集合中,每个对象都具有相同的处理机会。当请求到达集合时,集合中的每个对象都有机会去处理请求,直到有一个对象成功地处理了请求或者所有对象都无法处理该请求为止。

责任链

java
public abstract class HandlerChain<T, R extends Rule> {
+
+    protected List<Handler<T, R>> handlers;
+
+    public void setHandlers(List<Handler<T, R>> handlers) {
+        this.handlers = handlers;
+    }
+
+    public List<Handler<T, R>> getHandlers() {
+        return handlers;
+    }
+
+    public void handle(T t) {
+        if (CollUtil.isNotEmpty(handlers)) {
+            for (Handler<T, R> handler : handlers) {
+                if (!handler.handle(t)) {
+                    break;
+                }
+            }
+        }
+    }
+
+    public void clear() {
+        if (CollUtil.isNotEmpty(handlers)) {
+            handlers.clear();
+        }
+    }
+}
public abstract class HandlerChain<T, R extends Rule> {
+
+    protected List<Handler<T, R>> handlers;
+
+    public void setHandlers(List<Handler<T, R>> handlers) {
+        this.handlers = handlers;
+    }
+
+    public List<Handler<T, R>> getHandlers() {
+        return handlers;
+    }
+
+    public void handle(T t) {
+        if (CollUtil.isNotEmpty(handlers)) {
+            for (Handler<T, R> handler : handlers) {
+                if (!handler.handle(t)) {
+                    break;
+                }
+            }
+        }
+    }
+
+    public void clear() {
+        if (CollUtil.isNotEmpty(handlers)) {
+            handlers.clear();
+        }
+    }
+}

处理器

java
public abstract class Handler<T, R extends Rule> {
+
+    protected CommonDynamicParam param;
+    protected R rule;
+
+    public abstract boolean handle(T t);
+
+    public CommonDynamicParam getParam() {
+        return param;
+    }
+
+    public void setParam(CommonDynamicParam param) {
+        this.param = param;
+    }
+
+    public R getRule() {
+        return rule;
+    }
+
+    public void setRule(R rule) {
+        this.rule = rule;
+        this.param = JSON.parseObject(rule.getSettlementCondition(), CommonDynamicParam.class);
+    }
+}
public abstract class Handler<T, R extends Rule> {
+
+    protected CommonDynamicParam param;
+    protected R rule;
+
+    public abstract boolean handle(T t);
+
+    public CommonDynamicParam getParam() {
+        return param;
+    }
+
+    public void setParam(CommonDynamicParam param) {
+        this.param = param;
+    }
+
+    public R getRule() {
+        return rule;
+    }
+
+    public void setRule(R rule) {
+        this.rule = rule;
+        this.param = JSON.parseObject(rule.getSettlementCondition(), CommonDynamicParam.class);
+    }
+}

大致流程

  1. 实现具体的Handler处理器逻辑(handle)

  2. 初始化处理器集合List<Handler>,并交给责任链对象HandlerChain管理

  3. 调用责任链的handle方法处理对象

+ + + + \ No newline at end of file diff --git "a/actions/env/Docker\345\256\271\345\231\250\345\206\205\350\256\277\351\227\256MacOS\345\256\277\344\270\273\346\234\272\344\270\255\347\232\204kafka.html" "b/actions/env/Docker\345\256\271\345\231\250\345\206\205\350\256\277\351\227\256MacOS\345\256\277\344\270\273\346\234\272\344\270\255\347\232\204kafka.html" new file mode 100644 index 000000000..61e13fca3 --- /dev/null +++ "b/actions/env/Docker\345\256\271\345\231\250\345\206\205\350\256\277\351\227\256MacOS\345\256\277\344\270\273\346\234\272\344\270\255\347\232\204kafka.html" @@ -0,0 +1,31 @@ + + + + + + Docker容器内访问MacOS宿主机中的kafka | 故事 + + + + + + + + + + + + + + +
Skip to content

Docker容器内访问MacOS宿主机中的kafka

kafka配置

properties
# server.properties
+listeners=PLAINTEXT://:9092
+advertised.listeners=PLAINTEXT://host.docker.internal:9092
# server.properties
+listeners=PLAINTEXT://:9092
+advertised.listeners=PLAINTEXT://host.docker.internal:9092

/etc/hosts配置

bash
127.0.0.1 host.docker.internal
127.0.0.1 host.docker.internal

验证

shell
# List brokers and topics in cluster
+$ docker run -it --rm --network=host --platform=linux/amd64 edenhill/kcat:1.7.1 -b host.docker.internal -L
# List brokers and topics in cluster
+$ docker run -it --rm --network=host --platform=linux/amd64 edenhill/kcat:1.7.1 -b host.docker.internal -L

原理

连接kafka的broker过程

  • kafka客户端通过bootstrap.servers配置的地址去连接kafka的broker
  • broker会返回advertised.listeners配置的地址给kafka客户端
  • kafka客户端会通过advertised.listeners配置的地址去连接kafka的broker

分析

容器内连接

如果kafka只配置listeners=PLAINTEXT://:9092而不配置advertised.listeners=PLAINTEXT://host.docker.internal:9092,那么kafka容器内部的客户端是无法访问到kafka的,因为kafka容器内部的客户端会通过advertised.listeners配置的地址去访问kafka,而默认的advertised.listeners配置和listeners一致,也是PLAINTEXT://:9092,对于容器内的客户端来说这个地址是容器内部的地址,所以容器内部的客户端无法访问到宿主机kafka。因此要加上advertised.listeners=PLAINTEXT://host.docker.internal:9092

宿主机上连接

因为配置了advertised.listeners=PLAINTEXT://host.docker.internal:9092,所以宿主机上的客户端也会通过host.docker.internal:9092访问kafka的broker,所以要在/etc/hosts中增加解析host.docker.internal到本地回环地址的配置。

+ + + + \ No newline at end of file diff --git "a/actions/env/Windows\344\270\213Docker Desktop\345\256\211\350\243\205.html" "b/actions/env/Windows\344\270\213Docker Desktop\345\256\211\350\243\205.html" new file mode 100644 index 000000000..9dc2c393e --- /dev/null +++ "b/actions/env/Windows\344\270\213Docker Desktop\345\256\211\350\243\205.html" @@ -0,0 +1,25 @@ + + + + + + Windows下Docker Desktop安装 | 故事 + + + + + + + + + + + + + + +
Skip to content

Windows下Docker Desktop安装

  1. 官网下载安装包Docker Desktop Installer.exe

  2. 运行安装并重启

  3. 提示WSL2的linux内核安装不完整,点击链接跟随教程操作

    image-20210427020640967

  4. 执行指引中的命令

    • 步骤 1 - 启用适用于 Linux 的 Windows 子系统dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
    • 步骤 2 - 检查运行 WSL 2 的要求,不同架构的系统版本要求不同
    • 步骤 3 - 启用虚拟机功能,前提是开启虚拟化,如何开启虚拟化不再赘述dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
    • 步骤 4 - 下载 Linux 内核更新包,地址:https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi
    • 步骤 5 - 将 WSL 2 设置为默认版本wsl --set-default-version 2
    • 步骤 6 - 安装所选的 Linux 分发,官方提供的方案是在微软商店中下载,因为网络原因很难实现,采取手动下载或命令行下载,参考地址https://docs.microsoft.com/zh-cn/windows/wsl/install-manual,下载完成后cd到下载的目录,执行Add-AppxPackage .\filename即可
      • 查看所有安装的分发版本wsl --list --all
      • 卸载指定的分发版本wsl --unregister <DistributionName>
    • 打开已安装的分发版本,在安装后会提示创建linux账户和密码
+ + + + \ No newline at end of file diff --git "a/actions/env/Windows\345\274\200\346\234\272\345\220\257\345\212\250WSL.html" "b/actions/env/Windows\345\274\200\346\234\272\345\220\257\345\212\250WSL.html" new file mode 100644 index 000000000..e7aa5c631 --- /dev/null +++ "b/actions/env/Windows\345\274\200\346\234\272\345\220\257\345\212\250WSL.html" @@ -0,0 +1,29 @@ + + + + + + Windows开机启动WSL | 故事 + + + + + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git "a/actions/env/docker+jenkins+gitee\350\207\252\345\212\250\345\214\226\351\203\250\347\275\262vue\351\241\271\347\233\256.html" "b/actions/env/docker+jenkins+gitee\350\207\252\345\212\250\345\214\226\351\203\250\347\275\262vue\351\241\271\347\233\256.html" new file mode 100644 index 000000000..b26b942a4 --- /dev/null +++ "b/actions/env/docker+jenkins+gitee\350\207\252\345\212\250\345\214\226\351\203\250\347\275\262vue\351\241\271\347\233\256.html" @@ -0,0 +1,137 @@ + + + + + + docker+jenkins+gitee自动化部署vue项目 | 故事 + + + + + + + + + + + + + + +
Skip to content

docker+jenkins+gitee自动化部署vue项目

之前个人博客一直用的travisCI部署在github page上,但是偶尔会抽风无法访问。之前一直偷懒没部署jenkins,手动部署到云服务器又比较麻烦,打包上传很浪费时间,这次就直接动手一步到位,在自己服务器上部署下jekins。

docker启动jenkins

  • 启动容器

最开始用的jenkins中文社区的镜像发现有个很恶心的问题,jenkins版本比较低而且安装了NodeJS插件后在全局工具配置中配置NodeJS安装环境时无法选择版本,所以还是官方镜像比较靠谱。

bash
# 拉取官方镜像
+docker pull jenkins/jenkins:lts
+lts: Pulling from jenkins/jenkins
+4c25b3090c26: Pull complete
+750d566fdd60: Pull complete
+2718cc36ca02: Pull complete
+5678b027ee14: Pull complete
+c839cd2df78d: Pull complete
+50861a5addda: Pull complete
+ff2b028e5cf5: Pull complete
+ee710b58f452: Pull complete
+2625c929bb0e: Pull complete
+6a6bf9181c04: Pull complete
+bee5e6792ac4: Pull complete
+6cc5edd2133e: Pull complete
+c07b16426ded: Pull complete
+e9ac42647ae3: Pull complete
+fa925738a490: Pull complete
+4a08c3886279: Pull complete
+2d43fec22b7e: Pull complete
+Digest: sha256:a942c30fc3bcf269a1c32ba27eb4a470148eff9aba086911320031a3c3943e6c
+Status: Downloaded newer image for jenkins/jenkins:lts
+docker.io/jenkins/jenkins:lts
+# 启动jenkins
+docker run --name jenkins -dp 8099:8080 -v /story/dist:/story/dist -v ~/jenkins_data:/var/jenkins_home -u root  -e TZ="Asia/Shanghai" -v /etc/localtime:/etc/localtime:ro jenkins/jenkins:lts
+# 参数说明 --name 指定容器名为jenkins -d 后台启动 -p 将容器的8080端口映射到宿主机的8099端口
+# -v 挂载宿主机目录 宿主机和容器的目录会同步 -u 指定用户为root 这里是必须的 不然后续操作文件系统会报无权限
+# 挂载时区的目录是因为镜像中的linux系统默认时区非北京时间,会导致时间显示不正确
# 拉取官方镜像
+docker pull jenkins/jenkins:lts
+lts: Pulling from jenkins/jenkins
+4c25b3090c26: Pull complete
+750d566fdd60: Pull complete
+2718cc36ca02: Pull complete
+5678b027ee14: Pull complete
+c839cd2df78d: Pull complete
+50861a5addda: Pull complete
+ff2b028e5cf5: Pull complete
+ee710b58f452: Pull complete
+2625c929bb0e: Pull complete
+6a6bf9181c04: Pull complete
+bee5e6792ac4: Pull complete
+6cc5edd2133e: Pull complete
+c07b16426ded: Pull complete
+e9ac42647ae3: Pull complete
+fa925738a490: Pull complete
+4a08c3886279: Pull complete
+2d43fec22b7e: Pull complete
+Digest: sha256:a942c30fc3bcf269a1c32ba27eb4a470148eff9aba086911320031a3c3943e6c
+Status: Downloaded newer image for jenkins/jenkins:lts
+docker.io/jenkins/jenkins:lts
+# 启动jenkins
+docker run --name jenkins -dp 8099:8080 -v /story/dist:/story/dist -v ~/jenkins_data:/var/jenkins_home -u root  -e TZ="Asia/Shanghai" -v /etc/localtime:/etc/localtime:ro jenkins/jenkins:lts
+# 参数说明 --name 指定容器名为jenkins -d 后台启动 -p 将容器的8080端口映射到宿主机的8099端口
+# -v 挂载宿主机目录 宿主机和容器的目录会同步 -u 指定用户为root 这里是必须的 不然后续操作文件系统会报无权限
+# 挂载时区的目录是因为镜像中的linux系统默认时区非北京时间,会导致时间显示不正确
  • 启动后直接访问本机8099端口:http://localhost:8099/(我这里是本地测试,实际请替换成自己的服务器地址)

apt安装

shell
curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key | sudo tee \
+  /usr/share/keyrings/jenkins-keyring.asc > /dev/null
+
+sudo sh -c 'echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian-stable binary/" > /etc/apt/sources.list.d/jenkins.list'
+
+sudo apt update
+
+# Jenkins requires Java 11 or 17 since Jenkins 2.357 and LTS 2.361.1. 
+apt install openjdk-17-jdk
+
+sudo apt install jenkins
curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key | sudo tee \
+  /usr/share/keyrings/jenkins-keyring.asc > /dev/null
+
+sudo sh -c 'echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian-stable binary/" > /etc/apt/sources.list.d/jenkins.list'
+
+sudo apt update
+
+# Jenkins requires Java 11 or 17 since Jenkins 2.357 and LTS 2.361.1. 
+apt install openjdk-17-jdk
+
+sudo apt install jenkins

image-20210914101747191

  • 进到容器中查询下密码
bash
docker exec -it jenkins /bin/bash
+cat /var/jenkins_home/secrets/initialAdminPassword
docker exec -it jenkins /bin/bash
+cat /var/jenkins_home/secrets/initialAdminPassword

或者直接在上面挂载的目录查询但要改一下路径

bash
cat ~/jenkins_data/secrets/initialAdminPassword
cat ~/jenkins_data/secrets/initialAdminPassword
  • 使用密码登录后直接安装推荐插件即可

image-20210914102028448

  • 等待安装完成

image-20210914102652130

  • 新建账户

image-20210914102716607

  • 实例配置

    image-20210914102906576

  • 完成

jenkins配置自动部署

安装node和gitee插件

Manage Jenkins ---> Manage Plugins ---> 可选插件分别搜索gitee和nodejs image-20210914111457469

image-20210914111526455

选择install without restart

安装完毕后返回工作台

配置node环境

Manage Jenkins ---> Global Tool Configuration ---> NodeJS

新增NodeJS取别名后保存即可

image-20210914111718160

新建自动部署任务

新建任务

工作台点击新建Item,输入任务名称后选择freestyle project确定

image-20210914111819597

配置gitee相关内容

源码管理

  • 添加gitee仓库地址,指定要打包的分支

image-20210914112017978

构建触发器

image-20210914112740392

  • 生成webhook密码

image-20210914112804397

  • 去gitee仓库中配置webhook内容

    仓库的管理tab页添加webhook

    image-20210914112946461

url和webhook密码分别填写后保存

image-20210914113229679

在这个页面点击测试,如果看到xxx has been accepted即为成功。

构建环境

选择前面已经配置好的node环境即可

image-20210914113502188

构建

首先在任务面板中点击立即构建,这样才会生成工作空间

image-20210914114129450

我这里选择执行shell

image-20210914113546293

然后就是写个简单的脚本执行打包,替换的工作

image-20210914113643970

第一步cd进入的目录是当前任务的工作空间,这里要把vuepress替换成自己的任务名称即可

TIP

这里涉及到文件系统操作的内容rm cp等命令需要root用户才能执行,所以在启动docker容器的时候必须使用-u root参数指定root用户,否则打包会失败,操作文件时会提示无权限

配置完毕保存即可

测试

点击立即构建或者往gitee仓库推送一次更新即可触发构建任务,然后等待构建完成即可。

image-20210914114542885

如果是第一次执行构建,jenkins还会自动安装解压nodejs。

通过脚本替换完打包好的dist后,通过nginx配置部署静态项目即可。

jenkins打包跨平台docker镜像并推送镜像仓库

这里是用宿主机直接安装的jenkins

  • 把jenkins用户添加到docker组中然后重启jenkins:sudo usermod -aG docker jenkins

    getent group groupname 查看组中有哪些用户

  • shell

shell
#!/bin/sh
+cd /var/lib/jenkins/workspace/xxx
+node -v
+npm -v
+docker -v
+
+npm install
+npm run build
+docker buildx ls
+docker buildx create --use --name jenkinsbuilder
+docker buildx ls
+
+# Dockerfile
+cat > Dockerfile <<EOF
+// doSomething
+EOF
+
+docker login xxx.com --username='xxx' --password=='xxx'
+docker buildx build --platform linux/amd64,linux/arm64 -t xxx/xxx . --push
#!/bin/sh
+cd /var/lib/jenkins/workspace/xxx
+node -v
+npm -v
+docker -v
+
+npm install
+npm run build
+docker buildx ls
+docker buildx create --use --name jenkinsbuilder
+docker buildx ls
+
+# Dockerfile
+cat > Dockerfile <<EOF
+// doSomething
+EOF
+
+docker login xxx.com --username='xxx' --password=='xxx'
+docker buildx build --platform linux/amd64,linux/arm64 -t xxx/xxx . --push

阻止jenkins杀死衍生进程

Execute shell中添加如下变量

shell
BUILD_ID=DONTKILLME
BUILD_ID=DONTKILLME
+ + + + \ No newline at end of file diff --git "a/actions/env/git\351\205\215\347\275\256socks5\344\273\243\347\220\206\350\247\243\345\206\263github\344\270\212down\344\273\243\347\240\201\346\205\242\347\232\204\351\227\256\351\242\230.html" "b/actions/env/git\351\205\215\347\275\256socks5\344\273\243\347\220\206\350\247\243\345\206\263github\344\270\212down\344\273\243\347\240\201\346\205\242\347\232\204\351\227\256\351\242\230.html" new file mode 100644 index 000000000..beecac119 --- /dev/null +++ "b/actions/env/git\351\205\215\347\275\256socks5\344\273\243\347\220\206\350\247\243\345\206\263github\344\270\212down\344\273\243\347\240\201\346\205\242\347\232\204\351\227\256\351\242\230.html" @@ -0,0 +1,41 @@ + + + + + + git配置socks5代理解决github上down代码慢的问题 | 故事 + + + + + + + + + + + + + + +
Skip to content

git配置socks5代理解决github上down代码慢的问题

bash
# 设置代理
+git config --global http.proxy 'socks5://127.0.0.1:10880'
+
+git config --global https.proxy 'socks5://127.0.0.1:10880'
+
+# 取消代理
+git config --global --unset http.proxy
+
+git config --global --unset https.proxy
# 设置代理
+git config --global http.proxy 'socks5://127.0.0.1:10880'
+
+git config --global https.proxy 'socks5://127.0.0.1:10880'
+
+# 取消代理
+git config --global --unset http.proxy
+
+git config --global --unset https.proxy

端口号根据自己本地的代理端口填写

+ + + + \ No newline at end of file diff --git "a/actions/env/git\351\205\215\347\275\256\345\244\232ssh-key && Gitee \345\222\214 Github \345\220\214\346\255\245\346\233\264\346\226\260.html" "b/actions/env/git\351\205\215\347\275\256\345\244\232ssh-key && Gitee \345\222\214 Github \345\220\214\346\255\245\346\233\264\346\226\260.html" new file mode 100644 index 000000000..bbde7ed73 --- /dev/null +++ "b/actions/env/git\351\205\215\347\275\256\345\244\232ssh-key && Gitee \345\222\214 Github \345\220\214\346\255\245\346\233\264\346\226\260.html" @@ -0,0 +1,55 @@ + + + + + + git配置多ssh-key && Gitee 和 Github 同步更新 | 故事 + + + + + + + + + + + + + + +
Skip to content

git配置多ssh-key && Gitee 和 Github 同步更新

配置多ssh-key

gitee或者gitlab账号和个人git账号同时在一台机器上使用时,可以为不同git服务器设置不同的ssh-key

  1. 生成一个个人github的ssh-key

    ssh-keygen -t rsa -C 'xxxxx@163.com' -f ~/.ssh/github_id_rsa

  2. 生成一个gitee的ssh-key

    ssh-keygen -t rsa -C 'xxxxx@company.cn' -f ~/.ssh/gitee_id_rsa

  3. ~/.ssh下新建config文件vim ~/.ssh/config,添加以下内容

    txt
    # gitee
    +Host gitee.com
    +HostName gitee.com
    +PreferredAuthentications publickey
    +IdentityFile ~/.ssh/gitee_id_rsa
    +# github
    +Host github.com
    +HostName github.com
    +PreferredAuthentications publickey
    +IdentityFile ~/.ssh/github_id_rsa
    # gitee
    +Host gitee.com
    +HostName gitee.com
    +PreferredAuthentications publickey
    +IdentityFile ~/.ssh/gitee_id_rsa
    +# github
    +Host github.com
    +HostName github.com
    +PreferredAuthentications publickey
    +IdentityFile ~/.ssh/github_id_rsa
  4. 分别在gitee和github中添加前两步生成的对应地址的公钥

  5. ssh命令测试

    bash
    ssh -T git@gitee.com
    +ssh -T git@github.com
    ssh -T git@gitee.com
    +ssh -T git@github.com

如果看到 hi xxx!。。。内容则证明配置成功

Gitee 和 Github 同步更新

假设我们有一个项目同时在github和gitee上都有仓库,当直接使用git clone命令拉取的代码默认remote为origin,如果要分别更新,我们要分别在两个本地仓库中push。这时我们可以给本地仓库添加多个origin,然后更新的时候分别推送即可实现一个本地仓库分别推送两个不同的远程仓库。

  1. 删除原有的remote地址

    git remote remove origin

  2. 添加新的远程仓库地址(gitee)

    bash
    git remote add 远程仓库名 远程仓库地址
    +eg: git remote add gitee git@gitee.com:xxx/xxx.git
    git remote add 远程仓库名 远程仓库地址
    +eg: git remote add gitee git@gitee.com:xxx/xxx.git
  3. 添加新的远程仓库地址(github)

    bash
    git remote add 远程仓库名 远程仓库地址
    +eg: git remote add github git@github.com:xxx/xxx.git
    git remote add 远程仓库名 远程仓库地址
    +eg: git remote add github git@github.com:xxx/xxx.git

    再次查看git remote:

    image-20210913184204396

  4. 推送的时候git push 远程仓库名即可

修改配置文件一次推送多个仓库

修改仓库下.git/config文件,新增内容

sh
[remote "all"]
+        url = repo1.git
+        url = repo2.git
+        url = repo3.git
[remote "all"]
+        url = repo1.git
+        url = repo2.git
+        url = repo3.git

直接git push all

+ + + + \ No newline at end of file diff --git "a/actions/env/macOS\345\274\200\345\220\257\347\273\210\347\253\257\347\232\204\344\273\243\347\220\206.html" "b/actions/env/macOS\345\274\200\345\220\257\347\273\210\347\253\257\347\232\204\344\273\243\347\220\206.html" new file mode 100644 index 000000000..82d53c88a --- /dev/null +++ "b/actions/env/macOS\345\274\200\345\220\257\347\273\210\347\253\257\347\232\204\344\273\243\347\220\206.html" @@ -0,0 +1,27 @@ + + + + + + macOS开启终端的代理 | 故事 + + + + + + + + + + + + + + +
Skip to content

macOS开启终端的代理

例如要走socks5的代理

  • 直接在终端运行
bash
export https_proxy=socks5://127.0.0.1:10880
export https_proxy=socks5://127.0.0.1:10880

这样配置只对当前终端有效,不会影响其他

  • 修改终端配置文件 例如.zshrc
bash
export http_proxy=socks5://127.0.0.1:10880
+export https_proxy=socks5://127.0.0.1:10880
export http_proxy=socks5://127.0.0.1:10880
+export https_proxy=socks5://127.0.0.1:10880

修改后保存,然后source ~/.zshrc立即成效。重启终端后即可全局代理。

也可以通过alias建立个别名,这样可以快速开启代理,编辑.zshrc 添加

alias proxy_on='export https_proxy=socks5://127.0.0.1:10880'

+ + + + \ No newline at end of file diff --git "a/actions/env/macos\345\274\200\346\234\272\350\207\252\345\212\250\346\211\247\350\241\214\350\204\232\346\234\254.html" "b/actions/env/macos\345\274\200\346\234\272\350\207\252\345\212\250\346\211\247\350\241\214\350\204\232\346\234\254.html" new file mode 100644 index 000000000..f6ee715dc --- /dev/null +++ "b/actions/env/macos\345\274\200\346\234\272\350\207\252\345\212\250\346\211\247\350\241\214\350\204\232\346\234\254.html" @@ -0,0 +1,69 @@ + + + + + + macos开机自动执行脚本 | 故事 + + + + + + + + + + + + + + +
Skip to content

macos开机自动执行脚本

linux开机启动可以用systemd很方便的实现,mac上稍微复杂一些,需要自己写个.plist文件

简介

launchd 是 Mac OS 下用于初始化系统环境的关键进程,它是内核装载成功之后在 OS 环境下启动的第一个进程,可以用来控制服务的自动启动或者关闭。

它的作用就是我们平时说的守护进程,简单来说,用户守护进程是作为系统的一部分运行在后台的非图形化程序。

采用这种方式来配置自启动项很简单,只需要一个 plist 文件,该文件存在的目录有:

用户登陆前 LaunchDaemons:

~/Library/LaunchDaemons

用户登录后 LaunchAgents:

~/Library/LaunchAgents

脚本

xml
<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN""http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+    <dict>
+        <key>KeepAlive</key>
+        <dict>
+            <key>SuccessfulExit</key>
+            <false/>
+        </dict>
+        <key>Label</key>
+        <string>com.storyxc.frpc</string>
+        <key>ProgramArguments</key>
+        <array>
+            <string>/Users/story/project/widget/frp/frpc</string>
+            <string>-c</string>
+            <string>/Users/story/project/widget/frp/frpc.ini</string>
+        </array>
+        <key>RunAtLoad</key>
+        <true/>
+    </dict>
+</plist>
<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN""http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+    <dict>
+        <key>KeepAlive</key>
+        <dict>
+            <key>SuccessfulExit</key>
+            <false/>
+        </dict>
+        <key>Label</key>
+        <string>com.storyxc.frpc</string>
+        <key>ProgramArguments</key>
+        <array>
+            <string>/Users/story/project/widget/frp/frpc</string>
+            <string>-c</string>
+            <string>/Users/story/project/widget/frp/frpc.ini</string>
+        </array>
+        <key>RunAtLoad</key>
+        <true/>
+    </dict>
+</plist>

将脚本命名为frpc.plist,然后移动到~/Library/LaunchAgents/

载入plist文件

启动服务:

launchctl [load|enable|bootstrap] -w plist_path

卸载服务:

launchctl [unload|disable|bootout] -w plist_path

设置别名

zsh
# frpc启动、停止
+alias frpc.start='launchctl load -w ~/Library/LaunchAgents/frpc.plist'
+alias frpc.stop='launchctl unload -w ~/Library/LaunchAgents/frpc.plist'
# frpc启动、停止
+alias frpc.start='launchctl load -w ~/Library/LaunchAgents/frpc.plist'
+alias frpc.stop='launchctl unload -w ~/Library/LaunchAgents/frpc.plist'
+ + + + \ No newline at end of file diff --git "a/actions/env/mysql\345\220\257\345\212\250\346\212\245\351\224\231\346\216\222\346\237\245\345\217\212\345\244\204\347\220\206.html" "b/actions/env/mysql\345\220\257\345\212\250\346\212\245\351\224\231\346\216\222\346\237\245\345\217\212\345\244\204\347\220\206.html" new file mode 100644 index 000000000..9202adb86 --- /dev/null +++ "b/actions/env/mysql\345\220\257\345\212\250\346\212\245\351\224\231\346\216\222\346\237\245\345\217\212\345\244\204\347\220\206.html" @@ -0,0 +1,157 @@ + + + + + + mysql启动报错排查及处理 | 故事 + + + + + + + + + + + + + + +
Skip to content

mysql启动报错排查及处理

今天访问我自己的老博客(www.storyxc.com )发现网站挂掉了,ssh上去看了一下nginx和我自己的java后台博客服务都挂掉了,可能是阿里云抽风服务器重启了。然后重启了nignx和服务访问了一下,查询一直在pending,再去看后台日志,发现获取不到连接,估计是mysql也挂了。

txt
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException: 
+### Error querying database.  Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is com.mysql.cj.jdbc.exceptions.Communi
+cationsException: Communications link failure
+
+The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
+### The error may exist in class path resource [mapper/ArticleDao.xml]
+### The error may involve com.storyxc.mapper.ArticleDao.queryHotArticle
+### The error occurred while executing a query
+### Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is com.mysql.cj.jdbc.exceptions.CommunicationsException: Communic
+ations link failure
+
+The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
+        at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:77) ~[mybatis-spring-1.3.1.jar!/:1.3.1]
+        at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446) ~[mybatis-spring-1.3.1.jar!/:1.3.1]
+        at com.sun.proxy.$Proxy61.selectList(Unknown Source) ~[na:na]
+        at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:230) ~[mybatis-spring-1.3.1.jar!/:1.3.1]
+        at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:137) ~[mybatis-3.4.5.jar!/:3.4.5]
+        at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:75) ~[mybatis-3.4.5.jar!/:3.4.5]
+        at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59) ~[mybatis-3.4.5.jar!/:3.4.5]
+        at com.sun.proxy.$Proxy82.queryHotArticle(Unknown Source) ~[na:na]
+        at com.storyxc.service.impl.ArticleServiceImpl.queryHotArticle(ArticleServiceImpl.java:113) ~[classes!/:1.0-SNAPSHOT]
+        at com.storyxc.service.impl.ArticleServiceImpl$$FastClassBySpringCGLIB$$edb0e759.invoke(<generated>) ~[classes!/:1.0-SNAPSHOT]
+        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.1.6.RELEASE.jar!/:5.1.6.RELEASE]
+        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:684) ~[spring-aop-5.1.6.RELEASE.jar!/:5.1.6.RELEASE]
+        at com.storyxc.service.impl.ArticleServiceImpl$$EnhancerBySpringCGLIB$$5695fd69.queryHotArticle(<generated>) ~[classes!/:1.0-SNAPSHOT]
+        at com.storyxc.controller.ArticleController.queryHotArticle(ArticleController.java:73) ~[classes!/:1.0-SNAPSHOT]
+        at com.storyxc.controller.ArticleController$$FastClassBySpringCGLIB$$954e681b.invoke(<generated>) ~[classes!/:1.0-SNAPSHOT]
+        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.1.6.RELEASE.jar!/:5.1.6.RELEASE]
+        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:684) ~[spring-aop-5.1.6.RELEASE.jar!/:5.1.6.RELEASE]
+        at com.storyxc.controller.ArticleController$$EnhancerBySpringCGLIB$$7f3f634c.queryHotArticle(<generated>) ~[classes!/:1.0-SNAPSHOT]
+        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_282]
+        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_282]
+        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_282]
+        at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_282]
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException: 
+### Error querying database.  Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is com.mysql.cj.jdbc.exceptions.Communi
+cationsException: Communications link failure
+
+The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
+### The error may exist in class path resource [mapper/ArticleDao.xml]
+### The error may involve com.storyxc.mapper.ArticleDao.queryHotArticle
+### The error occurred while executing a query
+### Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is com.mysql.cj.jdbc.exceptions.CommunicationsException: Communic
+ations link failure
+
+The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
+        at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:77) ~[mybatis-spring-1.3.1.jar!/:1.3.1]
+        at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446) ~[mybatis-spring-1.3.1.jar!/:1.3.1]
+        at com.sun.proxy.$Proxy61.selectList(Unknown Source) ~[na:na]
+        at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:230) ~[mybatis-spring-1.3.1.jar!/:1.3.1]
+        at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:137) ~[mybatis-3.4.5.jar!/:3.4.5]
+        at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:75) ~[mybatis-3.4.5.jar!/:3.4.5]
+        at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59) ~[mybatis-3.4.5.jar!/:3.4.5]
+        at com.sun.proxy.$Proxy82.queryHotArticle(Unknown Source) ~[na:na]
+        at com.storyxc.service.impl.ArticleServiceImpl.queryHotArticle(ArticleServiceImpl.java:113) ~[classes!/:1.0-SNAPSHOT]
+        at com.storyxc.service.impl.ArticleServiceImpl$$FastClassBySpringCGLIB$$edb0e759.invoke(<generated>) ~[classes!/:1.0-SNAPSHOT]
+        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.1.6.RELEASE.jar!/:5.1.6.RELEASE]
+        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:684) ~[spring-aop-5.1.6.RELEASE.jar!/:5.1.6.RELEASE]
+        at com.storyxc.service.impl.ArticleServiceImpl$$EnhancerBySpringCGLIB$$5695fd69.queryHotArticle(<generated>) ~[classes!/:1.0-SNAPSHOT]
+        at com.storyxc.controller.ArticleController.queryHotArticle(ArticleController.java:73) ~[classes!/:1.0-SNAPSHOT]
+        at com.storyxc.controller.ArticleController$$FastClassBySpringCGLIB$$954e681b.invoke(<generated>) ~[classes!/:1.0-SNAPSHOT]
+        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.1.6.RELEASE.jar!/:5.1.6.RELEASE]
+        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:684) ~[spring-aop-5.1.6.RELEASE.jar!/:5.1.6.RELEASE]
+        at com.storyxc.controller.ArticleController$$EnhancerBySpringCGLIB$$7f3f634c.queryHotArticle(<generated>) ~[classes!/:1.0-SNAPSHOT]
+        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_282]
+        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_282]
+        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_282]
+        at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_282]

然后尝试重启mysql:service mysql start

直接报错:Starting MySQL...The server quit without updating PID file [FAILED]b/mysql/iz2ze09hymnzdn4lgmltlmz.pid).

但就这样的报错没法排查,试图看下mysql的错误日志:less /var/log/mysql/error.log结果没有,

这才想起来当时没给mysql配置错误日志路径。

给mysql配置错误文件的路径:vim /etc/my.cnf

在[mysqld]下面加一行:log_error=/var/log/mysql/error.log ,然后创建/var/log/mysql这个目录

再次启动,依旧报错,但这次我们可以去看错误日志了。继续 less /var/log/mysql/error.log

txt
210627 00:34:02 mysqld_safe Starting mysqld daemon with databases from /var/lib/mysql
+2021-06-27 00:34:03 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
+2021-06-27 00:34:03 3127 [Note] Plugin 'FEDERATED' is disabled.
+2021-06-27 00:34:03 3127 [Note] InnoDB: Using atomics to ref count buffer pool pages
+2021-06-27 00:34:03 3127 [Note] InnoDB: The InnoDB memory heap is disabled
+2021-06-27 00:34:03 3127 [Note] InnoDB: Mutexes and rw_locks use GCC atomic builtins
+2021-06-27 00:34:03 3127 [Note] InnoDB: Memory barrier is not used
+2021-06-27 00:34:03 3127 [Note] InnoDB: Compressed tables use zlib 1.2.3
+2021-06-27 00:34:03 3127 [Note] InnoDB: Using Linux native AIO
+2021-06-27 00:34:03 3127 [Note] InnoDB: Not using CPU crc32 instructions
+2021-06-27 00:34:03 3127 [Note] InnoDB: Initializing buffer pool, size = 128.0M
+InnoDB: mmap(136019968 bytes) failed; errno 12
+2021-06-27 00:34:03 3127 [ERROR] InnoDB: Cannot allocate memory for the buffer pool
+2021-06-27 00:34:03 3127 [ERROR] Plugin 'InnoDB' init function returned error.
+2021-06-27 00:34:03 3127 [ERROR] Plugin 'InnoDB' registration as a STORAGE ENGINE failed.
+2021-06-27 00:34:03 3127 [ERROR] Unknown/unsupported storage engine: InnoDB
+2021-06-27 00:34:03 3127 [ERROR] Aborting
+
+2021-06-27 00:34:03 3127 [Note] Binlog end
210627 00:34:02 mysqld_safe Starting mysqld daemon with databases from /var/lib/mysql
+2021-06-27 00:34:03 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
+2021-06-27 00:34:03 3127 [Note] Plugin 'FEDERATED' is disabled.
+2021-06-27 00:34:03 3127 [Note] InnoDB: Using atomics to ref count buffer pool pages
+2021-06-27 00:34:03 3127 [Note] InnoDB: The InnoDB memory heap is disabled
+2021-06-27 00:34:03 3127 [Note] InnoDB: Mutexes and rw_locks use GCC atomic builtins
+2021-06-27 00:34:03 3127 [Note] InnoDB: Memory barrier is not used
+2021-06-27 00:34:03 3127 [Note] InnoDB: Compressed tables use zlib 1.2.3
+2021-06-27 00:34:03 3127 [Note] InnoDB: Using Linux native AIO
+2021-06-27 00:34:03 3127 [Note] InnoDB: Not using CPU crc32 instructions
+2021-06-27 00:34:03 3127 [Note] InnoDB: Initializing buffer pool, size = 128.0M
+InnoDB: mmap(136019968 bytes) failed; errno 12
+2021-06-27 00:34:03 3127 [ERROR] InnoDB: Cannot allocate memory for the buffer pool
+2021-06-27 00:34:03 3127 [ERROR] Plugin 'InnoDB' init function returned error.
+2021-06-27 00:34:03 3127 [ERROR] Plugin 'InnoDB' registration as a STORAGE ENGINE failed.
+2021-06-27 00:34:03 3127 [ERROR] Unknown/unsupported storage engine: InnoDB
+2021-06-27 00:34:03 3127 [ERROR] Aborting
+
+2021-06-27 00:34:03 3127 [Note] Binlog end

查看内存情况:free -h,由于我买的是阿里云的轻量级应用服务器-穷逼版,只有2G内存

bash
total        used        free      shared  buff/cache   available
+     Mem:           1.8G        1.2G        245M         88M        323M        268M
+     Swap:           0B          0B          0B
total        used        free      shared  buff/cache   available
+     Mem:           1.8G        1.2G        245M         88M        323M        268M
+     Swap:           0B          0B          0B

swap为0,执行命令建立临时分区

bash
dd if=/dev/zero of=/swap bs=1M count=128  //创建一个swap文件,大小为128M
+mkswap /swap                              //将swap文件变为swap分区文件
+swapon /swap                              //将其映射为swap分区
dd if=/dev/zero of=/swap bs=1M count=128  //创建一个swap文件,大小为128M
+mkswap /swap                              //将swap文件变为swap分区文件
+swapon /swap                              //将其映射为swap分区

再次查看内存:

bash
total        used        free      shared  buff/cache   available
+Mem:           1.8G        1.3G         82M         88M        463M        236M
+Swap:          127M          0B        127M
total        used        free      shared  buff/cache   available
+Mem:           1.8G        1.3G         82M         88M        463M        236M
+Swap:          127M          0B        127M

swap分区已存在,执行命令使系统重启swap分区自动加载:vim /etc/fstab

bash
/swap swap swap defaults 0 0
/swap swap swap defaults 0 0

再次启动 还是他喵不行,执行命令查看下当前内存占用大户。

ps -eo pid,ppid,%mem,%cpu,cmd --sort=-%mem | head

bash
PID  PPID %MEM %CPU CMD
+19872     1 14.0 45.7 ./phpupdate
+20228     1 14.0 45.7 /etc/phpupdate
+ 9372     1  8.0  1.3 java -jar storyxc.jar
+27880     1  3.1  0.0 /opt/openoffice4/program/soffice.bin -headless -accept=socket,host=127.0.0.1,port=8100;urp; -nofirststartwizard
+13389     1  2.6  0.0 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
+23244 21287  2.3  4.7 ./networkmanager 15
+13322     1  1.3  0.0 /usr/bin/containerd
+  489   455  1.0  0.1 CmsGoAgent-Worker start
+19905     1  0.6  0.0 ./phpguard
PID  PPID %MEM %CPU CMD
+19872     1 14.0 45.7 ./phpupdate
+20228     1 14.0 45.7 /etc/phpupdate
+ 9372     1  8.0  1.3 java -jar storyxc.jar
+27880     1  3.1  0.0 /opt/openoffice4/program/soffice.bin -headless -accept=socket,host=127.0.0.1,port=8100;urp; -nofirststartwizard
+13389     1  2.6  0.0 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
+23244 21287  2.3  4.7 ./networkmanager 15
+13322     1  1.3  0.0 /usr/bin/containerd
+  489   455  1.0  0.1 CmsGoAgent-Worker start
+19905     1  0.6  0.0 ./phpguard

好家伙,从哪冒出来的phpupdate 直接全杀掉,世界瞬间清净了,腾出来600M的内存,

再次启动mysql,成功,问题解决。

+ + + + \ No newline at end of file diff --git "a/actions/env/\344\275\277\347\224\250github actions\350\277\233\350\241\214\346\214\201\347\273\255\351\203\250\347\275\262.html" "b/actions/env/\344\275\277\347\224\250github actions\350\277\233\350\241\214\346\214\201\347\273\255\351\203\250\347\275\262.html" new file mode 100644 index 000000000..d8189edef --- /dev/null +++ "b/actions/env/\344\275\277\347\224\250github actions\350\277\233\350\241\214\346\214\201\347\273\255\351\203\250\347\275\262.html" @@ -0,0 +1,137 @@ + + + + + + 使用github actions进行持续部署 | 故事 + + + + + + + + + + + + + + +
Skip to content

使用github actions进行持续部署

Github Actions官方文档https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#name

Github Actions是什么

Github推出的持续集成工具

使用

配置

Github Actions的配置文件叫做workflow文件,需要存放在repo根路径下的./github/workflows目录中。workflow文件使用yaml格式编写,文件名可以自定义,后缀统一为yml,一个repo中可以有多个workflow,Github只要发现./github/workflows目录中有.yml文件就会自动运行。

本博客的workflow文件:

yml
# 自定义当前执行文件的名称
+name: vuepress
+# 整个流程在main分支发生push事件时触发
+on:
+  push:
+    branches:
+      - main
+jobs:
+  build-and-deploy:
+    runs-on: ubuntu-latest # 运行在ubuntu-latest环境的虚拟机中
+    strategy:
+      matrix:              # 矩阵
+        node-version: [10.x]
+    steps: # 每个 job 由多个 step 构成,它会从上至下依次执行。
+    # 获取仓库源码
+    - name: Checkout
+      uses: actions/checkout@v2         # github actions提供了一些官方的action,例如checkout @v2是action的版本
+    # 安装node
+    - name: Use Node.js ${{ matrix.node-version }} # 定义好的node版本
+      uses: actions/setup-node@v1 # 作用:安装nodejs
+      with:
+        node-version: ${{ matrix.node-version }} # 定义好的node版本
+    # 构建和部署
+    - name: Deploy
+      env: # 环境变量
+        GITHUB_TOKEN: ${{ secrets.vuepress_actions_access_token }}
+      run: npm install && npm run deploy # npm run deploy需要在package.json中定义"deploy: bash deploy.sh"
# 自定义当前执行文件的名称
+name: vuepress
+# 整个流程在main分支发生push事件时触发
+on:
+  push:
+    branches:
+      - main
+jobs:
+  build-and-deploy:
+    runs-on: ubuntu-latest # 运行在ubuntu-latest环境的虚拟机中
+    strategy:
+      matrix:              # 矩阵
+        node-version: [10.x]
+    steps: # 每个 job 由多个 step 构成,它会从上至下依次执行。
+    # 获取仓库源码
+    - name: Checkout
+      uses: actions/checkout@v2         # github actions提供了一些官方的action,例如checkout @v2是action的版本
+    # 安装node
+    - name: Use Node.js ${{ matrix.node-version }} # 定义好的node版本
+      uses: actions/setup-node@v1 # 作用:安装nodejs
+      with:
+        node-version: ${{ matrix.node-version }} # 定义好的node版本
+    # 构建和部署
+    - name: Deploy
+      env: # 环境变量
+        GITHUB_TOKEN: ${{ secrets.vuepress_actions_access_token }}
+      run: npm install && npm run deploy # npm run deploy需要在package.json中定义"deploy: bash deploy.sh"

steps:

  • checkout源码到workflow中
  • 安装指定版本的node环境
  • 从仓库的设置中读取配置好的access_token,安装依赖,执行项目中的deploy.sh脚本

配置access_token

  1. 在github个人设置页面找到开发者设置

image-20220421224529199

  1. 选择个人access tokens,选择生成新token

image-20220421224739995

  1. 将生成好的token保存,关闭页面后将不会再显示此token,然后回到仓库保存

repo

Deploy.sh

shell
#!/usr/bin/env sh
+
+# 确保脚本抛出遇到的错误
+set -e
+
+# 生成静态文件
+npm run build
+
+# 进入生成的文件夹
+cd docs/.vuepress/dist
+
+# 如果是发布到自定义域名
+echo 'blog.storyxc.com' > CNAME
+
+if [ -z "${GITHUB_TOKEN}" ]; then
+  echo "GITHUB_TOKEN is not set"
+  exit 1
+else
+  msg='github actions自动部署'
+  githubUrl=https://storyxc:${GITHUB_TOKEN}@github.com/storyxc/vuepress.git
+  git config --global user.name "storyxc"
+  git config --global user.email "storyxc@163.com"
+fi
+
+git init
+git add -A
+git commit -m "${msg}"
+
+git push -f $githubUrl master:gh-pages
+
+cd -
#!/usr/bin/env sh
+
+# 确保脚本抛出遇到的错误
+set -e
+
+# 生成静态文件
+npm run build
+
+# 进入生成的文件夹
+cd docs/.vuepress/dist
+
+# 如果是发布到自定义域名
+echo 'blog.storyxc.com' > CNAME
+
+if [ -z "${GITHUB_TOKEN}" ]; then
+  echo "GITHUB_TOKEN is not set"
+  exit 1
+else
+  msg='github actions自动部署'
+  githubUrl=https://storyxc:${GITHUB_TOKEN}@github.com/storyxc/vuepress.git
+  git config --global user.name "storyxc"
+  git config --global user.email "storyxc@163.com"
+fi
+
+git init
+git add -A
+git commit -m "${msg}"
+
+git push -f $githubUrl master:gh-pages
+
+cd -

这里我是用了github pages发布,然后配置了自定义域名,这个域名要在服务商域名解析配置CNAME,然后在仓库的page页面添加自定义域名即可

image-20220421230458141

流程梳理

  • 配置workflow,推送代码到触发job的分支
  • github actions会根据workflow中的配置拉代码,打包,然后调用deploy.sh,将生成的静态文件推送到gh-page分支
  • gh-page分支配置github page,自定义域名
+ + + + \ No newline at end of file diff --git a/actions/index.html b/actions/index.html new file mode 100644 index 000000000..257152e8d --- /dev/null +++ b/actions/index.html @@ -0,0 +1,25 @@ + + + + + + Actions | 故事 + + + + + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git "a/actions/tools/Markdown\345\237\272\347\241\200\350\257\255\346\263\225.html" "b/actions/tools/Markdown\345\237\272\347\241\200\350\257\255\346\263\225.html" new file mode 100644 index 000000000..bc42f5267 --- /dev/null +++ "b/actions/tools/Markdown\345\237\272\347\241\200\350\257\255\346\263\225.html" @@ -0,0 +1,123 @@ + + + + + + Markdown基础语法 | 故事 + + + + + + + + + + + + + + +
Skip to content

Markdown基础语法

Markdown 是一种轻量级标记语言,它允许人们使用易读易写的纯文本格式编写文档。

Markdown 语言在 2004 由约翰·格鲁伯(英语:John Gruber)创建。

Markdown 编写的文档可以导出 HTML 、Word、图像、PDF、Epub 等多种格式的文档。

Markdown 编写的文档后缀为 .md, .markdown。

一、标题

示例:

txt
# 一级标题
+## 二级标题
+### 三级标题
+#### 四级标题
+##### 五级标题
+###### 六级标题
# 一级标题
+## 二级标题
+### 三级标题
+#### 四级标题
+##### 五级标题
+###### 六级标题

二、字体

  • 加粗 要加粗的文字左右分别用两个*号包起来

  • 斜体 要倾斜的文字左右分别用一个*号包起来

  • 斜体加粗 要倾斜和加粗的文字左右分别用三个*号包起来

  • 删除线 要加删除线的文字左右分别用两个~~号包起来

示例:

txt
**这是加粗的文字**
+*这是倾斜的文字*`
+***这是斜体加粗的文字***
+~~这是加删除线的文字~~
**这是加粗的文字**
+*这是倾斜的文字*`
+***这是斜体加粗的文字***
+~~这是加删除线的文字~~

效果: 这是加粗的文字这是倾斜的文字` 这是斜体加粗的文字这是加删除线的文字

三、引用

只需要在你希望引用的文字前面加上 > 就好,例如:

txt
> 这是一条引用
> 这是一条引用

效果如下:

这是一条引用

引用还可以进行多级嵌套

txt
> 这是一条引用
+>> 这是一条引用
+>>> 这是一条引用
> 这是一条引用
+>> 这是一条引用
+>>> 这是一条引用

效果:

这是一条引用

这是一条引用

这是一条引用

四、分割线

三个或者三个以上的 - 或者 * 都可以。

txt
---
+----
+***
+*****
---
+----
+***
+*****




五、图片

语法:

txt
![图片alt](图片地址 ''图片title'')
+
+图片alt就是显示在图片下面的文字,相当于对图片内容的解释。
+图片title是图片的标题,当鼠标移到图片上时显示的内容。title可加可不加
![图片alt](图片地址 ''图片title'')
+
+图片alt就是显示在图片下面的文字,相当于对图片内容的解释。
+图片title是图片的标题,当鼠标移到图片上时显示的内容。title可加可不加

示例:

txt
![示例图片alt](http://io.storyxc.com/images/MegellanicCloud_ZH-CN5132305226_1920x1080.jpg '示例图片title')
![示例图片alt](http://io.storyxc.com/images/MegellanicCloud_ZH-CN5132305226_1920x1080.jpg '示例图片title')

效果: 示例图片alt

六、超链接

语法:

txt
[超链接名](超链接地址 "超链接title")
+title可加可不加
[超链接名](超链接地址 "超链接title")
+title可加可不加

示例:

txt
[故事的博客](https://www.storyxc.com "故事的博客")
[故事的博客](https://www.storyxc.com "故事的博客")

效果 故事的博客

七、列表

无序列表 语法: 无序列表用 - + * 任何一种都可以 示例:

txt
- 列表1
++ 列表2
+* 列表3
- 列表1
++ 列表2
+* 列表3

效果

  • 列表1
  • 列表2
  • 列表3

有序列表 语法: 数字加点 示例:

txt
1. 111
+2. 222
+3. 333
1. 111
+2. 222
+3. 333

效果:

  1. 111
  2. 222
  3. 333

列表嵌套

上一级和下一级之间tab即可

  • 第一层
    • 第二层
      • 第三层

八、表格

语法:

txt
表头|表头|表头
+---|:--:|---:
+内容|内容|内容
+内容|内容|内容
+
+第二行分割表头和内容。
+- 有一个就行,为了对齐,多加了几个
+文字默认居左
+-两边加:表示文字居中
+-右边加:表示文字居右
+注:原生的语法两边都要用 | 包起来。此处省略
+
+姓名|技能|排行
+--|:--:|--:
+刘备|哭|大哥
+关羽|打|二哥
+张飞|骂|三弟
表头|表头|表头
+---|:--:|---:
+内容|内容|内容
+内容|内容|内容
+
+第二行分割表头和内容。
+- 有一个就行,为了对齐,多加了几个
+文字默认居左
+-两边加:表示文字居中
+-右边加:表示文字居右
+注:原生的语法两边都要用 | 包起来。此处省略
+
+姓名|技能|排行
+--|:--:|--:
+刘备|哭|大哥
+关羽|打|二哥
+张飞|骂|三弟

效果:

姓名技能排行
刘备大哥
关羽二哥
张飞三弟

九、代码块

  • 单行代码:代码之间分别用一个反引号包起来

示例:

txt
`这是一行代码`
`这是一行代码`

效果: 这是一行代码

  • 代码块 语法: 使用时把括号去掉
txt
(```)语言名
+	代码
+(```)
(```)语言名
+	代码
+(```)

示例:以java为例

txt
(```)java
+	public class HelloWorld{
+		public static void main(Stringargs[]){
+			System.out.println("Hello World!");
+		}
+	}
+(```)
(```)java
+	public class HelloWorld{
+		public static void main(Stringargs[]){
+			System.out.println("Hello World!");
+		}
+	}
+(```)

效果

java
public class HelloWorld{
+	public static void main(String args[]){
+		System.out.println("Hello World!");
+	}
+}
public class HelloWorld{
+	public static void main(String args[]){
+		System.out.println("Hello World!");
+	}
+}
+ + + + \ No newline at end of file diff --git a/actions/tools/Sketch.html b/actions/tools/Sketch.html new file mode 100644 index 000000000..db91dd0b6 --- /dev/null +++ b/actions/tools/Sketch.html @@ -0,0 +1,25 @@ + + + + + + Sketch | 故事 + + + + + + + + + + + + + + +
Skip to content

Sketch

快捷键

画布

  • 参考线ctrl+R
  • 缩放z/option+z
  • 拖动画布space
  • 锁定图层cmd+shift+l
  • 包含框选选择拖动+option

形状工具

  • 矩形R

  • 椭圆O

  • 圆角矩形U:也可以使用矩形,然后拖动四角的点实现

  • 直线L

  • 形状选中后在边角点位处按住cmd可以旋转

  • 双击形状进入编辑模式,可以编辑形状,鼠标点击边框线可增加锚点进行调整

  • 选中按住option拖动可以直接复制、cmd+d可以重复复制动作进行等距复制

以上工具可以按住option使用扩散的绘制效果

样式工具

  • f切换填充
  • b切换边框
  • ctrl+c吸色工具

矢量工具

  • v钢笔
  • p铅笔

文字工具

  • t+点文本
  • t+框选段落文本

画板

  • a创建画板

切片工具

  • s切片

编辑工具

  • ctrl+cmd+m蒙版工具

  • cmd+k缩放工具

  • cmd+option+o轮廓

+ + + + \ No newline at end of file diff --git "a/actions/tools/Typora\343\200\201PicGo\343\200\201\344\270\203\347\211\233\344\272\221\345\256\236\347\216\260markdown\345\233\276\347\211\207\350\207\252\345\212\250\344\270\212\344\274\240\345\233\276\345\272\212.html" "b/actions/tools/Typora\343\200\201PicGo\343\200\201\344\270\203\347\211\233\344\272\221\345\256\236\347\216\260markdown\345\233\276\347\211\207\350\207\252\345\212\250\344\270\212\344\274\240\345\233\276\345\272\212.html" new file mode 100644 index 000000000..c70896864 --- /dev/null +++ "b/actions/tools/Typora\343\200\201PicGo\343\200\201\344\270\203\347\211\233\344\272\221\345\256\236\347\216\260markdown\345\233\276\347\211\207\350\207\252\345\212\250\344\270\212\344\274\240\345\233\276\345\272\212.html" @@ -0,0 +1,25 @@ + + + + + + Typora、PicGo、七牛云实现markdown图片自动上传图床 | 故事 + + + + + + + + + + + + + + +
Skip to content

Typora、PicGo、七牛云实现markdown图片自动上传图床

背景

由于最近刚把博客迁到VuePress上来,写博客从原来自己博客项目自定义的web端Markdown编辑器换回了原来的Typora,这个文本编辑工具虽然很好用,但是在markdown中插入图片的时候就会碰到比较烦的问题,可能随便截了个图放在桌面了,在markdown中引入的话还要先把图片放到vuepress项目的静态资源文件夹里面。原来我自己开发的博客编辑器是通过axios调用后台接口把图片传到七牛云图床上去,现在换了博客框架原来的方案不好使了。之前typora也没有这方面的支持。不过我发现typora更新之后也支持了添加图片后的事件触发。

image-20210410202031986

选项还是很多样的,为typora点赞。

这里我还是选择了上传到图床,上传支持PicGo和自定义脚本,本来打算写个python脚本的,后来发现picgo这个应用也很好用,那就直接拿过来用吧。

配置picgo

通过gui配置

1.下载picgo

点击PicGo 进入仓库下载,这里选择windows版的执行程序,下载之后打开

2.申请七牛云账号配置存储空间

没有图床的可以搜一下相关教程,这里不再赘述

3.配置图床信息

image-20210410202814211

image-20210413020009764

选择七牛云图床、或者自己选择其他图床也许,按照自己情况来。

  • 七牛云的AccessKey和SecretKey在七牛云的密钥管理界面
  • 储存空间名就是自己的对象存储里面的具体某个空间名
  • 访问网址如果你有配自定义的cdn就填cdn,如果没有那就是原生的地址,在空间概览页面,不要忘了加上协议http://或者https😕/
  • 存储区域指的是华南华北这些地区对应的编号,具体对应值见下图,比如我是华南,存储区域就填z2
  • 设置完成后不要忘记点一下设为默认图床,否则默认不会用七牛云的,其他图床同理

image-20210410203157833

4.回到typora测试一下吧

image-20210410203327770

image-20210410203412022

先验证一下,可以看到成功了。这样再在typora中添加图片就可以看到图片会自动上传到你的图床并修改markdown中的地址了。

通过picgo-core配置

npm安装picgo-core:npm install -g picgo

配置uploader:picgo set uploader

使用uploader:picgo use uploader

配置文件地址为~/.picgo/config.json,可以手动修改

配置完成后在typora中配置自定义命令上传,命令格式 node_path picgo_path upload例如我的是/opt/homebrew/bin/node /opt/homebrew/bin/picgo upload

image-20220214180934655

验证上传选项,看到返回图片地址即可

+ + + + \ No newline at end of file diff --git "a/actions/tools/book-searcher\347\224\265\345\255\220\344\271\246\351\225\234\345\203\217.html" "b/actions/tools/book-searcher\347\224\265\345\255\220\344\271\246\351\225\234\345\203\217.html" new file mode 100644 index 000000000..61e4171e7 --- /dev/null +++ "b/actions/tools/book-searcher\347\224\265\345\255\220\344\271\246\351\225\234\345\203\217.html" @@ -0,0 +1,51 @@ + + + + + + book-searcher电子书镜像站点 | 故事 + + + + + + + + + + + + + + +
Skip to content

book-searcher电子书镜像站点

项目地址:https://github.com/book-searcher-org/book-searcher

docker-compose.yml

yml
version: '3'
+
+services:
+  book-searcher:
+    image: ghcr.io/book-searcher-org/book-searcher:latest
+    container_name: book-searcher
+    restart: always
+    ports:
+      - "7070:7070"
+    volumes:
+      - ./index:/index
version: '3'
+
+services:
+  book-searcher:
+    image: ghcr.io/book-searcher-org/book-searcher:latest
+    container_name: book-searcher
+    restart: always
+    ports:
+      - "7070:7070"
+    volumes:
+      - ./index:/index

下载index文件

https://zh.annas-archive.org/datasets

https://onedrive.caomingjun.com/zh-CN/🖥软件/zlib-searcher/

启动容器配置IPFS网关

txt
https://cloudflare-ipfs.com
+https://dweb.link
+https://ipfs.io
+https://dw.oho.im
https://cloudflare-ipfs.com
+https://dweb.link
+https://ipfs.io
+https://dw.oho.im
+ + + + \ No newline at end of file diff --git "a/actions/tools/git\345\221\275\344\273\244\346\225\264\347\220\206.html" "b/actions/tools/git\345\221\275\344\273\244\346\225\264\347\220\206.html" new file mode 100644 index 000000000..e8701c4cf --- /dev/null +++ "b/actions/tools/git\345\221\275\344\273\244\346\225\264\347\220\206.html" @@ -0,0 +1,31 @@ + + + + + + git命令整理 | 故事 + + + + + + + + + + + + + + +
Skip to content

git命令整理

版本控制工具一直用的GIT,之前提交代码都是用IDEA集成的GIT可视化工具,命令行几乎不怎么用,由于接下来项目要整合到微服务平台中,项目代码管理也要迁到Gerrit,idea的集成支持不太好,所以整理下GIT的命令,方便后面使用命令行提交代码。 180874352b52aaf65be47442.jpg

Remote:远程仓库
+Reporsitory:本地仓库
+WorkSpace:工作区
+Index:暂存区
Remote:远程仓库
+Reporsitory:本地仓库
+WorkSpace:工作区
+Index:暂存区

撤回修改

  • git commit --amend :提交完发现漏掉了几个文件没有添加,或者提交信息写错了,此时,可以运行带有 --amend 选项的提交命令来重新提交

  • git checkout -- <file>readme.txt文件在工作区的修改全部撤销,这里有两种情况:

    一种是readme.txt自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态;

    一种是readme.txt已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态。

  • git reset [--soft | --mixed | --hard] [HEAD]

    • 使用soft只会移动HEAD到上一个版本,可以理解为撤回上一次commit,暂存区和工作区不受影响
    • 使用mixed在移动HEAD到上一个版本,并且回退暂存区的内容,工作区不受影响
    • 使用hard,除了移动HEAD指针,取消暂存内容,还会覆盖回退工作区内容,属于比较危险的命令,谨慎使用
    • 不加括号中的参数 默认参数为mixed, 如果想回退多个版本可以修改为HEAD^^^或HEAD~3代表回退3个版本,依此类推
  • 关于git checkoutgit reset建议看下这篇文章,git重置

切换分支

git checkout branch_name

切换分之前要注意本地分支是否有未commit的文件,如果有可以撤销改动,或者commit,再或者使用git stash将当前分支的改动临时保存起来,使当前分支的工作空间和暂存区变干净。然后再进行切换分支;切换回之前的分支,需要恢复被临时保存的改动

暂存和恢复本地修改

git stash -u

恢复本地修改:

1.先查看有多少个临时保存的改动

git stash list

2.再用git stash apply --index stash@{n},n为使用git stash list查看到的某个改动的数字

3.再用git stash drop stash@{n}删除临时保存的改动

如果只有一个临时的stash,那么可以直接git stash apply即可恢复上次的临时保存记录

创建本地分支

基于本地master分支创建test分支为例:

先切换到master分支:git checkout master

建分支: git branch test

切分支:git checkout test

或者

建分支后切到该分支:git checkout -b test master

以基于某次commit id创建test分支为例:

git checkout -b test 0faceff

其中的0faceff为commit id的前7位

以基于某个tag创建test分支为例

git checkout -b test v0.1.0

v0.1.0为tag的名称

查看分支

git branch: 只显示本地分支名,当前分支名前有星号

git branch -v:显示本地分支名,当前分支前有星号,显示commit id

git branch -vv:显示本地分支名,当前分支名前有星号,显示commit id,显示追踪的远程分支名

git branch -a:显示所有分支名(包括远程分支)

git branch -r:查看远程分支名

删除本地分支

普通删除:git branch -d branch_name

强制删除(分支上有修改未合并到其他分支):git branch -D branch_name

更新代码

git pull或者git fetch

git pull -v --progress "origin"命令可以显示更详细的信息,git pull命令会fetch所有的远程分支的信息到本地,同时当前本地分支会被合并。

如果本地有修改文件,而且远程仓库也修改了该文件,pull会失败,提示本地的修改会被合并覆盖,此时可以commit本地的修改或者stash本地的修改,再pull。

修改代码

首先使用git checkout branch_name切换到正确分支,pull,新建或修改代码,再使用git add 文件名把修改或新增的文件添加到暂存区,再执行commit命令提交到本地仓库。

其中:

git add 某个文件

git add 多个文件(文件名用空格隔开)

git add -u 添加所有修改的文件到暂存区

git add .添加所有修改和新增的文件到暂存区

git add -A:添加所有修改,新增和删除的文件到暂存区

git commit 文件名 -m "注释":commit某个文件

git commit 文件1 文件2 -m "注释" commit多个文件,用空格隔开

git commit -m "注释"commit所有文件

如果是删除文件,可以使用

rm 文件

git add 文件

git commit 文件 -m "注释"

如果是重命名文件或者移动文件,可以使用

git mv 源文件路径 目标文件路径

git commit 文件 -m "注释"

  • 每次编辑前先进行pull操作,避免再push时产生合并冲突

push代码带远程仓库

本地代码从本地branch_name分支推到远端branch_name分支:

git checkout branch_name

git pull

git push origin HEAD:refs/for/branch_name

或者

git checkout branch_name

git pull

git push origin branch_name:refs/for/branch_name

查看信息

git status 显示有变更的文件

git log 显示当前分支的版本历史

git log --stat 显示commit历史,以及每次commit发生变更的文件

git log -S [keyword] 搜索提交历史,根据关键词

git log [tag] HEAD --pretty=format:%s 显示某个commit之后的所有变动,每个commit占据一行

git log [tag] HEAD --grep feature 显示某个commit之后的所有变动,其"提交说明"必须符合搜索条件

git log -p [file] 显示指定文件相关的每一次diff

git log -5 --pretty --oneline 显示过去5次提交

git shortlog -sn 显示所有提交过的用户,按提交次数排序

git blame [file] 显示指定文件是什么人在什么时间修改过

git diff 显示暂存区和工作区的代码差异

git diff --cached [file] 显示暂存区和上一个commit的差异

git diff HEAD 显示工作区与当前分支最新commit之间的差异

git diff [first-branch]...[second-branch] 显示两次提交之间的差异

git diff --shortstat "@{0 day ago}" 显示今天你写了多少行代码

git show [commit] 显示某次提交的元数据和内容变化

git show --name-only [commit] 显示某次提交发生变化的文件

git show [commit]:[filename] 显示某次提交时,某个文件的内容

git rebase [branch] 从本地master拉取代码更新当前分支:branch 一般为master

fetch vs pull

git fetch是将远程的最新内容拉到本地,用户在检查了以后决定是否合并到本地分支中。 而git pull 则是将远程的最新内容拉下来后直接合并,即:git pull = git fetch + git merge,这样可能会产生冲突,需要手动解决。

+ + + + \ No newline at end of file diff --git "a/actions/tools/iterm2\351\205\215\345\220\210oh-my-zsh\351\205\215\347\275\256\344\270\252\346\200\247\344\270\273\351\242\230\347\273\210\347\253\257.html" "b/actions/tools/iterm2\351\205\215\345\220\210oh-my-zsh\351\205\215\347\275\256\344\270\252\346\200\247\344\270\273\351\242\230\347\273\210\347\253\257.html" new file mode 100644 index 000000000..3f0273be6 --- /dev/null +++ "b/actions/tools/iterm2\351\205\215\345\220\210oh-my-zsh\351\205\215\347\275\256\344\270\252\346\200\247\344\270\273\351\242\230\347\273\210\347\253\257.html" @@ -0,0 +1,41 @@ + + + + + + iterm2配合oh-my-zsh配置个性主题终端 | 故事 + + + + + + + + + + + + + + +
Skip to content

iterm2配合oh-my-zsh配置个性主题终端

安装iterm2

官网下载:https://iterm2.com/

安装oh my zsh

官网:https://ohmyz.sh/

安装脚本:sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

因为网络原因无法执行这个脚本的可以找gitee上的国内源

更改iterm2的主题颜色为dracula

在iterm2的dracula主题仓库中下载color文件

仓库地址: https://github.com/dracula/iterm.git

image-20211119150619779

打开iterm2导入刚下载的color文件

image-20211119150804269

如图,导入完之后就可以选择导入的dracula主题颜色

安装命令高亮插件

clone代码到本地

git clone https://github.com/zsh-users/zsh-syntax-highlighting $ZSH_CUSTOM/plugins/zsh-syntax-highlighting

安装历史指令提示插件

clone代码到本地

git clone https://github.com/zsh-users/zsh-autosuggestions $ZSH_CUSTOM/plugins/zsh-autosuggestions

修改.zshrc配置

  • source ~/.zsh/zsh-autosuggestions/zsh-autosuggestions.zsh
  • source ~/.zsh/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
shell
# ~/.zshrc
+plugins=(
+  git
+  zsh-autosuggestions
+  zsh-syntax-highlighting
+)
# ~/.zshrc
+plugins=(
+  git
+  zsh-autosuggestions
+  zsh-syntax-highlighting
+)

效果

image-20211119151720033

powerlevel10k主题

https://github.com/romkatv/powerlevel10k

shell
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ~/powerlevel10k
+echo 'source ~/powerlevel10k/powerlevel10k.zsh-theme' >>~/.zshrc
+
+p10k configure
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ~/powerlevel10k
+echo 'source ~/powerlevel10k/powerlevel10k.zsh-theme' >>~/.zshrc
+
+p10k configure
+ + + + \ No newline at end of file diff --git "a/actions/tools/iterm2\351\205\215\347\275\256ssh\345\277\253\351\200\237\350\277\236\346\216\245.html" "b/actions/tools/iterm2\351\205\215\347\275\256ssh\345\277\253\351\200\237\350\277\236\346\216\245.html" new file mode 100644 index 000000000..5c27010cb --- /dev/null +++ "b/actions/tools/iterm2\351\205\215\347\275\256ssh\345\277\253\351\200\237\350\277\236\346\216\245.html" @@ -0,0 +1,25 @@ + + + + + + iterm2配置ssh快速连接 | 故事 + + + + + + + + + + + + + + +
Skip to content

iterm2配置ssh快速连接

macos生态的ssh工具有很多,但是试了很多还是感觉很差劲,不如windows生态的mobaxterm和xshell,可惜这两个软件没有mac的版本。不过iterm2作为mac生态下的终端工具代表倒是很简洁方便,但是没有专门的ssh工具那种简易的远程连接配置,需要自己动手折腾一下才可以。

安装iterm2

官网下载,不赘述。

配置profile

  1. command+,打开偏好设置,选择profilesimage-20211120112843166

  2. 新建一个profile设置,将command设置从login shell改为command,并输入需要执行的ssh指令

    image-20211120113120836

  3. 切换到advanced选项卡,选择编辑triggers触发器。新增一个触发器,action选择send text,触发的表达式改为root@xxx.xxx.xxx.xxx's password,参数改为登陆账户的密码+\n,这里注意一定要加\n代表输入回车,不然就会卡在输入密码那里需要手动回车才能登陆,然后勾选上instant立即触发。

    image-20211120113408181

image-20211120114241239

这里触发器的表达式即是输入ssh命令时,服务器给出的需要输入密码的提示文字,所以想配置什么服务器的触发器直接改个登陆名和服务器地址就可以

image-20211120114556561

测试

配置完毕后,可以根据菜单栏的profiles选项卡,选择需要连接的服务器即可

image-20211120114848453

image-20211120114946181

+ + + + \ No newline at end of file diff --git "a/actions/tools/linux\350\256\276\347\275\256macOS\346\227\266\351\227\264\346\234\272\345\231\250server.html" "b/actions/tools/linux\350\256\276\347\275\256macOS\346\227\266\351\227\264\346\234\272\345\231\250server.html" new file mode 100644 index 000000000..a379915a2 --- /dev/null +++ "b/actions/tools/linux\350\256\276\347\275\256macOS\346\227\266\351\227\264\346\234\272\345\231\250server.html" @@ -0,0 +1,29 @@ + + + + + + linux设置macOS时间机器server | 故事 + + + + + + + + + + + + + + +
Skip to content

linux设置macOS时间机器server

安装需要的包

sudo apt install netatalk avahi-daemon

编辑netatalk配置文件

sudo vim /etc/netatalk/afp.conf

添加Time Machine配置

txt
[Time Machine]
+path = /mnt/data/backup/time_machine
+time machine = yes
[Time Machine]
+path = /mnt/data/backup/time_machine
+time machine = yes

创建目录

sudo mkdir -p /mnt/data/backup/time_machine

sudo chown nobody:nogroup /mnt/data/backup/time_machine

sudo chmod 777 /mnt/data/backup/time_machine

重启netatalk服务

sudo systemctl restart netatalk

在mac上进行备份

时间机器中选择磁盘,连接linux server即可。

+ + + + \ No newline at end of file diff --git "a/actions/tools/powershell\347\276\216\345\214\226.html" "b/actions/tools/powershell\347\276\216\345\214\226.html" new file mode 100644 index 000000000..5fe3bb798 --- /dev/null +++ "b/actions/tools/powershell\347\276\216\345\214\226.html" @@ -0,0 +1,43 @@ + + + + + + powershell美化 | 故事 + + + + + + + + + + + + + + +
Skip to content

powershell美化

https://ohmyposh.dev/docs/

安装windows terminal和powershell

安装oh-my-posh

shell
Set-ExecutionPolicy Bypass -Scope Process -Force; Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://ohmyposh.dev/install.ps1'))
Set-ExecutionPolicy Bypass -Scope Process -Force; Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://ohmyposh.dev/install.ps1'))

安装Nerd Fonts

如果不安装Nerd Fonts会有乱码情况,oh-my-posh推荐安装Meslo LGM NF字体,也可以从https://www.nerdfonts.com/font-downloads自行选择下载。下载后解压放到C:\windows\Fonts文件夹中。编辑Windows Terminal默认设置将默认字体改为喜欢的Nerd Fonts。

编辑profile

code $PROFILEnotepad $PROFILE

shell
oh-my-posh init pwsh | Invoke-Expression # 默认主题
+
+oh-my-posh init pwsh --config C:\Users\story\AppData\Local\Programs\oh-my-posh\themes\robbyrussel.omp.json | Invoke-Expression # --config可以配置喜欢的主题
+
+--config 'https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/jandedobbeleer.omp.json' # 也可以配置远程主题
oh-my-posh init pwsh | Invoke-Expression # 默认主题
+
+oh-my-posh init pwsh --config C:\Users\story\AppData\Local\Programs\oh-my-posh\themes\robbyrussel.omp.json | Invoke-Expression # --config可以配置喜欢的主题
+
+--config 'https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/jandedobbeleer.omp.json' # 也可以配置远程主题
shell
# 设置预测文本来源为历史记录
+Set-PSReadLineOption -PredictionSource History
+# 设置向上键为后向搜索历史记录
+Set-PSReadLineKeyHandler -Key UpArrow -Function HistorySearchBackward
+# 设置向下键为前向搜索历史纪录
+Set-PSReadLineKeyHandler -Key DownArrow -Function HistorySearchForward
# 设置预测文本来源为历史记录
+Set-PSReadLineOption -PredictionSource History
+# 设置向上键为后向搜索历史记录
+Set-PSReadLineKeyHandler -Key UpArrow -Function HistorySearchBackward
+# 设置向下键为前向搜索历史纪录
+Set-PSReadLineKeyHandler -Key DownArrow -Function HistorySearchForward

windows安装后默认的主题文件夹为:C:\Users\[your username]\AppData\Local\Programs\oh-my-posh\themes,也可以通过echo $env:POSH_THEMES_PATH命令查看主题的路径

挑选喜欢的主题

配置立即生效:

. $PROFILE
. $PROFILE

效果图:

  • 默认:

    image-20220607011525725

  • jandedobbeleer:

    image-20220607011557983

+ + + + \ No newline at end of file diff --git "a/actions/tools/\345\220\204\347\263\273\347\273\237\344\270\213\346\240\241\351\252\214\346\226\207\344\273\266\344\270\200\350\207\264\346\200\247.html" "b/actions/tools/\345\220\204\347\263\273\347\273\237\344\270\213\346\240\241\351\252\214\346\226\207\344\273\266\344\270\200\350\207\264\346\200\247.html" new file mode 100644 index 000000000..630c02709 --- /dev/null +++ "b/actions/tools/\345\220\204\347\263\273\347\273\237\344\270\213\346\240\241\351\252\214\346\226\207\344\273\266\344\270\200\350\207\264\346\200\247.html" @@ -0,0 +1,25 @@ + + + + + + 各系统下校验文件一致性 | 故事 + + + + + + + + + + + + + + +
Skip to content

各系统下校验文件一致性

之前从网上下软件一直没有校验的习惯,直到从某知名mac破解网站上下了个被人恶意投毒的navicat,本文记录下各系统下校验的操作。内容引用自Apache Kafka官方文档。

校验哈希值

WindowsLinuxMac
SHA-1 (deprecated)certUtil -hashfile file SHA1sha1sum fileshasum -a 1 file
SHA-256certUtil -hashfile file SHA256sha256sum fileshasum -a 256 file
SHA-512certUtil -hashfile file SHA512sha512sum fileshasum -a 512 file
MD5 (deprecated)certUtil -hashfile file MD5md5sum filemd5 file
+ + + + \ No newline at end of file diff --git "a/actions/tools/\345\223\252\345\220\222\346\216\242\351\222\210\351\241\265\351\235\242\347\276\216\345\214\226.html" "b/actions/tools/\345\223\252\345\220\222\346\216\242\351\222\210\351\241\265\351\235\242\347\276\216\345\214\226.html" new file mode 100644 index 000000000..53e8bb400 --- /dev/null +++ "b/actions/tools/\345\223\252\345\220\222\346\216\242\351\222\210\351\241\265\351\235\242\347\276\216\345\214\226.html" @@ -0,0 +1,1421 @@ + + + + + + 哪吒探针页面美化 | 故事 + + + + + + + + + + + + + + +
Skip to content

哪吒探针页面美化

找到容器的文件系统目录

shell
docker inspect dashboard-dashboard-1 --format='{{.GraphDriver.Data.MergedDir}}'
docker inspect dashboard-dashboard-1 --format='{{.GraphDriver.Data.MergedDir}}'

替换template中的文件

  • 路径:/var/lib/docker/overlay2/xxxx/merged/dashboard/resource/template

  • 替换文件common/header.html

    html
    {{define "common/header"}}
    +<!DOCTYPE html>
    +<html lang="{{.Conf.Language}}">
    +
    +<head>
    +    <meta charset="UTF-8">
    +    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    +    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    +    <meta content="telephone=no" name="format-detection">
    +    <title>{{.Title}}</title>
    +    <link rel="stylesheet" type="text/css"
    +        href="https://cdn.staticfile.org/semantic-ui/2.4.1/semantic.min.css">
    +    <link href="https://cdn.staticfile.org/font-logos/0.17/font-logos.min.css" type="text/css"
    +        rel="stylesheet" />
    +            <link href="https://cdn.staticfile.org/bootstrap-icons/1.10.3/font/bootstrap-icons.css" type="text/css"
    +        rel="stylesheet" />
    +    <link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
    +    <link rel="stylesheet" type="text/css" href="/static/main.css?v2022042314">
    +    <link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
    +</head>
    +
    +<body>
    +    {{end}}
    {{define "common/header"}}
    +<!DOCTYPE html>
    +<html lang="{{.Conf.Language}}">
    +
    +<head>
    +    <meta charset="UTF-8">
    +    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    +    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    +    <meta content="telephone=no" name="format-detection">
    +    <title>{{.Title}}</title>
    +    <link rel="stylesheet" type="text/css"
    +        href="https://cdn.staticfile.org/semantic-ui/2.4.1/semantic.min.css">
    +    <link href="https://cdn.staticfile.org/font-logos/0.17/font-logos.min.css" type="text/css"
    +        rel="stylesheet" />
    +            <link href="https://cdn.staticfile.org/bootstrap-icons/1.10.3/font/bootstrap-icons.css" type="text/css"
    +        rel="stylesheet" />
    +    <link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
    +    <link rel="stylesheet" type="text/css" href="/static/main.css?v2022042314">
    +    <link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
    +</head>
    +
    +<body>
    +    {{end}}
  • 替换文件theme-default/home.html

    html
    {{define "theme-default/home"}}
    +{{template "common/header" .}}
    +{{if ts .CustomCode}} {{.CustomCode|safe}} {{end}}
    +{{template "common/menu" .}}
    +<div class="nb-container">
    +  <div class="ui container">
    +    <div id="app">
    +      <div class="ui styled fluid accordion" v-for="group in groups">
    +        <div class="active title">
    +          <i class="dropdown icon"></i>
    +          @#(group.Tag!==''?group.Tag:'{{tr "Default"}}')#@
    +        </div>
    +        <div class="active content">
    +          <div class="ui four stackable status cards">
    +            <div v-for="server in group.data" :id="server.ID" class="ui card">
    +              <div class="content" v-if="server.Host" style="margin-top: 10px; padding-bottom: 5px">
    +                <div class="header">
    +                <i :class="server.Host.CountryCode + ' flag'"></i>&nbsp;<i v-if='server.Host.Platform == "darwin"'
    +                    class="apple icon"></i><i v-else-if='isWindowsPlatform(server.Host.Platform)'
    +                    class="windows icon"></i><i v-else :class="'fl-' + getFontLogoClass(server.Host.Platform)"></i>
    +                  @#server.Name + (server.live?'':'[{{tr "Offline"}}]')#@
    +                  <i class="nezha-secondary-font info circle icon" style="height: 28px"></i>
    +                  <div class="ui content popup" style="margin-bottom: 0">
    +                    {{tr "Platform"}}: @#server.Host.Platform#@-@#server.Host.PlatformVersion#@
    +                    [<span
    +                      v-if="server.Host.Virtualization">@#server.Host.Virtualization#@:</span>@#server.Host.Arch#@]<br />
    +                    CPU: @#server.Host.CPU#@<br />
    +                    {{tr "DiskUsed"}}:
    +                    @#formatByteSize(server.State.DiskUsed)#@/@#formatByteSize(server.Host.DiskTotal)#@<br />
    +                    {{tr "MemUsed"}}:
    +                    @#formatByteSize(server.State.MemUsed)#@/@#formatByteSize(server.Host.MemTotal)#@<br />
    +                    {{tr "SwapUsed"}}:
    +                    @#formatByteSize(server.State.SwapUsed)#@/@#formatByteSize(server.Host.SwapTotal)#@<br />
    +                    {{tr "NetTransfer"}}: <i
    +                      class="arrow alternate circle down outline icon"></i>@#formatByteSize(server.State.NetInTransfer)#@<i
    +                      class="arrow alternate circle up outline icon"></i>@#formatByteSize(server.State.NetOutTransfer)#@<br />
    +                    {{tr "Load"}}: @# toFixed2(server.State.Load1) #@/@# toFixed2(server.State.Load5) #@/@#
    +                    toFixed2(server.State.Load15) #@<br />
    +                    {{tr "ProcessCount"}}: @# server.State.ProcessCount #@<br />
    +                    {{tr "ConnCount"}}: TCP @# server.State.TcpConnCount #@ / UDP @# server.State.UdpConnCount #@<br />
    +                    {{tr "BootTime"}}: @# formatTimestamp(server.Host.BootTime) #@<br />
    +                    {{tr "LastActive"}}: @# new Date(server.LastActive).toLocaleString() #@<br />
    +                    {{tr "Version"}}: @#server.Host.Version#@<br />
    +                  </div>
    +                  <div class="ui divider" style="margin-bottom: 5px"></div>
    +                </div>
    +                <div class="description">
    +                  <div class="ui grid">
    +                    <div class="three wide column">CPU</div>
    +                    <div class="thirteen wide column">
    +                      <div :class="formatPercent(server.live,server.State.CPU, 100).class">
    +                        <div class="bar" :style="formatPercent(server.live,server.State.CPU, 100).style">
    +                          <small>@#formatPercent(server.live,server.State.CPU,100).percent#@%</small>
    +                        </div>
    +                      </div>
    +                    </div>
    +                    <div class="three wide column">{{tr "MemUsed"}}</div>
    +                    <div class="thirteen wide column">
    +                      <div :class="formatPercent(server.live,server.State.MemUsed, server.Host.MemTotal).class">
    +                        <div class="bar"
    +                          :style="formatPercent(server.live,server.State.MemUsed, server.Host.MemTotal).style">
    +                          <small>@#parseInt(server.State?server.State.MemUsed/server.Host.MemTotal*100:0)#@%</small>
    +                        </div>
    +                      </div>
    +                    </div>
    +                    <div class="three wide column">{{tr "SwapUsed"}}</div>
    +                    <div class="thirteen wide column">
    +                      <div :class="formatPercent(server.live,server.State.SwapUsed, server.Host.SwapTotal).class">
    +                        <div class="bar"
    +                          :style="formatPercent(server.live,server.State.SwapUsed, server.Host.SwapTotal).style">
    +                          <small>@#parseInt(server.State?server.State.SwapUsed/server.Host.SwapTotal*100:0)#@%</small>
    +                        </div>
    +                      </div>
    +                    </div>
    +                    <div class="three wide column">{{tr "DiskUsed"}}</div>
    +                    <div class="thirteen wide column">
    +                      <div :class="formatPercent(server.live,server.State.DiskUsed, server.Host.DiskTotal).class">
    +                        <div class="bar"
    +                          :style="formatPercent(server.live,server.State.DiskUsed, server.Host.DiskTotal).style">
    +                          <small>@#parseInt(server.State?server.State.DiskUsed/server.Host.DiskTotal*100:0)#@%</small>
    +                        </div>
    +                      </div>
    +                    </div>
    +                    <div class="three wide column">{{tr "NetSpeed"}}</div>
    +                    <div class="thirteen wide column">
    +                      <i class="arrow alternate circle down outline icon"></i>
    +                      @#formatByteSize(server.State.NetInSpeed)#@/s
    +                      <i class="arrow alternate circle up outline icon"></i>
    +                      @#formatByteSize(server.State.NetOutSpeed)#@/s
    +                    </div>
    +                    <div class="three wide column">流量</div>
    +                    <div class="thirteen wide column">
    +                      <i class="arrow circle down icon"></i>
    +                      @#formatByteSize(server.State.NetInTransfer)#@
    +                      &nbsp;
    +                      <i class="arrow circle up icon"></i>
    +                      @#formatByteSize(server.State.NetOutTransfer)#@
    +                    </div>
    +                    <div class="three wide column">信息</div>
    +                    <div class="thirteen wide column">
    +                      <i class="bi bi-cpu-fill" style="font-size: 1.1rem; color: #4a86e8;"></i> @#getCoreAndGHz(server.Host.CPU)#@
    +                      &nbsp;
    +                      <i class="bi bi-memory" style="font-size: 1.1rem; color: #00ac0d;"></i> @#getK2Gb(server.Host.MemTotal)#@
    +                      &nbsp;
    +                      <i class="bi bi-hdd-rack-fill" style="font-size: 1.1rem; color: #980000"></i> @#getK2Gb(server.Host.DiskTotal)#@
    +                    </div>
    +                    <div class="three wide column">{{tr "Uptime"}}</div>
    +                    <div class="thirteen wide column">
    +                      <i class="clock icon"></i>@#secondToDate(server.State.Uptime)#@
    +                    </div>
    +                  </div>
    +                </div>
    +              </div>
    +              <div class="content" v-else>
    +                <p>@#server.Name#@</p>
    +                <p>{{tr "ServerIsOffline"}}</p>
    +              </div>
    +            </div>
    +          </div>
    +        </div>
    +      </div>
    +    </div>
    +  </div>
    +</div>
    +{{template "common/footer" .}}
    +<script>
    +  const initData = JSON.parse('{{.Servers}}').servers;
    +  var statusCards = new Vue({
    +    el: '#app',
    +    delimiters: ['@#', '#@'],
    +    data: {
    +      data: initData,
    +      groups: [],
    +      cache: [],
    +    },
    +    created() {
    +      this.group()
    +    },
    +    mounted() {
    +      $('.nezha-secondary-font.info.icon').popup({
    +        popup: '.ui.content.popup',
    +        exclusive: true,
    +      });
    +    },
    +    methods: {
    +      toFixed2(f) {
    +        return f.toFixed(2)
    +      },
    +      isWindowsPlatform(str) {
    +        return str.includes('Windows')
    +      },
    +      getFontLogoClass(str) {
    +        if (["almalinux",
    +          "alpine",
    +          "aosc",
    +          "apple",
    +          "archlinux",
    +          "archlabs",
    +          "artix",
    +          "budgie",
    +          "centos",
    +          "coreos",
    +          "debian",
    +          "deepin",
    +          "devuan",
    +          "docker",
    +          "elementary",
    +          "fedora",
    +          "ferris",
    +          "flathub",
    +          "freebsd",
    +          "gentoo",
    +          "gnu-guix",
    +          "illumos",
    +          "kali-linux",
    +          "linuxmint",
    +          "mageia",
    +          "mandriva",
    +          "manjaro",
    +          "nixos",
    +          "openbsd",
    +          "opensuse",
    +          "pop-os",
    +          "raspberry-pi",
    +          "redhat",
    +          "rocky-linux",
    +          "sabayon",
    +          "slackware",
    +          "snappy",
    +          "solus",
    +          "tux",
    +          "ubuntu",
    +          "void",
    +          "zorin"].indexOf(str)
    +          > -1) {
    +            return str;
    +        }
    +        if (['openwrt','linux'].indexOf(str) > -1) {
    +          return 'tux';
    +        }
    +        if (str == 'amazon') {
    +          return 'redhat';
    +        }
    +        if (str == 'arch') {
    +          return 'archlinux';
    +        }
    +        return '';
    +      },
    +      group() {
    +        this.groups = groupingData(this.data, "Tag")
    +      },
    +      formatPercent(live, used, total) {
    +        const percent = live ? (parseInt(used / total * 100) || 0) : -1
    +        if (!this.cache[percent]) {
    +          this.cache[percent] = {
    +            class: {
    +              ui: true,
    +              progress: true,
    +            },
    +            style: {
    +              'transition-duration': '300ms',
    +              'min-width': 'unset',
    +              width: percent + '% !important',
    +            },
    +            percent,
    +          }
    +          if (percent < 0) {
    +            this.cache[percent].style['background-color'] = 'slategray'
    +            this.cache[percent].class.offline = true
    +          } else if (percent < 51) {
    +            this.cache[percent].style['background-color'] = '#0a94f2'
    +            this.cache[percent].class.fine = true
    +          } else if (percent < 81) {
    +            this.cache[percent].style['background-color'] = 'orange'
    +            this.cache[percent].class.warning = true
    +          } else {
    +            this.cache[percent].style['background-color'] = 'crimson'
    +            this.cache[percent].class.error = true
    +          }
    +        }
    +        return this.cache[percent]
    +      },
    +      secondToDate(s) {
    +        var d = Math.floor(s / 3600 / 24);
    +        if (d > 0) {
    +          return d + " {{tr "Day"}}"
    +        }
    +        var h = Math.floor(s / 3600 % 24);
    +        var m = Math.floor(s / 60 % 60);
    +        var s = Math.floor(s % 60);
    +        return h + ":" + ("0" + m).slice(-2) + ":" + ("0" + s).slice(-2);
    +      },
    +      formatTimestamp(t) {
    +        return new Date(t * 1000).toLocaleString()
    +      },
    +      formatByteSize(bs) {
    +        const x = readableBytes(bs)
    +        return x != "NaN undefined" ? x : '0B'
    +      },
    +      getCoreAndGHz(str){
    +        if((str || []).hasOwnProperty(0) === false){
    +            return '';
    +        }
    +        str = str[0];
    +	    let GHz = str.match(/(\d|\.)+GHz/g);
    +	    let Core = str.match(/(\d|\.)+ Physical/g);
    +	    GHz = GHz!==null?GHz.hasOwnProperty(0)===false?'':GHz[0]:''
    +	    Core = Core!==null?Core.hasOwnProperty(0)===false?'?':Core[0]:'?'
    +	    if(Core === '?'){
    +	        let Core = str.match(/(\d|\.)+ Virtual/g);
    +	        Core = Core!==null?Core.hasOwnProperty(0)===false?'?':Core[0]:'?'
    +	        return Core.replace('Virtual','Core')
    +	    }
    +	    return Core.replace('Physical','Core');
    +	   
    +      },
    +      getK2Gb(bs){
    +          bs = bs / 1024 /1024 /1024;
    +          return Math.ceil(bs.toFixed(2)) + 'GB';
    +      },
    +      listTipsMouseenter(obj,strs,tipsNum=1){
    +          this.layerIndex = layer.tips(strs, '#'+obj,{tips: [tipsNum, 'rgb(0 0 0 / 85%)'],time:0});
    +          $('#'+obj).attr('layerIndex',this.layerIndex)
    +      },
    +      listTipsMouseleave(obj){
    +          layer.close(this.layerIndex)
    +      }
    +    }
    +  })
    +
    +  function groupingData(data, field) {
    +    let map = {};
    +    let dest = [];
    +    data.forEach(item => {
    +      if (!map[item[field]]) {
    +        dest.push({
    +          [field]: item[field],
    +          data: [item]
    +        });
    +        map[item[field]] = item;
    +      } else {
    +        dest.forEach(dItem => {
    +          if (dItem[field] == item[field]) {
    +            dItem.data.push(item);
    +          }
    +        });
    +      }
    +    })
    +    return dest;
    +  }
    +
    +  let canShowError = true;
    +  function connect() {
    +    const wsProtocol = window.location.protocol == "https:" ? "wss" : "ws"
    +    const ws = new WebSocket(wsProtocol + '://' + window.location.host + '/ws');
    +    ws.onopen = function (evt) {
    +      canShowError = true;
    +      $.suiAlert({
    +        title: '{{tr "RealtimeChannelEstablished"}}',
    +        description: '{{tr "GetTheLatestMonitoringDataInRealTime"}}',
    +        type: 'success',
    +        time: '2',
    +        position: 'top-center',
    +      });
    +    }
    +    ws.onmessage = function (evt) {
    +      const oldServers = statusCards.servers
    +      const data = JSON.parse(evt.data)
    +      statusCards.servers = data.servers
    +      for (let i = 0; i < statusCards.servers.length; i++) {
    +        const ns = statusCards.servers[i];
    +        if (!ns.Host) ns.live = false
    +        else {
    +          const lastActive = new Date(ns.LastActive).getTime()
    +          if (data.now - lastActive > 10 * 1000) {
    +            ns.live = false
    +          } else {
    +            ns.live = true
    +          }
    +        }
    +      }
    +      statusCards.groups = groupingData(statusCards.servers, "Tag")
    +    }
    +    ws.onclose = function () {
    +      if (canShowError) {
    +        canShowError = false;
    +        $.suiAlert({
    +          title: '{{tr "RealtimeChannelDisconnect"}}',
    +          description: '{{tr "CanNotGetTheLatestMonitoringDataInRealTime"}}',
    +          type: 'warning',
    +          time: '2',
    +          position: 'top-center',
    +        });
    +      }
    +      setTimeout(function () {
    +        connect()
    +      }, 3000);
    +    }
    +    ws.onerror = function () {
    +      ws.close()
    +    }
    +  }
    +
    +  connect();
    +
    +  $('.ui.accordion').accordion({ "exclusive": false });
    +</script>
    +{{end}}
    {{define "theme-default/home"}}
    +{{template "common/header" .}}
    +{{if ts .CustomCode}} {{.CustomCode|safe}} {{end}}
    +{{template "common/menu" .}}
    +<div class="nb-container">
    +  <div class="ui container">
    +    <div id="app">
    +      <div class="ui styled fluid accordion" v-for="group in groups">
    +        <div class="active title">
    +          <i class="dropdown icon"></i>
    +          @#(group.Tag!==''?group.Tag:'{{tr "Default"}}')#@
    +        </div>
    +        <div class="active content">
    +          <div class="ui four stackable status cards">
    +            <div v-for="server in group.data" :id="server.ID" class="ui card">
    +              <div class="content" v-if="server.Host" style="margin-top: 10px; padding-bottom: 5px">
    +                <div class="header">
    +                <i :class="server.Host.CountryCode + ' flag'"></i>&nbsp;<i v-if='server.Host.Platform == "darwin"'
    +                    class="apple icon"></i><i v-else-if='isWindowsPlatform(server.Host.Platform)'
    +                    class="windows icon"></i><i v-else :class="'fl-' + getFontLogoClass(server.Host.Platform)"></i>
    +                  @#server.Name + (server.live?'':'[{{tr "Offline"}}]')#@
    +                  <i class="nezha-secondary-font info circle icon" style="height: 28px"></i>
    +                  <div class="ui content popup" style="margin-bottom: 0">
    +                    {{tr "Platform"}}: @#server.Host.Platform#@-@#server.Host.PlatformVersion#@
    +                    [<span
    +                      v-if="server.Host.Virtualization">@#server.Host.Virtualization#@:</span>@#server.Host.Arch#@]<br />
    +                    CPU: @#server.Host.CPU#@<br />
    +                    {{tr "DiskUsed"}}:
    +                    @#formatByteSize(server.State.DiskUsed)#@/@#formatByteSize(server.Host.DiskTotal)#@<br />
    +                    {{tr "MemUsed"}}:
    +                    @#formatByteSize(server.State.MemUsed)#@/@#formatByteSize(server.Host.MemTotal)#@<br />
    +                    {{tr "SwapUsed"}}:
    +                    @#formatByteSize(server.State.SwapUsed)#@/@#formatByteSize(server.Host.SwapTotal)#@<br />
    +                    {{tr "NetTransfer"}}: <i
    +                      class="arrow alternate circle down outline icon"></i>@#formatByteSize(server.State.NetInTransfer)#@<i
    +                      class="arrow alternate circle up outline icon"></i>@#formatByteSize(server.State.NetOutTransfer)#@<br />
    +                    {{tr "Load"}}: @# toFixed2(server.State.Load1) #@/@# toFixed2(server.State.Load5) #@/@#
    +                    toFixed2(server.State.Load15) #@<br />
    +                    {{tr "ProcessCount"}}: @# server.State.ProcessCount #@<br />
    +                    {{tr "ConnCount"}}: TCP @# server.State.TcpConnCount #@ / UDP @# server.State.UdpConnCount #@<br />
    +                    {{tr "BootTime"}}: @# formatTimestamp(server.Host.BootTime) #@<br />
    +                    {{tr "LastActive"}}: @# new Date(server.LastActive).toLocaleString() #@<br />
    +                    {{tr "Version"}}: @#server.Host.Version#@<br />
    +                  </div>
    +                  <div class="ui divider" style="margin-bottom: 5px"></div>
    +                </div>
    +                <div class="description">
    +                  <div class="ui grid">
    +                    <div class="three wide column">CPU</div>
    +                    <div class="thirteen wide column">
    +                      <div :class="formatPercent(server.live,server.State.CPU, 100).class">
    +                        <div class="bar" :style="formatPercent(server.live,server.State.CPU, 100).style">
    +                          <small>@#formatPercent(server.live,server.State.CPU,100).percent#@%</small>
    +                        </div>
    +                      </div>
    +                    </div>
    +                    <div class="three wide column">{{tr "MemUsed"}}</div>
    +                    <div class="thirteen wide column">
    +                      <div :class="formatPercent(server.live,server.State.MemUsed, server.Host.MemTotal).class">
    +                        <div class="bar"
    +                          :style="formatPercent(server.live,server.State.MemUsed, server.Host.MemTotal).style">
    +                          <small>@#parseInt(server.State?server.State.MemUsed/server.Host.MemTotal*100:0)#@%</small>
    +                        </div>
    +                      </div>
    +                    </div>
    +                    <div class="three wide column">{{tr "SwapUsed"}}</div>
    +                    <div class="thirteen wide column">
    +                      <div :class="formatPercent(server.live,server.State.SwapUsed, server.Host.SwapTotal).class">
    +                        <div class="bar"
    +                          :style="formatPercent(server.live,server.State.SwapUsed, server.Host.SwapTotal).style">
    +                          <small>@#parseInt(server.State?server.State.SwapUsed/server.Host.SwapTotal*100:0)#@%</small>
    +                        </div>
    +                      </div>
    +                    </div>
    +                    <div class="three wide column">{{tr "DiskUsed"}}</div>
    +                    <div class="thirteen wide column">
    +                      <div :class="formatPercent(server.live,server.State.DiskUsed, server.Host.DiskTotal).class">
    +                        <div class="bar"
    +                          :style="formatPercent(server.live,server.State.DiskUsed, server.Host.DiskTotal).style">
    +                          <small>@#parseInt(server.State?server.State.DiskUsed/server.Host.DiskTotal*100:0)#@%</small>
    +                        </div>
    +                      </div>
    +                    </div>
    +                    <div class="three wide column">{{tr "NetSpeed"}}</div>
    +                    <div class="thirteen wide column">
    +                      <i class="arrow alternate circle down outline icon"></i>
    +                      @#formatByteSize(server.State.NetInSpeed)#@/s
    +                      <i class="arrow alternate circle up outline icon"></i>
    +                      @#formatByteSize(server.State.NetOutSpeed)#@/s
    +                    </div>
    +                    <div class="three wide column">流量</div>
    +                    <div class="thirteen wide column">
    +                      <i class="arrow circle down icon"></i>
    +                      @#formatByteSize(server.State.NetInTransfer)#@
    +                      &nbsp;
    +                      <i class="arrow circle up icon"></i>
    +                      @#formatByteSize(server.State.NetOutTransfer)#@
    +                    </div>
    +                    <div class="three wide column">信息</div>
    +                    <div class="thirteen wide column">
    +                      <i class="bi bi-cpu-fill" style="font-size: 1.1rem; color: #4a86e8;"></i> @#getCoreAndGHz(server.Host.CPU)#@
    +                      &nbsp;
    +                      <i class="bi bi-memory" style="font-size: 1.1rem; color: #00ac0d;"></i> @#getK2Gb(server.Host.MemTotal)#@
    +                      &nbsp;
    +                      <i class="bi bi-hdd-rack-fill" style="font-size: 1.1rem; color: #980000"></i> @#getK2Gb(server.Host.DiskTotal)#@
    +                    </div>
    +                    <div class="three wide column">{{tr "Uptime"}}</div>
    +                    <div class="thirteen wide column">
    +                      <i class="clock icon"></i>@#secondToDate(server.State.Uptime)#@
    +                    </div>
    +                  </div>
    +                </div>
    +              </div>
    +              <div class="content" v-else>
    +                <p>@#server.Name#@</p>
    +                <p>{{tr "ServerIsOffline"}}</p>
    +              </div>
    +            </div>
    +          </div>
    +        </div>
    +      </div>
    +    </div>
    +  </div>
    +</div>
    +{{template "common/footer" .}}
    +<script>
    +  const initData = JSON.parse('{{.Servers}}').servers;
    +  var statusCards = new Vue({
    +    el: '#app',
    +    delimiters: ['@#', '#@'],
    +    data: {
    +      data: initData,
    +      groups: [],
    +      cache: [],
    +    },
    +    created() {
    +      this.group()
    +    },
    +    mounted() {
    +      $('.nezha-secondary-font.info.icon').popup({
    +        popup: '.ui.content.popup',
    +        exclusive: true,
    +      });
    +    },
    +    methods: {
    +      toFixed2(f) {
    +        return f.toFixed(2)
    +      },
    +      isWindowsPlatform(str) {
    +        return str.includes('Windows')
    +      },
    +      getFontLogoClass(str) {
    +        if (["almalinux",
    +          "alpine",
    +          "aosc",
    +          "apple",
    +          "archlinux",
    +          "archlabs",
    +          "artix",
    +          "budgie",
    +          "centos",
    +          "coreos",
    +          "debian",
    +          "deepin",
    +          "devuan",
    +          "docker",
    +          "elementary",
    +          "fedora",
    +          "ferris",
    +          "flathub",
    +          "freebsd",
    +          "gentoo",
    +          "gnu-guix",
    +          "illumos",
    +          "kali-linux",
    +          "linuxmint",
    +          "mageia",
    +          "mandriva",
    +          "manjaro",
    +          "nixos",
    +          "openbsd",
    +          "opensuse",
    +          "pop-os",
    +          "raspberry-pi",
    +          "redhat",
    +          "rocky-linux",
    +          "sabayon",
    +          "slackware",
    +          "snappy",
    +          "solus",
    +          "tux",
    +          "ubuntu",
    +          "void",
    +          "zorin"].indexOf(str)
    +          > -1) {
    +            return str;
    +        }
    +        if (['openwrt','linux'].indexOf(str) > -1) {
    +          return 'tux';
    +        }
    +        if (str == 'amazon') {
    +          return 'redhat';
    +        }
    +        if (str == 'arch') {
    +          return 'archlinux';
    +        }
    +        return '';
    +      },
    +      group() {
    +        this.groups = groupingData(this.data, "Tag")
    +      },
    +      formatPercent(live, used, total) {
    +        const percent = live ? (parseInt(used / total * 100) || 0) : -1
    +        if (!this.cache[percent]) {
    +          this.cache[percent] = {
    +            class: {
    +              ui: true,
    +              progress: true,
    +            },
    +            style: {
    +              'transition-duration': '300ms',
    +              'min-width': 'unset',
    +              width: percent + '% !important',
    +            },
    +            percent,
    +          }
    +          if (percent < 0) {
    +            this.cache[percent].style['background-color'] = 'slategray'
    +            this.cache[percent].class.offline = true
    +          } else if (percent < 51) {
    +            this.cache[percent].style['background-color'] = '#0a94f2'
    +            this.cache[percent].class.fine = true
    +          } else if (percent < 81) {
    +            this.cache[percent].style['background-color'] = 'orange'
    +            this.cache[percent].class.warning = true
    +          } else {
    +            this.cache[percent].style['background-color'] = 'crimson'
    +            this.cache[percent].class.error = true
    +          }
    +        }
    +        return this.cache[percent]
    +      },
    +      secondToDate(s) {
    +        var d = Math.floor(s / 3600 / 24);
    +        if (d > 0) {
    +          return d + " {{tr "Day"}}"
    +        }
    +        var h = Math.floor(s / 3600 % 24);
    +        var m = Math.floor(s / 60 % 60);
    +        var s = Math.floor(s % 60);
    +        return h + ":" + ("0" + m).slice(-2) + ":" + ("0" + s).slice(-2);
    +      },
    +      formatTimestamp(t) {
    +        return new Date(t * 1000).toLocaleString()
    +      },
    +      formatByteSize(bs) {
    +        const x = readableBytes(bs)
    +        return x != "NaN undefined" ? x : '0B'
    +      },
    +      getCoreAndGHz(str){
    +        if((str || []).hasOwnProperty(0) === false){
    +            return '';
    +        }
    +        str = str[0];
    +	    let GHz = str.match(/(\d|\.)+GHz/g);
    +	    let Core = str.match(/(\d|\.)+ Physical/g);
    +	    GHz = GHz!==null?GHz.hasOwnProperty(0)===false?'':GHz[0]:''
    +	    Core = Core!==null?Core.hasOwnProperty(0)===false?'?':Core[0]:'?'
    +	    if(Core === '?'){
    +	        let Core = str.match(/(\d|\.)+ Virtual/g);
    +	        Core = Core!==null?Core.hasOwnProperty(0)===false?'?':Core[0]:'?'
    +	        return Core.replace('Virtual','Core')
    +	    }
    +	    return Core.replace('Physical','Core');
    +	   
    +      },
    +      getK2Gb(bs){
    +          bs = bs / 1024 /1024 /1024;
    +          return Math.ceil(bs.toFixed(2)) + 'GB';
    +      },
    +      listTipsMouseenter(obj,strs,tipsNum=1){
    +          this.layerIndex = layer.tips(strs, '#'+obj,{tips: [tipsNum, 'rgb(0 0 0 / 85%)'],time:0});
    +          $('#'+obj).attr('layerIndex',this.layerIndex)
    +      },
    +      listTipsMouseleave(obj){
    +          layer.close(this.layerIndex)
    +      }
    +    }
    +  })
    +
    +  function groupingData(data, field) {
    +    let map = {};
    +    let dest = [];
    +    data.forEach(item => {
    +      if (!map[item[field]]) {
    +        dest.push({
    +          [field]: item[field],
    +          data: [item]
    +        });
    +        map[item[field]] = item;
    +      } else {
    +        dest.forEach(dItem => {
    +          if (dItem[field] == item[field]) {
    +            dItem.data.push(item);
    +          }
    +        });
    +      }
    +    })
    +    return dest;
    +  }
    +
    +  let canShowError = true;
    +  function connect() {
    +    const wsProtocol = window.location.protocol == "https:" ? "wss" : "ws"
    +    const ws = new WebSocket(wsProtocol + '://' + window.location.host + '/ws');
    +    ws.onopen = function (evt) {
    +      canShowError = true;
    +      $.suiAlert({
    +        title: '{{tr "RealtimeChannelEstablished"}}',
    +        description: '{{tr "GetTheLatestMonitoringDataInRealTime"}}',
    +        type: 'success',
    +        time: '2',
    +        position: 'top-center',
    +      });
    +    }
    +    ws.onmessage = function (evt) {
    +      const oldServers = statusCards.servers
    +      const data = JSON.parse(evt.data)
    +      statusCards.servers = data.servers
    +      for (let i = 0; i < statusCards.servers.length; i++) {
    +        const ns = statusCards.servers[i];
    +        if (!ns.Host) ns.live = false
    +        else {
    +          const lastActive = new Date(ns.LastActive).getTime()
    +          if (data.now - lastActive > 10 * 1000) {
    +            ns.live = false
    +          } else {
    +            ns.live = true
    +          }
    +        }
    +      }
    +      statusCards.groups = groupingData(statusCards.servers, "Tag")
    +    }
    +    ws.onclose = function () {
    +      if (canShowError) {
    +        canShowError = false;
    +        $.suiAlert({
    +          title: '{{tr "RealtimeChannelDisconnect"}}',
    +          description: '{{tr "CanNotGetTheLatestMonitoringDataInRealTime"}}',
    +          type: 'warning',
    +          time: '2',
    +          position: 'top-center',
    +        });
    +      }
    +      setTimeout(function () {
    +        connect()
    +      }, 3000);
    +    }
    +    ws.onerror = function () {
    +      ws.close()
    +    }
    +  }
    +
    +  connect();
    +
    +  $('.ui.accordion').accordion({ "exclusive": false });
    +</script>
    +{{end}}

重启容器

docker restart dashboard-dashboard-1

后台管理自定义CSS

html
<style>
+/* 屏幕适配 */
+@media only screen and (min-width: 1200px) {
+.ui.container {
+width: 80% !important;
+}
+}
+ 
+@media only screen and (max-width: 767px) {
+.ui.card>.content>.header:not(.ui), .ui.cards>.card>.content>.header:not(.ui) {
+    margin-top: 0.4em !important;
+}
+}
+ 
+/* 整体图标 */
+i.icon {
+color: #000;
+width: 1.2em !important;
+}
+ 
+/* 背景图片 */
+body {
+content: " " !important;
+background: fixed !important;
+z-index: -1 !important;
+top: 0 !important;
+right: 0 !important;
+bottom: 0 !important;
+left: 0 !important;
+background-position: top !important;
+background-repeat: no-repeat !important;
+background-size: cover !important;
+//background-image: url(https://api.kdcc.cn/img/) !important;
+font-family: Arial,Helvetica,sans-serif !important;
+}
+ 
+/* 导航栏 */
+.ui.large.menu {
+border: 0 !important;
+border-radius: 0px !important;
+background-color: rgba(255, 255, 255, 55%) !important;
+}
+ 
+/* 首页按钮 */
+.ui.menu .active.item {
+background-color: transparent !important;
+}
+ 
+/* 导航栏下拉框 */
+.ui.dropdown .menu {
+border: 0 !important;
+border-radius: 0 !important;
+background-color: rgba(255, 255, 255, 80%) !important;
+}
+ 
+/* 登陆按钮 */
+.nezha-primary-btn {
+background-color: transparent !important;
+color: #000 !important;
+}
+ 
+/* 大卡片 */
+#app .ui.fluid.accordion {
+background-color: #fbfbfb26 !important;
+border-radius: 0.4rem !important;
+}
+ 
+/* 小卡片 */
+.ui.four.cards>.card {
+border-radius: 0.6rem !important;
+background-color: #fafafaa3 !important;
+}
+ 
+.status.cards .wide.column {
+padding-top: 0 !important;
+padding-bottom: 0 !important;
+height: 3.3rem !important;
+}
+ 
+.status.cards .three.wide.column {
+padding-right: 0rem !important;
+}
+ 
+.status.cards .wide.column:nth-child(1) {
+margin-top: 2rem !important;
+}
+ 
+.status.cards .wide.column:nth-child(2) {
+margin-top: 2rem !important;
+}
+ 
+.status.cards .description {
+padding-bottom: 0 !important;
+}
+ 
+/* 小鸡名 */
+.status.cards .flag {
+margin-right: 0.5rem !important;
+}
+ 
+/* 弹出卡片图标 */
+.status.cards .header > .info.icon {
+margin-right: 0 !important;
+}
+ 
+.nezha-secondary-font {
+color: #21ba45 !important;
+}
+ 
+/* 进度条 */
+.ui.progress {
+border-radius: 50rem !important;
+}
+ 
+.ui.progress .bar {
+min-width: 1.8em !important;
+border-radius: 15px !important;
+line-height: 1.65em !important;
+}
+ 
+.ui.fine.progress> .bar {
+background-color: #21ba45 !important;
+}
+ 
+.ui.progress> .bar {
+background-color: #000 !important;
+}
+ 
+.ui.progress.fine .bar {
+background-color: #21ba45 !important;
+}
+ 
+.ui.progress.warning .bar {
+background-color: #ff9800 !important;
+}
+ 
+.ui.progress.error .bar {
+background-color: #e41e10 !important;
+}
+ 
+.ui.progress.offline .bar {
+background-color: #000 !important;
+}
+ 
+/* 上传下载 */
+.status.cards .outline.icon {
+margin-right: 1px !important;
+}
+ 
+ 
+i.arrow.alternate.circle.down.outline.icon
+{
+color: #21ba45 !important;
+}
+i.arrow.alternate.circle.up.outline.icon
+ 
+ 
+{
+color: red !important;
+}
+ 
+/* 弹出卡片小箭头 */
+.ui.right.center.popup {
+margin: -3px 0 0 0.914286em !important;
+-webkit-transform-origin: left 50% !important;
+transform-origin: left 50% !important;
+}
+ 
+.ui.bottom.left.popup {
+margin-left: 1px !important;
+margin-top: 3px !important;
+}
+ 
+.ui.top.left.popup {
+margin-left: 0 !important;
+margin-bottom: 10px !important;
+}
+ 
+.ui.top.right.popup {
+margin-right: 0 !important;
+margin-bottom: 8px !important;
+}
+ 
+.ui.left.center.popup {
+margin: -3px .91428571em 0 0 !important;
+-webkit-transform-origin: right 50% !important;
+transform-origin: right 50% !important;
+}
+ 
+.ui.right.center.popup:before,
+.ui.left.center.popup:before {
+border: 0px solid #fafafaeb !important;
+background: #fafafaeb !important;
+}
+ 
+.ui.top.popup:before {
+border-color: #fafafaeb transparent transparent !important;
+}
+ 
+.ui.popup:before {
+border-color: #fafafaeb transparent transparent !important;
+}
+ 
+.ui.bottom.left.popup:before {
+border-radius: 0 !important;
+border: 1px solid transparent !important;
+border-color: #fafafaeb transparent transparent !important;
+background: #fafafaeb !important;
+-webkit-box-shadow: 0px 0px 0 0 
+#fafafaeb !important;
+box-shadow: 0px 0px 0 0 #fafafaeb !important;
+-webkit-tap-highlight-color: rgba(0,0,0,0) !important;
+}
+ 
+.ui.bottom.right.popup:before {
+border-radius: 0 !important;
+border: 1px solid transparent !important;
+border-color: #fafafaeb transparent transparent !important;
+background: #fafafaeb !important
+-webkit-box-shadow: 0px 0px 0 0 #fafafaeb !important;
+box-shadow: 0px 0px 0 0 #fafafaeb !important;
+-webkit-tap-highlight-color: rgba(0,0,0,0) !important;
+}
+ 
+.ui.top.left.popup:before {
+border-radius: 0 !important;
+border: 1px solid transparent !important;
+border-color: #fafafaeb transparent transparent !important;
+background: #fafafaeb !important;
+-webkit-box-shadow: 0px 0px 0 0 #fafafaeb !important;
+box-shadow: 0px 0px 0 0 #fafafaeb !important;
+-webkit-tap-highlight-color: rgba(0,0,0,0) !important;
+}
+ 
+.ui.top.right.popup:before {
+border-radius: 0 !important;
+border: 1px solid transparent !important;
+border-color: #fafafaeb transparent transparent !important;
+background: #fafafaeb !important;
+-webkit-box-shadow: 0px 0px 0 0 #fafafaeb !important;
+box-shadow: 0px 0px 0 0 #fafafaeb !important;
+-webkit-tap-highlight-color: rgba(0,0,0,0) !important;
+}
+ 
+.ui.left.center.popup:before {
+border-radius: 0 !important;
+border: 1px solid transparent !important;
+border-color: #fafafaeb transparent transparent !important;
+background: #fafafaeb !important;
+-webkit-box-shadow: 0px 0px 0 0 #fafafaeb !important;
+box-shadow: 0px 0px 0 0 #fafafaeb !important;
+-webkit-tap-highlight-color: rgba(0,0,0,0) !important;
+}
+ 
+/* 弹出卡片 */
+.status.cards .ui.content.popup {
+min-width: 20rem !important;
+line-height: 2rem !important;
+border-radius: 5px !important;
+border: 1px solid transparent !important;
+background-color: #fafafaeb !important;
+font-family: Arial,Helvetica,sans-serif !important;
+}
+ 
+.ui.content {
+margin: 0 !important;
+padding: 1em !important;
+}
+ 
+/* 服务页 */
+.ui.table {
+background: RGB(225,225,225,0.6) !important;
+}
+ 
+.ui.table thead th {
+background: transparent !important;
+}
+ 
+/* 服务页进度条 */
+.service-status .good {
+background-color: #21ba45 !important;
+}
+ 
+.service-status .danger {
+background-color: red !important;
+}
+ 
+.service-status .warning {
+background-color: orange !important;
+}
+ 
+/* 版权 */
+.ui.inverted.segment, 
+.ui.primary.inverted.segment {
+color: #000 !important;
+font-weight: bold !important;
+background-color: #fafafaa3 !important;
+}
+</style>
+
+<script>
+window.onload = function(){
+var avatar=document.querySelector(".item img")
+var footer=document.querySelector("div.is-size-7")
+footer.innerHTML="Copyright © 2023 All Rights Reserved."
+footer.style.visibility="visible"
+avatar.src="/static/logo.svg"
+avatar.style.visibility="visible"
+}
+</script>
<style>
+/* 屏幕适配 */
+@media only screen and (min-width: 1200px) {
+.ui.container {
+width: 80% !important;
+}
+}
+ 
+@media only screen and (max-width: 767px) {
+.ui.card>.content>.header:not(.ui), .ui.cards>.card>.content>.header:not(.ui) {
+    margin-top: 0.4em !important;
+}
+}
+ 
+/* 整体图标 */
+i.icon {
+color: #000;
+width: 1.2em !important;
+}
+ 
+/* 背景图片 */
+body {
+content: " " !important;
+background: fixed !important;
+z-index: -1 !important;
+top: 0 !important;
+right: 0 !important;
+bottom: 0 !important;
+left: 0 !important;
+background-position: top !important;
+background-repeat: no-repeat !important;
+background-size: cover !important;
+//background-image: url(https://api.kdcc.cn/img/) !important;
+font-family: Arial,Helvetica,sans-serif !important;
+}
+ 
+/* 导航栏 */
+.ui.large.menu {
+border: 0 !important;
+border-radius: 0px !important;
+background-color: rgba(255, 255, 255, 55%) !important;
+}
+ 
+/* 首页按钮 */
+.ui.menu .active.item {
+background-color: transparent !important;
+}
+ 
+/* 导航栏下拉框 */
+.ui.dropdown .menu {
+border: 0 !important;
+border-radius: 0 !important;
+background-color: rgba(255, 255, 255, 80%) !important;
+}
+ 
+/* 登陆按钮 */
+.nezha-primary-btn {
+background-color: transparent !important;
+color: #000 !important;
+}
+ 
+/* 大卡片 */
+#app .ui.fluid.accordion {
+background-color: #fbfbfb26 !important;
+border-radius: 0.4rem !important;
+}
+ 
+/* 小卡片 */
+.ui.four.cards>.card {
+border-radius: 0.6rem !important;
+background-color: #fafafaa3 !important;
+}
+ 
+.status.cards .wide.column {
+padding-top: 0 !important;
+padding-bottom: 0 !important;
+height: 3.3rem !important;
+}
+ 
+.status.cards .three.wide.column {
+padding-right: 0rem !important;
+}
+ 
+.status.cards .wide.column:nth-child(1) {
+margin-top: 2rem !important;
+}
+ 
+.status.cards .wide.column:nth-child(2) {
+margin-top: 2rem !important;
+}
+ 
+.status.cards .description {
+padding-bottom: 0 !important;
+}
+ 
+/* 小鸡名 */
+.status.cards .flag {
+margin-right: 0.5rem !important;
+}
+ 
+/* 弹出卡片图标 */
+.status.cards .header > .info.icon {
+margin-right: 0 !important;
+}
+ 
+.nezha-secondary-font {
+color: #21ba45 !important;
+}
+ 
+/* 进度条 */
+.ui.progress {
+border-radius: 50rem !important;
+}
+ 
+.ui.progress .bar {
+min-width: 1.8em !important;
+border-radius: 15px !important;
+line-height: 1.65em !important;
+}
+ 
+.ui.fine.progress> .bar {
+background-color: #21ba45 !important;
+}
+ 
+.ui.progress> .bar {
+background-color: #000 !important;
+}
+ 
+.ui.progress.fine .bar {
+background-color: #21ba45 !important;
+}
+ 
+.ui.progress.warning .bar {
+background-color: #ff9800 !important;
+}
+ 
+.ui.progress.error .bar {
+background-color: #e41e10 !important;
+}
+ 
+.ui.progress.offline .bar {
+background-color: #000 !important;
+}
+ 
+/* 上传下载 */
+.status.cards .outline.icon {
+margin-right: 1px !important;
+}
+ 
+ 
+i.arrow.alternate.circle.down.outline.icon
+{
+color: #21ba45 !important;
+}
+i.arrow.alternate.circle.up.outline.icon
+ 
+ 
+{
+color: red !important;
+}
+ 
+/* 弹出卡片小箭头 */
+.ui.right.center.popup {
+margin: -3px 0 0 0.914286em !important;
+-webkit-transform-origin: left 50% !important;
+transform-origin: left 50% !important;
+}
+ 
+.ui.bottom.left.popup {
+margin-left: 1px !important;
+margin-top: 3px !important;
+}
+ 
+.ui.top.left.popup {
+margin-left: 0 !important;
+margin-bottom: 10px !important;
+}
+ 
+.ui.top.right.popup {
+margin-right: 0 !important;
+margin-bottom: 8px !important;
+}
+ 
+.ui.left.center.popup {
+margin: -3px .91428571em 0 0 !important;
+-webkit-transform-origin: right 50% !important;
+transform-origin: right 50% !important;
+}
+ 
+.ui.right.center.popup:before,
+.ui.left.center.popup:before {
+border: 0px solid #fafafaeb !important;
+background: #fafafaeb !important;
+}
+ 
+.ui.top.popup:before {
+border-color: #fafafaeb transparent transparent !important;
+}
+ 
+.ui.popup:before {
+border-color: #fafafaeb transparent transparent !important;
+}
+ 
+.ui.bottom.left.popup:before {
+border-radius: 0 !important;
+border: 1px solid transparent !important;
+border-color: #fafafaeb transparent transparent !important;
+background: #fafafaeb !important;
+-webkit-box-shadow: 0px 0px 0 0 
+#fafafaeb !important;
+box-shadow: 0px 0px 0 0 #fafafaeb !important;
+-webkit-tap-highlight-color: rgba(0,0,0,0) !important;
+}
+ 
+.ui.bottom.right.popup:before {
+border-radius: 0 !important;
+border: 1px solid transparent !important;
+border-color: #fafafaeb transparent transparent !important;
+background: #fafafaeb !important
+-webkit-box-shadow: 0px 0px 0 0 #fafafaeb !important;
+box-shadow: 0px 0px 0 0 #fafafaeb !important;
+-webkit-tap-highlight-color: rgba(0,0,0,0) !important;
+}
+ 
+.ui.top.left.popup:before {
+border-radius: 0 !important;
+border: 1px solid transparent !important;
+border-color: #fafafaeb transparent transparent !important;
+background: #fafafaeb !important;
+-webkit-box-shadow: 0px 0px 0 0 #fafafaeb !important;
+box-shadow: 0px 0px 0 0 #fafafaeb !important;
+-webkit-tap-highlight-color: rgba(0,0,0,0) !important;
+}
+ 
+.ui.top.right.popup:before {
+border-radius: 0 !important;
+border: 1px solid transparent !important;
+border-color: #fafafaeb transparent transparent !important;
+background: #fafafaeb !important;
+-webkit-box-shadow: 0px 0px 0 0 #fafafaeb !important;
+box-shadow: 0px 0px 0 0 #fafafaeb !important;
+-webkit-tap-highlight-color: rgba(0,0,0,0) !important;
+}
+ 
+.ui.left.center.popup:before {
+border-radius: 0 !important;
+border: 1px solid transparent !important;
+border-color: #fafafaeb transparent transparent !important;
+background: #fafafaeb !important;
+-webkit-box-shadow: 0px 0px 0 0 #fafafaeb !important;
+box-shadow: 0px 0px 0 0 #fafafaeb !important;
+-webkit-tap-highlight-color: rgba(0,0,0,0) !important;
+}
+ 
+/* 弹出卡片 */
+.status.cards .ui.content.popup {
+min-width: 20rem !important;
+line-height: 2rem !important;
+border-radius: 5px !important;
+border: 1px solid transparent !important;
+background-color: #fafafaeb !important;
+font-family: Arial,Helvetica,sans-serif !important;
+}
+ 
+.ui.content {
+margin: 0 !important;
+padding: 1em !important;
+}
+ 
+/* 服务页 */
+.ui.table {
+background: RGB(225,225,225,0.6) !important;
+}
+ 
+.ui.table thead th {
+background: transparent !important;
+}
+ 
+/* 服务页进度条 */
+.service-status .good {
+background-color: #21ba45 !important;
+}
+ 
+.service-status .danger {
+background-color: red !important;
+}
+ 
+.service-status .warning {
+background-color: orange !important;
+}
+ 
+/* 版权 */
+.ui.inverted.segment, 
+.ui.primary.inverted.segment {
+color: #000 !important;
+font-weight: bold !important;
+background-color: #fafafaa3 !important;
+}
+</style>
+
+<script>
+window.onload = function(){
+var avatar=document.querySelector(".item img")
+var footer=document.querySelector("div.is-size-7")
+footer.innerHTML="Copyright © 2023 All Rights Reserved."
+footer.style.visibility="visible"
+avatar.src="/static/logo.svg"
+avatar.style.visibility="visible"
+}
+</script>
+ + + + \ No newline at end of file diff --git "a/assets/actions_designpattern_\347\255\226\347\225\245\346\250\241\345\274\217\347\232\204\345\205\267\344\275\223\345\256\236\347\216\260.md.b828e5be.js" "b/assets/actions_designpattern_\347\255\226\347\225\245\346\250\241\345\274\217\347\232\204\345\205\267\344\275\223\345\256\236\347\216\260.md.b828e5be.js" new file mode 100644 index 000000000..541723cfb --- /dev/null +++ "b/assets/actions_designpattern_\347\255\226\347\225\245\346\250\241\345\274\217\347\232\204\345\205\267\344\275\223\345\256\236\347\216\260.md.b828e5be.js" @@ -0,0 +1,649 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const g=JSON.parse('{"title":"策略模式的具体实现","description":"","frontmatter":{},"headers":[],"relativePath":"actions/designpattern/策略模式的具体实现.md","filePath":"actions/designpattern/策略模式的具体实现.md","lastUpdated":1708510102000}'),p={name:"actions/designpattern/策略模式的具体实现.md"},o=l(`

策略模式的具体实现

背景

开发中经常有这样一种场景,一个接口需要处理的请求中的内容包含多种不同的类型。比如支付系统,订单支付的时候可能是支付宝支付,微信支付或者银联支付等。又或者是订单系统,订单可能是普通订单,可能是团购订单,也可能是秒杀订单。前阵子做的一个预览Office文件的功能也与之类似,文件的类型不同,也需要采取不同的处理方案。这时候最简单的做法就是在controller中写n多个if else:

java
if ( "excel".equals(file.getType)) {
+	//***
+} else if"word".equals(file.getType()){
+    //***
+} ……
if ( "excel".equals(file.getType)) {
+	//***
+} else if"word".equals(file.getType()){
+    //***
+} ……

如果后面再加其他的类型,那就继续加if else语句,这样代码就会变的很丑陋,而且每次都需要对controller代码进行修改,后续的扩展很麻烦。所以这种情况通常会采用策略模式来进行处理,这样我们的代码会变得更加优雅,方便后续的维护。

策略模式

策略模式是一种行为模式,主要作用是在程序运行时动态切换一个类的行为或者算法。我们需要做的就是创建一个定义行为的Strategy接口以及它的具体策略实现类,以及一个策略的上下文来动态切换策略。 无标题.png

下面将介绍具体的实现方案,以订单系统为例

环境搭建

xml
<parent>
+	<groupId>org.springframework.boot</groupId>
+	<artifactId>spring-boot-starter-parent</artifactId>
+	<version>2.1.4.RELEASE</version>
+</parent>
+<dependencies>
+	<dependency>
+		<groupId>org.projectlombok</groupId>
+		<artifactId>lombok</artifactId>
+		<version>1.18.6</version>
+	</dependency>
+	<dependency>
+		<groupId>org.springframework.boot</groupId>
+		<artifactId>spring-boot-starter-web</artifactId>
+		<version>2.1.4.RELEASE</version>
+	</dependency>
+	<dependency>
+		<groupId>org.apache.commons</groupId>
+		<artifactId>commons-lang3</artifactId>
+		<version>3.8.1</version>
+	</dependency>
+</dependencies>
<parent>
+	<groupId>org.springframework.boot</groupId>
+	<artifactId>spring-boot-starter-parent</artifactId>
+	<version>2.1.4.RELEASE</version>
+</parent>
+<dependencies>
+	<dependency>
+		<groupId>org.projectlombok</groupId>
+		<artifactId>lombok</artifactId>
+		<version>1.18.6</version>
+	</dependency>
+	<dependency>
+		<groupId>org.springframework.boot</groupId>
+		<artifactId>spring-boot-starter-web</artifactId>
+		<version>2.1.4.RELEASE</version>
+	</dependency>
+	<dependency>
+		<groupId>org.apache.commons</groupId>
+		<artifactId>commons-lang3</artifactId>
+		<version>3.8.1</version>
+	</dependency>
+</dependencies>

订单实体类

java
@Data
+public class Order {
+    private String code;
+    private BigDecimal price;
+    /**
+     * 1: 普通订单
+     * 2: 秒杀订单
+     * 3: 团购订单
+     */
+    private String type;
+}
@Data
+public class Order {
+    private String code;
+    private BigDecimal price;
+    /**
+     * 1: 普通订单
+     * 2: 秒杀订单
+     * 3: 团购订单
+     */
+    private String type;
+}

抽象策略接口

java
public interface OrderStrategy {
+    String handleOrder(Order order);
+}
public interface OrderStrategy {
+    String handleOrder(Order order);
+}

策略具体实现

java
@Component
+public class NormalHandler implements OrderStrategy {
+
+    @Override
+    public String handleOrder(Order order) {
+        return "普通订单处理完毕";
+    }
+}
+
+@Component
+public class GroupHandler implements OrderStrategy {
+    @Override
+    public String handleOrder(Order order) {
+        return "团购订单处理完毕";
+    }
+}
+
+@Component
+public class SecKillHandler implements OrderStrategy {
+    @Override
+    public String handleOrder(Order order) {
+        return "秒杀订单处理完毕";
+    }
+}
@Component
+public class NormalHandler implements OrderStrategy {
+
+    @Override
+    public String handleOrder(Order order) {
+        return "普通订单处理完毕";
+    }
+}
+
+@Component
+public class GroupHandler implements OrderStrategy {
+    @Override
+    public String handleOrder(Order order) {
+        return "团购订单处理完毕";
+    }
+}
+
+@Component
+public class SecKillHandler implements OrderStrategy {
+    @Override
+    public String handleOrder(Order order) {
+        return "秒杀订单处理完毕";
+    }
+}

SpringUtils

@Component
+public class SpringUtils implements ApplicationContextAware {
+
+    private static ApplicationContext applicationContext;
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        SpringUtils.applicationContext = applicationContext;
+    }
+
+    //获取applicationContext
+    private static ApplicationContext getApplicationContext() {
+        return applicationContext;
+    }
+
+    //通过name获取 Bean.
+    public static Object getBean(String name){
+        return getApplicationContext().getBean(name);
+    }
+
+    //通过class获取Bean.
+    public static <T> T getBean(Class<T> clazz){
+        return getApplicationContext().getBean(clazz);
+    }
+
+    //通过name,以及Clazz返回指定的Bean
+    public static <T> T getBean(String name,Class<T> clazz){
+        return getApplicationContext().getBean(name, clazz);
+    }
+
+}
@Component
+public class SpringUtils implements ApplicationContextAware {
+
+    private static ApplicationContext applicationContext;
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        SpringUtils.applicationContext = applicationContext;
+    }
+
+    //获取applicationContext
+    private static ApplicationContext getApplicationContext() {
+        return applicationContext;
+    }
+
+    //通过name获取 Bean.
+    public static Object getBean(String name){
+        return getApplicationContext().getBean(name);
+    }
+
+    //通过class获取Bean.
+    public static <T> T getBean(Class<T> clazz){
+        return getApplicationContext().getBean(clazz);
+    }
+
+    //通过name,以及Clazz返回指定的Bean
+    public static <T> T getBean(String name,Class<T> clazz){
+        return getApplicationContext().getBean(name, clazz);
+    }
+
+}

第一种实现

可以采取配置的方式,将不同类型和对应的handler的bean name配置在配置文件或者是数据库中,这样我们在context中可以直接获取配置文件中的bean name或者去数据库中查询,然后从spring容器中获取对应的bean并调用处理方法即可。

以将映射关系持久化到数据库为例,我们需要建一张表来维护类型和具体处理器之间的关系 字段为type和对应处理器的bean名称

controller

java
@RestController
+@RequestMapping("/api/order")
+public class OrderController {
+
+    @Autowired
+    private IOrderService orderService;
+
+
+    @GetMapping("/{type}")
+    public String handleOrder(@PathVariable String type){
+        return orderService.handleOrder(type);
+    }
+}
@RestController
+@RequestMapping("/api/order")
+public class OrderController {
+
+    @Autowired
+    private IOrderService orderService;
+
+
+    @GetMapping("/{type}")
+    public String handleOrder(@PathVariable String type){
+        return orderService.handleOrder(type);
+    }
+}

service

java
@Service
+public class OrderServiceImpl implements IOrderService {
+
+    @Autowired
+    private OrderStrategyContext context;
+
+    @Override
+    public String handleOrder(String type) {
+        return context.getBean(type).handleOrder(type);
+    }
+}
@Service
+public class OrderServiceImpl implements IOrderService {
+
+    @Autowired
+    private OrderStrategyContext context;
+
+    @Override
+    public String handleOrder(String type) {
+        return context.getBean(type).handleOrder(type);
+    }
+}

context

java
@Component
+public class OrderStrategyContext {
+    @Autowired
+	private StrategyMapper mapper;
+
+    public OrderStrategy getBean(String type){
+        String beanName = mapper.getBeanName(type);
+        return SpringUtils.getBean(beanName);
+    }
+
+}
@Component
+public class OrderStrategyContext {
+    @Autowired
+	private StrategyMapper mapper;
+
+    public OrderStrategy getBean(String type){
+        String beanName = mapper.getBeanName(type);
+        return SpringUtils.getBean(beanName);
+    }
+
+}

第二种实现

第一种方案相较于无尽的if else已经好很多了,但是还是需要增加配置文件或者数据库中新建表来维护类型和对应处理器的映射关系。还可以直接自定义注解来实现这个关系的对应。

自定义注解HandlerType

java
@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface HandlerType {
+    String value();
+}
@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface HandlerType {
+    String value();
+}

然后在每个具体策略类上加上注解

java
@Component
+@HandlerType("1")
+public class NormalHandler implements OrderStrategy {
+
+    @Override
+    public String handleOrder(String type) {
+        return "普通订单处理完毕";
+    }
+}
@Component
+@HandlerType("1")
+public class NormalHandler implements OrderStrategy {
+
+    @Override
+    public String handleOrder(String type) {
+        return "普通订单处理完毕";
+    }
+}
java
@Component
+@HandlerType("2")
+public class GroupHandler implements OrderStrategy {
+    @Override
+    public String handleOrder(String type) {
+        return "团购订单处理完毕";
+    }
+}
@Component
+@HandlerType("2")
+public class GroupHandler implements OrderStrategy {
+    @Override
+    public String handleOrder(String type) {
+        return "团购订单处理完毕";
+    }
+}
java
@Component
+@HandlerType("3")
+public class SecKillHandler implements OrderStrategy {
+
+    @Override
+    public String handleOrder(String type) {
+        return "秒杀订单处理完毕";
+    }
+}
@Component
+@HandlerType("3")
+public class SecKillHandler implements OrderStrategy {
+
+    @Override
+    public String handleOrder(String type) {
+        return "秒杀订单处理完毕";
+    }
+}

策略上下文context修改为

java
public class OrderStrategyContext {
+
+    private Map<String,Class> handlerMap;
+
+    public OrderStrategyContext(Map<String,Class> handlerMap){
+        this.handlerMap = handlerMap;
+    }
+
+    public OrderStrategy getBean(String type){
+        Class clazz = handlerMap.get(type);
+        if (clazz == null) {
+            throw new IllegalArgumentException("not found handler for type :" + type);
+        }
+        return (OrderStrategy) SpringUtils.getBean(clazz);
+    }
+}
public class OrderStrategyContext {
+
+    private Map<String,Class> handlerMap;
+
+    public OrderStrategyContext(Map<String,Class> handlerMap){
+        this.handlerMap = handlerMap;
+    }
+
+    public OrderStrategy getBean(String type){
+        Class clazz = handlerMap.get(type);
+        if (clazz == null) {
+            throw new IllegalArgumentException("not found handler for type :" + type);
+        }
+        return (OrderStrategy) SpringUtils.getBean(clazz);
+    }
+}

自定义注解后,我们需要将注解的value和对应策略类的bean_name放到上下文的handlerMap中,并将策略上下文对象注册到spring容器里,需要一个处理类HandlerProcessor

java
@Component
+public class HandlerProcessor implements BeanFactoryPostProcessor {
+
+    private static final String HANDLE_PACKAGE = "com.test.handler";
+
+    @Override
+    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
+        Map<String,Class> map = new HashMap<>();
+        ClassScaner.scan(HANDLE_PACKAGE, HandlerType.class).forEach(clazz -> {
+            //获取注解中对应的类型
+            String type = clazz.getAnnotation(HandlerType.class).value();
+            //注解的类型值作为key,对应的类作为value,存储在map中
+            map.put(type,clazz);
+        });
+        //初始化HandlerContext,注册到Spring容器中
+        OrderStrategyContext context = new OrderStrategyContext(map);
+        beanFactory.registerSingleton(OrderStrategyContext.class.getName(),context);
+    }
+}
@Component
+public class HandlerProcessor implements BeanFactoryPostProcessor {
+
+    private static final String HANDLE_PACKAGE = "com.test.handler";
+
+    @Override
+    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
+        Map<String,Class> map = new HashMap<>();
+        ClassScaner.scan(HANDLE_PACKAGE, HandlerType.class).forEach(clazz -> {
+            //获取注解中对应的类型
+            String type = clazz.getAnnotation(HandlerType.class).value();
+            //注解的类型值作为key,对应的类作为value,存储在map中
+            map.put(type,clazz);
+        });
+        //初始化HandlerContext,注册到Spring容器中
+        OrderStrategyContext context = new OrderStrategyContext(map);
+        beanFactory.registerSingleton(OrderStrategyContext.class.getName(),context);
+    }
+}

ClassScaner

java
public class ClassScaner implements ResourceLoaderAware {
+
+    private final List<TypeFilter> includeFilters = new LinkedList<TypeFilter>();
+    private final List<TypeFilter> excludeFilters = new LinkedList<TypeFilter>();
+
+    private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
+    private MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(this.resourcePatternResolver);
+
+    @SafeVarargs
+    public static Set<Class<?>> scan(String[] basePackages, Class<? extends Annotation>... annotations) {
+        ClassScaner cs = new ClassScaner();
+
+        if (ArrayUtils.isNotEmpty(annotations)) {
+            for (Class anno : annotations) {
+                cs.addIncludeFilter(new AnnotationTypeFilter(anno));
+            }
+        }
+
+        Set<Class<?>> classes = new HashSet<>();
+        for (String s : basePackages) {
+            classes.addAll(cs.doScan(s));
+        }
+
+        return classes;
+    }
+
+    @SafeVarargs
+    public static Set<Class<?>> scan(String basePackages, Class<? extends Annotation>... annotations) {
+        return ClassScaner.scan(StringUtils.tokenizeToStringArray(basePackages, ",; \\t\\n"), annotations);
+    }
+
+    public final ResourceLoader getResourceLoader() {
+        return this.resourcePatternResolver;
+    }
+
+    @Override
+    public void setResourceLoader(ResourceLoader resourceLoader) {
+        this.resourcePatternResolver = ResourcePatternUtils
+                .getResourcePatternResolver(resourceLoader);
+        this.metadataReaderFactory = new CachingMetadataReaderFactory(
+                resourceLoader);
+    }
+
+    public void addIncludeFilter(TypeFilter includeFilter) {
+        this.includeFilters.add(includeFilter);
+    }
+
+    public void addExcludeFilter(TypeFilter excludeFilter) {
+        this.excludeFilters.add(0, excludeFilter);
+    }
+
+    public void resetFilters(boolean useDefaultFilters) {
+        this.includeFilters.clear();
+        this.excludeFilters.clear();
+    }
+
+    public Set<Class<?>> doScan(String basePackage) {
+        Set<Class<?>> classes = new HashSet<>();
+        try {
+            String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
+                    + org.springframework.util.ClassUtils
+                    .convertClassNameToResourcePath(SystemPropertyUtils
+                            .resolvePlaceholders(basePackage))
+                    + "/**/*.class";
+            Resource[] resources = this.resourcePatternResolver
+                    .getResources(packageSearchPath);
+
+            for (int i = 0; i < resources.length; i++) {
+                Resource resource = resources[i];
+                if (resource.isReadable()) {
+                    MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
+                    if ((includeFilters.size() == 0 && excludeFilters.size() == 0) || matches(metadataReader)) {
+                        try {
+                            classes.add(Class.forName(metadataReader
+                                    .getClassMetadata().getClassName()));
+                        } catch (ClassNotFoundException e) {
+                            e.printStackTrace();
+                        }
+                    }
+                }
+            }
+        } catch (IOException ex) {
+            throw new BeanDefinitionStoreException(
+                    "I/O failure during classpath scanning", ex);
+        }
+        return classes;
+    }
+
+    protected boolean matches(MetadataReader metadataReader) throws IOException {
+        for (TypeFilter tf : this.excludeFilters) {
+            if (tf.match(metadataReader, this.metadataReaderFactory)) {
+                return false;
+            }
+        }
+        for (TypeFilter tf : this.includeFilters) {
+            if (tf.match(metadataReader, this.metadataReaderFactory)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
public class ClassScaner implements ResourceLoaderAware {
+
+    private final List<TypeFilter> includeFilters = new LinkedList<TypeFilter>();
+    private final List<TypeFilter> excludeFilters = new LinkedList<TypeFilter>();
+
+    private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
+    private MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(this.resourcePatternResolver);
+
+    @SafeVarargs
+    public static Set<Class<?>> scan(String[] basePackages, Class<? extends Annotation>... annotations) {
+        ClassScaner cs = new ClassScaner();
+
+        if (ArrayUtils.isNotEmpty(annotations)) {
+            for (Class anno : annotations) {
+                cs.addIncludeFilter(new AnnotationTypeFilter(anno));
+            }
+        }
+
+        Set<Class<?>> classes = new HashSet<>();
+        for (String s : basePackages) {
+            classes.addAll(cs.doScan(s));
+        }
+
+        return classes;
+    }
+
+    @SafeVarargs
+    public static Set<Class<?>> scan(String basePackages, Class<? extends Annotation>... annotations) {
+        return ClassScaner.scan(StringUtils.tokenizeToStringArray(basePackages, ",; \\t\\n"), annotations);
+    }
+
+    public final ResourceLoader getResourceLoader() {
+        return this.resourcePatternResolver;
+    }
+
+    @Override
+    public void setResourceLoader(ResourceLoader resourceLoader) {
+        this.resourcePatternResolver = ResourcePatternUtils
+                .getResourcePatternResolver(resourceLoader);
+        this.metadataReaderFactory = new CachingMetadataReaderFactory(
+                resourceLoader);
+    }
+
+    public void addIncludeFilter(TypeFilter includeFilter) {
+        this.includeFilters.add(includeFilter);
+    }
+
+    public void addExcludeFilter(TypeFilter excludeFilter) {
+        this.excludeFilters.add(0, excludeFilter);
+    }
+
+    public void resetFilters(boolean useDefaultFilters) {
+        this.includeFilters.clear();
+        this.excludeFilters.clear();
+    }
+
+    public Set<Class<?>> doScan(String basePackage) {
+        Set<Class<?>> classes = new HashSet<>();
+        try {
+            String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
+                    + org.springframework.util.ClassUtils
+                    .convertClassNameToResourcePath(SystemPropertyUtils
+                            .resolvePlaceholders(basePackage))
+                    + "/**/*.class";
+            Resource[] resources = this.resourcePatternResolver
+                    .getResources(packageSearchPath);
+
+            for (int i = 0; i < resources.length; i++) {
+                Resource resource = resources[i];
+                if (resource.isReadable()) {
+                    MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
+                    if ((includeFilters.size() == 0 && excludeFilters.size() == 0) || matches(metadataReader)) {
+                        try {
+                            classes.add(Class.forName(metadataReader
+                                    .getClassMetadata().getClassName()));
+                        } catch (ClassNotFoundException e) {
+                            e.printStackTrace();
+                        }
+                    }
+                }
+            }
+        } catch (IOException ex) {
+            throw new BeanDefinitionStoreException(
+                    "I/O failure during classpath scanning", ex);
+        }
+        return classes;
+    }
+
+    protected boolean matches(MetadataReader metadataReader) throws IOException {
+        for (TypeFilter tf : this.excludeFilters) {
+            if (tf.match(metadataReader, this.metadataReaderFactory)) {
+                return false;
+            }
+        }
+        for (TypeFilter tf : this.includeFilters) {
+            if (tf.match(metadataReader, this.metadataReaderFactory)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}

第三种方案

扫描指定的包还是很麻烦,还可以直接使用ioc容器来直接进行操作 将OrderStrategyContext进行修改,不再需要processor类

java
@Component
+public class OrderStrategyContext implements ApplicationContextAware, CommandLineRunner {
+
+    private Map<String,Object> handlerMap = new HashMap<>();
+
+    public OrderStrategy getInstance(String type) {
+        Object obj = handlerMap.get(type);
+        if (obj == null) {
+            throw new IllegalArgumentException("handler not found for type : " + type);
+        }
+        if (obj instanceof OrderStrategy) {
+            return (OrderStrategy) obj;
+        } else {
+            throw new IllegalArgumentException("handler not found for type : " + type);
+        }
+    }
+
+    private ApplicationContext context;
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        this.context = applicationContext;
+    }
+
+
+    @Override
+    public void run(String... args) throws Exception {
+        this.loadBean();
+    }
+
+    public void loadBean() {
+        Map<String, Object> beansWithAnnotation = context.getBeansWithAnnotation(HandlerType.class);
+        beansWithAnnotation.forEach((handlerBeanName,handlerBean)->{
+            Class<?> clazz = handlerBean.getClass();
+            HandlerType annotation = clazz.getAnnotation(HandlerType.class);
+            String annotationValue = annotation.value();
+            handlerMap.put(annotationValue,handlerBean);
+        });
+    }
+}
@Component
+public class OrderStrategyContext implements ApplicationContextAware, CommandLineRunner {
+
+    private Map<String,Object> handlerMap = new HashMap<>();
+
+    public OrderStrategy getInstance(String type) {
+        Object obj = handlerMap.get(type);
+        if (obj == null) {
+            throw new IllegalArgumentException("handler not found for type : " + type);
+        }
+        if (obj instanceof OrderStrategy) {
+            return (OrderStrategy) obj;
+        } else {
+            throw new IllegalArgumentException("handler not found for type : " + type);
+        }
+    }
+
+    private ApplicationContext context;
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        this.context = applicationContext;
+    }
+
+
+    @Override
+    public void run(String... args) throws Exception {
+        this.loadBean();
+    }
+
+    public void loadBean() {
+        Map<String, Object> beansWithAnnotation = context.getBeansWithAnnotation(HandlerType.class);
+        beansWithAnnotation.forEach((handlerBeanName,handlerBean)->{
+            Class<?> clazz = handlerBean.getClass();
+            HandlerType annotation = clazz.getAnnotation(HandlerType.class);
+            String annotationValue = annotation.value();
+            handlerMap.put(annotationValue,handlerBean);
+        });
+    }
+}

启动项目并测试

1.jpg3.jpg

`,46),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const u=s(p,[["render",t]]);export{g as __pageData,u as default}; diff --git "a/assets/actions_designpattern_\347\255\226\347\225\245\346\250\241\345\274\217\347\232\204\345\205\267\344\275\223\345\256\236\347\216\260.md.b828e5be.lean.js" "b/assets/actions_designpattern_\347\255\226\347\225\245\346\250\241\345\274\217\347\232\204\345\205\267\344\275\223\345\256\236\347\216\260.md.b828e5be.lean.js" new file mode 100644 index 000000000..7ed702ae6 --- /dev/null +++ "b/assets/actions_designpattern_\347\255\226\347\225\245\346\250\241\345\274\217\347\232\204\345\205\267\344\275\223\345\256\236\347\216\260.md.b828e5be.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const g=JSON.parse('{"title":"策略模式的具体实现","description":"","frontmatter":{},"headers":[],"relativePath":"actions/designpattern/策略模式的具体实现.md","filePath":"actions/designpattern/策略模式的具体实现.md","lastUpdated":1708510102000}'),p={name:"actions/designpattern/策略模式的具体实现.md"},o=l("",46),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const u=s(p,[["render",t]]);export{g as __pageData,u as default}; diff --git "a/assets/actions_designpattern_\350\264\243\344\273\273\351\223\276\346\250\241\345\274\217.md.952322e1.js" "b/assets/actions_designpattern_\350\264\243\344\273\273\351\223\276\346\250\241\345\274\217.md.952322e1.js" new file mode 100644 index 000000000..080555b16 --- /dev/null +++ "b/assets/actions_designpattern_\350\264\243\344\273\273\351\223\276\346\250\241\345\274\217.md.952322e1.js" @@ -0,0 +1,101 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"责任链模式","description":"","frontmatter":{},"headers":[],"relativePath":"actions/designpattern/责任链模式.md","filePath":"actions/designpattern/责任链模式.md","lastUpdated":1708510102000}'),p={name:"actions/designpattern/责任链模式.md"},o=l(`

责任链模式

背景

根据不同的订单结算金额规则配置,来计算出每一条订单商品的结算金额,每条规则都有自己的匹配条件,匹配上的则应用该条规则所配置的结算金额计算公式。客户可以配置多条不同的规则,组成一条规则链。每条订单商品记录按规则的顺序依次进行条件匹配,如果匹配上则停止,匹配不上继续,直至规则链结束,没有匹配则使用默认规则进行兜底。

责任链模式

责任链模式(Chain of Responsibility Pattern)是一种软件设计模式,它可以让多个对象都有机会处理请求,从而避免了请求的发送者和接收者之间的耦合关系。在责任链模式中,每个对象都有其对应的处理请求的方法,如果一个对象不能够处理该请求,那么它会将这个请求传递给下一个对象来处理,直到找到能够处理该请求的对象为止。

责任链模式通常由以下几个角色组成:

  1. 抽象处理者(Handler):定义了处理请求的接口,同时也可以实现一些公共的处理逻辑;
  2. 具体处理者(Concrete Handler):继承自抽象处理者,实现了具体的处理方法,如果能够处理该请求,则处理请求;否则将请求转发给下一个处理者;
  3. 客户端(Client):创建请求对象,并将请求对象传递给第一个处理者;
  4. 请求对象(Request):封装了需要处理的数据和请求类型等信息。

责任链模式的优点在于它能够降低系统的耦合度,增强系统的可扩展性和灵活性。同时,由于责任链模式中的处理者之间是松散耦合的,因此可以方便地增加或删除处理者,而不会影响到其他部分的功能。

实现

上述需求,其实就是典型的责任链模式的应用场景。而责任链模式一般有两种实现:指针和集合的方式。

指针模式是最常见的责任链模式实现方式之一。在这种模式下,每个处理对象都持有一个指向下一个处理对象的引用,形成一个链表。当请求到达一个处理对象时,如果该对象无法处理该请求,则将请求传递给链表中的下一个对象,直到找到能够处理该请求的对象为止。

集合模式是另一种责任链模式实现方式。与指针模式不同的是,集合模式下,所有的处理对象被封装在一个集合中,每个对象都具有相同的处理机会。当请求到达集合时,集合中的每个对象都有机会去处理请求,直到有一个对象成功地处理了请求或者所有对象都无法处理该请求为止。

责任链

java
public abstract class HandlerChain<T, R extends Rule> {
+
+    protected List<Handler<T, R>> handlers;
+
+    public void setHandlers(List<Handler<T, R>> handlers) {
+        this.handlers = handlers;
+    }
+
+    public List<Handler<T, R>> getHandlers() {
+        return handlers;
+    }
+
+    public void handle(T t) {
+        if (CollUtil.isNotEmpty(handlers)) {
+            for (Handler<T, R> handler : handlers) {
+                if (!handler.handle(t)) {
+                    break;
+                }
+            }
+        }
+    }
+
+    public void clear() {
+        if (CollUtil.isNotEmpty(handlers)) {
+            handlers.clear();
+        }
+    }
+}
public abstract class HandlerChain<T, R extends Rule> {
+
+    protected List<Handler<T, R>> handlers;
+
+    public void setHandlers(List<Handler<T, R>> handlers) {
+        this.handlers = handlers;
+    }
+
+    public List<Handler<T, R>> getHandlers() {
+        return handlers;
+    }
+
+    public void handle(T t) {
+        if (CollUtil.isNotEmpty(handlers)) {
+            for (Handler<T, R> handler : handlers) {
+                if (!handler.handle(t)) {
+                    break;
+                }
+            }
+        }
+    }
+
+    public void clear() {
+        if (CollUtil.isNotEmpty(handlers)) {
+            handlers.clear();
+        }
+    }
+}

处理器

java
public abstract class Handler<T, R extends Rule> {
+
+    protected CommonDynamicParam param;
+    protected R rule;
+
+    public abstract boolean handle(T t);
+
+    public CommonDynamicParam getParam() {
+        return param;
+    }
+
+    public void setParam(CommonDynamicParam param) {
+        this.param = param;
+    }
+
+    public R getRule() {
+        return rule;
+    }
+
+    public void setRule(R rule) {
+        this.rule = rule;
+        this.param = JSON.parseObject(rule.getSettlementCondition(), CommonDynamicParam.class);
+    }
+}
public abstract class Handler<T, R extends Rule> {
+
+    protected CommonDynamicParam param;
+    protected R rule;
+
+    public abstract boolean handle(T t);
+
+    public CommonDynamicParam getParam() {
+        return param;
+    }
+
+    public void setParam(CommonDynamicParam param) {
+        this.param = param;
+    }
+
+    public R getRule() {
+        return rule;
+    }
+
+    public void setRule(R rule) {
+        this.rule = rule;
+        this.param = JSON.parseObject(rule.getSettlementCondition(), CommonDynamicParam.class);
+    }
+}

大致流程

  1. 实现具体的Handler处理器逻辑(handle)

  2. 初始化处理器集合List<Handler>,并交给责任链对象HandlerChain管理

  3. 调用责任链的handle方法处理对象

`,19),e=[o];function t(c,r,E,y,i,d){return a(),n("div",null,e)}const u=s(p,[["render",t]]);export{h as __pageData,u as default}; diff --git "a/assets/actions_designpattern_\350\264\243\344\273\273\351\223\276\346\250\241\345\274\217.md.952322e1.lean.js" "b/assets/actions_designpattern_\350\264\243\344\273\273\351\223\276\346\250\241\345\274\217.md.952322e1.lean.js" new file mode 100644 index 000000000..838287a17 --- /dev/null +++ "b/assets/actions_designpattern_\350\264\243\344\273\273\351\223\276\346\250\241\345\274\217.md.952322e1.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"责任链模式","description":"","frontmatter":{},"headers":[],"relativePath":"actions/designpattern/责任链模式.md","filePath":"actions/designpattern/责任链模式.md","lastUpdated":1708510102000}'),p={name:"actions/designpattern/责任链模式.md"},o=l("",19),e=[o];function t(c,r,E,y,i,d){return a(),n("div",null,e)}const u=s(p,[["render",t]]);export{h as __pageData,u as default}; diff --git "a/assets/actions_env_Docker\345\256\271\345\231\250\345\206\205\350\256\277\351\227\256MacOS\345\256\277\344\270\273\346\234\272\344\270\255\347\232\204kafka.md.375de9c2.js" "b/assets/actions_env_Docker\345\256\271\345\231\250\345\206\205\350\256\277\351\227\256MacOS\345\256\277\344\270\273\346\234\272\344\270\255\347\232\204kafka.md.375de9c2.js" new file mode 100644 index 000000000..0de536aec --- /dev/null +++ "b/assets/actions_env_Docker\345\256\271\345\231\250\345\206\205\350\256\277\351\227\256MacOS\345\256\277\344\270\273\346\234\272\344\270\255\347\232\204kafka.md.375de9c2.js" @@ -0,0 +1,7 @@ +import{_ as a,o as s,c as e,Q as o}from"./chunks/framework.dc1f4608.js";const y=JSON.parse('{"title":"Docker容器内访问MacOS宿主机中的kafka","description":"","frontmatter":{},"headers":[],"relativePath":"actions/env/Docker容器内访问MacOS宿主机中的kafka.md","filePath":"actions/env/Docker容器内访问MacOS宿主机中的kafka.md","lastUpdated":1708510102000}'),n={name:"actions/env/Docker容器内访问MacOS宿主机中的kafka.md"},l=o(`

Docker容器内访问MacOS宿主机中的kafka

kafka配置

properties
# server.properties
+listeners=PLAINTEXT://:9092
+advertised.listeners=PLAINTEXT://host.docker.internal:9092
# server.properties
+listeners=PLAINTEXT://:9092
+advertised.listeners=PLAINTEXT://host.docker.internal:9092

/etc/hosts配置

bash
127.0.0.1 host.docker.internal
127.0.0.1 host.docker.internal

验证

shell
# List brokers and topics in cluster
+$ docker run -it --rm --network=host --platform=linux/amd64 edenhill/kcat:1.7.1 -b host.docker.internal -L
# List brokers and topics in cluster
+$ docker run -it --rm --network=host --platform=linux/amd64 edenhill/kcat:1.7.1 -b host.docker.internal -L

原理

连接kafka的broker过程

分析

容器内连接

如果kafka只配置listeners=PLAINTEXT://:9092而不配置advertised.listeners=PLAINTEXT://host.docker.internal:9092,那么kafka容器内部的客户端是无法访问到kafka的,因为kafka容器内部的客户端会通过advertised.listeners配置的地址去访问kafka,而默认的advertised.listeners配置和listeners一致,也是PLAINTEXT://:9092,对于容器内的客户端来说这个地址是容器内部的地址,所以容器内部的客户端无法访问到宿主机kafka。因此要加上advertised.listeners=PLAINTEXT://host.docker.internal:9092

宿主机上连接

因为配置了advertised.listeners=PLAINTEXT://host.docker.internal:9092,所以宿主机上的客户端也会通过host.docker.internal:9092访问kafka的broker,所以要在/etc/hosts中增加解析host.docker.internal到本地回环地址的配置。

`,15),t=[l];function r(c,p,i,d,k,h){return s(),e("div",null,t)}const f=a(n,[["render",r]]);export{y as __pageData,f as default}; diff --git "a/assets/actions_env_Docker\345\256\271\345\231\250\345\206\205\350\256\277\351\227\256MacOS\345\256\277\344\270\273\346\234\272\344\270\255\347\232\204kafka.md.375de9c2.lean.js" "b/assets/actions_env_Docker\345\256\271\345\231\250\345\206\205\350\256\277\351\227\256MacOS\345\256\277\344\270\273\346\234\272\344\270\255\347\232\204kafka.md.375de9c2.lean.js" new file mode 100644 index 000000000..7c0ad3e49 --- /dev/null +++ "b/assets/actions_env_Docker\345\256\271\345\231\250\345\206\205\350\256\277\351\227\256MacOS\345\256\277\344\270\273\346\234\272\344\270\255\347\232\204kafka.md.375de9c2.lean.js" @@ -0,0 +1 @@ +import{_ as a,o as s,c as e,Q as o}from"./chunks/framework.dc1f4608.js";const y=JSON.parse('{"title":"Docker容器内访问MacOS宿主机中的kafka","description":"","frontmatter":{},"headers":[],"relativePath":"actions/env/Docker容器内访问MacOS宿主机中的kafka.md","filePath":"actions/env/Docker容器内访问MacOS宿主机中的kafka.md","lastUpdated":1708510102000}'),n={name:"actions/env/Docker容器内访问MacOS宿主机中的kafka.md"},l=o("",15),t=[l];function r(c,p,i,d,k,h){return s(),e("div",null,t)}const f=a(n,[["render",r]]);export{y as __pageData,f as default}; diff --git "a/assets/actions_env_Windows\344\270\213Docker Desktop\345\256\211\350\243\205.md.2d631526.js" "b/assets/actions_env_Windows\344\270\213Docker Desktop\345\256\211\350\243\205.md.2d631526.js" new file mode 100644 index 000000000..3c394c0b1 --- /dev/null +++ "b/assets/actions_env_Windows\344\270\213Docker Desktop\345\256\211\350\243\205.md.2d631526.js" @@ -0,0 +1 @@ +import{_ as e,o,c as t,Q as s}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"Windows下Docker Desktop安装","description":"","frontmatter":{},"headers":[],"relativePath":"actions/env/Windows下Docker Desktop安装.md","filePath":"actions/env/Windows下Docker Desktop安装.md","lastUpdated":1708510102000}'),i={name:"actions/env/Windows下Docker Desktop安装.md"},l=s('

Windows下Docker Desktop安装

  1. 官网下载安装包Docker Desktop Installer.exe

  2. 运行安装并重启

  3. 提示WSL2的linux内核安装不完整,点击链接跟随教程操作

    image-20210427020640967

  4. 执行指引中的命令

    • 步骤 1 - 启用适用于 Linux 的 Windows 子系统dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
    • 步骤 2 - 检查运行 WSL 2 的要求,不同架构的系统版本要求不同
    • 步骤 3 - 启用虚拟机功能,前提是开启虚拟化,如何开启虚拟化不再赘述dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
    • 步骤 4 - 下载 Linux 内核更新包,地址:https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi
    • 步骤 5 - 将 WSL 2 设置为默认版本wsl --set-default-version 2
    • 步骤 6 - 安装所选的 Linux 分发,官方提供的方案是在微软商店中下载,因为网络原因很难实现,采取手动下载或命令行下载,参考地址https://docs.microsoft.com/zh-cn/windows/wsl/install-manual,下载完成后cd到下载的目录,执行Add-AppxPackage .\\filename即可
      • 查看所有安装的分发版本wsl --list --all
      • 卸载指定的分发版本wsl --unregister <DistributionName>
    • 打开已安装的分发版本,在安装后会提示创建linux账户和密码
',2),a=[l];function n(r,c,d,p,_,u){return o(),t("div",null,a)}const k=e(i,[["render",n]]);export{m as __pageData,k as default}; diff --git "a/assets/actions_env_Windows\344\270\213Docker Desktop\345\256\211\350\243\205.md.2d631526.lean.js" "b/assets/actions_env_Windows\344\270\213Docker Desktop\345\256\211\350\243\205.md.2d631526.lean.js" new file mode 100644 index 000000000..4af5803dd --- /dev/null +++ "b/assets/actions_env_Windows\344\270\213Docker Desktop\345\256\211\350\243\205.md.2d631526.lean.js" @@ -0,0 +1 @@ +import{_ as e,o,c as t,Q as s}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"Windows下Docker Desktop安装","description":"","frontmatter":{},"headers":[],"relativePath":"actions/env/Windows下Docker Desktop安装.md","filePath":"actions/env/Windows下Docker Desktop安装.md","lastUpdated":1708510102000}'),i={name:"actions/env/Windows下Docker Desktop安装.md"},l=s("",2),a=[l];function n(r,c,d,p,_,u){return o(),t("div",null,a)}const k=e(i,[["render",n]]);export{m as __pageData,k as default}; diff --git "a/assets/actions_env_Windows\345\274\200\346\234\272\345\220\257\345\212\250WSL.md.2ecc3382.js" "b/assets/actions_env_Windows\345\274\200\346\234\272\345\220\257\345\212\250WSL.md.2ecc3382.js" new file mode 100644 index 000000000..8341bea2f --- /dev/null +++ "b/assets/actions_env_Windows\345\274\200\346\234\272\345\220\257\345\212\250WSL.md.2ecc3382.js" @@ -0,0 +1,5 @@ +import{_ as s,o as a,c as o,Q as n}from"./chunks/framework.dc1f4608.js";const F=JSON.parse('{"title":"Windows开机启动WSL","description":"","frontmatter":{},"headers":[],"relativePath":"actions/env/Windows开机启动WSL.md","filePath":"actions/env/Windows开机启动WSL.md","lastUpdated":1708510102000}'),l={name:"actions/env/Windows开机启动WSL.md"},e=n(`

Windows开机启动WSL

win+R运行shell:startup打开开机启动程序文件夹,创建vbs脚本wsl.vbs

bash
Set objShell = CreateObject("WScript.Shell")
+objShell.Run "cmd /c wsl", 0
+Set objShell = Nothing
Set objShell = CreateObject("WScript.Shell")
+objShell.Run "cmd /c wsl", 0
+Set objShell = Nothing

重启即可

`,4),p=[e];function t(c,r,i,E,d,y){return a(),o("div",null,p)}const _=s(l,[["render",t]]);export{F as __pageData,_ as default}; diff --git "a/assets/actions_env_Windows\345\274\200\346\234\272\345\220\257\345\212\250WSL.md.2ecc3382.lean.js" "b/assets/actions_env_Windows\345\274\200\346\234\272\345\220\257\345\212\250WSL.md.2ecc3382.lean.js" new file mode 100644 index 000000000..a39c7d5b1 --- /dev/null +++ "b/assets/actions_env_Windows\345\274\200\346\234\272\345\220\257\345\212\250WSL.md.2ecc3382.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as o,Q as n}from"./chunks/framework.dc1f4608.js";const F=JSON.parse('{"title":"Windows开机启动WSL","description":"","frontmatter":{},"headers":[],"relativePath":"actions/env/Windows开机启动WSL.md","filePath":"actions/env/Windows开机启动WSL.md","lastUpdated":1708510102000}'),l={name:"actions/env/Windows开机启动WSL.md"},e=n("",4),p=[e];function t(c,r,i,E,d,y){return a(),o("div",null,p)}const _=s(l,[["render",t]]);export{F as __pageData,_ as default}; diff --git "a/assets/actions_env_docker_jenkins_gitee\350\207\252\345\212\250\345\214\226\351\203\250\347\275\262vue\351\241\271\347\233\256.md.b6d298c3.js" "b/assets/actions_env_docker_jenkins_gitee\350\207\252\345\212\250\345\214\226\351\203\250\347\275\262vue\351\241\271\347\233\256.md.b6d298c3.js" new file mode 100644 index 000000000..2c9d30450 --- /dev/null +++ "b/assets/actions_env_docker_jenkins_gitee\350\207\252\345\212\250\345\214\226\351\203\250\347\275\262vue\351\241\271\347\233\256.md.b6d298c3.js" @@ -0,0 +1,113 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const g=JSON.parse('{"title":"docker+jenkins+gitee自动化部署vue项目","description":"","frontmatter":{},"headers":[],"relativePath":"actions/env/docker+jenkins+gitee自动化部署vue项目.md","filePath":"actions/env/docker+jenkins+gitee自动化部署vue项目.md","lastUpdated":1708510102000}'),p={name:"actions/env/docker+jenkins+gitee自动化部署vue项目.md"},o=l(`

docker+jenkins+gitee自动化部署vue项目

之前个人博客一直用的travisCI部署在github page上,但是偶尔会抽风无法访问。之前一直偷懒没部署jenkins,手动部署到云服务器又比较麻烦,打包上传很浪费时间,这次就直接动手一步到位,在自己服务器上部署下jekins。

docker启动jenkins

最开始用的jenkins中文社区的镜像发现有个很恶心的问题,jenkins版本比较低而且安装了NodeJS插件后在全局工具配置中配置NodeJS安装环境时无法选择版本,所以还是官方镜像比较靠谱。

bash
# 拉取官方镜像
+docker pull jenkins/jenkins:lts
+lts: Pulling from jenkins/jenkins
+4c25b3090c26: Pull complete
+750d566fdd60: Pull complete
+2718cc36ca02: Pull complete
+5678b027ee14: Pull complete
+c839cd2df78d: Pull complete
+50861a5addda: Pull complete
+ff2b028e5cf5: Pull complete
+ee710b58f452: Pull complete
+2625c929bb0e: Pull complete
+6a6bf9181c04: Pull complete
+bee5e6792ac4: Pull complete
+6cc5edd2133e: Pull complete
+c07b16426ded: Pull complete
+e9ac42647ae3: Pull complete
+fa925738a490: Pull complete
+4a08c3886279: Pull complete
+2d43fec22b7e: Pull complete
+Digest: sha256:a942c30fc3bcf269a1c32ba27eb4a470148eff9aba086911320031a3c3943e6c
+Status: Downloaded newer image for jenkins/jenkins:lts
+docker.io/jenkins/jenkins:lts
+# 启动jenkins
+docker run --name jenkins -dp 8099:8080 -v /story/dist:/story/dist -v ~/jenkins_data:/var/jenkins_home -u root  -e TZ="Asia/Shanghai" -v /etc/localtime:/etc/localtime:ro jenkins/jenkins:lts
+# 参数说明 --name 指定容器名为jenkins -d 后台启动 -p 将容器的8080端口映射到宿主机的8099端口
+# -v 挂载宿主机目录 宿主机和容器的目录会同步 -u 指定用户为root 这里是必须的 不然后续操作文件系统会报无权限
+# 挂载时区的目录是因为镜像中的linux系统默认时区非北京时间,会导致时间显示不正确
# 拉取官方镜像
+docker pull jenkins/jenkins:lts
+lts: Pulling from jenkins/jenkins
+4c25b3090c26: Pull complete
+750d566fdd60: Pull complete
+2718cc36ca02: Pull complete
+5678b027ee14: Pull complete
+c839cd2df78d: Pull complete
+50861a5addda: Pull complete
+ff2b028e5cf5: Pull complete
+ee710b58f452: Pull complete
+2625c929bb0e: Pull complete
+6a6bf9181c04: Pull complete
+bee5e6792ac4: Pull complete
+6cc5edd2133e: Pull complete
+c07b16426ded: Pull complete
+e9ac42647ae3: Pull complete
+fa925738a490: Pull complete
+4a08c3886279: Pull complete
+2d43fec22b7e: Pull complete
+Digest: sha256:a942c30fc3bcf269a1c32ba27eb4a470148eff9aba086911320031a3c3943e6c
+Status: Downloaded newer image for jenkins/jenkins:lts
+docker.io/jenkins/jenkins:lts
+# 启动jenkins
+docker run --name jenkins -dp 8099:8080 -v /story/dist:/story/dist -v ~/jenkins_data:/var/jenkins_home -u root  -e TZ="Asia/Shanghai" -v /etc/localtime:/etc/localtime:ro jenkins/jenkins:lts
+# 参数说明 --name 指定容器名为jenkins -d 后台启动 -p 将容器的8080端口映射到宿主机的8099端口
+# -v 挂载宿主机目录 宿主机和容器的目录会同步 -u 指定用户为root 这里是必须的 不然后续操作文件系统会报无权限
+# 挂载时区的目录是因为镜像中的linux系统默认时区非北京时间,会导致时间显示不正确

apt安装

shell
curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key | sudo tee \\
+  /usr/share/keyrings/jenkins-keyring.asc > /dev/null
+
+sudo sh -c 'echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian-stable binary/" > /etc/apt/sources.list.d/jenkins.list'
+
+sudo apt update
+
+# Jenkins requires Java 11 or 17 since Jenkins 2.357 and LTS 2.361.1. 
+apt install openjdk-17-jdk
+
+sudo apt install jenkins
curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key | sudo tee \\
+  /usr/share/keyrings/jenkins-keyring.asc > /dev/null
+
+sudo sh -c 'echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian-stable binary/" > /etc/apt/sources.list.d/jenkins.list'
+
+sudo apt update
+
+# Jenkins requires Java 11 or 17 since Jenkins 2.357 and LTS 2.361.1. 
+apt install openjdk-17-jdk
+
+sudo apt install jenkins

image-20210914101747191

bash
docker exec -it jenkins /bin/bash
+cat /var/jenkins_home/secrets/initialAdminPassword
docker exec -it jenkins /bin/bash
+cat /var/jenkins_home/secrets/initialAdminPassword

或者直接在上面挂载的目录查询但要改一下路径

bash
cat ~/jenkins_data/secrets/initialAdminPassword
cat ~/jenkins_data/secrets/initialAdminPassword

image-20210914102028448

image-20210914102652130

image-20210914102716607

jenkins配置自动部署

安装node和gitee插件

Manage Jenkins ---> Manage Plugins ---> 可选插件分别搜索gitee和nodejs image-20210914111457469

image-20210914111526455

选择install without restart

安装完毕后返回工作台

配置node环境

Manage Jenkins ---> Global Tool Configuration ---> NodeJS

新增NodeJS取别名后保存即可

image-20210914111718160

新建自动部署任务

新建任务

工作台点击新建Item,输入任务名称后选择freestyle project确定

image-20210914111819597

配置gitee相关内容

源码管理

image-20210914112017978

构建触发器

image-20210914112740392

image-20210914112804397

url和webhook密码分别填写后保存

image-20210914113229679

在这个页面点击测试,如果看到xxx has been accepted即为成功。

构建环境

选择前面已经配置好的node环境即可

image-20210914113502188

构建

首先在任务面板中点击立即构建,这样才会生成工作空间

image-20210914114129450

我这里选择执行shell

image-20210914113546293

然后就是写个简单的脚本执行打包,替换的工作

image-20210914113643970

第一步cd进入的目录是当前任务的工作空间,这里要把vuepress替换成自己的任务名称即可

TIP

这里涉及到文件系统操作的内容rm cp等命令需要root用户才能执行,所以在启动docker容器的时候必须使用-u root参数指定root用户,否则打包会失败,操作文件时会提示无权限

配置完毕保存即可

测试

点击立即构建或者往gitee仓库推送一次更新即可触发构建任务,然后等待构建完成即可。

image-20210914114542885

如果是第一次执行构建,jenkins还会自动安装解压nodejs。

通过脚本替换完打包好的dist后,通过nginx配置部署静态项目即可。

jenkins打包跨平台docker镜像并推送镜像仓库

这里是用宿主机直接安装的jenkins

shell
#!/bin/sh
+cd /var/lib/jenkins/workspace/xxx
+node -v
+npm -v
+docker -v
+
+npm install
+npm run build
+docker buildx ls
+docker buildx create --use --name jenkinsbuilder
+docker buildx ls
+
+# Dockerfile
+cat > Dockerfile <<EOF
+// doSomething
+EOF
+
+docker login xxx.com --username='xxx' --password=='xxx'
+docker buildx build --platform linux/amd64,linux/arm64 -t xxx/xxx . --push
#!/bin/sh
+cd /var/lib/jenkins/workspace/xxx
+node -v
+npm -v
+docker -v
+
+npm install
+npm run build
+docker buildx ls
+docker buildx create --use --name jenkinsbuilder
+docker buildx ls
+
+# Dockerfile
+cat > Dockerfile <<EOF
+// doSomething
+EOF
+
+docker login xxx.com --username='xxx' --password=='xxx'
+docker buildx build --platform linux/amd64,linux/arm64 -t xxx/xxx . --push

阻止jenkins杀死衍生进程

Execute shell中添加如下变量

shell
BUILD_ID=DONTKILLME
BUILD_ID=DONTKILLME
`,72),e=[o];function c(t,r,y,i,E,F){return n(),a("div",null,e)}const u=s(p,[["render",c]]);export{g as __pageData,u as default}; diff --git "a/assets/actions_env_docker_jenkins_gitee\350\207\252\345\212\250\345\214\226\351\203\250\347\275\262vue\351\241\271\347\233\256.md.b6d298c3.lean.js" "b/assets/actions_env_docker_jenkins_gitee\350\207\252\345\212\250\345\214\226\351\203\250\347\275\262vue\351\241\271\347\233\256.md.b6d298c3.lean.js" new file mode 100644 index 000000000..1bafa0fc1 --- /dev/null +++ "b/assets/actions_env_docker_jenkins_gitee\350\207\252\345\212\250\345\214\226\351\203\250\347\275\262vue\351\241\271\347\233\256.md.b6d298c3.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const g=JSON.parse('{"title":"docker+jenkins+gitee自动化部署vue项目","description":"","frontmatter":{},"headers":[],"relativePath":"actions/env/docker+jenkins+gitee自动化部署vue项目.md","filePath":"actions/env/docker+jenkins+gitee自动化部署vue项目.md","lastUpdated":1708510102000}'),p={name:"actions/env/docker+jenkins+gitee自动化部署vue项目.md"},o=l("",72),e=[o];function c(t,r,y,i,E,F){return n(),a("div",null,e)}const u=s(p,[["render",c]]);export{g as __pageData,u as default}; diff --git "a/assets/actions_env_git\351\205\215\347\275\256socks5\344\273\243\347\220\206\350\247\243\345\206\263github\344\270\212down\344\273\243\347\240\201\346\205\242\347\232\204\351\227\256\351\242\230.md.bfa9056c.js" "b/assets/actions_env_git\351\205\215\347\275\256socks5\344\273\243\347\220\206\350\247\243\345\206\263github\344\270\212down\344\273\243\347\240\201\346\205\242\347\232\204\351\227\256\351\242\230.md.bfa9056c.js" new file mode 100644 index 000000000..8cba85b0c --- /dev/null +++ "b/assets/actions_env_git\351\205\215\347\275\256socks5\344\273\243\347\220\206\350\247\243\345\206\263github\344\270\212down\344\273\243\347\240\201\346\205\242\347\232\204\351\227\256\351\242\230.md.bfa9056c.js" @@ -0,0 +1,17 @@ +import{_ as s,o as n,c as a,Q as o}from"./chunks/framework.dc1f4608.js";const _=JSON.parse('{"title":"git配置socks5代理解决github上down代码慢的问题","description":"","frontmatter":{},"headers":[],"relativePath":"actions/env/git配置socks5代理解决github上down代码慢的问题.md","filePath":"actions/env/git配置socks5代理解决github上down代码慢的问题.md","lastUpdated":1708510102000}'),p={name:"actions/env/git配置socks5代理解决github上down代码慢的问题.md"},l=o(`

git配置socks5代理解决github上down代码慢的问题

bash
# 设置代理
+git config --global http.proxy 'socks5://127.0.0.1:10880'
+
+git config --global https.proxy 'socks5://127.0.0.1:10880'
+
+# 取消代理
+git config --global --unset http.proxy
+
+git config --global --unset https.proxy
# 设置代理
+git config --global http.proxy 'socks5://127.0.0.1:10880'
+
+git config --global https.proxy 'socks5://127.0.0.1:10880'
+
+# 取消代理
+git config --global --unset http.proxy
+
+git config --global --unset https.proxy

端口号根据自己本地的代理端口填写

`,3),t=[l];function e(c,r,y,i,E,g){return n(),a("div",null,t)}const d=s(p,[["render",e]]);export{_ as __pageData,d as default}; diff --git "a/assets/actions_env_git\351\205\215\347\275\256socks5\344\273\243\347\220\206\350\247\243\345\206\263github\344\270\212down\344\273\243\347\240\201\346\205\242\347\232\204\351\227\256\351\242\230.md.bfa9056c.lean.js" "b/assets/actions_env_git\351\205\215\347\275\256socks5\344\273\243\347\220\206\350\247\243\345\206\263github\344\270\212down\344\273\243\347\240\201\346\205\242\347\232\204\351\227\256\351\242\230.md.bfa9056c.lean.js" new file mode 100644 index 000000000..25a855d3b --- /dev/null +++ "b/assets/actions_env_git\351\205\215\347\275\256socks5\344\273\243\347\220\206\350\247\243\345\206\263github\344\270\212down\344\273\243\347\240\201\346\205\242\347\232\204\351\227\256\351\242\230.md.bfa9056c.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as o}from"./chunks/framework.dc1f4608.js";const _=JSON.parse('{"title":"git配置socks5代理解决github上down代码慢的问题","description":"","frontmatter":{},"headers":[],"relativePath":"actions/env/git配置socks5代理解决github上down代码慢的问题.md","filePath":"actions/env/git配置socks5代理解决github上down代码慢的问题.md","lastUpdated":1708510102000}'),p={name:"actions/env/git配置socks5代理解决github上down代码慢的问题.md"},l=o("",3),t=[l];function e(c,r,y,i,E,g){return n(),a("div",null,t)}const d=s(p,[["render",e]]);export{_ as __pageData,d as default}; diff --git "a/assets/actions_env_git\351\205\215\347\275\256\345\244\232ssh-key __ Gitee \345\222\214 Github \345\220\214\346\255\245\346\233\264\346\226\260.md.e089c9d0.js" "b/assets/actions_env_git\351\205\215\347\275\256\345\244\232ssh-key __ Gitee \345\222\214 Github \345\220\214\346\255\245\346\233\264\346\226\260.md.e089c9d0.js" new file mode 100644 index 000000000..ad76d386c --- /dev/null +++ "b/assets/actions_env_git\351\205\215\347\275\256\345\244\232ssh-key __ Gitee \345\222\214 Github \345\220\214\346\255\245\346\233\264\346\226\260.md.e089c9d0.js" @@ -0,0 +1,31 @@ +import{_ as s,o as a,c as n,Q as e}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"git配置多ssh-key && Gitee 和 Github 同步更新","description":"","frontmatter":{},"headers":[],"relativePath":"actions/env/git配置多ssh-key && Gitee 和 Github 同步更新.md","filePath":"actions/env/git配置多ssh-key && Gitee 和 Github 同步更新.md","lastUpdated":1708510102000}'),p={name:"actions/env/git配置多ssh-key && Gitee 和 Github 同步更新.md"},l=e(`

git配置多ssh-key && Gitee 和 Github 同步更新

配置多ssh-key

gitee或者gitlab账号和个人git账号同时在一台机器上使用时,可以为不同git服务器设置不同的ssh-key

  1. 生成一个个人github的ssh-key

    ssh-keygen -t rsa -C 'xxxxx@163.com' -f ~/.ssh/github_id_rsa

  2. 生成一个gitee的ssh-key

    ssh-keygen -t rsa -C 'xxxxx@company.cn' -f ~/.ssh/gitee_id_rsa

  3. ~/.ssh下新建config文件vim ~/.ssh/config,添加以下内容

    txt
    # gitee
    +Host gitee.com
    +HostName gitee.com
    +PreferredAuthentications publickey
    +IdentityFile ~/.ssh/gitee_id_rsa
    +# github
    +Host github.com
    +HostName github.com
    +PreferredAuthentications publickey
    +IdentityFile ~/.ssh/github_id_rsa
    # gitee
    +Host gitee.com
    +HostName gitee.com
    +PreferredAuthentications publickey
    +IdentityFile ~/.ssh/gitee_id_rsa
    +# github
    +Host github.com
    +HostName github.com
    +PreferredAuthentications publickey
    +IdentityFile ~/.ssh/github_id_rsa
  4. 分别在gitee和github中添加前两步生成的对应地址的公钥

  5. ssh命令测试

    bash
    ssh -T git@gitee.com
    +ssh -T git@github.com
    ssh -T git@gitee.com
    +ssh -T git@github.com

如果看到 hi xxx!。。。内容则证明配置成功

Gitee 和 Github 同步更新

假设我们有一个项目同时在github和gitee上都有仓库,当直接使用git clone命令拉取的代码默认remote为origin,如果要分别更新,我们要分别在两个本地仓库中push。这时我们可以给本地仓库添加多个origin,然后更新的时候分别推送即可实现一个本地仓库分别推送两个不同的远程仓库。

  1. 删除原有的remote地址

    git remote remove origin

  2. 添加新的远程仓库地址(gitee)

    bash
    git remote add 远程仓库名 远程仓库地址
    +eg: git remote add gitee git@gitee.com:xxx/xxx.git
    git remote add 远程仓库名 远程仓库地址
    +eg: git remote add gitee git@gitee.com:xxx/xxx.git
  3. 添加新的远程仓库地址(github)

    bash
    git remote add 远程仓库名 远程仓库地址
    +eg: git remote add github git@github.com:xxx/xxx.git
    git remote add 远程仓库名 远程仓库地址
    +eg: git remote add github git@github.com:xxx/xxx.git

    再次查看git remote:

    image-20210913184204396

  4. 推送的时候git push 远程仓库名即可

修改配置文件一次推送多个仓库

修改仓库下.git/config文件,新增内容

sh
[remote "all"]
+        url = repo1.git
+        url = repo2.git
+        url = repo3.git
[remote "all"]
+        url = repo1.git
+        url = repo2.git
+        url = repo3.git

直接git push all

`,12),o=[l];function t(c,i,r,y,E,g){return a(),n("div",null,o)}const F=s(p,[["render",t]]);export{d as __pageData,F as default}; diff --git "a/assets/actions_env_git\351\205\215\347\275\256\345\244\232ssh-key __ Gitee \345\222\214 Github \345\220\214\346\255\245\346\233\264\346\226\260.md.e089c9d0.lean.js" "b/assets/actions_env_git\351\205\215\347\275\256\345\244\232ssh-key __ Gitee \345\222\214 Github \345\220\214\346\255\245\346\233\264\346\226\260.md.e089c9d0.lean.js" new file mode 100644 index 000000000..74df08dd9 --- /dev/null +++ "b/assets/actions_env_git\351\205\215\347\275\256\345\244\232ssh-key __ Gitee \345\222\214 Github \345\220\214\346\255\245\346\233\264\346\226\260.md.e089c9d0.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as e}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"git配置多ssh-key && Gitee 和 Github 同步更新","description":"","frontmatter":{},"headers":[],"relativePath":"actions/env/git配置多ssh-key && Gitee 和 Github 同步更新.md","filePath":"actions/env/git配置多ssh-key && Gitee 和 Github 同步更新.md","lastUpdated":1708510102000}'),p={name:"actions/env/git配置多ssh-key && Gitee 和 Github 同步更新.md"},l=e("",12),o=[l];function t(c,i,r,y,E,g){return a(),n("div",null,o)}const F=s(p,[["render",t]]);export{d as __pageData,F as default}; diff --git "a/assets/actions_env_macOS\345\274\200\345\220\257\347\273\210\347\253\257\347\232\204\344\273\243\347\220\206.md.323e2f5e.js" "b/assets/actions_env_macOS\345\274\200\345\220\257\347\273\210\347\253\257\347\232\204\344\273\243\347\220\206.md.323e2f5e.js" new file mode 100644 index 000000000..2492413c1 --- /dev/null +++ "b/assets/actions_env_macOS\345\274\200\345\220\257\347\273\210\347\253\257\347\232\204\344\273\243\347\220\206.md.323e2f5e.js" @@ -0,0 +1,3 @@ +import{_ as s,o as a,c as o,Q as p}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"macOS开启终端的代理","description":"","frontmatter":{},"headers":[],"relativePath":"actions/env/macOS开启终端的代理.md","filePath":"actions/env/macOS开启终端的代理.md","lastUpdated":1708510102000}'),e={name:"actions/env/macOS开启终端的代理.md"},t=p(`

macOS开启终端的代理

例如要走socks5的代理

bash
export https_proxy=socks5://127.0.0.1:10880
export https_proxy=socks5://127.0.0.1:10880

这样配置只对当前终端有效,不会影响其他

bash
export http_proxy=socks5://127.0.0.1:10880
+export https_proxy=socks5://127.0.0.1:10880
export http_proxy=socks5://127.0.0.1:10880
+export https_proxy=socks5://127.0.0.1:10880

修改后保存,然后source ~/.zshrc立即成效。重启终端后即可全局代理。

也可以通过alias建立个别名,这样可以快速开启代理,编辑.zshrc 添加

alias proxy_on='export https_proxy=socks5://127.0.0.1:10880'

`,10),n=[t];function l(c,r,i,d,_,h){return a(),o("div",null,n)}const m=s(e,[["render",l]]);export{u as __pageData,m as default}; diff --git "a/assets/actions_env_macOS\345\274\200\345\220\257\347\273\210\347\253\257\347\232\204\344\273\243\347\220\206.md.323e2f5e.lean.js" "b/assets/actions_env_macOS\345\274\200\345\220\257\347\273\210\347\253\257\347\232\204\344\273\243\347\220\206.md.323e2f5e.lean.js" new file mode 100644 index 000000000..c6e589444 --- /dev/null +++ "b/assets/actions_env_macOS\345\274\200\345\220\257\347\273\210\347\253\257\347\232\204\344\273\243\347\220\206.md.323e2f5e.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as o,Q as p}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"macOS开启终端的代理","description":"","frontmatter":{},"headers":[],"relativePath":"actions/env/macOS开启终端的代理.md","filePath":"actions/env/macOS开启终端的代理.md","lastUpdated":1708510102000}'),e={name:"actions/env/macOS开启终端的代理.md"},t=p("",10),n=[t];function l(c,r,i,d,_,h){return a(),o("div",null,n)}const m=s(e,[["render",l]]);export{u as __pageData,m as default}; diff --git "a/assets/actions_env_macos\345\274\200\346\234\272\350\207\252\345\212\250\346\211\247\350\241\214\350\204\232\346\234\254.md.6cf6a3a7.js" "b/assets/actions_env_macos\345\274\200\346\234\272\350\207\252\345\212\250\346\211\247\350\241\214\350\204\232\346\234\254.md.6cf6a3a7.js" new file mode 100644 index 000000000..5c48d8ee8 --- /dev/null +++ "b/assets/actions_env_macos\345\274\200\346\234\272\350\207\252\345\212\250\346\211\247\350\241\214\350\204\232\346\234\254.md.6cf6a3a7.js" @@ -0,0 +1,45 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"macos开机自动执行脚本","description":"","frontmatter":{},"headers":[],"relativePath":"actions/env/macos开机自动执行脚本.md","filePath":"actions/env/macos开机自动执行脚本.md","lastUpdated":1708510102000}'),p={name:"actions/env/macos开机自动执行脚本.md"},o=l(`

macos开机自动执行脚本

linux开机启动可以用systemd很方便的实现,mac上稍微复杂一些,需要自己写个.plist文件

简介

launchd 是 Mac OS 下用于初始化系统环境的关键进程,它是内核装载成功之后在 OS 环境下启动的第一个进程,可以用来控制服务的自动启动或者关闭。

它的作用就是我们平时说的守护进程,简单来说,用户守护进程是作为系统的一部分运行在后台的非图形化程序。

采用这种方式来配置自启动项很简单,只需要一个 plist 文件,该文件存在的目录有:

用户登陆前 LaunchDaemons:

~/Library/LaunchDaemons

用户登录后 LaunchAgents:

~/Library/LaunchAgents

脚本

xml
<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN""http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+    <dict>
+        <key>KeepAlive</key>
+        <dict>
+            <key>SuccessfulExit</key>
+            <false/>
+        </dict>
+        <key>Label</key>
+        <string>com.storyxc.frpc</string>
+        <key>ProgramArguments</key>
+        <array>
+            <string>/Users/story/project/widget/frp/frpc</string>
+            <string>-c</string>
+            <string>/Users/story/project/widget/frp/frpc.ini</string>
+        </array>
+        <key>RunAtLoad</key>
+        <true/>
+    </dict>
+</plist>
<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN""http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+    <dict>
+        <key>KeepAlive</key>
+        <dict>
+            <key>SuccessfulExit</key>
+            <false/>
+        </dict>
+        <key>Label</key>
+        <string>com.storyxc.frpc</string>
+        <key>ProgramArguments</key>
+        <array>
+            <string>/Users/story/project/widget/frp/frpc</string>
+            <string>-c</string>
+            <string>/Users/story/project/widget/frp/frpc.ini</string>
+        </array>
+        <key>RunAtLoad</key>
+        <true/>
+    </dict>
+</plist>

将脚本命名为frpc.plist,然后移动到~/Library/LaunchAgents/

载入plist文件

启动服务:

launchctl [load|enable|bootstrap] -w plist_path

卸载服务:

launchctl [unload|disable|bootout] -w plist_path

设置别名

zsh
# frpc启动、停止
+alias frpc.start='launchctl load -w ~/Library/LaunchAgents/frpc.plist'
+alias frpc.stop='launchctl unload -w ~/Library/LaunchAgents/frpc.plist'
# frpc启动、停止
+alias frpc.start='launchctl load -w ~/Library/LaunchAgents/frpc.plist'
+alias frpc.stop='launchctl unload -w ~/Library/LaunchAgents/frpc.plist'
`,20),t=[o];function e(c,r,E,y,i,g){return a(),n("div",null,t)}const h=s(p,[["render",e]]);export{u as __pageData,h as default}; diff --git "a/assets/actions_env_macos\345\274\200\346\234\272\350\207\252\345\212\250\346\211\247\350\241\214\350\204\232\346\234\254.md.6cf6a3a7.lean.js" "b/assets/actions_env_macos\345\274\200\346\234\272\350\207\252\345\212\250\346\211\247\350\241\214\350\204\232\346\234\254.md.6cf6a3a7.lean.js" new file mode 100644 index 000000000..cdd7c558b --- /dev/null +++ "b/assets/actions_env_macos\345\274\200\346\234\272\350\207\252\345\212\250\346\211\247\350\241\214\350\204\232\346\234\254.md.6cf6a3a7.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"macos开机自动执行脚本","description":"","frontmatter":{},"headers":[],"relativePath":"actions/env/macos开机自动执行脚本.md","filePath":"actions/env/macos开机自动执行脚本.md","lastUpdated":1708510102000}'),p={name:"actions/env/macos开机自动执行脚本.md"},o=l("",20),t=[o];function e(c,r,E,y,i,g){return a(),n("div",null,t)}const h=s(p,[["render",e]]);export{u as __pageData,h as default}; diff --git "a/assets/actions_env_mysql\345\220\257\345\212\250\346\212\245\351\224\231\346\216\222\346\237\245\345\217\212\345\244\204\347\220\206.md.e9f9fd1d.js" "b/assets/actions_env_mysql\345\220\257\345\212\250\346\212\245\351\224\231\346\216\222\346\237\245\345\217\212\345\244\204\347\220\206.md.e9f9fd1d.js" new file mode 100644 index 000000000..2b14e024b --- /dev/null +++ "b/assets/actions_env_mysql\345\220\257\345\212\250\346\212\245\351\224\231\346\216\222\346\237\245\345\217\212\345\244\204\347\220\206.md.e9f9fd1d.js" @@ -0,0 +1,133 @@ +import{_ as s,o as a,c as n,Q as e}from"./chunks/framework.dc1f4608.js";const C=JSON.parse('{"title":"mysql启动报错排查及处理","description":"","frontmatter":{},"headers":[],"relativePath":"actions/env/mysql启动报错排查及处理.md","filePath":"actions/env/mysql启动报错排查及处理.md","lastUpdated":1708510102000}'),p={name:"actions/env/mysql启动报错排查及处理.md"},l=e(`

mysql启动报错排查及处理

今天访问我自己的老博客(www.storyxc.com )发现网站挂掉了,ssh上去看了一下nginx和我自己的java后台博客服务都挂掉了,可能是阿里云抽风服务器重启了。然后重启了nignx和服务访问了一下,查询一直在pending,再去看后台日志,发现获取不到连接,估计是mysql也挂了。

txt
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException: 
+### Error querying database.  Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is com.mysql.cj.jdbc.exceptions.Communi
+cationsException: Communications link failure
+
+The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
+### The error may exist in class path resource [mapper/ArticleDao.xml]
+### The error may involve com.storyxc.mapper.ArticleDao.queryHotArticle
+### The error occurred while executing a query
+### Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is com.mysql.cj.jdbc.exceptions.CommunicationsException: Communic
+ations link failure
+
+The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
+        at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:77) ~[mybatis-spring-1.3.1.jar!/:1.3.1]
+        at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446) ~[mybatis-spring-1.3.1.jar!/:1.3.1]
+        at com.sun.proxy.$Proxy61.selectList(Unknown Source) ~[na:na]
+        at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:230) ~[mybatis-spring-1.3.1.jar!/:1.3.1]
+        at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:137) ~[mybatis-3.4.5.jar!/:3.4.5]
+        at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:75) ~[mybatis-3.4.5.jar!/:3.4.5]
+        at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59) ~[mybatis-3.4.5.jar!/:3.4.5]
+        at com.sun.proxy.$Proxy82.queryHotArticle(Unknown Source) ~[na:na]
+        at com.storyxc.service.impl.ArticleServiceImpl.queryHotArticle(ArticleServiceImpl.java:113) ~[classes!/:1.0-SNAPSHOT]
+        at com.storyxc.service.impl.ArticleServiceImpl$$FastClassBySpringCGLIB$$edb0e759.invoke(<generated>) ~[classes!/:1.0-SNAPSHOT]
+        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.1.6.RELEASE.jar!/:5.1.6.RELEASE]
+        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:684) ~[spring-aop-5.1.6.RELEASE.jar!/:5.1.6.RELEASE]
+        at com.storyxc.service.impl.ArticleServiceImpl$$EnhancerBySpringCGLIB$$5695fd69.queryHotArticle(<generated>) ~[classes!/:1.0-SNAPSHOT]
+        at com.storyxc.controller.ArticleController.queryHotArticle(ArticleController.java:73) ~[classes!/:1.0-SNAPSHOT]
+        at com.storyxc.controller.ArticleController$$FastClassBySpringCGLIB$$954e681b.invoke(<generated>) ~[classes!/:1.0-SNAPSHOT]
+        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.1.6.RELEASE.jar!/:5.1.6.RELEASE]
+        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:684) ~[spring-aop-5.1.6.RELEASE.jar!/:5.1.6.RELEASE]
+        at com.storyxc.controller.ArticleController$$EnhancerBySpringCGLIB$$7f3f634c.queryHotArticle(<generated>) ~[classes!/:1.0-SNAPSHOT]
+        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_282]
+        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_282]
+        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_282]
+        at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_282]
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException: 
+### Error querying database.  Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is com.mysql.cj.jdbc.exceptions.Communi
+cationsException: Communications link failure
+
+The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
+### The error may exist in class path resource [mapper/ArticleDao.xml]
+### The error may involve com.storyxc.mapper.ArticleDao.queryHotArticle
+### The error occurred while executing a query
+### Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is com.mysql.cj.jdbc.exceptions.CommunicationsException: Communic
+ations link failure
+
+The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
+        at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:77) ~[mybatis-spring-1.3.1.jar!/:1.3.1]
+        at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446) ~[mybatis-spring-1.3.1.jar!/:1.3.1]
+        at com.sun.proxy.$Proxy61.selectList(Unknown Source) ~[na:na]
+        at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:230) ~[mybatis-spring-1.3.1.jar!/:1.3.1]
+        at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:137) ~[mybatis-3.4.5.jar!/:3.4.5]
+        at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:75) ~[mybatis-3.4.5.jar!/:3.4.5]
+        at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59) ~[mybatis-3.4.5.jar!/:3.4.5]
+        at com.sun.proxy.$Proxy82.queryHotArticle(Unknown Source) ~[na:na]
+        at com.storyxc.service.impl.ArticleServiceImpl.queryHotArticle(ArticleServiceImpl.java:113) ~[classes!/:1.0-SNAPSHOT]
+        at com.storyxc.service.impl.ArticleServiceImpl$$FastClassBySpringCGLIB$$edb0e759.invoke(<generated>) ~[classes!/:1.0-SNAPSHOT]
+        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.1.6.RELEASE.jar!/:5.1.6.RELEASE]
+        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:684) ~[spring-aop-5.1.6.RELEASE.jar!/:5.1.6.RELEASE]
+        at com.storyxc.service.impl.ArticleServiceImpl$$EnhancerBySpringCGLIB$$5695fd69.queryHotArticle(<generated>) ~[classes!/:1.0-SNAPSHOT]
+        at com.storyxc.controller.ArticleController.queryHotArticle(ArticleController.java:73) ~[classes!/:1.0-SNAPSHOT]
+        at com.storyxc.controller.ArticleController$$FastClassBySpringCGLIB$$954e681b.invoke(<generated>) ~[classes!/:1.0-SNAPSHOT]
+        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.1.6.RELEASE.jar!/:5.1.6.RELEASE]
+        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:684) ~[spring-aop-5.1.6.RELEASE.jar!/:5.1.6.RELEASE]
+        at com.storyxc.controller.ArticleController$$EnhancerBySpringCGLIB$$7f3f634c.queryHotArticle(<generated>) ~[classes!/:1.0-SNAPSHOT]
+        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_282]
+        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_282]
+        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_282]
+        at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_282]

然后尝试重启mysql:service mysql start

直接报错:Starting MySQL...The server quit without updating PID file [FAILED]b/mysql/iz2ze09hymnzdn4lgmltlmz.pid).

但就这样的报错没法排查,试图看下mysql的错误日志:less /var/log/mysql/error.log结果没有,

这才想起来当时没给mysql配置错误日志路径。

给mysql配置错误文件的路径:vim /etc/my.cnf

在[mysqld]下面加一行:log_error=/var/log/mysql/error.log ,然后创建/var/log/mysql这个目录

再次启动,依旧报错,但这次我们可以去看错误日志了。继续 less /var/log/mysql/error.log

txt
210627 00:34:02 mysqld_safe Starting mysqld daemon with databases from /var/lib/mysql
+2021-06-27 00:34:03 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
+2021-06-27 00:34:03 3127 [Note] Plugin 'FEDERATED' is disabled.
+2021-06-27 00:34:03 3127 [Note] InnoDB: Using atomics to ref count buffer pool pages
+2021-06-27 00:34:03 3127 [Note] InnoDB: The InnoDB memory heap is disabled
+2021-06-27 00:34:03 3127 [Note] InnoDB: Mutexes and rw_locks use GCC atomic builtins
+2021-06-27 00:34:03 3127 [Note] InnoDB: Memory barrier is not used
+2021-06-27 00:34:03 3127 [Note] InnoDB: Compressed tables use zlib 1.2.3
+2021-06-27 00:34:03 3127 [Note] InnoDB: Using Linux native AIO
+2021-06-27 00:34:03 3127 [Note] InnoDB: Not using CPU crc32 instructions
+2021-06-27 00:34:03 3127 [Note] InnoDB: Initializing buffer pool, size = 128.0M
+InnoDB: mmap(136019968 bytes) failed; errno 12
+2021-06-27 00:34:03 3127 [ERROR] InnoDB: Cannot allocate memory for the buffer pool
+2021-06-27 00:34:03 3127 [ERROR] Plugin 'InnoDB' init function returned error.
+2021-06-27 00:34:03 3127 [ERROR] Plugin 'InnoDB' registration as a STORAGE ENGINE failed.
+2021-06-27 00:34:03 3127 [ERROR] Unknown/unsupported storage engine: InnoDB
+2021-06-27 00:34:03 3127 [ERROR] Aborting
+
+2021-06-27 00:34:03 3127 [Note] Binlog end
210627 00:34:02 mysqld_safe Starting mysqld daemon with databases from /var/lib/mysql
+2021-06-27 00:34:03 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
+2021-06-27 00:34:03 3127 [Note] Plugin 'FEDERATED' is disabled.
+2021-06-27 00:34:03 3127 [Note] InnoDB: Using atomics to ref count buffer pool pages
+2021-06-27 00:34:03 3127 [Note] InnoDB: The InnoDB memory heap is disabled
+2021-06-27 00:34:03 3127 [Note] InnoDB: Mutexes and rw_locks use GCC atomic builtins
+2021-06-27 00:34:03 3127 [Note] InnoDB: Memory barrier is not used
+2021-06-27 00:34:03 3127 [Note] InnoDB: Compressed tables use zlib 1.2.3
+2021-06-27 00:34:03 3127 [Note] InnoDB: Using Linux native AIO
+2021-06-27 00:34:03 3127 [Note] InnoDB: Not using CPU crc32 instructions
+2021-06-27 00:34:03 3127 [Note] InnoDB: Initializing buffer pool, size = 128.0M
+InnoDB: mmap(136019968 bytes) failed; errno 12
+2021-06-27 00:34:03 3127 [ERROR] InnoDB: Cannot allocate memory for the buffer pool
+2021-06-27 00:34:03 3127 [ERROR] Plugin 'InnoDB' init function returned error.
+2021-06-27 00:34:03 3127 [ERROR] Plugin 'InnoDB' registration as a STORAGE ENGINE failed.
+2021-06-27 00:34:03 3127 [ERROR] Unknown/unsupported storage engine: InnoDB
+2021-06-27 00:34:03 3127 [ERROR] Aborting
+
+2021-06-27 00:34:03 3127 [Note] Binlog end

查看内存情况:free -h,由于我买的是阿里云的轻量级应用服务器-穷逼版,只有2G内存

bash
total        used        free      shared  buff/cache   available
+     Mem:           1.8G        1.2G        245M         88M        323M        268M
+     Swap:           0B          0B          0B
total        used        free      shared  buff/cache   available
+     Mem:           1.8G        1.2G        245M         88M        323M        268M
+     Swap:           0B          0B          0B

swap为0,执行命令建立临时分区

bash
dd if=/dev/zero of=/swap bs=1M count=128  //创建一个swap文件,大小为128M
+mkswap /swap                              //将swap文件变为swap分区文件
+swapon /swap                              //将其映射为swap分区
dd if=/dev/zero of=/swap bs=1M count=128  //创建一个swap文件,大小为128M
+mkswap /swap                              //将swap文件变为swap分区文件
+swapon /swap                              //将其映射为swap分区

再次查看内存:

bash
total        used        free      shared  buff/cache   available
+Mem:           1.8G        1.3G         82M         88M        463M        236M
+Swap:          127M          0B        127M
total        used        free      shared  buff/cache   available
+Mem:           1.8G        1.3G         82M         88M        463M        236M
+Swap:          127M          0B        127M

swap分区已存在,执行命令使系统重启swap分区自动加载:vim /etc/fstab

bash
/swap swap swap defaults 0 0
/swap swap swap defaults 0 0

再次启动 还是他喵不行,执行命令查看下当前内存占用大户。

ps -eo pid,ppid,%mem,%cpu,cmd --sort=-%mem | head

bash
PID  PPID %MEM %CPU CMD
+19872     1 14.0 45.7 ./phpupdate
+20228     1 14.0 45.7 /etc/phpupdate
+ 9372     1  8.0  1.3 java -jar storyxc.jar
+27880     1  3.1  0.0 /opt/openoffice4/program/soffice.bin -headless -accept=socket,host=127.0.0.1,port=8100;urp; -nofirststartwizard
+13389     1  2.6  0.0 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
+23244 21287  2.3  4.7 ./networkmanager 15
+13322     1  1.3  0.0 /usr/bin/containerd
+  489   455  1.0  0.1 CmsGoAgent-Worker start
+19905     1  0.6  0.0 ./phpguard
PID  PPID %MEM %CPU CMD
+19872     1 14.0 45.7 ./phpupdate
+20228     1 14.0 45.7 /etc/phpupdate
+ 9372     1  8.0  1.3 java -jar storyxc.jar
+27880     1  3.1  0.0 /opt/openoffice4/program/soffice.bin -headless -accept=socket,host=127.0.0.1,port=8100;urp; -nofirststartwizard
+13389     1  2.6  0.0 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
+23244 21287  2.3  4.7 ./networkmanager 15
+13322     1  1.3  0.0 /usr/bin/containerd
+  489   455  1.0  0.1 CmsGoAgent-Worker start
+19905     1  0.6  0.0 ./phpguard

好家伙,从哪冒出来的phpupdate 直接全杀掉,世界瞬间清净了,腾出来600M的内存,

再次启动mysql,成功,问题解决。

`,24),o=[l];function t(r,c,y,i,E,F){return a(),n("div",null,o)}const m=s(p,[["render",t]]);export{C as __pageData,m as default}; diff --git "a/assets/actions_env_mysql\345\220\257\345\212\250\346\212\245\351\224\231\346\216\222\346\237\245\345\217\212\345\244\204\347\220\206.md.e9f9fd1d.lean.js" "b/assets/actions_env_mysql\345\220\257\345\212\250\346\212\245\351\224\231\346\216\222\346\237\245\345\217\212\345\244\204\347\220\206.md.e9f9fd1d.lean.js" new file mode 100644 index 000000000..d97bde04c --- /dev/null +++ "b/assets/actions_env_mysql\345\220\257\345\212\250\346\212\245\351\224\231\346\216\222\346\237\245\345\217\212\345\244\204\347\220\206.md.e9f9fd1d.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as e}from"./chunks/framework.dc1f4608.js";const C=JSON.parse('{"title":"mysql启动报错排查及处理","description":"","frontmatter":{},"headers":[],"relativePath":"actions/env/mysql启动报错排查及处理.md","filePath":"actions/env/mysql启动报错排查及处理.md","lastUpdated":1708510102000}'),p={name:"actions/env/mysql启动报错排查及处理.md"},l=e("",24),o=[l];function t(r,c,y,i,E,F){return a(),n("div",null,o)}const m=s(p,[["render",t]]);export{C as __pageData,m as default}; diff --git "a/assets/actions_env_\344\275\277\347\224\250github actions\350\277\233\350\241\214\346\214\201\347\273\255\351\203\250\347\275\262.md.01116c37.js" "b/assets/actions_env_\344\275\277\347\224\250github actions\350\277\233\350\241\214\346\214\201\347\273\255\351\203\250\347\275\262.md.01116c37.js" new file mode 100644 index 000000000..d3fd611f1 --- /dev/null +++ "b/assets/actions_env_\344\275\277\347\224\250github actions\350\277\233\350\241\214\346\214\201\347\273\255\351\203\250\347\275\262.md.01116c37.js" @@ -0,0 +1,113 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"使用github actions进行持续部署","description":"","frontmatter":{},"headers":[],"relativePath":"actions/env/使用github actions进行持续部署.md","filePath":"actions/env/使用github actions进行持续部署.md","lastUpdated":1708510102000}'),p={name:"actions/env/使用github actions进行持续部署.md"},o=l(`

使用github actions进行持续部署

Github Actions官方文档https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#name

Github Actions是什么

Github推出的持续集成工具

使用

配置

Github Actions的配置文件叫做workflow文件,需要存放在repo根路径下的./github/workflows目录中。workflow文件使用yaml格式编写,文件名可以自定义,后缀统一为yml,一个repo中可以有多个workflow,Github只要发现./github/workflows目录中有.yml文件就会自动运行。

本博客的workflow文件:

yml
# 自定义当前执行文件的名称
+name: vuepress
+# 整个流程在main分支发生push事件时触发
+on:
+  push:
+    branches:
+      - main
+jobs:
+  build-and-deploy:
+    runs-on: ubuntu-latest # 运行在ubuntu-latest环境的虚拟机中
+    strategy:
+      matrix:              # 矩阵
+        node-version: [10.x]
+    steps: # 每个 job 由多个 step 构成,它会从上至下依次执行。
+    # 获取仓库源码
+    - name: Checkout
+      uses: actions/checkout@v2         # github actions提供了一些官方的action,例如checkout @v2是action的版本
+    # 安装node
+    - name: Use Node.js \${{ matrix.node-version }} # 定义好的node版本
+      uses: actions/setup-node@v1 # 作用:安装nodejs
+      with:
+        node-version: \${{ matrix.node-version }} # 定义好的node版本
+    # 构建和部署
+    - name: Deploy
+      env: # 环境变量
+        GITHUB_TOKEN: \${{ secrets.vuepress_actions_access_token }}
+      run: npm install && npm run deploy # npm run deploy需要在package.json中定义"deploy: bash deploy.sh"
# 自定义当前执行文件的名称
+name: vuepress
+# 整个流程在main分支发生push事件时触发
+on:
+  push:
+    branches:
+      - main
+jobs:
+  build-and-deploy:
+    runs-on: ubuntu-latest # 运行在ubuntu-latest环境的虚拟机中
+    strategy:
+      matrix:              # 矩阵
+        node-version: [10.x]
+    steps: # 每个 job 由多个 step 构成,它会从上至下依次执行。
+    # 获取仓库源码
+    - name: Checkout
+      uses: actions/checkout@v2         # github actions提供了一些官方的action,例如checkout @v2是action的版本
+    # 安装node
+    - name: Use Node.js \${{ matrix.node-version }} # 定义好的node版本
+      uses: actions/setup-node@v1 # 作用:安装nodejs
+      with:
+        node-version: \${{ matrix.node-version }} # 定义好的node版本
+    # 构建和部署
+    - name: Deploy
+      env: # 环境变量
+        GITHUB_TOKEN: \${{ secrets.vuepress_actions_access_token }}
+      run: npm install && npm run deploy # npm run deploy需要在package.json中定义"deploy: bash deploy.sh"

steps:

配置access_token

  1. 在github个人设置页面找到开发者设置

image-20220421224529199

  1. 选择个人access tokens,选择生成新token

image-20220421224739995

  1. 将生成好的token保存,关闭页面后将不会再显示此token,然后回到仓库保存

repo

Deploy.sh

shell
#!/usr/bin/env sh
+
+# 确保脚本抛出遇到的错误
+set -e
+
+# 生成静态文件
+npm run build
+
+# 进入生成的文件夹
+cd docs/.vuepress/dist
+
+# 如果是发布到自定义域名
+echo 'blog.storyxc.com' > CNAME
+
+if [ -z "\${GITHUB_TOKEN}" ]; then
+  echo "GITHUB_TOKEN is not set"
+  exit 1
+else
+  msg='github actions自动部署'
+  githubUrl=https://storyxc:\${GITHUB_TOKEN}@github.com/storyxc/vuepress.git
+  git config --global user.name "storyxc"
+  git config --global user.email "storyxc@163.com"
+fi
+
+git init
+git add -A
+git commit -m "\${msg}"
+
+git push -f $githubUrl master:gh-pages
+
+cd -
#!/usr/bin/env sh
+
+# 确保脚本抛出遇到的错误
+set -e
+
+# 生成静态文件
+npm run build
+
+# 进入生成的文件夹
+cd docs/.vuepress/dist
+
+# 如果是发布到自定义域名
+echo 'blog.storyxc.com' > CNAME
+
+if [ -z "\${GITHUB_TOKEN}" ]; then
+  echo "GITHUB_TOKEN is not set"
+  exit 1
+else
+  msg='github actions自动部署'
+  githubUrl=https://storyxc:\${GITHUB_TOKEN}@github.com/storyxc/vuepress.git
+  git config --global user.name "storyxc"
+  git config --global user.email "storyxc@163.com"
+fi
+
+git init
+git add -A
+git commit -m "\${msg}"
+
+git push -f $githubUrl master:gh-pages
+
+cd -

这里我是用了github pages发布,然后配置了自定义域名,这个域名要在服务商域名解析配置CNAME,然后在仓库的page页面添加自定义域名即可

image-20220421230458141

流程梳理

`,23),e=[o];function t(c,r,y,E,i,F){return n(),a("div",null,e)}const d=s(p,[["render",t]]);export{h as __pageData,d as default}; diff --git "a/assets/actions_env_\344\275\277\347\224\250github actions\350\277\233\350\241\214\346\214\201\347\273\255\351\203\250\347\275\262.md.01116c37.lean.js" "b/assets/actions_env_\344\275\277\347\224\250github actions\350\277\233\350\241\214\346\214\201\347\273\255\351\203\250\347\275\262.md.01116c37.lean.js" new file mode 100644 index 000000000..ff0f1e2ed --- /dev/null +++ "b/assets/actions_env_\344\275\277\347\224\250github actions\350\277\233\350\241\214\346\214\201\347\273\255\351\203\250\347\275\262.md.01116c37.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"使用github actions进行持续部署","description":"","frontmatter":{},"headers":[],"relativePath":"actions/env/使用github actions进行持续部署.md","filePath":"actions/env/使用github actions进行持续部署.md","lastUpdated":1708510102000}'),p={name:"actions/env/使用github actions进行持续部署.md"},o=l("",23),e=[o];function t(c,r,y,E,i,F){return n(),a("div",null,e)}const d=s(p,[["render",t]]);export{h as __pageData,d as default}; diff --git a/assets/actions_index.md.26d63a13.js b/assets/actions_index.md.26d63a13.js new file mode 100644 index 000000000..e18981a38 --- /dev/null +++ b/assets/actions_index.md.26d63a13.js @@ -0,0 +1 @@ +import{_ as t,o as a,c as n,k as e,a as s}from"./chunks/framework.dc1f4608.js";const x=JSON.parse('{"title":"Actions","description":"","frontmatter":{},"headers":[],"relativePath":"actions/index.md","filePath":"actions/index.md","lastUpdated":1708510102000}'),o={name:"actions/index.md"},i=e("h1",{id:"actions",tabindex:"-1"},[s("Actions "),e("a",{class:"header-anchor",href:"#actions","aria-label":'Permalink to "Actions"'},"​")],-1),c=e("ul",null,[e("li",null,"工具"),e("li",null,"环境"),e("li",null,"设计模式")],-1),l=[i,c];function r(d,_,p,h,m,u){return a(),n("div",null,l)}const k=t(o,[["render",r]]);export{x as __pageData,k as default}; diff --git a/assets/actions_index.md.26d63a13.lean.js b/assets/actions_index.md.26d63a13.lean.js new file mode 100644 index 000000000..e18981a38 --- /dev/null +++ b/assets/actions_index.md.26d63a13.lean.js @@ -0,0 +1 @@ +import{_ as t,o as a,c as n,k as e,a as s}from"./chunks/framework.dc1f4608.js";const x=JSON.parse('{"title":"Actions","description":"","frontmatter":{},"headers":[],"relativePath":"actions/index.md","filePath":"actions/index.md","lastUpdated":1708510102000}'),o={name:"actions/index.md"},i=e("h1",{id:"actions",tabindex:"-1"},[s("Actions "),e("a",{class:"header-anchor",href:"#actions","aria-label":'Permalink to "Actions"'},"​")],-1),c=e("ul",null,[e("li",null,"工具"),e("li",null,"环境"),e("li",null,"设计模式")],-1),l=[i,c];function r(d,_,p,h,m,u){return a(),n("div",null,l)}const k=t(o,[["render",r]]);export{x as __pageData,k as default}; diff --git "a/assets/actions_tools_Markdown\345\237\272\347\241\200\350\257\255\346\263\225.md.9b7f7a0a.js" "b/assets/actions_tools_Markdown\345\237\272\347\241\200\350\257\255\346\263\225.md.9b7f7a0a.js" new file mode 100644 index 000000000..7f26001c4 --- /dev/null +++ "b/assets/actions_tools_Markdown\345\237\272\347\241\200\350\257\255\346\263\225.md.9b7f7a0a.js" @@ -0,0 +1,99 @@ +import{_ as s,o as a,c as n,Q as e}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"Markdown基础语法","description":"","frontmatter":{},"headers":[],"relativePath":"actions/tools/Markdown基础语法.md","filePath":"actions/tools/Markdown基础语法.md","lastUpdated":1708510102000}'),l={name:"actions/tools/Markdown基础语法.md"},p=e(`

Markdown基础语法

Markdown 是一种轻量级标记语言,它允许人们使用易读易写的纯文本格式编写文档。

Markdown 语言在 2004 由约翰·格鲁伯(英语:John Gruber)创建。

Markdown 编写的文档可以导出 HTML 、Word、图像、PDF、Epub 等多种格式的文档。

Markdown 编写的文档后缀为 .md, .markdown。

一、标题

示例:

txt
# 一级标题
+## 二级标题
+### 三级标题
+#### 四级标题
+##### 五级标题
+###### 六级标题
# 一级标题
+## 二级标题
+### 三级标题
+#### 四级标题
+##### 五级标题
+###### 六级标题

二、字体

示例:

txt
**这是加粗的文字**
+*这是倾斜的文字*\`
+***这是斜体加粗的文字***
+~~这是加删除线的文字~~
**这是加粗的文字**
+*这是倾斜的文字*\`
+***这是斜体加粗的文字***
+~~这是加删除线的文字~~

效果: 这是加粗的文字这是倾斜的文字\` 这是斜体加粗的文字这是加删除线的文字

三、引用

只需要在你希望引用的文字前面加上 > 就好,例如:

txt
> 这是一条引用
> 这是一条引用

效果如下:

这是一条引用

引用还可以进行多级嵌套

txt
> 这是一条引用
+>> 这是一条引用
+>>> 这是一条引用
> 这是一条引用
+>> 这是一条引用
+>>> 这是一条引用

效果:

这是一条引用

这是一条引用

这是一条引用

四、分割线

三个或者三个以上的 - 或者 * 都可以。

txt
---
+----
+***
+*****
---
+----
+***
+*****




五、图片

语法:

txt
![图片alt](图片地址 ''图片title'')
+
+图片alt就是显示在图片下面的文字,相当于对图片内容的解释。
+图片title是图片的标题,当鼠标移到图片上时显示的内容。title可加可不加
![图片alt](图片地址 ''图片title'')
+
+图片alt就是显示在图片下面的文字,相当于对图片内容的解释。
+图片title是图片的标题,当鼠标移到图片上时显示的内容。title可加可不加

示例:

txt
![示例图片alt](http://io.storyxc.com/images/MegellanicCloud_ZH-CN5132305226_1920x1080.jpg '示例图片title')
![示例图片alt](http://io.storyxc.com/images/MegellanicCloud_ZH-CN5132305226_1920x1080.jpg '示例图片title')

效果: 示例图片alt

六、超链接

语法:

txt
[超链接名](超链接地址 "超链接title")
+title可加可不加
[超链接名](超链接地址 "超链接title")
+title可加可不加

示例:

txt
[故事的博客](https://www.storyxc.com "故事的博客")
[故事的博客](https://www.storyxc.com "故事的博客")

效果 故事的博客

七、列表

无序列表 语法: 无序列表用 - + * 任何一种都可以 示例:

txt
- 列表1
++ 列表2
+* 列表3
- 列表1
++ 列表2
+* 列表3

效果

有序列表 语法: 数字加点 示例:

txt
1. 111
+2. 222
+3. 333
1. 111
+2. 222
+3. 333

效果:

  1. 111
  2. 222
  3. 333

列表嵌套

上一级和下一级之间tab即可

八、表格

语法:

txt
表头|表头|表头
+---|:--:|---:
+内容|内容|内容
+内容|内容|内容
+
+第二行分割表头和内容。
+- 有一个就行,为了对齐,多加了几个
+文字默认居左
+-两边加:表示文字居中
+-右边加:表示文字居右
+注:原生的语法两边都要用 | 包起来。此处省略
+
+姓名|技能|排行
+--|:--:|--:
+刘备|哭|大哥
+关羽|打|二哥
+张飞|骂|三弟
表头|表头|表头
+---|:--:|---:
+内容|内容|内容
+内容|内容|内容
+
+第二行分割表头和内容。
+- 有一个就行,为了对齐,多加了几个
+文字默认居左
+-两边加:表示文字居中
+-右边加:表示文字居右
+注:原生的语法两边都要用 | 包起来。此处省略
+
+姓名|技能|排行
+--|:--:|--:
+刘备|哭|大哥
+关羽|打|二哥
+张飞|骂|三弟

效果:

姓名技能排行
刘备大哥
关羽二哥
张飞三弟

九、代码块

示例:

txt
\`这是一行代码\`
\`这是一行代码\`

效果: 这是一行代码

txt
(\`\`\`)语言名
+	代码
+(\`\`\`)
(\`\`\`)语言名
+	代码
+(\`\`\`)

示例:以java为例

txt
(\`\`\`)java
+	public class HelloWorld{
+		public static void main(Stringargs[]){
+			System.out.println("Hello World!");
+		}
+	}
+(\`\`\`)
(\`\`\`)java
+	public class HelloWorld{
+		public static void main(Stringargs[]){
+			System.out.println("Hello World!");
+		}
+	}
+(\`\`\`)

效果

java
public class HelloWorld{
+	public static void main(String args[]){
+		System.out.println("Hello World!");
+	}
+}
public class HelloWorld{
+	public static void main(String args[]){
+		System.out.println("Hello World!");
+	}
+}
`,71),t=[p];function o(c,i,r,d,y,h){return a(),n("div",null,t)}const b=s(l,[["render",o]]);export{u as __pageData,b as default}; diff --git "a/assets/actions_tools_Markdown\345\237\272\347\241\200\350\257\255\346\263\225.md.9b7f7a0a.lean.js" "b/assets/actions_tools_Markdown\345\237\272\347\241\200\350\257\255\346\263\225.md.9b7f7a0a.lean.js" new file mode 100644 index 000000000..7d9d1d38d --- /dev/null +++ "b/assets/actions_tools_Markdown\345\237\272\347\241\200\350\257\255\346\263\225.md.9b7f7a0a.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as e}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"Markdown基础语法","description":"","frontmatter":{},"headers":[],"relativePath":"actions/tools/Markdown基础语法.md","filePath":"actions/tools/Markdown基础语法.md","lastUpdated":1708510102000}'),l={name:"actions/tools/Markdown基础语法.md"},p=e("",71),t=[p];function o(c,i,r,d,y,h){return a(),n("div",null,t)}const b=s(l,[["render",o]]);export{u as __pageData,b as default}; diff --git a/assets/actions_tools_Sketch.md.b25b7913.js b/assets/actions_tools_Sketch.md.b25b7913.js new file mode 100644 index 000000000..92295bf9c --- /dev/null +++ b/assets/actions_tools_Sketch.md.b25b7913.js @@ -0,0 +1 @@ +import{_ as e,o,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"Sketch","description":"","frontmatter":{},"headers":[],"relativePath":"actions/tools/Sketch.md","filePath":"actions/tools/Sketch.md","lastUpdated":1708510102000}'),c={name:"actions/tools/Sketch.md"},i=l('

Sketch

快捷键

画布

形状工具

以上工具可以按住option使用扩散的绘制效果

样式工具

矢量工具

文字工具

画板

切片工具

编辑工具

',19),d=[i];function t(r,h,n,s,u,p){return o(),a("div",null,d)}const k=e(c,[["render",t]]);export{m as __pageData,k as default}; diff --git a/assets/actions_tools_Sketch.md.b25b7913.lean.js b/assets/actions_tools_Sketch.md.b25b7913.lean.js new file mode 100644 index 000000000..f68ee46af --- /dev/null +++ b/assets/actions_tools_Sketch.md.b25b7913.lean.js @@ -0,0 +1 @@ +import{_ as e,o,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"Sketch","description":"","frontmatter":{},"headers":[],"relativePath":"actions/tools/Sketch.md","filePath":"actions/tools/Sketch.md","lastUpdated":1708510102000}'),c={name:"actions/tools/Sketch.md"},i=l("",19),d=[i];function t(r,h,n,s,u,p){return o(),a("div",null,d)}const k=e(c,[["render",t]]);export{m as __pageData,k as default}; diff --git "a/assets/actions_tools_Typora\343\200\201PicGo\343\200\201\344\270\203\347\211\233\344\272\221\345\256\236\347\216\260markdown\345\233\276\347\211\207\350\207\252\345\212\250\344\270\212\344\274\240\345\233\276\345\272\212.md.6002b22d.js" "b/assets/actions_tools_Typora\343\200\201PicGo\343\200\201\344\270\203\347\211\233\344\272\221\345\256\236\347\216\260markdown\345\233\276\347\211\207\350\207\252\345\212\250\344\270\212\344\274\240\345\233\276\345\272\212.md.6002b22d.js" new file mode 100644 index 000000000..0b3bb8b2d --- /dev/null +++ "b/assets/actions_tools_Typora\343\200\201PicGo\343\200\201\344\270\203\347\211\233\344\272\221\345\256\236\347\216\260markdown\345\233\276\347\211\207\350\207\252\345\212\250\344\270\212\344\274\240\345\233\276\345\272\212.md.6002b22d.js" @@ -0,0 +1 @@ +import{_ as a,o,c as e,Q as t}from"./chunks/framework.dc1f4608.js";const _=JSON.parse('{"title":"Typora、PicGo、七牛云实现markdown图片自动上传图床","description":"","frontmatter":{},"headers":[],"relativePath":"actions/tools/Typora、PicGo、七牛云实现markdown图片自动上传图床.md","filePath":"actions/tools/Typora、PicGo、七牛云实现markdown图片自动上传图床.md","lastUpdated":1708510102000}'),r={name:"actions/tools/Typora、PicGo、七牛云实现markdown图片自动上传图床.md"},i=t('

Typora、PicGo、七牛云实现markdown图片自动上传图床

背景

由于最近刚把博客迁到VuePress上来,写博客从原来自己博客项目自定义的web端Markdown编辑器换回了原来的Typora,这个文本编辑工具虽然很好用,但是在markdown中插入图片的时候就会碰到比较烦的问题,可能随便截了个图放在桌面了,在markdown中引入的话还要先把图片放到vuepress项目的静态资源文件夹里面。原来我自己开发的博客编辑器是通过axios调用后台接口把图片传到七牛云图床上去,现在换了博客框架原来的方案不好使了。之前typora也没有这方面的支持。不过我发现typora更新之后也支持了添加图片后的事件触发。

image-20210410202031986

选项还是很多样的,为typora点赞。

这里我还是选择了上传到图床,上传支持PicGo和自定义脚本,本来打算写个python脚本的,后来发现picgo这个应用也很好用,那就直接拿过来用吧。

配置picgo

通过gui配置

1.下载picgo

点击PicGo 进入仓库下载,这里选择windows版的执行程序,下载之后打开

2.申请七牛云账号配置存储空间

没有图床的可以搜一下相关教程,这里不再赘述

3.配置图床信息

image-20210410202814211

image-20210413020009764

选择七牛云图床、或者自己选择其他图床也许,按照自己情况来。

image-20210410203157833

4.回到typora测试一下吧

image-20210410203327770

image-20210410203412022

先验证一下,可以看到成功了。这样再在typora中添加图片就可以看到图片会自动上传到你的图床并修改markdown中的地址了。

通过picgo-core配置

npm安装picgo-core:npm install -g picgo

配置uploader:picgo set uploader

使用uploader:picgo use uploader

配置文件地址为~/.picgo/config.json,可以手动修改

配置完成后在typora中配置自定义命令上传,命令格式 node_path picgo_path upload例如我的是/opt/homebrew/bin/node /opt/homebrew/bin/picgo upload

image-20220214180934655

验证上传选项,看到返回图片地址即可

',30),p=[i];function c(l,n,s,d,g,h){return o(),e("div",null,p)}const u=a(r,[["render",c]]);export{_ as __pageData,u as default}; diff --git "a/assets/actions_tools_Typora\343\200\201PicGo\343\200\201\344\270\203\347\211\233\344\272\221\345\256\236\347\216\260markdown\345\233\276\347\211\207\350\207\252\345\212\250\344\270\212\344\274\240\345\233\276\345\272\212.md.6002b22d.lean.js" "b/assets/actions_tools_Typora\343\200\201PicGo\343\200\201\344\270\203\347\211\233\344\272\221\345\256\236\347\216\260markdown\345\233\276\347\211\207\350\207\252\345\212\250\344\270\212\344\274\240\345\233\276\345\272\212.md.6002b22d.lean.js" new file mode 100644 index 000000000..2cd42f249 --- /dev/null +++ "b/assets/actions_tools_Typora\343\200\201PicGo\343\200\201\344\270\203\347\211\233\344\272\221\345\256\236\347\216\260markdown\345\233\276\347\211\207\350\207\252\345\212\250\344\270\212\344\274\240\345\233\276\345\272\212.md.6002b22d.lean.js" @@ -0,0 +1 @@ +import{_ as a,o,c as e,Q as t}from"./chunks/framework.dc1f4608.js";const _=JSON.parse('{"title":"Typora、PicGo、七牛云实现markdown图片自动上传图床","description":"","frontmatter":{},"headers":[],"relativePath":"actions/tools/Typora、PicGo、七牛云实现markdown图片自动上传图床.md","filePath":"actions/tools/Typora、PicGo、七牛云实现markdown图片自动上传图床.md","lastUpdated":1708510102000}'),r={name:"actions/tools/Typora、PicGo、七牛云实现markdown图片自动上传图床.md"},i=t("",30),p=[i];function c(l,n,s,d,g,h){return o(),e("div",null,p)}const u=a(r,[["render",c]]);export{_ as __pageData,u as default}; diff --git "a/assets/actions_tools_book-searcher\347\224\265\345\255\220\344\271\246\351\225\234\345\203\217.md.c0cc3ed0.js" "b/assets/actions_tools_book-searcher\347\224\265\345\255\220\344\271\246\351\225\234\345\203\217.md.c0cc3ed0.js" new file mode 100644 index 000000000..0774bdcc1 --- /dev/null +++ "b/assets/actions_tools_book-searcher\347\224\265\345\255\220\344\271\246\351\225\234\345\203\217.md.c0cc3ed0.js" @@ -0,0 +1,27 @@ +import{_ as s,o as a,c as n,Q as e}from"./chunks/framework.dc1f4608.js";const b=JSON.parse('{"title":"book-searcher电子书镜像站点","description":"","frontmatter":{},"headers":[],"relativePath":"actions/tools/book-searcher电子书镜像.md","filePath":"actions/tools/book-searcher电子书镜像.md","lastUpdated":1708510102000}'),o={name:"actions/tools/book-searcher电子书镜像.md"},l=e(`

book-searcher电子书镜像站点

项目地址:https://github.com/book-searcher-org/book-searcher

docker-compose.yml

yml
version: '3'
+
+services:
+  book-searcher:
+    image: ghcr.io/book-searcher-org/book-searcher:latest
+    container_name: book-searcher
+    restart: always
+    ports:
+      - "7070:7070"
+    volumes:
+      - ./index:/index
version: '3'
+
+services:
+  book-searcher:
+    image: ghcr.io/book-searcher-org/book-searcher:latest
+    container_name: book-searcher
+    restart: always
+    ports:
+      - "7070:7070"
+    volumes:
+      - ./index:/index

下载index文件

https://zh.annas-archive.org/datasets

https://onedrive.caomingjun.com/zh-CN/🖥软件/zlib-searcher/

启动容器配置IPFS网关

txt
https://cloudflare-ipfs.com
+https://dweb.link
+https://ipfs.io
+https://dw.oho.im
https://cloudflare-ipfs.com
+https://dweb.link
+https://ipfs.io
+https://dw.oho.im
`,8),p=[l];function r(t,c,i,h,E,y){return a(),n("div",null,p)}const k=s(o,[["render",r]]);export{b as __pageData,k as default}; diff --git "a/assets/actions_tools_book-searcher\347\224\265\345\255\220\344\271\246\351\225\234\345\203\217.md.c0cc3ed0.lean.js" "b/assets/actions_tools_book-searcher\347\224\265\345\255\220\344\271\246\351\225\234\345\203\217.md.c0cc3ed0.lean.js" new file mode 100644 index 000000000..7c95d67b9 --- /dev/null +++ "b/assets/actions_tools_book-searcher\347\224\265\345\255\220\344\271\246\351\225\234\345\203\217.md.c0cc3ed0.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as e}from"./chunks/framework.dc1f4608.js";const b=JSON.parse('{"title":"book-searcher电子书镜像站点","description":"","frontmatter":{},"headers":[],"relativePath":"actions/tools/book-searcher电子书镜像.md","filePath":"actions/tools/book-searcher电子书镜像.md","lastUpdated":1708510102000}'),o={name:"actions/tools/book-searcher电子书镜像.md"},l=e("",8),p=[l];function r(t,c,i,h,E,y){return a(),n("div",null,p)}const k=s(o,[["render",r]]);export{b as __pageData,k as default}; diff --git "a/assets/actions_tools_git\345\221\275\344\273\244\346\225\264\347\220\206.md.853e93cd.js" "b/assets/actions_tools_git\345\221\275\344\273\244\346\225\264\347\220\206.md.853e93cd.js" new file mode 100644 index 000000000..8344b9846 --- /dev/null +++ "b/assets/actions_tools_git\345\221\275\344\273\244\346\225\264\347\220\206.md.853e93cd.js" @@ -0,0 +1,7 @@ +import{_ as e,o,c as t,Q as c}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"git命令整理","description":"","frontmatter":{},"headers":[],"relativePath":"actions/tools/git命令整理.md","filePath":"actions/tools/git命令整理.md","lastUpdated":1708510102000}'),a={name:"actions/tools/git命令整理.md"},p=c(`

git命令整理

版本控制工具一直用的GIT,之前提交代码都是用IDEA集成的GIT可视化工具,命令行几乎不怎么用,由于接下来项目要整合到微服务平台中,项目代码管理也要迁到Gerrit,idea的集成支持不太好,所以整理下GIT的命令,方便后面使用命令行提交代码。 180874352b52aaf65be47442.jpg

Remote:远程仓库
+Reporsitory:本地仓库
+WorkSpace:工作区
+Index:暂存区
Remote:远程仓库
+Reporsitory:本地仓库
+WorkSpace:工作区
+Index:暂存区

撤回修改

切换分支

git checkout branch_name

切换分之前要注意本地分支是否有未commit的文件,如果有可以撤销改动,或者commit,再或者使用git stash将当前分支的改动临时保存起来,使当前分支的工作空间和暂存区变干净。然后再进行切换分支;切换回之前的分支,需要恢复被临时保存的改动

暂存和恢复本地修改

git stash -u

恢复本地修改:

1.先查看有多少个临时保存的改动

git stash list

2.再用git stash apply --index stash@{n},n为使用git stash list查看到的某个改动的数字

3.再用git stash drop stash@{n}删除临时保存的改动

如果只有一个临时的stash,那么可以直接git stash apply即可恢复上次的临时保存记录

创建本地分支

基于本地master分支创建test分支为例:

先切换到master分支:git checkout master

建分支: git branch test

切分支:git checkout test

或者

建分支后切到该分支:git checkout -b test master

以基于某次commit id创建test分支为例:

git checkout -b test 0faceff

其中的0faceff为commit id的前7位

以基于某个tag创建test分支为例

git checkout -b test v0.1.0

v0.1.0为tag的名称

查看分支

git branch: 只显示本地分支名,当前分支名前有星号

git branch -v:显示本地分支名,当前分支前有星号,显示commit id

git branch -vv:显示本地分支名,当前分支名前有星号,显示commit id,显示追踪的远程分支名

git branch -a:显示所有分支名(包括远程分支)

git branch -r:查看远程分支名

删除本地分支

普通删除:git branch -d branch_name

强制删除(分支上有修改未合并到其他分支):git branch -D branch_name

更新代码

git pull或者git fetch

git pull -v --progress "origin"命令可以显示更详细的信息,git pull命令会fetch所有的远程分支的信息到本地,同时当前本地分支会被合并。

如果本地有修改文件,而且远程仓库也修改了该文件,pull会失败,提示本地的修改会被合并覆盖,此时可以commit本地的修改或者stash本地的修改,再pull。

修改代码

首先使用git checkout branch_name切换到正确分支,pull,新建或修改代码,再使用git add 文件名把修改或新增的文件添加到暂存区,再执行commit命令提交到本地仓库。

其中:

git add 某个文件

git add 多个文件(文件名用空格隔开)

git add -u 添加所有修改的文件到暂存区

git add .添加所有修改和新增的文件到暂存区

git add -A:添加所有修改,新增和删除的文件到暂存区

git commit 文件名 -m "注释":commit某个文件

git commit 文件1 文件2 -m "注释" commit多个文件,用空格隔开

git commit -m "注释"commit所有文件

如果是删除文件,可以使用

rm 文件

git add 文件

git commit 文件 -m "注释"

如果是重命名文件或者移动文件,可以使用

git mv 源文件路径 目标文件路径

git commit 文件 -m "注释"

push代码带远程仓库

本地代码从本地branch_name分支推到远端branch_name分支:

git checkout branch_name

git pull

git push origin HEAD:refs/for/branch_name

或者

git checkout branch_name

git pull

git push origin branch_name:refs/for/branch_name

查看信息

git status 显示有变更的文件

git log 显示当前分支的版本历史

git log --stat 显示commit历史,以及每次commit发生变更的文件

git log -S [keyword] 搜索提交历史,根据关键词

git log [tag] HEAD --pretty=format:%s 显示某个commit之后的所有变动,每个commit占据一行

git log [tag] HEAD --grep feature 显示某个commit之后的所有变动,其"提交说明"必须符合搜索条件

git log -p [file] 显示指定文件相关的每一次diff

git log -5 --pretty --oneline 显示过去5次提交

git shortlog -sn 显示所有提交过的用户,按提交次数排序

git blame [file] 显示指定文件是什么人在什么时间修改过

git diff 显示暂存区和工作区的代码差异

git diff --cached [file] 显示暂存区和上一个commit的差异

git diff HEAD 显示工作区与当前分支最新commit之间的差异

git diff [first-branch]...[second-branch] 显示两次提交之间的差异

git diff --shortstat "@{0 day ago}" 显示今天你写了多少行代码

git show [commit] 显示某次提交的元数据和内容变化

git show --name-only [commit] 显示某次提交发生变化的文件

git show [commit]:[filename] 显示某次提交时,某个文件的内容

git rebase [branch] 从本地master拉取代码更新当前分支:branch 一般为master

fetch vs pull

git fetch是将远程的最新内容拉到本地,用户在检查了以后决定是否合并到本地分支中。 而git pull 则是将远程的最新内容拉下来后直接合并,即:git pull = git fetch + git merge,这样可能会产生冲突,需要手动解决。

`,92),i=[p];function d(s,l,r,n,h,g){return o(),t("div",null,i)}const f=e(a,[["render",d]]);export{u as __pageData,f as default}; diff --git "a/assets/actions_tools_git\345\221\275\344\273\244\346\225\264\347\220\206.md.853e93cd.lean.js" "b/assets/actions_tools_git\345\221\275\344\273\244\346\225\264\347\220\206.md.853e93cd.lean.js" new file mode 100644 index 000000000..7a21d1079 --- /dev/null +++ "b/assets/actions_tools_git\345\221\275\344\273\244\346\225\264\347\220\206.md.853e93cd.lean.js" @@ -0,0 +1 @@ +import{_ as e,o,c as t,Q as c}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"git命令整理","description":"","frontmatter":{},"headers":[],"relativePath":"actions/tools/git命令整理.md","filePath":"actions/tools/git命令整理.md","lastUpdated":1708510102000}'),a={name:"actions/tools/git命令整理.md"},p=c("",92),i=[p];function d(s,l,r,n,h,g){return o(),t("div",null,i)}const f=e(a,[["render",d]]);export{u as __pageData,f as default}; diff --git "a/assets/actions_tools_iterm2\351\205\215\345\220\210oh-my-zsh\351\205\215\347\275\256\344\270\252\346\200\247\344\270\273\351\242\230\347\273\210\347\253\257.md.f87cdce3.js" "b/assets/actions_tools_iterm2\351\205\215\345\220\210oh-my-zsh\351\205\215\347\275\256\344\270\252\346\200\247\344\270\273\351\242\230\347\273\210\347\253\257.md.f87cdce3.js" new file mode 100644 index 000000000..0fe6fb3fb --- /dev/null +++ "b/assets/actions_tools_iterm2\351\205\215\345\220\210oh-my-zsh\351\205\215\347\275\256\344\270\252\346\200\247\344\270\273\351\242\230\347\273\210\347\253\257.md.f87cdce3.js" @@ -0,0 +1,17 @@ +import{_ as s,o as a,c as e,Q as o}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"iterm2配合oh-my-zsh配置个性主题终端","description":"","frontmatter":{},"headers":[],"relativePath":"actions/tools/iterm2配合oh-my-zsh配置个性主题终端.md","filePath":"actions/tools/iterm2配合oh-my-zsh配置个性主题终端.md","lastUpdated":1708510102000}'),l={name:"actions/tools/iterm2配合oh-my-zsh配置个性主题终端.md"},n=o(`

iterm2配合oh-my-zsh配置个性主题终端

安装iterm2

官网下载:https://iterm2.com/

安装oh my zsh

官网:https://ohmyz.sh/

安装脚本:sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

因为网络原因无法执行这个脚本的可以找gitee上的国内源

更改iterm2的主题颜色为dracula

在iterm2的dracula主题仓库中下载color文件

仓库地址: https://github.com/dracula/iterm.git

image-20211119150619779

打开iterm2导入刚下载的color文件

image-20211119150804269

如图,导入完之后就可以选择导入的dracula主题颜色

安装命令高亮插件

clone代码到本地

git clone https://github.com/zsh-users/zsh-syntax-highlighting $ZSH_CUSTOM/plugins/zsh-syntax-highlighting

安装历史指令提示插件

clone代码到本地

git clone https://github.com/zsh-users/zsh-autosuggestions $ZSH_CUSTOM/plugins/zsh-autosuggestions

修改.zshrc配置

shell
# ~/.zshrc
+plugins=(
+  git
+  zsh-autosuggestions
+  zsh-syntax-highlighting
+)
# ~/.zshrc
+plugins=(
+  git
+  zsh-autosuggestions
+  zsh-syntax-highlighting
+)

效果

image-20211119151720033

powerlevel10k主题

https://github.com/romkatv/powerlevel10k

shell
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ~/powerlevel10k
+echo 'source ~/powerlevel10k/powerlevel10k.zsh-theme' >>~/.zshrc
+
+p10k configure
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ~/powerlevel10k
+echo 'source ~/powerlevel10k/powerlevel10k.zsh-theme' >>~/.zshrc
+
+p10k configure
`,28),t=[n];function r(p,c,i,h,d,m){return a(),e("div",null,t)}const y=s(l,[["render",r]]);export{u as __pageData,y as default}; diff --git "a/assets/actions_tools_iterm2\351\205\215\345\220\210oh-my-zsh\351\205\215\347\275\256\344\270\252\346\200\247\344\270\273\351\242\230\347\273\210\347\253\257.md.f87cdce3.lean.js" "b/assets/actions_tools_iterm2\351\205\215\345\220\210oh-my-zsh\351\205\215\347\275\256\344\270\252\346\200\247\344\270\273\351\242\230\347\273\210\347\253\257.md.f87cdce3.lean.js" new file mode 100644 index 000000000..d0d687dcb --- /dev/null +++ "b/assets/actions_tools_iterm2\351\205\215\345\220\210oh-my-zsh\351\205\215\347\275\256\344\270\252\346\200\247\344\270\273\351\242\230\347\273\210\347\253\257.md.f87cdce3.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as e,Q as o}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"iterm2配合oh-my-zsh配置个性主题终端","description":"","frontmatter":{},"headers":[],"relativePath":"actions/tools/iterm2配合oh-my-zsh配置个性主题终端.md","filePath":"actions/tools/iterm2配合oh-my-zsh配置个性主题终端.md","lastUpdated":1708510102000}'),l={name:"actions/tools/iterm2配合oh-my-zsh配置个性主题终端.md"},n=o("",28),t=[n];function r(p,c,i,h,d,m){return a(),e("div",null,t)}const y=s(l,[["render",r]]);export{u as __pageData,y as default}; diff --git "a/assets/actions_tools_iterm2\351\205\215\347\275\256ssh\345\277\253\351\200\237\350\277\236\346\216\245.md.3487ddd6.js" "b/assets/actions_tools_iterm2\351\205\215\347\275\256ssh\345\277\253\351\200\237\350\277\236\346\216\245.md.3487ddd6.js" new file mode 100644 index 000000000..c02da5cd1 --- /dev/null +++ "b/assets/actions_tools_iterm2\351\205\215\347\275\256ssh\345\277\253\351\200\237\350\277\236\346\216\245.md.3487ddd6.js" @@ -0,0 +1 @@ +import{_ as e,o as a,c as t,Q as s}from"./chunks/framework.dc1f4608.js";const _=JSON.parse('{"title":"iterm2配置ssh快速连接","description":"","frontmatter":{},"headers":[],"relativePath":"actions/tools/iterm2配置ssh快速连接.md","filePath":"actions/tools/iterm2配置ssh快速连接.md","lastUpdated":1708510102000}'),o={name:"actions/tools/iterm2配置ssh快速连接.md"},i=s('

iterm2配置ssh快速连接

macos生态的ssh工具有很多,但是试了很多还是感觉很差劲,不如windows生态的mobaxterm和xshell,可惜这两个软件没有mac的版本。不过iterm2作为mac生态下的终端工具代表倒是很简洁方便,但是没有专门的ssh工具那种简易的远程连接配置,需要自己动手折腾一下才可以。

安装iterm2

官网下载,不赘述。

配置profile

  1. command+,打开偏好设置,选择profilesimage-20211120112843166

  2. 新建一个profile设置,将command设置从login shell改为command,并输入需要执行的ssh指令

    image-20211120113120836

  3. 切换到advanced选项卡,选择编辑triggers触发器。新增一个触发器,action选择send text,触发的表达式改为root@xxx.xxx.xxx.xxx's password,参数改为登陆账户的密码+\\n,这里注意一定要加\\n代表输入回车,不然就会卡在输入密码那里需要手动回车才能登陆,然后勾选上instant立即触发。

    image-20211120113408181

image-20211120114241239

这里触发器的表达式即是输入ssh命令时,服务器给出的需要输入密码的提示文字,所以想配置什么服务器的触发器直接改个登陆名和服务器地址就可以

image-20211120114556561

测试

配置完毕后,可以根据菜单栏的profiles选项卡,选择需要连接的服务器即可

image-20211120114848453

image-20211120114946181

',14),r=[i];function m(c,l,p,n,h,g){return a(),t("div",null,r)}const x=e(o,[["render",m]]);export{_ as __pageData,x as default}; diff --git "a/assets/actions_tools_iterm2\351\205\215\347\275\256ssh\345\277\253\351\200\237\350\277\236\346\216\245.md.3487ddd6.lean.js" "b/assets/actions_tools_iterm2\351\205\215\347\275\256ssh\345\277\253\351\200\237\350\277\236\346\216\245.md.3487ddd6.lean.js" new file mode 100644 index 000000000..1d55c6330 --- /dev/null +++ "b/assets/actions_tools_iterm2\351\205\215\347\275\256ssh\345\277\253\351\200\237\350\277\236\346\216\245.md.3487ddd6.lean.js" @@ -0,0 +1 @@ +import{_ as e,o as a,c as t,Q as s}from"./chunks/framework.dc1f4608.js";const _=JSON.parse('{"title":"iterm2配置ssh快速连接","description":"","frontmatter":{},"headers":[],"relativePath":"actions/tools/iterm2配置ssh快速连接.md","filePath":"actions/tools/iterm2配置ssh快速连接.md","lastUpdated":1708510102000}'),o={name:"actions/tools/iterm2配置ssh快速连接.md"},i=s("",14),r=[i];function m(c,l,p,n,h,g){return a(),t("div",null,r)}const x=e(o,[["render",m]]);export{_ as __pageData,x as default}; diff --git "a/assets/actions_tools_linux\350\256\276\347\275\256macOS\346\227\266\351\227\264\346\234\272\345\231\250server.md.3c94d4f3.js" "b/assets/actions_tools_linux\350\256\276\347\275\256macOS\346\227\266\351\227\264\346\234\272\345\231\250server.md.3c94d4f3.js" new file mode 100644 index 000000000..3229f579c --- /dev/null +++ "b/assets/actions_tools_linux\350\256\276\347\275\256macOS\346\227\266\351\227\264\346\234\272\345\231\250server.md.3c94d4f3.js" @@ -0,0 +1,5 @@ +import{_ as a,o as e,c as t,Q as n}from"./chunks/framework.dc1f4608.js";const _=JSON.parse('{"title":"linux设置macOS时间机器server","description":"","frontmatter":{},"headers":[],"relativePath":"actions/tools/linux设置macOS时间机器server.md","filePath":"actions/tools/linux设置macOS时间机器server.md","lastUpdated":1708510102000}'),s={name:"actions/tools/linux设置macOS时间机器server.md"},o=n(`

linux设置macOS时间机器server

安装需要的包

sudo apt install netatalk avahi-daemon

编辑netatalk配置文件

sudo vim /etc/netatalk/afp.conf

添加Time Machine配置

txt
[Time Machine]
+path = /mnt/data/backup/time_machine
+time machine = yes
[Time Machine]
+path = /mnt/data/backup/time_machine
+time machine = yes

创建目录

sudo mkdir -p /mnt/data/backup/time_machine

sudo chown nobody:nogroup /mnt/data/backup/time_machine

sudo chmod 777 /mnt/data/backup/time_machine

重启netatalk服务

sudo systemctl restart netatalk

在mac上进行备份

时间机器中选择磁盘,连接linux server即可。

`,15),c=[o];function i(l,r,d,p,h,m){return e(),t("div",null,c)}const k=a(s,[["render",i]]);export{_ as __pageData,k as default}; diff --git "a/assets/actions_tools_linux\350\256\276\347\275\256macOS\346\227\266\351\227\264\346\234\272\345\231\250server.md.3c94d4f3.lean.js" "b/assets/actions_tools_linux\350\256\276\347\275\256macOS\346\227\266\351\227\264\346\234\272\345\231\250server.md.3c94d4f3.lean.js" new file mode 100644 index 000000000..219207354 --- /dev/null +++ "b/assets/actions_tools_linux\350\256\276\347\275\256macOS\346\227\266\351\227\264\346\234\272\345\231\250server.md.3c94d4f3.lean.js" @@ -0,0 +1 @@ +import{_ as a,o as e,c as t,Q as n}from"./chunks/framework.dc1f4608.js";const _=JSON.parse('{"title":"linux设置macOS时间机器server","description":"","frontmatter":{},"headers":[],"relativePath":"actions/tools/linux设置macOS时间机器server.md","filePath":"actions/tools/linux设置macOS时间机器server.md","lastUpdated":1708510102000}'),s={name:"actions/tools/linux设置macOS时间机器server.md"},o=n("",15),c=[o];function i(l,r,d,p,h,m){return e(),t("div",null,c)}const k=a(s,[["render",i]]);export{_ as __pageData,k as default}; diff --git "a/assets/actions_tools_powershell\347\276\216\345\214\226.md.d9be26ce.js" "b/assets/actions_tools_powershell\347\276\216\345\214\226.md.d9be26ce.js" new file mode 100644 index 000000000..2d112fd85 --- /dev/null +++ "b/assets/actions_tools_powershell\347\276\216\345\214\226.md.d9be26ce.js" @@ -0,0 +1,19 @@ +import{_ as s,o,c as a,Q as n}from"./chunks/framework.dc1f4608.js";const F=JSON.parse('{"title":"powershell美化","description":"","frontmatter":{},"headers":[],"relativePath":"actions/tools/powershell美化.md","filePath":"actions/tools/powershell美化.md","lastUpdated":1708510102000}'),l={name:"actions/tools/powershell美化.md"},p=n(`

powershell美化

https://ohmyposh.dev/docs/

安装windows terminal和powershell

安装oh-my-posh

shell
Set-ExecutionPolicy Bypass -Scope Process -Force; Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://ohmyposh.dev/install.ps1'))
Set-ExecutionPolicy Bypass -Scope Process -Force; Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://ohmyposh.dev/install.ps1'))

安装Nerd Fonts

如果不安装Nerd Fonts会有乱码情况,oh-my-posh推荐安装Meslo LGM NF字体,也可以从https://www.nerdfonts.com/font-downloads自行选择下载。下载后解压放到C:\\windows\\Fonts文件夹中。编辑Windows Terminal默认设置将默认字体改为喜欢的Nerd Fonts。

编辑profile

code $PROFILEnotepad $PROFILE

shell
oh-my-posh init pwsh | Invoke-Expression # 默认主题
+
+oh-my-posh init pwsh --config C:\\Users\\story\\AppData\\Local\\Programs\\oh-my-posh\\themes\\robbyrussel.omp.json | Invoke-Expression # --config可以配置喜欢的主题
+
+--config 'https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/jandedobbeleer.omp.json' # 也可以配置远程主题
oh-my-posh init pwsh | Invoke-Expression # 默认主题
+
+oh-my-posh init pwsh --config C:\\Users\\story\\AppData\\Local\\Programs\\oh-my-posh\\themes\\robbyrussel.omp.json | Invoke-Expression # --config可以配置喜欢的主题
+
+--config 'https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/jandedobbeleer.omp.json' # 也可以配置远程主题
shell
# 设置预测文本来源为历史记录
+Set-PSReadLineOption -PredictionSource History
+# 设置向上键为后向搜索历史记录
+Set-PSReadLineKeyHandler -Key UpArrow -Function HistorySearchBackward
+# 设置向下键为前向搜索历史纪录
+Set-PSReadLineKeyHandler -Key DownArrow -Function HistorySearchForward
# 设置预测文本来源为历史记录
+Set-PSReadLineOption -PredictionSource History
+# 设置向上键为后向搜索历史记录
+Set-PSReadLineKeyHandler -Key UpArrow -Function HistorySearchBackward
+# 设置向下键为前向搜索历史纪录
+Set-PSReadLineKeyHandler -Key DownArrow -Function HistorySearchForward

windows安装后默认的主题文件夹为:C:\\Users\\[your username]\\AppData\\Local\\Programs\\oh-my-posh\\themes,也可以通过echo $env:POSH_THEMES_PATH命令查看主题的路径

挑选喜欢的主题

配置立即生效:

. $PROFILE
. $PROFILE

效果图:

`,19),e=[p];function t(r,c,y,i,E,h){return o(),a("div",null,e)}const m=s(l,[["render",t]]);export{F as __pageData,m as default}; diff --git "a/assets/actions_tools_powershell\347\276\216\345\214\226.md.d9be26ce.lean.js" "b/assets/actions_tools_powershell\347\276\216\345\214\226.md.d9be26ce.lean.js" new file mode 100644 index 000000000..a6a4afe4f --- /dev/null +++ "b/assets/actions_tools_powershell\347\276\216\345\214\226.md.d9be26ce.lean.js" @@ -0,0 +1 @@ +import{_ as s,o,c as a,Q as n}from"./chunks/framework.dc1f4608.js";const F=JSON.parse('{"title":"powershell美化","description":"","frontmatter":{},"headers":[],"relativePath":"actions/tools/powershell美化.md","filePath":"actions/tools/powershell美化.md","lastUpdated":1708510102000}'),l={name:"actions/tools/powershell美化.md"},p=n("",19),e=[p];function t(r,c,y,i,E,h){return o(),a("div",null,e)}const m=s(l,[["render",t]]);export{F as __pageData,m as default}; diff --git "a/assets/actions_tools_\345\220\204\347\263\273\347\273\237\344\270\213\346\240\241\351\252\214\346\226\207\344\273\266\344\270\200\350\207\264\346\200\247.md.98a2c898.js" "b/assets/actions_tools_\345\220\204\347\263\273\347\273\237\344\270\213\346\240\241\351\252\214\346\226\207\344\273\266\344\270\200\350\207\264\346\200\247.md.98a2c898.js" new file mode 100644 index 000000000..d593b1d3f --- /dev/null +++ "b/assets/actions_tools_\345\220\204\347\263\273\347\273\237\344\270\213\346\240\241\351\252\214\346\226\207\344\273\266\344\270\200\350\207\264\346\200\247.md.98a2c898.js" @@ -0,0 +1 @@ +import{_ as t,o as e,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const x=JSON.parse('{"title":"各系统下校验文件一致性","description":"","frontmatter":{},"headers":[],"relativePath":"actions/tools/各系统下校验文件一致性.md","filePath":"actions/tools/各系统下校验文件一致性.md","lastUpdated":1708510102000}'),s={name:"actions/tools/各系统下校验文件一致性.md"},d=l('

各系统下校验文件一致性

之前从网上下软件一直没有校验的习惯,直到从某知名mac破解网站上下了个被人恶意投毒的navicat,本文记录下各系统下校验的操作。内容引用自Apache Kafka官方文档。

校验哈希值

WindowsLinuxMac
SHA-1 (deprecated)certUtil -hashfile file SHA1sha1sum fileshasum -a 1 file
SHA-256certUtil -hashfile file SHA256sha256sum fileshasum -a 256 file
SHA-512certUtil -hashfile file SHA512sha512sum fileshasum -a 512 file
MD5 (deprecated)certUtil -hashfile file MD5md5sum filemd5 file
',4),i=[d];function n(r,c,m,o,h,_){return e(),a("div",null,i)}const p=t(s,[["render",n]]);export{x as __pageData,p as default}; diff --git "a/assets/actions_tools_\345\220\204\347\263\273\347\273\237\344\270\213\346\240\241\351\252\214\346\226\207\344\273\266\344\270\200\350\207\264\346\200\247.md.98a2c898.lean.js" "b/assets/actions_tools_\345\220\204\347\263\273\347\273\237\344\270\213\346\240\241\351\252\214\346\226\207\344\273\266\344\270\200\350\207\264\346\200\247.md.98a2c898.lean.js" new file mode 100644 index 000000000..7f11e383c --- /dev/null +++ "b/assets/actions_tools_\345\220\204\347\263\273\347\273\237\344\270\213\346\240\241\351\252\214\346\226\207\344\273\266\344\270\200\350\207\264\346\200\247.md.98a2c898.lean.js" @@ -0,0 +1 @@ +import{_ as t,o as e,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const x=JSON.parse('{"title":"各系统下校验文件一致性","description":"","frontmatter":{},"headers":[],"relativePath":"actions/tools/各系统下校验文件一致性.md","filePath":"actions/tools/各系统下校验文件一致性.md","lastUpdated":1708510102000}'),s={name:"actions/tools/各系统下校验文件一致性.md"},d=l("",4),i=[d];function n(r,c,m,o,h,_){return e(),a("div",null,i)}const p=t(s,[["render",n]]);export{x as __pageData,p as default}; diff --git "a/assets/actions_tools_\345\223\252\345\220\222\346\216\242\351\222\210\351\241\265\351\235\242\347\276\216\345\214\226.md.852dab4f.js" "b/assets/actions_tools_\345\223\252\345\220\222\346\216\242\351\222\210\351\241\265\351\235\242\347\276\216\345\214\226.md.852dab4f.js" new file mode 100644 index 000000000..4b515e15d --- /dev/null +++ "b/assets/actions_tools_\345\223\252\345\220\222\346\216\242\351\222\210\351\241\265\351\235\242\347\276\216\345\214\226.md.852dab4f.js" @@ -0,0 +1,1397 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"哪吒探针页面美化","description":"","frontmatter":{},"headers":[],"relativePath":"actions/tools/哪吒探针页面美化.md","filePath":"actions/tools/哪吒探针页面美化.md","lastUpdated":1708510102000}'),p={name:"actions/tools/哪吒探针页面美化.md"},o=l(`

哪吒探针页面美化

找到容器的文件系统目录

shell
docker inspect dashboard-dashboard-1 --format='{{.GraphDriver.Data.MergedDir}}'
docker inspect dashboard-dashboard-1 --format='{{.GraphDriver.Data.MergedDir}}'

替换template中的文件

重启容器

docker restart dashboard-dashboard-1

后台管理自定义CSS

html
<style>
+/* 屏幕适配 */
+@media only screen and (min-width: 1200px) {
+.ui.container {
+width: 80% !important;
+}
+}
+ 
+@media only screen and (max-width: 767px) {
+.ui.card>.content>.header:not(.ui), .ui.cards>.card>.content>.header:not(.ui) {
+    margin-top: 0.4em !important;
+}
+}
+ 
+/* 整体图标 */
+i.icon {
+color: #000;
+width: 1.2em !important;
+}
+ 
+/* 背景图片 */
+body {
+content: " " !important;
+background: fixed !important;
+z-index: -1 !important;
+top: 0 !important;
+right: 0 !important;
+bottom: 0 !important;
+left: 0 !important;
+background-position: top !important;
+background-repeat: no-repeat !important;
+background-size: cover !important;
+//background-image: url(https://api.kdcc.cn/img/) !important;
+font-family: Arial,Helvetica,sans-serif !important;
+}
+ 
+/* 导航栏 */
+.ui.large.menu {
+border: 0 !important;
+border-radius: 0px !important;
+background-color: rgba(255, 255, 255, 55%) !important;
+}
+ 
+/* 首页按钮 */
+.ui.menu .active.item {
+background-color: transparent !important;
+}
+ 
+/* 导航栏下拉框 */
+.ui.dropdown .menu {
+border: 0 !important;
+border-radius: 0 !important;
+background-color: rgba(255, 255, 255, 80%) !important;
+}
+ 
+/* 登陆按钮 */
+.nezha-primary-btn {
+background-color: transparent !important;
+color: #000 !important;
+}
+ 
+/* 大卡片 */
+#app .ui.fluid.accordion {
+background-color: #fbfbfb26 !important;
+border-radius: 0.4rem !important;
+}
+ 
+/* 小卡片 */
+.ui.four.cards>.card {
+border-radius: 0.6rem !important;
+background-color: #fafafaa3 !important;
+}
+ 
+.status.cards .wide.column {
+padding-top: 0 !important;
+padding-bottom: 0 !important;
+height: 3.3rem !important;
+}
+ 
+.status.cards .three.wide.column {
+padding-right: 0rem !important;
+}
+ 
+.status.cards .wide.column:nth-child(1) {
+margin-top: 2rem !important;
+}
+ 
+.status.cards .wide.column:nth-child(2) {
+margin-top: 2rem !important;
+}
+ 
+.status.cards .description {
+padding-bottom: 0 !important;
+}
+ 
+/* 小鸡名 */
+.status.cards .flag {
+margin-right: 0.5rem !important;
+}
+ 
+/* 弹出卡片图标 */
+.status.cards .header > .info.icon {
+margin-right: 0 !important;
+}
+ 
+.nezha-secondary-font {
+color: #21ba45 !important;
+}
+ 
+/* 进度条 */
+.ui.progress {
+border-radius: 50rem !important;
+}
+ 
+.ui.progress .bar {
+min-width: 1.8em !important;
+border-radius: 15px !important;
+line-height: 1.65em !important;
+}
+ 
+.ui.fine.progress> .bar {
+background-color: #21ba45 !important;
+}
+ 
+.ui.progress> .bar {
+background-color: #000 !important;
+}
+ 
+.ui.progress.fine .bar {
+background-color: #21ba45 !important;
+}
+ 
+.ui.progress.warning .bar {
+background-color: #ff9800 !important;
+}
+ 
+.ui.progress.error .bar {
+background-color: #e41e10 !important;
+}
+ 
+.ui.progress.offline .bar {
+background-color: #000 !important;
+}
+ 
+/* 上传下载 */
+.status.cards .outline.icon {
+margin-right: 1px !important;
+}
+ 
+ 
+i.arrow.alternate.circle.down.outline.icon
+{
+color: #21ba45 !important;
+}
+i.arrow.alternate.circle.up.outline.icon
+ 
+ 
+{
+color: red !important;
+}
+ 
+/* 弹出卡片小箭头 */
+.ui.right.center.popup {
+margin: -3px 0 0 0.914286em !important;
+-webkit-transform-origin: left 50% !important;
+transform-origin: left 50% !important;
+}
+ 
+.ui.bottom.left.popup {
+margin-left: 1px !important;
+margin-top: 3px !important;
+}
+ 
+.ui.top.left.popup {
+margin-left: 0 !important;
+margin-bottom: 10px !important;
+}
+ 
+.ui.top.right.popup {
+margin-right: 0 !important;
+margin-bottom: 8px !important;
+}
+ 
+.ui.left.center.popup {
+margin: -3px .91428571em 0 0 !important;
+-webkit-transform-origin: right 50% !important;
+transform-origin: right 50% !important;
+}
+ 
+.ui.right.center.popup:before,
+.ui.left.center.popup:before {
+border: 0px solid #fafafaeb !important;
+background: #fafafaeb !important;
+}
+ 
+.ui.top.popup:before {
+border-color: #fafafaeb transparent transparent !important;
+}
+ 
+.ui.popup:before {
+border-color: #fafafaeb transparent transparent !important;
+}
+ 
+.ui.bottom.left.popup:before {
+border-radius: 0 !important;
+border: 1px solid transparent !important;
+border-color: #fafafaeb transparent transparent !important;
+background: #fafafaeb !important;
+-webkit-box-shadow: 0px 0px 0 0 
+#fafafaeb !important;
+box-shadow: 0px 0px 0 0 #fafafaeb !important;
+-webkit-tap-highlight-color: rgba(0,0,0,0) !important;
+}
+ 
+.ui.bottom.right.popup:before {
+border-radius: 0 !important;
+border: 1px solid transparent !important;
+border-color: #fafafaeb transparent transparent !important;
+background: #fafafaeb !important
+-webkit-box-shadow: 0px 0px 0 0 #fafafaeb !important;
+box-shadow: 0px 0px 0 0 #fafafaeb !important;
+-webkit-tap-highlight-color: rgba(0,0,0,0) !important;
+}
+ 
+.ui.top.left.popup:before {
+border-radius: 0 !important;
+border: 1px solid transparent !important;
+border-color: #fafafaeb transparent transparent !important;
+background: #fafafaeb !important;
+-webkit-box-shadow: 0px 0px 0 0 #fafafaeb !important;
+box-shadow: 0px 0px 0 0 #fafafaeb !important;
+-webkit-tap-highlight-color: rgba(0,0,0,0) !important;
+}
+ 
+.ui.top.right.popup:before {
+border-radius: 0 !important;
+border: 1px solid transparent !important;
+border-color: #fafafaeb transparent transparent !important;
+background: #fafafaeb !important;
+-webkit-box-shadow: 0px 0px 0 0 #fafafaeb !important;
+box-shadow: 0px 0px 0 0 #fafafaeb !important;
+-webkit-tap-highlight-color: rgba(0,0,0,0) !important;
+}
+ 
+.ui.left.center.popup:before {
+border-radius: 0 !important;
+border: 1px solid transparent !important;
+border-color: #fafafaeb transparent transparent !important;
+background: #fafafaeb !important;
+-webkit-box-shadow: 0px 0px 0 0 #fafafaeb !important;
+box-shadow: 0px 0px 0 0 #fafafaeb !important;
+-webkit-tap-highlight-color: rgba(0,0,0,0) !important;
+}
+ 
+/* 弹出卡片 */
+.status.cards .ui.content.popup {
+min-width: 20rem !important;
+line-height: 2rem !important;
+border-radius: 5px !important;
+border: 1px solid transparent !important;
+background-color: #fafafaeb !important;
+font-family: Arial,Helvetica,sans-serif !important;
+}
+ 
+.ui.content {
+margin: 0 !important;
+padding: 1em !important;
+}
+ 
+/* 服务页 */
+.ui.table {
+background: RGB(225,225,225,0.6) !important;
+}
+ 
+.ui.table thead th {
+background: transparent !important;
+}
+ 
+/* 服务页进度条 */
+.service-status .good {
+background-color: #21ba45 !important;
+}
+ 
+.service-status .danger {
+background-color: red !important;
+}
+ 
+.service-status .warning {
+background-color: orange !important;
+}
+ 
+/* 版权 */
+.ui.inverted.segment, 
+.ui.primary.inverted.segment {
+color: #000 !important;
+font-weight: bold !important;
+background-color: #fafafaa3 !important;
+}
+</style>
+
+<script>
+window.onload = function(){
+var avatar=document.querySelector(".item img")
+var footer=document.querySelector("div.is-size-7")
+footer.innerHTML="Copyright © 2023 All Rights Reserved."
+footer.style.visibility="visible"
+avatar.src="/static/logo.svg"
+avatar.style.visibility="visible"
+}
+</script>
<style>
+/* 屏幕适配 */
+@media only screen and (min-width: 1200px) {
+.ui.container {
+width: 80% !important;
+}
+}
+ 
+@media only screen and (max-width: 767px) {
+.ui.card>.content>.header:not(.ui), .ui.cards>.card>.content>.header:not(.ui) {
+    margin-top: 0.4em !important;
+}
+}
+ 
+/* 整体图标 */
+i.icon {
+color: #000;
+width: 1.2em !important;
+}
+ 
+/* 背景图片 */
+body {
+content: " " !important;
+background: fixed !important;
+z-index: -1 !important;
+top: 0 !important;
+right: 0 !important;
+bottom: 0 !important;
+left: 0 !important;
+background-position: top !important;
+background-repeat: no-repeat !important;
+background-size: cover !important;
+//background-image: url(https://api.kdcc.cn/img/) !important;
+font-family: Arial,Helvetica,sans-serif !important;
+}
+ 
+/* 导航栏 */
+.ui.large.menu {
+border: 0 !important;
+border-radius: 0px !important;
+background-color: rgba(255, 255, 255, 55%) !important;
+}
+ 
+/* 首页按钮 */
+.ui.menu .active.item {
+background-color: transparent !important;
+}
+ 
+/* 导航栏下拉框 */
+.ui.dropdown .menu {
+border: 0 !important;
+border-radius: 0 !important;
+background-color: rgba(255, 255, 255, 80%) !important;
+}
+ 
+/* 登陆按钮 */
+.nezha-primary-btn {
+background-color: transparent !important;
+color: #000 !important;
+}
+ 
+/* 大卡片 */
+#app .ui.fluid.accordion {
+background-color: #fbfbfb26 !important;
+border-radius: 0.4rem !important;
+}
+ 
+/* 小卡片 */
+.ui.four.cards>.card {
+border-radius: 0.6rem !important;
+background-color: #fafafaa3 !important;
+}
+ 
+.status.cards .wide.column {
+padding-top: 0 !important;
+padding-bottom: 0 !important;
+height: 3.3rem !important;
+}
+ 
+.status.cards .three.wide.column {
+padding-right: 0rem !important;
+}
+ 
+.status.cards .wide.column:nth-child(1) {
+margin-top: 2rem !important;
+}
+ 
+.status.cards .wide.column:nth-child(2) {
+margin-top: 2rem !important;
+}
+ 
+.status.cards .description {
+padding-bottom: 0 !important;
+}
+ 
+/* 小鸡名 */
+.status.cards .flag {
+margin-right: 0.5rem !important;
+}
+ 
+/* 弹出卡片图标 */
+.status.cards .header > .info.icon {
+margin-right: 0 !important;
+}
+ 
+.nezha-secondary-font {
+color: #21ba45 !important;
+}
+ 
+/* 进度条 */
+.ui.progress {
+border-radius: 50rem !important;
+}
+ 
+.ui.progress .bar {
+min-width: 1.8em !important;
+border-radius: 15px !important;
+line-height: 1.65em !important;
+}
+ 
+.ui.fine.progress> .bar {
+background-color: #21ba45 !important;
+}
+ 
+.ui.progress> .bar {
+background-color: #000 !important;
+}
+ 
+.ui.progress.fine .bar {
+background-color: #21ba45 !important;
+}
+ 
+.ui.progress.warning .bar {
+background-color: #ff9800 !important;
+}
+ 
+.ui.progress.error .bar {
+background-color: #e41e10 !important;
+}
+ 
+.ui.progress.offline .bar {
+background-color: #000 !important;
+}
+ 
+/* 上传下载 */
+.status.cards .outline.icon {
+margin-right: 1px !important;
+}
+ 
+ 
+i.arrow.alternate.circle.down.outline.icon
+{
+color: #21ba45 !important;
+}
+i.arrow.alternate.circle.up.outline.icon
+ 
+ 
+{
+color: red !important;
+}
+ 
+/* 弹出卡片小箭头 */
+.ui.right.center.popup {
+margin: -3px 0 0 0.914286em !important;
+-webkit-transform-origin: left 50% !important;
+transform-origin: left 50% !important;
+}
+ 
+.ui.bottom.left.popup {
+margin-left: 1px !important;
+margin-top: 3px !important;
+}
+ 
+.ui.top.left.popup {
+margin-left: 0 !important;
+margin-bottom: 10px !important;
+}
+ 
+.ui.top.right.popup {
+margin-right: 0 !important;
+margin-bottom: 8px !important;
+}
+ 
+.ui.left.center.popup {
+margin: -3px .91428571em 0 0 !important;
+-webkit-transform-origin: right 50% !important;
+transform-origin: right 50% !important;
+}
+ 
+.ui.right.center.popup:before,
+.ui.left.center.popup:before {
+border: 0px solid #fafafaeb !important;
+background: #fafafaeb !important;
+}
+ 
+.ui.top.popup:before {
+border-color: #fafafaeb transparent transparent !important;
+}
+ 
+.ui.popup:before {
+border-color: #fafafaeb transparent transparent !important;
+}
+ 
+.ui.bottom.left.popup:before {
+border-radius: 0 !important;
+border: 1px solid transparent !important;
+border-color: #fafafaeb transparent transparent !important;
+background: #fafafaeb !important;
+-webkit-box-shadow: 0px 0px 0 0 
+#fafafaeb !important;
+box-shadow: 0px 0px 0 0 #fafafaeb !important;
+-webkit-tap-highlight-color: rgba(0,0,0,0) !important;
+}
+ 
+.ui.bottom.right.popup:before {
+border-radius: 0 !important;
+border: 1px solid transparent !important;
+border-color: #fafafaeb transparent transparent !important;
+background: #fafafaeb !important
+-webkit-box-shadow: 0px 0px 0 0 #fafafaeb !important;
+box-shadow: 0px 0px 0 0 #fafafaeb !important;
+-webkit-tap-highlight-color: rgba(0,0,0,0) !important;
+}
+ 
+.ui.top.left.popup:before {
+border-radius: 0 !important;
+border: 1px solid transparent !important;
+border-color: #fafafaeb transparent transparent !important;
+background: #fafafaeb !important;
+-webkit-box-shadow: 0px 0px 0 0 #fafafaeb !important;
+box-shadow: 0px 0px 0 0 #fafafaeb !important;
+-webkit-tap-highlight-color: rgba(0,0,0,0) !important;
+}
+ 
+.ui.top.right.popup:before {
+border-radius: 0 !important;
+border: 1px solid transparent !important;
+border-color: #fafafaeb transparent transparent !important;
+background: #fafafaeb !important;
+-webkit-box-shadow: 0px 0px 0 0 #fafafaeb !important;
+box-shadow: 0px 0px 0 0 #fafafaeb !important;
+-webkit-tap-highlight-color: rgba(0,0,0,0) !important;
+}
+ 
+.ui.left.center.popup:before {
+border-radius: 0 !important;
+border: 1px solid transparent !important;
+border-color: #fafafaeb transparent transparent !important;
+background: #fafafaeb !important;
+-webkit-box-shadow: 0px 0px 0 0 #fafafaeb !important;
+box-shadow: 0px 0px 0 0 #fafafaeb !important;
+-webkit-tap-highlight-color: rgba(0,0,0,0) !important;
+}
+ 
+/* 弹出卡片 */
+.status.cards .ui.content.popup {
+min-width: 20rem !important;
+line-height: 2rem !important;
+border-radius: 5px !important;
+border: 1px solid transparent !important;
+background-color: #fafafaeb !important;
+font-family: Arial,Helvetica,sans-serif !important;
+}
+ 
+.ui.content {
+margin: 0 !important;
+padding: 1em !important;
+}
+ 
+/* 服务页 */
+.ui.table {
+background: RGB(225,225,225,0.6) !important;
+}
+ 
+.ui.table thead th {
+background: transparent !important;
+}
+ 
+/* 服务页进度条 */
+.service-status .good {
+background-color: #21ba45 !important;
+}
+ 
+.service-status .danger {
+background-color: red !important;
+}
+ 
+.service-status .warning {
+background-color: orange !important;
+}
+ 
+/* 版权 */
+.ui.inverted.segment, 
+.ui.primary.inverted.segment {
+color: #000 !important;
+font-weight: bold !important;
+background-color: #fafafaa3 !important;
+}
+</style>
+
+<script>
+window.onload = function(){
+var avatar=document.querySelector(".item img")
+var footer=document.querySelector("div.is-size-7")
+footer.innerHTML="Copyright © 2023 All Rights Reserved."
+footer.style.visibility="visible"
+avatar.src="/static/logo.svg"
+avatar.style.visibility="visible"
+}
+</script>
`,9),t=[o];function e(c,r,E,y,i,F){return n(),a("div",null,t)}const B=s(p,[["render",e]]);export{u as __pageData,B as default}; diff --git "a/assets/actions_tools_\345\223\252\345\220\222\346\216\242\351\222\210\351\241\265\351\235\242\347\276\216\345\214\226.md.852dab4f.lean.js" "b/assets/actions_tools_\345\223\252\345\220\222\346\216\242\351\222\210\351\241\265\351\235\242\347\276\216\345\214\226.md.852dab4f.lean.js" new file mode 100644 index 000000000..5516cbcf5 --- /dev/null +++ "b/assets/actions_tools_\345\223\252\345\220\222\346\216\242\351\222\210\351\241\265\351\235\242\347\276\216\345\214\226.md.852dab4f.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"哪吒探针页面美化","description":"","frontmatter":{},"headers":[],"relativePath":"actions/tools/哪吒探针页面美化.md","filePath":"actions/tools/哪吒探针页面美化.md","lastUpdated":1708510102000}'),p={name:"actions/tools/哪吒探针页面美化.md"},o=l("",9),t=[o];function e(c,r,E,y,i,F){return n(),a("div",null,t)}const B=s(p,[["render",e]]);export{u as __pageData,B as default}; diff --git a/assets/app.058f2afe.js b/assets/app.058f2afe.js new file mode 100644 index 000000000..c4e49afe9 --- /dev/null +++ b/assets/app.058f2afe.js @@ -0,0 +1 @@ +import{s as o,a0 as p,a1 as i,a2 as u,a3 as c,a4 as l,a5 as d,a6 as f,a7 as m,a8 as h,a9 as A,U as g,d as P,u as v,j as y,y as C,aa as w,ab as _,ac as b,ad as E}from"./chunks/framework.dc1f4608.js";import{t as R}from"./chunks/theme.bfdc69c3.js";function r(e){if(e.extends){const a=r(e.extends);return{...a,...e,async enhanceApp(t){a.enhanceApp&&await a.enhanceApp(t),e.enhanceApp&&await e.enhanceApp(t)}}}return e}const s=r(R),D=P({name:"VitePressApp",setup(){const{site:e}=v();return y(()=>{C(()=>{document.documentElement.lang=e.value.lang,document.documentElement.dir=e.value.dir})}),w(),_(),b(),s.setup&&s.setup(),()=>E(s.Layout)}});async function j(){const e=S(),a=O();a.provide(i,e);const t=u(e.route);return a.provide(c,t),a.component("Content",l),a.component("ClientOnly",d),Object.defineProperties(a.config.globalProperties,{$frontmatter:{get(){return t.frontmatter.value}},$params:{get(){return t.page.value.params}}}),s.enhanceApp&&await s.enhanceApp({app:a,router:e,siteData:f}),{app:a,router:e,data:t}}function O(){return m(D)}function S(){let e=o,a;return h(t=>{let n=A(t);return n?(e&&(a=n),(e||a===n)&&(n=n.replace(/\.js$/,".lean.js")),o&&(e=!1),g(()=>import(n),[])):null},s.NotFound)}o&&j().then(({app:e,router:a,data:t})=>{a.go().then(()=>{p(a.route,t.site),e.mount("#app")})});export{j as createApp}; diff --git a/assets/chunks/VPAlgoliaSearchBox.bbe9601b.js b/assets/chunks/VPAlgoliaSearchBox.bbe9601b.js new file mode 100644 index 000000000..010368c6b --- /dev/null +++ b/assets/chunks/VPAlgoliaSearchBox.bbe9601b.js @@ -0,0 +1,17 @@ +import{d as so,ae as fo,K as mo,j as po,x as vo,o as ho,c as yo}from"./framework.dc1f4608.js";import{u as go}from"./theme.bfdc69c3.js";/*! @docsearch/js 3.5.2 | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */function un(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(o){return Object.getOwnPropertyDescriptor(t,o).enumerable})),n.push.apply(n,r)}return n}function I(t){for(var e=1;e=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}function se(t,e){return function(n){if(Array.isArray(n))return n}(t)||function(n,r){var o=n==null?null:typeof Symbol<"u"&&n[Symbol.iterator]||n["@@iterator"];if(o!=null){var i,a,u=[],c=!0,s=!1;try{for(o=o.call(n);!(c=(i=o.next()).done)&&(u.push(i.value),!r||u.length!==r);c=!0);}catch(l){s=!0,a=l}finally{try{c||o.return==null||o.return()}finally{if(s)throw a}}return u}}(t,e)||yr(t,e)||function(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}()}function ft(t){return function(e){if(Array.isArray(e))return Lt(e)}(t)||function(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}(t)||yr(t)||function(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}()}function yr(t,e){if(t){if(typeof t=="string")return Lt(t,e);var n=Object.prototype.toString.call(t).slice(8,-1);return n==="Object"&&t.constructor&&(n=t.constructor.name),n==="Map"||n==="Set"?Array.from(t):n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?Lt(t,e):void 0}}function Lt(t,e){(e==null||e>t.length)&&(e=t.length);for(var n=0,r=new Array(e);n3)for(n=[n],i=3;i0?Ie(v.type,v.props,v.key,null,v.__v):v)!=null){if(v.__=n,v.__b=n.__b+1,(p=b[l])===null||p&&v.key==p.key&&v.type===p.type)b[l]=void 0;else for(m=0;m<_;m++){if((p=b[m])&&v.key==p.key&&v.type===p.type){b[m]=void 0;break}p=null}Yt(t,v,p=p||mt,o,i,a,u,c,s),d=v.__e,(m=v.ref)&&p.ref!=m&&(y||(y=[]),p.ref&&y.push(p.ref,null,v),y.push(m,v.__c||d,v)),d!=null?(h==null&&(h=d),typeof v.type=="function"&&v.__k!=null&&v.__k===p.__k?v.__d=c=wr(v,c,t):c=jr(t,v,p,b,d,c),s||n.type!=="option"?typeof n.type=="function"&&(n.__d=c):t.value=""):c&&p.__e==c&&c.parentNode!=t&&(c=We(p))}for(n.__e=h,l=_;l--;)b[l]!=null&&(typeof n.type=="function"&&b[l].__e!=null&&b[l].__e==n.__d&&(n.__d=We(r,l+1)),Ir(b[l],b[l]));if(y)for(l=0;l3)for(n=[n],i=3;i=n.__.length&&n.__.push({}),n.__[t]}function kr(t){return pe=1,Ar(xr,t)}function Ar(t,e,n){var r=Je(de++,2);return r.t=t,r.__c||(r.__=[n?n(e):xr(void 0,e),function(o){var i=r.t(r.__[0],o);r.__[0]!==i&&(r.__=[i,r.__[1]],r.__c.setState({}))}],r.__c=q),r.__}function Cr(t,e){var n=Je(de++,3);!w.__s&&Gt(n.__H,e)&&(n.__=t,n.__H=e,q.__H.__h.push(n))}function bn(t,e){var n=Je(de++,4);!w.__s&&Gt(n.__H,e)&&(n.__=t,n.__H=e,q.__h.push(n))}function Pt(t,e){var n=Je(de++,7);return Gt(n.__H,e)&&(n.__=t(),n.__H=e,n.__h=t),n.__}function Eo(){Ht.forEach(function(t){if(t.__P)try{t.__H.__h.forEach(ct),t.__H.__h.forEach(Ut),t.__H.__h=[]}catch(e){t.__H.__h=[],w.__e(e,t.__v)}}),Ht=[]}w.__b=function(t){q=null,vn&&vn(t)},w.__r=function(t){dn&&dn(t),de=0;var e=(q=t.__c).__H;e&&(e.__h.forEach(ct),e.__h.forEach(Ut),e.__h=[])},w.diffed=function(t){hn&&hn(t);var e=t.__c;e&&e.__H&&e.__H.__h.length&&(Ht.push(e)!==1&&pn===w.requestAnimationFrame||((pn=w.requestAnimationFrame)||function(n){var r,o=function(){clearTimeout(i),_n&&cancelAnimationFrame(r),setTimeout(n)},i=setTimeout(o,100);_n&&(r=requestAnimationFrame(o))})(Eo)),q=void 0},w.__c=function(t,e){e.some(function(n){try{n.__h.forEach(ct),n.__h=n.__h.filter(function(r){return!r.__||Ut(r)})}catch(r){e.some(function(o){o.__h&&(o.__h=[])}),e=[],w.__e(r,n.__v)}}),yn&&yn(t,e)},w.unmount=function(t){gn&&gn(t);var e=t.__c;if(e&&e.__H)try{e.__H.__.forEach(ct)}catch(n){w.__e(n,e.__v)}};var _n=typeof requestAnimationFrame=="function";function ct(t){var e=q;typeof t.__c=="function"&&t.__c(),q=e}function Ut(t){var e=q;t.__c=t.__(),q=e}function Gt(t,e){return!t||t.length!==e.length||e.some(function(n,r){return n!==t[r]})}function xr(t,e){return typeof e=="function"?e(t):e}function Nr(t,e){for(var n in e)t[n]=e[n];return t}function Ft(t,e){for(var n in t)if(n!=="__source"&&!(n in e))return!0;for(var r in e)if(r!=="__source"&&t[r]!==e[r])return!0;return!1}function Bt(t){this.props=t}(Bt.prototype=new K).isPureReactComponent=!0,Bt.prototype.shouldComponentUpdate=function(t,e){return Ft(this.props,t)||Ft(this.state,e)};var On=w.__b;w.__b=function(t){t.type&&t.type.__f&&t.ref&&(t.props.ref=t.ref,t.ref=null),On&&On(t)};var Po=typeof Symbol<"u"&&Symbol.for&&Symbol.for("react.forward_ref")||3911,Sn=function(t,e){return t==null?null:$($(t).map(e))},Io={map:Sn,forEach:Sn,count:function(t){return t?$(t).length:0},only:function(t){var e=$(t);if(e.length!==1)throw"Children.only";return e[0]},toArray:$},Do=w.__e;function ut(){this.__u=0,this.t=null,this.__b=null}function Tr(t){var e=t.__.__c;return e&&e.__e&&e.__e(t)}function je(){this.u=null,this.o=null}w.__e=function(t,e,n){if(t.then){for(var r,o=e;o=o.__;)if((r=o.__c)&&r.__c)return e.__e==null&&(e.__e=n.__e,e.__k=n.__k),r.__c(t,e)}Do(t,e,n)},(ut.prototype=new K).__c=function(t,e){var n=e.__c,r=this;r.t==null&&(r.t=[]),r.t.push(n);var o=Tr(r.__v),i=!1,a=function(){i||(i=!0,n.componentWillUnmount=n.__c,o?o(u):u())};n.__c=n.componentWillUnmount,n.componentWillUnmount=function(){a(),n.__c&&n.__c()};var u=function(){if(!--r.__u){if(r.state.__e){var s=r.state.__e;r.__v.__k[0]=function m(p,v,d){return p&&(p.__v=null,p.__k=p.__k&&p.__k.map(function(h){return m(h,v,d)}),p.__c&&p.__c.__P===v&&(p.__e&&d.insertBefore(p.__e,p.__d),p.__c.__e=!0,p.__c.__P=d)),p}(s,s.__c.__P,s.__c.__O)}var l;for(r.setState({__e:r.__b=null});l=r.t.pop();)l.forceUpdate()}},c=e.__h===!0;r.__u++||c||r.setState({__e:r.__b=r.__v.__k[0]}),t.then(a,a)},ut.prototype.componentWillUnmount=function(){this.t=[]},ut.prototype.render=function(t,e){if(this.__b){if(this.__v.__k){var n=document.createElement("div"),r=this.__v.__k[0].__c;this.__v.__k[0]=function i(a,u,c){return a&&(a.__c&&a.__c.__H&&(a.__c.__H.__.forEach(function(s){typeof s.__c=="function"&&s.__c()}),a.__c.__H=null),(a=Nr({},a)).__c!=null&&(a.__c.__P===c&&(a.__c.__P=u),a.__c=null),a.__k=a.__k&&a.__k.map(function(s){return i(s,u,c)})),a}(this.__b,n,r.__O=r.__P)}this.__b=null}var o=e.__e&&W(X,null,t.fallback);return o&&(o.__h=null),[W(X,null,e.__e?null:t.children),o]};var wn=function(t,e,n){if(++n[1]===n[0]&&t.o.delete(e),t.props.revealOrder&&(t.props.revealOrder[0]!=="t"||!t.o.size))for(n=t.u;n;){for(;n.length>3;)n.pop()();if(n[1]>>1,1),e.i.removeChild(r)}}),Ke(W(ko,{context:e.context},t.__v),e.l)):e.l&&e.componentWillUnmount()}function Rr(t,e){return W(Ao,{__v:t,i:e})}(je.prototype=new K).__e=function(t){var e=this,n=Tr(e.__v),r=e.o.get(t);return r[0]++,function(o){var i=function(){e.props.revealOrder?(r.push(o),wn(e,t,r)):o()};n?n(i):i()}},je.prototype.render=function(t){this.u=null,this.o=new Map;var e=$(t.children);t.revealOrder&&t.revealOrder[0]==="b"&&e.reverse();for(var n=e.length;n--;)this.o.set(e[n],this.u=[1,0,this.u]);return t.children},je.prototype.componentDidUpdate=je.prototype.componentDidMount=function(){var t=this;this.o.forEach(function(e,n){wn(t,n,e)})};var qr=typeof Symbol<"u"&&Symbol.for&&Symbol.for("react.element")||60103,Co=/^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|fill|flood|font|glyph(?!R)|horiz|marker(?!H|W|U)|overline|paint|stop|strikethrough|stroke|text(?!L)|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/,xo=function(t){return(typeof Symbol<"u"&&Ve(Symbol())=="symbol"?/fil|che|rad/i:/fil|che|ra/i).test(t)};function Lr(t,e,n){return e.__k==null&&(e.textContent=""),Ke(t,e),typeof n=="function"&&n(),t?t.__c:null}K.prototype.isReactComponent={},["componentWillMount","componentWillReceiveProps","componentWillUpdate"].forEach(function(t){Object.defineProperty(K.prototype,t,{configurable:!0,get:function(){return this["UNSAFE_"+t]},set:function(e){Object.defineProperty(this,t,{configurable:!0,writable:!0,value:e})}})});var jn=w.event;function No(){}function To(){return this.cancelBubble}function Ro(){return this.defaultPrevented}w.event=function(t){return jn&&(t=jn(t)),t.persist=No,t.isPropagationStopped=To,t.isDefaultPrevented=Ro,t.nativeEvent=t};var Mr,En={configurable:!0,get:function(){return this.class}},Pn=w.vnode;w.vnode=function(t){var e=t.type,n=t.props,r=n;if(typeof e=="string"){for(var o in r={},n){var i=n[o];o==="value"&&"defaultValue"in n&&i==null||(o==="defaultValue"&&"value"in n&&n.value==null?o="value":o==="download"&&i===!0?i="":/ondoubleclick/i.test(o)?o="ondblclick":/^onchange(textarea|input)/i.test(o+e)&&!xo(n.type)?o="oninput":/^on(Ani|Tra|Tou|BeforeInp)/.test(o)?o=o.toLowerCase():Co.test(o)?o=o.replace(/[A-Z0-9]/,"-$&").toLowerCase():i===null&&(i=void 0),r[o]=i)}e=="select"&&r.multiple&&Array.isArray(r.value)&&(r.value=$(n.children).forEach(function(a){a.props.selected=r.value.indexOf(a.props.value)!=-1})),e=="select"&&r.defaultValue!=null&&(r.value=$(n.children).forEach(function(a){a.props.selected=r.multiple?r.defaultValue.indexOf(a.props.value)!=-1:r.defaultValue==a.props.value})),t.props=r}e&&n.class!=n.className&&(En.enumerable="className"in n,n.className!=null&&(r.class=n.className),Object.defineProperty(r,"className",En)),t.$$typeof=qr,Pn&&Pn(t)};var In=w.__r;w.__r=function(t){In&&In(t),Mr=t.__c};var qo={ReactCurrentDispatcher:{current:{readContext:function(t){return Mr.__n[t.__c].props.value}}}};(typeof performance>"u"?"undefined":Ve(performance))=="object"&&typeof performance.now=="function"&&performance.now.bind(performance);function Dn(t){return!!t&&t.$$typeof===qr}var f={useState:kr,useReducer:Ar,useEffect:Cr,useLayoutEffect:bn,useRef:function(t){return pe=5,Pt(function(){return{current:t}},[])},useImperativeHandle:function(t,e,n){pe=6,bn(function(){typeof t=="function"?t(e()):t&&(t.current=e())},n==null?n:n.concat(t))},useMemo:Pt,useCallback:function(t,e){return pe=8,Pt(function(){return t},e)},useContext:function(t){var e=q.context[t.__c],n=Je(de++,9);return n.__c=t,e?(n.__==null&&(n.__=!0,e.sub(q)),e.props.value):t.__},useDebugValue:function(t,e){w.useDebugValue&&w.useDebugValue(e?e(t):t)},version:"16.8.0",Children:Io,render:Lr,hydrate:function(t,e,n){return Dr(t,e),typeof n=="function"&&n(),t?t.__c:null},unmountComponentAtNode:function(t){return!!t.__k&&(Ke(null,t),!0)},createPortal:Rr,createElement:W,createContext:function(t,e){var n={__c:e="__cC"+br++,__:t,Consumer:function(r,o){return r.children(o)},Provider:function(r){var o,i;return this.getChildContext||(o=[],(i={})[e]=this,this.getChildContext=function(){return i},this.shouldComponentUpdate=function(a){this.props.value!==a.value&&o.some(Mt)},this.sub=function(a){o.push(a);var u=a.componentWillUnmount;a.componentWillUnmount=function(){o.splice(o.indexOf(a),1),u&&u.call(a)}}),r.children}};return n.Provider.__=n.Consumer.contextType=n},createFactory:function(t){return W.bind(null,t)},cloneElement:function(t){return Dn(t)?jo.apply(null,arguments):t},createRef:function(){return{current:null}},Fragment:X,isValidElement:Dn,findDOMNode:function(t){return t&&(t.base||t.nodeType===1&&t)||null},Component:K,PureComponent:Bt,memo:function(t,e){function n(o){var i=this.props.ref,a=i==o.ref;return!a&&i&&(i.call?i(null):i.current=null),e?!e(this.props,o)||!a:Ft(this.props,o)}function r(o){return this.shouldComponentUpdate=n,W(t,o)}return r.displayName="Memo("+(t.displayName||t.name)+")",r.prototype.isReactComponent=!0,r.__f=!0,r},forwardRef:function(t){function e(n,r){var o=Nr({},n);return delete o.ref,t(o,(r=n.ref||r)&&(Ve(r)!="object"||"current"in r)?r:null)}return e.$$typeof=Po,e.render=e,e.prototype.isReactComponent=e.__f=!0,e.displayName="ForwardRef("+(t.displayName||t.name)+")",e},unstable_batchedUpdates:function(t,e){return t(e)},StrictMode:X,Suspense:ut,SuspenseList:je,lazy:function(t){var e,n,r;function o(i){if(e||(e=t()).then(function(a){n=a.default||a},function(a){r=a}),r)throw r;if(!n)throw e;return W(n,i)}return o.displayName="Lazy",o.__f=!0,o},__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED:qo};function Lo(){return f.createElement("svg",{width:"15",height:"15",className:"DocSearch-Control-Key-Icon"},f.createElement("path",{d:"M4.505 4.496h2M5.505 5.496v5M8.216 4.496l.055 5.993M10 7.5c.333.333.5.667.5 1v2M12.326 4.5v5.996M8.384 4.496c1.674 0 2.116 0 2.116 1.5s-.442 1.5-2.116 1.5M3.205 9.303c-.09.448-.277 1.21-1.241 1.203C1 10.5.5 9.513.5 8V7c0-1.57.5-2.5 1.464-2.494.964.006 1.134.598 1.24 1.342M12.553 10.5h1.953",strokeWidth:"1.2",stroke:"currentColor",fill:"none",strokeLinecap:"square"}))}function Hr(){return f.createElement("svg",{width:"20",height:"20",className:"DocSearch-Search-Icon",viewBox:"0 0 20 20"},f.createElement("path",{d:"M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z",stroke:"currentColor",fill:"none",fillRule:"evenodd",strokeLinecap:"round",strokeLinejoin:"round"}))}var Mo=["translations"];function Vt(){return Vt=Object.assign||function(t){for(var e=1;et.length)&&(e=t.length);for(var n=0,r=new Array(e);n=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}var Fo=f.forwardRef(function(t,e){var n=t.translations,r=n===void 0?{}:n,o=Uo(t,Mo),i=r.buttonText,a=i===void 0?"Search":i,u=r.buttonAriaLabel,c=u===void 0?"Search":u,s=Ho(kr(null),2),l=s[0],m=s[1];return Cr(function(){typeof navigator<"u"&&(/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)?m("⌘"):m("Ctrl"))},[]),f.createElement("button",Vt({type:"button",className:"DocSearch DocSearch-Button","aria-label":c},o,{ref:e}),f.createElement("span",{className:"DocSearch-Button-Container"},f.createElement(Hr,null),f.createElement("span",{className:"DocSearch-Button-Placeholder"},a)),f.createElement("span",{className:"DocSearch-Button-Keys"},l!==null&&f.createElement(f.Fragment,null,f.createElement("kbd",{className:"DocSearch-Button-Key"},l==="Ctrl"?f.createElement(Lo,null):l),f.createElement("kbd",{className:"DocSearch-Button-Key"},"K"))))});function Ur(t,e){var n=void 0;return function(){for(var r=arguments.length,o=new Array(r),i=0;it.length)&&(e=t.length);for(var n=0,r=new Array(e);nt.length)&&(e=t.length);for(var n=0,r=new Array(e);n=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}function Nn(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(o){return Object.getOwnPropertyDescriptor(t,o).enumerable})),n.push.apply(n,r)}return n}function ve(t){for(var e=1;e1&&arguments[1]!==void 0?arguments[1]:20,n=[],r=0;r=3||n===2&&r>=4||n===1&&r>=10);function i(a,u,c){if(o&&c!==void 0){var s=c[0].__autocomplete_algoliaCredentials,l={"X-Algolia-Application-Id":s.appId,"X-Algolia-API-Key":s.apiKey};t.apply(void 0,[a].concat(Ge(u),[{headers:l}]))}else t.apply(void 0,[a].concat(Ge(u)))}return{init:function(a,u){t("init",{appId:a,apiKey:u})},setUserToken:function(a){t("setUserToken",a)},clickedObjectIDsAfterSearch:function(){for(var a=arguments.length,u=new Array(a),c=0;c0&&i("clickedObjectIDsAfterSearch",Xe(u),u[0].items)},clickedObjectIDs:function(){for(var a=arguments.length,u=new Array(a),c=0;c0&&i("clickedObjectIDs",Xe(u),u[0].items)},clickedFilters:function(){for(var a=arguments.length,u=new Array(a),c=0;c0&&t.apply(void 0,["clickedFilters"].concat(u))},convertedObjectIDsAfterSearch:function(){for(var a=arguments.length,u=new Array(a),c=0;c0&&i("convertedObjectIDsAfterSearch",Xe(u),u[0].items)},convertedObjectIDs:function(){for(var a=arguments.length,u=new Array(a),c=0;c0&&i("convertedObjectIDs",Xe(u),u[0].items)},convertedFilters:function(){for(var a=arguments.length,u=new Array(a),c=0;c0&&t.apply(void 0,["convertedFilters"].concat(u))},viewedObjectIDs:function(){for(var a=arguments.length,u=new Array(a),c=0;c0&&u.reduce(function(s,l){var m=l.items,p=Br(l,zo);return[].concat(Ge(s),Ge($o(ve(ve({},p),{},{objectIDs:(m==null?void 0:m.map(function(v){return v.objectID}))||p.objectIDs})).map(function(v){return{items:m,payload:v}})))},[]).forEach(function(s){var l=s.items;return i("viewedObjectIDs",[s.payload],l)})},viewedFilters:function(){for(var a=arguments.length,u=new Array(a),c=0;c0&&t.apply(void 0,["viewedFilters"].concat(u))}}}function Zo(t){var e=t.items.reduce(function(n,r){var o;return n[r.__autocomplete_indexName]=((o=n[r.__autocomplete_indexName])!==null&&o!==void 0?o:[]).concat(r),n},{});return Object.keys(e).map(function(n){return{index:n,items:e[n],algoliaSource:["autocomplete"]}})}function Dt(t){return t.objectID&&t.__autocomplete_indexName&&t.__autocomplete_queryID}function ke(t){return ke=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},ke(t)}function ie(t){return function(e){if(Array.isArray(e))return kt(e)}(t)||function(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}(t)||function(e,n){if(e){if(typeof e=="string")return kt(e,n);var r=Object.prototype.toString.call(e).slice(8,-1);if(r==="Object"&&e.constructor&&(r=e.constructor.name),r==="Map"||r==="Set")return Array.from(e);if(r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return kt(e,n)}}(t)||function(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}()}function kt(t,e){(e==null||e>t.length)&&(e=t.length);for(var n=0,r=new Array(e);n0&&Xo({onItemsChange:r,items:p,insights:u,state:m}))}},0);return{name:"aa.algoliaInsightsPlugin",subscribe:function(l){var m=l.setContext,p=l.onSelect,v=l.onActive;a("addAlgoliaAgent","insights-plugin"),m({algoliaInsightsPlugin:{__algoliaSearchParameters:{clickAnalytics:!0},insights:u}}),p(function(d){var h=d.item,y=d.state,b=d.event;Dt(h)&&o({state:y,event:b,insights:u,item:h,insightsEvents:[G({eventName:"Item Selected"},Cn({item:h,items:c.current}))]})}),v(function(d){var h=d.item,y=d.state,b=d.event;Dt(h)&&i({state:y,event:b,insights:u,item:h,insightsEvents:[G({eventName:"Item Active"},Cn({item:h,items:c.current}))]})})},onStateChange:function(l){var m=l.state;s({state:m})},__autocomplete_pluginOptions:t}}function lt(t,e){var n=e;return{then:function(r,o){return lt(t.then(et(r,n,t),et(o,n,t)),n)},catch:function(r){return lt(t.catch(et(r,n,t)),n)},finally:function(r){return r&&n.onCancelList.push(r),lt(t.finally(et(r&&function(){return n.onCancelList=[],r()},n,t)),n)},cancel:function(){n.isCanceled=!0;var r=n.onCancelList;n.onCancelList=[],r.forEach(function(o){o()})},isCanceled:function(){return n.isCanceled===!0}}}function Rn(t){return lt(t,{isCanceled:!1,onCancelList:[]})}function et(t,e,n){return t?function(r){return e.isCanceled?r:t(r)}:n}function qn(t,e,n,r){if(!n)return null;if(t<0&&(e===null||r!==null&&e===0))return n+t;var o=(e===null?-1:e)+t;return o<=-1||o>=n?r===null?null:0:o}function Ln(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(o){return Object.getOwnPropertyDescriptor(t,o).enumerable})),n.push.apply(n,r)}return n}function Mn(t){for(var e=1;et.length)&&(e=t.length);for(var n=0,r=new Array(e);n0},reshape:function(i){return i.sources}},t),{},{id:(n=t.id)!==null&&n!==void 0?n:"autocomplete-".concat(Bo++),plugins:o,initialState:ae({activeItemId:null,query:"",completion:null,collections:[],isOpen:!1,status:"idle",context:{}},t.initialState),onStateChange:function(i){var a;(a=t.onStateChange)===null||a===void 0||a.call(t,i),o.forEach(function(u){var c;return(c=u.onStateChange)===null||c===void 0?void 0:c.call(u,i)})},onSubmit:function(i){var a;(a=t.onSubmit)===null||a===void 0||a.call(t,i),o.forEach(function(u){var c;return(c=u.onSubmit)===null||c===void 0?void 0:c.call(u,i)})},onReset:function(i){var a;(a=t.onReset)===null||a===void 0||a.call(t,i),o.forEach(function(u){var c;return(c=u.onReset)===null||c===void 0?void 0:c.call(u,i)})},getSources:function(i){return Promise.all([].concat(ai(o.map(function(a){return a.getSources})),[t.getSources]).filter(Boolean).map(function(a){return function(u,c){var s=[];return Promise.resolve(u(c)).then(function(l){return Promise.all(l.filter(function(m){return!!m}).map(function(m){if(m.sourceId,s.includes(m.sourceId))throw new Error("[Autocomplete] The `sourceId` ".concat(JSON.stringify(m.sourceId)," is not unique."));s.push(m.sourceId);var p={getItemInputValue:function(d){return d.state.query},getItemUrl:function(){},onSelect:function(d){(0,d.setIsOpen)(!1)},onActive:vt,onResolve:vt};Object.keys(p).forEach(function(d){p[d].__default=!0});var v=Mn(Mn({},p),m);return Promise.resolve(v)}))})}(a,i)})).then(function(a){return ze(a)}).then(function(a){return a.map(function(u){return ae(ae({},u),{},{onSelect:function(c){u.onSelect(c),e.forEach(function(s){var l;return(l=s.onSelect)===null||l===void 0?void 0:l.call(s,c)})},onActive:function(c){u.onActive(c),e.forEach(function(s){var l;return(l=s.onActive)===null||l===void 0?void 0:l.call(s,c)})},onResolve:function(c){u.onResolve(c),e.forEach(function(s){var l;return(l=s.onResolve)===null||l===void 0?void 0:l.call(s,c)})}})})})},navigator:ae({navigate:function(i){var a=i.itemUrl;r.location.assign(a)},navigateNewTab:function(i){var a=i.itemUrl,u=r.open(a,"_blank","noopener");u==null||u.focus()},navigateNewWindow:function(i){var a=i.itemUrl;r.open(a,"_blank","noopener")}},t.navigator)})}function Te(t){return Te=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Te(t)}function Bn(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(o){return Object.getOwnPropertyDescriptor(t,o).enumerable})),n.push.apply(n,r)}return n}function nt(t){for(var e=1;et.length)&&(e=t.length);for(var n=0,r=new Array(e);n=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}var Kn,xt,ot,we=null,zn=(Kn=-1,xt=-1,ot=void 0,function(t){var e=++Kn;return Promise.resolve(t).then(function(n){return ot&&e=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}function Me(t){return Me=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Me(t)}var bi=["props","refresh","store"],_i=["inputElement","formElement","panelElement"],Oi=["inputElement"],Si=["inputElement","maxLength"],wi=["sourceIndex"],ji=["sourceIndex"],Ei=["item","source","sourceIndex"];function $n(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(o){return Object.getOwnPropertyDescriptor(t,o).enumerable})),n.push.apply(n,r)}return n}function R(t){for(var e=1;e=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}function Ii(t){var e=t.props,n=t.refresh,r=t.store,o=ne(t,bi),i=function(a,u){return u!==void 0?"".concat(a,"-").concat(u):a};return{getEnvironmentProps:function(a){var u=a.inputElement,c=a.formElement,s=a.panelElement;function l(m){!r.getState().isOpen&&r.pendingRequests.isEmpty()||m.target===u||[c,s].some(function(p){return v=p,d=m.target,v===d||v.contains(d);var v,d})===!1&&(r.dispatch("blur",null),e.debug||r.pendingRequests.cancelAll())}return R({onTouchStart:l,onMouseDown:l,onTouchMove:function(m){r.getState().isOpen!==!1&&u===e.environment.document.activeElement&&m.target!==u&&u.blur()}},ne(a,_i))},getRootProps:function(a){return R({role:"combobox","aria-expanded":r.getState().isOpen,"aria-haspopup":"listbox","aria-owns":r.getState().isOpen?"".concat(e.id,"-list"):void 0,"aria-labelledby":"".concat(e.id,"-label")},a)},getFormProps:function(a){return a.inputElement,R({action:"",noValidate:!0,role:"search",onSubmit:function(u){var c;u.preventDefault(),e.onSubmit(R({event:u,refresh:n,state:r.getState()},o)),r.dispatch("submit",null),(c=a.inputElement)===null||c===void 0||c.blur()},onReset:function(u){var c;u.preventDefault(),e.onReset(R({event:u,refresh:n,state:r.getState()},o)),r.dispatch("reset",null),(c=a.inputElement)===null||c===void 0||c.focus()}},ne(a,Oi))},getLabelProps:function(a){var u=a||{},c=u.sourceIndex,s=ne(u,wi);return R({htmlFor:"".concat(i(e.id,c),"-input"),id:"".concat(i(e.id,c),"-label")},s)},getInputProps:function(a){var u;function c(y){(e.openOnFocus||r.getState().query)&&le(R({event:y,props:e,query:r.getState().completion||r.getState().query,refresh:n,store:r},o)),r.dispatch("focus",null)}var s=a||{},l=(s.inputElement,s.maxLength),m=l===void 0?512:l,p=ne(s,Si),v=fe(r.getState()),d=function(y){return!!(y&&y.match(ni))}(((u=e.environment.navigator)===null||u===void 0?void 0:u.userAgent)||""),h=v!=null&&v.itemUrl&&!d?"go":"search";return R({"aria-autocomplete":"both","aria-activedescendant":r.getState().isOpen&&r.getState().activeItemId!==null?"".concat(e.id,"-item-").concat(r.getState().activeItemId):void 0,"aria-controls":r.getState().isOpen?"".concat(e.id,"-list"):void 0,"aria-labelledby":"".concat(e.id,"-label"),value:r.getState().completion||r.getState().query,id:"".concat(e.id,"-input"),autoComplete:"off",autoCorrect:"off",autoCapitalize:"off",enterKeyHint:h,spellCheck:"false",autoFocus:e.autoFocus,placeholder:e.placeholder,maxLength:m,type:"search",onChange:function(y){le(R({event:y,props:e,query:y.currentTarget.value.slice(0,m),refresh:n,store:r},o))},onKeyDown:function(y){(function(b){var _=b.event,S=b.props,O=b.refresh,g=b.store,P=gi(b,hi);if(_.key==="ArrowUp"||_.key==="ArrowDown"){var C=function(){var M=S.environment.document.getElementById("".concat(S.id,"-item-").concat(g.getState().activeItemId));M&&(M.scrollIntoViewIfNeeded?M.scrollIntoViewIfNeeded(!1):M.scrollIntoView(!1))},L=function(){var M=fe(g.getState());if(g.getState().activeItemId!==null&&M){var Ot=M.item,St=M.itemInputValue,$e=M.itemUrl,B=M.source;B.onActive(te({event:_,item:Ot,itemInputValue:St,itemUrl:$e,refresh:O,source:B,state:g.getState()},P))}};_.preventDefault(),g.getState().isOpen===!1&&(S.openOnFocus||g.getState().query)?le(te({event:_,props:S,query:g.getState().query,refresh:O,store:g},P)).then(function(){g.dispatch(_.key,{nextActiveItemId:S.defaultActiveItemId}),L(),setTimeout(C,0)}):(g.dispatch(_.key,{}),L(),C())}else if(_.key==="Escape")_.preventDefault(),g.dispatch(_.key,null),g.pendingRequests.cancelAll();else if(_.key==="Tab")g.dispatch("blur",null),g.pendingRequests.cancelAll();else if(_.key==="Enter"){if(g.getState().activeItemId===null||g.getState().collections.every(function(M){return M.items.length===0}))return void(S.debug||g.pendingRequests.cancelAll());_.preventDefault();var x=fe(g.getState()),k=x.item,N=x.itemInputValue,U=x.itemUrl,F=x.source;if(_.metaKey||_.ctrlKey)U!==void 0&&(F.onSelect(te({event:_,item:k,itemInputValue:N,itemUrl:U,refresh:O,source:F,state:g.getState()},P)),S.navigator.navigateNewTab({itemUrl:U,item:k,state:g.getState()}));else if(_.shiftKey)U!==void 0&&(F.onSelect(te({event:_,item:k,itemInputValue:N,itemUrl:U,refresh:O,source:F,state:g.getState()},P)),S.navigator.navigateNewWindow({itemUrl:U,item:k,state:g.getState()}));else if(!_.altKey){if(U!==void 0)return F.onSelect(te({event:_,item:k,itemInputValue:N,itemUrl:U,refresh:O,source:F,state:g.getState()},P)),void S.navigator.navigate({itemUrl:U,item:k,state:g.getState()});le(te({event:_,nextState:{isOpen:!1},props:S,query:N,refresh:O,store:g},P)).then(function(){F.onSelect(te({event:_,item:k,itemInputValue:N,itemUrl:U,refresh:O,source:F,state:g.getState()},P))})}}})(R({event:y,props:e,refresh:n,store:r},o))},onFocus:c,onBlur:vt,onClick:function(y){a.inputElement!==e.environment.document.activeElement||r.getState().isOpen||c(y)}},p)},getPanelProps:function(a){return R({onMouseDown:function(u){u.preventDefault()},onMouseLeave:function(){r.dispatch("mouseleave",null)}},a)},getListProps:function(a){var u=a||{},c=u.sourceIndex,s=ne(u,ji);return R({role:"listbox","aria-labelledby":"".concat(i(e.id,c),"-label"),id:"".concat(i(e.id,c),"-list")},s)},getItemProps:function(a){var u=a.item,c=a.source,s=a.sourceIndex,l=ne(a,Ei);return R({id:"".concat(i(e.id,s),"-item-").concat(u.__autocomplete_id),role:"option","aria-selected":r.getState().activeItemId===u.__autocomplete_id,onMouseMove:function(m){if(u.__autocomplete_id!==r.getState().activeItemId){r.dispatch("mousemove",u.__autocomplete_id);var p=fe(r.getState());if(r.getState().activeItemId!==null&&p){var v=p.item,d=p.itemInputValue,h=p.itemUrl,y=p.source;y.onActive(R({event:m,item:v,itemInputValue:d,itemUrl:h,refresh:n,source:y,state:r.getState()},o))}}},onMouseDown:function(m){m.preventDefault()},onClick:function(m){var p=c.getItemInputValue({item:u,state:r.getState()}),v=c.getItemUrl({item:u,state:r.getState()});(v?Promise.resolve():le(R({event:m,nextState:{isOpen:!1},props:e,query:p,refresh:n,store:r},o))).then(function(){c.onSelect(R({event:m,item:u,itemInputValue:p,itemUrl:v,refresh:n,source:c,state:r.getState()},o))})}},l)}}}function He(t){return He=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},He(t)}function Qn(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(o){return Object.getOwnPropertyDescriptor(t,o).enumerable})),n.push.apply(n,r)}return n}function Di(t){for(var e=1;et.length)&&(e=t.length);for(var n=0,r=new Array(e);n=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}function Zi(t){var e=t.translations,n=e===void 0?{}:e,r=Qi(t,Ji),o=n.noResultsText,i=o===void 0?"No results for":o,a=n.suggestedQueryText,u=a===void 0?"Try searching for":a,c=n.reportMissingResultsText,s=c===void 0?"Believe this query should return results?":c,l=n.reportMissingResultsLinkText,m=l===void 0?"Let us know.":l,p=r.state.context.searchSuggestions;return f.createElement("div",{className:"DocSearch-NoResults"},f.createElement("div",{className:"DocSearch-Screen-Icon"},f.createElement(Ki,null)),f.createElement("p",{className:"DocSearch-Title"},i,' "',f.createElement("strong",null,r.state.query),'"'),p&&p.length>0&&f.createElement("div",{className:"DocSearch-NoResults-Prefill-List"},f.createElement("p",{className:"DocSearch-Help"},u,":"),f.createElement("ul",null,p.slice(0,3).reduce(function(v,d){return[].concat($i(v),[f.createElement("li",{key:d},f.createElement("button",{className:"DocSearch-Prefill",key:d,type:"button",onClick:function(){r.setQuery(d.toLowerCase()+" "),r.refresh(),r.inputRef.current.focus()}},d))])},[]))),r.getMissingResultsUrl&&f.createElement("p",{className:"DocSearch-Help"},"".concat(s," "),f.createElement("a",{href:r.getMissingResultsUrl({query:r.state.query}),target:"_blank",rel:"noopener noreferrer"},m)))}var Yi=["hit","attribute","tagName"];function er(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(o){return Object.getOwnPropertyDescriptor(t,o).enumerable})),n.push.apply(n,r)}return n}function tr(t){for(var e=1;e=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}function nr(t,e){return e.split(".").reduce(function(n,r){return n!=null&&n[r]?n[r]:null},t)}function ue(t){var e=t.hit,n=t.attribute,r=t.tagName;return W(r===void 0?"span":r,tr(tr({},Xi(t,Yi)),{},{dangerouslySetInnerHTML:{__html:nr(e,"_snippetResult.".concat(n,".value"))||nr(e,n)}}))}function rr(t,e){return function(n){if(Array.isArray(n))return n}(t)||function(n,r){var o=n==null?null:typeof Symbol<"u"&&n[Symbol.iterator]||n["@@iterator"];if(o!=null){var i,a,u=[],c=!0,s=!1;try{for(o=o.call(n);!(c=(i=o.next()).done)&&(u.push(i.value),!r||u.length!==r);c=!0);}catch(l){s=!0,a=l}finally{try{c||o.return==null||o.return()}finally{if(s)throw a}}return u}}(t,e)||function(n,r){if(n){if(typeof n=="string")return or(n,r);var o=Object.prototype.toString.call(n).slice(8,-1);if(o==="Object"&&n.constructor&&(o=n.constructor.name),o==="Map"||o==="Set")return Array.from(n);if(o==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(o))return or(n,r)}}(t,e)||function(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}()}function or(t,e){(e==null||e>t.length)&&(e=t.length);for(var n=0,r=new Array(e);n|<\/mark>)/g,na=RegExp(zr.source);function Jr(t){var e,n,r=t;if(!r.__docsearch_parent&&!t._highlightResult)return t.hierarchy.lvl0;var o=((r.__docsearch_parent?(e=r.__docsearch_parent)===null||e===void 0||(e=e._highlightResult)===null||e===void 0||(e=e.hierarchy)===null||e===void 0?void 0:e.lvl0:(n=t._highlightResult)===null||n===void 0||(n=n.hierarchy)===null||n===void 0?void 0:n.lvl0)||{}).value;return o&&na.test(o)?o.replace(zr,""):o}function Jt(){return Jt=Object.assign||function(t){for(var e=1;e=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}function aa(t){var e=t.translations,n=e===void 0?{}:e,r=ia(t,oa),o=n.recentSearchesTitle,i=o===void 0?"Recent":o,a=n.noRecentSearchesText,u=a===void 0?"No recent searches":a,c=n.saveRecentSearchButtonTitle,s=c===void 0?"Save this search":c,l=n.removeRecentSearchButtonTitle,m=l===void 0?"Remove this search from history":l,p=n.favoriteSearchesTitle,v=p===void 0?"Favorite":p,d=n.removeFavoriteSearchButtonTitle,h=d===void 0?"Remove this search from favorites":d;return r.state.status==="idle"&&r.hasCollections===!1?r.disableUserPersonalization?null:f.createElement("div",{className:"DocSearch-StartScreen"},f.createElement("p",{className:"DocSearch-Help"},u)):r.hasCollections===!1?null:f.createElement("div",{className:"DocSearch-Dropdown-Container"},f.createElement(zt,ht({},r,{title:i,collection:r.state.collections[0],renderIcon:function(){return f.createElement("div",{className:"DocSearch-Hit-icon"},f.createElement(Mi,null))},renderAction:function(y){var b=y.item,_=y.runFavoriteTransition,S=y.runDeleteTransition;return f.createElement(f.Fragment,null,f.createElement("div",{className:"DocSearch-Hit-action"},f.createElement("button",{className:"DocSearch-Hit-action-button",title:s,type:"submit",onClick:function(O){O.preventDefault(),O.stopPropagation(),_(function(){r.favoriteSearches.add(b),r.recentSearches.remove(b),r.refresh()})}},f.createElement(Xn,null))),f.createElement("div",{className:"DocSearch-Hit-action"},f.createElement("button",{className:"DocSearch-Hit-action-button",title:m,type:"submit",onClick:function(O){O.preventDefault(),O.stopPropagation(),S(function(){r.recentSearches.remove(b),r.refresh()})}},f.createElement(Kt,null))))}})),f.createElement(zt,ht({},r,{title:v,collection:r.state.collections[1],renderIcon:function(){return f.createElement("div",{className:"DocSearch-Hit-icon"},f.createElement(Xn,null))},renderAction:function(y){var b=y.item,_=y.runDeleteTransition;return f.createElement("div",{className:"DocSearch-Hit-action"},f.createElement("button",{className:"DocSearch-Hit-action-button",title:h,type:"submit",onClick:function(S){S.preventDefault(),S.stopPropagation(),_(function(){r.favoriteSearches.remove(b),r.refresh()})}},f.createElement(Kt,null)))}})))}var ca=["translations"];function yt(){return yt=Object.assign||function(t){for(var e=1;e=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}var la=f.memo(function(t){var e=t.translations,n=e===void 0?{}:e,r=ua(t,ca);if(r.state.status==="error")return f.createElement(zi,{translations:n==null?void 0:n.errorScreen});var o=r.state.collections.some(function(i){return i.items.length>0});return r.state.query?o===!1?f.createElement(Zi,yt({},r,{translations:n==null?void 0:n.noResultsScreen})):f.createElement(ra,r):f.createElement(aa,yt({},r,{hasCollections:o,translations:n==null?void 0:n.startScreen}))},function(t,e){return e.state.status==="loading"||e.state.status==="stalled"}),sa=["translations"];function gt(){return gt=Object.assign||function(t){for(var e=1;e=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}function ma(t){var e=t.translations,n=e===void 0?{}:e,r=fa(t,sa),o=n.resetButtonTitle,i=o===void 0?"Clear the query":o,a=n.resetButtonAriaLabel,u=a===void 0?"Clear the query":a,c=n.cancelButtonText,s=c===void 0?"Cancel":c,l=n.cancelButtonAriaLabel,m=l===void 0?"Cancel":l,p=r.getFormProps({inputElement:r.inputRef.current}).onReset;return f.useEffect(function(){r.autoFocus&&r.inputRef.current&&r.inputRef.current.focus()},[r.autoFocus,r.inputRef]),f.useEffect(function(){r.isFromSelection&&r.inputRef.current&&r.inputRef.current.select()},[r.isFromSelection,r.inputRef]),f.createElement(f.Fragment,null,f.createElement("form",{className:"DocSearch-Form",onSubmit:function(v){v.preventDefault()},onReset:p},f.createElement("label",gt({className:"DocSearch-MagnifierLabel"},r.getLabelProps()),f.createElement(Hr,null)),f.createElement("div",{className:"DocSearch-LoadingIndicator"},f.createElement(Li,null)),f.createElement("input",gt({className:"DocSearch-Input",ref:r.inputRef},r.getInputProps({inputElement:r.inputRef.current,autoFocus:r.autoFocus,maxLength:64}))),f.createElement("button",{type:"reset",title:i,className:"DocSearch-Reset","aria-label":u,hidden:!r.state.query},f.createElement(Kt,null))),f.createElement("button",{className:"DocSearch-Cancel",type:"reset","aria-label":m,onClick:r.onClose},s))}var pa=["_highlightResult","_snippetResult"];function va(t,e){if(t==null)return{};var n,r,o=function(a,u){if(a==null)return{};var c,s,l={},m=Object.keys(a);for(s=0;s=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}function da(t){return function(){var e="__TEST_KEY__";try{return localStorage.setItem(e,""),localStorage.removeItem(e),!0}catch{return!1}}()===!1?{setItem:function(){},getItem:function(){return[]}}:{setItem:function(e){return window.localStorage.setItem(t,JSON.stringify(e))},getItem:function(){var e=window.localStorage.getItem(t);return e?JSON.parse(e):[]}}}function cr(t){var e=t.key,n=t.limit,r=n===void 0?5:n,o=da(e),i=o.getItem().slice(0,r);return{add:function(a){var u=a,c=(u._highlightResult,u._snippetResult,va(u,pa)),s=i.findIndex(function(l){return l.objectID===c.objectID});s>-1&&i.splice(s,1),i.unshift(c),i=i.slice(0,r),o.setItem(i)},remove:function(a){i=i.filter(function(u){return u.objectID!==a.objectID}),o.setItem(i)},getAll:function(){return i}}}var ha=["facetName","facetQuery"];function ya(t){var e,n="algoliasearch-client-js-".concat(t.key),r=function(){return e===void 0&&(e=t.localStorage||window.localStorage),e},o=function(){return JSON.parse(r().getItem(n)||"{}")},i=function(u){r().setItem(n,JSON.stringify(u))},a=function(){var u=t.timeToLive?1e3*t.timeToLive:null,c=o(),s=Object.fromEntries(Object.entries(c).filter(function(m){return se(m,2)[1].timestamp!==void 0}));if(i(s),u){var l=Object.fromEntries(Object.entries(s).filter(function(m){var p=se(m,2)[1],v=new Date().getTime();return!(p.timestamp+u2&&arguments[2]!==void 0?arguments[2]:{miss:function(){return Promise.resolve()}};return Promise.resolve().then(function(){a();var l=JSON.stringify(u);return o()[l]}).then(function(l){return Promise.all([l?l.value:c(),l!==void 0])}).then(function(l){var m=se(l,2),p=m[0],v=m[1];return Promise.all([p,v||s.miss(p)])}).then(function(l){return se(l,1)[0]})},set:function(u,c){return Promise.resolve().then(function(){var s=o();return s[JSON.stringify(u)]={timestamp:new Date().getTime(),value:c},r().setItem(n,JSON.stringify(s)),c})},delete:function(u){return Promise.resolve().then(function(){var c=o();delete c[JSON.stringify(u)],r().setItem(n,JSON.stringify(c))})},clear:function(){return Promise.resolve().then(function(){r().removeItem(n)})}}}function Ee(t){var e=ft(t.caches),n=e.shift();return n===void 0?{get:function(r,o){var i=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{miss:function(){return Promise.resolve()}};return o().then(function(a){return Promise.all([a,i.miss(a)])}).then(function(a){return se(a,1)[0]})},set:function(r,o){return Promise.resolve(o)},delete:function(r){return Promise.resolve()},clear:function(){return Promise.resolve()}}:{get:function(r,o){var i=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{miss:function(){return Promise.resolve()}};return n.get(r,o,i).catch(function(){return Ee({caches:e}).get(r,o,i)})},set:function(r,o){return n.set(r,o).catch(function(){return Ee({caches:e}).set(r,o)})},delete:function(r){return n.delete(r).catch(function(){return Ee({caches:e}).delete(r)})},clear:function(){return n.clear().catch(function(){return Ee({caches:e}).clear()})}}}function Tt(){var t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{serializable:!0},e={};return{get:function(n,r){var o=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{miss:function(){return Promise.resolve()}},i=JSON.stringify(n);if(i in e)return Promise.resolve(t.serializable?JSON.parse(e[i]):e[i]);var a=r(),u=o&&o.miss||function(){return Promise.resolve()};return a.then(function(c){return u(c)}).then(function(){return a})},set:function(n,r){return e[JSON.stringify(n)]=t.serializable?JSON.stringify(r):r,Promise.resolve(r)},delete:function(n){return delete e[JSON.stringify(n)],Promise.resolve()},clear:function(){return e={},Promise.resolve()}}}function ga(t){for(var e=t.length-1;e>0;e--){var n=Math.floor(Math.random()*(e+1)),r=t[e];t[e]=t[n],t[n]=r}return t}function $r(t,e){return e&&Object.keys(e).forEach(function(n){t[n]=e[n](t)}),t}function bt(t){for(var e=arguments.length,n=new Array(e>1?e-1:0),r=1;r0?r:void 0,timeout:n.timeout||e,headers:n.headers||{},queryParameters:n.queryParameters||{},cacheable:n.cacheable}}var me={Read:1,Write:2,Any:3},Qr=1,ba=2,Zr=3;function Yr(t){var e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:Qr;return I(I({},t),{},{status:e,lastUpdate:Date.now()})}function Gr(t){return typeof t=="string"?{protocol:"https",url:t,accept:me.Any}:{protocol:t.protocol||"https",url:t.url,accept:t.accept||me.Any}}var $t="GET",_t="POST";function _a(t,e){return Promise.all(e.map(function(n){return t.get(n,function(){return Promise.resolve(Yr(n))})})).then(function(n){var r=n.filter(function(a){return function(u){return u.status===Qr||Date.now()-u.lastUpdate>12e4}(a)}),o=n.filter(function(a){return function(u){return u.status===Zr&&Date.now()-u.lastUpdate<=12e4}(a)}),i=[].concat(ft(r),ft(o));return{getTimeout:function(a,u){return(o.length===0&&a===0?1:o.length+3+a)*u},statelessHosts:i.length>0?i.map(function(a){return Gr(a)}):e}})}function lr(t,e,n,r){var o=[],i=function(p,v){if(!(p.method===$t||p.data===void 0&&v.data===void 0)){var d=Array.isArray(p.data)?p.data:I(I({},p.data),v.data);return JSON.stringify(d)}}(n,r),a=function(p,v){var d=I(I({},p.headers),v.headers),h={};return Object.keys(d).forEach(function(y){var b=d[y];h[y.toLowerCase()]=b}),h}(t,r),u=n.method,c=n.method!==$t?{}:I(I({},n.data),r.data),s=I(I(I({"x-algolia-agent":t.userAgent.value},t.queryParameters),c),r.queryParameters),l=0,m=function p(v,d){var h=v.pop();if(h===void 0)throw{name:"RetryError",message:"Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.",transporterStackTrace:sr(o)};var y={data:i,headers:a,method:u,url:Sa(h,n.path,s),connectTimeout:d(l,t.timeouts.connect),responseTimeout:d(l,r.timeout)},b=function(S){var O={request:y,response:S,host:h,triesLeft:v.length};return o.push(O),O},_={onSuccess:function(S){return function(O){try{return JSON.parse(O.content)}catch(g){throw function(P,C){return{name:"DeserializationError",message:P,response:C}}(g.message,O)}}(S)},onRetry:function(S){var O=b(S);return S.isTimedOut&&l++,Promise.all([t.logger.info("Retryable failure",eo(O)),t.hostsCache.set(h,Yr(h,S.isTimedOut?Zr:ba))]).then(function(){return p(v,d)})},onFail:function(S){throw b(S),function(O,g){var P=O.content,C=O.status,L=P;try{L=JSON.parse(P).message}catch{}return function(x,k,N){return{name:"ApiError",message:x,status:k,transporterStackTrace:N}}(L,C,g)}(S,sr(o))}};return t.requester.send(y).then(function(S){return function(O,g){return function(P){var C=P.status;return P.isTimedOut||function(L){var x=L.isTimedOut,k=L.status;return!x&&~~k==0}(P)||~~(C/100)!=2&&~~(C/100)!=4}(O)?g.onRetry(O):~~(O.status/100)==2?g.onSuccess(O):g.onFail(O)}(S,_)})};return _a(t.hostsCache,e).then(function(p){return m(ft(p.statelessHosts).reverse(),p.getTimeout)})}function Oa(t){var e={value:"Algolia for JavaScript (".concat(t,")"),add:function(n){var r="; ".concat(n.segment).concat(n.version!==void 0?" (".concat(n.version,")"):"");return e.value.indexOf(r)===-1&&(e.value="".concat(e.value).concat(r)),e}};return e}function Sa(t,e,n){var r=Xr(n),o="".concat(t.protocol,"://").concat(t.url,"/").concat(e.charAt(0)==="/"?e.substr(1):e);return r.length&&(o+="?".concat(r)),o}function Xr(t){return Object.keys(t).map(function(e){return bt("%s=%s",e,(n=t[e],Object.prototype.toString.call(n)==="[object Object]"||Object.prototype.toString.call(n)==="[object Array]"?JSON.stringify(t[e]):t[e]));var n}).join("&")}function sr(t){return t.map(function(e){return eo(e)})}function eo(t){var e=t.request.headers["x-algolia-api-key"]?{"x-algolia-api-key":"*****"}:{};return I(I({},t),{},{request:I(I({},t.request),{},{headers:I(I({},t.request.headers),e)})})}var wa=function(t){var e=t.appId,n=function(i,a,u){var c={"x-algolia-api-key":u,"x-algolia-application-id":a};return{headers:function(){return i===st.WithinHeaders?c:{}},queryParameters:function(){return i===st.WithinQueryParameters?c:{}}}}(t.authMode!==void 0?t.authMode:st.WithinHeaders,e,t.apiKey),r=function(i){var a=i.hostsCache,u=i.logger,c=i.requester,s=i.requestsCache,l=i.responsesCache,m=i.timeouts,p=i.userAgent,v=i.hosts,d=i.queryParameters,h={hostsCache:a,logger:u,requester:c,requestsCache:s,responsesCache:l,timeouts:m,userAgent:p,headers:i.headers,queryParameters:d,hosts:v.map(function(y){return Gr(y)}),read:function(y,b){var _=ur(b,h.timeouts.read),S=function(){return lr(h,h.hosts.filter(function(g){return(g.accept&me.Read)!=0}),y,_)};if((_.cacheable!==void 0?_.cacheable:y.cacheable)!==!0)return S();var O={request:y,mappedRequestOptions:_,transporter:{queryParameters:h.queryParameters,headers:h.headers}};return h.responsesCache.get(O,function(){return h.requestsCache.get(O,function(){return h.requestsCache.set(O,S()).then(function(g){return Promise.all([h.requestsCache.delete(O),g])},function(g){return Promise.all([h.requestsCache.delete(O),Promise.reject(g)])}).then(function(g){var P=se(g,2);return P[0],P[1]})})},{miss:function(g){return h.responsesCache.set(O,g)}})},write:function(y,b){return lr(h,h.hosts.filter(function(_){return(_.accept&me.Write)!=0}),y,ur(b,h.timeouts.write))}};return h}(I(I({hosts:[{url:"".concat(e,"-dsn.algolia.net"),accept:me.Read},{url:"".concat(e,".algolia.net"),accept:me.Write}].concat(ga([{url:"".concat(e,"-1.algolianet.com")},{url:"".concat(e,"-2.algolianet.com")},{url:"".concat(e,"-3.algolianet.com")}]))},t),{},{headers:I(I(I({},n.headers()),{"content-type":"application/x-www-form-urlencoded"}),t.headers),queryParameters:I(I({},n.queryParameters()),t.queryParameters)})),o={transporter:r,appId:e,addAlgoliaAgent:function(i,a){r.userAgent.add({segment:i,version:a})},clearCache:function(){return Promise.all([r.requestsCache.clear(),r.responsesCache.clear()]).then(function(){})}};return $r(o,t.methods)},ja=function(t){return function(e,n){return e.method===$t?t.transporter.read(e,n):t.transporter.write(e,n)}},to=function(t){return function(e){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},r={transporter:t.transporter,appId:t.appId,indexName:e};return $r(r,n.methods)}},fr=function(t){return function(e,n){var r=e.map(function(o){return I(I({},o),{},{params:Xr(o.params||{})})});return t.transporter.read({method:_t,path:"1/indexes/*/queries",data:{requests:r},cacheable:!0},n)}},mr=function(t){return function(e,n){return Promise.all(e.map(function(r){var o=r.params,i=o.facetName,a=o.facetQuery,u=_o(o,ha);return to(t)(r.indexName,{methods:{searchForFacetValues:no}}).searchForFacetValues(i,a,I(I({},n),u))}))}},Ea=function(t){return function(e,n,r){return t.transporter.read({method:_t,path:bt("1/answers/%s/prediction",t.indexName),data:{query:e,queryLanguages:n},cacheable:!0},r)}},Pa=function(t){return function(e,n){return t.transporter.read({method:_t,path:bt("1/indexes/%s/query",t.indexName),data:{query:e},cacheable:!0},n)}},no=function(t){return function(e,n,r){return t.transporter.read({method:_t,path:bt("1/indexes/%s/facets/%s/query",t.indexName,e),data:{facetQuery:n},cacheable:!0},r)}},Ia=1,Da=2,ka=3;function ro(t,e,n){var r,o={appId:t,apiKey:e,timeouts:{connect:1,read:2,write:30},requester:{send:function(i){return new Promise(function(a){var u=new XMLHttpRequest;u.open(i.method,i.url,!0),Object.keys(i.headers).forEach(function(m){return u.setRequestHeader(m,i.headers[m])});var c,s=function(m,p){return setTimeout(function(){u.abort(),a({status:0,content:p,isTimedOut:!0})},1e3*m)},l=s(i.connectTimeout,"Connection timeout");u.onreadystatechange=function(){u.readyState>u.OPENED&&c===void 0&&(clearTimeout(l),c=s(i.responseTimeout,"Socket timeout"))},u.onerror=function(){u.status===0&&(clearTimeout(l),clearTimeout(c),a({content:u.responseText||"Network request failed",status:u.status,isTimedOut:!1}))},u.onload=function(){clearTimeout(l),clearTimeout(c),a({content:u.responseText,status:u.status,isTimedOut:!1})},u.send(i.data)})}},logger:(r=ka,{debug:function(i,a){return Ia>=r&&console.debug(i,a),Promise.resolve()},info:function(i,a){return Da>=r&&console.info(i,a),Promise.resolve()},error:function(i,a){return console.error(i,a),Promise.resolve()}}),responsesCache:Tt(),requestsCache:Tt({serializable:!1}),hostsCache:Ee({caches:[ya({key:"".concat("4.19.1","-").concat(t)}),Tt()]}),userAgent:Oa("4.19.1").add({segment:"Browser",version:"lite"}),authMode:st.WithinQueryParameters};return wa(I(I(I({},o),n),{},{methods:{search:fr,searchForFacetValues:mr,multipleQueries:fr,multipleSearchForFacetValues:mr,customRequest:ja,initIndex:function(i){return function(a){return to(i)(a,{methods:{search:Pa,searchForFacetValues:no,findAnswers:Ea}})}}}}))}ro.version="4.19.1";var Aa=["footer","searchBox"];function Be(){return Be=Object.assign||function(t){for(var e=1;et.length)&&(e=t.length);for(var n=0,r=new Array(e);n=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}function Ta(t){var e=t.appId,n=t.apiKey,r=t.indexName,o=t.placeholder,i=o===void 0?"Search docs":o,a=t.searchParameters,u=t.maxResultsPerGroup,c=t.onClose,s=c===void 0?ta:c,l=t.transformItems,m=l===void 0?ar:l,p=t.hitComponent,v=p===void 0?qi:p,d=t.resultsFooterComponent,h=d===void 0?function(){return null}:d,y=t.navigator,b=t.initialScrollY,_=b===void 0?0:b,S=t.transformSearchClient,O=S===void 0?ar:S,g=t.disableUserPersonalization,P=g!==void 0&&g,C=t.initialQuery,L=C===void 0?"":C,x=t.translations,k=x===void 0?{}:x,N=t.getMissingResultsUrl,U=t.insights,F=U!==void 0&&U,M=k.footer,Ot=k.searchBox,St=Na(k,Aa),$e=xa(f.useState({query:"",collections:[],completion:null,context:{},isOpen:!1,activeItemId:null,status:"idle"}),2),B=$e[0],oo=$e[1],Xt=f.useRef(null),wt=f.useRef(null),en=f.useRef(null),Qe=f.useRef(null),he=f.useRef(null),Q=f.useRef(10),tn=f.useRef(typeof window<"u"?window.getSelection().toString().slice(0,64):"").current,ee=f.useRef(L||tn).current,nn=function(j,D,T){return f.useMemo(function(){var H=ro(j,D);return H.addAlgoliaAgent("docsearch","3.5.2"),/docsearch.js \(.*\)/.test(H.transporter.userAgent.value)===!1&&H.addAlgoliaAgent("docsearch-react","3.5.2"),T(H)},[j,D,T])}(e,n,O),oe=f.useRef(cr({key:"__DOCSEARCH_FAVORITE_SEARCHES__".concat(r),limit:10})).current,ye=f.useRef(cr({key:"__DOCSEARCH_RECENT_SEARCHES__".concat(r),limit:oe.getAll().length===0?7:4})).current,ge=f.useCallback(function(j){if(!P){var D=j.type==="content"?j.__docsearch_parent:j;D&&oe.getAll().findIndex(function(T){return T.objectID===D.objectID})===-1&&ye.add(D)}},[oe,ye,P]),io=f.useCallback(function(j){if(B.context.algoliaInsightsPlugin&&j.__autocomplete_id){var D=j,T={eventName:"Item Selected",index:D.__autocomplete_indexName,items:[D],positions:[j.__autocomplete_id],queryID:D.__autocomplete_queryID};B.context.algoliaInsightsPlugin.insights.clickedObjectIDsAfterSearch(T)}},[B.context.algoliaInsightsPlugin]),be=f.useMemo(function(){return Ni({id:"docsearch",defaultActiveItemId:0,placeholder:i,openOnFocus:!0,initialState:{query:ee,context:{searchSuggestions:[]}},insights:F,navigator:y,onStateChange:function(j){oo(j.state)},getSources:function(j){var D=j.query,T=j.state,H=j.setContext,Z=j.setStatus;if(!D)return P?[]:[{sourceId:"recentSearches",onSelect:function(A){var V=A.item,_e=A.event;ge(V),at(_e)||s()},getItemUrl:function(A){return A.item.url},getItems:function(){return ye.getAll()}},{sourceId:"favoriteSearches",onSelect:function(A){var V=A.item,_e=A.event;ge(V),at(_e)||s()},getItemUrl:function(A){return A.item.url},getItems:function(){return oe.getAll()}}];var Y=!!F;return nn.search([{query:D,indexName:r,params:Rt({attributesToRetrieve:["hierarchy.lvl0","hierarchy.lvl1","hierarchy.lvl2","hierarchy.lvl3","hierarchy.lvl4","hierarchy.lvl5","hierarchy.lvl6","content","type","url"],attributesToSnippet:["hierarchy.lvl1:".concat(Q.current),"hierarchy.lvl2:".concat(Q.current),"hierarchy.lvl3:".concat(Q.current),"hierarchy.lvl4:".concat(Q.current),"hierarchy.lvl5:".concat(Q.current),"hierarchy.lvl6:".concat(Q.current),"content:".concat(Q.current)],snippetEllipsisText:"…",highlightPreTag:"",highlightPostTag:"",hitsPerPage:20,clickAnalytics:Y},a)}]).catch(function(A){throw A.name==="RetryError"&&Z("error"),A}).then(function(A){var V=A.results[0],_e=V.hits,uo=V.nbHits,jt=ir(_e,function(Et){return Jr(Et)},u);T.context.searchSuggestions.length0&&(rn(),he.current&&he.current.focus())},[ee,rn]),f.useEffect(function(){function j(){if(wt.current){var D=.01*window.innerHeight;wt.current.style.setProperty("--docsearch-vh","".concat(D,"px"))}}return j(),window.addEventListener("resize",j),function(){window.removeEventListener("resize",j)}},[]),f.createElement("div",Be({ref:Xt},co({"aria-expanded":!0}),{className:["DocSearch","DocSearch-Container",B.status==="stalled"&&"DocSearch-Container--Stalled",B.status==="error"&&"DocSearch-Container--Errored"].filter(Boolean).join(" "),role:"button",tabIndex:0,onMouseDown:function(j){j.target===j.currentTarget&&s()}}),f.createElement("div",{className:"DocSearch-Modal",ref:wt},f.createElement("header",{className:"DocSearch-SearchBar",ref:en},f.createElement(ma,Be({},be,{state:B,autoFocus:ee.length===0,inputRef:he,isFromSelection:!!ee&&ee===tn,translations:Ot,onClose:s}))),f.createElement("div",{className:"DocSearch-Dropdown",ref:Qe},f.createElement(la,Be({},be,{indexName:r,state:B,hitComponent:v,resultsFooterComponent:h,disableUserPersonalization:P,recentSearches:ye,favoriteSearches:oe,inputRef:he,translations:St,getMissingResultsUrl:N,onItemClick:function(j,D){io(j),ge(j),at(D)||s()}}))),f.createElement("footer",{className:"DocSearch-Footer"},f.createElement(Ri,{translations:M}))))}function Qt(){return Qt=Object.assign||function(t){for(var e=1;et.length)&&(e=t.length);for(var n=0,r=new Array(e);n1&&arguments[1]!==void 0?arguments[1]:window;return typeof e=="string"?n.document.querySelector(e):e}(t.container,t.environment))}const La={id:"docsearch"},Ua=so({__name:"VPAlgoliaSearchBox",props:{algolia:{}},setup(t){const e=t,n=fo(),r=mo(),{site:o,localeIndex:i,lang:a}=go();po(u),vo(i,u);function u(){var v,d;const l={...e.algolia,...(v=e.algolia.locales)==null?void 0:v[i.value]},m=((d=l.searchParameters)==null?void 0:d.facetFilters)??[],p=[...(Array.isArray(m)?m:[m]).filter(h=>!h.startsWith("lang:")),`lang:${a.value}`];c({...l,searchParameters:{...l.searchParameters,facetFilters:p}})}function c(l){const m=Object.assign({},l,{container:"#docsearch",navigator:{navigate({itemUrl:p}){const{pathname:v}=new URL(window.location.origin+p);r.path===v?window.location.assign(window.location.origin+p):n.go(p)}},transformItems(p){return p.map(v=>Object.assign({},v,{url:s(v.url)}))},hitComponent({hit:p,children:v}){return{__v:null,type:"a",ref:void 0,constructor:void 0,key:void 0,props:{href:p.url,children:v}}}});qa(m)}function s(l){const{pathname:m,hash:p}=new URL(l);return m.replace(/\.html$/,o.value.cleanUrls?"":".html")+p}return(l,m)=>(ho(),yo("div",La))}});export{Ua as default}; diff --git a/assets/chunks/framework.dc1f4608.js b/assets/chunks/framework.dc1f4608.js new file mode 100644 index 000000000..c27464658 --- /dev/null +++ b/assets/chunks/framework.dc1f4608.js @@ -0,0 +1,29 @@ +/** +* @vue/shared v3.4.19 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function fr(e,t){const n=new Set(e.split(","));return t?r=>n.has(r.toLowerCase()):r=>n.has(r)}const ne={},mt=[],be=()=>{},co=()=>!1,kt=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),dr=e=>e.startsWith("onUpdate:"),oe=Object.assign,hr=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},ao=Object.prototype.hasOwnProperty,X=(e,t)=>ao.call(e,t),U=Array.isArray,_t=e=>wn(e)==="[object Map]",Ns=e=>wn(e)==="[object Set]",W=e=>typeof e=="function",re=e=>typeof e=="string",Tt=e=>typeof e=="symbol",Z=e=>e!==null&&typeof e=="object",Hs=e=>(Z(e)||W(e))&&W(e.then)&&W(e.catch),$s=Object.prototype.toString,wn=e=>$s.call(e),uo=e=>wn(e).slice(8,-1),Ds=e=>wn(e)==="[object Object]",pr=e=>re(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,yt=fr(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),En=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},fo=/-(\w)/g,Fe=En(e=>e.replace(fo,(t,n)=>n?n.toUpperCase():"")),ho=/\B([A-Z])/g,ft=En(e=>e.replace(ho,"-$1").toLowerCase()),Cn=En(e=>e.charAt(0).toUpperCase()+e.slice(1)),cn=En(e=>e?`on${Cn(e)}`:""),ze=(e,t)=>!Object.is(e,t),Vn=(e,t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,value:n})},po=e=>{const t=parseFloat(e);return isNaN(t)?e:t},go=e=>{const t=re(e)?Number(e):NaN;return isNaN(t)?e:t};let Vr;const gr=()=>Vr||(Vr=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function mr(e){if(U(e)){const t={};for(let n=0;n{if(n){const r=n.split(_o);r.length>1&&(t[r[0].trim()]=r[1].trim())}}),t}function _r(e){let t="";if(re(e))t=e;else if(U(e))for(let n=0;nre(e)?e:e==null?"":U(e)||Z(e)&&(e.toString===$s||!W(e.toString))?JSON.stringify(e,js,2):String(e),js=(e,t)=>t&&t.__v_isRef?js(e,t.value):_t(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[r,s],i)=>(n[jn(r,i)+" =>"]=s,n),{})}:Ns(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>jn(n))}:Tt(t)?jn(t):Z(t)&&!U(t)&&!Ds(t)?String(t):t,jn=(e,t="")=>{var n;return Tt(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};/** +* @vue/reactivity v3.4.19 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let ve;class Eo{constructor(t=!1){this.detached=t,this._active=!0,this.effects=[],this.cleanups=[],this.parent=ve,!t&&ve&&(this.index=(ve.scopes||(ve.scopes=[])).push(this)-1)}get active(){return this._active}run(t){if(this._active){const n=ve;try{return ve=this,t()}finally{ve=n}}}on(){ve=this}off(){ve=this.parent}stop(t){if(this._active){let n,r;for(n=0,r=this.effects.length;n=4))break}this._dirtyLevel===1&&(this._dirtyLevel=0),Qe()}return this._dirtyLevel>=4}set dirty(t){this._dirtyLevel=t?4:0}run(){if(this._dirtyLevel=0,!this.active)return this.fn();let t=Ye,n=lt;try{return Ye=!0,lt=this,this._runnings++,jr(this),this.fn()}finally{Ur(this),this._runnings--,lt=n,Ye=t}}stop(){var t;this.active&&(jr(this),Ur(this),(t=this.onStop)==null||t.call(this),this.active=!1)}}function To(e){return e.value}function jr(e){e._trackId++,e._depsLength=0}function Ur(e){if(e.deps.length>e._depsLength){for(let t=e._depsLength;t{const n=new Map;return n.cleanup=e,n.computed=t,n},dn=new WeakMap,ct=Symbol(""),er=Symbol("");function _e(e,t,n){if(Ye&<){let r=dn.get(e);r||dn.set(e,r=new Map);let s=r.get(n);s||r.set(n,s=qs(()=>r.delete(n))),Ks(lt,s)}}function He(e,t,n,r,s,i){const o=dn.get(e);if(!o)return;let l=[];if(t==="clear")l=[...o.values()];else if(n==="length"&&U(e)){const c=Number(r);o.forEach((u,d)=>{(d==="length"||!Tt(d)&&d>=c)&&l.push(u)})}else switch(n!==void 0&&l.push(o.get(n)),t){case"add":U(e)?pr(n)&&l.push(o.get("length")):(l.push(o.get(ct)),_t(e)&&l.push(o.get(er)));break;case"delete":U(e)||(l.push(o.get(ct)),_t(e)&&l.push(o.get(er)));break;case"set":_t(e)&&l.push(o.get(ct));break}vr();for(const c of l)c&&Ws(c,4);br()}function So(e,t){var n;return(n=dn.get(e))==null?void 0:n.get(t)}const Ao=fr("__proto__,__v_isRef,__isVue"),Ys=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(Tt)),Br=Ro();function Ro(){const e={};return["includes","indexOf","lastIndexOf"].forEach(t=>{e[t]=function(...n){const r=J(this);for(let i=0,o=this.length;i{e[t]=function(...n){Xe(),vr();const r=J(this)[t].apply(this,n);return br(),Qe(),r}}),e}function Oo(e){const t=J(this);return _e(t,"has",e),t.hasOwnProperty(e)}class Gs{constructor(t=!1,n=!1){this._isReadonly=t,this._shallow=n}get(t,n,r){const s=this._isReadonly,i=this._shallow;if(n==="__v_isReactive")return!s;if(n==="__v_isReadonly")return s;if(n==="__v_isShallow")return i;if(n==="__v_raw")return r===(s?i?Bo:Qs:i?Xs:Js).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(r)?t:void 0;const o=U(t);if(!s){if(o&&X(Br,n))return Reflect.get(Br,n,r);if(n==="hasOwnProperty")return Oo}const l=Reflect.get(t,n,r);return(Tt(n)?Ys.has(n):Ao(n))||(s||_e(t,"get",n),i)?l:ue(l)?o&&pr(n)?l:l.value:Z(l)?s?Sn(l):Tn(l):l}}class zs extends Gs{constructor(t=!1){super(!1,t)}set(t,n,r,s){let i=t[n];if(!this._shallow){const c=Ct(i);if(!hn(r)&&!Ct(r)&&(i=J(i),r=J(r)),!U(t)&&ue(i)&&!ue(r))return c?!1:(i.value=r,!0)}const o=U(t)&&pr(n)?Number(n)e,xn=e=>Reflect.getPrototypeOf(e);function zt(e,t,n=!1,r=!1){e=e.__v_raw;const s=J(e),i=J(t);n||(ze(t,i)&&_e(s,"get",t),_e(s,"get",i));const{has:o}=xn(s),l=r?wr:n?xr:Dt;if(o.call(s,t))return l(e.get(t));if(o.call(s,i))return l(e.get(i));e!==s&&e.get(t)}function Jt(e,t=!1){const n=this.__v_raw,r=J(n),s=J(e);return t||(ze(e,s)&&_e(r,"has",e),_e(r,"has",s)),e===s?n.has(e):n.has(e)||n.has(s)}function Xt(e,t=!1){return e=e.__v_raw,!t&&_e(J(e),"iterate",ct),Reflect.get(e,"size",e)}function kr(e){e=J(e);const t=J(this);return xn(t).has.call(t,e)||(t.add(e),He(t,"add",e,e)),this}function Kr(e,t){t=J(t);const n=J(this),{has:r,get:s}=xn(n);let i=r.call(n,e);i||(e=J(e),i=r.call(n,e));const o=s.call(n,e);return n.set(e,t),i?ze(t,o)&&He(n,"set",e,t):He(n,"add",e,t),this}function Wr(e){const t=J(this),{has:n,get:r}=xn(t);let s=n.call(t,e);s||(e=J(e),s=n.call(t,e)),r&&r.call(t,e);const i=t.delete(e);return s&&He(t,"delete",e,void 0),i}function qr(){const e=J(this),t=e.size!==0,n=e.clear();return t&&He(e,"clear",void 0,void 0),n}function Qt(e,t){return function(r,s){const i=this,o=i.__v_raw,l=J(o),c=t?wr:e?xr:Dt;return!e&&_e(l,"iterate",ct),o.forEach((u,d)=>r.call(s,c(u),c(d),i))}}function Zt(e,t,n){return function(...r){const s=this.__v_raw,i=J(s),o=_t(i),l=e==="entries"||e===Symbol.iterator&&o,c=e==="keys"&&o,u=s[e](...r),d=n?wr:t?xr:Dt;return!t&&_e(i,"iterate",c?er:ct),{next(){const{value:h,done:_}=u.next();return _?{value:h,done:_}:{value:l?[d(h[0]),d(h[1])]:d(h),done:_}},[Symbol.iterator](){return this}}}}function Ve(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function Fo(){const e={get(i){return zt(this,i)},get size(){return Xt(this)},has:Jt,add:kr,set:Kr,delete:Wr,clear:qr,forEach:Qt(!1,!1)},t={get(i){return zt(this,i,!1,!0)},get size(){return Xt(this)},has:Jt,add:kr,set:Kr,delete:Wr,clear:qr,forEach:Qt(!1,!0)},n={get(i){return zt(this,i,!0)},get size(){return Xt(this,!0)},has(i){return Jt.call(this,i,!0)},add:Ve("add"),set:Ve("set"),delete:Ve("delete"),clear:Ve("clear"),forEach:Qt(!0,!1)},r={get(i){return zt(this,i,!0,!0)},get size(){return Xt(this,!0)},has(i){return Jt.call(this,i,!0)},add:Ve("add"),set:Ve("set"),delete:Ve("delete"),clear:Ve("clear"),forEach:Qt(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(i=>{e[i]=Zt(i,!1,!1),n[i]=Zt(i,!0,!1),t[i]=Zt(i,!1,!0),r[i]=Zt(i,!0,!0)}),[e,n,t,r]}const[No,Ho,$o,Do]=Fo();function Er(e,t){const n=t?e?Do:$o:e?Ho:No;return(r,s,i)=>s==="__v_isReactive"?!e:s==="__v_isReadonly"?e:s==="__v_raw"?r:Reflect.get(X(n,s)&&s in r?n:r,s,i)}const Vo={get:Er(!1,!1)},jo={get:Er(!1,!0)},Uo={get:Er(!0,!1)},Js=new WeakMap,Xs=new WeakMap,Qs=new WeakMap,Bo=new WeakMap;function ko(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Ko(e){return e.__v_skip||!Object.isExtensible(e)?0:ko(uo(e))}function Tn(e){return Ct(e)?e:Cr(e,!1,Lo,Vo,Js)}function Wo(e){return Cr(e,!1,Po,jo,Xs)}function Sn(e){return Cr(e,!0,Mo,Uo,Qs)}function Cr(e,t,n,r,s){if(!Z(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const i=s.get(e);if(i)return i;const o=Ko(e);if(o===0)return e;const l=new Proxy(e,o===2?r:n);return s.set(e,l),l}function vt(e){return Ct(e)?vt(e.__v_raw):!!(e&&e.__v_isReactive)}function Ct(e){return!!(e&&e.__v_isReadonly)}function hn(e){return!!(e&&e.__v_isShallow)}function Zs(e){return vt(e)||Ct(e)}function J(e){const t=e&&e.__v_raw;return t?J(t):e}function Lt(e){return Object.isExtensible(e)&&fn(e,"__v_skip",!0),e}const Dt=e=>Z(e)?Tn(e):e,xr=e=>Z(e)?Sn(e):e;class ei{constructor(t,n,r,s){this._setter=n,this.dep=void 0,this.__v_isRef=!0,this.__v_isReadonly=!1,this.effect=new yr(()=>t(this._value),()=>Mt(this,this.effect._dirtyLevel===2?2:3)),this.effect.computed=this,this.effect.active=this._cacheable=!s,this.__v_isReadonly=r}get value(){const t=J(this);return(!t._cacheable||t.effect.dirty)&&ze(t._value,t._value=t.effect.run())&&Mt(t,4),Tr(t),t.effect._dirtyLevel>=2&&Mt(t,2),t._value}set value(t){this._setter(t)}get _dirty(){return this.effect.dirty}set _dirty(t){this.effect.dirty=t}}function qo(e,t,n=!1){let r,s;const i=W(e);return i?(r=e,s=be):(r=e.get,s=e.set),new ei(r,s,i||!s,n)}function Tr(e){var t;Ye&<&&(e=J(e),Ks(lt,(t=e.dep)!=null?t:e.dep=qs(()=>e.dep=void 0,e instanceof ei?e:void 0)))}function Mt(e,t=4,n){e=J(e);const r=e.dep;r&&Ws(r,t)}function ue(e){return!!(e&&e.__v_isRef===!0)}function de(e){return ni(e,!1)}function ti(e){return ni(e,!0)}function ni(e,t){return ue(e)?e:new Yo(e,t)}class Yo{constructor(t,n){this.__v_isShallow=n,this.dep=void 0,this.__v_isRef=!0,this._rawValue=n?t:J(t),this._value=n?t:Dt(t)}get value(){return Tr(this),this._value}set value(t){const n=this.__v_isShallow||hn(t)||Ct(t);t=n?t:J(t),ze(t,this._rawValue)&&(this._rawValue=t,this._value=n?t:Dt(t),Mt(this,4))}}function ri(e){return ue(e)?e.value:e}const Go={get:(e,t,n)=>ri(Reflect.get(e,t,n)),set:(e,t,n,r)=>{const s=e[t];return ue(s)&&!ue(n)?(s.value=n,!0):Reflect.set(e,t,n,r)}};function si(e){return vt(e)?e:new Proxy(e,Go)}class zo{constructor(t){this.dep=void 0,this.__v_isRef=!0;const{get:n,set:r}=t(()=>Tr(this),()=>Mt(this));this._get=n,this._set=r}get value(){return this._get()}set value(t){this._set(t)}}function Jo(e){return new zo(e)}class Xo{constructor(t,n,r){this._object=t,this._key=n,this._defaultValue=r,this.__v_isRef=!0}get value(){const t=this._object[this._key];return t===void 0?this._defaultValue:t}set value(t){this._object[this._key]=t}get dep(){return So(J(this._object),this._key)}}class Qo{constructor(t){this._getter=t,this.__v_isRef=!0,this.__v_isReadonly=!0}get value(){return this._getter()}}function Zo(e,t,n){return ue(e)?e:W(e)?new Qo(e):Z(e)&&arguments.length>1?el(e,t,n):de(e)}function el(e,t,n){const r=e[t];return ue(r)?r:new Xo(e,t,n)}/** +* @vue/runtime-core v3.4.19 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/const Pt=[];function et(e,...t){Xe();const n=Pt.length?Pt[Pt.length-1].component:null,r=n&&n.appContext.config.warnHandler,s=tl();if(r)$e(r,n,11,[e+t.join(""),n&&n.proxy,s.map(({vnode:i})=>`at <${Ui(n,i.type)}>`).join(` +`),s]);else{const i=[`[Vue warn]: ${e}`,...t];s.length&&i.push(` +`,...nl(s)),console.warn(...i)}Qe()}function tl(){let e=Pt[Pt.length-1];if(!e)return[];const t=[];for(;e;){const n=t[0];n&&n.vnode===e?n.recurseCount++:t.push({vnode:e,recurseCount:0});const r=e.component&&e.component.parent;e=r&&r.vnode}return t}function nl(e){const t=[];return e.forEach((n,r)=>{t.push(...r===0?[]:[` +`],...rl(n))}),t}function rl({vnode:e,recurseCount:t}){const n=t>0?`... (${t} recursive calls)`:"",r=e.component?e.component.parent==null:!1,s=` at <${Ui(e.component,e.type,r)}`,i=">"+n;return e.props?[s,...sl(e.props),i]:[s+i]}function sl(e){const t=[],n=Object.keys(e);return n.slice(0,3).forEach(r=>{t.push(...ii(r,e[r]))}),n.length>3&&t.push(" ..."),t}function ii(e,t,n){return re(t)?(t=JSON.stringify(t),n?t:[`${e}=${t}`]):typeof t=="number"||typeof t=="boolean"||t==null?n?t:[`${e}=${t}`]:ue(t)?(t=ii(e,J(t.value),!0),n?t:[`${e}=Ref<`,t,">"]):W(t)?[`${e}=fn${t.name?`<${t.name}>`:""}`]:(t=J(t),n?t:[`${e}=`,t])}function $e(e,t,n,r){try{return r?e(...r):e()}catch(s){Kt(s,t,n)}}function Te(e,t,n,r){if(W(e)){const i=$e(e,t,n,r);return i&&Hs(i)&&i.catch(o=>{Kt(o,t,n)}),i}const s=[];for(let i=0;i>>1,s=fe[r],i=jt(s);iPe&&fe.splice(t,1)}function cl(e){U(e)?bt.push(...e):(!ke||!ke.includes(e,e.allowRecurse?st+1:st))&&bt.push(e),li()}function Yr(e,t,n=Vt?Pe+1:0){for(;njt(n)-jt(r));if(bt.length=0,ke){ke.push(...t);return}for(ke=t,st=0;ste.id==null?1/0:e.id,al=(e,t)=>{const n=jt(e)-jt(t);if(n===0){if(e.pre&&!t.pre)return-1;if(t.pre&&!e.pre)return 1}return n};function ci(e){tr=!1,Vt=!0,fe.sort(al);const t=be;try{for(Pe=0;Pere(w)?w.trim():w)),h&&(s=n.map(po))}let l,c=r[l=cn(t)]||r[l=cn(Fe(t))];!c&&i&&(c=r[l=cn(ft(t))]),c&&Te(c,e,6,s);const u=r[l+"Once"];if(u){if(!e.emitted)e.emitted={};else if(e.emitted[l])return;e.emitted[l]=!0,Te(u,e,6,s)}}function ai(e,t,n=!1){const r=t.emitsCache,s=r.get(e);if(s!==void 0)return s;const i=e.emits;let o={},l=!1;if(!W(e)){const c=u=>{const d=ai(u,t,!0);d&&(l=!0,oe(o,d))};!n&&t.mixins.length&&t.mixins.forEach(c),e.extends&&c(e.extends),e.mixins&&e.mixins.forEach(c)}return!i&&!l?(Z(e)&&r.set(e,null),null):(U(i)?i.forEach(c=>o[c]=null):oe(o,i),Z(e)&&r.set(e,o),o)}function On(e,t){return!e||!kt(t)?!1:(t=t.slice(2).replace(/Once$/,""),X(e,t[0].toLowerCase()+t.slice(1))||X(e,ft(t))||X(e,t))}let he=null,In=null;function gn(e){const t=he;return he=e,In=e&&e.type.__scopeId||null,t}function Va(e){In=e}function ja(){In=null}function fl(e,t=he,n){if(!t||e._n)return e;const r=(...s)=>{r._d&&is(-1);const i=gn(t);let o;try{o=e(...s)}finally{gn(i),r._d&&is(1)}return o};return r._n=!0,r._c=!0,r._d=!0,r}function Un(e){const{type:t,vnode:n,proxy:r,withProxy:s,props:i,propsOptions:[o],slots:l,attrs:c,emit:u,render:d,renderCache:h,data:_,setupState:w,ctx:x,inheritAttrs:L}=e;let N,B;const k=gn(e);try{if(n.shapeFlag&4){const m=s||r,F=m;N=Ae(d.call(F,m,h,i,w,_,x)),B=c}else{const m=t;N=Ae(m.length>1?m(i,{attrs:c,slots:l,emit:u}):m(i,null)),B=t.props?c:dl(c)}}catch(m){$t.length=0,Kt(m,e,1),N=se(we)}let p=N;if(B&&L!==!1){const m=Object.keys(B),{shapeFlag:F}=p;m.length&&F&7&&(o&&m.some(dr)&&(B=hl(B,o)),p=Je(p,B))}return n.dirs&&(p=Je(p),p.dirs=p.dirs?p.dirs.concat(n.dirs):n.dirs),n.transition&&(p.transition=n.transition),N=p,gn(k),N}const dl=e=>{let t;for(const n in e)(n==="class"||n==="style"||kt(n))&&((t||(t={}))[n]=e[n]);return t},hl=(e,t)=>{const n={};for(const r in e)(!dr(r)||!(r.slice(9)in t))&&(n[r]=e[r]);return n};function pl(e,t,n){const{props:r,children:s,component:i}=e,{props:o,children:l,patchFlag:c}=t,u=i.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return r?Gr(r,o,u):!!o;if(c&8){const d=t.dynamicProps;for(let h=0;he.__isSuspense;function di(e,t){t&&t.pendingBranch?U(e)?t.effects.push(...e):t.effects.push(e):cl(e)}const _l=Symbol.for("v-scx"),yl=()=>Et(_l);function hi(e,t){return Ln(e,null,t)}function ka(e,t){return Ln(e,null,{flush:"post"})}const en={};function Ge(e,t,n){return Ln(e,t,n)}function Ln(e,t,{immediate:n,deep:r,flush:s,once:i,onTrack:o,onTrigger:l}=ne){if(t&&i){const M=t;t=(...j)=>{M(...j),F()}}const c=ce,u=M=>r===!0?M:gt(M,r===!1?1:void 0);let d,h=!1,_=!1;if(ue(e)?(d=()=>e.value,h=hn(e)):vt(e)?(d=()=>u(e),h=!0):U(e)?(_=!0,h=e.some(M=>vt(M)||hn(M)),d=()=>e.map(M=>{if(ue(M))return M.value;if(vt(M))return u(M);if(W(M))return $e(M,c,2)})):W(e)?t?d=()=>$e(e,c,2):d=()=>(w&&w(),Te(e,c,3,[x])):d=be,t&&r){const M=d;d=()=>gt(M())}let w,x=M=>{w=p.onStop=()=>{$e(M,c,4),w=p.onStop=void 0}},L;if(Yt)if(x=be,t?n&&Te(t,c,3,[d(),_?[]:void 0,x]):d(),s==="sync"){const M=yl();L=M.__watcherHandles||(M.__watcherHandles=[])}else return be;let N=_?new Array(e.length).fill(en):en;const B=()=>{if(!(!p.active||!p.dirty))if(t){const M=p.run();(r||h||(_?M.some((j,I)=>ze(j,N[I])):ze(M,N)))&&(w&&w(),Te(t,c,3,[M,N===en?void 0:_&&N[0]===en?[]:N,x]),N=M)}else p.run()};B.allowRecurse=!!t;let k;s==="sync"?k=B:s==="post"?k=()=>ge(B,c&&c.suspense):(B.pre=!0,c&&(B.id=c.uid),k=()=>Rn(B));const p=new yr(d,be,k),m=Us(),F=()=>{p.stop(),m&&hr(m.effects,p)};return t?n?B():N=p.run():s==="post"?ge(p.run.bind(p),c&&c.suspense):p.run(),L&&L.push(F),F}function vl(e,t,n){const r=this.proxy,s=re(e)?e.includes(".")?pi(r,e):()=>r[e]:e.bind(r,r);let i;W(t)?i=t:(i=t.handler,n=t);const o=qt(this),l=Ln(s,i.bind(r),n);return o(),l}function pi(e,t){const n=t.split(".");return()=>{let r=e;for(let s=0;s0){if(n>=t)return e;n++}if(r=r||new Set,r.has(e))return e;if(r.add(e),ue(e))gt(e.value,t,n,r);else if(U(e))for(let s=0;s{gt(s,t,n,r)});else if(Ds(e))for(const s in e)gt(e[s],t,n,r);return e}function Me(e,t,n,r){const s=e.dirs,i=t&&t.dirs;for(let o=0;o{e.isMounted=!0}),vi(()=>{e.isUnmounting=!0}),e}const Ee=[Function,Array],gi={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:Ee,onEnter:Ee,onAfterEnter:Ee,onEnterCancelled:Ee,onBeforeLeave:Ee,onLeave:Ee,onAfterLeave:Ee,onLeaveCancelled:Ee,onBeforeAppear:Ee,onAppear:Ee,onAfterAppear:Ee,onAppearCancelled:Ee},wl={name:"BaseTransition",props:gi,setup(e,{slots:t}){const n=Nn(),r=bl();let s;return()=>{const i=t.default&&_i(t.default(),!0);if(!i||!i.length)return;let o=i[0];if(i.length>1){for(const L of i)if(L.type!==we){o=L;break}}const l=J(e),{mode:c}=l;if(r.isLeaving)return Bn(o);const u=Jr(o);if(!u)return Bn(o);const d=nr(u,l,r,n);rr(u,d);const h=n.subTree,_=h&&Jr(h);let w=!1;const{getTransitionKey:x}=u.type;if(x){const L=x();s===void 0?s=L:L!==s&&(s=L,w=!0)}if(_&&_.type!==we&&(!it(u,_)||w)){const L=nr(_,l,r,n);if(rr(_,L),c==="out-in")return r.isLeaving=!0,L.afterLeave=()=>{r.isLeaving=!1,n.update.active!==!1&&(n.effect.dirty=!0,n.update())},Bn(o);c==="in-out"&&u.type!==we&&(L.delayLeave=(N,B,k)=>{const p=mi(r,_);p[String(_.key)]=_,N[Ke]=()=>{B(),N[Ke]=void 0,delete d.delayedLeave},d.delayedLeave=k})}return o}}},El=wl;function mi(e,t){const{leavingVNodes:n}=e;let r=n.get(t.type);return r||(r=Object.create(null),n.set(t.type,r)),r}function nr(e,t,n,r){const{appear:s,mode:i,persisted:o=!1,onBeforeEnter:l,onEnter:c,onAfterEnter:u,onEnterCancelled:d,onBeforeLeave:h,onLeave:_,onAfterLeave:w,onLeaveCancelled:x,onBeforeAppear:L,onAppear:N,onAfterAppear:B,onAppearCancelled:k}=t,p=String(e.key),m=mi(n,e),F=(I,S)=>{I&&Te(I,r,9,S)},M=(I,S)=>{const A=S[1];F(I,S),U(I)?I.every(q=>q.length<=1)&&A():I.length<=1&&A()},j={mode:i,persisted:o,beforeEnter(I){let S=l;if(!n.isMounted)if(s)S=L||l;else return;I[Ke]&&I[Ke](!0);const A=m[p];A&&it(e,A)&&A.el[Ke]&&A.el[Ke](),F(S,[I])},enter(I){let S=c,A=u,q=d;if(!n.isMounted)if(s)S=N||c,A=B||u,q=k||d;else return;let R=!1;const Y=I[tn]=ee=>{R||(R=!0,ee?F(q,[I]):F(A,[I]),j.delayedLeave&&j.delayedLeave(),I[tn]=void 0)};S?M(S,[I,Y]):Y()},leave(I,S){const A=String(e.key);if(I[tn]&&I[tn](!0),n.isUnmounting)return S();F(h,[I]);let q=!1;const R=I[Ke]=Y=>{q||(q=!0,S(),Y?F(x,[I]):F(w,[I]),I[Ke]=void 0,m[A]===e&&delete m[A])};m[A]=e,_?M(_,[I,R]):R()},clone(I){return nr(I,t,n,r)}};return j}function Bn(e){if(Wt(e))return e=Je(e),e.children=null,e}function Jr(e){return Wt(e)?e.children?e.children[0]:void 0:e}function rr(e,t){e.shapeFlag&6&&e.component?rr(e.component.subTree,t):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function _i(e,t=!1,n){let r=[],s=0;for(let i=0;i1)for(let i=0;ioe({name:e.name},t,{setup:e}))():e}const wt=e=>!!e.type.__asyncLoader;/*! #__NO_SIDE_EFFECTS__ */function Ka(e){W(e)&&(e={loader:e});const{loader:t,loadingComponent:n,errorComponent:r,delay:s=200,timeout:i,suspensible:o=!0,onError:l}=e;let c=null,u,d=0;const h=()=>(d++,c=null,_()),_=()=>{let w;return c||(w=c=t().catch(x=>{if(x=x instanceof Error?x:new Error(String(x)),l)return new Promise((L,N)=>{l(x,()=>L(h()),()=>N(x),d+1)});throw x}).then(x=>w!==c&&c?c:(x&&(x.__esModule||x[Symbol.toStringTag]==="Module")&&(x=x.default),u=x,x)))};return Rr({name:"AsyncComponentWrapper",__asyncLoader:_,get __asyncResolved(){return u},setup(){const w=ce;if(u)return()=>kn(u,w);const x=k=>{c=null,Kt(k,w,13,!r)};if(o&&w.suspense||Yt)return _().then(k=>()=>kn(k,w)).catch(k=>(x(k),()=>r?se(r,{error:k}):null));const L=de(!1),N=de(),B=de(!!s);return s&&setTimeout(()=>{B.value=!1},s),i!=null&&setTimeout(()=>{if(!L.value&&!N.value){const k=new Error(`Async component timed out after ${i}ms.`);x(k),N.value=k}},i),_().then(()=>{L.value=!0,w.parent&&Wt(w.parent.vnode)&&(w.parent.effect.dirty=!0,Rn(w.parent.update))}).catch(k=>{x(k),N.value=k}),()=>{if(L.value&&u)return kn(u,w);if(N.value&&r)return se(r,{error:N.value});if(n&&!B.value)return se(n)}}})}function kn(e,t){const{ref:n,props:r,children:s,ce:i}=t.vnode,o=se(e,r,s);return o.ref=n,o.ce=i,delete t.vnode.ce,o}const Wt=e=>e.type.__isKeepAlive;function Cl(e,t){yi(e,"a",t)}function xl(e,t){yi(e,"da",t)}function yi(e,t,n=ce){const r=e.__wdc||(e.__wdc=()=>{let s=n;for(;s;){if(s.isDeactivated)return;s=s.parent}return e()});if(Mn(t,r,n),n){let s=n.parent;for(;s&&s.parent;)Wt(s.parent.vnode)&&Tl(r,t,n,s),s=s.parent}}function Tl(e,t,n,r){const s=Mn(t,e,r,!0);Pn(()=>{hr(r[t],s)},n)}function Mn(e,t,n=ce,r=!1){if(n){const s=n[e]||(n[e]=[]),i=t.__weh||(t.__weh=(...o)=>{if(n.isUnmounted)return;Xe();const l=qt(n),c=Te(t,n,e,o);return l(),Qe(),c});return r?s.unshift(i):s.push(i),i}}const De=e=>(t,n=ce)=>(!Yt||e==="sp")&&Mn(e,(...r)=>t(...r),n),Sl=De("bm"),St=De("m"),Al=De("bu"),Rl=De("u"),vi=De("bum"),Pn=De("um"),Ol=De("sp"),Il=De("rtg"),Ll=De("rtc");function Ml(e,t=ce){Mn("ec",e,t)}function Wa(e,t,n,r){let s;const i=n&&n[r];if(U(e)||re(e)){s=new Array(e.length);for(let o=0,l=e.length;ot(o,l,void 0,i&&i[l]));else{const o=Object.keys(e);s=new Array(o.length);for(let l=0,c=o.length;lyn(t)?!(t.type===we||t.type===me&&!bi(t.children)):!0)?e:null}function Ya(e,t){const n={};for(const r in e)n[t&&/[A-Z]/.test(r)?`on:${r}`:cn(r)]=e[r];return n}const sr=e=>e?$i(e)?Mr(e)||e.proxy:sr(e.parent):null,Ft=oe(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>sr(e.parent),$root:e=>sr(e.root),$emit:e=>e.emit,$options:e=>Or(e),$forceUpdate:e=>e.f||(e.f=()=>{e.effect.dirty=!0,Rn(e.update)}),$nextTick:e=>e.n||(e.n=An.bind(e.proxy)),$watch:e=>vl.bind(e)}),Kn=(e,t)=>e!==ne&&!e.__isScriptSetup&&X(e,t),Pl={get({_:e},t){const{ctx:n,setupState:r,data:s,props:i,accessCache:o,type:l,appContext:c}=e;let u;if(t[0]!=="$"){const w=o[t];if(w!==void 0)switch(w){case 1:return r[t];case 2:return s[t];case 4:return n[t];case 3:return i[t]}else{if(Kn(r,t))return o[t]=1,r[t];if(s!==ne&&X(s,t))return o[t]=2,s[t];if((u=e.propsOptions[0])&&X(u,t))return o[t]=3,i[t];if(n!==ne&&X(n,t))return o[t]=4,n[t];ir&&(o[t]=0)}}const d=Ft[t];let h,_;if(d)return t==="$attrs"&&_e(e,"get",t),d(e);if((h=l.__cssModules)&&(h=h[t]))return h;if(n!==ne&&X(n,t))return o[t]=4,n[t];if(_=c.config.globalProperties,X(_,t))return _[t]},set({_:e},t,n){const{data:r,setupState:s,ctx:i}=e;return Kn(s,t)?(s[t]=n,!0):r!==ne&&X(r,t)?(r[t]=n,!0):X(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:r,appContext:s,propsOptions:i}},o){let l;return!!n[o]||e!==ne&&X(e,o)||Kn(t,o)||(l=i[0])&&X(l,o)||X(r,o)||X(Ft,o)||X(s.config.globalProperties,o)},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:X(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function Ga(){return Fl().slots}function Fl(){const e=Nn();return e.setupContext||(e.setupContext=Vi(e))}function Xr(e){return U(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let ir=!0;function Nl(e){const t=Or(e),n=e.proxy,r=e.ctx;ir=!1,t.beforeCreate&&Qr(t.beforeCreate,e,"bc");const{data:s,computed:i,methods:o,watch:l,provide:c,inject:u,created:d,beforeMount:h,mounted:_,beforeUpdate:w,updated:x,activated:L,deactivated:N,beforeDestroy:B,beforeUnmount:k,destroyed:p,unmounted:m,render:F,renderTracked:M,renderTriggered:j,errorCaptured:I,serverPrefetch:S,expose:A,inheritAttrs:q,components:R,directives:Y,filters:ee}=t;if(u&&Hl(u,r,null),o)for(const z in o){const $=o[z];W($)&&(r[z]=$.bind(n))}if(s){const z=s.call(n,n);Z(z)&&(e.data=Tn(z))}if(ir=!0,i)for(const z in i){const $=i[z],Oe=W($)?$.bind(n,n):W($.get)?$.get.bind(n,n):be,dt=!W($)&&W($.set)?$.set.bind(n):be,Ze=ie({get:Oe,set:dt});Object.defineProperty(r,z,{enumerable:!0,configurable:!0,get:()=>Ze.value,set:Ie=>Ze.value=Ie})}if(l)for(const z in l)wi(l[z],r,n,z);if(c){const z=W(c)?c.call(n):c;Reflect.ownKeys(z).forEach($=>{Bl($,z[$])})}d&&Qr(d,e,"c");function D(z,$){U($)?$.forEach(Oe=>z(Oe.bind(n))):$&&z($.bind(n))}if(D(Sl,h),D(St,_),D(Al,w),D(Rl,x),D(Cl,L),D(xl,N),D(Ml,I),D(Ll,M),D(Il,j),D(vi,k),D(Pn,m),D(Ol,S),U(A))if(A.length){const z=e.exposed||(e.exposed={});A.forEach($=>{Object.defineProperty(z,$,{get:()=>n[$],set:Oe=>n[$]=Oe})})}else e.exposed||(e.exposed={});F&&e.render===be&&(e.render=F),q!=null&&(e.inheritAttrs=q),R&&(e.components=R),Y&&(e.directives=Y)}function Hl(e,t,n=be){U(e)&&(e=or(e));for(const r in e){const s=e[r];let i;Z(s)?"default"in s?i=Et(s.from||r,s.default,!0):i=Et(s.from||r):i=Et(s),ue(i)?Object.defineProperty(t,r,{enumerable:!0,configurable:!0,get:()=>i.value,set:o=>i.value=o}):t[r]=i}}function Qr(e,t,n){Te(U(e)?e.map(r=>r.bind(t.proxy)):e.bind(t.proxy),t,n)}function wi(e,t,n,r){const s=r.includes(".")?pi(n,r):()=>n[r];if(re(e)){const i=t[e];W(i)&&Ge(s,i)}else if(W(e))Ge(s,e.bind(n));else if(Z(e))if(U(e))e.forEach(i=>wi(i,t,n,r));else{const i=W(e.handler)?e.handler.bind(n):t[e.handler];W(i)&&Ge(s,i,e)}}function Or(e){const t=e.type,{mixins:n,extends:r}=t,{mixins:s,optionsCache:i,config:{optionMergeStrategies:o}}=e.appContext,l=i.get(t);let c;return l?c=l:!s.length&&!n&&!r?c=t:(c={},s.length&&s.forEach(u=>mn(c,u,o,!0)),mn(c,t,o)),Z(t)&&i.set(t,c),c}function mn(e,t,n,r=!1){const{mixins:s,extends:i}=t;i&&mn(e,i,n,!0),s&&s.forEach(o=>mn(e,o,n,!0));for(const o in t)if(!(r&&o==="expose")){const l=$l[o]||n&&n[o];e[o]=l?l(e[o],t[o]):t[o]}return e}const $l={data:Zr,props:es,emits:es,methods:It,computed:It,beforeCreate:pe,created:pe,beforeMount:pe,mounted:pe,beforeUpdate:pe,updated:pe,beforeDestroy:pe,beforeUnmount:pe,destroyed:pe,unmounted:pe,activated:pe,deactivated:pe,errorCaptured:pe,serverPrefetch:pe,components:It,directives:It,watch:Vl,provide:Zr,inject:Dl};function Zr(e,t){return t?e?function(){return oe(W(e)?e.call(this,this):e,W(t)?t.call(this,this):t)}:t:e}function Dl(e,t){return It(or(e),or(t))}function or(e){if(U(e)){const t={};for(let n=0;n1)return n&&W(t)?t.call(r&&r.proxy):t}}function kl(e,t,n,r=!1){const s={},i={};fn(i,Fn,1),e.propsDefaults=Object.create(null),Ci(e,t,s,i);for(const o in e.propsOptions[0])o in s||(s[o]=void 0);n?e.props=r?s:Wo(s):e.type.props?e.props=s:e.props=i,e.attrs=i}function Kl(e,t,n,r){const{props:s,attrs:i,vnode:{patchFlag:o}}=e,l=J(s),[c]=e.propsOptions;let u=!1;if((r||o>0)&&!(o&16)){if(o&8){const d=e.vnode.dynamicProps;for(let h=0;h{c=!0;const[_,w]=xi(h,t,!0);oe(o,_),w&&l.push(...w)};!n&&t.mixins.length&&t.mixins.forEach(d),e.extends&&d(e.extends),e.mixins&&e.mixins.forEach(d)}if(!i&&!c)return Z(e)&&r.set(e,mt),mt;if(U(i))for(let d=0;d-1,w[1]=L<0||x-1||X(w,"default"))&&l.push(h)}}}const u=[o,l];return Z(e)&&r.set(e,u),u}function ts(e){return e[0]!=="$"&&!yt(e)}function ns(e){return e===null?"null":typeof e=="function"?e.name||"":typeof e=="object"&&e.constructor&&e.constructor.name||""}function rs(e,t){return ns(e)===ns(t)}function ss(e,t){return U(t)?t.findIndex(n=>rs(n,e)):W(t)&&rs(t,e)?0:-1}const Ti=e=>e[0]==="_"||e==="$stable",Ir=e=>U(e)?e.map(Ae):[Ae(e)],Wl=(e,t,n)=>{if(t._n)return t;const r=fl((...s)=>Ir(t(...s)),n);return r._c=!1,r},Si=(e,t,n)=>{const r=e._ctx;for(const s in e){if(Ti(s))continue;const i=e[s];if(W(i))t[s]=Wl(s,i,r);else if(i!=null){const o=Ir(i);t[s]=()=>o}}},Ai=(e,t)=>{const n=Ir(t);e.slots.default=()=>n},ql=(e,t)=>{if(e.vnode.shapeFlag&32){const n=t._;n?(e.slots=J(t),fn(t,"_",n)):Si(t,e.slots={})}else e.slots={},t&&Ai(e,t);fn(e.slots,Fn,1)},Yl=(e,t,n)=>{const{vnode:r,slots:s}=e;let i=!0,o=ne;if(r.shapeFlag&32){const l=t._;l?n&&l===1?i=!1:(oe(s,t),!n&&l===1&&delete s._):(i=!t.$stable,Si(t,s)),o=t}else t&&(Ai(e,t),o={default:1});if(i)for(const l in s)!Ti(l)&&o[l]==null&&delete s[l]};function _n(e,t,n,r,s=!1){if(U(e)){e.forEach((_,w)=>_n(_,t&&(U(t)?t[w]:t),n,r,s));return}if(wt(r)&&!s)return;const i=r.shapeFlag&4?Mr(r.component)||r.component.proxy:r.el,o=s?null:i,{i:l,r:c}=e,u=t&&t.r,d=l.refs===ne?l.refs={}:l.refs,h=l.setupState;if(u!=null&&u!==c&&(re(u)?(d[u]=null,X(h,u)&&(h[u]=null)):ue(u)&&(u.value=null)),W(c))$e(c,l,12,[o,d]);else{const _=re(c),w=ue(c);if(_||w){const x=()=>{if(e.f){const L=_?X(h,c)?h[c]:d[c]:c.value;s?U(L)&&hr(L,i):U(L)?L.includes(i)||L.push(i):_?(d[c]=[i],X(h,c)&&(h[c]=d[c])):(c.value=[i],e.k&&(d[e.k]=c.value))}else _?(d[c]=o,X(h,c)&&(h[c]=o)):w&&(c.value=o,e.k&&(d[e.k]=o))};o?(x.id=-1,ge(x,n)):x()}}}let je=!1;const Gl=e=>e.namespaceURI.includes("svg")&&e.tagName!=="foreignObject",zl=e=>e.namespaceURI.includes("MathML"),nn=e=>{if(Gl(e))return"svg";if(zl(e))return"mathml"},Rt=e=>e.nodeType===8;function Jl(e){const{mt:t,p:n,o:{patchProp:r,createText:s,nextSibling:i,parentNode:o,remove:l,insert:c,createComment:u}}=e,d=(p,m)=>{if(!m.hasChildNodes()){__VUE_PROD_HYDRATION_MISMATCH_DETAILS__&&et("Attempting to hydrate existing markup but container is empty. Performing full mount instead."),n(null,p,m),pn(),m._vnode=p;return}je=!1,h(m.firstChild,p,null,null,null),pn(),m._vnode=p,je&&console.error("Hydration completed but contains mismatches.")},h=(p,m,F,M,j,I=!1)=>{const S=Rt(p)&&p.data==="[",A=()=>L(p,m,F,M,j,S),{type:q,ref:R,shapeFlag:Y,patchFlag:ee}=m;let ae=p.nodeType;m.el=p,ee===-2&&(I=!1,m.dynamicChildren=null);let D=null;switch(q){case xt:ae!==3?m.children===""?(c(m.el=s(""),o(p),p),D=p):D=A():(p.data!==m.children&&(je=!0,__VUE_PROD_HYDRATION_MISMATCH_DETAILS__&&et("Hydration text mismatch in",p.parentNode,` + - rendered on server: ${JSON.stringify(p.data)} + - expected on client: ${JSON.stringify(m.children)}`),p.data=m.children),D=i(p));break;case we:k(p)?(D=i(p),B(m.el=p.content.firstChild,p,F)):ae!==8||S?D=A():D=i(p);break;case Ht:if(S&&(p=i(p),ae=p.nodeType),ae===1||ae===3){D=p;const z=!m.children.length;for(let $=0;${I=I||!!m.dynamicChildren;const{type:S,props:A,patchFlag:q,shapeFlag:R,dirs:Y,transition:ee}=m,ae=S==="input"||S==="option";if(ae||q!==-1){Y&&Me(m,null,F,"created");let D=!1;if(k(p)){D=Ri(M,ee)&&F&&F.vnode.props&&F.vnode.props.appear;const $=p.content.firstChild;D&&ee.beforeEnter($),B($,p,F),m.el=p=$}if(R&16&&!(A&&(A.innerHTML||A.textContent))){let $=w(p.firstChild,m,p,F,M,j,I),Oe=!1;for(;$;){je=!0,__VUE_PROD_HYDRATION_MISMATCH_DETAILS__&&!Oe&&(et("Hydration children mismatch on",p,` +Server rendered element contains more child nodes than client vdom.`),Oe=!0);const dt=$;$=$.nextSibling,l(dt)}}else R&8&&p.textContent!==m.children&&(je=!0,__VUE_PROD_HYDRATION_MISMATCH_DETAILS__&&et("Hydration text content mismatch on",p,` + - rendered on server: ${p.textContent} + - expected on client: ${m.children}`),p.textContent=m.children);if(A)if(ae||!I||q&48)for(const $ in A)(ae&&($.endsWith("value")||$==="indeterminate")||kt($)&&!yt($)||$[0]===".")&&r(p,$,null,A[$],void 0,void 0,F);else A.onClick&&r(p,"onClick",null,A.onClick,void 0,void 0,F);let z;(z=A&&A.onVnodeBeforeMount)&&Ce(z,F,m),Y&&Me(m,null,F,"beforeMount"),((z=A&&A.onVnodeMounted)||Y||D)&&di(()=>{z&&Ce(z,F,m),D&&ee.enter(p),Y&&Me(m,null,F,"mounted")},M)}return p.nextSibling},w=(p,m,F,M,j,I,S)=>{S=S||!!m.dynamicChildren;const A=m.children,q=A.length;let R=!1;for(let Y=0;Y{const{slotScopeIds:S}=m;S&&(j=j?j.concat(S):S);const A=o(p),q=w(i(p),m,A,F,M,j,I);return q&&Rt(q)&&q.data==="]"?i(m.anchor=q):(je=!0,c(m.anchor=u("]"),A,q),q)},L=(p,m,F,M,j,I)=>{if(je=!0,__VUE_PROD_HYDRATION_MISMATCH_DETAILS__&&et(`Hydration node mismatch: +- rendered on server:`,p,p.nodeType===3?"(text)":Rt(p)&&p.data==="["?"(start of fragment)":"",` +- expected on client:`,m.type),m.el=null,I){const q=N(p);for(;;){const R=i(p);if(R&&R!==q)l(R);else break}}const S=i(p),A=o(p);return l(p),n(null,m,A,S,F,M,nn(A),j),S},N=(p,m="[",F="]")=>{let M=0;for(;p;)if(p=i(p),p&&Rt(p)&&(p.data===m&&M++,p.data===F)){if(M===0)return i(p);M--}return p},B=(p,m,F)=>{const M=m.parentNode;M&&M.replaceChild(p,m);let j=F;for(;j;)j.vnode.el===m&&(j.vnode.el=j.subTree.el=p),j=j.parent},k=p=>p.nodeType===1&&p.tagName.toLowerCase()==="template";return[d,h]}function Xl(){typeof __VUE_PROD_HYDRATION_MISMATCH_DETAILS__!="boolean"&&(gr().__VUE_PROD_HYDRATION_MISMATCH_DETAILS__=!1)}const ge=di;function Ql(e){return Zl(e,Jl)}function Zl(e,t){Xl();const n=gr();n.__VUE__=!0;const{insert:r,remove:s,patchProp:i,createElement:o,createText:l,createComment:c,setText:u,setElementText:d,parentNode:h,nextSibling:_,setScopeId:w=be,insertStaticContent:x}=e,L=(a,f,g,y=null,v=null,C=null,O=void 0,E=null,T=!!f.dynamicChildren)=>{if(a===f)return;a&&!it(a,f)&&(y=Gt(a),Ie(a,v,C,!0),a=null),f.patchFlag===-2&&(T=!1,f.dynamicChildren=null);const{type:b,ref:P,shapeFlag:V}=f;switch(b){case xt:N(a,f,g,y);break;case we:B(a,f,g,y);break;case Ht:a==null&&k(f,g,y,O);break;case me:R(a,f,g,y,v,C,O,E,T);break;default:V&1?F(a,f,g,y,v,C,O,E,T):V&6?Y(a,f,g,y,v,C,O,E,T):(V&64||V&128)&&b.process(a,f,g,y,v,C,O,E,T,ht)}P!=null&&v&&_n(P,a&&a.ref,C,f||a,!f)},N=(a,f,g,y)=>{if(a==null)r(f.el=l(f.children),g,y);else{const v=f.el=a.el;f.children!==a.children&&u(v,f.children)}},B=(a,f,g,y)=>{a==null?r(f.el=c(f.children||""),g,y):f.el=a.el},k=(a,f,g,y)=>{[a.el,a.anchor]=x(a.children,f,g,y,a.el,a.anchor)},p=({el:a,anchor:f},g,y)=>{let v;for(;a&&a!==f;)v=_(a),r(a,g,y),a=v;r(f,g,y)},m=({el:a,anchor:f})=>{let g;for(;a&&a!==f;)g=_(a),s(a),a=g;s(f)},F=(a,f,g,y,v,C,O,E,T)=>{f.type==="svg"?O="svg":f.type==="math"&&(O="mathml"),a==null?M(f,g,y,v,C,O,E,T):S(a,f,v,C,O,E,T)},M=(a,f,g,y,v,C,O,E)=>{let T,b;const{props:P,shapeFlag:V,transition:H,dirs:K}=a;if(T=a.el=o(a.type,C,P&&P.is,P),V&8?d(T,a.children):V&16&&I(a.children,T,null,y,v,Wn(a,C),O,E),K&&Me(a,null,y,"created"),j(T,a,a.scopeId,O,y),P){for(const Q in P)Q!=="value"&&!yt(Q)&&i(T,Q,null,P[Q],C,a.children,y,v,Ne);"value"in P&&i(T,"value",null,P.value,C),(b=P.onVnodeBeforeMount)&&Ce(b,y,a)}K&&Me(a,null,y,"beforeMount");const G=Ri(v,H);G&&H.beforeEnter(T),r(T,f,g),((b=P&&P.onVnodeMounted)||G||K)&&ge(()=>{b&&Ce(b,y,a),G&&H.enter(T),K&&Me(a,null,y,"mounted")},v)},j=(a,f,g,y,v)=>{if(g&&w(a,g),y)for(let C=0;C{for(let b=T;b{const E=f.el=a.el;let{patchFlag:T,dynamicChildren:b,dirs:P}=f;T|=a.patchFlag&16;const V=a.props||ne,H=f.props||ne;let K;if(g&&tt(g,!1),(K=H.onVnodeBeforeUpdate)&&Ce(K,g,f,a),P&&Me(f,a,g,"beforeUpdate"),g&&tt(g,!0),b?A(a.dynamicChildren,b,E,g,y,Wn(f,v),C):O||$(a,f,E,null,g,y,Wn(f,v),C,!1),T>0){if(T&16)q(E,f,V,H,g,y,v);else if(T&2&&V.class!==H.class&&i(E,"class",null,H.class,v),T&4&&i(E,"style",V.style,H.style,v),T&8){const G=f.dynamicProps;for(let Q=0;Q{K&&Ce(K,g,f,a),P&&Me(f,a,g,"updated")},y)},A=(a,f,g,y,v,C,O)=>{for(let E=0;E{if(g!==y){if(g!==ne)for(const E in g)!yt(E)&&!(E in y)&&i(a,E,g[E],null,O,f.children,v,C,Ne);for(const E in y){if(yt(E))continue;const T=y[E],b=g[E];T!==b&&E!=="value"&&i(a,E,b,T,O,f.children,v,C,Ne)}"value"in y&&i(a,"value",g.value,y.value,O)}},R=(a,f,g,y,v,C,O,E,T)=>{const b=f.el=a?a.el:l(""),P=f.anchor=a?a.anchor:l("");let{patchFlag:V,dynamicChildren:H,slotScopeIds:K}=f;K&&(E=E?E.concat(K):K),a==null?(r(b,g,y),r(P,g,y),I(f.children||[],g,P,v,C,O,E,T)):V>0&&V&64&&H&&a.dynamicChildren?(A(a.dynamicChildren,H,g,v,C,O,E),(f.key!=null||v&&f===v.subTree)&&Oi(a,f,!0)):$(a,f,g,P,v,C,O,E,T)},Y=(a,f,g,y,v,C,O,E,T)=>{f.slotScopeIds=E,a==null?f.shapeFlag&512?v.ctx.activate(f,g,y,O,T):ee(f,g,y,v,C,O,T):ae(a,f,T)},ee=(a,f,g,y,v,C,O)=>{const E=a.component=cc(a,y,v);if(Wt(a)&&(E.ctx.renderer=ht),ac(E),E.asyncDep){if(v&&v.registerDep(E,D),!a.el){const T=E.subTree=se(we);B(null,T,f,g)}}else D(E,a,f,g,v,C,O)},ae=(a,f,g)=>{const y=f.component=a.component;if(pl(a,f,g))if(y.asyncDep&&!y.asyncResolved){z(y,f,g);return}else y.next=f,ll(y.update),y.effect.dirty=!0,y.update();else f.el=a.el,y.vnode=f},D=(a,f,g,y,v,C,O)=>{const E=()=>{if(a.isMounted){let{next:P,bu:V,u:H,parent:K,vnode:G}=a;{const pt=Ii(a);if(pt){P&&(P.el=G.el,z(a,P,O)),pt.asyncDep.then(()=>{a.isUnmounted||E()});return}}let Q=P,te;tt(a,!1),P?(P.el=G.el,z(a,P,O)):P=G,V&&Vn(V),(te=P.props&&P.props.onVnodeBeforeUpdate)&&Ce(te,K,P,G),tt(a,!0);const le=Un(a),Se=a.subTree;a.subTree=le,L(Se,le,h(Se.el),Gt(Se),a,v,C),P.el=le.el,Q===null&&gl(a,le.el),H&&ge(H,v),(te=P.props&&P.props.onVnodeUpdated)&&ge(()=>Ce(te,K,P,G),v)}else{let P;const{el:V,props:H}=f,{bm:K,m:G,parent:Q}=a,te=wt(f);if(tt(a,!1),K&&Vn(K),!te&&(P=H&&H.onVnodeBeforeMount)&&Ce(P,Q,f),tt(a,!0),V&&Dn){const le=()=>{a.subTree=Un(a),Dn(V,a.subTree,a,v,null)};te?f.type.__asyncLoader().then(()=>!a.isUnmounted&&le()):le()}else{const le=a.subTree=Un(a);L(null,le,g,y,a,v,C),f.el=le.el}if(G&&ge(G,v),!te&&(P=H&&H.onVnodeMounted)){const le=f;ge(()=>Ce(P,Q,le),v)}(f.shapeFlag&256||Q&&wt(Q.vnode)&&Q.vnode.shapeFlag&256)&&a.a&&ge(a.a,v),a.isMounted=!0,f=g=y=null}},T=a.effect=new yr(E,be,()=>Rn(b),a.scope),b=a.update=()=>{T.dirty&&T.run()};b.id=a.uid,tt(a,!0),b()},z=(a,f,g)=>{f.component=a;const y=a.vnode.props;a.vnode=f,a.next=null,Kl(a,f.props,y,g),Yl(a,f.children,g),Xe(),Yr(a),Qe()},$=(a,f,g,y,v,C,O,E,T=!1)=>{const b=a&&a.children,P=a?a.shapeFlag:0,V=f.children,{patchFlag:H,shapeFlag:K}=f;if(H>0){if(H&128){dt(b,V,g,y,v,C,O,E,T);return}else if(H&256){Oe(b,V,g,y,v,C,O,E,T);return}}K&8?(P&16&&Ne(b,v,C),V!==b&&d(g,V)):P&16?K&16?dt(b,V,g,y,v,C,O,E,T):Ne(b,v,C,!0):(P&8&&d(g,""),K&16&&I(V,g,y,v,C,O,E,T))},Oe=(a,f,g,y,v,C,O,E,T)=>{a=a||mt,f=f||mt;const b=a.length,P=f.length,V=Math.min(b,P);let H;for(H=0;HP?Ne(a,v,C,!0,!1,V):I(f,g,y,v,C,O,E,T,V)},dt=(a,f,g,y,v,C,O,E,T)=>{let b=0;const P=f.length;let V=a.length-1,H=P-1;for(;b<=V&&b<=H;){const K=a[b],G=f[b]=T?We(f[b]):Ae(f[b]);if(it(K,G))L(K,G,g,null,v,C,O,E,T);else break;b++}for(;b<=V&&b<=H;){const K=a[V],G=f[H]=T?We(f[H]):Ae(f[H]);if(it(K,G))L(K,G,g,null,v,C,O,E,T);else break;V--,H--}if(b>V){if(b<=H){const K=H+1,G=KH)for(;b<=V;)Ie(a[b],v,C,!0),b++;else{const K=b,G=b,Q=new Map;for(b=G;b<=H;b++){const ye=f[b]=T?We(f[b]):Ae(f[b]);ye.key!=null&&Q.set(ye.key,b)}let te,le=0;const Se=H-G+1;let pt=!1,Hr=0;const At=new Array(Se);for(b=0;b=Se){Ie(ye,v,C,!0);continue}let Le;if(ye.key!=null)Le=Q.get(ye.key);else for(te=G;te<=H;te++)if(At[te-G]===0&&it(ye,f[te])){Le=te;break}Le===void 0?Ie(ye,v,C,!0):(At[Le-G]=b+1,Le>=Hr?Hr=Le:pt=!0,L(ye,f[Le],g,null,v,C,O,E,T),le++)}const $r=pt?ec(At):mt;for(te=$r.length-1,b=Se-1;b>=0;b--){const ye=G+b,Le=f[ye],Dr=ye+1{const{el:C,type:O,transition:E,children:T,shapeFlag:b}=a;if(b&6){Ze(a.component.subTree,f,g,y);return}if(b&128){a.suspense.move(f,g,y);return}if(b&64){O.move(a,f,g,ht);return}if(O===me){r(C,f,g);for(let V=0;VE.enter(C),v);else{const{leave:V,delayLeave:H,afterLeave:K}=E,G=()=>r(C,f,g),Q=()=>{V(C,()=>{G(),K&&K()})};H?H(C,G,Q):Q()}else r(C,f,g)},Ie=(a,f,g,y=!1,v=!1)=>{const{type:C,props:O,ref:E,children:T,dynamicChildren:b,shapeFlag:P,patchFlag:V,dirs:H}=a;if(E!=null&&_n(E,null,g,a,!0),P&256){f.ctx.deactivate(a);return}const K=P&1&&H,G=!wt(a);let Q;if(G&&(Q=O&&O.onVnodeBeforeUnmount)&&Ce(Q,f,a),P&6)lo(a.component,g,y);else{if(P&128){a.suspense.unmount(g,y);return}K&&Me(a,null,f,"beforeUnmount"),P&64?a.type.remove(a,f,g,v,ht,y):b&&(C!==me||V>0&&V&64)?Ne(b,f,g,!1,!0):(C===me&&V&384||!v&&P&16)&&Ne(T,f,g),y&&Fr(a)}(G&&(Q=O&&O.onVnodeUnmounted)||K)&&ge(()=>{Q&&Ce(Q,f,a),K&&Me(a,null,f,"unmounted")},g)},Fr=a=>{const{type:f,el:g,anchor:y,transition:v}=a;if(f===me){oo(g,y);return}if(f===Ht){m(a);return}const C=()=>{s(g),v&&!v.persisted&&v.afterLeave&&v.afterLeave()};if(a.shapeFlag&1&&v&&!v.persisted){const{leave:O,delayLeave:E}=v,T=()=>O(g,C);E?E(a.el,C,T):T()}else C()},oo=(a,f)=>{let g;for(;a!==f;)g=_(a),s(a),a=g;s(f)},lo=(a,f,g)=>{const{bum:y,scope:v,update:C,subTree:O,um:E}=a;y&&Vn(y),v.stop(),C&&(C.active=!1,Ie(O,a,f,g)),E&&ge(E,f),ge(()=>{a.isUnmounted=!0},f),f&&f.pendingBranch&&!f.isUnmounted&&a.asyncDep&&!a.asyncResolved&&a.suspenseId===f.pendingId&&(f.deps--,f.deps===0&&f.resolve())},Ne=(a,f,g,y=!1,v=!1,C=0)=>{for(let O=C;Oa.shapeFlag&6?Gt(a.component.subTree):a.shapeFlag&128?a.suspense.next():_(a.anchor||a.el);let Hn=!1;const Nr=(a,f,g)=>{a==null?f._vnode&&Ie(f._vnode,null,null,!0):L(f._vnode||null,a,f,null,null,null,g),Hn||(Hn=!0,Yr(),pn(),Hn=!1),f._vnode=a},ht={p:L,um:Ie,m:Ze,r:Fr,mt:ee,mc:I,pc:$,pbc:A,n:Gt,o:e};let $n,Dn;return t&&([$n,Dn]=t(ht)),{render:Nr,hydrate:$n,createApp:Ul(Nr,$n)}}function Wn({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function tt({effect:e,update:t},n){e.allowRecurse=t.allowRecurse=n}function Ri(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function Oi(e,t,n=!1){const r=e.children,s=t.children;if(U(r)&&U(s))for(let i=0;i>1,e[n[l]]0&&(t[r]=n[i-1]),n[i]=r)}}for(i=n.length,o=n[i-1];i-- >0;)n[i]=o,o=t[o];return n}function Ii(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:Ii(t)}const tc=e=>e.__isTeleport,me=Symbol.for("v-fgt"),xt=Symbol.for("v-txt"),we=Symbol.for("v-cmt"),Ht=Symbol.for("v-stc"),$t=[];let Re=null;function Li(e=!1){$t.push(Re=e?null:[])}function nc(){$t.pop(),Re=$t[$t.length-1]||null}let Ut=1;function is(e){Ut+=e}function Mi(e){return e.dynamicChildren=Ut>0?Re||mt:null,nc(),Ut>0&&Re&&Re.push(e),e}function za(e,t,n,r,s,i){return Mi(Ni(e,t,n,r,s,i,!0))}function Pi(e,t,n,r,s){return Mi(se(e,t,n,r,s,!0))}function yn(e){return e?e.__v_isVNode===!0:!1}function it(e,t){return e.type===t.type&&e.key===t.key}const Fn="__vInternal",Fi=({key:e})=>e??null,an=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?re(e)||ue(e)||W(e)?{i:he,r:e,k:t,f:!!n}:e:null);function Ni(e,t=null,n=null,r=0,s=null,i=e===me?0:1,o=!1,l=!1){const c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&Fi(t),ref:t&&an(t),scopeId:In,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:r,dynamicProps:s,dynamicChildren:null,appContext:null,ctx:he};return l?(Lr(c,n),i&128&&e.normalize(c)):n&&(c.shapeFlag|=re(n)?8:16),Ut>0&&!o&&Re&&(c.patchFlag>0||i&6)&&c.patchFlag!==32&&Re.push(c),c}const se=rc;function rc(e,t=null,n=null,r=0,s=null,i=!1){if((!e||e===ui)&&(e=we),yn(e)){const l=Je(e,t,!0);return n&&Lr(l,n),Ut>0&&!i&&Re&&(l.shapeFlag&6?Re[Re.indexOf(e)]=l:Re.push(l)),l.patchFlag|=-2,l}if(pc(e)&&(e=e.__vccOpts),t){t=sc(t);let{class:l,style:c}=t;l&&!re(l)&&(t.class=_r(l)),Z(c)&&(Zs(c)&&!U(c)&&(c=oe({},c)),t.style=mr(c))}const o=re(e)?1:ml(e)?128:tc(e)?64:Z(e)?4:W(e)?2:0;return Ni(e,t,n,r,s,o,i,!0)}function sc(e){return e?Zs(e)||Fn in e?oe({},e):e:null}function Je(e,t,n=!1){const{props:r,ref:s,patchFlag:i,children:o}=e,l=t?ic(r||{},t):r;return{__v_isVNode:!0,__v_skip:!0,type:e.type,props:l,key:l&&Fi(l),ref:t&&t.ref?n&&s?U(s)?s.concat(an(t)):[s,an(t)]:an(t):s,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:o,target:e.target,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==me?i===-1?16:i|16:i,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:e.transition,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&Je(e.ssContent),ssFallback:e.ssFallback&&Je(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce}}function Hi(e=" ",t=0){return se(xt,null,e,t)}function Ja(e,t){const n=se(Ht,null,e);return n.staticCount=t,n}function Xa(e="",t=!1){return t?(Li(),Pi(we,null,e)):se(we,null,e)}function Ae(e){return e==null||typeof e=="boolean"?se(we):U(e)?se(me,null,e.slice()):typeof e=="object"?We(e):se(xt,null,String(e))}function We(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:Je(e)}function Lr(e,t){let n=0;const{shapeFlag:r}=e;if(t==null)t=null;else if(U(t))n=16;else if(typeof t=="object")if(r&65){const s=t.default;s&&(s._c&&(s._d=!1),Lr(e,s()),s._c&&(s._d=!0));return}else{n=32;const s=t._;!s&&!(Fn in t)?t._ctx=he:s===3&&he&&(he.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else W(t)?(t={default:t,_ctx:he},n=32):(t=String(t),r&64?(n=16,t=[Hi(t)]):n=8);e.children=t,e.shapeFlag|=n}function ic(...e){const t={};for(let n=0;nce||he;let vn,cr;{const e=gr(),t=(n,r)=>{let s;return(s=e[n])||(s=e[n]=[]),s.push(r),i=>{s.length>1?s.forEach(o=>o(i)):s[0](i)}};vn=t("__VUE_INSTANCE_SETTERS__",n=>ce=n),cr=t("__VUE_SSR_SETTERS__",n=>Yt=n)}const qt=e=>{const t=ce;return vn(e),e.scope.on(),()=>{e.scope.off(),vn(t)}},os=()=>{ce&&ce.scope.off(),vn(null)};function $i(e){return e.vnode.shapeFlag&4}let Yt=!1;function ac(e,t=!1){t&&cr(t);const{props:n,children:r}=e.vnode,s=$i(e);kl(e,n,s,t),ql(e,r);const i=s?uc(e,t):void 0;return t&&cr(!1),i}function uc(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=Lt(new Proxy(e.ctx,Pl));const{setup:r}=n;if(r){const s=e.setupContext=r.length>1?Vi(e):null,i=qt(e);Xe();const o=$e(r,e,0,[e.props,s]);if(Qe(),i(),Hs(o)){if(o.then(os,os),t)return o.then(l=>{ls(e,l,t)}).catch(l=>{Kt(l,e,0)});e.asyncDep=o}else ls(e,o,t)}else Di(e,t)}function ls(e,t,n){W(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:Z(t)&&(e.setupState=si(t)),Di(e,n)}let cs;function Di(e,t,n){const r=e.type;if(!e.render){if(!t&&cs&&!r.render){const s=r.template||Or(e).template;if(s){const{isCustomElement:i,compilerOptions:o}=e.appContext.config,{delimiters:l,compilerOptions:c}=r,u=oe(oe({isCustomElement:i,delimiters:l},o),c);r.render=cs(s,u)}}e.render=r.render||be}{const s=qt(e);Xe();try{Nl(e)}finally{Qe(),s()}}}function fc(e){return e.attrsProxy||(e.attrsProxy=new Proxy(e.attrs,{get(t,n){return _e(e,"get","$attrs"),t[n]}}))}function Vi(e){const t=n=>{e.exposed=n||{}};return{get attrs(){return fc(e)},slots:e.slots,emit:e.emit,expose:t}}function Mr(e){if(e.exposed)return e.exposeProxy||(e.exposeProxy=new Proxy(si(Lt(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Ft)return Ft[n](e)},has(t,n){return n in t||n in Ft}}))}const dc=/(?:^|[-_])(\w)/g,hc=e=>e.replace(dc,t=>t.toUpperCase()).replace(/[-_]/g,"");function ji(e,t=!0){return W(e)?e.displayName||e.name:e.name||t&&e.__name}function Ui(e,t,n=!1){let r=ji(t);if(!r&&t.__file){const s=t.__file.match(/([^/\\]+)\.\w+$/);s&&(r=s[1])}if(!r&&e&&e.parent){const s=i=>{for(const o in i)if(i[o]===t)return o};r=s(e.components||e.parent.type.components)||s(e.appContext.components)}return r?hc(r):n?"App":"Anonymous"}function pc(e){return W(e)&&"__vccOpts"in e}const ie=(e,t)=>qo(e,t,Yt);function ar(e,t,n){const r=arguments.length;return r===2?Z(t)&&!U(t)?yn(t)?se(e,null,[t]):se(e,t):se(e,null,t):(r>3?n=Array.prototype.slice.call(arguments,2):r===3&&yn(n)&&(n=[n]),se(e,t,n))}const gc="3.4.19";/** +* @vue/runtime-dom v3.4.19 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/const mc="http://www.w3.org/2000/svg",_c="http://www.w3.org/1998/Math/MathML",qe=typeof document<"u"?document:null,as=qe&&qe.createElement("template"),yc={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,r)=>{const s=t==="svg"?qe.createElementNS(mc,e):t==="mathml"?qe.createElementNS(_c,e):qe.createElement(e,n?{is:n}:void 0);return e==="select"&&r&&r.multiple!=null&&s.setAttribute("multiple",r.multiple),s},createText:e=>qe.createTextNode(e),createComment:e=>qe.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>qe.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,r,s,i){const o=n?n.previousSibling:t.lastChild;if(s&&(s===i||s.nextSibling))for(;t.insertBefore(s.cloneNode(!0),n),!(s===i||!(s=s.nextSibling)););else{as.innerHTML=r==="svg"?`${e}`:r==="mathml"?`${e}`:e;const l=as.content;if(r==="svg"||r==="mathml"){const c=l.firstChild;for(;c.firstChild;)l.appendChild(c.firstChild);l.removeChild(c)}t.insertBefore(l,n)}return[o?o.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},Ue="transition",Ot="animation",Bt=Symbol("_vtc"),Bi=(e,{slots:t})=>ar(El,vc(e),t);Bi.displayName="Transition";const ki={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String};Bi.props=oe({},gi,ki);const nt=(e,t=[])=>{U(e)?e.forEach(n=>n(...t)):e&&e(...t)},us=e=>e?U(e)?e.some(t=>t.length>1):e.length>1:!1;function vc(e){const t={};for(const R in e)R in ki||(t[R]=e[R]);if(e.css===!1)return t;const{name:n="v",type:r,duration:s,enterFromClass:i=`${n}-enter-from`,enterActiveClass:o=`${n}-enter-active`,enterToClass:l=`${n}-enter-to`,appearFromClass:c=i,appearActiveClass:u=o,appearToClass:d=l,leaveFromClass:h=`${n}-leave-from`,leaveActiveClass:_=`${n}-leave-active`,leaveToClass:w=`${n}-leave-to`}=e,x=bc(s),L=x&&x[0],N=x&&x[1],{onBeforeEnter:B,onEnter:k,onEnterCancelled:p,onLeave:m,onLeaveCancelled:F,onBeforeAppear:M=B,onAppear:j=k,onAppearCancelled:I=p}=t,S=(R,Y,ee)=>{rt(R,Y?d:l),rt(R,Y?u:o),ee&&ee()},A=(R,Y)=>{R._isLeaving=!1,rt(R,h),rt(R,w),rt(R,_),Y&&Y()},q=R=>(Y,ee)=>{const ae=R?j:k,D=()=>S(Y,R,ee);nt(ae,[Y,D]),fs(()=>{rt(Y,R?c:i),Be(Y,R?d:l),us(ae)||ds(Y,r,L,D)})};return oe(t,{onBeforeEnter(R){nt(B,[R]),Be(R,i),Be(R,o)},onBeforeAppear(R){nt(M,[R]),Be(R,c),Be(R,u)},onEnter:q(!1),onAppear:q(!0),onLeave(R,Y){R._isLeaving=!0;const ee=()=>A(R,Y);Be(R,h),Cc(),Be(R,_),fs(()=>{R._isLeaving&&(rt(R,h),Be(R,w),us(m)||ds(R,r,N,ee))}),nt(m,[R,ee])},onEnterCancelled(R){S(R,!1),nt(p,[R])},onAppearCancelled(R){S(R,!0),nt(I,[R])},onLeaveCancelled(R){A(R),nt(F,[R])}})}function bc(e){if(e==null)return null;if(Z(e))return[qn(e.enter),qn(e.leave)];{const t=qn(e);return[t,t]}}function qn(e){return go(e)}function Be(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e[Bt]||(e[Bt]=new Set)).add(t)}function rt(e,t){t.split(/\s+/).forEach(r=>r&&e.classList.remove(r));const n=e[Bt];n&&(n.delete(t),n.size||(e[Bt]=void 0))}function fs(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let wc=0;function ds(e,t,n,r){const s=e._endId=++wc,i=()=>{s===e._endId&&r()};if(n)return setTimeout(i,n);const{type:o,timeout:l,propCount:c}=Ec(e,t);if(!o)return r();const u=o+"end";let d=0;const h=()=>{e.removeEventListener(u,_),i()},_=w=>{w.target===e&&++d>=c&&h()};setTimeout(()=>{d(n[x]||"").split(", "),s=r(`${Ue}Delay`),i=r(`${Ue}Duration`),o=hs(s,i),l=r(`${Ot}Delay`),c=r(`${Ot}Duration`),u=hs(l,c);let d=null,h=0,_=0;t===Ue?o>0&&(d=Ue,h=o,_=i.length):t===Ot?u>0&&(d=Ot,h=u,_=c.length):(h=Math.max(o,u),d=h>0?o>u?Ue:Ot:null,_=d?d===Ue?i.length:c.length:0);const w=d===Ue&&/\b(transform|all)(,|$)/.test(r(`${Ue}Property`).toString());return{type:d,timeout:h,propCount:_,hasTransform:w}}function hs(e,t){for(;e.lengthps(n)+ps(e[r])))}function ps(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function Cc(){return document.body.offsetHeight}function xc(e,t,n){const r=e[Bt];r&&(t=(t?[t,...r]:[...r]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const gs=Symbol("_vod"),Tc=Symbol(""),Sc=/(^|;)\s*display\s*:/;function Ac(e,t,n){const r=e.style,s=re(n),i=r.display;let o=!1;if(n&&!s){if(t&&!re(t))for(const l in t)n[l]==null&&ur(r,l,"");for(const l in n)l==="display"&&(o=!0),ur(r,l,n[l])}else if(s){if(t!==n){const l=r[Tc];l&&(n+=";"+l),r.cssText=n,o=Sc.test(n)}}else t&&e.removeAttribute("style");gs in e&&(e[gs]=o?r.display:"",r.display=i)}const ms=/\s*!important$/;function ur(e,t,n){if(U(n))n.forEach(r=>ur(e,t,r));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const r=Rc(e,t);ms.test(n)?e.setProperty(ft(r),n.replace(ms,""),"important"):e[r]=n}}const _s=["Webkit","Moz","ms"],Yn={};function Rc(e,t){const n=Yn[t];if(n)return n;let r=Fe(t);if(r!=="filter"&&r in e)return Yn[t]=r;r=Cn(r);for(let s=0;s<_s.length;s++){const i=_s[s]+r;if(i in e)return Yn[t]=i}return t}const ys="http://www.w3.org/1999/xlink";function Oc(e,t,n,r,s){if(r&&t.startsWith("xlink:"))n==null?e.removeAttributeNS(ys,t.slice(6,t.length)):e.setAttributeNS(ys,t,n);else{const i=wo(t);n==null||i&&!Vs(n)?e.removeAttribute(t):e.setAttribute(t,i?"":n)}}function Ic(e,t,n,r,s,i,o){if(t==="innerHTML"||t==="textContent"){r&&o(r,s,i),e[t]=n??"";return}const l=e.tagName;if(t==="value"&&l!=="PROGRESS"&&!l.includes("-")){e._value=n;const u=l==="OPTION"?e.getAttribute("value"):e.value,d=n??"";u!==d&&(e.value=d),n==null&&e.removeAttribute(t);return}let c=!1;if(n===""||n==null){const u=typeof e[t];u==="boolean"?n=Vs(n):n==null&&u==="string"?(n="",c=!0):u==="number"&&(n=0,c=!0)}try{e[t]=n}catch{}c&&e.removeAttribute(t)}function Lc(e,t,n,r){e.addEventListener(t,n,r)}function Mc(e,t,n,r){e.removeEventListener(t,n,r)}const vs=Symbol("_vei");function Pc(e,t,n,r,s=null){const i=e[vs]||(e[vs]={}),o=i[t];if(r&&o)o.value=r;else{const[l,c]=Fc(t);if(r){const u=i[t]=$c(r,s);Lc(e,l,u,c)}else o&&(Mc(e,l,o,c),i[t]=void 0)}}const bs=/(?:Once|Passive|Capture)$/;function Fc(e){let t;if(bs.test(e)){t={};let r;for(;r=e.match(bs);)e=e.slice(0,e.length-r[0].length),t[r[0].toLowerCase()]=!0}return[e[2]===":"?e.slice(3):ft(e.slice(2)),t]}let Gn=0;const Nc=Promise.resolve(),Hc=()=>Gn||(Nc.then(()=>Gn=0),Gn=Date.now());function $c(e,t){const n=r=>{if(!r._vts)r._vts=Date.now();else if(r._vts<=n.attached)return;Te(Dc(r,n.value),t,5,[r])};return n.value=e,n.attached=Hc(),n}function Dc(e,t){if(U(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(r=>s=>!s._stopped&&r&&r(s))}else return t}const ws=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,Vc=(e,t,n,r,s,i,o,l,c)=>{const u=s==="svg";t==="class"?xc(e,r,u):t==="style"?Ac(e,n,r):kt(t)?dr(t)||Pc(e,t,n,r,o):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):jc(e,t,r,u))?Ic(e,t,r,i,o,l,c):(t==="true-value"?e._trueValue=r:t==="false-value"&&(e._falseValue=r),Oc(e,t,r,u))};function jc(e,t,n,r){if(r)return!!(t==="innerHTML"||t==="textContent"||t in e&&ws(t)&&W(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const s=e.tagName;if(s==="IMG"||s==="VIDEO"||s==="CANVAS"||s==="SOURCE")return!1}return ws(t)&&re(n)?!1:t in e}const Uc=["ctrl","shift","alt","meta"],Bc={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>Uc.some(n=>e[`${n}Key`]&&!t.includes(n))},Qa=(e,t)=>{const n=e._withMods||(e._withMods={}),r=t.join(".");return n[r]||(n[r]=(s,...i)=>{for(let o=0;o{const n=e._withKeys||(e._withKeys={}),r=t.join(".");return n[r]||(n[r]=s=>{if(!("key"in s))return;const i=ft(s.key);if(t.some(o=>o===i||kc[o]===i))return e(s)})},Kc=oe({patchProp:Vc},yc);let zn,Es=!1;function Wc(){return zn=Es?zn:Ql(Kc),Es=!0,zn}const eu=(...e)=>{const t=Wc().createApp(...e),{mount:n}=t;return t.mount=r=>{const s=Yc(r);if(s)return n(s,!0,qc(s))},t};function qc(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function Yc(e){return re(e)?document.querySelector(e):e}const tu=(e,t)=>{const n=e.__vccOpts||e;for(const[r,s]of t)n[r]=s;return n},Gc="modulepreload",zc=function(e){return"/"+e},Cs={},nu=function(t,n,r){if(!n||n.length===0)return t();const s=document.getElementsByTagName("link");return Promise.all(n.map(i=>{if(i=zc(i),i in Cs)return;Cs[i]=!0;const o=i.endsWith(".css"),l=o?'[rel="stylesheet"]':"";if(!!r)for(let d=s.length-1;d>=0;d--){const h=s[d];if(h.href===i&&(!o||h.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${i}"]${l}`))return;const u=document.createElement("link");if(u.rel=o?"stylesheet":Gc,o||(u.as="script",u.crossOrigin=""),u.href=i,document.head.appendChild(u),o)return new Promise((d,h)=>{u.addEventListener("load",d),u.addEventListener("error",()=>h(new Error(`Unable to preload CSS for ${i}`)))})})).then(()=>t()).catch(i=>{const o=new Event("vite:preloadError",{cancelable:!0});if(o.payload=i,window.dispatchEvent(o),!o.defaultPrevented)throw i})},Jc=window.__VP_SITE_DATA__;function Pr(e){return Us()?(xo(e),!0):!1}function at(e){return typeof e=="function"?e():ri(e)}const Ki=typeof window<"u"&&typeof document<"u";typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope;const Xc=Object.prototype.toString,Qc=e=>Xc.call(e)==="[object Object]",Wi=()=>{},xs=Zc();function Zc(){var e,t;return Ki&&((e=window==null?void 0:window.navigator)==null?void 0:e.userAgent)&&(/iP(ad|hone|od)/.test(window.navigator.userAgent)||((t=window==null?void 0:window.navigator)==null?void 0:t.maxTouchPoints)>2&&/iPad|Macintosh/.test(window==null?void 0:window.navigator.userAgent))}function ea(e,t){function n(...r){return new Promise((s,i)=>{Promise.resolve(e(()=>t.apply(this,r),{fn:t,thisArg:this,args:r})).then(s).catch(i)})}return n}const qi=e=>e();function ta(e=qi){const t=de(!0);function n(){t.value=!1}function r(){t.value=!0}const s=(...i)=>{t.value&&e(...i)};return{isActive:Sn(t),pause:n,resume:r,eventFilter:s}}function na(e){return e||Nn()}function Yi(...e){if(e.length!==1)return Zo(...e);const t=e[0];return typeof t=="function"?Sn(Jo(()=>({get:t,set:Wi}))):de(t)}function ra(e,t,n={}){const{eventFilter:r=qi,...s}=n;return Ge(e,ea(r,t),s)}function sa(e,t,n={}){const{eventFilter:r,...s}=n,{eventFilter:i,pause:o,resume:l,isActive:c}=ta(r);return{stop:ra(e,t,{...s,eventFilter:i}),pause:o,resume:l,isActive:c}}function Gi(e,t=!0,n){na()?St(e,n):t?e():An(e)}function zi(e){var t;const n=at(e);return(t=n==null?void 0:n.$el)!=null?t:n}const ut=Ki?window:void 0;function bn(...e){let t,n,r,s;if(typeof e[0]=="string"||Array.isArray(e[0])?([n,r,s]=e,t=ut):[t,n,r,s]=e,!t)return Wi;Array.isArray(n)||(n=[n]),Array.isArray(r)||(r=[r]);const i=[],o=()=>{i.forEach(d=>d()),i.length=0},l=(d,h,_,w)=>(d.addEventListener(h,_,w),()=>d.removeEventListener(h,_,w)),c=Ge(()=>[zi(t),at(s)],([d,h])=>{if(o(),!d)return;const _=Qc(h)?{...h}:h;i.push(...n.flatMap(w=>r.map(x=>l(d,w,x,_))))},{immediate:!0,flush:"post"}),u=()=>{c(),o()};return Pr(u),u}function ia(){const e=de(!1),t=Nn();return t&&St(()=>{e.value=!0},t),e}function oa(e){const t=ia();return ie(()=>(t.value,!!e()))}function la(e,t={}){const{window:n=ut}=t,r=oa(()=>n&&"matchMedia"in n&&typeof n.matchMedia=="function");let s;const i=de(!1),o=u=>{i.value=u.matches},l=()=>{s&&("removeEventListener"in s?s.removeEventListener("change",o):s.removeListener(o))},c=hi(()=>{r.value&&(l(),s=n.matchMedia(at(e)),"addEventListener"in s?s.addEventListener("change",o):s.addListener(o),i.value=s.matches)});return Pr(()=>{c(),l(),s=void 0}),i}const rn=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},sn="__vueuse_ssr_handlers__",ca=aa();function aa(){return sn in rn||(rn[sn]=rn[sn]||{}),rn[sn]}function Ji(e,t){return ca[e]||t}function ua(e){return e==null?"any":e instanceof Set?"set":e instanceof Map?"map":e instanceof Date?"date":typeof e=="boolean"?"boolean":typeof e=="string"?"string":typeof e=="object"?"object":Number.isNaN(e)?"any":"number"}const fa={boolean:{read:e=>e==="true",write:e=>String(e)},object:{read:e=>JSON.parse(e),write:e=>JSON.stringify(e)},number:{read:e=>Number.parseFloat(e),write:e=>String(e)},any:{read:e=>e,write:e=>String(e)},string:{read:e=>e,write:e=>String(e)},map:{read:e=>new Map(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e.entries()))},set:{read:e=>new Set(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e))},date:{read:e=>new Date(e),write:e=>e.toISOString()}},Ts="vueuse-storage";function da(e,t,n,r={}){var s;const{flush:i="pre",deep:o=!0,listenToStorageChanges:l=!0,writeDefaults:c=!0,mergeDefaults:u=!1,shallow:d,window:h=ut,eventFilter:_,onError:w=S=>{console.error(S)},initOnMounted:x}=r,L=(d?ti:de)(typeof t=="function"?t():t);if(!n)try{n=Ji("getDefaultStorage",()=>{var S;return(S=ut)==null?void 0:S.localStorage})()}catch(S){w(S)}if(!n)return L;const N=at(t),B=ua(N),k=(s=r.serializer)!=null?s:fa[B],{pause:p,resume:m}=sa(L,()=>F(L.value),{flush:i,deep:o,eventFilter:_});return h&&l&&Gi(()=>{bn(h,"storage",I),bn(h,Ts,j),x&&I()}),x||I(),L;function F(S){try{const A=n.getItem(e),q=R=>{h&&h.dispatchEvent(new CustomEvent(Ts,{detail:{key:e,oldValue:A,newValue:R,storageArea:n}}))};if(S==null)q(null),n.removeItem(e);else{const R=k.write(S);A!==R&&(n.setItem(e,R),q(R))}}catch(A){w(A)}}function M(S){const A=S?S.newValue:n.getItem(e);if(A==null)return c&&N!=null&&n.setItem(e,k.write(N)),N;if(!S&&u){const q=k.read(A);return typeof u=="function"?u(q,N):B==="object"&&!Array.isArray(q)?{...N,...q}:q}else return typeof A!="string"?A:k.read(A)}function j(S){I(S.detail)}function I(S){if(!(S&&S.storageArea!==n)){if(S&&S.key==null){L.value=N;return}if(!(S&&S.key!==e)){p();try{(S==null?void 0:S.newValue)!==k.write(L.value)&&(L.value=M(S))}catch(A){w(A)}finally{S?An(m):m()}}}}}function Xi(e){return la("(prefers-color-scheme: dark)",e)}function ha(e={}){const{selector:t="html",attribute:n="class",initialValue:r="auto",window:s=ut,storage:i,storageKey:o="vueuse-color-scheme",listenToStorageChanges:l=!0,storageRef:c,emitAuto:u,disableTransition:d=!0}=e,h={auto:"",light:"light",dark:"dark",...e.modes||{}},_=Xi({window:s}),w=ie(()=>_.value?"dark":"light"),x=c||(o==null?Yi(r):da(o,r,i,{window:s,listenToStorageChanges:l})),L=ie(()=>x.value==="auto"?w.value:x.value),N=Ji("updateHTMLAttrs",(m,F,M)=>{const j=typeof m=="string"?s==null?void 0:s.document.querySelector(m):zi(m);if(!j)return;let I;if(d){I=s.document.createElement("style");const S="*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}";I.appendChild(document.createTextNode(S)),s.document.head.appendChild(I)}if(F==="class"){const S=M.split(/\s/g);Object.values(h).flatMap(A=>(A||"").split(/\s/g)).filter(Boolean).forEach(A=>{S.includes(A)?j.classList.add(A):j.classList.remove(A)})}else j.setAttribute(F,M);d&&(s.getComputedStyle(I).opacity,document.head.removeChild(I))});function B(m){var F;N(t,n,(F=h[m])!=null?F:m)}function k(m){e.onChanged?e.onChanged(m,B):B(m)}Ge(L,k,{flush:"post",immediate:!0}),Gi(()=>k(L.value));const p=ie({get(){return u?x.value:L.value},set(m){x.value=m}});try{return Object.assign(p,{store:x,system:w,state:L})}catch{return p}}function pa(e={}){const{valueDark:t="dark",valueLight:n="",window:r=ut}=e,s=ha({...e,onChanged:(l,c)=>{var u;e.onChanged?(u=e.onChanged)==null||u.call(e,l==="dark",c,l):c(l)},modes:{dark:t,light:n}}),i=ie(()=>s.system?s.system.value:Xi({window:r}).value?"dark":"light");return ie({get(){return s.value==="dark"},set(l){const c=l?"dark":"light";i.value===c?s.value="auto":s.value=c}})}function Jn(e){return typeof Window<"u"&&e instanceof Window?e.document.documentElement:typeof Document<"u"&&e instanceof Document?e.documentElement:e}function Qi(e){const t=window.getComputedStyle(e);if(t.overflowX==="scroll"||t.overflowY==="scroll"||t.overflowX==="auto"&&e.clientWidth1?!0:(t.preventDefault&&t.preventDefault(),!1)}const on=new WeakMap;function ru(e,t=!1){const n=de(t);let r=null,s;Ge(Yi(e),l=>{const c=Jn(at(l));if(c){const u=c;on.get(u)||on.set(u,s),n.value&&(u.style.overflow="hidden")}},{immediate:!0});const i=()=>{const l=Jn(at(e));!l||n.value||(xs&&(r=bn(l,"touchmove",c=>{ga(c)},{passive:!1})),l.style.overflow="hidden",n.value=!0)},o=()=>{var l;const c=Jn(at(e));!c||!n.value||(xs&&(r==null||r()),c.style.overflow=(l=on.get(c))!=null?l:"",on.delete(c),n.value=!1)};return Pr(o),ie({get(){return n.value},set(l){l?i():o()}})}function su(e={}){const{window:t=ut,behavior:n="auto"}=e;if(!t)return{x:de(0),y:de(0)};const r=de(t.scrollX),s=de(t.scrollY),i=ie({get(){return r.value},set(l){scrollTo({left:l,behavior:n})}}),o=ie({get(){return s.value},set(l){scrollTo({top:l,behavior:n})}});return bn(t,"scroll",()=>{r.value=t.scrollX,s.value=t.scrollY},{capture:!1,passive:!0}),{x:i,y:o}}const Zi=/^[a-z]+:/i,ma="vitepress-theme-appearance",eo=/#.*$/,_a=/(index)?\.(md|html)$/,xe=typeof document<"u",to={relativePath:"",filePath:"",title:"404",description:"Not Found",headers:[],frontmatter:{sidebar:!1,layout:"page"},lastUpdated:0,isNotFound:!0};function ya(e,t,n=!1){if(t===void 0)return!1;if(e=Ss(`/${e}`),n)return new RegExp(t).test(e);if(Ss(t)!==e)return!1;const r=t.match(eo);return r?(xe?location.hash:"")===r[0]:!0}function Ss(e){return decodeURI(e).replace(eo,"").replace(_a,"")}function va(e){return Zi.test(e)}function ba(e,t){var r,s,i,o,l,c,u;const n=Object.keys(e.locales).find(d=>d!=="root"&&!va(d)&&ya(t,`/${d}/`,!0))||"root";return Object.assign({},e,{localeIndex:n,lang:((r=e.locales[n])==null?void 0:r.lang)??e.lang,dir:((s=e.locales[n])==null?void 0:s.dir)??e.dir,title:((i=e.locales[n])==null?void 0:i.title)??e.title,titleTemplate:((o=e.locales[n])==null?void 0:o.titleTemplate)??e.titleTemplate,description:((l=e.locales[n])==null?void 0:l.description)??e.description,head:ro(e.head,((c=e.locales[n])==null?void 0:c.head)??[]),themeConfig:{...e.themeConfig,...(u=e.locales[n])==null?void 0:u.themeConfig}})}function no(e,t){const n=t.title||e.title,r=t.titleTemplate??e.titleTemplate;if(typeof r=="string"&&r.includes(":title"))return r.replace(/:title/g,n);const s=wa(e.title,r);return`${n}${s}`}function wa(e,t){return t===!1?"":t===!0||t===void 0?` | ${e}`:e===t?"":` | ${t}`}function Ea(e,t){const[n,r]=t;if(n!=="meta")return!1;const s=Object.entries(r)[0];return s==null?!1:e.some(([i,o])=>i===n&&o[s[0]]===s[1])}function ro(e,t){return[...e.filter(n=>!Ea(t,n)),...t]}const Ca=/[\u0000-\u001F"#$&*+,:;<=>?[\]^`{|}\u007F]/g,xa=/^[a-z]:/i;function As(e){const t=xa.exec(e),n=t?t[0]:"";return n+e.slice(n.length).replace(Ca,"_").replace(/(^|\/)_+(?=[^/]*$)/,"$1")}const Ta=Symbol(),ot=ti(Jc);function iu(e){const t=ie(()=>ba(ot.value,e.data.relativePath)),n=t.value.appearance?pa({storageKey:ma,initialValue:()=>typeof t.value.appearance=="string"?t.value.appearance:"auto",...typeof t.value.appearance=="object"?t.value.appearance:{}}):de(!1);return{site:t,theme:ie(()=>t.value.themeConfig),page:ie(()=>e.data),frontmatter:ie(()=>e.data.frontmatter),params:ie(()=>e.data.params),lang:ie(()=>t.value.lang),dir:ie(()=>t.value.dir),localeIndex:ie(()=>t.value.localeIndex||"root"),title:ie(()=>no(t.value,e.data)),description:ie(()=>e.data.description||t.value.description),isDark:n}}function Sa(){const e=Et(Ta);if(!e)throw new Error("vitepress data not properly injected in app");return e}function Aa(e,t){return`${e}${t}`.replace(/\/+/g,"/")}function Rs(e){return Zi.test(e)||!e.startsWith("/")?e:Aa(ot.value.base,e)}function Ra(e){let t=e.replace(/\.html$/,"");if(t=decodeURIComponent(t),t=t.replace(/\/$/,"/index"),xe){const n="/";t=As(t.slice(n.length).replace(/\//g,"_")||"index")+".md";let r=__VP_HASH_MAP__[t.toLowerCase()];if(r||(t=t.endsWith("_index.md")?t.slice(0,-9)+".md":t.slice(0,-3)+"_index.md",r=__VP_HASH_MAP__[t.toLowerCase()]),!r)return null;t=`${n}assets/${t}.${r}.js`}else t=`./${As(t.slice(1).replace(/\//g,"_"))}.md.js`;return t}let un=[];function ou(e){un.push(e),Pn(()=>{un=un.filter(t=>t!==e)})}const Oa=Symbol(),Os="http://a.com",Ia=()=>({path:"/",component:null,data:to});function lu(e,t){const n=Tn(Ia()),r={route:n,go:s};async function s(l=xe?location.href:"/"){var u,d;if(await((u=r.onBeforeRouteChange)==null?void 0:u.call(r,l))===!1)return;const c=new URL(l,Os);ot.value.cleanUrls||!c.pathname.endsWith("/")&&!c.pathname.endsWith(".html")&&(c.pathname+=".html",l=c.pathname+c.search+c.hash),Ms(l),await o(l),await((d=r.onAfterRouteChanged)==null?void 0:d.call(r,l))}let i=null;async function o(l,c=0,u=!1){var _;if(await((_=r.onBeforePageLoad)==null?void 0:_.call(r,l))===!1)return;const d=new URL(l,Os),h=i=d.pathname;try{let w=await e(h);if(!w)throw new Error(`Page not found: ${h}`);if(i===h){i=null;const{default:x,__pageData:L}=w;if(!x)throw new Error(`Invalid route component: ${x}`);n.path=xe?h:Rs(h),n.component=Lt(x),n.data=Lt(L),xe&&An(()=>{let N=ot.value.base+L.relativePath.replace(/(?:(^|\/)index)?\.md$/,"$1");if(!ot.value.cleanUrls&&!N.endsWith("/")&&(N+=".html"),N!==d.pathname&&(d.pathname=N,l=N+d.search+d.hash,history.replaceState(null,"",l)),d.hash&&!c){let B=null;try{B=document.getElementById(decodeURIComponent(d.hash).slice(1))}catch(k){console.warn(k)}if(B){Is(B,d.hash);return}}window.scrollTo(0,c)})}}catch(w){if(!/fetch|Page not found/.test(w.message)&&!/^\/404(\.html|\/)?$/.test(l)&&console.error(w),!u)try{const x=await fetch(ot.value.base+"hashmap.json");window.__VP_HASH_MAP__=await x.json(),await o(l,c,!0);return}catch{}i===h&&(i=null,n.path=xe?h:Rs(h),n.component=t?Lt(t):null,n.data=to)}}return xe&&(window.addEventListener("click",l=>{if(l.target.closest("button"))return;const u=l.target.closest("a");if(u&&!u.closest(".vp-raw")&&(u instanceof SVGElement||!u.download)){const{target:d}=u,{href:h,origin:_,pathname:w,hash:x,search:L}=new URL(u.href instanceof SVGAnimatedString?u.href.animVal:u.href,u.baseURI),N=window.location,B=w.match(/\.\w+$/);!l.ctrlKey&&!l.shiftKey&&!l.altKey&&!l.metaKey&&!d&&_===N.origin&&!(B&&B[0]!==".html")&&(l.preventDefault(),w===N.pathname&&L===N.search?(x!==N.hash&&(history.pushState(null,"",x),window.dispatchEvent(new Event("hashchange"))),x?Is(u,x,u.classList.contains("header-anchor")):(Ms(h),window.scrollTo(0,0))):s(h))}},{capture:!0}),window.addEventListener("popstate",l=>{o(location.href,l.state&&l.state.scrollPosition||0)}),window.addEventListener("hashchange",l=>{l.preventDefault()})),r}function La(){const e=Et(Oa);if(!e)throw new Error("useRouter() is called without provider.");return e}function so(){return La().route}function Is(e,t,n=!1){let r=null;try{r=e.classList.contains("header-anchor")?e:document.getElementById(decodeURIComponent(t).slice(1))}catch(s){console.warn(s)}if(r){let u=function(){!n||Math.abs(c-window.scrollY)>window.innerHeight?window.scrollTo(0,c):window.scrollTo({left:0,top:c,behavior:"smooth"})},s=ot.value.scrollOffset,i=0,o=24;if(typeof s=="object"&&"padding"in s&&(o=s.padding,s=s.selector),typeof s=="number")i=s;else if(typeof s=="string")i=Ls(s,o);else if(Array.isArray(s))for(const d of s){const h=Ls(d,o);if(h){i=h;break}}const l=parseInt(window.getComputedStyle(r).paddingTop,10),c=window.scrollY+r.getBoundingClientRect().top-i+l;requestAnimationFrame(u)}}function Ls(e,t){const n=document.querySelector(e);if(!n)return 0;const r=n.getBoundingClientRect().bottom;return r<0?0:r+t}function Ms(e){xe&&e!==location.href&&(history.replaceState({scrollPosition:window.scrollY},document.title),history.pushState(null,"",e))}const Ps=()=>un.forEach(e=>e()),cu=Rr({name:"VitePressContent",props:{as:{type:[Object,String],default:"div"}},setup(e){const t=so(),{site:n}=Sa();return()=>ar(e.as,n.value.contentProps??{style:{position:"relative"}},[t.component?ar(t.component,{onVnodeMounted:Ps,onVnodeUpdated:Ps}):"404 Page Not Found"])}}),au=Rr({setup(e,{slots:t}){const n=de(!1);return St(()=>{n.value=!0}),()=>n.value&&t.default?t.default():null}});function uu(){xe&&window.addEventListener("click",e=>{var n;const t=e.target;if(t.matches(".vp-code-group input")){const r=(n=t.parentElement)==null?void 0:n.parentElement;if(!r)return;const s=Array.from(r.querySelectorAll("input")).indexOf(t);if(s<0)return;const i=r.querySelector(".blocks");if(!i)return;const o=Array.from(i.children).find(u=>u.classList.contains("active"));if(!o)return;const l=i.children[s];if(!l||o===l)return;o.classList.remove("active"),l.classList.add("active");const c=r==null?void 0:r.querySelector(`label[for="${t.id}"]`);c==null||c.scrollIntoView({block:"nearest"})}})}function fu(){if(xe){const e=new WeakMap;window.addEventListener("click",t=>{var r;const n=t.target;if(n.matches('div[class*="language-"] > button.copy')){const s=n.parentElement,i=(r=n.nextElementSibling)==null?void 0:r.nextElementSibling;if(!s||!i)return;const o=/language-(shellscript|shell|bash|sh|zsh)/.test(s.className);let l="";i.querySelectorAll("span.line:not(.diff.remove)").forEach(c=>l+=(c.textContent||"")+` +`),l=l.slice(0,-1),o&&(l=l.replace(/^ *(\$|>) /gm,"").trim()),Ma(l).then(()=>{n.classList.add("copied"),clearTimeout(e.get(n));const c=setTimeout(()=>{n.classList.remove("copied"),n.blur(),e.delete(n)},2e3);e.set(n,c)})}})}}async function Ma(e){try{return navigator.clipboard.writeText(e)}catch{const t=document.createElement("textarea"),n=document.activeElement;t.value=e,t.setAttribute("readonly",""),t.style.contain="strict",t.style.position="absolute",t.style.left="-9999px",t.style.fontSize="12pt";const r=document.getSelection(),s=r?r.rangeCount>0&&r.getRangeAt(0):null;document.body.appendChild(t),t.select(),t.selectionStart=0,t.selectionEnd=e.length,document.execCommand("copy"),document.body.removeChild(t),s&&(r.removeAllRanges(),r.addRange(s)),n&&n.focus()}}function du(e,t){let n=[],r=!0;const s=i=>{if(r){r=!1;return}n.forEach(o=>document.head.removeChild(o)),n=[],i.forEach(o=>{const l=Fs(o);document.head.appendChild(l),n.push(l)})};hi(()=>{const i=e.data,o=t.value,l=i&&i.description,c=i&&i.frontmatter.head||[];document.title=no(o,i);const u=l||o.description;let d=document.querySelector("meta[name=description]");d?d.setAttribute("content",u):Fs(["meta",{name:"description",content:u}]),s(ro(o.head,Fa(c)))})}function Fs([e,t,n]){const r=document.createElement(e);for(const s in t)r.setAttribute(s,t[s]);return n&&(r.innerHTML=n),r}function Pa(e){return e[0]==="meta"&&e[1]&&e[1].name==="description"}function Fa(e){return e.filter(t=>!Pa(t))}const Xn=new Set,io=()=>document.createElement("link"),Na=e=>{const t=io();t.rel="prefetch",t.href=e,document.head.appendChild(t)},Ha=e=>{const t=new XMLHttpRequest;t.open("GET",e,t.withCredentials=!0),t.send()};let ln;const $a=xe&&(ln=io())&&ln.relList&&ln.relList.supports&&ln.relList.supports("prefetch")?Na:Ha;function hu(){if(!xe||!window.IntersectionObserver)return;let e;if((e=navigator.connection)&&(e.saveData||/2g/.test(e.effectiveType)))return;const t=window.requestIdleCallback||setTimeout;let n=null;const r=()=>{n&&n.disconnect(),n=new IntersectionObserver(i=>{i.forEach(o=>{if(o.isIntersecting){const l=o.target;n.unobserve(l);const{pathname:c}=l;if(!Xn.has(c)){Xn.add(c);const u=Ra(c);u&&$a(u)}}})}),t(()=>{document.querySelectorAll("#app a").forEach(i=>{const{hostname:o,pathname:l}=new URL(i.href instanceof SVGAnimatedString?i.href.animVal:i.href,i.baseURI),c=l.match(/\.\w+$/);c&&c[0]!==".html"||i.target!=="_blank"&&o===location.hostname&&(l!==location.pathname?n.observe(i):Xn.add(l))})})};St(r);const s=so();Ge(()=>s.path,r),Pn(()=>{n&&n.disconnect()})}export{Ga as $,ka as A,Rl as B,Ua as C,Wa as D,ti as E,me as F,ou as G,se as H,Ba as I,Zi as J,so as K,ic as L,Et as M,mr as N,An as O,su as P,Ja as Q,Sn as R,Ka as S,Bi as T,nu as U,ru as V,Bl as W,Za as X,Ya as Y,Qa as Z,tu as _,Hi as a,du as a0,Oa as a1,iu as a2,Ta as a3,cu as a4,au as a5,ot as a6,eu as a7,lu as a8,Ra as a9,hu as aa,fu as ab,uu as ac,ar as ad,La as ae,Pi as b,za as c,Rr as d,Xa as e,Rs as f,ie as g,de as h,va as i,St as j,Ni as k,ri as l,ja as m,_r as n,Li as o,Va as p,ya as q,qa as r,xe as s,Da as t,Sa as u,la as v,fl as w,Ge as x,hi as y,Pn as z}; diff --git a/assets/chunks/theme.bfdc69c3.js b/assets/chunks/theme.bfdc69c3.js new file mode 100644 index 000000000..b76ef9c96 --- /dev/null +++ b/assets/chunks/theme.bfdc69c3.js @@ -0,0 +1 @@ +import{d as g,o as a,c as i,r as u,n as M,a as H,t as w,_ as m,b as $,w as _,T as ie,e as f,u as Fe,i as Oe,f as ce,g as y,h as L,j as O,k as c,l,p as E,m as D,q,s as J,v as ae,x as R,y as ue,z as ee,A as Le,B as Ge,C as j,F as S,D as x,E as de,G as W,H as h,I as U,J as Se,K as te,L as Z,M as se,N as Me,O as Ue,P as Ce,Q as Re,R as je,S as qe,U as Ke,V as Ie,W as Ne,X as We,Y as Ye,Z as Je,$ as Xe}from"./framework.dc1f4608.js";const Ze=g({__name:"VPBadge",props:{text:{},type:{default:"tip"}},setup(s){return(e,t)=>(a(),i("span",{class:M(["VPBadge",e.type])},[u(e.$slots,"default",{},()=>[H(w(e.text),1)],!0)],2))}});const Qe=m(Ze,[["__scopeId","data-v-9613cc9f"]]),et={key:0,class:"VPBackdrop"},tt=g({__name:"VPBackdrop",props:{show:{type:Boolean}},setup(s){return(e,t)=>(a(),$(ie,{name:"fade"},{default:_(()=>[e.show?(a(),i("div",et)):f("",!0)]),_:1}))}});const st=m(tt,[["__scopeId","data-v-c79a1216"]]),P=Fe;function nt(s,e){let t,n=!1;return()=>{t&&clearTimeout(t),n?t=setTimeout(s,e):(s(),(n=!0)&&setTimeout(()=>n=!1,e))}}function le(s){return/^\//.test(s)?s:`/${s}`}function Y(s){if(Oe(s))return s;const{site:e}=P(),{pathname:t,search:n,hash:o}=new URL(s,"http://a.com"),r=t.endsWith("/")||t.endsWith(".html")?s:s.replace(/(?:(^\.+)\/)?.*$/,`$1${t.replace(/(\.md)?$/,e.value.cleanUrls?"":".html")}${n}${o}`);return ce(r)}function X({removeCurrent:s=!0,correspondingLink:e=!1}={}){const{site:t,localeIndex:n,page:o,theme:r}=P(),d=y(()=>{var v,b;return{label:(v=t.value.locales[n.value])==null?void 0:v.label,link:((b=t.value.locales[n.value])==null?void 0:b.link)||(n.value==="root"?"/":`/${n.value}/`)}});return{localeLinks:y(()=>Object.entries(t.value.locales).flatMap(([v,b])=>s&&d.value.label===b.label?[]:{text:b.label,link:ot(b.link||(v==="root"?"/":`/${v}/`),r.value.i18nRouting!==!1&&e,o.value.relativePath.slice(d.value.link.length-1),!t.value.cleanUrls)})),currentLang:d}}function ot(s,e,t,n){return e?s.replace(/\/$/,"")+le(t.replace(/(^|\/)index\.md$/,"$1").replace(/\.md$/,n?".html":"")):s}const at=s=>(E("data-v-f87ff6e4"),s=s(),D(),s),lt={class:"NotFound"},rt={class:"code"},it={class:"title"},ct=at(()=>c("div",{class:"divider"},null,-1)),ut={class:"quote"},dt={class:"action"},_t=["href","aria-label"],vt=g({__name:"NotFound",setup(s){const{site:e,theme:t}=P(),{localeLinks:n}=X({removeCurrent:!1}),o=L("/");return O(()=>{var d;const r=window.location.pathname.replace(e.value.base,"").replace(/(^.*?\/).*$/,"/$1");n.value.length&&(o.value=((d=n.value.find(({link:p})=>p.startsWith(r)))==null?void 0:d.link)||n.value[0].link)}),(r,d)=>{var p,v,b,N,T;return a(),i("div",lt,[c("p",rt,w(((p=l(t).notFound)==null?void 0:p.code)??"404"),1),c("h1",it,w(((v=l(t).notFound)==null?void 0:v.title)??"PAGE NOT FOUND"),1),ct,c("blockquote",ut,w(((b=l(t).notFound)==null?void 0:b.quote)??"But if you don't change your direction, and if you keep looking, you may end up where you are heading."),1),c("div",dt,[c("a",{class:"link",href:l(ce)(o.value),"aria-label":((N=l(t).notFound)==null?void 0:N.linkLabel)??"go to home"},w(((T=l(t).notFound)==null?void 0:T.linkText)??"Take me home"),9,_t)])])}}});const pt=m(vt,[["__scopeId","data-v-f87ff6e4"]]);function Te(s,e){if(Array.isArray(s))return Q(s);if(s==null)return[];e=le(e);const t=Object.keys(s).sort((o,r)=>r.split("/").length-o.split("/").length).find(o=>e.startsWith(le(o))),n=t?s[t]:[];return Array.isArray(n)?Q(n):Q(n.items,n.base)}function ht(s){const e=[];let t=0;for(const n in s){const o=s[n];if(o.items){t=e.push(o);continue}e[t]||e.push({items:[]}),e[t].items.push(o)}return e}function ft(s){const e=[];function t(n){for(const o of n)o.text&&o.link&&e.push({text:o.text,link:o.link,docFooterText:o.docFooterText}),o.items&&t(o.items)}return t(s),e}function re(s,e){return Array.isArray(e)?e.some(t=>re(s,t)):q(s,e.link)?!0:e.items?re(s,e.items):!1}function Q(s,e){return[...s].map(t=>{const n={...t},o=n.base||e;return o&&n.link&&(n.link=o+n.link),n.items&&(n.items=Q(n.items,o)),n})}function F(){const{frontmatter:s,page:e,theme:t}=P(),n=ae("(min-width: 960px)"),o=L(!1),r=y(()=>{const V=t.value.sidebar,k=e.value.relativePath;return V?Te(V,k):[]}),d=L(r.value);R(r,(V,k)=>{JSON.stringify(V)!==JSON.stringify(k)&&(d.value=r.value)});const p=y(()=>s.value.sidebar!==!1&&d.value.length>0&&s.value.layout!=="home"),v=y(()=>b?s.value.aside==null?t.value.aside==="left":s.value.aside==="left":!1),b=y(()=>s.value.layout==="home"?!1:s.value.aside!=null?!!s.value.aside:t.value.aside!==!1),N=y(()=>p.value&&n.value),T=y(()=>p.value?ht(d.value):[]);function A(){o.value=!0}function I(){o.value=!1}function B(){o.value?I():A()}return{isOpen:o,sidebar:d,sidebarGroups:T,hasSidebar:p,hasAside:b,leftAside:v,isSidebarEnabled:N,open:A,close:I,toggle:B}}function mt(s,e){let t;ue(()=>{t=s.value?document.activeElement:void 0}),O(()=>{window.addEventListener("keyup",n)}),ee(()=>{window.removeEventListener("keyup",n)});function n(o){o.key==="Escape"&&s.value&&(e(),t==null||t.focus())}}const Be=L(J?location.hash:"");J&&window.addEventListener("hashchange",()=>{Be.value=location.hash});function gt(s){const{page:e}=P(),t=L(!1),n=y(()=>s.value.collapsed!=null),o=y(()=>!!s.value.link),r=L(!1),d=()=>{r.value=q(e.value.relativePath,s.value.link)};R([e,s,Be],d),O(d);const p=y(()=>r.value?!0:s.value.items?re(e.value.relativePath,s.value.items):!1),v=y(()=>!!(s.value.items&&s.value.items.length));ue(()=>{t.value=!!(n.value&&s.value.collapsed)}),Le(()=>{(r.value||p.value)&&(t.value=!1)});function b(){n.value&&(t.value=!t.value)}return{collapsed:t,collapsible:n,isLink:o,isActiveLink:r,hasActiveLink:p,hasChildren:v,toggle:b}}function $t(){const{hasSidebar:s}=F(),e=ae("(min-width: 960px)"),t=ae("(min-width: 1280px)");return{isAsideEnabled:y(()=>!t.value&&!e.value?!1:s.value?t.value:e.value)}}const kt=71;function _e(s){return typeof s.outline=="object"&&!Array.isArray(s.outline)&&s.outline.label||s.outlineTitle||"On this page"}function ve(s){const e=[...document.querySelectorAll(".VPDoc :where(h1,h2,h3,h4,h5,h6)")].filter(t=>t.id&&t.hasChildNodes()).map(t=>{const n=Number(t.tagName[1]);return{title:bt(t),link:"#"+t.id,level:n}});return yt(e,s)}function bt(s){let e="";for(const t of s.childNodes)if(t.nodeType===1){if(t.classList.contains("VPBadge")||t.classList.contains("header-anchor"))continue;e+=t.textContent}else t.nodeType===3&&(e+=t.textContent);return e.trim()}function yt(s,e){if(e===!1)return[];const t=(typeof e=="object"&&!Array.isArray(e)?e.level:e)||2,[n,o]=typeof t=="number"?[t,t]:t==="deep"?[2,6]:t;s=s.filter(d=>d.level>=n&&d.level<=o);const r=[];e:for(let d=0;d=0;v--){const b=s[v];if(b.level{requestAnimationFrame(r),window.addEventListener("scroll",n)}),Ge(()=>{d(location.hash)}),ee(()=>{window.removeEventListener("scroll",n)});function r(){if(!t.value)return;const p=[].slice.call(s.value.querySelectorAll(".outline-link")),v=[].slice.call(document.querySelectorAll(".content .header-anchor")).filter(I=>p.some(B=>B.hash===I.hash&&I.offsetParent!==null)),b=window.scrollY,N=window.innerHeight,T=document.body.offsetHeight,A=Math.abs(b+N-T)<1;if(v.length&&A){d(v[v.length-1].hash);return}for(let I=0;I{const o=j("VPDocOutlineItem",!0);return a(),i("ul",{class:M(t.root?"root":"nested")},[(a(!0),i(S,null,x(t.headers,({children:r,link:d,title:p})=>(a(),i("li",null,[c("a",{class:"outline-link",href:d,onClick:e,title:p},w(p),9,wt),r!=null&&r.length?(a(),$(o,{key:0,headers:r},null,8,["headers"])):f("",!0)]))),256))],2)}}});const pe=m(Lt,[["__scopeId","data-v-d0ee3533"]]),St=s=>(E("data-v-6ae8e080"),s=s(),D(),s),Mt={class:"content"},Ct={class:"outline-title",role:"heading"},It={"aria-labelledby":"doc-outline-aria-label"},Nt=St(()=>c("span",{class:"visually-hidden",id:"doc-outline-aria-label"}," Table of Contents for current page ",-1)),Tt=g({__name:"VPDocAsideOutline",setup(s){const{frontmatter:e,theme:t}=P(),n=de([]);W(()=>{n.value=ve(e.value.outline??t.value.outline)});const o=L(),r=L();return Pt(o,r),(d,p)=>(a(),i("div",{class:M(["VPDocAsideOutline",{"has-outline":n.value.length>0}]),ref_key:"container",ref:o,role:"navigation"},[c("div",Mt,[c("div",{class:"outline-marker",ref_key:"marker",ref:r},null,512),c("div",Ct,w(l(_e)(l(t))),1),c("nav",It,[Nt,h(pe,{headers:n.value,root:!0},null,8,["headers"])])])],2))}});const Bt=m(Tt,[["__scopeId","data-v-6ae8e080"]]),At={class:"VPDocAsideCarbonAds"},xt=g({__name:"VPDocAsideCarbonAds",props:{carbonAds:{}},setup(s){const e=()=>null;return(t,n)=>(a(),i("div",At,[h(l(e),{"carbon-ads":t.carbonAds},null,8,["carbon-ads"])]))}}),Ht=s=>(E("data-v-3f215769"),s=s(),D(),s),zt={class:"VPDocAside"},Et=Ht(()=>c("div",{class:"spacer"},null,-1)),Dt=g({__name:"VPDocAside",setup(s){const{theme:e}=P();return(t,n)=>(a(),i("div",zt,[u(t.$slots,"aside-top",{},void 0,!0),u(t.$slots,"aside-outline-before",{},void 0,!0),h(Bt),u(t.$slots,"aside-outline-after",{},void 0,!0),Et,u(t.$slots,"aside-ads-before",{},void 0,!0),l(e).carbonAds?(a(),$(xt,{key:0,"carbon-ads":l(e).carbonAds},null,8,["carbon-ads"])):f("",!0),u(t.$slots,"aside-ads-after",{},void 0,!0),u(t.$slots,"aside-bottom",{},void 0,!0)]))}});const Ft=m(Dt,[["__scopeId","data-v-3f215769"]]);function Ot(){const{theme:s,page:e}=P();return y(()=>{const{text:t="Edit this page",pattern:n=""}=s.value.editLink||{};let o;return typeof n=="function"?o=n(e.value):o=n.replace(/:path/g,e.value.filePath),{url:o,text:t}})}function Gt(){const{page:s,theme:e,frontmatter:t}=P();return y(()=>{var v,b,N,T,A,I,B,V;const n=Te(e.value.sidebar,s.value.relativePath),o=ft(n),r=o.findIndex(k=>q(s.value.relativePath,k.link)),d=((v=e.value.docFooter)==null?void 0:v.prev)===!1&&!t.value.prev||t.value.prev===!1,p=((b=e.value.docFooter)==null?void 0:b.next)===!1&&!t.value.next||t.value.next===!1;return{prev:d?void 0:{text:(typeof t.value.prev=="string"?t.value.prev:typeof t.value.prev=="object"?t.value.prev.text:void 0)??((N=o[r-1])==null?void 0:N.docFooterText)??((T=o[r-1])==null?void 0:T.text),link:(typeof t.value.prev=="object"?t.value.prev.link:void 0)??((A=o[r-1])==null?void 0:A.link)},next:p?void 0:{text:(typeof t.value.next=="string"?t.value.next:typeof t.value.next=="object"?t.value.next.text:void 0)??((I=o[r+1])==null?void 0:I.docFooterText)??((B=o[r+1])==null?void 0:B.text),link:(typeof t.value.next=="object"?t.value.next.link:void 0)??((V=o[r+1])==null?void 0:V.link)}}})}const Ut={},Rt={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},jt=c("path",{d:"M18,23H4c-1.7,0-3-1.3-3-3V6c0-1.7,1.3-3,3-3h7c0.6,0,1,0.4,1,1s-0.4,1-1,1H4C3.4,5,3,5.4,3,6v14c0,0.6,0.4,1,1,1h14c0.6,0,1-0.4,1-1v-7c0-0.6,0.4-1,1-1s1,0.4,1,1v7C21,21.7,19.7,23,18,23z"},null,-1),qt=c("path",{d:"M8,17c-0.3,0-0.5-0.1-0.7-0.3C7,16.5,6.9,16.1,7,15.8l1-4c0-0.2,0.1-0.3,0.3-0.5l9.5-9.5c1.2-1.2,3.2-1.2,4.4,0c1.2,1.2,1.2,3.2,0,4.4l-9.5,9.5c-0.1,0.1-0.3,0.2-0.5,0.3l-4,1C8.2,17,8.1,17,8,17zM9.9,12.5l-0.5,2.1l2.1-0.5l9.3-9.3c0.4-0.4,0.4-1.1,0-1.6c-0.4-0.4-1.2-0.4-1.6,0l0,0L9.9,12.5z M18.5,2.5L18.5,2.5L18.5,2.5z"},null,-1),Kt=[jt,qt];function Wt(s,e){return a(),i("svg",Rt,Kt)}const Yt=m(Ut,[["render",Wt]]),G=g({__name:"VPLink",props:{tag:{},href:{},noIcon:{type:Boolean},target:{},rel:{}},setup(s){const e=s,t=y(()=>e.tag??(e.href?"a":"span")),n=y(()=>e.href&&Se.test(e.href));return(o,r)=>(a(),$(U(t.value),{class:M(["VPLink",{link:o.href,"vp-external-link-icon":n.value,"no-icon":o.noIcon}]),href:o.href?l(Y)(o.href):void 0,target:o.target??(n.value?"_blank":void 0),rel:o.rel??(n.value?"noreferrer":void 0)},{default:_(()=>[u(o.$slots,"default")]),_:3},8,["class","href","target","rel"]))}}),Jt={class:"VPLastUpdated"},Xt=["datetime"],Zt=g({__name:"VPDocFooterLastUpdated",setup(s){const{theme:e,page:t,frontmatter:n}=P(),o=y(()=>new Date(n.value.lastUpdated??t.value.lastUpdated)),r=y(()=>o.value.toISOString()),d=L("");return O(()=>{ue(()=>{var p;d.value=new Intl.DateTimeFormat(void 0,((p=e.value.lastUpdated)==null?void 0:p.formatOptions)??{dateStyle:"short",timeStyle:"short"}).format(o.value)})}),(p,v)=>{var b;return a(),i("p",Jt,[H(w(((b=l(e).lastUpdated)==null?void 0:b.text)||l(e).lastUpdatedText||"Last updated")+": ",1),c("time",{datetime:r.value},w(d.value),9,Xt)])}}});const Qt=m(Zt,[["__scopeId","data-v-7de715c0"]]),es={key:0,class:"VPDocFooter"},ts={key:0,class:"edit-info"},ss={key:0,class:"edit-link"},ns={key:1,class:"last-updated"},os={key:1,class:"prev-next"},as={class:"pager"},ls=["href"],rs=["innerHTML"],is=["innerHTML"],cs={class:"pager"},us=["href"],ds=["innerHTML"],_s=["innerHTML"],vs=g({__name:"VPDocFooter",setup(s){const{theme:e,page:t,frontmatter:n}=P(),o=Ot(),r=Gt(),d=y(()=>e.value.editLink&&n.value.editLink!==!1),p=y(()=>t.value.lastUpdated&&n.value.lastUpdated!==!1),v=y(()=>d.value||p.value||r.value.prev||r.value.next);return(b,N)=>{var T,A,I,B,V,k;return v.value?(a(),i("footer",es,[u(b.$slots,"doc-footer-before",{},void 0,!0),d.value||p.value?(a(),i("div",ts,[d.value?(a(),i("div",ss,[h(G,{class:"edit-link-button",href:l(o).url,"no-icon":!0},{default:_(()=>[h(Yt,{class:"edit-link-icon","aria-label":"edit icon"}),H(" "+w(l(o).text),1)]),_:1},8,["href"])])):f("",!0),p.value?(a(),i("div",ns,[h(Qt)])):f("",!0)])):f("",!0),(T=l(r).prev)!=null&&T.link||(A=l(r).next)!=null&&A.link?(a(),i("nav",os,[c("div",as,[(I=l(r).prev)!=null&&I.link?(a(),i("a",{key:0,class:"pager-link prev",href:l(Y)(l(r).prev.link)},[c("span",{class:"desc",innerHTML:((B=l(e).docFooter)==null?void 0:B.prev)||"Previous page"},null,8,rs),c("span",{class:"title",innerHTML:l(r).prev.text},null,8,is)],8,ls)):f("",!0)]),c("div",cs,[(V=l(r).next)!=null&&V.link?(a(),i("a",{key:0,class:"pager-link next",href:l(Y)(l(r).next.link)},[c("span",{class:"desc",innerHTML:((k=l(e).docFooter)==null?void 0:k.next)||"Next page"},null,8,ds),c("span",{class:"title",innerHTML:l(r).next.text},null,8,_s)],8,us)):f("",!0)])])):f("",!0)])):f("",!0)}}});const ps=m(vs,[["__scopeId","data-v-ef5dee53"]]),hs={},fs={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},ms=c("path",{d:"M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"},null,-1),gs=[ms];function $s(s,e){return a(),i("svg",fs,gs)}const he=m(hs,[["render",$s]]),ks={key:0,class:"VPDocOutlineDropdown"},bs={key:0,class:"items"},ys=g({__name:"VPDocOutlineDropdown",setup(s){const{frontmatter:e,theme:t}=P(),n=L(!1);W(()=>{n.value=!1});const o=de([]);return W(()=>{o.value=ve(e.value.outline??t.value.outline)}),(r,d)=>o.value.length>0?(a(),i("div",ks,[c("button",{onClick:d[0]||(d[0]=p=>n.value=!n.value),class:M({open:n.value})},[H(w(l(_e)(l(t)))+" ",1),h(he,{class:"icon"})],2),n.value?(a(),i("div",bs,[h(pe,{headers:o.value},null,8,["headers"])])):f("",!0)])):f("",!0)}});const Ps=m(ys,[["__scopeId","data-v-eadfb36b"]]),Vs=s=>(E("data-v-6b87e69f"),s=s(),D(),s),ws={class:"container"},Ls=Vs(()=>c("div",{class:"aside-curtain"},null,-1)),Ss={class:"aside-container"},Ms={class:"aside-content"},Cs={class:"content"},Is={class:"content-container"},Ns={class:"main"},Ts=g({__name:"VPDoc",setup(s){const{theme:e}=P(),t=te(),{hasSidebar:n,hasAside:o,leftAside:r}=F(),d=y(()=>t.path.replace(/[./]+/g,"_").replace(/_html$/,""));return(p,v)=>{const b=j("Content");return a(),i("div",{class:M(["VPDoc",{"has-sidebar":l(n),"has-aside":l(o)}])},[u(p.$slots,"doc-top",{},void 0,!0),c("div",ws,[l(o)?(a(),i("div",{key:0,class:M(["aside",{"left-aside":l(r)}])},[Ls,c("div",Ss,[c("div",Ms,[h(Ft,null,{"aside-top":_(()=>[u(p.$slots,"aside-top",{},void 0,!0)]),"aside-bottom":_(()=>[u(p.$slots,"aside-bottom",{},void 0,!0)]),"aside-outline-before":_(()=>[u(p.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":_(()=>[u(p.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":_(()=>[u(p.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":_(()=>[u(p.$slots,"aside-ads-after",{},void 0,!0)]),_:3})])])],2)):f("",!0),c("div",Cs,[c("div",Is,[u(p.$slots,"doc-before",{},void 0,!0),h(Ps),c("main",Ns,[h(b,{class:M(["vp-doc",[d.value,l(e).externalLinkIcon&&"external-link-icon-enabled"]])},null,8,["class"])]),h(ps,null,{"doc-footer-before":_(()=>[u(p.$slots,"doc-footer-before",{},void 0,!0)]),_:3}),u(p.$slots,"doc-after",{},void 0,!0)])])]),u(p.$slots,"doc-bottom",{},void 0,!0)],2)}}});const Bs=m(Ts,[["__scopeId","data-v-6b87e69f"]]),As=g({__name:"VPButton",props:{tag:{},size:{default:"medium"},theme:{default:"brand"},text:{},href:{}},setup(s){const e=s,t=y(()=>e.href&&Se.test(e.href)),n=y(()=>e.tag||e.href?"a":"button");return(o,r)=>(a(),$(U(n.value),{class:M(["VPButton",[o.size,o.theme]]),href:o.href?l(Y)(o.href):void 0,target:t.value?"_blank":void 0,rel:t.value?"noreferrer":void 0},{default:_(()=>[H(w(o.text),1)]),_:1},8,["class","href","target","rel"]))}});const xs=m(As,[["__scopeId","data-v-c1c5efc1"]]),Hs=["src","alt"],zs=g({inheritAttrs:!1,__name:"VPImage",props:{image:{},alt:{}},setup(s){return(e,t)=>{const n=j("VPImage",!0);return e.image?(a(),i(S,{key:0},[typeof e.image=="string"||"src"in e.image?(a(),i("img",Z({key:0,class:"VPImage"},typeof e.image=="string"?e.$attrs:{...e.image,...e.$attrs},{src:l(ce)(typeof e.image=="string"?e.image:e.image.src),alt:e.alt??(typeof e.image=="string"?"":e.image.alt||"")}),null,16,Hs)):(a(),i(S,{key:1},[h(n,Z({class:"dark",image:e.image.dark,alt:e.image.alt},e.$attrs),null,16,["image","alt"]),h(n,Z({class:"light",image:e.image.light,alt:e.image.alt},e.$attrs),null,16,["image","alt"])],64))],64)):f("",!0)}}});const fe=m(zs,[["__scopeId","data-v-8426fc1a"]]),Es=s=>(E("data-v-da5d1713"),s=s(),D(),s),Ds={class:"container"},Fs={class:"main"},Os={key:0,class:"name"},Gs=["innerHTML"],Us=["innerHTML"],Rs=["innerHTML"],js={key:0,class:"actions"},qs={key:0,class:"image"},Ks={class:"image-container"},Ws=Es(()=>c("div",{class:"image-bg"},null,-1)),Ys=g({__name:"VPHero",props:{name:{},text:{},tagline:{},image:{},actions:{}},setup(s){const e=se("hero-image-slot-exists");return(t,n)=>(a(),i("div",{class:M(["VPHero",{"has-image":t.image||l(e)}])},[c("div",Ds,[c("div",Fs,[u(t.$slots,"home-hero-info",{},()=>[t.name?(a(),i("h1",Os,[c("span",{innerHTML:t.name,class:"clip"},null,8,Gs)])):f("",!0),t.text?(a(),i("p",{key:1,innerHTML:t.text,class:"text"},null,8,Us)):f("",!0),t.tagline?(a(),i("p",{key:2,innerHTML:t.tagline,class:"tagline"},null,8,Rs)):f("",!0)],!0),t.actions?(a(),i("div",js,[(a(!0),i(S,null,x(t.actions,o=>(a(),i("div",{key:o.link,class:"action"},[h(xs,{tag:"a",size:"medium",theme:o.theme,text:o.text,href:o.link},null,8,["theme","text","href"])]))),128))])):f("",!0)]),t.image||l(e)?(a(),i("div",qs,[c("div",Ks,[Ws,u(t.$slots,"home-hero-image",{},()=>[t.image?(a(),$(fe,{key:0,class:"image-src",image:t.image},null,8,["image"])):f("",!0)],!0)])])):f("",!0)])],2))}});const Js=m(Ys,[["__scopeId","data-v-da5d1713"]]),Xs=g({__name:"VPHomeHero",setup(s){const{frontmatter:e}=P();return(t,n)=>l(e).hero?(a(),$(Js,{key:0,class:"VPHomeHero",name:l(e).hero.name,text:l(e).hero.text,tagline:l(e).hero.tagline,image:l(e).hero.image,actions:l(e).hero.actions},{"home-hero-info":_(()=>[u(t.$slots,"home-hero-info")]),"home-hero-image":_(()=>[u(t.$slots,"home-hero-image")]),_:3},8,["name","text","tagline","image","actions"])):f("",!0)}}),Zs={},Qs={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},en=c("path",{d:"M19.9,12.4c0.1-0.2,0.1-0.5,0-0.8c-0.1-0.1-0.1-0.2-0.2-0.3l-7-7c-0.4-0.4-1-0.4-1.4,0s-0.4,1,0,1.4l5.3,5.3H5c-0.6,0-1,0.4-1,1s0.4,1,1,1h11.6l-5.3,5.3c-0.4,0.4-0.4,1,0,1.4c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3l7-7C19.8,12.6,19.9,12.5,19.9,12.4z"},null,-1),tn=[en];function sn(s,e){return a(),i("svg",Qs,tn)}const nn=m(Zs,[["render",sn]]),on={class:"box"},an=["innerHTML"],ln=["innerHTML"],rn=["innerHTML"],cn={key:3,class:"link-text"},un={class:"link-text-value"},dn=g({__name:"VPFeature",props:{icon:{},title:{},details:{},link:{},linkText:{},rel:{}},setup(s){return(e,t)=>(a(),$(G,{class:"VPFeature",href:e.link,rel:e.rel,"no-icon":!0,tag:e.link?"a":"div"},{default:_(()=>[c("article",on,[typeof e.icon=="object"?(a(),$(fe,{key:0,image:e.icon,alt:e.icon.alt,height:e.icon.height||48,width:e.icon.width||48},null,8,["image","alt","height","width"])):e.icon?(a(),i("div",{key:1,class:"icon",innerHTML:e.icon},null,8,an)):f("",!0),c("h2",{class:"title",innerHTML:e.title},null,8,ln),e.details?(a(),i("p",{key:2,class:"details",innerHTML:e.details},null,8,rn)):f("",!0),e.linkText?(a(),i("div",cn,[c("p",un,[H(w(e.linkText)+" ",1),h(nn,{class:"link-text-icon"})])])):f("",!0)])]),_:1},8,["href","rel","tag"]))}});const _n=m(dn,[["__scopeId","data-v-3e216711"]]),vn={key:0,class:"VPFeatures"},pn={class:"container"},hn={class:"items"},fn=g({__name:"VPFeatures",props:{features:{}},setup(s){const e=s,t=y(()=>{const n=e.features.length;if(n){if(n===2)return"grid-2";if(n===3)return"grid-3";if(n%3===0)return"grid-6";if(n>3)return"grid-4"}else return});return(n,o)=>n.features?(a(),i("div",vn,[c("div",pn,[c("div",hn,[(a(!0),i(S,null,x(n.features,r=>(a(),i("div",{key:r.title,class:M(["item",[t.value]])},[h(_n,{icon:r.icon,title:r.title,details:r.details,link:r.link,"link-text":r.linkText,rel:r.rel},null,8,["icon","title","details","link","link-text","rel"])],2))),128))])])])):f("",!0)}});const mn=m(fn,[["__scopeId","data-v-39646fad"]]),gn=g({__name:"VPHomeFeatures",setup(s){const{frontmatter:e}=P();return(t,n)=>l(e).features?(a(),$(mn,{key:0,class:"VPHomeFeatures",features:l(e).features},null,8,["features"])):f("",!0)}}),$n={class:"VPHome"},kn=g({__name:"VPHome",setup(s){return(e,t)=>{const n=j("Content");return a(),i("div",$n,[u(e.$slots,"home-hero-before",{},void 0,!0),h(Xs,null,{"home-hero-info":_(()=>[u(e.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-image":_(()=>[u(e.$slots,"home-hero-image",{},void 0,!0)]),_:3}),u(e.$slots,"home-hero-after",{},void 0,!0),u(e.$slots,"home-features-before",{},void 0,!0),h(gn),u(e.$slots,"home-features-after",{},void 0,!0),h(n)])}}});const bn=m(kn,[["__scopeId","data-v-d82743a8"]]),yn={},Pn={class:"VPPage"};function Vn(s,e){const t=j("Content");return a(),i("div",Pn,[u(s.$slots,"page-top"),h(t),u(s.$slots,"page-bottom")])}const wn=m(yn,[["render",Vn]]),Ln=g({__name:"VPContent",setup(s){const{page:e,frontmatter:t}=P(),{hasSidebar:n}=F();return(o,r)=>(a(),i("div",{class:M(["VPContent",{"has-sidebar":l(n),"is-home":l(t).layout==="home"}]),id:"VPContent"},[l(e).isNotFound?u(o.$slots,"not-found",{key:0},()=>[h(pt)],!0):l(t).layout==="page"?(a(),$(wn,{key:1},{"page-top":_(()=>[u(o.$slots,"page-top",{},void 0,!0)]),"page-bottom":_(()=>[u(o.$slots,"page-bottom",{},void 0,!0)]),_:3})):l(t).layout==="home"?(a(),$(bn,{key:2},{"home-hero-before":_(()=>[u(o.$slots,"home-hero-before",{},void 0,!0)]),"home-hero-info":_(()=>[u(o.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-image":_(()=>[u(o.$slots,"home-hero-image",{},void 0,!0)]),"home-hero-after":_(()=>[u(o.$slots,"home-hero-after",{},void 0,!0)]),"home-features-before":_(()=>[u(o.$slots,"home-features-before",{},void 0,!0)]),"home-features-after":_(()=>[u(o.$slots,"home-features-after",{},void 0,!0)]),_:3})):l(t).layout&&l(t).layout!=="doc"?(a(),$(U(l(t).layout),{key:3})):(a(),$(Bs,{key:4},{"doc-top":_(()=>[u(o.$slots,"doc-top",{},void 0,!0)]),"doc-bottom":_(()=>[u(o.$slots,"doc-bottom",{},void 0,!0)]),"doc-footer-before":_(()=>[u(o.$slots,"doc-footer-before",{},void 0,!0)]),"doc-before":_(()=>[u(o.$slots,"doc-before",{},void 0,!0)]),"doc-after":_(()=>[u(o.$slots,"doc-after",{},void 0,!0)]),"aside-top":_(()=>[u(o.$slots,"aside-top",{},void 0,!0)]),"aside-outline-before":_(()=>[u(o.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":_(()=>[u(o.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":_(()=>[u(o.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":_(()=>[u(o.$slots,"aside-ads-after",{},void 0,!0)]),"aside-bottom":_(()=>[u(o.$slots,"aside-bottom",{},void 0,!0)]),_:3}))],2))}});const Sn=m(Ln,[["__scopeId","data-v-669faec9"]]),Mn={class:"container"},Cn=["innerHTML"],In=["innerHTML"],Nn=g({__name:"VPFooter",setup(s){const{theme:e,frontmatter:t}=P(),{hasSidebar:n}=F();return(o,r)=>l(e).footer&&l(t).footer!==!1?(a(),i("footer",{key:0,class:M(["VPFooter",{"has-sidebar":l(n)}])},[c("div",Mn,[l(e).footer.message?(a(),i("p",{key:0,class:"message",innerHTML:l(e).footer.message},null,8,Cn)):f("",!0),l(e).footer.copyright?(a(),i("p",{key:1,class:"copyright",innerHTML:l(e).footer.copyright},null,8,In)):f("",!0)])],2)):f("",!0)}});const Tn=m(Nn,[["__scopeId","data-v-e03eb2e1"]]),Bn={class:"header"},An={class:"outline"},xn=g({__name:"VPLocalNavOutlineDropdown",props:{headers:{},navHeight:{}},setup(s){const e=s,{theme:t}=P(),n=L(!1),o=L(0),r=L();W(()=>{n.value=!1});function d(){n.value=!n.value,o.value=window.innerHeight+Math.min(window.scrollY-e.navHeight,0)}function p(b){b.target.classList.contains("outline-link")&&(r.value&&(r.value.style.transition="none"),Ue(()=>{n.value=!1}))}function v(){n.value=!1,window.scrollTo({top:0,left:0,behavior:"smooth"})}return(b,N)=>(a(),i("div",{class:"VPLocalNavOutlineDropdown",style:Me({"--vp-vh":o.value+"px"})},[b.headers.length>0?(a(),i("button",{key:0,onClick:d,class:M({open:n.value})},[H(w(l(_e)(l(t)))+" ",1),h(he,{class:"icon"})],2)):(a(),i("button",{key:1,onClick:v},w(l(t).returnToTopLabel||"Return to top"),1)),h(ie,{name:"flyout"},{default:_(()=>[n.value?(a(),i("div",{key:0,ref_key:"items",ref:r,class:"items",onClick:p},[c("div",Bn,[c("a",{class:"top-link",href:"#",onClick:v},w(l(t).returnToTopLabel||"Return to top"),1)]),c("div",An,[h(pe,{headers:b.headers},null,8,["headers"])])],512)):f("",!0)]),_:1})],4))}});const Hn=m(xn,[["__scopeId","data-v-1c15a60a"]]),zn={},En={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},Dn=c("path",{d:"M17,11H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S17.6,11,17,11z"},null,-1),Fn=c("path",{d:"M21,7H3C2.4,7,2,6.6,2,6s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,7,21,7z"},null,-1),On=c("path",{d:"M21,15H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,15,21,15z"},null,-1),Gn=c("path",{d:"M17,19H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S17.6,19,17,19z"},null,-1),Un=[Dn,Fn,On,Gn];function Rn(s,e){return a(),i("svg",En,Un)}const jn=m(zn,[["render",Rn]]),qn=["aria-expanded"],Kn={class:"menu-text"},Wn=g({__name:"VPLocalNav",props:{open:{type:Boolean}},emits:["open-menu"],setup(s){const{theme:e,frontmatter:t}=P(),{hasSidebar:n}=F(),{y:o}=Ce(),r=de([]),d=L(0);O(()=>{d.value=parseInt(getComputedStyle(document.documentElement).getPropertyValue("--vp-nav-height"))}),W(()=>{r.value=ve(t.value.outline??e.value.outline)});const p=y(()=>r.value.length===0&&!n.value),v=y(()=>({VPLocalNav:!0,fixed:p.value,"reached-top":o.value>=d.value}));return(b,N)=>l(t).layout!=="home"&&(!p.value||l(o)>=d.value)?(a(),i("div",{key:0,class:M(v.value)},[l(n)?(a(),i("button",{key:0,class:"menu","aria-expanded":b.open,"aria-controls":"VPSidebarNav",onClick:N[0]||(N[0]=T=>b.$emit("open-menu"))},[h(jn,{class:"menu-icon"}),c("span",Kn,w(l(e).sidebarMenuLabel||"Menu"),1)],8,qn)):f("",!0),h(Hn,{headers:r.value,navHeight:d.value},null,8,["headers","navHeight"])],2)):f("",!0)}});const Yn=m(Wn,[["__scopeId","data-v-79c8c1df"]]);function Jn(){const s=L(!1);function e(){s.value=!0,window.addEventListener("resize",o)}function t(){s.value=!1,window.removeEventListener("resize",o)}function n(){s.value?t():e()}function o(){window.outerWidth>=768&&t()}const r=te();return R(()=>r.path,t),{isScreenOpen:s,openScreen:e,closeScreen:t,toggleScreen:n}}const Xn={},Zn={class:"VPSwitch",type:"button",role:"switch"},Qn={class:"check"},eo={key:0,class:"icon"};function to(s,e){return a(),i("button",Zn,[c("span",Qn,[s.$slots.default?(a(),i("span",eo,[u(s.$slots,"default",{},void 0,!0)])):f("",!0)])])}const so=m(Xn,[["render",to],["__scopeId","data-v-b1685198"]]),no={},oo={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},ao=c("path",{d:"M12.1,22c-0.3,0-0.6,0-0.9,0c-5.5-0.5-9.5-5.4-9-10.9c0.4-4.8,4.2-8.6,9-9c0.4,0,0.8,0.2,1,0.5c0.2,0.3,0.2,0.8-0.1,1.1c-2,2.7-1.4,6.4,1.3,8.4c2.1,1.6,5,1.6,7.1,0c0.3-0.2,0.7-0.3,1.1-0.1c0.3,0.2,0.5,0.6,0.5,1c-0.2,2.7-1.5,5.1-3.6,6.8C16.6,21.2,14.4,22,12.1,22zM9.3,4.4c-2.9,1-5,3.6-5.2,6.8c-0.4,4.4,2.8,8.3,7.2,8.7c2.1,0.2,4.2-0.4,5.8-1.8c1.1-0.9,1.9-2.1,2.4-3.4c-2.5,0.9-5.3,0.5-7.5-1.1C9.2,11.4,8.1,7.7,9.3,4.4z"},null,-1),lo=[ao];function ro(s,e){return a(),i("svg",oo,lo)}const io=m(no,[["render",ro]]),co={},uo={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},_o=Re('',9),vo=[_o];function po(s,e){return a(),i("svg",uo,vo)}const ho=m(co,[["render",po]]),fo=g({__name:"VPSwitchAppearance",setup(s){const{isDark:e}=P(),t=se("toggle-appearance",()=>{e.value=!e.value});return(n,o)=>(a(),$(so,{title:"toggle dark mode",class:"VPSwitchAppearance","aria-checked":l(e),onClick:l(t)},{default:_(()=>[h(ho,{class:"sun"}),h(io,{class:"moon"})]),_:1},8,["aria-checked","onClick"]))}});const me=m(fo,[["__scopeId","data-v-ce54a7d1"]]),mo={key:0,class:"VPNavBarAppearance"},go=g({__name:"VPNavBarAppearance",setup(s){const{site:e}=P();return(t,n)=>l(e).appearance?(a(),i("div",mo,[h(me)])):f("",!0)}});const $o=m(go,[["__scopeId","data-v-f6a63727"]]),ge=L();let Ae=!1,oe=0;function ko(s){const e=L(!1);if(J){!Ae&&bo(),oe++;const t=R(ge,n=>{var o,r,d;n===s.el.value||(o=s.el.value)!=null&&o.contains(n)?(e.value=!0,(r=s.onFocus)==null||r.call(s)):(e.value=!1,(d=s.onBlur)==null||d.call(s))});ee(()=>{t(),oe--,oe||yo()})}return je(e)}function bo(){document.addEventListener("focusin",xe),Ae=!0,ge.value=document.activeElement}function yo(){document.removeEventListener("focusin",xe)}function xe(){ge.value=document.activeElement}const Po={},Vo={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},wo=c("path",{d:"M12,16c-0.3,0-0.5-0.1-0.7-0.3l-6-6c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l5.3,5.3l5.3-5.3c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-6,6C12.5,15.9,12.3,16,12,16z"},null,-1),Lo=[wo];function So(s,e){return a(),i("svg",Vo,Lo)}const He=m(Po,[["render",So]]),Mo={},Co={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},Io=c("circle",{cx:"12",cy:"12",r:"2"},null,-1),No=c("circle",{cx:"19",cy:"12",r:"2"},null,-1),To=c("circle",{cx:"5",cy:"12",r:"2"},null,-1),Bo=[Io,No,To];function Ao(s,e){return a(),i("svg",Co,Bo)}const xo=m(Mo,[["render",Ao]]),Ho={class:"VPMenuLink"},zo=g({__name:"VPMenuLink",props:{item:{}},setup(s){const{page:e}=P();return(t,n)=>(a(),i("div",Ho,[h(G,{class:M({active:l(q)(l(e).relativePath,t.item.activeMatch||t.item.link,!!t.item.activeMatch)}),href:t.item.link,target:t.item.target,rel:t.item.rel},{default:_(()=>[H(w(t.item.text),1)]),_:1},8,["class","href","target","rel"])]))}});const ne=m(zo,[["__scopeId","data-v-43f1e123"]]),Eo={class:"VPMenuGroup"},Do={key:0,class:"title"},Fo=g({__name:"VPMenuGroup",props:{text:{},items:{}},setup(s){return(e,t)=>(a(),i("div",Eo,[e.text?(a(),i("p",Do,w(e.text),1)):f("",!0),(a(!0),i(S,null,x(e.items,n=>(a(),i(S,null,["link"in n?(a(),$(ne,{key:0,item:n},null,8,["item"])):f("",!0)],64))),256))]))}});const Oo=m(Fo,[["__scopeId","data-v-69e747b5"]]),Go={class:"VPMenu"},Uo={key:0,class:"items"},Ro=g({__name:"VPMenu",props:{items:{}},setup(s){return(e,t)=>(a(),i("div",Go,[e.items?(a(),i("div",Uo,[(a(!0),i(S,null,x(e.items,n=>(a(),i(S,{key:n.text},["link"in n?(a(),$(ne,{key:0,item:n},null,8,["item"])):(a(),$(Oo,{key:1,text:n.text,items:n.items},null,8,["text","items"]))],64))),128))])):f("",!0),u(e.$slots,"default",{},void 0,!0)]))}});const jo=m(Ro,[["__scopeId","data-v-e7ea1737"]]),qo=["aria-expanded","aria-label"],Ko={key:0,class:"text"},Wo=["innerHTML"],Yo={class:"menu"},Jo=g({__name:"VPFlyout",props:{icon:{},button:{},label:{},items:{}},setup(s){const e=L(!1),t=L();ko({el:t,onBlur:n});function n(){e.value=!1}return(o,r)=>(a(),i("div",{class:"VPFlyout",ref_key:"el",ref:t,onMouseenter:r[1]||(r[1]=d=>e.value=!0),onMouseleave:r[2]||(r[2]=d=>e.value=!1)},[c("button",{type:"button",class:"button","aria-haspopup":"true","aria-expanded":e.value,"aria-label":o.label,onClick:r[0]||(r[0]=d=>e.value=!e.value)},[o.button||o.icon?(a(),i("span",Ko,[o.icon?(a(),$(U(o.icon),{key:0,class:"option-icon"})):f("",!0),o.button?(a(),i("span",{key:1,innerHTML:o.button},null,8,Wo)):f("",!0),h(He,{class:"text-icon"})])):(a(),$(xo,{key:1,class:"icon"}))],8,qo),c("div",Yo,[h(jo,{items:o.items},{default:_(()=>[u(o.$slots,"default",{},void 0,!0)]),_:3},8,["items"])])],544))}});const $e=m(Jo,[["__scopeId","data-v-9c007e85"]]),Xo={discord:'Discord',facebook:'Facebook',github:'GitHub',instagram:'Instagram',linkedin:'LinkedIn',mastodon:'Mastodon',slack:'Slack',twitter:'Twitter',x:'X',youtube:'YouTube'},Zo=["href","aria-label","innerHTML"],Qo=g({__name:"VPSocialLink",props:{icon:{},link:{},ariaLabel:{}},setup(s){const e=s,t=y(()=>typeof e.icon=="object"?e.icon.svg:Xo[e.icon]);return(n,o)=>(a(),i("a",{class:"VPSocialLink no-icon",href:n.link,"aria-label":n.ariaLabel??(typeof n.icon=="string"?n.icon:""),target:"_blank",rel:"noopener",innerHTML:t.value},null,8,Zo))}});const ea=m(Qo,[["__scopeId","data-v-f80f8133"]]),ta={class:"VPSocialLinks"},sa=g({__name:"VPSocialLinks",props:{links:{}},setup(s){return(e,t)=>(a(),i("div",ta,[(a(!0),i(S,null,x(e.links,({link:n,icon:o,ariaLabel:r})=>(a(),$(ea,{key:n,icon:o,link:n,ariaLabel:r},null,8,["icon","link","ariaLabel"]))),128))]))}});const ke=m(sa,[["__scopeId","data-v-7bc22406"]]),na={key:0,class:"group translations"},oa={class:"trans-title"},aa={key:1,class:"group"},la={class:"item appearance"},ra={class:"label"},ia={class:"appearance-action"},ca={key:2,class:"group"},ua={class:"item social-links"},da=g({__name:"VPNavBarExtra",setup(s){const{site:e,theme:t}=P(),{localeLinks:n,currentLang:o}=X({correspondingLink:!0}),r=y(()=>n.value.length&&o.value.label||e.value.appearance||t.value.socialLinks);return(d,p)=>r.value?(a(),$($e,{key:0,class:"VPNavBarExtra",label:"extra navigation"},{default:_(()=>[l(n).length&&l(o).label?(a(),i("div",na,[c("p",oa,w(l(o).label),1),(a(!0),i(S,null,x(l(n),v=>(a(),$(ne,{key:v.link,item:v},null,8,["item"]))),128))])):f("",!0),l(e).appearance?(a(),i("div",aa,[c("div",la,[c("p",ra,w(l(t).darkModeSwitchLabel||"Appearance"),1),c("div",ia,[h(me)])])])):f("",!0),l(t).socialLinks?(a(),i("div",ca,[c("div",ua,[h(ke,{class:"social-links-list",links:l(t).socialLinks},null,8,["links"])])])):f("",!0)]),_:1})):f("",!0)}});const _a=m(da,[["__scopeId","data-v-40855f84"]]),va=s=>(E("data-v-e5dd9c1c"),s=s(),D(),s),pa=["aria-expanded"],ha=va(()=>c("span",{class:"container"},[c("span",{class:"top"}),c("span",{class:"middle"}),c("span",{class:"bottom"})],-1)),fa=[ha],ma=g({__name:"VPNavBarHamburger",props:{active:{type:Boolean}},emits:["click"],setup(s){return(e,t)=>(a(),i("button",{type:"button",class:M(["VPNavBarHamburger",{active:e.active}]),"aria-label":"mobile navigation","aria-expanded":e.active,"aria-controls":"VPNavScreen",onClick:t[0]||(t[0]=n=>e.$emit("click"))},fa,10,pa))}});const ga=m(ma,[["__scopeId","data-v-e5dd9c1c"]]),$a=["innerHTML"],ka=g({__name:"VPNavBarMenuLink",props:{item:{}},setup(s){const{page:e}=P();return(t,n)=>(a(),$(G,{class:M({VPNavBarMenuLink:!0,active:l(q)(l(e).relativePath,t.item.activeMatch||t.item.link,!!t.item.activeMatch)}),href:t.item.link,target:t.item.target,rel:t.item.rel,tabindex:"0"},{default:_(()=>[c("span",{innerHTML:t.item.text},null,8,$a)]),_:1},8,["class","href","target","rel"]))}});const ba=m(ka,[["__scopeId","data-v-42ef59de"]]),ya=g({__name:"VPNavBarMenuGroup",props:{item:{}},setup(s){const{page:e}=P();return(t,n)=>(a(),$($e,{class:M({VPNavBarMenuGroup:!0,active:l(q)(l(e).relativePath,t.item.activeMatch,!!t.item.activeMatch)}),button:t.item.text,items:t.item.items},null,8,["class","button","items"]))}}),Pa=s=>(E("data-v-7f418b0f"),s=s(),D(),s),Va={key:0,"aria-labelledby":"main-nav-aria-label",class:"VPNavBarMenu"},wa=Pa(()=>c("span",{id:"main-nav-aria-label",class:"visually-hidden"},"Main Navigation",-1)),La=g({__name:"VPNavBarMenu",setup(s){const{theme:e}=P();return(t,n)=>l(e).nav?(a(),i("nav",Va,[wa,(a(!0),i(S,null,x(l(e).nav,o=>(a(),i(S,{key:o.text},["link"in o?(a(),$(ba,{key:0,item:o},null,8,["item"])):(a(),$(ya,{key:1,item:o},null,8,["item"]))],64))),128))])):f("",!0)}});const Sa=m(La,[["__scopeId","data-v-7f418b0f"]]);const Ma={type:"button",class:"DocSearch DocSearch-Button","aria-label":"Search"},Ca={class:"DocSearch-Button-Container"},Ia=c("svg",{class:"DocSearch-Search-Icon",width:"20",height:"20",viewBox:"0 0 20 20","aria-label":"search icon"},[c("path",{d:"M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z",stroke:"currentColor",fill:"none","fill-rule":"evenodd","stroke-linecap":"round","stroke-linejoin":"round"})],-1),Na={class:"DocSearch-Button-Placeholder"},Ta=c("span",{class:"DocSearch-Button-Keys"},[c("kbd",{class:"DocSearch-Button-Key"}),c("kbd",{class:"DocSearch-Button-Key"},"K")],-1),we=g({__name:"VPNavBarSearchButton",props:{placeholder:{}},setup(s){return(e,t)=>(a(),i("button",Ma,[c("span",Ca,[Ia,c("span",Na,w(e.placeholder),1)]),Ta]))}});const Ba={id:"local-search"},Aa={key:1,id:"docsearch"},xa=g({__name:"VPNavBarSearch",setup(s){const e=()=>null,t=qe(()=>Ke(()=>import("./VPAlgoliaSearchBox.bbe9601b.js"),["assets/chunks/VPAlgoliaSearchBox.bbe9601b.js","assets/chunks/framework.dc1f4608.js"])),{theme:n,localeIndex:o}=P(),r=L(!1),d=L(!1),p=y(()=>{var k,C,z,K,be,ye,Pe;const V=((k=n.value.search)==null?void 0:k.options)??n.value.algolia;return((be=(K=(z=(C=V==null?void 0:V.locales)==null?void 0:C[o.value])==null?void 0:z.translations)==null?void 0:K.button)==null?void 0:be.buttonText)||((Pe=(ye=V==null?void 0:V.translations)==null?void 0:ye.button)==null?void 0:Pe.buttonText)||"Search"}),v=()=>{const V="VPAlgoliaPreconnect";(window.requestIdleCallback||setTimeout)(()=>{var z;const C=document.createElement("link");C.id=V,C.rel="preconnect",C.href=`https://${(((z=n.value.search)==null?void 0:z.options)??n.value.algolia).appId}-dsn.algolia.net`,C.crossOrigin="",document.head.appendChild(C)})};O(()=>{v();const V=C=>{(C.key.toLowerCase()==="k"&&(C.metaKey||C.ctrlKey)||!T(C)&&C.key==="/")&&(C.preventDefault(),b(),k())},k=()=>{window.removeEventListener("keydown",V)};window.addEventListener("keydown",V),ee(k)});function b(){r.value||(r.value=!0,setTimeout(N,16))}function N(){const V=new Event("keydown");V.key="k",V.metaKey=!0,window.dispatchEvent(V),setTimeout(()=>{document.querySelector(".DocSearch-Modal")||N()},16)}function T(V){const k=V.target,C=k.tagName;return k.isContentEditable||C==="INPUT"||C==="SELECT"||C==="TEXTAREA"}const A=L(!1),I=L("'Meta'");O(()=>{I.value=/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)?"'⌘'":"'Ctrl'"});const B="algolia";return(V,k)=>{var C;return a(),i("div",{class:"VPNavBarSearch",style:Me({"--vp-meta-key":I.value})},[l(B)==="local"?(a(),i(S,{key:0},[A.value?(a(),$(l(e),{key:0,placeholder:p.value,onClose:k[0]||(k[0]=z=>A.value=!1)},null,8,["placeholder"])):f("",!0),c("div",Ba,[h(we,{placeholder:p.value,onClick:k[1]||(k[1]=z=>A.value=!0)},null,8,["placeholder"])])],64)):l(B)==="algolia"?(a(),i(S,{key:1},[r.value?(a(),$(l(t),{key:0,algolia:((C=l(n).search)==null?void 0:C.options)??l(n).algolia,onVnodeBeforeMount:k[2]||(k[2]=z=>d.value=!0)},null,8,["algolia"])):f("",!0),d.value?f("",!0):(a(),i("div",Aa,[h(we,{placeholder:p.value,onClick:b},null,8,["placeholder"])]))],64)):f("",!0)],4)}}});const Ha=g({__name:"VPNavBarSocialLinks",setup(s){const{theme:e}=P();return(t,n)=>l(e).socialLinks?(a(),$(ke,{key:0,class:"VPNavBarSocialLinks",links:l(e).socialLinks},null,8,["links"])):f("",!0)}});const za=m(Ha,[["__scopeId","data-v-0394ad82"]]),Ea=["href"],Da=g({__name:"VPNavBarTitle",setup(s){const{site:e,theme:t}=P(),{hasSidebar:n}=F(),{currentLang:o}=X();return(r,d)=>(a(),i("div",{class:M(["VPNavBarTitle",{"has-sidebar":l(n)}])},[c("a",{class:"title",href:l(t).logoLink??l(Y)(l(o).link)},[u(r.$slots,"nav-bar-title-before",{},void 0,!0),l(t).logo?(a(),$(fe,{key:0,class:"logo",image:l(t).logo},null,8,["image"])):f("",!0),l(t).siteTitle?(a(),i(S,{key:1},[H(w(l(t).siteTitle),1)],64)):l(t).siteTitle===void 0?(a(),i(S,{key:2},[H(w(l(e).title),1)],64)):f("",!0),u(r.$slots,"nav-bar-title-after",{},void 0,!0)],8,Ea)],2))}});const Fa=m(Da,[["__scopeId","data-v-86d1bed8"]]),Oa={},Ga={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},Ua=c("path",{d:"M0 0h24v24H0z",fill:"none"},null,-1),Ra=c("path",{d:" M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z ",class:"css-c4d79v"},null,-1),ja=[Ua,Ra];function qa(s,e){return a(),i("svg",Ga,ja)}const ze=m(Oa,[["render",qa]]),Ka={class:"items"},Wa={class:"title"},Ya=g({__name:"VPNavBarTranslations",setup(s){const{theme:e}=P(),{localeLinks:t,currentLang:n}=X({correspondingLink:!0});return(o,r)=>l(t).length&&l(n).label?(a(),$($e,{key:0,class:"VPNavBarTranslations",icon:ze,label:l(e).langMenuLabel||"Change language"},{default:_(()=>[c("div",Ka,[c("p",Wa,w(l(n).label),1),(a(!0),i(S,null,x(l(t),d=>(a(),$(ne,{key:d.link,item:d},null,8,["item"]))),128))])]),_:1},8,["label"])):f("",!0)}});const Ja=m(Ya,[["__scopeId","data-v-74abcbb9"]]),Xa=s=>(E("data-v-a0fd61f4"),s=s(),D(),s),Za={class:"container"},Qa={class:"title"},el={class:"content"},tl=Xa(()=>c("div",{class:"curtain"},null,-1)),sl={class:"content-body"},nl=g({__name:"VPNavBar",props:{isScreenOpen:{type:Boolean}},emits:["toggle-screen"],setup(s){const{y:e}=Ce(),{hasSidebar:t}=F(),{frontmatter:n}=P(),o=L({});return Le(()=>{o.value={"has-sidebar":t.value,top:n.value.layout==="home"&&e.value===0}}),(r,d)=>(a(),i("div",{class:M(["VPNavBar",o.value])},[c("div",Za,[c("div",Qa,[h(Fa,null,{"nav-bar-title-before":_(()=>[u(r.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":_(()=>[u(r.$slots,"nav-bar-title-after",{},void 0,!0)]),_:3})]),c("div",el,[tl,c("div",sl,[u(r.$slots,"nav-bar-content-before",{},void 0,!0),h(xa,{class:"search"}),h(Sa,{class:"menu"}),h(Ja,{class:"translations"}),h($o,{class:"appearance"}),h(za,{class:"social-links"}),h(_a,{class:"extra"}),u(r.$slots,"nav-bar-content-after",{},void 0,!0),h(ga,{class:"hamburger",active:r.isScreenOpen,onClick:d[0]||(d[0]=p=>r.$emit("toggle-screen"))},null,8,["active"])])])])],2))}});const ol=m(nl,[["__scopeId","data-v-a0fd61f4"]]),al={key:0,class:"VPNavScreenAppearance"},ll={class:"text"},rl=g({__name:"VPNavScreenAppearance",setup(s){const{site:e,theme:t}=P();return(n,o)=>l(e).appearance?(a(),i("div",al,[c("p",ll,w(l(t).darkModeSwitchLabel||"Appearance"),1),h(me)])):f("",!0)}});const il=m(rl,[["__scopeId","data-v-add8f686"]]),cl=g({__name:"VPNavScreenMenuLink",props:{item:{}},setup(s){const e=se("close-screen");return(t,n)=>(a(),$(G,{class:"VPNavScreenMenuLink",href:t.item.link,target:t.item.target,rel:t.item.rel,onClick:l(e)},{default:_(()=>[H(w(t.item.text),1)]),_:1},8,["href","target","rel","onClick"]))}});const ul=m(cl,[["__scopeId","data-v-05f27b2a"]]),dl={},_l={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},vl=c("path",{d:"M18.9,10.9h-6v-6c0-0.6-0.4-1-1-1s-1,0.4-1,1v6h-6c-0.6,0-1,0.4-1,1s0.4,1,1,1h6v6c0,0.6,0.4,1,1,1s1-0.4,1-1v-6h6c0.6,0,1-0.4,1-1S19.5,10.9,18.9,10.9z"},null,-1),pl=[vl];function hl(s,e){return a(),i("svg",_l,pl)}const fl=m(dl,[["render",hl]]),ml=g({__name:"VPNavScreenMenuGroupLink",props:{item:{}},setup(s){const e=se("close-screen");return(t,n)=>(a(),$(G,{class:"VPNavScreenMenuGroupLink",href:t.item.link,target:t.item.target,rel:t.item.rel,onClick:l(e)},{default:_(()=>[H(w(t.item.text),1)]),_:1},8,["href","target","rel","onClick"]))}});const Ee=m(ml,[["__scopeId","data-v-19976ae1"]]),gl={class:"VPNavScreenMenuGroupSection"},$l={key:0,class:"title"},kl=g({__name:"VPNavScreenMenuGroupSection",props:{text:{},items:{}},setup(s){return(e,t)=>(a(),i("div",gl,[e.text?(a(),i("p",$l,w(e.text),1)):f("",!0),(a(!0),i(S,null,x(e.items,n=>(a(),$(Ee,{key:n.text,item:n},null,8,["item"]))),128))]))}});const bl=m(kl,[["__scopeId","data-v-8133b170"]]),yl=["aria-controls","aria-expanded"],Pl={class:"button-text"},Vl=["id"],wl={key:1,class:"group"},Ll=g({__name:"VPNavScreenMenuGroup",props:{text:{},items:{}},setup(s){const e=s,t=L(!1),n=y(()=>`NavScreenGroup-${e.text.replace(" ","-").toLowerCase()}`);function o(){t.value=!t.value}return(r,d)=>(a(),i("div",{class:M(["VPNavScreenMenuGroup",{open:t.value}])},[c("button",{class:"button","aria-controls":n.value,"aria-expanded":t.value,onClick:o},[c("span",Pl,w(r.text),1),h(fl,{class:"button-icon"})],8,yl),c("div",{id:n.value,class:"items"},[(a(!0),i(S,null,x(r.items,p=>(a(),i(S,{key:p.text},["link"in p?(a(),i("div",{key:p.text,class:"item"},[h(Ee,{item:p},null,8,["item"])])):(a(),i("div",wl,[h(bl,{text:p.text,items:p.items},null,8,["text","items"])]))],64))),128))],8,Vl)],2))}});const Sl=m(Ll,[["__scopeId","data-v-1ecb84e7"]]),Ml={key:0,class:"VPNavScreenMenu"},Cl=g({__name:"VPNavScreenMenu",setup(s){const{theme:e}=P();return(t,n)=>l(e).nav?(a(),i("nav",Ml,[(a(!0),i(S,null,x(l(e).nav,o=>(a(),i(S,{key:o.text},["link"in o?(a(),$(ul,{key:0,item:o},null,8,["item"])):(a(),$(Sl,{key:1,text:o.text||"",items:o.items},null,8,["text","items"]))],64))),128))])):f("",!0)}}),Il=g({__name:"VPNavScreenSocialLinks",setup(s){const{theme:e}=P();return(t,n)=>l(e).socialLinks?(a(),$(ke,{key:0,class:"VPNavScreenSocialLinks",links:l(e).socialLinks},null,8,["links"])):f("",!0)}}),Nl={class:"list"},Tl=g({__name:"VPNavScreenTranslations",setup(s){const{localeLinks:e,currentLang:t}=X({correspondingLink:!0}),n=L(!1);function o(){n.value=!n.value}return(r,d)=>l(e).length&&l(t).label?(a(),i("div",{key:0,class:M(["VPNavScreenTranslations",{open:n.value}])},[c("button",{class:"title",onClick:o},[h(ze,{class:"icon lang"}),H(" "+w(l(t).label)+" ",1),h(He,{class:"icon chevron"})]),c("ul",Nl,[(a(!0),i(S,null,x(l(e),p=>(a(),i("li",{key:p.link,class:"item"},[h(G,{class:"link",href:p.link},{default:_(()=>[H(w(p.text),1)]),_:2},1032,["href"])]))),128))])],2)):f("",!0)}});const Bl=m(Tl,[["__scopeId","data-v-d72aa483"]]),Al={class:"container"},xl=g({__name:"VPNavScreen",props:{open:{type:Boolean}},setup(s){const e=L(null),t=Ie(J?document.body:null);return(n,o)=>(a(),$(ie,{name:"fade",onEnter:o[0]||(o[0]=r=>t.value=!0),onAfterLeave:o[1]||(o[1]=r=>t.value=!1)},{default:_(()=>[n.open?(a(),i("div",{key:0,class:"VPNavScreen",ref_key:"screen",ref:e,id:"VPNavScreen"},[c("div",Al,[u(n.$slots,"nav-screen-content-before",{},void 0,!0),h(Cl,{class:"menu"}),h(Bl,{class:"translations"}),h(il,{class:"appearance"}),h(Il,{class:"social-links"}),u(n.$slots,"nav-screen-content-after",{},void 0,!0)])],512)):f("",!0)]),_:3}))}});const Hl=m(xl,[["__scopeId","data-v-cc5739dd"]]),zl={class:"VPNav"},El=g({__name:"VPNav",setup(s){const{isScreenOpen:e,closeScreen:t,toggleScreen:n}=Jn();return Ne("close-screen",t),(o,r)=>(a(),i("header",zl,[h(ol,{"is-screen-open":l(e),onToggleScreen:l(n)},{"nav-bar-title-before":_(()=>[u(o.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":_(()=>[u(o.$slots,"nav-bar-title-after",{},void 0,!0)]),"nav-bar-content-before":_(()=>[u(o.$slots,"nav-bar-content-before",{},void 0,!0)]),"nav-bar-content-after":_(()=>[u(o.$slots,"nav-bar-content-after",{},void 0,!0)]),_:3},8,["is-screen-open","onToggleScreen"]),h(Hl,{open:l(e)},{"nav-screen-content-before":_(()=>[u(o.$slots,"nav-screen-content-before",{},void 0,!0)]),"nav-screen-content-after":_(()=>[u(o.$slots,"nav-screen-content-after",{},void 0,!0)]),_:3},8,["open"])]))}});const Dl=m(El,[["__scopeId","data-v-7e5bc4a5"]]),Fl=s=>(E("data-v-e31bd47b"),s=s(),D(),s),Ol=["role","tabindex"],Gl=Fl(()=>c("div",{class:"indicator"},null,-1)),Ul={key:1,class:"items"},Rl=g({__name:"VPSidebarItem",props:{item:{},depth:{}},setup(s){const e=s,{collapsed:t,collapsible:n,isLink:o,isActiveLink:r,hasActiveLink:d,hasChildren:p,toggle:v}=gt(y(()=>e.item)),b=y(()=>p.value?"section":"div"),N=y(()=>o.value?"a":"div"),T=y(()=>p.value?e.depth+2===7?"p":`h${e.depth+2}`:"p"),A=y(()=>o.value?void 0:"button"),I=y(()=>[[`level-${e.depth}`],{collapsible:n.value},{collapsed:t.value},{"is-link":o.value},{"is-active":r.value},{"has-active":d.value}]);function B(k){"key"in k&&k.key!=="Enter"||!e.item.link&&v()}function V(){e.item.link&&v()}return(k,C)=>{const z=j("VPSidebarItem",!0);return a(),$(U(b.value),{class:M(["VPSidebarItem",I.value])},{default:_(()=>[k.item.text?(a(),i("div",Z({key:0,class:"item",role:A.value},Ye(k.item.items?{click:B,keydown:B}:{},!0),{tabindex:k.item.items&&0}),[Gl,k.item.link?(a(),$(G,{key:0,tag:N.value,class:"link",href:k.item.link,rel:k.item.rel,target:k.item.target},{default:_(()=>[(a(),$(U(T.value),{class:"text",innerHTML:k.item.text},null,8,["innerHTML"]))]),_:1},8,["tag","href","rel","target"])):(a(),$(U(T.value),{key:1,class:"text",innerHTML:k.item.text},null,8,["innerHTML"])),k.item.collapsed!=null?(a(),i("div",{key:2,class:"caret",role:"button","aria-label":"toggle section",onClick:V,onKeydown:We(V,["enter"]),tabindex:"0"},[h(he,{class:"caret-icon"})],32)):f("",!0)],16,Ol)):f("",!0),k.item.items&&k.item.items.length?(a(),i("div",Ul,[k.depth<5?(a(!0),i(S,{key:0},x(k.item.items,K=>(a(),$(z,{key:K.text,item:K,depth:k.depth+1},null,8,["item","depth"]))),128)):f("",!0)])):f("",!0)]),_:1},8,["class"])}}});const jl=m(Rl,[["__scopeId","data-v-e31bd47b"]]),De=s=>(E("data-v-b00e2fdd"),s=s(),D(),s),ql=De(()=>c("div",{class:"curtain"},null,-1)),Kl={class:"nav",id:"VPSidebarNav","aria-labelledby":"sidebar-aria-label",tabindex:"-1"},Wl=De(()=>c("span",{class:"visually-hidden",id:"sidebar-aria-label"}," Sidebar Navigation ",-1)),Yl=g({__name:"VPSidebar",props:{open:{type:Boolean}},setup(s){const{sidebarGroups:e,hasSidebar:t}=F(),n=s,o=L(null),r=Ie(J?document.body:null);return R([n,o],()=>{var d;n.open?(r.value=!0,(d=o.value)==null||d.focus()):r.value=!1},{immediate:!0,flush:"post"}),(d,p)=>l(t)?(a(),i("aside",{key:0,class:M(["VPSidebar",{open:d.open}]),ref_key:"navEl",ref:o,onClick:p[0]||(p[0]=Je(()=>{},["stop"]))},[ql,c("nav",Kl,[Wl,u(d.$slots,"sidebar-nav-before",{},void 0,!0),(a(!0),i(S,null,x(l(e),v=>(a(),i("div",{key:v.text,class:"group"},[h(jl,{item:v,depth:0},null,8,["item"])]))),128)),u(d.$slots,"sidebar-nav-after",{},void 0,!0)])],2)):f("",!0)}});const Jl=m(Yl,[["__scopeId","data-v-b00e2fdd"]]),Xl=g({__name:"VPSkipLink",setup(s){const e=te(),t=L();R(()=>e.path,()=>t.value.focus());function n({target:o}){const r=document.getElementById(decodeURIComponent(o.hash).slice(1));if(r){const d=()=>{r.removeAttribute("tabindex"),r.removeEventListener("blur",d)};r.setAttribute("tabindex","-1"),r.addEventListener("blur",d),r.focus(),window.scrollTo(0,0)}}return(o,r)=>(a(),i(S,null,[c("span",{ref_key:"backToTop",ref:t,tabindex:"-1"},null,512),c("a",{href:"#VPContent",class:"VPSkipLink visually-hidden",onClick:n}," Skip to content ")],64))}});const Zl=m(Xl,[["__scopeId","data-v-0f60ec36"]]),Ql=g({__name:"Layout",setup(s){const{isOpen:e,open:t,close:n}=F(),o=te();R(()=>o.path,n),mt(e,n);const{frontmatter:r}=P(),d=Xe(),p=y(()=>!!d["home-hero-image"]);return Ne("hero-image-slot-exists",p),(v,b)=>{const N=j("Content");return l(r).layout!==!1?(a(),i("div",{key:0,class:M(["Layout",l(r).pageClass])},[u(v.$slots,"layout-top",{},void 0,!0),h(Zl),h(st,{class:"backdrop",show:l(e),onClick:l(n)},null,8,["show","onClick"]),l(r).navbar!==!1?(a(),$(Dl,{key:0},{"nav-bar-title-before":_(()=>[u(v.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":_(()=>[u(v.$slots,"nav-bar-title-after",{},void 0,!0)]),"nav-bar-content-before":_(()=>[u(v.$slots,"nav-bar-content-before",{},void 0,!0)]),"nav-bar-content-after":_(()=>[u(v.$slots,"nav-bar-content-after",{},void 0,!0)]),"nav-screen-content-before":_(()=>[u(v.$slots,"nav-screen-content-before",{},void 0,!0)]),"nav-screen-content-after":_(()=>[u(v.$slots,"nav-screen-content-after",{},void 0,!0)]),_:3})):f("",!0),h(Yn,{open:l(e),onOpenMenu:l(t)},null,8,["open","onOpenMenu"]),h(Jl,{open:l(e)},{"sidebar-nav-before":_(()=>[u(v.$slots,"sidebar-nav-before",{},void 0,!0)]),"sidebar-nav-after":_(()=>[u(v.$slots,"sidebar-nav-after",{},void 0,!0)]),_:3},8,["open"]),h(Sn,null,{"page-top":_(()=>[u(v.$slots,"page-top",{},void 0,!0)]),"page-bottom":_(()=>[u(v.$slots,"page-bottom",{},void 0,!0)]),"not-found":_(()=>[u(v.$slots,"not-found",{},void 0,!0)]),"home-hero-before":_(()=>[u(v.$slots,"home-hero-before",{},void 0,!0)]),"home-hero-info":_(()=>[u(v.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-image":_(()=>[u(v.$slots,"home-hero-image",{},void 0,!0)]),"home-hero-after":_(()=>[u(v.$slots,"home-hero-after",{},void 0,!0)]),"home-features-before":_(()=>[u(v.$slots,"home-features-before",{},void 0,!0)]),"home-features-after":_(()=>[u(v.$slots,"home-features-after",{},void 0,!0)]),"doc-footer-before":_(()=>[u(v.$slots,"doc-footer-before",{},void 0,!0)]),"doc-before":_(()=>[u(v.$slots,"doc-before",{},void 0,!0)]),"doc-after":_(()=>[u(v.$slots,"doc-after",{},void 0,!0)]),"doc-top":_(()=>[u(v.$slots,"doc-top",{},void 0,!0)]),"doc-bottom":_(()=>[u(v.$slots,"doc-bottom",{},void 0,!0)]),"aside-top":_(()=>[u(v.$slots,"aside-top",{},void 0,!0)]),"aside-bottom":_(()=>[u(v.$slots,"aside-bottom",{},void 0,!0)]),"aside-outline-before":_(()=>[u(v.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":_(()=>[u(v.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":_(()=>[u(v.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":_(()=>[u(v.$slots,"aside-ads-after",{},void 0,!0)]),_:3}),h(Tn),u(v.$slots,"layout-bottom",{},void 0,!0)],2)):(a(),$(N,{key:1}))}}});const er=m(Ql,[["__scopeId","data-v-1919c326"]]);const sr={Layout:er,enhanceApp:({app:s})=>{s.component("Badge",Qe)}};export{sr as t,P as u}; diff --git "a/assets/docker_Dockerfile\350\257\255\346\263\225.md.7c5d66d7.js" "b/assets/docker_Dockerfile\350\257\255\346\263\225.md.7c5d66d7.js" new file mode 100644 index 000000000..39a96766d --- /dev/null +++ "b/assets/docker_Dockerfile\350\257\255\346\263\225.md.7c5d66d7.js" @@ -0,0 +1,101 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const D=JSON.parse('{"title":"Dockerfile语法","description":"","frontmatter":{},"headers":[],"relativePath":"docker/Dockerfile语法.md","filePath":"docker/Dockerfile语法.md","lastUpdated":1708510102000}'),p={name:"docker/Dockerfile语法.md"},o=l(`

Dockerfile语法

Docker可以通过读取Dockerfile中的指令自动构建映像。Dockerfile是一个文本文档,其中包含用户在命令行上调用来组装镜像的所有命令。

https://docs.docker.com/engine/reference/builder/

语法

dockerfile
# 注释
+INSTRUCTION arguments
# 注释
+INSTRUCTION arguments

指令大小写不敏感,所以使用小写也不影响构建,但习惯上都将指令大写用于区分指令和参数。

Dockerfile必须以FROM指令开始(特殊情况:ARG指令)。

dockerfile
ARG  CODE_VERSION=latest
+FROM base:\${CODE_VERSION}
+CMD  /code/run-app
ARG  CODE_VERSION=latest
+FROM base:\${CODE_VERSION}
+CMD  /code/run-app

支持环境变量替换的指令

  • ADD
  • COPY
  • ENV
  • EXPOSE
  • FROM
  • LABEL
  • STOPSIGNAL
  • USER
  • VOLUME
  • WORKDIR
  • ONBUILD
dockerfile
FROM busybox
+ENV FOO=/bar
+WORKDIR \${FOO}   # WORKDIR /bar
+ADD . $FOO       # ADD . /bar
+COPY \\$FOO /quux # COPY $FOO /quux
FROM busybox
+ENV FOO=/bar
+WORKDIR \${FOO}   # WORKDIR /bar
+ADD . $FOO       # ADD . /bar
+COPY \\$FOO /quux # COPY $FOO /quux

基本模板

dockerfile
FROM image_name:version as alias1
+# FROM [--platform=<platform>] <image>[:<tag>] [AS <name>] 一个Dockerfile中FROM可以多次出现 用于构建多个镜像或者将一个构建阶段用作另一个构建阶段的依赖项
+MAINTAINER storyxc #维护文档的人,现在用LABEL xxx=xxx代替了
+
+RUN xxx 
+# RUN <command> shell格式,默认在linux上使用/bin/sh -c执行,windows上 cmd /S /C执行
+# RUN ['executable', 'param1', 'param2'] exec格式 
+
+# Deploy Biliup
+FROM python:3.9 as alias2
+
+ENV TZ=Asia/Shanghai
+# ENV指定环境变量
+
+EXPOSE 19159/tcp
+EXPOSE 19149/udp
+# 注明暴露的端口,只是声明作用,实际没有功能
+
+ADD hom* /mydir/
+# ADD指令用于向镜像内拷贝文件 目录 不仅能复制本机的文件,也能将远程URL的资源复制到镜像中
+# ADD [--chown=<user>:<group>] [--chmod=<perms>] [--checksum=<checksum>] <src>... <dest>
+# ADD [--chown=<user>:<group>] [--chmod=<perms>] ["<src>",... "<dest>"]
+# ADD可以识别压缩格式,把可解压的文件解压为目录,远程URL资源不会被解压
+
+VOLUME /opt
+# 指定创建一个具有指定名称的挂载点 可以使用JSON数组格式 或多个参数纯字符串
+
+COPY --from=alias1 /dir1 /dir2
+# COPY [--chown=<user>:<group>] [--chmod=<perms>] <src>... <dest>
+# COPY [--chown=<user>:<group>] [--chmod=<perms>] ["<src>",... "<dest>"]
+
+WORKDIR /opt
+# WORKDIR指定工作目录,如果没有则会被创建,如果WORKDIR后出现了相对路径,都是相对WORKDIR的
+
+CMD ["param1", "param2"]
+# CMD指令有三种格式
+# CMD ["executable","param1","param2"]  exec格式 最常用,不会有变量替换,需要变量替换需要使用shell格式或类似["shell", "-c", "e cho $HOME"]
+# CMD ["param1","param2"] 作为ENTRYPOINT的默认参数,如果是这个用法 那么ENTRYPOINT指令也要用JSON数组的格式书写
+# CMD command param1 param2 shell格式
+# 一个Dockerfile中只能有一个CMD指令,如果写了多个那么只有最后一个生效
+
+ENTRYPOINT ["biliup"]
+# ENTRYPOINT ["executable", "param1", "param2"]
+# ENTRYPOINT command param1 param2
FROM image_name:version as alias1
+# FROM [--platform=<platform>] <image>[:<tag>] [AS <name>] 一个Dockerfile中FROM可以多次出现 用于构建多个镜像或者将一个构建阶段用作另一个构建阶段的依赖项
+MAINTAINER storyxc #维护文档的人,现在用LABEL xxx=xxx代替了
+
+RUN xxx 
+# RUN <command> shell格式,默认在linux上使用/bin/sh -c执行,windows上 cmd /S /C执行
+# RUN ['executable', 'param1', 'param2'] exec格式 
+
+# Deploy Biliup
+FROM python:3.9 as alias2
+
+ENV TZ=Asia/Shanghai
+# ENV指定环境变量
+
+EXPOSE 19159/tcp
+EXPOSE 19149/udp
+# 注明暴露的端口,只是声明作用,实际没有功能
+
+ADD hom* /mydir/
+# ADD指令用于向镜像内拷贝文件 目录 不仅能复制本机的文件,也能将远程URL的资源复制到镜像中
+# ADD [--chown=<user>:<group>] [--chmod=<perms>] [--checksum=<checksum>] <src>... <dest>
+# ADD [--chown=<user>:<group>] [--chmod=<perms>] ["<src>",... "<dest>"]
+# ADD可以识别压缩格式,把可解压的文件解压为目录,远程URL资源不会被解压
+
+VOLUME /opt
+# 指定创建一个具有指定名称的挂载点 可以使用JSON数组格式 或多个参数纯字符串
+
+COPY --from=alias1 /dir1 /dir2
+# COPY [--chown=<user>:<group>] [--chmod=<perms>] <src>... <dest>
+# COPY [--chown=<user>:<group>] [--chmod=<perms>] ["<src>",... "<dest>"]
+
+WORKDIR /opt
+# WORKDIR指定工作目录,如果没有则会被创建,如果WORKDIR后出现了相对路径,都是相对WORKDIR的
+
+CMD ["param1", "param2"]
+# CMD指令有三种格式
+# CMD ["executable","param1","param2"]  exec格式 最常用,不会有变量替换,需要变量替换需要使用shell格式或类似["shell", "-c", "e cho $HOME"]
+# CMD ["param1","param2"] 作为ENTRYPOINT的默认参数,如果是这个用法 那么ENTRYPOINT指令也要用JSON数组的格式书写
+# CMD command param1 param2 shell格式
+# 一个Dockerfile中只能有一个CMD指令,如果写了多个那么只有最后一个生效
+
+ENTRYPOINT ["biliup"]
+# ENTRYPOINT ["executable", "param1", "param2"]
+# ENTRYPOINT command param1 param2

image-20230324223443650

`,14),e=[o];function t(c,r,i,y,u,E){return a(),n("div",null,e)}const g=s(p,[["render",t]]);export{D as __pageData,g as default}; diff --git "a/assets/docker_Dockerfile\350\257\255\346\263\225.md.7c5d66d7.lean.js" "b/assets/docker_Dockerfile\350\257\255\346\263\225.md.7c5d66d7.lean.js" new file mode 100644 index 000000000..54c98ccc9 --- /dev/null +++ "b/assets/docker_Dockerfile\350\257\255\346\263\225.md.7c5d66d7.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const D=JSON.parse('{"title":"Dockerfile语法","description":"","frontmatter":{},"headers":[],"relativePath":"docker/Dockerfile语法.md","filePath":"docker/Dockerfile语法.md","lastUpdated":1708510102000}'),p={name:"docker/Dockerfile语法.md"},o=l("",14),e=[o];function t(c,r,i,y,u,E){return a(),n("div",null,e)}const g=s(p,[["render",t]]);export{D as __pageData,g as default}; diff --git "a/assets/docker_docker-compose\350\257\255\346\263\225.md.5b63270f.js" "b/assets/docker_docker-compose\350\257\255\346\263\225.md.5b63270f.js" new file mode 100644 index 000000000..2548c3cf2 --- /dev/null +++ "b/assets/docker_docker-compose\350\257\255\346\263\225.md.5b63270f.js" @@ -0,0 +1,95 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const D=JSON.parse('{"title":"docker-compose语法","description":"","frontmatter":{},"headers":[],"relativePath":"docker/docker-compose语法.md","filePath":"docker/docker-compose语法.md","lastUpdated":1708510102000}'),p={name:"docker/docker-compose语法.md"},o=l(`

docker-compose语法

基础模板

https://docs.docker.com/compose/compose-file/03-compose-file/

yml
version: "3.8" # version是compose文件格式版本号 需要和Docker Engine对应 https://docs.docker.com/compose/compose-file/compose-file-v3/
+
+services:
+  service1:
+    image: image_name:version  #指定镜像
+    container_name: service1       #容器名
+    environment: #指定环境变量	
+      - A=1
+      - B=2
+    restart: always            #重启策略
+    volumes: #数据卷挂载
+      - /etc/localtime:/etc/localtime:ro # 挂载宿主机文件
+      - data:/opt/data # 具名卷挂载
+    ports: #端口映射配置
+      - "6610:6610"
+      - "6611:6611"
+    privileged: true # 将服务容器配置为以提升的权限运行
+    links: #定义到另一个服务中的容器的网络链接,可以在此容器直接用服务名访问另一个容器,links也有服务之间的隐式依赖关系,因此也决定了服务启动的顺序。
+      - service2
+    env_file:
+      - ./a.env
+      - ./b.env
+    devices:
+      - "/dev/ttyUSB0:/dev/ttyUSB0"
+      - "/dev/sda:/dev/xvda:rwm"
+    dns:
+      - 8.8.8.8
+  service2:
+    build: #构建配置
+      context: .               #指定包含Dockerfile的目录或一个git仓库的url
+      dockerfile: webapp.Dockerfile   #指定要使用的Dockerfile名称,默认找Dockerfile,和dockerfile_inline参数不能同时使用
+      dockerfile_inline: #直接在compose文件里写Dockerfile指令 和dockerfile参数不能同时使用
+        FROM xxx
+        RUN some command
+    container_name: service2
+    network_mode: "host"      #配置网络模式,none(禁用所有容器网络)/host(使用宿主接口)/service:{name}(只能访问指定服务)
+    networks: #指定容器连接的docker网络
+      - netA
+      - netB
+    depends_on: #依赖某个服务,决定了服务的启动和关闭顺序
+      - service3
+
+volumes:
+  data:
+    
+networks:
+  netA:
+  netB:
version: "3.8" # version是compose文件格式版本号 需要和Docker Engine对应 https://docs.docker.com/compose/compose-file/compose-file-v3/
+
+services:
+  service1:
+    image: image_name:version  #指定镜像
+    container_name: service1       #容器名
+    environment: #指定环境变量	
+      - A=1
+      - B=2
+    restart: always            #重启策略
+    volumes: #数据卷挂载
+      - /etc/localtime:/etc/localtime:ro # 挂载宿主机文件
+      - data:/opt/data # 具名卷挂载
+    ports: #端口映射配置
+      - "6610:6610"
+      - "6611:6611"
+    privileged: true # 将服务容器配置为以提升的权限运行
+    links: #定义到另一个服务中的容器的网络链接,可以在此容器直接用服务名访问另一个容器,links也有服务之间的隐式依赖关系,因此也决定了服务启动的顺序。
+      - service2
+    env_file:
+      - ./a.env
+      - ./b.env
+    devices:
+      - "/dev/ttyUSB0:/dev/ttyUSB0"
+      - "/dev/sda:/dev/xvda:rwm"
+    dns:
+      - 8.8.8.8
+  service2:
+    build: #构建配置
+      context: .               #指定包含Dockerfile的目录或一个git仓库的url
+      dockerfile: webapp.Dockerfile   #指定要使用的Dockerfile名称,默认找Dockerfile,和dockerfile_inline参数不能同时使用
+      dockerfile_inline: #直接在compose文件里写Dockerfile指令 和dockerfile参数不能同时使用
+        FROM xxx
+        RUN some command
+    container_name: service2
+    network_mode: "host"      #配置网络模式,none(禁用所有容器网络)/host(使用宿主接口)/service:{name}(只能访问指定服务)
+    networks: #指定容器连接的docker网络
+      - netA
+      - netB
+    depends_on: #依赖某个服务,决定了服务的启动和关闭顺序
+      - service3
+
+volumes:
+  data:
+    
+networks:
+  netA:
+  netB:
`,4),e=[o];function c(t,r,E,y,i,d){return n(),a("div",null,e)}const m=s(p,[["render",c]]);export{D as __pageData,m as default}; diff --git "a/assets/docker_docker-compose\350\257\255\346\263\225.md.5b63270f.lean.js" "b/assets/docker_docker-compose\350\257\255\346\263\225.md.5b63270f.lean.js" new file mode 100644 index 000000000..f9c633d1d --- /dev/null +++ "b/assets/docker_docker-compose\350\257\255\346\263\225.md.5b63270f.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const D=JSON.parse('{"title":"docker-compose语法","description":"","frontmatter":{},"headers":[],"relativePath":"docker/docker-compose语法.md","filePath":"docker/docker-compose语法.md","lastUpdated":1708510102000}'),p={name:"docker/docker-compose语法.md"},o=l("",4),e=[o];function c(t,r,E,y,i,d){return n(),a("div",null,e)}const m=s(p,[["render",c]]);export{D as __pageData,m as default}; diff --git "a/assets/docker_docker\347\275\221\347\273\234\344\271\213macvlan.md.15dfe39b.js" "b/assets/docker_docker\347\275\221\347\273\234\344\271\213macvlan.md.15dfe39b.js" new file mode 100644 index 000000000..f0f491d1a --- /dev/null +++ "b/assets/docker_docker\347\275\221\347\273\234\344\271\213macvlan.md.15dfe39b.js" @@ -0,0 +1,43 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const v=JSON.parse('{"title":"macvlan","description":"","frontmatter":{},"headers":[],"relativePath":"docker/docker网络之macvlan.md","filePath":"docker/docker网络之macvlan.md","lastUpdated":1708510102000}'),p={name:"docker/docker网络之macvlan.md"},o=l(`

macvlan

在日常使用docker的时候,可以通过设置端口映射的方式,实现通过宿主机ip访问docker容器的目的。当然,也有办法可以实现给docker镜像设置单独的IP。这里就要用到macvlan技术。

macvlan 是 Linux kernel 支持的新特性,macvlan能将一块物理网卡虚拟成多块虚拟网卡,docker这种容器就可以通过虚拟网卡获取IP,利用IP进行独立上网,真正做到跟物理机完全一样的体验。

docker网络中的macvlan

docker中的macvlan 是 Docker中的一种网络驱动,它允许容器直接使用物理网络接口的 MAC 地址。这使得容器可以像物理设备一样存在于网络中,而不需要进行端口映射或NAT。

linux中开启macvlan

shell
// 加载 macvlan 模块
+modprobe macvlan
+// 查看是否已加载
+lsmod | grep macvlan
+// 如果有有输出证明已经加载macvlan模块,否则说明不支持
+// macvlan   24576  0
// 加载 macvlan 模块
+modprobe macvlan
+// 查看是否已加载
+lsmod | grep macvlan
+// 如果有有输出证明已经加载macvlan模块,否则说明不支持
+// macvlan   24576  0

docker创建macvlan网络

shell
docker network create -d macvlan \\
+  --subnet=192.168.2.0/24 \\
+  --gateway=192.168.2.1 \\
+  -o parent=enp3s0 \\
+  vlan
docker network create -d macvlan \\
+  --subnet=192.168.2.0/24 \\
+  --gateway=192.168.2.1 \\
+  -o parent=enp3s0 \\
+  vlan
  • subnet:与物理网络相同的子网。
  • gateway:与物理网络相同的网关。
  • -o parent:宿主机上的物理网络接口,如 eth0。

创建容器并连接到macvlan

shell
docker run -d --name 容器名 --net vlan --ip=指定的IP地址 镜像名
docker run -d --name 容器名 --net vlan --ip=指定的IP地址 镜像名

例如

shell
docker run -it --name busybox --net vlan --ip=192.168.2.166 busybox /bin/sh
docker run -it --name busybox --net vlan --ip=192.168.2.166 busybox /bin/sh

docker-compose配置macvlan

yaml
version: "3"
+
+services:
+  busybox:
+    ...
+    networks:
+      vlan:
+        ipv4_address: 192.168.2.166
+
+
+networks:
+  vlan:
+    external: true
version: "3"
+
+services:
+  busybox:
+    ...
+    networks:
+      vlan:
+        ipv4_address: 192.168.2.166
+
+
+networks:
+  vlan:
+    external: true

容器启动后就可以通过指定的固定ip来访问docker容器中的服务了

`,17),e=[o];function c(t,r,E,y,i,d){return a(),n("div",null,e)}const C=s(p,[["render",c]]);export{v as __pageData,C as default}; diff --git "a/assets/docker_docker\347\275\221\347\273\234\344\271\213macvlan.md.15dfe39b.lean.js" "b/assets/docker_docker\347\275\221\347\273\234\344\271\213macvlan.md.15dfe39b.lean.js" new file mode 100644 index 000000000..9860b0a6a --- /dev/null +++ "b/assets/docker_docker\347\275\221\347\273\234\344\271\213macvlan.md.15dfe39b.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const v=JSON.parse('{"title":"macvlan","description":"","frontmatter":{},"headers":[],"relativePath":"docker/docker网络之macvlan.md","filePath":"docker/docker网络之macvlan.md","lastUpdated":1708510102000}'),p={name:"docker/docker网络之macvlan.md"},o=l("",17),e=[o];function c(t,r,E,y,i,d){return a(),n("div",null,e)}const C=s(p,[["render",c]]);export{v as __pageData,C as default}; diff --git a/assets/docker_index.md.f95bad4d.js b/assets/docker_index.md.f95bad4d.js new file mode 100644 index 000000000..9a0c8e9dd --- /dev/null +++ b/assets/docker_index.md.f95bad4d.js @@ -0,0 +1 @@ +import{_ as o,o as a,c as n,k as e,a as t}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"Docker","description":"","frontmatter":{},"headers":[],"relativePath":"docker/index.md","filePath":"docker/index.md","lastUpdated":1708510102000}'),r={name:"docker/index.md"},i=e("h1",{id:"docker",tabindex:"-1"},[t("Docker "),e("a",{class:"header-anchor",href:"#docker","aria-label":'Permalink to "Docker"'},"​")],-1),c=e("blockquote",null,[e("p",null,"Docker is an open platform for developing, shipping, and running applications. Docker enables you to separate your applications from your infrastructure so you can deliver software quickly. With Docker, you can manage your infrastructure in the same ways you manage your applications. By taking advantage of Docker’s methodologies for shipping, testing, and deploying code quickly, you can significantly reduce the delay between writing code and running it in production.")],-1),s=[i,c];function d(l,p,u,k,f,h){return a(),n("div",null,s)}const y=o(r,[["render",d]]);export{m as __pageData,y as default}; diff --git a/assets/docker_index.md.f95bad4d.lean.js b/assets/docker_index.md.f95bad4d.lean.js new file mode 100644 index 000000000..9a0c8e9dd --- /dev/null +++ b/assets/docker_index.md.f95bad4d.lean.js @@ -0,0 +1 @@ +import{_ as o,o as a,c as n,k as e,a as t}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"Docker","description":"","frontmatter":{},"headers":[],"relativePath":"docker/index.md","filePath":"docker/index.md","lastUpdated":1708510102000}'),r={name:"docker/index.md"},i=e("h1",{id:"docker",tabindex:"-1"},[t("Docker "),e("a",{class:"header-anchor",href:"#docker","aria-label":'Permalink to "Docker"'},"​")],-1),c=e("blockquote",null,[e("p",null,"Docker is an open platform for developing, shipping, and running applications. Docker enables you to separate your applications from your infrastructure so you can deliver software quickly. With Docker, you can manage your infrastructure in the same ways you manage your applications. By taking advantage of Docker’s methodologies for shipping, testing, and deploying code quickly, you can significantly reduce the delay between writing code and running it in production.")],-1),s=[i,c];function d(l,p,u,k,f,h){return a(),n("div",null,s)}const y=o(r,[["render",d]]);export{m as __pageData,y as default}; diff --git "a/assets/docker_\345\270\270\347\224\250\346\214\207\344\273\244.md.6b637e11.js" "b/assets/docker_\345\270\270\347\224\250\346\214\207\344\273\244.md.6b637e11.js" new file mode 100644 index 000000000..7be1d72d3 --- /dev/null +++ "b/assets/docker_\345\270\270\347\224\250\346\214\207\344\273\244.md.6b637e11.js" @@ -0,0 +1,11 @@ +import{_ as s,o as a,c as o,Q as l}from"./chunks/framework.dc1f4608.js";const F=JSON.parse('{"title":"常用指令","description":"","frontmatter":{},"headers":[],"relativePath":"docker/常用指令.md","filePath":"docker/常用指令.md","lastUpdated":1708510102000}'),e={name:"docker/常用指令.md"},n=l(`

常用指令

安装

shell
curl -fsSL https://get.docker.com | sh
curl -fsSL https://get.docker.com | sh

Dockerfile给ubuntu换源

dockerfile
RUN sed -i s@/archive.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list
RUN sed -i s@/archive.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list

macOS运行容器时同步宿主机时间

shell
-e TZ=\`ls -la /etc/localtime | cut -d/ -f8-9\`
-e TZ=\`ls -la /etc/localtime | cut -d/ -f8-9\`

MacOS中容器访问宿主机

可以用host.docker.internal来访问宿主机

https://docs.docker.com/desktop/networking/#use-cases-and-workarounds

构建跨平台镜像

docker buildx

shell
# 创建builder
+docker buildx create --name cross-platform-builder --driver docker-container --use
+# 执行构建
+docker buildx build --platform linux/amd64,linux/arm64 -t 镜像名:tag [-o type=registry | --push] . 
+# 查看推送到远程的镜像信息
+docker buildx imagetools inspect 镜像名:tag
# 创建builder
+docker buildx create --name cross-platform-builder --driver docker-container --use
+# 执行构建
+docker buildx build --platform linux/amd64,linux/arm64 -t 镜像名:tag [-o type=registry | --push] . 
+# 查看推送到远程的镜像信息
+docker buildx imagetools inspect 镜像名:tag

buildx 实例通过两种方式来执行构建任务,两种执行方式被称为使用不同的「驱动」:

  • docker 驱动:使用 Docker 服务程序中集成的 BuildKit 库执行构建。
  • docker-container 驱动:启动一个包含 BuildKit 的容器并在容器中执行构建。

docker 驱动无法使用一小部分 buildx 的特性(如在一次运行中同时构建多个平台镜像),此外在镜像的默认输出格式上也有所区别:docker 驱动默认将构建结果以 Docker 镜像格式直接输出到 docker 的镜像目录(通常是 /var/lib/overlay2),之后执行 docker images 命令可以列出所输出的镜像;而 docker container 则需要通过 --output 选项指定输出格式为镜像或其他格式。

docker buildx build 支持丰富的输出行为,通过--output=[PATH,-,type=TYPE[,KEY=VALUE] 选项可以指定构建结果的输出类型和路径等,常用的输出类型有以下几种:

  • local:构建结果将以文件系统格式写入 dest 指定的本地路径, 如 --output type=local,dest=./output
  • tar:构建结果将在打包后写入 dest 指定的本地路径。
  • oci:构建结果以 OCI 标准镜像格式写入 dest 指定的本地路径。
  • docker:构建结果以 Docker 标准镜像格式写入 dest 指定的本地路径或加载到 docker 的镜像库中。同时指定多个目标平台时无法使用该选项。
  • image:以镜像或者镜像列表输出,并支持 push=true 选项直接推送到远程仓库,同时指定多个目标平台时可使用该选项。
  • registry:type=image,push=true 的精简表示。

https://waynerv.com/posts/building-multi-architecture-images-with-docker-buildx/

`,19),p=[n];function c(t,r,d,i,y,E){return a(),o("div",null,p)}const h=s(e,[["render",c]]);export{F as __pageData,h as default}; diff --git "a/assets/docker_\345\270\270\347\224\250\346\214\207\344\273\244.md.6b637e11.lean.js" "b/assets/docker_\345\270\270\347\224\250\346\214\207\344\273\244.md.6b637e11.lean.js" new file mode 100644 index 000000000..a0c597590 --- /dev/null +++ "b/assets/docker_\345\270\270\347\224\250\346\214\207\344\273\244.md.6b637e11.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as o,Q as l}from"./chunks/framework.dc1f4608.js";const F=JSON.parse('{"title":"常用指令","description":"","frontmatter":{},"headers":[],"relativePath":"docker/常用指令.md","filePath":"docker/常用指令.md","lastUpdated":1708510102000}'),e={name:"docker/常用指令.md"},n=l("",19),p=[n];function c(t,r,d,i,y,E){return a(),o("div",null,p)}const h=s(e,[["render",c]]);export{F as __pageData,h as default}; diff --git a/assets/frontend_base_CSS.md.9a8416e3.js b/assets/frontend_base_CSS.md.9a8416e3.js new file mode 100644 index 000000000..a305438db --- /dev/null +++ b/assets/frontend_base_CSS.md.9a8416e3.js @@ -0,0 +1,391 @@ +import{_ as s,o as a,c as l,Q as n}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"CSS","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/base/CSS.md","filePath":"frontend/base/CSS.md","lastUpdated":1708510102000}'),p={name:"frontend/base/CSS.md"},o=n(`

CSS

CSS特性

  • 继承性
  • 层叠性
  • 优先级:继承 < 通配符选择器 < 标签选择器 < 类选择器 < id选择器 < 行内样式 < !important
    • !important写在属性值后面,分号前面
    • !important不能提升继承的优先级,只要是继承优先级最低
    • 复合选择器需要根据权重叠加计算(行内样式个数,id选择器个数,类选择器个数,标签选择器个数)

书写顺序:浏览器执行效率更高

  1. 浮动 / display
  2. 盒子模型 margin border padding 宽高背景色
  3. 文字样式

选择器

  • 标签选择器 tagName { css }

  • 类选择器: .class { css }

  • id选择器: #id { css }

  • 通配符选择器: * { css }

  • 复合选择器

    • 后代选择器: 选择器1 选择器2 { css }
      • 后代包含所有:子、孙子。。
    • 子代选择器: 选择器1 > 选择器2 { css }
      • 只包括子
  • 属性选择器: 选择器[attribute=xxx]

  • 并集选择器: 选择器1,选择器2 { css }

  • 交集选择器: 选择器1选择器2 { css }

    • 如果有标签选择器,标签选择器必须写在最前面
  • 伪类选择器: 标签:伪类选择器 { css }

    • 伪类

      • 状态(hover/active..)
      • 结构(fisrt-child/last-child/nth-child()/nth-last-child())
    • 伪对象

      • 页面中非主体内容可以使用伪对象,由css模拟出标签效果

      • ::before::after

      • css
        .content::before {
        +  content: 'test1';
        +}
        +
        +.content::after {
        +  content: 'test2';
        +}
        .content::before {
        +  content: 'test1';
        +}
        +
        +.content::after {
        +  content: 'test2';
        +}
      • 默认是行内元素,content必须添加 否则伪对象不生效

字体和文本样式

字体样式

  • 字体大小:font-size

    • 取值:数字+px,Google Chrome默认16px
  • 字体粗细:font-weight

    • 关键字:normal/bold/bolder/lighter
    • 数字:100-900的整百数,正常400 加粗700
  • 字体样式:font-style

    • 正常:normal
    • 倾斜:italic
  • 字体类型:font-family

    • 从左往右查找,如果未安装,则显示下一个,如果都不支持则显示最后字体系列的默认字体,多个字体推荐引号包裹,最后一个字体系列不需要引号
  • 字体类型:属性连写

    • style weight size family
    • 只能省略前两个,省略相当于设置默认值
    • 如果同时使用单独和连写,要么把单独的写在连写下面,或者写在连写里面

文本样式

  • 文本缩进:text-indent

    • 取值:数字+px或者数字+em,推荐使用em(1em = 当前标签font-size的大小)
  • 文本水平对齐方式(内容对齐方式):text-align

    • left/center/right
    • 如果需要让文本水平居中,text-align属性给文本所在的标签设置
    • 能让哪些元素水平居中:文本、span、a、input、img,需要给元素的父元素设置居中
  • 文本修饰:text-decoration

    • underline 下划线
    • line-through 删除线
    • overline 上划线
    • none 无装饰线
  • 行高:line-heigh

    • 行高:上间距+文本高度+下间距
    • 控制两行文字之间的距离
    • 取值:数字+px / 倍数 (当前标签font-size的倍数)
    • 应用:
      • 让单行文本垂直居中可以设置line-height:文字父元素高度
      • 网页精准布局会设置line-height:1;可以取消上下间距
    • 行高和font连写的注意点:
      • 如果同时设置了行高和font连写,需要注意覆盖问题
      • font: style weight size/line-height family;

背景属性

背景色

  • 属性:backgroud-color

  • 取值:

    • 关键字
    • rgb
    • rgba
    • 十六进制
  • 默认是透明:rgba(0,0,0,0)、transparent

背景图

  • 属性:backgroud-image
  • backgroud-image: url('图片链接')
  • url可以省略引号
  • 背景图默认水平和垂直方向平铺

背景平铺

  • 属性:background-repeat
  • 取值:repeat、no-repeat、repeat-x、repeat-y

背景位置

  • background-position
  • 取值
    • 方位:
      • 水平:left、center、right
      • 垂直:top、center、bottom
    • 数字+px
      • 坐标系原点(0,0),图片左上角
      • x轴 水平向右(图片向左移动取负数、向右移动取正数)
      • y轴 垂直向下(图片向上移动取负数、向下移动取正数)
  • 方位和坐标可以混用,第一个值为水平,第二个值为垂直

背景属性连写

  • background: color image repeat position

元素显示模式

元素显示模式

  • 块级元素
    • 显示特点:独占一行,宽度默认是父元素宽度,高度默认由内容撑开,可以设置宽、高
    • 代表标签:div、p、h、ul、li、dl、dt、dd、form、header、nav、footer。。。
  • 行内元素
    • 显示特点:一行可以显示多个,宽度和高度默认由内容撑开,不能设置宽、高
    • 代表标签:a、span、b、u、i、s、strong、ins、em、del。。。
  • 行内块元素
    • 显示特点:一行可以显示多个,可以设置宽、高
    • 代表标签:input、textarea、button、select
    • 特殊:img标签有行内块特点,但是Chrome显示是inline

元素显示模式转换

css
span {
+  display: inline-block;
+}
+
+div {
+  display: inline;
+}
+
+button {
+  display: block;
+}
span {
+  display: inline-block;
+}
+
+div {
+  display: inline;
+}
+
+button {
+  display: block;
+}

盒子模型

页面中的每一个标签都可以看作一个“盒子”,通过盒子的视角更方便的进行布局。

每个盒子分别由:内容区域(content)、内边距区域(padding)、边框区域(border)、外边距区域(margin)构成。


  • 内容区域宽高:weight/height
    • 取值 数字+px
  • 边框:border
    • border:10px solid red;
    • border-top/bottom/left/right
  • 内边距:padding
    • 四方向顺序:上 右 下 左 (从上顺时针取值)

border和padding会撑大盒子,css属性box-sizing: border-box; 可以设置内减模式

  • 外边距:margin

    • 顺序和padding一样

    • 清除默认的内外边距

      • css
        * {
        +  margin: 0;
        +  padding: 0;
        +}
        * {
        +  margin: 0;
        +  padding: 0;
        +}
    • 版心居中

      • css
        #main {
        +  margin: 0 auto;
        +}
        #main {
        +  margin: 0 auto;
        +}

顺序: 宽高背景色-> 放内容 -> 调整位置 -> 调整文字细节

  • 外边距问题

  • 折叠现象:垂直布局的块级元素,上下的margin会被合并,取两者的最大值

  • 塌陷现象:互相嵌套的块级元素,子元素的margin-top会作用在父元素上

    • 解决:1.父元素设置border-top或者padding-top(分隔父子元素的margin-top) 2.父元素设置overflow: hidden;3.转换为行内块元素4.设置浮动
  • 行内元素的内外边距问题:如果想通过margin/padding改变行内元素的垂直(top/bottom)位置,无法生效

    • 解决:设置行高

浮动

标准流

  • 标准流:又称文档流,浏览器在渲染显示网页内容时默认采取的一套排版规则,规定了何种方式排列元素
    • 块级元素:从上往下,垂直布局,独占一行
    • 行内元素/行内块元素:从左往右,水平布局,空间不够自动折行

浮动作用

  • 早期:图文环绕
  • 现在:网页布局

块级元素转行内块时 换行会产生空格

浮动的规则

  • 向左浮动或者向右浮动直到自己的边界紧贴着包含块(一般是父元素)或者其他浮动元素的边界为止

  • 不能超出包含块,如果元素是向左(右)浮动,浮动元素的左(右)边界不能超出包含块的左(右)边界

  • 浮动元素不能层叠

    • 如果一个元素浮动,另一个浮动元素已经在那个位置了,后浮动的元素将紧贴着前一个浮动元素(左浮找左浮,右浮找右浮)
    • 如果水平方向剩余的空间不够显示浮动元素,浮动元素将向下移动,直到有充足的空间为止
  • 浮动元素会将行内级元素内容推出

    • 浮动元素不能与行内级内容层叠,行内级内容将会被浮动元素推出

    • 比如行内级元素inline-block元素、块级元素的文字内容

    • 图文环绕效果

  • 浮动只能左右浮动, 不能超出本行的高度

    • 行内级元素、inline-block元素浮动后,其顶部将与所在行的顶部对齐

浮动特点

  • 浮动元素会脱离标准流,在标准流中不占位置
    • 相当于从地面飘到了空中
  • 浮动元素比标准流高半个级别,可以覆盖标准流中的元素
  • 浮动找浮动,下一个浮动元素会在上一个浮动元素后面左右浮动
  • 浮动元素有特殊的显示效果
    • 一行可以显示多个
    • 可以设置宽高

浮动的元素不能通过text-align: center 或者margin: 0 auto居中

清除浮动

  • 含义:清除浮动带来的影响。如果子元素浮动,此时子元素不能撑开标准流的块级父元素

  • 原因:子元素浮动后脱离标准流 -> 不占位置

  • 目的:需要父元素有高度,不影响其他元素的布局

  • 方法:

    • 给父元素加高度

    • 额外标签:给父元素内容的最后加一个块级元素,给添加的块级元素设置clear: both;

    • 单伪元素:用伪元素代替额外标签

      css
      .clearfix::after {
      +  content: '';
      +  display: block;
      +  clear: both;
      +}
      +---
      +.clearfix::after {
      +  content: '';
      +  displat: block;
      +  clear: both;
      +  height: 0;
      +  visibility: hidden;
      +}
      .clearfix::after {
      +  content: '';
      +  display: block;
      +  clear: both;
      +}
      +---
      +.clearfix::after {
      +  content: '';
      +  displat: block;
      +  clear: both;
      +  height: 0;
      +  visibility: hidden;
      +}
    • 双伪元素:

      css
      .clearfix::before, /* 解决外边距塌陷问题 */ 
      +.clearfix::after {
      +  content: '';
      +  display: table;
      +}
      +.clearfix::after {
      +  clear: both;
      +}
      .clearfix::before, /* 解决外边距塌陷问题 */ 
      +.clearfix::after {
      +  content: '';
      +  display: table;
      +}
      +.clearfix::after {
      +  clear: both;
      +}
    • 父元素设置overflow: hidden

      BFC(Block Formatting Context)全称是块级格式化上下文,用于对块级元素排版,默认情况下只有根元素(body)一个块级上下文,但是如果一个块级元素设置了float:left,overflow:hidden或position:absolute样式,就会为这个块级元素生产一个独立的块级上下文,使这个块级元素内部的排版完全独立。

      作用:独立的块级上下文可以包裹浮动流,全部浮动子元素也不会引起容器高度塌陷,就是说包含块会把浮动元素的高度也计算在内,所以就不用清除浮动来撑起包含块的高度。

      那什么时候会触发 BFC 呢?常见的情况如下:

      • 根元素;

      • float的值不为none;

      • overflow的值为auto、scroll或hidden;

      • display的值为table-cell、table-caption和inline-block中的任何一个;

      • position的值不为relative和static。

定位

作用

  1. 可以让元素自由的摆放在网页的任意位置
  2. 一般用于盒子之间的层叠情况

常见应用场景

  • 可以解决盒子与盒子之间层叠问题
    • 定位后的元素层级最高,可以层叠在其他盒子上面
  • 可以让盒子始终固定带屏幕中的某个位置

使用

  • 设置定位方式

    • 属性名:position

    • 常见属性:

      • 静态定位:static

        • 元素处于标准流中,默认设置
      • 相对定位:relative,相对自己之前的位置进行移动

        • 需要配合方位属性实现移动
        • 相对自己原来的位置移动
        • 在页面中占位置(没有脱标)
        • 应用场景:1.配合绝对定位(子绝对 父相对) 2.用于小范围移动
      • 绝对定位:absolute,先找已经定位的父级(逐级查找),如果有这样的父级就以这个父级为参照定位,有父级但父级没有定位则以浏览器为参照定位

        • 页面中不占位置(脱标)

        • 改变标签的显示模式特点,具备行内块特点

        • 绝对定位的元素不能使用margin: 0 auto居中

          • css
            position: absolute;
            +left: 50%;
            +top: 50%;
            +transform: translate(-50%, -50%);
            position: absolute;
            +left: 50%;
            +top: 50%;
            +transform: translate(-50%, -50%);
      • 固定定位:fixed,相对于浏览器进行定位移动

        • 需要配合方位属性实现移动
        • 相对浏览器可视区域进行移动
        • 在页面中不占位置(脱标)具备行内块特点
        • 应用场景:让盒子固定在屏幕中某个位置
  • 设置偏移值

    • 偏移值分为两个方向,水平和垂直方向各选一个使用即可,水平以left为准,垂直以top为准
    • 选取原则一般是就近原则

元素层级问题

不同布局方式元素的层级关系

  • 标准流 < 浮动 < 定位

不同定位之间的层级关系

  • 相对、绝对、固定默认层级相同
  • 此时默认HTML中写在下面的元素层级更高,会覆盖上面的元素
  • z-index(配合定位才生效):整数,取值越大,层级越高,显示顺序越靠上,z-index默认为0

定位装饰

垂直对齐方式

  • 属性:vertical-align
  • 取值:baseline(默认,基线对齐)、top(顶部对齐)、middle(中部对齐)、bottom(底部对齐)

浏览器遇到行内和行内块标签当作文字处理,默认文字按照基线对齐

光标类型

  • 属性:cursor
  • 常见取值:
    • default(默认,通常是箭头)
    • pointer(小手,提示可以点击)
    • text(工字型,提示可以选择)
    • move(十字光标,提示可以移动)

边框圆角

  • 属性:border-radius

  • 常见取值:数字+px、百分比

  • 赋值:从左上角开始,顺时针赋值,没有赋值的看对角

正圆:正方形盒子,border-radius: 50%

胶囊按钮:长方形盒子,border-radius: 高度的一半

百分比表示的是圆角半径的大小为盒子较小边长的一半,例如盒子宽200px,高100px,border-radius: 50% 50%; 则这两个50%都是相对于100px的。

溢出部分显示效果

  • 属性:overflow
  • 常见取值:
    • visible 默认值,可见
    • hidden 隐藏
    • scroll 始终显示滚动条
    • auto 自动显示、隐藏滚动条

元素本身隐藏

  • 属性:
    • visibility: hidden(占位隐藏)
    • display: none(不占位隐藏)

元素整体透明度

  • 属性:opacity
  • 取值:0-1之间数字,1完全不透明,0完全透明
  • 特点:opacity会让元素整体透明,包括里面的文字、子元素等

补充

精灵图

CSS精灵图是一种将多个小的背景图片合并到一张大图中的技术,通过CSS的background-position属性控制显示不同的小图片,达到减少HTTP请求、减小页面大小、提高加载速度的效果。

CSS精灵图可以将多个小背景图像合并为一张大图,这样在页面上加载一次大图后,就可以通过background-position属性来控制显示不同的小图像,实现了将多个HTTP请求转化为一次请求,减小了网络延迟和服务器压力。同时,由于减少了HTTP请求和加载的内容大小,减小了页面的带宽消耗,加快了网页的加载速度,提高了用户的体验感受。

在web前端开发中,经常使用CSS精灵图技术,特别是在一些需要大量小icon的场合,如网站菜单栏、按钮、分页等等。

背景图片大小

  • 设置背景图片的大小:background-size: 宽 高

  • 取值

    • 数字+px
    • 百分比
    • contain:等比例缩放,直到不会超出盒子的最大
    • cover:等比例缩放,直到刚好填满整个盒子

background连写

backgound:color image repeat position/size

盒子阴影

  • 属性:box-shadow
  • 取值
    • h-shadow:必须,水平偏移量,允许负值
    • v-shadow:必须,垂直偏移量,允许负值
    • blur:可选,模糊度
    • spread:可选,阴影扩大
    • color:可选,阴影颜色
    • inset:可选,改为内部阴影

过渡

  • 属性:transition
  • 作用:让元素样式慢慢变化,常配合hover使用
  • 常见取值
    • 过渡属性:all(所有能过渡的属性都过渡)、具体属性名如width(只有width过渡)
    • 过渡时长: 数字+s
  • 注意点
    • 默认状态和hover状态样式不同才有过渡效果
    • transition属性需要给过渡的元素本身加
    • transition属性设置在不同状态中,效果不同
      • 给默认状态设置,鼠标移入移出都有效果
      • 给hover状态设置,只有移入有过渡效果

.box {

​ width: 200px;

​ height: 200px;

​ background-color: pink;

​ /transition: all 1s/

​ transition: width 1s, background-color 2s;

}

.box hover {

​ width: 600px;

​ background-color: red;

}

字体图标

  • 展示的是图标,实质是文字,用作处理简单的、颜色单一的图片

  • iconfont

  • <link rel="stylesheet" href="./iconfont.css"

  • <span class="iconfont icon-kuaijiezhifu"></span>

平面转换

平面转换(2D转换):改变盒子在平面内的形态,可以使用transform属性实现元素的位移、旋转、缩放等效果

位移

  • transform: translate(水平移动距离, 垂直移动距离)
  • 取值: 1.像素单位数值 2.百分比(参照为自身盒子尺寸)
  • 只给一个值代表x轴位移,单独设置:tranlateX() / translateY()

旋转

  • 语法:transform: rotate(度数 + deg); 正数:顺时针;负数:逆时针
  • transform-origin
    • 作用:改变转换的原点,默认的原点是盒子中心点
    • transform-origin: 原点水平位置 原点垂直位置
    • 取值:方位名词(left、top、right、bottom、center
    • 像素
    • 百分比

多重转换

transform: translate() rotate()

rotate会改变坐标轴向,位移方向会受影响

多重转换如果涉及旋转,旋转往最后写

缩放

  • scale:改变元素尺寸
  • 语法:transform: scale(x轴缩放倍数, y轴缩放倍数)
  • 一般transform: scale(缩放倍数),等比缩放
  • scale大于1表示放大,小于1表示缩小

渐变

linear-gradient

空间转换

在空间内位移、旋转、缩放等效果

空间位移

transform: tranlate3d(x,y,z)

单个坐标轴transform: translat[X|Y|Z]

透视

使用perspective(视距)属性实现透视效果,添加给父级元素,取值一般800-1200

空间旋转

transform: rotateX()

transform: rotateY()

transform: rotateZ()

transform: rotate3d(x,y,z,角度度数):用来设置自定义旋转轴的位置及旋转角度,xyz取值为0-1之间的数字

立体呈现

使用transform-style: perserve-3d呈现立体图形

  • 父元素添加transform-style: perserve-3d使子集元素处于3d空间

  • 默认值flat,表示子元素处于2D平面

空间缩放

transform: scaleX()

transform: scaleY()

transform: scaleY()

transform: scale3d(x,y,z)

动画

使用animation实现多个状态间的变化过程,动画过程可控(重复播放、最终画面、是否暂停)

实现步骤

  1. 定义动画

    css
    @keyframes 动画名称 {
    +  from {}
    +  to {}
    +}
    +
    +@keyframes 动画名称 {
    +  /* 百分比指的是动画总时长的占比 */
    +  0% {}
    +  10% {}
    +  15% {}
    +  100% {}
    +}
    @keyframes 动画名称 {
    +  from {}
    +  to {}
    +}
    +
    +@keyframes 动画名称 {
    +  /* 百分比指的是动画总时长的占比 */
    +  0% {}
    +  10% {}
    +  15% {}
    +  100% {}
    +}
  2. 使用动画: animation: 动画名称 动画花费时长

动画属性

animation: 动画名称 动画时长 速度曲线 延迟时间 重复次数 动画方向 执行完毕时状态 播放状态

  • 动画名称和动画时长必须赋值
  • 取值不分先后顺序
  • 如果有2个时间,第一个表示动画时长,第二个表示延迟时间
  • animation-name:
  • animation-duration
  • animation-timing-function:ease;ease-in;ease-out;ease-in-out;linear;step;
  • animation-delay
  • animation-iteration-count: 1,2,..infinite;
  • animation-direction: normal;reverse;alternate;alternate-reverse;
  • animation-fill-mode: forward;backward;both;
  • animation-play-state: pause;running;
  • animation-

多组动画

animation: 动画1,动画2...,动画n;

flex布局

  • 是一种浏览器提倡的布局模型
  • 布局网页更简单、灵活
  • 避免了浮动脱标的问题

组成部分

  • 弹性容器
  • 弹性项
  • 主轴
  • 侧轴/交叉轴

语法

css
display: flex;
display: flex;

主轴对齐方式

主轴整体对齐

  • justify-content

  • 取值:

    • left/right
    • start/end
    • flex-start
    • flex-end
    • center
    • space-between
    • space-around
    • space-evenly
    • first/last baseline

主轴单行对齐方式

  • justify-items:相当于给给每个项设置默认的justify-self值

主轴单个对齐方式

  • justify-self

侧轴对齐方式

侧轴多行整体对齐

  • 属性:align-content

侧轴单行对齐方式

  • 属性:align-items,相当于给每个项设置默认的align-self值
  • 取值:
    • center
    • stretch: 拉伸,默认值
    • start/end
    • flex-start/flex-end
    • self-[start/end]
    • first/last baseline

侧轴单个对齐方式

  • 属性:align-self

伸缩比

  • 属性:flex
  • 说明: flex是flex-grow(增长系数)、flex-shrink(收缩系数)和flex-basis(初始尺寸)三个属性的简写,第一个无单位数代表flex-grow、第二无单位数代表flex-shrink,带像素单位的是flex-basis的值

修改主轴方向

  • 属性:flex-direction
  • 取值:
    • row
    • column
    • row-reverse
    • column-reverse

弹性项换行

  • 属性:flex-wrap
  • 取值:
    • nowrap
    • wrap
    • wrap-reverse

flex-flow

  • flex-flow:flex-direction flex-wrap;

移动适配

rem

  • 相对单位
  • rem单位是相对HTML标签的字号计算结果
  • 1rem=1HTML字号大小
  • 将网页等分成10份,HTML标签的字号为视口宽度的1/10
  • px单位数值/基准根字号

媒体查询

css
@media 逻辑操作符 媒体类型 and (媒体特性) {
+  选择器 {
+    CSS属性
+  }
+}
+
+@media (媒体特性) {
+  选择器 {
+    CSS属性
+  }
+}
+
+@media (min-width:320px) {
+  html {
+    font-size: 32px;
+  }
+}
@media 逻辑操作符 媒体类型 and (媒体特性) {
+  选择器 {
+    CSS属性
+  }
+}
+
+@media (媒体特性) {
+  选择器 {
+    CSS属性
+  }
+}
+
+@media (min-width:320px) {
+  html {
+    font-size: 32px;
+  }
+}

关键词

  • and
  • only
  • not
  • or

媒体类型

  • screen(带屏幕的设备)
  • print(打印预览模式)
  • speech(屏幕阅读模式)
  • all(默认值)

媒体特性

  • 视口宽高:width、height、max-width、max-height、min-width、min-height
  • 屏幕方向:orientation,portrait竖屏,landscape横屏

vw/vh

  • vw = 1/100视口宽度
  • vh = 1/100视口高度

Less

Less(Leaner Style Sheets)是一门向后兼容的CSS 扩展语言,是一个CSS预处理器,扩充了CSS,使CSS具备一定的逻辑性、计算能力。

https://less.bootcss.com

变量

less
@width: 10px;
+@height: @width + 10px;
+
+#header {
+  width: @width;
+  height: @height;
+}
@width: 10px;
+@height: @width + 10px;
+
+#header {
+  width: @width;
+  height: @height;
+}

编译为

css
#header {
+  width: 10px;
+  height: 20px;
+}
#header {
+  width: 10px;
+  height: 20px;
+}

混合

less
.bordered {
+  border-top: dotted 1px black;
+  border-bottom: solid 2px black;
+}
+
+#menu a {
+  color: #111;
+  .bordered();
+}
+
+.post a {
+  color: red;
+  .bordered();
+}
.bordered {
+  border-top: dotted 1px black;
+  border-bottom: solid 2px black;
+}
+
+#menu a {
+  color: #111;
+  .bordered();
+}
+
+.post a {
+  color: red;
+  .bordered();
+}

.bordered 类所包含的属性就将同时出现在 #menu a.post a 中了。

嵌套

less
#header {
+  color: black;
+  .navigation {
+    font-size: 12px;
+  }
+  .logo {
+    width: 300px;
+  }
+}
#header {
+  color: black;
+  .navigation {
+    font-size: 12px;
+  }
+  .logo {
+    width: 300px;
+  }
+}
css
#header {
+  color: black;
+}
+#header .navigation {
+  font-size: 12px;
+}
+#header .logo {
+  width: 300px;
+}
#header {
+  color: black;
+}
+#header .navigation {
+  font-size: 12px;
+}
+#header .logo {
+  width: 300px;
+}

规则嵌套和冒泡

less
.component {
+  width: 300px;
+  @media (min-width: 768px) {
+    width: 600px;
+    @media  (min-resolution: 192dpi) {
+      background-image: url(/img/retina2x.png);
+    }
+  }
+  @media (min-width: 1280px) {
+    width: 800px;
+  }
+}
.component {
+  width: 300px;
+  @media (min-width: 768px) {
+    width: 600px;
+    @media  (min-resolution: 192dpi) {
+      background-image: url(/img/retina2x.png);
+    }
+  }
+  @media (min-width: 1280px) {
+    width: 800px;
+  }
+}

编译为

css
.component {
+  width: 300px;
+}
+@media (min-width: 768px) {
+  .component {
+    width: 600px;
+  }
+}
+@media (min-width: 768px) and (min-resolution: 192dpi) {
+  .component {
+    background-image: url(/img/retina2x.png);
+  }
+}
+@media (min-width: 1280px) {
+  .component {
+    width: 800px;
+  }
+}
.component {
+  width: 300px;
+}
+@media (min-width: 768px) {
+  .component {
+    width: 600px;
+  }
+}
+@media (min-width: 768px) and (min-resolution: 192dpi) {
+  .component {
+    background-image: url(/img/retina2x.png);
+  }
+}
+@media (min-width: 1280px) {
+  .component {
+    width: 800px;
+  }
+}

运算

算术运算符 +-*/ 可以对任何数字、颜色或变量进行运算。如果可能的话,算术运算符在加、减或比较之前会进行单位换算。计算的结果以最左侧操作数的单位类型为准。如果单位换算无效或失去意义,则忽略单位。无效的单位换算例如:px 到 cm 或 rad 到 % 的转换。

less
// 所有操作数被转换成相同的单位
+@conversion-1: 5cm + 10mm; // 结果是 6cm
+@conversion-2: 2 - 3cm - 5mm; // 结果是 -1.5cm
+
+// conversion is impossible
+@incompatible-units: 2 + 5px - 3cm; // 结果是 4px
+
+// example with variables
+@base: 5%;
+@filler: @base * 2; // 结果是 10%
+@other: @base + @filler; // 结果是 15%
// 所有操作数被转换成相同的单位
+@conversion-1: 5cm + 10mm; // 结果是 6cm
+@conversion-2: 2 - 3cm - 5mm; // 结果是 -1.5cm
+
+// conversion is impossible
+@incompatible-units: 2 + 5px - 3cm; // 结果是 4px
+
+// example with variables
+@base: 5%;
+@filler: @base * 2; // 结果是 10%
+@other: @base + @filler; // 结果是 15%

乘法和除法不作转换。因为这两种运算在大多数情况下都没有意义,一个长度乘以一个长度就得到一个区域,而 CSS 是不支持指定区域的。Less 将按数字的原样进行操作,并将为计算结果指定明确的单位类型。

less
@base: 2cm * 3mm; // 结果是 6cm
@base: 2cm * 3mm; // 结果是 6cm

转义

转义(Escaping)允许你使用任意字符串作为属性或变量值。任何 ~"anything"~'anything' 形式的内容都将按原样输出,除非 interpolation

less
@min768: ~"(min-width: 768px)";
+.element {
+  @media @min768 {
+    font-size: 1.2rem;
+  }
+}
@min768: ~"(min-width: 768px)";
+.element {
+  @media @min768 {
+    font-size: 1.2rem;
+  }
+}

编译为:

less
@media (min-width: 768px) {
+  .element {
+    font-size: 1.2rem;
+  }
+}
@media (min-width: 768px) {
+  .element {
+    font-size: 1.2rem;
+  }
+}

注意,从 Less 3.5 开始,可以简写为:

less
@min768: (min-width: 768px);
+.element {
+  @media @min768 {
+    font-size: 1.2rem;
+  }
+}
@min768: (min-width: 768px);
+.element {
+  @media @min768 {
+    font-size: 1.2rem;
+  }
+}

函数

Less 内置了多种函数用于转换颜色、处理字符串、算术运算等。这些函数在Less 函数手册中有详细介绍。

函数的用法非常简单。下面这个例子利用 percentage 函数将 0.5 转换为 50%,将颜色饱和度增加 5%,以及颜色亮度降低 25% 并且色相值增加 8 等用法:

less
@base: #f04615;
+@width: 0.5;
+
+.class {
+  width: percentage(@width); // returns \`50%\`
+  color: saturate(@base, 5%);
+  background-color: spin(lighten(@base, 25%), 8);
+}
@base: #f04615;
+@width: 0.5;
+
+.class {
+  width: percentage(@width); // returns \`50%\`
+  color: saturate(@base, 5%);
+  background-color: spin(lighten(@base, 25%), 8);
+}

映射

从 Less 3.5 版本开始,你还可以将混合(mixins)和规则集(rulesets)作为一组值的映射(map)使用。

less
#colors() {
+  primary: blue;
+  secondary: green;
+}
+
+.button {
+  color: #colors[primary];
+  border: 1px solid #colors[secondary];
+}
#colors() {
+  primary: blue;
+  secondary: green;
+}
+
+.button {
+  color: #colors[primary];
+  border: 1px solid #colors[secondary];
+}

输出符合预期:

css
.button {
+  color: blue;
+  border: 1px solid green;
+}
.button {
+  color: blue;
+  border: 1px solid green;
+}

作用域

Less 中的作用域与 CSS 中的作用域非常类似。首先在本地查找变量和混合(mixins),如果找不到,则从“父”级作用域继承。

less
@var: red;
+
+#page {
+  @var: white;
+  #header {
+    color: @var; // white
+  }
+}
@var: red;
+
+#page {
+  @var: white;
+  #header {
+    color: @var; // white
+  }
+}

与 CSS 自定义属性一样,混合(mixin)和变量的定义不必在引用之前事先定义。因此,下面的 Less 代码示例和上面的代码示例是相同的:

less
@var: red;
+
+#page {
+  #header {
+    color: @var; // white
+  }
+  @var: white;
+}
@var: red;
+
+#page {
+  #header {
+    color: @var; // white
+  }
+  @var: white;
+}

导入

css
@import "library"; // library.less
+@import "typo.css";
@import "library"; // library.less
+@import "typo.css";

被引入的less文件不会生成单独的css文件

控制Less编译输出

webstorm FileWatcher配置:

  • npm install -g less
  • aruments:--no-color $FileName$ ../css/$FileNameWithoutExtension$.css
  • output paths to refresh : ../css/$FileNameWithoutExtension$.css

VS Code EasyLess配置:

  • out: "../css/"
  • 首行添加注释控制编译的输出情况
    • // out: ./dir/
    • // out: ./dir/xxx.css
    • // out: false

BootStrap

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">

<script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>

栅格系统

  • 栅格化是指将整个网页的宽度分成若干等份,BootStrap3默认将网页分为12等份
超小屏幕小屏幕中等屏幕大屏幕
响应断点<768px>=768px>=992px>=1200px
别名xssmmdlg
容器宽度100%750px970px1170px
类前缀col-xs-*col-sm-*col-md-*col-lg-*
列数12121212
列间隙30px30px30px30px
  • .container是BootStrap中提供的类名,应用该类名的盒子,默认被指定宽度且居中
  • .container-fluid是BootStrap中提供的类名,所有应用的盒子,宽度为100%
  • 分别用.row类名和.col类名定义栅格布局的行和列

TIP

  1. container类自带间距15px
  2. row类自带间距-15px

bootstrap样式

https://v3.bootcss.com/css/

bootstrap组件

https://v3.bootcss.com/components/

Record

滚动条滑动效果

css
html {
+  scroll-behavior: smooth;
+}
html {
+  scroll-behavior: smooth;
+}
`,240),e=[o];function t(c,r,i,E,y,d){return a(),l("div",null,e)}const F=s(p,[["render",t]]);export{u as __pageData,F as default}; diff --git a/assets/frontend_base_CSS.md.9a8416e3.lean.js b/assets/frontend_base_CSS.md.9a8416e3.lean.js new file mode 100644 index 000000000..94da339ae --- /dev/null +++ b/assets/frontend_base_CSS.md.9a8416e3.lean.js @@ -0,0 +1 @@ +import{_ as s,o as a,c as l,Q as n}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"CSS","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/base/CSS.md","filePath":"frontend/base/CSS.md","lastUpdated":1708510102000}'),p={name:"frontend/base/CSS.md"},o=n("",240),e=[o];function t(c,r,i,E,y,d){return a(),l("div",null,e)}const F=s(p,[["render",t]]);export{u as __pageData,F as default}; diff --git a/assets/frontend_base_HTML.md.bd02116d.js b/assets/frontend_base_HTML.md.bd02116d.js new file mode 100644 index 000000000..942402007 --- /dev/null +++ b/assets/frontend_base_HTML.md.bd02116d.js @@ -0,0 +1 @@ +import{_ as a,o as e,c as t,Q as o}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"HTML","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/base/HTML.md","filePath":"frontend/base/HTML.md","lastUpdated":1708510102000}'),i={name:"frontend/base/HTML.md"},l=o('

HTML

标签

排版标签

标题:h1-h6

段落:p

换行:br

水平分割线:hr

文本格式化标签

加粗:strong/b

下划线:ins/u

倾斜:em/i

删除线:del/s

媒体标签

图片:img

音频:audio [src|controls|autoplay|loop]

视频:video [src|controls|autoplay|loop]

链接

超链接:a

列表

有序列表:ol > li

无序列表:ul > li

自定义列表:dl > dt > dd

表格

table > tr > td

th:表头

caption:标题

结构标签:thead/tbody/tfoot

合并单元格:rowspan-跨行合并 colspan-跨列合并(不能跨结构标签合并)

表单

input系列:text/password/radio/checkbox/file/submit/reset/button

button

select

textarea

label

语义化标签

没有语义的布局标签:div/span

语义化标签:header/nav/footer/aside/section/article(显示特点和div一致,多了语义)

字符实体

空格:&nbsp

骨架结构标签

  • <!DOCTYPE html>:声明网页HTML版本
  • <html lang="en":标识网页使用的语言,作用:搜索引擎归类+浏览器翻译, zh-CN/en
  • <meta>:元数据标签,元数据是关于文档的数据,例如文档的作者、字符集、关键字和描述等信息,这些信息对于搜索引擎的抓取和用户的浏览很有帮助。
    • charset:指定文档的字符编码。
    • name:定义元数据的名称。
    • content:定义元数据的内容。
    • http-equiv:提供有关如何处理文档的其他信息。
    • viewport:指定当前网页的视口(viewport)尺寸和缩放比例,以便浏览器正确渲染页面。

SEO(Search Engine Optimization)标签

  • <title>:网页标题
  • <meta name="description">:网页描述标签
  • <meta name="keywords":网页关键词

ico图标

<link rel="icon" href="favicon.ico">

',45),r=[l];function n(d,h,c,p,s,u){return e(),t("div",null,r)}const q=a(i,[["render",n]]);export{m as __pageData,q as default}; diff --git a/assets/frontend_base_HTML.md.bd02116d.lean.js b/assets/frontend_base_HTML.md.bd02116d.lean.js new file mode 100644 index 000000000..74a073538 --- /dev/null +++ b/assets/frontend_base_HTML.md.bd02116d.lean.js @@ -0,0 +1 @@ +import{_ as a,o as e,c as t,Q as o}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"HTML","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/base/HTML.md","filePath":"frontend/base/HTML.md","lastUpdated":1708510102000}'),i={name:"frontend/base/HTML.md"},l=o("",45),r=[l];function n(d,h,c,p,s,u){return e(),t("div",null,r)}const q=a(i,[["render",n]]);export{m as __pageData,q as default}; diff --git a/assets/frontend_base_JavaScript.md.5b22eba4.js b/assets/frontend_base_JavaScript.md.5b22eba4.js new file mode 100644 index 000000000..8945725f9 --- /dev/null +++ b/assets/frontend_base_JavaScript.md.5b22eba4.js @@ -0,0 +1,1461 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"JavaScript","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/base/JavaScript.md","filePath":"frontend/base/JavaScript.md","lastUpdated":1708510102000}'),p={name:"frontend/base/JavaScript.md"},o=l(`

JavaScript

组成

  • ECMAScript:规定了js基础语法,比如变量、分支语句、循环语句、对象等
  • Web APIs
    • DOM:操作文档,比如对页面元素移动、添加删除等操作
    • BOM:操作浏览器,比如页面弹窗、检测窗口宽度、存储数据到浏览器等

基本语法

输入

prompt()

输出

console.log()

document.write()

alert()

alert和prompt会跳过页面渲染先被执行

变量

声明

  • let

比较旧的JavaScript中使用var声明变量

var的一些问题:

  • 可以先使用,再声明
  • var声明过的变量可以重复声明
  • 变量提升、全局变量、没有块级作用域

命名

  • 只能下划线、字母、数字、$,且不能数字开头
  • 字母区分大小写
规范
  • 小驼峰

数组

  • let arr = [1, 2, 3] / let arr = new Array(1, 2, 3)

  • 数组有序

  • 取值:数组[下标]

  • 长度:数组.length

  • 修改:arr[下标] = 新值

  • 增加

    • arr.push() 将一个或多个元素新增到末尾,返回新的数组长度
    • arr.unshift()将一个或多个元素新增到开头,返回新的数组长度
  • 删除

    • arr.pop() 删除最后一个元素,并返回该元素的值
    • arr.shift()删除第一个元素,并返回该元素的值
    • arr.splice(操作的下标,删除的个数),删除指定元素并返回

常量

  • 声明:const
  • 声明常量必须赋值

数据类型

基本数据类型

  • number

  • string

    • 模板字符串: 使用反引号包裹数据,使用\${}替换数据

      js
      let age = 20
      +console.log(\`我今年\${age}岁\`)
      let age = 20
      +console.log(\`我今年\${age}岁\`)
  • boolean

  • undefined

    • 没有赋值
    • undefined +1 -> NaN
  • null

    • 内容为空
    • null + 1 -> 1
NaN

NaN代表一个计算错误,是一个不正确或未定义的数学操作得到的结果,任何对NaN的操作都会返回NaN

typeof

  • 运算符写法:typeof 变量
  • 函数写法:typeof(变量)

数据类型转换

隐式转换
  • +号两边只要有字符串,都会转字符串
  • 除了+,其他算数运算符会把数据转换为数字类型
  • +作为正号可以转换数字
显式转换
  • Number(变量)

引用数据类型

  • object

    js
    let obj = {
    +  uname: 'abc',
    +  age: 18,
    +  gender: '女',
    +  speak: function(x) {
    +    console.log('hello' + x)
    +  }
    +}
    let obj = {
    +  uname: 'abc',
    +  age: 18,
    +  gender: '女',
    +  speak: function(x) {
    +    console.log('hello' + x)
    +  }
    +}
    • 属性名可以用引号,一般省略,除非遇到特殊符号(空格、中横线等)
  • 查看:

    • 对象.属性
    • 对象['属性']
  • 修改: 对象.属性 = 新值

  • 新增: 对象.新属性 = 值

  • 删除: delete 对象.属性

  • 对象方法: 对象.方法名()

  • 遍历对象

    • js
      for (let k in obj) {
      +  console.log(obj[k]) //k带引号
      +}
      for (let k in obj) {
      +  console.log(obj[k]) //k带引号
      +}

for in遍历数组 是数组下标,但是是字符串

运算符

赋值运算符

  • +=
  • -=
  • *=
  • /=
  • %=

一元运算符

  • ++
  • --

比较运算符

  • <
  • >
  • >=
  • <=
  • ==: 值是否相等
  • ===:类型和值是否都相等
  • !==:是否不全等

逻辑运算符

  • &&
  • ||
  • !

流程控制语句

  • if

    • 除了0,所有的数字都为真
    • 除了'',所有字符串都为真
  • switch case

    • 数据和值必须满足全等===

    • js
      switch (数据) {
      +  case 值1:
      +    代码1
      +    break
      +  case 值2:
      +    代码2
      +    break
      +  default:
      +    代码n
      +}
      switch (数据) {
      +  case 值1:
      +    代码1
      +    break
      +  case 值2:
      +    代码2
      +    break
      +  default:
      +    代码n
      +}
  • 三元运算符

循环控制语句

  • while
  • for
  • break/continue

函数

js
function 函数名(参数列表) {
+  函数体
+}
function 函数名(参数列表) {
+  函数体
+}
  • 命名 小驼峰

  • return

    • 没有return 默认返回undefined

作用域

  • 全局变量
    • 局部变量或块级变量 没有let声明直接赋值的当全局变量看(不提倡)
  • 局部变量

匿名函数

  • 函数表达式:把匿名函数赋值给一个变量,通过变量名调用

    • js
      let fn = function () {}
      let fn = function () {}
  • 立即执行函数

    • js
      (function() {...})();
      +(function() {...})();
      +---
      +(function(x, y) {
      +  console.log(x + y)
      +})(1, 3)
      (function() {...})();
      +(function() {...})();
      +---
      +(function(x, y) {
      +  console.log(x + y)
      +})(1, 3)
    • 前一个括号声明,后一个括号调用

    • 分号

逻辑中断

  • 短路:只存在&&||中,当满足一定条件会让右边代码不执行
    • &&:左边为false就短路
    • ||:左边为true就短路

转换boolean

  • "": false

  • 0: false

  • undefined: false

  • null: false

  • NaN: false

  • "" + 1 = 1

  • null经过数字转换会变0

  • undefined经过数字转换会变NaN

Web APIs

DOM

获取DOM元素

  • document.querySelector(CSS选择器):获取匹配的第一个元素
  • document.querySelectorAll(CSS选择器):获取匹配的多个元素
  • document.getElementById():通过元素的 id 属性获取一个 DOM 元素
  • document.getElementsByName():通过元素的 name 属性获取一个类数组的元素集合,该方法返回一个 NodeList 对象
  • document.getElementsByClassName():方法通过元素的 name 属性获取一个类数组的元素集合,该方法返回一个 NodeList 对象
  • document.getElementsBytagName():通过元素的标签名获取一个类数组的元素集合,该方法返回一个 NodeList 对象

操作元素内容

  • 对象.innerText

  • 对象.innerHTML

  • 对象.属性=值

    • js
      const image = document.querySelector('img')
      +image.src = 'xxx.jpg'
      +image.title = '123'
      const image = document.querySelector('img')
      +image.src = 'xxx.jpg'
      +image.title = '123'
  • 对象.style.样式属性=值

    • js
      box.style.width = '300px'
      +box.backgroundColor = 'pink' //小驼峰
      box.style.width = '300px'
      +box.backgroundColor = 'pink' //小驼峰
  • 通过类名修改属性,会覆盖

    • js
      //定义好类对应的属性,给对象添加类名
      +对象.className = 类名
      //定义好类对应的属性,给对象添加类名
      +对象.className = 类名
  • 通过classList操作类控制CSS,用于追加和删除

    • js
      元素.classList.add(类名)//追加
      +元素.classList.remove(类名)//删除
      +元素.classList.toggle(类名)//切换
      元素.classList.add(类名)//追加
      +元素.classList.remove(类名)//删除
      +元素.classList.toggle(类名)//切换
  • 自定义属性

    • H5中推出的data-自定义属性

    • 在标签上一律以data-开头

    • DOM对象上一律以dataset对象方式获取

    • html
      <body>
      +  <div class="box" data-id="10">盒子</div>
      +  <script>
      +    const box = document.querySelector('.box')
      +    console.log(box.dataset.id)
      +  </script>
      +</body>
      <body>
      +  <div class="box" data-id="10">盒子</div>
      +  <script>
      +    const box = document.querySelector('.box')
      +    console.log(box.dataset.id)
      +  </script>
      +</body>

事件监听

  • 元素对象.addEventListener('事件类型', 要执行的函数)

元素.on事件:也可以添加事件监听,但会被覆盖,且只能冒泡 不能捕获,addEventListener不会被覆盖,能冒泡 也能捕获。

  • 事件类型
    • 鼠标事件
      • click
      • mouseenter: 没冒泡,只会在鼠标进入目标元素时触发一次
      • mouseover:有冒泡,事件在鼠标经过目标元素或任何子元素时会不断触发
      • mouseleave
      • mousemove: 鼠标移动
    • 焦点事件
      • focus
      • blur
    • 键盘事件
      • keydown
      • keyup
    • 文本事件
      • input

事件对象

事件对象中有事件触发时的相关信息,例如鼠标点击时的位置,键盘按下时的键位

js
btn.addEventListener('click', function(e){
+  console.log(e)
+})
btn.addEventListener('click', function(e){
+  console.log(e)
+})
常用对象属性
  • type:事件类型
  • clientX/clientY:光标相对于浏览器可见窗口左上角的位置
  • offsetX/offsetY:光标相对于当前DOM元素左上角的位置
  • key:用户按下的键盘的值,现在不提倡使用keyCode

环境对象

指的是函数内部特殊的变量this,它代表着当前函数运行时所处的环境

  • 函数的调用方式不同,this的指代对象也不通
  • this指向的粗略规则是谁调用指向谁(addEventListener指向绑定的元素,普通函数指向window)

回调函数

函数A作为参数传递给函数B,A就被称为回调函数

事件流

事件流指的是事件完整执行过程中的流动路径

事件捕获

DOM的根元素开始去执行对应的事件(从父元素到子元素)

js
DOM.addEventListener(事件类型, 函数, 是否使用捕获机制)
DOM.addEventListener(事件类型, 函数, 是否使用捕获机制)

L0事件只有冒泡,没有捕获

事件冒泡

当一个元素的事件被触发时,同样的事件会在该元素的所有祖先元素中依次被触发。这一过程被称为事件冒泡(从子元素到父元素)

  • 简单理解:当一个元素触发事件后,会依次向上调用所有父级元素的同名事件

  • 事件冒泡是默认存在的

阻止事件传播
  • 事件对象.stopPropagation()
  • 阻断事件流动传播,既能阻止冒泡,也能阻止捕获
js
btn.addEventListener('click', function(e){
+  e.stopPropagation()
+})
btn.addEventListener('click', function(e){
+  e.stopPropagation()
+})
解绑事件
  • on事件方式

    • js
      // 绑定事件
      +btn.onClick = function(e){
      +  console.log(e)
      +}
      +// 解绑事件
      +btn.onClick = null
      // 绑定事件
      +btn.onClick = function(e){
      +  console.log(e)
      +}
      +// 解绑事件
      +btn.onClick = null
  • addEventListener方式

    • js
      function fn(e){
      +  console.log(e)
      +}
      +//绑定事件
      +btn.addEventListener('click', fn)
      +//解绑事件
      +btn.removeEventListener('click', fn)
      function fn(e){
      +  console.log(e)
      +}
      +//绑定事件
      +btn.addEventListener('click', fn)
      +//解绑事件
      +btn.removeEventListener('click', fn)
    • 匿名函数无法解绑

事件委托

事件委托是利用事件流特征解决开发问题的技巧,可以减少事件注册次数,提高程序性能,原理是利用事件冒泡特点,给父元素注册事件,当触发子元素的时候,会冒泡到父元素身上,从而触发父元素的事件

阻止元素默认行为

e.preventDefault()

其他事件
  • 页面加载事件

    • 外部资源加载完毕时触发的事件

      • 等待页面所有资源加载完毕,执行回调函数:window.addEventListener('load', function() {})

        也可以针对某个资源绑定事件:img.addEventListener('load', function() {})

    • 初始HTML文档被完全加载和解析完成后,DOMContentLoaded事件被触发,无需等待样式表、图像等完全加载

      • document.addEventListener('DOMContentLoaded', function() {})
  • 页面滚动事件

    • 滚动条在滚动的时候持续触发的事件

      • window.addEventListener('scroll', function() {})
      • 给window或document添加scroll事件
      • 也可以监听某个元素内部滚动
    • 获取滚动位置

      • scrollLeft**(可读写)**

      • scrollTop**(可读写)**

      • js
        window.addEventListener('scroll', function() {
        +  const n = document.documentElement.scrollTop
        +  console.log(n)
        +})
        window.addEventListener('scroll', function() {
        +  const n = document.documentElement.scrollTop
        +  console.log(n)
        +})

        document.documentElement返回对象为HTML元素

        ......
    • 滚动到指定坐标

      • scrollTo(x, y)
  • 页面尺寸事件

    • 窗口尺寸改变时触发的事件resize
      • window.addEventListener('resize', function() {})
    • 获取元素可见部分的宽高clientWidthclientHeight
      • 不包含border,margin,滚动条

元素尺寸位置

获取宽高
  • offsetWidth和offsetHeight
  • 获取元素自身的宽高,包含padding,border
  • 结果是数值
  • 获取的是可视宽高,如果盒子隐藏,结果是0
获取位置
  • offsetLeft和offsetTop
  • 获取元素距离自己定位父级元素的左、上距离,只读属性
获取元素大小及其相对视口的位置
  • element.getBoundingClientRect()

日期对象

  • 实例化

    • const date = new Date()
    • const date = new Date('2023-4-8 08:00:00')
  • 常用方法

    • getFullYear():四位数年份
    • getMonth():月份,范围0-11
    • getDate():获取月份中的每一天
    • getDay():获取星期,0-6
    • getHours():小时,0-23
    • getMinutes():分钟,0-59
    • getSeconds():秒,0-59
    • toLocaleString(): yyyy/m/d HH:mm:ss
  • 时间戳

    • date.getTime()
    • +new Date()
    • Date.now()

DOM节点

节点类型
  • 元素节点
  • 属性节点
  • 文本节点
  • 其他(注释、文档类型、CDATA、实体引用、处理指令。。。)
查找节点
  • 父节点
    • 元素.parentNode
  • 子节点
    • 元素.childNodes:获取所有子节点,包括文本(空格、换行)、注释节点等
    • 元素.children:仅获取元素节点,返回的是一个伪数组
  • 兄弟节点
    • nextElementSibling:下一个兄弟节点
    • previousElementSibling:上一个兄弟节点
新增节点
创建节点
  • const div = document.createElement('div')
追加节点
  • 父元素.appendChild(div)

  • 父元素.insertBefore(要插入的元素, 在哪个元素前面):插入某个元素之前

    • 例:ul.insertBefore(li, ul.children[0])
克隆节点
  • 元素.cloneNode(布尔值)
    • true:克隆时会包含后代节点一起克隆
    • false:不包含后代节点,默认值
删除节点
  • 父元素.removeChild(子元素)

BOM

组成

BOM(Browser Object Model)是浏览器对象模型,包含:navigator、location、document、history、screen

window是一个全局对象,document、alert()、console.log()都是window的属性

  • 所有通过var定义在全局作用域中的变量、函数都会变成window对象的属性和方法
  • window对象下的属性和方法调用的时候可以省略window

定时器

延时函数
  • let timer = setTimeout(回调函数, 等待时间ms),返回id,setTimeout只执行一次
  • 关闭:clearTimeout(timer)
间歇函数
  • let interval = setInterval(函数, 间隔时间ms),返回的是的是一个id数字,不断执行
  • 关闭:clearInterval(interval)

事件循环

js是单线程,所有任务需要排队。HTML5提出了Web Worker标准,允许JavaScript脚本创建多个线程。于是JS出现了同步和异步。

  • 同步任务:都在主线程执行,形成执行栈
  • 异步任务:通过回调函数实现,异步任务添加到任务队列中,一般异步任务有以下三种类型
    • 普通事件:click、resize等
    • 资源加载:load、error等
    • 定时器:setTimeout、setInterval等
执行机制
  1. 先执行执行栈中的同步任务
  2. 异步任务放到任务队列中
  3. 执行栈中的所有同步任务执行完毕,系统会按次序读取任务队列中的异步任务,被读取的异步任务结束等待状态,进入执行栈,开始执行

location

localtion的数据类型是对象,它拆分保存了URL地址的各个组成部分

  • location.href:常用于页面跳转

  • location.search:获取地址中携带的参数,符号?后面的部分

  • location.hash:获取地址中的hash值,符号#后面的部分

  • location.reload():用来刷新当前页面,传入参数true时强制刷新

navigator的数据类型是对象,该对象下记录了浏览器自身的相关信息

  • navigator.userAgent:检测浏览器版本和平台

history

history数据类型是对象,主要管理历史记录,该对象与浏览器地址栏的操作相对应,如前进、后退、历史记录等

  • history.back()
  • history.forward()
  • history.go(参数): 1->前进一个页面,-1->后退一个页面

本地存储

介绍

数据存储在用户浏览器中,设置、读取方便,刷新页面不会丢失数据,sessionStorage和localStorage约5M

分类
  • localStorage
    • 可以多窗口(页面)共享(同一浏览器可以共享)
    • 键值对形式存储使用
    • 语法
      • 存储:localStorage.setItem(key, value)
      • 查询:localStorage.getItem(key)
      • 删除:localStorage.removeItem(key)
  • sessionStorage
    • 生命周期到关闭浏览器窗口截止
    • 在同一个窗口(页面)下数据可以共享
    • 键值对形式存储使用
    • 用法api和localStorage一致
存储复杂数据类型

把复杂数据类型转成字符串形式存储

  • JSON.stringify
  • JSON.parse

数组map和join

map
  • 遍历数组处理数据,返回新的数组

  • js
    const arr = ['red', 'blue']
    +const newArr = arr.map(function(ele, index) {
    +  return ele + '颜色'
    +})
    +console.log(newArr) // ['red颜色', 'blue颜色']
    const arr = ['red', 'blue']
    +const newArr = arr.map(function(ele, index) {
    +  return ele + '颜色'
    +})
    +console.log(newArr) // ['red颜色', 'blue颜色']
join
  • 把数组所有元素转换为一个字符串
  • const newStr = join(字符串):元素用指定字符串相连

进阶

正则表达式

  • 定义:const reg = /表达式/
  • 判断是否匹配:reg.test(被检测字符串),匹配返回true,否则false
  • 查找:reg.exec(被检测字符串),找到返回数组,否则为null

元字符

  • 边界符

    • ^:开始
    • $:结束
  • 量词

    • *:0或多次
    • +:1或多次
    • ?:0或1次
    • {n}:重复n次
    • {n,}:重复n次或更多
    • {n,m}:重复n次到m次
  • 字符类

    • []:匹配字符集合,匹配任一个都是true
    • [a-zA-Z]:字母
    • [^a-z]:[]中的^表示取反
    • .:除换行之外的任何单个字符
    • \\d:数字
    • \\D:所有0-9以外字符,等于[^0-9]
    • \\w:任一字母、数字、下划线,相当于[a-zA-Z0-9_]
    • \\W:匹配除字母、数字、下划线之外的字符,相当于[^a-zA-Z0-9_]
    • \\s:匹配空格(包括制表符、换行符、空格符等),相当于[\\t\\r\\n\\v\\f]
    • \\S:匹配非空格,相当于[^\\t\\r\\n\\v\\f]

修饰符

  • 语法:/表达式/修饰符

  • 修饰符:

    • i:ignore,匹配时,不区分大小写
    • g:global,匹配所有满足正则的结果

替换

  • 语法:字符串.replace(/正则表达式/, 替换的文本),返回替换后的字符串

作用域

  • 局部作用域

  • 全局作用域

  • 作用域链

  • JS垃圾回收机制

    • 全局变量一般不会回收(关闭页面回收)
    • 一般情况下局部变量的值不再被使用会被自动回收
    • 内存由于某种原因未释放或无法释放会内存泄漏
    • 栈:由操作系统自动分配释放函数的参数值、局部变量等基本数据类型放在栈里
    • 堆:一般由开发分配释放,若开发不释放由垃圾回收机制回收。复杂数据类型放在堆里。

    引用计数法(有循环引用问题)

    • 定义“内存不再使用”,看一个对象是否有指向它的引用,没有引用就回收对象
      • 根据记录被引用的次数
      • 被引用一次,就+1,多次引用会累加
      • 如果减少一个引用就-1
      • 如果引用次数是0,则释放内存

    标记清除法

    • 将不再使用的对象定义为无法达到的对象
    • 从根部(JS中就是全局对象)出发定时扫描内存中的对象,凡是能从根部到达的对象,都是还需要使用的
    • 无法由根部出发触及的对象标记为不再使用,稍后进行回收
  • 闭包

    • 和python中的闭包一样:如果在一个外部函数中定义一个内部函数,内部函数对外部作用域的变量进行引用,外部函数的返回值是内部函数,这样的函数就被认为是闭包(closure)。
  • 变量提升

    • 允许变量在声明之前即被访问(var声明变量)
    • js会在执行之前把当前作用域下var声明的变量提升到当前作用域的最前面,只提升声明,不提升赋值
  • 函数提升

    • 代码执行前会把所有函数声明提升到当前作用域的最前面
    • 只提升声明,不提升调用

函数表达式特殊,必须先声明赋值后调用

函数进阶

  • 动态参数:arguments,只存在于函数里,伪数组

  • 剩余参数:function getSum(paramA, paramB, ...arr),arr是个真数组

  • 展开运算符:...能将一个数组进行展开

    • js
      const arr = [1,5,3]
      +console.log(...arr)// 1 5 3
      const arr = [1,5,3]
      +console.log(...arr)// 1 5 3
    • 用于求数组最大/小值Math.max(...arr)

    • 用于合并数组:const arr = [...arr1, ...arr2]

箭头函数

引入箭头函数是为了更简洁的写法,适用于需要匿名函数的地方

js
const fn = () => {}
+const fn = x => { console.log(x) }
+const fn = x => console.log(x)
+const fn = x => x * 2
+const fn = (uname) => ({ uname: uname }) //返回一个对象
const fn = () => {}
+const fn = x => { console.log(x) }
+const fn = x => console.log(x)
+const fn = x => x * 2
+const fn = (uname) => ({ uname: uname }) //返回一个对象
箭头函数的this

箭头函数不会创建自己的this对象,它只会从自己的作用域链的上一层

解构赋值

数组解构

数组结构是将数组的单元值快速批量赋值给一系列变量的简洁语法

  • const [max, min, avg] = [100, 60, 80]
  • 典型用法:交换两个变量
  • 可以设置默认值
  • 可以用剩余参数防止undefined传递
  • 可以忽略某些值const [a, ,c, d] = [1, 2, 3, 4]

js必须加分号场景:

  1. 两个连续的立即执行函数
  2. 使用数组

对象解构

对象解构是将对象的属性和方法快速批量赋值给一系列变量的简介语法

  • js
    const user = {
    +  name: '小明',
    +  age: 18
    +}
    +const {name, age} = user
    const user = {
    +  name: '小明',
    +  age: 18
    +}
    +const {name, age} = user
  • 对象的属性值将会被赋值给与属性名相同的变量

  • 对象中找不到与变量名一致的属性时变量值为undefined

  • 数组对象解构

    js
    const pig = [
    +  {
    +    name: '佩奇',
    +    age: 6
    +  }
    +]
    +
    +const [{ name, age }] = pig
    +console.log(name,age)
    const pig = [
    +  {
    +    name: '佩奇',
    +    age: 6
    +  }
    +]
    +
    +const [{ name, age }] = pig
    +console.log(name,age)
  • 多级对象解构

    js
    const pig = {
    +  name: '佩奇',
    +  age: 6,
    +  family: {
    +    mother: 'mon',
    +    father: 'dad'
    +  }
    +}
    +
    +const { name, family: { mother, father }} = pig
    const pig = {
    +  name: '佩奇',
    +  age: 6,
    +  family: {
    +    mother: 'mon',
    +    father: 'dad'
    +  }
    +}
    +
    +const { name, family: { mother, father }} = pig

对象

创建对象的方式

  • 字面量创建

  • 构造函数

    • 命名以大写字母开头
    • 只能由new操作符来执行
    • 实例化执行过程
      • 创建新对象
      • 构造函数this指向新对象
      • 执行构造函数代码,修改this,添加属性
      • 返回新对象

实例成员&静态成员

  • 实例成员:构造函数创建的对象为实例对象,实例对象的属性和方法称为实例成员
  • 静态成员:构造函数的属性和方法称为静态成员
    • 静态成员只能由构造函数访问
    • 静态方法中的this指向构造函数
    • Date.now()、Math.PI、Math.random()

内置构造函数

  • Object

    • Object.keys()
    • Object.values()
    • Object.assign(dest, source)
  • Array

    • 实例方法:forEach、filter、map、reduce、join、find、every、some、concat、splice、reverse、findIndex...

    • 伪数组转换为真数组:Array.from()

    • arr.some((item, index)=> {
      +	//some code, some循环可以终止
      +	return true
      +})
      +
      +//every 判断数组每一项是否都满足条件
      +let res = arr.every(item => item.state) 
      +
      +//reduce 
      +arr.reduce((累加的结果, 当前循环项) => {}, 初始值)
      +arr.reduce((amt, item) => amt += item.price, 0)
      arr.some((item, index)=> {
      +	//some code, some循环可以终止
      +	return true
      +})
      +
      +//every 判断数组每一项是否都满足条件
      +let res = arr.every(item => item.state) 
      +
      +//reduce 
      +arr.reduce((累加的结果, 当前循环项) => {}, 初始值)
      +arr.reduce((amt, item) => amt += item.price, 0)
  • String

    • 实例属性、方法:length、split()、substring()、startsWith()、includes()、toUpperCase()、toLowerCase()、indexOf()、endsWith()、replace()、match()...
  • Number

    • toFixed()设置保留小数位数

原型Prototype

  • 构造函数通过原型分配的函数是所有对象所共享的。
  • JavaScript每一个构造函数都有一个prototype属性,指向另一个对象,所以也称为原型对象
  • prototype对象可以挂载函数,对象实例化不会多次创建原型上函数,节约内存
  • 可以把不变的方法直接定义在prototype对象上,这样所有对象的实例就可以共享这些方法
  • 构造函数和原型对象中的this都指向实例化的对象

constructor属性

每个原型对象里都有个constructor属性,该属性指向该原型对象的构造函数

对象原型

每个对象都有一个属性__proto__,指向构造函数的prototype对象

  • __proto__是JS非标准属性
  • [[prototype]]和__proto__意义相同
  • 用来表明当前实例对象指向哪个原型对象prototype
  • __proto__对象原型里也有一个constructor属性,指向创建该实例对象的构造函数

原型继承

通过原型可以继承公共属性

js
const Person = {
+  eyes: 2,
+  nose: 1
+}
+
+function Man() {
+  
+}
+Man.prototype = Person
+Man.prototype.constructor = Man
+
+---
+  
+const Person = {
+  this.eyes: 2,
+  this.nose: 1
+}
+
+function Man() {
+  
+}
+Man.prototype = new Person()
const Person = {
+  eyes: 2,
+  nose: 1
+}
+
+function Man() {
+  
+}
+Man.prototype = Person
+Man.prototype.constructor = Man
+
+---
+  
+const Person = {
+  this.eyes: 2,
+  this.nose: 1
+}
+
+function Man() {
+  
+}
+Man.prototype = new Person()

原型链

基于原型对象的继承使得不同构造函数的原型对象关联在一起,并且这种关联关系是一种链状的解构,称为原型链

查找规则
  • 当访问一个对象的属性/方法时,首先查找这个对象自身有无该属性
  • 如果没有就查找他的原型(__proto__指向的prototype对象)
  • 如果还没有就查找原型对象的原型(Object的prototype)
  • 依此类推一直到Object为止(null)
  • __proto__对象原型的意义就在于为对象成员查找机制提供方向
  • 可以使用instanceof运算符检测构造函数的prototype属性是否出现在某个实例对象的原型链上

深浅拷贝

深浅拷贝只针对引用数据类型

  • 浅拷贝:如果是简单数据类型拷贝值,引用数据类型拷贝的是地址
    • 拷贝对象:Object.assign() / 展开运算符 {...obj}拷贝对象
    • 拷贝数组:Array.prototype.concat() 或者 [...arr]
  • 深拷贝:拷贝的是对象,不是地址
    • 通过递归实现深拷贝
    • lodash中的_.cloneDeep()
    • JSON.stringify()

异常

抛出异常

  • throw msg
  • throw new Error(msg)

异常捕获

js
try {
+  
+} catch (err) {
+  
+} finally {
+  
+}
try {
+  
+} catch (err) {
+  
+} finally {
+  
+}

debugger

debugger

this

普通函数

  • 普通函数的调用方式决定了this的值,即谁调用 this的值指向谁

  • 普通函数没有明确调用者时this的值为window,严格模式下没有调用者时this的值为undefined

箭头函数

  • 箭头函数中并不存在this
  • 箭头函数会默认绑定外层this的值,所以在箭头函数中this的值和外层的this是一样的
  • 箭头函数中的this引用的就是最近作用域中的this
  • 向外层作用域中,一层一层查找this,直到有this的定义

改变this指向

  • fun.call(thisArg, arg1, arg2...)
    • thisArg:fun函数运营时指定的this值
    • arg1,arg2:传递的其他参数
  • apply(thisArg, [argsArray])
    • thisArg:fun函数运营时指定的this值
    • argsArray:传递的值,必须包含在数组里
  • bind()
    • bind不会调用函数,但能改变函数内部this的指向
    • fun.bind(thisArg, arg1, arg2...)
    • thisArg:fun函数运营时指定的this值
    • arg1,arg2:传递的其他参数
    • 返回由指定this值和初始化参数改造的原函数的拷贝

防抖(debounce)

  • 单位时间内,频繁触发事件,只执行最后一次

  • lodash库的_.debounce(fun, 时间)

思路

  1. 声明一个定时器
  2. 每次触发事件都先判断是否有定时器,如果有先清除
  3. 如果没有则开启定时器并保存变量
  4. 在定时器中调用要执行的函数
js
const box = document.querySelector('.box')
+let i = 1
+function mouseMove() {
+  box.innerHTML = i++
+}
+
+function debounce(fn, t) {
+  let timer
+  return function() {
+    if (timer) clearTimeout(timer)
+    timer = setTimeout(function() {
+      fn()
+    }, t)
+  }
+}
+
+box.addEventListener('mousemove', debounce(mouseMove, 500))
const box = document.querySelector('.box')
+let i = 1
+function mouseMove() {
+  box.innerHTML = i++
+}
+
+function debounce(fn, t) {
+  let timer
+  return function() {
+    if (timer) clearTimeout(timer)
+    timer = setTimeout(function() {
+      fn()
+    }, t)
+  }
+}
+
+box.addEventListener('mousemove', debounce(mouseMove, 500))

节流(throttle)

  • 单位时间内,频繁触发事件,只执行一次
  • lodash库的_.throttle(fun, 时间)

思路

  1. 声明一个定时器
  2. 每次触发事件都判断是否有定时器,如果有则不开启新定时器
  3. 如果没有定时器则开启定时器并保存变量
    1. 定时器里调用执行的函数
    2. 定时器里要把上一个定时器清空
js
function throttle(fn, t) {
+  let timer = null
+  return function() {
+    if(!timer) {
+      timer = setTimeout(function(){
+        fn()
+        // setTimeout中无法删除定时器,因为定时器还在运作,所以不能用clearTimeout
+        timer = null
+      }, t)
+    }
+  }
+}
+
+box.addEventListener('mousemove', throttle(mouseMove, 500))
function throttle(fn, t) {
+  let timer = null
+  return function() {
+    if(!timer) {
+      timer = setTimeout(function(){
+        fn()
+        // setTimeout中无法删除定时器,因为定时器还在运作,所以不能用clearTimeout
+        timer = null
+      }, t)
+    }
+  }
+}
+
+box.addEventListener('mousemove', throttle(mouseMove, 500))

案例:页面打开,记录上一次的视频播放位置

两个事件
  • ontimeupdate:事件在视频/音频当前播放位置发生改变时触发
  • onloadeddata:事件在当前帧的数据加载完成且还没有足够的数据播放视频/音频的下一帧时触发
js
video.ontimeupdadte = _.throttle(() => {
+  localStorage.setItem('currentTime', video.currentTime)
+}, 1000)
+
+video.onloadeddata = () => {
+  video.currentTime = localStorage.getItem('currentTime') || 0
+}
video.ontimeupdadte = _.throttle(() => {
+  localStorage.setItem('currentTime', video.currentTime)
+}, 1000)
+
+video.onloadeddata = () => {
+  video.currentTime = localStorage.getItem('currentTime') || 0
+}

ES6

Promise

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

Promise对象有以下两个特点。

(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

基本用法

ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。

下面代码创造了一个Promise实例。

js
const promise = new Promise(function(resolve, reject) {
+  // ... some code
+
+  if (/* 异步操作成功 */){
+    resolve(value);
+  } else {
+    reject(error);
+  }
+});
const promise = new Promise(function(resolve, reject) {
+  // ... some code
+
+  if (/* 异步操作成功 */){
+    resolve(value);
+  } else {
+    reject(error);
+  }
+});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供。

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。

js
promise.then(function(value) {
+  // success
+}, function(error) {
+  // failure
+});
promise.then(function(value) {
+  // success
+}, function(error) {
+  // failure
+});

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。这两个函数都是可选的,不一定要提供。它们都接受Promise对象传出的值作为参数。

下面是一个Promise对象的简单例子。

js
function timeout(ms) {
+  return new Promise((resolve, reject) => {
+    setTimeout(resolve, ms, 'done');//setTimeout的第三个参数是给第一个函数参数传递的参数,即done会传递给resolve函数作为参数
+  });
+}
+
+timeout(100).then((value) => {
+  console.log(value);
+});
function timeout(ms) {
+  return new Promise((resolve, reject) => {
+    setTimeout(resolve, ms, 'done');//setTimeout的第三个参数是给第一个函数参数传递的参数,即done会传递给resolve函数作为参数
+  });
+}
+
+timeout(100).then((value) => {
+  console.log(value);
+});

上面代码中,timeout方法返回一个Promise实例,表示一段时间以后才会发生的结果。过了指定的时间(ms参数)以后,Promise实例的状态变为resolved,就会触发then方法绑定的回调函数。

Promise 新建后就会立即执行。

js
let promise = new Promise(function(resolve, reject) {
+  console.log('Promise');
+  resolve();
+});
+
+promise.then(function() {
+  console.log('resolved.');
+});
+
+console.log('Hi!');
+
+// Promise
+// Hi!
+// resolved
let promise = new Promise(function(resolve, reject) {
+  console.log('Promise');
+  resolve();
+});
+
+promise.then(function() {
+  console.log('resolved.');
+});
+
+console.log('Hi!');
+
+// Promise
+// Hi!
+// resolved

上面代码中,Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出。

下面是异步加载图片的例子。

js
function loadImageAsync(url) {
+  return new Promise(function(resolve, reject) {
+    const image = new Image();
+
+    image.onload = function() {
+      resolve(image);
+    };
+
+    image.onerror = function() {
+      reject(new Error('Could not load image at ' + url));
+    };
+
+    image.src = url;
+  });
+}
function loadImageAsync(url) {
+  return new Promise(function(resolve, reject) {
+    const image = new Image();
+
+    image.onload = function() {
+      resolve(image);
+    };
+
+    image.onerror = function() {
+      reject(new Error('Could not load image at ' + url));
+    };
+
+    image.src = url;
+  });
+}

上面代码中,使用Promise包装了一个图片加载的异步操作。如果加载成功,就调用resolve方法,否则就调用reject方法。

Generator

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。本章详细介绍 Generator 函数的语法和 API,它的异步编程应用请看《Generator 函数的异步应用》一章。

Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。

执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

js
function* helloWorldGenerator() {
+  yield 'hello';
+  yield 'world';
+  return 'ending';
+}
+
+var hw = helloWorldGenerator();
function* helloWorldGenerator() {
+  yield 'hello';
+  yield 'world';
+  return 'ending';
+}
+
+var hw = helloWorldGenerator();

上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(helloworld),即该函数有三个状态:hello,world 和 return 语句(结束执行)。

然后,Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)。

下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

js
hw.next()
+// { value: 'hello', done: false }
+
+hw.next()
+// { value: 'world', done: false }
+
+hw.next()
+// { value: 'ending', done: true }
+
+hw.next()
+// { value: undefined, done: true }
hw.next()
+// { value: 'hello', done: false }
+
+hw.next()
+// { value: 'world', done: false }
+
+hw.next()
+// { value: 'ending', done: true }
+
+hw.next()
+// { value: undefined, done: true }

yield 表达式

由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。

遍历器对象的next方法的运行逻辑如下。

(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。

(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

(4)如果该函数没有return语句,则返回的对象的value属性值为undefined

需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

js
function* gen() {
+  yield  123 + 456;
+}
function* gen() {
+  yield  123 + 456;
+}

上面代码中,yield后面的表达式123 + 456,不会立即求值,只会在next方法将指针移到这一句时,才会求值。

yield表达式与return语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield表达式。正常函数只能返回一个值,因为只能执行一次return;Generator 函数可以返回一系列的值,因为可以有任意多个yield。从另一个角度看,也可以说 Generator 生成了一系列的值,这也就是它的名称的来历(英语中,generator 这个词是“生成器”的意思)。

await

正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

js
async function f() {
+  // 等同于
+  // return 123;
+  return await 123;
+}
+
+f().then(v => console.log(v))
+// 123
async function f() {
+  // 等同于
+  // return 123;
+  return await 123;
+}
+
+f().then(v => console.log(v))
+// 123

上面代码中,await命令的参数是数值123,这时等同于return 123

另一种情况是,await命令后面是一个thenable对象(即定义了then方法的对象),那么await会将其等同于 Promise 对象。

js
class Sleep {
+  constructor(timeout) {
+    this.timeout = timeout;
+  }
+  then(resolve, reject) {
+    const startTime = Date.now();
+    setTimeout(
+      () => resolve(Date.now() - startTime),
+      this.timeout
+    );
+  }
+}
+
+(async () => {
+  const sleepTime = await new Sleep(1000);
+  console.log(sleepTime);
+})();
+// 1000
class Sleep {
+  constructor(timeout) {
+    this.timeout = timeout;
+  }
+  then(resolve, reject) {
+    const startTime = Date.now();
+    setTimeout(
+      () => resolve(Date.now() - startTime),
+      this.timeout
+    );
+  }
+}
+
+(async () => {
+  const sleepTime = await new Sleep(1000);
+  console.log(sleepTime);
+})();
+// 1000

上面代码中,await命令后面是一个Sleep对象的实例。这个实例不是 Promise 对象,但是因为定义了then方法,await会将其视为Promise处理。

这个例子还演示了如何实现休眠效果。JavaScript 一直没有休眠的语法,但是借助await命令就可以让程序停顿指定的时间。下面给出了一个简化的sleep实现。

js
function sleep(interval) {
+  return new Promise(resolve => {
+    setTimeout(resolve, interval);
+  })
+}
+
+// 用法
+async function one2FiveInAsync() {
+  for(let i = 1; i <= 5; i++) {
+    console.log(i);
+    await sleep(1000);
+  }
+}
+
+one2FiveInAsync();
function sleep(interval) {
+  return new Promise(resolve => {
+    setTimeout(resolve, interval);
+  })
+}
+
+// 用法
+async function one2FiveInAsync() {
+  for(let i = 1; i <= 5; i++) {
+    console.log(i);
+    await sleep(1000);
+  }
+}
+
+one2FiveInAsync();

await命令后面的 Promise 对象如果变为reject状态,则reject的参数会被catch方法的回调函数接收到。

js
async function f() {
+  await Promise.reject('出错了');
+}
+
+f()
+.then(v => console.log(v))
+.catch(e => console.log(e))
+// 出错了
async function f() {
+  await Promise.reject('出错了');
+}
+
+f()
+.then(v => console.log(v))
+.catch(e => console.log(e))
+// 出错了

注意,上面代码中,await语句前面没有return,但是reject方法的参数依然传入了catch方法的回调函数。这里如果在await前面加上return,效果是一样的。

任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。

js
async function f() {
+  await Promise.reject('出错了');
+  await Promise.resolve('hello world'); // 不会执行
+}
async function f() {
+  await Promise.reject('出错了');
+  await Promise.resolve('hello world'); // 不会执行
+}

上面代码中,第二个await语句是不会执行的,因为第一个await语句状态变成了reject

有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await放在try...catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行。

js
async function f() {
+  try {
+    await Promise.reject('出错了');
+  } catch(e) {
+  }
+  return await Promise.resolve('hello world');
+}
+
+f()
+.then(v => console.log(v))
+// hello world
async function f() {
+  try {
+    await Promise.reject('出错了');
+  } catch(e) {
+  }
+  return await Promise.resolve('hello world');
+}
+
+f()
+.then(v => console.log(v))
+// hello world

另一种方法是await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。

js
async function f() {
+  await Promise.reject('出错了')
+    .catch(e => console.log(e));
+  return await Promise.resolve('hello world');
+}
+
+f()
+.then(v => console.log(v))
+// 出错了
+// hello world
async function f() {
+  await Promise.reject('出错了')
+    .catch(e => console.log(e));
+  return await Promise.resolve('hello world');
+}
+
+f()
+.then(v => console.log(v))
+// 出错了
+// hello world

async

async 函数是什么?一句话,它就是 Generator 函数的语法糖。返回值是 Promise 对象

Generator 函数,依次读取两个文件。

js
const fs = require('fs');
+
+const readFile = function (fileName) {
+  return new Promise(function (resolve, reject) {
+    fs.readFile(fileName, function(error, data) {
+      if (error) return reject(error);
+      resolve(data);
+    });
+  });
+};
+
+const gen = function* () {
+  const f1 = yield readFile('/etc/fstab');
+  const f2 = yield readFile('/etc/shells');
+  console.log(f1.toString());
+  console.log(f2.toString());
+};
+
+const g = gen();
+g.next().value.then(function (data) {
+  g.next(data).value.then(function (data) {
+    g.next(data);
+  });
+});
const fs = require('fs');
+
+const readFile = function (fileName) {
+  return new Promise(function (resolve, reject) {
+    fs.readFile(fileName, function(error, data) {
+      if (error) return reject(error);
+      resolve(data);
+    });
+  });
+};
+
+const gen = function* () {
+  const f1 = yield readFile('/etc/fstab');
+  const f2 = yield readFile('/etc/shells');
+  console.log(f1.toString());
+  console.log(f2.toString());
+};
+
+const g = gen();
+g.next().value.then(function (data) {
+  g.next(data).value.then(function (data) {
+    g.next(data);
+  });
+});

上面代码的函数gen可以写成async函数,就是下面这样。

js
const asyncReadFile = async function () {
+  const f1 = await readFile('/etc/fstab');
+  const f2 = await readFile('/etc/shells');
+  console.log(f1.toString());
+  console.log(f2.toString());
+};
const asyncReadFile = async function () {
+  const f1 = await readFile('/etc/fstab');
+  const f2 = await readFile('/etc/shells');
+  console.log(f1.toString());
+  console.log(f2.toString());
+};

一比较就会发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。

async函数对 Generator 函数的改进,体现在以下四点。

(1)内置执行器。

Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。

js
asyncReadFile();
asyncReadFile();

上面的代码调用了asyncReadFile函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用next方法,或者用co模块,才能真正执行,得到最后结果。

(2)更好的语义。

asyncawait,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

(3)更广的适用性。

co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。

(4)返回值是 Promise。

async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

基本用法

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

下面是一个例子。

js
async function getStockPriceByName(name) {
+  const symbol = await getStockSymbol(name);
+  const stockPrice = await getStockPrice(symbol);
+  return stockPrice;
+}
+
+getStockPriceByName('goog').then(function (result) {
+  console.log(result);
+});
async function getStockPriceByName(name) {
+  const symbol = await getStockSymbol(name);
+  const stockPrice = await getStockPrice(symbol);
+  return stockPrice;
+}
+
+getStockPriceByName('goog').then(function (result) {
+  console.log(result);
+});

上面代码是一个获取股票报价的函数,函数前面的async关键字,表明该函数内部有异步操作。调用该函数时,会立即返回一个Promise对象。

下面是另一个例子,指定多少毫秒后输出一个值。

js
function timeout(ms) {
+  return new Promise((resolve) => {
+    setTimeout(resolve, ms);
+  });
+}
+
+async function asyncPrint(value, ms) {
+  await timeout(ms);
+  console.log(value);
+}
+
+asyncPrint('hello world', 50);
function timeout(ms) {
+  return new Promise((resolve) => {
+    setTimeout(resolve, ms);
+  });
+}
+
+async function asyncPrint(value, ms) {
+  await timeout(ms);
+  console.log(value);
+}
+
+asyncPrint('hello world', 50);

上面代码指定 50 毫秒以后,输出hello world

由于async函数返回的是 Promise 对象,可以作为await命令的参数。所以,上面的例子也可以写成下面的形式。

js
async function timeout(ms) {
+  await new Promise((resolve) => {
+    setTimeout(resolve, ms);
+  });
+}
+
+async function asyncPrint(value, ms) {
+  await timeout(ms);
+  console.log(value);
+}
+
+asyncPrint('hello world', 50);
async function timeout(ms) {
+  await new Promise((resolve) => {
+    setTimeout(resolve, ms);
+  });
+}
+
+async function asyncPrint(value, ms) {
+  await timeout(ms);
+  console.log(value);
+}
+
+asyncPrint('hello world', 50);

async 函数有多种使用形式。

js
// 函数声明
+async function foo() {}
+
+// 函数表达式
+const foo = async function () {};
+
+// 对象的方法
+let obj = { async foo() {} };
+obj.foo().then(...)
+
+// Class 的方法
+class Storage {
+  constructor() {
+    this.cachePromise = caches.open('avatars');
+  }
+
+  async getAvatar(name) {
+    const cache = await this.cachePromise;
+    return cache.match(\`/avatars/\${name}.jpg\`);
+  }
+}
+
+const storage = new Storage();
+storage.getAvatar('jake').then(…);
+
+// 箭头函数
+const foo = async () => {};
// 函数声明
+async function foo() {}
+
+// 函数表达式
+const foo = async function () {};
+
+// 对象的方法
+let obj = { async foo() {} };
+obj.foo().then(...)
+
+// Class 的方法
+class Storage {
+  constructor() {
+    this.cachePromise = caches.open('avatars');
+  }
+
+  async getAvatar(name) {
+    const cache = await this.cachePromise;
+    return cache.match(\`/avatars/\${name}.jpg\`);
+  }
+}
+
+const storage = new Storage();
+storage.getAvatar('jake').then(…);
+
+// 箭头函数
+const foo = async () => {};

语法

返回Promise对象

async函数返回一个 Promise 对象。

async函数内部return语句返回的值,会成为then方法回调函数的参数。

js
async function f() {
+  return 'hello world';
+}
+
+f().then(v => console.log(v))
+// "hello world"
async function f() {
+  return 'hello world';
+}
+
+f().then(v => console.log(v))
+// "hello world"

上面代码中,函数f内部return命令返回的值,会被then方法回调函数接收到。

async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。

js
async function f() {
+  throw new Error('出错了');
+}
+
+f().then(
+  v => console.log('resolve', v),
+  e => console.log('reject', e)
+)
+//reject Error: 出错了
async function f() {
+  throw new Error('出错了');
+}
+
+f().then(
+  v => console.log('resolve', v),
+  e => console.log('reject', e)
+)
+//reject Error: 出错了

Promise对象状态变化

async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

下面是一个例子。

js
async function getTitle(url) {
+  let response = await fetch(url);
+  let html = await response.text();
+  return html.match(/<title>([\\s\\S]+)<\\/title>/i)[1];
+}
+getTitle('https://tc39.github.io/ecma262/').then(console.log)
+// "ECMAScript 2017 Language Specification"
async function getTitle(url) {
+  let response = await fetch(url);
+  let html = await response.text();
+  return html.match(/<title>([\\s\\S]+)<\\/title>/i)[1];
+}
+getTitle('https://tc39.github.io/ecma262/').then(console.log)
+// "ECMAScript 2017 Language Specification"

上面代码中,函数getTitle内部有三个操作:抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行then方法里面的console.log

错误处理

如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject

js
async function f() {
+  await new Promise(function (resolve, reject) {
+    throw new Error('出错了');
+  });
+}
+
+f()
+.then(v => console.log(v))
+.catch(e => console.log(e))
+// Error:出错了
async function f() {
+  await new Promise(function (resolve, reject) {
+    throw new Error('出错了');
+  });
+}
+
+f()
+.then(v => console.log(v))
+.catch(e => console.log(e))
+// Error:出错了

上面代码中,async函数f执行后,await后面的 Promise 对象会抛出一个错误对象,导致catch方法的回调函数被调用,它的参数就是抛出的错误对象。

防止出错的方法,也是将其放在try...catch代码块之中。

js
async function f() {
+  try {
+    await new Promise(function (resolve, reject) {
+      throw new Error('出错了');
+    });
+  } catch(e) {
+  }
+  return await('hello world');
+}
async function f() {
+  try {
+    await new Promise(function (resolve, reject) {
+      throw new Error('出错了');
+    });
+  } catch(e) {
+  }
+  return await('hello world');
+}

如果有多个await命令,可以统一放在try...catch结构中。

js
async function main() {
+  try {
+    const val1 = await firstStep();
+    const val2 = await secondStep(val1);
+    const val3 = await thirdStep(val1, val2);
+
+    console.log('Final: ', val3);
+  }
+  catch (err) {
+    console.error(err);
+  }
+}
async function main() {
+  try {
+    const val1 = await firstStep();
+    const val2 = await secondStep(val1);
+    const val3 = await thirdStep(val1, val2);
+
+    console.log('Final: ', val3);
+  }
+  catch (err) {
+    console.error(err);
+  }
+}

下面的例子使用try...catch结构,实现多次重复尝试。

js
const superagent = require('superagent');
+const NUM_RETRIES = 3;
+
+async function test() {
+  let i;
+  for (i = 0; i < NUM_RETRIES; ++i) {
+    try {
+      await superagent.get('http://google.com/this-throws-an-error');
+      break;
+    } catch(err) {}
+  }
+  console.log(i); // 3
+}
+
+test();
const superagent = require('superagent');
+const NUM_RETRIES = 3;
+
+async function test() {
+  let i;
+  for (i = 0; i < NUM_RETRIES; ++i) {
+    try {
+      await superagent.get('http://google.com/this-throws-an-error');
+      break;
+    } catch(err) {}
+  }
+  console.log(i); // 3
+}
+
+test();

上面代码中,如果await操作成功,就会使用break语句退出循环;如果失败,会被catch语句捕捉,然后进入下一轮循环。

async 函数的实现原理

async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。

js
async function fn(args) {
+  // ...
+}
+
+// 等同于
+
+function fn(args) {
+  return spawn(function* () {
+    // ...
+  });
+}
async function fn(args) {
+  // ...
+}
+
+// 等同于
+
+function fn(args) {
+  return spawn(function* () {
+    // ...
+  });
+}

所有的async函数都可以写成上面的第二种形式,其中的spawn函数就是自动执行器。

spawn函数的实现

js
function spawn(genF) {
+  return new Promise(function(resolve, reject) {
+    const gen = genF();
+    function step(nextF) {
+      let next;
+      try {
+        next = nextF();
+      } catch(e) {
+        return reject(e);
+      }
+      if(next.done) {
+        return resolve(next.value);
+      }
+      Promise.resolve(next.value).then(function(v) {
+        step(function() { return gen.next(v); });
+      }, function(e) {
+        step(function() { return gen.throw(e); });
+      });
+    }
+    step(function() { return gen.next(undefined); });
+  });
+}
function spawn(genF) {
+  return new Promise(function(resolve, reject) {
+    const gen = genF();
+    function step(nextF) {
+      let next;
+      try {
+        next = nextF();
+      } catch(e) {
+        return reject(e);
+      }
+      if(next.done) {
+        return resolve(next.value);
+      }
+      Promise.resolve(next.value).then(function(v) {
+        step(function() { return gen.next(v); });
+      }, function(e) {
+        step(function() { return gen.throw(e); });
+      });
+    }
+    step(function() { return gen.next(undefined); });
+  });
+}

实例:按顺序完成异步操作

实际开发中,经常遇到一组异步操作,需要按照顺序完成。比如,依次远程读取一组 URL,然后按照读取的顺序输出结果。

Promise 的写法如下。

js
function logInOrder(urls) {
+  // 远程读取所有URL
+  const textPromises = urls.map(url => {
+    return fetch(url).then(response => response.text());
+  });
+
+  // 按次序输出
+  textPromises.reduce((chain, textPromise) => {
+    return chain.then(() => textPromise)
+      .then(text => console.log(text));
+  }, Promise.resolve());
+}
function logInOrder(urls) {
+  // 远程读取所有URL
+  const textPromises = urls.map(url => {
+    return fetch(url).then(response => response.text());
+  });
+
+  // 按次序输出
+  textPromises.reduce((chain, textPromise) => {
+    return chain.then(() => textPromise)
+      .then(text => console.log(text));
+  }, Promise.resolve());
+}

上面代码使用fetch方法,同时远程读取一组 URL。每个fetch操作都返回一个 Promise 对象,放入textPromises数组。然后,reduce方法依次处理每个 Promise 对象,然后使用then,将所有 Promise 对象连起来,因此就可以依次输出结果。

这种写法不太直观,可读性比较差。下面是 async 函数实现。

js
async function logInOrder(urls) {
+  for (const url of urls) {
+    const response = await fetch(url);
+    console.log(await response.text());
+  }
+}
async function logInOrder(urls) {
+  for (const url of urls) {
+    const response = await fetch(url);
+    console.log(await response.text());
+  }
+}

上面代码确实大大简化,问题是所有远程操作都是继发。只有前一个 URL 返回结果,才会去读取下一个 URL,这样做效率很差,非常浪费时间。我们需要的是并发发出远程请求。

js
async function logInOrder(urls) {
+  // 并发读取远程URL
+  const textPromises = urls.map(async url => {
+    const response = await fetch(url);
+    return response.text();
+  });
+
+  // 按次序输出
+  for (const textPromise of textPromises) {
+    console.log(await textPromise);
+  }
+}
async function logInOrder(urls) {
+  // 并发读取远程URL
+  const textPromises = urls.map(async url => {
+    const response = await fetch(url);
+    return response.text();
+  });
+
+  // 按次序输出
+  for (const textPromise of textPromises) {
+    console.log(await textPromise);
+  }
+}

上面代码中,虽然map方法的参数是async函数,但它是并发执行的,因为只有async函数内部是继发执行,外部不受影响。后面的for..of循环内部使用了await,因此实现了按顺序输出。

Class

基本语法

JavaScript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子。

js
function Point(x, y) {
+  this.x = x;
+  this.y = y;
+}
+
+Point.prototype.toString = function () {
+  return '(' + this.x + ', ' + this.y + ')';
+};
+
+var p = new Point(1, 2);
function Point(x, y) {
+  this.x = x;
+  this.y = y;
+}
+
+Point.prototype.toString = function () {
+  return '(' + this.x + ', ' + this.y + ')';
+};
+
+var p = new Point(1, 2);

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。

基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用 ES6 的class改写,就是下面这样。

js
class Point {
+  constructor(x, y) {
+    this.x = x;
+    this.y = y;
+  }
+
+  toString() {
+    return '(' + this.x + ', ' + this.y + ')';
+  }
+}
class Point {
+  constructor(x, y) {
+    this.x = x;
+    this.y = y;
+  }
+
+  toString() {
+    return '(' + this.x + ', ' + this.y + ')';
+  }
+}

上面代码定义了一个“类”,可以看到里面有一个constructor()方法,这就是构造方法,而this关键字则代表实例对象。这种新的 Class 写法,本质上与本章开头的 ES5 的构造函数Point是一致的。

Point类除了构造方法,还定义了一个toString()方法。注意,定义toString()方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法与方法之间不需要逗号分隔,加了会报错。

ES6 的类,完全可以看作构造函数的另一种写法。

js
class Point {
+  // ...
+}
+
+typeof Point // "function"
+Point === Point.prototype.constructor // true
class Point {
+  // ...
+}
+
+typeof Point // "function"
+Point === Point.prototype.constructor // true

上面代码表明,类的数据类型就是函数,类本身就指向构造函数。

使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。

js
class Bar {
+  doStuff() {
+    console.log('stuff');
+  }
+}
+
+const b = new Bar();
+b.doStuff() // "stuff"
class Bar {
+  doStuff() {
+    console.log('stuff');
+  }
+}
+
+const b = new Bar();
+b.doStuff() // "stuff"

构造函数的prototype属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。

js
class Point {
+  constructor() {
+    // ...
+  }
+
+  toString() {
+    // ...
+  }
+
+  toValue() {
+    // ...
+  }
+}
+
+// 等同于
+
+Point.prototype = {
+  constructor() {},
+  toString() {},
+  toValue() {},
+};
class Point {
+  constructor() {
+    // ...
+  }
+
+  toString() {
+    // ...
+  }
+
+  toValue() {
+    // ...
+  }
+}
+
+// 等同于
+
+Point.prototype = {
+  constructor() {},
+  toString() {},
+  toValue() {},
+};

上面代码中,constructor()toString()toValue()这三个方法,其实都是定义在Point.prototype上面。

实例属性新写法

ES2022 为类的实例属性,又规定了一种新写法。实例属性现在除了可以定义在constructor()方法里面的this上面,也可以定义在类内部的最顶层。

js
// 原来的写法
+class IncreasingCounter {
+  constructor() {
+    this._count = 0;
+  }
+  get value() {
+    console.log('Getting the current value!');
+    return this._count;
+  }
+  increment() {
+    this._count++;
+  }
+}
// 原来的写法
+class IncreasingCounter {
+  constructor() {
+    this._count = 0;
+  }
+  get value() {
+    console.log('Getting the current value!');
+    return this._count;
+  }
+  increment() {
+    this._count++;
+  }
+}

上面示例中,实例属性_count定义在constructor()方法里面的this上面。

现在的新写法是,这个属性也可以定义在类的最顶层,其他都不变。

js
class IncreasingCounter {
+  _count = 0;
+  get value() {
+    console.log('Getting the current value!');
+    return this._count;
+  }
+  increment() {
+    this._count++;
+  }
+}
class IncreasingCounter {
+  _count = 0;
+  get value() {
+    console.log('Getting the current value!');
+    return this._count;
+  }
+  increment() {
+    this._count++;
+  }
+}

上面代码中,实例属性_count与取值函数value()increment()方法,处于同一个层级。这时,不需要在实例属性前面加上this

注意,新写法定义的属性是实例对象自身的属性,而不是定义在实例对象的原型上面。

这种新写法的好处是,所有实例对象自身的属性都定义在类的头部,看上去比较整齐,一眼就能看出这个类有哪些实例属性。

js
class foo {
+  bar = 'hello';
+  baz = 'world';
+
+  constructor() {
+    // ...
+  }
+}
class foo {
+  bar = 'hello';
+  baz = 'world';
+
+  constructor() {
+    // ...
+  }
+}

上面的代码,一眼就能看出,foo类有两个实例属性,一目了然。另外,写起来也比较简洁。

getter和setter

与 ES5 一样,在“类”的内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

js
class MyClass {
+  constructor() {
+    // ...
+  }
+  get prop() {
+    return 'getter';
+  }
+  set prop(value) {
+    console.log('setter: '+value);
+  }
+}
+
+let inst = new MyClass();
+
+inst.prop = 123;
+// setter: 123
+
+inst.prop
+// 'getter'
class MyClass {
+  constructor() {
+    // ...
+  }
+  get prop() {
+    return 'getter';
+  }
+  set prop(value) {
+    console.log('setter: '+value);
+  }
+}
+
+let inst = new MyClass();
+
+inst.prop = 123;
+// setter: 123
+
+inst.prop
+// 'getter'

上面代码中,prop属性有对应的存值函数和取值函数,因此赋值和读取行为都被自定义了。

存值函数和取值函数是设置在属性的 Descriptor 对象上的。

js
class CustomHTMLElement {
+  constructor(element) {
+    this.element = element;
+  }
+
+  get html() {
+    return this.element.innerHTML;
+  }
+
+  set html(value) {
+    this.element.innerHTML = value;
+  }
+}
+
+var descriptor = Object.getOwnPropertyDescriptor(
+  CustomHTMLElement.prototype, "html"
+);
+
+"get" in descriptor  // true
+"set" in descriptor  // true
class CustomHTMLElement {
+  constructor(element) {
+    this.element = element;
+  }
+
+  get html() {
+    return this.element.innerHTML;
+  }
+
+  set html(value) {
+    this.element.innerHTML = value;
+  }
+}
+
+var descriptor = Object.getOwnPropertyDescriptor(
+  CustomHTMLElement.prototype, "html"
+);
+
+"get" in descriptor  // true
+"set" in descriptor  // true

上面代码中,存值函数和取值函数是定义在html属性的描述对象上面,这与 ES5 完全一致。

属性表达式

类的属性名,可以采用表达式。

js
let methodName = 'getArea';
+
+class Square {
+  constructor(length) {
+    // ...
+  }
+
+  [methodName]() {
+    // ...
+  }
+}
let methodName = 'getArea';
+
+class Square {
+  constructor(length) {
+    // ...
+  }
+
+  [methodName]() {
+    // ...
+  }
+}

上面代码中,Square类的方法名getArea,是从表达式得到的。

Class表达式

与函数一样,类也可以使用表达式的形式定义。

js
const MyClass = class Me {
+  getClassName() {
+    return Me.name;
+  }
+};
const MyClass = class Me {
+  getClassName() {
+    return Me.name;
+  }
+};

上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是Me,但是Me只在 Class 的内部可用,指代当前类。在 Class 外部,这个类只能用MyClass引用。

静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

js
class Foo {
+  static classMethod() {
+    return 'hello';
+  }
+}
+
+Foo.classMethod() // 'hello'
+
+var foo = new Foo();
+foo.classMethod()
+// TypeError: foo.classMethod is not a function
class Foo {
+  static classMethod() {
+    return 'hello';
+  }
+}
+
+Foo.classMethod() // 'hello'
+
+var foo = new Foo();
+foo.classMethod()
+// TypeError: foo.classMethod is not a function

上面代码中,Foo类的classMethod方法前有static关键字,表明该方法是一个静态方法,可以直接在Foo类上调用(Foo.classMethod()),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。

注意,如果静态方法包含this关键字,这个this指的是类,而不是实例。

静态方法可以与非静态方法重名。

父类的静态方法,可以被子类继承。

静态属性

静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。

js
class Foo {
+}
+
+Foo.prop = 1;
+Foo.prop // 1
class Foo {
+}
+
+Foo.prop = 1;
+Foo.prop // 1

上面的写法为Foo类定义了一个静态属性prop

私有方法和属性

在属性名之前使用#表示。

in运算符

Class的继承

Class 可以通过extends关键字实现继承,让子类继承父类的属性和方法。extends 的写法比 ES5 的原型链继承,要清晰和方便很多。

js
class Point {
+}
+
+class ColorPoint extends Point {
+}
class Point {
+}
+
+class ColorPoint extends Point {
+}
  • 在子类的构造函数中,只有调用super()之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,必须先完成父类的继承,只有super()方法才能让子类实例继承父类。

  • 父类所有的属性和方法,都会被子类继承,除了私有的属性和方法。

  • 父类的静态属性和静态方法,也会被子类继承。

  • super关键字,既可以当作函数使用,也可以当作对象使用。

  • 大多数浏览器的 ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。

​ (1)子类的__proto__属性,表示构造函数的继承,总是指向父类。

​ (2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

  • 子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。也就是说,子类的原型的原型,是父类的原型。

Module

CommonJS 模块就是对象,输入时必须查找对象属性。

js
// CommonJS模块
+let { stat, exists, readfile } = require('fs');
+
+// 等同于
+let _fs = require('fs');
+let stat = _fs.stat;
+let exists = _fs.exists;
+let readfile = _fs.readfile;
// CommonJS模块
+let { stat, exists, readfile } = require('fs');
+
+// 等同于
+let _fs = require('fs');
+let stat = _fs.stat;
+let exists = _fs.exists;
+let readfile = _fs.readfile;

上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。

ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

js
// ES6模块
+import { stat, exists, readFile } from 'fs';
// ES6模块
+import { stat, exists, readFile } from 'fs';

上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。

由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。

严格模式

ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";

export

模块功能主要由两个命令构成:exportimportexport命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个 JS 文件,里面使用export命令输出变量。

js
// profile.js
+export var firstName = 'Michael';
+export var lastName = 'Jackson';
+export var year = 1958;
// profile.js
+export var firstName = 'Michael';
+export var lastName = 'Jackson';
+export var year = 1958;

上面代码是profile.js文件,保存了用户信息。ES6 将其视为一个模块,里面用export命令对外部输出了三个变量。

export的写法,除了像上面这样,还有另外一种。

js
// profile.js
+var firstName = 'Michael';
+var lastName = 'Jackson';
+var year = 1958;
+
+export { firstName, lastName, year };
// profile.js
+var firstName = 'Michael';
+var lastName = 'Jackson';
+var year = 1958;
+
+export { firstName, lastName, year };

上面代码在export命令后面,使用大括号指定所要输出的一组变量。它与前一种写法(直接放置在var语句前)是等价的,但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。

export命令除了输出变量,还可以输出函数或类(class)。

js
export function multiply(x, y) {
+  return x * y;
+};
export function multiply(x, y) {
+  return x * y;
+};

上面代码对外输出一个函数multiply

通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。

js
function v1() { ... }
+function v2() { ... }
+
+export {
+  v1 as streamV1,
+  v2 as streamV2,
+  v2 as streamLatestVersion
+};
function v1() { ... }
+function v2() { ... }
+
+export {
+  v1 as streamV1,
+  v2 as streamV2,
+  v2 as streamLatestVersion
+};

上面代码使用as关键字,重命名了函数v1v2的对外接口。重命名后,v2可以用不同的名字输出两次。

需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

import

使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。

js
// main.js
+import { firstName, lastName, year } from './profile.js';
+
+function setName(element) {
+  element.textContent = firstName + ' ' + lastName;
+}
// main.js
+import { firstName, lastName, year } from './profile.js';
+
+function setName(element) {
+  element.textContent = firstName + ' ' + lastName;
+}

面代码的import命令,用于加载profile.js文件,并从中输入变量。import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。

如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。

js
import { lastName as surname } from './profile.js';
import { lastName as surname } from './profile.js';

import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。

js
import {a} from './xxx.js'
+
+a = {}; // Syntax Error : 'a' is read-only;
import {a} from './xxx.js'
+
+a = {}; // Syntax Error : 'a' is read-only;

上面代码中,脚本加载了变量a,对其重新赋值就会报错,因为a是一个只读的接口。但是,如果a是一个对象,改写a的属性是允许的。

js
import {a} from './xxx.js'
+
+a.foo = 'hello'; // 合法操作
import {a} from './xxx.js'
+
+a.foo = 'hello'; // 合法操作

上面代码中,a的属性可以成功改写,并且其他模块也可以读到改写后的值。不过,这种写法很难查错,建议凡是输入的变量,都当作完全只读,不要轻易改变它的属性。

import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径。如果不带有路径,只是一个模块名,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。

js
import { myMethod } from 'util';
import { myMethod } from 'util';

上面代码中,util是模块文件名,由于不带有路径,必须通过配置,告诉引擎怎么取到这个模块。

除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。

js
import * as circle from './circle';
+
+console.log('圆面积:' + circle.area(4));
+console.log('圆周长:' + circle.circumference(14));
import * as circle from './circle';
+
+console.log('圆面积:' + circle.area(4));
+console.log('圆周长:' + circle.circumference(14));

export default

使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。

为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

js
// export-default.js
+export default function () {
+  console.log('foo');
+}
// export-default.js
+export default function () {
+  console.log('foo');
+}

上面代码是一个模块文件export-default.js,它的默认输出是一个函数。

其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

js
// import-default.js
+import customName from './export-default';
+customName(); // 'foo'
// import-default.js
+import customName from './export-default';
+customName(); // 'foo'

上面代码的import命令,可以用任意名称指向export-default.js输出的方法,这时就不需要知道原模块输出的函数名。需要注意的是,这时import命令后面,不使用大括号。

export default命令用在非匿名函数前,也是可以的。

js
// export-default.js
+export default function foo() {
+  console.log('foo');
+}
+
+// 或者写成
+
+function foo() {
+  console.log('foo');
+}
+
+export default foo;
// export-default.js
+export default function foo() {
+  console.log('foo');
+}
+
+// 或者写成
+
+function foo() {
+  console.log('foo');
+}
+
+export default foo;

上面代码中,foo函数的函数名foo,在模块外部是无效的。加载的时候,视同匿名函数加载。

export和import复合写法

js
export { foo, bar } from 'my_module';
+
+// 可以简单理解为
+import { foo, bar } from 'my_module';
+export { foo, bar };
export { foo, bar } from 'my_module';
+
+// 可以简单理解为
+import { foo, bar } from 'my_module';
+export { foo, bar };

上面代码中,exportimport语句可以结合在一起,写成一行。但需要注意的是,写成一行以后,foobar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foobar

模块的接口改名和整体输出,也可以采用这种写法。

js
// 接口改名
+export { foo as myFoo } from 'my_module';
+
+// 整体输出
+export * from 'my_module';
// 接口改名
+export { foo as myFoo } from 'my_module';
+
+// 整体输出
+export * from 'my_module';

默认接口的写法如下。

js
export { default } from 'foo';
export { default } from 'foo';

具名接口改为默认接口的写法如下。

js
export { es6 as default } from './someModule';
+
+// 等同于
+import { es6 } from './someModule';
+export default es6;
export { es6 as default } from './someModule';
+
+// 等同于
+import { es6 } from './someModule';
+export default es6;

同样地,默认接口也可以改名为具名接口。

js
export { default as es6 } from './someModule';
export { default as es6 } from './someModule';

跨模块常量

js
// constants.js 模块
+export const A = 1;
+export const B = 3;
+export const C = 4;
+
+// test1.js 模块
+import * as constants from './constants';
+console.log(constants.A); // 1
+console.log(constants.B); // 3
+
+// test2.js 模块
+import {A, B} from './constants';
+console.log(A); // 1
+console.log(B); // 3
// constants.js 模块
+export const A = 1;
+export const B = 3;
+export const C = 4;
+
+// test1.js 模块
+import * as constants from './constants';
+console.log(constants.A); // 1
+console.log(constants.B); // 3
+
+// test2.js 模块
+import {A, B} from './constants';
+console.log(A); // 1
+console.log(B); // 3

模块的继承

circleplus模块,继承了circle模块。

js
// circleplus.js
+
+export * from 'circle';
+export var e = 2.71828182846;
+export default function(x) {
+  return Math.exp(x);
+}
// circleplus.js
+
+export * from 'circle';
+export var e = 2.71828182846;
+export default function(x) {
+  return Math.exp(x);
+}

这时,也可以将circle的属性或方法,改名后再输出。

js
// circleplus.js
+
+export { area as circleArea } from 'circle';
// circleplus.js
+
+export { area as circleArea } from 'circle';

上面代码表示,只输出circle模块的area方法,且将其改名为circleArea

加载上面模块的写法如下。

js
// main.js
+
+import * as math from 'circleplus';
+import exp from 'circleplus';
+console.log(exp(math.e));
// main.js
+
+import * as math from 'circleplus';
+import exp from 'circleplus';
+console.log(exp(math.e));

上面代码中的import exp表示,将circleplus模块的默认方法加载为exp方法。

`,514),e=[o];function c(t,r,E,y,i,d){return n(),a("div",null,e)}const h=s(p,[["render",c]]);export{u as __pageData,h as default}; diff --git a/assets/frontend_base_JavaScript.md.5b22eba4.lean.js b/assets/frontend_base_JavaScript.md.5b22eba4.lean.js new file mode 100644 index 000000000..e98c2ba1e --- /dev/null +++ b/assets/frontend_base_JavaScript.md.5b22eba4.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"JavaScript","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/base/JavaScript.md","filePath":"frontend/base/JavaScript.md","lastUpdated":1708510102000}'),p={name:"frontend/base/JavaScript.md"},o=l("",514),e=[o];function c(t,r,E,y,i,d){return n(),a("div",null,e)}const h=s(p,[["render",c]]);export{u as __pageData,h as default}; diff --git a/assets/frontend_base_Nodejs.md.a3da6ed0.js b/assets/frontend_base_Nodejs.md.a3da6ed0.js new file mode 100644 index 000000000..e508f42d3 --- /dev/null +++ b/assets/frontend_base_Nodejs.md.a3da6ed0.js @@ -0,0 +1,29 @@ +import{_ as s,o as l,c as a,Q as e}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"Node.js","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/base/Nodejs.md","filePath":"frontend/base/Nodejs.md","lastUpdated":1708510102000}'),n={name:"frontend/base/Nodejs.md"},o=e(`

Node.js

Node.js简介

Node.js是一个基于Chrome V8引擎的JavaScript运行环境。

浏览器是js的前端运行环境

Node.js是js的后端运行环境

Node.js中的JavaScript运行环境

  • V8引擎
  • 内置API
    • fs、path、http、JS、querystring。。。

Node.js可以做什么

  • 基于Express可以构建Web应用
  • 基于Electron可以构建跨平台桌面应用
  • 基于restify可以构建API接口项目
  • 读写和操作数据库、创建实用的命令行工具辅助开发。。

基础模块

fs模块

  • fs.readFile()
    • fs.readFile(path[, options], callback)
  • fs.writeFile()
    • fs.writeFile(file,data[,options], callback)
  • 动态拼接路径问题:__dirname替代当前文件所处目录
    • __dirname + '/file/1.txt'

path模块

  • path.join()
  • path.basename()

http模块

创建基本的web服务器

js
const http = require('http')
+
+const server = http.createServer()
+server.on('request', (req, res) => {
+  console.log('visit')
+  res.setHeader('Content-Type', 'text/html; charset=utf-8')
+  res.end('111')
+})
+
+server.listen(80, () => {
+  console.log("http server running at http://127.0.0.1")
+})
const http = require('http')
+
+const server = http.createServer()
+server.on('request', (req, res) => {
+  console.log('visit')
+  res.setHeader('Content-Type', 'text/html; charset=utf-8')
+  res.end('111')
+})
+
+server.listen(80, () => {
+  console.log("http server running at http://127.0.0.1")
+})
  • req
    • url:请求url
    • method:请求方法
  • res
    • end():发送响应

模块化

模块化是指解决一个复杂问题时,自顶向下逐层把系统划分为若干模块的过程,对于整个系统来说,模块是可组合、分解和更换的单元。

模块化优势:

  • 提高复用性
  • 提高可维护性
  • 可以实现按需加载

Node.js模块分类

  • 内置模块
  • 自定义模块
  • 第三方模块

加载模块

  • const moduleName = require('moduleName')

使用require加载模块时会执行被加载模块的代码

使用自定义模块可以省略.js

模块作用域

在自定义模块中定义的变量、方法等成员只能在当前模块内被访问,这种模块级别的访问限制,叫做模块作用域。

好处:防止全局变量污染

向外共享模块作用域中的成员

  • module对象:在每个.js自定义模块中都有一个module对象,里面存储了和当前模块有关的信息

    • console.log(module)
  • module.exports:可以使用此对象将模块内的成员共享出去供外部使用,外界使用require()导入自定义模块时,得到的就是module.exports所指向的对象

    • js
      module.exports.username = 'test'
      +module.exports.sayHello = function() {
      +  console.log('hello')
      +}
      module.exports.username = 'test'
      +module.exports.sayHello = function() {
      +  console.log('hello')
      +}

exports对象

exports对象是module.exports的简化写法,两者指向同一个对象

模块化规范

Node.js遵循Common.js的模块化规范

CommonJS规定

  • 每个模块内部,module代表当前模块
  • module变量是一个对象,它的exports属性是对外的接口
  • 加载某个模块,其实是加载该模块的module.exports属性。require()方法用于加载模块

npm和包

npm是Node.js的包管理工具

  • npm install

  • npm uninstall

  • npm install package

  • npm i package

  • npm i package@version

包版本的语义化规范

  • 点分十进制,总共三位数字,例如2.14.0

  • 第一位数:大版本

  • 第二位数:功能版本

  • 第三位数:bug修复版本

  • 版本号提升规则:前面版本增长,后面版本号归零

安装包后多出来的文件

  • node_modules: 存放所有已安装到项目中的包
  • package-lock.json:记录node_modules目录下每一个包的下载信息,例如包的名字、版本号、下载地址等

包管理配置文件

npm规定,项目根目录必须提供package.json的包管理配置文件。用来记录与项目有关的配置信息,例如:

  • 项目的名称、版本号、描述
  • 项目中用到了哪些包
  • 哪些包在开发期间会用到
  • 哪些包在开发和部署时都会用到

npm创建package.json命令:npm init -y

运行npm install时,npm会自动把包名、版本记录到package.json中

dependencies节点

package.json中有一个dependencies节点专门记录npm install安装了哪些包

devDependencies节点

记录只在开发阶段使用、上线不会用到的包

  • 安装到dev节点中:npm i packageName -D / npm install packageName --save-dev

修改镜像

  • npm config get registry

  • npm config set registry=https://registry.npm.taobao.org/

  • nrm工具切换镜像源

    • npm i nrm -g
    • nrm ls
    • nrm use taobao

包分类

  • 项目包:被安装到node_modules的都是项目包
    • 开发依赖包
    • 核心依赖包
  • 全局包:执行npm install时指定-g参数则会安装为全局包
    • 一般为工具性质的包
    • npm install package -g
    • npm uninstall package -g

i5ting_toc: md转换为html工具

i5ting_toc -f md -o

包结构

规范的包结构必须符合:

  • 包必须单独目录
  • 根目录必须包含package.json
  • package.json必须包含name、version、main三个属性,对应包名、版本号、包的入口

模块的加载机制

  • 优先从缓存中加载:模块在第一次被加载后会被缓存,意味着多次调用require()不会导致模块的代码被多次执行
  • 内置模块的加载优先级最高
  • require()加载自定义模块必需指定./../开头的路径标识符,否则会被当作内置模块或第三方模块,同时如果导入时省略了扩展名,Node.js会按顺序尝试加载以下文件:
    • 按确切文件名加载
    • 补全.js加载
    • 补全.json加载
    • 补全.node记载
    • 加载失败,报错
  • 如果required()的标识符不是内置模块也不是./开头的路径标识符会被当作第三方模块,会从/node_modules中加载第三方模块
    • 如果没有找到,就移动到上一层进行加载,直到文件系统根目录,找不到报错
  • 目录作为模块标识符的加载顺序
    • 在目录中找package.json的main属性做为require的加载入口
    • 然后找根目录的index.js
    • 都找不到报错
`,64),p=[o];function t(r,c,i,d,E,u){return l(),a("div",null,p)}const m=s(n,[["render",t]]);export{h as __pageData,m as default}; diff --git a/assets/frontend_base_Nodejs.md.a3da6ed0.lean.js b/assets/frontend_base_Nodejs.md.a3da6ed0.lean.js new file mode 100644 index 000000000..d61d0e7eb --- /dev/null +++ b/assets/frontend_base_Nodejs.md.a3da6ed0.lean.js @@ -0,0 +1 @@ +import{_ as s,o as l,c as a,Q as e}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"Node.js","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/base/Nodejs.md","filePath":"frontend/base/Nodejs.md","lastUpdated":1708510102000}'),n={name:"frontend/base/Nodejs.md"},o=e("",64),p=[o];function t(r,c,i,d,E,u){return l(),a("div",null,p)}const m=s(n,[["render",t]]);export{h as __pageData,m as default}; diff --git a/assets/frontend_base_Typescript.md.47bd8e1c.js b/assets/frontend_base_Typescript.md.47bd8e1c.js new file mode 100644 index 000000000..e005e3dcb --- /dev/null +++ b/assets/frontend_base_Typescript.md.47bd8e1c.js @@ -0,0 +1,1371 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const C=JSON.parse('{"title":"TypeScript","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/base/Typescript.md","filePath":"frontend/base/Typescript.md","lastUpdated":1708510102000}'),p={name:"frontend/base/Typescript.md"},o=l(`

TypeScript

语法

原始类型

  • 字符串
    • 支持模板字符串赋值
  • 布尔
  • 数字
    • 支持十进制、十六进制、二进制、八进制、NaN、Infinity
  • Null和Undefined

Any

在编程阶段还不清楚类型的变量指定的一个类型

Void

某种程度上来说,void类型像是与any类型相反,它表示没有任何类型。

Null和Undefined

TypeScript里,undefinednull两者各自有自己的类型分别叫做undefinednull。 和 void相似,它们的本身的类型用处不是很大。

默认情况下nullundefined是所有类型的子类型。 就是说你可以把 nullundefined赋值给number类型的变量。

然而,当指定了--strictNullChecks标记,nullundefined只能赋值给void和它们各自。 这能避免 很多常见的问题。

Object

  • Object:包含所有类型
  • object:表示非原始类型也就是除numberstringbooleansymbolnullundefined之外的类型。
  • {}:同new Object(),包含所有类型,但无法修改属性、赋值等

接口和对象

typescript中定义对象的方式是用interface,定义一种约束,让数据结构满足约束格式

typescript
interface Man extends Person{
+  age?:number
+  [propName:string]:any
+  readonly id:number
+}
+
+interface Person {
+  name:string
+}
+
+let p:Man = {
+  name: 'zs',
+  id: 1,
+  age: 18,
+  a:1,
+  b:2
+}
+  
+  
+interface Fn {
+  (name:string):number[]
+}
+
+const fn:Fn = function(name:string) {
+  return [1]
+}
interface Man extends Person{
+  age?:number
+  [propName:string]:any
+  readonly id:number
+}
+
+interface Person {
+  name:string
+}
+
+let p:Man = {
+  name: 'zs',
+  id: 1,
+  age: 18,
+  a:1,
+  b:2
+}
+  
+  
+interface Fn {
+  (name:string):number[]
+}
+
+const fn:Fn = function(name:string) {
+  return [1]
+}
  • 对象属性必须和interface完全一致
  • 重名的interface会被合并
  • 任意key(索引签名)
  • 可选?
  • 只读readonly
  • 定义函数类型

数组

  • 元素类型后加[],例如:number[]string[]
  • 数组范型:Array<number>
  • 定义对象数组用interface
  • 二维数组:let arr:number[][] = [[1], [2]]
  • 一把梭:any[]

函数

typescript
//返回值
+function add(a:number, b:number): number {
+  return a+b
+}
+//箭头函数和定义返回值
+const add = (a:number,b:number):number => a+b 
+
+//默认参数
+function add(a:number = 10, b:number = 20) {
+  return a+b
+}
+//可选参数
+function add(a:number = 10, b?:number): number{
+  return a+b
+}
+//传对象
+interface User {
+  name:string
+  age:nubmer
+}
+
+function add(user: User): User{
+  return user
+}
+console.log(add({ name: "111", age: 18}))
+
+//ts可以定义this的类型,在js中无法使用,必须是第一个参数定义this的类型(有点类似python里面的self?)
+interface Obj {
+  user:number[]
+  add:(this:Obj,num:number)=>void
+}
+
+let obj:Obj = {
+  user:[1,2,3],
+  add(this:Obj,num:number) {
+    this.user.push(num)
+  }
+}
+obj.add(4)
+console.log(obj)
+
+//函数重载
+let user:number[] = [1,2,3]
+
+
+function findNum():number[]
+function findNum(id:number):number[] 
+function findNum(add:number[]):number[]
+
+//跟据入参走不同逻辑
+function findNum(ids?:number | number[]):number[] {
+  if(typeof ids == 'number') {
+    return user.filter(v=> v == ids)
+  }else if (Array.isArray(ids)) {
+    user.push(...ids)
+    return user
+  }else {
+    return user
+  }
+}
+console.log(findNum())
//返回值
+function add(a:number, b:number): number {
+  return a+b
+}
+//箭头函数和定义返回值
+const add = (a:number,b:number):number => a+b 
+
+//默认参数
+function add(a:number = 10, b:number = 20) {
+  return a+b
+}
+//可选参数
+function add(a:number = 10, b?:number): number{
+  return a+b
+}
+//传对象
+interface User {
+  name:string
+  age:nubmer
+}
+
+function add(user: User): User{
+  return user
+}
+console.log(add({ name: "111", age: 18}))
+
+//ts可以定义this的类型,在js中无法使用,必须是第一个参数定义this的类型(有点类似python里面的self?)
+interface Obj {
+  user:number[]
+  add:(this:Obj,num:number)=>void
+}
+
+let obj:Obj = {
+  user:[1,2,3],
+  add(this:Obj,num:number) {
+    this.user.push(num)
+  }
+}
+obj.add(4)
+console.log(obj)
+
+//函数重载
+let user:number[] = [1,2,3]
+
+
+function findNum():number[]
+function findNum(id:number):number[] 
+function findNum(add:number[]):number[]
+
+//跟据入参走不同逻辑
+function findNum(ids?:number | number[]):number[] {
+  if(typeof ids == 'number') {
+    return user.filter(v=> v == ids)
+  }else if (Array.isArray(ids)) {
+    user.push(...ids)
+    return user
+  }else {
+    return user
+  }
+}
+console.log(findNum())

联合类型

typescript
let phone:number | string = '123456'
+
+//函数使用联合类型
+let fn = function(type:number | boolean):boolean {
+  return !!type
+}
let phone:number | string = '123456'
+
+//函数使用联合类型
+let fn = function(type:number | boolean):boolean {
+  return !!type
+}

TypeScript中的 !!是一个逻辑非(not)操作符的双重否定形式,它可以用于将一个值转换成对应的布尔值。基本上,!!可以将任何值强制转换为对应的布尔值类型。

例如,使用!!可以将下列值转换为布尔类型的值:

!!true // true

!!1 // true

!!"hello" // true

!!undefined // false

!!null // false

!!0 // false

!!"" // false

交叉类型

typescript
interface People {
+  name:string,
+  age:string
+}
+
+interface Man {
+  sex:number
+}
+
+const p = (param:People & Man):void => {
+  console.log(man)
+}
+
+p({
+  name:"ikun",
+  age:"两年半",
+  sex:1
+})
interface People {
+  name:string,
+  age:string
+}
+
+interface Man {
+  sex:number
+}
+
+const p = (param:People & Man):void => {
+  console.log(man)
+}
+
+p({
+  name:"ikun",
+  age:"两年半",
+  sex:1
+})

类型断言

尖括号写法

typescript
let someValue: any = "this is a string";
+
+let strLength: number = (<string>someValue).length;
let someValue: any = "this is a string";
+
+let strLength: number = (<string>someValue).length;

as写法

typescript
let someValue: any = "this is a string";
+
+let strLength: number = (someValue as string).length;
let someValue: any = "this is a string";
+
+let strLength: number = (someValue as string).length;

使用JSX时只允许as写法

内置对象

  • Number(1)
  • Date()
  • RegExp(/\\w/)
  • Error('wrong')
  • XMLHttpRequest
  • HTML(元素名称)Element / HTMLElement / Element
  • NodeList / NodeListOf<HTMLDivElement | HTMLElement>
  • Storage
  • Location
  • Promise
  • ...

Class

typescript
//class的基本用法 继承 和类型约束 implements
+interface Options {
+  el: string | HTMLElement;
+}
+interface VueClass {
+  options: Options;
+  init(): void;
+}
+interface Vnode {
+  tag: string;
+  text: string;
+  children?: Vnode[];
+}
+//虚拟dom
+class Dom {
+  //创建dom节点
+  createElement(el: string) {
+    return document.createElement(el);
+  }
+  //填充文本
+  setText(el: HTMLElement, text: string | null) {
+    el.textContent = text;
+  }
+  //渲染函数
+  render(data: Vnode) {
+    let root = this.createElement(data.tag);
+    if (data.children && Array.isArray(data.children)) {
+      data.children.forEach((item) => {
+        let child = this.render(item);
+        root.appendChild(child);
+      });
+    } else {
+      this.setText(root, data.text === undefined ? "" : data.text);
+    }
+    return root;
+  }
+}
+
+class Vue extends Dom implements VueClass {
+  options: Options;
+  constructor(options: Options) {
+    super();
+    this.options = options;
+    this.init();
+  }
+  init(): void {
+    let data: Vnode = {
+      tag: "div",
+      text: '111',
+      children: [
+        {
+          tag: "section",
+          text: "子节点1",
+        },
+        {
+          tag: "section",
+          text: "子节点2",
+        },
+        {
+          tag: "section",
+          text: "子节点3",
+        }
+      ],
+    };
+    let app =
+      typeof this.options.el == "string"
+        ? document.querySelector(this.options.el)
+        : this.options.el;
+    app?.appendChild(this.render(data));
+  }
+}
+
+new Vue({
+  el: "#app"
+});
//class的基本用法 继承 和类型约束 implements
+interface Options {
+  el: string | HTMLElement;
+}
+interface VueClass {
+  options: Options;
+  init(): void;
+}
+interface Vnode {
+  tag: string;
+  text: string;
+  children?: Vnode[];
+}
+//虚拟dom
+class Dom {
+  //创建dom节点
+  createElement(el: string) {
+    return document.createElement(el);
+  }
+  //填充文本
+  setText(el: HTMLElement, text: string | null) {
+    el.textContent = text;
+  }
+  //渲染函数
+  render(data: Vnode) {
+    let root = this.createElement(data.tag);
+    if (data.children && Array.isArray(data.children)) {
+      data.children.forEach((item) => {
+        let child = this.render(item);
+        root.appendChild(child);
+      });
+    } else {
+      this.setText(root, data.text === undefined ? "" : data.text);
+    }
+    return root;
+  }
+}
+
+class Vue extends Dom implements VueClass {
+  options: Options;
+  constructor(options: Options) {
+    super();
+    this.options = options;
+    this.init();
+  }
+  init(): void {
+    let data: Vnode = {
+      tag: "div",
+      text: '111',
+      children: [
+        {
+          tag: "section",
+          text: "子节点1",
+        },
+        {
+          tag: "section",
+          text: "子节点2",
+        },
+        {
+          tag: "section",
+          text: "子节点3",
+        }
+      ],
+    };
+    let app =
+      typeof this.options.el == "string"
+        ? document.querySelector(this.options.el)
+        : this.options.el;
+    app?.appendChild(this.render(data));
+  }
+}
+
+new Vue({
+  el: "#app"
+});
  • readonly
  • private
  • protected
  • public
  • super()
  • 静态方法 static
  • get set

抽象类 & 抽象方法

  • abstract className
  • abstract functionName

元组Tuple

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。

typescript
// Declare a tuple type
+let x: [string, number];
+// Initialize it
+x = ['hello', 10]; // OK
+// Initialize it incorrectly
+x = [10, 'hello']; // Error
// Declare a tuple type
+let x: [string, number];
+// Initialize it
+x = ['hello', 10]; // OK
+// Initialize it incorrectly
+x = [10, 'hello']; // Error

当访问一个已知索引的元素,会得到正确的类型:

ts
console.log(x[0].substr(1)); // OK
+console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'
console.log(x[0].substr(1)); // OK
+console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'

当访问一个越界的元素,会使用联合类型替代:

ts
x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型
+
+console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString
+
+x[6] = true; // Error, 布尔不是(string | number)类型
x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型
+
+console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString
+
+x[6] = true; // Error, 布尔不是(string | number)类型

枚举

enum类型是对JavaScript标准数据类型的一个补充。

ts
enum Color {Red, Green, Blue}
+let c: Color = Color.Green;
enum Color {Red, Green, Blue}
+let c: Color = Color.Green;

默认情况下,从0开始为元素编号。 你也可以手动的指定成员的数值。 例如,我们将上面的例子改成从 1开始编号:

ts
enum Color {Red = 1, Green, Blue}
+let c: Color = Color.Green;
enum Color {Red = 1, Green, Blue}
+let c: Color = Color.Green;

或者,全部都采用手动赋值:

ts
enum Color {Red = 1, Green = 2, Blue = 4}
+let c: Color = Color.Green;
enum Color {Red = 1, Green = 2, Blue = 4}
+let c: Color = Color.Green;

枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字:

ts
enum Color {Red = 1, Green, Blue}
+let colorName: string = Color[2];
+
+console.log(colorName);  // 显示'Green'因为上面代码里它的值是2
enum Color {Red = 1, Green, Blue}
+let colorName: string = Color[2];
+
+console.log(colorName);  // 显示'Green'因为上面代码里它的值是2

类型推断

TypeScript里,在有些没有明确指出类型的地方,类型推论会帮助提供类型。

如果没有指出类型 & 没赋值 会被推断成any类型。

类型别名

typescript
type s = string | null
+
+let str:s = 'test'
+
+let str1 = '123'
+type s1 = typeof str1
+
+
+type num = 1 extends number ? 1 : 0
type s = string | null
+
+let str:s = 'test'
+
+let str1 = '123'
+type s1 = typeof str1
+
+
+type num = 1 extends number ? 1 : 0

image-20230419001413539

Never

never类型表示的是那些永不存在的值的类型。 例如, never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是 never类型,当它们被永不为真的类型保护所约束时。

never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使 any也不可以赋值给never

typescript
type A = string & number //never
+type A = void | number | never //never会被忽略掉
+
+
+type A = '唱' | '跳' | 'rap'
+
+function kun(value: A) {
+    switch (value) {
+        case '唱':
+            break;
+        case '跳':
+            break;
+        case 'rap':
+            break;
+        default:
+            const check: never = value;
+            break;
+    }
+}
type A = string & number //never
+type A = void | number | never //never会被忽略掉
+
+
+type A = '唱' | '跳' | 'rap'
+
+function kun(value: A) {
+    switch (value) {
+        case '唱':
+            break;
+        case '跳':
+            break;
+        case 'rap':
+            break;
+        default:
+            const check: never = value;
+            break;
+    }
+}

Symbol

typescript
let a1:symbol = Symbol(1)
+let a2:symbol = Symbol(2)
+console.log(a1 === a2) // false
+
+//for Symbol 有没有注册过这个key 如果有直接用 没有就创建
+console.log(Symbol.for('1') === Symbol.for('1')) // true
let a1:symbol = Symbol(1)
+let a2:symbol = Symbol(2)
+console.log(a1 === a2) // false
+
+//for Symbol 有没有注册过这个key 如果有直接用 没有就创建
+console.log(Symbol.for('1') === Symbol.for('1')) // true
  • 可以用来避免属性被覆盖

生成器 迭代器

typescript
function* gen() {
+  yield Promise.resovle('111')
+  yield '1'
+  yield '2'
+}
+const g = gen()
+console.log(g.next())
+console.log(g.next())
+console.log(g.next())
+console.log(g.next())
+/*
+{ value: Promise { '111' }, done: false }
+{ value: '1', done: false }
+{ value: '2', done: false }
+{ value: undefined, done: true }
+*/
function* gen() {
+  yield Promise.resovle('111')
+  yield '1'
+  yield '2'
+}
+const g = gen()
+console.log(g.next())
+console.log(g.next())
+console.log(g.next())
+console.log(g.next())
+/*
+{ value: Promise { '111' }, done: false }
+{ value: '1', done: false }
+{ value: '2', done: false }
+{ value: undefined, done: true }
+*/
typescript
let Set:Set<number> = new Set([1,1,2,3,3,3]) // 1 2 3
+
+let map:Map<any, any> = new Map()
+let arr = [1,2,3]
+map.set(arr, '123')
+
+function args(){
+  console.log(arguments) //伪数组 IArguments
+}
+
+const each = (value:any) => {
+  let It: any = value[Symbol.iterator]()
+  let next: any = { done: false }
+  while (!next.done) {
+    next = It.next()
+    if (!next.done) {
+      console.log(next.value)
+    }
+  }
+}
+each([1,2,3])
+/*
+1
+2
+3
+*/
+
+
+//迭代器语法糖 for of
+//对象不能用 for of语法
+for (let value of map) {
+  console.log(value)
+}
+
+//数组解构 底层原理也是调用iterator
+let a = [4,5,6]
+let copy = [...a]
+console.log(a)
+
+let obj = {
+  max:5,
+  current:0,
+  [Symbol.iterator]() {
+    return {
+      max: this.max,
+      current: this.current,
+      next() {
+        if (this.current == this.max) {
+          return {
+            value: undefined,
+            done:true
+          }
+        }else {
+          return {
+            value: this.current++,
+            done:false
+          }
+        }
+      }
+    }
+  }
+}
+
+for (let value of obj) {
+  console.log(value)
+}
+/*
+0
+1
+2
+3
+4
+*/
let Set:Set<number> = new Set([1,1,2,3,3,3]) // 1 2 3
+
+let map:Map<any, any> = new Map()
+let arr = [1,2,3]
+map.set(arr, '123')
+
+function args(){
+  console.log(arguments) //伪数组 IArguments
+}
+
+const each = (value:any) => {
+  let It: any = value[Symbol.iterator]()
+  let next: any = { done: false }
+  while (!next.done) {
+    next = It.next()
+    if (!next.done) {
+      console.log(next.value)
+    }
+  }
+}
+each([1,2,3])
+/*
+1
+2
+3
+*/
+
+
+//迭代器语法糖 for of
+//对象不能用 for of语法
+for (let value of map) {
+  console.log(value)
+}
+
+//数组解构 底层原理也是调用iterator
+let a = [4,5,6]
+let copy = [...a]
+console.log(a)
+
+let obj = {
+  max:5,
+  current:0,
+  [Symbol.iterator]() {
+    return {
+      max: this.max,
+      current: this.current,
+      next() {
+        if (this.current == this.max) {
+          return {
+            value: undefined,
+            done:true
+          }
+        }else {
+          return {
+            value: this.current++,
+            done:false
+          }
+        }
+      }
+    }
+  }
+}
+
+for (let value of obj) {
+  console.log(value)
+}
+/*
+0
+1
+2
+3
+4
+*/

泛型

typescript
function fun<T>(a:T, b:T):Array<T> {
+	return [a,b]
+}
+
+type A<T> = string | number | T
+let a:A<boolean> = true
+
+interface Date<T> {
+  msg:T
+}
+let data:Date<number> = {
+  msg:1
+}
+
+function add<T = number,K = number>(a:T,b:K):Array<T | K> {
+  return [a,b]
+}
+add(false, '1')
+
+const axios = {
+  get<T>(url:string) {
+    return new Promise<T>((resolve,reject)=>{
+      let xhr:XMLHttpRequest = new XMLHttpRequest()
+      xhr.open('GET',url)
+      xhr.onreadystatechange = () => {
+        if(xhr.readyState ==4 && xhr.status == 200) {
+          resolve(JSON.parse(xhr.responseText))
+        }
+      }
+      xhr.send(null)
+    })
+  }
+}
function fun<T>(a:T, b:T):Array<T> {
+	return [a,b]
+}
+
+type A<T> = string | number | T
+let a:A<boolean> = true
+
+interface Date<T> {
+  msg:T
+}
+let data:Date<number> = {
+  msg:1
+}
+
+function add<T = number,K = number>(a:T,b:K):Array<T | K> {
+  return [a,b]
+}
+add(false, '1')
+
+const axios = {
+  get<T>(url:string) {
+    return new Promise<T>((resolve,reject)=>{
+      let xhr:XMLHttpRequest = new XMLHttpRequest()
+      xhr.open('GET',url)
+      xhr.onreadystatechange = () => {
+        if(xhr.readyState ==4 && xhr.status == 200) {
+          resolve(JSON.parse(xhr.responseText))
+        }
+      }
+      xhr.send(null)
+    })
+  }
+}

泛型约束

typescript
// extends
+interface Len {
+  length:number
+}
+
+function func<T extends Len)(a:T) {
+  console.log(a.length)
+}
+
+let obj = {
+  name: 'test',
+  sex: 1
+}
+
+// 约束对象的key
+type key = keyof typeof obj // "name" | "sex"
+
+function ob<T extends object, K extends keyof T>(obj:T, key:K) {
+  
+}
+
+
+interface Data {
+  name:string
+  age:number
+  sex:string
+}
+
+type Options<T extends object> = {
+  //readonly [Key in keyof T]?:T[Key]
+  [Key in keyof T]?:T[Key]
+}
+
+type B = Options<Data>
+/*
+type B = {
+    name?: string | undefined;
+    age?: number | undefined;
+    sex?: string | undefined;
+}
+type B = {
+    name?: string | undefined;
+    age?: number | undefined;
+    sex?: string | undefined;
+}
+*/
// extends
+interface Len {
+  length:number
+}
+
+function func<T extends Len)(a:T) {
+  console.log(a.length)
+}
+
+let obj = {
+  name: 'test',
+  sex: 1
+}
+
+// 约束对象的key
+type key = keyof typeof obj // "name" | "sex"
+
+function ob<T extends object, K extends keyof T>(obj:T, key:K) {
+  
+}
+
+
+interface Data {
+  name:string
+  age:number
+  sex:string
+}
+
+type Options<T extends object> = {
+  //readonly [Key in keyof T]?:T[Key]
+  [Key in keyof T]?:T[Key]
+}
+
+type B = Options<Data>
+/*
+type B = {
+    name?: string | undefined;
+    age?: number | undefined;
+    sex?: string | undefined;
+}
+type B = {
+    name?: string | undefined;
+    age?: number | undefined;
+    sex?: string | undefined;
+}
+*/

tsconfig.json

通过tsc --init生成

json
"compilerOptions": {
+  "incremental": true, // TS编译器在第一次编译之后会生成一个存储编译信息的文件,第二次编译会在第一次的基础上进行增量编译,可以提高编译的速度
+  "tsBuildInfoFile": "./buildFile", // 增量编译文件的存储位置
+  "diagnostics": true, // 打印诊断信息 
+  "target": "ES5", // 目标语言的版本
+  "module": "CommonJS", // 生成代码的模板标准
+  "outFile": "./app.js", // 将多个相互依赖的文件生成一个文件,可以用在AMD模块中,即开启时应设置"module": "AMD",
+  "lib": ["DOM", "ES2015", "ScriptHost", "ES2019.Array"], // TS需要引用的库,即声明文件,es5 默认引用dom、es5、scripthost,如需要使用es的高级版本特性,通常都需要配置,如es8的数组新特性需要引入"ES2019.Array",
+  "allowJS": true, // 允许编译器编译JS,JSX文件
+  "checkJs": true, // 允许在JS文件中报错,通常与allowJS一起使用
+  "outDir": "./dist", // 指定输出目录
+  "rootDir": "./", // 指定输出文件目录(用于输出),用于控制输出目录结构
+  "declaration": true, // 生成声明文件,开启后会自动生成声明文件
+  "declarationDir": "./file", // 指定生成声明文件存放目录
+  "emitDeclarationOnly": true, // 只生成声明文件,而不会生成js文件
+  "sourceMap": true, // 生成目标文件的sourceMap文件
+  "inlineSourceMap": true, // 生成目标文件的inline SourceMap,inline SourceMap会包含在生成的js文件中
+  "declarationMap": true, // 为声明文件生成sourceMap
+  "typeRoots": [], // 声明文件目录,默认时node_modules/@types
+  "types": [], // 加载的声明文件包
+  "removeComments":true, // 删除注释 
+  "noEmit": true, // 不输出文件,即编译后不会生成任何js文件
+  "noEmitOnError": true, // 发送错误时不输出任何文件
+  "noEmitHelpers": true, // 不生成helper函数,减小体积,需要额外安装,常配合importHelpers一起使用
+  "importHelpers": true, // 通过tslib引入helper函数,文件必须是模块
+  "downlevelIteration": true, // 降级遍历器实现,如果目标源是es3/5,那么遍历器会有降级的实现
+  "strict": true, // 开启所有严格的类型检查
+  "alwaysStrict": true, // 在代码中注入'use strict'
+  "noImplicitAny": true, // 不允许隐式的any类型
+  "strictNullChecks": true, // 不允许把null、undefined赋值给其他类型的变量
+  "strictFunctionTypes": true, // 不允许函数参数双向协变
+  "strictPropertyInitialization": true, // 类的实例属性必须初始化
+  "strictBindCallApply": true, // 严格的bind/call/apply检查
+  "noImplicitThis": true, // 不允许this有隐式的any类型
+  "noUnusedLocals": true, // 检查只声明、未使用的局部变量(只提示不报错)
+  "noUnusedParameters": true, // 检查未使用的函数参数(只提示不报错)
+  "noFallthroughCasesInSwitch": true, // 防止switch语句贯穿(即如果没有break语句后面不会执行)
+  "noImplicitReturns": true, //每个分支都会有返回值
+  "esModuleInterop": true, // 允许export=导出,由import from 导入
+  "allowUmdGlobalAccess": true, // 允许在模块中全局变量的方式访问umd模块
+  "moduleResolution": "node", // 模块解析策略,ts默认用node的解析策略,即相对的方式导入
+  "baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
+  "paths": { // 路径映射,相对于baseUrl
+    // 如使用jq时不想使用默认版本,而需要手动指定版本,可进行如下配置
+    "jquery": ["node_modules/jquery/dist/jquery.min.js"]
+  },
+  "rootDirs": ["src","out"], // 将多个目录放在一个虚拟目录下,用于运行时,即编译后引入文件的位置可能发生变化,这也设置可以虚拟src和out在同一个目录下,不用再去改变路径也不会报错
+  "listEmittedFiles": true, // 打印输出文件
+  "listFiles": true// 打印编译的文件(包括引用的声明文件)
+}
+ 
+// 指定一个匹配列表(属于自动指定该路径下的所有ts相关文件)
+"include": [
+   "src/**/*"
+],
+// 指定一个排除列表(include的反向操作)
+ "exclude": [
+   "demo.ts"
+],
+// 指定哪些文件使用该配置(属于手动一个个指定文件)
+ "files": [
+   "demo.ts"
+]
"compilerOptions": {
+  "incremental": true, // TS编译器在第一次编译之后会生成一个存储编译信息的文件,第二次编译会在第一次的基础上进行增量编译,可以提高编译的速度
+  "tsBuildInfoFile": "./buildFile", // 增量编译文件的存储位置
+  "diagnostics": true, // 打印诊断信息 
+  "target": "ES5", // 目标语言的版本
+  "module": "CommonJS", // 生成代码的模板标准
+  "outFile": "./app.js", // 将多个相互依赖的文件生成一个文件,可以用在AMD模块中,即开启时应设置"module": "AMD",
+  "lib": ["DOM", "ES2015", "ScriptHost", "ES2019.Array"], // TS需要引用的库,即声明文件,es5 默认引用dom、es5、scripthost,如需要使用es的高级版本特性,通常都需要配置,如es8的数组新特性需要引入"ES2019.Array",
+  "allowJS": true, // 允许编译器编译JS,JSX文件
+  "checkJs": true, // 允许在JS文件中报错,通常与allowJS一起使用
+  "outDir": "./dist", // 指定输出目录
+  "rootDir": "./", // 指定输出文件目录(用于输出),用于控制输出目录结构
+  "declaration": true, // 生成声明文件,开启后会自动生成声明文件
+  "declarationDir": "./file", // 指定生成声明文件存放目录
+  "emitDeclarationOnly": true, // 只生成声明文件,而不会生成js文件
+  "sourceMap": true, // 生成目标文件的sourceMap文件
+  "inlineSourceMap": true, // 生成目标文件的inline SourceMap,inline SourceMap会包含在生成的js文件中
+  "declarationMap": true, // 为声明文件生成sourceMap
+  "typeRoots": [], // 声明文件目录,默认时node_modules/@types
+  "types": [], // 加载的声明文件包
+  "removeComments":true, // 删除注释 
+  "noEmit": true, // 不输出文件,即编译后不会生成任何js文件
+  "noEmitOnError": true, // 发送错误时不输出任何文件
+  "noEmitHelpers": true, // 不生成helper函数,减小体积,需要额外安装,常配合importHelpers一起使用
+  "importHelpers": true, // 通过tslib引入helper函数,文件必须是模块
+  "downlevelIteration": true, // 降级遍历器实现,如果目标源是es3/5,那么遍历器会有降级的实现
+  "strict": true, // 开启所有严格的类型检查
+  "alwaysStrict": true, // 在代码中注入'use strict'
+  "noImplicitAny": true, // 不允许隐式的any类型
+  "strictNullChecks": true, // 不允许把null、undefined赋值给其他类型的变量
+  "strictFunctionTypes": true, // 不允许函数参数双向协变
+  "strictPropertyInitialization": true, // 类的实例属性必须初始化
+  "strictBindCallApply": true, // 严格的bind/call/apply检查
+  "noImplicitThis": true, // 不允许this有隐式的any类型
+  "noUnusedLocals": true, // 检查只声明、未使用的局部变量(只提示不报错)
+  "noUnusedParameters": true, // 检查未使用的函数参数(只提示不报错)
+  "noFallthroughCasesInSwitch": true, // 防止switch语句贯穿(即如果没有break语句后面不会执行)
+  "noImplicitReturns": true, //每个分支都会有返回值
+  "esModuleInterop": true, // 允许export=导出,由import from 导入
+  "allowUmdGlobalAccess": true, // 允许在模块中全局变量的方式访问umd模块
+  "moduleResolution": "node", // 模块解析策略,ts默认用node的解析策略,即相对的方式导入
+  "baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
+  "paths": { // 路径映射,相对于baseUrl
+    // 如使用jq时不想使用默认版本,而需要手动指定版本,可进行如下配置
+    "jquery": ["node_modules/jquery/dist/jquery.min.js"]
+  },
+  "rootDirs": ["src","out"], // 将多个目录放在一个虚拟目录下,用于运行时,即编译后引入文件的位置可能发生变化,这也设置可以虚拟src和out在同一个目录下,不用再去改变路径也不会报错
+  "listEmittedFiles": true, // 打印输出文件
+  "listFiles": true// 打印编译的文件(包括引用的声明文件)
+}
+ 
+// 指定一个匹配列表(属于自动指定该路径下的所有ts相关文件)
+"include": [
+   "src/**/*"
+],
+// 指定一个排除列表(include的反向操作)
+ "exclude": [
+   "demo.ts"
+],
+// 指定哪些文件使用该配置(属于手动一个个指定文件)
+ "files": [
+   "demo.ts"
+]

namespace

typescript提供了namespace避免全局变量污染的问题。

任何包含顶级import或export的文件都被当作一个模块。相反的,如果不带,那么它的内容被视为全局可见的。

  • 命名空间在ts1.5之前叫内部模块外部模块现在简称为模块。
  • 命名空间内的类默认私有
  • 通过export暴露
  • 通过namespace关键字定义
typescript
namespace A {
+  export const a = 1
+}
+// 实现:
+"use strict"
+var A;
+(function (A) {
+  A.a = 1;
+})(A || A = {});
+
+// 嵌套命名空间
+namespace A {
+  export namespace C {
+    export const D = 5;
+  }
+}
+
+console.log(A.C.D)
+
+//抽离命名空间
+export namespace V {
+  export const a = 1
+}
+
+import {V} from '../index'
+console.log(V)// {a:1}
+
+
+//简化命名空间
+namespace A {
+  export namespace C {
+    export const D = 5;
+  }
+}
+
+import a = A.C
+console.log(a.D)
+
+//命名空间合并
+namespace A {
+  export const b = 2
+}
+namespace A {
+	export const a = 1
+}
+//等价于
+namespace A {
+  export const b = 2
+  export const a = 1
+}
namespace A {
+  export const a = 1
+}
+// 实现:
+"use strict"
+var A;
+(function (A) {
+  A.a = 1;
+})(A || A = {});
+
+// 嵌套命名空间
+namespace A {
+  export namespace C {
+    export const D = 5;
+  }
+}
+
+console.log(A.C.D)
+
+//抽离命名空间
+export namespace V {
+  export const a = 1
+}
+
+import {V} from '../index'
+console.log(V)// {a:1}
+
+
+//简化命名空间
+namespace A {
+  export namespace C {
+    export const D = 5;
+  }
+}
+
+import a = A.C
+console.log(a.D)
+
+//命名空间合并
+namespace A {
+  export const b = 2
+}
+namespace A {
+	export const a = 1
+}
+//等价于
+namespace A {
+  export const b = 2
+  export const a = 1
+}

三斜线指令

三斜线指令是包含单个XML标签的单行注释。 注释的内容会做为编译器指令使用。

typescript
/// <reference path="..." />
+/// <reference path="..." />指令是三斜线指令中最常见的一种。 它用于声明文件间的 依赖。
/// <reference path="..." />
+/// <reference path="..." />指令是三斜线指令中最常见的一种。 它用于声明文件间的 依赖。

三斜线引用告诉编译器在编译过程中要引入的额外的文件。

typescript
/// <reference types="..." />
+/// <reference path="..." />指令相似,这个指令是用来声明 依赖的; 一个 /// <reference types="..." />指令则声明了对某个包的依赖。
+
+对这些包的名字的解析与在 import语句里对模块名的解析类似。 可以简单地把三斜线类型引用指令当做 import声明的包。
+
+例如,把 /// <reference types="node" />引入到声明文件,表明这个文件使用了 @types/node/index.d.ts里面声明的名字; 并且,这个包需要在编译阶段与声明文件一起被包含进来。
+
+仅当在你需要写一个d.ts文件时才使用这个指令。
+
+对于那些在编译阶段生成的声明文件,编译器会自动地添加/// <reference types="..." />; 当且仅当结果文件中使用了引用的包里的声明时才会在生成的声明文件里添加/// <reference types="..." />语句。
+
+若要在.ts文件里声明一个对@types包的依赖,使用--types命令行选项或在tsconfig.json里指定。
/// <reference types="..." />
+/// <reference path="..." />指令相似,这个指令是用来声明 依赖的; 一个 /// <reference types="..." />指令则声明了对某个包的依赖。
+
+对这些包的名字的解析与在 import语句里对模块名的解析类似。 可以简单地把三斜线类型引用指令当做 import声明的包。
+
+例如,把 /// <reference types="node" />引入到声明文件,表明这个文件使用了 @types/node/index.d.ts里面声明的名字; 并且,这个包需要在编译阶段与声明文件一起被包含进来。
+
+仅当在你需要写一个d.ts文件时才使用这个指令。
+
+对于那些在编译阶段生成的声明文件,编译器会自动地添加/// <reference types="..." />; 当且仅当结果文件中使用了引用的包里的声明时才会在生成的声明文件里添加/// <reference types="..." />语句。
+
+若要在.ts文件里声明一个对@types包的依赖,使用--types命令行选项或在tsconfig.json里指定。

声明文件

使用第三方库时需要引用它的声明文件d.ts才能获得对应的代码补全、接口提示等功能

typescript
npm i @types/xxx
npm i @types/xxx

Mixins混入

除了传统的面向对象继承方式,还流行一种通过可重用组件创建类的方式,就是联合另一个简单类的代码。

typescript
// Disposable Mixin
+class Disposable {
+    isDisposed: boolean;
+    dispose() {
+        this.isDisposed = true;
+    }
+
+}
+
+// Activatable Mixin
+class Activatable {
+    isActive: boolean;
+    activate() {
+        this.isActive = true;
+    }
+    deactivate() {
+        this.isActive = false;
+    }
+}
+
+class SmartObject implements Disposable, Activatable {
+    constructor() {
+        setInterval(() => console.log(this.isActive + " : " + this.isDisposed), 500);
+    }
+
+    interact() {
+        this.activate();
+    }
+
+    // Disposable
+    isDisposed: boolean = false;
+    dispose: () => void;
+    // Activatable
+    isActive: boolean = false;
+    activate: () => void;
+    deactivate: () => void;
+}
+applyMixins(SmartObject, [Disposable, Activatable]);
+
+let smartObj = new SmartObject();
+setTimeout(() => smartObj.interact(), 1000);
+
+////////////////////////////////////////
+// In your runtime library somewhere
+////////////////////////////////////////
+
+function applyMixins(derivedCtor: any, baseCtors: any[]) {
+    baseCtors.forEach(baseCtor => {
+        Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
+            derivedCtor.prototype[name] = baseCtor.prototype[name];
+        });
+    });
+}
// Disposable Mixin
+class Disposable {
+    isDisposed: boolean;
+    dispose() {
+        this.isDisposed = true;
+    }
+
+}
+
+// Activatable Mixin
+class Activatable {
+    isActive: boolean;
+    activate() {
+        this.isActive = true;
+    }
+    deactivate() {
+        this.isActive = false;
+    }
+}
+
+class SmartObject implements Disposable, Activatable {
+    constructor() {
+        setInterval(() => console.log(this.isActive + " : " + this.isDisposed), 500);
+    }
+
+    interact() {
+        this.activate();
+    }
+
+    // Disposable
+    isDisposed: boolean = false;
+    dispose: () => void;
+    // Activatable
+    isActive: boolean = false;
+    activate: () => void;
+    deactivate: () => void;
+}
+applyMixins(SmartObject, [Disposable, Activatable]);
+
+let smartObj = new SmartObject();
+setTimeout(() => smartObj.interact(), 1000);
+
+////////////////////////////////////////
+// In your runtime library somewhere
+////////////////////////////////////////
+
+function applyMixins(derivedCtor: any, baseCtors: any[]) {
+    baseCtors.forEach(baseCtor => {
+        Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
+            derivedCtor.prototype[name] = baseCtor.prototype[name];
+        });
+    });
+}

装饰器Decorator

随着TypeScript和ES6里引入了类,在一些场景下我们需要额外的特性来支持标注或修改类及其成员。 装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。

若要启用实验性的装饰器特性,你必须在命令行或tsconfig.json里启用experimentalDecorators编译器选项:

命令行:

shell
tsc --target ES5 --experimentalDecorators
tsc --target ES5 --experimentalDecorators

tsconfig.json:

json
{
+    "compilerOptions": {
+        "target": "ES5",
+        "experimentalDecorators": true
+    }
+}
{
+    "compilerOptions": {
+        "target": "ES5",
+        "experimentalDecorators": true
+    }
+}

装饰器是一种特殊类型的声明,它能够被附加到类声明方法访问符属性参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

类装饰器

typescript
const IKun: ClassDecorator = (target) => {
+    console.log(target);
+    target.prototype.name = 'ikun';
+    target.prototype.slogan = () => {
+        console.log('鸡你太美');
+    }
+}
+
+@IKun
+class Person {
+
+}
+
+const person = new Person() as any;
+person.slogan(); // 鸡你太美
const IKun: ClassDecorator = (target) => {
+    console.log(target);
+    target.prototype.name = 'ikun';
+    target.prototype.slogan = () => {
+        console.log('鸡你太美');
+    }
+}
+
+@IKun
+class Person {
+
+}
+
+const person = new Person() as any;
+person.slogan(); // 鸡你太美

装饰器工厂

typescript
const IKun = (name: string) => {
+    const decorator: ClassDecorator = (target) => {
+        target.prototype.name = name;
+        target.prototype.slogan = () => {
+            console.log('鸡你太美');
+        }
+    }
+    return decorator
+
+}
+
+@IKun('小黑子')
+class Person {
+
+}
+
+const person = new Person() as any;
+console.log(person.name); // 小黑子
+person.slogan(); // 鸡你太美
const IKun = (name: string) => {
+    const decorator: ClassDecorator = (target) => {
+        target.prototype.name = name;
+        target.prototype.slogan = () => {
+            console.log('鸡你太美');
+        }
+    }
+    return decorator
+
+}
+
+@IKun('小黑子')
+class Person {
+
+}
+
+const person = new Person() as any;
+console.log(person.name); // 小黑子
+person.slogan(); // 鸡你太美

方法装饰器

方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。 方法装饰器不能用在声明文件( .d.ts),重载或者任何外部上下文(比如declare的类)中。

方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 成员的属性描述符

如果方法装饰器返回一个值,它会被用作方法的属性描述符

typescript
const logResult = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
+  const fn = descriptor.value
+  descriptor.value = function(...rest) {
+    // 使用新的方法来替换原有方法,输出 方法名称 + 输入的参数 实现日志的增强功能
+    const result = fn.apply(this, rest)
+    console.log(propertyKey + ':' + result)
+    return result
+  }
+}
+
+class Person {
+    name: string = ''
+    age: number = 0
+
+    constructor(name: string, age: number) {
+        this.name = name
+        this.age = age
+    }
+
+    @logResult
+    getName() {
+      return this.name
+    }
+
+    @logResult
+    getAge() {
+      return this.age
+    }
+}
+
+const p = new Person('张三', 18)
+p.getName() // getName:张三
+p.getAge() // getAge:18
const logResult = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
+  const fn = descriptor.value
+  descriptor.value = function(...rest) {
+    // 使用新的方法来替换原有方法,输出 方法名称 + 输入的参数 实现日志的增强功能
+    const result = fn.apply(this, rest)
+    console.log(propertyKey + ':' + result)
+    return result
+  }
+}
+
+class Person {
+    name: string = ''
+    age: number = 0
+
+    constructor(name: string, age: number) {
+        this.name = name
+        this.age = age
+    }
+
+    @logResult
+    getName() {
+      return this.name
+    }
+
+    @logResult
+    getAge() {
+      return this.age
+    }
+}
+
+const p = new Person('张三', 18)
+p.getName() // getName:张三
+p.getAge() // getAge:18
typescript
import "reflect-metadata";
+
+const formatMetadataKey = Symbol("format");
+
+function format(formatString: string) {
+    return Reflect.metadata(formatMetadataKey, formatString);
+}
+
+function getFormat(target: any, propertyKey: string) {
+    return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
+}
+
+class Greeter {
+    @format("Hello, %s")
+    greeting: string;
+
+    constructor(message: string) {
+        this.greeting = message;
+    }
+
+    greet() {
+        let formatString = getFormat(this, "greeting");
+        return formatString.replace("%s", this.greeting);
+    }
+}
+
+console.log(new Greeter("world").greet()); // "Hello, world"
import "reflect-metadata";
+
+const formatMetadataKey = Symbol("format");
+
+function format(formatString: string) {
+    return Reflect.metadata(formatMetadataKey, formatString);
+}
+
+function getFormat(target: any, propertyKey: string) {
+    return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
+}
+
+class Greeter {
+    @format("Hello, %s")
+    greeting: string;
+
+    constructor(message: string) {
+        this.greeting = message;
+    }
+
+    greet() {
+        let formatString = getFormat(this, "greeting");
+        return formatString.replace("%s", this.greeting);
+    }
+}
+
+console.log(new Greeter("world").greet()); // "Hello, world"

参数装饰器

参数装饰器用于装饰函数参数,参数装饰器接收3个参数:

  • target: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • propertyKey: 方法名。
  • paramIndex: 参数所在位置的索引。
typescript
const paramDecorator = (target: any, propertyKey: string, paramIndex: number) => {
+    console.log(target, propertyKey, paramIndex)
+}
+
+class Person {
+    name: string = ''
+    age: number = 0
+
+    constructor(name: string, age: number) {
+        this.name = name
+        this.age = age
+    }
+
+    setName(@paramDecorator name: string) {
+      this.name = name
+    }
+}
+
+const p = new Person('张三', 18) // Person、setName、0
+p.setName('李四')
const paramDecorator = (target: any, propertyKey: string, paramIndex: number) => {
+    console.log(target, propertyKey, paramIndex)
+}
+
+class Person {
+    name: string = ''
+    age: number = 0
+
+    constructor(name: string, age: number) {
+        this.name = name
+        this.age = age
+    }
+
+    setName(@paramDecorator name: string) {
+      this.name = name
+    }
+}
+
+const p = new Person('张三', 18) // Person、setName、0
+p.setName('李四')
`,118),e=[o];function t(c,r,y,E,i,F){return n(),a("div",null,e)}const d=s(p,[["render",t]]);export{C as __pageData,d as default}; diff --git a/assets/frontend_base_Typescript.md.47bd8e1c.lean.js b/assets/frontend_base_Typescript.md.47bd8e1c.lean.js new file mode 100644 index 000000000..db4c896ea --- /dev/null +++ b/assets/frontend_base_Typescript.md.47bd8e1c.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const C=JSON.parse('{"title":"TypeScript","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/base/Typescript.md","filePath":"frontend/base/Typescript.md","lastUpdated":1708510102000}'),p={name:"frontend/base/Typescript.md"},o=l("",118),e=[o];function t(c,r,y,E,i,F){return n(),a("div",null,e)}const d=s(p,[["render",t]]);export{C as __pageData,d as default}; diff --git a/assets/frontend_base_Webpack.md.f49018f3.js b/assets/frontend_base_Webpack.md.f49018f3.js new file mode 100644 index 000000000..dc5fe840e --- /dev/null +++ b/assets/frontend_base_Webpack.md.f49018f3.js @@ -0,0 +1,141 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const F=JSON.parse('{"title":"Webpack","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/base/Webpack.md","filePath":"frontend/base/Webpack.md","lastUpdated":1708510102000}'),p={name:"frontend/base/Webpack.md"},o=l(`

Webpack

webpack时前端项目工程化的具体解决方案。webpack的主要功能:提供了友好的前端模块化开发支持、代码压缩混淆、处理浏览器端javascript得兼容性、性能优化等强大的功能。

前端工程化

  • 模块化
  • 组件化
  • 规范话
  • 自动化

基础使用

  • npm install webpack@version webpack-cli@version -D

  • webpack.config.js

    • js
      module.exports = {
      +  mode: 'development' //可取值development和production,前者不会压缩和性能优化,打包速度快
      +}
      module.exports = {
      +  mode: 'development' //可取值development和production,前者不会压缩和性能优化,打包速度快
      +}
  • package.json

    • json
      "script": {
      +  "dev": "webpack"
      +}
      "script": {
      +  "dev": "webpack"
      +}

默认约定

webpack 4.x和5.x版本有如下默认约定

  • 默认打包入口文件:src -> index.js
  • 默认输出文件路径:dist -> main.js

修改打包默认约定

可以在webpack.config.js中

  • 修改entry节点指定打包入口
  • 修改output节点指定输出
js
const path = require('path')
+
+module.exports = {
+  entry: path.join(__dirname, './src/index.js'),
+  output: {
+    path: path.join(__dirname, './dist'),
+    filename: 'js/bundle.js'
+  }
+}
const path = require('path')
+
+module.exports = {
+  entry: path.join(__dirname, './src/index.js'),
+  output: {
+    path: path.join(__dirname, './dist'),
+    filename: 'js/bundle.js'
+  }
+}

插件

webpack-dev-server

  • 类似node.js中的nodemon

  • 修改源代码后webpack会自动进行项目打包和构建

  • npm install webpack-dev-server@version -D

  • 修改package.json的script

    • json
      "scripts": {
      +  "dev": "webpack serve"
      +}
      "scripts": {
      +  "dev": "webpack serve"
      +}

html-webpack-plugin

  • 可以定制index.html内容

  • npm install html-webpack-plugin@version -D

  • 配置webpack.config.js

    • js
      const HtmlPlugin = require('html-webpack-plugin')
      +
      +const htmlPlugin = new HtmlPlugin({
      +  template: './src/index.html',//源文件
      +  filename: './index.html'//生成的问题
      +})
      +
      +module.exports = {
      +  mode: 'development',
      +  plugins: [htmlPlugin]
      +}
      const HtmlPlugin = require('html-webpack-plugin')
      +
      +const htmlPlugin = new HtmlPlugin({
      +  template: './src/index.html',//源文件
      +  filename: './index.html'//生成的问题
      +})
      +
      +module.exports = {
      +  mode: 'development',
      +  plugins: [htmlPlugin]
      +}

clean-webpack-plugin

  • 每次打包发布自动清理dist目录的旧文件

  • npm i clean-webpack-plugin@version -D

  • 配置

    js
    const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    +const cleanPlugin = new CleanWebpackPlugin()
    +
    +plugins: [htmlPlugin, cleanPlugin]
    const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    +const cleanPlugin = new CleanWebpackPlugin()
    +
    +plugins: [htmlPlugin, cleanPlugin]

devServer节点

js
devServer: {
+  open: true,
+  host: 127.0.0.1,
+  port: 80
+}
devServer: {
+  open: true,
+  host: 127.0.0.1,
+  port: 80
+}

loader

实际开发中,webpack只能打包处理.js模块,其他后缀的模块需要调用loader加载器才能正常打包

loader加载器作用:协助webpack打包处理特定的文件模块,比如:

  • css-loader可以处理.css文件

    • npm i style-loader@version css-loader@version -D

    • webpack.config.js的module->rules数组中添加loader规则

    • js
      module: {
      +  rules: [
      +    { test: /\\.css$/, use: ['style-loader', 'css-loader']} //先执行css-loader,从后往前
      +  ]
      +}
      module: {
      +  rules: [
      +    { test: /\\.css$/, use: ['style-loader', 'css-loader']} //先执行css-loader,从后往前
      +  ]
      +}
  • less-loader可以处理.less文件

    • npm i less-loader@version -D
    • js
      module: {
      +  rules: [
      +    { test: /\\.less$/, use: ['style-loader', 'css-loader', 'less-loader']} //先执行less-loader,从后往前
      +  ]
      +}
      module: {
      +  rules: [
      +    { test: /\\.less$/, use: ['style-loader', 'css-loader', 'less-loader']} //先执行less-loader,从后往前
      +  ]
      +}
  • babel-loader可以处理webpack无法处理的高级JS语法

    • npm i babel-loader@version @babel/core@version @babel/plugin-proposal-decorators@version -D

    • js
      module: {
      +  rules: [
      +    { test: /\\.js$/, use: 'babel-loader', exclude: /node_modules/} //
      +  ]
      +}
      module: {
      +  rules: [
      +    { test: /\\.js$/, use: 'babel-loader', exclude: /node_modules/} //
      +  ]
      +}
    • 配置babel-loader

      • 在根目录下创建babel.config.js配置文件

        js
        module.exports = {
        +  plugins: [['@babel@plugin-proposal-decorators'], {legacy: true}]
        +}
        module.exports = {
        +  plugins: [['@babel@plugin-proposal-decorators'], {legacy: true}]
        +}
  • url-loader可以处理样式表中与url路径相关的文件

    • npm i url-loader@version file-loader@version -D

    • js
      module: {
      +  rules: [
      +    /* { 
      +     test: /\\.jpg|png|gif$/,
      +     use: { 
      +       loader: 'url-loader',
      +       options: {
      +         limit: 50000, //limit指定图片大小,单位byte,只有小于等于limit的图片才会被转为base64格式
      +         outputPath: 'images'
      +       }
      +     }
      +    } 
      +    */
      +    { test: /\\.jpg|png|gif$/, use: 'url-loader?limit=50000&outputPath=images'}
      +  ]
      +}
      module: {
      +  rules: [
      +    /* { 
      +     test: /\\.jpg|png|gif$/,
      +     use: { 
      +       loader: 'url-loader',
      +       options: {
      +         limit: 50000, //limit指定图片大小,单位byte,只有小于等于limit的图片才会被转为base64格式
      +         outputPath: 'images'
      +       }
      +     }
      +    } 
      +    */
      +    { test: /\\.jpg|png|gif$/, use: 'url-loader?limit=50000&outputPath=images'}
      +  ]
      +}

build打包发布

在package.json的script节点下新增build命令:

json
"scripts": {
+  "dev": "webpack serve",
+  "build": "webpack --mode production"
+}
"scripts": {
+  "dev": "webpack serve",
+  "build": "webpack --mode production"
+}

SourceMap

SourceMap是一个信息文件,里面存着位置信息。也就是说SourceMap文件中存储着压缩混淆后的代码所对应的转换前的位置。

出错的时候除错工具直接显示原始代码,而不是转换后的代码,方便后期调试。

  • 开发环境在webpack.config.js中添加配置,保证运行时报错和源代码行数一致:
js
module.exports = {
+  devtool: 'eval-source-map'
+}
module.exports = {
+  devtool: 'eval-source-map'
+}
  • 生产环境省略devltool选项则最终生成文件不包含SourceMap,能够防止源码通过SourceMap暴露。如果只想定位报错具体行数,且不想暴露源码,可以将devtool设置为nosources-source-map

其他

  • webpack.config.js配置目录别名
js
resolve: {
+  alias: {
+  //@代表源码目录
+  '@': path.join(__dirname, './src/')
+  }
+}
resolve: {
+  alias: {
+  //@代表源码目录
+  '@': path.join(__dirname, './src/')
+  }
+}
`,38),e=[o];function c(t,r,i,E,y,d){return a(),n("div",null,e)}const h=s(p,[["render",c]]);export{F as __pageData,h as default}; diff --git a/assets/frontend_base_Webpack.md.f49018f3.lean.js b/assets/frontend_base_Webpack.md.f49018f3.lean.js new file mode 100644 index 000000000..1543811ef --- /dev/null +++ b/assets/frontend_base_Webpack.md.f49018f3.lean.js @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const F=JSON.parse('{"title":"Webpack","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/base/Webpack.md","filePath":"frontend/base/Webpack.md","lastUpdated":1708510102000}'),p={name:"frontend/base/Webpack.md"},o=l("",38),e=[o];function c(t,r,i,E,y,d){return a(),n("div",null,e)}const h=s(p,[["render",c]]);export{F as __pageData,h as default}; diff --git a/assets/frontend_base_jQuery.md.4056042a.js b/assets/frontend_base_jQuery.md.4056042a.js new file mode 100644 index 000000000..66eb07fd0 --- /dev/null +++ b/assets/frontend_base_jQuery.md.4056042a.js @@ -0,0 +1,115 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"jQuery","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/base/jQuery.md","filePath":"frontend/base/jQuery.md","lastUpdated":1708510102000}'),o={name:"frontend/base/jQuery.md"},e=l(`

jQuery

顶级对象

$是jQuery的别称,也是jQuery的顶级对象,相当于原生JavaScript肿的window,把元素用$包装成jQuery对象,就可以调用jQuery的方法。

入口函数

js
$(document).ready(function () {
+    //do something
+});
+$(function () {
+    //do something
+})
$(document).ready(function () {
+    //do something
+});
+$(function () {
+    //do something
+})

DOM对象和jQuery对象

  • 用原生JS获取的对象是DOM对象
  • jQuery方法获取的元素是jQuery对象,本质是$对DOM对象包装后产生的对象(伪数组形式存储)
  • DOM和jQuery对象互相转换
    • DOM对象转jQuery对象:$(dom对象)
    • jQuery对象转DOM对象
      • $('div')[index]
      • $('div').get(index)

jQuery常用API

选择器

  • $("选择器")
  • 筛选选择器
    • $('li:first')
    • $('li:last')
    • $('li:eq(2)'):索引号等于2
    • $('li:odd'):索引号为奇数
    • $('li:even'):索引号为偶数
  • 筛选方法
    • parent()
    • children(selector)
    • find(selector)
    • siblings(selector):查找兄弟节点 不包括本身
    • nextAll([expr]):查找当前元素之后所有同辈元素
    • prevtAll([expr]):查找当前之前所有同辈元素
    • hasClass(class)
    • eq(index)

遍历DOM元素(伪数组形式存储)的过程叫隐式迭代:给匹配到的所有元素进行循环遍历,执行相应的方法,而不用手动进行循环调用

jQuery支持链式编程

样式操作

  • 操作样式:jQuery对象.css(属性, 值)
  • 参数可以是对象形式,设置多组样式:jQuery对象.css({"color": "pink", "font-size": "15px"}) (属性可以不用加引号)
  • 获取样式属性值:jQuery对象.css(属性)
  • 设置类样式:
    • jQuery对象.addClass(className)
    • jQuery对象.removeClass(className)
    • jQuery对象.toggleClass(className)

效果

显示隐藏

  • show([speed, [easing], [fn]])
    • 参数都可以省略,无动画直接显示
    • speed:三种预设速度之一的字符串(slow/normal/fast)或表示动画时长的毫秒数值
    • easing:用来切换指定效果,默认swing,可用参数linear
    • fn:回调函数,在动画完成时执行的函数,每个元素执行一次
  • hide()
  • toggle()

滑动

  • slideDown()
  • slideUp()
  • slideToggle()

动画队列停止排队:stop(),必须写在动画的前面

jQuery对象.children("ul").stop().slideToggle()

淡入淡出

  • fadeIn()
  • fadeOut()
  • fadeToggle()
  • fadeTo([[speed], opacity, [easing], [fn]]):修改透明度

自定义动画

  • animate(params, [speed], [easing],[fn])
  • params:想要更改的样式属性,以对象形式传递,必传。属性名可以不带引号,如果是复合属性需要采取驼峰命名

属性操作

  • prop(属性名):获取元素固有属性
  • prop(属性名, 值):设置元素固有属性
  • attr(属性名):获取元素自定义属性
  • attr(属性名, 值):设置元素自定义属性
  • data():可以在指定元素上存取数据,并不会修改DOM元素结构,页面刷新,存放的数据会被移除,也可以获取h5自定义属性,不用data-开头

文本属性

  • html():相当于原生js中的innerHTML
  • text():相当于原生js中的innerText
  • val():相当于原生js的value

元素操作

txt
- jQuery对象.each(function(index, domElement) { } ):遍历匹配的每一个元素,index是索引号,domElement是DOM元素对象,不是jQuery对象
+- $.each(obj, function(index, domElement) { }):遍历指定对象
+- 创建元素:var li = $("<li> </li>")
+- 添加元素
+  - element.append(li):拼接到最后
+  - element.prepend(li):插入到最前
+  - element.before(li):放到元素之后
+  - element.after(li):放到元素之前
+- 删除元素
+  - element.remove():删除匹配的元素
+  - element.empty():删除匹配元素的子节点
+  - element.html(""):等价于empty()
- jQuery对象.each(function(index, domElement) { } ):遍历匹配的每一个元素,index是索引号,domElement是DOM元素对象,不是jQuery对象
+- $.each(obj, function(index, domElement) { }):遍历指定对象
+- 创建元素:var li = $("<li> </li>")
+- 添加元素
+  - element.append(li):拼接到最后
+  - element.prepend(li):插入到最前
+  - element.before(li):放到元素之后
+  - element.after(li):放到元素之前
+- 删除元素
+  - element.remove():删除匹配的元素
+  - element.empty():删除匹配元素的子节点
+  - element.html(""):等价于empty()

尺寸、位置操作

尺寸

  • width()/height():只包含宽高
  • innerWidth()/innerHeight():+padding
  • outerWidth()/outerHeight():+padding、border
  • outerWidth(true)/outerHeight(true):+padding、border、margin

位置

txt
- offset():设置或返回被选元素相对于文档(document)的偏移坐标,跟父级没有关系
+    - 有两个属性left、top
+    - 修改传递对象{top: 10, left: 30}
+- position():返回被选元素相对**带有定位父级**偏移坐标,如果父级都没有定位,以文档为准
+- scrollTop()/scrollLeft():被卷去的头部/左侧
+    - 可以传递参数直接跳到指定位置
- offset():设置或返回被选元素相对于文档(document)的偏移坐标,跟父级没有关系
+    - 有两个属性left、top
+    - 修改传递对象{top: 10, left: 30}
+- position():返回被选元素相对**带有定位父级**偏移坐标,如果父级都没有定位,以文档为准
+- scrollTop()/scrollLeft():被卷去的头部/左侧
+    - 可以传递参数直接跳到指定位置

jQuery事件

事件注册

  • element.事件(function(){})

事件处理

on
  • element.on(events, [selector], fn)
    • events:一个或多个用空格分隔的事件类型,如click、keydown
    • selector:元素的子元素选择器
    • fn:回调函数,即绑定在元素身上的侦听函数
js
$("div").on({
+    mouseenter: function () {
+        $(this).css("background", "skyblue");
+    },
+    click: function () {
+        $(this).css("background", "red");
+    }
+});
+
+$("div").on("mouseenter mouseleave", function () {
+        $(this).toggleClass("current")
+    }
+);
$("div").on({
+    mouseenter: function () {
+        $(this).css("background", "skyblue");
+    },
+    click: function () {
+        $(this).css("background", "red");
+    }
+});
+
+$("div").on("mouseenter mouseleave", function () {
+        $(this).toggleClass("current")
+    }
+);
事件委托

事件绑定在父元素上

js
$("ul").on("click", "li", function () {
+    alert('111')
+})
$("ul").on("click", "li", function () {
+    alert('111')
+})
on可以给未来元素绑定事件
事件解绑off
  • element.off()解绑所有
  • element.off(事件1,事件2...)解绑指定
只触发一次的事件one

element.one(事件,fn)

自动触发
  • element.事件()
  • element.trigger(事件)
  • element.triggerHandler(事件):不会触发元素的默认行为

事件对象

事件被触发,就会有事件对象的产生

js
element.on(events, [selector], function (event) {
+    console.log(event)
+    event.preventDefault();//阻止默认行为
+    return false; //阻止默认行为
+    event.stopPropagation();//阻止冒泡
+})
element.on(events, [selector], function (event) {
+    console.log(event)
+    event.preventDefault();//阻止默认行为
+    return false; //阻止默认行为
+    event.stopPropagation();//阻止冒泡
+})

其他方法

拷贝对象

$.extend([deep], target, object1, [objectN])

  • deep:true-深拷贝,默认false-浅拷贝

  • target:目标对象

  • object:源对象

  • objectN:第N个源对象,会覆盖前面的相同属性

多库共存

  • $统一改为jQuery
  • 新的名称$.noConflict()/jQuery.noConflict()

jQuery插件

  • 瀑布流
  • 图片懒加载
  • 全屏滚动:fullpage.js
  • Bootstrap组件、插件

jQuery请求

  • $.get(url, [data], [callback])

    js
    $(function() {
    +  $('#btn').on('click', function() {
    +      $.get('xxx.com/api/getXxx', 'a=b', function(res) {
    +          console.log(res)
    +      })
    +  })
    +})
    $(function() {
    +  $('#btn').on('click', function() {
    +      $.get('xxx.com/api/getXxx', 'a=b', function(res) {
    +          console.log(res)
    +      })
    +  })
    +})
  • $.post(url, [data], [callback])

    js
    $(function() {
    +  $('#btn').on('click', function() {
    +      $.get('xxx.com/api/getXxx', {"a": "b"}, function(res) {
    +          console.log(res)
    +      })
    +  })
    +})
    $(function() {
    +  $('#btn').on('click', function() {
    +      $.get('xxx.com/api/getXxx', {"a": "b"}, function(res) {
    +          console.log(res)
    +      })
    +  })
    +})
  • $.ajax()

    js
    $.ajax({
    +  type: '',
    +  url: '',
    +  data: {},
    +  success: function(res) {}
    +})
    $.ajax({
    +  type: '',
    +  url: '',
    +  data: {},
    +  success: function(res) {}
    +})
`,64),p=[e];function t(c,r,i,y,E,d){return a(),n("div",null,p)}const F=s(o,[["render",t]]);export{h as __pageData,F as default}; diff --git a/assets/frontend_base_jQuery.md.4056042a.lean.js b/assets/frontend_base_jQuery.md.4056042a.lean.js new file mode 100644 index 000000000..d9671a3fd --- /dev/null +++ b/assets/frontend_base_jQuery.md.4056042a.lean.js @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"jQuery","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/base/jQuery.md","filePath":"frontend/base/jQuery.md","lastUpdated":1708510102000}'),o={name:"frontend/base/jQuery.md"},e=l("",64),p=[e];function t(c,r,i,y,E,d){return a(),n("div",null,p)}const F=s(o,[["render",t]]);export{h as __pageData,F as default}; diff --git a/assets/frontend_framework_Vue.md.98b6e4a2.js b/assets/frontend_framework_Vue.md.98b6e4a2.js new file mode 100644 index 000000000..7aba4c6a9 --- /dev/null +++ b/assets/frontend_framework_Vue.md.98b6e4a2.js @@ -0,0 +1,923 @@ +import{_ as o,o as e,c as t,k as s,a,t as l,Q as p}from"./chunks/framework.dc1f4608.js";const k=JSON.parse('{"title":"Vue","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/framework/Vue.md","filePath":"frontend/framework/Vue.md","lastUpdated":1708510102000}'),c={name:"frontend/framework/Vue.md"},r=p(`

Vue

Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架

特性

  • 数据驱动视图:在数据变化时页面会重新渲染
  • 双向数据绑定:DOM元素中的数据和Vue实例中的data保持一致,无论谁被改变,另一方都会更新为相同的数据

MVVM

MVVM是Vue实现数据驱动视图和双向数据绑定的原理。MVVM指的是Model、View和ViewModel。

  • Model:当前页面渲染时依赖的数据源
  • View:当前页面渲染的DOM结构
  • ViewModel:Vue的实例,MVVM的核心

ViewModel把Model和View连接在一起,同时监听DOM变化和数据源的变化。

起步

html
<!doctype html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport"
+          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+</head>
+<body>
+<div id="app">
+    {{ msg }}
+</div>
+<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
+<script>
+    const vm = new Vue({
+        el: '#app',
+        data: {
+            msg: 'hello world'
+        }
+    })
+</script>
+</body>
+</html>
<!doctype html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport"
+          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+</head>
+<body>
+<div id="app">
+    {{ msg }}
+</div>
+<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
+<script>
+    const vm = new Vue({
+        el: '#app',
+        data: {
+            msg: 'hello world'
+        }
+    })
+</script>
+</body>
+</html>

指令和过滤器

内容渲染

`,12),E=s("li",null,[a("v-test "),s("ul",null,[s("li",null,[s("code",null,"

"),a(":把username值渲染到p标签中")]),s("li",null,[s("code",null,"

性别

"),a(":把gender值渲染到p标签中,原有的值会被覆盖")])])],-1),y=s("li",null,"插值表达式(Mustache),专门用来解决v-text会覆盖默认文本内容的问题,不能用在属性上",-1),i=s("li",null,"支持javascript表达式",-1),d=s("li",null,[a("v-html "),s("ul",null,[s("li",null,"把包含HTML标签的字符串渲染为页面的HTML元素")])],-1),u=p(`

属性绑定

  • v-bind:单向绑定
    • v-bind:属性名
    • 简写为:属性名
    • 支持javascript表达式

事件绑定

v-on

  • v-on:事件名="函数(param)":v-on:click="add"

  • 简写为:@,例如@click="add"

  • 函数定义在vue实例的methods中

  • js
    methods: {
    +  add: function(param) {
    +  	console.log(1)
    +	}
    +}
    +---
    +ES6写法: 
    +methods: {
    +  add(param) {
    +  	console.log(1)
    +	}
    +}
    methods: {
    +  add: function(param) {
    +  	console.log(1)
    +	}
    +}
    +---
    +ES6写法: 
    +methods: {
    +  add(param) {
    +  	console.log(1)
    +	}
    +}
  • 不传参数默认参数列表有事件对象e,如果传参可以用$event传递事件对象

  • 事件修饰符

    • @click.prevent=show():绑定事件并阻止默认行为
    • stop:阻止事件冒泡
    • capture:以捕获模式触发当前事件处理函数
    • once:绑定事件只触发一次
    • self:只有在even.target时当前元素自身时触发事件处理函数
  • 按键修饰符

    • 判断详细的案件
      • @keyup.enter=submit
      • esc

双向绑定

  • v-model:不操作DOM情况下,快速获取表单数据
  • 修饰符
    • .nubmer:自动将输入转为数值
    • .trim:自动过滤输入的首尾空白字符
    • .lazy:在change时更新,input时不更新

条件渲染

控制DOM的显示与隐藏

  • v-if
    • 通过添加、移除元素实现
    • 如果刚进入页面不需要被展示,而且后期可能也不需要展示此时v-if性能更好
    • 配套指令:v-else、v-else-if
  • v-show
    • display控制元素显示、隐藏
    • 如果频繁切换显示状态用v-show更好

列表渲染

v-for渲染数组

基于一个数组来循环渲染一个列表结构。v-for指令需要用item in items形式的特殊语法

html
<li v-for="item in items">姓名是: {{ item.name }}</li>
+
+<li v-for="(item,index) in items">姓名是: {{ item.name }}</li>
<li v-for="item in items">姓名是: {{ item.name }}</li>
+
+<li v-for="(item,index) in items">姓名是: {{ item.name }}</li>
  • items:待循环数组
  • item:被循环的每一项
  • index:索引号,从0开始

建议用到v-for指令,要绑定一个:key属性,而且尽量把id作为key

  • key的值要是字符串/数字类型
  • index作为key没有任何意义,因为index没有唯一性(和数据没有绑定关系)
  • 指定key可以提升性能、防止列表状态紊乱

v-for渲染对象

完整语法

html
<li v-for="(value, key, index) in myObject"> 
+{{ value }} {{ kye }} {{ index }}
+</li>
<li v-for="(value, key, index) in myObject"> 
+{{ value }} {{ kye }} {{ index }}
+</li>

过滤器(vue3已移除)

常用于文本格式化,过滤器可以用在两个地方:插值表达式和v-bind属性绑定,过滤器本质是函数,被定义在vue实例的filters节点下

`,22),h=s("li",null,[s("code",null,'
js
filters: {
+  capitalize(val) {
+    return val.charAt(0).toUppercase() + var.slice(1)
+  }
+}
filters: {
+  capitalize(val) {
+    return val.charAt(0).toUppercase() + var.slice(1)
+  }
+}

私有过滤器和全局过滤器

  • 私有过滤器:定义在vue实例的filters节点下
  • 全局过滤器:使用Vue.filter(filter, (str) => {return xxx})定义

连续调用&传参

js
{{ msg | filterA | filterB(arg1, arg2)}}
{{ msg | filterA | filterB(arg1, arg2)}}

侦听器

watch侦听器语序开发者监视数据的变化,从而针对数据的变化做特定的操作。

侦听器格式

watch定义在vue实例的watch节点下

  • 方法格式的侦听器

    • js
      watch: {
      +  username(newVal, oldVal) {
      +    console.log(newVal, oldVal)
      +  }
      +}
      watch: {
      +  username(newVal, oldVal) {
      +    console.log(newVal, oldVal)
      +  }
      +}
    • 缺点

      • 无法在刚进入页面时自动触发
      • 如果侦听的是一个对象,对象属性发生变化不会触发侦听器
  • 对象格式的侦听器

    • js
      watch: {
      +  username: {
      +    handler(newVal, oldVal) {
      +    console.log(newVal, oldVal)
      +  },
      +  immediate: true,
      +  deep: true,
      +  'info.age'(newVal) {
      +    console.log(newVal)
      +  }
      +}
      watch: {
      +  username: {
      +    handler(newVal, oldVal) {
      +    console.log(newVal, oldVal)
      +  },
      +  immediate: true,
      +  deep: true,
      +  'info.age'(newVal) {
      +    console.log(newVal)
      +  }
      +}
    • 可以通过immediate选项让侦听器立即触发

    • 可以通过deep选项开启深度监听,可以监听到对象的任何一个属性变化

    • 如果要侦听的是对象的子属性变化,则必须包裹一层单引号

计算属性

通过运算得到的属性值,可以被模版结构或methods方法使用。

计算属性放在vue实例的computed节点中

js
var vm = new Vue({
+  el: '#app',
+  data: {
+    r: 0, g: 0, b: 0
+  },
+  computed: {
+    //计算属性rgb
+    rgb() { return \`rgb(\${this.r}, \${this.g}, \${this.b})\`}
+    //计算属性 allChecked
+    allChecked: {
+    	get() {
+      	return this.goodsList.every(item => item.goods_state)
+    	},
+    	set(newVal) {
+      	this.goodsList.forEach(item => item.goods_state = newVal)
+    	}
+		}
+  },
+  methods: {
+    show() { console.log(this.rgb) }
+  }
+})
var vm = new Vue({
+  el: '#app',
+  data: {
+    r: 0, g: 0, b: 0
+  },
+  computed: {
+    //计算属性rgb
+    rgb() { return \`rgb(\${this.r}, \${this.g}, \${this.b})\`}
+    //计算属性 allChecked
+    allChecked: {
+    	get() {
+      	return this.goodsList.every(item => item.goods_state)
+    	},
+    	set(newVal) {
+      	this.goodsList.forEach(item => item.goods_state = newVal)
+    	}
+		}
+  },
+  methods: {
+    show() { console.log(this.rgb) }
+  }
+})
js
computed: {
+  allChecked: {
+    get() {
+      return this.goodsList.every(item => item.goods_state)
+    },
+    set(newVal) {
+      this.goodsList.forEach(item => item.goods_state = newVal)
+    }
+  }
+}
computed: {
+  allChecked: {
+    get() {
+      return this.goodsList.every(item => item.goods_state)
+    },
+    set(newVal) {
+      this.goodsList.forEach(item => item.goods_state = newVal)
+    }
+  }
+}

axios

axios一个专注于网络请求的库

基本语法:

js
axios({
+  method: '请求类型',
+  // URL中的query参数
+  params: {
+    
+  },
+  // body参数
+  data: {
+  
+}
+  url: '请求的URL地址',
+}).then((result) => {
+  //.then用来指定成功的回调,result是请求成功后的结果
+})
axios({
+  method: '请求类型',
+  // URL中的query参数
+  params: {
+    
+  },
+  // body参数
+  data: {
+  
+}
+  url: '请求的URL地址',
+}).then((result) => {
+  //.then用来指定成功的回调,result是请求成功后的结果
+})

结合async和await使用axios

js
document.querySelector('#btn').addEventListener('click', async function(){
+  // 如果调用方法返回值是Promise实例,则可以在前面添加await,await只能用在被async“修饰”的方法中
+  // 解构赋值的时候使用:进行重命名
+  const { data: res } = await axios({
+    method: 'POST',
+    url: 'xxx',
+    data: {
+      name: '111'
+    }
+  })
+  console.log(res.data)
+})
document.querySelector('#btn').addEventListener('click', async function(){
+  // 如果调用方法返回值是Promise实例,则可以在前面添加await,await只能用在被async“修饰”的方法中
+  // 解构赋值的时候使用:进行重命名
+  const { data: res } = await axios({
+    method: 'POST',
+    url: 'xxx',
+    data: {
+      name: '111'
+    }
+  })
+  console.log(res.data)
+})
  • axios.get()
  • axios.post()
  • axios.delete()
  • axios.put()

vue工程中使用

js
// main.js
+
+import Vue from 'vue'
+import App from './App.vue'
+import axios from 'axios'
+
+Vue.config.productionTip = false
+
+// 缺点:不利于api接口复用
+// 组件实例中直接用\`this.$http\`使用
+// axios.defaults.baseURL = '请求根路径'
+// Vue.prototype.$http = axios
+
+new Vue({
+  reder: h => h(App)
+}).$mount(#app)
// main.js
+
+import Vue from 'vue'
+import App from './App.vue'
+import axios from 'axios'
+
+Vue.config.productionTip = false
+
+// 缺点:不利于api接口复用
+// 组件实例中直接用\`this.$http\`使用
+// axios.defaults.baseURL = '请求根路径'
+// Vue.prototype.$http = axios
+
+new Vue({
+  reder: h => h(App)
+}).$mount(#app)

vue-cli

单页面应用程序(Single Page Application)简称SPA,指的是一个Web网站中只有唯一的一个HTML页面,所有的功能与交互都在这唯一的一个页面内完成。

vue-cli是Vue.js开发的标准工具。简化了基于webpack创建工程化的Vue项目的过程。

安装

npm install -g @vue/cli

创建项目

vue create projectName

vue组件

组件组成

组件后缀名是.vue,vue组件包括三个组成部分

template

vue
<!--template是一个虚拟标签,只起到包裹作用,不会被渲染成任何实质性HTML-->
+<template>
+	<div>
+    <!--template中只能有一个根元素-->
+  </div>
+</template>
<!--template是一个虚拟标签,只起到包裹作用,不会被渲染成任何实质性HTML-->
+<template>
+	<div>
+    <!--template中只能有一个根元素-->
+  </div>
+</template>

script

vue
<script>
+export default {
+  name: 'xxx',// <keep-alive>实现组件缓存功能,调整工具中看到的标签名称
+  // data必须是一个函数
+  data() {
+    return {
+      xx: xx
+    }
+  },
+  methods: {
+    fun() {
+      // 组件中的this代表当前组件的实例对象
+      console.log(this)
+      this.xx = yy
+    }
+  },
+  watch: {},
+  computed: {}
+  ...
+}
+</script>
<script>
+export default {
+  name: 'xxx',// <keep-alive>实现组件缓存功能,调整工具中看到的标签名称
+  // data必须是一个函数
+  data() {
+    return {
+      xx: xx
+    }
+  },
+  methods: {
+    fun() {
+      // 组件中的this代表当前组件的实例对象
+      console.log(this)
+      this.xx = yy
+    }
+  },
+  watch: {},
+  computed: {}
+  ...
+}
+</script>

style

vue
<style lang="less">/* 默认lang="css" */
+
+</style>
<style lang="less">/* 默认lang="css" */
+
+</style>

组件之间的父子关系

组件被封装好后,彼此之间是相互独立的,不存在父子关系。

使用组件时,根据彼此的嵌套关系,形成了父子关系,兄弟关系。

组件使用步骤

注册私有子组件

  1. import语法导入需要的组件
js
import A from '@/components/A.vue
import A from '@/components/A.vue
  1. 使用components节点注册组件
js
export default {
+	components: {
+		A //注册名称主要用于 以标签形式把注册的组件 渲染和使用到页面结构之中
+  }
+}
export default {
+	components: {
+		A //注册名称主要用于 以标签形式把注册的组件 渲染和使用到页面结构之中
+  }
+}
  1. 以标签形式使用注册的组件
vue
<div class="box">
+  <A></A>
+</div>
<div class="box">
+  <A></A>
+</div>

注册全局组件

js
// main.js
+
+import Test from '@/components/Test.vue'
+
+Vue.component('MyTest', Test)
// main.js
+
+import Test from '@/components/Test.vue'
+
+Vue.component('MyTest', Test)

组件的props

props是组件的自定义属性,在封装通用组件的时候,合理的使用props可以极大提高组件的复用性。

  • props中的数据,可以直接在模板结构中使用
  • props是只读的
vue
<script>
+export default {
+  props: {
+    initCount: {
+      default: 0,//默认值
+      type: Number, //规定属性的值类型,如果传递的值不符合,则会报错
+      required: true //必填项
+    }
+  },
+  data() {
+    return {
+      count: this.initCount
+    }
+  }
+}
+</script>
<script>
+export default {
+  props: {
+    initCount: {
+      default: 0,//默认值
+      type: Number, //规定属性的值类型,如果传递的值不符合,则会报错
+      required: true //必填项
+    }
+  },
+  data() {
+    return {
+      count: this.initCount
+    }
+  }
+}
+</script>

组件之间样式冲突

默认情况下,写在组件中的样式会全局生效,原因是:

  1. 单页面应用程序中所有的DOM结构都是基于唯一的index.hmtl页面进行呈现的
  2. 每个组件中的样式都会影响整个index.html的DOM元素

解决方案

  • 使用自定义属性:DOM元素增加自定义属性data-v-xxx,使用属性选择器设置样式div[data-v-xxx]

  • style标签增加scoped属性,会自动为每个标签生成data-v属性

deep样式穿透

/deep/ 选择器

当使用第三方组件库,如果有修改第三方组件库的默认样式需求,需要用到deep

组件的生命周期

生命周期是指一个组件从创建->运行->销毁的整个阶段。

分类

  • 组件创建阶段
    • beforeCreate:组件的props/data/methods尚未被创建,都处于不可用状态
    • created:组件的props/data/methods被创建,都处于可用状态,但是组件的模板结构尚未生成
    • beforeMount:将要把内存中编译好的HTML结果渲染到浏览器中,此时浏览器中还没有当前组件的DOM结构
    • mounted:已经把内存中编译好的HTML结果渲染到浏览器中,此时浏览器已经包含当前组件DOM结构
  • 组件运行阶段
    • beforeUpdate:将要根据变化过后、最新的数据重新渲染组件的模版结构
    • updated:已经根据最新的数据,完成了组件DOM结构的重新渲染
  • 组件销毁阶段
    • beforeDestroy:将要销毁此组件但还未销毁,组件还处于正常工作状态
    • destroyed:组件已被销毁,此组件在浏览器中对应的DOM结构已被完全移除

组件数据共享

父组件向子组件共享数据需要使用自定义属性

子向父传值需要使用自定义事件

js
//子组件
+methods: {
+  add() {
+    this.count += 1
+	  this.$emit('numchange', this.count)
+  }
+}
//子组件
+methods: {
+  add() {
+    this.count += 1
+	  this.$emit('numchange', this.count)
+  }
+}
js
//父组件
+<Son @numchange="getNewCount"></Son>
+
+---
+
+methods: {
+	getNewCount(val) {
+		this.countFromSon = val
+	}
+}
//父组件
+<Son @numchange="getNewCount"></Son>
+
+---
+
+methods: {
+	getNewCount(val) {
+		this.countFromSon = val
+	}
+}

兄弟组件之间数据共享需要使用EventBus

js
//A组件
+import bus from './eventBus.js'
+
+methods: {
+	sendMsg() {
+		bus.$emit('share', this.msg)
+	}
+}
+
+//eventBus.js
+import Vue from 'vue'
+
+export default new Vue()
+
+//兄弟组件B
+import bus from './eventBus.js'
+
+created() {
+	bus.$on('share', val => {
+		this.msgFromSibling = val
+	})
+}
//A组件
+import bus from './eventBus.js'
+
+methods: {
+	sendMsg() {
+		bus.$emit('share', this.msg)
+	}
+}
+
+//eventBus.js
+import Vue from 'vue'
+
+export default new Vue()
+
+//兄弟组件B
+import bus from './eventBus.js'
+
+created() {
+	bus.$on('share', val => {
+		this.msgFromSibling = val
+	})
+}

ref引用

ref用来辅助开发者在不依赖jQuery的情况下,获取DOM元素或组件的引用

每个vue组件的实例上,都包含一个$refs对象,里面存储着对应的DOM元素或组件的引用,默认情况下组件的$refs指向一个空对象

使用ref引用页面上的DOM元素

vue
<div ref="myDiv"></div>
+
+// methods中访问
+this.$refs.myDiv
<div ref="myDiv"></div>
+
+// methods中访问
+this.$refs.myDiv

使用ref引用组件

vue
<Son ref="compSon"></Son>
+
+// methods中访问
+this.$refs.compSon.方法
+this.$refs.compSon.属性
<Son ref="compSon"></Son>
+
+// methods中访问
+this.$refs.compSon.方法
+this.$refs.compSon.属性

$nextTick(callback)

组件的$nextTick(callback)方法,会把callback函数会推迟到下一个DOM更新之后执行。

动态组件

动态组件指的是动态切换组件的显示与隐藏。

component标签is属性

vue
<template>
+	<component :is="componentName"></component>
+</template>
+<script>
+import Left from '@/components/Left.vue'  
+import Right from '@/components/Right.vue'
+export default {
+  data() {
+    return {
+      componentName: "Left"
+    }
+  },
+  components: {
+    Left,
+    Right
+  }
+}
+</script>
<template>
+	<component :is="componentName"></component>
+</template>
+<script>
+import Left from '@/components/Left.vue'  
+import Right from '@/components/Right.vue'
+export default {
+  data() {
+    return {
+      componentName: "Left"
+    }
+  },
+  components: {
+    Left,
+    Right
+  }
+}
+</script>

keep-alive

keep-alive标签能把内部的组件进行缓存,而不是销毁组件

vue
<template>
+	<keep-alive>
+    <component :is="componentName"></component>
+  </keep-alive>
+</template>
<template>
+	<keep-alive>
+    <component :is="componentName"></component>
+  </keep-alive>
+</template>

对应的生命周期函数

  • 被缓存:deactivated生命周期函数
  • 被激活:activated生命周期函数,当组件第一次被创建也会执行

include/exclude属性

  • include可以指定哪些组件被缓存,只有名称匹配的组件会被缓存,多个用,分隔

  • exclude相反

  • 两个属性不能同时使用

vue
<template>
+	<keep-alive include="Left">
+    <component :is="componentName"></component>
+  </keep-alive>
+</template>
<template>
+	<keep-alive include="Left">
+    <component :is="componentName"></component>
+  </keep-alive>
+</template>

插槽

插槽(Slot)是vue为组件的封装者提供的能力。允许开发者在封装组件时,把不确定的、希望由用户指定的部份定义为插槽。

vue
<!--Left.vue-->
+<template>
+	<slot name="default">
+    这里可以指定默认内容,会被覆盖
+  </slot>
+</template>
<!--Left.vue-->
+<template>
+	<slot name="default">
+    这里可以指定默认内容,会被覆盖
+  </slot>
+</template>

渲染Left组件时

vue
<template>
+	<Left>
+    <!--此区域必须在组件中声明插槽才会渲染-->
+    <!--默认情况下 会被填充到名为default的插槽内-->
+    <p>
+      自定义内容
+  </p>
+  </Left>
+</template>
+
+---
+<template>
+	<Left>
+		<template v-slot:default> 
+      <p>
+      自定义内容
+  		</p>
+		</template>
+  </Left>
+</template>
+---
+<template>
+	<Left>
+		<template #default> 
+      <p>
+      自定义内容
+  		</p>
+		</template>
+  </Left>
+</template>
<template>
+	<Left>
+    <!--此区域必须在组件中声明插槽才会渲染-->
+    <!--默认情况下 会被填充到名为default的插槽内-->
+    <p>
+      自定义内容
+  </p>
+  </Left>
+</template>
+
+---
+<template>
+	<Left>
+		<template v-slot:default> 
+      <p>
+      自定义内容
+  		</p>
+		</template>
+  </Left>
+</template>
+---
+<template>
+	<Left>
+		<template #default> 
+      <p>
+      自定义内容
+  		</p>
+		</template>
+  </Left>
+</template>

slot

  • 声明一个插槽区域

  • 每个插槽都要有一个name属性,如果省略,则使用默认名称default

  • 作用域插槽:封装组件时,为预留的slot提供属性对应的值

    • html
      定义:
      +<slot name="content" msg="hello world"></slot>
      +-- 
      +使用:
      +<template #content="scope">
      +	<p>
      +    {{ scope.msg }}
      +  </p>
      +</template>
      定义:
      +<slot name="content" msg="hello world"></slot>
      +-- 
      +使用:
      +<template #content="scope">
      +	<p>
      +    {{ scope.msg }}
      +  </p>
      +</template>
    • 作用域插槽解构赋值

      • html
        <slot name="content" msg="hello world" :user="user"></slot>
        +
        +---
        +
        +<template #content="{msg, user}">
        +	<p>
        +    {{msg}}
        +  </p>
        +  <p>
        +    {{user}}
        +  </p>
        +</template>
        <slot name="content" msg="hello world" :user="user"></slot>
        +
        +---
        +
        +<template #content="{msg, user}">
        +	<p>
        +    {{msg}}
        +  </p>
        +  <p>
        +    {{user}}
        +  </p>
        +</template>

v-slot

  • 只能用在template标签上
  • 使用具名插槽简写形式#default

自定义指令

私有自定义指令

在每个vue组件中,可以在directives节点下声明私有自定义指令

bind函数

  • 当指令第一次被绑定到元素上的时候,会立刻触发bind函数,且只会触发一次

  • 形参el:绑定了此指令的原生的DOM对象

  • 形参binding:传递过来的参数是binding中的value

js
directives: {
+  color: {
+    bind(el, binding) {
+      el.style.color = binding.value
+    }
+  }
+}
directives: {
+  color: {
+    bind(el, binding) {
+      el.style.color = binding.value
+    }
+  }
+}

update函数

  • 第一次不会触发

  • 在DOM更新的时候就会触发update函数

js
directives: {
+  color: {
+    update(el, binding) {
+      el.style.color = binding.value
+    }
+  }
+}
directives: {
+  color: {
+    update(el, binding) {
+      el.style.color = binding.value
+    }
+  }
+}

函数简写

如果bind和update函数的逻辑完全相同,则对象格式的自定义指令可以简写为

js
directives: {
+  color(el, binding) {
+    el.style.color = binding.value
+  }
+}
directives: {
+  color(el, binding) {
+    el.style.color = binding.value
+  }
+}

全局自定义指令

使用Vue.directive声明

js
Vue.directive('color', function(el, binding){
+  el.style.color = binding.value
+})
+---
+Vue.directive('color', {
+  binding(el, binding) {
+      el.style.color = binding.value
+  },
+  update(el, binding) {
+      el.style.color = binding.value
+  }
+})
Vue.directive('color', function(el, binding){
+  el.style.color = binding.value
+})
+---
+Vue.directive('color', {
+  binding(el, binding) {
+      el.style.color = binding.value
+  },
+  update(el, binding) {
+      el.style.color = binding.value
+  }
+})

路由(Router)

模式

  • hash模式

  • history模式

    • publicPath/baseUrl

      js
      //vue.config.js
      +module.exports = {
      +  publicPath: process.env.NODE_ENV === 'production'
      +    ? '/production-sub-path/'
      +    : '/'
      +}
      //vue.config.js
      +module.exports = {
      +  publicPath: process.env.NODE_ENV === 'production'
      +    ? '/production-sub-path/'
      +    : '/'
      +}
    • createWebHistory(base?)

工作方式

  1. 用户点击路由链接
  2. 导致了URL地址栏中Hash值发生变化
  3. 前端路由监听到了Hash地址变化
  4. 前端路由把当前Hash地址对应的组件渲染到浏览器中

vue-router

安装

npm i vue-router@version

创建路由模块

在src目录下,新建router/index.js模块

js
import Vue from 'vue'
+import VueRouter from 'vue-router'
+
+// 调用Vue.use()函数,把VueRouter安装为Vue的插件
+Vue.use(VueRouter)
+
+const router = new VueRouter()
+
+export default router
import Vue from 'vue'
+import VueRouter from 'vue-router'
+
+// 调用Vue.use()函数,把VueRouter安装为Vue的插件
+Vue.use(VueRouter)
+
+const router = new VueRouter()
+
+export default router

导入并挂载路由模块

main.js中挂载路由模块

js
//main.js
+import ...
+//import router from '@/router/index.js'
+//简写
+import router from '@/router'
+
+new Vue({
+  render: h => h(App),
+  //router: router
+  //属性名 属性值一致 可以简写
+  router
+})
//main.js
+import ...
+//import router from '@/router/index.js'
+//简写
+import router from '@/router'
+
+new Vue({
+  render: h => h(App),
+  //router: router
+  //属性名 属性值一致 可以简写
+  router
+})

声明路由链接和占位符

  • 路由链接:router-link
  • 占位符router-view,组件在这里展示
vue
<template>
+	<div class="container">
+    <a href="#/home">首页</a>
+    <a href="#/movie">电影</a>
+    <a href="#/about">关于</a>
+    <hr/>
+    可以用router-link标签代替普通a标签,可以省略#
+    <router-link to="#/home">首页</router-link>
+    <router-link to="#/movie">电影</router-link>
+    <router-link to="#/about">关于</router-link>
+    
+    <router-view></router-view>
+  </div>
+</template>
<template>
+	<div class="container">
+    <a href="#/home">首页</a>
+    <a href="#/movie">电影</a>
+    <a href="#/about">关于</a>
+    <hr/>
+    可以用router-link标签代替普通a标签,可以省略#
+    <router-link to="#/home">首页</router-link>
+    <router-link to="#/movie">电影</router-link>
+    <router-link to="#/about">关于</router-link>
+    
+    <router-view></router-view>
+  </div>
+</template>

修改router/index.js

js
import Vue from 'vue'
+import VueRouter from 'vue-router'
+
+import Home from '@/components/Home.vue'
+import Movie from '@/components/Movie.vue'
+import About from '@/components/About.vue'
+
+// 调用Vue.use()函数,把VueRouter安装为Vue的插件
+Vue.use(VueRouter)
+
+const router = new VueRouter({
+  // routes是一个数组:定义hash地址和组件之间的对应关系
+  routes: [
+    // 路由规则
+    { path: '/', redirect: '/home' }, // 重定向
+    { path: '/home', component: Home },
+    { path: '/movie', component: Movie },
+    { path: '/about', component: About }
+  ]
+})
+
+export default router
import Vue from 'vue'
+import VueRouter from 'vue-router'
+
+import Home from '@/components/Home.vue'
+import Movie from '@/components/Movie.vue'
+import About from '@/components/About.vue'
+
+// 调用Vue.use()函数,把VueRouter安装为Vue的插件
+Vue.use(VueRouter)
+
+const router = new VueRouter({
+  // routes是一个数组:定义hash地址和组件之间的对应关系
+  routes: [
+    // 路由规则
+    { path: '/', redirect: '/home' }, // 重定向
+    { path: '/home', component: Home },
+    { path: '/movie', component: Movie },
+    { path: '/about', component: About }
+  ]
+})
+
+export default router

嵌套路由

通过路由实现组件的嵌套展示,叫做嵌套路由

  • 模板内容中又有子级路由链接
  • 点击子级路由链接显示子级模板内容

通过children属性声明子路由规则

js
import Vue from 'vue'
+import VueRouter from 'vue-router'
+
+import Home from '@/components/Home.vue'
+import Movie from '@/components/Movie.vue'
+import About from '@/components/About.vue'
+import Tab1 from '@/components/tabs/Tab1.vue'
+import Tab2 from '@/components/tabs/Tab2.vue'
+
+// 调用Vue.use()函数,把VueRouter安装为Vue的插件
+Vue.use(VueRouter)
+
+const router = new VueRouter({
+  // routes是一个数组:定义hash地址和组件之间的对应关系
+  routes: [
+    // 路由规则
+    { path: '/', redirect: '/home' }, // 重定向
+    { path: '/home', component: Home },
+    { path: '/movie', component: Movie },
+    { 
+      path: '/about',
+      component: About,
+      children: [
+        { path: 'tab1', component: Tab1 },
+        { path: 'tab2', component: Tab2 }
+      ]
+    }
+  ]
+})
+
+export default router
import Vue from 'vue'
+import VueRouter from 'vue-router'
+
+import Home from '@/components/Home.vue'
+import Movie from '@/components/Movie.vue'
+import About from '@/components/About.vue'
+import Tab1 from '@/components/tabs/Tab1.vue'
+import Tab2 from '@/components/tabs/Tab2.vue'
+
+// 调用Vue.use()函数,把VueRouter安装为Vue的插件
+Vue.use(VueRouter)
+
+const router = new VueRouter({
+  // routes是一个数组:定义hash地址和组件之间的对应关系
+  routes: [
+    // 路由规则
+    { path: '/', redirect: '/home' }, // 重定向
+    { path: '/home', component: Home },
+    { path: '/movie', component: Movie },
+    { 
+      path: '/about',
+      component: About,
+      children: [
+        { path: 'tab1', component: Tab1 },
+        { path: 'tab2', component: Tab2 }
+      ]
+    }
+  ]
+})
+
+export default router
js
const routes = [
+  {
+    path: '/',
+    component: Home,
+    meta: {
+      keepAlive: true,
+      isRecord: true,
+      top: 0
+    }
+  },
+  {
+    path: '/user',
+    component: User
+  }
+]
+
+const router = new VueRouter({
+  routes,
+  scrollBehavior(to, from, savedPosition) {
+    if (savedPosition) {
+      return savedPosition
+    }
+    return {
+      x: 0,
+      y: to.meta.top || 0
+    }
+  }
+})
const routes = [
+  {
+    path: '/',
+    component: Home,
+    meta: {
+      keepAlive: true,
+      isRecord: true,
+      top: 0
+    }
+  },
+  {
+    path: '/user',
+    component: User
+  }
+]
+
+const router = new VueRouter({
+  routes,
+  scrollBehavior(to, from, savedPosition) {
+    if (savedPosition) {
+      return savedPosition
+    }
+    return {
+      x: 0,
+      y: to.meta.top || 0
+    }
+  }
+})

动态路由匹配

把Hash地址中可变的部分定义为参数项,可以提高路由规则的复用性。

使用:来定义路由的参数项

js
{ path: '/movie/:id', component: Movie, props: true}
{ path: '/movie/:id', component: Movie, props: true}
  • 组件中可以通过$route.params.id获取路径参数(Path Variable)

$route.params.query获取查询参数

$route.params.fullPath:包含路径和参数

$route.params.path:只有路径没有参数

  • 可以在路由规则中添加props传餐,在组件中定义props直接获取

导航

声明式导航

  • a标签
  • route-link标签

编程式导航

  • location.href跳转
  • vue-router的编程式导航api
    • this.$router.push('hash地址'):跳转到hash地址,增加一条历史记录
    • this.$router.replace('hash地址'):跳转到hash地址,并替换掉当前的历史记录
    • this.$router.go(数值n):可以在浏览历史中前进或后退
      • this.$router.back()
      • this.$router.forward()

导航守卫

可以控制路由的访问权限

全局前置守卫

每次发生路由导航跳转时,都会触发前置守卫,在前置守卫中,可以对每个路由进行访问权限控制。

js
const router = new VueRouter({})
+// 每次路由跳转都会触发回调函数                              
+router.beforeEach((to, from, next) => {
+  // to:将要访问的路由信息对象
+  // from:将要离开的路由信息对象
+  // next:一个函数,调用next()表示放行,允许这次路由导航
+})
const router = new VueRouter({})
+// 每次路由跳转都会触发回调函数                              
+router.beforeEach((to, from, next) => {
+  // to:将要访问的路由信息对象
+  // from:将要离开的路由信息对象
+  // next:一个函数,调用next()表示放行,允许这次路由导航
+})
next的三种调用方式
  • next():直接放行
  • next('/path'):强制跳转到指定页面
  • next(false):不允许跳转,强制保留在当前页面

Vue3.0

创建项目

vite

使用:npm init vite-app projectName

create-vue

create-vue创建的项目也是基于vite的构建设置

使用:npm create vue@3 或者npm init vue@latest

crete-vue同样支持vue2:npm create vue@2 / npm init vue@2

Vite和Webpack的区别(内容来自ChatGPT):

Vite和Webpack是两种常用的前端构建工具。

  1. 底层实现不同

Vite使用ES modules(ESM)作为模块系统管理,而Webpack使用CommonJS来管理模块。这意味着,在使用Webpack打包项目时,所有模块都将被打包到一个或多个bundle.js文件中,而Vite将原始文件作为模块提取和处理,并将其以一种非常高效的方式提供给浏览器。

  1. 开发环境下的性能

Vite在开发环境下启动非常快,不需要等待代码打包时间,并且在修改代码时,也可以直接进行热更新,非常适合在开发阶段使用。而Webpack在开发模式下代码打包速度较慢,启动速度也相对较慢,修改代码后也需要较长的时间来重新打包。

  1. 生产环境下的性能

在生产环境下,Webpack可以通过代码分割(Code Splitting)和 Tree Shaking来优化代码,减小打包后的文件大小。而Vite在生产环境下目前还不支持代码分割。因此,如果项目需要大量使用Code Splitting和Tree Shaking等技术,使用Webpack可能会更加合适。

  1. 生态和可定制性

Webpack具有强大的社区和众多的插件和Loader来处理各种文件和场景,可以根据不同的需求进行高度的定制。而Vite的生态和可定制性方面要弱于Webpack,它的插件数量还比较少。

总的来说,Vite是一种专门为现代浏览器设计的前端构建工具,它在开发环境下性能卓越,但在生产环境方面还有一些局限。而Webpack则是一种更加稳健和灵活的构建工具,它可以用于各种复杂的场景和需求,并且有着更强大的生态和定制能力,但需要进行更多的配置。选择使用哪种工具,应该根据具体项目需求和使用场景来进行选择。

create-vue和vue-cli的区别:

  • vue-cli基于webpack,而create-vue基于vite

构建一个vue实例

js
import { createApp } from 'vue'
+
+createApp({
+  data() {
+    return {
+      count: 0
+    }
+  }
+}).mount('#app')
import { createApp } from 'vue'
+
+createApp({
+  data() {
+    return {
+      count: 0
+    }
+  }
+}).mount('#app')

feature

  • vue3的模板中可以有多个根节点

  • api类型不同,vue2使用选项式api,vue3使用组合式api

  • 定义变量和方法方式不同:vue2定义在data和methods节点中,vue3使用setup()方法,此方法在组件初始化构造的时候触发

    • 从vue引入reactive

    • 使用reactive()方法声明数据为响应式数据const state = reactive({ count: 0 })

    • <script setup>:顶层的导入和变量声明可在同一组件的模板中直接使用。可以理解为模板中的表达式和 script setup中的代码处在同一个作用域中。

    • 使用ref()定义响应式变量,ref() 将传入参数的值包装为一个带 .value 属性的 ref 对象:

      js
      import { ref } from 'vue'
      +
      +const count = ref(0)
      import { ref } from 'vue'
      +
      +const count = ref(0)

      和响应式对象的属性类似,ref 的 .value 属性也是响应式的。同时,当值为对象类型时,会用 reactive() 自动转换它的 .value

      一个包含对象类型值的 ref 可以响应式地替换整个对象:

      js

      const objectRef = ref({ count: 0 })
      +
      +// 这是响应式的替换
      +objectRef.value = { count: 1 }
      const objectRef = ref({ count: 0 })
      +
      +// 这是响应式的替换
      +objectRef.value = { count: 1 }

    响应式

    https://cn.vuejs.org/guide/essentials/reactivity-fundamentals.html

  • 类和样式绑定

  • 生命周期变化

响应式对象

  • ref()

    • 标注类型

      • typescript
        import type { Ref } from 'vue'
        +
        +const count: Ref<number> = ref(0)
        import type { Ref } from 'vue'
        +
        +const count: Ref<number> = ref(0)
    • 当 ref 在模板中作为顶层属性被访问时,它们会被自动“解包”,所以不需要使用 .value

    • 当一个 ref 被嵌套在一个深层响应式对象中,作为属性被访问或更改时,它会自动解包

`,181);function g(n,m,v,b,C,A){return e(),t("div",null,[r,s("ul",null,[E,s("li",null,[a(l()+" ",1),s("ul",null,[y,s("li",null,[s("code",null,"

性别 "+l(n.gender)+"

",1)]),i])]),d]),u,s("ul",null,[s("li",null,[s("code",null,"

"+l(n.message|n.capitalize)+"

",1),a(":调用captitalize过滤器,对message进行格式化")]),h]),F])}const f=o(c,[["render",g]]);export{k as __pageData,f as default}; diff --git a/assets/frontend_framework_Vue.md.98b6e4a2.lean.js b/assets/frontend_framework_Vue.md.98b6e4a2.lean.js new file mode 100644 index 000000000..88efea8aa --- /dev/null +++ b/assets/frontend_framework_Vue.md.98b6e4a2.lean.js @@ -0,0 +1 @@ +import{_ as o,o as e,c as t,k as s,a,t as l,Q as p}from"./chunks/framework.dc1f4608.js";const k=JSON.parse('{"title":"Vue","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/framework/Vue.md","filePath":"frontend/framework/Vue.md","lastUpdated":1708510102000}'),c={name:"frontend/framework/Vue.md"},r=p("",12),E=s("li",null,[a("v-test "),s("ul",null,[s("li",null,[s("code",null,"

"),a(":把username值渲染到p标签中")]),s("li",null,[s("code",null,"

性别

"),a(":把gender值渲染到p标签中,原有的值会被覆盖")])])],-1),y=s("li",null,"插值表达式(Mustache),专门用来解决v-text会覆盖默认文本内容的问题,不能用在属性上",-1),i=s("li",null,"支持javascript表达式",-1),d=s("li",null,[a("v-html "),s("ul",null,[s("li",null,"把包含HTML标签的字符串渲染为页面的HTML元素")])],-1),u=p("",22),h=s("li",null,[s("code",null,'
性别 "+l(n.gender)+"

",1)]),i])]),d]),u,s("ul",null,[s("li",null,[s("code",null,"

"+l(n.message|n.capitalize)+"

",1),a(":调用captitalize过滤器,对message进行格式化")]),h]),F])}const f=o(c,[["render",g]]);export{k as __pageData,f as default}; diff --git a/assets/frontend_index.md.01cb292c.js b/assets/frontend_index.md.01cb292c.js new file mode 100644 index 000000000..97eb7a322 --- /dev/null +++ b/assets/frontend_index.md.01cb292c.js @@ -0,0 +1 @@ +import{_ as t,o as n,c as a,k as e,a as o}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"Frontend","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/index.md","filePath":"frontend/index.md","lastUpdated":1708510102000}'),r={name:"frontend/index.md"},d=e("h1",{id:"frontend",tabindex:"-1"},[o("Frontend "),e("a",{class:"header-anchor",href:"#frontend","aria-label":'Permalink to "Frontend"'},"​")],-1),s=e("p",null,"没点前端开发能力自己想写点东西太难了,总不能全写命令行工具吧orz。。。",-1),c=[d,s];function i(_,l,f,p,h,m){return n(),a("div",null,c)}const k=t(r,[["render",i]]);export{u as __pageData,k as default}; diff --git a/assets/frontend_index.md.01cb292c.lean.js b/assets/frontend_index.md.01cb292c.lean.js new file mode 100644 index 000000000..97eb7a322 --- /dev/null +++ b/assets/frontend_index.md.01cb292c.lean.js @@ -0,0 +1 @@ +import{_ as t,o as n,c as a,k as e,a as o}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"Frontend","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/index.md","filePath":"frontend/index.md","lastUpdated":1708510102000}'),r={name:"frontend/index.md"},d=e("h1",{id:"frontend",tabindex:"-1"},[o("Frontend "),e("a",{class:"header-anchor",href:"#frontend","aria-label":'Permalink to "Frontend"'},"​")],-1),s=e("p",null,"没点前端开发能力自己想写点东西太难了,总不能全写命令行工具吧orz。。。",-1),c=[d,s];function i(_,l,f,p,h,m){return n(),a("div",null,c)}const k=t(r,[["render",i]]);export{u as __pageData,k as default}; diff --git "a/assets/frontend_others_ElementPlus el-upload\346\272\220\347\240\201\345\210\206\346\236\220.md.6309dbd0.js" "b/assets/frontend_others_ElementPlus el-upload\346\272\220\347\240\201\345\210\206\346\236\220.md.6309dbd0.js" new file mode 100644 index 000000000..71454d5b1 --- /dev/null +++ "b/assets/frontend_others_ElementPlus el-upload\346\272\220\347\240\201\345\210\206\346\236\220.md.6309dbd0.js" @@ -0,0 +1,603 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const C=JSON.parse('{"title":"ElementPlus el-upload源码分析","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/others/ElementPlus el-upload源码分析.md","filePath":"frontend/others/ElementPlus el-upload源码分析.md","lastUpdated":1708510102000}'),p={name:"frontend/others/ElementPlus el-upload源码分析.md"},o=l(`

ElementPlus el-upload源码分析

背景

使用el-upload实现限制上传文件个数、超出数量后后者覆盖前者、自动上传且自行实现上传文件请求,文档写的不清不楚,试了半天都不行,还是看了源码才明白el-upload组件的整个流程。

https://github.com/ElemeFE/element/blob/dev/packages/upload/src/index.vue

https://github.com/ElemeFE/element/blob/dev/packages/upload/src/upload.vue

分析

  • 入口:index.vue的render函数
  • uploadData定义了参数结构,用于向子组件传参数,props中的几个属性就是el-upload标签的钩子
js
// index.vue
+render(h) {
+    let uploadList;
+
+    if (this.showFileList) {
+      uploadList = (
+        <UploadList
+          disabled={this.uploadDisabled}
+          listType={this.listType}
+          files={this.uploadFiles}
+          on-remove={this.handleRemove}
+          handlePreview={this.onPreview}>
+          {
+            (props) => {
+              if (this.$scopedSlots.file) {
+                return this.$scopedSlots.file({
+                  file: props.file
+                });
+              }
+            }
+          }
+        </UploadList>
+      );
+    }
+
+    const uploadData = {
+      props: {
+        type: this.type,
+        drag: this.drag,
+        action: this.action,
+        multiple: this.multiple,
+        'before-upload': this.beforeUpload,
+        'with-credentials': this.withCredentials,
+        headers: this.headers,
+        name: this.name,
+        data: this.data,
+        accept: this.accept,
+        fileList: this.uploadFiles,
+        autoUpload: this.autoUpload,
+        listType: this.listType,
+        disabled: this.uploadDisabled,
+        limit: this.limit,
+        'on-exceed': this.onExceed,
+        'on-start': this.handleStart,
+        'on-progress': this.handleProgress,
+        'on-success': this.handleSuccess,
+        'on-error': this.handleError,
+        'on-preview': this.onPreview,
+        'on-remove': this.handleRemove,
+        'http-request': this.httpRequest
+      },
+      ref: 'upload-inner'
+    };
+
+    const trigger = this.$slots.trigger || this.$slots.default;
+    const uploadComponent = <upload {...uploadData}>{trigger}</upload>;
+
+    return (
+      <div>
+        { this.listType === 'picture-card' ? uploadList : ''}
+        {
+          this.$slots.trigger
+            ? [uploadComponent, this.$slots.default]
+            : uploadComponent
+        }
+        {this.$slots.tip}
+        { this.listType !== 'picture-card' ? uploadList : ''}
+      </div>
+    );
+  }
// index.vue
+render(h) {
+    let uploadList;
+
+    if (this.showFileList) {
+      uploadList = (
+        <UploadList
+          disabled={this.uploadDisabled}
+          listType={this.listType}
+          files={this.uploadFiles}
+          on-remove={this.handleRemove}
+          handlePreview={this.onPreview}>
+          {
+            (props) => {
+              if (this.$scopedSlots.file) {
+                return this.$scopedSlots.file({
+                  file: props.file
+                });
+              }
+            }
+          }
+        </UploadList>
+      );
+    }
+
+    const uploadData = {
+      props: {
+        type: this.type,
+        drag: this.drag,
+        action: this.action,
+        multiple: this.multiple,
+        'before-upload': this.beforeUpload,
+        'with-credentials': this.withCredentials,
+        headers: this.headers,
+        name: this.name,
+        data: this.data,
+        accept: this.accept,
+        fileList: this.uploadFiles,
+        autoUpload: this.autoUpload,
+        listType: this.listType,
+        disabled: this.uploadDisabled,
+        limit: this.limit,
+        'on-exceed': this.onExceed,
+        'on-start': this.handleStart,
+        'on-progress': this.handleProgress,
+        'on-success': this.handleSuccess,
+        'on-error': this.handleError,
+        'on-preview': this.onPreview,
+        'on-remove': this.handleRemove,
+        'http-request': this.httpRequest
+      },
+      ref: 'upload-inner'
+    };
+
+    const trigger = this.$slots.trigger || this.$slots.default;
+    const uploadComponent = <upload {...uploadData}>{trigger}</upload>;
+
+    return (
+      <div>
+        { this.listType === 'picture-card' ? uploadList : ''}
+        {
+          this.$slots.trigger
+            ? [uploadComponent, this.$slots.default]
+            : uploadComponent
+        }
+        {this.$slots.tip}
+        { this.listType !== 'picture-card' ? uploadList : ''}
+      </div>
+    );
+  }
  • 渲染upload子组件,下面是upload组件的render函数
js
// upload.vue
+render(h) {
+    let {
+      handleClick,
+      drag,
+      name,
+      handleChange,
+      multiple,
+      accept,
+      listType,
+      uploadFiles,
+      disabled,
+      handleKeydown
+    } = this;
+    const data = {
+      class: {
+        'el-upload': true
+      },
+      on: {
+        click: handleClick,
+        keydown: handleKeydown
+      }
+    };
+    data.class[\`el-upload--\${listType}\`] = true;
+    return (
+      <div {...data} tabindex="0" >
+        {
+          drag
+            ? <upload-dragger disabled={disabled} on-file={uploadFiles}>{this.$slots.default}</upload-dragger>
+            : this.$slots.default
+        }
+        <input class="el-upload__input" type="file" ref="input" name={name} on-change={handleChange} multiple={multiple} accept={accept}></input>
+      </div>
+    );
+  }
// upload.vue
+render(h) {
+    let {
+      handleClick,
+      drag,
+      name,
+      handleChange,
+      multiple,
+      accept,
+      listType,
+      uploadFiles,
+      disabled,
+      handleKeydown
+    } = this;
+    const data = {
+      class: {
+        'el-upload': true
+      },
+      on: {
+        click: handleClick,
+        keydown: handleKeydown
+      }
+    };
+    data.class[\`el-upload--\${listType}\`] = true;
+    return (
+      <div {...data} tabindex="0" >
+        {
+          drag
+            ? <upload-dragger disabled={disabled} on-file={uploadFiles}>{this.$slots.default}</upload-dragger>
+            : this.$slots.default
+        }
+        <input class="el-upload__input" type="file" ref="input" name={name} on-change={handleChange} multiple={multiple} accept={accept}></input>
+      </div>
+    );
+  }
  • 主要关注<input class="el-upload__input" type="file" ref="input" name={name} on-change={handleChange} multiple={multiple} accept={accept}></input>这个jsx代码中绑定了onChange时间,调用函数是handleChange

  • // upload.vue
    +handleChange(ev) {
    +      const files = ev.target.files;
    +
    +      if (!files) return;
    +      this.uploadFiles(files);
    +}
    // upload.vue
    +handleChange(ev) {
    +      const files = ev.target.files;
    +
    +      if (!files) return;
    +      this.uploadFiles(files);
    +}
  • 可以看到handleChange直接调用了uploadFiles函数,而uploadFiles函数就是我们要追溯的内容了

js
uploadFiles(files) {
+  if (this.limit && this.fileList.length + files.length > this.limit) {
+    this.onExceed && this.onExceed(files, this.fileList);
+    return;
+  }
+
+  let postFiles = Array.prototype.slice.call(files);
+  if (!this.multiple) { postFiles = postFiles.slice(0, 1); }
+
+  if (postFiles.length === 0) { return; }
+
+  postFiles.forEach(rawFile => {
+    this.onStart(rawFile);
+    if (this.autoUpload) this.upload(rawFile);
+  });
+},
+upload(rawFile) {
+  this.$refs.input.value = null;
+
+  if (!this.beforeUpload) {
+    return this.post(rawFile);
+  }
+
+  const before = this.beforeUpload(rawFile);
+  if (before && before.then) {
+    before.then(processedFile => {
+      const fileType = Object.prototype.toString.call(processedFile);
+
+      if (fileType === '[object File]' || fileType === '[object Blob]') {
+        if (fileType === '[object Blob]') {
+          processedFile = new File([processedFile], rawFile.name, {
+            type: rawFile.type
+          });
+        }
+        for (const p in rawFile) {
+          if (rawFile.hasOwnProperty(p)) {
+            processedFile[p] = rawFile[p];
+          }
+        }
+        this.post(processedFile);
+      } else {
+        this.post(rawFile);
+      }
+    }, () => {
+      this.onRemove(null, rawFile);
+    });
+  } else if (before !== false) {
+    this.post(rawFile);
+  } else {
+    this.onRemove(null, rawFile);
+  }
+}
uploadFiles(files) {
+  if (this.limit && this.fileList.length + files.length > this.limit) {
+    this.onExceed && this.onExceed(files, this.fileList);
+    return;
+  }
+
+  let postFiles = Array.prototype.slice.call(files);
+  if (!this.multiple) { postFiles = postFiles.slice(0, 1); }
+
+  if (postFiles.length === 0) { return; }
+
+  postFiles.forEach(rawFile => {
+    this.onStart(rawFile);
+    if (this.autoUpload) this.upload(rawFile);
+  });
+},
+upload(rawFile) {
+  this.$refs.input.value = null;
+
+  if (!this.beforeUpload) {
+    return this.post(rawFile);
+  }
+
+  const before = this.beforeUpload(rawFile);
+  if (before && before.then) {
+    before.then(processedFile => {
+      const fileType = Object.prototype.toString.call(processedFile);
+
+      if (fileType === '[object File]' || fileType === '[object Blob]') {
+        if (fileType === '[object Blob]') {
+          processedFile = new File([processedFile], rawFile.name, {
+            type: rawFile.type
+          });
+        }
+        for (const p in rawFile) {
+          if (rawFile.hasOwnProperty(p)) {
+            processedFile[p] = rawFile[p];
+          }
+        }
+        this.post(processedFile);
+      } else {
+        this.post(rawFile);
+      }
+    }, () => {
+      this.onRemove(null, rawFile);
+    });
+  } else if (before !== false) {
+    this.post(rawFile);
+  } else {
+    this.onRemove(null, rawFile);
+  }
+}
  • 首先判断是否超过限制,如果没有则会遍历files调用onStart函数,onStart函数是在index.vue中定义的,做了三件事
    • 基于原始file构建新的file对象,基于当前时间增加uid,如果是图片展示模式,还会创建一个url做展示
    • 将新的file对象push进上传文件的数组uploadFiles中
    • 将处理过的uploadFiles传递给on-change钩子函数(文件状态改变,添加文件、上传成功和上传失败时都会被调用)
js
handleStart(rawFile) {
+  rawFile.uid = Date.now() + this.tempIndex++;
+  let file = {
+    status: 'ready',
+    name: rawFile.name,
+    size: rawFile.size,
+    percentage: 0,
+    uid: rawFile.uid,
+    raw: rawFile
+  };
+
+  if (this.listType === 'picture-card' || this.listType === 'picture') {
+    try {
+      file.url = URL.createObjectURL(rawFile);
+    } catch (err) {
+      console.error('[Element Error][Upload]', err);
+      return;
+    }
+  }
+
+  this.uploadFiles.push(file);
+  this.onChange(file, this.uploadFiles);
+}
handleStart(rawFile) {
+  rawFile.uid = Date.now() + this.tempIndex++;
+  let file = {
+    status: 'ready',
+    name: rawFile.name,
+    size: rawFile.size,
+    percentage: 0,
+    uid: rawFile.uid,
+    raw: rawFile
+  };
+
+  if (this.listType === 'picture-card' || this.listType === 'picture') {
+    try {
+      file.url = URL.createObjectURL(rawFile);
+    } catch (err) {
+      console.error('[Element Error][Upload]', err);
+      return;
+    }
+  }
+
+  this.uploadFiles.push(file);
+  this.onChange(file, this.uploadFiles);
+}
  • onStart执行后会判断是否开启了自动上传,如果开启则会调用upload函数
    • upload中会调用判断是否定义了beforeUpload事件,如果没有直接调用post函数上传
    • 如果定义了beforeUpload则会执行该函数,并根据该方法返回的结果决定如何处理文件
      • 如果 beforeUpload 返回一个 Promise,则等待 Promise 执行完毕,并获取其返回值 processedFile。如果 processedFile 是一个 File 或 Blob 类型,则使用它来替换原始的文件并保留原始文件的属性,最后调用 post 方法上传替换后的文件;否则,仍然使用原始的文件上传。
      • 如果 beforeUpload返回 false,则直接调用 onRemove 方法移除文件。
      • 如果 beforeUpload返回其他非 false 的值,则直接上传原始文件。
js
upload(rawFile) {
+  this.$refs.input.value = null;
+
+  if (!this.beforeUpload) {
+    return this.post(rawFile);
+  }
+
+  const before = this.beforeUpload(rawFile);
+  if (before && before.then) {
+    before.then(processedFile => {
+      const fileType = Object.prototype.toString.call(processedFile);
+
+      if (fileType === '[object File]' || fileType === '[object Blob]') {
+        if (fileType === '[object Blob]') {
+          processedFile = new File([processedFile], rawFile.name, {
+            type: rawFile.type
+          });
+        }
+        for (const p in rawFile) {
+          if (rawFile.hasOwnProperty(p)) {
+            processedFile[p] = rawFile[p];
+          }
+        }
+        this.post(processedFile);
+      } else {
+        this.post(rawFile);
+      }
+    }, () => {
+      this.onRemove(null, rawFile);
+    });
+  } else if (before !== false) {
+    this.post(rawFile);
+  } else {
+    this.onRemove(null, rawFile);
+  }
+}
upload(rawFile) {
+  this.$refs.input.value = null;
+
+  if (!this.beforeUpload) {
+    return this.post(rawFile);
+  }
+
+  const before = this.beforeUpload(rawFile);
+  if (before && before.then) {
+    before.then(processedFile => {
+      const fileType = Object.prototype.toString.call(processedFile);
+
+      if (fileType === '[object File]' || fileType === '[object Blob]') {
+        if (fileType === '[object Blob]') {
+          processedFile = new File([processedFile], rawFile.name, {
+            type: rawFile.type
+          });
+        }
+        for (const p in rawFile) {
+          if (rawFile.hasOwnProperty(p)) {
+            processedFile[p] = rawFile[p];
+          }
+        }
+        this.post(processedFile);
+      } else {
+        this.post(rawFile);
+      }
+    }, () => {
+      this.onRemove(null, rawFile);
+    });
+  } else if (before !== false) {
+    this.post(rawFile);
+  } else {
+    this.onRemove(null, rawFile);
+  }
+}
  • post函数中的上传方法是httpRequest(options),我们可以使用el-upload标签的http-request替换成自己的实现
js
post(rawFile) {
+  const { uid } = rawFile;
+  const options = {
+    headers: this.headers,
+    withCredentials: this.withCredentials,
+    file: rawFile,
+    data: this.data,
+    filename: this.name,
+    action: this.action,
+    onProgress: e => {
+      this.onProgress(e, rawFile);
+    },
+    onSuccess: res => {
+      this.onSuccess(res, rawFile);
+      delete this.reqs[uid];
+    },
+    onError: err => {
+      this.onError(err, rawFile);
+      delete this.reqs[uid];
+    }
+  };
+  const req = this.httpRequest(options);
+  this.reqs[uid] = req;
+  if (req && req.then) {
+    req.then(options.onSuccess, options.onError);
+  }
+},
+handleClick() {
+  if (!this.disabled) {
+    this.$refs.input.value = null;
+    this.$refs.input.click();
+  }
+},
+handleKeydown(e) {
+  if (e.target !== e.currentTarget) return;
+  if (e.keyCode === 13 || e.keyCode === 32) {
+    this.handleClick();
+  }
+}
post(rawFile) {
+  const { uid } = rawFile;
+  const options = {
+    headers: this.headers,
+    withCredentials: this.withCredentials,
+    file: rawFile,
+    data: this.data,
+    filename: this.name,
+    action: this.action,
+    onProgress: e => {
+      this.onProgress(e, rawFile);
+    },
+    onSuccess: res => {
+      this.onSuccess(res, rawFile);
+      delete this.reqs[uid];
+    },
+    onError: err => {
+      this.onError(err, rawFile);
+      delete this.reqs[uid];
+    }
+  };
+  const req = this.httpRequest(options);
+  this.reqs[uid] = req;
+  if (req && req.then) {
+    req.then(options.onSuccess, options.onError);
+  }
+},
+handleClick() {
+  if (!this.disabled) {
+    this.$refs.input.value = null;
+    this.$refs.input.click();
+  }
+},
+handleKeydown(e) {
+  if (e.target !== e.currentTarget) return;
+  if (e.keyCode === 13 || e.keyCode === 32) {
+    this.handleClick();
+  }
+}

结果

上面分析的是如果没有超过limit限制的情况,而我要实现的就是在超过限制之后依然能够自动上传。

回归到代码,uploadFiles函数在执行了onExceed钩子之后直接就return了,所以下面一直到上传的流程都需要我们自己去调用

js
uploadFiles(files) {
+  if (this.limit && this.fileList.length + files.length > this.limit) {
+    this.onExceed && this.onExceed(files, this.fileList);
+    return;
+  }
+  //...
+}
uploadFiles(files) {
+  if (this.limit && this.fileList.length + files.length > this.limit) {
+    this.onExceed && this.onExceed(files, this.fileList);
+    return;
+  }
+  //...
+}

在回顾下我的目标:限制上传文件个数为一个、超出数量后后者覆盖前者、自动上传且自行实现上传文件请求

所以:终极目标是执行完onExceed后能调用上传的函数,el-upload暴露给我们的函数是submit()

js
// index.vue
+submit() {
+  this.uploadFiles
+    .filter(file => file.status === 'ready')
+    .forEach(file => {
+      this.$refs['upload-inner'].upload(file.raw);
+    });
+}
// index.vue
+submit() {
+  this.uploadFiles
+    .filter(file => file.status === 'ready')
+    .forEach(file => {
+      this.$refs['upload-inner'].upload(file.raw);
+    });
+}

可以看到这个函数是将uploadFiles中的文件都调用了一次upload,所以我们要在onExceed函数中要做的事:

  1. 在函数中把原有文件清除掉,然后替换为最新的文件
  2. el-upload暴露的清除文件的方法: clearFiles
js
clearFiles() {
+  this.uploadFiles = [];
+}
clearFiles() {
+  this.uploadFiles = [];
+}
  1. 把最新的文件push到uploadFiles中,这里就要用到上面分析到的handleStart方法,而且这个方法中还会调用onChange钩子
  2. 文件已经push到uploadFiles中,所以只需要调用submit()函数即可

大致代码

vue
<template>
+	<el-upload 
+      action="#" 
+      ref="upload" 
+      v-model:file-list="fileList"
+      :limit="Number(1)" 
+      :on-exceed="handleExceed" 
+      :http-request="handleUpload" 
+      list-type="picture-card"
+    	:auto-upload="true" >
+  </el-upload>
+</template>
+
+<script setup lang="ts">
+import type { UploadProps, UploadInstance, UploadRawFile } from 'element-plus'
+const upload = ref<UploadInstance>()
+/**
+ * 超过限制后调用的方法
+ * @param files
+ */
+const handleExceed: UploadProps['onExceed'] = (files) => {
+   // 清除所有文件
+    upload.value!.clearFiles()
+    const file = files[0] as UploadRawFile
+    file.uid = genFileId()
+    /**
+     * 调用handleStart把file push到uploadFiles中 -> handleStart会调用onChange方法
+     */
+    upload.value!.handleStart(file)
+    upload.value!.submit()
+}
+</script>
<template>
+	<el-upload 
+      action="#" 
+      ref="upload" 
+      v-model:file-list="fileList"
+      :limit="Number(1)" 
+      :on-exceed="handleExceed" 
+      :http-request="handleUpload" 
+      list-type="picture-card"
+    	:auto-upload="true" >
+  </el-upload>
+</template>
+
+<script setup lang="ts">
+import type { UploadProps, UploadInstance, UploadRawFile } from 'element-plus'
+const upload = ref<UploadInstance>()
+/**
+ * 超过限制后调用的方法
+ * @param files
+ */
+const handleExceed: UploadProps['onExceed'] = (files) => {
+   // 清除所有文件
+    upload.value!.clearFiles()
+    const file = files[0] as UploadRawFile
+    file.uid = genFileId()
+    /**
+     * 调用handleStart把file push到uploadFiles中 -> handleStart会调用onChange方法
+     */
+    upload.value!.handleStart(file)
+    upload.value!.submit()
+}
+</script>
`,30),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const u=s(p,[["render",t]]);export{C as __pageData,u as default}; diff --git "a/assets/frontend_others_ElementPlus el-upload\346\272\220\347\240\201\345\210\206\346\236\220.md.6309dbd0.lean.js" "b/assets/frontend_others_ElementPlus el-upload\346\272\220\347\240\201\345\210\206\346\236\220.md.6309dbd0.lean.js" new file mode 100644 index 000000000..9c6f91a49 --- /dev/null +++ "b/assets/frontend_others_ElementPlus el-upload\346\272\220\347\240\201\345\210\206\346\236\220.md.6309dbd0.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const C=JSON.parse('{"title":"ElementPlus el-upload源码分析","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/others/ElementPlus el-upload源码分析.md","filePath":"frontend/others/ElementPlus el-upload源码分析.md","lastUpdated":1708510102000}'),p={name:"frontend/others/ElementPlus el-upload源码分析.md"},o=l("",30),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const u=s(p,[["render",t]]);export{C as __pageData,u as default}; diff --git a/assets/frontend_others_HackingWithSwift-1.md.355cc977.js b/assets/frontend_others_HackingWithSwift-1.md.355cc977.js new file mode 100644 index 000000000..e742b5176 --- /dev/null +++ b/assets/frontend_others_HackingWithSwift-1.md.355cc977.js @@ -0,0 +1,853 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"HackingWithSwift-1","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/others/HackingWithSwift-1.md","filePath":"frontend/others/HackingWithSwift-1.md","lastUpdated":1708510102000}'),p={name:"frontend/others/HackingWithSwift-1.md"},o=l(`

HackingWithSwift-1

https://www.youtube.com/playlist?list=PLuoeXyslFTuZRi4q4VT6lZKxYbr7so1Mr

WeSplitApp

核心代码

swift
struct ContentView: View {
+    @State private var checkAmount = 0.0
+    @State private var numberOfPeople = 0
+    @State private var tipPercentage = 20
+    @FocusState private var amountIsFocused: Bool
+    
+    private var totalPerPerson: Double {
+        let peopleCount = Double(numberOfPeople + 2)
+        let tipSelection = Double(tipPercentage)
+        return checkAmount * tipSelection / peopleCount / 100
+    }
+    
+    let tipPercentages = [10, 15, 20, 25, 0]
+    
+    var body: some View {
+        NavigationView {
+            Form {
+                Section("") {
+                    TextField("Amount", value: $checkAmount, format: .currency(code: Locale.current.currencyCode ?? "USD"))
+                        .keyboardType(.decimalPad)
+                        .focused($amountIsFocused)
+                    
+                    Picker("Number of people", selection: $numberOfPeople) {
+                        ForEach(2..<100) {
+                            Text("\\($0) people")
+                        }
+                    }
+                }
+                
+                Section {
+                    Picker("Tip percentage", selection: $tipPercentage) {
+                        ForEach(tipPercentages, id: \\.self) {
+                            Text($0, format: .percent)
+                        }
+                    }
+                    .pickerStyle(.segmented)
+                }header: {
+                    Text("How much tip do you want to leave")
+                }
+                
+                Section {
+                    Text(totalPerPerson, format: .currency(code: Locale.current.currencyCode ?? "USD"))
+                }
+            }
+            .navigationTitle("WeSplit")
+            .toolbar {
+                ToolbarItemGroup(placement: .keyboard) {
+                    Spacer()
+                    Button("Done") {
+                        amountIsFocused = false
+                    }
+                }
+            }
+        }
+    }
+}
struct ContentView: View {
+    @State private var checkAmount = 0.0
+    @State private var numberOfPeople = 0
+    @State private var tipPercentage = 20
+    @FocusState private var amountIsFocused: Bool
+    
+    private var totalPerPerson: Double {
+        let peopleCount = Double(numberOfPeople + 2)
+        let tipSelection = Double(tipPercentage)
+        return checkAmount * tipSelection / peopleCount / 100
+    }
+    
+    let tipPercentages = [10, 15, 20, 25, 0]
+    
+    var body: some View {
+        NavigationView {
+            Form {
+                Section("") {
+                    TextField("Amount", value: $checkAmount, format: .currency(code: Locale.current.currencyCode ?? "USD"))
+                        .keyboardType(.decimalPad)
+                        .focused($amountIsFocused)
+                    
+                    Picker("Number of people", selection: $numberOfPeople) {
+                        ForEach(2..<100) {
+                            Text("\\($0) people")
+                        }
+                    }
+                }
+                
+                Section {
+                    Picker("Tip percentage", selection: $tipPercentage) {
+                        ForEach(tipPercentages, id: \\.self) {
+                            Text($0, format: .percent)
+                        }
+                    }
+                    .pickerStyle(.segmented)
+                }header: {
+                    Text("How much tip do you want to leave")
+                }
+                
+                Section {
+                    Text(totalPerPerson, format: .currency(code: Locale.current.currencyCode ?? "USD"))
+                }
+            }
+            .navigationTitle("WeSplit")
+            .toolbar {
+                ToolbarItemGroup(placement: .keyboard) {
+                    Spacer()
+                    Button("Done") {
+                        amountIsFocused = false
+                    }
+                }
+            }
+        }
+    }
+}

知识点

  • Form

  • TextField & format: .currency(code: Locale.current.currencyCode ?? "USD")

  • Picker

  • Section

  • toolbar修饰符

效果

image-20220711231438513

GuessTheFlag

核心代码

swift
struct ContentView: View {
+    @State private var showScores = false
+    @State private var scoreTitle = ""
+    
+    @State private var scores = 0
+    
+    @State private var countries = ["Estonia", "France", "Germany", "Ireland", "Italy", "Nigeria",
+                             "Poland", "Russia", "Spain", "UK", "US"].shuffled()
+    @State var correctAnswer = Int.random(in: 0...2)
+    
+    var body: some View {
+        ZStack {
+            //AngularGradient(gradient: Gradient(colors: [.red, .yellow, .green,.blue, .purple,.red]), center: .center)
+            RadialGradient(stops: [
+                .init(color: Color(red: 0.1, green: 0.2, blue: 0.45), location: 0.3),
+                .init(color: Color(red: 0.76, green: 0.15, blue: 0.26), location: 0.3)
+            ], center: .top, startRadius: 200, endRadius: 700)
+                .ignoresSafeArea()
+            
+            VStack {
+                
+                Spacer()
+
+                Text("Guess the flag")
+                    .font(.largeTitle.weight(.bold))
+                    .foregroundColor(.white)
+                
+                VStack(spacing: 15) {
+                    VStack{
+                        Text("Tap the flag of")
+                            .foregroundStyle(.secondary)
+                            .font(.subheadline.weight(.heavy))
+                        Text(countries[correctAnswer])
+                            .font(.largeTitle.weight(.semibold))
+                    }
+                    
+                    ForEach(0..<3) {number in
+                        Button {
+                            flagTapper(number)
+                        }label: {
+                            Image(countries[number])
+                                .renderingMode(.original)
+                                .clipShape(Capsule())
+                        }
+                    }
+                }
+                .frame(maxWidth: .infinity)
+                .padding(.vertical, 20)
+                .background(.regularMaterial)
+                .clipShape(RoundedRectangle(cornerRadius: 20))
+                
+                Spacer()
+                
+                Text("Score \\(scores)")
+                    .foregroundColor(.white)
+                    .font(.title.bold())
+                Spacer()
+            }
+            .padding()
+        }
+        .alert(scoreTitle, isPresented: $showScores) {
+            Button("Continue", action: askQuestion)
+        } message: {
+            Text("Your score is \\(scores)")
+        }
+    }
+    func flagTapper(_ number: Int) {
+        if number == correctAnswer {
+            scoreTitle = "Correct"
+            scores += 10
+        } else {
+            scoreTitle = "Wrong"
+        }
+        showScores = true
+    }
+    
+    func askQuestion() {
+        
+        countries.shuffle()
+        correctAnswer = Int.random(in: 0...2)
+    }
+}
struct ContentView: View {
+    @State private var showScores = false
+    @State private var scoreTitle = ""
+    
+    @State private var scores = 0
+    
+    @State private var countries = ["Estonia", "France", "Germany", "Ireland", "Italy", "Nigeria",
+                             "Poland", "Russia", "Spain", "UK", "US"].shuffled()
+    @State var correctAnswer = Int.random(in: 0...2)
+    
+    var body: some View {
+        ZStack {
+            //AngularGradient(gradient: Gradient(colors: [.red, .yellow, .green,.blue, .purple,.red]), center: .center)
+            RadialGradient(stops: [
+                .init(color: Color(red: 0.1, green: 0.2, blue: 0.45), location: 0.3),
+                .init(color: Color(red: 0.76, green: 0.15, blue: 0.26), location: 0.3)
+            ], center: .top, startRadius: 200, endRadius: 700)
+                .ignoresSafeArea()
+            
+            VStack {
+                
+                Spacer()
+
+                Text("Guess the flag")
+                    .font(.largeTitle.weight(.bold))
+                    .foregroundColor(.white)
+                
+                VStack(spacing: 15) {
+                    VStack{
+                        Text("Tap the flag of")
+                            .foregroundStyle(.secondary)
+                            .font(.subheadline.weight(.heavy))
+                        Text(countries[correctAnswer])
+                            .font(.largeTitle.weight(.semibold))
+                    }
+                    
+                    ForEach(0..<3) {number in
+                        Button {
+                            flagTapper(number)
+                        }label: {
+                            Image(countries[number])
+                                .renderingMode(.original)
+                                .clipShape(Capsule())
+                        }
+                    }
+                }
+                .frame(maxWidth: .infinity)
+                .padding(.vertical, 20)
+                .background(.regularMaterial)
+                .clipShape(RoundedRectangle(cornerRadius: 20))
+                
+                Spacer()
+                
+                Text("Score \\(scores)")
+                    .foregroundColor(.white)
+                    .font(.title.bold())
+                Spacer()
+            }
+            .padding()
+        }
+        .alert(scoreTitle, isPresented: $showScores) {
+            Button("Continue", action: askQuestion)
+        } message: {
+            Text("Your score is \\(scores)")
+        }
+    }
+    func flagTapper(_ number: Int) {
+        if number == correctAnswer {
+            scoreTitle = "Correct"
+            scores += 10
+        } else {
+            scoreTitle = "Wrong"
+        }
+        showScores = true
+    }
+    
+    func askQuestion() {
+        
+        countries.shuffle()
+        correctAnswer = Int.random(in: 0...2)
+    }
+}

知识点

  • Gradient
  • alert

效果

image-20220711232134199

BetterRest

核心代码

swift
import CoreML
+import SwiftUI
+
+struct ContentView: View {
+    @State private var wakeUp = defaultWakeTime
+    @State private var sleepAmount = 8.0
+    @State private var coffeeAmount = 1
+    
+    @State private var alertTitle = ""
+    @State private var alertMessage = ""
+    @State private var showingAlert = false
+    
+    static var defaultWakeTime: Date {
+        var components = DateComponents()
+        components.hour = 7
+        components.minute = 0
+        return Calendar.current.date(from: components) ?? Date.now
+    }
+    
+    var body: some View {
+        NavigationView {
+            Form {
+                VStack(alignment: .leading, spacing: 0) {
+                    Text("When do you want to wake up?")
+                        .font(.headline)
+                    DatePicker("Please enter atime", selection: $wakeUp, displayedComponents: .hourAndMinute)
+                        .labelsHidden()
+                }
+                
+                
+                VStack(alignment: .leading, spacing: 0) {
+                    Text("Desired amount of sleep")
+                        .font(.headline)
+                    Stepper("\\(sleepAmount.formatted()) hours", value: $sleepAmount, in: 4...12, step: 0.25)
+                }
+                
+                
+                VStack(alignment: .leading, spacing: 0) {
+                    Text("Daily coffee intake")
+                    Stepper(coffeeAmount == 1 ? "1 cup" : "\\(coffeeAmount) cups", value: $coffeeAmount, in: 0...20)
+                }
+                
+            }
+            .navigationTitle("BetterRest")
+            .toolbar {
+                Button("Calculate", action: calculateBedtime)
+            }
+            .alert(alertTitle, isPresented: $showingAlert) {
+                Button("OK") {}
+            }message: {
+                Text(alertMessage)
+            }
+        }
+        
+    }
+    
+    func calculateBedtime() {
+        do {
+            let config = MLModelConfiguration()
+            let model = try SleepCalculator(configuration: config)
+            let components = Calendar.current.dateComponents([.hour, .minute], from: wakeUp)
+            let hour = (components.hour ?? 0) * 60 * 60
+            let minute = (components.minute ?? 0) * 60
+            
+            let prediction = try model.prediction(wake: Double(hour + minute), estimatedSleep: sleepAmount, coffee: Double(coffeeAmount))
+            
+            let sleepTime = wakeUp - prediction.actualSleep
+            alertTitle = "Your ideal bedtime is ..."
+            alertMessage = sleepTime.formatted(date: .omitted, time: .shortened)
+        }catch {
+            alertTitle = "Error"
+            alertMessage = "Sorry, there was a problem calculating your bedtime."
+        }
+        
+        showingAlert = true
+    }
+}
import CoreML
+import SwiftUI
+
+struct ContentView: View {
+    @State private var wakeUp = defaultWakeTime
+    @State private var sleepAmount = 8.0
+    @State private var coffeeAmount = 1
+    
+    @State private var alertTitle = ""
+    @State private var alertMessage = ""
+    @State private var showingAlert = false
+    
+    static var defaultWakeTime: Date {
+        var components = DateComponents()
+        components.hour = 7
+        components.minute = 0
+        return Calendar.current.date(from: components) ?? Date.now
+    }
+    
+    var body: some View {
+        NavigationView {
+            Form {
+                VStack(alignment: .leading, spacing: 0) {
+                    Text("When do you want to wake up?")
+                        .font(.headline)
+                    DatePicker("Please enter atime", selection: $wakeUp, displayedComponents: .hourAndMinute)
+                        .labelsHidden()
+                }
+                
+                
+                VStack(alignment: .leading, spacing: 0) {
+                    Text("Desired amount of sleep")
+                        .font(.headline)
+                    Stepper("\\(sleepAmount.formatted()) hours", value: $sleepAmount, in: 4...12, step: 0.25)
+                }
+                
+                
+                VStack(alignment: .leading, spacing: 0) {
+                    Text("Daily coffee intake")
+                    Stepper(coffeeAmount == 1 ? "1 cup" : "\\(coffeeAmount) cups", value: $coffeeAmount, in: 0...20)
+                }
+                
+            }
+            .navigationTitle("BetterRest")
+            .toolbar {
+                Button("Calculate", action: calculateBedtime)
+            }
+            .alert(alertTitle, isPresented: $showingAlert) {
+                Button("OK") {}
+            }message: {
+                Text(alertMessage)
+            }
+        }
+        
+    }
+    
+    func calculateBedtime() {
+        do {
+            let config = MLModelConfiguration()
+            let model = try SleepCalculator(configuration: config)
+            let components = Calendar.current.dateComponents([.hour, .minute], from: wakeUp)
+            let hour = (components.hour ?? 0) * 60 * 60
+            let minute = (components.minute ?? 0) * 60
+            
+            let prediction = try model.prediction(wake: Double(hour + minute), estimatedSleep: sleepAmount, coffee: Double(coffeeAmount))
+            
+            let sleepTime = wakeUp - prediction.actualSleep
+            alertTitle = "Your ideal bedtime is ..."
+            alertMessage = sleepTime.formatted(date: .omitted, time: .shortened)
+        }catch {
+            alertTitle = "Error"
+            alertMessage = "Sorry, there was a problem calculating your bedtime."
+        }
+        
+        showingAlert = true
+    }
+}

知识点

  • CoreML机器学习

  • DateComponents、DatePicker

  • alert修饰符

  • Stepper

WordScramble

核心代码

swift
struct ContentView: View {
+    @State private var usedWords = [String]()
+    @State private var rootWord = ""
+    @State private var newWord = ""
+    
+    @State private var errorTitle = ""
+    @State private var errorMessage = ""
+    @State private var showingError = false
+    
+    var body: some View {
+        NavigationView {
+            List {
+                Section {
+                    TextField("enter your word", text: $newWord)
+                        .autocapitalization(.none)
+                }
+                
+                Section {
+                    ForEach(usedWords, id: \\.self) { word in
+                        HStack{
+                            Image(systemName: "\\(word.count).circle")
+                            Text(word)
+                        }
+                    }
+                }
+            }
+            .navigationTitle(rootWord)
+            .onSubmit(addNewWord)
+            .onAppear(perform: startGame)
+            .alert(errorTitle, isPresented: $showingError) {
+                Button("OK", role: .cancel) {}
+            }message: {
+                Text(errorMessage)
+            }
+        }
+    }
+    
+    func addNewWord() {
+        let answer = newWord.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
+        guard answer.count > 0 else { return }
+        
+        guard isOriginal(word: answer) else {
+            wordError(title: "Word used already", message: "Be more original")
+            return
+        }
+        
+        guard isPossible(word: answer) else {
+            wordError(title: "word not possible", message: "you can't spell that word from '\\(rootWord)'")
+            return
+        }
+        
+        guard isReal(word: answer) else {
+            wordError(title: "word not recognized", message: "you can't just make them up, you know!")
+            return
+        }
+        
+        
+        withAnimation{
+            usedWords.insert(answer, at: 0)
+        }
+      
+        newWord = ""
+    }
+    
+    func startGame() {
+        if let fileURL = Bundle.main.url(forResource: "start", withExtension: "txt") {
+            if let startWords = try? String(contentsOf: fileURL) {
+                let allWords = startWords.components(separatedBy: "\\n")
+                rootWord = allWords.randomElement() ?? "silkworm"
+                return
+            }
+        }
+        fatalError("Could not load start.txt from bundle")
+    }
+    
+    func isOriginal(word: String) -> Bool {
+        !usedWords.contains(word)
+    }
+    
+    func isPossible(word: String) -> Bool {
+        var tempWord = rootWord
+        
+        for letter in word {
+            if let pos = tempWord.firstIndex(of: letter) {
+                tempWord.remove(at: pos)
+            } else {
+                return false
+            }
+        }
+        return true
+    }
+    
+    func isReal(word: String) -> Bool {
+        let checker = UITextChecker()
+        let range = NSRange(location: 0, length: word.utf16.count)
+        let misspelledRange = checker.rangeOfMisspelledWord(in: word, range: range, startingAt: 0, wrap: false, language: "en")
+        
+        return misspelledRange.location == NSNotFound
+    }
+    
+    func wordError(title: String, message: String) {
+        errorTitle = title
+        errorMessage = message
+        showingError = true
+    }
+}
struct ContentView: View {
+    @State private var usedWords = [String]()
+    @State private var rootWord = ""
+    @State private var newWord = ""
+    
+    @State private var errorTitle = ""
+    @State private var errorMessage = ""
+    @State private var showingError = false
+    
+    var body: some View {
+        NavigationView {
+            List {
+                Section {
+                    TextField("enter your word", text: $newWord)
+                        .autocapitalization(.none)
+                }
+                
+                Section {
+                    ForEach(usedWords, id: \\.self) { word in
+                        HStack{
+                            Image(systemName: "\\(word.count).circle")
+                            Text(word)
+                        }
+                    }
+                }
+            }
+            .navigationTitle(rootWord)
+            .onSubmit(addNewWord)
+            .onAppear(perform: startGame)
+            .alert(errorTitle, isPresented: $showingError) {
+                Button("OK", role: .cancel) {}
+            }message: {
+                Text(errorMessage)
+            }
+        }
+    }
+    
+    func addNewWord() {
+        let answer = newWord.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
+        guard answer.count > 0 else { return }
+        
+        guard isOriginal(word: answer) else {
+            wordError(title: "Word used already", message: "Be more original")
+            return
+        }
+        
+        guard isPossible(word: answer) else {
+            wordError(title: "word not possible", message: "you can't spell that word from '\\(rootWord)'")
+            return
+        }
+        
+        guard isReal(word: answer) else {
+            wordError(title: "word not recognized", message: "you can't just make them up, you know!")
+            return
+        }
+        
+        
+        withAnimation{
+            usedWords.insert(answer, at: 0)
+        }
+      
+        newWord = ""
+    }
+    
+    func startGame() {
+        if let fileURL = Bundle.main.url(forResource: "start", withExtension: "txt") {
+            if let startWords = try? String(contentsOf: fileURL) {
+                let allWords = startWords.components(separatedBy: "\\n")
+                rootWord = allWords.randomElement() ?? "silkworm"
+                return
+            }
+        }
+        fatalError("Could not load start.txt from bundle")
+    }
+    
+    func isOriginal(word: String) -> Bool {
+        !usedWords.contains(word)
+    }
+    
+    func isPossible(word: String) -> Bool {
+        var tempWord = rootWord
+        
+        for letter in word {
+            if let pos = tempWord.firstIndex(of: letter) {
+                tempWord.remove(at: pos)
+            } else {
+                return false
+            }
+        }
+        return true
+    }
+    
+    func isReal(word: String) -> Bool {
+        let checker = UITextChecker()
+        let range = NSRange(location: 0, length: word.utf16.count)
+        let misspelledRange = checker.rangeOfMisspelledWord(in: word, range: range, startingAt: 0, wrap: false, language: "en")
+        
+        return misspelledRange.location == NSNotFound
+    }
+    
+    func wordError(title: String, message: String) {
+        errorTitle = title
+        errorMessage = message
+        showingError = true
+    }
+}

知识点

  • TextField("enter your word", text: $newWord).autocapitalization(.none)

  • onSubmitonAppear修饰符

  • 读取静态资源

    swift
    if let fileURL = Bundle.main.url(forResource: "start", withExtension: "txt") {
    +            if let startWords = try? String(contentsOf: fileURL) {
    +                let allWords = startWords.components(separatedBy: "\\n")
    +                rootWord = allWords.randomElement() ?? "silkworm"
    +                return
    +            }
    +        }
    +        fatalError("Could not load start.txt from bundle")
    if let fileURL = Bundle.main.url(forResource: "start", withExtension: "txt") {
    +            if let startWords = try? String(contentsOf: fileURL) {
    +                let allWords = startWords.components(separatedBy: "\\n")
    +                rootWord = allWords.randomElement() ?? "silkworm"
    +                return
    +            }
    +        }
    +        fatalError("Could not load start.txt from bundle")
  • UITextChecker()

效果

image-20220711232805443

iExpense

核心代码

Expenses(ViewModel)

swift
class Expenses: ObservableObject {
+    @Published var items = [ExpenseItem]() {
+        didSet {
+            let encoder = JSONEncoder()
+            
+            if let encoded = try? encoder.encode(items) {
+                UserDefaults.standard.set(encoded, forKey: "Items")
+            }
+        }
+    }
+    
+    init() {
+        if let itemArr = UserDefaults.standard.data(forKey: "Items") {
+            let decoder = JSONDecoder()
+            if let decodedItems = try? decoder.decode([ExpenseItem].self, from: itemArr) {
+                items = decodedItems
+                return
+            }
+        }
+        items = []
+    }
+}
class Expenses: ObservableObject {
+    @Published var items = [ExpenseItem]() {
+        didSet {
+            let encoder = JSONEncoder()
+            
+            if let encoded = try? encoder.encode(items) {
+                UserDefaults.standard.set(encoded, forKey: "Items")
+            }
+        }
+    }
+    
+    init() {
+        if let itemArr = UserDefaults.standard.data(forKey: "Items") {
+            let decoder = JSONDecoder()
+            if let decodedItems = try? decoder.decode([ExpenseItem].self, from: itemArr) {
+                items = decodedItems
+                return
+            }
+        }
+        items = []
+    }
+}

ExpenseItem(Model)

swift
struct ExpenseItem: Identifiable, Codable {
+    let name: String
+    let type: String
+    let amount: Double
+    let id = UUID()
+}
struct ExpenseItem: Identifiable, Codable {
+    let name: String
+    let type: String
+    let amount: Double
+    let id = UUID()
+}

AddView

swift
struct AddView: View {
+    @ObservedObject var expenses: Expenses
+    
+    @State private var name = ""
+    @State private var type = "Personal"
+    @State private var amount = 0.0
+    @Environment(\\.dismiss) var dismiss
+    
+    let types = ["Business", "Personal"]
+    
+    
+    var body: some View {
+        NavigationView {
+            Form {
+                TextField("Name", text: $name)
+                
+                Picker("Type", selection: $type) {
+                    ForEach(types, id: \\.self) {
+                        Text($0)
+                    }
+                }
+                
+                TextField("Amount", value: $amount, format: .currency(code: Locale.current.currencyCode ?? "CNY"))
+                    .keyboardType(.decimalPad)
+            }
+            .navigationTitle("Add new expense")
+            .toolbar {
+                Button("Save") {
+                    let item = ExpenseItem(name: name, type: type, amount: amount)
+                    expenses.items.append(item)
+                    dismiss()
+                }
+            }
+        }
+    }
+}
struct AddView: View {
+    @ObservedObject var expenses: Expenses
+    
+    @State private var name = ""
+    @State private var type = "Personal"
+    @State private var amount = 0.0
+    @Environment(\\.dismiss) var dismiss
+    
+    let types = ["Business", "Personal"]
+    
+    
+    var body: some View {
+        NavigationView {
+            Form {
+                TextField("Name", text: $name)
+                
+                Picker("Type", selection: $type) {
+                    ForEach(types, id: \\.self) {
+                        Text($0)
+                    }
+                }
+                
+                TextField("Amount", value: $amount, format: .currency(code: Locale.current.currencyCode ?? "CNY"))
+                    .keyboardType(.decimalPad)
+            }
+            .navigationTitle("Add new expense")
+            .toolbar {
+                Button("Save") {
+                    let item = ExpenseItem(name: name, type: type, amount: amount)
+                    expenses.items.append(item)
+                    dismiss()
+                }
+            }
+        }
+    }
+}

ContentView

swift
struct ContentView: View {
+    @StateObject var expenses = Expenses()
+    @State private var showingAddExpense = false
+    
+    var body: some View {
+        NavigationView {
+            List {
+                ForEach(expenses.items) { item in
+                    HStack {
+                        VStack(alignment: .leading) {
+                            Text(item.name)
+                                .font(.headline)
+                            Text(item.type)
+                                .font(.subheadline)
+                        }
+                        Spacer()
+                        Text(item.amount, format: .currency(code: Locale.current.currencyCode ?? "CNY"))
+                    }
+                }
+                .onDelete(perform: removeItems)
+            }
+            .navigationTitle("iExpense")
+            .toolbar {
+                Button {
+                    showingAddExpense = true
+                } label: {
+                    Image(systemName: "plus")
+                }
+            }
+            .sheet(isPresented: $showingAddExpense) {
+                AddView(expenses: expenses)
+                
+            }
+        }
+    }
+    
+    func removeItems(at offsets: IndexSet) {
+        expenses.items.remove(atOffsets: offsets)
+    }
+}
struct ContentView: View {
+    @StateObject var expenses = Expenses()
+    @State private var showingAddExpense = false
+    
+    var body: some View {
+        NavigationView {
+            List {
+                ForEach(expenses.items) { item in
+                    HStack {
+                        VStack(alignment: .leading) {
+                            Text(item.name)
+                                .font(.headline)
+                            Text(item.type)
+                                .font(.subheadline)
+                        }
+                        Spacer()
+                        Text(item.amount, format: .currency(code: Locale.current.currencyCode ?? "CNY"))
+                    }
+                }
+                .onDelete(perform: removeItems)
+            }
+            .navigationTitle("iExpense")
+            .toolbar {
+                Button {
+                    showingAddExpense = true
+                } label: {
+                    Image(systemName: "plus")
+                }
+            }
+            .sheet(isPresented: $showingAddExpense) {
+                AddView(expenses: expenses)
+                
+            }
+        }
+    }
+    
+    func removeItems(at offsets: IndexSet) {
+        expenses.items.remove(atOffsets: offsets)
+    }
+}

知识点

  • MVVM

  • @StateObject、@Published、ObservableObject、@ObservedObject

  • UserDefaults(存储少量数据,常用于存储Preference)

  • JSONEncoder、JSONDecoder、Codable

  • 属性观察器(willSet、didSet) 配合 UserDefaults保存数据

  • onDelete修饰符

  • sheet修饰符(弹出新页面)

    swift
    .sheet(isPresented: $showingAddExpense) {
    +    AddView(expenses: expenses)
    +}
    .sheet(isPresented: $showingAddExpense) {
    +    AddView(expenses: expenses)
    +}
  • @Environment(.dismiss) var dismiss(@Environment(keyPath) 可以读取系统环境数据,dismiss用于关闭当前展示页面)

效果

image-20220711233432310

image-20220711233750123

`,43),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const d=s(p,[["render",t]]);export{u as __pageData,d as default}; diff --git a/assets/frontend_others_HackingWithSwift-1.md.355cc977.lean.js b/assets/frontend_others_HackingWithSwift-1.md.355cc977.lean.js new file mode 100644 index 000000000..e673eb588 --- /dev/null +++ b/assets/frontend_others_HackingWithSwift-1.md.355cc977.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"HackingWithSwift-1","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/others/HackingWithSwift-1.md","filePath":"frontend/others/HackingWithSwift-1.md","lastUpdated":1708510102000}'),p={name:"frontend/others/HackingWithSwift-1.md"},o=l("",43),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const d=s(p,[["render",t]]);export{u as __pageData,d as default}; diff --git a/assets/frontend_others_HackingWithSwift-2.md.85e4f87a.js b/assets/frontend_others_HackingWithSwift-2.md.85e4f87a.js new file mode 100644 index 000000000..98a8044c1 --- /dev/null +++ b/assets/frontend_others_HackingWithSwift-2.md.85e4f87a.js @@ -0,0 +1,1413 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"HackingWithSwift-2","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/others/HackingWithSwift-2.md","filePath":"frontend/others/HackingWithSwift-2.md","lastUpdated":1708510102000}'),p={name:"frontend/others/HackingWithSwift-2.md"},o=l(`

HackingWithSwift-2

https://www.youtube.com/playlist?list=PLuoeXyslFTuZRi4q4VT6lZKxYbr7so1Mr

Moonshot

核心代码

Bundle-Decodable

swift
extension Bundle {
+    func decode<T: Decodable>(_ file: String) -> T {
+        guard let url = self.url(forResource: file, withExtension: nil) else {
+            fatalError("Failed to locate \\(file) in bundle")
+        }
+        
+        guard let data = try? Data(contentsOf: url) else {
+            fatalError("Failed to locate \\(file) in bundle")
+        }
+        
+        let decoder = JSONDecoder()
+        let formatter = DateFormatter()
+        formatter.dateFormat = "y-MM-dd"
+        decoder.dateDecodingStrategy = .formatted(formatter)
+        
+        guard let loaded = try? decoder.decode( T.self, from: data) else {
+            fatalError("Failed to locate \\(file) in bundle")
+        }
+        
+        return loaded
+    }
+}
extension Bundle {
+    func decode<T: Decodable>(_ file: String) -> T {
+        guard let url = self.url(forResource: file, withExtension: nil) else {
+            fatalError("Failed to locate \\(file) in bundle")
+        }
+        
+        guard let data = try? Data(contentsOf: url) else {
+            fatalError("Failed to locate \\(file) in bundle")
+        }
+        
+        let decoder = JSONDecoder()
+        let formatter = DateFormatter()
+        formatter.dateFormat = "y-MM-dd"
+        decoder.dateDecodingStrategy = .formatted(formatter)
+        
+        guard let loaded = try? decoder.decode( T.self, from: data) else {
+            fatalError("Failed to locate \\(file) in bundle")
+        }
+        
+        return loaded
+    }
+}

Color-Theme

swift
import SwiftUI
+
+extension ShapeStyle where Self == Color {
+    static var darkBackground: Color {
+        Color(red: 0.1, green: 0.1, blue: 0.2)
+    }
+    
+    static var lightBackgroud: Color {
+        Color(red: 0.2, green: 0.2, blue: 0.3)
+    }
+}
import SwiftUI
+
+extension ShapeStyle where Self == Color {
+    static var darkBackground: Color {
+        Color(red: 0.1, green: 0.1, blue: 0.2)
+    }
+    
+    static var lightBackgroud: Color {
+        Color(red: 0.2, green: 0.2, blue: 0.3)
+    }
+}

Model

swift
struct Astronaut: Codable, Identifiable {
+    let id: String
+    let name: String
+    let description: String
+}
+
+
+struct Mission: Codable, Identifiable {
+    struct CrewRole: Codable {
+        let name: String
+        let role: String
+    }
+    
+    let id: Int
+    let launchDate: Date?
+    let crew: [CrewRole]
+    let description: String
+    
+    var displayName: String {
+        "Apollo \\(id)"
+    }
+    
+    var image: String {
+        "apollo\\(id)"
+    }
+    
+    var formattedLaunchDate: String {
+        launchDate?.formatted(date: .abbreviated, time: .omitted) ?? "N/A"
+    }
+}
struct Astronaut: Codable, Identifiable {
+    let id: String
+    let name: String
+    let description: String
+}
+
+
+struct Mission: Codable, Identifiable {
+    struct CrewRole: Codable {
+        let name: String
+        let role: String
+    }
+    
+    let id: Int
+    let launchDate: Date?
+    let crew: [CrewRole]
+    let description: String
+    
+    var displayName: String {
+        "Apollo \\(id)"
+    }
+    
+    var image: String {
+        "apollo\\(id)"
+    }
+    
+    var formattedLaunchDate: String {
+        launchDate?.formatted(date: .abbreviated, time: .omitted) ?? "N/A"
+    }
+}

ContentView

swift
struct ContentView: View {
+    let astronauts: [String: Astronaut] = Bundle.main.decode("astronauts.json")
+    let missions: [Mission] = Bundle.main.decode("missions.json")
+    
+    let columns = [
+        GridItem(.adaptive(minimum: 150))
+    ]
+    
+    var body: some View {
+        NavigationView {
+            ScrollView {
+                LazyVGrid(columns: columns) {
+                    ForEach(missions) { mission in
+                        NavigationLink{
+                            MissionView(mission: mission, astronauts: astronauts)
+                        } label: {
+                            VStack{
+                                Image(mission.image)
+                                    .resizable()
+                                    .scaledToFit()
+                                    .frame(width: 100, height: 100)
+                                VStack {
+                                    Text(mission.displayName)
+                                        .font(.headline)
+                                        .foregroundColor(.white)
+                                    
+                                    Text(mission.formattedLaunchDate)
+                                        .font(.subheadline)
+                                        .foregroundColor(.white.opacity(0.5))
+                                }
+                                .padding(.vertical)
+                                .frame(maxWidth: .infinity)
+                                .background(.lightBackgroud)
+                            }
+                            .clipShape(RoundedRectangle(cornerRadius: 10))
+                            .overlay{
+                                RoundedRectangle(cornerRadius: 10)
+                                    .stroke()
+                            }
+                        }
+                    }
+                }
+                .padding([.horizontal, .bottom])
+            }
+            .navigationTitle("Moonshot")
+            .background(.darkBackground)
+            .preferredColorScheme(.dark)
+        }
+    }
+}
struct ContentView: View {
+    let astronauts: [String: Astronaut] = Bundle.main.decode("astronauts.json")
+    let missions: [Mission] = Bundle.main.decode("missions.json")
+    
+    let columns = [
+        GridItem(.adaptive(minimum: 150))
+    ]
+    
+    var body: some View {
+        NavigationView {
+            ScrollView {
+                LazyVGrid(columns: columns) {
+                    ForEach(missions) { mission in
+                        NavigationLink{
+                            MissionView(mission: mission, astronauts: astronauts)
+                        } label: {
+                            VStack{
+                                Image(mission.image)
+                                    .resizable()
+                                    .scaledToFit()
+                                    .frame(width: 100, height: 100)
+                                VStack {
+                                    Text(mission.displayName)
+                                        .font(.headline)
+                                        .foregroundColor(.white)
+                                    
+                                    Text(mission.formattedLaunchDate)
+                                        .font(.subheadline)
+                                        .foregroundColor(.white.opacity(0.5))
+                                }
+                                .padding(.vertical)
+                                .frame(maxWidth: .infinity)
+                                .background(.lightBackgroud)
+                            }
+                            .clipShape(RoundedRectangle(cornerRadius: 10))
+                            .overlay{
+                                RoundedRectangle(cornerRadius: 10)
+                                    .stroke()
+                            }
+                        }
+                    }
+                }
+                .padding([.horizontal, .bottom])
+            }
+            .navigationTitle("Moonshot")
+            .background(.darkBackground)
+            .preferredColorScheme(.dark)
+        }
+    }
+}

MissionView

swift
struct MissionView: View {
+    struct CrewMember {
+        let role: String
+        let astronaut: Astronaut
+    }
+    
+    
+    let mission: Mission
+    
+    let crew: [CrewMember]
+    
+    var body: some View {
+        GeometryReader { geometry in
+            ScrollView {
+                VStack {
+                    Image(mission.image)
+                        .resizable()
+                        .scaledToFit()
+                        .frame(maxWidth: geometry.size.width * 0.6)
+                        .padding(.top)
+                    
+                    VStack(alignment: .leading) {
+                        Rectangle()
+                            .frame(height: 2)
+                            .foregroundColor(.lightBackgroud)
+                            .padding(.vertical)
+                        
+                        Text("Mission Highlights")
+                            .font(.title.bold())
+                            .padding(.bottom, 5)
+                        
+                        Text(mission.description)
+                        Rectangle()
+                            .frame(height: 2)
+                            .foregroundColor(.lightBackgroud)
+                            .padding(.vertical)
+                        
+                        Text("Crew")
+                            .font(.title.bold())
+                            .padding(.bottom, 5)
+                    }
+                    .padding(.horizontal)
+                    
+                    
+                    
+
+                    
+                    ScrollView(.horizontal, showsIndicators: false) {
+                        HStack {
+                            ForEach(crew, id: \\.role) { crewMember in
+                                NavigationLink {
+                                    AstronautView(astronaut: crewMember.astronaut)
+                                }label: {
+                                    HStack {
+                                        Image(crewMember.astronaut.id)
+                                            .resizable()
+                                            .frame(width: 104, height: 72)
+                                            .clipShape(Capsule())
+                                            .overlay{
+                                                Capsule()
+                                                    .strokeBorder(.white, lineWidth: 1)
+                                            }
+                                        VStack(alignment: .leading) {
+                                            Text(crewMember.astronaut.name)
+                                                .foregroundColor(.white)
+                                                .font(.headline)
+                                            
+                                            Text(crewMember.role)
+                                                .foregroundColor(.secondary)
+                                            
+                                        }
+                                    }
+                                }
+                            }
+                                
+                        }
+                    }
+                    .padding(.horizontal)
+                }
+                .padding(.bottom)
+            }
+            .navigationTitle(mission.displayName)
+            .navigationBarTitleDisplayMode(.inline)
+            .background(.darkBackground)
+        }
+        
+    }
+    
+    
+    init(mission: Mission, astronauts: [String: Astronaut]) {
+        self.mission = mission
+        
+        self.crew = mission.crew.map { member in
+            if let astronaut = astronauts[member.name] {
+                return CrewMember(role: member.role, astronaut: astronaut)
+            }else {
+                fatalError("Missing \\(member.name)")
+            }
+        }
+    }
+}
struct MissionView: View {
+    struct CrewMember {
+        let role: String
+        let astronaut: Astronaut
+    }
+    
+    
+    let mission: Mission
+    
+    let crew: [CrewMember]
+    
+    var body: some View {
+        GeometryReader { geometry in
+            ScrollView {
+                VStack {
+                    Image(mission.image)
+                        .resizable()
+                        .scaledToFit()
+                        .frame(maxWidth: geometry.size.width * 0.6)
+                        .padding(.top)
+                    
+                    VStack(alignment: .leading) {
+                        Rectangle()
+                            .frame(height: 2)
+                            .foregroundColor(.lightBackgroud)
+                            .padding(.vertical)
+                        
+                        Text("Mission Highlights")
+                            .font(.title.bold())
+                            .padding(.bottom, 5)
+                        
+                        Text(mission.description)
+                        Rectangle()
+                            .frame(height: 2)
+                            .foregroundColor(.lightBackgroud)
+                            .padding(.vertical)
+                        
+                        Text("Crew")
+                            .font(.title.bold())
+                            .padding(.bottom, 5)
+                    }
+                    .padding(.horizontal)
+                    
+                    
+                    
+
+                    
+                    ScrollView(.horizontal, showsIndicators: false) {
+                        HStack {
+                            ForEach(crew, id: \\.role) { crewMember in
+                                NavigationLink {
+                                    AstronautView(astronaut: crewMember.astronaut)
+                                }label: {
+                                    HStack {
+                                        Image(crewMember.astronaut.id)
+                                            .resizable()
+                                            .frame(width: 104, height: 72)
+                                            .clipShape(Capsule())
+                                            .overlay{
+                                                Capsule()
+                                                    .strokeBorder(.white, lineWidth: 1)
+                                            }
+                                        VStack(alignment: .leading) {
+                                            Text(crewMember.astronaut.name)
+                                                .foregroundColor(.white)
+                                                .font(.headline)
+                                            
+                                            Text(crewMember.role)
+                                                .foregroundColor(.secondary)
+                                            
+                                        }
+                                    }
+                                }
+                            }
+                                
+                        }
+                    }
+                    .padding(.horizontal)
+                }
+                .padding(.bottom)
+            }
+            .navigationTitle(mission.displayName)
+            .navigationBarTitleDisplayMode(.inline)
+            .background(.darkBackground)
+        }
+        
+    }
+    
+    
+    init(mission: Mission, astronauts: [String: Astronaut]) {
+        self.mission = mission
+        
+        self.crew = mission.crew.map { member in
+            if let astronaut = astronauts[member.name] {
+                return CrewMember(role: member.role, astronaut: astronaut)
+            }else {
+                fatalError("Missing \\(member.name)")
+            }
+        }
+    }
+}

AstronautView

swift
struct AstronautView: View {
+    let astronaut: Astronaut
+    
+    var body: some View {
+        ScrollView {
+            VStack {
+                Image(astronaut.id)
+                    .resizable()
+                    .scaledToFit()
+                    
+                
+                Text(astronaut.description)
+                    .padding()
+                
+                
+            }
+            
+        }
+        .background(.darkBackground)
+        .navigationTitle(astronaut.name)
+        .navigationBarTitleDisplayMode(.inline)
+    }
+}
struct AstronautView: View {
+    let astronaut: Astronaut
+    
+    var body: some View {
+        ScrollView {
+            VStack {
+                Image(astronaut.id)
+                    .resizable()
+                    .scaledToFit()
+                    
+                
+                Text(astronaut.description)
+                    .padding()
+                
+                
+            }
+            
+        }
+        .background(.darkBackground)
+        .navigationTitle(astronaut.name)
+        .navigationBarTitleDisplayMode(.inline)
+    }
+}

知识点

  • extension扩展Bubble读取静态资源文件、扩展Color用于设置背景色
  • LazyVGrid设置自适应的网格视图
  • .preferredColorScheme(.dark)设置首选项颜色
  • GeometryReader,可以获取父View的坐标、尺寸等
  • Image调整大小- resizable(), scaleToFit(),frame()

效果

image-20220712152252199

image-20220712152306254

image-20220712152317084

CupcakeCorner

核心代码

Order

swift
class Order: ObservableObject, Codable {
+    
+    enum CodingKeys: CodingKey {
+        case type, quantity, extraFrosting, addSprinkles, name, streetAddress, city, zip
+    }
+    
+    static let types = ["Vanilla", "Strawberry", "Chocolate", "Rainbow"]
+    
+    @Published var type = 0
+    @Published var quantity = 3
+    @Published var specialRequestEnabled = false {
+        didSet {
+            if specialRequestEnabled == false {
+                extraFrosting = false
+                addSprinkles = false
+            }
+        }
+    }
+    @Published var extraFrosting = false
+    @Published var addSprinkles = false
+    
+    
+    @Published var name = ""
+    @Published var streetAddress = ""
+    @Published var city = ""
+    @Published var zip = ""
+    
+    
+    var hasValidAddress: Bool {
+        if name.isEmpty || streetAddress.isEmpty || city.isEmpty || zip.isEmpty {
+            return false
+        }
+        return true
+    }
+    
+    
+    var cost: Double {
+        // $2 per cake
+        var cost = Double(quantity) * 2
+        
+        // complicated cakes cost more
+        cost += Double(type) / 2
+        
+        // $1/cake for extra frosting
+        if extraFrosting {
+            cost += Double(quantity)
+        }
+        
+        // $0.5/cake for sprinkles
+        if addSprinkles {
+            cost += Double(quantity) / 2
+        }
+        
+        return cost
+    }
+    
+    init() {}
+    
+    
+    func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        
+        try container.encode(type, forKey: .type)
+        try container.encode(quantity, forKey: .quantity)
+        try container.encode(extraFrosting, forKey: .extraFrosting)
+        try container.encode(addSprinkles, forKey: .addSprinkles)
+        try container.encode(name, forKey: .name)
+        try container.encode(streetAddress, forKey: .streetAddress)
+        try container.encode(city, forKey: .city)
+        try container.encode(zip, forKey: .zip)
+    }
+    
+    required init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        
+        type = try container.decode(Int.self, forKey: .type)
+        quantity = try container.decode(Int.self, forKey: .quantity)
+        extraFrosting = try container.decode(Bool.self, forKey: .extraFrosting)
+        addSprinkles = try container.decode(Bool.self, forKey: .addSprinkles)
+        name = try container.decode(String.self, forKey: .name)
+        city = try container.decode(String.self, forKey: .city)
+        streetAddress = try container.decode(String.self, forKey: .streetAddress)
+        zip = try container.decode(String.self, forKey: .zip)
+    } 
+}
class Order: ObservableObject, Codable {
+    
+    enum CodingKeys: CodingKey {
+        case type, quantity, extraFrosting, addSprinkles, name, streetAddress, city, zip
+    }
+    
+    static let types = ["Vanilla", "Strawberry", "Chocolate", "Rainbow"]
+    
+    @Published var type = 0
+    @Published var quantity = 3
+    @Published var specialRequestEnabled = false {
+        didSet {
+            if specialRequestEnabled == false {
+                extraFrosting = false
+                addSprinkles = false
+            }
+        }
+    }
+    @Published var extraFrosting = false
+    @Published var addSprinkles = false
+    
+    
+    @Published var name = ""
+    @Published var streetAddress = ""
+    @Published var city = ""
+    @Published var zip = ""
+    
+    
+    var hasValidAddress: Bool {
+        if name.isEmpty || streetAddress.isEmpty || city.isEmpty || zip.isEmpty {
+            return false
+        }
+        return true
+    }
+    
+    
+    var cost: Double {
+        // $2 per cake
+        var cost = Double(quantity) * 2
+        
+        // complicated cakes cost more
+        cost += Double(type) / 2
+        
+        // $1/cake for extra frosting
+        if extraFrosting {
+            cost += Double(quantity)
+        }
+        
+        // $0.5/cake for sprinkles
+        if addSprinkles {
+            cost += Double(quantity) / 2
+        }
+        
+        return cost
+    }
+    
+    init() {}
+    
+    
+    func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        
+        try container.encode(type, forKey: .type)
+        try container.encode(quantity, forKey: .quantity)
+        try container.encode(extraFrosting, forKey: .extraFrosting)
+        try container.encode(addSprinkles, forKey: .addSprinkles)
+        try container.encode(name, forKey: .name)
+        try container.encode(streetAddress, forKey: .streetAddress)
+        try container.encode(city, forKey: .city)
+        try container.encode(zip, forKey: .zip)
+    }
+    
+    required init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        
+        type = try container.decode(Int.self, forKey: .type)
+        quantity = try container.decode(Int.self, forKey: .quantity)
+        extraFrosting = try container.decode(Bool.self, forKey: .extraFrosting)
+        addSprinkles = try container.decode(Bool.self, forKey: .addSprinkles)
+        name = try container.decode(String.self, forKey: .name)
+        city = try container.decode(String.self, forKey: .city)
+        streetAddress = try container.decode(String.self, forKey: .streetAddress)
+        zip = try container.decode(String.self, forKey: .zip)
+    } 
+}

ContentView

swift
struct ContentView: View {
+    @StateObject var order = Order()
+    
+    var body: some View {
+        NavigationView{
+            Form {
+                Section {
+                    Picker("Select your cake type", selection: $order.type) {
+                        ForEach(Order.types.indices, id: \\.self) {
+                            Text(Order.types[$0])
+                        }
+                    }
+                    Stepper("Number of cakes: \\(order.quantity)", value: $order.quantity, in:  3...20)
+                }
+                
+                
+                Section {
+                    Toggle("Any special requests?", isOn: $order.specialRequestEnabled.animation())
+                    
+                    if order.specialRequestEnabled {
+                        Toggle("Add extra frosting", isOn: $order.extraFrosting)
+                        Toggle("Add extra sprinkles", isOn: $order.addSprinkles)
+                    }
+                }
+                
+                Section {
+                    NavigationLink {
+                        AddressView(order: order)
+                    } label: {
+                        Text("Deliver details")
+                    }
+                }
+            }
+            .navigationTitle("Cupcake Corner")
+        }
+    }
+}
struct ContentView: View {
+    @StateObject var order = Order()
+    
+    var body: some View {
+        NavigationView{
+            Form {
+                Section {
+                    Picker("Select your cake type", selection: $order.type) {
+                        ForEach(Order.types.indices, id: \\.self) {
+                            Text(Order.types[$0])
+                        }
+                    }
+                    Stepper("Number of cakes: \\(order.quantity)", value: $order.quantity, in:  3...20)
+                }
+                
+                
+                Section {
+                    Toggle("Any special requests?", isOn: $order.specialRequestEnabled.animation())
+                    
+                    if order.specialRequestEnabled {
+                        Toggle("Add extra frosting", isOn: $order.extraFrosting)
+                        Toggle("Add extra sprinkles", isOn: $order.addSprinkles)
+                    }
+                }
+                
+                Section {
+                    NavigationLink {
+                        AddressView(order: order)
+                    } label: {
+                        Text("Deliver details")
+                    }
+                }
+            }
+            .navigationTitle("Cupcake Corner")
+        }
+    }
+}

AddressView

swift
struct AddressView: View {
+    @ObservedObject var order: Order
+    var body: some View {
+        Form {
+            Section {
+                TextField("name", text: $order.name)
+                TextField("Street address", text: $order.streetAddress)
+                TextField("City", text: $order.city)
+                TextField("Zip", text: $order.zip)
+            }
+            
+            Section {
+                NavigationLink {
+                    CheckoutView(order: order)
+                } label: {
+                    Text("Check out")
+                }
+            }
+            .disabled(!order.hasValidAddress)
+        }
+        .navigationTitle("Delivery details")
+        .navigationBarTitleDisplayMode(.inline)
+    }
+}
struct AddressView: View {
+    @ObservedObject var order: Order
+    var body: some View {
+        Form {
+            Section {
+                TextField("name", text: $order.name)
+                TextField("Street address", text: $order.streetAddress)
+                TextField("City", text: $order.city)
+                TextField("Zip", text: $order.zip)
+            }
+            
+            Section {
+                NavigationLink {
+                    CheckoutView(order: order)
+                } label: {
+                    Text("Check out")
+                }
+            }
+            .disabled(!order.hasValidAddress)
+        }
+        .navigationTitle("Delivery details")
+        .navigationBarTitleDisplayMode(.inline)
+    }
+}

CheckoutView

swift
struct CheckoutView: View {
+    @ObservedObject var order: Order
+    @State private var confirmationMessage = ""
+    @State private var showingConfirmation = false
+    
+    
+    var body: some View {
+        ScrollView {
+            
+            VStack {
+                AsyncImage(url: URL(string: "https://hws.dev/img/cupcakes@3x.jpg"), scale: 3) { image in
+                    image
+                        .resizable()
+                        .scaledToFit()
+                } placeholder: {
+                    ProgressView()
+                }
+                
+               
+                Text("Your total is \\(order.cost, format: .currency(code: "USD"))")
+                    .font(.title)
+                
+                Button("Place Order") {
+                    Task {
+                        await placeOrder()
+                    }
+                }
+                .padding()
+           
+            }
+        }
+        .navigationTitle("Check out")
+        .navigationBarTitleDisplayMode(.inline)
+        .alert("Thank you!", isPresented: $showingConfirmation) {
+            Button("OK") {}
+        } message: {
+            Text(confirmationMessage)
+        }
+    }
+    
+    func placeOrder() async {
+        guard let encoded = try? JSONEncoder().encode(order) else {
+            print("Failed to encode order")
+                return
+        }
+        
+        let url = URL(string: "https://reqres.in/api/cupcakes")!
+        var request = URLRequest(url: url)
+        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+        request.httpMethod = "POST"
+        
+        do {
+            let (data, _) = try await URLSession.shared.upload(for: request, from: encoded)
+            let decodedOrder = try JSONDecoder().decode(Order.self, from: data)
+            confirmationMessage = "Your order for \\(decodedOrder.quantity)x\\(Order.types[decodedOrder.type].lowercased()) cupcakes is on its way !"
+            showingConfirmation = true
+        } catch {
+            print("Check out failed ...")
+        }
+        
+    }
+}
struct CheckoutView: View {
+    @ObservedObject var order: Order
+    @State private var confirmationMessage = ""
+    @State private var showingConfirmation = false
+    
+    
+    var body: some View {
+        ScrollView {
+            
+            VStack {
+                AsyncImage(url: URL(string: "https://hws.dev/img/cupcakes@3x.jpg"), scale: 3) { image in
+                    image
+                        .resizable()
+                        .scaledToFit()
+                } placeholder: {
+                    ProgressView()
+                }
+                
+               
+                Text("Your total is \\(order.cost, format: .currency(code: "USD"))")
+                    .font(.title)
+                
+                Button("Place Order") {
+                    Task {
+                        await placeOrder()
+                    }
+                }
+                .padding()
+           
+            }
+        }
+        .navigationTitle("Check out")
+        .navigationBarTitleDisplayMode(.inline)
+        .alert("Thank you!", isPresented: $showingConfirmation) {
+            Button("OK") {}
+        } message: {
+            Text(confirmationMessage)
+        }
+    }
+    
+    func placeOrder() async {
+        guard let encoded = try? JSONEncoder().encode(order) else {
+            print("Failed to encode order")
+                return
+        }
+        
+        let url = URL(string: "https://reqres.in/api/cupcakes")!
+        var request = URLRequest(url: url)
+        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+        request.httpMethod = "POST"
+        
+        do {
+            let (data, _) = try await URLSession.shared.upload(for: request, from: encoded)
+            let decodedOrder = try JSONDecoder().decode(Order.self, from: data)
+            confirmationMessage = "Your order for \\(decodedOrder.quantity)x\\(Order.types[decodedOrder.type].lowercased()) cupcakes is on its way !"
+            showingConfirmation = true
+        } catch {
+            print("Check out failed ...")
+        }
+        
+    }
+}

知识点

  • Codable协议无法处理被@Published等属性包装器修饰的属性,需要额外编码来手动实现Codable协议

  • Form表单校验使用.disabled()修饰符

  • 异步加载图像AsyncImage,这个View不能像普通Image一样直接使用.resizable()调整大小,需要特殊处理

    swift
    AsyncImage(url: URL(string: "https://hws.dev/img/cupcakes@3x.jpg"), scale: 3) { image in
    +                    image
    +                        .resizable()
    +                        .scaledToFit()
    +                } placeholder: {
    +                    ProgressView() //加载中loading view
    +                }
    AsyncImage(url: URL(string: "https://hws.dev/img/cupcakes@3x.jpg"), scale: 3) { image in
    +                    image
    +                        .resizable()
    +                        .scaledToFit()
    +                } placeholder: {
    +                    ProgressView() //加载中loading view
    +                }
  • Button的action使用异步方法时需要使用Task

    swift
    Button("Place Order") {
    +    Task {
    +        await placeOrder()
    +    }
    +}
    Button("Place Order") {
    +    Task {
    +        await placeOrder()
    +    }
    +}
  • 使用URLRequest、URLSession发送http请求

  • 异步方法,async,await关键字

效果

image-20220713143307866

image-20220713143325947

image-20220713143335966

Bookworm

核心代码

BookwormApp

swift
@main
+struct BookwormApp: App {
+    
+    @StateObject private var dataController = DataController()
+    
+    var body: some Scene {
+        WindowGroup {
+            ContentView()
+                .environment(\\.managedObjectContext, dataController.container.viewContext )
+        }
+    }
+}
@main
+struct BookwormApp: App {
+    
+    @StateObject private var dataController = DataController()
+    
+    var body: some Scene {
+        WindowGroup {
+            ContentView()
+                .environment(\\.managedObjectContext, dataController.container.viewContext )
+        }
+    }
+}

ContentView

swift
struct ContentView: View {
+    @Environment(\\.managedObjectContext) var moc
+    @FetchRequest(sortDescriptors: [SortDescriptor(\\.title, order: .forward), SortDescriptor(\\.author, order: .forward)]) var books: FetchedResults<Book>
+    
+    @State private var showingAddScreen = false
+    
+    var body: some View {
+        NavigationView {
+            List {
+                ForEach(books) { book in
+                    NavigationLink {
+                        DetailView(book: book)
+                    } label: {
+                        HStack {
+                            EmojiRatingView(rating: book.rating)
+                                .font(.largeTitle)
+                            VStack(alignment: .leading) {
+                                Text(book.title ?? "Unknown title")
+                                    .font(.headline)
+                                Text(book.author ?? "Unknown author")
+                                    .foregroundColor(.secondary)
+                            }
+                        }
+                    }
+                }
+                .onDelete(perform: deleteBooks)
+            }
+            .navigationTitle("Bookworm")
+            .toolbar {
+                ToolbarItem(placement: .navigationBarLeading) {
+                    EditButton()
+                }
+                
+                ToolbarItem(placement: .navigationBarTrailing) {
+                    Button {
+                        showingAddScreen.toggle()
+                    } label: {
+                        Label("Add book", systemImage: "plus")
+                    }
+                }
+            }
+            .sheet(isPresented: $showingAddScreen) {
+                AddBookView()
+            }
+        }
+    }
+    
+    func deleteBooks(at offsets: IndexSet) {
+        for offset in offsets {
+            let book = books[offset]
+            moc.delete(book)
+        }
+        try? moc.save()
+    }
+}
struct ContentView: View {
+    @Environment(\\.managedObjectContext) var moc
+    @FetchRequest(sortDescriptors: [SortDescriptor(\\.title, order: .forward), SortDescriptor(\\.author, order: .forward)]) var books: FetchedResults<Book>
+    
+    @State private var showingAddScreen = false
+    
+    var body: some View {
+        NavigationView {
+            List {
+                ForEach(books) { book in
+                    NavigationLink {
+                        DetailView(book: book)
+                    } label: {
+                        HStack {
+                            EmojiRatingView(rating: book.rating)
+                                .font(.largeTitle)
+                            VStack(alignment: .leading) {
+                                Text(book.title ?? "Unknown title")
+                                    .font(.headline)
+                                Text(book.author ?? "Unknown author")
+                                    .foregroundColor(.secondary)
+                            }
+                        }
+                    }
+                }
+                .onDelete(perform: deleteBooks)
+            }
+            .navigationTitle("Bookworm")
+            .toolbar {
+                ToolbarItem(placement: .navigationBarLeading) {
+                    EditButton()
+                }
+                
+                ToolbarItem(placement: .navigationBarTrailing) {
+                    Button {
+                        showingAddScreen.toggle()
+                    } label: {
+                        Label("Add book", systemImage: "plus")
+                    }
+                }
+            }
+            .sheet(isPresented: $showingAddScreen) {
+                AddBookView()
+            }
+        }
+    }
+    
+    func deleteBooks(at offsets: IndexSet) {
+        for offset in offsets {
+            let book = books[offset]
+            moc.delete(book)
+        }
+        try? moc.save()
+    }
+}

DataController

swift
import CoreData
+
+class DataController: ObservableObject {
+    let container = NSPersistentContainer(name: "Bookworm")
+    
+    
+    init() {
+        container.loadPersistentStores { description, error in
+            if let error = error {
+                print("Core data failed to load: \\(error.localizedDescription)")
+            }
+            
+        }
+    }
+}
import CoreData
+
+class DataController: ObservableObject {
+    let container = NSPersistentContainer(name: "Bookworm")
+    
+    
+    init() {
+        container.loadPersistentStores { description, error in
+            if let error = error {
+                print("Core data failed to load: \\(error.localizedDescription)")
+            }
+            
+        }
+    }
+}

AddBookView

swift
struct AddBookView: View {
+    
+    @Environment(\\.managedObjectContext) var moc
+    @Environment(\\.dismiss) var dismiss
+    @State private var title = ""
+    @State private var author = ""
+    @State private var rating = 3
+    @State private var genre = ""
+    @State private var review = ""
+    
+    let genres = ["Fantasy", "Horror", "Kids", "Mystery", "Poetry", "Romance", "Thriller"]
+    
+    var body: some View {
+        NavigationView {
+            Form {
+                Section {
+                    TextField("Name of book", text: $title)
+                    TextField("Author's name", text: $author)
+                    
+                    Picker("Genre", selection: $genre) {
+                        ForEach(genres, id: \\.self) {
+                            Text($0)
+                        }
+                    }
+                }
+                
+                Section {
+                    TextEditor(text: $review)
+                    
+                    RatingView(rating: $rating)
+                } header: {
+                    Text("Write a review")
+                }
+                
+                Section {
+                    Button("Save") {
+                        let book = Book(context: moc)
+                        book.id = UUID()
+                        book.title = self.title
+                        book.author = self.author
+                        book.genre = self.genre
+                        book.review = self.review
+                        book.rating = Int16(self.rating)
+                        try? moc.save()
+                        dismiss()
+                    }
+                }
+            }
+            .navigationTitle("Add book")
+        }
+    }
+}
struct AddBookView: View {
+    
+    @Environment(\\.managedObjectContext) var moc
+    @Environment(\\.dismiss) var dismiss
+    @State private var title = ""
+    @State private var author = ""
+    @State private var rating = 3
+    @State private var genre = ""
+    @State private var review = ""
+    
+    let genres = ["Fantasy", "Horror", "Kids", "Mystery", "Poetry", "Romance", "Thriller"]
+    
+    var body: some View {
+        NavigationView {
+            Form {
+                Section {
+                    TextField("Name of book", text: $title)
+                    TextField("Author's name", text: $author)
+                    
+                    Picker("Genre", selection: $genre) {
+                        ForEach(genres, id: \\.self) {
+                            Text($0)
+                        }
+                    }
+                }
+                
+                Section {
+                    TextEditor(text: $review)
+                    
+                    RatingView(rating: $rating)
+                } header: {
+                    Text("Write a review")
+                }
+                
+                Section {
+                    Button("Save") {
+                        let book = Book(context: moc)
+                        book.id = UUID()
+                        book.title = self.title
+                        book.author = self.author
+                        book.genre = self.genre
+                        book.review = self.review
+                        book.rating = Int16(self.rating)
+                        try? moc.save()
+                        dismiss()
+                    }
+                }
+            }
+            .navigationTitle("Add book")
+        }
+    }
+}

RatingView

swift
struct RatingView: View {
+    @Binding var rating: Int
+    
+    var label = ""
+    var maxmiumRating = 5
+    var offImage: Image?
+    var onImage =  Image(systemName: "star.fill")
+    var offColor = Color.gray
+    var onColor = Color.yellow
+    
+    
+    var body: some View {
+        HStack {
+            if label.isEmpty == false {
+                Text(label)
+            }
+            
+            ForEach(1..<maxmiumRating + 1, id: \\.self) { number in
+                image(for: number)
+                    .foregroundColor(number > rating ? offColor : onColor)
+                    .onTapGesture {
+                        rating = number
+                    }
+            }
+        }
+    }
+    
+    func image(for number: Int) -> Image {
+        if number > rating {
+            return offImage ?? onImage
+        } else {
+            return onImage
+        }
+    }
+}
struct RatingView: View {
+    @Binding var rating: Int
+    
+    var label = ""
+    var maxmiumRating = 5
+    var offImage: Image?
+    var onImage =  Image(systemName: "star.fill")
+    var offColor = Color.gray
+    var onColor = Color.yellow
+    
+    
+    var body: some View {
+        HStack {
+            if label.isEmpty == false {
+                Text(label)
+            }
+            
+            ForEach(1..<maxmiumRating + 1, id: \\.self) { number in
+                image(for: number)
+                    .foregroundColor(number > rating ? offColor : onColor)
+                    .onTapGesture {
+                        rating = number
+                    }
+            }
+        }
+    }
+    
+    func image(for number: Int) -> Image {
+        if number > rating {
+            return offImage ?? onImage
+        } else {
+            return onImage
+        }
+    }
+}

EmojiRatingView

swift
struct EmojiRatingView: View {
+    let rating: Int16
+    
+    var body: some View {
+        switch rating {
+        case 1:
+            return Text("☹️")
+        case 2:
+            return Text("😞")
+        case 3:
+            return Text("😊")
+        case 4:
+            return Text("😍")
+        default:
+            return Text("🤩")
+        }
+    }
+}
struct EmojiRatingView: View {
+    let rating: Int16
+    
+    var body: some View {
+        switch rating {
+        case 1:
+            return Text("☹️")
+        case 2:
+            return Text("😞")
+        case 3:
+            return Text("😊")
+        case 4:
+            return Text("😍")
+        default:
+            return Text("🤩")
+        }
+    }
+}

DetailView

swift
struct DetailView: View {
+    let book: Book
+    @Environment(\\.managedObjectContext) var moc
+    @Environment(\\.dismiss) var dismiss
+    @State private var showingAlert = false
+    
+    var body: some View {
+        ScrollView {
+            ZStack(alignment: .bottomTrailing) {
+                Image(book.genre ?? "Fantasy")
+                    .resizable()
+                    .scaledToFit()
+                
+                Text(book.genre?.uppercased() ?? "FANTASY")
+                    .font(.caption)
+                    .fontWeight(.black)
+                    .padding(8)
+                    .foregroundColor(.white)
+                    .background(.black.opacity(0.75))
+                    .clipShape(Capsule())
+                    .offset(x: -5, y: -5)
+            }
+            
+            Text(book.author ?? "Unknown author")
+                .font(.title)
+                .foregroundColor(.secondary)
+            
+            Text(book.review ?? "No review")
+                .padding()
+            
+            RatingView(rating: .constant(Int(book.rating)))
+        }
+        .navigationTitle(book.title ?? "Unknown book")
+        .navigationBarTitleDisplayMode(.inline)
+        .alert("Delete this book?", isPresented: $showingAlert) {
+            Button("OK", role: .destructive,action: deleteBook)
+            Button("Cancle", role: .cancel) { }
+        } message: {
+            Text("Are you sure?")
+        }
+        .toolbar {
+            Button {
+                showingAlert = true
+            } label: {
+                Label("Delete this book", systemImage: "trash")
+            }
+        }
+        
+    }
+    
+    
+    func deleteBook() {
+        moc.delete(book)
+        
+        try? moc.save()
+        
+        dismiss()
+        
+    }
+}
struct DetailView: View {
+    let book: Book
+    @Environment(\\.managedObjectContext) var moc
+    @Environment(\\.dismiss) var dismiss
+    @State private var showingAlert = false
+    
+    var body: some View {
+        ScrollView {
+            ZStack(alignment: .bottomTrailing) {
+                Image(book.genre ?? "Fantasy")
+                    .resizable()
+                    .scaledToFit()
+                
+                Text(book.genre?.uppercased() ?? "FANTASY")
+                    .font(.caption)
+                    .fontWeight(.black)
+                    .padding(8)
+                    .foregroundColor(.white)
+                    .background(.black.opacity(0.75))
+                    .clipShape(Capsule())
+                    .offset(x: -5, y: -5)
+            }
+            
+            Text(book.author ?? "Unknown author")
+                .font(.title)
+                .foregroundColor(.secondary)
+            
+            Text(book.review ?? "No review")
+                .padding()
+            
+            RatingView(rating: .constant(Int(book.rating)))
+        }
+        .navigationTitle(book.title ?? "Unknown book")
+        .navigationBarTitleDisplayMode(.inline)
+        .alert("Delete this book?", isPresented: $showingAlert) {
+            Button("OK", role: .destructive,action: deleteBook)
+            Button("Cancle", role: .cancel) { }
+        } message: {
+            Text("Are you sure?")
+        }
+        .toolbar {
+            Button {
+                showingAlert = true
+            } label: {
+                Label("Delete this book", systemImage: "trash")
+            }
+        }
+        
+    }
+    
+    
+    func deleteBook() {
+        moc.delete(book)
+        
+        try? moc.save()
+        
+        dismiss()
+        
+    }
+}

Bookworm.xcdatamodeld

image-20220714192129472

知识点

  • CoreData相关概念,.xcdatamodeld文件、NSPersistentContainer、@Environment(.managedObjectContext)、context.save()、context.delete(xxx)、FetchRequest
  • TextEditor
  • 通用RatingView

效果

image-20220714192654745

image-20220714192710927

image-20220714192720403

CoreData

  • Conditional saving of NSManagedObjectContex
swift
@Environment(\\.managedObjectContext) var moc
+
+var body: some View {
+  Button("Save") {
+    if moc.hasChanges {
+      try? moc.save()
+    }
+  }
+}
@Environment(\\.managedObjectContext) var moc
+
+var body: some View {
+  Button("Save") {
+    if moc.hasChanges {
+      try? moc.save()
+    }
+  }
+}
  • Ensuring Core Data objects are unique using constraints

    • 配置CoreDataEntity的constranits
    • DataController增加
    swift
    self.container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
    self.container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
  • Dynamically filtering @FetchRequest with SwiftUI

swift
struct FilteredList<T: NSManagedOBject, Context: View>: View {
+  @FetchRequest var fetchRequest: FetchedResults<T>
+  
+  var body: some View {
+    List(fetchRequest, id:\\.self) { item in
+			self.content(item)
+    }
+  }
+  
+  init(filterKey: String, filterValue: String, @ViewBuilder content: @escaping (T) -> Content) {
+    _fetchRequest = FetchRequest<T>(sortDescriptors: [], predicate: NSPredicate(format: "%K BEGINSWITH %@", filterKey, filterValue))
+    self.content = content
+  }
+}
struct FilteredList<T: NSManagedOBject, Context: View>: View {
+  @FetchRequest var fetchRequest: FetchedResults<T>
+  
+  var body: some View {
+    List(fetchRequest, id:\\.self) { item in
+			self.content(item)
+    }
+  }
+  
+  init(filterKey: String, filterValue: String, @ViewBuilder content: @escaping (T) -> Content) {
+    _fetchRequest = FetchRequest<T>(sortDescriptors: [], predicate: NSPredicate(format: "%K BEGINSWITH %@", filterKey, filterValue))
+    self.content = content
+  }
+}
`,67),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const u=s(p,[["render",t]]);export{d as __pageData,u as default}; diff --git a/assets/frontend_others_HackingWithSwift-2.md.85e4f87a.lean.js b/assets/frontend_others_HackingWithSwift-2.md.85e4f87a.lean.js new file mode 100644 index 000000000..a2ee2d005 --- /dev/null +++ b/assets/frontend_others_HackingWithSwift-2.md.85e4f87a.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"HackingWithSwift-2","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/others/HackingWithSwift-2.md","filePath":"frontend/others/HackingWithSwift-2.md","lastUpdated":1708510102000}'),p={name:"frontend/others/HackingWithSwift-2.md"},o=l("",67),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const u=s(p,[["render",t]]);export{d as __pageData,u as default}; diff --git "a/assets/frontend_others_SwiftUI\345\205\245\351\227\250.md.f738d450.js" "b/assets/frontend_others_SwiftUI\345\205\245\351\227\250.md.f738d450.js" new file mode 100644 index 000000000..6c57cd354 --- /dev/null +++ "b/assets/frontend_others_SwiftUI\345\205\245\351\227\250.md.f738d450.js" @@ -0,0 +1,319 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"SwiftUI入门","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/others/SwiftUI入门.md","filePath":"frontend/others/SwiftUI入门.md","lastUpdated":1708510102000}'),p={name:"frontend/others/SwiftUI入门.md"},o=l(`

SwiftUI入门

MVVM

MVVM是一种架构设计范式,把数据和视图分离开,Model和View必须通过ViewModel通信。

Model

数据模型,负责数据和逻辑的处理,独立于UI界面,数据流(data flows)在映射到视图中的过程是只读

View

渲染UI界面,展示Model数据,声明式(为UI声明的方法,在任何时候做它们应做的事情)、无状态的(不需要关心任何状态变化)、响应式的(跟随Model数据变化重新渲染)。

ViewModel

执行解释工作(interpreter),绑定View和Model。ViewModel关注Model中的变化(notices changes),然后把Model的数据变更发布出去(publishes changed),订阅了(subsrcbes)某个发布(publication)的View会进行rebuild。

ViewModel没有指向View的指针,不直接与View对话,如果View订阅了某个发布,就会询问ViewModel怎么适应变化,这个过程不会涉及Model,因为ViewModel的作用就是解释Model的变化。

MVVM的Processes Intent

MVVM有一个对应的关联架构,是Model-View-Intent。如果用户意图(intent)做一些操作,那么这些Intent就要进行View到Model这个反向传递过程。而swiftUI还没有进行这个设计,所以我们用下面一系列操作来处理Intent:

  • View Calls Intent function 视图调用方法
  • ViewModel modifies the Model 视图模型修改模型
  • Model changes 模型改动变化
  • ViewModel notices changes and publishes 模型关注到变化并发布
  • View whitch subscribes Reflect the Model 订阅变化的视图进行模型映射

对比MVVM的映射过程,多了ViewModel处理View操作,并且修改Model这两个操作。

https://www.jianshu.com/p/c14c70c0c9f7

Layout

HStack and VStack

stacks划分提供给自身的空间,然后把空间分配给内部的视图。优先给least flexible的子视图分配空间。

  • Example of inflexible view : Image,Image视图需要一个固定尺寸
  • Another example(slightly more flexible): Text,需要一个完全适合内部文本的尺寸
  • Example of a very flexible View: RoundedRectangle,总是使用所有可用的空间

在给一个视图它需要的空间后,这块空间从可用空间中被移除,然后stack继续给下一个least flexible的视图分配空间。very flexible views最后会平分空间。

在子视图选择了它们的尺寸后,stack会调整自己的size来适应它们,如果有very flexible的子视图,那么这个stack也会变得very flexible

.layoutPriority(Double)

可以使用.layoutPriority(Double)改变获取空间的优先级,默认值为0。.layoutPriority(Double)的优先级要比least flexible更高。

alignment

why .leading instead of .left?Stacks会根据语言环境判断对齐方式,例如有些语言(阿拉伯语)的文本是从右向左的。

LazyHStack and LazyVStack

不会build不可见的视图内容,通常用在ScrollView中

ScrollView

占据所有可用空间,子视图大小根据滚动轴调整

List、Form、OutlineGroup

really smart VStacks

.backgroup 修饰符

Text("hello").backgroup(Rectangle().foregroundColor(.red)),效果类似ZStack(Text在上),但是区别是这个例子中最终的View大小是由Text决定的

.overlay 修饰符

swift
Image(systemName: "folder")
+   .font(.system(size: 55, weight: .thin))
+   .overlay(Text("❤️"), alignment: .bottom)
Image(systemName: "folder")
+   .font(.system(size: 55, weight: .thin))
+   .overlay(Text("❤️"), alignment: .bottom)

视图的大小由Image决定,Text会堆叠在Image上,底部对齐

Modifiers

所有修饰符都会返回一个View

Example

swift
HStack{
+  ForEach(viewModel.cards) { card in
+ 		 CardView(card: card).aspectRatio(2/3, contentMode: .fit)                       
+  }
+}
+.foregroundColor(.orange)
+.padding(10)
HStack{
+  ForEach(viewModel.cards) { card in
+ 		 CardView(card: card).aspectRatio(2/3, contentMode: .fit)                       
+  }
+}
+.foregroundColor(.orange)
+.padding(10)
  • 首先被提供空间的是.padding(10)
  • 然后内边距10的空间会提供给.foregroudColor
  • 最后所有空间被提供给HStack
  • 然后空间被平均分给.aspectRatio
  • 每个.aspectRatio会设置宽度,然后遵循2/3的长宽比设置高度,或者在HStack高度不足时,占据所有高度,然后按2/3设置宽度。
  • .aspectRatio把所有空间提供给CardView

Spacer(minLength: CGFloat)

总是占据提供给他的所有空间,不绘制任何东西.

Divider()

分割线,在HStack中绘制垂直的线,VStack中是水平线。

@ViewBuilder

@ViewBuilder是一个参数属性,作用于构造视图的闭包参数上,允许闭包提供多个子视图。

swift
@ViewBuilder
+func front(of card: Card) -> some View {
+  let shape = RoundedRectangle(cornerRadius: 20)
+  shape
+  shape.stroke()
+  Text(card.content)
+}
@ViewBuilder
+func front(of card: Card) -> some View {
+  let shape = RoundedRectangle(cornerRadius: 20)
+  shape
+  shape.stroke()
+  Text(card.content)
+}

Property Wrapper

swift
@propertyWrapper
+struct Converter1{
+    let from:String
+    let to:String
+    let rate:Double
+    
+    var value:Double
+    var wrappedValue:String{
+        get{
+            "\\(from)\\(value)"
+        }
+        set{
+            value = Double(newValue) ?? -1
+        }
+    }
+    
+    var projectedValue:String{
+        return "\\(to)\\(value * rate)"
+    }
+    
+    init(initialValue:String,
+         from:String,
+         to:String,
+         rate:Double
+    ) {
+        self.rate = rate
+        self.value = 0
+        self.from = from
+        self.to = to
+        self.wrappedValue = initialValue
+    }
+    
+    
+}
+
+struct TestWraper {
+    @State var myname = ""
+    @Converter1(initialValue: "100", from: "USD", to: "CNY", rate: 6.88)
+    var usd_cny
+    
+    @Converter1(initialValue: "100", from: "CNY", to: "EUR", rate: 0.13)
+    var cny_eur
+    
+    func test1(){
+        print("\\(usd_cny)=\\($usd_cny)")
+        print("\\(cny_eur)=\\($cny_eur)")
+    }
+    /*
+     USD100.0=CNY688.0
+     CNY100.0=EUR13.0
+     */
+}
@propertyWrapper
+struct Converter1{
+    let from:String
+    let to:String
+    let rate:Double
+    
+    var value:Double
+    var wrappedValue:String{
+        get{
+            "\\(from)\\(value)"
+        }
+        set{
+            value = Double(newValue) ?? -1
+        }
+    }
+    
+    var projectedValue:String{
+        return "\\(to)\\(value * rate)"
+    }
+    
+    init(initialValue:String,
+         from:String,
+         to:String,
+         rate:Double
+    ) {
+        self.rate = rate
+        self.value = 0
+        self.from = from
+        self.to = to
+        self.wrappedValue = initialValue
+    }
+    
+    
+}
+
+struct TestWraper {
+    @State var myname = ""
+    @Converter1(initialValue: "100", from: "USD", to: "CNY", rate: 6.88)
+    var usd_cny
+    
+    @Converter1(initialValue: "100", from: "CNY", to: "EUR", rate: 0.13)
+    var cny_eur
+    
+    func test1(){
+        print("\\(usd_cny)=\\($usd_cny)")
+        print("\\(cny_eur)=\\($cny_eur)")
+    }
+    /*
+     USD100.0=CNY688.0
+     CNY100.0=EUR13.0
+     */
+}
  • 属性包装器必须有一个包装值,名为wrappedValue的计算属性
  • 预计值为projectedValue,访问预计值的方式为.$属性名projectedValue是只读的。

Property Wrapper使用限制

  • protocol中无法使用
  • 通过wrapper包装的实例属性不能在extension中声明
  • 不能在enum中声明
  • class中通过wrapper包装的属性无法被另外一个属性通过override覆盖
  • 通过wrapper包装的实例属性不能用lazy@NSCopying@NSManagedweakunowned修饰

@State

  • 视图是只读的

    所有视图的struct都是完全、彻底只读的,所以View中只有letcomputed(常量和计算属性)才有意义。(被@ObservedObject装饰的属性除外,这种属性必须被标记为var

  • 为什么

    View一直在被创建、丢弃,只有body才会存在很久,所以View不太需要一些需要被修改的属性

don't worry,之所以这样是因为View应该是stateless的,只负责渲染model,不需要自身具有什么状态属性。但是极少数情况下View也是需要状态的(it turns out there are a few rare times when a View needs some state),但这种状态存储总是暂时的(always temporary),所有持久化的状态都存在Model中。

例如:进入编辑模式,需要提前收集数据来为用户修改数据的intent作准备,需要暂时展示其他的View(编辑页面)来收集数据,编辑完后需要一个动画效果来关闭这个编辑页面,所以需要一个"编辑模式状态"的属性来标记何时该关闭。

上述场景中可以使用@State来标记这个临时状态存储变量

swift
@State private var somethingTemporary: SomeType //someType can be any struct
@State private var somethingTemporary: SomeType //someType can be any struct

这个临时状态变量是private修饰的,是因为只有当前View能访问这个变量。@State变量的变化会导致这个View的body重新渲染。这和@ObservedObject类似,但是@State作用的是一个随机的数据(值语义),而@ObservedObject作用在ViewModel上(对象语义)。

@ObservedObject

多个视图数据共享和更新时,需要一个数据模型的概念,即多视图的状态可以根据Data-Model进行更新,这种场景下@State就不再适用了。

  • ObservableObject协议定义了一个数据模型的数据发生变化时发布通知的能力

  • @ObservedObject这个属性包装器包装的属性可以监听到数据的变化,也可以利用它去更新数据。

  • @Published这个属性包装器包装的属性,都会被转化为一个publisher(Combine框架的概念),当值发生变化时,会通知系统,然后系统再去更新画面

@StateObject

和@ObservedObject类似,也是修饰对象语义,和@ObservedObject的区别在于,实例是否被创建其的View所持有,其生命周期是否完全可控,@StateObject修饰的属性的生命周期由创建该对象的对象维护(这一点又类似@State)

swift
class DataSource: ObservableObject {
+  @Published var counter = 0
+}
+
+struct Counter: View {
+  @ObservedObject var dataSource = DataSource()
+
+  var body: some View {
+    VStack {
+      Button("Increment counter") {
+        dataSource.counter += 1
+      }
+
+      Text("Count is \\(dataSource.counter)")
+    }
+  }
+}
+
+struct ItemList: View {
+  @State private var items = ["hello", "world"]
+
+  var body: some View {
+    VStack {
+      Button("Append item to list") {
+        items.append("test")
+      }
+
+      List(items, id: \\.self) { name in
+        Text(name)
+      }
+
+      Counter()
+    }
+  }
+}
class DataSource: ObservableObject {
+  @Published var counter = 0
+}
+
+struct Counter: View {
+  @ObservedObject var dataSource = DataSource()
+
+  var body: some View {
+    VStack {
+      Button("Increment counter") {
+        dataSource.counter += 1
+      }
+
+      Text("Count is \\(dataSource.counter)")
+    }
+  }
+}
+
+struct ItemList: View {
+  @State private var items = ["hello", "world"]
+
+  var body: some View {
+    VStack {
+      Button("Append item to list") {
+        items.append("test")
+      }
+
+      List(items, id: \\.self) { name in
+        Text(name)
+      }
+
+      Counter()
+    }
+  }
+}

在这个例子中,每次点击Append item to listButton,counter都会被重置,这是因为每次重新渲染,DataSource()都会被重新创建。解决这个问题有两个方法:

  1. 在ItemList中创建DataSource,并把DataSource传递给Counter
  2. 把@ObservedObject替换为@StateObject

将DataSource标记为@StateObject意味着DataSource被实例化后会保存在Counter的外部,当Counter重新渲染时,会直接用这个值。

@EnvironmentObject

使用@ObservedObject可以在视图间共享数据、刷新画面,但是必须为需要的视图进行引用的传递。如果视图的层级较多,且各个View和子View使用同一个数据模型,那么@ObservedObject的传递将会变得笨重且易出错。

SwiftUI提供了另一种选择,@EnvironmentObject就是把数据模型引用保存到了一个共同的环境变量中,environment是一个共通的存储区域,保存了app的信息和Views,当然也可以保存自定义数据,包括对observable object的引用。

@Environment

@State类似,App也可以响应iOS系统过来的state变化,例如语言环境、字体大小、暗黑模式切换等,为了及时响应这些变化,app可以使用@Environment(KeyPath)来进行获取实时的信息。

Combine框架

@Published属性包装器和ObservableObject的实现定义在Combine框架中。Combine框架中定义了一些协议和数据类型,可以让我们处理数据,当一个代码数据发生变化,可以应用这个框架来通知另外一处代码有新数据可以使用。

这样就会出现两个类型的任务,一个是发布者(publisher),一个是订阅者(subscriber)。发布者决定了数据和错误信息的产生并发给订阅者,订阅者会接受这些信息。

在SwiftUI中,被@Published修饰的属性,会被自动转化为Publisher,ObservableObject协议的实现中,定义了被@Published修饰的属性作为发布者,在属性的值发生变化的时候,发布者将通知订阅者。@ObservedObject@EnvironmentObject修饰的属性,扮演订阅者的角色。

Just发布者和Subscribers.Sink

swift
import Combine
+import Foundation
+
+let myPublisher = Just("55")
+
+let mySubscriber = Subscribers.Sink<String,Never> (receiveCompletion: { completion in
+    if completion == .finished {
+        print("111")
+    }else {
+        print("222")
+    }
+    
+}, receiveValue: { value in
+    print(value)
+})
+
+myPublisher.subscribe(mySubscriber)
import Combine
+import Foundation
+
+let myPublisher = Just("55")
+
+let mySubscriber = Subscribers.Sink<String,Never> (receiveCompletion: { completion in
+    if completion == .finished {
+        print("111")
+    }else {
+        print("222")
+    }
+    
+}, receiveValue: { value in
+    print(value)
+})
+
+myPublisher.subscribe(mySubscriber)

数据的转换

中间发布者

Publishers.Map

Publishers.Filter

...

或Just().操作符

Subjects

Combine还有一种发布者叫Subjects,实现了Subject协议,可以调用send方法发送数据

  • PassthroughSubject()
  • CurrentValueSubject(value)
swift
import Combine
+import Foundation
+
+enum MyErrors: Error {
+    case wrongValue
+}
+
+let myPublisher = PassthroughSubject<String, MyErrors>()
+//let myPublisher = CurrentValueSubject<String, MyErrors>("100")
+
+let mySubscriber = myPublisher.filter({
+    return $0.count < 5
+}).sink(receiveCompletion: {completion in
+    if completion == .failure(MyErrors.wrongValue) {
+        print("MyErrors.wrongValue")
+    }else {
+        print(completion)
+    }
+    
+}, receiveValue: { value in
+    print("value: \\(value)")
+})
+
+
+myPublisher.send("h")
import Combine
+import Foundation
+
+enum MyErrors: Error {
+    case wrongValue
+}
+
+let myPublisher = PassthroughSubject<String, MyErrors>()
+//let myPublisher = CurrentValueSubject<String, MyErrors>("100")
+
+let mySubscriber = myPublisher.filter({
+    return $0.count < 5
+}).sink(receiveCompletion: {completion in
+    if completion == .failure(MyErrors.wrongValue) {
+        print("MyErrors.wrongValue")
+    }else {
+        print(completion)
+    }
+    
+}, receiveValue: { value in
+    print("value: \\(value)")
+})
+
+
+myPublisher.send("h")

.onReceive

SwiftUI中,View协议有一个修饰符.onReceive(Publisher, perform: Closure)把任何View转换成一个订阅者,来接受来自发布者的数据,SwiftUI使UI组件和Combine结合带来了扩展可能。

swift
import SwiftUI
+
+class ContentViewData: ObservableObject {
+  @Published var counter: Int = 0
+  let timePublisher = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
+}
+
+struct ContentView: View {
+  @ObservedObject var contentData = ContentViewData()
+  
+  var body: some View {
+    Text("hello, world! \\(self.contentData.counter)")
+    .onReceive(contentData.timePublisher, perform: { value in
+      self.contentData.counter += 1
+      if self.contentData.counter > 20 {
+        self.contentData.timePublisher.upstream.connect().cancel()
+        print("stop")
+      }
+    })
+  }
+}
import SwiftUI
+
+class ContentViewData: ObservableObject {
+  @Published var counter: Int = 0
+  let timePublisher = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
+}
+
+struct ContentView: View {
+  @ObservedObject var contentData = ContentViewData()
+  
+  var body: some View {
+    Text("hello, world! \\(self.contentData.counter)")
+    .onReceive(contentData.timePublisher, perform: { value in
+      self.contentData.counter += 1
+      if self.contentData.counter > 20 {
+        self.contentData.timePublisher.upstream.connect().cancel()
+        print("stop")
+      }
+    })
+  }
+}
`,93),e=[o];function t(c,r,E,y,i,u){return a(),n("div",null,e)}const C=s(p,[["render",t]]);export{d as __pageData,C as default}; diff --git "a/assets/frontend_others_SwiftUI\345\205\245\351\227\250.md.f738d450.lean.js" "b/assets/frontend_others_SwiftUI\345\205\245\351\227\250.md.f738d450.lean.js" new file mode 100644 index 000000000..2d8396d4c --- /dev/null +++ "b/assets/frontend_others_SwiftUI\345\205\245\351\227\250.md.f738d450.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"SwiftUI入门","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/others/SwiftUI入门.md","filePath":"frontend/others/SwiftUI入门.md","lastUpdated":1708510102000}'),p={name:"frontend/others/SwiftUI入门.md"},o=l("",93),e=[o];function t(c,r,E,y,i,u){return a(),n("div",null,e)}const C=s(p,[["render",t]]);export{d as __pageData,C as default}; diff --git "a/assets/frontend_others_swift\350\257\255\346\263\225.md.9c4e5628.js" "b/assets/frontend_others_swift\350\257\255\346\263\225.md.9c4e5628.js" new file mode 100644 index 000000000..bd4343f48 --- /dev/null +++ "b/assets/frontend_others_swift\350\257\255\346\263\225.md.9c4e5628.js" @@ -0,0 +1,305 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"swift语法","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/others/swift语法.md","filePath":"frontend/others/swift语法.md","lastUpdated":1708510102000}'),p={name:"frontend/others/swift语法.md"},o=l(`

swift语法

英文原版:https://docs.swift.org/swift-book/

中文版:https://swiftgg.gitbook.io/swift/

可选类型

*可选类型(optionals)*来处理值可能缺失的情况。可选类型实际也是一个枚举:

swift
enum Optional<T> {
+  case none
+  case some(T) 
+}
enum Optional<T> {
+  case none
+  case some(T) 
+}

nil

如果声明一个可选常量或者变量但是没有赋值,它们会自动被设置为nil

swift
var hello: String?							var hello: Optional<String> = .none
+var hello: String? = "hello"    var hello: Optional<String> = .some("hello")
+var hello: String? = nil        var hello: Optional<String> = .none
var hello: String?							var hello: Optional<String> = .none
+var hello: String? = "hello"    var hello: Optional<String> = .some("hello")
+var hello: String? = nil        var hello: Optional<String> = .none
swift
var surveyAnswer: String? 
+// surveyAnswer 会被自动设置为 nil -> var surveyAnswer: String? = nil
var surveyAnswer: String? 
+// surveyAnswer 会被自动设置为 nil -> var surveyAnswer: String? = nil

强制解析

swift
let hello: String? = ...
+print(hello!)
let hello: String? = ...
+print(hello!)

等价于

swift
switch hello {
+  case .none: //raise an exception(crash)
+  case .some(let data): print(data)
+}
switch hello {
+  case .none: //raise an exception(crash)
+  case .some(let data): print(data)
+}

可选绑定

强制解析可能会导致异常,可以使用if let来安全的获取可选类型中的值

swift
if let safehello = hello {
+  print(safehello)
+} else {
+  //do something else
+}
if let safehello = hello {
+  print(safehello)
+} else {
+  //do something else
+}

等价于

swift
switch hello {
+  case .none: //do something else
+  case .some(let data): print(data)
+}
switch hello {
+  case .none: //do something else
+  case .some(let data): print(data)
+}

使用可选绑定时后面不能用&&,可以用,隔开语句

空合运算符(Nil Coalescing Operator)

空合运算符a ?? b)将对可选类型 a 进行空判断,如果 a 包含一个值就进行解包,否则就返回一个默认值 b。表达式 a 必须是 Optional 类型。默认值 b 的类型必须要和 a 存储值的类型保持一致。

空合运算符是对以下代码的简短表达方法:

swift
a != nil ? a! : b
a != nil ? a! : b

例子:

swift
let x: String? = ...
+let y = x ?? z
let x: String? = ...
+let y = x ?? z

实际等价于上:

swift
switch a {
+  case .none: y = z
+  case .some(let data): y = data
+}
switch a {
+  case .none: y = z
+  case .some(let data): y = data
+}

可选链

可选链式调用是一种可以在当前值可能为 nil 的可选值上请求和调用属性、方法及下标的方法。如果可选值有值,那么调用就会成功;如果可选值是 nil,那么调用将返回 nil。多个调用可以连接在一起形成一个调用链,如果其中任何一个节点为 nil,整个调用链都会失败,即返回 nil

swift
let x: String = ...
+let y = x?.foo()?.bar?.z
let x: String = ...
+let y = x?.foo()?.bar?.z

等价于

swift
switch x {
+  case .none: y = nil
+  case .some(let xval):
+  	switch xval.foo() {
+      case .none: y = nil
+      case .some(let xfooval):
+      	switch xfooval.bar {
+          case .none: y = nil
+          case .some(let xfbval): y = xfbval.z
+        }
+    }
+}
switch x {
+  case .none: y = nil
+  case .some(let xval):
+  	switch xval.foo() {
+      case .none: y = nil
+      case .some(let xfooval):
+      	switch xfooval.bar {
+          case .none: y = nil
+          case .some(let xfbval): y = xfbval.z
+        }
+    }
+}

闭包

闭包表达式

swift
{ (parameters) -> return_type in
+    statements
+}
{ (parameters) -> return_type in
+    statements
+}

尾随闭包(Trailing Closures)

尾随闭包是一个书写在函数圆括号之后的闭包表达式,函数支持将其作为最后一个参数调用。在使用尾随闭包时,不用写出它的参数标签

例如:

swift
ForEach(modelData.categories.keys.sorted(), id: \\.self) { key in
+    CategoryRow(categoryName: key, items: modelData.categories[key]!)
+}
ForEach(modelData.categories.keys.sorted(), id: \\.self) { key in
+    CategoryRow(categoryName: key, items: modelData.categories[key]!)
+}

多重尾随闭包(multiple trailing closure)

swift
struct SignInView: View {
+    var body: some View {
+        Button {
+            showingProfile.toggle()
+        } label: {
+            Label("User Profile", systemImage: "person.crop.circle")
+        }
+    }
+}
struct SignInView: View {
+    var body: some View {
+        Button {
+            showingProfile.toggle()
+        } label: {
+            Label("User Profile", systemImage: "person.crop.circle")
+        }
+    }
+}

方法

在实例方法中修改值类型

结构体和枚举是值类型。默认情况下,值类型的属性不能在它的实例方法中被修改。

如果确实需要在某个特定的方法中修改结构体或者枚举的属性,可以为这个方法选择 可变(mutating)

swift
struct Point {
+    var x = 0.0, y = 0.0
+    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
+        x += deltaX
+        y += deltaY
+    }
+}
+var somePoint = Point(x: 1.0, y: 1.0)
+somePoint.moveBy(x: 2.0, y: 3.0)
+print("The point is now at (\\(somePoint.x), \\(somePoint.y))")
+// 打印“The point is now at (3.0, 4.0)”
struct Point {
+    var x = 0.0, y = 0.0
+    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
+        x += deltaX
+        y += deltaY
+    }
+}
+var somePoint = Point(x: 1.0, y: 1.0)
+somePoint.moveBy(x: 2.0, y: 3.0)
+print("The point is now at (\\(somePoint.x), \\(somePoint.y))")
+// 打印“The point is now at (3.0, 4.0)”

属性

计算属性

计算属性不直接存储值,而是提供一个 getter 和一个可选的 setter,来间接获取和设置其他属性或变量的值。

swift
struct Point {
+    var x = 0.0, y = 0.0
+}
+struct Size {
+    var width = 0.0, height = 0.0
+}
+struct Rect {
+    var origin = Point()
+    var size = Size()
+    var center: Point {
+        get {
+            let centerX = origin.x + (size.width / 2)
+            let centerY = origin.y + (size.height / 2)
+            return Point(x: centerX, y: centerY)
+        }
+        set(newCenter) {
+            origin.x = newCenter.x - (size.width / 2)
+            origin.y = newCenter.y - (size.height / 2)
+        }
+    }
+}
+var square = Rect(origin: Point(x: 0.0, y: 0.0),
+    size: Size(width: 10.0, height: 10.0))
+let initialSquareCenter = square.center
+square.center = Point(x: 15.0, y: 15.0)
+print("square.origin is now at (\\(square.origin.x), \\(square.origin.y))")
+// 打印“square.origin is now at (10.0, 10.0)”
struct Point {
+    var x = 0.0, y = 0.0
+}
+struct Size {
+    var width = 0.0, height = 0.0
+}
+struct Rect {
+    var origin = Point()
+    var size = Size()
+    var center: Point {
+        get {
+            let centerX = origin.x + (size.width / 2)
+            let centerY = origin.y + (size.height / 2)
+            return Point(x: centerX, y: centerY)
+        }
+        set(newCenter) {
+            origin.x = newCenter.x - (size.width / 2)
+            origin.y = newCenter.y - (size.height / 2)
+        }
+    }
+}
+var square = Rect(origin: Point(x: 0.0, y: 0.0),
+    size: Size(width: 10.0, height: 10.0))
+let initialSquareCenter = square.center
+square.center = Point(x: 15.0, y: 15.0)
+print("square.origin is now at (\\(square.origin.x), \\(square.origin.y))")
+// 打印“square.origin is now at (10.0, 10.0)”

简化 Setter 声明

如果计算属性的 setter 没有定义表示新值的参数名,则可以使用默认名称 newValue。下面是使用了简化 setter 声明的 Rect 结构体代码:

swift
struct AlternativeRect {
+    var origin = Point()
+    var size = Size()
+    var center: Point {
+        get {
+            let centerX = origin.x + (size.width / 2)
+            let centerY = origin.y + (size.height / 2)
+            return Point(x: centerX, y: centerY)
+        }
+        set {
+            origin.x = newValue.x - (size.width / 2)
+            origin.y = newValue.y - (size.height / 2)
+        }
+    }
+}
struct AlternativeRect {
+    var origin = Point()
+    var size = Size()
+    var center: Point {
+        get {
+            let centerX = origin.x + (size.width / 2)
+            let centerY = origin.y + (size.height / 2)
+            return Point(x: centerX, y: centerY)
+        }
+        set {
+            origin.x = newValue.x - (size.width / 2)
+            origin.y = newValue.y - (size.height / 2)
+        }
+    }
+}

简化 Getter 声明

如果整个 getter 是单一表达式,getter 会隐式地返回这个表达式结果。下面是另一个版本的 Rect 结构体,用到了简化的 getter 和 setter 声明:

swift
struct CompactRect {
+    var origin = Point()
+    var size = Size()
+    var center: Point {
+        get {
+            Point(x: origin.x + (size.width / 2),
+                  y: origin.y + (size.height / 2))
+        }
+        set {
+            origin.x = newValue.x - (size.width / 2)
+            origin.y = newValue.y - (size.height / 2)
+        }
+    }
+}
struct CompactRect {
+    var origin = Point()
+    var size = Size()
+    var center: Point {
+        get {
+            Point(x: origin.x + (size.width / 2),
+                  y: origin.y + (size.height / 2))
+        }
+        set {
+            origin.x = newValue.x - (size.width / 2)
+            origin.y = newValue.y - (size.height / 2)
+        }
+    }
+}

在 getter 中忽略 return 与在函数中忽略 return 的规则相同.

只读计算属性

只有 getter 没有 setter 的计算属性叫只读计算属性。只读计算属性总是返回一个值,可以通过点运算符访问,但不能设置新的值。

注意

必须使用 var 关键字定义计算属性,包括只读计算属性,因为它们的值不是固定的。let 关键字只用来声明常量属性,表示初始化后再也无法修改的值。

只读计算属性的声明可以去掉 get 关键字和花括号:

swift
struct Cuboid {
+    var width = 0.0, height = 0.0, depth = 0.0
+    var volume: Double {
+    	return width * height * depth
+    }
+}
+let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)
+print("the volume of fourByFiveByTwo is \\(fourByFiveByTwo.volume)")
+// 打印“the volume of fourByFiveByTwo is 40.0”
struct Cuboid {
+    var width = 0.0, height = 0.0, depth = 0.0
+    var volume: Double {
+    	return width * height * depth
+    }
+}
+let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)
+print("the volume of fourByFiveByTwo is \\(fourByFiveByTwo.volume)")
+// 打印“the volume of fourByFiveByTwo is 40.0”

属性观察器

属性观察器监控和响应属性值的变化,每次属性被设置值的时候都会调用属性观察器,即使新值和当前值相同的时候也不例外。

可以在以下位置添加属性观察器:

  • 自定义的存储属性
  • 继承的存储属性
  • 继承的计算属性

可以为属性添加其中一个或两个观察器:

  • willSet 在新的值被设置之前调用
  • didSet 在新的值被设置之后调用

willSet 观察器会将新的属性值作为常量参数传入,在 willSet 的实现代码中可以为这个参数指定一个名称,如果不指定则参数仍然可用,这时使用默认名称 newValue 表示。

同样,didSet 观察器会将旧的属性值作为参数传入,可以为该参数指定一个名称或者使用默认参数名 oldValue。如果在 didSet 方法中再次对该属性赋值,那么新值会覆盖旧的值。

在父类初始化方法调用之后,在子类构造器中给父类的属性赋值时,会调用父类属性的 willSetdidSet 观察器。而在父类初始化方法调用之前,给子类的属性赋值时不会调用子类属性的观察器。

swift
class StepCounter {
+    var totalSteps: Int = 0 {
+        willSet(newTotalSteps) {
+            print("将 totalSteps 的值设置为 \\(newTotalSteps)")
+        }
+        didSet {
+            if totalSteps > oldValue  {
+                print("增加了 \\(totalSteps - oldValue) 步")
+            }
+        }
+    }
+}
+let stepCounter = StepCounter()
+stepCounter.totalSteps = 200
+// 将 totalSteps 的值设置为 200
+// 增加了 200 步
+stepCounter.totalSteps = 360
+// 将 totalSteps 的值设置为 360
+// 增加了 160 步
+stepCounter.totalSteps = 896
+// 将 totalSteps 的值设置为 896
+// 增加了 536 步
class StepCounter {
+    var totalSteps: Int = 0 {
+        willSet(newTotalSteps) {
+            print("将 totalSteps 的值设置为 \\(newTotalSteps)")
+        }
+        didSet {
+            if totalSteps > oldValue  {
+                print("增加了 \\(totalSteps - oldValue) 步")
+            }
+        }
+    }
+}
+let stepCounter = StepCounter()
+stepCounter.totalSteps = 200
+// 将 totalSteps 的值设置为 200
+// 增加了 200 步
+stepCounter.totalSteps = 360
+// 将 totalSteps 的值设置为 360
+// 增加了 160 步
+stepCounter.totalSteps = 896
+// 将 totalSteps 的值设置为 896
+// 增加了 536 步

protocol

sort of a "stripped-down" struct/class

有函数和变量,但是没有具体实现,类似interface, 当实现一个协议时,必须实现协议中所有的函数和变量

使用protocol来限制entension:

swift
extension Array where Element: Hashable {...}
extension Array where Element: Hashable {...}

使用protocol来限制函数:

swift
init(data: Data) where Data: Collection, Data.Element: Identifiable
init(data: Data) where Data: Collection, Data.Element: Identifiable

protocol extension

可以通过extension给protocol的func或var添加默认的实现

swift
struct Tesla: Vehicle {
+  //...
+}
+
+extension Vehicle {
+  fun registerWithDMV() { // actual implementation }
+}
struct Tesla: Vehicle {
+  //...
+}
+
+extension Vehicle {
+  fun registerWithDMV() { // actual implementation }
+}
swift
protocol View {
+  var body: some View
+}
protocol View {
+  var body: some View
+}
swift
extension View {
+  func foregroundColor(_ color: Color) -> some View { /* implementation */ }
+  func font(_ font: Font?) -> some View { /* implementation */ }
+  ...
+}
extension View {
+  func foregroundColor(_ color: Color) -> some View { /* implementation */ }
+  func font(_ font: Font?) -> some View { /* implementation */ }
+  ...
+}

generics + protocols

swift
protocol Identifiable {
+  associatedtype ID
+  var id: ID { get }
+}
protocol Identifiable {
+  associatedtype ID
+  var id: ID { get }
+}
`,86),e=[o];function t(c,r,y,E,i,F){return a(),n("div",null,e)}const C=s(p,[["render",t]]);export{h as __pageData,C as default}; diff --git "a/assets/frontend_others_swift\350\257\255\346\263\225.md.9c4e5628.lean.js" "b/assets/frontend_others_swift\350\257\255\346\263\225.md.9c4e5628.lean.js" new file mode 100644 index 000000000..abd1d7368 --- /dev/null +++ "b/assets/frontend_others_swift\350\257\255\346\263\225.md.9c4e5628.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"swift语法","description":"","frontmatter":{},"headers":[],"relativePath":"frontend/others/swift语法.md","filePath":"frontend/others/swift语法.md","lastUpdated":1708510102000}'),p={name:"frontend/others/swift语法.md"},o=l("",86),e=[o];function t(c,r,y,E,i,F){return a(),n("div",null,e)}const C=s(p,[["render",t]]);export{h as __pageData,C as default}; diff --git a/assets/golang_base_GoTemplate.md.26e53b7c.js b/assets/golang_base_GoTemplate.md.26e53b7c.js new file mode 100644 index 000000000..daf0796a7 --- /dev/null +++ b/assets/golang_base_GoTemplate.md.26e53b7c.js @@ -0,0 +1,391 @@ +import{_ as s,o as n,c as a,Q as p}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"Go Template","description":"","frontmatter":{},"headers":[],"relativePath":"golang/base/GoTemplate.md","filePath":"golang/base/GoTemplate.md","lastUpdated":1708510102000}'),l={name:"golang/base/GoTemplate.md"},o=p(`

Go Template

Go Template是一种用于生成文本输出的模板引擎,它是Go语言标准库中内置的一部分。Go Template使用简单而强大的语法来描述要生成的最终文本的结构和内容。

Go Template的语法是基于文本插值的思想,通过在模板文件中插入占位符和控制指令来控制输出的结果。模板可以包含静态文本和动态值,并且可以使用控制指令来迭代、条件判断和执行其他逻辑操作。

示例

html
<!--test.html-->
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <title>Go Web</title>
+</head>
+<body>
+{{ . }}
+</body>
+</html>
<!--test.html-->
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <title>Go Web</title>
+</head>
+<body>
+{{ . }}
+</body>
+</html>
go
package main
+
+import (
+	"html/template"
+	"net/http"
+)
+
+func tmpl(w http.ResponseWriter, r *http.Request) {
+	t1, err := template.ParseFiles("test.html")
+	if err != nil {
+		panic(err)
+	}
+	t1.Execute(w, "hello world")
+}
+
+func main() {
+	server := http.Server{
+		Addr: "127.0.0.1:8080",
+	}
+	http.HandleFunc("/", tmpl)
+	server.ListenAndServe()
+}
package main
+
+import (
+	"html/template"
+	"net/http"
+)
+
+func tmpl(w http.ResponseWriter, r *http.Request) {
+	t1, err := template.ParseFiles("test.html")
+	if err != nil {
+		panic(err)
+	}
+	t1.Execute(w, "hello world")
+}
+
+func main() {
+	server := http.Server{
+		Addr: "127.0.0.1:8080",
+	}
+	http.HandleFunc("/", tmpl)
+	server.ListenAndServe()
+}

.和作用域

在写template的时候,会经常用到"."。

在template中,点"."代表当前作用域的当前对象。它类似于java/c++的this关键字,类似于perl/python的self。

go
type Person struct {
+	Name string
+	Age  int
+}
+
+func main(){
+	p := Person{"leo",23}
+	tmpl, _ := template.New("test").Parse("Name: {{.Name}}, Age: {{.Age}}")
+	_ = tmpl.Execute(os.Stdout, p)
+}
+// Name: leo, Age: 23
type Person struct {
+	Name string
+	Age  int
+}
+
+func main(){
+	p := Person{"leo",23}
+	tmpl, _ := template.New("test").Parse("Name: {{.Name}}, Age: {{.Age}}")
+	_ = tmpl.Execute(os.Stdout, p)
+}
+// Name: leo, Age: 23

但是并非只有一个顶级作用域,range、with、if等内置action都有自己的本地作用域。

go
package main
+
+import (
+	"os"
+	"text/template"
+)
+
+type Friend struct {
+	Fname string
+}
+type Person struct {
+	UserName string
+	Emails   []string
+	Friends  []*Friend
+}
+
+func main() {
+	f1 := Friend{Fname: "xiaofang"}
+	f2 := Friend{Fname: "wugui"}
+	t := template.New("test")
+	t = template.Must(t.Parse(
+		\`hello {{.UserName}}!
+{{ range .Emails }}
+an email {{ . }}
+{{- end }}
+{{ with .Friends }}
+{{- range . }}
+my friend name is {{.Fname}}
+{{- end }}
+{{ end }}\`))
+	p := Person{UserName: "test",
+		Emails:  []string{"a1@qq.com", "a2@gmail.com"},
+		Friends: []*Friend{&f1, &f2}}
+	t.Execute(os.Stdout, p)
+}
package main
+
+import (
+	"os"
+	"text/template"
+)
+
+type Friend struct {
+	Fname string
+}
+type Person struct {
+	UserName string
+	Emails   []string
+	Friends  []*Friend
+}
+
+func main() {
+	f1 := Friend{Fname: "xiaofang"}
+	f2 := Friend{Fname: "wugui"}
+	t := template.New("test")
+	t = template.Must(t.Parse(
+		\`hello {{.UserName}}!
+{{ range .Emails }}
+an email {{ . }}
+{{- end }}
+{{ with .Friends }}
+{{- range . }}
+my friend name is {{.Fname}}
+{{- end }}
+{{ end }}\`))
+	p := Person{UserName: "test",
+		Emails:  []string{"a1@qq.com", "a2@gmail.com"},
+		Friends: []*Friend{&f1, &f2}}
+	t.Execute(os.Stdout, p)
+}

输出:

hello test!
+
+an email a1@qq.com
+an email a2@gmail.com
+
+my friend name is xiaofang
+my friend name is wugui
hello test!
+
+an email a1@qq.com
+an email a2@gmail.com
+
+my friend name is xiaofang
+my friend name is wugui

去除空白

template引擎在进行替换的时候,是完全按照文本格式进行替换的。除了需要评估和替换的地方,所有的行分隔符、空格等等空白都原样保留。所以, 对于要解析的内容,不要随意缩进、随意换行

go
//可以在\`{{</span>\`符号的后面加上短横线并保留一个或多个空格"- "来去除它前面的空白(包括换行符、制表符、空格等)
+//,即\`{{- xxxx\`。
+//在\`}}\`的前面加上一个或多个空格以及一个短横线"-"来去除它后面的空白,即\`xxxx -}}\`。
+
+{{23}} < {{45}}        -> 23 < 45
+{{23}} < {{- 45}}      ->  23 <45
+{{23 -}} < {{45}}      ->  23< 45
+{{23 -}} < {{- 45}}    ->  23<45
//可以在\`{{</span>\`符号的后面加上短横线并保留一个或多个空格"- "来去除它前面的空白(包括换行符、制表符、空格等)
+//,即\`{{- xxxx\`。
+//在\`}}\`的前面加上一个或多个空格以及一个短横线"-"来去除它后面的空白,即\`xxxx -}}\`。
+
+{{23}} < {{45}}        -> 23 < 45
+{{23}} < {{- 45}}      ->  23 <45
+{{23 -}} < {{45}}      ->  23< 45
+{{23 -}} < {{- 45}}    ->  23<45

上面的例子

go
t.Parse(
+\`hello {{.UserName}}!
+{{ range .Emails }}
+an email {{ . }}
+{{- end }}
+{{ with .Friends }}
+{{- range . }}
+my friend name is {{.Fname}}
+{{- end }}
+{{ end }}\`)
t.Parse(
+\`hello {{.UserName}}!
+{{ range .Emails }}
+an email {{ . }}
+{{- end }}
+{{ with .Friends }}
+{{- range . }}
+my friend name is {{.Fname}}
+{{- end }}
+{{ end }}\`)

注意,上面没有进行缩进。因为缩进的制表符或空格在替换的时候会保留。

注释

注释方式:

go
{{/* a comment */}}
{{/* a comment */}}

注释后的内容不会被引擎进行替换。但需要注意,注释行在替换的时候也会占用行,所以应该去除前缀和后缀空白,否则会多一空行。

go
{{- /* a comment without prefix/suffix space */}}
+{{/* a comment without prefix/suffix space */ -}}
+{{- /* a comment without prefix/suffix space */ -}}
{{- /* a comment without prefix/suffix space */}}
+{{/* a comment without prefix/suffix space */ -}}
+{{- /* a comment without prefix/suffix space */ -}}

注意,应该只去除前缀或后缀空白,不要同时都去除,否则会破坏原有的格式。

管道pipeline

pipeline是指产生数据的操作。

可以使用管道符号|链接多个命令,用法和unix下的管道类似:|前面的命令将运算结果(或返回值)传递给后一个命令的最后一个位置。

例如:

go
{{.}} | printf "%s\\n" "abcd"
{{.}} | printf "%s\\n" "abcd"

命令可以有超过1个的返回值,这时第二个返回值必须为err类型。

需要注意的是,并非只有使用了|才是pipeline。Go template中,pipeline的概念是传递数据,只要能产生数据的,都是pipeline。这使得某些操作可以作为另一些操作内部的表达式先运行得到结果,就像是Unix下的命令替换一样。

例如,下面的(len "output")是pipeline,它整体先运行。

go
{{println (len "output")}}
{{println (len "output")}}

下面是Pipeline的几种示例,它们都输出"output"

go
{{\`"output"\`}}
+{{printf "%q" "output"}}
+{{"output" | printf "%q"}}
+{{printf "%q" (print "out" "put")}}
+{{"put" | printf "%s%s" "out" | printf "%q"}}
+{{"output" | printf "%s" | printf "%q"}}
{{\`"output"\`}}
+{{printf "%q" "output"}}
+{{"output" | printf "%q"}}
+{{printf "%q" (print "out" "put")}}
+{{"put" | printf "%s%s" "out" | printf "%q"}}
+{{"output" | printf "%s" | printf "%q"}}

变量

可以在template中定义变量:

go
// 未定义过的变量
+$var := pipeline
+
+// 已定义过的变量
+$var = pipeline
// 未定义过的变量
+$var := pipeline
+
+// 已定义过的变量
+$var = pipeline
go
{{- $how_long :=(len "output")}}
+{{- println $how_long}}   // 输出6
{{- $how_long :=(len "output")}}
+{{- println $how_long}}   // 输出6
go
tx := template.Must(template.New("hh").Parse(
+\`{{range $x := . -}}
+{{$y := 333}}
+{{- if (gt $x 33)}}{{println $x $y ($z := 444)}}{{- end}}
+{{- end}}
+\`))
+s := []int{11, 22, 33, 44, 55}
+_ = tx.Execute(os.Stdout, s)
+//44 333 444
+//55 333 444
tx := template.Must(template.New("hh").Parse(
+\`{{range $x := . -}}
+{{$y := 333}}
+{{- if (gt $x 33)}}{{println $x $y ($z := 444)}}{{- end}}
+{{- end}}
+\`))
+s := []int{11, 22, 33, 44, 55}
+_ = tx.Execute(os.Stdout, s)
+//44 333 444
+//55 333 444

上面的示例中,使用range迭代slice,每个元素都被赋值给变量$x,每次迭代过程中,都新设置一个变量$y ,在内层嵌套的if结构中,可以使用这个两个外层的变量。在if的条件表达式中,使用了一个内置的比较函数gt,如果$x 大于33,则为true。在println的参数中还定义了一个$z,之所以能定义,是因为($z := 444)的过程是一个Pipeline,可以先运行。

需要注意三点:

  1. 变量有作用域,只要出现end,则当前层次的作用域结束。内层可以访问外层变量,但外层不能访问内层变量
  2. 有一个特殊变量$,它代表模板的最顶级作用域对象(通俗地理解,是以模板为全局作用域的全局变量),在Execute() 执行的时候进行赋值,且一直不变。例如上面的示例中,$ = [11 22 33 44 55]。再例如,define定义了一个模板t1,则t1中的$ 作用域只属于这个t1。
  3. 变量不可在模板之间继承。普通变量可能比较容易理解,但对于特殊变量"."和"$",比较容易搞混。见下面的例子。

条件判断

go
{{if pipeline}} T1 {{end}}
+{{if pipeline}} T1 {{else}} T0 {{end}}
+{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
+{{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}
{{if pipeline}} T1 {{end}}
+{{if pipeline}} T1 {{else}} T0 {{end}}
+{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
+{{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}

需要注意的是,pipeline为false的情况是各种数据对象的0值:数值0,指针或接口是nil,数组、slice、map或string则是len为0。

range...end迭代

有两种迭代表达式

go
{{range pipeline}} T1 {{end}}
+{{range pipeline}} T1 {{else}} T0 {{end}}
{{range pipeline}} T1 {{end}}
+{{range pipeline}} T1 {{else}} T0 {{end}}

range可以迭代slice、数组、map或channel。迭代的时候,会设置"."为当前正在迭代的元素。

对于第一个表达式,当迭代对象的值为0值时,则range直接跳过,就像if一样。对于第二个表达式,则在迭代到0值时执行else语句。

go
tx := template.Must(template.New("hh").Parse(
+\`{{range $x := . -}}
+{{println $x}}
+{{- end}}
+\`))
+s := []int{11, 22, 33, 44, 55}
+_ = tx.Execute(os.Stdout, s)
tx := template.Must(template.New("hh").Parse(
+\`{{range $x := . -}}
+{{println $x}}
+{{- end}}
+\`))
+s := []int{11, 22, 33, 44, 55}
+_ = tx.Execute(os.Stdout, s)

需注意的是,range的参数部分是pipeline,所以在迭代的过程中是可以进行赋值的。但有两种赋值情况:

go
{{range $value := .}}
+{{range $key,$value := .}}
{{range $value := .}}
+{{range $key,$value := .}}

如果range中只赋值给一个变量,则这个变量是当前正在迭代元素的值。如果赋值给两个变量,则第一个变量是索引值( map/slice是数值,map是key),第二个变量是当前正在迭代元素的值。

with...end

with用来设置"."的值。两种格式:

go
{{with pipeline}} T1 {{end}}
+{{with pipeline}} T1 {{else}} T0 {{end}}
{{with pipeline}} T1 {{end}}
+{{with pipeline}} T1 {{else}} T0 {{end}}

对于第一种格式,当pipeline不为0值的时候,点"." 设置为pipeline运算的值,否则跳过。对于第二种格式,当pipeline为0值时,执行else语句块,否则"."设置为pipeline运算的值,并执行T1。

go
{{with "xx"}}{{println .}}{{end}}
{{with "xx"}}{{println .}}{{end}}

上面将输出xx,因为"."已经设置为"xx"。

内置函数和自定义函数

template定义了一些内置函数,也支持自定义函数

go
and
+    返回第一个为空的参数或最后一个参数。可以有任意多个参数。
+    and x y等价于if x then y else x
+
+not
+    布尔取反。只能一个参数。
+
+or
+    返回第一个不为空的参数或最后一个参数。可以有任意多个参数。
+    "or x y"等价于"if x then x else y"
+
+print
+printf
+println
+    分别等价于fmt包中的Sprint、Sprintf、Sprintln
+
+len
+    返回参数的length。
+
+index
+    对可索引对象进行索引取值。第一个参数是索引对象,后面的参数是索引位。
+    "index x 1 2 3"代表的是x[1][2][3]。
+    可索引对象包括map、slice、array。
+
+call
+    显式调用函数。第一个参数必须是函数类型,且不是template中的函数,而是外部函数。
+    例如一个struct中的某个字段是func类型的。
+    "call .X.Y 1 2"表示调用dot.X.Y(1, 2),Y必须是func类型,函数参数是1和2。
+    函数必须只能有一个或2个返回值,如果有第二个返回值,则必须为error类型。
and
+    返回第一个为空的参数或最后一个参数。可以有任意多个参数。
+    and x y等价于if x then y else x
+
+not
+    布尔取反。只能一个参数。
+
+or
+    返回第一个不为空的参数或最后一个参数。可以有任意多个参数。
+    "or x y"等价于"if x then x else y"
+
+print
+printf
+println
+    分别等价于fmt包中的Sprint、Sprintf、Sprintln
+
+len
+    返回参数的length。
+
+index
+    对可索引对象进行索引取值。第一个参数是索引对象,后面的参数是索引位。
+    "index x 1 2 3"代表的是x[1][2][3]。
+    可索引对象包括map、slice、array。
+
+call
+    显式调用函数。第一个参数必须是函数类型,且不是template中的函数,而是外部函数。
+    例如一个struct中的某个字段是func类型的。
+    "call .X.Y 1 2"表示调用dot.X.Y(1, 2),Y必须是func类型,函数参数是1和2。
+    函数必须只能有一个或2个返回值,如果有第二个返回值,则必须为error类型。
go
eq arg1 arg2:
+    arg1 == arg2时为true
+ne arg1 arg2:
+    arg1 != arg2时为true
+lt arg1 arg2:
+    arg1 < arg2时为true
+le arg1 arg2:
+    arg1 <= arg2时为true
+gt arg1 arg2:
+    arg1 > arg2时为true
+ge arg1 arg2:
+    arg1 >= arg2时为true
eq arg1 arg2:
+    arg1 == arg2时为true
+ne arg1 arg2:
+    arg1 != arg2时为true
+lt arg1 arg2:
+    arg1 < arg2时为true
+le arg1 arg2:
+    arg1 <= arg2时为true
+gt arg1 arg2:
+    arg1 > arg2时为true
+ge arg1 arg2:
+    arg1 >= arg2时为true

对于eq函数,支持多个参数,它们都和第一个参数arg1进行比较。它等价于:

go
eq arg1 arg2 arg3 arg4...
+arg1==arg2 || arg1==arg3 || arg1==arg4
eq arg1 arg2 arg3 arg4...
+arg1==arg2 || arg1==arg3 || arg1==arg4

嵌套template:define和template

define可以直接在待解析内容中定义一个模板,这个模板会加入到common结构组中,并关联到关联名称上。

定义了模板之后,可以使用template这个action来执行模板。template有两种格式:

go
{{template "name"}}
+{{template "name" pipeline}}
{{template "name"}}
+{{template "name" pipeline}}

第一种是直接执行名为name的template,点设置为nil。第二种是点"."设置为pipeline的值,并执行名为name的template。可以将template看作是函数:

go
template("name)
+template("name",pipeline)
template("name)
+template("name",pipeline)
go
func main() {
+	t1 := template.New("test1")
+	tmpl, _ := t1.Parse(
+\`{{- define "T1"}}ONE {{println .}}{{end}}
+{{- define "T2"}}TWO {{println .}}{{end}}
+{{- define "T3"}}{{template "T1"}}{{template "T2" "haha"}}{{end}}
+{{- template "T3" -}}
+\`)
+	_ = tmpl.Execute(os.Stdout, "hello world")
+}
+//ONE <nil>
+//TWO haha
func main() {
+	t1 := template.New("test1")
+	tmpl, _ := t1.Parse(
+\`{{- define "T1"}}ONE {{println .}}{{end}}
+{{- define "T2"}}TWO {{println .}}{{end}}
+{{- define "T3"}}{{template "T1"}}{{template "T2" "haha"}}{{end}}
+{{- template "T3" -}}
+\`)
+	_ = tmpl.Execute(os.Stdout, "hello world")
+}
+//ONE <nil>
+//TWO haha

block块

go
{{block "name" pipeline}} T1 {{end}}
+	A block is shorthand for defining a template
+		{{define "name"}} T1 {{end}}
+	and then executing it in place
+		{{template "name" pipeline}}
+	The typical use is to define a set of root templates that are
+	then customized by redefining the block templates within.
{{block "name" pipeline}} T1 {{end}}
+	A block is shorthand for defining a template
+		{{define "name"}} T1 {{end}}
+	and then executing it in place
+		{{template "name" pipeline}}
+	The typical use is to define a set of root templates that are
+	then customized by redefining the block templates within.

根据官方文档的解释:block等价于define定义一个名为name的模板,并在"有需要"的地方执行这个模板,执行时将"."设置为pipeline的值。

但应该注意,**block的第一个动作是执行名为name的模板,如果不存在,则在此处自动定义这个模板,并执行这个临时定义的模板。换句话说,block可以认为是设置一个默认模板 **。

例如:

go
{{block "T1" .}} one {{end}}
{{block "T1" .}} one {{end}}

它首先找到T1模板,如果T1存在,则执行找到的T1,如果没找到T1,则临时定义一个,并执行它。

不转义

上下文感知的自动转义能让程序更加安全,比如防止XSS攻击(例如在表单中输入带有<script>...</script> 的内容并提交,会使得用户提交的这部分script被执行)。

如果确实不想转义,可以进行类型转换。

go
type CSS
+type HTML
+type JS
+type URL
type CSS
+type HTML
+type JS
+type URL
go
func process(w http.ResponseWriter, r *http.Request) {
+	t, _ := template.ParseFiles("tmpl.html")
+	t.Execute(w, template.HTML(r.FormValue("comment")))
+}
func process(w http.ResponseWriter, r *http.Request) {
+	t, _ := template.ParseFiles("tmpl.html")
+	t.Execute(w, template.HTML(r.FormValue("comment")))
+}
`,88),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const g=s(l,[["render",t]]);export{d as __pageData,g as default}; diff --git a/assets/golang_base_GoTemplate.md.26e53b7c.lean.js b/assets/golang_base_GoTemplate.md.26e53b7c.lean.js new file mode 100644 index 000000000..4ab46e5b6 --- /dev/null +++ b/assets/golang_base_GoTemplate.md.26e53b7c.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as p}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"Go Template","description":"","frontmatter":{},"headers":[],"relativePath":"golang/base/GoTemplate.md","filePath":"golang/base/GoTemplate.md","lastUpdated":1708510102000}'),l={name:"golang/base/GoTemplate.md"},o=p("",88),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const g=s(l,[["render",t]]);export{d as __pageData,g as default}; diff --git "a/assets/golang_base_golang\345\237\272\347\241\200\350\257\255\346\263\225.md.5d06c4a6.js" "b/assets/golang_base_golang\345\237\272\347\241\200\350\257\255\346\263\225.md.5d06c4a6.js" new file mode 100644 index 000000000..14ead1b33 --- /dev/null +++ "b/assets/golang_base_golang\345\237\272\347\241\200\350\257\255\346\263\225.md.5d06c4a6.js" @@ -0,0 +1,5003 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"Golang基础语法","description":"","frontmatter":{},"headers":[],"relativePath":"golang/base/golang基础语法.md","filePath":"golang/base/golang基础语法.md","lastUpdated":1708510102000}'),p={name:"golang/base/golang基础语法.md"},o=l(`

Golang基础语法

第一个Go程序

go
package main
+
+import "fmt"
+
+func main() {
+	fmt.Println("Hello, World!")
+}
package main
+
+import "fmt"
+
+func main() {
+	fmt.Println("Hello, World!")
+}

关键字

  • package:定义当前源码文件所属的包。
  • import:导入其他包。
  • func:定义函数。
  • var:声明变量。
  • const:声明常量。
  • type:定义类型。
  • struct:定义结构体。
  • interface:定义接口。
  • map:定义映射类型。
  • range:用于循环迭代。
  • select:用于通道操作。
  • defer:延迟执行。
  • go:启动一个新的 goroutine。
  • chan:定义通道类型。
  • default:select 语句中的默认情况。
  • fallthrough:在 switch 语句中贯穿到下一个 case。
  • if:条件语句。
  • else:if 语句中的默认情况。
  • switch:多分支条件语句。
  • case:switch 语句中的分支情况。
  • for:循环语句。
  • break:跳出循环或 switch 语句。
  • continue:结束当前循环,开始下一次循环。
  • return:返回函数结果。
  • panic:抛出异常。

变量与常量

变量

声明变量不赋值

go
package main
+
+import "fmt"
+
+func main() {
+	var a int
+	fmt.Println("a = ", a)
+	fmt.Printf("a的类型是%T\\n", a)
+}
+// a = 0
+// a的类型是int
package main
+
+import "fmt"
+
+func main() {
+	var a int
+	fmt.Println("a = ", a)
+	fmt.Printf("a的类型是%T\\n", a)
+}
+// a = 0
+// a的类型是int
  • 整型、浮点型变量的默认值为0和0.0
  • 字符串变量的默认值为空字符串
  • 布尔类型变量默认为false
  • 切片、函数、指针变量的默认为nil

声明变量并初始化

go
package main
+
+import "fmt"
+
+func main() {
+	var a int = 10
+	fmt.Println("a =", a)
+	fmt.Printf("a的类型是%T\\n", a)
+
+	var b string = "hello"
+	fmt.Println("b =", b)
+	fmt.Printf("b的类型是%T\\n", b)
+}
+// a = 10
+// a的类型是int
+// b = hello
+// b的类型是string
package main
+
+import "fmt"
+
+func main() {
+	var a int = 10
+	fmt.Println("a =", a)
+	fmt.Printf("a的类型是%T\\n", a)
+
+	var b string = "hello"
+	fmt.Println("b =", b)
+	fmt.Printf("b的类型是%T\\n", b)
+}
+// a = 10
+// a的类型是int
+// b = hello
+// b的类型是string

声明变量省略类型

go
package main
+
+import "fmt"
+
+func main() {
+	var a = 10
+	fmt.Println("a =", a)
+	fmt.Printf("a的类型是%T\\n", a)
+
+	var b = "hello"
+	fmt.Println("b =", b)
+	fmt.Printf("b的类型是%T\\n", b)
+}
+// a = 10
+// a的类型是int
+// b = hello
+// b的类型是string
package main
+
+import "fmt"
+
+func main() {
+	var a = 10
+	fmt.Println("a =", a)
+	fmt.Printf("a的类型是%T\\n", a)
+
+	var b = "hello"
+	fmt.Println("b =", b)
+	fmt.Printf("b的类型是%T\\n", b)
+}
+// a = 10
+// a的类型是int
+// b = hello
+// b的类型是string

短声明(只能在函数内)

go
package main
+
+import "fmt"
+
+func main() {
+	c := "1"
+	fmt.Printf("c = %s, c的类型是%T\\n", c, c)
+}
+// c = 1, c的类型是string
package main
+
+import "fmt"
+
+func main() {
+	c := "1"
+	fmt.Printf("c = %s, c的类型是%T\\n", c, c)
+}
+// c = 1, c的类型是string

多变量声明

go
package main
+
+func main(){
+    var xx, yy int = 100, 200
+    var kk, wx = 300, "666
+    var (
+        nn int = 100
+        mm bool = true
+    )
+}
package main
+
+func main(){
+    var xx, yy int = 100, 200
+    var kk, wx = 300, "666
+    var (
+        nn int = 100
+        mm bool = true
+    )
+}

匿名变量

匿名变量:下划线_,本身就是一个特殊的标识符。可以像其他标识符那样用于变量的声明,任何类型都可以赋值给它,但任何赋值给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用。

变量交换

和其他静态类型语言不同,可以直接交换变量(python也有这个语法)

go
package main
+
+import "fmt"
+
+func main() {
+	var (
+		a = 100
+		b = 200
+	)
+
+	a, b = b, a
+
+	fmt.Println(a, b)
+}
+// 200 100
package main
+
+import "fmt"
+
+func main() {
+	var (
+		a = 100
+		b = 200
+	)
+
+	a, b = b, a
+
+	fmt.Println(a, b)
+}
+// 200 100

常量

go
package main
+
+import "fmt"
+
+func main(){
+    // 常量(只读属性)
+    const length int = 10
+    // length = 100  // 常量是不允许被修改的
+    fmt.Println("length = ", length)
+}
package main
+
+import "fmt"
+
+func main(){
+    // 常量(只读属性)
+    const length int = 10
+    // length = 100  // 常量是不允许被修改的
+    fmt.Println("length = ", length)
+}

使用常量定义枚举类型

go
package main
+
+import "fmt"
+
+// const来定义枚举类型
+const (
+    BEIJING = 0
+    SHANGHAI = 1
+    SHENZHEN = 2
+)
+
+func main() {
+    fmt.Println("BEIJING = ", BEIJING)      // 0
+    fmt.Println("SHANGHAI = ", SHANGHAI)    // 1
+    fmt.Println("SHENZHEN = ", SHENZHEN)    // 2
+}
package main
+
+import "fmt"
+
+// const来定义枚举类型
+const (
+    BEIJING = 0
+    SHANGHAI = 1
+    SHENZHEN = 2
+)
+
+func main() {
+    fmt.Println("BEIJING = ", BEIJING)      // 0
+    fmt.Println("SHANGHAI = ", SHANGHAI)    // 1
+    fmt.Println("SHENZHEN = ", SHENZHEN)    // 2
+}

iota常量计数器

iota 是一个常量生成器,用于生成一组相关的枚举值。iota 可以与 const 关键字一起使用,在定义一组枚举时,用来生成连续的值。const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)

go
// iota 初始值为 0,每当出现一个新的常量声明时,它的值就会自动加 1,因此 Monday 的值为 1,Tuesday 的值为 2,以此类推。
+const (
+    Sunday = iota // 0
+    Monday        // 1
+    Tuesday       // 2
+    Wednesday     // 3
+    Thursday      // 4
+    Friday        // 5
+    Saturday      // 6
+)
+
+// 在下面的例子中,B 被显式赋值为 3.14,因此接下来的 C 的值为 iota + 1,即 2,而 D 的值也是 iota + 1,所以它的值为 3。
+const (
+    A = iota // 0
+    B = 3.14 // 3.14
+    C = iota // 2
+    D        // 3
+)
// iota 初始值为 0,每当出现一个新的常量声明时,它的值就会自动加 1,因此 Monday 的值为 1,Tuesday 的值为 2,以此类推。
+const (
+    Sunday = iota // 0
+    Monday        // 1
+    Tuesday       // 2
+    Wednesday     // 3
+    Thursday      // 4
+    Friday        // 5
+    Saturday      // 6
+)
+
+// 在下面的例子中,B 被显式赋值为 3.14,因此接下来的 C 的值为 iota + 1,即 2,而 D 的值也是 iota + 1,所以它的值为 3。
+const (
+    A = iota // 0
+    B = 3.14 // 3.14
+    C = iota // 2
+    D        // 3
+)
go
package main
+
+import "fmt"
+
+// 定义递增的步长
+const (
+    BEIJING = iota * 10
+    SHANGHAI
+    SHENZHEN
+)
+
+func main() {
+    fmt.Println("BEIJING = ", BEIJING)      // 0
+    fmt.Println("SHANGHAI = ", SHANGHAI)    // 10
+    fmt.Println("SHENZHEN = ", SHENZHEN)    // 20
+}
package main
+
+import "fmt"
+
+// 定义递增的步长
+const (
+    BEIJING = iota * 10
+    SHANGHAI
+    SHENZHEN
+)
+
+func main() {
+    fmt.Println("BEIJING = ", BEIJING)      // 0
+    fmt.Println("SHANGHAI = ", SHANGHAI)    // 10
+    fmt.Println("SHENZHEN = ", SHENZHEN)    // 20
+}

基本数据类型

整型

  • int8:有符号 8 位整数类型,取值范围为 -128 到 127。
  • uint8(或 byte):无符号 8 位整数类型,取值范围为 0 到 255。
  • int16:有符号 16 位整数类型,取值范围为 -32768 到 32767。
  • uint16:无符号 16 位整数类型,取值范围为 0 到 65535。
  • int32(或 rune):有符号 32 位整数类型,取值范围为 -2147483648 到 2147483647。
  • uint32:无符号 32 位整数类型,取值范围为 0 到 4294967295。
  • int64:有符号 64 位整数类型,取值范围为 -9223372036854775808 到 9223372036854775807。
  • uint64:无符号 64 位整数类型,取值范围为 0 到 18446744073709551615。
go
package main
+
+import (
+	"fmt"
+	"math"
+	"unsafe"
+)
+
+// 有符号整型
+func Integer() {
+	var num8 int8 = 127
+	var num16 int16 = 32767
+	var num32 int32 = math.MaxInt32
+	var num64 int64 = math.MaxInt64
+	var num int = math.MaxInt
+	fmt.Printf("num8的类型是 %T, num8的大小 %d, num8是 %d\\n",
+		num8, unsafe.Sizeof(num8), num8)
+	fmt.Printf("num16的类型是 %T, num16的大小 %d, num16是 %d\\n",
+		num16, unsafe.Sizeof(num16), num16)
+	fmt.Printf("num32的类型是 %T, num32的大小 %d, num32是 %d\\n",
+		num32, unsafe.Sizeof(num32), num32)
+	fmt.Printf("num64的类型是 %T, num64的大小 %d, num64是 %d\\n",
+		num64, unsafe.Sizeof(num64), num64)
+	fmt.Printf("num的类型是 %T, num的大小 %d, num是 %d\\n",
+		num, unsafe.Sizeof(num), num)
+}
+
+// 无符号整型
+func unsignedInteger() {
+	var num8 uint8 = 128
+	var num16 uint16 = 32768
+	var num32 uint32 = math.MaxUint32
+	var num64 uint64 = math.MaxUint64
+	var num uint = math.MaxUint
+	fmt.Printf("num8的类型是 %T, num8的大小 %d, num8是 %d\\n",
+		num8, unsafe.Sizeof(num8), num8)
+	fmt.Printf("num16的类型是 %T, num16的大小 %d, num16是 %d\\n",
+		num16, unsafe.Sizeof(num16), num16)
+	fmt.Printf("num32的类型是 %T, num32的大小 %d, num32是 %d\\n",
+		num32, unsafe.Sizeof(num32), num32)
+	fmt.Printf("num64的类型是 %T, num64的大小 %d, num64是 %d\\n",
+		num64, unsafe.Sizeof(num64), num64)
+	fmt.Printf("num的类型是 %T, num的大小 %d, num是 %d\\n",
+		num, unsafe.Sizeof(num), num)
+}
+
+func main() {
+	Integer()
+	println("---------------------------------------")
+	unsignedInteger()
+}
package main
+
+import (
+	"fmt"
+	"math"
+	"unsafe"
+)
+
+// 有符号整型
+func Integer() {
+	var num8 int8 = 127
+	var num16 int16 = 32767
+	var num32 int32 = math.MaxInt32
+	var num64 int64 = math.MaxInt64
+	var num int = math.MaxInt
+	fmt.Printf("num8的类型是 %T, num8的大小 %d, num8是 %d\\n",
+		num8, unsafe.Sizeof(num8), num8)
+	fmt.Printf("num16的类型是 %T, num16的大小 %d, num16是 %d\\n",
+		num16, unsafe.Sizeof(num16), num16)
+	fmt.Printf("num32的类型是 %T, num32的大小 %d, num32是 %d\\n",
+		num32, unsafe.Sizeof(num32), num32)
+	fmt.Printf("num64的类型是 %T, num64的大小 %d, num64是 %d\\n",
+		num64, unsafe.Sizeof(num64), num64)
+	fmt.Printf("num的类型是 %T, num的大小 %d, num是 %d\\n",
+		num, unsafe.Sizeof(num), num)
+}
+
+// 无符号整型
+func unsignedInteger() {
+	var num8 uint8 = 128
+	var num16 uint16 = 32768
+	var num32 uint32 = math.MaxUint32
+	var num64 uint64 = math.MaxUint64
+	var num uint = math.MaxUint
+	fmt.Printf("num8的类型是 %T, num8的大小 %d, num8是 %d\\n",
+		num8, unsafe.Sizeof(num8), num8)
+	fmt.Printf("num16的类型是 %T, num16的大小 %d, num16是 %d\\n",
+		num16, unsafe.Sizeof(num16), num16)
+	fmt.Printf("num32的类型是 %T, num32的大小 %d, num32是 %d\\n",
+		num32, unsafe.Sizeof(num32), num32)
+	fmt.Printf("num64的类型是 %T, num64的大小 %d, num64是 %d\\n",
+		num64, unsafe.Sizeof(num64), num64)
+	fmt.Printf("num的类型是 %T, num的大小 %d, num是 %d\\n",
+		num, unsafe.Sizeof(num), num)
+}
+
+func main() {
+	Integer()
+	println("---------------------------------------")
+	unsignedInteger()
+}

TIP

  • 除非对整型的大小有特定的需求,否则你通常应该使用 int 表示整型宽度,在 32 位系统下是 32 位,而在 64 位系统下是 64 位。表示范围:在 32 位系统下是 -2147483648 ~ 2147483647 ,而在 64 位系统是 -9223372036854775808 ~ 9223372036854775807
  • 对于 int8int16 等这些类型后面有跟一个数值的类型来说,它们能表示的数值个数是固定的。所以,在有的时候:例如在二进制传输、读写文件的结构描述(为了保持文件的结构不会受到不同编译目标平台字节长度的影响)等情况下,使用更加精确的 int32int64 是更好的。

浮点型

  • float32 类型的变量占用 4 个字节的内存,可以表示的数值范围为±1.401298464324817e-45 到±3.4028234663852886e+38,精度约为 7 个十进制位。

  • float64 类型的变量占用 8 个字节的内存, 可以表示的数值范围为±4.9406564584124654e-324 到±1.7976931348623157e+308,精度约为 15 个十进制位。

Go 语言中的浮点数默认为 float64 类型,如果需要使用 float32 类型,需要显式声明。

go
package main
+
+import (
+	"fmt"
+	"math"
+)
+
+func showFloat() {
+	var num1 float32 = math.MaxFloat32
+	var num2 float64 = math.MaxFloat64
+	fmt.Printf("num1的类型是%T,num1是%g\\n", num1, num1)
+	fmt.Printf("num2的类型是%T,num1是%g\\n", num2, num2)
+}
+
+func main() {
+	showFloat()
+}
+//num1的类型是float32,num1是3.4028235e+38
+//num2的类型是float64,num1是1.7976931348623157e+308
package main
+
+import (
+	"fmt"
+	"math"
+)
+
+func showFloat() {
+	var num1 float32 = math.MaxFloat32
+	var num2 float64 = math.MaxFloat64
+	fmt.Printf("num1的类型是%T,num1是%g\\n", num1, num1)
+	fmt.Printf("num2的类型是%T,num1是%g\\n", num2, num2)
+}
+
+func main() {
+	showFloat()
+}
+//num1的类型是float32,num1是3.4028235e+38
+//num2的类型是float64,num1是1.7976931348623157e+308

字符

字符串中的每一个元素叫作“字符”,定义字符时使用单引号。Go 语言的字符有两种。

  • byte类型,占用1个字节,表示 UTF-8 字符串的单个字节的值,表示的是 ASCII 码表中的一个字符,uint8 的别名类型
  • rune类型,占用4个字节,表示单个 unicode 字符,int32 的别名类型
go
package main
+
+import (
+	"fmt"
+	"unsafe"
+)
+
+func showChar() {
+	var x byte = 65
+	var y uint8 = 65
+	z := 'A'
+	fmt.Printf("x = %c\\n", x) // x = A
+	fmt.Printf("y = %c\\n", y) // y = A
+	fmt.Printf("z = %c\\n", z) // z = A
+
+}
+
+func sizeOfChar() {
+	var x byte = 65
+	fmt.Printf("x = %c\\n", x)
+	fmt.Printf("x 占用 %d 个字节\\n", unsafe.Sizeof(x))
+
+	var y rune = 'A'
+	fmt.Printf("y = %c\\n", y)
+	fmt.Printf("y 占用 %d 个字节\\n", unsafe.Sizeof(y))
+}
+
+func main() {
+	showChar()
+	sizeOfChar()
+}
package main
+
+import (
+	"fmt"
+	"unsafe"
+)
+
+func showChar() {
+	var x byte = 65
+	var y uint8 = 65
+	z := 'A'
+	fmt.Printf("x = %c\\n", x) // x = A
+	fmt.Printf("y = %c\\n", y) // y = A
+	fmt.Printf("z = %c\\n", z) // z = A
+
+}
+
+func sizeOfChar() {
+	var x byte = 65
+	fmt.Printf("x = %c\\n", x)
+	fmt.Printf("x 占用 %d 个字节\\n", unsafe.Sizeof(x))
+
+	var y rune = 'A'
+	fmt.Printf("y = %c\\n", y)
+	fmt.Printf("y 占用 %d 个字节\\n", unsafe.Sizeof(y))
+}
+
+func main() {
+	showChar()
+	sizeOfChar()
+}

字符串

字符串在Go语言中是基本数据类型。

go
var study string  	 		// 定义名为str的字符串类型变量
+study = "《123》"		// 将变量赋值
+study2 := "《789》"		// 以自动推断方式初始化
var study string  	 		// 定义名为str的字符串类型变量
+study = "《123》"		// 将变量赋值
+study2 := "《789》"		// 以自动推断方式初始化

定义多行字符串的方法如下。

  • 双引号书写字符串被称为字符串字面量(string literal),这种字面量不能跨行。
  • 多行字符串需要使用反引号“\`”,多用于内嵌源码和内嵌数据。
  • 在反引号中的所有代码不会被编译器识别,而只是作为字符串的一部分。
go
package main
+import "fmt"
+
+func main() {
+  var s1 string
+	s1 = \`
+    		study := 'Go语言'
+    		fmt.Println(study)
+			\`
+	fmt.Println(s1)
+}
package main
+import "fmt"
+
+func main() {
+  var s1 string
+	s1 = \`
+    		study := 'Go语言'
+    		fmt.Println(study)
+			\`
+	fmt.Println(s1)
+}

布尔

go
func showBool(){
+	a := true
+	b := false
+	fmt.Println("a=", a)
+	fmt.Println("b=", b)
+	fmt.Println("true && false = ", a && b)
+	fmt.Println("true || false = ", a || b)
+}
+
+func main() {
+    showBool()
+}
func showBool(){
+	a := true
+	b := false
+	fmt.Println("a=", a)
+	fmt.Println("b=", b)
+	fmt.Println("true && false = ", a && b)
+	fmt.Println("true || false = ", a || b)
+}
+
+func main() {
+    showBool()
+}

复数

类 型字 节 数说 明
complex64864 位的复数型,由 float32 类型的实部和虚部联合表示
complex12816128 位的复数型,由 float64 类型的实部和虚部联合表示
go
func showComplex() {
+	// 内置的 complex 函数用于构建复数
+	var x complex64 = complex(1, 2)
+	var y complex128 = complex(3, 4)
+	var z complex128 = complex(5, 6)
+	fmt.Println("x = ", x)
+	fmt.Println("y = ", y)
+	fmt.Println("z = ", z)
+
+	// 内建的 real 和 imag 函数分别返回复数的实部和虚部
+	fmt.Println("real(x) = ", real(x))
+	fmt.Println("imag(x) = ", imag(x))
+	fmt.Println("y * z = ", y*z)
+}
+
+func main() {
+   showComplex()
+}
func showComplex() {
+	// 内置的 complex 函数用于构建复数
+	var x complex64 = complex(1, 2)
+	var y complex128 = complex(3, 4)
+	var z complex128 = complex(5, 6)
+	fmt.Println("x = ", x)
+	fmt.Println("y = ", y)
+	fmt.Println("z = ", z)
+
+	// 内建的 real 和 imag 函数分别返回复数的实部和虚部
+	fmt.Println("real(x) = ", real(x))
+	fmt.Println("imag(x) = ", imag(x))
+	fmt.Println("y * z = ", y*z)
+}
+
+func main() {
+   showComplex()
+}

TIP

同样可以用自然方式表示复数

go
x := 1 + 2i
+y := 3 + 4i
+z := 5 + 6i
x := 1 + 2i
+y := 3 + 4i
+z := 5 + 6i

fmt格式化输出

格式含义
%%一个%字面量
%b一个二进制整数值(基数为 2),或者是一个(高级的)用科学计数法表示的指数为 2 的浮点数
%c字符型。可以把输入的数字按照 ASCII 码相应转换为对应的字符
%d一个十进制数值(基数为 10)
%f以标准记数法表示的浮点数或者复数值
%o一个以八进制表示的数字(基数为 8)
%p以十六进制(基数为 16)表示的一个值的地址,前缀为 0x,字母使用小写的 a-f 表示
%q使用 Go 语法以及必须时使用转义,以双引号括起来的字符串或者字节切片[]byte,或者是以单引号括起来的数字
%s字符串。输出字符串中的字符直至字符串中的空字符(字符串以’\\0‘结尾,这个’\\0’即空字符)
%t以 true 或者 false 输出的布尔值
%T使用 Go 语法输出的值的类型
%x以十六进制表示的整型值(基数为十六),数字 a-f 使用小写表示
%X以十六进制表示的整型值(基数为十六),数字 A-F 使用小写表示
  • fmt.Print: 将指定的内容打印到标准输出,不换行。

  • fmt.Println: 将指定的内容打印到标准输出,并在末尾添加换行符。

  • fmt.Printf: 根据格式字符串将指定的内容格式化后打印到标准输出。

  • fmt.Sprintf: 根据格式字符串将指定的内容格式化后返回一个格式化的字符串。

  • fmt.Scan: 从标准输入读取内容,并将其存储到指定的变量中。

  • fmt.Scanln: 从标准输入按空格分隔读取内容,并将其存储到指定的变量中,遇到换行符停止。

  • fmt.Scanf: 根据格式字符串从标准输入读取内容,并将其按指定的格式存储到指定的变量中。

  • fmt.Errorf: 根据格式字符串创建一个新的错误。

运算符

算数运算符

+-*/%++--

关系运算符

==!=><>=<=

逻辑运算符

&&||!

位运算符

&|^<<>>

容器类型

数组

Go 中的数组是值类型而不是引用类型。当数组赋值给一个新的变量时,该变量会得到一个原始数组的一个副本。如果对新变量进行更改,不会影响原始数组。

go
func arrByValue() {
+	arr := [...]string{"123", "456", "789"}
+	copy := arr
+	copy[0] = "Golang"
+	fmt.Println(arr)
+	fmt.Println(copy)
+}
func arrByValue() {
+	arr := [...]string{"123", "456", "789"}
+	copy := arr
+	copy[0] = "Golang"
+	fmt.Println(arr)
+	fmt.Println(copy)
+}

声明

var variable_name [SIZE]variable_type

初始化

  • var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
  • balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

如果数组长度不确定,可以使用 ... 代替数组的长度,编译器会根据元素个数自行推断数组的长度:

go
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
+
+balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
+
+balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

如果设置了数组的长度,我们还可以通过指定下标来初始化元素:

go
//  将索引为 1 和 3 的元素初始化
+balance := [5]float32{1:2.0,3:7.0}
//  将索引为 1 和 3 的元素初始化
+balance := [5]float32{1:2.0,3:7.0}

初始化数组中 {} 中的元素个数不能大于 [] 中的数字。

如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小:

go
balance[4] = 50.0
balance[4] = 50.0

访问

数组元素可以通过索引(位置)来读取。格式为数组名后加中括号,中括号中为索引的值。例如:

go
var salary float32 = balance[9]
var salary float32 = balance[9]

数组长度

len(arr)

数组遍历

使用for range循环

go
func showArr() {
+	arr := [...]string{"123", "456", "789"}
+	for index, value := range arr {
+		fmt.Printf("arr[%d]=%s\\n", index, value)
+	}
+
+	for _, value := range arr {
+		fmt.Printf("value=%s\\n", value)
+	}
+}
func showArr() {
+	arr := [...]string{"123", "456", "789"}
+	for index, value := range arr {
+		fmt.Printf("arr[%d]=%s\\n", index, value)
+	}
+
+	for _, value := range arr {
+		fmt.Printf("value=%s\\n", value)
+	}
+}

切片Slice

Go 语言切片是对数组的抽象。

Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

定义切片

go
var identifier []type
var identifier []type

切片不需要说明长度。

或使用 make() 函数来创建切片:

go
var slice1 []type = make([]type, len)
+
+也可以简写为
+
+slice1 := make([]type, len)
var slice1 []type = make([]type, len)
+
+也可以简写为
+
+slice1 := make([]type, len)

也可以指定容量,其中 capacity 为可选参数。

go
make([]T, length, capacity)
make([]T, length, capacity)

这里 len 是数组的长度并且也是切片的初始长度。

切片初始化

直接初始化切片,[] 表示是切片类型,{1,2,3} 初始化值依次是 1,2,3,其 cap=len=3

go
s :=[] int {1,2,3 }
s :=[] int {1,2,3 }

初始化切片 s,是数组 arr 的引用。

go
s := arr[:]
s := arr[:]

将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片。

go
s := arr[startIndex:endIndex]
s := arr[startIndex:endIndex]

默认 endIndex 时将表示一直到arr的最后一个元素。

go
s := arr[startIndex:]
s := arr[startIndex:]

默认 startIndex 时将表示从 arr 的第一个元素开始。

go
s := arr[:endIndex]
s := arr[:endIndex]

通过切片 s 初始化切片 s1。

go
s1 := s[startIndex:endIndex]
s1 := s[startIndex:endIndex]

通过内置函数 make() 初始化切片s[]int 标识为其元素类型为 int 的切片。

go
s :=make([]int,len,cap)
s :=make([]int,len,cap)

make([]T, length, capacity) 用于创建一个指定类型 T、长度为 length、容量为 capacity 的切片。其中,length 表示切片的实际长度,而 capacity 则表示切片底层数组的容量。

切片的容量可以理解为底层数组能够容纳的元素数量。当切片的容量不足以容纳新添加的元素时,Go 会自动将底层数组扩展一倍,并将原有的元素复制到新的数组中。因此,在预先分配足够容量的情况下,可以避免频繁的内存分配和数据复制操作,提高代码的性能。

需要注意的是,capacity 参数不能小于 length 参数。如果 capacity 小于 length,则会抛出一个运行时异常。

  • 由于 slice 是引用类型,所以你不对它进行赋值的话,它的默认值是 nil
go
var numList []int
+fmt.Println(numList == nil) // true
var numList []int
+fmt.Println(numList == nil) // true
  • 切片之间不能比较,因此我们不能使用 == 操作符来判断两个 slice 是否含有全部相等元素。特别注意,如果你需要测试一个 slice 是否是空的,使用 len(s) == 0 来判断,而不应该用 s == nil 来判断。

切片的长度和容量

一个 slice 由三个部分构成:指针长度容量 。指针指向第一个 slice 元素对应的底层数组元素的地址,要注意的是 slice 的第一个元素并不一定就是数组的第一个元素。长度对应 slice 中元素的数目;长度不能超过容量,容量一般是从 slice 的开始位置到底层数据的结尾位置。简单的讲,容量就是从创建切片索引开始的底层数组中的元素个数,而长度是切片中的元素个数。

内置的 lencap 函数分别返回 slice 的长度和容量。

go
s := make([]string, 3, 5)
+fmt.Println(len(s)) // 3
+fmt.Println(cap(s)) // 5
s := make([]string, 3, 5)
+fmt.Println(len(s)) // 3
+fmt.Println(cap(s)) // 5

切片元素修改

切片自己不拥有任何数据。它只是底层数组的一种表示。对切片所做的任何修改都会反映在底层数组中。

go
func modifySlice() {
+	var arr = [...]string{"123", "456", "789"}
+	s := arr[:] //[0:len(arr)]
+	fmt.Println(arr) 
+	fmt.Println(s)
+
+	s[0] = "Go语言"
+	fmt.Println(arr) 
+	fmt.Println(s) 
+}
func modifySlice() {
+	var arr = [...]string{"123", "456", "789"}
+	s := arr[:] //[0:len(arr)]
+	fmt.Println(arr) 
+	fmt.Println(s)
+
+	s[0] = "Go语言"
+	fmt.Println(arr) 
+	fmt.Println(s) 
+}

这里的 arr[:] 没有填入起始值和结束值,默认就是 0len(arr)

追加切片元素

使用 append 可以将新元素追加到切片上。append 函数的定义是 func append(slice []Type, elems ...Type) []Type 。其中 elems ...Type 在函数定义中表示该函数接受参数 elems 的个数是可变的。这些类型的函数被称为可变参数。

go
func appendSliceData() {
+	s := []string{"123"}
+	fmt.Println(s)
+	fmt.Println(cap(s))
+
+	s = append(s, "567")
+	fmt.Println(s)
+	fmt.Println(cap(s))
+
+	s = append(s, "789", "0")
+	fmt.Println(s)
+	fmt.Println(cap(s))
+
+	s = append(s, []string{"1", "2"}...)
+	fmt.Println(s)
+	fmt.Println(cap(s))
+}
func appendSliceData() {
+	s := []string{"123"}
+	fmt.Println(s)
+	fmt.Println(cap(s))
+
+	s = append(s, "567")
+	fmt.Println(s)
+	fmt.Println(cap(s))
+
+	s = append(s, "789", "0")
+	fmt.Println(s)
+	fmt.Println(cap(s))
+
+	s = append(s, []string{"1", "2"}...)
+	fmt.Println(s)
+	fmt.Println(cap(s))
+}

当新的元素被添加到切片时,如果容量不足,会创建一个新的数组。现有数组的元素被复制到这个新数组中,并返回新的引用。现在新切片的容量是旧切片的两倍。

多维切片

类似于数组,切片也可以有多个维度。

go
func mSlice() {
+	numList := [][]string{
+		{"1", "123"},
+		{"2", "456"},
+		{"3", "789"},
+	}
+	fmt.Println(numList)
+}
func mSlice() {
+	numList := [][]string{
+		{"1", "123"},
+		{"2", "456"},
+		{"3", "789"},
+	}
+	fmt.Println(numList)
+}

Map

在 Go 语言中,map 是散列表(哈希表)的引用。它是一个拥有键值对元素的无序集合,在这个集合中,键是唯一的,可以通过键来获取、更新或移除操作。无论这个散列表有多大,这些操作基本上是通过常量时间完成的。所有可比较的类型,如 整型字符串 等,都可以作为 key

创建Map

使用 make 函数传入键和值的类型,可以创建 map 。具体语法为 make(map[KeyType]ValueType)

go
// 创建一个键类型为 string 值类型为 int 名为 scores 的 map
+scores := make(map[string]int)
+steps := make(map[string]string)
// 创建一个键类型为 string 值类型为 int 名为 scores 的 map
+scores := make(map[string]int)
+steps := make(map[string]string)

字面量创建:

go
var steps2 map[string]string = map[string]string{
+		"第一步": "123",
+		"第二步": "456",
+		"第三步": "789",
+}
+fmt.Println(steps2)
var steps2 map[string]string = map[string]string{
+		"第一步": "123",
+		"第二步": "456",
+		"第三步": "789",
+}
+fmt.Println(steps2)
go
steps3 := map[string]string{
+		"第一步": "123",
+		"第二步": "456",
+		"第三步": "789",
+}
+fmt.Println(steps3)
steps3 := map[string]string{
+		"第一步": "123",
+		"第二步": "456",
+		"第三步": "789",
+}
+fmt.Println(steps3)

Map操作

  • 添加元素

    GO
    // 可以使用 \`map[key] = value\` 向 map 添加元素。
    +steps3["第四步"] = "总监"
    // 可以使用 \`map[key] = value\` 向 map 添加元素。
    +steps3["第四步"] = "总监"
  • 更新元素

    GO
    // 若 key 已存在,使用 map[key] = value 可以直接更新对应 key 的 value 值。
    +steps3["第四步"] = "CTO"
    // 若 key 已存在,使用 map[key] = value 可以直接更新对应 key 的 value 值。
    +steps3["第四步"] = "CTO"
  • 获取元素

    GO
    // 直接使用 map[key] 即可获取对应 key 的 value 值,如果 key不存在,会返回其 value 类型的零值。
    +fmt.Println(steps3["第四步"] )
    // 直接使用 map[key] 即可获取对应 key 的 value 值,如果 key不存在,会返回其 value 类型的零值。
    +fmt.Println(steps3["第四步"] )
  • 删除元素

    GO
    //使用 delete(map, key)可以删除 map 中的对应 key 键值对,如果 key 不存在,delete 函数会静默处理,不会报错。
    +delete(steps3, "第四步")
    //使用 delete(map, key)可以删除 map 中的对应 key 键值对,如果 key 不存在,delete 函数会静默处理,不会报错。
    +delete(steps3, "第四步")
  • 判断 key 是否存在

    GO
    // 如果我们想知道 map 中的某个 key 是否存在,可以使用下面的语法:value, ok := map[key]
    +v3, ok := steps3["第三步"]
    +fmt.Println(ok)
    +fmt.Println(v3)
    +
    +v4, ok := steps3["第四步"]
    +fmt.Println(ok)
    +fmt.Println(v4)
    // 如果我们想知道 map 中的某个 key 是否存在,可以使用下面的语法:value, ok := map[key]
    +v3, ok := steps3["第三步"]
    +fmt.Println(ok)
    +fmt.Println(v3)
    +
    +v4, ok := steps3["第四步"]
    +fmt.Println(ok)
    +fmt.Println(v4)

    这个语句说明 map 的下标读取可以返回两个值,第一个值为当前 keyvalue 值,第二个值表示对应的 key 是否存在,若存在 oktrue ,若不存在,则 okfalse

  • 遍历 map

    GO
    // 遍历 map 中所有的元素需要用 for range 循环。
    +for key, value := range steps3 {
    +    fmt.Printf("key: %s, value: %s\\n", key, value)
    +}
    // 遍历 map 中所有的元素需要用 for range 循环。
    +for key, value := range steps3 {
    +    fmt.Printf("key: %s, value: %s\\n", key, value)
    +}
  • 获取 map 长度

    GO
    // 使用 len 函数可以获取 map 长度
    +func createMap() {
    +  	//...
    +     fmt.Println(len(steps3))    // 4
    +}
    // 使用 len 函数可以获取 map 长度
    +func createMap() {
    +  	//...
    +     fmt.Println(len(steps3))    // 4
    +}

map是引用类型

map 被赋值为一个新变量的时候,它们指向同一个内部数据结构。因此,改变其中一个变量,就会影响到另一变量。

GO
func mapByReference() {
+		steps4 := map[string]string{
+		"第一步": "123",
+		"第二步": "456",
+		"第三步": "789",
+	}
+	fmt.Println("steps4: ", steps4)
+	// steps4:  map[第一步:123 第三步:789 第二步:456]
+	newSteps4 := steps4
+	newSteps4["第一步"] = "123-222"
+	newSteps4["第二步"] = "456-222"
+	newSteps4["第三步"] = "789-222"
+	fmt.Println("steps4: ", steps4)
+  // steps4:  map[第一步:123-222 第三步:789-222 第二步:456-222]
+	fmt.Println("newSteps4: ", newSteps4)
+  // newSteps4:  map[第一步:123-222 第三步:789-222 第二步:456-222]
+}
func mapByReference() {
+		steps4 := map[string]string{
+		"第一步": "123",
+		"第二步": "456",
+		"第三步": "789",
+	}
+	fmt.Println("steps4: ", steps4)
+	// steps4:  map[第一步:123 第三步:789 第二步:456]
+	newSteps4 := steps4
+	newSteps4["第一步"] = "123-222"
+	newSteps4["第二步"] = "456-222"
+	newSteps4["第三步"] = "789-222"
+	fmt.Println("steps4: ", steps4)
+  // steps4:  map[第一步:123-222 第三步:789-222 第二步:456-222]
+	fmt.Println("newSteps4: ", newSteps4)
+  // newSteps4:  map[第一步:123-222 第三步:789-222 第二步:456-222]
+}

map 作为函数参数传递时也会发生同样的情况。

流程控制语句

条件语句

go
if 条件1 {
+  逻辑代码1
+} else if  条件2 {
+  逻辑代码2
+} else if 条件 ... {
+  逻辑代码 ...
+} else {
+  逻辑代码 else
+}
if 条件1 {
+  逻辑代码1
+} else if  条件2 {
+  逻辑代码2
+} else if 条件 ... {
+  逻辑代码 ...
+} else {
+  逻辑代码 else
+}
go
score := 88
+if score >= 90 {
+    fmt.Println("成绩等级为A")
+} else if score >= 80 {
+    fmt.Println("成绩等级为B")
+} else if score >= 70 {
+    fmt.Println("成绩等级为C")
+} else if score >= 60 {
+    fmt.Println("成绩等级为D")
+} else {
+    fmt.Println("成绩等级为E 成绩不及格")
+}
score := 88
+if score >= 90 {
+    fmt.Println("成绩等级为A")
+} else if score >= 80 {
+    fmt.Println("成绩等级为B")
+} else if score >= 70 {
+    fmt.Println("成绩等级为C")
+} else if score >= 60 {
+    fmt.Println("成绩等级为D")
+} else {
+    fmt.Println("成绩等级为E 成绩不及格")
+}

if 还有另外一种写法,它包含一个 statement 可选语句部分,该可选语句在条件判断之前运行。它的语法是:

go
if statement; condition {
+}
+
+if score := 88; score >= 60 {
+    fmt.Println("成绩及格")
+}
if statement; condition {
+}
+
+if score := 88; score >= 60 {
+    fmt.Println("成绩及格")
+}

switch case

go
switch 表达式 {
+    case 表达式值1:
+        业务逻辑代码1
+    case 表达式值2:
+        业务逻辑代码2
+    case 表达式值3:
+        业务逻辑代码3
+    case 表达式值 ...:
+        业务逻辑代码 ...
+    default:
+        业务逻辑代码
+}
switch 表达式 {
+    case 表达式值1:
+        业务逻辑代码1
+    case 表达式值2:
+        业务逻辑代码2
+    case 表达式值3:
+        业务逻辑代码3
+    case 表达式值 ...:
+        业务逻辑代码 ...
+    default:
+        业务逻辑代码
+}
go
grade := "B"
+switch grade {
+case "A":
+    fmt.Println("Your score is between 90 and 100.")
+case "B":
+    fmt.Println("Your score is between 80 and 90.")
+case "C":
+    fmt.Println("Your score is between 70 and 80.")
+case "D":
+    fmt.Println("Your score is between 60 and 70.")
+default:
+    fmt.Println("Your score is below 60.")
+}
grade := "B"
+switch grade {
+case "A":
+    fmt.Println("Your score is between 90 and 100.")
+case "B":
+    fmt.Println("Your score is between 80 and 90.")
+case "C":
+    fmt.Println("Your score is between 70 and 80.")
+case "D":
+    fmt.Println("Your score is between 60 and 70.")
+default:
+    fmt.Println("Your score is below 60.")
+}

一个 case 多个条件

go
month := 5
+switch month {
+case 1, 3, 5, 7, 8, 10, 12:
+    fmt.Println("该月份有 31 天")
+case 4, 6, 9, 11:
+    fmt.Println("该月份有 30 天")
+case 2:
+    fmt.Println("该月份闰年为 29 天,非闰年为 28 天")
+default:
+    fmt.Println("输入有误!")
+}
month := 5
+switch month {
+case 1, 3, 5, 7, 8, 10, 12:
+    fmt.Println("该月份有 31 天")
+case 4, 6, 9, 11:
+    fmt.Println("该月份有 30 天")
+case 2:
+    fmt.Println("该月份闰年为 29 天,非闰年为 28 天")
+default:
+    fmt.Println("输入有误!")
+}

switch 还有另外一种写法,它包含一个 statement 可选语句部分,该可选语句在表达式之前运行。它的语法是:

go
switch statement; expression {
+}
+
+
+switch month := 5; month {
+case 1, 3, 5, 7, 8, 10, 12:
+    fmt.Println("该月份有 31 天")
+case 4, 6, 9, 11:
+    fmt.Println("该月份有 30 天")
+case 2:
+    fmt.Println("该月份闰年为 29 天,非闰年为 28 天")
+default:
+    fmt.Println("输入有误!")
+}
switch statement; expression {
+}
+
+
+switch month := 5; month {
+case 1, 3, 5, 7, 8, 10, 12:
+    fmt.Println("该月份有 31 天")
+case 4, 6, 9, 11:
+    fmt.Println("该月份有 30 天")
+case 2:
+    fmt.Println("该月份闰年为 29 天,非闰年为 28 天")
+default:
+    fmt.Println("输入有误!")
+}

这里 month 变量的作用域就仅限于这个 switch 内。

switch 后可接函数

switch 后面可以接一个函数,只要保证 case 后的值类型与函数的返回值一致即可。

go
package main
+
+import "fmt"
+
+func getResult(args ...int) bool {
+ for _, v := range args {
+  if v < 60 {
+   return false
+  }
+ }
+ return true
+}
+
+func main() {
+ chinese := 88
+ math := 90
+ english := 95
+
+ switch getResult(chinese, math, english) {
+ case true:
+  fmt.Println("考试通过")
+ case false:
+  fmt.Println("考试未通过")
+ }
+}
package main
+
+import "fmt"
+
+func getResult(args ...int) bool {
+ for _, v := range args {
+  if v < 60 {
+   return false
+  }
+ }
+ return true
+}
+
+func main() {
+ chinese := 88
+ math := 90
+ english := 95
+
+ switch getResult(chinese, math, english) {
+ case true:
+  fmt.Println("考试通过")
+ case false:
+  fmt.Println("考试未通过")
+ }
+}

无表达式的 switch

switch 后面的表达式是可选的。如果省略该表达式,则表示这个 switch 语句等同于 switch true ,并且每个 case 表达式都被认定为有效,相应的代码块也会被执行。

go
score := 88
+switch {
+case score >= 90 && score <= 100:
+    fmt.Println("grade A")
+case score >= 80 && score < 90:
+    fmt.Println("grade B")
+case score >= 70 && score < 80:
+    fmt.Println("grade C")
+case score >= 60 && score < 70:
+    fmt.Println("grade D")
+case score < 60:
+    fmt.Println("grade E")
+}
score := 88
+switch {
+case score >= 90 && score <= 100:
+    fmt.Println("grade A")
+case score >= 80 && score < 90:
+    fmt.Println("grade B")
+case score >= 70 && score < 80:
+    fmt.Println("grade C")
+case score >= 60 && score < 70:
+    fmt.Println("grade D")
+case score < 60:
+    fmt.Println("grade E")
+}

switch-case 语句相当于 if-elseif-else 语句。

fallthrough 语句

正常情况下 switch-case 语句在执行时只要有一个 case 满足条件,就会直接退出 switch-case ,如果一个都没有满足,才会执行 default 的代码块。不同于其他语言需要在每个 case 中添加 break 语句才能退出。使用 fallthrough 语句可以在已经执行完成的 case 之后,把控制权转移到下一个 case 的执行代码中。fallthrough 只能穿透一层,不管你有没有匹配上,都要退出了。fallthrough 语句是 case 子句的最后一个语句。如果它出现在了 case 语句的中间,编译会不通过。

go
s := "123"
+switch {
+case s == "123":
+    fmt.Println("123")
+    fallthrough
+case s == "456":
+    fmt.Println("456")
+case s != "789":
+    fmt.Println("789")
+}
s := "123"
+switch {
+case s == "123":
+    fmt.Println("123")
+    fallthrough
+case s == "456":
+    fmt.Println("456")
+case s != "789":
+    fmt.Println("789")
+}

循环语句

循环语句 可以用来重复执行某一段代码。在 C 语言中,循环语句有 forwhiledo while 三种循环。但在 Go 中只有 for 一种循环语句。下面是 for 循环语句的四种基本模型:

go
// for 接三个表达式
+for initialisation; condition; post {
+   code
+}
+
+// for 接一个条件表达式
+for condition {
+   code
+}
+
+// for 接一个 range 表达式
+for range_expression {
+   code
+}
+
+// for 不接表达式
+for {
+   code
+}
// for 接三个表达式
+for initialisation; condition; post {
+   code
+}
+
+// for 接一个条件表达式
+for condition {
+   code
+}
+
+// for 接一个 range 表达式
+for range_expression {
+   code
+}
+
+// for 不接表达式
+for {
+   code
+}
  • 接一个条件表达式

    go
    num := 0
    +for num < 4 {
    +    fmt.Println(num)
    +    num++
    +}
    num := 0
    +for num < 4 {
    +    fmt.Println(num)
    +    num++
    +}
  • 接三个表达式

    for 后面接的这三个表达式,各有各的用途:

    • 第一个表达式(initialisation):初始化控制变量,在整个循环生命周期内,只执行一次;
    • 第二个表达式(condition):设置循环控制条件,该表达式值为 true 时循环,值为 false 时结束循环;
    • 第三个表达式(post):每次循环完都会执行此表达式,可以利用其让控制变量增量或减量。

    这三个表达式,使用 ; 分隔。

    go
    for num := 0; num < 4; num++ {
    +    fmt.Println(num)
    +}
    for num := 0; num < 4; num++ {
    +    fmt.Println(num)
    +}
  • 接一个 range 表达式

    go
    str := "Golang"
    +for index, value := range str{
    +    fmt.Printf("index %d, value %c\\n", index, value)
    +}
    str := "Golang"
    +for index, value := range str{
    +    fmt.Printf("index %d, value %c\\n", index, value)
    +}
  • 不接表达式

    for 后面不接表达式就相当于无限循环,当然,可以使用 break 语句退出循环

    go
    // 第一种写法
    +for {
    +    code
    +}
    +// 第二种写法
    +for ;; {
    +    code
    +}
    // 第一种写法
    +for {
    +    code
    +}
    +// 第二种写法
    +for ;; {
    +    code
    +}
  • break 语句

    break 语句用于终止 for 循环,之后程序将执行在 for 循环后的代码。上面的例子已经演示了 break 语句的使用。

  • continue 语句

    continue 语句用来跳出 for 循环中的当前循环。在 continue 语句后的所有的 for 循环语句都不会在本次循环中执行,执行完 continue 语句后将会继续执行一下次循环。下面的程序会打印出 10 以内的奇数。

defer延迟调用

含有 defer 语句的函数,会在该函数将要返回之前,调用另一个函数。简单点说就是 defer 语句后面跟着的函数会延迟到当前函数执行完后再执行。

go
package main
+
+import "fmt"
+
+func bookPrint() {
+	fmt.Println("123")
+}
+
+func main() {
+	defer bookPrint()
+	fmt.Println("main函数...")
+}
package main
+
+import "fmt"
+
+func bookPrint() {
+	fmt.Println("123")
+}
+
+func main() {
+	defer bookPrint()
+	fmt.Println("main函数...")
+}

首先,执行 main 函数,因为 bookPrint() 函数前有 defer 关键字,所以会在执行完 main 函数后再执行 bookPrint() 函数,所以先打印出 main函数... ,再执行 bookPrint() 函数打印 123

即时求值的变量快照

使用 defer 只是延时调用函数,传递给函数里的变量,不应该受到后续程序的影响。

go
str := "123"
+defer fmt.Println(str)
+str = "456"
+fmt.Println(str)
+// 456
+// 123
str := "123"
+defer fmt.Println(str)
+str = "456"
+fmt.Println(str)
+// 456
+// 123

延迟方法

defer 不仅能够延迟函数的执行,也能延迟方法的执行。

go
package main
+
+import "fmt"
+
+type Book struct {
+	bookName, authorName string
+}
+
+func (b Book) printName() {
+	fmt.Printf("%s %s", b.bookName, b.authorName)
+}
+
+func main() {
+	book := Book{"123", "456"}
+	defer book.printName()
+	fmt.Printf("main... ")
+}
+// main... 123 456
package main
+
+import "fmt"
+
+type Book struct {
+	bookName, authorName string
+}
+
+func (b Book) printName() {
+	fmt.Printf("%s %s", b.bookName, b.authorName)
+}
+
+func main() {
+	book := Book{"123", "456"}
+	defer book.printName()
+	fmt.Printf("main... ")
+}
+// main... 123 456

defer 栈

当一个函数内多次调用 defer 时,Go 会把 defer 调用放入到一个栈中,随后按照 后进先出 的顺序执行。

go
package main
+
+import "fmt"
+
+func main() {
+	defer fmt.Printf("123")
+	defer fmt.Printf("456")
+	defer fmt.Printf("789")
+	fmt.Printf("main...")
+}
+//main...789456123
package main
+
+import "fmt"
+
+func main() {
+	defer fmt.Printf("123")
+	defer fmt.Printf("456")
+	defer fmt.Printf("789")
+	fmt.Printf("main...")
+}
+//main...789456123

defer 在 return 后调用

go
package main
+
+import "fmt"
+
+var s string = "123"
+
+func showLesson() string {
+    defer func() {
+        s = "456"
+    }()
+    fmt.Println("showLesson: s =", s)
+    return s
+}
+
+func main() {
+    lesson := showLesson()
+    fmt.Println("main: s =", s)
+    fmt.Println("main: lesson =", lesson)
+}
+//showLesson: s = 123
+//main: s = 456
+//main: lesson = 123
package main
+
+import "fmt"
+
+var s string = "123"
+
+func showLesson() string {
+    defer func() {
+        s = "456"
+    }()
+    fmt.Println("showLesson: s =", s)
+    return s
+}
+
+func main() {
+    lesson := showLesson()
+    fmt.Println("main: s =", s)
+    fmt.Println("main: lesson =", lesson)
+}
+//showLesson: s = 123
+//main: s = 456
+//main: lesson = 123

Go 中 defer 和 return 执行的先后顺序

  1. 多个defer的执行顺序为“后进先出”;
  2. defer、return、返回值三者的执行逻辑应该是:return最先执行,return负责将结果写入返回值中;接着defer开始执行一些收尾工作;最后函数携带当前返回值退出。

如果函数的返回值是无名的(不带命名返回值),则go语言会在执行return的时候会执行一个类似创建一个临时变量作为保存return值的动作,而有名返回值的函数,由于返回值在函数定义的时候已经将该变量进行定义,在执行return的时候会先执行返回值保存操作,而后续的defer函数会改变这个返回值(虽然defer是在return之后执行的,但是由于使用的函数定义的变量,所以执行defer操作后对该变量的修改会影响到return的值

defer 可以使代码更简洁

如果没有使用 defer ,当在一个操作资源的函数里调用多个 return 时,每次都得释放资源,你可能这样写代码:

go
func f() {
+    r := getResource()  //0,获取资源
+    ......
+    if ... {
+        r.release()  //1,释放资源
+        return
+    }
+    ......
+    if ... {
+        r.release()  //2,释放资源
+        return
+    }
+    ......
+    if ... {
+        r.release()  //3,释放资源
+        return
+    }
+    ......
+    r.release()     //4,释放资源
+    return
+}
func f() {
+    r := getResource()  //0,获取资源
+    ......
+    if ... {
+        r.release()  //1,释放资源
+        return
+    }
+    ......
+    if ... {
+        r.release()  //2,释放资源
+        return
+    }
+    ......
+    if ... {
+        r.release()  //3,释放资源
+        return
+    }
+    ......
+    r.release()     //4,释放资源
+    return
+}

有了 defer 之后,可以简洁地写成下面这样:

go
func f() {
+    r := getResource()  //0,获取资源
+
+    defer r.release()  //1,释放资源
+    ......
+    if ... {
+        ...
+        return
+    }
+    ......
+    if ... {
+        ...
+        return
+    }
+    ......
+    if ... {
+        ...
+        return
+    }
+    ......
+    return
+}
func f() {
+    r := getResource()  //0,获取资源
+
+    defer r.release()  //1,释放资源
+    ......
+    if ... {
+        ...
+        return
+    }
+    ......
+    if ... {
+        ...
+        return
+    }
+    ......
+    if ... {
+        ...
+        return
+    }
+    ......
+    return
+}

goto无条件跳转

在 Go 语言中保留 gotogoto 后面接的是标签,表示下一步要执行哪里的代码。

go
package main
+
+import "fmt"
+
+func main() {
+	fmt.Println("123")
+	goto label
+	fmt.Println("456")
+label:
+    fmt.Println("789")
+}
+//123
+//789
package main
+
+import "fmt"
+
+func main() {
+	fmt.Println("123")
+	goto label
+	fmt.Println("456")
+label:
+    fmt.Println("789")
+}
+//123
+//789

指针

一个指针变量指向了一个值的内存地址。

类似于变量和常量,在使用指针前你需要声明指针。指针声明格式如下:

go
var var_name *var-type
var var_name *var-type

var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针。以下是有效的指针声明:

go
var ip *int        /* 指向整型*/
+var fp *float32    /* 指向浮点型 */
var ip *int        /* 指向整型*/
+var fp *float32    /* 指向浮点型 */

操作符

  • & 操作符可以从一个变量中取到其内存地址。
  • 操作符如果在赋值操作值的左边,指该指针指向的变量;* 操作符如果在赋值操作符的右边,指从一个指针变量中取得变量值,又称指针的解引用。

如何使用指针

指针使用流程:

  • 定义指针变量。
  • 为指针变量赋值。
  • 访问指针变量中指向地址的值。
  • 在指针类型前面加上 * 号(前缀)来获取指针所指向的内容。
go
package main
+
+import "fmt"
+
+func main() {
+   var a int= 20   /* 声明实际变量 */
+   var ip *int        /* 声明指针变量 */
+
+   ip = &a  /* 指针变量的存储地址 */
+
+   fmt.Printf("a 变量的地址是: %x\\n", &a  )
+
+   /* 指针变量的存储地址 */
+   fmt.Printf("ip 变量储存的指针地址: %x\\n", ip )
+
+   /* 使用指针访问值 */
+   fmt.Printf("*ip 变量的值: %d\\n", *ip )
+}
+//a 变量的地址是: 20818a220
+//ip 变量储存的指针地址: 20818a220
+//*ip 变量的值: 20
package main
+
+import "fmt"
+
+func main() {
+   var a int= 20   /* 声明实际变量 */
+   var ip *int        /* 声明指针变量 */
+
+   ip = &a  /* 指针变量的存储地址 */
+
+   fmt.Printf("a 变量的地址是: %x\\n", &a  )
+
+   /* 指针变量的存储地址 */
+   fmt.Printf("ip 变量储存的指针地址: %x\\n", ip )
+
+   /* 使用指针访问值 */
+   fmt.Printf("*ip 变量的值: %d\\n", *ip )
+}
+//a 变量的地址是: 20818a220
+//ip 变量储存的指针地址: 20818a220
+//*ip 变量的值: 20

空指针

当一个指针被定义后没有分配到任何变量时,它的值为 nil。

nil 指针也称为空指针。

nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。

一个指针变量通常缩写为 ptr。

go
package main
+
+import "fmt"
+
+func main() {
+  var ptr *int
+
+  fmt.Printf("ptr 的值为 : %x**\\n**", ptr  )
+}
+//ptr 的值为 : 0
package main
+
+import "fmt"
+
+func main() {
+  var ptr *int
+
+  fmt.Printf("ptr 的值为 : %x**\\n**", ptr  )
+}
+//ptr 的值为 : 0

空指针判断

go
if(ptr != nil)     /* ptr 不是空指针 */
+if(ptr == nil)    /* ptr 是空指针 */
if(ptr != nil)     /* ptr 不是空指针 */
+if(ptr == nil)    /* ptr 是空指针 */

函数传递指针函数

在函数中对指针参数所做的修改,在函数返回后会保存相应的修改。

go
package main
+
+import (
+	"fmt"
+)
+
+func changeByPointer(value *int) {
+	*value = 200
+}
+
+func main() {
+	x3 := 99
+	p3 := &x3
+	fmt.Println("执行changeByPointer函数之前p3是", *p3)
+	changeByPointer(p3)
+	fmt.Println("执行changeByPointer函数之后p3是", *p3)
+}
+//执行changeByPointer函数之前p3是 99
+//执行changeByPointer函数之后p3是 200
package main
+
+import (
+	"fmt"
+)
+
+func changeByPointer(value *int) {
+	*value = 200
+}
+
+func main() {
+	x3 := 99
+	p3 := &x3
+	fmt.Println("执行changeByPointer函数之前p3是", *p3)
+	changeByPointer(p3)
+	fmt.Println("执行changeByPointer函数之后p3是", *p3)
+}
+//执行changeByPointer函数之前p3是 99
+//执行changeByPointer函数之后p3是 200

指针与切片

切片与指针一样是引用类型,如果我们想通过一个函数改变一个数组的值,可以将该数组的切片当作参数传给函数,也可以将这个数组的指针当作参数传给函数。但 Go 中建议使用第一种方法,即将该数组的切片当作参数传给函数,因为这么写更加简洁易读。

go
package main
+
+import "fmt"
+
+// 使用切片
+func changeSlice(value []int) {
+	value[0] = 200
+}
+
+// 使用数组指针
+func changeArray(value *[3]int) {
+	(*value)[0] = 200
+}
+
+func main() {
+	x := [3]int{10, 20, 30}
+	changeSlice(x[:])
+	fmt.Println(x) // [200 20 30]
+
+	y := [3]int{100, 200, 300}
+	changeArray(&y)
+	fmt.Println(y) // [200 200 300]
+}
package main
+
+import "fmt"
+
+// 使用切片
+func changeSlice(value []int) {
+	value[0] = 200
+}
+
+// 使用数组指针
+func changeArray(value *[3]int) {
+	(*value)[0] = 200
+}
+
+func main() {
+	x := [3]int{10, 20, 30}
+	changeSlice(x[:])
+	fmt.Println(x) // [200 20 30]
+
+	y := [3]int{100, 200, 300}
+	changeArray(&y)
+	fmt.Println(y) // [200 200 300]
+}

结构体

Go 语言中数组可以存储同一类型的数据,但在结构体中我们可以为不同项定义不同的数据类型。

结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。Go中没有class的概念,只有struct结构体,所以也没有继承。

声明

go
type struct_name struct {
+    attribute_name1   attribute_type
+    attribute_name2   attribute_type
+    ...
+}
+
+type Lesson struct {
+	name   string //名称
+	target string //学习目标
+	spend  int    //学习花费时间
+}
+//可以把相同类型的属性声明在同一行,这样可以使结构体变得更加紧凑
+type Lesson2 struct {
+    name, target    string
+    spend             int
+}
type struct_name struct {
+    attribute_name1   attribute_type
+    attribute_name2   attribute_type
+    ...
+}
+
+type Lesson struct {
+	name   string //名称
+	target string //学习目标
+	spend  int    //学习花费时间
+}
+//可以把相同类型的属性声明在同一行,这样可以使结构体变得更加紧凑
+type Lesson2 struct {
+    name, target    string
+    spend             int
+}

上面的结构体称为命名结构体Named Structure。声明结构体时也可以不用声明新类型,这种结构体类型称为匿名结构体Anonymous Structure

go
var Lesson3 struct {
+    name, target    string
+    spend             int
+}
var Lesson3 struct {
+    name, target    string
+    spend             int
+}

创建命名结构体

go
package main
+
+import "fmt"
+
+type Lesson struct {
+	name, target    string
+	spend             int
+}
+
+func main() {
+	// 使用字段名创建结构体
+	lesson1 := Lesson{
+		name: "Golang",
+		target: "学习Go语言,并完成一个单体服务",
+		spend:  5,
+	}
+	// 不使用字段名创建结构体,按字段声明顺序初始化
+	lesson2 := Lesson{"Golang", "学习Go语言,并完成一个单体服务", 5}
+
+	fmt.Println("lesson1 ", lesson1)
+	fmt.Println("lesson2 ", lesson2)
+}
package main
+
+import "fmt"
+
+type Lesson struct {
+	name, target    string
+	spend             int
+}
+
+func main() {
+	// 使用字段名创建结构体
+	lesson1 := Lesson{
+		name: "Golang",
+		target: "学习Go语言,并完成一个单体服务",
+		spend:  5,
+	}
+	// 不使用字段名创建结构体,按字段声明顺序初始化
+	lesson2 := Lesson{"Golang", "学习Go语言,并完成一个单体服务", 5}
+
+	fmt.Println("lesson1 ", lesson1)
+	fmt.Println("lesson2 ", lesson2)
+}

结构体标签

Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。

Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:

go
\`key1:"value1" key2:"value2"\`
\`key1:"value1" key2:"value2"\`

结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔。 注意事项: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。

例如我们为Student结构体的每个字段定义json序列化时使用的Tag:

go
//Student 学生
+type Student struct {
+    ID     int    \`json:"id"\` //通过指定tag实现json序列化该字段时的key
+    Gender string //json序列化是默认使用字段名作为key
+    name   string //私有不能被json包访问
+}
+
+func main() {
+    s1 := Student{
+        ID:     1,
+        Gender: "女",
+        name:   "pprof",
+    }
+    data, err := json.Marshal(s1)
+    if err != nil {
+        fmt.Println("json marshal failed!")
+        return
+    }
+    fmt.Printf("json str:%s\\n", data) //json str:{"id":1,"Gender":"女"}
+}
//Student 学生
+type Student struct {
+    ID     int    \`json:"id"\` //通过指定tag实现json序列化该字段时的key
+    Gender string //json序列化是默认使用字段名作为key
+    name   string //私有不能被json包访问
+}
+
+func main() {
+    s1 := Student{
+        ID:     1,
+        Gender: "女",
+        name:   "pprof",
+    }
+    data, err := json.Marshal(s1)
+    if err != nil {
+        fmt.Println("json marshal failed!")
+        return
+    }
+    fmt.Printf("json str:%s\\n", data) //json str:{"id":1,"Gender":"女"}
+}

创建匿名结构体

go
package main
+
+import "fmt"
+
+func main() {
+	// 创建匿名结构体变量
+	lesson3 := struct {
+		name, target string
+		spend          int
+	}{
+		name:   "Go语言",
+		target: "掌握GO语言",
+		spend:   3,
+	}
+
+	fmt.Println("lesson3 ", lesson3)
+}
package main
+
+import "fmt"
+
+func main() {
+	// 创建匿名结构体变量
+	lesson3 := struct {
+		name, target string
+		spend          int
+	}{
+		name:   "Go语言",
+		target: "掌握GO语言",
+		spend:   3,
+	}
+
+	fmt.Println("lesson3 ", lesson3)
+}

结构体的零值(Zero Value)

当定义好的结构体没有被显式初始化时,结构体的字段将会默认赋为相应类型的零值。

go
package main
+
+import "fmt"
+
+type Lesson struct {
+	name, target    string
+	spend             int
+}
+
+func main() {
+	// 不初始化结构体
+	var lesson4 = Lesson{}
+
+	fmt.Println("lesson4 ", lesson4)
+}
+//lesson4  {  0}
package main
+
+import "fmt"
+
+type Lesson struct {
+	name, target    string
+	spend             int
+}
+
+func main() {
+	// 不初始化结构体
+	var lesson4 = Lesson{}
+
+	fmt.Println("lesson4 ", lesson4)
+}
+//lesson4  {  0}

访问结构体字段

使用.点操作符访问:lesson.name

使用.也可用与给结构体字段赋值:lesson.name = "test"

指向结构体的指针

go
package main
+
+import "fmt"
+
+type Lesson struct {
+	name, target    string
+	spend             int
+}
+
+func main() {
+	lesson8 := &Lesson{"Go语言", "Go语言微服务", 50}
+	fmt.Println("lesson8 name: ", (*lesson8).name)
+	fmt.Println("lesson8 name: ", lesson8.name)
+}
package main
+
+import "fmt"
+
+type Lesson struct {
+	name, target    string
+	spend             int
+}
+
+func main() {
+	lesson8 := &Lesson{"Go语言", "Go语言微服务", 50}
+	fmt.Println("lesson8 name: ", (*lesson8).name)
+	fmt.Println("lesson8 name: ", lesson8.name)
+}

lesson8 是一个指向结构体 Lesson 的指针,用 (*lesson8).name 访问 lesson8name 字段, lesson8.name 代替 (*lesson8).name 的解引用访问。

匿名字段

在创建结构体时,字段可以只有类型没有字段名,这种字段称为 匿名字段(Anonymous Field)

go
package main
+
+import "fmt"
+
+type Lesson4 struct {
+	string
+	int
+}
+
+func main() {
+	lesson9 := Lesson4{"Golang", 50}
+	fmt.Println("lesson9 ", lesson9)
+	fmt.Println("lesson9 string: ", lesson9.string)
+	fmt.Println("lesson9 int: ", lesson9.int)
+}
package main
+
+import "fmt"
+
+type Lesson4 struct {
+	string
+	int
+}
+
+func main() {
+	lesson9 := Lesson4{"Golang", 50}
+	fmt.Println("lesson9 ", lesson9)
+	fmt.Println("lesson9 string: ", lesson9.string)
+	fmt.Println("lesson9 int: ", lesson9.int)
+}

上面的程序结构体定义了两个匿名字段,虽然这两个字段没有字段名,但匿名字段的名称默认就是它的类型。所以上面的结构体 Lesoon4 有两个名为 stringint 的字段。

嵌套结构体

结构体的字段也可能是另外一个结构体,这样的结构体称为 嵌套结构体(Nested Structs)

go
package main
+
+import "fmt"
+
+type Author struct {
+	name string
+  	wx string
+}
+
+type Lesson5 struct {
+	name,target string
+	spend int
+	author Author
+}
+
+func main() {
+	lesson10 := Lesson5{
+		name: "Go语言",
+		spend: 50,
+	}
+	lesson10.author = Author{
+		name: "golang",
+		wx: "666",
+	}
+	fmt.Println("lesson10 name:", lesson10.name)
+	fmt.Println("lesson10 spend:", lesson10.spend)
+	fmt.Println("lesson10 author name:", lesson10.author.name)
+	fmt.Println("lesson10 author wx:", lesson10.author.wx)
+}
package main
+
+import "fmt"
+
+type Author struct {
+	name string
+  	wx string
+}
+
+type Lesson5 struct {
+	name,target string
+	spend int
+	author Author
+}
+
+func main() {
+	lesson10 := Lesson5{
+		name: "Go语言",
+		spend: 50,
+	}
+	lesson10.author = Author{
+		name: "golang",
+		wx: "666",
+	}
+	fmt.Println("lesson10 name:", lesson10.name)
+	fmt.Println("lesson10 spend:", lesson10.spend)
+	fmt.Println("lesson10 author name:", lesson10.author.name)
+	fmt.Println("lesson10 author wx:", lesson10.author.wx)
+}

上面的程序 Lesson5 结构体有一个字段 author ,而且它的类型也是一个结构体 Author

提升字段

结构体中如果有匿名的结构体类型字段,则该匿名结构体里的字段就称为 提升字段(Promoted Fields) 。这是因为提升字段就像是属于外部结构体一样,可以用外部结构体直接访问。就像刚刚上面的程序,如果我们把 Lesson 结构体中的字段 author 直接用匿名字段 Author 代替, Author 结构体的字段例如 name 就不用像上面那样使用 lesson10.author.wx 访问,而是使用 lesson10.wx 就能访问 Author 结构体中的 wx 字段。现在结构体 Authornamewx 两个字段,访问字段就像在 Lesson 里直接声明的一样,因此我们称之为提升字段。

go
package main
+
+import "fmt"
+
+type Author struct {
+	name string
+  	wx string
+}
+
+type Lesson6 struct {
+	name,target string
+	spend int
+	Author
+}
+
+func main() {
+	lesson10 := Lesson6{
+		name:   "Go语言",
+		target: "掌握Go语言",
+	}
+	lesson10.Author = Author{
+		name: "golang",
+		wx:   "666",
+	}
+	fmt.Println("lesson10 name:", lesson10.name)
+	fmt.Println("lesson10 target:", lesson10.target)
+	fmt.Println("lesson10 author wx:", lesson10.wx)
+}
+//lesson10 name: Go语言
+//lesson10 target: 掌握Go语言
+//lesson10 author wx: 666
package main
+
+import "fmt"
+
+type Author struct {
+	name string
+  	wx string
+}
+
+type Lesson6 struct {
+	name,target string
+	spend int
+	Author
+}
+
+func main() {
+	lesson10 := Lesson6{
+		name:   "Go语言",
+		target: "掌握Go语言",
+	}
+	lesson10.Author = Author{
+		name: "golang",
+		wx:   "666",
+	}
+	fmt.Println("lesson10 name:", lesson10.name)
+	fmt.Println("lesson10 target:", lesson10.target)
+	fmt.Println("lesson10 author wx:", lesson10.wx)
+}
+//lesson10 name: Go语言
+//lesson10 target: 掌握Go语言
+//lesson10 author wx: 666

结构体比较

如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用 ==!= 运算符进行比较。可以通过==运算符或 DeeplyEqual()函数比较两个结构相同的类型并包含相同的字段值。因此下面两个比较的表达式是等价的:

go
package main
+
+import "fmt"
+
+type  Lesson  struct{
+	name,target string
+	spend int
+}
+
+func main() {
+	lesson11 := Lesson{
+		name:   "Go语言",
+		target: "掌握Go语言",
+	}
+	lesson12 := Lesson{
+		name:   "Go语言",
+		target: "掌握Go语言",
+	}
+	fmt.Println(lesson11.name == lesson12.name && lesson11.target == lesson12.target) // true
+	fmt.Println(lesson11 == lesson12) // true
+}
package main
+
+import "fmt"
+
+type  Lesson  struct{
+	name,target string
+	spend int
+}
+
+func main() {
+	lesson11 := Lesson{
+		name:   "Go语言",
+		target: "掌握Go语言",
+	}
+	lesson12 := Lesson{
+		name:   "Go语言",
+		target: "掌握Go语言",
+	}
+	fmt.Println(lesson11.name == lesson12.name && lesson11.target == lesson12.target) // true
+	fmt.Println(lesson11 == lesson12) // true
+}

给结构体定义方法

在 Go 中无法在结构体内部定义方法

go
package main
+
+import "fmt"
+
+// Lesson 定义一个名为 Lesson 的结构体
+type Lesson struct {
+	name, target string
+	spend        int
+}
+
+// ShowLessonInfo 定义一个与 Lesson 的绑定的方法
+func (l Lesson) ShowLessonInfo() {
+	fmt.Println("name:", l.name)
+	fmt.Println("target:", l.target)
+}
+
+func main() {
+	lesson13 := Lesson{
+		name:   "Go语言",
+		target: "掌握Go语言",
+	}
+	lesson13.ShowLessonInfo()
+}
package main
+
+import "fmt"
+
+// Lesson 定义一个名为 Lesson 的结构体
+type Lesson struct {
+	name, target string
+	spend        int
+}
+
+// ShowLessonInfo 定义一个与 Lesson 的绑定的方法
+func (l Lesson) ShowLessonInfo() {
+	fmt.Println("name:", l.name)
+	fmt.Println("target:", l.target)
+}
+
+func main() {
+	lesson13 := Lesson{
+		name:   "Go语言",
+		target: "掌握Go语言",
+	}
+	lesson13.ShowLessonInfo()
+}

上面的程序中定义了一个与结构体 Lesson 绑定的方法 ShowLessonInfo() ,其中 ShowLessonInfo 是方法名, (l Lesson) 表示将此方法与 Lesson 的实例绑定,这在 Go 语言中称为接收者,而 l 表示实例本身,相当于 Python 中的 self ,在方法内可以使用 实例本身.属性名称 来访问实例属性。

方法的参数传递方式

如果绑定结构体的方法中要改变实例的属性时,必须使用指针作为方法的接收者。

go
package main
+
+import "fmt"
+
+// Lesson 定义一个名为 Lesson 的结构体
+type Lesson struct {
+	name,target string
+	spend int
+}
+
+// ShowLessonInfo 定义一个与 Lesson 的绑定的方法
+func (l Lesson) ShowLessonInfo() {
+	fmt.Println("name:", l.name)
+	fmt.Println("target:", l.target)
+}
+
+// AddTime 定义一个与 Lesson 的绑定的方法,使 spend 值加 n
+func (l *Lesson) AddTime(n int) {
+	l.spend = l.spend + n
+}
+
+func main() {
+	lesson13 := Lesson{
+		name:   "Go语言",
+		target: "掌握Go语言",
+        spend:50,
+	}
+	fmt.Println("添加add方法前")
+	lesson13.ShowLessonInfo()
+	lesson13.AddTime(5)
+	fmt.Println("添加add方法后")
+	lesson13.ShowLessonInfo()
+}
package main
+
+import "fmt"
+
+// Lesson 定义一个名为 Lesson 的结构体
+type Lesson struct {
+	name,target string
+	spend int
+}
+
+// ShowLessonInfo 定义一个与 Lesson 的绑定的方法
+func (l Lesson) ShowLessonInfo() {
+	fmt.Println("name:", l.name)
+	fmt.Println("target:", l.target)
+}
+
+// AddTime 定义一个与 Lesson 的绑定的方法,使 spend 值加 n
+func (l *Lesson) AddTime(n int) {
+	l.spend = l.spend + n
+}
+
+func main() {
+	lesson13 := Lesson{
+		name:   "Go语言",
+		target: "掌握Go语言",
+        spend:50,
+	}
+	fmt.Println("添加add方法前")
+	lesson13.ShowLessonInfo()
+	lesson13.AddTime(5)
+	fmt.Println("添加add方法后")
+	lesson13.ShowLessonInfo()
+}

函数

函数 是基于功能或逻辑进行封装的可复用的代码结构。将一段功能复杂、很长的一段代码封装成多个代码片段(即函数),有助于提高代码可读性和可维护性。由于 Go 语言是编译型语言,所以函数编写的顺序是无关紧要的。

声明

go
func function_name(parameter_list) (result_list) {
+    //函数体
+}
func function_name(parameter_list) (result_list) {
+    //函数体
+}

可变参数

多个类型一致的参数

在参数类型前面加 ... 表示一个切片,用来接收调用者传入的参数。注意,如果该函数下有其他类型的参数,这些其他参数必须放在参数列表的前面,切片必须放在最后。

go
package main
+
+import "fmt"
+
+func show(args ...string) int {
+	sum := 0
+	for _, item := range args {
+        fmt.Println(item)
+		sum += 1
+	}
+	return sum
+}
+
+func main() {
+	fmt.Println(show("1","2","3"))
+}
package main
+
+import "fmt"
+
+func show(args ...string) int {
+	sum := 0
+	for _, item := range args {
+        fmt.Println(item)
+		sum += 1
+	}
+	return sum
+}
+
+func main() {
+	fmt.Println(show("1","2","3"))
+}

多个类型不一致的参数

如果传多个参数的类型都不一样,可以指定类型为 ...interface{} ,然后再遍历。

go
package main
+
+import "fmt"
+
+func PrintType(args ...interface{}) {
+	for _, arg := range args {
+		switch arg.(type) {
+		case int:
+			fmt.Println(arg, "type is int.")
+		case string:
+			fmt.Println(arg, "type is string.")
+		case float64:
+			fmt.Println(arg, "type is float64.")
+		default:
+			fmt.Println(arg, "is an unknown type.")
+		}
+	}
+}
+
+func main() {
+	PrintType(57, 3.14, "123")
+}
package main
+
+import "fmt"
+
+func PrintType(args ...interface{}) {
+	for _, arg := range args {
+		switch arg.(type) {
+		case int:
+			fmt.Println(arg, "type is int.")
+		case string:
+			fmt.Println(arg, "type is string.")
+		case float64:
+			fmt.Println(arg, "type is float64.")
+		default:
+			fmt.Println(arg, "is an unknown type.")
+		}
+	}
+}
+
+func main() {
+	PrintType(57, 3.14, "123")
+}

解序列

使用 ... 可以用来解序列

函数的返回值

当函数没有返回值时,函数体可以使用 return 语句返回。在 Go 中一个函数可以返回多个值。

go
package main
+
+import "fmt"
+
+func showBookInfo(bookName, authorName string) (string, error) {
+	if bookName == "" {
+		return "", errors.New("图书名称为空")
+	}
+	if authorName == "" {
+		return "", errors.New("作者名称为空")
+	}
+	return bookName + ",作者:" + authorName, nil
+}
+
+func main() {
+	bookInfo, err := showBookInfo("123", "45")
+	fmt.Printf("bookInfo = %s, err = %v", bookInfo, err)
+}
package main
+
+import "fmt"
+
+func showBookInfo(bookName, authorName string) (string, error) {
+	if bookName == "" {
+		return "", errors.New("图书名称为空")
+	}
+	if authorName == "" {
+		return "", errors.New("作者名称为空")
+	}
+	return bookName + ",作者:" + authorName, nil
+}
+
+func main() {
+	bookInfo, err := showBookInfo("123", "45")
+	fmt.Printf("bookInfo = %s, err = %v", bookInfo, err)
+}

返回带有变量名的值

go
func showBookInfo2(bookName, authorName string) (info string, err error) {
+	info = ""
+	if bookName == "" {
+		err = errors.New("图书名称为空")
+		return
+	}
+	if authorName == "" {
+		err = errors.New("作者名称为空")
+		return
+	}
+    // 不使用 := 因为已经在返回值那里声明了
+	info = bookName + ",作者:" + authorName
+  	// 直接返回即可
+	return
+}
func showBookInfo2(bookName, authorName string) (info string, err error) {
+	info = ""
+	if bookName == "" {
+		err = errors.New("图书名称为空")
+		return
+	}
+	if authorName == "" {
+		err = errors.New("作者名称为空")
+		return
+	}
+    // 不使用 := 因为已经在返回值那里声明了
+	info = bookName + ",作者:" + authorName
+  	// 直接返回即可
+	return
+}

匿名函数

go
func (parameter_list) (result_list) {
+	body
+}
func (parameter_list) (result_list) {
+	body
+}

内部方法与外部方法

在 Go 语言中,函数名通过首字母大小写实现控制对方法的访问权限。

  • 当方法的首字母为 大写 时,这个方法对于 所有包 都是 Public ,其他包可以随意调用。
  • 当方法的首字母为 小写 时,这个方法是 Private ,其他包是无法访问的。

方法

方法 其实就是一个函数,在 func 这个关键字和方法名中间加入了一个特殊的接收器类型。接收器可以是结构体类型或者是非结构体类型。接收器是可以在方法的内部访问的。

go
func (t Type) methodName(parameterList) returnList{
+}
func (t Type) methodName(parameterList) returnList{
+}

实例绑定

go
package main
+
+import "fmt"
+
+// Lesson 定义一个名为 Lesson 的结构体
+type Lesson struct {
+    Name   string
+    Target string
+}
+
+// PrintInfo 定义一个与 Lesson 的绑定的方法
+func (lesson Lesson) PrintInfo() {
+    fmt.Println("name:", lesson.Name)
+    fmt.Println("target:", lesson.Target)
+}
+
+
+func main() {
+    l := Lesson{
+        Name: "Go语言",
+        Target: "掌握Go语言",
+    }
+    l.PrintInfo()
+}
package main
+
+import "fmt"
+
+// Lesson 定义一个名为 Lesson 的结构体
+type Lesson struct {
+    Name   string
+    Target string
+}
+
+// PrintInfo 定义一个与 Lesson 的绑定的方法
+func (lesson Lesson) PrintInfo() {
+    fmt.Println("name:", lesson.Name)
+    fmt.Println("target:", lesson.Target)
+}
+
+
+func main() {
+    l := Lesson{
+        Name: "Go语言",
+        Target: "掌握Go语言",
+    }
+    l.PrintInfo()
+}

也可以把上面程序的方法改成一个函数

go
package main
+
+import "fmt"
+
+type Lesson struct {
+    Name   string
+    Target string
+}
+
+func PrintInfo(lesson Lesson) {
+    fmt.Println("name:", lesson.Name)
+    fmt.Println("target:", lesson.Target)
+}
+
+func main() {
+    lesson := Lesson{
+        Name: "Go语言",
+        Target: "掌握Go语言",
+    }
+    PrintInfo(lesson)
+}
package main
+
+import "fmt"
+
+type Lesson struct {
+    Name   string
+    Target string
+}
+
+func PrintInfo(lesson Lesson) {
+    fmt.Println("name:", lesson.Name)
+    fmt.Println("target:", lesson.Target)
+}
+
+func main() {
+    lesson := Lesson{
+        Name: "Go语言",
+        Target: "掌握Go语言",
+    }
+    PrintInfo(lesson)
+}

运行这个程序,也同样会输出上面一样的答案,那么我们为什么还要用方法呢?因为在 Go 中,相同的名字的方法可以定义在不同的类型上,而相同名字的函数是不被允许的。如果你在上面这个程序添加一个同名函数,就会报错。但是在不同的结构体上面定义同名的方法就是可行的。

go
package main
+
+import "fmt"
+
+type Lesson struct {
+    Name   string
+    Target string
+}
+
+func (lesson Lesson) PrintInfo() {
+    fmt.Println("Lesson name:", lesson.Name)
+    fmt.Println("Lesson target:", lesson.Target)
+}
+
+type Author struct {
+    Name string
+}
+
+func (author Author) PrintInfo() {
+    fmt.Println("author name:", author.Name)
+}
+
+func main() {
+    lesson := Lesson{
+        Name: "Go语言",
+        Target: "掌握Go语言",
+    }
+    lesson.PrintInfo()
+    author := Author{"Google"}
+    author.PrintInfo()
+}
package main
+
+import "fmt"
+
+type Lesson struct {
+    Name   string
+    Target string
+}
+
+func (lesson Lesson) PrintInfo() {
+    fmt.Println("Lesson name:", lesson.Name)
+    fmt.Println("Lesson target:", lesson.Target)
+}
+
+type Author struct {
+    Name string
+}
+
+func (author Author) PrintInfo() {
+    fmt.Println("author name:", author.Name)
+}
+
+func main() {
+    lesson := Lesson{
+        Name: "Go语言",
+        Target: "掌握Go语言",
+    }
+    lesson.PrintInfo()
+    author := Author{"Google"}
+    author.PrintInfo()
+}

指针接收器与值接收器

值接收器和指针接收器之间的区别在于,在指针接收器的方法内部的改变对于调用者是可见的,然而值接收器的方法内部的改变对于调用者是不可见的,所以若要改变实例的属性时,必须使用指针作为方法的接收者。

go
package main
+
+import "fmt"
+
+// Lesson 定义一个名为 Lesson 的结构体
+type Lesson struct {
+	Name      string
+	Target    string
+	SpendTime int
+}
+
+// PrintInfo 定义一个与 Lesson 的绑定的方法
+func (lesson Lesson) PrintInfo() {
+	fmt.Println("name:", lesson.Name)
+	fmt.Println("target:", lesson.Target)
+	fmt.Println("spendTime:", lesson.SpendTime)
+}
+
+func (lesson Lesson) ChangeLessonName(name string) {
+	lesson.Name = name
+}
+
+func (lesson *Lesson) AddSpendTime(n int) {
+	lesson.SpendTime = lesson.SpendTime + n
+}
+
+func main() {
+	lesson := Lesson{
+		Name:      "Go语言",
+		Target:    "掌握Go语言",
+		SpendTime: 1,
+	}
+	fmt.Println("before change")
+	lesson.PrintInfo()
+
+	fmt.Println("after change")
+	lesson.AddSpendTime(2)
+	lesson.ChangeLessonName("Go语言123")
+	lesson.PrintInfo()
+}
package main
+
+import "fmt"
+
+// Lesson 定义一个名为 Lesson 的结构体
+type Lesson struct {
+	Name      string
+	Target    string
+	SpendTime int
+}
+
+// PrintInfo 定义一个与 Lesson 的绑定的方法
+func (lesson Lesson) PrintInfo() {
+	fmt.Println("name:", lesson.Name)
+	fmt.Println("target:", lesson.Target)
+	fmt.Println("spendTime:", lesson.SpendTime)
+}
+
+func (lesson Lesson) ChangeLessonName(name string) {
+	lesson.Name = name
+}
+
+func (lesson *Lesson) AddSpendTime(n int) {
+	lesson.SpendTime = lesson.SpendTime + n
+}
+
+func main() {
+	lesson := Lesson{
+		Name:      "Go语言",
+		Target:    "掌握Go语言",
+		SpendTime: 1,
+	}
+	fmt.Println("before change")
+	lesson.PrintInfo()
+
+	fmt.Println("after change")
+	lesson.AddSpendTime(2)
+	lesson.ChangeLessonName("Go语言123")
+	lesson.PrintInfo()
+}

在上面的程序中, AddSpendTime 使用指针接收器最终能改变实例的 SpendTime 值,然而使用值接收器的 ChangeLessonName 最终没有改变实例 Name 的值。

在方法中使用值接收器 与 在函数中使用值参数

当一个函数有一个值参数,它只能接受一个值参数。当一个方法有一个值接收器,它可以接受值接收器和指针接收器。

go
package main
+
+import "fmt"
+
+type Lesson struct {
+	Name string
+}
+
+func (lesson Lesson) PrintInfo() {
+	fmt.Println(lesson.Name)
+}
+
+func PrintInfo(lesson Lesson) {
+	fmt.Println(lesson.Name)
+}
+
+func main() {
+	lesson := Lesson{"Go语言"}
+	PrintInfo(lesson)
+	lesson.PrintInfo()
+
+	bPtr := &lesson
+	//PrintInfo(bPtr) // error
+	bPtr.PrintInfo()
+}
package main
+
+import "fmt"
+
+type Lesson struct {
+	Name string
+}
+
+func (lesson Lesson) PrintInfo() {
+	fmt.Println(lesson.Name)
+}
+
+func PrintInfo(lesson Lesson) {
+	fmt.Println(lesson.Name)
+}
+
+func main() {
+	lesson := Lesson{"Go语言"}
+	PrintInfo(lesson)
+	lesson.PrintInfo()
+
+	bPtr := &lesson
+	//PrintInfo(bPtr) // error
+	bPtr.PrintInfo()
+}

在上面的程序中,使用值参数 PrintInfo(lesson) 来调用这个函数是合法的,使用值接收器来调用 lesson.PrintInfo() 也是合法的。

然后在程序中我们创建了一个指向 Lesson 的指针 bPtr ,通过使用指针接收器来调用 bPtr.PrintInfo() 是合法的,但使用值参数调用 PrintInfo(bPtr) 是非法的。

在非结构体上的方法

go
package main
+
+import "fmt"
+
+type myInt int
+
+func (a myInt) add(b myInt) myInt {
+    return a + b
+}
+
+func main() {
+    var x myInt = 50
+    var y myInt = 7
+    fmt.Println(x.add(y))   // 57
+}
package main
+
+import "fmt"
+
+type myInt int
+
+func (a myInt) add(b myInt) myInt {
+    return a + b
+}
+
+func main() {
+    var x myInt = 50
+    var y myInt = 7
+    fmt.Println(x.add(y))   // 57
+}

接口

在 Go 语言中, 接口 就是方法签名(Method Signature)的集合。在面向对象的领域里,接口定义一个对象的行为,接口只指定了对象应该做什么,至于如何实现这个行为,则由对象本身去确定。当一个类型实现了接口中的所有方法,我们称它实现了该接口。接口指定了一个类型应该具有的方法,并由该类型决定如何实现这些方法。

定义

go
type interface_name interface {
+    method()
+}
type interface_name interface {
+    method()
+}

接口实现

go
package main
+
+import "fmt"
+
+type Study interface {
+    learn()
+}
+
+type Student struct {
+    name string
+    book string
+}
+
+func (s Student) learn() {
+    fmt.Printf("%s 在读 %s", s.name, s.book)
+}
+
+func main() {
+    student1 := Student{
+        name: "张三",
+        book: "《Go语言》",
+    }
+    student1.learn()
+}
package main
+
+import "fmt"
+
+type Study interface {
+    learn()
+}
+
+type Student struct {
+    name string
+    book string
+}
+
+func (s Student) learn() {
+    fmt.Printf("%s 在读 %s", s.name, s.book)
+}
+
+func main() {
+    student1 := Student{
+        name: "张三",
+        book: "《Go语言》",
+    }
+    student1.learn()
+}

上面的程序定义了一个名为 Study 的接口,接口中有未实现的方法 learn() ,这里还定义了名为 Student 的结构体,其绑定了方法 learn() ,也就隐式实现了 Study 接口,实现的内容是打印语句。

接口实现多态

go
package main
+
+import "fmt"
+
+type Study interface {
+	learn()
+}
+type Student struct {
+	name, book string
+}
+
+func (s Student) learn() {
+	fmt.Printf("%s 在读 %s", s.name, s.book)
+}
+
+type Worker struct {
+	name string
+	book string
+	by   string
+}
+
+func (w *Worker) learn() {
+	fmt.Printf("%s 在读 %s,通过方式 %s", w.name, w.book, w.by)
+}
+
+func main() {
+	var s1 Study
+	var s2 Study
+
+	student2 := Student{
+		name: "李四",
+		book: "《Go语言》",
+	}
+	s1 = student2
+	s1.learn()
+
+	student3 := Student{
+		name: "王五",
+		book: "Go语言1",
+	}
+	s1 = &student3
+	s1.learn()
+
+	worker1 := Worker{
+		name: "老王",
+		book: "Go语言2",
+		by:   "视频",
+	}
+	// s2 = worker1 // error
+	s2 = &worker1
+	s2.learn()
+}
package main
+
+import "fmt"
+
+type Study interface {
+	learn()
+}
+type Student struct {
+	name, book string
+}
+
+func (s Student) learn() {
+	fmt.Printf("%s 在读 %s", s.name, s.book)
+}
+
+type Worker struct {
+	name string
+	book string
+	by   string
+}
+
+func (w *Worker) learn() {
+	fmt.Printf("%s 在读 %s,通过方式 %s", w.name, w.book, w.by)
+}
+
+func main() {
+	var s1 Study
+	var s2 Study
+
+	student2 := Student{
+		name: "李四",
+		book: "《Go语言》",
+	}
+	s1 = student2
+	s1.learn()
+
+	student3 := Student{
+		name: "王五",
+		book: "Go语言1",
+	}
+	s1 = &student3
+	s1.learn()
+
+	worker1 := Worker{
+		name: "老王",
+		book: "Go语言2",
+		by:   "视频",
+	}
+	// s2 = worker1 // error
+	s2 = &worker1
+	s2.learn()
+}

接口的内部表示

可以把接口的内部看做 (type, value)type 是接口底层的具体类型(Concrete Type),而 value 是具体类型的值。

go
package main
+
+import "fmt"
+
+type Study interface {
+	learn()
+}
+type Student struct {
+	name, book string
+}
+
+func (s Student) learn() {
+	fmt.Printf("%s 在读 %s", s.name, s.book)
+}
+func ShowInterface(s Study) {
+	fmt.Printf("接口类型: %T\\n 接口值: %v\\n", s, s)
+}
+
+func main() {
+	var s Study
+	student2 := Student{
+		name: "李四",
+		book: "《Go语言》",
+	}
+	s = student2
+	ShowInterface(s)
+	s.learn()
+}
+//接口类型: main.Student
+//接口值: {李四 《Go语言》}
+//李四 在读 《Go语言》
package main
+
+import "fmt"
+
+type Study interface {
+	learn()
+}
+type Student struct {
+	name, book string
+}
+
+func (s Student) learn() {
+	fmt.Printf("%s 在读 %s", s.name, s.book)
+}
+func ShowInterface(s Study) {
+	fmt.Printf("接口类型: %T\\n 接口值: %v\\n", s, s)
+}
+
+func main() {
+	var s Study
+	student2 := Student{
+		name: "李四",
+		book: "《Go语言》",
+	}
+	s = student2
+	ShowInterface(s)
+	s.learn()
+}
+//接口类型: main.Student
+//接口值: {李四 《Go语言》}
+//李四 在读 《Go语言》

空接口

空接口 是特殊形式的接口类型,没有定义任何方法的接口就称为空接口,可以说所有类型都至少实现了空接口,空接口表示为 interface{} 。例如,我们之前的写过的空接口参数函数,可以接受任何类型的参数:

go
package main
+
+import "fmt"
+
+func ShowType(i interface{}) {
+    fmt.Printf("类型: %T, 值: %v\\n", i, i)
+}
+
+func main() {
+    str := "Go语言"
+    ShowType(str)
+    num := 3.14
+    ShowType(num)
+}
package main
+
+import "fmt"
+
+func ShowType(i interface{}) {
+    fmt.Printf("类型: %T, 值: %v\\n", i, i)
+}
+
+func main() {
+    str := "Go语言"
+    ShowType(str)
+    num := 3.14
+    ShowType(num)
+}

通过上面的例子不难发现接口都有两个属性,一个是值,而另一个是类型。对于空接口来说,这两个属性都为 nil

go
package main
+
+import "fmt"
+
+func main() {
+    var i interface{}
+    fmt.Printf("Type: %T, Value: %v", i, i)
+    // Type: <nil>, Value: <nil>
+}
package main
+
+import "fmt"
+
+func main() {
+    var i interface{}
+    fmt.Printf("Type: %T, Value: %v", i, i)
+    // Type: <nil>, Value: <nil>
+}

除了上面讲到的使用空接口作为函数参数的用法,空接口还有以下两种用法。

直接使用 interface{} 作为类型声明一个实例,这个实例就能承载任何类型的值:

go
package main
+
+import "fmt"
+
+func main() {
+    var i interface{}
+
+    i = "Go语言"
+    fmt.Println(i) // Let's go
+
+    i = 3.14
+    fmt.Println(i) // 3.14
+}
package main
+
+import "fmt"
+
+func main() {
+    var i interface{}
+
+    i = "Go语言"
+    fmt.Println(i) // Let's go
+
+    i = 3.14
+    fmt.Println(i) // 3.14
+}

我们也可以定义一个接收任何类型的 arrayslicemapstrcut 。例如:

go
package main
+
+import "fmt"
+
+func main() {
+    x := make([]interface{}, 3)
+    x[0] = "Go"
+    x[1] = 3.14
+    x[2] = []int{1, 2, 3}
+    for _, value := range x {
+        fmt.Println(value)
+    }
+}
package main
+
+import "fmt"
+
+func main() {
+    x := make([]interface{}, 3)
+    x[0] = "Go"
+    x[1] = 3.14
+    x[2] = []int{1, 2, 3}
+    for _, value := range x {
+        fmt.Println(value)
+    }
+}

空接口可以承载任何值,但是空接口类型的对象是不能赋值给另一个固定类型对象的。

go
package main
+
+func main() {
+    var num = 1
+    var i interface{} = num
+    var str string = i // error
+}
package main
+
+func main() {
+    var num = 1
+    var i interface{} = num
+    var str string = i // error
+}

当空接口承载数组和切片后,该对象无法再进行切片。

go
package main
+
+import "fmt"
+
+func main() {
+    var s = []int{1, 2, 3}
+
+    var i interface{} = s
+
+    var s2 = i[1:2] // error
+    fmt.Println(s2)
+}
package main
+
+import "fmt"
+
+func main() {
+    var s = []int{1, 2, 3}
+
+    var i interface{} = s
+
+    var s2 = i[1:2] // error
+    fmt.Println(s2)
+}

类型断言

类型断言用于提取接口的底层值(Underlying Value)。使用 interface.(Type) 可以获取接口的底层值,其中接口 interface 的具体类型是 Type

go
package main
+
+import "fmt"
+
+func assert(i interface{}) {
+    value, ok := i.(int)
+    fmt.Println(value, ok)
+}
+
+func main() {
+    var x interface{} = 3
+    assert(x)
+    var y interface{} = "Go语言"
+    assert(y)
+}
package main
+
+import "fmt"
+
+func assert(i interface{}) {
+    value, ok := i.(int)
+    fmt.Println(value, ok)
+}
+
+func main() {
+    var x interface{} = 3
+    assert(x)
+    var y interface{} = "Go语言"
+    assert(y)
+}

第一次调用 assert(x) 输出 3 true,表示将整数 3 转换为 int 类型成功。

第二次调用 assert(y) 输出 0 false,表示将字符串 "Go语言" 转换为 int 类型失败,因为该字符串无法转换为整数。

类型选择

go
package main
+
+import "fmt"
+
+func getTypeValue(i interface{}) {
+    switch i.(type) {
+    case int:
+        fmt.Printf("Type: int, Value: %d\\n", i.(int))
+    case string:
+        fmt.Printf("Type: string, Value: %s\\n", i.(string))
+    default:
+        fmt.Printf("Unknown type\\n")
+    }
+}
+
+func main() {
+    getTypeValue(300)
+    getTypeValue("Go语言")
+    getTypeValue(true)
+}
package main
+
+import "fmt"
+
+func getTypeValue(i interface{}) {
+    switch i.(type) {
+    case int:
+        fmt.Printf("Type: int, Value: %d\\n", i.(int))
+    case string:
+        fmt.Printf("Type: string, Value: %s\\n", i.(string))
+    default:
+        fmt.Printf("Unknown type\\n")
+    }
+}
+
+func main() {
+    getTypeValue(300)
+    getTypeValue("Go语言")
+    getTypeValue(true)
+}

实现多个接口

类型或者结构体可以实现多个接口

接口的嵌套

虽然在 Go 中没有继承机制,但可以通过接口的嵌套实现类似功能。

go
package main
+
+import "fmt"
+
+// 定义一个简单的读取器接口
+type Reader interface {
+    Read() string
+}
+
+// 定义一个简单的写入器接口
+type Writer interface {
+    Write(data string)
+}
+
+// 定义一个复合接口,嵌套了Reader和Writer接口
+type ReadWriter interface {
+    Reader
+    Writer
+}
+
+// 实现Reader接口
+type MyReader struct{}
+
+func (r MyReader) Read() string {
+    return "Data read from MyReader"
+}
+
+// 实现Writer接口
+type MyWriter struct{}
+
+func (w MyWriter) Write(data string) {
+    fmt.Println("Writing data:", data)
+}
+
+// 实现ReadWriter接口
+type MyReadWriter struct {
+    MyReader
+    MyWriter
+}
+
+// 使用ReadWriter接口作为参数进行函数调用
+func ProcessData(rw ReadWriter) {
+    data := rw.Read()
+    rw.Write(data + " modified")
+}
+
+func main() {
+    // 创建MyReadWriter实例
+    myRW := MyReadWriter{}
+    
+    // 调用ProcessData函数,传入myRW作为参数
+    ProcessData(myRW)
+}
package main
+
+import "fmt"
+
+// 定义一个简单的读取器接口
+type Reader interface {
+    Read() string
+}
+
+// 定义一个简单的写入器接口
+type Writer interface {
+    Write(data string)
+}
+
+// 定义一个复合接口,嵌套了Reader和Writer接口
+type ReadWriter interface {
+    Reader
+    Writer
+}
+
+// 实现Reader接口
+type MyReader struct{}
+
+func (r MyReader) Read() string {
+    return "Data read from MyReader"
+}
+
+// 实现Writer接口
+type MyWriter struct{}
+
+func (w MyWriter) Write(data string) {
+    fmt.Println("Writing data:", data)
+}
+
+// 实现ReadWriter接口
+type MyReadWriter struct {
+    MyReader
+    MyWriter
+}
+
+// 使用ReadWriter接口作为参数进行函数调用
+func ProcessData(rw ReadWriter) {
+    data := rw.Read()
+    rw.Write(data + " modified")
+}
+
+func main() {
+    // 创建MyReadWriter实例
+    myRW := MyReadWriter{}
+    
+    // 调用ProcessData函数,传入myRW作为参数
+    ProcessData(myRW)
+}

定义了三个接口:ReaderWriterReadWriter。然后,我们实现了这些接口的具体类型:MyReaderMyWriterMyReadWriter

MyReadWriter结构体通过嵌套MyReaderMyWriter,同时实现了ReaderWriter接口。这样,MyReadWriter可以以ReadWriter类型的方式使用。

main函数中,我们创建了一个MyReadWriter实例myRW,然后将其作为参数传递给ProcessData函数。ProcessData函数接收一个ReadWriter类型的参数,并调用其中的方法。

通过接口嵌套,我们可以更灵活地组织和复用代码

包(package) 用于组织 Go 源代码,提供了更好的可重用性与可读性.可以用 go list std命令查看标准包,标准库为大多数的程序提供了必要的基础组件。

创建包

先创建一个 book 文件夹,位于该目录下创建一个 book.go 源文件,里面实现自定义的数学加法函数。函数名的首字母要大写。

go
// Package book
+package book
+
+func ShowBookInfo(bookName, authorName string) (string, error) {
+  if bookName == "" {
+    return "", errors.New("图书名称为空")
+  }
+  if authorName == "" {
+    return "", errors.New("作者名称为空")
+  }
+  return bookName + ",作者:" + authorName, nil
+}
// Package book
+package book
+
+func ShowBookInfo(bookName, authorName string) (string, error) {
+  if bookName == "" {
+    return "", errors.New("图书名称为空")
+  }
+  if authorName == "" {
+    return "", errors.New("作者名称为空")
+  }
+  return bookName + ",作者:" + authorName, nil
+}

导入包

使用包之前我们需要导入包,在 GoLand 中会帮你自动导入所需要的包。导入包的语法为 import path ,其中 path 可以是相对于工作区文件夹的相对路径,也可以是绝对路径。

go
package main
+
+import (
+	"fmt"
+	"learn/book"
+)
+
+func main() {
+	bookName := "《Go语言》"
+	author := "Golang"
+	bookInfo, _ := book.ShowBookInfo(bookName, author)
+	fmt.Println("bookInfo = ", bookInfo)
+}
package main
+
+import (
+	"fmt"
+	"learn/book"
+)
+
+func main() {
+	bookName := "《Go语言》"
+	author := "Golang"
+	bookInfo, _ := book.ShowBookInfo(bookName, author)
+	fmt.Println("bookInfo = ", bookInfo)
+}

使用别名

go
import (
+    "crypto/rand"
+    mrand "math/rand" // 将名称替换为 mrand 避免冲突
+)
import (
+    "crypto/rand"
+    mrand "math/rand" // 将名称替换为 mrand 避免冲突
+)

使用点操作

go
import . "fmt"
+
+func main() {
+    Println("hello, world")
+}
import . "fmt"
+
+func main() {
+    Println("hello, world")
+}

对于一些使用高频的包,例如 fmt 包,每次调用打印函数时都要使用 fmt.Println() 进行调用,很不方便。可以在导入包的时,使用 import . package_path 语法。打印就不用加 fmt 了。

包的初始化

每个包都允许有一个或多个 init 函数, init 函数不应该有任何返回值类型和参数,在代码中也不能显式调用它,当这个包被导入时,就会执行这个包的 init 函数,做初始化任务, init 函数优先于 main 函数执行。该函数形式如下:

go
func init() {
+}
func init() {
+}

包的初始化顺序:首先初始化 包级别(Package Level) 的变量,紧接着调用 init 函数。包可以有多个 init 函数(在一个文件或分布于多个文件中),它们按照编译器解析它们的顺序进行调用。如果一个包导入了另一个包,会先初始化被导入的包。尽管一个包可能会被导入多次,但是它只会被初始化一次。

包的匿名导入

导入一个没有使用的包编译会报错。但有时候我们只是想执行包里的 init 函数来执行一些初始化任务,可以使用匿名导入的方法,使用 空白标识符(Blank Identifier)

go
import _ "fmt"
import _ "fmt"

协程

Go 语言的 协程(Groutine) 是与其他函数或方法一起并发运行的工作方式。协程可以看作是轻量级线程。与线程相比,创建一个协程的成本很小。因此在 Go 应用中,常常会看到会有很多协程并发地运行。

启动一个 go 协程

调用函数或者方法时,如果在前面加上关键字 go ,就可以让一个新的 Go 协程并发地运行。

go
// 定义一个函数
+func functionName(parameterList) {
+    code
+}
+
+// 执行一个函数
+functionName(parameterList)
+
+// 开启一个协程执行这个函数
+go functionName(parameterList)
// 定义一个函数
+func functionName(parameterList) {
+    code
+}
+
+// 执行一个函数
+functionName(parameterList)
+
+// 开启一个协程执行这个函数
+go functionName(parameterList)
go
package main
+
+import (
+ "fmt"
+ "time"
+)
+
+func PrintInfo() {
+ fmt.Println("Go语言")
+}
+
+func main() {
+ // 开启一个协程执行 PrintInfo 函数
+ go PrintInfo()
+ // 使主协程休眠 1 秒
+ time.Sleep(1 * time.Second)
+ // 打印 main
+ fmt.Println("main")
+}
package main
+
+import (
+ "fmt"
+ "time"
+)
+
+func PrintInfo() {
+ fmt.Println("Go语言")
+}
+
+func main() {
+ // 开启一个协程执行 PrintInfo 函数
+ go PrintInfo()
+ // 使主协程休眠 1 秒
+ time.Sleep(1 * time.Second)
+ // 打印 main
+ fmt.Println("main")
+}

PrintInfo() 函数与 main() 函数会并发执行,主函数运行在一个特殊的协程上,这个协程称之为 主协程(Main Goroutine)

启动一个新的协程时,协程的调用会立即返回。与函数不同,程序控制不会去等待 Go 协程执行完毕。在调用 Go 协程之后,程序控制会立即返回到代码的下一行,忽略该协程的任何返回值。如果 Go 主协程终止,则程序终止,于是其他 Go 协程也会终止。为了让新的协程能继续运行,在 main() 函数添加了 time.Sleep(1 * time.Second) 使主协程休眠 1 秒

启动多个 Go 协程

go
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func PrintNum(num int) {
+	for i := 0; i < 3; i++ {
+		fmt.Println(num)
+		// 避免观察不到并发效果 加个休眠
+		time.Sleep(100 * time.Millisecond)
+	}
+}
+
+func main() {
+	// 开启 1 号协程
+	go PrintNum(1)
+	// 开启 2 号协程
+	go PrintNum(2)
+	// 使主协程休眠 1 秒
+	time.Sleep(time.Second)
+}
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func PrintNum(num int) {
+	for i := 0; i < 3; i++ {
+		fmt.Println(num)
+		// 避免观察不到并发效果 加个休眠
+		time.Sleep(100 * time.Millisecond)
+	}
+}
+
+func main() {
+	// 开启 1 号协程
+	go PrintNum(1)
+	// 开启 2 号协程
+	go PrintNum(2)
+	// 使主协程休眠 1 秒
+	time.Sleep(time.Second)
+}

通道

通道(channel) ,就是一个管道,可以想像成 Go 协程之间通信的管道。它是一种队列式的数据结构,遵循先入先出的规则。

通道的声明

每个通道都只能传递一种数据类型的数据,在你声明的时候,我们要指定通道的类型。chan Type 表示 Type 类型的通道。通道的零值为 nil

go
var channel_name chan channel_types
+
+var ch chan string
var channel_name chan channel_types
+
+var ch chan string

通道的初始化

声明完通道后,通道的值为 nil ,我们不能直接使用,必须先使用 make 函数对通道进行初始化操作。

go
ch = make(chan channel_type)
+
+ch = make(chan string)
ch = make(chan channel_type)
+
+ch = make(chan string)

这样,我们就已经定义好了一个 string 类型的通道 nameChan 。当然,也可以使用简短声明语句一次性定义一个通道:

go
ch := make(chan string)
ch := make(chan string)

使用通道发送和接收数据

发送数据:

go
// 把 data 数据发送到 channel_name 通道中
+// 即把 data 数据写入到 channel_name 通道中
+channel_name <- data
// 把 data 数据发送到 channel_name 通道中
+// 即把 data 数据写入到 channel_name 通道中
+channel_name <- data

接收数据:

go
// 从 channel_name 通道中接收数据到 value
+// 即从 channel_name 通道中读取数据到 value
+value := <- channel_name
// 从 channel_name 通道中接收数据到 value
+// 即从 channel_name 通道中读取数据到 value
+value := <- channel_name

通道旁的箭头方向指定了是发送数据还是接收数据。箭头指向通道,代表数据写入到通道中;箭头往通道指向外,代表从通道读数据出去。

go
package main
+
+import (
+	"fmt"
+)
+
+func PrintChan(c chan string) {
+	// 往通道传入数据 
+	c <- "学习Go语言"
+}
+
+func main() {
+	// 创建一个通道
+	ch := make(chan string)
+	// 打印 "学习课程:"
+	fmt.Println("学习课程:")
+	// 开启协程
+	go PrintChan(ch)
+	// 从通道接收数据
+	rec := <- ch
+	// 打印从通道接收到的数据
+	fmt.Println(rec)
+}
package main
+
+import (
+	"fmt"
+)
+
+func PrintChan(c chan string) {
+	// 往通道传入数据 
+	c <- "学习Go语言"
+}
+
+func main() {
+	// 创建一个通道
+	ch := make(chan string)
+	// 打印 "学习课程:"
+	fmt.Println("学习课程:")
+	// 开启协程
+	go PrintChan(ch)
+	// 从通道接收数据
+	rec := <- ch
+	// 打印从通道接收到的数据
+	fmt.Println(rec)
+}

Tips: 发送与接收默认是阻塞的

  • 从上面的例子我们知道,如果从通道接收数据没接收完主协程是不会继续执行下去的。当把数据发送到通道时,会在发送数据的语句处发生阻塞,直到有其它协程从通道读取到数据,才会解除阻塞。与此类似,当读取通道的数据时,如果没有其它的协程把数据写入到这个通道,那么读取过程就会一直阻塞着。

通道的关闭

go
close(channel_name)
close(channel_name)

这里要注意,对于一个已经关闭的通道如果再次关闭会导致报错,我们可以在接收数据时,判断通道是否已经关闭,从通道读取数据返回的第二个值表示通道是否没被关闭,如果已经关闭,返回值为 false ;如果还未关闭,返回值为 true

go
value, ok := <- channel_name
value, ok := <- channel_name

通道的容量与长度

make 函数是可以接收两个参数的,同理,创建通道可以传入第二个参数——容量。

  • 当容量为 0 时,说明通道中不能存放数据,在发送数据时,必须要求立马有人接收,否则会报错。此时的通道称之为无缓冲通道。
  • 当容量为 1 时,说明通道只能缓存一个数据,若通道中已有一个数据,此时再往里发送数据,会造成程序阻塞。利用这点可以利用通道来做锁。
  • 当容量大于 1 时,通道中可以存放多个数据,可以用于多个协程之间的通信管道,共享资源。

既然通道有容量和长度,那么我们可以通过 cap 函数和 len 函数获取通道的容量和长度。

go
package main
+
+import (
+	"fmt"
+)
+
+func main() {
+	// 创建一个通道
+	c := make(chan int, 3)
+	fmt.Println("初始化后:")
+	fmt.Println("cap =", cap(c))
+	fmt.Println("len =", len(c))
+	c <- 1
+	c <- 2
+	fmt.Println("传入两个数后:")
+	fmt.Println("cap =", cap(c))
+	fmt.Println("len =", len(c))
+	<- c
+	fmt.Println("取出一个数后:")
+	fmt.Println("cap =", cap(c))
+	fmt.Println("len =", len(c))
+}
package main
+
+import (
+	"fmt"
+)
+
+func main() {
+	// 创建一个通道
+	c := make(chan int, 3)
+	fmt.Println("初始化后:")
+	fmt.Println("cap =", cap(c))
+	fmt.Println("len =", len(c))
+	c <- 1
+	c <- 2
+	fmt.Println("传入两个数后:")
+	fmt.Println("cap =", cap(c))
+	fmt.Println("len =", len(c))
+	<- c
+	fmt.Println("取出一个数后:")
+	fmt.Println("cap =", cap(c))
+	fmt.Println("len =", len(c))
+}

缓冲通道与无缓冲通道

按照是否可缓冲数据可分为:缓冲通道无缓冲通道

无缓冲通道在通道里无法存储数据,接收端必须先于发送端准备好,以确保你发送完数据后,有人立马接收数据,否则发送端就会造成阻塞,原因很简单,通道中无法存储数据。也就是说发送端和接收端是同步运行的。

go
c := make(chan int)
+// 或者
+c := make(chan int, 0)
c := make(chan int)
+// 或者
+c := make(chan int, 0)

缓冲通道允许通道里存储一个或多个数据,设置缓冲区后,发送端和接收端可以处于异步的状态。

go
c := make(chan int, 3)
c := make(chan int, 3)

双向通道

到目前为止,上面定义的都是双向通道,既可以发送数据也可以接收数据。例如:

go
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func main() {
+	// 创建一个通道
+	c := make(chan int)
+
+	// 发送数据
+	go func() {
+		fmt.Println("send: 1")
+		c <- 1
+	}()
+
+	// 接收数据
+	go func() {
+		n := <- c
+		fmt.Println("receive:", n)
+	}()
+
+	// 主协程休眠
+	time.Sleep(time.Millisecond)
+}
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func main() {
+	// 创建一个通道
+	c := make(chan int)
+
+	// 发送数据
+	go func() {
+		fmt.Println("send: 1")
+		c <- 1
+	}()
+
+	// 接收数据
+	go func() {
+		n := <- c
+		fmt.Println("receive:", n)
+	}()
+
+	// 主协程休眠
+	time.Sleep(time.Millisecond)
+}

单向通道

单向通道只能发送或者接收数据。所以可以具体细分为只读通道和只写通道。

<-chan 表示只读通道:

chan<- 表示只写通道:

go
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+// Sender 只写通道类型
+type Sender = chan<- string
+
+// Receiver 只读通道类型
+type Receiver = <-chan string
+
+func main() {
+	// 创建一个双向通道
+	var ch = make(chan string)
+
+	// 开启一个协程
+	go func() {
+		// 只能写通道
+		var sender Sender = ch
+		fmt.Println("即将学习:")
+		sender <- "Go语言"
+	}()
+
+	// 开启一个协程
+	go func() {
+		// 只能读通道
+		var receiver Receiver = ch
+		message := <-receiver
+		fmt.Println("开始学习: ", message)
+	}()
+
+	time.Sleep(time.Millisecond)
+}
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+// Sender 只写通道类型
+type Sender = chan<- string
+
+// Receiver 只读通道类型
+type Receiver = <-chan string
+
+func main() {
+	// 创建一个双向通道
+	var ch = make(chan string)
+
+	// 开启一个协程
+	go func() {
+		// 只能写通道
+		var sender Sender = ch
+		fmt.Println("即将学习:")
+		sender <- "Go语言"
+	}()
+
+	// 开启一个协程
+	go func() {
+		// 只能读通道
+		var receiver Receiver = ch
+		message := <-receiver
+		fmt.Println("开始学习: ", message)
+	}()
+
+	time.Sleep(time.Millisecond)
+}

遍历通道

使用 for range 循环可以遍历通道,但在遍历时要确保通道是处于关闭状态,否则循环会被阻塞。

go
package main
+
+import (
+   "fmt"
+)
+
+func loopPrint(c chan int) {
+   for i := 0; i < 10; i++ {
+      c <- i
+   }
+   // 记得要关闭通道
+   // 否则主协程遍历完不会结束,而会阻塞
+   close(c)
+}
+
+func main() {
+   // 创建一个通道
+   var ch2 = make(chan int, 5)
+   go loopPrint(ch2)
+   for v := range ch2 {
+      fmt.Println(v)
+   }
+}
package main
+
+import (
+   "fmt"
+)
+
+func loopPrint(c chan int) {
+   for i := 0; i < 10; i++ {
+      c <- i
+   }
+   // 记得要关闭通道
+   // 否则主协程遍历完不会结束,而会阻塞
+   close(c)
+}
+
+func main() {
+   // 创建一个通道
+   var ch2 = make(chan int, 5)
+   go loopPrint(ch2)
+   for v := range ch2 {
+      fmt.Println(v)
+   }
+}

用通道做锁

上面讲过,当通道容量为 1 时,说明通道只能缓存一个数据,若通道中已有一个数据,此时再往里发送数据,会造成程序阻塞。例如:

go
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+// 由于 x = x+1 不是原子操作
+// 所以应避免多个协程对 x 进行操作
+// 使用容量为 1 的通道可以达到锁的效果
+func increment(ch chan bool, x *int) {
+	ch <- true
+	*x = *x + 1
+	<- ch
+}
+
+func main() {
+	ch3 := make(chan bool, 1)
+	var x int
+	for i := 0; i < 10000; i++ {
+		go increment(ch3, &x)
+	}
+	time.Sleep(time.Millisecond)
+	fmt.Println("x =", x)
+}
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+// 由于 x = x+1 不是原子操作
+// 所以应避免多个协程对 x 进行操作
+// 使用容量为 1 的通道可以达到锁的效果
+func increment(ch chan bool, x *int) {
+	ch <- true
+	*x = *x + 1
+	<- ch
+}
+
+func main() {
+	ch3 := make(chan bool, 1)
+	var x int
+	for i := 0; i < 10000; i++ {
+		go increment(ch3, &x)
+	}
+	time.Sleep(time.Millisecond)
+	fmt.Println("x =", x)
+}

死锁

当协程给一个通道发送数据时,照理说会有其他 Go 协程来接收数据。如果没有的话,程序就会在运行时触发 panic ,形成死锁。同理,当有协程等着从一个通道接收数据时,我们期望其他的 Go 协程会向该通道写入数据,要不然程序也会触发 panic

go
package main
+
+func main() {
+	ch := make(chan bool)
+	ch <- true
+}
+//fatal error: all goroutines are asleep - deadlock!
package main
+
+func main() {
+	ch := make(chan bool)
+	ch <- true
+}
+//fatal error: all goroutines are asleep - deadlock!
go
package main
+
+import "fmt"
+
+func main() {
+	ch := make(chan bool)
+	ch <- true
+	fmt.Println(<-ch)
+}
+//fatal error: all goroutines are asleep - deadlock!
+//使用 make 函数创建通道时默认不传递第二个参数,通道中不能存放数据,在发送数据时,必须要求立马有人接收,即该通道为无缓冲通道。所以在接收者没有准备好前,发送操作会被阻塞。
package main
+
+import "fmt"
+
+func main() {
+	ch := make(chan bool)
+	ch <- true
+	fmt.Println(<-ch)
+}
+//fatal error: all goroutines are asleep - deadlock!
+//使用 make 函数创建通道时默认不传递第二个参数,通道中不能存放数据,在发送数据时,必须要求立马有人接收,即该通道为无缓冲通道。所以在接收者没有准备好前,发送操作会被阻塞。
go
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func funcRecieve(c chan bool) {
+	fmt.Println(<-c)
+}
+func main() {
+	ch4 := make(chan bool)
+	go funcRecieve(ch4)
+	ch4 <- true
+	time.Sleep(time.Millisecond)
+}
+
+
+// 或
+
+package main
+
+import "fmt"
+
+func main() {
+	ch6 := make(chan bool, 1)
+	ch6 <- true
+	ch6 <- false
+	fmt.Println(<-ch6)
+}
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func funcRecieve(c chan bool) {
+	fmt.Println(<-c)
+}
+func main() {
+	ch4 := make(chan bool)
+	go funcRecieve(ch4)
+	ch4 <- true
+	time.Sleep(time.Millisecond)
+}
+
+
+// 或
+
+package main
+
+import "fmt"
+
+func main() {
+	ch6 := make(chan bool, 1)
+	ch6 <- true
+	ch6 <- false
+	fmt.Println(<-ch6)
+}

WaitGroup

在实际开发中我们并不能保证每个协程执行的时间,如果需要等待多个协程,全部结束任务后,再执行某个业务逻辑。下面我们介绍处理这种情况的方式。

WaitGroup 有几个方法:

  • Add:初始值为 0 ,这里直接传入子协程的数量,你传入的值会往计数器上加。
  • Done:当某个子协程完成后,可调用此方法,会从计数器上减一,即子协程的数量减一,通常使用 defer 来调用。
  • Wait:阻塞当前协程,直到实例里的计数器归零。

使用信道

信道可以实现多个协程间的通信,于是乎我们可以定义一个信道,在任务执行完成后,往信道中写入 true ,然后在主协程中获取到 true ,就可以认为子协程已经执行完毕。

go
package main
+
+import "fmt"
+
+func main() {
+	isDone := make(chan bool)
+	go func() {
+		for i := 0; i < 5; i++{
+			fmt.Println(i)
+		}
+		isDone <- true
+	}()
+	<- isDone
+}
package main
+
+import "fmt"
+
+func main() {
+	isDone := make(chan bool)
+	go func() {
+		for i := 0; i < 5; i++{
+			fmt.Println(i)
+		}
+		isDone <- true
+	}()
+	<- isDone
+}

运行上面的程序,主协程就会等待创建的协程执行完毕后退出。

使用 WaitGroup

使用上面的信道方法,虽然可行,但在你程序中使用很多协程的话,你的代码就会看起来很复杂,这里就要介绍一种更好的方法,那就是使用 sync 包中提供的 WaitGroup 类型。WaitGroup 用于等待一批 Go 协程执行结束。程序控制会一直阻塞,直到这些协程全部执行完毕。当然 WaitGroup 也可以用于实现工作池。

WaitGroup 实例化后就能使用:

go
var name sync.WaitGroup
var name sync.WaitGroup
go
package main
+
+import (
+	"fmt"
+	"sync"
+)
+
+func task(taskNum int, wg *sync.WaitGroup) {
+	// 延迟调用 执行完子协程计数器减一
+	defer wg.Done()
+	// 输出任务号
+	for i := 0; i < 3; i++ {
+		fmt.Printf("task %d: %d\\n", taskNum, i)
+	}
+}
+
+func main() {
+	// 实例化 sync.WaitGroup
+	var waitGroup sync.WaitGroup
+	// 传入子协程的数量
+	waitGroup.Add(3)
+	// 开启一个子协程 协程 1 以及 实例 waitGroup
+	go task(1, &waitGroup)
+	// 开启一个子协程 协程 2 以及 实例 waitGroup
+	go task(2, &waitGroup)
+	// 开启一个子协程 协程 3 以及 实例 waitGroup
+	go task(3, &waitGroup)
+	// 实例 waitGroup 阻塞当前协程 等待所有子协程执行完
+	waitGroup.Wait()
+}
package main
+
+import (
+	"fmt"
+	"sync"
+)
+
+func task(taskNum int, wg *sync.WaitGroup) {
+	// 延迟调用 执行完子协程计数器减一
+	defer wg.Done()
+	// 输出任务号
+	for i := 0; i < 3; i++ {
+		fmt.Printf("task %d: %d\\n", taskNum, i)
+	}
+}
+
+func main() {
+	// 实例化 sync.WaitGroup
+	var waitGroup sync.WaitGroup
+	// 传入子协程的数量
+	waitGroup.Add(3)
+	// 开启一个子协程 协程 1 以及 实例 waitGroup
+	go task(1, &waitGroup)
+	// 开启一个子协程 协程 2 以及 实例 waitGroup
+	go task(2, &waitGroup)
+	// 开启一个子协程 协程 3 以及 实例 waitGroup
+	go task(3, &waitGroup)
+	// 实例 waitGroup 阻塞当前协程 等待所有子协程执行完
+	waitGroup.Wait()
+}

Select

select 语句用在多个发送/接收通道操作中进行选择。

  • select 语句会一直阻塞,直到发送/接收操作准备就绪。
  • 如果有多个通道操作准备完毕, select 会随机地选取其中之一执行。

select 语法如下:

go
select {
+    case expression1:
+        code
+    case expression2:
+        code
+    default:
+        code
+}
select {
+    case expression1:
+        code
+    case expression2:
+        code
+    default:
+        code
+}
go
package main
+
+import "fmt"
+
+func main() {
+    // 创建3个通道
+    ch1 := make(chan string, 1)
+    ch2 := make(chan string, 1)
+    ch3 := make(chan string, 1)
+    // 往通道 1 发送数据 
+    ch1 <- "Go语言1"
+    // 往通道 2 发送数据 
+    ch2 <- "Go语言2"
+    // 往通道 3 发送数据 
+    ch3 <- "Go语言3"
+
+    select {
+    // 如果从通道 1 收到数据
+    case message1 := <-ch1:
+        fmt.Println("ch1 received:", message1)
+    // 如果从通道 2 收到数据
+    case message2 := <-ch2:
+        fmt.Println("ch2 received:", message2)
+    // 如果从通道 3 收到数据
+    case message3 := <-ch3:
+        fmt.Println("ch3 received:", message3)
+    // 默认输出
+    default:
+        fmt.Println("No data received.")
+    }
+}
package main
+
+import "fmt"
+
+func main() {
+    // 创建3个通道
+    ch1 := make(chan string, 1)
+    ch2 := make(chan string, 1)
+    ch3 := make(chan string, 1)
+    // 往通道 1 发送数据 
+    ch1 <- "Go语言1"
+    // 往通道 2 发送数据 
+    ch2 <- "Go语言2"
+    // 往通道 3 发送数据 
+    ch3 <- "Go语言3"
+
+    select {
+    // 如果从通道 1 收到数据
+    case message1 := <-ch1:
+        fmt.Println("ch1 received:", message1)
+    // 如果从通道 2 收到数据
+    case message2 := <-ch2:
+        fmt.Println("ch2 received:", message2)
+    // 如果从通道 3 收到数据
+    case message3 := <-ch3:
+        fmt.Println("ch3 received:", message3)
+    // 默认输出
+    default:
+        fmt.Println("No data received.")
+    }
+}

在执行 select 语句时,如果有机会的话会运行所有表达式,只要其中一个通道接收到数据,那么就会执行对应的 case 代码,然后退出。

select 的应用

每个任务执行的时间不同,使用 select 语句等待相应的通道发出响应。select 会选择首先响应先完成的 task,而忽略其它的响应。使用这种方法,我们可以做多个 task,并给用户返回最快的 task 结果。

go
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func task1(ch chan string) {
+	time.Sleep(5 * time.Second)
+	ch <- "Go语言1"
+}
+
+func task2(ch chan string) {
+	time.Sleep(7 * time.Second)
+	ch <- "Go语言2"
+}
+
+func task3(ch chan string) {
+	time.Sleep(2 * time.Second)
+	ch <- "Go语言3"
+}
+
+func main() {
+	// 创建三个通道
+	ch1 := make(chan string)
+	ch2 := make(chan string)
+	ch3 := make(chan string)
+	go task1(ch1)
+	go task2(ch2)
+	go task3(ch3)
+
+	select {
+	// 如果从通道 1 收到数据
+	case message1 := <-ch1:
+		fmt.Println("ch1 received:", message1)
+	// 如果从通道 2 收到数据
+	case message2 := <-ch2:
+		fmt.Println("ch2 received:", message2)
+	// 如果从通道 3 收到数据
+	case message3 := <-ch3:
+		fmt.Println("ch3 received:", message3)
+	}
+}
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func task1(ch chan string) {
+	time.Sleep(5 * time.Second)
+	ch <- "Go语言1"
+}
+
+func task2(ch chan string) {
+	time.Sleep(7 * time.Second)
+	ch <- "Go语言2"
+}
+
+func task3(ch chan string) {
+	time.Sleep(2 * time.Second)
+	ch <- "Go语言3"
+}
+
+func main() {
+	// 创建三个通道
+	ch1 := make(chan string)
+	ch2 := make(chan string)
+	ch3 := make(chan string)
+	go task1(ch1)
+	go task2(ch2)
+	go task3(ch3)
+
+	select {
+	// 如果从通道 1 收到数据
+	case message1 := <-ch1:
+		fmt.Println("ch1 received:", message1)
+	// 如果从通道 2 收到数据
+	case message2 := <-ch2:
+		fmt.Println("ch2 received:", message2)
+	// 如果从通道 3 收到数据
+	case message3 := <-ch3:
+		fmt.Println("ch3 received:", message3)
+	}
+}

上面的程序会发现,没有 default 分支,因为如果加了该默认分支,如果还没从通道接收到数据, select 语句就会直接执行 default 分支然后退出,而不是被阻塞。

造成死锁

如果没有 default 分支, select 就会阻塞,如果一直没有命中其中的某个 case 最后会造成死锁。

go
package main
+
+import (
+    "fmt"
+)
+
+func main() {
+    // 创建两个通道
+    ch1 := make(chan string, 1)
+    ch2 := make(chan string, 1)
+    ch3 := make(chan string, 1)
+
+    select {
+    // 如果从通道 1 收到数据
+    case message1 := <-ch1:
+        fmt.Println("ch1 received:", message1)
+    // 如果从通道 2 收到数据
+    case message2 := <-ch2:
+        fmt.Println("ch2 received:", message2)
+	// 如果从通道 3 收到数据
+    case message3 := <-ch3:
+        fmt.Println("ch3 received:", message3)
+    }
+}
+//fatal error: all goroutines are asleep - deadlock!
package main
+
+import (
+    "fmt"
+)
+
+func main() {
+    // 创建两个通道
+    ch1 := make(chan string, 1)
+    ch2 := make(chan string, 1)
+    ch3 := make(chan string, 1)
+
+    select {
+    // 如果从通道 1 收到数据
+    case message1 := <-ch1:
+        fmt.Println("ch1 received:", message1)
+    // 如果从通道 2 收到数据
+    case message2 := <-ch2:
+        fmt.Println("ch2 received:", message2)
+	// 如果从通道 3 收到数据
+    case message3 := <-ch3:
+        fmt.Println("ch3 received:", message3)
+    }
+}
+//fatal error: all goroutines are asleep - deadlock!

运行上面的程序会造成死锁。解决该问题的方法是写好 default 分支。

还有另一种情况会导致死锁的发生,那就是使用空 select

go
package main
+
+func main() {
+    select {}
+}
package main
+
+func main() {
+    select {}
+}

运行上面的程序会抛出 panic

Tips:

  • switch-case 里面的 case 是顺序执行的,但在 select 里并不是顺序执行的。在上面的第一个例子就可以看出,当 select 由多个 case 准备就绪时,将会随机地选取其中之一去执行。

select超时处理

case 里的通道始终没有接收到数据时,而且也没有 default 语句时, select 整体就会阻塞,但是有时我们并不希望 select 一直阻塞下去,这时候就可以手动设置一个超时时间。

go
package main
+
+import (
+    "fmt"
+    "time"
+)
+
+func makeTimeout(ch chan bool, t int) {
+    time.Sleep(time.Second * time.Duration(t))
+    ch <- true
+}
+
+func main() {
+    c1 := make(chan string, 1)
+    c2 := make(chan string, 1)
+    c3 := make(chan string, 1)
+    timeout := make(chan bool, 1)
+
+    go makeTimeout(timeout, 2)
+
+    select {
+    case msg1 := <-c1:
+        fmt.Println("c1 received: ", msg1)
+    case msg2 := <-c2:
+        fmt.Println("c2 received: ", msg2)
+    case msg3 := <-c3:
+        fmt.Println("c3 received: ", msg3)
+    case <-timeout:
+        fmt.Println("Timeout, exit.")
+    }
+}
package main
+
+import (
+    "fmt"
+    "time"
+)
+
+func makeTimeout(ch chan bool, t int) {
+    time.Sleep(time.Second * time.Duration(t))
+    ch <- true
+}
+
+func main() {
+    c1 := make(chan string, 1)
+    c2 := make(chan string, 1)
+    c3 := make(chan string, 1)
+    timeout := make(chan bool, 1)
+
+    go makeTimeout(timeout, 2)
+
+    select {
+    case msg1 := <-c1:
+        fmt.Println("c1 received: ", msg1)
+    case msg2 := <-c2:
+        fmt.Println("c2 received: ", msg2)
+    case msg3 := <-c3:
+        fmt.Println("c3 received: ", msg3)
+    case <-timeout:
+        fmt.Println("Timeout, exit.")
+    }
+}

读取/写入数据

select 里的 case 表达式只能对通道进行操作,不管你是往通道写入数据,还是从通道读出数据。

go
package main
+
+import (
+    "fmt"
+)
+
+func main() {
+    c1 := make(chan string, 2)
+
+    c1 <- "Go语言1"
+    select {
+    case c1 <- "Go语言2":
+        fmt.Println("c1 received: ", <-c1)
+        fmt.Println("c1 received: ", <-c1)
+    default:
+        fmt.Println("channel blocking")
+    }
+}
+//c1 received:  Go语言1
+//c1 received:  Go语言2
package main
+
+import (
+    "fmt"
+)
+
+func main() {
+    c1 := make(chan string, 2)
+
+    c1 <- "Go语言1"
+    select {
+    case c1 <- "Go语言2":
+        fmt.Println("c1 received: ", <-c1)
+        fmt.Println("c1 received: ", <-c1)
+    default:
+        fmt.Println("channel blocking")
+    }
+}
+//c1 received:  Go语言1
+//c1 received:  Go语言2

线程同步

Go 语言中,经常会遇到并发的问题,当然我们会优先考虑使用通道,同时 Go 语言也给出了传统的解决方式 Mutex(互斥锁)RWMutex(读写锁) 来处理竞争条件。

go
type Bank struct {
+    balance int
+}
+
+func (b *Bank) Deposit(amount int) {
+    b.balance += amount
+}
+
+func (b *Bank) Balance() int {
+    return b.balance
+}
+
+func main() {
+    b := &Bank{}
+
+    b.Deposit(1000)
+    b.Deposit(1000)
+    b.Deposit(1000)
+
+    fmt.Println(b.Balance())  //3000
+}
type Bank struct {
+    balance int
+}
+
+func (b *Bank) Deposit(amount int) {
+    b.balance += amount
+}
+
+func (b *Bank) Balance() int {
+    return b.balance
+}
+
+func main() {
+    b := &Bank{}
+
+    b.Deposit(1000)
+    b.Deposit(1000)
+    b.Deposit(1000)
+
+    fmt.Println(b.Balance())  //3000
+}

临界区

当程序并发地运行时,多个 Go 协程不应该同时访问那些修改共享资源的代码。这些修改共享资源的代码称为临界区

go
package main
+
+import (
+	"fmt"
+	"sync"
+)
+
+type Bank struct {
+	balance int
+}
+
+func (b *Bank) Deposit(amount int) {
+	b.balance += amount
+}
+
+func (b *Bank) Balance() int {
+	return b.balance
+}
+func main() {
+	var wg sync.WaitGroup
+	b := &Bank{}
+
+	n := 1000
+	wg.Add(n)
+	for i := 1; i <= n; i++ {
+		go func() {
+			b.Deposit(1000)
+			wg.Done()
+		}()
+	}
+	wg.Wait()
+	fmt.Println(b.Balance()) //972000,962000,941000
+}
package main
+
+import (
+	"fmt"
+	"sync"
+)
+
+type Bank struct {
+	balance int
+}
+
+func (b *Bank) Deposit(amount int) {
+	b.balance += amount
+}
+
+func (b *Bank) Balance() int {
+	return b.balance
+}
+func main() {
+	var wg sync.WaitGroup
+	b := &Bank{}
+
+	n := 1000
+	wg.Add(n)
+	for i := 1; i <= n; i++ {
+		go func() {
+			b.Deposit(1000)
+			wg.Done()
+		}()
+	}
+	wg.Wait()
+	fmt.Println(b.Balance()) //972000,962000,941000
+}

举一个简单的例子,当前变量的值增加 b.balance += amount

当然,对于只有一个协程的程序来说,上面的代码没有任何问题。但是,如果有多个协程并发运行时,就会发生错误,这种情况就称之为数据竞争(data race)。使用下面的互斥锁 Mutex 就能避免这种情况的发生。

互斥锁 Mutex

互斥锁(Mutex,mutual exclusion) 用于提供一种 加锁机制(Locking Mechanism) ,可确保在某时刻只有一个协程在临界区运行,以防止出现竞争。也是为了来保护一个资源不会因为并发操作而引起冲突导致数据不准确。

Mutex 有两个方法,分别是 Lock()Unlock() ,即对应的加锁和解锁。在 Lock()Unlock() 之间的代码,都只能由一个协程执行,就能避免竞争条件。

如果有一个协程已经持有了锁(Lock),当其他协程试图获得该锁时,这些协程会被阻塞,直到Mutex解除锁定。

go
package main
+
+import (
+    "fmt"
+    "sync"
+)
+
+type BankV2 struct {
+    balance int
+    m       sync.Mutex
+}
+
+func (b *BankV2) Deposit(amount int) {
+    b.m.Lock()
+    b.balance += amount
+    b.m.Unlock()
+}
+
+func (b *BankV2) Balance() int {
+    return b.balance
+}
+
+func main() {
+    var wg sync.WaitGroup
+    b := &BankV2{}
+
+    n := 1000
+    wg.Add(n)
+    for i := 1; i <= n; i++ {
+        go func() {
+            b.Deposit(1000)
+            wg.Done()
+        }()
+    }
+    wg.Wait()
+    fmt.Println(b.Balance()) //1000000
+}
package main
+
+import (
+    "fmt"
+    "sync"
+)
+
+type BankV2 struct {
+    balance int
+    m       sync.Mutex
+}
+
+func (b *BankV2) Deposit(amount int) {
+    b.m.Lock()
+    b.balance += amount
+    b.m.Unlock()
+}
+
+func (b *BankV2) Balance() int {
+    return b.balance
+}
+
+func main() {
+    var wg sync.WaitGroup
+    b := &BankV2{}
+
+    n := 1000
+    wg.Add(n)
+    for i := 1; i <= n; i++ {
+        go func() {
+            b.Deposit(1000)
+            wg.Done()
+        }()
+    }
+    wg.Wait()
+    fmt.Println(b.Balance()) //1000000
+}

要注意同一协程里不要在尚未解锁时再次加锁,也不要对已经解锁的锁再次解锁。

读写锁 RWMutex

sync.RWMutex 类型实现读写互斥锁,适用于读多写少的场景,它规定了当有人还在读取数据(即读锁占用)时,不允许有人更新这个数据(即写锁会阻塞);为了保证程序的效率,多个人(协程)读取数据(拥有读锁)时,互不影响不会造成阻塞,它不会像 Mutex 那样只允许有一个人(协程)读取同一个数据。读锁与读锁兼容,读锁与写锁互斥,写锁与写锁互斥。

  • 可以同时申请多个读锁;
  • 有读锁时申请写锁将阻塞,有写锁时申请读锁将阻塞;
  • 只要有写锁,后续申请读锁和写锁都将阻塞。

定义一个 RWMuteux 读写锁:

go
var rwMutex sync.RWMutex
var rwMutex sync.RWMutex

RWMutex 里提供了两种锁,每种锁分别对应两个方法,为了避免死锁,两个方法应成对出现,必要时请使用 defer

  • 读锁:调用 RLock 方法开启锁,调用 RUnlock 释放锁;
  • 写锁:调用 Lock 方法开启锁,调用 Unlock 释放锁。
go
package main
+
+import (
+    "fmt"
+    "sync"
+    "time"
+)
+
+type BankV3 struct {
+    balance int
+    rwMutex sync.RWMutex // read write lock
+}
+
+func (b *BankV3) Deposit(amount int) {
+    b.rwMutex.Lock() // write lock
+    b.balance += amount
+    b.rwMutex.Unlock() // wirte unlock
+}
+
+func (b *BankV3) Balance() (balance int) {
+    b.rwMutex.RLock() // read lock
+    balance = b.balance
+    b.rwMutex.RUnlock() // read unlock
+    return
+}
+
+func main() {
+    var wg sync.WaitGroup
+    b := &BankV3{}
+
+    n := 1000
+    wg.Add(n)
+    for i := 1; i <= n; i++ {
+        go func() {
+            b.Deposit(1000)
+            wg.Done()
+        }()
+    }
+    wg.Wait()
+    fmt.Println(b.Balance())
+}
package main
+
+import (
+    "fmt"
+    "sync"
+    "time"
+)
+
+type BankV3 struct {
+    balance int
+    rwMutex sync.RWMutex // read write lock
+}
+
+func (b *BankV3) Deposit(amount int) {
+    b.rwMutex.Lock() // write lock
+    b.balance += amount
+    b.rwMutex.Unlock() // wirte unlock
+}
+
+func (b *BankV3) Balance() (balance int) {
+    b.rwMutex.RLock() // read lock
+    balance = b.balance
+    b.rwMutex.RUnlock() // read unlock
+    return
+}
+
+func main() {
+    var wg sync.WaitGroup
+    b := &BankV3{}
+
+    n := 1000
+    wg.Add(n)
+    for i := 1; i <= n; i++ {
+        go func() {
+            b.Deposit(1000)
+            wg.Done()
+        }()
+    }
+    wg.Wait()
+    fmt.Println(b.Balance())
+}

条件变量 sync.Cond

Cond 实现了一个条件变量,在 Locker 的基础上增加的一个消息通知的功能,保存了一个通知列表,用来唤醒一个或所有因等待条件变量而阻塞的 Go 程,以此来实现多个 Go 程间的同步。

错误与异常

错误

内建错误

在 Go 中, 错误 使用内建的 error 类型表示。error 类型是一个接口类型,它的定义如下:

go
type error interface {
+    Error() string
+}
type error interface {
+    Error() string
+}

error 有了一个签名为 Error() string 的方法。所有实现该接口的类型都可以当作一个错误类型。Error() 方法给出了错误的描述。fmt.Println 在打印错误时,会在内部调用 Error() string 方法来得到该错误的描述。

go
package main
+
+import (
+    "fmt"
+    "os"
+)
+
+func main() {
+    // 尝试打开文件
+    file, err := os.Open("/a.txt")
+    // 如果打开文件时发生错误 返回一个不等于 nil 的错误
+    if err != nil {
+        fmt.Println(err)
+        return
+    }
+    // 如果打开文件成功 返回一个文件句柄 和 一个值为 nil 的错误
+    fmt.Println(file.Name(), "opened successfully")
+}
+// open /a.txt: no such file or directory
package main
+
+import (
+    "fmt"
+    "os"
+)
+
+func main() {
+    // 尝试打开文件
+    file, err := os.Open("/a.txt")
+    // 如果打开文件时发生错误 返回一个不等于 nil 的错误
+    if err != nil {
+        fmt.Println(err)
+        return
+    }
+    // 如果打开文件成功 返回一个文件句柄 和 一个值为 nil 的错误
+    fmt.Println(file.Name(), "opened successfully")
+}
+// open /a.txt: no such file or directory

自定义错误

使用 errors 包中的 New 函数可以创建自定义错误。下面是 errors 包中 New 函数的实现代码:

go
package errors
+
+func New(text string) error {
+    return &errorString{text}
+}
+
+type errorString struct {
+    s string
+}
+
+func (e *errorString) Error() string {
+    return e.s
+}
package errors
+
+func New(text string) error {
+    return &errorString{text}
+}
+
+type errorString struct {
+    s string
+}
+
+func (e *errorString) Error() string {
+    return e.s
+}

errorString 是一个结构体类型,只有一个字符串字段 s 。它使用了 errorString 指针接受者,来实现 error 接口的 Error() string 方法。New 函数有一个字符串参数,通过这个参数创建了 errorString 类型的变量,并返回了它的地址。于是它就创建并返回了一个新的错误。

下面是一个简单的自定义错误例子,该例子创建了一个计算矩形面积的函数,当矩形的长和宽两者有一个为负数时,就会返回一个错误:

go
package main
+
+import (
+    "errors"
+    "fmt"
+)
+
+func area(a, b int) (int, error) {
+    if a < 0 || b < 0 {
+        return 0, errors.New("计算错误, 长度或宽度,不能小于0.")
+    }
+    return a * b, nil
+}
+func main() {
+    a := 100
+    b := -10
+    r, err := area(a, b)
+    if err != nil {
+        fmt.Println(err)
+        return
+    }
+    fmt.Println("Area =", r)
+}
package main
+
+import (
+    "errors"
+    "fmt"
+)
+
+func area(a, b int) (int, error) {
+    if a < 0 || b < 0 {
+        return 0, errors.New("计算错误, 长度或宽度,不能小于0.")
+    }
+    return a * b, nil
+}
+func main() {
+    a := 100
+    b := -10
+    r, err := area(a, b)
+    if err != nil {
+        fmt.Println(err)
+        return
+    }
+    fmt.Println("Area =", r)
+}

给错误添加更多信息

上面的程序能报出我们自定义的错误,但是没有具体说明是哪个数据出了问题,所以下面就来改进一下这个程序,我们使用 fmt 包中的 Errorf 函数,规定错误格式,并返回一个符合该错误的字符串。

go
package main
+
+import (
+    "fmt"
+)
+
+func area(a, b int) (int, error) {
+    if a < 0 || b < 0 {
+        return 0, fmt.Errorf("计算错误, 长度%d或宽度%d,不能小于0", a, b)
+    }
+    return a * b, nil
+}
+func main() {
+    a := 100
+    b := -10
+    area, err := area(a, b)
+    if err != nil {
+        fmt.Println(err)
+        return
+    }
+    fmt.Println("Area =", area)
+}
package main
+
+import (
+    "fmt"
+)
+
+func area(a, b int) (int, error) {
+    if a < 0 || b < 0 {
+        return 0, fmt.Errorf("计算错误, 长度%d或宽度%d,不能小于0", a, b)
+    }
+    return a * b, nil
+}
+func main() {
+    a := 100
+    b := -10
+    area, err := area(a, b)
+    if err != nil {
+        fmt.Println(err)
+        return
+    }
+    fmt.Println("Area =", area)
+}

给错误添加更多信息还可以 使用结构体类型和字段 实现。下面还是通过改进上面的程序来讲解这种方法的实现:

首先创建一个表示错误的结构体类型,一般错误类型名称都是以 Error 结尾,上面的错误是由于面积计算中长度或宽度错误导致的,所以这里把结构体命名为 areaError

go
package main
+
+import (
+    "fmt"
+)
+
+type areaError struct {
+    // 错误信息
+    err string
+    // 错误有关的长度
+    length int
+    // 错误有关的宽度
+    width int
+}
+
+// 使用指针接收者 *areaError 实现了 error 接口的 Error() string 方法
+func (e *areaError) Error() string {
+    // 打印长度和宽度以及错误的描述
+    return fmt.Sprintf("length %d, width %d : %s", e.length, e.width, e.err)
+}
+
+func rectangleArea(a, b int) (int, error) {
+    if a < 0 || b < 0 {
+        return 0, &areaError{"length or width is negative", a, b}
+    }
+    return a * b, nil
+}
+func main() {
+    a := 100
+    b := -10
+    area, err := rectangleArea(a, b)
+    // 检查了错误是否为 nil
+    if err != nil {
+        // 断言 *areaError 类型
+        if err, ok := err.(*areaError); ok {
+            // 如果错误是 *areaError 类型
+            // 用 err.length 和 err.width 来获取错误的长度和宽度 打印出自定义错误的消息
+            fmt.Printf("length %d or width %d is less than zero", err.length, err.width)
+            return
+        }
+        fmt.Println(err)
+        return
+    }
+    fmt.Println("Area =", area)
+}
package main
+
+import (
+    "fmt"
+)
+
+type areaError struct {
+    // 错误信息
+    err string
+    // 错误有关的长度
+    length int
+    // 错误有关的宽度
+    width int
+}
+
+// 使用指针接收者 *areaError 实现了 error 接口的 Error() string 方法
+func (e *areaError) Error() string {
+    // 打印长度和宽度以及错误的描述
+    return fmt.Sprintf("length %d, width %d : %s", e.length, e.width, e.err)
+}
+
+func rectangleArea(a, b int) (int, error) {
+    if a < 0 || b < 0 {
+        return 0, &areaError{"length or width is negative", a, b}
+    }
+    return a * b, nil
+}
+func main() {
+    a := 100
+    b := -10
+    area, err := rectangleArea(a, b)
+    // 检查了错误是否为 nil
+    if err != nil {
+        // 断言 *areaError 类型
+        if err, ok := err.(*areaError); ok {
+            // 如果错误是 *areaError 类型
+            // 用 err.length 和 err.width 来获取错误的长度和宽度 打印出自定义错误的消息
+            fmt.Printf("length %d or width %d is less than zero", err.length, err.width)
+            return
+        }
+        fmt.Println(err)
+        return
+    }
+    fmt.Println("Area =", area)
+}

还可以使用 结构体类型的方法 来给错误添加更多信息。下面我们继续完善上面的程序,让程序更加精确的定位是长度引发的错误还是宽度引发的错误。

go
package main
+
+import (
+    "fmt"
+)
+
+type areaError struct {
+    // 错误信息
+    err string
+    // 长度
+    length int
+    // 宽度
+    width int
+}
+
+// 使用指针接收者 *areaError 实现了 error 接口的 Error() string 方法
+func (e *areaError) Error() string {
+    return e.err
+}
+
+// 长度为负数返回 true
+func (e *areaError) lengthNegative() bool {
+    return e.length < 0
+}
+
+// 宽度为负数返回 true
+func (e *areaError) widthNegative() bool {
+    return e.width < 0
+}
+
+func area(length, width int) (int, error) {
+    err := ""
+    if length < 0 {
+        err += "length is less than zero"
+    }
+    if width < 0 {
+        if err == "" {
+            err = "width is less than zero"
+        } else {
+            err += " and width is less than zero"
+        }
+    }
+    if err != "" {
+        return 0, &areaError{err, length, width}
+    }
+    return length * width, nil
+}
+
+func main() {
+    length := 100
+    width := -10
+    area, err := area(length, width)
+    // 检查了错误是否为 nil
+    if err != nil {
+        // 断言 *areaError 类型
+        if err, ok := err.(*areaError); ok {
+            // 如果错误是 *areaError 类型
+            // 如果长度为负数 打印错误长度具体值
+            if err.lengthNegative() {
+                fmt.Printf("error: 长度 %d 小于0\\n", err.length)
+            }
+            // 如果宽度为负数 打印错误宽度具体值
+            if err.widthNegative() {
+                fmt.Printf("error: 宽度 %d 小于0\\n", err.width)
+            }
+            return
+        }
+        fmt.Println(err)
+        return
+    }
+    fmt.Println("Area =", area)
+}
package main
+
+import (
+    "fmt"
+)
+
+type areaError struct {
+    // 错误信息
+    err string
+    // 长度
+    length int
+    // 宽度
+    width int
+}
+
+// 使用指针接收者 *areaError 实现了 error 接口的 Error() string 方法
+func (e *areaError) Error() string {
+    return e.err
+}
+
+// 长度为负数返回 true
+func (e *areaError) lengthNegative() bool {
+    return e.length < 0
+}
+
+// 宽度为负数返回 true
+func (e *areaError) widthNegative() bool {
+    return e.width < 0
+}
+
+func area(length, width int) (int, error) {
+    err := ""
+    if length < 0 {
+        err += "length is less than zero"
+    }
+    if width < 0 {
+        if err == "" {
+            err = "width is less than zero"
+        } else {
+            err += " and width is less than zero"
+        }
+    }
+    if err != "" {
+        return 0, &areaError{err, length, width}
+    }
+    return length * width, nil
+}
+
+func main() {
+    length := 100
+    width := -10
+    area, err := area(length, width)
+    // 检查了错误是否为 nil
+    if err != nil {
+        // 断言 *areaError 类型
+        if err, ok := err.(*areaError); ok {
+            // 如果错误是 *areaError 类型
+            // 如果长度为负数 打印错误长度具体值
+            if err.lengthNegative() {
+                fmt.Printf("error: 长度 %d 小于0\\n", err.length)
+            }
+            // 如果宽度为负数 打印错误宽度具体值
+            if err.widthNegative() {
+                fmt.Printf("error: 宽度 %d 小于0\\n", err.width)
+            }
+            return
+        }
+        fmt.Println(err)
+        return
+    }
+    fmt.Println("Area =", area)
+}

异常

错误和异常是两个不同的概念,非常容易混淆。错误指的是可能出现问题的地方出现了问题;而异常指的是不应该出现问题的地方出现了问题。

panic

在有些情况,当程序发生异常时,无法继续运行。在这种情况下,我们会使用 panic 来终止程序。当函数发生 panic 时,它会终止运行,在执行完所有的延迟函数后,程序返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪,最后程序终止。

我们应该尽可能地使用错误,而不是使用 panicrecover 。只有当程序不能继续运行的时候,才应该使用 panicrecover 机制。

panic 有两个合理的用例:

  • 发生了一个不能恢复的错误,此时程序不能继续运行。一个例子就是 web 服务器无法绑定所要求的端口。在这种情况下,就应该使用 panic ,因为如果不能绑定端口,啥也做不了。
  • 发生了一个编程上的错误。假如我们有一个接收指针参数的方法,而其他人使用 nil 作为参数调用了它。在这种情况下,我们可以使用 panic ,因为这是一个编程错误:用 nil 参数调用了一个只能接收合法指针的方法。
go
func panic(v interface{})
func panic(v interface{})
go
package main
+
+func main() {
+    panic("panic error")
+}
package main
+
+func main() {
+    panic("panic error")
+}

发生 panic 时的 defer

上面已经提到了,当函数发生 panic 时,它会终止运行,在执行完所有的延迟函数后,程序返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪,最后程序终止。

go
package main
+
+import "fmt"
+
+func myTest() {
+    defer fmt.Println("defer myTest")
+    panic("panic myTest")
+}
+func main() {
+    defer fmt.Println("defer main")
+    myTest()
+}
+// defer myTest
+// defer main
+// panic: panic myTest
package main
+
+import "fmt"
+
+func myTest() {
+    defer fmt.Println("defer myTest")
+    panic("panic myTest")
+}
+func main() {
+    defer fmt.Println("defer main")
+    myTest()
+}
+// defer myTest
+// defer main
+// panic: panic myTest

recover

recover 是一个内建函数,用于重新获得 panic 协程的控制。下面是内建函数 recover 的签名:

go
func recover() interface{}
func recover() interface{}

recover 必须在 defer 函数中才能生效,在其他作用域下,它是不工作的。在延迟函数内调用 recover ,可以取到 panic 的错误信息,并且停止 panic 续发事件,程序运行恢复正常。

go
package main
+
+import "fmt"
+
+func outOfArray(x int) {
+    defer func() {
+        // recover() 可以将捕获到的 panic 信息打印
+        if err := recover(); err != nil {
+            fmt.Println(err)
+        }
+    }()
+    var array [5]int
+    array[x] = 1
+}
+func main() {
+    // 故意制造数组越界 触发 panic
+    outOfArray(20)
+    // 如果能执行到这句 说明 panic 被捕获了
+    // 后续的程序能继续运行
+    fmt.Println("main...")
+}
+// runtime error: index out of range [20] with length 5
+// main...
package main
+
+import "fmt"
+
+func outOfArray(x int) {
+    defer func() {
+        // recover() 可以将捕获到的 panic 信息打印
+        if err := recover(); err != nil {
+            fmt.Println(err)
+        }
+    }()
+    var array [5]int
+    array[x] = 1
+}
+func main() {
+    // 故意制造数组越界 触发 panic
+    outOfArray(20)
+    // 如果能执行到这句 说明 panic 被捕获了
+    // 后续的程序能继续运行
+    fmt.Println("main...")
+}
+// runtime error: index out of range [20] with length 5
+// main...

虽然该程序触发了 panic ,但由于我们使用了 recover() 捕获了 panic 异常,并输出 panic 信息,即使 panic 会导致整个程序退出,但在退出前,有 defer 延迟函数,还是得执行完 defer 。然后程序还会继续执行下去

只有在相同的协程中调用 recover 才管用, recover 不能恢复一个不同协程的 panic

make 和 new

new函数

内置函数 new 分配内存。该函数只接受一个参数,该参数是一个任意类型(包括自定义类型),而不是值,返回指向该类型新分配零值的指针。

go
// The new built-in function allocates memory. The first argument is a type,
+// not a value, and the value returned is a pointer to a newly
+// allocated zero value of that type.
+func new(Type) *Type
// The new built-in function allocates memory. The first argument is a type,
+// not a value, and the value returned is a pointer to a newly
+// allocated zero value of that type.
+func new(Type) *Type

使用 new 函数首先会分配内存,并设置类型零值,最后返回指向该类型新分配零值的指针。

go
package main
+
+import (
+	"fmt"
+)
+
+func main() {
+	num := new(int)
+	// 打印出类型的值
+	fmt.Println(*num)  // 0
+}
package main
+
+import (
+	"fmt"
+)
+
+func main() {
+	num := new(int)
+	// 打印出类型的值
+	fmt.Println(*num)  // 0
+}

make函数

内置函数 make 只能分配和初始化类型为 slicemapchan 的对象。与 new 一样,第一个参数是类型,而不是值。与 new 不同, make 的返回类型与其参数的类型相同,而不是指向它的指针。结果取决于类型:

  • slice:size 指定长度。切片的容量等于其长度。可提供第三个参数以指定不同的容量;它不能小于长度。
  • map:为空映射分配足够的空间来容纳指定数量的元素。可以省略大小,在这种情况下,分配一个小的起始大小。
  • chan:使用指定的缓冲区容量初始化通道的缓冲区。如果为零,或者忽略了大小,则通道是无缓冲的。
go
func make(t Type, size ...IntegerType) Type
func make(t Type, size ...IntegerType) Type

使用make函数必须初始化

go
// slice
+a := make([]int, 2, 10)
+
+// map
+b := make(map[string]int)
+
+// chan
+c := make(chan int, 10)
// slice
+a := make([]int, 2, 10)
+
+// map
+b := make(map[string]int)
+
+// chan
+c := make(chan int, 10)

new 和 make 的区别

new:为所有的类型分配内存,并初始化为零值,返回指针。

make:只能为 slicemapchan 分配内存,并初始化,返回的是类型。

反射

reflect 包

Go 语言提供了一种机制,能够在运行时更新变量和检查它们的值、调用它们的方法,而不需要在编译时就知道这些变量的具体类型。这种机制被称为 反射

在 Go 中 reflect 包实现了运行时反射。reflect 包会帮助识别 interface{} 变量的底层具体类型和具体值。

reflect.Type

reflect.Type 表示 interface{} 的具体类型。reflect.TypeOf() 方法返回 reflect.Type

go
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func reflectType(x interface{}) {
+    obj := reflect.TypeOf(x)
+    fmt.Println(obj)
+}
+
+func main() {
+    var a int64 = 123
+    reflectType(a)
+    var b string = "Go语言"
+    reflectType(b)
+}
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func reflectType(x interface{}) {
+    obj := reflect.TypeOf(x)
+    fmt.Println(obj)
+}
+
+func main() {
+    var a int64 = 123
+    reflectType(a)
+    var b string = "Go语言"
+    reflectType(b)
+}

reflect.Value

reflect.Value 表示 interface{} 的具体值。reflect.ValueOf() 方法返回 reflect.Value

go
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func reflectType(x interface{}) {
+    typeX := reflect.TypeOf(x)
+    valueX := reflect.ValueOf(x)
+    fmt.Println(typeX)
+    fmt.Println(valueX)
+}
+
+func main() {
+    var a int64 = 123
+    reflectType(a)
+    var b string = "Go语言"
+    reflectType(b)
+}
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func reflectType(x interface{}) {
+    typeX := reflect.TypeOf(x)
+    valueX := reflect.ValueOf(x)
+    fmt.Println(typeX)
+    fmt.Println(valueX)
+}
+
+func main() {
+    var a int64 = 123
+    reflectType(a)
+    var b string = "Go语言"
+    reflectType(b)
+}

relfect.Kind

relfect.Kind 表示的是种类。在使用反射时,需要理解类型(Type)和种类(Kind)的区别。编程中,使用最多的是类型,但在反射中,当需要区分一个大品种的类型时,就会用到种类(Kind)。

Go 语言程序中的类型(Type)指的是系统原生数据类型,如 intstringboolfloat32 等类型,以及使用 type 关键字定义的类型,这些类型的名称就是其类型本身的名称。例如使用 type A struct{} 定义结构体时,A 就是 struct{} 的类型。

种类(Kind)指的是对象归属的品种,在 reflect 包中有如下定义:

go
// A Kind represents the specific kind of type that a Type represents.
+// The zero Kind is not a valid kind.
+type Kind uint
+
+const (
+    Invalid Kind = iota
+    Bool
+    Int
+    Int8
+    Int16
+    Int32
+    Int64
+    Uint
+    Uint8
+    Uint16
+    Uint32
+    Uint64
+    Uintptr
+    Float32
+    Float64
+    Complex64
+    Complex128
+    Array
+    Chan
+    Func
+    Interface
+    Map
+    Ptr
+    Slice
+    String
+    Struct
+    UnsafePointer
+)
// A Kind represents the specific kind of type that a Type represents.
+// The zero Kind is not a valid kind.
+type Kind uint
+
+const (
+    Invalid Kind = iota
+    Bool
+    Int
+    Int8
+    Int16
+    Int32
+    Int64
+    Uint
+    Uint8
+    Uint16
+    Uint32
+    Uint64
+    Uintptr
+    Float32
+    Float64
+    Complex64
+    Complex128
+    Array
+    Chan
+    Func
+    Interface
+    Map
+    Ptr
+    Slice
+    String
+    Struct
+    UnsafePointer
+)
go
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func reflectType(x interface{}) {
+    typeX := reflect.TypeOf(x)
+    fmt.Println(typeX.Kind()) // struct
+    fmt.Println(typeX)        // main.book
+}
+
+type book struct {
+}
+
+func main() {
+    var b book
+    reflectType(b)
+}
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func reflectType(x interface{}) {
+    typeX := reflect.TypeOf(x)
+    fmt.Println(typeX.Kind()) // struct
+    fmt.Println(typeX)        // main.book
+}
+
+type book struct {
+}
+
+func main() {
+    var b book
+    reflectType(b)
+}

relfect.NumField()

relfect.NumField() 方法返回结构体中字段的数量。

go
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func reflectNumField(x interface{}) {
+    // 检查 x 的类别是 struct
+    if reflect.ValueOf(x).Kind() == reflect.Struct {
+        v := reflect.ValueOf(x)
+        fmt.Println("Number of fields", v.NumField())
+    }
+}
+
+type book struct {
+    name string
+    spend  int
+}
+
+func main() {
+    var b book
+    reflectNumField(b)
+}
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func reflectNumField(x interface{}) {
+    // 检查 x 的类别是 struct
+    if reflect.ValueOf(x).Kind() == reflect.Struct {
+        v := reflect.ValueOf(x)
+        fmt.Println("Number of fields", v.NumField())
+    }
+}
+
+type book struct {
+    name string
+    spend  int
+}
+
+func main() {
+    var b book
+    reflectNumField(b)
+}

relfect.Field()

relfect.Field(i int) 方法返回字段 ireflect.Value

go
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func reflectNumField(x interface{}) {
+    // 检查 x 的类别是 struct
+    if reflect.ValueOf(x).Kind() == reflect.Struct {
+        v := reflect.ValueOf(x)
+        fmt.Println("Number of fields", v.NumField())
+        for i := 0; i < v.NumField(); i++ {
+            fmt.Printf("Field:%d type:%T value:%v\\n", i, v.Field(i), v.Field(i))
+        }
+    }
+}
+
+type book struct {
+    name string
+    spend  int
+}
+
+func main() {
+    var b = book{"Go语言", 8}
+    reflectNumField(a)
+}
+// Number of fields 2
+// Field:0 type:reflect.Value value:Go语言
+// Field:1 type:reflect.Value value:8
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func reflectNumField(x interface{}) {
+    // 检查 x 的类别是 struct
+    if reflect.ValueOf(x).Kind() == reflect.Struct {
+        v := reflect.ValueOf(x)
+        fmt.Println("Number of fields", v.NumField())
+        for i := 0; i < v.NumField(); i++ {
+            fmt.Printf("Field:%d type:%T value:%v\\n", i, v.Field(i), v.Field(i))
+        }
+    }
+}
+
+type book struct {
+    name string
+    spend  int
+}
+
+func main() {
+    var b = book{"Go语言", 8}
+    reflectNumField(a)
+}
+// Number of fields 2
+// Field:0 type:reflect.Value value:Go语言
+// Field:1 type:reflect.Value value:8

反射的三大定律

一个接口变量,实际上都是由一 pair 对(type 和 data)组合而成,pair 对中记录着实际变量的值和类型。也就是说在真实世界(反射前环境)里,type 和 value 是合并在一起组成接口变量的。

而在反射的世界(反射后的环境)里,type 和 data 却是分开的,他们分别由 reflect.Typereflect.Value 来表现。

Go语言反射三定律:

  1. Reflection goes from interface value to reflection object.
  2. Reflection goes from reflection object to interface value.
  3. To modify a reflection object, the value must be settable.
go
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func main() {
+    var a interface{} = 3.14
+
+    fmt.Printf("接口变量的类型为 %T ,值为 %v\\n", a, a)
+
+    t := reflect.TypeOf(a)
+    v := reflect.ValueOf(a)
+
+    // 反射第一定律
+    fmt.Printf("从接口变量到反射对象:Type对象类型为 %T\\n", t)
+    fmt.Printf("从接口变量到反射对象:Value对象类型为 %T\\n", v)
+
+    // 反射第二定律
+    i := v.Interface()
+    fmt.Printf("从反射对象到接口变量:对象类型为 %T,值为 %v\\n", i, i)
+    // 使用类型断言进行转换
+    x := v.Interface().(float64)
+    fmt.Printf("x 类型为 %T,值为 %v\\n", x, x)
+}
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func main() {
+    var a interface{} = 3.14
+
+    fmt.Printf("接口变量的类型为 %T ,值为 %v\\n", a, a)
+
+    t := reflect.TypeOf(a)
+    v := reflect.ValueOf(a)
+
+    // 反射第一定律
+    fmt.Printf("从接口变量到反射对象:Type对象类型为 %T\\n", t)
+    fmt.Printf("从接口变量到反射对象:Value对象类型为 %T\\n", v)
+
+    // 反射第二定律
+    i := v.Interface()
+    fmt.Printf("从反射对象到接口变量:对象类型为 %T,值为 %v\\n", i, i)
+    // 使用类型断言进行转换
+    x := v.Interface().(float64)
+    fmt.Printf("x 类型为 %T,值为 %v\\n", x, x)
+}
go
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func main() {
+    var a float64 = 3.14
+    v := reflect.ValueOf(a)
+    fmt.Println("是否可写:", v.CanSet())
+}
+---
+package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func main() {
+    var a float64 = 3.14
+    v := reflect.ValueOf(&a)
+    fmt.Println("是否可写:", v.CanSet())
+}
+---
+package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func main() {
+    var a float64 = 3.14
+    v := reflect.ValueOf(&a).Elem()
+    fmt.Println("是否可写:", v.CanSet())
+
+    v.SetFloat(2)
+    fmt.Println(v)
+}
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func main() {
+    var a float64 = 3.14
+    v := reflect.ValueOf(a)
+    fmt.Println("是否可写:", v.CanSet())
+}
+---
+package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func main() {
+    var a float64 = 3.14
+    v := reflect.ValueOf(&a)
+    fmt.Println("是否可写:", v.CanSet())
+}
+---
+package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func main() {
+    var a float64 = 3.14
+    v := reflect.ValueOf(&a).Elem()
+    fmt.Println("是否可写:", v.CanSet())
+
+    v.SetFloat(2)
+    fmt.Println(v)
+}
`,592),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const C=s(p,[["render",t]]);export{d as __pageData,C as default}; diff --git "a/assets/golang_base_golang\345\237\272\347\241\200\350\257\255\346\263\225.md.5d06c4a6.lean.js" "b/assets/golang_base_golang\345\237\272\347\241\200\350\257\255\346\263\225.md.5d06c4a6.lean.js" new file mode 100644 index 000000000..f7b67b3dd --- /dev/null +++ "b/assets/golang_base_golang\345\237\272\347\241\200\350\257\255\346\263\225.md.5d06c4a6.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"Golang基础语法","description":"","frontmatter":{},"headers":[],"relativePath":"golang/base/golang基础语法.md","filePath":"golang/base/golang基础语法.md","lastUpdated":1708510102000}'),p={name:"golang/base/golang基础语法.md"},o=l("",592),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const C=s(p,[["render",t]]);export{d as __pageData,C as default}; diff --git a/assets/golang_cli_Cobra.md.d15fd694.js b/assets/golang_cli_Cobra.md.d15fd694.js new file mode 100644 index 000000000..9a0884112 --- /dev/null +++ b/assets/golang_cli_Cobra.md.d15fd694.js @@ -0,0 +1,369 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const C=JSON.parse('{"title":"Cobra","description":"","frontmatter":{},"headers":[],"relativePath":"golang/cli/Cobra.md","filePath":"golang/cli/Cobra.md","lastUpdated":1708510102000}'),p={name:"golang/cli/Cobra.md"},o=l(`

Cobra

Cobra是一个能够快速构建cli工具的库,相比于之前用过的Python的argparser模块,Cobra更加强大、灵活,还有自动生成文档等功能。

https://github.com/spf13/cobra/blob/main/site/content/user_guide.md

安装cobra依赖

go get -u github.com/spf13/cobra@latest

安装cobra-cli工具

go install github.com/spf13/cobra-cli@latest

cobra-cli会被安装到GOPATH的bin目录

使用cobra-cli初始化项目

shell
cd cobra-learn
+cobra-cli init                             
+// Your Cobra application is ready at
+// /Users/story/Developer/go/src/cobra-learn
cd cobra-learn
+cobra-cli init                             
+// Your Cobra application is ready at
+// /Users/story/Developer/go/src/cobra-learn

生成的目录结构:

shell
├── LICENSE
+├── cmd
+│   └── root.go
+├── go.mod
+├── go.sum
+└── main.go
├── LICENSE
+├── cmd
+│   └── root.go
+├── go.mod
+├── go.sum
+└── main.go
go
// main.go
+package main
+
+import "cobra-learn/cmd"
+
+func main() {
+	cmd.Execute()
+}
// main.go
+package main
+
+import "cobra-learn/cmd"
+
+func main() {
+	cmd.Execute()
+}
go
// root.go
+package cmd
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "cobra-learn",
+	Short: "A brief description of your application",
+	Long: \`A longer description that spans multiple lines and likely contains
+examples and usage of using your application. For example:
+
+Cobra is a CLI library for Go that empowers applications.
+This application is a tool to generate the needed files
+to quickly create a Cobra application.\`,
+	// Uncomment the following line if your bare application
+	// has an action associated with it:
+	// Run: func(cmd *cobra.Command, args []string) { },
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	// Here you will define your flags and configuration settings.
+	// Cobra supports persistent flags, which, if defined here,
+	// will be global for your application.
+
+	// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra-learn.yaml)")
+
+	// Cobra also supports local flags, which will only run
+	// when this action is called directly.
+	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}
// root.go
+package cmd
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "cobra-learn",
+	Short: "A brief description of your application",
+	Long: \`A longer description that spans multiple lines and likely contains
+examples and usage of using your application. For example:
+
+Cobra is a CLI library for Go that empowers applications.
+This application is a tool to generate the needed files
+to quickly create a Cobra application.\`,
+	// Uncomment the following line if your bare application
+	// has an action associated with it:
+	// Run: func(cmd *cobra.Command, args []string) { },
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	// Here you will define your flags and configuration settings.
+	// Cobra supports persistent flags, which, if defined here,
+	// will be global for your application.
+
+	// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra-learn.yaml)")
+
+	// Cobra also supports local flags, which will only run
+	// when this action is called directly.
+	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}

执行命令go run main.go会输出定义的详细描述

shell
  cobra-learn go run main.go                             
+A longer description that spans multiple lines and likely contains
+examples and usage of using your application. For example:
+
+Cobra is a CLI library for Go that empowers applications.
+This application is a tool to generate the needed files
+to quickly create a Cobra application.
  cobra-learn go run main.go                             
+A longer description that spans multiple lines and likely contains
+examples and usage of using your application. For example:
+
+Cobra is a CLI library for Go that empowers applications.
+This application is a tool to generate the needed files
+to quickly create a Cobra application.

给命令添加子命令

shell
  cobra-learn cobra-cli add version
+version created at /Users/story/Developer/go/src/cobra-learn
  cobra-learn cobra-cli add version
+version created at /Users/story/Developer/go/src/cobra-learn

目录结构:

shell
├── LICENSE
+├── cmd
+│   ├── root.go
+│   └── version.go
+├── go.mod
+├── go.sum
+└── main.go
├── LICENSE
+├── cmd
+│   ├── root.go
+│   └── version.go
+├── go.mod
+├── go.sum
+└── main.go
go
// version.go
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+)
+
+// versionCmd represents the version command
+var versionCmd = &cobra.Command{
+	Use:   "version",
+	Short: "A brief description of your command",
+	Long: \`A longer description that spans multiple lines and likely contains examples
+and usage of using your command. For example:
+
+Cobra is a CLI library for Go that empowers applications.
+This application is a tool to generate the needed files
+to quickly create a Cobra application.\`,
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Println("version called")
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(versionCmd)
+
+	// Here you will define your flags and configuration settings.
+
+	// Cobra supports Persistent Flags which will work for this command
+	// and all subcommands, e.g.:
+	// versionCmd.PersistentFlags().String("foo", "", "A help for foo")
+
+	// Cobra supports local flags which will only run when this command
+	// is called directly, e.g.:
+	// versionCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}
// version.go
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+)
+
+// versionCmd represents the version command
+var versionCmd = &cobra.Command{
+	Use:   "version",
+	Short: "A brief description of your command",
+	Long: \`A longer description that spans multiple lines and likely contains examples
+and usage of using your command. For example:
+
+Cobra is a CLI library for Go that empowers applications.
+This application is a tool to generate the needed files
+to quickly create a Cobra application.\`,
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Println("version called")
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(versionCmd)
+
+	// Here you will define your flags and configuration settings.
+
+	// Cobra supports Persistent Flags which will work for this command
+	// and all subcommands, e.g.:
+	// versionCmd.PersistentFlags().String("foo", "", "A help for foo")
+
+	// Cobra supports local flags which will only run when this command
+	// is called directly, e.g.:
+	// versionCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}

执行go build编译项目,会在项目根目录生成二进制文件cobra-learn执行该命令:

shell
  cobra-learn ./cobra-learn    
+A longer description that spans multiple lines and likely contains
+examples and usage of using your application. For example:
+
+Cobra is a CLI library for Go that empowers applications.
+This application is a tool to generate the needed files
+to quickly create a Cobra application.
+
+Usage:
+  cobra-learn [command]
+
+Available Commands:
+  completion  Generate the autocompletion script for the specified shell
+  help        Help about any command
+  version     A brief description of your command
+
+Flags:
+  -h, --help     help for cobra-learn
+  -t, --toggle   Help message for toggle
+
+Use "cobra-learn [command] --help" for more information about a command.
  cobra-learn ./cobra-learn    
+A longer description that spans multiple lines and likely contains
+examples and usage of using your application. For example:
+
+Cobra is a CLI library for Go that empowers applications.
+This application is a tool to generate the needed files
+to quickly create a Cobra application.
+
+Usage:
+  cobra-learn [command]
+
+Available Commands:
+  completion  Generate the autocompletion script for the specified shell
+  help        Help about any command
+  version     A brief description of your command
+
+Flags:
+  -h, --help     help for cobra-learn
+  -t, --toggle   Help message for toggle
+
+Use "cobra-learn [command] --help" for more information about a command.

执行cobra-learn version

shell
  cobra-learn ./cobra-learn version
+version called
  cobra-learn ./cobra-learn version
+version called

可以看到调用命令执行的就是Run属性对应的函数

给命令增加flag

go
func init() {
+	rootCmd.AddCommand(versionCmd)
+
+	// Here you will define your flags and configuration settings.
+
+	// Cobra supports Persistent Flags which will work for this command
+	// and all subcommands, e.g.:
+	// versionCmd.PersistentFlags().String("foo", "", "A help for foo")
+
+	// Cobra supports local flags which will only run when this command
+	// is called directly, e.g.:
+	versionCmd.Flags().StringP("ver", "v", "1.0", "版本号")
+}
func init() {
+	rootCmd.AddCommand(versionCmd)
+
+	// Here you will define your flags and configuration settings.
+
+	// Cobra supports Persistent Flags which will work for this command
+	// and all subcommands, e.g.:
+	// versionCmd.PersistentFlags().String("foo", "", "A help for foo")
+
+	// Cobra supports local flags which will only run when this command
+	// is called directly, e.g.:
+	versionCmd.Flags().StringP("ver", "v", "1.0", "版本号")
+}
shell
  cobra-learn go build
+  cobra-learn ./cobra-learn version
+Usage:
+  cobra-learn version [flags]
+
+Flags:
+  -h, --help         help for version
+  -v, --ver string   版本号 (default "1.0")
  cobra-learn go build
+  cobra-learn ./cobra-learn version
+Usage:
+  cobra-learn version [flags]
+
+Flags:
+  -h, --help         help for version
+  -v, --ver string   版本号 (default "1.0")

在Run函数中获取flag

go
Run: func(cmd *cobra.Command, args []string) {
+		ver, _ := cmd.Flags().GetString("ver")
+		fmt.Println(ver)
+}
Run: func(cmd *cobra.Command, args []string) {
+		ver, _ := cmd.Flags().GetString("ver")
+		fmt.Println(ver)
+}
shell
  cobra-learn go build                       
+  cobra-learn ./cobra-learn version --ver 123
+123 # 使用name
+  cobra-learn ./cobra-learn version -v 1234  
+1234 # 使用shorthand
+  cobra-learn ./cobra-learn version        
+1.0 # 不带flag 使用默认值
  cobra-learn go build                       
+  cobra-learn ./cobra-learn version --ver 123
+123 # 使用name
+  cobra-learn ./cobra-learn version -v 1234  
+1234 # 使用shorthand
+  cobra-learn ./cobra-learn version        
+1.0 # 不带flag 使用默认值

修改命令配置

自定义usage输出

可以看到上面输出的./cobra-learn version的uage信息是默认的

shell
Usage:
+  cobra-learn version [flags]
Usage:
+  cobra-learn version [flags]

我们可以通过SetUsageTemplateSetUsageFunc自定义这一内容:

  • SetUsageTemplate
go
func init() {
+	rootCmd.AddCommand(versionCmd)
+	rootCmd.AddCommand(versionCmd)
+	versionCmd.SetUsageTemplate(
+		\`Usage: story version [options] <ver>\` + "\\n" +
+			\`版本号\` + "\\n" +
+			\`Options:\` + "\\n" +
+			\`  -h, --help   help for version\` + "\\n",
+	)
+}
func init() {
+	rootCmd.AddCommand(versionCmd)
+	rootCmd.AddCommand(versionCmd)
+	versionCmd.SetUsageTemplate(
+		\`Usage: story version [options] <ver>\` + "\\n" +
+			\`版本号\` + "\\n" +
+			\`Options:\` + "\\n" +
+			\`  -h, --help   help for version\` + "\\n",
+	)
+}
shell
  cobra-learn go build
+  cobra-learn ./cobra-learn version
+Error: accepts 1 arg(s), received 0
+Usage: story version [options] <ver>
+版本号
+Options:
+  -h, --help   help for version
  cobra-learn go build
+  cobra-learn ./cobra-learn version
+Error: accepts 1 arg(s), received 0
+Usage: story version [options] <ver>
+版本号
+Options:
+  -h, --help   help for version
  • SetUsageFunc
go
func init() {
+	rootCmd.AddCommand(versionCmd)
+	versionCmd.SetUsageFunc(func(cmd *cobra.Command) error {
+		fmt.Println("Usage: story version")
+		return nil
+	})
+}
func init() {
+	rootCmd.AddCommand(versionCmd)
+	versionCmd.SetUsageFunc(func(cmd *cobra.Command) error {
+		fmt.Println("Usage: story version")
+		return nil
+	})
+}
go
➜  cobra-learn go build             
+➜  cobra-learn ./cobra-learn version
+Usage: story version
➜  cobra-learn go build             
+➜  cobra-learn ./cobra-learn version
+Usage: story version

限制arg参数

  • Number of arguments:

    • NoArgs - report an error if there are any positional args.
    • ArbitraryArgs - accept any number of args.
    • MinimumNArgs(int) - report an error if less than N positional args are provided.
    • MaximumNArgs(int) - report an error if more than N positional args are provided.
    • ExactArgs(int) - report an error if there are not exactly N positional args.
    • RangeArgs(min, max) - report an error if the number of args is not between min and max.
  • Content of the arguments:

    • OnlyValidArgs - report an error if there are any positional args not specified in the ValidArgs field of Command, which can optionally be set to a list of valid values for positional args.

例:Args: cobra.ExactArgs(1)

执行不带参数时:

shell
  cobra-learn ./cobra-learn version
+Error: accepts 1 arg(s), received 0 # 提示需要提供一个参数
+Usage: story version
  cobra-learn ./cobra-learn version
+Error: accepts 1 arg(s), received 0 # 提示需要提供一个参数
+Usage: story version
`,48),e=[o];function c(t,r,E,y,i,F){return a(),n("div",null,e)}const u=s(p,[["render",c]]);export{C as __pageData,u as default}; diff --git a/assets/golang_cli_Cobra.md.d15fd694.lean.js b/assets/golang_cli_Cobra.md.d15fd694.lean.js new file mode 100644 index 000000000..3d30a4fbf --- /dev/null +++ b/assets/golang_cli_Cobra.md.d15fd694.lean.js @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const C=JSON.parse('{"title":"Cobra","description":"","frontmatter":{},"headers":[],"relativePath":"golang/cli/Cobra.md","filePath":"golang/cli/Cobra.md","lastUpdated":1708510102000}'),p={name:"golang/cli/Cobra.md"},o=l("",48),e=[o];function c(t,r,E,y,i,F){return a(),n("div",null,e)}const u=s(p,[["render",c]]);export{C as __pageData,u as default}; diff --git a/assets/golang_index.md.56aeaf33.js b/assets/golang_index.md.56aeaf33.js new file mode 100644 index 000000000..ef5bbda7a --- /dev/null +++ b/assets/golang_index.md.56aeaf33.js @@ -0,0 +1 @@ +import{_ as a,o as n,c as l,k as e,a as t}from"./chunks/framework.dc1f4608.js";const f=JSON.parse('{"title":"Golang","description":"","frontmatter":{},"headers":[],"relativePath":"golang/index.md","filePath":"golang/index.md","lastUpdated":1708510102000}'),o={name:"golang/index.md"},s=e("h1",{id:"golang",tabindex:"-1"},[t("Golang "),e("a",{class:"header-anchor",href:"#golang","aria-label":'Permalink to "Golang"'},"​")],-1),i=e("ul",null,[e("li",null,"基础"),e("li",null,"Web"),e("li",null,"Cli"),e("li",null,"工具")],-1),r=[s,i];function d(c,_,g,p,h,u){return n(),l("div",null,r)}const x=a(o,[["render",d]]);export{f as __pageData,x as default}; diff --git a/assets/golang_index.md.56aeaf33.lean.js b/assets/golang_index.md.56aeaf33.lean.js new file mode 100644 index 000000000..ef5bbda7a --- /dev/null +++ b/assets/golang_index.md.56aeaf33.lean.js @@ -0,0 +1 @@ +import{_ as a,o as n,c as l,k as e,a as t}from"./chunks/framework.dc1f4608.js";const f=JSON.parse('{"title":"Golang","description":"","frontmatter":{},"headers":[],"relativePath":"golang/index.md","filePath":"golang/index.md","lastUpdated":1708510102000}'),o={name:"golang/index.md"},s=e("h1",{id:"golang",tabindex:"-1"},[t("Golang "),e("a",{class:"header-anchor",href:"#golang","aria-label":'Permalink to "Golang"'},"​")],-1),i=e("ul",null,[e("li",null,"基础"),e("li",null,"Web"),e("li",null,"Cli"),e("li",null,"工具")],-1),r=[s,i];function d(c,_,g,p,h,u){return n(),l("div",null,r)}const x=a(o,[["render",d]]);export{f as __pageData,x as default}; diff --git a/assets/golang_tools_redis-cleaner.md.c2849590.js b/assets/golang_tools_redis-cleaner.md.c2849590.js new file mode 100644 index 000000000..d25199ec2 --- /dev/null +++ b/assets/golang_tools_redis-cleaner.md.c2849590.js @@ -0,0 +1,143 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"Redis-cleaner","description":"","frontmatter":{},"headers":[],"relativePath":"golang/tools/redis-cleaner.md","filePath":"golang/tools/redis-cleaner.md","lastUpdated":1708510102000}'),p={name:"golang/tools/redis-cleaner.md"},o=l(`

Redis-cleaner

go
package main
+
+import (
+	"context"
+	"fmt"
+	"github.com/redis/go-redis/v9"
+	"log"
+)
+
+func main() {
+	ctx := context.Background()
+	// 创建Redis客户端
+	client := redis.NewClient(&redis.Options{
+		Addr: "localhost:6379",
+		DB:   1,
+	})
+
+	// 定义匹配模式和批量处理大小
+	matchPattern := "*"
+	batchSize := 1000
+
+	// 设置游标初始值和删除计数器
+	startCursor := uint64(0)
+	keysDeleted := 0
+	memSaved := 0
+
+	for {
+		// 扫描Redis中的key
+		keys, cursor, err := client.Scan(ctx, startCursor, matchPattern, int64(batchSize)).Result()
+
+		if err != nil {
+			log.Fatal(err)
+		}
+
+		// 检查每个key的过期时间并删除符合条件的键
+		for _, key := range keys {
+			ttl, err := client.TTL(ctx, key).Result()
+			if err != nil {
+				log.Fatal(err)
+			}
+
+			// 如果过期时间大于15年,则删除该键
+			if ttl.Hours() > 24*365*10 {
+				mem, err := client.MemoryUsage(ctx, key).Result()
+				if err != nil {
+					log.Fatal(err)
+				}
+				err = client.Del(ctx, key).Err()
+				if err != nil {
+					log.Fatal(err)
+				}
+				memSaved += int(mem)
+				keysDeleted++
+			}
+		}
+
+		// 如果游标为0,则表示已完成遍历
+		if cursor == 0 {
+			break
+		}
+		startCursor = cursor
+	}
+
+	fmt.Printf("已删除 %d 个过期时间大于10年的键\\n", keysDeleted)
+	fmt.Printf("已释放 %d MB内存\\n", memSaved/1024/1024)
+
+	// 关闭Redis客户端连接
+	err := client.Close()
+	if err != nil {
+		log.Fatal(err)
+	}
+}
package main
+
+import (
+	"context"
+	"fmt"
+	"github.com/redis/go-redis/v9"
+	"log"
+)
+
+func main() {
+	ctx := context.Background()
+	// 创建Redis客户端
+	client := redis.NewClient(&redis.Options{
+		Addr: "localhost:6379",
+		DB:   1,
+	})
+
+	// 定义匹配模式和批量处理大小
+	matchPattern := "*"
+	batchSize := 1000
+
+	// 设置游标初始值和删除计数器
+	startCursor := uint64(0)
+	keysDeleted := 0
+	memSaved := 0
+
+	for {
+		// 扫描Redis中的key
+		keys, cursor, err := client.Scan(ctx, startCursor, matchPattern, int64(batchSize)).Result()
+
+		if err != nil {
+			log.Fatal(err)
+		}
+
+		// 检查每个key的过期时间并删除符合条件的键
+		for _, key := range keys {
+			ttl, err := client.TTL(ctx, key).Result()
+			if err != nil {
+				log.Fatal(err)
+			}
+
+			// 如果过期时间大于15年,则删除该键
+			if ttl.Hours() > 24*365*10 {
+				mem, err := client.MemoryUsage(ctx, key).Result()
+				if err != nil {
+					log.Fatal(err)
+				}
+				err = client.Del(ctx, key).Err()
+				if err != nil {
+					log.Fatal(err)
+				}
+				memSaved += int(mem)
+				keysDeleted++
+			}
+		}
+
+		// 如果游标为0,则表示已完成遍历
+		if cursor == 0 {
+			break
+		}
+		startCursor = cursor
+	}
+
+	fmt.Printf("已删除 %d 个过期时间大于10年的键\\n", keysDeleted)
+	fmt.Printf("已释放 %d MB内存\\n", memSaved/1024/1024)
+
+	// 关闭Redis客户端连接
+	err := client.Close()
+	if err != nil {
+		log.Fatal(err)
+	}
+}
`,2),t=[o];function e(c,r,E,y,i,F){return n(),a("div",null,t)}const u=s(p,[["render",e]]);export{d as __pageData,u as default}; diff --git a/assets/golang_tools_redis-cleaner.md.c2849590.lean.js b/assets/golang_tools_redis-cleaner.md.c2849590.lean.js new file mode 100644 index 000000000..c9961dfb5 --- /dev/null +++ b/assets/golang_tools_redis-cleaner.md.c2849590.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"Redis-cleaner","description":"","frontmatter":{},"headers":[],"relativePath":"golang/tools/redis-cleaner.md","filePath":"golang/tools/redis-cleaner.md","lastUpdated":1708510102000}'),p={name:"golang/tools/redis-cleaner.md"},o=l("",2),t=[o];function e(c,r,E,y,i,F){return n(),a("div",null,t)}const u=s(p,[["render",e]]);export{d as __pageData,u as default}; diff --git a/assets/golang_web_Gin.md.a83e3304.js b/assets/golang_web_Gin.md.a83e3304.js new file mode 100644 index 000000000..35232cd49 --- /dev/null +++ b/assets/golang_web_Gin.md.a83e3304.js @@ -0,0 +1,491 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const q=JSON.parse('{"title":"Gin","description":"","frontmatter":{},"headers":[],"relativePath":"golang/web/Gin.md","filePath":"golang/web/Gin.md","lastUpdated":1708510102000}'),p={name:"golang/web/Gin.md"},o=l(`

Gin

Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance -- up to 40 times faster. If you need smashing performance, get yourself some Gin.

Gin安装和基本使用

go get -u github.com/gin-gonic/gin

go
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/thinkerou/favicon"
+)
+
+func main() {
+  // 创建服务
+	ginServer := gin.Default()
+	ginServer.Use(favicon.New("./favicon.ico"))
+	ginServer.GET("/", func(c *gin.Context) {
+		c.String(200, "Hello World!")
+	})
+	ginServer.POST("/post", func(c *gin.Context) {
+		c.JSON(200, gin.H{
+			"message": "POST Data",
+		})
+	})
+
+	_ = ginServer.Run(":8080")
+
+}
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/thinkerou/favicon"
+)
+
+func main() {
+  // 创建服务
+	ginServer := gin.Default()
+	ginServer.Use(favicon.New("./favicon.ico"))
+	ginServer.GET("/", func(c *gin.Context) {
+		c.String(200, "Hello World!")
+	})
+	ginServer.POST("/post", func(c *gin.Context) {
+		c.JSON(200, gin.H{
+			"message": "POST Data",
+		})
+	})
+
+	_ = ginServer.Run(":8080")
+
+}
  • curl -X GET http://localhost:8080

    • Hello World!
  • curl -X POST http://localhost:8080/post

    • {"message":"POST Data"}

返回一个静态页

go
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/thinkerou/favicon"
+)
+
+func main() {
+  // 创建服务
+	ginServer := gin.Default()
+  // 设置favicon
+	ginServer.Use(favicon.New("./favicon.ico"))
+
+	// 加载静态页
+	ginServer.LoadHTMLGlob("templates/*")
+	// 响应页面给前端
+	ginServer.GET("/index", func(context *gin.Context) {
+		context.HTML(200, "index.html", gin.H{
+			"title": "Main website",
+		})
+	})
+	_ = ginServer.Run(":8080")
+}
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/thinkerou/favicon"
+)
+
+func main() {
+  // 创建服务
+	ginServer := gin.Default()
+  // 设置favicon
+	ginServer.Use(favicon.New("./favicon.ico"))
+
+	// 加载静态页
+	ginServer.LoadHTMLGlob("templates/*")
+	// 响应页面给前端
+	ginServer.GET("/index", func(context *gin.Context) {
+		context.HTML(200, "index.html", gin.H{
+			"title": "Main website",
+		})
+	})
+	_ = ginServer.Run(":8080")
+}

访问localhost:8080localhost:8080/index

静态页

加载资源文件

go
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/thinkerou/favicon"
+)
+
+func main() {
+  // 创建服务
+	ginServer := gin.Default()
+  // 设置favicon
+	ginServer.Use(favicon.New("./favicon.ico"))
+
+	// 加载静态页
+	ginServer.LoadHTMLGlob("templates/*")
+	// 响应页面给前端
+	ginServer.GET("/index", func(context *gin.Context) {
+		context.HTML(200, "index.html", gin.H{
+			"title": "Main website",
+		})
+	})
+	_ = ginServer.Run(":8080")
+}
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/thinkerou/favicon"
+)
+
+func main() {
+  // 创建服务
+	ginServer := gin.Default()
+  // 设置favicon
+	ginServer.Use(favicon.New("./favicon.ico"))
+
+	// 加载静态页
+	ginServer.LoadHTMLGlob("templates/*")
+	// 响应页面给前端
+	ginServer.GET("/index", func(context *gin.Context) {
+		context.HTML(200, "index.html", gin.H{
+			"title": "Main website",
+		})
+	})
+	_ = ginServer.Run(":8080")
+}

加载资源文件

Restful API

Query参数

go
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func main() {
+	ginServer := gin.Default()
+
+
+	ginServer.GET("/user/info", func(context *gin.Context) {
+		userId := context.Query("userId")
+		userName := context.Query("userName")
+		context.JSON(http.StatusOK, gin.H{
+			"userId":   userId,
+			"userName": userName,
+		})
+	})
+
+	_ = ginServer.Run(":8080")
+}
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func main() {
+	ginServer := gin.Default()
+
+
+	ginServer.GET("/user/info", func(context *gin.Context) {
+		userId := context.Query("userId")
+		userName := context.Query("userName")
+		context.JSON(http.StatusOK, gin.H{
+			"userId":   userId,
+			"userName": userName,
+		})
+	})
+
+	_ = ginServer.Run(":8080")
+}
  • curl -X GET 'http://localhost:8080/user/info?userId=123&userName=小明'
    • {"userId":"123","userName":"小明"}

Body参数

go
package main
+
+import (
+	"encoding/json"
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func main() {
+	ginServer := gin.Default()
+
+	ginServer.POST("/user", func(context *gin.Context) {
+    // request body []byte, err
+		body, _ := context.GetRawData()
+    // 包装为map类型
+		var m map[string]interface{}
+		_ = json.Unmarshal(body, &m)
+
+		context.JSON(http.StatusOK, m)
+	})
+
+	_ = ginServer.Run(":8080")
+}
package main
+
+import (
+	"encoding/json"
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func main() {
+	ginServer := gin.Default()
+
+	ginServer.POST("/user", func(context *gin.Context) {
+    // request body []byte, err
+		body, _ := context.GetRawData()
+    // 包装为map类型
+		var m map[string]interface{}
+		_ = json.Unmarshal(body, &m)
+
+		context.JSON(http.StatusOK, m)
+	})
+
+	_ = ginServer.Run(":8080")
+}
  • curl -X POST 'http://127.0.0.1:8080/user' --header 'Content-Type: application/json' --data '{"userName": "张三"}'
    • {"userName":"张三"}

表单参数

html
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Title</title>
+    <link rel="stylesheet" href="/static/base.css">
+</head>
+<body>
+<h1>Hello World!</h1>
+
+<form action="/user/add" method="post">
+    <input type="text" name="username">
+    <input type="password" name="password">
+
+    <button type="submit">提交</button>
+</form>
+<script src="/static/base.js"></script>
+</body>
+</html>
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Title</title>
+    <link rel="stylesheet" href="/static/base.css">
+</head>
+<body>
+<h1>Hello World!</h1>
+
+<form action="/user/add" method="post">
+    <input type="text" name="username">
+    <input type="password" name="password">
+
+    <button type="submit">提交</button>
+</form>
+<script src="/static/base.js"></script>
+</body>
+</html>
go
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func main() {
+	ginServer := gin.Default()
+
+	ginServer.LoadHTMLGlob("templates/*")
+
+	ginServer.GET("/index", func(context *gin.Context) {
+		context.HTML(http.StatusOK, "index.html", gin.H{
+			"title": "Hello World",
+		})
+	})
+
+	ginServer.POST("/user/add", func(context *gin.Context) {
+		username := context.PostForm("username")
+		password := context.PostForm("password")
+
+		context.JSON(http.StatusOK, gin.H{
+			"msg": "success",
+			"data": gin.H{
+				"username": username,
+				"password": password,
+			},
+		})
+	})
+
+	_ = ginServer.Run(":8080")
+}
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func main() {
+	ginServer := gin.Default()
+
+	ginServer.LoadHTMLGlob("templates/*")
+
+	ginServer.GET("/index", func(context *gin.Context) {
+		context.HTML(http.StatusOK, "index.html", gin.H{
+			"title": "Hello World",
+		})
+	})
+
+	ginServer.POST("/user/add", func(context *gin.Context) {
+		username := context.PostForm("username")
+		password := context.PostForm("password")
+
+		context.JSON(http.StatusOK, gin.H{
+			"msg": "success",
+			"data": gin.H{
+				"username": username,
+				"password": password,
+			},
+		})
+	})
+
+	_ = ginServer.Run(":8080")
+}

路由

go
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func main() {
+	ginServer := gin.Default()
+
+	ginServer.LoadHTMLGlob("templates/*")
+  // 重定向到首页
+	ginServer.GET("/", func(context *gin.Context) {
+		context.Redirect(http.StatusMovedPermanently, "/index")
+	})
+
+	ginServer.GET("/index", func(context *gin.Context) {
+		context.HTML(http.StatusOK, "index.html", gin.H{
+			"title": "Hello World",
+		})
+	})
+  // 404页面
+  ginServer.NoRoute(func(context *gin.Context) {
+		context.HTML(http.StatusNotFound, "404.html", gin.H{
+			"title": "404",
+		})
+	})
+
+	_ = ginServer.Run(":8080")
+}
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func main() {
+	ginServer := gin.Default()
+
+	ginServer.LoadHTMLGlob("templates/*")
+  // 重定向到首页
+	ginServer.GET("/", func(context *gin.Context) {
+		context.Redirect(http.StatusMovedPermanently, "/index")
+	})
+
+	ginServer.GET("/index", func(context *gin.Context) {
+		context.HTML(http.StatusOK, "index.html", gin.H{
+			"title": "Hello World",
+		})
+	})
+  // 404页面
+  ginServer.NoRoute(func(context *gin.Context) {
+		context.HTML(http.StatusNotFound, "404.html", gin.H{
+			"title": "404",
+		})
+	})
+
+	_ = ginServer.Run(":8080")
+}

路由组

go
package main
+
+import (
+	"github.com/gin-gonic/gin"
+)
+
+func main() {
+	ginServer := gin.Default()
+
+	userGroup := ginServer.Group("/user")
+	{
+		userGroup.GET("/get", func(c *gin.Context) {
+			c.JSON(200, gin.H{
+				"message": "get user",
+			})
+		})
+		userGroup.POST("/post", func(c *gin.Context) {
+			c.JSON(200, gin.H{
+				"message": "post user",
+			})
+		})
+	}
+
+	_ = ginServer.Run(":8080")
+}
package main
+
+import (
+	"github.com/gin-gonic/gin"
+)
+
+func main() {
+	ginServer := gin.Default()
+
+	userGroup := ginServer.Group("/user")
+	{
+		userGroup.GET("/get", func(c *gin.Context) {
+			c.JSON(200, gin.H{
+				"message": "get user",
+			})
+		})
+		userGroup.POST("/post", func(c *gin.Context) {
+			c.JSON(200, gin.H{
+				"message": "post user",
+			})
+		})
+	}
+
+	_ = ginServer.Run(":8080")
+}

自定义中间件 拦截器

go
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"log"
+)
+
+func myHandler() gin.HandlerFunc {
+	return func(context *gin.Context) {
+		// do something
+		context.Set("name", "zhangsan")
+		context.Next() // 放行
+		// context.Abort() 阻止
+	}
+}
+
+func main() {
+	ginServer := gin.Default()
+
+	userGroup := ginServer.Group("/user")
+	{
+		userGroup.GET("/get", myHandler(), func(c *gin.Context) {
+			// 获取拦截器里设置的值
+			name := c.MustGet("name").(string)
+			log.Println(name)
+			c.JSON(200, gin.H{
+				"message": "get user",
+			})
+		})
+	}
+
+	_ = ginServer.Run(":8080")
+}
+// 2023/07/01 21:36:53 zhangsan
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"log"
+)
+
+func myHandler() gin.HandlerFunc {
+	return func(context *gin.Context) {
+		// do something
+		context.Set("name", "zhangsan")
+		context.Next() // 放行
+		// context.Abort() 阻止
+	}
+}
+
+func main() {
+	ginServer := gin.Default()
+
+	userGroup := ginServer.Group("/user")
+	{
+		userGroup.GET("/get", myHandler(), func(c *gin.Context) {
+			// 获取拦截器里设置的值
+			name := c.MustGet("name").(string)
+			log.Println(name)
+			c.JSON(200, gin.H{
+				"message": "get user",
+			})
+		})
+	}
+
+	_ = ginServer.Run(":8080")
+}
+// 2023/07/01 21:36:53 zhangsan
`,29),t=[o];function e(c,r,E,y,i,u){return n(),a("div",null,t)}const g=s(p,[["render",e]]);export{q as __pageData,g as default}; diff --git a/assets/golang_web_Gin.md.a83e3304.lean.js b/assets/golang_web_Gin.md.a83e3304.lean.js new file mode 100644 index 000000000..9651b349e --- /dev/null +++ b/assets/golang_web_Gin.md.a83e3304.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const q=JSON.parse('{"title":"Gin","description":"","frontmatter":{},"headers":[],"relativePath":"golang/web/Gin.md","filePath":"golang/web/Gin.md","lastUpdated":1708510102000}'),p={name:"golang/web/Gin.md"},o=l("",29),t=[o];function e(c,r,E,y,i,u){return n(),a("div",null,t)}const g=s(p,[["render",e]]);export{q as __pageData,g as default}; diff --git a/assets/index.md.a91419bd.js b/assets/index.md.a91419bd.js new file mode 100644 index 000000000..d5b9ccbba --- /dev/null +++ b/assets/index.md.a91419bd.js @@ -0,0 +1 @@ +import{_ as t,o as e,c as a}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"故事","text":"Document","tagline":"🚀 热爱,是所有的理由和答案","actions":[{"theme":"brand","text":"进入博客 →","link":"/java/"},{"theme":"alt","text":"GitHub","link":"https://github.com/storyxc"}],"image":{"src":"/logo.png","alt":"Story"}},"features":[{"title":"⌨️ Coding","details":"编程让我解构世界。"},{"title":"⚽️ Football","details":"白云偶尔会遮住蓝天,但蓝天永远在白云之上。"},{"title":"🎸 Guitar","details":"还记得,年少时的梦吗?"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":1708510102000}'),n={name:"index.md"};function i(o,s,l,r,d,c){return e(),a("div")}const p=t(n,[["render",i]]);export{m as __pageData,p as default}; diff --git a/assets/index.md.a91419bd.lean.js b/assets/index.md.a91419bd.lean.js new file mode 100644 index 000000000..d5b9ccbba --- /dev/null +++ b/assets/index.md.a91419bd.lean.js @@ -0,0 +1 @@ +import{_ as t,o as e,c as a}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"故事","text":"Document","tagline":"🚀 热爱,是所有的理由和答案","actions":[{"theme":"brand","text":"进入博客 →","link":"/java/"},{"theme":"alt","text":"GitHub","link":"https://github.com/storyxc"}],"image":{"src":"/logo.png","alt":"Story"}},"features":[{"title":"⌨️ Coding","details":"编程让我解构世界。"},{"title":"⚽️ Football","details":"白云偶尔会遮住蓝天,但蓝天永远在白云之上。"},{"title":"🎸 Guitar","details":"还记得,年少时的梦吗?"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":1708510102000}'),n={name:"index.md"};function i(o,s,l,r,d,c){return e(),a("div")}const p=t(n,[["render",i]]);export{m as __pageData,p as default}; diff --git a/assets/inter-italic-cyrillic-ext.33bd5a8e.woff2 b/assets/inter-italic-cyrillic-ext.33bd5a8e.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..2a687296748f6b8bc8076cd11bde49cd27e4442b GIT binary patch literal 28332 zcmV(^K-Ir@Pew8T0RR910B)=R5dZ)H0L(-H0B%750|eaw00000000000000000000 z0000QgDD%9791)+NLE2ohdBmdKT}jeRDl`*gBUMt3W0+R>k}}6+I9gp0we>63JZfs z00bZfg$M^A8&tgo+lIZ{0W$kf`dwxsbvsBZKijgA2%9xXMMwW9BpqW2)F+!l*M37V zl9T{JHdk?)M!T60nkxGasf@PS$3btkm4;ibH5~*Z*uTsmJGUKxX9cyg+F)d-5ys4C zo7|FZ`ph?caYdg&{|^%(5eV_PgnKGlxbGk&;@QKi9rFvf2ykadkugvB=bv=iyMMk$ zBY7+a5GAr4D>kv^F3Pf`OSoyhikEeWmv~9(oDIp6)-@vO+gl-}bh(56@L;!pH{2TT zIOp!%>5R+DSWy{w>@s%z%*G*i5$ug1FAp@aNHgepU=2^W?cc^RPQ37bnbQu{L-xl)y`!#La!bOvoPMoxG z(!!|=7oITjgkwyl&T2DBcbsl=3a50PM{xuZMi|BC7=>&cVHSH4Ya!&qB80t`*D}}3 z`}&jTwP&xd(tnwI*4M%FsVgNJ76J%GP2N4ADlq+3=~DETeStFR0FZaB_jw3ckIPK5 z`;UwJRvMJRS2&Vxp&I27!Y~h>|K6$HS@;S8T&H{1H zelfBi^#8vfzd84dLnu@l4ca%u)S^X7BWn~bkwwTv(YI&*QD-XSu8ViQ;qDt_(A>&U z^4xoV?`61~?puQ!i;krj+b4{06eEmq6h{!nD2{Lx<51xU-#?;)LXGpo38GLXQ8~q@ za(=|6dQ_zf<@vAuKHjyvZ{LUW{|}LikSRYLR8evAo_P~QNvMDvATH znox36Duh0!eB}lajkZ~yyJImL1K?AGZIKwvZ_1`1J4dCns(MBe$gm+td?8lPjD$92 zHiYoJ%iYp|Wjxl6?XfL$T6WCpuolbmEY`s+7Mza7a!6WRj+xB~9qO$1+8N35Cdbeb zbyosCj6F`|T`vGgnxYGo|21;cUv~;O3xwtgKbS=WR?q^?dAOU;f3MTF?@BzWm9@s9DrlFs?>c$@j9$lUv|> zQg+A+L08w+=oPL;u&}CrG}mkQ3IN7^A<3HAw-cD%KT7GZ>C|xjc5QqSQ&K(v32^j^23>tYX z9Ey~w;NelHPLmcL`gYn=J|L_>I0Q~yxbxt}+kX2U;NL(%gn$SG5fMa*7Asb~B&kxR zJ0x3I1Y1P%_YuCZPc1EaLPt1W^?(C1fcTxWB$^RaAq4^g z@}DZQNWESVKxs;JK`-ls58)9QwnG31q*M5A?gzWuK>^ixaA+&!?LVwgKmlkP5di)- zI3+nGxuDceEwv9`MQ&?tdkYR6o};HYmOmnmx+||d_hhmYMc0FMUp9Uo$i!j)vK4$y zZaeqYW2xP0JZsV-`MYJZLsPzcP(sd%*5AW5U%Ajh{X2;qvF}k4r)SSpAf=>mU|qe> zPFfaO>Syt!6xxG4O>LrDO`6*bB5m8Rj-0o)ReR@^a8FuIxzG97_2`1swL+EH?hk9Q zZLu6+f9`p*3q~M23H9`edMulW1?jZ~Y6)+?HsMGo(ulUa#5^+Uep_2D#cvzv$gAF7 zkf+fqQkz!OxHQ#2Y7KTwI$yM#Nl2PRdQNCadaL^)+r))EuS`03G`O4HKKl73UE|+B z5!ZPwJ^xugdmK_H!eC)q;x7(*fgbEWb;$q!x7vd#I3@Q0C{V#Lm z%9Br|)G6goJ438SoqEk$v})I(QKP~- zP5G`1Nos^MDfWno^cHV%FL}O^*Zxr_6%@9fIScqpwMa!P{0C`mLO&~dpg|S zFbu9upcw+Lb;9@yg}_&s(4!>-2p|F`m_xz?5Q2Fls4I>l3=t4WKm-rHAPI2CsC9#< z?1y9Z8DaVb^#CZqfQ-GfGBL@*yPV1WnnP8gu>AK!-Fr=>0cA&YjUi2(3IM+FoB z699Aqj>TF8NWdVY5CDT!&;SH}XzPQNk`Q7hTKiI*&r8q2@avm-N`UXP0JzgqK%Iw7 zmDXW^UNgxj_Mfa79#lwczNlYAoo!D~IQFZKhhF{db?o(1umAM+Zy#R!_}b^K4>dpX z@>6c0^#%V6ftP}BHAehzZGG$M_twXPp9Q}Pe0OKk|6SmRTSvGlJS9vSzQ6aoT!Go8 z0oSiwzjMLrTVJNE-w1fa4K*Enwg1ceK>iSbj{^G%sJ}yl(wJ?KJ_YipK>r+gA5q-r z0skT}Uj*)ph<*a-uYvqc-0}s0--hsafczece}F9|V15F?F97`~0RIN?zk{~70NcBH z1lae(mf3;3aL;p0;A~AR(1b0sF;BD@Z7XAlR?bLU|2Lx6DI;ik4c3WF2E z(F?iLn%zf1PgRD%r2}_*Xk4}#Qx}yE2joHnc9Bh=#s(WqVyglx7CJgEjj364SJQ$2 z(*y(0>r@l&RUT=Axg#K&d83?oL~0frV6rdXA&hx4LX+p;`c9c0OT)aQ^NyW3F*g$|LW%FGIW8gTuGoDu^dLn*zZlyjgKG|2|04q(cR~s^5*KHy{sbxiY*B|bLJLHF2f z(O1*<7VeKAG7B4hf)MUwNH&h$pK*xR}6|SkJr^=wShMaRprR(mSbk`-LrabTvO^9h3SoB#s zpxkBdvRMh#>Ls!;w!)D_z==nYz(WpCxlZX<OIAgYOg#_(m6ZCg(ffgPU(z$k;y1sr-CPjBPICUkhM%`zWCGOm(!g*;sh1DI}^@yISa z?Pg*RQ^mSxOSuNm@cC1eGG|VJ0Qzo`mB3hnurM(7dxUqBrtQqMMcN(uLoggCcMdvf z#zYoP(naH7r~8SYkRFv+Krgw;lvR(g^E)T2n6{8vLdJfe%nMHNVJtMVuB6e4xKoBJ zzTd44r3>Iu7j{)2v(Ca)B6djzqhLS(W`2n|(Js1b~LGrr=)yeQ~5hxgg+anlQ+#`CL z#_0ROw2`YBFxk~+M@>UWhSFP9)HGhvmP3#pvUEbg;>ufs!s?xjmk!-h>);5Fz7Rx1>lIakMr9bZ1A3%z zYq@zdv#%xzxo!YSLh&zNV}{B@ZfOgYt2C4Sc20d!^7^Ux^7S(N1}=6)L~~yVS`)DmH|^gZ{~a2*vqmqD2YVWk$NBV|IZ0>1DlnSQaYG zU<|)LfaV>TVIS*;wc99_oaDC9RyYAE+o??9l!ZEP^UrVgi`bJZ6h9rHa?%nH{f>%D z7DZdf;z*-T8iHVtym(*P%L66Fi(1v68LlHiuPTP*Gx2)(3t6NkE2W0@DU1V9ti_h@ zoK;p>C=%0%Wi+~2iI-AiakVZZsW!M3Z=9&=K^>GnQ8by53Nj4;A*2O1`8S|E=KDcG zvK>)FW?kLWq8pi$K2dpv@KUeQ+O-?ukx9UTFYe3}DO93m$!gsPV{sqHIz>=w*i&O> z4g>O0@J3U2Fvp@vrae@4Q0c7wFdfyfu&Oy4@{wueX@tt`@^Z7MNcgW*%tC9z&Rs}y z4Fh_TzHM3WEUKjiwZAk5ZE26B9Xdf6%wbVSBH9tK8o!9eH?;1DJOGeiyU-(P71ke` zGHQT|v7lqQ*RBN1Fb0yhJB$n8d27J0ra3|YcdUKwx{*h8(Dou+pq0tO5$OT}{ZzLq z)5=ENXdd2#M@tV=a(8H?Qs0VfPx3S};&JWICGxC lkKL(qGBcDXXZHb~GENV%oa5o3p`t5VO%nZMDK^_*W?ZTf^ae0dT}7-GX0Etc~C)9 zsCkr)Nr(B$S=a_?mf$3rHl-+o8B&r%!4Rd^%5v-6aOc*ak>|>lE!rrvtXH~a4^BQI zNUQkeRMGHIDqsA-KrSmu=(JHzSr65^ubmoSLIi`$ghfGqN&&Fu-Ji#tvmz|xPv@1Z^&ckpgFqS`X<{=f8fK~0*KrgVTwAYS&CTCkK9!48~ zOFFh~+PM4#=Fwe-mXB~0-u)QKjgN14sKzci7+|sG))q+{Q1xTbO`J8t8<6HsD!E19 zez6Q*T9CTuD&4hZ^c}*x^m7rb7d=bPJ==8tFVcOH_^Qr>h~kved2>FZn-!f1d1*XLbr3}dt| zgdy86sMDv{14IA2K$2-ZIRkTNioOIH4}LNAa4EQLr`_Yqk$kKVZ`5Xq-q?NdStte! z*`4&W-e5F;X^JvlC36bQcN?&D_p##8li#aA?Aj^e=X4EUo#$Y0N?f zn#MG>H*$B)q3|`ua3! zroTVz&%n40J4^g<8)h$l%aKVpdW|F36V0nrs z=dV02gde$&2uz95%Q)FXl@9oEG4JNrV~#z(_A&kZE|e0PKW{LbO)pF*Zi??r+8Sfk z#yrktKAlg_j%)QK6``x-OBB^G2f-Og=Wqm-Agc_=lc6Hv{~}N#A%KGVh<{oW9hTS0 z7qsYC*a>u2R|d52nqy5`@vyb-JDh@{@wCBlyz80vR~BG8-fTKE>wTn_G)_P0FHFxG zm#A7`9=l)DnKe90pSmu93v2EIrM zwJ!?r{A1}8hP62#jPL-)ZnnVR98^>;CH09S}su7!p;u{;^w&^}qm}GeE3j-Si zGo6{6KjC62pF7>rKLC}n1#{JyP_9i`SSsM0&39C2D}b;0*R7-r>tLGY6BsYZ+h(I+ zi?Ah*%jBU2iJFf7SUXqqs=Rx=WDB^9%F-K32>(q+258W=kvjw-=3pDY`xkn+dvh^w zR7Jjy=BG#R5~lNZ!$jLfnSeO~c=Cv5x!uU|K(t1(rc#ZeXqh&46Ov_(LiC+m9jTsZ zSueGS$825q0^4=q!`9V5oa;ZK+OVS%g91fW(@71~G4bS1X6pMq2ZltLx$VgLZ# zMUl^KawI^>7Rd44N!y3rm2^h{O9Yevf?f%f^x;5Z1OV~`VA6*NbipHHJU!?eGoapzbZFyn|5+Jz9Rl_PqO{> zZH-Z`Pgz$m!tWC9H0}$TFy>@2d*X3SZV%C)J5P>ail~_c_rI|1`ibFqAL`+{%qosD z@rtvle_tLk#M$b9N=>x=AKtTPbSizxso|2jGWTy{a6NmnS0+y|*`AVHymGAU1tp7C z$mnrdAI>cHtJBYkRCk&;GfEmej|$j3G$YD^wVhQKM;_zp6?4xS*)o3A5n?P+*iBXU z7%18F0K@F3)XAuW%5Rn5uZH^EpZU5C37?ms!)qI>kF6HhKp8!(i&n_1WheaV%rerr zZW8svI}4O-FwcI@9!UsN`C$3g@U~q~r$2XUxMZfDQZ|CTBX5h(E=b2QF0gLzdo>zV z>ECIxBGr_J!yd4P#b}{eB4Z0A8K0>(Y3s+YivP~^2JBG`e@23V4Y96>Ct8xn0*h{f zN9;M)z2qpBFUlWYA8;L-nq%HiiB|ch{Qixf^U(NgGjeagFjY}It5)8ime`(*!zK^^Q zUVXK`qj96izzWx*H@`I6d9C!Up-rri8HNXY+SRx%IgD?{H5Fb3!ixYQl%Flo7ilnt z=QL=;CnRcoo6^u0E+~k2OgjIBt%mRf&;iT|rGk>L)P_w|pl3<<-a~n>ea?20Q;CsI zEq4#pv~w55hbMgtTgmZq<#l}>(GMNVN~I63E7Z+@H7q)R=Vxscpq0zFzkFY>jT_)} z%CYY-dY=5TIR;#ZIv^@cLx)Q3(V?Z7vuJ6{2pfU0>WwPDilZR_bOE5s3NycVd?tXN zHFFnv!uW2}RMijjt~UKM?8Pyn!uCxK64a2OyRZeXUarJIm}_h$@N+~DHv?nUm0h{* zO^0JlHF3hFWdnby@e3o=wymJN=I|jR%WC*aYA%x`IPK%Z6G$4V+d=RX@1r#)7HR<4g|I_sD?) z3P|F*=x1EIZ(57oYH?Mx3Pl?#_a%fcQY@i0;ezKFhf;{juRQK{=k)^iqwyHn$Pyfk zpG$W84mGdp8FZ}+#1u5!F40O7q*xbi?LDH+*(Ja(2M={(EhbI@g;8(E01D|MlWWd#u=l+!gNZKFeqQ=c}GZmEBW63G{|vX?vZ2_tR=E zr+EjR`+-BC8w=lk`mWrYp#=^PjU*mQ*#CTMessrnQ-AK0F%QmocjwEy9-iFiW*Lf; z!v}a$>8BQe*RPYkns{}}_Kg=O%-`Ks{Tw~fQo&;NsnZlEL#@8v>Yei+d~c`tz${1Q zeKAYq-CmHrMk_ZBQ(kTKPQ4AJkIXE~S5;m*%bt|lpziq!E&4x02Ir<0%k z<7Wc3AE8yd4gvPKe#Q>18SmAPHsQJ^Mh?0>oU42^)Hqp}F}-o#P2yo|nGw5w*#Id@I_Y_7i>{XnkB+<{w%B3v5$-WYF zL9uLy2YdQa)tjCEOLzP;Nz6r&D`IBLw9CxQkgAl{k@X>QXl>s?GKmdS; zmnxG!(2-gW_Lp2iflL9Aq)jTn&7?pNHNG?Ny+V*Vj`fwggBxZHP~CrN$+u!hF!R#! z^xET!-&Vr%f1pAxm8WVe8|F_*WG6}`z%VUPedsbZue&Zm(ab|CK1idM=r4Qx>O)$5 zN7JXs)Y~A(*F@AzA|ZeTCulYiG*e$4!22FST($U2wf5=?8^+W=UN{$32=Lp1yO{g8 zR?RUQ`!cQa!Y_1Z4V^I@Mej>bX~z4qdOy8bEHBJD)kv65YRW1saJ_mpDQ<-(WRg@1 zXSyIV@XRF?AGZW{nZ-Oc9kFx);K~DSC2MRIV(5I#c2jlecpK9S<42YBlej7aK?((T zZUp9n-4^Crf}!P|1z<>S3@kY=j9hMS4c^3H-*7Z{(RnUtD@WvBBdA3PK++G)VmU?R zE&xz6RNl#cf2_twRvXRR`XQuU5(=ql$pb}%m8!;%Dv@Q^CjN+$gohlUqPD`|U`dNf za!Fe&pA~I1k7CP+ZZb)kBNM4b+BR5I_4zeF1kNO5uiF_&%r<6p#(4F-8}PRV#4#Z0 zBB3pE7u{Jc8ohGi2rONb?({*TqL5B8J5y{IQ2EOc>=Fr3Fw_zp9ol&Vc^1tS&RrvX zB6A+mjW7n+mH*2MK7@s3^heLsbY>XWGDP<-EV22n` z6&v=0=)1bP!ss0_b^_;}a`JIbYsBEpt2axYyCpmZvj=!Ap!)(zi=jt|*wd8+I|dQBf1f3c2E znU0`e%1Z4b`f~=CzD;cx2Kb%bHNS3va_{TaR4Q{PaSlFr#J66ZtfX^ibJ$nXqnn9= zGDpIOry~juLhCKe8i-qlN)TW^r3X)?=t*ac@(SYutLqHv`RSsfjz&{iRu7|3`Q^8e zsE5y{@bgg#%5RBr?CYH>-#bgWiEp=-Fl3U0wk{z?1d+p@eHt1DlGqQbJck!at#XdsU0;UUZ502R#mbmHyl&WrOIj8krz5hef+H3Qivr2s9mtUHtj{ZY4EGa{^D$yAGz1=2r)ssRcNh!WxP4 zsn;~*mxA|)b|kq(RL}E^gL1F}azfeBtJA(ifV+513CnmB8N((xD*uXq0X}Px>JeV~ zKikh{e*xB+n%r1$zVUqltqRAuF!n|_7N{3gWwfFC+M-q4;Bjq0GYpJWkFsv~8lA`$ z1=!>5dthLJ0!#7@&<8`eZa%nszJ%M$Pl7>}RobQ(FrbFrG)NWXau>bK!4T0A2-v(vIyV%=UNj25;c)X~j5-?6 zDJcej^G4{1QFm(6v_>2la!}`S}w0I(p5ck2b=o2}aw<;?gj!M6R;>;&8pq{DiX|NgczwK8M(I?6x2> z%o8iX+L#9U?Xfu7=b~J3*twUI#_VNuKQT_f?wis^PGDUbzCAZ0A6hZ~nelmu-IG?N znZ^dwY~twhruLBUsp&{LIook(r@Y$9j=a2;p0Ngpf77$-k4**`1o;Gg4>(!Bl6CMm zh~K#u*R@8aisR-JH{Fp<1HQ3A8Zb{?M9fyTa{2m{YM1>Q?m&o6HfCJim3~>L*ep%Q z)XdKuYi~4=L#rMuIqOoSbIhzo$KEU0!P`7IpC(+R9k29^j`zM^$6Zaib(=GRf{{2A zX?IgfpLDQ#{P}PE`Ru_Pze!p3_3w&8sN6?+Pww;(Rr;8v`C3h7%3ae8vd3$%aOe zxFhA{T*qy;ChK=5^~i7ejl~OD`;~7doao5QTZ;&Am~Q_YJ5|v3)Yq+Bo-Q-^huH>Z z&NLJ%%k8ye_4(H<6I!+WhKhb2@H+$a$C{xi`qvE?RsXt%G(*nQ?lfN zUUWE+Ue(_**-PUAywF_r!_=Vs*Rx$5foa|^%IKw>KStUmPe@Gz(_8GDL@%4Rum-ce!>T)+oGOe? zVIA0mfU=l0DS?OpXY5xWh0zA*4jR>gK%qN#@mlxcMGP%VP~o+W;{U{?)ten&{-FhG zcIV%;`!V6FAC%v&2K(Hb`L+$|AMm}?=5G$Fb^iF*LqAu>Xd3(3Y>@q<>8?-Zky6YK zI61ifaR`)cgPAE^b*3)rh~cb&d{NIHp< z+>v>Agxu12?=#)&LjeT8io3``cy=B8OC2qH^!A@)*ZOl8`8n+Q=uW5XGS?C5iq9}y|{QX&gT`Q!@z9Gz>Ib( z|3peaUn{HEqMX@txWmYNiMtrk+W`hJq(o`00V!8bl$dA@4FQ{9eT^$EzL(+gAN$tW zmBP)ApI@2O%|Hk5J;C`h;YD@CBW$6=F4k4^^vgOY%?|18GTR@LbV>B8v0_c3&e55` z8fn}e8^SWWens6}!U)$H9j&jv#uXdD;2I+wm8a8A1)q>0$7GVFGuPDfU)%qz)+Lq+ z@h9RU8m|Vx(&pN(xGOq!W>q>aXHS>WD*FbEur>m|*{-8HO=hDy>E?iJOQKNxeWU6a zgD9}mn)5WcyPnNDt$ZB}6<%9n zUtPBj);VC7qZ4eFWlpd+8t9`IysPhzF4j3{=B)#?5Hn(GY+q@iNK|@9QguYz#n!>W z{hv!>a>Dtt!oq^GE`Yd5$JN2s+BJVIDvrHeDk{t`g<#?%nl27jdtK-hXns^^%BTrm z9=5joJPgUVwf%BgDNPJu_5hK+H{0`3ebCyaVjp|F%hujK=#SD}st}2avh%M)pxug_ z{dZb)Vde9Vs6xuiSqFb4tbNj-J($zg#%rcdR#z9w)}RjNhtY0q^VcMV(M zJ%{(u!&G?6Z8u^sq~)y_hLRYAg|=g`k~J?u&X1uPo5!N>tGwOSpI+X~k4?hY9(le9 z-Mzp|f5?}i^7^-GIUor{3SN0yZw`~Ld)mWmld@}zGkqDDjX7Ev8WFK^KwZlU1pr)K<-I9hg=8krzW&`f6qe-&?tQ4vG zy03!0zRDP{SmlT=0S@-~GaQ>YrfJ=*rY|feEU3p6*;4wD>PEj1C1{8U(w2*d7mR~i zA+hwh6yaT?)|G-33B}Zkf_kHxFBHziNsJvID#MsH4~qGuXy$hvX7dFzyQG1-AF^~Cy3 zNM(VhH69lD^JQ!@ z&j2E)g81#>5(tl@K9Z(j0It&btzo{egx)#sb)+ydt0}qAHVY&Wrol?kDC#~oJ@(-<2IiFNhHUE7(aKxZ*Kma?zKB; z-@?0FzDY;~KuHL-fku{BO+y+$A|ckeH}CCXD^v!P?dKsCmY?O(Wod5nV4X3;3I5L~ z=F|b=6IV11^8uJ}N2rlw_+&UT)fTkKi;*1Zc3K`FY&bnf#8+j~!vFpcb8IYDe-`%T ze}Zu;gd{y-Vm!a4nmy-6_}Kqia-o`WKT&-AhTA{)xM6c zl$M{2f~hc|8XzT2B|zu@+Cy_~fSGY>e;tstKN~;^p%;Dd2y)d2Nu-R*RAn~Jk-wSX zs>(f>kNBy|mjy)80PtX$4{r6tV3T~w_Gw{X|DSQ7n*+eKe-6km6l1Mt{2$JIkmH>P z;BOBAxWi~{%-@IK?{gY2lMmd)90{pRQ(Q`20UKQp{?8YJr}76TS*m10eAmzIL5a`B zt^Vs?6A<*O6czwb6I!)90iXa7{hQBbk=R4&lKSCGf1xB7KKt=|{Dn2je#*RL>doiO z&4$_O44J3$c6fUfhw{0aWxuHYU;P0JWr`AiOT3%-SK!mY&m4VHncS4z8ax}k9GsuF zI`y|ybK{Z5j;(2-WG8MA55rUNY`hkJ7v3JfAD@grjsKa$BNr>W7LXo0j++8XUYIYYTPxgNP&au4M;=u&i5x)D8uevy8M{#M>u-d|oU-!1>0 zVZk`d_`-~2US$zj(+W6+vuqOEn}g%%aD<$B&R0bx#lIAD6iXEA6whz7+2*6io3vX-k*dDSyX}fT{ zc>BB>R82w6K&@YGSnaXeE444`Ks`c(qVbnTghsl?znV!}HJp#DRg1{0_g#JHYWFoF ziBwX)_=&~jHHWUb^w3DLtXLVTjBJa%5iwu4|GLkvU-pRSdjBK6kHi|a#eaRfq-5XZ zPbRx2e>eGO6aZ@g2ULLtGy$kA2(A?Bln(tP5+Q`9{WCzQuXc{3lot*gJM&a(XjenE zp;l`_E(`_|QYADraXU945O7d&3gyTEF^@+gd3<|h7oC-@7&rdQ!k*k-g(Q@-tvelc zLDXOkFvxWgOfD2bz0ugO;uK9Cg!6~xIM&OriXDHb?FY(#V0=E-CgOWaVtH|31<_PO zOpam3$NE*IoAHX>F%B3rABs4~Bjk#6L!PM{XHK;^ zmbQ5K*<(@(I5=6Ma;3O-;MWYYX$^lIj#uC__Uz0pO8v1>lX2N!HT?d{BBXkc=`5H@GOBmE@KA!*A7-tHatZZP5xX-R5p1RI; zi<>7GmOl1eUAAI#wzk6aZ|`9n{lTk$F_6T}D|J%ZUB-o`_8@v#vH84PT?xktvEo7K9 zMzzpr`Mph2Wim#fb^s~ESeS~WfV>?Pq3vvID|f$Rg?6HrC`G=o->y7%dM3f7{sJej zGty+~MJS%|bU7_c15A@O**L=+(7<(&u-mcq=rx6JBG8*G*gBd{fFATNB$8ta%TUJ# zILWnwfc9A3FJ5X5Ov*VcaKny6dFrE1PlEi6dE-X0$$Bv#B;2aFh=7D*eFV>-OHjpY z7`v@iA~E%s*#xZvMNm``3)9cC9WgHCXVGkDGybTVZm9cIsL!s_C{Kjry=mVK}8V!Efrc4L3L+0zHjJiUP%I zDG9yvwPfx#MvNJ%;5kvfPg73lR8pxjS?A36BQe&?&PNGt%8OW=n}N7P2@dAO5ERM^ zH4bplGrR=^9{8?CD>Kk}OV_w{_1IfQ0Wk=aqdUs4WGsN#Y+?09PT z8VKw=xFfXt-NnIWebMXFgoSyzfPUtp`?0e%E8#1j>bSV7Yu0yX6>N9}y%^e)L#mlt zf9eP6Pi9cuS_={DEN`qv|LR_^x7feU%E3j?hMJyxFMKaD!WER(+7MGoP{}w}7A2n? z@`!XxsHqEIlbV?>6H5-1bvPimO77eu~p;&n8))_br)}tO;BPo3%C-PkV!5qm> zn^p|;#dCxG1g<@uZrj{ukwA#9l`BQC_)#t<5+jLlaMzpg${YNl;6Y?LN4Y(iQ0U06 zPTh5T!0#&6AJ4v46X1up;rqWkRMdvMnPDG&sxl%SO{uWZa>X+kQY3>9znk|^nN&-quScB0GV z76Bny5WsEytuTM zJHi$fWviITldm!@yx)QU_O-7Qwzxu0Or{364SZH4CyG=Jt=@ZWg3DC$0Dw9gvI`@h z_H->=6F%Rcq?9LZC=e%)h9USK)a4$&lvNe0`l-k?sPXTY$EV@y%evZ|8|%{Z0R7uY z$PspC7W5bUduB{eTZW8tTGWRCDh;JLOg`ye{*S`_z_r#dS_xHEMm1Qg)e$XEi4<-*LPZ5CFAAlR%V|XIJ0YUbmpNGU6 z?=fZk**RzkGF~?B7VM;E5k4yWp?*7mI17||(<;uWp8fC5|M#hG)1#==R?`Jpn|+x% zk9My9&fN^wrXgS@^`KDElEXdi+t(OE@L{;l|4W{yxrn!KmmNK8K=V~ce@D)&NNR(| zxaQ9)`n5sk(Q%1v?w_S{BHH5}fpRg(et@(>v-xmi{9G^@3Q47@56WsFvmH4@d*M%S z%~OE8OFG7yhG@Dzs9H3)a(S|e>1)b_myq^<7_qSmH>nIa#9UhKpG}{Cq;SF9?9`>5 z{$m#xa_^of+eEqd-HX=zwd{qu``%f)0n}qViIcJmX&U4H*%ws;dAjv=S1|7$&}i82z1kZ#>ZEt-Le;2{lYMm{eJ1+O4v-5<-b z4PYNl+SCQ2jGH0Q*h&`-bdwaIs+m?pLPBH!Ql`XR?}ZD+)9_TRlnG;v&M4HfHaPj4u3U zx@P1Y=JRW?Zu$i@lNx(L9sSPE?1F zWPEWFanprLIybFZYkf+-lH+=TA4b^Q<6hVLm8{7U!cBIxciPDo!XB2?-Q55Rn%^0t zJtpQJB8yRVYCYnxH?9(#8V8SmBPN~ZG)D;wE~+b~RKk}Tld+&l>aOg{IuhA(H0Q9UjO~(#frC!1MTe91mvmifTfQ>;K-F9R$}>FvgQ6-N4&Vd}k8?jQbuIzJlonio!V zosCRiak@=J!uHGV#Z;{Tzf%=6A)W0%R4y`^2k3yOv|JkmwbdyHQ@<)n0EO9(Y-m&} zOpJB@{wp&ym2>I)zQjruv{A)|cyAGl@xwu7&VoUs&QA?xU5*as*>bxhy&pw%&Dba8 zu*@uMb6d%lS4S+bKx3jmas(Fd_BYK?j~{mEVcfsJCXqVY3({(qg~MNnZI5La6?XI) zM7t@cqD%Kwi+4tIGFly0jrI$R=5$f@Xvp{#P#KiE)(9yG(T>JMI#Z^;A2@KYXuZ+N%++_OplW{%JHN}EqvR$7?hO1%D=^(*%a#8OcX`1IpWSdJqT87aFfE|G8- zKO3@k?}*Ahnnd>T_Qts}rhgEXN-V=(WeSZq?dS{EK+8{OxY)NE?wBPP?RyDC^_sj;1b;oF1(NO&#j>VvBXHp(fZJM!@4fK?uWPyI% z2)ra_=SGlMbJSWOiP8jSXhWR8*%6zjliCY zpB)v)X2+Z6)PkCL9H5kQyy3{Y1-uaywVu5bLC{e*&!3HTY?`%PqB>KPLcr>7v3r-S z2!|D7M5mdT1B7fzlAMmL4lDb`|G50RA?w(XCa#8!ZWDPA4@Q)hdW^ilqri3bm!sL7 zr6W+CL`7Y;6hW>&%in57VPGbeQUq2>jvzj^)yS*X5GT|PiJ8;kkWVU=a0I{i2Ns|} z@X%mu!!OMCj#_>sA`6=MZ~+v?W7S!_!JXNB<>t4n=0Q*p++0fnrw+WKep3wKT0tpe zVB_ro3}I4KX)6ZBcV>!bFct1$)sk!}3|-1{xGC}WNE`*TCM*>C{T5h^b=^h1dWC`k z;tDqS$w-B&l8YB{<76JzAcr;((_spn5%*t1L>6xkCwg0ky_voyV`01~Ev)N8<7|gr6e_*BD z5dg3HnSP9s{hAoW&;83IEZKM`cg+~v5!*q^3-l?oUB1@(XY5ZtR=03pJI3xhFVY{NAa)?l= zPT1G9YA&e=PU`H8(QSg2ctBm}s{SZvBvw0@^2dhdS;i@DcV_%U%0^m4+uUO#@S#gUYw z{(@E*5~Q^LP612W2rn){B4ZYD4hAcDy0{k0a3O$KoGVtqui6&Puroq|BJitZGWj6PKj8`Qw&I8UR}DD)36n|7Pg{D9LgM4S*D#rM+Le!b z7Zx}v)%X^x6@l!=y3ovc1Yv_fkUo?H}(1Rta4QHKnjxiMqRfTC1~ccZkNg>C~TZ_ev;~v0W+BPi10V0y&dRZo20qxB!M+Vf*?yWQ^}aSCjOK{6`nyHC(-F?rm2?7DWa%J^&u$&>0P%xlGX2;qG0I2sM<^A ztm0XK8ZBBwg@CX zI)wgGJf~nDLEe7}ve!?9;+z%5G^d*iWm?-T72YRh*|ng!ba6)b%EkXXCRyC=WBonH z+z!nurWpClqt^Mcl6qj>j4s*cV3T`OY2}4b=t6fd^i!64cq8k=BSM2m;yaw-4Q?_B%_(R))kQ{J~u55y-#h-Y51Iyfsro zIE6eS@I(sbOii1f+nmilbQrrrjrcl?$9}$P)3v}=+zyOol*jKqBHZHBw~W#O7g@@m zQ33%49s-m2!J;`2n9bc~ol#1Z28{p#EFFQ#BzxWXDy~h(0DfS+^K)ZjIYbyz-R!De zcL9PfebIEE@esrX-~U@!J6*mtSrMNMMt@w=XMZFreKaTP|9qBXnr|%ce`uKyoTy+> zpL@=Xwd!J!v->A|1y4B;dduv_5M4GEMUpvWAUoF=GbvKvylid7RTxE@v4Hf!K^hk^2*5?BC$9xh%9s_f*2+X@fCg}F z9=@Rieg8y8<;9Wn*Xx3P(H{<_WW*fB1W8o-wR?1MC`+)sXxW2<6N&CaQn#WAS-k8X zSQ-6uWW2+DSA)zIrya4!FlomoNfxPn$V|RDb>gGk*&L1^C6l2*_!b%Zy(fyy@6CWl zRZ!5?H$>5-;UWM##{J_e_53{fe>S`O%4dfhu1P!e(jpVBLyKW12s%n3uqKkt8iXh* zL#R9IL>!fKw3Ewj5b1Q*rC*h!m>PpjM2;txWaO7?7h8unVnBLL*NG&;!2bl zvT%$(Y}aVntECbJx2Fcb;9^`}=C$*mYi2;1(=q2Kh}U`Ju%RWxFlRBy&WJ^XY1Vbs z3DJ_xgT1|5kC_EQ;gUX>V;UjlWH$T8Ot#*B&ekif_!oY4BZsn9R63F{b~e<%cEi-{ zWPgYtwtJ>?c!3!kyCX)@ge9dkU`*X6hAv=r8_6ewJUR3!p#u8{Ula8P4!Dj98T0-uJXnOn)TQXHgQp1!uy(?+2)>HDzQrwZ!f7*GJyKA zobj=wF#8zQeWkm1hXd@oI+G{;%rUWh)`8&&5blsYqU{ER+$3=J8Mcef|ADn~Z=P(8 zkDmxeJ1wI^UV0<`CUd1*-jguhbaIM2dZo)o;;nJfk8^>tWI(*jzi!oSl4W!j6%dHt zp36@tNfuMqu`sTwtKCuW)C0R^IPGkT2j&Ql7$iGXB%8VeEuO8p4bw#Hq|;82G%)qJ zFO)9nb>v#*giSy;c&OUx<9EXY>3N{DBn~w9AH?TR57UdDX;sx{Tj0XxS5<5uOWeFO zDYWG@5XsDfdWWZRT|~a1ANh^4%o*dFpBfsIj(;OIz+~nDI+fC82Hn9?v$+5c07|&f zI1`0&k_8iw_g%;4{x@n-uled>N4m4f@Hr9*xR>rnz^{fDP3GTWCw5nR4O_lTHp$(g zeP6*}Lx?`Xr^%d0HK2trr>gbIfM0p&yxEd1+y_{n*{336K1D!U3tPWIX+zg{Wu@1w zsf|h)lcb$~GHhYadnFH_H>>`E0ghdzvS8;x^>vx9=z5Iexp;wZmq~#5fW~jJQJIbJ z_9U#&Gl-mttSCrDcnPw%lwJJStzJpL_wDtONc6_&c#{*N&KMOohI5>VZC%%Si!h!- zYdg}YX7~7T>sXl#MJVqHe}aJ88_&&G z%BCWIFnn^h0-`tNujOnmXxJPk5Zw}3U<1`V9OzRCxYB&prcfZvj*NsgY}6XkEJn+m z>HypRaCJY>I8)9gNM*aWE}@Ev^%=CVIXG%{rp?jM^hbxf#^Tls!mVMi;2HI!dWGU5 zkawm~6JaUcuP^}+u8H_XghJ~?J&xf-5JUKK}+IX9W<_&^)Nl*#v zu(w}E=5Fx)hd?BOtRIDhTMk;7;WZ{@+Qrrn+(z>U9*I+17mQnMwgWr;n$yLFA4wWC z#7?y^$XLH*%*VT_cJanixY{;Od^Sa}lpQ=ASD0DGZb^CyY zaN036^_GaFd^E5)XVHx>YvE*??98Jx(@f^t&zPs35B`O^3B{mgVb^}cduRq?gz#1x z?4npH6@F{95GRI0%?5H4;_(Tshg_$sXDO_~0+WLa56ZmLwfBE1dHblA!m47G&!hk~t^C>4oQYwP;1!en zV8nlA@T7Ul+|1v6|K^hBS+dVT87DQ$MQZ7~plD2>o(7@ej+2EBV+s~eT_FtJc4b}H zA}^1kkL)ZHwB;^N=BQq6Bq)RNdzG6G8rHzG!k94eB1S#%*6Z^gHcc`;Tu$K=H6KZ%F6WE z_1ojS;}D)@uHOrIimh{=I_X7Bp@qLzt$E37y-b!bLf?~hRgs*ld@?d z!zQoxJ2O>QZ0YZ{f47N|>5@!q8R8CDa&I-^Ey(mMp}j_nAbgsU4oicFgLvw+ashNA z^r?B8J*RMhD_Xs2@rvoWdEQK6DcS`IArrDCV66pm)kIhPI=s$bJ&wyak5?g79ot;> zD>9;%{Q>5U8wl?bdk<5Stl{c1Yk^TB=sVTr}^?CwMs%_5OuYA60%3hYiBJEOo~&%uN{@(uz_DIAp|p2T7Y**#p<)8pQ}^^@gG z$F8QQuN;=!JI(e5oBfx7S&3sijPVtOwgx63=*e@gY&N?f!4V0oBi)o-u;K{~-e-P3 z??)&DR0nk6`|~|OQ!VJO%n$g=)VG3R&izx}KY)gOY(1G%9|~q0)swOz3}ZN)xg~ba zSi{{JE5SGJMYa|7Kgy)L2uHHl0gu`z0On)bIefch*qj6HY)xW}S zmaJEuFsd;o5>)b7gP5qjJ~C^!1`x)s>fj(*HXg2U>D2RV7!0UHg6N2D$4$JUis;#z z2r-KE(9qmxl4KtV7tVo46K75oH9YzY%aYUP9EOz%Ve?pk7fY7Rh8ZUFU~3m96My8&rQmVHre&CKinO0rp{*67db%sN{<;K9foINS2`}&T zM=y?Ymsf|KVQ2L!?%2gMp)$&#?!IA!@6kncKq_rD8%eunl18a18JB+^^1N-}OK=dh zFG_L8?$bwr>x|e5D7fZsXXLj^n0~Ht+a9ksxr^}^}D_`SA#;t$5xoXzp zQgIBGun$ynppK7`U)1oNLcXxP>bd;VAEhtmaz4%KDavz-Yhnzu7^=y@*V;i6_ILTu z9OU+N&^%G(BHfo*@{h*`%b!~X^lw|nqqk}3tp1c~4M&B$Yb4SrTsFRsx`U5efwA-9 zffW-4;-UpzsKhR^kxuWS+kF=F)d@sjHaqe;Z@4ylbLV^VC@pwcPUzlTnmQg=9dzsb zY;A`!$h;!03 zZ)TIR7dLRTqwzKwm+ADRY>h5!YFN;7s8T$lk03o2w!Lh0YhKtlFw}LM%VMAzC+*Nz z-Ta6CkvuBX1_w=YPP|pAMmdIyBOKxYoiXL88du?jM$!Jis;s zb(A%w7n%NAcZ7?$Zoc}(lDE>|ANfdcu?HjFo|+RTdlGx{h$=!#d7U=w#1=e?PTWZ~8s;cLS96w!03}V> zf;QZXAD~lE6U7a&NW_1{Is;CVCN+4E5FGX`WGLpc)%AIgI?)(o^g#B5F9ybR+a4t# zW8stC$HxZ=A?aL8PCy^V(Rhh;9w4k-><=9+Sex=K(#En`otPl!Hqoq#8OC0=cXJV) z;xGoriau@e`w3yg`P}&vlR6w_)&moMz5!ssGY3wVArob>_q_C4`L`jk?zQL6-<`~= zzaO9K{n6#-JrTnAA+P$RJEpj61?=PQ0q@`t$8 zKV<-~0%h0v&FQM?cTA_&FO@7)HpRe|eCjh*HTEh#HhKQuYLm-I#JouZT54INPjUU7 z(P5xG1kBeIvU?*y9i^(SluAPo#H{|UMfJfn*Oq3B>62qUuN)lrd*z2?ZuRuiRb8Pj z+ZmK?k>X`t>PTSy>vN%dM}9AI0^mKsm^|F>t@*}Vjm&2>BctWxsFkY z%bXA>$$?;NS&<@jcC=MgRLW&ND|3Tof{-2rs7mpcq|^`V z@L12kNG@D6xkISYBD8o?sXG4c%(l&#tRQhN(8F0_(dWeRvGRh;cbUO#QgW7)l6v_| zGYgjIH)7oyoXt!s@AqNd!nU)3z~r?#*QMLbA)J+kt{;q zf~W9jR)}@ATBl~Z&dJA%pW_jHyG~6SPgsg!92`#w3&`+wgt@B_>EcLGzbkPHhn3EK zrE>(9Mt*Ohe1$mt#ey-1Rx0qVy)2kLz5YW1?ARI3FBg~9o`-28BZ_F(DmT{jxA3!C zGimUYO#4R!WOTy}0$eE(O&eHa?1NoDZKGr^JaWm=w49x7(bnYRxY*jI0*=p2VD zLt2xv{hik)(;D(T&aW(U`wg8=&757`$S4%Gb+%#7Sr7_)OX;)D?T#g8FL*m!-ucYh zpy9kW7$g^E`046>Giudy){(^}Z0^lNDx`(CGk}B z*vjtnRm8^S92dqDP{Q-6xEPw4q>rEC|MkB-vUJl5Ybzd5jOG#M1*QK*e^r#6Q#6|M zoO)EW#WtsgZzg`~^u`l^#UsP148Z^+MwC0E;(65ub`cDvA@oJ6GYe>qWR#d3a4WGY z3MSa6QL$%K;%B8p63}w&z6Fb=5VtY`)8K4ng0rI%P;YL`0*>a0gX>qBZ0_$SW82t$ zx&0Y$wcvCy{NSBH45fx`)$Q@IvM~US4@|Dj9sB>&Ge5=MWQR?1mrafN<-VxtrS#M^ zJhPBOg7r4{cZWdWPW&z4w0`U#`u7Yp1z}v29^NT_p_B|Y2p>R(4Veu8Wa+?lPhYg> z)`^}}a+WZg90Zyg@-f90t4_PVj%Xmdk<3`}ZL$CjJcjjNsC>%jTkB;e?bWghvLpSB zbPWRm@Yx0)cXavt*l(^&{S=ku;Ree{m|MMl%^t9}Mc5kzr88x{Ys4i)n)2XBjgqlT zA4E;aTtv{>j_d^oii9h0y+4Htcw}G-^G#m1)8-&vKcYV#rH+?f1Hp(!34sTJ9ji7k zUQ$?EB9W=LTaemC$6j+VBss@rI*XBzj~KQthCpN_RZyfN)C%0X92PB|O{(md!naQ7 z7DzDXW`}!gSL~YVGb8e2iO763qi-NHetPKifBN29GMGQud+>uDhLHBLI*J;dZ*pt= zjm-3WA03N$!@*yfNz<|)`{DgrX!dX2&8udqyv$f%&Dm2f3xb8T$B?8Ld{Ob{++xw9 ze1V9^?JMHG1q1w=V(CA^ebB1j@D4AULHbik51(r@^kF7??XQjTD^U-=aV3xAd6l=a znvQJ_`|QVJ^jy8YV_{kLzkyX-*AAkPzjr#PAsw@17CG;+ug zG-(s^t^#lsaMH@3qGU*0J2kog8`|{HV}NMKDr%{|lY)s5Hb)|pruT96Y}d>&^a06= zcFiO4uQLgBSt4G8OrJ=T4Yt>(6TWJo1rOn?)MLg)2!YIq(V7hg<^!lfMyA#kF@Wys zyZZnQzlD%0ie$nCu#Nxz=;Ppvh}HB?V*zYB_=5X|W>~9KtmC7-v`6VCLX<0v3PM^7kI)N&`ec)YDZxq=KD^*OKFOQ@ z(b*yhlz^LWxhcFRP6huxT@)>biXa>nU?#6-=;EBfT$sE0-QhtDIq{$}-i`|MTAoa+ z^Z9gc`lXb51q6J-Z5ki2rY=6( zNd|^}$Cbz2Jdevs-R2y{Urh0nQHy7#io})-^uos|nl`9bI*!b+6Ov1yQ=-$7s*72P&+6Bm33;=bw z7@djvJvSJ};~ew7MA3pfL6%NmmHYFQYbwsoS-2XrA6?<|uM?wk97LR$AEUAI;Gh?i zuf2jml45A3qa0vTmTVwDHfCm}h5vs!EKT1fEP1*~s<_uN1XCl#3?7*_>~pl9R_L?( z4p(i|g;1lPDOF13&E_hp&_IzP(G+qBy{PYIb0R{4VGgyzq;6_6ju9HQQJRcKsG8N{O-&EBgI1&6c0xM?;NR(@uG`@zyU(aw*toEQvFstPnxz zm&{$=uzdq!YPJ3#4TB+~phU-DH=Lz{j4n(P6eJQg36$gsmZ$>@f~Q_AD^H4iq9@cS z>(uaRfu@sCisB(I_nbLXS`yQh7AM+M<9vkThu0gr{8dEEmDFF?h-pWLd#uLv2|E&o zA!#g)`kP4OJgvwS{pOnjpq;U@k^beGDY+c_-4mR?pd1`B^~!b{DBv?~M65WhbJIwE zzX5<=pkJtrrhclfj*cJ{k?{(L@DzH`56Xb7YG6q+n>9Q`HF7WzO33AnkJX3*I#GpC z9%9~t|68|9g5c%jdRCb=Rl)_EJxjIc3Yi52xKJtsKr5Pt-DWCxjC^%9vj!L6_tUR!v@YLUdAStF+y` zxi7(2N0IWP3jqX00zZQoW>Tv$mB?93KdT)Nvs`nOd6~+{sHECR4S^fm1vPD~$p53s zt=vknCPgkEqttpco*2iRnLYxcv9P|zE-ZDSnWbsTEn{@~U^747@!uO9s6;3~qQa>J($tC;Fx7)#gTdPK zt{`<^1q~GK1DY2QK=Vytw#fyyl;ZdxgDTJfK3xzvBiWn)H!y@=sd}}^yy5BNim-TH zu%=`J=l2Ma4yyUNBv8F4J@NooOE7keaEY&s*K6aeG;vwL?5ct`2 zZS+sLZ}G^s7zL_}zz*WTJJVH%f>D?dcGEl>skEgP!7|VRbE*{#b2<(_%2C9sX1{oz28R0nk3r4D|zpF!BB1uIL zMXZ@2lO(wxIf~zk#5O7C`PIbjzdy!s zjrVUgP)KN8jA+NRFd&y|&R~EQW_^NXV6b&Z43-9TECGO~`pI$xf+0E&5nCW2Jjg1( zRMC^R$p0MyP-OUQxjX}bSF)P$*)=7HI118J@L`j_psWU5ct24zRw8P=JKGwiscEQE zr%O}Fiko(-18l;#5x@q9iYa}x0qMo5)v7#t75)hAbyuo)$Gv+7&Mu1kJLk{OK6kZz z`0%NJwyF}GcHmz9`^^>Z^bM6R60hdl2^tX`20^xMMM?6M13s_9Ezv7O+BK$JnTO+I z6w4p#uG5x5DT|+-9oRw>kOT1>p5k7CY>SKFpK59roRZEaR+zP?fx;C5Dv-M08Je-E zp=Lhb={bf=WazZsFGJ`+8DRj3bf}ayW>lj)=I+tz)o;`3x$~Ede9+%9QFDE}eZ;_E z*JsEDdvM*{lUh|SlGDb>d)vxU>cjCy66{^Yt5l=I&KgqyrwpRPABPXd0blWEY-mO4 zvLcb53bbO|kcS-Gn3qx?ept2V_5G~TAGgHyt?)cCA+K@uz<3Uy%jNZQ_6Ar1I1U;5 zqY2TvLX6=GO>w-D1V5J*ZzSd|IMoy*r38T}1t=^F{v)@()8Dsw<$j{GD;c-zT^P+_ zLWjtRjTjHu%{T)ZTjF?g-U@hP_#Ff*A0GR97-NZVmC1A^OLL-A!r8gXz5Cq{;RW6C zc`@5<&elv$M=RYmN2TCHa6?OHzevm#355%nt!vl-iAU)n7&UFyC-QBJzE=eGa+?14 zyW;1p7*-|{w-rm(`*|A;6zYQQD^yV0^co=nR7J7Vl6w(fu>Z^SjEQ24!+J~1qFEZ_FdP0!1|o~vA&`3N8s7HEax_k(wi_~N&uPssk81Pe#+kY486-2+KX z#Zrv9BS4-QeX5|?UYXWglolIWummORbb(DygblHuRM1*wI09W~iQ@CPE(GoIVNzPK$M%eC;*;+9D?^Erp@g@ zyRr_Dd{^#;z&$MVbeNFevMG+DDGl*vTi^R=LaJK{bMiiKC!3sK{C=;CBB6T-$2_5{ z#+jBk@@k$oq+C<@>1I?G5yaKmXE)nOxEX9eGlO{-- z_jZB8K<6iQx^3FByRecSj?I%u!e)vLCla@uDcxF`)89_>kGo`W=g*zTCpGIU1!-Lj zQp6xOewG;{H+r+hEF{Wup`ID~#8W7K7R5@kPGXQ1W_^TC2B4`7ftPD8vv(RfH+`>O zwJ@D&V!PQ!Q)ON-3yru=Jr0S1fJF(IZ0upnqhYMdPCo)p9J#WT3KM7)Z^A0=VL$Tf z?Q_%Pew!PrX{hgRt_^Lv95Z2gwufKpA5IUc4-4c>NwK6}^5#u|P7ZC-6t zXE}~#r8>;GVL{S)JakNfHC9;jJ}loQC25pq5)2tasu6)&D>8RAVn#24zRe>OLj#FG zi(^@7Hz8Cs3uF`?f{`9R`YwL8I}o^V1N_@zzhGSD)Xnab?Spdk?jr^b_JV%8DLQ)S zXg1%raccVixrs)H|FJXX6?{+$FD!b$lXJe%v>?d#QN8NFrI(jM!SEh0?dbH!-nSWP zt;-OC9d=E}W{;dZ-Qy1^340F(G~A6tqqbTlB3)depF@f-G%&qKPF{v%d<}b&AU&tT9fR~G{Rma-E7^-SY<`+arjdmP$OKM{)Lyd ze>sEn#@ZPL!vLJCntpJt`EY#FWj?PE>Zl6YM=EKB2<(mHprh2fXBDFPP)JaItnUKV zQyIxd0#W{G!(?Y`I-$KagkR_SxzGml7GYRCP~O}KY+$1>jj{EHWeulr9U<%p6&dsz zhV${*wwcW5{KT}V7>0LFKP0hV%&gL+(rqqr5krcKDWwRJPA%0Z@Mq;!Q?uMP%S(!h zTrIeJDu&_z`>m6*1!Wlzu-0tv0~xtHf+K=<+b7_xOMkvAlcD_L_AHQhH8*pIAAQw|#qEO{KL;K~mSLc5KOmxpm!D72Z0ffyJEO+_ehB z>vz@%88>B-cSN)sPFzr=ASS*vT+va{Zc{Po0KC1jCFam4gObpm?N``LIVFQ=ubo)t zkd;GA+LfJ`T%?)Mr#5Vjr)6Y4w7mZj!!i=mWnjGv1XBxe916xmcwQ(7QFJ1PZURN; zU`FOM;&7nQjALH#u!7&LS8s#@9xpxU=Ct$tJm%$}`I!9D0_)?-G{lypWLIcVHF5F; z38rs3&r(KhqtR zqr*kPf1qlGs7RSwM#rb-1*|lN7KgNWev#^-(q=(%Z)JeiPuI%7sDGq#d!jfxS}_^v z_2JfwJ$^ahi-jW;?I)8-C?1K$D9@o40k3Vj`CM9vblSDkSvRq82}Nb2l9NVS5HBgO zQIS&oNNoh2&}}qQ@hL&z9ATBNjT#`oA@mF|x1tNsR$OENVhLz&nV(c8O(!HFs~QdL z+fCQ5VR6)u@kVqZ3n@k}nmR3OoI13sDu+t}E9B*ZqD2KEooSIG^wJH54^CcOR1Jsa zy5H3)eG~fbRo|fCx@1t{q6`ie742q_4dVb}t)_lOqPlh{M5SLCdq3saA1Z>7lT(aulX;FI7r?JFamG9>V zY03{8i`C?TLQY=P`}JuFmMU=YU~D&|=}$J0q-hoU9f`4+C4?Ud8^gmm*`GDBpdU9r zWT2n~-6dXabiV=fCl-~GO+qh53QZiIj40HMlUaw7aRro}yN$(y=o9(#-4jNTRLhGmIKhdS2=MraO1BMt=ntq#ile9(Z*;evOePYf|frx~Pgng0$5W zxROjw8F%IRo^FETj6HyTQbEGPf`Xw)qEMw=6cngQ-VznD(J4|$FN;K~Ku;zh>p3u- zkHFY`UV1MNJd2%==%4EYS6;JP_TjEsESUj^F*Mjmg@N@ATR?5*URLmg0&-TI+*K>o zjZ26eQ6znMsBH9Foh%&swD|w!W`Dq>crgzK%+X2R&5< zby2vBk5-+2N7U|c*t``UVf*l?Q41k&kTa(eKCiHo&8$^O!jc?m#tK!lSt}aMu5qI_ zQEPUc2Z~h+_7>}u-67d>Ozmy?ih>0!yRW}p7Yu!D^l=IJ%k8k;lAIP)-w2nnNI400iL&Xc`5XTVAs<)T#WA8WNMc z7E^A9xD$PludgX%O%jNCGjI*J*-#xNPH}oe#bTT6=8$-ddMdxn0!fH$$t>eEI#k?r zCoLF*#}wNND#I;7?N3T=)L%@!F=t}t>c+MgKB;EUu?|PJQt-oMqTIFa0 zzyJh*zLWvLzrW;{iX-#$-f5Hs$jBe+;Ufj;q|tr!HZtQAEdV~YI^Z{>mJmkL$YjL6 zNsKzF0=s?oLjXcOwWWHlI;AtMG-#M8vN7(nx1<60r6NYAct=C1+F{TuDQ0M*qmMJ} z@ER}=4SK^}rxVo6_66=%dK_56D)pO50n@T$eloBX_huU1XR}QJ-o3+XP7WGMB2xkB zlxIJ}9ExI9q)0&1L94*jDYizyZ;liYH#GG2CY@}9mXQFznQp`rF<|axIzAA2@6onZ zq}5u$+=9w^`*ZNX;P0q@g{ z8PcBPZD0zMYEb|}RyVHHlzZL^?FagPEE^2KB}N)OZ(b-6Zq$^a!%h-wb^%!^4^lu7 zm2{7|A}jF*Js%wuFzhjx9~7ugy%hk&N>MvKSJ+1Z9QsM5;Lkpgg{%4vYDQS9XQg3A zZgVv~ao<1eeo62gb|OCT6;X!udQOTd53U7tq&+cef+Q@ey5F`mLOn-<{}c>9VgaB> zP=Cs5J_jIs0c)aOR6F!q10YaJK;t0m3<$sg_YA1oa1La^Xo>WPQ!=C&s`5j$>(C~g zhnNwO^(>>ElJ$%Za&c{)qbOu#$tTX}!f9KmB HNCp4^t1hY; literal 0 HcmV?d00001 diff --git a/assets/inter-italic-cyrillic.ea42a392.woff2 b/assets/inter-italic-cyrillic.ea42a392.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..f64035158d7e4c01654e3f23dcd6e8299928a28c GIT binary patch literal 17824 zcmV(|K+(TGZYzoV0yihRy3;{L*Bm;yv3xXa1 z1Rw>42nQe=C3zL>7I?PUshK)MG!ta^L3b^d$O{#w!;WOi#caU z9epdi^~z}FXC1zMc;nw||B-~iJKZ7-`H(Kn3lKjZ5N5RqaHof~Lr#JsXW1jml58&> zQGi~|0cnL4zB|P8}0t5&U@c|JdMvNGlmzMw!d_sgsBSwh~ z<3uKERMdzOA;C{wc9u@*KWk~1ZrQFoOIvrA?XoRxnU;2`Lr2EaDbuDiOouv|*;F%- z#gKPJ>8fl=HUf*mFq&P*op#SRB5uMwrI5O=D|BV|t7*jYszMn6_y=vT5wbG)8zDBN(kmfH1b)al)=7_XtiK52#|ESncdAW0QK2HvRY`_;WiO>pz_-Xk zH!T)&YR`MAJa7J@pNA;Ve=XlUuY2Z4Y1bh`Bcza^p&9I}QLUiMj74g|4NyqjXq$hi zG1}j&v`P|}Bqr@5l@eV;qy>fq`v}SgHrfA#@*v!mV~$a+B6D&Jh-sRueXL+`%jr?vt30*Vq&Bs(79VL�!Z1%E;0yxE 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 literal 0 HcmV?d00001 diff --git a/assets/inter-italic-latin-ext.bd8920cc.woff2 b/assets/inter-italic-latin-ext.bd8920cc.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..9c1b9440ed419d4a71ba46b0db3951164f9e10df GIT binary patch literal 63552 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 literal 0 HcmV?d00001 diff --git a/assets/inter-italic-latin.bd3b6f56.woff2 b/assets/inter-italic-latin.bd3b6f56.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..01fcf20724f915f68a974ef2fb85f86f3039b1d8 GIT binary patch literal 46048 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 literal 0 HcmV?d00001 diff --git a/assets/inter-italic-vietnamese.6ce511fb.woff2 b/assets/inter-italic-vietnamese.6ce511fb.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..e4f788ee02bb687bc1d5045483ff0d381e7654e0 GIT binary patch literal 8784 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 G0002QVTgqQ literal 0 HcmV?d00001 diff --git a/assets/inter-roman-cyrillic-ext.e75737ce.woff2 b/assets/inter-roman-cyrillic-ext.e75737ce.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..28593ccb8a4d849a746f2b970678fe426cb136e8 GIT binary patch literal 26600 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 literal 0 HcmV?d00001 diff --git a/assets/inter-roman-cyrillic.5f2c6c8c.woff2 b/assets/inter-roman-cyrillic.5f2c6c8c.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..a20adc161f433a7c4e3d92306301b9228bcf9fb4 GIT binary patch literal 16780 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

KZ;k8ywA} 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 O1{o}0SRMEP0000Yp_Hru literal 0 HcmV?d00001 diff --git a/assets/inter-roman-greek.d5a6d92a.woff2 b/assets/inter-roman-greek.d5a6d92a.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..f790e047daa346583880da2be470431e35aa6054 GIT binary patch literal 21776 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 De!(H( literal 0 HcmV?d00001 diff --git a/assets/inter-roman-latin-ext.0030eebd.woff2 b/assets/inter-roman-latin-ext.0030eebd.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..715bd903b9b14d22a056f10e6d13b8d7e0acce57 GIT binary patch literal 59608 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

JDK1.8 HashMap源码学习

HashMap

HashMap是开发中最常用的容器之一,它也是Java集合框架中极为重要的组成部分,HashMap实现了Map接口,并继承 AbstractMap 抽象类

java
public class HashMap<K,V> extends AbstractMap<K,V>
+    implements Map<K,V>, Cloneable, Serializable {
+	***    
+}
public class HashMap<K,V> extends AbstractMap<K,V>
+    implements Map<K,V>, Cloneable, Serializable {
+	***    
+}

首先了解下HashMap的几个字段

java
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认的初始容量
+static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量
+static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认负载因子
+transient Node<K,V>[] table; //哈希桶数组
+transient int size; //容器中键值对的个数
+transient int modCount;	//容器内部结构发生变化的次数,主要用于迭代的快速失败
+int threshold; //阈值 table * loadFactor size超过这个值就会扩容
+final float loadFactor; //负载因子 默认0.75
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认的初始容量
+static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量
+static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认负载因子
+transient Node<K,V>[] table; //哈希桶数组
+transient int size; //容器中键值对的个数
+transient int modCount;	//容器内部结构发生变化的次数,主要用于迭代的快速失败
+int threshold; //阈值 table * loadFactor size超过这个值就会扩容
+final float loadFactor; //负载因子 默认0.75

Node的结构

java
static class Node<K,V> implements Map.Entry<K,V> {
+    final int hash;
+    final K key;
+    V value;
+    Node<K,V> next;
+
+    Node(int hash, K key, V value, Node<K,V> next) {
+        this.hash = hash;
+        this.key = key;
+        this.value = value;
+        this.next = next;
+    }
+
+    public final K getKey()        { return key; }
+    public final V getValue()      { return value; }
+    public final String toString() { return key + "=" + value; }
+
+    public final int hashCode() {
+        return Objects.hashCode(key) ^ Objects.hashCode(value);
+    }
+
+    public final V setValue(V newValue) {
+        V oldValue = value;
+        value = newValue;
+        return oldValue;
+    }
+
+    public final boolean equals(Object o) {
+        if (o == this)
+            return true;
+        if (o instanceof Map.Entry) {
+            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
+            if (Objects.equals(key, e.getKey()) &&
+                Objects.equals(value, e.getValue()))
+                return true;
+        }
+        return false;
+    }
+}
static class Node<K,V> implements Map.Entry<K,V> {
+    final int hash;
+    final K key;
+    V value;
+    Node<K,V> next;
+
+    Node(int hash, K key, V value, Node<K,V> next) {
+        this.hash = hash;
+        this.key = key;
+        this.value = value;
+        this.next = next;
+    }
+
+    public final K getKey()        { return key; }
+    public final V getValue()      { return value; }
+    public final String toString() { return key + "=" + value; }
+
+    public final int hashCode() {
+        return Objects.hashCode(key) ^ Objects.hashCode(value);
+    }
+
+    public final V setValue(V newValue) {
+        V oldValue = value;
+        value = newValue;
+        return oldValue;
+    }
+
+    public final boolean equals(Object o) {
+        if (o == this)
+            return true;
+        if (o instanceof Map.Entry) {
+            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
+            if (Objects.equals(key, e.getKey()) &&
+                Objects.equals(value, e.getValue()))
+                return true;
+        }
+        return false;
+    }
+}

Node(JDK1.8之前叫Entry)是HashMap的静态内部类,实现了Map.Entry接口,包括了hash,key,value和下一节点next四个属性,是构成哈希表的基石,是哈希表存储的元素的具体形式。

构造方法

java
public HashMap(int initialCapacity, float loadFactor) {
+    if (initialCapacity < 0)
+        throw new IllegalArgumentException("Illegal initial capacity: " +
+                                           initialCapacity);
+    if (initialCapacity > MAXIMUM_CAPACITY)
+        initialCapacity = MAXIMUM_CAPACITY;
+    if (loadFactor <= 0 || Float.isNaN(loadFactor))
+        throw new IllegalArgumentException("Illegal load factor: " +
+                                           loadFactor);
+    this.loadFactor = loadFactor;
+    this.threshold = tableSizeFor(initialCapacity);
+}
+=====================================================================
+public HashMap(int initialCapacity) {
+    this(initialCapacity, DEFAULT_LOAD_FACTOR);
+}
+=====================================================================
+public HashMap() {
+   this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
+}
+=====================================================================
+public HashMap(Map<? extends K, ? extends V> m) {
+    this.loadFactor = DEFAULT_LOAD_FACTOR;
+    putMapEntries(m, false);
+}
public HashMap(int initialCapacity, float loadFactor) {
+    if (initialCapacity < 0)
+        throw new IllegalArgumentException("Illegal initial capacity: " +
+                                           initialCapacity);
+    if (initialCapacity > MAXIMUM_CAPACITY)
+        initialCapacity = MAXIMUM_CAPACITY;
+    if (loadFactor <= 0 || Float.isNaN(loadFactor))
+        throw new IllegalArgumentException("Illegal load factor: " +
+                                           loadFactor);
+    this.loadFactor = loadFactor;
+    this.threshold = tableSizeFor(initialCapacity);
+}
+=====================================================================
+public HashMap(int initialCapacity) {
+    this(initialCapacity, DEFAULT_LOAD_FACTOR);
+}
+=====================================================================
+public HashMap() {
+   this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
+}
+=====================================================================
+public HashMap(Map<? extends K, ? extends V> m) {
+    this.loadFactor = DEFAULT_LOAD_FACTOR;
+    putMapEntries(m, false);
+}

HashMap的初始容量和负载因子是影响map性能的关键参数,容量指的是table数组的大小,负载因子是衡量哈希表使用程度的一个尺度,负载因子越小,那哈希表空间的利用率就越低,造成的空间浪费就越严重。而如果负载因子越大,哈希表的利用率越高,带来的问题就是查找效率的下降。0.75是对空间和时间成本的折衷选择,一般情况下无需修改。

HashMap的数据结构

  • hash的概念:把任意长度的输入,通过hash算法,转换成相同长度的输出(通常为整型)。

  • HashMap的数据结构:

    HashMap的底层数据结构结合了数组和链表(JDK1.8之前),实际上就是一个链表数组,也就是上文提到的Node<K,V> table[]。我们都知道数组的优势是查找快,增删慢,而链表则是增删快,查找慢,而这种链表数组——拉链法,结合了二者的优势,查找快,增删也快。查找元素的时间复杂度为O(1+n),n为链表的长度。在JDK1.8之后,为了解决哈希冲突频繁的问题,在原来的基础上又引入了红黑树,当链表长度超过8时,链表便会转化为红黑树结构,查找的时间复杂度从O(1+n)变成了O(1+lgn),大大提高了查询效率,不必再遍历链表。

  • JDK1.8之前的HashMap结构 Snipaste_20200608_234022.png

  • JDK1.8的HashMap结构 Snipaste_20200608_234101.png

HashMap的功能

HashMap中的方法很多,这里主要从hash,put,get,resize几个点深入研究

hash

java
static final int hash(Object key) {
+    int h;
+    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
+}
static final int hash(Object key) {
+    int h;
+    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
+}

首先,我们可以发现HashMap是允许null键的,而HashTable则不支持。

hash方法的本质是对key.hashCode()key.hashCode()>>>16进行异或运算,>>>为无符号右移运算符,就是将补码右移后高位补0。^异或是参与运算的数每一位上的数字对比,相同结果为0,不同结果为1。

所以hash方法的功能就是对高16bit和低16bit进行异或运算来减少碰撞。

put方法

java
public V put(K key, V value) {
+    return putVal(hash(key), key, value, false, true);
+}
public V put(K key, V value) {
+    return putVal(hash(key), key, value, false, true);
+}

putVal方法

java
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
+               boolean evict) {
+    Node<K,V>[] tab; Node<K,V> p; int n, i;
+    //如果table为空或者为初始化则进行初始化
+    if ((tab = table) == null || (n = tab.length) == 0)
+        n = (tab = resize()).length;
+    //计算插入的索引值(n- 1) & hash,如果数组中这个索引位置为空 则直接插入,next指针指向为null
+    if ((p = tab[i = (n - 1) & hash]) == null)
+        tab[i] = newNode(hash, key, value, null);
+    else {
+        //如果key已经存在了,直接覆盖value
+        Node<K,V> e; K k;
+        if (p.hash == hash &&
+            ((k = p.key) == key || (key != null && key.equals(k))))
+            //把第一个元素赋值给e记录
+            e = p;
+        //如果是红黑树
+        else if (p instanceof TreeNode)
+            //插入红黑树
+            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
+        else {
+            //如果为链表,进行遍历直到下一个节点为null或者key存在
+            for (int binCount = 0; ; ++binCount) {
+                if ((e = p.next) == null) {
+                    //插入链表最末端
+                    p.next = newNode(hash, key, value, null);
+                    //如果链表长度达到阈值 转换为红黑树
+                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
+                        treeifyBin(tab, hash);
+                    break;
+                }
+                //判断链表中的节点key与即将插入的key是否相同
+                if (e.hash == hash &&
+                    ((k = e.key) == key || (key != null && key.equals(k))))
+                    break;
+                //和前面的e = p.next对应 用来遍历链表
+                p = e;
+            }
+        }
+        //如果key存在,则覆盖原来的value,返回oldValue
+        if (e != null) { // existing mapping for key
+            V oldValue = e.value;
+            if (!onlyIfAbsent || oldValue == null)
+                e.value = value;
+            //访问后的回调函数
+            afterNodeAccess(e);
+            return oldValue;
+        }
+    }
+    //记录结构修改
+    ++modCount;
+    //如果哈希表中的键值对数量达到阈值,进行扩容
+    if (++size > threshold)
+        resize();
+    //插入后的回调函数
+    afterNodeInsertion(evict);
+    return null;
+}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
+               boolean evict) {
+    Node<K,V>[] tab; Node<K,V> p; int n, i;
+    //如果table为空或者为初始化则进行初始化
+    if ((tab = table) == null || (n = tab.length) == 0)
+        n = (tab = resize()).length;
+    //计算插入的索引值(n- 1) & hash,如果数组中这个索引位置为空 则直接插入,next指针指向为null
+    if ((p = tab[i = (n - 1) & hash]) == null)
+        tab[i] = newNode(hash, key, value, null);
+    else {
+        //如果key已经存在了,直接覆盖value
+        Node<K,V> e; K k;
+        if (p.hash == hash &&
+            ((k = p.key) == key || (key != null && key.equals(k))))
+            //把第一个元素赋值给e记录
+            e = p;
+        //如果是红黑树
+        else if (p instanceof TreeNode)
+            //插入红黑树
+            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
+        else {
+            //如果为链表,进行遍历直到下一个节点为null或者key存在
+            for (int binCount = 0; ; ++binCount) {
+                if ((e = p.next) == null) {
+                    //插入链表最末端
+                    p.next = newNode(hash, key, value, null);
+                    //如果链表长度达到阈值 转换为红黑树
+                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
+                        treeifyBin(tab, hash);
+                    break;
+                }
+                //判断链表中的节点key与即将插入的key是否相同
+                if (e.hash == hash &&
+                    ((k = e.key) == key || (key != null && key.equals(k))))
+                    break;
+                //和前面的e = p.next对应 用来遍历链表
+                p = e;
+            }
+        }
+        //如果key存在,则覆盖原来的value,返回oldValue
+        if (e != null) { // existing mapping for key
+            V oldValue = e.value;
+            if (!onlyIfAbsent || oldValue == null)
+                e.value = value;
+            //访问后的回调函数
+            afterNodeAccess(e);
+            return oldValue;
+        }
+    }
+    //记录结构修改
+    ++modCount;
+    //如果哈希表中的键值对数量达到阈值,进行扩容
+    if (++size > threshold)
+        resize();
+    //插入后的回调函数
+    afterNodeInsertion(evict);
+    return null;
+}

流程梳理:

  1. 计算key的hash值 (h = key.hashCode() ^ h >>> 16)

  2. 根据hash值和table数组的长度n计算插入table数组的索引,(n - 1) & hash。由于n始终是2的n次方(为什么后面会介绍),所以(n - 1) & hash 相等于 hash % n ,但是位运算要比取模效率更高

  3. 多种情况

    • 如果该位置没有数据,新生成一个节点保存新数据,返回null

    • 如果该位置有数据且是红黑树结构,那么执行相应的插入 / 更新操作;

    • 如果该位置有数据且是链表

      • 该链表没有这个节点,采用尾插法新增节点保存新数据,返回null
      • 链表上有这个节点,比较key.hash是否一致,一致则覆盖value,返回oldValue

流程图: Snipaste_20200620_163737.png

get

java
public V get(Object key) {
+    Node<K,V> e;
+    return (e = getNode(hash(key), key)) == null ? null : e.value;
+}
+
+final Node<K,V> getNode(int hash, Object key) {
+    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
+    // 1. 定位键值对所在桶的位置
+    if ((tab = table) != null && (n = tab.length) > 0 &&
+        (first = tab[(n - 1) & hash]) != null) {
+        if (first.hash == hash && // always check first node
+            ((k = first.key) == key || (key != null && key.equals(k))))
+            return first;
+        if ((e = first.next) != null) {
+            // 2. 如果 first 是 TreeNode 类型,则调用黑红树查找方法
+            if (first instanceof TreeNode)
+                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
+                
+            // 2. 对链表进行查找
+            do {
+                if (e.hash == hash &&
+                    ((k = e.key) == key || (key != null && key.equals(k))))
+                    return e;
+            } while ((e = e.next) != null);
+        }
+    }
+    return null;
+}
public V get(Object key) {
+    Node<K,V> e;
+    return (e = getNode(hash(key), key)) == null ? null : e.value;
+}
+
+final Node<K,V> getNode(int hash, Object key) {
+    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
+    // 1. 定位键值对所在桶的位置
+    if ((tab = table) != null && (n = tab.length) > 0 &&
+        (first = tab[(n - 1) & hash]) != null) {
+        if (first.hash == hash && // always check first node
+            ((k = first.key) == key || (key != null && key.equals(k))))
+            return first;
+        if ((e = first.next) != null) {
+            // 2. 如果 first 是 TreeNode 类型,则调用黑红树查找方法
+            if (first instanceof TreeNode)
+                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
+                
+            // 2. 对链表进行查找
+            do {
+                if (e.hash == hash &&
+                    ((k = e.key) == key || (key != null && key.equals(k))))
+                    return e;
+            } while ((e = e.next) != null);
+        }
+    }
+    return null;
+}

resize

java
final Node<K,V>[] resize() {
+        //oldTable指向当前hash桶数组
+        Node<K,V>[] oldTab = table;
+        int oldCap = (oldTab == null) ? 0 : oldTab.length;
+        int oldThr = threshold;
+        int newCap, newThr = 0;
+        if (oldCap > 0) {//如果旧的hash桶不为空
+            if (oldCap >= MAXIMUM_CAPACITY) {
+                //如果大于最大容量,则阈值设置为最大整数的值 
+                threshold = Integer.MAX_VALUE;
+                return oldTab;
+            }
+            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
+                     oldCap >= DEFAULT_INITIAL_CAPACITY)
+                //如果旧的hash桶容量扩容一次(左移1位)后小于最大值,并且旧的桶容量大于默认值16
+                //新桶容量 = 旧桶容量*2
+                newThr = oldThr << 1; // double threshold
+        }
+        else if (oldThr > 0) // initial capacity was placed in threshold
+            newCap = oldThr;
+        else {               // zero initial threshold signifies using defaults
+            //初始化
+            newCap = DEFAULT_INITIAL_CAPACITY;
+            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
+        }
+        if (newThr == 0) {
+            float ft = (float)newCap * loadFactor;
+            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
+                      (int)ft : Integer.MAX_VALUE);
+        }
+        threshold = newThr;
+        @SuppressWarnings({"rawtypes","unchecked"})
+        //初始化一个容量为newCap的新hash桶数组
+        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
+        //将新桶复制给旧的hash桶数组
+        table = newTab;
+        if (oldTab != null) {
+            //如果旧的hash桶不为空,则开始扩容操作
+            for (int j = 0; j < oldCap; ++j) {
+                Node<K,V> e;
+                if ((e = oldTab[j]) != null) {//旧桶j处节点值赋值给e,如果不为空,将旧桶j节点设置为空
+                    oldTab[j] = null;
+                    if (e.next == null)//如果e后面没有节点
+                        newTab[e.hash & (newCap - 1)] = e;//直接对e的hash值对新数组长度求模获得hash桶中存储位置
+                    else if (e instanceof TreeNode)//如果e为红黑树节点,将e添加到红黑树中
+                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
+                    else { // preserve order //如果是链表
+                        Node<K,V> loHead = null, loTail = null;
+                        Node<K,V> hiHead = null, hiTail = null;
+                        Node<K,V> next;
+                        do {
+                            next = e.next;//将e的下一节点赋值给next
+                            if ((e.hash & oldCap) == 0) {//如果e的hash值对旧桶长度求模为0
+                                if (loTail == null)
+                                    loHead = e;//如果loTail为null,将e赋给loHead
+                                else
+                                    loTail.next = e;//非则将e赋值给loTail的next节点
+                                loTail = e;//将e赋值给loTail节点
+                            }
+                            else {//如果e的hash值对旧桶长度取模不为0
+                                if (hiTail == null)
+                                    hiHead = e;//hiHead为null,将e赋值给hiHead
+                                else
+                                    hiTail.next = e;//否则e赋值给hiTail.next
+                                hiTail = e;//将e赋值给hiTail
+                            }
+                        } while ((e = next) != null);//直到e为空
+                        if (loTail != null) {
+                            //如果loTail不为空,将loTail的下一节点设置为null
+                            loTail.next = null;
+                            //将loHead赋值给新桶的j处
+                            newTab[j] = loHead;
+                        }
+                        if (hiTail != null) {
+                            //如果hiTail不为空,将hiTail的下一节点设置为null
+                            hiTail.next = null;
+                            //hiHead赋值给新桶的j+旧桶数组长度处
+                            newTab[j + oldCap] = hiHead;
+                        }
+                    }
+                }
+            }
+        }
+        return newTab;
+    }
final Node<K,V>[] resize() {
+        //oldTable指向当前hash桶数组
+        Node<K,V>[] oldTab = table;
+        int oldCap = (oldTab == null) ? 0 : oldTab.length;
+        int oldThr = threshold;
+        int newCap, newThr = 0;
+        if (oldCap > 0) {//如果旧的hash桶不为空
+            if (oldCap >= MAXIMUM_CAPACITY) {
+                //如果大于最大容量,则阈值设置为最大整数的值 
+                threshold = Integer.MAX_VALUE;
+                return oldTab;
+            }
+            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
+                     oldCap >= DEFAULT_INITIAL_CAPACITY)
+                //如果旧的hash桶容量扩容一次(左移1位)后小于最大值,并且旧的桶容量大于默认值16
+                //新桶容量 = 旧桶容量*2
+                newThr = oldThr << 1; // double threshold
+        }
+        else if (oldThr > 0) // initial capacity was placed in threshold
+            newCap = oldThr;
+        else {               // zero initial threshold signifies using defaults
+            //初始化
+            newCap = DEFAULT_INITIAL_CAPACITY;
+            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
+        }
+        if (newThr == 0) {
+            float ft = (float)newCap * loadFactor;
+            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
+                      (int)ft : Integer.MAX_VALUE);
+        }
+        threshold = newThr;
+        @SuppressWarnings({"rawtypes","unchecked"})
+        //初始化一个容量为newCap的新hash桶数组
+        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
+        //将新桶复制给旧的hash桶数组
+        table = newTab;
+        if (oldTab != null) {
+            //如果旧的hash桶不为空,则开始扩容操作
+            for (int j = 0; j < oldCap; ++j) {
+                Node<K,V> e;
+                if ((e = oldTab[j]) != null) {//旧桶j处节点值赋值给e,如果不为空,将旧桶j节点设置为空
+                    oldTab[j] = null;
+                    if (e.next == null)//如果e后面没有节点
+                        newTab[e.hash & (newCap - 1)] = e;//直接对e的hash值对新数组长度求模获得hash桶中存储位置
+                    else if (e instanceof TreeNode)//如果e为红黑树节点,将e添加到红黑树中
+                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
+                    else { // preserve order //如果是链表
+                        Node<K,V> loHead = null, loTail = null;
+                        Node<K,V> hiHead = null, hiTail = null;
+                        Node<K,V> next;
+                        do {
+                            next = e.next;//将e的下一节点赋值给next
+                            if ((e.hash & oldCap) == 0) {//如果e的hash值对旧桶长度求模为0
+                                if (loTail == null)
+                                    loHead = e;//如果loTail为null,将e赋给loHead
+                                else
+                                    loTail.next = e;//非则将e赋值给loTail的next节点
+                                loTail = e;//将e赋值给loTail节点
+                            }
+                            else {//如果e的hash值对旧桶长度取模不为0
+                                if (hiTail == null)
+                                    hiHead = e;//hiHead为null,将e赋值给hiHead
+                                else
+                                    hiTail.next = e;//否则e赋值给hiTail.next
+                                hiTail = e;//将e赋值给hiTail
+                            }
+                        } while ((e = next) != null);//直到e为空
+                        if (loTail != null) {
+                            //如果loTail不为空,将loTail的下一节点设置为null
+                            loTail.next = null;
+                            //将loHead赋值给新桶的j处
+                            newTab[j] = loHead;
+                        }
+                        if (hiTail != null) {
+                            //如果hiTail不为空,将hiTail的下一节点设置为null
+                            hiTail.next = null;
+                            //hiHead赋值给新桶的j+旧桶数组长度处
+                            newTab[j + oldCap] = hiHead;
+                        }
+                    }
+                }
+            }
+        }
+        return newTab;
+    }

为什么table的长度总是2的n次幂

  • tableSizeFor(int cap)方法
java
static final int tableSizeFor(int cap) {
+        int n = cap - 1;
+        n |= n >>> 1;
+        n |= n >>> 2;
+        n |= n >>> 4;
+        n |= n >>> 8;
+        n |= n >>> 16;
+        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
+    }
static final int tableSizeFor(int cap) {
+        int n = cap - 1;
+        n |= n >>> 1;
+        n |= n >>> 2;
+        n |= n >>> 4;
+        n |= n >>> 8;
+        n |= n >>> 16;
+        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
+    }

测试类

java
public class Test {
+    public static void main(String[] args) {
+
+        int cap = 65538;
+        System.out.println(Integer.toBinaryString(cap));
+        System.out.println(Integer.toBinaryString(cap-1));
+        int i = tableSizeFor(cap);
+        System.out.println(Integer.toBinaryString(i));
+    }
+
+    static final int tableSizeFor(int cap) {
+        int n = cap - 1;
+        n |= n >>> 1;
+        System.out.println(Integer.toBinaryString(n));
+        n |= n >>> 2;
+        System.out.println(Integer.toBinaryString(n));
+        n |= n >>> 4;
+        System.out.println(Integer.toBinaryString(n));
+        n |= n >>> 8;
+        System.out.println(Integer.toBinaryString(n));
+        n |= n >>> 16;
+        System.out.println(Integer.toBinaryString(n));
+        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
+    }
+}
public class Test {
+    public static void main(String[] args) {
+
+        int cap = 65538;
+        System.out.println(Integer.toBinaryString(cap));
+        System.out.println(Integer.toBinaryString(cap-1));
+        int i = tableSizeFor(cap);
+        System.out.println(Integer.toBinaryString(i));
+    }
+
+    static final int tableSizeFor(int cap) {
+        int n = cap - 1;
+        n |= n >>> 1;
+        System.out.println(Integer.toBinaryString(n));
+        n |= n >>> 2;
+        System.out.println(Integer.toBinaryString(n));
+        n |= n >>> 4;
+        System.out.println(Integer.toBinaryString(n));
+        n |= n >>> 8;
+        System.out.println(Integer.toBinaryString(n));
+        n |= n >>> 16;
+        System.out.println(Integer.toBinaryString(n));
+        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
+    }
+}

运行结果

010000000000000010
+010000000000000001
+011000000000000001
+011110000000000001
+011111111000000001
+011111111111111111
+011111111111111111
+100000000000000000
010000000000000010
+010000000000000001
+011000000000000001
+011110000000000001
+011111111000000001
+011111111111111111
+011111111111111111
+100000000000000000
  • 分析 第一次运行: 10000000000000010 n >>> 1; 01000000000000000 进行|运算 11000000000000001 把最大位的1,通过位移后移一位,并且通过|运算,组合起来 第二次运行: 11000000000000001 n >>> 2; 00110000000000000 进行|运算 11110000000000001 把最大的两位,已经变成1的,往后移动两位,并且通过|运算,组合起来 第三次运行: 11110000000000001 n >>> 4; 00001111000000000 进行|运算 11111111000000001 把最大4位,已经变成1的,往后移动4位,并且通过|运算,组合起来 第四次运行: 11111111000000001 n >>> 8; 00000000111111110 进行|运算 11111111111111111 把最大的8位,已经变成1的,往后移动8位,并且通过|运算,组合起来 第五次运算: 同上进行16位运算 第六次运算: 返回结果,格式为最高位为1其他为全为0的值,即一定是2的整数次幂
`,40),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const h=s(p,[["render",t]]);export{D as __pageData,h as default}; diff --git "a/assets/java_base_JDK1.8HashMap\346\272\220\347\240\201\345\255\246\344\271\240.md.9d40a87c.lean.js" "b/assets/java_base_JDK1.8HashMap\346\272\220\347\240\201\345\255\246\344\271\240.md.9d40a87c.lean.js" new file mode 100644 index 000000000..1b8954141 --- /dev/null +++ "b/assets/java_base_JDK1.8HashMap\346\272\220\347\240\201\345\255\246\344\271\240.md.9d40a87c.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const D=JSON.parse('{"title":"JDK1.8 HashMap源码学习","description":"","frontmatter":{},"headers":[],"relativePath":"java/base/JDK1.8HashMap源码学习.md","filePath":"java/base/JDK1.8HashMap源码学习.md","lastUpdated":1708510102000}'),p={name:"java/base/JDK1.8HashMap源码学习.md"},o=l("",40),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const h=s(p,[["render",t]]);export{D as __pageData,h as default}; diff --git "a/assets/java_base_Java8\346\226\260\347\211\271\346\200\247\345\233\236\351\241\276.md.771d4e14.js" "b/assets/java_base_Java8\346\226\260\347\211\271\346\200\247\345\233\236\351\241\276.md.771d4e14.js" new file mode 100644 index 000000000..437278599 --- /dev/null +++ "b/assets/java_base_Java8\346\226\260\347\211\271\346\200\247\345\233\236\351\241\276.md.771d4e14.js" @@ -0,0 +1,133 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"Java8新特性回顾","description":"","frontmatter":{},"headers":[],"relativePath":"java/base/Java8新特性回顾.md","filePath":"java/base/Java8新特性回顾.md","lastUpdated":1708510102000}'),p={name:"java/base/Java8新特性回顾.md"},o=l(`

Java8新特性回顾

lambda表达式

Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)

语法

java
(parameters) -> expression
+
+(parameters) ->{ statements; }
(parameters) -> expression
+
+(parameters) ->{ statements; }

以下是lambda表达式的重要特征:

  • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定表达式返回了一个数值。

示例

java
// 1. 不需要参数,返回值为 5  
+() -> 5  
+  
+// 2. 接收一个参数(数字类型),返回其2倍的值  
+x -> 2 * x  
+  
+// 3. 接受2个参数(数字),并返回他们的差值  
+(x, y) -> x – y  
+  
+// 4. 接收2个int型整数,返回他们的和  
+(int x, int y) -> x + y  
+  
+// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)  
+(String s) -> System.out.print(s)
// 1. 不需要参数,返回值为 5  
+() -> 5  
+  
+// 2. 接收一个参数(数字类型),返回其2倍的值  
+x -> 2 * x  
+  
+// 3. 接受2个参数(数字),并返回他们的差值  
+(x, y) -> x – y  
+  
+// 4. 接收2个int型整数,返回他们的和  
+(int x, int y) -> x + y  
+  
+// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)  
+(String s) -> System.out.print(s)

变量作用域

  • lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在 lambda 内部修改定义在域外的局部变量,否则会编译错误。
  • lambda 表达式的局部变量可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有 final 的语义)
  • 在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量。

lambda表达和匿名内部类的区别

第一点:所需类型不同

匿名内部类:可以是接口,也可以是抽象类,还可以是具体类。 **Lambda表达式:**只能是接口。

第二点:使用限制不同

1,如果接口中有且仅有一个抽象方法,可以使用Lambda表达式,也可以使用匿名内部类。 2,如果接口中有多于一个抽象方法,只能使用匿名内部类,不可以使用Lambda表达式。

第三点:实现原理不同

匿名内部类:编译之后会产生一个单独的.class字节码文件。 Lambda表达式:编译之后没有产生一个单独的.class字节码文件,对应的字节码文件会在运行的时候动态生成。

函数式接口

函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。

函数式接口可以被隐式转换为 lambda 表达式。

Lambda 表达式和方法引用(实际上也可认为是Lambda表达式)上。

java
@FunctionalInterface
+interface GreetingService 
+{
+    void sayMessage(String message);
+}
+
+---
+  
+GreetingService greetService1 = message -> System.out.println("Hello " + message);
@FunctionalInterface
+interface GreetingService 
+{
+    void sayMessage(String message);
+}
+
+---
+  
+GreetingService greetService1 = message -> System.out.println("Hello " + message);

Stream

Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。

这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。

元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。

txt
+--------------------+       +------+   +------+   +---+   +-------+
+| stream of elements +-----> |filter+-> |sorted+-> |map+-> |collect|
++--------------------+       +------+   +------+   +---+   +-------+
+--------------------+       +------+   +------+   +---+   +-------+
+| stream of elements +-----> |filter+-> |sorted+-> |map+-> |collect|
++--------------------+       +------+   +------+   +---+   +-------+

以上的流程转换为 Java 代码为:

java
List<Integer> transactionsIds = 
+widgets.stream()
+             .filter(b -> b.getColor() == RED)
+             .sorted((x,y) -> x.getWeight() - y.getWeight())
+             .mapToInt(Widget::getWeight)
+             .sum();
List<Integer> transactionsIds = 
+widgets.stream()
+             .filter(b -> b.getColor() == RED)
+             .sorted((x,y) -> x.getWeight() - y.getWeight())
+             .mapToInt(Widget::getWeight)
+             .sum();

Stream(流)是一个来自数据源的元素队列并支持聚合操作

  • 元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。
  • 数据源 流的来源。 可以是集合,数组,I/O channel等。
  • 聚合操作 类似SQL语句一样的操作, 比如filter, map, reduce, find, match, sorted等。

和以前的Collection操作不同, Stream操作还有两个基础的特征:

  • Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
  • 内部迭代: 以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现。

流的特性

  • stream不存储数据,而是按照特定的规则对数据进行计算,一般会输出结果;

  • stream不会改变数据源,通常情况下会产生一个新的集合;

  • stream具有延迟执行特性,只有调用终端操作时,中间操作才会执行

  • stream操作分为终端操作和中间操作,那么这两者分别代表什么呢? 终端操作:会消费流,这种操作会产生一个结果的,如果一个流被消费过了,那它就不能被重用的。 中间操作:中间操作会产生另一个流。因此中间操作可以用来创建执行一系列动作的管道。一个特别需要注意的点是: 中间操作不是立即发生的。相反,当在中间操作创建的新流上执行完终端操作后,中间操作指定的操作才会发生。所以中间操作是延迟发生的,中间操作的延迟行为主要是让流API能够更加高效地执行。

  • stream不可复用,对一个已经进行过终端操作的流再次调用,会抛出异常。

生成流

集合创建流:

  • stream() − 为集合创建串行流。
  • parallelStream() − 为集合创建并行流。

数组创建流:

  • Arrays.stream(arr)
  • Stream.of(arr)

常用方法

  • foreach:迭代
  • map:将元素映射为另一种元素
  • filter:过滤元素
  • limit:获取制定数量的流
  • count:统计数量
  • max:最大值
  • min:最小值
  • sorted:排序,可以使用自然排序或特定比较器。
  • reduce:把一个流缩减为一个值,比如对一个集合求和等
  • collect:collect操作可接收各种方法为参数,将流中数据汇总,例如Collectors.toList()。
  • joining:将元素用特定字符链接起来
  • concat:可以组合两个流

Optional

Optional 类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。

Optional 是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。

Optional 类的引入很好的解决空指针异常。

示例

java
public class Java8Tester {
+   public static void main(String args[]){
+   
+      Java8Tester java8Tester = new Java8Tester();
+      Integer value1 = null;
+      Integer value2 = new Integer(10);
+        
+      // Optional.ofNullable - 允许传递为 null 参数
+      Optional<Integer> a = Optional.ofNullable(value1);
+        
+      // Optional.of - 如果传递的参数是 null,抛出异常 NullPointerException
+      Optional<Integer> b = Optional.of(value2);
+      System.out.println(java8Tester.sum(a,b));
+   }
+    
+   public Integer sum(Optional<Integer> a, Optional<Integer> b){
+    
+      // Optional.isPresent - 判断值是否存在
+        
+      System.out.println("第一个参数值存在: " + a.isPresent());
+      System.out.println("第二个参数值存在: " + b.isPresent());
+        
+      // Optional.orElse - 如果值存在,返回它,否则返回默认值
+      Integer value1 = a.orElse(new Integer(0));
+        
+      //Optional.get - 获取值,值需要存在
+      Integer value2 = b.get();
+      return value1 + value2;
+   }
+}
+
+--- 
+  
+  
+第一个参数值存在: false
+第二个参数值存在: true
+10
public class Java8Tester {
+   public static void main(String args[]){
+   
+      Java8Tester java8Tester = new Java8Tester();
+      Integer value1 = null;
+      Integer value2 = new Integer(10);
+        
+      // Optional.ofNullable - 允许传递为 null 参数
+      Optional<Integer> a = Optional.ofNullable(value1);
+        
+      // Optional.of - 如果传递的参数是 null,抛出异常 NullPointerException
+      Optional<Integer> b = Optional.of(value2);
+      System.out.println(java8Tester.sum(a,b));
+   }
+    
+   public Integer sum(Optional<Integer> a, Optional<Integer> b){
+    
+      // Optional.isPresent - 判断值是否存在
+        
+      System.out.println("第一个参数值存在: " + a.isPresent());
+      System.out.println("第二个参数值存在: " + b.isPresent());
+        
+      // Optional.orElse - 如果值存在,返回它,否则返回默认值
+      Integer value1 = a.orElse(new Integer(0));
+        
+      //Optional.get - 获取值,值需要存在
+      Integer value2 = b.get();
+      return value1 + value2;
+   }
+}
+
+--- 
+  
+  
+第一个参数值存在: false
+第二个参数值存在: true
+10
`,44),e=[o];function t(c,r,E,y,i,g){return a(),n("div",null,e)}const F=s(p,[["render",t]]);export{u as __pageData,F as default}; diff --git "a/assets/java_base_Java8\346\226\260\347\211\271\346\200\247\345\233\236\351\241\276.md.771d4e14.lean.js" "b/assets/java_base_Java8\346\226\260\347\211\271\346\200\247\345\233\236\351\241\276.md.771d4e14.lean.js" new file mode 100644 index 000000000..6ad59bb71 --- /dev/null +++ "b/assets/java_base_Java8\346\226\260\347\211\271\346\200\247\345\233\236\351\241\276.md.771d4e14.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"Java8新特性回顾","description":"","frontmatter":{},"headers":[],"relativePath":"java/base/Java8新特性回顾.md","filePath":"java/base/Java8新特性回顾.md","lastUpdated":1708510102000}'),p={name:"java/base/Java8新特性回顾.md"},o=l("",44),e=[o];function t(c,r,E,y,i,g){return a(),n("div",null,e)}const F=s(p,[["render",t]]);export{u as __pageData,F as default}; diff --git "a/assets/java_base_String\347\261\273\347\232\204\346\267\261\345\205\245\345\255\246\344\271\240.md.d1707e3c.js" "b/assets/java_base_String\347\261\273\347\232\204\346\267\261\345\205\245\345\255\246\344\271\240.md.d1707e3c.js" new file mode 100644 index 000000000..c5213fdcf --- /dev/null +++ "b/assets/java_base_String\347\261\273\347\232\204\346\267\261\345\205\245\345\255\246\344\271\240.md.d1707e3c.js" @@ -0,0 +1,89 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"String类的深入学习","description":"","frontmatter":{},"headers":[],"relativePath":"java/base/String类的深入学习.md","filePath":"java/base/String类的深入学习.md","lastUpdated":1708510102000}'),p={name:"java/base/String类的深入学习.md"},o=l(`

String类的深入学习

String类

java
public final class String
+    implements java.io.Serializable, Comparable<String>, CharSequence {
+    /** The value is used for character storage. */
+    private final char value[];
+    ...
+}
public final class String
+    implements java.io.Serializable, Comparable<String>, CharSequence {
+    /** The value is used for character storage. */
+    private final char value[];
+    ...
+}

1.String类的声明

  • String类是final的,不可以被继承
  • String类底层是char型数组实现的
  • value[] 也是final的,而且是private修饰的,这就保证了String类的对象一旦被初始化就无法更改。

String对象被创建后就无法更改指的是常规方法无法更改,因为String类是由char型数组实现的,而这个数组value也是一个引用,我们可以通过暴力反射setAccessible(true),来修改value数组的内容。

2.常用构造方法

1.String str = new String();构造一个空字符串

java
public String() {
+    this.value = "".value;
+}
public String() {
+    this.value = "".value;
+}

2.String str = new String(String string)根据指定的字符串来构造新的String对象

java
public String(String original) {
+    this.value = original.value;
+    this.hash = original.hash;
+}
public String(String original) {
+    this.value = original.value;
+    this.hash = original.hash;
+}

3.String str = new String(char[] value)通过指定字符数组来构造新的String对象

java
public String(char value[]) {
+    this.value = Arrays.copyOf(value, value.length);
+}
public String(char value[]) {
+    this.value = Arrays.copyOf(value, value.length);
+}

当然,还有我们最常用的String str = "123",不过这种通过字面量形式构造对象的方式完全等同于上述的第三种形式,实际上它的实现是:

java
char[] chars = {'1','2','3'};
+String str = new String(chars);
char[] chars = {'1','2','3'};
+String str = new String(chars);

3.关于声明一个字符串是否创建了对象

  • 由字面量声明的字符串不一定创建了对象

String a = "123";是否创建了对象要看字符串常量池中,是否已经有了"123"这个对象,如果有,那么这句代码就没有创建对象,如果没有,那么就在字符串常量池中创建了一个"123"对象.

  • 通过new出来的字符串一定创建了对象

String a = new String("123"),无论字符串常量池是否有"123"这个对象,这句代码都会创建对象,区别就在于创建了一个还是两个.假如常量池中没有,那么就分别在常量池和堆区都分别创建了"123"对象。如果常量池中有该对象,那么就只在堆区中创建一个"123"对象。

字符串常量池

1.字符串常量池的设计思想

  • 字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能

  • JVM为了提高性能和减少内存开销,为字符串开辟一个字符串常量池,类似于缓存区,创建字符串常量时,首先判断字符串常量池是否存在该字符串.如果存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中

  • 实现的基础:字符串不可变

2.字符串常量池的位置

字符串常量池在JDK1.6之前是存放在Perm区的(永久代),也就是我们常说的方法区,而在JDK1.7之后,字符串常量池已经被移到了Heap区(堆区)存放,而在JDK1.8,Perm区已经被移除了,取而代之是元空间。

我们常说的方法区,其实是JVM中提出的规范,永久代和方法区的关系,类似我们Java中的类与接口,方法区是一个接口,制定了规范,而永久代是HotSpot虚拟机对这个规范的实现

并且,字符串常量池相较其他常量池有着特殊性:

  • 直接使用字面量声明的String对象,如果在常量池中不存在,那么就会直接存储在字符串常量池中,如果存在,那么就会直接指向字符串常量池中的对象

  • 而通过new关键字创建的String对象,如果在常量池中不存在,可以通过native方法intern手动入池。而即使常量池中存在这个字符串,这个方法就不会生效.

  • 能否入池,都取决于字符串常量池中是否存在该字符串。

    • 如果不存在就会入池。
    • 如果存在,那么通过字面量声明的字符串就会直接从常量池中取值;而intern方法就没有任何效果

很重要的一点:

​ 由于JDK对于字符串常量池的改动,在JDK1.7和之后的版本,字符串常量池都在堆区中了,而且,使用intern方法入池的字符串,不会再在字符串常量池中创建一个对象,而是保存调用intern方法的这个字符串的引用。

StringBuilder和StringBuffer

众所周知,在Java中,运算符+在和字符串一起使用时的作用是拼接,而非运算,那么到底是什么原因呢,其实底层就是StringBuffer(JDK1.0)和StringBuilder(JDK1.5之后)实现的。

看了API就会发现,StringBuilder和StringBuffer都是可变字符序列,而且两者的方法是完全一样的,唯一的区别就是线程安全问题,StringBuilder是线程不安全的,而StringBuffer是线程安全的,而StringBuffer始于JDK1.0,StringBuilder始于JDK1.5,也就是说,StringBuilder的出现,就是为了在单线程条件下替换StringBuffer,也就意味着,在不考虑线程安全问题的情况下,我们通常都会使用StringBuilder,因为没有线程问题的影响,StringBuilder的速度更快。

再说回字符串拼接的问题,

java
String a = "1";
+String b = "2";
+String c = "3";
+String d = a+b+c;
+System.out.print(d);//"123"
String a = "1";
+String b = "2";
+String c = "3";
+String d = a+b+c;
+System.out.print(d);//"123"

上面这个代码片段的底层实现,其实是:

java
String d = (new StringBuilder(String.valueof(a))).append(b).append(c).toString();
String d = (new StringBuilder(String.valueof(a))).append(b).append(c).toString();

换言之,字符串拼接,其实是创建了一个新的StringBuilder的对象,来调用append方法进行拼接,拼接完成后再调用toString方法来返回一个新的字符串.

因此,

java
String a = "Hello";
+String b = "World";
+String c = a+b;
+String d = a+b;
+System.out.println(c);
+System.out.println(d);
+System.out.println(c==d);
String a = "Hello";
+String b = "World";
+String c = a+b;
+String d = a+b;
+System.out.println(c);
+System.out.println(d);
+System.out.println(c==d);

这个代码片段的结果是HelloWorld,HelloWorld,false。

原因是StringBuilder的toString方法每次都会返回一个new出来的String对象。源码如下:

java
//StringBuilder类重写的toString方法
+@Override
+    public String toString() {
+        // Create a copy, don't share the array
+        return new String(value, 0, count);
+    }
//StringBuilder类重写的toString方法
+@Override
+    public String toString() {
+        // Create a copy, don't share the array
+        return new String(value, 0, count);
+    }

关于String类的面试题

题目1:

String str = new String("123")一共创建了几个对象

答:

1.假如字符串常量池中没有"123"这个字符串,那么这条代码就创建了两个对象,第一个是在字符串常量池中创建了字符串对象"123",然后在堆区创建了一个字符串对象"123",接着会把堆区这个"123"的引用地址值赋给在栈区声明的str。

2.假如字符串常量池中有"123"这个字符串,那么就只创建了一个对象,就是在堆区中创建了对象"123",然后把地址值赋给str.

题目2:

引用自:深入解析String#intern-美团技术团队

代码片段1:

java
String s1 = new String("1");
+s.intern();
+String s2 = "1";
+System.out.println(s == s2);
+
+String s3 = new String("1") + new String("1");
+s3.intern();
+String s4 = "11";
+System.out.println(s3 == s4);
String s1 = new String("1");
+s.intern();
+String s2 = "1";
+System.out.println(s == s2);
+
+String s3 = new String("1") + new String("1");
+s3.intern();
+String s4 = "11";
+System.out.println(s3 == s4);

在JDK1.6中:结果是false false

在JDK1.7中:结果是 false true

分析:

  • 在JDK1.6中:字符串常量池存储于永久代

String s1 = new String("1")首先在字符串常量池中创建了对象"1",然后在堆区创建对象"1",s1的引用指向的是堆区的对象;

s1.intern()方法,会查找字符串常量池中是否有"1"这个对象,结果里面有,所以这个方法没有生效,等于没写.

String s2 = "1",因为常量池中已经有"1"这个字符串了,所以s2指向了常量池中"1"

s1指向堆区,s2指向永久代,显然二者地址不同,结果为false

String s3 = new String("1") + new String("1");这句代码在堆区创建了两个匿名"1"对象,拼接后的等于在堆区中创建了字符串"11"对象

s3.intern()方法将"11"对象保存在了字符串常量池中

String s4 = "11",指向的是字符串常量池中"11"对象

s3指向堆区,s4指向永久代,结果为false

  • 在JDK1.7中:字符串常量池存储于堆区,且intern方法不会创建对象,而是保存堆区对象的引用

s1在常量池和堆区分别创建了对象"1",s1指向的是堆区的"1"对象,s.intern()方法无效,s2指向的是常量池中的"1"对象,s1和s2指向地址不同,所以是false;

s3在堆区创建了对象"11",s3.intern()将堆区对象存在常量池中,但是!!! 这里的是将堆区中"11"的引用存在了常量池,而非创建对象.

所以s4指向常量池中的引用,其实就是s3的引用,所以s3==s4为true。

代码片段2:

java
String s1 = new String("1");
+String s2 = "1";
+s1.intern();
+System.out.println(s == s2);
+
+String s3 = new String("1") + new String("1");
+String s4 = "11";
+s3.intern();
+System.out.println(s3 == s4);
String s1 = new String("1");
+String s2 = "1";
+s1.intern();
+System.out.println(s == s2);
+
+String s3 = new String("1") + new String("1");
+String s4 = "11";
+s3.intern();
+System.out.println(s3 == s4);

JDK1.6结果:false false

JDK1.7结果:false false

分析:

  • 在JDK1.6中:字符串常量池存储于永久代

s1在常量池和堆区分别创建了对象"1",s1指堆区的对象"1";

s2指向的是常量池的对象"1"

因为常量池中有"1"这个对象,所以s1.intern()无效

s1,s2二者指向地址不同,所以是false.

s3在堆区创建了对象"11"

s4在常量池创建了对象"11",并指向了常量池中的"11"对象

常量池中已经有了对象"11",s3.intern()无效

s3,s4指向不同,所以false

  • 在JDK1.7中:字符串常量池存储于堆区,且intern方法不会创建对象,而是保存堆区对象的引用

s1在常量池和堆区分别创建了对象"1",s1指堆区的对象"1";

s2指向的是常量池的对象"1"

因为常量池中有"1"这个对象,所以s1.intern()无效

s1,s2二者指向地址不同,所以是false.

s3在堆区创建了对象"11"

s4在常量池创建了对象"11",并指向了常量池中的"11"对象

常量池中已经有了对象"11",s3.intern()无效

s3,s4指向不同,所以false

`,92),t=[o];function e(r,c,E,i,y,u){return n(),a("div",null,t)}const q=s(p,[["render",e]]);export{d as __pageData,q as default}; diff --git "a/assets/java_base_String\347\261\273\347\232\204\346\267\261\345\205\245\345\255\246\344\271\240.md.d1707e3c.lean.js" "b/assets/java_base_String\347\261\273\347\232\204\346\267\261\345\205\245\345\255\246\344\271\240.md.d1707e3c.lean.js" new file mode 100644 index 000000000..5fc4a4ff3 --- /dev/null +++ "b/assets/java_base_String\347\261\273\347\232\204\346\267\261\345\205\245\345\255\246\344\271\240.md.d1707e3c.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"String类的深入学习","description":"","frontmatter":{},"headers":[],"relativePath":"java/base/String类的深入学习.md","filePath":"java/base/String类的深入学习.md","lastUpdated":1708510102000}'),p={name:"java/base/String类的深入学习.md"},o=l("",92),t=[o];function e(r,c,E,i,y,u){return n(),a("div",null,t)}const q=s(p,[["render",e]]);export{d as __pageData,q as default}; diff --git "a/assets/java_database_MySql\347\264\242\345\274\225.md.46f173cb.js" "b/assets/java_database_MySql\347\264\242\345\274\225.md.46f173cb.js" new file mode 100644 index 000000000..d83c98885 --- /dev/null +++ "b/assets/java_database_MySql\347\264\242\345\274\225.md.46f173cb.js" @@ -0,0 +1,23 @@ +import{_ as s,o as a,c as l,Q as n}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"MySql索引","description":"","frontmatter":{},"headers":[],"relativePath":"java/database/MySql索引.md","filePath":"java/database/MySql索引.md","lastUpdated":1708510102000}'),p={name:"java/database/MySql索引.md"},e=n(`

MySql索引

本篇内容基于MySQL的InnoDB存储引擎。

索引的概念

索引是一个单独的、存储在磁盘上的数据库结构,它们包含着对数据表里所有记录的引用指针。使用索引用于快速找出在某个或多个列中有一特定值的行,所有MySQL列类型都可以被索引,==对相关列使用索引是提高查询操作速度的最佳途径==。

InnoDB存储引擎中的索引都是指BTree索引,MySQL中还有Hash索引,详见官网存储引擎索引类型

12712542020101314214516280980408.png

索引的原理

内容引用自美团技术团队发表的文章MySQL索引原理及慢查询优化

索引的目的在于提高查询效率,可以类比字典,如果要查“mysql”这个单词,我们肯定需要定位到m字母,然后从下往下找到y字母,再找到剩下的sql。如果没有索引,那么你可能需要把所有单词看一遍才能找到你想要的,如果我想找到m开头的单词呢?或者ze开头的单词呢?是不是觉得如果没有索引,这个事情根本无法完成?

数据库的索引结构需要做的事:每次查找数据时把磁盘IO次数控制在一个很小的数量级,最好是常数数量级。那么我们就想到如果一个高度可控的多路搜索树是否能满足需求呢?就这样,b+树应运而生。

b树.jpg 如上图,是一颗b+树,关于b+树的定义可以参见B+树,这里只说一些重点,浅蓝色的块我们称之为一个磁盘块,可以看到每个磁盘块包含几个数据项(深蓝色所示)和指针(黄色所示),如磁盘块1包含数据项17和35,包含指针P1、P2、P3,P1表示小于17的磁盘块,P2表示在17和35之间的磁盘块,P3表示大于35的磁盘块。真实的数据存在于叶子节点即3、5、9、10、13、15、28、29、36、60、75、79、90、99。非叶子节点只不存储真实的数据,只存储指引搜索方向的数据项,如17、35并不真实存在于数据表中。

b+树的查找过程

如图所示,如果要查找数据项29,那么首先会把磁盘块1由磁盘加载到内存,此时发生一次IO,在内存中用二分查找确定29在17和35之间,锁定磁盘块1的P2指针,内存时间因为非常短(相比磁盘的IO)可以忽略不计,通过磁盘块1的P2指针的磁盘地址把磁盘块3由磁盘加载到内存,发生第二次IO,29在26和30之间,锁定磁盘块3的P2指针,通过指针加载磁盘块8到内存,发生第三次IO,同时内存中做二分查找找到29,结束查询,总计三次IO。真实的情况是,3层的b+树可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,那么总共需要百万次的IO,显然成本非常非常高。

b+树性质

1.通过上面的分析,我们知道IO次数取决于b+数的高度h,假设当前数据表的数据为N,每个磁盘块的数据项的数量是m,则有h=㏒(m+1)N,当数据量N一定的情况下,m越大,h越小;而m = 磁盘块的大小 / 数据项的大小,磁盘块的大小也就是一个数据页的大小,是固定的,如果数据项占的空间越小,数据项的数量越多,树的高度越低。这就是为什么每个数据项,即索引字段要尽量的小,比如int占4字节,要比bigint8字节少一半。这也是为什么b+树要求把真实的数据放到叶子节点而不是内层节点,一旦放到内层节点,磁盘块的数据项会大幅度下降,导致树增高。当数据项等于1时将会退化成线性表。

2.当b+树的数据项是复合的数据结构,比如(name,age,sex)的时候,b+数是按照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样的数据来检索的时候,b+树会优先比较name来确定下一步的所搜方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了, 这个是非常重要的性质,即索引的最左匹配特性。

索引的类别

1.Primary Key(主键索引)

主键索引是一种特殊的唯一索引,这个时候需要一个表只能有一个主键,不允许有空值。一般是在建表的时候同时创建主键索引

InnoDB存储引擎的表,如果建表的时候没有指定主键,则会使用第一非空的唯一索引作为聚集索引,如果没有主键也没有合适的唯一索引,那么innodb内部会生成一个隐藏的主键作为聚集索引,这个隐藏的主键是一个6个字节的列,该列的值会随着数据的插入自增。

在其他存储引擎或其他数据库中主键索引不一定就是聚集索引

2.Unique(唯一索引)

唯一索引列的值必须是唯一的,但允许有空值,如果是组合索引,则列值的组合必须是唯一的。

sql
CREATE UNIQUE index 索引名 on 表名(列名)
CREATE UNIQUE index 索引名 on 表名(列名)

3.Key(普通索引)

MySQL中的基本索引类型,允许在定义索引的列中插入重复值和空值

sql
CREATE index 索引名 on 表名(列名)
CREATE index 索引名 on 表名(列名)

4.组合索引

指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用组合索引时遵循最左前缀原则

sql
CREATE index 索引名 on 表名(列名,列名...)
CREATE index 索引名 on 表名(列名,列名...)

5.全文索引

全文索引类型为FULLTEXT,在定义索引的列上支持值的全文查找,允许在这些索引列中插入重复值和空值。全文索引可以在CHAR、VARCHAR或者TEXT类型的列上创建

索引的基本语法

  • 创建
sql
ALTER mytable ADD  [UNIQUE]  INDEX [indexName] ON 表名(列名)
ALTER mytable ADD  [UNIQUE]  INDEX [indexName] ON 表名(列名)
  • 删除
sql
DROP INDEX [indexName] ON 表名;
DROP INDEX [indexName] ON 表名;
  • 查看
sql
SHOW INDEX FROM 表名
SHOW INDEX FROM 表名
  • alter命令
sql
-- 有四种方式来添加数据表的索引:
+ALTER TABLE tbl_name ADD PRIMARY KEY (column_list): 该语句添加一个主键,这意味着索引值必须是唯一的,且不能为NULL。
+ 
+ALTER TABLE tbl_name ADD UNIQUE index_name (column_list): 这条语句创建索引的值必须是唯一的(除了NULL外,NULL可能会出现多次)。
+ 
+ALTER TABLE tbl_name ADD INDEX index_name (column_list): 添加普通索引,索引值可出现多次。
+ 
+ALTER TABLE tbl_name ADD FULLTEXT index_name (column_list):该语句指定了索引为 FULLTEXT ,用于全文索引。
-- 有四种方式来添加数据表的索引:
+ALTER TABLE tbl_name ADD PRIMARY KEY (column_list): 该语句添加一个主键,这意味着索引值必须是唯一的,且不能为NULL。
+ 
+ALTER TABLE tbl_name ADD UNIQUE index_name (column_list): 这条语句创建索引的值必须是唯一的(除了NULL外,NULL可能会出现多次)。
+ 
+ALTER TABLE tbl_name ADD INDEX index_name (column_list): 添加普通索引,索引值可出现多次。
+ 
+ALTER TABLE tbl_name ADD FULLTEXT index_name (column_list):该语句指定了索引为 FULLTEXT ,用于全文索引。

索引的使用场景

1.适合使用索引

  • 频繁作为查询条件的字段
  • 多表查询中与其他表进行关联的字段,外键关系建立索引
  • 单列/组合索引的选择,在高并发的场景下适合建立组合索引
  • 查询中常用于排序的字段
  • 查询中常用于分组或统计的字段

2.不适合使用索引

  • 频繁更新的字段

  • where条件中用不到的字段

  • 表记录很少

  • 重复记录非常多的表

  • 数据区分不明显的字段,例如性别栏位

索引有效场景

  • 全值匹配的查询,例如根据订单id查询select * from t_order where order_id = '9999676623'

  • 匹配范围值的查询,例如 where id > '123456'

  • 最左匹配原则,如user表的username pwd创建了组合索引那么以下几种都可以命中索引

    sql
    select username from user where username='zhangsan' and pwd ='axsedf1sd'
    +
    +select username from user where pwd ='axsedf1sd' and username='zhangsan'
    +
    +select username from user where username='zhangsan'
    select username from user where username='zhangsan' and pwd ='axsedf1sd'
    +
    +select username from user where pwd ='axsedf1sd' and username='zhangsan'
    +
    +select username from user where username='zhangsan'

    sql
    select username from user where pwd ='axsedf1sd'
    select username from user where pwd ='axsedf1sd'

    不能命中索引

  • 非前导模糊查询, 例如 where name like 'xiaoming%'

索引失效场景

  • 负向查询会使索引失效,例如 id not in (1,2,3)
  • 在索引字段进行运算会使索引失效,例如计算,函数,类型转换
  • !=或者<>会使索引失效
  • is not null无法使用索引,但是is null可以
  • 前导模糊查询会使索引失效,例如 name like '%xiaoming',但是非前导可以
  • 字符串不加单引号会使索引失效
  • 使用组合索引时不遵循最左匹配原则会使索引失效
`,41),o=[e];function t(r,c,i,y,E,d){return a(),l("div",null,o)}const m=s(p,[["render",t]]);export{u as __pageData,m as default}; diff --git "a/assets/java_database_MySql\347\264\242\345\274\225.md.46f173cb.lean.js" "b/assets/java_database_MySql\347\264\242\345\274\225.md.46f173cb.lean.js" new file mode 100644 index 000000000..5d4df0637 --- /dev/null +++ "b/assets/java_database_MySql\347\264\242\345\274\225.md.46f173cb.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as l,Q as n}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"MySql索引","description":"","frontmatter":{},"headers":[],"relativePath":"java/database/MySql索引.md","filePath":"java/database/MySql索引.md","lastUpdated":1708510102000}'),p={name:"java/database/MySql索引.md"},e=n("",41),o=[e];function t(r,c,i,y,E,d){return a(),l("div",null,o)}const m=s(p,[["render",t]]);export{u as __pageData,m as default}; diff --git "a/assets/java_database_Otter\345\256\236\347\216\260\346\225\260\346\215\256\345\205\250\351\207\217\345\242\236\351\207\217\345\220\214\346\255\245.md.892bc0fe.js" "b/assets/java_database_Otter\345\256\236\347\216\260\346\225\260\346\215\256\345\205\250\351\207\217\345\242\236\351\207\217\345\220\214\346\255\245.md.892bc0fe.js" new file mode 100644 index 000000000..a8986e2ad --- /dev/null +++ "b/assets/java_database_Otter\345\256\236\347\216\260\346\225\260\346\215\256\345\205\250\351\207\217\345\242\236\351\207\217\345\220\214\346\255\245.md.892bc0fe.js" @@ -0,0 +1,123 @@ +import{_ as s,o as a,c as o,Q as e}from"./chunks/framework.dc1f4608.js";const i=JSON.parse('{"title":"Otter实现数据全量增量同步","description":"","frontmatter":{},"headers":[],"relativePath":"java/database/Otter实现数据全量增量同步.md","filePath":"java/database/Otter实现数据全量增量同步.md","lastUpdated":1708510102000}'),n={name:"java/database/Otter实现数据全量增量同步.md"},l=e(`

Otter实现数据全量增量同步

otter是一款基于数据库增量日志解析,准实时同步到本机房或异地机房的mysql/oracle数据库. 一个分布式数据库同步系统

仓库地址:https://github.com/alibaba/otter

前置工作

  • 源库开启binlog,并且必须是ROW模式

  • 需要启动zookeeper

  • otter是基于canal的,但是otter项目本身内嵌了canal,所以无需独立启动canal-server

  • 初始化otter数据库

    • sql

    CREATE DATABASE /!32312 IF NOT EXISTS/ otter /*!40100 DEFAULT CHARACTER SET utf8 COLLATE utf8_bin */;

    USE otter;

    SET sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';

    CREATE TABLE ALARM_RULE ( ID bigint(20) unsigned NOT NULL AUTO_INCREMENT, MONITOR_NAME varchar(1024) DEFAULT NULL, RECEIVER_KEY varchar(1024) DEFAULT NULL, STATUS varchar(32) DEFAULT NULL, PIPELINE_ID bigint(20) NOT NULL, DESCRIPTION varchar(256) DEFAULT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, MATCH_VALUE varchar(1024) DEFAULT NULL, PARAMETERS text DEFAULT NULL, PRIMARY KEY (ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE AUTOKEEPER_CLUSTER ( ID bigint(20) NOT NULL AUTO_INCREMENT, CLUSTER_NAME varchar(200) NOT NULL, SERVER_LIST varchar(1024) NOT NULL, DESCRIPTION varchar(200) DEFAULT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE CANAL ( ID bigint(20) unsigned NOT NULL AUTO_INCREMENT, NAME varchar(200) DEFAULT NULL, DESCRIPTION varchar(200) DEFAULT NULL, PARAMETERS text DEFAULT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), UNIQUE KEY CANALUNIQUE (NAME) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE CHANNEL ( ID bigint(20) NOT NULL AUTO_INCREMENT, NAME varchar(200) NOT NULL, DESCRIPTION varchar(200) DEFAULT NULL, PARAMETERS text DEFAULT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), UNIQUE KEY CHANNELUNIQUE (NAME) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE COLUMN_PAIR ( ID bigint(20) NOT NULL AUTO_INCREMENT, SOURCE_COLUMN varchar(200) DEFAULT NULL, TARGET_COLUMN varchar(200) DEFAULT NULL, DATA_MEDIA_PAIR_ID bigint(20) NOT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), KEY idx_DATA_MEDIA_PAIR_ID (DATA_MEDIA_PAIR_ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE COLUMN_PAIR_GROUP ( ID bigint(20) NOT NULL AUTO_INCREMENT, DATA_MEDIA_PAIR_ID bigint(20) NOT NULL, COLUMN_PAIR_CONTENT text DEFAULT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), KEY idx_DATA_MEDIA_PAIR_ID (DATA_MEDIA_PAIR_ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE DATA_MEDIA ( ID bigint(20) NOT NULL AUTO_INCREMENT, NAME varchar(200) NOT NULL, NAMESPACE varchar(200) NOT NULL, PROPERTIES varchar(1000) NOT NULL, DATA_MEDIA_SOURCE_ID bigint(20) NOT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), UNIQUE KEY DATAMEDIAUNIQUE (NAME,NAMESPACE,DATA_MEDIA_SOURCE_ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE DATA_MEDIA_PAIR ( ID bigint(20) NOT NULL AUTO_INCREMENT, PULLWEIGHT bigint(20) DEFAULT NULL, PUSHWEIGHT bigint(20) DEFAULT NULL, RESOLVER text DEFAULT NULL, FILTER text DEFAULT NULL, SOURCE_DATA_MEDIA_ID bigint(20) DEFAULT NULL, TARGET_DATA_MEDIA_ID bigint(20) DEFAULT NULL, PIPELINE_ID bigint(20) NOT NULL, COLUMN_PAIR_MODE varchar(20) DEFAULT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), KEY idx_PipelineID (PIPELINE_ID,ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE DATA_MEDIA_SOURCE ( ID bigint(20) NOT NULL AUTO_INCREMENT, NAME varchar(200) NOT NULL, TYPE varchar(20) NOT NULL, PROPERTIES varchar(1000) NOT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), UNIQUE KEY DATAMEDIASOURCEUNIQUE (NAME) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE DELAY_STAT ( ID bigint(20) NOT NULL AUTO_INCREMENT, DELAY_TIME bigint(20) NOT NULL, DELAY_NUMBER bigint(20) NOT NULL, PIPELINE_ID bigint(20) NOT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), KEY idx_PipelineID_GmtModified_ID (PIPELINE_ID,GMT_MODIFIED,ID), KEY idx_Pipeline_GmtCreate (PIPELINE_ID,GMT_CREATE), KEY idx_GmtCreate_id (GMT_CREATE,ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE LOG_RECORD ( ID bigint(20) NOT NULL AUTO_INCREMENT, NID varchar(200) DEFAULT NULL, CHANNEL_ID varchar(200) NOT NULL, PIPELINE_ID varchar(200) NOT NULL, TITLE varchar(1000) DEFAULT NULL, MESSAGE text DEFAULT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), KEY logRecord_pipelineId (PIPELINE_ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE NODE ( ID bigint(20) NOT NULL AUTO_INCREMENT, NAME varchar(200) NOT NULL, IP varchar(200) NOT NULL, PORT bigint(20) NOT NULL, DESCRIPTION varchar(200) DEFAULT NULL, PARAMETERS text DEFAULT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), UNIQUE KEY NODEUNIQUE (NAME,IP) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE PIPELINE ( ID bigint(20) NOT NULL AUTO_INCREMENT, NAME varchar(200) NOT NULL, DESCRIPTION varchar(200) DEFAULT NULL, PARAMETERS text DEFAULT NULL, CHANNEL_ID bigint(20) NOT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), UNIQUE KEY PIPELINEUNIQUE (NAME,CHANNEL_ID), KEY idx_ChannelID (CHANNEL_ID,ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE PIPELINE_NODE_RELATION ( ID bigint(20) NOT NULL AUTO_INCREMENT, NODE_ID bigint(20) NOT NULL, PIPELINE_ID bigint(20) NOT NULL, LOCATION varchar(20) NOT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), KEY idx_PipelineID (PIPELINE_ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE SYSTEM_PARAMETER ( ID bigint(20) unsigned NOT NULL, VALUE text DEFAULT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

    CREATE TABLE TABLE_HISTORY_STAT ( ID bigint(20) unsigned NOT NULL AUTO_INCREMENT, FILE_SIZE bigint(20) DEFAULT NULL, FILE_COUNT bigint(20) DEFAULT NULL, INSERT_COUNT bigint(20) DEFAULT NULL, UPDATE_COUNT bigint(20) DEFAULT NULL, DELETE_COUNT bigint(20) DEFAULT NULL, DATA_MEDIA_PAIR_ID bigint(20) DEFAULT NULL, PIPELINE_ID bigint(20) DEFAULT NULL, START_TIME timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', END_TIME timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), KEY idx_DATA_MEDIA_PAIR_ID_END_TIME (DATA_MEDIA_PAIR_ID,END_TIME), KEY idx_GmtCreate_id (GMT_CREATE,ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE TABLE_STAT ( ID bigint(20) NOT NULL AUTO_INCREMENT, FILE_SIZE bigint(20) NOT NULL, FILE_COUNT bigint(20) NOT NULL, INSERT_COUNT bigint(20) NOT NULL, UPDATE_COUNT bigint(20) NOT NULL, DELETE_COUNT bigint(20) NOT NULL, DATA_MEDIA_PAIR_ID bigint(20) NOT NULL, PIPELINE_ID bigint(20) NOT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), KEY idx_PipelineID_DataMediaPairID (PIPELINE_ID,DATA_MEDIA_PAIR_ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE THROUGHPUT_STAT ( ID bigint(20) NOT NULL AUTO_INCREMENT, TYPE varchar(20) NOT NULL, NUMBER bigint(20) NOT NULL, SIZE bigint(20) NOT NULL, PIPELINE_ID bigint(20) NOT NULL, START_TIME timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', END_TIME timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), KEY idx_PipelineID_Type_GmtCreate_ID (PIPELINE_ID,TYPE,GMT_CREATE,ID), KEY idx_PipelineID_Type_EndTime_ID (PIPELINE_ID,TYPE,END_TIME,ID), KEY idx_GmtCreate_id (GMT_CREATE,ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE USER ( ID bigint(20) NOT NULL AUTO_INCREMENT, USERNAME varchar(20) NOT NULL, PASSWORD varchar(20) NOT NULL, AUTHORIZETYPE varchar(20) NOT NULL, DEPARTMENT varchar(20) NOT NULL, REALNAME varchar(20) NOT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), UNIQUE KEY USERUNIQUE (USERNAME) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE DATA_MATRIX ( ID bigint(20) NOT NULL AUTO_INCREMENT, GROUP_KEY varchar(200) DEFAULT NULL, MASTER varchar(200) DEFAULT NULL, SLAVE varchar(200) DEFAULT NULL, DESCRIPTION varchar(200) DEFAULT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), KEY GROUPKEY (GROUP_KEY) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE IF NOT EXISTS meta_history ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', gmt_create datetime NOT NULL COMMENT '创建时间', gmt_modified datetime NOT NULL COMMENT '修改时间', destination varchar(128) DEFAULT NULL COMMENT '通道名称', binlog_file varchar(64) DEFAULT NULL COMMENT 'binlog文件名', binlog_offest bigint(20) DEFAULT NULL COMMENT 'binlog偏移量', binlog_master_id varchar(64) DEFAULT NULL COMMENT 'binlog节点id', binlog_timestamp bigint(20) DEFAULT NULL COMMENT 'binlog应用的时间戳', use_schema varchar(1024) DEFAULT NULL COMMENT '执行sql时对应的schema', sql_schema varchar(1024) DEFAULT NULL COMMENT '对应的schema', sql_table varchar(1024) DEFAULT NULL COMMENT '对应的table', sql_text longtext DEFAULT NULL COMMENT '执行的sql', sql_type varchar(256) DEFAULT NULL COMMENT 'sql类型', extra text DEFAULT NULL COMMENT '额外的扩展信息', PRIMARY KEY (id), UNIQUE KEY binlog_file_offest(destination,binlog_master_id,binlog_file,binlog_offest), KEY destination (destination), KEY destination_timestamp (destination,binlog_timestamp), KEY gmt_modified (gmt_modified) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='表结构变化明细表';

    CREATE TABLE IF NOT EXISTS meta_snapshot ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', gmt_create datetime NOT NULL COMMENT '创建时间', gmt_modified datetime NOT NULL COMMENT '修改时间', destination varchar(128) DEFAULT NULL COMMENT '通道名称', binlog_file varchar(64) DEFAULT NULL COMMENT 'binlog文件名', binlog_offest bigint(20) DEFAULT NULL COMMENT 'binlog偏移量', binlog_master_id varchar(64) DEFAULT NULL COMMENT 'binlog节点id', binlog_timestamp bigint(20) DEFAULT NULL COMMENT 'binlog应用的时间戳', data longtext DEFAULT NULL COMMENT '表结构数据', extra text DEFAULT NULL COMMENT '额外的扩展信息', PRIMARY KEY (id), UNIQUE KEY binlog_file_offest(destination,binlog_master_id,binlog_file,binlog_offest), KEY destination (destination), KEY destination_timestamp (destination,binlog_timestamp), KEY gmt_modified (gmt_modified) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='表结构记录表快照表';

    insert into USER(ID,USERNAME,PASSWORD,AUTHORIZETYPE,DEPARTMENT,REALNAME,GMT_CREATE,GMT_MODIFIED) values(null,'admin','801fc357a5a74743894a','ADMIN','admin','admin',now(),now()); insert into USER(ID,USERNAME,PASSWORD,AUTHORIZETYPE,DEPARTMENT,REALNAME,GMT_CREATE,GMT_MODIFIED) values(null,'guest','471e02a154a2121dc577','OPERATOR','guest','guest',now(),now());

manager配置

## otter manager domain name
+otter.domainName = xxx.com
+## otter manager http port
+otter.port = 
+
+## otter manager database config
+otter.database.driver.class.name = com.mysql.jdbc.Driver
+otter.database.driver.url = jdbc:mysql://127.0.0.1:3306/otter?useUnicode=true&characterEncoding=UTF-8&useSSL=false
+otter.database.driver.username = 
+otter.database.driver.password = 
+
+## otter communication port
+otter.communication.manager.port = 1099
+## default zookeeper address
+otter.zookeeper.cluster.default = 127.0.0.1:2181
## otter manager domain name
+otter.domainName = xxx.com
+## otter manager http port
+otter.port = 
+
+## otter manager database config
+otter.database.driver.class.name = com.mysql.jdbc.Driver
+otter.database.driver.url = jdbc:mysql://127.0.0.1:3306/otter?useUnicode=true&characterEncoding=UTF-8&useSSL=false
+otter.database.driver.username = 
+otter.database.driver.password = 
+
+## otter communication port
+otter.communication.manager.port = 1099
+## default zookeeper address
+otter.zookeeper.cluster.default = 127.0.0.1:2181

修改完关键配置后即可执行bin/startup.sh启动manager服务,webUI访问时默认是游客,admin密码默认为admin ,otter没有提供权限控制,游客用户也能看到所有配置信息,因此不能暴露在公网。

坑:startup.sh中的java启动参数-Xss(单个线程栈内存)值都设置的是256k,使用jdk1.8是无法启动的,需要调成大一点例如512k

The stack size specified is too small, Specify at least 384k

切换到admin用户后需要配置zookeeper & node信息,这个node的id为1

image-20230228201401938

node配置

在node/conf下执行echo 1 > nid,调整node配置后启动node,看到node状态为已启动即可

image-20230228201429514

数据源配置

配置两个数据源,一个作为源库,一个作为目标库

数据表配置

表明可以用模糊匹配,也可以指定具体的表

canal配置

监听源库,开启tsdb监控表结构变化

配置Channel

添加channel

基于日志变更、行记录模式

添加pipeline

select和load机器直接选node,同步数据来源的canal选择刚才配置的

配置映射关系列表

添加源表和目标表的映射,视图模式的include/exclude分别代表选中的字段同步/选中的字段排除(不同步),配置好映射关系后保存

批量添加

schema1,table1,sourceId1,schema2,table2,sourceId2

schema1,table1,sourceId1为源表信息

schema2,table2,sourceId2为目标表信息

sourceId是数据源的id,在数据源配置页面可以找到

映射权重 (对应的数字越大,同步会越后面得到同步,优先同步权重小的数据)

启动channel

启动channel即可测试源库源表的数据变更后,目标库的目标表是否跟着一起更新。

全量数据同步

方案1

上述介绍的是增量同步数据的基本操作,但是往往源表中已经有了大量的存量数据需要全量同步一次。

Otter官方提供了一种叫自由门的方案可以用于:

  • 数据订正
  • 全量数据同步

原理是基于otter系统表retl_buffer,插入特定数据,otter系统感知后回根据表明和pk提取对应记录和正常增量同步数据一起同步到目标库

前提

  • 已经建好了两个表的同步channel并且启动

retl库建表sql

sql
/*
+供 otter 使用, otter 需要对 retl.* 的读写权限,以及对业务表的读写权限
+1. 创建database retl
+*/
+CREATE DATABASE retl;
+
+/* 2. 用户授权 给同步用户授权 */
+CREATE USER retl@'%' IDENTIFIED BY 'retl';
+GRANT USAGE ON *.* TO \`retl\`@'%';
+GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO \`retl\`@'%';
+GRANT SELECT, INSERT, UPDATE, DELETE, EXECUTE ON \`retl\`.* TO \`retl\`@'%';
+/* 业务表授权,这里可以限定只授权同步业务的表 */
+GRANT SELECT, INSERT, UPDATE, DELETE ON *.* TO \`retl\`@'%';  
+
+/* 3. 创建系统表 */
+USE retl;
+DROP TABLE IF EXISTS retl.retl_buffer;
+DROP TABLE IF EXISTS retl.retl_mark;
+DROP TABLE IF EXISTS retl.xdual;
+
+CREATE TABLE retl_buffer
+(	
+	ID BIGINT(20) AUTO_INCREMENT,
+	TABLE_ID INT(11) NOT NULL,
+	FULL_NAME varchar(512),
+	TYPE CHAR(1) NOT NULL,
+	PK_DATA VARCHAR(256) NOT NULL,
+	GMT_CREATE TIMESTAMP NOT NULL,
+	GMT_MODIFIED TIMESTAMP NOT NULL,
+	CONSTRAINT RETL_BUFFER_ID PRIMARY KEY (ID) 
+)  ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE retl_mark
+(	
+	ID BIGINT AUTO_INCREMENT,
+	CHANNEL_ID INT(11),
+	CHANNEL_INFO varchar(128),
+	CONSTRAINT RETL_MARK_ID PRIMARY KEY (ID) 
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
+
+CREATE TABLE xdual (
+  ID BIGINT(20) NOT NULL AUTO_INCREMENT,
+  X timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (ID)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
+
+/* 4. 插入初始化数据 */
+INSERT INTO retl.xdual(id, x) VALUES (1,now()) ON DUPLICATE KEY UPDATE x = now();
/*
+供 otter 使用, otter 需要对 retl.* 的读写权限,以及对业务表的读写权限
+1. 创建database retl
+*/
+CREATE DATABASE retl;
+
+/* 2. 用户授权 给同步用户授权 */
+CREATE USER retl@'%' IDENTIFIED BY 'retl';
+GRANT USAGE ON *.* TO \`retl\`@'%';
+GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO \`retl\`@'%';
+GRANT SELECT, INSERT, UPDATE, DELETE, EXECUTE ON \`retl\`.* TO \`retl\`@'%';
+/* 业务表授权,这里可以限定只授权同步业务的表 */
+GRANT SELECT, INSERT, UPDATE, DELETE ON *.* TO \`retl\`@'%';  
+
+/* 3. 创建系统表 */
+USE retl;
+DROP TABLE IF EXISTS retl.retl_buffer;
+DROP TABLE IF EXISTS retl.retl_mark;
+DROP TABLE IF EXISTS retl.xdual;
+
+CREATE TABLE retl_buffer
+(	
+	ID BIGINT(20) AUTO_INCREMENT,
+	TABLE_ID INT(11) NOT NULL,
+	FULL_NAME varchar(512),
+	TYPE CHAR(1) NOT NULL,
+	PK_DATA VARCHAR(256) NOT NULL,
+	GMT_CREATE TIMESTAMP NOT NULL,
+	GMT_MODIFIED TIMESTAMP NOT NULL,
+	CONSTRAINT RETL_BUFFER_ID PRIMARY KEY (ID) 
+)  ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE retl_mark
+(	
+	ID BIGINT AUTO_INCREMENT,
+	CHANNEL_ID INT(11),
+	CHANNEL_INFO varchar(128),
+	CONSTRAINT RETL_MARK_ID PRIMARY KEY (ID) 
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
+
+CREATE TABLE xdual (
+  ID BIGINT(20) NOT NULL AUTO_INCREMENT,
+  X timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (ID)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
+
+/* 4. 插入初始化数据 */
+INSERT INTO retl.xdual(id, x) VALUES (1,now()) ON DUPLICATE KEY UPDATE x = now();

全量同步操作

insert into retl.retl_buffer(ID,TABLE_ID, FULL_NAME,TYPE,PK_DATA,GMT_CREATE,GMT_MODIFIED) (select null,0,'$schema.table$','I',id,now(),now() from $schema.table$);
insert into retl.retl_buffer(ID,TABLE_ID, FULL_NAME,TYPE,PK_DATA,GMT_CREATE,GMT_MODIFIED) (select null,0,'$schema.table$','I',id,now(),now() from $schema.table$);

例如:insert into retl.retl_buffer(ID,TABLE_ID, FULL_NAME,TYPE,PK_DATA,GMT_CREATE,GMT_MODIFIED) (select null,0,'test.user','I',id,now(),now() from test.user);

方案2

上述基于数据库插入记录触发otter同步的方案,如果数据量大会比较耗时。也可以直接把源表数据导出并记录导出时binlog的position,先将全量数据导入一次目标表,再基于导出数据时的binlog的position进行增量同步。

导出数据

mysqldump -uxxx -pxxx --single-transaction --master-data=2 --databases xxx --tables xxx > data.sql

这样,导出的data.sql文件中会有一行信息,记录导出时的binlog文件和position

image-20230228203126413

导入数据

在目标库执行导入sql

配置canal的读取postion

image-20230228203443257

后续新建channel的操作和普通增量同步一样即可。

踩坑

更换zookeeper后manager webui无法访问

更换zookeeper后,manager管理页面无法进入,报错内容类似org.I0Itec.zkclient.exception.ZkNoNodeException: org.apache.zookeeper.KeeperException$NoNodeException: KeeperErrorCode = NoNode for /otter/channel/3/3/process 。原因是otter会在zookepper中存储一些节点信息,更换zookeeper后,需要复制节点数据,或者删除数据库中的channel、pipeline等表的数据内容 或者访问 http://域名:端口/system_reduction.htm,点击一键修复即可。

canal指定的binlog被清除

show master logsshow binlog events in 'binlog.000048' from 1226 limit 4; 更新canal中的位点配置重新启动

读取从库binlog

低权限用户需要授权,否则无法读取binlogGRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'xxx'@'%' IDENTIFIED BY '';

  • 从库需要设置log_slave_updates=1,将主库binlog中的操作写入到从库的binlog中,默认是关闭的,虽然数据可以同步,但是从库binlog没有记录这些内容。
`,63),p=[l];function c(E,t,r,T,d,A){return a(),o("div",null,p)}const L=s(n,[["render",c]]);export{i as __pageData,L as default}; diff --git "a/assets/java_database_Otter\345\256\236\347\216\260\346\225\260\346\215\256\345\205\250\351\207\217\345\242\236\351\207\217\345\220\214\346\255\245.md.892bc0fe.lean.js" "b/assets/java_database_Otter\345\256\236\347\216\260\346\225\260\346\215\256\345\205\250\351\207\217\345\242\236\351\207\217\345\220\214\346\255\245.md.892bc0fe.lean.js" new file mode 100644 index 000000000..10d84d32b --- /dev/null +++ "b/assets/java_database_Otter\345\256\236\347\216\260\346\225\260\346\215\256\345\205\250\351\207\217\345\242\236\351\207\217\345\220\214\346\255\245.md.892bc0fe.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as o,Q as e}from"./chunks/framework.dc1f4608.js";const i=JSON.parse('{"title":"Otter实现数据全量增量同步","description":"","frontmatter":{},"headers":[],"relativePath":"java/database/Otter实现数据全量增量同步.md","filePath":"java/database/Otter实现数据全量增量同步.md","lastUpdated":1708510102000}'),n={name:"java/database/Otter实现数据全量增量同步.md"},l=e("",63),p=[l];function c(E,t,r,T,d,A){return a(),o("div",null,p)}const L=s(n,[["render",c]]);export{i as __pageData,L as default}; diff --git "a/assets/java_database_SQL\344\274\230\345\214\226\345\255\246\344\271\240.md.84dffe0e.js" "b/assets/java_database_SQL\344\274\230\345\214\226\345\255\246\344\271\240.md.84dffe0e.js" new file mode 100644 index 000000000..4f1c79563 --- /dev/null +++ "b/assets/java_database_SQL\344\274\230\345\214\226\345\255\246\344\271\240.md.84dffe0e.js" @@ -0,0 +1,7 @@ +import{_ as a,o as e,c as s,Q as l}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"SQL优化学习","description":"","frontmatter":{},"headers":[],"relativePath":"java/database/SQL优化学习.md","filePath":"java/database/SQL优化学习.md","lastUpdated":1708510102000}'),n={name:"java/database/SQL优化学习.md"},o=l(`

SQL优化学习

MySQL查询的流程

v233c07d2bec1fd9f093e45fd448aaacfa_hd.jpg

  • 客户端将查询发送至服务器
  • 服务器检查查询缓存,如果找到了,就从缓存中返回结果,否则进行下一步
  • 服务器解析,预处理
  • 查询优化器优化查询
  • 生成执行计划,执行引擎调用存储引擎API执行查询
  • 服务器将结果发送回客户端

1.客户端连接阶段

show processlist命令可以查看mysql连接的状态,以我自己的阿里云服务器为例

1.png

常见的状态:

  • Sleep:线程正在等待客户端发送数据
  • Query:连接线程正在执行查询
  • Locked:线程正在等待表锁的释放
  • Sorting result:线程正在对结果进行排序
  • Sending data:向请求端返回数据

完整状态列表说明请见官网地址

2.查询缓存

在解析一个查询语句之前,==如果查询缓存是打开的==(默认是关闭的,浪费性能),那么MySQL会优先检查这个查询是否命中查询缓存中的数据,如果命中缓存直接从缓存中拿到结果并返回给客户端。这种情况下,查询不会被解析,不用生成执行计划,不会被执行。

可以使用show variables like 'query_cache%'来查看缓存的设置情况

Snipaste_20210302_130903 如图,query_cache_type一栏时OFF关闭状态,如要开启可以修改my.cnf文件设置query_cache_type=1

query_cache_type=0时表示关闭,1时表示打开,2表示只要select 中明确指定SQL_CACHE才缓存。

3.解析、预处理、优化阶段

MySQL通过关键字将SQL语句进行解析,并生成一棵对应的“解析树”。MySQL解析器将使用MySQL语法规则验证和解析查询。语法树被校验合法后由优化器转成查询计划,一条语句可以有很多种执行方式,最后返回相同的结果。优化器的作用就是找到这其中最好的执行计划。

执行计划可以用==explain==命令查看,详见后文。

4.查询执行引擎

在解析和优化阶段,MySQL将生成查询对应的执行计划,MySQL的查询执行引擎则根据这个执行计划来完成整个查询。最常使用的也是比较最多的引擎是MyISAM引擎和InnoDB引擎。mysql5.5开始的默认存储引擎已经变更为innodb。

慢查询

可以使用show variable like 'long_query_time'命令来查看慢查询的时间

Snipaste_20210302_134903.png

可以看到MySQL中默认的慢查询为10s,然而这个时间属于根本无法接受的地步了,可以将这个时间设置为业务可以接受的范围.

一般的慢SQL定位:业务驱动,测试驱动,慢查询日志

开启慢查询日志

使用SHOW VARIABLES LIKE 'SLOW_QUERY_LOG'命令查看是否开启了慢查询日志保存

Snipaste_20210302_135529.png

可以看到默认是没有开启的

可以使用以下命令开启保存慢查询日志(==重启mysql后会失效==)

sql
set global slow_query_log = on //-- 打开慢日志
+set global slow_query_log_file = '/var/lib/mysql/test-slow.log' //--慢日志保存位置
+set global log_queries_not_using_indexes = on //-- 没有命中索引的是否要记录慢日志
+set global long_query_time = 2 (秒) //-- 执行时间超过多少为慢日志
set global slow_query_log = on //-- 打开慢日志
+set global slow_query_log_file = '/var/lib/mysql/test-slow.log' //--慢日志保存位置
+set global log_queries_not_using_indexes = on //-- 没有命中索引的是否要记录慢日志
+set global long_query_time = 2 (秒) //-- 执行时间超过多少为慢日志

或者直接修改my.cnf文件添加相应配置后重启mysql(==永久生效==)

Explain命令

EXPLAIN可以帮助开发人员分析SQL问题,EXPLAIN显示了MySQL如何使用使用SQL执行计划,可以帮助开发人员写出更优化的查询语句。使用方法,在select语句前加上Explain就可以了

例如:EXPLAIN SELECT * FROM ARTICLE WHERE ARTICLE_ID = '1'

Snipaste_20210302_145846.png 结果列说明:

1.id

这是SELECT的查询序列号,表示查询中执行select子句或操作表的顺序,id相同,执行顺序从上到下,id不同,id值越大执行优先级越高

2.select_type

表示SELECT语句的类型

  • SIMPLE:简单的select查询(不使用连接查询或者子查询)

  • PRIMARY:表示主查询,或者是最外层的查询语句,最外层查询为PRIMARY,也就是最后加载的就是PRIMARY

  • UNION:表示连接查询的第二个或者后面的查询语句,不依赖外部查询的结果集

  • DEPENDENT UNION:union中的第二个或后面的select语句,取决于外面的查询。

  • UNION RESULT:连接查询的结果;

  • SUBQUERY:子查询中的第1个SELECT语句;不依赖于外部查询的结果集

  • DEPENDENT SUBQUERY:子查询中的第1个SELECT,依赖于外面的查询;

  • DERIVED:导出表的SELECT(FROM子句的子查询),MySQL会递归执行这些子查询,把结果放在临时表里

  • DEPENDENT DERIVED:派生表依赖于另一个表

  • MATERIALIZED:物化子查询

  • UNCACHEABLE SUBQUERY:子查询,其结果无法缓存,必须针对外部查询的每一行重新进行评估

  • UNCACHEABLE UNION:UNION中的第二个或随后的 select 查询,属于不可缓存的子查询

3.table

表示查询的表

4.type

表示表的连接类型,从最好到最差的连接类型为

system > const > eq_ref > ref > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL

  1. system:表仅有一行,这是const类型的特列,平时不会出现,这个也可以忽略不计。

  2. const:数据表最多只有一个匹配行,因为只匹配一行数据,所以很快

  3. eq_ref:mysql手册是这样说的:"对于每个来自于前面的表的行组合,从该表中读取一行。这可能是最好的联接类型,除了const类型。它用在一个索引的所有部分被联接使用并且索引是UNIQUE或PRIMARY KEY"。eq_ref可以用于使用=比较带索引的列。

  4. ref:查询条件索引既不是UNIQUE也不是PRIMARY KEY的情况。ref可用于=或<或>操作符的带索引的列。

  5. ref_or_null:该连接类型如同ref,但是添加了MySQL可以专门搜索包含NULL值的行。在解决子查询中经常使用该联接类型的优化。

  6. index_merge:该连接类型表示使用了索引合并优化方法。在这种情况下,key列包含了使用的索引的清单,key_len包含了使用的索引的最长的关键元素。

  7. unique_subquery:该类型替换了下面形式的IN子查询的ref: value IN (SELECT primary_key FROM single_table WHERE some_expr) unique_subquery是一个索引查找函数,可以完全替换子查询,效率更高。

  8. index_subquery:该连接类型类似于unique_subquery。可以替换IN子查询,但只适合下列形式的子查询中的非唯一索引: value IN (SELECT key_column FROM single_table WHERE some_expr)

  9. range:只检索给定范围的行,使用一个索引来选择行。key列显示使用了哪个索引。key_len包含所使用索引的最长关键元素。当使用=、<>、>、>=、<、<=、IS NULL、<=>、BETWEEN或者IN操作符用常量比较关键字列时,类型为range

  10. index:该连接类型与ALL相同,除了只有索引树被扫描。通常比ALL快,因为索引文件通常比数据文件小。

    这个类型发生在这两种方式:

    **1)**如果索引是查询的覆盖索引,并且可用于满足表中所需的所有数据,则仅扫描索引树。在这种情况下,Extra列显示为 Using index

    **2)**使用对索引的读取执行全表扫描,以按索引顺序查找数据行。 Uses index没有出现在 Extra列中。

  11. ALL:对于每个来自于先前的表的行组合,进行完整的表扫描。(性能最差)

5.possible_keys

指MySQL能使用哪个索引在该表中找到行。如果为空,没有相关的索引。这时如果要提升性能,可以通过检验WHERE子句,看它是否引用某些列或适合索引的列来提高查询性能。如果是这样,可以创建适合的索引来提高查询的性能。

6.key

表示查询实际使用的索引,如果没有选择索引,该列的值是NULL。如果为primary的话,表示使用了主键。要想强制MySQL使用或忽视possible_keys列中的索引,在查询中使用FORCE INDEX、USE INDEX或者IGNORE INDEX

7.key_len

表示MySQL选择的索引字段按字节计算的长度,若键是NULL,则长度为NULL。通过key_len值可以确定MySQL将实际使用一个多列索引中的几个字段

8.ref

表示使用哪个列或常数与索引一起来查询记录。

9.rows

显示MySQL在表中进行查询时必须检查的行数。

10.extra

表示MySQL在处理查询时的详细信息

  • Distinct:MySQL发现第1个匹配行后,停止为当前的行组合搜索更多的行。
  • Not exists:MySQL能够对查询进行LEFT JOIN优化,发现1个匹配LEFT JOIN标准的行后,不再为前面的的行组合在该表内检查更多的行。
  • range checked for each record (index map: #):MySQL没有发现好的可以使用的索引,但发现如果来自前面的表的列值已知,可能部分索引可以使用。
  • Using filesort:MySQL需要额外的一次传递,以找出如何按排序顺序检索行。
  • Using index:从只使用索引树中的信息而不需要进一步搜索读取实际的行来检索表中的列信息。
  • Using temporary:为了解决查询,MySQL需要创建一个临时表来容纳结果。
  • Using where:WHERE 子句用于限制哪一个行匹配下一个表或发送到客户。
  • Using sort_union(...), Using union(...), Using intersect(...):这些函数说明如何为index_merge联接类型合并索引扫描。
  • Using index for group-by:类似于访问表的Using index方式,Using index for group-by表示MySQL发现了一个索引,可以用来查 询GROUP BY或DISTINCT查询的所有列,而不要额外搜索硬盘访问实际的表。

贴一个美团技术团队的文章:MySQL索引原理及慢查询优化,以下内容引用自该文章

建索引的几大原则

1.最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。

2.=和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式。

3.尽量选择区分度高的列作为索引,区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就是0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要join的字段我们都要求是0.1以上,即平均1条扫描10条记录。

4.索引列不能参与计算,保持列“干净”,比如from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,原因很简单,b+树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本太大。所以语句应该写成create_time = unix_timestamp(’2014-05-29’)。

5.尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。

慢查询优化基本步骤

0.先运行看看是否真的很慢,注意设置SQL_NO_CACHE

1.where条件单表查,锁定最小返回记录表。这句话的意思是把查询语句的where都应用到表中返回的记录数最小的表开始查起,单表每个字段分别查询,看哪个字段的区分度最高

2.explain查看执行计划,是否与1预期一致(从锁定记录较少的表开始查询)

3.order by limit 形式的sql语句让排序的表优先查

4.了解业务方使用场景

5.加索引时参照建索引的几大原则

6.观察结果,不符合预期继续从0分析

`,75),p=[o];function t(r,i,c,y,d,_){return e(),s("div",null,p)}const g=a(n,[["render",t]]);export{h as __pageData,g as default}; diff --git "a/assets/java_database_SQL\344\274\230\345\214\226\345\255\246\344\271\240.md.84dffe0e.lean.js" "b/assets/java_database_SQL\344\274\230\345\214\226\345\255\246\344\271\240.md.84dffe0e.lean.js" new file mode 100644 index 000000000..1540cf7bb --- /dev/null +++ "b/assets/java_database_SQL\344\274\230\345\214\226\345\255\246\344\271\240.md.84dffe0e.lean.js" @@ -0,0 +1 @@ +import{_ as a,o as e,c as s,Q as l}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"SQL优化学习","description":"","frontmatter":{},"headers":[],"relativePath":"java/database/SQL优化学习.md","filePath":"java/database/SQL优化学习.md","lastUpdated":1708510102000}'),n={name:"java/database/SQL优化学习.md"},o=l("",75),p=[o];function t(r,i,c,y,d,_){return e(),s("div",null,p)}const g=a(n,[["render",t]]);export{h as __pageData,g as default}; diff --git "a/assets/java_devtool_Maven\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237.md.e9b078b3.js" "b/assets/java_devtool_Maven\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237.md.e9b078b3.js" new file mode 100644 index 000000000..61f6640de --- /dev/null +++ "b/assets/java_devtool_Maven\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237.md.e9b078b3.js" @@ -0,0 +1 @@ +import{_ as t,o as e,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const y=JSON.parse('{"title":"Maven的生命周期","description":"","frontmatter":{},"headers":[],"relativePath":"java/devtool/Maven的生命周期.md","filePath":"java/devtool/Maven的生命周期.md","lastUpdated":1708510102000}'),n={name:"java/devtool/Maven的生命周期.md"},s=l('

Maven的生命周期

maven共有三个标准生命周期:

  • clean:项目清理的处理

  • default:项目部署的处理

  • site:站点文件生成的处理

Maven的构建生命周期

阶段处理描述
验证 validate验证项目验证项目是否正确且所有必须信息是可用的
编译 compile执行编译源代码编译在此阶段完成
测试 Test测试使用适当的单元测试框架(例如JUnit)运行测试。
包装 package打包创建JAR/WAR包如在 pom.xml 中定义提及的包
检查 verify检查对集成测试的结果进行检查,以保证质量达标
安装 install安装安装打包的项目到本地仓库,以供其他项目使用
部署 deploy部署拷贝最终的工程包到远程仓库中,以共享给其他开发人员和工程

平常用到最多的是mvn clean package或者mvn install,两者都会在target生成最终的jar或war文件,区别在于install命令还会将生成的jar或war包安装至本地仓库。而当需要跨项目引用jar包时,我们需要把自己的jar包上传至maven私服(远程仓库)中,之前还傻傻的直接动手去maven私服手动上传jar包,不过相比于mvn deploy肯定是麻烦很多的,maven的deploy可以很方便的将我们工程中的jar发布至maven私服中方便团队间的jar包共享。

TIP

当deploy快照版本时,maven会给快照打上时间戳以区分快照的版本,因为快照可能会频繁变更。maven会以包名是否包含SNAPSHOT来判断是否快照。

Clean 生命周期

当我们执行 mvn post-clean 命令时,Maven 调用 clean 生命周期,它包含以下阶段:

  • pre-clean:执行一些需要在clean之前完成的工作
  • clean:移除所有上一次构建生成的文件
  • post-clean:执行一些需要在clean之后立刻完成的工作

mvn clean 中的 clean 就是上面的 clean,在一个生命周期中,运行某个阶段的时候,它之前的所有阶段都会被运行,也就是说,如果执行 mvn clean 将运行以下两个生命周期阶段:

pre-clean, clean
pre-clean, clean

如果我们运行 mvn post-clean ,则运行以下三个生命周期阶段:

pre-clean, clean, post-clean
pre-clean, clean, post-clean

Site 生命周期

Maven Site 插件一般用来创建新的报告文档、部署站点等。

  • pre-site:执行一些需要在生成站点文档之前完成的工作
  • site:生成项目的站点文档
  • post-site: 执行一些需要在生成站点文档之后完成的工作,并且为部署做准备
  • site-deploy:将生成的站点文档部署到特定的服务器上
',17),i=[s];function d(p,c,o,r,v,h){return e(),a("div",null,i)}const f=t(n,[["render",d]]);export{y as __pageData,f as default}; diff --git "a/assets/java_devtool_Maven\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237.md.e9b078b3.lean.js" "b/assets/java_devtool_Maven\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237.md.e9b078b3.lean.js" new file mode 100644 index 000000000..d52292ff7 --- /dev/null +++ "b/assets/java_devtool_Maven\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237.md.e9b078b3.lean.js" @@ -0,0 +1 @@ +import{_ as t,o as e,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const y=JSON.parse('{"title":"Maven的生命周期","description":"","frontmatter":{},"headers":[],"relativePath":"java/devtool/Maven的生命周期.md","filePath":"java/devtool/Maven的生命周期.md","lastUpdated":1708510102000}'),n={name:"java/devtool/Maven的生命周期.md"},s=l("",17),i=[s];function d(p,c,o,r,v,h){return e(),a("div",null,i)}const f=t(n,[["render",d]]);export{y as __pageData,f as default}; diff --git "a/assets/java_devtool_nexus\346\227\240\346\263\225\344\270\213\350\275\275\344\276\235\350\265\226\347\232\204\351\227\256\351\242\230\350\256\260\345\275\225.md.647fa7c0.js" "b/assets/java_devtool_nexus\346\227\240\346\263\225\344\270\213\350\275\275\344\276\235\350\265\226\347\232\204\351\227\256\351\242\230\350\256\260\345\275\225.md.647fa7c0.js" new file mode 100644 index 000000000..0cceab4f9 --- /dev/null +++ "b/assets/java_devtool_nexus\346\227\240\346\263\225\344\270\213\350\275\275\344\276\235\350\265\226\347\232\204\351\227\256\351\242\230\350\256\260\345\275\225.md.647fa7c0.js" @@ -0,0 +1,229 @@ +import{_ as e,o as a,c as s,Q as n}from"./chunks/framework.dc1f4608.js";const g=JSON.parse('{"title":"nexus无法下载依赖的问题记录","description":"","frontmatter":{},"headers":[],"relativePath":"java/devtool/nexus无法下载依赖的问题记录.md","filePath":"java/devtool/nexus无法下载依赖的问题记录.md","lastUpdated":1708510102000}'),t={name:"java/devtool/nexus无法下载依赖的问题记录.md"},l=n(`

nexus无法下载依赖的问题记录

maven私服突然无法下载依赖,在idea中使用命令查看下详细错误信息mvn clean install -U -e -X

Client报错信息:

txt
Caused by: org.eclipse.aether.transfer.ArtifactTransferException: Could not transfer artifact org.springframework.boot:spring-boot:jar:2.4.1 from/to nexus (xxxxxxxxxxxxxxxxxxxxxxxxx): GET request of: org/springframework/boot/spring-boot/2.4.1/spring-boot-2.4.1.jar from nexus failed
Caused by: org.eclipse.aether.transfer.ArtifactTransferException: Could not transfer artifact org.springframework.boot:spring-boot:jar:2.4.1 from/to nexus (xxxxxxxxxxxxxxxxxxxxxxxxx): GET request of: org/springframework/boot/spring-boot/2.4.1/spring-boot-2.4.1.jar from nexus failed
txt
Could not transfer artifact org.springframework.boot:spring-boot:jar:2.4.1 from/to nexus (xxxxxxxxxxxxxxxxxxxxxx): GET request of: org/springframework/boot/spring-boot/2.4.1/spring-boot-2.4.1.jar from nexus failed: Premature end of Content-Length delimited message body (expected: 1,302,347; received: 76,515) -> [Help 1]
Could not transfer artifact org.springframework.boot:spring-boot:jar:2.4.1 from/to nexus (xxxxxxxxxxxxxxxxxxxxxx): GET request of: org/springframework/boot/spring-boot/2.4.1/spring-boot-2.4.1.jar from nexus failed: Premature end of Content-Length delimited message body (expected: 1,302,347; received: 76,515) -> [Help 1]

服务端错误日志:

txt
2022-04-10 16:51:14,488+0800 INFO  [qtp1752276351-145] *UNKNOWN org.apache.shiro.session.mgt.AbstractValidatingSessionManager - Enabling session validation scheduler...
+2022-04-10 16:51:14,502+0800 INFO  [qtp1752276351-51] *UNKNOWN org.sonatype.nexus.internal.security.anonymous.AnonymousManagerImpl - Loaded configuration: AnonymousConfiguration{enabled=false, userId='anonymous', realmName='NexusAuthorizingRealm'}
+2022-04-10 16:51:15,233+0800 WARN  [qtp1752276351-48] deployment org.sonatype.nexus.repository.httpbridge.internal.ViewServlet - Failure servicing: GET /repository/maven-public/org/springframework/boot/spring-boot/2.4.1/spring-boot-2.4.1.jar
+org.eclipse.jetty.io.EofException: null
+	at org.eclipse.jetty.io.ChannelEndPoint.flush(ChannelEndPoint.java:284)
+	at org.eclipse.jetty.io.WriteFlusher.flush(WriteFlusher.java:393)
+	at org.eclipse.jetty.io.WriteFlusher.write(WriteFlusher.java:277)
+	at org.eclipse.jetty.io.AbstractEndPoint.write(AbstractEndPoint.java:380)
+	at org.eclipse.jetty.server.HttpConnection$SendCallback.process(HttpConnection.java:826)
+	at org.eclipse.jetty.util.IteratingCallback.processing(IteratingCallback.java:241)
+	at org.eclipse.jetty.util.IteratingCallback.iterate(IteratingCallback.java:224)
+	at org.eclipse.jetty.server.HttpConnection.send(HttpConnection.java:550)
+	at org.eclipse.jetty.server.HttpChannel.sendResponse(HttpChannel.java:850)
+	at org.eclipse.jetty.server.HttpChannel.write(HttpChannel.java:921)
+	at org.eclipse.jetty.server.HttpOutput.write(HttpOutput.java:249)
+	at org.eclipse.jetty.server.HttpOutput.write(HttpOutput.java:225)
+	at org.eclipse.jetty.server.HttpOutput.write(HttpOutput.java:524)
+	at com.google.common.io.ByteStreams.copy(ByteStreams.java:113)
+	at org.sonatype.nexus.repository.view.Payload.copy(Payload.java:61)
+	at org.sonatype.nexus.repository.view.Content.copy(Content.java:116)
+	at org.sonatype.nexus.repository.httpbridge.internal.DefaultHttpResponseSender.send(DefaultHttpResponseSender.java:81)
+	at org.sonatype.nexus.repository.httpbridge.internal.ViewServlet.dispatchAndSend(ViewServlet.java:228)
+	at org.sonatype.nexus.repository.httpbridge.internal.ViewServlet.doService(ViewServlet.java:174)
+	at org.sonatype.nexus.repository.httpbridge.internal.ViewServlet.service(ViewServlet.java:126)
+	at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
+	at com.google.inject.servlet.ServletDefinition.doServiceImpl(ServletDefinition.java:286)
+	at com.google.inject.servlet.ServletDefinition.doService(ServletDefinition.java:276)
+	at com.google.inject.servlet.ServletDefinition.service(ServletDefinition.java:181)
+	at com.google.inject.servlet.DynamicServletPipeline.service(DynamicServletPipeline.java:71)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:85)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:112)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:61)
+	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
+	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
+	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
+	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
+	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
+	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
+	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
+	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
+	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
+	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
+	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
+	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
+	at org.apache.shiro.web.servlet.AbstractShiroFilter.executeChain(AbstractShiroFilter.java:449)
+	at org.sonatype.nexus.security.SecurityFilter.executeChain(SecurityFilter.java:85)
+	at org.apache.shiro.web.servlet.AbstractShiroFilter$1.call(AbstractShiroFilter.java:365)
+	at org.apache.shiro.subject.support.SubjectCallable.doCall(SubjectCallable.java:90)
+	at org.apache.shiro.subject.support.SubjectCallable.call(SubjectCallable.java:83)
+	at org.apache.shiro.subject.support.DelegatingSubject.execute(DelegatingSubject.java:383)
+	at org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal(AbstractShiroFilter.java:362)
+	at org.sonatype.nexus.security.SecurityFilter.doFilterInternal(SecurityFilter.java:101)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at org.sonatype.nexus.repository.httpbridge.internal.ExhaustRequestFilter.doFilter(ExhaustRequestFilter.java:80)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at com.sonatype.nexus.licensing.internal.LicensingRedirectFilter.doFilter(LicensingRedirectFilter.java:114)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at com.codahale.metrics.servlet.AbstractInstrumentedFilter.doFilter(AbstractInstrumentedFilter.java:112)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at org.sonatype.nexus.internal.web.ErrorPageFilter.doFilter(ErrorPageFilter.java:79)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at org.sonatype.nexus.internal.web.EnvironmentFilter.doFilter(EnvironmentFilter.java:101)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at org.sonatype.nexus.internal.web.HeaderPatternFilter.doFilter(HeaderPatternFilter.java:98)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at com.google.inject.servlet.DynamicFilterPipeline.dispatch(DynamicFilterPipeline.java:104)
+	at com.google.inject.servlet.GuiceFilter.doFilter(GuiceFilter.java:135)
+	at org.sonatype.nexus.bootstrap.osgi.DelegatingFilter.doFilter(DelegatingFilter.java:73)
+	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1602)
+	at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:540)
+	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:146)
+	at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:548)
+	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
+	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:257)
+	at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1700)
+	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:255)
+	at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1345)
+	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:203)
+	at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:480)
+	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1667)
+	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:201)
+	at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1247)
+	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:144)
+	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
+	at com.codahale.metrics.jetty9.InstrumentedHandler.handle(InstrumentedHandler.java:239)
+	at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:152)
+	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
+	at org.eclipse.jetty.server.Server.handle(Server.java:505)
+	at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:370)
+	at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:267)
+	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:305)
+	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103)
+	at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:117)
+	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:333)
+	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:310)
+	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:168)
+	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:126)
+	at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:366)
+	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:698)
+	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:804)
+	at java.lang.Thread.run(Thread.java:748)
+Caused by: java.io.IOException: Connection reset by peer
+	at sun.nio.ch.FileDispatcherImpl.write0(Native Method)
+	at sun.nio.ch.SocketDispatcher.write(SocketDispatcher.java:47)
+	at sun.nio.ch.IOUtil.writeFromNativeBuffer(IOUtil.java:93)
+	at sun.nio.ch.IOUtil.write(IOUtil.java:51)
+	at sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:471)
+	at org.eclipse.jetty.io.ChannelEndPoint.flush(ChannelEndPoint.java:262)
+	... 102 common frames omitted
2022-04-10 16:51:14,488+0800 INFO  [qtp1752276351-145] *UNKNOWN org.apache.shiro.session.mgt.AbstractValidatingSessionManager - Enabling session validation scheduler...
+2022-04-10 16:51:14,502+0800 INFO  [qtp1752276351-51] *UNKNOWN org.sonatype.nexus.internal.security.anonymous.AnonymousManagerImpl - Loaded configuration: AnonymousConfiguration{enabled=false, userId='anonymous', realmName='NexusAuthorizingRealm'}
+2022-04-10 16:51:15,233+0800 WARN  [qtp1752276351-48] deployment org.sonatype.nexus.repository.httpbridge.internal.ViewServlet - Failure servicing: GET /repository/maven-public/org/springframework/boot/spring-boot/2.4.1/spring-boot-2.4.1.jar
+org.eclipse.jetty.io.EofException: null
+	at org.eclipse.jetty.io.ChannelEndPoint.flush(ChannelEndPoint.java:284)
+	at org.eclipse.jetty.io.WriteFlusher.flush(WriteFlusher.java:393)
+	at org.eclipse.jetty.io.WriteFlusher.write(WriteFlusher.java:277)
+	at org.eclipse.jetty.io.AbstractEndPoint.write(AbstractEndPoint.java:380)
+	at org.eclipse.jetty.server.HttpConnection$SendCallback.process(HttpConnection.java:826)
+	at org.eclipse.jetty.util.IteratingCallback.processing(IteratingCallback.java:241)
+	at org.eclipse.jetty.util.IteratingCallback.iterate(IteratingCallback.java:224)
+	at org.eclipse.jetty.server.HttpConnection.send(HttpConnection.java:550)
+	at org.eclipse.jetty.server.HttpChannel.sendResponse(HttpChannel.java:850)
+	at org.eclipse.jetty.server.HttpChannel.write(HttpChannel.java:921)
+	at org.eclipse.jetty.server.HttpOutput.write(HttpOutput.java:249)
+	at org.eclipse.jetty.server.HttpOutput.write(HttpOutput.java:225)
+	at org.eclipse.jetty.server.HttpOutput.write(HttpOutput.java:524)
+	at com.google.common.io.ByteStreams.copy(ByteStreams.java:113)
+	at org.sonatype.nexus.repository.view.Payload.copy(Payload.java:61)
+	at org.sonatype.nexus.repository.view.Content.copy(Content.java:116)
+	at org.sonatype.nexus.repository.httpbridge.internal.DefaultHttpResponseSender.send(DefaultHttpResponseSender.java:81)
+	at org.sonatype.nexus.repository.httpbridge.internal.ViewServlet.dispatchAndSend(ViewServlet.java:228)
+	at org.sonatype.nexus.repository.httpbridge.internal.ViewServlet.doService(ViewServlet.java:174)
+	at org.sonatype.nexus.repository.httpbridge.internal.ViewServlet.service(ViewServlet.java:126)
+	at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
+	at com.google.inject.servlet.ServletDefinition.doServiceImpl(ServletDefinition.java:286)
+	at com.google.inject.servlet.ServletDefinition.doService(ServletDefinition.java:276)
+	at com.google.inject.servlet.ServletDefinition.service(ServletDefinition.java:181)
+	at com.google.inject.servlet.DynamicServletPipeline.service(DynamicServletPipeline.java:71)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:85)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:112)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:61)
+	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
+	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
+	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
+	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
+	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
+	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
+	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
+	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
+	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
+	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
+	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
+	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
+	at org.apache.shiro.web.servlet.AbstractShiroFilter.executeChain(AbstractShiroFilter.java:449)
+	at org.sonatype.nexus.security.SecurityFilter.executeChain(SecurityFilter.java:85)
+	at org.apache.shiro.web.servlet.AbstractShiroFilter$1.call(AbstractShiroFilter.java:365)
+	at org.apache.shiro.subject.support.SubjectCallable.doCall(SubjectCallable.java:90)
+	at org.apache.shiro.subject.support.SubjectCallable.call(SubjectCallable.java:83)
+	at org.apache.shiro.subject.support.DelegatingSubject.execute(DelegatingSubject.java:383)
+	at org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal(AbstractShiroFilter.java:362)
+	at org.sonatype.nexus.security.SecurityFilter.doFilterInternal(SecurityFilter.java:101)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at org.sonatype.nexus.repository.httpbridge.internal.ExhaustRequestFilter.doFilter(ExhaustRequestFilter.java:80)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at com.sonatype.nexus.licensing.internal.LicensingRedirectFilter.doFilter(LicensingRedirectFilter.java:114)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at com.codahale.metrics.servlet.AbstractInstrumentedFilter.doFilter(AbstractInstrumentedFilter.java:112)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at org.sonatype.nexus.internal.web.ErrorPageFilter.doFilter(ErrorPageFilter.java:79)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at org.sonatype.nexus.internal.web.EnvironmentFilter.doFilter(EnvironmentFilter.java:101)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at org.sonatype.nexus.internal.web.HeaderPatternFilter.doFilter(HeaderPatternFilter.java:98)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at com.google.inject.servlet.DynamicFilterPipeline.dispatch(DynamicFilterPipeline.java:104)
+	at com.google.inject.servlet.GuiceFilter.doFilter(GuiceFilter.java:135)
+	at org.sonatype.nexus.bootstrap.osgi.DelegatingFilter.doFilter(DelegatingFilter.java:73)
+	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1602)
+	at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:540)
+	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:146)
+	at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:548)
+	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
+	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:257)
+	at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1700)
+	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:255)
+	at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1345)
+	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:203)
+	at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:480)
+	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1667)
+	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:201)
+	at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1247)
+	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:144)
+	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
+	at com.codahale.metrics.jetty9.InstrumentedHandler.handle(InstrumentedHandler.java:239)
+	at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:152)
+	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
+	at org.eclipse.jetty.server.Server.handle(Server.java:505)
+	at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:370)
+	at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:267)
+	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:305)
+	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103)
+	at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:117)
+	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:333)
+	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:310)
+	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:168)
+	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:126)
+	at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:366)
+	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:698)
+	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:804)
+	at java.lang.Thread.run(Thread.java:748)
+Caused by: java.io.IOException: Connection reset by peer
+	at sun.nio.ch.FileDispatcherImpl.write0(Native Method)
+	at sun.nio.ch.SocketDispatcher.write(SocketDispatcher.java:47)
+	at sun.nio.ch.IOUtil.writeFromNativeBuffer(IOUtil.java:93)
+	at sun.nio.ch.IOUtil.write(IOUtil.java:51)
+	at sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:471)
+	at org.eclipse.jetty.io.ChannelEndPoint.flush(ChannelEndPoint.java:262)
+	... 102 common frames omitted

解决

image-20220410223038569

在nginx的location配置中增加proxy_max_temp_file_size 0;

`,10),r=[l];function o(p,i,c,v,d,y){return a(),s("div",null,r)}const j=e(t,[["render",o]]);export{g as __pageData,j as default}; diff --git "a/assets/java_devtool_nexus\346\227\240\346\263\225\344\270\213\350\275\275\344\276\235\350\265\226\347\232\204\351\227\256\351\242\230\350\256\260\345\275\225.md.647fa7c0.lean.js" "b/assets/java_devtool_nexus\346\227\240\346\263\225\344\270\213\350\275\275\344\276\235\350\265\226\347\232\204\351\227\256\351\242\230\350\256\260\345\275\225.md.647fa7c0.lean.js" new file mode 100644 index 000000000..c663f5177 --- /dev/null +++ "b/assets/java_devtool_nexus\346\227\240\346\263\225\344\270\213\350\275\275\344\276\235\350\265\226\347\232\204\351\227\256\351\242\230\350\256\260\345\275\225.md.647fa7c0.lean.js" @@ -0,0 +1 @@ +import{_ as e,o as a,c as s,Q as n}from"./chunks/framework.dc1f4608.js";const g=JSON.parse('{"title":"nexus无法下载依赖的问题记录","description":"","frontmatter":{},"headers":[],"relativePath":"java/devtool/nexus无法下载依赖的问题记录.md","filePath":"java/devtool/nexus无法下载依赖的问题记录.md","lastUpdated":1708510102000}'),t={name:"java/devtool/nexus无法下载依赖的问题记录.md"},l=n("",10),r=[l];function o(p,i,c,v,d,y){return a(),s("div",null,r)}const j=e(t,[["render",o]]);export{g as __pageData,j as default}; diff --git "a/assets/java_framework_ConfigurationProperties\346\263\250\350\247\243.md.8a9eff27.js" "b/assets/java_framework_ConfigurationProperties\346\263\250\350\247\243.md.8a9eff27.js" new file mode 100644 index 000000000..9aaf7eef4 --- /dev/null +++ "b/assets/java_framework_ConfigurationProperties\346\263\250\350\247\243.md.8a9eff27.js" @@ -0,0 +1,7 @@ +import{_ as e,o,c as r,Q as a}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"ConfigurationProperties注解","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/ConfigurationProperties注解.md","filePath":"java/framework/ConfigurationProperties注解.md","lastUpdated":1708510102000}'),t={name:"java/framework/ConfigurationProperties注解.md"},n=a(`

ConfigurationProperties注解

@ConfigurationProperties注解可以从外部获取配置信息,并将其绑定到JavaBean中。

原理

SpringBoot可以让配置信息外部化,支持的配置有多种,最常见的.properties.yaml文件,启动时命令行参数--xxx、系统环境变量、Java系统属性(System.getProperties())...

@ConfigurationProperties注解的功能由ConfigurationPropertiesBindingPostProcessor这个后置处理器实现,spring容器中的enviroment.propertySources记录着外部的属性值,properties后置处理器会从中找到匹配的值绑定到JavaBean中。

属性的绑定是会被覆盖的,排序靠后的会覆盖靠前的,即越靠后的优先级越高。(os环境变量可以覆盖application.properties,java系统属性可以覆盖系统环境变量,命令行参数可以覆盖java系统属性...)

这些配置的方式和可以参照spring boot官方文档:

https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config

2. Externalized Configuration

Spring Boot lets you externalize your configuration so that you can work with the same application code in different environments. You can use a variety of external configuration sources, include Java properties files, YAML files, environment variables, and command-line arguments.

Property values can be injected directly into your beans by using the @Value annotation, accessed through Spring’s Environment abstraction, or be bound to structured objects through @ConfigurationProperties.

Spring Boot uses a very particular PropertySource order that is designed to allow sensible overriding of values. Properties are considered in the following order (with values from lower items overriding earlier ones):

  1. Default properties (specified by setting SpringApplication.setDefaultProperties).
  2. @PropertySource annotations on your @Configuration classes. Please note that such property sources are not added to the Environment until the application context is being refreshed. This is too late to configure certain properties such as logging.* and spring.main.* which are read before refresh begins.
  3. Config data (such as application.properties files).
  4. A RandomValuePropertySource that has properties only in random.*.
  5. OS environment variables.
  6. Java System properties (System.getProperties()).
  7. JNDI attributes from java:comp/env.
  8. ServletContext init parameters.
  9. ServletConfig init parameters.
  10. Properties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or system property).
  11. Command line arguments.
  12. properties attribute on your tests. Available on @SpringBootTest and the test annotations for testing a particular slice of your application.
  13. @TestPropertySource annotations on your tests.
  14. Devtools global settings properties in the $HOME/.config/spring-boot directory when devtools is active.

Config data files are considered in the following order:

  1. Application properties packaged inside your jar (application.properties and YAML variants).
  2. Profile-specific application properties packaged inside your jar (application-{profile}.properties and YAML variants).
  3. Application properties outside of your packaged jar (application.properties and YAML variants).
  4. Profile-specific application properties outside of your packaged jar (application-{profile}.properties and YAML variants).

系统环境变量的方式

这里通过系统环境变量的绑定方式大致记录下,因为java应用的docker镜像通常使用这种方式,例如docker启动指令里加上-e xxx=xxx,就是在指定docker容器的系统环境变量。比较常见的-e JAVA_OPTS=xxx ,因为java应用的镜像通常entrypoint都是sh -c java $JAVA_OPTS xxx.jar

上文中enviroment.propertySources会读取外部的配置,系统环境变量是通过System.getenv()获取的,通过docker指令给镜像添加了系统环境变量后,就会通过这种方式绑定到java应用的配置类中。

但是

通过docker指令配置系统环境变量的方式,参数的命名需要做对应的调整,例如:

java
@ConfigurationProperties(prefix="user")
+public class Test{
+    private String name;
+}
@ConfigurationProperties(prefix="user")
+public class Test{
+    private String name;
+}

如果是通过.properties文件来配置那么文件中应该是user.name=xxx,如果是通过linux系统环境变量的方式,则环境变量中应该是USER_NAME=xxx.这是因为不同操作系统对环境变量的命名规则都有严格的要求,spring boot的宽松绑定规则要尽可能兼容不同系统的限制.

linux shell变量的命名规则:可以a-zA-Z0-9,可以下划线_,按照惯例,变量名都是大写的。所以,通过环境变量读取java配置时,应该遵循的原则

  • .替换为_

  • 删除所有破折号-

  • 变量名转为大写

例:spring.main.log-startup-info -> SPRING_MAIN_LOGSTARTUPINFO

Binding from Environment Variables

Most operating systems impose strict rules around the names that can be used for environment variables. For example, Linux shell variables can contain only letters (a to z or A to Z), numbers (0 to 9) or the underscore character (_). By convention, Unix shell variables will also have their names in UPPERCASE.

Spring Boot’s relaxed binding rules are, as much as possible, designed to be compatible with these naming restrictions.

To convert a property name in the canonical-form to an environment variable name you can follow these rules:

  • Replace dots (.) with underscores (_).
  • Remove any dashes (-).
  • Convert to uppercase.

For example, the configuration property spring.main.log-startup-info would be an environment variable named SPRING_MAIN_LOGSTARTUPINFO.

Environment variables can also be used when binding to object lists. To bind to a List, the element number should be surrounded with underscores in the variable name.

For example, the configuration property my.service[0].other would use an environment variable named MY_SERVICE_0_OTHER.

`,19),s=[n];function i(c,l,p,d,u,f){return o(),r("div",null,s)}const m=e(t,[["render",i]]);export{h as __pageData,m as default}; diff --git "a/assets/java_framework_ConfigurationProperties\346\263\250\350\247\243.md.8a9eff27.lean.js" "b/assets/java_framework_ConfigurationProperties\346\263\250\350\247\243.md.8a9eff27.lean.js" new file mode 100644 index 000000000..66ee86998 --- /dev/null +++ "b/assets/java_framework_ConfigurationProperties\346\263\250\350\247\243.md.8a9eff27.lean.js" @@ -0,0 +1 @@ +import{_ as e,o,c as r,Q as a}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"ConfigurationProperties注解","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/ConfigurationProperties注解.md","filePath":"java/framework/ConfigurationProperties注解.md","lastUpdated":1708510102000}'),t={name:"java/framework/ConfigurationProperties注解.md"},n=a("",19),s=[n];function i(c,l,p,d,u,f){return o(),r("div",null,s)}const m=e(t,[["render",i]]);export{h as __pageData,m as default}; diff --git "a/assets/java_framework_JavaSPI\346\234\272\345\210\266\345\222\214Springboot\350\207\252\345\212\250\350\243\205\351\205\215\345\216\237\347\220\206.md.6a5d7c18.js" "b/assets/java_framework_JavaSPI\346\234\272\345\210\266\345\222\214Springboot\350\207\252\345\212\250\350\243\205\351\205\215\345\216\237\347\220\206.md.6a5d7c18.js" new file mode 100644 index 000000000..9ee98e912 --- /dev/null +++ "b/assets/java_framework_JavaSPI\346\234\272\345\210\266\345\222\214Springboot\350\207\252\345\212\250\350\243\205\351\205\215\345\216\237\347\220\206.md.6a5d7c18.js" @@ -0,0 +1 @@ +import{_ as a,o as i,c as r,Q as o}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"JavaSPI机制和Springboot自动装配原理","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/JavaSPI机制和Springboot自动装配原理.md","filePath":"java/framework/JavaSPI机制和Springboot自动装配原理.md","lastUpdated":1708510102000}'),e={name:"java/framework/JavaSPI机制和Springboot自动装配原理.md"},t=o('

JavaSPI机制和Springboot自动装配原理

SPI机制

SPI(Service Provider Interface)是一种服务发现机制,提供服务接口,且为该接口寻找服务的实现。

从Java6开始引入,是一种基于ClassLoader类加载器发现并加载服务的机制。

标准的SPI构成:

  • Service:公开的接口或抽象类,定义一个抽象的功能模块
  • Service Provider:Service接口/抽象类的具体实现
  • ServiceLoader:SPI中的核心组件,负责在运行时发现并加载Service Provider

JAVA SPI的规范要素

  • 规范的配置文件
    • 文件路径:必须要在JAR中的META-INF/services下
    • 文件名称:Service接口全限定名
    • 文件内容:Service实现类的全限定名,如果有多个,则每个类单独占一行
  • ServiceProvider必须有无参构造方法,因为要通过反射实例化
  • 保证能加载到配置文件和ServiceProvider类
    • 将jar包放到classpath下
    • jar包安装到jre的扩展目录下
    • 自定义一个ClassLoader

场景

SPI在JDBC中的应用:JDBC要求Driver实现类在类加载的时候将自身的实例注册到DriverManager中,从而加载数据库驱动,在SPI出现之前,加载数据库驱动时要执行Class.forName("com.mysql.jdbc.Driver"), 在SPI出现后,只需要引入对应依赖的JAR包后,ServiceLoader会自动去约定的路径下寻找需要加载的类。

以mysql-connector-java的jar文件为例

  • 配置文件

  • 无参构造器

image-20220410224639940

总结

  • SPI提供了一种组件发现和注册的方式,可以用于各种插件、组件的灵活替换
  • 可以实现模块间解耦
  • 面向接口+配置文件+反射
  • 应用:JDBC、SLF4J。。。

简单实例

image-20220410231011833

image-20220410231049522

image-20220410231109810

当spi-company依赖spi-lt时运行main方法:

image-20220410231225984

当spi-company依赖spi-yd时运行main方法:

image-20220410231313507

springboot的自动装配

自动装配,即auto-configuration,是基于引入的依赖jar包对springboot应用进行自动配置,提供自动配置的jar包通常以starter结尾,比如mybatis-spring-boot-starter等等。

springboot默认会扫描项目下所有的配置类并注入到ioc容器中,但集成到其他框架并不能直接注入。为了实现真正的auto configuration,springboot的自动装配也采用了和spi类似的设计思想:

使用约定的配置文件:自动装配的配置文件为META-INF/spring.factories,文件内容为org.springframework.boot.autoconfigure.EnableAutoConfiguration=class1,class2,..classN,class是自动配置类的类名

  • 提供自动配置类的jar包中,需要提供配置文件META-INF/spring.factories
  • 使用ClassLoader的getResource和getResources方法,读取classpath中的配置文件并使用反射实例化

例如mybatis-spring-boot-starter的包结构:

image-20220410232256701

总结

springboot的自动装配核心流程:springboot程序启动,通过spring factories机制加载classpath下的META-INF/spring.factories文件,筛选出所有EnableAutoConfiguration的配置类,反射实例化后注入到springIOC容器中。

todo:实现一个自定义springboot-starter

',34),s=[t];function l(n,p,c,g,m,h){return i(),r("div",null,s)}const b=a(e,[["render",l]]);export{u as __pageData,b as default}; diff --git "a/assets/java_framework_JavaSPI\346\234\272\345\210\266\345\222\214Springboot\350\207\252\345\212\250\350\243\205\351\205\215\345\216\237\347\220\206.md.6a5d7c18.lean.js" "b/assets/java_framework_JavaSPI\346\234\272\345\210\266\345\222\214Springboot\350\207\252\345\212\250\350\243\205\351\205\215\345\216\237\347\220\206.md.6a5d7c18.lean.js" new file mode 100644 index 000000000..46eee6d3d --- /dev/null +++ "b/assets/java_framework_JavaSPI\346\234\272\345\210\266\345\222\214Springboot\350\207\252\345\212\250\350\243\205\351\205\215\345\216\237\347\220\206.md.6a5d7c18.lean.js" @@ -0,0 +1 @@ +import{_ as a,o as i,c as r,Q as o}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"JavaSPI机制和Springboot自动装配原理","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/JavaSPI机制和Springboot自动装配原理.md","filePath":"java/framework/JavaSPI机制和Springboot自动装配原理.md","lastUpdated":1708510102000}'),e={name:"java/framework/JavaSPI机制和Springboot自动装配原理.md"},t=o("",34),s=[t];function l(n,p,c,g,m,h){return i(),r("div",null,s)}const b=a(e,[["render",l]]);export{u as __pageData,b as default}; diff --git "a/assets/java_framework_Java\346\227\245\345\277\227\345\217\221\345\261\225\345\216\206\345\217\262.md.f427bc3e.js" "b/assets/java_framework_Java\346\227\245\345\277\227\345\217\221\345\261\225\345\216\206\345\217\262.md.f427bc3e.js" new file mode 100644 index 000000000..d4c90abb2 --- /dev/null +++ "b/assets/java_framework_Java\346\227\245\345\277\227\345\217\221\345\261\225\345\216\206\345\217\262.md.f427bc3e.js" @@ -0,0 +1 @@ +import{_ as a,o,c as e,Q as l}from"./chunks/framework.dc1f4608.js";const j=JSON.parse('{"title":"Java日志发展历史","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/Java日志发展历史.md","filePath":"java/framework/Java日志发展历史.md","lastUpdated":1708510102000}'),t={name:"java/framework/Java日志发展历史.md"},r=l('

Java日志发展历史

最近java社区被log4j2的远程代码执行漏洞引爆了,不过还好我们公司的日志主要用的logback,只有少数几个服务用的log4j2,不过还是因为这个加了会班=.=。而java日志有很多乱七八糟的log4j,log4j2,logback,slf4j,jul。。。

https://www.bilibili.com/video/BV1U44y1E7sE

日志发展史

阶段一

2001年以前,java是没有日志库的,打印日志全靠System.outSystem.err

缺点:

1.产生大量的io操作

2.输出的内容不能保存到文件

3.只能打印在控制台,打印完就看不到了

4.无法定制化,且粒度不够细

阶段二

大佬Ceki Gülcü在2001年开发出了日志库Log4j,后来Log4j成为了Apache项目,Ceki大佬也加入了Apache组织

Apache曾经还建议过SUN公司引入Log4j到java标准库中,但被拒绝了

阶段三

2002年2月JDK1.4发布,SUN推出了自己的日志标准库JUL(Java Util Logging),其实是照着log4j抄的,但是没抄好,在JDK1.5之后性能和可用性才有所提升

由于Log4j比JUL好用,并且比较成熟,所以Log4j更具优势

阶段四

2002年8月Apache推出了JCL(Jakarta Commons Logging),也就是日志抽象层,支持运行时动态加载日志组件的实现,也提供了一个默认实现的Simple Log。

(在ClassLoader中进行查找,如果能找到Log4j就默认使用Log4j的实现,如果没有则使用JUL实现,再没有则使用JCL内部提供的Simple Log实现)

但是JCL有三个缺点:

  1. 效率较低
  2. 容易引发混乱
  3. 使用了自定义ClassLoader的程序中,会引发内存泄露

阶段五

2006年大佬Ceki Gülcü(Log4j)因为一些原因离开了Apache,之后Ceki觉得JCL不好用,自己重新开发了一套新的日志标准接口规范Slf4j(Simple Logging Facade for Java),也可称为日志门面,很明显Slf4j是为了对标JCL,后面也证明了Slf4j比JCL更加优秀

大佬Ceki提供了一系列的桥接包来帮助Slf4j接口与其他日志库建立关系,这种方式称为桥接设置模式。

代码使用Slf4j接口,就可以实现日志的统一标准化,后续如果想要更换日志的实现,只需要引入Slf4j和相关的桥接包,再引入具体的日志标准库即可。

阶段六

Ceki大佬觉得市场上的日志标准库都是间接实现Slf4j接口,每次都需要配合桥接包,因此在2016年,Ceki大佬基于Slf4j接口开发出了Logback日志标准库作为Slf4j的默认实现,Logback也十分给力,在功能的完整度和性能上超越了所有已有的日志标准库

阶段七

2012年,Apache推出新项目Log4j2(不兼容Log4j),Log4j2全面借鉴了Slf4j+Logback,虽然log4j2有明显抄袭嫌疑,但是汲取了logback优秀的设计,还解决了一些问题,性能有了极大提升,官方测试是18倍

Log4j2不仅具有Logback的所有特性,还做了分离设计,分为log4j-api和log4j-core,api是日志接口,core是日志标准库,而且Apache也为Log4j2提供了各种桥接包

',29),i=[r];function c(p,h,s,d,n,b){return o(),e("div",null,i)}const g=a(t,[["render",c]]);export{j as __pageData,g as default}; diff --git "a/assets/java_framework_Java\346\227\245\345\277\227\345\217\221\345\261\225\345\216\206\345\217\262.md.f427bc3e.lean.js" "b/assets/java_framework_Java\346\227\245\345\277\227\345\217\221\345\261\225\345\216\206\345\217\262.md.f427bc3e.lean.js" new file mode 100644 index 000000000..761b7d926 --- /dev/null +++ "b/assets/java_framework_Java\346\227\245\345\277\227\345\217\221\345\261\225\345\216\206\345\217\262.md.f427bc3e.lean.js" @@ -0,0 +1 @@ +import{_ as a,o,c as e,Q as l}from"./chunks/framework.dc1f4608.js";const j=JSON.parse('{"title":"Java日志发展历史","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/Java日志发展历史.md","filePath":"java/framework/Java日志发展历史.md","lastUpdated":1708510102000}'),t={name:"java/framework/Java日志发展历史.md"},r=l("",29),i=[r];function c(p,h,s,d,n,b){return o(),e("div",null,i)}const g=a(t,[["render",c]]);export{j as __pageData,g as default}; diff --git "a/assets/java_framework_Mybatis Generator\351\205\215\347\275\256.md.c35e48c8.js" "b/assets/java_framework_Mybatis Generator\351\205\215\347\275\256.md.c35e48c8.js" new file mode 100644 index 000000000..2a0b23309 --- /dev/null +++ "b/assets/java_framework_Mybatis Generator\351\205\215\347\275\256.md.c35e48c8.js" @@ -0,0 +1,737 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const g=JSON.parse('{"title":"Mybatis Generator配置","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/Mybatis Generator配置.md","filePath":"java/framework/Mybatis Generator配置.md","lastUpdated":1708510102000}'),p={name:"java/framework/Mybatis Generator配置.md"},o=l(`

Mybatis Generator配置

  1. 仓库地址:https://github.com/mybatis/generator
  2. 官方文档:http://www.mybatis.org/generator/index.html

mbg逆向工程能快速生成实体类和mapper文件以及xml,提升开发效率。记录下不同持久层框架对应的mbg配置备忘。

  • mbg的context标签可以自定义targetRuntime,具体的区别可以在http://mybatis.org/generator/quickstart.html#target-runtime-information-and-samples查看。MyBatis3,生成的代码量比较大,会有byExample和selective相关的代码生成。MyBatis3Simple生成的代码量比较小,不会有byExample和selective方法生成。

  • 常用运行方式(还包含 ant、命令行、eclipse)

    • java代码
    • maven plugin
  • maven依赖

    xml
    <dependency>
    +			<groupId>org.mybatis.generator</groupId>
    +			<artifactId>mybatis-generator-core</artifactId>
    +			<version>1.3.5</version>
    +</dependency>
    +<dependency>
    +       <groupId>mysql</groupId>
    +       <artifactId>mysql-connector-java</artifactId>
    +       <version>8.0.16</version>
    +</dependency>
    <dependency>
    +			<groupId>org.mybatis.generator</groupId>
    +			<artifactId>mybatis-generator-core</artifactId>
    +			<version>1.3.5</version>
    +</dependency>
    +<dependency>
    +       <groupId>mysql</groupId>
    +       <artifactId>mysql-connector-java</artifactId>
    +       <version>8.0.16</version>
    +</dependency>

    使用maven插件时

    xml
    <build>
    +        <plugins>
    +            <plugin>
    +                <groupId>org.mybatis.generator</groupId>
    +                <artifactId>mybatis-generator-maven-plugin</artifactId>
    +                <version>1.3.5</version>
    +                <configuration>
    +                    <!-- 在控制台打印执行日志 -->
    +                    <verbose>true</verbose>
    +                    <!-- 重复生成时会覆盖之前的文件-->
    +                    <overwrite>true</overwrite>
    +                 <configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
    +                </configuration>
    +                <dependencies>
    +                    <dependency>
    +                        <groupId>mysql</groupId>
    +                        <artifactId>mysql-connector-java</artifactId>
    +                        <version>8.0.16</version>
    +                    </dependency>
    +                </dependencies>
    +            </plugin>
    +        </plugins>
    +    </build>
    <build>
    +        <plugins>
    +            <plugin>
    +                <groupId>org.mybatis.generator</groupId>
    +                <artifactId>mybatis-generator-maven-plugin</artifactId>
    +                <version>1.3.5</version>
    +                <configuration>
    +                    <!-- 在控制台打印执行日志 -->
    +                    <verbose>true</verbose>
    +                    <!-- 重复生成时会覆盖之前的文件-->
    +                    <overwrite>true</overwrite>
    +                 <configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
    +                </configuration>
    +                <dependencies>
    +                    <dependency>
    +                        <groupId>mysql</groupId>
    +                        <artifactId>mysql-connector-java</artifactId>
    +                        <version>8.0.16</version>
    +                    </dependency>
    +                </dependencies>
    +            </plugin>
    +        </plugins>
    +    </build>

通用mapper

xml
<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE generatorConfiguration
+        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
+        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
+
+<generatorConfiguration>
+
+    <context id="Mysql" targetRuntime="MyBatis3Simple"
+             defaultModelType="flat">
+
+        <!--property*,-->
+        <property name="beginningDelimiter" value="\`"/>
+        <property name="endingDelimiter" value="\`"/>
+
+        <!--plugin*,-->
+        <!-- 为继承的BaseMapper接口添加对应的实现类 -->
+        <plugin type="tk.mybatis.mapper.generator.MapperPlugin">
+            <property name="mappers" value="cn.xxx.CustomBaseMapper"/>
+        </plugin>
+
+        <!--commentGenerator?,-->
+        <!--<commentGenerator type="mybatis.generator.MyCommentGenerator"></commentGenerator>-->
+
+        <!--jdbcConnection,-->
+        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
+                        connectionURL="jdbc:mysql://xxx.xxx.xxx/xxx?autoReconnect=true&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;tinyInt1isBit=false&amp;useSSL=false"
+                        userId=""
+                        password="">
+        </jdbcConnection>
+
+        <!--javaTypeResolver?, 自定义jdbcType和javaType的映射关系,比如默认的TINYINT会对应Java的Byte类型,如果我们想让TINYINT对应JavaType为Integer就需要在解析类中自定义,这种方式适合源码方式运行mbg,使用maven plugin会比较麻烦-->
+        <javaTypeResolver type="mybatis.generator.MyJavaTypeResolver"></javaTypeResolver>
+
+        <!--javaModelGenerator,-->
+        <javaModelGenerator targetPackage="cn.xxx.dao.entity"
+                            targetProject="/Users/story/project/xx/src/main/java">
+            <!--<property name="rootClass" value="xx.BaseEntity"/>  entity会继承的类-->
+        </javaModelGenerator>
+
+        <!--sqlMapGenerator?,-->
+        <sqlMapGenerator targetPackage="mapper"
+                         targetProject="/Users/story/project/xx/src/main/resources"/>
+
+        <!--javaClientGenerator?,-->
+        <javaClientGenerator targetPackage="cn.superdesk.app.wechat.dao.mapper"
+                             targetProject="/Users/story/project/xxx/src/main/java"
+                             type="XMLMAPPER"/>
+
+        <!--table+-->
+        <table tableName="tb_xx" domainObjectName="XxEntity">
+            <generatedKey column="id" sqlStatement="Mysql" identity="true"/>
+        </table>
+        
+    </context>
+
+</generatorConfiguration>
<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE generatorConfiguration
+        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
+        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
+
+<generatorConfiguration>
+
+    <context id="Mysql" targetRuntime="MyBatis3Simple"
+             defaultModelType="flat">
+
+        <!--property*,-->
+        <property name="beginningDelimiter" value="\`"/>
+        <property name="endingDelimiter" value="\`"/>
+
+        <!--plugin*,-->
+        <!-- 为继承的BaseMapper接口添加对应的实现类 -->
+        <plugin type="tk.mybatis.mapper.generator.MapperPlugin">
+            <property name="mappers" value="cn.xxx.CustomBaseMapper"/>
+        </plugin>
+
+        <!--commentGenerator?,-->
+        <!--<commentGenerator type="mybatis.generator.MyCommentGenerator"></commentGenerator>-->
+
+        <!--jdbcConnection,-->
+        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
+                        connectionURL="jdbc:mysql://xxx.xxx.xxx/xxx?autoReconnect=true&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;tinyInt1isBit=false&amp;useSSL=false"
+                        userId=""
+                        password="">
+        </jdbcConnection>
+
+        <!--javaTypeResolver?, 自定义jdbcType和javaType的映射关系,比如默认的TINYINT会对应Java的Byte类型,如果我们想让TINYINT对应JavaType为Integer就需要在解析类中自定义,这种方式适合源码方式运行mbg,使用maven plugin会比较麻烦-->
+        <javaTypeResolver type="mybatis.generator.MyJavaTypeResolver"></javaTypeResolver>
+
+        <!--javaModelGenerator,-->
+        <javaModelGenerator targetPackage="cn.xxx.dao.entity"
+                            targetProject="/Users/story/project/xx/src/main/java">
+            <!--<property name="rootClass" value="xx.BaseEntity"/>  entity会继承的类-->
+        </javaModelGenerator>
+
+        <!--sqlMapGenerator?,-->
+        <sqlMapGenerator targetPackage="mapper"
+                         targetProject="/Users/story/project/xx/src/main/resources"/>
+
+        <!--javaClientGenerator?,-->
+        <javaClientGenerator targetPackage="cn.superdesk.app.wechat.dao.mapper"
+                             targetProject="/Users/story/project/xxx/src/main/java"
+                             type="XMLMAPPER"/>
+
+        <!--table+-->
+        <table tableName="tb_xx" domainObjectName="XxEntity">
+            <generatedKey column="id" sqlStatement="Mysql" identity="true"/>
+        </table>
+        
+    </context>
+
+</generatorConfiguration>
  • 生成的Mapper统一继承的接口
java
public interface CustomBaseMapper<T> extends tk.mybatis.mapper.common.BaseMapper<T>, MySqlMapper<T> {
+
+}
public interface CustomBaseMapper<T> extends tk.mybatis.mapper.common.BaseMapper<T>, MySqlMapper<T> {
+
+}
  • 自定义类型解析器
java
public class MyJavaTypeResolver implements JavaTypeResolver {
+
+    protected List<String> warnings;
+
+    protected Properties properties;
+
+    protected Context context;
+
+    protected boolean forceBigDecimals;
+
+    protected Map<Integer, JavaTypeResolverDefaultImpl.JdbcTypeInformation> typeMap;
+
+    public MyJavaTypeResolver() {
+        super();
+        properties = new Properties();
+        typeMap = new HashMap<Integer, JavaTypeResolverDefaultImpl.JdbcTypeInformation>();
+
+        typeMap.put(Types.ARRAY, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("ARRAY", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.BIGINT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("BIGINT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Long.class.getName())));
+        typeMap.put(Types.BINARY, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("BINARY", //$NON-NLS-1$
+                new FullyQualifiedJavaType("byte[]"))); //$NON-NLS-1$
+        typeMap.put(Types.BIT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("BIT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Boolean.class.getName())));
+        typeMap.put(Types.BLOB, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("BLOB", //$NON-NLS-1$
+                new FullyQualifiedJavaType("byte[]"))); //$NON-NLS-1$
+        typeMap.put(Types.BOOLEAN, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("BOOLEAN", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Boolean.class.getName())));
+        typeMap.put(Types.CHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("CHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Types.CLOB, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("CLOB", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Types.DATALINK, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("DATALINK", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.DATE, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("DATE", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Date.class.getName())));
+        typeMap.put(Types.DISTINCT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("DISTINCT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.DOUBLE, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("DOUBLE", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Double.class.getName())));
+        typeMap.put(Types.FLOAT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("FLOAT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Double.class.getName())));
+        typeMap.put(Types.INTEGER, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("INTEGER", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Integer.class.getName())));
+        typeMap.put(Types.JAVA_OBJECT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("JAVA_OBJECT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Jdbc4Types.LONGNVARCHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("LONGNVARCHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Types.LONGVARBINARY, new JavaTypeResolverDefaultImpl.JdbcTypeInformation(
+                "LONGVARBINARY", //$NON-NLS-1$
+                new FullyQualifiedJavaType("byte[]"))); //$NON-NLS-1$
+        typeMap.put(Types.LONGVARCHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("LONGVARCHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Jdbc4Types.NCHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("NCHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Jdbc4Types.NCLOB, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("NCLOB", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Jdbc4Types.NVARCHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("NVARCHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Types.NULL, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("NULL", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.OTHER, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("OTHER", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.REAL, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("REAL", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Float.class.getName())));
+        typeMap.put(Types.REF, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("REF", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.SMALLINT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("SMALLINT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Integer.class.getName())));
+        typeMap.put(Types.STRUCT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("STRUCT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.TIME, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("TIME", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Date.class.getName())));
+        typeMap.put(Types.TIMESTAMP, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("TIMESTAMP", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Date.class.getName())));
+
+        typeMap.put(Types.TINYINT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("TINYINT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Integer.class.getName())));
+
+        typeMap.put(Types.VARBINARY, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("VARBINARY", //$NON-NLS-1$
+                new FullyQualifiedJavaType("byte[]"))); //$NON-NLS-1$
+        typeMap.put(Types.VARCHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("VARCHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+
+    }
+
+    @Override
+    public void addConfigurationProperties(Properties properties) {
+        this.properties.putAll(properties);
+        forceBigDecimals = StringUtility.isTrue(properties.getProperty(PropertyRegistry.TYPE_RESOLVER_FORCE_BIG_DECIMALS));
+    }
+
+    @Override
+    public FullyQualifiedJavaType calculateJavaType(
+            IntrospectedColumn introspectedColumn) {
+        FullyQualifiedJavaType answer;
+        JavaTypeResolverDefaultImpl.JdbcTypeInformation jdbcTypeInformation = typeMap
+                .get(introspectedColumn.getJdbcType());
+
+        if (jdbcTypeInformation == null) {
+            switch (introspectedColumn.getJdbcType()) {
+                case Types.DECIMAL:
+                case Types.NUMERIC:
+                    if (introspectedColumn.getScale() > 0
+                            || introspectedColumn.getLength() > 18
+                            || forceBigDecimals) {
+                        answer = new FullyQualifiedJavaType(BigDecimal.class
+                                .getName());
+                    } else if (introspectedColumn.getLength() > 9) {
+                        answer = new FullyQualifiedJavaType(Long.class.getName());
+                    } else if (introspectedColumn.getLength() > 4) {
+                        answer = new FullyQualifiedJavaType(Integer.class.getName());
+                    } else {
+                        answer = new FullyQualifiedJavaType(Short.class.getName());
+                    }
+                    break;
+
+                default:
+                    answer = null;
+                    break;
+            }
+        } else {
+            answer = jdbcTypeInformation.getFullyQualifiedJavaType();
+        }
+
+        return answer;
+    }
+
+    @Override
+    public String calculateJdbcTypeName(IntrospectedColumn introspectedColumn) {
+        String answer;
+        JavaTypeResolverDefaultImpl.JdbcTypeInformation jdbcTypeInformation = typeMap
+                .get(introspectedColumn.getJdbcType());
+
+        if (jdbcTypeInformation == null) {
+            switch (introspectedColumn.getJdbcType()) {
+                case Types.DECIMAL:
+                    answer = "DECIMAL"; //$NON-NLS-1$
+                    break;
+                case Types.NUMERIC:
+                    answer = "NUMERIC"; //$NON-NLS-1$
+                    break;
+                default:
+                    answer = null;
+                    break;
+            }
+        } else {
+            answer = jdbcTypeInformation.getJdbcTypeName();
+        }
+
+        return answer;
+    }
+
+    @Override
+    public void setWarnings(List<String> warnings) {
+        this.warnings = warnings;
+    }
+
+    @Override
+    public void setContext(Context context) {
+        this.context = context;
+    }
+
+    public static class JdbcTypeInformation {
+        private String jdbcTypeName;
+
+        private FullyQualifiedJavaType fullyQualifiedJavaType;
+
+        public JdbcTypeInformation(String jdbcTypeName,
+                                   FullyQualifiedJavaType fullyQualifiedJavaType) {
+            this.jdbcTypeName = jdbcTypeName;
+            this.fullyQualifiedJavaType = fullyQualifiedJavaType;
+        }
+
+        public String getJdbcTypeName() {
+            return jdbcTypeName;
+        }
+
+        public FullyQualifiedJavaType getFullyQualifiedJavaType() {
+            return fullyQualifiedJavaType;
+        }
+    }
+}
public class MyJavaTypeResolver implements JavaTypeResolver {
+
+    protected List<String> warnings;
+
+    protected Properties properties;
+
+    protected Context context;
+
+    protected boolean forceBigDecimals;
+
+    protected Map<Integer, JavaTypeResolverDefaultImpl.JdbcTypeInformation> typeMap;
+
+    public MyJavaTypeResolver() {
+        super();
+        properties = new Properties();
+        typeMap = new HashMap<Integer, JavaTypeResolverDefaultImpl.JdbcTypeInformation>();
+
+        typeMap.put(Types.ARRAY, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("ARRAY", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.BIGINT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("BIGINT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Long.class.getName())));
+        typeMap.put(Types.BINARY, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("BINARY", //$NON-NLS-1$
+                new FullyQualifiedJavaType("byte[]"))); //$NON-NLS-1$
+        typeMap.put(Types.BIT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("BIT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Boolean.class.getName())));
+        typeMap.put(Types.BLOB, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("BLOB", //$NON-NLS-1$
+                new FullyQualifiedJavaType("byte[]"))); //$NON-NLS-1$
+        typeMap.put(Types.BOOLEAN, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("BOOLEAN", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Boolean.class.getName())));
+        typeMap.put(Types.CHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("CHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Types.CLOB, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("CLOB", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Types.DATALINK, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("DATALINK", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.DATE, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("DATE", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Date.class.getName())));
+        typeMap.put(Types.DISTINCT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("DISTINCT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.DOUBLE, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("DOUBLE", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Double.class.getName())));
+        typeMap.put(Types.FLOAT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("FLOAT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Double.class.getName())));
+        typeMap.put(Types.INTEGER, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("INTEGER", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Integer.class.getName())));
+        typeMap.put(Types.JAVA_OBJECT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("JAVA_OBJECT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Jdbc4Types.LONGNVARCHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("LONGNVARCHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Types.LONGVARBINARY, new JavaTypeResolverDefaultImpl.JdbcTypeInformation(
+                "LONGVARBINARY", //$NON-NLS-1$
+                new FullyQualifiedJavaType("byte[]"))); //$NON-NLS-1$
+        typeMap.put(Types.LONGVARCHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("LONGVARCHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Jdbc4Types.NCHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("NCHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Jdbc4Types.NCLOB, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("NCLOB", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Jdbc4Types.NVARCHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("NVARCHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Types.NULL, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("NULL", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.OTHER, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("OTHER", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.REAL, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("REAL", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Float.class.getName())));
+        typeMap.put(Types.REF, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("REF", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.SMALLINT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("SMALLINT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Integer.class.getName())));
+        typeMap.put(Types.STRUCT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("STRUCT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.TIME, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("TIME", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Date.class.getName())));
+        typeMap.put(Types.TIMESTAMP, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("TIMESTAMP", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Date.class.getName())));
+
+        typeMap.put(Types.TINYINT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("TINYINT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Integer.class.getName())));
+
+        typeMap.put(Types.VARBINARY, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("VARBINARY", //$NON-NLS-1$
+                new FullyQualifiedJavaType("byte[]"))); //$NON-NLS-1$
+        typeMap.put(Types.VARCHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("VARCHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+
+    }
+
+    @Override
+    public void addConfigurationProperties(Properties properties) {
+        this.properties.putAll(properties);
+        forceBigDecimals = StringUtility.isTrue(properties.getProperty(PropertyRegistry.TYPE_RESOLVER_FORCE_BIG_DECIMALS));
+    }
+
+    @Override
+    public FullyQualifiedJavaType calculateJavaType(
+            IntrospectedColumn introspectedColumn) {
+        FullyQualifiedJavaType answer;
+        JavaTypeResolverDefaultImpl.JdbcTypeInformation jdbcTypeInformation = typeMap
+                .get(introspectedColumn.getJdbcType());
+
+        if (jdbcTypeInformation == null) {
+            switch (introspectedColumn.getJdbcType()) {
+                case Types.DECIMAL:
+                case Types.NUMERIC:
+                    if (introspectedColumn.getScale() > 0
+                            || introspectedColumn.getLength() > 18
+                            || forceBigDecimals) {
+                        answer = new FullyQualifiedJavaType(BigDecimal.class
+                                .getName());
+                    } else if (introspectedColumn.getLength() > 9) {
+                        answer = new FullyQualifiedJavaType(Long.class.getName());
+                    } else if (introspectedColumn.getLength() > 4) {
+                        answer = new FullyQualifiedJavaType(Integer.class.getName());
+                    } else {
+                        answer = new FullyQualifiedJavaType(Short.class.getName());
+                    }
+                    break;
+
+                default:
+                    answer = null;
+                    break;
+            }
+        } else {
+            answer = jdbcTypeInformation.getFullyQualifiedJavaType();
+        }
+
+        return answer;
+    }
+
+    @Override
+    public String calculateJdbcTypeName(IntrospectedColumn introspectedColumn) {
+        String answer;
+        JavaTypeResolverDefaultImpl.JdbcTypeInformation jdbcTypeInformation = typeMap
+                .get(introspectedColumn.getJdbcType());
+
+        if (jdbcTypeInformation == null) {
+            switch (introspectedColumn.getJdbcType()) {
+                case Types.DECIMAL:
+                    answer = "DECIMAL"; //$NON-NLS-1$
+                    break;
+                case Types.NUMERIC:
+                    answer = "NUMERIC"; //$NON-NLS-1$
+                    break;
+                default:
+                    answer = null;
+                    break;
+            }
+        } else {
+            answer = jdbcTypeInformation.getJdbcTypeName();
+        }
+
+        return answer;
+    }
+
+    @Override
+    public void setWarnings(List<String> warnings) {
+        this.warnings = warnings;
+    }
+
+    @Override
+    public void setContext(Context context) {
+        this.context = context;
+    }
+
+    public static class JdbcTypeInformation {
+        private String jdbcTypeName;
+
+        private FullyQualifiedJavaType fullyQualifiedJavaType;
+
+        public JdbcTypeInformation(String jdbcTypeName,
+                                   FullyQualifiedJavaType fullyQualifiedJavaType) {
+            this.jdbcTypeName = jdbcTypeName;
+            this.fullyQualifiedJavaType = fullyQualifiedJavaType;
+        }
+
+        public String getJdbcTypeName() {
+            return jdbcTypeName;
+        }
+
+        public FullyQualifiedJavaType getFullyQualifiedJavaType() {
+            return fullyQualifiedJavaType;
+        }
+    }
+}
  • 入口方法
java
public class MyBatisGeneratorTool {
+    public static void main(String[] args) {
+
+        URL resource = Thread.currentThread().getContextClassLoader().getResource("");
+
+        System.out.println(resource.getPath());
+
+        List<String>        warnings   = new ArrayList<String>();
+        boolean             overwrite  = true;
+        File                configFile = new File(resource.getPath() + "../../src/test/resources/generator/generatorConfig.xml");
+        ConfigurationParser cp         = new ConfigurationParser(warnings);
+        Configuration       config     = null;
+
+        try {
+            config = cp.parseConfiguration(configFile);
+        } catch (IOException e) {
+            e.printStackTrace();
+        } catch (XMLParserException e) {
+            e.printStackTrace();
+        }
+
+        DefaultShellCallback callback         = new DefaultShellCallback(overwrite);
+        MyBatisGenerator     myBatisGenerator = null;
+
+        try {
+            myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
+        } catch (InvalidConfigurationException e) {
+            e.printStackTrace();
+        }
+
+        try {
+            myBatisGenerator.generate(null);
+        } catch (SQLException e) {
+            e.printStackTrace();
+        } catch (IOException e) {
+            e.printStackTrace();
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+}
public class MyBatisGeneratorTool {
+    public static void main(String[] args) {
+
+        URL resource = Thread.currentThread().getContextClassLoader().getResource("");
+
+        System.out.println(resource.getPath());
+
+        List<String>        warnings   = new ArrayList<String>();
+        boolean             overwrite  = true;
+        File                configFile = new File(resource.getPath() + "../../src/test/resources/generator/generatorConfig.xml");
+        ConfigurationParser cp         = new ConfigurationParser(warnings);
+        Configuration       config     = null;
+
+        try {
+            config = cp.parseConfiguration(configFile);
+        } catch (IOException e) {
+            e.printStackTrace();
+        } catch (XMLParserException e) {
+            e.printStackTrace();
+        }
+
+        DefaultShellCallback callback         = new DefaultShellCallback(overwrite);
+        MyBatisGenerator     myBatisGenerator = null;
+
+        try {
+            myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
+        } catch (InvalidConfigurationException e) {
+            e.printStackTrace();
+        }
+
+        try {
+            myBatisGenerator.generate(null);
+        } catch (SQLException e) {
+            e.printStackTrace();
+        } catch (IOException e) {
+            e.printStackTrace();
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+}

image-20220409210512601

mybatis-plus

xml
<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE generatorConfiguration
+        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
+        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
+<generatorConfiguration>
+    <!-- context 是逆向工程的主要配置信息 -->
+    <!-- id:起个名字 -->
+    <!-- targetRuntime:设置生成的文件适用于那个 mybatis 版本 -->
+    <context id="default" targetRuntime="MyBatis3">
+        <!--optional,指在创建class时,对注释进行控制-->
+        <commentGenerator>
+            <property name="suppressDate" value="true"/>
+            <!-- 是否去除自动生成的注释 true:是 : false:否 -->
+            <property name="suppressAllComments" value="true"/>
+        </commentGenerator>
+        <!--jdbc的数据库连接 wg_insert 为数据库名字-->
+        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
+                        connectionURL="jdbc:mysql://xxx/xx?autoReconnect=true&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;tinyInt1isBit=false&amp;useSSL=false&amp;serverTimezone=Asia/Shanghai&amp;nullCatalogMeansCurrent=true"
+                        userId=""
+                        password="">
+        </jdbcConnection>
+        <!--非必须,类型处理器,在数据库类型和java类型之间的转换控制-->
+        <javaTypeResolver>
+            <!-- 默认情况下数据库中的 decimal,bigInt 在 Java 对应是 sql 下的 BigDecimal 类 -->
+            <!-- 不是 double 和 long 类型 -->
+            <!-- 使用常用的基本类型代替 sql 包下的引用类型 -->
+            <property name="forceBigDecimals" value="false"/>
+        </javaTypeResolver>
+        <!-- targetPackage:生成的实体类所在的包 -->
+        <!-- targetProject:生成的实体类所在的硬盘位置 -->
+        <javaModelGenerator targetPackage="xxx"
+                            targetProject="/Users/story/project/xxx/src/main/java">
+            <!-- 是否允许子包 -->
+            <property name="enableSubPackages" value="false"/>
+            <!-- 是否对modal添加构造函数 -->
+<!--            <property name="constructorBased" value="true"/>-->
+            <!-- 是否清理从数据库中查询出的字符串左右两边的空白字符 -->
+<!--            <property name="trimStrings" value="true"/>-->
+            <!-- 建立modal对象是否不可改变 即生成的modal对象不会有setter方法,只有构造方法 -->
+<!--            <property name="immutable" value="false"/>-->
+        </javaModelGenerator>
+        <!-- targetPackage 和 targetProject:生成的 mapper 文件的包和位置 -->
+        <sqlMapGenerator targetPackage="xx"
+                         targetProject="/Users/story/project/xx/src/main/resources">
+            <!-- 针对数据库的一个配置,是否把 schema 作为字包名 -->
+            <property name="enableSubPackages" value="false"/>
+        </sqlMapGenerator>
+        <!-- targetPackage 和 targetProject:生成的 interface 文件的包和位置 -->
+        <javaClientGenerator type="XMLMAPPER"
+                             targetPackage="xx" targetProject="/Users/story/project/xxx/src/main/java">
+            <!-- 针对 oracle 数据库的一个配置,是否把 schema 作为字包名 -->
+            <property name="enableSubPackages" value="false"/>
+        </javaClientGenerator>
+        <!-- tableName是数据库中的表名,domainObjectName是生成的JAVA模型名,后面的参数不用改,要生成更多的表就在下面继续加table标签 -->
+        <table tableName="xxx" domainObjectName="XxxEntity">
+        </table>
+    </context>
+</generatorConfiguration>
<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE generatorConfiguration
+        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
+        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
+<generatorConfiguration>
+    <!-- context 是逆向工程的主要配置信息 -->
+    <!-- id:起个名字 -->
+    <!-- targetRuntime:设置生成的文件适用于那个 mybatis 版本 -->
+    <context id="default" targetRuntime="MyBatis3">
+        <!--optional,指在创建class时,对注释进行控制-->
+        <commentGenerator>
+            <property name="suppressDate" value="true"/>
+            <!-- 是否去除自动生成的注释 true:是 : false:否 -->
+            <property name="suppressAllComments" value="true"/>
+        </commentGenerator>
+        <!--jdbc的数据库连接 wg_insert 为数据库名字-->
+        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
+                        connectionURL="jdbc:mysql://xxx/xx?autoReconnect=true&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;tinyInt1isBit=false&amp;useSSL=false&amp;serverTimezone=Asia/Shanghai&amp;nullCatalogMeansCurrent=true"
+                        userId=""
+                        password="">
+        </jdbcConnection>
+        <!--非必须,类型处理器,在数据库类型和java类型之间的转换控制-->
+        <javaTypeResolver>
+            <!-- 默认情况下数据库中的 decimal,bigInt 在 Java 对应是 sql 下的 BigDecimal 类 -->
+            <!-- 不是 double 和 long 类型 -->
+            <!-- 使用常用的基本类型代替 sql 包下的引用类型 -->
+            <property name="forceBigDecimals" value="false"/>
+        </javaTypeResolver>
+        <!-- targetPackage:生成的实体类所在的包 -->
+        <!-- targetProject:生成的实体类所在的硬盘位置 -->
+        <javaModelGenerator targetPackage="xxx"
+                            targetProject="/Users/story/project/xxx/src/main/java">
+            <!-- 是否允许子包 -->
+            <property name="enableSubPackages" value="false"/>
+            <!-- 是否对modal添加构造函数 -->
+<!--            <property name="constructorBased" value="true"/>-->
+            <!-- 是否清理从数据库中查询出的字符串左右两边的空白字符 -->
+<!--            <property name="trimStrings" value="true"/>-->
+            <!-- 建立modal对象是否不可改变 即生成的modal对象不会有setter方法,只有构造方法 -->
+<!--            <property name="immutable" value="false"/>-->
+        </javaModelGenerator>
+        <!-- targetPackage 和 targetProject:生成的 mapper 文件的包和位置 -->
+        <sqlMapGenerator targetPackage="xx"
+                         targetProject="/Users/story/project/xx/src/main/resources">
+            <!-- 针对数据库的一个配置,是否把 schema 作为字包名 -->
+            <property name="enableSubPackages" value="false"/>
+        </sqlMapGenerator>
+        <!-- targetPackage 和 targetProject:生成的 interface 文件的包和位置 -->
+        <javaClientGenerator type="XMLMAPPER"
+                             targetPackage="xx" targetProject="/Users/story/project/xxx/src/main/java">
+            <!-- 针对 oracle 数据库的一个配置,是否把 schema 作为字包名 -->
+            <property name="enableSubPackages" value="false"/>
+        </javaClientGenerator>
+        <!-- tableName是数据库中的表名,domainObjectName是生成的JAVA模型名,后面的参数不用改,要生成更多的表就在下面继续加table标签 -->
+        <table tableName="xxx" domainObjectName="XxxEntity">
+        </table>
+    </context>
+</generatorConfiguration>

image-20220409211128783

`,16),e=[o];function t(c,r,y,E,i,F){return a(),n("div",null,e)}const m=s(p,[["render",t]]);export{g as __pageData,m as default}; diff --git "a/assets/java_framework_Mybatis Generator\351\205\215\347\275\256.md.c35e48c8.lean.js" "b/assets/java_framework_Mybatis Generator\351\205\215\347\275\256.md.c35e48c8.lean.js" new file mode 100644 index 000000000..fbc94b52d --- /dev/null +++ "b/assets/java_framework_Mybatis Generator\351\205\215\347\275\256.md.c35e48c8.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const g=JSON.parse('{"title":"Mybatis Generator配置","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/Mybatis Generator配置.md","filePath":"java/framework/Mybatis Generator配置.md","lastUpdated":1708510102000}'),p={name:"java/framework/Mybatis Generator配置.md"},o=l("",16),e=[o];function t(c,r,y,E,i,F){return a(),n("div",null,e)}const m=s(p,[["render",t]]);export{g as __pageData,m as default}; diff --git "a/assets/java_framework_POI\344\272\213\344\273\266\346\250\241\345\274\217\350\247\243\346\236\220\345\271\266\350\257\273\345\217\226Excel\346\226\207\344\273\266\346\225\260\346\215\256.md.ecde083a.js" "b/assets/java_framework_POI\344\272\213\344\273\266\346\250\241\345\274\217\350\247\243\346\236\220\345\271\266\350\257\273\345\217\226Excel\346\226\207\344\273\266\346\225\260\346\215\256.md.ecde083a.js" new file mode 100644 index 000000000..e098ed7e6 --- /dev/null +++ "b/assets/java_framework_POI\344\272\213\344\273\266\346\250\241\345\274\217\350\247\243\346\236\220\345\271\266\350\257\273\345\217\226Excel\346\226\207\344\273\266\346\225\260\346\215\256.md.ecde083a.js" @@ -0,0 +1,1809 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const A=JSON.parse('{"title":"POI事件模式解析并读取Excel文件数据","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/POI事件模式解析并读取Excel文件数据.md","filePath":"java/framework/POI事件模式解析并读取Excel文件数据.md","lastUpdated":1708510102000}'),p={name:"java/framework/POI事件模式解析并读取Excel文件数据.md"},o=l(`

POI事件模式解析并读取Excel文件数据

背景

传统的POI用户模式解析Excel为我们操作提供了丰富的API,用起来很方便,但是这种模式是一次性将Excel中的数据全部写入内存,并且要还封装结构,使得内存的占用远远超过原本Excel文件的大小,稍微大一点的文件采用用户模式进行解析都会内存溢出,由于现在做的项目要处理的Excel数据量普遍都很大,而且业务对导入功能的使用尤其频繁,所以必须采用事件模式的解析方式来实现业务需求.

事件模式避免内存溢出的原理很简单,就是采用SAX方式解析Excel文件底层的XML文件,这样逐行读取,逐行处理的方式可以完美解决内存占用的问题.

简单的入门程序可以从POI官网找到,下面是根据官方demo封装的一个比较完善的类,只需要创建一个类继承该抽象类然后实现抽象方法rowHandler,然后在rowHandler里进行我们需要的业务操作即可.

关于事件模式解析Excel的代码执行具体流程以后有时间再写篇文章进行总结~

代码

java
/**
+ * @author storyxc
+ * @description POI事件模式读取Excel抽象类
+ * @createdTime 2020/6/18 14:27
+ */
+public abstract class POIEventModeHandler extends DefaultHandler {
+
+    /**
+     * 构造方法
+     *
+     * @param parseCellValueStringFlag 是否将单元格值解析成字符串
+     * @param ignoreFirstRow           是否忽略首行(一般是表头)
+     */
+    public POIEventModeHandler(final boolean parseCellValueStringFlag, final boolean ignoreFirstRow) {
+        this.parseCellValueStringFlag = parseCellValueStringFlag;
+        this.ignoreFirstRow = ignoreFirstRow;
+    }
+
+    /**
+     * 构造方法
+     *
+     * @param parseCellValueStringFlag 将单元格值解析成字符串
+     * @param ignoreFirstRow           忽略首行
+     * @param dataFormatStyle          指定日期类型解析格式 默认yyyy-MM-dd
+     */
+    public POIEventModeHandler(final boolean parseCellValueStringFlag, final boolean ignoreFirstRow, final String dataFormatStyle) {
+        this.parseCellValueStringFlag = parseCellValueStringFlag;
+        this.ignoreFirstRow = ignoreFirstRow;
+        this.dateFormatStyle = dataFormatStyle;
+    }
+
+    /**
+     * 单元格类型索引
+     */
+    protected enum CellDataType {
+        /**
+         * 布尔值
+         */
+        BOOL("b"),
+        /**
+         * 异常错误
+         */
+        ERROR("e"),
+        /**
+         * 公式
+         */
+        FORMULA("str"),
+        /**
+         * 字符
+         */
+        INLINESTR("inlineStr"),
+        /**
+         * 共享字符表
+         */
+        SSTINDEX("s"),
+        /**
+         * 数值
+         */
+        NUMBER("n"),
+        /**
+         * 空
+         */
+        NULL("null");
+
+        private final String cellType;
+
+        String getCellType() {
+            return this.cellType;
+        }
+
+        CellDataType(final String cellType) {
+            this.cellType = cellType;
+        }
+
+        static CellDataType getCellTypeEnum(final String cellType) {
+            for (final CellDataType cellDataType : CellDataType.values()) {
+                //数字类型时c标签没有t属性
+                if (cellType == null) {
+                    return NUMBER;
+                } else if (StringUtils.equals(cellDataType.getCellType(), cellType)) {
+                    return cellDataType;
+                }
+            }
+            return null;
+        }
+    }
+
+    /**
+     * sheet样式
+     */
+    @Data
+    @EqualsAndHashCode(callSuper = false)
+    protected class SheetStyle {
+        /**
+         * sheet顺序索引
+         */
+        private int sheetId;
+        /**
+         * sheet名称
+         */
+        private String sheetName;
+        /**
+         * 缩放百分比
+         */
+        private double zoomPercent;
+        /**
+         * 自适应
+         */
+        private boolean fitToPage = false;
+        /**
+         * 是否显示网格线
+         */
+        private boolean showGridLines = true;
+        /**
+         * 默认行高
+         */
+        private double defaultRowHeight;
+        /**
+         * sheet中每一列的样式
+         */
+        Map<String, ColumnStyle> columnStyles;
+        /**
+         * 合并单元格
+         */
+        private List<Integer[]> mergeCells;
+        /* 打印属性 */
+        /**
+         * 上边距
+         */
+        private double topMargin = 0.75;
+        /**
+         * 下边距
+         */
+        private double bottomMargin = 0.75;
+        /**
+         * 左边距
+         */
+        private double leftMargin = 0.7;
+        /**
+         * 右边距
+         */
+        private double rightMargin = 0.7;
+        /**
+         * 页脚边距
+         */
+        private double footerMargin = 0.3;
+        /**
+         * 页头边距
+         */
+        private double headerMargin = 0.3;
+        /**
+         * 缩放比例
+         */
+        private short scale = 100;
+        /**
+         * 页宽
+         */
+        private short fitWidth = 1;
+        /**
+         * 页高
+         */
+        private short fitHeight = 1;
+        /**
+         * 纸张设置
+         */
+        private short pageSize = PrintSetup.A4_PAPERSIZE;
+        /**
+         * 垂直居中
+         */
+        private boolean verticallyCenter;
+        /**
+         * 水平居中
+         */
+        private boolean horizontallyCenter;
+        /**
+         * 横向打印
+         */
+        private boolean landscape;
+        /**
+         * 网格线
+         */
+        private boolean printGridlines;
+        /**
+         * 行号列标
+         */
+        private boolean printHeadings;
+        /**
+         * 草稿品质
+         */
+        private boolean draft;
+        /**
+         * 单色打印
+         */
+        private boolean noColor;
+        /**
+         * 打印顺序 true:先行后列 false:先列后行
+         */
+        private boolean leftToRigh;
+        /**
+         * 起始页页码自动
+         */
+        private boolean usePage;
+        /**
+         * 起始页码
+         */
+        private short pageStart = 1;
+        /**
+         * 页眉页脚与页边距对齐
+         */
+        private boolean alignWithMargins = true;
+    }
+
+    /**
+     * 列样式
+     */
+    @Data
+    @EqualsAndHashCode(callSuper = false)
+    protected class ColumnStyle {
+        /**
+         * 列宽度
+         */
+        private double columnWidth;
+        /**
+         * 列是否隐藏
+         */
+        private boolean isHidden;
+        /**
+         * 默认的列样式
+         */
+        private int defaultColumnStyleIndex;
+    }
+
+    /**
+     * 行数据
+     */
+    @Data
+    @EqualsAndHashCode(callSuper = false)
+    protected class RowData {
+        /**
+         * 当前行高
+         */
+        private double rowHeight;
+        /**
+         * 当前行数据
+         */
+        private List<Object> cellDataValues;
+        /**
+         * 当前行单元格样式索引
+         */
+        private List<Integer> cellStyles;
+        /**
+         * 当前行单元格数据类型
+         */
+        private List<CellDataType> cellDataTypes;
+        /**
+         * 单元格公式
+         */
+        private List<String> cellFormula;
+
+        RowData() {
+            this.cellDataValues = new ArrayList<>();
+            this.cellStyles = new ArrayList<>();
+            this.cellDataTypes = new ArrayList<>();
+            this.cellFormula = new ArrayList<>();
+        }
+    }
+
+    /**
+     * sheet打印区域
+     */
+    @Data
+    @EqualsAndHashCode(callSuper = false)
+    protected class SheetPrint {
+        private String sheetName;
+        private String sheetIndex;
+        private String printArea;
+        private String printTitleRows;
+        private String printTitleColumns;
+    }
+
+    /**
+     * 共享字符表
+     */
+    protected SharedStringsTable sst;
+    /**
+     * 单元格样式
+     */
+    private StylesTable stylesTable;
+    /**
+     * sheet游标
+     */
+    private int sheetIndex = 0;
+    /**
+     * 行游标
+     */
+    private int rowIndex;
+    /**
+     * 列坐标
+     */
+    private int colIndex;
+    /**
+     * 最大行数量
+     */
+    protected int rowMax;
+    /**
+     * 最大列数量
+     */
+    protected int colMax;
+    /**
+     * 是否为有效数据
+     */
+    private boolean valueFlag;
+    /**
+     * T
+     */
+    private boolean isTElement = false;
+    /**
+     * 记录当前值
+     */
+    private StringBuilder cellBuilder;
+    /**
+     * 是否过滤首行
+     */
+    protected boolean ignoreFirstRow = false;
+    /**
+     * 格式化日期样式
+     */
+    protected String dateFormatStyle = "yyyy-MM-dd";
+    /**
+     * 格式化样式
+     */
+    protected Map<Short, String> formatStyleMap = new HashMap<>();
+    /**
+     * 数据格式化formatter
+     */
+    private final DataFormatter dataFormatter = new DataFormatter();
+    /**
+     * 当前sheet样式
+     */
+    private SheetStyle sheetStyle;
+    /**
+     * 行数据
+     */
+    private RowData rowData;
+    /**
+     * sheet打印区域 key:sheetIndex
+     */
+    protected final Map<String, SheetPrint> sheetPrintMap = new HashMap<>();
+    /**
+     * 所有sheetName,key:sheetIndex
+     */
+    private final Map<String, String> sheetNameMap = new HashMap<>();
+    /**
+     * 当前打印标签的sheetIndex
+     */
+    private String localSheetId;
+    /**
+     * 辨别是否打印区域数据
+     */
+    private boolean printFlag;
+    /**
+     * 是否解析成String
+     */
+    private final boolean parseCellValueStringFlag;
+    /**
+     * 是否读取公式
+     */
+    protected boolean isReadFormula = false;
+    /**
+     * 是否共享公式
+     */
+    private boolean isSharedFormula;
+    /**
+     * 共享公式存放
+     */
+    private final Map<String, String> sharedFormulaMap = new HashMap<>();
+    /**
+     * 共享公式插值
+     */
+    private final Map<String, List<Integer>> diffMap = new HashMap<>();
+    /**
+     * 共享公式存放key
+     */
+    private String si;
+
+    /**
+     * 核心抽象方法,读取一行后的操作,将业务逻辑在该方法中实现
+     *
+     * @param sheetIndex
+     * @param rowIndex
+     * @param rowData
+     */
+    protected abstract void rowHandler(final int sheetIndex, final int rowIndex, final RowData rowData);
+
+    /**
+     * sheet处理完后调用的方法
+     *
+     * @param sheetIndex
+     * @param sheetStyle
+     */
+    protected void sheetOver(final int sheetIndex, final SheetStyle sheetStyle) {
+        return;
+    }
+
+    /**
+     * 整个工作簿处理完后调用
+     */
+    protected void workbookOver() {
+        return;
+    }
+
+    /**
+     * 处理完整的Excel
+     *
+     * @param filePath
+     * @throws OpenXML4JException
+     * @throws IOException
+     * @throws SAXException
+     */
+    public void handleExcel(final String filePath) throws OpenXML4JException, IOException, SAXException {
+        OPCPackage opcPackage = null;
+        try {
+            opcPackage = OPCPackage.open(filePath);
+            final XMLReader parser = fetchSheetParser();
+            final XSSFReader.SheetIterator sheets = parseSheet(opcPackage, parser);
+            while (sheets.hasNext()) {
+                final InputStream sheet = sheets.next();
+                sheetStyle = new SheetStyle();
+                sheetStyle.setSheetName(sheets.getSheetName());
+                final InputSource sheetSource = new InputSource(sheet);
+                parser.parse(sheetSource);
+                sheet.close();
+            }
+        } finally {
+            if (opcPackage != null) {
+                opcPackage.close();
+            }
+        }
+        workbookOver();
+    }
+
+    /**
+     * 处理指定的sheet页
+     * @param filePath
+     * @param sheetIdx
+     * @throws OpenXML4JException
+     * @throws SAXException
+     * @throws IOException
+     */
+    public void handleExcel(final String filePath, final int sheetIdx) throws OpenXML4JException, SAXException, IOException {
+        OPCPackage opcPackage = null;
+        try {
+            opcPackage = OPCPackage.open(filePath);
+            final XMLReader parser = fetchSheetParser();
+            final XSSFReader.SheetIterator sheets = parseSheet(opcPackage, parser);
+            while (sheets.hasNext()) {
+                final InputStream sheet = sheets.next();
+                if (sheetIndex + 1 != sheetIdx) {
+                    continue;
+                }
+                sheetStyle = new SheetStyle();
+                sheetStyle.setSheetName(sheets.getSheetName());
+                final InputSource sheetSource = new InputSource(sheet);
+                parser.parse(sheetSource);
+                sheet.close();
+            }
+        } finally {
+            if (opcPackage != null) {
+                opcPackage.close();
+            }
+        }
+        workbookOver();
+    }
+
+    private XSSFReader.SheetIterator parseSheet(OPCPackage opcPackage, XMLReader parser) throws IOException, OpenXML4JException, SAXException {
+        final XSSFReader reader = new XSSFReader(opcPackage);
+        sst = reader.getSharedStringsTable();
+        stylesTable = reader.getStylesTable();
+        final XSSFReader.SheetIterator sheets = (XSSFReader.SheetIterator) reader.getSheetsData();
+        //读取工作簿内容
+        final InputStream workbookData = reader.getWorkbookData();
+        final InputSource workbookDataSource = new InputSource(workbookData);
+        parser.parse(workbookDataSource);
+        workbookData.close();
+        //读取单元格样式
+        final InputStream stylesData = reader.getStylesData();
+        final InputSource stylesDataSource = new InputSource(stylesData);
+        parser.parse(stylesDataSource);
+        stylesData.close();
+        return sheets;
+
+    }
+
+    private XMLReader fetchSheetParser() throws SAXException {
+        final XMLReader parser = XMLReaderFactory.createXMLReader("org.apache.xerces.parsers.SAXParser");
+        parser.setContentHandler(this);
+        return parser;
+    }
+
+
+    /**
+     * 读取每对标签的开始标签时调用的方法
+     *
+     * @param uri
+     * @param localName
+     * @param name       当前标签名
+     * @param attributes 当前标签上的属性
+     */
+    @Override
+    public void startElement(final String uri, final String localName,
+                             final String name, final Attributes attributes) {
+        if ("numFmt".equals(name)) {
+            final short numFmtId = Short.parseShort(attributes.getValue("numFmtId"));
+            final String formatCode = attributes.getValue("formatCode");
+            formatStyleMap.put(numFmtId, formatCode);
+        } else if ("mergeCells".equals(name)) {
+            sheetStyle.setMergeCells(new ArrayList<Integer[]>());
+        } else if ("mergeCell".equals(name)) {
+            final String[] range = attributes.getValue("ref").split(":");
+            final Integer[] positionDx = new Integer[4];
+            final int[] start = parsePosition(range[0]);
+            final int[] end = parsePosition(range[1]);
+            positionDx[0] = start[1];
+            positionDx[1] = end[1];
+            positionDx[2] = start[0] - 1;
+            positionDx[3] = end[0] - 1;
+            sheetStyle.getMergeCells().add(positionDx);
+        } else if ("pageSetUpPr".equals(name)) {
+            if (attributes.getValue("fitToPage") != null && attributes.getValue("fitToPage").equals("1")) {
+                sheetStyle.setFitToPage(true);
+            }
+        } else if ("sheetView".equals(name)) {
+            if (attributes.getValue("showGridLines") != null && attributes.getValue("showGridLines").equals("0")) {
+                sheetStyle.setShowGridLines(false);
+            }
+            if (attributes.getValue("zoomScale") != null) {
+                sheetStyle.setZoomPercent((int) Double.parseDouble(attributes.getValue("zoomScale")));
+            }
+        } else if ("sheetFormatPr".equals(name)) {
+            if (attributes.getValue("defaultRowHeight") != null) {
+                sheetStyle.setDefaultRowHeight(Double.parseDouble(attributes.getValue("defaultRowHeight")));
+            }
+        } else if ("sheetData".equals(name)) {
+            //开始读取sheet页数据
+            sheetIndex++;
+            rowIndex = 1;
+            cellBuilder = new StringBuilder();
+            rowData = new RowData();
+            sheetHandler(sheetIndex, sheetStyle);
+        } else if ("dimension".equals(name)) {
+            handleMax(attributes.getValue("ref"));
+            sheetStyle.setColumnStyles(new HashMap<String, ColumnStyle>());
+        } else if ("col".equals(name)) {
+            final int startColIndex = Integer.parseInt(attributes.getValue("min"));
+            final int endColIndex = Integer.parseInt(attributes.getValue("max"));
+            final double columnWidth = Double.parseDouble(attributes.getValue("width"));
+            int defaultColumnStyleIndex = 0;
+            boolean hidden = false;
+            if (attributes.getValue("style") != null) {
+                defaultColumnStyleIndex = Integer.parseInt(attributes.getValue("style"));
+            }
+            if (attributes.getValue("hidden") != null && (attributes.getValue("hidden").equals("true") || attributes.getValue("hidden").equals("1"))) {
+                hidden = true;
+            }
+            final ColumnStyle columnStyle = new ColumnStyle();
+            columnStyle.setDefaultColumnStyleIndex(defaultColumnStyleIndex);
+            columnStyle.setHidden(hidden);
+            columnStyle.setColumnWidth(columnWidth);
+            sheetStyle.getColumnStyles().put(Integer.toString(startColIndex) + ":" + Integer.toString(endColIndex), columnStyle);
+        } else if ("row".equals(name)) {
+            //开始读取行数据
+            rowIndex = Integer.parseInt(attributes.getValue("r"));
+            adjustRowMax(rowIndex);
+            //清空数据容器中保留的上一行的数据
+            rowData.getCellDataValues().clear();
+            rowData.getCellStyles().clear();
+            rowData.getCellDataTypes().clear();
+            rowData.getCellFormula().clear();
+            colIndex = 1;
+            if (attributes.getValue("ht") != null) {
+                rowData.setRowHeight(Double.parseDouble(attributes.getValue("ht")));
+            }
+        } else if ("c".equals(name)) {
+            //读取单元格内容
+            final String position = attributes.getValue("r");
+            if (position != null) {
+                colIndex = parsePosition(position)[0];
+                adjustColMax(colIndex);
+                for (int idx = rowData.getCellStyles().size() + 1; idx < colIndex; idx++) {
+                    rowData.getCellStyles().add(null);
+                }
+                for (int idx = rowData.getCellDataValues().size() + 1; idx < colIndex; idx++) {
+                    rowData.getCellDataValues().add(null);
+                }
+                for (int idx = rowData.getCellDataTypes().size() + 1; idx < colIndex; idx++) {
+                    rowData.getCellDataTypes().add(null);
+                }
+                for (int idx = rowData.getCellFormula().size() + 1; idx < colIndex; idx++) {
+                    rowData.getCellFormula().add(null);
+                }
+            }
+            if (attributes.getValue("s") != null) {
+                rowData.getCellStyles().add(Integer.parseInt(attributes.getValue("s")));
+            } else {
+                rowData.getCellStyles().add(null);
+            }
+            rowData.getCellDataTypes().add(CellDataType.getCellTypeEnum(attributes.getValue("t")));
+        } else if ("v".equals(name)) {
+            //单元格数据
+            valueFlag = true;
+        } else if ("t".equals(name)) {
+            isTElement = true;
+            valueFlag = true;
+        } else if ("definedName".equals(name) && StringUtils.isNotBlank(attributes.getValue("localSheetId"))) {
+            final String value = attributes.getValue("name");
+            if ("_xlnm.Print_Area".equals(value) || "_xlnm.Print_Titles".equals(value)) {
+                valueFlag = true;
+                printFlag = true;
+                localSheetId = attributes.getValue("localSheetId");
+                cellBuilder = new StringBuilder();
+            }
+        } else if ("sheet".equals(name)) {
+            sheetNameMap.put(attributes.getValue("r:id"), attributes.getValue("name"));
+        } else if ("pageMargins".equals(name)) {
+            //页边距
+
+        } else if ("pageSetup".equals(name)) {
+            //页面设置
+
+        } else if ("printOptions".equals(name)) {
+            //打印选项
+
+        } else if ("headerFooter".equals(name)) {
+            //页眉页脚
+
+        } else if (isReadFormula && "f".equals(name)) {
+            //公式
+            valueFlag = true;
+            if ("shared".equals(attributes.getValue("t"))) {
+                //共享公式
+                isSharedFormula = true;
+                si = attributes.getValue("si");
+            }
+        }
+    }
+
+    /**
+     * 读取每对标签的结束标签时调用
+     *
+     * @param uri
+     * @param localName
+     * @param name
+     */
+    @Override
+    public void endElement(final String uri, final String localName, final String name) {
+        Object result;
+        if ("worksheet".equals(name)) {
+            //一个sheet读取完毕
+            cellBuilder = null;
+            rowIndex = 0;
+            sheetOver(sheetIndex, sheetStyle);
+        } else if ("row".equals(name)) {
+            //一行数据读取完毕
+            if (rowIndex == 1 && ignoreFirstRow) {
+                //过滤首行数据 一般为表头
+                return;
+            }
+            //调用实现的业务逻辑方法处理当前行数据
+            rowHandler(sheetIndex, rowIndex, rowData);
+            rowIndex++;
+            rowData.setRowHeight(0);
+        } else if ("v".equals(name)) {
+            //读取到单元格的数据标签
+            final CellDataType cellDataType = rowData.getCellDataTypes().get(colIndex - 1);
+            switch (cellDataType) {
+                case BOOL:
+                    final char firstFlag = cellBuilder.toString().charAt(0);
+                    if (parseCellValueStringFlag) {
+                        result = firstFlag == '0' ? "false" : "true";
+                    } else {
+                        result = firstFlag != '0';
+                    }
+                    break;
+                case ERROR:
+                    result = "\\"ERROR:" + cellBuilder.toString() + "\\"";
+                    break;
+                case FORMULA:
+                    if (parseCellValueStringFlag) {
+                        result = cellBuilder.toString();
+                    } else {
+                        try {
+                            result = Double.parseDouble(cellBuilder.toString());
+                        } catch (Exception e) {
+                            result = cellBuilder.toString();
+                        }
+                    }
+                    break;
+                case INLINESTR:
+                    result = new XSSFRichTextString(cellBuilder.toString());
+                    break;
+                case SSTINDEX:
+                    //共享字符需要从共享字符表中取
+                    final int idx = Integer.parseInt(cellBuilder.toString());
+                    result = new XSSFRichTextString(sst.getEntryAt(idx));
+                    break;
+                case NUMBER:
+                    if (parseCellValueStringFlag) {
+                        final Integer styleAt = rowData.getCellStyles().get(colIndex - 1);
+                        if (styleAt != null) {
+                            final XSSFCellStyle cellStyle = stylesTable.getStyleAt(styleAt);
+                            final short formatIndex = cellStyle.getDataFormat();
+                            final String formatString = cellStyle.getDataFormatString();
+                            if (formatString == null) {
+                                result = cellBuilder.toString();
+                            } else if (formatString.contains("m/dd/yy")
+                                    || formatString.contains("m/d/yy")
+                                    || formatString.contains("yyyy/mm/dd")
+                                    || formatString.contains("yyyy/m/d")) {
+                                result = dataFormatter.formatRawCellContents(
+                                        Double.parseDouble(cellBuilder.toString()),
+                                        formatIndex, dateFormatStyle).replace("T", "");
+                            } else {
+                                result = dataFormatter.formatRawCellContents(Double.parseDouble(cellBuilder.toString()),
+                                        formatIndex, formatString).replace("_", "").trim();
+                            }
+                        } else {
+                            result = cellBuilder.toString();
+                        }
+                    } else {
+                        result = Double.parseDouble(cellBuilder.toString());
+                    }
+                    break;
+                default:
+                    result = null;
+            }
+            writeColData(result);
+            valueFlag = false;
+        } else if ("c".equals(name)) {
+            colIndex++;
+        } else if ("f".equals(name)) {
+            if (isReadFormula) {
+                rowData.getCellDataTypes().set(colIndex - 1, CellDataType.FORMULA);
+                writeColData(cellBuilder);
+                valueFlag = false;
+            }
+            cellBuilder.delete(0, cellBuilder.length());
+        } else if (isTElement) {
+            result = cellBuilder.toString().trim();
+            writeColData(result);
+            isTElement = false;
+            valueFlag = false;
+        } else if (printFlag && "definedName".equals(name)) {
+            result = cellBuilder.toString();
+            writePrint(result);
+            valueFlag = false;
+            printFlag = false;
+            cellBuilder.delete(0, cellBuilder.length());
+        }
+    }
+
+    /**
+     * 写sheet打印区域和打印标题
+     *
+     * @param result
+     */
+    private void writePrint(Object result) {
+        final String res = result.toString();
+        if (StringUtils.isBlank(localSheetId) || StringUtils.equals(res, "#REF!") || StringUtils.isBlank(res)) {
+            return;
+        }
+        final String rId = "rId" + (Integer.parseInt(localSheetId) + 1);
+        final String sheetName = sheetNameMap.get(rId);
+        final SheetPrint sheetPrint = sheetPrintMap.containsKey(sheetName) ? sheetPrintMap.get(sheetName) : new SheetPrint();
+        sheetPrint.setSheetName(sheetName);
+        sheetPrint.setSheetIndex(localSheetId);
+        for (String str : res.split(",")) {
+            final int i = isArea(str.split("!")[1]);
+            if (i == 1) {
+                sheetPrint.setPrintArea(res);
+            } else if (i == 2) {
+                sheetPrint.setPrintTitleRows(str);
+            } else if (i == 3) {
+                sheetPrint.setPrintTitleColumns(str);
+            }
+        }
+        sheetPrintMap.put(sheetName, sheetPrint);
+        localSheetId = null;
+    }
+
+    /**
+     * @param str
+     * @return 1-区域 2-顶端 3-左端 0-判断错误
+     */
+    private int isArea(String str) {
+        String split = str.split(":")[0];
+        int countMatches = StringUtils.countMatches(split, "$");
+        if (countMatches == 2) {
+            return 1;
+        } else if (countMatches == 1) {
+            String substr = split.substring(split.length() - 1);
+            char charAt = substr.charAt(0);
+            Pattern pattern = Pattern.compile("[0-9]*");
+            if (pattern.matcher(substr).matches()) {
+                return 2;
+            } else if ((charAt >= 'a' && charAt <= 'z') || (charAt >= 'A' && charAt <= 'Z')) {
+                return 3;
+            }
+        }
+        return 0;
+
+    }
+
+    /**
+     * 计算当前的最大行和列数
+     *
+     * @param ref
+     */
+    private void handleMax(String ref) {
+        final String[] range = ref.split(":");
+        String maxStr;
+        if (range.length == 1) {
+            maxStr = range[0];
+        } else {
+            maxStr = range[1];
+        }
+        final int[] maxPosition = parsePosition(maxStr);
+        rowMax = maxPosition[1];
+        colMax = maxPosition[0];
+    }
+
+    /**
+     * 把单元格数据添加到当前行的数据容器中
+     *
+     * @param result
+     */
+    private void writeColData(final Object result) {
+        rowData.getCellDataValues().add(result);
+        cellBuilder.delete(0, cellBuilder.length());
+    }
+
+    /**
+     * 数据append到cellBuilder中
+     *
+     * @param ch
+     * @param start
+     * @param length
+     */
+    @Override
+    public void characters(final char[] ch, final int start, final int length) {
+        if (valueFlag) {
+            cellBuilder.append(ch, start, length);
+        }
+    }
+
+    /**
+     * sheet处理
+     *
+     * @param sheetIndex
+     * @param sheetStyle
+     */
+    private void sheetHandler(int sheetIndex, SheetStyle sheetStyle) {
+
+    }
+
+    /**
+     * 获取position坐标的实际坐标数据
+     *
+     * @param position
+     * @return
+     */
+    private int[] parsePosition(String position) {
+        final int[] result = new int[2];
+        final String amPosition = position.replaceAll("[0-9]", "");
+        final char[] chars = amPosition.toUpperCase().toCharArray();
+        int ret = 0;
+        for (int i = 0; i < chars.length; i++) {
+            ret += (chars[i] - 'A' + 1) * Math.pow(26, chars.length - i - 1);
+        }
+        result[0] = ret;
+        result[1] = Integer.parseInt(position.replaceAll("[A-Z]", ""));
+        return result;
+    }
+
+    /**
+     * 调整列最大值
+     *
+     * @param colIndex
+     */
+    private void adjustColMax(final int colIndex) {
+        if (colIndex > colMax) {
+            colMax = colIndex;
+        }
+    }
+
+    /**
+     * 调整行最大值
+     *
+     * @param rowIndex
+     */
+    private void adjustRowMax(final int rowIndex) {
+        if (rowIndex > rowMax) {
+            rowMax = rowIndex;
+        }
+    }
+}
/**
+ * @author storyxc
+ * @description POI事件模式读取Excel抽象类
+ * @createdTime 2020/6/18 14:27
+ */
+public abstract class POIEventModeHandler extends DefaultHandler {
+
+    /**
+     * 构造方法
+     *
+     * @param parseCellValueStringFlag 是否将单元格值解析成字符串
+     * @param ignoreFirstRow           是否忽略首行(一般是表头)
+     */
+    public POIEventModeHandler(final boolean parseCellValueStringFlag, final boolean ignoreFirstRow) {
+        this.parseCellValueStringFlag = parseCellValueStringFlag;
+        this.ignoreFirstRow = ignoreFirstRow;
+    }
+
+    /**
+     * 构造方法
+     *
+     * @param parseCellValueStringFlag 将单元格值解析成字符串
+     * @param ignoreFirstRow           忽略首行
+     * @param dataFormatStyle          指定日期类型解析格式 默认yyyy-MM-dd
+     */
+    public POIEventModeHandler(final boolean parseCellValueStringFlag, final boolean ignoreFirstRow, final String dataFormatStyle) {
+        this.parseCellValueStringFlag = parseCellValueStringFlag;
+        this.ignoreFirstRow = ignoreFirstRow;
+        this.dateFormatStyle = dataFormatStyle;
+    }
+
+    /**
+     * 单元格类型索引
+     */
+    protected enum CellDataType {
+        /**
+         * 布尔值
+         */
+        BOOL("b"),
+        /**
+         * 异常错误
+         */
+        ERROR("e"),
+        /**
+         * 公式
+         */
+        FORMULA("str"),
+        /**
+         * 字符
+         */
+        INLINESTR("inlineStr"),
+        /**
+         * 共享字符表
+         */
+        SSTINDEX("s"),
+        /**
+         * 数值
+         */
+        NUMBER("n"),
+        /**
+         * 空
+         */
+        NULL("null");
+
+        private final String cellType;
+
+        String getCellType() {
+            return this.cellType;
+        }
+
+        CellDataType(final String cellType) {
+            this.cellType = cellType;
+        }
+
+        static CellDataType getCellTypeEnum(final String cellType) {
+            for (final CellDataType cellDataType : CellDataType.values()) {
+                //数字类型时c标签没有t属性
+                if (cellType == null) {
+                    return NUMBER;
+                } else if (StringUtils.equals(cellDataType.getCellType(), cellType)) {
+                    return cellDataType;
+                }
+            }
+            return null;
+        }
+    }
+
+    /**
+     * sheet样式
+     */
+    @Data
+    @EqualsAndHashCode(callSuper = false)
+    protected class SheetStyle {
+        /**
+         * sheet顺序索引
+         */
+        private int sheetId;
+        /**
+         * sheet名称
+         */
+        private String sheetName;
+        /**
+         * 缩放百分比
+         */
+        private double zoomPercent;
+        /**
+         * 自适应
+         */
+        private boolean fitToPage = false;
+        /**
+         * 是否显示网格线
+         */
+        private boolean showGridLines = true;
+        /**
+         * 默认行高
+         */
+        private double defaultRowHeight;
+        /**
+         * sheet中每一列的样式
+         */
+        Map<String, ColumnStyle> columnStyles;
+        /**
+         * 合并单元格
+         */
+        private List<Integer[]> mergeCells;
+        /* 打印属性 */
+        /**
+         * 上边距
+         */
+        private double topMargin = 0.75;
+        /**
+         * 下边距
+         */
+        private double bottomMargin = 0.75;
+        /**
+         * 左边距
+         */
+        private double leftMargin = 0.7;
+        /**
+         * 右边距
+         */
+        private double rightMargin = 0.7;
+        /**
+         * 页脚边距
+         */
+        private double footerMargin = 0.3;
+        /**
+         * 页头边距
+         */
+        private double headerMargin = 0.3;
+        /**
+         * 缩放比例
+         */
+        private short scale = 100;
+        /**
+         * 页宽
+         */
+        private short fitWidth = 1;
+        /**
+         * 页高
+         */
+        private short fitHeight = 1;
+        /**
+         * 纸张设置
+         */
+        private short pageSize = PrintSetup.A4_PAPERSIZE;
+        /**
+         * 垂直居中
+         */
+        private boolean verticallyCenter;
+        /**
+         * 水平居中
+         */
+        private boolean horizontallyCenter;
+        /**
+         * 横向打印
+         */
+        private boolean landscape;
+        /**
+         * 网格线
+         */
+        private boolean printGridlines;
+        /**
+         * 行号列标
+         */
+        private boolean printHeadings;
+        /**
+         * 草稿品质
+         */
+        private boolean draft;
+        /**
+         * 单色打印
+         */
+        private boolean noColor;
+        /**
+         * 打印顺序 true:先行后列 false:先列后行
+         */
+        private boolean leftToRigh;
+        /**
+         * 起始页页码自动
+         */
+        private boolean usePage;
+        /**
+         * 起始页码
+         */
+        private short pageStart = 1;
+        /**
+         * 页眉页脚与页边距对齐
+         */
+        private boolean alignWithMargins = true;
+    }
+
+    /**
+     * 列样式
+     */
+    @Data
+    @EqualsAndHashCode(callSuper = false)
+    protected class ColumnStyle {
+        /**
+         * 列宽度
+         */
+        private double columnWidth;
+        /**
+         * 列是否隐藏
+         */
+        private boolean isHidden;
+        /**
+         * 默认的列样式
+         */
+        private int defaultColumnStyleIndex;
+    }
+
+    /**
+     * 行数据
+     */
+    @Data
+    @EqualsAndHashCode(callSuper = false)
+    protected class RowData {
+        /**
+         * 当前行高
+         */
+        private double rowHeight;
+        /**
+         * 当前行数据
+         */
+        private List<Object> cellDataValues;
+        /**
+         * 当前行单元格样式索引
+         */
+        private List<Integer> cellStyles;
+        /**
+         * 当前行单元格数据类型
+         */
+        private List<CellDataType> cellDataTypes;
+        /**
+         * 单元格公式
+         */
+        private List<String> cellFormula;
+
+        RowData() {
+            this.cellDataValues = new ArrayList<>();
+            this.cellStyles = new ArrayList<>();
+            this.cellDataTypes = new ArrayList<>();
+            this.cellFormula = new ArrayList<>();
+        }
+    }
+
+    /**
+     * sheet打印区域
+     */
+    @Data
+    @EqualsAndHashCode(callSuper = false)
+    protected class SheetPrint {
+        private String sheetName;
+        private String sheetIndex;
+        private String printArea;
+        private String printTitleRows;
+        private String printTitleColumns;
+    }
+
+    /**
+     * 共享字符表
+     */
+    protected SharedStringsTable sst;
+    /**
+     * 单元格样式
+     */
+    private StylesTable stylesTable;
+    /**
+     * sheet游标
+     */
+    private int sheetIndex = 0;
+    /**
+     * 行游标
+     */
+    private int rowIndex;
+    /**
+     * 列坐标
+     */
+    private int colIndex;
+    /**
+     * 最大行数量
+     */
+    protected int rowMax;
+    /**
+     * 最大列数量
+     */
+    protected int colMax;
+    /**
+     * 是否为有效数据
+     */
+    private boolean valueFlag;
+    /**
+     * T
+     */
+    private boolean isTElement = false;
+    /**
+     * 记录当前值
+     */
+    private StringBuilder cellBuilder;
+    /**
+     * 是否过滤首行
+     */
+    protected boolean ignoreFirstRow = false;
+    /**
+     * 格式化日期样式
+     */
+    protected String dateFormatStyle = "yyyy-MM-dd";
+    /**
+     * 格式化样式
+     */
+    protected Map<Short, String> formatStyleMap = new HashMap<>();
+    /**
+     * 数据格式化formatter
+     */
+    private final DataFormatter dataFormatter = new DataFormatter();
+    /**
+     * 当前sheet样式
+     */
+    private SheetStyle sheetStyle;
+    /**
+     * 行数据
+     */
+    private RowData rowData;
+    /**
+     * sheet打印区域 key:sheetIndex
+     */
+    protected final Map<String, SheetPrint> sheetPrintMap = new HashMap<>();
+    /**
+     * 所有sheetName,key:sheetIndex
+     */
+    private final Map<String, String> sheetNameMap = new HashMap<>();
+    /**
+     * 当前打印标签的sheetIndex
+     */
+    private String localSheetId;
+    /**
+     * 辨别是否打印区域数据
+     */
+    private boolean printFlag;
+    /**
+     * 是否解析成String
+     */
+    private final boolean parseCellValueStringFlag;
+    /**
+     * 是否读取公式
+     */
+    protected boolean isReadFormula = false;
+    /**
+     * 是否共享公式
+     */
+    private boolean isSharedFormula;
+    /**
+     * 共享公式存放
+     */
+    private final Map<String, String> sharedFormulaMap = new HashMap<>();
+    /**
+     * 共享公式插值
+     */
+    private final Map<String, List<Integer>> diffMap = new HashMap<>();
+    /**
+     * 共享公式存放key
+     */
+    private String si;
+
+    /**
+     * 核心抽象方法,读取一行后的操作,将业务逻辑在该方法中实现
+     *
+     * @param sheetIndex
+     * @param rowIndex
+     * @param rowData
+     */
+    protected abstract void rowHandler(final int sheetIndex, final int rowIndex, final RowData rowData);
+
+    /**
+     * sheet处理完后调用的方法
+     *
+     * @param sheetIndex
+     * @param sheetStyle
+     */
+    protected void sheetOver(final int sheetIndex, final SheetStyle sheetStyle) {
+        return;
+    }
+
+    /**
+     * 整个工作簿处理完后调用
+     */
+    protected void workbookOver() {
+        return;
+    }
+
+    /**
+     * 处理完整的Excel
+     *
+     * @param filePath
+     * @throws OpenXML4JException
+     * @throws IOException
+     * @throws SAXException
+     */
+    public void handleExcel(final String filePath) throws OpenXML4JException, IOException, SAXException {
+        OPCPackage opcPackage = null;
+        try {
+            opcPackage = OPCPackage.open(filePath);
+            final XMLReader parser = fetchSheetParser();
+            final XSSFReader.SheetIterator sheets = parseSheet(opcPackage, parser);
+            while (sheets.hasNext()) {
+                final InputStream sheet = sheets.next();
+                sheetStyle = new SheetStyle();
+                sheetStyle.setSheetName(sheets.getSheetName());
+                final InputSource sheetSource = new InputSource(sheet);
+                parser.parse(sheetSource);
+                sheet.close();
+            }
+        } finally {
+            if (opcPackage != null) {
+                opcPackage.close();
+            }
+        }
+        workbookOver();
+    }
+
+    /**
+     * 处理指定的sheet页
+     * @param filePath
+     * @param sheetIdx
+     * @throws OpenXML4JException
+     * @throws SAXException
+     * @throws IOException
+     */
+    public void handleExcel(final String filePath, final int sheetIdx) throws OpenXML4JException, SAXException, IOException {
+        OPCPackage opcPackage = null;
+        try {
+            opcPackage = OPCPackage.open(filePath);
+            final XMLReader parser = fetchSheetParser();
+            final XSSFReader.SheetIterator sheets = parseSheet(opcPackage, parser);
+            while (sheets.hasNext()) {
+                final InputStream sheet = sheets.next();
+                if (sheetIndex + 1 != sheetIdx) {
+                    continue;
+                }
+                sheetStyle = new SheetStyle();
+                sheetStyle.setSheetName(sheets.getSheetName());
+                final InputSource sheetSource = new InputSource(sheet);
+                parser.parse(sheetSource);
+                sheet.close();
+            }
+        } finally {
+            if (opcPackage != null) {
+                opcPackage.close();
+            }
+        }
+        workbookOver();
+    }
+
+    private XSSFReader.SheetIterator parseSheet(OPCPackage opcPackage, XMLReader parser) throws IOException, OpenXML4JException, SAXException {
+        final XSSFReader reader = new XSSFReader(opcPackage);
+        sst = reader.getSharedStringsTable();
+        stylesTable = reader.getStylesTable();
+        final XSSFReader.SheetIterator sheets = (XSSFReader.SheetIterator) reader.getSheetsData();
+        //读取工作簿内容
+        final InputStream workbookData = reader.getWorkbookData();
+        final InputSource workbookDataSource = new InputSource(workbookData);
+        parser.parse(workbookDataSource);
+        workbookData.close();
+        //读取单元格样式
+        final InputStream stylesData = reader.getStylesData();
+        final InputSource stylesDataSource = new InputSource(stylesData);
+        parser.parse(stylesDataSource);
+        stylesData.close();
+        return sheets;
+
+    }
+
+    private XMLReader fetchSheetParser() throws SAXException {
+        final XMLReader parser = XMLReaderFactory.createXMLReader("org.apache.xerces.parsers.SAXParser");
+        parser.setContentHandler(this);
+        return parser;
+    }
+
+
+    /**
+     * 读取每对标签的开始标签时调用的方法
+     *
+     * @param uri
+     * @param localName
+     * @param name       当前标签名
+     * @param attributes 当前标签上的属性
+     */
+    @Override
+    public void startElement(final String uri, final String localName,
+                             final String name, final Attributes attributes) {
+        if ("numFmt".equals(name)) {
+            final short numFmtId = Short.parseShort(attributes.getValue("numFmtId"));
+            final String formatCode = attributes.getValue("formatCode");
+            formatStyleMap.put(numFmtId, formatCode);
+        } else if ("mergeCells".equals(name)) {
+            sheetStyle.setMergeCells(new ArrayList<Integer[]>());
+        } else if ("mergeCell".equals(name)) {
+            final String[] range = attributes.getValue("ref").split(":");
+            final Integer[] positionDx = new Integer[4];
+            final int[] start = parsePosition(range[0]);
+            final int[] end = parsePosition(range[1]);
+            positionDx[0] = start[1];
+            positionDx[1] = end[1];
+            positionDx[2] = start[0] - 1;
+            positionDx[3] = end[0] - 1;
+            sheetStyle.getMergeCells().add(positionDx);
+        } else if ("pageSetUpPr".equals(name)) {
+            if (attributes.getValue("fitToPage") != null && attributes.getValue("fitToPage").equals("1")) {
+                sheetStyle.setFitToPage(true);
+            }
+        } else if ("sheetView".equals(name)) {
+            if (attributes.getValue("showGridLines") != null && attributes.getValue("showGridLines").equals("0")) {
+                sheetStyle.setShowGridLines(false);
+            }
+            if (attributes.getValue("zoomScale") != null) {
+                sheetStyle.setZoomPercent((int) Double.parseDouble(attributes.getValue("zoomScale")));
+            }
+        } else if ("sheetFormatPr".equals(name)) {
+            if (attributes.getValue("defaultRowHeight") != null) {
+                sheetStyle.setDefaultRowHeight(Double.parseDouble(attributes.getValue("defaultRowHeight")));
+            }
+        } else if ("sheetData".equals(name)) {
+            //开始读取sheet页数据
+            sheetIndex++;
+            rowIndex = 1;
+            cellBuilder = new StringBuilder();
+            rowData = new RowData();
+            sheetHandler(sheetIndex, sheetStyle);
+        } else if ("dimension".equals(name)) {
+            handleMax(attributes.getValue("ref"));
+            sheetStyle.setColumnStyles(new HashMap<String, ColumnStyle>());
+        } else if ("col".equals(name)) {
+            final int startColIndex = Integer.parseInt(attributes.getValue("min"));
+            final int endColIndex = Integer.parseInt(attributes.getValue("max"));
+            final double columnWidth = Double.parseDouble(attributes.getValue("width"));
+            int defaultColumnStyleIndex = 0;
+            boolean hidden = false;
+            if (attributes.getValue("style") != null) {
+                defaultColumnStyleIndex = Integer.parseInt(attributes.getValue("style"));
+            }
+            if (attributes.getValue("hidden") != null && (attributes.getValue("hidden").equals("true") || attributes.getValue("hidden").equals("1"))) {
+                hidden = true;
+            }
+            final ColumnStyle columnStyle = new ColumnStyle();
+            columnStyle.setDefaultColumnStyleIndex(defaultColumnStyleIndex);
+            columnStyle.setHidden(hidden);
+            columnStyle.setColumnWidth(columnWidth);
+            sheetStyle.getColumnStyles().put(Integer.toString(startColIndex) + ":" + Integer.toString(endColIndex), columnStyle);
+        } else if ("row".equals(name)) {
+            //开始读取行数据
+            rowIndex = Integer.parseInt(attributes.getValue("r"));
+            adjustRowMax(rowIndex);
+            //清空数据容器中保留的上一行的数据
+            rowData.getCellDataValues().clear();
+            rowData.getCellStyles().clear();
+            rowData.getCellDataTypes().clear();
+            rowData.getCellFormula().clear();
+            colIndex = 1;
+            if (attributes.getValue("ht") != null) {
+                rowData.setRowHeight(Double.parseDouble(attributes.getValue("ht")));
+            }
+        } else if ("c".equals(name)) {
+            //读取单元格内容
+            final String position = attributes.getValue("r");
+            if (position != null) {
+                colIndex = parsePosition(position)[0];
+                adjustColMax(colIndex);
+                for (int idx = rowData.getCellStyles().size() + 1; idx < colIndex; idx++) {
+                    rowData.getCellStyles().add(null);
+                }
+                for (int idx = rowData.getCellDataValues().size() + 1; idx < colIndex; idx++) {
+                    rowData.getCellDataValues().add(null);
+                }
+                for (int idx = rowData.getCellDataTypes().size() + 1; idx < colIndex; idx++) {
+                    rowData.getCellDataTypes().add(null);
+                }
+                for (int idx = rowData.getCellFormula().size() + 1; idx < colIndex; idx++) {
+                    rowData.getCellFormula().add(null);
+                }
+            }
+            if (attributes.getValue("s") != null) {
+                rowData.getCellStyles().add(Integer.parseInt(attributes.getValue("s")));
+            } else {
+                rowData.getCellStyles().add(null);
+            }
+            rowData.getCellDataTypes().add(CellDataType.getCellTypeEnum(attributes.getValue("t")));
+        } else if ("v".equals(name)) {
+            //单元格数据
+            valueFlag = true;
+        } else if ("t".equals(name)) {
+            isTElement = true;
+            valueFlag = true;
+        } else if ("definedName".equals(name) && StringUtils.isNotBlank(attributes.getValue("localSheetId"))) {
+            final String value = attributes.getValue("name");
+            if ("_xlnm.Print_Area".equals(value) || "_xlnm.Print_Titles".equals(value)) {
+                valueFlag = true;
+                printFlag = true;
+                localSheetId = attributes.getValue("localSheetId");
+                cellBuilder = new StringBuilder();
+            }
+        } else if ("sheet".equals(name)) {
+            sheetNameMap.put(attributes.getValue("r:id"), attributes.getValue("name"));
+        } else if ("pageMargins".equals(name)) {
+            //页边距
+
+        } else if ("pageSetup".equals(name)) {
+            //页面设置
+
+        } else if ("printOptions".equals(name)) {
+            //打印选项
+
+        } else if ("headerFooter".equals(name)) {
+            //页眉页脚
+
+        } else if (isReadFormula && "f".equals(name)) {
+            //公式
+            valueFlag = true;
+            if ("shared".equals(attributes.getValue("t"))) {
+                //共享公式
+                isSharedFormula = true;
+                si = attributes.getValue("si");
+            }
+        }
+    }
+
+    /**
+     * 读取每对标签的结束标签时调用
+     *
+     * @param uri
+     * @param localName
+     * @param name
+     */
+    @Override
+    public void endElement(final String uri, final String localName, final String name) {
+        Object result;
+        if ("worksheet".equals(name)) {
+            //一个sheet读取完毕
+            cellBuilder = null;
+            rowIndex = 0;
+            sheetOver(sheetIndex, sheetStyle);
+        } else if ("row".equals(name)) {
+            //一行数据读取完毕
+            if (rowIndex == 1 && ignoreFirstRow) {
+                //过滤首行数据 一般为表头
+                return;
+            }
+            //调用实现的业务逻辑方法处理当前行数据
+            rowHandler(sheetIndex, rowIndex, rowData);
+            rowIndex++;
+            rowData.setRowHeight(0);
+        } else if ("v".equals(name)) {
+            //读取到单元格的数据标签
+            final CellDataType cellDataType = rowData.getCellDataTypes().get(colIndex - 1);
+            switch (cellDataType) {
+                case BOOL:
+                    final char firstFlag = cellBuilder.toString().charAt(0);
+                    if (parseCellValueStringFlag) {
+                        result = firstFlag == '0' ? "false" : "true";
+                    } else {
+                        result = firstFlag != '0';
+                    }
+                    break;
+                case ERROR:
+                    result = "\\"ERROR:" + cellBuilder.toString() + "\\"";
+                    break;
+                case FORMULA:
+                    if (parseCellValueStringFlag) {
+                        result = cellBuilder.toString();
+                    } else {
+                        try {
+                            result = Double.parseDouble(cellBuilder.toString());
+                        } catch (Exception e) {
+                            result = cellBuilder.toString();
+                        }
+                    }
+                    break;
+                case INLINESTR:
+                    result = new XSSFRichTextString(cellBuilder.toString());
+                    break;
+                case SSTINDEX:
+                    //共享字符需要从共享字符表中取
+                    final int idx = Integer.parseInt(cellBuilder.toString());
+                    result = new XSSFRichTextString(sst.getEntryAt(idx));
+                    break;
+                case NUMBER:
+                    if (parseCellValueStringFlag) {
+                        final Integer styleAt = rowData.getCellStyles().get(colIndex - 1);
+                        if (styleAt != null) {
+                            final XSSFCellStyle cellStyle = stylesTable.getStyleAt(styleAt);
+                            final short formatIndex = cellStyle.getDataFormat();
+                            final String formatString = cellStyle.getDataFormatString();
+                            if (formatString == null) {
+                                result = cellBuilder.toString();
+                            } else if (formatString.contains("m/dd/yy")
+                                    || formatString.contains("m/d/yy")
+                                    || formatString.contains("yyyy/mm/dd")
+                                    || formatString.contains("yyyy/m/d")) {
+                                result = dataFormatter.formatRawCellContents(
+                                        Double.parseDouble(cellBuilder.toString()),
+                                        formatIndex, dateFormatStyle).replace("T", "");
+                            } else {
+                                result = dataFormatter.formatRawCellContents(Double.parseDouble(cellBuilder.toString()),
+                                        formatIndex, formatString).replace("_", "").trim();
+                            }
+                        } else {
+                            result = cellBuilder.toString();
+                        }
+                    } else {
+                        result = Double.parseDouble(cellBuilder.toString());
+                    }
+                    break;
+                default:
+                    result = null;
+            }
+            writeColData(result);
+            valueFlag = false;
+        } else if ("c".equals(name)) {
+            colIndex++;
+        } else if ("f".equals(name)) {
+            if (isReadFormula) {
+                rowData.getCellDataTypes().set(colIndex - 1, CellDataType.FORMULA);
+                writeColData(cellBuilder);
+                valueFlag = false;
+            }
+            cellBuilder.delete(0, cellBuilder.length());
+        } else if (isTElement) {
+            result = cellBuilder.toString().trim();
+            writeColData(result);
+            isTElement = false;
+            valueFlag = false;
+        } else if (printFlag && "definedName".equals(name)) {
+            result = cellBuilder.toString();
+            writePrint(result);
+            valueFlag = false;
+            printFlag = false;
+            cellBuilder.delete(0, cellBuilder.length());
+        }
+    }
+
+    /**
+     * 写sheet打印区域和打印标题
+     *
+     * @param result
+     */
+    private void writePrint(Object result) {
+        final String res = result.toString();
+        if (StringUtils.isBlank(localSheetId) || StringUtils.equals(res, "#REF!") || StringUtils.isBlank(res)) {
+            return;
+        }
+        final String rId = "rId" + (Integer.parseInt(localSheetId) + 1);
+        final String sheetName = sheetNameMap.get(rId);
+        final SheetPrint sheetPrint = sheetPrintMap.containsKey(sheetName) ? sheetPrintMap.get(sheetName) : new SheetPrint();
+        sheetPrint.setSheetName(sheetName);
+        sheetPrint.setSheetIndex(localSheetId);
+        for (String str : res.split(",")) {
+            final int i = isArea(str.split("!")[1]);
+            if (i == 1) {
+                sheetPrint.setPrintArea(res);
+            } else if (i == 2) {
+                sheetPrint.setPrintTitleRows(str);
+            } else if (i == 3) {
+                sheetPrint.setPrintTitleColumns(str);
+            }
+        }
+        sheetPrintMap.put(sheetName, sheetPrint);
+        localSheetId = null;
+    }
+
+    /**
+     * @param str
+     * @return 1-区域 2-顶端 3-左端 0-判断错误
+     */
+    private int isArea(String str) {
+        String split = str.split(":")[0];
+        int countMatches = StringUtils.countMatches(split, "$");
+        if (countMatches == 2) {
+            return 1;
+        } else if (countMatches == 1) {
+            String substr = split.substring(split.length() - 1);
+            char charAt = substr.charAt(0);
+            Pattern pattern = Pattern.compile("[0-9]*");
+            if (pattern.matcher(substr).matches()) {
+                return 2;
+            } else if ((charAt >= 'a' && charAt <= 'z') || (charAt >= 'A' && charAt <= 'Z')) {
+                return 3;
+            }
+        }
+        return 0;
+
+    }
+
+    /**
+     * 计算当前的最大行和列数
+     *
+     * @param ref
+     */
+    private void handleMax(String ref) {
+        final String[] range = ref.split(":");
+        String maxStr;
+        if (range.length == 1) {
+            maxStr = range[0];
+        } else {
+            maxStr = range[1];
+        }
+        final int[] maxPosition = parsePosition(maxStr);
+        rowMax = maxPosition[1];
+        colMax = maxPosition[0];
+    }
+
+    /**
+     * 把单元格数据添加到当前行的数据容器中
+     *
+     * @param result
+     */
+    private void writeColData(final Object result) {
+        rowData.getCellDataValues().add(result);
+        cellBuilder.delete(0, cellBuilder.length());
+    }
+
+    /**
+     * 数据append到cellBuilder中
+     *
+     * @param ch
+     * @param start
+     * @param length
+     */
+    @Override
+    public void characters(final char[] ch, final int start, final int length) {
+        if (valueFlag) {
+            cellBuilder.append(ch, start, length);
+        }
+    }
+
+    /**
+     * sheet处理
+     *
+     * @param sheetIndex
+     * @param sheetStyle
+     */
+    private void sheetHandler(int sheetIndex, SheetStyle sheetStyle) {
+
+    }
+
+    /**
+     * 获取position坐标的实际坐标数据
+     *
+     * @param position
+     * @return
+     */
+    private int[] parsePosition(String position) {
+        final int[] result = new int[2];
+        final String amPosition = position.replaceAll("[0-9]", "");
+        final char[] chars = amPosition.toUpperCase().toCharArray();
+        int ret = 0;
+        for (int i = 0; i < chars.length; i++) {
+            ret += (chars[i] - 'A' + 1) * Math.pow(26, chars.length - i - 1);
+        }
+        result[0] = ret;
+        result[1] = Integer.parseInt(position.replaceAll("[A-Z]", ""));
+        return result;
+    }
+
+    /**
+     * 调整列最大值
+     *
+     * @param colIndex
+     */
+    private void adjustColMax(final int colIndex) {
+        if (colIndex > colMax) {
+            colMax = colIndex;
+        }
+    }
+
+    /**
+     * 调整行最大值
+     *
+     * @param rowIndex
+     */
+    private void adjustRowMax(final int rowIndex) {
+        if (rowIndex > rowMax) {
+            rowMax = rowIndex;
+        }
+    }
+}
`,8),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const u=s(p,[["render",t]]);export{A as __pageData,u as default}; diff --git "a/assets/java_framework_POI\344\272\213\344\273\266\346\250\241\345\274\217\350\247\243\346\236\220\345\271\266\350\257\273\345\217\226Excel\346\226\207\344\273\266\346\225\260\346\215\256.md.ecde083a.lean.js" "b/assets/java_framework_POI\344\272\213\344\273\266\346\250\241\345\274\217\350\247\243\346\236\220\345\271\266\350\257\273\345\217\226Excel\346\226\207\344\273\266\346\225\260\346\215\256.md.ecde083a.lean.js" new file mode 100644 index 000000000..202cca191 --- /dev/null +++ "b/assets/java_framework_POI\344\272\213\344\273\266\346\250\241\345\274\217\350\247\243\346\236\220\345\271\266\350\257\273\345\217\226Excel\346\226\207\344\273\266\346\225\260\346\215\256.md.ecde083a.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const A=JSON.parse('{"title":"POI事件模式解析并读取Excel文件数据","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/POI事件模式解析并读取Excel文件数据.md","filePath":"java/framework/POI事件模式解析并读取Excel文件数据.md","lastUpdated":1708510102000}'),p={name:"java/framework/POI事件模式解析并读取Excel文件数据.md"},o=l("",8),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const u=s(p,[["render",t]]);export{A as __pageData,u as default}; diff --git "a/assets/java_framework_Spring Cloud Config\346\216\245\345\205\245.md.d19b8abd.js" "b/assets/java_framework_Spring Cloud Config\346\216\245\345\205\245.md.d19b8abd.js" new file mode 100644 index 000000000..a0bf39a23 --- /dev/null +++ "b/assets/java_framework_Spring Cloud Config\346\216\245\345\205\245.md.d19b8abd.js" @@ -0,0 +1,121 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"Spring Cloud Config接入","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/Spring Cloud Config接入.md","filePath":"java/framework/Spring Cloud Config接入.md","lastUpdated":1708510102000}'),p={name:"java/framework/Spring Cloud Config接入.md"},o=l(`

Spring Cloud Config接入

微服务配置中心能帮助统一管理多个环境、多个应用程序的外部化配置,不需要在某个配置有改动或新增某个配置项时去多个节点上一个个修改,做到了一次改动,处处使用。这里和Eureka注册中心配合使用。

Spring Cloud Config

SpringCloud子项目,提供了分布式系统中外部配置管理的能力,分为serverclient 两部分。官网地址

配合git仓库使用

建立一个git仓库,将配置文件放在仓库里,配置文件格式应用名-profile.properties(yml) ,多服务的情况包名可根据服务前缀命名,在配置中心server端配置中增加search-path配置即可,比如多个包名app-1/app-2,可以配置search-path:app-*

服务端

依赖

xml
<dependencies>
+    <dependency>
+        <groupId>org.springframework.cloud</groupId>
+        <artifactId>spring-cloud-config-server</artifactId>
+    </dependency>
+    <dependency>
+        <groupId>org.springframework.cloud</groupId>
+        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
+    </dependency>
+</dependencies>
<dependencies>
+    <dependency>
+        <groupId>org.springframework.cloud</groupId>
+        <artifactId>spring-cloud-config-server</artifactId>
+    </dependency>
+    <dependency>
+        <groupId>org.springframework.cloud</groupId>
+        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
+    </dependency>
+</dependencies>

配置文件

yaml
server:
+  port: 8077
+spring:
+  application:
+    name: spring-cloud-config-center
+  cloud:
+    config:
+      server:
+        git:
+          uri: git仓库地址
+          username: 账号
+          password: 密码
+          search-paths: app-* #搜索路径,使用通配符
+
+eureka:
+  client:
+    service-url:
+      defaultZone: http://admin:admin@127.0.0.1:19991/eureka/
+      fetch-registry-interval-seconds: 5
+
+  instance:
+    prefer-ip-address: true
+    lease-expiration-duration-in-seconds: 30
+    lease-renewal-interval-in-seconds: 10
server:
+  port: 8077
+spring:
+  application:
+    name: spring-cloud-config-center
+  cloud:
+    config:
+      server:
+        git:
+          uri: git仓库地址
+          username: 账号
+          password: 密码
+          search-paths: app-* #搜索路径,使用通配符
+
+eureka:
+  client:
+    service-url:
+      defaultZone: http://admin:admin@127.0.0.1:19991/eureka/
+      fetch-registry-interval-seconds: 5
+
+  instance:
+    prefer-ip-address: true
+    lease-expiration-duration-in-seconds: 30
+    lease-renewal-interval-in-seconds: 10

启动类

java
@SpringBootApplication
+@EnableEurekaClient
+@EnableConfigServer
+public class ConfigCenterApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(ConfigCenterApplication.class, args);
+    }
+}
@SpringBootApplication
+@EnableEurekaClient
+@EnableConfigServer
+public class ConfigCenterApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(ConfigCenterApplication.class, args);
+    }
+}

服务端

依赖

xml
<!--springcloud config客户端-->
+<dependency>
+    <groupId>org.springframework.cloud</groupId>
+    <artifactId>spring-cloud-config-client</artifactId>
+</dependency>
<!--springcloud config客户端-->
+<dependency>
+    <groupId>org.springframework.cloud</groupId>
+    <artifactId>spring-cloud-config-client</artifactId>
+</dependency>

配置文件

yaml
spring:
+  application:
+    name: app-1
+  cloud:
+    config:
+      profile: @spring.profiles.active@
+      label: master
+      discovery:
+        enabled: true
+        service-id: spring-cloud-config-center
+eureka:
+  client:
+    service-url:
+      defaultZone: http://admin:admin@127.0.0.1:19991/eureka/
spring:
+  application:
+    name: app-1
+  cloud:
+    config:
+      profile: @spring.profiles.active@
+      label: master
+      discovery:
+        enabled: true
+        service-id: spring-cloud-config-center
+eureka:
+  client:
+    service-url:
+      defaultZone: http://admin:admin@127.0.0.1:19991/eureka/

TIP

SpringCloudConfig配合服务发现使用时,必须在bootstrap.yaml(或环境变量) 开启服务发现spring.cloud.config.discovery.enabled=true,并且要配置注册中心的地址eureka.client.serviceUrl.defaultZone ,服务启动时会首先去注册中心找到配置中心server。

The HTTP service has resources in the following form:

txt
/{application}/{profile}[/{label}]
+/{application}-{profile}.yml
+/{label}/{application}-{profile}.yml
+/{application}-{profile}.properties
+/{label}/{application}-{profile}.properties
/{application}/{profile}[/{label}]
+/{application}-{profile}.yml
+/{label}/{application}-{profile}.yml
+/{application}-{profile}.properties
+/{label}/{application}-{profile}.properties

以下引自官网

Discovery First Bootstrap

If you use a DiscoveryClient implementation, such as Spring Cloud Netflix and Eureka Service Discovery or Spring Cloud Consul, you can have the Config Server register with the Discovery Service. However, in the default “Config First” mode, clients cannot take advantage of the registration.

If you prefer to use DiscoveryClient to locate the Config Server, you can do so by setting spring.cloud.config.discovery.enabled=true (the default is false). The net result of doing so is that client applications all need a bootstrap.yml (or an environment variable) with the appropriate discovery configuration. For example, with Spring Cloud Netflix, you need to define the Eureka server address (for example, in eureka.client.serviceUrl.defaultZone). The price for using this option is an extra network round trip on startup, to locate the service registration. The benefit is that, as long as the Discovery Service is a fixed point, the Config Server can change its coordinates. The default service ID is configserver, but you can change that on the client by setting spring.cloud.config.discovery.serviceId (and on the server, in the usual way for a service, such as by setting spring.application.name).

配置中心启动后,其他服务向配置中心获取配置文件时,配置中心会去git上拉配置并缓存在本地的临时目录。

默认情况下,它们被放在带有config-repo-前缀的系统临时目录中。例如,在linux上,它可以是/tmp/config-repo-<randomid> 。一些操作系统会定期清理临时目录,导致意料之外的情况发生,例如丢失属性。需要在配置文件中增加配置spring.cloud.config.server.git.basedirspring.cloud.config.server.svn.basedir来指定一个不在系统临时路径下的目录。

`,26),e=[o];function t(c,r,i,E,y,d){return n(),a("div",null,e)}const h=s(p,[["render",t]]);export{u as __pageData,h as default}; diff --git "a/assets/java_framework_Spring Cloud Config\346\216\245\345\205\245.md.d19b8abd.lean.js" "b/assets/java_framework_Spring Cloud Config\346\216\245\345\205\245.md.d19b8abd.lean.js" new file mode 100644 index 000000000..296503558 --- /dev/null +++ "b/assets/java_framework_Spring Cloud Config\346\216\245\345\205\245.md.d19b8abd.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"Spring Cloud Config接入","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/Spring Cloud Config接入.md","filePath":"java/framework/Spring Cloud Config接入.md","lastUpdated":1708510102000}'),p={name:"java/framework/Spring Cloud Config接入.md"},o=l("",26),e=[o];function t(c,r,i,E,y,d){return n(),a("div",null,e)}const h=s(p,[["render",t]]);export{u as __pageData,h as default}; diff --git "a/assets/java_framework_SpringMVC\347\232\204\346\211\247\350\241\214\346\265\201\347\250\213\346\272\220\347\240\201\345\210\206\346\236\220.md.ca08bf1a.js" "b/assets/java_framework_SpringMVC\347\232\204\346\211\247\350\241\214\346\265\201\347\250\213\346\272\220\347\240\201\345\210\206\346\236\220.md.ca08bf1a.js" new file mode 100644 index 000000000..699d03ae5 --- /dev/null +++ "b/assets/java_framework_SpringMVC\347\232\204\346\211\247\350\241\214\346\265\201\347\250\213\346\272\220\347\240\201\345\210\206\346\236\220.md.ca08bf1a.js" @@ -0,0 +1,617 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"SpringMVC的执行流程源码分析","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/SpringMVC的执行流程源码分析.md","filePath":"java/framework/SpringMVC的执行流程源码分析.md","lastUpdated":1708510102000}'),p={name:"java/framework/SpringMVC的执行流程源码分析.md"},e=l(`

SpringMVC的执行流程源码分析

背景

一个常见的面试/笔试题: SpringMVC的执行流程

答:

1、前端请求到核心前端控制器DispatcherServlet

2、DispatcherServlet收到请求调用HandlerMapping处理器映射器。

3、处理器映射器找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。

4、 DispatcherServlet调用HandlerAdapter处理器适配器。

5、HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。

6、Controller执行完成返回ModelAndView。

7、HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。

8、DispatcherServlet将ModelAndView传给ViewReslover视图解析器。

9、ViewReslover解析后返回具体View.

10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。

11、DispatcherServlet响应用户。

为了应付面试相信很多人和我一样死记硬背过,今天就来看下源码,看看这个流程的庐山真面目。

首先找到DispatcherServlet类,看看它的继承关系 diagram.png

1.DispathcerServlet的初始化过程

过程图 2.jpg

初始化方法

java
/**
+	 * This implementation calls {@link #initStrategies}.
+	 */
+	@Override
+	protected void onRefresh(ApplicationContext context) {
+		initStrategies(context);
+	}
+
+	/**
+	 * Initialize the strategy objects that this servlet uses.
+	 * <p>May be overridden in subclasses in order to initialize further strategy objects.
+	 */
+	protected void initStrategies(ApplicationContext context) {
+		initMultipartResolver(context);
+		initLocaleResolver(context);
+		initThemeResolver(context);
+		initHandlerMappings(context);
+		initHandlerAdapters(context);
+		initHandlerExceptionResolvers(context);
+		initRequestToViewNameTranslator(context);
+		initViewResolvers(context);
+		initFlashMapManager(context);
+	}
/**
+	 * This implementation calls {@link #initStrategies}.
+	 */
+	@Override
+	protected void onRefresh(ApplicationContext context) {
+		initStrategies(context);
+	}
+
+	/**
+	 * Initialize the strategy objects that this servlet uses.
+	 * <p>May be overridden in subclasses in order to initialize further strategy objects.
+	 */
+	protected void initStrategies(ApplicationContext context) {
+		initMultipartResolver(context);
+		initLocaleResolver(context);
+		initThemeResolver(context);
+		initHandlerMappings(context);
+		initHandlerAdapters(context);
+		initHandlerExceptionResolvers(context);
+		initRequestToViewNameTranslator(context);
+		initViewResolvers(context);
+		initFlashMapManager(context);
+	}

可以看到initStrategies方法初始化了9个组件,其中不乏文章开头中问题涉及到的组件

这九个初始化方法做的事情如下:

  • initMultipartResolver:初始化MultipartResolver,用于处理文件上传服务,如果有文件上传,那么就会将当前的HttpServletRequest包装成DefaultMultipartHttpServletRequest,并且将每个上传的内容封装成CommonsMultipartFile对象。需要在dispatcherServlet-servlet.xml中配置文件上传解
  • initLocaleResolver:用于处理应用的国际化问题,本地化解析策略。
  • initThemeResolver:用于定义一个主题。
  • initHandlerMapping:用于定义请求映射关系。
  • initHandlerAdapters:用于根据Handler的类型定义不同的处理规则。
  • initHandlerExceptionResolvers:当Handler处理出错后,会通过此将错误日志记录在log文件中,默认实现类是SimpleMappingExceptionResolver
  • initRequestToViewNameTranslators:将指定的ViewName按照定义的RequestToViewNameTranslators替换成想要的格式。
  • initViewResolvers:用于将View解析成页面。
  • initFlashMapManager:用于生成FlashMap管理器。

2.DispatcherServlet如何处理用户请求

首先要明确DispatcherServlet也是一个Servlet,也要遵守servlet接口的规范,servlet通过service方法来根据不同的请求方式来执行doGet,doPost等方法。而FrameworkServlet重写了service方法,并调用了processRequest方法,processRequest方法中又调用了抽象方法doService,DispatcherServlet实现了doService方法,并在该方法中调用了doDispatch方法,doDispatch方法就是具体的请求处理过程

过程图: 3.jpeg

3.doDispatch方法

java
/**
+	 * Process the actual dispatching to the handler.
+	 * <p>The handler will be obtained by applying the servlet's HandlerMappings in order.
+	 * The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters
+	 * to find the first that supports the handler class.
+	 * <p>All HTTP methods are handled by this method. It's up to HandlerAdapters or handlers
+	 * themselves to decide which methods are acceptable.
+	 * @param request current HTTP request
+	 * @param response current HTTP response
+	 * @throws Exception in case of any kind of processing failure
+	 */
+	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
+		HttpServletRequest processedRequest = request;
+		HandlerExecutionChain mappedHandler = null;
+		boolean multipartRequestParsed = false;
+
+		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
+
+		try {
+			ModelAndView mv = null;
+			Exception dispatchException = null;
+
+			try {
+                //判断是否为上传文件的请求,如果不是就返回原始的request,否则做相应的处理
+				processedRequest = checkMultipart(request);
+				multipartRequestParsed = (processedRequest != request);
+				
+				// Determine handler for the current request.
+                //找到当前请求对应的处理器,返回的是对应的处理器及拦截器集合
+				mappedHandler = getHandler(processedRequest);
+				if (mappedHandler == null) {
+					noHandlerFound(processedRequest, response);
+					return;
+				}
+
+				// Determine handler adapter for the current request.
+                //根据上一步找到的处理器,再找到对应的处理器适配器
+				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
+
+				// Process last-modified header, if supported by the handler.
+				String method = request.getMethod();
+				boolean isGet = "GET".equals(method);
+				if (isGet || "HEAD".equals(method)) {
+					long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
+					if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
+						return;
+					}
+				}
+				//这里执行了所有的拦截器中的preHandle方法 也就是为什么拦截器总在controller前先执行
+				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
+					return;
+				}
+
+				// Actually invoke the handler.
+                //调用处理器的处理方法
+				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
+
+				if (asyncManager.isConcurrentHandlingStarted()) {
+					return;
+				}
+				//设置modelAndView的默认名
+				applyDefaultViewName(processedRequest, mv);
+                //执行拦截器的postHanle方法
+				mappedHandler.applyPostHandle(processedRequest, response, mv);
+			}
+			catch (Exception ex) {
+				dispatchException = ex;
+			}
+			catch (Throwable err) {
+				// As of 4.3, we're processing Errors thrown from handler methods as well,
+				// making them available for @ExceptionHandler methods and other scenarios.
+				dispatchException = new NestedServletException("Handler dispatch failed", err);
+			}
+            //处理modelAndView并渲染
+			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
+		}
+		catch (Exception ex) {
+			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
+		}
+		catch (Throwable err) {
+			triggerAfterCompletion(processedRequest, response, mappedHandler,
+					new NestedServletException("Handler processing failed", err));
+		}
+		finally {
+			if (asyncManager.isConcurrentHandlingStarted()) {
+				// Instead of postHandle and afterCompletion
+				if (mappedHandler != null) {
+					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
+				}
+			}
+			else {
+				// Clean up any resources used by a multipart request.
+				if (multipartRequestParsed) {
+					cleanupMultipart(processedRequest);
+				}
+			}
+		}
+	}
/**
+	 * Process the actual dispatching to the handler.
+	 * <p>The handler will be obtained by applying the servlet's HandlerMappings in order.
+	 * The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters
+	 * to find the first that supports the handler class.
+	 * <p>All HTTP methods are handled by this method. It's up to HandlerAdapters or handlers
+	 * themselves to decide which methods are acceptable.
+	 * @param request current HTTP request
+	 * @param response current HTTP response
+	 * @throws Exception in case of any kind of processing failure
+	 */
+	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
+		HttpServletRequest processedRequest = request;
+		HandlerExecutionChain mappedHandler = null;
+		boolean multipartRequestParsed = false;
+
+		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
+
+		try {
+			ModelAndView mv = null;
+			Exception dispatchException = null;
+
+			try {
+                //判断是否为上传文件的请求,如果不是就返回原始的request,否则做相应的处理
+				processedRequest = checkMultipart(request);
+				multipartRequestParsed = (processedRequest != request);
+				
+				// Determine handler for the current request.
+                //找到当前请求对应的处理器,返回的是对应的处理器及拦截器集合
+				mappedHandler = getHandler(processedRequest);
+				if (mappedHandler == null) {
+					noHandlerFound(processedRequest, response);
+					return;
+				}
+
+				// Determine handler adapter for the current request.
+                //根据上一步找到的处理器,再找到对应的处理器适配器
+				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
+
+				// Process last-modified header, if supported by the handler.
+				String method = request.getMethod();
+				boolean isGet = "GET".equals(method);
+				if (isGet || "HEAD".equals(method)) {
+					long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
+					if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
+						return;
+					}
+				}
+				//这里执行了所有的拦截器中的preHandle方法 也就是为什么拦截器总在controller前先执行
+				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
+					return;
+				}
+
+				// Actually invoke the handler.
+                //调用处理器的处理方法
+				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
+
+				if (asyncManager.isConcurrentHandlingStarted()) {
+					return;
+				}
+				//设置modelAndView的默认名
+				applyDefaultViewName(processedRequest, mv);
+                //执行拦截器的postHanle方法
+				mappedHandler.applyPostHandle(processedRequest, response, mv);
+			}
+			catch (Exception ex) {
+				dispatchException = ex;
+			}
+			catch (Throwable err) {
+				// As of 4.3, we're processing Errors thrown from handler methods as well,
+				// making them available for @ExceptionHandler methods and other scenarios.
+				dispatchException = new NestedServletException("Handler dispatch failed", err);
+			}
+            //处理modelAndView并渲染
+			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
+		}
+		catch (Exception ex) {
+			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
+		}
+		catch (Throwable err) {
+			triggerAfterCompletion(processedRequest, response, mappedHandler,
+					new NestedServletException("Handler processing failed", err));
+		}
+		finally {
+			if (asyncManager.isConcurrentHandlingStarted()) {
+				// Instead of postHandle and afterCompletion
+				if (mappedHandler != null) {
+					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
+				}
+			}
+			else {
+				// Clean up any resources used by a multipart request.
+				if (multipartRequestParsed) {
+					cleanupMultipart(processedRequest);
+				}
+			}
+		}
+	}

1.getHandler方法

该方法返回的是HandlerExecutionChain对象,其中包含了处理器和过滤器的集合,这里调用了handlerMapping的getHandler方法,该方法主要调用了getHandlerExecutionChain方法,handlerMapping的集合是在初始化dispatchServlet的时候从beanFactory中查找并封装的,具体的handlerMappings初始化细节可以看initHandlerMappings方法,handlerMapping有多种类型,对应不同的请求,比如请求静态资源的和请求接口的等,此处我们以请求一个查询接口为例

protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
+   if (this.handlerMappings != null) {
+   		//循环所有的handlerMapping,直到找到对应的handler
+      for (HandlerMapping mapping : this.handlerMappings) {
+         HandlerExecutionChain handler = mapping.getHandler(request);
+         if (handler != null) {
+            return handler;
+         }
+      }
+   }
+   return null;
+}
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
+   if (this.handlerMappings != null) {
+   		//循环所有的handlerMapping,直到找到对应的handler
+      for (HandlerMapping mapping : this.handlerMappings) {
+         HandlerExecutionChain handler = mapping.getHandler(request);
+         if (handler != null) {
+            return handler;
+         }
+      }
+   }
+   return null;
+}
  • getHandlerExecutionChain方法

这里调用的是AbstractHandlerMethodMapping的getHandlerInternal方法,该方法又调用了同一个类中的lookupHandlerMethod方法

  • lookupHandlerMethod方法会根据请求的uri在mappingRegistry中查询已经注册了的请求路径(requestMapping注解中的路径),如果能直接从map中get到非空的list,就直接根据list匹配对应的HandleMethod对象,如果mappingRegistry中get不到,就尝试使用uri路径匹配,例如带有url参数的这种格式/test/{username}的格式,{username}会被替换为.*的正则表达式去进行匹配,匹配到后返回;

  • getHandlerExecutionChain方法则是根据请求的路径匹配拦截器的路径,如果有匹配到的,就添加到执行链当中

java
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
+    	//根据request找到对应的handler
+		Object handler = getHandlerInternal(request);
+		if (handler == null) {
+			handler = getDefaultHandler();
+		}
+		if (handler == null) {
+			return null;
+		}
+		// Bean name or resolved handler?
+		if (handler instanceof String) {
+			String handlerName = (String) handler;
+			handler = obtainApplicationContext().getBean(handlerName);
+		}
+
+		HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
+
+		if (logger.isTraceEnabled()) {
+			logger.trace("Mapped to " + handler);
+		}
+		else if (logger.isDebugEnabled() && !request.getDispatcherType().equals(DispatcherType.ASYNC)) {
+			logger.debug("Mapped to " + executionChain.getHandler());
+		}
+
+		if (CorsUtils.isCorsRequest(request)) {
+			CorsConfiguration globalConfig = this.corsConfigurationSource.getCorsConfiguration(request);
+			CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
+			CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
+			executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
+		}
+
+		return executionChain;
+	}
+
+
+
+protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
+		HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
+				(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
+
+		String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
+		for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
+			if (interceptor instanceof MappedInterceptor) {
+				MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
+				if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
+					chain.addInterceptor(mappedInterceptor.getInterceptor());
+				}
+			}
+			else {
+				chain.addInterceptor(interceptor);
+			}
+		}
+		return chain;
+	}
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
+    	//根据request找到对应的handler
+		Object handler = getHandlerInternal(request);
+		if (handler == null) {
+			handler = getDefaultHandler();
+		}
+		if (handler == null) {
+			return null;
+		}
+		// Bean name or resolved handler?
+		if (handler instanceof String) {
+			String handlerName = (String) handler;
+			handler = obtainApplicationContext().getBean(handlerName);
+		}
+
+		HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
+
+		if (logger.isTraceEnabled()) {
+			logger.trace("Mapped to " + handler);
+		}
+		else if (logger.isDebugEnabled() && !request.getDispatcherType().equals(DispatcherType.ASYNC)) {
+			logger.debug("Mapped to " + executionChain.getHandler());
+		}
+
+		if (CorsUtils.isCorsRequest(request)) {
+			CorsConfiguration globalConfig = this.corsConfigurationSource.getCorsConfiguration(request);
+			CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
+			CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
+			executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
+		}
+
+		return executionChain;
+	}
+
+
+
+protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
+		HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
+				(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
+
+		String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
+		for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
+			if (interceptor instanceof MappedInterceptor) {
+				MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
+				if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
+					chain.addInterceptor(mappedInterceptor.getInterceptor());
+				}
+			}
+			else {
+				chain.addInterceptor(interceptor);
+			}
+		}
+		return chain;
+	}

2.getHandlerAdapter方法

这个方法比较简单,就是从handlerAdapter集合中遍历找到支持当前请求的处理器适配器,用到了handlerAdapter的supports方法,测试的接口请求会调用AbstractHandlerMethodAdapter这个类的supports方法

java
/**
+ * This implementation expects the handler to be an {@link HandlerMethod}.
+ * @param handler the handler instance to check
+ * @return whether or not this adapter can adapt the given handler
+ */
+@Override
+public final boolean supports(Object handler) {
+   return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
+}
/**
+ * This implementation expects the handler to be an {@link HandlerMethod}.
+ * @param handler the handler instance to check
+ * @return whether or not this adapter can adapt the given handler
+ */
+@Override
+public final boolean supports(Object handler) {
+   return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
+}

3.handle方法

在找到对应的处理器适配器后,会执行拦截器的preHandle方法,然后执行处理器适配器的handle方法,这个就是实际上调用我们所写的controller了,该方法有几个实现 Snipaste_20200716_131323.jpg 这里调用的是AbstractHandlerMethodAdapter的方法,该方法调用了抽象方法handleInternal,它的实现在RequestMappingHandlerAdapter类中

java
protected ModelAndView handleInternal(HttpServletRequest request,
+      HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
+
+   ModelAndView mav;
+   checkRequest(request);
+
+   // Execute invokeHandlerMethod in synchronized block if required.
+   if (this.synchronizeOnSession) {
+      HttpSession session = request.getSession(false);
+      if (session != null) {
+         Object mutex = WebUtils.getSessionMutex(session);
+         synchronized (mutex) {
+            mav = invokeHandlerMethod(request, response, handlerMethod);
+         }
+      }
+      else {
+         // No HttpSession available -> no mutex necessary
+         mav = invokeHandlerMethod(request, response, handlerMethod);
+      }
+   }
+   else {
+      // No synchronization on session demanded at all...
+      mav = invokeHandlerMethod(request, response, handlerMethod);
+   }
+
+   if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
+      if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
+         applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
+      }
+      else {
+         prepareResponse(response);
+      }
+   }
+
+   return mav;
+}
protected ModelAndView handleInternal(HttpServletRequest request,
+      HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
+
+   ModelAndView mav;
+   checkRequest(request);
+
+   // Execute invokeHandlerMethod in synchronized block if required.
+   if (this.synchronizeOnSession) {
+      HttpSession session = request.getSession(false);
+      if (session != null) {
+         Object mutex = WebUtils.getSessionMutex(session);
+         synchronized (mutex) {
+            mav = invokeHandlerMethod(request, response, handlerMethod);
+         }
+      }
+      else {
+         // No HttpSession available -> no mutex necessary
+         mav = invokeHandlerMethod(request, response, handlerMethod);
+      }
+   }
+   else {
+      // No synchronization on session demanded at all...
+      mav = invokeHandlerMethod(request, response, handlerMethod);
+   }
+
+   if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
+      if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
+         applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
+      }
+      else {
+         prepareResponse(response);
+      }
+   }
+
+   return mav;
+}

其中重点在invokeHandlerMethod方法,这个方法首先初始化了一个新的handlerMethod对象,添加了相关的解析组件,返回值处理器等等,然后执行了invokeAndHandle方法,然后最终调用了InvocableHandlerMethod类中的doInvoke方法

java
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
+      HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
+   ServletWebRequest webRequest = new ServletWebRequest(request, response);
+   try {
+      WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
+      ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
+
+      ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
+      if (this.argumentResolvers != null) {
+         invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
+      }
+      if (this.returnValueHandlers != null) {
+         invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
+      }
+      invocableMethod.setDataBinderFactory(binderFactory);
+      invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
+
+      ModelAndViewContainer mavContainer = new ModelAndViewContainer();
+      mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
+      modelFactory.initModel(webRequest, mavContainer, invocableMethod);
+      mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
+
+      AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
+      asyncWebRequest.setTimeout(this.asyncRequestTimeout);
+
+      WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
+      asyncManager.setTaskExecutor(this.taskExecutor);
+      asyncManager.setAsyncWebRequest(asyncWebRequest);
+      asyncManager.registerCallableInterceptors(this.callableInterceptors);
+      asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
+
+      if (asyncManager.hasConcurrentResult()) {
+         Object result = asyncManager.getConcurrentResult();
+         mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
+         asyncManager.clearConcurrentResult();
+         LogFormatUtils.traceDebug(logger, traceOn -> {
+            String formatted = LogFormatUtils.formatValue(result, !traceOn);
+            return "Resume with async result [" + formatted + "]";
+         });
+         invocableMethod = invocableMethod.wrapConcurrentResult(result);
+      }
+
+      invocableMethod.invokeAndHandle(webRequest, mavContainer);
+      if (asyncManager.isConcurrentHandlingStarted()) {
+         return null;
+      }
+
+      return getModelAndView(mavContainer, modelFactory, webRequest);
+   }
+   finally {
+      webRequest.requestCompleted();
+   }
+}
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
+      HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
+   ServletWebRequest webRequest = new ServletWebRequest(request, response);
+   try {
+      WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
+      ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
+
+      ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
+      if (this.argumentResolvers != null) {
+         invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
+      }
+      if (this.returnValueHandlers != null) {
+         invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
+      }
+      invocableMethod.setDataBinderFactory(binderFactory);
+      invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
+
+      ModelAndViewContainer mavContainer = new ModelAndViewContainer();
+      mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
+      modelFactory.initModel(webRequest, mavContainer, invocableMethod);
+      mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
+
+      AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
+      asyncWebRequest.setTimeout(this.asyncRequestTimeout);
+
+      WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
+      asyncManager.setTaskExecutor(this.taskExecutor);
+      asyncManager.setAsyncWebRequest(asyncWebRequest);
+      asyncManager.registerCallableInterceptors(this.callableInterceptors);
+      asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
+
+      if (asyncManager.hasConcurrentResult()) {
+         Object result = asyncManager.getConcurrentResult();
+         mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
+         asyncManager.clearConcurrentResult();
+         LogFormatUtils.traceDebug(logger, traceOn -> {
+            String formatted = LogFormatUtils.formatValue(result, !traceOn);
+            return "Resume with async result [" + formatted + "]";
+         });
+         invocableMethod = invocableMethod.wrapConcurrentResult(result);
+      }
+
+      invocableMethod.invokeAndHandle(webRequest, mavContainer);
+      if (asyncManager.isConcurrentHandlingStarted()) {
+         return null;
+      }
+
+      return getModelAndView(mavContainer, modelFactory, webRequest);
+   }
+   finally {
+      webRequest.requestCompleted();
+   }
+}
  • doInvoke方法

这里就比较明显了,首先利用暴力反射将方法设置为可访问的,然后直接反射调用并返回结果

java
/**
+ * Invoke the handler method with the given argument values.
+ */
+@Nullable
+protected Object doInvoke(Object... args) throws Exception {
+   ReflectionUtils.makeAccessible(getBridgedMethod());
+   try {
+      return getBridgedMethod().invoke(getBean(), args);
+   }
+   catch (IllegalArgumentException ex) {
+      assertTargetBean(getBridgedMethod(), getBean(), args);
+      String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
+      throw new IllegalStateException(formatInvokeError(text, args), ex);
+   }
+   catch (InvocationTargetException ex) {
+      // Unwrap for HandlerExceptionResolvers ...
+      Throwable targetException = ex.getTargetException();
+      if (targetException instanceof RuntimeException) {
+         throw (RuntimeException) targetException;
+      }
+      else if (targetException instanceof Error) {
+         throw (Error) targetException;
+      }
+      else if (targetException instanceof Exception) {
+         throw (Exception) targetException;
+      }
+      else {
+         throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException);
+      }
+   }
+}
/**
+ * Invoke the handler method with the given argument values.
+ */
+@Nullable
+protected Object doInvoke(Object... args) throws Exception {
+   ReflectionUtils.makeAccessible(getBridgedMethod());
+   try {
+      return getBridgedMethod().invoke(getBean(), args);
+   }
+   catch (IllegalArgumentException ex) {
+      assertTargetBean(getBridgedMethod(), getBean(), args);
+      String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
+      throw new IllegalStateException(formatInvokeError(text, args), ex);
+   }
+   catch (InvocationTargetException ex) {
+      // Unwrap for HandlerExceptionResolvers ...
+      Throwable targetException = ex.getTargetException();
+      if (targetException instanceof RuntimeException) {
+         throw (RuntimeException) targetException;
+      }
+      else if (targetException instanceof Error) {
+         throw (Error) targetException;
+      }
+      else if (targetException instanceof Exception) {
+         throw (Exception) targetException;
+      }
+      else {
+         throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException);
+      }
+   }
+}

返回modelAndview对象后就是渲染的一些操作

`,48),o=[e];function t(r,c,E,y,i,d){return n(),a("div",null,o)}const F=s(p,[["render",t]]);export{u as __pageData,F as default}; diff --git "a/assets/java_framework_SpringMVC\347\232\204\346\211\247\350\241\214\346\265\201\347\250\213\346\272\220\347\240\201\345\210\206\346\236\220.md.ca08bf1a.lean.js" "b/assets/java_framework_SpringMVC\347\232\204\346\211\247\350\241\214\346\265\201\347\250\213\346\272\220\347\240\201\345\210\206\346\236\220.md.ca08bf1a.lean.js" new file mode 100644 index 000000000..4c61244ee --- /dev/null +++ "b/assets/java_framework_SpringMVC\347\232\204\346\211\247\350\241\214\346\265\201\347\250\213\346\272\220\347\240\201\345\210\206\346\236\220.md.ca08bf1a.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"SpringMVC的执行流程源码分析","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/SpringMVC的执行流程源码分析.md","filePath":"java/framework/SpringMVC的执行流程源码分析.md","lastUpdated":1708510102000}'),p={name:"java/framework/SpringMVC的执行流程源码分析.md"},e=l("",48),o=[e];function t(r,c,E,y,i,d){return n(),a("div",null,o)}const F=s(p,[["render",t]]);export{u as __pageData,F as default}; diff --git "a/assets/java_framework_Spring\351\205\215\347\275\256\345\222\214\346\235\241\344\273\266\345\214\226\347\273\204\344\273\266\345\212\240\350\275\275\346\263\250\350\247\243.md.e8c07bfa.js" "b/assets/java_framework_Spring\351\205\215\347\275\256\345\222\214\346\235\241\344\273\266\345\214\226\347\273\204\344\273\266\345\212\240\350\275\275\346\263\250\350\247\243.md.e8c07bfa.js" new file mode 100644 index 000000000..d2673532e --- /dev/null +++ "b/assets/java_framework_Spring\351\205\215\347\275\256\345\222\214\346\235\241\344\273\266\345\214\226\347\273\204\344\273\266\345\212\240\350\275\275\346\263\250\350\247\243.md.e8c07bfa.js" @@ -0,0 +1 @@ +import{_ as o,o as e,c as i,Q as n}from"./chunks/framework.dc1f4608.js";const g=JSON.parse('{"title":"Spring配置和条件化组件加载注解","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/Spring配置和条件化组件加载注解.md","filePath":"java/framework/Spring配置和条件化组件加载注解.md","lastUpdated":1708510102000}'),a={name:"java/framework/Spring配置和条件化组件加载注解.md"},t=n('

Spring配置和条件化组件加载注解

  • @ConditionalOnProperty:该注解用于根据配置属性的值来决定是否启用或禁用特定的配置项。通过指定属性名称和值,可以在配置文件中动态地控制应用程序的行为。

  • @ConditionalOnClass:该注解在类路径中存在特定的类时才会生效。它可以用来根据是否引入了某个类来决定是否加载或配置相关的组件。

  • @EnableConfigurationProperties:该注解用于启用特定的配置属性绑定功能。它通常与 @ConfigurationProperties 注解一起使用,用于将配置文件中的属性值绑定到对应的 Java 对象中。

  • @ConfigurationProperties:将配置文件中的属性值映射到指定的Java类。

  • @ConditionalOnBean:根据指定的bean的存在与否,有条件地加载一个组件。

  • @Conditional:根据指定的条件,有条件地加载一个组件。可以使用自定义条件类。

  • @ConditionalOnMissingBean:如果指定的bean不存在,则有条件地加载一个组件。

  • @ConditionalOnMissingClass:如果类路径中缺少指定的类,则有条件地加载一个组件。

  • @AutoConfigureBefore:用于指定某个自动配置类在另一个指定的自动配置类之前生效。它可以控制自动配置类的加载顺序,确保特定的自动配置类在其他自动配置之前被应用。

  • @ConditionalOnExpression:根据指定的SpEL表达式,有条件地加载一个组件。

  • @ConditionalOnWebApplication:根据应用程序是否为Web应用程序,有条件地加载一个组件。

  • @ConditionalOnResource:根据指定资源的存在与否,有条件地加载一个组件。

',2),r=[t];function l(p,d,c,s,_,f){return e(),i("div",null,r)}const u=o(a,[["render",l]]);export{g as __pageData,u as default}; diff --git "a/assets/java_framework_Spring\351\205\215\347\275\256\345\222\214\346\235\241\344\273\266\345\214\226\347\273\204\344\273\266\345\212\240\350\275\275\346\263\250\350\247\243.md.e8c07bfa.lean.js" "b/assets/java_framework_Spring\351\205\215\347\275\256\345\222\214\346\235\241\344\273\266\345\214\226\347\273\204\344\273\266\345\212\240\350\275\275\346\263\250\350\247\243.md.e8c07bfa.lean.js" new file mode 100644 index 000000000..af2541127 --- /dev/null +++ "b/assets/java_framework_Spring\351\205\215\347\275\256\345\222\214\346\235\241\344\273\266\345\214\226\347\273\204\344\273\266\345\212\240\350\275\275\346\263\250\350\247\243.md.e8c07bfa.lean.js" @@ -0,0 +1 @@ +import{_ as o,o as e,c as i,Q as n}from"./chunks/framework.dc1f4608.js";const g=JSON.parse('{"title":"Spring配置和条件化组件加载注解","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/Spring配置和条件化组件加载注解.md","filePath":"java/framework/Spring配置和条件化组件加载注解.md","lastUpdated":1708510102000}'),a={name:"java/framework/Spring配置和条件化组件加载注解.md"},t=n("",2),r=[t];function l(p,d,c,s,_,f){return e(),i("div",null,r)}const u=o(a,[["render",l]]);export{g as __pageData,u as default}; diff --git "a/assets/java_framework_dubbo\346\216\245\345\217\243\350\266\205\346\227\266\351\205\215\347\275\256.md.ba4117be.js" "b/assets/java_framework_dubbo\346\216\245\345\217\243\350\266\205\346\227\266\351\205\215\347\275\256.md.ba4117be.js" new file mode 100644 index 000000000..50d7a2e47 --- /dev/null +++ "b/assets/java_framework_dubbo\346\216\245\345\217\243\350\266\205\346\227\266\351\205\215\347\275\256.md.ba4117be.js" @@ -0,0 +1,119 @@ +import{_ as s,o as a,c as n,Q as o}from"./chunks/framework.dc1f4608.js";const b=JSON.parse('{"title":"dubbo接口超时配置","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/dubbo接口超时配置.md","filePath":"java/framework/dubbo接口超时配置.md","lastUpdated":1708510102000}'),l={name:"java/framework/dubbo接口超时配置.md"},p=o(`

dubbo接口超时配置

最近在跟第三方公司对接商城的账单数据,按对账单维度直接推送所有订单和明细,在测试大批量数据时出现了接口超时情况。原因是provider和consumer的超时时间都设置的比较短,因此要把这个接口超时时间调整更大一些。

dubbo服务超时时间设置及优先级

dubbo的服务超时时间有三个范围,分别是接口方法、接口类、全局。优先级接口方法>接口类>全局。而consumer服务和provider服务又分别可以配置这些超时时间。优先级为consumer>provider。因此完整的dubbo服务超时配置优先级为消费方method >提供方method>消费方reference>提供方service>消费方全局配置provider>提供方全局配置consumer

使用注解配置

provider:

java
@Service(timeout=60000)//alibaba包中的service,包含了spring framework的@Service功能和暴露服务功能,timeout单位ms
@Service(timeout=60000)//alibaba包中的service,包含了spring framework的@Service功能和暴露服务功能,timeout单位ms

cosumer:

java
@Reference(timeout=60000)//单位ms
@Reference(timeout=60000)//单位ms

使用xml配置

提供方

xml
<dubbo:provider timeout=“5000”/> 全局配置
+
+<dubbo:service timeout=“4000” …/> 接口类配置
+
+<dubbo:method timeout=“3000” …> 方法配置
<dubbo:provider timeout=“5000”/> 全局配置
+
+<dubbo:service timeout=“4000” …/> 接口类配置
+
+<dubbo:method timeout=“3000” …> 方法配置
xml
<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans-4.3.xsd        http://dubbo.apache.org/schema/dubbo        http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
+ 
+    <!-- 提供方应用信息,用于计算依赖关系 -->
+    <dubbo:application name="user-service-provider"  />
+ 
+    <!-- 使用zookeeper广播注册中心暴露服务地址 -->
+    <dubbo:registry address="zookeeper://192.168.0.126:2181" />
+ 
+    <!-- 用dubbo协议在20880端口暴露服务 -->
+    <dubbo:protocol name="dubbo" port="20880" />
+ 
+    <!-- 和本地bean一样实现服务 -->
+    <bean id="userService" class="com.storyxc.service.impl.UserServiceImpl" />
+ 
+    <!-- 声明需要暴露的服务接口 timeout 接口类中全部方法超时配置 优先级2 -->
+    <dubbo:service interface="com.storyxc.service.UserService" ref="userService" timeout="4000" >
+        <!-- 单个方法超时配置优先级最高1 -->
+        <dubbo:method name="queryAllUserAddress" timeout="3000"></dubbo:method>
+    </dubbo:service>
+ 
+    <!-- 服务提供者全局超时时间配置优先级最低 -->
+    <dubbo:provider timeout="5000"></dubbo:provider>
+ 
+</beans>
<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans-4.3.xsd        http://dubbo.apache.org/schema/dubbo        http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
+ 
+    <!-- 提供方应用信息,用于计算依赖关系 -->
+    <dubbo:application name="user-service-provider"  />
+ 
+    <!-- 使用zookeeper广播注册中心暴露服务地址 -->
+    <dubbo:registry address="zookeeper://192.168.0.126:2181" />
+ 
+    <!-- 用dubbo协议在20880端口暴露服务 -->
+    <dubbo:protocol name="dubbo" port="20880" />
+ 
+    <!-- 和本地bean一样实现服务 -->
+    <bean id="userService" class="com.storyxc.service.impl.UserServiceImpl" />
+ 
+    <!-- 声明需要暴露的服务接口 timeout 接口类中全部方法超时配置 优先级2 -->
+    <dubbo:service interface="com.storyxc.service.UserService" ref="userService" timeout="4000" >
+        <!-- 单个方法超时配置优先级最高1 -->
+        <dubbo:method name="queryAllUserAddress" timeout="3000"></dubbo:method>
+    </dubbo:service>
+ 
+    <!-- 服务提供者全局超时时间配置优先级最低 -->
+    <dubbo:provider timeout="5000"></dubbo:provider>
+ 
+</beans>

消费方

xml
<dubbo:consumer timeout=“4000” > 全局配置
+
+<dubbo:reference timeout=“3000” …> 接口类配置
+
+<dubbo:method timeout=“2000” …> 方法配置
<dubbo:consumer timeout=“4000” > 全局配置
+
+<dubbo:reference timeout=“3000” …> 接口类配置
+
+<dubbo:method timeout=“2000” …> 方法配置
xml
<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans-4.3.xsd        http://dubbo.apache.org/schema/dubbo        http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
+ 
+    <!-- 提供方应用信息,用于计算依赖关系 -->
+    <dubbo:application name="order-service-consumer"  />
+ 
+    <!-- 使用zookeeper广播注册中心暴露服务地址 -->
+    <dubbo:registry address="zookeeper://192.168.0.126:2181" />
+ 
+    <!-- 引用配置: 创建一个远程服务代理,一个引用可以指向多个注册中心 -->
+    <dubbo:reference id="userService" timeout="3000" interface="com.storyxc.service.UserService">
+        <dubbo:method name="queryAllUserAddress" timeout="2000"></dubbo:method>
+    </dubbo:reference>
+ 
+    <!-- orderService -->
+    <bean class="com.storyxc.service.impl.OrderServiceImpl">
+        <property name="userService" ref="userService"></property>
+    </bean>
+ 
+    <!-- 超时全局配置 -->
+    <dubbo:consumer timeout="4000"></dubbo:consumer>
+</beans>
<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans-4.3.xsd        http://dubbo.apache.org/schema/dubbo        http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
+ 
+    <!-- 提供方应用信息,用于计算依赖关系 -->
+    <dubbo:application name="order-service-consumer"  />
+ 
+    <!-- 使用zookeeper广播注册中心暴露服务地址 -->
+    <dubbo:registry address="zookeeper://192.168.0.126:2181" />
+ 
+    <!-- 引用配置: 创建一个远程服务代理,一个引用可以指向多个注册中心 -->
+    <dubbo:reference id="userService" timeout="3000" interface="com.storyxc.service.UserService">
+        <dubbo:method name="queryAllUserAddress" timeout="2000"></dubbo:method>
+    </dubbo:reference>
+ 
+    <!-- orderService -->
+    <bean class="com.storyxc.service.impl.OrderServiceImpl">
+        <property name="userService" ref="userService"></property>
+    </bean>
+ 
+    <!-- 超时全局配置 -->
+    <dubbo:consumer timeout="4000"></dubbo:consumer>
+</beans>
`,16),e=[p];function t(c,r,E,y,i,u){return a(),n("div",null,e)}const F=s(l,[["render",t]]);export{b as __pageData,F as default}; diff --git "a/assets/java_framework_dubbo\346\216\245\345\217\243\350\266\205\346\227\266\351\205\215\347\275\256.md.ba4117be.lean.js" "b/assets/java_framework_dubbo\346\216\245\345\217\243\350\266\205\346\227\266\351\205\215\347\275\256.md.ba4117be.lean.js" new file mode 100644 index 000000000..313590cb3 --- /dev/null +++ "b/assets/java_framework_dubbo\346\216\245\345\217\243\350\266\205\346\227\266\351\205\215\347\275\256.md.ba4117be.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as o}from"./chunks/framework.dc1f4608.js";const b=JSON.parse('{"title":"dubbo接口超时配置","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/dubbo接口超时配置.md","filePath":"java/framework/dubbo接口超时配置.md","lastUpdated":1708510102000}'),l={name:"java/framework/dubbo接口超时配置.md"},p=o("",16),e=[p];function t(c,r,E,y,i,u){return a(),n("div",null,e)}const F=s(l,[["render",t]]);export{b as __pageData,F as default}; diff --git "a/assets/java_framework_logback\350\207\252\345\256\232\344\271\211.md.9e87748a.js" "b/assets/java_framework_logback\350\207\252\345\256\232\344\271\211.md.9e87748a.js" new file mode 100644 index 000000000..ea323f354 --- /dev/null +++ "b/assets/java_framework_logback\350\207\252\345\256\232\344\271\211.md.9e87748a.js" @@ -0,0 +1,183 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"logback自定义","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/logback自定义.md","filePath":"java/framework/logback自定义.md","lastUpdated":1708510102000}'),p={name:"java/framework/logback自定义.md"},o=l(`

logback自定义

自定义日志动态输出内容

可以通过自定义全局拦截器通过MDC存储数据,在logback配置文件中直接通过%X{变量名} 读取变量。

java
/**
+ * @author xc
+ * @description 全局拦截器
+ * @date 2023/5/11 16:13
+ */
+@Component
+@Slf4j
+public class GlobalInterceptor implements HandlerInterceptor {
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        // 获取当前登录用户信息
+        LoginSession loginSession = LoginContext.getLoginSession();
+        if (loginSession != null) {
+            MDC.put("operator", loginSession.getUserName());
+        }else {
+            MDC.put("operator", "anonymous");
+        }
+        return true;
+    }
+}
/**
+ * @author xc
+ * @description 全局拦截器
+ * @date 2023/5/11 16:13
+ */
+@Component
+@Slf4j
+public class GlobalInterceptor implements HandlerInterceptor {
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        // 获取当前登录用户信息
+        LoginSession loginSession = LoginContext.getLoginSession();
+        if (loginSession != null) {
+            MDC.put("operator", loginSession.getUserName());
+        }else {
+            MDC.put("operator", "anonymous");
+        }
+        return true;
+    }
+}

logback-spring.xml

xml
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+    <!--日志文件输出格式-->
+    <!--%X{operator}可以直接获取当前线程MDC中的operator参数输出到日志中-->
+    <encoder>
+        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} %X{operator}  %msg%n</pattern> 
+        <charset>UTF-8</charset>
+    </encoder>
+    ...
+</appender>
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+    <!--日志文件输出格式-->
+    <!--%X{operator}可以直接获取当前线程MDC中的operator参数输出到日志中-->
+    <encoder>
+        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} %X{operator}  %msg%n</pattern> 
+        <charset>UTF-8</charset>
+    </encoder>
+    ...
+</appender>

自定义converter

通过自定义转换器来对日志输出的内容进行自定义处理

java
/**
+ * @author xc
+ * @description
+ * @date 2023/5/23 15:39
+ */
+public class OperatorConverter extends ClassicConverter {
+    @Override
+    public String convert(ILoggingEvent event) {
+        String operator = event.getMDCPropertyMap().get("operator");
+        return StrUtil.isNotBlank(operator) ? "- " + operator + " - " : "";
+    }
+}
/**
+ * @author xc
+ * @description
+ * @date 2023/5/23 15:39
+ */
+public class OperatorConverter extends ClassicConverter {
+    @Override
+    public String convert(ILoggingEvent event) {
+        String operator = event.getMDCPropertyMap().get("operator");
+        return StrUtil.isNotBlank(operator) ? "- " + operator + " - " : "";
+    }
+}

logback-spring.xml

xml
<configuration scan="true" scanPeriod="10 seconds">
+
+    <!--先声明一个转换器-->
+    <conversionRule conversionWord="operator" converterClass="com.storyxc.config.logback.converter.OperatorConverter" />
+    <!--在输出的pattern中直接使用,此时就不需要用%X{变量}了,直接%conversionWord即可-->
+    <!--此时如果MDC中没有operator变量时%operator会输出空串,否则会输出 "- 操作人姓名 - "-->
+    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+    <!--日志文件输出格式-->
+    <encoder>
+        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} %operator%msg%n</pattern> 
+        <charset>UTF-8</charset>
+    </encoder>
+    ...
+</appender>
+</configuration>
<configuration scan="true" scanPeriod="10 seconds">
+
+    <!--先声明一个转换器-->
+    <conversionRule conversionWord="operator" converterClass="com.storyxc.config.logback.converter.OperatorConverter" />
+    <!--在输出的pattern中直接使用,此时就不需要用%X{变量}了,直接%conversionWord即可-->
+    <!--此时如果MDC中没有operator变量时%operator会输出空串,否则会输出 "- 操作人姓名 - "-->
+    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+    <!--日志文件输出格式-->
+    <encoder>
+        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} %operator%msg%n</pattern> 
+        <charset>UTF-8</charset>
+    </encoder>
+    ...
+</appender>
+</configuration>

自定义filter

java
/**
+ * @author xc
+ * @description logback日志过滤器:开发环境下只打印本项目路径下的debug & debug以上级别的日志
+ * @date 2023/5/16 20:15
+ */
+public class LogBackDebugPackageFilter extends AbstractMatcherFilter<ILoggingEvent> {
+    private static final String PROJECT_BASE_PACKAGE = "com.storyxc";
+
+    @Override
+    public FilterReply decide(ILoggingEvent event) {
+        if (!isStarted()) {
+            return FilterReply.NEUTRAL;
+        }
+        Level level = event.getLevel();
+        if (level.isGreaterOrEqual(Level.DEBUG)) {
+            String loggerName = event.getLoggerName();
+            if (level.equals(Level.DEBUG)) {
+                if (loggerName != null && loggerName.startsWith(PROJECT_BASE_PACKAGE)) {
+                    return FilterReply.ACCEPT;
+                } else {
+                    return FilterReply.DENY;
+                }
+            }else {
+                return FilterReply.ACCEPT;
+            }
+        } else {
+            return FilterReply.NEUTRAL;
+        }
+    }
+}
/**
+ * @author xc
+ * @description logback日志过滤器:开发环境下只打印本项目路径下的debug & debug以上级别的日志
+ * @date 2023/5/16 20:15
+ */
+public class LogBackDebugPackageFilter extends AbstractMatcherFilter<ILoggingEvent> {
+    private static final String PROJECT_BASE_PACKAGE = "com.storyxc";
+
+    @Override
+    public FilterReply decide(ILoggingEvent event) {
+        if (!isStarted()) {
+            return FilterReply.NEUTRAL;
+        }
+        Level level = event.getLevel();
+        if (level.isGreaterOrEqual(Level.DEBUG)) {
+            String loggerName = event.getLoggerName();
+            if (level.equals(Level.DEBUG)) {
+                if (loggerName != null && loggerName.startsWith(PROJECT_BASE_PACKAGE)) {
+                    return FilterReply.ACCEPT;
+                } else {
+                    return FilterReply.DENY;
+                }
+            }else {
+                return FilterReply.ACCEPT;
+            }
+        } else {
+            return FilterReply.NEUTRAL;
+        }
+    }
+}

logback-spring.xml

xml
<!--输出到控制台-->
+<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+    <filter class="com.storyxc.config.logback.LogBackDebugPackageFilter">
+    </filter>
+    <encoder>
+        <Pattern>\${CONSOLE_LOG_PATTERN}</Pattern>
+        <!-- 设置字符集 -->
+        <charset>UTF-8</charset>
+    </encoder>
+</appender>
<!--输出到控制台-->
+<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+    <filter class="com.storyxc.config.logback.LogBackDebugPackageFilter">
+    </filter>
+    <encoder>
+        <Pattern>\${CONSOLE_LOG_PATTERN}</Pattern>
+        <!-- 设置字符集 -->
+        <charset>UTF-8</charset>
+    </encoder>
+</appender>
`,15),e=[o];function t(c,r,E,y,i,g){return n(),a("div",null,e)}const u=s(p,[["render",t]]);export{d as __pageData,u as default}; diff --git "a/assets/java_framework_logback\350\207\252\345\256\232\344\271\211.md.9e87748a.lean.js" "b/assets/java_framework_logback\350\207\252\345\256\232\344\271\211.md.9e87748a.lean.js" new file mode 100644 index 000000000..b2ea406d4 --- /dev/null +++ "b/assets/java_framework_logback\350\207\252\345\256\232\344\271\211.md.9e87748a.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"logback自定义","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/logback自定义.md","filePath":"java/framework/logback自定义.md","lastUpdated":1708510102000}'),p={name:"java/framework/logback自定义.md"},o=l("",15),e=[o];function t(c,r,E,y,i,g){return n(),a("div",null,e)}const u=s(p,[["render",t]]);export{d as __pageData,u as default}; diff --git "a/assets/java_framework_netty_websocket\345\256\236\347\216\260\345\215\263\346\227\266\351\200\232\350\256\257\345\212\237\350\203\275.md.955a4962.js" "b/assets/java_framework_netty_websocket\345\256\236\347\216\260\345\215\263\346\227\266\351\200\232\350\256\257\345\212\237\350\203\275.md.955a4962.js" new file mode 100644 index 000000000..b89601fde --- /dev/null +++ "b/assets/java_framework_netty_websocket\345\256\236\347\216\260\345\215\263\346\227\266\351\200\232\350\256\257\345\212\237\350\203\275.md.955a4962.js" @@ -0,0 +1,419 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const A=JSON.parse('{"title":"netty+websocket实现即时通讯功能","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/netty+websocket实现即时通讯功能.md","filePath":"java/framework/netty+websocket实现即时通讯功能.md","lastUpdated":1708510102000}'),p={name:"java/framework/netty+websocket实现即时通讯功能.md"},o=l(`

netty+websocket实现即时通讯功能

大致思路:

  • 后台应用启动后开启nettyserver
  • 前台登录后使用websocket连接netty
  • 登录时先向netty发送一条初始化消息,服务器将保存通道和当前用户的关系映射
  • 通讯双方都上线时,即可以开始聊天

后台:

  • 应用启动后,开启netty服务器
java
@Component
+public class ServerStarter implements ApplicationListener<ContextRefreshedEvent> {
+
+    @Override
+    public void onApplicationEvent(ContextRefreshedEvent event) {
+        if (event.getApplicationContext().getParent() == null ){
+            try {
+                new IMServer().start();
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+    }
@Component
+public class ServerStarter implements ApplicationListener<ContextRefreshedEvent> {
+
+    @Override
+    public void onApplicationEvent(ContextRefreshedEvent event) {
+        if (event.getApplicationContext().getParent() == null ){
+            try {
+                new IMServer().start();
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+    }
  • nettyserver
java
/**
+ * @author Xc
+ * @description
+ * @createdTime 2020/7/9 9:23
+ */
+public class IMServer {
+    Logger logger = LoggerFactory.getLogger(IMServer.class);
+
+    public void start() throws InterruptedException {
+        NioEventLoopGroup boss = new NioEventLoopGroup();
+        NioEventLoopGroup worker = new NioEventLoopGroup();
+        ServerBootstrap serverBootstrap = new ServerBootstrap();
+        serverBootstrap.group(boss,worker)
+                .channel(NioServerSocketChannel.class)
+                .localAddress(8000)
+		//自定义初始化器
+                .childHandler(new IMStoryInitializer());
+        ChannelFuture future = serverBootstrap.bind();
+        future.addListener(new ChannelFutureListener() {
+            @Override
+            public void operationComplete(ChannelFuture future) throws Exception {
+                logger.info("server start on 8000");
+            }
+        });
+    }
+}
/**
+ * @author Xc
+ * @description
+ * @createdTime 2020/7/9 9:23
+ */
+public class IMServer {
+    Logger logger = LoggerFactory.getLogger(IMServer.class);
+
+    public void start() throws InterruptedException {
+        NioEventLoopGroup boss = new NioEventLoopGroup();
+        NioEventLoopGroup worker = new NioEventLoopGroup();
+        ServerBootstrap serverBootstrap = new ServerBootstrap();
+        serverBootstrap.group(boss,worker)
+                .channel(NioServerSocketChannel.class)
+                .localAddress(8000)
+		//自定义初始化器
+                .childHandler(new IMStoryInitializer());
+        ChannelFuture future = serverBootstrap.bind();
+        future.addListener(new ChannelFutureListener() {
+            @Override
+            public void operationComplete(ChannelFuture future) throws Exception {
+                logger.info("server start on 8000");
+            }
+        });
+    }
+}
  • 初始化器
java
/**
+ * @author Xc
+ * @description
+ * @createdTime 2020/7/9 9:28
+ */
+public class IMStoryInitializer extends ChannelInitializer<SocketChannel> {
+    
+    @Override
+    protected void initChannel(SocketChannel ch) throws Exception {
+        ChannelPipeline pipeline = ch.pipeline();
+        //http编解码器
+        pipeline.addLast(new HttpServerCodec());
+        //以块写数据
+        pipeline.addLast(new ChunkedWriteHandler());
+        //聚合器
+        pipeline.addLast(new HttpObjectAggregator(64*1024));
+
+        //websocket
+        pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
+        //自定义handler
+        pipeline.addLast(new ChatHandler());
+        
+    }
+}
/**
+ * @author Xc
+ * @description
+ * @createdTime 2020/7/9 9:28
+ */
+public class IMStoryInitializer extends ChannelInitializer<SocketChannel> {
+    
+    @Override
+    protected void initChannel(SocketChannel ch) throws Exception {
+        ChannelPipeline pipeline = ch.pipeline();
+        //http编解码器
+        pipeline.addLast(new HttpServerCodec());
+        //以块写数据
+        pipeline.addLast(new ChunkedWriteHandler());
+        //聚合器
+        pipeline.addLast(new HttpObjectAggregator(64*1024));
+
+        //websocket
+        pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
+        //自定义handler
+        pipeline.addLast(new ChatHandler());
+        
+    }
+}
  • 自定义handler
java
/**
+ * @author Xc
+ * @description
+ * @createdTime 2020/7/9 9:34
+ */
+public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
+
+    /**
+     * 管理所有channel
+     */
+    public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
+
+    @Override
+    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
+        //客户端发过来的消息
+        String content = msg.text();
+        //当前通道
+        Channel channel = ctx.channel();
+
+        Message message = JSON.parseObject(content, Message.class);
+
+        String data = message.getMsg();
+
+        String fromUser = message.getFromUser();
+        //客户端建立连接后先发送一条init消息,后台保存这个通道和用户信息的映射
+        if (StringUtils.equals(message.getAction(), "init")) {
+            ChannelUserContext.put(fromUser,channel);
+        } else if (StringUtils.equals(message.getAction(),"chat")){
+            Channel toChannel = ChannelUserContext.get(message.getToUser());
+            if (toChannel != null) {
+                //消息接收方在线
+                toChannel.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(message.getFromUser() + " : " + message.getMsg())));
+            } else {
+                //接收方不在线 离线消息推送
+            }
+
+        }
+
+    }
+
+    @Override
+    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
+        channels.add(ctx.channel());
+    }
+
+    @Override
+    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
+        channels.remove(ctx.channel());
+    }
+}
/**
+ * @author Xc
+ * @description
+ * @createdTime 2020/7/9 9:34
+ */
+public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
+
+    /**
+     * 管理所有channel
+     */
+    public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
+
+    @Override
+    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
+        //客户端发过来的消息
+        String content = msg.text();
+        //当前通道
+        Channel channel = ctx.channel();
+
+        Message message = JSON.parseObject(content, Message.class);
+
+        String data = message.getMsg();
+
+        String fromUser = message.getFromUser();
+        //客户端建立连接后先发送一条init消息,后台保存这个通道和用户信息的映射
+        if (StringUtils.equals(message.getAction(), "init")) {
+            ChannelUserContext.put(fromUser,channel);
+        } else if (StringUtils.equals(message.getAction(),"chat")){
+            Channel toChannel = ChannelUserContext.get(message.getToUser());
+            if (toChannel != null) {
+                //消息接收方在线
+                toChannel.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(message.getFromUser() + " : " + message.getMsg())));
+            } else {
+                //接收方不在线 离线消息推送
+            }
+
+        }
+
+    }
+
+    @Override
+    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
+        channels.add(ctx.channel());
+    }
+
+    @Override
+    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
+        channels.remove(ctx.channel());
+    }
+}
  • 通道和用户的映射保存
java
/**
+ * @author Xc
+ * @description
+ * @createdTime 2020/7/9 9:46
+ */
+public class ChannelUserContext {
+
+    private static ConcurrentHashMap<String, Channel> userChannelMap;
+
+    static{
+        userChannelMap = new ConcurrentHashMap<>();
+    }
+
+    public static void put(String user, Channel channel){
+        userChannelMap.put(user,channel);
+    }
+
+    public static Channel get(String user) {
+        return userChannelMap.get(user);
+    }
+    
+}
/**
+ * @author Xc
+ * @description
+ * @createdTime 2020/7/9 9:46
+ */
+public class ChannelUserContext {
+
+    private static ConcurrentHashMap<String, Channel> userChannelMap;
+
+    static{
+        userChannelMap = new ConcurrentHashMap<>();
+    }
+
+    public static void put(String user, Channel channel){
+        userChannelMap.put(user,channel);
+    }
+
+    public static Channel get(String user) {
+        return userChannelMap.get(user);
+    }
+    
+}
  • message
java
/**
+ * @author Xc
+ * @description
+ * @createdTime 2020/7/9 9:38
+ */
+@Data
+public class Message implements Serializable {
+    private static final long serialVersionUID = 301234912340234L;
+    private String msg;
+    private String fromUser;
+    private String toUser;
+    private String action;
+}
/**
+ * @author Xc
+ * @description
+ * @createdTime 2020/7/9 9:38
+ */
+@Data
+public class Message implements Serializable {
+    private static final long serialVersionUID = 301234912340234L;
+    private String msg;
+    private String fromUser;
+    private String toUser;
+    private String action;
+}

前台页面1

html
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>WebSocket客户端</title>
+</head>
+<body>
+<script type="text/javascript">
+    var socket;
+
+    //如果浏览器支持WebSocket
+    if(window.WebSocket){
+        //参数就是与服务器连接的地址
+        socket = new WebSocket("ws://localhost:8000/ws");
+
+        //客户端收到服务器消息的时候就会执行这个回调方法
+        socket.onmessage = function (event) {
+			console.log(event);
+            var ta = document.getElementById("responseText");
+            ta.value = ta.value + "\\n"+event.data;
+        }
+
+        //连接建立的回调函数
+        socket.onopen = function(event){
+            var ta = document.getElementById("responseText");
+
+            ta.value = "连接开启";
+            var message = '{"action":"init","msg":"test","fromUser":"张三","toUser":"李四"}';
+            socket.send(message);
+        }
+
+        //连接断掉的回调函数
+        socket.onclose = function (event) {
+            var ta = document.getElementById("responseText");
+            ta.value = ta.value +"\\n"+"连接关闭";
+        }
+    }else{
+        alert("浏览器不支持WebSocket!");
+    }
+
+    //发送数据
+    function send(message){
+        if(!window.WebSocket){
+            return;
+        }
+
+        //当websocket状态打开
+        if(socket.readyState == WebSocket.OPEN){
+			message = '{"action":"chat","msg":"'+ message +'","fromUser":"张三","toUser":"李四"}';
+            socket.send(message);
+        }else{
+            alert("连接没有开启");
+        }
+    }
+</script>
+<form onsubmit="return false">
+    <textarea name = "message" style="width: 400px;height: 200px"></textarea>
+
+    <input type ="button" value="张三:发送数据" onclick="send(this.form.message.value);">
+
+    <h3>服务器输出:</h3>
+
+    <textarea id ="responseText" style="width: 400px;height: 300px;"></textarea>
+
+    <input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="清空数据">
+</form>
+</body>
+</html>
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>WebSocket客户端</title>
+</head>
+<body>
+<script type="text/javascript">
+    var socket;
+
+    //如果浏览器支持WebSocket
+    if(window.WebSocket){
+        //参数就是与服务器连接的地址
+        socket = new WebSocket("ws://localhost:8000/ws");
+
+        //客户端收到服务器消息的时候就会执行这个回调方法
+        socket.onmessage = function (event) {
+			console.log(event);
+            var ta = document.getElementById("responseText");
+            ta.value = ta.value + "\\n"+event.data;
+        }
+
+        //连接建立的回调函数
+        socket.onopen = function(event){
+            var ta = document.getElementById("responseText");
+
+            ta.value = "连接开启";
+            var message = '{"action":"init","msg":"test","fromUser":"张三","toUser":"李四"}';
+            socket.send(message);
+        }
+
+        //连接断掉的回调函数
+        socket.onclose = function (event) {
+            var ta = document.getElementById("responseText");
+            ta.value = ta.value +"\\n"+"连接关闭";
+        }
+    }else{
+        alert("浏览器不支持WebSocket!");
+    }
+
+    //发送数据
+    function send(message){
+        if(!window.WebSocket){
+            return;
+        }
+
+        //当websocket状态打开
+        if(socket.readyState == WebSocket.OPEN){
+			message = '{"action":"chat","msg":"'+ message +'","fromUser":"张三","toUser":"李四"}';
+            socket.send(message);
+        }else{
+            alert("连接没有开启");
+        }
+    }
+</script>
+<form onsubmit="return false">
+    <textarea name = "message" style="width: 400px;height: 200px"></textarea>
+
+    <input type ="button" value="张三:发送数据" onclick="send(this.form.message.value);">
+
+    <h3>服务器输出:</h3>
+
+    <textarea id ="responseText" style="width: 400px;height: 300px;"></textarea>
+
+    <input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="清空数据">
+</form>
+</body>
+</html>

页面二就是对这个页面稍微改一下

启动后台后打开两个页面,即可开始进行通讯 1.jpg

2.jpg3.jpg4.jpg

`,21),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const d=s(p,[["render",t]]);export{A as __pageData,d as default}; diff --git "a/assets/java_framework_netty_websocket\345\256\236\347\216\260\345\215\263\346\227\266\351\200\232\350\256\257\345\212\237\350\203\275.md.955a4962.lean.js" "b/assets/java_framework_netty_websocket\345\256\236\347\216\260\345\215\263\346\227\266\351\200\232\350\256\257\345\212\237\350\203\275.md.955a4962.lean.js" new file mode 100644 index 000000000..662cad538 --- /dev/null +++ "b/assets/java_framework_netty_websocket\345\256\236\347\216\260\345\215\263\346\227\266\351\200\232\350\256\257\345\212\237\350\203\275.md.955a4962.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const A=JSON.parse('{"title":"netty+websocket实现即时通讯功能","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/netty+websocket实现即时通讯功能.md","filePath":"java/framework/netty+websocket实现即时通讯功能.md","lastUpdated":1708510102000}'),p={name:"java/framework/netty+websocket实现即时通讯功能.md"},o=l("",21),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const d=s(p,[["render",t]]);export{A as __pageData,d as default}; diff --git "a/assets/java_framework_spring-session\345\256\236\347\216\260\351\233\206\347\276\244session\345\205\261\344\272\253.md.4d9b54de.js" "b/assets/java_framework_spring-session\345\256\236\347\216\260\351\233\206\347\276\244session\345\205\261\344\272\253.md.4d9b54de.js" new file mode 100644 index 000000000..fbef5e93c --- /dev/null +++ "b/assets/java_framework_spring-session\345\256\236\347\216\260\351\233\206\347\276\244session\345\205\261\344\272\253.md.4d9b54de.js" @@ -0,0 +1,103 @@ +import{_ as s,o as n,c as a,Q as p}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"spring-session实现集群session共享","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/spring-session实现集群session共享.md","filePath":"java/framework/spring-session实现集群session共享.md","lastUpdated":1708510102000}'),o={name:"java/framework/spring-session实现集群session共享.md"},l=p(`

spring-session实现集群session共享

问题场景

系统后台登录使用的是图片验证码,生成验证码接口会把验证码放在session中,登录时前端会携带账号密码的密文和验证码到后台,后台再用session中的验证码和前端带过来的进行校验。利用单机的session存储信息有个显而易见的问题,集群环境下这个功能是不可用的,因此要对这个过程进行改造,实现集群共享session,这里采用的是spring-session + redis的方案。

实现

spring-session和springboot的集成非常简单,如果项目使用的是springboot只需要引入依赖,再添加几个简单的配置类即可。如果是ssm那就要稍微麻烦点,要写xml的配置文件。本文是springboot的集成方案,详细内容可以下载spring-session官方sample查看。

依赖

xml
<dependencies>
+		<!--spring-session-->
+		<dependency>
+			<groupId>org.springframework.session</groupId>
+			<artifactId>spring-session-data-redis</artifactId>
+			<version>2.0.3.RELEASE</version>
+		</dependency>
+</dependencies>
<dependencies>
+		<!--spring-session-->
+		<dependency>
+			<groupId>org.springframework.session</groupId>
+			<artifactId>spring-session-data-redis</artifactId>
+			<version>2.0.3.RELEASE</version>
+		</dependency>
+</dependencies>

配置类

java
import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer;
+
+public class Initializer extends AbstractHttpSessionApplicationInitializer {
+
+	public Initializer() {
+		super(Config.class);
+	}
+
+}
import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer;
+
+public class Initializer extends AbstractHttpSessionApplicationInitializer {
+
+	public Initializer() {
+		super(Config.class);
+	}
+
+}
java
import org.springframework.context.annotation.Import;
+import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
+
+@Import(EmbeddedRedisConfig.class)
+@EnableRedisHttpSession
+public class Config {
+
+}
import org.springframework.context.annotation.Import;
+import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
+
+@Import(EmbeddedRedisConfig.class)
+@EnableRedisHttpSession
+public class Config {
+
+}
java
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+
+@Configuration
+@Slf4j
+public class EmbeddedRedisConfig {
+
+    @Value("\${session.share.redis.ip}")
+    private String ip;
+    @Value("\${session.share.redis.db}")
+    private String db;
+
+
+    @Bean
+    public RedisConnectionFactory redisConnectionFactory() {
+        log.info("session redis ip :{}, db:{}",ip,db);
+        RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();
+        redisConfig.setHostName(ip);
+        redisConfig.setPort(6379);
+        //指定database
+        redisConfig.setDatabase(Integer.parseInt(db));
+        return new JedisConnectionFactory(redisConfig);
+    }
+
+}
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+
+@Configuration
+@Slf4j
+public class EmbeddedRedisConfig {
+
+    @Value("\${session.share.redis.ip}")
+    private String ip;
+    @Value("\${session.share.redis.db}")
+    private String db;
+
+
+    @Bean
+    public RedisConnectionFactory redisConnectionFactory() {
+        log.info("session redis ip :{}, db:{}",ip,db);
+        RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();
+        redisConfig.setHostName(ip);
+        redisConfig.setPort(6379);
+        //指定database
+        redisConfig.setDatabase(Integer.parseInt(db));
+        return new JedisConnectionFactory(redisConfig);
+    }
+
+}

业务代码

这里就省略了,思路很简单,就是用session_id作为key存储登录验证码,从而实现不论登录请求落到集群的哪个服务器都能从redis中获取到正确的验证码。

总结

不得不感慨下spring生态的强大,毫不费力的就集成了一个功能。spring-session使用redis共享session的原理是通过RedisHttpSessionConfiguration配置类,生成一个过滤器SessionRepositoryFilter并且加入到过滤器链中,而且这个过滤器优先级是最高的,然后在SessionRepositoryFilter的doFilter方法中使用HttpServletRequest和HttpServletResponse的包装类把原始的request、response对象包装一下传递到其他过滤器中,在doFilter的final代码段里通过commitSession实现session到redis的持久化。简单的说就是spring-session使用redis存储的session替换了tomcat的httpsession实现。

`,15),e=[l];function t(r,c,i,E,y,d){return n(),a("div",null,e)}const f=s(o,[["render",t]]);export{u as __pageData,f as default}; diff --git "a/assets/java_framework_spring-session\345\256\236\347\216\260\351\233\206\347\276\244session\345\205\261\344\272\253.md.4d9b54de.lean.js" "b/assets/java_framework_spring-session\345\256\236\347\216\260\351\233\206\347\276\244session\345\205\261\344\272\253.md.4d9b54de.lean.js" new file mode 100644 index 000000000..e61416766 --- /dev/null +++ "b/assets/java_framework_spring-session\345\256\236\347\216\260\351\233\206\347\276\244session\345\205\261\344\272\253.md.4d9b54de.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as p}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"spring-session实现集群session共享","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/spring-session实现集群session共享.md","filePath":"java/framework/spring-session实现集群session共享.md","lastUpdated":1708510102000}'),o={name:"java/framework/spring-session实现集群session共享.md"},l=p("",15),e=[l];function t(r,c,i,E,y,d){return n(),a("div",null,e)}const f=s(o,[["render",t]]);export{u as __pageData,f as default}; diff --git "a/assets/java_framework_springcloud\344\274\230\351\233\205\344\270\213\347\272\277\346\234\215\345\212\241.md.6feadc2c.js" "b/assets/java_framework_springcloud\344\274\230\351\233\205\344\270\213\347\272\277\346\234\215\345\212\241.md.6feadc2c.js" new file mode 100644 index 000000000..b607748f5 --- /dev/null +++ "b/assets/java_framework_springcloud\344\274\230\351\233\205\344\270\213\347\272\277\346\234\215\345\212\241.md.6feadc2c.js" @@ -0,0 +1,241 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"springcloud优雅下线服务","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/springcloud优雅下线服务.md","filePath":"java/framework/springcloud优雅下线服务.md","lastUpdated":1708510102000}'),p={name:"java/framework/springcloud优雅下线服务.md"},o=l(`

springcloud优雅下线服务

问题场景

线上问题需要紧急修复部署,但是服务一直在跑,网关还会一直往服务中路由请求进行处理,如果直接停掉服务,客户端会出现业务中断的问题。因此需要在替换更新前先手动下线服务,这样用户请求不会被分发到下线的节点上,就可以直接进行更新而不影响用户体验。

实现

查询注册中心中服务的状态

bash
curl -GET -u eureka_username:eureka_password http://eureka_ip:eureka_port/eureka/apps/服务名称/实例ip:实例名称:实例port
curl -GET -u eureka_username:eureka_password http://eureka_ip:eureka_port/eureka/apps/服务名称/实例ip:实例名称:实例port

例如

bash
curl -GET -u admin:admin123 http://127.0.0.1:19991/eureka/apps/app-platform/127.0.0.1:app-platform-1.0.0-SNAPSHOT:8001
curl -GET -u admin:admin123 http://127.0.0.1:19991/eureka/apps/app-platform/127.0.0.1:app-platform-1.0.0-SNAPSHOT:8001

结果

xml
<instance>
+  <instanceId>127.0.0.1:app-platform-2.0.0-SNAPSHOT:8001</instanceId>
+  <hostName>127.0.0.1</hostName>
+  <app>app-platform</app>
+  <ipAddr>127.0.0.1</ipAddr>
+  <status>UP</status>  # 状态为在线
+  <overriddenstatus>UNKNOWN</overriddenstatus> # 重写状态为空
+  <port enabled="true">8001</port>
+  <securePort enabled="false">443</securePort>
+  <countryId>1</countryId>
+  <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
+    <name>MyOwn</name>
+  </dataCenterInfo>
+  <leaseInfo>
+    <renewalIntervalInSecs>10</renewalIntervalInSecs>
+    <durationInSecs>30</durationInSecs>
+    <registrationTimestamp>1636639654493</registrationTimestamp>
+    <lastRenewalTimestamp>1636961178498</lastRenewalTimestamp>
+    <evictionTimestamp>0</evictionTimestamp>
+    <serviceUpTimestamp>1636639654493</serviceUpTimestamp>
+  </leaseInfo>
+  <metadata>
+    <management.port>8001</management.port>
+    <nodeId>127.0.0.1_kkg</nodeId>
+  </metadata>
+  <homePageUrl>http://127.0.0.1:8001/</homePageUrl>
+  <statusPageUrl>http://127.0.0.1:8001/actuator/info</statusPageUrl>
+  <healthCheckUrl>http://127.0.0.1:8001/actuator/health</healthCheckUrl>
+  <vipAddress>app-platform</vipAddress>
+  <secureVipAddress>app-platform</secureVipAddress>
+  <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
+  <lastUpdatedTimestamp>1636639654493</lastUpdatedTimestamp>
+  <lastDirtyTimestamp>1636639654480</lastDirtyTimestamp>
+  <actionType>ADDED</actionType>
+</instance>
<instance>
+  <instanceId>127.0.0.1:app-platform-2.0.0-SNAPSHOT:8001</instanceId>
+  <hostName>127.0.0.1</hostName>
+  <app>app-platform</app>
+  <ipAddr>127.0.0.1</ipAddr>
+  <status>UP</status>  # 状态为在线
+  <overriddenstatus>UNKNOWN</overriddenstatus> # 重写状态为空
+  <port enabled="true">8001</port>
+  <securePort enabled="false">443</securePort>
+  <countryId>1</countryId>
+  <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
+    <name>MyOwn</name>
+  </dataCenterInfo>
+  <leaseInfo>
+    <renewalIntervalInSecs>10</renewalIntervalInSecs>
+    <durationInSecs>30</durationInSecs>
+    <registrationTimestamp>1636639654493</registrationTimestamp>
+    <lastRenewalTimestamp>1636961178498</lastRenewalTimestamp>
+    <evictionTimestamp>0</evictionTimestamp>
+    <serviceUpTimestamp>1636639654493</serviceUpTimestamp>
+  </leaseInfo>
+  <metadata>
+    <management.port>8001</management.port>
+    <nodeId>127.0.0.1_kkg</nodeId>
+  </metadata>
+  <homePageUrl>http://127.0.0.1:8001/</homePageUrl>
+  <statusPageUrl>http://127.0.0.1:8001/actuator/info</statusPageUrl>
+  <healthCheckUrl>http://127.0.0.1:8001/actuator/health</healthCheckUrl>
+  <vipAddress>app-platform</vipAddress>
+  <secureVipAddress>app-platform</secureVipAddress>
+  <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
+  <lastUpdatedTimestamp>1636639654493</lastUpdatedTimestamp>
+  <lastDirtyTimestamp>1636639654480</lastDirtyTimestamp>
+  <actionType>ADDED</actionType>
+</instance>

通知注册中心服务下线

bash
curl -i -X PUT -u eureka_username:eureka_password http://eureka_ip:eureka_port/eureka/apps/服务名称/实例ip:实例名称:实例port/status?value=OUT_OF_SERVICE
curl -i -X PUT -u eureka_username:eureka_password http://eureka_ip:eureka_port/eureka/apps/服务名称/实例ip:实例名称:实例port/status?value=OUT_OF_SERVICE

例如:

bash
curl -i -X PUT admin:admin123 http://127.0.0.1:19991/eureka/apps/app-platform/127.0.0.1:app-platform-1.0.0-SNAPSHOT:8001/status?value=OUT_OF_SERVICE
curl -i -X PUT admin:admin123 http://127.0.0.1:19991/eureka/apps/app-platform/127.0.0.1:app-platform-1.0.0-SNAPSHOT:8001/status?value=OUT_OF_SERVICE

结果

bash
HTTP/1.1 200 # 请求成功
+X-Content-Type-Options: nosniff
+X-XSS-Protection: 1; mode=block
+Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+Pragma: no-cache
+Expires: 0
+X-Frame-Options: DENY
+Content-Type: application/xml
+Content-Length: 0
+Date: Mon, 15 Nov 2021 07:30:32 GMT
HTTP/1.1 200 # 请求成功
+X-Content-Type-Options: nosniff
+X-XSS-Protection: 1; mode=block
+Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+Pragma: no-cache
+Expires: 0
+X-Frame-Options: DENY
+Content-Type: application/xml
+Content-Length: 0
+Date: Mon, 15 Nov 2021 07:30:32 GMT

再次查询服务状态

xml
<instance>
+  <instanceId>127.0.0.1:app-platform-2.0.0-SNAPSHOT:8001</instanceId>
+  <hostName>127.0.0.1</hostName>
+  <app>app-platform</app>
+  <ipAddr>127.0.0.1</ipAddr>
+  <status>OUT_OF_SERVICE</status> # 服务状态-已下线
+  <overriddenstatus>OUT_OF_SERVICE</overriddenstatus> # 重写状态为已下线
+  <port enabled="true">8001</port>
+  <securePort enabled="false">443</securePort>
+  <countryId>1</countryId>
+  <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
+    <name>MyOwn</name>
+  </dataCenterInfo>
+  <leaseInfo>
+    <renewalIntervalInSecs>10</renewalIntervalInSecs>
+    <durationInSecs>30</durationInSecs>
+    <registrationTimestamp>1636639654493</registrationTimestamp>
+    <lastRenewalTimestamp>1636961468691</lastRenewalTimestamp>
+    <evictionTimestamp>0</evictionTimestamp>
+    <serviceUpTimestamp>1636639654493</serviceUpTimestamp>
+  </leaseInfo>
+  <metadata>
+    <management.port>8001</management.port>
+    <nodeId>127.0.0.1_kkg</nodeId>
+  </metadata>
+  <homePageUrl>http://127.0.0.1:8001/</homePageUrl>
+  <statusPageUrl>http://127.0.0.1:8001/actuator/info</statusPageUrl>
+  <healthCheckUrl>http://127.0.0.1:8001/actuator/health</healthCheckUrl>
+  <vipAddress>app-platform</vipAddress>
+  <secureVipAddress>app-platform</secureVipAddress>
+  <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
+  <lastUpdatedTimestamp>1636961432181</lastUpdatedTimestamp>
+  <lastDirtyTimestamp>1636639654480</lastDirtyTimestamp>
+  <actionType>MODIFIED</actionType>
+</instance>
<instance>
+  <instanceId>127.0.0.1:app-platform-2.0.0-SNAPSHOT:8001</instanceId>
+  <hostName>127.0.0.1</hostName>
+  <app>app-platform</app>
+  <ipAddr>127.0.0.1</ipAddr>
+  <status>OUT_OF_SERVICE</status> # 服务状态-已下线
+  <overriddenstatus>OUT_OF_SERVICE</overriddenstatus> # 重写状态为已下线
+  <port enabled="true">8001</port>
+  <securePort enabled="false">443</securePort>
+  <countryId>1</countryId>
+  <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
+    <name>MyOwn</name>
+  </dataCenterInfo>
+  <leaseInfo>
+    <renewalIntervalInSecs>10</renewalIntervalInSecs>
+    <durationInSecs>30</durationInSecs>
+    <registrationTimestamp>1636639654493</registrationTimestamp>
+    <lastRenewalTimestamp>1636961468691</lastRenewalTimestamp>
+    <evictionTimestamp>0</evictionTimestamp>
+    <serviceUpTimestamp>1636639654493</serviceUpTimestamp>
+  </leaseInfo>
+  <metadata>
+    <management.port>8001</management.port>
+    <nodeId>127.0.0.1_kkg</nodeId>
+  </metadata>
+  <homePageUrl>http://127.0.0.1:8001/</homePageUrl>
+  <statusPageUrl>http://127.0.0.1:8001/actuator/info</statusPageUrl>
+  <healthCheckUrl>http://127.0.0.1:8001/actuator/health</healthCheckUrl>
+  <vipAddress>app-platform</vipAddress>
+  <secureVipAddress>app-platform</secureVipAddress>
+  <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
+  <lastUpdatedTimestamp>1636961432181</lastUpdatedTimestamp>
+  <lastDirtyTimestamp>1636639654480</lastDirtyTimestamp>
+  <actionType>MODIFIED</actionType>
+</instance>

可以看到此时,服务状态为已经下线,不过还是要等到网关服务不再路由请求到该服务时再停掉服务。

通知注册中心服务上线

bash
curl -i -X DELETE -u eureka_username:eureka_password http://eureka_ip:eureka_port/eureka/apps/服务名称/实例ip:实例名称:实例port/status
curl -i -X DELETE -u eureka_username:eureka_password http://eureka_ip:eureka_port/eureka/apps/服务名称/实例ip:实例名称:实例port/status

例如:

bash
curl -i -X DELETE -u admin:admin123 http://127.0.0.1:19991/eureka/apps/app-platform/127.0.0.1:app-platform-1.0.0-SNAPSHOT:8001/status
curl -i -X DELETE -u admin:admin123 http://127.0.0.1:19991/eureka/apps/app-platform/127.0.0.1:app-platform-1.0.0-SNAPSHOT:8001/status

结果

bash
HTTP/1.1 200 # 请求成功
+X-Content-Type-Options: nosniff
+X-XSS-Protection: 1; mode=block
+Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+Pragma: no-cache
+Expires: 0
+X-Frame-Options: DENY
+Content-Type: application/xml
+Content-Length: 0
+Date: Mon, 15 Nov 2021 07:37:11 GMT
HTTP/1.1 200 # 请求成功
+X-Content-Type-Options: nosniff
+X-XSS-Protection: 1; mode=block
+Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+Pragma: no-cache
+Expires: 0
+X-Frame-Options: DENY
+Content-Type: application/xml
+Content-Length: 0
+Date: Mon, 15 Nov 2021 07:37:11 GMT

再次查询服务状态

xml
<instance>
+  <instanceId>127.0.0.1:app-platform-2.0.0-SNAPSHOT:8001</instanceId>
+  <hostName>127.0.0.1</hostName>
+  <app>app-platform</app>
+  <ipAddr>127.0.0.1</ipAddr>
+  <status>UP</status> # 此时服务已上线 如果通知后立刻查询,状态可能会是unknown,隔一段时间再查询即可
+  <overriddenstatus>UNKNOWN</overriddenstatus>
+  <port enabled="true">8001</port>
+  <securePort enabled="false">443</securePort>
+  <countryId>1</countryId>
+  <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
+    <name>MyOwn</name>
+  </dataCenterInfo>
+  <leaseInfo>
+    <renewalIntervalInSecs>10</renewalIntervalInSecs>
+    <durationInSecs>30</durationInSecs>
+    <registrationTimestamp>1636961828894</registrationTimestamp>
+    <lastRenewalTimestamp>1636961861799</lastRenewalTimestamp>
+    <evictionTimestamp>0</evictionTimestamp>
+    <serviceUpTimestamp>1636639654493</serviceUpTimestamp>
+  </leaseInfo>
+  <metadata>
+    <management.port>8001</management.port>
+    <nodeId>127.0.0.1_kkg</nodeId>
+  </metadata>
+  <homePageUrl>http://127.0.0.1:8001/</homePageUrl>
+  <statusPageUrl>http://127.0.0.1:8001/actuator/info</statusPageUrl>
+  <healthCheckUrl>http://127.0.0.1:8001/actuator/health</healthCheckUrl>
+  <vipAddress>app-platform</vipAddress>
+  <secureVipAddress>app-platform</secureVipAddress>
+  <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
+  <lastUpdatedTimestamp>1636961828894</lastUpdatedTimestamp>
+  <lastDirtyTimestamp>1636961828889</lastDirtyTimestamp>
+  <actionType>ADDED</actionType>
+</instance>
<instance>
+  <instanceId>127.0.0.1:app-platform-2.0.0-SNAPSHOT:8001</instanceId>
+  <hostName>127.0.0.1</hostName>
+  <app>app-platform</app>
+  <ipAddr>127.0.0.1</ipAddr>
+  <status>UP</status> # 此时服务已上线 如果通知后立刻查询,状态可能会是unknown,隔一段时间再查询即可
+  <overriddenstatus>UNKNOWN</overriddenstatus>
+  <port enabled="true">8001</port>
+  <securePort enabled="false">443</securePort>
+  <countryId>1</countryId>
+  <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
+    <name>MyOwn</name>
+  </dataCenterInfo>
+  <leaseInfo>
+    <renewalIntervalInSecs>10</renewalIntervalInSecs>
+    <durationInSecs>30</durationInSecs>
+    <registrationTimestamp>1636961828894</registrationTimestamp>
+    <lastRenewalTimestamp>1636961861799</lastRenewalTimestamp>
+    <evictionTimestamp>0</evictionTimestamp>
+    <serviceUpTimestamp>1636639654493</serviceUpTimestamp>
+  </leaseInfo>
+  <metadata>
+    <management.port>8001</management.port>
+    <nodeId>127.0.0.1_kkg</nodeId>
+  </metadata>
+  <homePageUrl>http://127.0.0.1:8001/</homePageUrl>
+  <statusPageUrl>http://127.0.0.1:8001/actuator/info</statusPageUrl>
+  <healthCheckUrl>http://127.0.0.1:8001/actuator/health</healthCheckUrl>
+  <vipAddress>app-platform</vipAddress>
+  <secureVipAddress>app-platform</secureVipAddress>
+  <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
+  <lastUpdatedTimestamp>1636961828894</lastUpdatedTimestamp>
+  <lastDirtyTimestamp>1636961828889</lastDirtyTimestamp>
+  <actionType>ADDED</actionType>
+</instance>
`,27),t=[o];function e(c,r,E,y,i,g){return a(),n("div",null,t)}const u=s(p,[["render",e]]);export{m as __pageData,u as default}; diff --git "a/assets/java_framework_springcloud\344\274\230\351\233\205\344\270\213\347\272\277\346\234\215\345\212\241.md.6feadc2c.lean.js" "b/assets/java_framework_springcloud\344\274\230\351\233\205\344\270\213\347\272\277\346\234\215\345\212\241.md.6feadc2c.lean.js" new file mode 100644 index 000000000..4a9c69cc1 --- /dev/null +++ "b/assets/java_framework_springcloud\344\274\230\351\233\205\344\270\213\347\272\277\346\234\215\345\212\241.md.6feadc2c.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"springcloud优雅下线服务","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/springcloud优雅下线服务.md","filePath":"java/framework/springcloud优雅下线服务.md","lastUpdated":1708510102000}'),p={name:"java/framework/springcloud优雅下线服务.md"},o=l("",27),t=[o];function e(c,r,E,y,i,g){return a(),n("div",null,t)}const u=s(p,[["render",e]]);export{m as __pageData,u as default}; diff --git a/assets/java_framework_swagger_knife4j.md.31965f41.js b/assets/java_framework_swagger_knife4j.md.31965f41.js new file mode 100644 index 000000000..9ac49691e --- /dev/null +++ b/assets/java_framework_swagger_knife4j.md.31965f41.js @@ -0,0 +1,377 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"swagger+knife4j","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/swagger+knife4j.md","filePath":"java/framework/swagger+knife4j.md","lastUpdated":1708510102000}'),p={name:"java/framework/swagger+knife4j.md"},o=l(`

swagger+knife4j

swagger2风格

POM

xml
<dependencies>
+	<dependency>
+    <groupId>io.springfox</groupId>
+    <artifactId>springfox-boot-starter</artifactId>
+    <version>3.0.0</version>
+	</dependency>
+	<dependency>
+    <groupId>com.github.xiaoymin</groupId>
+    <artifactId>knife4j-spring-boot-starter</artifactId>
+    <version>3.0.2</version>
+	</dependency>
+</dependencies>
<dependencies>
+	<dependency>
+    <groupId>io.springfox</groupId>
+    <artifactId>springfox-boot-starter</artifactId>
+    <version>3.0.0</version>
+	</dependency>
+	<dependency>
+    <groupId>com.github.xiaoymin</groupId>
+    <artifactId>knife4j-spring-boot-starter</artifactId>
+    <version>3.0.2</version>
+	</dependency>
+</dependencies>

swagger config

java
// swagger config
+@Configuration
+@EnableOpenApi
+@EnableKnife4j
+public class SwaggerConfig {
+
+    @Bean
+    public Docket createRestApi() {
+        // 返回文档摘要信息
+        return new Docket(DocumentationType.OAS_30)
+                .apiInfo(apiInfo())
+                .enable(true)
+                .select()
+                // .apis(RequestHandlerSelectors.withMethodAnnotation(Operation.class))
+                .apis(RequestHandlerSelectors.basePackage("com.storyxc"))
+                .paths(PathSelectors.any())
+                .build();
+                .globalRequestParameters(getGlobalRequestParameters())
+                .globalResponses(HttpMethod.GET, getGlobalResponseMessage())
+                .globalResponses(HttpMethod.POST, getGlobalResponseMessage());
+    }
+
+    /**
+     * 生成接口信息,包括标题、联系人等
+     */
+    private ApiInfo apiInfo() {
+        return new ApiInfoBuilder()
+                .title("接口文档")
+                .description("如有雷同,纯属故意")
+                .contact(new Contact("storyxc", "", "storyxc@163.com"))
+                .version("1.0")
+                .build();
+    }
+
+    /**
+     * 封装全局通用参数
+     */
+    private List<RequestParameter> getGlobalRequestParameters() {
+        List<RequestParameter> parameters = new ArrayList<>();
+        parameters.add(new RequestParameterBuilder()
+                .name("token")
+                .description("token")
+                .required(true)
+                .in(ParameterType.QUERY)
+                .query(q -> q.model(m -> m.scalarModel(ScalarType.STRING)))
+                .required(false)
+                .build());
+        return parameters;
+    }
+
+    /**
+     * 封装通用响应信息
+     */
+    private List<Response> getGlobalResponseMessage() {
+        List<Response> responseList = new ArrayList<>();
+        responseList.add(new ResponseBuilder().code("404").description("未找到资源").build());
+        return responseList;
+    }
+}
// swagger config
+@Configuration
+@EnableOpenApi
+@EnableKnife4j
+public class SwaggerConfig {
+
+    @Bean
+    public Docket createRestApi() {
+        // 返回文档摘要信息
+        return new Docket(DocumentationType.OAS_30)
+                .apiInfo(apiInfo())
+                .enable(true)
+                .select()
+                // .apis(RequestHandlerSelectors.withMethodAnnotation(Operation.class))
+                .apis(RequestHandlerSelectors.basePackage("com.storyxc"))
+                .paths(PathSelectors.any())
+                .build();
+                .globalRequestParameters(getGlobalRequestParameters())
+                .globalResponses(HttpMethod.GET, getGlobalResponseMessage())
+                .globalResponses(HttpMethod.POST, getGlobalResponseMessage());
+    }
+
+    /**
+     * 生成接口信息,包括标题、联系人等
+     */
+    private ApiInfo apiInfo() {
+        return new ApiInfoBuilder()
+                .title("接口文档")
+                .description("如有雷同,纯属故意")
+                .contact(new Contact("storyxc", "", "storyxc@163.com"))
+                .version("1.0")
+                .build();
+    }
+
+    /**
+     * 封装全局通用参数
+     */
+    private List<RequestParameter> getGlobalRequestParameters() {
+        List<RequestParameter> parameters = new ArrayList<>();
+        parameters.add(new RequestParameterBuilder()
+                .name("token")
+                .description("token")
+                .required(true)
+                .in(ParameterType.QUERY)
+                .query(q -> q.model(m -> m.scalarModel(ScalarType.STRING)))
+                .required(false)
+                .build());
+        return parameters;
+    }
+
+    /**
+     * 封装通用响应信息
+     */
+    private List<Response> getGlobalResponseMessage() {
+        List<Response> responseList = new ArrayList<>();
+        responseList.add(new ResponseBuilder().code("404").description("未找到资源").build());
+        return responseList;
+    }
+}

WebMvcConfig

java
@Configuration
+@RequiredArgsConstructor
+public class WebMvcConfiguration implements WebMvcConfigurer {
+    private final TokenInterceptor tokenInterceptor;
+
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**").allowedOriginPatterns("*").allowedMethods("*").allowCredentials(true).maxAge(3600);
+    }
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        String[] ignore = {
+                "/doc.html**",
+                "/webjars/js/**",
+                "/webjars/css/**",
+                "/swagger-ui/**",
+                "/swagger-resources/**",
+                "/v3/**"
+        };
+        registry.addInterceptor(tokenInterceptor).addPathPatterns("/**").excludePathPatterns(ignore);
+    }
+}
@Configuration
+@RequiredArgsConstructor
+public class WebMvcConfiguration implements WebMvcConfigurer {
+    private final TokenInterceptor tokenInterceptor;
+
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**").allowedOriginPatterns("*").allowedMethods("*").allowCredentials(true).maxAge(3600);
+    }
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        String[] ignore = {
+                "/doc.html**",
+                "/webjars/js/**",
+                "/webjars/css/**",
+                "/swagger-ui/**",
+                "/swagger-resources/**",
+                "/v3/**"
+        };
+        registry.addInterceptor(tokenInterceptor).addPathPatterns("/**").excludePathPatterns(ignore);
+    }
+}

swagger2注解

  • @Api:定义接口分组名称
  • @ApiImplicitParam: 单个参数注释
  • @ApiImplicitParams:多个参数注释
  • @ApiModel:实体类定义
  • @ApiModelProperty:实体属性定义
  • @ApiOperation:接口定义
  • @ApiParam:参数注释
  • @ApiResponse:响应码
  • @ApiResponses:多个响应码

knife4j+OpenApi3.0风格

POM

xml
<dependencies>    
+	<dependency>
+     <groupId>com.github.xiaoymin</groupId>
+     <artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
+     <version>4.1.0</version>
+  </dependency>
+</dependencies>
<dependencies>    
+	<dependency>
+     <groupId>com.github.xiaoymin</groupId>
+     <artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
+     <version>4.1.0</version>
+  </dependency>
+</dependencies>

swagger config

java
@Configuration
+public class Swagger3Config {
+    @Bean
+    public GlobalOpenApiCustomizer orderGlobalOpenApiCustomizer() {
+        return openApi -> {
+            Info info = openApi.getInfo();
+            // 可以覆写信息
+            info.title("API");
+            info.version("1.0");
+        };
+    }
+
+    @Bean
+    public GroupedOpenApi opcenterApi() {
+        String[] packagedToMatch = {"com.storyxc.controller.wallpaper"};
+        return GroupedOpenApi.builder().group("wallpaper")
+                // token header
+                .addOperationCustomizer(((operation, handlerMethod) -> operation.addParametersItem(
+                        new HeaderParameter()
+                                .name("token")
+                                .description("token")
+                                .required(true)
+                                .schema(new io.swagger.v3.oas.models.media.StringSchema())
+                                .allowEmptyValue(false)
+                )))
+                .packagesToScan(packagedToMatch).build();
+    }
+
+    @Bean
+    public GroupedOpenApi adminApi() {
+        return GroupedOpenApi.builder()
+                .group("admin")
+                .packagesToScan("com.storyxc.controller.admin")
+                .build();
+    }
+
+    @Bean
+    public OpenAPI customOpenAPI() {
+        return new OpenAPI()
+                .info(new Info()
+                        .title("story")
+                        .version("1.0")
+                        .description("API服务")
+                        .termsOfService("https://storyxc.com")
+                        .license(new License().name("GPLv3")
+                                .url("https://www.gnu.org/licenses/gpl-3.0.html"))
+                        .contact(new Contact().name("storyxc").email("storyxc@163.cn").url("https://storyxc.com"))
+                        .summary("API服务")
+                );
+    }
+}
@Configuration
+public class Swagger3Config {
+    @Bean
+    public GlobalOpenApiCustomizer orderGlobalOpenApiCustomizer() {
+        return openApi -> {
+            Info info = openApi.getInfo();
+            // 可以覆写信息
+            info.title("API");
+            info.version("1.0");
+        };
+    }
+
+    @Bean
+    public GroupedOpenApi opcenterApi() {
+        String[] packagedToMatch = {"com.storyxc.controller.wallpaper"};
+        return GroupedOpenApi.builder().group("wallpaper")
+                // token header
+                .addOperationCustomizer(((operation, handlerMethod) -> operation.addParametersItem(
+                        new HeaderParameter()
+                                .name("token")
+                                .description("token")
+                                .required(true)
+                                .schema(new io.swagger.v3.oas.models.media.StringSchema())
+                                .allowEmptyValue(false)
+                )))
+                .packagesToScan(packagedToMatch).build();
+    }
+
+    @Bean
+    public GroupedOpenApi adminApi() {
+        return GroupedOpenApi.builder()
+                .group("admin")
+                .packagesToScan("com.storyxc.controller.admin")
+                .build();
+    }
+
+    @Bean
+    public OpenAPI customOpenAPI() {
+        return new OpenAPI()
+                .info(new Info()
+                        .title("story")
+                        .version("1.0")
+                        .description("API服务")
+                        .termsOfService("https://storyxc.com")
+                        .license(new License().name("GPLv3")
+                                .url("https://www.gnu.org/licenses/gpl-3.0.html"))
+                        .contact(new Contact().name("storyxc").email("storyxc@163.cn").url("https://storyxc.com"))
+                        .summary("API服务")
+                );
+    }
+}

WebMvcConfig

java
@Configuration
+@RequiredArgsConstructor
+public class WebMvcConfiguration implements WebMvcConfigurer {
+    private final TokenInterceptor tokenInterceptor;
+
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**")
+          .allowedOriginPatterns("*")
+          .allowedMethods("*")
+          .allowCredentials(true)
+          .maxAge(3600);
+    }
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        String[] ignore = {
+                "/doc.html**",
+                "/webjars/js/**",
+                "/webjars/css/**",
+                "/swagger-ui/**",
+                "/swagger-resources/**",
+                "/v3/**"
+        };
+        registry.addInterceptor(tokenInterceptor).addPathPatterns("/**").excludePathPatterns(ignore);
+    }
+}
@Configuration
+@RequiredArgsConstructor
+public class WebMvcConfiguration implements WebMvcConfigurer {
+    private final TokenInterceptor tokenInterceptor;
+
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**")
+          .allowedOriginPatterns("*")
+          .allowedMethods("*")
+          .allowCredentials(true)
+          .maxAge(3600);
+    }
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        String[] ignore = {
+                "/doc.html**",
+                "/webjars/js/**",
+                "/webjars/css/**",
+                "/swagger-ui/**",
+                "/swagger-resources/**",
+                "/v3/**"
+        };
+        registry.addInterceptor(tokenInterceptor).addPathPatterns("/**").excludePathPatterns(ignore);
+    }
+}

application.yaml

yml
springdoc:
+  api-docs:
+    enabled: true
+
+knife4j:
+  enable: true #增强模式
+  production: false #是否生产,生产会关闭knife4j 需要开启增强模式生效
+  setting:
+    swagger-model-name: 模型
+  documents:
+    - name: 项目文档
+      locations: classpath:doc/*
+      group: wallpaper
+    - name: sql
+      locations: classpath:sql/*
+      group: sql
springdoc:
+  api-docs:
+    enabled: true
+
+knife4j:
+  enable: true #增强模式
+  production: false #是否生产,生产会关闭knife4j 需要开启增强模式生效
+  setting:
+    swagger-model-name: 模型
+  documents:
+    - name: 项目文档
+      locations: classpath:doc/*
+      group: wallpaper
+    - name: sql
+      locations: classpath:sql/*
+      group: sql

OpenApi3注解

Swagger3注解说明
@Tag(name = “接口类描述”)Controller 类
@Operation(summary =“接口方法描述”)Controller 方法
@ParametersController 方法
@Parameter(description=“参数描述”)Controller 方法上 @Parameters 里Controller 方法的参数
@Parameter(hidden = true) 、@Operation(hidden = true)@Hidden排除或隐藏api
@SchemaDTO实体DTO实体属性
`,21),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const g=s(p,[["render",t]]);export{d as __pageData,g as default}; diff --git a/assets/java_framework_swagger_knife4j.md.31965f41.lean.js b/assets/java_framework_swagger_knife4j.md.31965f41.lean.js new file mode 100644 index 000000000..7f18dd2ed --- /dev/null +++ b/assets/java_framework_swagger_knife4j.md.31965f41.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"swagger+knife4j","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/swagger+knife4j.md","filePath":"java/framework/swagger+knife4j.md","lastUpdated":1708510102000}'),p={name:"java/framework/swagger+knife4j.md"},o=l("",21),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const g=s(p,[["render",t]]);export{d as __pageData,g as default}; diff --git "a/assets/java_framework_\344\275\277\347\224\250EasyExcel\345\257\274\345\207\272excel.md.6234940e.js" "b/assets/java_framework_\344\275\277\347\224\250EasyExcel\345\257\274\345\207\272excel.md.6234940e.js" new file mode 100644 index 000000000..2f99c4a52 --- /dev/null +++ "b/assets/java_framework_\344\275\277\347\224\250EasyExcel\345\257\274\345\207\272excel.md.6234940e.js" @@ -0,0 +1,75 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"使用EasyExcel导出excel","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/使用EasyExcel导出excel.md","filePath":"java/framework/使用EasyExcel导出excel.md","lastUpdated":1708510102000}'),p={name:"java/framework/使用EasyExcel导出excel.md"},o=l(`

使用EasyExcel导出excel

背景

本来一直在用easypoi,但是发现多sheet导出easyexcel的支持更好一点,遂切换

依赖

xml
<dependency>
+	<groupId>com.alibaba</groupId>
+	<artifactId>easyexcel</artifactId>
+	<version>2.2.7</version>
+</dependency>
<dependency>
+	<groupId>com.alibaba</groupId>
+	<artifactId>easyexcel</artifactId>
+	<version>2.2.7</version>
+</dependency>

导出多sheet

java
public void export(X param, HttpServletResponse response) {
+        String templatePath = "xxx"
+        OutputStream fos = null;
+        ExcelWriter excelWriter;
+        try {
+            fos = response.getOutputStream();
+            response.setContentType("application/vnd.ms-excel");
+            SimpleDateFormat yyyyMMdd = new SimpleDateFormat("yyyyMMddHHmmss");
+            String date = yyyyMMdd.format(new Date());
+            String fileName = "xxx" + date;
+            fileName = new String(fileName.getBytes(StandardCharsets.UTF_8), "ISO8859-1");
+            response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xls");
+
+            excelWriter = EasyExcel.write(fos).withTemplate(templatePath).build();
+            WriteSheet sheet0 = EasyExcel.writerSheet(0, "xxx").build();
+            WriteSheet sheet1 = EasyExcel.writerSheet(1, "xxx").build();
+
+            FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.FALSE).build();
+            //遍历:模版文件 {t.属性}  
+            excelWriter.fill(new FillWrapper("t", xxx), fillConfig, sheet0);//.fill(exportDate, writeSheet0); 填充单独属性
+            excelWriter.fill(new FillWrapper("t", xxx), fillConfig, sheet1);
+            excelWriter.finish();
+        } catch (Exception e) {
+            log.info("xxx");
+        } finally {
+            if (fos != null) {
+                try {
+                    fos.close();
+                } catch (IOException e) {
+                    log.error("xxxx");
+                }
+            }
+        }
+    }
public void export(X param, HttpServletResponse response) {
+        String templatePath = "xxx"
+        OutputStream fos = null;
+        ExcelWriter excelWriter;
+        try {
+            fos = response.getOutputStream();
+            response.setContentType("application/vnd.ms-excel");
+            SimpleDateFormat yyyyMMdd = new SimpleDateFormat("yyyyMMddHHmmss");
+            String date = yyyyMMdd.format(new Date());
+            String fileName = "xxx" + date;
+            fileName = new String(fileName.getBytes(StandardCharsets.UTF_8), "ISO8859-1");
+            response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xls");
+
+            excelWriter = EasyExcel.write(fos).withTemplate(templatePath).build();
+            WriteSheet sheet0 = EasyExcel.writerSheet(0, "xxx").build();
+            WriteSheet sheet1 = EasyExcel.writerSheet(1, "xxx").build();
+
+            FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.FALSE).build();
+            //遍历:模版文件 {t.属性}  
+            excelWriter.fill(new FillWrapper("t", xxx), fillConfig, sheet0);//.fill(exportDate, writeSheet0); 填充单独属性
+            excelWriter.fill(new FillWrapper("t", xxx), fillConfig, sheet1);
+            excelWriter.finish();
+        } catch (Exception e) {
+            log.info("xxx");
+        } finally {
+            if (fos != null) {
+                try {
+                    fos.close();
+                } catch (IOException e) {
+                    log.error("xxxx");
+                }
+            }
+        }
+    }
`,7),e=[o];function t(c,r,E,y,i,F){return a(),n("div",null,e)}const d=s(p,[["render",t]]);export{u as __pageData,d as default}; diff --git "a/assets/java_framework_\344\275\277\347\224\250EasyExcel\345\257\274\345\207\272excel.md.6234940e.lean.js" "b/assets/java_framework_\344\275\277\347\224\250EasyExcel\345\257\274\345\207\272excel.md.6234940e.lean.js" new file mode 100644 index 000000000..654ae466a --- /dev/null +++ "b/assets/java_framework_\344\275\277\347\224\250EasyExcel\345\257\274\345\207\272excel.md.6234940e.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"使用EasyExcel导出excel","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/使用EasyExcel导出excel.md","filePath":"java/framework/使用EasyExcel导出excel.md","lastUpdated":1708510102000}'),p={name:"java/framework/使用EasyExcel导出excel.md"},o=l("",7),e=[o];function t(c,r,E,y,i,F){return a(),n("div",null,e)}const d=s(p,[["render",t]]);export{u as __pageData,d as default}; diff --git "a/assets/java_framework_\344\275\277\347\224\250spring validation\350\277\233\350\241\214\345\217\202\346\225\260\346\240\241\351\252\214.md.eb10cc89.js" "b/assets/java_framework_\344\275\277\347\224\250spring validation\350\277\233\350\241\214\345\217\202\346\225\260\346\240\241\351\252\214.md.eb10cc89.js" new file mode 100644 index 000000000..d0a4170de --- /dev/null +++ "b/assets/java_framework_\344\275\277\347\224\250spring validation\350\277\233\350\241\214\345\217\202\346\225\260\346\240\241\351\252\214.md.eb10cc89.js" @@ -0,0 +1,177 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const F=JSON.parse('{"title":"使用spring validation进行参数校验","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/使用spring validation进行参数校验.md","filePath":"java/framework/使用spring validation进行参数校验.md","lastUpdated":1708510102000}'),p={name:"java/framework/使用spring validation进行参数校验.md"},o=l(`

使用spring validation进行参数校验

后台的参数校验如果全写在业务代码里会导致代码很臃肿,此时可以引入Spring Validation来进行参数校验,还可以自定义校验规则,自定义参数校验异常等

简单使用

  1. 项目引入spring-boot-starter-web依赖
xml
<dependency>
+    <groupId>org.hibernate</groupId>
+    <artifactId>hibernate-validator</artifactId>
+</dependency>
+<dependency>
+    <groupId>com.fasterxml.jackson.core</groupId>
+    <artifactId>jackson-databind</artifactId>
+</dependency>
<dependency>
+    <groupId>org.hibernate</groupId>
+    <artifactId>hibernate-validator</artifactId>
+</dependency>
+<dependency>
+    <groupId>com.fasterxml.jackson.core</groupId>
+    <artifactId>jackson-databind</artifactId>
+</dependency>
  1. 在需要校验的对象前,加上@Validated注解,在对象字段上使用具体校验规则的注解即可。如果是嵌套对象,则在嵌套的对象上使用@Valid注解表明需要嵌套校验

    java
    public String receive(@RequestBody @Validated StatementDto dto, BindingResult result) {
    +		if (result.hasErrors()) {
    +			throw new ParameterNotValidException(result);
    +		}
    +		return "test";
    +	}
    public String receive(@RequestBody @Validated StatementDto dto, BindingResult result) {
    +		if (result.hasErrors()) {
    +			throw new ParameterNotValidException(result);
    +		}
    +		return "test";
    +	}
    java
    public class QchjcCustomerStatementDto implements Serializable {
    +
    +	@NotEmpty(message = "不能为空")
    +	@Valid
    +	private List<OrderDto> order;
    +
    +	@DecimalMin(value = "0",message = "金额需大于等于0.00")
    +	private BigDecimal amount;
    +}
    public class QchjcCustomerStatementDto implements Serializable {
    +
    +	@NotEmpty(message = "不能为空")
    +	@Valid
    +	private List<OrderDto> order;
    +
    +	@DecimalMin(value = "0",message = "金额需大于等于0.00")
    +	private BigDecimal amount;
    +}

    TIP

    @Validated注解标记的参数会被spring进行校验,校验的信息会存放到其后的BindingResult中,如果有多个参数需要校验可以采用如下形式:(@Validated Person person, BindingResult fooBindingResult ,@Validated Bar bar, BindingResult barBindingResult);即一个校验类对应一个校验结果。

常用校验

  1. JSR303/JSR-349: JSR303是一项标准,只提供规范不提供实现,规定一些校验规范即校验注解,如@Null,@NotNull,@Pattern,位于javax.validation.constraints包下。JSR-349是其的升级版本,添加了一些新特性。

    1. @Null 被注释的元素必须为null

    2. @NotNull 被注释的元素必须不为null

    3. @AssertTrue 被注释的元素必须为true

    4. @AssertFalse 被注释的元素必须为false

    5. @Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值

    6. @Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值

    7. @DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定

      最小值

    8. @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定

      最大值

    9. @Size(max, min) 被注释的元素的大小必须在指定的范围内

    10. @Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内

    11. @Past 被注释的元素必须是一个过去的日期

    12. @Future 被注释的元素必须是一个将来的日期

    13. @Pattern(value) 被注释的元素必须符合指定的正则表达式

  2. hibernate validation:hibernate validation是对这个规范的实现,并增加了一些其他校验注解,如@Email,@Length,@Range等等

    1. @Email 被注释的元素必须是电子邮箱地址
    2. @Length 被注释的字符串的大小必须在指定的范围内
    3. @NotEmpty 被注释的字符串的必须非空
    4. @Range 被注释的元素必须在合适的范围内
  3. spring validation:spring validation对hibernate validation进行了二次封装,在springmvc模块中添加了自动校验,并将校验信息封装进了特定的类中

统一异常处理

如果方法参数中不声明BindingResult,那么spring校验不通过后会直接抛出BindException,体验很不好。因此我们可以进行自定义异常处理。

  • 自定义异常
java
public class ParameterNotValidException extends RuntimeException{
+    private static final long serialVersionUID = 1L;
+    private BindingResult bindingResult;
+
+    public ParameterNotValidException(BindingResult bindingResult) {
+        this.bindingResult = bindingResult;
+    }
+
+    public BindingResult getBindingResult() {
+        return bindingResult;
+    }
+
+    @Override
+    public String getMessage() {
+        return "参数校验不通过";
+    }
+}
public class ParameterNotValidException extends RuntimeException{
+    private static final long serialVersionUID = 1L;
+    private BindingResult bindingResult;
+
+    public ParameterNotValidException(BindingResult bindingResult) {
+        this.bindingResult = bindingResult;
+    }
+
+    public BindingResult getBindingResult() {
+        return bindingResult;
+    }
+
+    @Override
+    public String getMessage() {
+        return "参数校验不通过";
+    }
+}
  • 统一异常处理
java
@ControllerAdvice
+public class ExceptionHandler {
+    /**
+     * 方法参数校验异常处理
+     */
+    @ExceptionHandler(ParameterNotValidException.class)
+    @ResponseBody
+    public O handleMethodArgumentNotValidException(ParameterNotValidException e) {
+        BindingResult bindingResult = e.getBindingResult();
+        FieldError fieldError = bindingResult.getFieldError();
+        String field = fieldError.getField();
+        String defaultMessage = fieldError.getDefaultMessage();
+        O o = new O();
+        o.setResultCode(500);
+        o.setSuccess(false);
+        o.setResultMessage(field + ":" + defaultMessage);
+        return o;
+    }
+}
@ControllerAdvice
+public class ExceptionHandler {
+    /**
+     * 方法参数校验异常处理
+     */
+    @ExceptionHandler(ParameterNotValidException.class)
+    @ResponseBody
+    public O handleMethodArgumentNotValidException(ParameterNotValidException e) {
+        BindingResult bindingResult = e.getBindingResult();
+        FieldError fieldError = bindingResult.getFieldError();
+        String field = fieldError.getField();
+        String defaultMessage = fieldError.getDefaultMessage();
+        O o = new O();
+        o.setResultCode(500);
+        o.setSuccess(false);
+        o.setResultMessage(field + ":" + defaultMessage);
+        return o;
+    }
+}

自定义校验规则

例如,我们需要新增一个校验规则,数字类型必须大于0

  • 新建校验注解

    java
    @Target(ElementType.FIELD)
    +@Retention(RetentionPolicy.RUNTIME)
    +@Constraint(validatedBy = GreaterThanZeroValidator.class)//标注校验由哪些类执行,可以是多个
    +public @interface GreaterThanZero {
    +    String message();
    +
    +    Class<?>[] groups() default {};//在不同接口中参数可能校验的规则不同,可以创建不同的组,并在校验规则标记组,在controller层也标记组,这样就可以不同接口实现不同校验规则
    +
    +    Class<?>[] payload() default {};
    +}
    @Target(ElementType.FIELD)
    +@Retention(RetentionPolicy.RUNTIME)
    +@Constraint(validatedBy = GreaterThanZeroValidator.class)//标注校验由哪些类执行,可以是多个
    +public @interface GreaterThanZero {
    +    String message();
    +
    +    Class<?>[] groups() default {};//在不同接口中参数可能校验的规则不同,可以创建不同的组,并在校验规则标记组,在controller层也标记组,这样就可以不同接口实现不同校验规则
    +
    +    Class<?>[] payload() default {};
    +}

    一个标注(annotation) 是通过@interface关键字来定义的. 这个标注中的属性是声明成类似方法的样式的. 根据Bean Validation API 规范的要求

    • message属性, 这个属性被用来定义默认得消息模版, 当这个约束条件被验证失败的时候,通过此属性来输出错误信息.

    groups 属性, 用于指定这个约束条件属于哪(些)个校验组.这个的默认值必须是Class<?>类型到空到数组.

    • payload 属性, Bean Validation API 的使用者可以通过此属性来给约束条件指定严重级别. 这个属性并不被API自身所使用.

      java
      public class Severity {
      +    public static class Info extends Payload {};
      +    public static class Error extends Payload {};
      +}
      +
      +public class ContactDetails {
      +    @NotNull(message="Name is mandatory", payload=Severity.Error.class)
      +    private String name;
      +
      +    @NotNull(message="Phone number not specified, but not mandatory", payload=Severity.Info.class)
      +    private String phoneNumber;
      +
      +    // ...
      +}
      public class Severity {
      +    public static class Info extends Payload {};
      +    public static class Error extends Payload {};
      +}
      +
      +public class ContactDetails {
      +    @NotNull(message="Name is mandatory", payload=Severity.Error.class)
      +    private String name;
      +
      +    @NotNull(message="Phone number not specified, but not mandatory", payload=Severity.Info.class)
      +    private String phoneNumber;
      +
      +    // ...
      +}

      这样, 在校验完一个ContactDetails 的示例之后, 你就可以通过调用ConstraintViolation.getConstraintDescriptor().getPayload()来得到之前指定到错误级别了,并且可以根据这个信息来决定接下来到行为.

  • 校验规则类

    java
    public class GreaterThanZeroValidator implements ConstraintValidator<GreaterThanZero, Object> {
    +    @Override
    +    public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
    +        if (o == null) {
    +            return false;
    +        }
    +        if (o instanceof Number) {
    +            return ((Number) o).intValue() > 0;
    +        }
    +
    +        return false;
    +    }
    +}
    public class GreaterThanZeroValidator implements ConstraintValidator<GreaterThanZero, Object> {
    +    @Override
    +    public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
    +        if (o == null) {
    +            return false;
    +        }
    +        if (o instanceof Number) {
    +            return ((Number) o).intValue() > 0;
    +        }
    +
    +        return false;
    +    }
    +}
    • ConstraintValidator定义了两个泛型参数, 第一个是这个校验器所服务到标注类型, 第二个这个校验器所支持到被校验元素到类型.如果一个约束标注支持多种类型到被校验元素的话, 那么需要为每个所支持的类型定义一个ConstraintValidator,并且注册到约束标注中.这个验证器的实现就很平常了
    • initialize() 方法传进来一个所要验证的标注类型的实例
    • isValid()是实现真正的校验逻辑的地方
`,17),e=[o];function t(c,r,E,y,i,d){return a(),n("div",null,e)}const g=s(p,[["render",t]]);export{F as __pageData,g as default}; diff --git "a/assets/java_framework_\344\275\277\347\224\250spring validation\350\277\233\350\241\214\345\217\202\346\225\260\346\240\241\351\252\214.md.eb10cc89.lean.js" "b/assets/java_framework_\344\275\277\347\224\250spring validation\350\277\233\350\241\214\345\217\202\346\225\260\346\240\241\351\252\214.md.eb10cc89.lean.js" new file mode 100644 index 000000000..cd4ddf6f3 --- /dev/null +++ "b/assets/java_framework_\344\275\277\347\224\250spring validation\350\277\233\350\241\214\345\217\202\346\225\260\346\240\241\351\252\214.md.eb10cc89.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const F=JSON.parse('{"title":"使用spring validation进行参数校验","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/使用spring validation进行参数校验.md","filePath":"java/framework/使用spring validation进行参数校验.md","lastUpdated":1708510102000}'),p={name:"java/framework/使用spring validation进行参数校验.md"},o=l("",17),e=[o];function t(c,r,E,y,i,d){return a(),n("div",null,e)}const g=s(p,[["render",t]]);export{F as __pageData,g as default}; diff --git "a/assets/java_framework_\345\210\206\345\270\203\345\274\217\345\256\232\346\227\266\344\273\273\345\212\241\350\247\243\345\206\263\346\226\271\346\241\210xxl-job.md.5669afc6.js" "b/assets/java_framework_\345\210\206\345\270\203\345\274\217\345\256\232\346\227\266\344\273\273\345\212\241\350\247\243\345\206\263\346\226\271\346\241\210xxl-job.md.5669afc6.js" new file mode 100644 index 000000000..f886f3061 --- /dev/null +++ "b/assets/java_framework_\345\210\206\345\270\203\345\274\217\345\256\232\346\227\266\344\273\273\345\212\241\350\247\243\345\206\263\346\226\271\346\241\210xxl-job.md.5669afc6.js" @@ -0,0 +1 @@ +import{_ as l,o as a,c as i,Q as o}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"分布式定时任务解决方案xxl-job","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/分布式定时任务解决方案xxl-job.md","filePath":"java/framework/分布式定时任务解决方案xxl-job.md","lastUpdated":1708510102000}'),e={name:"java/framework/分布式定时任务解决方案xxl-job.md"},t=o('

分布式定时任务解决方案xxl-job

背景

今天领导让我了解下分布式定时任务的内容,对项目中目前的定时任务改造一下,公司目前项目中封装的定时任务注解是基于spring的scheduler的,单机环境下没问题,但是为了服务的高可用生产都是集群部署的,会导致任务多次运行的问题,上家公司用的ssm,定时任务选型是Quartz,生产上采用的quartz的集群部署,quartz集群是不会出现任务重复执行的,原理跟下文讲到的xxl-job一样都是通过数据库表来加锁实现

xxl-job

Xxl-job官网:https://www.xuxueli.com/xxl-job/

XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。

选择xxl-job的原因有以下几点:

  • 调度和任务解耦,有单独的调度中心及控制台
  • 代码易扩展
  • 实现的功能比较全面

使用流程:

  • 执行doc中的sql脚本,建相关的库表,具体表的作用可阅读帮助文档

  • 修改xxl-job-admin配置文件后可以直接启动admin应用

  • 修改执行器应用中配置文件,注意匹配admin的相关信息

  • copy一份示例执行器的代码 修改应用端口和执行端口

  • 启动两个示例执行器应用(模拟集群)

  • 打开控制台可以看到已经注册上了两个节点 Snipaste_20210318_201851.png

  • 在控制台任务管理中修改任务的相关信息,这里只试个最简单的,更多高级配置请看使用文档

Snipaste_20210318_202127.png

Snipaste_20210318_202218.png

Snipaste_20210318_202226.png

可以在控制台的调度记录中看到,集群模式下任务也只执行了一次.

除了cron任务,xxl-job中还支持周期性的任务,shell任务等.路由策略还支持轮询,一致性哈希,故障转移等,在admin中配置好java.mail信息,在控制配置任务时添加告警邮件,在任务调度出现异常时还会邮件告警

集成到微服务系统的思路:

  • 数据库建表
  • 新建一个job-admin服务将xxl-job-admin迁移,修改配置信息
  • 在需要定时任务的业务服务中引入xxl-core依赖
  • 在任务类中实现业务逻辑,任务方法上加上@XxlJob(value = "xxHandler")
  • admin服务启动后启动具体的业务服务
  • 在调度中心新建任务,配置相关信息,注意JobHandler的值为@XxlJob注解的Value
',17),p=[t];function r(x,s,n,_,c,m){return a(),i("div",null,p)}const h=l(e,[["render",r]]);export{d as __pageData,h as default}; diff --git "a/assets/java_framework_\345\210\206\345\270\203\345\274\217\345\256\232\346\227\266\344\273\273\345\212\241\350\247\243\345\206\263\346\226\271\346\241\210xxl-job.md.5669afc6.lean.js" "b/assets/java_framework_\345\210\206\345\270\203\345\274\217\345\256\232\346\227\266\344\273\273\345\212\241\350\247\243\345\206\263\346\226\271\346\241\210xxl-job.md.5669afc6.lean.js" new file mode 100644 index 000000000..d5035c759 --- /dev/null +++ "b/assets/java_framework_\345\210\206\345\270\203\345\274\217\345\256\232\346\227\266\344\273\273\345\212\241\350\247\243\345\206\263\346\226\271\346\241\210xxl-job.md.5669afc6.lean.js" @@ -0,0 +1 @@ +import{_ as l,o as a,c as i,Q as o}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"分布式定时任务解决方案xxl-job","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/分布式定时任务解决方案xxl-job.md","filePath":"java/framework/分布式定时任务解决方案xxl-job.md","lastUpdated":1708510102000}'),e={name:"java/framework/分布式定时任务解决方案xxl-job.md"},t=o("",17),p=[t];function r(x,s,n,_,c,m){return a(),i("div",null,p)}const h=l(e,[["render",r]]);export{d as __pageData,h as default}; diff --git "a/assets/java_framework_\345\220\216\347\253\257\345\205\201\350\256\270\350\267\250\345\237\237\351\205\215\347\275\256.md.dcf47fae.js" "b/assets/java_framework_\345\220\216\347\253\257\345\205\201\350\256\270\350\267\250\345\237\237\351\205\215\347\275\256.md.dcf47fae.js" new file mode 100644 index 000000000..44b239f55 --- /dev/null +++ "b/assets/java_framework_\345\220\216\347\253\257\345\205\201\350\256\270\350\267\250\345\237\237\351\205\215\347\275\256.md.dcf47fae.js" @@ -0,0 +1,55 @@ +import{_ as s,o as n,c as a,Q as o}from"./chunks/framework.dc1f4608.js";const C=JSON.parse('{"title":"后端允许跨域配置","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/后端允许跨域配置.md","filePath":"java/framework/后端允许跨域配置.md","lastUpdated":1708510102000}'),l={name:"java/framework/后端允许跨域配置.md"},p=o(`

后端允许跨域配置

过滤器方案

java
@Configuration
+public class CorsFilterConfig {
+    @Bean
+    public CorsFilter corsFilter() {
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        CorsConfiguration corsConfiguration = new CorsConfiguration();
+        corsConfiguration.setAllowCredentials(true);
+        corsConfiguration.addAllowedOriginPattern("*");
+        corsConfiguration.addAllowedHeader("*");
+        corsConfiguration.addAllowedMethod("*");
+        corsConfiguration.setMaxAge(10000L);
+        source.registerCorsConfiguration("/**", corsConfiguration);
+        return new CorsFilter(source);
+    }
+
+}
@Configuration
+public class CorsFilterConfig {
+    @Bean
+    public CorsFilter corsFilter() {
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        CorsConfiguration corsConfiguration = new CorsConfiguration();
+        corsConfiguration.setAllowCredentials(true);
+        corsConfiguration.addAllowedOriginPattern("*");
+        corsConfiguration.addAllowedHeader("*");
+        corsConfiguration.addAllowedMethod("*");
+        corsConfiguration.setMaxAge(10000L);
+        source.registerCorsConfiguration("/**", corsConfiguration);
+        return new CorsFilter(source);
+    }
+
+}

Spring拦截器方案

java
@Configuration
+@RequiredArgsConstructor
+public class WebMvcConfiguration implements WebMvcConfigurer {
+  @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**")
+                .allowedOriginPatterns("*")
+                .allowedMethods("*")
+                .allowCredentials(true)
+                .allowedHeaders("*")
+                .maxAge(3600);
+    } 
+}
@Configuration
+@RequiredArgsConstructor
+public class WebMvcConfiguration implements WebMvcConfigurer {
+  @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**")
+                .allowedOriginPatterns("*")
+                .allowedMethods("*")
+                .allowCredentials(true)
+                .allowedHeaders("*")
+                .maxAge(3600);
+    } 
+}
`,5),e=[p];function r(t,c,E,y,i,u){return n(),a("div",null,e)}const F=s(l,[["render",r]]);export{C as __pageData,F as default}; diff --git "a/assets/java_framework_\345\220\216\347\253\257\345\205\201\350\256\270\350\267\250\345\237\237\351\205\215\347\275\256.md.dcf47fae.lean.js" "b/assets/java_framework_\345\220\216\347\253\257\345\205\201\350\256\270\350\267\250\345\237\237\351\205\215\347\275\256.md.dcf47fae.lean.js" new file mode 100644 index 000000000..1f8d04e62 --- /dev/null +++ "b/assets/java_framework_\345\220\216\347\253\257\345\205\201\350\256\270\350\267\250\345\237\237\351\205\215\347\275\256.md.dcf47fae.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as o}from"./chunks/framework.dc1f4608.js";const C=JSON.parse('{"title":"后端允许跨域配置","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/后端允许跨域配置.md","filePath":"java/framework/后端允许跨域配置.md","lastUpdated":1708510102000}'),l={name:"java/framework/后端允许跨域配置.md"},p=o("",5),e=[p];function r(t,c,E,y,i,u){return n(),a("div",null,e)}const F=s(l,[["render",r]]);export{C as __pageData,F as default}; diff --git "a/assets/java_framework_\350\207\252\345\256\232\344\271\211MybatisPlusGenerator.md.cb2de2f5.js" "b/assets/java_framework_\350\207\252\345\256\232\344\271\211MybatisPlusGenerator.md.cb2de2f5.js" new file mode 100644 index 000000000..caddfe386 --- /dev/null +++ "b/assets/java_framework_\350\207\252\345\256\232\344\271\211MybatisPlusGenerator.md.cb2de2f5.js" @@ -0,0 +1,813 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const F=JSON.parse('{"title":"自定义MybatisPlusGenerator","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/自定义MybatisPlusGenerator.md","filePath":"java/framework/自定义MybatisPlusGenerator.md","lastUpdated":1708510102000}'),p={name:"java/framework/自定义MybatisPlusGenerator.md"},o=l(`

自定义MybatisPlusGenerator

入口类

java
import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.core.toolkit.StringPool;
+import com.baomidou.mybatisplus.generator.AutoGenerator;
+import com.baomidou.mybatisplus.generator.InjectionConfig;
+import com.baomidou.mybatisplus.generator.config.*;
+import com.baomidou.mybatisplus.generator.config.po.TableInfo;
+import com.baomidou.mybatisplus.generator.config.rules.DateType;
+import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
+import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author xc
+ * @description
+ * @date 2023/5/19 09:20
+ */
+public class MybatisPlusCodeGenerator {
+
+    private static final String projectPath = System.getProperty("user.dir");
+
+    public static void main(String[] args) {
+        //====================配置变量区域=====================//
+        String author = "storyxc";// 生成文件的作者,可以不填
+        String rootPackage = "com.storyxc";// 生成的entity、controller、service等包所在的公共上一级包路径全限定名
+        String modelModuleName = "storyxc-model";
+        String serviceModuleName = "storyxc-web";
+        String controllerModuleName = "storyxc-web";
+        // 数据库配置
+        String url="jdbc:mysql://127.0.0.1/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false";
+        String driverClassName = "com.mysql.cj.jdbc.Driver";// 或者com.mysql.cj.jdbc.Driver
+        String username = "root";
+        String password = "root";
+
+        String[] tableNames = new String[]{""};
+        String pkgName = "";
+        //====================配置变量区域=====================//
+        String[] tablePrefix = new String[]{""};
+        // 代码生成器
+        AutoGenerator generator = new AutoGenerator();
+        // 全局配置
+        GlobalConfig globalConfig = new GlobalConfig();
+        globalConfig.setOutputDir(projectPath + "/" + modelModuleName + "/src/main/java");// 生成文件的输出目录
+        globalConfig.setFileOverride(false);// 是否覆盖已有文件,默认false
+        globalConfig.setOpen(false);// 是否打开输出目录
+        globalConfig.setAuthor(author);
+        globalConfig.setServiceName("%sService");// 去掉service接口的首字母I
+        globalConfig.setBaseResultMap(true);// 开启 BaseResultMap
+        globalConfig.setDateType(DateType.ONLY_DATE);// 只使用 java.util.date代替
+        globalConfig.setIdType(IdType.ASSIGN_ID);// 分配ID (主键类型为number或string)
+        generator.setGlobalConfig(globalConfig);
+
+        // 数据源配置
+        DataSourceConfig dataSourceConfig = new DataSourceConfig();
+        dataSourceConfig.setUrl(url);
+        dataSourceConfig.setDbType(DbType.MYSQL);// 数据库类型
+        dataSourceConfig.setDriverName(driverClassName);
+        dataSourceConfig.setUsername(username);
+        dataSourceConfig.setPassword(password);
+        generator.setDataSource(dataSourceConfig);
+
+        // 包配置
+        PackageConfig packageConfig = new PackageConfig();
+        //packageConfig.setModuleName(scanner("模块名"));
+        packageConfig.setParent(rootPackage);
+        packageConfig.setController("controller" + (StrUtil.isNotBlank(pkgName) ? "." + pkgName : ""));
+        packageConfig.setService("service" + (StrUtil.isNotBlank(pkgName) ? "." + pkgName : ""));
+        packageConfig.setServiceImpl("service" + (StrUtil.isNotBlank(pkgName) ? "." + pkgName + ".impl" : ".impl"));
+        packageConfig.setEntity("dao.entity" + (StrUtil.isNotBlank(pkgName) ? "." + pkgName : ""));
+        packageConfig.setMapper("dao.mapper" + (StrUtil.isNotBlank(pkgName) ? "." + pkgName : ""));
+
+        //packageConfig.setXml("dao.mapper.xml");
+        generator.setPackageInfo(packageConfig);
+
+        // 注意:模板引擎在mybatisplus依赖中的templates目录下,可以依照此默认模板进行自定义
+
+        // 策略配置:配置根据哪张表生成代码
+        StrategyConfig strategy = new StrategyConfig();
+        strategy.setInclude(tableNames);// 表名,多个英文逗号分割(与exclude二选一配置)
+        strategy.setNaming(NamingStrategy.underline_to_camel);
+        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
+        // strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!");
+        strategy.setEntityLombokModel(true);// lombok模型,@Accessors(chain = true)setter链式操作
+        strategy.setRestControllerStyle(true);// controller生成@RestController
+        strategy.setEntityTableFieldAnnotationEnable(true);// 是否生成实体时,生成字段注解
+        // strategy.setEntityColumnConstant(true);// 是否生成字段常量(默认 false)
+        strategy.setTablePrefix(tablePrefix);// 生成实体时去掉表前缀
+
+        TemplateConfig templateConfig = new TemplateConfig();
+        templateConfig.setController(null);
+        templateConfig.setService(null);
+        templateConfig.setServiceImpl(null);
+        templateConfig.setXml(null);
+        templateConfig.setMapper(null);
+        templateConfig.setEntity(null);
+        generator.setTemplate(templateConfig);
+
+
+        generator.setStrategy(strategy);
+        generator.setTemplateEngine(new FreemarkerTemplateEngine());
+
+        /**
+         * 自定义输出路径
+         */
+        // controller
+        List<FileOutConfig> focList = new ArrayList<>();
+
+        // mapper.xml
+        focList.add(new FileOutConfig("/templates/story-entity.java.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
+                return projectPath + "/" + modelModuleName + "/src/main/java/" + rootPackage.replace(".", "/") + "/dao/entity/" + pkgName + "/" + tableInfo.getEntityName() + StringPool.DOT_JAVA;
+            }
+        });
+
+        focList.add(new FileOutConfig("/templates/story-controller.java.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                return projectPath + "/" + controllerModuleName + "/src/main/java/" + rootPackage.replace(".", "/") + "/controller/" + pkgName + "/" + tableInfo.getEntityName() + "Controller" + StringPool.DOT_JAVA;
+            }
+        });
+        // service
+        focList.add(new FileOutConfig("/templates/service.java.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                return projectPath + "/" + serviceModuleName + "/src/main/java/" + rootPackage.replace(".", "/") + "/service/" + pkgName + "/" +  tableInfo.getEntityName() + "Service" + StringPool.DOT_JAVA;
+            }
+        });
+
+        // serviceImpl
+        focList.add(new FileOutConfig("/templates/story-serviceImpl.java.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                return projectPath + "/" + serviceModuleName + "/src/main/java/" + rootPackage.replace(".", "/") + "/service/" + pkgName + "/impl/" + tableInfo.getEntityName() + "ServiceImpl" + StringPool.DOT_JAVA;
+            }
+        });
+
+        // mapper.java
+        // service
+        focList.add(new FileOutConfig("/templates/story-mapper.java.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                return projectPath + "/" + modelModuleName + "/src/main/java/" + rootPackage.replace(".", "/") + "/dao/mapper/" + pkgName + "/" +  tableInfo.getEntityName() + "Mapper" + StringPool.DOT_JAVA;
+            }
+        });
+
+        // mapper.xml
+        focList.add(new FileOutConfig("/templates/mapper.xml.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
+                return projectPath + "/" + modelModuleName + "/src/main/resources/mapper/" + pkgName + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
+            }
+        });
+
+        InjectionConfig injectionConfig = new InjectionConfig() {
+            @Override
+            public void initMap() { }
+        };
+        injectionConfig.setFileOutConfigList(focList);
+
+        generator.setCfg(injectionConfig);
+        generator.execute();
+    }
+}
import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.core.toolkit.StringPool;
+import com.baomidou.mybatisplus.generator.AutoGenerator;
+import com.baomidou.mybatisplus.generator.InjectionConfig;
+import com.baomidou.mybatisplus.generator.config.*;
+import com.baomidou.mybatisplus.generator.config.po.TableInfo;
+import com.baomidou.mybatisplus.generator.config.rules.DateType;
+import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
+import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author xc
+ * @description
+ * @date 2023/5/19 09:20
+ */
+public class MybatisPlusCodeGenerator {
+
+    private static final String projectPath = System.getProperty("user.dir");
+
+    public static void main(String[] args) {
+        //====================配置变量区域=====================//
+        String author = "storyxc";// 生成文件的作者,可以不填
+        String rootPackage = "com.storyxc";// 生成的entity、controller、service等包所在的公共上一级包路径全限定名
+        String modelModuleName = "storyxc-model";
+        String serviceModuleName = "storyxc-web";
+        String controllerModuleName = "storyxc-web";
+        // 数据库配置
+        String url="jdbc:mysql://127.0.0.1/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false";
+        String driverClassName = "com.mysql.cj.jdbc.Driver";// 或者com.mysql.cj.jdbc.Driver
+        String username = "root";
+        String password = "root";
+
+        String[] tableNames = new String[]{""};
+        String pkgName = "";
+        //====================配置变量区域=====================//
+        String[] tablePrefix = new String[]{""};
+        // 代码生成器
+        AutoGenerator generator = new AutoGenerator();
+        // 全局配置
+        GlobalConfig globalConfig = new GlobalConfig();
+        globalConfig.setOutputDir(projectPath + "/" + modelModuleName + "/src/main/java");// 生成文件的输出目录
+        globalConfig.setFileOverride(false);// 是否覆盖已有文件,默认false
+        globalConfig.setOpen(false);// 是否打开输出目录
+        globalConfig.setAuthor(author);
+        globalConfig.setServiceName("%sService");// 去掉service接口的首字母I
+        globalConfig.setBaseResultMap(true);// 开启 BaseResultMap
+        globalConfig.setDateType(DateType.ONLY_DATE);// 只使用 java.util.date代替
+        globalConfig.setIdType(IdType.ASSIGN_ID);// 分配ID (主键类型为number或string)
+        generator.setGlobalConfig(globalConfig);
+
+        // 数据源配置
+        DataSourceConfig dataSourceConfig = new DataSourceConfig();
+        dataSourceConfig.setUrl(url);
+        dataSourceConfig.setDbType(DbType.MYSQL);// 数据库类型
+        dataSourceConfig.setDriverName(driverClassName);
+        dataSourceConfig.setUsername(username);
+        dataSourceConfig.setPassword(password);
+        generator.setDataSource(dataSourceConfig);
+
+        // 包配置
+        PackageConfig packageConfig = new PackageConfig();
+        //packageConfig.setModuleName(scanner("模块名"));
+        packageConfig.setParent(rootPackage);
+        packageConfig.setController("controller" + (StrUtil.isNotBlank(pkgName) ? "." + pkgName : ""));
+        packageConfig.setService("service" + (StrUtil.isNotBlank(pkgName) ? "." + pkgName : ""));
+        packageConfig.setServiceImpl("service" + (StrUtil.isNotBlank(pkgName) ? "." + pkgName + ".impl" : ".impl"));
+        packageConfig.setEntity("dao.entity" + (StrUtil.isNotBlank(pkgName) ? "." + pkgName : ""));
+        packageConfig.setMapper("dao.mapper" + (StrUtil.isNotBlank(pkgName) ? "." + pkgName : ""));
+
+        //packageConfig.setXml("dao.mapper.xml");
+        generator.setPackageInfo(packageConfig);
+
+        // 注意:模板引擎在mybatisplus依赖中的templates目录下,可以依照此默认模板进行自定义
+
+        // 策略配置:配置根据哪张表生成代码
+        StrategyConfig strategy = new StrategyConfig();
+        strategy.setInclude(tableNames);// 表名,多个英文逗号分割(与exclude二选一配置)
+        strategy.setNaming(NamingStrategy.underline_to_camel);
+        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
+        // strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!");
+        strategy.setEntityLombokModel(true);// lombok模型,@Accessors(chain = true)setter链式操作
+        strategy.setRestControllerStyle(true);// controller生成@RestController
+        strategy.setEntityTableFieldAnnotationEnable(true);// 是否生成实体时,生成字段注解
+        // strategy.setEntityColumnConstant(true);// 是否生成字段常量(默认 false)
+        strategy.setTablePrefix(tablePrefix);// 生成实体时去掉表前缀
+
+        TemplateConfig templateConfig = new TemplateConfig();
+        templateConfig.setController(null);
+        templateConfig.setService(null);
+        templateConfig.setServiceImpl(null);
+        templateConfig.setXml(null);
+        templateConfig.setMapper(null);
+        templateConfig.setEntity(null);
+        generator.setTemplate(templateConfig);
+
+
+        generator.setStrategy(strategy);
+        generator.setTemplateEngine(new FreemarkerTemplateEngine());
+
+        /**
+         * 自定义输出路径
+         */
+        // controller
+        List<FileOutConfig> focList = new ArrayList<>();
+
+        // mapper.xml
+        focList.add(new FileOutConfig("/templates/story-entity.java.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
+                return projectPath + "/" + modelModuleName + "/src/main/java/" + rootPackage.replace(".", "/") + "/dao/entity/" + pkgName + "/" + tableInfo.getEntityName() + StringPool.DOT_JAVA;
+            }
+        });
+
+        focList.add(new FileOutConfig("/templates/story-controller.java.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                return projectPath + "/" + controllerModuleName + "/src/main/java/" + rootPackage.replace(".", "/") + "/controller/" + pkgName + "/" + tableInfo.getEntityName() + "Controller" + StringPool.DOT_JAVA;
+            }
+        });
+        // service
+        focList.add(new FileOutConfig("/templates/service.java.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                return projectPath + "/" + serviceModuleName + "/src/main/java/" + rootPackage.replace(".", "/") + "/service/" + pkgName + "/" +  tableInfo.getEntityName() + "Service" + StringPool.DOT_JAVA;
+            }
+        });
+
+        // serviceImpl
+        focList.add(new FileOutConfig("/templates/story-serviceImpl.java.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                return projectPath + "/" + serviceModuleName + "/src/main/java/" + rootPackage.replace(".", "/") + "/service/" + pkgName + "/impl/" + tableInfo.getEntityName() + "ServiceImpl" + StringPool.DOT_JAVA;
+            }
+        });
+
+        // mapper.java
+        // service
+        focList.add(new FileOutConfig("/templates/story-mapper.java.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                return projectPath + "/" + modelModuleName + "/src/main/java/" + rootPackage.replace(".", "/") + "/dao/mapper/" + pkgName + "/" +  tableInfo.getEntityName() + "Mapper" + StringPool.DOT_JAVA;
+            }
+        });
+
+        // mapper.xml
+        focList.add(new FileOutConfig("/templates/mapper.xml.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
+                return projectPath + "/" + modelModuleName + "/src/main/resources/mapper/" + pkgName + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
+            }
+        });
+
+        InjectionConfig injectionConfig = new InjectionConfig() {
+            @Override
+            public void initMap() { }
+        };
+        injectionConfig.setFileOutConfigList(focList);
+
+        generator.setCfg(injectionConfig);
+        generator.execute();
+    }
+}

模板

WARNING

模板不能使用IDE格式化,否则生成的文件缩进会有问题

story-entity.java.ftl

xml
package \${package.Entity};
+
+<#list table.importPackages as pkg>
+import \${pkg};
+</#list>
+<#if swagger2>
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+</#if>
+<#if entityLombokModel>
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+    <#if chainModel>
+import lombok.experimental.Accessors;
+    </#if>
+</#if>
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.experimental.FieldNameConstants;
+
+/**
+ * \${table.comment!}
+ *
+ * @author \${author}
+ * @since \${date}
+ */
+<#if entityLombokModel>
+@Data
+    <#if superEntityClass??>
+@EqualsAndHashCode(callSuper = true)
+    <#else>
+@EqualsAndHashCode(callSuper = false)
+    </#if>
+    <#if chainModel>
+@Accessors(chain = true)
+    </#if>
+</#if>
+<#if table.convert>
+@TableName("\${table.name}")
+</#if>
+<#if swagger2>
+@ApiModel(value="\${entity}对象", description="\${table.comment!}")
+</#if>
+@FieldNameConstants
+<#if superEntityClass??>
+public class \${entity} extends \${superEntityClass}<#if activeRecord><\${entity}></#if> {
+<#elseif activeRecord>
+public class \${entity} extends Model<\${entity}> {
+<#else>
+public class \${entity} implements Serializable {
+</#if>
+
+<#if entitySerialVersionUID>
+    private static final long serialVersionUID = 1L;
+</#if>
+<#-- ----------  BEGIN 字段循环遍历  ---------->
+<#list table.fields as field>
+    <#if field.keyFlag>
+        <#assign keyPropertyName="\${field.propertyName}"/>
+    </#if>
+
+    @Schema(description = "\${field.comment}")
+    <#if field.keyFlag>
+        <#-- 主键 -->
+        <#if field.keyIdentityFlag>
+    @TableId(value = "\${field.annotationColumnName}", type = IdType.AUTO)
+        <#elseif idType??>
+    @TableId(value = "\${field.annotationColumnName}", type = IdType.\${idType})
+        <#elseif field.convert>
+    @TableId("\${field.annotationColumnName}")
+        </#if>
+        <#-- 普通字段 -->
+    <#elseif field.fill??>
+    <#-- -----   存在字段填充设置   ----->
+        <#if field.convert>
+    @TableField(value = "\${field.annotationColumnName}", fill = FieldFill.\${field.fill})
+        <#else>
+    @TableField(fill = FieldFill.\${field.fill})
+        </#if>
+    <#elseif field.convert>
+    @TableField("\${field.annotationColumnName}")
+    </#if>
+    <#-- 乐观锁注解 -->
+    <#if (versionFieldName!"") == field.name>
+    @Version
+    </#if>
+    <#-- 逻辑删除注解 -->
+    <#if (logicDeleteFieldName!"") == field.name>
+    @TableLogic
+    </#if>
+    private \${field.propertyType} \${field.propertyName};
+</#list>
+<#------------  END 字段循环遍历  ---------->
+
+<#if !entityLombokModel>
+    <#list table.fields as field>
+        <#if field.propertyType == "boolean">
+            <#assign getprefix="is"/>
+        <#else>
+            <#assign getprefix="get"/>
+        </#if>
+    public \${field.propertyType} \${getprefix}\${field.capitalName}() {
+        return \${field.propertyName};
+    }
+
+    <#if chainModel>
+    public \${entity} set\${field.capitalName}(\${field.propertyType} \${field.propertyName}) {
+    <#else>
+    public void set\${field.capitalName}(\${field.propertyType} \${field.propertyName}) {
+    </#if>
+        this.\${field.propertyName} = \${field.propertyName};
+        <#if chainModel>
+        return this;
+        </#if>
+    }
+    </#list>
+</#if>
+
+<#if entityColumnConstant>
+    <#list table.fields as field>
+    public static final String \${field.name?upper_case} = "\${field.name}";
+
+    </#list>
+</#if>
+<#if activeRecord>
+    @Override
+    protected Serializable pkVal() {
+    <#if keyPropertyName??>
+        return this.\${keyPropertyName};
+    <#else>
+        return null;
+    </#if>
+    }
+
+</#if>
+<#if !entityLombokModel>
+    @Override
+    public String toString() {
+        return "\${entity}{" +
+    <#list table.fields as field>
+        <#if field_index==0>
+            "\${field.propertyName}=" + \${field.propertyName} +
+        <#else>
+            ", \${field.propertyName}=" + \${field.propertyName} +
+        </#if>
+    </#list>
+        "}";
+    }
+</#if>
+}
package \${package.Entity};
+
+<#list table.importPackages as pkg>
+import \${pkg};
+</#list>
+<#if swagger2>
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+</#if>
+<#if entityLombokModel>
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+    <#if chainModel>
+import lombok.experimental.Accessors;
+    </#if>
+</#if>
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.experimental.FieldNameConstants;
+
+/**
+ * \${table.comment!}
+ *
+ * @author \${author}
+ * @since \${date}
+ */
+<#if entityLombokModel>
+@Data
+    <#if superEntityClass??>
+@EqualsAndHashCode(callSuper = true)
+    <#else>
+@EqualsAndHashCode(callSuper = false)
+    </#if>
+    <#if chainModel>
+@Accessors(chain = true)
+    </#if>
+</#if>
+<#if table.convert>
+@TableName("\${table.name}")
+</#if>
+<#if swagger2>
+@ApiModel(value="\${entity}对象", description="\${table.comment!}")
+</#if>
+@FieldNameConstants
+<#if superEntityClass??>
+public class \${entity} extends \${superEntityClass}<#if activeRecord><\${entity}></#if> {
+<#elseif activeRecord>
+public class \${entity} extends Model<\${entity}> {
+<#else>
+public class \${entity} implements Serializable {
+</#if>
+
+<#if entitySerialVersionUID>
+    private static final long serialVersionUID = 1L;
+</#if>
+<#-- ----------  BEGIN 字段循环遍历  ---------->
+<#list table.fields as field>
+    <#if field.keyFlag>
+        <#assign keyPropertyName="\${field.propertyName}"/>
+    </#if>
+
+    @Schema(description = "\${field.comment}")
+    <#if field.keyFlag>
+        <#-- 主键 -->
+        <#if field.keyIdentityFlag>
+    @TableId(value = "\${field.annotationColumnName}", type = IdType.AUTO)
+        <#elseif idType??>
+    @TableId(value = "\${field.annotationColumnName}", type = IdType.\${idType})
+        <#elseif field.convert>
+    @TableId("\${field.annotationColumnName}")
+        </#if>
+        <#-- 普通字段 -->
+    <#elseif field.fill??>
+    <#-- -----   存在字段填充设置   ----->
+        <#if field.convert>
+    @TableField(value = "\${field.annotationColumnName}", fill = FieldFill.\${field.fill})
+        <#else>
+    @TableField(fill = FieldFill.\${field.fill})
+        </#if>
+    <#elseif field.convert>
+    @TableField("\${field.annotationColumnName}")
+    </#if>
+    <#-- 乐观锁注解 -->
+    <#if (versionFieldName!"") == field.name>
+    @Version
+    </#if>
+    <#-- 逻辑删除注解 -->
+    <#if (logicDeleteFieldName!"") == field.name>
+    @TableLogic
+    </#if>
+    private \${field.propertyType} \${field.propertyName};
+</#list>
+<#------------  END 字段循环遍历  ---------->
+
+<#if !entityLombokModel>
+    <#list table.fields as field>
+        <#if field.propertyType == "boolean">
+            <#assign getprefix="is"/>
+        <#else>
+            <#assign getprefix="get"/>
+        </#if>
+    public \${field.propertyType} \${getprefix}\${field.capitalName}() {
+        return \${field.propertyName};
+    }
+
+    <#if chainModel>
+    public \${entity} set\${field.capitalName}(\${field.propertyType} \${field.propertyName}) {
+    <#else>
+    public void set\${field.capitalName}(\${field.propertyType} \${field.propertyName}) {
+    </#if>
+        this.\${field.propertyName} = \${field.propertyName};
+        <#if chainModel>
+        return this;
+        </#if>
+    }
+    </#list>
+</#if>
+
+<#if entityColumnConstant>
+    <#list table.fields as field>
+    public static final String \${field.name?upper_case} = "\${field.name}";
+
+    </#list>
+</#if>
+<#if activeRecord>
+    @Override
+    protected Serializable pkVal() {
+    <#if keyPropertyName??>
+        return this.\${keyPropertyName};
+    <#else>
+        return null;
+    </#if>
+    }
+
+</#if>
+<#if !entityLombokModel>
+    @Override
+    public String toString() {
+        return "\${entity}{" +
+    <#list table.fields as field>
+        <#if field_index==0>
+            "\${field.propertyName}=" + \${field.propertyName} +
+        <#else>
+            ", \${field.propertyName}=" + \${field.propertyName} +
+        </#if>
+    </#list>
+        "}";
+    }
+</#if>
+}

story-controller.java.ftl

xml
package \${package.Controller};
+
+import \${package.Service}.\${table.serviceName};
+import org.springframework.web.bind.annotation.RequestMapping;
+<#if restControllerStyle>
+import org.springframework.web.bind.annotation.RestController;
+<#else>
+import org.springframework.stereotype.Controller;
+</#if>
+<#if superControllerClassPackage??>
+import \${superControllerClassPackage};
+</#if>
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * \${table.comment!} 前端控制器
+ *
+ * @author \${author}
+ * @since \${date}
+ */
+@Tag(name = "")
+@Slf4j
+@RequiredArgsConstructor
+<#if restControllerStyle>
+@RestController
+<#else>
+@Controller
+</#if>
+@RequestMapping("<#if package.ModuleName?? && package.ModuleName != "">/\${package.ModuleName}</#if>/<#if controllerMappingHyphenStyle??>\${controllerMappingHyphen}<#else>\${table.entityPath}</#if>")
+<#if kotlin>
+class \${table.controllerName}<#if superControllerClass??> : \${superControllerClass}()</#if>
+<#else>
+<#if superControllerClass??>
+public class \${table.controllerName} extends \${superControllerClass} {
+<#else>
+public class \${table.controllerName} {
+</#if>
+    private final \${table.serviceName} \${table.serviceName?uncap_first};
+
+}
+</#if>
package \${package.Controller};
+
+import \${package.Service}.\${table.serviceName};
+import org.springframework.web.bind.annotation.RequestMapping;
+<#if restControllerStyle>
+import org.springframework.web.bind.annotation.RestController;
+<#else>
+import org.springframework.stereotype.Controller;
+</#if>
+<#if superControllerClassPackage??>
+import \${superControllerClassPackage};
+</#if>
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * \${table.comment!} 前端控制器
+ *
+ * @author \${author}
+ * @since \${date}
+ */
+@Tag(name = "")
+@Slf4j
+@RequiredArgsConstructor
+<#if restControllerStyle>
+@RestController
+<#else>
+@Controller
+</#if>
+@RequestMapping("<#if package.ModuleName?? && package.ModuleName != "">/\${package.ModuleName}</#if>/<#if controllerMappingHyphenStyle??>\${controllerMappingHyphen}<#else>\${table.entityPath}</#if>")
+<#if kotlin>
+class \${table.controllerName}<#if superControllerClass??> : \${superControllerClass}()</#if>
+<#else>
+<#if superControllerClass??>
+public class \${table.controllerName} extends \${superControllerClass} {
+<#else>
+public class \${table.controllerName} {
+</#if>
+    private final \${table.serviceName} \${table.serviceName?uncap_first};
+
+}
+</#if>

story-serviceImpl.java.ftl

xml
package \${package.ServiceImpl};
+
+import \${package.Entity}.\${entity};
+import \${package.Mapper}.\${table.mapperName};
+import \${package.Service}.\${table.serviceName};
+import \${superServiceImplClassPackage};
+import org.springframework.stereotype.Service;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * \${table.comment!} 服务实现类
+ *
+ * @author \${author}
+ * @since \${date}
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+<#if kotlin>
+open class \${table.serviceImplName} : \${superServiceImplClass}<\${table.mapperName}, \${entity}>(), \${table.serviceName} {
+
+}
+<#else>
+public class \${table.serviceImplName} extends \${superServiceImplClass}<\${table.mapperName}, \${entity}> implements \${table.serviceName} {
+
+}
+</#if>
package \${package.ServiceImpl};
+
+import \${package.Entity}.\${entity};
+import \${package.Mapper}.\${table.mapperName};
+import \${package.Service}.\${table.serviceName};
+import \${superServiceImplClassPackage};
+import org.springframework.stereotype.Service;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * \${table.comment!} 服务实现类
+ *
+ * @author \${author}
+ * @since \${date}
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+<#if kotlin>
+open class \${table.serviceImplName} : \${superServiceImplClass}<\${table.mapperName}, \${entity}>(), \${table.serviceName} {
+
+}
+<#else>
+public class \${table.serviceImplName} extends \${superServiceImplClass}<\${table.mapperName}, \${entity}> implements \${table.serviceName} {
+
+}
+</#if>

story-mapper.java.ftl

java
package \${package.Mapper};
+
+import \${package.Entity}.\${entity};
+import \${superMapperClassPackage};
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * <p>
+ * \${table.comment!} Mapper 接口
+ * </p>
+ *
+ * @author \${author}
+ * @since \${date}
+ */
+<#if kotlin>
+interface ${table.mapperName} : \${superMapperClass}<\${entity}>
+<#else>
+@Mapper
+public interface ${table.mapperName} extends \${superMapperClass}<\${entity}> {
+
+}
+</#if>
package \${package.Mapper};
+
+import \${package.Entity}.\${entity};
+import \${superMapperClassPackage};
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * <p>
+ * \${table.comment!} Mapper 接口
+ * </p>
+ *
+ * @author \${author}
+ * @since \${date}
+ */
+<#if kotlin>
+interface ${table.mapperName} : \${superMapperClass}<\${entity}>
+<#else>
+@Mapper
+public interface ${table.mapperName} extends \${superMapperClass}<\${entity}> {
+
+}
+</#if>
`,13),e=[o];function t(c,r,E,y,i,u){return n(),a("div",null,e)}const m=s(p,[["render",t]]);export{F as __pageData,m as default}; diff --git "a/assets/java_framework_\350\207\252\345\256\232\344\271\211MybatisPlusGenerator.md.cb2de2f5.lean.js" "b/assets/java_framework_\350\207\252\345\256\232\344\271\211MybatisPlusGenerator.md.cb2de2f5.lean.js" new file mode 100644 index 000000000..8974506f3 --- /dev/null +++ "b/assets/java_framework_\350\207\252\345\256\232\344\271\211MybatisPlusGenerator.md.cb2de2f5.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const F=JSON.parse('{"title":"自定义MybatisPlusGenerator","description":"","frontmatter":{},"headers":[],"relativePath":"java/framework/自定义MybatisPlusGenerator.md","filePath":"java/framework/自定义MybatisPlusGenerator.md","lastUpdated":1708510102000}'),p={name:"java/framework/自定义MybatisPlusGenerator.md"},o=l("",13),e=[o];function t(c,r,E,y,i,u){return n(),a("div",null,e)}const m=s(p,[["render",t]]);export{F as __pageData,m as default}; diff --git a/assets/java_index.md.a76c7870.js b/assets/java_index.md.a76c7870.js new file mode 100644 index 000000000..3787644db --- /dev/null +++ b/assets/java_index.md.a76c7870.js @@ -0,0 +1 @@ +import{_ as e,o as t,c as l,k as a,a as n}from"./chunks/framework.dc1f4608.js";const f=JSON.parse('{"title":"Java","description":"","frontmatter":{},"headers":[],"relativePath":"java/index.md","filePath":"java/index.md","lastUpdated":1708510102000}'),s={name:"java/index.md"},i=a("h1",{id:"java",tabindex:"-1"},[n("Java "),a("a",{class:"header-anchor",href:"#java","aria-label":'Permalink to "Java"'},"​")],-1),o=a("ul",null,[a("li",null,"基础"),a("li",null,"框架"),a("li",null,"中间件"),a("li",null,"数据库"),a("li",null,"开发工具"),a("li",null,"其他")],-1),r=[i,o];function d(c,_,u,p,h,m){return t(),l("div",null,r)}const x=e(s,[["render",d]]);export{f as __pageData,x as default}; diff --git a/assets/java_index.md.a76c7870.lean.js b/assets/java_index.md.a76c7870.lean.js new file mode 100644 index 000000000..3787644db --- /dev/null +++ b/assets/java_index.md.a76c7870.lean.js @@ -0,0 +1 @@ +import{_ as e,o as t,c as l,k as a,a as n}from"./chunks/framework.dc1f4608.js";const f=JSON.parse('{"title":"Java","description":"","frontmatter":{},"headers":[],"relativePath":"java/index.md","filePath":"java/index.md","lastUpdated":1708510102000}'),s={name:"java/index.md"},i=a("h1",{id:"java",tabindex:"-1"},[n("Java "),a("a",{class:"header-anchor",href:"#java","aria-label":'Permalink to "Java"'},"​")],-1),o=a("ul",null,[a("li",null,"基础"),a("li",null,"框架"),a("li",null,"中间件"),a("li",null,"数据库"),a("li",null,"开发工具"),a("li",null,"其他")],-1),r=[i,o];function d(c,_,u,p,h,m){return t(),l("div",null,r)}const x=e(s,[["render",d]]);export{f as __pageData,x as default}; diff --git "a/assets/java_middleware_kafka\345\255\246\344\271\240\350\256\260\345\275\225.md.2b882b43.js" "b/assets/java_middleware_kafka\345\255\246\344\271\240\350\256\260\345\275\225.md.2b882b43.js" new file mode 100644 index 000000000..f76e34210 --- /dev/null +++ "b/assets/java_middleware_kafka\345\255\246\344\271\240\350\256\260\345\275\225.md.2b882b43.js" @@ -0,0 +1 @@ +import{_ as a,o as e,c as r,Q as t}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"kafka学习记录","description":"","frontmatter":{},"headers":[],"relativePath":"java/middleware/kafka学习记录.md","filePath":"java/middleware/kafka学习记录.md","lastUpdated":1708510102000}'),o={name:"java/middleware/kafka学习记录.md"},i=t('

kafka学习记录

新公司用的消息中间件是kafka,此前没有接触过,先大致了解下内容,后续再补充

kafka简介

​ Kafka is a distributed,partitioned,replicated commit logservice。

Apache Kafka 是一个分布式发布 - 订阅消息系统和一个强大的队列,可以处理大量的数据,并使你能够将消息从一个端点传递到另一个端点。 Kafka 适合离线和在线消息消费。 Kafka 消息保留在磁盘上,并在群集内复制以防止数据丢失。 Kafka 构建在 ZooKeeper 同步服务之上。 它与 Apache Storm 和 Spark 非常好地集成,用于实时流式数据分析。

Kafka 是一个分布式消息队列,具有高性能、持久化、多副本备份、横向扩展能力。生产者往队列里写消息,消费者从队列里取消息进行业务逻辑。一般在架构设计中起到解耦、削峰、异步处理的作用。

相关术语:

  • broker:kafka集群中的每一台server称为一个kafka实例,也叫broker

  • topic:主题,一个topic中保存同一类消息,相当于对消息的分类

  • partition:分区,每个topic都可以分成多个partition,每个partition的存储层面都是append log文件.任何发布到该partition的消息都会被追加到log文件尾部.

    分区的根本原因:kafka基于文件进行存储,当文件内容达到一定程度很容易达到单个磁盘的上限,因此采取分区的办法,一个分区对应一个文件,这样就可以将数据分别存储到不同server上,另外也可以负载均衡,容纳更多消费者

  • offset:偏移量,一个分区对应一个磁盘上的文件,而消息在文件中的位置就是offset偏移量,offset为一个long型数字,可以唯一标记一条消息,kafka没有提供其他额外的索引机制来存储offset,所以只能顺序读写,在kafka中几乎不允许对消息进行"随机读写"

总结:

  • kafka是一个基于发布-订阅模型的分布式消息系统(消息队列)
  • kafka面向大数据,消息保存在topic中,每个topic有多个partition
  • kafka的消息数据保存在磁盘,每个partition对应磁盘上的一个文件,消息写入就是在append log文件尾部追加内容,文件可以在集群内备份以防丢失
  • 即使消息被消费,kafka也不会立即删除消息,可以通过配置使得过一段时间后自动删除以释放磁盘空间
  • kafka依赖分布式协调服务zookeeper,适合离线/在线消息的消费

基本原理

1、分布式和分区(distributed、partitioned)

一个topic对应的多个partition分散存储到集群的多个broker上,存储方式是一个partition对应一个文件,每个broker负责存储在自己机器上的partition中的消息读写

2、副本(replicated)

kafka可以配置partitions需要备份的个数(replicas),每个partition将会备份到多台机器上,以提高可用性.既然有副本,就涉及到对同一个文件的多个备份如何进行管理调度,kafka的方案是每个partition选举一个server作为leader,由leader负责所有对该分区的读写,其他server作为follower只需要简单的与leader同步,如果原来的leader失效,会重新选举由其他的follower来成为新的leader

如何选举leader:kafka使用zookeeper在broker中选出一个controller,用于partition的分配和leader选举

另外,作为leader的server承担了该分区所有的读写,所以压力较大,从整体考虑,有多少个partition就有多少个leader,kafka会将leader分散到不同的broker上,确保整体负载均衡

3、整体数据流程

202009161733104250.jpg

1)数据生产过程(producer)

生产者写入的消息,可以指定四个参数:topic、partition、key、value其中topic和value是必须指定的,key和partition是可选的。

对于一条记录,先对其进行序列化,然后按照topic和partition,放进对应的发送队列中。如果partition没有指定,那么情况如下:a、Key有填,按照key进行hash,相同的key去一个partition

b、key没填,round-robin轮询来选partition 202009161733117119.png

producer会和topic下的所有partition leader保持socket连接,消息经过producer直接通过socket发送至broker。其中partition leader的位置(host:port)注册在zookeeper中,producer作为zookeeper客户端已经注册了watch监听partition leader的变更事件,因此可以准确的知道谁是当前leader。

producer端采用异步发送,多条消息暂且在客户端buffer起来,并将他们批量发送到broker,小数据IO太多,会拖慢整体的网络延迟,批量延迟发送事实上提升了网络效率

2)数据消费过程(consumer)

消费者不是以单独形式存在,每一个消费者属于一个consumer group消费者组,一个group包含多个consumer。订阅topic是以一个消费组来订阅的,发送到topic的消息,只会被订阅此topic的每个group中的consumer消费。

如果所有的consumer都具有相同group,那么就像一个点对点的消息系统,如果每个consumer都具有不同的group,那消息就会广播给所有消费者。

具体说来,这实际上是根据partition来分的,==一个partition只能被消费组里的一个消费者消费,但是可以被多个消费组消费==,消费组里的每个消费者是关联到一个partition的,因此有这样的说法:对于一个topic,同一个group中不能有多于partitions个数的consumer同时消费,否则意味着某些消费者无法得到消息

同一个消费组的两个消费者不会同时消费一个partition.

在kafka中,采用了pull的方式,即consumer和broker建立连接之后,主动去pull(或者说fetch)消息,首先consumer端可以根据自己的消费能力适时去fetch消息并处理,且可以控制消息消费的进度(offset)。

partition中的消息只有一个consumer在消费,且不存在消息状态的控制,也没有复杂的消息确认机制,所以kafka的broker端很轻量级。当消息被consumer接受之后,需要保存offset记录消费到哪,以前保存在zk中,由于zk的性能瓶颈,以前的解决方案是consumer一分钟上报一次,在0.10版本后kafka把offset保存,从zk中剥离,保存在consumeroffsets topic的topic中,由此可见,consumer客户端也很轻量级

4、消息传送机制

kafka支持三种消息投递语义,在业务中通常使用At least once模型

  • At most once:最多一次,消息可能丢失,不会重复
  • At least once:最少一次,消息不会丢失,可能重复
  • Exactly once:只且一次,消息不丢失不重复,且只消费一次。

集群架构

20190805220416350.pngcluster_architecture.jpg

',37),p=[i];function l(c,n,k,s,d,f){return e(),r("div",null,p)}const b=a(o,[["render",l]]);export{h as __pageData,b as default}; diff --git "a/assets/java_middleware_kafka\345\255\246\344\271\240\350\256\260\345\275\225.md.2b882b43.lean.js" "b/assets/java_middleware_kafka\345\255\246\344\271\240\350\256\260\345\275\225.md.2b882b43.lean.js" new file mode 100644 index 000000000..089a2ab2f --- /dev/null +++ "b/assets/java_middleware_kafka\345\255\246\344\271\240\350\256\260\345\275\225.md.2b882b43.lean.js" @@ -0,0 +1 @@ +import{_ as a,o as e,c as r,Q as t}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"kafka学习记录","description":"","frontmatter":{},"headers":[],"relativePath":"java/middleware/kafka学习记录.md","filePath":"java/middleware/kafka学习记录.md","lastUpdated":1708510102000}'),o={name:"java/middleware/kafka学习记录.md"},i=t("",37),p=[i];function l(c,n,k,s,d,f){return e(),r("div",null,p)}const b=a(o,[["render",l]]);export{h as __pageData,b as default}; diff --git "a/assets/java_middleware_kafka\345\270\270\347\224\250\345\221\275\344\273\244\350\256\260\345\275\225.md.3a75876e.js" "b/assets/java_middleware_kafka\345\270\270\347\224\250\345\221\275\344\273\244\350\256\260\345\275\225.md.3a75876e.js" new file mode 100644 index 000000000..6993ef8d4 --- /dev/null +++ "b/assets/java_middleware_kafka\345\270\270\347\224\250\345\221\275\344\273\244\350\256\260\345\275\225.md.3a75876e.js" @@ -0,0 +1 @@ +import{_ as a,o as e,c as o,Q as t}from"./chunks/framework.dc1f4608.js";const b=JSON.parse('{"title":"kafka常用命令记录","description":"","frontmatter":{},"headers":[],"relativePath":"java/middleware/kafka常用命令记录.md","filePath":"java/middleware/kafka常用命令记录.md","lastUpdated":1708510102000}'),r={name:"java/middleware/kafka常用命令记录.md"},i=t('

kafka常用命令记录

环境信息:MacOS 13.4.1

Kafka版本:Homebrew Kafka_3.4.0

topic

新建topic

kafka-topics --create --topic topic-name --partitions 4 --replication-factor 2 --bootstrap-server localhost:9092

查看所有topic

kafka-topics --bootstrap-server 127.0.0.1:9092 --list

查看topic信息

kafka-topics --bootstrap-server 127.0.0.1:9092 --describe --topic topic-name

修改topic分区数

kafka-topics --bootstrap-server 127.0.0.1:9092 --alter --partitions 2 --topic topic-name

删除topic

kafka-topics --bootstrap-server 127.0.0.1:9092 --delete --topic topic-name

清空topic的消息

给topic保留消息时间改为1s,然后等topic中的消息被自动删除,再删除该配置

删除topic也可以实现清空消息

kafka-configs --bootstrap-server 127.0.0.1:9092 --entity-type topics --alter --entity-name example --add-config retention.ms=1000

kafka-configs --bootstrap-server 127.0.0.1:9092 --entity-type topics --alter --entity-name example --delete-config retention.ms

消费者组

查看所有消费者组

kafka-consumer-groups --bootstrap-server 127.0.0.1:9092 --list

查看消费者组详情

kafka-consumer-groups --bootstrap-server 127.0.0.1:9092 --describe --group topic-name

查看消费者组里具体成员

kafka-consumer-groups --bootstrap-server 127.0.0.1:9092 --describe --members --group topic-name

消费者console

查看topic中的所有消息

kafka-console-consumer --bootstrap-server 127.0.0.1:9092 --topic topic-name --from-beginning

指定分区、offset的消息

kafka-console-consumer --bootstrap-server 127.0.0.1:9092 --topic topic-name --partition 0 --offset 1

',29),c=[i];function p(s,n,d,l,h,k){return e(),o("div",null,c)}const m=a(r,[["render",p]]);export{b as __pageData,m as default}; diff --git "a/assets/java_middleware_kafka\345\270\270\347\224\250\345\221\275\344\273\244\350\256\260\345\275\225.md.3a75876e.lean.js" "b/assets/java_middleware_kafka\345\270\270\347\224\250\345\221\275\344\273\244\350\256\260\345\275\225.md.3a75876e.lean.js" new file mode 100644 index 000000000..bd12e4239 --- /dev/null +++ "b/assets/java_middleware_kafka\345\270\270\347\224\250\345\221\275\344\273\244\350\256\260\345\275\225.md.3a75876e.lean.js" @@ -0,0 +1 @@ +import{_ as a,o as e,c as o,Q as t}from"./chunks/framework.dc1f4608.js";const b=JSON.parse('{"title":"kafka常用命令记录","description":"","frontmatter":{},"headers":[],"relativePath":"java/middleware/kafka常用命令记录.md","filePath":"java/middleware/kafka常用命令记录.md","lastUpdated":1708510102000}'),r={name:"java/middleware/kafka常用命令记录.md"},i=t("",29),c=[i];function p(s,n,d,l,h,k){return e(),o("div",null,c)}const m=a(r,[["render",p]]);export{b as __pageData,m as default}; diff --git "a/assets/java_middleware_kakfa\345\256\236\350\267\265.md.012f67f9.js" "b/assets/java_middleware_kakfa\345\256\236\350\267\265.md.012f67f9.js" new file mode 100644 index 000000000..0601f023c --- /dev/null +++ "b/assets/java_middleware_kakfa\345\256\236\350\267\265.md.012f67f9.js" @@ -0,0 +1,305 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"kafka实践","description":"","frontmatter":{},"headers":[],"relativePath":"java/middleware/kakfa实践.md","filePath":"java/middleware/kakfa实践.md","lastUpdated":1708510102000}'),p={name:"java/middleware/kakfa实践.md"},o=l(`

kafka实践

下载

官方地址:https://kafka.apache.org/downloads.html

kafka入门介绍

kafka作为一个分布式流平台,意味着什么

  • 发布和订阅消息(流),在这方面它类似一个消息队列
  • 以容错(故障转移)的方式存储消息(流)
  • 在消息流发生时处理它们

kafka的优势

  • 构建实时的数据管道,可靠的获取系统和应用程序间的数据
  • 构建实时流的应用程序,对数据流进行转换或反应

kafka的几个概念

  • kafka作为集群运行在一个或多个服务器上
  • kafka集群存储的消息是以topic为类别记录的
  • 每个消息由一个key,一个value和时间戳构成

kafka四个核心API

  • Product API:发布消息到一个或多个topic中
  • Consumer API:订阅一个或多个topic,并处理产生的消息
  • Streams API:充当一个流处理器 ,从一个或多个topic消费输入流,并产生一个输出流到一个或多个输出topic,有效地将输入流转换到输出流
  • Connector API:可构建或运行可重用的生产者或消费者,将topic连接到现有的应用程序或数据系统。例如连接到关系型数据库的连接器可以捕捉表的每个变更

快速启动kafka

官方文档:http://kafka.apache.org/quickstart

TIP

强烈建议新学习一项新内容的时候尽量阅读英文原版文档

启动kafka需要的运行环境

使用kafka自带的zookeeper启动

bash
tar -xzf kafka_2.13-2.8.0.tgz
+cd kafka_2.13-2.8.0
+bin/zookeeper-server-start.sh config/zookeeper.properties & #后台启动
tar -xzf kafka_2.13-2.8.0.tgz
+cd kafka_2.13-2.8.0
+bin/zookeeper-server-start.sh config/zookeeper.properties & #后台启动

在kafka0.5x版本后已经自带了zookeeper, 而在最新的kafka2.8版本中,不再需要zookeeper服务,官网把这种称之为KRaft模式

打开另一个命令终端启动kafka服务

bash
bin/kafka-server-start.sh config/server.properties
bin/kafka-server-start.sh config/server.properties

等所有服务等启动完毕,kafka就已经是可用的了

创建一个主题(topic)

创建一个名为quickstart-events的topic,只有一个分区和一个备份

bash
bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic quickstart-events
+
+查看已创建的topic信息
+
+\`\`\`bash
+[root@localhost kafka_2.13-2.8.0]# bin/kafka-topics.sh --describe --topic quickstart-events --bootstrap-server localhost:9092
+Topic: quickstart-events        TopicId: iOM06pJVQV-y_A6QkmfeHw PartitionCount: 1       ReplicationFactor: 1  Configs: segment.bytes=1073741824
+        Topic: quickstart-events        Partition: 0    Leader: 0       Replicas: 0     Isr: 0
bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic quickstart-events
+
+查看已创建的topic信息
+
+\`\`\`bash
+[root@localhost kafka_2.13-2.8.0]# bin/kafka-topics.sh --describe --topic quickstart-events --bootstrap-server localhost:9092
+Topic: quickstart-events        TopicId: iOM06pJVQV-y_A6QkmfeHw PartitionCount: 1       ReplicationFactor: 1  Configs: segment.bytes=1073741824
+        Topic: quickstart-events        Partition: 0    Leader: 0       Replicas: 0     Isr: 0

发送消息到Topic

kafka提供了一个命令行工具,可以从输入文件或命令行中读取消息并发送给kafka集群,每一行是一条消息.运行producer(生产者) ,然后再控制台输入几条消息到服务器

bash
bin/kafka-console-producer.sh --topic quickstart-events --bootstrap-server localhost:9092
bin/kafka-console-producer.sh --topic quickstart-events --bootstrap-server localhost:9092

输入此命令后进入交互模式,便可以开始发送消息到topic

bash
>hello
+>hello world
+>this is first kafka message
+>
>hello
+>hello world
+>this is first kafka message
+>

可以使用Ctrl+C退出交互模式

消费Topic中的消息

打开另外一个终端窗口运行命令

bash
bin/kafka-console-consumer.sh --topic quickstart-events --from-beginning --bootstrap-server localhost:9092
bin/kafka-console-consumer.sh --topic quickstart-events --from-beginning --bootstrap-server localhost:9092

可以看到输出了刚才我们通过生产者终端发送的消息

bash
hello
+hello world
+this is first kafka message
hello
+hello world
+this is first kafka message

此时如果我们再在生产者终端发送消息,消费者终端也能实时进行消费,同样可以使用Ctrl+C 退出消费者的交互模式,消息会被持久化到kafka中,所以消息可以被消费多次以及被多个消费者消费,比如我们再打开一个窗口,执行消费的命令

bash
[root@localhost kafka_2.13-2.8.0]# bin/kafka-console-consumer.sh --topic quickstart-events --from-beginning --bootstrap-server localhost:9092
+hello
+hello world
+this is first kafka message
+
+可以看到,新的消费者又消费到了刚才的消息
[root@localhost kafka_2.13-2.8.0]# bin/kafka-console-consumer.sh --topic quickstart-events --from-beginning --bootstrap-server localhost:9092
+hello
+hello world
+this is first kafka message
+
+可以看到,新的消费者又消费到了刚才的消息

Kafka的Java客户端

依赖

xml
<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter</artifactId>
+</dependency>
+<dependency>
+<groupId>org.apache.kafka</groupId>
+<artifactId>kafka-streams</artifactId>
+</dependency>
+<dependency>
+<groupId>org.springframework.kafka</groupId>
+<artifactId>spring-kafka</artifactId>
+</dependency>
+
+<dependency>
+<groupId>org.springframework.boot</groupId>
+<artifactId>spring-boot-starter-test</artifactId>
+<scope>test</scope>
+</dependency>
+<dependency>
+<groupId>org.springframework.kafka</groupId>
+<artifactId>spring-kafka-test</artifactId>
+<scope>test</scope>
+</dependency>
<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter</artifactId>
+</dependency>
+<dependency>
+<groupId>org.apache.kafka</groupId>
+<artifactId>kafka-streams</artifactId>
+</dependency>
+<dependency>
+<groupId>org.springframework.kafka</groupId>
+<artifactId>spring-kafka</artifactId>
+</dependency>
+
+<dependency>
+<groupId>org.springframework.boot</groupId>
+<artifactId>spring-boot-starter-test</artifactId>
+<scope>test</scope>
+</dependency>
+<dependency>
+<groupId>org.springframework.kafka</groupId>
+<artifactId>spring-kafka-test</artifactId>
+<scope>test</scope>
+</dependency>

修改kafka配置

bash
修改config/server.properties
+listeners=PLAINTEXT://0.0.0.0:9092
+advertised.listeners=PLAINTEXT://192.168.174.130:9092
修改config/server.properties
+listeners=PLAINTEXT://0.0.0.0:9092
+advertised.listeners=PLAINTEXT://192.168.174.130:9092

开放虚拟机端口9092

firewall-cmd --zone=public --add-port=9092/tcp --permanent

firewall-cmd --reload

生产者

java
/**
+ * @author xc
+ * @description
+ * @createdTime 2021/5/10 23:58
+ */
+public class KafkaProducerTests {
+    @Test
+    void kafkaProducerTest() {
+        Properties properties = new Properties();
+        properties.put("bootstrap.servers", "192.168.174.130:9092");
+        properties.put("acks", "all");
+        properties.put("retries", 0);
+        properties.put("batch.size", 16384);
+        properties.put("linger.ms", 1);
+        properties.put("buffer.memory", 33554432);
+        properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
+        properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
+
+        KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
+        for (int i = 0; i < 100; i++) {
+            producer.send(new ProducerRecord<String, String>("test", Integer.toString(i), "Kafka message " + i));
+            System.out.println("发送了消息");
+        }
+        producer.close();
+
+    }
+}
/**
+ * @author xc
+ * @description
+ * @createdTime 2021/5/10 23:58
+ */
+public class KafkaProducerTests {
+    @Test
+    void kafkaProducerTest() {
+        Properties properties = new Properties();
+        properties.put("bootstrap.servers", "192.168.174.130:9092");
+        properties.put("acks", "all");
+        properties.put("retries", 0);
+        properties.put("batch.size", 16384);
+        properties.put("linger.ms", 1);
+        properties.put("buffer.memory", 33554432);
+        properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
+        properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
+
+        KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
+        for (int i = 0; i < 100; i++) {
+            producer.send(new ProducerRecord<String, String>("test", Integer.toString(i), "Kafka message " + i));
+            System.out.println("发送了消息");
+        }
+        producer.close();
+
+    }
+}
  • send()方法是异步的,添加消息到缓冲区等待发送并立即return,生产者会将单个消息批量在一起进行发送

  • ack是判断是否发送成功的,all将会阻塞消息,这种性能是最低的,但是是最可靠的

  • retries,如果请求失败,生产者会自动重试,如果启用重试,可能会产生重复消息

  • producer缓存每个分区未发送的消息,缓存的大小通过batch.size配置指定,数值较大会产生更大的批次并需要更大的内存

    默认缓冲可立即发送,即使缓存空间没有满,但是如果想减少请求的数量,可设置linger.ms大于0,这将让生产者在发送请求前等待一会儿,希望更多的消息来填补到缓冲区中

  • buffer.memory 控制生产者可用的缓存总量,如果消息发送速度比其传输到服务器的快,将会耗尽缓存空间,当缓存空间耗尽时,其他发送调用将会被阻塞,阻塞实践的阈值通过max.block.ms 设定,之后它将抛出一个TimeoutException

  • key.serializervalue.serializer将用户提供的key和value对象ProducerRecord转换成字节,可以使用附带 的* *ByteArraySerializerStringSeriializer**处理byte或string类型

消费者

java
/**
+ * @author xc
+ * @description
+ * @createdTime 2021/5/10 23:59
+ */
+public class KafkaConsumerTests {
+    @Test
+    void kafkaConsumerTest(){
+        Properties props = new Properties();
+        props.setProperty("bootstrap.servers", "192.168.174.130:9092");
+        props.setProperty("group.id", "test");
+        props.setProperty("enable.auto.commit", "true");
+        props.setProperty("auto.commit.interval.ms", "1000");
+        props.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
+        props.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
+        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
+        consumer.subscribe(Collections.singletonList("test"));
+        while (true) {
+            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
+            for (ConsumerRecord<String, String> record : records)
+                System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
+        }
+    }
+}
/**
+ * @author xc
+ * @description
+ * @createdTime 2021/5/10 23:59
+ */
+public class KafkaConsumerTests {
+    @Test
+    void kafkaConsumerTest(){
+        Properties props = new Properties();
+        props.setProperty("bootstrap.servers", "192.168.174.130:9092");
+        props.setProperty("group.id", "test");
+        props.setProperty("enable.auto.commit", "true");
+        props.setProperty("auto.commit.interval.ms", "1000");
+        props.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
+        props.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
+        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
+        consumer.subscribe(Collections.singletonList("test"));
+        while (true) {
+            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
+            for (ConsumerRecord<String, String> record : records)
+                System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
+        }
+    }
+}
  • enable.auto.commit自动提交偏移量,auto.commit.interval.ms控制提交的频率
  • 客户端订阅了名为test的topic,消费者组叫test

broker通过心跳检测test消费组中的进程,消费者会自动ping集群,告诉集群他还活着,只要消费者停止心跳的时间超过了session.timeout.ms 就会被认定为故障,它的分区将会被分配到别的进程

启动

先启动消费者,后启动生产者,可以看到消费者的终端输出了

bash
offset = 502, key = 0, value = Kafka message 0
+offset = 503, key = 1, value = Kafka message 1
+offset = 504, key = 2, value = Kafka message 2
+offset = 505, key = 3, value = Kafka message 3
+offset = 506, key = 4, value = Kafka message 4
+offset = 507, key = 5, value = Kafka message 5
+offset = 508, key = 6, value = Kafka message 6
+offset = 509, key = 7, value = Kafka message 7
+offset = 510, key = 8, value = Kafka message 8
+offset = 511, key = 9, value = Kafka message 9
+offset = 512, key = 10, value = Kafka message 10
+offset = 513, key = 11, value = Kafka message 11
+offset = 514, key = 12, value = Kafka message 12
+offset = 515, key = 13, value = Kafka message 13
+offset = 516, key = 14, value = Kafka message 14
+offset = 517, key = 15, value = Kafka message 15
+offset = 518, key = 16, value = Kafka message 16
+offset = 519, key = 17, value = Kafka message 17
+offset = 520, key = 18, value = Kafka message 18
+offset = 521, key = 19, value = Kafka message 19
+offset = 522, key = 20, value = Kafka message 20
+......
offset = 502, key = 0, value = Kafka message 0
+offset = 503, key = 1, value = Kafka message 1
+offset = 504, key = 2, value = Kafka message 2
+offset = 505, key = 3, value = Kafka message 3
+offset = 506, key = 4, value = Kafka message 4
+offset = 507, key = 5, value = Kafka message 5
+offset = 508, key = 6, value = Kafka message 6
+offset = 509, key = 7, value = Kafka message 7
+offset = 510, key = 8, value = Kafka message 8
+offset = 511, key = 9, value = Kafka message 9
+offset = 512, key = 10, value = Kafka message 10
+offset = 513, key = 11, value = Kafka message 11
+offset = 514, key = 12, value = Kafka message 12
+offset = 515, key = 13, value = Kafka message 13
+offset = 516, key = 14, value = Kafka message 14
+offset = 517, key = 15, value = Kafka message 15
+offset = 518, key = 16, value = Kafka message 16
+offset = 519, key = 17, value = Kafka message 17
+offset = 520, key = 18, value = Kafka message 18
+offset = 521, key = 19, value = Kafka message 19
+offset = 522, key = 20, value = Kafka message 20
+......

springboot集成kafka

依赖

xml
<dependency>
+    <groupId>org.springframework.kafka</groupId>
+    <artifactId>spring-kafka</artifactId>
+</dependency>
<dependency>
+    <groupId>org.springframework.kafka</groupId>
+    <artifactId>spring-kafka</artifactId>
+</dependency>

生产者

java
/**
+ * @author xc
+ * @description
+ * @createdTime 2021/5/10 21:49
+ */
+@RestController
+@RequestMapping("/kafka")
+public class KafkaDemoProducer {
+
+    @Autowired
+    private KafkaTemplate kafkaTemplate;
+
+    @GetMapping("/send/{msg}")
+    public String sendMessage(@PathVariable String msg) {
+        ListenableFuture send = kafkaTemplate.send("springboot-kafka", "测试发送:" + msg + "-" + System.currentTimeMillis());
+        System.out.println(send);
+        return "发送成功";
+    }
+}
/**
+ * @author xc
+ * @description
+ * @createdTime 2021/5/10 21:49
+ */
+@RestController
+@RequestMapping("/kafka")
+public class KafkaDemoProducer {
+
+    @Autowired
+    private KafkaTemplate kafkaTemplate;
+
+    @GetMapping("/send/{msg}")
+    public String sendMessage(@PathVariable String msg) {
+        ListenableFuture send = kafkaTemplate.send("springboot-kafka", "测试发送:" + msg + "-" + System.currentTimeMillis());
+        System.out.println(send);
+        return "发送成功";
+    }
+}

消费者

java
/**
+ * @author xc
+ * @description
+ * @createdTime 2021/5/15 23:55
+ */
+@Component
+public class KafkaDemoConsumer {
+
+
+    @KafkaListener(topics = {"springboot-kafka"})
+    public void onReceive(ConsumerRecord<?, ?> record) {
+        System.out.println("接收消息:" + record.topic() + "-" + record.partition() + "-" + record.value());
+    }
+}
/**
+ * @author xc
+ * @description
+ * @createdTime 2021/5/15 23:55
+ */
+@Component
+public class KafkaDemoConsumer {
+
+
+    @KafkaListener(topics = {"springboot-kafka"})
+    public void onReceive(ConsumerRecord<?, ?> record) {
+        System.out.println("接收消息:" + record.topic() + "-" + record.partition() + "-" + record.value());
+    }
+}

测试

  • 启动应用

  • 发送消息

  • 日志

bash
2021-05-16 15:56:18.430  INFO 10808 --- [nio-8080-exec-1] o.a.kafka.common.utils.AppInfoParser     : Kafka version: 2.6.0
+2021-05-16 15:56:18.430  INFO 10808 --- [nio-8080-exec-1] o.a.kafka.common.utils.AppInfoParser     : Kafka commitId: 62abe01bee039651
+2021-05-16 15:56:18.430  INFO 10808 --- [nio-8080-exec-1] o.a.kafka.common.utils.AppInfoParser     : Kafka startTimeMs: 1621151778430
+2021-05-16 15:56:18.435  INFO 10808 --- [ad | producer-1] org.apache.kafka.clients.Metadata        : [Producer clientId=producer-1] Cluster ID: t54vUJ_qTWm-o8WmD-dfag
+org.springframework.util.concurrent.SettableListenableFuture@2a59dc33
+接收消息:springboot-kafka-0-测试发送:hello-1621151778418
2021-05-16 15:56:18.430  INFO 10808 --- [nio-8080-exec-1] o.a.kafka.common.utils.AppInfoParser     : Kafka version: 2.6.0
+2021-05-16 15:56:18.430  INFO 10808 --- [nio-8080-exec-1] o.a.kafka.common.utils.AppInfoParser     : Kafka commitId: 62abe01bee039651
+2021-05-16 15:56:18.430  INFO 10808 --- [nio-8080-exec-1] o.a.kafka.common.utils.AppInfoParser     : Kafka startTimeMs: 1621151778430
+2021-05-16 15:56:18.435  INFO 10808 --- [ad | producer-1] org.apache.kafka.clients.Metadata        : [Producer clientId=producer-1] Cluster ID: t54vUJ_qTWm-o8WmD-dfag
+org.springframework.util.concurrent.SettableListenableFuture@2a59dc33
+接收消息:springboot-kafka-0-测试发送:hello-1621151778418
`,66),e=[o];function t(c,r,E,y,F,i){return a(),n("div",null,e)}const u=s(p,[["render",t]]);export{d as __pageData,u as default}; diff --git "a/assets/java_middleware_kakfa\345\256\236\350\267\265.md.012f67f9.lean.js" "b/assets/java_middleware_kakfa\345\256\236\350\267\265.md.012f67f9.lean.js" new file mode 100644 index 000000000..7fc7f1584 --- /dev/null +++ "b/assets/java_middleware_kakfa\345\256\236\350\267\265.md.012f67f9.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"kafka实践","description":"","frontmatter":{},"headers":[],"relativePath":"java/middleware/kakfa实践.md","filePath":"java/middleware/kakfa实践.md","lastUpdated":1708510102000}'),p={name:"java/middleware/kakfa实践.md"},o=l("",66),e=[o];function t(c,r,E,y,F,i){return a(),n("div",null,e)}const u=s(p,[["render",t]]);export{d as __pageData,u as default}; diff --git "a/assets/java_middleware_kakfa\351\207\215\345\244\215\346\266\210\350\264\271\351\227\256\351\242\230\345\244\204\347\220\206.md.e0988449.js" "b/assets/java_middleware_kakfa\351\207\215\345\244\215\346\266\210\350\264\271\351\227\256\351\242\230\345\244\204\347\220\206.md.e0988449.js" new file mode 100644 index 000000000..29f212a7c --- /dev/null +++ "b/assets/java_middleware_kakfa\351\207\215\345\244\215\346\266\210\350\264\271\351\227\256\351\242\230\345\244\204\347\220\206.md.e0988449.js" @@ -0,0 +1 @@ +import{_ as e,o as a,c as o,Q as l}from"./chunks/framework.dc1f4608.js";const g=JSON.parse('{"title":"kafka重复消费问题处理","description":"","frontmatter":{},"headers":[],"relativePath":"java/middleware/kakfa重复消费问题处理.md","filePath":"java/middleware/kakfa重复消费问题处理.md","lastUpdated":1708510102000}'),s={name:"java/middleware/kakfa重复消费问题处理.md"},t=l('

kafka重复消费问题处理

背景

最近在支援开发公司的财务对账系统,其中涉及一个功能点是某个对账单创建开票申请后,需要更新对账单的开票申请状态(未申请,部分申请,全部申请),这里使用了kafka,当创建开票申请后生产者即发送一条消息,消费者再去进行消费。

然后就碰到个问题,生产者这里是有事务的,因此会出现事务还没提交,开票申请数据还没有写入到数据库中,但是消息已经发送到kafka了,此时消费者直接消费消息时是查询不到这批开票申请数据的,就会出现创建开票申请业务实际成功了,但是对账单的开票申请状态仍为未申请。所以用了一个笨方法解决这个问题,在消费者线程中sleep(5000),来确保消费到消息时生产者的事务已经提交。

在测试环境测试少量账单&开票申请数据时是完全没有问题的,但是上了生产之后会有批量的账单数据推送过来,创建了大量的开票申请。这时候就发现异常情况了:消息队列中堆积了大量的消息,虽然日志一直还在正常跑,但是offset一直没变,并且过一段时间还会重复消费已经跑过日志的消息。

重复消费的原因

消费重复消费的根本原因都是:已经消费了数据,但是offset并没有提交。

网上找到的资料里有一段这么写的:

kafka消息重复消费很大一部分原因在于发生了再均衡。

1)消费者宕机、重启等。导致消息已经消费但是没有提交offset。

2)消费者使用自动提交offset,但当还没有提交的时候,有新的消费者加入或者移除,发生了rebalance。再次消费的时候,消费者会根据提交的偏移量来,于是重复消费了数据。

3)消息处理耗时,或者消费者拉取的消息量太多,处理耗时,超过了max.poll.interval.ms的配置时间,导致认为当前消费者已经死掉,触发再均衡。

日志:

txt
This member will leave the group because consumer poll timeout has expired. This means the time between subsequent calls to poll() was longer than the configured max.poll.interval.ms, which typically implies that the poll loop is spending too much time processing messages. You can address this either by increasing max.poll.interval.ms or by reducing the maximum size of batches returned in poll() with max.poll.records.
This member will leave the group because consumer poll timeout has expired. This means the time between subsequent calls to poll() was longer than the configured max.poll.interval.ms, which typically implies that the poll loop is spending too much time processing messages. You can address this either by increasing max.poll.interval.ms or by reducing the maximum size of batches returned in poll() with max.poll.records.

这里我碰到的情况就是第三种,因为消费者consumer每次拉取的消息较多,且有线程sleep的操作,所以导致处理超时了,超过max.poll.interval.ms的时间就会触发rebalance,然后就会重新分配消费者再次消费上次拉取的那批数据,这就是重复消费的原因。

处理方案

了解了超时的原因,就好解决了,根据日志中的提示,我们可以调整max.poll.interval.msmax.poll.records两个参数

  1. 增加max.poll.interval.ms的值,默认值为300000,单位是ms,即5分钟,可以调整为10分钟
  2. 减少max.poll.records的值,默认是500,可以调整为100
  3. 减少业务代码中线程休眠的时间

参考资料:

  1. https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html
  2. https://www.cnblogs.com/yangyongjie/p/14675119.html

Kafka知识回顾

1、消费者常见参数

①:fetch.min.bytes,配置Consumer一次拉取请求中能从Kafka中拉取的最小数据量,默认为1B,如果小于这个参数配置的值,就需要进行等待,直到数据量满足这个参数的配置大小。调大可以提交吞吐量,但也会造成延迟

②:fetch.max.bytes,一次拉取数据的最大数据量,默认为52428800B,也就是50M,但是如果设置的值过小,甚至小于每条消息的值,实际上也是能消费成功的

③:fetch.wait.max.ms,若是不满足fetch.min.bytes时,等待消费端请求的最长等待时间,默认是500ms

④:max.poll.records,单次poll调用返回的最大消息记录数,如果处理逻辑很轻量,可以适当提高该值。一次从kafka中poll出来的数据条数,max.poll.records条数据需要在在session.timeout.ms这个时间内处理完,默认值为500

⑤:consumer.poll(100) ,100 毫秒是一个超时时间,一旦拿到足够多的数据(fetch.min.bytes 参数设置),consumer.poll(100)会立即返回 ConsumerRecords<String, String> records。如果没有拿到足够多的数据,会阻塞100ms,但不会超过100ms就会返回

⑥:session. timeout. ms ,默认值是10s,该参数是 Consumer Group 主动检测 (组内成员comsummer)崩溃的时间间隔。若超过这个时间内没有收到心跳报文,则认为此消费者已经下线。将触发再均衡操作

⑦:max.poll.interval.ms,两次拉取消息的间隔,默认5分钟;通过消费组管理消费者时,该配置指定拉取消息线程最长空闲时间,若超过这个时间间隔没有发起poll操作,则消费组认为该消费者已离开了消费组,将进行再均衡操作(将分区分配给组内其他消费者成员)

若超过这个时间则报如下异常:

org.apache.kafka.clients.consumer.CommitFailedException: Commit cannot be completed since the group has already rebalanced and assigned the partitions to another member. This means that the time between subsequent calls to poll() was longer than the configured max.poll.interval.ms, which typically implies that the poll loop is spending too much time message processing. You can address this either by increasing the session timeout or by reducing the maximum size of batches returned in poll() with max.poll.records.
org.apache.kafka.clients.consumer.CommitFailedException: Commit cannot be completed since the group has already rebalanced and assigned the partitions to another member. This means that the time between subsequent calls to poll() was longer than the configured max.poll.interval.ms, which typically implies that the poll loop is spending too much time message processing. You can address this either by increasing the session timeout or by reducing the maximum size of batches returned in poll() with max.poll.records.

即:无法完成提交,因为组已经重新平衡并将分区分配给另一个成员。这意味着对poll()的后续调用之间的时间比配置的max.poll.interval.ms长,这通常意味着poll循环花费了太多的时间来处理消息。

可以通过增加max.poll.interval.ms来解决这个问题,也可以通过减少在poll()中使用max.poll.records返回的批的最大大小来解决这个问题

2、poll机制

①:每次poll的消息处理完成之后再进行下一次poll,是同步操作

②:每次poll之前检查是否可以进行位移提交,如果可以,那么就会提交上一次轮询的位移

③:每次poll时,consumer都将尝试使用上次消费的offset作为起始offset,然后依次拉取消息

④:poll(long timeout),timeout指等待轮询缓冲区的数据所花费的时间,单位是毫秒

3、再均衡 rebalance

将分区的所有权从一个消费者转移到其他消费者的行为称为再均衡(重平衡,rebalance)。

消费者通过向组织协调者(kafka broker)发送心跳来维护自己是消费者组的一员并确认其拥有的分区。对于不同不的消费群体来说,其组织协调者可以是不同的。只要消费者定期发送心跳,就会认为 消费者是存活的并处理其分区中的消息。当消费者检索记录或者提交它所消费的记录时就会发送心跳。

如果过了一段时间Kafka停止发送心跳了,会话(session)就会过期,组织协调者就会认为这个consumer已经死亡,就会触发一次重平衡。如果消费者宕机并且停止发送消息,组织协调者会等待几秒钟,确认它死亡了才会触发重平衡。在这段时间里,死亡的消费者将不处理任何消息。在清理消费者时,消费者将通知协调者它要离开群组,组织协调者会触发一次重平衡,尽量降低处理停顿。

重平衡是一把双刃剑,它为消费者群组带来高可用性和伸缩性的同时,还有有一些明显的缺点(bug),而这些 bug 到现在社区还无法修改。也就是说,在重平衡期间,消费者组中的消费者实例都会停止消费(Stop The World),等待重平衡的完成。而且重平衡这个过程很慢。

触发再均衡的情况:

①:有新的消费者加入消费组、或已有消费者主动离开组

②:消费者超过session时间未发送心跳(已有 consumer 崩溃了)

③:一次poll()之后的消息处理时间超过了max.poll.interval.ms的配置时间,因为一次poll()处理完才会触发下次poll() (已有 consumer 崩溃了)

④:订阅主题数发生变更

⑤:订阅主题的分区数发生变更

三、重复消费的解决方案

由于网络问题,重复消费不可避免,因此,消费者需要实现消费幂等。

方案:

①:消息表

②:数据库唯一索引

③:缓存消费过的消息id

四、项目kafka重复消费的排查

重复消费问题1:

每次拉取的消息记录数max.poll.records为100,poll最大拉取间隔max.poll.interval.ms为 300s,消息处理过于耗时导致时长大于了这个值,导致再均衡发生重复消费

解决办法:

①:减少每次拉取的消息记录数和增大poll之间的时间间隔

②:拉取到消息之后异步处理(保证成功消费)

',18),p=[t];function i(n,r,c,m,h,d){return a(),o("div",null,p)}const f=e(s,[["render",i]]);export{g as __pageData,f as default}; diff --git "a/assets/java_middleware_kakfa\351\207\215\345\244\215\346\266\210\350\264\271\351\227\256\351\242\230\345\244\204\347\220\206.md.e0988449.lean.js" "b/assets/java_middleware_kakfa\351\207\215\345\244\215\346\266\210\350\264\271\351\227\256\351\242\230\345\244\204\347\220\206.md.e0988449.lean.js" new file mode 100644 index 000000000..1a9c7c86a --- /dev/null +++ "b/assets/java_middleware_kakfa\351\207\215\345\244\215\346\266\210\350\264\271\351\227\256\351\242\230\345\244\204\347\220\206.md.e0988449.lean.js" @@ -0,0 +1 @@ +import{_ as e,o as a,c as o,Q as l}from"./chunks/framework.dc1f4608.js";const g=JSON.parse('{"title":"kafka重复消费问题处理","description":"","frontmatter":{},"headers":[],"relativePath":"java/middleware/kakfa重复消费问题处理.md","filePath":"java/middleware/kakfa重复消费问题处理.md","lastUpdated":1708510102000}'),s={name:"java/middleware/kakfa重复消费问题处理.md"},t=l("",18),p=[t];function i(n,r,c,m,h,d){return a(),o("div",null,p)}const f=e(s,[["render",i]]);export{g as __pageData,f as default}; diff --git "a/assets/java_middleware_\345\205\263\344\272\216\346\266\210\346\201\257\344\270\255\351\227\264\344\273\266MQ.md.68363b78.js" "b/assets/java_middleware_\345\205\263\344\272\216\346\266\210\346\201\257\344\270\255\351\227\264\344\273\266MQ.md.68363b78.js" new file mode 100644 index 000000000..f5708ecfc --- /dev/null +++ "b/assets/java_middleware_\345\205\263\344\272\216\346\266\210\346\201\257\344\270\255\351\227\264\344\273\266MQ.md.68363b78.js" @@ -0,0 +1,59 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"关于消息中间件MQ","description":"","frontmatter":{},"headers":[],"relativePath":"java/middleware/关于消息中间件MQ.md","filePath":"java/middleware/关于消息中间件MQ.md","lastUpdated":1708510102000}'),p={name:"java/middleware/关于消息中间件MQ.md"},o=l(`

关于消息中间件MQ

本文以RabbitMQ为例

1.为什么要使用MQ

这个问题也可以理解为MQ的作用,MQ的作用:

  • 异步:系统A中产生的一个数据,另外的系统BCD都需要对数据进行操作,不引入MQ时可以用A依次调用BCD的接口进行数据处理,这也就会耗费大量的时间,对于前台是无法接受的.如果引入MQ,可以将A系统的数据写入MQ,其他系统分别去消费数据,可以大大节省时间,优化体验
  • 解耦:如上面所说的,不使用MQ时,需要在A系统的代码里分别调用BCD的接口,如果BCD的服务宕机就会对A系统产生影响,又或者BCD系统如果后期不需要这个数据了,那就要删除A系统中对应的代码,如果要增加E服务处理A的数据,那又要增加相应的E系统的代码,耦合严重.如果引入MQ,系统中不会存在太大影响,就算其他系统宕机,也不会对A产生影响
  • 削峰:在高并发的情况下,如秒杀抢购活动,会在短时间内有大量请求涌入,如果流量太大,超过了系统的处理能力,可能就会导致我们的系统,数据库崩溃,可以将用户请求写入MQ,按照系统最大承载能力去处理请求,超过一定的阈值就将请求丢弃或给出错误提示

2.消息队列的优缺点

优点:

  • 对结构复杂的操作进行解耦,降低了系统的维护成本
  • 对一个可以异步进行的操作进行异步化,可以减少响应时间
  • 对高并发请求进行削峰,保证系统稳定性

缺点:

  • 系统复杂度提高。需要考虑MQ的各种情况,如消息丢失,重复消费,顺序消费等

  • 一致性问题。如A系统返回了成功的结果,BC系统成功了但D系统失败了

  • 系统可用性问题。如果MQ宕机,可能会导致系统的崩溃

3.如何保证消息队列高可用

RabbitMQ有三种模式:单机、普通集群、镜像集群

**普通集群:**就是在多台服务器上启动多个rabbitmq实例,但是创建的队列只会放在一个rabbitmq实例中,其他的实例会同步这个队列的元数据。消费的时候如果连接了另一个实例,也会从拥有队列的那个实例获取消息然后返回。

84949674832d2e63865764d.webp

这种方案并不能做到高可用

**镜像集群:**真正的高可用模式,创建的queue无论元数据还是消息数据都存放在多个实例中,每次写消息到queue时,都会自动把消息同步到多个queue中。

84949673a4af86b205cebcf.webp

优点:实现了高可用,任何一台机器宕机,其他机器能继续使用

缺点:1、性能消耗较大,所有机器都要进行消息同步 2、没有扩展性,如果有一个queue负载很重,就算增加机器,新增的机器也包含这个queue的全部数据,

4.如何保证消息不重复消费

保证消费的幂等性,让每条消息带一个全局唯一的bizId,具体过程:

1、消费者获取消息后先根据redis/db是否有该消息

2、如果不存在,则正常消费,消费完毕后写入redis/db

3、如果已经存在,证明已经消费过,直接丢弃

5.如何保证消息不丢失

原则:数据不能多也不能少,不能多是指不重复消费,不能少是指不能丢数据

丢失数据场景:

  • 生产者丢失数据:生产者发送数据到mq时可能因为网络波动丢失数据
  • rabbitmq丢失数据:如果没有开启rabbitmq持久化,一旦mq重启,数据就丢了
  • 消费者丢失数据:消费者刚消费到还没开始处理,消费者就挂掉了,重启后mq就认为已经消费过了,丢掉了数据

解决方案:

针对生产者丢失数据:

  • rabbitmq事务,生产者发送消息前开启事务,如果消息没有发送成功生产者会收到异常报错,这时可以回滚并重试发送
java
channel.txSelect();
+try{
+  //发送消息
+}catch(Exception e){
+  channel.rollback();
+  //重新发送
+}
channel.txSelect();
+try{
+  //发送消息
+}catch(Exception e){
+  channel.rollback();
+  //重新发送
+}

**缺点:**开启事务会变成阻塞操作,造成生产者的性能和吞吐量的下降

  • 把channel设置成confirm模式,每次写的消息都会分配一个唯一的id,如果mq接到消息就会回调生产者的接口,通知消息已经收到,如果mq接受报错,也会回调通知,这样可以重试发送数据,伪代码如下
java
//开启confirm模式
+channel.confirm();
+//发送消息
+
+在生产者服务提供一个回调接口的实现
+
+public void ack(String messageId){
+	//已经收到消息
+}
+
+public void nack(String messageId){
+    //重发消息
+}
//开启confirm模式
+channel.confirm();
+//发送消息
+
+在生产者服务提供一个回调接口的实现
+
+public void ack(String messageId){
+	//已经收到消息
+}
+
+public void nack(String messageId){
+    //重发消息
+}

**针对mq丢失数据:**开启mq的持久化,将交换机/队列的durable设置为true,表示交换机/队列时持久化的,在服务崩溃或重启后无需重新创建

java
@RabbitListener(
+     bindings = {
+        @QueueBinding(
+            value = @Queue(value = "dynamicQueue", autoDelete = "false", durable = "true"),
+            exchange = @Exchange(value = "exchange", durable = "true", type = ExchangeTypes.DIRECT),
+            key = "routingKey"
+        )
+    }
+)
+public void dynamicQueue(Message message, Channel channel) {
+        System.out.println("接收消息:" + new String(message.getBody()));
+}
@RabbitListener(
+     bindings = {
+        @QueueBinding(
+            value = @Queue(value = "dynamicQueue", autoDelete = "false", durable = "true"),
+            exchange = @Exchange(value = "exchange", durable = "true", type = ExchangeTypes.DIRECT),
+            key = "routingKey"
+        )
+    }
+)
+public void dynamicQueue(Message message, Channel channel) {
+        System.out.println("接收消息:" + new String(message.getBody()));
+}

如果消息想从rabbitmq崩溃中回复,消息必须实现:

  • 消息发送前,把投递模式设置为2(持久)来标记为持久消息
  • 将消息发送到持久交换机
  • 将消息发送到持久队列

针对消费者丢失数据:关闭消费者的autoAck机制,然后每次处理完一条消息,主动发送ack给rabbitmq,如果此时还没发送ack就宕机,mq没有收到ack消息,就会重新将消息重新分配给其他

强制消费者手动确认:

yml
spring.rabbitmq.listener.simple.acknowledge-mode: manual
spring.rabbitmq.listener.simple.acknowledge-mode: manual

消费者手动ack:

java
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);

6.如何保证消息的顺序消费

一个queue一个consumer

7.消息积压

1.先修复consumer的问题,确保恢复消费速度,然后停掉所有consumer

2.临时建立数十倍的queue

3.写一个临时分发的consumer程序,部署上去消费积压的消息,消费不做处理,直接轮询写入上一步建好的queue中

4.重新部署consumer(机器加倍),每一批consumer消费一个临时queue

`,51),e=[o];function c(t,r,E,y,i,u){return a(),n("div",null,e)}const F=s(p,[["render",c]]);export{h as __pageData,F as default}; diff --git "a/assets/java_middleware_\345\205\263\344\272\216\346\266\210\346\201\257\344\270\255\351\227\264\344\273\266MQ.md.68363b78.lean.js" "b/assets/java_middleware_\345\205\263\344\272\216\346\266\210\346\201\257\344\270\255\351\227\264\344\273\266MQ.md.68363b78.lean.js" new file mode 100644 index 000000000..0497ea633 --- /dev/null +++ "b/assets/java_middleware_\345\205\263\344\272\216\346\266\210\346\201\257\344\270\255\351\227\264\344\273\266MQ.md.68363b78.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"关于消息中间件MQ","description":"","frontmatter":{},"headers":[],"relativePath":"java/middleware/关于消息中间件MQ.md","filePath":"java/middleware/关于消息中间件MQ.md","lastUpdated":1708510102000}'),p={name:"java/middleware/关于消息中间件MQ.md"},o=l("",51),e=[o];function c(t,r,E,y,i,u){return a(),n("div",null,e)}const F=s(p,[["render",c]]);export{h as __pageData,F as default}; diff --git "a/assets/java_middleware_\345\210\206\345\270\203\345\274\217\351\224\201\350\247\243\345\206\263\346\226\271\346\241\210.md.6bed79a9.js" "b/assets/java_middleware_\345\210\206\345\270\203\345\274\217\351\224\201\350\247\243\345\206\263\346\226\271\346\241\210.md.6bed79a9.js" new file mode 100644 index 000000000..5ca4e192b --- /dev/null +++ "b/assets/java_middleware_\345\210\206\345\270\203\345\274\217\351\224\201\350\247\243\345\206\263\346\226\271\346\241\210.md.6bed79a9.js" @@ -0,0 +1,143 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"分布式锁解决方案","description":"","frontmatter":{},"headers":[],"relativePath":"java/middleware/分布式锁解决方案.md","filePath":"java/middleware/分布式锁解决方案.md","lastUpdated":1708510102000}'),p={name:"java/middleware/分布式锁解决方案.md"},o=l(`

分布式锁解决方案

分布式锁的概念

在单体应用中,对于并发处理公共资源时例如卖票,减商品库存这类操作,可以简单的加锁实现同步.但当单体应用服务化后,在分布式场景下,简单的加锁操作就无法实现需求了.这时就需要借助第三方组件来达到多进程多线程之间的同步操作.

分布式锁:在分布式环境下,多个程序/线程都需要对某一份(有限份)数据进行修改时,针对程序进行控制,保证在同一时间节点下,只有一个程序/线程对数据进行操作的技术

分布式锁的执行流程

Snipaste_20210305_152634.png

常见的解决方案:

  • 基于数据库的唯一索引
  • 基于数据库的排他锁
  • 基于Redis的EX NX参数
  • 基于ZooKeeper的临时有序节点

基于数据库的唯一索引

步骤:

1.创建一张表,表中要有唯一索引的字段用于记录当前哪个程序在进行操作

2.程序访问时,将程序的编号insert到这张表中(保证这个编号符合规则可以区分)

3.insert操作成功则代表该程序获得了锁,可以执行业务逻辑

4.当其他相同编号的程序进行insert时,由于唯一索引的限制会失败,则代表获取锁失败

5.当占用锁的程序业务逻辑执行完毕后删除该数据,代表释放锁

基于数据库的排他锁

以mysql为例的innodb引擎为例,可以使用for update语句来给数据库表加排他锁,当一个线程执行了for update操作给一条记录加锁后,其他线程无法再在该记录上增加排他锁

步骤:

1.开启事务

2.在查询语句后跟for update 例如 select * from tb_goods where id=1 for update

3.成功获取排他锁的线程即获得了分布式锁,可以执行业务逻辑

4.执行完毕后需要commit提交事务来释放锁

==这种方式需要关闭数据库事务的自动提交==

以上两种通过数据库来实现分布式锁的方案比较==简单方便,可以快速实现==,但是基于数据库的操作,开销非常大,对服务的性能存在影响

基于Redis实现分布式锁

redis实现分布式锁最核心的方法setnx,setnx的含义就是set if not exists,其中有两个参数setnx(key,value).该方法是原子的,如果key不存在,则设置当前key成功,返回1,如果key已存在,则设置当前key失败,返回0;

Snipaste_20210305_161717.png

锁超时

理想情况下,当某个程序抢占了锁后,处理完业务流程应该删除对应的key,如果这个过程中发生了问题,导致锁超时或者出现了异常,没有办法释放锁,就会产生死锁问题.

redis提供的另一个指令EXPIRE,来设置锁的过期时间EXPIRE KEY seconds来设置key的生存时间,如图

Snipaste_20210305_162330.png

两秒以后key就被自动删除了.

但是程序里我们也不能写成如下的代码

java
public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {
+ 
+    Long result = jedis.setnx(lockKey, requestId);
+    if (result == 1) {
+        jedis.expire(lockKey, expireTime);
+        doSomething();
+    }
+}
public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {
+ 
+    Long result = jedis.setnx(lockKey, requestId);
+    if (result == 1) {
+        jedis.expire(lockKey, expireTime);
+        doSomething();
+    }
+}

因为setnx和expire两个指令是两条命令并不具有原子性,如果在执行setnx后程序突然崩溃,没有设置过期时间就会发生死锁.

在redis2.6.12版本后,又提供了一个新的方案

SET key value [EX seconds] [PX millisecounds] [NX|XX] EX seconds:设置键的过期时间为second秒 PX millisecounds:设置键的过期时间为millisecounds 毫秒 NX:只在键不存在的时候,才对键进行设置操作 XX:只在键已经存在的时候,才对键进行设置操作 SET操作成功后,返回的是OK,失败返回NIL

Snipaste_20210307_145207.png

如何释放锁

释放锁的时候很容易联想到del指令,可是这个指令如果直接在代码中使用,会产生一个很严重的问题,拥有超时锁的线程会释放掉当前拥有锁的那个线程的锁

(删了不属于自己的锁)

场景:

  • 线程1,2,3分别尝试加锁最后只有1成功获得锁,其他线程加锁失败继续尝试
  • 线程1开始执行业务代码,线程2,3继续尝试
  • 线程1的锁超时了,自动释放了锁,此时线程2获得了锁,线程3继续尝试加锁
  • 线程1的任务执行完毕,使用了del指令删除锁,线程2在执行业务代码,线程3这时会获得锁(因为线程1把线程2的锁删了)

所以,我们要在set键值对的时候,保证可以区分开当前锁是否属于执行指令的这个线程,value我们可以设置为当前的requestID或线程的id

而这些步骤如果直接用代码控制则会显得较为繁琐,可以引入lua脚本

lua
if redis.call('get', KEYS[1]) == ARGV[1] 
+    then 
+	    return redis.call('del', KEYS[1]) 
+	else 
+	    return 0 
+end
if redis.call('get', KEYS[1]) == ARGV[1] 
+    then 
+	    return redis.call('del', KEYS[1]) 
+	else 
+	    return 0 
+end

上述lua脚本用于比较KEYS[1]对应的VALUE和ARG[1]的值是否一直,即当前锁是否属于当前线程,如果是true则删除锁,返回del结果,否则直接返回0

执行脚本可以使用redis的eval指令

java
jedis.eval(String script, List<String> kyes,List<String>args);
jedis.eval(String script, List<String> kyes,List<String>args);

存在的问题:上面讨论的是单机redis的场景,如果是分布式下的redis哨兵集群会存在问题,如果线程1的锁加在了主库,这时候主库直接宕机,redis还没来得及将这个锁的数据同步至从节点,sentinel就将从库中的一台选举为主库了,这时候另一个线程也来进行加锁,也会成功,这时候便有两个线程同时获得了锁

这种情况可以使用Redisson redlock

java
Config config = new Config();
+config.useSentinelServers().addSentinelAddress("127.0.0.1:6369","127.0.0.1:6379", "127.0.0.1:6389")
+		.setMasterName("masterName")
+		.setPassword("password").setDatabase(0);
+RedissonClient redissonClient = Redisson.create(config);
+RLock redLock = redissonClient.getLock("REDLOCK_KEY");
+boolean isLock;
+try {
+	isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
+	if (isLock) {
+		//TODO if get lock success, do something;
+	}
+} catch (Exception e) {
+} finally {
+	redLock.unlock();
+}
Config config = new Config();
+config.useSentinelServers().addSentinelAddress("127.0.0.1:6369","127.0.0.1:6379", "127.0.0.1:6389")
+		.setMasterName("masterName")
+		.setPassword("password").setDatabase(0);
+RedissonClient redissonClient = Redisson.create(config);
+RLock redLock = redissonClient.getLock("REDLOCK_KEY");
+boolean isLock;
+try {
+	isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
+	if (isLock) {
+		//TODO if get lock success, do something;
+	}
+} catch (Exception e) {
+} finally {
+	redLock.unlock();
+}

由于 redisson 包的实现中,通过 lua 脚本校验了解锁时的 client 身份,所以我们无需再在 finally 中去判断是否加锁成功,也无需做额外的身份校验,可以说已经达到开箱即用的程度了。

基于ZooKeeper实现分布式锁

  • zookeeper一般有多个节点构成(单数个),采用zab一致性协议,所以可以将zk看成一个单点结构,对其修改数据其内部会自动将所有节点进行修改才提供查询服务

  • zookeeper的数据是目录树的方式存储的,每个目录称为znode,znode可以存储数据,还可以在其中增加子节点

  • 子节点有三种类型

    • 序列化节点:每在该节点下增加一个节点会自动给节点的名字自增
    • 临时节点:一旦创建这个节点的客户端与服务器失去联系会自动删除节点
    • 普通节点
  • Watch机制,客户端可以监控每个节点的变化,产生变化时会给客户端产生一个事件

zookeeper分布式锁原理

  • 获取和释放锁:利用临时节点的特性和watch机制,每个锁占用一个普通节点/lock,当需要获取锁时在/lock目录下创建一个临时节点,创建成功表示获取锁成功,失败则watch /lock节点,有删除操作时再去竞争锁。临时节点的好处在于当进程挂掉后自动上锁的节点会自动删除,即释放锁.
  • 获取锁的顺序:上锁为创建临时有序节点,每个上锁的节点均能创建节点成功,只是序号不同,只有序号最小的才能拥有锁,如果节点序号不是最小则watch序号比自身小的前一个节点(公平锁)

获取锁的流程:

1.先有一个锁根节点,lockRootNode,可以是一个永久节点

2.客户端获取锁,先在lockRootNode下创建一个顺序的临时节点

3.调用lockRootNode节点的getChildren()方法获取所有节点,并从小到大排序,如果创建的最小的节点是当前节点,则返回true,获取锁成功,否则,watch比自己序号小的节点的释放动作(exist watch),这也可以保证每个客户端只需要watch一个节点

4.如果有节点释放操作,重复第3步

代码中可以使用Apache Curator来实现基于临时节点的分布式锁

java
public class CuratorDistrLockTest {
+
+    /** Zookeeper info */
+    private static final String ZK_ADDRESS = "192.168.1.100:2181";
+    private static final String ZK_LOCK_PATH = "/lockRootNode";
+
+    public static void main(String[] args) throws InterruptedException {
+        // 1.Connect to zk
+        CuratorFramework client = CuratorFrameworkFactory.newClient(
+                ZK_ADDRESS,
+                new RetryNTimes(10, 5000)
+        );
+        client.start();
+        System.out.println("zk client start successfully!");
+
+        Thread t1 = new Thread(() -> {
+            doWithLock(client);
+        }, "t1");
+        Thread t2 = new Thread(() -> {
+            doWithLock(client);
+        }, "t2");
+
+        t1.start();
+        t2.start();
+    }
+
+    private static void doWithLock(CuratorFramework client) {
+        InterProcessMutex lock = new InterProcessMutex(client, ZK_LOCK_PATH);
+        try {
+            if (lock.acquire(10 * 1000, TimeUnit.SECONDS)) {
+                System.out.println(Thread.currentThread().getName() + " hold lock");
+                Thread.sleep(5000L);
+                System.out.println(Thread.currentThread().getName() + " release lock");
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                lock.release();
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+    }
+}
public class CuratorDistrLockTest {
+
+    /** Zookeeper info */
+    private static final String ZK_ADDRESS = "192.168.1.100:2181";
+    private static final String ZK_LOCK_PATH = "/lockRootNode";
+
+    public static void main(String[] args) throws InterruptedException {
+        // 1.Connect to zk
+        CuratorFramework client = CuratorFrameworkFactory.newClient(
+                ZK_ADDRESS,
+                new RetryNTimes(10, 5000)
+        );
+        client.start();
+        System.out.println("zk client start successfully!");
+
+        Thread t1 = new Thread(() -> {
+            doWithLock(client);
+        }, "t1");
+        Thread t2 = new Thread(() -> {
+            doWithLock(client);
+        }, "t2");
+
+        t1.start();
+        t2.start();
+    }
+
+    private static void doWithLock(CuratorFramework client) {
+        InterProcessMutex lock = new InterProcessMutex(client, ZK_LOCK_PATH);
+        try {
+            if (lock.acquire(10 * 1000, TimeUnit.SECONDS)) {
+                System.out.println(Thread.currentThread().getName() + " hold lock");
+                Thread.sleep(5000L);
+                System.out.println(Thread.currentThread().getName() + " release lock");
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                lock.release();
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+    }
+}

优缺点:

优点:

  • 客户端如果宕机,可以立即释放锁
  • 集群模式的稳定性高
  • 可以通过watch机制实现阻塞锁

缺点:

  • 一旦出现网络抖动,zk就会认为客户端挂掉并断掉连接,其他客户端就会获取锁
  • 性能不高,每次获取锁和释放锁都基于创建删除临时节点,zk创建和删除节点只能通过leader服务器进行,然后再同步至所有follower上
`,68),e=[o];function t(c,r,E,y,i,d){return n(),a("div",null,e)}const h=s(p,[["render",t]]);export{u as __pageData,h as default}; diff --git "a/assets/java_middleware_\345\210\206\345\270\203\345\274\217\351\224\201\350\247\243\345\206\263\346\226\271\346\241\210.md.6bed79a9.lean.js" "b/assets/java_middleware_\345\210\206\345\270\203\345\274\217\351\224\201\350\247\243\345\206\263\346\226\271\346\241\210.md.6bed79a9.lean.js" new file mode 100644 index 000000000..406aa36e5 --- /dev/null +++ "b/assets/java_middleware_\345\210\206\345\270\203\345\274\217\351\224\201\350\247\243\345\206\263\346\226\271\346\241\210.md.6bed79a9.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"分布式锁解决方案","description":"","frontmatter":{},"headers":[],"relativePath":"java/middleware/分布式锁解决方案.md","filePath":"java/middleware/分布式锁解决方案.md","lastUpdated":1708510102000}'),p={name:"java/middleware/分布式锁解决方案.md"},o=l("",68),e=[o];function t(c,r,E,y,i,d){return n(),a("div",null,e)}const h=s(p,[["render",t]]);export{u as __pageData,h as default}; diff --git "a/assets/java_others_OpenJDK\346\262\241\346\234\211jstack\347\255\211\345\221\275\344\273\244\347\232\204\350\247\243\345\206\263\345\212\236\346\263\225.md.249c1c70.js" "b/assets/java_others_OpenJDK\346\262\241\346\234\211jstack\347\255\211\345\221\275\344\273\244\347\232\204\350\247\243\345\206\263\345\212\236\346\263\225.md.249c1c70.js" new file mode 100644 index 000000000..269ea954a --- /dev/null +++ "b/assets/java_others_OpenJDK\346\262\241\346\234\211jstack\347\255\211\345\221\275\344\273\244\347\232\204\350\247\243\345\206\263\345\212\236\346\263\225.md.249c1c70.js" @@ -0,0 +1 @@ +import{_ as t,o as s,c as o,k as a,a as e}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"OpenJDK没有jstack等命令的解决办法","description":"","frontmatter":{},"headers":[],"relativePath":"java/others/OpenJDK没有jstack等命令的解决办法.md","filePath":"java/others/OpenJDK没有jstack等命令的解决办法.md","lastUpdated":1708510102000}'),c={name:"java/others/OpenJDK没有jstack等命令的解决办法.md"},n=a("h1",{id:"openjdk没有jstack等命令的解决办法",tabindex:"-1"},[e("OpenJDK没有jstack等命令的解决办法 "),a("a",{class:"header-anchor",href:"#openjdk没有jstack等命令的解决办法","aria-label":'Permalink to "OpenJDK没有jstack等命令的解决办法"'},"​")],-1),r=a("p",null,"使用yum list --showduplicate | grep java-1.8 找出相关的软件版本",-1),_=a("p",null,[e("yum install 安装图中这两个软件即可"),a("img",{src:"https://storyxc.com/images/blog//5b912b2afd7847419e5d2af77f733692.png",alt:"jstack.png"})],-1),p=[n,r,_];function d(l,i,h,j,k,m){return s(),o("div",null,p)}const D=t(c,[["render",d]]);export{u as __pageData,D as default}; diff --git "a/assets/java_others_OpenJDK\346\262\241\346\234\211jstack\347\255\211\345\221\275\344\273\244\347\232\204\350\247\243\345\206\263\345\212\236\346\263\225.md.249c1c70.lean.js" "b/assets/java_others_OpenJDK\346\262\241\346\234\211jstack\347\255\211\345\221\275\344\273\244\347\232\204\350\247\243\345\206\263\345\212\236\346\263\225.md.249c1c70.lean.js" new file mode 100644 index 000000000..269ea954a --- /dev/null +++ "b/assets/java_others_OpenJDK\346\262\241\346\234\211jstack\347\255\211\345\221\275\344\273\244\347\232\204\350\247\243\345\206\263\345\212\236\346\263\225.md.249c1c70.lean.js" @@ -0,0 +1 @@ +import{_ as t,o as s,c as o,k as a,a as e}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"OpenJDK没有jstack等命令的解决办法","description":"","frontmatter":{},"headers":[],"relativePath":"java/others/OpenJDK没有jstack等命令的解决办法.md","filePath":"java/others/OpenJDK没有jstack等命令的解决办法.md","lastUpdated":1708510102000}'),c={name:"java/others/OpenJDK没有jstack等命令的解决办法.md"},n=a("h1",{id:"openjdk没有jstack等命令的解决办法",tabindex:"-1"},[e("OpenJDK没有jstack等命令的解决办法 "),a("a",{class:"header-anchor",href:"#openjdk没有jstack等命令的解决办法","aria-label":'Permalink to "OpenJDK没有jstack等命令的解决办法"'},"​")],-1),r=a("p",null,"使用yum list --showduplicate | grep java-1.8 找出相关的软件版本",-1),_=a("p",null,[e("yum install 安装图中这两个软件即可"),a("img",{src:"https://storyxc.com/images/blog//5b912b2afd7847419e5d2af77f733692.png",alt:"jstack.png"})],-1),p=[n,r,_];function d(l,i,h,j,k,m){return s(),o("div",null,p)}const D=t(c,[["render",d]]);export{u as __pageData,D as default}; diff --git "a/assets/java_others_POI\350\270\251\345\235\221-zip file is closed.md.0fc74901.js" "b/assets/java_others_POI\350\270\251\345\235\221-zip file is closed.md.0fc74901.js" new file mode 100644 index 000000000..8dc111ad3 --- /dev/null +++ "b/assets/java_others_POI\350\270\251\345\235\221-zip file is closed.md.0fc74901.js" @@ -0,0 +1,15 @@ +import{_ as s,o as a,c as p,Q as l}from"./chunks/framework.dc1f4608.js";const F=JSON.parse('{"title":"POI踩坑-zip file is closed","description":"","frontmatter":{},"headers":[],"relativePath":"java/others/POI踩坑-zip file is closed.md","filePath":"java/others/POI踩坑-zip file is closed.md","lastUpdated":1708510102000}'),n={name:"java/others/POI踩坑-zip file is closed.md"},o=l(`

POI踩坑-zip file is closed

前几天做一个大批量数据异步的导入,采用的是之前用过的事件模式处理,大致流程是用户上传excel文件后,主线程对excel表头进行校验,如果通过校验,则开辟子线程进行业务处理,主线程返回响应.

整个导入流程和原来做的没什么区别,就是这次整个业务流程都放在子线程中进行处理,原来做的那个导入是只是主线程读数据,读到设定的阈值时则新开线程进行数据的持久化.

之前的那个系统做的功能是没有什么问题的,只是前台需要等待主线程读取完数据,后台在异步的分批插入数据到db,主线程读取数据这个过程前端页面一直在loading,体验不是很好. 这次为了优化用户体验,整个读取和业务流程操作都放在子线程中,并且新增了进度条展示,但是出现了标题的Zip File is closed异常,但不能完全重现,时而出现,时而正常.完整堆栈信息如下:

java
Exception in thread "main" java.lang.IllegalStateException: Zip File is closed
+	at org.apache.poi.openxml4j.util.ZipFileZipEntrySource.getEntries(ZipFileZipEntrySource.java:45)
+	at org.apache.poi.openxml4j.opc.ZipPackage.getPartsImpl(ZipPackage.java:161)
+	at org.apache.poi.openxml4j.opc.OPCPackage.getParts(OPCPackage.java:662)
+	at org.apache.poi.openxml4j.opc.OPCPackage.open(OPCPackage.java:223)
+	at org.apache.poi.openxml4j.opc.OPCPackage.open(OPCPackage.java:186)
+	at com.test.util.POIEventModeHandler.handleExcel(POIEventModeHandler.java:482)
+	at com.test.util.Test.main(Test.java:20)
Exception in thread "main" java.lang.IllegalStateException: Zip File is closed
+	at org.apache.poi.openxml4j.util.ZipFileZipEntrySource.getEntries(ZipFileZipEntrySource.java:45)
+	at org.apache.poi.openxml4j.opc.ZipPackage.getPartsImpl(ZipPackage.java:161)
+	at org.apache.poi.openxml4j.opc.OPCPackage.getParts(OPCPackage.java:662)
+	at org.apache.poi.openxml4j.opc.OPCPackage.open(OPCPackage.java:223)
+	at org.apache.poi.openxml4j.opc.OPCPackage.open(OPCPackage.java:186)
+	at com.test.util.POIEventModeHandler.handleExcel(POIEventModeHandler.java:482)
+	at com.test.util.Test.main(Test.java:20)

追踪源码后发现,出现这个异常时,读取的zipentry为null,简单点说就是读了个不存在的文件.

然后我才想起来项目里文件上传有个对应的拦截器,请求过来之后,拦截器会把前台传的文件先保存在服务器的临时目录中,我们的controller里拿到的文件路径封装的是服务器上的临时文件目录,而不是前台那个MultipartFile的,在controller响应的时候,这个拦截器又把临时文件删除了.所以我这个问题就是线程执行的随机性导致的,正常的时候先执行了子线程中读取临时文件的方法,此时再切换到主线程返回响应时,拦截器无法删除这个文件,而如果一直先执行的是主线程,拦截器会先把文件删掉,这个时候子线程执行open那个临时文件就会出现zip file is closed的异常.

由于拦截器是公用的,而且原来的逻辑是响应后删除临时文件避免服务器磁盘占用,所以处理方案就是主线程在新建子线程任务之前把这个临时文件再copy一份让子线程去读取这个copy,子线程的finally中再把这个copy的文件删除即可.

  • 补充 在搜这个问题的时候还看到有人直接用request中的multipartfile进行读取也会报这个错的情况,原因是客户端的文件路径poi没法直接读到,但是还有人直接用poi处理inputStream也会报错,所以推荐大家在使用poi事件模式读取前台上传的文件时,先将文件保存在服务器端,然后去读取服务器上的文件,处理完后再删除即可.
`,9),e=[o];function t(c,r,i,E,y,d){return a(),p("div",null,e)}const g=s(n,[["render",t]]);export{F as __pageData,g as default}; diff --git "a/assets/java_others_POI\350\270\251\345\235\221-zip file is closed.md.0fc74901.lean.js" "b/assets/java_others_POI\350\270\251\345\235\221-zip file is closed.md.0fc74901.lean.js" new file mode 100644 index 000000000..df5df5f33 --- /dev/null +++ "b/assets/java_others_POI\350\270\251\345\235\221-zip file is closed.md.0fc74901.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as p,Q as l}from"./chunks/framework.dc1f4608.js";const F=JSON.parse('{"title":"POI踩坑-zip file is closed","description":"","frontmatter":{},"headers":[],"relativePath":"java/others/POI踩坑-zip file is closed.md","filePath":"java/others/POI踩坑-zip file is closed.md","lastUpdated":1708510102000}'),n={name:"java/others/POI踩坑-zip file is closed.md"},o=l("",9),e=[o];function t(c,r,i,E,y,d){return a(),p("div",null,e)}const g=s(n,[["render",t]]);export{F as __pageData,g as default}; diff --git "a/assets/java_others_cpu\345\215\240\347\224\250\347\216\207\351\253\230\346\216\222\346\237\245\346\200\235\350\267\257.md.e4a204f9.js" "b/assets/java_others_cpu\345\215\240\347\224\250\347\216\207\351\253\230\346\216\222\346\237\245\346\200\235\350\267\257.md.e4a204f9.js" new file mode 100644 index 000000000..64292001a --- /dev/null +++ "b/assets/java_others_cpu\345\215\240\347\224\250\347\216\207\351\253\230\346\216\222\346\237\245\346\200\235\350\267\257.md.e4a204f9.js" @@ -0,0 +1,19 @@ +import{_ as s,o as a,c as e,Q as n}from"./chunks/framework.dc1f4608.js";const y=JSON.parse('{"title":"cpu占用率高排查思路","description":"","frontmatter":{},"headers":[],"relativePath":"java/others/cpu占用率高排查思路.md","filePath":"java/others/cpu占用率高排查思路.md","lastUpdated":1708510102000}'),p={name:"java/others/cpu占用率高排查思路.md"},l=n(`

cpu占用率高排查思路

1.top命令找出cpu占用率高的进程pid

2.top -H -p pid 找出cpu占用率高的线程tid

3.printf "%x" tid命令打印出tid的十六进制形式

4.jstack pid | grep 十六进制tid -A 行数打印堆栈信息 或者 jstack pid >> log.txt将堆栈信息保存在文件中,再从文件中查找对应线程的信息

5.jstat -gcutil pid 5000 每隔5秒打印一次gc情况

S0:幸存1区当前使用比例
+S1:幸存2区当前使用比例
+E:伊甸园区使用比例
+O:老年代使用比例
+M:元数据区使用比例
+CCS:压缩使用比例
+YGC:年轻代垃圾回收次数
+FGC:老年代垃圾回收次数
+FGCT:老年代垃圾回收消耗时间
+GCT:垃圾回收消耗总时间
S0:幸存1区当前使用比例
+S1:幸存2区当前使用比例
+E:伊甸园区使用比例
+O:老年代使用比例
+M:元数据区使用比例
+CCS:压缩使用比例
+YGC:年轻代垃圾回收次数
+FGC:老年代垃圾回收次数
+FGCT:老年代垃圾回收消耗时间
+GCT:垃圾回收消耗总时间

6.jmap -heap pid 查看堆内存详细信息

7.jmap -histo pid > xxx.log 输出gc日志到文件

`,9),c=[l];function o(t,i,d,r,_,u){return a(),e("div",null,c)}const g=s(p,[["render",o]]);export{y as __pageData,g as default}; diff --git "a/assets/java_others_cpu\345\215\240\347\224\250\347\216\207\351\253\230\346\216\222\346\237\245\346\200\235\350\267\257.md.e4a204f9.lean.js" "b/assets/java_others_cpu\345\215\240\347\224\250\347\216\207\351\253\230\346\216\222\346\237\245\346\200\235\350\267\257.md.e4a204f9.lean.js" new file mode 100644 index 000000000..bec671c42 --- /dev/null +++ "b/assets/java_others_cpu\345\215\240\347\224\250\347\216\207\351\253\230\346\216\222\346\237\245\346\200\235\350\267\257.md.e4a204f9.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as e,Q as n}from"./chunks/framework.dc1f4608.js";const y=JSON.parse('{"title":"cpu占用率高排查思路","description":"","frontmatter":{},"headers":[],"relativePath":"java/others/cpu占用率高排查思路.md","filePath":"java/others/cpu占用率高排查思路.md","lastUpdated":1708510102000}'),p={name:"java/others/cpu占用率高排查思路.md"},l=n("",9),c=[l];function o(t,i,d,r,_,u){return a(),e("div",null,c)}const g=s(p,[["render",o]]);export{y as __pageData,g as default}; diff --git "a/assets/java_others_feign\350\257\267\346\261\202\345\257\274\350\207\264\347\232\204\347\224\250\346\210\267ip\350\216\267\345\217\226\351\227\256\351\242\230\350\256\260\345\275\225.md.f55418cc.js" "b/assets/java_others_feign\350\257\267\346\261\202\345\257\274\350\207\264\347\232\204\347\224\250\346\210\267ip\350\216\267\345\217\226\351\227\256\351\242\230\350\256\260\345\275\225.md.f55418cc.js" new file mode 100644 index 000000000..338324a2b --- /dev/null +++ "b/assets/java_others_feign\350\257\267\346\261\202\345\257\274\350\207\264\347\232\204\347\224\250\346\210\267ip\350\216\267\345\217\226\351\227\256\351\242\230\350\256\260\345\275\225.md.f55418cc.js" @@ -0,0 +1,129 @@ +import{_ as s,o as n,c as a,Q as p}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"feign请求导致的用户ip获取问题记录","description":"","frontmatter":{},"headers":[],"relativePath":"java/others/feign请求导致的用户ip获取问题记录.md","filePath":"java/others/feign请求导致的用户ip获取问题记录.md","lastUpdated":1708510102000}'),l={name:"java/others/feign请求导致的用户ip获取问题记录.md"},o=p(`

feign请求导致的用户ip获取问题记录

背景

有个功能需要做ip的判断,区分请求是来自用户端还是来自其他服务的feign调用,其中有个一个服务发起的feign调用,一直无法获取到真实的服务器ip,而一直是用户端的ip。

排查

  1. 首先贴出来获取ip的工具类
java
public static String getIpAddr(HttpServletRequest request) {
+        String ipAddress = request.getHeader("x-forwarded-for");
+        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getHeader("Proxy-Client-IP");
+        }
+        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getHeader("WL-Proxy-Client-IP");
+        }
+        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getRemoteAddr();
+            if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) {
+                //根据网卡取本机配置的IP
+                InetAddress inet = null;
+                try {
+                    inet = InetAddress.getLocalHost();
+                } catch (UnknownHostException e) {
+                    e.printStackTrace();
+                }
+                ipAddress = inet.getHostAddress();
+            }
+        }
+        //对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
+        if (ipAddress != null && ipAddress.length() > 15) { //"***.***.***.***".length() = 15
+            if (ipAddress.indexOf(",") > 0) {
+                ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
+            }
+        }
+        return ipAddress;
+    }
public static String getIpAddr(HttpServletRequest request) {
+        String ipAddress = request.getHeader("x-forwarded-for");
+        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getHeader("Proxy-Client-IP");
+        }
+        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getHeader("WL-Proxy-Client-IP");
+        }
+        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getRemoteAddr();
+            if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) {
+                //根据网卡取本机配置的IP
+                InetAddress inet = null;
+                try {
+                    inet = InetAddress.getLocalHost();
+                } catch (UnknownHostException e) {
+                    e.printStackTrace();
+                }
+                ipAddress = inet.getHostAddress();
+            }
+        }
+        //对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
+        if (ipAddress != null && ipAddress.length() > 15) { //"***.***.***.***".length() = 15
+            if (ipAddress.indexOf(",") > 0) {
+                ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
+            }
+        }
+        return ipAddress;
+    }
  1. 根据调试发现,请求的\`\`x-forwarded-for\`请求头是一直包含两个ip的,第一个ip是用户端的ip,第二个是nginx所在的服务器ip,这个也好理解,通过代理转发的请求,会把代理ip拼在真实ip后面。但是问题是这个请求是由其他服务feign发起的,理应不存在nginx转发(通过注册中心的服务名调用,绕过nginx)。

HTTP 请求头中的 X-Forwarded-For

Nginx remote_addr和proxy_add_x_forwarded_for变量详解]

  1. 把问题转到排查feign调用上后发现,该服务实例化了feign.builder来实现传递自定义请求头的目标
java
@Bean
+	public Feign.Builder feignBuilder() {
+		return Feign.builder().requestInterceptor(new RequestInterceptor() {
+			@Override
+			public void apply(RequestTemplate requestTemplate) {
+				Map<String, String> customHeaders = WebUtils.getCustomHeaders();
+				customHeaders.forEach((k, v) -> {
+					requestTemplate.header(k, v);
+				});
+			}
+		});
+	}
+
+---
+  
+public static Map<String, String> getCustomHeaders() {
+        Map<String, String> headers = new HashMap();
+        HttpServletRequest request = RequestContextHelper.getRequest();
+        Enumeration<String> headerNames = request.getHeaderNames();
+
+        while(headerNames.hasMoreElements()) {
+            String headerName = ((String)headerNames.nextElement()).toLowerCase();
+            if (headerName.startsWith("x-")) {
+                String headerValue = request.getHeader(headerName);
+                if (headerValue != null) {
+                    headers.put(headerName, headerValue);
+                }
+            }
+        }
+
+        headers.put("x-invoker-ip", IpUtils.getLocalIpAddr());
+        if (!headers.containsKey("x-auth-token")) {
+            headers.put("x-auth-token", TokenGenerator.generateWithSign());
+        }
+
+        return headers;
+    }
@Bean
+	public Feign.Builder feignBuilder() {
+		return Feign.builder().requestInterceptor(new RequestInterceptor() {
+			@Override
+			public void apply(RequestTemplate requestTemplate) {
+				Map<String, String> customHeaders = WebUtils.getCustomHeaders();
+				customHeaders.forEach((k, v) -> {
+					requestTemplate.header(k, v);
+				});
+			}
+		});
+	}
+
+---
+  
+public static Map<String, String> getCustomHeaders() {
+        Map<String, String> headers = new HashMap();
+        HttpServletRequest request = RequestContextHelper.getRequest();
+        Enumeration<String> headerNames = request.getHeaderNames();
+
+        while(headerNames.hasMoreElements()) {
+            String headerName = ((String)headerNames.nextElement()).toLowerCase();
+            if (headerName.startsWith("x-")) {
+                String headerValue = request.getHeader(headerName);
+                if (headerValue != null) {
+                    headers.put(headerName, headerValue);
+                }
+            }
+        }
+
+        headers.put("x-invoker-ip", IpUtils.getLocalIpAddr());
+        if (!headers.containsKey("x-auth-token")) {
+            headers.put("x-auth-token", TokenGenerator.generateWithSign());
+        }
+
+        return headers;
+    }

至此,问题解决,x-forwarded-for被放在了feign请求头中,导致了上述问题,这里把x-forwarded-for请求头排除即可。

  1. 分析:

    请求流程: 用户->nginx->网关服务->a服务->feign调用->b服务

用户的请求通过nignx后代理ip就会被添加到x-forwarded-for请求头中,此时x-forwarded-for请求头为:用户ip,nginx服务器的ip,后请求从网关路由到a服务,a服务通过feign.buider的添加拦截器方法,增加了一个添加指定请求头到feign的请求中的拦截器,导致原来的x-forwarded-for被原封不动的传到了b服务,b服务根据这个请求头获取ip时,就拿到了原始用户的ip。

`,13),e=[o];function t(r,c,E,y,i,d){return n(),a("div",null,e)}const g=s(l,[["render",t]]);export{u as __pageData,g as default}; diff --git "a/assets/java_others_feign\350\257\267\346\261\202\345\257\274\350\207\264\347\232\204\347\224\250\346\210\267ip\350\216\267\345\217\226\351\227\256\351\242\230\350\256\260\345\275\225.md.f55418cc.lean.js" "b/assets/java_others_feign\350\257\267\346\261\202\345\257\274\350\207\264\347\232\204\347\224\250\346\210\267ip\350\216\267\345\217\226\351\227\256\351\242\230\350\256\260\345\275\225.md.f55418cc.lean.js" new file mode 100644 index 000000000..dbca6d033 --- /dev/null +++ "b/assets/java_others_feign\350\257\267\346\261\202\345\257\274\350\207\264\347\232\204\347\224\250\346\210\267ip\350\216\267\345\217\226\351\227\256\351\242\230\350\256\260\345\275\225.md.f55418cc.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as p}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"feign请求导致的用户ip获取问题记录","description":"","frontmatter":{},"headers":[],"relativePath":"java/others/feign请求导致的用户ip获取问题记录.md","filePath":"java/others/feign请求导致的用户ip获取问题记录.md","lastUpdated":1708510102000}'),l={name:"java/others/feign请求导致的用户ip获取问题记录.md"},o=p("",13),e=[o];function t(r,c,E,y,i,d){return n(),a("div",null,e)}const g=s(l,[["render",t]]);export{u as __pageData,g as default}; diff --git "a/assets/java_others_linux\346\234\215\345\212\241\345\231\250\345\256\211\350\243\205OpenOffice\350\270\251\345\235\221.md.aab490ac.js" "b/assets/java_others_linux\346\234\215\345\212\241\345\231\250\345\256\211\350\243\205OpenOffice\350\270\251\345\235\221.md.aab490ac.js" new file mode 100644 index 000000000..06913768d --- /dev/null +++ "b/assets/java_others_linux\346\234\215\345\212\241\345\231\250\345\256\211\350\243\205OpenOffice\350\270\251\345\235\221.md.aab490ac.js" @@ -0,0 +1 @@ +import{_ as e,o,c as t,Q as a}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"linux服务器安装OpenOffice踩坑","description":"","frontmatter":{},"headers":[],"relativePath":"java/others/linux服务器安装OpenOffice踩坑.md","filePath":"java/others/linux服务器安装OpenOffice踩坑.md","lastUpdated":1708510102000}'),i={name:"java/others/linux服务器安装OpenOffice踩坑.md"},n=a('

linux服务器安装OpenOffice踩坑

背景

最近公司项目需要做一个在线预览Office文件的功能,尝试了使用OpenOffice把Office文档转化成PDF格式和HTML格式的文件再由前端解析PDF或者直接通过iFrame访问HTML文件的方案,windows系统直接下载安装后cmd运行命令就可以启动openoffice的服务了.服务器就稍微麻烦一点,本文记录了我在自己的阿里云服务器上安装OpenOffice遇到的坑和解决的办法. 解压安装OpenOffice,这个网上搜一下有很多,不再详细记录.可以参考 linux环境下安装 openOffice 并启动服务

遇到的问题

  • 找不到Java运行环境 这个问题是因为我服务器上的JDK是解压版本的,虽然配置了JAVA_HOME这些但是openoffice识别不到,后来我就删了这个解压版的JDK,然后用yum命令安装了OpenJDK 1.8,命令:yum install openjdk 1.8重新配置了JAVA_HOME后解决了这个报错
  • 找不到libXext.so.6 /opt/openoffice4/program/下缺少libXext.so.6文件 运行yum install libXext.X86_64. 这个是64位linux的版本,32位的系统需要改成对应的版本
  • no suitable windowing system found existing 运行yum groupinstall "X Window System",我在运行这个命令的时候,又碰到了另一个报错:no packages in any requested group available to install or update, 这里命令后面要加上一些参数,执行yum groupinstall "X Window System" --setoptgroup_package_type=mandatory.default.optional

解决报错后,再重新执行启动服务的命令 nohup ./soffice -headless -accept="socket,host=127.0.0.1,port=8100;urp;" -nofirststartwizard &

执行后可以使用netstat命令查看8100端口的占用情况net stat -lnp|grep 8100 可以看到soffice.bin就说明服务成功启动了

然后启动测试的demo后发现,office文件转换成PDF后中文出现乱码,这是因为服务器上没有中文字体,用ftp工具把windows的中文字体直接传到服务器上的/usr/share/fonts文件夹中,清除缓存后重新启动openoffice服务后就能正确显示中文了.

',8),c=[n];function r(l,s,f,p,d,u){return o(),t("div",null,c)}const O=e(i,[["render",r]]);export{h as __pageData,O as default}; diff --git "a/assets/java_others_linux\346\234\215\345\212\241\345\231\250\345\256\211\350\243\205OpenOffice\350\270\251\345\235\221.md.aab490ac.lean.js" "b/assets/java_others_linux\346\234\215\345\212\241\345\231\250\345\256\211\350\243\205OpenOffice\350\270\251\345\235\221.md.aab490ac.lean.js" new file mode 100644 index 000000000..b1dce4828 --- /dev/null +++ "b/assets/java_others_linux\346\234\215\345\212\241\345\231\250\345\256\211\350\243\205OpenOffice\350\270\251\345\235\221.md.aab490ac.lean.js" @@ -0,0 +1 @@ +import{_ as e,o,c as t,Q as a}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"linux服务器安装OpenOffice踩坑","description":"","frontmatter":{},"headers":[],"relativePath":"java/others/linux服务器安装OpenOffice踩坑.md","filePath":"java/others/linux服务器安装OpenOffice踩坑.md","lastUpdated":1708510102000}'),i={name:"java/others/linux服务器安装OpenOffice踩坑.md"},n=a("",8),c=[n];function r(l,s,f,p,d,u){return o(),t("div",null,c)}const O=e(i,[["render",r]]);export{h as __pageData,O as default}; diff --git "a/assets/java_others_mybatis\347\232\204classpath\351\205\215\347\275\256\345\257\274\350\207\264\347\232\204jar\345\214\205\350\257\273\345\217\226\351\227\256\351\242\230.md.6a44f13e.js" "b/assets/java_others_mybatis\347\232\204classpath\351\205\215\347\275\256\345\257\274\350\207\264\347\232\204jar\345\214\205\350\257\273\345\217\226\351\227\256\351\242\230.md.6a44f13e.js" new file mode 100644 index 000000000..47b031483 --- /dev/null +++ "b/assets/java_others_mybatis\347\232\204classpath\351\205\215\347\275\256\345\257\274\350\207\264\347\232\204jar\345\214\205\350\257\273\345\217\226\351\227\256\351\242\230.md.6a44f13e.js" @@ -0,0 +1 @@ +import{_ as a,o as s,c as t,Q as e}from"./chunks/framework.dc1f4608.js";const b=JSON.parse('{"title":"mybatis的classpath配置导致的jar包读取问题","description":"","frontmatter":{},"headers":[],"relativePath":"java/others/mybatis的classpath配置导致的jar包读取问题.md","filePath":"java/others/mybatis的classpath配置导致的jar包读取问题.md","lastUpdated":1708510102000}'),c={name:"java/others/mybatis的classpath配置导致的jar包读取问题.md"},r=e('

mybatis的classpath配置导致的jar包读取问题

今天同事碰到个问题,在服务中引入了另一个服务的mapper文件后找不到其中配置的resultMap引发报错。后经排查是因为配置文件中classpath的配置问题引起的。

classpath:: 只会从当前服务的class目录下寻找文件

classpath*:: 会从class目录下寻找文件,还会从引入的依赖(打包后lib文件夹中的jar包)中寻找文件

所以当把mybatis的classpath配置从classpath:改为classpath*:后问题能解决了

',5),p=[r];function _(o,l,h,i,d,m){return s(),t("div",null,p)}const j=a(c,[["render",_]]);export{b as __pageData,j as default}; diff --git "a/assets/java_others_mybatis\347\232\204classpath\351\205\215\347\275\256\345\257\274\350\207\264\347\232\204jar\345\214\205\350\257\273\345\217\226\351\227\256\351\242\230.md.6a44f13e.lean.js" "b/assets/java_others_mybatis\347\232\204classpath\351\205\215\347\275\256\345\257\274\350\207\264\347\232\204jar\345\214\205\350\257\273\345\217\226\351\227\256\351\242\230.md.6a44f13e.lean.js" new file mode 100644 index 000000000..f9ff1a70c --- /dev/null +++ "b/assets/java_others_mybatis\347\232\204classpath\351\205\215\347\275\256\345\257\274\350\207\264\347\232\204jar\345\214\205\350\257\273\345\217\226\351\227\256\351\242\230.md.6a44f13e.lean.js" @@ -0,0 +1 @@ +import{_ as a,o as s,c as t,Q as e}from"./chunks/framework.dc1f4608.js";const b=JSON.parse('{"title":"mybatis的classpath配置导致的jar包读取问题","description":"","frontmatter":{},"headers":[],"relativePath":"java/others/mybatis的classpath配置导致的jar包读取问题.md","filePath":"java/others/mybatis的classpath配置导致的jar包读取问题.md","lastUpdated":1708510102000}'),c={name:"java/others/mybatis的classpath配置导致的jar包读取问题.md"},r=e("",5),p=[r];function _(o,l,h,i,d,m){return s(),t("div",null,p)}const j=a(c,[["render",_]]);export{b as __pageData,j as default}; diff --git "a/assets/java_others_ribbon\345\210\267\346\226\260\346\234\215\345\212\241\345\210\227\350\241\250\351\227\264\351\232\224\345\222\214canal\347\232\204\345\235\221.md.98a4b036.js" "b/assets/java_others_ribbon\345\210\267\346\226\260\346\234\215\345\212\241\345\210\227\350\241\250\351\227\264\351\232\224\345\222\214canal\347\232\204\345\235\221.md.98a4b036.js" new file mode 100644 index 000000000..ba77e74da --- /dev/null +++ "b/assets/java_others_ribbon\345\210\267\346\226\260\346\234\215\345\212\241\345\210\227\350\241\250\351\227\264\351\232\224\345\222\214canal\347\232\204\345\235\221.md.98a4b036.js" @@ -0,0 +1,69 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const F=JSON.parse('{"title":"ribbon刷新服务列表间隔和canal的坑","description":"","frontmatter":{},"headers":[],"relativePath":"java/others/ribbon刷新服务列表间隔和canal的坑.md","filePath":"java/others/ribbon刷新服务列表间隔和canal的坑.md","lastUpdated":1708510102000}'),p={name:"java/others/ribbon刷新服务列表间隔和canal的坑.md"},o=l(`

ribbon刷新服务列表间隔和canal的坑

ribbon有个参数可以用来调整刷新server list的时间间隔参数。

ServerListRefreshInterval

ribbon-loadbalancer-2.2.0-sources.jar!/com/netflix/client/config/CommonClientConfigKey.java

java
public static final IClientConfigKey<Integer> ServerListRefreshInterval = new CommonClientConfigKey<Integer>("ServerListRefreshInterval"){};
public static final IClientConfigKey<Integer> ServerListRefreshInterval = new CommonClientConfigKey<Integer>("ServerListRefreshInterval"){};

PollingServerListUpdater

ribbon-loadbalancer-2.2.0-sources.jar!/com/netflix/loadbalancer/PollingServerListUpdater.java

java
private static long getRefreshIntervalMs(IClientConfig clientConfig) {
+        return clientConfig.get(CommonClientConfigKey.ServerListRefreshInterval, LISTOFSERVERS_CACHE_REPEAT_INTERVAL);
+    }
+    
+    @Override
+    public synchronized void start(final UpdateAction updateAction) {
+        if (isActive.compareAndSet(false, true)) {
+            final Runnable wrapperRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    if (!isActive.get()) {
+                        if (scheduledFuture != null) {
+                            scheduledFuture.cancel(true);
+                        }
+                        return;
+                    }
+                    try {
+                        updateAction.doUpdate();
+                        lastUpdated = System.currentTimeMillis();
+                    } catch (Exception e) {
+                        logger.warn("Failed one update cycle", e);
+                    }
+                }
+            };
+ 
+            scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
+                    wrapperRunnable,
+                    initialDelayMs,
+                    refreshIntervalMs,
+                    TimeUnit.MILLISECONDS
+            );
+        } else {
+            logger.info("Already active, no-op");
+        }
+    }
private static long getRefreshIntervalMs(IClientConfig clientConfig) {
+        return clientConfig.get(CommonClientConfigKey.ServerListRefreshInterval, LISTOFSERVERS_CACHE_REPEAT_INTERVAL);
+    }
+    
+    @Override
+    public synchronized void start(final UpdateAction updateAction) {
+        if (isActive.compareAndSet(false, true)) {
+            final Runnable wrapperRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    if (!isActive.get()) {
+                        if (scheduledFuture != null) {
+                            scheduledFuture.cancel(true);
+                        }
+                        return;
+                    }
+                    try {
+                        updateAction.doUpdate();
+                        lastUpdated = System.currentTimeMillis();
+                    } catch (Exception e) {
+                        logger.warn("Failed one update cycle", e);
+                    }
+                }
+            };
+ 
+            scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
+                    wrapperRunnable,
+                    initialDelayMs,
+                    refreshIntervalMs,
+                    TimeUnit.MILLISECONDS
+            );
+        } else {
+            logger.info("Already active, no-op");
+        }
+    }

可以看到有个定时线程,每隔一定时间会去刷新服务的列表,因此,这个时间不要改的太长,默认应该是30s,公司的测试环境不知道是哪位大佬出于什么考虑改的,我反正已经改回去了 = =.

canal的坑

公司用canal主要是因为有新旧两个系统,两个系统同时在运转,目前还没有完全迁移,因此用canal保证新旧系统数据的一致性,这次主要是修复一个楼层信息不同步的bug,问题是更新的时候有个状态字段被写死了,没有用kafka里消息体里的数据更新,这个小问题上周就修复了,自测通过了的,这周测试复测结果发现什么操作都同步不了.这使我一度陷入了自我怀疑.... 然后去看监控的服务日志,发现确实是什么数据库的变更都检测不到了,因为不熟悉这套东西,看了半天也没看出来问题,只能求助我们头儿,然后据他说这个问题经常出现,然后就删了canal实例里面的meta.bat文件,然后运行./restart.sh,重启之后果然恢复正常了,具体啥原因我还没搞懂,日后研究明白了再补充

后续:canal提供了tsdb时序数据库,上次碰到的canal报错问题,是因为监控表的元数据跟现有数据不一致导致的,tsdb中会记录所有对监控的表的操作记录,但是有一张表加了两个字段(57+2)后,这两条alter语句不知道什么情况没有被记录到tsdb的history表中,导致监控到的表的列数不匹配,canal认为还是57列,实际为59,由于是测试环境,所以直接把这张表copy了一份然后把原表删了,再把copy的表改个名字就ok了

`,12),e=[o];function t(c,r,E,y,i,d){return n(),a("div",null,e)}const h=s(p,[["render",t]]);export{F as __pageData,h as default}; diff --git "a/assets/java_others_ribbon\345\210\267\346\226\260\346\234\215\345\212\241\345\210\227\350\241\250\351\227\264\351\232\224\345\222\214canal\347\232\204\345\235\221.md.98a4b036.lean.js" "b/assets/java_others_ribbon\345\210\267\346\226\260\346\234\215\345\212\241\345\210\227\350\241\250\351\227\264\351\232\224\345\222\214canal\347\232\204\345\235\221.md.98a4b036.lean.js" new file mode 100644 index 000000000..d431c3d19 --- /dev/null +++ "b/assets/java_others_ribbon\345\210\267\346\226\260\346\234\215\345\212\241\345\210\227\350\241\250\351\227\264\351\232\224\345\222\214canal\347\232\204\345\235\221.md.98a4b036.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const F=JSON.parse('{"title":"ribbon刷新服务列表间隔和canal的坑","description":"","frontmatter":{},"headers":[],"relativePath":"java/others/ribbon刷新服务列表间隔和canal的坑.md","filePath":"java/others/ribbon刷新服务列表间隔和canal的坑.md","lastUpdated":1708510102000}'),p={name:"java/others/ribbon刷新服务列表间隔和canal的坑.md"},o=l("",12),e=[o];function t(c,r,E,y,i,d){return n(),a("div",null,e)}const h=s(p,[["render",t]]);export{F as __pageData,h as default}; diff --git "a/assets/java_others_\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217\345\212\240\345\257\206\346\225\260\346\215\256\345\257\271\347\247\260\350\247\243\345\257\206\345\267\245\345\205\267\347\261\273.md.41297ec5.js" "b/assets/java_others_\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217\345\212\240\345\257\206\346\225\260\346\215\256\345\257\271\347\247\260\350\247\243\345\257\206\345\267\245\345\205\267\347\261\273.md.41297ec5.js" new file mode 100644 index 000000000..1fa506d0e --- /dev/null +++ "b/assets/java_others_\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217\345\212\240\345\257\206\346\225\260\346\215\256\345\257\271\347\247\260\350\247\243\345\257\206\345\267\245\345\205\267\347\261\273.md.41297ec5.js" @@ -0,0 +1,131 @@ +import{_ as s,o as a,c as n,Q as p}from"./chunks/framework.dc1f4608.js";const A=JSON.parse('{"title":"微信小程序加密数据对称解密工具类","description":"","frontmatter":{},"headers":[],"relativePath":"java/others/微信小程序加密数据对称解密工具类.md","filePath":"java/others/微信小程序加密数据对称解密工具类.md","lastUpdated":1708510102000}'),l={name:"java/others/微信小程序加密数据对称解密工具类.md"},o=p(`

微信小程序加密数据对称解密工具类

背景

小程序授权登录获取用户手机号功能

依赖

xml
<dependency>
+    <groupId>org.bouncycastle</groupId>
+    <artifactId>bcprov-jdk16</artifactId>
+    <version>1.46</version>
+</dependency>
+ 
+ <dependency>
+    <groupId>commons-codec</groupId>
+    <artifactId>commons-codec</artifactId>
+    <version>1.4</version>
+</dependency>
<dependency>
+    <groupId>org.bouncycastle</groupId>
+    <artifactId>bcprov-jdk16</artifactId>
+    <version>1.46</version>
+</dependency>
+ 
+ <dependency>
+    <groupId>commons-codec</groupId>
+    <artifactId>commons-codec</artifactId>
+    <version>1.4</version>
+</dependency>

代码

java
import com.alibaba.fastjson.JSON;
+import org.apache.commons.codec.binary.Base64;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.Security;
+import java.security.spec.AlgorithmParameterSpec;
+import java.util.Map;
+
+/**
+ * @author storyxc
+ * @description 微信敏感数据对称解密工具类
+ * @create 2021/4/30 15:32
+ */
+public class WeixinDecryptUtils {
+
+
+    /**
+     * 微信加密数据对称解密
+     * @param appId       公众号/小程序id
+     * @param encryptData   加密数据
+     * @param iv          加密初始向量
+     * @param sessionKey    会话密钥
+     * @return              解密数据
+     */
+    public static Map<String,Object> decrypt(String appId,String sessionKey,String encryptData,String iv)
+            throws Exception{
+        byte[] decodeEncryptData = Base64.decodeBase64(encryptData);
+        byte[] decodeIv = Base64.decodeBase64(iv);
+        byte[] decodeSessionKey = Base64.decodeBase64(sessionKey);
+        Security.addProvider(new BouncyCastleProvider());
+        AlgorithmParameterSpec ivSpec = new IvParameterSpec(decodeIv);
+        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding","BC");
+        SecretKeySpec keySpec = new SecretKeySpec(decodeSessionKey, "AES");
+        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
+        byte[] doFinal = cipher.doFinal(decodeEncryptData);
+        String str = new String(doFinal);
+        Map map = JSON.parseObject(str, Map.class);
+        Map<String,Object> watermark = (Map<String, Object>) map.get("watermark");
+        if (watermark != null && !appId.equals(watermark.get("appid"))) {
+            throw new RunException(500,"Invalid encrpytedData watermark appId "+appId+",parsed appID " +(String)watermark.get("appid"));
+        }
+        return map;
+    }
+
+    public static void main(String[] args) throws Exception{
+        String appId = "wx4f4bc4dec97d474b";
+        String sessionKey = "tiihtNczf5v6AKRyjwEUhQ==";
+        String encryptedData = "CiyLU1Aw2KjvrjMdj8YKliAjtP4gsMZMQmRzooG2xrDcvSnxIMXFufNstNGTyaGS9uT5geRa0W4oTOb1WT7fJlAC+oNPdbB+3hVbJSRgv+4lGOETKUQz6OYStslQ142dNCuabNPGBzlooOmB231qMM85d2/fV6ChevvXvQP8Hkue1poOFtnEtpyxVLW1zAo6/1Xx1COxFvrc2d7UL/lmHInNlxuacJXwu0fjpXfz/YqYzBIBzD6WUfTIF9GRHpOn/Hz7saL8xz+W//FRAUid1OksQaQx4CMs8LOddcQhULW4ucetDf96JcR3g0gfRK4PC7E/r7Z6xNrXd2UIeorGj5Ef7b1pJAYB6Y5anaHqZ9J6nKEBvB4DnNLIVWSgARns/8wR2SiRS7MNACwTyrGvt9ts8p12PKFdlqYTopNHR1Vf7XjfhQlVsAJdNiKdYmYVoKlaRv85IfVunYzO0IKXsyl7JCUjCpoG20f0a04COwfneQAGGwd5oa+T8yO5hzuyDb/XcxxmK01EpqOyuxINew==";
+        String iv = "r7BXXKkLb8qrSNn05n0qiA==";
+        Map<String, Object> decode = decrypt(appId, sessionKey, encryptedData, iv);
+        System.out.println(JSON.toJSONString(decode));
+    }
+}
import com.alibaba.fastjson.JSON;
+import org.apache.commons.codec.binary.Base64;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.Security;
+import java.security.spec.AlgorithmParameterSpec;
+import java.util.Map;
+
+/**
+ * @author storyxc
+ * @description 微信敏感数据对称解密工具类
+ * @create 2021/4/30 15:32
+ */
+public class WeixinDecryptUtils {
+
+
+    /**
+     * 微信加密数据对称解密
+     * @param appId       公众号/小程序id
+     * @param encryptData   加密数据
+     * @param iv          加密初始向量
+     * @param sessionKey    会话密钥
+     * @return              解密数据
+     */
+    public static Map<String,Object> decrypt(String appId,String sessionKey,String encryptData,String iv)
+            throws Exception{
+        byte[] decodeEncryptData = Base64.decodeBase64(encryptData);
+        byte[] decodeIv = Base64.decodeBase64(iv);
+        byte[] decodeSessionKey = Base64.decodeBase64(sessionKey);
+        Security.addProvider(new BouncyCastleProvider());
+        AlgorithmParameterSpec ivSpec = new IvParameterSpec(decodeIv);
+        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding","BC");
+        SecretKeySpec keySpec = new SecretKeySpec(decodeSessionKey, "AES");
+        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
+        byte[] doFinal = cipher.doFinal(decodeEncryptData);
+        String str = new String(doFinal);
+        Map map = JSON.parseObject(str, Map.class);
+        Map<String,Object> watermark = (Map<String, Object>) map.get("watermark");
+        if (watermark != null && !appId.equals(watermark.get("appid"))) {
+            throw new RunException(500,"Invalid encrpytedData watermark appId "+appId+",parsed appID " +(String)watermark.get("appid"));
+        }
+        return map;
+    }
+
+    public static void main(String[] args) throws Exception{
+        String appId = "wx4f4bc4dec97d474b";
+        String sessionKey = "tiihtNczf5v6AKRyjwEUhQ==";
+        String encryptedData = "CiyLU1Aw2KjvrjMdj8YKliAjtP4gsMZMQmRzooG2xrDcvSnxIMXFufNstNGTyaGS9uT5geRa0W4oTOb1WT7fJlAC+oNPdbB+3hVbJSRgv+4lGOETKUQz6OYStslQ142dNCuabNPGBzlooOmB231qMM85d2/fV6ChevvXvQP8Hkue1poOFtnEtpyxVLW1zAo6/1Xx1COxFvrc2d7UL/lmHInNlxuacJXwu0fjpXfz/YqYzBIBzD6WUfTIF9GRHpOn/Hz7saL8xz+W//FRAUid1OksQaQx4CMs8LOddcQhULW4ucetDf96JcR3g0gfRK4PC7E/r7Z6xNrXd2UIeorGj5Ef7b1pJAYB6Y5anaHqZ9J6nKEBvB4DnNLIVWSgARns/8wR2SiRS7MNACwTyrGvt9ts8p12PKFdlqYTopNHR1Vf7XjfhQlVsAJdNiKdYmYVoKlaRv85IfVunYzO0IKXsyl7JCUjCpoG20f0a04COwfneQAGGwd5oa+T8yO5hzuyDb/XcxxmK01EpqOyuxINew==";
+        String iv = "r7BXXKkLb8qrSNn05n0qiA==";
+        Map<String, Object> decode = decrypt(appId, sessionKey, encryptedData, iv);
+        System.out.println(JSON.toJSONString(decode));
+    }
+}
`,7),e=[o];function t(c,r,y,E,i,d){return a(),n("div",null,e)}const D=s(l,[["render",t]]);export{A as __pageData,D as default}; diff --git "a/assets/java_others_\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217\345\212\240\345\257\206\346\225\260\346\215\256\345\257\271\347\247\260\350\247\243\345\257\206\345\267\245\345\205\267\347\261\273.md.41297ec5.lean.js" "b/assets/java_others_\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217\345\212\240\345\257\206\346\225\260\346\215\256\345\257\271\347\247\260\350\247\243\345\257\206\345\267\245\345\205\267\347\261\273.md.41297ec5.lean.js" new file mode 100644 index 000000000..44e722085 --- /dev/null +++ "b/assets/java_others_\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217\345\212\240\345\257\206\346\225\260\346\215\256\345\257\271\347\247\260\350\247\243\345\257\206\345\267\245\345\205\267\347\261\273.md.41297ec5.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as p}from"./chunks/framework.dc1f4608.js";const A=JSON.parse('{"title":"微信小程序加密数据对称解密工具类","description":"","frontmatter":{},"headers":[],"relativePath":"java/others/微信小程序加密数据对称解密工具类.md","filePath":"java/others/微信小程序加密数据对称解密工具类.md","lastUpdated":1708510102000}'),l={name:"java/others/微信小程序加密数据对称解密工具类.md"},o=p("",7),e=[o];function t(c,r,y,E,i,d){return a(),n("div",null,e)}const D=s(l,[["render",t]]);export{A as __pageData,D as default}; diff --git "a/assets/linux_applications_Canal\351\203\250\347\275\262.md.c251b94e.js" "b/assets/linux_applications_Canal\351\203\250\347\275\262.md.c251b94e.js" new file mode 100644 index 000000000..67c057700 --- /dev/null +++ "b/assets/linux_applications_Canal\351\203\250\347\275\262.md.c251b94e.js" @@ -0,0 +1,637 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const g=JSON.parse('{"title":"canal部署","description":"","frontmatter":{},"headers":[],"relativePath":"linux/applications/Canal部署.md","filePath":"linux/applications/Canal部署.md","lastUpdated":1708510102000}'),p={name:"linux/applications/Canal部署.md"},o=l(`

canal部署

canal官方仓库:https://github.com/alibaba/canal/

wiki:https://github.com/alibaba/canal/wiki

canal的用途是基于mysql的binlog日志解析,提供增量数据的订阅和消费。简单的场景:通过canal监控mysql数据变更从而及时更新redis中对应的缓存或更新es中的文档内容

本文主要介绍canal在服务器端的部署,包括canal-admin,canal-tsdb配置以及instance配置。mysql版本为5.7,系统为macOS14.2.1。

首先需要下载canal的发行版,下载地址:https://github.com/alibaba/canal/releases,可自行选择版本,这里选择1.1.4。

mysql

自建MySQL,需要先开启 Binlog 写入功能,配置 binlog-format 为 ROW 模式,my.cnf 中配置如下

ini
[mysqld]
+log-bin=mysql-bin # 开启 binlog
+binlog-format=ROW # 选择 ROW 模式
+server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复
[mysqld]
+log-bin=mysql-bin # 开启 binlog
+binlog-format=ROW # 选择 ROW 模式
+server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复

canal admin

  • 解压
bash
mkdir -p ~/Downloads/canal/admin && tar -zxvf canal.admin-1.1.4.tar.gz -C ~/Downloads/canal/admin
mkdir -p ~/Downloads/canal/admin && tar -zxvf canal.admin-1.1.4.tar.gz -C ~/Downloads/canal/admin
  • 执行conf文件夹中的canal_manager.sql建表

    shell
    mysql -uroot -p < ~/Downloads/canal/admin/conf/canal_manager.sql
    mysql -uroot -p < ~/Downloads/canal/admin/conf/canal_manager.sql

image-20210617000438523

  • 创建canal用户并授权canal链接 MySQL 账号具有作为 MySQL slave 的权限

    sql
    CREATE USER canal IDENTIFIED BY 'canal'; 
    +GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
    +-- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ;
    +FLUSH PRIVILEGES;
    CREATE USER canal IDENTIFIED BY 'canal'; 
    +GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
    +-- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ;
    +FLUSH PRIVILEGES;

修改conf文件夹中的application.yml

image-20231223000510198

  • 执行admin/bin目录的startup.sh

  • 访问8089端口

    image-20210617001200010

  • 使用默认账号密码 admin/123456即可登录

    TIP

    application.yml中的canal.adminUser和canal.adminPasswd并非WebUI的登录用户名和密码,而是和canal-server交互用的

    image-20210617001457272

canal-admin的核心模型主要有:

  1. instance,对应canal-server里的instance,一个最小的订阅mysql的队列
  2. server,对应canal-server,一个server里可以包含多个instance
  3. 集群,对应一组canal-server,组合在一起面向高可用HA的运维

简单解释:

  1. instance是最原始的业务订阅诉求,它会和 server/集群 这两个面向资源服务属性的进行关联,比如instance A绑定到server A上或者集群 A上,
  2. 有了任务和资源的绑定关系后,对应的资源服务就会接收到这个任务配置,在对应的资源上动态加载instance,并提供服务
  • 动态加载的过程,对应配置文件中的autoScan配置,只不过基于canal-admin之后可就以变为远程的web操作,而不需要在机器上运维配置文件
  1. 将server抽象成资源之后,原本canal-server运行所需要的canal.properties/instance.properties配置文件就需要在web ui上进行统一运维,每个server只需要以最基本的启动配置 (比如知道一下canal-admin的manager地址,以及访问配置的账号、密码即可)
  • 新建server,按照图中配置即可

    image-20231223001200351

    配置项:

    • 所属集群,可以选择为单机 或者 集群。一般单机Server的模式主要用于一次性的任务或者测试任务
    • Server名称,唯一即可,方便自己记忆
    • Server Ip,机器ip
    • admin端口,canal 1.1.4版本新增的能力,会在canal-server上提供远程管理操作,默认值11110
    • tcp端口,canal提供netty数据订阅服务的端口
    • metric端口, promethues的exporter监控数据端口 (未来会对接监控)

server端配置

  • 修改为kafka模式并配置kafka相关参数
  • tsdb改为mysql
properties
#################################################
+######### 		common argument		#############
+#################################################
+# tcp bind ip
+canal.ip =
+# register ip to zookeeper
+canal.register.ip =
+canal.port = 11111
+canal.metrics.pull.port = 11112
+# canal instance user/passwd
+canal.user = canal
+canal.passwd = E3619321C1A937C46A0D8BD1DAC39F93B27D4458
+
+# canal admin config
+canal.admin.manager = 127.0.0.1:8089
+canal.admin.port = 11110
+canal.admin.user = admin
+canal.admin.passwd = 4ACFE3202A5FF5CF467898FC58AAB1D615029441
+
+canal.zkServers =
+# flush data to zk
+canal.zookeeper.flush.period = 1000
+canal.withoutNetty = false
+# tcp, kafka, RocketMQ
+canal.serverMode = kafka
+# flush meta cursor/parse position to file
+canal.file.data.dir = \${canal.conf.dir}
+canal.file.flush.period = 1000
+## memory store RingBuffer size, should be Math.pow(2,n)
+canal.instance.memory.buffer.size = 16384
+## memory store RingBuffer used memory unit size , default 1kb
+canal.instance.memory.buffer.memunit = 1024 
+## meory store gets mode used MEMSIZE or ITEMSIZE
+canal.instance.memory.batch.mode = MEMSIZE
+canal.instance.memory.rawEntry = true
+
+## detecing config
+canal.instance.detecting.enable = false
+#canal.instance.detecting.sql = insert into retl.xdual values(1,now()) on duplicate key update x=now()
+canal.instance.detecting.sql = select 1
+canal.instance.detecting.interval.time = 3
+canal.instance.detecting.retry.threshold = 3
+canal.instance.detecting.heartbeatHaEnable = false
+
+# support maximum transaction size, more than the size of the transaction will be cut into multiple transactions delivery
+canal.instance.transaction.size =  1024
+# mysql fallback connected to new master should fallback times
+canal.instance.fallbackIntervalInSeconds = 60
+
+# network config
+canal.instance.network.receiveBufferSize = 16384
+canal.instance.network.sendBufferSize = 16384
+canal.instance.network.soTimeout = 30
+
+# binlog filter config
+canal.instance.filter.druid.ddl = true
+canal.instance.filter.query.dcl = false
+canal.instance.filter.query.dml = false
+canal.instance.filter.query.ddl = false
+canal.instance.filter.table.error = false
+canal.instance.filter.rows = false
+canal.instance.filter.transaction.entry = false
+
+# binlog format/image check
+canal.instance.binlog.format = ROW,STATEMENT,MIXED 
+canal.instance.binlog.image = FULL,MINIMAL,NOBLOB
+
+# binlog ddl isolation
+canal.instance.get.ddl.isolation = false
+
+# parallel parser config
+canal.instance.parser.parallel = true
+## concurrent thread number, default 60% available processors, suggest not to exceed Runtime.getRuntime().availableProcessors()
+#canal.instance.parser.parallelThreadSize = 16
+## disruptor ringbuffer size, must be power of 2
+canal.instance.parser.parallelBufferSize = 256
+
+# table meta tsdb info
+canal.instance.tsdb.enable = true
+#canal.instance.tsdb.dir = \${canal.file.data.dir:../conf}/\${canal.instance.destination:}
+#canal.instance.tsdb.url = jdbc:h2:\${canal.instance.tsdb.dir}/h2;CACHE_SIZE=1000;MODE=MYSQL;
+canal.instance.tsdb.dbUsername = canal
+canal.instance.tsdb.dbPassword = canal
+# dump snapshot interval, default 24 hour
+canal.instance.tsdb.snapshot.interval = 24
+# purge snapshot expire , default 360 hour(15 days)
+canal.instance.tsdb.snapshot.expire = 360
+
+# aliyun ak/sk , support rds/mq
+canal.aliyun.accessKey =
+canal.aliyun.secretKey =
+
+#################################################
+######### 		destinations		#############
+#################################################
+canal.destinations =
+# conf root dir
+canal.conf.dir = ../conf
+# auto scan instance dir add/remove and start/stop instance
+canal.auto.scan = true
+canal.auto.scan.interval = 5
+
+#canal.instance.tsdb.spring.xml = classpath:spring/tsdb/h2-tsdb.xml
+canal.instance.tsdb.spring.xml = classpath:spring/tsdb/mysql-tsdb.xml
+
+canal.instance.global.mode = manager
+canal.instance.global.lazy = false
+canal.instance.global.manager.address = \${canal.admin.manager}
+#canal.instance.global.spring.xml = classpath:spring/memory-instance.xml
+canal.instance.global.spring.xml = classpath:spring/file-instance.xml
+#canal.instance.global.spring.xml = classpath:spring/default-instance.xml
+
+##################################################
+######### 		     MQ 		     #############
+##################################################
+canal.mq.servers = 127.0.0.1:9092 
+canal.mq.retries = 0
+canal.mq.batchSize = 16384
+canal.mq.maxRequestSize = 1048576
+canal.mq.lingerMs = 100
+canal.mq.bufferMemory = 33554432
+canal.mq.canalBatchSize = 50
+canal.mq.canalGetTimeout = 100
+canal.mq.flatMessage = true
+canal.mq.compressionType = none
+canal.mq.acks = all
+#canal.mq.properties. =
+canal.mq.producerGroup = canal-test
+# Set this value to "cloud", if you want open message trace feature in aliyun.
+canal.mq.accessChannel = local
+# aliyun mq namespace
+#canal.mq.namespace =
+
+##################################################
+#########     Kafka Kerberos Info    #############
+##################################################
+canal.mq.kafka.kerberos.enable = false
+canal.mq.kafka.kerberos.krb5FilePath = "../conf/kerberos/krb5.conf"
+canal.mq.kafka.kerberos.jaasFilePath = "../conf/kerberos/jaas.conf"
#################################################
+######### 		common argument		#############
+#################################################
+# tcp bind ip
+canal.ip =
+# register ip to zookeeper
+canal.register.ip =
+canal.port = 11111
+canal.metrics.pull.port = 11112
+# canal instance user/passwd
+canal.user = canal
+canal.passwd = E3619321C1A937C46A0D8BD1DAC39F93B27D4458
+
+# canal admin config
+canal.admin.manager = 127.0.0.1:8089
+canal.admin.port = 11110
+canal.admin.user = admin
+canal.admin.passwd = 4ACFE3202A5FF5CF467898FC58AAB1D615029441
+
+canal.zkServers =
+# flush data to zk
+canal.zookeeper.flush.period = 1000
+canal.withoutNetty = false
+# tcp, kafka, RocketMQ
+canal.serverMode = kafka
+# flush meta cursor/parse position to file
+canal.file.data.dir = \${canal.conf.dir}
+canal.file.flush.period = 1000
+## memory store RingBuffer size, should be Math.pow(2,n)
+canal.instance.memory.buffer.size = 16384
+## memory store RingBuffer used memory unit size , default 1kb
+canal.instance.memory.buffer.memunit = 1024 
+## meory store gets mode used MEMSIZE or ITEMSIZE
+canal.instance.memory.batch.mode = MEMSIZE
+canal.instance.memory.rawEntry = true
+
+## detecing config
+canal.instance.detecting.enable = false
+#canal.instance.detecting.sql = insert into retl.xdual values(1,now()) on duplicate key update x=now()
+canal.instance.detecting.sql = select 1
+canal.instance.detecting.interval.time = 3
+canal.instance.detecting.retry.threshold = 3
+canal.instance.detecting.heartbeatHaEnable = false
+
+# support maximum transaction size, more than the size of the transaction will be cut into multiple transactions delivery
+canal.instance.transaction.size =  1024
+# mysql fallback connected to new master should fallback times
+canal.instance.fallbackIntervalInSeconds = 60
+
+# network config
+canal.instance.network.receiveBufferSize = 16384
+canal.instance.network.sendBufferSize = 16384
+canal.instance.network.soTimeout = 30
+
+# binlog filter config
+canal.instance.filter.druid.ddl = true
+canal.instance.filter.query.dcl = false
+canal.instance.filter.query.dml = false
+canal.instance.filter.query.ddl = false
+canal.instance.filter.table.error = false
+canal.instance.filter.rows = false
+canal.instance.filter.transaction.entry = false
+
+# binlog format/image check
+canal.instance.binlog.format = ROW,STATEMENT,MIXED 
+canal.instance.binlog.image = FULL,MINIMAL,NOBLOB
+
+# binlog ddl isolation
+canal.instance.get.ddl.isolation = false
+
+# parallel parser config
+canal.instance.parser.parallel = true
+## concurrent thread number, default 60% available processors, suggest not to exceed Runtime.getRuntime().availableProcessors()
+#canal.instance.parser.parallelThreadSize = 16
+## disruptor ringbuffer size, must be power of 2
+canal.instance.parser.parallelBufferSize = 256
+
+# table meta tsdb info
+canal.instance.tsdb.enable = true
+#canal.instance.tsdb.dir = \${canal.file.data.dir:../conf}/\${canal.instance.destination:}
+#canal.instance.tsdb.url = jdbc:h2:\${canal.instance.tsdb.dir}/h2;CACHE_SIZE=1000;MODE=MYSQL;
+canal.instance.tsdb.dbUsername = canal
+canal.instance.tsdb.dbPassword = canal
+# dump snapshot interval, default 24 hour
+canal.instance.tsdb.snapshot.interval = 24
+# purge snapshot expire , default 360 hour(15 days)
+canal.instance.tsdb.snapshot.expire = 360
+
+# aliyun ak/sk , support rds/mq
+canal.aliyun.accessKey =
+canal.aliyun.secretKey =
+
+#################################################
+######### 		destinations		#############
+#################################################
+canal.destinations =
+# conf root dir
+canal.conf.dir = ../conf
+# auto scan instance dir add/remove and start/stop instance
+canal.auto.scan = true
+canal.auto.scan.interval = 5
+
+#canal.instance.tsdb.spring.xml = classpath:spring/tsdb/h2-tsdb.xml
+canal.instance.tsdb.spring.xml = classpath:spring/tsdb/mysql-tsdb.xml
+
+canal.instance.global.mode = manager
+canal.instance.global.lazy = false
+canal.instance.global.manager.address = \${canal.admin.manager}
+#canal.instance.global.spring.xml = classpath:spring/memory-instance.xml
+canal.instance.global.spring.xml = classpath:spring/file-instance.xml
+#canal.instance.global.spring.xml = classpath:spring/default-instance.xml
+
+##################################################
+######### 		     MQ 		     #############
+##################################################
+canal.mq.servers = 127.0.0.1:9092 
+canal.mq.retries = 0
+canal.mq.batchSize = 16384
+canal.mq.maxRequestSize = 1048576
+canal.mq.lingerMs = 100
+canal.mq.bufferMemory = 33554432
+canal.mq.canalBatchSize = 50
+canal.mq.canalGetTimeout = 100
+canal.mq.flatMessage = true
+canal.mq.compressionType = none
+canal.mq.acks = all
+#canal.mq.properties. =
+canal.mq.producerGroup = canal-test
+# Set this value to "cloud", if you want open message trace feature in aliyun.
+canal.mq.accessChannel = local
+# aliyun mq namespace
+#canal.mq.namespace =
+
+##################################################
+#########     Kafka Kerberos Info    #############
+##################################################
+canal.mq.kafka.kerberos.enable = false
+canal.mq.kafka.kerberos.krb5FilePath = "../conf/kerberos/krb5.conf"
+canal.mq.kafka.kerberos.jaasFilePath = "../conf/kerberos/jaas.conf"

TIP

canal.auto.scan如果设置为true,canal.destinations可以不填写,server会自动扫描instance然后启动

java
// CanalController
+// 初始化monitor机制
+autoScan = BooleanUtils.toBoolean(getProperty(properties, CanalConstants.CANAL_AUTO_SCAN));
+if (autoScan) {
+    defaultAction = new InstanceAction() {
+        public void start(String destination) {
+            InstanceConfig config = instanceConfigs.get(destination);
+            if (config == null) {
+                // 重新读取一下instance config
+                config = parseInstanceConfig(properties, destination);
+                instanceConfigs.put(destination, config);
+            }
+            if (!embededCanalServer.isStart(destination)) {
+                // HA机制启动
+                ServerRunningMonitor runningMonitor = ServerRunningMonitors.getRunningMonitor(destination);
+                if (!config.getLazy() && !runningMonitor.isStart()) {
+                    runningMonitor.start();
+                }
+            }
+            logger.info("auto notify start {} successful.", destination);
+        }
+        //...
+    }
+}
+
+instanceConfigMonitors = MigrateMap.makeComputingMap(new Function<InstanceMode, InstanceConfigMonitor>() {
+
+    public InstanceConfigMonitor apply(InstanceMode mode) {
+        int scanInterval = Integer.valueOf(getProperty(properties,
+            CanalConstants.CANAL_AUTO_SCAN_INTERVAL,
+            "5"));
+
+        if (mode.isSpring()) {
+            SpringInstanceConfigMonitor monitor = new SpringInstanceConfigMonitor();
+            monitor.setScanIntervalInSecond(scanInterval);
+            monitor.setDefaultAction(defaultAction);
+            // 设置conf目录,默认是user.dir + conf目录组成
+            String rootDir = getProperty(properties, CanalConstants.CANAL_CONF_DIR);
+            if (StringUtils.isEmpty(rootDir)) {
+                rootDir = "../conf";
+            }
+
+            if (StringUtils.equals("otter-canal", System.getProperty("appName"))) {
+                monitor.setRootConf(rootDir);
+            } else {
+                // eclipse debug模式
+                monitor.setRootConf("src/main/resources/");
+            }
+            return monitor;
+        } else if (mode.isManager()) {
+            ManagerInstanceConfigMonitor monitor = new ManagerInstanceConfigMonitor();
+            monitor.setScanIntervalInSecond(scanInterval);
+            monitor.setDefaultAction(defaultAction);
+            String managerAddress = getProperty(properties, CanalConstants.CANAL_ADMIN_MANAGER);
+            monitor.setConfigClient(getManagerClient(managerAddress));
+            return monitor;
+        } else {
+            throw new UnsupportedOperationException("unknow mode :" + mode + " for monitor");
+        }
+    }
+});
+
+
+// CanalController.start()
+public void start() throws Throwable {
+    // ...
+    // 尝试启动一下非lazy状态的通道
+    for (Map.Entry<String, InstanceConfig> entry : instanceConfigs.entrySet()) {
+        final String destination = entry.getKey();
+        InstanceConfig config = entry.getValue();
+        // 创建destination的工作节点
+        if (!embededCanalServer.isStart(destination)) {
+            // HA机制启动
+            ServerRunningMonitor runningMonitor = ServerRunningMonitors.getRunningMonitor(destination);
+            if (!config.getLazy() && !runningMonitor.isStart()) {
+                runningMonitor.start();
+            }
+        }
+
+        if (autoScan) {
+            instanceConfigMonitors.get(config.getMode()).register(destination, defaultAction);
+        }
+    }
+
+    if (autoScan) {
+        instanceConfigMonitors.get(globalInstanceConfig.getMode()).start();
+        for (InstanceConfigMonitor monitor : instanceConfigMonitors.values()) {
+            if (!monitor.isStart()) {
+                monitor.start();
+            }
+        }
+    }
+		// ...
+}
+
+// 然后会调用ManagerInstanceConfigMonitor的start方法,start方法会启动一个定时任务,每隔scanInterval秒调用scan方法
+public void start() {
+    super.start();
+    executor.scheduleWithFixedDelay(new Runnable() {
+
+        public void run() {
+            try {
+                scan();
+                if (isFirst) {
+                    isFirst = false;
+                }
+            } catch (Throwable e) {
+                logger.error("scan failed", e);
+            }
+        }
+
+    }, 0, scanIntervalInSecond, TimeUnit.SECONDS);
+}
+
+// scan方法中会通过configClient调用canal-admin的接口获取instance的配置信息,
+// 最后对instance进行stop/reload/start操作
+
+private void scan() {
+    String instances = configClient.findInstances(null);
+    final List<String> is = Lists.newArrayList(StringUtils.split(instances, ','));
+    List<String> start = Lists.newArrayList();
+    List<String> stop = Lists.newArrayList();
+    List<String> restart = Lists.newArrayList();
+    for (String instance : is) {
+        if (!configs.containsKey(instance)) {
+            PlainCanal newPlainCanal = configClient.findInstance(instance, null);
+            if (newPlainCanal != null) {
+                configs.put(instance, newPlainCanal);
+                start.add(instance);
+            }
+        } else {
+            PlainCanal plainCanal = configs.get(instance);
+            PlainCanal newPlainCanal = configClient.findInstance(instance, plainCanal.getMd5());
+            if (newPlainCanal != null) {
+                // 配置有变化
+                restart.add(instance);
+                configs.put(instance, newPlainCanal);
+            }
+        }
+    }
+
+    configs.forEach((instance, plainCanal) -> {
+        if (!is.contains(instance)) {
+            stop.add(instance);
+        }
+    });
+
+    stop.forEach(instance -> {
+        notifyStop(instance);
+    });
+
+    restart.forEach(instance -> {
+        notifyReload(instance);
+    });
+
+    start.forEach(instance -> {
+        notifyStart(instance);
+    });
+
+}
// CanalController
+// 初始化monitor机制
+autoScan = BooleanUtils.toBoolean(getProperty(properties, CanalConstants.CANAL_AUTO_SCAN));
+if (autoScan) {
+    defaultAction = new InstanceAction() {
+        public void start(String destination) {
+            InstanceConfig config = instanceConfigs.get(destination);
+            if (config == null) {
+                // 重新读取一下instance config
+                config = parseInstanceConfig(properties, destination);
+                instanceConfigs.put(destination, config);
+            }
+            if (!embededCanalServer.isStart(destination)) {
+                // HA机制启动
+                ServerRunningMonitor runningMonitor = ServerRunningMonitors.getRunningMonitor(destination);
+                if (!config.getLazy() && !runningMonitor.isStart()) {
+                    runningMonitor.start();
+                }
+            }
+            logger.info("auto notify start {} successful.", destination);
+        }
+        //...
+    }
+}
+
+instanceConfigMonitors = MigrateMap.makeComputingMap(new Function<InstanceMode, InstanceConfigMonitor>() {
+
+    public InstanceConfigMonitor apply(InstanceMode mode) {
+        int scanInterval = Integer.valueOf(getProperty(properties,
+            CanalConstants.CANAL_AUTO_SCAN_INTERVAL,
+            "5"));
+
+        if (mode.isSpring()) {
+            SpringInstanceConfigMonitor monitor = new SpringInstanceConfigMonitor();
+            monitor.setScanIntervalInSecond(scanInterval);
+            monitor.setDefaultAction(defaultAction);
+            // 设置conf目录,默认是user.dir + conf目录组成
+            String rootDir = getProperty(properties, CanalConstants.CANAL_CONF_DIR);
+            if (StringUtils.isEmpty(rootDir)) {
+                rootDir = "../conf";
+            }
+
+            if (StringUtils.equals("otter-canal", System.getProperty("appName"))) {
+                monitor.setRootConf(rootDir);
+            } else {
+                // eclipse debug模式
+                monitor.setRootConf("src/main/resources/");
+            }
+            return monitor;
+        } else if (mode.isManager()) {
+            ManagerInstanceConfigMonitor monitor = new ManagerInstanceConfigMonitor();
+            monitor.setScanIntervalInSecond(scanInterval);
+            monitor.setDefaultAction(defaultAction);
+            String managerAddress = getProperty(properties, CanalConstants.CANAL_ADMIN_MANAGER);
+            monitor.setConfigClient(getManagerClient(managerAddress));
+            return monitor;
+        } else {
+            throw new UnsupportedOperationException("unknow mode :" + mode + " for monitor");
+        }
+    }
+});
+
+
+// CanalController.start()
+public void start() throws Throwable {
+    // ...
+    // 尝试启动一下非lazy状态的通道
+    for (Map.Entry<String, InstanceConfig> entry : instanceConfigs.entrySet()) {
+        final String destination = entry.getKey();
+        InstanceConfig config = entry.getValue();
+        // 创建destination的工作节点
+        if (!embededCanalServer.isStart(destination)) {
+            // HA机制启动
+            ServerRunningMonitor runningMonitor = ServerRunningMonitors.getRunningMonitor(destination);
+            if (!config.getLazy() && !runningMonitor.isStart()) {
+                runningMonitor.start();
+            }
+        }
+
+        if (autoScan) {
+            instanceConfigMonitors.get(config.getMode()).register(destination, defaultAction);
+        }
+    }
+
+    if (autoScan) {
+        instanceConfigMonitors.get(globalInstanceConfig.getMode()).start();
+        for (InstanceConfigMonitor monitor : instanceConfigMonitors.values()) {
+            if (!monitor.isStart()) {
+                monitor.start();
+            }
+        }
+    }
+		// ...
+}
+
+// 然后会调用ManagerInstanceConfigMonitor的start方法,start方法会启动一个定时任务,每隔scanInterval秒调用scan方法
+public void start() {
+    super.start();
+    executor.scheduleWithFixedDelay(new Runnable() {
+
+        public void run() {
+            try {
+                scan();
+                if (isFirst) {
+                    isFirst = false;
+                }
+            } catch (Throwable e) {
+                logger.error("scan failed", e);
+            }
+        }
+
+    }, 0, scanIntervalInSecond, TimeUnit.SECONDS);
+}
+
+// scan方法中会通过configClient调用canal-admin的接口获取instance的配置信息,
+// 最后对instance进行stop/reload/start操作
+
+private void scan() {
+    String instances = configClient.findInstances(null);
+    final List<String> is = Lists.newArrayList(StringUtils.split(instances, ','));
+    List<String> start = Lists.newArrayList();
+    List<String> stop = Lists.newArrayList();
+    List<String> restart = Lists.newArrayList();
+    for (String instance : is) {
+        if (!configs.containsKey(instance)) {
+            PlainCanal newPlainCanal = configClient.findInstance(instance, null);
+            if (newPlainCanal != null) {
+                configs.put(instance, newPlainCanal);
+                start.add(instance);
+            }
+        } else {
+            PlainCanal plainCanal = configs.get(instance);
+            PlainCanal newPlainCanal = configClient.findInstance(instance, plainCanal.getMd5());
+            if (newPlainCanal != null) {
+                // 配置有变化
+                restart.add(instance);
+                configs.put(instance, newPlainCanal);
+            }
+        }
+    }
+
+    configs.forEach((instance, plainCanal) -> {
+        if (!is.contains(instance)) {
+            stop.add(instance);
+        }
+    });
+
+    stop.forEach(instance -> {
+        notifyStop(instance);
+    });
+
+    restart.forEach(instance -> {
+        notifyReload(instance);
+    });
+
+    start.forEach(instance -> {
+        notifyStart(instance);
+    });
+
+}

canal deployer

  • 解压
bash
mkdir -p ~/Downloads/canal/deployer && tar -zxvf canal.deployer-1.1.4.tar.gz -C ~/Downloads/canal/deployer
mkdir -p ~/Downloads/canal/deployer && tar -zxvf canal.deployer-1.1.4.tar.gz -C ~/Downloads/canal/deployer
  • 用conf目录下的canal_local.properties替换canal.properties

  • 修改配置

    properties
    # canal.ip
    +canal.ip = 127.0.0.1 
    +# register ip
    +canal.register.ip = 127.0.0.1
    # canal.ip
    +canal.ip = 127.0.0.1 
    +# register ip
    +canal.register.ip = 127.0.0.1

    TIP

    1. 注意ip前后不能有空格,不然会无法启动netty server从而无法启动canal server,应该是后台没做trim

    2. 如果不填写canal.ipcanal.register.ip两个配置项,代码中将通过AddressUtils.getHostIp()获取本机的ip地址,如果本地有docker/orbstack等创建的虚拟网络设备会导致启动canal-server后识别到多个server且是不同的ip(docker0网桥或orbstack容器等的ip),比较膈应人。(#issue47)

    源码:

    java
    // CanalController.java
    +
    +if (StringUtils.isEmpty(ip) && StringUtils.isEmpty(registerIp)) {
    +    ip = registerIp = AddressUtils.getHostIp();
    +}
    +
    +if (StringUtils.isEmpty(ip)) {
    +    ip = AddressUtils.getHostIp();
    +}
    +
    +if (StringUtils.isEmpty(registerIp)) {
    +    registerIp = ip; // 兼容以前配置
    +}
    // CanalController.java
    +
    +if (StringUtils.isEmpty(ip) && StringUtils.isEmpty(registerIp)) {
    +    ip = registerIp = AddressUtils.getHostIp();
    +}
    +
    +if (StringUtils.isEmpty(ip)) {
    +    ip = AddressUtils.getHostIp();
    +}
    +
    +if (StringUtils.isEmpty(registerIp)) {
    +    registerIp = ip; // 兼容以前配置
    +}
  • 执行bin目录下的startup.sh

  • 直接在canal admin的webUI界面中配置instance,等待启动即可,如有问题查看deploy/logs/story/story.log

    具体配置项参见wiki

    image-20231223002954201

TIP

如果在server的配置文件中填了相同的配置项,那么instance中的配置会被server中的覆盖,例如canal.instance.tsdb.url配置(#issue4669)

验证

此时我们的实例story已经开始监听story数据库下面的操作,如果有变更,就会推送至kafka的canal-story这个topic中

image-20231223004300829

之后就需要通过业务端根据情况监听队列中的数据变化,做相应的操作。

`,35),e=[o];function c(t,r,i,E,y,d){return n(),a("div",null,e)}const A=s(p,[["render",c]]);export{g as __pageData,A as default}; diff --git "a/assets/linux_applications_Canal\351\203\250\347\275\262.md.c251b94e.lean.js" "b/assets/linux_applications_Canal\351\203\250\347\275\262.md.c251b94e.lean.js" new file mode 100644 index 000000000..4aae91581 --- /dev/null +++ "b/assets/linux_applications_Canal\351\203\250\347\275\262.md.c251b94e.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const g=JSON.parse('{"title":"canal部署","description":"","frontmatter":{},"headers":[],"relativePath":"linux/applications/Canal部署.md","filePath":"linux/applications/Canal部署.md","lastUpdated":1708510102000}'),p={name:"linux/applications/Canal部署.md"},o=l("",35),e=[o];function c(t,r,i,E,y,d){return n(),a("div",null,e)}const A=s(p,[["render",c]]);export{g as __pageData,A as default}; diff --git a/assets/linux_applications_Clash.md.57ceb8e9.js b/assets/linux_applications_Clash.md.57ceb8e9.js new file mode 100644 index 000000000..f587b0f3a --- /dev/null +++ b/assets/linux_applications_Clash.md.57ceb8e9.js @@ -0,0 +1,659 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"Clash","description":"","frontmatter":{},"headers":[],"relativePath":"linux/applications/Clash.md","filePath":"linux/applications/Clash.md","lastUpdated":1708510102000}'),p={name:"linux/applications/Clash.md"},o=l(`

Clash

Repo:https://github.com/Dreamacro/clash

Premium Binary:https://github.com/Dreamacro/clash/releases/tag/premium

Configuration:https://dreamacro.github.io/clash/configuration/configuration-reference.html

配置

  • 下载最新版的premium内核二进制文件

  • 编写配置文件

    yaml
    # /root/.config/clash/config.yaml
    +
    +# (HTTP and SOCKS5 in one port)
    +mixed-port: 7890
    +# RESTful API for clash
    +external-controller: 127.0.0.1:9090
    +secret: 123456
    +allow-lan: false
    +mode: rule
    +log-level: warning
    +
    +tun:
    +  enable: true
    +  stack: system # system gvisor
    +  auto-route: true
    +  auto-detect-interface: true
    +  dns-hijack:
    +    - any:53 # DNS hijacking might result in a failure, if the system DNS is at a private IP address (since auto-route does not capture private network traffic).
    +
    +dns:
    +  enable: true
    +  listen: 0.0.0.0:7874
    +  ipv6: false
    +  default-nameserver:
    +    - 114.114.114.114
    +  nameserver:
    +    - 119.29.29.29
    +    - 223.5.5.5
    +    - 8.8.8.8
    +  fallback:
    +    - https://dns.cloudflare.com/dns-query
    +    - tls://dns.google:853
    +    - https://1.1.1.1/dns-query
    +  enhanced-mode: fake-ip # fake-ip redir-host
    +  fake-ip-range: 198.18.0.1/16
    +  fake-ip-filter:
    +  - "*.lan"
    +  - "*.localdomain"
    +  - "*.example"
    +  - "*.invalid"
    +  - "*.localhost"
    +  - "*.test"
    +  - "*.local"
    +  - "*.home.arpa"
    +  - time.*.com
    +  - time.*.gov
    +  - time.*.edu.cn
    +  - time.*.apple.com
    +  - time-ios.apple.com
    +  - time1.*.com
    +  - time2.*.com
    +  - time3.*.com
    +  - time4.*.com
    +  - time5.*.com
    +  - time6.*.com
    +  - time7.*.com
    +  - ntp.*.com
    +  - ntp1.*.com
    +  - ntp2.*.com
    +  - ntp3.*.com
    +  - ntp4.*.com
    +  - ntp5.*.com
    +  - ntp6.*.com
    +  - ntp7.*.com
    +  - "*.time.edu.cn"
    +  - "*.ntp.org.cn"
    +  - "+.pool.ntp.org"
    +  - time1.cloud.tencent.com
    +  - music.163.com
    +  - "*.music.163.com"
    +  - "*.126.net"
    +  - musicapi.taihe.com
    +  - music.taihe.com
    +  - songsearch.kugou.com
    +  - trackercdn.kugou.com
    +  - "*.kuwo.cn"
    +  - api-jooxtt.sanook.com
    +  - api.joox.com
    +  - joox.com
    +  - y.qq.com
    +  - "*.y.qq.com"
    +  - streamoc.music.tc.qq.com
    +  - mobileoc.music.tc.qq.com
    +  - isure.stream.qqmusic.qq.com
    +  - dl.stream.qqmusic.qq.com
    +  - aqqmusic.tc.qq.com
    +  - amobile.music.tc.qq.com
    +  - "*.xiami.com"
    +  - "*.music.migu.cn"
    +  - music.migu.cn
    +  - "+.msftconnecttest.com"
    +  - "+.msftncsi.com"
    +  - localhost.ptlogin2.qq.com
    +  - localhost.sec.qq.com
    +  - "+.qq.com"
    +  - "+.tencent.com"
    +  - "+.srv.nintendo.net"
    +  - "*.n.n.srv.nintendo.net"
    +  - "+.stun.playstation.net"
    +  - xbox.*.*.microsoft.com
    +  - "*.*.xboxlive.com"
    +  - xbox.*.microsoft.com
    +  - xnotify.xboxlive.com
    +  - "+.battlenet.com.cn"
    +  - "+.wotgame.cn"
    +  - "+.wggames.cn"
    +  - "+.wowsgame.cn"
    +  - "+.wargaming.net"
    +  - proxy.golang.org
    +  - stun.*.*
    +  - stun.*.*.*
    +  - "+.stun.*.*"
    +  - "+.stun.*.*.*"
    +  - "+.stun.*.*.*.*"
    +  - "+.stun.*.*.*.*.*"
    +  - heartbeat.belkin.com
    +  - "*.linksys.com"
    +  - "*.linksyssmartwifi.com"
    +  - mobileoc.music.tc.qq.com
    +  - isure.stream.qqmusic.qq.com
    +  - dl.stream.qqmusic.qq.com
    +  - aqqmusic.tc.qq.com
    +  - amobile.music.tc.qq.com
    +  - "*.xiami.com"
    +  - "*.music.migu.cn"
    +  - music.migu.cn
    +  - "+.msftconnecttest.com"
    +  - "+.msftncsi.com"
    +  - localhost.ptlogin2.qq.com
    +  - localhost.sec.qq.com
    +  - "+.qq.com"
    +  - "+.tencent.com"
    +  - "+.srv.nintendo.net"
    +  - "*.n.n.srv.nintendo.net"
    +  - "+.stun.playstation.net"
    +  - xbox.*.*.microsoft.com
    +  - "*.*.xboxlive.com"
    +  - xbox.*.microsoft.com
    +  - xnotify.xboxlive.com
    +  - "+.battlenet.com.cn"
    +  - "+.wotgame.cn"
    +  - "+.wggames.cn"
    +  - "+.wowsgame.cn"
    +  - "+.wargaming.net"
    +  - proxy.golang.org
    +  - stun.*.*
    +  - stun.*.*.*
    +  - "+.stun.*.*"
    +  - "+.stun.*.*.*"
    +  - "+.stun.*.*.*.*"
    +  - "+.stun.*.*.*.*.*"
    +  - heartbeat.belkin.com
    +  - "*.linksys.com"
    +  - "*.linksyssmartwifi.com"
    +  - "*.router.asus.com"
    +  - mesu.apple.com
    +  - swscan.apple.com
    +  - swquery.apple.com
    +  - swdownload.apple.com
    +  - swcdn.apple.com
    +  - swdist.apple.com
    +  - lens.l.google.com
    +  - stun.l.google.com
    +  - "+.nflxvideo.net"
    +  - "*.square-enix.com"
    +  - "*.finalfantasyxiv.com"
    +  - "*.ffxiv.com"
    +  - "*.ff14.sdo.com"
    +  - ff.dorado.sdo.com
    +  - "*.mcdn.bilivideo.cn"
    +  - "+.media.dssott.com"
    +  - shark007.net
    +  - Mijia Cloud
    +  - "+.cmbchina.com"
    +  - "+.cmbimg.com"
    +  - local.adguard.org
    +  - "+.sandai.net"
    +  - "+.n0808.com"
    +    
    +proxy-providers:
    +  provider:
    +    type: http
    +    path: ./profile/provider.yaml
    +    url: subscription.url
    +    interval: 36000
    +    health-check:
    +      enable: true
    +      url: http://www.gstatic.com/generate_204
    +      interval: 3600
    +
    +proxy-groups:
    +  - name: PROXY
    +    type: select
    +    use:
    +      - provider
    +
    +rule-providers:
    +  reject:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/reject.txt"
    +    path: ./ruleset/reject.yaml
    +    interval: 86400
    +
    +  icloud:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/icloud.txt"
    +    path: ./ruleset/icloud.yaml
    +    interval: 86400
    +
    +  apple:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/apple.txt"
    +    path: ./ruleset/apple.yaml
    +    interval: 86400
    +
    +  # google:
    +  #   type: http
    +  #   behavior: domain
    +  #   url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/google.txt"
    +  #   path: ./ruleset/google.yaml
    +  #   interval: 86400
    +
    +  proxy:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/proxy.txt"
    +    path: ./ruleset/proxy.yaml
    +    interval: 86400
    +
    +  direct:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/direct.txt"
    +    path: ./ruleset/direct.yaml
    +    interval: 86400
    +  private:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/private.txt"
    +    path: ./ruleset/private.yaml
    +    interval: 86400
    +
    +  gfw:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/gfw.txt"
    +    path: ./ruleset/gfw.yaml
    +    interval: 86400
    +
    +  tld-not-cn:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/tld-not-cn.txt"
    +    path: ./ruleset/tld-not-cn.yaml
    +    interval: 86400
    +
    +  telegramcidr:
    +    type: http
    +    behavior: ipcidr
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/telegramcidr.txt"
    +    path: ./ruleset/telegramcidr.yaml
    +    interval: 86400
    +
    +  cncidr:
    +    type: http
    +    behavior: ipcidr
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/cncidr.txt"
    +    path: ./ruleset/cncidr.yaml
    +    interval: 86400
    +
    +  lancidr:
    +    type: http
    +    behavior: ipcidr
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/lancidr.txt"
    +    path: ./ruleset/lancidr.yaml
    +    interval: 86400
    +
    +  applications:
    +    type: http
    +    behavior: classical
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/applications.txt"
    +    path: ./ruleset/applications.yaml
    +    interval: 86400
    +
    +
    +# Script Shortcuts enables the use of script in rules mode. By default, DNS resolution takes place for SCRIPT rules. no-resolve can be appended to the rule to prevent the resolution. (i.e.: SCRIPT,quic,DIRECT,no-resolve)
    +script:
    +  engine: expr # or starlark (10x to 20x slower)
    +  shortcuts:
    +    quic: network == 'udp' and dst_port == 443
    +    curl: resolve_process_name() == 'curl'
    +    # curl: resolve_process_path() == '/usr/bin/curl'
    +    not_common_port: dst_port not in [21, 22, 23, 53, 80, 123, 143, 194, 443, 465, 587, 853, 993, 995, 998, 2052, 2053, 2082, 2083, 2086, 2095, 2096, 5222, 5228, 5229, 5230, 8080, 8443, 8880, 8888, 8889]
    +
    +
    +rules:
    +  - SCRIPT,not_common_port,DIRECT # 只代理常规端口,常用于防止PT、BT的流量走代理
    +  - SCRIPT,quic,REJECT
    +  - DST-PORT,22,DIRECT
    +  - RULE-SET,applications,DIRECT
    +  - DOMAIN,clash.razord.top,DIRECT
    +  - DOMAIN,yacd.haishan.me,DIRECT
    +  - RULE-SET,private,DIRECT
    +  - RULE-SET,reject,REJECT
    +  - RULE-SET,icloud,DIRECT
    +  - RULE-SET,apple,DIRECT
    +  # - RULE-SET,google,DIRECT
    +  - RULE-SET,proxy,PROXY
    +  - RULE-SET,direct,DIRECT
    +  - RULE-SET,lancidr,DIRECT
    +  - RULE-SET,cncidr,DIRECT
    +  - RULE-SET,telegramcidr,PROXY
    +  - GEOIP,LAN,DIRECT
    +  - GEOIP,CN,DIRECT
    +  - MATCH,PROXY
    # /root/.config/clash/config.yaml
    +
    +# (HTTP and SOCKS5 in one port)
    +mixed-port: 7890
    +# RESTful API for clash
    +external-controller: 127.0.0.1:9090
    +secret: 123456
    +allow-lan: false
    +mode: rule
    +log-level: warning
    +
    +tun:
    +  enable: true
    +  stack: system # system gvisor
    +  auto-route: true
    +  auto-detect-interface: true
    +  dns-hijack:
    +    - any:53 # DNS hijacking might result in a failure, if the system DNS is at a private IP address (since auto-route does not capture private network traffic).
    +
    +dns:
    +  enable: true
    +  listen: 0.0.0.0:7874
    +  ipv6: false
    +  default-nameserver:
    +    - 114.114.114.114
    +  nameserver:
    +    - 119.29.29.29
    +    - 223.5.5.5
    +    - 8.8.8.8
    +  fallback:
    +    - https://dns.cloudflare.com/dns-query
    +    - tls://dns.google:853
    +    - https://1.1.1.1/dns-query
    +  enhanced-mode: fake-ip # fake-ip redir-host
    +  fake-ip-range: 198.18.0.1/16
    +  fake-ip-filter:
    +  - "*.lan"
    +  - "*.localdomain"
    +  - "*.example"
    +  - "*.invalid"
    +  - "*.localhost"
    +  - "*.test"
    +  - "*.local"
    +  - "*.home.arpa"
    +  - time.*.com
    +  - time.*.gov
    +  - time.*.edu.cn
    +  - time.*.apple.com
    +  - time-ios.apple.com
    +  - time1.*.com
    +  - time2.*.com
    +  - time3.*.com
    +  - time4.*.com
    +  - time5.*.com
    +  - time6.*.com
    +  - time7.*.com
    +  - ntp.*.com
    +  - ntp1.*.com
    +  - ntp2.*.com
    +  - ntp3.*.com
    +  - ntp4.*.com
    +  - ntp5.*.com
    +  - ntp6.*.com
    +  - ntp7.*.com
    +  - "*.time.edu.cn"
    +  - "*.ntp.org.cn"
    +  - "+.pool.ntp.org"
    +  - time1.cloud.tencent.com
    +  - music.163.com
    +  - "*.music.163.com"
    +  - "*.126.net"
    +  - musicapi.taihe.com
    +  - music.taihe.com
    +  - songsearch.kugou.com
    +  - trackercdn.kugou.com
    +  - "*.kuwo.cn"
    +  - api-jooxtt.sanook.com
    +  - api.joox.com
    +  - joox.com
    +  - y.qq.com
    +  - "*.y.qq.com"
    +  - streamoc.music.tc.qq.com
    +  - mobileoc.music.tc.qq.com
    +  - isure.stream.qqmusic.qq.com
    +  - dl.stream.qqmusic.qq.com
    +  - aqqmusic.tc.qq.com
    +  - amobile.music.tc.qq.com
    +  - "*.xiami.com"
    +  - "*.music.migu.cn"
    +  - music.migu.cn
    +  - "+.msftconnecttest.com"
    +  - "+.msftncsi.com"
    +  - localhost.ptlogin2.qq.com
    +  - localhost.sec.qq.com
    +  - "+.qq.com"
    +  - "+.tencent.com"
    +  - "+.srv.nintendo.net"
    +  - "*.n.n.srv.nintendo.net"
    +  - "+.stun.playstation.net"
    +  - xbox.*.*.microsoft.com
    +  - "*.*.xboxlive.com"
    +  - xbox.*.microsoft.com
    +  - xnotify.xboxlive.com
    +  - "+.battlenet.com.cn"
    +  - "+.wotgame.cn"
    +  - "+.wggames.cn"
    +  - "+.wowsgame.cn"
    +  - "+.wargaming.net"
    +  - proxy.golang.org
    +  - stun.*.*
    +  - stun.*.*.*
    +  - "+.stun.*.*"
    +  - "+.stun.*.*.*"
    +  - "+.stun.*.*.*.*"
    +  - "+.stun.*.*.*.*.*"
    +  - heartbeat.belkin.com
    +  - "*.linksys.com"
    +  - "*.linksyssmartwifi.com"
    +  - mobileoc.music.tc.qq.com
    +  - isure.stream.qqmusic.qq.com
    +  - dl.stream.qqmusic.qq.com
    +  - aqqmusic.tc.qq.com
    +  - amobile.music.tc.qq.com
    +  - "*.xiami.com"
    +  - "*.music.migu.cn"
    +  - music.migu.cn
    +  - "+.msftconnecttest.com"
    +  - "+.msftncsi.com"
    +  - localhost.ptlogin2.qq.com
    +  - localhost.sec.qq.com
    +  - "+.qq.com"
    +  - "+.tencent.com"
    +  - "+.srv.nintendo.net"
    +  - "*.n.n.srv.nintendo.net"
    +  - "+.stun.playstation.net"
    +  - xbox.*.*.microsoft.com
    +  - "*.*.xboxlive.com"
    +  - xbox.*.microsoft.com
    +  - xnotify.xboxlive.com
    +  - "+.battlenet.com.cn"
    +  - "+.wotgame.cn"
    +  - "+.wggames.cn"
    +  - "+.wowsgame.cn"
    +  - "+.wargaming.net"
    +  - proxy.golang.org
    +  - stun.*.*
    +  - stun.*.*.*
    +  - "+.stun.*.*"
    +  - "+.stun.*.*.*"
    +  - "+.stun.*.*.*.*"
    +  - "+.stun.*.*.*.*.*"
    +  - heartbeat.belkin.com
    +  - "*.linksys.com"
    +  - "*.linksyssmartwifi.com"
    +  - "*.router.asus.com"
    +  - mesu.apple.com
    +  - swscan.apple.com
    +  - swquery.apple.com
    +  - swdownload.apple.com
    +  - swcdn.apple.com
    +  - swdist.apple.com
    +  - lens.l.google.com
    +  - stun.l.google.com
    +  - "+.nflxvideo.net"
    +  - "*.square-enix.com"
    +  - "*.finalfantasyxiv.com"
    +  - "*.ffxiv.com"
    +  - "*.ff14.sdo.com"
    +  - ff.dorado.sdo.com
    +  - "*.mcdn.bilivideo.cn"
    +  - "+.media.dssott.com"
    +  - shark007.net
    +  - Mijia Cloud
    +  - "+.cmbchina.com"
    +  - "+.cmbimg.com"
    +  - local.adguard.org
    +  - "+.sandai.net"
    +  - "+.n0808.com"
    +    
    +proxy-providers:
    +  provider:
    +    type: http
    +    path: ./profile/provider.yaml
    +    url: subscription.url
    +    interval: 36000
    +    health-check:
    +      enable: true
    +      url: http://www.gstatic.com/generate_204
    +      interval: 3600
    +
    +proxy-groups:
    +  - name: PROXY
    +    type: select
    +    use:
    +      - provider
    +
    +rule-providers:
    +  reject:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/reject.txt"
    +    path: ./ruleset/reject.yaml
    +    interval: 86400
    +
    +  icloud:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/icloud.txt"
    +    path: ./ruleset/icloud.yaml
    +    interval: 86400
    +
    +  apple:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/apple.txt"
    +    path: ./ruleset/apple.yaml
    +    interval: 86400
    +
    +  # google:
    +  #   type: http
    +  #   behavior: domain
    +  #   url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/google.txt"
    +  #   path: ./ruleset/google.yaml
    +  #   interval: 86400
    +
    +  proxy:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/proxy.txt"
    +    path: ./ruleset/proxy.yaml
    +    interval: 86400
    +
    +  direct:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/direct.txt"
    +    path: ./ruleset/direct.yaml
    +    interval: 86400
    +  private:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/private.txt"
    +    path: ./ruleset/private.yaml
    +    interval: 86400
    +
    +  gfw:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/gfw.txt"
    +    path: ./ruleset/gfw.yaml
    +    interval: 86400
    +
    +  tld-not-cn:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/tld-not-cn.txt"
    +    path: ./ruleset/tld-not-cn.yaml
    +    interval: 86400
    +
    +  telegramcidr:
    +    type: http
    +    behavior: ipcidr
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/telegramcidr.txt"
    +    path: ./ruleset/telegramcidr.yaml
    +    interval: 86400
    +
    +  cncidr:
    +    type: http
    +    behavior: ipcidr
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/cncidr.txt"
    +    path: ./ruleset/cncidr.yaml
    +    interval: 86400
    +
    +  lancidr:
    +    type: http
    +    behavior: ipcidr
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/lancidr.txt"
    +    path: ./ruleset/lancidr.yaml
    +    interval: 86400
    +
    +  applications:
    +    type: http
    +    behavior: classical
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/applications.txt"
    +    path: ./ruleset/applications.yaml
    +    interval: 86400
    +
    +
    +# Script Shortcuts enables the use of script in rules mode. By default, DNS resolution takes place for SCRIPT rules. no-resolve can be appended to the rule to prevent the resolution. (i.e.: SCRIPT,quic,DIRECT,no-resolve)
    +script:
    +  engine: expr # or starlark (10x to 20x slower)
    +  shortcuts:
    +    quic: network == 'udp' and dst_port == 443
    +    curl: resolve_process_name() == 'curl'
    +    # curl: resolve_process_path() == '/usr/bin/curl'
    +    not_common_port: dst_port not in [21, 22, 23, 53, 80, 123, 143, 194, 443, 465, 587, 853, 993, 995, 998, 2052, 2053, 2082, 2083, 2086, 2095, 2096, 5222, 5228, 5229, 5230, 8080, 8443, 8880, 8888, 8889]
    +
    +
    +rules:
    +  - SCRIPT,not_common_port,DIRECT # 只代理常规端口,常用于防止PT、BT的流量走代理
    +  - SCRIPT,quic,REJECT
    +  - DST-PORT,22,DIRECT
    +  - RULE-SET,applications,DIRECT
    +  - DOMAIN,clash.razord.top,DIRECT
    +  - DOMAIN,yacd.haishan.me,DIRECT
    +  - RULE-SET,private,DIRECT
    +  - RULE-SET,reject,REJECT
    +  - RULE-SET,icloud,DIRECT
    +  - RULE-SET,apple,DIRECT
    +  # - RULE-SET,google,DIRECT
    +  - RULE-SET,proxy,PROXY
    +  - RULE-SET,direct,DIRECT
    +  - RULE-SET,lancidr,DIRECT
    +  - RULE-SET,cncidr,DIRECT
    +  - RULE-SET,telegramcidr,PROXY
    +  - GEOIP,LAN,DIRECT
    +  - GEOIP,CN,DIRECT
    +  - MATCH,PROXY
    shell
    cat > /etc/systemd/system/clash.service <<EOF
    +[Unit]
    +Description=clash
    +After=network.target
    +[Service]
    +Type=simple
    +ExecStart=/usr/bin/clash -d /root/.config/clash
    +Restart=on-failure
    +[Install]
    +WantedBy=multi-user.target
    +EOF
    +
    +# systemctl daemon-reload
    cat > /etc/systemd/system/clash.service <<EOF
    +[Unit]
    +Description=clash
    +After=network.target
    +[Service]
    +Type=simple
    +ExecStart=/usr/bin/clash -d /root/.config/clash
    +Restart=on-failure
    +[Install]
    +WantedBy=multi-user.target
    +EOF
    +
    +# systemctl daemon-reload

管理API

  • 获取代理信息:curl -X GET http://127.0.0.1:9090/proxies --header 'Authorization: Bearer 123456'
  • 获取指定代理信息:curl -X GET http://127.0.0.1:9090/proxies/{proxyGroupName} --header 'Authorization: Bearer 123456'
  • 代理组切换指定节点:curl -X PUT http://127.0.0.1:9090/proxies/{proxyGroupName} --header 'Authorization: Bearer 123456' --header "Content-Type: application/json" -d '{"name": "代理节点名称"}'
`,6),e=[o];function c(t,r,E,y,i,F){return n(),a("div",null,e)}const q=s(p,[["render",c]]);export{m as __pageData,q as default}; diff --git a/assets/linux_applications_Clash.md.57ceb8e9.lean.js b/assets/linux_applications_Clash.md.57ceb8e9.lean.js new file mode 100644 index 000000000..bd4580537 --- /dev/null +++ b/assets/linux_applications_Clash.md.57ceb8e9.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"Clash","description":"","frontmatter":{},"headers":[],"relativePath":"linux/applications/Clash.md","filePath":"linux/applications/Clash.md","lastUpdated":1708510102000}'),p={name:"linux/applications/Clash.md"},o=l("",6),e=[o];function c(t,r,E,y,i,F){return n(),a("div",null,e)}const q=s(p,[["render",c]]);export{m as __pageData,q as default}; diff --git "a/assets/linux_applications_FFmpeg\347\233\270\345\205\263.md.fa57cc0e.js" "b/assets/linux_applications_FFmpeg\347\233\270\345\205\263.md.fa57cc0e.js" new file mode 100644 index 000000000..fd994eebf --- /dev/null +++ "b/assets/linux_applications_FFmpeg\347\233\270\345\205\263.md.fa57cc0e.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as o,Q as p}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"FFmpeg相关","description":"","frontmatter":{},"headers":[],"relativePath":"linux/applications/FFmpeg相关.md","filePath":"linux/applications/FFmpeg相关.md","lastUpdated":1708510102000}'),l={name:"linux/applications/FFmpeg相关.md"},n=p('

FFmpeg相关

FFmpeg: A complete, cross-platform solution to record, convert and stream audio and video.

提取视频文件中的音频文件并输出

shell
ffmpeg -i input.mp4 -vn -acodec libmp3lame -q:a 0 ~/Downlaods/output.mp3
ffmpeg -i input.mp4 -vn -acodec libmp3lame -q:a 0 ~/Downlaods/output.mp3
  • -vn:禁用复制视频流,只提取音频流
  • -acodec libmp3lame:指定输出使用 LAME MP3 编码器进行编码。
  • -q:a 0:设置输出音频的质量。0 表示最高品质。

yt-dlp下载最高质量音频并由ffmpeg输出mp3

shell
yt-dlp -f bestaudio "https://www.youtube.com/watch?v=example" -o - | ffmpeg -i - -vn -acodec libmp3lame -q:a 0 ~/downloads/output.mp3
yt-dlp -f bestaudio "https://www.youtube.com/watch?v=example" -o - | ffmpeg -i - -vn -acodec libmp3lame -q:a 0 ~/downloads/output.mp3

yt-dlp下载最高质量视频&音频并合成一个视频

shell
yt-dlp -f bestvideo+bestaudio "https://www.youtube.com/watch?v=example" -o ~/Downloads/output.mp4 --recode-video mp4
yt-dlp -f bestvideo+bestaudio "https://www.youtube.com/watch?v=example" -o ~/Downloads/output.mp4 --recode-video mp4
',9),e=[n];function t(c,r,y,E,d,i){return a(),o("div",null,e)}const u=s(l,[["render",t]]);export{m as __pageData,u as default}; diff --git "a/assets/linux_applications_FFmpeg\347\233\270\345\205\263.md.fa57cc0e.lean.js" "b/assets/linux_applications_FFmpeg\347\233\270\345\205\263.md.fa57cc0e.lean.js" new file mode 100644 index 000000000..06ef0d2b4 --- /dev/null +++ "b/assets/linux_applications_FFmpeg\347\233\270\345\205\263.md.fa57cc0e.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as o,Q as p}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"FFmpeg相关","description":"","frontmatter":{},"headers":[],"relativePath":"linux/applications/FFmpeg相关.md","filePath":"linux/applications/FFmpeg相关.md","lastUpdated":1708510102000}'),l={name:"linux/applications/FFmpeg相关.md"},n=p("",9),e=[n];function t(c,r,y,E,d,i){return a(),o("div",null,e)}const u=s(l,[["render",t]]);export{m as __pageData,u as default}; diff --git a/assets/linux_applications_Grafana.md.27b9670d.js b/assets/linux_applications_Grafana.md.27b9670d.js new file mode 100644 index 000000000..c7442b0dd --- /dev/null +++ b/assets/linux_applications_Grafana.md.27b9670d.js @@ -0,0 +1,35 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"Grafana","description":"","frontmatter":{},"headers":[],"relativePath":"linux/applications/Grafana.md","filePath":"linux/applications/Grafana.md","lastUpdated":1708510102000}'),e={name:"linux/applications/Grafana.md"},p=l(`

Grafana

告警

grafana的告警可以使用Go Template语法来读取内置的变量数据并输出到告警邮件中

比如alert query从Loki日志中查询,可以同时从日志中提取出自己需要的关键属性作为标签:

shell
count_over_time({job="wechat"} |= \`订单申请退款失败\` | pattern \`<_> orderNo=<orderNo> refundNo=<refundNo>\` [1m])
count_over_time({job="wechat"} |= \`订单申请退款失败\` | pattern \`<_> orderNo=<orderNo> refundNo=<refundNo>\` [1m])

上面的查询提取了订单号、退款单号的数据,标签会存在_value_string_中,可以使用$values访问,在Summary中填写以下模板:

go
{{ with $values }}
+{{ range $k, $v := . }}
+  订单编号: {{$v.Labels.orderNo}}
+  退款单号: {{$v.Labels.refundNo}}
+  服务器实例: {{$v.Labels.instance}}
+{{ end }}
+{{ end }}
{{ with $values }}
+{{ range $k, $v := . }}
+  订单编号: {{$v.Labels.orderNo}}
+  退款单号: {{$v.Labels.refundNo}}
+  服务器实例: {{$v.Labels.instance}}
+{{ end }}
+{{ end }}

https://community.grafana.com/t/how-to-use-alert-message-templates-in-grafana/67537/3

匿名访问

  • 首先在Administration中创建新的组织Guest
  • 修改配置文件
ini
# grafana.ini
+
+[auth.anonymous]
+# enable anonymous access
+enabled = true
+# specify organization name that should be used for unauthenticated users
+org_name = Guest
+# specify role for unauthenticated users
+org_role = Viewer
+
+# mask the Grafana version number for unauthenticated users
+hide_version = true
# grafana.ini
+
+[auth.anonymous]
+# enable anonymous access
+enabled = true
+# specify organization name that should be used for unauthenticated users
+org_name = Guest
+# specify role for unauthenticated users
+org_role = Viewer
+
+# mask the Grafana version number for unauthenticated users
+hide_version = true
`,11),o=[p];function t(r,c,i,y,d,E){return a(),n("div",null,o)}const F=s(e,[["render",t]]);export{h as __pageData,F as default}; diff --git a/assets/linux_applications_Grafana.md.27b9670d.lean.js b/assets/linux_applications_Grafana.md.27b9670d.lean.js new file mode 100644 index 000000000..6ec290341 --- /dev/null +++ b/assets/linux_applications_Grafana.md.27b9670d.lean.js @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"Grafana","description":"","frontmatter":{},"headers":[],"relativePath":"linux/applications/Grafana.md","filePath":"linux/applications/Grafana.md","lastUpdated":1708510102000}'),e={name:"linux/applications/Grafana.md"},p=l("",11),o=[p];function t(r,c,i,y,d,E){return a(),n("div",null,o)}const F=s(e,[["render",t]]);export{h as __pageData,F as default}; diff --git "a/assets/linux_applications_Linux\344\270\255\344\275\277\347\224\250selenium.md.918cfe65.js" "b/assets/linux_applications_Linux\344\270\255\344\275\277\347\224\250selenium.md.918cfe65.js" new file mode 100644 index 000000000..f6c39d3b0 --- /dev/null +++ "b/assets/linux_applications_Linux\344\270\255\344\275\277\347\224\250selenium.md.918cfe65.js" @@ -0,0 +1,37 @@ +import{_ as s,o as n,c as a,Q as o}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"Linux中使用selenium","description":"","frontmatter":{},"headers":[],"relativePath":"linux/applications/Linux中使用selenium.md","filePath":"linux/applications/Linux中使用selenium.md","lastUpdated":1708510102000}'),e={name:"linux/applications/Linux中使用selenium.md"},p=o(`

Linux中使用selenium

安装linux版chrome

centos

wget https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm

yum install google-chrome-stable_current_x86_64.rpm

安装相关库

yum install mesa-libOSMesa-devel gnu-free-sans-fonts wqy-zenhei-fonts

ubuntu

wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.debsudo dpkg -i google-chrome-stable_current_amd64.deb

安装报错

dpkg: error processing package google-chrome-stable (--install): dependency problems - leaving unconfigured Processing triggers for mime-support (3.64ubuntu1) ... Processing triggers for man-db (2.9.1-1) ... Errors were encountered while processing: google-chrome-stable

使用sudo apt-get install -f修复依赖关系,

如果系统中有某个软件包不满足依赖条件,这个命令就会自动修复,将要安装那个软件包依赖的软件包。

安装chromedriver

淘宝源地址:http://npm.taobao.org/mirrors/chromedriver/

需要根据不同版本的chrome进行选择下载,比如我安装的chrome是96版本的,那么chromedriver就需要找对应的96版本

image-20220104161559216

这里选择一个最近更新的即可

image-20220104161718088

这里下载linux版本,下载后解压,把解压后的chromedriver可执行文件移动到path下,例如/usr/bin

bash
wget http://npm.taobao.org/mirrors/chromedriver/96.0.4664.45/chromedriver_linux64.zip
+
+unzip chromedriver_linux64.zip
+
+mv chromedriver /usr/bin
+
+chmod +x /usr/bin/chromedriver
wget http://npm.taobao.org/mirrors/chromedriver/96.0.4664.45/chromedriver_linux64.zip
+
+unzip chromedriver_linux64.zip
+
+mv chromedriver /usr/bin
+
+chmod +x /usr/bin/chromedriver

使用python测试

linux下无头浏览器模式:

python
from selenium import webdriver
+from selenium.webdriver.chrome.options import Options
+
+chrome_options = Options()
+chrome_options.add_argument('--headless')
+chrome_options.add_argument('--no-sandbox')
+chrome_options.add_argument('--disable-gpu')
+chrome_options.add_argument('--disable-dev-shm-usage')
+driver = webdriver.Chrome(chrome_options=chrome_options)
+driver.get("https://www.baidu.com")
+
+with open("./baidu.html", "w", encoding="utf-8") as fp:
+        fp.write(driver.page_source)
from selenium import webdriver
+from selenium.webdriver.chrome.options import Options
+
+chrome_options = Options()
+chrome_options.add_argument('--headless')
+chrome_options.add_argument('--no-sandbox')
+chrome_options.add_argument('--disable-gpu')
+chrome_options.add_argument('--disable-dev-shm-usage')
+driver = webdriver.Chrome(chrome_options=chrome_options)
+driver.get("https://www.baidu.com")
+
+with open("./baidu.html", "w", encoding="utf-8") as fp:
+        fp.write(driver.page_source)
`,23),l=[p];function r(t,c,i,d,E,y){return n(),a("div",null,l)}const h=s(e,[["render",r]]);export{m as __pageData,h as default}; diff --git "a/assets/linux_applications_Linux\344\270\255\344\275\277\347\224\250selenium.md.918cfe65.lean.js" "b/assets/linux_applications_Linux\344\270\255\344\275\277\347\224\250selenium.md.918cfe65.lean.js" new file mode 100644 index 000000000..bace6a7b7 --- /dev/null +++ "b/assets/linux_applications_Linux\344\270\255\344\275\277\347\224\250selenium.md.918cfe65.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as o}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"Linux中使用selenium","description":"","frontmatter":{},"headers":[],"relativePath":"linux/applications/Linux中使用selenium.md","filePath":"linux/applications/Linux中使用selenium.md","lastUpdated":1708510102000}'),e={name:"linux/applications/Linux中使用selenium.md"},p=o("",23),l=[p];function r(t,c,i,d,E,y){return n(),a("div",null,l)}const h=s(e,[["render",r]]);export{m as __pageData,h as default}; diff --git "a/assets/linux_applications_Nginx\351\205\215\347\275\256.md.cec7f5cb.js" "b/assets/linux_applications_Nginx\351\205\215\347\275\256.md.cec7f5cb.js" new file mode 100644 index 000000000..1a7863d54 --- /dev/null +++ "b/assets/linux_applications_Nginx\351\205\215\347\275\256.md.cec7f5cb.js" @@ -0,0 +1,79 @@ +import{_ as s,o as a,c as t,Q as e}from"./chunks/framework.dc1f4608.js";const _=JSON.parse('{"title":"nginx配置","description":"","frontmatter":{},"headers":[],"relativePath":"linux/applications/Nginx配置.md","filePath":"linux/applications/Nginx配置.md","lastUpdated":1708510102000}'),n={name:"linux/applications/Nginx配置.md"},o=e(`

nginx配置

记录常用、踩坑的nginx配置内容

http

upstream

upstream指令主要用于负载均衡,设置一系列的后端服务器

server

server块的指令主要用于指定主机和端口

listen

监听的端口号

  • default_server:定义默认的 server 处理没有成功匹配 server_name 的请求,如果没有显式定义,则会选取第一个定义的server作为default_server。

    • 显式定义:listen 80 default_server

    • 隐式定义

      nginx
      http {
      +    # 如果没有显式声明 default server 则第一个 server 会被隐式的设为 default server
      +    server {
      +        listen 80;
      +        server_name _; # _ 并不是重点 __ 也可以 ___也可以
      +        return 403; # 403 forbidden
      +    }
      +    
      +    server {
      +        listen 80;
      +        server_name www.a.com;
      +        ...
      +    }
      +}
      http {
      +    # 如果没有显式声明 default server 则第一个 server 会被隐式的设为 default server
      +    server {
      +        listen 80;
      +        server_name _; # _ 并不是重点 __ 也可以 ___也可以
      +        return 403; # 403 forbidden
      +    }
      +    
      +    server {
      +        listen 80;
      +        server_name www.a.com;
      +        ...
      +    }
      +}

server_name

  • server_name storyxc.com:完整匹配

  • server_name *.storyxc.com*开始的通配符匹配

    • 特殊情况:.storyxc.com能同时匹配storyxc.com*.storyxc.com

    A wildcard name may contain an asterisk only on the name’s start or end, and only on a dot border. The names “www.*.example.org” and “w*.example.org” are invalid. However, these names can be specified using regular expressions, for example, “~^www\\..+\\.example\\.org$” and “~^w.*\\.example\\.org$”. An asterisk can match several name parts. The name “*.example.org” matches not only www.example.org but www.sub.example.org as well.

    A special wildcard name in the form “.example.org” can be used to match both the exact name “example.org” and the wildcard name “*.example.org”.

  • server_name mail.* *结尾的通配符匹配

  • server_name ~^(?<user>.+)\\.storyxc\\.com$第一个匹配的正则表达式(按照配置文件中出现的顺序)

  • server_name _:通常使用_作为default server的server_name

location

location块用于匹配网页位置

匹配规则

location支持正则表达式匹配,也支持条件判断匹配

语法规则: location [=|~|~*|^~] /uri/ { … }

  • =:完全精确匹配
  • ^~:表示uri以某个常规字符串开头,理解为匹配url路径即可,nginx不对url进行编码
  • ~:表示区分大小写的正则匹配
  • ~*:表示不区分大小写的正则匹配
  • !~:区分大小写不匹配
  • !~*:不区分大小写不匹配
  • /:通用匹配,优先级最低

匹配顺序:=最高,正则匹配其次(按照规则顺序),通用匹配/最低,匹配成功时停止匹配按照当前规则处理请求

nginx
location ~ .*\\.(gif|jpg|jpeg|png|bmp|swf)$ {
+    # 匹配所有扩展名以.gif、.jpg、.jpeg、.png、.bmp、.swf结尾的静态文件
+    root /wwwroot/xxx;
+    # expires用来指定静态文件的过期时间,这里是30天
+    expires 30d;
+}
location ~ .*\\.(gif|jpg|jpeg|png|bmp|swf)$ {
+    # 匹配所有扩展名以.gif、.jpg、.jpeg、.png、.bmp、.swf结尾的静态文件
+    root /wwwroot/xxx;
+    # expires用来指定静态文件的过期时间,这里是30天
+    expires 30d;
+}
nginx
location ~ ^/(upload|html)/ {
+	# 匹配所有/upload /html
+	root /web/wwwroot/www.cszhi.com;
+	expires 30d;
+}
location ~ ^/(upload|html)/ {
+	# 匹配所有/upload /html
+	root /web/wwwroot/www.cszhi.com;
+	expires 30d;
+}

location和proxy_pass是否带/的影响

https://github.com/xqin/nginx-proxypass-server-paths

Case #Nginx locationproxy_pass URLTest URLPath received
01/test01http://127.0.0.1:8080/test01/abc/test/test01/abc/test
02/test02http://127.0.0.1:8080//test02/abc/test//abc/test
03/test03/http://127.0.0.1:8080/test03/abc/test/test03/abc/test
04/test04/http://127.0.0.1:8080//test04/abc/test/abc/test
05/test05http://127.0.0.1:8080/app1/test05/abc/test/app1/abc/test
06/test06http://127.0.0.1:8080/app1//test06/abc/test/app1//abc/test
07/test07/http://127.0.0.1:8080/app1/test07/abc/test/app1abc/test
08/test08/http://127.0.0.1:8080/app1//test08/abc/test/app1/abc/test
09/http://127.0.0.1:8080/test09/abc/test/test09/abc/test
10/http://127.0.0.1:8080//test10/abc/test/test10/abc/test
11/http://127.0.0.1:8080/app1/test11/abc/test/app1test11/abc/test
12/http://127.0.0.1:8080/app2//test12/abc/test/app2/test12/abc/test

root和alias区别

  • alias指定的是准确目录,且最后必须是/,否则就会访问失败
  • root是指定目录的上级目录
nginx
location /abc {
+  root /wwwroot/aaa;
+ 	# 此规则匹配的最终资源路径为/wwwroot/aaa/abc/
+  # 如果访问的是/abc/a.html,则最终访问的资源是服务器中的/wwwroot/aaa/abc/a.html
+  index index.html;
+}
+
+location /abc {
+  alias /wwwroot/aaa/;
+  # 此规则匹配的最终资源路径为/wwwroot/aaa/
+  # 如果访问的是/abc/b.txt,则最终访问的资源是/wwwroot/aaa/b.txt
+  index index.html;
+}
location /abc {
+  root /wwwroot/aaa;
+ 	# 此规则匹配的最终资源路径为/wwwroot/aaa/abc/
+  # 如果访问的是/abc/a.html,则最终访问的资源是服务器中的/wwwroot/aaa/abc/a.html
+  index index.html;
+}
+
+location /abc {
+  alias /wwwroot/aaa/;
+  # 此规则匹配的最终资源路径为/wwwroot/aaa/
+  # 如果访问的是/abc/b.txt,则最终访问的资源是/wwwroot/aaa/b.txt
+  index index.html;
+}

访问静态资源重定向问题

当nginx监听的不是80端口时,访问文件夹且末尾不是/,则nginx会进行301永久重定向,此时会丢掉客户端访问时的端口号,可以通过以下配置解决,作用是将不以 / 结尾的目录 URL 重定向至以 / 结尾的目录 URL。使用 -d 判断 $request_filename 是否为一个目录,如果是,则使用 rewrite 指令进行重写。其中,[^/]$ 表示匹配不以 / 结尾的 URL,即目录 URL,$scheme://$http_host$uri/ 表示重定向目标 URL,其中使用了 $scheme 变量表示客户端请求所使用的协议(HTTP 或 HTTPS)、$http_host 变量表示客户端请求的 HOST 头部信息、$uri 变量表示客户端请求的 URI。

nginx
location / {
+    if (-d $request_filename) {
+        rewrite [^/]$ $scheme://$http_host$uri/ permanent;
+    }
+    try_files $uri $uri/ /index.html;
+}
location / {
+    if (-d $request_filename) {
+        rewrite [^/]$ $scheme://$http_host$uri/ permanent;
+    }
+    try_files $uri $uri/ /index.html;
+}

变量

变量名作用
$scheme请求使用的协议 (http 或 https)
$host当前请求的主机名,不包括端口号。
$http_host完整的HTTP主机头,包括主机名和端口号。
$request_uri完整的请求 URI,包括查询字符串
$uri当前请求的 URI (不包含请求参数)
$args当前请求的参数部分,不包括问号
$request_method当前请求的方法 (GET、POST 等)
$remote_addr客户端 IP 地址
$server_addr服务器 IP 地址
$server_name当前请求的服务器名称
$server_protocol服务器使用的协议版本
$request_filename当前请求的文件路径和名称
$document_root当前请求的根目录
$is_args如果请求包含参数部分,值为 ?,否则为空字符串
$query_string当前请求的查询字符串部分,包括问号(?)
$http_user_agent客户端发送的 User-Agent 头部信息
$http_referer客户端发送的 Referer 头部信息
$http_cookie客户端发送的 Cookie 头部信息
$remote_port客户端端口号
$server_port服务器端口号
$realpath_root请求根目录的实际路径
$content_type请求的内容类型
$content_length请求的内容长度
$request_body请求的主体内容

proxy_set_header

配置指令作用
proxy_set_header Host $host;设置代理请求的主机头,通常用于传递客户端的原始主机头。$http_host传递包含端口号。
proxy_set_header X-Real-IP $remote_addr;设置代理请求的客户端真实IP地址,用于传递客户端的真实IP地址给后端服务器。
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;用于将客户端的原始IP地址添加到X-Forwarded-For头中,以便后端服务器知道请求的真实来源。
proxy_set_header X-Forwarded-Proto $scheme;设置代理请求的协议(HTTP或HTTPS),以便后端服务器知道请求的协议类型。
proxy_set_header User-Agent $http_user_agent;传递客户端的User-Agent头,用于识别客户端的浏览器或应用程序。
proxy_set_header Referer $http_referer;传递客户端的Referer头,通常用于跟踪页面来源。
proxy_set_header Cookie $http_cookie;传递客户端的Cookie头,以便后端服务器可以访问客户端的Cookie数据。
proxy_set_header Connection "";清除代理请求的Connection头,通常用于避免代理干扰连接的管理。
proxy_set_header Upgrade $http_upgrade;用于处理WebSocket连接的Upgrade头,通常与WebSocket代理一起使用。
proxy_set_header X-Frame-Options SAMEORIGIN;用于设置X-Frame-Options头,控制网页是否可以嵌套在其他网页中显示。
`,34),l=[o];function p(r,c,d,i,E,y){return a(),t("div",null,l)}const b=s(n,[["render",p]]);export{_ as __pageData,b as default}; diff --git "a/assets/linux_applications_Nginx\351\205\215\347\275\256.md.cec7f5cb.lean.js" "b/assets/linux_applications_Nginx\351\205\215\347\275\256.md.cec7f5cb.lean.js" new file mode 100644 index 000000000..2b8bb7e0a --- /dev/null +++ "b/assets/linux_applications_Nginx\351\205\215\347\275\256.md.cec7f5cb.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as t,Q as e}from"./chunks/framework.dc1f4608.js";const _=JSON.parse('{"title":"nginx配置","description":"","frontmatter":{},"headers":[],"relativePath":"linux/applications/Nginx配置.md","filePath":"linux/applications/Nginx配置.md","lastUpdated":1708510102000}'),n={name:"linux/applications/Nginx配置.md"},o=e("",34),l=[o];function p(r,c,d,i,E,y){return a(),t("div",null,l)}const b=s(n,[["render",p]]);export{_ as __pageData,b as default}; diff --git a/assets/linux_applications_Rclone.md.b98100d3.js b/assets/linux_applications_Rclone.md.b98100d3.js new file mode 100644 index 000000000..71e155276 --- /dev/null +++ b/assets/linux_applications_Rclone.md.b98100d3.js @@ -0,0 +1,327 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const B=JSON.parse('{"title":"Rclone","description":"","frontmatter":{},"headers":[],"relativePath":"linux/applications/Rclone.md","filePath":"linux/applications/Rclone.md","lastUpdated":1708510102000}'),p={name:"linux/applications/Rclone.md"},o=l(`

Rclone

rclone 作为文件和对象存储的管理工具, 经过近些年的发展已经完好的支持各种存储协议, 比如 HDFS, FTP, SFTP, GCS 和 S3(兼容 aws, 金山云, 腾讯云, 阿里云等)等, 逐渐有统一管理云存储之势, 从 rclone-github 来看, 各大云厂商也逐渐将各自的存储协议合并到了 rclone 中. 这在对象存储统一管理, 尤其是多云管理的场景中带来了很大的便利, 也便于我们实现诸如统一运维管理的目标.

安装

两种方式:

  • apt install rclone
  • curl https://rclone.org/install.sh | sudo bash

配置

bash
rclone config
+2023/10/20 16:58:06 NOTICE: Config file "/root/.config/rclone/rclone.conf" not found - using defaults
+No remotes found - make a new one
+n) New remote
+s) Set configuration password
+q) Quit config
+n/s/q>n
+name>alist
+Type of storage to configure.
+Enter a string value. Press Enter for the default ("").
+Choose a number from below, or type in your own value
+ 1 / 1Fichier
+   \\ "fichier"
+ 2 / Alias for an existing remote
+   \\ "alias"
+ 3 / Amazon Drive
+   \\ "amazon cloud drive"
+ 4 / Amazon S3 Compliant Storage Provider (AWS, Alibaba, Ceph, Digital Ocean, Dreamhost, IBM COS, Minio, Tencent COS, etc)
+   \\ "s3"
+ 5 / Backblaze B2
+   \\ "b2"
+ 6 / Box
+   \\ "box"
+ 7 / Cache a remote
+   \\ "cache"
+ 8 / Citrix Sharefile
+   \\ "sharefile"
+ 9 / Dropbox
+   \\ "dropbox"
+10 / Encrypt/Decrypt a remote
+   \\ "crypt"
+11 / FTP Connection
+   \\ "ftp"
+12 / Google Cloud Storage (this is not Google Drive)
+   \\ "google cloud storage"
+13 / Google Drive
+   \\ "drive"
+14 / Google Photos
+   \\ "google photos"
+15 / Hubic
+   \\ "hubic"
+16 / In memory object storage system.
+   \\ "memory"
+17 / Jottacloud
+   \\ "jottacloud"
+18 / Koofr
+   \\ "koofr"
+19 / Local Disk
+   \\ "local"
+20 / Mail.ru Cloud
+   \\ "mailru"
+21 / Microsoft Azure Blob Storage
+   \\ "azureblob"
+22 / Microsoft OneDrive
+   \\ "onedrive"
+23 / OpenDrive
+   \\ "opendrive"
+24 / OpenStack Swift (Rackspace Cloud Files, Memset Memstore, OVH)
+   \\ "swift"
+25 / Pcloud
+   \\ "pcloud"
+26 / Put.io
+   \\ "putio"
+27 / SSH/SFTP Connection
+   \\ "sftp"
+28 / Sugarsync
+   \\ "sugarsync"
+29 / Transparently chunk/split large files
+   \\ "chunker"
+30 / Union merges the contents of several upstream fs
+   \\ "union"
+31 / Webdav
+   \\ "webdav"
+32 / Yandex Disk
+   \\ "yandex"
+33 / http Connection
+   \\ "http"
+34 / premiumize.me
+   \\ "premiumizeme"
+35 / seafile
+   \\ "seafile"
+Storage> 31
+** See help for webdav backend at: https://rclone.org/webdav/ **
+
+URL of http host to connect to
+Enter a string value. Press Enter for the default ("").
+Choose a number from below, or type in your own value
+ 1 / Connect to example.com
+   \\ "https://example.com"
+url> https://alist.storyxc.com:63443
+Name of the Webdav site/service/software you are using
+Enter a string value. Press Enter for the default ("").
+Choose a number from below, or type in your own value
+ 1 / Nextcloud
+   \\ "nextcloud"
+ 2 / Owncloud
+   \\ "owncloud"
+ 3 / Sharepoint
+   \\ "sharepoint"
+ 4 / Other site/service or software
+   \\ "other"
+vendor> 4
+User name
+Enter a string value. Press Enter for the default ("").
+user> 
+Password.
+y) Yes type in my own password
+g) Generate random password
+n) No leave this optional password blank (default)
+y/g/n> n
+Enter the password:
+password:
+Confirm the password:
+password:
+Bearer token instead of user/pass (eg a Macaroon)
+Enter a string value. Press Enter for the default ("").
+bearer_token>
+Edit advanced config? (y/n)
+y) Yes
+n) No (default)
+y/n> n
+Remote config
+--------------------
+[alist]
+url = 
+vendor = other
+user = 
+pass = *** ENCRYPTED ***
+--------------------
+y) Yes this is OK (default)
+e) Edit this remote
+d) Delete this remote
+y/e/d> y
+
+Current remotes:
+
+Name                 Type
+====                 ====
+alist                webdav
+
+e) Edit existing remote
+n) New remote
+d) Delete remote
+r) Rename remote
+c) Copy remote
+s) Set configuration password
+q) Quit config
+e/n/d/r/c/s/q> q
rclone config
+2023/10/20 16:58:06 NOTICE: Config file "/root/.config/rclone/rclone.conf" not found - using defaults
+No remotes found - make a new one
+n) New remote
+s) Set configuration password
+q) Quit config
+n/s/q>n
+name>alist
+Type of storage to configure.
+Enter a string value. Press Enter for the default ("").
+Choose a number from below, or type in your own value
+ 1 / 1Fichier
+   \\ "fichier"
+ 2 / Alias for an existing remote
+   \\ "alias"
+ 3 / Amazon Drive
+   \\ "amazon cloud drive"
+ 4 / Amazon S3 Compliant Storage Provider (AWS, Alibaba, Ceph, Digital Ocean, Dreamhost, IBM COS, Minio, Tencent COS, etc)
+   \\ "s3"
+ 5 / Backblaze B2
+   \\ "b2"
+ 6 / Box
+   \\ "box"
+ 7 / Cache a remote
+   \\ "cache"
+ 8 / Citrix Sharefile
+   \\ "sharefile"
+ 9 / Dropbox
+   \\ "dropbox"
+10 / Encrypt/Decrypt a remote
+   \\ "crypt"
+11 / FTP Connection
+   \\ "ftp"
+12 / Google Cloud Storage (this is not Google Drive)
+   \\ "google cloud storage"
+13 / Google Drive
+   \\ "drive"
+14 / Google Photos
+   \\ "google photos"
+15 / Hubic
+   \\ "hubic"
+16 / In memory object storage system.
+   \\ "memory"
+17 / Jottacloud
+   \\ "jottacloud"
+18 / Koofr
+   \\ "koofr"
+19 / Local Disk
+   \\ "local"
+20 / Mail.ru Cloud
+   \\ "mailru"
+21 / Microsoft Azure Blob Storage
+   \\ "azureblob"
+22 / Microsoft OneDrive
+   \\ "onedrive"
+23 / OpenDrive
+   \\ "opendrive"
+24 / OpenStack Swift (Rackspace Cloud Files, Memset Memstore, OVH)
+   \\ "swift"
+25 / Pcloud
+   \\ "pcloud"
+26 / Put.io
+   \\ "putio"
+27 / SSH/SFTP Connection
+   \\ "sftp"
+28 / Sugarsync
+   \\ "sugarsync"
+29 / Transparently chunk/split large files
+   \\ "chunker"
+30 / Union merges the contents of several upstream fs
+   \\ "union"
+31 / Webdav
+   \\ "webdav"
+32 / Yandex Disk
+   \\ "yandex"
+33 / http Connection
+   \\ "http"
+34 / premiumize.me
+   \\ "premiumizeme"
+35 / seafile
+   \\ "seafile"
+Storage> 31
+** See help for webdav backend at: https://rclone.org/webdav/ **
+
+URL of http host to connect to
+Enter a string value. Press Enter for the default ("").
+Choose a number from below, or type in your own value
+ 1 / Connect to example.com
+   \\ "https://example.com"
+url> https://alist.storyxc.com:63443
+Name of the Webdav site/service/software you are using
+Enter a string value. Press Enter for the default ("").
+Choose a number from below, or type in your own value
+ 1 / Nextcloud
+   \\ "nextcloud"
+ 2 / Owncloud
+   \\ "owncloud"
+ 3 / Sharepoint
+   \\ "sharepoint"
+ 4 / Other site/service or software
+   \\ "other"
+vendor> 4
+User name
+Enter a string value. Press Enter for the default ("").
+user> 
+Password.
+y) Yes type in my own password
+g) Generate random password
+n) No leave this optional password blank (default)
+y/g/n> n
+Enter the password:
+password:
+Confirm the password:
+password:
+Bearer token instead of user/pass (eg a Macaroon)
+Enter a string value. Press Enter for the default ("").
+bearer_token>
+Edit advanced config? (y/n)
+y) Yes
+n) No (default)
+y/n> n
+Remote config
+--------------------
+[alist]
+url = 
+vendor = other
+user = 
+pass = *** ENCRYPTED ***
+--------------------
+y) Yes this is OK (default)
+e) Edit this remote
+d) Delete this remote
+y/e/d> y
+
+Current remotes:
+
+Name                 Type
+====                 ====
+alist                webdav
+
+e) Edit existing remote
+n) New remote
+d) Delete remote
+r) Rename remote
+c) Copy remote
+s) Set configuration password
+q) Quit config
+e/n/d/r/c/s/q> q

systemd开机自动挂载

shell
mkdir /alist
+#将后面修改成你上面手动运行命令中,除了rclone的全部参数
+command="mount alist:/ /alist --cache-dir /tmp --allow-other --vfs-cache-mode writes --allow-non-empty"
+#以下是一整条命令,一起复制到SSH客户端运行
+cat > /etc/systemd/system/rclone.service <<EOF
+[Unit]
+Description=Rclone
+# 在网络和docker服务之后启动,因为要先启动阿里云盘的webdav服务容器
+After=network-online.target docker.service
+[Service]
+Type=simple
+ExecStart=$(command -v rclone) \${command}
+Restart=on-abort
+User=root
+[Install]
+WantedBy=default.target
+EOF
mkdir /alist
+#将后面修改成你上面手动运行命令中,除了rclone的全部参数
+command="mount alist:/ /alist --cache-dir /tmp --allow-other --vfs-cache-mode writes --allow-non-empty"
+#以下是一整条命令,一起复制到SSH客户端运行
+cat > /etc/systemd/system/rclone.service <<EOF
+[Unit]
+Description=Rclone
+# 在网络和docker服务之后启动,因为要先启动阿里云盘的webdav服务容器
+After=network-online.target docker.service
+[Service]
+Type=simple
+ExecStart=$(command -v rclone) \${command}
+Restart=on-abort
+User=root
+[Install]
+WantedBy=default.target
+EOF

启动:

shell
systemctl start rclone
systemctl start rclone

开机自启动:

shell
systemctl enable rclone
systemctl enable rclone
`,13),e=[o];function t(c,r,E,y,F,i){return n(),a("div",null,e)}const u=s(p,[["render",t]]);export{B as __pageData,u as default}; diff --git a/assets/linux_applications_Rclone.md.b98100d3.lean.js b/assets/linux_applications_Rclone.md.b98100d3.lean.js new file mode 100644 index 000000000..2f415141a --- /dev/null +++ b/assets/linux_applications_Rclone.md.b98100d3.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const B=JSON.parse('{"title":"Rclone","description":"","frontmatter":{},"headers":[],"relativePath":"linux/applications/Rclone.md","filePath":"linux/applications/Rclone.md","lastUpdated":1708510102000}'),p={name:"linux/applications/Rclone.md"},o=l("",13),e=[o];function t(c,r,E,y,F,i){return n(),a("div",null,e)}const u=s(p,[["render",t]]);export{B as __pageData,u as default}; diff --git a/assets/linux_applications_iptables.md.4a6d73d9.js b/assets/linux_applications_iptables.md.4a6d73d9.js new file mode 100644 index 000000000..7b7e8099f --- /dev/null +++ b/assets/linux_applications_iptables.md.4a6d73d9.js @@ -0,0 +1,11 @@ +import{_ as s,o as a,c as l,Q as p}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"iptables","description":"","frontmatter":{},"headers":[],"relativePath":"linux/applications/iptables.md","filePath":"linux/applications/iptables.md","lastUpdated":1708510102000}'),o={name:"linux/applications/iptables.md"},n=p(`

iptables

iptables是一个用户级程序,用于操作内核级的网络模块netfilter

表和链

iptables的功能由表的形式呈现,每张表由若干个链组成,每个链可以分配一组规则

iptables有五张内建表,按照优先级高到底分别是:Raw、Mangle、NAT、Filter、Security

Raw

此表负责数据包标记,决定数据包是否被状态跟踪机制处理,Raw表有2个内建链

  • PREROUTING:用于通过任何网络接口到达的数据包
  • OUTPUT:针对本地进程产生的数据包

Mangle

此表负责更改数据包内容,Mangle表有5个内建链

  • PREROUTING
  • OUTPUT
  • FORWARD
  • INPUT
  • POSTROUTING

NAT

此表负责数据包的ip地址转换,NAT表有3种内建链

  • PREROUTING:处理刚到达本机并在路由转发前的数据包。它会转换数据包中的目标IP地址(destination ip address),通常用于DNAT(destination NAT)。
  • POSTROUTING:处理即将离开本机的数据包。它会转换数据包中的源IP地址(source ip address),通常用于SNAT(source NAT)。
  • OUTPUT:处理本机产生的数据包。

Filter

此表负责过滤数据包,iptables的默认表,具有3种内建链

  • INPUT:处理来自外部的数据。
  • OUTPUT:处理向外发送的数据。
  • FORWARD:将数据转发到本机的其他网卡设备上。

Security

新加入的特性,用于强制访问控制(MAC)网络规则,有3种内建链

  • INPUT
  • OUTPUT
  • FORWARD

iptables数据包处理流程图

v2-6c9358844d8f440486551d925dfe36b5_1440w

iptables语法

语法

iptables [-t 表名] 命令选项 [链名] [匹配条件] [-j 策略]

iptables命令包含五个部分

  • 表名:要操作的表,不指定时默认操作Filter表

  • 链名:要操作的链,不指定链时默认表内所有链

  • 命令选项:要进行的操作

  • 匹配条件:定义规则适用哪些数据包(匹配哪些数据包)

  • 策略:匹配数据的目标执行的操作,说白了就是packet匹配上规则后该干嘛

常见的命令选项

  • -A 在指定链的末尾添加(append)一条新的规则
  • -D 删除(delete)指定链中的某一条规则,可以按规则序号和内容删除
  • -I 在指定链中插入(insert)一条新的规则,默认在第一行添加
  • -R 修改、替换(replace)指定链中的某一条规则,可以按规则序号和内容替换
  • -L 列出(list)指定链中所有的规则进行查看
  • -E 重命名用户定义的链,不改变链本身
  • -F 清空(flush)
  • -N 新建(new-chain)一条用户自己定义的规则链
  • -X 删除指定表中用户自定义的规则链(delete-chain)
  • -P 设置指定链的默认策略(policy)
  • -Z 将所有表的所有链的字节和数据包计数器清零
  • -n 使用数字形式(numeric)显示输出结果
  • -v 查看规则表详细信息(verbose)的信息
  • -V 查看版本(version)
  • -h 获取帮助(help)

常见策略(target)

  • ACCEPT:允许数据包通过。

  • DROP:直接丢弃数据包,不给任何回应信息,这时候客户端会感觉自己的请求泥牛入海了,过了超时时间才会有反应。

  • REJECT:拒绝数据包通过,必要时会给数据发送端一个响应的信息,客户端刚请求就会收到拒绝的信息。

  • SNAT:源地址转换,解决内网用户用同一个公网地址上网的问题。

  • MASQUERADE:是SNAT的一种特殊形式,适用于动态的、临时会变的ip上。

  • DNAT:目标地址转换。

  • REDIRECT:在本机做端口映射。

添加规则

  • -A:在链的末尾追加一条规则,例如iptables -A INPUT -s 127.0.0.1 -p tcp --dport 3306 -j ACCEPT
  • -I:在链的开头(或指定序号)插入一条规则,例如iptables -I INPUT -p tcp -j ACCEPTiptables -I INPUT 2 -p tcp -j ACCEPT

查看规则

  • -L:列出所有规则

  • -n:以数字形式显示地址、端口等信息

  • -v:显示更详细规则信息

  • --line-numbers:显示规则序号

删除规则

  • 删除nat表INPUT链的第三条规则:iptables -t nat -D INPUT 3

  • 清空nat表所有规则:iptables -t nat -F

给指定的链设置默认策略

  • iptables -t filter -P FORWARD DROP

通用匹配

协议匹配 -p(protocol)

  • 指定规则的协议,如tcp, udp, icmp等,可以使用all来指定所有协议。
  • 如果不指定**-p参数,则默认是all**值。这并不明智,请总是明确指定协议名称。
  • 可以使用协议名(如tcp),或者是协议值(比如6代表tcp)来指定协议。映射关系请查看/etc/protocols
  • 还可以使用**–protocol参数代替-p**参数

地址匹配 -s -d:

-s 源地址
  • 指定数据包的源地址
  • 参数可以使IP地址、网络地址、主机名
  • 例如:-s 192.168.1.101指定IP地址
  • 例如:-s 192.168.1.10/24指定网络地址
  • 如果不指定-s参数,就代表所有地址
  • 还可以使用**–src或者–source**
-d 目标地址
  • 指定目的地址
  • 参数和**-s**相同
  • 还可以使用**–dst或者–destination**

接口匹配 -i -o

-i 输入接口(input interface)
  • -i代表输入接口(input interface)
  • -i指定了要处理来自哪个接口的数据包
  • 这些数据包即将进入INPUT, FORWARD, PREROUTE链
  • 例如:-i eth0指定了要处理经由eth0进入的数据包
  • 如果不指定**-i**参数,那么将处理进入所有接口的数据包
  • 如果出现**!** -i eth0,那么将处理所有经由eth0以外的接口进入的数据包
  • 如果出现-i eth**+,那么将处理所有经由eth开头的接口**进入的数据包
  • 还可以使用**–in-interface**参数
-o 输出(out interface)
  • -o代表”output interface”
  • -o指定了数据包由哪个接口输出
  • 这些数据包即将进入FORWARD, OUTPUT, POSTROUTING链
  • 如果不指定**-o**选项,那么系统上的所有接口都可以作为输出接口
  • 如果出现**!** -o eth0,那么将从eth0以外的接口输出
  • 如果出现-i eth**+,那么将仅从eth开头的接口**输出
  • 还可以使用**–out-interface**参数

隐含匹配

端口匹配

-–sport 源端口(source port) 针对-p tcp 或者 -p udp
  • 缺省情况下,将匹配所有端口
  • 可以指定端口号或者端口名称,例如”–sport 22″与”–sport ssh”。
  • /etc/services文件描述了上述映射关系。
  • 从性能上讲,使用端口号更好
  • 使用冒号可以匹配端口范围,如”–sport 22:100″
  • 还可以使用”–source-port”
–-dport 目的端口(destination port) 针对-p tcp 或者 -p udp
  • 参数和–sport类似
  • 还可以使用”–destination-port”
-–tcp-flags TCP标志 针对-p tcp
  • 可以指定由逗号分隔的多个参数
  • 有效值可以是:SYN, ACK, FIN, RST, URG, PSH
  • 可以使用ALL或者NONE
-–icmp-type ICMP类型 针对-p icmp
  • –icmp-type 0 表示Echo Reply
  • –icmp-type 8 表示Echo

显式匹配

多端口匹配 -m multiport --sport 源端口列表 -m multiport --dport 目的端口列表 IP范围匹配 -m iprange --src-range IP范围 MAC地址匹配 -m mac –mac1-source MAC地址 状态匹配 -m state --state 连接状态

shell
iptables -A INPUT -p tcp -m multiport --dport 25,80,110,143 -j ACCEPT
+iptables -A FORWARD -p tcp -m iprange --src-range 192.168.4.21-192.168.4.28 -j ACCEPT
+iptables -A INPUT -m mac --mac-source 00:0c:29:c0:55:3f -j DROP
+iptables -P INPUT DROP
+iptables -I INPUT -p tcp -m multiport --dport 80-82,85 -j ACCEPT
+iptables -I INPUT -p tcp -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp -m multiport --dport 25,80,110,143 -j ACCEPT
+iptables -A FORWARD -p tcp -m iprange --src-range 192.168.4.21-192.168.4.28 -j ACCEPT
+iptables -A INPUT -m mac --mac-source 00:0c:29:c0:55:3f -j DROP
+iptables -P INPUT DROP
+iptables -I INPUT -p tcp -m multiport --dport 80-82,85 -j ACCEPT
+iptables -I INPUT -p tcp -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
`,65),t=[n];function e(r,i,c,E,y,h){return a(),l("div",null,t)}const F=s(o,[["render",e]]);export{u as __pageData,F as default}; diff --git a/assets/linux_applications_iptables.md.4a6d73d9.lean.js b/assets/linux_applications_iptables.md.4a6d73d9.lean.js new file mode 100644 index 000000000..0520fd89e --- /dev/null +++ b/assets/linux_applications_iptables.md.4a6d73d9.lean.js @@ -0,0 +1 @@ +import{_ as s,o as a,c as l,Q as p}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"iptables","description":"","frontmatter":{},"headers":[],"relativePath":"linux/applications/iptables.md","filePath":"linux/applications/iptables.md","lastUpdated":1708510102000}'),o={name:"linux/applications/iptables.md"},n=p("",65),t=[n];function e(r,i,c,E,y,h){return a(),l("div",null,t)}const F=s(o,[["render",e]]);export{u as __pageData,F as default}; diff --git "a/assets/linux_applications_screen\347\232\204\350\277\233\351\230\266\347\224\250\346\263\225.md.7103b3aa.js" "b/assets/linux_applications_screen\347\232\204\350\277\233\351\230\266\347\224\250\346\263\225.md.7103b3aa.js" new file mode 100644 index 000000000..ea696b3ff --- /dev/null +++ "b/assets/linux_applications_screen\347\232\204\350\277\233\351\230\266\347\224\250\346\263\225.md.7103b3aa.js" @@ -0,0 +1,13 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"screen的进阶用法","description":"","frontmatter":{},"headers":[],"relativePath":"linux/applications/screen的进阶用法.md","filePath":"linux/applications/screen的进阶用法.md","lastUpdated":1708510102000}'),o={name:"linux/applications/screen的进阶用法.md"},p=l(`

screen的进阶用法

以detatch模式创建daemon会话

screen -dmS <name>

向会话窗口中发送文本

screen -S <name> -X stuff <text>

例如:screen -S <name> -X stuff abc,当attach之后,窗口中已经有了abc

如果想执行命令:

shell
screen -S <name> -X stuff "<command> \\n"
+screen -S <name> -X stuff "<command> \\r"
+---
+screen -S centos -X stuff  $'<command> \\n'
+---
+screen -S new_screen -X stuff "cd /dir
+"
screen -S <name> -X stuff "<command> \\n"
+screen -S <name> -X stuff "<command> \\r"
+---
+screen -S centos -X stuff  $'<command> \\n'
+---
+screen -S new_screen -X stuff "cd /dir
+"

清理screen窗口

screen -S <name> -X quit

`,10),e=[p];function t(c,r,y,E,F,i){return a(),n("div",null,e)}const C=s(o,[["render",t]]);export{u as __pageData,C as default}; diff --git "a/assets/linux_applications_screen\347\232\204\350\277\233\351\230\266\347\224\250\346\263\225.md.7103b3aa.lean.js" "b/assets/linux_applications_screen\347\232\204\350\277\233\351\230\266\347\224\250\346\263\225.md.7103b3aa.lean.js" new file mode 100644 index 000000000..12f74dfce --- /dev/null +++ "b/assets/linux_applications_screen\347\232\204\350\277\233\351\230\266\347\224\250\346\263\225.md.7103b3aa.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"screen的进阶用法","description":"","frontmatter":{},"headers":[],"relativePath":"linux/applications/screen的进阶用法.md","filePath":"linux/applications/screen的进阶用法.md","lastUpdated":1708510102000}'),o={name:"linux/applications/screen的进阶用法.md"},p=l("",10),e=[p];function t(c,r,y,E,F,i){return a(),n("div",null,e)}const C=s(o,[["render",t]]);export{u as __pageData,C as default}; diff --git "a/assets/linux_env_ArchLinux\345\256\211\350\243\205.md.991f6f32.js" "b/assets/linux_env_ArchLinux\345\256\211\350\243\205.md.991f6f32.js" new file mode 100644 index 000000000..26a459b03 --- /dev/null +++ "b/assets/linux_env_ArchLinux\345\256\211\350\243\205.md.991f6f32.js" @@ -0,0 +1,81 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const F=JSON.parse('{"title":"ArchLinux安装","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/ArchLinux安装.md","filePath":"linux/env/ArchLinux安装.md","lastUpdated":1708510102000}'),o={name:"linux/env/ArchLinux安装.md"},p=l(`

ArchLinux安装

安装指南:https://wiki.archlinux.org/title/Installation_guide

镜像下载:https://archlinux.org/download/

使用UEFI模式引导

禁用Secure Boot

安装

使用ssh连接操作

ip add查看当前ip

passwd修改密码

设置镜像(可选)

shell
reflector -c China --sort rate --save /etc/pacman.d/mirrorlist
+reflector -c China --sort rate --save /mnt/etc/pacman.d/mirrorlist
reflector -c China --sort rate --save /etc/pacman.d/mirrorlist
+reflector -c China --sort rate --save /mnt/etc/pacman.d/mirrorlist

文件系统

查看磁盘设备情况fdisk -llsblk,以rom、loop、airoot结尾的设备可以忽略

磁盘分区

https://wiki.archlinux.org/title/EFI_system_partition

arch linux推荐的分区表设置

挂载点分区分区类型建议大小
/mnt/boot/dev/efi_system_partitionEFI system partitionAt least 300 MiB. If multiple kernels will be installed, then no less than 1 GiB.
[SWAP]/dev/*swap_partitionLinux swapMore than 512 MiB
/mnt/dev/root_partitionLinux x86-64 root (/)Remainder of the device
操作
shell
fdisk /dev/sda
+
+g # 将磁盘分区表设置为GPT格式
+
+n # 新增一个分区,分区号默认会递增,直接回车,起始扇区直接回车按默认值,结束输入+300M并回车,标识分区大小为300M
+# 重复新建分区操作,创建好3个空间大小分别为300M、1G、剩余全部空间的三个分区
+
+t # 更改分区类型
+# 重复t操作 把1、2、3分区分别改为1(EFI System Partition)、19(SWAP)、23(linux x86-64 root)
+
+w # 写入保存分区表
fdisk /dev/sda
+
+g # 将磁盘分区表设置为GPT格式
+
+n # 新增一个分区,分区号默认会递增,直接回车,起始扇区直接回车按默认值,结束输入+300M并回车,标识分区大小为300M
+# 重复新建分区操作,创建好3个空间大小分别为300M、1G、剩余全部空间的三个分区
+
+t # 更改分区类型
+# 重复t操作 把1、2、3分区分别改为1(EFI System Partition)、19(SWAP)、23(linux x86-64 root)
+
+w # 写入保存分区表
磁盘格式化
shell
mkfs.fat -F 32 /dev/sda1
+mkswap /dev/sda2
+mkfs.ext4 /dev/sda3
mkfs.fat -F 32 /dev/sda1
+mkswap /dev/sda2
+mkfs.ext4 /dev/sda3
分区挂载到文件系统
shell
mount /dev/sda3 /mnt
+mkdir -p /mnt/boot && mount /dev/sda1 /mnt/boot #创建/mnt/boot目录供挂载
+swapon /dev/sda2 # 挂载swap
mount /dev/sda3 /mnt
+mkdir -p /mnt/boot && mount /dev/sda1 /mnt/boot #创建/mnt/boot目录供挂载
+swapon /dev/sda2 # 挂载swap

安装基本软件包

shell
pacstrap -K /mnt base linux linux-firmware
+pacman -Sy
+pacman -S vim
pacstrap -K /mnt base linux linux-firmware
+pacman -Sy
+pacman -S vim

生成fstab

shell
genfstab -U /mnt >> /mnt/etc/fstab && cat /mnt/etc/fstab
genfstab -U /mnt >> /mnt/etc/fstab && cat /mnt/etc/fstab

更换当前根目录到硬盘上的系统

https://wiki.archlinux.org/title/Chroot

shell
arch-chroot /mnt
arch-chroot /mnt

调整时区

shell
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime # 切换时区到东8区 或者timedatectl set-timezone Asia/Shanghai
+
+hwclock --systohc # 将系统时间同步到硬件时钟 生成/etc/adjtime文件
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime # 切换时区到东8区 或者timedatectl set-timezone Asia/Shanghai
+
+hwclock --systohc # 将系统时间同步到硬件时钟 生成/etc/adjtime文件

编辑/etc/locale.gen配置本地化

shell
vim /etc/locale.gen
+# 将en_US.UTF-8和zh_CN.UTF-8注释去掉
+# 保存
+locale-gen
vim /etc/locale.gen
+# 将en_US.UTF-8和zh_CN.UTF-8注释去掉
+# 保存
+locale-gen

创建/etc/locale.conf文件并编辑

shell
#/etc/locale.conf
+LANG=en_US.UTF-8
#/etc/locale.conf
+LANG=en_US.UTF-8

主机名

shell
echo "archlinux" >> /etc/hostname
echo "archlinux" >> /etc/hostname

设置密码

shell
passwd
passwd

引导程序

shell
pacman -S dosfstools grub efibootmgr  # 安装引导程序
+grub-install --target=x86_64-efi --efi-directory=/boot --recheck  # 将grub安装至EFI分区
+grub-mkconfig -o /boot/grub/grub.cfg  # 生成grub配置
pacman -S dosfstools grub efibootmgr  # 安装引导程序
+grub-install --target=x86_64-efi --efi-directory=/boot --recheck  # 将grub安装至EFI分区
+grub-mkconfig -o /boot/grub/grub.cfg  # 生成grub配置

应用安装

shell
pacman -S networkmanager network-manager-applet dhcpcd dialog os-prober mtools ntfs-3g base-devel linux-headers reflector git net-tools dnsutils inetutils iproute2
pacman -S networkmanager network-manager-applet dhcpcd dialog os-prober mtools ntfs-3g base-devel linux-headers reflector git net-tools dnsutils inetutils iproute2

退出chroot环境重新启动

shell
exit  # 返回至arch-chroot之前的环境
+
+# 卸载
+umount /mnt/boot
+umount /mnt
+reboot  # 重启
exit  # 返回至arch-chroot之前的环境
+
+# 卸载
+umount /mnt/boot
+umount /mnt
+reboot  # 重启

安装后操作

shell
systemctl start dhcpcd  
+systemctl enable dhcpcd  # dhcpcd开机自启
+
+systemctl start sshd
+systemctl enable sshd # sshd开机自启
systemctl start dhcpcd  
+systemctl enable dhcpcd  # dhcpcd开机自启
+
+systemctl start sshd
+systemctl enable sshd # sshd开机自启

ssh配置

shell
# /etc/ssh/sshd_config
+PermitRootLogin yes
+PasswordAuthentication yes
# /etc/ssh/sshd_config
+PermitRootLogin yes
+PasswordAuthentication yes

systemctl restart sshd

静态ip

https://wiki.archlinux.org/title/Dhcpcd

shell
# /etc/dhcpcd.conf
+interface eth0
+static ip_address=192.168.2.67/24	
+static routers=192.168.2.1
+static domain_name_servers=192.168.2.1
# /etc/dhcpcd.conf
+interface eth0
+static ip_address=192.168.2.67/24	
+static routers=192.168.2.1
+static domain_name_servers=192.168.2.1

General recommendations

https://wiki.archlinux.org/title/General_recommendations

`,55),e=[p];function t(c,r,i,y,E,d){return a(),n("div",null,e)}const u=s(o,[["render",t]]);export{F as __pageData,u as default}; diff --git "a/assets/linux_env_ArchLinux\345\256\211\350\243\205.md.991f6f32.lean.js" "b/assets/linux_env_ArchLinux\345\256\211\350\243\205.md.991f6f32.lean.js" new file mode 100644 index 000000000..43182ba53 --- /dev/null +++ "b/assets/linux_env_ArchLinux\345\256\211\350\243\205.md.991f6f32.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const F=JSON.parse('{"title":"ArchLinux安装","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/ArchLinux安装.md","filePath":"linux/env/ArchLinux安装.md","lastUpdated":1708510102000}'),o={name:"linux/env/ArchLinux安装.md"},p=l("",55),e=[p];function t(c,r,i,y,E,d){return a(),n("div",null,e)}const u=s(o,[["render",t]]);export{F as __pageData,u as default}; diff --git "a/assets/linux_env_Centos7\345\256\211\350\243\205Python3\347\216\257\345\242\203.md.92d82d76.js" "b/assets/linux_env_Centos7\345\256\211\350\243\205Python3\347\216\257\345\242\203.md.92d82d76.js" new file mode 100644 index 000000000..fea2fd009 --- /dev/null +++ "b/assets/linux_env_Centos7\345\256\211\350\243\205Python3\347\216\257\345\242\203.md.92d82d76.js" @@ -0,0 +1,21 @@ +import{_ as s,o as a,c as l,Q as n}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"Centos7安装Python3环境","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/Centos7安装Python3环境.md","filePath":"linux/env/Centos7安装Python3环境.md","lastUpdated":1708510102000}'),p={name:"linux/env/Centos7安装Python3环境.md"},o=n(`

Centos7安装Python3环境

  1. 安装编译工具
bash
yum -y groupinstall "Development tools"
+yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel
+yum install -y libffi-devel zlib1g-dev
+yum install zlib* -y
yum -y groupinstall "Development tools"
+yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel
+yum install -y libffi-devel zlib1g-dev
+yum install zlib* -y
  1. 下载安装包
bash
wget https://www.python.org/ftp/python/3.7.2/Python-3.7.2.tar.xz
wget https://www.python.org/ftp/python/3.7.2/Python-3.7.2.tar.xz

这一步骤提示我wget命令找不到,所以要先安装wget

bash
yum -y install wget
yum -y install wget

再次执行下载安装包的命令

  1. 下载完成后解压并安装
bash
mkdir /usr/local/python3  
+tar -xvf  Python-3.7.2.tar.xz
+cd Python-3.7.2
+# 指定安装位置 提高运行速度 第三个是为了解决pip需要用到ssl
+./configure --prefix=/usr/local/python3 --with-ssl 
+make && make install
mkdir /usr/local/python3  
+tar -xvf  Python-3.7.2.tar.xz
+cd Python-3.7.2
+# 指定安装位置 提高运行速度 第三个是为了解决pip需要用到ssl
+./configure --prefix=/usr/local/python3 --with-ssl 
+make && make install

./configure --prefix=/usr/local/python3 --enable-optimizations --with-ssl --enable-optimizations参数可能在低版本gcc导致编译报错 去掉即可

  1. 创建软链接
bash
ln -s /usr/local/python3/bin/python3 /usr/local/bin/python3
+ln -s /usr/local/python3/bin/pip3 /usr/local/bin/pip3
ln -s /usr/local/python3/bin/python3 /usr/local/bin/python3
+ln -s /usr/local/python3/bin/pip3 /usr/local/bin/pip3
  1. 验证
bash
python3 -v
+pip3 -v
python3 -v
+pip3 -v
`,15),e=[o];function t(c,r,y,E,i,F){return a(),l("div",null,e)}const C=s(p,[["render",t]]);export{h as __pageData,C as default}; diff --git "a/assets/linux_env_Centos7\345\256\211\350\243\205Python3\347\216\257\345\242\203.md.92d82d76.lean.js" "b/assets/linux_env_Centos7\345\256\211\350\243\205Python3\347\216\257\345\242\203.md.92d82d76.lean.js" new file mode 100644 index 000000000..6b75ecad7 --- /dev/null +++ "b/assets/linux_env_Centos7\345\256\211\350\243\205Python3\347\216\257\345\242\203.md.92d82d76.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as l,Q as n}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"Centos7安装Python3环境","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/Centos7安装Python3环境.md","filePath":"linux/env/Centos7安装Python3环境.md","lastUpdated":1708510102000}'),p={name:"linux/env/Centos7安装Python3环境.md"},o=n("",15),e=[o];function t(c,r,y,E,i,F){return a(),l("div",null,e)}const C=s(p,[["render",t]]);export{h as __pageData,C as default}; diff --git "a/assets/linux_env_Linux\345\270\270\347\224\250\346\214\207\344\273\244.md.d450f692.js" "b/assets/linux_env_Linux\345\270\270\347\224\250\346\214\207\344\273\244.md.d450f692.js" new file mode 100644 index 000000000..89f9ab68b --- /dev/null +++ "b/assets/linux_env_Linux\345\270\270\347\224\250\346\214\207\344\273\244.md.d450f692.js" @@ -0,0 +1,21 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"Linux常用指令","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/Linux常用指令.md","filePath":"linux/env/Linux常用指令.md","lastUpdated":1708510102000}'),e={name:"linux/env/Linux常用指令.md"},p=l(`

Linux常用指令

设置时区

shell
# 查看当前时间、时区
+date 
+# 列出可用时区
+timedatectl list-timezones
+# 设置时区
+timedatectl set-timezone Asia/Shanghai
+# 确认已经更改
+timedatectl
# 查看当前时间、时区
+date 
+# 列出可用时区
+timedatectl list-timezones
+# 设置时区
+timedatectl set-timezone Asia/Shanghai
+# 确认已经更改
+timedatectl

修改主机名

shell
# 查看主机信息
+hostnamectl
+# 修改主机名
+hostnamectl set-hostname newhostname
# 查看主机信息
+hostnamectl
+# 修改主机名
+hostnamectl set-hostname newhostname
`,5),o=[p];function t(c,i,r,d,h,y){return a(),n("div",null,o)}const E=s(e,[["render",t]]);export{m as __pageData,E as default}; diff --git "a/assets/linux_env_Linux\345\270\270\347\224\250\346\214\207\344\273\244.md.d450f692.lean.js" "b/assets/linux_env_Linux\345\270\270\347\224\250\346\214\207\344\273\244.md.d450f692.lean.js" new file mode 100644 index 000000000..ee90a61d8 --- /dev/null +++ "b/assets/linux_env_Linux\345\270\270\347\224\250\346\214\207\344\273\244.md.d450f692.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"Linux常用指令","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/Linux常用指令.md","filePath":"linux/env/Linux常用指令.md","lastUpdated":1708510102000}'),e={name:"linux/env/Linux常用指令.md"},p=l("",5),o=[p];function t(c,i,r,d,h,y){return a(),n("div",null,o)}const E=s(e,[["render",t]]);export{m as __pageData,E as default}; diff --git "a/assets/linux_env_Linux\346\234\215\345\212\241\345\231\250\346\226\207\344\273\266\347\233\256\345\275\225\345\205\261\344\272\253\346\230\240\345\260\204\351\205\215\347\275\256.md.2e59da1d.js" "b/assets/linux_env_Linux\346\234\215\345\212\241\345\231\250\346\226\207\344\273\266\347\233\256\345\275\225\345\205\261\344\272\253\346\230\240\345\260\204\351\205\215\347\275\256.md.2e59da1d.js" new file mode 100644 index 000000000..581c0ce51 --- /dev/null +++ "b/assets/linux_env_Linux\346\234\215\345\212\241\345\231\250\346\226\207\344\273\266\347\233\256\345\275\225\345\205\261\344\272\253\346\230\240\345\260\204\351\205\215\347\275\256.md.2e59da1d.js" @@ -0,0 +1,3 @@ +import{_ as a,o as s,c as e,Q as n}from"./chunks/framework.dc1f4608.js";const f=JSON.parse('{"title":"Linux服务器文件目录共享映射配置","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/Linux服务器文件目录共享映射配置.md","filePath":"linux/env/Linux服务器文件目录共享映射配置.md","lastUpdated":1708510102000}'),o={name:"linux/env/Linux服务器文件目录共享映射配置.md"},l=n(`

Linux服务器文件目录共享映射配置

使用场景:由于公司没有单独的文件服务器/nas之类的共享存储,图片之类的也没有上云,目前业务中上传的文件也是放在应用服务器当中的。生产环境都是高可用的多节点部署,这样就会产生问题。比如用户上传一张图片,请求被分发到A服务器中,由于图片访问的域名是统一的B服务器域名,上传完成后用B服务器的域名去访问静态文件就会404。后续我们考虑搭建fastdfs来统一管理文件。目前的临时解决方案是通过配置服务器共享目录来实现文件在多个节点服务器间的同步。

准备工作

  • 环境:CentOS 7.6

  • 分别在A、B两台服务器上安装nfs和rpcbind

    • yum install nfs-utils rpcbind

server配置

比如我们的静态资源存储在B服务器,则需要把B服务器的目录暴露出来,共享给A服务器,让A服务器挂载该目录。

  • 在B服务器(提供资源的服务器)上修改/etc/exports文件,把指定的目录暴露给A服务器并分配权限

    bash
    # /需要暴露的目录 A服务器IP(rw,async,no_root_squash)
    +/data/images 192.168.111.1(rw,async,no_root_squash)
    # /需要暴露的目录 A服务器IP(rw,async,no_root_squash)
    +/data/images 192.168.111.1(rw,async,no_root_squash)
  • 修改完后需要使配置立即生效,执行exportfs -arv命令

  • 关闭防火墙/端口111(tcp/udp)、2049(tcp)、4046(udp)向指定ip开放

启动nfs服务和rpcbind服务

  • service rpcbind start
  • service nfs start

在A服务器中挂载远程目录

执行命令:mount -o rw -t nfs B服务器IP:/B服务器暴露的路径 /要映射的本机(A服务器)的路径

例如:mount -o rw -t nfs 192.168.111.2:/data/images /data/images

验证

可以在A服务器的/data/images下上传一些文件,然后看B服务器中的/data/images目录中是否同步,如果同步了则说明挂载成功,共享目录就配置完成了。

`,14),t=[l];function r(i,p,c,d,u,h){return s(),e("div",null,t)}const b=a(o,[["render",r]]);export{f as __pageData,b as default}; diff --git "a/assets/linux_env_Linux\346\234\215\345\212\241\345\231\250\346\226\207\344\273\266\347\233\256\345\275\225\345\205\261\344\272\253\346\230\240\345\260\204\351\205\215\347\275\256.md.2e59da1d.lean.js" "b/assets/linux_env_Linux\346\234\215\345\212\241\345\231\250\346\226\207\344\273\266\347\233\256\345\275\225\345\205\261\344\272\253\346\230\240\345\260\204\351\205\215\347\275\256.md.2e59da1d.lean.js" new file mode 100644 index 000000000..14563b374 --- /dev/null +++ "b/assets/linux_env_Linux\346\234\215\345\212\241\345\231\250\346\226\207\344\273\266\347\233\256\345\275\225\345\205\261\344\272\253\346\230\240\345\260\204\351\205\215\347\275\256.md.2e59da1d.lean.js" @@ -0,0 +1 @@ +import{_ as a,o as s,c as e,Q as n}from"./chunks/framework.dc1f4608.js";const f=JSON.parse('{"title":"Linux服务器文件目录共享映射配置","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/Linux服务器文件目录共享映射配置.md","filePath":"linux/env/Linux服务器文件目录共享映射配置.md","lastUpdated":1708510102000}'),o={name:"linux/env/Linux服务器文件目录共享映射配置.md"},l=n("",14),t=[l];function r(i,p,c,d,u,h){return s(),e("div",null,t)}const b=a(o,[["render",r]]);export{f as __pageData,b as default}; diff --git "a/assets/linux_env_Linux\347\247\201\351\222\245\347\231\273\351\231\206\346\217\220\347\244\272server refused our key.md.23bf3202.js" "b/assets/linux_env_Linux\347\247\201\351\222\245\347\231\273\351\231\206\346\217\220\347\244\272server refused our key.md.23bf3202.js" new file mode 100644 index 000000000..7ebd3a7e1 --- /dev/null +++ "b/assets/linux_env_Linux\347\247\201\351\222\245\347\231\273\351\231\206\346\217\220\347\244\272server refused our key.md.23bf3202.js" @@ -0,0 +1,15 @@ +import{_ as s,o as a,c as e,Q as n}from"./chunks/framework.dc1f4608.js";const E=JSON.parse('{"title":"Linux私钥登陆提示server refused our key","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/Linux私钥登陆提示server refused our key.md","filePath":"linux/env/Linux私钥登陆提示server refused our key.md","lastUpdated":1708510102000}'),l={name:"linux/env/Linux私钥登陆提示server refused our key.md"},o=n(`

Linux私钥登陆提示server refused our key

背景

家庭内网装了个物理机的Ubuntu server,用的最新版本的22.04,然后用windows端的mobaxterm和navicat使用ssh私钥连接内网服务器时返回了Server refused our key的异常

问题原因

openssh 8.8开始默认禁用了使用SHA-1哈希算法的RSA签名,看了一下ubuntu server 22.04的默认openssh版本:

shell
  ~ ssh -V
+OpenSSH_8.9p1 Ubuntu-3, OpenSSL 3.0.2 15 Mar 2022
  ~ ssh -V
+OpenSSH_8.9p1 Ubuntu-3, OpenSSL 3.0.2 15 Mar 2022

https://www.openssh.com/txt/release-8.8

This release disables RSA signatures using the SHA-1 hash algorithm by default. This change has been made as the SHA-1 hash algorithm is cryptographically broken, and it is possible to create chosen-prefix hash collisions for <USD$50K [1]

解决方案

shell
vim /etc/ssh/sshd_config
+
+# 添加配置
+PubkeyAcceptedKeyTypes +ssh-rsa
+
+
+systemctl restart sshd
vim /etc/ssh/sshd_config
+
+# 添加配置
+PubkeyAcceptedKeyTypes +ssh-rsa
+
+
+systemctl restart sshd
`,9),p=[o];function r(t,c,i,y,d,h){return a(),e("div",null,p)}const F=s(l,[["render",r]]);export{E as __pageData,F as default}; diff --git "a/assets/linux_env_Linux\347\247\201\351\222\245\347\231\273\351\231\206\346\217\220\347\244\272server refused our key.md.23bf3202.lean.js" "b/assets/linux_env_Linux\347\247\201\351\222\245\347\231\273\351\231\206\346\217\220\347\244\272server refused our key.md.23bf3202.lean.js" new file mode 100644 index 000000000..3c70ce411 --- /dev/null +++ "b/assets/linux_env_Linux\347\247\201\351\222\245\347\231\273\351\231\206\346\217\220\347\244\272server refused our key.md.23bf3202.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as e,Q as n}from"./chunks/framework.dc1f4608.js";const E=JSON.parse('{"title":"Linux私钥登陆提示server refused our key","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/Linux私钥登陆提示server refused our key.md","filePath":"linux/env/Linux私钥登陆提示server refused our key.md","lastUpdated":1708510102000}'),l={name:"linux/env/Linux私钥登陆提示server refused our key.md"},o=n("",9),p=[o];function r(t,c,i,y,d,h){return a(),e("div",null,p)}const F=s(l,[["render",r]]);export{E as __pageData,F as default}; diff --git "a/assets/linux_env_Linux\350\256\276\347\275\256swap\347\251\272\351\227\264.md.84a44d7f.js" "b/assets/linux_env_Linux\350\256\276\347\275\256swap\347\251\272\351\227\264.md.84a44d7f.js" new file mode 100644 index 000000000..a0a1c0b3b --- /dev/null +++ "b/assets/linux_env_Linux\350\256\276\347\275\256swap\347\251\272\351\227\264.md.84a44d7f.js" @@ -0,0 +1 @@ +import{_ as e,o as a,c as s,Q as p}from"./chunks/framework.dc1f4608.js";const _=JSON.parse('{"title":"Linux设置swap空间","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/Linux设置swap空间.md","filePath":"linux/env/Linux设置swap空间.md","lastUpdated":1708510102000}'),c={name:"linux/env/Linux设置swap空间.md"},o=p('

Linux设置swap空间

服务器上日常运行着静态博客+云笔记+jenkins的容器,jenkins用于gitee上托管的博客repo的ci/cd。当jenkins进行构建时,内存占用率会急剧升高,轻则容器宕机,重则服务器跟着一起boom。因此需要设置swap来缓解jenkins内存占用瞬时升高的状况。

查看磁盘分区

  • df -h

image-20220310010316020

Filesystem中/dev/vda1挂载点为/的就是我们的磁盘

一般来说swap大小设置规则:

4G以内RAM,Swap设置为2倍RAM

4G-8GRAM,Swap设置等于内存大小

8G-64G,Swap设置为8G

64G-256G,Swap设置为16G

创建Swap分区文件

我这个是腾讯云4c 4G 80G的机器,这里swap设置为4G:fallocate -l 4G /swap

启用Swap分区

修改权限使文件只能root访问:chmod 600 /swap

将文件标记为swap空间:mkswap /swap

启用swap文件:swapon /swap

验证交换是否可用:swapon --show

持久化swap挂载

备份fstab:cp /etc/fstab /etc/fstab.bak

添加一条记录: echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

调整Swap设置

  • swappiness

调整Swappiness参数,该参数主要配置系统将数据从RAM交换到交换空间的频率。该参数的值是介于0和100之间的百分比。范围在0到100之间。 低参数值会让内核尽量少用交换,更高参数值会使内核更多的去使用交换空间。

查看当前swappiness值:cat /proc/sys/vm/swappiness

临时修改,重启失效:sysctl vm.swappiness=10

永久设置:vim /etc/sysctl.conf,最后一行加上vm.swappiness=10,保存并退出。sysctl -p 立即生效

  • vfs_cache_pressure 表示强制Linux VM最低保留多少空闲内存(Kbytes)。当可用内存低于这个参数时,系统开始回收cache内存,以释放内存,直到可用内存大于这个值。 查看 cat /proc/sys/vm/vfs_cache_pressure 缺省值100表示内核将根据pagecache和swapcache,把directory和inode cache保持在一个合理的百分比;降低该值低于100,将导致内核倾向于保留directory和inode cache;增加该值超过100,将导致内核倾向于回收directory和inode cache。

vim /etc/sysctl.conf,最后一行加上vm.vfs_cache_pressure=50

',25),t=[o];function i(d,n,r,l,w,h){return a(),s("div",null,t)}const m=e(c,[["render",i]]);export{_ as __pageData,m as default}; diff --git "a/assets/linux_env_Linux\350\256\276\347\275\256swap\347\251\272\351\227\264.md.84a44d7f.lean.js" "b/assets/linux_env_Linux\350\256\276\347\275\256swap\347\251\272\351\227\264.md.84a44d7f.lean.js" new file mode 100644 index 000000000..69b80e226 --- /dev/null +++ "b/assets/linux_env_Linux\350\256\276\347\275\256swap\347\251\272\351\227\264.md.84a44d7f.lean.js" @@ -0,0 +1 @@ +import{_ as e,o as a,c as s,Q as p}from"./chunks/framework.dc1f4608.js";const _=JSON.parse('{"title":"Linux设置swap空间","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/Linux设置swap空间.md","filePath":"linux/env/Linux设置swap空间.md","lastUpdated":1708510102000}'),c={name:"linux/env/Linux设置swap空间.md"},o=p("",25),t=[o];function i(d,n,r,l,w,h){return a(),s("div",null,t)}const m=e(c,[["render",i]]);export{_ as __pageData,m as default}; diff --git "a/assets/linux_env_Linux\350\256\277\351\227\256\346\235\203\351\231\220\346\216\247\345\210\266\344\271\213ACL.md.66efcc69.js" "b/assets/linux_env_Linux\350\256\277\351\227\256\346\235\203\351\231\220\346\216\247\345\210\266\344\271\213ACL.md.66efcc69.js" new file mode 100644 index 000000000..f2bce10b0 --- /dev/null +++ "b/assets/linux_env_Linux\350\256\277\351\227\256\346\235\203\351\231\220\346\216\247\345\210\266\344\271\213ACL.md.66efcc69.js" @@ -0,0 +1 @@ +import{_ as e,o as t,c as a,Q as s}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"Linux访问权限控制之ACL","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/Linux访问权限控制之ACL.md","filePath":"linux/env/Linux访问权限控制之ACL.md","lastUpdated":1708510102000}'),o={name:"linux/env/Linux访问权限控制之ACL.md"},r=s('

Linux访问权限控制之ACL

什么是ACL

ACL即Access Control List,访问控制列表,POSIX 1003.1e/1003.2c标准中定义的权限管理方式。详细内容可以参考POSIX Access Control Lists on Linux这篇文章。

POSIX:POSIX是可移植操作系统接口(Portable Operating System Interface of UNIX)的缩写,POSIX标准定义了操作系统应该为应用程序提供的接口标准,是在各种UNIX操作系统上运行的软件的一系列API标准的总称。POSIX标准意在期望获得源代码级别的软件可移植性。换句话说,为一个POSIX兼容的操作系统编写的程序,应该可以在任何其它的POSIX操作系统(即使是来自另一个厂商)上编译执行。

为什么使用ACL

经典的Unix权限模型中每个文件系统对象都定义了三组权限,owner、group和other。也就是常用的chmod命令中修改的权限,包括读(r)、写入(w)和执行(x)。但是如果我们想更细颗粒度的控制某些用户的权限时,使用chmodchown就很难办了。这就需要ACL来针对单一用户、单一文件或目录来进行权限控制。

ACL的工作原理

ACL由一系列Access Entry组成,Access Entry又包括三个部分:Entry type,qualifier(非必须)、权限

Entry type有以下类型:

  • Owner/ACL_USER_OBJ : 相当于Linux里file_owner的权限

  • Named user/ACL_USER: 定义了额外的用户可以对此文件拥有的权限

  • Owning group/ACL_GROUP_OBJ: 相当于Linux里group的权限

  • Named group/ACL_GROUP: 定义了额外的组可以对此文件拥有的权限

  • Mask/ACL_MASK: 定义了ACL_USER, ACL_GROUP_OBJ和ACL_GROUP的最大权限

  • Others/ACL_OTHER: 相当于Linux里other的权限

访问检查算法

进程请求访问文件系统对象。执行两个步骤。第一步选择与请求进程最匹配的 ACL 条目。ACL 条目按以下顺序查看:所有者、命名用户、(拥有或命名)组、其他。只有一个条目决定访问。第二步检查匹配条目是否包含足够的权限。

一个进程可以是多个组中的成员,因此可以匹配多个组条目。如果这些匹配的组条目中的任何一个包含请求的权限,则选择包含请求的权限的条目(无论选择哪个条目,结果都是相同的)。如果没有匹配的组条目包含请求的权限,则无论选择哪个条目都将拒绝访问。

访问检查算法可以用伪代码描述如下。

  • if

    进程的用户ID是所有者,所有者条目决定访问权限

  • else if

    进程的用户 ID 与指定用户条目之一中的限定符匹配,此条目确定访问权限

  • else if

    进程的组 ID 之一与所属组匹配,并且所属组条目包含请求的权限,此条目确定访问权限

  • else if

    进程的组 ID 之一与命名组条目之一的限定符匹配,并且此条目包含请求的权限,此条目确定访问权限

  • else if

    进程的组 ID 之一与所属组或任何命名组条目匹配,但所属组条目或任何匹配命名组条目均不包含请求的权限,这确定访问被拒绝

  • else

    另一个条目决定访问。

  • if

    此选择产生的匹配条目是所有者或其他条目,并且它包含请求的权限,授予访问权限

  • else if

    匹配条目是命名用户、拥有组或命名组条目,并且此条目包含请求的权限,并且掩码条目也包含请求的权限(或没有掩码条目),授予访问权限

  • else

    访问被拒绝。

The access check algorithm can be described in pseudo-code as follows.

  • If

    the user ID of the process is the owner, the owner entry determines access

  • else if

    the user ID of the process matches the qualifier in one of the named user entries, this entry determines access

  • else if

    one of the group IDs of the process matches the owning group and the owning group entry contains the requested permissions, this entry determines access

  • else if

    one of the group IDs of the process matches the qualifier of one of the named group entries and this entry contains the requested permissions, this entry determines access

  • else if

    one of the group IDs of the process matches the owning group or any of the named group entries, but neither the owning group entry nor any of the matching named group entries contains the requested permissions, this determines that access is denied

  • else

    the other entry determines access.

  • If

    the matching entry resulting from this selection is the owner or other entry and it contains the requested permissions, access is granted

  • else if

    the matching entry is a named user, owning group, or named group entry and this entry contains the requested permissions and the mask entry also contains the requested permissions (or there is no mask entry), access is granted

  • else

    access is denied.

ACL命令

  • getfacl:查看某个文件/目录的ACL权限

  • setfacl:设置某个文件/目录的ACL权限

    • -m:设置后续acl参数
    • -x:删除后续acl参数
    • -b:删除全部acl参数
    • -k:删除默认acl参数
    • -R:递归设置acl参数
    • -d:设置默认acl参数

例子

设置指定用户/组权限

  • setfacl -m u:user1:rx /test:给user1用户设置/test目录的rx(r-x)权限
  • setfacl -m g:group1:rx /test:给group1组设置/test目录的rx(r-x)权限
  • setfacl -m o:--x /test:给other设置x(--x)权限(chmod o+x /test)

默认ACL权限

默认权限下新建的子目录会继承父目录的权限,只有目录才能给默认权限,不是目录的对象仅继承父目录的默认 ACL 作为其访问 ACL。

  • setfacl -m d:u:user1:rx /test:给user1用户设置/test目录的默认rx权限
  • setfacl -m d:g:group1:rx /test:给group1组设置/test目录的默认rx权限
  • setfacl -m d:o:--x /test:给other设置/test目录的默认x权限

最大有效权限mask

如上文所描述的,mask定义了ACL_USER, ACL_GROUP_OBJ和ACL_GROUP的最大权限。例如,mask权限为r--,此时不管ACL_USER的权限为多大,就算是``rwx,最终生效的都只会是r--`。概括来说:用户和用户组的权限必须在mask权限的范围之内才能生效,mask权限就是最大有效权限。

修改mask权限

setfacl -m m:rx /test:设置mask权限为r-x,使用setfacl -m m:权限 路径

删除所有acl权限

setfacl -b /test

删除指定acl权限

setfacl -x u:user1 /test

',32),i=[r];function l(n,c,p,h,d,u){return t(),a("div",null,i)}const f=e(o,[["render",l]]);export{m as __pageData,f as default}; diff --git "a/assets/linux_env_Linux\350\256\277\351\227\256\346\235\203\351\231\220\346\216\247\345\210\266\344\271\213ACL.md.66efcc69.lean.js" "b/assets/linux_env_Linux\350\256\277\351\227\256\346\235\203\351\231\220\346\216\247\345\210\266\344\271\213ACL.md.66efcc69.lean.js" new file mode 100644 index 000000000..128e51b44 --- /dev/null +++ "b/assets/linux_env_Linux\350\256\277\351\227\256\346\235\203\351\231\220\346\216\247\345\210\266\344\271\213ACL.md.66efcc69.lean.js" @@ -0,0 +1 @@ +import{_ as e,o as t,c as a,Q as s}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"Linux访问权限控制之ACL","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/Linux访问权限控制之ACL.md","filePath":"linux/env/Linux访问权限控制之ACL.md","lastUpdated":1708510102000}'),o={name:"linux/env/Linux访问权限控制之ACL.md"},r=s("",32),i=[r];function l(n,c,p,h,d,u){return t(),a("div",null,i)}const f=e(o,[["render",l]]);export{m as __pageData,f as default}; diff --git a/assets/linux_env_Ubuntu-server.md.b53d8eca.js b/assets/linux_env_Ubuntu-server.md.b53d8eca.js new file mode 100644 index 000000000..992f9083c --- /dev/null +++ b/assets/linux_env_Ubuntu-server.md.b53d8eca.js @@ -0,0 +1,243 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"Ubuntu-server","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/Ubuntu-server.md","filePath":"linux/env/Ubuntu-server.md","lastUpdated":1708510102000}'),p={name:"linux/env/Ubuntu-server.md"},o=l(`

Ubuntu-server

关闭欢迎提示

chmod -x /etc/update-motd.d/*

关闭ssh登录motd广告

vim /etc/default/motd-news 将enabled改为0

关闭ssh登录系统信息

apt remove landscape-common landscape-client

系统盘迁移

shell
# 1.备份数据
+# 2.制作一个linux启动盘 例如live server的
+# 3.连接原启动盘和需要迁移到的目标盘
+# 4.U盘启动直接进入shell
+# 5.查看磁盘信息
+lsblk
+# 6.dd命令直接全盘迁移
+dd if=/dev/sda of=/dev/sdb bs=4096 conv=sync,noerror
+# 7.拷贝完成后使用新磁盘启动
+# 8.删除旧分区&resize
+fdisk /dev/sdX
+d #删除磁盘最后一个分区
+n #新建一个分区,扇区开始结束都用默认即可
+w #写盘保存
+
+partprobe #重新读取分区表并更新分区信息
+
+resize2fs /dev/sdXX #调整文件系统的大小
# 1.备份数据
+# 2.制作一个linux启动盘 例如live server的
+# 3.连接原启动盘和需要迁移到的目标盘
+# 4.U盘启动直接进入shell
+# 5.查看磁盘信息
+lsblk
+# 6.dd命令直接全盘迁移
+dd if=/dev/sda of=/dev/sdb bs=4096 conv=sync,noerror
+# 7.拷贝完成后使用新磁盘启动
+# 8.删除旧分区&resize
+fdisk /dev/sdX
+d #删除磁盘最后一个分区
+n #新建一个分区,扇区开始结束都用默认即可
+w #写盘保存
+
+partprobe #重新读取分区表并更新分区信息
+
+resize2fs /dev/sdXX #调整文件系统的大小

内核模块加载

最近打算给home server换一块瑞昱的2.5G网卡(8125b芯片),所以想提前安装下网卡驱动,执行官网那个autorun.sh脚本前也没仔细看, 结果脚本直接把原来的8169网卡驱动卸载了,导致机器直接失联。还好脚本里有备份,所以只需要恢复回去就行了。

shell
#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-only
+
+# invoke insmod with all arguments we got
+# and use a pathname, as insmod doesn't look in . by default
+
+TARGET_PATH=$(find /lib/modules/$(uname -r)/kernel/drivers/net/ethernet -name realtek -type d)
+if [ "$TARGET_PATH" = "" ]; then
+	TARGET_PATH=$(find /lib/modules/$(uname -r)/kernel/drivers/net -name realtek -type d)
+fi
+if [ "$TARGET_PATH" = "" ]; then
+	TARGET_PATH=/lib/modules/$(uname -r)/kernel/drivers/net
+fi
+echo
+echo "Check old driver and unload it."
+check=\`lsmod | grep r8169\`
+if [ "$check" != "" ]; then
+        echo "rmmod r8169"
+        /sbin/rmmod r8169
+fi
+
+check=\`lsmod | grep r8125\`
+if [ "$check" != "" ]; then
+        echo "rmmod r8125"
+        /sbin/rmmod r8125
+fi
+
+echo "Build the module and install"
+echo "-------------------------------" >> log.txt
+date 1>>log.txt
+make $@ all 1>>log.txt || exit 1
+module=\`ls src/*.ko\`
+module=\${module#src/}
+module=\${module%.ko}
+
+if [ "$module" = "" ]; then
+	echo "No driver exists!!!"
+	exit 1
+elif [ "$module" != "r8169" ]; then
+	if test -e $TARGET_PATH/r8169.ko ; then
+		echo "Backup r8169.ko"
+		if test -e $TARGET_PATH/r8169.bak ; then
+			i=0
+			while test -e $TARGET_PATH/r8169.bak$i
+			do
+				i=$(($i+1))
+			done
+			echo "rename r8169.ko to r8169.bak$i"
+			mv $TARGET_PATH/r8169.ko $TARGET_PATH/r8169.bak$i
+		else
+			echo "rename r8169.ko to r8169.bak"
+			mv $TARGET_PATH/r8169.ko $TARGET_PATH/r8169.bak
+		fi
+	fi
+fi
+
+echo "DEPMOD $(uname -r)"
+depmod \`uname -r\`
+echo "load module $module"
+modprobe $module
+
+is_update_initramfs=n
+distrib_list="ubuntu debian"
+
+if [ -r /etc/debian_version ]; then
+	is_update_initramfs=y
+elif [ -r /etc/lsb-release ]; then
+	for distrib in $distrib_list
+	do
+		/bin/grep -i "$distrib" /etc/lsb-release 2>&1 /dev/null && \\
+			is_update_initramfs=y && break
+	done
+fi
+
+if [ "$is_update_initramfs" = "y" ]; then
+	if which update-initramfs >/dev/null ; then
+		echo "Updating initramfs. Please wait."
+		update-initramfs -u -k $(uname -r)
+	else
+		echo "update-initramfs: command not found"
+		exit 1
+	fi
+fi
+
+echo "Completed."
+exit 0
#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-only
+
+# invoke insmod with all arguments we got
+# and use a pathname, as insmod doesn't look in . by default
+
+TARGET_PATH=$(find /lib/modules/$(uname -r)/kernel/drivers/net/ethernet -name realtek -type d)
+if [ "$TARGET_PATH" = "" ]; then
+	TARGET_PATH=$(find /lib/modules/$(uname -r)/kernel/drivers/net -name realtek -type d)
+fi
+if [ "$TARGET_PATH" = "" ]; then
+	TARGET_PATH=/lib/modules/$(uname -r)/kernel/drivers/net
+fi
+echo
+echo "Check old driver and unload it."
+check=\`lsmod | grep r8169\`
+if [ "$check" != "" ]; then
+        echo "rmmod r8169"
+        /sbin/rmmod r8169
+fi
+
+check=\`lsmod | grep r8125\`
+if [ "$check" != "" ]; then
+        echo "rmmod r8125"
+        /sbin/rmmod r8125
+fi
+
+echo "Build the module and install"
+echo "-------------------------------" >> log.txt
+date 1>>log.txt
+make $@ all 1>>log.txt || exit 1
+module=\`ls src/*.ko\`
+module=\${module#src/}
+module=\${module%.ko}
+
+if [ "$module" = "" ]; then
+	echo "No driver exists!!!"
+	exit 1
+elif [ "$module" != "r8169" ]; then
+	if test -e $TARGET_PATH/r8169.ko ; then
+		echo "Backup r8169.ko"
+		if test -e $TARGET_PATH/r8169.bak ; then
+			i=0
+			while test -e $TARGET_PATH/r8169.bak$i
+			do
+				i=$(($i+1))
+			done
+			echo "rename r8169.ko to r8169.bak$i"
+			mv $TARGET_PATH/r8169.ko $TARGET_PATH/r8169.bak$i
+		else
+			echo "rename r8169.ko to r8169.bak"
+			mv $TARGET_PATH/r8169.ko $TARGET_PATH/r8169.bak
+		fi
+	fi
+fi
+
+echo "DEPMOD $(uname -r)"
+depmod \`uname -r\`
+echo "load module $module"
+modprobe $module
+
+is_update_initramfs=n
+distrib_list="ubuntu debian"
+
+if [ -r /etc/debian_version ]; then
+	is_update_initramfs=y
+elif [ -r /etc/lsb-release ]; then
+	for distrib in $distrib_list
+	do
+		/bin/grep -i "$distrib" /etc/lsb-release 2>&1 /dev/null && \\
+			is_update_initramfs=y && break
+	done
+fi
+
+if [ "$is_update_initramfs" = "y" ]; then
+	if which update-initramfs >/dev/null ; then
+		echo "Updating initramfs. Please wait."
+		update-initramfs -u -k $(uname -r)
+	else
+		echo "update-initramfs: command not found"
+		exit 1
+	fi
+fi
+
+echo "Completed."
+exit 0
shell
# 进入网卡驱动目录
+cd /lib/modules/$(uname -r)//kernel/drivers/net/ethernet/realtek
+# 恢复备份
+mv r8169.bak r8169.ko
+# 加载模块
+insmod r8169.ko
+# 验证是否加载
+lsmod | grep r8169
+# 更新模块依赖关系
+depmod \`uname -r\`
+# 更新initramfs
+update-initramfs -u -k $(uname -r)
# 进入网卡驱动目录
+cd /lib/modules/$(uname -r)//kernel/drivers/net/ethernet/realtek
+# 恢复备份
+mv r8169.bak r8169.ko
+# 加载模块
+insmod r8169.ko
+# 验证是否加载
+lsmod | grep r8169
+# 更新模块依赖关系
+depmod \`uname -r\`
+# 更新initramfs
+update-initramfs -u -k $(uname -r)

瑞昱2.5G网卡r8125b驱动安装

A DKMS package for easy use of Realtek r8125 driver, which supports 2.5 GbE. https://github.com/awesometic/realtek-r8125-dkms

  • sudo add-apt-repository ppa:awesometic/ppa

  • sudo apt install realtek-r8125-dkms

  • shell
    sudo tee -a /etc/modprobe.d/blacklist-r8169.conf > /dev/null <<EOT
    +# To use r8125 driver explicitly
    +blacklist r8169
    +EOT
    sudo tee -a /etc/modprobe.d/blacklist-r8169.conf > /dev/null <<EOT
    +# To use r8125 driver explicitly
    +blacklist r8169
    +EOT
  • sudo update-initramfs -u

  • vim /etc/netplan/00-installer-config.yaml

  • shell
    # This is the network config written by 'subiquity'
    +network:
    +  ethernets:
    +    enp1s0: # 根据ifconfig -a找到2.5G网卡设备的名称
    +      dhcp4: true
    +  version: 2
    # This is the network config written by 'subiquity'
    +network:
    +  ethernets:
    +    enp1s0: # 根据ifconfig -a找到2.5G网卡设备的名称
    +      dhcp4: true
    +  version: 2
  • netplan apply

  • reboot

`,15),e=[o];function t(c,r,y,E,F,i){return n(),a("div",null,e)}const C=s(p,[["render",t]]);export{u as __pageData,C as default}; diff --git a/assets/linux_env_Ubuntu-server.md.b53d8eca.lean.js b/assets/linux_env_Ubuntu-server.md.b53d8eca.lean.js new file mode 100644 index 000000000..78e1c1295 --- /dev/null +++ b/assets/linux_env_Ubuntu-server.md.b53d8eca.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"Ubuntu-server","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/Ubuntu-server.md","filePath":"linux/env/Ubuntu-server.md","lastUpdated":1708510102000}'),p={name:"linux/env/Ubuntu-server.md"},o=l("",15),e=[o];function t(c,r,y,E,F,i){return n(),a("div",null,e)}const C=s(p,[["render",t]]);export{u as __pageData,C as default}; diff --git "a/assets/linux_env_bash\345\270\270\347\224\250\347\232\204\345\277\253\346\215\267\351\224\256.md.7a89f167.js" "b/assets/linux_env_bash\345\270\270\347\224\250\347\232\204\345\277\253\346\215\267\351\224\256.md.7a89f167.js" new file mode 100644 index 000000000..988f21149 --- /dev/null +++ "b/assets/linux_env_bash\345\270\270\347\224\250\347\232\204\345\277\253\346\215\267\351\224\256.md.7a89f167.js" @@ -0,0 +1 @@ +import{_ as l,o as t,c as i,Q as a}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"bash常用的快捷键","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/bash常用的快捷键.md","filePath":"linux/env/bash常用的快捷键.md","lastUpdated":1708510102000}'),e={name:"linux/env/bash常用的快捷键.md"},c=a('

bash常用的快捷键

  • ctrl+a:光标移到行首
  • ctrl+b:光标左移一个字母
  • ctrl+c:杀死当前进程
  • ctrl+d:退出当前Shell
  • ctrl+e:光标移到行尾
  • ctrl+h:删除光标前一个字符,同backspace键相同
  • ctrl+k:清除光标后至行尾的内容
  • ctrl+l:清屏,相当于clear
  • ctrl+r:搜索之前打过的命令会有一个提示,根据你输入的关键字进行搜索bash的history
  • ctrl+u:清除光标前至行首间的所有内容。
  • ctrl+w:移除光标前的一个单词
  • ctrl+t:交换光标位置前的两个字符
  • ctrl+y:粘贴或者恢复上次的删除
  • ctrl+d:删除光标所在字母;注意和backspace以及ctrl+h的区别,这2个是删除光标前的字符
  • ctrl+f:光标右移
  • ctrl+z:把当前进程转到后台运行,使用’fg‘命令恢复。比如top-d1然后ctrl+z,到后台,然后fg,重新恢复
',2),r=[c];function s(_,o,n,h,d,b){return t(),i("div",null,r)}const f=l(e,[["render",s]]);export{u as __pageData,f as default}; diff --git "a/assets/linux_env_bash\345\270\270\347\224\250\347\232\204\345\277\253\346\215\267\351\224\256.md.7a89f167.lean.js" "b/assets/linux_env_bash\345\270\270\347\224\250\347\232\204\345\277\253\346\215\267\351\224\256.md.7a89f167.lean.js" new file mode 100644 index 000000000..169c1a6ea --- /dev/null +++ "b/assets/linux_env_bash\345\270\270\347\224\250\347\232\204\345\277\253\346\215\267\351\224\256.md.7a89f167.lean.js" @@ -0,0 +1 @@ +import{_ as l,o as t,c as i,Q as a}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"bash常用的快捷键","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/bash常用的快捷键.md","filePath":"linux/env/bash常用的快捷键.md","lastUpdated":1708510102000}'),e={name:"linux/env/bash常用的快捷键.md"},c=a("",2),r=[c];function s(_,o,n,h,d,b){return t(),i("div",null,r)}const f=l(e,[["render",s]]);export{u as __pageData,f as default}; diff --git "a/assets/linux_env_centos7\351\230\262\347\201\253\345\242\231\345\221\275\344\273\244.md.15b22e4a.js" "b/assets/linux_env_centos7\351\230\262\347\201\253\345\242\231\345\221\275\344\273\244.md.15b22e4a.js" new file mode 100644 index 000000000..cc0e8b4a8 --- /dev/null +++ "b/assets/linux_env_centos7\351\230\262\347\201\253\345\242\231\345\221\275\344\273\244.md.15b22e4a.js" @@ -0,0 +1,5 @@ +import{_ as s,o as a,c as l,Q as e}from"./chunks/framework.dc1f4608.js";const b=JSON.parse('{"title":"centos7防火墙命令","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/centos7防火墙命令.md","filePath":"linux/env/centos7防火墙命令.md","lastUpdated":1708510102000}'),p={name:"linux/env/centos7防火墙命令.md"},n=e(`

centos7防火墙命令

centos7.0以上的版本默认为firewalld,以下是iptables,整理一下命令备用。

firewall-cmd

查看防火墙状态

bash
firewall-cmd --state
firewall-cmd --state

查看防火墙规则

bash
firewall-cmd --list-all
firewall-cmd --list-all

更新防火墙规则

bash
firewall-cmd --reload
firewall-cmd --reload

关闭/开启防火墙

bash
systemctl stop firewalld.service
+systemctl start firewalld.service
systemctl stop firewalld.service
+systemctl start firewalld.service

端口操作

永久开放端口

bash
firewall-cmd --zone=public --add-port=5672/tcp --permanent
firewall-cmd --zone=public --add-port=5672/tcp --permanent

关闭端口

bash
firewall-cmd --zone=public --remove-port=5672/tcp --permanent
firewall-cmd --zone=public --remove-port=5672/tcp --permanent

iptables

开启/关闭防火墙

bash
systemctl start iptables.service
+systemctl stop iptables.service
systemctl start iptables.service
+systemctl stop iptables.service

查看防火墙状态

bash
systemctl status iptables.service
systemctl status iptables.service

开放端口

1.命令

bash
iptables -I INPUT -p tcp --dport 8000 -j ACCEPT
iptables -I INPUT -p tcp --dport 8000 -j ACCEPT

2.直接修改/etc/sysconfig/iptables

-A INPUT -m state --state NEW -m tcp -p tcp --dport 8000 -j ACCEPT 

重启iptables

bash
service iptables restart
service iptables restart
`,29),o=[n];function t(c,r,i,d,h,y){return a(),l("div",null,o)}const F=s(p,[["render",t]]);export{b as __pageData,F as default}; diff --git "a/assets/linux_env_centos7\351\230\262\347\201\253\345\242\231\345\221\275\344\273\244.md.15b22e4a.lean.js" "b/assets/linux_env_centos7\351\230\262\347\201\253\345\242\231\345\221\275\344\273\244.md.15b22e4a.lean.js" new file mode 100644 index 000000000..a0388f850 --- /dev/null +++ "b/assets/linux_env_centos7\351\230\262\347\201\253\345\242\231\345\221\275\344\273\244.md.15b22e4a.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as l,Q as e}from"./chunks/framework.dc1f4608.js";const b=JSON.parse('{"title":"centos7防火墙命令","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/centos7防火墙命令.md","filePath":"linux/env/centos7防火墙命令.md","lastUpdated":1708510102000}'),p={name:"linux/env/centos7防火墙命令.md"},n=e("",29),o=[n];function t(c,r,i,d,h,y){return a(),l("div",null,o)}const F=s(p,[["render",t]]);export{b as __pageData,F as default}; diff --git "a/assets/linux_env_\344\273\216\351\233\266\346\220\255\345\273\272Linux\350\231\232\346\213\237\346\234\272\347\216\257\345\242\203.md.c536b8a7.js" "b/assets/linux_env_\344\273\216\351\233\266\346\220\255\345\273\272Linux\350\231\232\346\213\237\346\234\272\347\216\257\345\242\203.md.c536b8a7.js" new file mode 100644 index 000000000..bee6d1324 --- /dev/null +++ "b/assets/linux_env_\344\273\216\351\233\266\346\220\255\345\273\272Linux\350\231\232\346\213\237\346\234\272\347\216\257\345\242\203.md.c536b8a7.js" @@ -0,0 +1,489 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const C=JSON.parse('{"title":"从零搭建Linux虚拟机环境","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/从零搭建Linux虚拟机环境.md","filePath":"linux/env/从零搭建Linux虚拟机环境.md","lastUpdated":1708510102000}'),p={name:"linux/env/从零搭建Linux虚拟机环境.md"},o=l(`

从零搭建Linux虚拟机环境

基础

镜像安装

CentOS7-1908版本

http://vault.centos.org/7.7.1908/isos/x86_64/CentOS-7-x86_64-DVD-1908.torrent

基础命令包安装

  • ifconfig

    • yum search ifconfig
    • yum install net-tools.x86_64
  • lsb_release

    • yum install -y redhat-lsb
  • yum提示没有可用镜像

    • curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
  • wget

    • yum install wget
  • dns

    • 虚拟机ping不通域名的解决办法 vi /etc/sysconfig/network-scripts/ifcfg-ens33
    • 虚拟机的静态ip和真机的必须在同一网段,添加谷歌的dns解析
    • 虚拟机内的配置

    image-20210505171618465

    • 真机的ip信息

      image-20210505171651704

环境搭建

  • JDK1.8

    • oracle官网下载jdk后上传虚拟机

    • 解压并配置环境变量,比如我下载的是jre1.8.0_202

      vi /etc/profile/

      bash
      JAVA_HOME=/usr/local/java/jre1.8.0_202
      +PATH=$JAVA_HOME/bin:$PATH
      +CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JAVA_HOME/jre/lib/rt.jar
      +export JAVA_HOME
      +export CLASSPATH
      +export PATH
      JAVA_HOME=/usr/local/java/jre1.8.0_202
      +PATH=$JAVA_HOME/bin:$PATH
      +CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JAVA_HOME/jre/lib/rt.jar
      +export JAVA_HOME
      +export CLASSPATH
      +export PATH
    • source /etc/profile或重启虚拟机使环境变量生效

  • python3

  • mysql 5.7

    • 下载并安装mysql官方的yum repository: wget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.41-1.el7.x86_64.rpm-bundle.tar

    • 解压包:tar -xvf mysql-5.7.41-1.el7.x86_64.rpm-bundle.tar

    • yum -y install mysql-community-common-5.7.41-1.el7.x86_64.rpm mysql-community-libs-5.7.41-1.el7.x86_64.rpm mysql-community-client-5.7.41-1.el7.x86_64.rpm mysql-community-server-5.7.41-1.el7.x86_64.rpm

    • 启动systemctl enable mysqld && systemctl start mysqld

    • 临时密码 grep 'temporary password' /var/log/mysqld.log

    • 根据临时密码登入mysql

    • 改密码 ALTER USER 'root'@'localhost' IDENTIFIED BY 'new pwd';

    • 更改密码弱口令设置,设置简单密码:

      • set global validate_password_policy=0;

      • set global validate_password_length=1;

    • 配置远程登陆

      • grant all on *.* to 'root'@'%' identified by 'pwd';

      • 立即生效: flush privileges;

    • 创建用户&授权

      sql
      -- 创建用户
      +CREATE USER '用户名'@'localhost' IDENTIFIED BY '密码'; 
      +CREATE USER '用户名'@'%' IDENTIFIED BY '密码';  
      +-- 授权全部
      +grant all privileges on 数据库名称.* to '用户名'@'localhost' identified by '密码';     #本地授权
      +grant all privileges on 数据库名称.* to '用户名'@'%' identified by '密码';             #远程授权
      +flush privileges;                                                                 #刷新系统权限表
      +-- 授权指定
      +grant select,update,delete,insert on 数据库名称.* to '用户'@'localhost' identified by '密码'; 
      +flush privileges; #刷新系统权限表
      +-- 删除用户
      +Delete FROM mysql.user Where User='用户名' and Host='localhost';          #删除本地用户
      +Delete FROM mysql.user Where User='用户名' and Host='%';                  #删除远程用户
      +flush privileges;                                                         #刷新系统权限表
      +-- 删除用户及权限
      +DROP USER 'username'@'localhost';
      +DROP USER 'username'@'%';
      +-- 查看当前用户权限
      +show grants;
      +-- 查看指定用户权限
      +show grants for username@localhost;
      -- 创建用户
      +CREATE USER '用户名'@'localhost' IDENTIFIED BY '密码'; 
      +CREATE USER '用户名'@'%' IDENTIFIED BY '密码';  
      +-- 授权全部
      +grant all privileges on 数据库名称.* to '用户名'@'localhost' identified by '密码';     #本地授权
      +grant all privileges on 数据库名称.* to '用户名'@'%' identified by '密码';             #远程授权
      +flush privileges;                                                                 #刷新系统权限表
      +-- 授权指定
      +grant select,update,delete,insert on 数据库名称.* to '用户'@'localhost' identified by '密码'; 
      +flush privileges; #刷新系统权限表
      +-- 删除用户
      +Delete FROM mysql.user Where User='用户名' and Host='localhost';          #删除本地用户
      +Delete FROM mysql.user Where User='用户名' and Host='%';                  #删除远程用户
      +flush privileges;                                                         #刷新系统权限表
      +-- 删除用户及权限
      +DROP USER 'username'@'localhost';
      +DROP USER 'username'@'%';
      +-- 查看当前用户权限
      +show grants;
      +-- 查看指定用户权限
      +show grants for username@localhost;
  • mysql 8.0

    sql
    -- 修改root密码
    +UPDATE mysql.user SET authentication_string=null WHERE User='root';
    +FLUSH PRIVILEGES;
    +
    +ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'new_password';
    +FLUSH PRIVILEGES;
    +
    +
    +CREATE USER ''@'' IDENTIFIED BY '';
    +
    +GRANT ALL PRIVILEGES ON *.* TO ''@'';
    -- 修改root密码
    +UPDATE mysql.user SET authentication_string=null WHERE User='root';
    +FLUSH PRIVILEGES;
    +
    +ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'new_password';
    +FLUSH PRIVILEGES;
    +
    +
    +CREATE USER ''@'' IDENTIFIED BY '';
    +
    +GRANT ALL PRIVILEGES ON *.* TO ''@'';
  • nginx

    • 依赖

      • centos: yum -y install gcc pcre pcre-devel zlib zlib-devel openssl openssl-devel
      • ubuntu: apt install libpcre3 libpcre3-dev openssl libssl-dev
    • 下载安装包 wget http://nginx.org/download/nginx-1.9.9.tar.gz

    • 解压到指定目录 tar -xzvf nginx-1.9.9.tar.gz -C /usr/local/nginx/

    • 切换到nginx的目录执行 cd /usr/local/nginx/nginx-1.9.9

      bash
      ./configure --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module  #配置ssl模块
      + 
      +make
      + 
      +make install
      ./configure --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module  #配置ssl模块
      + 
      +make
      + 
      +make install
    • 安装后切换到/usr/local/nginx/sbin启动nginx并访问

      image-20210505174840552

    • 开机自启nginx

      • vi /etc/init.d/nginx

        bash
        #!/bin/bash
        +#
        +# nginx - this script starts and stops the nginx daemon
        +#
        +# chkconfig:   - 85 15
        +# description:  NGINX is an HTTP(S) server, HTTP(S) reverse \\
        +#               proxy and IMAP/POP3 proxy server
        +# processname: nginx
        +# config:      /etc/nginx/nginx.conf
        +# config:      /etc/sysconfig/nginx
        +# pidfile:     /var/run/nginx.pid
        +
        +# Source function library.
        +. /etc/rc.d/init.d/functions
        +
        +# Source networking configuration.
        +. /etc/sysconfig/network
        +
        +# Check that networking is up.
        +[ "$NETWORKING" = "no" ] && exit 0
        +
        +nginx="/usr/local/nginx/sbin/nginx"
        +prog=$(basename $nginx)
        +
        +NGINX_CONF_FILE="/usr/local/nginx/conf/nginx.conf"
        +
        +[ -f /etc/sysconfig/nginx ] && . /etc/sysconfig/nginx
        +
        +lockfile=/var/lock/subsys/nginx
        +
        +make_dirs() {
        +  # make required directories
        +  user=\`$nginx -V 2>&1 | grep "configure arguments:.*--user=" | sed 's/[^*]*--user=\\([^ ]*\\).*/\\1/g' -\`
        +  if [ -n "$user" ]; then
        +    if [ -z "\`grep $user /etc/passwd\`" ]; then
        +      useradd -M -s /bin/nologin $user
        +    fi
        +    options=\`$nginx -V 2>&1 | grep 'configure arguments:'\`
        +    for opt in $options; do
        +        if [ \`echo $opt | grep '.*-temp-path'\` ]; then
        +          value=\`echo $opt | cut -d "=" -f 2\`
        +          if [ ! -d "$value" ]; then
        +            # echo "creating" $value
        +            mkdir -p $value && chown -R $user $value
        +          fi
        +        fi
        +    done
        +  fi
        +}
        +
        +start() {
        +  [ -x $nginx ] || exit 5
        +  [ -f $NGINX_CONF_FILE ] || exit 6
        +  make_dirs
        +  echo -n $"Starting $prog: "
        +  daemon $nginx -c $NGINX_CONF_FILE
        +  retval=$?
        +  echo
        +  [ $retval -eq 0 ] && touch $lockfile
        +  return $retval
        +}
        +
        +stop() {
        +  echo -n $"Stopping $prog: "
        +  killproc $prog -QUIT
        +  retval=$?
        +  echo
        +  [ $retval -eq 0 ] && rm -f $lockfile
        +  return $retval
        +}
        +
        +restart() {
        +  configtest || return $?
        +  stop
        +  sleep 1
        +  start
        +}
        +
        +reload() {
        +  configtest || return $?
        +  echo -n $"Reloading $prog: "
        +  killproc $nginx -HUP
        +  RETVAL=$?
        +  echo
        +}
        +
        +force_reload() {
        +  restart
        +}
        +
        +configtest() {
        +  $nginx -t -c $NGINX_CONF_FILE
        +}
        +
        +rh_status() {
        +  status $prog
        +}
        +
        +rh_status_q() {
        +  rh_status >/dev/null 2>&1
        +}
        +
        +case "$1" in
        +  start)
        +    rh_status_q && exit 0
        +    $1
        +    ;;
        +  stop)
        +    rh_status_q || exit 0
        +    $1
        +    ;;
        +  restart|configtest)
        +    $1
        +    ;;
        +  reload)
        +    rh_status_q || exit 7
        +    $1
        +    ;;
        +  force-reload)
        +    force_reload
        +    ;;
        +  status)
        +    rh_status
        +    ;;
        +  condrestart|try-restart)
        +    rh_status_q || exit 0
        +    ;;
        +  *)
        +    echo $"Usage: $0 {start|stop|status|restart|reload|configtest}"
        +    exit 2
        +esac
        #!/bin/bash
        +#
        +# nginx - this script starts and stops the nginx daemon
        +#
        +# chkconfig:   - 85 15
        +# description:  NGINX is an HTTP(S) server, HTTP(S) reverse \\
        +#               proxy and IMAP/POP3 proxy server
        +# processname: nginx
        +# config:      /etc/nginx/nginx.conf
        +# config:      /etc/sysconfig/nginx
        +# pidfile:     /var/run/nginx.pid
        +
        +# Source function library.
        +. /etc/rc.d/init.d/functions
        +
        +# Source networking configuration.
        +. /etc/sysconfig/network
        +
        +# Check that networking is up.
        +[ "$NETWORKING" = "no" ] && exit 0
        +
        +nginx="/usr/local/nginx/sbin/nginx"
        +prog=$(basename $nginx)
        +
        +NGINX_CONF_FILE="/usr/local/nginx/conf/nginx.conf"
        +
        +[ -f /etc/sysconfig/nginx ] && . /etc/sysconfig/nginx
        +
        +lockfile=/var/lock/subsys/nginx
        +
        +make_dirs() {
        +  # make required directories
        +  user=\`$nginx -V 2>&1 | grep "configure arguments:.*--user=" | sed 's/[^*]*--user=\\([^ ]*\\).*/\\1/g' -\`
        +  if [ -n "$user" ]; then
        +    if [ -z "\`grep $user /etc/passwd\`" ]; then
        +      useradd -M -s /bin/nologin $user
        +    fi
        +    options=\`$nginx -V 2>&1 | grep 'configure arguments:'\`
        +    for opt in $options; do
        +        if [ \`echo $opt | grep '.*-temp-path'\` ]; then
        +          value=\`echo $opt | cut -d "=" -f 2\`
        +          if [ ! -d "$value" ]; then
        +            # echo "creating" $value
        +            mkdir -p $value && chown -R $user $value
        +          fi
        +        fi
        +    done
        +  fi
        +}
        +
        +start() {
        +  [ -x $nginx ] || exit 5
        +  [ -f $NGINX_CONF_FILE ] || exit 6
        +  make_dirs
        +  echo -n $"Starting $prog: "
        +  daemon $nginx -c $NGINX_CONF_FILE
        +  retval=$?
        +  echo
        +  [ $retval -eq 0 ] && touch $lockfile
        +  return $retval
        +}
        +
        +stop() {
        +  echo -n $"Stopping $prog: "
        +  killproc $prog -QUIT
        +  retval=$?
        +  echo
        +  [ $retval -eq 0 ] && rm -f $lockfile
        +  return $retval
        +}
        +
        +restart() {
        +  configtest || return $?
        +  stop
        +  sleep 1
        +  start
        +}
        +
        +reload() {
        +  configtest || return $?
        +  echo -n $"Reloading $prog: "
        +  killproc $nginx -HUP
        +  RETVAL=$?
        +  echo
        +}
        +
        +force_reload() {
        +  restart
        +}
        +
        +configtest() {
        +  $nginx -t -c $NGINX_CONF_FILE
        +}
        +
        +rh_status() {
        +  status $prog
        +}
        +
        +rh_status_q() {
        +  rh_status >/dev/null 2>&1
        +}
        +
        +case "$1" in
        +  start)
        +    rh_status_q && exit 0
        +    $1
        +    ;;
        +  stop)
        +    rh_status_q || exit 0
        +    $1
        +    ;;
        +  restart|configtest)
        +    $1
        +    ;;
        +  reload)
        +    rh_status_q || exit 7
        +    $1
        +    ;;
        +  force-reload)
        +    force_reload
        +    ;;
        +  status)
        +    rh_status
        +    ;;
        +  condrestart|try-restart)
        +    rh_status_q || exit 0
        +    ;;
        +  *)
        +    echo $"Usage: $0 {start|stop|status|restart|reload|configtest}"
        +    exit 2
        +esac
      • bash
        chmod 777 /etc/init.d/nginx
        chmod 777 /etc/init.d/nginx
      • bash
        chkconfig nginx on
        chkconfig nginx on
  • 开放端口

    • 查看已经开放的端口firewall-cmd --list-ports
    • 开启端口 firewall-cmd --zone=public --add-port=80/tcp --permanent
      • zone:作用域
      • -add-port=80/tcp 端口/协议
      • --permanent 永久生效,没有此参数后重启失效
    • firewall-cmd --reload #重启firewall
    • systemctl stop firewalld.service #停止firewall
    • systemctl disable firewalld.service #禁止firewall开机启动
    • firewall-cmd --state #查看默认防火墙状态
  • redis

    • 下载安装包wget http://download.redis.io/releases/redis-5.0.7.tar.gz

    • tar -zxvf redis-5.0.7.tar.gz

    • mv /root/redis-5.0.7 /usr/local/redis

    • cd /usr/local/redis/redis-5.0.7

    • make && make PREFIX=/usr/local/redis install

      • 可能会报错,因为centos自带的gcc版本太低

      • 执行命令

        bash
        yum -y install centos-release-scl devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils 
        +# scl enable devtoolset-9 bash #临时启用新版本的gcc
        +echo "source /opt/rh/devtoolset-9/enable" >>/etc/profile # 永久启用新版gcc
        yum -y install centos-release-scl devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils 
        +# scl enable devtoolset-9 bash #临时启用新版本的gcc
        +echo "source /opt/rh/devtoolset-9/enable" >>/etc/profile # 永久启用新版gcc
    • 开机自启redis

      • bash
        vi /etc/init.d/redis
        vi /etc/init.d/redis
      • bash
        #!/bin/sh
        +# chkconfig: 2345 10 90
        +# description: Start and Stop redis
        +
        +REDISPORT=6379
        +EXEC=/usr/local/redis/bin/redis-server
        +CLIEXEC=/usr/local/redis/bin/redis-cli
        +
        +PIDFILE=/var/run/redis_\${REDISPORT}.pid
        +CONF="/usr/local/redis/redis.conf"
        +
        +case "$1" in
        +    start)
        +        if [ -f $PIDFILE ]
        +        then
        +                echo "$PIDFILE exists, process is already running or crashed"
        +        else
        +                echo "Starting Redis server..."
        +                $EXEC $CONF &
        +        fi
        +        ;;
        +    stop)
        +        if [ ! -f $PIDFILE ]
        +        then
        +                echo "$PIDFILE does not exist, process is not running"
        +        else
        +                PID=$(cat $PIDFILE)
        +                echo "Stopping ..."
        +                $CLIEXEC -p $REDISPORT shutdown
        +                while [ -x /proc/\${PID} ]
        +                do
        +                    echo "Waiting for Redis to shutdown ..."
        +                    sleep 1
        +                done
        +                echo "Redis stopped"
        +        fi
        +        ;;
        +    restart)
        +        "$0" stop
        +        sleep 3
        +        "$0" start
        +        ;;
        +    *)
        +        echo "Please use start or stop or restart as first argument"
        +        ;;
        +esac
        #!/bin/sh
        +# chkconfig: 2345 10 90
        +# description: Start and Stop redis
        +
        +REDISPORT=6379
        +EXEC=/usr/local/redis/bin/redis-server
        +CLIEXEC=/usr/local/redis/bin/redis-cli
        +
        +PIDFILE=/var/run/redis_\${REDISPORT}.pid
        +CONF="/usr/local/redis/redis.conf"
        +
        +case "$1" in
        +    start)
        +        if [ -f $PIDFILE ]
        +        then
        +                echo "$PIDFILE exists, process is already running or crashed"
        +        else
        +                echo "Starting Redis server..."
        +                $EXEC $CONF &
        +        fi
        +        ;;
        +    stop)
        +        if [ ! -f $PIDFILE ]
        +        then
        +                echo "$PIDFILE does not exist, process is not running"
        +        else
        +                PID=$(cat $PIDFILE)
        +                echo "Stopping ..."
        +                $CLIEXEC -p $REDISPORT shutdown
        +                while [ -x /proc/\${PID} ]
        +                do
        +                    echo "Waiting for Redis to shutdown ..."
        +                    sleep 1
        +                done
        +                echo "Redis stopped"
        +        fi
        +        ;;
        +    restart)
        +        "$0" stop
        +        sleep 3
        +        "$0" start
        +        ;;
        +    *)
        +        echo "Please use start or stop or restart as first argument"
        +        ;;
        +esac
      • bash
        vim /usr/local/redis/redis.conf
        +修改
        +bind 0.0.0.0 #所有ipv4端口
        +protected-mode no # 关闭保护模式
        +daemonize yes # 守护进程
        +requirepass password #需要密码登录
        +pidfile /var/run/redis_6379.pid #pid文件
        vim /usr/local/redis/redis.conf
        +修改
        +bind 0.0.0.0 #所有ipv4端口
        +protected-mode no # 关闭保护模式
        +daemonize yes # 守护进程
        +requirepass password #需要密码登录
        +pidfile /var/run/redis_6379.pid #pid文件
      • bash
        # 授权
        +chmod 777 /etc/init.d/redis
        # 授权
        +chmod 777 /etc/init.d/redis
      • bash
        # 开机启动
        +chkconfig redis on
        # 开机启动
        +chkconfig redis on
      • bash
        # 创建客户端软链接
        +ln -s /usr/local/redis/bin/redis-cli /usr/local/bin/redis-cli
        # 创建客户端软链接
        +ln -s /usr/local/redis/bin/redis-cli /usr/local/bin/redis-cli
  • docker

    • 卸载旧版本

      bash
      sudo yum remove docker \\
      +                  docker-client \\
      +                  docker-client-latest \\
      +                  docker-common \\
      +                  docker-latest \\
      +                  docker-latest-logrotate \\
      +                  docker-logrotate \\
      +                  docker-engine
      sudo yum remove docker \\
      +                  docker-client \\
      +                  docker-client-latest \\
      +                  docker-common \\
      +                  docker-latest \\
      +                  docker-latest-logrotate \\
      +                  docker-logrotate \\
      +                  docker-engine
    • 使用docker repository安装

      bash
      # set up repository
      +sudo yum install -y yum-utils
      + 
      +sudo yum-config-manager \\
      +    --add-repo \\
      +    https://download.docker.com/linux/centos/docker-ce.repo
      +    
      +    
      +# install docker engine
      +sudo yum install docker-ce docker-ce-cli containerd.io
      +
      +# start docker engine
      +sudo systemctl start docker
      # set up repository
      +sudo yum install -y yum-utils
      + 
      +sudo yum-config-manager \\
      +    --add-repo \\
      +    https://download.docker.com/linux/centos/docker-ce.repo
      +    
      +    
      +# install docker engine
      +sudo yum install docker-ce docker-ce-cli containerd.io
      +
      +# start docker engine
      +sudo systemctl start docker
`,9),e=[o];function c(t,r,y,E,i,F){return n(),a("div",null,e)}const u=s(p,[["render",c]]);export{C as __pageData,u as default}; diff --git "a/assets/linux_env_\344\273\216\351\233\266\346\220\255\345\273\272Linux\350\231\232\346\213\237\346\234\272\347\216\257\345\242\203.md.c536b8a7.lean.js" "b/assets/linux_env_\344\273\216\351\233\266\346\220\255\345\273\272Linux\350\231\232\346\213\237\346\234\272\347\216\257\345\242\203.md.c536b8a7.lean.js" new file mode 100644 index 000000000..b9f650f83 --- /dev/null +++ "b/assets/linux_env_\344\273\216\351\233\266\346\220\255\345\273\272Linux\350\231\232\346\213\237\346\234\272\347\216\257\345\242\203.md.c536b8a7.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const C=JSON.parse('{"title":"从零搭建Linux虚拟机环境","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/从零搭建Linux虚拟机环境.md","filePath":"linux/env/从零搭建Linux虚拟机环境.md","lastUpdated":1708510102000}'),p={name:"linux/env/从零搭建Linux虚拟机环境.md"},o=l("",9),e=[o];function c(t,r,y,E,i,F){return n(),a("div",null,e)}const u=s(p,[["render",c]]);export{C as __pageData,u as default}; diff --git "a/assets/linux_env_\346\234\215\345\212\241\345\231\250\345\220\257\347\224\250ssh\345\257\206\351\222\245\347\231\273\345\275\225\345\271\266\347\246\201\347\224\250\345\257\206\347\240\201\347\231\273\345\275\225.md.8b926ddc.js" "b/assets/linux_env_\346\234\215\345\212\241\345\231\250\345\220\257\347\224\250ssh\345\257\206\351\222\245\347\231\273\345\275\225\345\271\266\347\246\201\347\224\250\345\257\206\347\240\201\347\231\273\345\275\225.md.8b926ddc.js" new file mode 100644 index 000000000..fe8ef255d --- /dev/null +++ "b/assets/linux_env_\346\234\215\345\212\241\345\231\250\345\220\257\347\224\250ssh\345\257\206\351\222\245\347\231\273\345\275\225\345\271\266\347\246\201\347\224\250\345\257\206\347\240\201\347\231\273\345\275\225.md.8b926ddc.js" @@ -0,0 +1,33 @@ +import{_ as s,o as a,c as n,Q as e}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"服务器启用ssh密钥登录并禁用密码登录","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/服务器启用ssh密钥登录并禁用密码登录.md","filePath":"linux/env/服务器启用ssh密钥登录并禁用密码登录.md","lastUpdated":1708510102000}'),o={name:"linux/env/服务器启用ssh密钥登录并禁用密码登录.md"},l=e(`

服务器启用ssh密钥登录并禁用密码登录

There were 89 failed login attempts since the last successful login.

最近登录阿里云服务器,总是发现有人恶意尝试登录,虽然密码强度很高,但是看着就闹心,索性把密码登录给ban掉改用密钥登录。

生成密钥对

bash
cd ~/.ssh
+ssh-keygen -t rsa -C "邮箱地址"
cd ~/.ssh
+ssh-keygen -t rsa -C "邮箱地址"

此时会在/root/.ssh下生成id_rsa和id_rsa.pub的私钥和公钥

服务器安装公钥

使用ssh-copy-id命令将公钥拷贝到服务器上

把本地的ssh公钥文件安装到远程主机对应的账户下,ssh-copy-id命令 可以把本地主机的公钥复制到远程主机的authorized_keys文件上,ssh-copy-id命令也会给远程主机的用户主目录(home)和~/.ssh, 和~/.ssh/authorized_keys设置合适的权限。 ssh-copy-id 用来将本地公钥复制到远程主机。如果不传入 -i 参数,ssh-copy-id 使用默认 ~/.ssh/identity.pub 作为默认公钥。如果多次运行 ssh-copy-id ,该命令不会检查重复,会在远程主机中多次写入 authorized_keys 。

bash
ssh-copy-id [-i identify_file] [user@]host
ssh-copy-id [-i identify_file] [user@]host

修改ssh登录设置

bash
vim /etc/ssh/sshd_config
+
+RSAAuthentication yes
+PubkeyAuthentication yes
+AuthorizedKeysFile .ssh/authorized_keys
+PasswordAuthentication yes #密码登录 此时不要关闭
vim /etc/ssh/sshd_config
+
+RSAAuthentication yes
+PubkeyAuthentication yes
+AuthorizedKeysFile .ssh/authorized_keys
+PasswordAuthentication yes #密码登录 此时不要关闭

修改并保存,重启sshd服务systemctl restart sshd

设置私钥权限

id_rsa文件权限需要调整,否则使用密钥登录会因为私钥文件权限问题被拒绝。

一般来说: .ssh目录设置700权限 id_rsa,authorized_keys文件设置600权限 id_rsa.pub,known_hosts文件设置644权限

修改登录命令

登录使用ssh -i "私钥文件全路径" root@xxx.xxx.xxx.xxx

尝试登录

image-20211218211917959

登录成功

关闭云服务器

密钥登录成功后即可关闭服务器的密码登录

bash
vim /etc/ssh/sshd_config
+
+PasswordAuthentication no #关闭密码登录
vim /etc/ssh/sshd_config
+
+PasswordAuthentication no #关闭密码登录

保存后重启sshd服务systemctl restart sshd

尝试密码登录

image-20211218212154923

可以看到密码登录已经被关闭

手动配置ssh密钥

shell
adduser username
+
+mkdir /home/username/.ssh
+# 编辑authorized_keys文件,将生成的公钥添加进去
+vim /home/username/.ssh/authorized_keys
+
+chown -R username:username /home/username.ssh
+chmod 700 /home/username/.ssh
+chmod 600 /home/username/.ssh/authorized_keys
adduser username
+
+mkdir /home/username/.ssh
+# 编辑authorized_keys文件,将生成的公钥添加进去
+vim /home/username/.ssh/authorized_keys
+
+chown -R username:username /home/username.ssh
+chmod 700 /home/username/.ssh
+chmod 600 /home/username/.ssh/authorized_keys
`,30),p=[l];function t(c,r,i,h,y,d){return a(),n("div",null,p)}const F=s(o,[["render",t]]);export{u as __pageData,F as default}; diff --git "a/assets/linux_env_\346\234\215\345\212\241\345\231\250\345\220\257\347\224\250ssh\345\257\206\351\222\245\347\231\273\345\275\225\345\271\266\347\246\201\347\224\250\345\257\206\347\240\201\347\231\273\345\275\225.md.8b926ddc.lean.js" "b/assets/linux_env_\346\234\215\345\212\241\345\231\250\345\220\257\347\224\250ssh\345\257\206\351\222\245\347\231\273\345\275\225\345\271\266\347\246\201\347\224\250\345\257\206\347\240\201\347\231\273\345\275\225.md.8b926ddc.lean.js" new file mode 100644 index 000000000..13bb90fb6 --- /dev/null +++ "b/assets/linux_env_\346\234\215\345\212\241\345\231\250\345\220\257\347\224\250ssh\345\257\206\351\222\245\347\231\273\345\275\225\345\271\266\347\246\201\347\224\250\345\257\206\347\240\201\347\231\273\345\275\225.md.8b926ddc.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as e}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"服务器启用ssh密钥登录并禁用密码登录","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/服务器启用ssh密钥登录并禁用密码登录.md","filePath":"linux/env/服务器启用ssh密钥登录并禁用密码登录.md","lastUpdated":1708510102000}'),o={name:"linux/env/服务器启用ssh密钥登录并禁用密码登录.md"},l=e("",30),p=[l];function t(c,r,i,h,y,d){return a(),n("div",null,p)}const F=s(o,[["render",t]]);export{u as __pageData,F as default}; diff --git "a/assets/linux_env_\351\223\276\346\216\245\345\222\214\345\210\253\345\220\215(ln\343\200\201alias).md.4f5e25d9.js" "b/assets/linux_env_\351\223\276\346\216\245\345\222\214\345\210\253\345\220\215(ln\343\200\201alias).md.4f5e25d9.js" new file mode 100644 index 000000000..478e61793 --- /dev/null +++ "b/assets/linux_env_\351\223\276\346\216\245\345\222\214\345\210\253\345\220\215(ln\343\200\201alias).md.4f5e25d9.js" @@ -0,0 +1 @@ +import{_ as a,o as l,c as e,Q as i}from"./chunks/framework.dc1f4608.js";const p=JSON.parse('{"title":"链接和别名(ln、alias)","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/链接和别名(ln、alias).md","filePath":"linux/env/链接和别名(ln、alias).md","lastUpdated":1708510102000}'),n={name:"linux/env/链接和别名(ln、alias).md"},t=i('

链接和别名(ln、alias)

链接

ln: link,链接,类似windows中的快捷方式的概念,主要针对路径比较长的文件夹,建立一个链接,让访问更加方便,分为软链接(-s)和硬链接

使用

  • ln /usr/local/maven/bin/mvn(真实的路径) /usr/bin/mvn(链接路径) ## 硬链接
  • ln -s /usr/local/maven/bin/mvn(真实的路径) /usr/bin/mvn(链接路径) ##软链接-符号链接

别名

alias:别名,主要是针对某个命令,如果命令的路径比较长、比较复杂,那么起个别名会更方便

使用

  • 查看别名:alias
  • 定义别名:alias la=‘ll -a'
  • 取消别名:unalias la
',9),s=[t];function r(o,_,c,d,h,u){return l(),e("div",null,s)}const b=a(n,[["render",r]]);export{p as __pageData,b as default}; diff --git "a/assets/linux_env_\351\223\276\346\216\245\345\222\214\345\210\253\345\220\215(ln\343\200\201alias).md.4f5e25d9.lean.js" "b/assets/linux_env_\351\223\276\346\216\245\345\222\214\345\210\253\345\220\215(ln\343\200\201alias).md.4f5e25d9.lean.js" new file mode 100644 index 000000000..bb8094d83 --- /dev/null +++ "b/assets/linux_env_\351\223\276\346\216\245\345\222\214\345\210\253\345\220\215(ln\343\200\201alias).md.4f5e25d9.lean.js" @@ -0,0 +1 @@ +import{_ as a,o as l,c as e,Q as i}from"./chunks/framework.dc1f4608.js";const p=JSON.parse('{"title":"链接和别名(ln、alias)","description":"","frontmatter":{},"headers":[],"relativePath":"linux/env/链接和别名(ln、alias).md","filePath":"linux/env/链接和别名(ln、alias).md","lastUpdated":1708510102000}'),n={name:"linux/env/链接和别名(ln、alias).md"},t=i("",9),s=[t];function r(o,_,c,d,h,u){return l(),e("div",null,s)}const b=a(n,[["render",r]]);export{p as __pageData,b as default}; diff --git "a/assets/linux_hardware_linux\347\243\201\347\233\230\346\223\215\344\275\234\347\233\270\345\205\263.md.0fc149c9.js" "b/assets/linux_hardware_linux\347\243\201\347\233\230\346\223\215\344\275\234\347\233\270\345\205\263.md.0fc149c9.js" new file mode 100644 index 000000000..40ed5ad20 --- /dev/null +++ "b/assets/linux_hardware_linux\347\243\201\347\233\230\346\223\215\344\275\234\347\233\270\345\205\263.md.0fc149c9.js" @@ -0,0 +1,65 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const B=JSON.parse('{"title":"linux磁盘操作相关","description":"","frontmatter":{},"headers":[],"relativePath":"linux/hardware/linux磁盘操作相关.md","filePath":"linux/hardware/linux磁盘操作相关.md","lastUpdated":1708510102000}'),p={name:"linux/hardware/linux磁盘操作相关.md"},o=l(`

linux磁盘操作相关

查看磁盘、分区情况

fdisk -l:查看硬盘个数、分区情况

分区相关

fdisk /dev/sdx:磁盘分区,只能分小容量(2T以内),mbr分区表

parted /dev/sdx:磁盘分区,gpt分区表

多个分区需要注意4k对齐,4k对齐内容介绍见下文引用的disk genius的文章

磁盘格式化

根据自己需求的文件格式选择,例如常见的ext4文件系统

mkfs.ext4 [-b block-size] [-C cluster-size] [-i bytes-per-inode] [-I inode-size] [-N number-of-inodes] [-t fs-type] [-T usage-type ] device

可以指定inode的个数和占用的空间和多少字节一个inode,需要注意inode如果过小,当inode使用完后,即使磁盘有空间也无法再写入数据。/etc/mke2fs.conf文件中定义了一些默认的类型配置,usage-type根据文件系统的主要用途对应的类型,例如largefile类型为1M一个inode,还有largefile4为4M一个inode,这样可以减少inode的占用空间和个数,用于存储大文件,但是实际上inode占用的空间对于大容量硬盘来说很少。

调整磁盘预留空间

ext2/3/4文件系统会预留5%空间用于紧急情况,保障在硬盘快满的时候不至于crash,但是一个16T的硬盘的5%将近800G,实在是浪费。可以通过tune2fs命令来减少预留空间

tune2fs -m 1 /dev/sdx:将设备的预留空间调整为1%,也可以改成0,但不建议。

磁盘挂载

手动挂载(重启失效):mount /dev/sdx /path

自动挂载(开机自动挂载):

  • 命令blkid /dev/sdx记录需要挂载设备的uuid
  • vim /etc/fstab添加一条记录,例如
shell
UUID=506ed1d0-bc35-4585-8496-1ff4de100982 /repo ext4 defaults 0 0
+# 第一项为设备uuid 第二项为挂载点即挂载目录 第三项为文件系统类型 第四项为挂载的状态例如ro(只读)或defaults(包含了读写rw,exec,async等) 
+# 第五项为DUMP选项 默认0 第六项为被fsck命令决定启动时被扫描的文件系统顺序 仓库盘可以填0不需要扫描,系统盘1,其他可2
UUID=506ed1d0-bc35-4585-8496-1ff4de100982 /repo ext4 defaults 0 0
+# 第一项为设备uuid 第二项为挂载点即挂载目录 第三项为文件系统类型 第四项为挂载的状态例如ro(只读)或defaults(包含了读写rw,exec,async等) 
+# 第五项为DUMP选项 默认0 第六项为被fsck命令决定启动时被扫描的文件系统顺序 仓库盘可以填0不需要扫描,系统盘1,其他可2

磁盘扩容

shell
# 使用\`parted /dev/sda\`
+GNU Parted 3.4
+Using /dev/sda
+Welcome to GNU Parted! Type 'help' to view a list of commands.
+
+# \`p\`输出信息
+# 扩容前:
+(parted) p
+Model: ATA QEMU HARDDISK (scsi)
+Disk /dev/sda: 859GB
+Sector size (logical/physical): 512B/512B
+Partition Table: gpt
+Disk Flags:
+
+Number  Start   End     Size    File system  Name  Flags
+ 1      1049kB  2097kB  1049kB                     bios_grub
+ 2      2097kB  537GB   537GB   ext4
+
+# 使用\`resizepart 2 -1\`
+
+# 扩容后
+(parted) p
+Model: ATA QEMU HARDDISK (scsi)
+Disk /dev/sda: 859GB
+Sector size (logical/physical): 512B/512B
+Partition Table: gpt
+Disk Flags:
+
+Number  Start   End     Size    File system  Name  Flags
+ 1      1049kB  2097kB  1049kB                     bios_grub
+ 2      2097kB  859GB   859GB   ext4
# 使用\`parted /dev/sda\`
+GNU Parted 3.4
+Using /dev/sda
+Welcome to GNU Parted! Type 'help' to view a list of commands.
+
+# \`p\`输出信息
+# 扩容前:
+(parted) p
+Model: ATA QEMU HARDDISK (scsi)
+Disk /dev/sda: 859GB
+Sector size (logical/physical): 512B/512B
+Partition Table: gpt
+Disk Flags:
+
+Number  Start   End     Size    File system  Name  Flags
+ 1      1049kB  2097kB  1049kB                     bios_grub
+ 2      2097kB  537GB   537GB   ext4
+
+# 使用\`resizepart 2 -1\`
+
+# 扩容后
+(parted) p
+Model: ATA QEMU HARDDISK (scsi)
+Disk /dev/sda: 859GB
+Sector size (logical/physical): 512B/512B
+Partition Table: gpt
+Disk Flags:
+
+Number  Start   End     Size    File system  Name  Flags
+ 1      1049kB  2097kB  1049kB                     bios_grub
+ 2      2097kB  859GB   859GB   ext4
  • resizepart partition end:end参数填分区结束扇区号,直接使用resizepart 2 -1可以自动扩展到可用空间的末尾
  • 或者使用fdisk扩容,先删除再新增

硬盘分区4k对齐

内容引用自disk genius文章:分区4K对齐那些事,你想知道的都在这里

物理扇区的概念

分区对齐,是指将分区起始位置对齐到一定的扇区。我们要先了解对齐和扇区的关系。我们知道,硬盘的基本读写单位是“扇区”。对于硬盘的读写操作,每次读写都是以扇区为单位进行的,最少一个扇区,通常是512个字节。由于硬盘数据存储结构的限制,单独读写1个或几个字节是不可能的。通过系统提供的接口读写文件数据时,看起来可以单独读写少量字节,实际上是经过了操作系统的转换才实现的。硬盘实际执行时读写的仍然是整个扇区。

近年来,随着对硬盘容量的要求不断增加,为了提高数据记录密度,硬盘厂商往往采用增大扇区大小的方法,于是出现了扇区大小为4096字节的硬盘。我们将这样的扇区称之为“物理扇区”。但是这样的大扇区会有兼容性问题,有的系统或软件无法适应。为了解决这个问题,硬盘内部将物理扇区在逻辑上划分为多个扇区片段并将其作为普通的扇区(一般为512字节大小)报告给操作系统及应用软件。这样的扇区片段我们称之为“逻辑扇区”。实际读写时由硬盘内的程序(固件)负责在逻辑扇区与物理扇区之间进行转换,上层程序“感觉”不到物理扇区的存在。

逻辑扇区是硬盘可以接受读写指令的最小操作单元,是操作系统及应用程序可以访问的扇区,多数情况下其大小为512字节。我们通常所说的扇区一般就是指的逻辑扇区。物理扇区是硬盘底层硬件意义上的扇区,是实际执行读写操作的最小单元。是只能由硬盘直接访问的扇区,操作系统及应用程序一般无法直接访问物理扇区。一个物理扇区可以包含一个或多个逻辑扇区(比如多数硬盘的物理扇区包含了8个逻辑扇区)。当要读写某个逻辑扇区时,硬盘底层在实际操作时都会读写逻辑扇区所在的整个物理扇区。

这里说的“硬盘”及其“扇区”的概念,同样适用于存储卡、固态硬盘(SSD)。接下来我们统称其为“磁盘”。它们在使用上的基本原理是一致的。其中固态硬盘在实现上更加复杂,它有“页”和“块”的概念,为了便于理解,我们可以简单的将其视同为逻辑扇区和物理扇区。另外固态硬盘在写入数据之前必须先执行擦除操作,不能直接写入到已存有数据的块,必须先擦除再写入。所以固态硬盘(SSD)对分区4K对齐的要求更高。如果没有对齐,额外的动作会增加更多,造成读写性能下降。

分区及其格式化

磁盘在使用之前必须要先分区并格式化。简单的理解,分区就是指从磁盘上划分出来的一大片连续的扇区。格式化则是对分区范围内扇区的使用进行规划。比如文件数据的储存如何安排、文件属性储存在哪里、目录结构如何存储等等。分区经过格式化后,就可以存储文件了。格式化程序会将分区里面的所有扇区从头至尾进行分组,划分为固定大小的“簇”,并按顺序进行编号。每个“簇”可固定包含一个或多个扇区,其扇区个数总是2的n次方。格式化以后,分区就会以“簇”为最小单位进行读写。文件的数据、属性等等信息都要保存到“簇”里面。

为什么要分区对齐

为磁盘划分分区时,是以逻辑扇区为单位进行划分的,分区可以从任意编号的逻辑扇区开始。如果分区的起始位置没有对齐到某个物理扇区的边缘,格式化后,所有的“簇”也将无法对齐到物理扇区的边缘。如下图所示,每个物理扇区由4个逻辑扇区组成。分区是从3号扇区开始的。格式化后,每个簇占用了4个扇区,这些簇都没有对齐到物理扇区的边缘,也就是说,每个簇都跨越了2个物理扇区。

为什么要分区对齐

由于磁盘总是以物理扇区为单位进行读写,在这样的分区情况下,当要读取某个簇时,实际上总是需要多读取一个物理扇区的数据。比如要读取0号簇共4个逻辑扇区的数据,磁盘实际执行时,必须要读取0号和1号两个物理扇区共8个逻辑扇区的数据。同理,对“簇”的写入操作也是这样。显而易见,这样会造成读写性能的严重下降。

下面再看对齐的情况。如下图所示,分区从4号扇区开始,刚好对齐到了物理扇区1的边缘,格式化后,每个簇同样占用了4个扇区,而且这些簇都对齐到了物理扇区的边缘。

为什么要分区对齐

在这样对齐的情况下,当要读取某个簇,磁盘实际执行时并不需要额外读取任何扇区,可以充分发挥磁盘的读写性能。显然这正是我们需要的。

由此可见,对于物理扇区大小与逻辑扇区大小不一致的磁盘,分区4K对齐才能充分发挥磁盘的读写性能。而不对齐就会造成磁盘读写性能的下降。

如何才能对齐

通过前述图示的两个例子可以看到,只要将分区的起始位置对齐到物理扇区的边缘,格式化程序就会将每个簇也对齐到物理扇区的边缘,这样就实现了分区的对齐。其实对齐很简单。

如何检测物理扇区大小

划分分区时,要想实现4K对齐,必须首先知道磁盘物理扇区的大小。那么如何查询呢?

打开DiskGenius软件,点击要检测的磁盘,在软件界面右侧的磁盘参数表中,可以找到“扇区大小”和“物理扇区大小”。其中“扇区大小”指的是逻辑扇区的大小。如图所示,这个磁盘的物理扇区大小为4096字节,通过计算得知,它包含了8个逻辑扇区。

DiskGenius查看结果

对齐到多少个扇区才正确

知道了“扇区大小”和“物理扇区大小”,用“物理扇区大小”除以“扇区大小”就能得到每个物理扇区所包含的逻辑扇区个数。这个数值就是我们要对齐的扇区个数的最小值。只要将分区起始位置对齐到这个数值的整数倍就可以了。举个例子,比如物理扇区大小是4096字节,逻辑扇区大小是512字节,那么4096除以512,等于8。我们只要将分区起始位置对齐到8的整数倍扇区就能满足分区对齐的要求。比如对齐到8、16、24、32、... 1024、2048等等。只要这个起始扇区号能够被8整除就都可以。并不是这个除数数值越大越好。Windows系统默认对齐的扇区数是2048。这个数值基本上能满足几乎所有磁盘的4K对齐要求了。

为什么大家都说4K对齐

习惯而已。因为开始出现物理扇区的概念时,多数磁盘的物理扇区大小都是4096即4K字节,习惯了就俗称4K对齐了。实际划分分区时还是要检测一下物理扇区大小,因为有些磁盘的物理扇区可能包含4个、8个、16个或者更多个逻辑扇区(总是2的n次方)。知道物理扇区大小后,再按照刚才说的计算方法,以物理扇区包含的逻辑扇区个数为基准,对齐到实际的物理扇区大小才是正确的。如果物理扇区大小是8192字节,那就要按照8192字节来对齐,严格来讲,这就不能叫4K对齐了。

划分分区时如何具体操作分区对齐

以DiskGenius软件为例,建立新分区时,在“建立新分区”对话框中勾选“对齐到下列扇区数的整数倍”,然后选择需要对齐的扇区数目,点“确定”后建立的分区就是对齐的了。

DiskGenius复制文件

软件在“扇区数目”下拉框中列出了很多的选项,从中选择任意一个大于物理扇区大小的扇区数都是可以的,都能满足对齐要求。软件列出那么多的扇区数选项只是增加了选择的自由度,并不是数值越大越好。使用过大的数值可能会造成磁盘空间的浪费。软件默认的设置已经能够满足几乎所有磁盘的 4K对齐要求。

除了“建立新分区”对话框,DiskGenius软件还有一个“快速分区”功能,其中也有相同的对齐设置。如下图所示:

注册DiskGenius

如何检测是否对齐

作为一款强大的分区管理软件,DiskGenius同样提供了分区4K对齐检测的功能。你可以用它检测一下自己硬盘的分区是否对齐了。使用方法很简单,打开软件后,首先在软件左侧选中要检测的磁盘,然后选择“工具”菜单中的“分区4KB扇区对齐检测”,软件立即显示检测结果,如下图所示:

注册DiskGenius

最右侧“对齐”一栏是“Y”的分区就是对齐的分区,否则就是没有对齐。没有对齐的分区会用红色字体显示。

`,24),e=[o];function t(c,r,y,E,i,F){return a(),n("div",null,e)}const h=s(p,[["render",t]]);export{B as __pageData,h as default}; diff --git "a/assets/linux_hardware_linux\347\243\201\347\233\230\346\223\215\344\275\234\347\233\270\345\205\263.md.0fc149c9.lean.js" "b/assets/linux_hardware_linux\347\243\201\347\233\230\346\223\215\344\275\234\347\233\270\345\205\263.md.0fc149c9.lean.js" new file mode 100644 index 000000000..e90005683 --- /dev/null +++ "b/assets/linux_hardware_linux\347\243\201\347\233\230\346\223\215\344\275\234\347\233\270\345\205\263.md.0fc149c9.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const B=JSON.parse('{"title":"linux磁盘操作相关","description":"","frontmatter":{},"headers":[],"relativePath":"linux/hardware/linux磁盘操作相关.md","filePath":"linux/hardware/linux磁盘操作相关.md","lastUpdated":1708510102000}'),p={name:"linux/hardware/linux磁盘操作相关.md"},o=l("",24),e=[o];function t(c,r,y,E,i,F){return a(),n("div",null,e)}const h=s(p,[["render",t]]);export{B as __pageData,h as default}; diff --git a/assets/linux_index.md.2f33c5bb.js b/assets/linux_index.md.2f33c5bb.js new file mode 100644 index 000000000..8c7f956f0 --- /dev/null +++ b/assets/linux_index.md.2f33c5bb.js @@ -0,0 +1 @@ +import{_ as t,o as a,c as n,k as e,a as o}from"./chunks/framework.dc1f4608.js";const f=JSON.parse('{"title":"Linux","description":"","frontmatter":{},"headers":[],"relativePath":"linux/index.md","filePath":"linux/index.md","lastUpdated":1708510102000}'),i={name:"linux/index.md"},s=e("h1",{id:"linux",tabindex:"-1"},[o("Linux "),e("a",{class:"header-anchor",href:"#linux","aria-label":'Permalink to "Linux"'},"​")],-1),c=e("blockquote",null,[e("p",null,"Talk is cheap,show me the code。")],-1),d=[s,c];function l(r,x,_,u,h,p){return a(),n("div",null,d)}const k=t(i,[["render",l]]);export{f as __pageData,k as default}; diff --git a/assets/linux_index.md.2f33c5bb.lean.js b/assets/linux_index.md.2f33c5bb.lean.js new file mode 100644 index 000000000..8c7f956f0 --- /dev/null +++ b/assets/linux_index.md.2f33c5bb.lean.js @@ -0,0 +1 @@ +import{_ as t,o as a,c as n,k as e,a as o}from"./chunks/framework.dc1f4608.js";const f=JSON.parse('{"title":"Linux","description":"","frontmatter":{},"headers":[],"relativePath":"linux/index.md","filePath":"linux/index.md","lastUpdated":1708510102000}'),i={name:"linux/index.md"},s=e("h1",{id:"linux",tabindex:"-1"},[o("Linux "),e("a",{class:"header-anchor",href:"#linux","aria-label":'Permalink to "Linux"'},"​")],-1),c=e("blockquote",null,[e("p",null,"Talk is cheap,show me the code。")],-1),d=[s,c];function l(r,x,_,u,h,p){return a(),n("div",null,d)}const k=t(i,[["render",l]]);export{f as __pageData,k as default}; diff --git a/assets/python_base_Poetry.md.86dc5eff.js b/assets/python_base_Poetry.md.86dc5eff.js new file mode 100644 index 000000000..fe496e376 --- /dev/null +++ b/assets/python_base_Poetry.md.86dc5eff.js @@ -0,0 +1,17 @@ +import{_ as o,o as s,c as e,Q as l}from"./chunks/framework.dc1f4608.js";const E=JSON.parse('{"title":"Poetry","description":"","frontmatter":{},"headers":[],"relativePath":"python/base/Poetry.md","filePath":"python/base/Poetry.md","lastUpdated":1708510102000}'),p={name:"python/base/Poetry.md"},a=l(`

Poetry

Poetry: PYTHON PACKAGING AND DEPENDENCY MANAGEMENT MADE EASY

https://python-poetry.org/

简介

Poetry是一个Python包管理工具,类似于pip,但是远比pip强大,除了依赖管理外,还能自动解析依赖的关系,还可以管理项目的虚拟环境,打包、发布。

pip的缺陷:移除依赖时不能自动解析依赖的关系

如果执行pip install flask,pip会自动安装flask所依赖的包,但是如果执行pip uninstall flask,pip只会移除flask包,而不会移除flask依赖的包,这样就会导致项目中存在无用的包,只能我们手动移除。但是手动移除包是很危险的,因为一不小心可能会把其他包依赖的包删除了导致项目不能正常运行。

Poetry使用了PEP 518提出的pyproject.toml配置文件,在依赖管理方面替代了传统的requirements.txt,在构建方面替换了传统的setup.py,更加清晰、灵活。

安装和基本使用

安装

https://python-poetry.org/docs/#installing-with-the-official-installer

Poetry在各个操作系统的默认安装路径:

  • ~/Library/Application Support/pypoetry on MacOS.
  • ~/.local/share/pypoetry on Linux/Unix.
  • %APPDATA%\\pypoetry on Windows.

在安装时指定POETRY_HOME环境变量:

sh
curl -sSL https://install.python-poetry.org | POETRY_HOME=/etc/poetry python3 -
curl -sSL https://install.python-poetry.org | POETRY_HOME=/etc/poetry python3 -

$POETRY_HOME/bin添加到PATH

查看版本:poetry --version

更新版本:poetry self update

命令补全:

sh
mkdir $ZSH_CUSTOM/plugins/poetry
+poetry completions zsh > $ZSH_CUSTOM/plugins/poetry/_poetr
mkdir $ZSH_CUSTOM/plugins/poetry
+poetry completions zsh > $ZSH_CUSTOM/plugins/poetry/_poetr
shell
# .zshrc
+plugins=(
+  git
+  zsh-autosuggestions
+  zsh-syntax-highlighting
+  poetry
+  ...
+)
# .zshrc
+plugins=(
+  git
+  zsh-autosuggestions
+  zsh-syntax-highlighting
+  poetry
+  ...
+)

初始化

  • 初始化项目:poetry init

使用

  • 配置虚拟环境生成到项目路径下:poetry config virtualenvs.in-project true

默认的情况poetry会将虚拟环境生成到特定目录(根据操作系统有不同),命名规则为项目名-random-python版本,这样并不方便管理,所以改为在项目目录下生成虚拟环境,更符合使用习惯,修改后生成的虚拟环境在项目路径下的.venv

  • 创建虚拟环境:poetry env use python

取决于python在PATH中link的版本,也可以改为poetry env use python3.11

  • 启动虚拟环境:poetry shell
  • 退出虚拟环境:exit
  • 新增依赖:poetry add

执行poetry add后会自动将add的包信息和版本添加到pyproject.toml中(不会记录该包依赖的其他包),这样就可以区分出主动安装的是什么包,和基于依赖关系安装的是什么包。

除了pyproject.toml,项目中还会生成一个poetry.lock文件(类似npm的lock文件或原来的requirements.txt),记录安装的所有依赖和对应版本

  • 指定版本新增依赖:

    • ^:表示匹配指定版本的最新次版本(minor version)和补丁版本(patch version),但不改变主版本(major version)。
      • 示例:poetry add Django@^2.1.0 表示匹配2.x.x中最新的版本不包括3.0.0。
    • ~:表示匹配指定版本的最新补丁版本,不改变主版本和次版本。
      • 示例:poetry add Django@~1.2.3表示匹配1.2.x中最新的版本,但不包括1.3.0。
    • >=:表示匹配指定版本或更高版本,不限制最后一位的变化。
      • 示例:poetry add Django>=3.2.0表示匹配Django3.2版本及其更高版本。
    • ==:表示严格匹配指定版本。
      • 示例:poetry add numpy==1.21.3表示只匹配精确版本号为1.21.3。
  • 新增依赖到dev-dependencies:poetry add xxx -Dpoetry add xxx --dev

  • 手动更新依赖版本:

    • 更新pyproject.toml中的依赖版本
    • 更新lock文件中的版本:poetry lock
    • 重新安装依赖到虚拟环境:poetry install
  • poetry install

    • 安装依赖项: 执行该命令将会读取pyproject.toml文件中的[tool.poetry.dependencies]部分,并根据其中的规范(例如包的名称和版本要求)来安装依赖项。
    • 生成锁文件: 如果项目中没有poetry.lock文件,poetry install会生成poetry.lock文件。这个锁文件包含了确切的依赖项版本,确保在不同的环境中使用相同的软件包版本。
    • 加速依赖项安装: 如果存在poetry.lock文件,poetry install将首先检查锁文件并使用其中的版本信息,而不是重新计算依赖关系。这有助于提高依赖项安装的速度。
    • 创建虚拟环境: 如果项目中没有虚拟环境poetry install会自动创建一个虚拟环境,并将依赖项安装到该虚拟环境中。如果已经存在虚拟环境,将会使用现有的虚拟环境。
  • 更新依赖:poetry update

  • 列出依赖:poetry show

    • 树状依赖:poetry show --tree
  • 移除依赖:poetry remove

  • 输出requirements.txt:poetry export

    • poetry export -f requirements.txt -o requirements.txt --without-hashes
  • 打包:poetry build

    • 只打包wheel:poetry build -f wheel
  • 发布:poetry publish

    需要配置仓库

`,29),n=[a];function t(c,r,i,d,y,h){return s(),e("div",null,n)}const g=o(p,[["render",t]]);export{E as __pageData,g as default}; diff --git a/assets/python_base_Poetry.md.86dc5eff.lean.js b/assets/python_base_Poetry.md.86dc5eff.lean.js new file mode 100644 index 000000000..080822d63 --- /dev/null +++ b/assets/python_base_Poetry.md.86dc5eff.lean.js @@ -0,0 +1 @@ +import{_ as o,o as s,c as e,Q as l}from"./chunks/framework.dc1f4608.js";const E=JSON.parse('{"title":"Poetry","description":"","frontmatter":{},"headers":[],"relativePath":"python/base/Poetry.md","filePath":"python/base/Poetry.md","lastUpdated":1708510102000}'),p={name:"python/base/Poetry.md"},a=l("",29),n=[a];function t(c,r,i,d,y,h){return s(),e("div",null,n)}const g=o(p,[["render",t]]);export{E as __pageData,g as default}; diff --git "a/assets/python_base_python\345\237\272\347\241\200\350\257\255\346\263\225.md.2ec54814.js" "b/assets/python_base_python\345\237\272\347\241\200\350\257\255\346\263\225.md.2ec54814.js" new file mode 100644 index 000000000..a50e1f1d9 --- /dev/null +++ "b/assets/python_base_python\345\237\272\347\241\200\350\257\255\346\263\225.md.2ec54814.js" @@ -0,0 +1,751 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const F=JSON.parse('{"title":"python基础语法","description":"","frontmatter":{},"headers":[],"relativePath":"python/base/python基础语法.md","filePath":"python/base/python基础语法.md","lastUpdated":1708510102000}'),p={name:"python/base/python基础语法.md"},o=l(`

python基础语法

第一个Python程序

python
print('hello world')
print('hello world')

基础语法

标识符

  • 第一个字符必须是字母或下划线_
  • 标识符由字母、数字、下划线组成
  • 标识符对大小写敏感

python3中支持中文变量名

python关键字

python
import keyword
+
+
+print(keyword.kwlist)
+
+
+['False', 'None', 'True', '__peg_parser__', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']
import keyword
+
+
+print(keyword.kwlist)
+
+
+['False', 'None', 'True', '__peg_parser__', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']

注释

python中单行注释#

多行注释可以多个#或者三单引号'''和三双引号"""

行与缩进

python使用缩进来表示代码块,而不是像Java一样的{},目前还很不习惯。。

python
if True:
+    print('true')
+else:
+    print('false')
if True:
+    print('true')
+else:
+    print('false')

多行语句

python一般一行写一条一句,特殊情况可以使用反斜杠\\实现多行语句

python
total = item_one + \\
+        item_two + \\
+        item_three
total = item_one + \\
+        item_two + \\
+        item_three

[],{},()中的多行语句不需要\\

数字类型

python中的数字有四种类型:

  • int整型
  • bool布尔
  • float浮点
  • complex复数 例如 1+2j

字符串

  • python中的单引号和双引号完全相同
  • 使用三引号可以指定多行字符串
  • 转义符\\
  • 反斜杠可用作转移,使用r可以让反斜杠不发生转义。例如 r‘this is a line with \\n’ 则会显示\\n而非换行
  • 级联字符串“this” “is” “string” 会转换为“this is string”
  • 字符串可以用+运算符实现拼接,用*表示重复
  • python中字符串有两种索引方式,从左往右从0开始,从右往左从-1开始
  • python中字符串不能改变(和java一样)
  • python没有字符类型,一个字符就是长度为1的字符串
  • 字符串切片:变量[头下标:尾下标:步长]

空行

函数之间或类之间用空行分隔,一般是两个空行,以分隔两段不同功能或含义的代码

同一行显示多条语句

使用;分隔(ps,python的语句后面不用每行加分号,好不习惯。。。)

代码块

缩进相同的一组语句构成一个代码块,我们称首行及后面的代码称为一个子句(clause)

print输出

print输出类似java中的println,默认是换行的,如果不想换行在print函数中加入参数ene=''

例如 print('x', end ='' )

基本数据类型

python中的变量不需要声明(但python是强类型语言),使用变量前必须赋值,赋值以后变量才会创建。

在python中,变量就是变量,没有类型。我们所说的类型是变量所指的内存中对象的数据类型

多个变量赋值

python
a = b = c = 1
a = b = c = 1

以上实例,创建一个整型对象1,从后向前赋值,三个变量被赋予相同的值

python
a, b, c = 1, 2, 'noob'
a, b, c = 1, 2, 'noob'

以上实例,两个整型对象1,2分配给a和b,字符串对象noob分配给变量c

标准数据类型

python3中有六个标准数据类型

  • Number
  • String
  • List
  • Tuple
  • Set
  • Dictionary

其中

  • 可变数据类型:List、Dictionary、Set
  • 不可变:Number、String、Tuple

判断数据类型

type()函数或者isinstance()函数

区别:

  • type()不会认为子类是父类的类型
  • isinstance()会认为子类是父类类型

数值运算符

特殊:

  • //除法,得到一个整数
  • %取余数(取模)
  • 混合计算时,python会把整型转换为浮点型

String

python的字符串用单引号或双引号括起来

特殊用法:

定义字符串时 加前缀 u/b/r/f

  • u:作用以Unicode格式编码字符串,一般用在中文字符串前面,防止因为源码储存格式问题,导致再次使用时出现乱码
  • b:表示:后面字符串是bytes 类型
  • r:作用是去除转义字符
  • f:作用是支持大括号内的python 表达式

字符串截取语法:变量[头下标:尾下标] 前闭后开

字符串的切片:

python
str = 'Runoob'
+
+print (str)          # 输出字符串
+print (str[0:-1])    # 输出第一个到倒数第二个的所有字符 
+print (str[0])       # 输出字符串第一个字符
+print (str[2:5])     # 输出从第三个开始到第五个的字符
+print (str[2:])      # 输出从第三个开始的后的所有字符
+print (str * 2)      # 输出字符串两次,也可以写成 print (2 * str)
+print (str + "TEST") # 连接字符串
+
+结果:
+Runoob
+Runoo
+R
+noo
+noob
+RunoobRunoob
+RunoobTEST
str = 'Runoob'
+
+print (str)          # 输出字符串
+print (str[0:-1])    # 输出第一个到倒数第二个的所有字符 
+print (str[0])       # 输出字符串第一个字符
+print (str[2:5])     # 输出从第三个开始到第五个的字符
+print (str[2:])      # 输出从第三个开始的后的所有字符
+print (str * 2)      # 输出字符串两次,也可以写成 print (2 * str)
+print (str + "TEST") # 连接字符串
+
+结果:
+Runoob
+Runoo
+R
+noo
+noob
+RunoobRunoob
+RunoobTEST

List

List是python中使用最频繁的数据类型

列表是写在方括号[]之间、用逗号分隔开的元素列表

和字符串一样,列表可以被索引和截取,列表被截取后返回一个新列表

注意:

  • 列表可以使用+进行拼接

Tuple元组

tuple和列表类似,但是是由()括起来的,且不可变数据类型

元素一样可以被索引和截取,元组也可以使用+进行拼接

Set

set可以使用大括号{}或者set()函数创建集合,创建一个空集合时必须使用set()函数而非{ },因为{ }被用来创建一个空字典

set和java中的set集合一样存储的数据都是不重复的

Dictionary

字典(dictionary)是Python中另一个非常有用的内置数据类型。 类似java的Map

列表是有序的对象集合,字典是无序的对象集合。两者之间的区别在于:字典当中的元素是通过键来存取的,而不是通过偏移存取。

字典是一种映射类型,字典用 { } 标识,它是一个无序的 键(key) : 值(value) 的集合。

键(key)必须使用不可变类型。

在同一个字典中,键(key)必须是唯一的。

python的数据类型转换

语法作用
[int(x ,base])将x转换为一个整数
float(x)将x转换到一个浮点数
[complex(real ,imag])创建一个复数
str(x)将对象 x 转换为字符串
repr(x)将对象 x 转换为表达式字符串
eval(str)用来计算在字符串中的有效Python表达式,并返回一个对象
tuple(s)将序列 s 转换为一个元组
list(s)将序列 s 转换为一个列表
set(s)转换为可变集合
dict(d)创建一个字典。d 必须是一个 (key, value)元组序列。
frozenset(s)转换为不可变集合
chr(x)将一个整数转换为一个字符
ord(x)将一个字符转换为它的整数值
hex(x)将一个整数转换为一个十六进制字符串
oct(x)将一个整数转换为一个八进制字符串

运算符

算术运算符

运算符描述实例
+加 - 两个对象相加a + b 输出结果 31
-减 - 得到负数或是一个数减去另一个数a - b 输出结果 -11
*乘 - 两个数相乘或是返回一个被重复若干次的字符串a * b 输出结果 210
/除 - x 除以 yb / a 输出结果 2.1
%取模 - 返回除法的余数b % a 输出结果 1
**幂 - 返回x的y次幂a**b 为10的21次方
//取整除 - 向下取接近商的整数>>> 9//2 4 >>> -9//2 -5

比较运算符

运算符描述实例
==等于 - 比较对象是否相等(a == b) 返回 False。
!=不等于 - 比较两个对象是否不相等(a != b) 返回 True。
>大于 - 返回x是否大于y(a > b) 返回 False。
<小于 - 返回x是否小于y。所有比较运算符返回1表示真,返回0表示假。这分别与特殊的变量True和False等价。注意,这些变量名的大写。(a < b) 返回 True。
>=大于等于 - 返回x是否大于等于y。(a >= b) 返回 False。
<=小于等于 - 返回x是否小于等于y。(a <= b) 返回 True。

赋值运算符

运算符描述实例
=简单的赋值运算符c = a + b 将 a + b 的运算结果赋值为 c
+=加法赋值运算符c += a 等效于 c = c + a
-=减法赋值运算符c -= a 等效于 c = c - a
*=乘法赋值运算符c *= a 等效于 c = c * a
/=除法赋值运算符c /= a 等效于 c = c / a
%=取模赋值运算符c %= a 等效于 c = c % a
**=幂赋值运算符c **= a 等效于 c = c ** a
//=取整除赋值运算符c //= a 等效于 c = c // a
:=海象运算符,可在表达式内部为变量赋值。Python3.8 版本新增运算符在这个示例中,赋值表达式可以避免调用 len() 两次:if (n := len(a)) > 10: print(f"List is too long ({n} elements, expected <= 10)")(这个特性java中默认就有)

逻辑运算符

运算符逻辑表达式描述实例
andx and y布尔"与" - 如果 x 为 False,x and y 返回 x 的值,否则返回 y 的计算值。(a and b) 返回 20。
orx or y布尔"或" - 如果 x 是 True,它返回 x 的值,否则它返回 y 的计算值。(a or b) 返回 10。
notnot x布尔"非" - 如果 x 为 True,返回 False 。如果 x 为 False,它返回 True。not(a and b) 返回 False

成员运算符

运算符描述实例
in如果在指定的序列中找到值返回 True,否则返回 False。x 在 y 序列中 , 如果 x 在 y 序列中返回 True。
not in如果在指定的序列中没有找到值返回 True,否则返回 False。x 不在 y 序列中 , 如果 x 不在 y 序列中返回 True。

身份运算符

运算符描述实例
isis 是判断两个标识符是不是引用自一个对象x is y, 类似 id(x) == id(y) , 如果引用的是同一个对象则返回 True,否则返回 False
is notis not 是判断两个标识符是不是引用自不同对象x is not y , 类似 id(a) != id(b)。如果引用的不是同一个对象则返回结果 True,否则返回 False。

流程控制语句

if条件控制

python的条件控控制语法上和格式上和java一些区别

python
if condition_1:
+    statement_block_1
+elif condition_2:
+    statement_block_2
+else:
+    statement_block_3
if condition_1:
+    statement_block_1
+elif condition_2:
+    statement_block_2
+else:
+    statement_block_3

Python 中用 elif 代替了 else if,所以if语句的关键字为:if – elif – else

注意:

  • 1、每个条件后面要使用冒号 :,表示接下来是满足条件后要执行的语句块。
  • 2、使用缩进来划分语句块,相同缩进数的语句在一起组成一个语句块。
  • 3、在Python中没有switch – case语句。

if后跟的表达式

  • 如果if后面的条件是数字,只要这个数字不是0,python都会把它当做True处理

  • 如果if后面跟的是字符串,则只要这个字符串不为空串,python就把它看作True

  • 同样的如果if后跟元组,list,set,字典 只要不为空就是true

三目运算

和java中的三元运算类似

语法: 变量 = 表达式1 if 条件 else 表达式2

例如

python
a = 1;
+b = a + 1 if a == 1 else a + 2;
+print(b)
+
+结果:2
a = 1;
+b = a + 1 if a == 1 else a + 2;
+print(b)
+
+结果:2

循环控制语句

python中的循环有for和while两种

while

python
while 判断条件(condition):
+    执行语句(statements)……
while 判断条件(condition):
+    执行语句(statements)……
while循环使用else

while...else在条件语句为false时执行else的代码块

python
while expr:
+    statement1
+else:
+    statement2
while expr:
+    statement1
+else:
+    statement2

for

for 循环可以遍历任何可选代对象,如一个列表或者一个字符串。

for循环的一般格式如下:

python
for <variable> in <sequence>:
+    <statements>
+else:
+    <statements>
for <variable> in <sequence>:
+    <statements>
+else:
+    <statements>

continue和break

用法和java一样

break区别

  • python中的else和配合循环使用,在循环穷尽列表(for循环)或条件变为 false (while循环)导致循环终止时被执行,但循环被 break 终止时不执行。

例如:

python
_list = [1, 2, 3, 4, 5]
+
+for i in _list:
+    if i == 3:
+        print('33333333')
+else:
+    print('循环完毕')
+
+执行结果:
+33333333
+循环完毕
_list = [1, 2, 3, 4, 5]
+
+for i in _list:
+    if i == 3:
+        print('33333333')
+else:
+    print('循环完毕')
+
+执行结果:
+33333333
+循环完毕

而当循环是break终止的时候,else代码块不会执行:

python
_list = [1, 2, 3, 4, 5]
+
+for i in _list:
+    if i == 3:
+        print('333')
+        break
+else:
+    print('循环完毕')
+
+执行结果:
+333
_list = [1, 2, 3, 4, 5]
+
+for i in _list:
+    if i == 3:
+        print('333')
+        break
+else:
+    print('循环完毕')
+
+执行结果:
+333

pass

Python pass是空语句,是为了保持程序结构的完整性。

pass 不做任何事情,一般用做占位语句

例如:

python
class MyTestClass:
+    pass
+
+
+def my_func():
+    pass
+
+
+if expr:
+    pass
class MyTestClass:
+    pass
+
+
+def my_func():
+    pass
+
+
+if expr:
+    pass

迭代器与生成器

迭代器

迭代器是python中访问集合元素的一种方式,有两个基本方法iter()next()

字符串、列表或元组对象都可以创建迭代器

python
_list = [1,2,3,4]
+it = iter(_list)
+print(next(it))
+print(next(it))
+
+结果:
+1
+2
_list = [1,2,3,4]
+it = iter(_list)
+print(next(it))
+print(next(it))
+
+结果:
+1
+2

迭代器对象可以使用常规for语句进行遍历:

python
_list = [1, 2, 3, 4]
+it = iter(_list)
+for x in it:
+    print(x, end=" ")
+
+结果:
+1 2 3 4
_list = [1, 2, 3, 4]
+it = iter(_list)
+for x in it:
+    print(x, end=" ")
+
+结果:
+1 2 3 4

生成器

在 Python 中,使用了 yield 的函数被称为生成器(generator)。

跟普通函数不同的是,生成器是一个返回迭代器的函数,只能用于迭代操作,更简单点理解生成器就是一个迭代器。

在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值, 并在下一次执行 next() 方法时从当前位置继续运行。

调用一个生成器函数,返回的是一个迭代器对象。

python
def gen_fun():
+    print('11111111111')
+    yield 1
+    print('22222222222')
+    yield 2
+    yield 3
+obj = gen_fun()
+print(obj)
+for i in obj:
+    print(i)
+#res
+<generator object gen_fun at 0x0000029394291AC0>
+11111111111
+1
+22222222222
+2
+3
def gen_fun():
+    print('11111111111')
+    yield 1
+    print('22222222222')
+    yield 2
+    yield 3
+obj = gen_fun()
+print(obj)
+for i in obj:
+    print(i)
+#res
+<generator object gen_fun at 0x0000029394291AC0>
+11111111111
+1
+22222222222
+2
+3

上面的代码可以看到在调用函数过程中,'111111'和'222222222'并没有打印出来,而是在for循环中才执行,这就是因为yield导致了函数的暂停,而for循环实际底层是迭代器实现,所以才恢复到print语句的位置继续执行

函数

对应java中的方法

定义一个函数

  • 函数代码块以 def 关键词开头,后接函数标识符名称和圆括号 ()
  • 任何传入参数和自变量必须放在圆括号中间,圆括号之间可以用于定义参数。
  • 函数的第一行语句可以选择性地使用文档字符串—用于存放函数说明。
  • 函数内容以冒号 : 起始,并且缩进。
  • return [表达式] 结束函数,选择性地返回一个值给调用方,不带表达式的 return 相当于返回 None。

一般格式:

python
def 函数名(参数列表):
+    函数体
def 函数名(参数列表):
+    函数体

默认情况下,参数值和名称是按声明的顺序匹配的

参数传递

由于python中的变量没有类型,所以不像java的参数列表都是有类型声明的

在 python 中,strings, tuples, 和 numbers 是不可更改的对象,而 list,dict 等则是可以修改的对象。

  • 不可变类型: 变量赋值 a=5 后再赋值 a=10,这里实际是新生成一个 int 值对象 10,再让 a 指向它,而 5 被丢弃,不是改变 a 的值,相当于新生成了 a。
  • 可变类型: 变量赋值 la=[1,2,3,4] 后再赋值 la[2]=5 则是将 list la 的第三个元素值更改,本身la没有动,只是其内部的一部分值被修改了。

python函数的参数传递:这个和java有区别,java都是值传递

  • 不可变类型: 值传递,如整数、字符串、元组。如 fun(a),传递的只是 a 的值,没有影响 a 对象本身。如果在 fun(a) 内部修改 a 的值,则是新生成一个 a 的对象。
  • 可变类型: 引用传递,如 列表,字典。如 fun(la),则是将 la 真正的传过去,修改后 fun 外部的 la 也会受影响

参数

以下是调用函数时可使用的正式参数类型:

必需参数

按照正确顺序传入参数,调用的数量和声明的数量必须一致

关键字参数

调用使用关键字参数来确定传入的参数值,使用关键字参数允许调用时与声明时的顺序不一致,因为python解释器能用参数名匹配参数值

python
def print_me(str):
+    print(str)
+    
+#调用
+print_me(str = 'tom')
+
+结果:
+tom
+
+
+def printinfo( name, age ):
+   "打印任何传入的字符串"
+   print ("名字: ", name)
+   print ("年龄: ", age)
+   return
+ 
+#调用printinfo函数
+printinfo( age=50, name="mike" )
+结果:
+名字: mike
+年龄: 50
def print_me(str):
+    print(str)
+    
+#调用
+print_me(str = 'tom')
+
+结果:
+tom
+
+
+def printinfo( name, age ):
+   "打印任何传入的字符串"
+   print ("名字: ", name)
+   print ("年龄: ", age)
+   return
+ 
+#调用printinfo函数
+printinfo( age=50, name="mike" )
+结果:
+名字: mike
+年龄: 50

默认参数

调用函数时,如果没有传递参数,则会使用默认参数。以下实例中如果没有传入 age 参数,则使用默认值

python
def printinfo( name, age = 35 ):
+   print ("名字: ", name)
+   print ("年龄: ", age)
+   return
+ 
+#调用printinfo函数
+printinfo( age=50, name="tom" )
+print ("------------------------")
+printinfo( name="tom" )
+
+结果:
+名字:  tom
+年龄:  50
+------------------------
+名字:  tom
+年龄:  35
def printinfo( name, age = 35 ):
+   print ("名字: ", name)
+   print ("年龄: ", age)
+   return
+ 
+#调用printinfo函数
+printinfo( age=50, name="tom" )
+print ("------------------------")
+printinfo( name="tom" )
+
+结果:
+名字:  tom
+年龄:  50
+------------------------
+名字:  tom
+年龄:  35

不定长参数

你可能需要一个函数能处理比当初声明时更多的参数。这些参数叫做不定长参数,和上述 2 种参数不同,声明时不会命名。

*args

*args就是就是传递一个可变参数列表给函数实参,这个参数列表的数目未知,甚至长度可以为0。下面这段代码演示了如何使用args

python
def test_args(first, *args):
+    print('Required argument: ', first)
+    print(type(args))
+    for v in args:
+        print ('Optional argument: ', v)
+
+test_args(1, 2, 3, 4)
+
+结果:
+Required argument:  1
+<class 'tuple'>
+Optional argument:  2
+Optional argument:  3
+Optional argument:  4
def test_args(first, *args):
+    print('Required argument: ', first)
+    print(type(args))
+    for v in args:
+        print ('Optional argument: ', v)
+
+test_args(1, 2, 3, 4)
+
+结果:
+Required argument:  1
+<class 'tuple'>
+Optional argument:  2
+Optional argument:  3
+Optional argument:  4
**kwargs

而**kwargs则是将一个可变的关键字参数的字典传给函数实参,同样参数列表长度可以为0或为其他值。下面这段代码演示了如何使用kwargs

python
def test_kwargs(first, *args, **kwargs):
+   print('Required argument: ', first)
+   print(type(kwargs))
+   for v in args:
+      print ('Optional argument (args): ', v)
+   for k, v in kwargs.items():
+      print ('Optional argument %s (kwargs): %s' % (k, v))
+
+test_kwargs(1, 2, 3, 4, k1=5, k2=6)
+
+结果:
+Required argument:  1
+<class 'dict'>
+Optional argument (args):  2
+Optional argument (args):  3
+Optional argument (args):  4
+Optional argument k2 (kwargs): 6
+Optional argument k1 (kwargs): 5
def test_kwargs(first, *args, **kwargs):
+   print('Required argument: ', first)
+   print(type(kwargs))
+   for v in args:
+      print ('Optional argument (args): ', v)
+   for k, v in kwargs.items():
+      print ('Optional argument %s (kwargs): %s' % (k, v))
+
+test_kwargs(1, 2, 3, 4, k1=5, k2=6)
+
+结果:
+Required argument:  1
+<class 'dict'>
+Optional argument (args):  2
+Optional argument (args):  3
+Optional argument (args):  4
+Optional argument k2 (kwargs): 6
+Optional argument k1 (kwargs): 5
参数中单独的*

声明函数时,参数中星号 * 可以单独出现,例如:

参数列表里的 * 星号,标志着位置参数的就此终结,之后的那些参数,都只能以关键字形式来指定。

python
def f(a,b,*,c):
+    return a+b+c
+
+# f(1,2,3)->会报错
+# f(1,2,c=3) -> 正常
def f(a,b,*,c):
+    return a+b+c
+
+# f(1,2,3)->会报错
+# f(1,2,c=3) -> 正常
调用

args和kwargs不仅可以在函数定义中使用,还可以在函数调用中使用。在函数定义时使用就相当于pack(打包),在函数调用时就相当于unpack(解包)。

首先来看一下使用args来解包调用函数的代码,

python
def test_args_kwargs(arg1, arg2, arg3):
+    print("arg1:", arg1)
+    print("arg2:", arg2)
+    print("arg3:", arg3)
+
+args = ("two", 3, 5)
+test_args_kwargs(*args)
+
+结果:
+arg1: two
+arg2: 3
+arg3: 5
def test_args_kwargs(arg1, arg2, arg3):
+    print("arg1:", arg1)
+    print("arg2:", arg2)
+    print("arg3:", arg3)
+
+args = ("two", 3, 5)
+test_args_kwargs(*args)
+
+结果:
+arg1: two
+arg2: 3
+arg3: 5

kwargs的用法类似:

bash
kwargs = {"arg3": 3, "arg2": "two", "arg1": 5}
+test_args_kwargs(**kwargs)
+
+#result
+arg1: 5
+arg2: two
+arg3: 3
kwargs = {"arg3": 3, "arg2": "two", "arg1": 5}
+test_args_kwargs(**kwargs)
+
+#result
+arg1: 5
+arg2: two
+arg3: 3

匿名函数

python中使用lambda来创建匿名函数

所谓匿名,意即不再使用 def 语句这样标准的形式定义一个函数。

  • lambda 只是一个表达式,函数体比 def 简单很多。
  • lambda的主体是一个表达式,而不是一个代码块。仅仅能在lambda表达式中封装有限的逻辑进去。
  • lambda 函数拥有自己的命名空间,且不能访问自己参数列表之外或全局命名空间里的参数。
  • lambda函数只能写一行

语法

python
lambda [arg1 [,arg2,.....argn]]:expression
lambda [arg1 [,arg2,.....argn]]:expression

例子:

python
sum = lambda arg1, arg2: arg1 + arg2
+ 
+# 调用sum函数
+print ("相加后的值为 : ", sum( 10, 20 ))
+print ("相加后的值为 : ", sum( 20, 20 ))
+
+相加后的值为 :  30
+相加后的值为 :  40
sum = lambda arg1, arg2: arg1 + arg2
+ 
+# 调用sum函数
+print ("相加后的值为 : ", sum( 10, 20 ))
+print ("相加后的值为 : ", sum( 20, 20 ))
+
+相加后的值为 :  30
+相加后的值为 :  40

列表推导式

作用:快速生成列表

语法:

python
变量 = [生成规则 for 临时变量 in 集合]
变量 = [生成规则 for 临时变量 in 集合]

每循环一次就会生成一个符合生成规则的数据添加到列表中

例如:

python
my_list = [i for i in range(5)]
+print(my_list)
+
+#res
+[0,1,2,3,4,5]
my_list = [i for i in range(5)]
+print(my_list)
+
+#res
+[0,1,2,3,4,5]

强制位置参数

Python3.8 新增了一个函数形参语法 / 用来指明函数形参必须使用指定位置参数,不能使用关键字参数的形式。

在以下的例子中,形参 a 和 b 必须使用指定位置参数,c 或 d 可以是位置形参或关键字形参,而 e 或 f 要求为关键字形参:

python
def f(a, b, /, c, d, *, e, f):
+    print(a, b, c, d, e, f)
def f(a, b, /, c, d, *, e, f):
+    print(a, b, c, d, e, f)

正确:

python
f(10, 20, 30, d=40, e=50, f=60)
f(10, 20, 30, d=40, e=50, f=60)

错误:

python
f(10, b=20, c=30, d=40, e=50, f=60)   # b 不能使用关键字参数的形式
+f(10, 20, 30, 40, 50, f=60)           # e 必须使用关键字参数的形式
f(10, b=20, c=30, d=40, e=50, f=60)   # b 不能使用关键字参数的形式
+f(10, 20, 30, 40, 50, f=60)           # e 必须使用关键字参数的形式

模块

python
import module1[, module2[,... moduleN]
+from modname import name1[, name2[, ... nameN]]
+from modname import *
import module1[, module2[,... moduleN]
+from modname import name1[, name2[, ... nameN]]
+from modname import *

__name__属性

一个模块被另一个程序第一次引入时,其主程序将运行。如果我们想在模块被引入时,模块中的某一程序块不执行,我们可以用__name__属性来使该程序块仅在该模块自身运行时执行。

python
if __name__ == '__main__':
+   print('程序自身在运行')
+else:
+   print('我来自另一模块')
if __name__ == '__main__':
+   print('程序自身在运行')
+else:
+   print('我来自另一模块')

说明: 每个模块都有一个__name__属性,当其值是'main'时,表明该模块自身在运行,否则是被引入。

说明:namemain 底下是双下划线

dir()函数

python
dir(sys)  
+['__displayhook__', '__doc__', '__excepthook__', '__loader__', '__name__',
+ '__package__', '__stderr__', '__stdin__', '__stdout__',
+ '_clear_type_cache', '_current_frames', '_debugmallocstats', '_getframe',
+ '_home', '_mercurial', '_xoptions', 'abiflags', 'api_version', 'argv',
+ 'base_exec_prefix', 'base_prefix', 'builtin_module_names', 'byteorder',
+ 'call_tracing', 'callstats', 'copyright', 'displayhook',
+ 'dont_write_bytecode', 'exc_info', 'excepthook', 'exec_prefix',
+ 'executable', 'exit', 'flags', 'float_info', 'float_repr_style',
+ 'getcheckinterval', 'getdefaultencoding', 'getdlopenflags',
+ 'getfilesystemencoding', 'getobjects', 'getprofile', 'getrecursionlimit',
+ 'getrefcount', 'getsizeof', 'getswitchinterval', 'gettotalrefcount',
+ 'gettrace', 'hash_info', 'hexversion', 'implementation', 'int_info',
+ 'intern', 'maxsize', 'maxunicode', 'meta_path', 'modules', 'path',
+ 'path_hooks', 'path_importer_cache', 'platform', 'prefix', 'ps1',
+ 'setcheckinterval', 'setdlopenflags', 'setprofile', 'setrecursionlimit',
+ 'setswitchinterval', 'settrace', 'stderr', 'stdin', 'stdout',
+ 'thread_info', 'version', 'version_info', 'warnoptions']
dir(sys)  
+['__displayhook__', '__doc__', '__excepthook__', '__loader__', '__name__',
+ '__package__', '__stderr__', '__stdin__', '__stdout__',
+ '_clear_type_cache', '_current_frames', '_debugmallocstats', '_getframe',
+ '_home', '_mercurial', '_xoptions', 'abiflags', 'api_version', 'argv',
+ 'base_exec_prefix', 'base_prefix', 'builtin_module_names', 'byteorder',
+ 'call_tracing', 'callstats', 'copyright', 'displayhook',
+ 'dont_write_bytecode', 'exc_info', 'excepthook', 'exec_prefix',
+ 'executable', 'exit', 'flags', 'float_info', 'float_repr_style',
+ 'getcheckinterval', 'getdefaultencoding', 'getdlopenflags',
+ 'getfilesystemencoding', 'getobjects', 'getprofile', 'getrecursionlimit',
+ 'getrefcount', 'getsizeof', 'getswitchinterval', 'gettotalrefcount',
+ 'gettrace', 'hash_info', 'hexversion', 'implementation', 'int_info',
+ 'intern', 'maxsize', 'maxunicode', 'meta_path', 'modules', 'path',
+ 'path_hooks', 'path_importer_cache', 'platform', 'prefix', 'ps1',
+ 'setcheckinterval', 'setdlopenflags', 'setprofile', 'setrecursionlimit',
+ 'setswitchinterval', 'settrace', 'stderr', 'stdin', 'stdout',
+ 'thread_info', 'version', 'version_info', 'warnoptions']

注意点

自定义模块名不要和系统中要使用的模块名字一样

模块搜索顺序->当前目录->系统目录(sys.path)-> 程序报错

包是一种管理 Python 模块命名空间的形式,采用"点模块名称"。

比如一个模块的名称是 A.B, 那么他表示一个包 A中的子模块 B 。采用点模块名称这种形式也不用担心不同库之间的模块重名的情况

sound/                          顶层包
+      __init__.py               初始化 sound 包
+      formats/                  文件格式转换子包
+              __init__.py
+              wavread.py
+              wavwrite.py
+              aiffread.py
+              aiffwrite.py
+              auread.py
+              auwrite.py
+              ...
+      effects/                  声音效果子包
+              __init__.py
+              echo.py
+              surround.py
+              reverse.py
+              ...
+      filters/                  filters 子包
+              __init__.py
+              equalizer.py
+              vocoder.py
+              karaoke.py
+              ...
sound/                          顶层包
+      __init__.py               初始化 sound 包
+      formats/                  文件格式转换子包
+              __init__.py
+              wavread.py
+              wavwrite.py
+              aiffread.py
+              aiffwrite.py
+              auread.py
+              auwrite.py
+              ...
+      effects/                  声音效果子包
+              __init__.py
+              echo.py
+              surround.py
+              reverse.py
+              ...
+      filters/                  filters 子包
+              __init__.py
+              equalizer.py
+              vocoder.py
+              karaoke.py
+              ...

在导入一个包的时候,Python 会根据 sys.path 中的目录来寻找这个包中包含的子目录。

目录只有包含一个叫做 __init__.py 的文件才会被认作是一个包,主要是为了避免一些滥俗的名字(比如叫做 string)不小心的影响搜索路径中的有效模块。

最简单的情况,放一个空的 :file:__init__.py就可以了。当然这个文件中也可以包含一些初始化代码或者为(将在后面介绍的) __all__变量赋值。

用户可以每次只导入一个包里面的特定模块,比如:

import sound.effects.echo
import sound.effects.echo

这将会导入子模块:sound.effects.echo。 他必须使用全名去访问:

sound.effects.echo.echofilter(input, output, delay=0.7, atten=4)
sound.effects.echo.echofilter(input, output, delay=0.7, atten=4)

还有一种导入子模块的方法是:

from sound.effects import echo
from sound.effects import echo

这同样会导入子模块: echo,并且他不需要那些冗长的前缀,所以他可以这样使用:

echo.echofilter(input, output, delay=0.7, atten=4)
echo.echofilter(input, output, delay=0.7, atten=4)

还有一种变化就是直接导入一个函数或者变量:

from sound.effects.echo import echofilter
from sound.effects.echo import echofilter

同样的,这种方法会导入子模块: echo,并且可以直接使用他的 echofilter() 函数:

echofilter(input, output, delay=0.7, atten=4)
echofilter(input, output, delay=0.7, atten=4)

注意当使用 from package import item 这种形式的时候,对应的 item 既可以是包里面的子模块(子包),或者包里面定义的其他名称,比如函数,类或者变量。

import 语法会首先把 item 当作一个包定义的名称,如果没找到,再试图按照一个模块去导入。如果还没找到,抛出一个 :exc:ImportError 异常。

反之,如果使用形如 import item.subitem.subsubitem 这种导入形式,除了最后一项,都必须是包,而最后一项则可以是模块或者是包,但是不可以是类,函数或者变量的名字。

从一个包中导入*

from sound.effects import * : Python 会进入文件系统,找到这个包里面所有的子模块,然后一个一个的把它们都导入进来。

导入语句遵循如下规则:如果包定义文件 __init__.py 存在一个叫做 all 的列表变量,那么在使用 from package import * 的时候就把这个列表中的所有名字作为包内容导入。

以下实例在 file:sounds/effects/_init_.py 中包含如下代码:

__all__ = ["echo", "surround", "reverse"]
__all__ = ["echo", "surround", "reverse"]

这表示当你使用from sound.effects import *这种用法时,你只会导入包里面这三个子模块。

如果 __all__ 真的没有定义,那么使用**from sound.effects import ***这种语法的时候,就不会导入包 sound.effects 里的任何子模块。他只是把包sound.effects和它里面定义的所有内容导入进来(可能运行__init__.py里定义的初始化代码)。

这会把__init__.py里面定义的所有名字导入进来。并且他不会破坏掉我们在这句话之前导入的所有明确指定的模块。看下这部分代码:

import sound.effects.echo
+import sound.effects.surround
+from sound.effects import *
import sound.effects.echo
+import sound.effects.surround
+from sound.effects import *

这个例子中,在执行 from...import 前,包 sound.effects 中的 echo 和 surround 模块都被导入到当前的命名空间中了。(当然如果定义了 __all__ 就更没问题了)

文件操作

python的io操作相比java的IO流简单太多了,直接就是一个open()函数

open() 方法

Python open() 方法用于打开一个文件,并返回文件对象,在对文件进行处理过程都需要使用到这个函数,如果该文件无法被打开,会抛出 OSError。

**注意:**使用 open() 方法一定要保证关闭文件对象,即调用 close() 方法。

open() 函数常用形式是接收两个参数:文件名(file)和模式(mode)。

完整的语法格式为:

python
open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)

参数说明:

  • file: 必需,文件路径(相对或者绝对路径)。
  • mode: 可选,文件打开模式
  • buffering: 设置缓冲
  • encoding: 一般使用utf8
  • errors: 报错级别
  • newline: 区分换行符
  • closefd: 传入的file参数类型
  • opener: 设置自定义开启器,开启器的返回值必须是一个打开的文件描述符。
模式描述
t文本模式 (默认)。
x写模式,新建一个文件,如果该文件已存在则会报错。
b二进制模式。
+打开一个文件进行更新(可读可写)。
r以只读方式打开文件。文件的指针将会放在文件的开头。这是默认模式。
rb以二进制格式打开一个文件用于只读。文件指针将会放在文件的开头。这是默认模式。一般用于非文本文件如图片等。
r+打开一个文件用于读写。文件指针将会放在文件的开头。
rb+以二进制格式打开一个文件用于读写。文件指针将会放在文件的开头。一般用于非文本文件如图片等。
w打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。
wb以二进制格式打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。一般用于非文本文件如图片等。
w+打开一个文件用于读写。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。
wb+以二进制格式打开一个文件用于读写。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。一般用于非文本文件如图片等。
a打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。
ab以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。
a+打开一个文件用于读写。如果该文件已存在,文件指针将会放在文件的结尾。文件打开时会是追加模式。如果该文件不存在,创建新文件用于读写。
ab+以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。如果该文件不存在,创建新文件用于读写。

file 对象

file 对象使用 open 函数来创建,下表列出了 file 对象常用的函数:

方法及描述
file.close()关闭文件。关闭后文件不能再进行读写操作。
file.flush()刷新文件内部缓冲,直接把内部缓冲区的数据立刻写入文件, 而不是被动的等待输出缓冲区写入。
file.fileno()返回一个整型的文件描述符(file descriptor FD 整型), 可以用在如os模块的read方法等一些底层操作上。
file.isatty()如果文件连接到一个终端设备返回 True,否则返回 False。
[file.read(size])从文件读取指定的字节数,如果未给定或为负则读取所有。
[file.readline(size])读取整行,包括 "\\n" 字符。
[file.readlines(sizeint])读取所有行并返回列表,若给定sizeint>0,返回总和大约为sizeint字节的行, 实际读取值可能比 sizeint 较大, 因为需要填充缓冲区。
[file.seek(offset, whence])移动文件读取指针到指定位置
file.tell()返回文件当前位置。
[file.truncate(size])从文件的首行首字符开始截断,截断文件为 size 个字符,无 size 表示从当前位置截断;截断之后后面的所有字符被删除,其中 windows 系统下的换行代表2个字符大小。
file.write(str)将字符串写入文件,返回的是写入的字符长度。
file.writelines(sequence)向文件写入一个序列字符串列表,如果需要换行则要自己加入每行的换行符。

文件和文件夹操作

python
import os #导入os模块
+
+#修改文件名
+os.rename(原文件名,新文件名)
+#删除文件
+os.remove(文件名)
+#创建文件夹
+os.mkdir(名称)
+#获取当前目录
+os.getcwd()
+#改变默认目录
+os.chdir(路径)
+#获取目录列表
+os.listdir(路径)
+#删除文件夹
+os.rmdir(路径)
import os #导入os模块
+
+#修改文件名
+os.rename(原文件名,新文件名)
+#删除文件
+os.remove(文件名)
+#创建文件夹
+os.mkdir(名称)
+#获取当前目录
+os.getcwd()
+#改变默认目录
+os.chdir(路径)
+#获取目录列表
+os.listdir(路径)
+#删除文件夹
+os.rmdir(路径)

面向对象

类的定义语法:

类名遵循大驼峰规则

python
"""
+新式类:直接或间接继承object,py3中所有类都是object的子类(same as java)
+"""
+class Demo(object):
+    pass
+"""
+旧式类:已过时
+"""
+class Demo1():
+    pass
+
+class Demo2:
+    pass
"""
+新式类:直接或间接继承object,py3中所有类都是object的子类(same as java)
+"""
+class Demo(object):
+    pass
+"""
+旧式类:已过时
+"""
+class Demo1():
+    pass
+
+class Demo2:
+    pass

类中定义方法

python
class Dog(Object):
+    def eat(self):  
+        print('吃')
class Dog(Object):
+    def eat(self):  
+        print('吃')

对象

创建对象语法:

Python
class Dog(Object):
+    def eat(self):  
+        print('吃')
+
+
+dog1 = Dog()
+dog1.eat()
+#res
+
class Dog(Object):
+    def eat(self):  
+        print('吃')
+
+
+dog1 = Dog()
+dog1.eat()
+#res
+

类外部添加和获取属性

给对象添加属性:对象.属性名 = 属性值

获取对象的属性变量 = 对象.属性名

修改:和添加一样,添加存在的属性就是修改

魔法方法

bash
python的类中,有一类方法,以\`两个下划线开头\`\`两个下划线结尾\`,并在满足\`某个特定条件下会自动调用\`,这类方法,称为\`魔法方法\` magic method
python的类中,有一类方法,以\`两个下划线开头\`\`两个下划线结尾\`,并在满足\`某个特定条件下会自动调用\`,这类方法,称为\`魔法方法\` magic method

__init__

在创建对象之后自动调用

作用:

  • 给对象添加属性,给对象属性一个初始值(构造方法)
  • 代码的业务需求,每创建一个对象,都需要执行的代码可以放在init方法中

注意点:

  • 如果__init__方法出现了self之外的形参,在创建对象的时候,需要给额外的形参传值类名(实参) 这个类似java中的构造方法的有参构造
python
class Dog(object):
+    def __init__(self,name):
+        self.name = name
+        print('init方法执行了')
+
+dog = Dog('大黄')
+print(dog.name)
+#res
+init方法执行了
+大黄
class Dog(object):
+    def __init__(self,name):
+        self.name = name
+        print('init方法执行了')
+
+dog = Dog('大黄')
+print(dog.name)
+#res
+init方法执行了
+大黄

__str__

类似java的toString:

  • 在print(对象)时会自动调用__str__方法,打印的结果是__str__方法的返回值
  • str(对象)将自定义类型转换为字符串的时候,会自动调用
  • 没有自定义__str__方法时,这个返回值是对象的地址

注意点:

  • 方法必须返回一个字符串,只有self一个参数

__del__

对象在内存当中被销毁的时候调用:

  1. 程序代码结束,程序运行过程中创建的对象和变量都会被删除
  2. 使用del 变量语句删除,将这个对象的引用计数变为0,会自动调用

引用计数:python内存管理的机制,指一块内存有多少变量在引用

  • 当一个变量引用一块内存时,引用计数+1
  • 删除一个变量或者这个变量不再引用这块内存,引用计数-1
  • 当内存的引用计数变为0,这块内存被删除,数据被销毁
补充:

Java中JVM为了避免对象间存在循环依赖导致对象无法被回收,JVM的垃圾回收算法采用的是可达性分析算法,通过gc roots对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到gc roots没有任何引用链相连时,则证明此对象是不可用的

类内部添加和获取属性

通过self操作:

self指的是当前实例(类似java中的this),作为类中方法的第一个形参,在通过对象调用方法的时候,不需要手动传参

python解释器会自动把调用方法的对象传递给self形参

self也可以改成其他的形参名,但一般不修改这个名字,默认为self

python
class Dog(object):
+    def play(self): 
+        print(f'{self.name}在玩耍')
+        
+dog = Dog()
+dog.name = '大黄'
+dog.play()
+
+#res
+大黄在玩耍
class Dog(object):
+    def play(self): 
+        print(f'{self.name}在玩耍')
+        
+dog = Dog()
+dog.name = '大黄'
+dog.play()
+
+#res
+大黄在玩耍

继承

python中继承的语法

  • 单继承
python
class Animal(object):
+    pass
+
+class Dog(Animal):
+    pass
class Animal(object):
+    pass
+
+class Dog(Animal):
+    pass
  • 多继承:python中允许多继承,java中是没有多继承的
python
class (object):
+    pass
+
+class (object):
+    pass
+
+class 骡子(,):
+    pass
class (object):
+    pass
+
+class (object):
+    pass
+
+class 骡子(,):
+    pass

需要注意:

多继承中圆括号中父类的顺序,若是父类中有相同的方法名,而在子类使用时未指定,python从左至右搜索 即方法在子类中未找到时,从左到右查找父类中是否包含方法。

子类重写父类方法

和java一样,子类重写父类中的同名方法,通过子类独对象调用方法时调用的是子类自己的方法

子类调用父类方法

java中调用父类就用super,python有以下几种方式

  • 父类名.方法名(self,其他参数)

  • super(类A,self).方法名(参数),会调用类A的父类中的方法

  • super().方法名(参数)=>super(当前类,self).方法名(参数) 是第二中的简写,调用当前类的父类

继承中的init方法

子类重写父类的init方法:在子类的init方法需要调用父类的init方法(和java也一样),给对象添加从父类继承的属性

注意:子类init方法的形参,一般先写父类的形参,再写自己独有的形参

python
class Dog(object):
+    def __init__(self,name):
+        self.name = name
+        self.age = 1
+       
+    def __str__(self):
+        return f'名字为{self.name},年龄为{self.age}'
+
+class MyDog(Dog):
+    def __init__(self,name,color):
+        super().__init__(name)
+        self.color = color
+        
+    def __str__(self):
+        return f'名字为{self.name},年龄为{self.age},颜色为{self.color}'
+    
+    
+dog = MyDog('大黄','黄色')
+print(dog)
+#res
+名字为大黄,年龄为1,颜色为黄色
class Dog(object):
+    def __init__(self,name):
+        self.name = name
+        self.age = 1
+       
+    def __str__(self):
+        return f'名字为{self.name},年龄为{self.age}'
+
+class MyDog(Dog):
+    def __init__(self,name,color):
+        super().__init__(name)
+        self.color = color
+        
+    def __str__(self):
+        return f'名字为{self.name},年龄为{self.age},颜色为{self.color}'
+    
+    
+dog = MyDog('大黄','黄色')
+print(dog)
+#res
+名字为大黄,年龄为1,颜色为黄色

封装

封装的意义:

  • 将属性和方法放在一起作为一个整体,通过实例化对象来进行操作
  • 隐藏内部实现
  • 对类的属性和方法增加访问权限控制

私有权限

python没有java中的权限修饰符public/private之类的,私有的属性或者方法都由两个下划线开头

  • 普通的属性前面加两个下划线就是私有属性

  • 方法名前面加两个下划线就是私有方法

和java一样私有属性不能被继承,私有方法不能在类外部访问,可以提供共有方法访问私有属性或私有方法

类属性

类似java中的静态变量

访问:类名.类属性

修改:类名.类属性 = 属性值

类方法

类方法:使用@classmethod装饰的方法称为类方法,第一个参数是cls,代表类对象自己

注意:

  1. 如果在方法中使用了实例属性,那么该方法必须是实例方法,不能为类方法

何时定义类方法:

  • 不需要使用实例属性,需要使用类属性

调用:

  • 对象.类方法
  • 类名.类方法

静态方法

使用@staticmethod装饰的方法称为静态方法,对参数没有特殊要求,可以有,可以没有

何时定义:

  • 不需要使用实例属性,也不需要使用类属性,可以定义方法为静态方法

调用:

  • 对象.静态方法
  • 类名.静态方法

多态

由于python不需要声明变量类型,因此多态体现的不是那么直观,思想和java一样,可以使用父类的地方,也可以使用子类,使用多态的意义在于提高应用的扩展性

异常

组成:

  • 异常的类型
  • 异常的描述

捕获单个异常

python
try:
+    statement1
+except 异常名:
+    statement2
try:
+    statement1
+except 异常名:
+    statement2

捕获多个异常

python
try:
+    statement1
+except (异常1,异常2,...):
+    statement2
+    
+try:
+    statement1
+except 异常1:
+    statement2
+except 异常2:
+    statement3
try:
+    statement1
+except (异常1,异常2,...):
+    statement2
+    
+try:
+    statement1
+except 异常1:
+    statement2
+except 异常2:
+    statement3

打印异常信息

python
try:
+    statement1
+except (异常1,异常2,...) as 变量名:
+    print(变量名)
try:
+    statement1
+except (异常1,异常2,...) as 变量名:
+    print(变量名)

捕获所有异常

python
try:
+    statement1
+except: #缺点 不能获取异常信息
+    statement2
+    
+try:
+    statement1
+except Exception as 变量名: 
+    print(变量名)
try:
+    statement1
+except: #缺点 不能获取异常信息
+    statement2
+    
+try:
+    statement1
+except Exception as 变量名: 
+    print(变量名)

异常的完整结构

python
try:
+    statement1
+except Exception as e:
+    print(e)
+else:
+    代码没有发生异常会执行的代码块
+finally:
+    不管有没有异常都会执行的代码块
try:
+    statement1
+except Exception as e:
+    print(e)
+else:
+    代码没有发生异常会执行的代码块
+finally:
+    不管有没有异常都会执行的代码块

抛出自定义异常

python
raise 异常对象
+
+
+
+异常对象 = 异常类(参数)
+
+
+抛出自定义异常:
+    1.自定义异常类,继承Exception或者BaseException
+    2.选择性定义__init__方法,__str__方法
+    3.抛出
raise 异常对象
+
+
+
+异常对象 = 异常类(参数)
+
+
+抛出自定义异常:
+    1.自定义异常类,继承Exception或者BaseException
+    2.选择性定义__init__方法,__str__方法
+    3.抛出
`,370),e=[o];function t(c,r,y,E,i,d){return a(),n("div",null,e)}const g=s(p,[["render",t]]);export{F as __pageData,g as default}; diff --git "a/assets/python_base_python\345\237\272\347\241\200\350\257\255\346\263\225.md.2ec54814.lean.js" "b/assets/python_base_python\345\237\272\347\241\200\350\257\255\346\263\225.md.2ec54814.lean.js" new file mode 100644 index 000000000..c77ad3a26 --- /dev/null +++ "b/assets/python_base_python\345\237\272\347\241\200\350\257\255\346\263\225.md.2ec54814.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as l}from"./chunks/framework.dc1f4608.js";const F=JSON.parse('{"title":"python基础语法","description":"","frontmatter":{},"headers":[],"relativePath":"python/base/python基础语法.md","filePath":"python/base/python基础语法.md","lastUpdated":1708510102000}'),p={name:"python/base/python基础语法.md"},o=l("",370),e=[o];function t(c,r,y,E,i,d){return a(),n("div",null,e)}const g=s(p,[["render",t]]);export{F as __pageData,g as default}; diff --git "a/assets/python_base_python\345\271\266\345\217\221\347\274\226\347\250\213.md.d5a5de0a.js" "b/assets/python_base_python\345\271\266\345\217\221\347\274\226\347\250\213.md.d5a5de0a.js" new file mode 100644 index 000000000..5fabf8b9a --- /dev/null +++ "b/assets/python_base_python\345\271\266\345\217\221\347\274\226\347\250\213.md.d5a5de0a.js" @@ -0,0 +1,1023 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"python并发编程","description":"","frontmatter":{},"headers":[],"relativePath":"python/base/python并发编程.md","filePath":"python/base/python并发编程.md","lastUpdated":1708510102000}'),p={name:"python/base/python并发编程.md"},o=l(`

python并发编程

python对并发编程的支持

  • 多线程:threading,利用CPU运算和IO可以同时执行,让CPU不会干巴巴等待IO完成

  • 多进程:multiprocessing,利用多核CPU的能力,真正的并行执行任务

  • 异步IO:asyncio,在单线程中利用CPU和IO同时执行的原理,实现函数异步执行

  • 使用Lock对资源加锁,防止冲突访问

  • 使用Queue实现不同线程、进程之间的数据通信,实现生产者消费者模式

  • 使用线程池Pool、进程池Pool,简化线程、进程任务的提交、等待结束、获取结果

  • 使用subprocess启动外部程序的进程,并进行输出交互

如何选择多线程/多进程/多协程

什么是CPU密集型、IO密集型

CPU密集型计算:也叫计算密集型,指I/O很短时间内就完成,CPU需要大量计算和处理,特点是CPU占用率很高,例如解压缩,加密解密,正则匹配等。

I/O密集型计算:硬盘、内存、网络的读写操作,例如文件处理、网络爬虫、读写数据库等。

对比

多进程Process(multiprocessing):

  • 优点:可以利用多核CPU进行并行运算
  • 缺点:占用资源多,可启动数目少
  • 适用于:计算密集型任务,例如解压缩、加解密。。

多线程Thread(threading):

  • 一个进程中可以启动多个线程

  • 相比进程更轻量级,占用资源更少。但只能单CPU并发执行,不能利用多CPU(GIL,全局解释器锁)

  • 相比协程,线程启动数目有限制,占用内存资源,有线程切换的开销

  • 适用于:IO密集型任务,同时运行的任务数目要求不高

多协程Coroutine(asyncio):

  • 一个线程中可以启动多个协程

  • 优点:内存开销最小,启动数量最多

  • 缺点:支持的库有限(aiohttp vs requests),代码实现较为复杂

  • 适用于:I/O密集型任务,需要超多任务运行且有现有库支持的场景

多线程

多线程的两种实现方式

通过threading模块的Thread类

python
import time
+import threading
+
+
+import requests
+import threading
+import time
+
+urls = [f'https://www.cnblogs.com/#p{i}' for i in range(1, 50)]
+
+
+def craw(url):
+    r = requests.get(url)
+    time.sleep(1)
+    print(url, len(r.text))
+
+
+if __name__ == '__main__':
+    start = time.time()
+    t = threading.Thread(target=craw, args=(urls[0],))
+    t.start()
+    t.join()
+    print(f'cost {start - time.time()}s')
import time
+import threading
+
+
+import requests
+import threading
+import time
+
+urls = [f'https://www.cnblogs.com/#p{i}' for i in range(1, 50)]
+
+
+def craw(url):
+    r = requests.get(url)
+    time.sleep(1)
+    print(url, len(r.text))
+
+
+if __name__ == '__main__':
+    start = time.time()
+    t = threading.Thread(target=craw, args=(urls[0],))
+    t.start()
+    t.join()
+    print(f'cost {start - time.time()}s')

通过继承Thread类

python
import requests
+import time
+import threading
+from blog_spider import urls
+
+
+class MyThread(threading.Thread):
+    def __init__(self, url):
+        super().__init__()
+        self.url = url
+
+    def run(self):
+        r = requests.get(self.url)
+        time.sleep(1)
+        print(self.url, len(r.text))
+
+
+if __name__ == '__main__':
+    start = time.time()
+    t = MyThread(urls[0])
+    t.start()
+    t.join()
+    print(time.time() - start)
import requests
+import time
+import threading
+from blog_spider import urls
+
+
+class MyThread(threading.Thread):
+    def __init__(self, url):
+        super().__init__()
+        self.url = url
+
+    def run(self):
+        r = requests.get(self.url)
+        time.sleep(1)
+        print(self.url, len(r.text))
+
+
+if __name__ == '__main__':
+    start = time.time()
+    t = MyThread(urls[0])
+    t.start()
+    t.join()
+    print(time.time() - start)

线程同步

使用 Thread 对象的 Lock 实现

python
import threading
+import time
+
+
+class MyThread(threading.Thread):
+    def __init__(self, thread_id, name, counter):
+        super().__init__(name=name)
+        self.name = name
+        self.thread_id = thread_id
+        self.counter = counter
+
+    def run(self):
+        print("开启线程: " + self.name)
+        # 获取锁.用于线程同步
+        my_lock.acquire()
+        print_time(self.name, self.counter, 3)
+        # 释放锁
+        my_lock.release()
+
+
+def print_time(thread_name, delay, counter):
+    while counter:
+        time.sleep(delay)
+        print(f"#{thread_name}: {time.ctime(time.time())}")
+        counter -= 1
+
+
+my_lock = threading.Lock()
+threads = []
+# 创建新线程
+thread1 = MyThread(1, "Thread-1", 1)
+thread2 = MyThread(2, "Thread-2", 2)
+thread1.start()
+thread2.start()
+# 添加到线程列表
+threads.append(thread1)
+threads.append(thread2)
+
+for t in threads:
+    t.join()
+print("退出主线程")
import threading
+import time
+
+
+class MyThread(threading.Thread):
+    def __init__(self, thread_id, name, counter):
+        super().__init__(name=name)
+        self.name = name
+        self.thread_id = thread_id
+        self.counter = counter
+
+    def run(self):
+        print("开启线程: " + self.name)
+        # 获取锁.用于线程同步
+        my_lock.acquire()
+        print_time(self.name, self.counter, 3)
+        # 释放锁
+        my_lock.release()
+
+
+def print_time(thread_name, delay, counter):
+    while counter:
+        time.sleep(delay)
+        print(f"#{thread_name}: {time.ctime(time.time())}")
+        counter -= 1
+
+
+my_lock = threading.Lock()
+threads = []
+# 创建新线程
+thread1 = MyThread(1, "Thread-1", 1)
+thread2 = MyThread(2, "Thread-2", 2)
+thread1.start()
+thread2.start()
+# 添加到线程列表
+threads.append(thread1)
+threads.append(thread2)
+
+for t in threads:
+    t.join()
+print("退出主线程")

结果:

bash
开启线程: Thread-1
+开启线程: Thread-2
+#Thread-1: Mon Apr 12 21:54:42 2021
+#Thread-1: Mon Apr 12 21:54:43 2021
+#Thread-1: Mon Apr 12 21:54:44 2021
+#Thread-2: Mon Apr 12 21:54:46 2021
+#Thread-2: Mon Apr 12 21:54:48 2021
+#Thread-2: Mon Apr 12 21:54:50 2021
+退出主线程
开启线程: Thread-1
+开启线程: Thread-2
+#Thread-1: Mon Apr 12 21:54:42 2021
+#Thread-1: Mon Apr 12 21:54:43 2021
+#Thread-1: Mon Apr 12 21:54:44 2021
+#Thread-2: Mon Apr 12 21:54:46 2021
+#Thread-2: Mon Apr 12 21:54:48 2021
+#Thread-2: Mon Apr 12 21:54:50 2021
+退出主线程

Lock的使用方式

1.try-finally模式

python
import threading 
+
+lock = threading.lock()
+lock.acquire()
+try:
+    #do something
+finally:
+    lock.release()
import threading 
+
+lock = threading.lock()
+lock.acquire()
+try:
+    #do something
+finally:
+    lock.release()
  1. with模式
python
import threading
+
+lock = threading.lock()
+
+with lock:
+    # do something
import threading
+
+lock = threading.lock()
+
+with lock:
+    # do something

生产者消费者模型

python
import requests
+from bs4 import BeautifulSoup
+
+urls = [f'https://www.cnblogs.com/#p{i}' for i in range(1, 50)]
+
+
+def craw(url):
+    r = requests.get(url)
+    return r.text
+
+
+def parse(html):
+    soup = BeautifulSoup(html, 'html.parser')
+    links = soup.find_all('a', class_='post-item-title')
+    return [(link.get('href'), link.get_text()) for link in links]
import requests
+from bs4 import BeautifulSoup
+
+urls = [f'https://www.cnblogs.com/#p{i}' for i in range(1, 50)]
+
+
+def craw(url):
+    r = requests.get(url)
+    return r.text
+
+
+def parse(html):
+    soup = BeautifulSoup(html, 'html.parser')
+    links = soup.find_all('a', class_='post-item-title')
+    return [(link.get('href'), link.get_text()) for link in links]
python
import queue
+import threading
+import time
+import random
+
+import blog_spider
+
+
+def do_crawl(url_queue: queue.Queue, html_queue: queue.Queue):
+    while True:
+        url = url_queue.get()
+        html = blog_spider.craw(url)
+        html_queue.put(html)
+
+
+def do_parse(html_queue: queue.Queue, fout):
+    while True:
+        html = html_queue.get()
+        results = blog_spider.parse(html)
+        for result in results:
+            fout.write(str(result) + '\\n')
+        time.sleep(1)
+
+
+if __name__ == '__main__':
+    url_queue = queue.Queue()
+    html_queue = queue.Queue()
+    for url in blog_spider.urls:
+        url_queue.put(url)
+
+    for i in range(3):
+        t = threading.Thread(target=do_crawl, args=(url_queue, html_queue), name='crawl-{}'.format(i))
+        t.start()
+
+    fout = open('results.txt', 'w')
+    for i in range(3):
+        t = threading.Thread(target=do_parse, args=(html_queue, fout), name='parse-{}'.format(i))
+        t.start()
import queue
+import threading
+import time
+import random
+
+import blog_spider
+
+
+def do_crawl(url_queue: queue.Queue, html_queue: queue.Queue):
+    while True:
+        url = url_queue.get()
+        html = blog_spider.craw(url)
+        html_queue.put(html)
+
+
+def do_parse(html_queue: queue.Queue, fout):
+    while True:
+        html = html_queue.get()
+        results = blog_spider.parse(html)
+        for result in results:
+            fout.write(str(result) + '\\n')
+        time.sleep(1)
+
+
+if __name__ == '__main__':
+    url_queue = queue.Queue()
+    html_queue = queue.Queue()
+    for url in blog_spider.urls:
+        url_queue.put(url)
+
+    for i in range(3):
+        t = threading.Thread(target=do_crawl, args=(url_queue, html_queue), name='crawl-{}'.format(i))
+        t.start()
+
+    fout = open('results.txt', 'w')
+    for i in range(3):
+        t = threading.Thread(target=do_parse, args=(html_queue, fout), name='parse-{}'.format(i))
+        t.start()

线程优先级队列实现

python
import queue
+import threading
+import time
+
+exitFlag = 0
+
+
+class MyThread(threading.Thread):
+    def __init__(self, thread_id, name, q):
+        super().__init__(name=name)
+        self.threadId = thread_id
+        self.name = name
+        self.q = q
+
+    def run(self):
+        print("开启线程: " + self.name)
+        process_data(self.name, self.q)
+        print("退出线程: " + self.name)
+
+
+def process_data(name, q):
+    while not exitFlag:
+        queueLock.acquire()
+        if not workQueue.empty():
+            data = q.get()
+            queueLock.release()
+            print(f"{name} processing {data}")
+        else:
+            queueLock.release()
+        time.sleep(1)
+
+
+threadList = ["Thread-1", "Thread-2", "Thread-3"]
+queueLock = threading.Lock()
+nameList = ["ONE", "TWO", "THREE", "FOUR", "FIVE"]
+workQueue = queue.Queue(10)
+threads = []
+threadId = 1
+
+for tname in threadList:
+    thread = MyThread(threadId, tname, workQueue)
+    thread.start()
+    threads.append(thread)
+    threadId += 1
+
+queueLock.acquire()
+for name in nameList:
+    workQueue.put(name)
+queueLock.release()
+
+while not workQueue.empty():
+    pass
+
+exitFlag = 1
+
+for t in threads:
+    t.join()
+
+print("主线程退出")
import queue
+import threading
+import time
+
+exitFlag = 0
+
+
+class MyThread(threading.Thread):
+    def __init__(self, thread_id, name, q):
+        super().__init__(name=name)
+        self.threadId = thread_id
+        self.name = name
+        self.q = q
+
+    def run(self):
+        print("开启线程: " + self.name)
+        process_data(self.name, self.q)
+        print("退出线程: " + self.name)
+
+
+def process_data(name, q):
+    while not exitFlag:
+        queueLock.acquire()
+        if not workQueue.empty():
+            data = q.get()
+            queueLock.release()
+            print(f"{name} processing {data}")
+        else:
+            queueLock.release()
+        time.sleep(1)
+
+
+threadList = ["Thread-1", "Thread-2", "Thread-3"]
+queueLock = threading.Lock()
+nameList = ["ONE", "TWO", "THREE", "FOUR", "FIVE"]
+workQueue = queue.Queue(10)
+threads = []
+threadId = 1
+
+for tname in threadList:
+    thread = MyThread(threadId, tname, workQueue)
+    thread.start()
+    threads.append(thread)
+    threadId += 1
+
+queueLock.acquire()
+for name in nameList:
+    workQueue.put(name)
+queueLock.release()
+
+while not workQueue.empty():
+    pass
+
+exitFlag = 1
+
+for t in threads:
+    t.join()
+
+print("主线程退出")

运行结果:

bash
开启线程: Thread-1
+开启线程: Thread-2
+开启线程: Thread-3
+Thread-1 processing ONE
+Thread-2 processing TWO
+Thread-3 processing THREE
+Thread-2 processing FOUR
+Thread-1 processing FIVE
+退出线程: Thread-2
+退出线程: Thread-1
+退出线程: Thread-3
+主线程退出
开启线程: Thread-1
+开启线程: Thread-2
+开启线程: Thread-3
+Thread-1 processing ONE
+Thread-2 processing TWO
+Thread-3 processing THREE
+Thread-2 processing FOUR
+Thread-1 processing FIVE
+退出线程: Thread-2
+退出线程: Thread-1
+退出线程: Thread-3
+主线程退出

多线程线程池ThreadPoolExecutor

python
from concurrent.futures import ThreadPoolExecutor, as_completed
+import time
+
+
+def get_data(times):
+    time.sleep(times)
+    print("get data {} success".format(times))
+
+
+thread_pool = ThreadPoolExecutor(max_workers=2)
+task1 = thread_pool.submit(get_data, 3)
+task2 = thread_pool.submit(get_data, 2)
+
+datas = [1, 2, 3]
+# submit后直接返回
+all_tasks = [thread_pool.submit(get_data, data) for data in datas]
+# as_complete底层是生成器
+# for future in as_completed(all_tasks):
+#    res = future.result()
+#    print(res)
+for data in thread_pool.map(get_data, datas):
+    print("get {} data ".format(data))
from concurrent.futures import ThreadPoolExecutor, as_completed
+import time
+
+
+def get_data(times):
+    time.sleep(times)
+    print("get data {} success".format(times))
+
+
+thread_pool = ThreadPoolExecutor(max_workers=2)
+task1 = thread_pool.submit(get_data, 3)
+task2 = thread_pool.submit(get_data, 2)
+
+datas = [1, 2, 3]
+# submit后直接返回
+all_tasks = [thread_pool.submit(get_data, data) for data in datas]
+# as_complete底层是生成器
+# for future in as_completed(all_tasks):
+#    res = future.result()
+#    print(res)
+for data in thread_pool.map(get_data, datas):
+    print("get {} data ".format(data))

ThreadPoolExecutor提交任务的两种方式

  • pool.map(func, params):func为处理函数,params为所有待处理的数据,返回值为按顺序返回。这种方式适合任务数据全部准备好一次提交处理的场景
  • future = pool.submit(func,param):func为处理函数,param为待处理的一条数据,返回值为future。这种方式适合一条条数据提交处理的场景。处理多个future集合futures时,可以直接遍历,也可以配合as_complete使用,这种方式是按任务完成顺序返回。

多进程

对于io操作来说,使用多线程

对于耗cpu的操作,用多进程

  • 进程的切换代价高于多线程
python
from concurrent.futures import ProcessPoolExecutor
+import multiprocessing
+import time
+
+
+# 多进程编程
+def get_html(n):
+    time.sleep(n)
+    return n
+
+
+if __name__ == '__main__':
+    # progress = multiprocessing.Process(target=get_html, args=(2,))
+    # print(progress.pid)
+    # progress.start()
+    # print(progress.pid)
+    # progress.join()
+    # print('main progress end')
+
+    # 使用进程池
+    pool = multiprocessing.Pool(multiprocessing.cpu_count())
+    # res = pool.apply_async(get_html, args=(3,))
+    # 不再接受任务
+    # pool.close()
+    # 等待所有任务完成
+    # pool.join()
+    # print(res)
+    # print(res.get())
+
+    # imap 按顺序
+    # for res in pool.imap(get_html, [1, 5, 3]):
+    #    print("{} sleep success".format(res))
+    # imap_unordered 按完成时间
+    for res in pool.imap_unordered(get_html, [1, 5, 3]):
+        print("{} sleep success".format(res))
from concurrent.futures import ProcessPoolExecutor
+import multiprocessing
+import time
+
+
+# 多进程编程
+def get_html(n):
+    time.sleep(n)
+    return n
+
+
+if __name__ == '__main__':
+    # progress = multiprocessing.Process(target=get_html, args=(2,))
+    # print(progress.pid)
+    # progress.start()
+    # print(progress.pid)
+    # progress.join()
+    # print('main progress end')
+
+    # 使用进程池
+    pool = multiprocessing.Pool(multiprocessing.cpu_count())
+    # res = pool.apply_async(get_html, args=(3,))
+    # 不再接受任务
+    # pool.close()
+    # 等待所有任务完成
+    # pool.join()
+    # print(res)
+    # print(res.get())
+
+    # imap 按顺序
+    # for res in pool.imap(get_html, [1, 5, 3]):
+    #    print("{} sleep success".format(res))
+    # imap_unordered 按完成时间
+    for res in pool.imap_unordered(get_html, [1, 5, 3]):
+        print("{} sleep success".format(res))

进程间通信

  • 使用multiprocessing中的Queue 用法和threading的Queue类似
  • 全局共享变量不适用与进程间通信(进程间的数据是隔离的)
  • multiprocessing中的Queue不能用于进程池pool中的进程通信
  • pool中的进程间通信需要使用multiprocessing中的Manager实例化后的queue(Manager().Queue())
  • 使用Pipe管道实现进程间通信 receive,send = Pipe() 只能适用于两个进程间通信
  • Manager().dict()等数据结构进行进程间通信

image-20220511013302618

协程

协程,又称微线程,纤程。英文名Coroutine。是一种用户态的上下文切换技术。协程的作用是在执行函数A时可以随时中断去执行函数B,然后中断函数B继续执行函数A(可以自由切换)。但这一过程并不是函数调用,这一整个过程看似像多线程,然而协程只有一个线程执行。

协程的优势

  • 效率极高,因为子程序切换不是线程切换,由程序自身控制,没有切换线程的开销,所以与多线程相比,线程的数量越多,协程的性能优势越明显。
  • 不需要多线程的同步机制,因为只有一个线程,也不存在同时写变量的线程安全问题,在控制共享资源时也不需要加锁,因此执行效率高很多。

协程可以处理IO密集型程序的效率问题,但是CPU密集型不是它的长处,要充分发挥CPU的利用率可以结合多进程+协程

实现协程的方式:

  • yield关键字
  • asyncio装饰器
  • async、await关键字(推荐)

事件循环

asyncio模块中,每一个进程都有一个事件循环。把一些函数注册到事件循环上,当满足事件发生的时候,调用相应的协程函数

事件循环的作用是管理所有的事件,在整个程序运行过程中不断循环执行,追踪事件发生的顺序将它们放到队列中,当主线程空闲的时候,调用相应的事件处理者处理事件。

伪代码:

python
任务列表 = [任务1,任务2,任务3...]
+
+while true:
+    可执行的任务列表,已完成的任务列表 = 检查所有任务,将可执行的和已完成的任务返回
+    for 就绪任务 in 可执行的任务:
+        执行就绪任务
+        
+    for 已完成的任务 in 已完成的任务:
+        剔除已完成的任务
+        
+    如果任务列表的全部任务都已完成,终止循环
任务列表 = [任务1,任务2,任务3...]
+
+while true:
+    可执行的任务列表,已完成的任务列表 = 检查所有任务,将可执行的和已完成的任务返回
+    for 就绪任务 in 可执行的任务:
+        执行就绪任务
+        
+    for 已完成的任务 in 已完成的任务:
+        剔除已完成的任务
+        
+    如果任务列表的全部任务都已完成,终止循环
python
import asyncio
+
+
+# 生成或获取一个事件循环
+loop = asyncio.get_event_loop()
+# 将任务放到任务列表
+loop.run_until_complete(任务)
import asyncio
+
+
+# 生成或获取一个事件循环
+loop = asyncio.get_event_loop()
+# 将任务放到任务列表
+loop.run_until_complete(任务)

image-20220511014404071

协程函数

定义函数时,如果是async def 函数的函数,就是一个协程函数

协程对象

执行协程函数得到的对象

TIP

执行协程函数创建协程对象,函数内部代码不会立即执行

如果想运行协程函数内部代码,必须将协程对象交给事件循环处理

python
import asyncio
+
+
+# 定义一个协程函数
+async def func():
+    print("异步编程")
+
+
+# 生成一个事件循环
+loop = asyncio.get_event_loop()
+# 得到协程对象
+res = func()
+# 将协程对象交给事件循环
+loop.run_until_complete(res)
+# asyncio.run(res)
+
+res:
+异步编程
import asyncio
+
+
+# 定义一个协程函数
+async def func():
+    print("异步编程")
+
+
+# 生成一个事件循环
+loop = asyncio.get_event_loop()
+# 得到协程对象
+res = func()
+# 将协程对象交给事件循环
+loop.run_until_complete(res)
+# asyncio.run(res)
+
+res:
+异步编程

如果不把协程对象放入事件循环

python
import asyncio
+
+
+# 定义一个协程函数
+async def func():
+    print("异步编程")
+
+
+# 生成一个事件循环
+loop = asyncio.get_event_loop()
+# 得到协程对象
+res = func()
+
+res:
+sys:1: RuntimeWarning: coroutine 'func' was never awaited
import asyncio
+
+
+# 定义一个协程函数
+async def func():
+    print("异步编程")
+
+
+# 生成一个事件循环
+loop = asyncio.get_event_loop()
+# 得到协程对象
+res = func()
+
+res:
+sys:1: RuntimeWarning: coroutine 'func' was never awaited

异步IO

python
import asyncio
+# 获取事件循环
+loop = asyncio.get_event_loop()
+
+# 定义协程函数
+async def hello(count):
+    print(f"Hello World! {count}")
+    await asyncio.sleep(1)
+
+# 创建task列表
+tasks = [loop.create_task(hello(count)) for count in range(10)]
+# 执行事件列表
+loop.run_until_complete(asyncio.wait(tasks))
import asyncio
+# 获取事件循环
+loop = asyncio.get_event_loop()
+
+# 定义协程函数
+async def hello(count):
+    print(f"Hello World! {count}")
+    await asyncio.sleep(1)
+
+# 创建task列表
+tasks = [loop.create_task(hello(count)) for count in range(10)]
+# 执行事件列表
+loop.run_until_complete(asyncio.wait(tasks))

异步IO爬虫

python
import asyncio
+import aiohttp
+import blog_spider
+import time
+
+async def async_craw(url):
+    print('开始爬取:', url)
+    async with aiohttp.ClientSession() as session:
+        async with session.get(url) as response:
+            result = await response.text()
+            print('爬取结束:', url, len(result))
+
+
+loop = asyncio.get_event_loop()
+
+tasks = [loop.create_task(async_craw(url)) for url in blog_spider.urls]
+
+start = time.time()
+loop.run_until_complete(asyncio.wait(tasks))
+print('耗时:', time.time() - start)
+
+---
import asyncio
+import aiohttp
+import blog_spider
+import time
+
+async def async_craw(url):
+    print('开始爬取:', url)
+    async with aiohttp.ClientSession() as session:
+        async with session.get(url) as response:
+            result = await response.text()
+            print('爬取结束:', url, len(result))
+
+
+loop = asyncio.get_event_loop()
+
+tasks = [loop.create_task(async_craw(url)) for url in blog_spider.urls]
+
+start = time.time()
+loop.run_until_complete(asyncio.wait(tasks))
+print('耗时:', time.time() - start)
+
+---

使用信号量控制异步爬虫并发度

python
sem = asyncio.Semaphore(10)
+
+async with sem:
+    # work with shared resource
+-----------------------------------
+
+sem = asyncio.Semaphore(10)
+
+await sem.acquire()
+try:
+    # work with shared resource
+finally:
+    sem.release()
sem = asyncio.Semaphore(10)
+
+async with sem:
+    # work with shared resource
+-----------------------------------
+
+sem = asyncio.Semaphore(10)
+
+await sem.acquire()
+try:
+    # work with shared resource
+finally:
+    sem.release()
python
import asyncio
+import aiohttp
+import blog_spider
+
+sem = asyncio.Semaphore(10)
+
+async def async_craw(url):
+    print('开始爬取:', url)
+    async with sem:
+        async with aiohttp.ClientSession() as session:
+            async with session.get(url) as response:
+                result = await response.text()
+                print('爬取结束:', url, len(result))
+
+
+loop = asyncio.get_event_loop()
+
+tasks = [loop.create_task(async_craw(url)) for url in blog_spider.urls]
+import time
+start = time.time()
+loop.run_until_complete(asyncio.wait(tasks))
+print('耗时:', time.time() - start)
import asyncio
+import aiohttp
+import blog_spider
+
+sem = asyncio.Semaphore(10)
+
+async def async_craw(url):
+    print('开始爬取:', url)
+    async with sem:
+        async with aiohttp.ClientSession() as session:
+            async with session.get(url) as response:
+                result = await response.text()
+                print('爬取结束:', url, len(result))
+
+
+loop = asyncio.get_event_loop()
+
+tasks = [loop.create_task(async_craw(url)) for url in blog_spider.urls]
+import time
+start = time.time()
+loop.run_until_complete(asyncio.wait(tasks))
+print('耗时:', time.time() - start)

python3.7后的新语法

使用asyncio.run()代替原来创建事件循环,使用事件循环执行函数的操作

python
import asyncio
+import blog_spider
+
+# async def 定义协程函数
+async def async_craw(url):
+    print('开始爬取:', url)
+    # 触发io操作,调用其他协程
+    await asyncio.sleep(0)
+    print('爬取完成:', url)
+
+
+async def main():
+    # 创建协程列表
+    tasks = [async_craw(url) for url in blog_spider.urls]
+    # asyncio.gather(*task)表示协同执行tasks列表里的所有协程
+    await asyncio.gather(*tasks)
+#
+asyncio.run(main())
import asyncio
+import blog_spider
+
+# async def 定义协程函数
+async def async_craw(url):
+    print('开始爬取:', url)
+    # 触发io操作,调用其他协程
+    await asyncio.sleep(0)
+    print('爬取完成:', url)
+
+
+async def main():
+    # 创建协程列表
+    tasks = [async_craw(url) for url in blog_spider.urls]
+    # asyncio.gather(*task)表示协同执行tasks列表里的所有协程
+    await asyncio.gather(*tasks)
+#
+asyncio.run(main())

asyncio.wait和asyncio.gather异同

  • 相同:从功能上看,asyncio.waitasyncio.gather 实现的效果是相同的,都是把所有 Task 任务结果收集起来。
  • 不同asyncio.wait 会返回两个值:donependingdone 为已完成的协程 Taskpending 为超时未完成的协程 Task,需通过 future.result 调用 Taskresult;而asyncio.gather 返回的是所有已完成 Taskresult,不需要再进行调用或其他操作,就可以得到全部结果。

await关键字

await + 可等待的对象(协程对象、Future对象、Task对象 -> io等待)

python
import asyncio
+
+
+async def func():
+    print('异步编程')
+    response = await asyncio.sleep(2)
+    print("结束",response)
+    
+asyncio.run(func())
import asyncio
+
+
+async def func():
+    print('异步编程')
+    response = await asyncio.sleep(2)
+    print("结束",response)
+    
+asyncio.run(func())

示例:

python
import asyncio
+
+
+async def others():
+    print('start')
+    await asyncio.sleep(2)
+    print('end')
+    return '返回值'
+
+
+async def func():
+    print('执行协程函数内部代码')
+    # 遇到IO操作挂起当前协程,等到IO完成后继续运行,当前协程挂起时,事件循环可以执行其他协程
+    response = await others()
+    print(f'IO的结果是:{response} ')
+
+asyncio.run(func())
+
+res:
+执行协程函数内部代码
+start
+end
+IO的结果是:返回值
import asyncio
+
+
+async def others():
+    print('start')
+    await asyncio.sleep(2)
+    print('end')
+    return '返回值'
+
+
+async def func():
+    print('执行协程函数内部代码')
+    # 遇到IO操作挂起当前协程,等到IO完成后继续运行,当前协程挂起时,事件循环可以执行其他协程
+    response = await others()
+    print(f'IO的结果是:{response} ')
+
+asyncio.run(func())
+
+res:
+执行协程函数内部代码
+start
+end
+IO的结果是:返回值

Task对象

Tasks用于并发调度协程,是对协程对象的一种封装,其中包含了任务的各个状态。通过asyncio.create_task()函数创建Task对象,这样可以让协程加入事件循环中等待调度执行。还可以使用低层级的loop.create_task()asyncio.ensure_future()函数。不建议手动实例化Task对象。

示例1:

python
import asyncio
+
+
+async def func():
+    print(1)
+    await asyncio.sleep(2)
+    print(2)
+    return '返回值'
+
+
+async def main():
+    print('main函数开始')
+
+    # 创建task对象,将当前执行func函数的任务添加到事件循环
+    task1 = asyncio.create_task(func())
+
+    task2 = asyncio.create_task(func())
+
+    print('main函数结束')
+
+    # 当执行某协程遇到IO操作,会自动切换执行其他任务
+    res1 = await task1
+    res2 = await task2
+    print(res1, res2)
+
+
+asyncio.run(main())
+
+res:
+main函数开始
+main函数结束
+1
+1
+2
+2
+返回值 返回值
import asyncio
+
+
+async def func():
+    print(1)
+    await asyncio.sleep(2)
+    print(2)
+    return '返回值'
+
+
+async def main():
+    print('main函数开始')
+
+    # 创建task对象,将当前执行func函数的任务添加到事件循环
+    task1 = asyncio.create_task(func())
+
+    task2 = asyncio.create_task(func())
+
+    print('main函数结束')
+
+    # 当执行某协程遇到IO操作,会自动切换执行其他任务
+    res1 = await task1
+    res2 = await task2
+    print(res1, res2)
+
+
+asyncio.run(main())
+
+res:
+main函数开始
+main函数结束
+1
+1
+2
+2
+返回值 返回值

示例2:

python
import asyncio
+
+
+async def func():
+    print(1)
+    await asyncio.sleep(2)
+    print(2)
+    return '返回值'
+
+
+async def main():
+    print('main函数开始')
+
+    
+    task_list = [
+       asyncio.create_task(func()),
+       asyncio.create_task(func())
+    ]
+    print('main函数结束')
+    done,pending = await asyncio.wait(task_list,timeout=None)
+    print(done)
+asyncio.run(main())
import asyncio
+
+
+async def func():
+    print(1)
+    await asyncio.sleep(2)
+    print(2)
+    return '返回值'
+
+
+async def main():
+    print('main函数开始')
+
+    
+    task_list = [
+       asyncio.create_task(func()),
+       asyncio.create_task(func())
+    ]
+    print('main函数结束')
+    done,pending = await asyncio.wait(task_list,timeout=None)
+    print(done)
+asyncio.run(main())

asyncio.Future对象

Task继承了Future,Task对象内部await的结果的处理基于Future对象

python
async def main():
+    loop = asyncio.get_running_loop()
+    _future = loop.create_future()
+    await _future
+asyncio.run(main())
async def main():
+    loop = asyncio.get_running_loop()
+    _future = loop.create_future()
+    await _future
+asyncio.run(main())

concurrent.futures.Future对象

使用线程池/进程池实现异步操作时用到的对象

python
import time
+from concurrent.futures import Future
+from concurrent.futures.thread import ThreadPoolExecutor
+
+def func(value):
+    time.sleep(1)
+    print(value)
+    return 123
+
+pool = ThreadPoolExecutory(max_workers=5)
+for i in range(5)
+	fut = pool.submit(func,1)
+    print(fut)
import time
+from concurrent.futures import Future
+from concurrent.futures.thread import ThreadPoolExecutor
+
+def func(value):
+    time.sleep(1)
+    print(value)
+    return 123
+
+pool = ThreadPoolExecutory(max_workers=5)
+for i in range(5)
+	fut = pool.submit(func,1)
+    print(fut)
`,100),e=[o];function c(t,r,E,y,i,F){return n(),a("div",null,e)}const h=s(p,[["render",c]]);export{d as __pageData,h as default}; diff --git "a/assets/python_base_python\345\271\266\345\217\221\347\274\226\347\250\213.md.d5a5de0a.lean.js" "b/assets/python_base_python\345\271\266\345\217\221\347\274\226\347\250\213.md.d5a5de0a.lean.js" new file mode 100644 index 000000000..ce3fb6e1e --- /dev/null +++ "b/assets/python_base_python\345\271\266\345\217\221\347\274\226\347\250\213.md.d5a5de0a.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"python并发编程","description":"","frontmatter":{},"headers":[],"relativePath":"python/base/python并发编程.md","filePath":"python/base/python并发编程.md","lastUpdated":1708510102000}'),p={name:"python/base/python并发编程.md"},o=l("",100),e=[o];function c(t,r,E,y,i,F){return n(),a("div",null,e)}const h=s(p,[["render",c]]);export{d as __pageData,h as default}; diff --git "a/assets/python_base_\350\243\205\351\245\260\345\231\250\346\267\261\345\205\245.md.6df2bfe7.js" "b/assets/python_base_\350\243\205\351\245\260\345\231\250\346\267\261\345\205\245.md.6df2bfe7.js" new file mode 100644 index 000000000..9dceb3f58 --- /dev/null +++ "b/assets/python_base_\350\243\205\351\245\260\345\231\250\346\267\261\345\205\245.md.6df2bfe7.js" @@ -0,0 +1,477 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const C=JSON.parse('{"title":"装饰器深入","description":"","frontmatter":{},"headers":[],"relativePath":"python/base/装饰器深入.md","filePath":"python/base/装饰器深入.md","lastUpdated":1708510102000}'),p={name:"python/base/装饰器深入.md"},o=l(`

装饰器深入

闭包

闭包的定义

如果在一个外部函数中定义一个内部函数,内部函数对外部作用域(但不是在全局作用域)的变量进行引用,外部函数的返回值是内部函数,这样的函数就被认为是闭包(closure)。

形成闭包的条件

  • 必须有内部函数
  • 内部函数必须引用外部函数的变量
  • 外部函数返回值必须是内部函数

装饰器

python装饰器实质上也是一个闭包函数,目的是在不改变原函数的情况下实现对原函数功能的增强。(类似Spring中的AOP)

装饰器的条件

  • 不修改已有函数代码
  • 不修改已有函数的调用方式
  • 给已有函数增加额外功能

装饰器语法糖

@装饰器函数名

@装饰器类名

@装饰器函数名(param)

@装饰器类名(param)

函数的函数装饰器(函数作为函数的装饰器)

不带参数的装饰器函数

这里的不带参数是指@装饰器后没有(参数)而非装饰器函数没有参数

例如下面的record函数就是一个简单装饰器,作用是记录被装饰函数的执行耗时

python
import time
+
+
+def record(func):
+    def decorator(*args, **kwargs):
+        print('====start====')
+        start = time.time()
+        func(*args, **kwargs)
+        print(f'===end cost : {time.time() - start} seconds===')
+
+    return decorator
+
+
+@record # 不带参数
+def test(name, age):
+    time.sleep(1)
+    print(f'my name is {name}, {age} years old')
+
+
+test('tom', 18)
+
+
+
+====start====
+my name is tom, 18 years old
+===end cost : 1.0049748420715332 seconds===
import time
+
+
+def record(func):
+    def decorator(*args, **kwargs):
+        print('====start====')
+        start = time.time()
+        func(*args, **kwargs)
+        print(f'===end cost : {time.time() - start} seconds===')
+
+    return decorator
+
+
+@record # 不带参数
+def test(name, age):
+    time.sleep(1)
+    print(f'my name is {name}, {age} years old')
+
+
+test('tom', 18)
+
+
+
+====start====
+my name is tom, 18 years old
+===end cost : 1.0049748420715332 seconds===

本质

在上述不带参数的装饰器函数例子中,14行@record实质上等于test = record(test),最终调用test('tom',18)的伪代码:

python
print('====start====')
+start = time.time()
+
+# func(*args, **kwargs) 
+time.sleep(1) # -> 原始的test('tom',18)
+print(f'my name is {name}, {age} years old') # -> 原始的test('tom',18)
+
+print(f'===end cost : {time.time() - start} seconds===')
print('====start====')
+start = time.time()
+
+# func(*args, **kwargs) 
+time.sleep(1) # -> 原始的test('tom',18)
+print(f'my name is {name}, {age} years old') # -> 原始的test('tom',18)
+
+print(f'===end cost : {time.time() - start} seconds===')

带参数的装饰器函数

python中一切皆对象,如果在对象后跟()即是执行调用的意思,例如函数,类,类里的函数,实现了__call__方法的对象都可以被调用,因为这些对象是callable对象。还是刚才的例子,@装饰器(参数)语法,实际上是在不带参数的装饰器函数基础上包了一层,由test = record(test)变成了decorator = record(count); test = decorator(test)

例子如下

python
import time
+
+def record(count):
+    def decorator(func):
+        def wrapper(*args, **kwargs):
+            print('====start====')
+            start = time.time()
+            for _num in range(count):
+                func(*args, **kwargs)
+            print(f'===end cost : {time.time() - start} seconds===')
+        return wrapper
+
+    return decorator
+
+
+@record(5)
+def test(name, age):
+    time.sleep(1)
+    print(f'my name is {name}, {age} years old')
+
+
+test('tom', 18)
+
+---
+====start====
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+===end cost : 5.020708799362183 seconds===
import time
+
+def record(count):
+    def decorator(func):
+        def wrapper(*args, **kwargs):
+            print('====start====')
+            start = time.time()
+            for _num in range(count):
+                func(*args, **kwargs)
+            print(f'===end cost : {time.time() - start} seconds===')
+        return wrapper
+
+    return decorator
+
+
+@record(5)
+def test(name, age):
+    time.sleep(1)
+    print(f'my name is {name}, {age} years old')
+
+
+test('tom', 18)
+
+---
+====start====
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+===end cost : 5.020708799362183 seconds===

本质

带参数的装饰器,实际就是加了一层函数的嵌套,可以把这种装饰器拆成两步分析,第一步执行record(5)返回了函数decorator,@decorator这样就是不带参数的装饰器形式了。

注意:装饰器返回的是一个全新的函数

装饰器返回的是一个全新的函数,对函数的装饰方法(常写成wrapper)的参数列表为了兼容性可以写为(*args, **kwargs),但是这个函数的参数实际可以写为任意形式(只要该参数包含被装饰函数的参数列表即可),回归到定义上来说就是不修改已有函数的调用方式即可。网上教程中通常把wrapper的参数写成和被装饰函数一致,很容易让人误以为这两者的参数列表必须保持一致。

例子:

python
import time
+from dataclasses import dataclass
+
+
+@dataclass
+class User(object):
+    name: str
+    info: tuple = ()
+
+
+def record(count):
+    def decorator(func):
+        def wrapper(user):
+            print('====start====')
+            start = time.time()
+            for _num in range(count):
+                func(*user.info)
+
+            print(f'===end cost : {time.time() - start} seconds===')
+        return wrapper
+
+    return decorator
+
+
+@record(5)
+def test(name, age):
+    time.sleep(1)
+    print(f'my name is {name}, {age} years old')
+
+
+# test('tom', 18)
+print(test.__name__)
+tom = User('tom', info=('tom', 18))
+test(tom)
+
+---
+
+wrapper
+====start====
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+===end cost : 5.013930797576904 seconds===
import time
+from dataclasses import dataclass
+
+
+@dataclass
+class User(object):
+    name: str
+    info: tuple = ()
+
+
+def record(count):
+    def decorator(func):
+        def wrapper(user):
+            print('====start====')
+            start = time.time()
+            for _num in range(count):
+                func(*user.info)
+
+            print(f'===end cost : {time.time() - start} seconds===')
+        return wrapper
+
+    return decorator
+
+
+@record(5)
+def test(name, age):
+    time.sleep(1)
+    print(f'my name is {name}, {age} years old')
+
+
+# test('tom', 18)
+print(test.__name__)
+tom = User('tom', info=('tom', 18))
+test(tom)
+
+---
+
+wrapper
+====start====
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+===end cost : 5.013930797576904 seconds===

可以看到,test函数的name为wrapper,也就是装饰功能的函数的名字,而且这里test函数的参数列表也已经变成了(user),也就是这里实际上test = wrapper(user)。如果使用装饰器后,想保留原函数的名称,可以使用@functools.wraps来装饰wrapper函数

例子:

python
import functools
+import time
+from dataclasses import dataclass
+
+
+@dataclass
+class User(object):
+    name: str
+    info: tuple = ()
+
+
+def record(count):
+    def decorator(func):
+        @functools.wraps(func)
+        def wrapper(user):
+            print('====start====')
+            start = time.time()
+            for _num in range(count):
+                func(*user.info)
+
+            print(f'===end cost : {time.time() - start} seconds===')
+        return wrapper
+
+    return decorator
+
+
+@record(5)
+def test(name, age):
+    time.sleep(1)
+    print(f'my name is {name}, {age} years old')
+
+
+# test('tom', 18)
+print(test.__name__)
+tom = User('tom', info=('tom', 18))
+test(tom)
+
+---
+
+test
+====start====
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+===end cost : 5.015993118286133 seconds===
import functools
+import time
+from dataclasses import dataclass
+
+
+@dataclass
+class User(object):
+    name: str
+    info: tuple = ()
+
+
+def record(count):
+    def decorator(func):
+        @functools.wraps(func)
+        def wrapper(user):
+            print('====start====')
+            start = time.time()
+            for _num in range(count):
+                func(*user.info)
+
+            print(f'===end cost : {time.time() - start} seconds===')
+        return wrapper
+
+    return decorator
+
+
+@record(5)
+def test(name, age):
+    time.sleep(1)
+    print(f'my name is {name}, {age} years old')
+
+
+# test('tom', 18)
+print(test.__name__)
+tom = User('tom', info=('tom', 18))
+test(tom)
+
+---
+
+test
+====start====
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+===end cost : 5.015993118286133 seconds===

可以看到,在对test进行了装饰后,返回的新的函数名称还是保持为test

函数的类装饰器(类作为函数的装饰器)

不带参数的装饰器类

类也可以作为装饰器使用,需要实现__init__函数和__call__函数,例子:

python
import time
+
+
+class Timer(object):
+
+    def __init__(self, func):
+        self.func = func
+
+    def __call__(self, *args, **kwargs):
+        start = time.time()
+        ret = self.func(*args, **kwargs)
+        print(f'Time : {time.time() - start}')
+        return ret
+
+
+@Timer
+def add(a, b):
+    return a + b
+
+# 等价于 add = Timer(add)
+  
+print(add(2, 3))
import time
+
+
+class Timer(object):
+
+    def __init__(self, func):
+        self.func = func
+
+    def __call__(self, *args, **kwargs):
+        start = time.time()
+        ret = self.func(*args, **kwargs)
+        print(f'Time : {time.time() - start}')
+        return ret
+
+
+@Timer
+def add(a, b):
+    return a + b
+
+# 等价于 add = Timer(add)
+  
+print(add(2, 3))

带参数的装饰器类

类似带参数的装饰器函数,带参数的装饰器类需要在__call__函数内部,再包一层

python
import time
+
+
+class Timer(object):
+
+    def __init__(self, pre_fix):
+        self.pre_fix = pre_fix
+
+    def __call__(self, func):
+        def wrapper(*args, **kwargs):
+            start = time.time()
+            ret = func(*args, **kwargs)
+            print(f'{self.pre_fix}: {time.time() - start}')
+            return ret
+
+        return wrapper
+
+
+@Timer(pre_fix='current_time')
+def add(a, b):
+    return a + b
+
+
+print(add(2, 3))
import time
+
+
+class Timer(object):
+
+    def __init__(self, pre_fix):
+        self.pre_fix = pre_fix
+
+    def __call__(self, func):
+        def wrapper(*args, **kwargs):
+            start = time.time()
+            ret = func(*args, **kwargs)
+            print(f'{self.pre_fix}: {time.time() - start}')
+            return ret
+
+        return wrapper
+
+
+@Timer(pre_fix='current_time')
+def add(a, b):
+    return a + b
+
+
+print(add(2, 3))

类的函数装饰器(函数作为类的装饰器)

不带参数

函数也可以装饰类,下面的例子中,add_str是一个参数为class,返回值也是class的函数,装饰了MyObj类,作用是把被装饰类的__str__函数替换为打印self.__dict__

python
def add_str(cls):
+    def __str__(self):
+        return str(self.__dict__)
+
+    cls.__str__ = __str__
+    return cls
+
+
+@add_str
+class MyObj(object):
+    def __init__(self, a, b):
+        self.a = a
+        self.b = b
+
+
+print(MyObj(1, 2))
+
+---
+
+{'a': 1, 'b': 2}
def add_str(cls):
+    def __str__(self):
+        return str(self.__dict__)
+
+    cls.__str__ = __str__
+    return cls
+
+
+@add_str
+class MyObj(object):
+    def __init__(self, a, b):
+        self.a = a
+        self.b = b
+
+
+print(MyObj(1, 2))
+
+---
+
+{'a': 1, 'b': 2}

带参数

python
def add_str(time):
+    def _cls(cls):
+        def __str__(self):
+            return f'调用时间 {time} 点 == ' + str(self.__dict__)
+
+        cls.__str__ = __str__
+
+        return cls
+
+    return _cls
+
+
+@add_str(time='19')
+class MyObj(object):
+    def __init__(self, a, b):
+        self.a = a
+        self.b = b
+
+
+print(MyObj(1, 2))
+
+---
+
+调用时间 19== {'a': 1, 'b': 2}
def add_str(time):
+    def _cls(cls):
+        def __str__(self):
+            return f'调用时间 {time} 点 == ' + str(self.__dict__)
+
+        cls.__str__ = __str__
+
+        return cls
+
+    return _cls
+
+
+@add_str(time='19')
+class MyObj(object):
+    def __init__(self, a, b):
+        self.a = a
+        self.b = b
+
+
+print(MyObj(1, 2))
+
+---
+
+调用时间 19== {'a': 1, 'b': 2}

类作为类的装饰器

没什么意义

`,52),e=[o];function c(t,r,y,E,i,F){return n(),a("div",null,e)}const m=s(p,[["render",c]]);export{C as __pageData,m as default}; diff --git "a/assets/python_base_\350\243\205\351\245\260\345\231\250\346\267\261\345\205\245.md.6df2bfe7.lean.js" "b/assets/python_base_\350\243\205\351\245\260\345\231\250\346\267\261\345\205\245.md.6df2bfe7.lean.js" new file mode 100644 index 000000000..50a54b40b --- /dev/null +++ "b/assets/python_base_\350\243\205\351\245\260\345\231\250\346\267\261\345\205\245.md.6df2bfe7.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const C=JSON.parse('{"title":"装饰器深入","description":"","frontmatter":{},"headers":[],"relativePath":"python/base/装饰器深入.md","filePath":"python/base/装饰器深入.md","lastUpdated":1708510102000}'),p={name:"python/base/装饰器深入.md"},o=l("",52),e=[o];function c(t,r,y,E,i,F){return n(),a("div",null,e)}const m=s(p,[["render",c]]);export{C as __pageData,m as default}; diff --git "a/assets/python_crawler_CrawlSpider\345\205\250\347\253\231\347\210\254\345\217\226.md.914da3d0.js" "b/assets/python_crawler_CrawlSpider\345\205\250\347\253\231\347\210\254\345\217\226.md.914da3d0.js" new file mode 100644 index 000000000..855a616be --- /dev/null +++ "b/assets/python_crawler_CrawlSpider\345\205\250\347\253\231\347\210\254\345\217\226.md.914da3d0.js" @@ -0,0 +1,229 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"CrawlSpider全站爬取","description":"","frontmatter":{},"headers":[],"relativePath":"python/crawler/CrawlSpider全站爬取.md","filePath":"python/crawler/CrawlSpider全站爬取.md","lastUpdated":1708510102000}'),p={name:"python/crawler/CrawlSpider全站爬取.md"},o=l(`

CrawlSpider全站爬取

CrawlSpider

CrawlSpider是Spider的一个子类,具有提取指定规则链接的功能

CrawlSpider的作用:

  • 全站爬取
    • 基于Spider手动请求
    • 基于CrawlSpider

项目创建

  • scrapy startproject crawl_spider
  • cd crawl_spider
  • 创建基于CrawlSpider的爬虫类:scrapy genspider -t crawl storyxc xxx.com 相比普通的增加了-t crawl参数

链接提取器

根据指定规则(allow=‘正则表达式“)提取符合要求的所有url

python
link = LinkExtractor(allow=r'id=1&page=\\d+')
link = LinkExtractor(allow=r'id=1&page=\\d+')

规则解析器

将链接提取器提取到的链接进行指定规则(callback)的解析

python
rules = (
+    Rule(link, callback='parse_item', follow=True),
+)
rules = (
+    Rule(link, callback='parse_item', follow=True),
+)
  • follow参数的作用:
    • True:可以将链接提取器继续作用到链接提取器提取到的链接(递归)
    • False:只提取起始页的数据

案例:提取东莞阳光问政平台的问政标题和编号

爬虫类

python
from scrapy.linkextractors import LinkExtractor
+from scrapy.spiders import CrawlSpider, Rule
+from crawl_spider.items import CrawlSpiderItem,DetailItem
+
+
+class StoryxcSpider(CrawlSpider):
+    name = 'storyxc'
+    start_urls = ['http://wz.sun0769.com/political/index/politicsNewest']
+
+    # 链接提取器,符合正则表达式的链接都会被提取
+    link = LinkExtractor(allow=r'id=1&page=\\d+')
+    detail_link = LinkExtractor(allow=r'\\/political\\/politics\\/index\\?id=\\d+')
+
+    rules = (
+        Rule(link, callback='parse_item', follow=True),
+        Rule(detail_link, callback='parse_detail'),
+    )
+
+    def parse_item(self, response):
+        li_list = response.xpath('/html/body/div[2]/div[3]/ul[2]/li')
+        for li in li_list:
+            wz_id = li.xpath('./span[1]/text()').extract_first()
+            wz_title = li.xpath('./span[3]/a/text()').extract_first()
+            item = CrawlSpiderItem()
+            item['num'] = wz_id
+            item['title'] = wz_title
+            yield item
+
+    def parse_detail(self, response):
+        id = response.xpath('/html/body/div[3]/div[2]/div[2]/div[1]/span[4]/text()').extract_first()
+        id = id.replace('编号:','')
+        content = ''.join(response.xpath('/html/body/div[3]/div[2]/div[2]/div[2]/pre/text()').extract())
+        item = DetailItem()
+        item['num'] = id
+        item['content'] = content
+        yield item
from scrapy.linkextractors import LinkExtractor
+from scrapy.spiders import CrawlSpider, Rule
+from crawl_spider.items import CrawlSpiderItem,DetailItem
+
+
+class StoryxcSpider(CrawlSpider):
+    name = 'storyxc'
+    start_urls = ['http://wz.sun0769.com/political/index/politicsNewest']
+
+    # 链接提取器,符合正则表达式的链接都会被提取
+    link = LinkExtractor(allow=r'id=1&page=\\d+')
+    detail_link = LinkExtractor(allow=r'\\/political\\/politics\\/index\\?id=\\d+')
+
+    rules = (
+        Rule(link, callback='parse_item', follow=True),
+        Rule(detail_link, callback='parse_detail'),
+    )
+
+    def parse_item(self, response):
+        li_list = response.xpath('/html/body/div[2]/div[3]/ul[2]/li')
+        for li in li_list:
+            wz_id = li.xpath('./span[1]/text()').extract_first()
+            wz_title = li.xpath('./span[3]/a/text()').extract_first()
+            item = CrawlSpiderItem()
+            item['num'] = wz_id
+            item['title'] = wz_title
+            yield item
+
+    def parse_detail(self, response):
+        id = response.xpath('/html/body/div[3]/div[2]/div[2]/div[1]/span[4]/text()').extract_first()
+        id = id.replace('编号:','')
+        content = ''.join(response.xpath('/html/body/div[3]/div[2]/div[2]/div[2]/pre/text()').extract())
+        item = DetailItem()
+        item['num'] = id
+        item['content'] = content
+        yield item

item类

python
# Define here the models for your scraped items
+#
+# See documentation in:
+# https://docs.scrapy.org/en/latest/topics/items.html
+
+import scrapy
+
+
+class CrawlSpiderItem(scrapy.Item):
+    # define the fields for your item here like:
+    # name = scrapy.Field()
+    title =scrapy.Field()
+    num = scrapy.Field()
+
+class DetailItem(scrapy.Item):
+    num = scrapy.Field()
+    content = scrapy.Field()
# Define here the models for your scraped items
+#
+# See documentation in:
+# https://docs.scrapy.org/en/latest/topics/items.html
+
+import scrapy
+
+
+class CrawlSpiderItem(scrapy.Item):
+    # define the fields for your item here like:
+    # name = scrapy.Field()
+    title =scrapy.Field()
+    num = scrapy.Field()
+
+class DetailItem(scrapy.Item):
+    num = scrapy.Field()
+    content = scrapy.Field()

Pipeline类

python
# Define your item pipelines here
+#
+# Don't forget to add your pipeline to the ITEM_PIPELINES setting
+# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
+
+
+# useful for handling different item types with a single interface
+from itemadapter import ItemAdapter
+from crawl_spider.items import CrawlSpiderItem, DetailItem
+import pymysql
+
+
+class CrawlSpiderPipeline:
+    def process_item(self, item, spider):
+        if item.__class__.__name__ == 'DetailItem':
+            with Mysql() as conn:
+                cursor = conn.cursor()
+                try:
+                    cursor.execute(
+                        'insert into tb_wz_content(id,content) values("%s","%s")' % (
+                            item['num'],item['content']))
+                    conn.commit()
+                except:
+                    print('插入问政内容失败!')
+                    conn.rollback()
+
+
+        else:
+            with Mysql() as conn:
+                cursor = conn.cursor()
+                try:
+                    cursor.execute(
+                        'insert into tb_wz_title(id,title) values("%s","%s")' % (item['num'],item['title']))
+                    conn.commit()
+                except:
+                    print('插入问政标题失败!')
+                    conn.rollback()
+
+
+class Mysql(object):
+    def __enter__(self):
+        self.connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+        return self.connection
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.connection.close()
# Define your item pipelines here
+#
+# Don't forget to add your pipeline to the ITEM_PIPELINES setting
+# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
+
+
+# useful for handling different item types with a single interface
+from itemadapter import ItemAdapter
+from crawl_spider.items import CrawlSpiderItem, DetailItem
+import pymysql
+
+
+class CrawlSpiderPipeline:
+    def process_item(self, item, spider):
+        if item.__class__.__name__ == 'DetailItem':
+            with Mysql() as conn:
+                cursor = conn.cursor()
+                try:
+                    cursor.execute(
+                        'insert into tb_wz_content(id,content) values("%s","%s")' % (
+                            item['num'],item['content']))
+                    conn.commit()
+                except:
+                    print('插入问政内容失败!')
+                    conn.rollback()
+
+
+        else:
+            with Mysql() as conn:
+                cursor = conn.cursor()
+                try:
+                    cursor.execute(
+                        'insert into tb_wz_title(id,title) values("%s","%s")' % (item['num'],item['title']))
+                    conn.commit()
+                except:
+                    print('插入问政标题失败!')
+                    conn.rollback()
+
+
+class Mysql(object):
+    def __enter__(self):
+        self.connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+        return self.connection
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.connection.close()

settings

python
BOT_NAME = 'crawl_spider'
+
+SPIDER_MODULES = ['crawl_spider.spiders']
+NEWSPIDER_MODULE = 'crawl_spider.spiders'
+
+
+# Crawl responsibly by identifying yourself (and your website) on the user-agent
+USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+
+# Obey robots.txt rules
+ROBOTSTXT_OBEY = False
+
+
+
+ITEM_PIPELINES = {
+   'crawl_spider.pipelines.CrawlSpiderPipeline': 300,
+}
BOT_NAME = 'crawl_spider'
+
+SPIDER_MODULES = ['crawl_spider.spiders']
+NEWSPIDER_MODULE = 'crawl_spider.spiders'
+
+
+# Crawl responsibly by identifying yourself (and your website) on the user-agent
+USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+
+# Obey robots.txt rules
+ROBOTSTXT_OBEY = False
+
+
+
+ITEM_PIPELINES = {
+   'crawl_spider.pipelines.CrawlSpiderPipeline': 300,
+}

启动爬虫:

数据库会新增数据

image-20210502012334780

`,26),e=[o];function t(c,r,y,E,i,F){return n(),a("div",null,e)}const h=s(p,[["render",t]]);export{m as __pageData,h as default}; diff --git "a/assets/python_crawler_CrawlSpider\345\205\250\347\253\231\347\210\254\345\217\226.md.914da3d0.lean.js" "b/assets/python_crawler_CrawlSpider\345\205\250\347\253\231\347\210\254\345\217\226.md.914da3d0.lean.js" new file mode 100644 index 000000000..311e6c3e5 --- /dev/null +++ "b/assets/python_crawler_CrawlSpider\345\205\250\347\253\231\347\210\254\345\217\226.md.914da3d0.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"CrawlSpider全站爬取","description":"","frontmatter":{},"headers":[],"relativePath":"python/crawler/CrawlSpider全站爬取.md","filePath":"python/crawler/CrawlSpider全站爬取.md","lastUpdated":1708510102000}'),p={name:"python/crawler/CrawlSpider全站爬取.md"},o=l("",26),e=[o];function t(c,r,y,E,i,F){return n(),a("div",null,e)}const h=s(p,[["render",t]]);export{m as __pageData,h as default}; diff --git "a/assets/python_crawler_python\351\205\215\345\220\210ffmpeg\344\270\213\350\275\275bilibili\350\247\206\351\242\221.md.89478ae1.js" "b/assets/python_crawler_python\351\205\215\345\220\210ffmpeg\344\270\213\350\275\275bilibili\350\247\206\351\242\221.md.89478ae1.js" new file mode 100644 index 000000000..411a1c237 --- /dev/null +++ "b/assets/python_crawler_python\351\205\215\345\220\210ffmpeg\344\270\213\350\275\275bilibili\350\247\206\351\242\221.md.89478ae1.js" @@ -0,0 +1,133 @@ +import{_ as s,o as n,c as a,Q as p}from"./chunks/framework.dc1f4608.js";const _=JSON.parse('{"title":"python配合ffmpeg下载bilibili视频","description":"","frontmatter":{},"headers":[],"relativePath":"python/crawler/python配合ffmpeg下载bilibili视频.md","filePath":"python/crawler/python配合ffmpeg下载bilibili视频.md","lastUpdated":1708510102000}'),l={name:"python/crawler/python配合ffmpeg下载bilibili视频.md"},o=p(`

python配合ffmpeg下载bilibili视频

直接上代码

TIP

需要提前下载ffmpeg并配置环境变量,ffmpeg下载地址:http://www.ffmpeg.org/download.html

python
import requests
+import re
+import json
+import os
+import subprocess
+
+headers = {
+    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36',
+    'Referer': 'https://www.bilibili.com'
+}
+
+"""
+    requests获取页面源码
+"""
+
+
+def send_request(b_url):
+    data = requests.get(url=b_url, headers=headers).text
+    return data
+
+
+"""
+    正则匹配视频和音频的真实地址
+"""
+
+
+def get_play_info(data):
+    json_data = json.loads(re.findall('<script>window\\.__playinfo__=(.*?)</script>', data)[0])
+    video_url = json_data['data']['dash']['video'][0]['backupUrl'][0]
+    audio_url = json_data['data']['dash']['audio'][0]['backupUrl'][0]
+    return video_url, audio_url
+
+
+"""
+    分别下载视频和音频文件后利用ffmpeg合并
+"""
+
+
+def download(info_list, info):
+    print(f'开始下载: {info}')
+    video_data = requests.get(url=info_list[0], headers=headers).content
+    audio_data = requests.get(url=info_list[1], headers=headers).content
+    desktop = os.path.join(os.path.expanduser("~"), 'Desktop')
+    video_path = desktop + '\\\\' + info
+    audio_path = desktop + '\\\\' + info + '_.mp3'
+    # 如果视频名称中有'-' 执行ffmpeg合并的时候会报错
+    video_path = video_path.replace('-',' ')
+    audio_path = audio_path.replace('-',' ')
+    with open(video_path + '_temp.mp4', 'wb') as f:
+        f.write(video_data)
+    with open(audio_path, 'wb') as f:
+        f.write(audio_data)
+    cmd = 'ffmpeg -y -i ' + video_path + '_temp.mp4' + ' -i ' \\
+          + audio_path + ' -c:v copy -c:a aac -strict experimental ' + video_path + '.mp4'
+    print(cmd)
+    subprocess.Popen(cmd, shell=True)
+    # os.system(cmd)
+    print('下载完成')
+
+
+if __name__ == '__main__':
+    url = input('请输入要下载的b站视频链接:')
+    page_data = send_request(url)
+    # 解析视频的名称
+    title = re.findall('<h1 title=\\"(.*?)\\" class=\\"video-title', page_data)[0]
+    play_info_list = get_play_info(page_data)
+    download(play_info_list, title)
import requests
+import re
+import json
+import os
+import subprocess
+
+headers = {
+    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36',
+    'Referer': 'https://www.bilibili.com'
+}
+
+"""
+    requests获取页面源码
+"""
+
+
+def send_request(b_url):
+    data = requests.get(url=b_url, headers=headers).text
+    return data
+
+
+"""
+    正则匹配视频和音频的真实地址
+"""
+
+
+def get_play_info(data):
+    json_data = json.loads(re.findall('<script>window\\.__playinfo__=(.*?)</script>', data)[0])
+    video_url = json_data['data']['dash']['video'][0]['backupUrl'][0]
+    audio_url = json_data['data']['dash']['audio'][0]['backupUrl'][0]
+    return video_url, audio_url
+
+
+"""
+    分别下载视频和音频文件后利用ffmpeg合并
+"""
+
+
+def download(info_list, info):
+    print(f'开始下载: {info}')
+    video_data = requests.get(url=info_list[0], headers=headers).content
+    audio_data = requests.get(url=info_list[1], headers=headers).content
+    desktop = os.path.join(os.path.expanduser("~"), 'Desktop')
+    video_path = desktop + '\\\\' + info
+    audio_path = desktop + '\\\\' + info + '_.mp3'
+    # 如果视频名称中有'-' 执行ffmpeg合并的时候会报错
+    video_path = video_path.replace('-',' ')
+    audio_path = audio_path.replace('-',' ')
+    with open(video_path + '_temp.mp4', 'wb') as f:
+        f.write(video_data)
+    with open(audio_path, 'wb') as f:
+        f.write(audio_data)
+    cmd = 'ffmpeg -y -i ' + video_path + '_temp.mp4' + ' -i ' \\
+          + audio_path + ' -c:v copy -c:a aac -strict experimental ' + video_path + '.mp4'
+    print(cmd)
+    subprocess.Popen(cmd, shell=True)
+    # os.system(cmd)
+    print('下载完成')
+
+
+if __name__ == '__main__':
+    url = input('请输入要下载的b站视频链接:')
+    page_data = send_request(url)
+    # 解析视频的名称
+    title = re.findall('<h1 title=\\"(.*?)\\" class=\\"video-title', page_data)[0]
+    play_info_list = get_play_info(page_data)
+    download(play_info_list, title)
`,4),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const u=s(l,[["render",t]]);export{_ as __pageData,u as default}; diff --git "a/assets/python_crawler_python\351\205\215\345\220\210ffmpeg\344\270\213\350\275\275bilibili\350\247\206\351\242\221.md.89478ae1.lean.js" "b/assets/python_crawler_python\351\205\215\345\220\210ffmpeg\344\270\213\350\275\275bilibili\350\247\206\351\242\221.md.89478ae1.lean.js" new file mode 100644 index 000000000..75e288e0b --- /dev/null +++ "b/assets/python_crawler_python\351\205\215\345\220\210ffmpeg\344\270\213\350\275\275bilibili\350\247\206\351\242\221.md.89478ae1.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as p}from"./chunks/framework.dc1f4608.js";const _=JSON.parse('{"title":"python配合ffmpeg下载bilibili视频","description":"","frontmatter":{},"headers":[],"relativePath":"python/crawler/python配合ffmpeg下载bilibili视频.md","filePath":"python/crawler/python配合ffmpeg下载bilibili视频.md","lastUpdated":1708510102000}'),l={name:"python/crawler/python配合ffmpeg下载bilibili视频.md"},o=p("",4),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const u=s(l,[["render",t]]);export{_ as __pageData,u as default}; diff --git "a/assets/python_crawler_requests\346\250\241\345\235\227\345\205\245\351\227\250.md.5ba42074.js" "b/assets/python_crawler_requests\346\250\241\345\235\227\345\205\245\351\227\250.md.5ba42074.js" new file mode 100644 index 000000000..88a1a71fd --- /dev/null +++ "b/assets/python_crawler_requests\346\250\241\345\235\227\345\205\245\351\227\250.md.5ba42074.js" @@ -0,0 +1,473 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"requests模块入门","description":"","frontmatter":{},"headers":[],"relativePath":"python/crawler/requests模块入门.md","filePath":"python/crawler/requests模块入门.md","lastUpdated":1708510102000}'),p={name:"python/crawler/requests模块入门.md"},o=l(`

requests模块入门

requests模块是python中原生的一款基于网络请求的模块,功能强大,简单便捷,效率极高。

作用:模拟浏览器发请求

  • 指定url
  • 发起请求
  • 获取响应
  • 持久化存储

入门程序

环境安装:pip install requests

python
import requests
+
+# 爬取搜狗首页的数据
+
+if __name__ == '__main__':
+    url = "https://www.sogou.com"
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+
+    response = requests.get(url,headers)
+    page_text = response.text
+    print(page_text)
+    with open("./sogou.html", "w", encoding="utf-8") as fp:
+        fp.write(page_text)
+    print("爬取结束")
import requests
+
+# 爬取搜狗首页的数据
+
+if __name__ == '__main__':
+    url = "https://www.sogou.com"
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+
+    response = requests.get(url,headers)
+    page_text = response.text
+    print(page_text)
+    with open("./sogou.html", "w", encoding="utf-8") as fp:
+        fp.write(page_text)
+    print("爬取结束")

WARNING

坑1:ValueError:requests check_hostname requires server_hostname

坑2:requests.exceptions.SSLError: hostname '127.0.0.1' doesn't match None of。。

网上有说降低requests版本的,有安装乱七八糟东西的,最后是降低了urllib3的版本解决的,据说是高版本的urllib有个bug

pip install urllib3==1.25.8

简易网页采集器

python
import requests
+
+if __name__ == '__main__':
+    url = 'https://www.sogou.com/web'
+    keyword = input('enter your keyword: ')
+    # UA伪装
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36'
+    }
+    param = {
+        'query': keyword
+    }
+    response = requests.get(url, params=param,headers=headers)
+    resp = response.text
+    file_name = keyword + '.html'
+    with open(file_name, 'w', encoding='utf-8') as f:
+        f.write(resp)
+    print(file_name, '保存成功')
import requests
+
+if __name__ == '__main__':
+    url = 'https://www.sogou.com/web'
+    keyword = input('enter your keyword: ')
+    # UA伪装
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36'
+    }
+    param = {
+        'query': keyword
+    }
+    response = requests.get(url, params=param,headers=headers)
+    resp = response.text
+    file_name = keyword + '.html'
+    with open(file_name, 'w', encoding='utf-8') as f:
+        f.write(resp)
+    print(file_name, '保存成功')

百度翻译内容提取

python
import requests
+import json
+
+if __name__ == '__main__':
+    url = 'https://fanyi.baidu.com/sug'
+    keyword = {
+        'kw': 'dog'
+    }
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    response = requests.post(url=url, data=keyword, headers=headers)
+    _json = response.json()
+    file_name = keyword.get('kw') + '.json'
+    with open(file_name, 'w', encoding='utf-8') as f:
+        json.dump(_json,f,ensure_ascii=False)
+    print('json存储成功')
import requests
+import json
+
+if __name__ == '__main__':
+    url = 'https://fanyi.baidu.com/sug'
+    keyword = {
+        'kw': 'dog'
+    }
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    response = requests.post(url=url, data=keyword, headers=headers)
+    _json = response.json()
+    file_name = keyword.get('kw') + '.json'
+    with open(file_name, 'w', encoding='utf-8') as f:
+        json.dump(_json,f,ensure_ascii=False)
+    print('json存储成功')

结果:

json
{
+  "errno": 0,
+  "data": [
+    {
+      "k": "dog",
+      "v": "n. 狗; 蹩脚货; 丑女人; 卑鄙小人 v. 困扰; 跟踪"
+    },
+    {
+      "k": "DOG",
+      "v": "abbr. Data Output Gate 数据输出门"
+    },
+    {
+      "k": "doge",
+      "v": "n. 共和国总督"
+    },
+    {
+      "k": "dogm",
+      "v": "abbr. dogmatic 教条的; 独断的; dogmatism 教条主义; dogmatist"
+    },
+    {
+      "k": "Dogo",
+      "v": "[地名] [马里、尼日尔、乍得] 多戈; [地名] [韩国] 道高"
+    }
+  ]
+}
{
+  "errno": 0,
+  "data": [
+    {
+      "k": "dog",
+      "v": "n. 狗; 蹩脚货; 丑女人; 卑鄙小人 v. 困扰; 跟踪"
+    },
+    {
+      "k": "DOG",
+      "v": "abbr. Data Output Gate 数据输出门"
+    },
+    {
+      "k": "doge",
+      "v": "n. 共和国总督"
+    },
+    {
+      "k": "dogm",
+      "v": "abbr. dogmatic 教条的; 独断的; dogmatism 教条主义; dogmatist"
+    },
+    {
+      "k": "Dogo",
+      "v": "[地名] [马里、尼日尔、乍得] 多戈; [地名] [韩国] 道高"
+    }
+  ]
+}

豆瓣电影排行榜

python
import requests
+import json
+
+if __name__ == '__main__':
+    url = 'https://movie.douban.com/j/chart/top_list'
+    total = 748
+    limit = 20
+    start = 0
+    params = {
+        'type': 11,
+        'interval_id': '100:90',
+        'action': '',
+        'start': start,
+        'limit': limit
+    }
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    result = total // limit
+    res_list = []
+    if result:
+        for i in range(result if total // limit == 0 else result + 1):
+            start += 20
+            res = requests.get(url=url, params=params, headers=headers)
+            res_json = res.json()
+            res_list.append(res_json)
+    with open('douban_movie.json', 'w', encoding='utf-8') as f:
+        json.dump(res_list, f, ensure_ascii=False)
+
+    print('over')
import requests
+import json
+
+if __name__ == '__main__':
+    url = 'https://movie.douban.com/j/chart/top_list'
+    total = 748
+    limit = 20
+    start = 0
+    params = {
+        'type': 11,
+        'interval_id': '100:90',
+        'action': '',
+        'start': start,
+        'limit': limit
+    }
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    result = total // limit
+    res_list = []
+    if result:
+        for i in range(result if total // limit == 0 else result + 1):
+            start += 20
+            res = requests.get(url=url, params=params, headers=headers)
+            res_json = res.json()
+            res_list.append(res_json)
+    with open('douban_movie.json', 'w', encoding='utf-8') as f:
+        json.dump(res_list, f, ensure_ascii=False)
+
+    print('over')

国家药品管理局化妆品生产许可信息

python
import requests
+import json
+
+if __name__ == '__main__':
+    # 列表页ajax
+    url = 'http://scxk.nmpa.gov.cn:81/xk/itownet/portalAction.do?method=getXkzsList'
+    # 详情页ajax
+    detail_url = 'http://scxk.nmpa.gov.cn:81/xk/itownet/portalAction.do?method=getXkzsById'
+
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    params = {
+        'on': True,
+        'page': 1,
+        'pageSize': 15,
+        'productName': '',
+        'conditionType': 1,
+        'applyname': '',
+        'applysn': ''
+    }
+
+    detail_params = {
+        'id': ''
+    }
+    # 数据容器
+    data_list = []
+    # 列表页响应
+    response = requests.post(url=url, params=params, headers=headers)
+    res_obj = response.json()
+    # 提取列表信息遍历
+    res_list = res_obj.get('list')
+    for data in res_list:
+        # id是详情页请求的参数
+        detail_id = data.get('ID')
+        detail_params['id'] = detail_id
+        # 详情页响应
+        resp = requests.post(url=detail_url, params=detail_params, headers=headers)
+        res_obj = resp.json()
+        # 容器保存
+        data_list.append(res_obj)
+    # 持久化存储
+    with open('make_up_xkz.json', 'a', encoding='utf-8') as f:
+        json.dump(data_list, f, ensure_ascii=False)
+    print('over')
import requests
+import json
+
+if __name__ == '__main__':
+    # 列表页ajax
+    url = 'http://scxk.nmpa.gov.cn:81/xk/itownet/portalAction.do?method=getXkzsList'
+    # 详情页ajax
+    detail_url = 'http://scxk.nmpa.gov.cn:81/xk/itownet/portalAction.do?method=getXkzsById'
+
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    params = {
+        'on': True,
+        'page': 1,
+        'pageSize': 15,
+        'productName': '',
+        'conditionType': 1,
+        'applyname': '',
+        'applysn': ''
+    }
+
+    detail_params = {
+        'id': ''
+    }
+    # 数据容器
+    data_list = []
+    # 列表页响应
+    response = requests.post(url=url, params=params, headers=headers)
+    res_obj = response.json()
+    # 提取列表信息遍历
+    res_list = res_obj.get('list')
+    for data in res_list:
+        # id是详情页请求的参数
+        detail_id = data.get('ID')
+        detail_params['id'] = detail_id
+        # 详情页响应
+        resp = requests.post(url=detail_url, params=detail_params, headers=headers)
+        res_obj = resp.json()
+        # 容器保存
+        data_list.append(res_obj)
+    # 持久化存储
+    with open('make_up_xkz.json', 'a', encoding='utf-8') as f:
+        json.dump(data_list, f, ensure_ascii=False)
+    print('over')

正则表达式匹配

爬取糗事百科图片

python
import requests
+import re
+
+if __name__ == '__main__':
+    for i in range(13):
+        page_no = str(i + 1)
+        url = 'https://www.qiushibaike.com/imgrank/page/%d'
+        headers = {
+            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+        }
+        url = format(url % page_no)
+        page_txt = requests.get(url=url, headers=headers).text
+        exp = '<div class="thumb">.*?<img src="(.*?)" alt.*?</div>'
+        src_list = re.findall(exp, page_txt, re.S)
+        # print(src_list)
+        for src in src_list:
+            url = 'https:' + src
+            # 向图片url发请求保存
+            stream = requests.get(url=url, headers=headers).content
+            # 文件名
+            src = src.split('/')[-1]
+            img_path = './img/' + src
+            f = open(img_path, 'wb')
+            f.write(stream)
+            print(img_path, '下载成功')
import requests
+import re
+
+if __name__ == '__main__':
+    for i in range(13):
+        page_no = str(i + 1)
+        url = 'https://www.qiushibaike.com/imgrank/page/%d'
+        headers = {
+            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+        }
+        url = format(url % page_no)
+        page_txt = requests.get(url=url, headers=headers).text
+        exp = '<div class="thumb">.*?<img src="(.*?)" alt.*?</div>'
+        src_list = re.findall(exp, page_txt, re.S)
+        # print(src_list)
+        for src in src_list:
+            url = 'https:' + src
+            # 向图片url发请求保存
+            stream = requests.get(url=url, headers=headers).content
+            # 文件名
+            src = src.split('/')[-1]
+            img_path = './img/' + src
+            f = open(img_path, 'wb')
+            f.write(stream)
+            print(img_path, '下载成功')

XPath解析

解析58二手房标题

python
from lxml import etree
+import requests
+
+if __name__ == '__main__':
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    url = 'https://bj.58.com/ershoufang/'
+    page_text = requests.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    div_list = tree.xpath('//div[@class="property"]')
+    with open('58.txt', 'w', encoding='utf-8') as f:
+        for div in div_list:
+            title = div.xpath('.//h3[@class="property-content-title-name"]/text()')
+            f.write(title[0]+'\\r\\n')
from lxml import etree
+import requests
+
+if __name__ == '__main__':
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    url = 'https://bj.58.com/ershoufang/'
+    page_text = requests.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    div_list = tree.xpath('//div[@class="property"]')
+    with open('58.txt', 'w', encoding='utf-8') as f:
+        for div in div_list:
+            title = div.xpath('.//h3[@class="property-content-title-name"]/text()')
+            f.write(title[0]+'\\r\\n')

多线程爬取美女图片

python
import requests
+from lxml import etree
+import os
+from concurrent.futures import ThreadPoolExecutor
+
+
+def download_pic(page_no, real_url, first_page_url, ):
+    print('=============开始下载第 ' + str(page_no) + ' 页=================')
+    if not page_no == 1:
+        pattern = '_' + str(page_no)
+        init_url = format(real_url % pattern)
+    else:
+        init_url = first_page_url
+    down_page_text = requests.get(url=init_url, headers=headers).text
+    down_tree = etree.HTML(down_page_text)
+    li_list = down_tree.xpath('//div[@class="slist"]//li')
+    for li in li_list:
+        img_url = 'https://pic.netbian.com' + li.xpath('.//img/@src')[0]
+        img_name = li.xpath('.//img/@alt')[0] + '.jpg'
+        img_name = img_name.encode('iso-8859-1').decode('gbk')
+        with open('./beauty/' + img_name, 'wb') as f:
+            stream = requests.get(url=img_url, headers=headers).content
+            f.write(stream)
+            print(img_name, ' 下载成功')
+
+
+if __name__ == '__main__':
+    url = 'https://pic.netbian.com/4kmeinv/index.html'
+    next_url = 'https://pic.netbian.com/4kmeinv/index%s.html'
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+
+    if not os.path.exists('./beauty'):
+        os.mkdir('./beauty')
+    page_text = requests.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    page_info = tree.xpath('//div[@class="page"]/a[7]/text()')
+    total_page_no = int(page_info[0])
+    thread_pool = ThreadPoolExecutor(max_workers=60)
+    for i in range(total_page_no):
+        i += 1
+        thread_pool.submit(download_pic, i, next_url, url)
import requests
+from lxml import etree
+import os
+from concurrent.futures import ThreadPoolExecutor
+
+
+def download_pic(page_no, real_url, first_page_url, ):
+    print('=============开始下载第 ' + str(page_no) + ' 页=================')
+    if not page_no == 1:
+        pattern = '_' + str(page_no)
+        init_url = format(real_url % pattern)
+    else:
+        init_url = first_page_url
+    down_page_text = requests.get(url=init_url, headers=headers).text
+    down_tree = etree.HTML(down_page_text)
+    li_list = down_tree.xpath('//div[@class="slist"]//li')
+    for li in li_list:
+        img_url = 'https://pic.netbian.com' + li.xpath('.//img/@src')[0]
+        img_name = li.xpath('.//img/@alt')[0] + '.jpg'
+        img_name = img_name.encode('iso-8859-1').decode('gbk')
+        with open('./beauty/' + img_name, 'wb') as f:
+            stream = requests.get(url=img_url, headers=headers).content
+            f.write(stream)
+            print(img_name, ' 下载成功')
+
+
+if __name__ == '__main__':
+    url = 'https://pic.netbian.com/4kmeinv/index.html'
+    next_url = 'https://pic.netbian.com/4kmeinv/index%s.html'
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+
+    if not os.path.exists('./beauty'):
+        os.mkdir('./beauty')
+    page_text = requests.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    page_info = tree.xpath('//div[@class="page"]/a[7]/text()')
+    total_page_no = int(page_info[0])
+    thread_pool = ThreadPoolExecutor(max_workers=60)
+    for i in range(total_page_no):
+        i += 1
+        thread_pool.submit(download_pic, i, next_url, url)

全国城市名称爬取

python
import requests
+from lxml import etree
+
+if __name__ == '__main__':
+    url = 'https://www.aqistudy.cn/historydata'
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    page_text = requests.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    cities = tree.xpath('//div[@class="bottom"]/ul//li/a/text()')
+    print(len(cities))
import requests
+from lxml import etree
+
+if __name__ == '__main__':
+    url = 'https://www.aqistudy.cn/historydata'
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    page_text = requests.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    cities = tree.xpath('//div[@class="bottom"]/ul//li/a/text()')
+    print(len(cities))
`,28),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const _=s(p,[["render",t]]);export{d as __pageData,_ as default}; diff --git "a/assets/python_crawler_requests\346\250\241\345\235\227\345\205\245\351\227\250.md.5ba42074.lean.js" "b/assets/python_crawler_requests\346\250\241\345\235\227\345\205\245\351\227\250.md.5ba42074.lean.js" new file mode 100644 index 000000000..0e1a2f375 --- /dev/null +++ "b/assets/python_crawler_requests\346\250\241\345\235\227\345\205\245\351\227\250.md.5ba42074.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"requests模块入门","description":"","frontmatter":{},"headers":[],"relativePath":"python/crawler/requests模块入门.md","filePath":"python/crawler/requests模块入门.md","lastUpdated":1708510102000}'),p={name:"python/crawler/requests模块入门.md"},o=l("",28),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const _=s(p,[["render",t]]);export{d as __pageData,_ as default}; diff --git "a/assets/python_crawler_scrapy\346\241\206\346\236\266\345\205\245\351\227\250.md.74ea41f3.js" "b/assets/python_crawler_scrapy\346\241\206\346\236\266\345\205\245\351\227\250.md.74ea41f3.js" new file mode 100644 index 000000000..f4987ee33 --- /dev/null +++ "b/assets/python_crawler_scrapy\346\241\206\346\236\266\345\205\245\351\227\250.md.74ea41f3.js" @@ -0,0 +1,281 @@ +import{_ as s,o as n,c as a,Q as p}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"scrapy框架入门","description":"","frontmatter":{},"headers":[],"relativePath":"python/crawler/scrapy框架入门.md","filePath":"python/crawler/scrapy框架入门.md","lastUpdated":1708510102000}'),l={name:"python/crawler/scrapy框架入门.md"},o=p(`

scrapy框架入门

高性能的持久化存储,高性能的数据解析,分布式。

使用

  • 安装:pip install scrapy
  • 创建项目:scrapy startproject yourProjectName
bash
story_spider/
+    scrapy.cfg            # 部署配置文件
+
+    story_spider/             # Python模块,代码写在这个目录下
+        __init__.py
+
+        items.py          # 项目项定义文件
+
+        pipelines.py      # 项目管道文件
+
+        settings.py       # 项目设置文件
+
+        spiders/          # 我们的爬虫/蜘蛛 目录
+            __init__.py
story_spider/
+    scrapy.cfg            # 部署配置文件
+
+    story_spider/             # Python模块,代码写在这个目录下
+        __init__.py
+
+        items.py          # 项目项定义文件
+
+        pipelines.py      # 项目管道文件
+
+        settings.py       # 项目设置文件
+
+        spiders/          # 我们的爬虫/蜘蛛 目录
+            __init__.py
  • 在spiders目录中创建一个爬虫文件

    • cd 项目目录(spriders文件夹所在的目录)
    • scrapy genspider storyxc storyxc.com
  • 爬虫文件内容

    python
    import scrapy
    +	
    +
    +# 必须继承scrapy.Spider
    +class StoryxcSpider(scrapy.Spider):
    +    # 爬虫文件的名称:爬虫源文件的一个唯一标识
    +    name = 'storyxc'
    +    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
    +    allowed_domains = ['storyxc.com']
    +    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
    +    start_urls = ['https://www.storyxc.com/', 'http://blog.storyxc.com']
    +
    +    # 用作数据解析:response参数表示的是请求成功后的响应对象
    +    def parse(self, response):
    +        print(response)
    import scrapy
    +	
    +
    +# 必须继承scrapy.Spider
    +class StoryxcSpider(scrapy.Spider):
    +    # 爬虫文件的名称:爬虫源文件的一个唯一标识
    +    name = 'storyxc'
    +    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
    +    allowed_domains = ['storyxc.com']
    +    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
    +    start_urls = ['https://www.storyxc.com/', 'http://blog.storyxc.com']
    +
    +    # 用作数据解析:response参数表示的是请求成功后的响应对象
    +    def parse(self, response):
    +        print(response)

    修改settings.py中的ROBOTSTXT_OBEY = False

    执行工程命令后可以加 --nolog

    也可以在setting.py中添加:

    #显示指定级别的日志信息
    +LOG_LEVEL = 'ERROR'
    #显示指定级别的日志信息
    +LOG_LEVEL = 'ERROR'
  • 执行工程scrapy crawl storyxc,日志信息

    bash
    <200 https://www.storyxc.com/>
    +<200 https://blog.storyxc.com/>
    <200 https://www.storyxc.com/>
    +<200 https://blog.storyxc.com/>

scrapy数据解析

解析糗事百科段子的作者和段子内容

python
import scrapy
+
+
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['https://www.qiushibaike.com/text/']
+
+    # 用作数据解析:response参数表示的是请求成功后的响应对象
+    def parse(self, response):
+        # 解析:作者的名称+段子内容
+        div_list = response.xpath('//div[@id="content"]/div/div[2]/div')
+        for div in div_list:
+            # extract()方法可以提取Selector对象中的data参数字符串
+            # extract_first()提取的是list数组里面的第一个字符串,
+            author = div.xpath('./div[1]/a[2]/h2/text()').extract_first()
+            # 列表调用了extract()表示将每一个Selector对象中的data字符串提取出来
+            content = ''.join(div.xpath('./a[1]/div[1]/span//text()').extract())
+            print(author,content)
import scrapy
+
+
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['https://www.qiushibaike.com/text/']
+
+    # 用作数据解析:response参数表示的是请求成功后的响应对象
+    def parse(self, response):
+        # 解析:作者的名称+段子内容
+        div_list = response.xpath('//div[@id="content"]/div/div[2]/div')
+        for div in div_list:
+            # extract()方法可以提取Selector对象中的data参数字符串
+            # extract_first()提取的是list数组里面的第一个字符串,
+            author = div.xpath('./div[1]/a[2]/h2/text()').extract_first()
+            # 列表调用了extract()表示将每一个Selector对象中的data字符串提取出来
+            content = ''.join(div.xpath('./a[1]/div[1]/span//text()').extract())
+            print(author,content)

基于终端指令持久化存储

代码改造:

python
import scrapy
+
+
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['https://www.qiushibaike.com/text/']
+
+    # 用作数据解析:response参数表示的是请求成功后的响应对象
+    def parse(self, response):
+        # 解析:作者的名称+段子内容
+        div_list = response.xpath('//div[@id="content"]/div/div[2]/div')
+        data_list = []
+        for div in div_list:
+            # extract()方法可以提取Selector对象中的data参数字符串
+            # extract_first()提取的是list数组里面的第一个字符串,
+            author = div.xpath('./div[1]/a[2]/h2/text()').extract_first()
+            # 列表调用了extract()表示将每一个Selector对象中的data字符串提取出来
+            content = ''.join(div.xpath('./a[1]/div[1]/span//text()').extract())
+            dict = {
+                'author':author,
+                'content':content
+            }
+            data_list.append(dict)
+        return data_list
import scrapy
+
+
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['https://www.qiushibaike.com/text/']
+
+    # 用作数据解析:response参数表示的是请求成功后的响应对象
+    def parse(self, response):
+        # 解析:作者的名称+段子内容
+        div_list = response.xpath('//div[@id="content"]/div/div[2]/div')
+        data_list = []
+        for div in div_list:
+            # extract()方法可以提取Selector对象中的data参数字符串
+            # extract_first()提取的是list数组里面的第一个字符串,
+            author = div.xpath('./div[1]/a[2]/h2/text()').extract_first()
+            # 列表调用了extract()表示将每一个Selector对象中的data字符串提取出来
+            content = ''.join(div.xpath('./a[1]/div[1]/span//text()').extract())
+            dict = {
+                'author':author,
+                'content':content
+            }
+            data_list.append(dict)
+        return data_list
  • 只能将parse方法返回的内容存储到本地文件中
  • 持久化存储的格式只有'json', 'jsonlines', 'jl', 'csv', 'xml', 'marshal', 'pickle'
  • 指令:scrapy crawl xxx -o path

基于管道持久化存储

流程:

  • 数据解析

  • 在item类中定义相关的属性

    • fieldName = scrapy.Field()
  • 将解析的数据封装存储到Item类型的对象中

  • 将item类型的对象提交给管道进行持久化存储的操作

  • 在管道类的process_item函数中要将其接受到的item对象中存储的数据进行持久化操作

  • 在settings.py中开启管道

代码改造

Python
import scrapy
+from story_spider.items import StorySpiderItem
+
+
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['https://www.qiushibaike.com/text/']
+
+    # 用作数据解析:response参数表示的是请求成功后的响应对象
+    def parse(self, response):
+        # 解析:作者的名称+段子内容
+        div_list = response.xpath('//div[@id="content"]/div/div[2]/div')
+        data_list = []
+        for div in div_list:
+            # extract()方法可以提取Selector对象中的data参数字符串
+            # extract_first()提取的是list数组里面的第一个字符串,
+            author = div.xpath('./div[1]/a[2]/h2/text()').extract_first()
+            # 列表调用了extract()表示将每一个Selector对象中的data字符串提取出来
+            content = ''.join(div.xpath('./a[1]/div[1]/span//text()').extract())
+            item = StorySpiderItem()
+            item['author'] = author
+            item['content'] = content
+            yield item
import scrapy
+from story_spider.items import StorySpiderItem
+
+
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['https://www.qiushibaike.com/text/']
+
+    # 用作数据解析:response参数表示的是请求成功后的响应对象
+    def parse(self, response):
+        # 解析:作者的名称+段子内容
+        div_list = response.xpath('//div[@id="content"]/div/div[2]/div')
+        data_list = []
+        for div in div_list:
+            # extract()方法可以提取Selector对象中的data参数字符串
+            # extract_first()提取的是list数组里面的第一个字符串,
+            author = div.xpath('./div[1]/a[2]/h2/text()').extract_first()
+            # 列表调用了extract()表示将每一个Selector对象中的data字符串提取出来
+            content = ''.join(div.xpath('./a[1]/div[1]/span//text()').extract())
+            item = StorySpiderItem()
+            item['author'] = author
+            item['content'] = content
+            yield item

items模块

python
import scrapy
+
+
+class StorySpiderItem(scrapy.Item):
+    # define the fields for your item here like:
+    # name = scrapy.Field()
+    author = scrapy.Field()
+    content = scrapy.Field()
import scrapy
+
+
+class StorySpiderItem(scrapy.Item):
+    # define the fields for your item here like:
+    # name = scrapy.Field()
+    author = scrapy.Field()
+    content = scrapy.Field()

pipelines模块

python
class StorySpiderPipeline:
+    fp = None
+
+    # open_spider方法只会在爬虫开始时调用一次,可以用于数据初始化操作
+    def open_spider(self, spider):
+        print('开始执行爬虫...')
+        self.fp = open('./qiubai.txt', 'w', encoding='utf-8')
+
+    # close_spider会在结束时调用一次
+    def close_spider(self, spider):
+        print('爬虫执行结束...')
+        self.fp.close()
+
+    # 专门用来处理item对象
+    # 该方法可以接收爬虫文件提交的item对象
+    def process_item(self, item, spider):
+        author = item['author']
+        content = item['content']
+        self.fp.write(author + ':' + content + '\\n')
+        return item
class StorySpiderPipeline:
+    fp = None
+
+    # open_spider方法只会在爬虫开始时调用一次,可以用于数据初始化操作
+    def open_spider(self, spider):
+        print('开始执行爬虫...')
+        self.fp = open('./qiubai.txt', 'w', encoding='utf-8')
+
+    # close_spider会在结束时调用一次
+    def close_spider(self, spider):
+        print('爬虫执行结束...')
+        self.fp.close()
+
+    # 专门用来处理item对象
+    # 该方法可以接收爬虫文件提交的item对象
+    def process_item(self, item, spider):
+        author = item['author']
+        content = item['content']
+        self.fp.write(author + ':' + content + '\\n')
+        return item

TIP

return item可以使item继续传递到下一个即将被执行的管道类中,以此可以实现多个管道类的操作,比如一份数据持久化到文件,一份数据持久化到数据库

配置文件中开启管道

settings.py中修改

python
# Configure item pipelines
+# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
+ITEM_PIPELINES = {
+    # 300表示优先级,数值越小,优先级越高
+   'story_spider.pipelines.StorySpiderPipeline': 300,
+}
# Configure item pipelines
+# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
+ITEM_PIPELINES = {
+    # 300表示优先级,数值越小,优先级越高
+   'story_spider.pipelines.StorySpiderPipeline': 300,
+}

执行爬虫

python
scrapy crawl storyxc
+开始执行爬虫...
+爬虫执行结束...
+目录下生成了qiubai.txt
scrapy crawl storyxc
+开始执行爬虫...
+爬虫执行结束...
+目录下生成了qiubai.txt
`,28),e=[o];function t(c,r,y,E,i,d){return n(),a("div",null,e)}const u=s(l,[["render",t]]);export{h as __pageData,u as default}; diff --git "a/assets/python_crawler_scrapy\346\241\206\346\236\266\345\205\245\351\227\250.md.74ea41f3.lean.js" "b/assets/python_crawler_scrapy\346\241\206\346\236\266\345\205\245\351\227\250.md.74ea41f3.lean.js" new file mode 100644 index 000000000..ed34c6a40 --- /dev/null +++ "b/assets/python_crawler_scrapy\346\241\206\346\236\266\345\205\245\351\227\250.md.74ea41f3.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as p}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"scrapy框架入门","description":"","frontmatter":{},"headers":[],"relativePath":"python/crawler/scrapy框架入门.md","filePath":"python/crawler/scrapy框架入门.md","lastUpdated":1708510102000}'),l={name:"python/crawler/scrapy框架入门.md"},o=p("",28),e=[o];function t(c,r,y,E,i,d){return n(),a("div",null,e)}const u=s(l,[["render",t]]);export{h as __pageData,u as default}; diff --git "a/assets/python_crawler_scrapy\350\277\233\351\230\266.md.9a37e129.js" "b/assets/python_crawler_scrapy\350\277\233\351\230\266.md.9a37e129.js" new file mode 100644 index 000000000..dd20c7678 --- /dev/null +++ "b/assets/python_crawler_scrapy\350\277\233\351\230\266.md.9a37e129.js" @@ -0,0 +1,473 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"scrapy进阶","description":"","frontmatter":{},"headers":[],"relativePath":"python/crawler/scrapy进阶.md","filePath":"python/crawler/scrapy进阶.md","lastUpdated":1708510102000}'),p={name:"python/crawler/scrapy进阶.md"},e=l(`

scrapy进阶

全站数据爬取

python
import scrapy
+from story_spider.items import StorySpiderItem
+
+
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['http://www.521609.com/meinvxiaohua/']
+    url_template = 'http://www.521609.com/meinvxiaohua/list12%d.html'
+    page_num = 2
+
+    def parse(self, response):
+        li_list = response.xpath('//div[@id="content"]/div[2]/div[2]/ul/li')
+        for li in li_list:
+            img_name = li.xpath('./a[2]//text()').extract_first()
+            print(img_name)
+        if self.page_num <= 11:
+            next_url = format(self.url_template % self.page_num)
+            self.page_num += 1
+            # 手动请求发送:yield scrapy.Request(url,callback)
+            # callback专门用作数据解析
+            yield scrapy.Request(url=next_url, callback=self.parse)
import scrapy
+from story_spider.items import StorySpiderItem
+
+
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['http://www.521609.com/meinvxiaohua/']
+    url_template = 'http://www.521609.com/meinvxiaohua/list12%d.html'
+    page_num = 2
+
+    def parse(self, response):
+        li_list = response.xpath('//div[@id="content"]/div[2]/div[2]/ul/li')
+        for li in li_list:
+            img_name = li.xpath('./a[2]//text()').extract_first()
+            print(img_name)
+        if self.page_num <= 11:
+            next_url = format(self.url_template % self.page_num)
+            self.page_num += 1
+            # 手动请求发送:yield scrapy.Request(url,callback)
+            # callback专门用作数据解析
+            yield scrapy.Request(url=next_url, callback=self.parse)

五大核心组件

  • 引擎
  • 调度器
  • 下载器
  • Spider
  • 管道

流程:

  • spider中产生url,对url进行请求发送

  • url会被封装成请求对象交给引擎,引擎把请求给调度器

  • 调度器会使用过滤器将引擎提交的请求去重,将去重后的请求对象放入队列

  • 调度器会把请求对象从队列中调度给引擎,引擎把请求交给下载器

  • 下载器去互联网中进行数据下载,将数据封装在response里返回给引擎

  • 引擎将response返回给spider,spider对数据进行解析,将数据封装到item当中,交给引擎

  • 引擎把item交给管道

  • 管道进行持久化存储

gzuo

请求传参

  • 使用场景:如果爬取解析的数据不再同一张页面中(深度爬取)

  • yield scrapy.Request(url,callback,meta= {'item':item})

    • 请求传参 item可以传递给callback回调函数

图片爬取之ImagePipeline

  • 字符串持久化:xpath解析交给管道持久化
  • 图片持久化:xpath解析出src属性,单独对图片地址发起请求获取图片二进制类型数据

ImagePipeline

只需要解析出img的src属性进行解析并提交到管道,管道就会对图片的src进行请求发送获取二进制数据

爬虫文件

python
import scrapy
+from story_spider.items import StorySpiderItem
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['http://sc.chinaz.com/tupian/']
+
+    def parse(self, response):
+        div_list = response.xpath('//div[@id="container"]/div')
+        for div in div_list:
+            # 该网站有懒加载,要使用伪属性
+            src = div.xpath('./div/a/img/@src2').extract_first()
+            real_src = 'https:' + src
+            # print(real_src)
+            item = StorySpiderItem()
+            item['src'] = real_src
+            yield item
import scrapy
+from story_spider.items import StorySpiderItem
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['http://sc.chinaz.com/tupian/']
+
+    def parse(self, response):
+        div_list = response.xpath('//div[@id="container"]/div')
+        for div in div_list:
+            # 该网站有懒加载,要使用伪属性
+            src = div.xpath('./div/a/img/@src2').extract_first()
+            real_src = 'https:' + src
+            # print(real_src)
+            item = StorySpiderItem()
+            item['src'] = real_src
+            yield item

Items

python
# Define here the models for your scraped items
+#
+# See documentation in:
+# https://docs.scrapy.org/en/latest/topics/items.html
+
+import scrapy
+
+
+class StorySpiderItem(scrapy.Item):
+    # define the fields for your item here like:
+    # name = scrapy.Field()
+    src = scrapy.Field()
# Define here the models for your scraped items
+#
+# See documentation in:
+# https://docs.scrapy.org/en/latest/topics/items.html
+
+import scrapy
+
+
+class StorySpiderItem(scrapy.Item):
+    # define the fields for your item here like:
+    # name = scrapy.Field()
+    src = scrapy.Field()

Pipeline

# Define your item pipelines here
+#
+# Don't forget to add your pipeline to the ITEM_PIPELINES setting
+# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
+
+
+# useful for handling different item types with a single interface
+
+from scrapy.pipelines.images import ImagesPipeline
+import scrapy
+
+
+class ImagePipeline(ImagesPipeline):
+    # 根据图片地址进行图片数据的请求
+    def get_media_requests(self, item, info):
+        print(item['src'])
+        yield scrapy.Request(item['src'])
+
+    def file_path(self, request, response=None, info=None, *, item=None):
+        # 指定图片存储路径
+        imageName = request.url.split('/')[-1]
+        return imageName
+
+    def item_completed(self, results, item, info):
+        return item  # 返回给下一个被执行的管道类
# Define your item pipelines here
+#
+# Don't forget to add your pipeline to the ITEM_PIPELINES setting
+# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
+
+
+# useful for handling different item types with a single interface
+
+from scrapy.pipelines.images import ImagesPipeline
+import scrapy
+
+
+class ImagePipeline(ImagesPipeline):
+    # 根据图片地址进行图片数据的请求
+    def get_media_requests(self, item, info):
+        print(item['src'])
+        yield scrapy.Request(item['src'])
+
+    def file_path(self, request, response=None, info=None, *, item=None):
+        # 指定图片存储路径
+        imageName = request.url.split('/')[-1]
+        return imageName
+
+    def item_completed(self, results, item, info):
+        return item  # 返回给下一个被执行的管道类

settings

# 保存的文件夹
+IMAGES_STORE = './imgs'
+# 启用管道
+ITEM_PIPELINES = {
+    # 300表示优先级,数值越小,优先级越高
+   'story_spider.pipelines.ImagePipeline': 300
+}
# 保存的文件夹
+IMAGES_STORE = './imgs'
+# 启用管道
+ITEM_PIPELINES = {
+    # 300表示优先级,数值越小,优先级越高
+   'story_spider.pipelines.ImagePipeline': 300
+}

DANGER

这里直接运行不会报错,但是会发现也没有下载成功,但是实际上url已经能拿到了,把日志的级别放开后,查看日志信息会发现有一句 2021-05-01 13:22:48 [scrapy.middleware] WARNING: Disabled ImgsPipeline: ImagesPipeline requires installing Pillow 4.0.0 or later

提示使用ImagesPipeline还需要安装下pillow :pip install pillow

这个很坑,不仔细看找不到,排查了半天才解决

安装完pillow后启动爬虫,可以看到图片已经下载完成

中间件

  • 下载中间件
    • 位置:引擎和下载器之间
    • 作用:批量拦截到整个工程中所有的请求和响应
    • 拦截请求:
      • UA伪装
      • 代理IP设置
    • 拦截响应:
      • 篡改响应数据,响应对象
    • 核心方法:
      • process_request:拦截请求
      • process_response:拦截响应
      • processs_exception:拦截发生异常的请求
  • 爬虫中间件
    • 位置:在引擎及爬虫
    • 作用:处理spider的输入(response)和输出(item及requests).

下载中间件

python
def process_request(self, request, spider):
+        # UA 伪装,也可以设置ua池,随机设置
+        request.headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+
+        # 设置代理
+        request.meta['proxy'] = 'https://ip:port'
+        return None
+
+    def process_response(self, request, response, spider):
+        # Called with the response returned from the downloader.
+
+        # Must either;
+        # - return a Response object
+        # - return a Request object
+        # - or raise IgnoreRequest
+        return response
+
+    def process_exception(self, request, exception, spider):
+
+        # Called when a download handler or a process_request()
+        # (from other downloader middleware) raises an exception.
+        # 发生异常的请求切换代理 也可以实现代理池,指定切换逻辑
+        request.meta['proxy'] = 'https://ip:port'
+
+        # Must either:
+        # - return None: continue processing this exception
+        # - return a Response object: stops process_exception() chain
+        # - return a Request object: stops process_exception() chain
+        return request #将修正后的request重新进行发送
def process_request(self, request, spider):
+        # UA 伪装,也可以设置ua池,随机设置
+        request.headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+
+        # 设置代理
+        request.meta['proxy'] = 'https://ip:port'
+        return None
+
+    def process_response(self, request, response, spider):
+        # Called with the response returned from the downloader.
+
+        # Must either;
+        # - return a Response object
+        # - return a Request object
+        # - or raise IgnoreRequest
+        return response
+
+    def process_exception(self, request, exception, spider):
+
+        # Called when a download handler or a process_request()
+        # (from other downloader middleware) raises an exception.
+        # 发生异常的请求切换代理 也可以实现代理池,指定切换逻辑
+        request.meta['proxy'] = 'https://ip:port'
+
+        # Must either:
+        # - return None: continue processing this exception
+        # - return a Response object: stops process_exception() chain
+        # - return a Request object: stops process_exception() chain
+        return request #将修正后的request重新进行发送

案例:爬取网易新闻指定分类下的新闻标题和内容

爬虫类

python
import scrapy
+from story_spider.items import StorySpiderItem
+
+
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['https://news.163.com/']
+    module_urls = []
+
+    def __init__(self):
+        super().__init__(self)
+        from selenium import webdriver
+        self.browser = webdriver.Chrome()
+
+    def parse(self, response):
+
+        li_list = response.xpath('//*[@id="index2016_wrap"]/div[1]/div[2]/div[2]/div[2]/div[2]/div/ul/li')
+        need_index = [3, 4, 6]
+        for index in need_index:
+            module_url = li_list[index].xpath('./a/@href').extract_first()
+            self.module_urls.append(module_url)
+
+        for url in self.module_urls:
+            yield scrapy.Request(url, callback=self.parse_module)
+
+    #  解析篡改过的 已经添加了动态加载数据的响应信息
+    def parse_module(self, response):
+        div_list = response.xpath('/html/body/div/div[3]/div[4]/div[1]/div[1]/div/ul/li/div/div')
+        for div in div_list:
+            # news_title = div.xpath('./div/div[1]/h3/a/text()').extract_first()
+            detail_url = div.xpath('./div/div[1]/h3/a/@href').extract_first()
+
+            yield scrapy.Request(url=detail_url,callback=self.parse_detail)
+
+
+    # 解析新闻详情页
+    def parse_detail(self, response):
+        title = response.xpath('//*[@id="container"]/div[1]/h1/text()').extract_first()
+        detail_text = response.xpath('//*[@id="content"]/div[2]//text()').extract()
+        detail_text = ''.join(detail_text)
+        item = StorySpiderItem()
+        item['title'] = title
+        item['content'] = detail_text
+        yield item
+
+    def closed(self,spider):
+        self.browser.quit()
import scrapy
+from story_spider.items import StorySpiderItem
+
+
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['https://news.163.com/']
+    module_urls = []
+
+    def __init__(self):
+        super().__init__(self)
+        from selenium import webdriver
+        self.browser = webdriver.Chrome()
+
+    def parse(self, response):
+
+        li_list = response.xpath('//*[@id="index2016_wrap"]/div[1]/div[2]/div[2]/div[2]/div[2]/div/ul/li')
+        need_index = [3, 4, 6]
+        for index in need_index:
+            module_url = li_list[index].xpath('./a/@href').extract_first()
+            self.module_urls.append(module_url)
+
+        for url in self.module_urls:
+            yield scrapy.Request(url, callback=self.parse_module)
+
+    #  解析篡改过的 已经添加了动态加载数据的响应信息
+    def parse_module(self, response):
+        div_list = response.xpath('/html/body/div/div[3]/div[4]/div[1]/div[1]/div/ul/li/div/div')
+        for div in div_list:
+            # news_title = div.xpath('./div/div[1]/h3/a/text()').extract_first()
+            detail_url = div.xpath('./div/div[1]/h3/a/@href').extract_first()
+
+            yield scrapy.Request(url=detail_url,callback=self.parse_detail)
+
+
+    # 解析新闻详情页
+    def parse_detail(self, response):
+        title = response.xpath('//*[@id="container"]/div[1]/h1/text()').extract_first()
+        detail_text = response.xpath('//*[@id="content"]/div[2]//text()').extract()
+        detail_text = ''.join(detail_text)
+        item = StorySpiderItem()
+        item['title'] = title
+        item['content'] = detail_text
+        yield item
+
+    def closed(self,spider):
+        self.browser.quit()

中间件类

只展示了下载中间件

python
class StorySpiderDownloaderMiddleware:
+
+    def process_request(self, request, spider):
+
+        return None
+
+    # 拦截模块对应的响应对象,进行篡改
+    # 由于是动态加载的内容,使用selenium
+    def process_response(self, request, response, spider):
+        # 过滤指定的响应对象
+        urls = spider.module_urls
+        bro = spider.browser
+        from scrapy.http import HtmlResponse
+        from time import sleep
+        # 只有指定模块url的数据才使用selenium请求并进行篡改数据
+        if request.url in urls:
+            bro.get(request.url) # selenium请求详情页
+            sleep(3)
+            page_data = bro.page_source # 包含了动态加载的新闻数据
+            # 要爬取的指定模块的响应内容
+            # 实例化一个新的响应对象 (包含动态加载的新闻数据),替代原来的响应对象
+            new_res = HtmlResponse(url=request.url,body=page_data,encoding='utf-8',request=request)
+            return new_res
+
+        return response
+
+    def process_exception(self, request, exception, spider):
+
+        return request #将修正后的request重新进行发送
class StorySpiderDownloaderMiddleware:
+
+    def process_request(self, request, spider):
+
+        return None
+
+    # 拦截模块对应的响应对象,进行篡改
+    # 由于是动态加载的内容,使用selenium
+    def process_response(self, request, response, spider):
+        # 过滤指定的响应对象
+        urls = spider.module_urls
+        bro = spider.browser
+        from scrapy.http import HtmlResponse
+        from time import sleep
+        # 只有指定模块url的数据才使用selenium请求并进行篡改数据
+        if request.url in urls:
+            bro.get(request.url) # selenium请求详情页
+            sleep(3)
+            page_data = bro.page_source # 包含了动态加载的新闻数据
+            # 要爬取的指定模块的响应内容
+            # 实例化一个新的响应对象 (包含动态加载的新闻数据),替代原来的响应对象
+            new_res = HtmlResponse(url=request.url,body=page_data,encoding='utf-8',request=request)
+            return new_res
+
+        return response
+
+    def process_exception(self, request, exception, spider):
+
+        return request #将修正后的request重新进行发送

Item类

python
import scrapy
+
+
+class StorySpiderItem(scrapy.Item):
+    # define the fields for your item here like:
+    # name = scrapy.Field()
+    title = scrapy.Field()
+    content = scrapy.Field()
import scrapy
+
+
+class StorySpiderItem(scrapy.Item):
+    # define the fields for your item here like:
+    # name = scrapy.Field()
+    title = scrapy.Field()
+    content = scrapy.Field()

Pipeline类

python
class StroyxcPipeline(object):
+    fp = None
+
+    def open_spider(self, spider):
+        self.fp = open('./163news.txt', 'w', encoding='utf-8')
+
+    def process_item(self, item, spider):
+        self.fp.write(item['title'] + ':' + item['content'] + '\\n')
+        return item
+
+    def close_spider(self, spider):
+        self.fp.close()
class StroyxcPipeline(object):
+    fp = None
+
+    def open_spider(self, spider):
+        self.fp = open('./163news.txt', 'w', encoding='utf-8')
+
+    def process_item(self, item, spider):
+        self.fp.write(item['title'] + ':' + item['content'] + '\\n')
+        return item
+
+    def close_spider(self, spider):
+        self.fp.close()

settings配置

python
BOT_NAME = 'story_spider'
+
+SPIDER_MODULES = ['story_spider.spiders']
+NEWSPIDER_MODULE = 'story_spider.spiders'
+
+# Crawl responsibly by identifying yourself (and your website) on the user-agent
+USER_AGENT = {
+    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36'
+}
+
+# Obey robots.txt rules
+ROBOTSTXT_OBEY = False
+
+
+# Enable or disable downloader middlewares
+# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
+DOWNLOADER_MIDDLEWARES = {
+   'story_spider.middlewares.StorySpiderDownloaderMiddleware': 543,
+}
+
+# Configure item pipelines
+# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
+ITEM_PIPELINES = {
+    # 300表示优先级,数值越小,优先级越高
+   'story_spider.pipelines.StroyxcPipeline': 300
+}
BOT_NAME = 'story_spider'
+
+SPIDER_MODULES = ['story_spider.spiders']
+NEWSPIDER_MODULE = 'story_spider.spiders'
+
+# Crawl responsibly by identifying yourself (and your website) on the user-agent
+USER_AGENT = {
+    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36'
+}
+
+# Obey robots.txt rules
+ROBOTSTXT_OBEY = False
+
+
+# Enable or disable downloader middlewares
+# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
+DOWNLOADER_MIDDLEWARES = {
+   'story_spider.middlewares.StorySpiderDownloaderMiddleware': 543,
+}
+
+# Configure item pipelines
+# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
+ITEM_PIPELINES = {
+    # 300表示优先级,数值越小,优先级越高
+   'story_spider.pipelines.StroyxcPipeline': 300
+}

运行结果:

image-20210501172946555

scrapy调试

  • pycharm中编辑运行/debug配置

  • 点击加号添加一个新的配置,选择python,给配置命个名,比如scrapy

  • script path选择python目录下的Lib/site-packages/scrapycmdline.py

  • parameter填crawl yourSpiderName

  • working directory填写爬虫项目路径

  • 保存,再debug运行scrapy这个配置就行

例如:

image-20210502101609233

`,47),o=[e];function t(r,c,y,E,i,d){return n(),a("div",null,o)}const m=s(p,[["render",t]]);export{u as __pageData,m as default}; diff --git "a/assets/python_crawler_scrapy\350\277\233\351\230\266.md.9a37e129.lean.js" "b/assets/python_crawler_scrapy\350\277\233\351\230\266.md.9a37e129.lean.js" new file mode 100644 index 000000000..49beabc2a --- /dev/null +++ "b/assets/python_crawler_scrapy\350\277\233\351\230\266.md.9a37e129.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"scrapy进阶","description":"","frontmatter":{},"headers":[],"relativePath":"python/crawler/scrapy进阶.md","filePath":"python/crawler/scrapy进阶.md","lastUpdated":1708510102000}'),p={name:"python/crawler/scrapy进阶.md"},e=l("",47),o=[e];function t(r,c,y,E,i,d){return n(),a("div",null,o)}const m=s(p,[["render",t]]);export{u as __pageData,m as default}; diff --git "a/assets/python_crawler_selenium\346\250\241\345\235\227.md.5446e227.js" "b/assets/python_crawler_selenium\346\250\241\345\235\227.md.5446e227.js" new file mode 100644 index 000000000..8f392de3a --- /dev/null +++ "b/assets/python_crawler_selenium\346\250\241\345\235\227.md.5446e227.js" @@ -0,0 +1,375 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"selenium模块","description":"","frontmatter":{},"headers":[],"relativePath":"python/crawler/selenium模块.md","filePath":"python/crawler/selenium模块.md","lastUpdated":1708510102000}'),p={name:"python/crawler/selenium模块.md"},o=l(`

selenium模块

selenium是一个用于web应用程序测试的工具,selenium测试直接运行在浏览器中,就像真正的用户在操作一样

selenium在爬虫中的应用:

  • 便捷的获取网站动态加载的数据
  • 便捷的实现模拟登录

selenium模块:

  • 基于浏览器自动化的一个模块

使用流程

  • 环境安装:pip install selenium
  • 下载浏览器驱动程序

http://chromedriver.storage.googleapis.com/index.html 驱动程序

  • 实例化浏览器对象
  • 编写基于浏览器对象操作的代码
python
from selenium import webdriver
+from lxml import etree
+import time
+
+# 实例化一个浏览器对象
+browser = webdriver.Chrome(executable_path=r'D:\\install\\tools\\chromedriver\\chromedriver.exe')
+# 访问url
+browser.get('https://movie.douban.com/typerank?type_name=%E5%89%A7%E6%83%85&type=11&interval_id=100:90&action=')
+# 获取页面源码内容
+page_text = browser.page_source
+
+tree = etree.HTML(page_text)
+span_list = tree.xpath('//div[@class="movie-content"]/div[1]/div[1]/span[1]')
+for span in span_list:
+    name = span.xpath('.//a/text()')
+    print(name)
+time.sleep(5)
+# 退出
+browser.quit()
+    
+res:
+['肖申克的救赎']
+['霸王别姬']
+['控方证人']
+['伊丽莎白']
+['阿甘正传']
+['美丽人生']
+['辛德勒的名单']
+['茶馆']
+['控方证人']
+['十二怒汉(电视版)']
+['这个杀手不太冷']
+['千与千寻']
+['泰坦尼克号']
+['忠犬八公的故事']
+['十二怒汉']
+['泰坦尼克号 3D版']
+['背靠背,脸对脸']
+['灿烂人生']
+['横空出世']
+['遥望南方的童年']
from selenium import webdriver
+from lxml import etree
+import time
+
+# 实例化一个浏览器对象
+browser = webdriver.Chrome(executable_path=r'D:\\install\\tools\\chromedriver\\chromedriver.exe')
+# 访问url
+browser.get('https://movie.douban.com/typerank?type_name=%E5%89%A7%E6%83%85&type=11&interval_id=100:90&action=')
+# 获取页面源码内容
+page_text = browser.page_source
+
+tree = etree.HTML(page_text)
+span_list = tree.xpath('//div[@class="movie-content"]/div[1]/div[1]/span[1]')
+for span in span_list:
+    name = span.xpath('.//a/text()')
+    print(name)
+time.sleep(5)
+# 退出
+browser.quit()
+    
+res:
+['肖申克的救赎']
+['霸王别姬']
+['控方证人']
+['伊丽莎白']
+['阿甘正传']
+['美丽人生']
+['辛德勒的名单']
+['茶馆']
+['控方证人']
+['十二怒汉(电视版)']
+['这个杀手不太冷']
+['千与千寻']
+['泰坦尼克号']
+['忠犬八公的故事']
+['十二怒汉']
+['泰坦尼克号 3D版']
+['背靠背,脸对脸']
+['灿烂人生']
+['横空出世']
+['遥望南方的童年']

简单操作

python
from selenium import webdriver
+from time import sleep
+
+bro = webdriver.Chrome(executable_path=r'D:\\install\\tools\\chromedriver\\chromedriver.exe')
+bro.get('https://www.taobao.com/')
+# 找到搜索框
+input_ = bro.find_element_by_id('q')
+# 输入值
+input_.send_keys('macbook')
+# 执行js
+bro.execute_script('window.scrollTo(0,document.body.scrollHeight)')
+sleep(2)
+# 点击搜索按钮
+btn = bro.find_element_by_xpath('//*[@id="J_TSearchForm"]/div[1]/button')
+btn.click()
+
+sleep(2)
+
+bro.get('https://www.baidu.com')
+sleep(1)
+# 后退
+bro.back()
+
+sleep(1)
+# 前进
+bro.forward()
+sleep(1)
+bro.quit()
from selenium import webdriver
+from time import sleep
+
+bro = webdriver.Chrome(executable_path=r'D:\\install\\tools\\chromedriver\\chromedriver.exe')
+bro.get('https://www.taobao.com/')
+# 找到搜索框
+input_ = bro.find_element_by_id('q')
+# 输入值
+input_.send_keys('macbook')
+# 执行js
+bro.execute_script('window.scrollTo(0,document.body.scrollHeight)')
+sleep(2)
+# 点击搜索按钮
+btn = bro.find_element_by_xpath('//*[@id="J_TSearchForm"]/div[1]/button')
+btn.click()
+
+sleep(2)
+
+bro.get('https://www.baidu.com')
+sleep(1)
+# 后退
+bro.back()
+
+sleep(1)
+# 前进
+bro.forward()
+sleep(1)
+bro.quit()

iframe和动作链

  • 如果定位的标签存在于iframe中,则必须使用switch_to.frame(id)
  • 动作链: from selenium.webdriver import ActionChains
    • 实例化: \`action = ActionChains(browser)
    • click_and_hold(div)长按点击操作
    • move_by_offset(x,y)
    • perform() 让动作链立即执行
    • action.release() 释放动作链

qq空间模拟登录

python
from selenium import webdriver
+import time
+
+url = 'https://qzone.qq.com/'
+browser = webdriver.Chrome()
+browser.get(url)
+browser.switch_to.frame('login_frame')
+btn = browser.find_element_by_id('switcher_plogin')
+btn.click()
+uname_input = browser.find_element_by_id('u')
+pwd_input = browser.find_element_by_id('p')
+uname_input.send_keys('1234')
+pwd_input.send_keys('123411234')
+login_btn = browser.find_element_by_id('login_button')
+login_btn.click()
+time.sleep(5)
+browser.quit()
from selenium import webdriver
+import time
+
+url = 'https://qzone.qq.com/'
+browser = webdriver.Chrome()
+browser.get(url)
+browser.switch_to.frame('login_frame')
+btn = browser.find_element_by_id('switcher_plogin')
+btn.click()
+uname_input = browser.find_element_by_id('u')
+pwd_input = browser.find_element_by_id('p')
+uname_input.send_keys('1234')
+pwd_input.send_keys('123411234')
+login_btn = browser.find_element_by_id('login_button')
+login_btn.click()
+time.sleep(5)
+browser.quit()

无头浏览器+规避检测

python
from selenium import webdriver
+from selenium.webdriver.chrome.options import Options
+from selenium.webdriver import ChromeOptions
+from time import sleep
+
+# 实现无可视化节目(无头浏览器)
+chrome_options = Options()
+chrome_options.add_argument('--headless')
+chrome_options.add_argument('--disable-gpu')
+# 实现规避检测
+option = ChromeOptions()
+option.add_experimental_option('excludeSwitches', ['enable-automation'])
+bro = webdriver.Chrome(chrome_options=chrome_options, options=option)
+bro.get('https://www.baidu.com')
+print(bro.page_source)
+sleep(2)
+bro.quit()
from selenium import webdriver
+from selenium.webdriver.chrome.options import Options
+from selenium.webdriver import ChromeOptions
+from time import sleep
+
+# 实现无可视化节目(无头浏览器)
+chrome_options = Options()
+chrome_options.add_argument('--headless')
+chrome_options.add_argument('--disable-gpu')
+# 实现规避检测
+option = ChromeOptions()
+option.add_experimental_option('excludeSwitches', ['enable-automation'])
+bro = webdriver.Chrome(chrome_options=chrome_options, options=option)
+bro.get('https://www.baidu.com')
+print(bro.page_source)
+sleep(2)
+bro.quit()

12306模拟登录

python
from selenium import webdriver
+from time import sleep
+from story.code import StoryClient
+from PIL import Image
+from selenium.webdriver import ActionChains
+from selenium.webdriver import ChromeOptions
+
+option = ChromeOptions()
+option.add_experimental_option('excludeSwitches', ['enable-automation'])
+# 访问12306
+url = 'https://www.12306.cn/index/index.html'
+bro = webdriver.Chrome(options=option)
+bro.get(url)
+script = 'Object.defineProperty(navigator,"webdriver",{get:()=>undefined,});'
+bro.execute_script(script)
+# 点击登录标签
+sleep(5)
+a_btn = bro.find_element_by_xpath('/html/body/div[2]/div/div[1]/div/div/ul/li[5]/a[1]')
+a_btn.click()
+sleep(2)
+# 点击账号登录按钮
+account_login = bro.find_element_by_xpath('/html/body/div[2]/div[2]/ul/li[2]/a')
+account_login.click()
+sleep(5)
+# 保存页面截图
+bro.save_screenshot('page.png')
+# 确定验证码图片对应的左上角和右下角的坐标,进行裁剪
+code_element = bro.find_element_by_id('J-loginImgArea')
+location = code_element.location  # 左上角坐标(x,y)
+size = code_element.size  # 验证码图片的长和宽
+rangle = (
+int(location['x']), int(location['y']), int(location['x'] + size['width']), int(location['y'] + size['height']))
+# 截取验证码图片
+image = Image.open('./page.png')
+frame = image.crop(rangle)
+print(rangle)
+frame.save('./code.png')
+# 超级鹰识别验证码
+client = StoryClient()
+im = open('./code.png', 'rb').read()
+res = client.post_pic(im, 9004)['pic_str']
+print(res)
+# 输入用户名和密码
+uname_input = bro.find_element_by_id('J-userName')
+pwd_input = bro.find_element_by_id('J-password')
+uname_input.send_keys('aaaaaaaaaaaa')
+pwd_input.send_keys('bbbbbbbbbbb')
+sleep(5)
+# 处理识别结果
+all_position_list = []  # 即将被点击的坐标
+if '|' in res:
+    list_1 = res.split('|')
+    count_1 = len(list_1)
+    for i in range(count_1):
+        xy_list = []
+        x = int(list_1[i].split(',')[0])
+        y = int(list_1[i].split(',')[1])
+        xy_list.append(x)
+        xy_list.append(y)
+        all_position_list.append(xy_list)
+else:
+    x = int(res.split(',')[0])
+    y = int(res.split(',')[1])
+    xy_list = []
+    xy_list.append(x)
+    xy_list.append(y)
+    all_position_list.append(xy_list)
+
+print(all_position_list)
+# 使用动作链点击验证码
+for l in all_position_list:
+    x = l[0]
+    y = l[1]
+    # 参照物是截取的验证码区域
+    ActionChains(bro).move_to_element_with_offset(code_element, x, y).click().perform()
+sleep(5)
+login_btn = bro.find_element_by_id('J-login')
+login_btn.click()
+sleep(2)
+#滑动验证码
+span = bro.find_element_by_xpath('//*[@id="nc_1_n1z"]')
+# 对div_tag进行滑动操作
+action = ActionChains(bro)
+action.click_and_hold(span).perform()
+action.drag_and_drop_by_offset(span, 400, 0).perform()
+action.release()
+
+sleep(10)
+bro.quit()
from selenium import webdriver
+from time import sleep
+from story.code import StoryClient
+from PIL import Image
+from selenium.webdriver import ActionChains
+from selenium.webdriver import ChromeOptions
+
+option = ChromeOptions()
+option.add_experimental_option('excludeSwitches', ['enable-automation'])
+# 访问12306
+url = 'https://www.12306.cn/index/index.html'
+bro = webdriver.Chrome(options=option)
+bro.get(url)
+script = 'Object.defineProperty(navigator,"webdriver",{get:()=>undefined,});'
+bro.execute_script(script)
+# 点击登录标签
+sleep(5)
+a_btn = bro.find_element_by_xpath('/html/body/div[2]/div/div[1]/div/div/ul/li[5]/a[1]')
+a_btn.click()
+sleep(2)
+# 点击账号登录按钮
+account_login = bro.find_element_by_xpath('/html/body/div[2]/div[2]/ul/li[2]/a')
+account_login.click()
+sleep(5)
+# 保存页面截图
+bro.save_screenshot('page.png')
+# 确定验证码图片对应的左上角和右下角的坐标,进行裁剪
+code_element = bro.find_element_by_id('J-loginImgArea')
+location = code_element.location  # 左上角坐标(x,y)
+size = code_element.size  # 验证码图片的长和宽
+rangle = (
+int(location['x']), int(location['y']), int(location['x'] + size['width']), int(location['y'] + size['height']))
+# 截取验证码图片
+image = Image.open('./page.png')
+frame = image.crop(rangle)
+print(rangle)
+frame.save('./code.png')
+# 超级鹰识别验证码
+client = StoryClient()
+im = open('./code.png', 'rb').read()
+res = client.post_pic(im, 9004)['pic_str']
+print(res)
+# 输入用户名和密码
+uname_input = bro.find_element_by_id('J-userName')
+pwd_input = bro.find_element_by_id('J-password')
+uname_input.send_keys('aaaaaaaaaaaa')
+pwd_input.send_keys('bbbbbbbbbbb')
+sleep(5)
+# 处理识别结果
+all_position_list = []  # 即将被点击的坐标
+if '|' in res:
+    list_1 = res.split('|')
+    count_1 = len(list_1)
+    for i in range(count_1):
+        xy_list = []
+        x = int(list_1[i].split(',')[0])
+        y = int(list_1[i].split(',')[1])
+        xy_list.append(x)
+        xy_list.append(y)
+        all_position_list.append(xy_list)
+else:
+    x = int(res.split(',')[0])
+    y = int(res.split(',')[1])
+    xy_list = []
+    xy_list.append(x)
+    xy_list.append(y)
+    all_position_list.append(xy_list)
+
+print(all_position_list)
+# 使用动作链点击验证码
+for l in all_position_list:
+    x = l[0]
+    y = l[1]
+    # 参照物是截取的验证码区域
+    ActionChains(bro).move_to_element_with_offset(code_element, x, y).click().perform()
+sleep(5)
+login_btn = bro.find_element_by_id('J-login')
+login_btn.click()
+sleep(2)
+#滑动验证码
+span = bro.find_element_by_xpath('//*[@id="nc_1_n1z"]')
+# 对div_tag进行滑动操作
+action = ActionChains(bro)
+action.click_and_hold(span).perform()
+action.drag_and_drop_by_offset(span, 400, 0).perform()
+action.release()
+
+sleep(10)
+bro.quit()

最后一步滑块验证码无法通过,还需要优化

`,22),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const _=s(p,[["render",t]]);export{m as __pageData,_ as default}; diff --git "a/assets/python_crawler_selenium\346\250\241\345\235\227.md.5446e227.lean.js" "b/assets/python_crawler_selenium\346\250\241\345\235\227.md.5446e227.lean.js" new file mode 100644 index 000000000..499e1f6be --- /dev/null +++ "b/assets/python_crawler_selenium\346\250\241\345\235\227.md.5446e227.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const m=JSON.parse('{"title":"selenium模块","description":"","frontmatter":{},"headers":[],"relativePath":"python/crawler/selenium模块.md","filePath":"python/crawler/selenium模块.md","lastUpdated":1708510102000}'),p={name:"python/crawler/selenium模块.md"},o=l("",22),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const _=s(p,[["render",t]]);export{m as __pageData,_ as default}; diff --git "a/assets/python_crawler_\345\210\206\345\270\203\345\274\217\347\210\254\350\231\253\345\222\214\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253.md.1722f14e.js" "b/assets/python_crawler_\345\210\206\345\270\203\345\274\217\347\210\254\350\231\253\345\222\214\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253.md.1722f14e.js" new file mode 100644 index 000000000..2955f5389 --- /dev/null +++ "b/assets/python_crawler_\345\210\206\345\270\203\345\274\217\347\210\254\350\231\253\345\222\214\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253.md.1722f14e.js" @@ -0,0 +1,15 @@ +import{_ as s,o as a,c as l,Q as e}from"./chunks/framework.dc1f4608.js";const E=JSON.parse('{"title":"分布式爬虫和增量式爬虫","description":"","frontmatter":{},"headers":[],"relativePath":"python/crawler/分布式爬虫和增量式爬虫.md","filePath":"python/crawler/分布式爬虫和增量式爬虫.md","lastUpdated":1708510102000}'),p={name:"python/crawler/分布式爬虫和增量式爬虫.md"},n=e(`

分布式爬虫和增量式爬虫

分布式爬虫

概念

搭建集群,让集群对一组资源进行联合爬取

作用

提升爬取数据效率

实现

  • 安装scrapy-redis组件

  • 原生scrapy无法实现分布式爬虫

    • 调度器不可被集群共享
    • 管道不可被集群共享
  • scrapy-redis组件作用

    • 给原生scrapy提供被共享的调度器和管道
  • 实现流程

    • 创建工程

    • 创建一个基于CrawlSpider的爬虫

    • 修改爬虫文件

      • 爬虫文件添加from scrapy_redis.spiders import RedisCrawlSpider

      • 注释掉start_urls和allowed_domains

      • 新增属性redis_key = 'story',代表被共享的调度器队列的名称

      • 编写数据解析操作

      • 将当前爬虫类的父类修改成RedisCrawlSpider

    • settings配置新增

      • 指定使用可以共享的管道

        python
        ITEM_PIPELINES = {
        +    'scrapy_redis.pipelines.RedisPipeline' : 400
        +}
        ITEM_PIPELINES = {
        +    'scrapy_redis.pipelines.RedisPipeline' : 400
        +}
      • 指定可以共享的调度器

        python
        # 增加一个去重容器的配置,使用redis的set来存储请求数据,实现请求去重持久化
        +DUPEFLTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
        +# 使用scrapy-redis组件自己的调度器
        +SCHEDULER = "scrapy_redis.scheduler.Scheduler"
        +# 配置调度器是否要持久化-爬虫结束要不要清空redis请求队列和去重的set
        +SCHEDULER_PERSIST = True
        # 增加一个去重容器的配置,使用redis的set来存储请求数据,实现请求去重持久化
        +DUPEFLTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
        +# 使用scrapy-redis组件自己的调度器
        +SCHEDULER = "scrapy_redis.scheduler.Scheduler"
        +# 配置调度器是否要持久化-爬虫结束要不要清空redis请求队列和去重的set
        +SCHEDULER_PERSIST = True
    • 配置redis的配置文件

      • bind 127.0.0.1注释掉
      • 关闭保护模式:protected-mode 改为no
    • 启动redis

    • 启动工程,进入到爬虫文件的目录后scrapy runspider xxx

    • 向调度器队列中放入起始url

      • lpush redis_key url

增量式爬虫

概念

检测网站数据更新情况,只会爬取网站最新更新的数据

实现

  • 指定起始url
  • 基于CrawlSpider获取其他页码链接
  • 基于Rule对其他页码进行请求
  • 从每一个页码对应源码中解析出详情页的url
  • ==检测详情页的url是否被请求过(redis/mysql)==
  • 对详情页发起请求
  • 持久化存储
`,13),o=[n];function i(r,t,c,d,y,u){return a(),l("div",null,o)}const _=s(p,[["render",i]]);export{E as __pageData,_ as default}; diff --git "a/assets/python_crawler_\345\210\206\345\270\203\345\274\217\347\210\254\350\231\253\345\222\214\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253.md.1722f14e.lean.js" "b/assets/python_crawler_\345\210\206\345\270\203\345\274\217\347\210\254\350\231\253\345\222\214\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253.md.1722f14e.lean.js" new file mode 100644 index 000000000..a056a95b1 --- /dev/null +++ "b/assets/python_crawler_\345\210\206\345\270\203\345\274\217\347\210\254\350\231\253\345\222\214\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253.md.1722f14e.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as l,Q as e}from"./chunks/framework.dc1f4608.js";const E=JSON.parse('{"title":"分布式爬虫和增量式爬虫","description":"","frontmatter":{},"headers":[],"relativePath":"python/crawler/分布式爬虫和增量式爬虫.md","filePath":"python/crawler/分布式爬虫和增量式爬虫.md","lastUpdated":1708510102000}'),p={name:"python/crawler/分布式爬虫和增量式爬虫.md"},n=e("",13),o=[n];function i(r,t,c,d,y,u){return a(),l("div",null,o)}const _=s(p,[["render",i]]);export{E as __pageData,_ as default}; diff --git "a/assets/python_crawler_\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253\345\256\236\350\267\265\346\241\210\344\276\213 \344\270\213\350\275\275\346\214\207\345\256\232b\347\253\231up\344\270\273\347\232\204\346\211\200\346\234\211\344\275\234\345\223\201.md.6ec9ffd2.js" "b/assets/python_crawler_\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253\345\256\236\350\267\265\346\241\210\344\276\213 \344\270\213\350\275\275\346\214\207\345\256\232b\347\253\231up\344\270\273\347\232\204\346\211\200\346\234\211\344\275\234\345\223\201.md.6ec9ffd2.js" new file mode 100644 index 000000000..06b042c39 --- /dev/null +++ "b/assets/python_crawler_\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253\345\256\236\350\267\265\346\241\210\344\276\213 \344\270\213\350\275\275\346\214\207\345\256\232b\347\253\231up\344\270\273\347\232\204\346\211\200\346\234\211\344\275\234\345\223\201.md.6ec9ffd2.js" @@ -0,0 +1,635 @@ +import{_ as s,o as n,c as a,Q as p}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"增量式爬虫实践案例 下载指定b站up主的所有作品","description":"","frontmatter":{},"headers":[],"relativePath":"python/crawler/增量式爬虫实践案例 下载指定b站up主的所有作品.md","filePath":"python/crawler/增量式爬虫实践案例 下载指定b站up主的所有作品.md","lastUpdated":1708510102000}'),l={name:"python/crawler/增量式爬虫实践案例 下载指定b站up主的所有作品.md"},o=p(`

增量式爬虫实践案例 下载指定b站up主的所有作品

背景

增量式爬取指定的up主的所有投稿作品,即实现一个增量式爬虫。

这次示范的up主是个妹子😏kototo使用了scrapy框架,主要是为了练手,不使用框架反而会更简单一些。

python模块:scrapy、selenium、requests、pymysql

其他环境:ffmpeg、mysql

创建一个项目并创建爬虫

bash
scrapy startproject kototo
+cd kototo
+scrapy genspider kototo bilibili.com
scrapy startproject kototo
+cd kototo
+scrapy genspider kototo bilibili.com

爬虫类

python
import scrapy
+from selenium import webdriver
+import re
+import json
+import requests
+import os
+from kototo.items import KototoItem
+import pymysql
+
+
+class KototoSpider(scrapy.Spider):
+    name = 'kototo'
+    start_urls = []
+
+    def __init__(self):
+        """
+        构造器,主要初始化了selenium对象并实现无头浏览器,以及
+        初始化需要爬取的url地址,因为b站的翻页是js实现的,所以要手动处理一下
+        """
+        super().__init__()
+        # 构造无头浏览器
+        from selenium.webdriver.chrome.options import Options
+        chrome_options = Options()
+        chrome_options.add_argument('--headless')
+        chrome_options.add_argument('--disable-gpu')
+        self.bro = webdriver.Chrome(chrome_options=chrome_options)
+        # 指定的up主的投稿页面,可以提到外面使用input输入
+        space_url = 'https://space.bilibili.com/17485141/video'
+        # 初始化需要爬取的列表页
+        self.init_start_urls(self.start_urls, space_url)
+        # 创建桌面文件夹
+        self.desktop_path = os.path.join(os.path.expanduser('~'), 'Desktop\\\\' + self.name + '\\\\')
+        if not os.path.exists(self.desktop_path):
+            os.mkdir(self.desktop_path)
+
+    def parse(self, response):
+        """
+        解析方法,解析列表页的视频li,拿到标题和详情页,然后主动请求详情页
+        :param response: 
+        :return: 
+        """
+        li_list = response.xpath('//*[@id="submit-video-list"]/ul[2]/li')
+        for li in li_list:
+            print(li.xpath('./a[2]/@title').extract_first())
+            print(detail_url := 'https://' + li.xpath('./a[2]/@href').extract_first()[2:])
+            yield scrapy.Request(url=detail_url, callback=self.parse_detail)
+
+    def parse_detail(self, response):
+        """
+        增量爬取: 解析详情页的音视频地址并交给管道处理
+        使用mysql实现
+        :param response: 
+        :return: 
+        """
+        title = response.xpath('//*[@id="viewbox_report"]/h1/@title').extract_first()
+        # 替换掉视频名称中无法用在文件名中或会导致cmd命令出错的字符
+        title = title.replace('-', '').replace(' ', '').replace('/', '').replace('|', '')
+        play_info_list = self.get_play_info(response)
+        # 这里使用mysql的唯一索引实现增量爬取,如果是服务器上跑也可以用redis
+        if self.insert_info(title, play_info_list[1]):
+            video_temp_path = (self.desktop_path + title + '_temp.mp4').replace('-', '')
+            video_path = self.desktop_path + title + '.mp4'
+            audio_path = self.desktop_path + title + '.mp3'
+            item = KototoItem()
+            item['video_url'] = play_info_list[0]
+            item['audio_url'] = play_info_list[1]
+            item['video_path'] = video_path
+            item['audio_path'] = audio_path
+            item['video_temp_path'] = video_temp_path
+            yield item
+        else:
+            print(title + ': 已经下载过了!')
+
+    def insert_info(self, vtitle, vurl):
+        """
+        mysql持久化存储爬取过的视频内容信息
+        :param vtitle: 标题
+        :param vurl: 视频链接
+        :return: 
+        """
+        with Mysql() as conn:
+            cursor = conn.cursor(pymysql.cursors.DictCursor)
+            try:
+                sql = 'insert into tb_kototo(title,url) values("%s","%s")' % (vtitle, vurl)
+                res = cursor.execute(sql)
+                conn.commit()
+                if res == 1:
+                    return True
+                else:
+                    return False
+            except:
+                return False
+
+    def get_play_info(self, resp):
+        """
+        解析详情页的源代码,提取其中的视频和文件真实地址
+        :param resp: 
+        :return: 
+        """
+        json_data = json.loads(re.findall('<script>window\\.__playinfo__=(.*?)</script>', resp.text)[0])
+        # 拿到视频和音频的真实链接地址
+        video_url = json_data['data']['dash']['video'][0]['backupUrl'][0]
+        audio_url = json_data['data']['dash']['audio'][0]['backupUrl'][0]
+        return video_url, audio_url
+
+    def init_start_urls(self, url_list, person_page):
+        """
+        初始化需要爬取的列表页,由于b站使用js翻页,无法在源码中找到翻页地址,
+        需要自己手动实现解析翻页url的操作
+        :param url_list: 
+        :param person_page: 
+        :return: 
+        """
+        mid = re.findall('https://space.bilibili.com/(.*?)/video\\w*', person_page)[0]
+        url = 'https://api.bilibili.com/x/space/arc/search?mid=' + mid + '&ps=30&tid=0&pn=1&keyword=&order=pubdate&jsonp=jsonp'
+        headers = {
+            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36',
+            'Referer': 'https://www.bilibili.com'
+        }
+        json_data = requests.get(url=url, headers=headers).json()
+        total_count = json_data['data']['page']['count']
+        page_size = json_data['data']['page']['ps']
+        if total_count <= page_size:
+            page_count = 1
+        elif total_count % page_size == 0:
+            page_count = total_count / page_size
+        else:
+            page_count = total_count // page_size + 1
+
+        url_template = 'https://space.bilibili.com/' + mid + '/video?tid=0&page=' + '%d' + '&keyword=&order=pubdate'
+        for i in range(page_count):
+            page_no = i + 1
+            url_list.append(url_template % page_no)
+
+    def closed(self, spider):
+        """
+        爬虫结束关闭selenium窗口
+        :param spider: 
+        :return: 
+        """
+        self.bro.quit()
+
+
+class Mysql(object):
+    def __enter__(self):
+        self.connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+        return self.connection
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.connection.close()
import scrapy
+from selenium import webdriver
+import re
+import json
+import requests
+import os
+from kototo.items import KototoItem
+import pymysql
+
+
+class KototoSpider(scrapy.Spider):
+    name = 'kototo'
+    start_urls = []
+
+    def __init__(self):
+        """
+        构造器,主要初始化了selenium对象并实现无头浏览器,以及
+        初始化需要爬取的url地址,因为b站的翻页是js实现的,所以要手动处理一下
+        """
+        super().__init__()
+        # 构造无头浏览器
+        from selenium.webdriver.chrome.options import Options
+        chrome_options = Options()
+        chrome_options.add_argument('--headless')
+        chrome_options.add_argument('--disable-gpu')
+        self.bro = webdriver.Chrome(chrome_options=chrome_options)
+        # 指定的up主的投稿页面,可以提到外面使用input输入
+        space_url = 'https://space.bilibili.com/17485141/video'
+        # 初始化需要爬取的列表页
+        self.init_start_urls(self.start_urls, space_url)
+        # 创建桌面文件夹
+        self.desktop_path = os.path.join(os.path.expanduser('~'), 'Desktop\\\\' + self.name + '\\\\')
+        if not os.path.exists(self.desktop_path):
+            os.mkdir(self.desktop_path)
+
+    def parse(self, response):
+        """
+        解析方法,解析列表页的视频li,拿到标题和详情页,然后主动请求详情页
+        :param response: 
+        :return: 
+        """
+        li_list = response.xpath('//*[@id="submit-video-list"]/ul[2]/li')
+        for li in li_list:
+            print(li.xpath('./a[2]/@title').extract_first())
+            print(detail_url := 'https://' + li.xpath('./a[2]/@href').extract_first()[2:])
+            yield scrapy.Request(url=detail_url, callback=self.parse_detail)
+
+    def parse_detail(self, response):
+        """
+        增量爬取: 解析详情页的音视频地址并交给管道处理
+        使用mysql实现
+        :param response: 
+        :return: 
+        """
+        title = response.xpath('//*[@id="viewbox_report"]/h1/@title').extract_first()
+        # 替换掉视频名称中无法用在文件名中或会导致cmd命令出错的字符
+        title = title.replace('-', '').replace(' ', '').replace('/', '').replace('|', '')
+        play_info_list = self.get_play_info(response)
+        # 这里使用mysql的唯一索引实现增量爬取,如果是服务器上跑也可以用redis
+        if self.insert_info(title, play_info_list[1]):
+            video_temp_path = (self.desktop_path + title + '_temp.mp4').replace('-', '')
+            video_path = self.desktop_path + title + '.mp4'
+            audio_path = self.desktop_path + title + '.mp3'
+            item = KototoItem()
+            item['video_url'] = play_info_list[0]
+            item['audio_url'] = play_info_list[1]
+            item['video_path'] = video_path
+            item['audio_path'] = audio_path
+            item['video_temp_path'] = video_temp_path
+            yield item
+        else:
+            print(title + ': 已经下载过了!')
+
+    def insert_info(self, vtitle, vurl):
+        """
+        mysql持久化存储爬取过的视频内容信息
+        :param vtitle: 标题
+        :param vurl: 视频链接
+        :return: 
+        """
+        with Mysql() as conn:
+            cursor = conn.cursor(pymysql.cursors.DictCursor)
+            try:
+                sql = 'insert into tb_kototo(title,url) values("%s","%s")' % (vtitle, vurl)
+                res = cursor.execute(sql)
+                conn.commit()
+                if res == 1:
+                    return True
+                else:
+                    return False
+            except:
+                return False
+
+    def get_play_info(self, resp):
+        """
+        解析详情页的源代码,提取其中的视频和文件真实地址
+        :param resp: 
+        :return: 
+        """
+        json_data = json.loads(re.findall('<script>window\\.__playinfo__=(.*?)</script>', resp.text)[0])
+        # 拿到视频和音频的真实链接地址
+        video_url = json_data['data']['dash']['video'][0]['backupUrl'][0]
+        audio_url = json_data['data']['dash']['audio'][0]['backupUrl'][0]
+        return video_url, audio_url
+
+    def init_start_urls(self, url_list, person_page):
+        """
+        初始化需要爬取的列表页,由于b站使用js翻页,无法在源码中找到翻页地址,
+        需要自己手动实现解析翻页url的操作
+        :param url_list: 
+        :param person_page: 
+        :return: 
+        """
+        mid = re.findall('https://space.bilibili.com/(.*?)/video\\w*', person_page)[0]
+        url = 'https://api.bilibili.com/x/space/arc/search?mid=' + mid + '&ps=30&tid=0&pn=1&keyword=&order=pubdate&jsonp=jsonp'
+        headers = {
+            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36',
+            'Referer': 'https://www.bilibili.com'
+        }
+        json_data = requests.get(url=url, headers=headers).json()
+        total_count = json_data['data']['page']['count']
+        page_size = json_data['data']['page']['ps']
+        if total_count <= page_size:
+            page_count = 1
+        elif total_count % page_size == 0:
+            page_count = total_count / page_size
+        else:
+            page_count = total_count // page_size + 1
+
+        url_template = 'https://space.bilibili.com/' + mid + '/video?tid=0&page=' + '%d' + '&keyword=&order=pubdate'
+        for i in range(page_count):
+            page_no = i + 1
+            url_list.append(url_template % page_no)
+
+    def closed(self, spider):
+        """
+        爬虫结束关闭selenium窗口
+        :param spider: 
+        :return: 
+        """
+        self.bro.quit()
+
+
+class Mysql(object):
+    def __enter__(self):
+        self.connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+        return self.connection
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.connection.close()

下载中间件

python
# Define here the models for your spider middleware
+#
+# See documentation in:
+# https://docs.scrapy.org/en/latest/topics/spider-middleware.html
+
+from scrapy import signals
+
+# useful for handling different item types with a single interface
+from itemadapter import is_item, ItemAdapter
+
+
+class KototoSpiderMiddleware:
+    # Not all methods need to be defined. If a method is not defined,
+    # scrapy acts as if the spider middleware does not modify the
+    # passed objects.
+
+    @classmethod
+    def from_crawler(cls, crawler):
+        # This method is used by Scrapy to create your spiders.
+        s = cls()
+        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
+        return s
+
+    def process_spider_input(self, response, spider):
+        # Called for each response that goes through the spider
+        # middleware and into the spider.
+
+        # Should return None or raise an exception.
+        return None
+
+    def process_spider_output(self, response, result, spider):
+        # Called with the results returned from the Spider, after
+        # it has processed the response.
+
+        # Must return an iterable of Request, or item objects.
+        for i in result:
+            yield i
+
+    def process_spider_exception(self, response, exception, spider):
+        # Called when a spider or process_spider_input() method
+        # (from other spider middleware) raises an exception.
+
+        # Should return either None or an iterable of Request or item objects.
+        pass
+
+    def process_start_requests(self, start_requests, spider):
+        # Called with the start requests of the spider, and works
+        # similarly to the process_spider_output() method, except
+        # that it doesn’t have a response associated.
+
+        # Must return only requests (not items).
+        for r in start_requests:
+            yield r
+
+    def spider_opened(self, spider):
+        spider.logger.info('Spider opened: %s' % spider.name)
+
+
+class KototoDownloaderMiddleware:
+
+    def process_request(self, request, spider):
+        return None
+
+    def process_response(self, request, response, spider):
+        """
+        篡改列表页的响应数据:
+            视频列表是通过ajax请求动态加载的,因此要通过selenium去加载这部分数据
+            并篡改响应内容
+        :param request: 
+        :param response: 
+        :param spider: 
+        :return: 
+        """
+        urls = spider.start_urls
+        bro = spider.bro
+        from scrapy.http import HtmlResponse
+        from time import sleep
+        if request.url in urls:
+            """
+            如果是列表页就进行响应篡改操作
+            """
+            bro.get(request.url)
+            sleep(3)
+            page_data = bro.page_source
+            new_response = HtmlResponse(url=request.url, body=page_data, encoding='utf-8', request=request)
+            # 返回篡改过的响应对象
+            return new_response
+        return response
+
+    def process_exception(self, request, exception, spider):
+        pass
# Define here the models for your spider middleware
+#
+# See documentation in:
+# https://docs.scrapy.org/en/latest/topics/spider-middleware.html
+
+from scrapy import signals
+
+# useful for handling different item types with a single interface
+from itemadapter import is_item, ItemAdapter
+
+
+class KototoSpiderMiddleware:
+    # Not all methods need to be defined. If a method is not defined,
+    # scrapy acts as if the spider middleware does not modify the
+    # passed objects.
+
+    @classmethod
+    def from_crawler(cls, crawler):
+        # This method is used by Scrapy to create your spiders.
+        s = cls()
+        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
+        return s
+
+    def process_spider_input(self, response, spider):
+        # Called for each response that goes through the spider
+        # middleware and into the spider.
+
+        # Should return None or raise an exception.
+        return None
+
+    def process_spider_output(self, response, result, spider):
+        # Called with the results returned from the Spider, after
+        # it has processed the response.
+
+        # Must return an iterable of Request, or item objects.
+        for i in result:
+            yield i
+
+    def process_spider_exception(self, response, exception, spider):
+        # Called when a spider or process_spider_input() method
+        # (from other spider middleware) raises an exception.
+
+        # Should return either None or an iterable of Request or item objects.
+        pass
+
+    def process_start_requests(self, start_requests, spider):
+        # Called with the start requests of the spider, and works
+        # similarly to the process_spider_output() method, except
+        # that it doesn’t have a response associated.
+
+        # Must return only requests (not items).
+        for r in start_requests:
+            yield r
+
+    def spider_opened(self, spider):
+        spider.logger.info('Spider opened: %s' % spider.name)
+
+
+class KototoDownloaderMiddleware:
+
+    def process_request(self, request, spider):
+        return None
+
+    def process_response(self, request, response, spider):
+        """
+        篡改列表页的响应数据:
+            视频列表是通过ajax请求动态加载的,因此要通过selenium去加载这部分数据
+            并篡改响应内容
+        :param request: 
+        :param response: 
+        :param spider: 
+        :return: 
+        """
+        urls = spider.start_urls
+        bro = spider.bro
+        from scrapy.http import HtmlResponse
+        from time import sleep
+        if request.url in urls:
+            """
+            如果是列表页就进行响应篡改操作
+            """
+            bro.get(request.url)
+            sleep(3)
+            page_data = bro.page_source
+            new_response = HtmlResponse(url=request.url, body=page_data, encoding='utf-8', request=request)
+            # 返回篡改过的响应对象
+            return new_response
+        return response
+
+    def process_exception(self, request, exception, spider):
+        pass

Item

python
import scrapy
+
+
+class KototoItem(scrapy.Item):
+    video_path = scrapy.Field()
+    video_url = scrapy.Field()
+    audio_path = scrapy.Field()
+    audio_url = scrapy.Field()
+    video_temp_path = scrapy.Field()
import scrapy
+
+
+class KototoItem(scrapy.Item):
+    video_path = scrapy.Field()
+    video_url = scrapy.Field()
+    audio_path = scrapy.Field()
+    audio_url = scrapy.Field()
+    video_temp_path = scrapy.Field()

Pipeline

python
import requests
+import os
+
+headers = {
+    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36',
+    'Referer': 'https://www.bilibili.com'
+}
+
+
+class KototoPipeline(object):
+    def process_item(self, item, spider):
+        video = item['video_url']
+        audio = item['audio_url']
+        video_temp_path = item['video_temp_path']
+        audio_path = item['audio_path']
+        video_data = requests.get(url=video, headers=headers).content
+        audio_data = requests.get(url=audio, headers=headers).content
+        with open(video_temp_path, 'wb') as f:
+            f.write(video_data)
+        with open(audio_path, 'wb') as f:
+            f.write(audio_data)
+        return item
+
+
+class MergePipeline(object):
+    """
+    删除临时文件
+    """
+
+    def process_item(self, item, spider):
+        video_temp_path = item['video_temp_path']
+        audio_path = item['audio_path']
+        video_path = item['video_path']
+        cmd = 'ffmpeg -y -i ' + video_temp_path + ' -i ' \\
+              + audio_path + ' -c:v copy -c:a aac -strict experimental ' + video_path
+        print(cmd)
+        # subprocess.Popen(cmd, shell=True)
+        os.system(cmd)
+        os.remove(video_temp_path)
+        os.remove(audio_path)
+        print(video_path, '下载完成')
+        return item
import requests
+import os
+
+headers = {
+    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36',
+    'Referer': 'https://www.bilibili.com'
+}
+
+
+class KototoPipeline(object):
+    def process_item(self, item, spider):
+        video = item['video_url']
+        audio = item['audio_url']
+        video_temp_path = item['video_temp_path']
+        audio_path = item['audio_path']
+        video_data = requests.get(url=video, headers=headers).content
+        audio_data = requests.get(url=audio, headers=headers).content
+        with open(video_temp_path, 'wb') as f:
+            f.write(video_data)
+        with open(audio_path, 'wb') as f:
+            f.write(audio_data)
+        return item
+
+
+class MergePipeline(object):
+    """
+    删除临时文件
+    """
+
+    def process_item(self, item, spider):
+        video_temp_path = item['video_temp_path']
+        audio_path = item['audio_path']
+        video_path = item['video_path']
+        cmd = 'ffmpeg -y -i ' + video_temp_path + ' -i ' \\
+              + audio_path + ' -c:v copy -c:a aac -strict experimental ' + video_path
+        print(cmd)
+        # subprocess.Popen(cmd, shell=True)
+        os.system(cmd)
+        os.remove(video_temp_path)
+        os.remove(audio_path)
+        print(video_path, '下载完成')
+        return item

settings

python
BOT_NAME = 'kototo'
+
+SPIDER_MODULES = ['kototo.spiders']
+NEWSPIDER_MODULE = 'kototo.spiders'
+
+
+USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+
+
+ROBOTSTXT_OBEY = False
+
+
+DEFAULT_REQUEST_HEADERS = {
+  'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+  'Referer': 'https://space.bilibili.com/17485141/video',
+  'Origin':  'https://space.bilibili.com'
+}
+FILES_STORE = './files'
+DOWNLOADER_MIDDLEWARES = {
+   'kototo.middlewares.KototoDownloaderMiddleware': 543,
+}
+
+ITEM_PIPELINES = {
+    # 下载
+   'kototo.pipelines.KototoPipeline': 1,
+   # 合并
+    'kototo.pipelines.MergePipeline': 2,
+}
BOT_NAME = 'kototo'
+
+SPIDER_MODULES = ['kototo.spiders']
+NEWSPIDER_MODULE = 'kototo.spiders'
+
+
+USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+
+
+ROBOTSTXT_OBEY = False
+
+
+DEFAULT_REQUEST_HEADERS = {
+  'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+  'Referer': 'https://space.bilibili.com/17485141/video',
+  'Origin':  'https://space.bilibili.com'
+}
+FILES_STORE = './files'
+DOWNLOADER_MIDDLEWARES = {
+   'kototo.middlewares.KototoDownloaderMiddleware': 543,
+}
+
+ITEM_PIPELINES = {
+    # 下载
+   'kototo.pipelines.KototoPipeline': 1,
+   # 合并
+    'kototo.pipelines.MergePipeline': 2,
+}

启动

  • 命令启动:scrapy crawl kototo

  • 配置pycharm启动(推荐)

    image-20210503002914982

下载结果

image-20210503003351357

mysql

image-20210503003551035

再次尝试下载时

image-20210503003617326

已经爬取过的资源会提示已经下载过,只会处理更新的内容。

`,27),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const _=s(l,[["render",t]]);export{u as __pageData,_ as default}; diff --git "a/assets/python_crawler_\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253\345\256\236\350\267\265\346\241\210\344\276\213 \344\270\213\350\275\275\346\214\207\345\256\232b\347\253\231up\344\270\273\347\232\204\346\211\200\346\234\211\344\275\234\345\223\201.md.6ec9ffd2.lean.js" "b/assets/python_crawler_\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253\345\256\236\350\267\265\346\241\210\344\276\213 \344\270\213\350\275\275\346\214\207\345\256\232b\347\253\231up\344\270\273\347\232\204\346\211\200\346\234\211\344\275\234\345\223\201.md.6ec9ffd2.lean.js" new file mode 100644 index 000000000..7715eb763 --- /dev/null +++ "b/assets/python_crawler_\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253\345\256\236\350\267\265\346\241\210\344\276\213 \344\270\213\350\275\275\346\214\207\345\256\232b\347\253\231up\344\270\273\347\232\204\346\211\200\346\234\211\344\275\234\345\223\201.md.6ec9ffd2.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as p}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"增量式爬虫实践案例 下载指定b站up主的所有作品","description":"","frontmatter":{},"headers":[],"relativePath":"python/crawler/增量式爬虫实践案例 下载指定b站up主的所有作品.md","filePath":"python/crawler/增量式爬虫实践案例 下载指定b站up主的所有作品.md","lastUpdated":1708510102000}'),l={name:"python/crawler/增量式爬虫实践案例 下载指定b站up主的所有作品.md"},o=p("",27),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const _=s(l,[["render",t]]);export{u as __pageData,_ as default}; diff --git "a/assets/python_crawler_\345\244\232\347\272\277\347\250\213\347\210\254\345\217\226\346\242\250\350\247\206\351\242\221\347\275\221\347\253\231\347\232\204\347\203\255\351\227\250\350\247\206\351\242\221.md.0cea7349.js" "b/assets/python_crawler_\345\244\232\347\272\277\347\250\213\347\210\254\345\217\226\346\242\250\350\247\206\351\242\221\347\275\221\347\253\231\347\232\204\347\203\255\351\227\250\350\247\206\351\242\221.md.0cea7349.js" new file mode 100644 index 000000000..8045fc3c4 --- /dev/null +++ "b/assets/python_crawler_\345\244\232\347\272\277\347\250\213\347\210\254\345\217\226\346\242\250\350\247\206\351\242\221\347\275\221\347\253\231\347\232\204\347\203\255\351\227\250\350\247\206\351\242\221.md.0cea7349.js" @@ -0,0 +1,151 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const _=JSON.parse('{"title":"多线程爬取梨视频网站的热门视频","description":"","frontmatter":{},"headers":[],"relativePath":"python/crawler/多线程爬取梨视频网站的热门视频.md","filePath":"python/crawler/多线程爬取梨视频网站的热门视频.md","lastUpdated":1708510102000}'),p={name:"python/crawler/多线程爬取梨视频网站的热门视频.md"},o=l(`

多线程爬取梨视频网站的热门视频

python
from multiprocessing.dummy import Pool
+import requests
+from lxml import etree
+import random
+import os
+
+
+# 体育分类视频url地址
+url = 'https://www.pearvideo.com/category_9'
+headers = {
+    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+}
+
+
+def get_videos():
+    page_text = requests.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    li_list = tree.xpath('//ul[@id="listvideoListUl"]/li')
+    if not os.path.exists('./video'):
+        os.mkdir('./video')
+    # 存储所有的视频真实地址和名称信息
+    video_url_list = []
+    for li in li_list:
+        # 视频id
+        video_id = li.xpath('.//a/@href')[0].split('_')[1]
+        # 视频名称
+        name = li.xpath('.//div[@class="vervideo-title"]/text()')[0] + '.mp4'
+        # 梨视频的video标签是动态加载的,通过请求抓包获取到的ajax地址
+        ajax_url = 'https://www.pearvideo.com/videoStatus.jsp'
+        query_param = {
+            'contId': video_id,
+            'mrd': str(random.random())
+        }
+        # 梨视频有Referer 防盗链验证
+        # 需要在普通的ua伪装中加入Referer请求头,否则会一直提示文章已下线
+        ajax_headers = {
+            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
+            'Referer': 'https://www.pearvideo.com/video_' + video_id
+        }
+        json_obj = requests.get(url=ajax_url, headers=ajax_headers, params=query_param).json()
+        # 响应的地址:https://video.pearvideo.com/mp4/adshort/20210419/1618849825266-15658816_adpkg-ad_hd.mp4
+        # 实际的地址:https://video.pearvideo.com/mp4/adshort/20210419/cont-1727112-15658816_adpkg-ad_hd.mp4
+        # 实际地址中cont-后是视频id 因此要把这串字符串处理掉
+        temp_url = json_obj['videoInfo']['videos']['srcUrl']
+        last_index = temp_url.rfind('/')
+        # 最后一个/前的内容 https://video.pearvideo.com/mp4/adshort/20210419
+        real_video_url = temp_url[:last_index]
+        # 最后一个/后的内容根据-切片(不包含) 1618849825266-15658816_adpkg-ad_hd.mp4
+        str_list = temp_url[last_index + 1:].split('-')
+        for i in range(0, len(str_list)):
+            if i == 0:
+                real_video_url = real_video_url + '/cont-' + video_id + '-'
+            elif i == len(str_list) - 1:
+                real_video_url = real_video_url + str_list[i]
+            else:
+                real_video_url = real_video_url + str_list[i] + '-'
+        # 字典存储视频信息
+        video_dict = {'name': name, "url": real_video_url}
+        video_url_list.append(video_dict)
+    return video_url_list
+
+
+# io操作较耗时,采用多线程进行
+def download_video(dict):
+    video_name = dict['name']
+    video_url = dict['url']
+    video_stream = requests.get(url=video_url, headers=headers).content
+    with open('./video/' + video_name, 'wb') as f:
+        f.write(video_stream)
+        print(f'============={video_name}下载完毕===============')
+
+
+if __name__ == '__main__':
+    # 多线程执行下载任务
+    pool = Pool(4)
+    pool.map(download_video, get_videos())
from multiprocessing.dummy import Pool
+import requests
+from lxml import etree
+import random
+import os
+
+
+# 体育分类视频url地址
+url = 'https://www.pearvideo.com/category_9'
+headers = {
+    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+}
+
+
+def get_videos():
+    page_text = requests.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    li_list = tree.xpath('//ul[@id="listvideoListUl"]/li')
+    if not os.path.exists('./video'):
+        os.mkdir('./video')
+    # 存储所有的视频真实地址和名称信息
+    video_url_list = []
+    for li in li_list:
+        # 视频id
+        video_id = li.xpath('.//a/@href')[0].split('_')[1]
+        # 视频名称
+        name = li.xpath('.//div[@class="vervideo-title"]/text()')[0] + '.mp4'
+        # 梨视频的video标签是动态加载的,通过请求抓包获取到的ajax地址
+        ajax_url = 'https://www.pearvideo.com/videoStatus.jsp'
+        query_param = {
+            'contId': video_id,
+            'mrd': str(random.random())
+        }
+        # 梨视频有Referer 防盗链验证
+        # 需要在普通的ua伪装中加入Referer请求头,否则会一直提示文章已下线
+        ajax_headers = {
+            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
+            'Referer': 'https://www.pearvideo.com/video_' + video_id
+        }
+        json_obj = requests.get(url=ajax_url, headers=ajax_headers, params=query_param).json()
+        # 响应的地址:https://video.pearvideo.com/mp4/adshort/20210419/1618849825266-15658816_adpkg-ad_hd.mp4
+        # 实际的地址:https://video.pearvideo.com/mp4/adshort/20210419/cont-1727112-15658816_adpkg-ad_hd.mp4
+        # 实际地址中cont-后是视频id 因此要把这串字符串处理掉
+        temp_url = json_obj['videoInfo']['videos']['srcUrl']
+        last_index = temp_url.rfind('/')
+        # 最后一个/前的内容 https://video.pearvideo.com/mp4/adshort/20210419
+        real_video_url = temp_url[:last_index]
+        # 最后一个/后的内容根据-切片(不包含) 1618849825266-15658816_adpkg-ad_hd.mp4
+        str_list = temp_url[last_index + 1:].split('-')
+        for i in range(0, len(str_list)):
+            if i == 0:
+                real_video_url = real_video_url + '/cont-' + video_id + '-'
+            elif i == len(str_list) - 1:
+                real_video_url = real_video_url + str_list[i]
+            else:
+                real_video_url = real_video_url + str_list[i] + '-'
+        # 字典存储视频信息
+        video_dict = {'name': name, "url": real_video_url}
+        video_url_list.append(video_dict)
+    return video_url_list
+
+
+# io操作较耗时,采用多线程进行
+def download_video(dict):
+    video_name = dict['name']
+    video_url = dict['url']
+    video_stream = requests.get(url=video_url, headers=headers).content
+    with open('./video/' + video_name, 'wb') as f:
+        f.write(video_stream)
+        print(f'============={video_name}下载完毕===============')
+
+
+if __name__ == '__main__':
+    # 多线程执行下载任务
+    pool = Pool(4)
+    pool.map(download_video, get_videos())

流程:

  • 根据主链接拿到最热视频的视频id和视频名称

  • 通过抓包拿到请求视频真实地址的ajax请求地址,修改参数,添加Referer请求头解决防盗链问题

  • 通过ajax请求拿到响应的json对象,解析出我们需要的视频地址

  • 通过对比可以得知视频地址是经过了字符串替换的,通过字符串操作得到真实的视频地址

  • 将解析出来的视频信息字典统一存在列表,再定义一个持久化方法

  • 通过多线程进行持久化操作

`,4),e=[o];function t(r,c,E,y,i,d){return n(),a("div",null,e)}const A=s(p,[["render",t]]);export{_ as __pageData,A as default}; diff --git "a/assets/python_crawler_\345\244\232\347\272\277\347\250\213\347\210\254\345\217\226\346\242\250\350\247\206\351\242\221\347\275\221\347\253\231\347\232\204\347\203\255\351\227\250\350\247\206\351\242\221.md.0cea7349.lean.js" "b/assets/python_crawler_\345\244\232\347\272\277\347\250\213\347\210\254\345\217\226\346\242\250\350\247\206\351\242\221\347\275\221\347\253\231\347\232\204\347\203\255\351\227\250\350\247\206\351\242\221.md.0cea7349.lean.js" new file mode 100644 index 000000000..21e53f91d --- /dev/null +++ "b/assets/python_crawler_\345\244\232\347\272\277\347\250\213\347\210\254\345\217\226\346\242\250\350\247\206\351\242\221\347\275\221\347\253\231\347\232\204\347\203\255\351\227\250\350\247\206\351\242\221.md.0cea7349.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const _=JSON.parse('{"title":"多线程爬取梨视频网站的热门视频","description":"","frontmatter":{},"headers":[],"relativePath":"python/crawler/多线程爬取梨视频网站的热门视频.md","filePath":"python/crawler/多线程爬取梨视频网站的热门视频.md","lastUpdated":1708510102000}'),p={name:"python/crawler/多线程爬取梨视频网站的热门视频.md"},o=l("",4),e=[o];function t(r,c,E,y,i,d){return n(),a("div",null,e)}const A=s(p,[["render",t]]);export{_ as __pageData,A as default}; diff --git "a/assets/python_crawler_\345\260\217\347\272\242\344\271\246\347\210\254\350\231\253.md.ef9e7902.js" "b/assets/python_crawler_\345\260\217\347\272\242\344\271\246\347\210\254\350\231\253.md.ef9e7902.js" new file mode 100644 index 000000000..a8355c12f --- /dev/null +++ "b/assets/python_crawler_\345\260\217\347\272\242\344\271\246\347\210\254\350\231\253.md.ef9e7902.js" @@ -0,0 +1 @@ +import{_ as t,o as r,c as o,k as e,a}from"./chunks/framework.dc1f4608.js";const k=JSON.parse('{"title":"小红书图片爬虫","description":"","frontmatter":{},"headers":[],"relativePath":"python/crawler/小红书爬虫.md","filePath":"python/crawler/小红书爬虫.md","lastUpdated":1708510102000}'),s={name:"python/crawler/小红书爬虫.md"},n=e("h1",{id:"小红书图片爬虫",tabindex:"-1"},[a("小红书图片爬虫 "),e("a",{class:"header-anchor",href:"#小红书图片爬虫","aria-label":'Permalink to "小红书图片爬虫"'},"​")],-1),c=e("h2",{id:"加密算法",tabindex:"-1"},[a("加密算法 "),e("a",{class:"header-anchor",href:"#加密算法","aria-label":'Permalink to "加密算法"'},"​")],-1),d=e("p",null,[e("code",null,"x-sign = 'X' + md5(url处于域名后边部分+'WSUDD')")],-1),l=[n,c,d];function _(i,h,p,m,f,u){return r(),o("div",null,l)}const b=t(s,[["render",_]]);export{k as __pageData,b as default}; diff --git "a/assets/python_crawler_\345\260\217\347\272\242\344\271\246\347\210\254\350\231\253.md.ef9e7902.lean.js" "b/assets/python_crawler_\345\260\217\347\272\242\344\271\246\347\210\254\350\231\253.md.ef9e7902.lean.js" new file mode 100644 index 000000000..a8355c12f --- /dev/null +++ "b/assets/python_crawler_\345\260\217\347\272\242\344\271\246\347\210\254\350\231\253.md.ef9e7902.lean.js" @@ -0,0 +1 @@ +import{_ as t,o as r,c as o,k as e,a}from"./chunks/framework.dc1f4608.js";const k=JSON.parse('{"title":"小红书图片爬虫","description":"","frontmatter":{},"headers":[],"relativePath":"python/crawler/小红书爬虫.md","filePath":"python/crawler/小红书爬虫.md","lastUpdated":1708510102000}'),s={name:"python/crawler/小红书爬虫.md"},n=e("h1",{id:"小红书图片爬虫",tabindex:"-1"},[a("小红书图片爬虫 "),e("a",{class:"header-anchor",href:"#小红书图片爬虫","aria-label":'Permalink to "小红书图片爬虫"'},"​")],-1),c=e("h2",{id:"加密算法",tabindex:"-1"},[a("加密算法 "),e("a",{class:"header-anchor",href:"#加密算法","aria-label":'Permalink to "加密算法"'},"​")],-1),d=e("p",null,[e("code",null,"x-sign = 'X' + md5(url处于域名后边部分+'WSUDD')")],-1),l=[n,c,d];function _(i,h,p,m,f,u){return r(),o("div",null,l)}const b=t(s,[["render",_]]);export{k as __pageData,b as default}; diff --git "a/assets/python_crawler_\351\252\214\350\257\201\347\240\201\350\257\206\345\210\253\345\222\214\346\250\241\346\213\237\347\231\273\345\275\225.md.042432f0.js" "b/assets/python_crawler_\351\252\214\350\257\201\347\240\201\350\257\206\345\210\253\345\222\214\346\250\241\346\213\237\347\231\273\345\275\225.md.042432f0.js" new file mode 100644 index 000000000..57507deec --- /dev/null +++ "b/assets/python_crawler_\351\252\214\350\257\201\347\240\201\350\257\206\345\210\253\345\222\214\346\250\241\346\213\237\347\231\273\345\275\225.md.042432f0.js" @@ -0,0 +1,247 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"验证码识别和模拟登录","description":"","frontmatter":{},"headers":[],"relativePath":"python/crawler/验证码识别和模拟登录.md","filePath":"python/crawler/验证码识别和模拟登录.md","lastUpdated":1708510102000}'),p={name:"python/crawler/验证码识别和模拟登录.md"},o=l(`

验证码识别和模拟登录

超级鹰验证码识别平台

地址:https://www.chaojiying.com/

使用方法:

  1. 注册并登陆
  2. 点击开发文档,选择自己使用的语言下载使用demo,比如python
  3. 解压后得到demo实例,配置自己的用户名密码和软件id,软件id在用户中心的侧边栏最下方选择软件ID即可
  4. 根据需要识别的验证码类型配置具体类型,比如4-6位英文数字的类型编号为1902
  5. 充值,每次识别根据不同的类型有不同题分的定价,这个平台1元=1000题分
  6. 开始开发

超级鹰识别古诗文网验证码案例

识别案例代码

python
import requests
+from lxml import etree
+from story.code import StoryClient
+import os
+
+
+if __name__ == '__main__':
+    url = 'https://so.gushiwen.cn/user/login.aspx'
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    page_text = requests.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    # 验证码url地址
+    code_url = 'https://so.gushiwen.cn' + tree.xpath('//*[@id="imgCode"]/@src')[0]
+    img_data = requests.get(url=code_url, headers=headers).content
+    if not os.path.exists('./img'):
+        os.mkdir('./img')
+    with open('./img/code.jpg', 'wb') as f:
+        f.write(img_data)
+    client = StoryClient()
+    with open('./img/code.jpp','rb') as f:
+        im = f.read()
+        res = client.post_pic(im,1902)['pic_str']
+        print(res)
import requests
+from lxml import etree
+from story.code import StoryClient
+import os
+
+
+if __name__ == '__main__':
+    url = 'https://so.gushiwen.cn/user/login.aspx'
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    page_text = requests.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    # 验证码url地址
+    code_url = 'https://so.gushiwen.cn' + tree.xpath('//*[@id="imgCode"]/@src')[0]
+    img_data = requests.get(url=code_url, headers=headers).content
+    if not os.path.exists('./img'):
+        os.mkdir('./img')
+    with open('./img/code.jpg', 'wb') as f:
+        f.write(img_data)
+    client = StoryClient()
+    with open('./img/code.jpp','rb') as f:
+        im = f.read()
+        res = client.post_pic(im,1902)['pic_str']
+        print(res)

超级鹰使用demo代码:

这里我微调了一下,把用户信息在代码里写死了,可以根据自己情况调整

python
import requests
+from hashlib import md5
+
+
+class StoryClient(object):
+
+    def __init__(self):
+        self.username = '超级鹰用户名'
+        password = '超级鹰密码'.encode('utf8')
+        self.password = md5(password).hexdigest()
+        self.soft_id = '超级鹰软件ID'
+        self.base_params = {
+            'user': self.username,
+            'pass2': self.password,
+            'softid': self.soft_id,
+        }
+        self.headers = {
+            'Connection': 'Keep-Alive',
+            'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)',
+        }
+
+    def post_pic(self, im, codetype):
+        """
+        im: 图片字节
+        codetype: 题目类型 参考 http://www.chaojiying.com/price.html
+        """
+        params = {
+            'codetype': codetype,
+        }
+        params.update(self.base_params)
+        files = {'userfile': ('ccc.jpg', im)}
+        r = requests.post('http://upload.chaojiying.net/Upload/Processing.php', data=params, files=files,
+                          headers=self.headers)
+        return r.json()
+
+    def ReportError(self, im_id):
+        """
+        im_id:报错题目的图片ID
+        """
+        params = {
+            'id': im_id,
+        }
+        params.update(self.base_params)
+        r = requests.post('http://upload.chaojiying.net/Upload/ReportError.php', data=params, headers=self.headers)
+        return r.json()
+
+
+if __name__ == '__main__':
+    client = StoryClient()  
+    im = open('a.jpg', 'rb').read()  # 本地图片文件路径 来替换 a.jpg 有时WIN系统须要//
+    print(client.post_pic(im, 1902))  # 1902 验证码类型  官方网站>>价格体系 3.4+版 print 后要加()
import requests
+from hashlib import md5
+
+
+class StoryClient(object):
+
+    def __init__(self):
+        self.username = '超级鹰用户名'
+        password = '超级鹰密码'.encode('utf8')
+        self.password = md5(password).hexdigest()
+        self.soft_id = '超级鹰软件ID'
+        self.base_params = {
+            'user': self.username,
+            'pass2': self.password,
+            'softid': self.soft_id,
+        }
+        self.headers = {
+            'Connection': 'Keep-Alive',
+            'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)',
+        }
+
+    def post_pic(self, im, codetype):
+        """
+        im: 图片字节
+        codetype: 题目类型 参考 http://www.chaojiying.com/price.html
+        """
+        params = {
+            'codetype': codetype,
+        }
+        params.update(self.base_params)
+        files = {'userfile': ('ccc.jpg', im)}
+        r = requests.post('http://upload.chaojiying.net/Upload/Processing.php', data=params, files=files,
+                          headers=self.headers)
+        return r.json()
+
+    def ReportError(self, im_id):
+        """
+        im_id:报错题目的图片ID
+        """
+        params = {
+            'id': im_id,
+        }
+        params.update(self.base_params)
+        r = requests.post('http://upload.chaojiying.net/Upload/ReportError.php', data=params, headers=self.headers)
+        return r.json()
+
+
+if __name__ == '__main__':
+    client = StoryClient()  
+    im = open('a.jpg', 'rb').read()  # 本地图片文件路径 来替换 a.jpg 有时WIN系统须要//
+    print(client.post_pic(im, 1902))  # 1902 验证码类型  官方网站>>价格体系 3.4+版 print 后要加()

流程:

  • 请求登录页面并保存验证码图片
  • 上传验证码图片到超级鹰并识别

人人网模拟登录

python
import requests
+from lxml import etree
+from story.code import StoryClient
+
+if __name__ == '__main__':
+    url = 'http://www.renren.com/SysHome.do'
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    # 使用session发送请求
+    session = requests.session()
+    # page_text = requests.get(url=url, headers=headers).text
+    page_text = session.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    code_url = tree.xpath('//*[@id="verifyPic_login"]/@src')[0]
+    code_img_data = requests.get(url=code_url, headers=headers).content
+    path = './img/code.jpg'
+    with open(path, 'wb') as f:
+        f.write(code_img_data)
+    # 超级鹰客户端
+    client = StoryClient()
+    with open(path, 'rb') as r:
+        im = r.read()
+    code = client.post_pic(im, 1902)['pic_str']
+    login_url = 'http://www.renren.com/ajaxLogin/login?1=1&uniqueTimestamp=2021312240804'
+    data = {
+        'email': '人人网用户名',
+        'icode': code,
+        'origURL': 'http://www.renren.com/home',
+        'domain': 'renren.com',
+        'key_id': 1,
+        'captcha_type': 'web_login',
+        'password': '人人网密码',
+        'rkey': 'asdfasdf',
+        'f': ''
+    }
+    #response = requests.post(url=login_url, headers=headers, data=data)
+    # 使用session
+    response = session.post(url=login_url, headers=headers, data=data)
+    print(response.status_code)
+    login_page_data = response.text
+    with open('renren.html', 'w', encoding='utf-8') as f:
+        f.write(login_page_data)
+
+    detail_url = 'http://www.renren.com/6666666/profile'
+    # detail_text = requests.get(url=detail_url,headers=headers).text
+    # 使用session
+    detail_text = session.get(url=detail_url,headers=headers).text
+    with open('./detail.html','w',encoding='utf-8') as dw:
+        dw.write(detail_text)
import requests
+from lxml import etree
+from story.code import StoryClient
+
+if __name__ == '__main__':
+    url = 'http://www.renren.com/SysHome.do'
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    # 使用session发送请求
+    session = requests.session()
+    # page_text = requests.get(url=url, headers=headers).text
+    page_text = session.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    code_url = tree.xpath('//*[@id="verifyPic_login"]/@src')[0]
+    code_img_data = requests.get(url=code_url, headers=headers).content
+    path = './img/code.jpg'
+    with open(path, 'wb') as f:
+        f.write(code_img_data)
+    # 超级鹰客户端
+    client = StoryClient()
+    with open(path, 'rb') as r:
+        im = r.read()
+    code = client.post_pic(im, 1902)['pic_str']
+    login_url = 'http://www.renren.com/ajaxLogin/login?1=1&uniqueTimestamp=2021312240804'
+    data = {
+        'email': '人人网用户名',
+        'icode': code,
+        'origURL': 'http://www.renren.com/home',
+        'domain': 'renren.com',
+        'key_id': 1,
+        'captcha_type': 'web_login',
+        'password': '人人网密码',
+        'rkey': 'asdfasdf',
+        'f': ''
+    }
+    #response = requests.post(url=login_url, headers=headers, data=data)
+    # 使用session
+    response = session.post(url=login_url, headers=headers, data=data)
+    print(response.status_code)
+    login_page_data = response.text
+    with open('renren.html', 'w', encoding='utf-8') as f:
+        f.write(login_page_data)
+
+    detail_url = 'http://www.renren.com/6666666/profile'
+    # detail_text = requests.get(url=detail_url,headers=headers).text
+    # 使用session
+    detail_text = session.get(url=detail_url,headers=headers).text
+    with open('./detail.html','w',encoding='utf-8') as dw:
+        dw.write(detail_text)

流程:

  • 请求登录页,抓取验证码上传到超级鹰识别
  • 浏览器抓包,拿到登录按钮的触发事件发送的login请求内容,复制出来并调整格式(代码中的data部分)
  • http是无状态的,登录后要想访问个人主页,需要携带cookie发送请求,可以手动抓包cookie到代码中,推荐使用requests.session()发送请求,然后可以继续爬取需要的个人主页详情信息
  • 超级鹰客户端部分见上一个案例中的demo代码
`,17),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const _=s(p,[["render",t]]);export{h as __pageData,_ as default}; diff --git "a/assets/python_crawler_\351\252\214\350\257\201\347\240\201\350\257\206\345\210\253\345\222\214\346\250\241\346\213\237\347\231\273\345\275\225.md.042432f0.lean.js" "b/assets/python_crawler_\351\252\214\350\257\201\347\240\201\350\257\206\345\210\253\345\222\214\346\250\241\346\213\237\347\231\273\345\275\225.md.042432f0.lean.js" new file mode 100644 index 000000000..a6ff584db --- /dev/null +++ "b/assets/python_crawler_\351\252\214\350\257\201\347\240\201\350\257\206\345\210\253\345\222\214\346\250\241\346\213\237\347\231\273\345\275\225.md.042432f0.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"验证码识别和模拟登录","description":"","frontmatter":{},"headers":[],"relativePath":"python/crawler/验证码识别和模拟登录.md","filePath":"python/crawler/验证码识别和模拟登录.md","lastUpdated":1708510102000}'),p={name:"python/crawler/验证码识别和模拟登录.md"},o=l("",17),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const _=s(p,[["render",t]]);export{h as __pageData,_ as default}; diff --git a/assets/python_index.md.700873bd.js b/assets/python_index.md.700873bd.js new file mode 100644 index 000000000..39e1bfad4 --- /dev/null +++ b/assets/python_index.md.700873bd.js @@ -0,0 +1 @@ +import{_ as t,o as n,c as a,k as e,a as o}from"./chunks/framework.dc1f4608.js";const x=JSON.parse('{"title":"Python","description":"","frontmatter":{},"headers":[],"relativePath":"python/index.md","filePath":"python/index.md","lastUpdated":1708510102000}'),l={name:"python/index.md"},s=e("h1",{id:"python",tabindex:"-1"},[o("Python "),e("a",{class:"header-anchor",href:"#python","aria-label":'Permalink to "Python"'},"​")],-1),i=e("ul",null,[e("li",null,"基础"),e("li",null,"爬虫"),e("li",null,"Web"),e("li",null,"脚本及其他")],-1),r=[s,i];function d(c,h,p,_,u,m){return n(),a("div",null,r)}const y=t(l,[["render",d]]);export{x as __pageData,y as default}; diff --git a/assets/python_index.md.700873bd.lean.js b/assets/python_index.md.700873bd.lean.js new file mode 100644 index 000000000..39e1bfad4 --- /dev/null +++ b/assets/python_index.md.700873bd.lean.js @@ -0,0 +1 @@ +import{_ as t,o as n,c as a,k as e,a as o}from"./chunks/framework.dc1f4608.js";const x=JSON.parse('{"title":"Python","description":"","frontmatter":{},"headers":[],"relativePath":"python/index.md","filePath":"python/index.md","lastUpdated":1708510102000}'),l={name:"python/index.md"},s=e("h1",{id:"python",tabindex:"-1"},[o("Python "),e("a",{class:"header-anchor",href:"#python","aria-label":'Permalink to "Python"'},"​")],-1),i=e("ul",null,[e("li",null,"基础"),e("li",null,"爬虫"),e("li",null,"Web"),e("li",null,"脚本及其他")],-1),r=[s,i];function d(c,h,p,_,u,m){return n(),a("div",null,r)}const y=t(l,[["render",d]]);export{x as __pageData,y as default}; diff --git "a/assets/python_others_Alfred\346\217\222\344\273\266-\345\277\253\351\200\237\344\275\277\347\224\250\347\274\226\350\276\221\345\231\250\346\211\223\345\274\200\346\214\207\345\256\232\346\226\207\344\273\266.md.056bae34.js" "b/assets/python_others_Alfred\346\217\222\344\273\266-\345\277\253\351\200\237\344\275\277\347\224\250\347\274\226\350\276\221\345\231\250\346\211\223\345\274\200\346\214\207\345\256\232\346\226\207\344\273\266.md.056bae34.js" new file mode 100644 index 000000000..c644192cb --- /dev/null +++ "b/assets/python_others_Alfred\346\217\222\344\273\266-\345\277\253\351\200\237\344\275\277\347\224\250\347\274\226\350\276\221\345\231\250\346\211\223\345\274\200\346\214\207\345\256\232\346\226\207\344\273\266.md.056bae34.js" @@ -0,0 +1,43 @@ +import{_ as s,o as a,c as n,Q as p}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"Alfred插件-快速使用编辑器打开指定文件","description":"","frontmatter":{},"headers":[],"relativePath":"python/others/Alfred插件-快速使用编辑器打开指定文件.md","filePath":"python/others/Alfred插件-快速使用编辑器打开指定文件.md","lastUpdated":1708510102000}'),l={name:"python/others/Alfred插件-快速使用编辑器打开指定文件.md"},o=p(`

Alfred插件-快速使用编辑器打开指定文件

python
import sys
+import subprocess
+import re
+import os
+
+_pattern = r'[^A-Za-z0-9_\\-.,:+\\/@\\n]'
+
+
+def replace_func(match_obj):
+    return '\\\\' + match_obj.group(0)
+
+
+def shell_escape(str_param):
+    return re.sub(_pattern, replace_func, str_param)
+
+
+if __name__ == '__main__':
+    if os.path.exists(sys.argv[1]):
+        file_path = shell_escape(sys.argv[1])
+        editor = shell_escape(sys.argv[2])
+        command = f"open -a {editor} {file_path}"
+        subprocess.Popen(command, shell=True)
import sys
+import subprocess
+import re
+import os
+
+_pattern = r'[^A-Za-z0-9_\\-.,:+\\/@\\n]'
+
+
+def replace_func(match_obj):
+    return '\\\\' + match_obj.group(0)
+
+
+def shell_escape(str_param):
+    return re.sub(_pattern, replace_func, str_param)
+
+
+if __name__ == '__main__':
+    if os.path.exists(sys.argv[1]):
+        file_path = shell_escape(sys.argv[1])
+        editor = shell_escape(sys.argv[2])
+        command = f"open -a {editor} {file_path}"
+        subprocess.Popen(command, shell=True)

release:https://github.com/storyxc/Alfred-open-with-editor/releases/download/Alfred/Open.with.Editor.alfredworkflow

repository:https://github.com/storyxc/Alfred-open-with-editor/releases/tag/Alfred

image-20220221194201279

`,5),e=[o];function t(r,c,y,E,i,_){return a(),n("div",null,e)}const h=s(l,[["render",t]]);export{d as __pageData,h as default}; diff --git "a/assets/python_others_Alfred\346\217\222\344\273\266-\345\277\253\351\200\237\344\275\277\347\224\250\347\274\226\350\276\221\345\231\250\346\211\223\345\274\200\346\214\207\345\256\232\346\226\207\344\273\266.md.056bae34.lean.js" "b/assets/python_others_Alfred\346\217\222\344\273\266-\345\277\253\351\200\237\344\275\277\347\224\250\347\274\226\350\276\221\345\231\250\346\211\223\345\274\200\346\214\207\345\256\232\346\226\207\344\273\266.md.056bae34.lean.js" new file mode 100644 index 000000000..196d26fe9 --- /dev/null +++ "b/assets/python_others_Alfred\346\217\222\344\273\266-\345\277\253\351\200\237\344\275\277\347\224\250\347\274\226\350\276\221\345\231\250\346\211\223\345\274\200\346\214\207\345\256\232\346\226\207\344\273\266.md.056bae34.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as p}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"Alfred插件-快速使用编辑器打开指定文件","description":"","frontmatter":{},"headers":[],"relativePath":"python/others/Alfred插件-快速使用编辑器打开指定文件.md","filePath":"python/others/Alfred插件-快速使用编辑器打开指定文件.md","lastUpdated":1708510102000}'),l={name:"python/others/Alfred插件-快速使用编辑器打开指定文件.md"},o=p("",5),e=[o];function t(r,c,y,E,i,_){return a(),n("div",null,e)}const h=s(l,[["render",t]]);export{d as __pageData,h as default}; diff --git "a/assets/python_others_argparse\346\250\241\345\235\227\345\205\245\351\227\250.md.9f5f1e90.js" "b/assets/python_others_argparse\346\250\241\345\235\227\345\205\245\351\227\250.md.9f5f1e90.js" new file mode 100644 index 000000000..dfa1d3e46 --- /dev/null +++ "b/assets/python_others_argparse\346\250\241\345\235\227\345\205\245\351\227\250.md.9f5f1e90.js" @@ -0,0 +1,359 @@ +import{_ as s,o as a,c as n,Q as p}from"./chunks/framework.dc1f4608.js";const B=JSON.parse('{"title":"argparse模块入门","description":"","frontmatter":{},"headers":[],"relativePath":"python/others/argparse模块入门.md","filePath":"python/others/argparse模块入门.md","lastUpdated":1708510102000}'),l={name:"python/others/argparse模块入门.md"},o=p(`

argparse模块入门

学习如何使用python编写一个命令行程序。

简介

argparse 模块可以让人轻松编写用户友好的命令行接口。程序定义它需要的参数,然后 argparse 将弄清如何从 sys.argv 解析出那些参数。 argparse 模块还会自动生成帮助和使用手册,并在用户给程序传入无效参数时报出错误信息。

官方文档地址:https://docs.python.org/zh-cn/3/library/argparse.html#upgrading-optparse-code

基础

python
import argparse
+parser = argparse.ArgumentParser()
+parser.parse_args()
import argparse
+parser = argparse.ArgumentParser()
+parser.parse_args()

使用命令行运行这个程序

bash
$ python main.py
+
+
+$ python main.py --help
+usage: main.py [-h]
+
+optional arguments:
+  -h, --help  show this help message and exit
+
+
+$ python main.py story
+usage: main.py [-h]
+main.py: error: unrecognized arguments: story
$ python main.py
+
+
+$ python main.py --help
+usage: main.py [-h]
+
+optional arguments:
+  -h, --help  show this help message and exit
+
+
+$ python main.py story
+usage: main.py [-h]
+main.py: error: unrecognized arguments: story

程序运行情况:

  • 在没有任何选项时,程序没有任何输出
  • argparse在我们什么逻辑代码都没有编写的情况下帮我们提供了一条帮助信息
  • --help可以缩写为-h,是唯一一个可以直接使用的选项,指定任何没有定义的内容都会报错,但是也会给出提示

位置参数

python
import argparse
+parser = argparse.ArgumentParser()
+parser.add_argument('print')
+args = parser.parse_args()
+print(args.print)
import argparse
+parser = argparse.ArgumentParser()
+parser.add_argument('print')
+args = parser.parse_args()
+print(args.print)

运行

bash
$ python main.py
+usage: main.py [-h] print
+main.py: error: the following arguments are required: print
+
+$ python main.py --help
+usage: main.py [-h] print
+
+positional arguments:
+  print
+
+optional arguments:
+  -h, --help  show this help message and exit
+
+
+$ python main.py test
+test
$ python main.py
+usage: main.py [-h] print
+main.py: error: the following arguments are required: print
+
+$ python main.py --help
+usage: main.py [-h] print
+
+positional arguments:
+  print
+
+optional arguments:
+  -h, --help  show this help message and exit
+
+
+$ python main.py test
+test

程序运行情况:

  • 增加了add_argument()方法,该方法指定程序能够接受哪些命令行选项。在例子中使用了print作为选项名
  • 现在调用程序必须指定一个选项
  • 这个选项就是一个位置参数

add_argument方法还可以添加提示信息

比如修改上面的代码再次运行:

parser.add_argument('print',help='print the string you typed')

bash
$ python main.py --help
+usage: main.py [-h] print
+
+positional arguments:
+  print       print the string you typed
+
+optional arguments:
+  -h, --help  show this help message and exit
$ python main.py --help
+usage: main.py [-h] print
+
+positional arguments:
+  print       print the string you typed
+
+optional arguments:
+  -h, --help  show this help message and exit

还可以指定输入的值的类型,否则argparse会把一切输入都当作字符串

parser.add_argument('print',help='print the number you typed',type=int)

运行:

bash
$ python main.py 1
+1
+
+$ python main.py two
+usage: main.py [-h] print
+main.py: error: argument print: invalid int value: 'two'
$ python main.py 1
+1
+
+$ python main.py two
+usage: main.py [-h] print
+main.py: error: argument print: invalid int value: 'two'

可选参数

python
import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--verbosity', help='increase output verbosity')
+args = parser.parse_args()
+if args.verbosity:
+    print('verbosity turn on')
import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--verbosity', help='increase output verbosity')
+args = parser.parse_args()
+if args.verbosity:
+    print('verbosity turn on')

运行:

bash
$ python main.py
+
+$ python main.py --verbosity test
+verbosity turn on
+
+$ python main.py -h
+usage: main.py [-h] [--verbosity VERBOSITY]
+
+optional arguments:
+  -h, --help            show this help message and exit
+  --verbosity VERBOSITY
+                        increase output verbosity
+
+$ python main.py --verbosity
+usage: main.py [-h] [--verbosity VERBOSITY]
+main.py: error: argument --verbosity: expected one argument
$ python main.py
+
+$ python main.py --verbosity test
+verbosity turn on
+
+$ python main.py -h
+usage: main.py [-h] [--verbosity VERBOSITY]
+
+optional arguments:
+  -h, --help            show this help message and exit
+  --verbosity VERBOSITY
+                        increase output verbosity
+
+$ python main.py --verbosity
+usage: main.py [-h] [--verbosity VERBOSITY]
+main.py: error: argument --verbosity: expected one argument

运行结果:

  • 当指定了--verbosity时打印turn on,否则不打印

  • 不添加这选项时不会报错,说明是可选参数,当一个可选参数没有被使用,对应的变量会被赋值为None,因此args.verbosity在if中被判断为逻辑假

  • 帮助信息多了VERBOSITY

  • 使用--verbosity选项时必须指定一个值,否则会报错

修改代码:

python
import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--verbose', help='increase output verbosity',
+                    action='store_true')
+args = parser.parse_args()
+if args.verbose:
+    print('verbosity turn on')
import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--verbose', help='increase output verbosity',
+                    action='store_true')
+args = parser.parse_args()
+if args.verbose:
+    print('verbosity turn on')

运行:

bash
$ python main.py
+
+
+$ python main.py --verbose
+verbosity turn on
+
+$ python main.py --help
+usage: main.py [-h] [--verbose]
+
+$ python main.py --verbose test
+usage: main.py [-h] [--verbose]
+main.py: error: unrecognized arguments: test
+
+
+optional arguments:
+  -h, --help  show this help message and exit
+  --verbose   increase output verbosity
$ python main.py
+
+
+$ python main.py --verbose
+verbosity turn on
+
+$ python main.py --help
+usage: main.py [-h] [--verbose]
+
+$ python main.py --verbose test
+usage: main.py [-h] [--verbose]
+main.py: error: unrecognized arguments: test
+
+
+optional arguments:
+  -h, --help  show this help message and exit
+  --verbose   increase output verbosity

运行结果:

  • 修改之后,这一选项更多是一个标志,而不需要接收值,新增加的参数action赋值为store_true,意味着,当这一选项存在时,为args.verbose赋值为True,没有指定该选项时为False
  • 当为其指定值时会报错
  • 不同的帮助文字

短参数

我们能注意到-h--help是功能相同的,我们也可以给自定义的参数指定简短的形式

python
import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument('-v', '--verbose', help='increase output verbosity',
+                    action='store_true')
+parser.add_argument('-s', '--square', type=int, help='display a square of a given number')
+args = parser.parse_args()
+answer = args.square ** 2  # **运算符为计算arg1的arg2次幂
+if args.verbose:
+    print('the square of {} is {}'.format(args.square, answer))
+else:
+    print(answer)
import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument('-v', '--verbose', help='increase output verbosity',
+                    action='store_true')
+parser.add_argument('-s', '--square', type=int, help='display a square of a given number')
+args = parser.parse_args()
+answer = args.square ** 2  # **运算符为计算arg1的arg2次幂
+if args.verbose:
+    print('the square of {} is {}'.format(args.square, answer))
+else:
+    print(answer)

运行:

bash
$ python main.py -s -v 1
+usage: main.py [-h] [-v] [-s SQUARE]
+main.py: error: argument -s/--square: expected one argument
+
+$ python main.py -s 2 -v
+the square of 2 is 4
+
+$ python main.py -s 2
+4
$ python main.py -s -v 1
+usage: main.py [-h] [-v] [-s SQUARE]
+main.py: error: argument -s/--square: expected one argument
+
+$ python main.py -s 2 -v
+the square of 2 is 4
+
+$ python main.py -s 2
+4

运行结果:

  • 必要的可选参数也要传值
  • 根据可选参数的指定与否我们可以控制一些功能的实现

修改代码:

python
import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument('-v', '--verbose', type=int,help='increase output verbosity')
+parser.add_argument('-s', '--square', type=int, help='display a square of a given number')
+args = parser.parse_args()
+answer = args.square ** 2  # **运算符为计算arg1的arg2次幂
+if args.verbose == 1:
+    print('the square of {} is {}'.format(args.square, answer))
+elif args.verbose == 2:
+    print('{}^2 is {}'.format(args.square, answer))
+else:
+    print(answer)
import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument('-v', '--verbose', type=int,help='increase output verbosity')
+parser.add_argument('-s', '--square', type=int, help='display a square of a given number')
+args = parser.parse_args()
+answer = args.square ** 2  # **运算符为计算arg1的arg2次幂
+if args.verbose == 1:
+    print('the square of {} is {}'.format(args.square, answer))
+elif args.verbose == 2:
+    print('{}^2 is {}'.format(args.square, answer))
+else:
+    print(answer)

运行结果:

bash
$ python main.py -s 2 -v 1
+the square of 2 is 4
+
+$ python main.py -s 2 -v 2
+2^2 is 4
+
+$ python main.py -s 2 -v 3
+4
$ python main.py -s 2 -v 1
+the square of 2 is 4
+
+$ python main.py -s 2 -v 2
+2^2 is 4
+
+$ python main.py -s 2 -v 3
+4

显然,用户指定-v的值是3是我们不愿意看见的,因此我们可以限定-v的取值范围

修改

python
parser.add_argument('-v', '--verbose', type=int,help='increase output verbosity',
+                    choices=[1,2])
parser.add_argument('-v', '--verbose', type=int,help='increase output verbosity',
+                    choices=[1,2])

再次运行:

bash
$ python main.py -s 2 -v 3
+usage: main.py [-h] [-v {1,2}] [-s SQUARE]
+main.py: error: argument -v/--verbose: invalid choice: 3 (choose from 1, 2)
+
+$ python main.py -h
+usage: main.py [-h] [-v {1,2}] [-s SQUARE]
+
+optional arguments:
+  -h, --help            show this help message and exit
+  -v {1,2}, --verbose {1,2}
+                        increase output verbosity
+  -s SQUARE, --square SQUARE
+                        display a square of a given number
$ python main.py -s 2 -v 3
+usage: main.py [-h] [-v {1,2}] [-s SQUARE]
+main.py: error: argument -v/--verbose: invalid choice: 3 (choose from 1, 2)
+
+$ python main.py -h
+usage: main.py [-h] [-v {1,2}] [-s SQUARE]
+
+optional arguments:
+  -h, --help            show this help message and exit
+  -v {1,2}, --verbose {1,2}
+                        increase output verbosity
+  -s SQUARE, --square SQUARE
+                        display a square of a given number

互斥参数

add_mutually_exclusive_group()方法允许指定彼此冲突的选项

python
import argparse
+
+parser = argparse.ArgumentParser()
+group = parser.add_mutually_exclusive_group()
+group.add_argument("-v", "--verbose", action="store_true")
+group.add_argument("-q", "--quiet", action="store_true")
+parser.add_argument("x", type=int, help="the base")
+parser.add_argument("y", type=int, help="the exponent")
+args = parser.parse_args()
+answer = args.x**args.y
+
+if args.quiet:
+    print(answer)
+elif args.verbose:
+    print("{} to the power {} equals {}".format(args.x, args.y, answer))
+else:
+    print("{}^{} == {}".format(args.x, args.y, answer))
import argparse
+
+parser = argparse.ArgumentParser()
+group = parser.add_mutually_exclusive_group()
+group.add_argument("-v", "--verbose", action="store_true")
+group.add_argument("-q", "--quiet", action="store_true")
+parser.add_argument("x", type=int, help="the base")
+parser.add_argument("y", type=int, help="the exponent")
+args = parser.parse_args()
+answer = args.x**args.y
+
+if args.quiet:
+    print(answer)
+elif args.verbose:
+    print("{} to the power {} equals {}".format(args.x, args.y, answer))
+else:
+    print("{}^{} == {}".format(args.x, args.y, answer))

运行:

bash
$ python main.py 4 2
+4^2 == 16
+
+$ python main.py 4 2 -v
+4 to the power 2 equals 16
+
+$ python main.py 4 2 -q
+16
+
+$ python main.py -h
+usage: main.py [-h] [-v | -q] x y
+
+calculate X to the power of Y
+
+positional arguments:
+  x              the base
+  y              the exponent
+
+optional arguments:
+  -h, --help     show this help message and exit
+  -v, --verbose
+  -q, --quiet
$ python main.py 4 2
+4^2 == 16
+
+$ python main.py 4 2 -v
+4 to the power 2 equals 16
+
+$ python main.py 4 2 -q
+16
+
+$ python main.py -h
+usage: main.py [-h] [-v | -q] x y
+
+calculate X to the power of Y
+
+positional arguments:
+  x              the base
+  y              the exponent
+
+optional arguments:
+  -h, --help     show this help message and exit
+  -v, --verbose
+  -q, --quiet

运行结果:

  • 根据指定-v还是-q,可以得到不同输出,实现不同功能

  • usage: main.py [-h] [-v | -q] x y中[-v|-q]代表可选其一,而不是使用两者

    如果同时使用会报错:

    bash
    $ python main.py 4 2 -v -q
    +usage: main.py [-h] [-v | -q] x y
    +main.py: error: argument -q/--quiet: not allowed with argument -v/--verbose
    $ python main.py 4 2 -v -q
    +usage: main.py [-h] [-v | -q] x y
    +main.py: error: argument -q/--quiet: not allowed with argument -v/--verbose
`,60),e=[o];function r(t,c,y,E,F,i){return a(),n("div",null,e)}const h=s(l,[["render",r]]);export{B as __pageData,h as default}; diff --git "a/assets/python_others_argparse\346\250\241\345\235\227\345\205\245\351\227\250.md.9f5f1e90.lean.js" "b/assets/python_others_argparse\346\250\241\345\235\227\345\205\245\351\227\250.md.9f5f1e90.lean.js" new file mode 100644 index 000000000..952ef1b46 --- /dev/null +++ "b/assets/python_others_argparse\346\250\241\345\235\227\345\205\245\351\227\250.md.9f5f1e90.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as p}from"./chunks/framework.dc1f4608.js";const B=JSON.parse('{"title":"argparse模块入门","description":"","frontmatter":{},"headers":[],"relativePath":"python/others/argparse模块入门.md","filePath":"python/others/argparse模块入门.md","lastUpdated":1708510102000}'),l={name:"python/others/argparse模块入门.md"},o=p("",60),e=[o];function r(t,c,y,E,F,i){return a(),n("div",null,e)}const h=s(l,[["render",r]]);export{B as __pageData,h as default}; diff --git a/assets/python_others_youtube-upload.md.3d98cb59.js b/assets/python_others_youtube-upload.md.3d98cb59.js new file mode 100644 index 000000000..e73cbc9b9 --- /dev/null +++ b/assets/python_others_youtube-upload.md.3d98cb59.js @@ -0,0 +1,381 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"youtube-upload","description":"","frontmatter":{},"headers":[],"relativePath":"python/others/youtube-upload.md","filePath":"python/others/youtube-upload.md","lastUpdated":1708510102000}'),p={name:"python/others/youtube-upload.md"},o=l(`

youtube-upload

instructions

  1. 安装google-api-python-client包,github仓库:https://github.com/googleapis/google-api-python-client

  2. 安装oauth2client包,github仓库:https://github.com/googleapis/oauth2client

  3. google cloud创建应用

    1. 在api库中搜索youtube data api v3并启用
    2. 在OAuth consent screen中创建一个应用,选择desktop app,把自己的邮箱添加到test user
    3. 在凭据Credentials中创建OAuth 2.0客户端id,把客户端id,客户端密钥替换到下面这个json中,并保存到项目下的client_secrets.json中(要和上传的python脚本文件在相同目录)
    json
    {
    +  "web": {
    +    "client_id": "[[INSERT CLIENT ID HERE]]",
    +    "client_secret": "[[INSERT CLIENT SECRET HERE]]",
    +    "redirect_uris": [],
    +    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    +    "token_uri": "https://accounts.google.com/o/oauth2/token"
    +  }
    +}
    {
    +  "web": {
    +    "client_id": "[[INSERT CLIENT ID HERE]]",
    +    "client_secret": "[[INSERT CLIENT SECRET HERE]]",
    +    "redirect_uris": [],
    +    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    +    "token_uri": "https://accounts.google.com/o/oauth2/token"
    +  }
    +}

sample request

shell
python3 upload_video.py --file="/tmp/test_video_file.flv"
+                       --title="Summer vacation in California"
+                       --description="Had fun surfing in Santa Cruz"
+                       --keywords="surfing,Santa Cruz"
+                       --category="22"
+                       --privacyStatus="private"
python3 upload_video.py --file="/tmp/test_video_file.flv"
+                       --title="Summer vacation in California"
+                       --description="Had fun surfing in Santa Cruz"
+                       --keywords="surfing,Santa Cruz"
+                       --category="22"
+                       --privacyStatus="private"

sample code

代码源自YouTube Data API,给的例子比较老旧,是python2的代码,还有没法用的httplib包,下面代码针对这些做了删改调整可以直接使用。

python
#!/usr/bin/python
+
+
+import httplib2
+import os
+import random
+import sys
+import time
+
+from apiclient.discovery import build
+from apiclient.errors import HttpError
+from apiclient.http import MediaFileUpload
+from oauth2client.client import flow_from_clientsecrets
+from oauth2client.file import Storage
+from oauth2client.tools import argparser, run_flow
+
+
+# Explicitly tell the underlying HTTP transport library not to retry, since
+# we are handling retry logic ourselves.
+httplib2.RETRIES = 1
+
+# Maximum number of times to retry before giving up.
+MAX_RETRIES = 10
+
+# Always retry when these exceptions are raised.
+RETRIABLE_EXCEPTIONS = (httplib2.HttpLib2Error, IOError)
+
+# Always retry when an apiclient.errors.HttpError with one of these status
+# codes is raised.
+RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
+
+# The CLIENT_SECRETS_FILE variable specifies the name of a file that contains
+# the OAuth 2.0 information for this application, including its client_id and
+# client_secret. You can acquire an OAuth 2.0 client ID and client secret from
+# the Google API Console at
+# https://console.developers.google.com/.
+# Please ensure that you have enabled the YouTube Data API for your project.
+# For more information about using OAuth2 to access the YouTube Data API, see:
+#   https://developers.google.com/youtube/v3/guides/authentication
+# For more information about the client_secrets.json file format, see:
+#   https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
+CLIENT_SECRETS_FILE = "client_secrets.json"
+
+# This OAuth 2.0 access scope allows an application to upload files to the
+# authenticated user's YouTube channel, but doesn't allow other types of access.
+YOUTUBE_UPLOAD_SCOPE = "https://www.googleapis.com/auth/youtube.upload"
+YOUTUBE_API_SERVICE_NAME = "youtube"
+YOUTUBE_API_VERSION = "v3"
+
+# This variable defines a message to display if the CLIENT_SECRETS_FILE is
+# missing.
+MISSING_CLIENT_SECRETS_MESSAGE = """
+WARNING: Please configure OAuth 2.0
+
+To make this sample run you will need to populate the client_secrets.json file
+found at:
+
+   %s
+
+with information from the API Console
+https://console.developers.google.com/
+
+For more information about the client_secrets.json file format, please visit:
+https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
+""" % os.path.abspath(os.path.join(os.path.dirname(__file__),
+                                   CLIENT_SECRETS_FILE))
+
+VALID_PRIVACY_STATUSES = ("public", "private", "unlisted")
+
+
+def get_authenticated_service(args):
+  flow = flow_from_clientsecrets(CLIENT_SECRETS_FILE,
+    scope=YOUTUBE_UPLOAD_SCOPE,
+    message=MISSING_CLIENT_SECRETS_MESSAGE)
+
+  storage = Storage("%s-oauth2.json" % sys.argv[0])
+  credentials = storage.get()
+
+  if credentials is None or credentials.invalid:
+    credentials = run_flow(flow, storage, args)
+
+  return build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION,
+    http=credentials.authorize(httplib2.Http()))
+
+def initialize_upload(youtube, options):
+  tags = None
+  if options.keywords:
+    tags = options.keywords.split(",")
+
+  body=dict(
+    snippet=dict(
+      title=options.title,
+      description=options.description,
+      tags=tags,
+      categoryId=options.category
+    ),
+    status=dict(
+      privacyStatus=options.privacyStatus
+    )
+  )
+
+  # Call the API's videos.insert method to create and upload the video.
+  insert_request = youtube.videos().insert(
+    part=",".join(body.keys()),
+    body=body,
+    # The chunksize parameter specifies the size of each chunk of data, in
+    # bytes, that will be uploaded at a time. Set a higher value for
+    # reliable connections as fewer chunks lead to faster uploads. Set a lower
+    # value for better recovery on less reliable connections.
+    #
+    # Setting "chunksize" equal to -1 in the code below means that the entire
+    # file will be uploaded in a single HTTP request. (If the upload fails,
+    # it will still be retried where it left off.) This is usually a best
+    # practice, but if you're using Python older than 2.6 or if you're
+    # running on App Engine, you should set the chunksize to something like
+    # 1024 * 1024 (1 megabyte).
+    media_body=MediaFileUpload(options.file, chunksize=-1, resumable=True)
+  )
+
+  resumable_upload(insert_request)
+
+# This method implements an exponential backoff strategy to resume a
+# failed upload.
+def resumable_upload(insert_request):
+  response = None
+  error = None
+  retry = 0
+  while response is None:
+    try:
+      print("Uploading file...")
+      status, response = insert_request.next_chunk()
+      if response is not None:
+        if 'id' in response:
+          print("Video id '%s' was successfully uploaded." % response['id'])
+        else:
+          exit("The upload failed with an unexpected response: %s" % response)
+    except HttpError as e:
+      if e.resp.status in RETRIABLE_STATUS_CODES:
+        error = "A retriable HTTP error %d occurred:\\n%s" % (e.resp.status,
+                                                             e.content)
+      else:
+        raise
+    except RETRIABLE_EXCEPTIONS as e:
+      error = "A retriable error occurred: %s" % e
+
+    if error is not None:
+      print(error)
+      retry += 1
+      if retry > MAX_RETRIES:
+        exit("No longer attempting to retry.")
+
+      max_sleep = 2 ** retry
+      sleep_seconds = random.random() * max_sleep
+      print("Sleeping %f seconds and then retrying..." % sleep_seconds)
+      time.sleep(sleep_seconds)
+
+if __name__ == '__main__':
+  argparser.add_argument("--file", required=True, help="Video file to upload")
+  argparser.add_argument("--title", help="Video title", default="Test Title")
+  argparser.add_argument("--description", help="Video description",
+    default="Test Description")
+  argparser.add_argument("--category", default="22",
+    help="Numeric video category. " +
+      "See https://developers.google.com/youtube/v3/docs/videoCategories/list")
+  argparser.add_argument("--keywords", help="Video keywords, comma separated",
+    default="")
+  argparser.add_argument("--privacyStatus", choices=VALID_PRIVACY_STATUSES,
+    default=VALID_PRIVACY_STATUSES[0], help="Video privacy status.")
+  args = argparser.parse_args()
+
+  if not os.path.exists(args.file):
+    exit("Please specify a valid file using the --file= parameter.")
+
+  youtube = get_authenticated_service(args)
+  try:
+    initialize_upload(youtube, args)
+  except HttpError as e:
+    print("An HTTP error %d occurred:\\n%s" % (e.resp.status, e.content))
#!/usr/bin/python
+
+
+import httplib2
+import os
+import random
+import sys
+import time
+
+from apiclient.discovery import build
+from apiclient.errors import HttpError
+from apiclient.http import MediaFileUpload
+from oauth2client.client import flow_from_clientsecrets
+from oauth2client.file import Storage
+from oauth2client.tools import argparser, run_flow
+
+
+# Explicitly tell the underlying HTTP transport library not to retry, since
+# we are handling retry logic ourselves.
+httplib2.RETRIES = 1
+
+# Maximum number of times to retry before giving up.
+MAX_RETRIES = 10
+
+# Always retry when these exceptions are raised.
+RETRIABLE_EXCEPTIONS = (httplib2.HttpLib2Error, IOError)
+
+# Always retry when an apiclient.errors.HttpError with one of these status
+# codes is raised.
+RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
+
+# The CLIENT_SECRETS_FILE variable specifies the name of a file that contains
+# the OAuth 2.0 information for this application, including its client_id and
+# client_secret. You can acquire an OAuth 2.0 client ID and client secret from
+# the Google API Console at
+# https://console.developers.google.com/.
+# Please ensure that you have enabled the YouTube Data API for your project.
+# For more information about using OAuth2 to access the YouTube Data API, see:
+#   https://developers.google.com/youtube/v3/guides/authentication
+# For more information about the client_secrets.json file format, see:
+#   https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
+CLIENT_SECRETS_FILE = "client_secrets.json"
+
+# This OAuth 2.0 access scope allows an application to upload files to the
+# authenticated user's YouTube channel, but doesn't allow other types of access.
+YOUTUBE_UPLOAD_SCOPE = "https://www.googleapis.com/auth/youtube.upload"
+YOUTUBE_API_SERVICE_NAME = "youtube"
+YOUTUBE_API_VERSION = "v3"
+
+# This variable defines a message to display if the CLIENT_SECRETS_FILE is
+# missing.
+MISSING_CLIENT_SECRETS_MESSAGE = """
+WARNING: Please configure OAuth 2.0
+
+To make this sample run you will need to populate the client_secrets.json file
+found at:
+
+   %s
+
+with information from the API Console
+https://console.developers.google.com/
+
+For more information about the client_secrets.json file format, please visit:
+https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
+""" % os.path.abspath(os.path.join(os.path.dirname(__file__),
+                                   CLIENT_SECRETS_FILE))
+
+VALID_PRIVACY_STATUSES = ("public", "private", "unlisted")
+
+
+def get_authenticated_service(args):
+  flow = flow_from_clientsecrets(CLIENT_SECRETS_FILE,
+    scope=YOUTUBE_UPLOAD_SCOPE,
+    message=MISSING_CLIENT_SECRETS_MESSAGE)
+
+  storage = Storage("%s-oauth2.json" % sys.argv[0])
+  credentials = storage.get()
+
+  if credentials is None or credentials.invalid:
+    credentials = run_flow(flow, storage, args)
+
+  return build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION,
+    http=credentials.authorize(httplib2.Http()))
+
+def initialize_upload(youtube, options):
+  tags = None
+  if options.keywords:
+    tags = options.keywords.split(",")
+
+  body=dict(
+    snippet=dict(
+      title=options.title,
+      description=options.description,
+      tags=tags,
+      categoryId=options.category
+    ),
+    status=dict(
+      privacyStatus=options.privacyStatus
+    )
+  )
+
+  # Call the API's videos.insert method to create and upload the video.
+  insert_request = youtube.videos().insert(
+    part=",".join(body.keys()),
+    body=body,
+    # The chunksize parameter specifies the size of each chunk of data, in
+    # bytes, that will be uploaded at a time. Set a higher value for
+    # reliable connections as fewer chunks lead to faster uploads. Set a lower
+    # value for better recovery on less reliable connections.
+    #
+    # Setting "chunksize" equal to -1 in the code below means that the entire
+    # file will be uploaded in a single HTTP request. (If the upload fails,
+    # it will still be retried where it left off.) This is usually a best
+    # practice, but if you're using Python older than 2.6 or if you're
+    # running on App Engine, you should set the chunksize to something like
+    # 1024 * 1024 (1 megabyte).
+    media_body=MediaFileUpload(options.file, chunksize=-1, resumable=True)
+  )
+
+  resumable_upload(insert_request)
+
+# This method implements an exponential backoff strategy to resume a
+# failed upload.
+def resumable_upload(insert_request):
+  response = None
+  error = None
+  retry = 0
+  while response is None:
+    try:
+      print("Uploading file...")
+      status, response = insert_request.next_chunk()
+      if response is not None:
+        if 'id' in response:
+          print("Video id '%s' was successfully uploaded." % response['id'])
+        else:
+          exit("The upload failed with an unexpected response: %s" % response)
+    except HttpError as e:
+      if e.resp.status in RETRIABLE_STATUS_CODES:
+        error = "A retriable HTTP error %d occurred:\\n%s" % (e.resp.status,
+                                                             e.content)
+      else:
+        raise
+    except RETRIABLE_EXCEPTIONS as e:
+      error = "A retriable error occurred: %s" % e
+
+    if error is not None:
+      print(error)
+      retry += 1
+      if retry > MAX_RETRIES:
+        exit("No longer attempting to retry.")
+
+      max_sleep = 2 ** retry
+      sleep_seconds = random.random() * max_sleep
+      print("Sleeping %f seconds and then retrying..." % sleep_seconds)
+      time.sleep(sleep_seconds)
+
+if __name__ == '__main__':
+  argparser.add_argument("--file", required=True, help="Video file to upload")
+  argparser.add_argument("--title", help="Video title", default="Test Title")
+  argparser.add_argument("--description", help="Video description",
+    default="Test Description")
+  argparser.add_argument("--category", default="22",
+    help="Numeric video category. " +
+      "See https://developers.google.com/youtube/v3/docs/videoCategories/list")
+  argparser.add_argument("--keywords", help="Video keywords, comma separated",
+    default="")
+  argparser.add_argument("--privacyStatus", choices=VALID_PRIVACY_STATUSES,
+    default=VALID_PRIVACY_STATUSES[0], help="Video privacy status.")
+  args = argparser.parse_args()
+
+  if not os.path.exists(args.file):
+    exit("Please specify a valid file using the --file= parameter.")
+
+  youtube = get_authenticated_service(args)
+  try:
+    initialize_upload(youtube, args)
+  except HttpError as e:
+    print("An HTTP error %d occurred:\\n%s" % (e.resp.status, e.content))
`,8),e=[o];function t(c,r,E,y,i,u){return n(),a("div",null,e)}const A=s(p,[["render",t]]);export{d as __pageData,A as default}; diff --git a/assets/python_others_youtube-upload.md.3d98cb59.lean.js b/assets/python_others_youtube-upload.md.3d98cb59.lean.js new file mode 100644 index 000000000..3106cc93d --- /dev/null +++ b/assets/python_others_youtube-upload.md.3d98cb59.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"youtube-upload","description":"","frontmatter":{},"headers":[],"relativePath":"python/others/youtube-upload.md","filePath":"python/others/youtube-upload.md","lastUpdated":1708510102000}'),p={name:"python/others/youtube-upload.md"},o=l("",8),e=[o];function t(c,r,E,y,i,u){return n(),a("div",null,e)}const A=s(p,[["render",t]]);export{d as __pageData,A as default}; diff --git "a/assets/python_others_\344\275\277\347\224\250pandas\346\250\241\345\235\227\350\277\233\350\241\214\346\225\260\346\215\256\345\244\204\347\220\206.md.fa389c23.js" "b/assets/python_others_\344\275\277\347\224\250pandas\346\250\241\345\235\227\350\277\233\350\241\214\346\225\260\346\215\256\345\244\204\347\220\206.md.fa389c23.js" new file mode 100644 index 000000000..2e9efdf7e --- /dev/null +++ "b/assets/python_others_\344\275\277\347\224\250pandas\346\250\241\345\235\227\350\277\233\350\241\214\346\225\260\346\215\256\345\244\204\347\220\206.md.fa389c23.js" @@ -0,0 +1,31 @@ +import{_ as a,o as s,c as l,Q as e}from"./chunks/framework.dc1f4608.js";const E=JSON.parse('{"title":"使用pandas模块进行数据处理","description":"","frontmatter":{},"headers":[],"relativePath":"python/others/使用pandas模块进行数据处理.md","filePath":"python/others/使用pandas模块进行数据处理.md","lastUpdated":1708510102000}'),n={name:"python/others/使用pandas模块进行数据处理.md"},p=e(`

使用pandas模块进行数据处理

以csv/txt文件为例

读取文件

pd.read_csv():返回一个DataFrame或TextFileReader

  • header指定具体表头行数,如果没有则header=None,第一行是表头则header=0,header还可以是一个列表例如header=[0,1,3],此时会有多个标题,且1和3之间的行会被忽略掉

  • seq指定分割符,默认为','

  • skiprows跳过某一行,行号从0开始,例如skiprows=2或skiprows=[0,1,200]

  • nrows指定需要读取的行数,从第一行开始,例如nrows=1000

  • na_values空值置换,会把指定的值替换为空值例如na_values=['\\N', 15]会把字符串\\N和数字15替换为空值NaN

    • 如果na_values的参数是一个字典,那就可以为具体的列来指定缺失值的样子。我们就可以指定在Age这一列,0要被看成缺失值;在Comment这一列,“该用户没有评价”被看成缺失值。na_values={'Age':0,'Comment':'该用户没有评价'}
  • iterator: True时返回一个TextFileReader,用于大文件处理,可以逐块处理文件

  • chunksize:指定文件块大小,返回一个TextFileReader

  • encoding:指定编码

  • index_col:读取时指定索引列,和df.set_index效果相同

  • names:文件中没有表头,手动指定表头,需要和header配合使用

names和header的使用场景主要如下:

  1. csv文件有表头并且是第一行,那么names和header都无需指定;
  2. csv文件有表头、但表头不是第一行,可能从下面几行开始才是真正的表头和数据,这个时候指定header即可;
  3. csv文件没有表头,全部是纯数据,那么我们可以通过names手动生成表头;
  4. csv文件有表头、但是这个表头你不想用,这个时候同时指定names和header。先用header选出表头和数据,然后再用names将表头替换掉,其实就等价于将数据读取进来之后再对列名进行rename;

数据相关概念

DataFrame

多行多列的二维数组、整个表格、多行多列

Series

一维数据、一行或一列

index

对应纵向上的行

替换索引为某一列的值:df.set_index('xxx', inplace=True)

columns

对应横向上的列

查询数据

几种方法

  1. df.loc,根据行、列的标签值查询(既能查询又能覆盖写入)

    • 行根据行标签,也就是索引筛选,列根据列标签,列名筛选

    • 如果选取的是所有行或者所有列,可以用:代替

    • 行标签选取的时候,两端都包含,比如[0:5]指的是0,1,2,3,4,5

  2. df.iloc,根据行、列的数字位置查询

    • iloc基于位置索引,简言之,就是第几行第几列,只不过这里的行列都是从0开始的。

    • iloc的0:X中不包括X,只能到X-1.

  3. df.where

  4. df.query

df.loc

  1. 使用单个label值查询

    • 查找并替换某一列的值&转换数据类型:df.loc[:, 'x'] = df['x'].str.replace('X','').astype('int32')

    • 查询单个值:df.loc['index', 'column']

    • 得到一个Series:df.loc['index', ['column1', 'column2']]

  2. 使用值列表批量查询

    • 得到一个Series:df.loc(['index1', 'index2', 'index3'], 'column1')
    • 得到DataFrame:df.loc(['index1', 'index2', 'index3'], ['column1', 'column2'])
  3. 使用数值区间进行范围查询(包含区间的开始和结尾)

    • 行index按区间:df.loc[1:2, 'colum1']
    • 列index按区间:df.loc[1, 'column1': 'column2']
    • 行列都按区间:df.loc[1:2, 'column1': 'column2']
  4. 使用条件表达式查询

    • 简单条件查询,年龄小于18:df.loc[df['age'] < 18, :]
    • 复杂条件查询,年龄小于18且姓名为张三:df.loc[(df['age'] < 18) & (df['name'] == '张三'), :]
  5. 调用函数查询

    • lambda表达式:df.loc[lambda df: df['age'] > 18, :]

    • 调用函数:

      python
      def query_adult(x):
      +    return df['age'] > 18
      +  
      +df.loc[query_adult, :]
      def query_adult(x):
      +    return df['age'] > 18
      +  
      +df.loc[query_adult, :]

新增数据列

几种方法

  1. 直接赋值

    • df.loc[:, 'newAge'] = df['age'] + 1
  2. df.apply

    • apply赋值 基于 0-'index' 1-'columns' 操作跨行/跨列

    • python
      def get_is_adult(x):
      +  if x['age'] >= 18:
      +      return '成年'
      +  else:
      +      return '未成年'
      +
      +df.loc[:, 'isAdult'] = df.apply(get_is_adult, axis=1)
      def get_is_adult(x):
      +  if x['age'] >= 18:
      +      return '成年'
      +  else:
      +      return '未成年'
      +
      +df.loc[:, 'isAdult'] = df.apply(get_is_adult, axis=1)
  3. df.assign

    • assign添加一列:返回一个新的DataFrame,存在的列会被覆盖,如果参数是callable只能直接操作DataFrame,如果不是callable则直接赋值
    • df = df.assign(newAge=lambda x: x['age'] + 1)
  4. 按条件选择分组并分别赋值

    • df.loc[df['highTemp'] - df['lowTemp'] > 10, 'tempDiff'] = '温差大'
    • df.loc[df['highTemp'] - df['lowTemp'] <= 10, 'tempDiff'] = '温差小'

    按字段分组查看数量: df['tempDiff'].value_counts()

数据合并

df_list = [df]
+df2 = pd.concat(df_list)
+
+if not os.path.exists('../resources/data1.csv'):
+    df2.to_csv('../resources/data1.csv', mode='a', index=False, header=True)
+else:
+    df2.to_csv('../resources/data1.csv', mode='a', index=False, header=False)
df_list = [df]
+df2 = pd.concat(df_list)
+
+if not os.path.exists('../resources/data1.csv'):
+    df2.to_csv('../resources/data1.csv', mode='a', index=False, header=True)
+else:
+    df2.to_csv('../resources/data1.csv', mode='a', index=False, header=False)

axis参数

pandas的axis参数:指的是跨该axis,例如指定columns 则是跨列,也就是沿着列名水平方向执行

  • 跨列操作:在横向上遍历每行,对每行的数据进行操作

  • 跨行操作:在水平方向遍历每列,对每列数据进行操作

`,29),o=[p];function t(i,c,r,d,u,y){return s(),l("div",null,o)}const f=a(n,[["render",t]]);export{E as __pageData,f as default}; diff --git "a/assets/python_others_\344\275\277\347\224\250pandas\346\250\241\345\235\227\350\277\233\350\241\214\346\225\260\346\215\256\345\244\204\347\220\206.md.fa389c23.lean.js" "b/assets/python_others_\344\275\277\347\224\250pandas\346\250\241\345\235\227\350\277\233\350\241\214\346\225\260\346\215\256\345\244\204\347\220\206.md.fa389c23.lean.js" new file mode 100644 index 000000000..5cb5af79a --- /dev/null +++ "b/assets/python_others_\344\275\277\347\224\250pandas\346\250\241\345\235\227\350\277\233\350\241\214\346\225\260\346\215\256\345\244\204\347\220\206.md.fa389c23.lean.js" @@ -0,0 +1 @@ +import{_ as a,o as s,c as l,Q as e}from"./chunks/framework.dc1f4608.js";const E=JSON.parse('{"title":"使用pandas模块进行数据处理","description":"","frontmatter":{},"headers":[],"relativePath":"python/others/使用pandas模块进行数据处理.md","filePath":"python/others/使用pandas模块进行数据处理.md","lastUpdated":1708510102000}'),n={name:"python/others/使用pandas模块进行数据处理.md"},p=e("",29),o=[p];function t(i,c,r,d,u,y){return s(),l("div",null,o)}const f=a(n,[["render",t]]);export{E as __pageData,f as default}; diff --git "a/assets/python_others_\345\210\207\346\215\242windows\344\273\243\347\220\206\350\256\276\347\275\256\345\274\200\345\205\263.md.88db6435.js" "b/assets/python_others_\345\210\207\346\215\242windows\344\273\243\347\220\206\350\256\276\347\275\256\345\274\200\345\205\263.md.88db6435.js" new file mode 100644 index 000000000..9e6c70354 --- /dev/null +++ "b/assets/python_others_\345\210\207\346\215\242windows\344\273\243\347\220\206\350\256\276\347\275\256\345\274\200\345\205\263.md.88db6435.js" @@ -0,0 +1,29 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"切换windows代理设置开关","description":"","frontmatter":{},"headers":[],"relativePath":"python/others/切换windows代理设置开关.md","filePath":"python/others/切换windows代理设置开关.md","lastUpdated":1708510102000}'),p={name:"python/others/切换windows代理设置开关.md"},o=l(`

切换windows代理设置开关

python
import winreg
+
+INTERNET_SETTINGS = winreg.OpenKey(winreg.HKEY_CURRENT_USER,
+    r'Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings',
+    0, winreg.KEY_ALL_ACCESS)
+name = 'ProxyEnable'
+
+def toggle_proxy():
+    _, reg_type = get_key()
+    winreg.SetValueEx(INTERNET_SETTINGS, name, 0, reg_type, 1 if _ == 0 else 0)
+
+def get_key():
+    return winreg.QueryValueEx(INTERNET_SETTINGS,name)
+
+toggle_proxy()
import winreg
+
+INTERNET_SETTINGS = winreg.OpenKey(winreg.HKEY_CURRENT_USER,
+    r'Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings',
+    0, winreg.KEY_ALL_ACCESS)
+name = 'ProxyEnable'
+
+def toggle_proxy():
+    _, reg_type = get_key()
+    winreg.SetValueEx(INTERNET_SETTINGS, name, 0, reg_type, 1 if _ == 0 else 0)
+
+def get_key():
+    return winreg.QueryValueEx(INTERNET_SETTINGS,name)
+
+toggle_proxy()

windows下可执行文件下载点我

`,3),e=[o];function t(r,c,E,y,i,_){return n(),a("div",null,e)}const g=s(p,[["render",t]]);export{d as __pageData,g as default}; diff --git "a/assets/python_others_\345\210\207\346\215\242windows\344\273\243\347\220\206\350\256\276\347\275\256\345\274\200\345\205\263.md.88db6435.lean.js" "b/assets/python_others_\345\210\207\346\215\242windows\344\273\243\347\220\206\350\256\276\347\275\256\345\274\200\345\205\263.md.88db6435.lean.js" new file mode 100644 index 000000000..ab1bc1c79 --- /dev/null +++ "b/assets/python_others_\345\210\207\346\215\242windows\344\273\243\347\220\206\350\256\276\347\275\256\345\274\200\345\205\263.md.88db6435.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"切换windows代理设置开关","description":"","frontmatter":{},"headers":[],"relativePath":"python/others/切换windows代理设置开关.md","filePath":"python/others/切换windows代理设置开关.md","lastUpdated":1708510102000}'),p={name:"python/others/切换windows代理设置开关.md"},o=l("",3),e=[o];function t(r,c,E,y,i,_){return n(),a("div",null,e)}const g=s(p,[["render",t]]);export{d as __pageData,g as default}; diff --git "a/assets/python_others_\350\257\273\345\217\226excel_ssh\351\200\232\351\201\223\350\277\236\346\216\245rds\346\233\264\346\226\260\346\225\260\346\215\256.md.3c80a264.js" "b/assets/python_others_\350\257\273\345\217\226excel_ssh\351\200\232\351\201\223\350\277\236\346\216\245rds\346\233\264\346\226\260\346\225\260\346\215\256.md.3c80a264.js" new file mode 100644 index 000000000..b647b082f --- /dev/null +++ "b/assets/python_others_\350\257\273\345\217\226excel_ssh\351\200\232\351\201\223\350\277\236\346\216\245rds\346\233\264\346\226\260\346\225\260\346\215\256.md.3c80a264.js" @@ -0,0 +1,195 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const _=JSON.parse('{"title":"读取excel & 使用ssh通道连接rds更新数据","description":"","frontmatter":{},"headers":[],"relativePath":"python/others/读取excel&ssh通道连接rds更新数据.md","filePath":"python/others/读取excel&ssh通道连接rds更新数据.md","lastUpdated":1708510102000}'),p={name:"python/others/读取excel&ssh通道连接rds更新数据.md"},o=l(`

读取excel & 使用ssh通道连接rds更新数据

python
import pymysql
+from sshtunnel import SSHTunnelForwarder
+import pymysql.cursors
+import xlrd
+
+
+def querySQL(ssh_config, db_config, sql):
+    with SSHTunnelForwarder(
+            (ssh_config['host'], ssh_config['port']),
+            ssh_password=ssh_config['password'],
+            ssh_username=ssh_config['username'],
+            remote_bind_address=(db_config['host'], db_config['port'])
+    ) as server:
+        db = pymysql.connect(
+            host='127.0.0.1',
+            port=server.local_bind_port,
+            user=db_config['username'],
+            passwd=db_config['password'],
+            db=db_config['db_name'],
+            charset="utf8",
+            cursorclass=pymysql.cursors.DictCursor)
+
+        cursor = db.cursor()
+        data = {}
+        try:
+            cursor.execute(sql)
+            data = cursor.fetchone()
+            db.commit()
+        except:
+            db.rollback()
+
+        db.close()
+        cursor.close()
+        return data
+
+
+class ExcelData(object):
+    def __init__(self, data_path, sheetname):
+        self.data_path = data_path  # excle表格路径,需传入绝对路径
+        self.sheetname = sheetname  # excle表格内sheet名
+        self.data = xlrd.open_workbook(self.data_path)  # 打开excel表格
+        self.table = self.data.sheet_by_name(self.sheetname)  # 切换到相应sheet
+        self.keys = self.table.row_values(0)  # 第一行作为key值
+        self.rowNum = self.table.nrows  # 获取表格行数
+        self.colNum = self.table.ncols  # 获取表格列数
+
+    def readExcel(self):
+        if self.rowNum < 2:
+            print("excle内数据行数小于2")
+        else:
+            L = []  # 列表L存放取出的数据
+            for i in range(1, self.rowNum):  # 从第二行(数据行)开始取数据
+                sheet_data = {}  # 定义一个字典用来存放对应数据
+                for j in range(self.colNum):  # j对应列值
+                    sheet_data[self.keys[j]] = self.table.row_values(i)[j]  # 把第i行第j列的值取出赋给第j列的键值,构成字典
+                L.append(sheet_data)  # 一行值取完之后(一个字典),追加到L列表中
+            # print(type(L))
+            return L
+
+
+if __name__ == "__main__":
+    # 远程登录配置信息
+    ssh_config = {
+        'host': '',
+        'port': 22,
+        'username': '',
+        'password': ''
+    }
+    # 数据库配置信息
+    db_config = {
+        'host': '',
+        'port': 3306,
+        'username': '',
+        'password': '',
+        'db_name': ''
+    }
+
+    path = ""
+    sheetname = ""
+    get_data = ExcelData(path, sheetname)
+    dataList = get_data.readExcel()
+    process_result = []
+    with open('./res.txt', 'w') as f:
+        for data in dataList:
+            # 查询语句
+            try:
+                sql = ''
+
+                # 查询
+                res = querySQL(ssh_config, db_config, sql)
+
+                update_sql = ""
+                f.write(update_sql + '\\n')
+                # print(res)
+            except Exception as e:
+                error = '... 处理失败'
+                process_result.append(error)
+    print(process_result)
import pymysql
+from sshtunnel import SSHTunnelForwarder
+import pymysql.cursors
+import xlrd
+
+
+def querySQL(ssh_config, db_config, sql):
+    with SSHTunnelForwarder(
+            (ssh_config['host'], ssh_config['port']),
+            ssh_password=ssh_config['password'],
+            ssh_username=ssh_config['username'],
+            remote_bind_address=(db_config['host'], db_config['port'])
+    ) as server:
+        db = pymysql.connect(
+            host='127.0.0.1',
+            port=server.local_bind_port,
+            user=db_config['username'],
+            passwd=db_config['password'],
+            db=db_config['db_name'],
+            charset="utf8",
+            cursorclass=pymysql.cursors.DictCursor)
+
+        cursor = db.cursor()
+        data = {}
+        try:
+            cursor.execute(sql)
+            data = cursor.fetchone()
+            db.commit()
+        except:
+            db.rollback()
+
+        db.close()
+        cursor.close()
+        return data
+
+
+class ExcelData(object):
+    def __init__(self, data_path, sheetname):
+        self.data_path = data_path  # excle表格路径,需传入绝对路径
+        self.sheetname = sheetname  # excle表格内sheet名
+        self.data = xlrd.open_workbook(self.data_path)  # 打开excel表格
+        self.table = self.data.sheet_by_name(self.sheetname)  # 切换到相应sheet
+        self.keys = self.table.row_values(0)  # 第一行作为key值
+        self.rowNum = self.table.nrows  # 获取表格行数
+        self.colNum = self.table.ncols  # 获取表格列数
+
+    def readExcel(self):
+        if self.rowNum < 2:
+            print("excle内数据行数小于2")
+        else:
+            L = []  # 列表L存放取出的数据
+            for i in range(1, self.rowNum):  # 从第二行(数据行)开始取数据
+                sheet_data = {}  # 定义一个字典用来存放对应数据
+                for j in range(self.colNum):  # j对应列值
+                    sheet_data[self.keys[j]] = self.table.row_values(i)[j]  # 把第i行第j列的值取出赋给第j列的键值,构成字典
+                L.append(sheet_data)  # 一行值取完之后(一个字典),追加到L列表中
+            # print(type(L))
+            return L
+
+
+if __name__ == "__main__":
+    # 远程登录配置信息
+    ssh_config = {
+        'host': '',
+        'port': 22,
+        'username': '',
+        'password': ''
+    }
+    # 数据库配置信息
+    db_config = {
+        'host': '',
+        'port': 3306,
+        'username': '',
+        'password': '',
+        'db_name': ''
+    }
+
+    path = ""
+    sheetname = ""
+    get_data = ExcelData(path, sheetname)
+    dataList = get_data.readExcel()
+    process_result = []
+    with open('./res.txt', 'w') as f:
+        for data in dataList:
+            # 查询语句
+            try:
+                sql = ''
+
+                # 查询
+                res = querySQL(ssh_config, db_config, sql)
+
+                update_sql = ""
+                f.write(update_sql + '\\n')
+                # print(res)
+            except Exception as e:
+                error = '... 处理失败'
+                process_result.append(error)
+    print(process_result)
`,2),e=[o];function c(t,r,E,y,i,F){return n(),a("div",null,e)}const A=s(p,[["render",c]]);export{_ as __pageData,A as default}; diff --git "a/assets/python_others_\350\257\273\345\217\226excel_ssh\351\200\232\351\201\223\350\277\236\346\216\245rds\346\233\264\346\226\260\346\225\260\346\215\256.md.3c80a264.lean.js" "b/assets/python_others_\350\257\273\345\217\226excel_ssh\351\200\232\351\201\223\350\277\236\346\216\245rds\346\233\264\346\226\260\346\225\260\346\215\256.md.3c80a264.lean.js" new file mode 100644 index 000000000..5d74debed --- /dev/null +++ "b/assets/python_others_\350\257\273\345\217\226excel_ssh\351\200\232\351\201\223\350\277\236\346\216\245rds\346\233\264\346\226\260\346\225\260\346\215\256.md.3c80a264.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const _=JSON.parse('{"title":"读取excel & 使用ssh通道连接rds更新数据","description":"","frontmatter":{},"headers":[],"relativePath":"python/others/读取excel&ssh通道连接rds更新数据.md","filePath":"python/others/读取excel&ssh通道连接rds更新数据.md","lastUpdated":1708510102000}'),p={name:"python/others/读取excel&ssh通道连接rds更新数据.md"},o=l("",2),e=[o];function c(t,r,E,y,i,F){return n(),a("div",null,e)}const A=s(p,[["render",c]]);export{_ as __pageData,A as default}; diff --git "a/assets/python_web_Django\345\205\245\351\227\250.md.7b29ab40.js" "b/assets/python_web_Django\345\205\245\351\227\250.md.7b29ab40.js" new file mode 100644 index 000000000..634101d11 --- /dev/null +++ "b/assets/python_web_Django\345\205\245\351\227\250.md.7b29ab40.js" @@ -0,0 +1,111 @@ +import{_ as s,o as a,c as n,Q as p}from"./chunks/framework.dc1f4608.js";const g=JSON.parse('{"title":"Django入门","description":"","frontmatter":{},"headers":[],"relativePath":"python/web/Django入门.md","filePath":"python/web/Django入门.md","lastUpdated":1708510102000}'),l={name:"python/web/Django入门.md"},o=p(`

Django入门

Django 是一个由 Python 编写的一个开放源代码的 Web 应用框架。

使用 Django,只要很少的代码,Python 的程序开发人员就可以轻松地完成一个正式网站所需要的大部分内容,并进一步开发出全功能的 Web 服务 Django 本身基于 MVC 模型,即 Model(模型)+ View(视图)+ Controller(控制器)设计模式,MVC 模式使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。

MVC 优势:

  • 低耦合
  • 开发快捷
  • 部署方便
  • 可重用性高
  • 维护成本低

MTV模型

Django 采用了 MVT 的软件设计模式,即模型(Model),视图(View)和模板(Template)。

Django 的 MTV 模式本质上和 MVC 是一样的,也是为了各组件间保持松耦合关系,只是定义上有些许不同,Django 的 MTV 分别是指:

  • M 表示模型(Model):编写程序应有的功能,负责业务对象与数据库的映射(ORM)。
  • T 表示模板 (Template):负责如何把页面(html)展示给用户。
  • V 表示视图(View):负责业务逻辑,并在适当时候调用 Model和 Template。

除了以上三层之外,还需要一个 URL 分发器,它的作用是将一个个 URL 的页面请求分发给不同的 View 处理,View 再调用相应的 Model 和 Template,MTV 的响应模式如下所示:

img

img

解析:

用户通过浏览器向我们的服务器发起一个请求(request),这个请求会去访问视图函数:

  • a.如果不涉及到数据调用,那么这个时候视图函数直接返回一个模板也就是一个网页给用户。
  • b.如果涉及到数据调用,那么视图函数调用模型,模型去数据库查找数据,然后逐级返回。

视图函数把返回的数据填充到模板中空格中,最后返回网页给用户。

Django安装与使用

  • 安装:pip install Django

  • 创建Django项目:

    • 命令行创建django-admin startproject 项目名称
    • pycharm创建
  • 项目结构

    image-20210430001401906

    • settings.py —- 包含了项目的默认设置,包括数据库信息,调试标志以及其他一些工作的变量。
    • urls.py —– 负责把URL模式映射到应用程序。
    • manage.py —– Django项目里面的工具,通过它可以调用django shell和数据库等。
    • asgi.py: 一个 ASGI 兼容的 Web 服务器的入口,以便运行你的项目。
    • wsgi.py: 一个 WSGI 兼容的 Web 服务器的入口,以便运行你的项目。
  • 启动:python manage.py runserver 8001

    • 访问localhost:8001

    image-20210430001707060

路由控制

python
# django的1.x和2.x不同
+# 1.x : url(正则表达式,views视图函数,参数,别名)
+# 2.x : path,re_path(原来的url)
+from django.contrib import admin
+from django.urls import path
+
+urlpatterns = [
+    path('admin/', admin.site.urls),
+]
# django的1.x和2.x不同
+# 1.x : url(正则表达式,views视图函数,参数,别名)
+# 2.x : path,re_path(原来的url)
+from django.contrib import admin
+from django.urls import path
+
+urlpatterns = [
+    path('admin/', admin.site.urls),
+]

示例:

python
from django.contrib import admin
+from django.urls import path
+from storyxc.views import index
+urlpatterns = [
+    path('admin/', admin.site.urls),
+    path('',index,{'param':'story'})
+]
from django.contrib import admin
+from django.urls import path
+from storyxc.views import index
+urlpatterns = [
+    path('admin/', admin.site.urls),
+    path('',index,{'param':'story'})
+]

views.py:

python
from django.shortcuts import HttpResponse
+
+
+def index(request,param):
+    print(param)
+    return HttpResponse('ok')
from django.shortcuts import HttpResponse
+
+
+def index(request,param):
+    print(param)
+    return HttpResponse('ok')

启动应用后访问localhost:8000

image-20210430003356198

控制台:

python
[30/Apr/2021 00:31:21] "GET / HTTP/1.1" 200 2
+story
[30/Apr/2021 00:31:21] "GET / HTTP/1.1" 200 2
+story

创建app

python3 manage.py startapp demo

shell
djangoProject
+├── demo
+│   ├── __init__.py
+│   ├── admin.py
+│   ├── apps.py
+│   ├── migrations
+│   │   └── __init__.py
+│   ├── models.py
+│   ├── tests.py
+│   └── views.py
+├── djangoProject
+│   ├── __init__.py
+│   ├── __pycache__
+│   │   ├── __init__.cpython-311.pyc
+│   │   └── settings.cpython-311.pyc
+│   ├── asgi.py
+│   ├── settings.py
+│   ├── urls.py
+│   └── wsgi.py
+├── manage.py
+└── templates
djangoProject
+├── demo
+│   ├── __init__.py
+│   ├── admin.py
+│   ├── apps.py
+│   ├── migrations
+│   │   └── __init__.py
+│   ├── models.py
+│   ├── tests.py
+│   └── views.py
+├── djangoProject
+│   ├── __init__.py
+│   ├── __pycache__
+│   │   ├── __init__.cpython-311.pyc
+│   │   └── settings.cpython-311.pyc
+│   ├── asgi.py
+│   ├── settings.py
+│   ├── urls.py
+│   └── wsgi.py
+├── manage.py
+└── templates

注册app

python
# djangoProject/settings.py
+
+# Application definition
+
+INSTALLED_APPS = [
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+     # 新增注册app
+    'demo.apps.DemoConfig'
+]
+# 可以配置可访问域名
+ALLOWED_HOSTS = ["192.168.2.2"]
# djangoProject/settings.py
+
+# Application definition
+
+INSTALLED_APPS = [
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+     # 新增注册app
+    'demo.apps.DemoConfig'
+]
+# 可以配置可访问域名
+ALLOWED_HOSTS = ["192.168.2.2"]
`,33),e=[o];function t(c,r,y,E,i,F){return a(),n("div",null,e)}const h=s(l,[["render",t]]);export{g as __pageData,h as default}; diff --git "a/assets/python_web_Django\345\205\245\351\227\250.md.7b29ab40.lean.js" "b/assets/python_web_Django\345\205\245\351\227\250.md.7b29ab40.lean.js" new file mode 100644 index 000000000..22fb81610 --- /dev/null +++ "b/assets/python_web_Django\345\205\245\351\227\250.md.7b29ab40.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as p}from"./chunks/framework.dc1f4608.js";const g=JSON.parse('{"title":"Django入门","description":"","frontmatter":{},"headers":[],"relativePath":"python/web/Django入门.md","filePath":"python/web/Django入门.md","lastUpdated":1708510102000}'),l={name:"python/web/Django入门.md"},o=p("",33),e=[o];function t(c,r,y,E,i,F){return a(),n("div",null,e)}const h=s(l,[["render",t]]);export{g as __pageData,h as default}; diff --git "a/assets/python_web_pymysql\344\275\277\347\224\250.md.b054a10e.js" "b/assets/python_web_pymysql\344\275\277\347\224\250.md.b054a10e.js" new file mode 100644 index 000000000..7ec48cb30 --- /dev/null +++ "b/assets/python_web_pymysql\344\275\277\347\224\250.md.b054a10e.js" @@ -0,0 +1,113 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"pymysql使用","description":"","frontmatter":{},"headers":[],"relativePath":"python/web/pymysql使用.md","filePath":"python/web/pymysql使用.md","lastUpdated":1708510102000}'),p={name:"python/web/pymysql使用.md"},o=l(`

pymysql使用

  • 安装pymysql
  • 建立数据库连接
  • 获取cursor对象
  • 使用cursor执行sql
  • 增删改-commit/rollback 查询-fetch
  • 关闭数据库连接

案例

安装:pip install pymysql

代码:

查询

python
import pymysql
+
+
+connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+cursor = connection.cursor(cursor=pymysql.cursors.DictCursor)
+cursor.execute('select * from tb_student')
+print(cursor.fetchall())
+connection.close()
+
+res:
+[{'id': 1, 'name': 'tom', 'age': 18}, {'id': 2, 'name': 'rose', 'age': 17}]
import pymysql
+
+
+connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+cursor = connection.cursor(cursor=pymysql.cursors.DictCursor)
+cursor.execute('select * from tb_student')
+print(cursor.fetchall())
+connection.close()
+
+res:
+[{'id': 1, 'name': 'tom', 'age': 18}, {'id': 2, 'name': 'rose', 'age': 17}]

修改

python
connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+cursor = connection.cursor()
+cursor.execute('update tb_student set name = "jack" where id = 1')
+connection.commit()
+connection.close()
connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+cursor = connection.cursor()
+cursor.execute('update tb_student set name = "jack" where id = 1')
+connection.commit()
+connection.close()

image-20210429231158834

删除

python
import pymysql
+
+connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+cursor = connection.cursor()
+try:
+    cursor.execute('delete from tb_student where id = 2')
+    connection.commit()
+except Exception as e:
+    connection.rollback()
+connection.close()
import pymysql
+
+connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+cursor = connection.cursor()
+try:
+    cursor.execute('delete from tb_student where id = 2')
+    connection.commit()
+except Exception as e:
+    connection.rollback()
+connection.close()

image-20210429231506901

新增

python
import pymysql
+
+connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+cursor = connection.cursor()
+try:
+    # 方式1
+    cursor.execute('insert into tb_student(name,age) values("mike",20)')
+    # 方式2
+    cursor.execute('insert into tb_student(name,age) values("%s",%s)' % ('mike',21))
+    connection.commit()
+except Exception as e:
+    connection.rollback()
+connection.close()
import pymysql
+
+connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+cursor = connection.cursor()
+try:
+    # 方式1
+    cursor.execute('insert into tb_student(name,age) values("mike",20)')
+    # 方式2
+    cursor.execute('insert into tb_student(name,age) values("%s",%s)' % ('mike',21))
+    connection.commit()
+except Exception as e:
+    connection.rollback()
+connection.close()

image-20210429232050677

通过上下文管理器自定义Mysql类

python
import pymysql
+
+
+class Mysql(object):
+    def __enter__(self):
+        self.connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+        return self.connection
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.connection.close()
+
+
+if __name__ == '__main__':
+    with Mysql() as conn:
+        cursor = conn.cursor(pymysql.cursors.DictCursor)
+        try:
+            sql = "select * from tb_student"
+            cursor.execute(sql)
+            res = cursor.fetchall()
+            print(res)
+        except:
+            print('error')
import pymysql
+
+
+class Mysql(object):
+    def __enter__(self):
+        self.connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+        return self.connection
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.connection.close()
+
+
+if __name__ == '__main__':
+    with Mysql() as conn:
+        cursor = conn.cursor(pymysql.cursors.DictCursor)
+        try:
+            sql = "select * from tb_student"
+            cursor.execute(sql)
+            res = cursor.fetchall()
+            print(res)
+        except:
+            print('error')
`,18),e=[o];function c(t,r,y,E,i,F){return n(),a("div",null,e)}const h=s(p,[["render",c]]);export{d as __pageData,h as default}; diff --git "a/assets/python_web_pymysql\344\275\277\347\224\250.md.b054a10e.lean.js" "b/assets/python_web_pymysql\344\275\277\347\224\250.md.b054a10e.lean.js" new file mode 100644 index 000000000..f6bf6ea5c --- /dev/null +++ "b/assets/python_web_pymysql\344\275\277\347\224\250.md.b054a10e.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const d=JSON.parse('{"title":"pymysql使用","description":"","frontmatter":{},"headers":[],"relativePath":"python/web/pymysql使用.md","filePath":"python/web/pymysql使用.md","lastUpdated":1708510102000}'),p={name:"python/web/pymysql使用.md"},o=l("",18),e=[o];function c(t,r,y,E,i,F){return n(),a("div",null,e)}const h=s(p,[["render",c]]);export{d as __pageData,h as default}; diff --git a/assets/style.cb20272f.css b/assets/style.cb20272f.css new file mode 100644 index 000000000..192232ed7 --- /dev/null +++ b/assets/style.cb20272f.css @@ -0,0 +1 @@ +@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/assets/inter-roman-cyrillic.5f2c6c8c.woff2) format("woff2");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/assets/inter-roman-cyrillic-ext.e75737ce.woff2) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/assets/inter-roman-greek.d5a6d92a.woff2) format("woff2");unicode-range:U+0370-03FF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/assets/inter-roman-greek-ext.ab0619bc.woff2) format("woff2");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/assets/inter-roman-latin.2ed14f66.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/assets/inter-roman-latin-ext.0030eebd.woff2) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/assets/inter-roman-vietnamese.14ce25a6.woff2) format("woff2");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/assets/inter-italic-cyrillic.ea42a392.woff2) format("woff2");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/assets/inter-italic-cyrillic-ext.33bd5a8e.woff2) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/assets/inter-italic-greek.8f4463c4.woff2) format("woff2");unicode-range:U+0370-03FF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/assets/inter-italic-greek-ext.4fbe9427.woff2) format("woff2");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/assets/inter-italic-latin.bd3b6f56.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/assets/inter-italic-latin-ext.bd8920cc.woff2) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/assets/inter-italic-vietnamese.6ce511fb.woff2) format("woff2");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+1EA0-1EF9,U+20AB}@font-face{font-family:Chinese Quotes;src:local("PingFang SC Regular"),local("PingFang SC"),local("SimHei"),local("Source Han Sans SC");unicode-range:U+2018,U+2019,U+201C,U+201D}:root{--vp-c-white: #ffffff;--vp-c-black: #000000;--vp-c-neutral: var(--vp-c-black);--vp-c-neutral-inverse: var(--vp-c-white)}.dark{--vp-c-neutral: var(--vp-c-white);--vp-c-neutral-inverse: var(--vp-c-black)}:root{--vp-c-gray-1: #dddde3;--vp-c-gray-2: #e4e4e9;--vp-c-gray-3: #ebebef;--vp-c-gray-soft: rgba(142, 150, 170, .14);--vp-c-indigo-1: #3451b2;--vp-c-indigo-2: #3a5ccc;--vp-c-indigo-3: #5672cd;--vp-c-indigo-soft: rgba(100, 108, 255, .14);--vp-c-green-1: #18794e;--vp-c-green-2: #299764;--vp-c-green-3: #30a46c;--vp-c-green-soft: rgba(16, 185, 129, .14);--vp-c-yellow-1: #915930;--vp-c-yellow-2: #946300;--vp-c-yellow-3: #9f6a00;--vp-c-yellow-soft: rgba(234, 179, 8, .14);--vp-c-red-1: #b8272c;--vp-c-red-2: #d5393e;--vp-c-red-3: #e0575b;--vp-c-red-soft: rgba(244, 63, 94, .14);--vp-c-sponsor: #db2777}.dark{--vp-c-gray-1: #515c67;--vp-c-gray-2: #414853;--vp-c-gray-3: #32363f;--vp-c-gray-soft: rgba(101, 117, 133, .16);--vp-c-indigo-1: #a8b1ff;--vp-c-indigo-2: #5c73e7;--vp-c-indigo-3: #3e63dd;--vp-c-indigo-soft: rgba(100, 108, 255, .16);--vp-c-green-1: #3dd68c;--vp-c-green-2: #30a46c;--vp-c-green-3: #298459;--vp-c-green-soft: rgba(16, 185, 129, .16);--vp-c-yellow-1: #f9b44e;--vp-c-yellow-2: #da8b17;--vp-c-yellow-3: #a46a0a;--vp-c-yellow-soft: rgba(234, 179, 8, .16);--vp-c-red-1: #f66f81;--vp-c-red-2: #f14158;--vp-c-red-3: #b62a3c;--vp-c-red-soft: rgba(244, 63, 94, .16)}:root{--vp-c-bg: #ffffff;--vp-c-bg-alt: #f6f6f7;--vp-c-bg-elv: #ffffff;--vp-c-bg-soft: #f6f6f7}.dark{--vp-c-bg: #1b1b1f;--vp-c-bg-alt: #161618;--vp-c-bg-elv: #202127;--vp-c-bg-soft: #202127}:root{--vp-c-border: #c2c2c4;--vp-c-divider: #e2e2e3;--vp-c-gutter: #e2e2e3}.dark{--vp-c-border: #3c3f44;--vp-c-divider: #2e2e32;--vp-c-gutter: #000000}:root{--vp-c-text-1: rgba(60, 60, 67);--vp-c-text-2: rgba(60, 60, 67, .78);--vp-c-text-3: rgba(60, 60, 67, .56)}.dark{--vp-c-text-1: rgba(255, 255, 245, .86);--vp-c-text-2: rgba(235, 235, 245, .6);--vp-c-text-3: rgba(235, 235, 245, .38)}:root{--vp-c-default-1: var(--vp-c-gray-1);--vp-c-default-2: var(--vp-c-gray-2);--vp-c-default-3: var(--vp-c-gray-3);--vp-c-default-soft: var(--vp-c-gray-soft);--vp-c-brand-1: var(--vp-c-indigo-1);--vp-c-brand-2: var(--vp-c-indigo-2);--vp-c-brand-3: var(--vp-c-indigo-3);--vp-c-brand-soft: var(--vp-c-indigo-soft);--vp-c-brand: var(--vp-c-brand-1);--vp-c-tip-1: var(--vp-c-brand-1);--vp-c-tip-2: var(--vp-c-brand-2);--vp-c-tip-3: var(--vp-c-brand-3);--vp-c-tip-soft: var(--vp-c-brand-soft);--vp-c-warning-1: var(--vp-c-yellow-1);--vp-c-warning-2: var(--vp-c-yellow-2);--vp-c-warning-3: var(--vp-c-yellow-3);--vp-c-warning-soft: var(--vp-c-yellow-soft);--vp-c-danger-1: var(--vp-c-red-1);--vp-c-danger-2: var(--vp-c-red-2);--vp-c-danger-3: var(--vp-c-red-3);--vp-c-danger-soft: var(--vp-c-red-soft)}:root{--vp-font-family-base: "Chinese Quotes", "Inter var", "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--vp-font-family-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}:root{--vp-shadow-1: 0 1px 2px rgba(0, 0, 0, .04), 0 1px 2px rgba(0, 0, 0, .06);--vp-shadow-2: 0 3px 12px rgba(0, 0, 0, .07), 0 1px 4px rgba(0, 0, 0, .07);--vp-shadow-3: 0 12px 32px rgba(0, 0, 0, .1), 0 2px 6px rgba(0, 0, 0, .08);--vp-shadow-4: 0 14px 44px rgba(0, 0, 0, .12), 0 3px 9px rgba(0, 0, 0, .12);--vp-shadow-5: 0 18px 56px rgba(0, 0, 0, .16), 0 4px 12px rgba(0, 0, 0, .16)}:root{--vp-z-index-local-nav: 10;--vp-z-index-nav: 20;--vp-z-index-layout-top: 30;--vp-z-index-backdrop: 40;--vp-z-index-sidebar: 50;--vp-z-index-footer: 60}:root{--vp-icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' height='20' width='20' stroke='rgba(128,128,128,1)' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2'/%3E%3C/svg%3E");--vp-icon-copied: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' height='20' width='20' stroke='rgba(128,128,128,1)' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2m-6 9 2 2 4-4'/%3E%3C/svg%3E")}:root{--vp-layout-max-width: 1440px}:root{--vp-header-anchor-symbol: "#"}:root{--vp-code-line-height: 1.7;--vp-code-font-size: .875em;--vp-code-color: var(--vp-c-brand-1);--vp-code-link-color: var(--vp-c-brand-1);--vp-code-link-hover-color: var(--vp-c-brand-2);--vp-code-bg: var(--vp-c-default-soft);--vp-code-block-color: var(--vp-c-text-2);--vp-code-block-bg: var(--vp-c-bg-alt);--vp-code-block-divider-color: var(--vp-c-gutter);--vp-code-lang-color: var(--vp-c-text-3);--vp-code-line-highlight-color: var(--vp-c-default-soft);--vp-code-line-number-color: var(--vp-c-text-3);--vp-code-line-diff-add-color: var(--vp-c-green-soft);--vp-code-line-diff-add-symbol-color: var(--vp-c-green-1);--vp-code-line-diff-remove-color: var(--vp-c-red-soft);--vp-code-line-diff-remove-symbol-color: var(--vp-c-red-1);--vp-code-line-warning-color: var(--vp-c-yellow-soft);--vp-code-line-error-color: var(--vp-c-red-soft);--vp-code-copy-code-border-color: var(--vp-c-divider);--vp-code-copy-code-bg: var(--vp-c-bg-soft);--vp-code-copy-code-hover-border-color: var(--vp-c-divider);--vp-code-copy-code-hover-bg: var(--vp-c-bg);--vp-code-copy-code-active-text: var(--vp-c-text-2);--vp-code-copy-copied-text-content: "Copied";--vp-code-tab-divider: var(--vp-code-block-divider-color);--vp-code-tab-text-color: var(--vp-c-text-2);--vp-code-tab-bg: var(--vp-code-block-bg);--vp-code-tab-hover-text-color: var(--vp-c-text-1);--vp-code-tab-active-text-color: var(--vp-c-text-1);--vp-code-tab-active-bar-color: var(--vp-c-brand-1)}:root{--vp-button-brand-border: transparent;--vp-button-brand-text: var(--vp-c-white);--vp-button-brand-bg: var(--vp-c-brand-3);--vp-button-brand-hover-border: transparent;--vp-button-brand-hover-text: var(--vp-c-white);--vp-button-brand-hover-bg: var(--vp-c-brand-2);--vp-button-brand-active-border: transparent;--vp-button-brand-active-text: var(--vp-c-white);--vp-button-brand-active-bg: var(--vp-c-brand-1);--vp-button-alt-border: transparent;--vp-button-alt-text: var(--vp-c-text-1);--vp-button-alt-bg: var(--vp-c-default-3);--vp-button-alt-hover-border: transparent;--vp-button-alt-hover-text: var(--vp-c-text-1);--vp-button-alt-hover-bg: var(--vp-c-default-2);--vp-button-alt-active-border: transparent;--vp-button-alt-active-text: var(--vp-c-text-1);--vp-button-alt-active-bg: var(--vp-c-default-1);--vp-button-sponsor-border: var(--vp-c-text-2);--vp-button-sponsor-text: var(--vp-c-text-2);--vp-button-sponsor-bg: transparent;--vp-button-sponsor-hover-border: var(--vp-c-sponsor);--vp-button-sponsor-hover-text: var(--vp-c-sponsor);--vp-button-sponsor-hover-bg: transparent;--vp-button-sponsor-active-border: var(--vp-c-sponsor);--vp-button-sponsor-active-text: var(--vp-c-sponsor);--vp-button-sponsor-active-bg: transparent}:root{--vp-custom-block-font-size: 14px;--vp-custom-block-code-font-size: 13px;--vp-custom-block-info-border: transparent;--vp-custom-block-info-text: var(--vp-c-text-1);--vp-custom-block-info-bg: var(--vp-c-default-soft);--vp-custom-block-info-code-bg: var(--vp-c-default-soft);--vp-custom-block-tip-border: transparent;--vp-custom-block-tip-text: var(--vp-c-text-1);--vp-custom-block-tip-bg: var(--vp-c-brand-soft);--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);--vp-custom-block-warning-border: transparent;--vp-custom-block-warning-text: var(--vp-c-text-1);--vp-custom-block-warning-bg: var(--vp-c-warning-soft);--vp-custom-block-warning-code-bg: var(--vp-c-warning-soft);--vp-custom-block-danger-border: transparent;--vp-custom-block-danger-text: var(--vp-c-text-1);--vp-custom-block-danger-bg: var(--vp-c-danger-soft);--vp-custom-block-danger-code-bg: var(--vp-c-danger-soft);--vp-custom-block-details-border: var(--vp-custom-block-info-border);--vp-custom-block-details-text: var(--vp-custom-block-info-text);--vp-custom-block-details-bg: var(--vp-custom-block-info-bg);--vp-custom-block-details-code-bg: var(--vp-custom-block-info-code-bg)}:root{--vp-input-border-color: var(--vp-c-border);--vp-input-bg-color: var(--vp-c-bg-alt);--vp-input-switch-bg-color: var(--vp-c-gray-soft)}:root{--vp-nav-height: 64px;--vp-nav-bg-color: var(--vp-c-bg);--vp-nav-screen-bg-color: var(--vp-c-bg);--vp-nav-logo-height: 24px}:root{--vp-local-nav-bg-color: var(--vp-c-bg)}:root{--vp-sidebar-width: 272px;--vp-sidebar-bg-color: var(--vp-c-bg-alt)}:root{--vp-backdrop-bg-color: rgba(0, 0, 0, .6)}:root{--vp-home-hero-name-color: var(--vp-c-brand-1);--vp-home-hero-name-background: transparent;--vp-home-hero-image-background-image: none;--vp-home-hero-image-filter: none}:root{--vp-badge-info-border: transparent;--vp-badge-info-text: var(--vp-c-text-2);--vp-badge-info-bg: var(--vp-c-default-soft);--vp-badge-tip-border: transparent;--vp-badge-tip-text: var(--vp-c-brand-1);--vp-badge-tip-bg: var(--vp-c-brand-soft);--vp-badge-warning-border: transparent;--vp-badge-warning-text: var(--vp-c-warning-1);--vp-badge-warning-bg: var(--vp-c-warning-soft);--vp-badge-danger-border: transparent;--vp-badge-danger-text: var(--vp-c-danger-1);--vp-badge-danger-bg: var(--vp-c-danger-soft)}:root{--vp-carbon-ads-text-color: var(--vp-c-text-1);--vp-carbon-ads-poweredby-color: var(--vp-c-text-2);--vp-carbon-ads-bg-color: var(--vp-c-bg-soft);--vp-carbon-ads-hover-text-color: var(--vp-c-brand-1);--vp-carbon-ads-hover-poweredby-color: var(--vp-c-text-1)}:root{--vp-local-search-bg: var(--vp-c-bg);--vp-local-search-result-bg: var(--vp-c-bg);--vp-local-search-result-border: var(--vp-c-divider);--vp-local-search-result-selected-bg: var(--vp-c-bg);--vp-local-search-result-selected-border: var(--vp-c-brand-1);--vp-local-search-highlight-bg: var(--vp-c-green-1);--vp-local-search-highlight-text: var(--vp-c-black)}@media (prefers-reduced-motion: reduce){*,:before,:after{animation-delay:-1ms!important;animation-duration:1ms!important;animation-iteration-count:1!important;background-attachment:initial!important;scroll-behavior:auto!important;transition-duration:0s!important;transition-delay:0s!important}}*,:before,:after{box-sizing:border-box}html{line-height:1.4;font-size:16px;-webkit-text-size-adjust:100%}html.dark{color-scheme:dark}body{margin:0;width:100%;min-width:320px;min-height:100vh;line-height:24px;font-family:var(--vp-font-family-base);font-size:16px;font-weight:400;color:var(--vp-c-text-1);background-color:var(--vp-c-bg);direction:ltr;font-synthesis:style;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}main{display:block}h1,h2,h3,h4,h5,h6{margin:0;line-height:24px;font-size:16px;font-weight:400}p{margin:0}strong,b{font-weight:600}a,area,button,[role=button],input,label,select,summary,textarea{touch-action:manipulation}a{color:inherit;text-decoration:inherit}ol,ul{list-style:none;margin:0;padding:0}blockquote{margin:0}pre,code,kbd,samp{font-family:var(--vp-font-family-mono)}img,svg,video,canvas,audio,iframe,embed,object{display:block}figure{margin:0}img,video{max-width:100%;height:auto}button,input,optgroup,select,textarea{border:0;padding:0;line-height:inherit;color:inherit}button{padding:0;font-family:inherit;background-color:transparent;background-image:none}button:enabled,[role=button]:enabled{cursor:pointer}button:focus,button:focus-visible{outline:1px dotted;outline:4px auto -webkit-focus-ring-color}button:focus:not(:focus-visible){outline:none!important}input:focus,textarea:focus,select:focus{outline:none}table{border-collapse:collapse}input{background-color:transparent}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:var(--vp-c-text-3)}input::-ms-input-placeholder,textarea::-ms-input-placeholder{color:var(--vp-c-text-3)}input::placeholder,textarea::placeholder{color:var(--vp-c-text-3)}input::-webkit-outer-spin-button,input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}input[type=number]{-moz-appearance:textfield}textarea{resize:vertical}select{-webkit-appearance:none}fieldset{margin:0;padding:0}h1,h2,h3,h4,h5,h6,li,p{overflow-wrap:break-word}vite-error-overlay{z-index:9999}.visually-hidden{position:absolute;width:1px;height:1px;white-space:nowrap;clip:rect(0 0 0 0);clip-path:inset(50%);overflow:hidden}.custom-block{border:1px solid transparent;border-radius:8px;padding:16px 16px 8px;line-height:24px;font-size:var(--vp-custom-block-font-size);color:var(--vp-c-text-2)}.custom-block.info{border-color:var(--vp-custom-block-info-border);color:var(--vp-custom-block-info-text);background-color:var(--vp-custom-block-info-bg)}.custom-block.info a,.custom-block.info code{color:var(--vp-c-brand-1)}.custom-block.info code{background-color:var(--vp-custom-block-info-code-bg)}.custom-block.tip{border-color:var(--vp-custom-block-tip-border);color:var(--vp-custom-block-tip-text);background-color:var(--vp-custom-block-tip-bg)}.custom-block.tip a,.custom-block.tip code{color:var(--vp-c-brand-1)}.custom-block.tip code{background-color:var(--vp-custom-block-tip-code-bg)}.custom-block.warning{border-color:var(--vp-custom-block-warning-border);color:var(--vp-custom-block-warning-text);background-color:var(--vp-custom-block-warning-bg)}.custom-block.warning a,.custom-block.warning code{color:var(--vp-c-warning-1)}.custom-block.warning code{background-color:var(--vp-custom-block-warning-code-bg)}.custom-block.danger{border-color:var(--vp-custom-block-danger-border);color:var(--vp-custom-block-danger-text);background-color:var(--vp-custom-block-danger-bg)}.custom-block.danger a,.custom-block.danger code{color:var(--vp-c-danger-1)}.custom-block.danger code{background-color:var(--vp-custom-block-danger-code-bg)}.custom-block.details{border-color:var(--vp-custom-block-details-border);color:var(--vp-custom-block-details-text);background-color:var(--vp-custom-block-details-bg)}.custom-block.details a{color:var(--vp-c-brand-1)}.custom-block.details code{background-color:var(--vp-custom-block-details-code-bg)}.custom-block-title{font-weight:600}.custom-block p+p{margin:8px 0}.custom-block.details summary{margin:0 0 8px;font-weight:700;cursor:pointer}.custom-block.details summary+p{margin:8px 0}.custom-block a{color:inherit;font-weight:600;text-decoration:underline;text-underline-offset:2px;transition:opacity .25s}.custom-block a:hover{opacity:.75}.custom-block code{font-size:var(--vp-custom-block-code-font-size)}.custom-block.custom-block th,.custom-block.custom-block blockquote>p{font-size:var(--vp-custom-block-font-size);color:inherit}.dark .vp-code-light{display:none}html:not(.dark) .vp-code-dark{display:none}.vp-code-group{margin-top:16px}.vp-code-group .tabs{position:relative;display:flex;margin-right:-24px;margin-left:-24px;padding:0 12px;background-color:var(--vp-code-tab-bg);overflow-x:auto;overflow-y:hidden;box-shadow:inset 0 -1px var(--vp-code-tab-divider)}@media (min-width: 640px){.vp-code-group .tabs{margin-right:0;margin-left:0;border-radius:8px 8px 0 0}}.vp-code-group .tabs input{position:fixed;opacity:0;pointer-events:none}.vp-code-group .tabs label{position:relative;display:inline-block;border-bottom:1px solid transparent;padding:0 12px;line-height:48px;font-size:14px;font-weight:500;color:var(--vp-code-tab-text-color);white-space:nowrap;cursor:pointer;transition:color .25s}.vp-code-group .tabs label:after{position:absolute;right:8px;bottom:-1px;left:8px;z-index:1;height:2px;border-radius:2px;content:"";background-color:transparent;transition:background-color .25s}.vp-code-group label:hover{color:var(--vp-code-tab-hover-text-color)}.vp-code-group input:checked+label{color:var(--vp-code-tab-active-text-color)}.vp-code-group input:checked+label:after{background-color:var(--vp-code-tab-active-bar-color)}.vp-code-group div[class*=language-],.vp-block{display:none;margin-top:0!important;border-top-left-radius:0!important;border-top-right-radius:0!important}.vp-code-group div[class*=language-].active,.vp-block.active{display:block}.vp-block{padding:20px 24px}.vp-doc h1,.vp-doc h2,.vp-doc h3,.vp-doc h4,.vp-doc h5,.vp-doc h6{position:relative;font-weight:600;outline:none}.vp-doc h1{letter-spacing:-.02em;line-height:40px;font-size:28px}.vp-doc h2{margin:48px 0 16px;border-top:1px solid var(--vp-c-divider);padding-top:24px;letter-spacing:-.02em;line-height:32px;font-size:24px}.vp-doc h3{margin:32px 0 0;letter-spacing:-.01em;line-height:28px;font-size:20px}.vp-doc .header-anchor{position:absolute;top:0;left:0;margin-left:-.87em;font-weight:500;-webkit-user-select:none;user-select:none;opacity:0;text-decoration:none;transition:color .25s,opacity .25s}.vp-doc .header-anchor:before{content:var(--vp-header-anchor-symbol)}.vp-doc h1:hover .header-anchor,.vp-doc h1 .header-anchor:focus,.vp-doc h2:hover .header-anchor,.vp-doc h2 .header-anchor:focus,.vp-doc h3:hover .header-anchor,.vp-doc h3 .header-anchor:focus,.vp-doc h4:hover .header-anchor,.vp-doc h4 .header-anchor:focus,.vp-doc h5:hover .header-anchor,.vp-doc h5 .header-anchor:focus,.vp-doc h6:hover .header-anchor,.vp-doc h6 .header-anchor:focus{opacity:1}@media (min-width: 768px){.vp-doc h1{letter-spacing:-.02em;line-height:40px;font-size:32px}}.vp-doc h2 .header-anchor{top:24px}.vp-doc p,.vp-doc summary{margin:16px 0}.vp-doc p{line-height:28px}.vp-doc blockquote{margin:16px 0;border-left:2px solid var(--vp-c-divider);padding-left:16px;transition:border-color .5s}.vp-doc blockquote>p{margin:0;font-size:16px;color:var(--vp-c-text-2);transition:color .5s}.vp-doc a{font-weight:500;color:var(--vp-c-brand-1);text-decoration:underline;text-underline-offset:2px;transition:color .25s,opacity .25s}.vp-doc a:hover{color:var(--vp-c-brand-2)}.vp-doc strong{font-weight:600}.vp-doc ul,.vp-doc ol{padding-left:1.25rem;margin:16px 0}.vp-doc ul{list-style:disc}.vp-doc ol{list-style:decimal}.vp-doc li+li{margin-top:8px}.vp-doc li>ol,.vp-doc li>ul{margin:8px 0 0}.vp-doc table{display:block;border-collapse:collapse;margin:20px 0;overflow-x:auto}.vp-doc tr{border-top:1px solid var(--vp-c-divider);transition:background-color .5s}.vp-doc tr:nth-child(2n){background-color:var(--vp-c-bg-soft)}.vp-doc th,.vp-doc td{border:1px solid var(--vp-c-divider);padding:8px 16px}.vp-doc th{text-align:left;font-size:14px;font-weight:600;color:var(--vp-c-text-2);background-color:var(--vp-c-bg-soft)}.vp-doc td{font-size:14px}.vp-doc hr{margin:16px 0;border:none;border-top:1px solid var(--vp-c-divider)}.vp-doc .custom-block{margin:16px 0}.vp-doc .custom-block p{margin:8px 0;line-height:24px}.vp-doc .custom-block p:first-child{margin:0}.vp-doc .custom-block div[class*=language-]{margin:8px 0;border-radius:8px}.vp-doc .custom-block div[class*=language-] code{font-weight:400;background-color:transparent}.vp-doc .custom-block .vp-code-group .tabs{margin:0;border-radius:8px 8px 0 0}.vp-doc :not(pre,h1,h2,h3,h4,h5,h6)>code{font-size:var(--vp-code-font-size);color:var(--vp-code-color)}.vp-doc :not(pre)>code{border-radius:4px;padding:3px 6px;background-color:var(--vp-code-bg);transition:color .25s,background-color .5s}.vp-doc a>code{color:var(--vp-code-link-color)}.vp-doc a:hover>code{color:var(--vp-code-link-hover-color)}.vp-doc h1>code,.vp-doc h2>code,.vp-doc h3>code{font-size:.9em}.vp-doc div[class*=language-],.vp-block{position:relative;margin:16px -24px;background-color:var(--vp-code-block-bg);overflow-x:auto;transition:background-color .5s}@media (min-width: 640px){.vp-doc div[class*=language-],.vp-block{border-radius:8px;margin:16px 0}}@media (max-width: 639px){.vp-doc li div[class*=language-]{border-radius:8px 0 0 8px}}.vp-doc div[class*=language-]+div[class*=language-],.vp-doc div[class$=-api]+div[class*=language-],.vp-doc div[class*=language-]+div[class$=-api]>div[class*=language-]{margin-top:-8px}.vp-doc [class*=language-] pre,.vp-doc [class*=language-] code{direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}.vp-doc [class*=language-] pre{position:relative;z-index:1;margin:0;padding:20px 0;background:transparent;overflow-x:auto}.vp-doc [class*=language-] code{display:block;padding:0 24px;width:fit-content;min-width:100%;line-height:var(--vp-code-line-height);font-size:var(--vp-code-font-size);color:var(--vp-code-block-color);transition:color .5s}.vp-doc [class*=language-] code .highlighted{background-color:var(--vp-code-line-highlight-color);transition:background-color .5s;margin:0 -24px;padding:0 24px;width:calc(100% + 48px);display:inline-block}.vp-doc [class*=language-] code .highlighted.error{background-color:var(--vp-code-line-error-color)}.vp-doc [class*=language-] code .highlighted.warning{background-color:var(--vp-code-line-warning-color)}.vp-doc [class*=language-] code .diff{transition:background-color .5s;margin:0 -24px;padding:0 24px;width:calc(100% + 48px);display:inline-block}.vp-doc [class*=language-] code .diff:before{position:absolute;left:10px}.vp-doc [class*=language-] .has-focused-lines .line:not(.has-focus){filter:blur(.095rem);opacity:.4;transition:filter .35s,opacity .35s}.vp-doc [class*=language-] .has-focused-lines .line:not(.has-focus){opacity:.7;transition:filter .35s,opacity .35s}.vp-doc [class*=language-]:hover .has-focused-lines .line:not(.has-focus){filter:blur(0);opacity:1}.vp-doc [class*=language-] code .diff.remove{background-color:var(--vp-code-line-diff-remove-color);opacity:.7}.vp-doc [class*=language-] code .diff.remove:before{content:"-";color:var(--vp-code-line-diff-remove-symbol-color)}.vp-doc [class*=language-] code .diff.add{background-color:var(--vp-code-line-diff-add-color)}.vp-doc [class*=language-] code .diff.add:before{content:"+";color:var(--vp-code-line-diff-add-symbol-color)}.vp-doc div[class*=language-].line-numbers-mode{padding-left:32px}.vp-doc .line-numbers-wrapper{position:absolute;top:0;bottom:0;left:0;z-index:3;border-right:1px solid var(--vp-code-block-divider-color);padding-top:20px;width:32px;text-align:center;font-family:var(--vp-font-family-mono);line-height:var(--vp-code-line-height);font-size:var(--vp-code-font-size);color:var(--vp-code-line-number-color);transition:border-color .5s,color .5s}.vp-doc [class*=language-]>button.copy{direction:ltr;position:absolute;top:12px;right:12px;z-index:3;border:1px solid var(--vp-code-copy-code-border-color);border-radius:4px;width:40px;height:40px;background-color:var(--vp-code-copy-code-bg);opacity:0;cursor:pointer;background-image:var(--vp-icon-copy);background-position:50%;background-size:20px;background-repeat:no-repeat;transition:border-color .25s,background-color .25s,opacity .25s}.vp-doc [class*=language-]:hover>button.copy,.vp-doc [class*=language-]>button.copy:focus{opacity:1}.vp-doc [class*=language-]>button.copy:hover,.vp-doc [class*=language-]>button.copy.copied{border-color:var(--vp-code-copy-code-hover-border-color);background-color:var(--vp-code-copy-code-hover-bg)}.vp-doc [class*=language-]>button.copy.copied,.vp-doc [class*=language-]>button.copy:hover.copied{border-radius:0 4px 4px 0;background-color:var(--vp-code-copy-code-hover-bg);background-image:var(--vp-icon-copied)}.vp-doc [class*=language-]>button.copy.copied:before,.vp-doc [class*=language-]>button.copy:hover.copied:before{position:relative;top:-1px;transform:translate(calc(-100% - 1px));display:flex;justify-content:center;align-items:center;border:1px solid var(--vp-code-copy-code-hover-border-color);border-right:0;border-radius:4px 0 0 4px;padding:0 10px;width:fit-content;height:40px;text-align:center;font-size:12px;font-weight:500;color:var(--vp-code-copy-code-active-text);background-color:var(--vp-code-copy-code-hover-bg);white-space:nowrap;content:var(--vp-code-copy-copied-text-content)}.vp-doc [class*=language-]>span.lang{position:absolute;top:2px;right:8px;z-index:2;font-size:12px;font-weight:500;color:var(--vp-code-lang-color);transition:color .4s,opacity .4s}.vp-doc [class*=language-]:hover>button.copy+span.lang,.vp-doc [class*=language-]>button.copy:focus+span.lang{opacity:0}.vp-doc .VPTeamMembers{margin-top:24px}.vp-doc .VPTeamMembers.small.count-1 .container{margin:0!important;max-width:calc((100% - 24px)/2)!important}.vp-doc .VPTeamMembers.small.count-2 .container,.vp-doc .VPTeamMembers.small.count-3 .container{max-width:100%!important}.vp-doc .VPTeamMembers.medium.count-1 .container{margin:0!important;max-width:calc((100% - 24px)/2)!important}:is(.vp-external-link-icon,.vp-doc a[href*="://"],.vp-doc a[target=_blank]):not(.no-icon):after{display:inline-block;margin-top:-1px;margin-left:4px;width:11px;height:11px;background:currentColor;color:var(--vp-c-text-3);flex-shrink:0;--icon: url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M0 0h24v24H0V0z' fill='none' /%3E%3Cpath d='M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z' /%3E%3C/svg%3E");-webkit-mask-image:var(--icon);mask-image:var(--icon)}.vp-external-link-icon:after{content:""}.vp-sponsor{border-radius:16px;overflow:hidden}.vp-sponsor.aside{border-radius:12px}.vp-sponsor-section+.vp-sponsor-section{margin-top:4px}.vp-sponsor-tier{margin-bottom:4px;text-align:center;letter-spacing:1px;line-height:24px;width:100%;font-weight:600;color:var(--vp-c-text-2);background-color:var(--vp-c-bg-soft)}.vp-sponsor.normal .vp-sponsor-tier{padding:13px 0 11px;font-size:14px}.vp-sponsor.aside .vp-sponsor-tier{padding:9px 0 7px;font-size:12px}.vp-sponsor-grid+.vp-sponsor-tier{margin-top:4px}.vp-sponsor-grid{display:flex;flex-wrap:wrap;gap:4px}.vp-sponsor-grid.xmini .vp-sponsor-grid-link{height:64px}.vp-sponsor-grid.xmini .vp-sponsor-grid-image{max-width:64px;max-height:22px}.vp-sponsor-grid.mini .vp-sponsor-grid-link{height:72px}.vp-sponsor-grid.mini .vp-sponsor-grid-image{max-width:96px;max-height:24px}.vp-sponsor-grid.small .vp-sponsor-grid-link{height:96px}.vp-sponsor-grid.small .vp-sponsor-grid-image{max-width:96px;max-height:24px}.vp-sponsor-grid.medium .vp-sponsor-grid-link{height:112px}.vp-sponsor-grid.medium .vp-sponsor-grid-image{max-width:120px;max-height:36px}.vp-sponsor-grid.big .vp-sponsor-grid-link{height:184px}.vp-sponsor-grid.big .vp-sponsor-grid-image{max-width:192px;max-height:56px}.vp-sponsor-grid[data-vp-grid="2"] .vp-sponsor-grid-item{width:calc((100% - 4px)/2)}.vp-sponsor-grid[data-vp-grid="3"] .vp-sponsor-grid-item{width:calc((100% - 4px * 2) / 3)}.vp-sponsor-grid[data-vp-grid="4"] .vp-sponsor-grid-item{width:calc((100% - 12px)/4)}.vp-sponsor-grid[data-vp-grid="5"] .vp-sponsor-grid-item{width:calc((100% - 16px)/5)}.vp-sponsor-grid[data-vp-grid="6"] .vp-sponsor-grid-item{width:calc((100% - 4px * 5) / 6)}.vp-sponsor-grid-item{flex-shrink:0;width:100%;background-color:var(--vp-c-bg-soft);transition:background-color .25s}.vp-sponsor-grid-item:hover{background-color:var(--vp-c-default-soft)}.vp-sponsor-grid-item:hover .vp-sponsor-grid-image{filter:grayscale(0) invert(0)}.vp-sponsor-grid-item.empty:hover{background-color:var(--vp-c-bg-soft)}.dark .vp-sponsor-grid-item:hover{background-color:var(--vp-c-white)}.dark .vp-sponsor-grid-item.empty:hover{background-color:var(--vp-c-bg-soft)}.vp-sponsor-grid-link{display:flex}.vp-sponsor-grid-box{display:flex;justify-content:center;align-items:center;width:100%}.vp-sponsor-grid-image{max-width:100%;filter:grayscale(1);transition:filter .25s}.dark .vp-sponsor-grid-image{filter:grayscale(1) invert(1)}.VPBadge[data-v-9613cc9f]{display:inline-block;margin-left:2px;border:1px solid transparent;border-radius:12px;padding:0 10px;line-height:22px;font-size:12px;font-weight:500;transform:translateY(-2px)}.vp-doc h1>.VPBadge[data-v-9613cc9f]{margin-top:4px;vertical-align:top}.vp-doc h2>.VPBadge[data-v-9613cc9f]{margin-top:3px;padding:0 8px;vertical-align:top}.vp-doc h3>.VPBadge[data-v-9613cc9f]{vertical-align:middle}.vp-doc h4>.VPBadge[data-v-9613cc9f],.vp-doc h5>.VPBadge[data-v-9613cc9f],.vp-doc h6>.VPBadge[data-v-9613cc9f]{vertical-align:middle;line-height:18px}.VPBadge.info[data-v-9613cc9f]{border-color:var(--vp-badge-info-border);color:var(--vp-badge-info-text);background-color:var(--vp-badge-info-bg)}.VPBadge.tip[data-v-9613cc9f]{border-color:var(--vp-badge-tip-border);color:var(--vp-badge-tip-text);background-color:var(--vp-badge-tip-bg)}.VPBadge.warning[data-v-9613cc9f]{border-color:var(--vp-badge-warning-border);color:var(--vp-badge-warning-text);background-color:var(--vp-badge-warning-bg)}.VPBadge.danger[data-v-9613cc9f]{border-color:var(--vp-badge-danger-border);color:var(--vp-badge-danger-text);background-color:var(--vp-badge-danger-bg)}.VPBackdrop[data-v-c79a1216]{position:fixed;top:0;right:0;bottom:0;left:0;z-index:var(--vp-z-index-backdrop);background:var(--vp-backdrop-bg-color);transition:opacity .5s}.VPBackdrop.fade-enter-from[data-v-c79a1216],.VPBackdrop.fade-leave-to[data-v-c79a1216]{opacity:0}.VPBackdrop.fade-leave-active[data-v-c79a1216]{transition-duration:.25s}@media (min-width: 1280px){.VPBackdrop[data-v-c79a1216]{display:none}}.NotFound[data-v-f87ff6e4]{padding:64px 24px 96px;text-align:center}@media (min-width: 768px){.NotFound[data-v-f87ff6e4]{padding:96px 32px 168px}}.code[data-v-f87ff6e4]{line-height:64px;font-size:64px;font-weight:600}.title[data-v-f87ff6e4]{padding-top:12px;letter-spacing:2px;line-height:20px;font-size:20px;font-weight:700}.divider[data-v-f87ff6e4]{margin:24px auto 18px;width:64px;height:1px;background-color:var(--vp-c-divider)}.quote[data-v-f87ff6e4]{margin:0 auto;max-width:256px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.action[data-v-f87ff6e4]{padding-top:20px}.link[data-v-f87ff6e4]{display:inline-block;border:1px solid var(--vp-c-brand-1);border-radius:16px;padding:3px 16px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:border-color .25s,color .25s}.link[data-v-f87ff6e4]:hover{border-color:var(--vp-c-brand-2);color:var(--vp-c-brand-2)}.root[data-v-d0ee3533]{position:relative;z-index:1}.nested[data-v-d0ee3533]{padding-left:16px}.outline-link[data-v-d0ee3533]{display:block;line-height:28px;color:var(--vp-c-text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .5s;font-weight:400}.outline-link[data-v-d0ee3533]:hover,.outline-link.active[data-v-d0ee3533]{color:var(--vp-c-text-1);transition:color .25s}.outline-link.nested[data-v-d0ee3533]{padding-left:13px}.VPDocAsideOutline[data-v-6ae8e080]{display:none}.VPDocAsideOutline.has-outline[data-v-6ae8e080]{display:block}.content[data-v-6ae8e080]{position:relative;border-left:1px solid var(--vp-c-divider);padding-left:16px;font-size:13px;font-weight:500}.outline-marker[data-v-6ae8e080]{position:absolute;top:32px;left:-1px;z-index:0;opacity:0;width:2px;border-radius:2px;height:18px;background-color:var(--vp-c-brand-1);transition:top .25s cubic-bezier(0,1,.5,1),background-color .5s,opacity .25s}.outline-title[data-v-6ae8e080]{letter-spacing:.4px;line-height:28px;font-size:13px;font-weight:600}.VPDocAside[data-v-3f215769]{display:flex;flex-direction:column;flex-grow:1}.spacer[data-v-3f215769]{flex-grow:1}.VPDocAside[data-v-3f215769] .spacer+.VPDocAsideSponsors,.VPDocAside[data-v-3f215769] .spacer+.VPDocAsideCarbonAds{margin-top:24px}.VPDocAside[data-v-3f215769] .VPDocAsideSponsors+.VPDocAsideCarbonAds{margin-top:16px}.VPLastUpdated[data-v-7de715c0]{line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}@media (min-width: 640px){.VPLastUpdated[data-v-7de715c0]{line-height:32px;font-size:14px;font-weight:500}}.VPDocFooter[data-v-ef5dee53]{margin-top:64px}.edit-info[data-v-ef5dee53]{padding-bottom:18px}@media (min-width: 640px){.edit-info[data-v-ef5dee53]{display:flex;justify-content:space-between;align-items:center;padding-bottom:14px}}.edit-link-button[data-v-ef5dee53]{display:flex;align-items:center;border:0;line-height:32px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:color .25s}.edit-link-button[data-v-ef5dee53]:hover{color:var(--vp-c-brand-2)}.edit-link-icon[data-v-ef5dee53]{margin-right:8px;width:14px;height:14px;fill:currentColor}.prev-next[data-v-ef5dee53]{border-top:1px solid var(--vp-c-divider);padding-top:24px;display:grid;grid-row-gap:8px}@media (min-width: 640px){.prev-next[data-v-ef5dee53]{grid-template-columns:repeat(2,1fr);grid-column-gap:16px}}.pager-link[data-v-ef5dee53]{display:block;border:1px solid var(--vp-c-divider);border-radius:8px;padding:11px 16px 13px;width:100%;height:100%;transition:border-color .25s}.pager-link[data-v-ef5dee53]:hover{border-color:var(--vp-c-brand-1)}.pager-link.next[data-v-ef5dee53]{margin-left:auto;text-align:right}.desc[data-v-ef5dee53]{display:block;line-height:20px;font-size:12px;font-weight:500;color:var(--vp-c-text-2)}.title[data-v-ef5dee53]{display:block;line-height:20px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:color .25s}.VPDocOutlineDropdown[data-v-eadfb36b]{margin-bottom:48px}.VPDocOutlineDropdown button[data-v-eadfb36b]{display:block;font-size:14px;font-weight:500;line-height:24px;border:1px solid var(--vp-c-border);padding:4px 12px;color:var(--vp-c-text-2);background-color:var(--vp-c-default-soft);border-radius:8px;transition:color .5s}.VPDocOutlineDropdown button[data-v-eadfb36b]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPDocOutlineDropdown button.open[data-v-eadfb36b]{color:var(--vp-c-text-1)}.icon[data-v-eadfb36b]{display:inline-block;vertical-align:middle;width:16px;height:16px;fill:currentColor}[data-v-eadfb36b] .outline-link{font-size:14px;font-weight:400}.open>.icon[data-v-eadfb36b]{transform:rotate(90deg)}.items[data-v-eadfb36b]{margin-top:12px;border-left:1px solid var(--vp-c-divider)}.VPDoc[data-v-6b87e69f]{padding:32px 24px 96px;width:100%}.VPDoc .VPDocOutlineDropdown[data-v-6b87e69f]{display:none}@media (min-width: 960px) and (max-width: 1279px){.VPDoc .VPDocOutlineDropdown[data-v-6b87e69f]{display:block}}@media (min-width: 768px){.VPDoc[data-v-6b87e69f]{padding:48px 32px 128px}}@media (min-width: 960px){.VPDoc[data-v-6b87e69f]{padding:32px 32px 0}.VPDoc:not(.has-sidebar) .container[data-v-6b87e69f]{display:flex;justify-content:center;max-width:992px}.VPDoc:not(.has-sidebar) .content[data-v-6b87e69f]{max-width:752px}}@media (min-width: 1280px){.VPDoc .container[data-v-6b87e69f]{display:flex;justify-content:center}.VPDoc .aside[data-v-6b87e69f]{display:block}}@media (min-width: 1440px){.VPDoc:not(.has-sidebar) .content[data-v-6b87e69f]{max-width:784px}.VPDoc:not(.has-sidebar) .container[data-v-6b87e69f]{max-width:1104px}}.container[data-v-6b87e69f]{margin:0 auto;width:100%}.aside[data-v-6b87e69f]{position:relative;display:none;order:2;flex-grow:1;padding-left:32px;width:100%;max-width:256px}.left-aside[data-v-6b87e69f]{order:1;padding-left:unset;padding-right:32px}.aside-container[data-v-6b87e69f]{position:fixed;top:0;padding-top:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + var(--vp-doc-top-height, 0px) + 32px);width:224px;height:100vh;overflow-x:hidden;overflow-y:auto;scrollbar-width:none}.aside-container[data-v-6b87e69f]::-webkit-scrollbar{display:none}.aside-curtain[data-v-6b87e69f]{position:fixed;bottom:0;z-index:10;width:224px;height:32px;background:linear-gradient(transparent,var(--vp-c-bg) 70%)}.aside-content[data-v-6b87e69f]{display:flex;flex-direction:column;min-height:calc(100vh - (var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 32px));padding-bottom:32px}.content[data-v-6b87e69f]{position:relative;margin:0 auto;width:100%}@media (min-width: 960px){.content[data-v-6b87e69f]{padding:0 32px 128px}}@media (min-width: 1280px){.content[data-v-6b87e69f]{order:1;margin:0;min-width:640px}}.content-container[data-v-6b87e69f]{margin:0 auto}.VPDoc.has-aside .content-container[data-v-6b87e69f]{max-width:688px}.external-link-icon-enabled :is(.vp-doc a[href*="://"][data-v-6b87e69f],.vp-doc a[target=_blank][data-v-6b87e69f]):after{content:"";color:currentColor}.VPButton[data-v-c1c5efc1]{display:inline-block;border:1px solid transparent;text-align:center;font-weight:600;white-space:nowrap;transition:color .25s,border-color .25s,background-color .25s}.VPButton[data-v-c1c5efc1]:active{transition:color .1s,border-color .1s,background-color .1s}.VPButton.medium[data-v-c1c5efc1]{border-radius:20px;padding:0 20px;line-height:38px;font-size:14px}.VPButton.big[data-v-c1c5efc1]{border-radius:24px;padding:0 24px;line-height:46px;font-size:16px}.VPButton.brand[data-v-c1c5efc1]{border-color:var(--vp-button-brand-border);color:var(--vp-button-brand-text);background-color:var(--vp-button-brand-bg)}.VPButton.brand[data-v-c1c5efc1]:hover{border-color:var(--vp-button-brand-hover-border);color:var(--vp-button-brand-hover-text);background-color:var(--vp-button-brand-hover-bg)}.VPButton.brand[data-v-c1c5efc1]:active{border-color:var(--vp-button-brand-active-border);color:var(--vp-button-brand-active-text);background-color:var(--vp-button-brand-active-bg)}.VPButton.alt[data-v-c1c5efc1]{border-color:var(--vp-button-alt-border);color:var(--vp-button-alt-text);background-color:var(--vp-button-alt-bg)}.VPButton.alt[data-v-c1c5efc1]:hover{border-color:var(--vp-button-alt-hover-border);color:var(--vp-button-alt-hover-text);background-color:var(--vp-button-alt-hover-bg)}.VPButton.alt[data-v-c1c5efc1]:active{border-color:var(--vp-button-alt-active-border);color:var(--vp-button-alt-active-text);background-color:var(--vp-button-alt-active-bg)}.VPButton.sponsor[data-v-c1c5efc1]{border-color:var(--vp-button-sponsor-border);color:var(--vp-button-sponsor-text);background-color:var(--vp-button-sponsor-bg)}.VPButton.sponsor[data-v-c1c5efc1]:hover{border-color:var(--vp-button-sponsor-hover-border);color:var(--vp-button-sponsor-hover-text);background-color:var(--vp-button-sponsor-hover-bg)}.VPButton.sponsor[data-v-c1c5efc1]:active{border-color:var(--vp-button-sponsor-active-border);color:var(--vp-button-sponsor-active-text);background-color:var(--vp-button-sponsor-active-bg)}html:not(.dark) .VPImage.dark[data-v-8426fc1a]{display:none}.dark .VPImage.light[data-v-8426fc1a]{display:none}.VPHero[data-v-da5d1713]{margin-top:calc((var(--vp-nav-height) + var(--vp-layout-top-height, 0px)) * -1);padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px) 24px 48px}@media (min-width: 640px){.VPHero[data-v-da5d1713]{padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 48px 64px}}@media (min-width: 960px){.VPHero[data-v-da5d1713]{padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 64px 64px}}.container[data-v-da5d1713]{display:flex;flex-direction:column;margin:0 auto;max-width:1152px}@media (min-width: 960px){.container[data-v-da5d1713]{flex-direction:row}}.main[data-v-da5d1713]{position:relative;z-index:10;order:2;flex-grow:1;flex-shrink:0}.VPHero.has-image .container[data-v-da5d1713]{text-align:center}@media (min-width: 960px){.VPHero.has-image .container[data-v-da5d1713]{text-align:left}}@media (min-width: 960px){.main[data-v-da5d1713]{order:1;width:calc((100% / 3) * 2)}.VPHero.has-image .main[data-v-da5d1713]{max-width:592px}}.name[data-v-da5d1713],.text[data-v-da5d1713]{max-width:392px;letter-spacing:-.4px;line-height:40px;font-size:32px;font-weight:700;white-space:pre-wrap}.VPHero.has-image .name[data-v-da5d1713],.VPHero.has-image .text[data-v-da5d1713]{margin:0 auto}.name[data-v-da5d1713]{color:var(--vp-home-hero-name-color)}.clip[data-v-da5d1713]{background:var(--vp-home-hero-name-background);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:var(--vp-home-hero-name-color)}@media (min-width: 640px){.name[data-v-da5d1713],.text[data-v-da5d1713]{max-width:576px;line-height:56px;font-size:48px}}@media (min-width: 960px){.name[data-v-da5d1713],.text[data-v-da5d1713]{line-height:64px;font-size:56px}.VPHero.has-image .name[data-v-da5d1713],.VPHero.has-image .text[data-v-da5d1713]{margin:0}}.tagline[data-v-da5d1713]{padding-top:8px;max-width:392px;line-height:28px;font-size:18px;font-weight:500;white-space:pre-wrap;color:var(--vp-c-text-2)}.VPHero.has-image .tagline[data-v-da5d1713]{margin:0 auto}@media (min-width: 640px){.tagline[data-v-da5d1713]{padding-top:12px;max-width:576px;line-height:32px;font-size:20px}}@media (min-width: 960px){.tagline[data-v-da5d1713]{line-height:36px;font-size:24px}.VPHero.has-image .tagline[data-v-da5d1713]{margin:0}}.actions[data-v-da5d1713]{display:flex;flex-wrap:wrap;margin:-6px;padding-top:24px}.VPHero.has-image .actions[data-v-da5d1713]{justify-content:center}@media (min-width: 640px){.actions[data-v-da5d1713]{padding-top:32px}}@media (min-width: 960px){.VPHero.has-image .actions[data-v-da5d1713]{justify-content:flex-start}}.action[data-v-da5d1713]{flex-shrink:0;padding:6px}.image[data-v-da5d1713]{order:1;margin:-76px -24px -48px}@media (min-width: 640px){.image[data-v-da5d1713]{margin:-108px -24px -48px}}@media (min-width: 960px){.image[data-v-da5d1713]{flex-grow:1;order:2;margin:0;min-height:100%}}.image-container[data-v-da5d1713]{position:relative;margin:0 auto;width:320px;height:320px}@media (min-width: 640px){.image-container[data-v-da5d1713]{width:392px;height:392px}}@media (min-width: 960px){.image-container[data-v-da5d1713]{display:flex;justify-content:center;align-items:center;width:100%;height:100%;transform:translate(-32px,-32px)}}.image-bg[data-v-da5d1713]{position:absolute;top:50%;left:50%;border-radius:50%;width:192px;height:192px;background-image:var(--vp-home-hero-image-background-image);filter:var(--vp-home-hero-image-filter);transform:translate(-50%,-50%)}@media (min-width: 640px){.image-bg[data-v-da5d1713]{width:256px;height:256px}}@media (min-width: 960px){.image-bg[data-v-da5d1713]{width:320px;height:320px}}[data-v-da5d1713] .image-src{position:absolute;top:50%;left:50%;max-width:192px;max-height:192px;transform:translate(-50%,-50%)}@media (min-width: 640px){[data-v-da5d1713] .image-src{max-width:256px;max-height:256px}}@media (min-width: 960px){[data-v-da5d1713] .image-src{max-width:320px;max-height:320px}}.VPFeature[data-v-3e216711]{display:block;border:1px solid var(--vp-c-bg-soft);border-radius:12px;height:100%;background-color:var(--vp-c-bg-soft);transition:border-color .25s,background-color .25s}.VPFeature.link[data-v-3e216711]:hover{border-color:var(--vp-c-brand-1)}.box[data-v-3e216711]{display:flex;flex-direction:column;padding:24px;height:100%}.VPFeature[data-v-3e216711] .VPImage{margin-bottom:20px}.icon[data-v-3e216711]{display:flex;justify-content:center;align-items:center;margin-bottom:20px;border-radius:6px;background-color:var(--vp-c-default-soft);width:48px;height:48px;font-size:24px;transition:background-color .25s}.title[data-v-3e216711]{line-height:24px;font-size:16px;font-weight:600}.details[data-v-3e216711]{flex-grow:1;padding-top:8px;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.link-text[data-v-3e216711]{padding-top:8px}.link-text-value[data-v-3e216711]{display:flex;align-items:center;font-size:14px;font-weight:500;color:var(--vp-c-brand-1)}.link-text-icon[data-v-3e216711]{display:inline-block;margin-left:6px;width:14px;height:14px;fill:currentColor}.VPFeatures[data-v-39646fad]{position:relative;padding:0 24px}@media (min-width: 640px){.VPFeatures[data-v-39646fad]{padding:0 48px}}@media (min-width: 960px){.VPFeatures[data-v-39646fad]{padding:0 64px}}.container[data-v-39646fad]{margin:0 auto;max-width:1152px}.items[data-v-39646fad]{display:flex;flex-wrap:wrap;margin:-8px}.item[data-v-39646fad]{padding:8px;width:100%}@media (min-width: 640px){.item.grid-2[data-v-39646fad],.item.grid-4[data-v-39646fad],.item.grid-6[data-v-39646fad]{width:50%}}@media (min-width: 768px){.item.grid-2[data-v-39646fad],.item.grid-4[data-v-39646fad]{width:50%}.item.grid-3[data-v-39646fad],.item.grid-6[data-v-39646fad]{width:calc(100% / 3)}}@media (min-width: 960px){.item.grid-4[data-v-39646fad]{width:25%}}.VPHome[data-v-d82743a8]{padding-bottom:96px}.VPHome[data-v-d82743a8] .VPHomeSponsors{margin-top:112px;margin-bottom:-128px}@media (min-width: 768px){.VPHome[data-v-d82743a8]{padding-bottom:128px}}.VPContent[data-v-669faec9]{flex-grow:1;flex-shrink:0;margin:var(--vp-layout-top-height, 0px) auto 0;width:100%}.VPContent.is-home[data-v-669faec9]{width:100%;max-width:100%}.VPContent.has-sidebar[data-v-669faec9]{margin:0}@media (min-width: 960px){.VPContent[data-v-669faec9]{padding-top:var(--vp-nav-height)}.VPContent.has-sidebar[data-v-669faec9]{margin:var(--vp-layout-top-height, 0px) 0 0;padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPContent.has-sidebar[data-v-669faec9]{padding-right:calc((100vw - var(--vp-layout-max-width)) / 2);padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.VPFooter[data-v-e03eb2e1]{position:relative;z-index:var(--vp-z-index-footer);border-top:1px solid var(--vp-c-gutter);padding:32px 24px;background-color:var(--vp-c-bg)}.VPFooter.has-sidebar[data-v-e03eb2e1]{display:none}@media (min-width: 768px){.VPFooter[data-v-e03eb2e1]{padding:32px}}.container[data-v-e03eb2e1]{margin:0 auto;max-width:var(--vp-layout-max-width);text-align:center}.message[data-v-e03eb2e1],.copyright[data-v-e03eb2e1]{line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.VPLocalNavOutlineDropdown[data-v-1c15a60a]{padding:12px 20px 11px}.VPLocalNavOutlineDropdown button[data-v-1c15a60a]{display:block;font-size:12px;font-weight:500;line-height:24px;color:var(--vp-c-text-2);transition:color .5s;position:relative}.VPLocalNavOutlineDropdown button[data-v-1c15a60a]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPLocalNavOutlineDropdown button.open[data-v-1c15a60a]{color:var(--vp-c-text-1)}.icon[data-v-1c15a60a]{display:inline-block;vertical-align:middle;margin-left:2px;width:14px;height:14px;fill:currentColor}[data-v-1c15a60a] .outline-link{font-size:14px;padding:2px 0}.open>.icon[data-v-1c15a60a]{transform:rotate(90deg)}.items[data-v-1c15a60a]{position:absolute;top:64px;right:16px;left:16px;display:grid;gap:1px;border:1px solid var(--vp-c-border);border-radius:8px;background-color:var(--vp-c-gutter);max-height:calc(var(--vp-vh, 100vh) - 86px);overflow:hidden auto;box-shadow:var(--vp-shadow-3)}.header[data-v-1c15a60a]{background-color:var(--vp-c-bg-soft)}.top-link[data-v-1c15a60a]{display:block;padding:0 16px;line-height:48px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1)}.outline[data-v-1c15a60a]{padding:8px 0;background-color:var(--vp-c-bg-soft)}.flyout-enter-active[data-v-1c15a60a]{transition:all .2s ease-out}.flyout-leave-active[data-v-1c15a60a]{transition:all .15s ease-in}.flyout-enter-from[data-v-1c15a60a],.flyout-leave-to[data-v-1c15a60a]{opacity:0;transform:translateY(-16px)}.VPLocalNav[data-v-79c8c1df]{position:sticky;top:0;left:0;z-index:var(--vp-z-index-local-nav);display:flex;justify-content:space-between;align-items:center;border-top:1px solid var(--vp-c-gutter);border-bottom:1px solid var(--vp-c-gutter);padding-top:var(--vp-layout-top-height, 0px);width:100%;background-color:var(--vp-local-nav-bg-color)}.VPLocalNav.fixed[data-v-79c8c1df]{position:fixed}.VPLocalNav.reached-top[data-v-79c8c1df]{border-top-color:transparent}@media (min-width: 960px){.VPLocalNav[data-v-79c8c1df]{display:none}}.menu[data-v-79c8c1df]{display:flex;align-items:center;padding:12px 24px 11px;line-height:24px;font-size:12px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.menu[data-v-79c8c1df]:hover{color:var(--vp-c-text-1);transition:color .25s}@media (min-width: 768px){.menu[data-v-79c8c1df]{padding:0 32px}}.menu-icon[data-v-79c8c1df]{margin-right:8px;width:16px;height:16px;fill:currentColor}.VPOutlineDropdown[data-v-79c8c1df]{padding:12px 24px 11px}@media (min-width: 768px){.VPOutlineDropdown[data-v-79c8c1df]{padding:12px 32px 11px}}.VPSwitch[data-v-b1685198]{position:relative;border-radius:11px;display:block;width:40px;height:22px;flex-shrink:0;border:1px solid var(--vp-input-border-color);background-color:var(--vp-input-switch-bg-color);transition:border-color .25s!important}.VPSwitch[data-v-b1685198]:hover{border-color:var(--vp-c-brand-1)}.check[data-v-b1685198]{position:absolute;top:1px;left:1px;width:18px;height:18px;border-radius:50%;background-color:var(--vp-c-neutral-inverse);box-shadow:var(--vp-shadow-1);transition:transform .25s!important}.icon[data-v-b1685198]{position:relative;display:block;width:18px;height:18px;border-radius:50%;overflow:hidden}.icon[data-v-b1685198] svg{position:absolute;top:3px;left:3px;width:12px;height:12px;fill:var(--vp-c-text-2)}.dark .icon[data-v-b1685198] svg{fill:var(--vp-c-text-1);transition:opacity .25s!important}.sun[data-v-ce54a7d1]{opacity:1}.moon[data-v-ce54a7d1],.dark .sun[data-v-ce54a7d1]{opacity:0}.dark .moon[data-v-ce54a7d1]{opacity:1}.dark .VPSwitchAppearance[data-v-ce54a7d1] .check{transform:translate(18px)}.VPNavBarAppearance[data-v-f6a63727]{display:none}@media (min-width: 1280px){.VPNavBarAppearance[data-v-f6a63727]{display:flex;align-items:center}}.VPMenuGroup+.VPMenuLink[data-v-43f1e123]{margin:12px -12px 0;border-top:1px solid var(--vp-c-divider);padding:12px 12px 0}.link[data-v-43f1e123]{display:block;border-radius:6px;padding:0 12px;line-height:32px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);white-space:nowrap;transition:background-color .25s,color .25s}.link[data-v-43f1e123]:hover{color:var(--vp-c-brand-1);background-color:var(--vp-c-default-soft)}.link.active[data-v-43f1e123]{color:var(--vp-c-brand-1)}.VPMenuGroup[data-v-69e747b5]{margin:12px -12px 0;border-top:1px solid var(--vp-c-divider);padding:12px 12px 0}.VPMenuGroup[data-v-69e747b5]:first-child{margin-top:0;border-top:0;padding-top:0}.VPMenuGroup+.VPMenuGroup[data-v-69e747b5]{margin-top:12px;border-top:1px solid var(--vp-c-divider)}.title[data-v-69e747b5]{padding:0 12px;line-height:32px;font-size:14px;font-weight:600;color:var(--vp-c-text-2);white-space:nowrap;transition:color .25s}.VPMenu[data-v-e7ea1737]{border-radius:12px;padding:12px;min-width:128px;border:1px solid var(--vp-c-divider);background-color:var(--vp-c-bg-elv);box-shadow:var(--vp-shadow-3);transition:background-color .5s;max-height:calc(100vh - var(--vp-nav-height));overflow-y:auto}.VPMenu[data-v-e7ea1737] .group{margin:0 -12px;padding:0 12px 12px}.VPMenu[data-v-e7ea1737] .group+.group{border-top:1px solid var(--vp-c-divider);padding:11px 12px 12px}.VPMenu[data-v-e7ea1737] .group:last-child{padding-bottom:0}.VPMenu[data-v-e7ea1737] .group+.item{border-top:1px solid var(--vp-c-divider);padding:11px 16px 0}.VPMenu[data-v-e7ea1737] .item{padding:0 16px;white-space:nowrap}.VPMenu[data-v-e7ea1737] .label{flex-grow:1;line-height:28px;font-size:12px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.VPMenu[data-v-e7ea1737] .action{padding-left:24px}.VPFlyout[data-v-9c007e85]{position:relative}.VPFlyout[data-v-9c007e85]:hover{color:var(--vp-c-brand-1);transition:color .25s}.VPFlyout:hover .text[data-v-9c007e85]{color:var(--vp-c-text-2)}.VPFlyout:hover .icon[data-v-9c007e85]{fill:var(--vp-c-text-2)}.VPFlyout.active .text[data-v-9c007e85]{color:var(--vp-c-brand-1)}.VPFlyout.active:hover .text[data-v-9c007e85]{color:var(--vp-c-brand-2)}.VPFlyout:hover .menu[data-v-9c007e85],.button[aria-expanded=true]+.menu[data-v-9c007e85]{opacity:1;visibility:visible;transform:translateY(0)}.button[aria-expanded=false]+.menu[data-v-9c007e85]{opacity:0;visibility:hidden;transform:translateY(0)}.button[data-v-9c007e85]{display:flex;align-items:center;padding:0 12px;height:var(--vp-nav-height);color:var(--vp-c-text-1);transition:color .5s}.text[data-v-9c007e85]{display:flex;align-items:center;line-height:var(--vp-nav-height);font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.option-icon[data-v-9c007e85]{margin-right:0;width:16px;height:16px;fill:currentColor}.text-icon[data-v-9c007e85]{margin-left:4px;width:14px;height:14px;fill:currentColor}.icon[data-v-9c007e85]{width:20px;height:20px;fill:currentColor;transition:fill .25s}.menu[data-v-9c007e85]{position:absolute;top:calc(var(--vp-nav-height) / 2 + 20px);right:0;opacity:0;visibility:hidden;transition:opacity .25s,visibility .25s,transform .25s}.VPSocialLink[data-v-f80f8133]{display:flex;justify-content:center;align-items:center;width:36px;height:36px;color:var(--vp-c-text-2);transition:color .5s}.VPSocialLink[data-v-f80f8133]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPSocialLink[data-v-f80f8133]>svg{width:20px;height:20px;fill:currentColor}.VPSocialLinks[data-v-7bc22406]{display:flex;justify-content:center}.VPNavBarExtra[data-v-40855f84]{display:none;margin-right:-12px}@media (min-width: 768px){.VPNavBarExtra[data-v-40855f84]{display:block}}@media (min-width: 1280px){.VPNavBarExtra[data-v-40855f84]{display:none}}.trans-title[data-v-40855f84]{padding:0 24px 0 12px;line-height:32px;font-size:14px;font-weight:700;color:var(--vp-c-text-1)}.item.appearance[data-v-40855f84],.item.social-links[data-v-40855f84]{display:flex;align-items:center;padding:0 12px}.item.appearance[data-v-40855f84]{min-width:176px}.appearance-action[data-v-40855f84]{margin-right:-2px}.social-links-list[data-v-40855f84]{margin:-4px -8px}.VPNavBarHamburger[data-v-e5dd9c1c]{display:flex;justify-content:center;align-items:center;width:48px;height:var(--vp-nav-height)}@media (min-width: 768px){.VPNavBarHamburger[data-v-e5dd9c1c]{display:none}}.container[data-v-e5dd9c1c]{position:relative;width:16px;height:14px;overflow:hidden}.VPNavBarHamburger:hover .top[data-v-e5dd9c1c]{top:0;left:0;transform:translate(4px)}.VPNavBarHamburger:hover .middle[data-v-e5dd9c1c]{top:6px;left:0;transform:translate(0)}.VPNavBarHamburger:hover .bottom[data-v-e5dd9c1c]{top:12px;left:0;transform:translate(8px)}.VPNavBarHamburger.active .top[data-v-e5dd9c1c]{top:6px;transform:translate(0) rotate(225deg)}.VPNavBarHamburger.active .middle[data-v-e5dd9c1c]{top:6px;transform:translate(16px)}.VPNavBarHamburger.active .bottom[data-v-e5dd9c1c]{top:6px;transform:translate(0) rotate(135deg)}.VPNavBarHamburger.active:hover .top[data-v-e5dd9c1c],.VPNavBarHamburger.active:hover .middle[data-v-e5dd9c1c],.VPNavBarHamburger.active:hover .bottom[data-v-e5dd9c1c]{background-color:var(--vp-c-text-2);transition:top .25s,background-color .25s,transform .25s}.top[data-v-e5dd9c1c],.middle[data-v-e5dd9c1c],.bottom[data-v-e5dd9c1c]{position:absolute;width:16px;height:2px;background-color:var(--vp-c-text-1);transition:top .25s,background-color .5s,transform .25s}.top[data-v-e5dd9c1c]{top:0;left:0;transform:translate(0)}.middle[data-v-e5dd9c1c]{top:6px;left:0;transform:translate(8px)}.bottom[data-v-e5dd9c1c]{top:12px;left:0;transform:translate(4px)}.VPNavBarMenuLink[data-v-42ef59de]{display:flex;align-items:center;padding:0 12px;line-height:var(--vp-nav-height);font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.VPNavBarMenuLink.active[data-v-42ef59de],.VPNavBarMenuLink[data-v-42ef59de]:hover{color:var(--vp-c-brand-1)}.VPNavBarMenu[data-v-7f418b0f]{display:none}@media (min-width: 768px){.VPNavBarMenu[data-v-7f418b0f]{display:flex}}/*! @docsearch/css 3.5.2 | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */:root{--docsearch-primary-color:#5468ff;--docsearch-text-color:#1c1e21;--docsearch-spacing:12px;--docsearch-icon-stroke-width:1.4;--docsearch-highlight-color:var(--docsearch-primary-color);--docsearch-muted-color:#969faf;--docsearch-container-background:rgba(101,108,133,.8);--docsearch-logo-color:#5468ff;--docsearch-modal-width:560px;--docsearch-modal-height:600px;--docsearch-modal-background:#f5f6f7;--docsearch-modal-shadow:inset 1px 1px 0 0 hsla(0,0%,100%,.5),0 3px 8px 0 #555a64;--docsearch-searchbox-height:56px;--docsearch-searchbox-background:#ebedf0;--docsearch-searchbox-focus-background:#fff;--docsearch-searchbox-shadow:inset 0 0 0 2px var(--docsearch-primary-color);--docsearch-hit-height:56px;--docsearch-hit-color:#444950;--docsearch-hit-active-color:#fff;--docsearch-hit-background:#fff;--docsearch-hit-shadow:0 1px 3px 0 #d4d9e1;--docsearch-key-gradient:linear-gradient(-225deg,#d5dbe4,#f8f8f8);--docsearch-key-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 2px 1px rgba(30,35,90,.4);--docsearch-footer-height:44px;--docsearch-footer-background:#fff;--docsearch-footer-shadow:0 -1px 0 0 #e0e3e8,0 -3px 6px 0 rgba(69,98,155,.12)}html[data-theme=dark]{--docsearch-text-color:#f5f6f7;--docsearch-container-background:rgba(9,10,17,.8);--docsearch-modal-background:#15172a;--docsearch-modal-shadow:inset 1px 1px 0 0 #2c2e40,0 3px 8px 0 #000309;--docsearch-searchbox-background:#090a11;--docsearch-searchbox-focus-background:#000;--docsearch-hit-color:#bec3c9;--docsearch-hit-shadow:none;--docsearch-hit-background:#090a11;--docsearch-key-gradient:linear-gradient(-26.5deg,#565872,#31355b);--docsearch-key-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 2px 2px 0 rgba(3,4,9,.3);--docsearch-footer-background:#1e2136;--docsearch-footer-shadow:inset 0 1px 0 0 rgba(73,76,106,.5),0 -4px 8px 0 rgba(0,0,0,.2);--docsearch-logo-color:#fff;--docsearch-muted-color:#7f8497}.DocSearch-Button{align-items:center;background:var(--docsearch-searchbox-background);border:0;border-radius:40px;color:var(--docsearch-muted-color);cursor:pointer;display:flex;font-weight:500;height:36px;justify-content:space-between;margin:0 0 0 16px;padding:0 8px;-webkit-user-select:none;user-select:none}.DocSearch-Button:active,.DocSearch-Button:focus,.DocSearch-Button:hover{background:var(--docsearch-searchbox-focus-background);box-shadow:var(--docsearch-searchbox-shadow);color:var(--docsearch-text-color);outline:none}.DocSearch-Button-Container{align-items:center;display:flex}.DocSearch-Search-Icon{stroke-width:1.6}.DocSearch-Button .DocSearch-Search-Icon{color:var(--docsearch-text-color)}.DocSearch-Button-Placeholder{font-size:1rem;padding:0 12px 0 6px}.DocSearch-Button-Keys{display:flex;min-width:calc(40px + .8em)}.DocSearch-Button-Key{align-items:center;background:var(--docsearch-key-gradient);border-radius:3px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;position:relative;padding:0 0 2px;border:0;top:-1px;width:20px}@media (max-width:768px){.DocSearch-Button-Keys,.DocSearch-Button-Placeholder{display:none}}.DocSearch--active{overflow:hidden!important}.DocSearch-Container,.DocSearch-Container *{box-sizing:border-box}.DocSearch-Container{background-color:var(--docsearch-container-background);height:100vh;left:0;position:fixed;top:0;width:100vw;z-index:200}.DocSearch-Container a{text-decoration:none}.DocSearch-Link{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;font:inherit;margin:0;padding:0}.DocSearch-Modal{background:var(--docsearch-modal-background);border-radius:6px;box-shadow:var(--docsearch-modal-shadow);flex-direction:column;margin:60px auto auto;max-width:var(--docsearch-modal-width);position:relative}.DocSearch-SearchBar{display:flex;padding:var(--docsearch-spacing) var(--docsearch-spacing) 0}.DocSearch-Form{align-items:center;background:var(--docsearch-searchbox-focus-background);border-radius:4px;box-shadow:var(--docsearch-searchbox-shadow);display:flex;height:var(--docsearch-searchbox-height);margin:0;padding:0 var(--docsearch-spacing);position:relative;width:100%}.DocSearch-Input{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:0;color:var(--docsearch-text-color);flex:1;font:inherit;font-size:1.2em;height:100%;outline:none;padding:0 0 0 8px;width:80%}.DocSearch-Input::placeholder{color:var(--docsearch-muted-color);opacity:1}.DocSearch-Input::-webkit-search-cancel-button,.DocSearch-Input::-webkit-search-decoration,.DocSearch-Input::-webkit-search-results-button,.DocSearch-Input::-webkit-search-results-decoration{display:none}.DocSearch-LoadingIndicator,.DocSearch-MagnifierLabel,.DocSearch-Reset{margin:0;padding:0}.DocSearch-MagnifierLabel,.DocSearch-Reset{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}.DocSearch-Container--Stalled .DocSearch-MagnifierLabel,.DocSearch-LoadingIndicator{display:none}.DocSearch-Container--Stalled .DocSearch-LoadingIndicator{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Reset{animation:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;right:0;stroke-width:var(--docsearch-icon-stroke-width)}}.DocSearch-Reset{animation:fade-in .1s ease-in forwards;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;padding:2px;right:0;stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Reset[hidden]{display:none}.DocSearch-Reset:hover{color:var(--docsearch-highlight-color)}.DocSearch-LoadingIndicator svg,.DocSearch-MagnifierLabel svg{height:24px;width:24px}.DocSearch-Cancel{display:none}.DocSearch-Dropdown{max-height:calc(var(--docsearch-modal-height) - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height));min-height:var(--docsearch-spacing);overflow-y:auto;overflow-y:overlay;padding:0 var(--docsearch-spacing);scrollbar-color:var(--docsearch-muted-color) var(--docsearch-modal-background);scrollbar-width:thin}.DocSearch-Dropdown::-webkit-scrollbar{width:12px}.DocSearch-Dropdown::-webkit-scrollbar-track{background:transparent}.DocSearch-Dropdown::-webkit-scrollbar-thumb{background-color:var(--docsearch-muted-color);border:3px solid var(--docsearch-modal-background);border-radius:20px}.DocSearch-Dropdown ul{list-style:none;margin:0;padding:0}.DocSearch-Label{font-size:.75em;line-height:1.6em}.DocSearch-Help,.DocSearch-Label{color:var(--docsearch-muted-color)}.DocSearch-Help{font-size:.9em;margin:0;-webkit-user-select:none;user-select:none}.DocSearch-Title{font-size:1.2em}.DocSearch-Logo a{display:flex}.DocSearch-Logo svg{color:var(--docsearch-logo-color);margin-left:8px}.DocSearch-Hits:last-of-type{margin-bottom:24px}.DocSearch-Hits mark{background:none;color:var(--docsearch-highlight-color)}.DocSearch-HitsFooter{color:var(--docsearch-muted-color);display:flex;font-size:.85em;justify-content:center;margin-bottom:var(--docsearch-spacing);padding:var(--docsearch-spacing)}.DocSearch-HitsFooter a{border-bottom:1px solid;color:inherit}.DocSearch-Hit{border-radius:4px;display:flex;padding-bottom:4px;position:relative}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--deleting{transition:none}}.DocSearch-Hit--deleting{opacity:0;transition:all .25s linear}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--favoriting{transition:none}}.DocSearch-Hit--favoriting{transform:scale(0);transform-origin:top center;transition:all .25s linear;transition-delay:.25s}.DocSearch-Hit a{background:var(--docsearch-hit-background);border-radius:4px;box-shadow:var(--docsearch-hit-shadow);display:block;padding-left:var(--docsearch-spacing);width:100%}.DocSearch-Hit-source{background:var(--docsearch-modal-background);color:var(--docsearch-highlight-color);font-size:.85em;font-weight:600;line-height:32px;margin:0 -4px;padding:8px 4px 0;position:sticky;top:0;z-index:10}.DocSearch-Hit-Tree{color:var(--docsearch-muted-color);height:var(--docsearch-hit-height);opacity:.5;stroke-width:var(--docsearch-icon-stroke-width);width:24px}.DocSearch-Hit[aria-selected=true] a{background-color:var(--docsearch-highlight-color)}.DocSearch-Hit[aria-selected=true] mark{text-decoration:underline}.DocSearch-Hit-Container{align-items:center;color:var(--docsearch-hit-color);display:flex;flex-direction:row;height:var(--docsearch-hit-height);padding:0 var(--docsearch-spacing) 0 0}.DocSearch-Hit-icon{height:20px;width:20px}.DocSearch-Hit-action,.DocSearch-Hit-icon{color:var(--docsearch-muted-color);stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Hit-action{align-items:center;display:flex;height:22px;width:22px}.DocSearch-Hit-action svg{display:block;height:18px;width:18px}.DocSearch-Hit-action+.DocSearch-Hit-action{margin-left:6px}.DocSearch-Hit-action-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:inherit;cursor:pointer;padding:2px}svg.DocSearch-Hit-Select-Icon{display:none}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Select-Icon{display:block}.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:rgba(0,0,0,.2);transition:background-color .1s ease-in}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{transition:none}}.DocSearch-Hit-action-button:focus path,.DocSearch-Hit-action-button:hover path{fill:#fff}.DocSearch-Hit-content-wrapper{display:flex;flex:1 1 auto;flex-direction:column;font-weight:500;justify-content:center;line-height:1.2em;margin:0 8px;overflow-x:hidden;position:relative;text-overflow:ellipsis;white-space:nowrap;width:80%}.DocSearch-Hit-title{font-size:.9em}.DocSearch-Hit-path{color:var(--docsearch-muted-color);font-size:.75em}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-action,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-icon,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-path,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-text,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-title,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Tree,.DocSearch-Hit[aria-selected=true] mark{color:var(--docsearch-hit-active-color)!important}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:rgba(0,0,0,.2);transition:none}}.DocSearch-ErrorScreen,.DocSearch-NoResults,.DocSearch-StartScreen{font-size:.9em;margin:0 auto;padding:36px 0;text-align:center;width:80%}.DocSearch-Screen-Icon{color:var(--docsearch-muted-color);padding-bottom:12px}.DocSearch-NoResults-Prefill-List{display:inline-block;padding-bottom:24px;text-align:left}.DocSearch-NoResults-Prefill-List ul{display:inline-block;padding:8px 0 0}.DocSearch-NoResults-Prefill-List li{list-style-position:inside;list-style-type:"» "}.DocSearch-Prefill{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:1em;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;font-size:1em;font-weight:700;padding:0}.DocSearch-Prefill:focus,.DocSearch-Prefill:hover{outline:none;text-decoration:underline}.DocSearch-Footer{align-items:center;background:var(--docsearch-footer-background);border-radius:0 0 8px 8px;box-shadow:var(--docsearch-footer-shadow);display:flex;flex-direction:row-reverse;flex-shrink:0;height:var(--docsearch-footer-height);justify-content:space-between;padding:0 var(--docsearch-spacing);position:relative;-webkit-user-select:none;user-select:none;width:100%;z-index:300}.DocSearch-Commands{color:var(--docsearch-muted-color);display:flex;list-style:none;margin:0;padding:0}.DocSearch-Commands li{align-items:center;display:flex}.DocSearch-Commands li:not(:last-of-type){margin-right:.8em}.DocSearch-Commands-Key{align-items:center;background:var(--docsearch-key-gradient);border-radius:2px;box-shadow:var(--docsearch-key-shadow);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 1px;color:var(--docsearch-muted-color);border:0;width:20px}@media (max-width:768px){:root{--docsearch-spacing:10px;--docsearch-footer-height:40px}.DocSearch-Dropdown{height:100%}.DocSearch-Container{height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);position:absolute}.DocSearch-Footer{border-radius:0;bottom:0;position:absolute}.DocSearch-Hit-content-wrapper{display:flex;position:relative;width:80%}.DocSearch-Modal{border-radius:0;box-shadow:none;height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);margin:0;max-width:100%;width:100%}.DocSearch-Dropdown{max-height:calc(var(--docsearch-vh, 1vh)*100 - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height))}.DocSearch-Cancel{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;flex:none;font:inherit;font-size:1em;font-weight:500;margin-left:var(--docsearch-spacing);outline:none;overflow:hidden;padding:0;-webkit-user-select:none;user-select:none;white-space:nowrap}.DocSearch-Commands,.DocSearch-Hit-Tree{display:none}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}[class*=DocSearch]{--docsearch-primary-color: var(--vp-c-brand-1);--docsearch-highlight-color: var(--docsearch-primary-color);--docsearch-text-color: var(--vp-c-text-1);--docsearch-muted-color: var(--vp-c-text-2);--docsearch-searchbox-shadow: none;--docsearch-searchbox-background: transparent;--docsearch-searchbox-focus-background: transparent;--docsearch-key-gradient: transparent;--docsearch-key-shadow: none;--docsearch-modal-background: var(--vp-c-bg-soft);--docsearch-footer-background: var(--vp-c-bg)}.dark [class*=DocSearch]{--docsearch-modal-shadow: none;--docsearch-footer-shadow: none;--docsearch-logo-color: var(--vp-c-text-2);--docsearch-hit-background: var(--vp-c-default-soft);--docsearch-hit-color: var(--vp-c-text-2);--docsearch-hit-shadow: none}.DocSearch-Button{display:flex;justify-content:center;align-items:center;margin:0;padding:0;width:48px;height:55px;background:transparent;transition:border-color .25s}.DocSearch-Button:hover{background:transparent}.DocSearch-Button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}.DocSearch-Button:focus:not(:focus-visible){outline:none!important}@media (min-width: 768px){.DocSearch-Button{justify-content:flex-start;border:1px solid transparent;border-radius:8px;padding:0 10px 0 12px;width:100%;height:40px;background-color:var(--vp-c-bg-alt)}.DocSearch-Button:hover{border-color:var(--vp-c-brand-1);background:var(--vp-c-bg-alt)}}.DocSearch-Button .DocSearch-Button-Container{display:flex;align-items:center}.DocSearch-Button .DocSearch-Search-Icon{position:relative;width:16px;height:16px;color:var(--vp-c-text-1);fill:currentColor;transition:color .5s}.DocSearch-Button:hover .DocSearch-Search-Icon{color:var(--vp-c-text-1)}@media (min-width: 768px){.DocSearch-Button .DocSearch-Search-Icon{top:1px;margin-right:8px;width:14px;height:14px;color:var(--vp-c-text-2)}}.DocSearch-Button .DocSearch-Button-Placeholder{display:none;margin-top:2px;padding:0 16px 0 0;font-size:13px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.DocSearch-Button:hover .DocSearch-Button-Placeholder{color:var(--vp-c-text-1)}@media (min-width: 768px){.DocSearch-Button .DocSearch-Button-Placeholder{display:inline-block}}.DocSearch-Button .DocSearch-Button-Keys{direction:ltr;display:none;min-width:auto}@media (min-width: 768px){.DocSearch-Button .DocSearch-Button-Keys{display:flex;align-items:center}}.DocSearch-Button .DocSearch-Button-Key{display:block;margin:2px 0 0;border:1px solid var(--vp-c-divider);border-right:none;border-radius:4px 0 0 4px;padding-left:6px;min-width:0;width:auto;height:22px;line-height:22px;font-family:var(--vp-font-family-base);font-size:12px;font-weight:500;transition:color .5s,border-color .5s}.DocSearch-Button .DocSearch-Button-Key+.DocSearch-Button-Key{border-right:1px solid var(--vp-c-divider);border-left:none;border-radius:0 4px 4px 0;padding-left:2px;padding-right:6px}.DocSearch-Button .DocSearch-Button-Key:first-child{font-size:1px;letter-spacing:-12px;color:transparent}.DocSearch-Button .DocSearch-Button-Key:first-child:after{content:var(--vp-meta-key);font-size:12px;letter-spacing:normal;color:var(--docsearch-muted-color)}.DocSearch-Button .DocSearch-Button-Key:first-child>*{display:none}.VPNavBarSearch{display:flex;align-items:center}@media (min-width: 768px){.VPNavBarSearch{flex-grow:1;padding-left:24px}}@media (min-width: 960px){.VPNavBarSearch{padding-left:32px}}.dark .DocSearch-Footer{border-top:1px solid var(--vp-c-divider)}.DocSearch-Form{border:1px solid var(--vp-c-brand-1);background-color:var(--vp-c-white)}.dark .DocSearch-Form{background-color:var(--vp-c-default-soft)}.DocSearch-Screen-Icon>svg{margin:auto}.VPNavBarSocialLinks[data-v-0394ad82]{display:none}@media (min-width: 1280px){.VPNavBarSocialLinks[data-v-0394ad82]{display:flex;align-items:center}}.title[data-v-86d1bed8]{display:flex;align-items:center;border-bottom:1px solid transparent;width:100%;height:var(--vp-nav-height);font-size:16px;font-weight:600;color:var(--vp-c-text-1);transition:opacity .25s}@media (min-width: 960px){.title[data-v-86d1bed8]{flex-shrink:0}.VPNavBarTitle.has-sidebar .title[data-v-86d1bed8]{border-bottom-color:var(--vp-c-divider)}}[data-v-86d1bed8] .logo{margin-right:8px;height:var(--vp-nav-logo-height)}.VPNavBarTranslations[data-v-74abcbb9]{display:none}@media (min-width: 1280px){.VPNavBarTranslations[data-v-74abcbb9]{display:flex;align-items:center}}.title[data-v-74abcbb9]{padding:0 24px 0 12px;line-height:32px;font-size:14px;font-weight:700;color:var(--vp-c-text-1)}.VPNavBar[data-v-a0fd61f4]{position:relative;border-bottom:1px solid transparent;padding:0 8px 0 24px;height:var(--vp-nav-height);pointer-events:none;white-space:nowrap}@media (min-width: 768px){.VPNavBar[data-v-a0fd61f4]{padding:0 32px}}@media (min-width: 960px){.VPNavBar.has-sidebar[data-v-a0fd61f4]{padding:0}.VPNavBar[data-v-a0fd61f4]:not(.has-sidebar):not(.top){border-bottom-color:var(--vp-c-gutter);background-color:var(--vp-nav-bg-color)}}.container[data-v-a0fd61f4]{display:flex;justify-content:space-between;margin:0 auto;max-width:calc(var(--vp-layout-max-width) - 64px);height:var(--vp-nav-height);pointer-events:none}.container>.title[data-v-a0fd61f4],.container>.content[data-v-a0fd61f4]{pointer-events:none}.container[data-v-a0fd61f4] *{pointer-events:auto}@media (min-width: 960px){.VPNavBar.has-sidebar .container[data-v-a0fd61f4]{max-width:100%}}.title[data-v-a0fd61f4]{flex-shrink:0;height:calc(var(--vp-nav-height) - 1px);transition:background-color .5s}@media (min-width: 960px){.VPNavBar.has-sidebar .title[data-v-a0fd61f4]{position:absolute;top:0;left:0;z-index:2;padding:0 32px;width:var(--vp-sidebar-width);height:var(--vp-nav-height);background-color:transparent}}@media (min-width: 1440px){.VPNavBar.has-sidebar .title[data-v-a0fd61f4]{padding-left:max(32px,calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));width:calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px)}}.content[data-v-a0fd61f4]{flex-grow:1}@media (min-width: 960px){.VPNavBar.has-sidebar .content[data-v-a0fd61f4]{position:relative;z-index:1;padding-right:32px;padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPNavBar.has-sidebar .content[data-v-a0fd61f4]{padding-right:calc((100vw - var(--vp-layout-max-width)) / 2 + 32px);padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.content-body[data-v-a0fd61f4]{display:flex;justify-content:flex-end;align-items:center;height:calc(var(--vp-nav-height) - 1px);transition:background-color .5s}@media (min-width: 960px){.VPNavBar:not(.top) .content-body[data-v-a0fd61f4]{position:relative;background-color:var(--vp-nav-bg-color)}}@media (max-width: 767px){.content-body[data-v-a0fd61f4]{column-gap:.5rem}}.menu+.translations[data-v-a0fd61f4]:before,.menu+.appearance[data-v-a0fd61f4]:before,.menu+.social-links[data-v-a0fd61f4]:before,.translations+.appearance[data-v-a0fd61f4]:before,.appearance+.social-links[data-v-a0fd61f4]:before{margin-right:8px;margin-left:8px;width:1px;height:24px;background-color:var(--vp-c-divider);content:""}.menu+.appearance[data-v-a0fd61f4]:before,.translations+.appearance[data-v-a0fd61f4]:before{margin-right:16px}.appearance+.social-links[data-v-a0fd61f4]:before{margin-left:16px}.social-links[data-v-a0fd61f4]{margin-right:-8px}@media (min-width: 960px){.VPNavBar.has-sidebar .curtain[data-v-a0fd61f4]{position:absolute;right:0;bottom:-31px;width:calc(100% - var(--vp-sidebar-width));height:32px}.VPNavBar.has-sidebar .curtain[data-v-a0fd61f4]:before{display:block;width:100%;height:32px;background:linear-gradient(var(--vp-c-bg),transparent 70%);content:""}}@media (min-width: 1440px){.VPNavBar.has-sidebar .curtain[data-v-a0fd61f4]{width:calc(100% - ((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width)))}}.VPNavScreenAppearance[data-v-add8f686]{display:flex;justify-content:space-between;align-items:center;border-radius:8px;padding:12px 14px 12px 16px;background-color:var(--vp-c-bg-soft)}.text[data-v-add8f686]{line-height:24px;font-size:12px;font-weight:500;color:var(--vp-c-text-2)}.VPNavScreenMenuLink[data-v-05f27b2a]{display:block;border-bottom:1px solid var(--vp-c-divider);padding:12px 0 11px;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:border-color .25s,color .25s}.VPNavScreenMenuLink[data-v-05f27b2a]:hover{color:var(--vp-c-brand-1)}.VPNavScreenMenuGroupLink[data-v-19976ae1]{display:block;margin-left:12px;line-height:32px;font-size:14px;font-weight:400;color:var(--vp-c-text-1);transition:color .25s}.VPNavScreenMenuGroupLink[data-v-19976ae1]:hover{color:var(--vp-c-brand-1)}.VPNavScreenMenuGroupSection[data-v-8133b170]{display:block}.title[data-v-8133b170]{line-height:32px;font-size:13px;font-weight:700;color:var(--vp-c-text-2);transition:color .25s}.VPNavScreenMenuGroup[data-v-1ecb84e7]{border-bottom:1px solid var(--vp-c-divider);height:48px;overflow:hidden;transition:border-color .5s}.VPNavScreenMenuGroup .items[data-v-1ecb84e7]{visibility:hidden}.VPNavScreenMenuGroup.open .items[data-v-1ecb84e7]{visibility:visible}.VPNavScreenMenuGroup.open[data-v-1ecb84e7]{padding-bottom:10px;height:auto}.VPNavScreenMenuGroup.open .button[data-v-1ecb84e7]{padding-bottom:6px;color:var(--vp-c-brand-1)}.VPNavScreenMenuGroup.open .button-icon[data-v-1ecb84e7]{transform:rotate(45deg)}.button[data-v-1ecb84e7]{display:flex;justify-content:space-between;align-items:center;padding:12px 4px 11px 0;width:100%;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.button[data-v-1ecb84e7]:hover{color:var(--vp-c-brand-1)}.button-icon[data-v-1ecb84e7]{width:14px;height:14px;fill:var(--vp-c-text-2);transition:fill .5s,transform .25s}.group[data-v-1ecb84e7]:first-child{padding-top:0}.group+.group[data-v-1ecb84e7],.group+.item[data-v-1ecb84e7]{padding-top:4px}.VPNavScreenTranslations[data-v-d72aa483]{height:24px;overflow:hidden}.VPNavScreenTranslations.open[data-v-d72aa483]{height:auto}.title[data-v-d72aa483]{display:flex;align-items:center;font-size:14px;font-weight:500;color:var(--vp-c-text-1)}.icon[data-v-d72aa483]{width:16px;height:16px;fill:currentColor}.icon.lang[data-v-d72aa483]{margin-right:8px}.icon.chevron[data-v-d72aa483]{margin-left:4px}.list[data-v-d72aa483]{padding:4px 0 0 24px}.link[data-v-d72aa483]{line-height:32px;font-size:13px;color:var(--vp-c-text-1)}.VPNavScreen[data-v-cc5739dd]{position:fixed;top:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 1px);right:0;bottom:0;left:0;padding:0 32px;width:100%;background-color:var(--vp-nav-screen-bg-color);overflow-y:auto;transition:background-color .5s;pointer-events:auto}.VPNavScreen.fade-enter-active[data-v-cc5739dd],.VPNavScreen.fade-leave-active[data-v-cc5739dd]{transition:opacity .25s}.VPNavScreen.fade-enter-active .container[data-v-cc5739dd],.VPNavScreen.fade-leave-active .container[data-v-cc5739dd]{transition:transform .25s ease}.VPNavScreen.fade-enter-from[data-v-cc5739dd],.VPNavScreen.fade-leave-to[data-v-cc5739dd]{opacity:0}.VPNavScreen.fade-enter-from .container[data-v-cc5739dd],.VPNavScreen.fade-leave-to .container[data-v-cc5739dd]{transform:translateY(-8px)}@media (min-width: 768px){.VPNavScreen[data-v-cc5739dd]{display:none}}.container[data-v-cc5739dd]{margin:0 auto;padding:24px 0 96px;max-width:288px}.menu+.translations[data-v-cc5739dd],.menu+.appearance[data-v-cc5739dd],.translations+.appearance[data-v-cc5739dd]{margin-top:24px}.menu+.social-links[data-v-cc5739dd]{margin-top:16px}.appearance+.social-links[data-v-cc5739dd]{margin-top:16px}.VPNav[data-v-7e5bc4a5]{position:relative;top:var(--vp-layout-top-height, 0px);left:0;z-index:var(--vp-z-index-nav);width:100%;pointer-events:none;transition:background-color .5s}@media (min-width: 960px){.VPNav[data-v-7e5bc4a5]{position:fixed}}.VPSidebarItem.level-0[data-v-e31bd47b]{padding-bottom:24px}.VPSidebarItem.collapsed.level-0[data-v-e31bd47b]{padding-bottom:10px}.item[data-v-e31bd47b]{position:relative;display:flex;width:100%}.VPSidebarItem.collapsible>.item[data-v-e31bd47b]{cursor:pointer}.indicator[data-v-e31bd47b]{position:absolute;top:6px;bottom:6px;left:-17px;width:2px;border-radius:2px;transition:background-color .25s}.VPSidebarItem.level-2.is-active>.item>.indicator[data-v-e31bd47b],.VPSidebarItem.level-3.is-active>.item>.indicator[data-v-e31bd47b],.VPSidebarItem.level-4.is-active>.item>.indicator[data-v-e31bd47b],.VPSidebarItem.level-5.is-active>.item>.indicator[data-v-e31bd47b]{background-color:var(--vp-c-brand-1)}.link[data-v-e31bd47b]{display:flex;align-items:center;flex-grow:1}.text[data-v-e31bd47b]{flex-grow:1;padding:4px 0;line-height:24px;font-size:14px;transition:color .25s}.VPSidebarItem.level-0 .text[data-v-e31bd47b]{font-weight:700;color:var(--vp-c-text-1)}.VPSidebarItem.level-1 .text[data-v-e31bd47b],.VPSidebarItem.level-2 .text[data-v-e31bd47b],.VPSidebarItem.level-3 .text[data-v-e31bd47b],.VPSidebarItem.level-4 .text[data-v-e31bd47b],.VPSidebarItem.level-5 .text[data-v-e31bd47b]{font-weight:500;color:var(--vp-c-text-2)}.VPSidebarItem.level-0.is-link>.item>.link:hover .text[data-v-e31bd47b],.VPSidebarItem.level-1.is-link>.item>.link:hover .text[data-v-e31bd47b],.VPSidebarItem.level-2.is-link>.item>.link:hover .text[data-v-e31bd47b],.VPSidebarItem.level-3.is-link>.item>.link:hover .text[data-v-e31bd47b],.VPSidebarItem.level-4.is-link>.item>.link:hover .text[data-v-e31bd47b],.VPSidebarItem.level-5.is-link>.item>.link:hover .text[data-v-e31bd47b]{color:var(--vp-c-brand-1)}.VPSidebarItem.level-0.has-active>.item>.text[data-v-e31bd47b],.VPSidebarItem.level-1.has-active>.item>.text[data-v-e31bd47b],.VPSidebarItem.level-2.has-active>.item>.text[data-v-e31bd47b],.VPSidebarItem.level-3.has-active>.item>.text[data-v-e31bd47b],.VPSidebarItem.level-4.has-active>.item>.text[data-v-e31bd47b],.VPSidebarItem.level-5.has-active>.item>.text[data-v-e31bd47b],.VPSidebarItem.level-0.has-active>.item>.link>.text[data-v-e31bd47b],.VPSidebarItem.level-1.has-active>.item>.link>.text[data-v-e31bd47b],.VPSidebarItem.level-2.has-active>.item>.link>.text[data-v-e31bd47b],.VPSidebarItem.level-3.has-active>.item>.link>.text[data-v-e31bd47b],.VPSidebarItem.level-4.has-active>.item>.link>.text[data-v-e31bd47b],.VPSidebarItem.level-5.has-active>.item>.link>.text[data-v-e31bd47b]{color:var(--vp-c-text-1)}.VPSidebarItem.level-0.is-active>.item .link>.text[data-v-e31bd47b],.VPSidebarItem.level-1.is-active>.item .link>.text[data-v-e31bd47b],.VPSidebarItem.level-2.is-active>.item .link>.text[data-v-e31bd47b],.VPSidebarItem.level-3.is-active>.item .link>.text[data-v-e31bd47b],.VPSidebarItem.level-4.is-active>.item .link>.text[data-v-e31bd47b],.VPSidebarItem.level-5.is-active>.item .link>.text[data-v-e31bd47b]{color:var(--vp-c-brand-1)}.caret[data-v-e31bd47b]{display:flex;justify-content:center;align-items:center;margin-right:-7px;width:32px;height:32px;color:var(--vp-c-text-3);cursor:pointer;transition:color .25s;flex-shrink:0}.item:hover .caret[data-v-e31bd47b]{color:var(--vp-c-text-2)}.item:hover .caret[data-v-e31bd47b]:hover{color:var(--vp-c-text-1)}.caret-icon[data-v-e31bd47b]{width:18px;height:18px;fill:currentColor;transform:rotate(90deg);transition:transform .25s}.VPSidebarItem.collapsed .caret-icon[data-v-e31bd47b]{transform:rotate(0)}.VPSidebarItem.level-1 .items[data-v-e31bd47b],.VPSidebarItem.level-2 .items[data-v-e31bd47b],.VPSidebarItem.level-3 .items[data-v-e31bd47b],.VPSidebarItem.level-4 .items[data-v-e31bd47b],.VPSidebarItem.level-5 .items[data-v-e31bd47b]{border-left:1px solid var(--vp-c-divider);padding-left:16px}.VPSidebarItem.collapsed .items[data-v-e31bd47b]{display:none}.VPSidebar[data-v-b00e2fdd]{position:fixed;top:var(--vp-layout-top-height, 0px);bottom:0;left:0;z-index:var(--vp-z-index-sidebar);padding:32px 32px 96px;width:calc(100vw - 64px);max-width:320px;background-color:var(--vp-sidebar-bg-color);opacity:0;box-shadow:var(--vp-c-shadow-3);overflow-x:hidden;overflow-y:auto;transform:translate(-100%);transition:opacity .5s,transform .25s ease;overscroll-behavior:contain}.VPSidebar.open[data-v-b00e2fdd]{opacity:1;visibility:visible;transform:translate(0);transition:opacity .25s,transform .5s cubic-bezier(.19,1,.22,1)}.dark .VPSidebar[data-v-b00e2fdd]{box-shadow:var(--vp-shadow-1)}@media (min-width: 960px){.VPSidebar[data-v-b00e2fdd]{z-index:1;padding-top:var(--vp-nav-height);padding-bottom:128px;width:var(--vp-sidebar-width);max-width:100%;background-color:var(--vp-sidebar-bg-color);opacity:1;visibility:visible;box-shadow:none;transform:translate(0)}}@media (min-width: 1440px){.VPSidebar[data-v-b00e2fdd]{padding-left:max(32px,calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));width:calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px)}}@media (min-width: 960px){.curtain[data-v-b00e2fdd]{position:sticky;top:-64px;left:0;z-index:1;margin-top:calc(var(--vp-nav-height) * -1);margin-right:-32px;margin-left:-32px;height:var(--vp-nav-height);background-color:var(--vp-sidebar-bg-color)}}.nav[data-v-b00e2fdd]{outline:0}.group+.group[data-v-b00e2fdd]{border-top:1px solid var(--vp-c-divider);padding-top:10px}@media (min-width: 960px){.group[data-v-b00e2fdd]{padding-top:10px;width:calc(var(--vp-sidebar-width) - 64px)}}.VPSkipLink[data-v-0f60ec36]{top:8px;left:8px;padding:8px 16px;z-index:999;border-radius:8px;font-size:12px;font-weight:700;text-decoration:none;color:var(--vp-c-brand-1);box-shadow:var(--vp-shadow-3);background-color:var(--vp-c-bg)}.VPSkipLink[data-v-0f60ec36]:focus{height:auto;width:auto;clip:auto;clip-path:none}@media (min-width: 1280px){.VPSkipLink[data-v-0f60ec36]{top:14px;left:16px}}.Layout[data-v-1919c326]{display:flex;flex-direction:column;min-height:100vh}.VPHomeSponsors[data-v-96bd69d5]{border-top:1px solid var(--vp-c-gutter);padding:88px 24px 96px;background-color:var(--vp-c-bg)}.container[data-v-96bd69d5]{margin:0 auto;max-width:1152px}.love[data-v-96bd69d5]{margin:0 auto;width:28px;height:28px;color:var(--vp-c-text-3)}.icon[data-v-96bd69d5]{width:28px;height:28px;fill:currentColor}.message[data-v-96bd69d5]{margin:0 auto;padding-top:10px;max-width:320px;text-align:center;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}.sponsors[data-v-96bd69d5]{padding-top:32px}.action[data-v-96bd69d5]{padding-top:40px;text-align:center}.VPTeamPage[data-v-10b00018]{padding-bottom:96px}@media (min-width: 768px){.VPTeamPage[data-v-10b00018]{padding-bottom:128px}}.VPTeamPageSection+.VPTeamPageSection[data-v-10b00018-s],.VPTeamMembers+.VPTeamPageSection[data-v-10b00018-s]{margin-top:64px}.VPTeamMembers+.VPTeamMembers[data-v-10b00018-s]{margin-top:24px}@media (min-width: 768px){.VPTeamPageTitle+.VPTeamPageSection[data-v-10b00018-s]{margin-top:16px}.VPTeamPageSection+.VPTeamPageSection[data-v-10b00018-s],.VPTeamMembers+.VPTeamPageSection[data-v-10b00018-s]{margin-top:96px}}.VPTeamMembers[data-v-10b00018-s]{padding:0 24px}@media (min-width: 768px){.VPTeamMembers[data-v-10b00018-s]{padding:0 48px}}@media (min-width: 960px){.VPTeamMembers[data-v-10b00018-s]{padding:0 64px}}.VPTeamPageTitle[data-v-bf2cbdac]{padding:48px 32px;text-align:center}@media (min-width: 768px){.VPTeamPageTitle[data-v-bf2cbdac]{padding:64px 48px 48px}}@media (min-width: 960px){.VPTeamPageTitle[data-v-bf2cbdac]{padding:80px 64px 48px}}.title[data-v-bf2cbdac]{letter-spacing:0;line-height:44px;font-size:36px;font-weight:500}@media (min-width: 768px){.title[data-v-bf2cbdac]{letter-spacing:-.5px;line-height:56px;font-size:48px}}.lead[data-v-bf2cbdac]{margin:0 auto;max-width:512px;padding-top:12px;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}@media (min-width: 768px){.lead[data-v-bf2cbdac]{max-width:592px;letter-spacing:.15px;line-height:28px;font-size:20px}}.VPTeamPageSection[data-v-b1a88750]{padding:0 32px}@media (min-width: 768px){.VPTeamPageSection[data-v-b1a88750]{padding:0 48px}}@media (min-width: 960px){.VPTeamPageSection[data-v-b1a88750]{padding:0 64px}}.title[data-v-b1a88750]{position:relative;margin:0 auto;max-width:1152px;text-align:center;color:var(--vp-c-text-2)}.title-line[data-v-b1a88750]{position:absolute;top:16px;left:0;width:100%;height:1px;background-color:var(--vp-c-divider)}.title-text[data-v-b1a88750]{position:relative;display:inline-block;padding:0 24px;letter-spacing:0;line-height:32px;font-size:20px;font-weight:500;background-color:var(--vp-c-bg)}.lead[data-v-b1a88750]{margin:0 auto;max-width:480px;padding-top:12px;text-align:center;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}.members[data-v-b1a88750]{padding-top:40px}.VPTeamMembersItem[data-v-28528e42]{display:flex;flex-direction:column;gap:2px;border-radius:12px;width:100%;height:100%;overflow:hidden}.VPTeamMembersItem.small .profile[data-v-28528e42]{padding:32px}.VPTeamMembersItem.small .data[data-v-28528e42]{padding-top:20px}.VPTeamMembersItem.small .avatar[data-v-28528e42]{width:64px;height:64px}.VPTeamMembersItem.small .name[data-v-28528e42]{line-height:24px;font-size:16px}.VPTeamMembersItem.small .affiliation[data-v-28528e42]{padding-top:4px;line-height:20px;font-size:14px}.VPTeamMembersItem.small .desc[data-v-28528e42]{padding-top:12px;line-height:20px;font-size:14px}.VPTeamMembersItem.small .links[data-v-28528e42]{margin:0 -16px -20px;padding:10px 0 0}.VPTeamMembersItem.medium .profile[data-v-28528e42]{padding:48px 32px}.VPTeamMembersItem.medium .data[data-v-28528e42]{padding-top:24px;text-align:center}.VPTeamMembersItem.medium .avatar[data-v-28528e42]{width:96px;height:96px}.VPTeamMembersItem.medium .name[data-v-28528e42]{letter-spacing:.15px;line-height:28px;font-size:20px}.VPTeamMembersItem.medium .affiliation[data-v-28528e42]{padding-top:4px;font-size:16px}.VPTeamMembersItem.medium .desc[data-v-28528e42]{padding-top:16px;max-width:288px;font-size:16px}.VPTeamMembersItem.medium .links[data-v-28528e42]{margin:0 -16px -12px;padding:16px 12px 0}.profile[data-v-28528e42]{flex-grow:1;background-color:var(--vp-c-bg-soft)}.data[data-v-28528e42]{text-align:center}.avatar[data-v-28528e42]{position:relative;flex-shrink:0;margin:0 auto;border-radius:50%;box-shadow:var(--vp-shadow-3)}.avatar-img[data-v-28528e42]{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:50%;object-fit:cover}.name[data-v-28528e42]{margin:0;font-weight:600}.affiliation[data-v-28528e42]{margin:0;font-weight:500;color:var(--vp-c-text-2)}.org.link[data-v-28528e42]{color:var(--vp-c-text-2);transition:color .25s}.org.link[data-v-28528e42]:hover{color:var(--vp-c-brand-1)}.desc[data-v-28528e42]{margin:0 auto}.desc[data-v-28528e42] a{font-weight:500;color:var(--vp-c-brand-1);text-decoration-style:dotted;transition:color .25s}.links[data-v-28528e42]{display:flex;justify-content:center;height:56px}.sp-link[data-v-28528e42]{display:flex;justify-content:center;align-items:center;text-align:center;padding:16px;font-size:14px;font-weight:500;color:var(--vp-c-sponsor);background-color:var(--vp-c-bg-soft);transition:color .25s,background-color .25s}.sp .sp-link.link[data-v-28528e42]:hover,.sp .sp-link.link[data-v-28528e42]:focus{outline:none;color:var(--vp-c-white);background-color:var(--vp-c-sponsor)}.sp-icon[data-v-28528e42]{margin-right:8px;width:16px;height:16px;fill:currentColor}.VPTeamMembers.small .container[data-v-6cb0dbc4]{grid-template-columns:repeat(auto-fit,minmax(224px,1fr))}.VPTeamMembers.small.count-1 .container[data-v-6cb0dbc4]{max-width:276px}.VPTeamMembers.small.count-2 .container[data-v-6cb0dbc4]{max-width:576px}.VPTeamMembers.small.count-3 .container[data-v-6cb0dbc4]{max-width:876px}.VPTeamMembers.medium .container[data-v-6cb0dbc4]{grid-template-columns:repeat(auto-fit,minmax(256px,1fr))}@media (min-width: 375px){.VPTeamMembers.medium .container[data-v-6cb0dbc4]{grid-template-columns:repeat(auto-fit,minmax(288px,1fr))}}.VPTeamMembers.medium.count-1 .container[data-v-6cb0dbc4]{max-width:368px}.VPTeamMembers.medium.count-2 .container[data-v-6cb0dbc4]{max-width:760px}.container[data-v-6cb0dbc4]{display:grid;gap:24px;margin:0 auto;max-width:1152px} diff --git a/assets/tinker_index.md.b79e9a67.js b/assets/tinker_index.md.b79e9a67.js new file mode 100644 index 000000000..958620dbd --- /dev/null +++ b/assets/tinker_index.md.b79e9a67.js @@ -0,0 +1 @@ +import{_ as t,o as a,c as n,k as e,a as r}from"./chunks/framework.dc1f4608.js";const x=JSON.parse('{"title":"Tinker","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/index.md","filePath":"tinker/index.md","lastUpdated":1708510102000}'),i={name:"tinker/index.md"},o=e("h1",{id:"tinker",tabindex:"-1"},[r("Tinker "),e("a",{class:"header-anchor",href:"#tinker","aria-label":'Permalink to "Tinker"'},"​")],-1),s=e("blockquote",null,[e("p",null,"生命不息,折腾不止。")],-1),d=[o,s];function c(l,_,k,p,h,m){return a(),n("div",null,d)}const u=t(i,[["render",c]]);export{x as __pageData,u as default}; diff --git a/assets/tinker_index.md.b79e9a67.lean.js b/assets/tinker_index.md.b79e9a67.lean.js new file mode 100644 index 000000000..958620dbd --- /dev/null +++ b/assets/tinker_index.md.b79e9a67.lean.js @@ -0,0 +1 @@ +import{_ as t,o as a,c as n,k as e,a as r}from"./chunks/framework.dc1f4608.js";const x=JSON.parse('{"title":"Tinker","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/index.md","filePath":"tinker/index.md","lastUpdated":1708510102000}'),i={name:"tinker/index.md"},o=e("h1",{id:"tinker",tabindex:"-1"},[r("Tinker "),e("a",{class:"header-anchor",href:"#tinker","aria-label":'Permalink to "Tinker"'},"​")],-1),s=e("blockquote",null,[e("p",null,"生命不息,折腾不止。")],-1),d=[o,s];function c(l,_,k,p,h,m){return a(),n("div",null,d)}const u=t(i,[["render",c]]);export{x as __pageData,u as default}; diff --git "a/assets/tinker_network_frp\345\206\205\347\275\221\347\251\277\351\200\217.md.7473690d.js" "b/assets/tinker_network_frp\345\206\205\347\275\221\347\251\277\351\200\217.md.7473690d.js" new file mode 100644 index 000000000..7d77046d0 --- /dev/null +++ "b/assets/tinker_network_frp\345\206\205\347\275\221\347\251\277\351\200\217.md.7473690d.js" @@ -0,0 +1,89 @@ +import{_ as s,o as n,c as a,Q as p}from"./chunks/framework.dc1f4608.js";const _=JSON.parse('{"title":"frp内网穿透","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/network/frp内网穿透.md","filePath":"tinker/network/frp内网穿透.md","lastUpdated":1708510102000}'),l={name:"tinker/network/frp内网穿透.md"},e=p(`

frp内网穿透

家庭服务器由于是移动宽带(大内网),没有办法申请公网ip,这样不在家的时候就无法进行服务器管理了。如果有公网ip,可以使用ddns,也可以用花生壳这类内网穿透工具。或者自己有一台有公网ip的云主机,可以通过frp应用来实现内网穿透。frp仓库地址:https://github.com/fatedier/frp

frp使用

具体使用可以查看frp使用文档,这里介绍下我用的场景:带sk校验的安全的ssh连接

在云主机上部署fprs,配置如下:

ini
[common]
+bind_addr = 0.0.0.0
+bind_port = 7000
+
+token = xxx
[common]
+bind_addr = 0.0.0.0
+bind_port = 7000
+
+token = xxx
shell
cat > /etc/systemd/system/frps.service <<EOF
+[Unit]
+Description=frps
+After=network.target
+[Service]
+Type=simple
+ExecStart=/usr/bin/frps -c /etc/frps/frps.ini
+Restart=on-failure
+[Install]
+WantedBy=multi-user.target
+EOF
cat > /etc/systemd/system/frps.service <<EOF
+[Unit]
+Description=frps
+After=network.target
+[Service]
+Type=simple
+ExecStart=/usr/bin/frps -c /etc/frps/frps.ini
+Restart=on-failure
+[Install]
+WantedBy=multi-user.target
+EOF
  • 在需要暴露到内网的机器A上部署 frpc,配置如下:
ini
[common]
+server_addr = x.x.x.x
+server_port = 7000
+token = xxx
+
+[ssh]
+type = tcp
+local_ip = 127.0.0.1
+local_port = 22
+remote_port = 6001
+
+[secret_ssh]
+type = stcp
+# 只有 sk 一致的用户才能访问到此服务
+sk = abcdefg
+local_ip = 127.0.0.1
+local_port = 22
[common]
+server_addr = x.x.x.x
+server_port = 7000
+token = xxx
+
+[ssh]
+type = tcp
+local_ip = 127.0.0.1
+local_port = 22
+remote_port = 6001
+
+[secret_ssh]
+type = stcp
+# 只有 sk 一致的用户才能访问到此服务
+sk = abcdefg
+local_ip = 127.0.0.1
+local_port = 22

在需要访问内网的机器上执行命令连接内网服务,例如用户为root

ssh -oPort=6001 root@x.x.x.x

  • 在需要访问内网的机器B上部署frpc,配置如下:
ini
[common]
+server_addr = x.x.x.x
+server_port = 7000
+token = xxx
+
+[secret_ssh_visitor]
+type = stcp
+# stcp 的访问者
+role = visitor
+# 要访问的 stcp 代理的名字
+server_name = secret_ssh
+sk = abcdefg
+# 绑定本地端口用于访问 SSH 服务
+bind_addr = 127.0.0.1
+bind_port = 6000
[common]
+server_addr = x.x.x.x
+server_port = 7000
+token = xxx
+
+[secret_ssh_visitor]
+type = stcp
+# stcp 的访问者
+role = visitor
+# 要访问的 stcp 代理的名字
+server_name = secret_ssh
+sk = abcdefg
+# 绑定本地端口用于访问 SSH 服务
+bind_addr = 127.0.0.1
+bind_port = 6000

在需要访问内网的机器上执行命令连接内网服务,例如用户为root

ssh -oPort 6000 root@127.0.0.1

如果内网机器开启了密钥登录,则需要指定内网服务器的私钥文件

ssh -oPort 6000 -i identityFile root@127.0.0.1

`,17),o=[e];function c(t,r,i,y,E,d){return n(),a("div",null,o)}const h=s(l,[["render",c]]);export{_ as __pageData,h as default}; diff --git "a/assets/tinker_network_frp\345\206\205\347\275\221\347\251\277\351\200\217.md.7473690d.lean.js" "b/assets/tinker_network_frp\345\206\205\347\275\221\347\251\277\351\200\217.md.7473690d.lean.js" new file mode 100644 index 000000000..7a2f412f6 --- /dev/null +++ "b/assets/tinker_network_frp\345\206\205\347\275\221\347\251\277\351\200\217.md.7473690d.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as p}from"./chunks/framework.dc1f4608.js";const _=JSON.parse('{"title":"frp内网穿透","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/network/frp内网穿透.md","filePath":"tinker/network/frp内网穿透.md","lastUpdated":1708510102000}'),l={name:"tinker/network/frp内网穿透.md"},e=p("",17),o=[e];function c(t,r,i,y,E,d){return n(),a("div",null,o)}const h=s(l,[["render",c]]);export{_ as __pageData,h as default}; diff --git "a/assets/tinker_network_home server\346\220\255\345\273\272.md.862c471d.js" "b/assets/tinker_network_home server\346\220\255\345\273\272.md.862c471d.js" new file mode 100644 index 000000000..45095b7c3 --- /dev/null +++ "b/assets/tinker_network_home server\346\220\255\345\273\272.md.862c471d.js" @@ -0,0 +1,1373 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"家庭服务器home server搭建","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/network/home server搭建.md","filePath":"tinker/network/home server搭建.md","lastUpdated":1708510102000}'),p={name:"tinker/network/home server搭建.md"},o=l(`

家庭服务器home server搭建

一直想搞一台nas玩玩儿,但是看了群晖、威联通这些成品nas低到令人发指的性价比,我最终还是决定diy一台小主机来实现自己的需求。

需求分析

需求

  1. 共享存储
  2. Docker服务
  3. 跑一些测试程序

分析

PC上还有块4T的希捷酷鹰,再添3块4T紫盘组raid5阵列。机箱的盘位就至少需要4个以上,挑了一圈就乔思伯N1(5盘位)和万由的810A(8盘位)能看的过去,虽然万由盘位多但是价格比n1高了大几百,目前也用不到这么多盘位,因此机箱确定了n1,主板也要买itx版型。

  1. 要跑的docker容器比较多,下载器服务、阿里云的webdav容器、直播录制程序容器等等。。。因此内存需要32G以上。

  2. 确定使用的系统是个比较复杂的过程,因为有过PVE虚拟机翻车的经历,这个服务器又主要承载了数据存储功能,所以要追求稳定,因此首先排除PVE和ESXi这些虚拟机系统,直接物理机装系统。然后我在虚拟机上装了最新版的Truenas scale体验了一下,这个系统是基于debian用python开发的,交互上倒没什么问题,但是因为是个纯nas系统,对主系统限制较多,自由度不高(不能直接装软件),因此也被pass,黑群晖这些就不说了,在我看来还不如truenas。一圈排除下来就只能直接装linux server了。去V2EX论坛问了老哥们的意见,推荐debian的很多,也有建议用最熟悉的系统的,最后我选择了后者,选了比较有把握的ubuntu server,正好ubuntu的22.04发行版刚出,就直接安排上了。

硬件

经过了好几天的挑选,最终敲定了这套配置

txt
cpu:i3-10100散片
+主板:七彩虹cvn b460i frozen
+内存:金士顿16g*2 2666
+固态:七彩虹 ssd sata3 128g
+cpu散热:超频3刀锋
+机械硬盘:西数海康oem紫盘4t*3 
+电源:tt 350w sfx电源
+机箱+线材:乔思伯n1
+扩展卡:乐扩m2转sata3接口扩展卡
cpu:i3-10100散片
+主板:七彩虹cvn b460i frozen
+内存:金士顿16g*2 2666
+固态:七彩虹 ssd sata3 128g
+cpu散热:超频3刀锋
+机械硬盘:西数海康oem紫盘4t*3 
+电源:tt 350w sfx电源
+机箱+线材:乔思伯n1
+扩展卡:乐扩m2转sata3接口扩展卡

其中散热、固态是在公司的福利商城购买,cpu、机械硬盘、机箱、扩展卡在淘宝购买,主板、电源在京东购买,内存在咸鱼淘的。不算硬盘花费是2480,加上硬盘3755。

组装完成后:

  • 灵魂走线,又不是不能用(doge)

D55DA0D4-322D-4A49-9634-9DB667BDD7A4_1_105_c

  • 侧面

6C0A8FED-B95C-4FE5-ACCB-32DD8DF553E8_1_105_c

B4E46C72-2CA5-4727-AD52-F3C25F94A74B_1_102_o

跟其他工业风机箱比起来,乔思伯n1这款颜值还是很不错的。

系统搭建

操作系统安装

ubuntu官网下载最新版的ubuntu-server-22.04,然后rufus刷写到U盘中,使用U盘引导启动。

安装过程不再赘述,这里记录几个重点步骤:

在配置Ubuntu安装镜像这一步最好选择国内的企业/大学镜像站,不然后面安装可能会在下载时卡住。网易镜像源http://mirrors.163.com/ubuntu/,阿里云镜像源https://mirrors.aliyun.com/ubuntu/,清华源https://mirrors.tuna.tsinghua.edu.cn/ubuntu/

  1. 磁盘分区选择自定义,然后根据自己的情况进行分区,我的固态只分了//boot 两个区,然后四块4T机械组了raid5。(ubuntu在建立阵列后会立刻进入重建过程,阵列中会有一个分区状态为spare rebuilding ,其他分区为active sync。这个重建过程很久,我4块4T重建总共用了十几个小时,重建完成后阵列下所有分区都会变为active sync 状态

image-20220501014554319

基础配置

  • 开启root登陆

    shell
    sudo vim /etc/ssh/sshd_config
    +
    +# 添加配置
    +PermitRootLogin yes
    +
    +# 给root修改密码
    +sudo passwd root
    +
    +systemctl restart sshd
    sudo vim /etc/ssh/sshd_config
    +
    +# 添加配置
    +PermitRootLogin yes
    +
    +# 给root修改密码
    +sudo passwd root
    +
    +systemctl restart sshd
  • 启用密钥登陆

见另一篇博客 阿里云服务器启用密钥登陆并禁用密码登陆

  • 时区同步

    sudo cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

安装服务

这个部分长期更新XD,一点点补上吧。

samba文件共享服务

shell
# 安装samba
+sudo apt install samba
+# 启动smb
+systemctl start smb
+# 开机自启
+systemctl enable smb
+
+# 创建共享文件夹 设置权限770
+mkdir /mnt/data
+sudo chmod 770 /mnt/data
+
+# 添加用户和密码
+sudo smbpasswd -a 用户名
+
+# 修改配置文件,在文件最后添加共享资源设置
+sudo vim /etc/samba/smb.conf
+
+[data]
+path = /mnt/data
+available = yes
+browseable = yes
+public = no
+writable = yes
+valid users = story
# 安装samba
+sudo apt install samba
+# 启动smb
+systemctl start smb
+# 开机自启
+systemctl enable smb
+
+# 创建共享文件夹 设置权限770
+mkdir /mnt/data
+sudo chmod 770 /mnt/data
+
+# 添加用户和密码
+sudo smbpasswd -a 用户名
+
+# 修改配置文件,在文件最后添加共享资源设置
+sudo vim /etc/samba/smb.conf
+
+[data]
+path = /mnt/data
+available = yes
+browseable = yes
+public = no
+writable = yes
+valid users = story

samba共享配置详解

[temp] #共享资源名称

comment = Temporary file space #简单的解释,内容无关紧要

path = /tmp #实际的共享目录

writable = yes #设置为可写入

browseable = yes #可以被所有用户浏览到资源名称,

guest ok = yes #可以让用户随意登录

public = yes #允许匿名查看

valid users = 用户名 #设置访问用户

valid users = @组名 #设置访问组

readonly = yes #只读

readonly = no #读写

hosts deny = 192.168.0.0 #表示禁止所有来自192.168.0.0/24 网段的IP 地址访问

hosts allow = 192.168.0.24 #表示允许192.168.0.24 这个IP 地址访问

[homes]为特殊共享目录,表示用户主目录。

[printers]表示共享打印机。

原文链接:https://blog.csdn.net/l1593572468/article/details/121444812

Docker安装

shell
# Uninstall old versions
+sudo apt-get remove docker docker-engine docker.io containerd runc
+
+# Update the apt package index and install packages to allow apt to use a repository over HTTPS:
+sudo apt-get update
+sudo apt-get install \\
+    ca-certificates \\
+    curl \\
+    gnupg \\
+    lsb-release
+# Add Docker’s official GPG key:
+curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
+
+# Use the following command to set up the stable repository.
+echo \\
+  "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \\
+  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
+  
+# Install Docker Engine
+sudo apt-get update
+sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
+
+
+systemctl enable docker
# Uninstall old versions
+sudo apt-get remove docker docker-engine docker.io containerd runc
+
+# Update the apt package index and install packages to allow apt to use a repository over HTTPS:
+sudo apt-get update
+sudo apt-get install \\
+    ca-certificates \\
+    curl \\
+    gnupg \\
+    lsb-release
+# Add Docker’s official GPG key:
+curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
+
+# Use the following command to set up the stable repository.
+echo \\
+  "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \\
+  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
+  
+# Install Docker Engine
+sudo apt-get update
+sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
+
+
+systemctl enable docker

挂载阿里云盘

参考另一篇博客挂载阿里云盘+开机自动挂载

transmission

shell
docker run -d \\
+  --name=transmission \\
+  -e TRANSMISSION_WEB_HOME=/transmission-web-control/ \\
+  -e PUID=1000 \\
+  -e PGID=1000 \\
+  -e TZ=Asia/Shanghai \\
+  -e USER=<user> \\
+  -e PASS=<pass> \\
+  -p 19091:9091 \\
+  -p 51413:51413 \\
+  -p 51413:51413/udp \\
+  -v /mnt/data/docker/transmission/data:/config \\
+  -v /mnt/data/downloads/others:/downloads/others \\
+  -v /mnt/data/downloads/tvseries:/downloads/tvseries \\
+  -v /mnt/data/docker/transmission/watch/folder:/watch \\
+  -v /mnt/data/downloads/movies:/downloads/movies \\
+  --restart=always \\
+  linuxserver/transmission
docker run -d \\
+  --name=transmission \\
+  -e TRANSMISSION_WEB_HOME=/transmission-web-control/ \\
+  -e PUID=1000 \\
+  -e PGID=1000 \\
+  -e TZ=Asia/Shanghai \\
+  -e USER=<user> \\
+  -e PASS=<pass> \\
+  -p 19091:9091 \\
+  -p 51413:51413 \\
+  -p 51413:51413/udp \\
+  -v /mnt/data/docker/transmission/data:/config \\
+  -v /mnt/data/downloads/others:/downloads/others \\
+  -v /mnt/data/downloads/tvseries:/downloads/tvseries \\
+  -v /mnt/data/docker/transmission/watch/folder:/watch \\
+  -v /mnt/data/downloads/movies:/downloads/movies \\
+  --restart=always \\
+  linuxserver/transmission

qbittorrent

shell
version: "3.2"
+
+services:
+  qbittorrent:
+    image: nevinee/qbittorrent:4.3.9
+    container_name: qbittorrent
+    environment:
+      - PUID=0
+      - PGID=0
+      - TZ=Asia/Shanghai
+      - WEBUI_PORT=18080
+      - BT_PORT=55555
+    volumes:
+      - /mnt/data/docker/qbittorrent/config:/data
+      - /repo:/downloads
+    network_mode: host
+    restart: unless-stopped
version: "3.2"
+
+services:
+  qbittorrent:
+    image: nevinee/qbittorrent:4.3.9
+    container_name: qbittorrent
+    environment:
+      - PUID=0
+      - PGID=0
+      - TZ=Asia/Shanghai
+      - WEBUI_PORT=18080
+      - BT_PORT=55555
+    volumes:
+      - /mnt/data/docker/qbittorrent/config:/data
+      - /repo:/downloads
+    network_mode: host
+    restart: unless-stopped

aria2

shell
docker run -d \\
+    --name aria2 \\
+    --restart always \\
+    --log-opt max-size=1m \\
+    -e TZ=Asia/Shanghai \\
+    -e PUID=$UID \\
+    -e PGID=$GID \\
+    -e UMASK_SET=022 \\
+    -e RPC_SECRET=<secret> \\
+    -e RPC_PORT=16800 \\
+    -p 16800:16800 \\
+    -e LISTEN_PORT=16888 \\
+    -p 16888:16888 \\
+    -p 16888:16888/udp \\
+    -v /mnt/data/docker/aria2/config:/config \\
+    -v /mnt/data/downloads/tvseries:/downloads/tvseries \\
+    -v /mnt/data/downloads/movies:/downloads/movies \\
+    -v /mnt/data/downloads/others:/downloads/others \\
+    p3terx/aria2-pro
docker run -d \\
+    --name aria2 \\
+    --restart always \\
+    --log-opt max-size=1m \\
+    -e TZ=Asia/Shanghai \\
+    -e PUID=$UID \\
+    -e PGID=$GID \\
+    -e UMASK_SET=022 \\
+    -e RPC_SECRET=<secret> \\
+    -e RPC_PORT=16800 \\
+    -p 16800:16800 \\
+    -e LISTEN_PORT=16888 \\
+    -p 16888:16888 \\
+    -p 16888:16888/udp \\
+    -v /mnt/data/docker/aria2/config:/config \\
+    -v /mnt/data/downloads/tvseries:/downloads/tvseries \\
+    -v /mnt/data/downloads/movies:/downloads/movies \\
+    -v /mnt/data/downloads/others:/downloads/others \\
+    p3terx/aria2-pro

jellyfin

shell
version: "3.2"
+
+services:
+  jenkins:
+    image: jenkins/jenkins:2.332.3-jdk11
+    container_name: jenkins
+    environment:
+      - TZ=Asia/Shanghai
+    user: root
+    volumes:
+      - /story/dist:/story/dist
+      - /mnt/data/docker/jenkins/jenkins_data:/var/jenkins_home
+      - /etc/localtime:/etc/localtime:ro
+    ports:
+      - "8099:8080"
+    restart: unless-stopped
version: "3.2"
+
+services:
+  jenkins:
+    image: jenkins/jenkins:2.332.3-jdk11
+    container_name: jenkins
+    environment:
+      - TZ=Asia/Shanghai
+    user: root
+    volumes:
+      - /story/dist:/story/dist
+      - /mnt/data/docker/jenkins/jenkins_data:/var/jenkins_home
+      - /etc/localtime:/etc/localtime:ro
+    ports:
+      - "8099:8080"
+    restart: unless-stopped

jellyfin硬解

shell
# 安装驱动
+apt install intel-media-va-driver
+# 解码支持确认
+/usr/lib/jellyfin-ffmpeg/vainfo
# 安装驱动
+apt install intel-media-va-driver
+# 解码支持确认
+/usr/lib/jellyfin-ffmpeg/vainfo

image-20230828233842999

kafka

shell
wget https://downloads.apache.org/kafka/3.6.1/kafka_2.13-3.6.1.tgz
+ tar -xzvf kafka_2.13-3.6.1.tgz -C /usr/local
wget https://downloads.apache.org/kafka/3.6.1/kafka_2.13-3.6.1.tgz
+ tar -xzvf kafka_2.13-3.6.1.tgz -C /usr/local

zookeeper.service

shell
[Unit]
+Description=zookeeper
+
+[Service]
+Type=forking
+Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+ExecStart=/usr/local/kafka/bin/zookeeper-server-start.sh -daemon /usr/local/kafka/config/zookeeper.properties
+ExecStop=/usr/local/kafka/bin/zookeeper-server-start.sh stop
+SyslogIdentifier=zookeeper
+
+[Install]
+WantedBy=multi-user.target
[Unit]
+Description=zookeeper
+
+[Service]
+Type=forking
+Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+ExecStart=/usr/local/kafka/bin/zookeeper-server-start.sh -daemon /usr/local/kafka/config/zookeeper.properties
+ExecStop=/usr/local/kafka/bin/zookeeper-server-start.sh stop
+SyslogIdentifier=zookeeper
+
+[Install]
+WantedBy=multi-user.target

kafka.service

shell
[Unit]
+Description=kafka
+After=zookeeper.service
+
+[Service]
+Type=forking
+Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+ExecStart=/usr/local/kafka/bin/kafka-server-start.sh -daemon /usr/local/kafka/config/server.properties
+ExecStop=/usr/local/kafka/bin/kafka-server-stop.sh
+
+[Install]
+WantedBy=multi-user.target
[Unit]
+Description=kafka
+After=zookeeper.service
+
+[Service]
+Type=forking
+Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+ExecStart=/usr/local/kafka/bin/kafka-server-start.sh -daemon /usr/local/kafka/config/server.properties
+ExecStop=/usr/local/kafka/bin/kafka-server-stop.sh
+
+[Install]
+WantedBy=multi-user.target

onedev

shell
docker run -d \\
+  --name onedev \\
+  -v /var/run/docker.sock:/var/run/docker.sock \\
+  -v /mnt/data/docker/onedev:/opt/onedev \\
+  -p 6610:6610 \\ # http
+  -p 6611:6611 \\ # ssh
+  --restart=always \\
+  1dev/server:latest
docker run -d \\
+  --name onedev \\
+  -v /var/run/docker.sock:/var/run/docker.sock \\
+  -v /mnt/data/docker/onedev:/opt/onedev \\
+  -p 6610:6610 \\ # http
+  -p 6611:6611 \\ # ssh
+  --restart=always \\
+  1dev/server:latest

rabbitmq

shell
sudo apt install rabbitmq-server
+
+cd /usr/lib/rabbitmq/bin
+# 开启rabbit网页控制台 默认端口号15672,重启rabbitmq服务
+ ./rabbitmq-plugins enable rabbitmq_management
+ 
+# rabbitmq默认用户guest不允许远程登陆,且systemd默认的启动用户为rabbitmq,可以改为root
+cd /lib/systemd/system
+vim rabbitmq-server.service
+ 
+# 新建rabbitmq用户
+cd /usr/lib/rabbitmq/bin
+./rabbitmqctl add_user username password
+# 授权
+./rabbitmqctl set_user_tags username administrator
+./rabbitmqctl set_permissions -p "/" username ".*" ".*" ".*"
+ 
+# 查看、删除、修改密码
+./rabbitmqctl list_users
+./rabbitmqctl delete_user username
+./rabbitmqctl change_password username newpassword
sudo apt install rabbitmq-server
+
+cd /usr/lib/rabbitmq/bin
+# 开启rabbit网页控制台 默认端口号15672,重启rabbitmq服务
+ ./rabbitmq-plugins enable rabbitmq_management
+ 
+# rabbitmq默认用户guest不允许远程登陆,且systemd默认的启动用户为rabbitmq,可以改为root
+cd /lib/systemd/system
+vim rabbitmq-server.service
+ 
+# 新建rabbitmq用户
+cd /usr/lib/rabbitmq/bin
+./rabbitmqctl add_user username password
+# 授权
+./rabbitmqctl set_user_tags username administrator
+./rabbitmqctl set_permissions -p "/" username ".*" ".*" ".*"
+ 
+# 查看、删除、修改密码
+./rabbitmqctl list_users
+./rabbitmqctl delete_user username
+./rabbitmqctl change_password username newpassword

gitea

yaml
version: "3"
+
+networks:
+  gitea:
+    external: false
+
+volumes:
+  gitea:
+    driver: local
+
+services:
+  server:
+    image: gitea/gitea:1.16.7
+    container_name: gitea
+    environment:
+      - DOMAIN=192.168.2.66
+      - HTTP_PORT=6610
+      - SSH_PORT=6611
+      - SSH_LISTEN_PORT=6611
+    restart: always
+    networks:
+      - gitea
+    volumes:
+      - gitea:/data
+      - /etc/timezone:/etc/timezone:ro
+      - /etc/localtime:/etc/localtime:ro
+    ports:
+      - "6610:6610"
+      - "6611:6611"
version: "3"
+
+networks:
+  gitea:
+    external: false
+
+volumes:
+  gitea:
+    driver: local
+
+services:
+  server:
+    image: gitea/gitea:1.16.7
+    container_name: gitea
+    environment:
+      - DOMAIN=192.168.2.66
+      - HTTP_PORT=6610
+      - SSH_PORT=6611
+      - SSH_LISTEN_PORT=6611
+    restart: always
+    networks:
+      - gitea
+    volumes:
+      - gitea:/data
+      - /etc/timezone:/etc/timezone:ro
+      - /etc/localtime:/etc/localtime:ro
+    ports:
+      - "6610:6610"
+      - "6611:6611"

gitea webhook allowed host list

shell
/var/lib/docker/volumes/gitea_gitea/_data/gitea/conf/app.ini
+
+# add the following lines to the end of the file
+[webhook]
+ALLOWED_HOST_LIST = 192.168.2.66
/var/lib/docker/volumes/gitea_gitea/_data/gitea/conf/app.ini
+
+# add the following lines to the end of the file
+[webhook]
+ALLOWED_HOST_LIST = 192.168.2.66

gitea and jenkins webhook

In Jenkins: on the job settings page set "Source Code Management" option to "Git", provide URL to your repo (http://gitea-url.your.org/username/repo.git), and in "Poll triggers" section check "Poll SCM" option with no schedule defined. This setup basically tells Jenkins to poll your Gitea repo only when requested via the webhook.

In Gitea: under repo -> Settings -> Webhooks, add new webhook, set the URL to http://jenkins_url.your.org/gitea-webhook/post, and clear the secret (leave it blank).

At this point clicking on "Test Delivery" button should produce a successful delivery attempt (green checkmark).

kafdrop

shell
docker run -d --name kafkaui -p 9000:9000 \\
+    -e KAFKA_BROKERCONNECT="192.168.2.66:9092"\\
+    -e JVM_OPTS="-Xms32M -Xmx64M" \\
+    -e SERVER_SERVLET_CONTEXTPATH="/" \\
+    obsidiandynamics/kafdrop
docker run -d --name kafkaui -p 9000:9000 \\
+    -e KAFKA_BROKERCONNECT="192.168.2.66:9092"\\
+    -e JVM_OPTS="-Xms32M -Xmx64M" \\
+    -e SERVER_SERVLET_CONTEXTPATH="/" \\
+    obsidiandynamics/kafdrop

cadvisor Docker监控

yaml
version: '3'
+
+services:
+  cadvisor:
+    image: gcr.io/cadvisor/cadvisor:v0.47.2
+    container_name: cadvisor
+    volumes:
+      - /:/rootfs:ro
+      - /var/run:/var/run:ro
+      - /sys:/sys:ro
+      - /var/lib/docker/:/var/lib/docker:ro
+      - /dev/disk/:/dev/disk:ro
+    ports:
+      - "28080:8080"
+    privileged: true
+    restart: unless-stopped
+    devices:
+      - /dev/kmsg
version: '3'
+
+services:
+  cadvisor:
+    image: gcr.io/cadvisor/cadvisor:v0.47.2
+    container_name: cadvisor
+    volumes:
+      - /:/rootfs:ro
+      - /var/run:/var/run:ro
+      - /sys:/sys:ro
+      - /var/lib/docker/:/var/lib/docker:ro
+      - /dev/disk/:/dev/disk:ro
+    ports:
+      - "28080:8080"
+    privileged: true
+    restart: unless-stopped
+    devices:
+      - /dev/kmsg

grafana+prometheus+node_exporter监控linux系统

node_exporter

  • wget https://github.com/prometheus/node_exporter/releases/download/v1.6.1/node_exporter-1.6.1.linux-amd64.tar.gz
  • tar -zxvf node_exporter-1.6.1.linux-amd64.tar.gz && mv node_exporter-1.6.1.linux-amd64/node_exporter /usr/local/bin
systemd
shell
# 编写systemd服务
+cat > /etc/systemd/system/node_exporter.service <<EOF
+[Unit]
+Description=node_exporeter
+After=network.target
+[Service]
+Type=simple
+ExecStart=/usr/local/bin/node_exporter
+Restart=on-failure
+[Install]
+WantedBy=multi-user.target
+EOF
+
+# 更新内核并启动,自启动
+systemctl daemon-reload && systemctl start node_exporter && systemctl enable node_exporter && systemctl status node_exporter
# 编写systemd服务
+cat > /etc/systemd/system/node_exporter.service <<EOF
+[Unit]
+Description=node_exporeter
+After=network.target
+[Service]
+Type=simple
+ExecStart=/usr/local/bin/node_exporter
+Restart=on-failure
+[Install]
+WantedBy=multi-user.target
+EOF
+
+# 更新内核并启动,自启动
+systemctl daemon-reload && systemctl start node_exporter && systemctl enable node_exporter && systemctl status node_exporter
init.d
shell
# openwrt要使用init.d
+# /etc/init.d/node_exporter
+#!/bin/sh /etc/rc.common
+
+START=99
+STOP=10
+
+start() {
+    echo "Starting Node Exporter..."
+    /usr/bin/node_exporter --web.listen-address=":9100" > /dev/null 2>&1 &
+}
+
+stop() {
+    echo "Stopping Node Exporter..."
+    killall node_exporter
+}
+
+restart() {
+    stop
+    sleep 1
+    start
+}
# openwrt要使用init.d
+# /etc/init.d/node_exporter
+#!/bin/sh /etc/rc.common
+
+START=99
+STOP=10
+
+start() {
+    echo "Starting Node Exporter..."
+    /usr/bin/node_exporter --web.listen-address=":9100" > /dev/null 2>&1 &
+}
+
+stop() {
+    echo "Stopping Node Exporter..."
+    killall node_exporter
+}
+
+restart() {
+    stop
+    sleep 1
+    start
+}

/etc/init.d/node_exporter enable && /etc/init.d/node_exporter start

redis_exporter

  • wget https://github.com/oliver006/redis_exporter/releases/download/v1.46.0/redis_exporter-v1.46.0.linux-amd64.tar.gz
  • tar -xvf redis_exporter-v1.46.0.linux-amd64.tar.gz && mv redis_exporter-v1.46.0.linux-amd64/redis_exporter /usr/local/bin
shell
# 编写systemd服务
+cat > /etc/systemd/system/redis_exporter.service <<EOF
+[Unit]
+Description=redis_exporter
+After=network.target
+[Service]
+Type=simple
+ExecStart=/usr/local/bin/redis_exporter -redis.addr ip:port
+Restart=on-failure
+[Install]
+WantedBy=multi-user.target
+EOF
+
+# 更新内核并启动,自启动
+systemctl daemon-reload && systemctl start redis_exporter && systemctl enable redis_exporter && systemctl status redis_exporter
# 编写systemd服务
+cat > /etc/systemd/system/redis_exporter.service <<EOF
+[Unit]
+Description=redis_exporter
+After=network.target
+[Service]
+Type=simple
+ExecStart=/usr/local/bin/redis_exporter -redis.addr ip:port
+Restart=on-failure
+[Install]
+WantedBy=multi-user.target
+EOF
+
+# 更新内核并启动,自启动
+systemctl daemon-reload && systemctl start redis_exporter && systemctl enable redis_exporter && systemctl status redis_exporter

grafana+prometheus

yml
# docker-compose.yml
+
+version: "3"
+
+
+services:
+  grafana:
+    image: grafana/grafana
+    container_name: grafana
+    restart: unless-stopped
+    ports:
+      - 3000:3000
+    user: root
+    volumes:
+      - /mnt/data/docker/monitor/grafana/conf/grafana.ini:/etc/grafana/grafana.ini
+      - /mnt/data/docker/monitor/grafana/data:/var/lib/grafana
+      - /mnt/data/docker/monitor/grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards
+      - /mnt/data/docker/monitor/grafana/provisioning/datasources:/etc/grafana/provisioning/datasources
+    environment:
+      - TZ=Asia/shanghai
+  prometheus:
+    image: prom/prometheus
+    container_name: prometheus
+    restart: unless-stopped
+    ports:
+      - 9090:9090
+    volumes:
+      - /mnt/data/docker/monitor/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
+      - prometheus_data:/prometheus
+    environment:
+      - TZ=Asia/shanghai
+
+volumes:
+  prometheus_data:
# docker-compose.yml
+
+version: "3"
+
+
+services:
+  grafana:
+    image: grafana/grafana
+    container_name: grafana
+    restart: unless-stopped
+    ports:
+      - 3000:3000
+    user: root
+    volumes:
+      - /mnt/data/docker/monitor/grafana/conf/grafana.ini:/etc/grafana/grafana.ini
+      - /mnt/data/docker/monitor/grafana/data:/var/lib/grafana
+      - /mnt/data/docker/monitor/grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards
+      - /mnt/data/docker/monitor/grafana/provisioning/datasources:/etc/grafana/provisioning/datasources
+    environment:
+      - TZ=Asia/shanghai
+  prometheus:
+    image: prom/prometheus
+    container_name: prometheus
+    restart: unless-stopped
+    ports:
+      - 9090:9090
+    volumes:
+      - /mnt/data/docker/monitor/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
+      - prometheus_data:/prometheus
+    environment:
+      - TZ=Asia/shanghai
+
+volumes:
+  prometheus_data:

prometheus.yml

yml
global:
+  scrape_interval: 15s
+  evaluation_interval: 15s
+
+
+scrape_configs:
+  - job_name: "linux"
+    scrape_interval: 5s
+    static_configs:
+      - targets: [ "192.168.2.66:9100" ]
+        labels:
+          instance: home-server-ubuntu
+  - job_name: "redis"
+    scrape_interval: 5s
+    static_configs:
+      - targets: [ "192.168.2.66:9121" ]
+        labels:
+          instance: home-server-ubuntu
+  - job_name: "cadvisor"
+    static_configs:
+      - targets: [ "192.168.2.66:28080" ]
+        labels:
+          instance: home-server-ubuntu
global:
+  scrape_interval: 15s
+  evaluation_interval: 15s
+
+
+scrape_configs:
+  - job_name: "linux"
+    scrape_interval: 5s
+    static_configs:
+      - targets: [ "192.168.2.66:9100" ]
+        labels:
+          instance: home-server-ubuntu
+  - job_name: "redis"
+    scrape_interval: 5s
+    static_configs:
+      - targets: [ "192.168.2.66:9121" ]
+        labels:
+          instance: home-server-ubuntu
+  - job_name: "cadvisor"
+    static_configs:
+      - targets: [ "192.168.2.66:28080" ]
+        labels:
+          instance: home-server-ubuntu
  • grafana.ini 从空白容器里复制出一份
ini
docker cp grafana:/etc/grafana.ini ~/
docker cp grafana:/etc/grafana.ini ~/
  • grafana监控大盘模板

    • 12633(Linux主机详情)

    • 1860(Node Exporter Full)

    • 193(Docker monitoring)

    • 14282(Cadvisor exporter)

    • 11835( Redis Dashboard)

Bitwarden

https://bitwarden.com/help/install-on-premise-linux/

Configure your domain

配置域名解析,Bitwarden默认使用80443端口,可以执行安装后在bwdata/config.yaml修改端口

yaml
http_prt: 80
+https_port: 443
http_prt: 80
+https_port: 443

修改完bwdata/config.yaml后需要执行./bitwarden.sh rebuild

Install Docker and Docker Compose

curl -fsSL https://get.docker.com | sudo sh

Create a Bitwarden user & directory from which to complete installation.

shell
sudo adduser bitwarden
+sudo passwd bitwarden
+sudo groupadd docker
+sudo usermod -aG docker bitwarden
+sudo mkdir /opt/bitwarden
+sudo chmod -R 700 /opt/bitwarden
+sudo chown -R bitwarden:bitwarden /opt/bitwarden
sudo adduser bitwarden
+sudo passwd bitwarden
+sudo groupadd docker
+sudo usermod -aG docker bitwarden
+sudo mkdir /opt/bitwarden
+sudo chmod -R 700 /opt/bitwarden
+sudo chown -R bitwarden:bitwarden /opt/bitwarden

Retrieve an installation id and key from [**https://bitwarden.com/host

**](https://bitwarden.com/host/) for use in installation.

For more information, see What are my installation id and installation key used for?

Install Bitwarden on your machine.

shell
curl -Lso bitwarden.sh "https://func.bitwarden.com/api/dl/?app=self-host&platform=linux" && chmod 700 bitwarden.sh
+
+./bitwarden.sh install
curl -Lso bitwarden.sh "https://func.bitwarden.com/api/dl/?app=self-host&platform=linux" && chmod 700 bitwarden.sh
+
+./bitwarden.sh install

Configure your environment by adjusting settings in ./bwdata/env/global.override.env.

properties
globalSettings__mail__replyToEmail=email@example.com
+globalSettings__mail__smtp__host=smtp.qq.com
+globalSettings__mail__smtp__port=465
+globalSettings__mail__smtp__ssl=true
+globalSettings__mail__smtp__username=email@example.com
+globalSettings__mail__smtp__password=password
+
+globalSettings__disableUserRegistration=true # 禁止注册
globalSettings__mail__replyToEmail=email@example.com
+globalSettings__mail__smtp__host=smtp.qq.com
+globalSettings__mail__smtp__port=465
+globalSettings__mail__smtp__ssl=true
+globalSettings__mail__smtp__username=email@example.com
+globalSettings__mail__smtp__password=password
+
+globalSettings__disableUserRegistration=true # 禁止注册

修改完后执行./bitwarden.sh restart

Start your instance

./bitwarden.sh start

backing up your server

backup bwdata folder

migration

https://bitwarden.com/help/migration/

如果低版本迁移到高版本,覆盖bwdata后,先执行./bitwarden.sh update

Client

https://bitwarden.com/download

Hoppscotch

https://github.com/hoppscotch/hoppscotch

docker-compose.yml

yaml
version: '3.8'
+
+services:
+  hoppscotch:
+    container_name: hoppscotch
+    image: hoppscotch/hoppscotch
+    ports:
+      - "53000:3000"
+      - "53100:3100"
+      - "53170:3170"
+    env_file: .env
+    restart: unless-stopped
+    links:
+      - postgresql
+    depends_on:
+      - postgresql
+    networks:
+      - hoppscotch
+  postgresql:
+    container_name: postgresql
+    image: postgres
+    environment:
+      POSTGRES_DB: db
+      POSTGRES_USER: user
+      POSTGRES_PASSWORD: passwd
+    ports:
+      - "5432:5432"
+    volumes:
+      - postgres_data:/var/lib/postgresql/data
+    restart: unless-stopped
+    networks:
+      - hoppscotch
+
+volumes:
+  postgres_data:
+
+networks:
+  hoppscotch:
version: '3.8'
+
+services:
+  hoppscotch:
+    container_name: hoppscotch
+    image: hoppscotch/hoppscotch
+    ports:
+      - "53000:3000"
+      - "53100:3100"
+      - "53170:3170"
+    env_file: .env
+    restart: unless-stopped
+    links:
+      - postgresql
+    depends_on:
+      - postgresql
+    networks:
+      - hoppscotch
+  postgresql:
+    container_name: postgresql
+    image: postgres
+    environment:
+      POSTGRES_DB: db
+      POSTGRES_USER: user
+      POSTGRES_PASSWORD: passwd
+    ports:
+      - "5432:5432"
+    volumes:
+      - postgres_data:/var/lib/postgresql/data
+    restart: unless-stopped
+    networks:
+      - hoppscotch
+
+volumes:
+  postgres_data:
+
+networks:
+  hoppscotch:

.env

properties
#-----------------------Backend Config------------------------------#
+# Prisma Config
+DATABASE_URL=postgresql://user:passwd@postgresql:5432/db
+
+# Auth Tokens Config
+JWT_SECRET="xxx"
+TOKEN_SALT_COMPLEXITY=10
+MAGIC_LINK_TOKEN_VALIDITY= 3
+REFRESH_TOKEN_VALIDITY="604800000" # Default validity is 7 days (604800000 ms) in ms
+ACCESS_TOKEN_VALIDITY="86400000" # Default validity is 1 day (86400000 ms) in ms
+SESSION_SECRET='xxx'
+
+# Hoppscotch App Domain Config
+REDIRECT_URL="https://hoppscotch.example.com"
+WHITELISTED_ORIGINS="https://hoppscotch.example.com/backend,https://hoppscotch.example.com,https://hoppadmin.example.com"
+VITE_ALLOWED_AUTH_PROVIDERS=GITHUB
+
+# Google Auth Config
+#GOOGLE_CLIENT_ID="************************************************"
+#GOOGLE_CLIENT_SECRET="************************************************"
+#GOOGLE_CALLBACK_URL="http://hoppscotch.example.com:3170/v1/auth/google/callback"
+#GOOGLE_SCOPE="email,profilstoryxc
+
+# Github Auth Config
+GITHUB_CLIENT_ID="xxx"
+GITHUB_CLIENT_SECRET="xxx"
+GITHUB_CALLBACK_URL="https://hoppscotch.example.com/backend/v1/auth/github/callback"
+GITHUB_SCOPE="user:email"
+
+# Microsoft Auth Config
+#MICROSOFT_CLIENT_ID="************************************************"
+#MICROSOFT_CLIENT_SECRET="************************************************"
+#MICROSOFT_CALLBACK_URL="http://hoppscotch.example.com:3170/v1/auth/microsoft/callback"
+#MICROSOFT_SCOPE="user.read"
+#MICROSOFT_TENANT="common"
+
+# Mailer config
+MAILER_SMTP_URL="smtps://user@domain.com:passwd@smtp.domain.com"
+MAILER_ADDRESS_FROM="user@domain.com"
+
+# Rate Limit Config
+RATE_LIMIT_TTL=60 # In seconds
+RATE_LIMIT_MAX=100 # Max requests per IP
+
+
+#-----------------------Frontend Config------------------------------#
+
+
+# Base URLs
+VITE_BASE_URL=https://hoppscotch.example.com
+VITE_SHORTCODE_BASE_URL=https://hoppscotch.example.com
+VITE_ADMIN_URL=https://hoppadmin.example.com
+
+# Backend URLs
+VITE_BACKEND_GQL_URL=https://hoppscotch.example.com/backend/graphql
+VITE_BACKEND_WS_URL=wss://hoppscotch.example.com/backend/ws/graphql
+VITE_BACKEND_API_URL=https://hoppscotch.example.com/backend/v1
+
+# Terms Of Service And Privacy Policy Links (Optional)
+VITE_APP_TOS_LINK=https://docs.hoppscotch.io/support/terms
+VITE_APP_PRIVACY_POLICY_LINK=https://docs.hoppscotch.io/support/privacy
#-----------------------Backend Config------------------------------#
+# Prisma Config
+DATABASE_URL=postgresql://user:passwd@postgresql:5432/db
+
+# Auth Tokens Config
+JWT_SECRET="xxx"
+TOKEN_SALT_COMPLEXITY=10
+MAGIC_LINK_TOKEN_VALIDITY= 3
+REFRESH_TOKEN_VALIDITY="604800000" # Default validity is 7 days (604800000 ms) in ms
+ACCESS_TOKEN_VALIDITY="86400000" # Default validity is 1 day (86400000 ms) in ms
+SESSION_SECRET='xxx'
+
+# Hoppscotch App Domain Config
+REDIRECT_URL="https://hoppscotch.example.com"
+WHITELISTED_ORIGINS="https://hoppscotch.example.com/backend,https://hoppscotch.example.com,https://hoppadmin.example.com"
+VITE_ALLOWED_AUTH_PROVIDERS=GITHUB
+
+# Google Auth Config
+#GOOGLE_CLIENT_ID="************************************************"
+#GOOGLE_CLIENT_SECRET="************************************************"
+#GOOGLE_CALLBACK_URL="http://hoppscotch.example.com:3170/v1/auth/google/callback"
+#GOOGLE_SCOPE="email,profilstoryxc
+
+# Github Auth Config
+GITHUB_CLIENT_ID="xxx"
+GITHUB_CLIENT_SECRET="xxx"
+GITHUB_CALLBACK_URL="https://hoppscotch.example.com/backend/v1/auth/github/callback"
+GITHUB_SCOPE="user:email"
+
+# Microsoft Auth Config
+#MICROSOFT_CLIENT_ID="************************************************"
+#MICROSOFT_CLIENT_SECRET="************************************************"
+#MICROSOFT_CALLBACK_URL="http://hoppscotch.example.com:3170/v1/auth/microsoft/callback"
+#MICROSOFT_SCOPE="user.read"
+#MICROSOFT_TENANT="common"
+
+# Mailer config
+MAILER_SMTP_URL="smtps://user@domain.com:passwd@smtp.domain.com"
+MAILER_ADDRESS_FROM="user@domain.com"
+
+# Rate Limit Config
+RATE_LIMIT_TTL=60 # In seconds
+RATE_LIMIT_MAX=100 # Max requests per IP
+
+
+#-----------------------Frontend Config------------------------------#
+
+
+# Base URLs
+VITE_BASE_URL=https://hoppscotch.example.com
+VITE_SHORTCODE_BASE_URL=https://hoppscotch.example.com
+VITE_ADMIN_URL=https://hoppadmin.example.com
+
+# Backend URLs
+VITE_BACKEND_GQL_URL=https://hoppscotch.example.com/backend/graphql
+VITE_BACKEND_WS_URL=wss://hoppscotch.example.com/backend/ws/graphql
+VITE_BACKEND_API_URL=https://hoppscotch.example.com/backend/v1
+
+# Terms Of Service And Privacy Policy Links (Optional)
+VITE_APP_TOS_LINK=https://docs.hoppscotch.io/support/terms
+VITE_APP_PRIVACY_POLICY_LINK=https://docs.hoppscotch.io/support/privacy

hoppscotch.example.com.conf

nginx
server {
+    listen              443 ssl;
+    listen              [::]:443 ssl;
+    server_name         hoppscotch.example.com;
+
+    # SSL
+    ssl_certificate     /etc/nginx/ssl/hoppscotch.example.com.crt;
+    ssl_certificate_key /etc/nginx/ssl/hoppscotch.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.1 TLSv1.2;
+
+    # security
+
+    # logging
+    access_log          /var/log/nginx/access.log combined buffer=512k flush=1m;
+    error_log           /var/log/nginx/error.log warn;
+
+    # additional config
+
+    location  /backend/ws/ {
+        proxy_pass http://127.0.0.1:53170/;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "Upgrade";
+        proxy_set_header X-Real-IP $remote_addr;
+    }
+
+    location  /backend/ {
+        proxy_pass http://127.0.0.1:53170/;
+    }
+
+    location / {
+        proxy_pass http://127.0.0.1:53000/;
+    }
+}
+
+# HTTP redirect
+server {
+    listen      80;
+    listen      [::]:80;
+    server_name hoppscotch.example.com;
+    return      301 https://hoppscotch.example.com$request_uri;
+}
server {
+    listen              443 ssl;
+    listen              [::]:443 ssl;
+    server_name         hoppscotch.example.com;
+
+    # SSL
+    ssl_certificate     /etc/nginx/ssl/hoppscotch.example.com.crt;
+    ssl_certificate_key /etc/nginx/ssl/hoppscotch.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.1 TLSv1.2;
+
+    # security
+
+    # logging
+    access_log          /var/log/nginx/access.log combined buffer=512k flush=1m;
+    error_log           /var/log/nginx/error.log warn;
+
+    # additional config
+
+    location  /backend/ws/ {
+        proxy_pass http://127.0.0.1:53170/;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "Upgrade";
+        proxy_set_header X-Real-IP $remote_addr;
+    }
+
+    location  /backend/ {
+        proxy_pass http://127.0.0.1:53170/;
+    }
+
+    location / {
+        proxy_pass http://127.0.0.1:53000/;
+    }
+}
+
+# HTTP redirect
+server {
+    listen      80;
+    listen      [::]:80;
+    server_name hoppscotch.example.com;
+    return      301 https://hoppscotch.example.com$request_uri;
+}

kutt

https://github.com/thedevs-network/kutt

docker-compose.yml

yaml
version: "3"
+
+services:
+  kutt:
+    image: kutt/kutt
+    depends_on:
+      - postgres
+      - redis
+    command: [ "./wait-for-it.sh", "postgres:5432", "--", "npm", "start" ]
+    ports:
+      - "3000:3000"
+    env_file:
+      - .env
+    environment:
+      DB_HOST: postgres
+      DB_NAME: kutt
+      DB_USER: user
+      DB_PASSWORD: passwd
+      REDIS_HOST: redis
+
+  redis:
+    image: redis:6.0-alpine
+    volumes:
+      - redis_data:/data
+
+  postgres:
+    image: postgres:12-alpine
+    environment:
+      POSTGRES_USER: user
+      POSTGRES_PASSWORD: passwd
+      POSTGRES_DB: kutt
+    volumes:
+      - postgres_data:/var/lib/postgresql/data
+
+volumes:
+  redis_data:
+  postgres_data:
version: "3"
+
+services:
+  kutt:
+    image: kutt/kutt
+    depends_on:
+      - postgres
+      - redis
+    command: [ "./wait-for-it.sh", "postgres:5432", "--", "npm", "start" ]
+    ports:
+      - "3000:3000"
+    env_file:
+      - .env
+    environment:
+      DB_HOST: postgres
+      DB_NAME: kutt
+      DB_USER: user
+      DB_PASSWORD: passwd
+      REDIS_HOST: redis
+
+  redis:
+    image: redis:6.0-alpine
+    volumes:
+      - redis_data:/data
+
+  postgres:
+    image: postgres:12-alpine
+    environment:
+      POSTGRES_USER: user
+      POSTGRES_PASSWORD: passwd
+      POSTGRES_DB: kutt
+    volumes:
+      - postgres_data:/var/lib/postgresql/data
+
+volumes:
+  redis_data:
+  postgres_data:

.env

properties
# App port to run on
+PORT=3000
+
+# The name of the site where Kutt is hosted
+SITE_NAME=Kutt
+
+# The domain that this website is on
+DEFAULT_DOMAIN=kutt.domain.com
+
+# Generated link length
+LINK_LENGTH=5
+
+# Postgres database credential details
+DB_HOST=postgres
+DB_PORT=5432
+DB_NAME=postgres
+DB_USER=user
+DB_PASSWORD=passwd
+DB_SSL=false
+
+# Redis host and port
+REDIS_HOST=redis
+REDIS_PORT=6379
+REDIS_PASSWORD=
+REDIS_DB=
+
+# Disable registration
+DISALLOW_REGISTRATION=true
+
+# Disable anonymous link creation
+DISALLOW_ANONYMOUS_LINKS=true
+
+# The daily limit for each user
+USER_LIMIT_PER_DAY=50
+
+# Create a cooldown for non-logged in users in minutes
+# Set 0 to disable
+NON_USER_COOLDOWN=0
+
+# Max number of visits for each link to have detailed stats
+DEFAULT_MAX_STATS_PER_LINK=5000
+
+# Use HTTPS for links with custom domain
+CUSTOM_DOMAIN_USE_HTTPS=false
+
+# A passphrase to encrypt JWT. Use a long and secure key.
+JWT_SECRET=xxx
+
+# Admin emails so they can access admin actions on settings page
+# Comma seperated
+ADMIN_EMAILS=user@domain.com
+
+# Invisible reCaptcha secret key
+# Create one in https://www.google.com/recaptcha/intro/
+RECAPTCHA_SITE_KEY=
+RECAPTCHA_SECRET_KEY=
+
+# Google Cloud API to prevent from users from submitting malware URLs.
+# Get it from https://developers.google.com/safe-browsing/v4/get-started
+GOOGLE_SAFE_BROWSING_KEY=
+
+# Your email host details to use to send verification emails.
+# More info on http://nodemailer.com/
+# Mail from example "Kutt <support@kutt.it>". Leave empty to use MAIL_USER
+MAIL_HOST=smtp.domain.com
+MAIL_PORT=465
+MAIL_SECURE=true
+MAIL_USER=user@domain.com
+MAIL_FROM=user@domain.com
+MAIL_PASSWORD=passwd
+
+# The email address that will receive submitted reports.
+REPORT_EMAIL=
+
+# Support email to show on the app
+CONTACT_EMAIL=
# App port to run on
+PORT=3000
+
+# The name of the site where Kutt is hosted
+SITE_NAME=Kutt
+
+# The domain that this website is on
+DEFAULT_DOMAIN=kutt.domain.com
+
+# Generated link length
+LINK_LENGTH=5
+
+# Postgres database credential details
+DB_HOST=postgres
+DB_PORT=5432
+DB_NAME=postgres
+DB_USER=user
+DB_PASSWORD=passwd
+DB_SSL=false
+
+# Redis host and port
+REDIS_HOST=redis
+REDIS_PORT=6379
+REDIS_PASSWORD=
+REDIS_DB=
+
+# Disable registration
+DISALLOW_REGISTRATION=true
+
+# Disable anonymous link creation
+DISALLOW_ANONYMOUS_LINKS=true
+
+# The daily limit for each user
+USER_LIMIT_PER_DAY=50
+
+# Create a cooldown for non-logged in users in minutes
+# Set 0 to disable
+NON_USER_COOLDOWN=0
+
+# Max number of visits for each link to have detailed stats
+DEFAULT_MAX_STATS_PER_LINK=5000
+
+# Use HTTPS for links with custom domain
+CUSTOM_DOMAIN_USE_HTTPS=false
+
+# A passphrase to encrypt JWT. Use a long and secure key.
+JWT_SECRET=xxx
+
+# Admin emails so they can access admin actions on settings page
+# Comma seperated
+ADMIN_EMAILS=user@domain.com
+
+# Invisible reCaptcha secret key
+# Create one in https://www.google.com/recaptcha/intro/
+RECAPTCHA_SITE_KEY=
+RECAPTCHA_SECRET_KEY=
+
+# Google Cloud API to prevent from users from submitting malware URLs.
+# Get it from https://developers.google.com/safe-browsing/v4/get-started
+GOOGLE_SAFE_BROWSING_KEY=
+
+# Your email host details to use to send verification emails.
+# More info on http://nodemailer.com/
+# Mail from example "Kutt <support@kutt.it>". Leave empty to use MAIL_USER
+MAIL_HOST=smtp.domain.com
+MAIL_PORT=465
+MAIL_SECURE=true
+MAIL_USER=user@domain.com
+MAIL_FROM=user@domain.com
+MAIL_PASSWORD=passwd
+
+# The email address that will receive submitted reports.
+REPORT_EMAIL=
+
+# Support email to show on the app
+CONTACT_EMAIL=

kutt.domain.com.conf

nginx
server {
+    listen              443 ssl;
+    listen              [::]:443 ssl;
+    server_name         kutt.domain.com;
+
+    # SSL
+    ssl_certificate     /etc/nginx/ssl/kutt.domain.com.crt;
+    ssl_certificate_key /etc/nginx/ssl/kutt.domain.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.1 TLSv1.2;
+
+    # security
+    include             nginxconfig.io/security.conf;
+
+    # logging
+    access_log          /var/log/nginx/access_kutt.log combined buffer=512k flush=1m;
+    error_log           /var/log/nginx/error_kutt.log warn;
+
+    # additional config
+    #include             nginxconfig.io/general.conf;
+    location / {
+        proxy_pass http://127.0.0.1:3000;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    }
+
+}
+
+
+# HTTP redirect
+server {
+    listen      80;
+    listen      [::]:80;
+    server_name .kutt.domain.com;
+    return      301 https://kutt.domain.com$request_uri;
+}
server {
+    listen              443 ssl;
+    listen              [::]:443 ssl;
+    server_name         kutt.domain.com;
+
+    # SSL
+    ssl_certificate     /etc/nginx/ssl/kutt.domain.com.crt;
+    ssl_certificate_key /etc/nginx/ssl/kutt.domain.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.1 TLSv1.2;
+
+    # security
+    include             nginxconfig.io/security.conf;
+
+    # logging
+    access_log          /var/log/nginx/access_kutt.log combined buffer=512k flush=1m;
+    error_log           /var/log/nginx/error_kutt.log warn;
+
+    # additional config
+    #include             nginxconfig.io/general.conf;
+    location / {
+        proxy_pass http://127.0.0.1:3000;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    }
+
+}
+
+
+# HTTP redirect
+server {
+    listen      80;
+    listen      [::]:80;
+    server_name .kutt.domain.com;
+    return      301 https://kutt.domain.com$request_uri;
+}

NAStool

yaml
version: "3"
+services:
+  nas-tools:
+    image: nastool/nas-tools:latest
+    ports:
+      - 3000:3000        # 默认的webui控制端口
+    volumes:
+      - ./config:/config   # 冒号左边请修改为你想保存配置的路径
+      - /repo/others:/repo/others   # 媒体目录,多个目录需要分别映射进来,需要满足配置文件说明中的要求
+      - /repo/movies:/repo/movies
+      - /repo/tvseries:/repo/tvseries
+      - /repo/resources/link:/repo/resources/link
+    environment:
+      - PUID=1000    # 想切换为哪个用户来运行程序,该用户的uid
+      - PGID=1000    # 想切换为哪个用户来运行程序,该用户的gid
+      - UMASK=022 # 掩码权限,默认000,可以考虑设置为022
+      - NASTOOL_AUTO_UPDATE=false  # 如需在启动容器时自动升级程程序请设置为true
+      - NASTOOL_CN_UPDATE=false # 如果开启了容器启动自动升级程序,并且网络不太友好时,可以设置为true,会使用国内源进行软件更新
+      #- REPO_URL=https://ghproxy.com/https://github.com/NAStool/nas-tools.git  # 当你访问github网络很差时,可以考虑解释本行注释
+    restart: always
+    network_mode: bridge
+    hostname: nas-tools
+    container_name: nastool
version: "3"
+services:
+  nas-tools:
+    image: nastool/nas-tools:latest
+    ports:
+      - 3000:3000        # 默认的webui控制端口
+    volumes:
+      - ./config:/config   # 冒号左边请修改为你想保存配置的路径
+      - /repo/others:/repo/others   # 媒体目录,多个目录需要分别映射进来,需要满足配置文件说明中的要求
+      - /repo/movies:/repo/movies
+      - /repo/tvseries:/repo/tvseries
+      - /repo/resources/link:/repo/resources/link
+    environment:
+      - PUID=1000    # 想切换为哪个用户来运行程序,该用户的uid
+      - PGID=1000    # 想切换为哪个用户来运行程序,该用户的gid
+      - UMASK=022 # 掩码权限,默认000,可以考虑设置为022
+      - NASTOOL_AUTO_UPDATE=false  # 如需在启动容器时自动升级程程序请设置为true
+      - NASTOOL_CN_UPDATE=false # 如果开启了容器启动自动升级程序,并且网络不太友好时,可以设置为true,会使用国内源进行软件更新
+      #- REPO_URL=https://ghproxy.com/https://github.com/NAStool/nas-tools.git  # 当你访问github网络很差时,可以考虑解释本行注释
+    restart: always
+    network_mode: bridge
+    hostname: nas-tools
+    container_name: nastool

telegram-bot-api

申请项目

https://core.telegram.org/api/obtaining_api_id

编译项目

repo:https://github.com/tdlib/telegram-bot-api

generator:https://tdlib.github.io/telegram-bot-api/build.html

shell
apt-get update
+apt-get upgrade
+apt-get install make git zlib1g-dev libssl-dev gperf cmake g++
+git clone --recursive https://github.com/tdlib/telegram-bot-api.git
+cd telegram-bot-api
+rm -rf build
+mkdir build
+cd build
+cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr/local ..
+cmake --build . --target install
+cd ../..
+ls -l /usr/local/bin/telegram-bot-api*
apt-get update
+apt-get upgrade
+apt-get install make git zlib1g-dev libssl-dev gperf cmake g++
+git clone --recursive https://github.com/tdlib/telegram-bot-api.git
+cd telegram-bot-api
+rm -rf build
+mkdir build
+cd build
+cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr/local ..
+cmake --build . --target install
+cd ../..
+ls -l /usr/local/bin/telegram-bot-api*

配置启动

systemctl edit --force --full tgbot-api.service

shell
[Unit]
+Description=telegram bot api
+After=network.target
+[Service]
+Environment="TELEGRAM_API_ID=xxx"
+Environment="TELEGRAM_API_HASH=xxx"
+ExecStart=/usr/local/bin/telegram-bot-api --http-port=16666 --local --log=/var/log/telegram-bot-api/tg-bot-api.log
+[Install]
+WantedBy=multi-user.target
[Unit]
+Description=telegram bot api
+After=network.target
+[Service]
+Environment="TELEGRAM_API_ID=xxx"
+Environment="TELEGRAM_API_HASH=xxx"
+ExecStart=/usr/local/bin/telegram-bot-api --http-port=16666 --local --log=/var/log/telegram-bot-api/tg-bot-api.log
+[Install]
+WantedBy=multi-user.target

immich

https://github.com/immich-app/immich

memos

https://github.com/usememos/memos

`,148),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const h=s(p,[["render",t]]);export{u as __pageData,h as default}; diff --git "a/assets/tinker_network_home server\346\220\255\345\273\272.md.862c471d.lean.js" "b/assets/tinker_network_home server\346\220\255\345\273\272.md.862c471d.lean.js" new file mode 100644 index 000000000..727ca2fbe --- /dev/null +++ "b/assets/tinker_network_home server\346\220\255\345\273\272.md.862c471d.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"家庭服务器home server搭建","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/network/home server搭建.md","filePath":"tinker/network/home server搭建.md","lastUpdated":1708510102000}'),p={name:"tinker/network/home server搭建.md"},o=l("",148),e=[o];function t(c,r,E,y,i,F){return n(),a("div",null,e)}const h=s(p,[["render",t]]);export{u as __pageData,h as default}; diff --git "a/assets/tinker_network_openwrt\345\256\211\350\243\205\345\217\212\351\205\215\347\275\256.md.55152baa.js" "b/assets/tinker_network_openwrt\345\256\211\350\243\205\345\217\212\351\205\215\347\275\256.md.55152baa.js" new file mode 100644 index 000000000..af438bcb7 --- /dev/null +++ "b/assets/tinker_network_openwrt\345\256\211\350\243\205\345\217\212\351\205\215\347\275\256.md.55152baa.js" @@ -0,0 +1,23 @@ +import{_ as e,o as a,c as s,Q as n}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"openwrt安装及配置","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/network/openwrt安装及配置.md","filePath":"tinker/network/openwrt安装及配置.md","lastUpdated":1708510102000}'),p={name:"tinker/network/openwrt安装及配置.md"},l=n(`

openwrt安装及配置

趁着五一假期把家用服务器装好跑起来了,另外还买了一台J4125工控机来做软路由。工控机的机器今天刚到,下午反复折腾了pve虚拟机安装openwrt和物理机直接安装,目前是用物理机装好配置完毕了,但是看了一眼这监控数据,总感觉有点浪费性能。所以这篇文章写完我还是要换回pve安装(doge),剩下的性能折腾下其他虚拟机。

物理机安装

下载需要的软件

txt
1. pe工具箱
+2. openwrt编译好的镜像,我用的是esir的固件
+3. 刷写镜像的软件physdiskwrite.exe
1. pe工具箱
+2. openwrt编译好的镜像,我用的是esir的固件
+3. 刷写镜像的软件physdiskwrite.exe

准备工作

txt
1. pe工具箱安装到U盘中
+2. 把解压后的x86.img镜像文件和physdiskwrite.exe软件复制到U盘中
+3. 这时候已经可以拔掉硬路由器了,光猫lan口连软路由wan口(要问卖家哪个是wan口,我这个eth1是wan口),pc连软路由lan口(0、2、3),这是为了pc能跟软路由在同一个网段,如果不在则需要手动配置静态ip
1. pe工具箱安装到U盘中
+2. 把解压后的x86.img镜像文件和physdiskwrite.exe软件复制到U盘中
+3. 这时候已经可以拔掉硬路由器了,光猫lan口连软路由wan口(要问卖家哪个是wan口,我这个eth1是wan口),pc连软路由lan口(0、2、3),这是为了pc能跟软路由在同一个网段,如果不在则需要手动配置静态ip

安装流程

txt
1. u盘启动工控机
+2. 用pe工具箱自带的diskgenius把装的硬盘格式化即可,注意这里不要进行分区,否则会写盘失败
+3. 打开cmd窗口,执行命令 physdiskwrite.exe -u x86.img,这里可以直接用鼠标把程序拖到cmd窗口,会自动拼出完整路径
+4. 根据终端输出选择要写入的硬盘,比如0是硬盘,1是u盘,就填0,enter即可开始写盘,等待写盘完毕即可
+5. 扩容系统分区,使用diskgenius找到刚刚写入的硬盘,然后再选中上面灰色未使用的分区,右击菜单中选择 “将空间分配给” -> “分区:未格式化(D:)”,然后确认即可
+6. 写盘完毕,选择重启,这时候直接拔出U盘即可
+7. 等待重启后,openwrt系统就已经成功启动了,如果不确定是否安装完,回车一下看看是否有lede的banner输出就行了
+8. openwrt一般ip为192.168.2.1(esir固件是5.1),这个可以看固件wiki或者自己ifconfig看一下
1. u盘启动工控机
+2. 用pe工具箱自带的diskgenius把装的硬盘格式化即可,注意这里不要进行分区,否则会写盘失败
+3. 打开cmd窗口,执行命令 physdiskwrite.exe -u x86.img,这里可以直接用鼠标把程序拖到cmd窗口,会自动拼出完整路径
+4. 根据终端输出选择要写入的硬盘,比如0是硬盘,1是u盘,就填0,enter即可开始写盘,等待写盘完毕即可
+5. 扩容系统分区,使用diskgenius找到刚刚写入的硬盘,然后再选中上面灰色未使用的分区,右击菜单中选择 “将空间分配给” -> “分区:未格式化(D:)”,然后确认即可
+6. 写盘完毕,选择重启,这时候直接拔出U盘即可
+7. 等待重启后,openwrt系统就已经成功启动了,如果不确定是否安装完,回车一下看看是否有lede的banner输出就行了
+8. openwrt一般ip为192.168.2.1(esir固件是5.1),这个可以看固件wiki或者自己ifconfig看一下

配置

上网相关配置

  1. 编辑lan口

image-20220503222750537

  1. 选择另外两个lan口,共三个lan口

image-20220503221537487

  1. 开启强制dhcp服务

image-20220503221601540

  1. 保存&应用
  2. 配置软路由拨号,这之前需要先把光猫设为桥接模式,参考前一篇博客移动光猫改桥接模式

image-20220503222634621

选择PPPoE协议并切换,填上宽带账号密码保存&应用即可

image-20220503222905747

固定ip配置

  1. 选择DHCP/DNS菜单,找到静态地址分配

image-20220503221716649

可以根据设备mac地址,自定义主机名和ip,并设置租期,这里一般我们设置静态ip都是永久的,填infinite。保存&应用,然后重启软路由即可。

这里可能重启软路由和设备后也不会让静态ip配置生效,网上找到了一篇文章的分析,跟第一次获取的ip租期未到期有管,可以手动释放旧的租约,然后刷新租约。windows下ipconfig /release & ipconfig /renew,linux下dhclient -r & dhclient -s 192.168.2.1

以下为原文:https://www.csdn.net/tags/NtjakgwsOTY3ODMtYmxvZwO0O0OO0O0O.html

我在使用 Openwrt 时手动分配了新的静态 IP 给我的电脑,但是在保存并应用后并没有立即生效,而且在我分别重启了电脑和路由器后仍然没有生效,为此我花了点时间找出了解决方法。

原因分析

在“DHCP/DNS->静态地址分配”中给电脑配置了静态地址不会立即生效,因为在此之前路由器已经通过 DHCP 分配了 IP 地址给电脑形成租约,在这个租约到期之前不会改变分配给电脑的 IP。通常我们在 Lan 中设置租约时间为 12h(小时),也就意味着要在 12 小时后电脑才会获取到我们设置的静态 IP。

不过我们可以清空路由器上的旧租约,同时将电脑断网重连,以此来使电脑获得新 IP 地址。最简单的方法就是将路由器重启,既清空了旧租约,又使电脑重连。但是为什么我之前重启会不起作用呢?

说实话这锅还真不好甩,我的电脑是 Win10 系统,在我重启路由器后,系统并不是向路由器请求一份新的租约,而是拿着旧的租约想要更新续约。这里你可能认为是路由器就直接续约了,但我认为并不是,OpenWrt 已经设定了静态地址,而电脑请求续约的 IP 不一样,结果是 OpenWrt 不会给续约,但也不会返回新的租约。

最终导致的结果就是电脑租约无法更新,但由于租约也没有到期,所以电脑继续使用旧的,而正好使用旧IP还能正常上网就一直沿用旧租约了。

解决方法

最简单的方法,设置的静态 IP 为原本 DHCP 获取到的 IP 地址,这样就不会存在不生效问题。但一定要更换 IP 的话,保证 OpenWrt 已重启,打开 Windows 命令行或者 Power Shell,输入以下命令执行:ipconfig /release

ipconfig /renew

第一条命令删除旧租约,这样就不会由于 IP 地址错误导致 OpenWrt 无法返回新租约,第二条命令就是手动更新租约。至此,解决了静态 IP 分配不生效的问题。

PVE虚拟机安装

参考b站up主司波图的教程

`,28),t=[l];function o(i,r,c,d,h,g){return a(),s("div",null,t)}const w=e(p,[["render",o]]);export{u as __pageData,w as default}; diff --git "a/assets/tinker_network_openwrt\345\256\211\350\243\205\345\217\212\351\205\215\347\275\256.md.55152baa.lean.js" "b/assets/tinker_network_openwrt\345\256\211\350\243\205\345\217\212\351\205\215\347\275\256.md.55152baa.lean.js" new file mode 100644 index 000000000..1b24717a0 --- /dev/null +++ "b/assets/tinker_network_openwrt\345\256\211\350\243\205\345\217\212\351\205\215\347\275\256.md.55152baa.lean.js" @@ -0,0 +1 @@ +import{_ as e,o as a,c as s,Q as n}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"openwrt安装及配置","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/network/openwrt安装及配置.md","filePath":"tinker/network/openwrt安装及配置.md","lastUpdated":1708510102000}'),p={name:"tinker/network/openwrt安装及配置.md"},l=n("",28),t=[l];function o(i,r,c,d,h,g){return a(),s("div",null,t)}const w=e(p,[["render",o]]);export{u as __pageData,w as default}; diff --git "a/assets/tinker_network_openwrt\345\274\200\345\220\257ipv6.md.edcbc94f.js" "b/assets/tinker_network_openwrt\345\274\200\345\220\257ipv6.md.edcbc94f.js" new file mode 100644 index 000000000..65ea0e27c --- /dev/null +++ "b/assets/tinker_network_openwrt\345\274\200\345\220\257ipv6.md.edcbc94f.js" @@ -0,0 +1 @@ +import{_ as a,o as e,c as t,Q as r}from"./chunks/framework.dc1f4608.js";const g=JSON.parse('{"title":"openwrt开启ipv6","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/network/openwrt开启ipv6.md","filePath":"tinker/network/openwrt开启ipv6.md","lastUpdated":1708510102000}'),o={name:"tinker/network/openwrt开启ipv6.md"},i=r('

openwrt开启ipv6

背景

由于移动宽带没有公网ip,玩儿pt上传上不去,所以考虑开启ipv6提速

具体操作

全局网络选项中清除IPv6 ULA前缀

image-20220510221313367

获取IPv6地址改为自动

image-20220510221440677

image-20220510221714830

DHCP/DNS中取消禁止解析IPv6 DNS记录的勾选

image-20220510221817531

重启openwrt

',12),n=[i];function p(s,l,c,h,d,m){return e(),t("div",null,n)}const v=a(o,[["render",p]]);export{g as __pageData,v as default}; diff --git "a/assets/tinker_network_openwrt\345\274\200\345\220\257ipv6.md.edcbc94f.lean.js" "b/assets/tinker_network_openwrt\345\274\200\345\220\257ipv6.md.edcbc94f.lean.js" new file mode 100644 index 000000000..cdd957956 --- /dev/null +++ "b/assets/tinker_network_openwrt\345\274\200\345\220\257ipv6.md.edcbc94f.lean.js" @@ -0,0 +1 @@ +import{_ as a,o as e,c as t,Q as r}from"./chunks/framework.dc1f4608.js";const g=JSON.parse('{"title":"openwrt开启ipv6","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/network/openwrt开启ipv6.md","filePath":"tinker/network/openwrt开启ipv6.md","lastUpdated":1708510102000}'),o={name:"tinker/network/openwrt开启ipv6.md"},i=r("",12),n=[i];function p(s,l,c,h,d,m){return e(),t("div",null,n)}const v=a(o,[["render",p]]);export{g as __pageData,v as default}; diff --git "a/assets/tinker_network_pt\344\270\213\350\275\275\345\205\245\351\227\250.md.0f6b065f.js" "b/assets/tinker_network_pt\344\270\213\350\275\275\345\205\245\351\227\250.md.0f6b065f.js" new file mode 100644 index 000000000..02f57c4c6 --- /dev/null +++ "b/assets/tinker_network_pt\344\270\213\350\275\275\345\205\245\351\227\250.md.0f6b065f.js" @@ -0,0 +1 @@ +import{_ as a,o as t,c as e,Q as r}from"./chunks/framework.dc1f4608.js";const f=JSON.parse('{"title":"pt下载入门","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/network/pt下载入门.md","filePath":"tinker/network/pt下载入门.md","lastUpdated":1708510102000}'),l={name:"tinker/network/pt下载入门.md"},i=r('

pt下载入门

什么是pt

PT(Private Tracker):是一种基于私有BT Tracker服务器的资源传播形式,经授权的用户使用受允许的客户端进行种子制作与下载,对参与的用户做出流量统计(需要用户完成一定量的上传)。

pt的优缺点

上面提到PT的关键词:私有、小范围、流量统计。传统的BT则是公有、大范围(整个互联网)、不统计流量。BT是公开的tracker,范围很广,但是有很多人只下载不上传(h&r/hit and run),俗称白嫖,因此无法保证种子的下载速度。PT则解决了这一痛点,强制要求用户上传,换来了高速下载,另外pt站点的资源质量很高,资源更新更快。当然PT的局限就是圈子小,入门门槛高,往往pt站点采用邀请制+捐赠制。

玩儿pt的必要条件

硬件准备

家用pc、nas、服务器、能装下载工具的路由器、各种下载机(玩客云、n1盒子等)均可

硬盘

越大越好

pt站点邀请码获取途径

  • 捐赠入站

  • 通过别人邀请

  • pt站点开放注册时注册加入

pt入门

基本概念

  • 种子:根据 BitTorrent 协议,文件发布者会根据要发布的文件生成提供一个.torrent 文件,即种子文件,也简称为“种子”。种子文件本质上是文本文件,包含 Tracker信息和文件信息两部分。Tracker 信息主要是 BT 下载中需要用到的 Tracker 服务器的地址和针对 Tracker 服务器的设置,文件信息是根据 对目标文件的计算生成的,计算结果根据 BitTorrent 协议内的规则进行编码。它的 主要原理是需要把提供下载的文件虚拟分成大小相等的块,块大小必须为 2k 的整数次方(由于是虚拟分块,硬盘上并不产生各个块文件),并把每个块的索引信息和 Hash 验证码写入种子文件中;所以,种子文件就是被下载文件的“索引”。 下载者要下载文件内容,需要先得到相应的种子文件,然后使用 BT 客户端软件进行下载。下载时,BT 客户端首先解析种子文件得到 Tracker 地址,然后连接 Tracker 服务器。Tracker 服务器回应下载者的请求,提供下载者其他下载者(包括发布者)的 IP。下载者再连接其他 下载者,根据种子文件,两者分别告知对方自己已经有的块,然后交换对方所没有的数据。 此时不需要其他服务器参与,分散了单个线路上的数据流量,因此减轻了服务器负担。 下载者每得到一个块,需要算出下载块的 Hash 验证码与种子文件中的对比,如果一样则说 明块正确,不一样则需要重新下载这个块。这种规定是为了解决下载内容淮确性的问题。 一般的 HTTP与FTP 下载,发布文件仅在某个或某几个服务器,下载的人太多,服务器的带宽 很易不胜负荷,变得很慢。而 BitTorrent 协议下载的特点是,下载的人越多,提供的带宽也 越多,下载速度就越快。同时,拥有完整文件的用户也会越来越多,使文件的“寿命”不断延长。

  • Tarcker: 收集下载者信息的服务器,并将此信息提供给其他下载者,可以理解为电话总机。

  • 做种:资源下载完毕后不删除任务,保持继续上传的过程

  • 辅种:其他人发布了资源,你手里刚好也有这个资源,那么你下载种子之后。只要数据通过了 hash 校验就会变成做种状态。(部分站点禁止)

  • 上传量:做种时上传的流量

  • 下载量:下载资源的流量

  • 分享率:上传量/下载量

  • 做种率:做种时间/下载时间

  • 魔力值:pt站点的积分,可以通过做种、签到等途径获取

  • 最小做种时间:为了保证种子的活跃度,一些PT站点严禁H&R(Hit and Run、下了就跑),要求用户至少持续做种一段时间

新手考核

类似游戏的新手区教学,让新手了解PT站点的规则,PT站点还会对新人进行考核,一般考核期为一个月,考核主要从以下几点:

考核点说明要求
上传量上传了多少数据30G-100G不等
下载量下载了多少数据30G-100G不等
分享率上传量/下载量一般要求分享率>1
做种率做种时间/下载时间一般要求3-8不等
魔力值通过签到、做种等途径获取的积分一般要求3000-8000不等

生存指南

考核期

老老实实下热门种子并持续做种

考核后

对于一些以影视作品为主的网站,尽可能的下载热门种子,这样能更快的获得上传量。可以使用 BT 客户端的 RSS 订阅功能,实现无人值守下载。 对于以小种为主的 PT 站,如 OpenCD 以及大部分教育网站点,则需要通过下载大量小体积种子并长时间做种以换取魔力值,再使用魔力值兑换上传。

养老期

保证良好分享率

技巧

种子挑选

  • 在种子板块新发布且带有Free标签的种子是不计算下载量的

  • 带有2xFree是不计算下载量且计算双倍上传流量的,一般选这种种子数据会更好看

  • 要注意免费时常

  • 进到种子页面,看种子的做种者数量和下载者数量,挑做种人少&下载人多的。

  • 无标记为正常计算上传、下载量

  • 带有%50标记为计算50%下载量

  • 带有2x%50为计算%50下载量、计算两倍上传量

提高分享率

不断下载免费种子,多维持一些,种子体积可以挑选大一点的,然后挂机做种提升上传量,在下载量不变的情况下,上传越高分量率就越高。

赚魔力值

  • 发布种子
  • 上传字幕
  • 发表主题贴
  • 辅种挂机

常用站点

',34),p=[i];function h(o,n,d,c,s,b){return t(),e("div",null,p)}const k=a(l,[["render",h]]);export{f as __pageData,k as default}; diff --git "a/assets/tinker_network_pt\344\270\213\350\275\275\345\205\245\351\227\250.md.0f6b065f.lean.js" "b/assets/tinker_network_pt\344\270\213\350\275\275\345\205\245\351\227\250.md.0f6b065f.lean.js" new file mode 100644 index 000000000..a70306205 --- /dev/null +++ "b/assets/tinker_network_pt\344\270\213\350\275\275\345\205\245\351\227\250.md.0f6b065f.lean.js" @@ -0,0 +1 @@ +import{_ as a,o as t,c as e,Q as r}from"./chunks/framework.dc1f4608.js";const f=JSON.parse('{"title":"pt下载入门","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/network/pt下载入门.md","filePath":"tinker/network/pt下载入门.md","lastUpdated":1708510102000}'),l={name:"tinker/network/pt下载入门.md"},i=r("",34),p=[i];function h(o,n,d,c,s,b){return t(),e("div",null,p)}const k=a(l,[["render",h]]);export{f as __pageData,k as default}; diff --git "a/assets/tinker_network_ubuntu-server\345\274\200\345\220\257\347\275\221\347\273\234\345\224\244\351\206\222.md.70e29fdd.js" "b/assets/tinker_network_ubuntu-server\345\274\200\345\220\257\347\275\221\347\273\234\345\224\244\351\206\222.md.70e29fdd.js" new file mode 100644 index 000000000..084023082 --- /dev/null +++ "b/assets/tinker_network_ubuntu-server\345\274\200\345\220\257\347\275\221\347\273\234\345\224\244\351\206\222.md.70e29fdd.js" @@ -0,0 +1,49 @@ +import{_ as s,o as a,c as n,Q as e}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"ubuntu-server开启网络唤醒和通电自动启动","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/network/ubuntu-server开启网络唤醒.md","filePath":"tinker/network/ubuntu-server开启网络唤醒.md","lastUpdated":1708510102000}'),l={name:"tinker/network/ubuntu-server开启网络唤醒.md"},o=e(`

ubuntu-server开启网络唤醒和通电自动启动

前提

主板支持WOL(Wake on LAN)功能和来电开机

配置

主板BIOS中开启网络唤醒功能和断电开机功能

https://endownload.colorful.cn/EnDownload/MotherBroard/2022/Intel 600/Manual/Intel 600 Series BIOS Chinese/Intel 600 Series BIOS User Guide.pdf

以七彩虹主板为例

txt
ADVANCED(高级模式)> Power Management Configuration(电源管理配置)> Wake By Lan(网卡唤醒)
+Wake By Lan(网卡唤醒) 
+设置网络唤醒功能。 
+[Enabled] 当检测到 LAN 设备已激活或有信号输入时,唤醒系统。 
+[Disabled] 关闭网络唤醒功能。
+
+
+ADVANCED(高级模式)> Power Management Configuration(电源管理配置)> AC Power Loss(断电开机功能) 
+AC Power Loss(断电开机功能) 
+设置计算机断电之后,电源再次被接通时计算机的响应状态。 
+[Power On] 通电后计算机自动开机。 
+[Power Off] 通电后计算机保持关机状态。 
+[Last State]] 通电后计算机恢复上次断电前的状态。
ADVANCED(高级模式)> Power Management Configuration(电源管理配置)> Wake By Lan(网卡唤醒)
+Wake By Lan(网卡唤醒) 
+设置网络唤醒功能。 
+[Enabled] 当检测到 LAN 设备已激活或有信号输入时,唤醒系统。 
+[Disabled] 关闭网络唤醒功能。
+
+
+ADVANCED(高级模式)> Power Management Configuration(电源管理配置)> AC Power Loss(断电开机功能) 
+AC Power Loss(断电开机功能) 
+设置计算机断电之后,电源再次被接通时计算机的响应状态。 
+[Power On] 通电后计算机自动开机。 
+[Power Off] 通电后计算机保持关机状态。 
+[Last State]] 通电后计算机恢复上次断电前的状态。

ubuntu-server中开启网卡的网络唤醒功能

使用ethtool查看信息

shell
# 如果没有需要先安装 apt install ethtool
+# 首先使用ifconfig或ip a查看设备名称 我的是enp3s0
+
+ethtool enp3s0 | grep "Wake-on"
+	Supports Wake-on: pumbg
+	Wake-on: d
# 如果没有需要先安装 apt install ethtool
+# 首先使用ifconfig或ip a查看设备名称 我的是enp3s0
+
+ethtool enp3s0 | grep "Wake-on"
+	Supports Wake-on: pumbg
+	Wake-on: d

这个信息说明支持pumbg几种唤醒方式,而d表示当前处于禁用状态

"Wake-on" 中的不同字母代表不同的唤醒模式 这些字母分别代表以下内容:

  • p:代表PHY(物理层)唤醒模式,这种模式是基于物理层的唤醒模式。
  • u:代表UDP数据包唤醒模式,这种模式需要特定的UDP数据包来唤醒设备。
  • m:代表多播唤醒模式,这允许多播数据包来唤醒设备。
  • b:代表广播唤醒模式,这允许广播数据包来唤醒设备。
  • g:代表魔术唤醒帧模式,这是一种常用的唤醒模式,需要特定的魔术唤醒数据包。

魔术唤醒数据包(Magic Wake-on-LAN Packet)是一种特殊的数据包,用于远程唤醒计算机或网络设备。它通常用于通过局域网远程唤醒处于休眠或关机状态的设备,以便进行远程管理或访问。这些数据包被称为"魔术",因为它们包含了一些特定的唤醒模式信息,以便网络接口卡能够识别并唤醒目标设备。

魔术唤醒数据包的结构通常包括以下元素:

  1. 目标设备的MAC地址:这是数据包的目标设备的物理地址,以便网络接口卡知道唤醒哪台设备。
  2. 以太网帧:数据包以标准的以太网帧格式进行封装。
  3. 魔术唤醒模式信息:这些信息告诉网络接口卡以特定方式处理数据包,以实现唤醒功能。

启用网络唤醒功能

ethtool -s enp3s0 wol g

重启后网络唤醒会失效,配置网络唤醒持久化

shell
sudo systemctl edit --force --full wol-enable.service
sudo systemctl edit --force --full wol-enable.service

可以使用update-alternatives --config editor修改默认编辑器

shell
[Unit]
+Description=Enable Wake-on-LAN
+
+[Service]
+ExecStart=/usr/sbin/ethtool -s enp3s0 wol g
+
+[Install]
+WantedBy=multi-user.target
[Unit]
+Description=Enable Wake-on-LAN
+
+[Service]
+ExecStart=/usr/sbin/ethtool -s enp3s0 wol g
+
+[Install]
+WantedBy=multi-user.target

systemctl daemon-reload && systemctl enable wol-enable.service && systemctl start wol-enable.service

网络唤醒调用

不同平台都有相对应的唤醒客户端,我主要是从软路由使用这个功能。使用Etherwake选中要唤醒的设备点击唤醒主机即可发送数据包成功唤醒。

image-20231020231608248

来电自启

可以配合UPS使用实现断电安全关机以及来电自动启动,详情见另一篇关于UPS和NUT的博客。

`,25),p=[o];function t(c,r,i,y,d,u){return a(),n("div",null,p)}const b=s(l,[["render",t]]);export{h as __pageData,b as default}; diff --git "a/assets/tinker_network_ubuntu-server\345\274\200\345\220\257\347\275\221\347\273\234\345\224\244\351\206\222.md.70e29fdd.lean.js" "b/assets/tinker_network_ubuntu-server\345\274\200\345\220\257\347\275\221\347\273\234\345\224\244\351\206\222.md.70e29fdd.lean.js" new file mode 100644 index 000000000..6ae01ef71 --- /dev/null +++ "b/assets/tinker_network_ubuntu-server\345\274\200\345\220\257\347\275\221\347\273\234\345\224\244\351\206\222.md.70e29fdd.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as e}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"ubuntu-server开启网络唤醒和通电自动启动","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/network/ubuntu-server开启网络唤醒.md","filePath":"tinker/network/ubuntu-server开启网络唤醒.md","lastUpdated":1708510102000}'),l={name:"tinker/network/ubuntu-server开启网络唤醒.md"},o=e("",25),p=[o];function t(c,r,i,y,d,u){return a(),n("div",null,p)}const b=s(l,[["render",t]]);export{h as __pageData,b as default}; diff --git "a/assets/tinker_network_windows\346\214\202\350\275\275webdav\347\232\204\351\227\256\351\242\230\345\244\204\347\220\206.md.3ad32a6e.js" "b/assets/tinker_network_windows\346\214\202\350\275\275webdav\347\232\204\351\227\256\351\242\230\345\244\204\347\220\206.md.3ad32a6e.js" new file mode 100644 index 000000000..f44bf26ca --- /dev/null +++ "b/assets/tinker_network_windows\346\214\202\350\275\275webdav\347\232\204\351\227\256\351\242\230\345\244\204\347\220\206.md.3ad32a6e.js" @@ -0,0 +1 @@ +import{_ as e,o as a,c as o,Q as t}from"./chunks/framework.dc1f4608.js";const b=JSON.parse('{"title":"windows挂载webdav的问题处理","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/network/windows挂载webdav的问题处理.md","filePath":"tinker/network/windows挂载webdav的问题处理.md","lastUpdated":1708510102000}'),d={name:"tinker/network/windows挂载webdav的问题处理.md"},i=t('

windows挂载webdav的问题处理

背景

home server里开着阿里云盘的webdav容器,想要在pc的windows中挂载,之前用的RaiDriver这个软件,但是开机启动总弹广告,遂弃用。用windows原生的挂载方式,直接在资源管理器中右键选择添加一个网络位置,填写http的webdav服务地址+端口后提示输入的文件夹似乎无效

解决方案

出现这个提示是因为windows本身的权限控制,可以在注册表中修改相关配置。

具体操作:

  • 修改注册表\\HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\WebClient\\ParametersBasicAuthLevel的值为2

  • 在服务中把WebClient启动,并把启动类型改为自动

然后就可以在资源管理器中再次尝试添加网络位置,输入ip+端口即可。

',8),r=[i];function n(s,c,w,_,l,p){return a(),o("div",null,r)}const v=e(d,[["render",n]]);export{b as __pageData,v as default}; diff --git "a/assets/tinker_network_windows\346\214\202\350\275\275webdav\347\232\204\351\227\256\351\242\230\345\244\204\347\220\206.md.3ad32a6e.lean.js" "b/assets/tinker_network_windows\346\214\202\350\275\275webdav\347\232\204\351\227\256\351\242\230\345\244\204\347\220\206.md.3ad32a6e.lean.js" new file mode 100644 index 000000000..ed813206a --- /dev/null +++ "b/assets/tinker_network_windows\346\214\202\350\275\275webdav\347\232\204\351\227\256\351\242\230\345\244\204\347\220\206.md.3ad32a6e.lean.js" @@ -0,0 +1 @@ +import{_ as e,o as a,c as o,Q as t}from"./chunks/framework.dc1f4608.js";const b=JSON.parse('{"title":"windows挂载webdav的问题处理","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/network/windows挂载webdav的问题处理.md","filePath":"tinker/network/windows挂载webdav的问题处理.md","lastUpdated":1708510102000}'),d={name:"tinker/network/windows挂载webdav的问题处理.md"},i=t("",8),r=[i];function n(s,c,w,_,l,p){return a(),o("div",null,r)}const v=e(d,[["render",n]]);export{b as __pageData,v as default}; diff --git "a/assets/tinker_network_\344\275\277\347\224\250https\350\256\277\351\227\256\345\206\205\347\275\221\346\234\215\345\212\241.md.43b6af56.js" "b/assets/tinker_network_\344\275\277\347\224\250https\350\256\277\351\227\256\345\206\205\347\275\221\346\234\215\345\212\241.md.43b6af56.js" new file mode 100644 index 000000000..03062d739 --- /dev/null +++ "b/assets/tinker_network_\344\275\277\347\224\250https\350\256\277\351\227\256\345\206\205\347\275\221\346\234\215\345\212\241.md.43b6af56.js" @@ -0,0 +1,57 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const F=JSON.parse('{"title":"使用https访问内网服务","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/network/使用https访问内网服务.md","filePath":"tinker/network/使用https访问内网服务.md","lastUpdated":1708510102000}'),p={name:"tinker/network/使用https访问内网服务.md"},o=l(`

使用https访问内网服务

https://github.com/linuxserver/docker-swag

配置dns解析

将需要的子域名使用ddns解析到wan口ip

openwrt启动docker-swag容器

yaml
# docker-compose.yml
+
+version: "3"
+services:
+  swag:
+    image: linuxserver/swag:latest
+    container_name: swag
+    cap_add:
+      - NET_ADMIN
+    environment:
+      - PUID=1000
+      - PGID=1000
+      - TZ=Asia/Shanghai # 时区
+      - URL=yourdomain.com # 主域名
+      - VALIDATION=dns # certbot验证的方法,一般选dns
+      - SUBDOMAINS=yoursubdoamin.yourdomain.com
+      - CERTPROVIDER= # 可以填zerossl,默认使用let's encrypt签发证书
+      - DNSPLUGIN=dnspod # 支持aliyun、dnspod、cloudflare等等,详情见官方文档
+      - PROPAGATION= # 选择覆盖dns插件的默认传播时间(以秒为单位)
+      - EMAIL=user@email.com # 邮箱
+      - ONLY_SUBDOMAINS=true # 是否只获取子域名的证书
+      - EXTRA_DOMAINS= # 其他完全限定域名(逗号分隔,无空格)例如 extradomain.com,subdomain.anotherdomain.org
+      - STAGING=false # 设置为 true 以在暂存模式下检索证书。但生成的证书将无法通过浏览器的安全测试。仅用于测试。
+    volumes:
+      - /docker/swag/config:/config
+    ports:
+      - 65111:443 # 映射端口,家宽可以选择高位端口
+#      - 80:80 #optional
+    restart: unless-stopped
# docker-compose.yml
+
+version: "3"
+services:
+  swag:
+    image: linuxserver/swag:latest
+    container_name: swag
+    cap_add:
+      - NET_ADMIN
+    environment:
+      - PUID=1000
+      - PGID=1000
+      - TZ=Asia/Shanghai # 时区
+      - URL=yourdomain.com # 主域名
+      - VALIDATION=dns # certbot验证的方法,一般选dns
+      - SUBDOMAINS=yoursubdoamin.yourdomain.com
+      - CERTPROVIDER= # 可以填zerossl,默认使用let's encrypt签发证书
+      - DNSPLUGIN=dnspod # 支持aliyun、dnspod、cloudflare等等,详情见官方文档
+      - PROPAGATION= # 选择覆盖dns插件的默认传播时间(以秒为单位)
+      - EMAIL=user@email.com # 邮箱
+      - ONLY_SUBDOMAINS=true # 是否只获取子域名的证书
+      - EXTRA_DOMAINS= # 其他完全限定域名(逗号分隔,无空格)例如 extradomain.com,subdomain.anotherdomain.org
+      - STAGING=false # 设置为 true 以在暂存模式下检索证书。但生成的证书将无法通过浏览器的安全测试。仅用于测试。
+    volumes:
+      - /docker/swag/config:/config
+    ports:
+      - 65111:443 # 映射端口,家宽可以选择高位端口
+#      - 80:80 #optional
+    restart: unless-stopped

启动容器后操作

  1. config/dns-conf目录中找到自己选择的dns插件的配置文件,按要求填写验证信息

  2. config/nginx/proxy-confs基于给出的模板配置文件修改出自己的虚拟主机配置,将请求反向代理到内网的指定服务

  3. 防火墙中配置端口转发规则:将要从外网访问的高位端口(例如65222)转发到上面映射的端口65111

重启容器

此时即可通过https://subdomain.domain.com:65222来访问对应的内网服务

`,10),e=[o];function c(t,r,E,y,i,d){return n(),a("div",null,e)}const h=s(p,[["render",c]]);export{F as __pageData,h as default}; diff --git "a/assets/tinker_network_\344\275\277\347\224\250https\350\256\277\351\227\256\345\206\205\347\275\221\346\234\215\345\212\241.md.43b6af56.lean.js" "b/assets/tinker_network_\344\275\277\347\224\250https\350\256\277\351\227\256\345\206\205\347\275\221\346\234\215\345\212\241.md.43b6af56.lean.js" new file mode 100644 index 000000000..c5340599b --- /dev/null +++ "b/assets/tinker_network_\344\275\277\347\224\250https\350\256\277\351\227\256\345\206\205\347\275\221\346\234\215\345\212\241.md.43b6af56.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const F=JSON.parse('{"title":"使用https访问内网服务","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/network/使用https访问内网服务.md","filePath":"tinker/network/使用https访问内网服务.md","lastUpdated":1708510102000}'),p={name:"tinker/network/使用https访问内网服务.md"},o=l("",10),e=[o];function c(t,r,E,y,i,d){return n(),a("div",null,e)}const h=s(p,[["render",c]]);export{F as __pageData,h as default}; diff --git "a/assets/tinker_network_\345\261\261\347\211\271ups\351\205\215\345\220\210nut\345\256\236\347\216\260\346\226\255\347\224\265\345\256\211\345\205\250\345\205\263\346\234\272.md.a93a1b0f.js" "b/assets/tinker_network_\345\261\261\347\211\271ups\351\205\215\345\220\210nut\345\256\236\347\216\260\346\226\255\347\224\265\345\256\211\345\205\250\345\205\263\346\234\272.md.a93a1b0f.js" new file mode 100644 index 000000000..0ac525c25 --- /dev/null +++ "b/assets/tinker_network_\345\261\261\347\211\271ups\351\205\215\345\220\210nut\345\256\236\347\216\260\346\226\255\347\224\265\345\256\211\345\205\250\345\205\263\346\234\272.md.a93a1b0f.js" @@ -0,0 +1,55 @@ +import{_ as s,o as a,c as n,Q as p}from"./chunks/framework.dc1f4608.js";const E=JSON.parse('{"title":"山特ups配合nut实现断电安全关机","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/network/山特ups配合nut实现断电安全关机.md","filePath":"tinker/network/山特ups配合nut实现断电安全关机.md","lastUpdated":1708510102000}'),e={name:"tinker/network/山特ups配合nut实现断电安全关机.md"},l=p(`

山特ups配合nut实现断电安全关机

背景

搞了7*24小时服务器之后经历了两次突然断电,每次重启磁盘检查都要卡很久,夏天一到用电量骤升,市电断电和跳闸的几率都增加了。万一多来几次突然断电,磁盘阵列可能要挂了,更关键的是数据无价啊,ups还是少不了。

选择

因为不是成品nas系统,想实现自动关机得依靠linux上已有的软件。而我对apcupsd 这款软件有所耳闻,所以第一选择就去看了新款的apc bk650m2-ch ,快下单了才得知新款不支持apcupsd,然后就听网友的建议看了山特的box600box850 ,山特这个型号有两排插座,一排防雷+不断电,一排是防雷,还省了了插排钱,自带的usb通讯端口可以通过nut 软件进行管理,实现自动关机以及自定义脚本执行等功能,虽然电池容量小了点,但是能省就省吧,最后下单了box600。

nut安装及配置

ups机器本身没什么可讲的,把附带的rj45接ups,usb线接主机,再把主机电源插在ups的不断电插口上,接着给ups通电即可。主要介绍下nut的使用和配置。

安装nut

apt install nut

配置驱动

首先可以用lsusb命令查看是否接入了ups,能看到ups即可

image-20220524232436626

然后编辑ups配置文件vim /etc/nut/ups.conf,增加配置如下

txt
maxretry = 3
+[santak]
+        driver = usbhid-ups
+        port = auto
+        desc = "my ups"
maxretry = 3
+[santak]
+        driver = usbhid-ups
+        port = auto
+        desc = "my ups"

santak是ups的设备名,可以自定义,后续有些命令这个设备名还会用到

配置nut服务

新建ups用户

vim /etc/nut/upsd.users新增配置

txt
[ups]
+        password = xxx
+        upsmon master
[ups]
+        password = xxx
+        upsmon master

ups为用户名,xxx为密码,upsmon master为运行模式

配置权限

shell
chown root:nut /etc/nut/upsd.conf /etc/nut/upsd.users
+chmod 0640 /etc/nut/upsd.conf /etc/nut/upsd.users
chown root:nut /etc/nut/upsd.conf /etc/nut/upsd.users
+chmod 0640 /etc/nut/upsd.conf /etc/nut/upsd.users

启动nut服务

vim /etc/nut/nut.conf修改模式为单机

txt
MODE=standalone
MODE=standalone

启动upsd服务

shell
/sbin/upsd
/sbin/upsd

查看ups信息

查看全部

shell
/bin/upsc santak@localhost # 这里的santak就是上面的设备名
/bin/upsc santak@localhost # 这里的santak就是上面的设备名

查看某个信息在后面接信息类别就行,例如查看电量

shell
/bin/upsc santak@127.0.0.1 battery.charge
+
+Init SSL without certificate database
+100
/bin/upsc santak@127.0.0.1 battery.charge
+
+Init SSL without certificate database
+100

设置自动关机

nut服务会在UPS发送LOWBATT时通知机器关机,触发时机默认为ups电量剩余20% 。我们需要添加upsmon配置vim /etc/nut/upsmon.conf

MONITOR监视器部分添加配置

txt
MONITOR santak@localhost 1 ups xxx master
+
+# MONITOR 设备名@ip 1 用户名 密码 节点
MONITOR santak@localhost 1 ups xxx master
+
+# MONITOR 设备名@ip 1 用户名 密码 节点

授权

shell
chown root:nut /etc/nut/upsmon.conf
+chmod 0640 /etc/nut/upsmon.conf
chown root:nut /etc/nut/upsmon.conf
+chmod 0640 /etc/nut/upsmon.conf

启动upsmon

shell
/sbin/upsmon
/sbin/upsmon

自定义脚本

实际上配置完上面的内容已经可以实现断电时安全关机了,但喜欢折腾的还可以自定义触发事件的脚本

修改upsmon配置

vim /etc/nut/upsmon.conf添加内容

txt
NOTIFYCMD /sbin/upssched
NOTIFYCMD /sbin/upssched

这个配置的作用是发生事件是运行upssched程序

设置触发条件,三个动作分别是记录日志+通知所有用户发生了事件+执行notifycmd,也就是/sbin/upssched

txt
NOTIFYFLAG ONBATT SYSLOG+WALL+EXEC
NOTIFYFLAG ONBATT SYSLOG+WALL+EXEC

配置upssched

vim /etc/nut/upssched.conf编辑内容

txt
CMDSCRIPT /usr/local/bin/upssched
+PIPEFN /var/run/nut/upssched/upssched.pipe
+LOCKFN /var/run/nut/upssched/upssched.lock
+AT ONBATT * START-TIMER power-off 10
+AT ONLINE * CANCEL-TIMER power-off
CMDSCRIPT /usr/local/bin/upssched
+PIPEFN /var/run/nut/upssched/upssched.pipe
+LOCKFN /var/run/nut/upssched/upssched.lock
+AT ONBATT * START-TIMER power-off 10
+AT ONLINE * CANCEL-TIMER power-off

这段配置通过CMDSCRIPT指定了发生事件时需要执行的脚本,这个脚本可以根据需求自定义,在断电时(ONBATT/电池供电) 会启动一个10秒的timer,之后会执行power-off事件,这里涉及的文件nut用户都需要配置权限

编写脚本

这个可以自定义,例如发邮件等操作,这里展示最基本的脚本格式,例如power-off事件发生时,将断电了 信息写入指定文件,还可以调用ups的指令/sbin/upsmon -c fsd执行立刻关机操作 (FSD = "Forced Shutdown")

shell
#! /bin/sh
+
+case $1 in
+  power-off)
+    echo '$(date +\\%Y-\\%m-\\%d\\ \\%H:\\%M:\\%S) - 断电了 ' >> /var/log/nut/nut.log
+    #/sbin/upsmon -c fsd #立即通知关机
+    ;;
+  *)
+    logger -t upssched "Unrecognized command: $1"
+    ;;
+esac
#! /bin/sh
+
+case $1 in
+  power-off)
+    echo '$(date +\\%Y-\\%m-\\%d\\ \\%H:\\%M:\\%S) - 断电了 ' >> /var/log/nut/nut.log
+    #/sbin/upsmon -c fsd #立即通知关机
+    ;;
+  *)
+    logger -t upssched "Unrecognized command: $1"
+    ;;
+esac
`,55),o=[l];function t(c,r,i,d,u,h){return a(),n("div",null,o)}const g=s(e,[["render",t]]);export{E as __pageData,g as default}; diff --git "a/assets/tinker_network_\345\261\261\347\211\271ups\351\205\215\345\220\210nut\345\256\236\347\216\260\346\226\255\347\224\265\345\256\211\345\205\250\345\205\263\346\234\272.md.a93a1b0f.lean.js" "b/assets/tinker_network_\345\261\261\347\211\271ups\351\205\215\345\220\210nut\345\256\236\347\216\260\346\226\255\347\224\265\345\256\211\345\205\250\345\205\263\346\234\272.md.a93a1b0f.lean.js" new file mode 100644 index 000000000..c8e363c63 --- /dev/null +++ "b/assets/tinker_network_\345\261\261\347\211\271ups\351\205\215\345\220\210nut\345\256\236\347\216\260\346\226\255\347\224\265\345\256\211\345\205\250\345\205\263\346\234\272.md.a93a1b0f.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,Q as p}from"./chunks/framework.dc1f4608.js";const E=JSON.parse('{"title":"山特ups配合nut实现断电安全关机","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/network/山特ups配合nut实现断电安全关机.md","filePath":"tinker/network/山特ups配合nut实现断电安全关机.md","lastUpdated":1708510102000}'),e={name:"tinker/network/山特ups配合nut实现断电安全关机.md"},l=p("",55),o=[l];function t(c,r,i,d,u,h){return a(),n("div",null,o)}const g=s(e,[["render",t]]);export{E as __pageData,g as default}; diff --git "a/assets/tinker_network_\347\247\273\345\212\250\345\205\211\347\214\253\346\224\271\346\241\245\346\216\245\346\250\241\345\274\217.md.dd786d69.js" "b/assets/tinker_network_\347\247\273\345\212\250\345\205\211\347\214\253\346\224\271\346\241\245\346\216\245\346\250\241\345\274\217.md.dd786d69.js" new file mode 100644 index 000000000..f39b81941 --- /dev/null +++ "b/assets/tinker_network_\347\247\273\345\212\250\345\205\211\347\214\253\346\224\271\346\241\245\346\216\245\346\250\241\345\274\217.md.dd786d69.js" @@ -0,0 +1 @@ +import{_ as a,o as e,c as t,Q as o}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"移动光猫改桥接模式","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/network/移动光猫改桥接模式.md","filePath":"tinker/network/移动光猫改桥接模式.md","lastUpdated":1708510102000}'),i={name:"tinker/network/移动光猫改桥接模式.md"},l=o('

移动光猫改桥接模式

坐标广东,移动千兆宽带,服务商的光猫是带路由功能的,但是性能极差,且跑不满带宽。然后我就在公司福利商城买了个小米的A4路由器千兆版,但是拨号还是光猫,然后就动手折腾了下,光猫改桥接模式,拨号功能改为路由器执行。这样把光猫和路由器指责剥离开来,光猫就负责光转电,路由器则进行拨号和提供Wi-Fi功能。

具体操作

拿到光猫后台的超级管理员账户密码

从网上找到的:

账号:CMCCAdmin

密码:aDm8H%MdA

这个可能各地不同,如果不行就要找宽带安装师傅或者移动问了。

用管理员登录后台修改配置

  1. 选择网络tab页,在宽带设置中找到名字带有INTERNET的那个连接,这个链接应该是路由模式,把使能单选去掉,把选中的lan口也去掉, 记录下这个通道的vlanid备用,然后点击修改保存,这时候就把拨号禁用了。
  2. 新建一个连接,选择桥模式即桥接模式,勾选使能单选,勾选原来连接选中的lan口,填上刚才记录的vlanid,修改保存。

登录路由器后台修改配置

  1. 将路由上网模式改为PPPoE拨号模式,输入自己的宽带账号密码,移动宽带账号为手机号@139.gd,密码如果不知道可以去移动app上修改宽带密码,输入后点击保存,路由器应该就可以可以进行拨号了

TIP

网络上有些教程说的是直接删掉光猫原有的那个路由模式连接配置,千万别这么干,一定要按照我这种方案取消使能和lan口的操作。这样只是相当于把那个连接禁用了,并不会删除,万一自己配置路由拨号不顺利,还可以重新启用原有的连接进行上网。如果删了,自己又没配置好,那就芭比Q了。

',13),r=[l];function n(d,c,s,_,h,p){return e(),t("div",null,r)}const k=a(i,[["render",n]]);export{u as __pageData,k as default}; diff --git "a/assets/tinker_network_\347\247\273\345\212\250\345\205\211\347\214\253\346\224\271\346\241\245\346\216\245\346\250\241\345\274\217.md.dd786d69.lean.js" "b/assets/tinker_network_\347\247\273\345\212\250\345\205\211\347\214\253\346\224\271\346\241\245\346\216\245\346\250\241\345\274\217.md.dd786d69.lean.js" new file mode 100644 index 000000000..3e64447a3 --- /dev/null +++ "b/assets/tinker_network_\347\247\273\345\212\250\345\205\211\347\214\253\346\224\271\346\241\245\346\216\245\346\250\241\345\274\217.md.dd786d69.lean.js" @@ -0,0 +1 @@ +import{_ as a,o as e,c as t,Q as o}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"移动光猫改桥接模式","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/network/移动光猫改桥接模式.md","filePath":"tinker/network/移动光猫改桥接模式.md","lastUpdated":1708510102000}'),i={name:"tinker/network/移动光猫改桥接模式.md"},l=o("",13),r=[l];function n(d,c,s,_,h,p){return e(),t("div",null,r)}const k=a(i,[["render",n]]);export{u as __pageData,k as default}; diff --git "a/assets/tinker_vm_PVE\345\274\202\345\270\270\345\205\263\346\234\272\345\220\216\347\243\201\347\233\230\346\243\200\346\237\245\345\244\204\347\220\206.md.ad24a8cd.js" "b/assets/tinker_vm_PVE\345\274\202\345\270\270\345\205\263\346\234\272\345\220\216\347\243\201\347\233\230\346\243\200\346\237\245\345\244\204\347\220\206.md.ad24a8cd.js" new file mode 100644 index 000000000..deb9cfe82 --- /dev/null +++ "b/assets/tinker_vm_PVE\345\274\202\345\270\270\345\205\263\346\234\272\345\220\216\347\243\201\347\233\230\346\243\200\346\237\245\345\244\204\347\220\206.md.ad24a8cd.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as e,Q as o}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"PVE虚拟机异常关机后磁盘检查处理","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/vm/PVE异常关机后磁盘检查处理.md","filePath":"tinker/vm/PVE异常关机后磁盘检查处理.md","lastUpdated":1708510102000}'),n={name:"tinker/vm/PVE异常关机后磁盘检查处理.md"},l=o('

PVE虚拟机异常关机后磁盘检查处理

背景

pve虚拟机因为断电异常关机后,会出现重启无法进入系统的问题,并提示磁盘检查相关的异常,例如我碰到的提示

shell
The root filesystem on /dev/mapper/pve-root requires a manual fsck
The root filesystem on /dev/mapper/pve-root requires a manual fsck

处理

手动执行fsck命令fsck -y /dev/mapper/pve-root即可

',6),p=[l];function t(r,c,E,i,d,y){return a(),e("div",null,p)}const F=s(n,[["render",t]]);export{h as __pageData,F as default}; diff --git "a/assets/tinker_vm_PVE\345\274\202\345\270\270\345\205\263\346\234\272\345\220\216\347\243\201\347\233\230\346\243\200\346\237\245\345\244\204\347\220\206.md.ad24a8cd.lean.js" "b/assets/tinker_vm_PVE\345\274\202\345\270\270\345\205\263\346\234\272\345\220\216\347\243\201\347\233\230\346\243\200\346\237\245\345\244\204\347\220\206.md.ad24a8cd.lean.js" new file mode 100644 index 000000000..c730490bc --- /dev/null +++ "b/assets/tinker_vm_PVE\345\274\202\345\270\270\345\205\263\346\234\272\345\220\216\347\243\201\347\233\230\346\243\200\346\237\245\345\244\204\347\220\206.md.ad24a8cd.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as e,Q as o}from"./chunks/framework.dc1f4608.js";const h=JSON.parse('{"title":"PVE虚拟机异常关机后磁盘检查处理","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/vm/PVE异常关机后磁盘检查处理.md","filePath":"tinker/vm/PVE异常关机后磁盘检查处理.md","lastUpdated":1708510102000}'),n={name:"tinker/vm/PVE异常关机后磁盘检查处理.md"},l=o("",6),p=[l];function t(r,c,E,i,d,y){return a(),e("div",null,p)}const F=s(n,[["render",t]]);export{h as __pageData,F as default}; diff --git "a/assets/tinker_vm_VMWare\350\231\232\346\213\237\346\234\272\347\232\204\345\207\240\347\247\215\347\275\221\347\273\234\350\277\236\346\216\245\346\250\241\345\274\217.md.86cddc5f.js" "b/assets/tinker_vm_VMWare\350\231\232\346\213\237\346\234\272\347\232\204\345\207\240\347\247\215\347\275\221\347\273\234\350\277\236\346\216\245\346\250\241\345\274\217.md.86cddc5f.js" new file mode 100644 index 000000000..d32b024c4 --- /dev/null +++ "b/assets/tinker_vm_VMWare\350\231\232\346\213\237\346\234\272\347\232\204\345\207\240\347\247\215\347\275\221\347\273\234\350\277\236\346\216\245\346\250\241\345\274\217.md.86cddc5f.js" @@ -0,0 +1 @@ +import{_ as a,o as e,c as t,Q as r}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"VMWare虚拟机的几种网络连接模式","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/vm/VMWare虚拟机的几种网络连接模式.md","filePath":"tinker/vm/VMWare虚拟机的几种网络连接模式.md","lastUpdated":1708510102000}'),i={name:"tinker/vm/VMWare虚拟机的几种网络连接模式.md"},o=r('

VMWare虚拟机的几种网络连接模式

简单结论

  • 桥接模式:虚拟网络内的虚拟机都可以互相访问且能与物理机及外网设备访问,相当于一台独立的主机
  • NAT模式:外网设备都无法访问虚拟机,但是虚拟机可以访问
  • 仅主机模式:虚拟机无法访问外网,只能和宿主机通信

安装完VMWare后,会自动生成两个虚拟网卡:

  • VMnet1:host网卡,用于host方式连接网络
  • VMnet8:NAT网卡,用于NAT方式连接网络,ip地址是随机生成的

区别

桥接模式

桥接模式分为两种模式:

  • 直接把虚拟机的网卡接到物理网络,这种方法是虚拟机的网卡直接与物理机网卡进行通信
    • 不推荐,有时候可能虚拟机无法连接到互联网
  • 选择特定虚拟网络,选择在虚拟网络编辑器中配置的桥接模式网卡,这种方法是通过一个虚拟网络进行桥接,相当于在虚拟网卡物理机网卡之间加了一个虚拟网络VMnet0,VMnet0可以选择桥接的网卡是有线网卡还是无线网卡,如果物理机使用无线网卡上网,选择了有线网卡,虚拟机就无法上网,一般选择自动,让VMnet0自动选择能上网的网卡。

桥接是虚拟机的网卡直接把数据包交给物理机的物理网卡进行处理,虚拟机必须有自己的ip、dns、网关信息

image-20220421232404355

NAT模式

NAT(Network Address Translation),网络地址转换,相当于在虚拟机和物理机之间添加了一个交换机,拥有NAT地址转换功能,能够自动把虚拟机的IP转换为与物理机在同一网段的IP。VMnet8是NAT模式,自带DHCP功能,能给虚拟机分配IP地址。能够实现虚拟机和物理机相互通信,虚拟机和外网通信,但是不能外网到虚拟机通信,如果想让虚拟机作为服务器不能选择该模式。

DHCP(动态主机配置协议)是一个局域网的网络协议。指的是由服务器控制一段IP地址范围,客户机登录服务器时就可以自动获得服务器分配的IP地址和子网掩码。

image-20220421232842082

仅主机模式

内部虚拟机连接到一个可提供 DHCP 功能的虚拟网卡VMnet1上去,VMnet1相当于一个交换机,将虚拟机发来的数据包转发给物理网卡,但是物理网卡不会将该数据包向外转发。所以仅主机模式只能用于虚拟机与虚拟机之间、虚拟机与物理机之间的通信。

image-20220421232923906

LAN区段

相当于模拟出一个交换机或者集线器出来,把不同虚拟机连接起来,与物理机不进行数据交流,与外网也不进行数据交流,构建一个独立的网络。没有 DHCP 功能,需要手工配置 IP 或者单独配置 DHCP 服务器。

image-20220421232954352

nat模式连不上网的解决

  1. vmware编辑-> 虚拟网络编辑器重建nat网络,把之前的删掉,新建的同样选择VMnet8
  2. 如果还不能启动,去windows服务里查看VMware DHCP ServiceVMware NAT ServiceVMware Workstation Server服务开启,如果处于停止状态则启动,此外,要把VMnet8的ipv4地址和dns设置为自动获取
',23),l=[o];function n(s,c,h,d,m,p){return e(),t("div",null,l)}const g=a(i,[["render",n]]);export{u as __pageData,g as default}; diff --git "a/assets/tinker_vm_VMWare\350\231\232\346\213\237\346\234\272\347\232\204\345\207\240\347\247\215\347\275\221\347\273\234\350\277\236\346\216\245\346\250\241\345\274\217.md.86cddc5f.lean.js" "b/assets/tinker_vm_VMWare\350\231\232\346\213\237\346\234\272\347\232\204\345\207\240\347\247\215\347\275\221\347\273\234\350\277\236\346\216\245\346\250\241\345\274\217.md.86cddc5f.lean.js" new file mode 100644 index 000000000..7c135cbc9 --- /dev/null +++ "b/assets/tinker_vm_VMWare\350\231\232\346\213\237\346\234\272\347\232\204\345\207\240\347\247\215\347\275\221\347\273\234\350\277\236\346\216\245\346\250\241\345\274\217.md.86cddc5f.lean.js" @@ -0,0 +1 @@ +import{_ as a,o as e,c as t,Q as r}from"./chunks/framework.dc1f4608.js";const u=JSON.parse('{"title":"VMWare虚拟机的几种网络连接模式","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/vm/VMWare虚拟机的几种网络连接模式.md","filePath":"tinker/vm/VMWare虚拟机的几种网络连接模式.md","lastUpdated":1708510102000}'),i={name:"tinker/vm/VMWare虚拟机的几种网络连接模式.md"},o=r("",23),l=[o];function n(s,c,h,d,m,p){return e(),t("div",null,l)}const g=a(i,[["render",n]]);export{u as __pageData,g as default}; diff --git "a/assets/tinker_vm_\345\256\211\350\243\205PVE\350\231\232\346\213\237\346\234\272\345\271\266\345\234\250PVE\345\256\211\350\243\205truenas.md.b6416835.js" "b/assets/tinker_vm_\345\256\211\350\243\205PVE\350\231\232\346\213\237\346\234\272\345\271\266\345\234\250PVE\345\256\211\350\243\205truenas.md.b6416835.js" new file mode 100644 index 000000000..806c49682 --- /dev/null +++ "b/assets/tinker_vm_\345\256\211\350\243\205PVE\350\231\232\346\213\237\346\234\272\345\271\266\345\234\250PVE\345\256\211\350\243\205truenas.md.b6416835.js" @@ -0,0 +1,13 @@ +import{_ as s,o as a,c as p,Q as l}from"./chunks/framework.dc1f4608.js";const g=JSON.parse('{"title":"安装PVE虚拟机并在PVE安装truenas","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/vm/安装PVE虚拟机并在PVE安装truenas.md","filePath":"tinker/vm/安装PVE虚拟机并在PVE安装truenas.md","lastUpdated":1708510102000}'),n={name:"tinker/vm/安装PVE虚拟机并在PVE安装truenas.md"},o=l(`

安装PVE虚拟机并在PVE安装truenas

安装PVE

制作系统安装盘

  1. 下载rufus
  2. 下载pve最新镜像
  3. rufus选择镜像并刷写到自己的U盘中

WARNING

一般的系统选择iso镜像模式写入,安装pve需要选择dd镜像模式写入

安装系统

等待刷写完毕按照正常U盘安装系统流程安装,注意ip网关等配置即可

PVE中安装Truenas Scale

上传iso镜像

官网下载truenas scale系统镜像,打开pve的配置地址,点击上传后选择下载好的镜像文件并上传

image-20220424104118496

创建虚拟机

image-20220424104248209

操作系统选择刚上传的镜像

image-20220424104316127

系统tab页默认即可,磁盘这里要注意总线/设备选择sata0,磁盘大小选择16即可,这里是分了一个虚拟的系统盘,不需要太大,因为truenas系统的系统引导盘和存储是分开的,分的太大在truenas中也无法用于存储

image-20220424104359163

cpu根据情况设置,内存最小8G,建议尽可能大,truenas scale官方建议最好16G,因为truenas的文件系统很依赖内存。

安装truenas

启动刚创建的虚拟机,在选择系统安装盘界面按空格选中刚分配的16G磁盘,选择ok即可

image-20220424104822972

然后输入root密码,安装完后选择3重启

image-20220424104905506

随后等待系统安装完毕

image-20220424104940291

输入9选择关机

给truenas分配存储盘

  1. 最简单的办法,直接添加虚拟硬盘

image-20220424105115934

总线这里序号会默认自增,存储选择磁盘,我这里是只有一个固态硬盘,磁盘大小根据实际情况选择即可,添加即完成了虚拟硬盘的添加。

image-20220424105147413

  1. 硬盘直通

    1)直通单个硬盘

    因为我这台测试机器只有一个固态硬盘,pve系统也装在这上面,所以无法用硬盘的直通,如果有多个机械盘,想直通给truenas,可以使用以下命令

    shell
    ls -l /dev/disk/by-id # 查看硬盘名称
    ls -l /dev/disk/by-id # 查看硬盘名称

    image-20220424105554763

    这里sda对应就是我的唯一一个硬盘,后面的sda1、2、3都是分区,可以忽略,记录下磁盘名称ata-WDC_WD10EZEX-08WN4A0_WD-WCC6Y6EDC1NT,执行命令进行直通

    shell
    qm set 100 -sata1 /dev/disk/by-id ata-WDC_WD10EZEX-08WN4A0_WD-WCC6Y6EDC1NT
    +---
    +返回
    +update VM 100: -sata1 /dev/disk/by-id ata-WDC_WD10EZEX-08WN4A0_WD-WCC6Y6EDC1NT
    qm set 100 -sata1 /dev/disk/by-id ata-WDC_WD10EZEX-08WN4A0_WD-WCC6Y6EDC1NT
    +---
    +返回
    +update VM 100: -sata1 /dev/disk/by-id ata-WDC_WD10EZEX-08WN4A0_WD-WCC6Y6EDC1NT

    看到这个返回信息即为直通成功,回到虚拟机下面可以看到已经新增了一个sata硬盘

    命令拆解:

    shell
    qm set vm编号 -总线编号 磁盘路径
    +- vm编号为pve虚拟机中的编号,例如100,101,102等
    +- 总线编号为指定编号下要新增的硬盘总线编号,例如虚拟机下只有一个硬盘sata0,要新增的就是sata1,这里就要填sata1
    +- 磁盘路径 就是/dev/disk/by-id/ + 我们上一步保存的硬盘名称
    qm set vm编号 -总线编号 磁盘路径
    +- vm编号为pve虚拟机中的编号,例如100,101,102等
    +- 总线编号为指定编号下要新增的硬盘总线编号,例如虚拟机下只有一个硬盘sata0,要新增的就是sata1,这里就要填sata1
    +- 磁盘路径 就是/dev/disk/by-id/ + 我们上一步保存的硬盘名称

    2)添加PCI设备,直通sata控制器

    Proxmox VE(PVE)系统直通SATA Controller(SATA 控制器),会把整个sata总线全部直通过去,就是直接将南桥或者直接把北桥连接的sata总线直通,那么有些主板sata接口就会全部被直通。

    WARNING

    如果PVE系统安装在sata硬盘中,会导致PVE无法启动,所以在直通sata控制器前要确认自己的PVE系统安装位置,或者直接安装到NVMe硬盘中

image-20220425090446637

  1. 删除直通设备

qm set vm编号 -delete 设备名,例如要删除设备ID为100的虚拟机下直通的sata1

那就是qm set 100 -delete sata1

`,36),e=[o];function t(r,c,i,E,y,d){return a(),p("div",null,e)}const h=s(n,[["render",t]]);export{g as __pageData,h as default}; diff --git "a/assets/tinker_vm_\345\256\211\350\243\205PVE\350\231\232\346\213\237\346\234\272\345\271\266\345\234\250PVE\345\256\211\350\243\205truenas.md.b6416835.lean.js" "b/assets/tinker_vm_\345\256\211\350\243\205PVE\350\231\232\346\213\237\346\234\272\345\271\266\345\234\250PVE\345\256\211\350\243\205truenas.md.b6416835.lean.js" new file mode 100644 index 000000000..5cc93abc5 --- /dev/null +++ "b/assets/tinker_vm_\345\256\211\350\243\205PVE\350\231\232\346\213\237\346\234\272\345\271\266\345\234\250PVE\345\256\211\350\243\205truenas.md.b6416835.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as p,Q as l}from"./chunks/framework.dc1f4608.js";const g=JSON.parse('{"title":"安装PVE虚拟机并在PVE安装truenas","description":"","frontmatter":{},"headers":[],"relativePath":"tinker/vm/安装PVE虚拟机并在PVE安装truenas.md","filePath":"tinker/vm/安装PVE虚拟机并在PVE安装truenas.md","lastUpdated":1708510102000}'),n={name:"tinker/vm/安装PVE虚拟机并在PVE安装truenas.md"},o=l("",36),e=[o];function t(r,c,i,E,y,d){return a(),p("div",null,e)}const h=s(n,[["render",t]]);export{g as __pageData,h as default}; diff --git "a/docker/Dockerfile\350\257\255\346\263\225.html" "b/docker/Dockerfile\350\257\255\346\263\225.html" new file mode 100644 index 000000000..c69b1a46a --- /dev/null +++ "b/docker/Dockerfile\350\257\255\346\263\225.html" @@ -0,0 +1,125 @@ + + + + + + Dockerfile语法 | 故事 + + + + + + + + + + + + + + +
Skip to content

Dockerfile语法

Docker可以通过读取Dockerfile中的指令自动构建映像。Dockerfile是一个文本文档,其中包含用户在命令行上调用来组装镜像的所有命令。

https://docs.docker.com/engine/reference/builder/

语法

dockerfile
# 注释
+INSTRUCTION arguments
# 注释
+INSTRUCTION arguments

指令大小写不敏感,所以使用小写也不影响构建,但习惯上都将指令大写用于区分指令和参数。

Dockerfile必须以FROM指令开始(特殊情况:ARG指令)。

dockerfile
ARG  CODE_VERSION=latest
+FROM base:${CODE_VERSION}
+CMD  /code/run-app
ARG  CODE_VERSION=latest
+FROM base:${CODE_VERSION}
+CMD  /code/run-app

支持环境变量替换的指令

  • ADD
  • COPY
  • ENV
  • EXPOSE
  • FROM
  • LABEL
  • STOPSIGNAL
  • USER
  • VOLUME
  • WORKDIR
  • ONBUILD
dockerfile
FROM busybox
+ENV FOO=/bar
+WORKDIR ${FOO}   # WORKDIR /bar
+ADD . $FOO       # ADD . /bar
+COPY \$FOO /quux # COPY $FOO /quux
FROM busybox
+ENV FOO=/bar
+WORKDIR ${FOO}   # WORKDIR /bar
+ADD . $FOO       # ADD . /bar
+COPY \$FOO /quux # COPY $FOO /quux

基本模板

dockerfile
FROM image_name:version as alias1
+# FROM [--platform=<platform>] <image>[:<tag>] [AS <name>] 一个Dockerfile中FROM可以多次出现 用于构建多个镜像或者将一个构建阶段用作另一个构建阶段的依赖项
+MAINTAINER storyxc #维护文档的人,现在用LABEL xxx=xxx代替了
+
+RUN xxx 
+# RUN <command> shell格式,默认在linux上使用/bin/sh -c执行,windows上 cmd /S /C执行
+# RUN ['executable', 'param1', 'param2'] exec格式 
+
+# Deploy Biliup
+FROM python:3.9 as alias2
+
+ENV TZ=Asia/Shanghai
+# ENV指定环境变量
+
+EXPOSE 19159/tcp
+EXPOSE 19149/udp
+# 注明暴露的端口,只是声明作用,实际没有功能
+
+ADD hom* /mydir/
+# ADD指令用于向镜像内拷贝文件 目录 不仅能复制本机的文件,也能将远程URL的资源复制到镜像中
+# ADD [--chown=<user>:<group>] [--chmod=<perms>] [--checksum=<checksum>] <src>... <dest>
+# ADD [--chown=<user>:<group>] [--chmod=<perms>] ["<src>",... "<dest>"]
+# ADD可以识别压缩格式,把可解压的文件解压为目录,远程URL资源不会被解压
+
+VOLUME /opt
+# 指定创建一个具有指定名称的挂载点 可以使用JSON数组格式 或多个参数纯字符串
+
+COPY --from=alias1 /dir1 /dir2
+# COPY [--chown=<user>:<group>] [--chmod=<perms>] <src>... <dest>
+# COPY [--chown=<user>:<group>] [--chmod=<perms>] ["<src>",... "<dest>"]
+
+WORKDIR /opt
+# WORKDIR指定工作目录,如果没有则会被创建,如果WORKDIR后出现了相对路径,都是相对WORKDIR的
+
+CMD ["param1", "param2"]
+# CMD指令有三种格式
+# CMD ["executable","param1","param2"]  exec格式 最常用,不会有变量替换,需要变量替换需要使用shell格式或类似["shell", "-c", "e cho $HOME"]
+# CMD ["param1","param2"] 作为ENTRYPOINT的默认参数,如果是这个用法 那么ENTRYPOINT指令也要用JSON数组的格式书写
+# CMD command param1 param2 shell格式
+# 一个Dockerfile中只能有一个CMD指令,如果写了多个那么只有最后一个生效
+
+ENTRYPOINT ["biliup"]
+# ENTRYPOINT ["executable", "param1", "param2"]
+# ENTRYPOINT command param1 param2
FROM image_name:version as alias1
+# FROM [--platform=<platform>] <image>[:<tag>] [AS <name>] 一个Dockerfile中FROM可以多次出现 用于构建多个镜像或者将一个构建阶段用作另一个构建阶段的依赖项
+MAINTAINER storyxc #维护文档的人,现在用LABEL xxx=xxx代替了
+
+RUN xxx 
+# RUN <command> shell格式,默认在linux上使用/bin/sh -c执行,windows上 cmd /S /C执行
+# RUN ['executable', 'param1', 'param2'] exec格式 
+
+# Deploy Biliup
+FROM python:3.9 as alias2
+
+ENV TZ=Asia/Shanghai
+# ENV指定环境变量
+
+EXPOSE 19159/tcp
+EXPOSE 19149/udp
+# 注明暴露的端口,只是声明作用,实际没有功能
+
+ADD hom* /mydir/
+# ADD指令用于向镜像内拷贝文件 目录 不仅能复制本机的文件,也能将远程URL的资源复制到镜像中
+# ADD [--chown=<user>:<group>] [--chmod=<perms>] [--checksum=<checksum>] <src>... <dest>
+# ADD [--chown=<user>:<group>] [--chmod=<perms>] ["<src>",... "<dest>"]
+# ADD可以识别压缩格式,把可解压的文件解压为目录,远程URL资源不会被解压
+
+VOLUME /opt
+# 指定创建一个具有指定名称的挂载点 可以使用JSON数组格式 或多个参数纯字符串
+
+COPY --from=alias1 /dir1 /dir2
+# COPY [--chown=<user>:<group>] [--chmod=<perms>] <src>... <dest>
+# COPY [--chown=<user>:<group>] [--chmod=<perms>] ["<src>",... "<dest>"]
+
+WORKDIR /opt
+# WORKDIR指定工作目录,如果没有则会被创建,如果WORKDIR后出现了相对路径,都是相对WORKDIR的
+
+CMD ["param1", "param2"]
+# CMD指令有三种格式
+# CMD ["executable","param1","param2"]  exec格式 最常用,不会有变量替换,需要变量替换需要使用shell格式或类似["shell", "-c", "e cho $HOME"]
+# CMD ["param1","param2"] 作为ENTRYPOINT的默认参数,如果是这个用法 那么ENTRYPOINT指令也要用JSON数组的格式书写
+# CMD command param1 param2 shell格式
+# 一个Dockerfile中只能有一个CMD指令,如果写了多个那么只有最后一个生效
+
+ENTRYPOINT ["biliup"]
+# ENTRYPOINT ["executable", "param1", "param2"]
+# ENTRYPOINT command param1 param2

image-20230324223443650

+ + + + \ No newline at end of file diff --git "a/docker/docker-compose\350\257\255\346\263\225.html" "b/docker/docker-compose\350\257\255\346\263\225.html" new file mode 100644 index 000000000..32e2a2d1d --- /dev/null +++ "b/docker/docker-compose\350\257\255\346\263\225.html" @@ -0,0 +1,119 @@ + + + + + + docker-compose语法 | 故事 + + + + + + + + + + + + + + +
Skip to content

docker-compose语法

基础模板

https://docs.docker.com/compose/compose-file/03-compose-file/

yml
version: "3.8" # version是compose文件格式版本号 需要和Docker Engine对应 https://docs.docker.com/compose/compose-file/compose-file-v3/
+
+services:
+  service1:
+    image: image_name:version  #指定镜像
+    container_name: service1       #容器名
+    environment: #指定环境变量	
+      - A=1
+      - B=2
+    restart: always            #重启策略
+    volumes: #数据卷挂载
+      - /etc/localtime:/etc/localtime:ro # 挂载宿主机文件
+      - data:/opt/data # 具名卷挂载
+    ports: #端口映射配置
+      - "6610:6610"
+      - "6611:6611"
+    privileged: true # 将服务容器配置为以提升的权限运行
+    links: #定义到另一个服务中的容器的网络链接,可以在此容器直接用服务名访问另一个容器,links也有服务之间的隐式依赖关系,因此也决定了服务启动的顺序。
+      - service2
+    env_file:
+      - ./a.env
+      - ./b.env
+    devices:
+      - "/dev/ttyUSB0:/dev/ttyUSB0"
+      - "/dev/sda:/dev/xvda:rwm"
+    dns:
+      - 8.8.8.8
+  service2:
+    build: #构建配置
+      context: .               #指定包含Dockerfile的目录或一个git仓库的url
+      dockerfile: webapp.Dockerfile   #指定要使用的Dockerfile名称,默认找Dockerfile,和dockerfile_inline参数不能同时使用
+      dockerfile_inline: #直接在compose文件里写Dockerfile指令 和dockerfile参数不能同时使用
+        FROM xxx
+        RUN some command
+    container_name: service2
+    network_mode: "host"      #配置网络模式,none(禁用所有容器网络)/host(使用宿主接口)/service:{name}(只能访问指定服务)
+    networks: #指定容器连接的docker网络
+      - netA
+      - netB
+    depends_on: #依赖某个服务,决定了服务的启动和关闭顺序
+      - service3
+
+volumes:
+  data:
+    
+networks:
+  netA:
+  netB:
version: "3.8" # version是compose文件格式版本号 需要和Docker Engine对应 https://docs.docker.com/compose/compose-file/compose-file-v3/
+
+services:
+  service1:
+    image: image_name:version  #指定镜像
+    container_name: service1       #容器名
+    environment: #指定环境变量	
+      - A=1
+      - B=2
+    restart: always            #重启策略
+    volumes: #数据卷挂载
+      - /etc/localtime:/etc/localtime:ro # 挂载宿主机文件
+      - data:/opt/data # 具名卷挂载
+    ports: #端口映射配置
+      - "6610:6610"
+      - "6611:6611"
+    privileged: true # 将服务容器配置为以提升的权限运行
+    links: #定义到另一个服务中的容器的网络链接,可以在此容器直接用服务名访问另一个容器,links也有服务之间的隐式依赖关系,因此也决定了服务启动的顺序。
+      - service2
+    env_file:
+      - ./a.env
+      - ./b.env
+    devices:
+      - "/dev/ttyUSB0:/dev/ttyUSB0"
+      - "/dev/sda:/dev/xvda:rwm"
+    dns:
+      - 8.8.8.8
+  service2:
+    build: #构建配置
+      context: .               #指定包含Dockerfile的目录或一个git仓库的url
+      dockerfile: webapp.Dockerfile   #指定要使用的Dockerfile名称,默认找Dockerfile,和dockerfile_inline参数不能同时使用
+      dockerfile_inline: #直接在compose文件里写Dockerfile指令 和dockerfile参数不能同时使用
+        FROM xxx
+        RUN some command
+    container_name: service2
+    network_mode: "host"      #配置网络模式,none(禁用所有容器网络)/host(使用宿主接口)/service:{name}(只能访问指定服务)
+    networks: #指定容器连接的docker网络
+      - netA
+      - netB
+    depends_on: #依赖某个服务,决定了服务的启动和关闭顺序
+      - service3
+
+volumes:
+  data:
+    
+networks:
+  netA:
+  netB:
+ + + + \ No newline at end of file diff --git "a/docker/docker\347\275\221\347\273\234\344\271\213macvlan.html" "b/docker/docker\347\275\221\347\273\234\344\271\213macvlan.html" new file mode 100644 index 000000000..a7ab947b7 --- /dev/null +++ "b/docker/docker\347\275\221\347\273\234\344\271\213macvlan.html" @@ -0,0 +1,67 @@ + + + + + + macvlan | 故事 + + + + + + + + + + + + + + +
Skip to content

macvlan

在日常使用docker的时候,可以通过设置端口映射的方式,实现通过宿主机ip访问docker容器的目的。当然,也有办法可以实现给docker镜像设置单独的IP。这里就要用到macvlan技术。

macvlan 是 Linux kernel 支持的新特性,macvlan能将一块物理网卡虚拟成多块虚拟网卡,docker这种容器就可以通过虚拟网卡获取IP,利用IP进行独立上网,真正做到跟物理机完全一样的体验。

docker网络中的macvlan

docker中的macvlan 是 Docker中的一种网络驱动,它允许容器直接使用物理网络接口的 MAC 地址。这使得容器可以像物理设备一样存在于网络中,而不需要进行端口映射或NAT。

linux中开启macvlan

shell
// 加载 macvlan 模块
+modprobe macvlan
+// 查看是否已加载
+lsmod | grep macvlan
+// 如果有有输出证明已经加载macvlan模块,否则说明不支持
+// macvlan   24576  0
// 加载 macvlan 模块
+modprobe macvlan
+// 查看是否已加载
+lsmod | grep macvlan
+// 如果有有输出证明已经加载macvlan模块,否则说明不支持
+// macvlan   24576  0

docker创建macvlan网络

shell
docker network create -d macvlan \
+  --subnet=192.168.2.0/24 \
+  --gateway=192.168.2.1 \
+  -o parent=enp3s0 \
+  vlan
docker network create -d macvlan \
+  --subnet=192.168.2.0/24 \
+  --gateway=192.168.2.1 \
+  -o parent=enp3s0 \
+  vlan
  • subnet:与物理网络相同的子网。
  • gateway:与物理网络相同的网关。
  • -o parent:宿主机上的物理网络接口,如 eth0。

创建容器并连接到macvlan

shell
docker run -d --name 容器名 --net vlan --ip=指定的IP地址 镜像名
docker run -d --name 容器名 --net vlan --ip=指定的IP地址 镜像名

例如

shell
docker run -it --name busybox --net vlan --ip=192.168.2.166 busybox /bin/sh
docker run -it --name busybox --net vlan --ip=192.168.2.166 busybox /bin/sh

docker-compose配置macvlan

yaml
version: "3"
+
+services:
+  busybox:
+    ...
+    networks:
+      vlan:
+        ipv4_address: 192.168.2.166
+
+
+networks:
+  vlan:
+    external: true
version: "3"
+
+services:
+  busybox:
+    ...
+    networks:
+      vlan:
+        ipv4_address: 192.168.2.166
+
+
+networks:
+  vlan:
+    external: true

容器启动后就可以通过指定的固定ip来访问docker容器中的服务了

+ + + + \ No newline at end of file diff --git a/docker/index.html b/docker/index.html new file mode 100644 index 000000000..09bd40f1b --- /dev/null +++ b/docker/index.html @@ -0,0 +1,25 @@ + + + + + + Docker | 故事 + + + + + + + + + + + + + + +
Skip to content

Docker

Docker is an open platform for developing, shipping, and running applications. Docker enables you to separate your applications from your infrastructure so you can deliver software quickly. With Docker, you can manage your infrastructure in the same ways you manage your applications. By taking advantage of Docker’s methodologies for shipping, testing, and deploying code quickly, you can significantly reduce the delay between writing code and running it in production.

+ + + + \ No newline at end of file diff --git "a/docker/\345\270\270\347\224\250\346\214\207\344\273\244.html" "b/docker/\345\270\270\347\224\250\346\214\207\344\273\244.html" new file mode 100644 index 000000000..c8359115e --- /dev/null +++ "b/docker/\345\270\270\347\224\250\346\214\207\344\273\244.html" @@ -0,0 +1,35 @@ + + + + + + 常用指令 | 故事 + + + + + + + + + + + + + + +
Skip to content

常用指令

安装

shell
curl -fsSL https://get.docker.com | sh
curl -fsSL https://get.docker.com | sh

Dockerfile给ubuntu换源

dockerfile
RUN sed -i s@/archive.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list
RUN sed -i s@/archive.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list

macOS运行容器时同步宿主机时间

shell
-e TZ=`ls -la /etc/localtime | cut -d/ -f8-9`
-e TZ=`ls -la /etc/localtime | cut -d/ -f8-9`

MacOS中容器访问宿主机

可以用host.docker.internal来访问宿主机

https://docs.docker.com/desktop/networking/#use-cases-and-workarounds

构建跨平台镜像

docker buildx

shell
# 创建builder
+docker buildx create --name cross-platform-builder --driver docker-container --use
+# 执行构建
+docker buildx build --platform linux/amd64,linux/arm64 -t 镜像名:tag [-o type=registry | --push] . 
+# 查看推送到远程的镜像信息
+docker buildx imagetools inspect 镜像名:tag
# 创建builder
+docker buildx create --name cross-platform-builder --driver docker-container --use
+# 执行构建
+docker buildx build --platform linux/amd64,linux/arm64 -t 镜像名:tag [-o type=registry | --push] . 
+# 查看推送到远程的镜像信息
+docker buildx imagetools inspect 镜像名:tag

buildx 实例通过两种方式来执行构建任务,两种执行方式被称为使用不同的「驱动」:

  • docker 驱动:使用 Docker 服务程序中集成的 BuildKit 库执行构建。
  • docker-container 驱动:启动一个包含 BuildKit 的容器并在容器中执行构建。

docker 驱动无法使用一小部分 buildx 的特性(如在一次运行中同时构建多个平台镜像),此外在镜像的默认输出格式上也有所区别:docker 驱动默认将构建结果以 Docker 镜像格式直接输出到 docker 的镜像目录(通常是 /var/lib/overlay2),之后执行 docker images 命令可以列出所输出的镜像;而 docker container 则需要通过 --output 选项指定输出格式为镜像或其他格式。

docker buildx build 支持丰富的输出行为,通过--output=[PATH,-,type=TYPE[,KEY=VALUE] 选项可以指定构建结果的输出类型和路径等,常用的输出类型有以下几种:

  • local:构建结果将以文件系统格式写入 dest 指定的本地路径, 如 --output type=local,dest=./output
  • tar:构建结果将在打包后写入 dest 指定的本地路径。
  • oci:构建结果以 OCI 标准镜像格式写入 dest 指定的本地路径。
  • docker:构建结果以 Docker 标准镜像格式写入 dest 指定的本地路径或加载到 docker 的镜像库中。同时指定多个目标平台时无法使用该选项。
  • image:以镜像或者镜像列表输出,并支持 push=true 选项直接推送到远程仓库,同时指定多个目标平台时可使用该选项。
  • registry:type=image,push=true 的精简表示。

https://waynerv.com/posts/building-multi-architecture-images-with-docker-buildx/

+ + + + \ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..beef6be1f7def35441822f535c46de4cf27a43f1 GIT binary patch literal 15406 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 + + + + + CSS | 故事 + + + + + + + + + + + + + + +
Skip to content

CSS

CSS特性

  • 继承性
  • 层叠性
  • 优先级:继承 < 通配符选择器 < 标签选择器 < 类选择器 < id选择器 < 行内样式 < !important
    • !important写在属性值后面,分号前面
    • !important不能提升继承的优先级,只要是继承优先级最低
    • 复合选择器需要根据权重叠加计算(行内样式个数,id选择器个数,类选择器个数,标签选择器个数)

书写顺序:浏览器执行效率更高

  1. 浮动 / display
  2. 盒子模型 margin border padding 宽高背景色
  3. 文字样式

选择器

  • 标签选择器 tagName { css }

  • 类选择器: .class { css }

  • id选择器: #id { css }

  • 通配符选择器: * { css }

  • 复合选择器

    • 后代选择器: 选择器1 选择器2 { css }
      • 后代包含所有:子、孙子。。
    • 子代选择器: 选择器1 > 选择器2 { css }
      • 只包括子
  • 属性选择器: 选择器[attribute=xxx]

  • 并集选择器: 选择器1,选择器2 { css }

  • 交集选择器: 选择器1选择器2 { css }

    • 如果有标签选择器,标签选择器必须写在最前面
  • 伪类选择器: 标签:伪类选择器 { css }

    • 伪类

      • 状态(hover/active..)
      • 结构(fisrt-child/last-child/nth-child()/nth-last-child())
    • 伪对象

      • 页面中非主体内容可以使用伪对象,由css模拟出标签效果

      • ::before::after

      • css
        .content::before {
        +  content: 'test1';
        +}
        +
        +.content::after {
        +  content: 'test2';
        +}
        .content::before {
        +  content: 'test1';
        +}
        +
        +.content::after {
        +  content: 'test2';
        +}
      • 默认是行内元素,content必须添加 否则伪对象不生效

字体和文本样式

字体样式

  • 字体大小:font-size

    • 取值:数字+px,Google Chrome默认16px
  • 字体粗细:font-weight

    • 关键字:normal/bold/bolder/lighter
    • 数字:100-900的整百数,正常400 加粗700
  • 字体样式:font-style

    • 正常:normal
    • 倾斜:italic
  • 字体类型:font-family

    • 从左往右查找,如果未安装,则显示下一个,如果都不支持则显示最后字体系列的默认字体,多个字体推荐引号包裹,最后一个字体系列不需要引号
  • 字体类型:属性连写

    • style weight size family
    • 只能省略前两个,省略相当于设置默认值
    • 如果同时使用单独和连写,要么把单独的写在连写下面,或者写在连写里面

文本样式

  • 文本缩进:text-indent

    • 取值:数字+px或者数字+em,推荐使用em(1em = 当前标签font-size的大小)
  • 文本水平对齐方式(内容对齐方式):text-align

    • left/center/right
    • 如果需要让文本水平居中,text-align属性给文本所在的标签设置
    • 能让哪些元素水平居中:文本、span、a、input、img,需要给元素的父元素设置居中
  • 文本修饰:text-decoration

    • underline 下划线
    • line-through 删除线
    • overline 上划线
    • none 无装饰线
  • 行高:line-heigh

    • 行高:上间距+文本高度+下间距
    • 控制两行文字之间的距离
    • 取值:数字+px / 倍数 (当前标签font-size的倍数)
    • 应用:
      • 让单行文本垂直居中可以设置line-height:文字父元素高度
      • 网页精准布局会设置line-height:1;可以取消上下间距
    • 行高和font连写的注意点:
      • 如果同时设置了行高和font连写,需要注意覆盖问题
      • font: style weight size/line-height family;

背景属性

背景色

  • 属性:backgroud-color

  • 取值:

    • 关键字
    • rgb
    • rgba
    • 十六进制
  • 默认是透明:rgba(0,0,0,0)、transparent

背景图

  • 属性:backgroud-image
  • backgroud-image: url('图片链接')
  • url可以省略引号
  • 背景图默认水平和垂直方向平铺

背景平铺

  • 属性:background-repeat
  • 取值:repeat、no-repeat、repeat-x、repeat-y

背景位置

  • background-position
  • 取值
    • 方位:
      • 水平:left、center、right
      • 垂直:top、center、bottom
    • 数字+px
      • 坐标系原点(0,0),图片左上角
      • x轴 水平向右(图片向左移动取负数、向右移动取正数)
      • y轴 垂直向下(图片向上移动取负数、向下移动取正数)
  • 方位和坐标可以混用,第一个值为水平,第二个值为垂直

背景属性连写

  • background: color image repeat position

元素显示模式

元素显示模式

  • 块级元素
    • 显示特点:独占一行,宽度默认是父元素宽度,高度默认由内容撑开,可以设置宽、高
    • 代表标签:div、p、h、ul、li、dl、dt、dd、form、header、nav、footer。。。
  • 行内元素
    • 显示特点:一行可以显示多个,宽度和高度默认由内容撑开,不能设置宽、高
    • 代表标签:a、span、b、u、i、s、strong、ins、em、del。。。
  • 行内块元素
    • 显示特点:一行可以显示多个,可以设置宽、高
    • 代表标签:input、textarea、button、select
    • 特殊:img标签有行内块特点,但是Chrome显示是inline

元素显示模式转换

css
span {
+  display: inline-block;
+}
+
+div {
+  display: inline;
+}
+
+button {
+  display: block;
+}
span {
+  display: inline-block;
+}
+
+div {
+  display: inline;
+}
+
+button {
+  display: block;
+}

盒子模型

页面中的每一个标签都可以看作一个“盒子”,通过盒子的视角更方便的进行布局。

每个盒子分别由:内容区域(content)、内边距区域(padding)、边框区域(border)、外边距区域(margin)构成。


  • 内容区域宽高:weight/height
    • 取值 数字+px
  • 边框:border
    • border:10px solid red;
    • border-top/bottom/left/right
  • 内边距:padding
    • 四方向顺序:上 右 下 左 (从上顺时针取值)

border和padding会撑大盒子,css属性box-sizing: border-box; 可以设置内减模式

  • 外边距:margin

    • 顺序和padding一样

    • 清除默认的内外边距

      • css
        * {
        +  margin: 0;
        +  padding: 0;
        +}
        * {
        +  margin: 0;
        +  padding: 0;
        +}
    • 版心居中

      • css
        #main {
        +  margin: 0 auto;
        +}
        #main {
        +  margin: 0 auto;
        +}

顺序: 宽高背景色-> 放内容 -> 调整位置 -> 调整文字细节

  • 外边距问题

  • 折叠现象:垂直布局的块级元素,上下的margin会被合并,取两者的最大值

  • 塌陷现象:互相嵌套的块级元素,子元素的margin-top会作用在父元素上

    • 解决:1.父元素设置border-top或者padding-top(分隔父子元素的margin-top) 2.父元素设置overflow: hidden;3.转换为行内块元素4.设置浮动
  • 行内元素的内外边距问题:如果想通过margin/padding改变行内元素的垂直(top/bottom)位置,无法生效

    • 解决:设置行高

浮动

标准流

  • 标准流:又称文档流,浏览器在渲染显示网页内容时默认采取的一套排版规则,规定了何种方式排列元素
    • 块级元素:从上往下,垂直布局,独占一行
    • 行内元素/行内块元素:从左往右,水平布局,空间不够自动折行

浮动作用

  • 早期:图文环绕
  • 现在:网页布局

块级元素转行内块时 换行会产生空格

浮动的规则

  • 向左浮动或者向右浮动直到自己的边界紧贴着包含块(一般是父元素)或者其他浮动元素的边界为止

  • 不能超出包含块,如果元素是向左(右)浮动,浮动元素的左(右)边界不能超出包含块的左(右)边界

  • 浮动元素不能层叠

    • 如果一个元素浮动,另一个浮动元素已经在那个位置了,后浮动的元素将紧贴着前一个浮动元素(左浮找左浮,右浮找右浮)
    • 如果水平方向剩余的空间不够显示浮动元素,浮动元素将向下移动,直到有充足的空间为止
  • 浮动元素会将行内级元素内容推出

    • 浮动元素不能与行内级内容层叠,行内级内容将会被浮动元素推出

    • 比如行内级元素inline-block元素、块级元素的文字内容

    • 图文环绕效果

  • 浮动只能左右浮动, 不能超出本行的高度

    • 行内级元素、inline-block元素浮动后,其顶部将与所在行的顶部对齐

浮动特点

  • 浮动元素会脱离标准流,在标准流中不占位置
    • 相当于从地面飘到了空中
  • 浮动元素比标准流高半个级别,可以覆盖标准流中的元素
  • 浮动找浮动,下一个浮动元素会在上一个浮动元素后面左右浮动
  • 浮动元素有特殊的显示效果
    • 一行可以显示多个
    • 可以设置宽高

浮动的元素不能通过text-align: center 或者margin: 0 auto居中

清除浮动

  • 含义:清除浮动带来的影响。如果子元素浮动,此时子元素不能撑开标准流的块级父元素

  • 原因:子元素浮动后脱离标准流 -> 不占位置

  • 目的:需要父元素有高度,不影响其他元素的布局

  • 方法:

    • 给父元素加高度

    • 额外标签:给父元素内容的最后加一个块级元素,给添加的块级元素设置clear: both;

    • 单伪元素:用伪元素代替额外标签

      css
      .clearfix::after {
      +  content: '';
      +  display: block;
      +  clear: both;
      +}
      +---
      +.clearfix::after {
      +  content: '';
      +  displat: block;
      +  clear: both;
      +  height: 0;
      +  visibility: hidden;
      +}
      .clearfix::after {
      +  content: '';
      +  display: block;
      +  clear: both;
      +}
      +---
      +.clearfix::after {
      +  content: '';
      +  displat: block;
      +  clear: both;
      +  height: 0;
      +  visibility: hidden;
      +}
    • 双伪元素:

      css
      .clearfix::before, /* 解决外边距塌陷问题 */ 
      +.clearfix::after {
      +  content: '';
      +  display: table;
      +}
      +.clearfix::after {
      +  clear: both;
      +}
      .clearfix::before, /* 解决外边距塌陷问题 */ 
      +.clearfix::after {
      +  content: '';
      +  display: table;
      +}
      +.clearfix::after {
      +  clear: both;
      +}
    • 父元素设置overflow: hidden

      BFC(Block Formatting Context)全称是块级格式化上下文,用于对块级元素排版,默认情况下只有根元素(body)一个块级上下文,但是如果一个块级元素设置了float:left,overflow:hidden或position:absolute样式,就会为这个块级元素生产一个独立的块级上下文,使这个块级元素内部的排版完全独立。

      作用:独立的块级上下文可以包裹浮动流,全部浮动子元素也不会引起容器高度塌陷,就是说包含块会把浮动元素的高度也计算在内,所以就不用清除浮动来撑起包含块的高度。

      那什么时候会触发 BFC 呢?常见的情况如下:

      • 根元素;

      • float的值不为none;

      • overflow的值为auto、scroll或hidden;

      • display的值为table-cell、table-caption和inline-block中的任何一个;

      • position的值不为relative和static。

定位

作用

  1. 可以让元素自由的摆放在网页的任意位置
  2. 一般用于盒子之间的层叠情况

常见应用场景

  • 可以解决盒子与盒子之间层叠问题
    • 定位后的元素层级最高,可以层叠在其他盒子上面
  • 可以让盒子始终固定带屏幕中的某个位置

使用

  • 设置定位方式

    • 属性名:position

    • 常见属性:

      • 静态定位:static

        • 元素处于标准流中,默认设置
      • 相对定位:relative,相对自己之前的位置进行移动

        • 需要配合方位属性实现移动
        • 相对自己原来的位置移动
        • 在页面中占位置(没有脱标)
        • 应用场景:1.配合绝对定位(子绝对 父相对) 2.用于小范围移动
      • 绝对定位:absolute,先找已经定位的父级(逐级查找),如果有这样的父级就以这个父级为参照定位,有父级但父级没有定位则以浏览器为参照定位

        • 页面中不占位置(脱标)

        • 改变标签的显示模式特点,具备行内块特点

        • 绝对定位的元素不能使用margin: 0 auto居中

          • css
            position: absolute;
            +left: 50%;
            +top: 50%;
            +transform: translate(-50%, -50%);
            position: absolute;
            +left: 50%;
            +top: 50%;
            +transform: translate(-50%, -50%);
      • 固定定位:fixed,相对于浏览器进行定位移动

        • 需要配合方位属性实现移动
        • 相对浏览器可视区域进行移动
        • 在页面中不占位置(脱标)具备行内块特点
        • 应用场景:让盒子固定在屏幕中某个位置
  • 设置偏移值

    • 偏移值分为两个方向,水平和垂直方向各选一个使用即可,水平以left为准,垂直以top为准
    • 选取原则一般是就近原则

元素层级问题

不同布局方式元素的层级关系

  • 标准流 < 浮动 < 定位

不同定位之间的层级关系

  • 相对、绝对、固定默认层级相同
  • 此时默认HTML中写在下面的元素层级更高,会覆盖上面的元素
  • z-index(配合定位才生效):整数,取值越大,层级越高,显示顺序越靠上,z-index默认为0

定位装饰

垂直对齐方式

  • 属性:vertical-align
  • 取值:baseline(默认,基线对齐)、top(顶部对齐)、middle(中部对齐)、bottom(底部对齐)

浏览器遇到行内和行内块标签当作文字处理,默认文字按照基线对齐

光标类型

  • 属性:cursor
  • 常见取值:
    • default(默认,通常是箭头)
    • pointer(小手,提示可以点击)
    • text(工字型,提示可以选择)
    • move(十字光标,提示可以移动)

边框圆角

  • 属性:border-radius

  • 常见取值:数字+px、百分比

  • 赋值:从左上角开始,顺时针赋值,没有赋值的看对角

正圆:正方形盒子,border-radius: 50%

胶囊按钮:长方形盒子,border-radius: 高度的一半

百分比表示的是圆角半径的大小为盒子较小边长的一半,例如盒子宽200px,高100px,border-radius: 50% 50%; 则这两个50%都是相对于100px的。

溢出部分显示效果

  • 属性:overflow
  • 常见取值:
    • visible 默认值,可见
    • hidden 隐藏
    • scroll 始终显示滚动条
    • auto 自动显示、隐藏滚动条

元素本身隐藏

  • 属性:
    • visibility: hidden(占位隐藏)
    • display: none(不占位隐藏)

元素整体透明度

  • 属性:opacity
  • 取值:0-1之间数字,1完全不透明,0完全透明
  • 特点:opacity会让元素整体透明,包括里面的文字、子元素等

补充

精灵图

CSS精灵图是一种将多个小的背景图片合并到一张大图中的技术,通过CSS的background-position属性控制显示不同的小图片,达到减少HTTP请求、减小页面大小、提高加载速度的效果。

CSS精灵图可以将多个小背景图像合并为一张大图,这样在页面上加载一次大图后,就可以通过background-position属性来控制显示不同的小图像,实现了将多个HTTP请求转化为一次请求,减小了网络延迟和服务器压力。同时,由于减少了HTTP请求和加载的内容大小,减小了页面的带宽消耗,加快了网页的加载速度,提高了用户的体验感受。

在web前端开发中,经常使用CSS精灵图技术,特别是在一些需要大量小icon的场合,如网站菜单栏、按钮、分页等等。

背景图片大小

  • 设置背景图片的大小:background-size: 宽 高

  • 取值

    • 数字+px
    • 百分比
    • contain:等比例缩放,直到不会超出盒子的最大
    • cover:等比例缩放,直到刚好填满整个盒子

background连写

backgound:color image repeat position/size

盒子阴影

  • 属性:box-shadow
  • 取值
    • h-shadow:必须,水平偏移量,允许负值
    • v-shadow:必须,垂直偏移量,允许负值
    • blur:可选,模糊度
    • spread:可选,阴影扩大
    • color:可选,阴影颜色
    • inset:可选,改为内部阴影

过渡

  • 属性:transition
  • 作用:让元素样式慢慢变化,常配合hover使用
  • 常见取值
    • 过渡属性:all(所有能过渡的属性都过渡)、具体属性名如width(只有width过渡)
    • 过渡时长: 数字+s
  • 注意点
    • 默认状态和hover状态样式不同才有过渡效果
    • transition属性需要给过渡的元素本身加
    • transition属性设置在不同状态中,效果不同
      • 给默认状态设置,鼠标移入移出都有效果
      • 给hover状态设置,只有移入有过渡效果

.box {

​ width: 200px;

​ height: 200px;

​ background-color: pink;

​ /transition: all 1s/

​ transition: width 1s, background-color 2s;

}

.box hover {

​ width: 600px;

​ background-color: red;

}

字体图标

  • 展示的是图标,实质是文字,用作处理简单的、颜色单一的图片

  • iconfont

  • <link rel="stylesheet" href="./iconfont.css"

  • <span class="iconfont icon-kuaijiezhifu"></span>

平面转换

平面转换(2D转换):改变盒子在平面内的形态,可以使用transform属性实现元素的位移、旋转、缩放等效果

位移

  • transform: translate(水平移动距离, 垂直移动距离)
  • 取值: 1.像素单位数值 2.百分比(参照为自身盒子尺寸)
  • 只给一个值代表x轴位移,单独设置:tranlateX() / translateY()

旋转

  • 语法:transform: rotate(度数 + deg); 正数:顺时针;负数:逆时针
  • transform-origin
    • 作用:改变转换的原点,默认的原点是盒子中心点
    • transform-origin: 原点水平位置 原点垂直位置
    • 取值:方位名词(left、top、right、bottom、center
    • 像素
    • 百分比

多重转换

transform: translate() rotate()

rotate会改变坐标轴向,位移方向会受影响

多重转换如果涉及旋转,旋转往最后写

缩放

  • scale:改变元素尺寸
  • 语法:transform: scale(x轴缩放倍数, y轴缩放倍数)
  • 一般transform: scale(缩放倍数),等比缩放
  • scale大于1表示放大,小于1表示缩小

渐变

linear-gradient

空间转换

在空间内位移、旋转、缩放等效果

空间位移

transform: tranlate3d(x,y,z)

单个坐标轴transform: translat[X|Y|Z]

透视

使用perspective(视距)属性实现透视效果,添加给父级元素,取值一般800-1200

空间旋转

transform: rotateX()

transform: rotateY()

transform: rotateZ()

transform: rotate3d(x,y,z,角度度数):用来设置自定义旋转轴的位置及旋转角度,xyz取值为0-1之间的数字

立体呈现

使用transform-style: perserve-3d呈现立体图形

  • 父元素添加transform-style: perserve-3d使子集元素处于3d空间

  • 默认值flat,表示子元素处于2D平面

空间缩放

transform: scaleX()

transform: scaleY()

transform: scaleY()

transform: scale3d(x,y,z)

动画

使用animation实现多个状态间的变化过程,动画过程可控(重复播放、最终画面、是否暂停)

实现步骤

  1. 定义动画

    css
    @keyframes 动画名称 {
    +  from {}
    +  to {}
    +}
    +
    +@keyframes 动画名称 {
    +  /* 百分比指的是动画总时长的占比 */
    +  0% {}
    +  10% {}
    +  15% {}
    +  100% {}
    +}
    @keyframes 动画名称 {
    +  from {}
    +  to {}
    +}
    +
    +@keyframes 动画名称 {
    +  /* 百分比指的是动画总时长的占比 */
    +  0% {}
    +  10% {}
    +  15% {}
    +  100% {}
    +}
  2. 使用动画: animation: 动画名称 动画花费时长

动画属性

animation: 动画名称 动画时长 速度曲线 延迟时间 重复次数 动画方向 执行完毕时状态 播放状态

  • 动画名称和动画时长必须赋值
  • 取值不分先后顺序
  • 如果有2个时间,第一个表示动画时长,第二个表示延迟时间
  • animation-name:
  • animation-duration
  • animation-timing-function:ease;ease-in;ease-out;ease-in-out;linear;step;
  • animation-delay
  • animation-iteration-count: 1,2,..infinite;
  • animation-direction: normal;reverse;alternate;alternate-reverse;
  • animation-fill-mode: forward;backward;both;
  • animation-play-state: pause;running;
  • animation-

多组动画

animation: 动画1,动画2...,动画n;

flex布局

  • 是一种浏览器提倡的布局模型
  • 布局网页更简单、灵活
  • 避免了浮动脱标的问题

组成部分

  • 弹性容器
  • 弹性项
  • 主轴
  • 侧轴/交叉轴

语法

css
display: flex;
display: flex;

主轴对齐方式

主轴整体对齐

  • justify-content

  • 取值:

    • left/right
    • start/end
    • flex-start
    • flex-end
    • center
    • space-between
    • space-around
    • space-evenly
    • first/last baseline

主轴单行对齐方式

  • justify-items:相当于给给每个项设置默认的justify-self值

主轴单个对齐方式

  • justify-self

侧轴对齐方式

侧轴多行整体对齐

  • 属性:align-content

侧轴单行对齐方式

  • 属性:align-items,相当于给每个项设置默认的align-self值
  • 取值:
    • center
    • stretch: 拉伸,默认值
    • start/end
    • flex-start/flex-end
    • self-[start/end]
    • first/last baseline

侧轴单个对齐方式

  • 属性:align-self

伸缩比

  • 属性:flex
  • 说明: flex是flex-grow(增长系数)、flex-shrink(收缩系数)和flex-basis(初始尺寸)三个属性的简写,第一个无单位数代表flex-grow、第二无单位数代表flex-shrink,带像素单位的是flex-basis的值

修改主轴方向

  • 属性:flex-direction
  • 取值:
    • row
    • column
    • row-reverse
    • column-reverse

弹性项换行

  • 属性:flex-wrap
  • 取值:
    • nowrap
    • wrap
    • wrap-reverse

flex-flow

  • flex-flow:flex-direction flex-wrap;

移动适配

rem

  • 相对单位
  • rem单位是相对HTML标签的字号计算结果
  • 1rem=1HTML字号大小
  • 将网页等分成10份,HTML标签的字号为视口宽度的1/10
  • px单位数值/基准根字号

媒体查询

css
@media 逻辑操作符 媒体类型 and (媒体特性) {
+  选择器 {
+    CSS属性
+  }
+}
+
+@media (媒体特性) {
+  选择器 {
+    CSS属性
+  }
+}
+
+@media (min-width:320px) {
+  html {
+    font-size: 32px;
+  }
+}
@media 逻辑操作符 媒体类型 and (媒体特性) {
+  选择器 {
+    CSS属性
+  }
+}
+
+@media (媒体特性) {
+  选择器 {
+    CSS属性
+  }
+}
+
+@media (min-width:320px) {
+  html {
+    font-size: 32px;
+  }
+}

关键词

  • and
  • only
  • not
  • or

媒体类型

  • screen(带屏幕的设备)
  • print(打印预览模式)
  • speech(屏幕阅读模式)
  • all(默认值)

媒体特性

  • 视口宽高:width、height、max-width、max-height、min-width、min-height
  • 屏幕方向:orientation,portrait竖屏,landscape横屏

vw/vh

  • vw = 1/100视口宽度
  • vh = 1/100视口高度

Less

Less(Leaner Style Sheets)是一门向后兼容的CSS 扩展语言,是一个CSS预处理器,扩充了CSS,使CSS具备一定的逻辑性、计算能力。

https://less.bootcss.com

变量

less
@width: 10px;
+@height: @width + 10px;
+
+#header {
+  width: @width;
+  height: @height;
+}
@width: 10px;
+@height: @width + 10px;
+
+#header {
+  width: @width;
+  height: @height;
+}

编译为

css
#header {
+  width: 10px;
+  height: 20px;
+}
#header {
+  width: 10px;
+  height: 20px;
+}

混合

less
.bordered {
+  border-top: dotted 1px black;
+  border-bottom: solid 2px black;
+}
+
+#menu a {
+  color: #111;
+  .bordered();
+}
+
+.post a {
+  color: red;
+  .bordered();
+}
.bordered {
+  border-top: dotted 1px black;
+  border-bottom: solid 2px black;
+}
+
+#menu a {
+  color: #111;
+  .bordered();
+}
+
+.post a {
+  color: red;
+  .bordered();
+}

.bordered 类所包含的属性就将同时出现在 #menu a.post a 中了。

嵌套

less
#header {
+  color: black;
+  .navigation {
+    font-size: 12px;
+  }
+  .logo {
+    width: 300px;
+  }
+}
#header {
+  color: black;
+  .navigation {
+    font-size: 12px;
+  }
+  .logo {
+    width: 300px;
+  }
+}
css
#header {
+  color: black;
+}
+#header .navigation {
+  font-size: 12px;
+}
+#header .logo {
+  width: 300px;
+}
#header {
+  color: black;
+}
+#header .navigation {
+  font-size: 12px;
+}
+#header .logo {
+  width: 300px;
+}

规则嵌套和冒泡

less
.component {
+  width: 300px;
+  @media (min-width: 768px) {
+    width: 600px;
+    @media  (min-resolution: 192dpi) {
+      background-image: url(/img/retina2x.png);
+    }
+  }
+  @media (min-width: 1280px) {
+    width: 800px;
+  }
+}
.component {
+  width: 300px;
+  @media (min-width: 768px) {
+    width: 600px;
+    @media  (min-resolution: 192dpi) {
+      background-image: url(/img/retina2x.png);
+    }
+  }
+  @media (min-width: 1280px) {
+    width: 800px;
+  }
+}

编译为

css
.component {
+  width: 300px;
+}
+@media (min-width: 768px) {
+  .component {
+    width: 600px;
+  }
+}
+@media (min-width: 768px) and (min-resolution: 192dpi) {
+  .component {
+    background-image: url(/img/retina2x.png);
+  }
+}
+@media (min-width: 1280px) {
+  .component {
+    width: 800px;
+  }
+}
.component {
+  width: 300px;
+}
+@media (min-width: 768px) {
+  .component {
+    width: 600px;
+  }
+}
+@media (min-width: 768px) and (min-resolution: 192dpi) {
+  .component {
+    background-image: url(/img/retina2x.png);
+  }
+}
+@media (min-width: 1280px) {
+  .component {
+    width: 800px;
+  }
+}

运算

算术运算符 +-*/ 可以对任何数字、颜色或变量进行运算。如果可能的话,算术运算符在加、减或比较之前会进行单位换算。计算的结果以最左侧操作数的单位类型为准。如果单位换算无效或失去意义,则忽略单位。无效的单位换算例如:px 到 cm 或 rad 到 % 的转换。

less
// 所有操作数被转换成相同的单位
+@conversion-1: 5cm + 10mm; // 结果是 6cm
+@conversion-2: 2 - 3cm - 5mm; // 结果是 -1.5cm
+
+// conversion is impossible
+@incompatible-units: 2 + 5px - 3cm; // 结果是 4px
+
+// example with variables
+@base: 5%;
+@filler: @base * 2; // 结果是 10%
+@other: @base + @filler; // 结果是 15%
// 所有操作数被转换成相同的单位
+@conversion-1: 5cm + 10mm; // 结果是 6cm
+@conversion-2: 2 - 3cm - 5mm; // 结果是 -1.5cm
+
+// conversion is impossible
+@incompatible-units: 2 + 5px - 3cm; // 结果是 4px
+
+// example with variables
+@base: 5%;
+@filler: @base * 2; // 结果是 10%
+@other: @base + @filler; // 结果是 15%

乘法和除法不作转换。因为这两种运算在大多数情况下都没有意义,一个长度乘以一个长度就得到一个区域,而 CSS 是不支持指定区域的。Less 将按数字的原样进行操作,并将为计算结果指定明确的单位类型。

less
@base: 2cm * 3mm; // 结果是 6cm
@base: 2cm * 3mm; // 结果是 6cm

转义

转义(Escaping)允许你使用任意字符串作为属性或变量值。任何 ~"anything"~'anything' 形式的内容都将按原样输出,除非 interpolation

less
@min768: ~"(min-width: 768px)";
+.element {
+  @media @min768 {
+    font-size: 1.2rem;
+  }
+}
@min768: ~"(min-width: 768px)";
+.element {
+  @media @min768 {
+    font-size: 1.2rem;
+  }
+}

编译为:

less
@media (min-width: 768px) {
+  .element {
+    font-size: 1.2rem;
+  }
+}
@media (min-width: 768px) {
+  .element {
+    font-size: 1.2rem;
+  }
+}

注意,从 Less 3.5 开始,可以简写为:

less
@min768: (min-width: 768px);
+.element {
+  @media @min768 {
+    font-size: 1.2rem;
+  }
+}
@min768: (min-width: 768px);
+.element {
+  @media @min768 {
+    font-size: 1.2rem;
+  }
+}

函数

Less 内置了多种函数用于转换颜色、处理字符串、算术运算等。这些函数在Less 函数手册中有详细介绍。

函数的用法非常简单。下面这个例子利用 percentage 函数将 0.5 转换为 50%,将颜色饱和度增加 5%,以及颜色亮度降低 25% 并且色相值增加 8 等用法:

less
@base: #f04615;
+@width: 0.5;
+
+.class {
+  width: percentage(@width); // returns `50%`
+  color: saturate(@base, 5%);
+  background-color: spin(lighten(@base, 25%), 8);
+}
@base: #f04615;
+@width: 0.5;
+
+.class {
+  width: percentage(@width); // returns `50%`
+  color: saturate(@base, 5%);
+  background-color: spin(lighten(@base, 25%), 8);
+}

映射

从 Less 3.5 版本开始,你还可以将混合(mixins)和规则集(rulesets)作为一组值的映射(map)使用。

less
#colors() {
+  primary: blue;
+  secondary: green;
+}
+
+.button {
+  color: #colors[primary];
+  border: 1px solid #colors[secondary];
+}
#colors() {
+  primary: blue;
+  secondary: green;
+}
+
+.button {
+  color: #colors[primary];
+  border: 1px solid #colors[secondary];
+}

输出符合预期:

css
.button {
+  color: blue;
+  border: 1px solid green;
+}
.button {
+  color: blue;
+  border: 1px solid green;
+}

作用域

Less 中的作用域与 CSS 中的作用域非常类似。首先在本地查找变量和混合(mixins),如果找不到,则从“父”级作用域继承。

less
@var: red;
+
+#page {
+  @var: white;
+  #header {
+    color: @var; // white
+  }
+}
@var: red;
+
+#page {
+  @var: white;
+  #header {
+    color: @var; // white
+  }
+}

与 CSS 自定义属性一样,混合(mixin)和变量的定义不必在引用之前事先定义。因此,下面的 Less 代码示例和上面的代码示例是相同的:

less
@var: red;
+
+#page {
+  #header {
+    color: @var; // white
+  }
+  @var: white;
+}
@var: red;
+
+#page {
+  #header {
+    color: @var; // white
+  }
+  @var: white;
+}

导入

css
@import "library"; // library.less
+@import "typo.css";
@import "library"; // library.less
+@import "typo.css";

被引入的less文件不会生成单独的css文件

控制Less编译输出

webstorm FileWatcher配置:

  • npm install -g less
  • aruments:--no-color $FileName$ ../css/$FileNameWithoutExtension$.css
  • output paths to refresh : ../css/$FileNameWithoutExtension$.css

VS Code EasyLess配置:

  • out: "../css/"
  • 首行添加注释控制编译的输出情况
    • // out: ./dir/
    • // out: ./dir/xxx.css
    • // out: false

BootStrap

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">

<script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>

栅格系统

  • 栅格化是指将整个网页的宽度分成若干等份,BootStrap3默认将网页分为12等份
超小屏幕小屏幕中等屏幕大屏幕
响应断点<768px>=768px>=992px>=1200px
别名xssmmdlg
容器宽度100%750px970px1170px
类前缀col-xs-*col-sm-*col-md-*col-lg-*
列数12121212
列间隙30px30px30px30px
  • .container是BootStrap中提供的类名,应用该类名的盒子,默认被指定宽度且居中
  • .container-fluid是BootStrap中提供的类名,所有应用的盒子,宽度为100%
  • 分别用.row类名和.col类名定义栅格布局的行和列

TIP

  1. container类自带间距15px
  2. row类自带间距-15px

bootstrap样式

https://v3.bootcss.com/css/

bootstrap组件

https://v3.bootcss.com/components/

Record

滚动条滑动效果

css
html {
+  scroll-behavior: smooth;
+}
html {
+  scroll-behavior: smooth;
+}
+ + + + \ No newline at end of file diff --git a/frontend/base/HTML.html b/frontend/base/HTML.html new file mode 100644 index 000000000..b71295db5 --- /dev/null +++ b/frontend/base/HTML.html @@ -0,0 +1,25 @@ + + + + + + HTML | 故事 + + + + + + + + + + + + + + +
Skip to content

HTML

标签

排版标签

标题:h1-h6

段落:p

换行:br

水平分割线:hr

文本格式化标签

加粗:strong/b

下划线:ins/u

倾斜:em/i

删除线:del/s

媒体标签

图片:img

音频:audio [src|controls|autoplay|loop]

视频:video [src|controls|autoplay|loop]

链接

超链接:a

列表

有序列表:ol > li

无序列表:ul > li

自定义列表:dl > dt > dd

表格

table > tr > td

th:表头

caption:标题

结构标签:thead/tbody/tfoot

合并单元格:rowspan-跨行合并 colspan-跨列合并(不能跨结构标签合并)

表单

input系列:text/password/radio/checkbox/file/submit/reset/button

button

select

textarea

label

语义化标签

没有语义的布局标签:div/span

语义化标签:header/nav/footer/aside/section/article(显示特点和div一致,多了语义)

字符实体

空格:&nbsp

骨架结构标签

  • <!DOCTYPE html>:声明网页HTML版本
  • <html lang="en":标识网页使用的语言,作用:搜索引擎归类+浏览器翻译, zh-CN/en
  • <meta>:元数据标签,元数据是关于文档的数据,例如文档的作者、字符集、关键字和描述等信息,这些信息对于搜索引擎的抓取和用户的浏览很有帮助。
    • charset:指定文档的字符编码。
    • name:定义元数据的名称。
    • content:定义元数据的内容。
    • http-equiv:提供有关如何处理文档的其他信息。
    • viewport:指定当前网页的视口(viewport)尺寸和缩放比例,以便浏览器正确渲染页面。

SEO(Search Engine Optimization)标签

  • <title>:网页标题
  • <meta name="description">:网页描述标签
  • <meta name="keywords":网页关键词

ico图标

<link rel="icon" href="favicon.ico">

+ + + + \ No newline at end of file diff --git a/frontend/base/JavaScript.html b/frontend/base/JavaScript.html new file mode 100644 index 000000000..9f484b733 --- /dev/null +++ b/frontend/base/JavaScript.html @@ -0,0 +1,1485 @@ + + + + + + JavaScript | 故事 + + + + + + + + + + + + + + +
Skip to content

JavaScript

组成

  • ECMAScript:规定了js基础语法,比如变量、分支语句、循环语句、对象等
  • Web APIs
    • DOM:操作文档,比如对页面元素移动、添加删除等操作
    • BOM:操作浏览器,比如页面弹窗、检测窗口宽度、存储数据到浏览器等

基本语法

输入

prompt()

输出

console.log()

document.write()

alert()

alert和prompt会跳过页面渲染先被执行

变量

声明

  • let

比较旧的JavaScript中使用var声明变量

var的一些问题:

  • 可以先使用,再声明
  • var声明过的变量可以重复声明
  • 变量提升、全局变量、没有块级作用域

命名

  • 只能下划线、字母、数字、$,且不能数字开头
  • 字母区分大小写
规范
  • 小驼峰

数组

  • let arr = [1, 2, 3] / let arr = new Array(1, 2, 3)

  • 数组有序

  • 取值:数组[下标]

  • 长度:数组.length

  • 修改:arr[下标] = 新值

  • 增加

    • arr.push() 将一个或多个元素新增到末尾,返回新的数组长度
    • arr.unshift()将一个或多个元素新增到开头,返回新的数组长度
  • 删除

    • arr.pop() 删除最后一个元素,并返回该元素的值
    • arr.shift()删除第一个元素,并返回该元素的值
    • arr.splice(操作的下标,删除的个数),删除指定元素并返回

常量

  • 声明:const
  • 声明常量必须赋值

数据类型

基本数据类型

  • number

  • string

    • 模板字符串: 使用反引号包裹数据,使用${}替换数据

      js
      let age = 20
      +console.log(`我今年${age}岁`)
      let age = 20
      +console.log(`我今年${age}岁`)
  • boolean

  • undefined

    • 没有赋值
    • undefined +1 -> NaN
  • null

    • 内容为空
    • null + 1 -> 1
NaN

NaN代表一个计算错误,是一个不正确或未定义的数学操作得到的结果,任何对NaN的操作都会返回NaN

typeof

  • 运算符写法:typeof 变量
  • 函数写法:typeof(变量)

数据类型转换

隐式转换
  • +号两边只要有字符串,都会转字符串
  • 除了+,其他算数运算符会把数据转换为数字类型
  • +作为正号可以转换数字
显式转换
  • Number(变量)

引用数据类型

  • object

    js
    let obj = {
    +  uname: 'abc',
    +  age: 18,
    +  gender: '女',
    +  speak: function(x) {
    +    console.log('hello' + x)
    +  }
    +}
    let obj = {
    +  uname: 'abc',
    +  age: 18,
    +  gender: '女',
    +  speak: function(x) {
    +    console.log('hello' + x)
    +  }
    +}
    • 属性名可以用引号,一般省略,除非遇到特殊符号(空格、中横线等)
  • 查看:

    • 对象.属性
    • 对象['属性']
  • 修改: 对象.属性 = 新值

  • 新增: 对象.新属性 = 值

  • 删除: delete 对象.属性

  • 对象方法: 对象.方法名()

  • 遍历对象

    • js
      for (let k in obj) {
      +  console.log(obj[k]) //k带引号
      +}
      for (let k in obj) {
      +  console.log(obj[k]) //k带引号
      +}

for in遍历数组 是数组下标,但是是字符串

运算符

赋值运算符

  • +=
  • -=
  • *=
  • /=
  • %=

一元运算符

  • ++
  • --

比较运算符

  • <
  • >
  • >=
  • <=
  • ==: 值是否相等
  • ===:类型和值是否都相等
  • !==:是否不全等

逻辑运算符

  • &&
  • ||
  • !

流程控制语句

  • if

    • 除了0,所有的数字都为真
    • 除了'',所有字符串都为真
  • switch case

    • 数据和值必须满足全等===

    • js
      switch (数据) {
      +  case 值1:
      +    代码1
      +    break
      +  case 值2:
      +    代码2
      +    break
      +  default:
      +    代码n
      +}
      switch (数据) {
      +  case 值1:
      +    代码1
      +    break
      +  case 值2:
      +    代码2
      +    break
      +  default:
      +    代码n
      +}
  • 三元运算符

循环控制语句

  • while
  • for
  • break/continue

函数

js
function 函数名(参数列表) {
+  函数体
+}
function 函数名(参数列表) {
+  函数体
+}
  • 命名 小驼峰

  • return

    • 没有return 默认返回undefined

作用域

  • 全局变量
    • 局部变量或块级变量 没有let声明直接赋值的当全局变量看(不提倡)
  • 局部变量

匿名函数

  • 函数表达式:把匿名函数赋值给一个变量,通过变量名调用

    • js
      let fn = function () {}
      let fn = function () {}
  • 立即执行函数

    • js
      (function() {...})();
      +(function() {...})();
      +---
      +(function(x, y) {
      +  console.log(x + y)
      +})(1, 3)
      (function() {...})();
      +(function() {...})();
      +---
      +(function(x, y) {
      +  console.log(x + y)
      +})(1, 3)
    • 前一个括号声明,后一个括号调用

    • 分号

逻辑中断

  • 短路:只存在&&||中,当满足一定条件会让右边代码不执行
    • &&:左边为false就短路
    • ||:左边为true就短路

转换boolean

  • "": false

  • 0: false

  • undefined: false

  • null: false

  • NaN: false

  • "" + 1 = 1

  • null经过数字转换会变0

  • undefined经过数字转换会变NaN

Web APIs

DOM

获取DOM元素

  • document.querySelector(CSS选择器):获取匹配的第一个元素
  • document.querySelectorAll(CSS选择器):获取匹配的多个元素
  • document.getElementById():通过元素的 id 属性获取一个 DOM 元素
  • document.getElementsByName():通过元素的 name 属性获取一个类数组的元素集合,该方法返回一个 NodeList 对象
  • document.getElementsByClassName():方法通过元素的 name 属性获取一个类数组的元素集合,该方法返回一个 NodeList 对象
  • document.getElementsBytagName():通过元素的标签名获取一个类数组的元素集合,该方法返回一个 NodeList 对象

操作元素内容

  • 对象.innerText

  • 对象.innerHTML

  • 对象.属性=值

    • js
      const image = document.querySelector('img')
      +image.src = 'xxx.jpg'
      +image.title = '123'
      const image = document.querySelector('img')
      +image.src = 'xxx.jpg'
      +image.title = '123'
  • 对象.style.样式属性=值

    • js
      box.style.width = '300px'
      +box.backgroundColor = 'pink' //小驼峰
      box.style.width = '300px'
      +box.backgroundColor = 'pink' //小驼峰
  • 通过类名修改属性,会覆盖

    • js
      //定义好类对应的属性,给对象添加类名
      +对象.className = 类名
      //定义好类对应的属性,给对象添加类名
      +对象.className = 类名
  • 通过classList操作类控制CSS,用于追加和删除

    • js
      元素.classList.add(类名)//追加
      +元素.classList.remove(类名)//删除
      +元素.classList.toggle(类名)//切换
      元素.classList.add(类名)//追加
      +元素.classList.remove(类名)//删除
      +元素.classList.toggle(类名)//切换
  • 自定义属性

    • H5中推出的data-自定义属性

    • 在标签上一律以data-开头

    • DOM对象上一律以dataset对象方式获取

    • html
      <body>
      +  <div class="box" data-id="10">盒子</div>
      +  <script>
      +    const box = document.querySelector('.box')
      +    console.log(box.dataset.id)
      +  </script>
      +</body>
      <body>
      +  <div class="box" data-id="10">盒子</div>
      +  <script>
      +    const box = document.querySelector('.box')
      +    console.log(box.dataset.id)
      +  </script>
      +</body>

事件监听

  • 元素对象.addEventListener('事件类型', 要执行的函数)

元素.on事件:也可以添加事件监听,但会被覆盖,且只能冒泡 不能捕获,addEventListener不会被覆盖,能冒泡 也能捕获。

  • 事件类型
    • 鼠标事件
      • click
      • mouseenter: 没冒泡,只会在鼠标进入目标元素时触发一次
      • mouseover:有冒泡,事件在鼠标经过目标元素或任何子元素时会不断触发
      • mouseleave
      • mousemove: 鼠标移动
    • 焦点事件
      • focus
      • blur
    • 键盘事件
      • keydown
      • keyup
    • 文本事件
      • input

事件对象

事件对象中有事件触发时的相关信息,例如鼠标点击时的位置,键盘按下时的键位

js
btn.addEventListener('click', function(e){
+  console.log(e)
+})
btn.addEventListener('click', function(e){
+  console.log(e)
+})
常用对象属性
  • type:事件类型
  • clientX/clientY:光标相对于浏览器可见窗口左上角的位置
  • offsetX/offsetY:光标相对于当前DOM元素左上角的位置
  • key:用户按下的键盘的值,现在不提倡使用keyCode

环境对象

指的是函数内部特殊的变量this,它代表着当前函数运行时所处的环境

  • 函数的调用方式不同,this的指代对象也不通
  • this指向的粗略规则是谁调用指向谁(addEventListener指向绑定的元素,普通函数指向window)

回调函数

函数A作为参数传递给函数B,A就被称为回调函数

事件流

事件流指的是事件完整执行过程中的流动路径

事件捕获

DOM的根元素开始去执行对应的事件(从父元素到子元素)

js
DOM.addEventListener(事件类型, 函数, 是否使用捕获机制)
DOM.addEventListener(事件类型, 函数, 是否使用捕获机制)

L0事件只有冒泡,没有捕获

事件冒泡

当一个元素的事件被触发时,同样的事件会在该元素的所有祖先元素中依次被触发。这一过程被称为事件冒泡(从子元素到父元素)

  • 简单理解:当一个元素触发事件后,会依次向上调用所有父级元素的同名事件

  • 事件冒泡是默认存在的

阻止事件传播
  • 事件对象.stopPropagation()
  • 阻断事件流动传播,既能阻止冒泡,也能阻止捕获
js
btn.addEventListener('click', function(e){
+  e.stopPropagation()
+})
btn.addEventListener('click', function(e){
+  e.stopPropagation()
+})
解绑事件
  • on事件方式

    • js
      // 绑定事件
      +btn.onClick = function(e){
      +  console.log(e)
      +}
      +// 解绑事件
      +btn.onClick = null
      // 绑定事件
      +btn.onClick = function(e){
      +  console.log(e)
      +}
      +// 解绑事件
      +btn.onClick = null
  • addEventListener方式

    • js
      function fn(e){
      +  console.log(e)
      +}
      +//绑定事件
      +btn.addEventListener('click', fn)
      +//解绑事件
      +btn.removeEventListener('click', fn)
      function fn(e){
      +  console.log(e)
      +}
      +//绑定事件
      +btn.addEventListener('click', fn)
      +//解绑事件
      +btn.removeEventListener('click', fn)
    • 匿名函数无法解绑

事件委托

事件委托是利用事件流特征解决开发问题的技巧,可以减少事件注册次数,提高程序性能,原理是利用事件冒泡特点,给父元素注册事件,当触发子元素的时候,会冒泡到父元素身上,从而触发父元素的事件

阻止元素默认行为

e.preventDefault()

其他事件
  • 页面加载事件

    • 外部资源加载完毕时触发的事件

      • 等待页面所有资源加载完毕,执行回调函数:window.addEventListener('load', function() {})

        也可以针对某个资源绑定事件:img.addEventListener('load', function() {})

    • 初始HTML文档被完全加载和解析完成后,DOMContentLoaded事件被触发,无需等待样式表、图像等完全加载

      • document.addEventListener('DOMContentLoaded', function() {})
  • 页面滚动事件

    • 滚动条在滚动的时候持续触发的事件

      • window.addEventListener('scroll', function() {})
      • 给window或document添加scroll事件
      • 也可以监听某个元素内部滚动
    • 获取滚动位置

      • scrollLeft**(可读写)**

      • scrollTop**(可读写)**

      • js
        window.addEventListener('scroll', function() {
        +  const n = document.documentElement.scrollTop
        +  console.log(n)
        +})
        window.addEventListener('scroll', function() {
        +  const n = document.documentElement.scrollTop
        +  console.log(n)
        +})

        document.documentElement返回对象为HTML元素

        ......
    • 滚动到指定坐标

      • scrollTo(x, y)
  • 页面尺寸事件

    • 窗口尺寸改变时触发的事件resize
      • window.addEventListener('resize', function() {})
    • 获取元素可见部分的宽高clientWidthclientHeight
      • 不包含border,margin,滚动条

元素尺寸位置

获取宽高
  • offsetWidth和offsetHeight
  • 获取元素自身的宽高,包含padding,border
  • 结果是数值
  • 获取的是可视宽高,如果盒子隐藏,结果是0
获取位置
  • offsetLeft和offsetTop
  • 获取元素距离自己定位父级元素的左、上距离,只读属性
获取元素大小及其相对视口的位置
  • element.getBoundingClientRect()

日期对象

  • 实例化

    • const date = new Date()
    • const date = new Date('2023-4-8 08:00:00')
  • 常用方法

    • getFullYear():四位数年份
    • getMonth():月份,范围0-11
    • getDate():获取月份中的每一天
    • getDay():获取星期,0-6
    • getHours():小时,0-23
    • getMinutes():分钟,0-59
    • getSeconds():秒,0-59
    • toLocaleString(): yyyy/m/d HH:mm:ss
  • 时间戳

    • date.getTime()
    • +new Date()
    • Date.now()

DOM节点

节点类型
  • 元素节点
  • 属性节点
  • 文本节点
  • 其他(注释、文档类型、CDATA、实体引用、处理指令。。。)
查找节点
  • 父节点
    • 元素.parentNode
  • 子节点
    • 元素.childNodes:获取所有子节点,包括文本(空格、换行)、注释节点等
    • 元素.children:仅获取元素节点,返回的是一个伪数组
  • 兄弟节点
    • nextElementSibling:下一个兄弟节点
    • previousElementSibling:上一个兄弟节点
新增节点
创建节点
  • const div = document.createElement('div')
追加节点
  • 父元素.appendChild(div)

  • 父元素.insertBefore(要插入的元素, 在哪个元素前面):插入某个元素之前

    • 例:ul.insertBefore(li, ul.children[0])
克隆节点
  • 元素.cloneNode(布尔值)
    • true:克隆时会包含后代节点一起克隆
    • false:不包含后代节点,默认值
删除节点
  • 父元素.removeChild(子元素)

BOM

组成

BOM(Browser Object Model)是浏览器对象模型,包含:navigator、location、document、history、screen

window是一个全局对象,document、alert()、console.log()都是window的属性

  • 所有通过var定义在全局作用域中的变量、函数都会变成window对象的属性和方法
  • window对象下的属性和方法调用的时候可以省略window

定时器

延时函数
  • let timer = setTimeout(回调函数, 等待时间ms),返回id,setTimeout只执行一次
  • 关闭:clearTimeout(timer)
间歇函数
  • let interval = setInterval(函数, 间隔时间ms),返回的是的是一个id数字,不断执行
  • 关闭:clearInterval(interval)

事件循环

js是单线程,所有任务需要排队。HTML5提出了Web Worker标准,允许JavaScript脚本创建多个线程。于是JS出现了同步和异步。

  • 同步任务:都在主线程执行,形成执行栈
  • 异步任务:通过回调函数实现,异步任务添加到任务队列中,一般异步任务有以下三种类型
    • 普通事件:click、resize等
    • 资源加载:load、error等
    • 定时器:setTimeout、setInterval等
执行机制
  1. 先执行执行栈中的同步任务
  2. 异步任务放到任务队列中
  3. 执行栈中的所有同步任务执行完毕,系统会按次序读取任务队列中的异步任务,被读取的异步任务结束等待状态,进入执行栈,开始执行

location

localtion的数据类型是对象,它拆分保存了URL地址的各个组成部分

  • location.href:常用于页面跳转

  • location.search:获取地址中携带的参数,符号?后面的部分

  • location.hash:获取地址中的hash值,符号#后面的部分

  • location.reload():用来刷新当前页面,传入参数true时强制刷新

navigator的数据类型是对象,该对象下记录了浏览器自身的相关信息

  • navigator.userAgent:检测浏览器版本和平台

history

history数据类型是对象,主要管理历史记录,该对象与浏览器地址栏的操作相对应,如前进、后退、历史记录等

  • history.back()
  • history.forward()
  • history.go(参数): 1->前进一个页面,-1->后退一个页面

本地存储

介绍

数据存储在用户浏览器中,设置、读取方便,刷新页面不会丢失数据,sessionStorage和localStorage约5M

分类
  • localStorage
    • 可以多窗口(页面)共享(同一浏览器可以共享)
    • 键值对形式存储使用
    • 语法
      • 存储:localStorage.setItem(key, value)
      • 查询:localStorage.getItem(key)
      • 删除:localStorage.removeItem(key)
  • sessionStorage
    • 生命周期到关闭浏览器窗口截止
    • 在同一个窗口(页面)下数据可以共享
    • 键值对形式存储使用
    • 用法api和localStorage一致
存储复杂数据类型

把复杂数据类型转成字符串形式存储

  • JSON.stringify
  • JSON.parse

数组map和join

map
  • 遍历数组处理数据,返回新的数组

  • js
    const arr = ['red', 'blue']
    +const newArr = arr.map(function(ele, index) {
    +  return ele + '颜色'
    +})
    +console.log(newArr) // ['red颜色', 'blue颜色']
    const arr = ['red', 'blue']
    +const newArr = arr.map(function(ele, index) {
    +  return ele + '颜色'
    +})
    +console.log(newArr) // ['red颜色', 'blue颜色']
join
  • 把数组所有元素转换为一个字符串
  • const newStr = join(字符串):元素用指定字符串相连

进阶

正则表达式

  • 定义:const reg = /表达式/
  • 判断是否匹配:reg.test(被检测字符串),匹配返回true,否则false
  • 查找:reg.exec(被检测字符串),找到返回数组,否则为null

元字符

  • 边界符

    • ^:开始
    • $:结束
  • 量词

    • *:0或多次
    • +:1或多次
    • ?:0或1次
    • {n}:重复n次
    • {n,}:重复n次或更多
    • {n,m}:重复n次到m次
  • 字符类

    • []:匹配字符集合,匹配任一个都是true
    • [a-zA-Z]:字母
    • [^a-z]:[]中的^表示取反
    • .:除换行之外的任何单个字符
    • \d:数字
    • \D:所有0-9以外字符,等于[^0-9]
    • \w:任一字母、数字、下划线,相当于[a-zA-Z0-9_]
    • \W:匹配除字母、数字、下划线之外的字符,相当于[^a-zA-Z0-9_]
    • \s:匹配空格(包括制表符、换行符、空格符等),相当于[\t\r\n\v\f]
    • \S:匹配非空格,相当于[^\t\r\n\v\f]

修饰符

  • 语法:/表达式/修饰符

  • 修饰符:

    • i:ignore,匹配时,不区分大小写
    • g:global,匹配所有满足正则的结果

替换

  • 语法:字符串.replace(/正则表达式/, 替换的文本),返回替换后的字符串

作用域

  • 局部作用域

  • 全局作用域

  • 作用域链

  • JS垃圾回收机制

    • 全局变量一般不会回收(关闭页面回收)
    • 一般情况下局部变量的值不再被使用会被自动回收
    • 内存由于某种原因未释放或无法释放会内存泄漏
    • 栈:由操作系统自动分配释放函数的参数值、局部变量等基本数据类型放在栈里
    • 堆:一般由开发分配释放,若开发不释放由垃圾回收机制回收。复杂数据类型放在堆里。

    引用计数法(有循环引用问题)

    • 定义“内存不再使用”,看一个对象是否有指向它的引用,没有引用就回收对象
      • 根据记录被引用的次数
      • 被引用一次,就+1,多次引用会累加
      • 如果减少一个引用就-1
      • 如果引用次数是0,则释放内存

    标记清除法

    • 将不再使用的对象定义为无法达到的对象
    • 从根部(JS中就是全局对象)出发定时扫描内存中的对象,凡是能从根部到达的对象,都是还需要使用的
    • 无法由根部出发触及的对象标记为不再使用,稍后进行回收
  • 闭包

    • 和python中的闭包一样:如果在一个外部函数中定义一个内部函数,内部函数对外部作用域的变量进行引用,外部函数的返回值是内部函数,这样的函数就被认为是闭包(closure)。
  • 变量提升

    • 允许变量在声明之前即被访问(var声明变量)
    • js会在执行之前把当前作用域下var声明的变量提升到当前作用域的最前面,只提升声明,不提升赋值
  • 函数提升

    • 代码执行前会把所有函数声明提升到当前作用域的最前面
    • 只提升声明,不提升调用

函数表达式特殊,必须先声明赋值后调用

函数进阶

  • 动态参数:arguments,只存在于函数里,伪数组

  • 剩余参数:function getSum(paramA, paramB, ...arr),arr是个真数组

  • 展开运算符:...能将一个数组进行展开

    • js
      const arr = [1,5,3]
      +console.log(...arr)// 1 5 3
      const arr = [1,5,3]
      +console.log(...arr)// 1 5 3
    • 用于求数组最大/小值Math.max(...arr)

    • 用于合并数组:const arr = [...arr1, ...arr2]

箭头函数

引入箭头函数是为了更简洁的写法,适用于需要匿名函数的地方

js
const fn = () => {}
+const fn = x => { console.log(x) }
+const fn = x => console.log(x)
+const fn = x => x * 2
+const fn = (uname) => ({ uname: uname }) //返回一个对象
const fn = () => {}
+const fn = x => { console.log(x) }
+const fn = x => console.log(x)
+const fn = x => x * 2
+const fn = (uname) => ({ uname: uname }) //返回一个对象
箭头函数的this

箭头函数不会创建自己的this对象,它只会从自己的作用域链的上一层

解构赋值

数组解构

数组结构是将数组的单元值快速批量赋值给一系列变量的简洁语法

  • const [max, min, avg] = [100, 60, 80]
  • 典型用法:交换两个变量
  • 可以设置默认值
  • 可以用剩余参数防止undefined传递
  • 可以忽略某些值const [a, ,c, d] = [1, 2, 3, 4]

js必须加分号场景:

  1. 两个连续的立即执行函数
  2. 使用数组

对象解构

对象解构是将对象的属性和方法快速批量赋值给一系列变量的简介语法

  • js
    const user = {
    +  name: '小明',
    +  age: 18
    +}
    +const {name, age} = user
    const user = {
    +  name: '小明',
    +  age: 18
    +}
    +const {name, age} = user
  • 对象的属性值将会被赋值给与属性名相同的变量

  • 对象中找不到与变量名一致的属性时变量值为undefined

  • 数组对象解构

    js
    const pig = [
    +  {
    +    name: '佩奇',
    +    age: 6
    +  }
    +]
    +
    +const [{ name, age }] = pig
    +console.log(name,age)
    const pig = [
    +  {
    +    name: '佩奇',
    +    age: 6
    +  }
    +]
    +
    +const [{ name, age }] = pig
    +console.log(name,age)
  • 多级对象解构

    js
    const pig = {
    +  name: '佩奇',
    +  age: 6,
    +  family: {
    +    mother: 'mon',
    +    father: 'dad'
    +  }
    +}
    +
    +const { name, family: { mother, father }} = pig
    const pig = {
    +  name: '佩奇',
    +  age: 6,
    +  family: {
    +    mother: 'mon',
    +    father: 'dad'
    +  }
    +}
    +
    +const { name, family: { mother, father }} = pig

对象

创建对象的方式

  • 字面量创建

  • 构造函数

    • 命名以大写字母开头
    • 只能由new操作符来执行
    • 实例化执行过程
      • 创建新对象
      • 构造函数this指向新对象
      • 执行构造函数代码,修改this,添加属性
      • 返回新对象

实例成员&静态成员

  • 实例成员:构造函数创建的对象为实例对象,实例对象的属性和方法称为实例成员
  • 静态成员:构造函数的属性和方法称为静态成员
    • 静态成员只能由构造函数访问
    • 静态方法中的this指向构造函数
    • Date.now()、Math.PI、Math.random()

内置构造函数

  • Object

    • Object.keys()
    • Object.values()
    • Object.assign(dest, source)
  • Array

    • 实例方法:forEach、filter、map、reduce、join、find、every、some、concat、splice、reverse、findIndex...

    • 伪数组转换为真数组:Array.from()

    • arr.some((item, index)=> {
      +	//some code, some循环可以终止
      +	return true
      +})
      +
      +//every 判断数组每一项是否都满足条件
      +let res = arr.every(item => item.state) 
      +
      +//reduce 
      +arr.reduce((累加的结果, 当前循环项) => {}, 初始值)
      +arr.reduce((amt, item) => amt += item.price, 0)
      arr.some((item, index)=> {
      +	//some code, some循环可以终止
      +	return true
      +})
      +
      +//every 判断数组每一项是否都满足条件
      +let res = arr.every(item => item.state) 
      +
      +//reduce 
      +arr.reduce((累加的结果, 当前循环项) => {}, 初始值)
      +arr.reduce((amt, item) => amt += item.price, 0)
  • String

    • 实例属性、方法:length、split()、substring()、startsWith()、includes()、toUpperCase()、toLowerCase()、indexOf()、endsWith()、replace()、match()...
  • Number

    • toFixed()设置保留小数位数

原型Prototype

  • 构造函数通过原型分配的函数是所有对象所共享的。
  • JavaScript每一个构造函数都有一个prototype属性,指向另一个对象,所以也称为原型对象
  • prototype对象可以挂载函数,对象实例化不会多次创建原型上函数,节约内存
  • 可以把不变的方法直接定义在prototype对象上,这样所有对象的实例就可以共享这些方法
  • 构造函数和原型对象中的this都指向实例化的对象

constructor属性

每个原型对象里都有个constructor属性,该属性指向该原型对象的构造函数

对象原型

每个对象都有一个属性__proto__,指向构造函数的prototype对象

  • __proto__是JS非标准属性
  • [[prototype]]和__proto__意义相同
  • 用来表明当前实例对象指向哪个原型对象prototype
  • __proto__对象原型里也有一个constructor属性,指向创建该实例对象的构造函数

原型继承

通过原型可以继承公共属性

js
const Person = {
+  eyes: 2,
+  nose: 1
+}
+
+function Man() {
+  
+}
+Man.prototype = Person
+Man.prototype.constructor = Man
+
+---
+  
+const Person = {
+  this.eyes: 2,
+  this.nose: 1
+}
+
+function Man() {
+  
+}
+Man.prototype = new Person()
const Person = {
+  eyes: 2,
+  nose: 1
+}
+
+function Man() {
+  
+}
+Man.prototype = Person
+Man.prototype.constructor = Man
+
+---
+  
+const Person = {
+  this.eyes: 2,
+  this.nose: 1
+}
+
+function Man() {
+  
+}
+Man.prototype = new Person()

原型链

基于原型对象的继承使得不同构造函数的原型对象关联在一起,并且这种关联关系是一种链状的解构,称为原型链

查找规则
  • 当访问一个对象的属性/方法时,首先查找这个对象自身有无该属性
  • 如果没有就查找他的原型(__proto__指向的prototype对象)
  • 如果还没有就查找原型对象的原型(Object的prototype)
  • 依此类推一直到Object为止(null)
  • __proto__对象原型的意义就在于为对象成员查找机制提供方向
  • 可以使用instanceof运算符检测构造函数的prototype属性是否出现在某个实例对象的原型链上

深浅拷贝

深浅拷贝只针对引用数据类型

  • 浅拷贝:如果是简单数据类型拷贝值,引用数据类型拷贝的是地址
    • 拷贝对象:Object.assign() / 展开运算符 {...obj}拷贝对象
    • 拷贝数组:Array.prototype.concat() 或者 [...arr]
  • 深拷贝:拷贝的是对象,不是地址
    • 通过递归实现深拷贝
    • lodash中的_.cloneDeep()
    • JSON.stringify()

异常

抛出异常

  • throw msg
  • throw new Error(msg)

异常捕获

js
try {
+  
+} catch (err) {
+  
+} finally {
+  
+}
try {
+  
+} catch (err) {
+  
+} finally {
+  
+}

debugger

debugger

this

普通函数

  • 普通函数的调用方式决定了this的值,即谁调用 this的值指向谁

  • 普通函数没有明确调用者时this的值为window,严格模式下没有调用者时this的值为undefined

箭头函数

  • 箭头函数中并不存在this
  • 箭头函数会默认绑定外层this的值,所以在箭头函数中this的值和外层的this是一样的
  • 箭头函数中的this引用的就是最近作用域中的this
  • 向外层作用域中,一层一层查找this,直到有this的定义

改变this指向

  • fun.call(thisArg, arg1, arg2...)
    • thisArg:fun函数运营时指定的this值
    • arg1,arg2:传递的其他参数
  • apply(thisArg, [argsArray])
    • thisArg:fun函数运营时指定的this值
    • argsArray:传递的值,必须包含在数组里
  • bind()
    • bind不会调用函数,但能改变函数内部this的指向
    • fun.bind(thisArg, arg1, arg2...)
    • thisArg:fun函数运营时指定的this值
    • arg1,arg2:传递的其他参数
    • 返回由指定this值和初始化参数改造的原函数的拷贝

防抖(debounce)

  • 单位时间内,频繁触发事件,只执行最后一次

  • lodash库的_.debounce(fun, 时间)

思路

  1. 声明一个定时器
  2. 每次触发事件都先判断是否有定时器,如果有先清除
  3. 如果没有则开启定时器并保存变量
  4. 在定时器中调用要执行的函数
js
const box = document.querySelector('.box')
+let i = 1
+function mouseMove() {
+  box.innerHTML = i++
+}
+
+function debounce(fn, t) {
+  let timer
+  return function() {
+    if (timer) clearTimeout(timer)
+    timer = setTimeout(function() {
+      fn()
+    }, t)
+  }
+}
+
+box.addEventListener('mousemove', debounce(mouseMove, 500))
const box = document.querySelector('.box')
+let i = 1
+function mouseMove() {
+  box.innerHTML = i++
+}
+
+function debounce(fn, t) {
+  let timer
+  return function() {
+    if (timer) clearTimeout(timer)
+    timer = setTimeout(function() {
+      fn()
+    }, t)
+  }
+}
+
+box.addEventListener('mousemove', debounce(mouseMove, 500))

节流(throttle)

  • 单位时间内,频繁触发事件,只执行一次
  • lodash库的_.throttle(fun, 时间)

思路

  1. 声明一个定时器
  2. 每次触发事件都判断是否有定时器,如果有则不开启新定时器
  3. 如果没有定时器则开启定时器并保存变量
    1. 定时器里调用执行的函数
    2. 定时器里要把上一个定时器清空
js
function throttle(fn, t) {
+  let timer = null
+  return function() {
+    if(!timer) {
+      timer = setTimeout(function(){
+        fn()
+        // setTimeout中无法删除定时器,因为定时器还在运作,所以不能用clearTimeout
+        timer = null
+      }, t)
+    }
+  }
+}
+
+box.addEventListener('mousemove', throttle(mouseMove, 500))
function throttle(fn, t) {
+  let timer = null
+  return function() {
+    if(!timer) {
+      timer = setTimeout(function(){
+        fn()
+        // setTimeout中无法删除定时器,因为定时器还在运作,所以不能用clearTimeout
+        timer = null
+      }, t)
+    }
+  }
+}
+
+box.addEventListener('mousemove', throttle(mouseMove, 500))

案例:页面打开,记录上一次的视频播放位置

两个事件
  • ontimeupdate:事件在视频/音频当前播放位置发生改变时触发
  • onloadeddata:事件在当前帧的数据加载完成且还没有足够的数据播放视频/音频的下一帧时触发
js
video.ontimeupdadte = _.throttle(() => {
+  localStorage.setItem('currentTime', video.currentTime)
+}, 1000)
+
+video.onloadeddata = () => {
+  video.currentTime = localStorage.getItem('currentTime') || 0
+}
video.ontimeupdadte = _.throttle(() => {
+  localStorage.setItem('currentTime', video.currentTime)
+}, 1000)
+
+video.onloadeddata = () => {
+  video.currentTime = localStorage.getItem('currentTime') || 0
+}

ES6

Promise

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

Promise对象有以下两个特点。

(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

基本用法

ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。

下面代码创造了一个Promise实例。

js
const promise = new Promise(function(resolve, reject) {
+  // ... some code
+
+  if (/* 异步操作成功 */){
+    resolve(value);
+  } else {
+    reject(error);
+  }
+});
const promise = new Promise(function(resolve, reject) {
+  // ... some code
+
+  if (/* 异步操作成功 */){
+    resolve(value);
+  } else {
+    reject(error);
+  }
+});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供。

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。

js
promise.then(function(value) {
+  // success
+}, function(error) {
+  // failure
+});
promise.then(function(value) {
+  // success
+}, function(error) {
+  // failure
+});

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。这两个函数都是可选的,不一定要提供。它们都接受Promise对象传出的值作为参数。

下面是一个Promise对象的简单例子。

js
function timeout(ms) {
+  return new Promise((resolve, reject) => {
+    setTimeout(resolve, ms, 'done');//setTimeout的第三个参数是给第一个函数参数传递的参数,即done会传递给resolve函数作为参数
+  });
+}
+
+timeout(100).then((value) => {
+  console.log(value);
+});
function timeout(ms) {
+  return new Promise((resolve, reject) => {
+    setTimeout(resolve, ms, 'done');//setTimeout的第三个参数是给第一个函数参数传递的参数,即done会传递给resolve函数作为参数
+  });
+}
+
+timeout(100).then((value) => {
+  console.log(value);
+});

上面代码中,timeout方法返回一个Promise实例,表示一段时间以后才会发生的结果。过了指定的时间(ms参数)以后,Promise实例的状态变为resolved,就会触发then方法绑定的回调函数。

Promise 新建后就会立即执行。

js
let promise = new Promise(function(resolve, reject) {
+  console.log('Promise');
+  resolve();
+});
+
+promise.then(function() {
+  console.log('resolved.');
+});
+
+console.log('Hi!');
+
+// Promise
+// Hi!
+// resolved
let promise = new Promise(function(resolve, reject) {
+  console.log('Promise');
+  resolve();
+});
+
+promise.then(function() {
+  console.log('resolved.');
+});
+
+console.log('Hi!');
+
+// Promise
+// Hi!
+// resolved

上面代码中,Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出。

下面是异步加载图片的例子。

js
function loadImageAsync(url) {
+  return new Promise(function(resolve, reject) {
+    const image = new Image();
+
+    image.onload = function() {
+      resolve(image);
+    };
+
+    image.onerror = function() {
+      reject(new Error('Could not load image at ' + url));
+    };
+
+    image.src = url;
+  });
+}
function loadImageAsync(url) {
+  return new Promise(function(resolve, reject) {
+    const image = new Image();
+
+    image.onload = function() {
+      resolve(image);
+    };
+
+    image.onerror = function() {
+      reject(new Error('Could not load image at ' + url));
+    };
+
+    image.src = url;
+  });
+}

上面代码中,使用Promise包装了一个图片加载的异步操作。如果加载成功,就调用resolve方法,否则就调用reject方法。

Generator

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。本章详细介绍 Generator 函数的语法和 API,它的异步编程应用请看《Generator 函数的异步应用》一章。

Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。

执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

js
function* helloWorldGenerator() {
+  yield 'hello';
+  yield 'world';
+  return 'ending';
+}
+
+var hw = helloWorldGenerator();
function* helloWorldGenerator() {
+  yield 'hello';
+  yield 'world';
+  return 'ending';
+}
+
+var hw = helloWorldGenerator();

上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(helloworld),即该函数有三个状态:hello,world 和 return 语句(结束执行)。

然后,Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)。

下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

js
hw.next()
+// { value: 'hello', done: false }
+
+hw.next()
+// { value: 'world', done: false }
+
+hw.next()
+// { value: 'ending', done: true }
+
+hw.next()
+// { value: undefined, done: true }
hw.next()
+// { value: 'hello', done: false }
+
+hw.next()
+// { value: 'world', done: false }
+
+hw.next()
+// { value: 'ending', done: true }
+
+hw.next()
+// { value: undefined, done: true }

yield 表达式

由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。

遍历器对象的next方法的运行逻辑如下。

(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。

(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

(4)如果该函数没有return语句,则返回的对象的value属性值为undefined

需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

js
function* gen() {
+  yield  123 + 456;
+}
function* gen() {
+  yield  123 + 456;
+}

上面代码中,yield后面的表达式123 + 456,不会立即求值,只会在next方法将指针移到这一句时,才会求值。

yield表达式与return语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield表达式。正常函数只能返回一个值,因为只能执行一次return;Generator 函数可以返回一系列的值,因为可以有任意多个yield。从另一个角度看,也可以说 Generator 生成了一系列的值,这也就是它的名称的来历(英语中,generator 这个词是“生成器”的意思)。

await

正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

js
async function f() {
+  // 等同于
+  // return 123;
+  return await 123;
+}
+
+f().then(v => console.log(v))
+// 123
async function f() {
+  // 等同于
+  // return 123;
+  return await 123;
+}
+
+f().then(v => console.log(v))
+// 123

上面代码中,await命令的参数是数值123,这时等同于return 123

另一种情况是,await命令后面是一个thenable对象(即定义了then方法的对象),那么await会将其等同于 Promise 对象。

js
class Sleep {
+  constructor(timeout) {
+    this.timeout = timeout;
+  }
+  then(resolve, reject) {
+    const startTime = Date.now();
+    setTimeout(
+      () => resolve(Date.now() - startTime),
+      this.timeout
+    );
+  }
+}
+
+(async () => {
+  const sleepTime = await new Sleep(1000);
+  console.log(sleepTime);
+})();
+// 1000
class Sleep {
+  constructor(timeout) {
+    this.timeout = timeout;
+  }
+  then(resolve, reject) {
+    const startTime = Date.now();
+    setTimeout(
+      () => resolve(Date.now() - startTime),
+      this.timeout
+    );
+  }
+}
+
+(async () => {
+  const sleepTime = await new Sleep(1000);
+  console.log(sleepTime);
+})();
+// 1000

上面代码中,await命令后面是一个Sleep对象的实例。这个实例不是 Promise 对象,但是因为定义了then方法,await会将其视为Promise处理。

这个例子还演示了如何实现休眠效果。JavaScript 一直没有休眠的语法,但是借助await命令就可以让程序停顿指定的时间。下面给出了一个简化的sleep实现。

js
function sleep(interval) {
+  return new Promise(resolve => {
+    setTimeout(resolve, interval);
+  })
+}
+
+// 用法
+async function one2FiveInAsync() {
+  for(let i = 1; i <= 5; i++) {
+    console.log(i);
+    await sleep(1000);
+  }
+}
+
+one2FiveInAsync();
function sleep(interval) {
+  return new Promise(resolve => {
+    setTimeout(resolve, interval);
+  })
+}
+
+// 用法
+async function one2FiveInAsync() {
+  for(let i = 1; i <= 5; i++) {
+    console.log(i);
+    await sleep(1000);
+  }
+}
+
+one2FiveInAsync();

await命令后面的 Promise 对象如果变为reject状态,则reject的参数会被catch方法的回调函数接收到。

js
async function f() {
+  await Promise.reject('出错了');
+}
+
+f()
+.then(v => console.log(v))
+.catch(e => console.log(e))
+// 出错了
async function f() {
+  await Promise.reject('出错了');
+}
+
+f()
+.then(v => console.log(v))
+.catch(e => console.log(e))
+// 出错了

注意,上面代码中,await语句前面没有return,但是reject方法的参数依然传入了catch方法的回调函数。这里如果在await前面加上return,效果是一样的。

任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。

js
async function f() {
+  await Promise.reject('出错了');
+  await Promise.resolve('hello world'); // 不会执行
+}
async function f() {
+  await Promise.reject('出错了');
+  await Promise.resolve('hello world'); // 不会执行
+}

上面代码中,第二个await语句是不会执行的,因为第一个await语句状态变成了reject

有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await放在try...catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行。

js
async function f() {
+  try {
+    await Promise.reject('出错了');
+  } catch(e) {
+  }
+  return await Promise.resolve('hello world');
+}
+
+f()
+.then(v => console.log(v))
+// hello world
async function f() {
+  try {
+    await Promise.reject('出错了');
+  } catch(e) {
+  }
+  return await Promise.resolve('hello world');
+}
+
+f()
+.then(v => console.log(v))
+// hello world

另一种方法是await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。

js
async function f() {
+  await Promise.reject('出错了')
+    .catch(e => console.log(e));
+  return await Promise.resolve('hello world');
+}
+
+f()
+.then(v => console.log(v))
+// 出错了
+// hello world
async function f() {
+  await Promise.reject('出错了')
+    .catch(e => console.log(e));
+  return await Promise.resolve('hello world');
+}
+
+f()
+.then(v => console.log(v))
+// 出错了
+// hello world

async

async 函数是什么?一句话,它就是 Generator 函数的语法糖。返回值是 Promise 对象

Generator 函数,依次读取两个文件。

js
const fs = require('fs');
+
+const readFile = function (fileName) {
+  return new Promise(function (resolve, reject) {
+    fs.readFile(fileName, function(error, data) {
+      if (error) return reject(error);
+      resolve(data);
+    });
+  });
+};
+
+const gen = function* () {
+  const f1 = yield readFile('/etc/fstab');
+  const f2 = yield readFile('/etc/shells');
+  console.log(f1.toString());
+  console.log(f2.toString());
+};
+
+const g = gen();
+g.next().value.then(function (data) {
+  g.next(data).value.then(function (data) {
+    g.next(data);
+  });
+});
const fs = require('fs');
+
+const readFile = function (fileName) {
+  return new Promise(function (resolve, reject) {
+    fs.readFile(fileName, function(error, data) {
+      if (error) return reject(error);
+      resolve(data);
+    });
+  });
+};
+
+const gen = function* () {
+  const f1 = yield readFile('/etc/fstab');
+  const f2 = yield readFile('/etc/shells');
+  console.log(f1.toString());
+  console.log(f2.toString());
+};
+
+const g = gen();
+g.next().value.then(function (data) {
+  g.next(data).value.then(function (data) {
+    g.next(data);
+  });
+});

上面代码的函数gen可以写成async函数,就是下面这样。

js
const asyncReadFile = async function () {
+  const f1 = await readFile('/etc/fstab');
+  const f2 = await readFile('/etc/shells');
+  console.log(f1.toString());
+  console.log(f2.toString());
+};
const asyncReadFile = async function () {
+  const f1 = await readFile('/etc/fstab');
+  const f2 = await readFile('/etc/shells');
+  console.log(f1.toString());
+  console.log(f2.toString());
+};

一比较就会发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。

async函数对 Generator 函数的改进,体现在以下四点。

(1)内置执行器。

Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。

js
asyncReadFile();
asyncReadFile();

上面的代码调用了asyncReadFile函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用next方法,或者用co模块,才能真正执行,得到最后结果。

(2)更好的语义。

asyncawait,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

(3)更广的适用性。

co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。

(4)返回值是 Promise。

async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

基本用法

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

下面是一个例子。

js
async function getStockPriceByName(name) {
+  const symbol = await getStockSymbol(name);
+  const stockPrice = await getStockPrice(symbol);
+  return stockPrice;
+}
+
+getStockPriceByName('goog').then(function (result) {
+  console.log(result);
+});
async function getStockPriceByName(name) {
+  const symbol = await getStockSymbol(name);
+  const stockPrice = await getStockPrice(symbol);
+  return stockPrice;
+}
+
+getStockPriceByName('goog').then(function (result) {
+  console.log(result);
+});

上面代码是一个获取股票报价的函数,函数前面的async关键字,表明该函数内部有异步操作。调用该函数时,会立即返回一个Promise对象。

下面是另一个例子,指定多少毫秒后输出一个值。

js
function timeout(ms) {
+  return new Promise((resolve) => {
+    setTimeout(resolve, ms);
+  });
+}
+
+async function asyncPrint(value, ms) {
+  await timeout(ms);
+  console.log(value);
+}
+
+asyncPrint('hello world', 50);
function timeout(ms) {
+  return new Promise((resolve) => {
+    setTimeout(resolve, ms);
+  });
+}
+
+async function asyncPrint(value, ms) {
+  await timeout(ms);
+  console.log(value);
+}
+
+asyncPrint('hello world', 50);

上面代码指定 50 毫秒以后,输出hello world

由于async函数返回的是 Promise 对象,可以作为await命令的参数。所以,上面的例子也可以写成下面的形式。

js
async function timeout(ms) {
+  await new Promise((resolve) => {
+    setTimeout(resolve, ms);
+  });
+}
+
+async function asyncPrint(value, ms) {
+  await timeout(ms);
+  console.log(value);
+}
+
+asyncPrint('hello world', 50);
async function timeout(ms) {
+  await new Promise((resolve) => {
+    setTimeout(resolve, ms);
+  });
+}
+
+async function asyncPrint(value, ms) {
+  await timeout(ms);
+  console.log(value);
+}
+
+asyncPrint('hello world', 50);

async 函数有多种使用形式。

js
// 函数声明
+async function foo() {}
+
+// 函数表达式
+const foo = async function () {};
+
+// 对象的方法
+let obj = { async foo() {} };
+obj.foo().then(...)
+
+// Class 的方法
+class Storage {
+  constructor() {
+    this.cachePromise = caches.open('avatars');
+  }
+
+  async getAvatar(name) {
+    const cache = await this.cachePromise;
+    return cache.match(`/avatars/${name}.jpg`);
+  }
+}
+
+const storage = new Storage();
+storage.getAvatar('jake').then(…);
+
+// 箭头函数
+const foo = async () => {};
// 函数声明
+async function foo() {}
+
+// 函数表达式
+const foo = async function () {};
+
+// 对象的方法
+let obj = { async foo() {} };
+obj.foo().then(...)
+
+// Class 的方法
+class Storage {
+  constructor() {
+    this.cachePromise = caches.open('avatars');
+  }
+
+  async getAvatar(name) {
+    const cache = await this.cachePromise;
+    return cache.match(`/avatars/${name}.jpg`);
+  }
+}
+
+const storage = new Storage();
+storage.getAvatar('jake').then(…);
+
+// 箭头函数
+const foo = async () => {};

语法

返回Promise对象

async函数返回一个 Promise 对象。

async函数内部return语句返回的值,会成为then方法回调函数的参数。

js
async function f() {
+  return 'hello world';
+}
+
+f().then(v => console.log(v))
+// "hello world"
async function f() {
+  return 'hello world';
+}
+
+f().then(v => console.log(v))
+// "hello world"

上面代码中,函数f内部return命令返回的值,会被then方法回调函数接收到。

async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。

js
async function f() {
+  throw new Error('出错了');
+}
+
+f().then(
+  v => console.log('resolve', v),
+  e => console.log('reject', e)
+)
+//reject Error: 出错了
async function f() {
+  throw new Error('出错了');
+}
+
+f().then(
+  v => console.log('resolve', v),
+  e => console.log('reject', e)
+)
+//reject Error: 出错了

Promise对象状态变化

async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

下面是一个例子。

js
async function getTitle(url) {
+  let response = await fetch(url);
+  let html = await response.text();
+  return html.match(/<title>([\s\S]+)<\/title>/i)[1];
+}
+getTitle('https://tc39.github.io/ecma262/').then(console.log)
+// "ECMAScript 2017 Language Specification"
async function getTitle(url) {
+  let response = await fetch(url);
+  let html = await response.text();
+  return html.match(/<title>([\s\S]+)<\/title>/i)[1];
+}
+getTitle('https://tc39.github.io/ecma262/').then(console.log)
+// "ECMAScript 2017 Language Specification"

上面代码中,函数getTitle内部有三个操作:抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行then方法里面的console.log

错误处理

如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject

js
async function f() {
+  await new Promise(function (resolve, reject) {
+    throw new Error('出错了');
+  });
+}
+
+f()
+.then(v => console.log(v))
+.catch(e => console.log(e))
+// Error:出错了
async function f() {
+  await new Promise(function (resolve, reject) {
+    throw new Error('出错了');
+  });
+}
+
+f()
+.then(v => console.log(v))
+.catch(e => console.log(e))
+// Error:出错了

上面代码中,async函数f执行后,await后面的 Promise 对象会抛出一个错误对象,导致catch方法的回调函数被调用,它的参数就是抛出的错误对象。

防止出错的方法,也是将其放在try...catch代码块之中。

js
async function f() {
+  try {
+    await new Promise(function (resolve, reject) {
+      throw new Error('出错了');
+    });
+  } catch(e) {
+  }
+  return await('hello world');
+}
async function f() {
+  try {
+    await new Promise(function (resolve, reject) {
+      throw new Error('出错了');
+    });
+  } catch(e) {
+  }
+  return await('hello world');
+}

如果有多个await命令,可以统一放在try...catch结构中。

js
async function main() {
+  try {
+    const val1 = await firstStep();
+    const val2 = await secondStep(val1);
+    const val3 = await thirdStep(val1, val2);
+
+    console.log('Final: ', val3);
+  }
+  catch (err) {
+    console.error(err);
+  }
+}
async function main() {
+  try {
+    const val1 = await firstStep();
+    const val2 = await secondStep(val1);
+    const val3 = await thirdStep(val1, val2);
+
+    console.log('Final: ', val3);
+  }
+  catch (err) {
+    console.error(err);
+  }
+}

下面的例子使用try...catch结构,实现多次重复尝试。

js
const superagent = require('superagent');
+const NUM_RETRIES = 3;
+
+async function test() {
+  let i;
+  for (i = 0; i < NUM_RETRIES; ++i) {
+    try {
+      await superagent.get('http://google.com/this-throws-an-error');
+      break;
+    } catch(err) {}
+  }
+  console.log(i); // 3
+}
+
+test();
const superagent = require('superagent');
+const NUM_RETRIES = 3;
+
+async function test() {
+  let i;
+  for (i = 0; i < NUM_RETRIES; ++i) {
+    try {
+      await superagent.get('http://google.com/this-throws-an-error');
+      break;
+    } catch(err) {}
+  }
+  console.log(i); // 3
+}
+
+test();

上面代码中,如果await操作成功,就会使用break语句退出循环;如果失败,会被catch语句捕捉,然后进入下一轮循环。

async 函数的实现原理

async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。

js
async function fn(args) {
+  // ...
+}
+
+// 等同于
+
+function fn(args) {
+  return spawn(function* () {
+    // ...
+  });
+}
async function fn(args) {
+  // ...
+}
+
+// 等同于
+
+function fn(args) {
+  return spawn(function* () {
+    // ...
+  });
+}

所有的async函数都可以写成上面的第二种形式,其中的spawn函数就是自动执行器。

spawn函数的实现

js
function spawn(genF) {
+  return new Promise(function(resolve, reject) {
+    const gen = genF();
+    function step(nextF) {
+      let next;
+      try {
+        next = nextF();
+      } catch(e) {
+        return reject(e);
+      }
+      if(next.done) {
+        return resolve(next.value);
+      }
+      Promise.resolve(next.value).then(function(v) {
+        step(function() { return gen.next(v); });
+      }, function(e) {
+        step(function() { return gen.throw(e); });
+      });
+    }
+    step(function() { return gen.next(undefined); });
+  });
+}
function spawn(genF) {
+  return new Promise(function(resolve, reject) {
+    const gen = genF();
+    function step(nextF) {
+      let next;
+      try {
+        next = nextF();
+      } catch(e) {
+        return reject(e);
+      }
+      if(next.done) {
+        return resolve(next.value);
+      }
+      Promise.resolve(next.value).then(function(v) {
+        step(function() { return gen.next(v); });
+      }, function(e) {
+        step(function() { return gen.throw(e); });
+      });
+    }
+    step(function() { return gen.next(undefined); });
+  });
+}

实例:按顺序完成异步操作

实际开发中,经常遇到一组异步操作,需要按照顺序完成。比如,依次远程读取一组 URL,然后按照读取的顺序输出结果。

Promise 的写法如下。

js
function logInOrder(urls) {
+  // 远程读取所有URL
+  const textPromises = urls.map(url => {
+    return fetch(url).then(response => response.text());
+  });
+
+  // 按次序输出
+  textPromises.reduce((chain, textPromise) => {
+    return chain.then(() => textPromise)
+      .then(text => console.log(text));
+  }, Promise.resolve());
+}
function logInOrder(urls) {
+  // 远程读取所有URL
+  const textPromises = urls.map(url => {
+    return fetch(url).then(response => response.text());
+  });
+
+  // 按次序输出
+  textPromises.reduce((chain, textPromise) => {
+    return chain.then(() => textPromise)
+      .then(text => console.log(text));
+  }, Promise.resolve());
+}

上面代码使用fetch方法,同时远程读取一组 URL。每个fetch操作都返回一个 Promise 对象,放入textPromises数组。然后,reduce方法依次处理每个 Promise 对象,然后使用then,将所有 Promise 对象连起来,因此就可以依次输出结果。

这种写法不太直观,可读性比较差。下面是 async 函数实现。

js
async function logInOrder(urls) {
+  for (const url of urls) {
+    const response = await fetch(url);
+    console.log(await response.text());
+  }
+}
async function logInOrder(urls) {
+  for (const url of urls) {
+    const response = await fetch(url);
+    console.log(await response.text());
+  }
+}

上面代码确实大大简化,问题是所有远程操作都是继发。只有前一个 URL 返回结果,才会去读取下一个 URL,这样做效率很差,非常浪费时间。我们需要的是并发发出远程请求。

js
async function logInOrder(urls) {
+  // 并发读取远程URL
+  const textPromises = urls.map(async url => {
+    const response = await fetch(url);
+    return response.text();
+  });
+
+  // 按次序输出
+  for (const textPromise of textPromises) {
+    console.log(await textPromise);
+  }
+}
async function logInOrder(urls) {
+  // 并发读取远程URL
+  const textPromises = urls.map(async url => {
+    const response = await fetch(url);
+    return response.text();
+  });
+
+  // 按次序输出
+  for (const textPromise of textPromises) {
+    console.log(await textPromise);
+  }
+}

上面代码中,虽然map方法的参数是async函数,但它是并发执行的,因为只有async函数内部是继发执行,外部不受影响。后面的for..of循环内部使用了await,因此实现了按顺序输出。

Class

基本语法

JavaScript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子。

js
function Point(x, y) {
+  this.x = x;
+  this.y = y;
+}
+
+Point.prototype.toString = function () {
+  return '(' + this.x + ', ' + this.y + ')';
+};
+
+var p = new Point(1, 2);
function Point(x, y) {
+  this.x = x;
+  this.y = y;
+}
+
+Point.prototype.toString = function () {
+  return '(' + this.x + ', ' + this.y + ')';
+};
+
+var p = new Point(1, 2);

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。

基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用 ES6 的class改写,就是下面这样。

js
class Point {
+  constructor(x, y) {
+    this.x = x;
+    this.y = y;
+  }
+
+  toString() {
+    return '(' + this.x + ', ' + this.y + ')';
+  }
+}
class Point {
+  constructor(x, y) {
+    this.x = x;
+    this.y = y;
+  }
+
+  toString() {
+    return '(' + this.x + ', ' + this.y + ')';
+  }
+}

上面代码定义了一个“类”,可以看到里面有一个constructor()方法,这就是构造方法,而this关键字则代表实例对象。这种新的 Class 写法,本质上与本章开头的 ES5 的构造函数Point是一致的。

Point类除了构造方法,还定义了一个toString()方法。注意,定义toString()方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法与方法之间不需要逗号分隔,加了会报错。

ES6 的类,完全可以看作构造函数的另一种写法。

js
class Point {
+  // ...
+}
+
+typeof Point // "function"
+Point === Point.prototype.constructor // true
class Point {
+  // ...
+}
+
+typeof Point // "function"
+Point === Point.prototype.constructor // true

上面代码表明,类的数据类型就是函数,类本身就指向构造函数。

使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。

js
class Bar {
+  doStuff() {
+    console.log('stuff');
+  }
+}
+
+const b = new Bar();
+b.doStuff() // "stuff"
class Bar {
+  doStuff() {
+    console.log('stuff');
+  }
+}
+
+const b = new Bar();
+b.doStuff() // "stuff"

构造函数的prototype属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。

js
class Point {
+  constructor() {
+    // ...
+  }
+
+  toString() {
+    // ...
+  }
+
+  toValue() {
+    // ...
+  }
+}
+
+// 等同于
+
+Point.prototype = {
+  constructor() {},
+  toString() {},
+  toValue() {},
+};
class Point {
+  constructor() {
+    // ...
+  }
+
+  toString() {
+    // ...
+  }
+
+  toValue() {
+    // ...
+  }
+}
+
+// 等同于
+
+Point.prototype = {
+  constructor() {},
+  toString() {},
+  toValue() {},
+};

上面代码中,constructor()toString()toValue()这三个方法,其实都是定义在Point.prototype上面。

实例属性新写法

ES2022 为类的实例属性,又规定了一种新写法。实例属性现在除了可以定义在constructor()方法里面的this上面,也可以定义在类内部的最顶层。

js
// 原来的写法
+class IncreasingCounter {
+  constructor() {
+    this._count = 0;
+  }
+  get value() {
+    console.log('Getting the current value!');
+    return this._count;
+  }
+  increment() {
+    this._count++;
+  }
+}
// 原来的写法
+class IncreasingCounter {
+  constructor() {
+    this._count = 0;
+  }
+  get value() {
+    console.log('Getting the current value!');
+    return this._count;
+  }
+  increment() {
+    this._count++;
+  }
+}

上面示例中,实例属性_count定义在constructor()方法里面的this上面。

现在的新写法是,这个属性也可以定义在类的最顶层,其他都不变。

js
class IncreasingCounter {
+  _count = 0;
+  get value() {
+    console.log('Getting the current value!');
+    return this._count;
+  }
+  increment() {
+    this._count++;
+  }
+}
class IncreasingCounter {
+  _count = 0;
+  get value() {
+    console.log('Getting the current value!');
+    return this._count;
+  }
+  increment() {
+    this._count++;
+  }
+}

上面代码中,实例属性_count与取值函数value()increment()方法,处于同一个层级。这时,不需要在实例属性前面加上this

注意,新写法定义的属性是实例对象自身的属性,而不是定义在实例对象的原型上面。

这种新写法的好处是,所有实例对象自身的属性都定义在类的头部,看上去比较整齐,一眼就能看出这个类有哪些实例属性。

js
class foo {
+  bar = 'hello';
+  baz = 'world';
+
+  constructor() {
+    // ...
+  }
+}
class foo {
+  bar = 'hello';
+  baz = 'world';
+
+  constructor() {
+    // ...
+  }
+}

上面的代码,一眼就能看出,foo类有两个实例属性,一目了然。另外,写起来也比较简洁。

getter和setter

与 ES5 一样,在“类”的内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

js
class MyClass {
+  constructor() {
+    // ...
+  }
+  get prop() {
+    return 'getter';
+  }
+  set prop(value) {
+    console.log('setter: '+value);
+  }
+}
+
+let inst = new MyClass();
+
+inst.prop = 123;
+// setter: 123
+
+inst.prop
+// 'getter'
class MyClass {
+  constructor() {
+    // ...
+  }
+  get prop() {
+    return 'getter';
+  }
+  set prop(value) {
+    console.log('setter: '+value);
+  }
+}
+
+let inst = new MyClass();
+
+inst.prop = 123;
+// setter: 123
+
+inst.prop
+// 'getter'

上面代码中,prop属性有对应的存值函数和取值函数,因此赋值和读取行为都被自定义了。

存值函数和取值函数是设置在属性的 Descriptor 对象上的。

js
class CustomHTMLElement {
+  constructor(element) {
+    this.element = element;
+  }
+
+  get html() {
+    return this.element.innerHTML;
+  }
+
+  set html(value) {
+    this.element.innerHTML = value;
+  }
+}
+
+var descriptor = Object.getOwnPropertyDescriptor(
+  CustomHTMLElement.prototype, "html"
+);
+
+"get" in descriptor  // true
+"set" in descriptor  // true
class CustomHTMLElement {
+  constructor(element) {
+    this.element = element;
+  }
+
+  get html() {
+    return this.element.innerHTML;
+  }
+
+  set html(value) {
+    this.element.innerHTML = value;
+  }
+}
+
+var descriptor = Object.getOwnPropertyDescriptor(
+  CustomHTMLElement.prototype, "html"
+);
+
+"get" in descriptor  // true
+"set" in descriptor  // true

上面代码中,存值函数和取值函数是定义在html属性的描述对象上面,这与 ES5 完全一致。

属性表达式

类的属性名,可以采用表达式。

js
let methodName = 'getArea';
+
+class Square {
+  constructor(length) {
+    // ...
+  }
+
+  [methodName]() {
+    // ...
+  }
+}
let methodName = 'getArea';
+
+class Square {
+  constructor(length) {
+    // ...
+  }
+
+  [methodName]() {
+    // ...
+  }
+}

上面代码中,Square类的方法名getArea,是从表达式得到的。

Class表达式

与函数一样,类也可以使用表达式的形式定义。

js
const MyClass = class Me {
+  getClassName() {
+    return Me.name;
+  }
+};
const MyClass = class Me {
+  getClassName() {
+    return Me.name;
+  }
+};

上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是Me,但是Me只在 Class 的内部可用,指代当前类。在 Class 外部,这个类只能用MyClass引用。

静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

js
class Foo {
+  static classMethod() {
+    return 'hello';
+  }
+}
+
+Foo.classMethod() // 'hello'
+
+var foo = new Foo();
+foo.classMethod()
+// TypeError: foo.classMethod is not a function
class Foo {
+  static classMethod() {
+    return 'hello';
+  }
+}
+
+Foo.classMethod() // 'hello'
+
+var foo = new Foo();
+foo.classMethod()
+// TypeError: foo.classMethod is not a function

上面代码中,Foo类的classMethod方法前有static关键字,表明该方法是一个静态方法,可以直接在Foo类上调用(Foo.classMethod()),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。

注意,如果静态方法包含this关键字,这个this指的是类,而不是实例。

静态方法可以与非静态方法重名。

父类的静态方法,可以被子类继承。

静态属性

静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。

js
class Foo {
+}
+
+Foo.prop = 1;
+Foo.prop // 1
class Foo {
+}
+
+Foo.prop = 1;
+Foo.prop // 1

上面的写法为Foo类定义了一个静态属性prop

私有方法和属性

在属性名之前使用#表示。

in运算符

Class的继承

Class 可以通过extends关键字实现继承,让子类继承父类的属性和方法。extends 的写法比 ES5 的原型链继承,要清晰和方便很多。

js
class Point {
+}
+
+class ColorPoint extends Point {
+}
class Point {
+}
+
+class ColorPoint extends Point {
+}
  • 在子类的构造函数中,只有调用super()之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,必须先完成父类的继承,只有super()方法才能让子类实例继承父类。

  • 父类所有的属性和方法,都会被子类继承,除了私有的属性和方法。

  • 父类的静态属性和静态方法,也会被子类继承。

  • super关键字,既可以当作函数使用,也可以当作对象使用。

  • 大多数浏览器的 ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。

​ (1)子类的__proto__属性,表示构造函数的继承,总是指向父类。

​ (2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

  • 子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。也就是说,子类的原型的原型,是父类的原型。

Module

CommonJS 模块就是对象,输入时必须查找对象属性。

js
// CommonJS模块
+let { stat, exists, readfile } = require('fs');
+
+// 等同于
+let _fs = require('fs');
+let stat = _fs.stat;
+let exists = _fs.exists;
+let readfile = _fs.readfile;
// CommonJS模块
+let { stat, exists, readfile } = require('fs');
+
+// 等同于
+let _fs = require('fs');
+let stat = _fs.stat;
+let exists = _fs.exists;
+let readfile = _fs.readfile;

上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。

ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

js
// ES6模块
+import { stat, exists, readFile } from 'fs';
// ES6模块
+import { stat, exists, readFile } from 'fs';

上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。

由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。

严格模式

ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";

export

模块功能主要由两个命令构成:exportimportexport命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个 JS 文件,里面使用export命令输出变量。

js
// profile.js
+export var firstName = 'Michael';
+export var lastName = 'Jackson';
+export var year = 1958;
// profile.js
+export var firstName = 'Michael';
+export var lastName = 'Jackson';
+export var year = 1958;

上面代码是profile.js文件,保存了用户信息。ES6 将其视为一个模块,里面用export命令对外部输出了三个变量。

export的写法,除了像上面这样,还有另外一种。

js
// profile.js
+var firstName = 'Michael';
+var lastName = 'Jackson';
+var year = 1958;
+
+export { firstName, lastName, year };
// profile.js
+var firstName = 'Michael';
+var lastName = 'Jackson';
+var year = 1958;
+
+export { firstName, lastName, year };

上面代码在export命令后面,使用大括号指定所要输出的一组变量。它与前一种写法(直接放置在var语句前)是等价的,但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。

export命令除了输出变量,还可以输出函数或类(class)。

js
export function multiply(x, y) {
+  return x * y;
+};
export function multiply(x, y) {
+  return x * y;
+};

上面代码对外输出一个函数multiply

通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。

js
function v1() { ... }
+function v2() { ... }
+
+export {
+  v1 as streamV1,
+  v2 as streamV2,
+  v2 as streamLatestVersion
+};
function v1() { ... }
+function v2() { ... }
+
+export {
+  v1 as streamV1,
+  v2 as streamV2,
+  v2 as streamLatestVersion
+};

上面代码使用as关键字,重命名了函数v1v2的对外接口。重命名后,v2可以用不同的名字输出两次。

需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

import

使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。

js
// main.js
+import { firstName, lastName, year } from './profile.js';
+
+function setName(element) {
+  element.textContent = firstName + ' ' + lastName;
+}
// main.js
+import { firstName, lastName, year } from './profile.js';
+
+function setName(element) {
+  element.textContent = firstName + ' ' + lastName;
+}

面代码的import命令,用于加载profile.js文件,并从中输入变量。import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。

如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。

js
import { lastName as surname } from './profile.js';
import { lastName as surname } from './profile.js';

import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。

js
import {a} from './xxx.js'
+
+a = {}; // Syntax Error : 'a' is read-only;
import {a} from './xxx.js'
+
+a = {}; // Syntax Error : 'a' is read-only;

上面代码中,脚本加载了变量a,对其重新赋值就会报错,因为a是一个只读的接口。但是,如果a是一个对象,改写a的属性是允许的。

js
import {a} from './xxx.js'
+
+a.foo = 'hello'; // 合法操作
import {a} from './xxx.js'
+
+a.foo = 'hello'; // 合法操作

上面代码中,a的属性可以成功改写,并且其他模块也可以读到改写后的值。不过,这种写法很难查错,建议凡是输入的变量,都当作完全只读,不要轻易改变它的属性。

import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径。如果不带有路径,只是一个模块名,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。

js
import { myMethod } from 'util';
import { myMethod } from 'util';

上面代码中,util是模块文件名,由于不带有路径,必须通过配置,告诉引擎怎么取到这个模块。

除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。

js
import * as circle from './circle';
+
+console.log('圆面积:' + circle.area(4));
+console.log('圆周长:' + circle.circumference(14));
import * as circle from './circle';
+
+console.log('圆面积:' + circle.area(4));
+console.log('圆周长:' + circle.circumference(14));

export default

使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。

为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

js
// export-default.js
+export default function () {
+  console.log('foo');
+}
// export-default.js
+export default function () {
+  console.log('foo');
+}

上面代码是一个模块文件export-default.js,它的默认输出是一个函数。

其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

js
// import-default.js
+import customName from './export-default';
+customName(); // 'foo'
// import-default.js
+import customName from './export-default';
+customName(); // 'foo'

上面代码的import命令,可以用任意名称指向export-default.js输出的方法,这时就不需要知道原模块输出的函数名。需要注意的是,这时import命令后面,不使用大括号。

export default命令用在非匿名函数前,也是可以的。

js
// export-default.js
+export default function foo() {
+  console.log('foo');
+}
+
+// 或者写成
+
+function foo() {
+  console.log('foo');
+}
+
+export default foo;
// export-default.js
+export default function foo() {
+  console.log('foo');
+}
+
+// 或者写成
+
+function foo() {
+  console.log('foo');
+}
+
+export default foo;

上面代码中,foo函数的函数名foo,在模块外部是无效的。加载的时候,视同匿名函数加载。

export和import复合写法

js
export { foo, bar } from 'my_module';
+
+// 可以简单理解为
+import { foo, bar } from 'my_module';
+export { foo, bar };
export { foo, bar } from 'my_module';
+
+// 可以简单理解为
+import { foo, bar } from 'my_module';
+export { foo, bar };

上面代码中,exportimport语句可以结合在一起,写成一行。但需要注意的是,写成一行以后,foobar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foobar

模块的接口改名和整体输出,也可以采用这种写法。

js
// 接口改名
+export { foo as myFoo } from 'my_module';
+
+// 整体输出
+export * from 'my_module';
// 接口改名
+export { foo as myFoo } from 'my_module';
+
+// 整体输出
+export * from 'my_module';

默认接口的写法如下。

js
export { default } from 'foo';
export { default } from 'foo';

具名接口改为默认接口的写法如下。

js
export { es6 as default } from './someModule';
+
+// 等同于
+import { es6 } from './someModule';
+export default es6;
export { es6 as default } from './someModule';
+
+// 等同于
+import { es6 } from './someModule';
+export default es6;

同样地,默认接口也可以改名为具名接口。

js
export { default as es6 } from './someModule';
export { default as es6 } from './someModule';

跨模块常量

js
// constants.js 模块
+export const A = 1;
+export const B = 3;
+export const C = 4;
+
+// test1.js 模块
+import * as constants from './constants';
+console.log(constants.A); // 1
+console.log(constants.B); // 3
+
+// test2.js 模块
+import {A, B} from './constants';
+console.log(A); // 1
+console.log(B); // 3
// constants.js 模块
+export const A = 1;
+export const B = 3;
+export const C = 4;
+
+// test1.js 模块
+import * as constants from './constants';
+console.log(constants.A); // 1
+console.log(constants.B); // 3
+
+// test2.js 模块
+import {A, B} from './constants';
+console.log(A); // 1
+console.log(B); // 3

模块的继承

circleplus模块,继承了circle模块。

js
// circleplus.js
+
+export * from 'circle';
+export var e = 2.71828182846;
+export default function(x) {
+  return Math.exp(x);
+}
// circleplus.js
+
+export * from 'circle';
+export var e = 2.71828182846;
+export default function(x) {
+  return Math.exp(x);
+}

这时,也可以将circle的属性或方法,改名后再输出。

js
// circleplus.js
+
+export { area as circleArea } from 'circle';
// circleplus.js
+
+export { area as circleArea } from 'circle';

上面代码表示,只输出circle模块的area方法,且将其改名为circleArea

加载上面模块的写法如下。

js
// main.js
+
+import * as math from 'circleplus';
+import exp from 'circleplus';
+console.log(exp(math.e));
// main.js
+
+import * as math from 'circleplus';
+import exp from 'circleplus';
+console.log(exp(math.e));

上面代码中的import exp表示,将circleplus模块的默认方法加载为exp方法。

+ + + + \ No newline at end of file diff --git a/frontend/base/Nodejs.html b/frontend/base/Nodejs.html new file mode 100644 index 000000000..6284d0bd5 --- /dev/null +++ b/frontend/base/Nodejs.html @@ -0,0 +1,53 @@ + + + + + + Node.js | 故事 + + + + + + + + + + + + + + +
Skip to content

Node.js

Node.js简介

Node.js是一个基于Chrome V8引擎的JavaScript运行环境。

浏览器是js的前端运行环境

Node.js是js的后端运行环境

Node.js中的JavaScript运行环境

  • V8引擎
  • 内置API
    • fs、path、http、JS、querystring。。。

Node.js可以做什么

  • 基于Express可以构建Web应用
  • 基于Electron可以构建跨平台桌面应用
  • 基于restify可以构建API接口项目
  • 读写和操作数据库、创建实用的命令行工具辅助开发。。

基础模块

fs模块

  • fs.readFile()
    • fs.readFile(path[, options], callback)
  • fs.writeFile()
    • fs.writeFile(file,data[,options], callback)
  • 动态拼接路径问题:__dirname替代当前文件所处目录
    • __dirname + '/file/1.txt'

path模块

  • path.join()
  • path.basename()

http模块

创建基本的web服务器

js
const http = require('http')
+
+const server = http.createServer()
+server.on('request', (req, res) => {
+  console.log('visit')
+  res.setHeader('Content-Type', 'text/html; charset=utf-8')
+  res.end('111')
+})
+
+server.listen(80, () => {
+  console.log("http server running at http://127.0.0.1")
+})
const http = require('http')
+
+const server = http.createServer()
+server.on('request', (req, res) => {
+  console.log('visit')
+  res.setHeader('Content-Type', 'text/html; charset=utf-8')
+  res.end('111')
+})
+
+server.listen(80, () => {
+  console.log("http server running at http://127.0.0.1")
+})
  • req
    • url:请求url
    • method:请求方法
  • res
    • end():发送响应

模块化

模块化是指解决一个复杂问题时,自顶向下逐层把系统划分为若干模块的过程,对于整个系统来说,模块是可组合、分解和更换的单元。

模块化优势:

  • 提高复用性
  • 提高可维护性
  • 可以实现按需加载

Node.js模块分类

  • 内置模块
  • 自定义模块
  • 第三方模块

加载模块

  • const moduleName = require('moduleName')

使用require加载模块时会执行被加载模块的代码

使用自定义模块可以省略.js

模块作用域

在自定义模块中定义的变量、方法等成员只能在当前模块内被访问,这种模块级别的访问限制,叫做模块作用域。

好处:防止全局变量污染

向外共享模块作用域中的成员

  • module对象:在每个.js自定义模块中都有一个module对象,里面存储了和当前模块有关的信息

    • console.log(module)
  • module.exports:可以使用此对象将模块内的成员共享出去供外部使用,外界使用require()导入自定义模块时,得到的就是module.exports所指向的对象

    • js
      module.exports.username = 'test'
      +module.exports.sayHello = function() {
      +  console.log('hello')
      +}
      module.exports.username = 'test'
      +module.exports.sayHello = function() {
      +  console.log('hello')
      +}

exports对象

exports对象是module.exports的简化写法,两者指向同一个对象

模块化规范

Node.js遵循Common.js的模块化规范

CommonJS规定

  • 每个模块内部,module代表当前模块
  • module变量是一个对象,它的exports属性是对外的接口
  • 加载某个模块,其实是加载该模块的module.exports属性。require()方法用于加载模块

npm和包

npm是Node.js的包管理工具

  • npm install

  • npm uninstall

  • npm install package

  • npm i package

  • npm i package@version

包版本的语义化规范

  • 点分十进制,总共三位数字,例如2.14.0

  • 第一位数:大版本

  • 第二位数:功能版本

  • 第三位数:bug修复版本

  • 版本号提升规则:前面版本增长,后面版本号归零

安装包后多出来的文件

  • node_modules: 存放所有已安装到项目中的包
  • package-lock.json:记录node_modules目录下每一个包的下载信息,例如包的名字、版本号、下载地址等

包管理配置文件

npm规定,项目根目录必须提供package.json的包管理配置文件。用来记录与项目有关的配置信息,例如:

  • 项目的名称、版本号、描述
  • 项目中用到了哪些包
  • 哪些包在开发期间会用到
  • 哪些包在开发和部署时都会用到

npm创建package.json命令:npm init -y

运行npm install时,npm会自动把包名、版本记录到package.json中

dependencies节点

package.json中有一个dependencies节点专门记录npm install安装了哪些包

devDependencies节点

记录只在开发阶段使用、上线不会用到的包

  • 安装到dev节点中:npm i packageName -D / npm install packageName --save-dev

修改镜像

  • npm config get registry

  • npm config set registry=https://registry.npm.taobao.org/

  • nrm工具切换镜像源

    • npm i nrm -g
    • nrm ls
    • nrm use taobao

包分类

  • 项目包:被安装到node_modules的都是项目包
    • 开发依赖包
    • 核心依赖包
  • 全局包:执行npm install时指定-g参数则会安装为全局包
    • 一般为工具性质的包
    • npm install package -g
    • npm uninstall package -g

i5ting_toc: md转换为html工具

i5ting_toc -f md -o

包结构

规范的包结构必须符合:

  • 包必须单独目录
  • 根目录必须包含package.json
  • package.json必须包含name、version、main三个属性,对应包名、版本号、包的入口

模块的加载机制

  • 优先从缓存中加载:模块在第一次被加载后会被缓存,意味着多次调用require()不会导致模块的代码被多次执行
  • 内置模块的加载优先级最高
  • require()加载自定义模块必需指定./../开头的路径标识符,否则会被当作内置模块或第三方模块,同时如果导入时省略了扩展名,Node.js会按顺序尝试加载以下文件:
    • 按确切文件名加载
    • 补全.js加载
    • 补全.json加载
    • 补全.node记载
    • 加载失败,报错
  • 如果required()的标识符不是内置模块也不是./开头的路径标识符会被当作第三方模块,会从/node_modules中加载第三方模块
    • 如果没有找到,就移动到上一层进行加载,直到文件系统根目录,找不到报错
  • 目录作为模块标识符的加载顺序
    • 在目录中找package.json的main属性做为require的加载入口
    • 然后找根目录的index.js
    • 都找不到报错
+ + + + \ No newline at end of file diff --git a/frontend/base/Typescript.html b/frontend/base/Typescript.html new file mode 100644 index 000000000..e4323b3f9 --- /dev/null +++ b/frontend/base/Typescript.html @@ -0,0 +1,1395 @@ + + + + + + TypeScript | 故事 + + + + + + + + + + + + + + +
Skip to content

TypeScript

语法

原始类型

  • 字符串
    • 支持模板字符串赋值
  • 布尔
  • 数字
    • 支持十进制、十六进制、二进制、八进制、NaN、Infinity
  • Null和Undefined

Any

在编程阶段还不清楚类型的变量指定的一个类型

Void

某种程度上来说,void类型像是与any类型相反,它表示没有任何类型。

Null和Undefined

TypeScript里,undefinednull两者各自有自己的类型分别叫做undefinednull。 和 void相似,它们的本身的类型用处不是很大。

默认情况下nullundefined是所有类型的子类型。 就是说你可以把 nullundefined赋值给number类型的变量。

然而,当指定了--strictNullChecks标记,nullundefined只能赋值给void和它们各自。 这能避免 很多常见的问题。

Object

  • Object:包含所有类型
  • object:表示非原始类型也就是除numberstringbooleansymbolnullundefined之外的类型。
  • {}:同new Object(),包含所有类型,但无法修改属性、赋值等

接口和对象

typescript中定义对象的方式是用interface,定义一种约束,让数据结构满足约束格式

typescript
interface Man extends Person{
+  age?:number
+  [propName:string]:any
+  readonly id:number
+}
+
+interface Person {
+  name:string
+}
+
+let p:Man = {
+  name: 'zs',
+  id: 1,
+  age: 18,
+  a:1,
+  b:2
+}
+  
+  
+interface Fn {
+  (name:string):number[]
+}
+
+const fn:Fn = function(name:string) {
+  return [1]
+}
interface Man extends Person{
+  age?:number
+  [propName:string]:any
+  readonly id:number
+}
+
+interface Person {
+  name:string
+}
+
+let p:Man = {
+  name: 'zs',
+  id: 1,
+  age: 18,
+  a:1,
+  b:2
+}
+  
+  
+interface Fn {
+  (name:string):number[]
+}
+
+const fn:Fn = function(name:string) {
+  return [1]
+}
  • 对象属性必须和interface完全一致
  • 重名的interface会被合并
  • 任意key(索引签名)
  • 可选?
  • 只读readonly
  • 定义函数类型

数组

  • 元素类型后加[],例如:number[]string[]
  • 数组范型:Array<number>
  • 定义对象数组用interface
  • 二维数组:let arr:number[][] = [[1], [2]]
  • 一把梭:any[]

函数

typescript
//返回值
+function add(a:number, b:number): number {
+  return a+b
+}
+//箭头函数和定义返回值
+const add = (a:number,b:number):number => a+b 
+
+//默认参数
+function add(a:number = 10, b:number = 20) {
+  return a+b
+}
+//可选参数
+function add(a:number = 10, b?:number): number{
+  return a+b
+}
+//传对象
+interface User {
+  name:string
+  age:nubmer
+}
+
+function add(user: User): User{
+  return user
+}
+console.log(add({ name: "111", age: 18}))
+
+//ts可以定义this的类型,在js中无法使用,必须是第一个参数定义this的类型(有点类似python里面的self?)
+interface Obj {
+  user:number[]
+  add:(this:Obj,num:number)=>void
+}
+
+let obj:Obj = {
+  user:[1,2,3],
+  add(this:Obj,num:number) {
+    this.user.push(num)
+  }
+}
+obj.add(4)
+console.log(obj)
+
+//函数重载
+let user:number[] = [1,2,3]
+
+
+function findNum():number[]
+function findNum(id:number):number[] 
+function findNum(add:number[]):number[]
+
+//跟据入参走不同逻辑
+function findNum(ids?:number | number[]):number[] {
+  if(typeof ids == 'number') {
+    return user.filter(v=> v == ids)
+  }else if (Array.isArray(ids)) {
+    user.push(...ids)
+    return user
+  }else {
+    return user
+  }
+}
+console.log(findNum())
//返回值
+function add(a:number, b:number): number {
+  return a+b
+}
+//箭头函数和定义返回值
+const add = (a:number,b:number):number => a+b 
+
+//默认参数
+function add(a:number = 10, b:number = 20) {
+  return a+b
+}
+//可选参数
+function add(a:number = 10, b?:number): number{
+  return a+b
+}
+//传对象
+interface User {
+  name:string
+  age:nubmer
+}
+
+function add(user: User): User{
+  return user
+}
+console.log(add({ name: "111", age: 18}))
+
+//ts可以定义this的类型,在js中无法使用,必须是第一个参数定义this的类型(有点类似python里面的self?)
+interface Obj {
+  user:number[]
+  add:(this:Obj,num:number)=>void
+}
+
+let obj:Obj = {
+  user:[1,2,3],
+  add(this:Obj,num:number) {
+    this.user.push(num)
+  }
+}
+obj.add(4)
+console.log(obj)
+
+//函数重载
+let user:number[] = [1,2,3]
+
+
+function findNum():number[]
+function findNum(id:number):number[] 
+function findNum(add:number[]):number[]
+
+//跟据入参走不同逻辑
+function findNum(ids?:number | number[]):number[] {
+  if(typeof ids == 'number') {
+    return user.filter(v=> v == ids)
+  }else if (Array.isArray(ids)) {
+    user.push(...ids)
+    return user
+  }else {
+    return user
+  }
+}
+console.log(findNum())

联合类型

typescript
let phone:number | string = '123456'
+
+//函数使用联合类型
+let fn = function(type:number | boolean):boolean {
+  return !!type
+}
let phone:number | string = '123456'
+
+//函数使用联合类型
+let fn = function(type:number | boolean):boolean {
+  return !!type
+}

TypeScript中的 !!是一个逻辑非(not)操作符的双重否定形式,它可以用于将一个值转换成对应的布尔值。基本上,!!可以将任何值强制转换为对应的布尔值类型。

例如,使用!!可以将下列值转换为布尔类型的值:

!!true // true

!!1 // true

!!"hello" // true

!!undefined // false

!!null // false

!!0 // false

!!"" // false

交叉类型

typescript
interface People {
+  name:string,
+  age:string
+}
+
+interface Man {
+  sex:number
+}
+
+const p = (param:People & Man):void => {
+  console.log(man)
+}
+
+p({
+  name:"ikun",
+  age:"两年半",
+  sex:1
+})
interface People {
+  name:string,
+  age:string
+}
+
+interface Man {
+  sex:number
+}
+
+const p = (param:People & Man):void => {
+  console.log(man)
+}
+
+p({
+  name:"ikun",
+  age:"两年半",
+  sex:1
+})

类型断言

尖括号写法

typescript
let someValue: any = "this is a string";
+
+let strLength: number = (<string>someValue).length;
let someValue: any = "this is a string";
+
+let strLength: number = (<string>someValue).length;

as写法

typescript
let someValue: any = "this is a string";
+
+let strLength: number = (someValue as string).length;
let someValue: any = "this is a string";
+
+let strLength: number = (someValue as string).length;

使用JSX时只允许as写法

内置对象

  • Number(1)
  • Date()
  • RegExp(/\w/)
  • Error('wrong')
  • XMLHttpRequest
  • HTML(元素名称)Element / HTMLElement / Element
  • NodeList / NodeListOf<HTMLDivElement | HTMLElement>
  • Storage
  • Location
  • Promise
  • ...

Class

typescript
//class的基本用法 继承 和类型约束 implements
+interface Options {
+  el: string | HTMLElement;
+}
+interface VueClass {
+  options: Options;
+  init(): void;
+}
+interface Vnode {
+  tag: string;
+  text: string;
+  children?: Vnode[];
+}
+//虚拟dom
+class Dom {
+  //创建dom节点
+  createElement(el: string) {
+    return document.createElement(el);
+  }
+  //填充文本
+  setText(el: HTMLElement, text: string | null) {
+    el.textContent = text;
+  }
+  //渲染函数
+  render(data: Vnode) {
+    let root = this.createElement(data.tag);
+    if (data.children && Array.isArray(data.children)) {
+      data.children.forEach((item) => {
+        let child = this.render(item);
+        root.appendChild(child);
+      });
+    } else {
+      this.setText(root, data.text === undefined ? "" : data.text);
+    }
+    return root;
+  }
+}
+
+class Vue extends Dom implements VueClass {
+  options: Options;
+  constructor(options: Options) {
+    super();
+    this.options = options;
+    this.init();
+  }
+  init(): void {
+    let data: Vnode = {
+      tag: "div",
+      text: '111',
+      children: [
+        {
+          tag: "section",
+          text: "子节点1",
+        },
+        {
+          tag: "section",
+          text: "子节点2",
+        },
+        {
+          tag: "section",
+          text: "子节点3",
+        }
+      ],
+    };
+    let app =
+      typeof this.options.el == "string"
+        ? document.querySelector(this.options.el)
+        : this.options.el;
+    app?.appendChild(this.render(data));
+  }
+}
+
+new Vue({
+  el: "#app"
+});
//class的基本用法 继承 和类型约束 implements
+interface Options {
+  el: string | HTMLElement;
+}
+interface VueClass {
+  options: Options;
+  init(): void;
+}
+interface Vnode {
+  tag: string;
+  text: string;
+  children?: Vnode[];
+}
+//虚拟dom
+class Dom {
+  //创建dom节点
+  createElement(el: string) {
+    return document.createElement(el);
+  }
+  //填充文本
+  setText(el: HTMLElement, text: string | null) {
+    el.textContent = text;
+  }
+  //渲染函数
+  render(data: Vnode) {
+    let root = this.createElement(data.tag);
+    if (data.children && Array.isArray(data.children)) {
+      data.children.forEach((item) => {
+        let child = this.render(item);
+        root.appendChild(child);
+      });
+    } else {
+      this.setText(root, data.text === undefined ? "" : data.text);
+    }
+    return root;
+  }
+}
+
+class Vue extends Dom implements VueClass {
+  options: Options;
+  constructor(options: Options) {
+    super();
+    this.options = options;
+    this.init();
+  }
+  init(): void {
+    let data: Vnode = {
+      tag: "div",
+      text: '111',
+      children: [
+        {
+          tag: "section",
+          text: "子节点1",
+        },
+        {
+          tag: "section",
+          text: "子节点2",
+        },
+        {
+          tag: "section",
+          text: "子节点3",
+        }
+      ],
+    };
+    let app =
+      typeof this.options.el == "string"
+        ? document.querySelector(this.options.el)
+        : this.options.el;
+    app?.appendChild(this.render(data));
+  }
+}
+
+new Vue({
+  el: "#app"
+});
  • readonly
  • private
  • protected
  • public
  • super()
  • 静态方法 static
  • get set

抽象类 & 抽象方法

  • abstract className
  • abstract functionName

元组Tuple

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。

typescript
// Declare a tuple type
+let x: [string, number];
+// Initialize it
+x = ['hello', 10]; // OK
+// Initialize it incorrectly
+x = [10, 'hello']; // Error
// Declare a tuple type
+let x: [string, number];
+// Initialize it
+x = ['hello', 10]; // OK
+// Initialize it incorrectly
+x = [10, 'hello']; // Error

当访问一个已知索引的元素,会得到正确的类型:

ts
console.log(x[0].substr(1)); // OK
+console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'
console.log(x[0].substr(1)); // OK
+console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'

当访问一个越界的元素,会使用联合类型替代:

ts
x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型
+
+console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString
+
+x[6] = true; // Error, 布尔不是(string | number)类型
x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型
+
+console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString
+
+x[6] = true; // Error, 布尔不是(string | number)类型

枚举

enum类型是对JavaScript标准数据类型的一个补充。

ts
enum Color {Red, Green, Blue}
+let c: Color = Color.Green;
enum Color {Red, Green, Blue}
+let c: Color = Color.Green;

默认情况下,从0开始为元素编号。 你也可以手动的指定成员的数值。 例如,我们将上面的例子改成从 1开始编号:

ts
enum Color {Red = 1, Green, Blue}
+let c: Color = Color.Green;
enum Color {Red = 1, Green, Blue}
+let c: Color = Color.Green;

或者,全部都采用手动赋值:

ts
enum Color {Red = 1, Green = 2, Blue = 4}
+let c: Color = Color.Green;
enum Color {Red = 1, Green = 2, Blue = 4}
+let c: Color = Color.Green;

枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字:

ts
enum Color {Red = 1, Green, Blue}
+let colorName: string = Color[2];
+
+console.log(colorName);  // 显示'Green'因为上面代码里它的值是2
enum Color {Red = 1, Green, Blue}
+let colorName: string = Color[2];
+
+console.log(colorName);  // 显示'Green'因为上面代码里它的值是2

类型推断

TypeScript里,在有些没有明确指出类型的地方,类型推论会帮助提供类型。

如果没有指出类型 & 没赋值 会被推断成any类型。

类型别名

typescript
type s = string | null
+
+let str:s = 'test'
+
+let str1 = '123'
+type s1 = typeof str1
+
+
+type num = 1 extends number ? 1 : 0
type s = string | null
+
+let str:s = 'test'
+
+let str1 = '123'
+type s1 = typeof str1
+
+
+type num = 1 extends number ? 1 : 0

image-20230419001413539

Never

never类型表示的是那些永不存在的值的类型。 例如, never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是 never类型,当它们被永不为真的类型保护所约束时。

never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使 any也不可以赋值给never

typescript
type A = string & number //never
+type A = void | number | never //never会被忽略掉
+
+
+type A = '唱' | '跳' | 'rap'
+
+function kun(value: A) {
+    switch (value) {
+        case '唱':
+            break;
+        case '跳':
+            break;
+        case 'rap':
+            break;
+        default:
+            const check: never = value;
+            break;
+    }
+}
type A = string & number //never
+type A = void | number | never //never会被忽略掉
+
+
+type A = '唱' | '跳' | 'rap'
+
+function kun(value: A) {
+    switch (value) {
+        case '唱':
+            break;
+        case '跳':
+            break;
+        case 'rap':
+            break;
+        default:
+            const check: never = value;
+            break;
+    }
+}

Symbol

typescript
let a1:symbol = Symbol(1)
+let a2:symbol = Symbol(2)
+console.log(a1 === a2) // false
+
+//for Symbol 有没有注册过这个key 如果有直接用 没有就创建
+console.log(Symbol.for('1') === Symbol.for('1')) // true
let a1:symbol = Symbol(1)
+let a2:symbol = Symbol(2)
+console.log(a1 === a2) // false
+
+//for Symbol 有没有注册过这个key 如果有直接用 没有就创建
+console.log(Symbol.for('1') === Symbol.for('1')) // true
  • 可以用来避免属性被覆盖

生成器 迭代器

typescript
function* gen() {
+  yield Promise.resovle('111')
+  yield '1'
+  yield '2'
+}
+const g = gen()
+console.log(g.next())
+console.log(g.next())
+console.log(g.next())
+console.log(g.next())
+/*
+{ value: Promise { '111' }, done: false }
+{ value: '1', done: false }
+{ value: '2', done: false }
+{ value: undefined, done: true }
+*/
function* gen() {
+  yield Promise.resovle('111')
+  yield '1'
+  yield '2'
+}
+const g = gen()
+console.log(g.next())
+console.log(g.next())
+console.log(g.next())
+console.log(g.next())
+/*
+{ value: Promise { '111' }, done: false }
+{ value: '1', done: false }
+{ value: '2', done: false }
+{ value: undefined, done: true }
+*/
typescript
let Set:Set<number> = new Set([1,1,2,3,3,3]) // 1 2 3
+
+let map:Map<any, any> = new Map()
+let arr = [1,2,3]
+map.set(arr, '123')
+
+function args(){
+  console.log(arguments) //伪数组 IArguments
+}
+
+const each = (value:any) => {
+  let It: any = value[Symbol.iterator]()
+  let next: any = { done: false }
+  while (!next.done) {
+    next = It.next()
+    if (!next.done) {
+      console.log(next.value)
+    }
+  }
+}
+each([1,2,3])
+/*
+1
+2
+3
+*/
+
+
+//迭代器语法糖 for of
+//对象不能用 for of语法
+for (let value of map) {
+  console.log(value)
+}
+
+//数组解构 底层原理也是调用iterator
+let a = [4,5,6]
+let copy = [...a]
+console.log(a)
+
+let obj = {
+  max:5,
+  current:0,
+  [Symbol.iterator]() {
+    return {
+      max: this.max,
+      current: this.current,
+      next() {
+        if (this.current == this.max) {
+          return {
+            value: undefined,
+            done:true
+          }
+        }else {
+          return {
+            value: this.current++,
+            done:false
+          }
+        }
+      }
+    }
+  }
+}
+
+for (let value of obj) {
+  console.log(value)
+}
+/*
+0
+1
+2
+3
+4
+*/
let Set:Set<number> = new Set([1,1,2,3,3,3]) // 1 2 3
+
+let map:Map<any, any> = new Map()
+let arr = [1,2,3]
+map.set(arr, '123')
+
+function args(){
+  console.log(arguments) //伪数组 IArguments
+}
+
+const each = (value:any) => {
+  let It: any = value[Symbol.iterator]()
+  let next: any = { done: false }
+  while (!next.done) {
+    next = It.next()
+    if (!next.done) {
+      console.log(next.value)
+    }
+  }
+}
+each([1,2,3])
+/*
+1
+2
+3
+*/
+
+
+//迭代器语法糖 for of
+//对象不能用 for of语法
+for (let value of map) {
+  console.log(value)
+}
+
+//数组解构 底层原理也是调用iterator
+let a = [4,5,6]
+let copy = [...a]
+console.log(a)
+
+let obj = {
+  max:5,
+  current:0,
+  [Symbol.iterator]() {
+    return {
+      max: this.max,
+      current: this.current,
+      next() {
+        if (this.current == this.max) {
+          return {
+            value: undefined,
+            done:true
+          }
+        }else {
+          return {
+            value: this.current++,
+            done:false
+          }
+        }
+      }
+    }
+  }
+}
+
+for (let value of obj) {
+  console.log(value)
+}
+/*
+0
+1
+2
+3
+4
+*/

泛型

typescript
function fun<T>(a:T, b:T):Array<T> {
+	return [a,b]
+}
+
+type A<T> = string | number | T
+let a:A<boolean> = true
+
+interface Date<T> {
+  msg:T
+}
+let data:Date<number> = {
+  msg:1
+}
+
+function add<T = number,K = number>(a:T,b:K):Array<T | K> {
+  return [a,b]
+}
+add(false, '1')
+
+const axios = {
+  get<T>(url:string) {
+    return new Promise<T>((resolve,reject)=>{
+      let xhr:XMLHttpRequest = new XMLHttpRequest()
+      xhr.open('GET',url)
+      xhr.onreadystatechange = () => {
+        if(xhr.readyState ==4 && xhr.status == 200) {
+          resolve(JSON.parse(xhr.responseText))
+        }
+      }
+      xhr.send(null)
+    })
+  }
+}
function fun<T>(a:T, b:T):Array<T> {
+	return [a,b]
+}
+
+type A<T> = string | number | T
+let a:A<boolean> = true
+
+interface Date<T> {
+  msg:T
+}
+let data:Date<number> = {
+  msg:1
+}
+
+function add<T = number,K = number>(a:T,b:K):Array<T | K> {
+  return [a,b]
+}
+add(false, '1')
+
+const axios = {
+  get<T>(url:string) {
+    return new Promise<T>((resolve,reject)=>{
+      let xhr:XMLHttpRequest = new XMLHttpRequest()
+      xhr.open('GET',url)
+      xhr.onreadystatechange = () => {
+        if(xhr.readyState ==4 && xhr.status == 200) {
+          resolve(JSON.parse(xhr.responseText))
+        }
+      }
+      xhr.send(null)
+    })
+  }
+}

泛型约束

typescript
// extends
+interface Len {
+  length:number
+}
+
+function func<T extends Len)(a:T) {
+  console.log(a.length)
+}
+
+let obj = {
+  name: 'test',
+  sex: 1
+}
+
+// 约束对象的key
+type key = keyof typeof obj // "name" | "sex"
+
+function ob<T extends object, K extends keyof T>(obj:T, key:K) {
+  
+}
+
+
+interface Data {
+  name:string
+  age:number
+  sex:string
+}
+
+type Options<T extends object> = {
+  //readonly [Key in keyof T]?:T[Key]
+  [Key in keyof T]?:T[Key]
+}
+
+type B = Options<Data>
+/*
+type B = {
+    name?: string | undefined;
+    age?: number | undefined;
+    sex?: string | undefined;
+}
+type B = {
+    name?: string | undefined;
+    age?: number | undefined;
+    sex?: string | undefined;
+}
+*/
// extends
+interface Len {
+  length:number
+}
+
+function func<T extends Len)(a:T) {
+  console.log(a.length)
+}
+
+let obj = {
+  name: 'test',
+  sex: 1
+}
+
+// 约束对象的key
+type key = keyof typeof obj // "name" | "sex"
+
+function ob<T extends object, K extends keyof T>(obj:T, key:K) {
+  
+}
+
+
+interface Data {
+  name:string
+  age:number
+  sex:string
+}
+
+type Options<T extends object> = {
+  //readonly [Key in keyof T]?:T[Key]
+  [Key in keyof T]?:T[Key]
+}
+
+type B = Options<Data>
+/*
+type B = {
+    name?: string | undefined;
+    age?: number | undefined;
+    sex?: string | undefined;
+}
+type B = {
+    name?: string | undefined;
+    age?: number | undefined;
+    sex?: string | undefined;
+}
+*/

tsconfig.json

通过tsc --init生成

json
"compilerOptions": {
+  "incremental": true, // TS编译器在第一次编译之后会生成一个存储编译信息的文件,第二次编译会在第一次的基础上进行增量编译,可以提高编译的速度
+  "tsBuildInfoFile": "./buildFile", // 增量编译文件的存储位置
+  "diagnostics": true, // 打印诊断信息 
+  "target": "ES5", // 目标语言的版本
+  "module": "CommonJS", // 生成代码的模板标准
+  "outFile": "./app.js", // 将多个相互依赖的文件生成一个文件,可以用在AMD模块中,即开启时应设置"module": "AMD",
+  "lib": ["DOM", "ES2015", "ScriptHost", "ES2019.Array"], // TS需要引用的库,即声明文件,es5 默认引用dom、es5、scripthost,如需要使用es的高级版本特性,通常都需要配置,如es8的数组新特性需要引入"ES2019.Array",
+  "allowJS": true, // 允许编译器编译JS,JSX文件
+  "checkJs": true, // 允许在JS文件中报错,通常与allowJS一起使用
+  "outDir": "./dist", // 指定输出目录
+  "rootDir": "./", // 指定输出文件目录(用于输出),用于控制输出目录结构
+  "declaration": true, // 生成声明文件,开启后会自动生成声明文件
+  "declarationDir": "./file", // 指定生成声明文件存放目录
+  "emitDeclarationOnly": true, // 只生成声明文件,而不会生成js文件
+  "sourceMap": true, // 生成目标文件的sourceMap文件
+  "inlineSourceMap": true, // 生成目标文件的inline SourceMap,inline SourceMap会包含在生成的js文件中
+  "declarationMap": true, // 为声明文件生成sourceMap
+  "typeRoots": [], // 声明文件目录,默认时node_modules/@types
+  "types": [], // 加载的声明文件包
+  "removeComments":true, // 删除注释 
+  "noEmit": true, // 不输出文件,即编译后不会生成任何js文件
+  "noEmitOnError": true, // 发送错误时不输出任何文件
+  "noEmitHelpers": true, // 不生成helper函数,减小体积,需要额外安装,常配合importHelpers一起使用
+  "importHelpers": true, // 通过tslib引入helper函数,文件必须是模块
+  "downlevelIteration": true, // 降级遍历器实现,如果目标源是es3/5,那么遍历器会有降级的实现
+  "strict": true, // 开启所有严格的类型检查
+  "alwaysStrict": true, // 在代码中注入'use strict'
+  "noImplicitAny": true, // 不允许隐式的any类型
+  "strictNullChecks": true, // 不允许把null、undefined赋值给其他类型的变量
+  "strictFunctionTypes": true, // 不允许函数参数双向协变
+  "strictPropertyInitialization": true, // 类的实例属性必须初始化
+  "strictBindCallApply": true, // 严格的bind/call/apply检查
+  "noImplicitThis": true, // 不允许this有隐式的any类型
+  "noUnusedLocals": true, // 检查只声明、未使用的局部变量(只提示不报错)
+  "noUnusedParameters": true, // 检查未使用的函数参数(只提示不报错)
+  "noFallthroughCasesInSwitch": true, // 防止switch语句贯穿(即如果没有break语句后面不会执行)
+  "noImplicitReturns": true, //每个分支都会有返回值
+  "esModuleInterop": true, // 允许export=导出,由import from 导入
+  "allowUmdGlobalAccess": true, // 允许在模块中全局变量的方式访问umd模块
+  "moduleResolution": "node", // 模块解析策略,ts默认用node的解析策略,即相对的方式导入
+  "baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
+  "paths": { // 路径映射,相对于baseUrl
+    // 如使用jq时不想使用默认版本,而需要手动指定版本,可进行如下配置
+    "jquery": ["node_modules/jquery/dist/jquery.min.js"]
+  },
+  "rootDirs": ["src","out"], // 将多个目录放在一个虚拟目录下,用于运行时,即编译后引入文件的位置可能发生变化,这也设置可以虚拟src和out在同一个目录下,不用再去改变路径也不会报错
+  "listEmittedFiles": true, // 打印输出文件
+  "listFiles": true// 打印编译的文件(包括引用的声明文件)
+}
+ 
+// 指定一个匹配列表(属于自动指定该路径下的所有ts相关文件)
+"include": [
+   "src/**/*"
+],
+// 指定一个排除列表(include的反向操作)
+ "exclude": [
+   "demo.ts"
+],
+// 指定哪些文件使用该配置(属于手动一个个指定文件)
+ "files": [
+   "demo.ts"
+]
"compilerOptions": {
+  "incremental": true, // TS编译器在第一次编译之后会生成一个存储编译信息的文件,第二次编译会在第一次的基础上进行增量编译,可以提高编译的速度
+  "tsBuildInfoFile": "./buildFile", // 增量编译文件的存储位置
+  "diagnostics": true, // 打印诊断信息 
+  "target": "ES5", // 目标语言的版本
+  "module": "CommonJS", // 生成代码的模板标准
+  "outFile": "./app.js", // 将多个相互依赖的文件生成一个文件,可以用在AMD模块中,即开启时应设置"module": "AMD",
+  "lib": ["DOM", "ES2015", "ScriptHost", "ES2019.Array"], // TS需要引用的库,即声明文件,es5 默认引用dom、es5、scripthost,如需要使用es的高级版本特性,通常都需要配置,如es8的数组新特性需要引入"ES2019.Array",
+  "allowJS": true, // 允许编译器编译JS,JSX文件
+  "checkJs": true, // 允许在JS文件中报错,通常与allowJS一起使用
+  "outDir": "./dist", // 指定输出目录
+  "rootDir": "./", // 指定输出文件目录(用于输出),用于控制输出目录结构
+  "declaration": true, // 生成声明文件,开启后会自动生成声明文件
+  "declarationDir": "./file", // 指定生成声明文件存放目录
+  "emitDeclarationOnly": true, // 只生成声明文件,而不会生成js文件
+  "sourceMap": true, // 生成目标文件的sourceMap文件
+  "inlineSourceMap": true, // 生成目标文件的inline SourceMap,inline SourceMap会包含在生成的js文件中
+  "declarationMap": true, // 为声明文件生成sourceMap
+  "typeRoots": [], // 声明文件目录,默认时node_modules/@types
+  "types": [], // 加载的声明文件包
+  "removeComments":true, // 删除注释 
+  "noEmit": true, // 不输出文件,即编译后不会生成任何js文件
+  "noEmitOnError": true, // 发送错误时不输出任何文件
+  "noEmitHelpers": true, // 不生成helper函数,减小体积,需要额外安装,常配合importHelpers一起使用
+  "importHelpers": true, // 通过tslib引入helper函数,文件必须是模块
+  "downlevelIteration": true, // 降级遍历器实现,如果目标源是es3/5,那么遍历器会有降级的实现
+  "strict": true, // 开启所有严格的类型检查
+  "alwaysStrict": true, // 在代码中注入'use strict'
+  "noImplicitAny": true, // 不允许隐式的any类型
+  "strictNullChecks": true, // 不允许把null、undefined赋值给其他类型的变量
+  "strictFunctionTypes": true, // 不允许函数参数双向协变
+  "strictPropertyInitialization": true, // 类的实例属性必须初始化
+  "strictBindCallApply": true, // 严格的bind/call/apply检查
+  "noImplicitThis": true, // 不允许this有隐式的any类型
+  "noUnusedLocals": true, // 检查只声明、未使用的局部变量(只提示不报错)
+  "noUnusedParameters": true, // 检查未使用的函数参数(只提示不报错)
+  "noFallthroughCasesInSwitch": true, // 防止switch语句贯穿(即如果没有break语句后面不会执行)
+  "noImplicitReturns": true, //每个分支都会有返回值
+  "esModuleInterop": true, // 允许export=导出,由import from 导入
+  "allowUmdGlobalAccess": true, // 允许在模块中全局变量的方式访问umd模块
+  "moduleResolution": "node", // 模块解析策略,ts默认用node的解析策略,即相对的方式导入
+  "baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
+  "paths": { // 路径映射,相对于baseUrl
+    // 如使用jq时不想使用默认版本,而需要手动指定版本,可进行如下配置
+    "jquery": ["node_modules/jquery/dist/jquery.min.js"]
+  },
+  "rootDirs": ["src","out"], // 将多个目录放在一个虚拟目录下,用于运行时,即编译后引入文件的位置可能发生变化,这也设置可以虚拟src和out在同一个目录下,不用再去改变路径也不会报错
+  "listEmittedFiles": true, // 打印输出文件
+  "listFiles": true// 打印编译的文件(包括引用的声明文件)
+}
+ 
+// 指定一个匹配列表(属于自动指定该路径下的所有ts相关文件)
+"include": [
+   "src/**/*"
+],
+// 指定一个排除列表(include的反向操作)
+ "exclude": [
+   "demo.ts"
+],
+// 指定哪些文件使用该配置(属于手动一个个指定文件)
+ "files": [
+   "demo.ts"
+]

namespace

typescript提供了namespace避免全局变量污染的问题。

任何包含顶级import或export的文件都被当作一个模块。相反的,如果不带,那么它的内容被视为全局可见的。

  • 命名空间在ts1.5之前叫内部模块外部模块现在简称为模块。
  • 命名空间内的类默认私有
  • 通过export暴露
  • 通过namespace关键字定义
typescript
namespace A {
+  export const a = 1
+}
+// 实现:
+"use strict"
+var A;
+(function (A) {
+  A.a = 1;
+})(A || A = {});
+
+// 嵌套命名空间
+namespace A {
+  export namespace C {
+    export const D = 5;
+  }
+}
+
+console.log(A.C.D)
+
+//抽离命名空间
+export namespace V {
+  export const a = 1
+}
+
+import {V} from '../index'
+console.log(V)// {a:1}
+
+
+//简化命名空间
+namespace A {
+  export namespace C {
+    export const D = 5;
+  }
+}
+
+import a = A.C
+console.log(a.D)
+
+//命名空间合并
+namespace A {
+  export const b = 2
+}
+namespace A {
+	export const a = 1
+}
+//等价于
+namespace A {
+  export const b = 2
+  export const a = 1
+}
namespace A {
+  export const a = 1
+}
+// 实现:
+"use strict"
+var A;
+(function (A) {
+  A.a = 1;
+})(A || A = {});
+
+// 嵌套命名空间
+namespace A {
+  export namespace C {
+    export const D = 5;
+  }
+}
+
+console.log(A.C.D)
+
+//抽离命名空间
+export namespace V {
+  export const a = 1
+}
+
+import {V} from '../index'
+console.log(V)// {a:1}
+
+
+//简化命名空间
+namespace A {
+  export namespace C {
+    export const D = 5;
+  }
+}
+
+import a = A.C
+console.log(a.D)
+
+//命名空间合并
+namespace A {
+  export const b = 2
+}
+namespace A {
+	export const a = 1
+}
+//等价于
+namespace A {
+  export const b = 2
+  export const a = 1
+}

三斜线指令

三斜线指令是包含单个XML标签的单行注释。 注释的内容会做为编译器指令使用。

typescript
/// <reference path="..." />
+/// <reference path="..." />指令是三斜线指令中最常见的一种。 它用于声明文件间的 依赖。
/// <reference path="..." />
+/// <reference path="..." />指令是三斜线指令中最常见的一种。 它用于声明文件间的 依赖。

三斜线引用告诉编译器在编译过程中要引入的额外的文件。

typescript
/// <reference types="..." />
+/// <reference path="..." />指令相似,这个指令是用来声明 依赖的; 一个 /// <reference types="..." />指令则声明了对某个包的依赖。
+
+对这些包的名字的解析与在 import语句里对模块名的解析类似。 可以简单地把三斜线类型引用指令当做 import声明的包。
+
+例如,把 /// <reference types="node" />引入到声明文件,表明这个文件使用了 @types/node/index.d.ts里面声明的名字; 并且,这个包需要在编译阶段与声明文件一起被包含进来。
+
+仅当在你需要写一个d.ts文件时才使用这个指令。
+
+对于那些在编译阶段生成的声明文件,编译器会自动地添加/// <reference types="..." />; 当且仅当结果文件中使用了引用的包里的声明时才会在生成的声明文件里添加/// <reference types="..." />语句。
+
+若要在.ts文件里声明一个对@types包的依赖,使用--types命令行选项或在tsconfig.json里指定。
/// <reference types="..." />
+/// <reference path="..." />指令相似,这个指令是用来声明 依赖的; 一个 /// <reference types="..." />指令则声明了对某个包的依赖。
+
+对这些包的名字的解析与在 import语句里对模块名的解析类似。 可以简单地把三斜线类型引用指令当做 import声明的包。
+
+例如,把 /// <reference types="node" />引入到声明文件,表明这个文件使用了 @types/node/index.d.ts里面声明的名字; 并且,这个包需要在编译阶段与声明文件一起被包含进来。
+
+仅当在你需要写一个d.ts文件时才使用这个指令。
+
+对于那些在编译阶段生成的声明文件,编译器会自动地添加/// <reference types="..." />; 当且仅当结果文件中使用了引用的包里的声明时才会在生成的声明文件里添加/// <reference types="..." />语句。
+
+若要在.ts文件里声明一个对@types包的依赖,使用--types命令行选项或在tsconfig.json里指定。

声明文件

使用第三方库时需要引用它的声明文件d.ts才能获得对应的代码补全、接口提示等功能

typescript
npm i @types/xxx
npm i @types/xxx

Mixins混入

除了传统的面向对象继承方式,还流行一种通过可重用组件创建类的方式,就是联合另一个简单类的代码。

typescript
// Disposable Mixin
+class Disposable {
+    isDisposed: boolean;
+    dispose() {
+        this.isDisposed = true;
+    }
+
+}
+
+// Activatable Mixin
+class Activatable {
+    isActive: boolean;
+    activate() {
+        this.isActive = true;
+    }
+    deactivate() {
+        this.isActive = false;
+    }
+}
+
+class SmartObject implements Disposable, Activatable {
+    constructor() {
+        setInterval(() => console.log(this.isActive + " : " + this.isDisposed), 500);
+    }
+
+    interact() {
+        this.activate();
+    }
+
+    // Disposable
+    isDisposed: boolean = false;
+    dispose: () => void;
+    // Activatable
+    isActive: boolean = false;
+    activate: () => void;
+    deactivate: () => void;
+}
+applyMixins(SmartObject, [Disposable, Activatable]);
+
+let smartObj = new SmartObject();
+setTimeout(() => smartObj.interact(), 1000);
+
+////////////////////////////////////////
+// In your runtime library somewhere
+////////////////////////////////////////
+
+function applyMixins(derivedCtor: any, baseCtors: any[]) {
+    baseCtors.forEach(baseCtor => {
+        Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
+            derivedCtor.prototype[name] = baseCtor.prototype[name];
+        });
+    });
+}
// Disposable Mixin
+class Disposable {
+    isDisposed: boolean;
+    dispose() {
+        this.isDisposed = true;
+    }
+
+}
+
+// Activatable Mixin
+class Activatable {
+    isActive: boolean;
+    activate() {
+        this.isActive = true;
+    }
+    deactivate() {
+        this.isActive = false;
+    }
+}
+
+class SmartObject implements Disposable, Activatable {
+    constructor() {
+        setInterval(() => console.log(this.isActive + " : " + this.isDisposed), 500);
+    }
+
+    interact() {
+        this.activate();
+    }
+
+    // Disposable
+    isDisposed: boolean = false;
+    dispose: () => void;
+    // Activatable
+    isActive: boolean = false;
+    activate: () => void;
+    deactivate: () => void;
+}
+applyMixins(SmartObject, [Disposable, Activatable]);
+
+let smartObj = new SmartObject();
+setTimeout(() => smartObj.interact(), 1000);
+
+////////////////////////////////////////
+// In your runtime library somewhere
+////////////////////////////////////////
+
+function applyMixins(derivedCtor: any, baseCtors: any[]) {
+    baseCtors.forEach(baseCtor => {
+        Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
+            derivedCtor.prototype[name] = baseCtor.prototype[name];
+        });
+    });
+}

装饰器Decorator

随着TypeScript和ES6里引入了类,在一些场景下我们需要额外的特性来支持标注或修改类及其成员。 装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。

若要启用实验性的装饰器特性,你必须在命令行或tsconfig.json里启用experimentalDecorators编译器选项:

命令行:

shell
tsc --target ES5 --experimentalDecorators
tsc --target ES5 --experimentalDecorators

tsconfig.json:

json
{
+    "compilerOptions": {
+        "target": "ES5",
+        "experimentalDecorators": true
+    }
+}
{
+    "compilerOptions": {
+        "target": "ES5",
+        "experimentalDecorators": true
+    }
+}

装饰器是一种特殊类型的声明,它能够被附加到类声明方法访问符属性参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

类装饰器

typescript
const IKun: ClassDecorator = (target) => {
+    console.log(target);
+    target.prototype.name = 'ikun';
+    target.prototype.slogan = () => {
+        console.log('鸡你太美');
+    }
+}
+
+@IKun
+class Person {
+
+}
+
+const person = new Person() as any;
+person.slogan(); // 鸡你太美
const IKun: ClassDecorator = (target) => {
+    console.log(target);
+    target.prototype.name = 'ikun';
+    target.prototype.slogan = () => {
+        console.log('鸡你太美');
+    }
+}
+
+@IKun
+class Person {
+
+}
+
+const person = new Person() as any;
+person.slogan(); // 鸡你太美

装饰器工厂

typescript
const IKun = (name: string) => {
+    const decorator: ClassDecorator = (target) => {
+        target.prototype.name = name;
+        target.prototype.slogan = () => {
+            console.log('鸡你太美');
+        }
+    }
+    return decorator
+
+}
+
+@IKun('小黑子')
+class Person {
+
+}
+
+const person = new Person() as any;
+console.log(person.name); // 小黑子
+person.slogan(); // 鸡你太美
const IKun = (name: string) => {
+    const decorator: ClassDecorator = (target) => {
+        target.prototype.name = name;
+        target.prototype.slogan = () => {
+            console.log('鸡你太美');
+        }
+    }
+    return decorator
+
+}
+
+@IKun('小黑子')
+class Person {
+
+}
+
+const person = new Person() as any;
+console.log(person.name); // 小黑子
+person.slogan(); // 鸡你太美

方法装饰器

方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。 方法装饰器不能用在声明文件( .d.ts),重载或者任何外部上下文(比如declare的类)中。

方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 成员的属性描述符

如果方法装饰器返回一个值,它会被用作方法的属性描述符

typescript
const logResult = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
+  const fn = descriptor.value
+  descriptor.value = function(...rest) {
+    // 使用新的方法来替换原有方法,输出 方法名称 + 输入的参数 实现日志的增强功能
+    const result = fn.apply(this, rest)
+    console.log(propertyKey + ':' + result)
+    return result
+  }
+}
+
+class Person {
+    name: string = ''
+    age: number = 0
+
+    constructor(name: string, age: number) {
+        this.name = name
+        this.age = age
+    }
+
+    @logResult
+    getName() {
+      return this.name
+    }
+
+    @logResult
+    getAge() {
+      return this.age
+    }
+}
+
+const p = new Person('张三', 18)
+p.getName() // getName:张三
+p.getAge() // getAge:18
const logResult = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
+  const fn = descriptor.value
+  descriptor.value = function(...rest) {
+    // 使用新的方法来替换原有方法,输出 方法名称 + 输入的参数 实现日志的增强功能
+    const result = fn.apply(this, rest)
+    console.log(propertyKey + ':' + result)
+    return result
+  }
+}
+
+class Person {
+    name: string = ''
+    age: number = 0
+
+    constructor(name: string, age: number) {
+        this.name = name
+        this.age = age
+    }
+
+    @logResult
+    getName() {
+      return this.name
+    }
+
+    @logResult
+    getAge() {
+      return this.age
+    }
+}
+
+const p = new Person('张三', 18)
+p.getName() // getName:张三
+p.getAge() // getAge:18
typescript
import "reflect-metadata";
+
+const formatMetadataKey = Symbol("format");
+
+function format(formatString: string) {
+    return Reflect.metadata(formatMetadataKey, formatString);
+}
+
+function getFormat(target: any, propertyKey: string) {
+    return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
+}
+
+class Greeter {
+    @format("Hello, %s")
+    greeting: string;
+
+    constructor(message: string) {
+        this.greeting = message;
+    }
+
+    greet() {
+        let formatString = getFormat(this, "greeting");
+        return formatString.replace("%s", this.greeting);
+    }
+}
+
+console.log(new Greeter("world").greet()); // "Hello, world"
import "reflect-metadata";
+
+const formatMetadataKey = Symbol("format");
+
+function format(formatString: string) {
+    return Reflect.metadata(formatMetadataKey, formatString);
+}
+
+function getFormat(target: any, propertyKey: string) {
+    return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
+}
+
+class Greeter {
+    @format("Hello, %s")
+    greeting: string;
+
+    constructor(message: string) {
+        this.greeting = message;
+    }
+
+    greet() {
+        let formatString = getFormat(this, "greeting");
+        return formatString.replace("%s", this.greeting);
+    }
+}
+
+console.log(new Greeter("world").greet()); // "Hello, world"

参数装饰器

参数装饰器用于装饰函数参数,参数装饰器接收3个参数:

  • target: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • propertyKey: 方法名。
  • paramIndex: 参数所在位置的索引。
typescript
const paramDecorator = (target: any, propertyKey: string, paramIndex: number) => {
+    console.log(target, propertyKey, paramIndex)
+}
+
+class Person {
+    name: string = ''
+    age: number = 0
+
+    constructor(name: string, age: number) {
+        this.name = name
+        this.age = age
+    }
+
+    setName(@paramDecorator name: string) {
+      this.name = name
+    }
+}
+
+const p = new Person('张三', 18) // Person、setName、0
+p.setName('李四')
const paramDecorator = (target: any, propertyKey: string, paramIndex: number) => {
+    console.log(target, propertyKey, paramIndex)
+}
+
+class Person {
+    name: string = ''
+    age: number = 0
+
+    constructor(name: string, age: number) {
+        this.name = name
+        this.age = age
+    }
+
+    setName(@paramDecorator name: string) {
+      this.name = name
+    }
+}
+
+const p = new Person('张三', 18) // Person、setName、0
+p.setName('李四')
+ + + + \ No newline at end of file diff --git a/frontend/base/Webpack.html b/frontend/base/Webpack.html new file mode 100644 index 000000000..61fb78587 --- /dev/null +++ b/frontend/base/Webpack.html @@ -0,0 +1,165 @@ + + + + + + Webpack | 故事 + + + + + + + + + + + + + + +
Skip to content

Webpack

webpack时前端项目工程化的具体解决方案。webpack的主要功能:提供了友好的前端模块化开发支持、代码压缩混淆、处理浏览器端javascript得兼容性、性能优化等强大的功能。

前端工程化

  • 模块化
  • 组件化
  • 规范话
  • 自动化

基础使用

  • npm install webpack@version webpack-cli@version -D

  • webpack.config.js

    • js
      module.exports = {
      +  mode: 'development' //可取值development和production,前者不会压缩和性能优化,打包速度快
      +}
      module.exports = {
      +  mode: 'development' //可取值development和production,前者不会压缩和性能优化,打包速度快
      +}
  • package.json

    • json
      "script": {
      +  "dev": "webpack"
      +}
      "script": {
      +  "dev": "webpack"
      +}

默认约定

webpack 4.x和5.x版本有如下默认约定

  • 默认打包入口文件:src -> index.js
  • 默认输出文件路径:dist -> main.js

修改打包默认约定

可以在webpack.config.js中

  • 修改entry节点指定打包入口
  • 修改output节点指定输出
js
const path = require('path')
+
+module.exports = {
+  entry: path.join(__dirname, './src/index.js'),
+  output: {
+    path: path.join(__dirname, './dist'),
+    filename: 'js/bundle.js'
+  }
+}
const path = require('path')
+
+module.exports = {
+  entry: path.join(__dirname, './src/index.js'),
+  output: {
+    path: path.join(__dirname, './dist'),
+    filename: 'js/bundle.js'
+  }
+}

插件

webpack-dev-server

  • 类似node.js中的nodemon

  • 修改源代码后webpack会自动进行项目打包和构建

  • npm install webpack-dev-server@version -D

  • 修改package.json的script

    • json
      "scripts": {
      +  "dev": "webpack serve"
      +}
      "scripts": {
      +  "dev": "webpack serve"
      +}

html-webpack-plugin

  • 可以定制index.html内容

  • npm install html-webpack-plugin@version -D

  • 配置webpack.config.js

    • js
      const HtmlPlugin = require('html-webpack-plugin')
      +
      +const htmlPlugin = new HtmlPlugin({
      +  template: './src/index.html',//源文件
      +  filename: './index.html'//生成的问题
      +})
      +
      +module.exports = {
      +  mode: 'development',
      +  plugins: [htmlPlugin]
      +}
      const HtmlPlugin = require('html-webpack-plugin')
      +
      +const htmlPlugin = new HtmlPlugin({
      +  template: './src/index.html',//源文件
      +  filename: './index.html'//生成的问题
      +})
      +
      +module.exports = {
      +  mode: 'development',
      +  plugins: [htmlPlugin]
      +}

clean-webpack-plugin

  • 每次打包发布自动清理dist目录的旧文件

  • npm i clean-webpack-plugin@version -D

  • 配置

    js
    const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    +const cleanPlugin = new CleanWebpackPlugin()
    +
    +plugins: [htmlPlugin, cleanPlugin]
    const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    +const cleanPlugin = new CleanWebpackPlugin()
    +
    +plugins: [htmlPlugin, cleanPlugin]

devServer节点

js
devServer: {
+  open: true,
+  host: 127.0.0.1,
+  port: 80
+}
devServer: {
+  open: true,
+  host: 127.0.0.1,
+  port: 80
+}

loader

实际开发中,webpack只能打包处理.js模块,其他后缀的模块需要调用loader加载器才能正常打包

loader加载器作用:协助webpack打包处理特定的文件模块,比如:

  • css-loader可以处理.css文件

    • npm i style-loader@version css-loader@version -D

    • webpack.config.js的module->rules数组中添加loader规则

    • js
      module: {
      +  rules: [
      +    { test: /\.css$/, use: ['style-loader', 'css-loader']} //先执行css-loader,从后往前
      +  ]
      +}
      module: {
      +  rules: [
      +    { test: /\.css$/, use: ['style-loader', 'css-loader']} //先执行css-loader,从后往前
      +  ]
      +}
  • less-loader可以处理.less文件

    • npm i less-loader@version -D
    • js
      module: {
      +  rules: [
      +    { test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader']} //先执行less-loader,从后往前
      +  ]
      +}
      module: {
      +  rules: [
      +    { test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader']} //先执行less-loader,从后往前
      +  ]
      +}
  • babel-loader可以处理webpack无法处理的高级JS语法

    • npm i babel-loader@version @babel/core@version @babel/plugin-proposal-decorators@version -D

    • js
      module: {
      +  rules: [
      +    { test: /\.js$/, use: 'babel-loader', exclude: /node_modules/} //
      +  ]
      +}
      module: {
      +  rules: [
      +    { test: /\.js$/, use: 'babel-loader', exclude: /node_modules/} //
      +  ]
      +}
    • 配置babel-loader

      • 在根目录下创建babel.config.js配置文件

        js
        module.exports = {
        +  plugins: [['@babel@plugin-proposal-decorators'], {legacy: true}]
        +}
        module.exports = {
        +  plugins: [['@babel@plugin-proposal-decorators'], {legacy: true}]
        +}
  • url-loader可以处理样式表中与url路径相关的文件

    • npm i url-loader@version file-loader@version -D

    • js
      module: {
      +  rules: [
      +    /* { 
      +     test: /\.jpg|png|gif$/,
      +     use: { 
      +       loader: 'url-loader',
      +       options: {
      +         limit: 50000, //limit指定图片大小,单位byte,只有小于等于limit的图片才会被转为base64格式
      +         outputPath: 'images'
      +       }
      +     }
      +    } 
      +    */
      +    { test: /\.jpg|png|gif$/, use: 'url-loader?limit=50000&outputPath=images'}
      +  ]
      +}
      module: {
      +  rules: [
      +    /* { 
      +     test: /\.jpg|png|gif$/,
      +     use: { 
      +       loader: 'url-loader',
      +       options: {
      +         limit: 50000, //limit指定图片大小,单位byte,只有小于等于limit的图片才会被转为base64格式
      +         outputPath: 'images'
      +       }
      +     }
      +    } 
      +    */
      +    { test: /\.jpg|png|gif$/, use: 'url-loader?limit=50000&outputPath=images'}
      +  ]
      +}

build打包发布

在package.json的script节点下新增build命令:

json
"scripts": {
+  "dev": "webpack serve",
+  "build": "webpack --mode production"
+}
"scripts": {
+  "dev": "webpack serve",
+  "build": "webpack --mode production"
+}

SourceMap

SourceMap是一个信息文件,里面存着位置信息。也就是说SourceMap文件中存储着压缩混淆后的代码所对应的转换前的位置。

出错的时候除错工具直接显示原始代码,而不是转换后的代码,方便后期调试。

  • 开发环境在webpack.config.js中添加配置,保证运行时报错和源代码行数一致:
js
module.exports = {
+  devtool: 'eval-source-map'
+}
module.exports = {
+  devtool: 'eval-source-map'
+}
  • 生产环境省略devltool选项则最终生成文件不包含SourceMap,能够防止源码通过SourceMap暴露。如果只想定位报错具体行数,且不想暴露源码,可以将devtool设置为nosources-source-map

其他

  • webpack.config.js配置目录别名
js
resolve: {
+  alias: {
+  //@代表源码目录
+  '@': path.join(__dirname, './src/')
+  }
+}
resolve: {
+  alias: {
+  //@代表源码目录
+  '@': path.join(__dirname, './src/')
+  }
+}
+ + + + \ No newline at end of file diff --git a/frontend/base/jQuery.html b/frontend/base/jQuery.html new file mode 100644 index 000000000..1108276d3 --- /dev/null +++ b/frontend/base/jQuery.html @@ -0,0 +1,139 @@ + + + + + + jQuery | 故事 + + + + + + + + + + + + + + +
Skip to content

jQuery

顶级对象

$是jQuery的别称,也是jQuery的顶级对象,相当于原生JavaScript肿的window,把元素用$包装成jQuery对象,就可以调用jQuery的方法。

入口函数

js
$(document).ready(function () {
+    //do something
+});
+$(function () {
+    //do something
+})
$(document).ready(function () {
+    //do something
+});
+$(function () {
+    //do something
+})

DOM对象和jQuery对象

  • 用原生JS获取的对象是DOM对象
  • jQuery方法获取的元素是jQuery对象,本质是$对DOM对象包装后产生的对象(伪数组形式存储)
  • DOM和jQuery对象互相转换
    • DOM对象转jQuery对象:$(dom对象)
    • jQuery对象转DOM对象
      • $('div')[index]
      • $('div').get(index)

jQuery常用API

选择器

  • $("选择器")
  • 筛选选择器
    • $('li:first')
    • $('li:last')
    • $('li:eq(2)'):索引号等于2
    • $('li:odd'):索引号为奇数
    • $('li:even'):索引号为偶数
  • 筛选方法
    • parent()
    • children(selector)
    • find(selector)
    • siblings(selector):查找兄弟节点 不包括本身
    • nextAll([expr]):查找当前元素之后所有同辈元素
    • prevtAll([expr]):查找当前之前所有同辈元素
    • hasClass(class)
    • eq(index)

遍历DOM元素(伪数组形式存储)的过程叫隐式迭代:给匹配到的所有元素进行循环遍历,执行相应的方法,而不用手动进行循环调用

jQuery支持链式编程

样式操作

  • 操作样式:jQuery对象.css(属性, 值)
  • 参数可以是对象形式,设置多组样式:jQuery对象.css({"color": "pink", "font-size": "15px"}) (属性可以不用加引号)
  • 获取样式属性值:jQuery对象.css(属性)
  • 设置类样式:
    • jQuery对象.addClass(className)
    • jQuery对象.removeClass(className)
    • jQuery对象.toggleClass(className)

效果

显示隐藏

  • show([speed, [easing], [fn]])
    • 参数都可以省略,无动画直接显示
    • speed:三种预设速度之一的字符串(slow/normal/fast)或表示动画时长的毫秒数值
    • easing:用来切换指定效果,默认swing,可用参数linear
    • fn:回调函数,在动画完成时执行的函数,每个元素执行一次
  • hide()
  • toggle()

滑动

  • slideDown()
  • slideUp()
  • slideToggle()

动画队列停止排队:stop(),必须写在动画的前面

jQuery对象.children("ul").stop().slideToggle()

淡入淡出

  • fadeIn()
  • fadeOut()
  • fadeToggle()
  • fadeTo([[speed], opacity, [easing], [fn]]):修改透明度

自定义动画

  • animate(params, [speed], [easing],[fn])
  • params:想要更改的样式属性,以对象形式传递,必传。属性名可以不带引号,如果是复合属性需要采取驼峰命名

属性操作

  • prop(属性名):获取元素固有属性
  • prop(属性名, 值):设置元素固有属性
  • attr(属性名):获取元素自定义属性
  • attr(属性名, 值):设置元素自定义属性
  • data():可以在指定元素上存取数据,并不会修改DOM元素结构,页面刷新,存放的数据会被移除,也可以获取h5自定义属性,不用data-开头

文本属性

  • html():相当于原生js中的innerHTML
  • text():相当于原生js中的innerText
  • val():相当于原生js的value

元素操作

txt
- jQuery对象.each(function(index, domElement) { } ):遍历匹配的每一个元素,index是索引号,domElement是DOM元素对象,不是jQuery对象
+- $.each(obj, function(index, domElement) { }):遍历指定对象
+- 创建元素:var li = $("<li> </li>")
+- 添加元素
+  - element.append(li):拼接到最后
+  - element.prepend(li):插入到最前
+  - element.before(li):放到元素之后
+  - element.after(li):放到元素之前
+- 删除元素
+  - element.remove():删除匹配的元素
+  - element.empty():删除匹配元素的子节点
+  - element.html(""):等价于empty()
- jQuery对象.each(function(index, domElement) { } ):遍历匹配的每一个元素,index是索引号,domElement是DOM元素对象,不是jQuery对象
+- $.each(obj, function(index, domElement) { }):遍历指定对象
+- 创建元素:var li = $("<li> </li>")
+- 添加元素
+  - element.append(li):拼接到最后
+  - element.prepend(li):插入到最前
+  - element.before(li):放到元素之后
+  - element.after(li):放到元素之前
+- 删除元素
+  - element.remove():删除匹配的元素
+  - element.empty():删除匹配元素的子节点
+  - element.html(""):等价于empty()

尺寸、位置操作

尺寸

  • width()/height():只包含宽高
  • innerWidth()/innerHeight():+padding
  • outerWidth()/outerHeight():+padding、border
  • outerWidth(true)/outerHeight(true):+padding、border、margin

位置

txt
- offset():设置或返回被选元素相对于文档(document)的偏移坐标,跟父级没有关系
+    - 有两个属性left、top
+    - 修改传递对象{top: 10, left: 30}
+- position():返回被选元素相对**带有定位父级**偏移坐标,如果父级都没有定位,以文档为准
+- scrollTop()/scrollLeft():被卷去的头部/左侧
+    - 可以传递参数直接跳到指定位置
- offset():设置或返回被选元素相对于文档(document)的偏移坐标,跟父级没有关系
+    - 有两个属性left、top
+    - 修改传递对象{top: 10, left: 30}
+- position():返回被选元素相对**带有定位父级**偏移坐标,如果父级都没有定位,以文档为准
+- scrollTop()/scrollLeft():被卷去的头部/左侧
+    - 可以传递参数直接跳到指定位置

jQuery事件

事件注册

  • element.事件(function(){})

事件处理

on
  • element.on(events, [selector], fn)
    • events:一个或多个用空格分隔的事件类型,如click、keydown
    • selector:元素的子元素选择器
    • fn:回调函数,即绑定在元素身上的侦听函数
js
$("div").on({
+    mouseenter: function () {
+        $(this).css("background", "skyblue");
+    },
+    click: function () {
+        $(this).css("background", "red");
+    }
+});
+
+$("div").on("mouseenter mouseleave", function () {
+        $(this).toggleClass("current")
+    }
+);
$("div").on({
+    mouseenter: function () {
+        $(this).css("background", "skyblue");
+    },
+    click: function () {
+        $(this).css("background", "red");
+    }
+});
+
+$("div").on("mouseenter mouseleave", function () {
+        $(this).toggleClass("current")
+    }
+);
事件委托

事件绑定在父元素上

js
$("ul").on("click", "li", function () {
+    alert('111')
+})
$("ul").on("click", "li", function () {
+    alert('111')
+})
on可以给未来元素绑定事件
事件解绑off
  • element.off()解绑所有
  • element.off(事件1,事件2...)解绑指定
只触发一次的事件one

element.one(事件,fn)

自动触发
  • element.事件()
  • element.trigger(事件)
  • element.triggerHandler(事件):不会触发元素的默认行为

事件对象

事件被触发,就会有事件对象的产生

js
element.on(events, [selector], function (event) {
+    console.log(event)
+    event.preventDefault();//阻止默认行为
+    return false; //阻止默认行为
+    event.stopPropagation();//阻止冒泡
+})
element.on(events, [selector], function (event) {
+    console.log(event)
+    event.preventDefault();//阻止默认行为
+    return false; //阻止默认行为
+    event.stopPropagation();//阻止冒泡
+})

其他方法

拷贝对象

$.extend([deep], target, object1, [objectN])

  • deep:true-深拷贝,默认false-浅拷贝

  • target:目标对象

  • object:源对象

  • objectN:第N个源对象,会覆盖前面的相同属性

多库共存

  • $统一改为jQuery
  • 新的名称$.noConflict()/jQuery.noConflict()

jQuery插件

  • 瀑布流
  • 图片懒加载
  • 全屏滚动:fullpage.js
  • Bootstrap组件、插件

jQuery请求

  • $.get(url, [data], [callback])

    js
    $(function() {
    +  $('#btn').on('click', function() {
    +      $.get('xxx.com/api/getXxx', 'a=b', function(res) {
    +          console.log(res)
    +      })
    +  })
    +})
    $(function() {
    +  $('#btn').on('click', function() {
    +      $.get('xxx.com/api/getXxx', 'a=b', function(res) {
    +          console.log(res)
    +      })
    +  })
    +})
  • $.post(url, [data], [callback])

    js
    $(function() {
    +  $('#btn').on('click', function() {
    +      $.get('xxx.com/api/getXxx', {"a": "b"}, function(res) {
    +          console.log(res)
    +      })
    +  })
    +})
    $(function() {
    +  $('#btn').on('click', function() {
    +      $.get('xxx.com/api/getXxx', {"a": "b"}, function(res) {
    +          console.log(res)
    +      })
    +  })
    +})
  • $.ajax()

    js
    $.ajax({
    +  type: '',
    +  url: '',
    +  data: {},
    +  success: function(res) {}
    +})
    $.ajax({
    +  type: '',
    +  url: '',
    +  data: {},
    +  success: function(res) {}
    +})
+ + + + \ No newline at end of file diff --git a/frontend/framework/Vue.html b/frontend/framework/Vue.html new file mode 100644 index 000000000..88c26e2f8 --- /dev/null +++ b/frontend/framework/Vue.html @@ -0,0 +1,947 @@ + + + + + + Vue | 故事 + + + + + + + + + + + + + + +
Skip to content

Vue

Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架

特性

  • 数据驱动视图:在数据变化时页面会重新渲染
  • 双向数据绑定:DOM元素中的数据和Vue实例中的data保持一致,无论谁被改变,另一方都会更新为相同的数据

MVVM

MVVM是Vue实现数据驱动视图和双向数据绑定的原理。MVVM指的是Model、View和ViewModel。

  • Model:当前页面渲染时依赖的数据源
  • View:当前页面渲染的DOM结构
  • ViewModel:Vue的实例,MVVM的核心

ViewModel把Model和View连接在一起,同时监听DOM变化和数据源的变化。

起步

html
<!doctype html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport"
+          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+</head>
+<body>
+<div id="app">
+    {{ msg }}
+</div>
+<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
+<script>
+    const vm = new Vue({
+        el: '#app',
+        data: {
+            msg: 'hello world'
+        }
+    })
+</script>
+</body>
+</html>
<!doctype html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport"
+          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+</head>
+<body>
+<div id="app">
+    {{ msg }}
+</div>
+<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
+<script>
+    const vm = new Vue({
+        el: '#app',
+        data: {
+            msg: 'hello world'
+        }
+    })
+</script>
+</body>
+</html>

指令和过滤器

内容渲染

  • v-test
    • <p v-test=username></p>:把username值渲染到p标签中
    • <p v-test=gender>性别</p>:把gender值渲染到p标签中,原有的值会被覆盖
    • 插值表达式(Mustache),专门用来解决v-text会覆盖默认文本内容的问题,不能用在属性上
    • <p>性别 </p>
    • 支持javascript表达式
  • v-html
    • 把包含HTML标签的字符串渲染为页面的HTML元素

属性绑定

  • v-bind:单向绑定
    • v-bind:属性名
    • 简写为:属性名
    • 支持javascript表达式

事件绑定

v-on

  • v-on:事件名="函数(param)":v-on:click="add"

  • 简写为:@,例如@click="add"

  • 函数定义在vue实例的methods中

  • js
    methods: {
    +  add: function(param) {
    +  	console.log(1)
    +	}
    +}
    +---
    +ES6写法: 
    +methods: {
    +  add(param) {
    +  	console.log(1)
    +	}
    +}
    methods: {
    +  add: function(param) {
    +  	console.log(1)
    +	}
    +}
    +---
    +ES6写法: 
    +methods: {
    +  add(param) {
    +  	console.log(1)
    +	}
    +}
  • 不传参数默认参数列表有事件对象e,如果传参可以用$event传递事件对象

  • 事件修饰符

    • @click.prevent=show():绑定事件并阻止默认行为
    • stop:阻止事件冒泡
    • capture:以捕获模式触发当前事件处理函数
    • once:绑定事件只触发一次
    • self:只有在even.target时当前元素自身时触发事件处理函数
  • 按键修饰符

    • 判断详细的案件
      • @keyup.enter=submit
      • esc

双向绑定

  • v-model:不操作DOM情况下,快速获取表单数据
  • 修饰符
    • .nubmer:自动将输入转为数值
    • .trim:自动过滤输入的首尾空白字符
    • .lazy:在change时更新,input时不更新

条件渲染

控制DOM的显示与隐藏

  • v-if
    • 通过添加、移除元素实现
    • 如果刚进入页面不需要被展示,而且后期可能也不需要展示此时v-if性能更好
    • 配套指令:v-else、v-else-if
  • v-show
    • display控制元素显示、隐藏
    • 如果频繁切换显示状态用v-show更好

列表渲染

v-for渲染数组

基于一个数组来循环渲染一个列表结构。v-for指令需要用item in items形式的特殊语法

html
<li v-for="item in items">姓名是: {{ item.name }}</li>
+
+<li v-for="(item,index) in items">姓名是: {{ item.name }}</li>
<li v-for="item in items">姓名是: {{ item.name }}</li>
+
+<li v-for="(item,index) in items">姓名是: {{ item.name }}</li>
  • items:待循环数组
  • item:被循环的每一项
  • index:索引号,从0开始

建议用到v-for指令,要绑定一个:key属性,而且尽量把id作为key

  • key的值要是字符串/数字类型
  • index作为key没有任何意义,因为index没有唯一性(和数据没有绑定关系)
  • 指定key可以提升性能、防止列表状态紊乱

v-for渲染对象

完整语法

html
<li v-for="(value, key, index) in myObject"> 
+{{ value }} {{ kye }} {{ index }}
+</li>
<li v-for="(value, key, index) in myObject"> 
+{{ value }} {{ kye }} {{ index }}
+</li>

过滤器(vue3已移除)

常用于文本格式化,过滤器可以用在两个地方:插值表达式和v-bind属性绑定,过滤器本质是函数,被定义在vue实例的filters节点下

  • <p> 0</p>:调用captitalize过滤器,对message进行格式化
  • <div v-bind:id="rawId | formatId"></div:调用formatId过滤器,对rawId进行格式化
js
filters: {
+  capitalize(val) {
+    return val.charAt(0).toUppercase() + var.slice(1)
+  }
+}
filters: {
+  capitalize(val) {
+    return val.charAt(0).toUppercase() + var.slice(1)
+  }
+}

私有过滤器和全局过滤器

  • 私有过滤器:定义在vue实例的filters节点下
  • 全局过滤器:使用Vue.filter(filter, (str) => {return xxx})定义

连续调用&传参

js
{{ msg | filterA | filterB(arg1, arg2)}}
{{ msg | filterA | filterB(arg1, arg2)}}

侦听器

watch侦听器语序开发者监视数据的变化,从而针对数据的变化做特定的操作。

侦听器格式

watch定义在vue实例的watch节点下

  • 方法格式的侦听器

    • js
      watch: {
      +  username(newVal, oldVal) {
      +    console.log(newVal, oldVal)
      +  }
      +}
      watch: {
      +  username(newVal, oldVal) {
      +    console.log(newVal, oldVal)
      +  }
      +}
    • 缺点

      • 无法在刚进入页面时自动触发
      • 如果侦听的是一个对象,对象属性发生变化不会触发侦听器
  • 对象格式的侦听器

    • js
      watch: {
      +  username: {
      +    handler(newVal, oldVal) {
      +    console.log(newVal, oldVal)
      +  },
      +  immediate: true,
      +  deep: true,
      +  'info.age'(newVal) {
      +    console.log(newVal)
      +  }
      +}
      watch: {
      +  username: {
      +    handler(newVal, oldVal) {
      +    console.log(newVal, oldVal)
      +  },
      +  immediate: true,
      +  deep: true,
      +  'info.age'(newVal) {
      +    console.log(newVal)
      +  }
      +}
    • 可以通过immediate选项让侦听器立即触发

    • 可以通过deep选项开启深度监听,可以监听到对象的任何一个属性变化

    • 如果要侦听的是对象的子属性变化,则必须包裹一层单引号

计算属性

通过运算得到的属性值,可以被模版结构或methods方法使用。

计算属性放在vue实例的computed节点中

js
var vm = new Vue({
+  el: '#app',
+  data: {
+    r: 0, g: 0, b: 0
+  },
+  computed: {
+    //计算属性rgb
+    rgb() { return `rgb(${this.r}, ${this.g}, ${this.b})`}
+    //计算属性 allChecked
+    allChecked: {
+    	get() {
+      	return this.goodsList.every(item => item.goods_state)
+    	},
+    	set(newVal) {
+      	this.goodsList.forEach(item => item.goods_state = newVal)
+    	}
+		}
+  },
+  methods: {
+    show() { console.log(this.rgb) }
+  }
+})
var vm = new Vue({
+  el: '#app',
+  data: {
+    r: 0, g: 0, b: 0
+  },
+  computed: {
+    //计算属性rgb
+    rgb() { return `rgb(${this.r}, ${this.g}, ${this.b})`}
+    //计算属性 allChecked
+    allChecked: {
+    	get() {
+      	return this.goodsList.every(item => item.goods_state)
+    	},
+    	set(newVal) {
+      	this.goodsList.forEach(item => item.goods_state = newVal)
+    	}
+		}
+  },
+  methods: {
+    show() { console.log(this.rgb) }
+  }
+})
js
computed: {
+  allChecked: {
+    get() {
+      return this.goodsList.every(item => item.goods_state)
+    },
+    set(newVal) {
+      this.goodsList.forEach(item => item.goods_state = newVal)
+    }
+  }
+}
computed: {
+  allChecked: {
+    get() {
+      return this.goodsList.every(item => item.goods_state)
+    },
+    set(newVal) {
+      this.goodsList.forEach(item => item.goods_state = newVal)
+    }
+  }
+}

axios

axios一个专注于网络请求的库

基本语法:

js
axios({
+  method: '请求类型',
+  // URL中的query参数
+  params: {
+    
+  },
+  // body参数
+  data: {
+  
+}
+  url: '请求的URL地址',
+}).then((result) => {
+  //.then用来指定成功的回调,result是请求成功后的结果
+})
axios({
+  method: '请求类型',
+  // URL中的query参数
+  params: {
+    
+  },
+  // body参数
+  data: {
+  
+}
+  url: '请求的URL地址',
+}).then((result) => {
+  //.then用来指定成功的回调,result是请求成功后的结果
+})

结合async和await使用axios

js
document.querySelector('#btn').addEventListener('click', async function(){
+  // 如果调用方法返回值是Promise实例,则可以在前面添加await,await只能用在被async“修饰”的方法中
+  // 解构赋值的时候使用:进行重命名
+  const { data: res } = await axios({
+    method: 'POST',
+    url: 'xxx',
+    data: {
+      name: '111'
+    }
+  })
+  console.log(res.data)
+})
document.querySelector('#btn').addEventListener('click', async function(){
+  // 如果调用方法返回值是Promise实例,则可以在前面添加await,await只能用在被async“修饰”的方法中
+  // 解构赋值的时候使用:进行重命名
+  const { data: res } = await axios({
+    method: 'POST',
+    url: 'xxx',
+    data: {
+      name: '111'
+    }
+  })
+  console.log(res.data)
+})
  • axios.get()
  • axios.post()
  • axios.delete()
  • axios.put()

vue工程中使用

js
// main.js
+
+import Vue from 'vue'
+import App from './App.vue'
+import axios from 'axios'
+
+Vue.config.productionTip = false
+
+// 缺点:不利于api接口复用
+// 组件实例中直接用`this.$http`使用
+// axios.defaults.baseURL = '请求根路径'
+// Vue.prototype.$http = axios
+
+new Vue({
+  reder: h => h(App)
+}).$mount(#app)
// main.js
+
+import Vue from 'vue'
+import App from './App.vue'
+import axios from 'axios'
+
+Vue.config.productionTip = false
+
+// 缺点:不利于api接口复用
+// 组件实例中直接用`this.$http`使用
+// axios.defaults.baseURL = '请求根路径'
+// Vue.prototype.$http = axios
+
+new Vue({
+  reder: h => h(App)
+}).$mount(#app)

vue-cli

单页面应用程序(Single Page Application)简称SPA,指的是一个Web网站中只有唯一的一个HTML页面,所有的功能与交互都在这唯一的一个页面内完成。

vue-cli是Vue.js开发的标准工具。简化了基于webpack创建工程化的Vue项目的过程。

安装

npm install -g @vue/cli

创建项目

vue create projectName

vue组件

组件组成

组件后缀名是.vue,vue组件包括三个组成部分

template

vue
<!--template是一个虚拟标签,只起到包裹作用,不会被渲染成任何实质性HTML-->
+<template>
+	<div>
+    <!--template中只能有一个根元素-->
+  </div>
+</template>
<!--template是一个虚拟标签,只起到包裹作用,不会被渲染成任何实质性HTML-->
+<template>
+	<div>
+    <!--template中只能有一个根元素-->
+  </div>
+</template>

script

vue
<script>
+export default {
+  name: 'xxx',// <keep-alive>实现组件缓存功能,调整工具中看到的标签名称
+  // data必须是一个函数
+  data() {
+    return {
+      xx: xx
+    }
+  },
+  methods: {
+    fun() {
+      // 组件中的this代表当前组件的实例对象
+      console.log(this)
+      this.xx = yy
+    }
+  },
+  watch: {},
+  computed: {}
+  ...
+}
+</script>
<script>
+export default {
+  name: 'xxx',// <keep-alive>实现组件缓存功能,调整工具中看到的标签名称
+  // data必须是一个函数
+  data() {
+    return {
+      xx: xx
+    }
+  },
+  methods: {
+    fun() {
+      // 组件中的this代表当前组件的实例对象
+      console.log(this)
+      this.xx = yy
+    }
+  },
+  watch: {},
+  computed: {}
+  ...
+}
+</script>

style

vue
<style lang="less">/* 默认lang="css" */
+
+</style>
<style lang="less">/* 默认lang="css" */
+
+</style>

组件之间的父子关系

组件被封装好后,彼此之间是相互独立的,不存在父子关系。

使用组件时,根据彼此的嵌套关系,形成了父子关系,兄弟关系。

组件使用步骤

注册私有子组件

  1. import语法导入需要的组件
js
import A from '@/components/A.vue
import A from '@/components/A.vue
  1. 使用components节点注册组件
js
export default {
+	components: {
+		A //注册名称主要用于 以标签形式把注册的组件 渲染和使用到页面结构之中
+  }
+}
export default {
+	components: {
+		A //注册名称主要用于 以标签形式把注册的组件 渲染和使用到页面结构之中
+  }
+}
  1. 以标签形式使用注册的组件
vue
<div class="box">
+  <A></A>
+</div>
<div class="box">
+  <A></A>
+</div>

注册全局组件

js
// main.js
+
+import Test from '@/components/Test.vue'
+
+Vue.component('MyTest', Test)
// main.js
+
+import Test from '@/components/Test.vue'
+
+Vue.component('MyTest', Test)

组件的props

props是组件的自定义属性,在封装通用组件的时候,合理的使用props可以极大提高组件的复用性。

  • props中的数据,可以直接在模板结构中使用
  • props是只读的
vue
<script>
+export default {
+  props: {
+    initCount: {
+      default: 0,//默认值
+      type: Number, //规定属性的值类型,如果传递的值不符合,则会报错
+      required: true //必填项
+    }
+  },
+  data() {
+    return {
+      count: this.initCount
+    }
+  }
+}
+</script>
<script>
+export default {
+  props: {
+    initCount: {
+      default: 0,//默认值
+      type: Number, //规定属性的值类型,如果传递的值不符合,则会报错
+      required: true //必填项
+    }
+  },
+  data() {
+    return {
+      count: this.initCount
+    }
+  }
+}
+</script>

组件之间样式冲突

默认情况下,写在组件中的样式会全局生效,原因是:

  1. 单页面应用程序中所有的DOM结构都是基于唯一的index.hmtl页面进行呈现的
  2. 每个组件中的样式都会影响整个index.html的DOM元素

解决方案

  • 使用自定义属性:DOM元素增加自定义属性data-v-xxx,使用属性选择器设置样式div[data-v-xxx]

  • style标签增加scoped属性,会自动为每个标签生成data-v属性

deep样式穿透

/deep/ 选择器

当使用第三方组件库,如果有修改第三方组件库的默认样式需求,需要用到deep

组件的生命周期

生命周期是指一个组件从创建->运行->销毁的整个阶段。

分类

  • 组件创建阶段
    • beforeCreate:组件的props/data/methods尚未被创建,都处于不可用状态
    • created:组件的props/data/methods被创建,都处于可用状态,但是组件的模板结构尚未生成
    • beforeMount:将要把内存中编译好的HTML结果渲染到浏览器中,此时浏览器中还没有当前组件的DOM结构
    • mounted:已经把内存中编译好的HTML结果渲染到浏览器中,此时浏览器已经包含当前组件DOM结构
  • 组件运行阶段
    • beforeUpdate:将要根据变化过后、最新的数据重新渲染组件的模版结构
    • updated:已经根据最新的数据,完成了组件DOM结构的重新渲染
  • 组件销毁阶段
    • beforeDestroy:将要销毁此组件但还未销毁,组件还处于正常工作状态
    • destroyed:组件已被销毁,此组件在浏览器中对应的DOM结构已被完全移除

组件数据共享

父组件向子组件共享数据需要使用自定义属性

子向父传值需要使用自定义事件

js
//子组件
+methods: {
+  add() {
+    this.count += 1
+	  this.$emit('numchange', this.count)
+  }
+}
//子组件
+methods: {
+  add() {
+    this.count += 1
+	  this.$emit('numchange', this.count)
+  }
+}
js
//父组件
+<Son @numchange="getNewCount"></Son>
+
+---
+
+methods: {
+	getNewCount(val) {
+		this.countFromSon = val
+	}
+}
//父组件
+<Son @numchange="getNewCount"></Son>
+
+---
+
+methods: {
+	getNewCount(val) {
+		this.countFromSon = val
+	}
+}

兄弟组件之间数据共享需要使用EventBus

js
//A组件
+import bus from './eventBus.js'
+
+methods: {
+	sendMsg() {
+		bus.$emit('share', this.msg)
+	}
+}
+
+//eventBus.js
+import Vue from 'vue'
+
+export default new Vue()
+
+//兄弟组件B
+import bus from './eventBus.js'
+
+created() {
+	bus.$on('share', val => {
+		this.msgFromSibling = val
+	})
+}
//A组件
+import bus from './eventBus.js'
+
+methods: {
+	sendMsg() {
+		bus.$emit('share', this.msg)
+	}
+}
+
+//eventBus.js
+import Vue from 'vue'
+
+export default new Vue()
+
+//兄弟组件B
+import bus from './eventBus.js'
+
+created() {
+	bus.$on('share', val => {
+		this.msgFromSibling = val
+	})
+}

ref引用

ref用来辅助开发者在不依赖jQuery的情况下,获取DOM元素或组件的引用

每个vue组件的实例上,都包含一个$refs对象,里面存储着对应的DOM元素或组件的引用,默认情况下组件的$refs指向一个空对象

使用ref引用页面上的DOM元素

vue
<div ref="myDiv"></div>
+
+// methods中访问
+this.$refs.myDiv
<div ref="myDiv"></div>
+
+// methods中访问
+this.$refs.myDiv

使用ref引用组件

vue
<Son ref="compSon"></Son>
+
+// methods中访问
+this.$refs.compSon.方法
+this.$refs.compSon.属性
<Son ref="compSon"></Son>
+
+// methods中访问
+this.$refs.compSon.方法
+this.$refs.compSon.属性

$nextTick(callback)

组件的$nextTick(callback)方法,会把callback函数会推迟到下一个DOM更新之后执行。

动态组件

动态组件指的是动态切换组件的显示与隐藏。

component标签is属性

vue
<template>
+	<component :is="componentName"></component>
+</template>
+<script>
+import Left from '@/components/Left.vue'  
+import Right from '@/components/Right.vue'
+export default {
+  data() {
+    return {
+      componentName: "Left"
+    }
+  },
+  components: {
+    Left,
+    Right
+  }
+}
+</script>
<template>
+	<component :is="componentName"></component>
+</template>
+<script>
+import Left from '@/components/Left.vue'  
+import Right from '@/components/Right.vue'
+export default {
+  data() {
+    return {
+      componentName: "Left"
+    }
+  },
+  components: {
+    Left,
+    Right
+  }
+}
+</script>

keep-alive

keep-alive标签能把内部的组件进行缓存,而不是销毁组件

vue
<template>
+	<keep-alive>
+    <component :is="componentName"></component>
+  </keep-alive>
+</template>
<template>
+	<keep-alive>
+    <component :is="componentName"></component>
+  </keep-alive>
+</template>

对应的生命周期函数

  • 被缓存:deactivated生命周期函数
  • 被激活:activated生命周期函数,当组件第一次被创建也会执行

include/exclude属性

  • include可以指定哪些组件被缓存,只有名称匹配的组件会被缓存,多个用,分隔

  • exclude相反

  • 两个属性不能同时使用

vue
<template>
+	<keep-alive include="Left">
+    <component :is="componentName"></component>
+  </keep-alive>
+</template>
<template>
+	<keep-alive include="Left">
+    <component :is="componentName"></component>
+  </keep-alive>
+</template>

插槽

插槽(Slot)是vue为组件的封装者提供的能力。允许开发者在封装组件时,把不确定的、希望由用户指定的部份定义为插槽。

vue
<!--Left.vue-->
+<template>
+	<slot name="default">
+    这里可以指定默认内容,会被覆盖
+  </slot>
+</template>
<!--Left.vue-->
+<template>
+	<slot name="default">
+    这里可以指定默认内容,会被覆盖
+  </slot>
+</template>

渲染Left组件时

vue
<template>
+	<Left>
+    <!--此区域必须在组件中声明插槽才会渲染-->
+    <!--默认情况下 会被填充到名为default的插槽内-->
+    <p>
+      自定义内容
+  </p>
+  </Left>
+</template>
+
+---
+<template>
+	<Left>
+		<template v-slot:default> 
+      <p>
+      自定义内容
+  		</p>
+		</template>
+  </Left>
+</template>
+---
+<template>
+	<Left>
+		<template #default> 
+      <p>
+      自定义内容
+  		</p>
+		</template>
+  </Left>
+</template>
<template>
+	<Left>
+    <!--此区域必须在组件中声明插槽才会渲染-->
+    <!--默认情况下 会被填充到名为default的插槽内-->
+    <p>
+      自定义内容
+  </p>
+  </Left>
+</template>
+
+---
+<template>
+	<Left>
+		<template v-slot:default> 
+      <p>
+      自定义内容
+  		</p>
+		</template>
+  </Left>
+</template>
+---
+<template>
+	<Left>
+		<template #default> 
+      <p>
+      自定义内容
+  		</p>
+		</template>
+  </Left>
+</template>

slot

  • 声明一个插槽区域

  • 每个插槽都要有一个name属性,如果省略,则使用默认名称default

  • 作用域插槽:封装组件时,为预留的slot提供属性对应的值

    • html
      定义:
      +<slot name="content" msg="hello world"></slot>
      +-- 
      +使用:
      +<template #content="scope">
      +	<p>
      +    {{ scope.msg }}
      +  </p>
      +</template>
      定义:
      +<slot name="content" msg="hello world"></slot>
      +-- 
      +使用:
      +<template #content="scope">
      +	<p>
      +    {{ scope.msg }}
      +  </p>
      +</template>
    • 作用域插槽解构赋值

      • html
        <slot name="content" msg="hello world" :user="user"></slot>
        +
        +---
        +
        +<template #content="{msg, user}">
        +	<p>
        +    {{msg}}
        +  </p>
        +  <p>
        +    {{user}}
        +  </p>
        +</template>
        <slot name="content" msg="hello world" :user="user"></slot>
        +
        +---
        +
        +<template #content="{msg, user}">
        +	<p>
        +    {{msg}}
        +  </p>
        +  <p>
        +    {{user}}
        +  </p>
        +</template>

v-slot

  • 只能用在template标签上
  • 使用具名插槽简写形式#default

自定义指令

私有自定义指令

在每个vue组件中,可以在directives节点下声明私有自定义指令

bind函数

  • 当指令第一次被绑定到元素上的时候,会立刻触发bind函数,且只会触发一次

  • 形参el:绑定了此指令的原生的DOM对象

  • 形参binding:传递过来的参数是binding中的value

js
directives: {
+  color: {
+    bind(el, binding) {
+      el.style.color = binding.value
+    }
+  }
+}
directives: {
+  color: {
+    bind(el, binding) {
+      el.style.color = binding.value
+    }
+  }
+}

update函数

  • 第一次不会触发

  • 在DOM更新的时候就会触发update函数

js
directives: {
+  color: {
+    update(el, binding) {
+      el.style.color = binding.value
+    }
+  }
+}
directives: {
+  color: {
+    update(el, binding) {
+      el.style.color = binding.value
+    }
+  }
+}

函数简写

如果bind和update函数的逻辑完全相同,则对象格式的自定义指令可以简写为

js
directives: {
+  color(el, binding) {
+    el.style.color = binding.value
+  }
+}
directives: {
+  color(el, binding) {
+    el.style.color = binding.value
+  }
+}

全局自定义指令

使用Vue.directive声明

js
Vue.directive('color', function(el, binding){
+  el.style.color = binding.value
+})
+---
+Vue.directive('color', {
+  binding(el, binding) {
+      el.style.color = binding.value
+  },
+  update(el, binding) {
+      el.style.color = binding.value
+  }
+})
Vue.directive('color', function(el, binding){
+  el.style.color = binding.value
+})
+---
+Vue.directive('color', {
+  binding(el, binding) {
+      el.style.color = binding.value
+  },
+  update(el, binding) {
+      el.style.color = binding.value
+  }
+})

路由(Router)

模式

  • hash模式

  • history模式

    • publicPath/baseUrl

      js
      //vue.config.js
      +module.exports = {
      +  publicPath: process.env.NODE_ENV === 'production'
      +    ? '/production-sub-path/'
      +    : '/'
      +}
      //vue.config.js
      +module.exports = {
      +  publicPath: process.env.NODE_ENV === 'production'
      +    ? '/production-sub-path/'
      +    : '/'
      +}
    • createWebHistory(base?)

工作方式

  1. 用户点击路由链接
  2. 导致了URL地址栏中Hash值发生变化
  3. 前端路由监听到了Hash地址变化
  4. 前端路由把当前Hash地址对应的组件渲染到浏览器中

vue-router

安装

npm i vue-router@version

创建路由模块

在src目录下,新建router/index.js模块

js
import Vue from 'vue'
+import VueRouter from 'vue-router'
+
+// 调用Vue.use()函数,把VueRouter安装为Vue的插件
+Vue.use(VueRouter)
+
+const router = new VueRouter()
+
+export default router
import Vue from 'vue'
+import VueRouter from 'vue-router'
+
+// 调用Vue.use()函数,把VueRouter安装为Vue的插件
+Vue.use(VueRouter)
+
+const router = new VueRouter()
+
+export default router

导入并挂载路由模块

main.js中挂载路由模块

js
//main.js
+import ...
+//import router from '@/router/index.js'
+//简写
+import router from '@/router'
+
+new Vue({
+  render: h => h(App),
+  //router: router
+  //属性名 属性值一致 可以简写
+  router
+})
//main.js
+import ...
+//import router from '@/router/index.js'
+//简写
+import router from '@/router'
+
+new Vue({
+  render: h => h(App),
+  //router: router
+  //属性名 属性值一致 可以简写
+  router
+})

声明路由链接和占位符

  • 路由链接:router-link
  • 占位符router-view,组件在这里展示
vue
<template>
+	<div class="container">
+    <a href="#/home">首页</a>
+    <a href="#/movie">电影</a>
+    <a href="#/about">关于</a>
+    <hr/>
+    可以用router-link标签代替普通a标签,可以省略#
+    <router-link to="#/home">首页</router-link>
+    <router-link to="#/movie">电影</router-link>
+    <router-link to="#/about">关于</router-link>
+    
+    <router-view></router-view>
+  </div>
+</template>
<template>
+	<div class="container">
+    <a href="#/home">首页</a>
+    <a href="#/movie">电影</a>
+    <a href="#/about">关于</a>
+    <hr/>
+    可以用router-link标签代替普通a标签,可以省略#
+    <router-link to="#/home">首页</router-link>
+    <router-link to="#/movie">电影</router-link>
+    <router-link to="#/about">关于</router-link>
+    
+    <router-view></router-view>
+  </div>
+</template>

修改router/index.js

js
import Vue from 'vue'
+import VueRouter from 'vue-router'
+
+import Home from '@/components/Home.vue'
+import Movie from '@/components/Movie.vue'
+import About from '@/components/About.vue'
+
+// 调用Vue.use()函数,把VueRouter安装为Vue的插件
+Vue.use(VueRouter)
+
+const router = new VueRouter({
+  // routes是一个数组:定义hash地址和组件之间的对应关系
+  routes: [
+    // 路由规则
+    { path: '/', redirect: '/home' }, // 重定向
+    { path: '/home', component: Home },
+    { path: '/movie', component: Movie },
+    { path: '/about', component: About }
+  ]
+})
+
+export default router
import Vue from 'vue'
+import VueRouter from 'vue-router'
+
+import Home from '@/components/Home.vue'
+import Movie from '@/components/Movie.vue'
+import About from '@/components/About.vue'
+
+// 调用Vue.use()函数,把VueRouter安装为Vue的插件
+Vue.use(VueRouter)
+
+const router = new VueRouter({
+  // routes是一个数组:定义hash地址和组件之间的对应关系
+  routes: [
+    // 路由规则
+    { path: '/', redirect: '/home' }, // 重定向
+    { path: '/home', component: Home },
+    { path: '/movie', component: Movie },
+    { path: '/about', component: About }
+  ]
+})
+
+export default router

嵌套路由

通过路由实现组件的嵌套展示,叫做嵌套路由

  • 模板内容中又有子级路由链接
  • 点击子级路由链接显示子级模板内容

通过children属性声明子路由规则

js
import Vue from 'vue'
+import VueRouter from 'vue-router'
+
+import Home from '@/components/Home.vue'
+import Movie from '@/components/Movie.vue'
+import About from '@/components/About.vue'
+import Tab1 from '@/components/tabs/Tab1.vue'
+import Tab2 from '@/components/tabs/Tab2.vue'
+
+// 调用Vue.use()函数,把VueRouter安装为Vue的插件
+Vue.use(VueRouter)
+
+const router = new VueRouter({
+  // routes是一个数组:定义hash地址和组件之间的对应关系
+  routes: [
+    // 路由规则
+    { path: '/', redirect: '/home' }, // 重定向
+    { path: '/home', component: Home },
+    { path: '/movie', component: Movie },
+    { 
+      path: '/about',
+      component: About,
+      children: [
+        { path: 'tab1', component: Tab1 },
+        { path: 'tab2', component: Tab2 }
+      ]
+    }
+  ]
+})
+
+export default router
import Vue from 'vue'
+import VueRouter from 'vue-router'
+
+import Home from '@/components/Home.vue'
+import Movie from '@/components/Movie.vue'
+import About from '@/components/About.vue'
+import Tab1 from '@/components/tabs/Tab1.vue'
+import Tab2 from '@/components/tabs/Tab2.vue'
+
+// 调用Vue.use()函数,把VueRouter安装为Vue的插件
+Vue.use(VueRouter)
+
+const router = new VueRouter({
+  // routes是一个数组:定义hash地址和组件之间的对应关系
+  routes: [
+    // 路由规则
+    { path: '/', redirect: '/home' }, // 重定向
+    { path: '/home', component: Home },
+    { path: '/movie', component: Movie },
+    { 
+      path: '/about',
+      component: About,
+      children: [
+        { path: 'tab1', component: Tab1 },
+        { path: 'tab2', component: Tab2 }
+      ]
+    }
+  ]
+})
+
+export default router
js
const routes = [
+  {
+    path: '/',
+    component: Home,
+    meta: {
+      keepAlive: true,
+      isRecord: true,
+      top: 0
+    }
+  },
+  {
+    path: '/user',
+    component: User
+  }
+]
+
+const router = new VueRouter({
+  routes,
+  scrollBehavior(to, from, savedPosition) {
+    if (savedPosition) {
+      return savedPosition
+    }
+    return {
+      x: 0,
+      y: to.meta.top || 0
+    }
+  }
+})
const routes = [
+  {
+    path: '/',
+    component: Home,
+    meta: {
+      keepAlive: true,
+      isRecord: true,
+      top: 0
+    }
+  },
+  {
+    path: '/user',
+    component: User
+  }
+]
+
+const router = new VueRouter({
+  routes,
+  scrollBehavior(to, from, savedPosition) {
+    if (savedPosition) {
+      return savedPosition
+    }
+    return {
+      x: 0,
+      y: to.meta.top || 0
+    }
+  }
+})

动态路由匹配

把Hash地址中可变的部分定义为参数项,可以提高路由规则的复用性。

使用:来定义路由的参数项

js
{ path: '/movie/:id', component: Movie, props: true}
{ path: '/movie/:id', component: Movie, props: true}
  • 组件中可以通过$route.params.id获取路径参数(Path Variable)

$route.params.query获取查询参数

$route.params.fullPath:包含路径和参数

$route.params.path:只有路径没有参数

  • 可以在路由规则中添加props传餐,在组件中定义props直接获取

导航

声明式导航

  • a标签
  • route-link标签

编程式导航

  • location.href跳转
  • vue-router的编程式导航api
    • this.$router.push('hash地址'):跳转到hash地址,增加一条历史记录
    • this.$router.replace('hash地址'):跳转到hash地址,并替换掉当前的历史记录
    • this.$router.go(数值n):可以在浏览历史中前进或后退
      • this.$router.back()
      • this.$router.forward()

导航守卫

可以控制路由的访问权限

全局前置守卫

每次发生路由导航跳转时,都会触发前置守卫,在前置守卫中,可以对每个路由进行访问权限控制。

js
const router = new VueRouter({})
+// 每次路由跳转都会触发回调函数                              
+router.beforeEach((to, from, next) => {
+  // to:将要访问的路由信息对象
+  // from:将要离开的路由信息对象
+  // next:一个函数,调用next()表示放行,允许这次路由导航
+})
const router = new VueRouter({})
+// 每次路由跳转都会触发回调函数                              
+router.beforeEach((to, from, next) => {
+  // to:将要访问的路由信息对象
+  // from:将要离开的路由信息对象
+  // next:一个函数,调用next()表示放行,允许这次路由导航
+})
next的三种调用方式
  • next():直接放行
  • next('/path'):强制跳转到指定页面
  • next(false):不允许跳转,强制保留在当前页面

Vue3.0

创建项目

vite

使用:npm init vite-app projectName

create-vue

create-vue创建的项目也是基于vite的构建设置

使用:npm create vue@3 或者npm init vue@latest

crete-vue同样支持vue2:npm create vue@2 / npm init vue@2

Vite和Webpack的区别(内容来自ChatGPT):

Vite和Webpack是两种常用的前端构建工具。

  1. 底层实现不同

Vite使用ES modules(ESM)作为模块系统管理,而Webpack使用CommonJS来管理模块。这意味着,在使用Webpack打包项目时,所有模块都将被打包到一个或多个bundle.js文件中,而Vite将原始文件作为模块提取和处理,并将其以一种非常高效的方式提供给浏览器。

  1. 开发环境下的性能

Vite在开发环境下启动非常快,不需要等待代码打包时间,并且在修改代码时,也可以直接进行热更新,非常适合在开发阶段使用。而Webpack在开发模式下代码打包速度较慢,启动速度也相对较慢,修改代码后也需要较长的时间来重新打包。

  1. 生产环境下的性能

在生产环境下,Webpack可以通过代码分割(Code Splitting)和 Tree Shaking来优化代码,减小打包后的文件大小。而Vite在生产环境下目前还不支持代码分割。因此,如果项目需要大量使用Code Splitting和Tree Shaking等技术,使用Webpack可能会更加合适。

  1. 生态和可定制性

Webpack具有强大的社区和众多的插件和Loader来处理各种文件和场景,可以根据不同的需求进行高度的定制。而Vite的生态和可定制性方面要弱于Webpack,它的插件数量还比较少。

总的来说,Vite是一种专门为现代浏览器设计的前端构建工具,它在开发环境下性能卓越,但在生产环境方面还有一些局限。而Webpack则是一种更加稳健和灵活的构建工具,它可以用于各种复杂的场景和需求,并且有着更强大的生态和定制能力,但需要进行更多的配置。选择使用哪种工具,应该根据具体项目需求和使用场景来进行选择。

create-vue和vue-cli的区别:

  • vue-cli基于webpack,而create-vue基于vite

构建一个vue实例

js
import { createApp } from 'vue'
+
+createApp({
+  data() {
+    return {
+      count: 0
+    }
+  }
+}).mount('#app')
import { createApp } from 'vue'
+
+createApp({
+  data() {
+    return {
+      count: 0
+    }
+  }
+}).mount('#app')

feature

  • vue3的模板中可以有多个根节点

  • api类型不同,vue2使用选项式api,vue3使用组合式api

  • 定义变量和方法方式不同:vue2定义在data和methods节点中,vue3使用setup()方法,此方法在组件初始化构造的时候触发

    • 从vue引入reactive

    • 使用reactive()方法声明数据为响应式数据const state = reactive({ count: 0 })

    • <script setup>:顶层的导入和变量声明可在同一组件的模板中直接使用。可以理解为模板中的表达式和 script setup中的代码处在同一个作用域中。

    • 使用ref()定义响应式变量,ref() 将传入参数的值包装为一个带 .value 属性的 ref 对象:

      js
      import { ref } from 'vue'
      +
      +const count = ref(0)
      import { ref } from 'vue'
      +
      +const count = ref(0)

      和响应式对象的属性类似,ref 的 .value 属性也是响应式的。同时,当值为对象类型时,会用 reactive() 自动转换它的 .value

      一个包含对象类型值的 ref 可以响应式地替换整个对象:

      js

      const objectRef = ref({ count: 0 })
      +
      +// 这是响应式的替换
      +objectRef.value = { count: 1 }
      const objectRef = ref({ count: 0 })
      +
      +// 这是响应式的替换
      +objectRef.value = { count: 1 }

    响应式

    https://cn.vuejs.org/guide/essentials/reactivity-fundamentals.html

  • 类和样式绑定

  • 生命周期变化

响应式对象

  • ref()

    • 标注类型

      • typescript
        import type { Ref } from 'vue'
        +
        +const count: Ref<number> = ref(0)
        import type { Ref } from 'vue'
        +
        +const count: Ref<number> = ref(0)
    • 当 ref 在模板中作为顶层属性被访问时,它们会被自动“解包”,所以不需要使用 .value

    • 当一个 ref 被嵌套在一个深层响应式对象中,作为属性被访问或更改时,它会自动解包

+ + + + \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 000000000..e8e67b278 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,25 @@ + + + + + + Frontend | 故事 + + + + + + + + + + + + + + +
Skip to content

Frontend

没点前端开发能力自己想写点东西太难了,总不能全写命令行工具吧orz。。。

+ + + + \ No newline at end of file diff --git "a/frontend/others/ElementPlus el-upload\346\272\220\347\240\201\345\210\206\346\236\220.html" "b/frontend/others/ElementPlus el-upload\346\272\220\347\240\201\345\210\206\346\236\220.html" new file mode 100644 index 000000000..ea0891e21 --- /dev/null +++ "b/frontend/others/ElementPlus el-upload\346\272\220\347\240\201\345\210\206\346\236\220.html" @@ -0,0 +1,627 @@ + + + + + + ElementPlus el-upload源码分析 | 故事 + + + + + + + + + + + + + + +
Skip to content

ElementPlus el-upload源码分析

背景

使用el-upload实现限制上传文件个数、超出数量后后者覆盖前者、自动上传且自行实现上传文件请求,文档写的不清不楚,试了半天都不行,还是看了源码才明白el-upload组件的整个流程。

https://github.com/ElemeFE/element/blob/dev/packages/upload/src/index.vue

https://github.com/ElemeFE/element/blob/dev/packages/upload/src/upload.vue

分析

  • 入口:index.vue的render函数
  • uploadData定义了参数结构,用于向子组件传参数,props中的几个属性就是el-upload标签的钩子
js
// index.vue
+render(h) {
+    let uploadList;
+
+    if (this.showFileList) {
+      uploadList = (
+        <UploadList
+          disabled={this.uploadDisabled}
+          listType={this.listType}
+          files={this.uploadFiles}
+          on-remove={this.handleRemove}
+          handlePreview={this.onPreview}>
+          {
+            (props) => {
+              if (this.$scopedSlots.file) {
+                return this.$scopedSlots.file({
+                  file: props.file
+                });
+              }
+            }
+          }
+        </UploadList>
+      );
+    }
+
+    const uploadData = {
+      props: {
+        type: this.type,
+        drag: this.drag,
+        action: this.action,
+        multiple: this.multiple,
+        'before-upload': this.beforeUpload,
+        'with-credentials': this.withCredentials,
+        headers: this.headers,
+        name: this.name,
+        data: this.data,
+        accept: this.accept,
+        fileList: this.uploadFiles,
+        autoUpload: this.autoUpload,
+        listType: this.listType,
+        disabled: this.uploadDisabled,
+        limit: this.limit,
+        'on-exceed': this.onExceed,
+        'on-start': this.handleStart,
+        'on-progress': this.handleProgress,
+        'on-success': this.handleSuccess,
+        'on-error': this.handleError,
+        'on-preview': this.onPreview,
+        'on-remove': this.handleRemove,
+        'http-request': this.httpRequest
+      },
+      ref: 'upload-inner'
+    };
+
+    const trigger = this.$slots.trigger || this.$slots.default;
+    const uploadComponent = <upload {...uploadData}>{trigger}</upload>;
+
+    return (
+      <div>
+        { this.listType === 'picture-card' ? uploadList : ''}
+        {
+          this.$slots.trigger
+            ? [uploadComponent, this.$slots.default]
+            : uploadComponent
+        }
+        {this.$slots.tip}
+        { this.listType !== 'picture-card' ? uploadList : ''}
+      </div>
+    );
+  }
// index.vue
+render(h) {
+    let uploadList;
+
+    if (this.showFileList) {
+      uploadList = (
+        <UploadList
+          disabled={this.uploadDisabled}
+          listType={this.listType}
+          files={this.uploadFiles}
+          on-remove={this.handleRemove}
+          handlePreview={this.onPreview}>
+          {
+            (props) => {
+              if (this.$scopedSlots.file) {
+                return this.$scopedSlots.file({
+                  file: props.file
+                });
+              }
+            }
+          }
+        </UploadList>
+      );
+    }
+
+    const uploadData = {
+      props: {
+        type: this.type,
+        drag: this.drag,
+        action: this.action,
+        multiple: this.multiple,
+        'before-upload': this.beforeUpload,
+        'with-credentials': this.withCredentials,
+        headers: this.headers,
+        name: this.name,
+        data: this.data,
+        accept: this.accept,
+        fileList: this.uploadFiles,
+        autoUpload: this.autoUpload,
+        listType: this.listType,
+        disabled: this.uploadDisabled,
+        limit: this.limit,
+        'on-exceed': this.onExceed,
+        'on-start': this.handleStart,
+        'on-progress': this.handleProgress,
+        'on-success': this.handleSuccess,
+        'on-error': this.handleError,
+        'on-preview': this.onPreview,
+        'on-remove': this.handleRemove,
+        'http-request': this.httpRequest
+      },
+      ref: 'upload-inner'
+    };
+
+    const trigger = this.$slots.trigger || this.$slots.default;
+    const uploadComponent = <upload {...uploadData}>{trigger}</upload>;
+
+    return (
+      <div>
+        { this.listType === 'picture-card' ? uploadList : ''}
+        {
+          this.$slots.trigger
+            ? [uploadComponent, this.$slots.default]
+            : uploadComponent
+        }
+        {this.$slots.tip}
+        { this.listType !== 'picture-card' ? uploadList : ''}
+      </div>
+    );
+  }
  • 渲染upload子组件,下面是upload组件的render函数
js
// upload.vue
+render(h) {
+    let {
+      handleClick,
+      drag,
+      name,
+      handleChange,
+      multiple,
+      accept,
+      listType,
+      uploadFiles,
+      disabled,
+      handleKeydown
+    } = this;
+    const data = {
+      class: {
+        'el-upload': true
+      },
+      on: {
+        click: handleClick,
+        keydown: handleKeydown
+      }
+    };
+    data.class[`el-upload--${listType}`] = true;
+    return (
+      <div {...data} tabindex="0" >
+        {
+          drag
+            ? <upload-dragger disabled={disabled} on-file={uploadFiles}>{this.$slots.default}</upload-dragger>
+            : this.$slots.default
+        }
+        <input class="el-upload__input" type="file" ref="input" name={name} on-change={handleChange} multiple={multiple} accept={accept}></input>
+      </div>
+    );
+  }
// upload.vue
+render(h) {
+    let {
+      handleClick,
+      drag,
+      name,
+      handleChange,
+      multiple,
+      accept,
+      listType,
+      uploadFiles,
+      disabled,
+      handleKeydown
+    } = this;
+    const data = {
+      class: {
+        'el-upload': true
+      },
+      on: {
+        click: handleClick,
+        keydown: handleKeydown
+      }
+    };
+    data.class[`el-upload--${listType}`] = true;
+    return (
+      <div {...data} tabindex="0" >
+        {
+          drag
+            ? <upload-dragger disabled={disabled} on-file={uploadFiles}>{this.$slots.default}</upload-dragger>
+            : this.$slots.default
+        }
+        <input class="el-upload__input" type="file" ref="input" name={name} on-change={handleChange} multiple={multiple} accept={accept}></input>
+      </div>
+    );
+  }
  • 主要关注<input class="el-upload__input" type="file" ref="input" name={name} on-change={handleChange} multiple={multiple} accept={accept}></input>这个jsx代码中绑定了onChange时间,调用函数是handleChange

  • // upload.vue
    +handleChange(ev) {
    +      const files = ev.target.files;
    +
    +      if (!files) return;
    +      this.uploadFiles(files);
    +}
    // upload.vue
    +handleChange(ev) {
    +      const files = ev.target.files;
    +
    +      if (!files) return;
    +      this.uploadFiles(files);
    +}
  • 可以看到handleChange直接调用了uploadFiles函数,而uploadFiles函数就是我们要追溯的内容了

js
uploadFiles(files) {
+  if (this.limit && this.fileList.length + files.length > this.limit) {
+    this.onExceed && this.onExceed(files, this.fileList);
+    return;
+  }
+
+  let postFiles = Array.prototype.slice.call(files);
+  if (!this.multiple) { postFiles = postFiles.slice(0, 1); }
+
+  if (postFiles.length === 0) { return; }
+
+  postFiles.forEach(rawFile => {
+    this.onStart(rawFile);
+    if (this.autoUpload) this.upload(rawFile);
+  });
+},
+upload(rawFile) {
+  this.$refs.input.value = null;
+
+  if (!this.beforeUpload) {
+    return this.post(rawFile);
+  }
+
+  const before = this.beforeUpload(rawFile);
+  if (before && before.then) {
+    before.then(processedFile => {
+      const fileType = Object.prototype.toString.call(processedFile);
+
+      if (fileType === '[object File]' || fileType === '[object Blob]') {
+        if (fileType === '[object Blob]') {
+          processedFile = new File([processedFile], rawFile.name, {
+            type: rawFile.type
+          });
+        }
+        for (const p in rawFile) {
+          if (rawFile.hasOwnProperty(p)) {
+            processedFile[p] = rawFile[p];
+          }
+        }
+        this.post(processedFile);
+      } else {
+        this.post(rawFile);
+      }
+    }, () => {
+      this.onRemove(null, rawFile);
+    });
+  } else if (before !== false) {
+    this.post(rawFile);
+  } else {
+    this.onRemove(null, rawFile);
+  }
+}
uploadFiles(files) {
+  if (this.limit && this.fileList.length + files.length > this.limit) {
+    this.onExceed && this.onExceed(files, this.fileList);
+    return;
+  }
+
+  let postFiles = Array.prototype.slice.call(files);
+  if (!this.multiple) { postFiles = postFiles.slice(0, 1); }
+
+  if (postFiles.length === 0) { return; }
+
+  postFiles.forEach(rawFile => {
+    this.onStart(rawFile);
+    if (this.autoUpload) this.upload(rawFile);
+  });
+},
+upload(rawFile) {
+  this.$refs.input.value = null;
+
+  if (!this.beforeUpload) {
+    return this.post(rawFile);
+  }
+
+  const before = this.beforeUpload(rawFile);
+  if (before && before.then) {
+    before.then(processedFile => {
+      const fileType = Object.prototype.toString.call(processedFile);
+
+      if (fileType === '[object File]' || fileType === '[object Blob]') {
+        if (fileType === '[object Blob]') {
+          processedFile = new File([processedFile], rawFile.name, {
+            type: rawFile.type
+          });
+        }
+        for (const p in rawFile) {
+          if (rawFile.hasOwnProperty(p)) {
+            processedFile[p] = rawFile[p];
+          }
+        }
+        this.post(processedFile);
+      } else {
+        this.post(rawFile);
+      }
+    }, () => {
+      this.onRemove(null, rawFile);
+    });
+  } else if (before !== false) {
+    this.post(rawFile);
+  } else {
+    this.onRemove(null, rawFile);
+  }
+}
  • 首先判断是否超过限制,如果没有则会遍历files调用onStart函数,onStart函数是在index.vue中定义的,做了三件事
    • 基于原始file构建新的file对象,基于当前时间增加uid,如果是图片展示模式,还会创建一个url做展示
    • 将新的file对象push进上传文件的数组uploadFiles中
    • 将处理过的uploadFiles传递给on-change钩子函数(文件状态改变,添加文件、上传成功和上传失败时都会被调用)
js
handleStart(rawFile) {
+  rawFile.uid = Date.now() + this.tempIndex++;
+  let file = {
+    status: 'ready',
+    name: rawFile.name,
+    size: rawFile.size,
+    percentage: 0,
+    uid: rawFile.uid,
+    raw: rawFile
+  };
+
+  if (this.listType === 'picture-card' || this.listType === 'picture') {
+    try {
+      file.url = URL.createObjectURL(rawFile);
+    } catch (err) {
+      console.error('[Element Error][Upload]', err);
+      return;
+    }
+  }
+
+  this.uploadFiles.push(file);
+  this.onChange(file, this.uploadFiles);
+}
handleStart(rawFile) {
+  rawFile.uid = Date.now() + this.tempIndex++;
+  let file = {
+    status: 'ready',
+    name: rawFile.name,
+    size: rawFile.size,
+    percentage: 0,
+    uid: rawFile.uid,
+    raw: rawFile
+  };
+
+  if (this.listType === 'picture-card' || this.listType === 'picture') {
+    try {
+      file.url = URL.createObjectURL(rawFile);
+    } catch (err) {
+      console.error('[Element Error][Upload]', err);
+      return;
+    }
+  }
+
+  this.uploadFiles.push(file);
+  this.onChange(file, this.uploadFiles);
+}
  • onStart执行后会判断是否开启了自动上传,如果开启则会调用upload函数
    • upload中会调用判断是否定义了beforeUpload事件,如果没有直接调用post函数上传
    • 如果定义了beforeUpload则会执行该函数,并根据该方法返回的结果决定如何处理文件
      • 如果 beforeUpload 返回一个 Promise,则等待 Promise 执行完毕,并获取其返回值 processedFile。如果 processedFile 是一个 File 或 Blob 类型,则使用它来替换原始的文件并保留原始文件的属性,最后调用 post 方法上传替换后的文件;否则,仍然使用原始的文件上传。
      • 如果 beforeUpload返回 false,则直接调用 onRemove 方法移除文件。
      • 如果 beforeUpload返回其他非 false 的值,则直接上传原始文件。
js
upload(rawFile) {
+  this.$refs.input.value = null;
+
+  if (!this.beforeUpload) {
+    return this.post(rawFile);
+  }
+
+  const before = this.beforeUpload(rawFile);
+  if (before && before.then) {
+    before.then(processedFile => {
+      const fileType = Object.prototype.toString.call(processedFile);
+
+      if (fileType === '[object File]' || fileType === '[object Blob]') {
+        if (fileType === '[object Blob]') {
+          processedFile = new File([processedFile], rawFile.name, {
+            type: rawFile.type
+          });
+        }
+        for (const p in rawFile) {
+          if (rawFile.hasOwnProperty(p)) {
+            processedFile[p] = rawFile[p];
+          }
+        }
+        this.post(processedFile);
+      } else {
+        this.post(rawFile);
+      }
+    }, () => {
+      this.onRemove(null, rawFile);
+    });
+  } else if (before !== false) {
+    this.post(rawFile);
+  } else {
+    this.onRemove(null, rawFile);
+  }
+}
upload(rawFile) {
+  this.$refs.input.value = null;
+
+  if (!this.beforeUpload) {
+    return this.post(rawFile);
+  }
+
+  const before = this.beforeUpload(rawFile);
+  if (before && before.then) {
+    before.then(processedFile => {
+      const fileType = Object.prototype.toString.call(processedFile);
+
+      if (fileType === '[object File]' || fileType === '[object Blob]') {
+        if (fileType === '[object Blob]') {
+          processedFile = new File([processedFile], rawFile.name, {
+            type: rawFile.type
+          });
+        }
+        for (const p in rawFile) {
+          if (rawFile.hasOwnProperty(p)) {
+            processedFile[p] = rawFile[p];
+          }
+        }
+        this.post(processedFile);
+      } else {
+        this.post(rawFile);
+      }
+    }, () => {
+      this.onRemove(null, rawFile);
+    });
+  } else if (before !== false) {
+    this.post(rawFile);
+  } else {
+    this.onRemove(null, rawFile);
+  }
+}
  • post函数中的上传方法是httpRequest(options),我们可以使用el-upload标签的http-request替换成自己的实现
js
post(rawFile) {
+  const { uid } = rawFile;
+  const options = {
+    headers: this.headers,
+    withCredentials: this.withCredentials,
+    file: rawFile,
+    data: this.data,
+    filename: this.name,
+    action: this.action,
+    onProgress: e => {
+      this.onProgress(e, rawFile);
+    },
+    onSuccess: res => {
+      this.onSuccess(res, rawFile);
+      delete this.reqs[uid];
+    },
+    onError: err => {
+      this.onError(err, rawFile);
+      delete this.reqs[uid];
+    }
+  };
+  const req = this.httpRequest(options);
+  this.reqs[uid] = req;
+  if (req && req.then) {
+    req.then(options.onSuccess, options.onError);
+  }
+},
+handleClick() {
+  if (!this.disabled) {
+    this.$refs.input.value = null;
+    this.$refs.input.click();
+  }
+},
+handleKeydown(e) {
+  if (e.target !== e.currentTarget) return;
+  if (e.keyCode === 13 || e.keyCode === 32) {
+    this.handleClick();
+  }
+}
post(rawFile) {
+  const { uid } = rawFile;
+  const options = {
+    headers: this.headers,
+    withCredentials: this.withCredentials,
+    file: rawFile,
+    data: this.data,
+    filename: this.name,
+    action: this.action,
+    onProgress: e => {
+      this.onProgress(e, rawFile);
+    },
+    onSuccess: res => {
+      this.onSuccess(res, rawFile);
+      delete this.reqs[uid];
+    },
+    onError: err => {
+      this.onError(err, rawFile);
+      delete this.reqs[uid];
+    }
+  };
+  const req = this.httpRequest(options);
+  this.reqs[uid] = req;
+  if (req && req.then) {
+    req.then(options.onSuccess, options.onError);
+  }
+},
+handleClick() {
+  if (!this.disabled) {
+    this.$refs.input.value = null;
+    this.$refs.input.click();
+  }
+},
+handleKeydown(e) {
+  if (e.target !== e.currentTarget) return;
+  if (e.keyCode === 13 || e.keyCode === 32) {
+    this.handleClick();
+  }
+}

结果

上面分析的是如果没有超过limit限制的情况,而我要实现的就是在超过限制之后依然能够自动上传。

回归到代码,uploadFiles函数在执行了onExceed钩子之后直接就return了,所以下面一直到上传的流程都需要我们自己去调用

js
uploadFiles(files) {
+  if (this.limit && this.fileList.length + files.length > this.limit) {
+    this.onExceed && this.onExceed(files, this.fileList);
+    return;
+  }
+  //...
+}
uploadFiles(files) {
+  if (this.limit && this.fileList.length + files.length > this.limit) {
+    this.onExceed && this.onExceed(files, this.fileList);
+    return;
+  }
+  //...
+}

在回顾下我的目标:限制上传文件个数为一个、超出数量后后者覆盖前者、自动上传且自行实现上传文件请求

所以:终极目标是执行完onExceed后能调用上传的函数,el-upload暴露给我们的函数是submit()

js
// index.vue
+submit() {
+  this.uploadFiles
+    .filter(file => file.status === 'ready')
+    .forEach(file => {
+      this.$refs['upload-inner'].upload(file.raw);
+    });
+}
// index.vue
+submit() {
+  this.uploadFiles
+    .filter(file => file.status === 'ready')
+    .forEach(file => {
+      this.$refs['upload-inner'].upload(file.raw);
+    });
+}

可以看到这个函数是将uploadFiles中的文件都调用了一次upload,所以我们要在onExceed函数中要做的事:

  1. 在函数中把原有文件清除掉,然后替换为最新的文件
  2. el-upload暴露的清除文件的方法: clearFiles
js
clearFiles() {
+  this.uploadFiles = [];
+}
clearFiles() {
+  this.uploadFiles = [];
+}
  1. 把最新的文件push到uploadFiles中,这里就要用到上面分析到的handleStart方法,而且这个方法中还会调用onChange钩子
  2. 文件已经push到uploadFiles中,所以只需要调用submit()函数即可

大致代码

vue
<template>
+	<el-upload 
+      action="#" 
+      ref="upload" 
+      v-model:file-list="fileList"
+      :limit="Number(1)" 
+      :on-exceed="handleExceed" 
+      :http-request="handleUpload" 
+      list-type="picture-card"
+    	:auto-upload="true" >
+  </el-upload>
+</template>
+
+<script setup lang="ts">
+import type { UploadProps, UploadInstance, UploadRawFile } from 'element-plus'
+const upload = ref<UploadInstance>()
+/**
+ * 超过限制后调用的方法
+ * @param files
+ */
+const handleExceed: UploadProps['onExceed'] = (files) => {
+   // 清除所有文件
+    upload.value!.clearFiles()
+    const file = files[0] as UploadRawFile
+    file.uid = genFileId()
+    /**
+     * 调用handleStart把file push到uploadFiles中 -> handleStart会调用onChange方法
+     */
+    upload.value!.handleStart(file)
+    upload.value!.submit()
+}
+</script>
<template>
+	<el-upload 
+      action="#" 
+      ref="upload" 
+      v-model:file-list="fileList"
+      :limit="Number(1)" 
+      :on-exceed="handleExceed" 
+      :http-request="handleUpload" 
+      list-type="picture-card"
+    	:auto-upload="true" >
+  </el-upload>
+</template>
+
+<script setup lang="ts">
+import type { UploadProps, UploadInstance, UploadRawFile } from 'element-plus'
+const upload = ref<UploadInstance>()
+/**
+ * 超过限制后调用的方法
+ * @param files
+ */
+const handleExceed: UploadProps['onExceed'] = (files) => {
+   // 清除所有文件
+    upload.value!.clearFiles()
+    const file = files[0] as UploadRawFile
+    file.uid = genFileId()
+    /**
+     * 调用handleStart把file push到uploadFiles中 -> handleStart会调用onChange方法
+     */
+    upload.value!.handleStart(file)
+    upload.value!.submit()
+}
+</script>
+ + + + \ No newline at end of file diff --git a/frontend/others/HackingWithSwift-1.html b/frontend/others/HackingWithSwift-1.html new file mode 100644 index 000000000..be5617536 --- /dev/null +++ b/frontend/others/HackingWithSwift-1.html @@ -0,0 +1,877 @@ + + + + + + HackingWithSwift-1 | 故事 + + + + + + + + + + + + + + +
Skip to content

HackingWithSwift-1

https://www.youtube.com/playlist?list=PLuoeXyslFTuZRi4q4VT6lZKxYbr7so1Mr

WeSplitApp

核心代码

swift
struct ContentView: View {
+    @State private var checkAmount = 0.0
+    @State private var numberOfPeople = 0
+    @State private var tipPercentage = 20
+    @FocusState private var amountIsFocused: Bool
+    
+    private var totalPerPerson: Double {
+        let peopleCount = Double(numberOfPeople + 2)
+        let tipSelection = Double(tipPercentage)
+        return checkAmount * tipSelection / peopleCount / 100
+    }
+    
+    let tipPercentages = [10, 15, 20, 25, 0]
+    
+    var body: some View {
+        NavigationView {
+            Form {
+                Section("") {
+                    TextField("Amount", value: $checkAmount, format: .currency(code: Locale.current.currencyCode ?? "USD"))
+                        .keyboardType(.decimalPad)
+                        .focused($amountIsFocused)
+                    
+                    Picker("Number of people", selection: $numberOfPeople) {
+                        ForEach(2..<100) {
+                            Text("\($0) people")
+                        }
+                    }
+                }
+                
+                Section {
+                    Picker("Tip percentage", selection: $tipPercentage) {
+                        ForEach(tipPercentages, id: \.self) {
+                            Text($0, format: .percent)
+                        }
+                    }
+                    .pickerStyle(.segmented)
+                }header: {
+                    Text("How much tip do you want to leave")
+                }
+                
+                Section {
+                    Text(totalPerPerson, format: .currency(code: Locale.current.currencyCode ?? "USD"))
+                }
+            }
+            .navigationTitle("WeSplit")
+            .toolbar {
+                ToolbarItemGroup(placement: .keyboard) {
+                    Spacer()
+                    Button("Done") {
+                        amountIsFocused = false
+                    }
+                }
+            }
+        }
+    }
+}
struct ContentView: View {
+    @State private var checkAmount = 0.0
+    @State private var numberOfPeople = 0
+    @State private var tipPercentage = 20
+    @FocusState private var amountIsFocused: Bool
+    
+    private var totalPerPerson: Double {
+        let peopleCount = Double(numberOfPeople + 2)
+        let tipSelection = Double(tipPercentage)
+        return checkAmount * tipSelection / peopleCount / 100
+    }
+    
+    let tipPercentages = [10, 15, 20, 25, 0]
+    
+    var body: some View {
+        NavigationView {
+            Form {
+                Section("") {
+                    TextField("Amount", value: $checkAmount, format: .currency(code: Locale.current.currencyCode ?? "USD"))
+                        .keyboardType(.decimalPad)
+                        .focused($amountIsFocused)
+                    
+                    Picker("Number of people", selection: $numberOfPeople) {
+                        ForEach(2..<100) {
+                            Text("\($0) people")
+                        }
+                    }
+                }
+                
+                Section {
+                    Picker("Tip percentage", selection: $tipPercentage) {
+                        ForEach(tipPercentages, id: \.self) {
+                            Text($0, format: .percent)
+                        }
+                    }
+                    .pickerStyle(.segmented)
+                }header: {
+                    Text("How much tip do you want to leave")
+                }
+                
+                Section {
+                    Text(totalPerPerson, format: .currency(code: Locale.current.currencyCode ?? "USD"))
+                }
+            }
+            .navigationTitle("WeSplit")
+            .toolbar {
+                ToolbarItemGroup(placement: .keyboard) {
+                    Spacer()
+                    Button("Done") {
+                        amountIsFocused = false
+                    }
+                }
+            }
+        }
+    }
+}

知识点

  • Form

  • TextField & format: .currency(code: Locale.current.currencyCode ?? "USD")

  • Picker

  • Section

  • toolbar修饰符

效果

image-20220711231438513

GuessTheFlag

核心代码

swift
struct ContentView: View {
+    @State private var showScores = false
+    @State private var scoreTitle = ""
+    
+    @State private var scores = 0
+    
+    @State private var countries = ["Estonia", "France", "Germany", "Ireland", "Italy", "Nigeria",
+                             "Poland", "Russia", "Spain", "UK", "US"].shuffled()
+    @State var correctAnswer = Int.random(in: 0...2)
+    
+    var body: some View {
+        ZStack {
+            //AngularGradient(gradient: Gradient(colors: [.red, .yellow, .green,.blue, .purple,.red]), center: .center)
+            RadialGradient(stops: [
+                .init(color: Color(red: 0.1, green: 0.2, blue: 0.45), location: 0.3),
+                .init(color: Color(red: 0.76, green: 0.15, blue: 0.26), location: 0.3)
+            ], center: .top, startRadius: 200, endRadius: 700)
+                .ignoresSafeArea()
+            
+            VStack {
+                
+                Spacer()
+
+                Text("Guess the flag")
+                    .font(.largeTitle.weight(.bold))
+                    .foregroundColor(.white)
+                
+                VStack(spacing: 15) {
+                    VStack{
+                        Text("Tap the flag of")
+                            .foregroundStyle(.secondary)
+                            .font(.subheadline.weight(.heavy))
+                        Text(countries[correctAnswer])
+                            .font(.largeTitle.weight(.semibold))
+                    }
+                    
+                    ForEach(0..<3) {number in
+                        Button {
+                            flagTapper(number)
+                        }label: {
+                            Image(countries[number])
+                                .renderingMode(.original)
+                                .clipShape(Capsule())
+                        }
+                    }
+                }
+                .frame(maxWidth: .infinity)
+                .padding(.vertical, 20)
+                .background(.regularMaterial)
+                .clipShape(RoundedRectangle(cornerRadius: 20))
+                
+                Spacer()
+                
+                Text("Score \(scores)")
+                    .foregroundColor(.white)
+                    .font(.title.bold())
+                Spacer()
+            }
+            .padding()
+        }
+        .alert(scoreTitle, isPresented: $showScores) {
+            Button("Continue", action: askQuestion)
+        } message: {
+            Text("Your score is \(scores)")
+        }
+    }
+    func flagTapper(_ number: Int) {
+        if number == correctAnswer {
+            scoreTitle = "Correct"
+            scores += 10
+        } else {
+            scoreTitle = "Wrong"
+        }
+        showScores = true
+    }
+    
+    func askQuestion() {
+        
+        countries.shuffle()
+        correctAnswer = Int.random(in: 0...2)
+    }
+}
struct ContentView: View {
+    @State private var showScores = false
+    @State private var scoreTitle = ""
+    
+    @State private var scores = 0
+    
+    @State private var countries = ["Estonia", "France", "Germany", "Ireland", "Italy", "Nigeria",
+                             "Poland", "Russia", "Spain", "UK", "US"].shuffled()
+    @State var correctAnswer = Int.random(in: 0...2)
+    
+    var body: some View {
+        ZStack {
+            //AngularGradient(gradient: Gradient(colors: [.red, .yellow, .green,.blue, .purple,.red]), center: .center)
+            RadialGradient(stops: [
+                .init(color: Color(red: 0.1, green: 0.2, blue: 0.45), location: 0.3),
+                .init(color: Color(red: 0.76, green: 0.15, blue: 0.26), location: 0.3)
+            ], center: .top, startRadius: 200, endRadius: 700)
+                .ignoresSafeArea()
+            
+            VStack {
+                
+                Spacer()
+
+                Text("Guess the flag")
+                    .font(.largeTitle.weight(.bold))
+                    .foregroundColor(.white)
+                
+                VStack(spacing: 15) {
+                    VStack{
+                        Text("Tap the flag of")
+                            .foregroundStyle(.secondary)
+                            .font(.subheadline.weight(.heavy))
+                        Text(countries[correctAnswer])
+                            .font(.largeTitle.weight(.semibold))
+                    }
+                    
+                    ForEach(0..<3) {number in
+                        Button {
+                            flagTapper(number)
+                        }label: {
+                            Image(countries[number])
+                                .renderingMode(.original)
+                                .clipShape(Capsule())
+                        }
+                    }
+                }
+                .frame(maxWidth: .infinity)
+                .padding(.vertical, 20)
+                .background(.regularMaterial)
+                .clipShape(RoundedRectangle(cornerRadius: 20))
+                
+                Spacer()
+                
+                Text("Score \(scores)")
+                    .foregroundColor(.white)
+                    .font(.title.bold())
+                Spacer()
+            }
+            .padding()
+        }
+        .alert(scoreTitle, isPresented: $showScores) {
+            Button("Continue", action: askQuestion)
+        } message: {
+            Text("Your score is \(scores)")
+        }
+    }
+    func flagTapper(_ number: Int) {
+        if number == correctAnswer {
+            scoreTitle = "Correct"
+            scores += 10
+        } else {
+            scoreTitle = "Wrong"
+        }
+        showScores = true
+    }
+    
+    func askQuestion() {
+        
+        countries.shuffle()
+        correctAnswer = Int.random(in: 0...2)
+    }
+}

知识点

  • Gradient
  • alert

效果

image-20220711232134199

BetterRest

核心代码

swift
import CoreML
+import SwiftUI
+
+struct ContentView: View {
+    @State private var wakeUp = defaultWakeTime
+    @State private var sleepAmount = 8.0
+    @State private var coffeeAmount = 1
+    
+    @State private var alertTitle = ""
+    @State private var alertMessage = ""
+    @State private var showingAlert = false
+    
+    static var defaultWakeTime: Date {
+        var components = DateComponents()
+        components.hour = 7
+        components.minute = 0
+        return Calendar.current.date(from: components) ?? Date.now
+    }
+    
+    var body: some View {
+        NavigationView {
+            Form {
+                VStack(alignment: .leading, spacing: 0) {
+                    Text("When do you want to wake up?")
+                        .font(.headline)
+                    DatePicker("Please enter atime", selection: $wakeUp, displayedComponents: .hourAndMinute)
+                        .labelsHidden()
+                }
+                
+                
+                VStack(alignment: .leading, spacing: 0) {
+                    Text("Desired amount of sleep")
+                        .font(.headline)
+                    Stepper("\(sleepAmount.formatted()) hours", value: $sleepAmount, in: 4...12, step: 0.25)
+                }
+                
+                
+                VStack(alignment: .leading, spacing: 0) {
+                    Text("Daily coffee intake")
+                    Stepper(coffeeAmount == 1 ? "1 cup" : "\(coffeeAmount) cups", value: $coffeeAmount, in: 0...20)
+                }
+                
+            }
+            .navigationTitle("BetterRest")
+            .toolbar {
+                Button("Calculate", action: calculateBedtime)
+            }
+            .alert(alertTitle, isPresented: $showingAlert) {
+                Button("OK") {}
+            }message: {
+                Text(alertMessage)
+            }
+        }
+        
+    }
+    
+    func calculateBedtime() {
+        do {
+            let config = MLModelConfiguration()
+            let model = try SleepCalculator(configuration: config)
+            let components = Calendar.current.dateComponents([.hour, .minute], from: wakeUp)
+            let hour = (components.hour ?? 0) * 60 * 60
+            let minute = (components.minute ?? 0) * 60
+            
+            let prediction = try model.prediction(wake: Double(hour + minute), estimatedSleep: sleepAmount, coffee: Double(coffeeAmount))
+            
+            let sleepTime = wakeUp - prediction.actualSleep
+            alertTitle = "Your ideal bedtime is ..."
+            alertMessage = sleepTime.formatted(date: .omitted, time: .shortened)
+        }catch {
+            alertTitle = "Error"
+            alertMessage = "Sorry, there was a problem calculating your bedtime."
+        }
+        
+        showingAlert = true
+    }
+}
import CoreML
+import SwiftUI
+
+struct ContentView: View {
+    @State private var wakeUp = defaultWakeTime
+    @State private var sleepAmount = 8.0
+    @State private var coffeeAmount = 1
+    
+    @State private var alertTitle = ""
+    @State private var alertMessage = ""
+    @State private var showingAlert = false
+    
+    static var defaultWakeTime: Date {
+        var components = DateComponents()
+        components.hour = 7
+        components.minute = 0
+        return Calendar.current.date(from: components) ?? Date.now
+    }
+    
+    var body: some View {
+        NavigationView {
+            Form {
+                VStack(alignment: .leading, spacing: 0) {
+                    Text("When do you want to wake up?")
+                        .font(.headline)
+                    DatePicker("Please enter atime", selection: $wakeUp, displayedComponents: .hourAndMinute)
+                        .labelsHidden()
+                }
+                
+                
+                VStack(alignment: .leading, spacing: 0) {
+                    Text("Desired amount of sleep")
+                        .font(.headline)
+                    Stepper("\(sleepAmount.formatted()) hours", value: $sleepAmount, in: 4...12, step: 0.25)
+                }
+                
+                
+                VStack(alignment: .leading, spacing: 0) {
+                    Text("Daily coffee intake")
+                    Stepper(coffeeAmount == 1 ? "1 cup" : "\(coffeeAmount) cups", value: $coffeeAmount, in: 0...20)
+                }
+                
+            }
+            .navigationTitle("BetterRest")
+            .toolbar {
+                Button("Calculate", action: calculateBedtime)
+            }
+            .alert(alertTitle, isPresented: $showingAlert) {
+                Button("OK") {}
+            }message: {
+                Text(alertMessage)
+            }
+        }
+        
+    }
+    
+    func calculateBedtime() {
+        do {
+            let config = MLModelConfiguration()
+            let model = try SleepCalculator(configuration: config)
+            let components = Calendar.current.dateComponents([.hour, .minute], from: wakeUp)
+            let hour = (components.hour ?? 0) * 60 * 60
+            let minute = (components.minute ?? 0) * 60
+            
+            let prediction = try model.prediction(wake: Double(hour + minute), estimatedSleep: sleepAmount, coffee: Double(coffeeAmount))
+            
+            let sleepTime = wakeUp - prediction.actualSleep
+            alertTitle = "Your ideal bedtime is ..."
+            alertMessage = sleepTime.formatted(date: .omitted, time: .shortened)
+        }catch {
+            alertTitle = "Error"
+            alertMessage = "Sorry, there was a problem calculating your bedtime."
+        }
+        
+        showingAlert = true
+    }
+}

知识点

  • CoreML机器学习

  • DateComponents、DatePicker

  • alert修饰符

  • Stepper

WordScramble

核心代码

swift
struct ContentView: View {
+    @State private var usedWords = [String]()
+    @State private var rootWord = ""
+    @State private var newWord = ""
+    
+    @State private var errorTitle = ""
+    @State private var errorMessage = ""
+    @State private var showingError = false
+    
+    var body: some View {
+        NavigationView {
+            List {
+                Section {
+                    TextField("enter your word", text: $newWord)
+                        .autocapitalization(.none)
+                }
+                
+                Section {
+                    ForEach(usedWords, id: \.self) { word in
+                        HStack{
+                            Image(systemName: "\(word.count).circle")
+                            Text(word)
+                        }
+                    }
+                }
+            }
+            .navigationTitle(rootWord)
+            .onSubmit(addNewWord)
+            .onAppear(perform: startGame)
+            .alert(errorTitle, isPresented: $showingError) {
+                Button("OK", role: .cancel) {}
+            }message: {
+                Text(errorMessage)
+            }
+        }
+    }
+    
+    func addNewWord() {
+        let answer = newWord.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
+        guard answer.count > 0 else { return }
+        
+        guard isOriginal(word: answer) else {
+            wordError(title: "Word used already", message: "Be more original")
+            return
+        }
+        
+        guard isPossible(word: answer) else {
+            wordError(title: "word not possible", message: "you can't spell that word from '\(rootWord)'")
+            return
+        }
+        
+        guard isReal(word: answer) else {
+            wordError(title: "word not recognized", message: "you can't just make them up, you know!")
+            return
+        }
+        
+        
+        withAnimation{
+            usedWords.insert(answer, at: 0)
+        }
+      
+        newWord = ""
+    }
+    
+    func startGame() {
+        if let fileURL = Bundle.main.url(forResource: "start", withExtension: "txt") {
+            if let startWords = try? String(contentsOf: fileURL) {
+                let allWords = startWords.components(separatedBy: "\n")
+                rootWord = allWords.randomElement() ?? "silkworm"
+                return
+            }
+        }
+        fatalError("Could not load start.txt from bundle")
+    }
+    
+    func isOriginal(word: String) -> Bool {
+        !usedWords.contains(word)
+    }
+    
+    func isPossible(word: String) -> Bool {
+        var tempWord = rootWord
+        
+        for letter in word {
+            if let pos = tempWord.firstIndex(of: letter) {
+                tempWord.remove(at: pos)
+            } else {
+                return false
+            }
+        }
+        return true
+    }
+    
+    func isReal(word: String) -> Bool {
+        let checker = UITextChecker()
+        let range = NSRange(location: 0, length: word.utf16.count)
+        let misspelledRange = checker.rangeOfMisspelledWord(in: word, range: range, startingAt: 0, wrap: false, language: "en")
+        
+        return misspelledRange.location == NSNotFound
+    }
+    
+    func wordError(title: String, message: String) {
+        errorTitle = title
+        errorMessage = message
+        showingError = true
+    }
+}
struct ContentView: View {
+    @State private var usedWords = [String]()
+    @State private var rootWord = ""
+    @State private var newWord = ""
+    
+    @State private var errorTitle = ""
+    @State private var errorMessage = ""
+    @State private var showingError = false
+    
+    var body: some View {
+        NavigationView {
+            List {
+                Section {
+                    TextField("enter your word", text: $newWord)
+                        .autocapitalization(.none)
+                }
+                
+                Section {
+                    ForEach(usedWords, id: \.self) { word in
+                        HStack{
+                            Image(systemName: "\(word.count).circle")
+                            Text(word)
+                        }
+                    }
+                }
+            }
+            .navigationTitle(rootWord)
+            .onSubmit(addNewWord)
+            .onAppear(perform: startGame)
+            .alert(errorTitle, isPresented: $showingError) {
+                Button("OK", role: .cancel) {}
+            }message: {
+                Text(errorMessage)
+            }
+        }
+    }
+    
+    func addNewWord() {
+        let answer = newWord.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
+        guard answer.count > 0 else { return }
+        
+        guard isOriginal(word: answer) else {
+            wordError(title: "Word used already", message: "Be more original")
+            return
+        }
+        
+        guard isPossible(word: answer) else {
+            wordError(title: "word not possible", message: "you can't spell that word from '\(rootWord)'")
+            return
+        }
+        
+        guard isReal(word: answer) else {
+            wordError(title: "word not recognized", message: "you can't just make them up, you know!")
+            return
+        }
+        
+        
+        withAnimation{
+            usedWords.insert(answer, at: 0)
+        }
+      
+        newWord = ""
+    }
+    
+    func startGame() {
+        if let fileURL = Bundle.main.url(forResource: "start", withExtension: "txt") {
+            if let startWords = try? String(contentsOf: fileURL) {
+                let allWords = startWords.components(separatedBy: "\n")
+                rootWord = allWords.randomElement() ?? "silkworm"
+                return
+            }
+        }
+        fatalError("Could not load start.txt from bundle")
+    }
+    
+    func isOriginal(word: String) -> Bool {
+        !usedWords.contains(word)
+    }
+    
+    func isPossible(word: String) -> Bool {
+        var tempWord = rootWord
+        
+        for letter in word {
+            if let pos = tempWord.firstIndex(of: letter) {
+                tempWord.remove(at: pos)
+            } else {
+                return false
+            }
+        }
+        return true
+    }
+    
+    func isReal(word: String) -> Bool {
+        let checker = UITextChecker()
+        let range = NSRange(location: 0, length: word.utf16.count)
+        let misspelledRange = checker.rangeOfMisspelledWord(in: word, range: range, startingAt: 0, wrap: false, language: "en")
+        
+        return misspelledRange.location == NSNotFound
+    }
+    
+    func wordError(title: String, message: String) {
+        errorTitle = title
+        errorMessage = message
+        showingError = true
+    }
+}

知识点

  • TextField("enter your word", text: $newWord).autocapitalization(.none)

  • onSubmitonAppear修饰符

  • 读取静态资源

    swift
    if let fileURL = Bundle.main.url(forResource: "start", withExtension: "txt") {
    +            if let startWords = try? String(contentsOf: fileURL) {
    +                let allWords = startWords.components(separatedBy: "\n")
    +                rootWord = allWords.randomElement() ?? "silkworm"
    +                return
    +            }
    +        }
    +        fatalError("Could not load start.txt from bundle")
    if let fileURL = Bundle.main.url(forResource: "start", withExtension: "txt") {
    +            if let startWords = try? String(contentsOf: fileURL) {
    +                let allWords = startWords.components(separatedBy: "\n")
    +                rootWord = allWords.randomElement() ?? "silkworm"
    +                return
    +            }
    +        }
    +        fatalError("Could not load start.txt from bundle")
  • UITextChecker()

效果

image-20220711232805443

iExpense

核心代码

Expenses(ViewModel)

swift
class Expenses: ObservableObject {
+    @Published var items = [ExpenseItem]() {
+        didSet {
+            let encoder = JSONEncoder()
+            
+            if let encoded = try? encoder.encode(items) {
+                UserDefaults.standard.set(encoded, forKey: "Items")
+            }
+        }
+    }
+    
+    init() {
+        if let itemArr = UserDefaults.standard.data(forKey: "Items") {
+            let decoder = JSONDecoder()
+            if let decodedItems = try? decoder.decode([ExpenseItem].self, from: itemArr) {
+                items = decodedItems
+                return
+            }
+        }
+        items = []
+    }
+}
class Expenses: ObservableObject {
+    @Published var items = [ExpenseItem]() {
+        didSet {
+            let encoder = JSONEncoder()
+            
+            if let encoded = try? encoder.encode(items) {
+                UserDefaults.standard.set(encoded, forKey: "Items")
+            }
+        }
+    }
+    
+    init() {
+        if let itemArr = UserDefaults.standard.data(forKey: "Items") {
+            let decoder = JSONDecoder()
+            if let decodedItems = try? decoder.decode([ExpenseItem].self, from: itemArr) {
+                items = decodedItems
+                return
+            }
+        }
+        items = []
+    }
+}

ExpenseItem(Model)

swift
struct ExpenseItem: Identifiable, Codable {
+    let name: String
+    let type: String
+    let amount: Double
+    let id = UUID()
+}
struct ExpenseItem: Identifiable, Codable {
+    let name: String
+    let type: String
+    let amount: Double
+    let id = UUID()
+}

AddView

swift
struct AddView: View {
+    @ObservedObject var expenses: Expenses
+    
+    @State private var name = ""
+    @State private var type = "Personal"
+    @State private var amount = 0.0
+    @Environment(\.dismiss) var dismiss
+    
+    let types = ["Business", "Personal"]
+    
+    
+    var body: some View {
+        NavigationView {
+            Form {
+                TextField("Name", text: $name)
+                
+                Picker("Type", selection: $type) {
+                    ForEach(types, id: \.self) {
+                        Text($0)
+                    }
+                }
+                
+                TextField("Amount", value: $amount, format: .currency(code: Locale.current.currencyCode ?? "CNY"))
+                    .keyboardType(.decimalPad)
+            }
+            .navigationTitle("Add new expense")
+            .toolbar {
+                Button("Save") {
+                    let item = ExpenseItem(name: name, type: type, amount: amount)
+                    expenses.items.append(item)
+                    dismiss()
+                }
+            }
+        }
+    }
+}
struct AddView: View {
+    @ObservedObject var expenses: Expenses
+    
+    @State private var name = ""
+    @State private var type = "Personal"
+    @State private var amount = 0.0
+    @Environment(\.dismiss) var dismiss
+    
+    let types = ["Business", "Personal"]
+    
+    
+    var body: some View {
+        NavigationView {
+            Form {
+                TextField("Name", text: $name)
+                
+                Picker("Type", selection: $type) {
+                    ForEach(types, id: \.self) {
+                        Text($0)
+                    }
+                }
+                
+                TextField("Amount", value: $amount, format: .currency(code: Locale.current.currencyCode ?? "CNY"))
+                    .keyboardType(.decimalPad)
+            }
+            .navigationTitle("Add new expense")
+            .toolbar {
+                Button("Save") {
+                    let item = ExpenseItem(name: name, type: type, amount: amount)
+                    expenses.items.append(item)
+                    dismiss()
+                }
+            }
+        }
+    }
+}

ContentView

swift
struct ContentView: View {
+    @StateObject var expenses = Expenses()
+    @State private var showingAddExpense = false
+    
+    var body: some View {
+        NavigationView {
+            List {
+                ForEach(expenses.items) { item in
+                    HStack {
+                        VStack(alignment: .leading) {
+                            Text(item.name)
+                                .font(.headline)
+                            Text(item.type)
+                                .font(.subheadline)
+                        }
+                        Spacer()
+                        Text(item.amount, format: .currency(code: Locale.current.currencyCode ?? "CNY"))
+                    }
+                }
+                .onDelete(perform: removeItems)
+            }
+            .navigationTitle("iExpense")
+            .toolbar {
+                Button {
+                    showingAddExpense = true
+                } label: {
+                    Image(systemName: "plus")
+                }
+            }
+            .sheet(isPresented: $showingAddExpense) {
+                AddView(expenses: expenses)
+                
+            }
+        }
+    }
+    
+    func removeItems(at offsets: IndexSet) {
+        expenses.items.remove(atOffsets: offsets)
+    }
+}
struct ContentView: View {
+    @StateObject var expenses = Expenses()
+    @State private var showingAddExpense = false
+    
+    var body: some View {
+        NavigationView {
+            List {
+                ForEach(expenses.items) { item in
+                    HStack {
+                        VStack(alignment: .leading) {
+                            Text(item.name)
+                                .font(.headline)
+                            Text(item.type)
+                                .font(.subheadline)
+                        }
+                        Spacer()
+                        Text(item.amount, format: .currency(code: Locale.current.currencyCode ?? "CNY"))
+                    }
+                }
+                .onDelete(perform: removeItems)
+            }
+            .navigationTitle("iExpense")
+            .toolbar {
+                Button {
+                    showingAddExpense = true
+                } label: {
+                    Image(systemName: "plus")
+                }
+            }
+            .sheet(isPresented: $showingAddExpense) {
+                AddView(expenses: expenses)
+                
+            }
+        }
+    }
+    
+    func removeItems(at offsets: IndexSet) {
+        expenses.items.remove(atOffsets: offsets)
+    }
+}

知识点

  • MVVM

  • @StateObject、@Published、ObservableObject、@ObservedObject

  • UserDefaults(存储少量数据,常用于存储Preference)

  • JSONEncoder、JSONDecoder、Codable

  • 属性观察器(willSet、didSet) 配合 UserDefaults保存数据

  • onDelete修饰符

  • sheet修饰符(弹出新页面)

    swift
    .sheet(isPresented: $showingAddExpense) {
    +    AddView(expenses: expenses)
    +}
    .sheet(isPresented: $showingAddExpense) {
    +    AddView(expenses: expenses)
    +}
  • @Environment(.dismiss) var dismiss(@Environment(keyPath) 可以读取系统环境数据,dismiss用于关闭当前展示页面)

效果

image-20220711233432310

image-20220711233750123

+ + + + \ No newline at end of file diff --git a/frontend/others/HackingWithSwift-2.html b/frontend/others/HackingWithSwift-2.html new file mode 100644 index 000000000..b4ad5fb8a --- /dev/null +++ b/frontend/others/HackingWithSwift-2.html @@ -0,0 +1,1437 @@ + + + + + + HackingWithSwift-2 | 故事 + + + + + + + + + + + + + + +
Skip to content

HackingWithSwift-2

https://www.youtube.com/playlist?list=PLuoeXyslFTuZRi4q4VT6lZKxYbr7so1Mr

Moonshot

核心代码

Bundle-Decodable

swift
extension Bundle {
+    func decode<T: Decodable>(_ file: String) -> T {
+        guard let url = self.url(forResource: file, withExtension: nil) else {
+            fatalError("Failed to locate \(file) in bundle")
+        }
+        
+        guard let data = try? Data(contentsOf: url) else {
+            fatalError("Failed to locate \(file) in bundle")
+        }
+        
+        let decoder = JSONDecoder()
+        let formatter = DateFormatter()
+        formatter.dateFormat = "y-MM-dd"
+        decoder.dateDecodingStrategy = .formatted(formatter)
+        
+        guard let loaded = try? decoder.decode( T.self, from: data) else {
+            fatalError("Failed to locate \(file) in bundle")
+        }
+        
+        return loaded
+    }
+}
extension Bundle {
+    func decode<T: Decodable>(_ file: String) -> T {
+        guard let url = self.url(forResource: file, withExtension: nil) else {
+            fatalError("Failed to locate \(file) in bundle")
+        }
+        
+        guard let data = try? Data(contentsOf: url) else {
+            fatalError("Failed to locate \(file) in bundle")
+        }
+        
+        let decoder = JSONDecoder()
+        let formatter = DateFormatter()
+        formatter.dateFormat = "y-MM-dd"
+        decoder.dateDecodingStrategy = .formatted(formatter)
+        
+        guard let loaded = try? decoder.decode( T.self, from: data) else {
+            fatalError("Failed to locate \(file) in bundle")
+        }
+        
+        return loaded
+    }
+}

Color-Theme

swift
import SwiftUI
+
+extension ShapeStyle where Self == Color {
+    static var darkBackground: Color {
+        Color(red: 0.1, green: 0.1, blue: 0.2)
+    }
+    
+    static var lightBackgroud: Color {
+        Color(red: 0.2, green: 0.2, blue: 0.3)
+    }
+}
import SwiftUI
+
+extension ShapeStyle where Self == Color {
+    static var darkBackground: Color {
+        Color(red: 0.1, green: 0.1, blue: 0.2)
+    }
+    
+    static var lightBackgroud: Color {
+        Color(red: 0.2, green: 0.2, blue: 0.3)
+    }
+}

Model

swift
struct Astronaut: Codable, Identifiable {
+    let id: String
+    let name: String
+    let description: String
+}
+
+
+struct Mission: Codable, Identifiable {
+    struct CrewRole: Codable {
+        let name: String
+        let role: String
+    }
+    
+    let id: Int
+    let launchDate: Date?
+    let crew: [CrewRole]
+    let description: String
+    
+    var displayName: String {
+        "Apollo \(id)"
+    }
+    
+    var image: String {
+        "apollo\(id)"
+    }
+    
+    var formattedLaunchDate: String {
+        launchDate?.formatted(date: .abbreviated, time: .omitted) ?? "N/A"
+    }
+}
struct Astronaut: Codable, Identifiable {
+    let id: String
+    let name: String
+    let description: String
+}
+
+
+struct Mission: Codable, Identifiable {
+    struct CrewRole: Codable {
+        let name: String
+        let role: String
+    }
+    
+    let id: Int
+    let launchDate: Date?
+    let crew: [CrewRole]
+    let description: String
+    
+    var displayName: String {
+        "Apollo \(id)"
+    }
+    
+    var image: String {
+        "apollo\(id)"
+    }
+    
+    var formattedLaunchDate: String {
+        launchDate?.formatted(date: .abbreviated, time: .omitted) ?? "N/A"
+    }
+}

ContentView

swift
struct ContentView: View {
+    let astronauts: [String: Astronaut] = Bundle.main.decode("astronauts.json")
+    let missions: [Mission] = Bundle.main.decode("missions.json")
+    
+    let columns = [
+        GridItem(.adaptive(minimum: 150))
+    ]
+    
+    var body: some View {
+        NavigationView {
+            ScrollView {
+                LazyVGrid(columns: columns) {
+                    ForEach(missions) { mission in
+                        NavigationLink{
+                            MissionView(mission: mission, astronauts: astronauts)
+                        } label: {
+                            VStack{
+                                Image(mission.image)
+                                    .resizable()
+                                    .scaledToFit()
+                                    .frame(width: 100, height: 100)
+                                VStack {
+                                    Text(mission.displayName)
+                                        .font(.headline)
+                                        .foregroundColor(.white)
+                                    
+                                    Text(mission.formattedLaunchDate)
+                                        .font(.subheadline)
+                                        .foregroundColor(.white.opacity(0.5))
+                                }
+                                .padding(.vertical)
+                                .frame(maxWidth: .infinity)
+                                .background(.lightBackgroud)
+                            }
+                            .clipShape(RoundedRectangle(cornerRadius: 10))
+                            .overlay{
+                                RoundedRectangle(cornerRadius: 10)
+                                    .stroke()
+                            }
+                        }
+                    }
+                }
+                .padding([.horizontal, .bottom])
+            }
+            .navigationTitle("Moonshot")
+            .background(.darkBackground)
+            .preferredColorScheme(.dark)
+        }
+    }
+}
struct ContentView: View {
+    let astronauts: [String: Astronaut] = Bundle.main.decode("astronauts.json")
+    let missions: [Mission] = Bundle.main.decode("missions.json")
+    
+    let columns = [
+        GridItem(.adaptive(minimum: 150))
+    ]
+    
+    var body: some View {
+        NavigationView {
+            ScrollView {
+                LazyVGrid(columns: columns) {
+                    ForEach(missions) { mission in
+                        NavigationLink{
+                            MissionView(mission: mission, astronauts: astronauts)
+                        } label: {
+                            VStack{
+                                Image(mission.image)
+                                    .resizable()
+                                    .scaledToFit()
+                                    .frame(width: 100, height: 100)
+                                VStack {
+                                    Text(mission.displayName)
+                                        .font(.headline)
+                                        .foregroundColor(.white)
+                                    
+                                    Text(mission.formattedLaunchDate)
+                                        .font(.subheadline)
+                                        .foregroundColor(.white.opacity(0.5))
+                                }
+                                .padding(.vertical)
+                                .frame(maxWidth: .infinity)
+                                .background(.lightBackgroud)
+                            }
+                            .clipShape(RoundedRectangle(cornerRadius: 10))
+                            .overlay{
+                                RoundedRectangle(cornerRadius: 10)
+                                    .stroke()
+                            }
+                        }
+                    }
+                }
+                .padding([.horizontal, .bottom])
+            }
+            .navigationTitle("Moonshot")
+            .background(.darkBackground)
+            .preferredColorScheme(.dark)
+        }
+    }
+}

MissionView

swift
struct MissionView: View {
+    struct CrewMember {
+        let role: String
+        let astronaut: Astronaut
+    }
+    
+    
+    let mission: Mission
+    
+    let crew: [CrewMember]
+    
+    var body: some View {
+        GeometryReader { geometry in
+            ScrollView {
+                VStack {
+                    Image(mission.image)
+                        .resizable()
+                        .scaledToFit()
+                        .frame(maxWidth: geometry.size.width * 0.6)
+                        .padding(.top)
+                    
+                    VStack(alignment: .leading) {
+                        Rectangle()
+                            .frame(height: 2)
+                            .foregroundColor(.lightBackgroud)
+                            .padding(.vertical)
+                        
+                        Text("Mission Highlights")
+                            .font(.title.bold())
+                            .padding(.bottom, 5)
+                        
+                        Text(mission.description)
+                        Rectangle()
+                            .frame(height: 2)
+                            .foregroundColor(.lightBackgroud)
+                            .padding(.vertical)
+                        
+                        Text("Crew")
+                            .font(.title.bold())
+                            .padding(.bottom, 5)
+                    }
+                    .padding(.horizontal)
+                    
+                    
+                    
+
+                    
+                    ScrollView(.horizontal, showsIndicators: false) {
+                        HStack {
+                            ForEach(crew, id: \.role) { crewMember in
+                                NavigationLink {
+                                    AstronautView(astronaut: crewMember.astronaut)
+                                }label: {
+                                    HStack {
+                                        Image(crewMember.astronaut.id)
+                                            .resizable()
+                                            .frame(width: 104, height: 72)
+                                            .clipShape(Capsule())
+                                            .overlay{
+                                                Capsule()
+                                                    .strokeBorder(.white, lineWidth: 1)
+                                            }
+                                        VStack(alignment: .leading) {
+                                            Text(crewMember.astronaut.name)
+                                                .foregroundColor(.white)
+                                                .font(.headline)
+                                            
+                                            Text(crewMember.role)
+                                                .foregroundColor(.secondary)
+                                            
+                                        }
+                                    }
+                                }
+                            }
+                                
+                        }
+                    }
+                    .padding(.horizontal)
+                }
+                .padding(.bottom)
+            }
+            .navigationTitle(mission.displayName)
+            .navigationBarTitleDisplayMode(.inline)
+            .background(.darkBackground)
+        }
+        
+    }
+    
+    
+    init(mission: Mission, astronauts: [String: Astronaut]) {
+        self.mission = mission
+        
+        self.crew = mission.crew.map { member in
+            if let astronaut = astronauts[member.name] {
+                return CrewMember(role: member.role, astronaut: astronaut)
+            }else {
+                fatalError("Missing \(member.name)")
+            }
+        }
+    }
+}
struct MissionView: View {
+    struct CrewMember {
+        let role: String
+        let astronaut: Astronaut
+    }
+    
+    
+    let mission: Mission
+    
+    let crew: [CrewMember]
+    
+    var body: some View {
+        GeometryReader { geometry in
+            ScrollView {
+                VStack {
+                    Image(mission.image)
+                        .resizable()
+                        .scaledToFit()
+                        .frame(maxWidth: geometry.size.width * 0.6)
+                        .padding(.top)
+                    
+                    VStack(alignment: .leading) {
+                        Rectangle()
+                            .frame(height: 2)
+                            .foregroundColor(.lightBackgroud)
+                            .padding(.vertical)
+                        
+                        Text("Mission Highlights")
+                            .font(.title.bold())
+                            .padding(.bottom, 5)
+                        
+                        Text(mission.description)
+                        Rectangle()
+                            .frame(height: 2)
+                            .foregroundColor(.lightBackgroud)
+                            .padding(.vertical)
+                        
+                        Text("Crew")
+                            .font(.title.bold())
+                            .padding(.bottom, 5)
+                    }
+                    .padding(.horizontal)
+                    
+                    
+                    
+
+                    
+                    ScrollView(.horizontal, showsIndicators: false) {
+                        HStack {
+                            ForEach(crew, id: \.role) { crewMember in
+                                NavigationLink {
+                                    AstronautView(astronaut: crewMember.astronaut)
+                                }label: {
+                                    HStack {
+                                        Image(crewMember.astronaut.id)
+                                            .resizable()
+                                            .frame(width: 104, height: 72)
+                                            .clipShape(Capsule())
+                                            .overlay{
+                                                Capsule()
+                                                    .strokeBorder(.white, lineWidth: 1)
+                                            }
+                                        VStack(alignment: .leading) {
+                                            Text(crewMember.astronaut.name)
+                                                .foregroundColor(.white)
+                                                .font(.headline)
+                                            
+                                            Text(crewMember.role)
+                                                .foregroundColor(.secondary)
+                                            
+                                        }
+                                    }
+                                }
+                            }
+                                
+                        }
+                    }
+                    .padding(.horizontal)
+                }
+                .padding(.bottom)
+            }
+            .navigationTitle(mission.displayName)
+            .navigationBarTitleDisplayMode(.inline)
+            .background(.darkBackground)
+        }
+        
+    }
+    
+    
+    init(mission: Mission, astronauts: [String: Astronaut]) {
+        self.mission = mission
+        
+        self.crew = mission.crew.map { member in
+            if let astronaut = astronauts[member.name] {
+                return CrewMember(role: member.role, astronaut: astronaut)
+            }else {
+                fatalError("Missing \(member.name)")
+            }
+        }
+    }
+}

AstronautView

swift
struct AstronautView: View {
+    let astronaut: Astronaut
+    
+    var body: some View {
+        ScrollView {
+            VStack {
+                Image(astronaut.id)
+                    .resizable()
+                    .scaledToFit()
+                    
+                
+                Text(astronaut.description)
+                    .padding()
+                
+                
+            }
+            
+        }
+        .background(.darkBackground)
+        .navigationTitle(astronaut.name)
+        .navigationBarTitleDisplayMode(.inline)
+    }
+}
struct AstronautView: View {
+    let astronaut: Astronaut
+    
+    var body: some View {
+        ScrollView {
+            VStack {
+                Image(astronaut.id)
+                    .resizable()
+                    .scaledToFit()
+                    
+                
+                Text(astronaut.description)
+                    .padding()
+                
+                
+            }
+            
+        }
+        .background(.darkBackground)
+        .navigationTitle(astronaut.name)
+        .navigationBarTitleDisplayMode(.inline)
+    }
+}

知识点

  • extension扩展Bubble读取静态资源文件、扩展Color用于设置背景色
  • LazyVGrid设置自适应的网格视图
  • .preferredColorScheme(.dark)设置首选项颜色
  • GeometryReader,可以获取父View的坐标、尺寸等
  • Image调整大小- resizable(), scaleToFit(),frame()

效果

image-20220712152252199

image-20220712152306254

image-20220712152317084

CupcakeCorner

核心代码

Order

swift
class Order: ObservableObject, Codable {
+    
+    enum CodingKeys: CodingKey {
+        case type, quantity, extraFrosting, addSprinkles, name, streetAddress, city, zip
+    }
+    
+    static let types = ["Vanilla", "Strawberry", "Chocolate", "Rainbow"]
+    
+    @Published var type = 0
+    @Published var quantity = 3
+    @Published var specialRequestEnabled = false {
+        didSet {
+            if specialRequestEnabled == false {
+                extraFrosting = false
+                addSprinkles = false
+            }
+        }
+    }
+    @Published var extraFrosting = false
+    @Published var addSprinkles = false
+    
+    
+    @Published var name = ""
+    @Published var streetAddress = ""
+    @Published var city = ""
+    @Published var zip = ""
+    
+    
+    var hasValidAddress: Bool {
+        if name.isEmpty || streetAddress.isEmpty || city.isEmpty || zip.isEmpty {
+            return false
+        }
+        return true
+    }
+    
+    
+    var cost: Double {
+        // $2 per cake
+        var cost = Double(quantity) * 2
+        
+        // complicated cakes cost more
+        cost += Double(type) / 2
+        
+        // $1/cake for extra frosting
+        if extraFrosting {
+            cost += Double(quantity)
+        }
+        
+        // $0.5/cake for sprinkles
+        if addSprinkles {
+            cost += Double(quantity) / 2
+        }
+        
+        return cost
+    }
+    
+    init() {}
+    
+    
+    func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        
+        try container.encode(type, forKey: .type)
+        try container.encode(quantity, forKey: .quantity)
+        try container.encode(extraFrosting, forKey: .extraFrosting)
+        try container.encode(addSprinkles, forKey: .addSprinkles)
+        try container.encode(name, forKey: .name)
+        try container.encode(streetAddress, forKey: .streetAddress)
+        try container.encode(city, forKey: .city)
+        try container.encode(zip, forKey: .zip)
+    }
+    
+    required init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        
+        type = try container.decode(Int.self, forKey: .type)
+        quantity = try container.decode(Int.self, forKey: .quantity)
+        extraFrosting = try container.decode(Bool.self, forKey: .extraFrosting)
+        addSprinkles = try container.decode(Bool.self, forKey: .addSprinkles)
+        name = try container.decode(String.self, forKey: .name)
+        city = try container.decode(String.self, forKey: .city)
+        streetAddress = try container.decode(String.self, forKey: .streetAddress)
+        zip = try container.decode(String.self, forKey: .zip)
+    } 
+}
class Order: ObservableObject, Codable {
+    
+    enum CodingKeys: CodingKey {
+        case type, quantity, extraFrosting, addSprinkles, name, streetAddress, city, zip
+    }
+    
+    static let types = ["Vanilla", "Strawberry", "Chocolate", "Rainbow"]
+    
+    @Published var type = 0
+    @Published var quantity = 3
+    @Published var specialRequestEnabled = false {
+        didSet {
+            if specialRequestEnabled == false {
+                extraFrosting = false
+                addSprinkles = false
+            }
+        }
+    }
+    @Published var extraFrosting = false
+    @Published var addSprinkles = false
+    
+    
+    @Published var name = ""
+    @Published var streetAddress = ""
+    @Published var city = ""
+    @Published var zip = ""
+    
+    
+    var hasValidAddress: Bool {
+        if name.isEmpty || streetAddress.isEmpty || city.isEmpty || zip.isEmpty {
+            return false
+        }
+        return true
+    }
+    
+    
+    var cost: Double {
+        // $2 per cake
+        var cost = Double(quantity) * 2
+        
+        // complicated cakes cost more
+        cost += Double(type) / 2
+        
+        // $1/cake for extra frosting
+        if extraFrosting {
+            cost += Double(quantity)
+        }
+        
+        // $0.5/cake for sprinkles
+        if addSprinkles {
+            cost += Double(quantity) / 2
+        }
+        
+        return cost
+    }
+    
+    init() {}
+    
+    
+    func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        
+        try container.encode(type, forKey: .type)
+        try container.encode(quantity, forKey: .quantity)
+        try container.encode(extraFrosting, forKey: .extraFrosting)
+        try container.encode(addSprinkles, forKey: .addSprinkles)
+        try container.encode(name, forKey: .name)
+        try container.encode(streetAddress, forKey: .streetAddress)
+        try container.encode(city, forKey: .city)
+        try container.encode(zip, forKey: .zip)
+    }
+    
+    required init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        
+        type = try container.decode(Int.self, forKey: .type)
+        quantity = try container.decode(Int.self, forKey: .quantity)
+        extraFrosting = try container.decode(Bool.self, forKey: .extraFrosting)
+        addSprinkles = try container.decode(Bool.self, forKey: .addSprinkles)
+        name = try container.decode(String.self, forKey: .name)
+        city = try container.decode(String.self, forKey: .city)
+        streetAddress = try container.decode(String.self, forKey: .streetAddress)
+        zip = try container.decode(String.self, forKey: .zip)
+    } 
+}

ContentView

swift
struct ContentView: View {
+    @StateObject var order = Order()
+    
+    var body: some View {
+        NavigationView{
+            Form {
+                Section {
+                    Picker("Select your cake type", selection: $order.type) {
+                        ForEach(Order.types.indices, id: \.self) {
+                            Text(Order.types[$0])
+                        }
+                    }
+                    Stepper("Number of cakes: \(order.quantity)", value: $order.quantity, in:  3...20)
+                }
+                
+                
+                Section {
+                    Toggle("Any special requests?", isOn: $order.specialRequestEnabled.animation())
+                    
+                    if order.specialRequestEnabled {
+                        Toggle("Add extra frosting", isOn: $order.extraFrosting)
+                        Toggle("Add extra sprinkles", isOn: $order.addSprinkles)
+                    }
+                }
+                
+                Section {
+                    NavigationLink {
+                        AddressView(order: order)
+                    } label: {
+                        Text("Deliver details")
+                    }
+                }
+            }
+            .navigationTitle("Cupcake Corner")
+        }
+    }
+}
struct ContentView: View {
+    @StateObject var order = Order()
+    
+    var body: some View {
+        NavigationView{
+            Form {
+                Section {
+                    Picker("Select your cake type", selection: $order.type) {
+                        ForEach(Order.types.indices, id: \.self) {
+                            Text(Order.types[$0])
+                        }
+                    }
+                    Stepper("Number of cakes: \(order.quantity)", value: $order.quantity, in:  3...20)
+                }
+                
+                
+                Section {
+                    Toggle("Any special requests?", isOn: $order.specialRequestEnabled.animation())
+                    
+                    if order.specialRequestEnabled {
+                        Toggle("Add extra frosting", isOn: $order.extraFrosting)
+                        Toggle("Add extra sprinkles", isOn: $order.addSprinkles)
+                    }
+                }
+                
+                Section {
+                    NavigationLink {
+                        AddressView(order: order)
+                    } label: {
+                        Text("Deliver details")
+                    }
+                }
+            }
+            .navigationTitle("Cupcake Corner")
+        }
+    }
+}

AddressView

swift
struct AddressView: View {
+    @ObservedObject var order: Order
+    var body: some View {
+        Form {
+            Section {
+                TextField("name", text: $order.name)
+                TextField("Street address", text: $order.streetAddress)
+                TextField("City", text: $order.city)
+                TextField("Zip", text: $order.zip)
+            }
+            
+            Section {
+                NavigationLink {
+                    CheckoutView(order: order)
+                } label: {
+                    Text("Check out")
+                }
+            }
+            .disabled(!order.hasValidAddress)
+        }
+        .navigationTitle("Delivery details")
+        .navigationBarTitleDisplayMode(.inline)
+    }
+}
struct AddressView: View {
+    @ObservedObject var order: Order
+    var body: some View {
+        Form {
+            Section {
+                TextField("name", text: $order.name)
+                TextField("Street address", text: $order.streetAddress)
+                TextField("City", text: $order.city)
+                TextField("Zip", text: $order.zip)
+            }
+            
+            Section {
+                NavigationLink {
+                    CheckoutView(order: order)
+                } label: {
+                    Text("Check out")
+                }
+            }
+            .disabled(!order.hasValidAddress)
+        }
+        .navigationTitle("Delivery details")
+        .navigationBarTitleDisplayMode(.inline)
+    }
+}

CheckoutView

swift
struct CheckoutView: View {
+    @ObservedObject var order: Order
+    @State private var confirmationMessage = ""
+    @State private var showingConfirmation = false
+    
+    
+    var body: some View {
+        ScrollView {
+            
+            VStack {
+                AsyncImage(url: URL(string: "https://hws.dev/img/cupcakes@3x.jpg"), scale: 3) { image in
+                    image
+                        .resizable()
+                        .scaledToFit()
+                } placeholder: {
+                    ProgressView()
+                }
+                
+               
+                Text("Your total is \(order.cost, format: .currency(code: "USD"))")
+                    .font(.title)
+                
+                Button("Place Order") {
+                    Task {
+                        await placeOrder()
+                    }
+                }
+                .padding()
+           
+            }
+        }
+        .navigationTitle("Check out")
+        .navigationBarTitleDisplayMode(.inline)
+        .alert("Thank you!", isPresented: $showingConfirmation) {
+            Button("OK") {}
+        } message: {
+            Text(confirmationMessage)
+        }
+    }
+    
+    func placeOrder() async {
+        guard let encoded = try? JSONEncoder().encode(order) else {
+            print("Failed to encode order")
+                return
+        }
+        
+        let url = URL(string: "https://reqres.in/api/cupcakes")!
+        var request = URLRequest(url: url)
+        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+        request.httpMethod = "POST"
+        
+        do {
+            let (data, _) = try await URLSession.shared.upload(for: request, from: encoded)
+            let decodedOrder = try JSONDecoder().decode(Order.self, from: data)
+            confirmationMessage = "Your order for \(decodedOrder.quantity)x\(Order.types[decodedOrder.type].lowercased()) cupcakes is on its way !"
+            showingConfirmation = true
+        } catch {
+            print("Check out failed ...")
+        }
+        
+    }
+}
struct CheckoutView: View {
+    @ObservedObject var order: Order
+    @State private var confirmationMessage = ""
+    @State private var showingConfirmation = false
+    
+    
+    var body: some View {
+        ScrollView {
+            
+            VStack {
+                AsyncImage(url: URL(string: "https://hws.dev/img/cupcakes@3x.jpg"), scale: 3) { image in
+                    image
+                        .resizable()
+                        .scaledToFit()
+                } placeholder: {
+                    ProgressView()
+                }
+                
+               
+                Text("Your total is \(order.cost, format: .currency(code: "USD"))")
+                    .font(.title)
+                
+                Button("Place Order") {
+                    Task {
+                        await placeOrder()
+                    }
+                }
+                .padding()
+           
+            }
+        }
+        .navigationTitle("Check out")
+        .navigationBarTitleDisplayMode(.inline)
+        .alert("Thank you!", isPresented: $showingConfirmation) {
+            Button("OK") {}
+        } message: {
+            Text(confirmationMessage)
+        }
+    }
+    
+    func placeOrder() async {
+        guard let encoded = try? JSONEncoder().encode(order) else {
+            print("Failed to encode order")
+                return
+        }
+        
+        let url = URL(string: "https://reqres.in/api/cupcakes")!
+        var request = URLRequest(url: url)
+        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+        request.httpMethod = "POST"
+        
+        do {
+            let (data, _) = try await URLSession.shared.upload(for: request, from: encoded)
+            let decodedOrder = try JSONDecoder().decode(Order.self, from: data)
+            confirmationMessage = "Your order for \(decodedOrder.quantity)x\(Order.types[decodedOrder.type].lowercased()) cupcakes is on its way !"
+            showingConfirmation = true
+        } catch {
+            print("Check out failed ...")
+        }
+        
+    }
+}

知识点

  • Codable协议无法处理被@Published等属性包装器修饰的属性,需要额外编码来手动实现Codable协议

  • Form表单校验使用.disabled()修饰符

  • 异步加载图像AsyncImage,这个View不能像普通Image一样直接使用.resizable()调整大小,需要特殊处理

    swift
    AsyncImage(url: URL(string: "https://hws.dev/img/cupcakes@3x.jpg"), scale: 3) { image in
    +                    image
    +                        .resizable()
    +                        .scaledToFit()
    +                } placeholder: {
    +                    ProgressView() //加载中loading view
    +                }
    AsyncImage(url: URL(string: "https://hws.dev/img/cupcakes@3x.jpg"), scale: 3) { image in
    +                    image
    +                        .resizable()
    +                        .scaledToFit()
    +                } placeholder: {
    +                    ProgressView() //加载中loading view
    +                }
  • Button的action使用异步方法时需要使用Task

    swift
    Button("Place Order") {
    +    Task {
    +        await placeOrder()
    +    }
    +}
    Button("Place Order") {
    +    Task {
    +        await placeOrder()
    +    }
    +}
  • 使用URLRequest、URLSession发送http请求

  • 异步方法,async,await关键字

效果

image-20220713143307866

image-20220713143325947

image-20220713143335966

Bookworm

核心代码

BookwormApp

swift
@main
+struct BookwormApp: App {
+    
+    @StateObject private var dataController = DataController()
+    
+    var body: some Scene {
+        WindowGroup {
+            ContentView()
+                .environment(\.managedObjectContext, dataController.container.viewContext )
+        }
+    }
+}
@main
+struct BookwormApp: App {
+    
+    @StateObject private var dataController = DataController()
+    
+    var body: some Scene {
+        WindowGroup {
+            ContentView()
+                .environment(\.managedObjectContext, dataController.container.viewContext )
+        }
+    }
+}

ContentView

swift
struct ContentView: View {
+    @Environment(\.managedObjectContext) var moc
+    @FetchRequest(sortDescriptors: [SortDescriptor(\.title, order: .forward), SortDescriptor(\.author, order: .forward)]) var books: FetchedResults<Book>
+    
+    @State private var showingAddScreen = false
+    
+    var body: some View {
+        NavigationView {
+            List {
+                ForEach(books) { book in
+                    NavigationLink {
+                        DetailView(book: book)
+                    } label: {
+                        HStack {
+                            EmojiRatingView(rating: book.rating)
+                                .font(.largeTitle)
+                            VStack(alignment: .leading) {
+                                Text(book.title ?? "Unknown title")
+                                    .font(.headline)
+                                Text(book.author ?? "Unknown author")
+                                    .foregroundColor(.secondary)
+                            }
+                        }
+                    }
+                }
+                .onDelete(perform: deleteBooks)
+            }
+            .navigationTitle("Bookworm")
+            .toolbar {
+                ToolbarItem(placement: .navigationBarLeading) {
+                    EditButton()
+                }
+                
+                ToolbarItem(placement: .navigationBarTrailing) {
+                    Button {
+                        showingAddScreen.toggle()
+                    } label: {
+                        Label("Add book", systemImage: "plus")
+                    }
+                }
+            }
+            .sheet(isPresented: $showingAddScreen) {
+                AddBookView()
+            }
+        }
+    }
+    
+    func deleteBooks(at offsets: IndexSet) {
+        for offset in offsets {
+            let book = books[offset]
+            moc.delete(book)
+        }
+        try? moc.save()
+    }
+}
struct ContentView: View {
+    @Environment(\.managedObjectContext) var moc
+    @FetchRequest(sortDescriptors: [SortDescriptor(\.title, order: .forward), SortDescriptor(\.author, order: .forward)]) var books: FetchedResults<Book>
+    
+    @State private var showingAddScreen = false
+    
+    var body: some View {
+        NavigationView {
+            List {
+                ForEach(books) { book in
+                    NavigationLink {
+                        DetailView(book: book)
+                    } label: {
+                        HStack {
+                            EmojiRatingView(rating: book.rating)
+                                .font(.largeTitle)
+                            VStack(alignment: .leading) {
+                                Text(book.title ?? "Unknown title")
+                                    .font(.headline)
+                                Text(book.author ?? "Unknown author")
+                                    .foregroundColor(.secondary)
+                            }
+                        }
+                    }
+                }
+                .onDelete(perform: deleteBooks)
+            }
+            .navigationTitle("Bookworm")
+            .toolbar {
+                ToolbarItem(placement: .navigationBarLeading) {
+                    EditButton()
+                }
+                
+                ToolbarItem(placement: .navigationBarTrailing) {
+                    Button {
+                        showingAddScreen.toggle()
+                    } label: {
+                        Label("Add book", systemImage: "plus")
+                    }
+                }
+            }
+            .sheet(isPresented: $showingAddScreen) {
+                AddBookView()
+            }
+        }
+    }
+    
+    func deleteBooks(at offsets: IndexSet) {
+        for offset in offsets {
+            let book = books[offset]
+            moc.delete(book)
+        }
+        try? moc.save()
+    }
+}

DataController

swift
import CoreData
+
+class DataController: ObservableObject {
+    let container = NSPersistentContainer(name: "Bookworm")
+    
+    
+    init() {
+        container.loadPersistentStores { description, error in
+            if let error = error {
+                print("Core data failed to load: \(error.localizedDescription)")
+            }
+            
+        }
+    }
+}
import CoreData
+
+class DataController: ObservableObject {
+    let container = NSPersistentContainer(name: "Bookworm")
+    
+    
+    init() {
+        container.loadPersistentStores { description, error in
+            if let error = error {
+                print("Core data failed to load: \(error.localizedDescription)")
+            }
+            
+        }
+    }
+}

AddBookView

swift
struct AddBookView: View {
+    
+    @Environment(\.managedObjectContext) var moc
+    @Environment(\.dismiss) var dismiss
+    @State private var title = ""
+    @State private var author = ""
+    @State private var rating = 3
+    @State private var genre = ""
+    @State private var review = ""
+    
+    let genres = ["Fantasy", "Horror", "Kids", "Mystery", "Poetry", "Romance", "Thriller"]
+    
+    var body: some View {
+        NavigationView {
+            Form {
+                Section {
+                    TextField("Name of book", text: $title)
+                    TextField("Author's name", text: $author)
+                    
+                    Picker("Genre", selection: $genre) {
+                        ForEach(genres, id: \.self) {
+                            Text($0)
+                        }
+                    }
+                }
+                
+                Section {
+                    TextEditor(text: $review)
+                    
+                    RatingView(rating: $rating)
+                } header: {
+                    Text("Write a review")
+                }
+                
+                Section {
+                    Button("Save") {
+                        let book = Book(context: moc)
+                        book.id = UUID()
+                        book.title = self.title
+                        book.author = self.author
+                        book.genre = self.genre
+                        book.review = self.review
+                        book.rating = Int16(self.rating)
+                        try? moc.save()
+                        dismiss()
+                    }
+                }
+            }
+            .navigationTitle("Add book")
+        }
+    }
+}
struct AddBookView: View {
+    
+    @Environment(\.managedObjectContext) var moc
+    @Environment(\.dismiss) var dismiss
+    @State private var title = ""
+    @State private var author = ""
+    @State private var rating = 3
+    @State private var genre = ""
+    @State private var review = ""
+    
+    let genres = ["Fantasy", "Horror", "Kids", "Mystery", "Poetry", "Romance", "Thriller"]
+    
+    var body: some View {
+        NavigationView {
+            Form {
+                Section {
+                    TextField("Name of book", text: $title)
+                    TextField("Author's name", text: $author)
+                    
+                    Picker("Genre", selection: $genre) {
+                        ForEach(genres, id: \.self) {
+                            Text($0)
+                        }
+                    }
+                }
+                
+                Section {
+                    TextEditor(text: $review)
+                    
+                    RatingView(rating: $rating)
+                } header: {
+                    Text("Write a review")
+                }
+                
+                Section {
+                    Button("Save") {
+                        let book = Book(context: moc)
+                        book.id = UUID()
+                        book.title = self.title
+                        book.author = self.author
+                        book.genre = self.genre
+                        book.review = self.review
+                        book.rating = Int16(self.rating)
+                        try? moc.save()
+                        dismiss()
+                    }
+                }
+            }
+            .navigationTitle("Add book")
+        }
+    }
+}

RatingView

swift
struct RatingView: View {
+    @Binding var rating: Int
+    
+    var label = ""
+    var maxmiumRating = 5
+    var offImage: Image?
+    var onImage =  Image(systemName: "star.fill")
+    var offColor = Color.gray
+    var onColor = Color.yellow
+    
+    
+    var body: some View {
+        HStack {
+            if label.isEmpty == false {
+                Text(label)
+            }
+            
+            ForEach(1..<maxmiumRating + 1, id: \.self) { number in
+                image(for: number)
+                    .foregroundColor(number > rating ? offColor : onColor)
+                    .onTapGesture {
+                        rating = number
+                    }
+            }
+        }
+    }
+    
+    func image(for number: Int) -> Image {
+        if number > rating {
+            return offImage ?? onImage
+        } else {
+            return onImage
+        }
+    }
+}
struct RatingView: View {
+    @Binding var rating: Int
+    
+    var label = ""
+    var maxmiumRating = 5
+    var offImage: Image?
+    var onImage =  Image(systemName: "star.fill")
+    var offColor = Color.gray
+    var onColor = Color.yellow
+    
+    
+    var body: some View {
+        HStack {
+            if label.isEmpty == false {
+                Text(label)
+            }
+            
+            ForEach(1..<maxmiumRating + 1, id: \.self) { number in
+                image(for: number)
+                    .foregroundColor(number > rating ? offColor : onColor)
+                    .onTapGesture {
+                        rating = number
+                    }
+            }
+        }
+    }
+    
+    func image(for number: Int) -> Image {
+        if number > rating {
+            return offImage ?? onImage
+        } else {
+            return onImage
+        }
+    }
+}

EmojiRatingView

swift
struct EmojiRatingView: View {
+    let rating: Int16
+    
+    var body: some View {
+        switch rating {
+        case 1:
+            return Text("☹️")
+        case 2:
+            return Text("😞")
+        case 3:
+            return Text("😊")
+        case 4:
+            return Text("😍")
+        default:
+            return Text("🤩")
+        }
+    }
+}
struct EmojiRatingView: View {
+    let rating: Int16
+    
+    var body: some View {
+        switch rating {
+        case 1:
+            return Text("☹️")
+        case 2:
+            return Text("😞")
+        case 3:
+            return Text("😊")
+        case 4:
+            return Text("😍")
+        default:
+            return Text("🤩")
+        }
+    }
+}

DetailView

swift
struct DetailView: View {
+    let book: Book
+    @Environment(\.managedObjectContext) var moc
+    @Environment(\.dismiss) var dismiss
+    @State private var showingAlert = false
+    
+    var body: some View {
+        ScrollView {
+            ZStack(alignment: .bottomTrailing) {
+                Image(book.genre ?? "Fantasy")
+                    .resizable()
+                    .scaledToFit()
+                
+                Text(book.genre?.uppercased() ?? "FANTASY")
+                    .font(.caption)
+                    .fontWeight(.black)
+                    .padding(8)
+                    .foregroundColor(.white)
+                    .background(.black.opacity(0.75))
+                    .clipShape(Capsule())
+                    .offset(x: -5, y: -5)
+            }
+            
+            Text(book.author ?? "Unknown author")
+                .font(.title)
+                .foregroundColor(.secondary)
+            
+            Text(book.review ?? "No review")
+                .padding()
+            
+            RatingView(rating: .constant(Int(book.rating)))
+        }
+        .navigationTitle(book.title ?? "Unknown book")
+        .navigationBarTitleDisplayMode(.inline)
+        .alert("Delete this book?", isPresented: $showingAlert) {
+            Button("OK", role: .destructive,action: deleteBook)
+            Button("Cancle", role: .cancel) { }
+        } message: {
+            Text("Are you sure?")
+        }
+        .toolbar {
+            Button {
+                showingAlert = true
+            } label: {
+                Label("Delete this book", systemImage: "trash")
+            }
+        }
+        
+    }
+    
+    
+    func deleteBook() {
+        moc.delete(book)
+        
+        try? moc.save()
+        
+        dismiss()
+        
+    }
+}
struct DetailView: View {
+    let book: Book
+    @Environment(\.managedObjectContext) var moc
+    @Environment(\.dismiss) var dismiss
+    @State private var showingAlert = false
+    
+    var body: some View {
+        ScrollView {
+            ZStack(alignment: .bottomTrailing) {
+                Image(book.genre ?? "Fantasy")
+                    .resizable()
+                    .scaledToFit()
+                
+                Text(book.genre?.uppercased() ?? "FANTASY")
+                    .font(.caption)
+                    .fontWeight(.black)
+                    .padding(8)
+                    .foregroundColor(.white)
+                    .background(.black.opacity(0.75))
+                    .clipShape(Capsule())
+                    .offset(x: -5, y: -5)
+            }
+            
+            Text(book.author ?? "Unknown author")
+                .font(.title)
+                .foregroundColor(.secondary)
+            
+            Text(book.review ?? "No review")
+                .padding()
+            
+            RatingView(rating: .constant(Int(book.rating)))
+        }
+        .navigationTitle(book.title ?? "Unknown book")
+        .navigationBarTitleDisplayMode(.inline)
+        .alert("Delete this book?", isPresented: $showingAlert) {
+            Button("OK", role: .destructive,action: deleteBook)
+            Button("Cancle", role: .cancel) { }
+        } message: {
+            Text("Are you sure?")
+        }
+        .toolbar {
+            Button {
+                showingAlert = true
+            } label: {
+                Label("Delete this book", systemImage: "trash")
+            }
+        }
+        
+    }
+    
+    
+    func deleteBook() {
+        moc.delete(book)
+        
+        try? moc.save()
+        
+        dismiss()
+        
+    }
+}

Bookworm.xcdatamodeld

image-20220714192129472

知识点

  • CoreData相关概念,.xcdatamodeld文件、NSPersistentContainer、@Environment(.managedObjectContext)、context.save()、context.delete(xxx)、FetchRequest
  • TextEditor
  • 通用RatingView

效果

image-20220714192654745

image-20220714192710927

image-20220714192720403

CoreData

  • Conditional saving of NSManagedObjectContex
swift
@Environment(\.managedObjectContext) var moc
+
+var body: some View {
+  Button("Save") {
+    if moc.hasChanges {
+      try? moc.save()
+    }
+  }
+}
@Environment(\.managedObjectContext) var moc
+
+var body: some View {
+  Button("Save") {
+    if moc.hasChanges {
+      try? moc.save()
+    }
+  }
+}
  • Ensuring Core Data objects are unique using constraints

    • 配置CoreDataEntity的constranits
    • DataController增加
    swift
    self.container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
    self.container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
  • Dynamically filtering @FetchRequest with SwiftUI

swift
struct FilteredList<T: NSManagedOBject, Context: View>: View {
+  @FetchRequest var fetchRequest: FetchedResults<T>
+  
+  var body: some View {
+    List(fetchRequest, id:\.self) { item in
+			self.content(item)
+    }
+  }
+  
+  init(filterKey: String, filterValue: String, @ViewBuilder content: @escaping (T) -> Content) {
+    _fetchRequest = FetchRequest<T>(sortDescriptors: [], predicate: NSPredicate(format: "%K BEGINSWITH %@", filterKey, filterValue))
+    self.content = content
+  }
+}
struct FilteredList<T: NSManagedOBject, Context: View>: View {
+  @FetchRequest var fetchRequest: FetchedResults<T>
+  
+  var body: some View {
+    List(fetchRequest, id:\.self) { item in
+			self.content(item)
+    }
+  }
+  
+  init(filterKey: String, filterValue: String, @ViewBuilder content: @escaping (T) -> Content) {
+    _fetchRequest = FetchRequest<T>(sortDescriptors: [], predicate: NSPredicate(format: "%K BEGINSWITH %@", filterKey, filterValue))
+    self.content = content
+  }
+}
+ + + + \ No newline at end of file diff --git "a/frontend/others/SwiftUI\345\205\245\351\227\250.html" "b/frontend/others/SwiftUI\345\205\245\351\227\250.html" new file mode 100644 index 000000000..8c62d761c --- /dev/null +++ "b/frontend/others/SwiftUI\345\205\245\351\227\250.html" @@ -0,0 +1,343 @@ + + + + + + SwiftUI入门 | 故事 + + + + + + + + + + + + + + +
Skip to content

SwiftUI入门

MVVM

MVVM是一种架构设计范式,把数据和视图分离开,Model和View必须通过ViewModel通信。

Model

数据模型,负责数据和逻辑的处理,独立于UI界面,数据流(data flows)在映射到视图中的过程是只读

View

渲染UI界面,展示Model数据,声明式(为UI声明的方法,在任何时候做它们应做的事情)、无状态的(不需要关心任何状态变化)、响应式的(跟随Model数据变化重新渲染)。

ViewModel

执行解释工作(interpreter),绑定View和Model。ViewModel关注Model中的变化(notices changes),然后把Model的数据变更发布出去(publishes changed),订阅了(subsrcbes)某个发布(publication)的View会进行rebuild。

ViewModel没有指向View的指针,不直接与View对话,如果View订阅了某个发布,就会询问ViewModel怎么适应变化,这个过程不会涉及Model,因为ViewModel的作用就是解释Model的变化。

MVVM的Processes Intent

MVVM有一个对应的关联架构,是Model-View-Intent。如果用户意图(intent)做一些操作,那么这些Intent就要进行View到Model这个反向传递过程。而swiftUI还没有进行这个设计,所以我们用下面一系列操作来处理Intent:

  • View Calls Intent function 视图调用方法
  • ViewModel modifies the Model 视图模型修改模型
  • Model changes 模型改动变化
  • ViewModel notices changes and publishes 模型关注到变化并发布
  • View whitch subscribes Reflect the Model 订阅变化的视图进行模型映射

对比MVVM的映射过程,多了ViewModel处理View操作,并且修改Model这两个操作。

https://www.jianshu.com/p/c14c70c0c9f7

Layout

HStack and VStack

stacks划分提供给自身的空间,然后把空间分配给内部的视图。优先给least flexible的子视图分配空间。

  • Example of inflexible view : Image,Image视图需要一个固定尺寸
  • Another example(slightly more flexible): Text,需要一个完全适合内部文本的尺寸
  • Example of a very flexible View: RoundedRectangle,总是使用所有可用的空间

在给一个视图它需要的空间后,这块空间从可用空间中被移除,然后stack继续给下一个least flexible的视图分配空间。very flexible views最后会平分空间。

在子视图选择了它们的尺寸后,stack会调整自己的size来适应它们,如果有very flexible的子视图,那么这个stack也会变得very flexible

.layoutPriority(Double)

可以使用.layoutPriority(Double)改变获取空间的优先级,默认值为0。.layoutPriority(Double)的优先级要比least flexible更高。

alignment

why .leading instead of .left?Stacks会根据语言环境判断对齐方式,例如有些语言(阿拉伯语)的文本是从右向左的。

LazyHStack and LazyVStack

不会build不可见的视图内容,通常用在ScrollView中

ScrollView

占据所有可用空间,子视图大小根据滚动轴调整

List、Form、OutlineGroup

really smart VStacks

.backgroup 修饰符

Text("hello").backgroup(Rectangle().foregroundColor(.red)),效果类似ZStack(Text在上),但是区别是这个例子中最终的View大小是由Text决定的

.overlay 修饰符

swift
Image(systemName: "folder")
+   .font(.system(size: 55, weight: .thin))
+   .overlay(Text("❤️"), alignment: .bottom)
Image(systemName: "folder")
+   .font(.system(size: 55, weight: .thin))
+   .overlay(Text("❤️"), alignment: .bottom)

视图的大小由Image决定,Text会堆叠在Image上,底部对齐

Modifiers

所有修饰符都会返回一个View

Example

swift
HStack{
+  ForEach(viewModel.cards) { card in
+ 		 CardView(card: card).aspectRatio(2/3, contentMode: .fit)                       
+  }
+}
+.foregroundColor(.orange)
+.padding(10)
HStack{
+  ForEach(viewModel.cards) { card in
+ 		 CardView(card: card).aspectRatio(2/3, contentMode: .fit)                       
+  }
+}
+.foregroundColor(.orange)
+.padding(10)
  • 首先被提供空间的是.padding(10)
  • 然后内边距10的空间会提供给.foregroudColor
  • 最后所有空间被提供给HStack
  • 然后空间被平均分给.aspectRatio
  • 每个.aspectRatio会设置宽度,然后遵循2/3的长宽比设置高度,或者在HStack高度不足时,占据所有高度,然后按2/3设置宽度。
  • .aspectRatio把所有空间提供给CardView

Spacer(minLength: CGFloat)

总是占据提供给他的所有空间,不绘制任何东西.

Divider()

分割线,在HStack中绘制垂直的线,VStack中是水平线。

@ViewBuilder

@ViewBuilder是一个参数属性,作用于构造视图的闭包参数上,允许闭包提供多个子视图。

swift
@ViewBuilder
+func front(of card: Card) -> some View {
+  let shape = RoundedRectangle(cornerRadius: 20)
+  shape
+  shape.stroke()
+  Text(card.content)
+}
@ViewBuilder
+func front(of card: Card) -> some View {
+  let shape = RoundedRectangle(cornerRadius: 20)
+  shape
+  shape.stroke()
+  Text(card.content)
+}

Property Wrapper

swift
@propertyWrapper
+struct Converter1{
+    let from:String
+    let to:String
+    let rate:Double
+    
+    var value:Double
+    var wrappedValue:String{
+        get{
+            "\(from)\(value)"
+        }
+        set{
+            value = Double(newValue) ?? -1
+        }
+    }
+    
+    var projectedValue:String{
+        return "\(to)\(value * rate)"
+    }
+    
+    init(initialValue:String,
+         from:String,
+         to:String,
+         rate:Double
+    ) {
+        self.rate = rate
+        self.value = 0
+        self.from = from
+        self.to = to
+        self.wrappedValue = initialValue
+    }
+    
+    
+}
+
+struct TestWraper {
+    @State var myname = ""
+    @Converter1(initialValue: "100", from: "USD", to: "CNY", rate: 6.88)
+    var usd_cny
+    
+    @Converter1(initialValue: "100", from: "CNY", to: "EUR", rate: 0.13)
+    var cny_eur
+    
+    func test1(){
+        print("\(usd_cny)=\($usd_cny)")
+        print("\(cny_eur)=\($cny_eur)")
+    }
+    /*
+     USD100.0=CNY688.0
+     CNY100.0=EUR13.0
+     */
+}
@propertyWrapper
+struct Converter1{
+    let from:String
+    let to:String
+    let rate:Double
+    
+    var value:Double
+    var wrappedValue:String{
+        get{
+            "\(from)\(value)"
+        }
+        set{
+            value = Double(newValue) ?? -1
+        }
+    }
+    
+    var projectedValue:String{
+        return "\(to)\(value * rate)"
+    }
+    
+    init(initialValue:String,
+         from:String,
+         to:String,
+         rate:Double
+    ) {
+        self.rate = rate
+        self.value = 0
+        self.from = from
+        self.to = to
+        self.wrappedValue = initialValue
+    }
+    
+    
+}
+
+struct TestWraper {
+    @State var myname = ""
+    @Converter1(initialValue: "100", from: "USD", to: "CNY", rate: 6.88)
+    var usd_cny
+    
+    @Converter1(initialValue: "100", from: "CNY", to: "EUR", rate: 0.13)
+    var cny_eur
+    
+    func test1(){
+        print("\(usd_cny)=\($usd_cny)")
+        print("\(cny_eur)=\($cny_eur)")
+    }
+    /*
+     USD100.0=CNY688.0
+     CNY100.0=EUR13.0
+     */
+}
  • 属性包装器必须有一个包装值,名为wrappedValue的计算属性
  • 预计值为projectedValue,访问预计值的方式为.$属性名projectedValue是只读的。

Property Wrapper使用限制

  • protocol中无法使用
  • 通过wrapper包装的实例属性不能在extension中声明
  • 不能在enum中声明
  • class中通过wrapper包装的属性无法被另外一个属性通过override覆盖
  • 通过wrapper包装的实例属性不能用lazy@NSCopying@NSManagedweakunowned修饰

@State

  • 视图是只读的

    所有视图的struct都是完全、彻底只读的,所以View中只有letcomputed(常量和计算属性)才有意义。(被@ObservedObject装饰的属性除外,这种属性必须被标记为var

  • 为什么

    View一直在被创建、丢弃,只有body才会存在很久,所以View不太需要一些需要被修改的属性

don't worry,之所以这样是因为View应该是stateless的,只负责渲染model,不需要自身具有什么状态属性。但是极少数情况下View也是需要状态的(it turns out there are a few rare times when a View needs some state),但这种状态存储总是暂时的(always temporary),所有持久化的状态都存在Model中。

例如:进入编辑模式,需要提前收集数据来为用户修改数据的intent作准备,需要暂时展示其他的View(编辑页面)来收集数据,编辑完后需要一个动画效果来关闭这个编辑页面,所以需要一个"编辑模式状态"的属性来标记何时该关闭。

上述场景中可以使用@State来标记这个临时状态存储变量

swift
@State private var somethingTemporary: SomeType //someType can be any struct
@State private var somethingTemporary: SomeType //someType can be any struct

这个临时状态变量是private修饰的,是因为只有当前View能访问这个变量。@State变量的变化会导致这个View的body重新渲染。这和@ObservedObject类似,但是@State作用的是一个随机的数据(值语义),而@ObservedObject作用在ViewModel上(对象语义)。

@ObservedObject

多个视图数据共享和更新时,需要一个数据模型的概念,即多视图的状态可以根据Data-Model进行更新,这种场景下@State就不再适用了。

  • ObservableObject协议定义了一个数据模型的数据发生变化时发布通知的能力

  • @ObservedObject这个属性包装器包装的属性可以监听到数据的变化,也可以利用它去更新数据。

  • @Published这个属性包装器包装的属性,都会被转化为一个publisher(Combine框架的概念),当值发生变化时,会通知系统,然后系统再去更新画面

@StateObject

和@ObservedObject类似,也是修饰对象语义,和@ObservedObject的区别在于,实例是否被创建其的View所持有,其生命周期是否完全可控,@StateObject修饰的属性的生命周期由创建该对象的对象维护(这一点又类似@State)

swift
class DataSource: ObservableObject {
+  @Published var counter = 0
+}
+
+struct Counter: View {
+  @ObservedObject var dataSource = DataSource()
+
+  var body: some View {
+    VStack {
+      Button("Increment counter") {
+        dataSource.counter += 1
+      }
+
+      Text("Count is \(dataSource.counter)")
+    }
+  }
+}
+
+struct ItemList: View {
+  @State private var items = ["hello", "world"]
+
+  var body: some View {
+    VStack {
+      Button("Append item to list") {
+        items.append("test")
+      }
+
+      List(items, id: \.self) { name in
+        Text(name)
+      }
+
+      Counter()
+    }
+  }
+}
class DataSource: ObservableObject {
+  @Published var counter = 0
+}
+
+struct Counter: View {
+  @ObservedObject var dataSource = DataSource()
+
+  var body: some View {
+    VStack {
+      Button("Increment counter") {
+        dataSource.counter += 1
+      }
+
+      Text("Count is \(dataSource.counter)")
+    }
+  }
+}
+
+struct ItemList: View {
+  @State private var items = ["hello", "world"]
+
+  var body: some View {
+    VStack {
+      Button("Append item to list") {
+        items.append("test")
+      }
+
+      List(items, id: \.self) { name in
+        Text(name)
+      }
+
+      Counter()
+    }
+  }
+}

在这个例子中,每次点击Append item to listButton,counter都会被重置,这是因为每次重新渲染,DataSource()都会被重新创建。解决这个问题有两个方法:

  1. 在ItemList中创建DataSource,并把DataSource传递给Counter
  2. 把@ObservedObject替换为@StateObject

将DataSource标记为@StateObject意味着DataSource被实例化后会保存在Counter的外部,当Counter重新渲染时,会直接用这个值。

@EnvironmentObject

使用@ObservedObject可以在视图间共享数据、刷新画面,但是必须为需要的视图进行引用的传递。如果视图的层级较多,且各个View和子View使用同一个数据模型,那么@ObservedObject的传递将会变得笨重且易出错。

SwiftUI提供了另一种选择,@EnvironmentObject就是把数据模型引用保存到了一个共同的环境变量中,environment是一个共通的存储区域,保存了app的信息和Views,当然也可以保存自定义数据,包括对observable object的引用。

@Environment

@State类似,App也可以响应iOS系统过来的state变化,例如语言环境、字体大小、暗黑模式切换等,为了及时响应这些变化,app可以使用@Environment(KeyPath)来进行获取实时的信息。

Combine框架

@Published属性包装器和ObservableObject的实现定义在Combine框架中。Combine框架中定义了一些协议和数据类型,可以让我们处理数据,当一个代码数据发生变化,可以应用这个框架来通知另外一处代码有新数据可以使用。

这样就会出现两个类型的任务,一个是发布者(publisher),一个是订阅者(subscriber)。发布者决定了数据和错误信息的产生并发给订阅者,订阅者会接受这些信息。

在SwiftUI中,被@Published修饰的属性,会被自动转化为Publisher,ObservableObject协议的实现中,定义了被@Published修饰的属性作为发布者,在属性的值发生变化的时候,发布者将通知订阅者。@ObservedObject@EnvironmentObject修饰的属性,扮演订阅者的角色。

Just发布者和Subscribers.Sink

swift
import Combine
+import Foundation
+
+let myPublisher = Just("55")
+
+let mySubscriber = Subscribers.Sink<String,Never> (receiveCompletion: { completion in
+    if completion == .finished {
+        print("111")
+    }else {
+        print("222")
+    }
+    
+}, receiveValue: { value in
+    print(value)
+})
+
+myPublisher.subscribe(mySubscriber)
import Combine
+import Foundation
+
+let myPublisher = Just("55")
+
+let mySubscriber = Subscribers.Sink<String,Never> (receiveCompletion: { completion in
+    if completion == .finished {
+        print("111")
+    }else {
+        print("222")
+    }
+    
+}, receiveValue: { value in
+    print(value)
+})
+
+myPublisher.subscribe(mySubscriber)

数据的转换

中间发布者

Publishers.Map

Publishers.Filter

...

或Just().操作符

Subjects

Combine还有一种发布者叫Subjects,实现了Subject协议,可以调用send方法发送数据

  • PassthroughSubject()
  • CurrentValueSubject(value)
swift
import Combine
+import Foundation
+
+enum MyErrors: Error {
+    case wrongValue
+}
+
+let myPublisher = PassthroughSubject<String, MyErrors>()
+//let myPublisher = CurrentValueSubject<String, MyErrors>("100")
+
+let mySubscriber = myPublisher.filter({
+    return $0.count < 5
+}).sink(receiveCompletion: {completion in
+    if completion == .failure(MyErrors.wrongValue) {
+        print("MyErrors.wrongValue")
+    }else {
+        print(completion)
+    }
+    
+}, receiveValue: { value in
+    print("value: \(value)")
+})
+
+
+myPublisher.send("h")
import Combine
+import Foundation
+
+enum MyErrors: Error {
+    case wrongValue
+}
+
+let myPublisher = PassthroughSubject<String, MyErrors>()
+//let myPublisher = CurrentValueSubject<String, MyErrors>("100")
+
+let mySubscriber = myPublisher.filter({
+    return $0.count < 5
+}).sink(receiveCompletion: {completion in
+    if completion == .failure(MyErrors.wrongValue) {
+        print("MyErrors.wrongValue")
+    }else {
+        print(completion)
+    }
+    
+}, receiveValue: { value in
+    print("value: \(value)")
+})
+
+
+myPublisher.send("h")

.onReceive

SwiftUI中,View协议有一个修饰符.onReceive(Publisher, perform: Closure)把任何View转换成一个订阅者,来接受来自发布者的数据,SwiftUI使UI组件和Combine结合带来了扩展可能。

swift
import SwiftUI
+
+class ContentViewData: ObservableObject {
+  @Published var counter: Int = 0
+  let timePublisher = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
+}
+
+struct ContentView: View {
+  @ObservedObject var contentData = ContentViewData()
+  
+  var body: some View {
+    Text("hello, world! \(self.contentData.counter)")
+    .onReceive(contentData.timePublisher, perform: { value in
+      self.contentData.counter += 1
+      if self.contentData.counter > 20 {
+        self.contentData.timePublisher.upstream.connect().cancel()
+        print("stop")
+      }
+    })
+  }
+}
import SwiftUI
+
+class ContentViewData: ObservableObject {
+  @Published var counter: Int = 0
+  let timePublisher = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
+}
+
+struct ContentView: View {
+  @ObservedObject var contentData = ContentViewData()
+  
+  var body: some View {
+    Text("hello, world! \(self.contentData.counter)")
+    .onReceive(contentData.timePublisher, perform: { value in
+      self.contentData.counter += 1
+      if self.contentData.counter > 20 {
+        self.contentData.timePublisher.upstream.connect().cancel()
+        print("stop")
+      }
+    })
+  }
+}
+ + + + \ No newline at end of file diff --git "a/frontend/others/swift\350\257\255\346\263\225.html" "b/frontend/others/swift\350\257\255\346\263\225.html" new file mode 100644 index 000000000..34f37576b --- /dev/null +++ "b/frontend/others/swift\350\257\255\346\263\225.html" @@ -0,0 +1,329 @@ + + + + + + swift语法 | 故事 + + + + + + + + + + + + + + +
Skip to content

swift语法

英文原版:https://docs.swift.org/swift-book/

中文版:https://swiftgg.gitbook.io/swift/

可选类型

*可选类型(optionals)*来处理值可能缺失的情况。可选类型实际也是一个枚举:

swift
enum Optional<T> {
+  case none
+  case some(T) 
+}
enum Optional<T> {
+  case none
+  case some(T) 
+}

nil

如果声明一个可选常量或者变量但是没有赋值,它们会自动被设置为nil

swift
var hello: String?							var hello: Optional<String> = .none
+var hello: String? = "hello"    var hello: Optional<String> = .some("hello")
+var hello: String? = nil        var hello: Optional<String> = .none
var hello: String?							var hello: Optional<String> = .none
+var hello: String? = "hello"    var hello: Optional<String> = .some("hello")
+var hello: String? = nil        var hello: Optional<String> = .none
swift
var surveyAnswer: String? 
+// surveyAnswer 会被自动设置为 nil -> var surveyAnswer: String? = nil
var surveyAnswer: String? 
+// surveyAnswer 会被自动设置为 nil -> var surveyAnswer: String? = nil

强制解析

swift
let hello: String? = ...
+print(hello!)
let hello: String? = ...
+print(hello!)

等价于

swift
switch hello {
+  case .none: //raise an exception(crash)
+  case .some(let data): print(data)
+}
switch hello {
+  case .none: //raise an exception(crash)
+  case .some(let data): print(data)
+}

可选绑定

强制解析可能会导致异常,可以使用if let来安全的获取可选类型中的值

swift
if let safehello = hello {
+  print(safehello)
+} else {
+  //do something else
+}
if let safehello = hello {
+  print(safehello)
+} else {
+  //do something else
+}

等价于

swift
switch hello {
+  case .none: //do something else
+  case .some(let data): print(data)
+}
switch hello {
+  case .none: //do something else
+  case .some(let data): print(data)
+}

使用可选绑定时后面不能用&&,可以用,隔开语句

空合运算符(Nil Coalescing Operator)

空合运算符a ?? b)将对可选类型 a 进行空判断,如果 a 包含一个值就进行解包,否则就返回一个默认值 b。表达式 a 必须是 Optional 类型。默认值 b 的类型必须要和 a 存储值的类型保持一致。

空合运算符是对以下代码的简短表达方法:

swift
a != nil ? a! : b
a != nil ? a! : b

例子:

swift
let x: String? = ...
+let y = x ?? z
let x: String? = ...
+let y = x ?? z

实际等价于上:

swift
switch a {
+  case .none: y = z
+  case .some(let data): y = data
+}
switch a {
+  case .none: y = z
+  case .some(let data): y = data
+}

可选链

可选链式调用是一种可以在当前值可能为 nil 的可选值上请求和调用属性、方法及下标的方法。如果可选值有值,那么调用就会成功;如果可选值是 nil,那么调用将返回 nil。多个调用可以连接在一起形成一个调用链,如果其中任何一个节点为 nil,整个调用链都会失败,即返回 nil

swift
let x: String = ...
+let y = x?.foo()?.bar?.z
let x: String = ...
+let y = x?.foo()?.bar?.z

等价于

swift
switch x {
+  case .none: y = nil
+  case .some(let xval):
+  	switch xval.foo() {
+      case .none: y = nil
+      case .some(let xfooval):
+      	switch xfooval.bar {
+          case .none: y = nil
+          case .some(let xfbval): y = xfbval.z
+        }
+    }
+}
switch x {
+  case .none: y = nil
+  case .some(let xval):
+  	switch xval.foo() {
+      case .none: y = nil
+      case .some(let xfooval):
+      	switch xfooval.bar {
+          case .none: y = nil
+          case .some(let xfbval): y = xfbval.z
+        }
+    }
+}

闭包

闭包表达式

swift
{ (parameters) -> return_type in
+    statements
+}
{ (parameters) -> return_type in
+    statements
+}

尾随闭包(Trailing Closures)

尾随闭包是一个书写在函数圆括号之后的闭包表达式,函数支持将其作为最后一个参数调用。在使用尾随闭包时,不用写出它的参数标签

例如:

swift
ForEach(modelData.categories.keys.sorted(), id: \.self) { key in
+    CategoryRow(categoryName: key, items: modelData.categories[key]!)
+}
ForEach(modelData.categories.keys.sorted(), id: \.self) { key in
+    CategoryRow(categoryName: key, items: modelData.categories[key]!)
+}

多重尾随闭包(multiple trailing closure)

swift
struct SignInView: View {
+    var body: some View {
+        Button {
+            showingProfile.toggle()
+        } label: {
+            Label("User Profile", systemImage: "person.crop.circle")
+        }
+    }
+}
struct SignInView: View {
+    var body: some View {
+        Button {
+            showingProfile.toggle()
+        } label: {
+            Label("User Profile", systemImage: "person.crop.circle")
+        }
+    }
+}

方法

在实例方法中修改值类型

结构体和枚举是值类型。默认情况下,值类型的属性不能在它的实例方法中被修改。

如果确实需要在某个特定的方法中修改结构体或者枚举的属性,可以为这个方法选择 可变(mutating)

swift
struct Point {
+    var x = 0.0, y = 0.0
+    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
+        x += deltaX
+        y += deltaY
+    }
+}
+var somePoint = Point(x: 1.0, y: 1.0)
+somePoint.moveBy(x: 2.0, y: 3.0)
+print("The point is now at (\(somePoint.x), \(somePoint.y))")
+// 打印“The point is now at (3.0, 4.0)”
struct Point {
+    var x = 0.0, y = 0.0
+    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
+        x += deltaX
+        y += deltaY
+    }
+}
+var somePoint = Point(x: 1.0, y: 1.0)
+somePoint.moveBy(x: 2.0, y: 3.0)
+print("The point is now at (\(somePoint.x), \(somePoint.y))")
+// 打印“The point is now at (3.0, 4.0)”

属性

计算属性

计算属性不直接存储值,而是提供一个 getter 和一个可选的 setter,来间接获取和设置其他属性或变量的值。

swift
struct Point {
+    var x = 0.0, y = 0.0
+}
+struct Size {
+    var width = 0.0, height = 0.0
+}
+struct Rect {
+    var origin = Point()
+    var size = Size()
+    var center: Point {
+        get {
+            let centerX = origin.x + (size.width / 2)
+            let centerY = origin.y + (size.height / 2)
+            return Point(x: centerX, y: centerY)
+        }
+        set(newCenter) {
+            origin.x = newCenter.x - (size.width / 2)
+            origin.y = newCenter.y - (size.height / 2)
+        }
+    }
+}
+var square = Rect(origin: Point(x: 0.0, y: 0.0),
+    size: Size(width: 10.0, height: 10.0))
+let initialSquareCenter = square.center
+square.center = Point(x: 15.0, y: 15.0)
+print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
+// 打印“square.origin is now at (10.0, 10.0)”
struct Point {
+    var x = 0.0, y = 0.0
+}
+struct Size {
+    var width = 0.0, height = 0.0
+}
+struct Rect {
+    var origin = Point()
+    var size = Size()
+    var center: Point {
+        get {
+            let centerX = origin.x + (size.width / 2)
+            let centerY = origin.y + (size.height / 2)
+            return Point(x: centerX, y: centerY)
+        }
+        set(newCenter) {
+            origin.x = newCenter.x - (size.width / 2)
+            origin.y = newCenter.y - (size.height / 2)
+        }
+    }
+}
+var square = Rect(origin: Point(x: 0.0, y: 0.0),
+    size: Size(width: 10.0, height: 10.0))
+let initialSquareCenter = square.center
+square.center = Point(x: 15.0, y: 15.0)
+print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
+// 打印“square.origin is now at (10.0, 10.0)”

简化 Setter 声明

如果计算属性的 setter 没有定义表示新值的参数名,则可以使用默认名称 newValue。下面是使用了简化 setter 声明的 Rect 结构体代码:

swift
struct AlternativeRect {
+    var origin = Point()
+    var size = Size()
+    var center: Point {
+        get {
+            let centerX = origin.x + (size.width / 2)
+            let centerY = origin.y + (size.height / 2)
+            return Point(x: centerX, y: centerY)
+        }
+        set {
+            origin.x = newValue.x - (size.width / 2)
+            origin.y = newValue.y - (size.height / 2)
+        }
+    }
+}
struct AlternativeRect {
+    var origin = Point()
+    var size = Size()
+    var center: Point {
+        get {
+            let centerX = origin.x + (size.width / 2)
+            let centerY = origin.y + (size.height / 2)
+            return Point(x: centerX, y: centerY)
+        }
+        set {
+            origin.x = newValue.x - (size.width / 2)
+            origin.y = newValue.y - (size.height / 2)
+        }
+    }
+}

简化 Getter 声明

如果整个 getter 是单一表达式,getter 会隐式地返回这个表达式结果。下面是另一个版本的 Rect 结构体,用到了简化的 getter 和 setter 声明:

swift
struct CompactRect {
+    var origin = Point()
+    var size = Size()
+    var center: Point {
+        get {
+            Point(x: origin.x + (size.width / 2),
+                  y: origin.y + (size.height / 2))
+        }
+        set {
+            origin.x = newValue.x - (size.width / 2)
+            origin.y = newValue.y - (size.height / 2)
+        }
+    }
+}
struct CompactRect {
+    var origin = Point()
+    var size = Size()
+    var center: Point {
+        get {
+            Point(x: origin.x + (size.width / 2),
+                  y: origin.y + (size.height / 2))
+        }
+        set {
+            origin.x = newValue.x - (size.width / 2)
+            origin.y = newValue.y - (size.height / 2)
+        }
+    }
+}

在 getter 中忽略 return 与在函数中忽略 return 的规则相同.

只读计算属性

只有 getter 没有 setter 的计算属性叫只读计算属性。只读计算属性总是返回一个值,可以通过点运算符访问,但不能设置新的值。

注意

必须使用 var 关键字定义计算属性,包括只读计算属性,因为它们的值不是固定的。let 关键字只用来声明常量属性,表示初始化后再也无法修改的值。

只读计算属性的声明可以去掉 get 关键字和花括号:

swift
struct Cuboid {
+    var width = 0.0, height = 0.0, depth = 0.0
+    var volume: Double {
+    	return width * height * depth
+    }
+}
+let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)
+print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")
+// 打印“the volume of fourByFiveByTwo is 40.0”
struct Cuboid {
+    var width = 0.0, height = 0.0, depth = 0.0
+    var volume: Double {
+    	return width * height * depth
+    }
+}
+let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)
+print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")
+// 打印“the volume of fourByFiveByTwo is 40.0”

属性观察器

属性观察器监控和响应属性值的变化,每次属性被设置值的时候都会调用属性观察器,即使新值和当前值相同的时候也不例外。

可以在以下位置添加属性观察器:

  • 自定义的存储属性
  • 继承的存储属性
  • 继承的计算属性

可以为属性添加其中一个或两个观察器:

  • willSet 在新的值被设置之前调用
  • didSet 在新的值被设置之后调用

willSet 观察器会将新的属性值作为常量参数传入,在 willSet 的实现代码中可以为这个参数指定一个名称,如果不指定则参数仍然可用,这时使用默认名称 newValue 表示。

同样,didSet 观察器会将旧的属性值作为参数传入,可以为该参数指定一个名称或者使用默认参数名 oldValue。如果在 didSet 方法中再次对该属性赋值,那么新值会覆盖旧的值。

在父类初始化方法调用之后,在子类构造器中给父类的属性赋值时,会调用父类属性的 willSetdidSet 观察器。而在父类初始化方法调用之前,给子类的属性赋值时不会调用子类属性的观察器。

swift
class StepCounter {
+    var totalSteps: Int = 0 {
+        willSet(newTotalSteps) {
+            print("将 totalSteps 的值设置为 \(newTotalSteps)")
+        }
+        didSet {
+            if totalSteps > oldValue  {
+                print("增加了 \(totalSteps - oldValue) 步")
+            }
+        }
+    }
+}
+let stepCounter = StepCounter()
+stepCounter.totalSteps = 200
+// 将 totalSteps 的值设置为 200
+// 增加了 200 步
+stepCounter.totalSteps = 360
+// 将 totalSteps 的值设置为 360
+// 增加了 160 步
+stepCounter.totalSteps = 896
+// 将 totalSteps 的值设置为 896
+// 增加了 536 步
class StepCounter {
+    var totalSteps: Int = 0 {
+        willSet(newTotalSteps) {
+            print("将 totalSteps 的值设置为 \(newTotalSteps)")
+        }
+        didSet {
+            if totalSteps > oldValue  {
+                print("增加了 \(totalSteps - oldValue) 步")
+            }
+        }
+    }
+}
+let stepCounter = StepCounter()
+stepCounter.totalSteps = 200
+// 将 totalSteps 的值设置为 200
+// 增加了 200 步
+stepCounter.totalSteps = 360
+// 将 totalSteps 的值设置为 360
+// 增加了 160 步
+stepCounter.totalSteps = 896
+// 将 totalSteps 的值设置为 896
+// 增加了 536 步

protocol

sort of a "stripped-down" struct/class

有函数和变量,但是没有具体实现,类似interface, 当实现一个协议时,必须实现协议中所有的函数和变量

使用protocol来限制entension:

swift
extension Array where Element: Hashable {...}
extension Array where Element: Hashable {...}

使用protocol来限制函数:

swift
init(data: Data) where Data: Collection, Data.Element: Identifiable
init(data: Data) where Data: Collection, Data.Element: Identifiable

protocol extension

可以通过extension给protocol的func或var添加默认的实现

swift
struct Tesla: Vehicle {
+  //...
+}
+
+extension Vehicle {
+  fun registerWithDMV() { // actual implementation }
+}
struct Tesla: Vehicle {
+  //...
+}
+
+extension Vehicle {
+  fun registerWithDMV() { // actual implementation }
+}
swift
protocol View {
+  var body: some View
+}
protocol View {
+  var body: some View
+}
swift
extension View {
+  func foregroundColor(_ color: Color) -> some View { /* implementation */ }
+  func font(_ font: Font?) -> some View { /* implementation */ }
+  ...
+}
extension View {
+  func foregroundColor(_ color: Color) -> some View { /* implementation */ }
+  func font(_ font: Font?) -> some View { /* implementation */ }
+  ...
+}

generics + protocols

swift
protocol Identifiable {
+  associatedtype ID
+  var id: ID { get }
+}
protocol Identifiable {
+  associatedtype ID
+  var id: ID { get }
+}
+ + + + \ No newline at end of file diff --git a/golang/base/GoTemplate.html b/golang/base/GoTemplate.html new file mode 100644 index 000000000..6aa7a3288 --- /dev/null +++ b/golang/base/GoTemplate.html @@ -0,0 +1,415 @@ + + + + + + Go Template | 故事 + + + + + + + + + + + + + + +
Skip to content

Go Template

Go Template是一种用于生成文本输出的模板引擎,它是Go语言标准库中内置的一部分。Go Template使用简单而强大的语法来描述要生成的最终文本的结构和内容。

Go Template的语法是基于文本插值的思想,通过在模板文件中插入占位符和控制指令来控制输出的结果。模板可以包含静态文本和动态值,并且可以使用控制指令来迭代、条件判断和执行其他逻辑操作。

示例

html
<!--test.html-->
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <title>Go Web</title>
+</head>
+<body>
+{{ . }}
+</body>
+</html>
<!--test.html-->
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <title>Go Web</title>
+</head>
+<body>
+{{ . }}
+</body>
+</html>
go
package main
+
+import (
+	"html/template"
+	"net/http"
+)
+
+func tmpl(w http.ResponseWriter, r *http.Request) {
+	t1, err := template.ParseFiles("test.html")
+	if err != nil {
+		panic(err)
+	}
+	t1.Execute(w, "hello world")
+}
+
+func main() {
+	server := http.Server{
+		Addr: "127.0.0.1:8080",
+	}
+	http.HandleFunc("/", tmpl)
+	server.ListenAndServe()
+}
package main
+
+import (
+	"html/template"
+	"net/http"
+)
+
+func tmpl(w http.ResponseWriter, r *http.Request) {
+	t1, err := template.ParseFiles("test.html")
+	if err != nil {
+		panic(err)
+	}
+	t1.Execute(w, "hello world")
+}
+
+func main() {
+	server := http.Server{
+		Addr: "127.0.0.1:8080",
+	}
+	http.HandleFunc("/", tmpl)
+	server.ListenAndServe()
+}

.和作用域

在写template的时候,会经常用到"."。

在template中,点"."代表当前作用域的当前对象。它类似于java/c++的this关键字,类似于perl/python的self。

go
type Person struct {
+	Name string
+	Age  int
+}
+
+func main(){
+	p := Person{"leo",23}
+	tmpl, _ := template.New("test").Parse("Name: {{.Name}}, Age: {{.Age}}")
+	_ = tmpl.Execute(os.Stdout, p)
+}
+// Name: leo, Age: 23
type Person struct {
+	Name string
+	Age  int
+}
+
+func main(){
+	p := Person{"leo",23}
+	tmpl, _ := template.New("test").Parse("Name: {{.Name}}, Age: {{.Age}}")
+	_ = tmpl.Execute(os.Stdout, p)
+}
+// Name: leo, Age: 23

但是并非只有一个顶级作用域,range、with、if等内置action都有自己的本地作用域。

go
package main
+
+import (
+	"os"
+	"text/template"
+)
+
+type Friend struct {
+	Fname string
+}
+type Person struct {
+	UserName string
+	Emails   []string
+	Friends  []*Friend
+}
+
+func main() {
+	f1 := Friend{Fname: "xiaofang"}
+	f2 := Friend{Fname: "wugui"}
+	t := template.New("test")
+	t = template.Must(t.Parse(
+		`hello {{.UserName}}!
+{{ range .Emails }}
+an email {{ . }}
+{{- end }}
+{{ with .Friends }}
+{{- range . }}
+my friend name is {{.Fname}}
+{{- end }}
+{{ end }}`))
+	p := Person{UserName: "test",
+		Emails:  []string{"a1@qq.com", "a2@gmail.com"},
+		Friends: []*Friend{&f1, &f2}}
+	t.Execute(os.Stdout, p)
+}
package main
+
+import (
+	"os"
+	"text/template"
+)
+
+type Friend struct {
+	Fname string
+}
+type Person struct {
+	UserName string
+	Emails   []string
+	Friends  []*Friend
+}
+
+func main() {
+	f1 := Friend{Fname: "xiaofang"}
+	f2 := Friend{Fname: "wugui"}
+	t := template.New("test")
+	t = template.Must(t.Parse(
+		`hello {{.UserName}}!
+{{ range .Emails }}
+an email {{ . }}
+{{- end }}
+{{ with .Friends }}
+{{- range . }}
+my friend name is {{.Fname}}
+{{- end }}
+{{ end }}`))
+	p := Person{UserName: "test",
+		Emails:  []string{"a1@qq.com", "a2@gmail.com"},
+		Friends: []*Friend{&f1, &f2}}
+	t.Execute(os.Stdout, p)
+}

输出:

hello test!
+
+an email a1@qq.com
+an email a2@gmail.com
+
+my friend name is xiaofang
+my friend name is wugui
hello test!
+
+an email a1@qq.com
+an email a2@gmail.com
+
+my friend name is xiaofang
+my friend name is wugui

去除空白

template引擎在进行替换的时候,是完全按照文本格式进行替换的。除了需要评估和替换的地方,所有的行分隔符、空格等等空白都原样保留。所以, 对于要解析的内容,不要随意缩进、随意换行

go
//可以在`{{</span>`符号的后面加上短横线并保留一个或多个空格"- "来去除它前面的空白(包括换行符、制表符、空格等)
+//,即`{{- xxxx`。
+//在`}}`的前面加上一个或多个空格以及一个短横线"-"来去除它后面的空白,即`xxxx -}}`。
+
+{{23}} < {{45}}        -> 23 < 45
+{{23}} < {{- 45}}      ->  23 <45
+{{23 -}} < {{45}}      ->  23< 45
+{{23 -}} < {{- 45}}    ->  23<45
//可以在`{{</span>`符号的后面加上短横线并保留一个或多个空格"- "来去除它前面的空白(包括换行符、制表符、空格等)
+//,即`{{- xxxx`。
+//在`}}`的前面加上一个或多个空格以及一个短横线"-"来去除它后面的空白,即`xxxx -}}`。
+
+{{23}} < {{45}}        -> 23 < 45
+{{23}} < {{- 45}}      ->  23 <45
+{{23 -}} < {{45}}      ->  23< 45
+{{23 -}} < {{- 45}}    ->  23<45

上面的例子

go
t.Parse(
+`hello {{.UserName}}!
+{{ range .Emails }}
+an email {{ . }}
+{{- end }}
+{{ with .Friends }}
+{{- range . }}
+my friend name is {{.Fname}}
+{{- end }}
+{{ end }}`)
t.Parse(
+`hello {{.UserName}}!
+{{ range .Emails }}
+an email {{ . }}
+{{- end }}
+{{ with .Friends }}
+{{- range . }}
+my friend name is {{.Fname}}
+{{- end }}
+{{ end }}`)

注意,上面没有进行缩进。因为缩进的制表符或空格在替换的时候会保留。

注释

注释方式:

go
{{/* a comment */}}
{{/* a comment */}}

注释后的内容不会被引擎进行替换。但需要注意,注释行在替换的时候也会占用行,所以应该去除前缀和后缀空白,否则会多一空行。

go
{{- /* a comment without prefix/suffix space */}}
+{{/* a comment without prefix/suffix space */ -}}
+{{- /* a comment without prefix/suffix space */ -}}
{{- /* a comment without prefix/suffix space */}}
+{{/* a comment without prefix/suffix space */ -}}
+{{- /* a comment without prefix/suffix space */ -}}

注意,应该只去除前缀或后缀空白,不要同时都去除,否则会破坏原有的格式。

管道pipeline

pipeline是指产生数据的操作。

可以使用管道符号|链接多个命令,用法和unix下的管道类似:|前面的命令将运算结果(或返回值)传递给后一个命令的最后一个位置。

例如:

go
{{.}} | printf "%s\n" "abcd"
{{.}} | printf "%s\n" "abcd"

命令可以有超过1个的返回值,这时第二个返回值必须为err类型。

需要注意的是,并非只有使用了|才是pipeline。Go template中,pipeline的概念是传递数据,只要能产生数据的,都是pipeline。这使得某些操作可以作为另一些操作内部的表达式先运行得到结果,就像是Unix下的命令替换一样。

例如,下面的(len "output")是pipeline,它整体先运行。

go
{{println (len "output")}}
{{println (len "output")}}

下面是Pipeline的几种示例,它们都输出"output"

go
{{`"output"`}}
+{{printf "%q" "output"}}
+{{"output" | printf "%q"}}
+{{printf "%q" (print "out" "put")}}
+{{"put" | printf "%s%s" "out" | printf "%q"}}
+{{"output" | printf "%s" | printf "%q"}}
{{`"output"`}}
+{{printf "%q" "output"}}
+{{"output" | printf "%q"}}
+{{printf "%q" (print "out" "put")}}
+{{"put" | printf "%s%s" "out" | printf "%q"}}
+{{"output" | printf "%s" | printf "%q"}}

变量

可以在template中定义变量:

go
// 未定义过的变量
+$var := pipeline
+
+// 已定义过的变量
+$var = pipeline
// 未定义过的变量
+$var := pipeline
+
+// 已定义过的变量
+$var = pipeline
go
{{- $how_long :=(len "output")}}
+{{- println $how_long}}   // 输出6
{{- $how_long :=(len "output")}}
+{{- println $how_long}}   // 输出6
go
tx := template.Must(template.New("hh").Parse(
+`{{range $x := . -}}
+{{$y := 333}}
+{{- if (gt $x 33)}}{{println $x $y ($z := 444)}}{{- end}}
+{{- end}}
+`))
+s := []int{11, 22, 33, 44, 55}
+_ = tx.Execute(os.Stdout, s)
+//44 333 444
+//55 333 444
tx := template.Must(template.New("hh").Parse(
+`{{range $x := . -}}
+{{$y := 333}}
+{{- if (gt $x 33)}}{{println $x $y ($z := 444)}}{{- end}}
+{{- end}}
+`))
+s := []int{11, 22, 33, 44, 55}
+_ = tx.Execute(os.Stdout, s)
+//44 333 444
+//55 333 444

上面的示例中,使用range迭代slice,每个元素都被赋值给变量$x,每次迭代过程中,都新设置一个变量$y ,在内层嵌套的if结构中,可以使用这个两个外层的变量。在if的条件表达式中,使用了一个内置的比较函数gt,如果$x 大于33,则为true。在println的参数中还定义了一个$z,之所以能定义,是因为($z := 444)的过程是一个Pipeline,可以先运行。

需要注意三点:

  1. 变量有作用域,只要出现end,则当前层次的作用域结束。内层可以访问外层变量,但外层不能访问内层变量
  2. 有一个特殊变量$,它代表模板的最顶级作用域对象(通俗地理解,是以模板为全局作用域的全局变量),在Execute() 执行的时候进行赋值,且一直不变。例如上面的示例中,$ = [11 22 33 44 55]。再例如,define定义了一个模板t1,则t1中的$ 作用域只属于这个t1。
  3. 变量不可在模板之间继承。普通变量可能比较容易理解,但对于特殊变量"."和"$",比较容易搞混。见下面的例子。

条件判断

go
{{if pipeline}} T1 {{end}}
+{{if pipeline}} T1 {{else}} T0 {{end}}
+{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
+{{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}
{{if pipeline}} T1 {{end}}
+{{if pipeline}} T1 {{else}} T0 {{end}}
+{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
+{{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}

需要注意的是,pipeline为false的情况是各种数据对象的0值:数值0,指针或接口是nil,数组、slice、map或string则是len为0。

range...end迭代

有两种迭代表达式

go
{{range pipeline}} T1 {{end}}
+{{range pipeline}} T1 {{else}} T0 {{end}}
{{range pipeline}} T1 {{end}}
+{{range pipeline}} T1 {{else}} T0 {{end}}

range可以迭代slice、数组、map或channel。迭代的时候,会设置"."为当前正在迭代的元素。

对于第一个表达式,当迭代对象的值为0值时,则range直接跳过,就像if一样。对于第二个表达式,则在迭代到0值时执行else语句。

go
tx := template.Must(template.New("hh").Parse(
+`{{range $x := . -}}
+{{println $x}}
+{{- end}}
+`))
+s := []int{11, 22, 33, 44, 55}
+_ = tx.Execute(os.Stdout, s)
tx := template.Must(template.New("hh").Parse(
+`{{range $x := . -}}
+{{println $x}}
+{{- end}}
+`))
+s := []int{11, 22, 33, 44, 55}
+_ = tx.Execute(os.Stdout, s)

需注意的是,range的参数部分是pipeline,所以在迭代的过程中是可以进行赋值的。但有两种赋值情况:

go
{{range $value := .}}
+{{range $key,$value := .}}
{{range $value := .}}
+{{range $key,$value := .}}

如果range中只赋值给一个变量,则这个变量是当前正在迭代元素的值。如果赋值给两个变量,则第一个变量是索引值( map/slice是数值,map是key),第二个变量是当前正在迭代元素的值。

with...end

with用来设置"."的值。两种格式:

go
{{with pipeline}} T1 {{end}}
+{{with pipeline}} T1 {{else}} T0 {{end}}
{{with pipeline}} T1 {{end}}
+{{with pipeline}} T1 {{else}} T0 {{end}}

对于第一种格式,当pipeline不为0值的时候,点"." 设置为pipeline运算的值,否则跳过。对于第二种格式,当pipeline为0值时,执行else语句块,否则"."设置为pipeline运算的值,并执行T1。

go
{{with "xx"}}{{println .}}{{end}}
{{with "xx"}}{{println .}}{{end}}

上面将输出xx,因为"."已经设置为"xx"。

内置函数和自定义函数

template定义了一些内置函数,也支持自定义函数

go
and
+    返回第一个为空的参数或最后一个参数。可以有任意多个参数。
+    and x y等价于if x then y else x
+
+not
+    布尔取反。只能一个参数。
+
+or
+    返回第一个不为空的参数或最后一个参数。可以有任意多个参数。
+    "or x y"等价于"if x then x else y"
+
+print
+printf
+println
+    分别等价于fmt包中的Sprint、Sprintf、Sprintln
+
+len
+    返回参数的length。
+
+index
+    对可索引对象进行索引取值。第一个参数是索引对象,后面的参数是索引位。
+    "index x 1 2 3"代表的是x[1][2][3]。
+    可索引对象包括map、slice、array。
+
+call
+    显式调用函数。第一个参数必须是函数类型,且不是template中的函数,而是外部函数。
+    例如一个struct中的某个字段是func类型的。
+    "call .X.Y 1 2"表示调用dot.X.Y(1, 2),Y必须是func类型,函数参数是1和2。
+    函数必须只能有一个或2个返回值,如果有第二个返回值,则必须为error类型。
and
+    返回第一个为空的参数或最后一个参数。可以有任意多个参数。
+    and x y等价于if x then y else x
+
+not
+    布尔取反。只能一个参数。
+
+or
+    返回第一个不为空的参数或最后一个参数。可以有任意多个参数。
+    "or x y"等价于"if x then x else y"
+
+print
+printf
+println
+    分别等价于fmt包中的Sprint、Sprintf、Sprintln
+
+len
+    返回参数的length。
+
+index
+    对可索引对象进行索引取值。第一个参数是索引对象,后面的参数是索引位。
+    "index x 1 2 3"代表的是x[1][2][3]。
+    可索引对象包括map、slice、array。
+
+call
+    显式调用函数。第一个参数必须是函数类型,且不是template中的函数,而是外部函数。
+    例如一个struct中的某个字段是func类型的。
+    "call .X.Y 1 2"表示调用dot.X.Y(1, 2),Y必须是func类型,函数参数是1和2。
+    函数必须只能有一个或2个返回值,如果有第二个返回值,则必须为error类型。
go
eq arg1 arg2:
+    arg1 == arg2时为true
+ne arg1 arg2:
+    arg1 != arg2时为true
+lt arg1 arg2:
+    arg1 < arg2时为true
+le arg1 arg2:
+    arg1 <= arg2时为true
+gt arg1 arg2:
+    arg1 > arg2时为true
+ge arg1 arg2:
+    arg1 >= arg2时为true
eq arg1 arg2:
+    arg1 == arg2时为true
+ne arg1 arg2:
+    arg1 != arg2时为true
+lt arg1 arg2:
+    arg1 < arg2时为true
+le arg1 arg2:
+    arg1 <= arg2时为true
+gt arg1 arg2:
+    arg1 > arg2时为true
+ge arg1 arg2:
+    arg1 >= arg2时为true

对于eq函数,支持多个参数,它们都和第一个参数arg1进行比较。它等价于:

go
eq arg1 arg2 arg3 arg4...
+arg1==arg2 || arg1==arg3 || arg1==arg4
eq arg1 arg2 arg3 arg4...
+arg1==arg2 || arg1==arg3 || arg1==arg4

嵌套template:define和template

define可以直接在待解析内容中定义一个模板,这个模板会加入到common结构组中,并关联到关联名称上。

定义了模板之后,可以使用template这个action来执行模板。template有两种格式:

go
{{template "name"}}
+{{template "name" pipeline}}
{{template "name"}}
+{{template "name" pipeline}}

第一种是直接执行名为name的template,点设置为nil。第二种是点"."设置为pipeline的值,并执行名为name的template。可以将template看作是函数:

go
template("name)
+template("name",pipeline)
template("name)
+template("name",pipeline)
go
func main() {
+	t1 := template.New("test1")
+	tmpl, _ := t1.Parse(
+`{{- define "T1"}}ONE {{println .}}{{end}}
+{{- define "T2"}}TWO {{println .}}{{end}}
+{{- define "T3"}}{{template "T1"}}{{template "T2" "haha"}}{{end}}
+{{- template "T3" -}}
+`)
+	_ = tmpl.Execute(os.Stdout, "hello world")
+}
+//ONE <nil>
+//TWO haha
func main() {
+	t1 := template.New("test1")
+	tmpl, _ := t1.Parse(
+`{{- define "T1"}}ONE {{println .}}{{end}}
+{{- define "T2"}}TWO {{println .}}{{end}}
+{{- define "T3"}}{{template "T1"}}{{template "T2" "haha"}}{{end}}
+{{- template "T3" -}}
+`)
+	_ = tmpl.Execute(os.Stdout, "hello world")
+}
+//ONE <nil>
+//TWO haha

block块

go
{{block "name" pipeline}} T1 {{end}}
+	A block is shorthand for defining a template
+		{{define "name"}} T1 {{end}}
+	and then executing it in place
+		{{template "name" pipeline}}
+	The typical use is to define a set of root templates that are
+	then customized by redefining the block templates within.
{{block "name" pipeline}} T1 {{end}}
+	A block is shorthand for defining a template
+		{{define "name"}} T1 {{end}}
+	and then executing it in place
+		{{template "name" pipeline}}
+	The typical use is to define a set of root templates that are
+	then customized by redefining the block templates within.

根据官方文档的解释:block等价于define定义一个名为name的模板,并在"有需要"的地方执行这个模板,执行时将"."设置为pipeline的值。

但应该注意,**block的第一个动作是执行名为name的模板,如果不存在,则在此处自动定义这个模板,并执行这个临时定义的模板。换句话说,block可以认为是设置一个默认模板 **。

例如:

go
{{block "T1" .}} one {{end}}
{{block "T1" .}} one {{end}}

它首先找到T1模板,如果T1存在,则执行找到的T1,如果没找到T1,则临时定义一个,并执行它。

不转义

上下文感知的自动转义能让程序更加安全,比如防止XSS攻击(例如在表单中输入带有<script>...</script> 的内容并提交,会使得用户提交的这部分script被执行)。

如果确实不想转义,可以进行类型转换。

go
type CSS
+type HTML
+type JS
+type URL
type CSS
+type HTML
+type JS
+type URL
go
func process(w http.ResponseWriter, r *http.Request) {
+	t, _ := template.ParseFiles("tmpl.html")
+	t.Execute(w, template.HTML(r.FormValue("comment")))
+}
func process(w http.ResponseWriter, r *http.Request) {
+	t, _ := template.ParseFiles("tmpl.html")
+	t.Execute(w, template.HTML(r.FormValue("comment")))
+}
+ + + + \ No newline at end of file diff --git "a/golang/base/golang\345\237\272\347\241\200\350\257\255\346\263\225.html" "b/golang/base/golang\345\237\272\347\241\200\350\257\255\346\263\225.html" new file mode 100644 index 000000000..b3b075890 --- /dev/null +++ "b/golang/base/golang\345\237\272\347\241\200\350\257\255\346\263\225.html" @@ -0,0 +1,5027 @@ + + + + + + Golang基础语法 | 故事 + + + + + + + + + + + + + + +
Skip to content

Golang基础语法

第一个Go程序

go
package main
+
+import "fmt"
+
+func main() {
+	fmt.Println("Hello, World!")
+}
package main
+
+import "fmt"
+
+func main() {
+	fmt.Println("Hello, World!")
+}

关键字

  • package:定义当前源码文件所属的包。
  • import:导入其他包。
  • func:定义函数。
  • var:声明变量。
  • const:声明常量。
  • type:定义类型。
  • struct:定义结构体。
  • interface:定义接口。
  • map:定义映射类型。
  • range:用于循环迭代。
  • select:用于通道操作。
  • defer:延迟执行。
  • go:启动一个新的 goroutine。
  • chan:定义通道类型。
  • default:select 语句中的默认情况。
  • fallthrough:在 switch 语句中贯穿到下一个 case。
  • if:条件语句。
  • else:if 语句中的默认情况。
  • switch:多分支条件语句。
  • case:switch 语句中的分支情况。
  • for:循环语句。
  • break:跳出循环或 switch 语句。
  • continue:结束当前循环,开始下一次循环。
  • return:返回函数结果。
  • panic:抛出异常。

变量与常量

变量

声明变量不赋值

go
package main
+
+import "fmt"
+
+func main() {
+	var a int
+	fmt.Println("a = ", a)
+	fmt.Printf("a的类型是%T\n", a)
+}
+// a = 0
+// a的类型是int
package main
+
+import "fmt"
+
+func main() {
+	var a int
+	fmt.Println("a = ", a)
+	fmt.Printf("a的类型是%T\n", a)
+}
+// a = 0
+// a的类型是int
  • 整型、浮点型变量的默认值为0和0.0
  • 字符串变量的默认值为空字符串
  • 布尔类型变量默认为false
  • 切片、函数、指针变量的默认为nil

声明变量并初始化

go
package main
+
+import "fmt"
+
+func main() {
+	var a int = 10
+	fmt.Println("a =", a)
+	fmt.Printf("a的类型是%T\n", a)
+
+	var b string = "hello"
+	fmt.Println("b =", b)
+	fmt.Printf("b的类型是%T\n", b)
+}
+// a = 10
+// a的类型是int
+// b = hello
+// b的类型是string
package main
+
+import "fmt"
+
+func main() {
+	var a int = 10
+	fmt.Println("a =", a)
+	fmt.Printf("a的类型是%T\n", a)
+
+	var b string = "hello"
+	fmt.Println("b =", b)
+	fmt.Printf("b的类型是%T\n", b)
+}
+// a = 10
+// a的类型是int
+// b = hello
+// b的类型是string

声明变量省略类型

go
package main
+
+import "fmt"
+
+func main() {
+	var a = 10
+	fmt.Println("a =", a)
+	fmt.Printf("a的类型是%T\n", a)
+
+	var b = "hello"
+	fmt.Println("b =", b)
+	fmt.Printf("b的类型是%T\n", b)
+}
+// a = 10
+// a的类型是int
+// b = hello
+// b的类型是string
package main
+
+import "fmt"
+
+func main() {
+	var a = 10
+	fmt.Println("a =", a)
+	fmt.Printf("a的类型是%T\n", a)
+
+	var b = "hello"
+	fmt.Println("b =", b)
+	fmt.Printf("b的类型是%T\n", b)
+}
+// a = 10
+// a的类型是int
+// b = hello
+// b的类型是string

短声明(只能在函数内)

go
package main
+
+import "fmt"
+
+func main() {
+	c := "1"
+	fmt.Printf("c = %s, c的类型是%T\n", c, c)
+}
+// c = 1, c的类型是string
package main
+
+import "fmt"
+
+func main() {
+	c := "1"
+	fmt.Printf("c = %s, c的类型是%T\n", c, c)
+}
+// c = 1, c的类型是string

多变量声明

go
package main
+
+func main(){
+    var xx, yy int = 100, 200
+    var kk, wx = 300, "666
+    var (
+        nn int = 100
+        mm bool = true
+    )
+}
package main
+
+func main(){
+    var xx, yy int = 100, 200
+    var kk, wx = 300, "666
+    var (
+        nn int = 100
+        mm bool = true
+    )
+}

匿名变量

匿名变量:下划线_,本身就是一个特殊的标识符。可以像其他标识符那样用于变量的声明,任何类型都可以赋值给它,但任何赋值给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用。

变量交换

和其他静态类型语言不同,可以直接交换变量(python也有这个语法)

go
package main
+
+import "fmt"
+
+func main() {
+	var (
+		a = 100
+		b = 200
+	)
+
+	a, b = b, a
+
+	fmt.Println(a, b)
+}
+// 200 100
package main
+
+import "fmt"
+
+func main() {
+	var (
+		a = 100
+		b = 200
+	)
+
+	a, b = b, a
+
+	fmt.Println(a, b)
+}
+// 200 100

常量

go
package main
+
+import "fmt"
+
+func main(){
+    // 常量(只读属性)
+    const length int = 10
+    // length = 100  // 常量是不允许被修改的
+    fmt.Println("length = ", length)
+}
package main
+
+import "fmt"
+
+func main(){
+    // 常量(只读属性)
+    const length int = 10
+    // length = 100  // 常量是不允许被修改的
+    fmt.Println("length = ", length)
+}

使用常量定义枚举类型

go
package main
+
+import "fmt"
+
+// const来定义枚举类型
+const (
+    BEIJING = 0
+    SHANGHAI = 1
+    SHENZHEN = 2
+)
+
+func main() {
+    fmt.Println("BEIJING = ", BEIJING)      // 0
+    fmt.Println("SHANGHAI = ", SHANGHAI)    // 1
+    fmt.Println("SHENZHEN = ", SHENZHEN)    // 2
+}
package main
+
+import "fmt"
+
+// const来定义枚举类型
+const (
+    BEIJING = 0
+    SHANGHAI = 1
+    SHENZHEN = 2
+)
+
+func main() {
+    fmt.Println("BEIJING = ", BEIJING)      // 0
+    fmt.Println("SHANGHAI = ", SHANGHAI)    // 1
+    fmt.Println("SHENZHEN = ", SHENZHEN)    // 2
+}

iota常量计数器

iota 是一个常量生成器,用于生成一组相关的枚举值。iota 可以与 const 关键字一起使用,在定义一组枚举时,用来生成连续的值。const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)

go
// iota 初始值为 0,每当出现一个新的常量声明时,它的值就会自动加 1,因此 Monday 的值为 1,Tuesday 的值为 2,以此类推。
+const (
+    Sunday = iota // 0
+    Monday        // 1
+    Tuesday       // 2
+    Wednesday     // 3
+    Thursday      // 4
+    Friday        // 5
+    Saturday      // 6
+)
+
+// 在下面的例子中,B 被显式赋值为 3.14,因此接下来的 C 的值为 iota + 1,即 2,而 D 的值也是 iota + 1,所以它的值为 3。
+const (
+    A = iota // 0
+    B = 3.14 // 3.14
+    C = iota // 2
+    D        // 3
+)
// iota 初始值为 0,每当出现一个新的常量声明时,它的值就会自动加 1,因此 Monday 的值为 1,Tuesday 的值为 2,以此类推。
+const (
+    Sunday = iota // 0
+    Monday        // 1
+    Tuesday       // 2
+    Wednesday     // 3
+    Thursday      // 4
+    Friday        // 5
+    Saturday      // 6
+)
+
+// 在下面的例子中,B 被显式赋值为 3.14,因此接下来的 C 的值为 iota + 1,即 2,而 D 的值也是 iota + 1,所以它的值为 3。
+const (
+    A = iota // 0
+    B = 3.14 // 3.14
+    C = iota // 2
+    D        // 3
+)
go
package main
+
+import "fmt"
+
+// 定义递增的步长
+const (
+    BEIJING = iota * 10
+    SHANGHAI
+    SHENZHEN
+)
+
+func main() {
+    fmt.Println("BEIJING = ", BEIJING)      // 0
+    fmt.Println("SHANGHAI = ", SHANGHAI)    // 10
+    fmt.Println("SHENZHEN = ", SHENZHEN)    // 20
+}
package main
+
+import "fmt"
+
+// 定义递增的步长
+const (
+    BEIJING = iota * 10
+    SHANGHAI
+    SHENZHEN
+)
+
+func main() {
+    fmt.Println("BEIJING = ", BEIJING)      // 0
+    fmt.Println("SHANGHAI = ", SHANGHAI)    // 10
+    fmt.Println("SHENZHEN = ", SHENZHEN)    // 20
+}

基本数据类型

整型

  • int8:有符号 8 位整数类型,取值范围为 -128 到 127。
  • uint8(或 byte):无符号 8 位整数类型,取值范围为 0 到 255。
  • int16:有符号 16 位整数类型,取值范围为 -32768 到 32767。
  • uint16:无符号 16 位整数类型,取值范围为 0 到 65535。
  • int32(或 rune):有符号 32 位整数类型,取值范围为 -2147483648 到 2147483647。
  • uint32:无符号 32 位整数类型,取值范围为 0 到 4294967295。
  • int64:有符号 64 位整数类型,取值范围为 -9223372036854775808 到 9223372036854775807。
  • uint64:无符号 64 位整数类型,取值范围为 0 到 18446744073709551615。
go
package main
+
+import (
+	"fmt"
+	"math"
+	"unsafe"
+)
+
+// 有符号整型
+func Integer() {
+	var num8 int8 = 127
+	var num16 int16 = 32767
+	var num32 int32 = math.MaxInt32
+	var num64 int64 = math.MaxInt64
+	var num int = math.MaxInt
+	fmt.Printf("num8的类型是 %T, num8的大小 %d, num8是 %d\n",
+		num8, unsafe.Sizeof(num8), num8)
+	fmt.Printf("num16的类型是 %T, num16的大小 %d, num16是 %d\n",
+		num16, unsafe.Sizeof(num16), num16)
+	fmt.Printf("num32的类型是 %T, num32的大小 %d, num32是 %d\n",
+		num32, unsafe.Sizeof(num32), num32)
+	fmt.Printf("num64的类型是 %T, num64的大小 %d, num64是 %d\n",
+		num64, unsafe.Sizeof(num64), num64)
+	fmt.Printf("num的类型是 %T, num的大小 %d, num是 %d\n",
+		num, unsafe.Sizeof(num), num)
+}
+
+// 无符号整型
+func unsignedInteger() {
+	var num8 uint8 = 128
+	var num16 uint16 = 32768
+	var num32 uint32 = math.MaxUint32
+	var num64 uint64 = math.MaxUint64
+	var num uint = math.MaxUint
+	fmt.Printf("num8的类型是 %T, num8的大小 %d, num8是 %d\n",
+		num8, unsafe.Sizeof(num8), num8)
+	fmt.Printf("num16的类型是 %T, num16的大小 %d, num16是 %d\n",
+		num16, unsafe.Sizeof(num16), num16)
+	fmt.Printf("num32的类型是 %T, num32的大小 %d, num32是 %d\n",
+		num32, unsafe.Sizeof(num32), num32)
+	fmt.Printf("num64的类型是 %T, num64的大小 %d, num64是 %d\n",
+		num64, unsafe.Sizeof(num64), num64)
+	fmt.Printf("num的类型是 %T, num的大小 %d, num是 %d\n",
+		num, unsafe.Sizeof(num), num)
+}
+
+func main() {
+	Integer()
+	println("---------------------------------------")
+	unsignedInteger()
+}
package main
+
+import (
+	"fmt"
+	"math"
+	"unsafe"
+)
+
+// 有符号整型
+func Integer() {
+	var num8 int8 = 127
+	var num16 int16 = 32767
+	var num32 int32 = math.MaxInt32
+	var num64 int64 = math.MaxInt64
+	var num int = math.MaxInt
+	fmt.Printf("num8的类型是 %T, num8的大小 %d, num8是 %d\n",
+		num8, unsafe.Sizeof(num8), num8)
+	fmt.Printf("num16的类型是 %T, num16的大小 %d, num16是 %d\n",
+		num16, unsafe.Sizeof(num16), num16)
+	fmt.Printf("num32的类型是 %T, num32的大小 %d, num32是 %d\n",
+		num32, unsafe.Sizeof(num32), num32)
+	fmt.Printf("num64的类型是 %T, num64的大小 %d, num64是 %d\n",
+		num64, unsafe.Sizeof(num64), num64)
+	fmt.Printf("num的类型是 %T, num的大小 %d, num是 %d\n",
+		num, unsafe.Sizeof(num), num)
+}
+
+// 无符号整型
+func unsignedInteger() {
+	var num8 uint8 = 128
+	var num16 uint16 = 32768
+	var num32 uint32 = math.MaxUint32
+	var num64 uint64 = math.MaxUint64
+	var num uint = math.MaxUint
+	fmt.Printf("num8的类型是 %T, num8的大小 %d, num8是 %d\n",
+		num8, unsafe.Sizeof(num8), num8)
+	fmt.Printf("num16的类型是 %T, num16的大小 %d, num16是 %d\n",
+		num16, unsafe.Sizeof(num16), num16)
+	fmt.Printf("num32的类型是 %T, num32的大小 %d, num32是 %d\n",
+		num32, unsafe.Sizeof(num32), num32)
+	fmt.Printf("num64的类型是 %T, num64的大小 %d, num64是 %d\n",
+		num64, unsafe.Sizeof(num64), num64)
+	fmt.Printf("num的类型是 %T, num的大小 %d, num是 %d\n",
+		num, unsafe.Sizeof(num), num)
+}
+
+func main() {
+	Integer()
+	println("---------------------------------------")
+	unsignedInteger()
+}

TIP

  • 除非对整型的大小有特定的需求,否则你通常应该使用 int 表示整型宽度,在 32 位系统下是 32 位,而在 64 位系统下是 64 位。表示范围:在 32 位系统下是 -2147483648 ~ 2147483647 ,而在 64 位系统是 -9223372036854775808 ~ 9223372036854775807
  • 对于 int8int16 等这些类型后面有跟一个数值的类型来说,它们能表示的数值个数是固定的。所以,在有的时候:例如在二进制传输、读写文件的结构描述(为了保持文件的结构不会受到不同编译目标平台字节长度的影响)等情况下,使用更加精确的 int32int64 是更好的。

浮点型

  • float32 类型的变量占用 4 个字节的内存,可以表示的数值范围为±1.401298464324817e-45 到±3.4028234663852886e+38,精度约为 7 个十进制位。

  • float64 类型的变量占用 8 个字节的内存, 可以表示的数值范围为±4.9406564584124654e-324 到±1.7976931348623157e+308,精度约为 15 个十进制位。

Go 语言中的浮点数默认为 float64 类型,如果需要使用 float32 类型,需要显式声明。

go
package main
+
+import (
+	"fmt"
+	"math"
+)
+
+func showFloat() {
+	var num1 float32 = math.MaxFloat32
+	var num2 float64 = math.MaxFloat64
+	fmt.Printf("num1的类型是%T,num1是%g\n", num1, num1)
+	fmt.Printf("num2的类型是%T,num1是%g\n", num2, num2)
+}
+
+func main() {
+	showFloat()
+}
+//num1的类型是float32,num1是3.4028235e+38
+//num2的类型是float64,num1是1.7976931348623157e+308
package main
+
+import (
+	"fmt"
+	"math"
+)
+
+func showFloat() {
+	var num1 float32 = math.MaxFloat32
+	var num2 float64 = math.MaxFloat64
+	fmt.Printf("num1的类型是%T,num1是%g\n", num1, num1)
+	fmt.Printf("num2的类型是%T,num1是%g\n", num2, num2)
+}
+
+func main() {
+	showFloat()
+}
+//num1的类型是float32,num1是3.4028235e+38
+//num2的类型是float64,num1是1.7976931348623157e+308

字符

字符串中的每一个元素叫作“字符”,定义字符时使用单引号。Go 语言的字符有两种。

  • byte类型,占用1个字节,表示 UTF-8 字符串的单个字节的值,表示的是 ASCII 码表中的一个字符,uint8 的别名类型
  • rune类型,占用4个字节,表示单个 unicode 字符,int32 的别名类型
go
package main
+
+import (
+	"fmt"
+	"unsafe"
+)
+
+func showChar() {
+	var x byte = 65
+	var y uint8 = 65
+	z := 'A'
+	fmt.Printf("x = %c\n", x) // x = A
+	fmt.Printf("y = %c\n", y) // y = A
+	fmt.Printf("z = %c\n", z) // z = A
+
+}
+
+func sizeOfChar() {
+	var x byte = 65
+	fmt.Printf("x = %c\n", x)
+	fmt.Printf("x 占用 %d 个字节\n", unsafe.Sizeof(x))
+
+	var y rune = 'A'
+	fmt.Printf("y = %c\n", y)
+	fmt.Printf("y 占用 %d 个字节\n", unsafe.Sizeof(y))
+}
+
+func main() {
+	showChar()
+	sizeOfChar()
+}
package main
+
+import (
+	"fmt"
+	"unsafe"
+)
+
+func showChar() {
+	var x byte = 65
+	var y uint8 = 65
+	z := 'A'
+	fmt.Printf("x = %c\n", x) // x = A
+	fmt.Printf("y = %c\n", y) // y = A
+	fmt.Printf("z = %c\n", z) // z = A
+
+}
+
+func sizeOfChar() {
+	var x byte = 65
+	fmt.Printf("x = %c\n", x)
+	fmt.Printf("x 占用 %d 个字节\n", unsafe.Sizeof(x))
+
+	var y rune = 'A'
+	fmt.Printf("y = %c\n", y)
+	fmt.Printf("y 占用 %d 个字节\n", unsafe.Sizeof(y))
+}
+
+func main() {
+	showChar()
+	sizeOfChar()
+}

字符串

字符串在Go语言中是基本数据类型。

go
var study string  	 		// 定义名为str的字符串类型变量
+study = "《123》"		// 将变量赋值
+study2 := "《789》"		// 以自动推断方式初始化
var study string  	 		// 定义名为str的字符串类型变量
+study = "《123》"		// 将变量赋值
+study2 := "《789》"		// 以自动推断方式初始化

定义多行字符串的方法如下。

  • 双引号书写字符串被称为字符串字面量(string literal),这种字面量不能跨行。
  • 多行字符串需要使用反引号“`”,多用于内嵌源码和内嵌数据。
  • 在反引号中的所有代码不会被编译器识别,而只是作为字符串的一部分。
go
package main
+import "fmt"
+
+func main() {
+  var s1 string
+	s1 = `
+    		study := 'Go语言'
+    		fmt.Println(study)
+			`
+	fmt.Println(s1)
+}
package main
+import "fmt"
+
+func main() {
+  var s1 string
+	s1 = `
+    		study := 'Go语言'
+    		fmt.Println(study)
+			`
+	fmt.Println(s1)
+}

布尔

go
func showBool(){
+	a := true
+	b := false
+	fmt.Println("a=", a)
+	fmt.Println("b=", b)
+	fmt.Println("true && false = ", a && b)
+	fmt.Println("true || false = ", a || b)
+}
+
+func main() {
+    showBool()
+}
func showBool(){
+	a := true
+	b := false
+	fmt.Println("a=", a)
+	fmt.Println("b=", b)
+	fmt.Println("true && false = ", a && b)
+	fmt.Println("true || false = ", a || b)
+}
+
+func main() {
+    showBool()
+}

复数

类 型字 节 数说 明
complex64864 位的复数型,由 float32 类型的实部和虚部联合表示
complex12816128 位的复数型,由 float64 类型的实部和虚部联合表示
go
func showComplex() {
+	// 内置的 complex 函数用于构建复数
+	var x complex64 = complex(1, 2)
+	var y complex128 = complex(3, 4)
+	var z complex128 = complex(5, 6)
+	fmt.Println("x = ", x)
+	fmt.Println("y = ", y)
+	fmt.Println("z = ", z)
+
+	// 内建的 real 和 imag 函数分别返回复数的实部和虚部
+	fmt.Println("real(x) = ", real(x))
+	fmt.Println("imag(x) = ", imag(x))
+	fmt.Println("y * z = ", y*z)
+}
+
+func main() {
+   showComplex()
+}
func showComplex() {
+	// 内置的 complex 函数用于构建复数
+	var x complex64 = complex(1, 2)
+	var y complex128 = complex(3, 4)
+	var z complex128 = complex(5, 6)
+	fmt.Println("x = ", x)
+	fmt.Println("y = ", y)
+	fmt.Println("z = ", z)
+
+	// 内建的 real 和 imag 函数分别返回复数的实部和虚部
+	fmt.Println("real(x) = ", real(x))
+	fmt.Println("imag(x) = ", imag(x))
+	fmt.Println("y * z = ", y*z)
+}
+
+func main() {
+   showComplex()
+}

TIP

同样可以用自然方式表示复数

go
x := 1 + 2i
+y := 3 + 4i
+z := 5 + 6i
x := 1 + 2i
+y := 3 + 4i
+z := 5 + 6i

fmt格式化输出

格式含义
%%一个%字面量
%b一个二进制整数值(基数为 2),或者是一个(高级的)用科学计数法表示的指数为 2 的浮点数
%c字符型。可以把输入的数字按照 ASCII 码相应转换为对应的字符
%d一个十进制数值(基数为 10)
%f以标准记数法表示的浮点数或者复数值
%o一个以八进制表示的数字(基数为 8)
%p以十六进制(基数为 16)表示的一个值的地址,前缀为 0x,字母使用小写的 a-f 表示
%q使用 Go 语法以及必须时使用转义,以双引号括起来的字符串或者字节切片[]byte,或者是以单引号括起来的数字
%s字符串。输出字符串中的字符直至字符串中的空字符(字符串以’\0‘结尾,这个’\0’即空字符)
%t以 true 或者 false 输出的布尔值
%T使用 Go 语法输出的值的类型
%x以十六进制表示的整型值(基数为十六),数字 a-f 使用小写表示
%X以十六进制表示的整型值(基数为十六),数字 A-F 使用小写表示
  • fmt.Print: 将指定的内容打印到标准输出,不换行。

  • fmt.Println: 将指定的内容打印到标准输出,并在末尾添加换行符。

  • fmt.Printf: 根据格式字符串将指定的内容格式化后打印到标准输出。

  • fmt.Sprintf: 根据格式字符串将指定的内容格式化后返回一个格式化的字符串。

  • fmt.Scan: 从标准输入读取内容,并将其存储到指定的变量中。

  • fmt.Scanln: 从标准输入按空格分隔读取内容,并将其存储到指定的变量中,遇到换行符停止。

  • fmt.Scanf: 根据格式字符串从标准输入读取内容,并将其按指定的格式存储到指定的变量中。

  • fmt.Errorf: 根据格式字符串创建一个新的错误。

运算符

算数运算符

+-*/%++--

关系运算符

==!=><>=<=

逻辑运算符

&&||!

位运算符

&|^<<>>

容器类型

数组

Go 中的数组是值类型而不是引用类型。当数组赋值给一个新的变量时,该变量会得到一个原始数组的一个副本。如果对新变量进行更改,不会影响原始数组。

go
func arrByValue() {
+	arr := [...]string{"123", "456", "789"}
+	copy := arr
+	copy[0] = "Golang"
+	fmt.Println(arr)
+	fmt.Println(copy)
+}
func arrByValue() {
+	arr := [...]string{"123", "456", "789"}
+	copy := arr
+	copy[0] = "Golang"
+	fmt.Println(arr)
+	fmt.Println(copy)
+}

声明

var variable_name [SIZE]variable_type

初始化

  • var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
  • balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

如果数组长度不确定,可以使用 ... 代替数组的长度,编译器会根据元素个数自行推断数组的长度:

go
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
+
+balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
+
+balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

如果设置了数组的长度,我们还可以通过指定下标来初始化元素:

go
//  将索引为 1 和 3 的元素初始化
+balance := [5]float32{1:2.0,3:7.0}
//  将索引为 1 和 3 的元素初始化
+balance := [5]float32{1:2.0,3:7.0}

初始化数组中 {} 中的元素个数不能大于 [] 中的数字。

如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小:

go
balance[4] = 50.0
balance[4] = 50.0

访问

数组元素可以通过索引(位置)来读取。格式为数组名后加中括号,中括号中为索引的值。例如:

go
var salary float32 = balance[9]
var salary float32 = balance[9]

数组长度

len(arr)

数组遍历

使用for range循环

go
func showArr() {
+	arr := [...]string{"123", "456", "789"}
+	for index, value := range arr {
+		fmt.Printf("arr[%d]=%s\n", index, value)
+	}
+
+	for _, value := range arr {
+		fmt.Printf("value=%s\n", value)
+	}
+}
func showArr() {
+	arr := [...]string{"123", "456", "789"}
+	for index, value := range arr {
+		fmt.Printf("arr[%d]=%s\n", index, value)
+	}
+
+	for _, value := range arr {
+		fmt.Printf("value=%s\n", value)
+	}
+}

切片Slice

Go 语言切片是对数组的抽象。

Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

定义切片

go
var identifier []type
var identifier []type

切片不需要说明长度。

或使用 make() 函数来创建切片:

go
var slice1 []type = make([]type, len)
+
+也可以简写为
+
+slice1 := make([]type, len)
var slice1 []type = make([]type, len)
+
+也可以简写为
+
+slice1 := make([]type, len)

也可以指定容量,其中 capacity 为可选参数。

go
make([]T, length, capacity)
make([]T, length, capacity)

这里 len 是数组的长度并且也是切片的初始长度。

切片初始化

直接初始化切片,[] 表示是切片类型,{1,2,3} 初始化值依次是 1,2,3,其 cap=len=3

go
s :=[] int {1,2,3 }
s :=[] int {1,2,3 }

初始化切片 s,是数组 arr 的引用。

go
s := arr[:]
s := arr[:]

将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片。

go
s := arr[startIndex:endIndex]
s := arr[startIndex:endIndex]

默认 endIndex 时将表示一直到arr的最后一个元素。

go
s := arr[startIndex:]
s := arr[startIndex:]

默认 startIndex 时将表示从 arr 的第一个元素开始。

go
s := arr[:endIndex]
s := arr[:endIndex]

通过切片 s 初始化切片 s1。

go
s1 := s[startIndex:endIndex]
s1 := s[startIndex:endIndex]

通过内置函数 make() 初始化切片s[]int 标识为其元素类型为 int 的切片。

go
s :=make([]int,len,cap)
s :=make([]int,len,cap)

make([]T, length, capacity) 用于创建一个指定类型 T、长度为 length、容量为 capacity 的切片。其中,length 表示切片的实际长度,而 capacity 则表示切片底层数组的容量。

切片的容量可以理解为底层数组能够容纳的元素数量。当切片的容量不足以容纳新添加的元素时,Go 会自动将底层数组扩展一倍,并将原有的元素复制到新的数组中。因此,在预先分配足够容量的情况下,可以避免频繁的内存分配和数据复制操作,提高代码的性能。

需要注意的是,capacity 参数不能小于 length 参数。如果 capacity 小于 length,则会抛出一个运行时异常。

  • 由于 slice 是引用类型,所以你不对它进行赋值的话,它的默认值是 nil
go
var numList []int
+fmt.Println(numList == nil) // true
var numList []int
+fmt.Println(numList == nil) // true
  • 切片之间不能比较,因此我们不能使用 == 操作符来判断两个 slice 是否含有全部相等元素。特别注意,如果你需要测试一个 slice 是否是空的,使用 len(s) == 0 来判断,而不应该用 s == nil 来判断。

切片的长度和容量

一个 slice 由三个部分构成:指针长度容量 。指针指向第一个 slice 元素对应的底层数组元素的地址,要注意的是 slice 的第一个元素并不一定就是数组的第一个元素。长度对应 slice 中元素的数目;长度不能超过容量,容量一般是从 slice 的开始位置到底层数据的结尾位置。简单的讲,容量就是从创建切片索引开始的底层数组中的元素个数,而长度是切片中的元素个数。

内置的 lencap 函数分别返回 slice 的长度和容量。

go
s := make([]string, 3, 5)
+fmt.Println(len(s)) // 3
+fmt.Println(cap(s)) // 5
s := make([]string, 3, 5)
+fmt.Println(len(s)) // 3
+fmt.Println(cap(s)) // 5

切片元素修改

切片自己不拥有任何数据。它只是底层数组的一种表示。对切片所做的任何修改都会反映在底层数组中。

go
func modifySlice() {
+	var arr = [...]string{"123", "456", "789"}
+	s := arr[:] //[0:len(arr)]
+	fmt.Println(arr) 
+	fmt.Println(s)
+
+	s[0] = "Go语言"
+	fmt.Println(arr) 
+	fmt.Println(s) 
+}
func modifySlice() {
+	var arr = [...]string{"123", "456", "789"}
+	s := arr[:] //[0:len(arr)]
+	fmt.Println(arr) 
+	fmt.Println(s)
+
+	s[0] = "Go语言"
+	fmt.Println(arr) 
+	fmt.Println(s) 
+}

这里的 arr[:] 没有填入起始值和结束值,默认就是 0len(arr)

追加切片元素

使用 append 可以将新元素追加到切片上。append 函数的定义是 func append(slice []Type, elems ...Type) []Type 。其中 elems ...Type 在函数定义中表示该函数接受参数 elems 的个数是可变的。这些类型的函数被称为可变参数。

go
func appendSliceData() {
+	s := []string{"123"}
+	fmt.Println(s)
+	fmt.Println(cap(s))
+
+	s = append(s, "567")
+	fmt.Println(s)
+	fmt.Println(cap(s))
+
+	s = append(s, "789", "0")
+	fmt.Println(s)
+	fmt.Println(cap(s))
+
+	s = append(s, []string{"1", "2"}...)
+	fmt.Println(s)
+	fmt.Println(cap(s))
+}
func appendSliceData() {
+	s := []string{"123"}
+	fmt.Println(s)
+	fmt.Println(cap(s))
+
+	s = append(s, "567")
+	fmt.Println(s)
+	fmt.Println(cap(s))
+
+	s = append(s, "789", "0")
+	fmt.Println(s)
+	fmt.Println(cap(s))
+
+	s = append(s, []string{"1", "2"}...)
+	fmt.Println(s)
+	fmt.Println(cap(s))
+}

当新的元素被添加到切片时,如果容量不足,会创建一个新的数组。现有数组的元素被复制到这个新数组中,并返回新的引用。现在新切片的容量是旧切片的两倍。

多维切片

类似于数组,切片也可以有多个维度。

go
func mSlice() {
+	numList := [][]string{
+		{"1", "123"},
+		{"2", "456"},
+		{"3", "789"},
+	}
+	fmt.Println(numList)
+}
func mSlice() {
+	numList := [][]string{
+		{"1", "123"},
+		{"2", "456"},
+		{"3", "789"},
+	}
+	fmt.Println(numList)
+}

Map

在 Go 语言中,map 是散列表(哈希表)的引用。它是一个拥有键值对元素的无序集合,在这个集合中,键是唯一的,可以通过键来获取、更新或移除操作。无论这个散列表有多大,这些操作基本上是通过常量时间完成的。所有可比较的类型,如 整型字符串 等,都可以作为 key

创建Map

使用 make 函数传入键和值的类型,可以创建 map 。具体语法为 make(map[KeyType]ValueType)

go
// 创建一个键类型为 string 值类型为 int 名为 scores 的 map
+scores := make(map[string]int)
+steps := make(map[string]string)
// 创建一个键类型为 string 值类型为 int 名为 scores 的 map
+scores := make(map[string]int)
+steps := make(map[string]string)

字面量创建:

go
var steps2 map[string]string = map[string]string{
+		"第一步": "123",
+		"第二步": "456",
+		"第三步": "789",
+}
+fmt.Println(steps2)
var steps2 map[string]string = map[string]string{
+		"第一步": "123",
+		"第二步": "456",
+		"第三步": "789",
+}
+fmt.Println(steps2)
go
steps3 := map[string]string{
+		"第一步": "123",
+		"第二步": "456",
+		"第三步": "789",
+}
+fmt.Println(steps3)
steps3 := map[string]string{
+		"第一步": "123",
+		"第二步": "456",
+		"第三步": "789",
+}
+fmt.Println(steps3)

Map操作

  • 添加元素

    GO
    // 可以使用 `map[key] = value` 向 map 添加元素。
    +steps3["第四步"] = "总监"
    // 可以使用 `map[key] = value` 向 map 添加元素。
    +steps3["第四步"] = "总监"
  • 更新元素

    GO
    // 若 key 已存在,使用 map[key] = value 可以直接更新对应 key 的 value 值。
    +steps3["第四步"] = "CTO"
    // 若 key 已存在,使用 map[key] = value 可以直接更新对应 key 的 value 值。
    +steps3["第四步"] = "CTO"
  • 获取元素

    GO
    // 直接使用 map[key] 即可获取对应 key 的 value 值,如果 key不存在,会返回其 value 类型的零值。
    +fmt.Println(steps3["第四步"] )
    // 直接使用 map[key] 即可获取对应 key 的 value 值,如果 key不存在,会返回其 value 类型的零值。
    +fmt.Println(steps3["第四步"] )
  • 删除元素

    GO
    //使用 delete(map, key)可以删除 map 中的对应 key 键值对,如果 key 不存在,delete 函数会静默处理,不会报错。
    +delete(steps3, "第四步")
    //使用 delete(map, key)可以删除 map 中的对应 key 键值对,如果 key 不存在,delete 函数会静默处理,不会报错。
    +delete(steps3, "第四步")
  • 判断 key 是否存在

    GO
    // 如果我们想知道 map 中的某个 key 是否存在,可以使用下面的语法:value, ok := map[key]
    +v3, ok := steps3["第三步"]
    +fmt.Println(ok)
    +fmt.Println(v3)
    +
    +v4, ok := steps3["第四步"]
    +fmt.Println(ok)
    +fmt.Println(v4)
    // 如果我们想知道 map 中的某个 key 是否存在,可以使用下面的语法:value, ok := map[key]
    +v3, ok := steps3["第三步"]
    +fmt.Println(ok)
    +fmt.Println(v3)
    +
    +v4, ok := steps3["第四步"]
    +fmt.Println(ok)
    +fmt.Println(v4)

    这个语句说明 map 的下标读取可以返回两个值,第一个值为当前 keyvalue 值,第二个值表示对应的 key 是否存在,若存在 oktrue ,若不存在,则 okfalse

  • 遍历 map

    GO
    // 遍历 map 中所有的元素需要用 for range 循环。
    +for key, value := range steps3 {
    +    fmt.Printf("key: %s, value: %s\n", key, value)
    +}
    // 遍历 map 中所有的元素需要用 for range 循环。
    +for key, value := range steps3 {
    +    fmt.Printf("key: %s, value: %s\n", key, value)
    +}
  • 获取 map 长度

    GO
    // 使用 len 函数可以获取 map 长度
    +func createMap() {
    +  	//...
    +     fmt.Println(len(steps3))    // 4
    +}
    // 使用 len 函数可以获取 map 长度
    +func createMap() {
    +  	//...
    +     fmt.Println(len(steps3))    // 4
    +}

map是引用类型

map 被赋值为一个新变量的时候,它们指向同一个内部数据结构。因此,改变其中一个变量,就会影响到另一变量。

GO
func mapByReference() {
+		steps4 := map[string]string{
+		"第一步": "123",
+		"第二步": "456",
+		"第三步": "789",
+	}
+	fmt.Println("steps4: ", steps4)
+	// steps4:  map[第一步:123 第三步:789 第二步:456]
+	newSteps4 := steps4
+	newSteps4["第一步"] = "123-222"
+	newSteps4["第二步"] = "456-222"
+	newSteps4["第三步"] = "789-222"
+	fmt.Println("steps4: ", steps4)
+  // steps4:  map[第一步:123-222 第三步:789-222 第二步:456-222]
+	fmt.Println("newSteps4: ", newSteps4)
+  // newSteps4:  map[第一步:123-222 第三步:789-222 第二步:456-222]
+}
func mapByReference() {
+		steps4 := map[string]string{
+		"第一步": "123",
+		"第二步": "456",
+		"第三步": "789",
+	}
+	fmt.Println("steps4: ", steps4)
+	// steps4:  map[第一步:123 第三步:789 第二步:456]
+	newSteps4 := steps4
+	newSteps4["第一步"] = "123-222"
+	newSteps4["第二步"] = "456-222"
+	newSteps4["第三步"] = "789-222"
+	fmt.Println("steps4: ", steps4)
+  // steps4:  map[第一步:123-222 第三步:789-222 第二步:456-222]
+	fmt.Println("newSteps4: ", newSteps4)
+  // newSteps4:  map[第一步:123-222 第三步:789-222 第二步:456-222]
+}

map 作为函数参数传递时也会发生同样的情况。

流程控制语句

条件语句

go
if 条件1 {
+  逻辑代码1
+} else if  条件2 {
+  逻辑代码2
+} else if 条件 ... {
+  逻辑代码 ...
+} else {
+  逻辑代码 else
+}
if 条件1 {
+  逻辑代码1
+} else if  条件2 {
+  逻辑代码2
+} else if 条件 ... {
+  逻辑代码 ...
+} else {
+  逻辑代码 else
+}
go
score := 88
+if score >= 90 {
+    fmt.Println("成绩等级为A")
+} else if score >= 80 {
+    fmt.Println("成绩等级为B")
+} else if score >= 70 {
+    fmt.Println("成绩等级为C")
+} else if score >= 60 {
+    fmt.Println("成绩等级为D")
+} else {
+    fmt.Println("成绩等级为E 成绩不及格")
+}
score := 88
+if score >= 90 {
+    fmt.Println("成绩等级为A")
+} else if score >= 80 {
+    fmt.Println("成绩等级为B")
+} else if score >= 70 {
+    fmt.Println("成绩等级为C")
+} else if score >= 60 {
+    fmt.Println("成绩等级为D")
+} else {
+    fmt.Println("成绩等级为E 成绩不及格")
+}

if 还有另外一种写法,它包含一个 statement 可选语句部分,该可选语句在条件判断之前运行。它的语法是:

go
if statement; condition {
+}
+
+if score := 88; score >= 60 {
+    fmt.Println("成绩及格")
+}
if statement; condition {
+}
+
+if score := 88; score >= 60 {
+    fmt.Println("成绩及格")
+}

switch case

go
switch 表达式 {
+    case 表达式值1:
+        业务逻辑代码1
+    case 表达式值2:
+        业务逻辑代码2
+    case 表达式值3:
+        业务逻辑代码3
+    case 表达式值 ...:
+        业务逻辑代码 ...
+    default:
+        业务逻辑代码
+}
switch 表达式 {
+    case 表达式值1:
+        业务逻辑代码1
+    case 表达式值2:
+        业务逻辑代码2
+    case 表达式值3:
+        业务逻辑代码3
+    case 表达式值 ...:
+        业务逻辑代码 ...
+    default:
+        业务逻辑代码
+}
go
grade := "B"
+switch grade {
+case "A":
+    fmt.Println("Your score is between 90 and 100.")
+case "B":
+    fmt.Println("Your score is between 80 and 90.")
+case "C":
+    fmt.Println("Your score is between 70 and 80.")
+case "D":
+    fmt.Println("Your score is between 60 and 70.")
+default:
+    fmt.Println("Your score is below 60.")
+}
grade := "B"
+switch grade {
+case "A":
+    fmt.Println("Your score is between 90 and 100.")
+case "B":
+    fmt.Println("Your score is between 80 and 90.")
+case "C":
+    fmt.Println("Your score is between 70 and 80.")
+case "D":
+    fmt.Println("Your score is between 60 and 70.")
+default:
+    fmt.Println("Your score is below 60.")
+}

一个 case 多个条件

go
month := 5
+switch month {
+case 1, 3, 5, 7, 8, 10, 12:
+    fmt.Println("该月份有 31 天")
+case 4, 6, 9, 11:
+    fmt.Println("该月份有 30 天")
+case 2:
+    fmt.Println("该月份闰年为 29 天,非闰年为 28 天")
+default:
+    fmt.Println("输入有误!")
+}
month := 5
+switch month {
+case 1, 3, 5, 7, 8, 10, 12:
+    fmt.Println("该月份有 31 天")
+case 4, 6, 9, 11:
+    fmt.Println("该月份有 30 天")
+case 2:
+    fmt.Println("该月份闰年为 29 天,非闰年为 28 天")
+default:
+    fmt.Println("输入有误!")
+}

switch 还有另外一种写法,它包含一个 statement 可选语句部分,该可选语句在表达式之前运行。它的语法是:

go
switch statement; expression {
+}
+
+
+switch month := 5; month {
+case 1, 3, 5, 7, 8, 10, 12:
+    fmt.Println("该月份有 31 天")
+case 4, 6, 9, 11:
+    fmt.Println("该月份有 30 天")
+case 2:
+    fmt.Println("该月份闰年为 29 天,非闰年为 28 天")
+default:
+    fmt.Println("输入有误!")
+}
switch statement; expression {
+}
+
+
+switch month := 5; month {
+case 1, 3, 5, 7, 8, 10, 12:
+    fmt.Println("该月份有 31 天")
+case 4, 6, 9, 11:
+    fmt.Println("该月份有 30 天")
+case 2:
+    fmt.Println("该月份闰年为 29 天,非闰年为 28 天")
+default:
+    fmt.Println("输入有误!")
+}

这里 month 变量的作用域就仅限于这个 switch 内。

switch 后可接函数

switch 后面可以接一个函数,只要保证 case 后的值类型与函数的返回值一致即可。

go
package main
+
+import "fmt"
+
+func getResult(args ...int) bool {
+ for _, v := range args {
+  if v < 60 {
+   return false
+  }
+ }
+ return true
+}
+
+func main() {
+ chinese := 88
+ math := 90
+ english := 95
+
+ switch getResult(chinese, math, english) {
+ case true:
+  fmt.Println("考试通过")
+ case false:
+  fmt.Println("考试未通过")
+ }
+}
package main
+
+import "fmt"
+
+func getResult(args ...int) bool {
+ for _, v := range args {
+  if v < 60 {
+   return false
+  }
+ }
+ return true
+}
+
+func main() {
+ chinese := 88
+ math := 90
+ english := 95
+
+ switch getResult(chinese, math, english) {
+ case true:
+  fmt.Println("考试通过")
+ case false:
+  fmt.Println("考试未通过")
+ }
+}

无表达式的 switch

switch 后面的表达式是可选的。如果省略该表达式,则表示这个 switch 语句等同于 switch true ,并且每个 case 表达式都被认定为有效,相应的代码块也会被执行。

go
score := 88
+switch {
+case score >= 90 && score <= 100:
+    fmt.Println("grade A")
+case score >= 80 && score < 90:
+    fmt.Println("grade B")
+case score >= 70 && score < 80:
+    fmt.Println("grade C")
+case score >= 60 && score < 70:
+    fmt.Println("grade D")
+case score < 60:
+    fmt.Println("grade E")
+}
score := 88
+switch {
+case score >= 90 && score <= 100:
+    fmt.Println("grade A")
+case score >= 80 && score < 90:
+    fmt.Println("grade B")
+case score >= 70 && score < 80:
+    fmt.Println("grade C")
+case score >= 60 && score < 70:
+    fmt.Println("grade D")
+case score < 60:
+    fmt.Println("grade E")
+}

switch-case 语句相当于 if-elseif-else 语句。

fallthrough 语句

正常情况下 switch-case 语句在执行时只要有一个 case 满足条件,就会直接退出 switch-case ,如果一个都没有满足,才会执行 default 的代码块。不同于其他语言需要在每个 case 中添加 break 语句才能退出。使用 fallthrough 语句可以在已经执行完成的 case 之后,把控制权转移到下一个 case 的执行代码中。fallthrough 只能穿透一层,不管你有没有匹配上,都要退出了。fallthrough 语句是 case 子句的最后一个语句。如果它出现在了 case 语句的中间,编译会不通过。

go
s := "123"
+switch {
+case s == "123":
+    fmt.Println("123")
+    fallthrough
+case s == "456":
+    fmt.Println("456")
+case s != "789":
+    fmt.Println("789")
+}
s := "123"
+switch {
+case s == "123":
+    fmt.Println("123")
+    fallthrough
+case s == "456":
+    fmt.Println("456")
+case s != "789":
+    fmt.Println("789")
+}

循环语句

循环语句 可以用来重复执行某一段代码。在 C 语言中,循环语句有 forwhiledo while 三种循环。但在 Go 中只有 for 一种循环语句。下面是 for 循环语句的四种基本模型:

go
// for 接三个表达式
+for initialisation; condition; post {
+   code
+}
+
+// for 接一个条件表达式
+for condition {
+   code
+}
+
+// for 接一个 range 表达式
+for range_expression {
+   code
+}
+
+// for 不接表达式
+for {
+   code
+}
// for 接三个表达式
+for initialisation; condition; post {
+   code
+}
+
+// for 接一个条件表达式
+for condition {
+   code
+}
+
+// for 接一个 range 表达式
+for range_expression {
+   code
+}
+
+// for 不接表达式
+for {
+   code
+}
  • 接一个条件表达式

    go
    num := 0
    +for num < 4 {
    +    fmt.Println(num)
    +    num++
    +}
    num := 0
    +for num < 4 {
    +    fmt.Println(num)
    +    num++
    +}
  • 接三个表达式

    for 后面接的这三个表达式,各有各的用途:

    • 第一个表达式(initialisation):初始化控制变量,在整个循环生命周期内,只执行一次;
    • 第二个表达式(condition):设置循环控制条件,该表达式值为 true 时循环,值为 false 时结束循环;
    • 第三个表达式(post):每次循环完都会执行此表达式,可以利用其让控制变量增量或减量。

    这三个表达式,使用 ; 分隔。

    go
    for num := 0; num < 4; num++ {
    +    fmt.Println(num)
    +}
    for num := 0; num < 4; num++ {
    +    fmt.Println(num)
    +}
  • 接一个 range 表达式

    go
    str := "Golang"
    +for index, value := range str{
    +    fmt.Printf("index %d, value %c\n", index, value)
    +}
    str := "Golang"
    +for index, value := range str{
    +    fmt.Printf("index %d, value %c\n", index, value)
    +}
  • 不接表达式

    for 后面不接表达式就相当于无限循环,当然,可以使用 break 语句退出循环

    go
    // 第一种写法
    +for {
    +    code
    +}
    +// 第二种写法
    +for ;; {
    +    code
    +}
    // 第一种写法
    +for {
    +    code
    +}
    +// 第二种写法
    +for ;; {
    +    code
    +}
  • break 语句

    break 语句用于终止 for 循环,之后程序将执行在 for 循环后的代码。上面的例子已经演示了 break 语句的使用。

  • continue 语句

    continue 语句用来跳出 for 循环中的当前循环。在 continue 语句后的所有的 for 循环语句都不会在本次循环中执行,执行完 continue 语句后将会继续执行一下次循环。下面的程序会打印出 10 以内的奇数。

defer延迟调用

含有 defer 语句的函数,会在该函数将要返回之前,调用另一个函数。简单点说就是 defer 语句后面跟着的函数会延迟到当前函数执行完后再执行。

go
package main
+
+import "fmt"
+
+func bookPrint() {
+	fmt.Println("123")
+}
+
+func main() {
+	defer bookPrint()
+	fmt.Println("main函数...")
+}
package main
+
+import "fmt"
+
+func bookPrint() {
+	fmt.Println("123")
+}
+
+func main() {
+	defer bookPrint()
+	fmt.Println("main函数...")
+}

首先,执行 main 函数,因为 bookPrint() 函数前有 defer 关键字,所以会在执行完 main 函数后再执行 bookPrint() 函数,所以先打印出 main函数... ,再执行 bookPrint() 函数打印 123

即时求值的变量快照

使用 defer 只是延时调用函数,传递给函数里的变量,不应该受到后续程序的影响。

go
str := "123"
+defer fmt.Println(str)
+str = "456"
+fmt.Println(str)
+// 456
+// 123
str := "123"
+defer fmt.Println(str)
+str = "456"
+fmt.Println(str)
+// 456
+// 123

延迟方法

defer 不仅能够延迟函数的执行,也能延迟方法的执行。

go
package main
+
+import "fmt"
+
+type Book struct {
+	bookName, authorName string
+}
+
+func (b Book) printName() {
+	fmt.Printf("%s %s", b.bookName, b.authorName)
+}
+
+func main() {
+	book := Book{"123", "456"}
+	defer book.printName()
+	fmt.Printf("main... ")
+}
+// main... 123 456
package main
+
+import "fmt"
+
+type Book struct {
+	bookName, authorName string
+}
+
+func (b Book) printName() {
+	fmt.Printf("%s %s", b.bookName, b.authorName)
+}
+
+func main() {
+	book := Book{"123", "456"}
+	defer book.printName()
+	fmt.Printf("main... ")
+}
+// main... 123 456

defer 栈

当一个函数内多次调用 defer 时,Go 会把 defer 调用放入到一个栈中,随后按照 后进先出 的顺序执行。

go
package main
+
+import "fmt"
+
+func main() {
+	defer fmt.Printf("123")
+	defer fmt.Printf("456")
+	defer fmt.Printf("789")
+	fmt.Printf("main...")
+}
+//main...789456123
package main
+
+import "fmt"
+
+func main() {
+	defer fmt.Printf("123")
+	defer fmt.Printf("456")
+	defer fmt.Printf("789")
+	fmt.Printf("main...")
+}
+//main...789456123

defer 在 return 后调用

go
package main
+
+import "fmt"
+
+var s string = "123"
+
+func showLesson() string {
+    defer func() {
+        s = "456"
+    }()
+    fmt.Println("showLesson: s =", s)
+    return s
+}
+
+func main() {
+    lesson := showLesson()
+    fmt.Println("main: s =", s)
+    fmt.Println("main: lesson =", lesson)
+}
+//showLesson: s = 123
+//main: s = 456
+//main: lesson = 123
package main
+
+import "fmt"
+
+var s string = "123"
+
+func showLesson() string {
+    defer func() {
+        s = "456"
+    }()
+    fmt.Println("showLesson: s =", s)
+    return s
+}
+
+func main() {
+    lesson := showLesson()
+    fmt.Println("main: s =", s)
+    fmt.Println("main: lesson =", lesson)
+}
+//showLesson: s = 123
+//main: s = 456
+//main: lesson = 123

Go 中 defer 和 return 执行的先后顺序

  1. 多个defer的执行顺序为“后进先出”;
  2. defer、return、返回值三者的执行逻辑应该是:return最先执行,return负责将结果写入返回值中;接着defer开始执行一些收尾工作;最后函数携带当前返回值退出。

如果函数的返回值是无名的(不带命名返回值),则go语言会在执行return的时候会执行一个类似创建一个临时变量作为保存return值的动作,而有名返回值的函数,由于返回值在函数定义的时候已经将该变量进行定义,在执行return的时候会先执行返回值保存操作,而后续的defer函数会改变这个返回值(虽然defer是在return之后执行的,但是由于使用的函数定义的变量,所以执行defer操作后对该变量的修改会影响到return的值

defer 可以使代码更简洁

如果没有使用 defer ,当在一个操作资源的函数里调用多个 return 时,每次都得释放资源,你可能这样写代码:

go
func f() {
+    r := getResource()  //0,获取资源
+    ......
+    if ... {
+        r.release()  //1,释放资源
+        return
+    }
+    ......
+    if ... {
+        r.release()  //2,释放资源
+        return
+    }
+    ......
+    if ... {
+        r.release()  //3,释放资源
+        return
+    }
+    ......
+    r.release()     //4,释放资源
+    return
+}
func f() {
+    r := getResource()  //0,获取资源
+    ......
+    if ... {
+        r.release()  //1,释放资源
+        return
+    }
+    ......
+    if ... {
+        r.release()  //2,释放资源
+        return
+    }
+    ......
+    if ... {
+        r.release()  //3,释放资源
+        return
+    }
+    ......
+    r.release()     //4,释放资源
+    return
+}

有了 defer 之后,可以简洁地写成下面这样:

go
func f() {
+    r := getResource()  //0,获取资源
+
+    defer r.release()  //1,释放资源
+    ......
+    if ... {
+        ...
+        return
+    }
+    ......
+    if ... {
+        ...
+        return
+    }
+    ......
+    if ... {
+        ...
+        return
+    }
+    ......
+    return
+}
func f() {
+    r := getResource()  //0,获取资源
+
+    defer r.release()  //1,释放资源
+    ......
+    if ... {
+        ...
+        return
+    }
+    ......
+    if ... {
+        ...
+        return
+    }
+    ......
+    if ... {
+        ...
+        return
+    }
+    ......
+    return
+}

goto无条件跳转

在 Go 语言中保留 gotogoto 后面接的是标签,表示下一步要执行哪里的代码。

go
package main
+
+import "fmt"
+
+func main() {
+	fmt.Println("123")
+	goto label
+	fmt.Println("456")
+label:
+    fmt.Println("789")
+}
+//123
+//789
package main
+
+import "fmt"
+
+func main() {
+	fmt.Println("123")
+	goto label
+	fmt.Println("456")
+label:
+    fmt.Println("789")
+}
+//123
+//789

指针

一个指针变量指向了一个值的内存地址。

类似于变量和常量,在使用指针前你需要声明指针。指针声明格式如下:

go
var var_name *var-type
var var_name *var-type

var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针。以下是有效的指针声明:

go
var ip *int        /* 指向整型*/
+var fp *float32    /* 指向浮点型 */
var ip *int        /* 指向整型*/
+var fp *float32    /* 指向浮点型 */

操作符

  • & 操作符可以从一个变量中取到其内存地址。
  • 操作符如果在赋值操作值的左边,指该指针指向的变量;* 操作符如果在赋值操作符的右边,指从一个指针变量中取得变量值,又称指针的解引用。

如何使用指针

指针使用流程:

  • 定义指针变量。
  • 为指针变量赋值。
  • 访问指针变量中指向地址的值。
  • 在指针类型前面加上 * 号(前缀)来获取指针所指向的内容。
go
package main
+
+import "fmt"
+
+func main() {
+   var a int= 20   /* 声明实际变量 */
+   var ip *int        /* 声明指针变量 */
+
+   ip = &a  /* 指针变量的存储地址 */
+
+   fmt.Printf("a 变量的地址是: %x\n", &a  )
+
+   /* 指针变量的存储地址 */
+   fmt.Printf("ip 变量储存的指针地址: %x\n", ip )
+
+   /* 使用指针访问值 */
+   fmt.Printf("*ip 变量的值: %d\n", *ip )
+}
+//a 变量的地址是: 20818a220
+//ip 变量储存的指针地址: 20818a220
+//*ip 变量的值: 20
package main
+
+import "fmt"
+
+func main() {
+   var a int= 20   /* 声明实际变量 */
+   var ip *int        /* 声明指针变量 */
+
+   ip = &a  /* 指针变量的存储地址 */
+
+   fmt.Printf("a 变量的地址是: %x\n", &a  )
+
+   /* 指针变量的存储地址 */
+   fmt.Printf("ip 变量储存的指针地址: %x\n", ip )
+
+   /* 使用指针访问值 */
+   fmt.Printf("*ip 变量的值: %d\n", *ip )
+}
+//a 变量的地址是: 20818a220
+//ip 变量储存的指针地址: 20818a220
+//*ip 变量的值: 20

空指针

当一个指针被定义后没有分配到任何变量时,它的值为 nil。

nil 指针也称为空指针。

nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。

一个指针变量通常缩写为 ptr。

go
package main
+
+import "fmt"
+
+func main() {
+  var ptr *int
+
+  fmt.Printf("ptr 的值为 : %x**\n**", ptr  )
+}
+//ptr 的值为 : 0
package main
+
+import "fmt"
+
+func main() {
+  var ptr *int
+
+  fmt.Printf("ptr 的值为 : %x**\n**", ptr  )
+}
+//ptr 的值为 : 0

空指针判断

go
if(ptr != nil)     /* ptr 不是空指针 */
+if(ptr == nil)    /* ptr 是空指针 */
if(ptr != nil)     /* ptr 不是空指针 */
+if(ptr == nil)    /* ptr 是空指针 */

函数传递指针函数

在函数中对指针参数所做的修改,在函数返回后会保存相应的修改。

go
package main
+
+import (
+	"fmt"
+)
+
+func changeByPointer(value *int) {
+	*value = 200
+}
+
+func main() {
+	x3 := 99
+	p3 := &x3
+	fmt.Println("执行changeByPointer函数之前p3是", *p3)
+	changeByPointer(p3)
+	fmt.Println("执行changeByPointer函数之后p3是", *p3)
+}
+//执行changeByPointer函数之前p3是 99
+//执行changeByPointer函数之后p3是 200
package main
+
+import (
+	"fmt"
+)
+
+func changeByPointer(value *int) {
+	*value = 200
+}
+
+func main() {
+	x3 := 99
+	p3 := &x3
+	fmt.Println("执行changeByPointer函数之前p3是", *p3)
+	changeByPointer(p3)
+	fmt.Println("执行changeByPointer函数之后p3是", *p3)
+}
+//执行changeByPointer函数之前p3是 99
+//执行changeByPointer函数之后p3是 200

指针与切片

切片与指针一样是引用类型,如果我们想通过一个函数改变一个数组的值,可以将该数组的切片当作参数传给函数,也可以将这个数组的指针当作参数传给函数。但 Go 中建议使用第一种方法,即将该数组的切片当作参数传给函数,因为这么写更加简洁易读。

go
package main
+
+import "fmt"
+
+// 使用切片
+func changeSlice(value []int) {
+	value[0] = 200
+}
+
+// 使用数组指针
+func changeArray(value *[3]int) {
+	(*value)[0] = 200
+}
+
+func main() {
+	x := [3]int{10, 20, 30}
+	changeSlice(x[:])
+	fmt.Println(x) // [200 20 30]
+
+	y := [3]int{100, 200, 300}
+	changeArray(&y)
+	fmt.Println(y) // [200 200 300]
+}
package main
+
+import "fmt"
+
+// 使用切片
+func changeSlice(value []int) {
+	value[0] = 200
+}
+
+// 使用数组指针
+func changeArray(value *[3]int) {
+	(*value)[0] = 200
+}
+
+func main() {
+	x := [3]int{10, 20, 30}
+	changeSlice(x[:])
+	fmt.Println(x) // [200 20 30]
+
+	y := [3]int{100, 200, 300}
+	changeArray(&y)
+	fmt.Println(y) // [200 200 300]
+}

结构体

Go 语言中数组可以存储同一类型的数据,但在结构体中我们可以为不同项定义不同的数据类型。

结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。Go中没有class的概念,只有struct结构体,所以也没有继承。

声明

go
type struct_name struct {
+    attribute_name1   attribute_type
+    attribute_name2   attribute_type
+    ...
+}
+
+type Lesson struct {
+	name   string //名称
+	target string //学习目标
+	spend  int    //学习花费时间
+}
+//可以把相同类型的属性声明在同一行,这样可以使结构体变得更加紧凑
+type Lesson2 struct {
+    name, target    string
+    spend             int
+}
type struct_name struct {
+    attribute_name1   attribute_type
+    attribute_name2   attribute_type
+    ...
+}
+
+type Lesson struct {
+	name   string //名称
+	target string //学习目标
+	spend  int    //学习花费时间
+}
+//可以把相同类型的属性声明在同一行,这样可以使结构体变得更加紧凑
+type Lesson2 struct {
+    name, target    string
+    spend             int
+}

上面的结构体称为命名结构体Named Structure。声明结构体时也可以不用声明新类型,这种结构体类型称为匿名结构体Anonymous Structure

go
var Lesson3 struct {
+    name, target    string
+    spend             int
+}
var Lesson3 struct {
+    name, target    string
+    spend             int
+}

创建命名结构体

go
package main
+
+import "fmt"
+
+type Lesson struct {
+	name, target    string
+	spend             int
+}
+
+func main() {
+	// 使用字段名创建结构体
+	lesson1 := Lesson{
+		name: "Golang",
+		target: "学习Go语言,并完成一个单体服务",
+		spend:  5,
+	}
+	// 不使用字段名创建结构体,按字段声明顺序初始化
+	lesson2 := Lesson{"Golang", "学习Go语言,并完成一个单体服务", 5}
+
+	fmt.Println("lesson1 ", lesson1)
+	fmt.Println("lesson2 ", lesson2)
+}
package main
+
+import "fmt"
+
+type Lesson struct {
+	name, target    string
+	spend             int
+}
+
+func main() {
+	// 使用字段名创建结构体
+	lesson1 := Lesson{
+		name: "Golang",
+		target: "学习Go语言,并完成一个单体服务",
+		spend:  5,
+	}
+	// 不使用字段名创建结构体,按字段声明顺序初始化
+	lesson2 := Lesson{"Golang", "学习Go语言,并完成一个单体服务", 5}
+
+	fmt.Println("lesson1 ", lesson1)
+	fmt.Println("lesson2 ", lesson2)
+}

结构体标签

Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。

Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:

go
`key1:"value1" key2:"value2"`
`key1:"value1" key2:"value2"`

结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔。 注意事项: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。

例如我们为Student结构体的每个字段定义json序列化时使用的Tag:

go
//Student 学生
+type Student struct {
+    ID     int    `json:"id"` //通过指定tag实现json序列化该字段时的key
+    Gender string //json序列化是默认使用字段名作为key
+    name   string //私有不能被json包访问
+}
+
+func main() {
+    s1 := Student{
+        ID:     1,
+        Gender: "女",
+        name:   "pprof",
+    }
+    data, err := json.Marshal(s1)
+    if err != nil {
+        fmt.Println("json marshal failed!")
+        return
+    }
+    fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"女"}
+}
//Student 学生
+type Student struct {
+    ID     int    `json:"id"` //通过指定tag实现json序列化该字段时的key
+    Gender string //json序列化是默认使用字段名作为key
+    name   string //私有不能被json包访问
+}
+
+func main() {
+    s1 := Student{
+        ID:     1,
+        Gender: "女",
+        name:   "pprof",
+    }
+    data, err := json.Marshal(s1)
+    if err != nil {
+        fmt.Println("json marshal failed!")
+        return
+    }
+    fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"女"}
+}

创建匿名结构体

go
package main
+
+import "fmt"
+
+func main() {
+	// 创建匿名结构体变量
+	lesson3 := struct {
+		name, target string
+		spend          int
+	}{
+		name:   "Go语言",
+		target: "掌握GO语言",
+		spend:   3,
+	}
+
+	fmt.Println("lesson3 ", lesson3)
+}
package main
+
+import "fmt"
+
+func main() {
+	// 创建匿名结构体变量
+	lesson3 := struct {
+		name, target string
+		spend          int
+	}{
+		name:   "Go语言",
+		target: "掌握GO语言",
+		spend:   3,
+	}
+
+	fmt.Println("lesson3 ", lesson3)
+}

结构体的零值(Zero Value)

当定义好的结构体没有被显式初始化时,结构体的字段将会默认赋为相应类型的零值。

go
package main
+
+import "fmt"
+
+type Lesson struct {
+	name, target    string
+	spend             int
+}
+
+func main() {
+	// 不初始化结构体
+	var lesson4 = Lesson{}
+
+	fmt.Println("lesson4 ", lesson4)
+}
+//lesson4  {  0}
package main
+
+import "fmt"
+
+type Lesson struct {
+	name, target    string
+	spend             int
+}
+
+func main() {
+	// 不初始化结构体
+	var lesson4 = Lesson{}
+
+	fmt.Println("lesson4 ", lesson4)
+}
+//lesson4  {  0}

访问结构体字段

使用.点操作符访问:lesson.name

使用.也可用与给结构体字段赋值:lesson.name = "test"

指向结构体的指针

go
package main
+
+import "fmt"
+
+type Lesson struct {
+	name, target    string
+	spend             int
+}
+
+func main() {
+	lesson8 := &Lesson{"Go语言", "Go语言微服务", 50}
+	fmt.Println("lesson8 name: ", (*lesson8).name)
+	fmt.Println("lesson8 name: ", lesson8.name)
+}
package main
+
+import "fmt"
+
+type Lesson struct {
+	name, target    string
+	spend             int
+}
+
+func main() {
+	lesson8 := &Lesson{"Go语言", "Go语言微服务", 50}
+	fmt.Println("lesson8 name: ", (*lesson8).name)
+	fmt.Println("lesson8 name: ", lesson8.name)
+}

lesson8 是一个指向结构体 Lesson 的指针,用 (*lesson8).name 访问 lesson8name 字段, lesson8.name 代替 (*lesson8).name 的解引用访问。

匿名字段

在创建结构体时,字段可以只有类型没有字段名,这种字段称为 匿名字段(Anonymous Field)

go
package main
+
+import "fmt"
+
+type Lesson4 struct {
+	string
+	int
+}
+
+func main() {
+	lesson9 := Lesson4{"Golang", 50}
+	fmt.Println("lesson9 ", lesson9)
+	fmt.Println("lesson9 string: ", lesson9.string)
+	fmt.Println("lesson9 int: ", lesson9.int)
+}
package main
+
+import "fmt"
+
+type Lesson4 struct {
+	string
+	int
+}
+
+func main() {
+	lesson9 := Lesson4{"Golang", 50}
+	fmt.Println("lesson9 ", lesson9)
+	fmt.Println("lesson9 string: ", lesson9.string)
+	fmt.Println("lesson9 int: ", lesson9.int)
+}

上面的程序结构体定义了两个匿名字段,虽然这两个字段没有字段名,但匿名字段的名称默认就是它的类型。所以上面的结构体 Lesoon4 有两个名为 stringint 的字段。

嵌套结构体

结构体的字段也可能是另外一个结构体,这样的结构体称为 嵌套结构体(Nested Structs)

go
package main
+
+import "fmt"
+
+type Author struct {
+	name string
+  	wx string
+}
+
+type Lesson5 struct {
+	name,target string
+	spend int
+	author Author
+}
+
+func main() {
+	lesson10 := Lesson5{
+		name: "Go语言",
+		spend: 50,
+	}
+	lesson10.author = Author{
+		name: "golang",
+		wx: "666",
+	}
+	fmt.Println("lesson10 name:", lesson10.name)
+	fmt.Println("lesson10 spend:", lesson10.spend)
+	fmt.Println("lesson10 author name:", lesson10.author.name)
+	fmt.Println("lesson10 author wx:", lesson10.author.wx)
+}
package main
+
+import "fmt"
+
+type Author struct {
+	name string
+  	wx string
+}
+
+type Lesson5 struct {
+	name,target string
+	spend int
+	author Author
+}
+
+func main() {
+	lesson10 := Lesson5{
+		name: "Go语言",
+		spend: 50,
+	}
+	lesson10.author = Author{
+		name: "golang",
+		wx: "666",
+	}
+	fmt.Println("lesson10 name:", lesson10.name)
+	fmt.Println("lesson10 spend:", lesson10.spend)
+	fmt.Println("lesson10 author name:", lesson10.author.name)
+	fmt.Println("lesson10 author wx:", lesson10.author.wx)
+}

上面的程序 Lesson5 结构体有一个字段 author ,而且它的类型也是一个结构体 Author

提升字段

结构体中如果有匿名的结构体类型字段,则该匿名结构体里的字段就称为 提升字段(Promoted Fields) 。这是因为提升字段就像是属于外部结构体一样,可以用外部结构体直接访问。就像刚刚上面的程序,如果我们把 Lesson 结构体中的字段 author 直接用匿名字段 Author 代替, Author 结构体的字段例如 name 就不用像上面那样使用 lesson10.author.wx 访问,而是使用 lesson10.wx 就能访问 Author 结构体中的 wx 字段。现在结构体 Authornamewx 两个字段,访问字段就像在 Lesson 里直接声明的一样,因此我们称之为提升字段。

go
package main
+
+import "fmt"
+
+type Author struct {
+	name string
+  	wx string
+}
+
+type Lesson6 struct {
+	name,target string
+	spend int
+	Author
+}
+
+func main() {
+	lesson10 := Lesson6{
+		name:   "Go语言",
+		target: "掌握Go语言",
+	}
+	lesson10.Author = Author{
+		name: "golang",
+		wx:   "666",
+	}
+	fmt.Println("lesson10 name:", lesson10.name)
+	fmt.Println("lesson10 target:", lesson10.target)
+	fmt.Println("lesson10 author wx:", lesson10.wx)
+}
+//lesson10 name: Go语言
+//lesson10 target: 掌握Go语言
+//lesson10 author wx: 666
package main
+
+import "fmt"
+
+type Author struct {
+	name string
+  	wx string
+}
+
+type Lesson6 struct {
+	name,target string
+	spend int
+	Author
+}
+
+func main() {
+	lesson10 := Lesson6{
+		name:   "Go语言",
+		target: "掌握Go语言",
+	}
+	lesson10.Author = Author{
+		name: "golang",
+		wx:   "666",
+	}
+	fmt.Println("lesson10 name:", lesson10.name)
+	fmt.Println("lesson10 target:", lesson10.target)
+	fmt.Println("lesson10 author wx:", lesson10.wx)
+}
+//lesson10 name: Go语言
+//lesson10 target: 掌握Go语言
+//lesson10 author wx: 666

结构体比较

如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用 ==!= 运算符进行比较。可以通过==运算符或 DeeplyEqual()函数比较两个结构相同的类型并包含相同的字段值。因此下面两个比较的表达式是等价的:

go
package main
+
+import "fmt"
+
+type  Lesson  struct{
+	name,target string
+	spend int
+}
+
+func main() {
+	lesson11 := Lesson{
+		name:   "Go语言",
+		target: "掌握Go语言",
+	}
+	lesson12 := Lesson{
+		name:   "Go语言",
+		target: "掌握Go语言",
+	}
+	fmt.Println(lesson11.name == lesson12.name && lesson11.target == lesson12.target) // true
+	fmt.Println(lesson11 == lesson12) // true
+}
package main
+
+import "fmt"
+
+type  Lesson  struct{
+	name,target string
+	spend int
+}
+
+func main() {
+	lesson11 := Lesson{
+		name:   "Go语言",
+		target: "掌握Go语言",
+	}
+	lesson12 := Lesson{
+		name:   "Go语言",
+		target: "掌握Go语言",
+	}
+	fmt.Println(lesson11.name == lesson12.name && lesson11.target == lesson12.target) // true
+	fmt.Println(lesson11 == lesson12) // true
+}

给结构体定义方法

在 Go 中无法在结构体内部定义方法

go
package main
+
+import "fmt"
+
+// Lesson 定义一个名为 Lesson 的结构体
+type Lesson struct {
+	name, target string
+	spend        int
+}
+
+// ShowLessonInfo 定义一个与 Lesson 的绑定的方法
+func (l Lesson) ShowLessonInfo() {
+	fmt.Println("name:", l.name)
+	fmt.Println("target:", l.target)
+}
+
+func main() {
+	lesson13 := Lesson{
+		name:   "Go语言",
+		target: "掌握Go语言",
+	}
+	lesson13.ShowLessonInfo()
+}
package main
+
+import "fmt"
+
+// Lesson 定义一个名为 Lesson 的结构体
+type Lesson struct {
+	name, target string
+	spend        int
+}
+
+// ShowLessonInfo 定义一个与 Lesson 的绑定的方法
+func (l Lesson) ShowLessonInfo() {
+	fmt.Println("name:", l.name)
+	fmt.Println("target:", l.target)
+}
+
+func main() {
+	lesson13 := Lesson{
+		name:   "Go语言",
+		target: "掌握Go语言",
+	}
+	lesson13.ShowLessonInfo()
+}

上面的程序中定义了一个与结构体 Lesson 绑定的方法 ShowLessonInfo() ,其中 ShowLessonInfo 是方法名, (l Lesson) 表示将此方法与 Lesson 的实例绑定,这在 Go 语言中称为接收者,而 l 表示实例本身,相当于 Python 中的 self ,在方法内可以使用 实例本身.属性名称 来访问实例属性。

方法的参数传递方式

如果绑定结构体的方法中要改变实例的属性时,必须使用指针作为方法的接收者。

go
package main
+
+import "fmt"
+
+// Lesson 定义一个名为 Lesson 的结构体
+type Lesson struct {
+	name,target string
+	spend int
+}
+
+// ShowLessonInfo 定义一个与 Lesson 的绑定的方法
+func (l Lesson) ShowLessonInfo() {
+	fmt.Println("name:", l.name)
+	fmt.Println("target:", l.target)
+}
+
+// AddTime 定义一个与 Lesson 的绑定的方法,使 spend 值加 n
+func (l *Lesson) AddTime(n int) {
+	l.spend = l.spend + n
+}
+
+func main() {
+	lesson13 := Lesson{
+		name:   "Go语言",
+		target: "掌握Go语言",
+        spend:50,
+	}
+	fmt.Println("添加add方法前")
+	lesson13.ShowLessonInfo()
+	lesson13.AddTime(5)
+	fmt.Println("添加add方法后")
+	lesson13.ShowLessonInfo()
+}
package main
+
+import "fmt"
+
+// Lesson 定义一个名为 Lesson 的结构体
+type Lesson struct {
+	name,target string
+	spend int
+}
+
+// ShowLessonInfo 定义一个与 Lesson 的绑定的方法
+func (l Lesson) ShowLessonInfo() {
+	fmt.Println("name:", l.name)
+	fmt.Println("target:", l.target)
+}
+
+// AddTime 定义一个与 Lesson 的绑定的方法,使 spend 值加 n
+func (l *Lesson) AddTime(n int) {
+	l.spend = l.spend + n
+}
+
+func main() {
+	lesson13 := Lesson{
+		name:   "Go语言",
+		target: "掌握Go语言",
+        spend:50,
+	}
+	fmt.Println("添加add方法前")
+	lesson13.ShowLessonInfo()
+	lesson13.AddTime(5)
+	fmt.Println("添加add方法后")
+	lesson13.ShowLessonInfo()
+}

函数

函数 是基于功能或逻辑进行封装的可复用的代码结构。将一段功能复杂、很长的一段代码封装成多个代码片段(即函数),有助于提高代码可读性和可维护性。由于 Go 语言是编译型语言,所以函数编写的顺序是无关紧要的。

声明

go
func function_name(parameter_list) (result_list) {
+    //函数体
+}
func function_name(parameter_list) (result_list) {
+    //函数体
+}

可变参数

多个类型一致的参数

在参数类型前面加 ... 表示一个切片,用来接收调用者传入的参数。注意,如果该函数下有其他类型的参数,这些其他参数必须放在参数列表的前面,切片必须放在最后。

go
package main
+
+import "fmt"
+
+func show(args ...string) int {
+	sum := 0
+	for _, item := range args {
+        fmt.Println(item)
+		sum += 1
+	}
+	return sum
+}
+
+func main() {
+	fmt.Println(show("1","2","3"))
+}
package main
+
+import "fmt"
+
+func show(args ...string) int {
+	sum := 0
+	for _, item := range args {
+        fmt.Println(item)
+		sum += 1
+	}
+	return sum
+}
+
+func main() {
+	fmt.Println(show("1","2","3"))
+}

多个类型不一致的参数

如果传多个参数的类型都不一样,可以指定类型为 ...interface{} ,然后再遍历。

go
package main
+
+import "fmt"
+
+func PrintType(args ...interface{}) {
+	for _, arg := range args {
+		switch arg.(type) {
+		case int:
+			fmt.Println(arg, "type is int.")
+		case string:
+			fmt.Println(arg, "type is string.")
+		case float64:
+			fmt.Println(arg, "type is float64.")
+		default:
+			fmt.Println(arg, "is an unknown type.")
+		}
+	}
+}
+
+func main() {
+	PrintType(57, 3.14, "123")
+}
package main
+
+import "fmt"
+
+func PrintType(args ...interface{}) {
+	for _, arg := range args {
+		switch arg.(type) {
+		case int:
+			fmt.Println(arg, "type is int.")
+		case string:
+			fmt.Println(arg, "type is string.")
+		case float64:
+			fmt.Println(arg, "type is float64.")
+		default:
+			fmt.Println(arg, "is an unknown type.")
+		}
+	}
+}
+
+func main() {
+	PrintType(57, 3.14, "123")
+}

解序列

使用 ... 可以用来解序列

函数的返回值

当函数没有返回值时,函数体可以使用 return 语句返回。在 Go 中一个函数可以返回多个值。

go
package main
+
+import "fmt"
+
+func showBookInfo(bookName, authorName string) (string, error) {
+	if bookName == "" {
+		return "", errors.New("图书名称为空")
+	}
+	if authorName == "" {
+		return "", errors.New("作者名称为空")
+	}
+	return bookName + ",作者:" + authorName, nil
+}
+
+func main() {
+	bookInfo, err := showBookInfo("123", "45")
+	fmt.Printf("bookInfo = %s, err = %v", bookInfo, err)
+}
package main
+
+import "fmt"
+
+func showBookInfo(bookName, authorName string) (string, error) {
+	if bookName == "" {
+		return "", errors.New("图书名称为空")
+	}
+	if authorName == "" {
+		return "", errors.New("作者名称为空")
+	}
+	return bookName + ",作者:" + authorName, nil
+}
+
+func main() {
+	bookInfo, err := showBookInfo("123", "45")
+	fmt.Printf("bookInfo = %s, err = %v", bookInfo, err)
+}

返回带有变量名的值

go
func showBookInfo2(bookName, authorName string) (info string, err error) {
+	info = ""
+	if bookName == "" {
+		err = errors.New("图书名称为空")
+		return
+	}
+	if authorName == "" {
+		err = errors.New("作者名称为空")
+		return
+	}
+    // 不使用 := 因为已经在返回值那里声明了
+	info = bookName + ",作者:" + authorName
+  	// 直接返回即可
+	return
+}
func showBookInfo2(bookName, authorName string) (info string, err error) {
+	info = ""
+	if bookName == "" {
+		err = errors.New("图书名称为空")
+		return
+	}
+	if authorName == "" {
+		err = errors.New("作者名称为空")
+		return
+	}
+    // 不使用 := 因为已经在返回值那里声明了
+	info = bookName + ",作者:" + authorName
+  	// 直接返回即可
+	return
+}

匿名函数

go
func (parameter_list) (result_list) {
+	body
+}
func (parameter_list) (result_list) {
+	body
+}

内部方法与外部方法

在 Go 语言中,函数名通过首字母大小写实现控制对方法的访问权限。

  • 当方法的首字母为 大写 时,这个方法对于 所有包 都是 Public ,其他包可以随意调用。
  • 当方法的首字母为 小写 时,这个方法是 Private ,其他包是无法访问的。

方法

方法 其实就是一个函数,在 func 这个关键字和方法名中间加入了一个特殊的接收器类型。接收器可以是结构体类型或者是非结构体类型。接收器是可以在方法的内部访问的。

go
func (t Type) methodName(parameterList) returnList{
+}
func (t Type) methodName(parameterList) returnList{
+}

实例绑定

go
package main
+
+import "fmt"
+
+// Lesson 定义一个名为 Lesson 的结构体
+type Lesson struct {
+    Name   string
+    Target string
+}
+
+// PrintInfo 定义一个与 Lesson 的绑定的方法
+func (lesson Lesson) PrintInfo() {
+    fmt.Println("name:", lesson.Name)
+    fmt.Println("target:", lesson.Target)
+}
+
+
+func main() {
+    l := Lesson{
+        Name: "Go语言",
+        Target: "掌握Go语言",
+    }
+    l.PrintInfo()
+}
package main
+
+import "fmt"
+
+// Lesson 定义一个名为 Lesson 的结构体
+type Lesson struct {
+    Name   string
+    Target string
+}
+
+// PrintInfo 定义一个与 Lesson 的绑定的方法
+func (lesson Lesson) PrintInfo() {
+    fmt.Println("name:", lesson.Name)
+    fmt.Println("target:", lesson.Target)
+}
+
+
+func main() {
+    l := Lesson{
+        Name: "Go语言",
+        Target: "掌握Go语言",
+    }
+    l.PrintInfo()
+}

也可以把上面程序的方法改成一个函数

go
package main
+
+import "fmt"
+
+type Lesson struct {
+    Name   string
+    Target string
+}
+
+func PrintInfo(lesson Lesson) {
+    fmt.Println("name:", lesson.Name)
+    fmt.Println("target:", lesson.Target)
+}
+
+func main() {
+    lesson := Lesson{
+        Name: "Go语言",
+        Target: "掌握Go语言",
+    }
+    PrintInfo(lesson)
+}
package main
+
+import "fmt"
+
+type Lesson struct {
+    Name   string
+    Target string
+}
+
+func PrintInfo(lesson Lesson) {
+    fmt.Println("name:", lesson.Name)
+    fmt.Println("target:", lesson.Target)
+}
+
+func main() {
+    lesson := Lesson{
+        Name: "Go语言",
+        Target: "掌握Go语言",
+    }
+    PrintInfo(lesson)
+}

运行这个程序,也同样会输出上面一样的答案,那么我们为什么还要用方法呢?因为在 Go 中,相同的名字的方法可以定义在不同的类型上,而相同名字的函数是不被允许的。如果你在上面这个程序添加一个同名函数,就会报错。但是在不同的结构体上面定义同名的方法就是可行的。

go
package main
+
+import "fmt"
+
+type Lesson struct {
+    Name   string
+    Target string
+}
+
+func (lesson Lesson) PrintInfo() {
+    fmt.Println("Lesson name:", lesson.Name)
+    fmt.Println("Lesson target:", lesson.Target)
+}
+
+type Author struct {
+    Name string
+}
+
+func (author Author) PrintInfo() {
+    fmt.Println("author name:", author.Name)
+}
+
+func main() {
+    lesson := Lesson{
+        Name: "Go语言",
+        Target: "掌握Go语言",
+    }
+    lesson.PrintInfo()
+    author := Author{"Google"}
+    author.PrintInfo()
+}
package main
+
+import "fmt"
+
+type Lesson struct {
+    Name   string
+    Target string
+}
+
+func (lesson Lesson) PrintInfo() {
+    fmt.Println("Lesson name:", lesson.Name)
+    fmt.Println("Lesson target:", lesson.Target)
+}
+
+type Author struct {
+    Name string
+}
+
+func (author Author) PrintInfo() {
+    fmt.Println("author name:", author.Name)
+}
+
+func main() {
+    lesson := Lesson{
+        Name: "Go语言",
+        Target: "掌握Go语言",
+    }
+    lesson.PrintInfo()
+    author := Author{"Google"}
+    author.PrintInfo()
+}

指针接收器与值接收器

值接收器和指针接收器之间的区别在于,在指针接收器的方法内部的改变对于调用者是可见的,然而值接收器的方法内部的改变对于调用者是不可见的,所以若要改变实例的属性时,必须使用指针作为方法的接收者。

go
package main
+
+import "fmt"
+
+// Lesson 定义一个名为 Lesson 的结构体
+type Lesson struct {
+	Name      string
+	Target    string
+	SpendTime int
+}
+
+// PrintInfo 定义一个与 Lesson 的绑定的方法
+func (lesson Lesson) PrintInfo() {
+	fmt.Println("name:", lesson.Name)
+	fmt.Println("target:", lesson.Target)
+	fmt.Println("spendTime:", lesson.SpendTime)
+}
+
+func (lesson Lesson) ChangeLessonName(name string) {
+	lesson.Name = name
+}
+
+func (lesson *Lesson) AddSpendTime(n int) {
+	lesson.SpendTime = lesson.SpendTime + n
+}
+
+func main() {
+	lesson := Lesson{
+		Name:      "Go语言",
+		Target:    "掌握Go语言",
+		SpendTime: 1,
+	}
+	fmt.Println("before change")
+	lesson.PrintInfo()
+
+	fmt.Println("after change")
+	lesson.AddSpendTime(2)
+	lesson.ChangeLessonName("Go语言123")
+	lesson.PrintInfo()
+}
package main
+
+import "fmt"
+
+// Lesson 定义一个名为 Lesson 的结构体
+type Lesson struct {
+	Name      string
+	Target    string
+	SpendTime int
+}
+
+// PrintInfo 定义一个与 Lesson 的绑定的方法
+func (lesson Lesson) PrintInfo() {
+	fmt.Println("name:", lesson.Name)
+	fmt.Println("target:", lesson.Target)
+	fmt.Println("spendTime:", lesson.SpendTime)
+}
+
+func (lesson Lesson) ChangeLessonName(name string) {
+	lesson.Name = name
+}
+
+func (lesson *Lesson) AddSpendTime(n int) {
+	lesson.SpendTime = lesson.SpendTime + n
+}
+
+func main() {
+	lesson := Lesson{
+		Name:      "Go语言",
+		Target:    "掌握Go语言",
+		SpendTime: 1,
+	}
+	fmt.Println("before change")
+	lesson.PrintInfo()
+
+	fmt.Println("after change")
+	lesson.AddSpendTime(2)
+	lesson.ChangeLessonName("Go语言123")
+	lesson.PrintInfo()
+}

在上面的程序中, AddSpendTime 使用指针接收器最终能改变实例的 SpendTime 值,然而使用值接收器的 ChangeLessonName 最终没有改变实例 Name 的值。

在方法中使用值接收器 与 在函数中使用值参数

当一个函数有一个值参数,它只能接受一个值参数。当一个方法有一个值接收器,它可以接受值接收器和指针接收器。

go
package main
+
+import "fmt"
+
+type Lesson struct {
+	Name string
+}
+
+func (lesson Lesson) PrintInfo() {
+	fmt.Println(lesson.Name)
+}
+
+func PrintInfo(lesson Lesson) {
+	fmt.Println(lesson.Name)
+}
+
+func main() {
+	lesson := Lesson{"Go语言"}
+	PrintInfo(lesson)
+	lesson.PrintInfo()
+
+	bPtr := &lesson
+	//PrintInfo(bPtr) // error
+	bPtr.PrintInfo()
+}
package main
+
+import "fmt"
+
+type Lesson struct {
+	Name string
+}
+
+func (lesson Lesson) PrintInfo() {
+	fmt.Println(lesson.Name)
+}
+
+func PrintInfo(lesson Lesson) {
+	fmt.Println(lesson.Name)
+}
+
+func main() {
+	lesson := Lesson{"Go语言"}
+	PrintInfo(lesson)
+	lesson.PrintInfo()
+
+	bPtr := &lesson
+	//PrintInfo(bPtr) // error
+	bPtr.PrintInfo()
+}

在上面的程序中,使用值参数 PrintInfo(lesson) 来调用这个函数是合法的,使用值接收器来调用 lesson.PrintInfo() 也是合法的。

然后在程序中我们创建了一个指向 Lesson 的指针 bPtr ,通过使用指针接收器来调用 bPtr.PrintInfo() 是合法的,但使用值参数调用 PrintInfo(bPtr) 是非法的。

在非结构体上的方法

go
package main
+
+import "fmt"
+
+type myInt int
+
+func (a myInt) add(b myInt) myInt {
+    return a + b
+}
+
+func main() {
+    var x myInt = 50
+    var y myInt = 7
+    fmt.Println(x.add(y))   // 57
+}
package main
+
+import "fmt"
+
+type myInt int
+
+func (a myInt) add(b myInt) myInt {
+    return a + b
+}
+
+func main() {
+    var x myInt = 50
+    var y myInt = 7
+    fmt.Println(x.add(y))   // 57
+}

接口

在 Go 语言中, 接口 就是方法签名(Method Signature)的集合。在面向对象的领域里,接口定义一个对象的行为,接口只指定了对象应该做什么,至于如何实现这个行为,则由对象本身去确定。当一个类型实现了接口中的所有方法,我们称它实现了该接口。接口指定了一个类型应该具有的方法,并由该类型决定如何实现这些方法。

定义

go
type interface_name interface {
+    method()
+}
type interface_name interface {
+    method()
+}

接口实现

go
package main
+
+import "fmt"
+
+type Study interface {
+    learn()
+}
+
+type Student struct {
+    name string
+    book string
+}
+
+func (s Student) learn() {
+    fmt.Printf("%s 在读 %s", s.name, s.book)
+}
+
+func main() {
+    student1 := Student{
+        name: "张三",
+        book: "《Go语言》",
+    }
+    student1.learn()
+}
package main
+
+import "fmt"
+
+type Study interface {
+    learn()
+}
+
+type Student struct {
+    name string
+    book string
+}
+
+func (s Student) learn() {
+    fmt.Printf("%s 在读 %s", s.name, s.book)
+}
+
+func main() {
+    student1 := Student{
+        name: "张三",
+        book: "《Go语言》",
+    }
+    student1.learn()
+}

上面的程序定义了一个名为 Study 的接口,接口中有未实现的方法 learn() ,这里还定义了名为 Student 的结构体,其绑定了方法 learn() ,也就隐式实现了 Study 接口,实现的内容是打印语句。

接口实现多态

go
package main
+
+import "fmt"
+
+type Study interface {
+	learn()
+}
+type Student struct {
+	name, book string
+}
+
+func (s Student) learn() {
+	fmt.Printf("%s 在读 %s", s.name, s.book)
+}
+
+type Worker struct {
+	name string
+	book string
+	by   string
+}
+
+func (w *Worker) learn() {
+	fmt.Printf("%s 在读 %s,通过方式 %s", w.name, w.book, w.by)
+}
+
+func main() {
+	var s1 Study
+	var s2 Study
+
+	student2 := Student{
+		name: "李四",
+		book: "《Go语言》",
+	}
+	s1 = student2
+	s1.learn()
+
+	student3 := Student{
+		name: "王五",
+		book: "Go语言1",
+	}
+	s1 = &student3
+	s1.learn()
+
+	worker1 := Worker{
+		name: "老王",
+		book: "Go语言2",
+		by:   "视频",
+	}
+	// s2 = worker1 // error
+	s2 = &worker1
+	s2.learn()
+}
package main
+
+import "fmt"
+
+type Study interface {
+	learn()
+}
+type Student struct {
+	name, book string
+}
+
+func (s Student) learn() {
+	fmt.Printf("%s 在读 %s", s.name, s.book)
+}
+
+type Worker struct {
+	name string
+	book string
+	by   string
+}
+
+func (w *Worker) learn() {
+	fmt.Printf("%s 在读 %s,通过方式 %s", w.name, w.book, w.by)
+}
+
+func main() {
+	var s1 Study
+	var s2 Study
+
+	student2 := Student{
+		name: "李四",
+		book: "《Go语言》",
+	}
+	s1 = student2
+	s1.learn()
+
+	student3 := Student{
+		name: "王五",
+		book: "Go语言1",
+	}
+	s1 = &student3
+	s1.learn()
+
+	worker1 := Worker{
+		name: "老王",
+		book: "Go语言2",
+		by:   "视频",
+	}
+	// s2 = worker1 // error
+	s2 = &worker1
+	s2.learn()
+}

接口的内部表示

可以把接口的内部看做 (type, value)type 是接口底层的具体类型(Concrete Type),而 value 是具体类型的值。

go
package main
+
+import "fmt"
+
+type Study interface {
+	learn()
+}
+type Student struct {
+	name, book string
+}
+
+func (s Student) learn() {
+	fmt.Printf("%s 在读 %s", s.name, s.book)
+}
+func ShowInterface(s Study) {
+	fmt.Printf("接口类型: %T\n 接口值: %v\n", s, s)
+}
+
+func main() {
+	var s Study
+	student2 := Student{
+		name: "李四",
+		book: "《Go语言》",
+	}
+	s = student2
+	ShowInterface(s)
+	s.learn()
+}
+//接口类型: main.Student
+//接口值: {李四 《Go语言》}
+//李四 在读 《Go语言》
package main
+
+import "fmt"
+
+type Study interface {
+	learn()
+}
+type Student struct {
+	name, book string
+}
+
+func (s Student) learn() {
+	fmt.Printf("%s 在读 %s", s.name, s.book)
+}
+func ShowInterface(s Study) {
+	fmt.Printf("接口类型: %T\n 接口值: %v\n", s, s)
+}
+
+func main() {
+	var s Study
+	student2 := Student{
+		name: "李四",
+		book: "《Go语言》",
+	}
+	s = student2
+	ShowInterface(s)
+	s.learn()
+}
+//接口类型: main.Student
+//接口值: {李四 《Go语言》}
+//李四 在读 《Go语言》

空接口

空接口 是特殊形式的接口类型,没有定义任何方法的接口就称为空接口,可以说所有类型都至少实现了空接口,空接口表示为 interface{} 。例如,我们之前的写过的空接口参数函数,可以接受任何类型的参数:

go
package main
+
+import "fmt"
+
+func ShowType(i interface{}) {
+    fmt.Printf("类型: %T, 值: %v\n", i, i)
+}
+
+func main() {
+    str := "Go语言"
+    ShowType(str)
+    num := 3.14
+    ShowType(num)
+}
package main
+
+import "fmt"
+
+func ShowType(i interface{}) {
+    fmt.Printf("类型: %T, 值: %v\n", i, i)
+}
+
+func main() {
+    str := "Go语言"
+    ShowType(str)
+    num := 3.14
+    ShowType(num)
+}

通过上面的例子不难发现接口都有两个属性,一个是值,而另一个是类型。对于空接口来说,这两个属性都为 nil

go
package main
+
+import "fmt"
+
+func main() {
+    var i interface{}
+    fmt.Printf("Type: %T, Value: %v", i, i)
+    // Type: <nil>, Value: <nil>
+}
package main
+
+import "fmt"
+
+func main() {
+    var i interface{}
+    fmt.Printf("Type: %T, Value: %v", i, i)
+    // Type: <nil>, Value: <nil>
+}

除了上面讲到的使用空接口作为函数参数的用法,空接口还有以下两种用法。

直接使用 interface{} 作为类型声明一个实例,这个实例就能承载任何类型的值:

go
package main
+
+import "fmt"
+
+func main() {
+    var i interface{}
+
+    i = "Go语言"
+    fmt.Println(i) // Let's go
+
+    i = 3.14
+    fmt.Println(i) // 3.14
+}
package main
+
+import "fmt"
+
+func main() {
+    var i interface{}
+
+    i = "Go语言"
+    fmt.Println(i) // Let's go
+
+    i = 3.14
+    fmt.Println(i) // 3.14
+}

我们也可以定义一个接收任何类型的 arrayslicemapstrcut 。例如:

go
package main
+
+import "fmt"
+
+func main() {
+    x := make([]interface{}, 3)
+    x[0] = "Go"
+    x[1] = 3.14
+    x[2] = []int{1, 2, 3}
+    for _, value := range x {
+        fmt.Println(value)
+    }
+}
package main
+
+import "fmt"
+
+func main() {
+    x := make([]interface{}, 3)
+    x[0] = "Go"
+    x[1] = 3.14
+    x[2] = []int{1, 2, 3}
+    for _, value := range x {
+        fmt.Println(value)
+    }
+}

空接口可以承载任何值,但是空接口类型的对象是不能赋值给另一个固定类型对象的。

go
package main
+
+func main() {
+    var num = 1
+    var i interface{} = num
+    var str string = i // error
+}
package main
+
+func main() {
+    var num = 1
+    var i interface{} = num
+    var str string = i // error
+}

当空接口承载数组和切片后,该对象无法再进行切片。

go
package main
+
+import "fmt"
+
+func main() {
+    var s = []int{1, 2, 3}
+
+    var i interface{} = s
+
+    var s2 = i[1:2] // error
+    fmt.Println(s2)
+}
package main
+
+import "fmt"
+
+func main() {
+    var s = []int{1, 2, 3}
+
+    var i interface{} = s
+
+    var s2 = i[1:2] // error
+    fmt.Println(s2)
+}

类型断言

类型断言用于提取接口的底层值(Underlying Value)。使用 interface.(Type) 可以获取接口的底层值,其中接口 interface 的具体类型是 Type

go
package main
+
+import "fmt"
+
+func assert(i interface{}) {
+    value, ok := i.(int)
+    fmt.Println(value, ok)
+}
+
+func main() {
+    var x interface{} = 3
+    assert(x)
+    var y interface{} = "Go语言"
+    assert(y)
+}
package main
+
+import "fmt"
+
+func assert(i interface{}) {
+    value, ok := i.(int)
+    fmt.Println(value, ok)
+}
+
+func main() {
+    var x interface{} = 3
+    assert(x)
+    var y interface{} = "Go语言"
+    assert(y)
+}

第一次调用 assert(x) 输出 3 true,表示将整数 3 转换为 int 类型成功。

第二次调用 assert(y) 输出 0 false,表示将字符串 "Go语言" 转换为 int 类型失败,因为该字符串无法转换为整数。

类型选择

go
package main
+
+import "fmt"
+
+func getTypeValue(i interface{}) {
+    switch i.(type) {
+    case int:
+        fmt.Printf("Type: int, Value: %d\n", i.(int))
+    case string:
+        fmt.Printf("Type: string, Value: %s\n", i.(string))
+    default:
+        fmt.Printf("Unknown type\n")
+    }
+}
+
+func main() {
+    getTypeValue(300)
+    getTypeValue("Go语言")
+    getTypeValue(true)
+}
package main
+
+import "fmt"
+
+func getTypeValue(i interface{}) {
+    switch i.(type) {
+    case int:
+        fmt.Printf("Type: int, Value: %d\n", i.(int))
+    case string:
+        fmt.Printf("Type: string, Value: %s\n", i.(string))
+    default:
+        fmt.Printf("Unknown type\n")
+    }
+}
+
+func main() {
+    getTypeValue(300)
+    getTypeValue("Go语言")
+    getTypeValue(true)
+}

实现多个接口

类型或者结构体可以实现多个接口

接口的嵌套

虽然在 Go 中没有继承机制,但可以通过接口的嵌套实现类似功能。

go
package main
+
+import "fmt"
+
+// 定义一个简单的读取器接口
+type Reader interface {
+    Read() string
+}
+
+// 定义一个简单的写入器接口
+type Writer interface {
+    Write(data string)
+}
+
+// 定义一个复合接口,嵌套了Reader和Writer接口
+type ReadWriter interface {
+    Reader
+    Writer
+}
+
+// 实现Reader接口
+type MyReader struct{}
+
+func (r MyReader) Read() string {
+    return "Data read from MyReader"
+}
+
+// 实现Writer接口
+type MyWriter struct{}
+
+func (w MyWriter) Write(data string) {
+    fmt.Println("Writing data:", data)
+}
+
+// 实现ReadWriter接口
+type MyReadWriter struct {
+    MyReader
+    MyWriter
+}
+
+// 使用ReadWriter接口作为参数进行函数调用
+func ProcessData(rw ReadWriter) {
+    data := rw.Read()
+    rw.Write(data + " modified")
+}
+
+func main() {
+    // 创建MyReadWriter实例
+    myRW := MyReadWriter{}
+    
+    // 调用ProcessData函数,传入myRW作为参数
+    ProcessData(myRW)
+}
package main
+
+import "fmt"
+
+// 定义一个简单的读取器接口
+type Reader interface {
+    Read() string
+}
+
+// 定义一个简单的写入器接口
+type Writer interface {
+    Write(data string)
+}
+
+// 定义一个复合接口,嵌套了Reader和Writer接口
+type ReadWriter interface {
+    Reader
+    Writer
+}
+
+// 实现Reader接口
+type MyReader struct{}
+
+func (r MyReader) Read() string {
+    return "Data read from MyReader"
+}
+
+// 实现Writer接口
+type MyWriter struct{}
+
+func (w MyWriter) Write(data string) {
+    fmt.Println("Writing data:", data)
+}
+
+// 实现ReadWriter接口
+type MyReadWriter struct {
+    MyReader
+    MyWriter
+}
+
+// 使用ReadWriter接口作为参数进行函数调用
+func ProcessData(rw ReadWriter) {
+    data := rw.Read()
+    rw.Write(data + " modified")
+}
+
+func main() {
+    // 创建MyReadWriter实例
+    myRW := MyReadWriter{}
+    
+    // 调用ProcessData函数,传入myRW作为参数
+    ProcessData(myRW)
+}

定义了三个接口:ReaderWriterReadWriter。然后,我们实现了这些接口的具体类型:MyReaderMyWriterMyReadWriter

MyReadWriter结构体通过嵌套MyReaderMyWriter,同时实现了ReaderWriter接口。这样,MyReadWriter可以以ReadWriter类型的方式使用。

main函数中,我们创建了一个MyReadWriter实例myRW,然后将其作为参数传递给ProcessData函数。ProcessData函数接收一个ReadWriter类型的参数,并调用其中的方法。

通过接口嵌套,我们可以更灵活地组织和复用代码

包(package) 用于组织 Go 源代码,提供了更好的可重用性与可读性.可以用 go list std命令查看标准包,标准库为大多数的程序提供了必要的基础组件。

创建包

先创建一个 book 文件夹,位于该目录下创建一个 book.go 源文件,里面实现自定义的数学加法函数。函数名的首字母要大写。

go
// Package book
+package book
+
+func ShowBookInfo(bookName, authorName string) (string, error) {
+  if bookName == "" {
+    return "", errors.New("图书名称为空")
+  }
+  if authorName == "" {
+    return "", errors.New("作者名称为空")
+  }
+  return bookName + ",作者:" + authorName, nil
+}
// Package book
+package book
+
+func ShowBookInfo(bookName, authorName string) (string, error) {
+  if bookName == "" {
+    return "", errors.New("图书名称为空")
+  }
+  if authorName == "" {
+    return "", errors.New("作者名称为空")
+  }
+  return bookName + ",作者:" + authorName, nil
+}

导入包

使用包之前我们需要导入包,在 GoLand 中会帮你自动导入所需要的包。导入包的语法为 import path ,其中 path 可以是相对于工作区文件夹的相对路径,也可以是绝对路径。

go
package main
+
+import (
+	"fmt"
+	"learn/book"
+)
+
+func main() {
+	bookName := "《Go语言》"
+	author := "Golang"
+	bookInfo, _ := book.ShowBookInfo(bookName, author)
+	fmt.Println("bookInfo = ", bookInfo)
+}
package main
+
+import (
+	"fmt"
+	"learn/book"
+)
+
+func main() {
+	bookName := "《Go语言》"
+	author := "Golang"
+	bookInfo, _ := book.ShowBookInfo(bookName, author)
+	fmt.Println("bookInfo = ", bookInfo)
+}

使用别名

go
import (
+    "crypto/rand"
+    mrand "math/rand" // 将名称替换为 mrand 避免冲突
+)
import (
+    "crypto/rand"
+    mrand "math/rand" // 将名称替换为 mrand 避免冲突
+)

使用点操作

go
import . "fmt"
+
+func main() {
+    Println("hello, world")
+}
import . "fmt"
+
+func main() {
+    Println("hello, world")
+}

对于一些使用高频的包,例如 fmt 包,每次调用打印函数时都要使用 fmt.Println() 进行调用,很不方便。可以在导入包的时,使用 import . package_path 语法。打印就不用加 fmt 了。

包的初始化

每个包都允许有一个或多个 init 函数, init 函数不应该有任何返回值类型和参数,在代码中也不能显式调用它,当这个包被导入时,就会执行这个包的 init 函数,做初始化任务, init 函数优先于 main 函数执行。该函数形式如下:

go
func init() {
+}
func init() {
+}

包的初始化顺序:首先初始化 包级别(Package Level) 的变量,紧接着调用 init 函数。包可以有多个 init 函数(在一个文件或分布于多个文件中),它们按照编译器解析它们的顺序进行调用。如果一个包导入了另一个包,会先初始化被导入的包。尽管一个包可能会被导入多次,但是它只会被初始化一次。

包的匿名导入

导入一个没有使用的包编译会报错。但有时候我们只是想执行包里的 init 函数来执行一些初始化任务,可以使用匿名导入的方法,使用 空白标识符(Blank Identifier)

go
import _ "fmt"
import _ "fmt"

协程

Go 语言的 协程(Groutine) 是与其他函数或方法一起并发运行的工作方式。协程可以看作是轻量级线程。与线程相比,创建一个协程的成本很小。因此在 Go 应用中,常常会看到会有很多协程并发地运行。

启动一个 go 协程

调用函数或者方法时,如果在前面加上关键字 go ,就可以让一个新的 Go 协程并发地运行。

go
// 定义一个函数
+func functionName(parameterList) {
+    code
+}
+
+// 执行一个函数
+functionName(parameterList)
+
+// 开启一个协程执行这个函数
+go functionName(parameterList)
// 定义一个函数
+func functionName(parameterList) {
+    code
+}
+
+// 执行一个函数
+functionName(parameterList)
+
+// 开启一个协程执行这个函数
+go functionName(parameterList)
go
package main
+
+import (
+ "fmt"
+ "time"
+)
+
+func PrintInfo() {
+ fmt.Println("Go语言")
+}
+
+func main() {
+ // 开启一个协程执行 PrintInfo 函数
+ go PrintInfo()
+ // 使主协程休眠 1 秒
+ time.Sleep(1 * time.Second)
+ // 打印 main
+ fmt.Println("main")
+}
package main
+
+import (
+ "fmt"
+ "time"
+)
+
+func PrintInfo() {
+ fmt.Println("Go语言")
+}
+
+func main() {
+ // 开启一个协程执行 PrintInfo 函数
+ go PrintInfo()
+ // 使主协程休眠 1 秒
+ time.Sleep(1 * time.Second)
+ // 打印 main
+ fmt.Println("main")
+}

PrintInfo() 函数与 main() 函数会并发执行,主函数运行在一个特殊的协程上,这个协程称之为 主协程(Main Goroutine)

启动一个新的协程时,协程的调用会立即返回。与函数不同,程序控制不会去等待 Go 协程执行完毕。在调用 Go 协程之后,程序控制会立即返回到代码的下一行,忽略该协程的任何返回值。如果 Go 主协程终止,则程序终止,于是其他 Go 协程也会终止。为了让新的协程能继续运行,在 main() 函数添加了 time.Sleep(1 * time.Second) 使主协程休眠 1 秒

启动多个 Go 协程

go
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func PrintNum(num int) {
+	for i := 0; i < 3; i++ {
+		fmt.Println(num)
+		// 避免观察不到并发效果 加个休眠
+		time.Sleep(100 * time.Millisecond)
+	}
+}
+
+func main() {
+	// 开启 1 号协程
+	go PrintNum(1)
+	// 开启 2 号协程
+	go PrintNum(2)
+	// 使主协程休眠 1 秒
+	time.Sleep(time.Second)
+}
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func PrintNum(num int) {
+	for i := 0; i < 3; i++ {
+		fmt.Println(num)
+		// 避免观察不到并发效果 加个休眠
+		time.Sleep(100 * time.Millisecond)
+	}
+}
+
+func main() {
+	// 开启 1 号协程
+	go PrintNum(1)
+	// 开启 2 号协程
+	go PrintNum(2)
+	// 使主协程休眠 1 秒
+	time.Sleep(time.Second)
+}

通道

通道(channel) ,就是一个管道,可以想像成 Go 协程之间通信的管道。它是一种队列式的数据结构,遵循先入先出的规则。

通道的声明

每个通道都只能传递一种数据类型的数据,在你声明的时候,我们要指定通道的类型。chan Type 表示 Type 类型的通道。通道的零值为 nil

go
var channel_name chan channel_types
+
+var ch chan string
var channel_name chan channel_types
+
+var ch chan string

通道的初始化

声明完通道后,通道的值为 nil ,我们不能直接使用,必须先使用 make 函数对通道进行初始化操作。

go
ch = make(chan channel_type)
+
+ch = make(chan string)
ch = make(chan channel_type)
+
+ch = make(chan string)

这样,我们就已经定义好了一个 string 类型的通道 nameChan 。当然,也可以使用简短声明语句一次性定义一个通道:

go
ch := make(chan string)
ch := make(chan string)

使用通道发送和接收数据

发送数据:

go
// 把 data 数据发送到 channel_name 通道中
+// 即把 data 数据写入到 channel_name 通道中
+channel_name <- data
// 把 data 数据发送到 channel_name 通道中
+// 即把 data 数据写入到 channel_name 通道中
+channel_name <- data

接收数据:

go
// 从 channel_name 通道中接收数据到 value
+// 即从 channel_name 通道中读取数据到 value
+value := <- channel_name
// 从 channel_name 通道中接收数据到 value
+// 即从 channel_name 通道中读取数据到 value
+value := <- channel_name

通道旁的箭头方向指定了是发送数据还是接收数据。箭头指向通道,代表数据写入到通道中;箭头往通道指向外,代表从通道读数据出去。

go
package main
+
+import (
+	"fmt"
+)
+
+func PrintChan(c chan string) {
+	// 往通道传入数据 
+	c <- "学习Go语言"
+}
+
+func main() {
+	// 创建一个通道
+	ch := make(chan string)
+	// 打印 "学习课程:"
+	fmt.Println("学习课程:")
+	// 开启协程
+	go PrintChan(ch)
+	// 从通道接收数据
+	rec := <- ch
+	// 打印从通道接收到的数据
+	fmt.Println(rec)
+}
package main
+
+import (
+	"fmt"
+)
+
+func PrintChan(c chan string) {
+	// 往通道传入数据 
+	c <- "学习Go语言"
+}
+
+func main() {
+	// 创建一个通道
+	ch := make(chan string)
+	// 打印 "学习课程:"
+	fmt.Println("学习课程:")
+	// 开启协程
+	go PrintChan(ch)
+	// 从通道接收数据
+	rec := <- ch
+	// 打印从通道接收到的数据
+	fmt.Println(rec)
+}

Tips: 发送与接收默认是阻塞的

  • 从上面的例子我们知道,如果从通道接收数据没接收完主协程是不会继续执行下去的。当把数据发送到通道时,会在发送数据的语句处发生阻塞,直到有其它协程从通道读取到数据,才会解除阻塞。与此类似,当读取通道的数据时,如果没有其它的协程把数据写入到这个通道,那么读取过程就会一直阻塞着。

通道的关闭

go
close(channel_name)
close(channel_name)

这里要注意,对于一个已经关闭的通道如果再次关闭会导致报错,我们可以在接收数据时,判断通道是否已经关闭,从通道读取数据返回的第二个值表示通道是否没被关闭,如果已经关闭,返回值为 false ;如果还未关闭,返回值为 true

go
value, ok := <- channel_name
value, ok := <- channel_name

通道的容量与长度

make 函数是可以接收两个参数的,同理,创建通道可以传入第二个参数——容量。

  • 当容量为 0 时,说明通道中不能存放数据,在发送数据时,必须要求立马有人接收,否则会报错。此时的通道称之为无缓冲通道。
  • 当容量为 1 时,说明通道只能缓存一个数据,若通道中已有一个数据,此时再往里发送数据,会造成程序阻塞。利用这点可以利用通道来做锁。
  • 当容量大于 1 时,通道中可以存放多个数据,可以用于多个协程之间的通信管道,共享资源。

既然通道有容量和长度,那么我们可以通过 cap 函数和 len 函数获取通道的容量和长度。

go
package main
+
+import (
+	"fmt"
+)
+
+func main() {
+	// 创建一个通道
+	c := make(chan int, 3)
+	fmt.Println("初始化后:")
+	fmt.Println("cap =", cap(c))
+	fmt.Println("len =", len(c))
+	c <- 1
+	c <- 2
+	fmt.Println("传入两个数后:")
+	fmt.Println("cap =", cap(c))
+	fmt.Println("len =", len(c))
+	<- c
+	fmt.Println("取出一个数后:")
+	fmt.Println("cap =", cap(c))
+	fmt.Println("len =", len(c))
+}
package main
+
+import (
+	"fmt"
+)
+
+func main() {
+	// 创建一个通道
+	c := make(chan int, 3)
+	fmt.Println("初始化后:")
+	fmt.Println("cap =", cap(c))
+	fmt.Println("len =", len(c))
+	c <- 1
+	c <- 2
+	fmt.Println("传入两个数后:")
+	fmt.Println("cap =", cap(c))
+	fmt.Println("len =", len(c))
+	<- c
+	fmt.Println("取出一个数后:")
+	fmt.Println("cap =", cap(c))
+	fmt.Println("len =", len(c))
+}

缓冲通道与无缓冲通道

按照是否可缓冲数据可分为:缓冲通道无缓冲通道

无缓冲通道在通道里无法存储数据,接收端必须先于发送端准备好,以确保你发送完数据后,有人立马接收数据,否则发送端就会造成阻塞,原因很简单,通道中无法存储数据。也就是说发送端和接收端是同步运行的。

go
c := make(chan int)
+// 或者
+c := make(chan int, 0)
c := make(chan int)
+// 或者
+c := make(chan int, 0)

缓冲通道允许通道里存储一个或多个数据,设置缓冲区后,发送端和接收端可以处于异步的状态。

go
c := make(chan int, 3)
c := make(chan int, 3)

双向通道

到目前为止,上面定义的都是双向通道,既可以发送数据也可以接收数据。例如:

go
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func main() {
+	// 创建一个通道
+	c := make(chan int)
+
+	// 发送数据
+	go func() {
+		fmt.Println("send: 1")
+		c <- 1
+	}()
+
+	// 接收数据
+	go func() {
+		n := <- c
+		fmt.Println("receive:", n)
+	}()
+
+	// 主协程休眠
+	time.Sleep(time.Millisecond)
+}
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func main() {
+	// 创建一个通道
+	c := make(chan int)
+
+	// 发送数据
+	go func() {
+		fmt.Println("send: 1")
+		c <- 1
+	}()
+
+	// 接收数据
+	go func() {
+		n := <- c
+		fmt.Println("receive:", n)
+	}()
+
+	// 主协程休眠
+	time.Sleep(time.Millisecond)
+}

单向通道

单向通道只能发送或者接收数据。所以可以具体细分为只读通道和只写通道。

<-chan 表示只读通道:

chan<- 表示只写通道:

go
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+// Sender 只写通道类型
+type Sender = chan<- string
+
+// Receiver 只读通道类型
+type Receiver = <-chan string
+
+func main() {
+	// 创建一个双向通道
+	var ch = make(chan string)
+
+	// 开启一个协程
+	go func() {
+		// 只能写通道
+		var sender Sender = ch
+		fmt.Println("即将学习:")
+		sender <- "Go语言"
+	}()
+
+	// 开启一个协程
+	go func() {
+		// 只能读通道
+		var receiver Receiver = ch
+		message := <-receiver
+		fmt.Println("开始学习: ", message)
+	}()
+
+	time.Sleep(time.Millisecond)
+}
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+// Sender 只写通道类型
+type Sender = chan<- string
+
+// Receiver 只读通道类型
+type Receiver = <-chan string
+
+func main() {
+	// 创建一个双向通道
+	var ch = make(chan string)
+
+	// 开启一个协程
+	go func() {
+		// 只能写通道
+		var sender Sender = ch
+		fmt.Println("即将学习:")
+		sender <- "Go语言"
+	}()
+
+	// 开启一个协程
+	go func() {
+		// 只能读通道
+		var receiver Receiver = ch
+		message := <-receiver
+		fmt.Println("开始学习: ", message)
+	}()
+
+	time.Sleep(time.Millisecond)
+}

遍历通道

使用 for range 循环可以遍历通道,但在遍历时要确保通道是处于关闭状态,否则循环会被阻塞。

go
package main
+
+import (
+   "fmt"
+)
+
+func loopPrint(c chan int) {
+   for i := 0; i < 10; i++ {
+      c <- i
+   }
+   // 记得要关闭通道
+   // 否则主协程遍历完不会结束,而会阻塞
+   close(c)
+}
+
+func main() {
+   // 创建一个通道
+   var ch2 = make(chan int, 5)
+   go loopPrint(ch2)
+   for v := range ch2 {
+      fmt.Println(v)
+   }
+}
package main
+
+import (
+   "fmt"
+)
+
+func loopPrint(c chan int) {
+   for i := 0; i < 10; i++ {
+      c <- i
+   }
+   // 记得要关闭通道
+   // 否则主协程遍历完不会结束,而会阻塞
+   close(c)
+}
+
+func main() {
+   // 创建一个通道
+   var ch2 = make(chan int, 5)
+   go loopPrint(ch2)
+   for v := range ch2 {
+      fmt.Println(v)
+   }
+}

用通道做锁

上面讲过,当通道容量为 1 时,说明通道只能缓存一个数据,若通道中已有一个数据,此时再往里发送数据,会造成程序阻塞。例如:

go
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+// 由于 x = x+1 不是原子操作
+// 所以应避免多个协程对 x 进行操作
+// 使用容量为 1 的通道可以达到锁的效果
+func increment(ch chan bool, x *int) {
+	ch <- true
+	*x = *x + 1
+	<- ch
+}
+
+func main() {
+	ch3 := make(chan bool, 1)
+	var x int
+	for i := 0; i < 10000; i++ {
+		go increment(ch3, &x)
+	}
+	time.Sleep(time.Millisecond)
+	fmt.Println("x =", x)
+}
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+// 由于 x = x+1 不是原子操作
+// 所以应避免多个协程对 x 进行操作
+// 使用容量为 1 的通道可以达到锁的效果
+func increment(ch chan bool, x *int) {
+	ch <- true
+	*x = *x + 1
+	<- ch
+}
+
+func main() {
+	ch3 := make(chan bool, 1)
+	var x int
+	for i := 0; i < 10000; i++ {
+		go increment(ch3, &x)
+	}
+	time.Sleep(time.Millisecond)
+	fmt.Println("x =", x)
+}

死锁

当协程给一个通道发送数据时,照理说会有其他 Go 协程来接收数据。如果没有的话,程序就会在运行时触发 panic ,形成死锁。同理,当有协程等着从一个通道接收数据时,我们期望其他的 Go 协程会向该通道写入数据,要不然程序也会触发 panic

go
package main
+
+func main() {
+	ch := make(chan bool)
+	ch <- true
+}
+//fatal error: all goroutines are asleep - deadlock!
package main
+
+func main() {
+	ch := make(chan bool)
+	ch <- true
+}
+//fatal error: all goroutines are asleep - deadlock!
go
package main
+
+import "fmt"
+
+func main() {
+	ch := make(chan bool)
+	ch <- true
+	fmt.Println(<-ch)
+}
+//fatal error: all goroutines are asleep - deadlock!
+//使用 make 函数创建通道时默认不传递第二个参数,通道中不能存放数据,在发送数据时,必须要求立马有人接收,即该通道为无缓冲通道。所以在接收者没有准备好前,发送操作会被阻塞。
package main
+
+import "fmt"
+
+func main() {
+	ch := make(chan bool)
+	ch <- true
+	fmt.Println(<-ch)
+}
+//fatal error: all goroutines are asleep - deadlock!
+//使用 make 函数创建通道时默认不传递第二个参数,通道中不能存放数据,在发送数据时,必须要求立马有人接收,即该通道为无缓冲通道。所以在接收者没有准备好前,发送操作会被阻塞。
go
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func funcRecieve(c chan bool) {
+	fmt.Println(<-c)
+}
+func main() {
+	ch4 := make(chan bool)
+	go funcRecieve(ch4)
+	ch4 <- true
+	time.Sleep(time.Millisecond)
+}
+
+
+// 或
+
+package main
+
+import "fmt"
+
+func main() {
+	ch6 := make(chan bool, 1)
+	ch6 <- true
+	ch6 <- false
+	fmt.Println(<-ch6)
+}
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func funcRecieve(c chan bool) {
+	fmt.Println(<-c)
+}
+func main() {
+	ch4 := make(chan bool)
+	go funcRecieve(ch4)
+	ch4 <- true
+	time.Sleep(time.Millisecond)
+}
+
+
+// 或
+
+package main
+
+import "fmt"
+
+func main() {
+	ch6 := make(chan bool, 1)
+	ch6 <- true
+	ch6 <- false
+	fmt.Println(<-ch6)
+}

WaitGroup

在实际开发中我们并不能保证每个协程执行的时间,如果需要等待多个协程,全部结束任务后,再执行某个业务逻辑。下面我们介绍处理这种情况的方式。

WaitGroup 有几个方法:

  • Add:初始值为 0 ,这里直接传入子协程的数量,你传入的值会往计数器上加。
  • Done:当某个子协程完成后,可调用此方法,会从计数器上减一,即子协程的数量减一,通常使用 defer 来调用。
  • Wait:阻塞当前协程,直到实例里的计数器归零。

使用信道

信道可以实现多个协程间的通信,于是乎我们可以定义一个信道,在任务执行完成后,往信道中写入 true ,然后在主协程中获取到 true ,就可以认为子协程已经执行完毕。

go
package main
+
+import "fmt"
+
+func main() {
+	isDone := make(chan bool)
+	go func() {
+		for i := 0; i < 5; i++{
+			fmt.Println(i)
+		}
+		isDone <- true
+	}()
+	<- isDone
+}
package main
+
+import "fmt"
+
+func main() {
+	isDone := make(chan bool)
+	go func() {
+		for i := 0; i < 5; i++{
+			fmt.Println(i)
+		}
+		isDone <- true
+	}()
+	<- isDone
+}

运行上面的程序,主协程就会等待创建的协程执行完毕后退出。

使用 WaitGroup

使用上面的信道方法,虽然可行,但在你程序中使用很多协程的话,你的代码就会看起来很复杂,这里就要介绍一种更好的方法,那就是使用 sync 包中提供的 WaitGroup 类型。WaitGroup 用于等待一批 Go 协程执行结束。程序控制会一直阻塞,直到这些协程全部执行完毕。当然 WaitGroup 也可以用于实现工作池。

WaitGroup 实例化后就能使用:

go
var name sync.WaitGroup
var name sync.WaitGroup
go
package main
+
+import (
+	"fmt"
+	"sync"
+)
+
+func task(taskNum int, wg *sync.WaitGroup) {
+	// 延迟调用 执行完子协程计数器减一
+	defer wg.Done()
+	// 输出任务号
+	for i := 0; i < 3; i++ {
+		fmt.Printf("task %d: %d\n", taskNum, i)
+	}
+}
+
+func main() {
+	// 实例化 sync.WaitGroup
+	var waitGroup sync.WaitGroup
+	// 传入子协程的数量
+	waitGroup.Add(3)
+	// 开启一个子协程 协程 1 以及 实例 waitGroup
+	go task(1, &waitGroup)
+	// 开启一个子协程 协程 2 以及 实例 waitGroup
+	go task(2, &waitGroup)
+	// 开启一个子协程 协程 3 以及 实例 waitGroup
+	go task(3, &waitGroup)
+	// 实例 waitGroup 阻塞当前协程 等待所有子协程执行完
+	waitGroup.Wait()
+}
package main
+
+import (
+	"fmt"
+	"sync"
+)
+
+func task(taskNum int, wg *sync.WaitGroup) {
+	// 延迟调用 执行完子协程计数器减一
+	defer wg.Done()
+	// 输出任务号
+	for i := 0; i < 3; i++ {
+		fmt.Printf("task %d: %d\n", taskNum, i)
+	}
+}
+
+func main() {
+	// 实例化 sync.WaitGroup
+	var waitGroup sync.WaitGroup
+	// 传入子协程的数量
+	waitGroup.Add(3)
+	// 开启一个子协程 协程 1 以及 实例 waitGroup
+	go task(1, &waitGroup)
+	// 开启一个子协程 协程 2 以及 实例 waitGroup
+	go task(2, &waitGroup)
+	// 开启一个子协程 协程 3 以及 实例 waitGroup
+	go task(3, &waitGroup)
+	// 实例 waitGroup 阻塞当前协程 等待所有子协程执行完
+	waitGroup.Wait()
+}

Select

select 语句用在多个发送/接收通道操作中进行选择。

  • select 语句会一直阻塞,直到发送/接收操作准备就绪。
  • 如果有多个通道操作准备完毕, select 会随机地选取其中之一执行。

select 语法如下:

go
select {
+    case expression1:
+        code
+    case expression2:
+        code
+    default:
+        code
+}
select {
+    case expression1:
+        code
+    case expression2:
+        code
+    default:
+        code
+}
go
package main
+
+import "fmt"
+
+func main() {
+    // 创建3个通道
+    ch1 := make(chan string, 1)
+    ch2 := make(chan string, 1)
+    ch3 := make(chan string, 1)
+    // 往通道 1 发送数据 
+    ch1 <- "Go语言1"
+    // 往通道 2 发送数据 
+    ch2 <- "Go语言2"
+    // 往通道 3 发送数据 
+    ch3 <- "Go语言3"
+
+    select {
+    // 如果从通道 1 收到数据
+    case message1 := <-ch1:
+        fmt.Println("ch1 received:", message1)
+    // 如果从通道 2 收到数据
+    case message2 := <-ch2:
+        fmt.Println("ch2 received:", message2)
+    // 如果从通道 3 收到数据
+    case message3 := <-ch3:
+        fmt.Println("ch3 received:", message3)
+    // 默认输出
+    default:
+        fmt.Println("No data received.")
+    }
+}
package main
+
+import "fmt"
+
+func main() {
+    // 创建3个通道
+    ch1 := make(chan string, 1)
+    ch2 := make(chan string, 1)
+    ch3 := make(chan string, 1)
+    // 往通道 1 发送数据 
+    ch1 <- "Go语言1"
+    // 往通道 2 发送数据 
+    ch2 <- "Go语言2"
+    // 往通道 3 发送数据 
+    ch3 <- "Go语言3"
+
+    select {
+    // 如果从通道 1 收到数据
+    case message1 := <-ch1:
+        fmt.Println("ch1 received:", message1)
+    // 如果从通道 2 收到数据
+    case message2 := <-ch2:
+        fmt.Println("ch2 received:", message2)
+    // 如果从通道 3 收到数据
+    case message3 := <-ch3:
+        fmt.Println("ch3 received:", message3)
+    // 默认输出
+    default:
+        fmt.Println("No data received.")
+    }
+}

在执行 select 语句时,如果有机会的话会运行所有表达式,只要其中一个通道接收到数据,那么就会执行对应的 case 代码,然后退出。

select 的应用

每个任务执行的时间不同,使用 select 语句等待相应的通道发出响应。select 会选择首先响应先完成的 task,而忽略其它的响应。使用这种方法,我们可以做多个 task,并给用户返回最快的 task 结果。

go
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func task1(ch chan string) {
+	time.Sleep(5 * time.Second)
+	ch <- "Go语言1"
+}
+
+func task2(ch chan string) {
+	time.Sleep(7 * time.Second)
+	ch <- "Go语言2"
+}
+
+func task3(ch chan string) {
+	time.Sleep(2 * time.Second)
+	ch <- "Go语言3"
+}
+
+func main() {
+	// 创建三个通道
+	ch1 := make(chan string)
+	ch2 := make(chan string)
+	ch3 := make(chan string)
+	go task1(ch1)
+	go task2(ch2)
+	go task3(ch3)
+
+	select {
+	// 如果从通道 1 收到数据
+	case message1 := <-ch1:
+		fmt.Println("ch1 received:", message1)
+	// 如果从通道 2 收到数据
+	case message2 := <-ch2:
+		fmt.Println("ch2 received:", message2)
+	// 如果从通道 3 收到数据
+	case message3 := <-ch3:
+		fmt.Println("ch3 received:", message3)
+	}
+}
package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func task1(ch chan string) {
+	time.Sleep(5 * time.Second)
+	ch <- "Go语言1"
+}
+
+func task2(ch chan string) {
+	time.Sleep(7 * time.Second)
+	ch <- "Go语言2"
+}
+
+func task3(ch chan string) {
+	time.Sleep(2 * time.Second)
+	ch <- "Go语言3"
+}
+
+func main() {
+	// 创建三个通道
+	ch1 := make(chan string)
+	ch2 := make(chan string)
+	ch3 := make(chan string)
+	go task1(ch1)
+	go task2(ch2)
+	go task3(ch3)
+
+	select {
+	// 如果从通道 1 收到数据
+	case message1 := <-ch1:
+		fmt.Println("ch1 received:", message1)
+	// 如果从通道 2 收到数据
+	case message2 := <-ch2:
+		fmt.Println("ch2 received:", message2)
+	// 如果从通道 3 收到数据
+	case message3 := <-ch3:
+		fmt.Println("ch3 received:", message3)
+	}
+}

上面的程序会发现,没有 default 分支,因为如果加了该默认分支,如果还没从通道接收到数据, select 语句就会直接执行 default 分支然后退出,而不是被阻塞。

造成死锁

如果没有 default 分支, select 就会阻塞,如果一直没有命中其中的某个 case 最后会造成死锁。

go
package main
+
+import (
+    "fmt"
+)
+
+func main() {
+    // 创建两个通道
+    ch1 := make(chan string, 1)
+    ch2 := make(chan string, 1)
+    ch3 := make(chan string, 1)
+
+    select {
+    // 如果从通道 1 收到数据
+    case message1 := <-ch1:
+        fmt.Println("ch1 received:", message1)
+    // 如果从通道 2 收到数据
+    case message2 := <-ch2:
+        fmt.Println("ch2 received:", message2)
+	// 如果从通道 3 收到数据
+    case message3 := <-ch3:
+        fmt.Println("ch3 received:", message3)
+    }
+}
+//fatal error: all goroutines are asleep - deadlock!
package main
+
+import (
+    "fmt"
+)
+
+func main() {
+    // 创建两个通道
+    ch1 := make(chan string, 1)
+    ch2 := make(chan string, 1)
+    ch3 := make(chan string, 1)
+
+    select {
+    // 如果从通道 1 收到数据
+    case message1 := <-ch1:
+        fmt.Println("ch1 received:", message1)
+    // 如果从通道 2 收到数据
+    case message2 := <-ch2:
+        fmt.Println("ch2 received:", message2)
+	// 如果从通道 3 收到数据
+    case message3 := <-ch3:
+        fmt.Println("ch3 received:", message3)
+    }
+}
+//fatal error: all goroutines are asleep - deadlock!

运行上面的程序会造成死锁。解决该问题的方法是写好 default 分支。

还有另一种情况会导致死锁的发生,那就是使用空 select

go
package main
+
+func main() {
+    select {}
+}
package main
+
+func main() {
+    select {}
+}

运行上面的程序会抛出 panic

Tips:

  • switch-case 里面的 case 是顺序执行的,但在 select 里并不是顺序执行的。在上面的第一个例子就可以看出,当 select 由多个 case 准备就绪时,将会随机地选取其中之一去执行。

select超时处理

case 里的通道始终没有接收到数据时,而且也没有 default 语句时, select 整体就会阻塞,但是有时我们并不希望 select 一直阻塞下去,这时候就可以手动设置一个超时时间。

go
package main
+
+import (
+    "fmt"
+    "time"
+)
+
+func makeTimeout(ch chan bool, t int) {
+    time.Sleep(time.Second * time.Duration(t))
+    ch <- true
+}
+
+func main() {
+    c1 := make(chan string, 1)
+    c2 := make(chan string, 1)
+    c3 := make(chan string, 1)
+    timeout := make(chan bool, 1)
+
+    go makeTimeout(timeout, 2)
+
+    select {
+    case msg1 := <-c1:
+        fmt.Println("c1 received: ", msg1)
+    case msg2 := <-c2:
+        fmt.Println("c2 received: ", msg2)
+    case msg3 := <-c3:
+        fmt.Println("c3 received: ", msg3)
+    case <-timeout:
+        fmt.Println("Timeout, exit.")
+    }
+}
package main
+
+import (
+    "fmt"
+    "time"
+)
+
+func makeTimeout(ch chan bool, t int) {
+    time.Sleep(time.Second * time.Duration(t))
+    ch <- true
+}
+
+func main() {
+    c1 := make(chan string, 1)
+    c2 := make(chan string, 1)
+    c3 := make(chan string, 1)
+    timeout := make(chan bool, 1)
+
+    go makeTimeout(timeout, 2)
+
+    select {
+    case msg1 := <-c1:
+        fmt.Println("c1 received: ", msg1)
+    case msg2 := <-c2:
+        fmt.Println("c2 received: ", msg2)
+    case msg3 := <-c3:
+        fmt.Println("c3 received: ", msg3)
+    case <-timeout:
+        fmt.Println("Timeout, exit.")
+    }
+}

读取/写入数据

select 里的 case 表达式只能对通道进行操作,不管你是往通道写入数据,还是从通道读出数据。

go
package main
+
+import (
+    "fmt"
+)
+
+func main() {
+    c1 := make(chan string, 2)
+
+    c1 <- "Go语言1"
+    select {
+    case c1 <- "Go语言2":
+        fmt.Println("c1 received: ", <-c1)
+        fmt.Println("c1 received: ", <-c1)
+    default:
+        fmt.Println("channel blocking")
+    }
+}
+//c1 received:  Go语言1
+//c1 received:  Go语言2
package main
+
+import (
+    "fmt"
+)
+
+func main() {
+    c1 := make(chan string, 2)
+
+    c1 <- "Go语言1"
+    select {
+    case c1 <- "Go语言2":
+        fmt.Println("c1 received: ", <-c1)
+        fmt.Println("c1 received: ", <-c1)
+    default:
+        fmt.Println("channel blocking")
+    }
+}
+//c1 received:  Go语言1
+//c1 received:  Go语言2

线程同步

Go 语言中,经常会遇到并发的问题,当然我们会优先考虑使用通道,同时 Go 语言也给出了传统的解决方式 Mutex(互斥锁)RWMutex(读写锁) 来处理竞争条件。

go
type Bank struct {
+    balance int
+}
+
+func (b *Bank) Deposit(amount int) {
+    b.balance += amount
+}
+
+func (b *Bank) Balance() int {
+    return b.balance
+}
+
+func main() {
+    b := &Bank{}
+
+    b.Deposit(1000)
+    b.Deposit(1000)
+    b.Deposit(1000)
+
+    fmt.Println(b.Balance())  //3000
+}
type Bank struct {
+    balance int
+}
+
+func (b *Bank) Deposit(amount int) {
+    b.balance += amount
+}
+
+func (b *Bank) Balance() int {
+    return b.balance
+}
+
+func main() {
+    b := &Bank{}
+
+    b.Deposit(1000)
+    b.Deposit(1000)
+    b.Deposit(1000)
+
+    fmt.Println(b.Balance())  //3000
+}

临界区

当程序并发地运行时,多个 Go 协程不应该同时访问那些修改共享资源的代码。这些修改共享资源的代码称为临界区

go
package main
+
+import (
+	"fmt"
+	"sync"
+)
+
+type Bank struct {
+	balance int
+}
+
+func (b *Bank) Deposit(amount int) {
+	b.balance += amount
+}
+
+func (b *Bank) Balance() int {
+	return b.balance
+}
+func main() {
+	var wg sync.WaitGroup
+	b := &Bank{}
+
+	n := 1000
+	wg.Add(n)
+	for i := 1; i <= n; i++ {
+		go func() {
+			b.Deposit(1000)
+			wg.Done()
+		}()
+	}
+	wg.Wait()
+	fmt.Println(b.Balance()) //972000,962000,941000
+}
package main
+
+import (
+	"fmt"
+	"sync"
+)
+
+type Bank struct {
+	balance int
+}
+
+func (b *Bank) Deposit(amount int) {
+	b.balance += amount
+}
+
+func (b *Bank) Balance() int {
+	return b.balance
+}
+func main() {
+	var wg sync.WaitGroup
+	b := &Bank{}
+
+	n := 1000
+	wg.Add(n)
+	for i := 1; i <= n; i++ {
+		go func() {
+			b.Deposit(1000)
+			wg.Done()
+		}()
+	}
+	wg.Wait()
+	fmt.Println(b.Balance()) //972000,962000,941000
+}

举一个简单的例子,当前变量的值增加 b.balance += amount

当然,对于只有一个协程的程序来说,上面的代码没有任何问题。但是,如果有多个协程并发运行时,就会发生错误,这种情况就称之为数据竞争(data race)。使用下面的互斥锁 Mutex 就能避免这种情况的发生。

互斥锁 Mutex

互斥锁(Mutex,mutual exclusion) 用于提供一种 加锁机制(Locking Mechanism) ,可确保在某时刻只有一个协程在临界区运行,以防止出现竞争。也是为了来保护一个资源不会因为并发操作而引起冲突导致数据不准确。

Mutex 有两个方法,分别是 Lock()Unlock() ,即对应的加锁和解锁。在 Lock()Unlock() 之间的代码,都只能由一个协程执行,就能避免竞争条件。

如果有一个协程已经持有了锁(Lock),当其他协程试图获得该锁时,这些协程会被阻塞,直到Mutex解除锁定。

go
package main
+
+import (
+    "fmt"
+    "sync"
+)
+
+type BankV2 struct {
+    balance int
+    m       sync.Mutex
+}
+
+func (b *BankV2) Deposit(amount int) {
+    b.m.Lock()
+    b.balance += amount
+    b.m.Unlock()
+}
+
+func (b *BankV2) Balance() int {
+    return b.balance
+}
+
+func main() {
+    var wg sync.WaitGroup
+    b := &BankV2{}
+
+    n := 1000
+    wg.Add(n)
+    for i := 1; i <= n; i++ {
+        go func() {
+            b.Deposit(1000)
+            wg.Done()
+        }()
+    }
+    wg.Wait()
+    fmt.Println(b.Balance()) //1000000
+}
package main
+
+import (
+    "fmt"
+    "sync"
+)
+
+type BankV2 struct {
+    balance int
+    m       sync.Mutex
+}
+
+func (b *BankV2) Deposit(amount int) {
+    b.m.Lock()
+    b.balance += amount
+    b.m.Unlock()
+}
+
+func (b *BankV2) Balance() int {
+    return b.balance
+}
+
+func main() {
+    var wg sync.WaitGroup
+    b := &BankV2{}
+
+    n := 1000
+    wg.Add(n)
+    for i := 1; i <= n; i++ {
+        go func() {
+            b.Deposit(1000)
+            wg.Done()
+        }()
+    }
+    wg.Wait()
+    fmt.Println(b.Balance()) //1000000
+}

要注意同一协程里不要在尚未解锁时再次加锁,也不要对已经解锁的锁再次解锁。

读写锁 RWMutex

sync.RWMutex 类型实现读写互斥锁,适用于读多写少的场景,它规定了当有人还在读取数据(即读锁占用)时,不允许有人更新这个数据(即写锁会阻塞);为了保证程序的效率,多个人(协程)读取数据(拥有读锁)时,互不影响不会造成阻塞,它不会像 Mutex 那样只允许有一个人(协程)读取同一个数据。读锁与读锁兼容,读锁与写锁互斥,写锁与写锁互斥。

  • 可以同时申请多个读锁;
  • 有读锁时申请写锁将阻塞,有写锁时申请读锁将阻塞;
  • 只要有写锁,后续申请读锁和写锁都将阻塞。

定义一个 RWMuteux 读写锁:

go
var rwMutex sync.RWMutex
var rwMutex sync.RWMutex

RWMutex 里提供了两种锁,每种锁分别对应两个方法,为了避免死锁,两个方法应成对出现,必要时请使用 defer

  • 读锁:调用 RLock 方法开启锁,调用 RUnlock 释放锁;
  • 写锁:调用 Lock 方法开启锁,调用 Unlock 释放锁。
go
package main
+
+import (
+    "fmt"
+    "sync"
+    "time"
+)
+
+type BankV3 struct {
+    balance int
+    rwMutex sync.RWMutex // read write lock
+}
+
+func (b *BankV3) Deposit(amount int) {
+    b.rwMutex.Lock() // write lock
+    b.balance += amount
+    b.rwMutex.Unlock() // wirte unlock
+}
+
+func (b *BankV3) Balance() (balance int) {
+    b.rwMutex.RLock() // read lock
+    balance = b.balance
+    b.rwMutex.RUnlock() // read unlock
+    return
+}
+
+func main() {
+    var wg sync.WaitGroup
+    b := &BankV3{}
+
+    n := 1000
+    wg.Add(n)
+    for i := 1; i <= n; i++ {
+        go func() {
+            b.Deposit(1000)
+            wg.Done()
+        }()
+    }
+    wg.Wait()
+    fmt.Println(b.Balance())
+}
package main
+
+import (
+    "fmt"
+    "sync"
+    "time"
+)
+
+type BankV3 struct {
+    balance int
+    rwMutex sync.RWMutex // read write lock
+}
+
+func (b *BankV3) Deposit(amount int) {
+    b.rwMutex.Lock() // write lock
+    b.balance += amount
+    b.rwMutex.Unlock() // wirte unlock
+}
+
+func (b *BankV3) Balance() (balance int) {
+    b.rwMutex.RLock() // read lock
+    balance = b.balance
+    b.rwMutex.RUnlock() // read unlock
+    return
+}
+
+func main() {
+    var wg sync.WaitGroup
+    b := &BankV3{}
+
+    n := 1000
+    wg.Add(n)
+    for i := 1; i <= n; i++ {
+        go func() {
+            b.Deposit(1000)
+            wg.Done()
+        }()
+    }
+    wg.Wait()
+    fmt.Println(b.Balance())
+}

条件变量 sync.Cond

Cond 实现了一个条件变量,在 Locker 的基础上增加的一个消息通知的功能,保存了一个通知列表,用来唤醒一个或所有因等待条件变量而阻塞的 Go 程,以此来实现多个 Go 程间的同步。

错误与异常

错误

内建错误

在 Go 中, 错误 使用内建的 error 类型表示。error 类型是一个接口类型,它的定义如下:

go
type error interface {
+    Error() string
+}
type error interface {
+    Error() string
+}

error 有了一个签名为 Error() string 的方法。所有实现该接口的类型都可以当作一个错误类型。Error() 方法给出了错误的描述。fmt.Println 在打印错误时,会在内部调用 Error() string 方法来得到该错误的描述。

go
package main
+
+import (
+    "fmt"
+    "os"
+)
+
+func main() {
+    // 尝试打开文件
+    file, err := os.Open("/a.txt")
+    // 如果打开文件时发生错误 返回一个不等于 nil 的错误
+    if err != nil {
+        fmt.Println(err)
+        return
+    }
+    // 如果打开文件成功 返回一个文件句柄 和 一个值为 nil 的错误
+    fmt.Println(file.Name(), "opened successfully")
+}
+// open /a.txt: no such file or directory
package main
+
+import (
+    "fmt"
+    "os"
+)
+
+func main() {
+    // 尝试打开文件
+    file, err := os.Open("/a.txt")
+    // 如果打开文件时发生错误 返回一个不等于 nil 的错误
+    if err != nil {
+        fmt.Println(err)
+        return
+    }
+    // 如果打开文件成功 返回一个文件句柄 和 一个值为 nil 的错误
+    fmt.Println(file.Name(), "opened successfully")
+}
+// open /a.txt: no such file or directory

自定义错误

使用 errors 包中的 New 函数可以创建自定义错误。下面是 errors 包中 New 函数的实现代码:

go
package errors
+
+func New(text string) error {
+    return &errorString{text}
+}
+
+type errorString struct {
+    s string
+}
+
+func (e *errorString) Error() string {
+    return e.s
+}
package errors
+
+func New(text string) error {
+    return &errorString{text}
+}
+
+type errorString struct {
+    s string
+}
+
+func (e *errorString) Error() string {
+    return e.s
+}

errorString 是一个结构体类型,只有一个字符串字段 s 。它使用了 errorString 指针接受者,来实现 error 接口的 Error() string 方法。New 函数有一个字符串参数,通过这个参数创建了 errorString 类型的变量,并返回了它的地址。于是它就创建并返回了一个新的错误。

下面是一个简单的自定义错误例子,该例子创建了一个计算矩形面积的函数,当矩形的长和宽两者有一个为负数时,就会返回一个错误:

go
package main
+
+import (
+    "errors"
+    "fmt"
+)
+
+func area(a, b int) (int, error) {
+    if a < 0 || b < 0 {
+        return 0, errors.New("计算错误, 长度或宽度,不能小于0.")
+    }
+    return a * b, nil
+}
+func main() {
+    a := 100
+    b := -10
+    r, err := area(a, b)
+    if err != nil {
+        fmt.Println(err)
+        return
+    }
+    fmt.Println("Area =", r)
+}
package main
+
+import (
+    "errors"
+    "fmt"
+)
+
+func area(a, b int) (int, error) {
+    if a < 0 || b < 0 {
+        return 0, errors.New("计算错误, 长度或宽度,不能小于0.")
+    }
+    return a * b, nil
+}
+func main() {
+    a := 100
+    b := -10
+    r, err := area(a, b)
+    if err != nil {
+        fmt.Println(err)
+        return
+    }
+    fmt.Println("Area =", r)
+}

给错误添加更多信息

上面的程序能报出我们自定义的错误,但是没有具体说明是哪个数据出了问题,所以下面就来改进一下这个程序,我们使用 fmt 包中的 Errorf 函数,规定错误格式,并返回一个符合该错误的字符串。

go
package main
+
+import (
+    "fmt"
+)
+
+func area(a, b int) (int, error) {
+    if a < 0 || b < 0 {
+        return 0, fmt.Errorf("计算错误, 长度%d或宽度%d,不能小于0", a, b)
+    }
+    return a * b, nil
+}
+func main() {
+    a := 100
+    b := -10
+    area, err := area(a, b)
+    if err != nil {
+        fmt.Println(err)
+        return
+    }
+    fmt.Println("Area =", area)
+}
package main
+
+import (
+    "fmt"
+)
+
+func area(a, b int) (int, error) {
+    if a < 0 || b < 0 {
+        return 0, fmt.Errorf("计算错误, 长度%d或宽度%d,不能小于0", a, b)
+    }
+    return a * b, nil
+}
+func main() {
+    a := 100
+    b := -10
+    area, err := area(a, b)
+    if err != nil {
+        fmt.Println(err)
+        return
+    }
+    fmt.Println("Area =", area)
+}

给错误添加更多信息还可以 使用结构体类型和字段 实现。下面还是通过改进上面的程序来讲解这种方法的实现:

首先创建一个表示错误的结构体类型,一般错误类型名称都是以 Error 结尾,上面的错误是由于面积计算中长度或宽度错误导致的,所以这里把结构体命名为 areaError

go
package main
+
+import (
+    "fmt"
+)
+
+type areaError struct {
+    // 错误信息
+    err string
+    // 错误有关的长度
+    length int
+    // 错误有关的宽度
+    width int
+}
+
+// 使用指针接收者 *areaError 实现了 error 接口的 Error() string 方法
+func (e *areaError) Error() string {
+    // 打印长度和宽度以及错误的描述
+    return fmt.Sprintf("length %d, width %d : %s", e.length, e.width, e.err)
+}
+
+func rectangleArea(a, b int) (int, error) {
+    if a < 0 || b < 0 {
+        return 0, &areaError{"length or width is negative", a, b}
+    }
+    return a * b, nil
+}
+func main() {
+    a := 100
+    b := -10
+    area, err := rectangleArea(a, b)
+    // 检查了错误是否为 nil
+    if err != nil {
+        // 断言 *areaError 类型
+        if err, ok := err.(*areaError); ok {
+            // 如果错误是 *areaError 类型
+            // 用 err.length 和 err.width 来获取错误的长度和宽度 打印出自定义错误的消息
+            fmt.Printf("length %d or width %d is less than zero", err.length, err.width)
+            return
+        }
+        fmt.Println(err)
+        return
+    }
+    fmt.Println("Area =", area)
+}
package main
+
+import (
+    "fmt"
+)
+
+type areaError struct {
+    // 错误信息
+    err string
+    // 错误有关的长度
+    length int
+    // 错误有关的宽度
+    width int
+}
+
+// 使用指针接收者 *areaError 实现了 error 接口的 Error() string 方法
+func (e *areaError) Error() string {
+    // 打印长度和宽度以及错误的描述
+    return fmt.Sprintf("length %d, width %d : %s", e.length, e.width, e.err)
+}
+
+func rectangleArea(a, b int) (int, error) {
+    if a < 0 || b < 0 {
+        return 0, &areaError{"length or width is negative", a, b}
+    }
+    return a * b, nil
+}
+func main() {
+    a := 100
+    b := -10
+    area, err := rectangleArea(a, b)
+    // 检查了错误是否为 nil
+    if err != nil {
+        // 断言 *areaError 类型
+        if err, ok := err.(*areaError); ok {
+            // 如果错误是 *areaError 类型
+            // 用 err.length 和 err.width 来获取错误的长度和宽度 打印出自定义错误的消息
+            fmt.Printf("length %d or width %d is less than zero", err.length, err.width)
+            return
+        }
+        fmt.Println(err)
+        return
+    }
+    fmt.Println("Area =", area)
+}

还可以使用 结构体类型的方法 来给错误添加更多信息。下面我们继续完善上面的程序,让程序更加精确的定位是长度引发的错误还是宽度引发的错误。

go
package main
+
+import (
+    "fmt"
+)
+
+type areaError struct {
+    // 错误信息
+    err string
+    // 长度
+    length int
+    // 宽度
+    width int
+}
+
+// 使用指针接收者 *areaError 实现了 error 接口的 Error() string 方法
+func (e *areaError) Error() string {
+    return e.err
+}
+
+// 长度为负数返回 true
+func (e *areaError) lengthNegative() bool {
+    return e.length < 0
+}
+
+// 宽度为负数返回 true
+func (e *areaError) widthNegative() bool {
+    return e.width < 0
+}
+
+func area(length, width int) (int, error) {
+    err := ""
+    if length < 0 {
+        err += "length is less than zero"
+    }
+    if width < 0 {
+        if err == "" {
+            err = "width is less than zero"
+        } else {
+            err += " and width is less than zero"
+        }
+    }
+    if err != "" {
+        return 0, &areaError{err, length, width}
+    }
+    return length * width, nil
+}
+
+func main() {
+    length := 100
+    width := -10
+    area, err := area(length, width)
+    // 检查了错误是否为 nil
+    if err != nil {
+        // 断言 *areaError 类型
+        if err, ok := err.(*areaError); ok {
+            // 如果错误是 *areaError 类型
+            // 如果长度为负数 打印错误长度具体值
+            if err.lengthNegative() {
+                fmt.Printf("error: 长度 %d 小于0\n", err.length)
+            }
+            // 如果宽度为负数 打印错误宽度具体值
+            if err.widthNegative() {
+                fmt.Printf("error: 宽度 %d 小于0\n", err.width)
+            }
+            return
+        }
+        fmt.Println(err)
+        return
+    }
+    fmt.Println("Area =", area)
+}
package main
+
+import (
+    "fmt"
+)
+
+type areaError struct {
+    // 错误信息
+    err string
+    // 长度
+    length int
+    // 宽度
+    width int
+}
+
+// 使用指针接收者 *areaError 实现了 error 接口的 Error() string 方法
+func (e *areaError) Error() string {
+    return e.err
+}
+
+// 长度为负数返回 true
+func (e *areaError) lengthNegative() bool {
+    return e.length < 0
+}
+
+// 宽度为负数返回 true
+func (e *areaError) widthNegative() bool {
+    return e.width < 0
+}
+
+func area(length, width int) (int, error) {
+    err := ""
+    if length < 0 {
+        err += "length is less than zero"
+    }
+    if width < 0 {
+        if err == "" {
+            err = "width is less than zero"
+        } else {
+            err += " and width is less than zero"
+        }
+    }
+    if err != "" {
+        return 0, &areaError{err, length, width}
+    }
+    return length * width, nil
+}
+
+func main() {
+    length := 100
+    width := -10
+    area, err := area(length, width)
+    // 检查了错误是否为 nil
+    if err != nil {
+        // 断言 *areaError 类型
+        if err, ok := err.(*areaError); ok {
+            // 如果错误是 *areaError 类型
+            // 如果长度为负数 打印错误长度具体值
+            if err.lengthNegative() {
+                fmt.Printf("error: 长度 %d 小于0\n", err.length)
+            }
+            // 如果宽度为负数 打印错误宽度具体值
+            if err.widthNegative() {
+                fmt.Printf("error: 宽度 %d 小于0\n", err.width)
+            }
+            return
+        }
+        fmt.Println(err)
+        return
+    }
+    fmt.Println("Area =", area)
+}

异常

错误和异常是两个不同的概念,非常容易混淆。错误指的是可能出现问题的地方出现了问题;而异常指的是不应该出现问题的地方出现了问题。

panic

在有些情况,当程序发生异常时,无法继续运行。在这种情况下,我们会使用 panic 来终止程序。当函数发生 panic 时,它会终止运行,在执行完所有的延迟函数后,程序返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪,最后程序终止。

我们应该尽可能地使用错误,而不是使用 panicrecover 。只有当程序不能继续运行的时候,才应该使用 panicrecover 机制。

panic 有两个合理的用例:

  • 发生了一个不能恢复的错误,此时程序不能继续运行。一个例子就是 web 服务器无法绑定所要求的端口。在这种情况下,就应该使用 panic ,因为如果不能绑定端口,啥也做不了。
  • 发生了一个编程上的错误。假如我们有一个接收指针参数的方法,而其他人使用 nil 作为参数调用了它。在这种情况下,我们可以使用 panic ,因为这是一个编程错误:用 nil 参数调用了一个只能接收合法指针的方法。
go
func panic(v interface{})
func panic(v interface{})
go
package main
+
+func main() {
+    panic("panic error")
+}
package main
+
+func main() {
+    panic("panic error")
+}

发生 panic 时的 defer

上面已经提到了,当函数发生 panic 时,它会终止运行,在执行完所有的延迟函数后,程序返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪,最后程序终止。

go
package main
+
+import "fmt"
+
+func myTest() {
+    defer fmt.Println("defer myTest")
+    panic("panic myTest")
+}
+func main() {
+    defer fmt.Println("defer main")
+    myTest()
+}
+// defer myTest
+// defer main
+// panic: panic myTest
package main
+
+import "fmt"
+
+func myTest() {
+    defer fmt.Println("defer myTest")
+    panic("panic myTest")
+}
+func main() {
+    defer fmt.Println("defer main")
+    myTest()
+}
+// defer myTest
+// defer main
+// panic: panic myTest

recover

recover 是一个内建函数,用于重新获得 panic 协程的控制。下面是内建函数 recover 的签名:

go
func recover() interface{}
func recover() interface{}

recover 必须在 defer 函数中才能生效,在其他作用域下,它是不工作的。在延迟函数内调用 recover ,可以取到 panic 的错误信息,并且停止 panic 续发事件,程序运行恢复正常。

go
package main
+
+import "fmt"
+
+func outOfArray(x int) {
+    defer func() {
+        // recover() 可以将捕获到的 panic 信息打印
+        if err := recover(); err != nil {
+            fmt.Println(err)
+        }
+    }()
+    var array [5]int
+    array[x] = 1
+}
+func main() {
+    // 故意制造数组越界 触发 panic
+    outOfArray(20)
+    // 如果能执行到这句 说明 panic 被捕获了
+    // 后续的程序能继续运行
+    fmt.Println("main...")
+}
+// runtime error: index out of range [20] with length 5
+// main...
package main
+
+import "fmt"
+
+func outOfArray(x int) {
+    defer func() {
+        // recover() 可以将捕获到的 panic 信息打印
+        if err := recover(); err != nil {
+            fmt.Println(err)
+        }
+    }()
+    var array [5]int
+    array[x] = 1
+}
+func main() {
+    // 故意制造数组越界 触发 panic
+    outOfArray(20)
+    // 如果能执行到这句 说明 panic 被捕获了
+    // 后续的程序能继续运行
+    fmt.Println("main...")
+}
+// runtime error: index out of range [20] with length 5
+// main...

虽然该程序触发了 panic ,但由于我们使用了 recover() 捕获了 panic 异常,并输出 panic 信息,即使 panic 会导致整个程序退出,但在退出前,有 defer 延迟函数,还是得执行完 defer 。然后程序还会继续执行下去

只有在相同的协程中调用 recover 才管用, recover 不能恢复一个不同协程的 panic

make 和 new

new函数

内置函数 new 分配内存。该函数只接受一个参数,该参数是一个任意类型(包括自定义类型),而不是值,返回指向该类型新分配零值的指针。

go
// The new built-in function allocates memory. The first argument is a type,
+// not a value, and the value returned is a pointer to a newly
+// allocated zero value of that type.
+func new(Type) *Type
// The new built-in function allocates memory. The first argument is a type,
+// not a value, and the value returned is a pointer to a newly
+// allocated zero value of that type.
+func new(Type) *Type

使用 new 函数首先会分配内存,并设置类型零值,最后返回指向该类型新分配零值的指针。

go
package main
+
+import (
+	"fmt"
+)
+
+func main() {
+	num := new(int)
+	// 打印出类型的值
+	fmt.Println(*num)  // 0
+}
package main
+
+import (
+	"fmt"
+)
+
+func main() {
+	num := new(int)
+	// 打印出类型的值
+	fmt.Println(*num)  // 0
+}

make函数

内置函数 make 只能分配和初始化类型为 slicemapchan 的对象。与 new 一样,第一个参数是类型,而不是值。与 new 不同, make 的返回类型与其参数的类型相同,而不是指向它的指针。结果取决于类型:

  • slice:size 指定长度。切片的容量等于其长度。可提供第三个参数以指定不同的容量;它不能小于长度。
  • map:为空映射分配足够的空间来容纳指定数量的元素。可以省略大小,在这种情况下,分配一个小的起始大小。
  • chan:使用指定的缓冲区容量初始化通道的缓冲区。如果为零,或者忽略了大小,则通道是无缓冲的。
go
func make(t Type, size ...IntegerType) Type
func make(t Type, size ...IntegerType) Type

使用make函数必须初始化

go
// slice
+a := make([]int, 2, 10)
+
+// map
+b := make(map[string]int)
+
+// chan
+c := make(chan int, 10)
// slice
+a := make([]int, 2, 10)
+
+// map
+b := make(map[string]int)
+
+// chan
+c := make(chan int, 10)

new 和 make 的区别

new:为所有的类型分配内存,并初始化为零值,返回指针。

make:只能为 slicemapchan 分配内存,并初始化,返回的是类型。

反射

reflect 包

Go 语言提供了一种机制,能够在运行时更新变量和检查它们的值、调用它们的方法,而不需要在编译时就知道这些变量的具体类型。这种机制被称为 反射

在 Go 中 reflect 包实现了运行时反射。reflect 包会帮助识别 interface{} 变量的底层具体类型和具体值。

reflect.Type

reflect.Type 表示 interface{} 的具体类型。reflect.TypeOf() 方法返回 reflect.Type

go
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func reflectType(x interface{}) {
+    obj := reflect.TypeOf(x)
+    fmt.Println(obj)
+}
+
+func main() {
+    var a int64 = 123
+    reflectType(a)
+    var b string = "Go语言"
+    reflectType(b)
+}
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func reflectType(x interface{}) {
+    obj := reflect.TypeOf(x)
+    fmt.Println(obj)
+}
+
+func main() {
+    var a int64 = 123
+    reflectType(a)
+    var b string = "Go语言"
+    reflectType(b)
+}

reflect.Value

reflect.Value 表示 interface{} 的具体值。reflect.ValueOf() 方法返回 reflect.Value

go
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func reflectType(x interface{}) {
+    typeX := reflect.TypeOf(x)
+    valueX := reflect.ValueOf(x)
+    fmt.Println(typeX)
+    fmt.Println(valueX)
+}
+
+func main() {
+    var a int64 = 123
+    reflectType(a)
+    var b string = "Go语言"
+    reflectType(b)
+}
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func reflectType(x interface{}) {
+    typeX := reflect.TypeOf(x)
+    valueX := reflect.ValueOf(x)
+    fmt.Println(typeX)
+    fmt.Println(valueX)
+}
+
+func main() {
+    var a int64 = 123
+    reflectType(a)
+    var b string = "Go语言"
+    reflectType(b)
+}

relfect.Kind

relfect.Kind 表示的是种类。在使用反射时,需要理解类型(Type)和种类(Kind)的区别。编程中,使用最多的是类型,但在反射中,当需要区分一个大品种的类型时,就会用到种类(Kind)。

Go 语言程序中的类型(Type)指的是系统原生数据类型,如 intstringboolfloat32 等类型,以及使用 type 关键字定义的类型,这些类型的名称就是其类型本身的名称。例如使用 type A struct{} 定义结构体时,A 就是 struct{} 的类型。

种类(Kind)指的是对象归属的品种,在 reflect 包中有如下定义:

go
// A Kind represents the specific kind of type that a Type represents.
+// The zero Kind is not a valid kind.
+type Kind uint
+
+const (
+    Invalid Kind = iota
+    Bool
+    Int
+    Int8
+    Int16
+    Int32
+    Int64
+    Uint
+    Uint8
+    Uint16
+    Uint32
+    Uint64
+    Uintptr
+    Float32
+    Float64
+    Complex64
+    Complex128
+    Array
+    Chan
+    Func
+    Interface
+    Map
+    Ptr
+    Slice
+    String
+    Struct
+    UnsafePointer
+)
// A Kind represents the specific kind of type that a Type represents.
+// The zero Kind is not a valid kind.
+type Kind uint
+
+const (
+    Invalid Kind = iota
+    Bool
+    Int
+    Int8
+    Int16
+    Int32
+    Int64
+    Uint
+    Uint8
+    Uint16
+    Uint32
+    Uint64
+    Uintptr
+    Float32
+    Float64
+    Complex64
+    Complex128
+    Array
+    Chan
+    Func
+    Interface
+    Map
+    Ptr
+    Slice
+    String
+    Struct
+    UnsafePointer
+)
go
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func reflectType(x interface{}) {
+    typeX := reflect.TypeOf(x)
+    fmt.Println(typeX.Kind()) // struct
+    fmt.Println(typeX)        // main.book
+}
+
+type book struct {
+}
+
+func main() {
+    var b book
+    reflectType(b)
+}
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func reflectType(x interface{}) {
+    typeX := reflect.TypeOf(x)
+    fmt.Println(typeX.Kind()) // struct
+    fmt.Println(typeX)        // main.book
+}
+
+type book struct {
+}
+
+func main() {
+    var b book
+    reflectType(b)
+}

relfect.NumField()

relfect.NumField() 方法返回结构体中字段的数量。

go
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func reflectNumField(x interface{}) {
+    // 检查 x 的类别是 struct
+    if reflect.ValueOf(x).Kind() == reflect.Struct {
+        v := reflect.ValueOf(x)
+        fmt.Println("Number of fields", v.NumField())
+    }
+}
+
+type book struct {
+    name string
+    spend  int
+}
+
+func main() {
+    var b book
+    reflectNumField(b)
+}
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func reflectNumField(x interface{}) {
+    // 检查 x 的类别是 struct
+    if reflect.ValueOf(x).Kind() == reflect.Struct {
+        v := reflect.ValueOf(x)
+        fmt.Println("Number of fields", v.NumField())
+    }
+}
+
+type book struct {
+    name string
+    spend  int
+}
+
+func main() {
+    var b book
+    reflectNumField(b)
+}

relfect.Field()

relfect.Field(i int) 方法返回字段 ireflect.Value

go
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func reflectNumField(x interface{}) {
+    // 检查 x 的类别是 struct
+    if reflect.ValueOf(x).Kind() == reflect.Struct {
+        v := reflect.ValueOf(x)
+        fmt.Println("Number of fields", v.NumField())
+        for i := 0; i < v.NumField(); i++ {
+            fmt.Printf("Field:%d type:%T value:%v\n", i, v.Field(i), v.Field(i))
+        }
+    }
+}
+
+type book struct {
+    name string
+    spend  int
+}
+
+func main() {
+    var b = book{"Go语言", 8}
+    reflectNumField(a)
+}
+// Number of fields 2
+// Field:0 type:reflect.Value value:Go语言
+// Field:1 type:reflect.Value value:8
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func reflectNumField(x interface{}) {
+    // 检查 x 的类别是 struct
+    if reflect.ValueOf(x).Kind() == reflect.Struct {
+        v := reflect.ValueOf(x)
+        fmt.Println("Number of fields", v.NumField())
+        for i := 0; i < v.NumField(); i++ {
+            fmt.Printf("Field:%d type:%T value:%v\n", i, v.Field(i), v.Field(i))
+        }
+    }
+}
+
+type book struct {
+    name string
+    spend  int
+}
+
+func main() {
+    var b = book{"Go语言", 8}
+    reflectNumField(a)
+}
+// Number of fields 2
+// Field:0 type:reflect.Value value:Go语言
+// Field:1 type:reflect.Value value:8

反射的三大定律

一个接口变量,实际上都是由一 pair 对(type 和 data)组合而成,pair 对中记录着实际变量的值和类型。也就是说在真实世界(反射前环境)里,type 和 value 是合并在一起组成接口变量的。

而在反射的世界(反射后的环境)里,type 和 data 却是分开的,他们分别由 reflect.Typereflect.Value 来表现。

Go语言反射三定律:

  1. Reflection goes from interface value to reflection object.
  2. Reflection goes from reflection object to interface value.
  3. To modify a reflection object, the value must be settable.
go
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func main() {
+    var a interface{} = 3.14
+
+    fmt.Printf("接口变量的类型为 %T ,值为 %v\n", a, a)
+
+    t := reflect.TypeOf(a)
+    v := reflect.ValueOf(a)
+
+    // 反射第一定律
+    fmt.Printf("从接口变量到反射对象:Type对象类型为 %T\n", t)
+    fmt.Printf("从接口变量到反射对象:Value对象类型为 %T\n", v)
+
+    // 反射第二定律
+    i := v.Interface()
+    fmt.Printf("从反射对象到接口变量:对象类型为 %T,值为 %v\n", i, i)
+    // 使用类型断言进行转换
+    x := v.Interface().(float64)
+    fmt.Printf("x 类型为 %T,值为 %v\n", x, x)
+}
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func main() {
+    var a interface{} = 3.14
+
+    fmt.Printf("接口变量的类型为 %T ,值为 %v\n", a, a)
+
+    t := reflect.TypeOf(a)
+    v := reflect.ValueOf(a)
+
+    // 反射第一定律
+    fmt.Printf("从接口变量到反射对象:Type对象类型为 %T\n", t)
+    fmt.Printf("从接口变量到反射对象:Value对象类型为 %T\n", v)
+
+    // 反射第二定律
+    i := v.Interface()
+    fmt.Printf("从反射对象到接口变量:对象类型为 %T,值为 %v\n", i, i)
+    // 使用类型断言进行转换
+    x := v.Interface().(float64)
+    fmt.Printf("x 类型为 %T,值为 %v\n", x, x)
+}
go
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func main() {
+    var a float64 = 3.14
+    v := reflect.ValueOf(a)
+    fmt.Println("是否可写:", v.CanSet())
+}
+---
+package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func main() {
+    var a float64 = 3.14
+    v := reflect.ValueOf(&a)
+    fmt.Println("是否可写:", v.CanSet())
+}
+---
+package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func main() {
+    var a float64 = 3.14
+    v := reflect.ValueOf(&a).Elem()
+    fmt.Println("是否可写:", v.CanSet())
+
+    v.SetFloat(2)
+    fmt.Println(v)
+}
package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func main() {
+    var a float64 = 3.14
+    v := reflect.ValueOf(a)
+    fmt.Println("是否可写:", v.CanSet())
+}
+---
+package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func main() {
+    var a float64 = 3.14
+    v := reflect.ValueOf(&a)
+    fmt.Println("是否可写:", v.CanSet())
+}
+---
+package main
+
+import (
+    "fmt"
+    "reflect"
+)
+
+func main() {
+    var a float64 = 3.14
+    v := reflect.ValueOf(&a).Elem()
+    fmt.Println("是否可写:", v.CanSet())
+
+    v.SetFloat(2)
+    fmt.Println(v)
+}
+ + + + \ No newline at end of file diff --git a/golang/cli/Cobra.html b/golang/cli/Cobra.html new file mode 100644 index 000000000..e528a7a54 --- /dev/null +++ b/golang/cli/Cobra.html @@ -0,0 +1,393 @@ + + + + + + Cobra | 故事 + + + + + + + + + + + + + + +
Skip to content

Cobra

Cobra是一个能够快速构建cli工具的库,相比于之前用过的Python的argparser模块,Cobra更加强大、灵活,还有自动生成文档等功能。

https://github.com/spf13/cobra/blob/main/site/content/user_guide.md

安装cobra依赖

go get -u github.com/spf13/cobra@latest

安装cobra-cli工具

go install github.com/spf13/cobra-cli@latest

cobra-cli会被安装到GOPATH的bin目录

使用cobra-cli初始化项目

shell
cd cobra-learn
+cobra-cli init                             
+// Your Cobra application is ready at
+// /Users/story/Developer/go/src/cobra-learn
cd cobra-learn
+cobra-cli init                             
+// Your Cobra application is ready at
+// /Users/story/Developer/go/src/cobra-learn

生成的目录结构:

shell
├── LICENSE
+├── cmd
+│   └── root.go
+├── go.mod
+├── go.sum
+└── main.go
├── LICENSE
+├── cmd
+│   └── root.go
+├── go.mod
+├── go.sum
+└── main.go
go
// main.go
+package main
+
+import "cobra-learn/cmd"
+
+func main() {
+	cmd.Execute()
+}
// main.go
+package main
+
+import "cobra-learn/cmd"
+
+func main() {
+	cmd.Execute()
+}
go
// root.go
+package cmd
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "cobra-learn",
+	Short: "A brief description of your application",
+	Long: `A longer description that spans multiple lines and likely contains
+examples and usage of using your application. For example:
+
+Cobra is a CLI library for Go that empowers applications.
+This application is a tool to generate the needed files
+to quickly create a Cobra application.`,
+	// Uncomment the following line if your bare application
+	// has an action associated with it:
+	// Run: func(cmd *cobra.Command, args []string) { },
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	// Here you will define your flags and configuration settings.
+	// Cobra supports persistent flags, which, if defined here,
+	// will be global for your application.
+
+	// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra-learn.yaml)")
+
+	// Cobra also supports local flags, which will only run
+	// when this action is called directly.
+	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}
// root.go
+package cmd
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "cobra-learn",
+	Short: "A brief description of your application",
+	Long: `A longer description that spans multiple lines and likely contains
+examples and usage of using your application. For example:
+
+Cobra is a CLI library for Go that empowers applications.
+This application is a tool to generate the needed files
+to quickly create a Cobra application.`,
+	// Uncomment the following line if your bare application
+	// has an action associated with it:
+	// Run: func(cmd *cobra.Command, args []string) { },
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	// Here you will define your flags and configuration settings.
+	// Cobra supports persistent flags, which, if defined here,
+	// will be global for your application.
+
+	// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra-learn.yaml)")
+
+	// Cobra also supports local flags, which will only run
+	// when this action is called directly.
+	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}

执行命令go run main.go会输出定义的详细描述

shell
  cobra-learn go run main.go                             
+A longer description that spans multiple lines and likely contains
+examples and usage of using your application. For example:
+
+Cobra is a CLI library for Go that empowers applications.
+This application is a tool to generate the needed files
+to quickly create a Cobra application.
  cobra-learn go run main.go                             
+A longer description that spans multiple lines and likely contains
+examples and usage of using your application. For example:
+
+Cobra is a CLI library for Go that empowers applications.
+This application is a tool to generate the needed files
+to quickly create a Cobra application.

给命令添加子命令

shell
  cobra-learn cobra-cli add version
+version created at /Users/story/Developer/go/src/cobra-learn
  cobra-learn cobra-cli add version
+version created at /Users/story/Developer/go/src/cobra-learn

目录结构:

shell
├── LICENSE
+├── cmd
+│   ├── root.go
+│   └── version.go
+├── go.mod
+├── go.sum
+└── main.go
├── LICENSE
+├── cmd
+│   ├── root.go
+│   └── version.go
+├── go.mod
+├── go.sum
+└── main.go
go
// version.go
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+)
+
+// versionCmd represents the version command
+var versionCmd = &cobra.Command{
+	Use:   "version",
+	Short: "A brief description of your command",
+	Long: `A longer description that spans multiple lines and likely contains examples
+and usage of using your command. For example:
+
+Cobra is a CLI library for Go that empowers applications.
+This application is a tool to generate the needed files
+to quickly create a Cobra application.`,
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Println("version called")
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(versionCmd)
+
+	// Here you will define your flags and configuration settings.
+
+	// Cobra supports Persistent Flags which will work for this command
+	// and all subcommands, e.g.:
+	// versionCmd.PersistentFlags().String("foo", "", "A help for foo")
+
+	// Cobra supports local flags which will only run when this command
+	// is called directly, e.g.:
+	// versionCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}
// version.go
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+)
+
+// versionCmd represents the version command
+var versionCmd = &cobra.Command{
+	Use:   "version",
+	Short: "A brief description of your command",
+	Long: `A longer description that spans multiple lines and likely contains examples
+and usage of using your command. For example:
+
+Cobra is a CLI library for Go that empowers applications.
+This application is a tool to generate the needed files
+to quickly create a Cobra application.`,
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Println("version called")
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(versionCmd)
+
+	// Here you will define your flags and configuration settings.
+
+	// Cobra supports Persistent Flags which will work for this command
+	// and all subcommands, e.g.:
+	// versionCmd.PersistentFlags().String("foo", "", "A help for foo")
+
+	// Cobra supports local flags which will only run when this command
+	// is called directly, e.g.:
+	// versionCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}

执行go build编译项目,会在项目根目录生成二进制文件cobra-learn执行该命令:

shell
  cobra-learn ./cobra-learn    
+A longer description that spans multiple lines and likely contains
+examples and usage of using your application. For example:
+
+Cobra is a CLI library for Go that empowers applications.
+This application is a tool to generate the needed files
+to quickly create a Cobra application.
+
+Usage:
+  cobra-learn [command]
+
+Available Commands:
+  completion  Generate the autocompletion script for the specified shell
+  help        Help about any command
+  version     A brief description of your command
+
+Flags:
+  -h, --help     help for cobra-learn
+  -t, --toggle   Help message for toggle
+
+Use "cobra-learn [command] --help" for more information about a command.
  cobra-learn ./cobra-learn    
+A longer description that spans multiple lines and likely contains
+examples and usage of using your application. For example:
+
+Cobra is a CLI library for Go that empowers applications.
+This application is a tool to generate the needed files
+to quickly create a Cobra application.
+
+Usage:
+  cobra-learn [command]
+
+Available Commands:
+  completion  Generate the autocompletion script for the specified shell
+  help        Help about any command
+  version     A brief description of your command
+
+Flags:
+  -h, --help     help for cobra-learn
+  -t, --toggle   Help message for toggle
+
+Use "cobra-learn [command] --help" for more information about a command.

执行cobra-learn version

shell
  cobra-learn ./cobra-learn version
+version called
  cobra-learn ./cobra-learn version
+version called

可以看到调用命令执行的就是Run属性对应的函数

给命令增加flag

go
func init() {
+	rootCmd.AddCommand(versionCmd)
+
+	// Here you will define your flags and configuration settings.
+
+	// Cobra supports Persistent Flags which will work for this command
+	// and all subcommands, e.g.:
+	// versionCmd.PersistentFlags().String("foo", "", "A help for foo")
+
+	// Cobra supports local flags which will only run when this command
+	// is called directly, e.g.:
+	versionCmd.Flags().StringP("ver", "v", "1.0", "版本号")
+}
func init() {
+	rootCmd.AddCommand(versionCmd)
+
+	// Here you will define your flags and configuration settings.
+
+	// Cobra supports Persistent Flags which will work for this command
+	// and all subcommands, e.g.:
+	// versionCmd.PersistentFlags().String("foo", "", "A help for foo")
+
+	// Cobra supports local flags which will only run when this command
+	// is called directly, e.g.:
+	versionCmd.Flags().StringP("ver", "v", "1.0", "版本号")
+}
shell
  cobra-learn go build
+  cobra-learn ./cobra-learn version
+Usage:
+  cobra-learn version [flags]
+
+Flags:
+  -h, --help         help for version
+  -v, --ver string   版本号 (default "1.0")
  cobra-learn go build
+  cobra-learn ./cobra-learn version
+Usage:
+  cobra-learn version [flags]
+
+Flags:
+  -h, --help         help for version
+  -v, --ver string   版本号 (default "1.0")

在Run函数中获取flag

go
Run: func(cmd *cobra.Command, args []string) {
+		ver, _ := cmd.Flags().GetString("ver")
+		fmt.Println(ver)
+}
Run: func(cmd *cobra.Command, args []string) {
+		ver, _ := cmd.Flags().GetString("ver")
+		fmt.Println(ver)
+}
shell
  cobra-learn go build                       
+  cobra-learn ./cobra-learn version --ver 123
+123 # 使用name
+  cobra-learn ./cobra-learn version -v 1234  
+1234 # 使用shorthand
+  cobra-learn ./cobra-learn version        
+1.0 # 不带flag 使用默认值
  cobra-learn go build                       
+  cobra-learn ./cobra-learn version --ver 123
+123 # 使用name
+  cobra-learn ./cobra-learn version -v 1234  
+1234 # 使用shorthand
+  cobra-learn ./cobra-learn version        
+1.0 # 不带flag 使用默认值

修改命令配置

自定义usage输出

可以看到上面输出的./cobra-learn version的uage信息是默认的

shell
Usage:
+  cobra-learn version [flags]
Usage:
+  cobra-learn version [flags]

我们可以通过SetUsageTemplateSetUsageFunc自定义这一内容:

  • SetUsageTemplate
go
func init() {
+	rootCmd.AddCommand(versionCmd)
+	rootCmd.AddCommand(versionCmd)
+	versionCmd.SetUsageTemplate(
+		`Usage: story version [options] <ver>` + "\n" +
+			`版本号` + "\n" +
+			`Options:` + "\n" +
+			`  -h, --help   help for version` + "\n",
+	)
+}
func init() {
+	rootCmd.AddCommand(versionCmd)
+	rootCmd.AddCommand(versionCmd)
+	versionCmd.SetUsageTemplate(
+		`Usage: story version [options] <ver>` + "\n" +
+			`版本号` + "\n" +
+			`Options:` + "\n" +
+			`  -h, --help   help for version` + "\n",
+	)
+}
shell
  cobra-learn go build
+  cobra-learn ./cobra-learn version
+Error: accepts 1 arg(s), received 0
+Usage: story version [options] <ver>
+版本号
+Options:
+  -h, --help   help for version
  cobra-learn go build
+  cobra-learn ./cobra-learn version
+Error: accepts 1 arg(s), received 0
+Usage: story version [options] <ver>
+版本号
+Options:
+  -h, --help   help for version
  • SetUsageFunc
go
func init() {
+	rootCmd.AddCommand(versionCmd)
+	versionCmd.SetUsageFunc(func(cmd *cobra.Command) error {
+		fmt.Println("Usage: story version")
+		return nil
+	})
+}
func init() {
+	rootCmd.AddCommand(versionCmd)
+	versionCmd.SetUsageFunc(func(cmd *cobra.Command) error {
+		fmt.Println("Usage: story version")
+		return nil
+	})
+}
go
➜  cobra-learn go build             
+➜  cobra-learn ./cobra-learn version
+Usage: story version
➜  cobra-learn go build             
+➜  cobra-learn ./cobra-learn version
+Usage: story version

限制arg参数

  • Number of arguments:

    • NoArgs - report an error if there are any positional args.
    • ArbitraryArgs - accept any number of args.
    • MinimumNArgs(int) - report an error if less than N positional args are provided.
    • MaximumNArgs(int) - report an error if more than N positional args are provided.
    • ExactArgs(int) - report an error if there are not exactly N positional args.
    • RangeArgs(min, max) - report an error if the number of args is not between min and max.
  • Content of the arguments:

    • OnlyValidArgs - report an error if there are any positional args not specified in the ValidArgs field of Command, which can optionally be set to a list of valid values for positional args.

例:Args: cobra.ExactArgs(1)

执行不带参数时:

shell
  cobra-learn ./cobra-learn version
+Error: accepts 1 arg(s), received 0 # 提示需要提供一个参数
+Usage: story version
  cobra-learn ./cobra-learn version
+Error: accepts 1 arg(s), received 0 # 提示需要提供一个参数
+Usage: story version
+ + + + \ No newline at end of file diff --git a/golang/index.html b/golang/index.html new file mode 100644 index 000000000..836ca64ec --- /dev/null +++ b/golang/index.html @@ -0,0 +1,25 @@ + + + + + + Golang | 故事 + + + + + + + + + + + + + + +
Skip to content

Golang

  • 基础
  • Web
  • Cli
  • 工具
+ + + + \ No newline at end of file diff --git a/golang/tools/redis-cleaner.html b/golang/tools/redis-cleaner.html new file mode 100644 index 000000000..9081b3cc1 --- /dev/null +++ b/golang/tools/redis-cleaner.html @@ -0,0 +1,167 @@ + + + + + + Redis-cleaner | 故事 + + + + + + + + + + + + + + +
Skip to content

Redis-cleaner

go
package main
+
+import (
+	"context"
+	"fmt"
+	"github.com/redis/go-redis/v9"
+	"log"
+)
+
+func main() {
+	ctx := context.Background()
+	// 创建Redis客户端
+	client := redis.NewClient(&redis.Options{
+		Addr: "localhost:6379",
+		DB:   1,
+	})
+
+	// 定义匹配模式和批量处理大小
+	matchPattern := "*"
+	batchSize := 1000
+
+	// 设置游标初始值和删除计数器
+	startCursor := uint64(0)
+	keysDeleted := 0
+	memSaved := 0
+
+	for {
+		// 扫描Redis中的key
+		keys, cursor, err := client.Scan(ctx, startCursor, matchPattern, int64(batchSize)).Result()
+
+		if err != nil {
+			log.Fatal(err)
+		}
+
+		// 检查每个key的过期时间并删除符合条件的键
+		for _, key := range keys {
+			ttl, err := client.TTL(ctx, key).Result()
+			if err != nil {
+				log.Fatal(err)
+			}
+
+			// 如果过期时间大于15年,则删除该键
+			if ttl.Hours() > 24*365*10 {
+				mem, err := client.MemoryUsage(ctx, key).Result()
+				if err != nil {
+					log.Fatal(err)
+				}
+				err = client.Del(ctx, key).Err()
+				if err != nil {
+					log.Fatal(err)
+				}
+				memSaved += int(mem)
+				keysDeleted++
+			}
+		}
+
+		// 如果游标为0,则表示已完成遍历
+		if cursor == 0 {
+			break
+		}
+		startCursor = cursor
+	}
+
+	fmt.Printf("已删除 %d 个过期时间大于10年的键\n", keysDeleted)
+	fmt.Printf("已释放 %d MB内存\n", memSaved/1024/1024)
+
+	// 关闭Redis客户端连接
+	err := client.Close()
+	if err != nil {
+		log.Fatal(err)
+	}
+}
package main
+
+import (
+	"context"
+	"fmt"
+	"github.com/redis/go-redis/v9"
+	"log"
+)
+
+func main() {
+	ctx := context.Background()
+	// 创建Redis客户端
+	client := redis.NewClient(&redis.Options{
+		Addr: "localhost:6379",
+		DB:   1,
+	})
+
+	// 定义匹配模式和批量处理大小
+	matchPattern := "*"
+	batchSize := 1000
+
+	// 设置游标初始值和删除计数器
+	startCursor := uint64(0)
+	keysDeleted := 0
+	memSaved := 0
+
+	for {
+		// 扫描Redis中的key
+		keys, cursor, err := client.Scan(ctx, startCursor, matchPattern, int64(batchSize)).Result()
+
+		if err != nil {
+			log.Fatal(err)
+		}
+
+		// 检查每个key的过期时间并删除符合条件的键
+		for _, key := range keys {
+			ttl, err := client.TTL(ctx, key).Result()
+			if err != nil {
+				log.Fatal(err)
+			}
+
+			// 如果过期时间大于15年,则删除该键
+			if ttl.Hours() > 24*365*10 {
+				mem, err := client.MemoryUsage(ctx, key).Result()
+				if err != nil {
+					log.Fatal(err)
+				}
+				err = client.Del(ctx, key).Err()
+				if err != nil {
+					log.Fatal(err)
+				}
+				memSaved += int(mem)
+				keysDeleted++
+			}
+		}
+
+		// 如果游标为0,则表示已完成遍历
+		if cursor == 0 {
+			break
+		}
+		startCursor = cursor
+	}
+
+	fmt.Printf("已删除 %d 个过期时间大于10年的键\n", keysDeleted)
+	fmt.Printf("已释放 %d MB内存\n", memSaved/1024/1024)
+
+	// 关闭Redis客户端连接
+	err := client.Close()
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+ + + + \ No newline at end of file diff --git a/golang/web/Gin.html b/golang/web/Gin.html new file mode 100644 index 000000000..2505ef2e5 --- /dev/null +++ b/golang/web/Gin.html @@ -0,0 +1,515 @@ + + + + + + Gin | 故事 + + + + + + + + + + + + + + +
Skip to content

Gin

Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance -- up to 40 times faster. If you need smashing performance, get yourself some Gin.

Gin安装和基本使用

go get -u github.com/gin-gonic/gin

go
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/thinkerou/favicon"
+)
+
+func main() {
+  // 创建服务
+	ginServer := gin.Default()
+	ginServer.Use(favicon.New("./favicon.ico"))
+	ginServer.GET("/", func(c *gin.Context) {
+		c.String(200, "Hello World!")
+	})
+	ginServer.POST("/post", func(c *gin.Context) {
+		c.JSON(200, gin.H{
+			"message": "POST Data",
+		})
+	})
+
+	_ = ginServer.Run(":8080")
+
+}
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/thinkerou/favicon"
+)
+
+func main() {
+  // 创建服务
+	ginServer := gin.Default()
+	ginServer.Use(favicon.New("./favicon.ico"))
+	ginServer.GET("/", func(c *gin.Context) {
+		c.String(200, "Hello World!")
+	})
+	ginServer.POST("/post", func(c *gin.Context) {
+		c.JSON(200, gin.H{
+			"message": "POST Data",
+		})
+	})
+
+	_ = ginServer.Run(":8080")
+
+}
  • curl -X GET http://localhost:8080

    • Hello World!
  • curl -X POST http://localhost:8080/post

    • {"message":"POST Data"}

返回一个静态页

go
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/thinkerou/favicon"
+)
+
+func main() {
+  // 创建服务
+	ginServer := gin.Default()
+  // 设置favicon
+	ginServer.Use(favicon.New("./favicon.ico"))
+
+	// 加载静态页
+	ginServer.LoadHTMLGlob("templates/*")
+	// 响应页面给前端
+	ginServer.GET("/index", func(context *gin.Context) {
+		context.HTML(200, "index.html", gin.H{
+			"title": "Main website",
+		})
+	})
+	_ = ginServer.Run(":8080")
+}
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/thinkerou/favicon"
+)
+
+func main() {
+  // 创建服务
+	ginServer := gin.Default()
+  // 设置favicon
+	ginServer.Use(favicon.New("./favicon.ico"))
+
+	// 加载静态页
+	ginServer.LoadHTMLGlob("templates/*")
+	// 响应页面给前端
+	ginServer.GET("/index", func(context *gin.Context) {
+		context.HTML(200, "index.html", gin.H{
+			"title": "Main website",
+		})
+	})
+	_ = ginServer.Run(":8080")
+}

访问localhost:8080localhost:8080/index

静态页

加载资源文件

go
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/thinkerou/favicon"
+)
+
+func main() {
+  // 创建服务
+	ginServer := gin.Default()
+  // 设置favicon
+	ginServer.Use(favicon.New("./favicon.ico"))
+
+	// 加载静态页
+	ginServer.LoadHTMLGlob("templates/*")
+	// 响应页面给前端
+	ginServer.GET("/index", func(context *gin.Context) {
+		context.HTML(200, "index.html", gin.H{
+			"title": "Main website",
+		})
+	})
+	_ = ginServer.Run(":8080")
+}
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/thinkerou/favicon"
+)
+
+func main() {
+  // 创建服务
+	ginServer := gin.Default()
+  // 设置favicon
+	ginServer.Use(favicon.New("./favicon.ico"))
+
+	// 加载静态页
+	ginServer.LoadHTMLGlob("templates/*")
+	// 响应页面给前端
+	ginServer.GET("/index", func(context *gin.Context) {
+		context.HTML(200, "index.html", gin.H{
+			"title": "Main website",
+		})
+	})
+	_ = ginServer.Run(":8080")
+}

加载资源文件

Restful API

Query参数

go
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func main() {
+	ginServer := gin.Default()
+
+
+	ginServer.GET("/user/info", func(context *gin.Context) {
+		userId := context.Query("userId")
+		userName := context.Query("userName")
+		context.JSON(http.StatusOK, gin.H{
+			"userId":   userId,
+			"userName": userName,
+		})
+	})
+
+	_ = ginServer.Run(":8080")
+}
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func main() {
+	ginServer := gin.Default()
+
+
+	ginServer.GET("/user/info", func(context *gin.Context) {
+		userId := context.Query("userId")
+		userName := context.Query("userName")
+		context.JSON(http.StatusOK, gin.H{
+			"userId":   userId,
+			"userName": userName,
+		})
+	})
+
+	_ = ginServer.Run(":8080")
+}
  • curl -X GET 'http://localhost:8080/user/info?userId=123&userName=小明'
    • {"userId":"123","userName":"小明"}

Body参数

go
package main
+
+import (
+	"encoding/json"
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func main() {
+	ginServer := gin.Default()
+
+	ginServer.POST("/user", func(context *gin.Context) {
+    // request body []byte, err
+		body, _ := context.GetRawData()
+    // 包装为map类型
+		var m map[string]interface{}
+		_ = json.Unmarshal(body, &m)
+
+		context.JSON(http.StatusOK, m)
+	})
+
+	_ = ginServer.Run(":8080")
+}
package main
+
+import (
+	"encoding/json"
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func main() {
+	ginServer := gin.Default()
+
+	ginServer.POST("/user", func(context *gin.Context) {
+    // request body []byte, err
+		body, _ := context.GetRawData()
+    // 包装为map类型
+		var m map[string]interface{}
+		_ = json.Unmarshal(body, &m)
+
+		context.JSON(http.StatusOK, m)
+	})
+
+	_ = ginServer.Run(":8080")
+}
  • curl -X POST 'http://127.0.0.1:8080/user' --header 'Content-Type: application/json' --data '{"userName": "张三"}'
    • {"userName":"张三"}

表单参数

html
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Title</title>
+    <link rel="stylesheet" href="/static/base.css">
+</head>
+<body>
+<h1>Hello World!</h1>
+
+<form action="/user/add" method="post">
+    <input type="text" name="username">
+    <input type="password" name="password">
+
+    <button type="submit">提交</button>
+</form>
+<script src="/static/base.js"></script>
+</body>
+</html>
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Title</title>
+    <link rel="stylesheet" href="/static/base.css">
+</head>
+<body>
+<h1>Hello World!</h1>
+
+<form action="/user/add" method="post">
+    <input type="text" name="username">
+    <input type="password" name="password">
+
+    <button type="submit">提交</button>
+</form>
+<script src="/static/base.js"></script>
+</body>
+</html>
go
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func main() {
+	ginServer := gin.Default()
+
+	ginServer.LoadHTMLGlob("templates/*")
+
+	ginServer.GET("/index", func(context *gin.Context) {
+		context.HTML(http.StatusOK, "index.html", gin.H{
+			"title": "Hello World",
+		})
+	})
+
+	ginServer.POST("/user/add", func(context *gin.Context) {
+		username := context.PostForm("username")
+		password := context.PostForm("password")
+
+		context.JSON(http.StatusOK, gin.H{
+			"msg": "success",
+			"data": gin.H{
+				"username": username,
+				"password": password,
+			},
+		})
+	})
+
+	_ = ginServer.Run(":8080")
+}
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func main() {
+	ginServer := gin.Default()
+
+	ginServer.LoadHTMLGlob("templates/*")
+
+	ginServer.GET("/index", func(context *gin.Context) {
+		context.HTML(http.StatusOK, "index.html", gin.H{
+			"title": "Hello World",
+		})
+	})
+
+	ginServer.POST("/user/add", func(context *gin.Context) {
+		username := context.PostForm("username")
+		password := context.PostForm("password")
+
+		context.JSON(http.StatusOK, gin.H{
+			"msg": "success",
+			"data": gin.H{
+				"username": username,
+				"password": password,
+			},
+		})
+	})
+
+	_ = ginServer.Run(":8080")
+}

路由

go
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func main() {
+	ginServer := gin.Default()
+
+	ginServer.LoadHTMLGlob("templates/*")
+  // 重定向到首页
+	ginServer.GET("/", func(context *gin.Context) {
+		context.Redirect(http.StatusMovedPermanently, "/index")
+	})
+
+	ginServer.GET("/index", func(context *gin.Context) {
+		context.HTML(http.StatusOK, "index.html", gin.H{
+			"title": "Hello World",
+		})
+	})
+  // 404页面
+  ginServer.NoRoute(func(context *gin.Context) {
+		context.HTML(http.StatusNotFound, "404.html", gin.H{
+			"title": "404",
+		})
+	})
+
+	_ = ginServer.Run(":8080")
+}
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func main() {
+	ginServer := gin.Default()
+
+	ginServer.LoadHTMLGlob("templates/*")
+  // 重定向到首页
+	ginServer.GET("/", func(context *gin.Context) {
+		context.Redirect(http.StatusMovedPermanently, "/index")
+	})
+
+	ginServer.GET("/index", func(context *gin.Context) {
+		context.HTML(http.StatusOK, "index.html", gin.H{
+			"title": "Hello World",
+		})
+	})
+  // 404页面
+  ginServer.NoRoute(func(context *gin.Context) {
+		context.HTML(http.StatusNotFound, "404.html", gin.H{
+			"title": "404",
+		})
+	})
+
+	_ = ginServer.Run(":8080")
+}

路由组

go
package main
+
+import (
+	"github.com/gin-gonic/gin"
+)
+
+func main() {
+	ginServer := gin.Default()
+
+	userGroup := ginServer.Group("/user")
+	{
+		userGroup.GET("/get", func(c *gin.Context) {
+			c.JSON(200, gin.H{
+				"message": "get user",
+			})
+		})
+		userGroup.POST("/post", func(c *gin.Context) {
+			c.JSON(200, gin.H{
+				"message": "post user",
+			})
+		})
+	}
+
+	_ = ginServer.Run(":8080")
+}
package main
+
+import (
+	"github.com/gin-gonic/gin"
+)
+
+func main() {
+	ginServer := gin.Default()
+
+	userGroup := ginServer.Group("/user")
+	{
+		userGroup.GET("/get", func(c *gin.Context) {
+			c.JSON(200, gin.H{
+				"message": "get user",
+			})
+		})
+		userGroup.POST("/post", func(c *gin.Context) {
+			c.JSON(200, gin.H{
+				"message": "post user",
+			})
+		})
+	}
+
+	_ = ginServer.Run(":8080")
+}

自定义中间件 拦截器

go
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"log"
+)
+
+func myHandler() gin.HandlerFunc {
+	return func(context *gin.Context) {
+		// do something
+		context.Set("name", "zhangsan")
+		context.Next() // 放行
+		// context.Abort() 阻止
+	}
+}
+
+func main() {
+	ginServer := gin.Default()
+
+	userGroup := ginServer.Group("/user")
+	{
+		userGroup.GET("/get", myHandler(), func(c *gin.Context) {
+			// 获取拦截器里设置的值
+			name := c.MustGet("name").(string)
+			log.Println(name)
+			c.JSON(200, gin.H{
+				"message": "get user",
+			})
+		})
+	}
+
+	_ = ginServer.Run(":8080")
+}
+// 2023/07/01 21:36:53 zhangsan
package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"log"
+)
+
+func myHandler() gin.HandlerFunc {
+	return func(context *gin.Context) {
+		// do something
+		context.Set("name", "zhangsan")
+		context.Next() // 放行
+		// context.Abort() 阻止
+	}
+}
+
+func main() {
+	ginServer := gin.Default()
+
+	userGroup := ginServer.Group("/user")
+	{
+		userGroup.GET("/get", myHandler(), func(c *gin.Context) {
+			// 获取拦截器里设置的值
+			name := c.MustGet("name").(string)
+			log.Println(name)
+			c.JSON(200, gin.H{
+				"message": "get user",
+			})
+		})
+	}
+
+	_ = ginServer.Run(":8080")
+}
+// 2023/07/01 21:36:53 zhangsan
+ + + + \ No newline at end of file diff --git a/hashmap.json b/hashmap.json new file mode 100644 index 000000000..7ce6ffbae --- /dev/null +++ b/hashmap.json @@ -0,0 +1 @@ +{"actions_env_docker_jenkins_gitee自动化部署vue项目.md":"b6d298c3","docker_dockerfile语法.md":"7c5d66d7","actions_env_docker容器内访问macos宿主机中的kafka.md":"375de9c2","docker_docker-compose语法.md":"5b63270f","docker_常用指令.md":"6b637e11","docker_index.md":"f95bad4d","actions_env_git配置多ssh-key __ gitee 和 github 同步更新.md":"e089c9d0","frontend_base_nodejs.md":"a3da6ed0","actions_tools_typora、picgo、七牛云实现markdown图片自动上传图床.md":"6002b22d","actions_tools_git命令整理.md":"853e93cd","actions_tools_iterm2配合oh-my-zsh配置个性主题终端.md":"f87cdce3","actions_tools_iterm2配置ssh快速连接.md":"3487ddd6","actions_tools_linux设置macos时间机器server.md":"3c94d4f3","actions_index.md":"26d63a13","actions_tools_powershell美化.md":"d9be26ce","actions_tools_各系统下校验文件一致性.md":"98a2c898","actions_env_windows开机启动wsl.md":"2ecc3382","java_framework_分布式定时任务解决方案xxl-job.md":"5669afc6","frontend_base_webpack.md":"f49018f3","frontend_index.md":"01cb292c","frontend_base_jquery.md":"4056042a","actions_tools_哪吒探针页面美化.md":"852dab4f","frontend_others_swiftui入门.md":"f738d450","golang_base_gotemplate.md":"26e53b7c","frontend_base_css.md":"9a8416e3","frontend_base_html.md":"bd02116d","actions_env_git配置socks5代理解决github上down代码慢的问题.md":"bfa9056c","actions_env_使用github actions进行持续部署.md":"01116c37","actions_env_macos开启终端的代理.md":"323e2f5e","actions_tools_book-searcher电子书镜像.md":"c0cc3ed0","actions_env_mysql启动报错排查及处理.md":"e9f9fd1d","golang_index.md":"56aeaf33","frontend_others_hackingwithswift-2.md":"85e4f87a","golang_web_gin.md":"a83e3304","golang_cli_cobra.md":"d15fd694","golang_tools_redis-cleaner.md":"c2849590","java_database_otter实现数据全量增量同步.md":"892bc0fe","java_base_string类的深入学习.md":"d1707e3c","java_framework_configurationproperties注解.md":"8a9eff27","java_devtool_maven的生命周期.md":"e9b078b3","java_framework_javaspi机制和springboot自动装配原理.md":"6a5d7c18","java_base_java8新特性回顾.md":"771d4e14","java_framework_spring cloud config接入.md":"d19b8abd","java_database_mysql索引.md":"46f173cb","actions_env_macos开机自动执行脚本.md":"6cf6a3a7","actions_designpattern_责任链模式.md":"952322e1","java_devtool_nexus无法下载依赖的问题记录.md":"647fa7c0","java_framework_logback自定义.md":"9e87748a","java_framework_spring配置和条件化组件加载注解.md":"e8c07bfa","java_framework_dubbo接口超时配置.md":"ba4117be","linux_applications_nginx配置.md":"cec7f5cb","java_framework_swagger_knife4j.md":"31965f41","linux_applications_rclone.md":"b98100d3","linux_applications_iptables.md":"4a6d73d9","linux_applications_screen的进阶用法.md":"7103b3aa","linux_env_archlinux安装.md":"991f6f32","linux_env_linux常用指令.md":"d450f692","linux_env_linux服务器文件目录共享映射配置.md":"2e59da1d","linux_env_centos7安装python3环境.md":"92d82d76","linux_env_linux私钥登陆提示server refused our key.md":"23bf3202","linux_env_linux访问权限控制之acl.md":"66efcc69","linux_env_bash常用的快捷键.md":"7a89f167","linux_env_linux设置swap空间.md":"84a44d7f","linux_env_centos7防火墙命令.md":"15b22e4a","linux_env_ubuntu-server.md":"b53d8eca","linux_index.md":"2f33c5bb","python_base_poetry.md":"86dc5eff","linux_env_服务器启用ssh密钥登录并禁用密码登录.md":"8b926ddc","linux_env_链接和别名(ln、alias).md":"4f5e25d9","linux_env_从零搭建linux虚拟机环境.md":"c536b8a7","linux_hardware_linux磁盘操作相关.md":"0fc149c9","golang_base_golang基础语法.md":"5d06c4a6","python_crawler_scrapy框架入门.md":"74ea41f3","python_base_python基础语法.md":"2ec54814","python_crawler_selenium模块.md":"5446e227","python_crawler_分布式爬虫和增量式爬虫.md":"1722f14e","tinker_network_frp内网穿透.md":"7473690d","tinker_network_openwrt开启ipv6.md":"edcbc94f","python_web_django入门.md":"7b29ab40","python_crawler_scrapy进阶.md":"9a37e129","python_others_argparse模块入门.md":"9f5f1e90","tinker_network_移动光猫改桥接模式.md":"dd786d69","tinker_vm_pve异常关机后磁盘检查处理.md":"ad24a8cd","python_crawler_增量式爬虫实践案例 下载指定b站up主的所有作品.md":"6ec9ffd2","tinker_vm_vmware虚拟机的几种网络连接模式.md":"86cddc5f","python_others_使用pandas模块进行数据处理.md":"fa389c23","tinker_vm_安装pve虚拟机并在pve安装truenas.md":"b6416835","java_framework_后端允许跨域配置.md":"dcf47fae","python_crawler_requests模块入门.md":"5ba42074","java_framework_自定义mybatisplusgenerator.md":"cb2de2f5","java_index.md":"a76c7870","java_middleware_kafka学习记录.md":"2b882b43","java_middleware_kafka常用命令记录.md":"3a75876e","java_middleware_kakfa实践.md":"012f67f9","java_middleware_关于消息中间件mq.md":"68363b78","java_others_openjdk没有jstack等命令的解决办法.md":"249c1c70","tinker_network_home server搭建.md":"862c471d","java_others_poi踩坑-zip file is closed.md":"0fc74901","java_others_cpu占用率高排查思路.md":"e4a204f9","java_middleware_kakfa重复消费问题处理.md":"e0988449","java_middleware_分布式锁解决方案.md":"6bed79a9","actions_env_windows下docker desktop安装.md":"2d631526","docker_docker网络之macvlan.md":"15dfe39b","java_others_微信小程序加密数据对称解密工具类.md":"41297ec5","linux_applications_linux中使用selenium.md":"918cfe65","linux_applications_grafana.md":"27b9670d","actions_designpattern_策略模式的具体实现.md":"b828e5be","linux_applications_ffmpeg相关.md":"fa57cc0e","frontend_others_swift语法.md":"9c4e5628","frontend_base_typescript.md":"47bd8e1c","index.md":"a91419bd","java_framework_使用spring validation进行参数校验.md":"eb10cc89","java_database_sql优化学习.md":"84dffe0e","frontend_framework_vue.md":"98b6e4a2","java_framework_java日志发展历史.md":"f427bc3e","java_framework_springmvc的执行流程源码分析.md":"ca08bf1a","frontend_base_javascript.md":"5b22eba4","java_framework_mybatis generator配置.md":"c35e48c8","java_framework_spring-session实现集群session共享.md":"4d9b54de","frontend_others_elementplus el-upload源码分析.md":"6309dbd0","actions_tools_markdown基础语法.md":"9b7f7a0a","java_framework_使用easyexcel导出excel.md":"6234940e","actions_tools_sketch.md":"b25b7913","java_framework_springcloud优雅下线服务.md":"6feadc2c","java_framework_netty_websocket实现即时通讯功能.md":"955a4962","java_base_jdk1.8hashmap源码学习.md":"9d40a87c","python_web_pymysql使用.md":"b054a10e","python_others_切换windows代理设置开关.md":"88db6435","python_crawler_多线程爬取梨视频网站的热门视频.md":"0cea7349","tinker_network_使用https访问内网服务.md":"43b6af56","python_crawler_验证码识别和模拟登录.md":"042432f0","tinker_network_ubuntu-server开启网络唤醒.md":"70e29fdd","tinker_network_山特ups配合nut实现断电安全关机.md":"a93a1b0f","tinker_index.md":"b79e9a67","tinker_network_openwrt安装及配置.md":"55152baa","python_others_youtube-upload.md":"3d98cb59","java_framework_poi事件模式解析并读取excel文件数据.md":"ecde083a","python_crawler_crawlspider全站爬取.md":"914da3d0","python_base_装饰器深入.md":"6df2bfe7","python_crawler_python配合ffmpeg下载bilibili视频.md":"89478ae1","python_index.md":"700873bd","tinker_network_pt下载入门.md":"0f6b065f","python_base_python并发编程.md":"d5a5de0a","python_others_alfred插件-快速使用编辑器打开指定文件.md":"056bae34","python_crawler_小红书爬虫.md":"ef9e7902","tinker_network_windows挂载webdav的问题处理.md":"3ad32a6e","java_others_feign请求导致的用户ip获取问题记录.md":"f55418cc","python_others_读取excel_ssh通道连接rds更新数据.md":"3c80a264","java_others_ribbon刷新服务列表间隔和canal的坑.md":"98a4b036","java_others_mybatis的classpath配置导致的jar包读取问题.md":"6a44f13e","java_others_linux服务器安装openoffice踩坑.md":"aab490ac","linux_applications_clash.md":"57ceb8e9","linux_applications_canal部署.md":"c251b94e","frontend_others_hackingwithswift-1.md":"355cc977"} diff --git a/index.html b/index.html new file mode 100644 index 000000000..08349ede7 --- /dev/null +++ b/index.html @@ -0,0 +1,25 @@ + + + + + + 故事 | 故事 + + + + + + + + + + + + + + +
Skip to content

故事

Document

🚀 热爱,是所有的理由和答案

Story
+ + + + \ No newline at end of file diff --git "a/java/base/JDK1.8HashMap\346\272\220\347\240\201\345\255\246\344\271\240.html" "b/java/base/JDK1.8HashMap\346\272\220\347\240\201\345\255\246\344\271\240.html" new file mode 100644 index 000000000..0beb014e8 --- /dev/null +++ "b/java/base/JDK1.8HashMap\346\272\220\347\240\201\345\255\246\344\271\240.html" @@ -0,0 +1,593 @@ + + + + + + JDK1.8 HashMap源码学习 | 故事 + + + + + + + + + + + + + + +
Skip to content

JDK1.8 HashMap源码学习

HashMap

HashMap是开发中最常用的容器之一,它也是Java集合框架中极为重要的组成部分,HashMap实现了Map接口,并继承 AbstractMap 抽象类

java
public class HashMap<K,V> extends AbstractMap<K,V>
+    implements Map<K,V>, Cloneable, Serializable {
+	***    
+}
public class HashMap<K,V> extends AbstractMap<K,V>
+    implements Map<K,V>, Cloneable, Serializable {
+	***    
+}

首先了解下HashMap的几个字段

java
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认的初始容量
+static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量
+static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认负载因子
+transient Node<K,V>[] table; //哈希桶数组
+transient int size; //容器中键值对的个数
+transient int modCount;	//容器内部结构发生变化的次数,主要用于迭代的快速失败
+int threshold; //阈值 table * loadFactor size超过这个值就会扩容
+final float loadFactor; //负载因子 默认0.75
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认的初始容量
+static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量
+static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认负载因子
+transient Node<K,V>[] table; //哈希桶数组
+transient int size; //容器中键值对的个数
+transient int modCount;	//容器内部结构发生变化的次数,主要用于迭代的快速失败
+int threshold; //阈值 table * loadFactor size超过这个值就会扩容
+final float loadFactor; //负载因子 默认0.75

Node的结构

java
static class Node<K,V> implements Map.Entry<K,V> {
+    final int hash;
+    final K key;
+    V value;
+    Node<K,V> next;
+
+    Node(int hash, K key, V value, Node<K,V> next) {
+        this.hash = hash;
+        this.key = key;
+        this.value = value;
+        this.next = next;
+    }
+
+    public final K getKey()        { return key; }
+    public final V getValue()      { return value; }
+    public final String toString() { return key + "=" + value; }
+
+    public final int hashCode() {
+        return Objects.hashCode(key) ^ Objects.hashCode(value);
+    }
+
+    public final V setValue(V newValue) {
+        V oldValue = value;
+        value = newValue;
+        return oldValue;
+    }
+
+    public final boolean equals(Object o) {
+        if (o == this)
+            return true;
+        if (o instanceof Map.Entry) {
+            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
+            if (Objects.equals(key, e.getKey()) &&
+                Objects.equals(value, e.getValue()))
+                return true;
+        }
+        return false;
+    }
+}
static class Node<K,V> implements Map.Entry<K,V> {
+    final int hash;
+    final K key;
+    V value;
+    Node<K,V> next;
+
+    Node(int hash, K key, V value, Node<K,V> next) {
+        this.hash = hash;
+        this.key = key;
+        this.value = value;
+        this.next = next;
+    }
+
+    public final K getKey()        { return key; }
+    public final V getValue()      { return value; }
+    public final String toString() { return key + "=" + value; }
+
+    public final int hashCode() {
+        return Objects.hashCode(key) ^ Objects.hashCode(value);
+    }
+
+    public final V setValue(V newValue) {
+        V oldValue = value;
+        value = newValue;
+        return oldValue;
+    }
+
+    public final boolean equals(Object o) {
+        if (o == this)
+            return true;
+        if (o instanceof Map.Entry) {
+            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
+            if (Objects.equals(key, e.getKey()) &&
+                Objects.equals(value, e.getValue()))
+                return true;
+        }
+        return false;
+    }
+}

Node(JDK1.8之前叫Entry)是HashMap的静态内部类,实现了Map.Entry接口,包括了hash,key,value和下一节点next四个属性,是构成哈希表的基石,是哈希表存储的元素的具体形式。

构造方法

java
public HashMap(int initialCapacity, float loadFactor) {
+    if (initialCapacity < 0)
+        throw new IllegalArgumentException("Illegal initial capacity: " +
+                                           initialCapacity);
+    if (initialCapacity > MAXIMUM_CAPACITY)
+        initialCapacity = MAXIMUM_CAPACITY;
+    if (loadFactor <= 0 || Float.isNaN(loadFactor))
+        throw new IllegalArgumentException("Illegal load factor: " +
+                                           loadFactor);
+    this.loadFactor = loadFactor;
+    this.threshold = tableSizeFor(initialCapacity);
+}
+=====================================================================
+public HashMap(int initialCapacity) {
+    this(initialCapacity, DEFAULT_LOAD_FACTOR);
+}
+=====================================================================
+public HashMap() {
+   this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
+}
+=====================================================================
+public HashMap(Map<? extends K, ? extends V> m) {
+    this.loadFactor = DEFAULT_LOAD_FACTOR;
+    putMapEntries(m, false);
+}
public HashMap(int initialCapacity, float loadFactor) {
+    if (initialCapacity < 0)
+        throw new IllegalArgumentException("Illegal initial capacity: " +
+                                           initialCapacity);
+    if (initialCapacity > MAXIMUM_CAPACITY)
+        initialCapacity = MAXIMUM_CAPACITY;
+    if (loadFactor <= 0 || Float.isNaN(loadFactor))
+        throw new IllegalArgumentException("Illegal load factor: " +
+                                           loadFactor);
+    this.loadFactor = loadFactor;
+    this.threshold = tableSizeFor(initialCapacity);
+}
+=====================================================================
+public HashMap(int initialCapacity) {
+    this(initialCapacity, DEFAULT_LOAD_FACTOR);
+}
+=====================================================================
+public HashMap() {
+   this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
+}
+=====================================================================
+public HashMap(Map<? extends K, ? extends V> m) {
+    this.loadFactor = DEFAULT_LOAD_FACTOR;
+    putMapEntries(m, false);
+}

HashMap的初始容量和负载因子是影响map性能的关键参数,容量指的是table数组的大小,负载因子是衡量哈希表使用程度的一个尺度,负载因子越小,那哈希表空间的利用率就越低,造成的空间浪费就越严重。而如果负载因子越大,哈希表的利用率越高,带来的问题就是查找效率的下降。0.75是对空间和时间成本的折衷选择,一般情况下无需修改。

HashMap的数据结构

  • hash的概念:把任意长度的输入,通过hash算法,转换成相同长度的输出(通常为整型)。

  • HashMap的数据结构:

    HashMap的底层数据结构结合了数组和链表(JDK1.8之前),实际上就是一个链表数组,也就是上文提到的Node<K,V> table[]。我们都知道数组的优势是查找快,增删慢,而链表则是增删快,查找慢,而这种链表数组——拉链法,结合了二者的优势,查找快,增删也快。查找元素的时间复杂度为O(1+n),n为链表的长度。在JDK1.8之后,为了解决哈希冲突频繁的问题,在原来的基础上又引入了红黑树,当链表长度超过8时,链表便会转化为红黑树结构,查找的时间复杂度从O(1+n)变成了O(1+lgn),大大提高了查询效率,不必再遍历链表。

  • JDK1.8之前的HashMap结构 Snipaste_20200608_234022.png

  • JDK1.8的HashMap结构 Snipaste_20200608_234101.png

HashMap的功能

HashMap中的方法很多,这里主要从hash,put,get,resize几个点深入研究

hash

java
static final int hash(Object key) {
+    int h;
+    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
+}
static final int hash(Object key) {
+    int h;
+    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
+}

首先,我们可以发现HashMap是允许null键的,而HashTable则不支持。

hash方法的本质是对key.hashCode()key.hashCode()>>>16进行异或运算,>>>为无符号右移运算符,就是将补码右移后高位补0。^异或是参与运算的数每一位上的数字对比,相同结果为0,不同结果为1。

所以hash方法的功能就是对高16bit和低16bit进行异或运算来减少碰撞。

put方法

java
public V put(K key, V value) {
+    return putVal(hash(key), key, value, false, true);
+}
public V put(K key, V value) {
+    return putVal(hash(key), key, value, false, true);
+}

putVal方法

java
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
+               boolean evict) {
+    Node<K,V>[] tab; Node<K,V> p; int n, i;
+    //如果table为空或者为初始化则进行初始化
+    if ((tab = table) == null || (n = tab.length) == 0)
+        n = (tab = resize()).length;
+    //计算插入的索引值(n- 1) & hash,如果数组中这个索引位置为空 则直接插入,next指针指向为null
+    if ((p = tab[i = (n - 1) & hash]) == null)
+        tab[i] = newNode(hash, key, value, null);
+    else {
+        //如果key已经存在了,直接覆盖value
+        Node<K,V> e; K k;
+        if (p.hash == hash &&
+            ((k = p.key) == key || (key != null && key.equals(k))))
+            //把第一个元素赋值给e记录
+            e = p;
+        //如果是红黑树
+        else if (p instanceof TreeNode)
+            //插入红黑树
+            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
+        else {
+            //如果为链表,进行遍历直到下一个节点为null或者key存在
+            for (int binCount = 0; ; ++binCount) {
+                if ((e = p.next) == null) {
+                    //插入链表最末端
+                    p.next = newNode(hash, key, value, null);
+                    //如果链表长度达到阈值 转换为红黑树
+                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
+                        treeifyBin(tab, hash);
+                    break;
+                }
+                //判断链表中的节点key与即将插入的key是否相同
+                if (e.hash == hash &&
+                    ((k = e.key) == key || (key != null && key.equals(k))))
+                    break;
+                //和前面的e = p.next对应 用来遍历链表
+                p = e;
+            }
+        }
+        //如果key存在,则覆盖原来的value,返回oldValue
+        if (e != null) { // existing mapping for key
+            V oldValue = e.value;
+            if (!onlyIfAbsent || oldValue == null)
+                e.value = value;
+            //访问后的回调函数
+            afterNodeAccess(e);
+            return oldValue;
+        }
+    }
+    //记录结构修改
+    ++modCount;
+    //如果哈希表中的键值对数量达到阈值,进行扩容
+    if (++size > threshold)
+        resize();
+    //插入后的回调函数
+    afterNodeInsertion(evict);
+    return null;
+}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
+               boolean evict) {
+    Node<K,V>[] tab; Node<K,V> p; int n, i;
+    //如果table为空或者为初始化则进行初始化
+    if ((tab = table) == null || (n = tab.length) == 0)
+        n = (tab = resize()).length;
+    //计算插入的索引值(n- 1) & hash,如果数组中这个索引位置为空 则直接插入,next指针指向为null
+    if ((p = tab[i = (n - 1) & hash]) == null)
+        tab[i] = newNode(hash, key, value, null);
+    else {
+        //如果key已经存在了,直接覆盖value
+        Node<K,V> e; K k;
+        if (p.hash == hash &&
+            ((k = p.key) == key || (key != null && key.equals(k))))
+            //把第一个元素赋值给e记录
+            e = p;
+        //如果是红黑树
+        else if (p instanceof TreeNode)
+            //插入红黑树
+            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
+        else {
+            //如果为链表,进行遍历直到下一个节点为null或者key存在
+            for (int binCount = 0; ; ++binCount) {
+                if ((e = p.next) == null) {
+                    //插入链表最末端
+                    p.next = newNode(hash, key, value, null);
+                    //如果链表长度达到阈值 转换为红黑树
+                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
+                        treeifyBin(tab, hash);
+                    break;
+                }
+                //判断链表中的节点key与即将插入的key是否相同
+                if (e.hash == hash &&
+                    ((k = e.key) == key || (key != null && key.equals(k))))
+                    break;
+                //和前面的e = p.next对应 用来遍历链表
+                p = e;
+            }
+        }
+        //如果key存在,则覆盖原来的value,返回oldValue
+        if (e != null) { // existing mapping for key
+            V oldValue = e.value;
+            if (!onlyIfAbsent || oldValue == null)
+                e.value = value;
+            //访问后的回调函数
+            afterNodeAccess(e);
+            return oldValue;
+        }
+    }
+    //记录结构修改
+    ++modCount;
+    //如果哈希表中的键值对数量达到阈值,进行扩容
+    if (++size > threshold)
+        resize();
+    //插入后的回调函数
+    afterNodeInsertion(evict);
+    return null;
+}

流程梳理:

  1. 计算key的hash值 (h = key.hashCode() ^ h >>> 16)

  2. 根据hash值和table数组的长度n计算插入table数组的索引,(n - 1) & hash。由于n始终是2的n次方(为什么后面会介绍),所以(n - 1) & hash 相等于 hash % n ,但是位运算要比取模效率更高

  3. 多种情况

    • 如果该位置没有数据,新生成一个节点保存新数据,返回null

    • 如果该位置有数据且是红黑树结构,那么执行相应的插入 / 更新操作;

    • 如果该位置有数据且是链表

      • 该链表没有这个节点,采用尾插法新增节点保存新数据,返回null
      • 链表上有这个节点,比较key.hash是否一致,一致则覆盖value,返回oldValue

流程图: Snipaste_20200620_163737.png

get

java
public V get(Object key) {
+    Node<K,V> e;
+    return (e = getNode(hash(key), key)) == null ? null : e.value;
+}
+
+final Node<K,V> getNode(int hash, Object key) {
+    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
+    // 1. 定位键值对所在桶的位置
+    if ((tab = table) != null && (n = tab.length) > 0 &&
+        (first = tab[(n - 1) & hash]) != null) {
+        if (first.hash == hash && // always check first node
+            ((k = first.key) == key || (key != null && key.equals(k))))
+            return first;
+        if ((e = first.next) != null) {
+            // 2. 如果 first 是 TreeNode 类型,则调用黑红树查找方法
+            if (first instanceof TreeNode)
+                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
+                
+            // 2. 对链表进行查找
+            do {
+                if (e.hash == hash &&
+                    ((k = e.key) == key || (key != null && key.equals(k))))
+                    return e;
+            } while ((e = e.next) != null);
+        }
+    }
+    return null;
+}
public V get(Object key) {
+    Node<K,V> e;
+    return (e = getNode(hash(key), key)) == null ? null : e.value;
+}
+
+final Node<K,V> getNode(int hash, Object key) {
+    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
+    // 1. 定位键值对所在桶的位置
+    if ((tab = table) != null && (n = tab.length) > 0 &&
+        (first = tab[(n - 1) & hash]) != null) {
+        if (first.hash == hash && // always check first node
+            ((k = first.key) == key || (key != null && key.equals(k))))
+            return first;
+        if ((e = first.next) != null) {
+            // 2. 如果 first 是 TreeNode 类型,则调用黑红树查找方法
+            if (first instanceof TreeNode)
+                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
+                
+            // 2. 对链表进行查找
+            do {
+                if (e.hash == hash &&
+                    ((k = e.key) == key || (key != null && key.equals(k))))
+                    return e;
+            } while ((e = e.next) != null);
+        }
+    }
+    return null;
+}

resize

java
final Node<K,V>[] resize() {
+        //oldTable指向当前hash桶数组
+        Node<K,V>[] oldTab = table;
+        int oldCap = (oldTab == null) ? 0 : oldTab.length;
+        int oldThr = threshold;
+        int newCap, newThr = 0;
+        if (oldCap > 0) {//如果旧的hash桶不为空
+            if (oldCap >= MAXIMUM_CAPACITY) {
+                //如果大于最大容量,则阈值设置为最大整数的值 
+                threshold = Integer.MAX_VALUE;
+                return oldTab;
+            }
+            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
+                     oldCap >= DEFAULT_INITIAL_CAPACITY)
+                //如果旧的hash桶容量扩容一次(左移1位)后小于最大值,并且旧的桶容量大于默认值16
+                //新桶容量 = 旧桶容量*2
+                newThr = oldThr << 1; // double threshold
+        }
+        else if (oldThr > 0) // initial capacity was placed in threshold
+            newCap = oldThr;
+        else {               // zero initial threshold signifies using defaults
+            //初始化
+            newCap = DEFAULT_INITIAL_CAPACITY;
+            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
+        }
+        if (newThr == 0) {
+            float ft = (float)newCap * loadFactor;
+            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
+                      (int)ft : Integer.MAX_VALUE);
+        }
+        threshold = newThr;
+        @SuppressWarnings({"rawtypes","unchecked"})
+        //初始化一个容量为newCap的新hash桶数组
+        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
+        //将新桶复制给旧的hash桶数组
+        table = newTab;
+        if (oldTab != null) {
+            //如果旧的hash桶不为空,则开始扩容操作
+            for (int j = 0; j < oldCap; ++j) {
+                Node<K,V> e;
+                if ((e = oldTab[j]) != null) {//旧桶j处节点值赋值给e,如果不为空,将旧桶j节点设置为空
+                    oldTab[j] = null;
+                    if (e.next == null)//如果e后面没有节点
+                        newTab[e.hash & (newCap - 1)] = e;//直接对e的hash值对新数组长度求模获得hash桶中存储位置
+                    else if (e instanceof TreeNode)//如果e为红黑树节点,将e添加到红黑树中
+                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
+                    else { // preserve order //如果是链表
+                        Node<K,V> loHead = null, loTail = null;
+                        Node<K,V> hiHead = null, hiTail = null;
+                        Node<K,V> next;
+                        do {
+                            next = e.next;//将e的下一节点赋值给next
+                            if ((e.hash & oldCap) == 0) {//如果e的hash值对旧桶长度求模为0
+                                if (loTail == null)
+                                    loHead = e;//如果loTail为null,将e赋给loHead
+                                else
+                                    loTail.next = e;//非则将e赋值给loTail的next节点
+                                loTail = e;//将e赋值给loTail节点
+                            }
+                            else {//如果e的hash值对旧桶长度取模不为0
+                                if (hiTail == null)
+                                    hiHead = e;//hiHead为null,将e赋值给hiHead
+                                else
+                                    hiTail.next = e;//否则e赋值给hiTail.next
+                                hiTail = e;//将e赋值给hiTail
+                            }
+                        } while ((e = next) != null);//直到e为空
+                        if (loTail != null) {
+                            //如果loTail不为空,将loTail的下一节点设置为null
+                            loTail.next = null;
+                            //将loHead赋值给新桶的j处
+                            newTab[j] = loHead;
+                        }
+                        if (hiTail != null) {
+                            //如果hiTail不为空,将hiTail的下一节点设置为null
+                            hiTail.next = null;
+                            //hiHead赋值给新桶的j+旧桶数组长度处
+                            newTab[j + oldCap] = hiHead;
+                        }
+                    }
+                }
+            }
+        }
+        return newTab;
+    }
final Node<K,V>[] resize() {
+        //oldTable指向当前hash桶数组
+        Node<K,V>[] oldTab = table;
+        int oldCap = (oldTab == null) ? 0 : oldTab.length;
+        int oldThr = threshold;
+        int newCap, newThr = 0;
+        if (oldCap > 0) {//如果旧的hash桶不为空
+            if (oldCap >= MAXIMUM_CAPACITY) {
+                //如果大于最大容量,则阈值设置为最大整数的值 
+                threshold = Integer.MAX_VALUE;
+                return oldTab;
+            }
+            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
+                     oldCap >= DEFAULT_INITIAL_CAPACITY)
+                //如果旧的hash桶容量扩容一次(左移1位)后小于最大值,并且旧的桶容量大于默认值16
+                //新桶容量 = 旧桶容量*2
+                newThr = oldThr << 1; // double threshold
+        }
+        else if (oldThr > 0) // initial capacity was placed in threshold
+            newCap = oldThr;
+        else {               // zero initial threshold signifies using defaults
+            //初始化
+            newCap = DEFAULT_INITIAL_CAPACITY;
+            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
+        }
+        if (newThr == 0) {
+            float ft = (float)newCap * loadFactor;
+            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
+                      (int)ft : Integer.MAX_VALUE);
+        }
+        threshold = newThr;
+        @SuppressWarnings({"rawtypes","unchecked"})
+        //初始化一个容量为newCap的新hash桶数组
+        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
+        //将新桶复制给旧的hash桶数组
+        table = newTab;
+        if (oldTab != null) {
+            //如果旧的hash桶不为空,则开始扩容操作
+            for (int j = 0; j < oldCap; ++j) {
+                Node<K,V> e;
+                if ((e = oldTab[j]) != null) {//旧桶j处节点值赋值给e,如果不为空,将旧桶j节点设置为空
+                    oldTab[j] = null;
+                    if (e.next == null)//如果e后面没有节点
+                        newTab[e.hash & (newCap - 1)] = e;//直接对e的hash值对新数组长度求模获得hash桶中存储位置
+                    else if (e instanceof TreeNode)//如果e为红黑树节点,将e添加到红黑树中
+                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
+                    else { // preserve order //如果是链表
+                        Node<K,V> loHead = null, loTail = null;
+                        Node<K,V> hiHead = null, hiTail = null;
+                        Node<K,V> next;
+                        do {
+                            next = e.next;//将e的下一节点赋值给next
+                            if ((e.hash & oldCap) == 0) {//如果e的hash值对旧桶长度求模为0
+                                if (loTail == null)
+                                    loHead = e;//如果loTail为null,将e赋给loHead
+                                else
+                                    loTail.next = e;//非则将e赋值给loTail的next节点
+                                loTail = e;//将e赋值给loTail节点
+                            }
+                            else {//如果e的hash值对旧桶长度取模不为0
+                                if (hiTail == null)
+                                    hiHead = e;//hiHead为null,将e赋值给hiHead
+                                else
+                                    hiTail.next = e;//否则e赋值给hiTail.next
+                                hiTail = e;//将e赋值给hiTail
+                            }
+                        } while ((e = next) != null);//直到e为空
+                        if (loTail != null) {
+                            //如果loTail不为空,将loTail的下一节点设置为null
+                            loTail.next = null;
+                            //将loHead赋值给新桶的j处
+                            newTab[j] = loHead;
+                        }
+                        if (hiTail != null) {
+                            //如果hiTail不为空,将hiTail的下一节点设置为null
+                            hiTail.next = null;
+                            //hiHead赋值给新桶的j+旧桶数组长度处
+                            newTab[j + oldCap] = hiHead;
+                        }
+                    }
+                }
+            }
+        }
+        return newTab;
+    }

为什么table的长度总是2的n次幂

  • tableSizeFor(int cap)方法
java
static final int tableSizeFor(int cap) {
+        int n = cap - 1;
+        n |= n >>> 1;
+        n |= n >>> 2;
+        n |= n >>> 4;
+        n |= n >>> 8;
+        n |= n >>> 16;
+        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
+    }
static final int tableSizeFor(int cap) {
+        int n = cap - 1;
+        n |= n >>> 1;
+        n |= n >>> 2;
+        n |= n >>> 4;
+        n |= n >>> 8;
+        n |= n >>> 16;
+        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
+    }

测试类

java
public class Test {
+    public static void main(String[] args) {
+
+        int cap = 65538;
+        System.out.println(Integer.toBinaryString(cap));
+        System.out.println(Integer.toBinaryString(cap-1));
+        int i = tableSizeFor(cap);
+        System.out.println(Integer.toBinaryString(i));
+    }
+
+    static final int tableSizeFor(int cap) {
+        int n = cap - 1;
+        n |= n >>> 1;
+        System.out.println(Integer.toBinaryString(n));
+        n |= n >>> 2;
+        System.out.println(Integer.toBinaryString(n));
+        n |= n >>> 4;
+        System.out.println(Integer.toBinaryString(n));
+        n |= n >>> 8;
+        System.out.println(Integer.toBinaryString(n));
+        n |= n >>> 16;
+        System.out.println(Integer.toBinaryString(n));
+        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
+    }
+}
public class Test {
+    public static void main(String[] args) {
+
+        int cap = 65538;
+        System.out.println(Integer.toBinaryString(cap));
+        System.out.println(Integer.toBinaryString(cap-1));
+        int i = tableSizeFor(cap);
+        System.out.println(Integer.toBinaryString(i));
+    }
+
+    static final int tableSizeFor(int cap) {
+        int n = cap - 1;
+        n |= n >>> 1;
+        System.out.println(Integer.toBinaryString(n));
+        n |= n >>> 2;
+        System.out.println(Integer.toBinaryString(n));
+        n |= n >>> 4;
+        System.out.println(Integer.toBinaryString(n));
+        n |= n >>> 8;
+        System.out.println(Integer.toBinaryString(n));
+        n |= n >>> 16;
+        System.out.println(Integer.toBinaryString(n));
+        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
+    }
+}

运行结果

010000000000000010
+010000000000000001
+011000000000000001
+011110000000000001
+011111111000000001
+011111111111111111
+011111111111111111
+100000000000000000
010000000000000010
+010000000000000001
+011000000000000001
+011110000000000001
+011111111000000001
+011111111111111111
+011111111111111111
+100000000000000000
  • 分析 第一次运行: 10000000000000010 n >>> 1; 01000000000000000 进行|运算 11000000000000001 把最大位的1,通过位移后移一位,并且通过|运算,组合起来 第二次运行: 11000000000000001 n >>> 2; 00110000000000000 进行|运算 11110000000000001 把最大的两位,已经变成1的,往后移动两位,并且通过|运算,组合起来 第三次运行: 11110000000000001 n >>> 4; 00001111000000000 进行|运算 11111111000000001 把最大4位,已经变成1的,往后移动4位,并且通过|运算,组合起来 第四次运行: 11111111000000001 n >>> 8; 00000000111111110 进行|运算 11111111111111111 把最大的8位,已经变成1的,往后移动8位,并且通过|运算,组合起来 第五次运算: 同上进行16位运算 第六次运算: 返回结果,格式为最高位为1其他为全为0的值,即一定是2的整数次幂
+ + + + \ No newline at end of file diff --git "a/java/base/Java8\346\226\260\347\211\271\346\200\247\345\233\236\351\241\276.html" "b/java/base/Java8\346\226\260\347\211\271\346\200\247\345\233\236\351\241\276.html" new file mode 100644 index 000000000..85de4070e --- /dev/null +++ "b/java/base/Java8\346\226\260\347\211\271\346\200\247\345\233\236\351\241\276.html" @@ -0,0 +1,157 @@ + + + + + + Java8新特性回顾 | 故事 + + + + + + + + + + + + + + +
Skip to content

Java8新特性回顾

lambda表达式

Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)

语法

java
(parameters) -> expression
+
+(parameters) ->{ statements; }
(parameters) -> expression
+
+(parameters) ->{ statements; }

以下是lambda表达式的重要特征:

  • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定表达式返回了一个数值。

示例

java
// 1. 不需要参数,返回值为 5  
+() -> 5  
+  
+// 2. 接收一个参数(数字类型),返回其2倍的值  
+x -> 2 * x  
+  
+// 3. 接受2个参数(数字),并返回他们的差值  
+(x, y) -> x – y  
+  
+// 4. 接收2个int型整数,返回他们的和  
+(int x, int y) -> x + y  
+  
+// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)  
+(String s) -> System.out.print(s)
// 1. 不需要参数,返回值为 5  
+() -> 5  
+  
+// 2. 接收一个参数(数字类型),返回其2倍的值  
+x -> 2 * x  
+  
+// 3. 接受2个参数(数字),并返回他们的差值  
+(x, y) -> x – y  
+  
+// 4. 接收2个int型整数,返回他们的和  
+(int x, int y) -> x + y  
+  
+// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)  
+(String s) -> System.out.print(s)

变量作用域

  • lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在 lambda 内部修改定义在域外的局部变量,否则会编译错误。
  • lambda 表达式的局部变量可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有 final 的语义)
  • 在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量。

lambda表达和匿名内部类的区别

第一点:所需类型不同

匿名内部类:可以是接口,也可以是抽象类,还可以是具体类。 **Lambda表达式:**只能是接口。

第二点:使用限制不同

1,如果接口中有且仅有一个抽象方法,可以使用Lambda表达式,也可以使用匿名内部类。 2,如果接口中有多于一个抽象方法,只能使用匿名内部类,不可以使用Lambda表达式。

第三点:实现原理不同

匿名内部类:编译之后会产生一个单独的.class字节码文件。 Lambda表达式:编译之后没有产生一个单独的.class字节码文件,对应的字节码文件会在运行的时候动态生成。

函数式接口

函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。

函数式接口可以被隐式转换为 lambda 表达式。

Lambda 表达式和方法引用(实际上也可认为是Lambda表达式)上。

java
@FunctionalInterface
+interface GreetingService 
+{
+    void sayMessage(String message);
+}
+
+---
+  
+GreetingService greetService1 = message -> System.out.println("Hello " + message);
@FunctionalInterface
+interface GreetingService 
+{
+    void sayMessage(String message);
+}
+
+---
+  
+GreetingService greetService1 = message -> System.out.println("Hello " + message);

Stream

Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。

这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。

元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。

txt
+--------------------+       +------+   +------+   +---+   +-------+
+| stream of elements +-----> |filter+-> |sorted+-> |map+-> |collect|
++--------------------+       +------+   +------+   +---+   +-------+
+--------------------+       +------+   +------+   +---+   +-------+
+| stream of elements +-----> |filter+-> |sorted+-> |map+-> |collect|
++--------------------+       +------+   +------+   +---+   +-------+

以上的流程转换为 Java 代码为:

java
List<Integer> transactionsIds = 
+widgets.stream()
+             .filter(b -> b.getColor() == RED)
+             .sorted((x,y) -> x.getWeight() - y.getWeight())
+             .mapToInt(Widget::getWeight)
+             .sum();
List<Integer> transactionsIds = 
+widgets.stream()
+             .filter(b -> b.getColor() == RED)
+             .sorted((x,y) -> x.getWeight() - y.getWeight())
+             .mapToInt(Widget::getWeight)
+             .sum();

Stream(流)是一个来自数据源的元素队列并支持聚合操作

  • 元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。
  • 数据源 流的来源。 可以是集合,数组,I/O channel等。
  • 聚合操作 类似SQL语句一样的操作, 比如filter, map, reduce, find, match, sorted等。

和以前的Collection操作不同, Stream操作还有两个基础的特征:

  • Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
  • 内部迭代: 以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现。

流的特性

  • stream不存储数据,而是按照特定的规则对数据进行计算,一般会输出结果;

  • stream不会改变数据源,通常情况下会产生一个新的集合;

  • stream具有延迟执行特性,只有调用终端操作时,中间操作才会执行

  • stream操作分为终端操作和中间操作,那么这两者分别代表什么呢? 终端操作:会消费流,这种操作会产生一个结果的,如果一个流被消费过了,那它就不能被重用的。 中间操作:中间操作会产生另一个流。因此中间操作可以用来创建执行一系列动作的管道。一个特别需要注意的点是: 中间操作不是立即发生的。相反,当在中间操作创建的新流上执行完终端操作后,中间操作指定的操作才会发生。所以中间操作是延迟发生的,中间操作的延迟行为主要是让流API能够更加高效地执行。

  • stream不可复用,对一个已经进行过终端操作的流再次调用,会抛出异常。

生成流

集合创建流:

  • stream() − 为集合创建串行流。
  • parallelStream() − 为集合创建并行流。

数组创建流:

  • Arrays.stream(arr)
  • Stream.of(arr)

常用方法

  • foreach:迭代
  • map:将元素映射为另一种元素
  • filter:过滤元素
  • limit:获取制定数量的流
  • count:统计数量
  • max:最大值
  • min:最小值
  • sorted:排序,可以使用自然排序或特定比较器。
  • reduce:把一个流缩减为一个值,比如对一个集合求和等
  • collect:collect操作可接收各种方法为参数,将流中数据汇总,例如Collectors.toList()。
  • joining:将元素用特定字符链接起来
  • concat:可以组合两个流

Optional

Optional 类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。

Optional 是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。

Optional 类的引入很好的解决空指针异常。

示例

java
public class Java8Tester {
+   public static void main(String args[]){
+   
+      Java8Tester java8Tester = new Java8Tester();
+      Integer value1 = null;
+      Integer value2 = new Integer(10);
+        
+      // Optional.ofNullable - 允许传递为 null 参数
+      Optional<Integer> a = Optional.ofNullable(value1);
+        
+      // Optional.of - 如果传递的参数是 null,抛出异常 NullPointerException
+      Optional<Integer> b = Optional.of(value2);
+      System.out.println(java8Tester.sum(a,b));
+   }
+    
+   public Integer sum(Optional<Integer> a, Optional<Integer> b){
+    
+      // Optional.isPresent - 判断值是否存在
+        
+      System.out.println("第一个参数值存在: " + a.isPresent());
+      System.out.println("第二个参数值存在: " + b.isPresent());
+        
+      // Optional.orElse - 如果值存在,返回它,否则返回默认值
+      Integer value1 = a.orElse(new Integer(0));
+        
+      //Optional.get - 获取值,值需要存在
+      Integer value2 = b.get();
+      return value1 + value2;
+   }
+}
+
+--- 
+  
+  
+第一个参数值存在: false
+第二个参数值存在: true
+10
public class Java8Tester {
+   public static void main(String args[]){
+   
+      Java8Tester java8Tester = new Java8Tester();
+      Integer value1 = null;
+      Integer value2 = new Integer(10);
+        
+      // Optional.ofNullable - 允许传递为 null 参数
+      Optional<Integer> a = Optional.ofNullable(value1);
+        
+      // Optional.of - 如果传递的参数是 null,抛出异常 NullPointerException
+      Optional<Integer> b = Optional.of(value2);
+      System.out.println(java8Tester.sum(a,b));
+   }
+    
+   public Integer sum(Optional<Integer> a, Optional<Integer> b){
+    
+      // Optional.isPresent - 判断值是否存在
+        
+      System.out.println("第一个参数值存在: " + a.isPresent());
+      System.out.println("第二个参数值存在: " + b.isPresent());
+        
+      // Optional.orElse - 如果值存在,返回它,否则返回默认值
+      Integer value1 = a.orElse(new Integer(0));
+        
+      //Optional.get - 获取值,值需要存在
+      Integer value2 = b.get();
+      return value1 + value2;
+   }
+}
+
+--- 
+  
+  
+第一个参数值存在: false
+第二个参数值存在: true
+10
+ + + + \ No newline at end of file diff --git "a/java/base/String\347\261\273\347\232\204\346\267\261\345\205\245\345\255\246\344\271\240.html" "b/java/base/String\347\261\273\347\232\204\346\267\261\345\205\245\345\255\246\344\271\240.html" new file mode 100644 index 000000000..b2c3f0d0d --- /dev/null +++ "b/java/base/String\347\261\273\347\232\204\346\267\261\345\205\245\345\255\246\344\271\240.html" @@ -0,0 +1,113 @@ + + + + + + String类的深入学习 | 故事 + + + + + + + + + + + + + + +
Skip to content

String类的深入学习

String类

java
public final class String
+    implements java.io.Serializable, Comparable<String>, CharSequence {
+    /** The value is used for character storage. */
+    private final char value[];
+    ...
+}
public final class String
+    implements java.io.Serializable, Comparable<String>, CharSequence {
+    /** The value is used for character storage. */
+    private final char value[];
+    ...
+}

1.String类的声明

  • String类是final的,不可以被继承
  • String类底层是char型数组实现的
  • value[] 也是final的,而且是private修饰的,这就保证了String类的对象一旦被初始化就无法更改。

String对象被创建后就无法更改指的是常规方法无法更改,因为String类是由char型数组实现的,而这个数组value也是一个引用,我们可以通过暴力反射setAccessible(true),来修改value数组的内容。

2.常用构造方法

1.String str = new String();构造一个空字符串

java
public String() {
+    this.value = "".value;
+}
public String() {
+    this.value = "".value;
+}

2.String str = new String(String string)根据指定的字符串来构造新的String对象

java
public String(String original) {
+    this.value = original.value;
+    this.hash = original.hash;
+}
public String(String original) {
+    this.value = original.value;
+    this.hash = original.hash;
+}

3.String str = new String(char[] value)通过指定字符数组来构造新的String对象

java
public String(char value[]) {
+    this.value = Arrays.copyOf(value, value.length);
+}
public String(char value[]) {
+    this.value = Arrays.copyOf(value, value.length);
+}

当然,还有我们最常用的String str = "123",不过这种通过字面量形式构造对象的方式完全等同于上述的第三种形式,实际上它的实现是:

java
char[] chars = {'1','2','3'};
+String str = new String(chars);
char[] chars = {'1','2','3'};
+String str = new String(chars);

3.关于声明一个字符串是否创建了对象

  • 由字面量声明的字符串不一定创建了对象

String a = "123";是否创建了对象要看字符串常量池中,是否已经有了"123"这个对象,如果有,那么这句代码就没有创建对象,如果没有,那么就在字符串常量池中创建了一个"123"对象.

  • 通过new出来的字符串一定创建了对象

String a = new String("123"),无论字符串常量池是否有"123"这个对象,这句代码都会创建对象,区别就在于创建了一个还是两个.假如常量池中没有,那么就分别在常量池和堆区都分别创建了"123"对象。如果常量池中有该对象,那么就只在堆区中创建一个"123"对象。

字符串常量池

1.字符串常量池的设计思想

  • 字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能

  • JVM为了提高性能和减少内存开销,为字符串开辟一个字符串常量池,类似于缓存区,创建字符串常量时,首先判断字符串常量池是否存在该字符串.如果存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中

  • 实现的基础:字符串不可变

2.字符串常量池的位置

字符串常量池在JDK1.6之前是存放在Perm区的(永久代),也就是我们常说的方法区,而在JDK1.7之后,字符串常量池已经被移到了Heap区(堆区)存放,而在JDK1.8,Perm区已经被移除了,取而代之是元空间。

我们常说的方法区,其实是JVM中提出的规范,永久代和方法区的关系,类似我们Java中的类与接口,方法区是一个接口,制定了规范,而永久代是HotSpot虚拟机对这个规范的实现

并且,字符串常量池相较其他常量池有着特殊性:

  • 直接使用字面量声明的String对象,如果在常量池中不存在,那么就会直接存储在字符串常量池中,如果存在,那么就会直接指向字符串常量池中的对象

  • 而通过new关键字创建的String对象,如果在常量池中不存在,可以通过native方法intern手动入池。而即使常量池中存在这个字符串,这个方法就不会生效.

  • 能否入池,都取决于字符串常量池中是否存在该字符串。

    • 如果不存在就会入池。
    • 如果存在,那么通过字面量声明的字符串就会直接从常量池中取值;而intern方法就没有任何效果

很重要的一点:

​ 由于JDK对于字符串常量池的改动,在JDK1.7和之后的版本,字符串常量池都在堆区中了,而且,使用intern方法入池的字符串,不会再在字符串常量池中创建一个对象,而是保存调用intern方法的这个字符串的引用。

StringBuilder和StringBuffer

众所周知,在Java中,运算符+在和字符串一起使用时的作用是拼接,而非运算,那么到底是什么原因呢,其实底层就是StringBuffer(JDK1.0)和StringBuilder(JDK1.5之后)实现的。

看了API就会发现,StringBuilder和StringBuffer都是可变字符序列,而且两者的方法是完全一样的,唯一的区别就是线程安全问题,StringBuilder是线程不安全的,而StringBuffer是线程安全的,而StringBuffer始于JDK1.0,StringBuilder始于JDK1.5,也就是说,StringBuilder的出现,就是为了在单线程条件下替换StringBuffer,也就意味着,在不考虑线程安全问题的情况下,我们通常都会使用StringBuilder,因为没有线程问题的影响,StringBuilder的速度更快。

再说回字符串拼接的问题,

java
String a = "1";
+String b = "2";
+String c = "3";
+String d = a+b+c;
+System.out.print(d);//"123"
String a = "1";
+String b = "2";
+String c = "3";
+String d = a+b+c;
+System.out.print(d);//"123"

上面这个代码片段的底层实现,其实是:

java
String d = (new StringBuilder(String.valueof(a))).append(b).append(c).toString();
String d = (new StringBuilder(String.valueof(a))).append(b).append(c).toString();

换言之,字符串拼接,其实是创建了一个新的StringBuilder的对象,来调用append方法进行拼接,拼接完成后再调用toString方法来返回一个新的字符串.

因此,

java
String a = "Hello";
+String b = "World";
+String c = a+b;
+String d = a+b;
+System.out.println(c);
+System.out.println(d);
+System.out.println(c==d);
String a = "Hello";
+String b = "World";
+String c = a+b;
+String d = a+b;
+System.out.println(c);
+System.out.println(d);
+System.out.println(c==d);

这个代码片段的结果是HelloWorld,HelloWorld,false。

原因是StringBuilder的toString方法每次都会返回一个new出来的String对象。源码如下:

java
//StringBuilder类重写的toString方法
+@Override
+    public String toString() {
+        // Create a copy, don't share the array
+        return new String(value, 0, count);
+    }
//StringBuilder类重写的toString方法
+@Override
+    public String toString() {
+        // Create a copy, don't share the array
+        return new String(value, 0, count);
+    }

关于String类的面试题

题目1:

String str = new String("123")一共创建了几个对象

答:

1.假如字符串常量池中没有"123"这个字符串,那么这条代码就创建了两个对象,第一个是在字符串常量池中创建了字符串对象"123",然后在堆区创建了一个字符串对象"123",接着会把堆区这个"123"的引用地址值赋给在栈区声明的str。

2.假如字符串常量池中有"123"这个字符串,那么就只创建了一个对象,就是在堆区中创建了对象"123",然后把地址值赋给str.

题目2:

引用自:深入解析String#intern-美团技术团队

代码片段1:

java
String s1 = new String("1");
+s.intern();
+String s2 = "1";
+System.out.println(s == s2);
+
+String s3 = new String("1") + new String("1");
+s3.intern();
+String s4 = "11";
+System.out.println(s3 == s4);
String s1 = new String("1");
+s.intern();
+String s2 = "1";
+System.out.println(s == s2);
+
+String s3 = new String("1") + new String("1");
+s3.intern();
+String s4 = "11";
+System.out.println(s3 == s4);

在JDK1.6中:结果是false false

在JDK1.7中:结果是 false true

分析:

  • 在JDK1.6中:字符串常量池存储于永久代

String s1 = new String("1")首先在字符串常量池中创建了对象"1",然后在堆区创建对象"1",s1的引用指向的是堆区的对象;

s1.intern()方法,会查找字符串常量池中是否有"1"这个对象,结果里面有,所以这个方法没有生效,等于没写.

String s2 = "1",因为常量池中已经有"1"这个字符串了,所以s2指向了常量池中"1"

s1指向堆区,s2指向永久代,显然二者地址不同,结果为false

String s3 = new String("1") + new String("1");这句代码在堆区创建了两个匿名"1"对象,拼接后的等于在堆区中创建了字符串"11"对象

s3.intern()方法将"11"对象保存在了字符串常量池中

String s4 = "11",指向的是字符串常量池中"11"对象

s3指向堆区,s4指向永久代,结果为false

  • 在JDK1.7中:字符串常量池存储于堆区,且intern方法不会创建对象,而是保存堆区对象的引用

s1在常量池和堆区分别创建了对象"1",s1指向的是堆区的"1"对象,s.intern()方法无效,s2指向的是常量池中的"1"对象,s1和s2指向地址不同,所以是false;

s3在堆区创建了对象"11",s3.intern()将堆区对象存在常量池中,但是!!! 这里的是将堆区中"11"的引用存在了常量池,而非创建对象.

所以s4指向常量池中的引用,其实就是s3的引用,所以s3==s4为true。

代码片段2:

java
String s1 = new String("1");
+String s2 = "1";
+s1.intern();
+System.out.println(s == s2);
+
+String s3 = new String("1") + new String("1");
+String s4 = "11";
+s3.intern();
+System.out.println(s3 == s4);
String s1 = new String("1");
+String s2 = "1";
+s1.intern();
+System.out.println(s == s2);
+
+String s3 = new String("1") + new String("1");
+String s4 = "11";
+s3.intern();
+System.out.println(s3 == s4);

JDK1.6结果:false false

JDK1.7结果:false false

分析:

  • 在JDK1.6中:字符串常量池存储于永久代

s1在常量池和堆区分别创建了对象"1",s1指堆区的对象"1";

s2指向的是常量池的对象"1"

因为常量池中有"1"这个对象,所以s1.intern()无效

s1,s2二者指向地址不同,所以是false.

s3在堆区创建了对象"11"

s4在常量池创建了对象"11",并指向了常量池中的"11"对象

常量池中已经有了对象"11",s3.intern()无效

s3,s4指向不同,所以false

  • 在JDK1.7中:字符串常量池存储于堆区,且intern方法不会创建对象,而是保存堆区对象的引用

s1在常量池和堆区分别创建了对象"1",s1指堆区的对象"1";

s2指向的是常量池的对象"1"

因为常量池中有"1"这个对象,所以s1.intern()无效

s1,s2二者指向地址不同,所以是false.

s3在堆区创建了对象"11"

s4在常量池创建了对象"11",并指向了常量池中的"11"对象

常量池中已经有了对象"11",s3.intern()无效

s3,s4指向不同,所以false

+ + + + \ No newline at end of file diff --git "a/java/database/MySql\347\264\242\345\274\225.html" "b/java/database/MySql\347\264\242\345\274\225.html" new file mode 100644 index 000000000..78b492b75 --- /dev/null +++ "b/java/database/MySql\347\264\242\345\274\225.html" @@ -0,0 +1,47 @@ + + + + + + MySql索引 | 故事 + + + + + + + + + + + + + + +
Skip to content

MySql索引

本篇内容基于MySQL的InnoDB存储引擎。

索引的概念

索引是一个单独的、存储在磁盘上的数据库结构,它们包含着对数据表里所有记录的引用指针。使用索引用于快速找出在某个或多个列中有一特定值的行,所有MySQL列类型都可以被索引,==对相关列使用索引是提高查询操作速度的最佳途径==。

InnoDB存储引擎中的索引都是指BTree索引,MySQL中还有Hash索引,详见官网存储引擎索引类型

12712542020101314214516280980408.png

索引的原理

内容引用自美团技术团队发表的文章MySQL索引原理及慢查询优化

索引的目的在于提高查询效率,可以类比字典,如果要查“mysql”这个单词,我们肯定需要定位到m字母,然后从下往下找到y字母,再找到剩下的sql。如果没有索引,那么你可能需要把所有单词看一遍才能找到你想要的,如果我想找到m开头的单词呢?或者ze开头的单词呢?是不是觉得如果没有索引,这个事情根本无法完成?

数据库的索引结构需要做的事:每次查找数据时把磁盘IO次数控制在一个很小的数量级,最好是常数数量级。那么我们就想到如果一个高度可控的多路搜索树是否能满足需求呢?就这样,b+树应运而生。

b树.jpg 如上图,是一颗b+树,关于b+树的定义可以参见B+树,这里只说一些重点,浅蓝色的块我们称之为一个磁盘块,可以看到每个磁盘块包含几个数据项(深蓝色所示)和指针(黄色所示),如磁盘块1包含数据项17和35,包含指针P1、P2、P3,P1表示小于17的磁盘块,P2表示在17和35之间的磁盘块,P3表示大于35的磁盘块。真实的数据存在于叶子节点即3、5、9、10、13、15、28、29、36、60、75、79、90、99。非叶子节点只不存储真实的数据,只存储指引搜索方向的数据项,如17、35并不真实存在于数据表中。

b+树的查找过程

如图所示,如果要查找数据项29,那么首先会把磁盘块1由磁盘加载到内存,此时发生一次IO,在内存中用二分查找确定29在17和35之间,锁定磁盘块1的P2指针,内存时间因为非常短(相比磁盘的IO)可以忽略不计,通过磁盘块1的P2指针的磁盘地址把磁盘块3由磁盘加载到内存,发生第二次IO,29在26和30之间,锁定磁盘块3的P2指针,通过指针加载磁盘块8到内存,发生第三次IO,同时内存中做二分查找找到29,结束查询,总计三次IO。真实的情况是,3层的b+树可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,那么总共需要百万次的IO,显然成本非常非常高。

b+树性质

1.通过上面的分析,我们知道IO次数取决于b+数的高度h,假设当前数据表的数据为N,每个磁盘块的数据项的数量是m,则有h=㏒(m+1)N,当数据量N一定的情况下,m越大,h越小;而m = 磁盘块的大小 / 数据项的大小,磁盘块的大小也就是一个数据页的大小,是固定的,如果数据项占的空间越小,数据项的数量越多,树的高度越低。这就是为什么每个数据项,即索引字段要尽量的小,比如int占4字节,要比bigint8字节少一半。这也是为什么b+树要求把真实的数据放到叶子节点而不是内层节点,一旦放到内层节点,磁盘块的数据项会大幅度下降,导致树增高。当数据项等于1时将会退化成线性表。

2.当b+树的数据项是复合的数据结构,比如(name,age,sex)的时候,b+数是按照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样的数据来检索的时候,b+树会优先比较name来确定下一步的所搜方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了, 这个是非常重要的性质,即索引的最左匹配特性。

索引的类别

1.Primary Key(主键索引)

主键索引是一种特殊的唯一索引,这个时候需要一个表只能有一个主键,不允许有空值。一般是在建表的时候同时创建主键索引

InnoDB存储引擎的表,如果建表的时候没有指定主键,则会使用第一非空的唯一索引作为聚集索引,如果没有主键也没有合适的唯一索引,那么innodb内部会生成一个隐藏的主键作为聚集索引,这个隐藏的主键是一个6个字节的列,该列的值会随着数据的插入自增。

在其他存储引擎或其他数据库中主键索引不一定就是聚集索引

2.Unique(唯一索引)

唯一索引列的值必须是唯一的,但允许有空值,如果是组合索引,则列值的组合必须是唯一的。

sql
CREATE UNIQUE index 索引名 on 表名(列名)
CREATE UNIQUE index 索引名 on 表名(列名)

3.Key(普通索引)

MySQL中的基本索引类型,允许在定义索引的列中插入重复值和空值

sql
CREATE index 索引名 on 表名(列名)
CREATE index 索引名 on 表名(列名)

4.组合索引

指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用组合索引时遵循最左前缀原则

sql
CREATE index 索引名 on 表名(列名,列名...)
CREATE index 索引名 on 表名(列名,列名...)

5.全文索引

全文索引类型为FULLTEXT,在定义索引的列上支持值的全文查找,允许在这些索引列中插入重复值和空值。全文索引可以在CHAR、VARCHAR或者TEXT类型的列上创建

索引的基本语法

  • 创建
sql
ALTER mytable ADD  [UNIQUE]  INDEX [indexName] ON 表名(列名)
ALTER mytable ADD  [UNIQUE]  INDEX [indexName] ON 表名(列名)
  • 删除
sql
DROP INDEX [indexName] ON 表名;
DROP INDEX [indexName] ON 表名;
  • 查看
sql
SHOW INDEX FROM 表名
SHOW INDEX FROM 表名
  • alter命令
sql
-- 有四种方式来添加数据表的索引:
+ALTER TABLE tbl_name ADD PRIMARY KEY (column_list): 该语句添加一个主键,这意味着索引值必须是唯一的,且不能为NULL。
+ 
+ALTER TABLE tbl_name ADD UNIQUE index_name (column_list): 这条语句创建索引的值必须是唯一的(除了NULL外,NULL可能会出现多次)。
+ 
+ALTER TABLE tbl_name ADD INDEX index_name (column_list): 添加普通索引,索引值可出现多次。
+ 
+ALTER TABLE tbl_name ADD FULLTEXT index_name (column_list):该语句指定了索引为 FULLTEXT ,用于全文索引。
-- 有四种方式来添加数据表的索引:
+ALTER TABLE tbl_name ADD PRIMARY KEY (column_list): 该语句添加一个主键,这意味着索引值必须是唯一的,且不能为NULL。
+ 
+ALTER TABLE tbl_name ADD UNIQUE index_name (column_list): 这条语句创建索引的值必须是唯一的(除了NULL外,NULL可能会出现多次)。
+ 
+ALTER TABLE tbl_name ADD INDEX index_name (column_list): 添加普通索引,索引值可出现多次。
+ 
+ALTER TABLE tbl_name ADD FULLTEXT index_name (column_list):该语句指定了索引为 FULLTEXT ,用于全文索引。

索引的使用场景

1.适合使用索引

  • 频繁作为查询条件的字段
  • 多表查询中与其他表进行关联的字段,外键关系建立索引
  • 单列/组合索引的选择,在高并发的场景下适合建立组合索引
  • 查询中常用于排序的字段
  • 查询中常用于分组或统计的字段

2.不适合使用索引

  • 频繁更新的字段

  • where条件中用不到的字段

  • 表记录很少

  • 重复记录非常多的表

  • 数据区分不明显的字段,例如性别栏位

索引有效场景

  • 全值匹配的查询,例如根据订单id查询select * from t_order where order_id = '9999676623'

  • 匹配范围值的查询,例如 where id > '123456'

  • 最左匹配原则,如user表的username pwd创建了组合索引那么以下几种都可以命中索引

    sql
    select username from user where username='zhangsan' and pwd ='axsedf1sd'
    +
    +select username from user where pwd ='axsedf1sd' and username='zhangsan'
    +
    +select username from user where username='zhangsan'
    select username from user where username='zhangsan' and pwd ='axsedf1sd'
    +
    +select username from user where pwd ='axsedf1sd' and username='zhangsan'
    +
    +select username from user where username='zhangsan'

    sql
    select username from user where pwd ='axsedf1sd'
    select username from user where pwd ='axsedf1sd'

    不能命中索引

  • 非前导模糊查询, 例如 where name like 'xiaoming%'

索引失效场景

  • 负向查询会使索引失效,例如 id not in (1,2,3)
  • 在索引字段进行运算会使索引失效,例如计算,函数,类型转换
  • !=或者<>会使索引失效
  • is not null无法使用索引,但是is null可以
  • 前导模糊查询会使索引失效,例如 name like '%xiaoming',但是非前导可以
  • 字符串不加单引号会使索引失效
  • 使用组合索引时不遵循最左匹配原则会使索引失效
+ + + + \ No newline at end of file diff --git "a/java/database/Otter\345\256\236\347\216\260\346\225\260\346\215\256\345\205\250\351\207\217\345\242\236\351\207\217\345\220\214\346\255\245.html" "b/java/database/Otter\345\256\236\347\216\260\346\225\260\346\215\256\345\205\250\351\207\217\345\242\236\351\207\217\345\220\214\346\255\245.html" new file mode 100644 index 000000000..421c66230 --- /dev/null +++ "b/java/database/Otter\345\256\236\347\216\260\346\225\260\346\215\256\345\205\250\351\207\217\345\242\236\351\207\217\345\220\214\346\255\245.html" @@ -0,0 +1,147 @@ + + + + + + Otter实现数据全量增量同步 | 故事 + + + + + + + + + + + + + + +
Skip to content

Otter实现数据全量增量同步

otter是一款基于数据库增量日志解析,准实时同步到本机房或异地机房的mysql/oracle数据库. 一个分布式数据库同步系统

仓库地址:https://github.com/alibaba/otter

前置工作

  • 源库开启binlog,并且必须是ROW模式

  • 需要启动zookeeper

  • otter是基于canal的,但是otter项目本身内嵌了canal,所以无需独立启动canal-server

  • 初始化otter数据库

    • sql

    CREATE DATABASE /!32312 IF NOT EXISTS/ otter /*!40100 DEFAULT CHARACTER SET utf8 COLLATE utf8_bin */;

    USE otter;

    SET sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';

    CREATE TABLE ALARM_RULE ( ID bigint(20) unsigned NOT NULL AUTO_INCREMENT, MONITOR_NAME varchar(1024) DEFAULT NULL, RECEIVER_KEY varchar(1024) DEFAULT NULL, STATUS varchar(32) DEFAULT NULL, PIPELINE_ID bigint(20) NOT NULL, DESCRIPTION varchar(256) DEFAULT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, MATCH_VALUE varchar(1024) DEFAULT NULL, PARAMETERS text DEFAULT NULL, PRIMARY KEY (ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE AUTOKEEPER_CLUSTER ( ID bigint(20) NOT NULL AUTO_INCREMENT, CLUSTER_NAME varchar(200) NOT NULL, SERVER_LIST varchar(1024) NOT NULL, DESCRIPTION varchar(200) DEFAULT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE CANAL ( ID bigint(20) unsigned NOT NULL AUTO_INCREMENT, NAME varchar(200) DEFAULT NULL, DESCRIPTION varchar(200) DEFAULT NULL, PARAMETERS text DEFAULT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), UNIQUE KEY CANALUNIQUE (NAME) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE CHANNEL ( ID bigint(20) NOT NULL AUTO_INCREMENT, NAME varchar(200) NOT NULL, DESCRIPTION varchar(200) DEFAULT NULL, PARAMETERS text DEFAULT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), UNIQUE KEY CHANNELUNIQUE (NAME) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE COLUMN_PAIR ( ID bigint(20) NOT NULL AUTO_INCREMENT, SOURCE_COLUMN varchar(200) DEFAULT NULL, TARGET_COLUMN varchar(200) DEFAULT NULL, DATA_MEDIA_PAIR_ID bigint(20) NOT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), KEY idx_DATA_MEDIA_PAIR_ID (DATA_MEDIA_PAIR_ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE COLUMN_PAIR_GROUP ( ID bigint(20) NOT NULL AUTO_INCREMENT, DATA_MEDIA_PAIR_ID bigint(20) NOT NULL, COLUMN_PAIR_CONTENT text DEFAULT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), KEY idx_DATA_MEDIA_PAIR_ID (DATA_MEDIA_PAIR_ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE DATA_MEDIA ( ID bigint(20) NOT NULL AUTO_INCREMENT, NAME varchar(200) NOT NULL, NAMESPACE varchar(200) NOT NULL, PROPERTIES varchar(1000) NOT NULL, DATA_MEDIA_SOURCE_ID bigint(20) NOT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), UNIQUE KEY DATAMEDIAUNIQUE (NAME,NAMESPACE,DATA_MEDIA_SOURCE_ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE DATA_MEDIA_PAIR ( ID bigint(20) NOT NULL AUTO_INCREMENT, PULLWEIGHT bigint(20) DEFAULT NULL, PUSHWEIGHT bigint(20) DEFAULT NULL, RESOLVER text DEFAULT NULL, FILTER text DEFAULT NULL, SOURCE_DATA_MEDIA_ID bigint(20) DEFAULT NULL, TARGET_DATA_MEDIA_ID bigint(20) DEFAULT NULL, PIPELINE_ID bigint(20) NOT NULL, COLUMN_PAIR_MODE varchar(20) DEFAULT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), KEY idx_PipelineID (PIPELINE_ID,ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE DATA_MEDIA_SOURCE ( ID bigint(20) NOT NULL AUTO_INCREMENT, NAME varchar(200) NOT NULL, TYPE varchar(20) NOT NULL, PROPERTIES varchar(1000) NOT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), UNIQUE KEY DATAMEDIASOURCEUNIQUE (NAME) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE DELAY_STAT ( ID bigint(20) NOT NULL AUTO_INCREMENT, DELAY_TIME bigint(20) NOT NULL, DELAY_NUMBER bigint(20) NOT NULL, PIPELINE_ID bigint(20) NOT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), KEY idx_PipelineID_GmtModified_ID (PIPELINE_ID,GMT_MODIFIED,ID), KEY idx_Pipeline_GmtCreate (PIPELINE_ID,GMT_CREATE), KEY idx_GmtCreate_id (GMT_CREATE,ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE LOG_RECORD ( ID bigint(20) NOT NULL AUTO_INCREMENT, NID varchar(200) DEFAULT NULL, CHANNEL_ID varchar(200) NOT NULL, PIPELINE_ID varchar(200) NOT NULL, TITLE varchar(1000) DEFAULT NULL, MESSAGE text DEFAULT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), KEY logRecord_pipelineId (PIPELINE_ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE NODE ( ID bigint(20) NOT NULL AUTO_INCREMENT, NAME varchar(200) NOT NULL, IP varchar(200) NOT NULL, PORT bigint(20) NOT NULL, DESCRIPTION varchar(200) DEFAULT NULL, PARAMETERS text DEFAULT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), UNIQUE KEY NODEUNIQUE (NAME,IP) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE PIPELINE ( ID bigint(20) NOT NULL AUTO_INCREMENT, NAME varchar(200) NOT NULL, DESCRIPTION varchar(200) DEFAULT NULL, PARAMETERS text DEFAULT NULL, CHANNEL_ID bigint(20) NOT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), UNIQUE KEY PIPELINEUNIQUE (NAME,CHANNEL_ID), KEY idx_ChannelID (CHANNEL_ID,ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE PIPELINE_NODE_RELATION ( ID bigint(20) NOT NULL AUTO_INCREMENT, NODE_ID bigint(20) NOT NULL, PIPELINE_ID bigint(20) NOT NULL, LOCATION varchar(20) NOT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), KEY idx_PipelineID (PIPELINE_ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE SYSTEM_PARAMETER ( ID bigint(20) unsigned NOT NULL, VALUE text DEFAULT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

    CREATE TABLE TABLE_HISTORY_STAT ( ID bigint(20) unsigned NOT NULL AUTO_INCREMENT, FILE_SIZE bigint(20) DEFAULT NULL, FILE_COUNT bigint(20) DEFAULT NULL, INSERT_COUNT bigint(20) DEFAULT NULL, UPDATE_COUNT bigint(20) DEFAULT NULL, DELETE_COUNT bigint(20) DEFAULT NULL, DATA_MEDIA_PAIR_ID bigint(20) DEFAULT NULL, PIPELINE_ID bigint(20) DEFAULT NULL, START_TIME timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', END_TIME timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), KEY idx_DATA_MEDIA_PAIR_ID_END_TIME (DATA_MEDIA_PAIR_ID,END_TIME), KEY idx_GmtCreate_id (GMT_CREATE,ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE TABLE_STAT ( ID bigint(20) NOT NULL AUTO_INCREMENT, FILE_SIZE bigint(20) NOT NULL, FILE_COUNT bigint(20) NOT NULL, INSERT_COUNT bigint(20) NOT NULL, UPDATE_COUNT bigint(20) NOT NULL, DELETE_COUNT bigint(20) NOT NULL, DATA_MEDIA_PAIR_ID bigint(20) NOT NULL, PIPELINE_ID bigint(20) NOT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), KEY idx_PipelineID_DataMediaPairID (PIPELINE_ID,DATA_MEDIA_PAIR_ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE THROUGHPUT_STAT ( ID bigint(20) NOT NULL AUTO_INCREMENT, TYPE varchar(20) NOT NULL, NUMBER bigint(20) NOT NULL, SIZE bigint(20) NOT NULL, PIPELINE_ID bigint(20) NOT NULL, START_TIME timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', END_TIME timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), KEY idx_PipelineID_Type_GmtCreate_ID (PIPELINE_ID,TYPE,GMT_CREATE,ID), KEY idx_PipelineID_Type_EndTime_ID (PIPELINE_ID,TYPE,END_TIME,ID), KEY idx_GmtCreate_id (GMT_CREATE,ID) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE USER ( ID bigint(20) NOT NULL AUTO_INCREMENT, USERNAME varchar(20) NOT NULL, PASSWORD varchar(20) NOT NULL, AUTHORIZETYPE varchar(20) NOT NULL, DEPARTMENT varchar(20) NOT NULL, REALNAME varchar(20) NOT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), UNIQUE KEY USERUNIQUE (USERNAME) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE DATA_MATRIX ( ID bigint(20) NOT NULL AUTO_INCREMENT, GROUP_KEY varchar(200) DEFAULT NULL, MASTER varchar(200) DEFAULT NULL, SLAVE varchar(200) DEFAULT NULL, DESCRIPTION varchar(200) DEFAULT NULL, GMT_CREATE timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', GMT_MODIFIED timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (ID), KEY GROUPKEY (GROUP_KEY) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    CREATE TABLE IF NOT EXISTS meta_history ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', gmt_create datetime NOT NULL COMMENT '创建时间', gmt_modified datetime NOT NULL COMMENT '修改时间', destination varchar(128) DEFAULT NULL COMMENT '通道名称', binlog_file varchar(64) DEFAULT NULL COMMENT 'binlog文件名', binlog_offest bigint(20) DEFAULT NULL COMMENT 'binlog偏移量', binlog_master_id varchar(64) DEFAULT NULL COMMENT 'binlog节点id', binlog_timestamp bigint(20) DEFAULT NULL COMMENT 'binlog应用的时间戳', use_schema varchar(1024) DEFAULT NULL COMMENT '执行sql时对应的schema', sql_schema varchar(1024) DEFAULT NULL COMMENT '对应的schema', sql_table varchar(1024) DEFAULT NULL COMMENT '对应的table', sql_text longtext DEFAULT NULL COMMENT '执行的sql', sql_type varchar(256) DEFAULT NULL COMMENT 'sql类型', extra text DEFAULT NULL COMMENT '额外的扩展信息', PRIMARY KEY (id), UNIQUE KEY binlog_file_offest(destination,binlog_master_id,binlog_file,binlog_offest), KEY destination (destination), KEY destination_timestamp (destination,binlog_timestamp), KEY gmt_modified (gmt_modified) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='表结构变化明细表';

    CREATE TABLE IF NOT EXISTS meta_snapshot ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', gmt_create datetime NOT NULL COMMENT '创建时间', gmt_modified datetime NOT NULL COMMENT '修改时间', destination varchar(128) DEFAULT NULL COMMENT '通道名称', binlog_file varchar(64) DEFAULT NULL COMMENT 'binlog文件名', binlog_offest bigint(20) DEFAULT NULL COMMENT 'binlog偏移量', binlog_master_id varchar(64) DEFAULT NULL COMMENT 'binlog节点id', binlog_timestamp bigint(20) DEFAULT NULL COMMENT 'binlog应用的时间戳', data longtext DEFAULT NULL COMMENT '表结构数据', extra text DEFAULT NULL COMMENT '额外的扩展信息', PRIMARY KEY (id), UNIQUE KEY binlog_file_offest(destination,binlog_master_id,binlog_file,binlog_offest), KEY destination (destination), KEY destination_timestamp (destination,binlog_timestamp), KEY gmt_modified (gmt_modified) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='表结构记录表快照表';

    insert into USER(ID,USERNAME,PASSWORD,AUTHORIZETYPE,DEPARTMENT,REALNAME,GMT_CREATE,GMT_MODIFIED) values(null,'admin','801fc357a5a74743894a','ADMIN','admin','admin',now(),now()); insert into USER(ID,USERNAME,PASSWORD,AUTHORIZETYPE,DEPARTMENT,REALNAME,GMT_CREATE,GMT_MODIFIED) values(null,'guest','471e02a154a2121dc577','OPERATOR','guest','guest',now(),now());

manager配置

## otter manager domain name
+otter.domainName = xxx.com
+## otter manager http port
+otter.port = 
+
+## otter manager database config
+otter.database.driver.class.name = com.mysql.jdbc.Driver
+otter.database.driver.url = jdbc:mysql://127.0.0.1:3306/otter?useUnicode=true&characterEncoding=UTF-8&useSSL=false
+otter.database.driver.username = 
+otter.database.driver.password = 
+
+## otter communication port
+otter.communication.manager.port = 1099
+## default zookeeper address
+otter.zookeeper.cluster.default = 127.0.0.1:2181
## otter manager domain name
+otter.domainName = xxx.com
+## otter manager http port
+otter.port = 
+
+## otter manager database config
+otter.database.driver.class.name = com.mysql.jdbc.Driver
+otter.database.driver.url = jdbc:mysql://127.0.0.1:3306/otter?useUnicode=true&characterEncoding=UTF-8&useSSL=false
+otter.database.driver.username = 
+otter.database.driver.password = 
+
+## otter communication port
+otter.communication.manager.port = 1099
+## default zookeeper address
+otter.zookeeper.cluster.default = 127.0.0.1:2181

修改完关键配置后即可执行bin/startup.sh启动manager服务,webUI访问时默认是游客,admin密码默认为admin ,otter没有提供权限控制,游客用户也能看到所有配置信息,因此不能暴露在公网。

坑:startup.sh中的java启动参数-Xss(单个线程栈内存)值都设置的是256k,使用jdk1.8是无法启动的,需要调成大一点例如512k

The stack size specified is too small, Specify at least 384k

切换到admin用户后需要配置zookeeper & node信息,这个node的id为1

image-20230228201401938

node配置

在node/conf下执行echo 1 > nid,调整node配置后启动node,看到node状态为已启动即可

image-20230228201429514

数据源配置

配置两个数据源,一个作为源库,一个作为目标库

数据表配置

表明可以用模糊匹配,也可以指定具体的表

canal配置

监听源库,开启tsdb监控表结构变化

配置Channel

添加channel

基于日志变更、行记录模式

添加pipeline

select和load机器直接选node,同步数据来源的canal选择刚才配置的

配置映射关系列表

添加源表和目标表的映射,视图模式的include/exclude分别代表选中的字段同步/选中的字段排除(不同步),配置好映射关系后保存

批量添加

schema1,table1,sourceId1,schema2,table2,sourceId2

schema1,table1,sourceId1为源表信息

schema2,table2,sourceId2为目标表信息

sourceId是数据源的id,在数据源配置页面可以找到

映射权重 (对应的数字越大,同步会越后面得到同步,优先同步权重小的数据)

启动channel

启动channel即可测试源库源表的数据变更后,目标库的目标表是否跟着一起更新。

全量数据同步

方案1

上述介绍的是增量同步数据的基本操作,但是往往源表中已经有了大量的存量数据需要全量同步一次。

Otter官方提供了一种叫自由门的方案可以用于:

  • 数据订正
  • 全量数据同步

原理是基于otter系统表retl_buffer,插入特定数据,otter系统感知后回根据表明和pk提取对应记录和正常增量同步数据一起同步到目标库

前提

  • 已经建好了两个表的同步channel并且启动

retl库建表sql

sql
/*
+供 otter 使用, otter 需要对 retl.* 的读写权限,以及对业务表的读写权限
+1. 创建database retl
+*/
+CREATE DATABASE retl;
+
+/* 2. 用户授权 给同步用户授权 */
+CREATE USER retl@'%' IDENTIFIED BY 'retl';
+GRANT USAGE ON *.* TO `retl`@'%';
+GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO `retl`@'%';
+GRANT SELECT, INSERT, UPDATE, DELETE, EXECUTE ON `retl`.* TO `retl`@'%';
+/* 业务表授权,这里可以限定只授权同步业务的表 */
+GRANT SELECT, INSERT, UPDATE, DELETE ON *.* TO `retl`@'%';  
+
+/* 3. 创建系统表 */
+USE retl;
+DROP TABLE IF EXISTS retl.retl_buffer;
+DROP TABLE IF EXISTS retl.retl_mark;
+DROP TABLE IF EXISTS retl.xdual;
+
+CREATE TABLE retl_buffer
+(	
+	ID BIGINT(20) AUTO_INCREMENT,
+	TABLE_ID INT(11) NOT NULL,
+	FULL_NAME varchar(512),
+	TYPE CHAR(1) NOT NULL,
+	PK_DATA VARCHAR(256) NOT NULL,
+	GMT_CREATE TIMESTAMP NOT NULL,
+	GMT_MODIFIED TIMESTAMP NOT NULL,
+	CONSTRAINT RETL_BUFFER_ID PRIMARY KEY (ID) 
+)  ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE retl_mark
+(	
+	ID BIGINT AUTO_INCREMENT,
+	CHANNEL_ID INT(11),
+	CHANNEL_INFO varchar(128),
+	CONSTRAINT RETL_MARK_ID PRIMARY KEY (ID) 
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
+
+CREATE TABLE xdual (
+  ID BIGINT(20) NOT NULL AUTO_INCREMENT,
+  X timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (ID)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
+
+/* 4. 插入初始化数据 */
+INSERT INTO retl.xdual(id, x) VALUES (1,now()) ON DUPLICATE KEY UPDATE x = now();
/*
+供 otter 使用, otter 需要对 retl.* 的读写权限,以及对业务表的读写权限
+1. 创建database retl
+*/
+CREATE DATABASE retl;
+
+/* 2. 用户授权 给同步用户授权 */
+CREATE USER retl@'%' IDENTIFIED BY 'retl';
+GRANT USAGE ON *.* TO `retl`@'%';
+GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO `retl`@'%';
+GRANT SELECT, INSERT, UPDATE, DELETE, EXECUTE ON `retl`.* TO `retl`@'%';
+/* 业务表授权,这里可以限定只授权同步业务的表 */
+GRANT SELECT, INSERT, UPDATE, DELETE ON *.* TO `retl`@'%';  
+
+/* 3. 创建系统表 */
+USE retl;
+DROP TABLE IF EXISTS retl.retl_buffer;
+DROP TABLE IF EXISTS retl.retl_mark;
+DROP TABLE IF EXISTS retl.xdual;
+
+CREATE TABLE retl_buffer
+(	
+	ID BIGINT(20) AUTO_INCREMENT,
+	TABLE_ID INT(11) NOT NULL,
+	FULL_NAME varchar(512),
+	TYPE CHAR(1) NOT NULL,
+	PK_DATA VARCHAR(256) NOT NULL,
+	GMT_CREATE TIMESTAMP NOT NULL,
+	GMT_MODIFIED TIMESTAMP NOT NULL,
+	CONSTRAINT RETL_BUFFER_ID PRIMARY KEY (ID) 
+)  ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE retl_mark
+(	
+	ID BIGINT AUTO_INCREMENT,
+	CHANNEL_ID INT(11),
+	CHANNEL_INFO varchar(128),
+	CONSTRAINT RETL_MARK_ID PRIMARY KEY (ID) 
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
+
+CREATE TABLE xdual (
+  ID BIGINT(20) NOT NULL AUTO_INCREMENT,
+  X timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (ID)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
+
+/* 4. 插入初始化数据 */
+INSERT INTO retl.xdual(id, x) VALUES (1,now()) ON DUPLICATE KEY UPDATE x = now();

全量同步操作

insert into retl.retl_buffer(ID,TABLE_ID, FULL_NAME,TYPE,PK_DATA,GMT_CREATE,GMT_MODIFIED) (select null,0,'$schema.table$','I',id,now(),now() from $schema.table$);
insert into retl.retl_buffer(ID,TABLE_ID, FULL_NAME,TYPE,PK_DATA,GMT_CREATE,GMT_MODIFIED) (select null,0,'$schema.table$','I',id,now(),now() from $schema.table$);

例如:insert into retl.retl_buffer(ID,TABLE_ID, FULL_NAME,TYPE,PK_DATA,GMT_CREATE,GMT_MODIFIED) (select null,0,'test.user','I',id,now(),now() from test.user);

方案2

上述基于数据库插入记录触发otter同步的方案,如果数据量大会比较耗时。也可以直接把源表数据导出并记录导出时binlog的position,先将全量数据导入一次目标表,再基于导出数据时的binlog的position进行增量同步。

导出数据

mysqldump -uxxx -pxxx --single-transaction --master-data=2 --databases xxx --tables xxx > data.sql

这样,导出的data.sql文件中会有一行信息,记录导出时的binlog文件和position

image-20230228203126413

导入数据

在目标库执行导入sql

配置canal的读取postion

image-20230228203443257

后续新建channel的操作和普通增量同步一样即可。

踩坑

更换zookeeper后manager webui无法访问

更换zookeeper后,manager管理页面无法进入,报错内容类似org.I0Itec.zkclient.exception.ZkNoNodeException: org.apache.zookeeper.KeeperException$NoNodeException: KeeperErrorCode = NoNode for /otter/channel/3/3/process 。原因是otter会在zookepper中存储一些节点信息,更换zookeeper后,需要复制节点数据,或者删除数据库中的channel、pipeline等表的数据内容 或者访问 http://域名:端口/system_reduction.htm,点击一键修复即可。

canal指定的binlog被清除

show master logsshow binlog events in 'binlog.000048' from 1226 limit 4; 更新canal中的位点配置重新启动

读取从库binlog

低权限用户需要授权,否则无法读取binlogGRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'xxx'@'%' IDENTIFIED BY '';

  • 从库需要设置log_slave_updates=1,将主库binlog中的操作写入到从库的binlog中,默认是关闭的,虽然数据可以同步,但是从库binlog没有记录这些内容。
+ + + + \ No newline at end of file diff --git "a/java/database/SQL\344\274\230\345\214\226\345\255\246\344\271\240.html" "b/java/database/SQL\344\274\230\345\214\226\345\255\246\344\271\240.html" new file mode 100644 index 000000000..4bfecc299 --- /dev/null +++ "b/java/database/SQL\344\274\230\345\214\226\345\255\246\344\271\240.html" @@ -0,0 +1,31 @@ + + + + + + SQL优化学习 | 故事 + + + + + + + + + + + + + + +
Skip to content

SQL优化学习

MySQL查询的流程

v233c07d2bec1fd9f093e45fd448aaacfa_hd.jpg

  • 客户端将查询发送至服务器
  • 服务器检查查询缓存,如果找到了,就从缓存中返回结果,否则进行下一步
  • 服务器解析,预处理
  • 查询优化器优化查询
  • 生成执行计划,执行引擎调用存储引擎API执行查询
  • 服务器将结果发送回客户端

1.客户端连接阶段

show processlist命令可以查看mysql连接的状态,以我自己的阿里云服务器为例

1.png

常见的状态:

  • Sleep:线程正在等待客户端发送数据
  • Query:连接线程正在执行查询
  • Locked:线程正在等待表锁的释放
  • Sorting result:线程正在对结果进行排序
  • Sending data:向请求端返回数据

完整状态列表说明请见官网地址

2.查询缓存

在解析一个查询语句之前,==如果查询缓存是打开的==(默认是关闭的,浪费性能),那么MySQL会优先检查这个查询是否命中查询缓存中的数据,如果命中缓存直接从缓存中拿到结果并返回给客户端。这种情况下,查询不会被解析,不用生成执行计划,不会被执行。

可以使用show variables like 'query_cache%'来查看缓存的设置情况

Snipaste_20210302_130903 如图,query_cache_type一栏时OFF关闭状态,如要开启可以修改my.cnf文件设置query_cache_type=1

query_cache_type=0时表示关闭,1时表示打开,2表示只要select 中明确指定SQL_CACHE才缓存。

3.解析、预处理、优化阶段

MySQL通过关键字将SQL语句进行解析,并生成一棵对应的“解析树”。MySQL解析器将使用MySQL语法规则验证和解析查询。语法树被校验合法后由优化器转成查询计划,一条语句可以有很多种执行方式,最后返回相同的结果。优化器的作用就是找到这其中最好的执行计划。

执行计划可以用==explain==命令查看,详见后文。

4.查询执行引擎

在解析和优化阶段,MySQL将生成查询对应的执行计划,MySQL的查询执行引擎则根据这个执行计划来完成整个查询。最常使用的也是比较最多的引擎是MyISAM引擎和InnoDB引擎。mysql5.5开始的默认存储引擎已经变更为innodb。

慢查询

可以使用show variable like 'long_query_time'命令来查看慢查询的时间

Snipaste_20210302_134903.png

可以看到MySQL中默认的慢查询为10s,然而这个时间属于根本无法接受的地步了,可以将这个时间设置为业务可以接受的范围.

一般的慢SQL定位:业务驱动,测试驱动,慢查询日志

开启慢查询日志

使用SHOW VARIABLES LIKE 'SLOW_QUERY_LOG'命令查看是否开启了慢查询日志保存

Snipaste_20210302_135529.png

可以看到默认是没有开启的

可以使用以下命令开启保存慢查询日志(==重启mysql后会失效==)

sql
set global slow_query_log = on //-- 打开慢日志
+set global slow_query_log_file = '/var/lib/mysql/test-slow.log' //--慢日志保存位置
+set global log_queries_not_using_indexes = on //-- 没有命中索引的是否要记录慢日志
+set global long_query_time = 2 (秒) //-- 执行时间超过多少为慢日志
set global slow_query_log = on //-- 打开慢日志
+set global slow_query_log_file = '/var/lib/mysql/test-slow.log' //--慢日志保存位置
+set global log_queries_not_using_indexes = on //-- 没有命中索引的是否要记录慢日志
+set global long_query_time = 2 (秒) //-- 执行时间超过多少为慢日志

或者直接修改my.cnf文件添加相应配置后重启mysql(==永久生效==)

Explain命令

EXPLAIN可以帮助开发人员分析SQL问题,EXPLAIN显示了MySQL如何使用使用SQL执行计划,可以帮助开发人员写出更优化的查询语句。使用方法,在select语句前加上Explain就可以了

例如:EXPLAIN SELECT * FROM ARTICLE WHERE ARTICLE_ID = '1'

Snipaste_20210302_145846.png 结果列说明:

1.id

这是SELECT的查询序列号,表示查询中执行select子句或操作表的顺序,id相同,执行顺序从上到下,id不同,id值越大执行优先级越高

2.select_type

表示SELECT语句的类型

  • SIMPLE:简单的select查询(不使用连接查询或者子查询)

  • PRIMARY:表示主查询,或者是最外层的查询语句,最外层查询为PRIMARY,也就是最后加载的就是PRIMARY

  • UNION:表示连接查询的第二个或者后面的查询语句,不依赖外部查询的结果集

  • DEPENDENT UNION:union中的第二个或后面的select语句,取决于外面的查询。

  • UNION RESULT:连接查询的结果;

  • SUBQUERY:子查询中的第1个SELECT语句;不依赖于外部查询的结果集

  • DEPENDENT SUBQUERY:子查询中的第1个SELECT,依赖于外面的查询;

  • DERIVED:导出表的SELECT(FROM子句的子查询),MySQL会递归执行这些子查询,把结果放在临时表里

  • DEPENDENT DERIVED:派生表依赖于另一个表

  • MATERIALIZED:物化子查询

  • UNCACHEABLE SUBQUERY:子查询,其结果无法缓存,必须针对外部查询的每一行重新进行评估

  • UNCACHEABLE UNION:UNION中的第二个或随后的 select 查询,属于不可缓存的子查询

3.table

表示查询的表

4.type

表示表的连接类型,从最好到最差的连接类型为

system > const > eq_ref > ref > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL

  1. system:表仅有一行,这是const类型的特列,平时不会出现,这个也可以忽略不计。

  2. const:数据表最多只有一个匹配行,因为只匹配一行数据,所以很快

  3. eq_ref:mysql手册是这样说的:"对于每个来自于前面的表的行组合,从该表中读取一行。这可能是最好的联接类型,除了const类型。它用在一个索引的所有部分被联接使用并且索引是UNIQUE或PRIMARY KEY"。eq_ref可以用于使用=比较带索引的列。

  4. ref:查询条件索引既不是UNIQUE也不是PRIMARY KEY的情况。ref可用于=或<或>操作符的带索引的列。

  5. ref_or_null:该连接类型如同ref,但是添加了MySQL可以专门搜索包含NULL值的行。在解决子查询中经常使用该联接类型的优化。

  6. index_merge:该连接类型表示使用了索引合并优化方法。在这种情况下,key列包含了使用的索引的清单,key_len包含了使用的索引的最长的关键元素。

  7. unique_subquery:该类型替换了下面形式的IN子查询的ref: value IN (SELECT primary_key FROM single_table WHERE some_expr) unique_subquery是一个索引查找函数,可以完全替换子查询,效率更高。

  8. index_subquery:该连接类型类似于unique_subquery。可以替换IN子查询,但只适合下列形式的子查询中的非唯一索引: value IN (SELECT key_column FROM single_table WHERE some_expr)

  9. range:只检索给定范围的行,使用一个索引来选择行。key列显示使用了哪个索引。key_len包含所使用索引的最长关键元素。当使用=、<>、>、>=、<、<=、IS NULL、<=>、BETWEEN或者IN操作符用常量比较关键字列时,类型为range

  10. index:该连接类型与ALL相同,除了只有索引树被扫描。通常比ALL快,因为索引文件通常比数据文件小。

    这个类型发生在这两种方式:

    **1)**如果索引是查询的覆盖索引,并且可用于满足表中所需的所有数据,则仅扫描索引树。在这种情况下,Extra列显示为 Using index

    **2)**使用对索引的读取执行全表扫描,以按索引顺序查找数据行。 Uses index没有出现在 Extra列中。

  11. ALL:对于每个来自于先前的表的行组合,进行完整的表扫描。(性能最差)

5.possible_keys

指MySQL能使用哪个索引在该表中找到行。如果为空,没有相关的索引。这时如果要提升性能,可以通过检验WHERE子句,看它是否引用某些列或适合索引的列来提高查询性能。如果是这样,可以创建适合的索引来提高查询的性能。

6.key

表示查询实际使用的索引,如果没有选择索引,该列的值是NULL。如果为primary的话,表示使用了主键。要想强制MySQL使用或忽视possible_keys列中的索引,在查询中使用FORCE INDEX、USE INDEX或者IGNORE INDEX

7.key_len

表示MySQL选择的索引字段按字节计算的长度,若键是NULL,则长度为NULL。通过key_len值可以确定MySQL将实际使用一个多列索引中的几个字段

8.ref

表示使用哪个列或常数与索引一起来查询记录。

9.rows

显示MySQL在表中进行查询时必须检查的行数。

10.extra

表示MySQL在处理查询时的详细信息

  • Distinct:MySQL发现第1个匹配行后,停止为当前的行组合搜索更多的行。
  • Not exists:MySQL能够对查询进行LEFT JOIN优化,发现1个匹配LEFT JOIN标准的行后,不再为前面的的行组合在该表内检查更多的行。
  • range checked for each record (index map: #):MySQL没有发现好的可以使用的索引,但发现如果来自前面的表的列值已知,可能部分索引可以使用。
  • Using filesort:MySQL需要额外的一次传递,以找出如何按排序顺序检索行。
  • Using index:从只使用索引树中的信息而不需要进一步搜索读取实际的行来检索表中的列信息。
  • Using temporary:为了解决查询,MySQL需要创建一个临时表来容纳结果。
  • Using where:WHERE 子句用于限制哪一个行匹配下一个表或发送到客户。
  • Using sort_union(...), Using union(...), Using intersect(...):这些函数说明如何为index_merge联接类型合并索引扫描。
  • Using index for group-by:类似于访问表的Using index方式,Using index for group-by表示MySQL发现了一个索引,可以用来查 询GROUP BY或DISTINCT查询的所有列,而不要额外搜索硬盘访问实际的表。

贴一个美团技术团队的文章:MySQL索引原理及慢查询优化,以下内容引用自该文章

建索引的几大原则

1.最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。

2.=和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式。

3.尽量选择区分度高的列作为索引,区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就是0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要join的字段我们都要求是0.1以上,即平均1条扫描10条记录。

4.索引列不能参与计算,保持列“干净”,比如from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,原因很简单,b+树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本太大。所以语句应该写成create_time = unix_timestamp(’2014-05-29’)。

5.尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。

慢查询优化基本步骤

0.先运行看看是否真的很慢,注意设置SQL_NO_CACHE

1.where条件单表查,锁定最小返回记录表。这句话的意思是把查询语句的where都应用到表中返回的记录数最小的表开始查起,单表每个字段分别查询,看哪个字段的区分度最高

2.explain查看执行计划,是否与1预期一致(从锁定记录较少的表开始查询)

3.order by limit 形式的sql语句让排序的表优先查

4.了解业务方使用场景

5.加索引时参照建索引的几大原则

6.观察结果,不符合预期继续从0分析

+ + + + \ No newline at end of file diff --git "a/java/devtool/Maven\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237.html" "b/java/devtool/Maven\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237.html" new file mode 100644 index 000000000..15f7ea168 --- /dev/null +++ "b/java/devtool/Maven\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237.html" @@ -0,0 +1,25 @@ + + + + + + Maven的生命周期 | 故事 + + + + + + + + + + + + + + +
Skip to content

Maven的生命周期

maven共有三个标准生命周期:

  • clean:项目清理的处理

  • default:项目部署的处理

  • site:站点文件生成的处理

Maven的构建生命周期

阶段处理描述
验证 validate验证项目验证项目是否正确且所有必须信息是可用的
编译 compile执行编译源代码编译在此阶段完成
测试 Test测试使用适当的单元测试框架(例如JUnit)运行测试。
包装 package打包创建JAR/WAR包如在 pom.xml 中定义提及的包
检查 verify检查对集成测试的结果进行检查,以保证质量达标
安装 install安装安装打包的项目到本地仓库,以供其他项目使用
部署 deploy部署拷贝最终的工程包到远程仓库中,以共享给其他开发人员和工程

平常用到最多的是mvn clean package或者mvn install,两者都会在target生成最终的jar或war文件,区别在于install命令还会将生成的jar或war包安装至本地仓库。而当需要跨项目引用jar包时,我们需要把自己的jar包上传至maven私服(远程仓库)中,之前还傻傻的直接动手去maven私服手动上传jar包,不过相比于mvn deploy肯定是麻烦很多的,maven的deploy可以很方便的将我们工程中的jar发布至maven私服中方便团队间的jar包共享。

TIP

当deploy快照版本时,maven会给快照打上时间戳以区分快照的版本,因为快照可能会频繁变更。maven会以包名是否包含SNAPSHOT来判断是否快照。

Clean 生命周期

当我们执行 mvn post-clean 命令时,Maven 调用 clean 生命周期,它包含以下阶段:

  • pre-clean:执行一些需要在clean之前完成的工作
  • clean:移除所有上一次构建生成的文件
  • post-clean:执行一些需要在clean之后立刻完成的工作

mvn clean 中的 clean 就是上面的 clean,在一个生命周期中,运行某个阶段的时候,它之前的所有阶段都会被运行,也就是说,如果执行 mvn clean 将运行以下两个生命周期阶段:

pre-clean, clean
pre-clean, clean

如果我们运行 mvn post-clean ,则运行以下三个生命周期阶段:

pre-clean, clean, post-clean
pre-clean, clean, post-clean

Site 生命周期

Maven Site 插件一般用来创建新的报告文档、部署站点等。

  • pre-site:执行一些需要在生成站点文档之前完成的工作
  • site:生成项目的站点文档
  • post-site: 执行一些需要在生成站点文档之后完成的工作,并且为部署做准备
  • site-deploy:将生成的站点文档部署到特定的服务器上
+ + + + \ No newline at end of file diff --git "a/java/devtool/nexus\346\227\240\346\263\225\344\270\213\350\275\275\344\276\235\350\265\226\347\232\204\351\227\256\351\242\230\350\256\260\345\275\225.html" "b/java/devtool/nexus\346\227\240\346\263\225\344\270\213\350\275\275\344\276\235\350\265\226\347\232\204\351\227\256\351\242\230\350\256\260\345\275\225.html" new file mode 100644 index 000000000..fa5958ff3 --- /dev/null +++ "b/java/devtool/nexus\346\227\240\346\263\225\344\270\213\350\275\275\344\276\235\350\265\226\347\232\204\351\227\256\351\242\230\350\256\260\345\275\225.html" @@ -0,0 +1,253 @@ + + + + + + nexus无法下载依赖的问题记录 | 故事 + + + + + + + + + + + + + + +
Skip to content

nexus无法下载依赖的问题记录

maven私服突然无法下载依赖,在idea中使用命令查看下详细错误信息mvn clean install -U -e -X

Client报错信息:

txt
Caused by: org.eclipse.aether.transfer.ArtifactTransferException: Could not transfer artifact org.springframework.boot:spring-boot:jar:2.4.1 from/to nexus (xxxxxxxxxxxxxxxxxxxxxxxxx): GET request of: org/springframework/boot/spring-boot/2.4.1/spring-boot-2.4.1.jar from nexus failed
Caused by: org.eclipse.aether.transfer.ArtifactTransferException: Could not transfer artifact org.springframework.boot:spring-boot:jar:2.4.1 from/to nexus (xxxxxxxxxxxxxxxxxxxxxxxxx): GET request of: org/springframework/boot/spring-boot/2.4.1/spring-boot-2.4.1.jar from nexus failed
txt
Could not transfer artifact org.springframework.boot:spring-boot:jar:2.4.1 from/to nexus (xxxxxxxxxxxxxxxxxxxxxx): GET request of: org/springframework/boot/spring-boot/2.4.1/spring-boot-2.4.1.jar from nexus failed: Premature end of Content-Length delimited message body (expected: 1,302,347; received: 76,515) -> [Help 1]
Could not transfer artifact org.springframework.boot:spring-boot:jar:2.4.1 from/to nexus (xxxxxxxxxxxxxxxxxxxxxx): GET request of: org/springframework/boot/spring-boot/2.4.1/spring-boot-2.4.1.jar from nexus failed: Premature end of Content-Length delimited message body (expected: 1,302,347; received: 76,515) -> [Help 1]

服务端错误日志:

txt
2022-04-10 16:51:14,488+0800 INFO  [qtp1752276351-145] *UNKNOWN org.apache.shiro.session.mgt.AbstractValidatingSessionManager - Enabling session validation scheduler...
+2022-04-10 16:51:14,502+0800 INFO  [qtp1752276351-51] *UNKNOWN org.sonatype.nexus.internal.security.anonymous.AnonymousManagerImpl - Loaded configuration: AnonymousConfiguration{enabled=false, userId='anonymous', realmName='NexusAuthorizingRealm'}
+2022-04-10 16:51:15,233+0800 WARN  [qtp1752276351-48] deployment org.sonatype.nexus.repository.httpbridge.internal.ViewServlet - Failure servicing: GET /repository/maven-public/org/springframework/boot/spring-boot/2.4.1/spring-boot-2.4.1.jar
+org.eclipse.jetty.io.EofException: null
+	at org.eclipse.jetty.io.ChannelEndPoint.flush(ChannelEndPoint.java:284)
+	at org.eclipse.jetty.io.WriteFlusher.flush(WriteFlusher.java:393)
+	at org.eclipse.jetty.io.WriteFlusher.write(WriteFlusher.java:277)
+	at org.eclipse.jetty.io.AbstractEndPoint.write(AbstractEndPoint.java:380)
+	at org.eclipse.jetty.server.HttpConnection$SendCallback.process(HttpConnection.java:826)
+	at org.eclipse.jetty.util.IteratingCallback.processing(IteratingCallback.java:241)
+	at org.eclipse.jetty.util.IteratingCallback.iterate(IteratingCallback.java:224)
+	at org.eclipse.jetty.server.HttpConnection.send(HttpConnection.java:550)
+	at org.eclipse.jetty.server.HttpChannel.sendResponse(HttpChannel.java:850)
+	at org.eclipse.jetty.server.HttpChannel.write(HttpChannel.java:921)
+	at org.eclipse.jetty.server.HttpOutput.write(HttpOutput.java:249)
+	at org.eclipse.jetty.server.HttpOutput.write(HttpOutput.java:225)
+	at org.eclipse.jetty.server.HttpOutput.write(HttpOutput.java:524)
+	at com.google.common.io.ByteStreams.copy(ByteStreams.java:113)
+	at org.sonatype.nexus.repository.view.Payload.copy(Payload.java:61)
+	at org.sonatype.nexus.repository.view.Content.copy(Content.java:116)
+	at org.sonatype.nexus.repository.httpbridge.internal.DefaultHttpResponseSender.send(DefaultHttpResponseSender.java:81)
+	at org.sonatype.nexus.repository.httpbridge.internal.ViewServlet.dispatchAndSend(ViewServlet.java:228)
+	at org.sonatype.nexus.repository.httpbridge.internal.ViewServlet.doService(ViewServlet.java:174)
+	at org.sonatype.nexus.repository.httpbridge.internal.ViewServlet.service(ViewServlet.java:126)
+	at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
+	at com.google.inject.servlet.ServletDefinition.doServiceImpl(ServletDefinition.java:286)
+	at com.google.inject.servlet.ServletDefinition.doService(ServletDefinition.java:276)
+	at com.google.inject.servlet.ServletDefinition.service(ServletDefinition.java:181)
+	at com.google.inject.servlet.DynamicServletPipeline.service(DynamicServletPipeline.java:71)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:85)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:112)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:61)
+	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
+	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
+	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
+	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
+	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
+	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
+	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
+	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
+	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
+	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
+	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
+	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
+	at org.apache.shiro.web.servlet.AbstractShiroFilter.executeChain(AbstractShiroFilter.java:449)
+	at org.sonatype.nexus.security.SecurityFilter.executeChain(SecurityFilter.java:85)
+	at org.apache.shiro.web.servlet.AbstractShiroFilter$1.call(AbstractShiroFilter.java:365)
+	at org.apache.shiro.subject.support.SubjectCallable.doCall(SubjectCallable.java:90)
+	at org.apache.shiro.subject.support.SubjectCallable.call(SubjectCallable.java:83)
+	at org.apache.shiro.subject.support.DelegatingSubject.execute(DelegatingSubject.java:383)
+	at org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal(AbstractShiroFilter.java:362)
+	at org.sonatype.nexus.security.SecurityFilter.doFilterInternal(SecurityFilter.java:101)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at org.sonatype.nexus.repository.httpbridge.internal.ExhaustRequestFilter.doFilter(ExhaustRequestFilter.java:80)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at com.sonatype.nexus.licensing.internal.LicensingRedirectFilter.doFilter(LicensingRedirectFilter.java:114)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at com.codahale.metrics.servlet.AbstractInstrumentedFilter.doFilter(AbstractInstrumentedFilter.java:112)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at org.sonatype.nexus.internal.web.ErrorPageFilter.doFilter(ErrorPageFilter.java:79)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at org.sonatype.nexus.internal.web.EnvironmentFilter.doFilter(EnvironmentFilter.java:101)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at org.sonatype.nexus.internal.web.HeaderPatternFilter.doFilter(HeaderPatternFilter.java:98)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at com.google.inject.servlet.DynamicFilterPipeline.dispatch(DynamicFilterPipeline.java:104)
+	at com.google.inject.servlet.GuiceFilter.doFilter(GuiceFilter.java:135)
+	at org.sonatype.nexus.bootstrap.osgi.DelegatingFilter.doFilter(DelegatingFilter.java:73)
+	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1602)
+	at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:540)
+	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:146)
+	at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:548)
+	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
+	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:257)
+	at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1700)
+	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:255)
+	at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1345)
+	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:203)
+	at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:480)
+	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1667)
+	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:201)
+	at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1247)
+	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:144)
+	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
+	at com.codahale.metrics.jetty9.InstrumentedHandler.handle(InstrumentedHandler.java:239)
+	at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:152)
+	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
+	at org.eclipse.jetty.server.Server.handle(Server.java:505)
+	at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:370)
+	at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:267)
+	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:305)
+	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103)
+	at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:117)
+	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:333)
+	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:310)
+	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:168)
+	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:126)
+	at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:366)
+	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:698)
+	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:804)
+	at java.lang.Thread.run(Thread.java:748)
+Caused by: java.io.IOException: Connection reset by peer
+	at sun.nio.ch.FileDispatcherImpl.write0(Native Method)
+	at sun.nio.ch.SocketDispatcher.write(SocketDispatcher.java:47)
+	at sun.nio.ch.IOUtil.writeFromNativeBuffer(IOUtil.java:93)
+	at sun.nio.ch.IOUtil.write(IOUtil.java:51)
+	at sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:471)
+	at org.eclipse.jetty.io.ChannelEndPoint.flush(ChannelEndPoint.java:262)
+	... 102 common frames omitted
2022-04-10 16:51:14,488+0800 INFO  [qtp1752276351-145] *UNKNOWN org.apache.shiro.session.mgt.AbstractValidatingSessionManager - Enabling session validation scheduler...
+2022-04-10 16:51:14,502+0800 INFO  [qtp1752276351-51] *UNKNOWN org.sonatype.nexus.internal.security.anonymous.AnonymousManagerImpl - Loaded configuration: AnonymousConfiguration{enabled=false, userId='anonymous', realmName='NexusAuthorizingRealm'}
+2022-04-10 16:51:15,233+0800 WARN  [qtp1752276351-48] deployment org.sonatype.nexus.repository.httpbridge.internal.ViewServlet - Failure servicing: GET /repository/maven-public/org/springframework/boot/spring-boot/2.4.1/spring-boot-2.4.1.jar
+org.eclipse.jetty.io.EofException: null
+	at org.eclipse.jetty.io.ChannelEndPoint.flush(ChannelEndPoint.java:284)
+	at org.eclipse.jetty.io.WriteFlusher.flush(WriteFlusher.java:393)
+	at org.eclipse.jetty.io.WriteFlusher.write(WriteFlusher.java:277)
+	at org.eclipse.jetty.io.AbstractEndPoint.write(AbstractEndPoint.java:380)
+	at org.eclipse.jetty.server.HttpConnection$SendCallback.process(HttpConnection.java:826)
+	at org.eclipse.jetty.util.IteratingCallback.processing(IteratingCallback.java:241)
+	at org.eclipse.jetty.util.IteratingCallback.iterate(IteratingCallback.java:224)
+	at org.eclipse.jetty.server.HttpConnection.send(HttpConnection.java:550)
+	at org.eclipse.jetty.server.HttpChannel.sendResponse(HttpChannel.java:850)
+	at org.eclipse.jetty.server.HttpChannel.write(HttpChannel.java:921)
+	at org.eclipse.jetty.server.HttpOutput.write(HttpOutput.java:249)
+	at org.eclipse.jetty.server.HttpOutput.write(HttpOutput.java:225)
+	at org.eclipse.jetty.server.HttpOutput.write(HttpOutput.java:524)
+	at com.google.common.io.ByteStreams.copy(ByteStreams.java:113)
+	at org.sonatype.nexus.repository.view.Payload.copy(Payload.java:61)
+	at org.sonatype.nexus.repository.view.Content.copy(Content.java:116)
+	at org.sonatype.nexus.repository.httpbridge.internal.DefaultHttpResponseSender.send(DefaultHttpResponseSender.java:81)
+	at org.sonatype.nexus.repository.httpbridge.internal.ViewServlet.dispatchAndSend(ViewServlet.java:228)
+	at org.sonatype.nexus.repository.httpbridge.internal.ViewServlet.doService(ViewServlet.java:174)
+	at org.sonatype.nexus.repository.httpbridge.internal.ViewServlet.service(ViewServlet.java:126)
+	at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
+	at com.google.inject.servlet.ServletDefinition.doServiceImpl(ServletDefinition.java:286)
+	at com.google.inject.servlet.ServletDefinition.doService(ServletDefinition.java:276)
+	at com.google.inject.servlet.ServletDefinition.service(ServletDefinition.java:181)
+	at com.google.inject.servlet.DynamicServletPipeline.service(DynamicServletPipeline.java:71)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:85)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:112)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:61)
+	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
+	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
+	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
+	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
+	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
+	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
+	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
+	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
+	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
+	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
+	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
+	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
+	at org.apache.shiro.web.servlet.AbstractShiroFilter.executeChain(AbstractShiroFilter.java:449)
+	at org.sonatype.nexus.security.SecurityFilter.executeChain(SecurityFilter.java:85)
+	at org.apache.shiro.web.servlet.AbstractShiroFilter$1.call(AbstractShiroFilter.java:365)
+	at org.apache.shiro.subject.support.SubjectCallable.doCall(SubjectCallable.java:90)
+	at org.apache.shiro.subject.support.SubjectCallable.call(SubjectCallable.java:83)
+	at org.apache.shiro.subject.support.DelegatingSubject.execute(DelegatingSubject.java:383)
+	at org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal(AbstractShiroFilter.java:362)
+	at org.sonatype.nexus.security.SecurityFilter.doFilterInternal(SecurityFilter.java:101)
+	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at org.sonatype.nexus.repository.httpbridge.internal.ExhaustRequestFilter.doFilter(ExhaustRequestFilter.java:80)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at com.sonatype.nexus.licensing.internal.LicensingRedirectFilter.doFilter(LicensingRedirectFilter.java:114)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at com.codahale.metrics.servlet.AbstractInstrumentedFilter.doFilter(AbstractInstrumentedFilter.java:112)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at org.sonatype.nexus.internal.web.ErrorPageFilter.doFilter(ErrorPageFilter.java:79)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at org.sonatype.nexus.internal.web.EnvironmentFilter.doFilter(EnvironmentFilter.java:101)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at org.sonatype.nexus.internal.web.HeaderPatternFilter.doFilter(HeaderPatternFilter.java:98)
+	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
+	at com.google.inject.servlet.DynamicFilterPipeline.dispatch(DynamicFilterPipeline.java:104)
+	at com.google.inject.servlet.GuiceFilter.doFilter(GuiceFilter.java:135)
+	at org.sonatype.nexus.bootstrap.osgi.DelegatingFilter.doFilter(DelegatingFilter.java:73)
+	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1602)
+	at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:540)
+	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:146)
+	at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:548)
+	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
+	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:257)
+	at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1700)
+	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:255)
+	at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1345)
+	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:203)
+	at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:480)
+	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1667)
+	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:201)
+	at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1247)
+	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:144)
+	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
+	at com.codahale.metrics.jetty9.InstrumentedHandler.handle(InstrumentedHandler.java:239)
+	at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:152)
+	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
+	at org.eclipse.jetty.server.Server.handle(Server.java:505)
+	at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:370)
+	at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:267)
+	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:305)
+	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103)
+	at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:117)
+	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:333)
+	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:310)
+	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:168)
+	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:126)
+	at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:366)
+	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:698)
+	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:804)
+	at java.lang.Thread.run(Thread.java:748)
+Caused by: java.io.IOException: Connection reset by peer
+	at sun.nio.ch.FileDispatcherImpl.write0(Native Method)
+	at sun.nio.ch.SocketDispatcher.write(SocketDispatcher.java:47)
+	at sun.nio.ch.IOUtil.writeFromNativeBuffer(IOUtil.java:93)
+	at sun.nio.ch.IOUtil.write(IOUtil.java:51)
+	at sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:471)
+	at org.eclipse.jetty.io.ChannelEndPoint.flush(ChannelEndPoint.java:262)
+	... 102 common frames omitted

解决

image-20220410223038569

在nginx的location配置中增加proxy_max_temp_file_size 0;

+ + + + \ No newline at end of file diff --git "a/java/framework/ConfigurationProperties\346\263\250\350\247\243.html" "b/java/framework/ConfigurationProperties\346\263\250\350\247\243.html" new file mode 100644 index 000000000..8aaec54e8 --- /dev/null +++ "b/java/framework/ConfigurationProperties\346\263\250\350\247\243.html" @@ -0,0 +1,31 @@ + + + + + + ConfigurationProperties注解 | 故事 + + + + + + + + + + + + + + +
Skip to content

ConfigurationProperties注解

@ConfigurationProperties注解可以从外部获取配置信息,并将其绑定到JavaBean中。

原理

SpringBoot可以让配置信息外部化,支持的配置有多种,最常见的.properties.yaml文件,启动时命令行参数--xxx、系统环境变量、Java系统属性(System.getProperties())...

@ConfigurationProperties注解的功能由ConfigurationPropertiesBindingPostProcessor这个后置处理器实现,spring容器中的enviroment.propertySources记录着外部的属性值,properties后置处理器会从中找到匹配的值绑定到JavaBean中。

属性的绑定是会被覆盖的,排序靠后的会覆盖靠前的,即越靠后的优先级越高。(os环境变量可以覆盖application.properties,java系统属性可以覆盖系统环境变量,命令行参数可以覆盖java系统属性...)

这些配置的方式和可以参照spring boot官方文档:

https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config

2. Externalized Configuration

Spring Boot lets you externalize your configuration so that you can work with the same application code in different environments. You can use a variety of external configuration sources, include Java properties files, YAML files, environment variables, and command-line arguments.

Property values can be injected directly into your beans by using the @Value annotation, accessed through Spring’s Environment abstraction, or be bound to structured objects through @ConfigurationProperties.

Spring Boot uses a very particular PropertySource order that is designed to allow sensible overriding of values. Properties are considered in the following order (with values from lower items overriding earlier ones):

  1. Default properties (specified by setting SpringApplication.setDefaultProperties).
  2. @PropertySource annotations on your @Configuration classes. Please note that such property sources are not added to the Environment until the application context is being refreshed. This is too late to configure certain properties such as logging.* and spring.main.* which are read before refresh begins.
  3. Config data (such as application.properties files).
  4. A RandomValuePropertySource that has properties only in random.*.
  5. OS environment variables.
  6. Java System properties (System.getProperties()).
  7. JNDI attributes from java:comp/env.
  8. ServletContext init parameters.
  9. ServletConfig init parameters.
  10. Properties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or system property).
  11. Command line arguments.
  12. properties attribute on your tests. Available on @SpringBootTest and the test annotations for testing a particular slice of your application.
  13. @TestPropertySource annotations on your tests.
  14. Devtools global settings properties in the $HOME/.config/spring-boot directory when devtools is active.

Config data files are considered in the following order:

  1. Application properties packaged inside your jar (application.properties and YAML variants).
  2. Profile-specific application properties packaged inside your jar (application-{profile}.properties and YAML variants).
  3. Application properties outside of your packaged jar (application.properties and YAML variants).
  4. Profile-specific application properties outside of your packaged jar (application-{profile}.properties and YAML variants).

系统环境变量的方式

这里通过系统环境变量的绑定方式大致记录下,因为java应用的docker镜像通常使用这种方式,例如docker启动指令里加上-e xxx=xxx,就是在指定docker容器的系统环境变量。比较常见的-e JAVA_OPTS=xxx ,因为java应用的镜像通常entrypoint都是sh -c java $JAVA_OPTS xxx.jar

上文中enviroment.propertySources会读取外部的配置,系统环境变量是通过System.getenv()获取的,通过docker指令给镜像添加了系统环境变量后,就会通过这种方式绑定到java应用的配置类中。

但是

通过docker指令配置系统环境变量的方式,参数的命名需要做对应的调整,例如:

java
@ConfigurationProperties(prefix="user")
+public class Test{
+    private String name;
+}
@ConfigurationProperties(prefix="user")
+public class Test{
+    private String name;
+}

如果是通过.properties文件来配置那么文件中应该是user.name=xxx,如果是通过linux系统环境变量的方式,则环境变量中应该是USER_NAME=xxx.这是因为不同操作系统对环境变量的命名规则都有严格的要求,spring boot的宽松绑定规则要尽可能兼容不同系统的限制.

linux shell变量的命名规则:可以a-zA-Z0-9,可以下划线_,按照惯例,变量名都是大写的。所以,通过环境变量读取java配置时,应该遵循的原则

  • .替换为_

  • 删除所有破折号-

  • 变量名转为大写

例:spring.main.log-startup-info -> SPRING_MAIN_LOGSTARTUPINFO

Binding from Environment Variables

Most operating systems impose strict rules around the names that can be used for environment variables. For example, Linux shell variables can contain only letters (a to z or A to Z), numbers (0 to 9) or the underscore character (_). By convention, Unix shell variables will also have their names in UPPERCASE.

Spring Boot’s relaxed binding rules are, as much as possible, designed to be compatible with these naming restrictions.

To convert a property name in the canonical-form to an environment variable name you can follow these rules:

  • Replace dots (.) with underscores (_).
  • Remove any dashes (-).
  • Convert to uppercase.

For example, the configuration property spring.main.log-startup-info would be an environment variable named SPRING_MAIN_LOGSTARTUPINFO.

Environment variables can also be used when binding to object lists. To bind to a List, the element number should be surrounded with underscores in the variable name.

For example, the configuration property my.service[0].other would use an environment variable named MY_SERVICE_0_OTHER.

+ + + + \ No newline at end of file diff --git "a/java/framework/JavaSPI\346\234\272\345\210\266\345\222\214Springboot\350\207\252\345\212\250\350\243\205\351\205\215\345\216\237\347\220\206.html" "b/java/framework/JavaSPI\346\234\272\345\210\266\345\222\214Springboot\350\207\252\345\212\250\350\243\205\351\205\215\345\216\237\347\220\206.html" new file mode 100644 index 000000000..376739953 --- /dev/null +++ "b/java/framework/JavaSPI\346\234\272\345\210\266\345\222\214Springboot\350\207\252\345\212\250\350\243\205\351\205\215\345\216\237\347\220\206.html" @@ -0,0 +1,25 @@ + + + + + + JavaSPI机制和Springboot自动装配原理 | 故事 + + + + + + + + + + + + + + +
Skip to content

JavaSPI机制和Springboot自动装配原理

SPI机制

SPI(Service Provider Interface)是一种服务发现机制,提供服务接口,且为该接口寻找服务的实现。

从Java6开始引入,是一种基于ClassLoader类加载器发现并加载服务的机制。

标准的SPI构成:

  • Service:公开的接口或抽象类,定义一个抽象的功能模块
  • Service Provider:Service接口/抽象类的具体实现
  • ServiceLoader:SPI中的核心组件,负责在运行时发现并加载Service Provider

JAVA SPI的规范要素

  • 规范的配置文件
    • 文件路径:必须要在JAR中的META-INF/services下
    • 文件名称:Service接口全限定名
    • 文件内容:Service实现类的全限定名,如果有多个,则每个类单独占一行
  • ServiceProvider必须有无参构造方法,因为要通过反射实例化
  • 保证能加载到配置文件和ServiceProvider类
    • 将jar包放到classpath下
    • jar包安装到jre的扩展目录下
    • 自定义一个ClassLoader

场景

SPI在JDBC中的应用:JDBC要求Driver实现类在类加载的时候将自身的实例注册到DriverManager中,从而加载数据库驱动,在SPI出现之前,加载数据库驱动时要执行Class.forName("com.mysql.jdbc.Driver"), 在SPI出现后,只需要引入对应依赖的JAR包后,ServiceLoader会自动去约定的路径下寻找需要加载的类。

以mysql-connector-java的jar文件为例

  • 配置文件

  • 无参构造器

image-20220410224639940

总结

  • SPI提供了一种组件发现和注册的方式,可以用于各种插件、组件的灵活替换
  • 可以实现模块间解耦
  • 面向接口+配置文件+反射
  • 应用:JDBC、SLF4J。。。

简单实例

image-20220410231011833

image-20220410231049522

image-20220410231109810

当spi-company依赖spi-lt时运行main方法:

image-20220410231225984

当spi-company依赖spi-yd时运行main方法:

image-20220410231313507

springboot的自动装配

自动装配,即auto-configuration,是基于引入的依赖jar包对springboot应用进行自动配置,提供自动配置的jar包通常以starter结尾,比如mybatis-spring-boot-starter等等。

springboot默认会扫描项目下所有的配置类并注入到ioc容器中,但集成到其他框架并不能直接注入。为了实现真正的auto configuration,springboot的自动装配也采用了和spi类似的设计思想:

使用约定的配置文件:自动装配的配置文件为META-INF/spring.factories,文件内容为org.springframework.boot.autoconfigure.EnableAutoConfiguration=class1,class2,..classN,class是自动配置类的类名

  • 提供自动配置类的jar包中,需要提供配置文件META-INF/spring.factories
  • 使用ClassLoader的getResource和getResources方法,读取classpath中的配置文件并使用反射实例化

例如mybatis-spring-boot-starter的包结构:

image-20220410232256701

总结

springboot的自动装配核心流程:springboot程序启动,通过spring factories机制加载classpath下的META-INF/spring.factories文件,筛选出所有EnableAutoConfiguration的配置类,反射实例化后注入到springIOC容器中。

todo:实现一个自定义springboot-starter

+ + + + \ No newline at end of file diff --git "a/java/framework/Java\346\227\245\345\277\227\345\217\221\345\261\225\345\216\206\345\217\262.html" "b/java/framework/Java\346\227\245\345\277\227\345\217\221\345\261\225\345\216\206\345\217\262.html" new file mode 100644 index 000000000..36675061a --- /dev/null +++ "b/java/framework/Java\346\227\245\345\277\227\345\217\221\345\261\225\345\216\206\345\217\262.html" @@ -0,0 +1,25 @@ + + + + + + Java日志发展历史 | 故事 + + + + + + + + + + + + + + +
Skip to content

Java日志发展历史

最近java社区被log4j2的远程代码执行漏洞引爆了,不过还好我们公司的日志主要用的logback,只有少数几个服务用的log4j2,不过还是因为这个加了会班=.=。而java日志有很多乱七八糟的log4j,log4j2,logback,slf4j,jul。。。

https://www.bilibili.com/video/BV1U44y1E7sE

日志发展史

阶段一

2001年以前,java是没有日志库的,打印日志全靠System.outSystem.err

缺点:

1.产生大量的io操作

2.输出的内容不能保存到文件

3.只能打印在控制台,打印完就看不到了

4.无法定制化,且粒度不够细

阶段二

大佬Ceki Gülcü在2001年开发出了日志库Log4j,后来Log4j成为了Apache项目,Ceki大佬也加入了Apache组织

Apache曾经还建议过SUN公司引入Log4j到java标准库中,但被拒绝了

阶段三

2002年2月JDK1.4发布,SUN推出了自己的日志标准库JUL(Java Util Logging),其实是照着log4j抄的,但是没抄好,在JDK1.5之后性能和可用性才有所提升

由于Log4j比JUL好用,并且比较成熟,所以Log4j更具优势

阶段四

2002年8月Apache推出了JCL(Jakarta Commons Logging),也就是日志抽象层,支持运行时动态加载日志组件的实现,也提供了一个默认实现的Simple Log。

(在ClassLoader中进行查找,如果能找到Log4j就默认使用Log4j的实现,如果没有则使用JUL实现,再没有则使用JCL内部提供的Simple Log实现)

但是JCL有三个缺点:

  1. 效率较低
  2. 容易引发混乱
  3. 使用了自定义ClassLoader的程序中,会引发内存泄露

阶段五

2006年大佬Ceki Gülcü(Log4j)因为一些原因离开了Apache,之后Ceki觉得JCL不好用,自己重新开发了一套新的日志标准接口规范Slf4j(Simple Logging Facade for Java),也可称为日志门面,很明显Slf4j是为了对标JCL,后面也证明了Slf4j比JCL更加优秀

大佬Ceki提供了一系列的桥接包来帮助Slf4j接口与其他日志库建立关系,这种方式称为桥接设置模式。

代码使用Slf4j接口,就可以实现日志的统一标准化,后续如果想要更换日志的实现,只需要引入Slf4j和相关的桥接包,再引入具体的日志标准库即可。

阶段六

Ceki大佬觉得市场上的日志标准库都是间接实现Slf4j接口,每次都需要配合桥接包,因此在2016年,Ceki大佬基于Slf4j接口开发出了Logback日志标准库作为Slf4j的默认实现,Logback也十分给力,在功能的完整度和性能上超越了所有已有的日志标准库

阶段七

2012年,Apache推出新项目Log4j2(不兼容Log4j),Log4j2全面借鉴了Slf4j+Logback,虽然log4j2有明显抄袭嫌疑,但是汲取了logback优秀的设计,还解决了一些问题,性能有了极大提升,官方测试是18倍

Log4j2不仅具有Logback的所有特性,还做了分离设计,分为log4j-api和log4j-core,api是日志接口,core是日志标准库,而且Apache也为Log4j2提供了各种桥接包

+ + + + \ No newline at end of file diff --git "a/java/framework/Mybatis Generator\351\205\215\347\275\256.html" "b/java/framework/Mybatis Generator\351\205\215\347\275\256.html" new file mode 100644 index 000000000..7a9b9fce2 --- /dev/null +++ "b/java/framework/Mybatis Generator\351\205\215\347\275\256.html" @@ -0,0 +1,761 @@ + + + + + + Mybatis Generator配置 | 故事 + + + + + + + + + + + + + + +
Skip to content

Mybatis Generator配置

  1. 仓库地址:https://github.com/mybatis/generator
  2. 官方文档:http://www.mybatis.org/generator/index.html

mbg逆向工程能快速生成实体类和mapper文件以及xml,提升开发效率。记录下不同持久层框架对应的mbg配置备忘。

  • mbg的context标签可以自定义targetRuntime,具体的区别可以在http://mybatis.org/generator/quickstart.html#target-runtime-information-and-samples查看。MyBatis3,生成的代码量比较大,会有byExample和selective相关的代码生成。MyBatis3Simple生成的代码量比较小,不会有byExample和selective方法生成。

  • 常用运行方式(还包含 ant、命令行、eclipse)

    • java代码
    • maven plugin
  • maven依赖

    xml
    <dependency>
    +			<groupId>org.mybatis.generator</groupId>
    +			<artifactId>mybatis-generator-core</artifactId>
    +			<version>1.3.5</version>
    +</dependency>
    +<dependency>
    +       <groupId>mysql</groupId>
    +       <artifactId>mysql-connector-java</artifactId>
    +       <version>8.0.16</version>
    +</dependency>
    <dependency>
    +			<groupId>org.mybatis.generator</groupId>
    +			<artifactId>mybatis-generator-core</artifactId>
    +			<version>1.3.5</version>
    +</dependency>
    +<dependency>
    +       <groupId>mysql</groupId>
    +       <artifactId>mysql-connector-java</artifactId>
    +       <version>8.0.16</version>
    +</dependency>

    使用maven插件时

    xml
    <build>
    +        <plugins>
    +            <plugin>
    +                <groupId>org.mybatis.generator</groupId>
    +                <artifactId>mybatis-generator-maven-plugin</artifactId>
    +                <version>1.3.5</version>
    +                <configuration>
    +                    <!-- 在控制台打印执行日志 -->
    +                    <verbose>true</verbose>
    +                    <!-- 重复生成时会覆盖之前的文件-->
    +                    <overwrite>true</overwrite>
    +                 <configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
    +                </configuration>
    +                <dependencies>
    +                    <dependency>
    +                        <groupId>mysql</groupId>
    +                        <artifactId>mysql-connector-java</artifactId>
    +                        <version>8.0.16</version>
    +                    </dependency>
    +                </dependencies>
    +            </plugin>
    +        </plugins>
    +    </build>
    <build>
    +        <plugins>
    +            <plugin>
    +                <groupId>org.mybatis.generator</groupId>
    +                <artifactId>mybatis-generator-maven-plugin</artifactId>
    +                <version>1.3.5</version>
    +                <configuration>
    +                    <!-- 在控制台打印执行日志 -->
    +                    <verbose>true</verbose>
    +                    <!-- 重复生成时会覆盖之前的文件-->
    +                    <overwrite>true</overwrite>
    +                 <configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
    +                </configuration>
    +                <dependencies>
    +                    <dependency>
    +                        <groupId>mysql</groupId>
    +                        <artifactId>mysql-connector-java</artifactId>
    +                        <version>8.0.16</version>
    +                    </dependency>
    +                </dependencies>
    +            </plugin>
    +        </plugins>
    +    </build>

通用mapper

xml
<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE generatorConfiguration
+        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
+        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
+
+<generatorConfiguration>
+
+    <context id="Mysql" targetRuntime="MyBatis3Simple"
+             defaultModelType="flat">
+
+        <!--property*,-->
+        <property name="beginningDelimiter" value="`"/>
+        <property name="endingDelimiter" value="`"/>
+
+        <!--plugin*,-->
+        <!-- 为继承的BaseMapper接口添加对应的实现类 -->
+        <plugin type="tk.mybatis.mapper.generator.MapperPlugin">
+            <property name="mappers" value="cn.xxx.CustomBaseMapper"/>
+        </plugin>
+
+        <!--commentGenerator?,-->
+        <!--<commentGenerator type="mybatis.generator.MyCommentGenerator"></commentGenerator>-->
+
+        <!--jdbcConnection,-->
+        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
+                        connectionURL="jdbc:mysql://xxx.xxx.xxx/xxx?autoReconnect=true&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;tinyInt1isBit=false&amp;useSSL=false"
+                        userId=""
+                        password="">
+        </jdbcConnection>
+
+        <!--javaTypeResolver?, 自定义jdbcType和javaType的映射关系,比如默认的TINYINT会对应Java的Byte类型,如果我们想让TINYINT对应JavaType为Integer就需要在解析类中自定义,这种方式适合源码方式运行mbg,使用maven plugin会比较麻烦-->
+        <javaTypeResolver type="mybatis.generator.MyJavaTypeResolver"></javaTypeResolver>
+
+        <!--javaModelGenerator,-->
+        <javaModelGenerator targetPackage="cn.xxx.dao.entity"
+                            targetProject="/Users/story/project/xx/src/main/java">
+            <!--<property name="rootClass" value="xx.BaseEntity"/>  entity会继承的类-->
+        </javaModelGenerator>
+
+        <!--sqlMapGenerator?,-->
+        <sqlMapGenerator targetPackage="mapper"
+                         targetProject="/Users/story/project/xx/src/main/resources"/>
+
+        <!--javaClientGenerator?,-->
+        <javaClientGenerator targetPackage="cn.superdesk.app.wechat.dao.mapper"
+                             targetProject="/Users/story/project/xxx/src/main/java"
+                             type="XMLMAPPER"/>
+
+        <!--table+-->
+        <table tableName="tb_xx" domainObjectName="XxEntity">
+            <generatedKey column="id" sqlStatement="Mysql" identity="true"/>
+        </table>
+        
+    </context>
+
+</generatorConfiguration>
<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE generatorConfiguration
+        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
+        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
+
+<generatorConfiguration>
+
+    <context id="Mysql" targetRuntime="MyBatis3Simple"
+             defaultModelType="flat">
+
+        <!--property*,-->
+        <property name="beginningDelimiter" value="`"/>
+        <property name="endingDelimiter" value="`"/>
+
+        <!--plugin*,-->
+        <!-- 为继承的BaseMapper接口添加对应的实现类 -->
+        <plugin type="tk.mybatis.mapper.generator.MapperPlugin">
+            <property name="mappers" value="cn.xxx.CustomBaseMapper"/>
+        </plugin>
+
+        <!--commentGenerator?,-->
+        <!--<commentGenerator type="mybatis.generator.MyCommentGenerator"></commentGenerator>-->
+
+        <!--jdbcConnection,-->
+        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
+                        connectionURL="jdbc:mysql://xxx.xxx.xxx/xxx?autoReconnect=true&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;tinyInt1isBit=false&amp;useSSL=false"
+                        userId=""
+                        password="">
+        </jdbcConnection>
+
+        <!--javaTypeResolver?, 自定义jdbcType和javaType的映射关系,比如默认的TINYINT会对应Java的Byte类型,如果我们想让TINYINT对应JavaType为Integer就需要在解析类中自定义,这种方式适合源码方式运行mbg,使用maven plugin会比较麻烦-->
+        <javaTypeResolver type="mybatis.generator.MyJavaTypeResolver"></javaTypeResolver>
+
+        <!--javaModelGenerator,-->
+        <javaModelGenerator targetPackage="cn.xxx.dao.entity"
+                            targetProject="/Users/story/project/xx/src/main/java">
+            <!--<property name="rootClass" value="xx.BaseEntity"/>  entity会继承的类-->
+        </javaModelGenerator>
+
+        <!--sqlMapGenerator?,-->
+        <sqlMapGenerator targetPackage="mapper"
+                         targetProject="/Users/story/project/xx/src/main/resources"/>
+
+        <!--javaClientGenerator?,-->
+        <javaClientGenerator targetPackage="cn.superdesk.app.wechat.dao.mapper"
+                             targetProject="/Users/story/project/xxx/src/main/java"
+                             type="XMLMAPPER"/>
+
+        <!--table+-->
+        <table tableName="tb_xx" domainObjectName="XxEntity">
+            <generatedKey column="id" sqlStatement="Mysql" identity="true"/>
+        </table>
+        
+    </context>
+
+</generatorConfiguration>
  • 生成的Mapper统一继承的接口
java
public interface CustomBaseMapper<T> extends tk.mybatis.mapper.common.BaseMapper<T>, MySqlMapper<T> {
+
+}
public interface CustomBaseMapper<T> extends tk.mybatis.mapper.common.BaseMapper<T>, MySqlMapper<T> {
+
+}
  • 自定义类型解析器
java
public class MyJavaTypeResolver implements JavaTypeResolver {
+
+    protected List<String> warnings;
+
+    protected Properties properties;
+
+    protected Context context;
+
+    protected boolean forceBigDecimals;
+
+    protected Map<Integer, JavaTypeResolverDefaultImpl.JdbcTypeInformation> typeMap;
+
+    public MyJavaTypeResolver() {
+        super();
+        properties = new Properties();
+        typeMap = new HashMap<Integer, JavaTypeResolverDefaultImpl.JdbcTypeInformation>();
+
+        typeMap.put(Types.ARRAY, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("ARRAY", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.BIGINT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("BIGINT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Long.class.getName())));
+        typeMap.put(Types.BINARY, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("BINARY", //$NON-NLS-1$
+                new FullyQualifiedJavaType("byte[]"))); //$NON-NLS-1$
+        typeMap.put(Types.BIT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("BIT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Boolean.class.getName())));
+        typeMap.put(Types.BLOB, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("BLOB", //$NON-NLS-1$
+                new FullyQualifiedJavaType("byte[]"))); //$NON-NLS-1$
+        typeMap.put(Types.BOOLEAN, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("BOOLEAN", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Boolean.class.getName())));
+        typeMap.put(Types.CHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("CHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Types.CLOB, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("CLOB", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Types.DATALINK, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("DATALINK", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.DATE, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("DATE", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Date.class.getName())));
+        typeMap.put(Types.DISTINCT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("DISTINCT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.DOUBLE, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("DOUBLE", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Double.class.getName())));
+        typeMap.put(Types.FLOAT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("FLOAT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Double.class.getName())));
+        typeMap.put(Types.INTEGER, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("INTEGER", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Integer.class.getName())));
+        typeMap.put(Types.JAVA_OBJECT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("JAVA_OBJECT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Jdbc4Types.LONGNVARCHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("LONGNVARCHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Types.LONGVARBINARY, new JavaTypeResolverDefaultImpl.JdbcTypeInformation(
+                "LONGVARBINARY", //$NON-NLS-1$
+                new FullyQualifiedJavaType("byte[]"))); //$NON-NLS-1$
+        typeMap.put(Types.LONGVARCHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("LONGVARCHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Jdbc4Types.NCHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("NCHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Jdbc4Types.NCLOB, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("NCLOB", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Jdbc4Types.NVARCHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("NVARCHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Types.NULL, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("NULL", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.OTHER, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("OTHER", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.REAL, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("REAL", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Float.class.getName())));
+        typeMap.put(Types.REF, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("REF", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.SMALLINT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("SMALLINT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Integer.class.getName())));
+        typeMap.put(Types.STRUCT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("STRUCT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.TIME, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("TIME", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Date.class.getName())));
+        typeMap.put(Types.TIMESTAMP, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("TIMESTAMP", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Date.class.getName())));
+
+        typeMap.put(Types.TINYINT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("TINYINT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Integer.class.getName())));
+
+        typeMap.put(Types.VARBINARY, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("VARBINARY", //$NON-NLS-1$
+                new FullyQualifiedJavaType("byte[]"))); //$NON-NLS-1$
+        typeMap.put(Types.VARCHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("VARCHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+
+    }
+
+    @Override
+    public void addConfigurationProperties(Properties properties) {
+        this.properties.putAll(properties);
+        forceBigDecimals = StringUtility.isTrue(properties.getProperty(PropertyRegistry.TYPE_RESOLVER_FORCE_BIG_DECIMALS));
+    }
+
+    @Override
+    public FullyQualifiedJavaType calculateJavaType(
+            IntrospectedColumn introspectedColumn) {
+        FullyQualifiedJavaType answer;
+        JavaTypeResolverDefaultImpl.JdbcTypeInformation jdbcTypeInformation = typeMap
+                .get(introspectedColumn.getJdbcType());
+
+        if (jdbcTypeInformation == null) {
+            switch (introspectedColumn.getJdbcType()) {
+                case Types.DECIMAL:
+                case Types.NUMERIC:
+                    if (introspectedColumn.getScale() > 0
+                            || introspectedColumn.getLength() > 18
+                            || forceBigDecimals) {
+                        answer = new FullyQualifiedJavaType(BigDecimal.class
+                                .getName());
+                    } else if (introspectedColumn.getLength() > 9) {
+                        answer = new FullyQualifiedJavaType(Long.class.getName());
+                    } else if (introspectedColumn.getLength() > 4) {
+                        answer = new FullyQualifiedJavaType(Integer.class.getName());
+                    } else {
+                        answer = new FullyQualifiedJavaType(Short.class.getName());
+                    }
+                    break;
+
+                default:
+                    answer = null;
+                    break;
+            }
+        } else {
+            answer = jdbcTypeInformation.getFullyQualifiedJavaType();
+        }
+
+        return answer;
+    }
+
+    @Override
+    public String calculateJdbcTypeName(IntrospectedColumn introspectedColumn) {
+        String answer;
+        JavaTypeResolverDefaultImpl.JdbcTypeInformation jdbcTypeInformation = typeMap
+                .get(introspectedColumn.getJdbcType());
+
+        if (jdbcTypeInformation == null) {
+            switch (introspectedColumn.getJdbcType()) {
+                case Types.DECIMAL:
+                    answer = "DECIMAL"; //$NON-NLS-1$
+                    break;
+                case Types.NUMERIC:
+                    answer = "NUMERIC"; //$NON-NLS-1$
+                    break;
+                default:
+                    answer = null;
+                    break;
+            }
+        } else {
+            answer = jdbcTypeInformation.getJdbcTypeName();
+        }
+
+        return answer;
+    }
+
+    @Override
+    public void setWarnings(List<String> warnings) {
+        this.warnings = warnings;
+    }
+
+    @Override
+    public void setContext(Context context) {
+        this.context = context;
+    }
+
+    public static class JdbcTypeInformation {
+        private String jdbcTypeName;
+
+        private FullyQualifiedJavaType fullyQualifiedJavaType;
+
+        public JdbcTypeInformation(String jdbcTypeName,
+                                   FullyQualifiedJavaType fullyQualifiedJavaType) {
+            this.jdbcTypeName = jdbcTypeName;
+            this.fullyQualifiedJavaType = fullyQualifiedJavaType;
+        }
+
+        public String getJdbcTypeName() {
+            return jdbcTypeName;
+        }
+
+        public FullyQualifiedJavaType getFullyQualifiedJavaType() {
+            return fullyQualifiedJavaType;
+        }
+    }
+}
public class MyJavaTypeResolver implements JavaTypeResolver {
+
+    protected List<String> warnings;
+
+    protected Properties properties;
+
+    protected Context context;
+
+    protected boolean forceBigDecimals;
+
+    protected Map<Integer, JavaTypeResolverDefaultImpl.JdbcTypeInformation> typeMap;
+
+    public MyJavaTypeResolver() {
+        super();
+        properties = new Properties();
+        typeMap = new HashMap<Integer, JavaTypeResolverDefaultImpl.JdbcTypeInformation>();
+
+        typeMap.put(Types.ARRAY, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("ARRAY", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.BIGINT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("BIGINT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Long.class.getName())));
+        typeMap.put(Types.BINARY, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("BINARY", //$NON-NLS-1$
+                new FullyQualifiedJavaType("byte[]"))); //$NON-NLS-1$
+        typeMap.put(Types.BIT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("BIT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Boolean.class.getName())));
+        typeMap.put(Types.BLOB, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("BLOB", //$NON-NLS-1$
+                new FullyQualifiedJavaType("byte[]"))); //$NON-NLS-1$
+        typeMap.put(Types.BOOLEAN, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("BOOLEAN", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Boolean.class.getName())));
+        typeMap.put(Types.CHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("CHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Types.CLOB, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("CLOB", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Types.DATALINK, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("DATALINK", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.DATE, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("DATE", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Date.class.getName())));
+        typeMap.put(Types.DISTINCT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("DISTINCT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.DOUBLE, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("DOUBLE", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Double.class.getName())));
+        typeMap.put(Types.FLOAT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("FLOAT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Double.class.getName())));
+        typeMap.put(Types.INTEGER, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("INTEGER", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Integer.class.getName())));
+        typeMap.put(Types.JAVA_OBJECT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("JAVA_OBJECT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Jdbc4Types.LONGNVARCHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("LONGNVARCHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Types.LONGVARBINARY, new JavaTypeResolverDefaultImpl.JdbcTypeInformation(
+                "LONGVARBINARY", //$NON-NLS-1$
+                new FullyQualifiedJavaType("byte[]"))); //$NON-NLS-1$
+        typeMap.put(Types.LONGVARCHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("LONGVARCHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Jdbc4Types.NCHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("NCHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Jdbc4Types.NCLOB, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("NCLOB", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Jdbc4Types.NVARCHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("NVARCHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+        typeMap.put(Types.NULL, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("NULL", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.OTHER, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("OTHER", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.REAL, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("REAL", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Float.class.getName())));
+        typeMap.put(Types.REF, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("REF", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.SMALLINT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("SMALLINT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Integer.class.getName())));
+        typeMap.put(Types.STRUCT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("STRUCT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Object.class.getName())));
+        typeMap.put(Types.TIME, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("TIME", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Date.class.getName())));
+        typeMap.put(Types.TIMESTAMP, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("TIMESTAMP", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Date.class.getName())));
+
+        typeMap.put(Types.TINYINT, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("TINYINT", //$NON-NLS-1$
+                new FullyQualifiedJavaType(Integer.class.getName())));
+
+        typeMap.put(Types.VARBINARY, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("VARBINARY", //$NON-NLS-1$
+                new FullyQualifiedJavaType("byte[]"))); //$NON-NLS-1$
+        typeMap.put(Types.VARCHAR, new JavaTypeResolverDefaultImpl.JdbcTypeInformation("VARCHAR", //$NON-NLS-1$
+                new FullyQualifiedJavaType(String.class.getName())));
+
+    }
+
+    @Override
+    public void addConfigurationProperties(Properties properties) {
+        this.properties.putAll(properties);
+        forceBigDecimals = StringUtility.isTrue(properties.getProperty(PropertyRegistry.TYPE_RESOLVER_FORCE_BIG_DECIMALS));
+    }
+
+    @Override
+    public FullyQualifiedJavaType calculateJavaType(
+            IntrospectedColumn introspectedColumn) {
+        FullyQualifiedJavaType answer;
+        JavaTypeResolverDefaultImpl.JdbcTypeInformation jdbcTypeInformation = typeMap
+                .get(introspectedColumn.getJdbcType());
+
+        if (jdbcTypeInformation == null) {
+            switch (introspectedColumn.getJdbcType()) {
+                case Types.DECIMAL:
+                case Types.NUMERIC:
+                    if (introspectedColumn.getScale() > 0
+                            || introspectedColumn.getLength() > 18
+                            || forceBigDecimals) {
+                        answer = new FullyQualifiedJavaType(BigDecimal.class
+                                .getName());
+                    } else if (introspectedColumn.getLength() > 9) {
+                        answer = new FullyQualifiedJavaType(Long.class.getName());
+                    } else if (introspectedColumn.getLength() > 4) {
+                        answer = new FullyQualifiedJavaType(Integer.class.getName());
+                    } else {
+                        answer = new FullyQualifiedJavaType(Short.class.getName());
+                    }
+                    break;
+
+                default:
+                    answer = null;
+                    break;
+            }
+        } else {
+            answer = jdbcTypeInformation.getFullyQualifiedJavaType();
+        }
+
+        return answer;
+    }
+
+    @Override
+    public String calculateJdbcTypeName(IntrospectedColumn introspectedColumn) {
+        String answer;
+        JavaTypeResolverDefaultImpl.JdbcTypeInformation jdbcTypeInformation = typeMap
+                .get(introspectedColumn.getJdbcType());
+
+        if (jdbcTypeInformation == null) {
+            switch (introspectedColumn.getJdbcType()) {
+                case Types.DECIMAL:
+                    answer = "DECIMAL"; //$NON-NLS-1$
+                    break;
+                case Types.NUMERIC:
+                    answer = "NUMERIC"; //$NON-NLS-1$
+                    break;
+                default:
+                    answer = null;
+                    break;
+            }
+        } else {
+            answer = jdbcTypeInformation.getJdbcTypeName();
+        }
+
+        return answer;
+    }
+
+    @Override
+    public void setWarnings(List<String> warnings) {
+        this.warnings = warnings;
+    }
+
+    @Override
+    public void setContext(Context context) {
+        this.context = context;
+    }
+
+    public static class JdbcTypeInformation {
+        private String jdbcTypeName;
+
+        private FullyQualifiedJavaType fullyQualifiedJavaType;
+
+        public JdbcTypeInformation(String jdbcTypeName,
+                                   FullyQualifiedJavaType fullyQualifiedJavaType) {
+            this.jdbcTypeName = jdbcTypeName;
+            this.fullyQualifiedJavaType = fullyQualifiedJavaType;
+        }
+
+        public String getJdbcTypeName() {
+            return jdbcTypeName;
+        }
+
+        public FullyQualifiedJavaType getFullyQualifiedJavaType() {
+            return fullyQualifiedJavaType;
+        }
+    }
+}
  • 入口方法
java
public class MyBatisGeneratorTool {
+    public static void main(String[] args) {
+
+        URL resource = Thread.currentThread().getContextClassLoader().getResource("");
+
+        System.out.println(resource.getPath());
+
+        List<String>        warnings   = new ArrayList<String>();
+        boolean             overwrite  = true;
+        File                configFile = new File(resource.getPath() + "../../src/test/resources/generator/generatorConfig.xml");
+        ConfigurationParser cp         = new ConfigurationParser(warnings);
+        Configuration       config     = null;
+
+        try {
+            config = cp.parseConfiguration(configFile);
+        } catch (IOException e) {
+            e.printStackTrace();
+        } catch (XMLParserException e) {
+            e.printStackTrace();
+        }
+
+        DefaultShellCallback callback         = new DefaultShellCallback(overwrite);
+        MyBatisGenerator     myBatisGenerator = null;
+
+        try {
+            myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
+        } catch (InvalidConfigurationException e) {
+            e.printStackTrace();
+        }
+
+        try {
+            myBatisGenerator.generate(null);
+        } catch (SQLException e) {
+            e.printStackTrace();
+        } catch (IOException e) {
+            e.printStackTrace();
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+}
public class MyBatisGeneratorTool {
+    public static void main(String[] args) {
+
+        URL resource = Thread.currentThread().getContextClassLoader().getResource("");
+
+        System.out.println(resource.getPath());
+
+        List<String>        warnings   = new ArrayList<String>();
+        boolean             overwrite  = true;
+        File                configFile = new File(resource.getPath() + "../../src/test/resources/generator/generatorConfig.xml");
+        ConfigurationParser cp         = new ConfigurationParser(warnings);
+        Configuration       config     = null;
+
+        try {
+            config = cp.parseConfiguration(configFile);
+        } catch (IOException e) {
+            e.printStackTrace();
+        } catch (XMLParserException e) {
+            e.printStackTrace();
+        }
+
+        DefaultShellCallback callback         = new DefaultShellCallback(overwrite);
+        MyBatisGenerator     myBatisGenerator = null;
+
+        try {
+            myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
+        } catch (InvalidConfigurationException e) {
+            e.printStackTrace();
+        }
+
+        try {
+            myBatisGenerator.generate(null);
+        } catch (SQLException e) {
+            e.printStackTrace();
+        } catch (IOException e) {
+            e.printStackTrace();
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+}

image-20220409210512601

mybatis-plus

xml
<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE generatorConfiguration
+        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
+        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
+<generatorConfiguration>
+    <!-- context 是逆向工程的主要配置信息 -->
+    <!-- id:起个名字 -->
+    <!-- targetRuntime:设置生成的文件适用于那个 mybatis 版本 -->
+    <context id="default" targetRuntime="MyBatis3">
+        <!--optional,指在创建class时,对注释进行控制-->
+        <commentGenerator>
+            <property name="suppressDate" value="true"/>
+            <!-- 是否去除自动生成的注释 true:是 : false:否 -->
+            <property name="suppressAllComments" value="true"/>
+        </commentGenerator>
+        <!--jdbc的数据库连接 wg_insert 为数据库名字-->
+        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
+                        connectionURL="jdbc:mysql://xxx/xx?autoReconnect=true&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;tinyInt1isBit=false&amp;useSSL=false&amp;serverTimezone=Asia/Shanghai&amp;nullCatalogMeansCurrent=true"
+                        userId=""
+                        password="">
+        </jdbcConnection>
+        <!--非必须,类型处理器,在数据库类型和java类型之间的转换控制-->
+        <javaTypeResolver>
+            <!-- 默认情况下数据库中的 decimal,bigInt 在 Java 对应是 sql 下的 BigDecimal 类 -->
+            <!-- 不是 double 和 long 类型 -->
+            <!-- 使用常用的基本类型代替 sql 包下的引用类型 -->
+            <property name="forceBigDecimals" value="false"/>
+        </javaTypeResolver>
+        <!-- targetPackage:生成的实体类所在的包 -->
+        <!-- targetProject:生成的实体类所在的硬盘位置 -->
+        <javaModelGenerator targetPackage="xxx"
+                            targetProject="/Users/story/project/xxx/src/main/java">
+            <!-- 是否允许子包 -->
+            <property name="enableSubPackages" value="false"/>
+            <!-- 是否对modal添加构造函数 -->
+<!--            <property name="constructorBased" value="true"/>-->
+            <!-- 是否清理从数据库中查询出的字符串左右两边的空白字符 -->
+<!--            <property name="trimStrings" value="true"/>-->
+            <!-- 建立modal对象是否不可改变 即生成的modal对象不会有setter方法,只有构造方法 -->
+<!--            <property name="immutable" value="false"/>-->
+        </javaModelGenerator>
+        <!-- targetPackage 和 targetProject:生成的 mapper 文件的包和位置 -->
+        <sqlMapGenerator targetPackage="xx"
+                         targetProject="/Users/story/project/xx/src/main/resources">
+            <!-- 针对数据库的一个配置,是否把 schema 作为字包名 -->
+            <property name="enableSubPackages" value="false"/>
+        </sqlMapGenerator>
+        <!-- targetPackage 和 targetProject:生成的 interface 文件的包和位置 -->
+        <javaClientGenerator type="XMLMAPPER"
+                             targetPackage="xx" targetProject="/Users/story/project/xxx/src/main/java">
+            <!-- 针对 oracle 数据库的一个配置,是否把 schema 作为字包名 -->
+            <property name="enableSubPackages" value="false"/>
+        </javaClientGenerator>
+        <!-- tableName是数据库中的表名,domainObjectName是生成的JAVA模型名,后面的参数不用改,要生成更多的表就在下面继续加table标签 -->
+        <table tableName="xxx" domainObjectName="XxxEntity">
+        </table>
+    </context>
+</generatorConfiguration>
<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE generatorConfiguration
+        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
+        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
+<generatorConfiguration>
+    <!-- context 是逆向工程的主要配置信息 -->
+    <!-- id:起个名字 -->
+    <!-- targetRuntime:设置生成的文件适用于那个 mybatis 版本 -->
+    <context id="default" targetRuntime="MyBatis3">
+        <!--optional,指在创建class时,对注释进行控制-->
+        <commentGenerator>
+            <property name="suppressDate" value="true"/>
+            <!-- 是否去除自动生成的注释 true:是 : false:否 -->
+            <property name="suppressAllComments" value="true"/>
+        </commentGenerator>
+        <!--jdbc的数据库连接 wg_insert 为数据库名字-->
+        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
+                        connectionURL="jdbc:mysql://xxx/xx?autoReconnect=true&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;tinyInt1isBit=false&amp;useSSL=false&amp;serverTimezone=Asia/Shanghai&amp;nullCatalogMeansCurrent=true"
+                        userId=""
+                        password="">
+        </jdbcConnection>
+        <!--非必须,类型处理器,在数据库类型和java类型之间的转换控制-->
+        <javaTypeResolver>
+            <!-- 默认情况下数据库中的 decimal,bigInt 在 Java 对应是 sql 下的 BigDecimal 类 -->
+            <!-- 不是 double 和 long 类型 -->
+            <!-- 使用常用的基本类型代替 sql 包下的引用类型 -->
+            <property name="forceBigDecimals" value="false"/>
+        </javaTypeResolver>
+        <!-- targetPackage:生成的实体类所在的包 -->
+        <!-- targetProject:生成的实体类所在的硬盘位置 -->
+        <javaModelGenerator targetPackage="xxx"
+                            targetProject="/Users/story/project/xxx/src/main/java">
+            <!-- 是否允许子包 -->
+            <property name="enableSubPackages" value="false"/>
+            <!-- 是否对modal添加构造函数 -->
+<!--            <property name="constructorBased" value="true"/>-->
+            <!-- 是否清理从数据库中查询出的字符串左右两边的空白字符 -->
+<!--            <property name="trimStrings" value="true"/>-->
+            <!-- 建立modal对象是否不可改变 即生成的modal对象不会有setter方法,只有构造方法 -->
+<!--            <property name="immutable" value="false"/>-->
+        </javaModelGenerator>
+        <!-- targetPackage 和 targetProject:生成的 mapper 文件的包和位置 -->
+        <sqlMapGenerator targetPackage="xx"
+                         targetProject="/Users/story/project/xx/src/main/resources">
+            <!-- 针对数据库的一个配置,是否把 schema 作为字包名 -->
+            <property name="enableSubPackages" value="false"/>
+        </sqlMapGenerator>
+        <!-- targetPackage 和 targetProject:生成的 interface 文件的包和位置 -->
+        <javaClientGenerator type="XMLMAPPER"
+                             targetPackage="xx" targetProject="/Users/story/project/xxx/src/main/java">
+            <!-- 针对 oracle 数据库的一个配置,是否把 schema 作为字包名 -->
+            <property name="enableSubPackages" value="false"/>
+        </javaClientGenerator>
+        <!-- tableName是数据库中的表名,domainObjectName是生成的JAVA模型名,后面的参数不用改,要生成更多的表就在下面继续加table标签 -->
+        <table tableName="xxx" domainObjectName="XxxEntity">
+        </table>
+    </context>
+</generatorConfiguration>

image-20220409211128783

+ + + + \ No newline at end of file diff --git "a/java/framework/POI\344\272\213\344\273\266\346\250\241\345\274\217\350\247\243\346\236\220\345\271\266\350\257\273\345\217\226Excel\346\226\207\344\273\266\346\225\260\346\215\256.html" "b/java/framework/POI\344\272\213\344\273\266\346\250\241\345\274\217\350\247\243\346\236\220\345\271\266\350\257\273\345\217\226Excel\346\226\207\344\273\266\346\225\260\346\215\256.html" new file mode 100644 index 000000000..a538f21a4 --- /dev/null +++ "b/java/framework/POI\344\272\213\344\273\266\346\250\241\345\274\217\350\247\243\346\236\220\345\271\266\350\257\273\345\217\226Excel\346\226\207\344\273\266\346\225\260\346\215\256.html" @@ -0,0 +1,1833 @@ + + + + + + POI事件模式解析并读取Excel文件数据 | 故事 + + + + + + + + + + + + + + +
Skip to content

POI事件模式解析并读取Excel文件数据

背景

传统的POI用户模式解析Excel为我们操作提供了丰富的API,用起来很方便,但是这种模式是一次性将Excel中的数据全部写入内存,并且要还封装结构,使得内存的占用远远超过原本Excel文件的大小,稍微大一点的文件采用用户模式进行解析都会内存溢出,由于现在做的项目要处理的Excel数据量普遍都很大,而且业务对导入功能的使用尤其频繁,所以必须采用事件模式的解析方式来实现业务需求.

事件模式避免内存溢出的原理很简单,就是采用SAX方式解析Excel文件底层的XML文件,这样逐行读取,逐行处理的方式可以完美解决内存占用的问题.

简单的入门程序可以从POI官网找到,下面是根据官方demo封装的一个比较完善的类,只需要创建一个类继承该抽象类然后实现抽象方法rowHandler,然后在rowHandler里进行我们需要的业务操作即可.

关于事件模式解析Excel的代码执行具体流程以后有时间再写篇文章进行总结~

代码

java
/**
+ * @author storyxc
+ * @description POI事件模式读取Excel抽象类
+ * @createdTime 2020/6/18 14:27
+ */
+public abstract class POIEventModeHandler extends DefaultHandler {
+
+    /**
+     * 构造方法
+     *
+     * @param parseCellValueStringFlag 是否将单元格值解析成字符串
+     * @param ignoreFirstRow           是否忽略首行(一般是表头)
+     */
+    public POIEventModeHandler(final boolean parseCellValueStringFlag, final boolean ignoreFirstRow) {
+        this.parseCellValueStringFlag = parseCellValueStringFlag;
+        this.ignoreFirstRow = ignoreFirstRow;
+    }
+
+    /**
+     * 构造方法
+     *
+     * @param parseCellValueStringFlag 将单元格值解析成字符串
+     * @param ignoreFirstRow           忽略首行
+     * @param dataFormatStyle          指定日期类型解析格式 默认yyyy-MM-dd
+     */
+    public POIEventModeHandler(final boolean parseCellValueStringFlag, final boolean ignoreFirstRow, final String dataFormatStyle) {
+        this.parseCellValueStringFlag = parseCellValueStringFlag;
+        this.ignoreFirstRow = ignoreFirstRow;
+        this.dateFormatStyle = dataFormatStyle;
+    }
+
+    /**
+     * 单元格类型索引
+     */
+    protected enum CellDataType {
+        /**
+         * 布尔值
+         */
+        BOOL("b"),
+        /**
+         * 异常错误
+         */
+        ERROR("e"),
+        /**
+         * 公式
+         */
+        FORMULA("str"),
+        /**
+         * 字符
+         */
+        INLINESTR("inlineStr"),
+        /**
+         * 共享字符表
+         */
+        SSTINDEX("s"),
+        /**
+         * 数值
+         */
+        NUMBER("n"),
+        /**
+         * 空
+         */
+        NULL("null");
+
+        private final String cellType;
+
+        String getCellType() {
+            return this.cellType;
+        }
+
+        CellDataType(final String cellType) {
+            this.cellType = cellType;
+        }
+
+        static CellDataType getCellTypeEnum(final String cellType) {
+            for (final CellDataType cellDataType : CellDataType.values()) {
+                //数字类型时c标签没有t属性
+                if (cellType == null) {
+                    return NUMBER;
+                } else if (StringUtils.equals(cellDataType.getCellType(), cellType)) {
+                    return cellDataType;
+                }
+            }
+            return null;
+        }
+    }
+
+    /**
+     * sheet样式
+     */
+    @Data
+    @EqualsAndHashCode(callSuper = false)
+    protected class SheetStyle {
+        /**
+         * sheet顺序索引
+         */
+        private int sheetId;
+        /**
+         * sheet名称
+         */
+        private String sheetName;
+        /**
+         * 缩放百分比
+         */
+        private double zoomPercent;
+        /**
+         * 自适应
+         */
+        private boolean fitToPage = false;
+        /**
+         * 是否显示网格线
+         */
+        private boolean showGridLines = true;
+        /**
+         * 默认行高
+         */
+        private double defaultRowHeight;
+        /**
+         * sheet中每一列的样式
+         */
+        Map<String, ColumnStyle> columnStyles;
+        /**
+         * 合并单元格
+         */
+        private List<Integer[]> mergeCells;
+        /* 打印属性 */
+        /**
+         * 上边距
+         */
+        private double topMargin = 0.75;
+        /**
+         * 下边距
+         */
+        private double bottomMargin = 0.75;
+        /**
+         * 左边距
+         */
+        private double leftMargin = 0.7;
+        /**
+         * 右边距
+         */
+        private double rightMargin = 0.7;
+        /**
+         * 页脚边距
+         */
+        private double footerMargin = 0.3;
+        /**
+         * 页头边距
+         */
+        private double headerMargin = 0.3;
+        /**
+         * 缩放比例
+         */
+        private short scale = 100;
+        /**
+         * 页宽
+         */
+        private short fitWidth = 1;
+        /**
+         * 页高
+         */
+        private short fitHeight = 1;
+        /**
+         * 纸张设置
+         */
+        private short pageSize = PrintSetup.A4_PAPERSIZE;
+        /**
+         * 垂直居中
+         */
+        private boolean verticallyCenter;
+        /**
+         * 水平居中
+         */
+        private boolean horizontallyCenter;
+        /**
+         * 横向打印
+         */
+        private boolean landscape;
+        /**
+         * 网格线
+         */
+        private boolean printGridlines;
+        /**
+         * 行号列标
+         */
+        private boolean printHeadings;
+        /**
+         * 草稿品质
+         */
+        private boolean draft;
+        /**
+         * 单色打印
+         */
+        private boolean noColor;
+        /**
+         * 打印顺序 true:先行后列 false:先列后行
+         */
+        private boolean leftToRigh;
+        /**
+         * 起始页页码自动
+         */
+        private boolean usePage;
+        /**
+         * 起始页码
+         */
+        private short pageStart = 1;
+        /**
+         * 页眉页脚与页边距对齐
+         */
+        private boolean alignWithMargins = true;
+    }
+
+    /**
+     * 列样式
+     */
+    @Data
+    @EqualsAndHashCode(callSuper = false)
+    protected class ColumnStyle {
+        /**
+         * 列宽度
+         */
+        private double columnWidth;
+        /**
+         * 列是否隐藏
+         */
+        private boolean isHidden;
+        /**
+         * 默认的列样式
+         */
+        private int defaultColumnStyleIndex;
+    }
+
+    /**
+     * 行数据
+     */
+    @Data
+    @EqualsAndHashCode(callSuper = false)
+    protected class RowData {
+        /**
+         * 当前行高
+         */
+        private double rowHeight;
+        /**
+         * 当前行数据
+         */
+        private List<Object> cellDataValues;
+        /**
+         * 当前行单元格样式索引
+         */
+        private List<Integer> cellStyles;
+        /**
+         * 当前行单元格数据类型
+         */
+        private List<CellDataType> cellDataTypes;
+        /**
+         * 单元格公式
+         */
+        private List<String> cellFormula;
+
+        RowData() {
+            this.cellDataValues = new ArrayList<>();
+            this.cellStyles = new ArrayList<>();
+            this.cellDataTypes = new ArrayList<>();
+            this.cellFormula = new ArrayList<>();
+        }
+    }
+
+    /**
+     * sheet打印区域
+     */
+    @Data
+    @EqualsAndHashCode(callSuper = false)
+    protected class SheetPrint {
+        private String sheetName;
+        private String sheetIndex;
+        private String printArea;
+        private String printTitleRows;
+        private String printTitleColumns;
+    }
+
+    /**
+     * 共享字符表
+     */
+    protected SharedStringsTable sst;
+    /**
+     * 单元格样式
+     */
+    private StylesTable stylesTable;
+    /**
+     * sheet游标
+     */
+    private int sheetIndex = 0;
+    /**
+     * 行游标
+     */
+    private int rowIndex;
+    /**
+     * 列坐标
+     */
+    private int colIndex;
+    /**
+     * 最大行数量
+     */
+    protected int rowMax;
+    /**
+     * 最大列数量
+     */
+    protected int colMax;
+    /**
+     * 是否为有效数据
+     */
+    private boolean valueFlag;
+    /**
+     * T
+     */
+    private boolean isTElement = false;
+    /**
+     * 记录当前值
+     */
+    private StringBuilder cellBuilder;
+    /**
+     * 是否过滤首行
+     */
+    protected boolean ignoreFirstRow = false;
+    /**
+     * 格式化日期样式
+     */
+    protected String dateFormatStyle = "yyyy-MM-dd";
+    /**
+     * 格式化样式
+     */
+    protected Map<Short, String> formatStyleMap = new HashMap<>();
+    /**
+     * 数据格式化formatter
+     */
+    private final DataFormatter dataFormatter = new DataFormatter();
+    /**
+     * 当前sheet样式
+     */
+    private SheetStyle sheetStyle;
+    /**
+     * 行数据
+     */
+    private RowData rowData;
+    /**
+     * sheet打印区域 key:sheetIndex
+     */
+    protected final Map<String, SheetPrint> sheetPrintMap = new HashMap<>();
+    /**
+     * 所有sheetName,key:sheetIndex
+     */
+    private final Map<String, String> sheetNameMap = new HashMap<>();
+    /**
+     * 当前打印标签的sheetIndex
+     */
+    private String localSheetId;
+    /**
+     * 辨别是否打印区域数据
+     */
+    private boolean printFlag;
+    /**
+     * 是否解析成String
+     */
+    private final boolean parseCellValueStringFlag;
+    /**
+     * 是否读取公式
+     */
+    protected boolean isReadFormula = false;
+    /**
+     * 是否共享公式
+     */
+    private boolean isSharedFormula;
+    /**
+     * 共享公式存放
+     */
+    private final Map<String, String> sharedFormulaMap = new HashMap<>();
+    /**
+     * 共享公式插值
+     */
+    private final Map<String, List<Integer>> diffMap = new HashMap<>();
+    /**
+     * 共享公式存放key
+     */
+    private String si;
+
+    /**
+     * 核心抽象方法,读取一行后的操作,将业务逻辑在该方法中实现
+     *
+     * @param sheetIndex
+     * @param rowIndex
+     * @param rowData
+     */
+    protected abstract void rowHandler(final int sheetIndex, final int rowIndex, final RowData rowData);
+
+    /**
+     * sheet处理完后调用的方法
+     *
+     * @param sheetIndex
+     * @param sheetStyle
+     */
+    protected void sheetOver(final int sheetIndex, final SheetStyle sheetStyle) {
+        return;
+    }
+
+    /**
+     * 整个工作簿处理完后调用
+     */
+    protected void workbookOver() {
+        return;
+    }
+
+    /**
+     * 处理完整的Excel
+     *
+     * @param filePath
+     * @throws OpenXML4JException
+     * @throws IOException
+     * @throws SAXException
+     */
+    public void handleExcel(final String filePath) throws OpenXML4JException, IOException, SAXException {
+        OPCPackage opcPackage = null;
+        try {
+            opcPackage = OPCPackage.open(filePath);
+            final XMLReader parser = fetchSheetParser();
+            final XSSFReader.SheetIterator sheets = parseSheet(opcPackage, parser);
+            while (sheets.hasNext()) {
+                final InputStream sheet = sheets.next();
+                sheetStyle = new SheetStyle();
+                sheetStyle.setSheetName(sheets.getSheetName());
+                final InputSource sheetSource = new InputSource(sheet);
+                parser.parse(sheetSource);
+                sheet.close();
+            }
+        } finally {
+            if (opcPackage != null) {
+                opcPackage.close();
+            }
+        }
+        workbookOver();
+    }
+
+    /**
+     * 处理指定的sheet页
+     * @param filePath
+     * @param sheetIdx
+     * @throws OpenXML4JException
+     * @throws SAXException
+     * @throws IOException
+     */
+    public void handleExcel(final String filePath, final int sheetIdx) throws OpenXML4JException, SAXException, IOException {
+        OPCPackage opcPackage = null;
+        try {
+            opcPackage = OPCPackage.open(filePath);
+            final XMLReader parser = fetchSheetParser();
+            final XSSFReader.SheetIterator sheets = parseSheet(opcPackage, parser);
+            while (sheets.hasNext()) {
+                final InputStream sheet = sheets.next();
+                if (sheetIndex + 1 != sheetIdx) {
+                    continue;
+                }
+                sheetStyle = new SheetStyle();
+                sheetStyle.setSheetName(sheets.getSheetName());
+                final InputSource sheetSource = new InputSource(sheet);
+                parser.parse(sheetSource);
+                sheet.close();
+            }
+        } finally {
+            if (opcPackage != null) {
+                opcPackage.close();
+            }
+        }
+        workbookOver();
+    }
+
+    private XSSFReader.SheetIterator parseSheet(OPCPackage opcPackage, XMLReader parser) throws IOException, OpenXML4JException, SAXException {
+        final XSSFReader reader = new XSSFReader(opcPackage);
+        sst = reader.getSharedStringsTable();
+        stylesTable = reader.getStylesTable();
+        final XSSFReader.SheetIterator sheets = (XSSFReader.SheetIterator) reader.getSheetsData();
+        //读取工作簿内容
+        final InputStream workbookData = reader.getWorkbookData();
+        final InputSource workbookDataSource = new InputSource(workbookData);
+        parser.parse(workbookDataSource);
+        workbookData.close();
+        //读取单元格样式
+        final InputStream stylesData = reader.getStylesData();
+        final InputSource stylesDataSource = new InputSource(stylesData);
+        parser.parse(stylesDataSource);
+        stylesData.close();
+        return sheets;
+
+    }
+
+    private XMLReader fetchSheetParser() throws SAXException {
+        final XMLReader parser = XMLReaderFactory.createXMLReader("org.apache.xerces.parsers.SAXParser");
+        parser.setContentHandler(this);
+        return parser;
+    }
+
+
+    /**
+     * 读取每对标签的开始标签时调用的方法
+     *
+     * @param uri
+     * @param localName
+     * @param name       当前标签名
+     * @param attributes 当前标签上的属性
+     */
+    @Override
+    public void startElement(final String uri, final String localName,
+                             final String name, final Attributes attributes) {
+        if ("numFmt".equals(name)) {
+            final short numFmtId = Short.parseShort(attributes.getValue("numFmtId"));
+            final String formatCode = attributes.getValue("formatCode");
+            formatStyleMap.put(numFmtId, formatCode);
+        } else if ("mergeCells".equals(name)) {
+            sheetStyle.setMergeCells(new ArrayList<Integer[]>());
+        } else if ("mergeCell".equals(name)) {
+            final String[] range = attributes.getValue("ref").split(":");
+            final Integer[] positionDx = new Integer[4];
+            final int[] start = parsePosition(range[0]);
+            final int[] end = parsePosition(range[1]);
+            positionDx[0] = start[1];
+            positionDx[1] = end[1];
+            positionDx[2] = start[0] - 1;
+            positionDx[3] = end[0] - 1;
+            sheetStyle.getMergeCells().add(positionDx);
+        } else if ("pageSetUpPr".equals(name)) {
+            if (attributes.getValue("fitToPage") != null && attributes.getValue("fitToPage").equals("1")) {
+                sheetStyle.setFitToPage(true);
+            }
+        } else if ("sheetView".equals(name)) {
+            if (attributes.getValue("showGridLines") != null && attributes.getValue("showGridLines").equals("0")) {
+                sheetStyle.setShowGridLines(false);
+            }
+            if (attributes.getValue("zoomScale") != null) {
+                sheetStyle.setZoomPercent((int) Double.parseDouble(attributes.getValue("zoomScale")));
+            }
+        } else if ("sheetFormatPr".equals(name)) {
+            if (attributes.getValue("defaultRowHeight") != null) {
+                sheetStyle.setDefaultRowHeight(Double.parseDouble(attributes.getValue("defaultRowHeight")));
+            }
+        } else if ("sheetData".equals(name)) {
+            //开始读取sheet页数据
+            sheetIndex++;
+            rowIndex = 1;
+            cellBuilder = new StringBuilder();
+            rowData = new RowData();
+            sheetHandler(sheetIndex, sheetStyle);
+        } else if ("dimension".equals(name)) {
+            handleMax(attributes.getValue("ref"));
+            sheetStyle.setColumnStyles(new HashMap<String, ColumnStyle>());
+        } else if ("col".equals(name)) {
+            final int startColIndex = Integer.parseInt(attributes.getValue("min"));
+            final int endColIndex = Integer.parseInt(attributes.getValue("max"));
+            final double columnWidth = Double.parseDouble(attributes.getValue("width"));
+            int defaultColumnStyleIndex = 0;
+            boolean hidden = false;
+            if (attributes.getValue("style") != null) {
+                defaultColumnStyleIndex = Integer.parseInt(attributes.getValue("style"));
+            }
+            if (attributes.getValue("hidden") != null && (attributes.getValue("hidden").equals("true") || attributes.getValue("hidden").equals("1"))) {
+                hidden = true;
+            }
+            final ColumnStyle columnStyle = new ColumnStyle();
+            columnStyle.setDefaultColumnStyleIndex(defaultColumnStyleIndex);
+            columnStyle.setHidden(hidden);
+            columnStyle.setColumnWidth(columnWidth);
+            sheetStyle.getColumnStyles().put(Integer.toString(startColIndex) + ":" + Integer.toString(endColIndex), columnStyle);
+        } else if ("row".equals(name)) {
+            //开始读取行数据
+            rowIndex = Integer.parseInt(attributes.getValue("r"));
+            adjustRowMax(rowIndex);
+            //清空数据容器中保留的上一行的数据
+            rowData.getCellDataValues().clear();
+            rowData.getCellStyles().clear();
+            rowData.getCellDataTypes().clear();
+            rowData.getCellFormula().clear();
+            colIndex = 1;
+            if (attributes.getValue("ht") != null) {
+                rowData.setRowHeight(Double.parseDouble(attributes.getValue("ht")));
+            }
+        } else if ("c".equals(name)) {
+            //读取单元格内容
+            final String position = attributes.getValue("r");
+            if (position != null) {
+                colIndex = parsePosition(position)[0];
+                adjustColMax(colIndex);
+                for (int idx = rowData.getCellStyles().size() + 1; idx < colIndex; idx++) {
+                    rowData.getCellStyles().add(null);
+                }
+                for (int idx = rowData.getCellDataValues().size() + 1; idx < colIndex; idx++) {
+                    rowData.getCellDataValues().add(null);
+                }
+                for (int idx = rowData.getCellDataTypes().size() + 1; idx < colIndex; idx++) {
+                    rowData.getCellDataTypes().add(null);
+                }
+                for (int idx = rowData.getCellFormula().size() + 1; idx < colIndex; idx++) {
+                    rowData.getCellFormula().add(null);
+                }
+            }
+            if (attributes.getValue("s") != null) {
+                rowData.getCellStyles().add(Integer.parseInt(attributes.getValue("s")));
+            } else {
+                rowData.getCellStyles().add(null);
+            }
+            rowData.getCellDataTypes().add(CellDataType.getCellTypeEnum(attributes.getValue("t")));
+        } else if ("v".equals(name)) {
+            //单元格数据
+            valueFlag = true;
+        } else if ("t".equals(name)) {
+            isTElement = true;
+            valueFlag = true;
+        } else if ("definedName".equals(name) && StringUtils.isNotBlank(attributes.getValue("localSheetId"))) {
+            final String value = attributes.getValue("name");
+            if ("_xlnm.Print_Area".equals(value) || "_xlnm.Print_Titles".equals(value)) {
+                valueFlag = true;
+                printFlag = true;
+                localSheetId = attributes.getValue("localSheetId");
+                cellBuilder = new StringBuilder();
+            }
+        } else if ("sheet".equals(name)) {
+            sheetNameMap.put(attributes.getValue("r:id"), attributes.getValue("name"));
+        } else if ("pageMargins".equals(name)) {
+            //页边距
+
+        } else if ("pageSetup".equals(name)) {
+            //页面设置
+
+        } else if ("printOptions".equals(name)) {
+            //打印选项
+
+        } else if ("headerFooter".equals(name)) {
+            //页眉页脚
+
+        } else if (isReadFormula && "f".equals(name)) {
+            //公式
+            valueFlag = true;
+            if ("shared".equals(attributes.getValue("t"))) {
+                //共享公式
+                isSharedFormula = true;
+                si = attributes.getValue("si");
+            }
+        }
+    }
+
+    /**
+     * 读取每对标签的结束标签时调用
+     *
+     * @param uri
+     * @param localName
+     * @param name
+     */
+    @Override
+    public void endElement(final String uri, final String localName, final String name) {
+        Object result;
+        if ("worksheet".equals(name)) {
+            //一个sheet读取完毕
+            cellBuilder = null;
+            rowIndex = 0;
+            sheetOver(sheetIndex, sheetStyle);
+        } else if ("row".equals(name)) {
+            //一行数据读取完毕
+            if (rowIndex == 1 && ignoreFirstRow) {
+                //过滤首行数据 一般为表头
+                return;
+            }
+            //调用实现的业务逻辑方法处理当前行数据
+            rowHandler(sheetIndex, rowIndex, rowData);
+            rowIndex++;
+            rowData.setRowHeight(0);
+        } else if ("v".equals(name)) {
+            //读取到单元格的数据标签
+            final CellDataType cellDataType = rowData.getCellDataTypes().get(colIndex - 1);
+            switch (cellDataType) {
+                case BOOL:
+                    final char firstFlag = cellBuilder.toString().charAt(0);
+                    if (parseCellValueStringFlag) {
+                        result = firstFlag == '0' ? "false" : "true";
+                    } else {
+                        result = firstFlag != '0';
+                    }
+                    break;
+                case ERROR:
+                    result = "\"ERROR:" + cellBuilder.toString() + "\"";
+                    break;
+                case FORMULA:
+                    if (parseCellValueStringFlag) {
+                        result = cellBuilder.toString();
+                    } else {
+                        try {
+                            result = Double.parseDouble(cellBuilder.toString());
+                        } catch (Exception e) {
+                            result = cellBuilder.toString();
+                        }
+                    }
+                    break;
+                case INLINESTR:
+                    result = new XSSFRichTextString(cellBuilder.toString());
+                    break;
+                case SSTINDEX:
+                    //共享字符需要从共享字符表中取
+                    final int idx = Integer.parseInt(cellBuilder.toString());
+                    result = new XSSFRichTextString(sst.getEntryAt(idx));
+                    break;
+                case NUMBER:
+                    if (parseCellValueStringFlag) {
+                        final Integer styleAt = rowData.getCellStyles().get(colIndex - 1);
+                        if (styleAt != null) {
+                            final XSSFCellStyle cellStyle = stylesTable.getStyleAt(styleAt);
+                            final short formatIndex = cellStyle.getDataFormat();
+                            final String formatString = cellStyle.getDataFormatString();
+                            if (formatString == null) {
+                                result = cellBuilder.toString();
+                            } else if (formatString.contains("m/dd/yy")
+                                    || formatString.contains("m/d/yy")
+                                    || formatString.contains("yyyy/mm/dd")
+                                    || formatString.contains("yyyy/m/d")) {
+                                result = dataFormatter.formatRawCellContents(
+                                        Double.parseDouble(cellBuilder.toString()),
+                                        formatIndex, dateFormatStyle).replace("T", "");
+                            } else {
+                                result = dataFormatter.formatRawCellContents(Double.parseDouble(cellBuilder.toString()),
+                                        formatIndex, formatString).replace("_", "").trim();
+                            }
+                        } else {
+                            result = cellBuilder.toString();
+                        }
+                    } else {
+                        result = Double.parseDouble(cellBuilder.toString());
+                    }
+                    break;
+                default:
+                    result = null;
+            }
+            writeColData(result);
+            valueFlag = false;
+        } else if ("c".equals(name)) {
+            colIndex++;
+        } else if ("f".equals(name)) {
+            if (isReadFormula) {
+                rowData.getCellDataTypes().set(colIndex - 1, CellDataType.FORMULA);
+                writeColData(cellBuilder);
+                valueFlag = false;
+            }
+            cellBuilder.delete(0, cellBuilder.length());
+        } else if (isTElement) {
+            result = cellBuilder.toString().trim();
+            writeColData(result);
+            isTElement = false;
+            valueFlag = false;
+        } else if (printFlag && "definedName".equals(name)) {
+            result = cellBuilder.toString();
+            writePrint(result);
+            valueFlag = false;
+            printFlag = false;
+            cellBuilder.delete(0, cellBuilder.length());
+        }
+    }
+
+    /**
+     * 写sheet打印区域和打印标题
+     *
+     * @param result
+     */
+    private void writePrint(Object result) {
+        final String res = result.toString();
+        if (StringUtils.isBlank(localSheetId) || StringUtils.equals(res, "#REF!") || StringUtils.isBlank(res)) {
+            return;
+        }
+        final String rId = "rId" + (Integer.parseInt(localSheetId) + 1);
+        final String sheetName = sheetNameMap.get(rId);
+        final SheetPrint sheetPrint = sheetPrintMap.containsKey(sheetName) ? sheetPrintMap.get(sheetName) : new SheetPrint();
+        sheetPrint.setSheetName(sheetName);
+        sheetPrint.setSheetIndex(localSheetId);
+        for (String str : res.split(",")) {
+            final int i = isArea(str.split("!")[1]);
+            if (i == 1) {
+                sheetPrint.setPrintArea(res);
+            } else if (i == 2) {
+                sheetPrint.setPrintTitleRows(str);
+            } else if (i == 3) {
+                sheetPrint.setPrintTitleColumns(str);
+            }
+        }
+        sheetPrintMap.put(sheetName, sheetPrint);
+        localSheetId = null;
+    }
+
+    /**
+     * @param str
+     * @return 1-区域 2-顶端 3-左端 0-判断错误
+     */
+    private int isArea(String str) {
+        String split = str.split(":")[0];
+        int countMatches = StringUtils.countMatches(split, "$");
+        if (countMatches == 2) {
+            return 1;
+        } else if (countMatches == 1) {
+            String substr = split.substring(split.length() - 1);
+            char charAt = substr.charAt(0);
+            Pattern pattern = Pattern.compile("[0-9]*");
+            if (pattern.matcher(substr).matches()) {
+                return 2;
+            } else if ((charAt >= 'a' && charAt <= 'z') || (charAt >= 'A' && charAt <= 'Z')) {
+                return 3;
+            }
+        }
+        return 0;
+
+    }
+
+    /**
+     * 计算当前的最大行和列数
+     *
+     * @param ref
+     */
+    private void handleMax(String ref) {
+        final String[] range = ref.split(":");
+        String maxStr;
+        if (range.length == 1) {
+            maxStr = range[0];
+        } else {
+            maxStr = range[1];
+        }
+        final int[] maxPosition = parsePosition(maxStr);
+        rowMax = maxPosition[1];
+        colMax = maxPosition[0];
+    }
+
+    /**
+     * 把单元格数据添加到当前行的数据容器中
+     *
+     * @param result
+     */
+    private void writeColData(final Object result) {
+        rowData.getCellDataValues().add(result);
+        cellBuilder.delete(0, cellBuilder.length());
+    }
+
+    /**
+     * 数据append到cellBuilder中
+     *
+     * @param ch
+     * @param start
+     * @param length
+     */
+    @Override
+    public void characters(final char[] ch, final int start, final int length) {
+        if (valueFlag) {
+            cellBuilder.append(ch, start, length);
+        }
+    }
+
+    /**
+     * sheet处理
+     *
+     * @param sheetIndex
+     * @param sheetStyle
+     */
+    private void sheetHandler(int sheetIndex, SheetStyle sheetStyle) {
+
+    }
+
+    /**
+     * 获取position坐标的实际坐标数据
+     *
+     * @param position
+     * @return
+     */
+    private int[] parsePosition(String position) {
+        final int[] result = new int[2];
+        final String amPosition = position.replaceAll("[0-9]", "");
+        final char[] chars = amPosition.toUpperCase().toCharArray();
+        int ret = 0;
+        for (int i = 0; i < chars.length; i++) {
+            ret += (chars[i] - 'A' + 1) * Math.pow(26, chars.length - i - 1);
+        }
+        result[0] = ret;
+        result[1] = Integer.parseInt(position.replaceAll("[A-Z]", ""));
+        return result;
+    }
+
+    /**
+     * 调整列最大值
+     *
+     * @param colIndex
+     */
+    private void adjustColMax(final int colIndex) {
+        if (colIndex > colMax) {
+            colMax = colIndex;
+        }
+    }
+
+    /**
+     * 调整行最大值
+     *
+     * @param rowIndex
+     */
+    private void adjustRowMax(final int rowIndex) {
+        if (rowIndex > rowMax) {
+            rowMax = rowIndex;
+        }
+    }
+}
/**
+ * @author storyxc
+ * @description POI事件模式读取Excel抽象类
+ * @createdTime 2020/6/18 14:27
+ */
+public abstract class POIEventModeHandler extends DefaultHandler {
+
+    /**
+     * 构造方法
+     *
+     * @param parseCellValueStringFlag 是否将单元格值解析成字符串
+     * @param ignoreFirstRow           是否忽略首行(一般是表头)
+     */
+    public POIEventModeHandler(final boolean parseCellValueStringFlag, final boolean ignoreFirstRow) {
+        this.parseCellValueStringFlag = parseCellValueStringFlag;
+        this.ignoreFirstRow = ignoreFirstRow;
+    }
+
+    /**
+     * 构造方法
+     *
+     * @param parseCellValueStringFlag 将单元格值解析成字符串
+     * @param ignoreFirstRow           忽略首行
+     * @param dataFormatStyle          指定日期类型解析格式 默认yyyy-MM-dd
+     */
+    public POIEventModeHandler(final boolean parseCellValueStringFlag, final boolean ignoreFirstRow, final String dataFormatStyle) {
+        this.parseCellValueStringFlag = parseCellValueStringFlag;
+        this.ignoreFirstRow = ignoreFirstRow;
+        this.dateFormatStyle = dataFormatStyle;
+    }
+
+    /**
+     * 单元格类型索引
+     */
+    protected enum CellDataType {
+        /**
+         * 布尔值
+         */
+        BOOL("b"),
+        /**
+         * 异常错误
+         */
+        ERROR("e"),
+        /**
+         * 公式
+         */
+        FORMULA("str"),
+        /**
+         * 字符
+         */
+        INLINESTR("inlineStr"),
+        /**
+         * 共享字符表
+         */
+        SSTINDEX("s"),
+        /**
+         * 数值
+         */
+        NUMBER("n"),
+        /**
+         * 空
+         */
+        NULL("null");
+
+        private final String cellType;
+
+        String getCellType() {
+            return this.cellType;
+        }
+
+        CellDataType(final String cellType) {
+            this.cellType = cellType;
+        }
+
+        static CellDataType getCellTypeEnum(final String cellType) {
+            for (final CellDataType cellDataType : CellDataType.values()) {
+                //数字类型时c标签没有t属性
+                if (cellType == null) {
+                    return NUMBER;
+                } else if (StringUtils.equals(cellDataType.getCellType(), cellType)) {
+                    return cellDataType;
+                }
+            }
+            return null;
+        }
+    }
+
+    /**
+     * sheet样式
+     */
+    @Data
+    @EqualsAndHashCode(callSuper = false)
+    protected class SheetStyle {
+        /**
+         * sheet顺序索引
+         */
+        private int sheetId;
+        /**
+         * sheet名称
+         */
+        private String sheetName;
+        /**
+         * 缩放百分比
+         */
+        private double zoomPercent;
+        /**
+         * 自适应
+         */
+        private boolean fitToPage = false;
+        /**
+         * 是否显示网格线
+         */
+        private boolean showGridLines = true;
+        /**
+         * 默认行高
+         */
+        private double defaultRowHeight;
+        /**
+         * sheet中每一列的样式
+         */
+        Map<String, ColumnStyle> columnStyles;
+        /**
+         * 合并单元格
+         */
+        private List<Integer[]> mergeCells;
+        /* 打印属性 */
+        /**
+         * 上边距
+         */
+        private double topMargin = 0.75;
+        /**
+         * 下边距
+         */
+        private double bottomMargin = 0.75;
+        /**
+         * 左边距
+         */
+        private double leftMargin = 0.7;
+        /**
+         * 右边距
+         */
+        private double rightMargin = 0.7;
+        /**
+         * 页脚边距
+         */
+        private double footerMargin = 0.3;
+        /**
+         * 页头边距
+         */
+        private double headerMargin = 0.3;
+        /**
+         * 缩放比例
+         */
+        private short scale = 100;
+        /**
+         * 页宽
+         */
+        private short fitWidth = 1;
+        /**
+         * 页高
+         */
+        private short fitHeight = 1;
+        /**
+         * 纸张设置
+         */
+        private short pageSize = PrintSetup.A4_PAPERSIZE;
+        /**
+         * 垂直居中
+         */
+        private boolean verticallyCenter;
+        /**
+         * 水平居中
+         */
+        private boolean horizontallyCenter;
+        /**
+         * 横向打印
+         */
+        private boolean landscape;
+        /**
+         * 网格线
+         */
+        private boolean printGridlines;
+        /**
+         * 行号列标
+         */
+        private boolean printHeadings;
+        /**
+         * 草稿品质
+         */
+        private boolean draft;
+        /**
+         * 单色打印
+         */
+        private boolean noColor;
+        /**
+         * 打印顺序 true:先行后列 false:先列后行
+         */
+        private boolean leftToRigh;
+        /**
+         * 起始页页码自动
+         */
+        private boolean usePage;
+        /**
+         * 起始页码
+         */
+        private short pageStart = 1;
+        /**
+         * 页眉页脚与页边距对齐
+         */
+        private boolean alignWithMargins = true;
+    }
+
+    /**
+     * 列样式
+     */
+    @Data
+    @EqualsAndHashCode(callSuper = false)
+    protected class ColumnStyle {
+        /**
+         * 列宽度
+         */
+        private double columnWidth;
+        /**
+         * 列是否隐藏
+         */
+        private boolean isHidden;
+        /**
+         * 默认的列样式
+         */
+        private int defaultColumnStyleIndex;
+    }
+
+    /**
+     * 行数据
+     */
+    @Data
+    @EqualsAndHashCode(callSuper = false)
+    protected class RowData {
+        /**
+         * 当前行高
+         */
+        private double rowHeight;
+        /**
+         * 当前行数据
+         */
+        private List<Object> cellDataValues;
+        /**
+         * 当前行单元格样式索引
+         */
+        private List<Integer> cellStyles;
+        /**
+         * 当前行单元格数据类型
+         */
+        private List<CellDataType> cellDataTypes;
+        /**
+         * 单元格公式
+         */
+        private List<String> cellFormula;
+
+        RowData() {
+            this.cellDataValues = new ArrayList<>();
+            this.cellStyles = new ArrayList<>();
+            this.cellDataTypes = new ArrayList<>();
+            this.cellFormula = new ArrayList<>();
+        }
+    }
+
+    /**
+     * sheet打印区域
+     */
+    @Data
+    @EqualsAndHashCode(callSuper = false)
+    protected class SheetPrint {
+        private String sheetName;
+        private String sheetIndex;
+        private String printArea;
+        private String printTitleRows;
+        private String printTitleColumns;
+    }
+
+    /**
+     * 共享字符表
+     */
+    protected SharedStringsTable sst;
+    /**
+     * 单元格样式
+     */
+    private StylesTable stylesTable;
+    /**
+     * sheet游标
+     */
+    private int sheetIndex = 0;
+    /**
+     * 行游标
+     */
+    private int rowIndex;
+    /**
+     * 列坐标
+     */
+    private int colIndex;
+    /**
+     * 最大行数量
+     */
+    protected int rowMax;
+    /**
+     * 最大列数量
+     */
+    protected int colMax;
+    /**
+     * 是否为有效数据
+     */
+    private boolean valueFlag;
+    /**
+     * T
+     */
+    private boolean isTElement = false;
+    /**
+     * 记录当前值
+     */
+    private StringBuilder cellBuilder;
+    /**
+     * 是否过滤首行
+     */
+    protected boolean ignoreFirstRow = false;
+    /**
+     * 格式化日期样式
+     */
+    protected String dateFormatStyle = "yyyy-MM-dd";
+    /**
+     * 格式化样式
+     */
+    protected Map<Short, String> formatStyleMap = new HashMap<>();
+    /**
+     * 数据格式化formatter
+     */
+    private final DataFormatter dataFormatter = new DataFormatter();
+    /**
+     * 当前sheet样式
+     */
+    private SheetStyle sheetStyle;
+    /**
+     * 行数据
+     */
+    private RowData rowData;
+    /**
+     * sheet打印区域 key:sheetIndex
+     */
+    protected final Map<String, SheetPrint> sheetPrintMap = new HashMap<>();
+    /**
+     * 所有sheetName,key:sheetIndex
+     */
+    private final Map<String, String> sheetNameMap = new HashMap<>();
+    /**
+     * 当前打印标签的sheetIndex
+     */
+    private String localSheetId;
+    /**
+     * 辨别是否打印区域数据
+     */
+    private boolean printFlag;
+    /**
+     * 是否解析成String
+     */
+    private final boolean parseCellValueStringFlag;
+    /**
+     * 是否读取公式
+     */
+    protected boolean isReadFormula = false;
+    /**
+     * 是否共享公式
+     */
+    private boolean isSharedFormula;
+    /**
+     * 共享公式存放
+     */
+    private final Map<String, String> sharedFormulaMap = new HashMap<>();
+    /**
+     * 共享公式插值
+     */
+    private final Map<String, List<Integer>> diffMap = new HashMap<>();
+    /**
+     * 共享公式存放key
+     */
+    private String si;
+
+    /**
+     * 核心抽象方法,读取一行后的操作,将业务逻辑在该方法中实现
+     *
+     * @param sheetIndex
+     * @param rowIndex
+     * @param rowData
+     */
+    protected abstract void rowHandler(final int sheetIndex, final int rowIndex, final RowData rowData);
+
+    /**
+     * sheet处理完后调用的方法
+     *
+     * @param sheetIndex
+     * @param sheetStyle
+     */
+    protected void sheetOver(final int sheetIndex, final SheetStyle sheetStyle) {
+        return;
+    }
+
+    /**
+     * 整个工作簿处理完后调用
+     */
+    protected void workbookOver() {
+        return;
+    }
+
+    /**
+     * 处理完整的Excel
+     *
+     * @param filePath
+     * @throws OpenXML4JException
+     * @throws IOException
+     * @throws SAXException
+     */
+    public void handleExcel(final String filePath) throws OpenXML4JException, IOException, SAXException {
+        OPCPackage opcPackage = null;
+        try {
+            opcPackage = OPCPackage.open(filePath);
+            final XMLReader parser = fetchSheetParser();
+            final XSSFReader.SheetIterator sheets = parseSheet(opcPackage, parser);
+            while (sheets.hasNext()) {
+                final InputStream sheet = sheets.next();
+                sheetStyle = new SheetStyle();
+                sheetStyle.setSheetName(sheets.getSheetName());
+                final InputSource sheetSource = new InputSource(sheet);
+                parser.parse(sheetSource);
+                sheet.close();
+            }
+        } finally {
+            if (opcPackage != null) {
+                opcPackage.close();
+            }
+        }
+        workbookOver();
+    }
+
+    /**
+     * 处理指定的sheet页
+     * @param filePath
+     * @param sheetIdx
+     * @throws OpenXML4JException
+     * @throws SAXException
+     * @throws IOException
+     */
+    public void handleExcel(final String filePath, final int sheetIdx) throws OpenXML4JException, SAXException, IOException {
+        OPCPackage opcPackage = null;
+        try {
+            opcPackage = OPCPackage.open(filePath);
+            final XMLReader parser = fetchSheetParser();
+            final XSSFReader.SheetIterator sheets = parseSheet(opcPackage, parser);
+            while (sheets.hasNext()) {
+                final InputStream sheet = sheets.next();
+                if (sheetIndex + 1 != sheetIdx) {
+                    continue;
+                }
+                sheetStyle = new SheetStyle();
+                sheetStyle.setSheetName(sheets.getSheetName());
+                final InputSource sheetSource = new InputSource(sheet);
+                parser.parse(sheetSource);
+                sheet.close();
+            }
+        } finally {
+            if (opcPackage != null) {
+                opcPackage.close();
+            }
+        }
+        workbookOver();
+    }
+
+    private XSSFReader.SheetIterator parseSheet(OPCPackage opcPackage, XMLReader parser) throws IOException, OpenXML4JException, SAXException {
+        final XSSFReader reader = new XSSFReader(opcPackage);
+        sst = reader.getSharedStringsTable();
+        stylesTable = reader.getStylesTable();
+        final XSSFReader.SheetIterator sheets = (XSSFReader.SheetIterator) reader.getSheetsData();
+        //读取工作簿内容
+        final InputStream workbookData = reader.getWorkbookData();
+        final InputSource workbookDataSource = new InputSource(workbookData);
+        parser.parse(workbookDataSource);
+        workbookData.close();
+        //读取单元格样式
+        final InputStream stylesData = reader.getStylesData();
+        final InputSource stylesDataSource = new InputSource(stylesData);
+        parser.parse(stylesDataSource);
+        stylesData.close();
+        return sheets;
+
+    }
+
+    private XMLReader fetchSheetParser() throws SAXException {
+        final XMLReader parser = XMLReaderFactory.createXMLReader("org.apache.xerces.parsers.SAXParser");
+        parser.setContentHandler(this);
+        return parser;
+    }
+
+
+    /**
+     * 读取每对标签的开始标签时调用的方法
+     *
+     * @param uri
+     * @param localName
+     * @param name       当前标签名
+     * @param attributes 当前标签上的属性
+     */
+    @Override
+    public void startElement(final String uri, final String localName,
+                             final String name, final Attributes attributes) {
+        if ("numFmt".equals(name)) {
+            final short numFmtId = Short.parseShort(attributes.getValue("numFmtId"));
+            final String formatCode = attributes.getValue("formatCode");
+            formatStyleMap.put(numFmtId, formatCode);
+        } else if ("mergeCells".equals(name)) {
+            sheetStyle.setMergeCells(new ArrayList<Integer[]>());
+        } else if ("mergeCell".equals(name)) {
+            final String[] range = attributes.getValue("ref").split(":");
+            final Integer[] positionDx = new Integer[4];
+            final int[] start = parsePosition(range[0]);
+            final int[] end = parsePosition(range[1]);
+            positionDx[0] = start[1];
+            positionDx[1] = end[1];
+            positionDx[2] = start[0] - 1;
+            positionDx[3] = end[0] - 1;
+            sheetStyle.getMergeCells().add(positionDx);
+        } else if ("pageSetUpPr".equals(name)) {
+            if (attributes.getValue("fitToPage") != null && attributes.getValue("fitToPage").equals("1")) {
+                sheetStyle.setFitToPage(true);
+            }
+        } else if ("sheetView".equals(name)) {
+            if (attributes.getValue("showGridLines") != null && attributes.getValue("showGridLines").equals("0")) {
+                sheetStyle.setShowGridLines(false);
+            }
+            if (attributes.getValue("zoomScale") != null) {
+                sheetStyle.setZoomPercent((int) Double.parseDouble(attributes.getValue("zoomScale")));
+            }
+        } else if ("sheetFormatPr".equals(name)) {
+            if (attributes.getValue("defaultRowHeight") != null) {
+                sheetStyle.setDefaultRowHeight(Double.parseDouble(attributes.getValue("defaultRowHeight")));
+            }
+        } else if ("sheetData".equals(name)) {
+            //开始读取sheet页数据
+            sheetIndex++;
+            rowIndex = 1;
+            cellBuilder = new StringBuilder();
+            rowData = new RowData();
+            sheetHandler(sheetIndex, sheetStyle);
+        } else if ("dimension".equals(name)) {
+            handleMax(attributes.getValue("ref"));
+            sheetStyle.setColumnStyles(new HashMap<String, ColumnStyle>());
+        } else if ("col".equals(name)) {
+            final int startColIndex = Integer.parseInt(attributes.getValue("min"));
+            final int endColIndex = Integer.parseInt(attributes.getValue("max"));
+            final double columnWidth = Double.parseDouble(attributes.getValue("width"));
+            int defaultColumnStyleIndex = 0;
+            boolean hidden = false;
+            if (attributes.getValue("style") != null) {
+                defaultColumnStyleIndex = Integer.parseInt(attributes.getValue("style"));
+            }
+            if (attributes.getValue("hidden") != null && (attributes.getValue("hidden").equals("true") || attributes.getValue("hidden").equals("1"))) {
+                hidden = true;
+            }
+            final ColumnStyle columnStyle = new ColumnStyle();
+            columnStyle.setDefaultColumnStyleIndex(defaultColumnStyleIndex);
+            columnStyle.setHidden(hidden);
+            columnStyle.setColumnWidth(columnWidth);
+            sheetStyle.getColumnStyles().put(Integer.toString(startColIndex) + ":" + Integer.toString(endColIndex), columnStyle);
+        } else if ("row".equals(name)) {
+            //开始读取行数据
+            rowIndex = Integer.parseInt(attributes.getValue("r"));
+            adjustRowMax(rowIndex);
+            //清空数据容器中保留的上一行的数据
+            rowData.getCellDataValues().clear();
+            rowData.getCellStyles().clear();
+            rowData.getCellDataTypes().clear();
+            rowData.getCellFormula().clear();
+            colIndex = 1;
+            if (attributes.getValue("ht") != null) {
+                rowData.setRowHeight(Double.parseDouble(attributes.getValue("ht")));
+            }
+        } else if ("c".equals(name)) {
+            //读取单元格内容
+            final String position = attributes.getValue("r");
+            if (position != null) {
+                colIndex = parsePosition(position)[0];
+                adjustColMax(colIndex);
+                for (int idx = rowData.getCellStyles().size() + 1; idx < colIndex; idx++) {
+                    rowData.getCellStyles().add(null);
+                }
+                for (int idx = rowData.getCellDataValues().size() + 1; idx < colIndex; idx++) {
+                    rowData.getCellDataValues().add(null);
+                }
+                for (int idx = rowData.getCellDataTypes().size() + 1; idx < colIndex; idx++) {
+                    rowData.getCellDataTypes().add(null);
+                }
+                for (int idx = rowData.getCellFormula().size() + 1; idx < colIndex; idx++) {
+                    rowData.getCellFormula().add(null);
+                }
+            }
+            if (attributes.getValue("s") != null) {
+                rowData.getCellStyles().add(Integer.parseInt(attributes.getValue("s")));
+            } else {
+                rowData.getCellStyles().add(null);
+            }
+            rowData.getCellDataTypes().add(CellDataType.getCellTypeEnum(attributes.getValue("t")));
+        } else if ("v".equals(name)) {
+            //单元格数据
+            valueFlag = true;
+        } else if ("t".equals(name)) {
+            isTElement = true;
+            valueFlag = true;
+        } else if ("definedName".equals(name) && StringUtils.isNotBlank(attributes.getValue("localSheetId"))) {
+            final String value = attributes.getValue("name");
+            if ("_xlnm.Print_Area".equals(value) || "_xlnm.Print_Titles".equals(value)) {
+                valueFlag = true;
+                printFlag = true;
+                localSheetId = attributes.getValue("localSheetId");
+                cellBuilder = new StringBuilder();
+            }
+        } else if ("sheet".equals(name)) {
+            sheetNameMap.put(attributes.getValue("r:id"), attributes.getValue("name"));
+        } else if ("pageMargins".equals(name)) {
+            //页边距
+
+        } else if ("pageSetup".equals(name)) {
+            //页面设置
+
+        } else if ("printOptions".equals(name)) {
+            //打印选项
+
+        } else if ("headerFooter".equals(name)) {
+            //页眉页脚
+
+        } else if (isReadFormula && "f".equals(name)) {
+            //公式
+            valueFlag = true;
+            if ("shared".equals(attributes.getValue("t"))) {
+                //共享公式
+                isSharedFormula = true;
+                si = attributes.getValue("si");
+            }
+        }
+    }
+
+    /**
+     * 读取每对标签的结束标签时调用
+     *
+     * @param uri
+     * @param localName
+     * @param name
+     */
+    @Override
+    public void endElement(final String uri, final String localName, final String name) {
+        Object result;
+        if ("worksheet".equals(name)) {
+            //一个sheet读取完毕
+            cellBuilder = null;
+            rowIndex = 0;
+            sheetOver(sheetIndex, sheetStyle);
+        } else if ("row".equals(name)) {
+            //一行数据读取完毕
+            if (rowIndex == 1 && ignoreFirstRow) {
+                //过滤首行数据 一般为表头
+                return;
+            }
+            //调用实现的业务逻辑方法处理当前行数据
+            rowHandler(sheetIndex, rowIndex, rowData);
+            rowIndex++;
+            rowData.setRowHeight(0);
+        } else if ("v".equals(name)) {
+            //读取到单元格的数据标签
+            final CellDataType cellDataType = rowData.getCellDataTypes().get(colIndex - 1);
+            switch (cellDataType) {
+                case BOOL:
+                    final char firstFlag = cellBuilder.toString().charAt(0);
+                    if (parseCellValueStringFlag) {
+                        result = firstFlag == '0' ? "false" : "true";
+                    } else {
+                        result = firstFlag != '0';
+                    }
+                    break;
+                case ERROR:
+                    result = "\"ERROR:" + cellBuilder.toString() + "\"";
+                    break;
+                case FORMULA:
+                    if (parseCellValueStringFlag) {
+                        result = cellBuilder.toString();
+                    } else {
+                        try {
+                            result = Double.parseDouble(cellBuilder.toString());
+                        } catch (Exception e) {
+                            result = cellBuilder.toString();
+                        }
+                    }
+                    break;
+                case INLINESTR:
+                    result = new XSSFRichTextString(cellBuilder.toString());
+                    break;
+                case SSTINDEX:
+                    //共享字符需要从共享字符表中取
+                    final int idx = Integer.parseInt(cellBuilder.toString());
+                    result = new XSSFRichTextString(sst.getEntryAt(idx));
+                    break;
+                case NUMBER:
+                    if (parseCellValueStringFlag) {
+                        final Integer styleAt = rowData.getCellStyles().get(colIndex - 1);
+                        if (styleAt != null) {
+                            final XSSFCellStyle cellStyle = stylesTable.getStyleAt(styleAt);
+                            final short formatIndex = cellStyle.getDataFormat();
+                            final String formatString = cellStyle.getDataFormatString();
+                            if (formatString == null) {
+                                result = cellBuilder.toString();
+                            } else if (formatString.contains("m/dd/yy")
+                                    || formatString.contains("m/d/yy")
+                                    || formatString.contains("yyyy/mm/dd")
+                                    || formatString.contains("yyyy/m/d")) {
+                                result = dataFormatter.formatRawCellContents(
+                                        Double.parseDouble(cellBuilder.toString()),
+                                        formatIndex, dateFormatStyle).replace("T", "");
+                            } else {
+                                result = dataFormatter.formatRawCellContents(Double.parseDouble(cellBuilder.toString()),
+                                        formatIndex, formatString).replace("_", "").trim();
+                            }
+                        } else {
+                            result = cellBuilder.toString();
+                        }
+                    } else {
+                        result = Double.parseDouble(cellBuilder.toString());
+                    }
+                    break;
+                default:
+                    result = null;
+            }
+            writeColData(result);
+            valueFlag = false;
+        } else if ("c".equals(name)) {
+            colIndex++;
+        } else if ("f".equals(name)) {
+            if (isReadFormula) {
+                rowData.getCellDataTypes().set(colIndex - 1, CellDataType.FORMULA);
+                writeColData(cellBuilder);
+                valueFlag = false;
+            }
+            cellBuilder.delete(0, cellBuilder.length());
+        } else if (isTElement) {
+            result = cellBuilder.toString().trim();
+            writeColData(result);
+            isTElement = false;
+            valueFlag = false;
+        } else if (printFlag && "definedName".equals(name)) {
+            result = cellBuilder.toString();
+            writePrint(result);
+            valueFlag = false;
+            printFlag = false;
+            cellBuilder.delete(0, cellBuilder.length());
+        }
+    }
+
+    /**
+     * 写sheet打印区域和打印标题
+     *
+     * @param result
+     */
+    private void writePrint(Object result) {
+        final String res = result.toString();
+        if (StringUtils.isBlank(localSheetId) || StringUtils.equals(res, "#REF!") || StringUtils.isBlank(res)) {
+            return;
+        }
+        final String rId = "rId" + (Integer.parseInt(localSheetId) + 1);
+        final String sheetName = sheetNameMap.get(rId);
+        final SheetPrint sheetPrint = sheetPrintMap.containsKey(sheetName) ? sheetPrintMap.get(sheetName) : new SheetPrint();
+        sheetPrint.setSheetName(sheetName);
+        sheetPrint.setSheetIndex(localSheetId);
+        for (String str : res.split(",")) {
+            final int i = isArea(str.split("!")[1]);
+            if (i == 1) {
+                sheetPrint.setPrintArea(res);
+            } else if (i == 2) {
+                sheetPrint.setPrintTitleRows(str);
+            } else if (i == 3) {
+                sheetPrint.setPrintTitleColumns(str);
+            }
+        }
+        sheetPrintMap.put(sheetName, sheetPrint);
+        localSheetId = null;
+    }
+
+    /**
+     * @param str
+     * @return 1-区域 2-顶端 3-左端 0-判断错误
+     */
+    private int isArea(String str) {
+        String split = str.split(":")[0];
+        int countMatches = StringUtils.countMatches(split, "$");
+        if (countMatches == 2) {
+            return 1;
+        } else if (countMatches == 1) {
+            String substr = split.substring(split.length() - 1);
+            char charAt = substr.charAt(0);
+            Pattern pattern = Pattern.compile("[0-9]*");
+            if (pattern.matcher(substr).matches()) {
+                return 2;
+            } else if ((charAt >= 'a' && charAt <= 'z') || (charAt >= 'A' && charAt <= 'Z')) {
+                return 3;
+            }
+        }
+        return 0;
+
+    }
+
+    /**
+     * 计算当前的最大行和列数
+     *
+     * @param ref
+     */
+    private void handleMax(String ref) {
+        final String[] range = ref.split(":");
+        String maxStr;
+        if (range.length == 1) {
+            maxStr = range[0];
+        } else {
+            maxStr = range[1];
+        }
+        final int[] maxPosition = parsePosition(maxStr);
+        rowMax = maxPosition[1];
+        colMax = maxPosition[0];
+    }
+
+    /**
+     * 把单元格数据添加到当前行的数据容器中
+     *
+     * @param result
+     */
+    private void writeColData(final Object result) {
+        rowData.getCellDataValues().add(result);
+        cellBuilder.delete(0, cellBuilder.length());
+    }
+
+    /**
+     * 数据append到cellBuilder中
+     *
+     * @param ch
+     * @param start
+     * @param length
+     */
+    @Override
+    public void characters(final char[] ch, final int start, final int length) {
+        if (valueFlag) {
+            cellBuilder.append(ch, start, length);
+        }
+    }
+
+    /**
+     * sheet处理
+     *
+     * @param sheetIndex
+     * @param sheetStyle
+     */
+    private void sheetHandler(int sheetIndex, SheetStyle sheetStyle) {
+
+    }
+
+    /**
+     * 获取position坐标的实际坐标数据
+     *
+     * @param position
+     * @return
+     */
+    private int[] parsePosition(String position) {
+        final int[] result = new int[2];
+        final String amPosition = position.replaceAll("[0-9]", "");
+        final char[] chars = amPosition.toUpperCase().toCharArray();
+        int ret = 0;
+        for (int i = 0; i < chars.length; i++) {
+            ret += (chars[i] - 'A' + 1) * Math.pow(26, chars.length - i - 1);
+        }
+        result[0] = ret;
+        result[1] = Integer.parseInt(position.replaceAll("[A-Z]", ""));
+        return result;
+    }
+
+    /**
+     * 调整列最大值
+     *
+     * @param colIndex
+     */
+    private void adjustColMax(final int colIndex) {
+        if (colIndex > colMax) {
+            colMax = colIndex;
+        }
+    }
+
+    /**
+     * 调整行最大值
+     *
+     * @param rowIndex
+     */
+    private void adjustRowMax(final int rowIndex) {
+        if (rowIndex > rowMax) {
+            rowMax = rowIndex;
+        }
+    }
+}
+ + + + \ No newline at end of file diff --git "a/java/framework/Spring Cloud Config\346\216\245\345\205\245.html" "b/java/framework/Spring Cloud Config\346\216\245\345\205\245.html" new file mode 100644 index 000000000..9aaf921e2 --- /dev/null +++ "b/java/framework/Spring Cloud Config\346\216\245\345\205\245.html" @@ -0,0 +1,145 @@ + + + + + + Spring Cloud Config接入 | 故事 + + + + + + + + + + + + + + +
Skip to content

Spring Cloud Config接入

微服务配置中心能帮助统一管理多个环境、多个应用程序的外部化配置,不需要在某个配置有改动或新增某个配置项时去多个节点上一个个修改,做到了一次改动,处处使用。这里和Eureka注册中心配合使用。

Spring Cloud Config

SpringCloud子项目,提供了分布式系统中外部配置管理的能力,分为serverclient 两部分。官网地址

配合git仓库使用

建立一个git仓库,将配置文件放在仓库里,配置文件格式应用名-profile.properties(yml) ,多服务的情况包名可根据服务前缀命名,在配置中心server端配置中增加search-path配置即可,比如多个包名app-1/app-2,可以配置search-path:app-*

服务端

依赖

xml
<dependencies>
+    <dependency>
+        <groupId>org.springframework.cloud</groupId>
+        <artifactId>spring-cloud-config-server</artifactId>
+    </dependency>
+    <dependency>
+        <groupId>org.springframework.cloud</groupId>
+        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
+    </dependency>
+</dependencies>
<dependencies>
+    <dependency>
+        <groupId>org.springframework.cloud</groupId>
+        <artifactId>spring-cloud-config-server</artifactId>
+    </dependency>
+    <dependency>
+        <groupId>org.springframework.cloud</groupId>
+        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
+    </dependency>
+</dependencies>

配置文件

yaml
server:
+  port: 8077
+spring:
+  application:
+    name: spring-cloud-config-center
+  cloud:
+    config:
+      server:
+        git:
+          uri: git仓库地址
+          username: 账号
+          password: 密码
+          search-paths: app-* #搜索路径,使用通配符
+
+eureka:
+  client:
+    service-url:
+      defaultZone: http://admin:admin@127.0.0.1:19991/eureka/
+      fetch-registry-interval-seconds: 5
+
+  instance:
+    prefer-ip-address: true
+    lease-expiration-duration-in-seconds: 30
+    lease-renewal-interval-in-seconds: 10
server:
+  port: 8077
+spring:
+  application:
+    name: spring-cloud-config-center
+  cloud:
+    config:
+      server:
+        git:
+          uri: git仓库地址
+          username: 账号
+          password: 密码
+          search-paths: app-* #搜索路径,使用通配符
+
+eureka:
+  client:
+    service-url:
+      defaultZone: http://admin:admin@127.0.0.1:19991/eureka/
+      fetch-registry-interval-seconds: 5
+
+  instance:
+    prefer-ip-address: true
+    lease-expiration-duration-in-seconds: 30
+    lease-renewal-interval-in-seconds: 10

启动类

java
@SpringBootApplication
+@EnableEurekaClient
+@EnableConfigServer
+public class ConfigCenterApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(ConfigCenterApplication.class, args);
+    }
+}
@SpringBootApplication
+@EnableEurekaClient
+@EnableConfigServer
+public class ConfigCenterApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(ConfigCenterApplication.class, args);
+    }
+}

服务端

依赖

xml
<!--springcloud config客户端-->
+<dependency>
+    <groupId>org.springframework.cloud</groupId>
+    <artifactId>spring-cloud-config-client</artifactId>
+</dependency>
<!--springcloud config客户端-->
+<dependency>
+    <groupId>org.springframework.cloud</groupId>
+    <artifactId>spring-cloud-config-client</artifactId>
+</dependency>

配置文件

yaml
spring:
+  application:
+    name: app-1
+  cloud:
+    config:
+      profile: @spring.profiles.active@
+      label: master
+      discovery:
+        enabled: true
+        service-id: spring-cloud-config-center
+eureka:
+  client:
+    service-url:
+      defaultZone: http://admin:admin@127.0.0.1:19991/eureka/
spring:
+  application:
+    name: app-1
+  cloud:
+    config:
+      profile: @spring.profiles.active@
+      label: master
+      discovery:
+        enabled: true
+        service-id: spring-cloud-config-center
+eureka:
+  client:
+    service-url:
+      defaultZone: http://admin:admin@127.0.0.1:19991/eureka/

TIP

SpringCloudConfig配合服务发现使用时,必须在bootstrap.yaml(或环境变量) 开启服务发现spring.cloud.config.discovery.enabled=true,并且要配置注册中心的地址eureka.client.serviceUrl.defaultZone ,服务启动时会首先去注册中心找到配置中心server。

The HTTP service has resources in the following form:

txt
/{application}/{profile}[/{label}]
+/{application}-{profile}.yml
+/{label}/{application}-{profile}.yml
+/{application}-{profile}.properties
+/{label}/{application}-{profile}.properties
/{application}/{profile}[/{label}]
+/{application}-{profile}.yml
+/{label}/{application}-{profile}.yml
+/{application}-{profile}.properties
+/{label}/{application}-{profile}.properties

以下引自官网

Discovery First Bootstrap

If you use a DiscoveryClient implementation, such as Spring Cloud Netflix and Eureka Service Discovery or Spring Cloud Consul, you can have the Config Server register with the Discovery Service. However, in the default “Config First” mode, clients cannot take advantage of the registration.

If you prefer to use DiscoveryClient to locate the Config Server, you can do so by setting spring.cloud.config.discovery.enabled=true (the default is false). The net result of doing so is that client applications all need a bootstrap.yml (or an environment variable) with the appropriate discovery configuration. For example, with Spring Cloud Netflix, you need to define the Eureka server address (for example, in eureka.client.serviceUrl.defaultZone). The price for using this option is an extra network round trip on startup, to locate the service registration. The benefit is that, as long as the Discovery Service is a fixed point, the Config Server can change its coordinates. The default service ID is configserver, but you can change that on the client by setting spring.cloud.config.discovery.serviceId (and on the server, in the usual way for a service, such as by setting spring.application.name).

配置中心启动后,其他服务向配置中心获取配置文件时,配置中心会去git上拉配置并缓存在本地的临时目录。

默认情况下,它们被放在带有config-repo-前缀的系统临时目录中。例如,在linux上,它可以是/tmp/config-repo-<randomid> 。一些操作系统会定期清理临时目录,导致意料之外的情况发生,例如丢失属性。需要在配置文件中增加配置spring.cloud.config.server.git.basedirspring.cloud.config.server.svn.basedir来指定一个不在系统临时路径下的目录。

+ + + + \ No newline at end of file diff --git "a/java/framework/SpringMVC\347\232\204\346\211\247\350\241\214\346\265\201\347\250\213\346\272\220\347\240\201\345\210\206\346\236\220.html" "b/java/framework/SpringMVC\347\232\204\346\211\247\350\241\214\346\265\201\347\250\213\346\272\220\347\240\201\345\210\206\346\236\220.html" new file mode 100644 index 000000000..6d751c873 --- /dev/null +++ "b/java/framework/SpringMVC\347\232\204\346\211\247\350\241\214\346\265\201\347\250\213\346\272\220\347\240\201\345\210\206\346\236\220.html" @@ -0,0 +1,641 @@ + + + + + + SpringMVC的执行流程源码分析 | 故事 + + + + + + + + + + + + + + +
Skip to content

SpringMVC的执行流程源码分析

背景

一个常见的面试/笔试题: SpringMVC的执行流程

答:

1、前端请求到核心前端控制器DispatcherServlet

2、DispatcherServlet收到请求调用HandlerMapping处理器映射器。

3、处理器映射器找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。

4、 DispatcherServlet调用HandlerAdapter处理器适配器。

5、HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。

6、Controller执行完成返回ModelAndView。

7、HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。

8、DispatcherServlet将ModelAndView传给ViewReslover视图解析器。

9、ViewReslover解析后返回具体View.

10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。

11、DispatcherServlet响应用户。

为了应付面试相信很多人和我一样死记硬背过,今天就来看下源码,看看这个流程的庐山真面目。

首先找到DispatcherServlet类,看看它的继承关系 diagram.png

1.DispathcerServlet的初始化过程

过程图 2.jpg

初始化方法

java
/**
+	 * This implementation calls {@link #initStrategies}.
+	 */
+	@Override
+	protected void onRefresh(ApplicationContext context) {
+		initStrategies(context);
+	}
+
+	/**
+	 * Initialize the strategy objects that this servlet uses.
+	 * <p>May be overridden in subclasses in order to initialize further strategy objects.
+	 */
+	protected void initStrategies(ApplicationContext context) {
+		initMultipartResolver(context);
+		initLocaleResolver(context);
+		initThemeResolver(context);
+		initHandlerMappings(context);
+		initHandlerAdapters(context);
+		initHandlerExceptionResolvers(context);
+		initRequestToViewNameTranslator(context);
+		initViewResolvers(context);
+		initFlashMapManager(context);
+	}
/**
+	 * This implementation calls {@link #initStrategies}.
+	 */
+	@Override
+	protected void onRefresh(ApplicationContext context) {
+		initStrategies(context);
+	}
+
+	/**
+	 * Initialize the strategy objects that this servlet uses.
+	 * <p>May be overridden in subclasses in order to initialize further strategy objects.
+	 */
+	protected void initStrategies(ApplicationContext context) {
+		initMultipartResolver(context);
+		initLocaleResolver(context);
+		initThemeResolver(context);
+		initHandlerMappings(context);
+		initHandlerAdapters(context);
+		initHandlerExceptionResolvers(context);
+		initRequestToViewNameTranslator(context);
+		initViewResolvers(context);
+		initFlashMapManager(context);
+	}

可以看到initStrategies方法初始化了9个组件,其中不乏文章开头中问题涉及到的组件

这九个初始化方法做的事情如下:

  • initMultipartResolver:初始化MultipartResolver,用于处理文件上传服务,如果有文件上传,那么就会将当前的HttpServletRequest包装成DefaultMultipartHttpServletRequest,并且将每个上传的内容封装成CommonsMultipartFile对象。需要在dispatcherServlet-servlet.xml中配置文件上传解
  • initLocaleResolver:用于处理应用的国际化问题,本地化解析策略。
  • initThemeResolver:用于定义一个主题。
  • initHandlerMapping:用于定义请求映射关系。
  • initHandlerAdapters:用于根据Handler的类型定义不同的处理规则。
  • initHandlerExceptionResolvers:当Handler处理出错后,会通过此将错误日志记录在log文件中,默认实现类是SimpleMappingExceptionResolver
  • initRequestToViewNameTranslators:将指定的ViewName按照定义的RequestToViewNameTranslators替换成想要的格式。
  • initViewResolvers:用于将View解析成页面。
  • initFlashMapManager:用于生成FlashMap管理器。

2.DispatcherServlet如何处理用户请求

首先要明确DispatcherServlet也是一个Servlet,也要遵守servlet接口的规范,servlet通过service方法来根据不同的请求方式来执行doGet,doPost等方法。而FrameworkServlet重写了service方法,并调用了processRequest方法,processRequest方法中又调用了抽象方法doService,DispatcherServlet实现了doService方法,并在该方法中调用了doDispatch方法,doDispatch方法就是具体的请求处理过程

过程图: 3.jpeg

3.doDispatch方法

java
/**
+	 * Process the actual dispatching to the handler.
+	 * <p>The handler will be obtained by applying the servlet's HandlerMappings in order.
+	 * The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters
+	 * to find the first that supports the handler class.
+	 * <p>All HTTP methods are handled by this method. It's up to HandlerAdapters or handlers
+	 * themselves to decide which methods are acceptable.
+	 * @param request current HTTP request
+	 * @param response current HTTP response
+	 * @throws Exception in case of any kind of processing failure
+	 */
+	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
+		HttpServletRequest processedRequest = request;
+		HandlerExecutionChain mappedHandler = null;
+		boolean multipartRequestParsed = false;
+
+		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
+
+		try {
+			ModelAndView mv = null;
+			Exception dispatchException = null;
+
+			try {
+                //判断是否为上传文件的请求,如果不是就返回原始的request,否则做相应的处理
+				processedRequest = checkMultipart(request);
+				multipartRequestParsed = (processedRequest != request);
+				
+				// Determine handler for the current request.
+                //找到当前请求对应的处理器,返回的是对应的处理器及拦截器集合
+				mappedHandler = getHandler(processedRequest);
+				if (mappedHandler == null) {
+					noHandlerFound(processedRequest, response);
+					return;
+				}
+
+				// Determine handler adapter for the current request.
+                //根据上一步找到的处理器,再找到对应的处理器适配器
+				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
+
+				// Process last-modified header, if supported by the handler.
+				String method = request.getMethod();
+				boolean isGet = "GET".equals(method);
+				if (isGet || "HEAD".equals(method)) {
+					long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
+					if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
+						return;
+					}
+				}
+				//这里执行了所有的拦截器中的preHandle方法 也就是为什么拦截器总在controller前先执行
+				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
+					return;
+				}
+
+				// Actually invoke the handler.
+                //调用处理器的处理方法
+				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
+
+				if (asyncManager.isConcurrentHandlingStarted()) {
+					return;
+				}
+				//设置modelAndView的默认名
+				applyDefaultViewName(processedRequest, mv);
+                //执行拦截器的postHanle方法
+				mappedHandler.applyPostHandle(processedRequest, response, mv);
+			}
+			catch (Exception ex) {
+				dispatchException = ex;
+			}
+			catch (Throwable err) {
+				// As of 4.3, we're processing Errors thrown from handler methods as well,
+				// making them available for @ExceptionHandler methods and other scenarios.
+				dispatchException = new NestedServletException("Handler dispatch failed", err);
+			}
+            //处理modelAndView并渲染
+			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
+		}
+		catch (Exception ex) {
+			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
+		}
+		catch (Throwable err) {
+			triggerAfterCompletion(processedRequest, response, mappedHandler,
+					new NestedServletException("Handler processing failed", err));
+		}
+		finally {
+			if (asyncManager.isConcurrentHandlingStarted()) {
+				// Instead of postHandle and afterCompletion
+				if (mappedHandler != null) {
+					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
+				}
+			}
+			else {
+				// Clean up any resources used by a multipart request.
+				if (multipartRequestParsed) {
+					cleanupMultipart(processedRequest);
+				}
+			}
+		}
+	}
/**
+	 * Process the actual dispatching to the handler.
+	 * <p>The handler will be obtained by applying the servlet's HandlerMappings in order.
+	 * The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters
+	 * to find the first that supports the handler class.
+	 * <p>All HTTP methods are handled by this method. It's up to HandlerAdapters or handlers
+	 * themselves to decide which methods are acceptable.
+	 * @param request current HTTP request
+	 * @param response current HTTP response
+	 * @throws Exception in case of any kind of processing failure
+	 */
+	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
+		HttpServletRequest processedRequest = request;
+		HandlerExecutionChain mappedHandler = null;
+		boolean multipartRequestParsed = false;
+
+		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
+
+		try {
+			ModelAndView mv = null;
+			Exception dispatchException = null;
+
+			try {
+                //判断是否为上传文件的请求,如果不是就返回原始的request,否则做相应的处理
+				processedRequest = checkMultipart(request);
+				multipartRequestParsed = (processedRequest != request);
+				
+				// Determine handler for the current request.
+                //找到当前请求对应的处理器,返回的是对应的处理器及拦截器集合
+				mappedHandler = getHandler(processedRequest);
+				if (mappedHandler == null) {
+					noHandlerFound(processedRequest, response);
+					return;
+				}
+
+				// Determine handler adapter for the current request.
+                //根据上一步找到的处理器,再找到对应的处理器适配器
+				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
+
+				// Process last-modified header, if supported by the handler.
+				String method = request.getMethod();
+				boolean isGet = "GET".equals(method);
+				if (isGet || "HEAD".equals(method)) {
+					long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
+					if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
+						return;
+					}
+				}
+				//这里执行了所有的拦截器中的preHandle方法 也就是为什么拦截器总在controller前先执行
+				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
+					return;
+				}
+
+				// Actually invoke the handler.
+                //调用处理器的处理方法
+				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
+
+				if (asyncManager.isConcurrentHandlingStarted()) {
+					return;
+				}
+				//设置modelAndView的默认名
+				applyDefaultViewName(processedRequest, mv);
+                //执行拦截器的postHanle方法
+				mappedHandler.applyPostHandle(processedRequest, response, mv);
+			}
+			catch (Exception ex) {
+				dispatchException = ex;
+			}
+			catch (Throwable err) {
+				// As of 4.3, we're processing Errors thrown from handler methods as well,
+				// making them available for @ExceptionHandler methods and other scenarios.
+				dispatchException = new NestedServletException("Handler dispatch failed", err);
+			}
+            //处理modelAndView并渲染
+			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
+		}
+		catch (Exception ex) {
+			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
+		}
+		catch (Throwable err) {
+			triggerAfterCompletion(processedRequest, response, mappedHandler,
+					new NestedServletException("Handler processing failed", err));
+		}
+		finally {
+			if (asyncManager.isConcurrentHandlingStarted()) {
+				// Instead of postHandle and afterCompletion
+				if (mappedHandler != null) {
+					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
+				}
+			}
+			else {
+				// Clean up any resources used by a multipart request.
+				if (multipartRequestParsed) {
+					cleanupMultipart(processedRequest);
+				}
+			}
+		}
+	}

1.getHandler方法

该方法返回的是HandlerExecutionChain对象,其中包含了处理器和过滤器的集合,这里调用了handlerMapping的getHandler方法,该方法主要调用了getHandlerExecutionChain方法,handlerMapping的集合是在初始化dispatchServlet的时候从beanFactory中查找并封装的,具体的handlerMappings初始化细节可以看initHandlerMappings方法,handlerMapping有多种类型,对应不同的请求,比如请求静态资源的和请求接口的等,此处我们以请求一个查询接口为例

protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
+   if (this.handlerMappings != null) {
+   		//循环所有的handlerMapping,直到找到对应的handler
+      for (HandlerMapping mapping : this.handlerMappings) {
+         HandlerExecutionChain handler = mapping.getHandler(request);
+         if (handler != null) {
+            return handler;
+         }
+      }
+   }
+   return null;
+}
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
+   if (this.handlerMappings != null) {
+   		//循环所有的handlerMapping,直到找到对应的handler
+      for (HandlerMapping mapping : this.handlerMappings) {
+         HandlerExecutionChain handler = mapping.getHandler(request);
+         if (handler != null) {
+            return handler;
+         }
+      }
+   }
+   return null;
+}
  • getHandlerExecutionChain方法

这里调用的是AbstractHandlerMethodMapping的getHandlerInternal方法,该方法又调用了同一个类中的lookupHandlerMethod方法

  • lookupHandlerMethod方法会根据请求的uri在mappingRegistry中查询已经注册了的请求路径(requestMapping注解中的路径),如果能直接从map中get到非空的list,就直接根据list匹配对应的HandleMethod对象,如果mappingRegistry中get不到,就尝试使用uri路径匹配,例如带有url参数的这种格式/test/{username}的格式,{username}会被替换为.*的正则表达式去进行匹配,匹配到后返回;

  • getHandlerExecutionChain方法则是根据请求的路径匹配拦截器的路径,如果有匹配到的,就添加到执行链当中

java
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
+    	//根据request找到对应的handler
+		Object handler = getHandlerInternal(request);
+		if (handler == null) {
+			handler = getDefaultHandler();
+		}
+		if (handler == null) {
+			return null;
+		}
+		// Bean name or resolved handler?
+		if (handler instanceof String) {
+			String handlerName = (String) handler;
+			handler = obtainApplicationContext().getBean(handlerName);
+		}
+
+		HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
+
+		if (logger.isTraceEnabled()) {
+			logger.trace("Mapped to " + handler);
+		}
+		else if (logger.isDebugEnabled() && !request.getDispatcherType().equals(DispatcherType.ASYNC)) {
+			logger.debug("Mapped to " + executionChain.getHandler());
+		}
+
+		if (CorsUtils.isCorsRequest(request)) {
+			CorsConfiguration globalConfig = this.corsConfigurationSource.getCorsConfiguration(request);
+			CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
+			CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
+			executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
+		}
+
+		return executionChain;
+	}
+
+
+
+protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
+		HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
+				(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
+
+		String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
+		for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
+			if (interceptor instanceof MappedInterceptor) {
+				MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
+				if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
+					chain.addInterceptor(mappedInterceptor.getInterceptor());
+				}
+			}
+			else {
+				chain.addInterceptor(interceptor);
+			}
+		}
+		return chain;
+	}
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
+    	//根据request找到对应的handler
+		Object handler = getHandlerInternal(request);
+		if (handler == null) {
+			handler = getDefaultHandler();
+		}
+		if (handler == null) {
+			return null;
+		}
+		// Bean name or resolved handler?
+		if (handler instanceof String) {
+			String handlerName = (String) handler;
+			handler = obtainApplicationContext().getBean(handlerName);
+		}
+
+		HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
+
+		if (logger.isTraceEnabled()) {
+			logger.trace("Mapped to " + handler);
+		}
+		else if (logger.isDebugEnabled() && !request.getDispatcherType().equals(DispatcherType.ASYNC)) {
+			logger.debug("Mapped to " + executionChain.getHandler());
+		}
+
+		if (CorsUtils.isCorsRequest(request)) {
+			CorsConfiguration globalConfig = this.corsConfigurationSource.getCorsConfiguration(request);
+			CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
+			CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
+			executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
+		}
+
+		return executionChain;
+	}
+
+
+
+protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
+		HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
+				(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
+
+		String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
+		for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
+			if (interceptor instanceof MappedInterceptor) {
+				MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
+				if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
+					chain.addInterceptor(mappedInterceptor.getInterceptor());
+				}
+			}
+			else {
+				chain.addInterceptor(interceptor);
+			}
+		}
+		return chain;
+	}

2.getHandlerAdapter方法

这个方法比较简单,就是从handlerAdapter集合中遍历找到支持当前请求的处理器适配器,用到了handlerAdapter的supports方法,测试的接口请求会调用AbstractHandlerMethodAdapter这个类的supports方法

java
/**
+ * This implementation expects the handler to be an {@link HandlerMethod}.
+ * @param handler the handler instance to check
+ * @return whether or not this adapter can adapt the given handler
+ */
+@Override
+public final boolean supports(Object handler) {
+   return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
+}
/**
+ * This implementation expects the handler to be an {@link HandlerMethod}.
+ * @param handler the handler instance to check
+ * @return whether or not this adapter can adapt the given handler
+ */
+@Override
+public final boolean supports(Object handler) {
+   return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
+}

3.handle方法

在找到对应的处理器适配器后,会执行拦截器的preHandle方法,然后执行处理器适配器的handle方法,这个就是实际上调用我们所写的controller了,该方法有几个实现 Snipaste_20200716_131323.jpg 这里调用的是AbstractHandlerMethodAdapter的方法,该方法调用了抽象方法handleInternal,它的实现在RequestMappingHandlerAdapter类中

java
protected ModelAndView handleInternal(HttpServletRequest request,
+      HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
+
+   ModelAndView mav;
+   checkRequest(request);
+
+   // Execute invokeHandlerMethod in synchronized block if required.
+   if (this.synchronizeOnSession) {
+      HttpSession session = request.getSession(false);
+      if (session != null) {
+         Object mutex = WebUtils.getSessionMutex(session);
+         synchronized (mutex) {
+            mav = invokeHandlerMethod(request, response, handlerMethod);
+         }
+      }
+      else {
+         // No HttpSession available -> no mutex necessary
+         mav = invokeHandlerMethod(request, response, handlerMethod);
+      }
+   }
+   else {
+      // No synchronization on session demanded at all...
+      mav = invokeHandlerMethod(request, response, handlerMethod);
+   }
+
+   if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
+      if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
+         applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
+      }
+      else {
+         prepareResponse(response);
+      }
+   }
+
+   return mav;
+}
protected ModelAndView handleInternal(HttpServletRequest request,
+      HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
+
+   ModelAndView mav;
+   checkRequest(request);
+
+   // Execute invokeHandlerMethod in synchronized block if required.
+   if (this.synchronizeOnSession) {
+      HttpSession session = request.getSession(false);
+      if (session != null) {
+         Object mutex = WebUtils.getSessionMutex(session);
+         synchronized (mutex) {
+            mav = invokeHandlerMethod(request, response, handlerMethod);
+         }
+      }
+      else {
+         // No HttpSession available -> no mutex necessary
+         mav = invokeHandlerMethod(request, response, handlerMethod);
+      }
+   }
+   else {
+      // No synchronization on session demanded at all...
+      mav = invokeHandlerMethod(request, response, handlerMethod);
+   }
+
+   if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
+      if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
+         applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
+      }
+      else {
+         prepareResponse(response);
+      }
+   }
+
+   return mav;
+}

其中重点在invokeHandlerMethod方法,这个方法首先初始化了一个新的handlerMethod对象,添加了相关的解析组件,返回值处理器等等,然后执行了invokeAndHandle方法,然后最终调用了InvocableHandlerMethod类中的doInvoke方法

java
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
+      HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
+   ServletWebRequest webRequest = new ServletWebRequest(request, response);
+   try {
+      WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
+      ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
+
+      ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
+      if (this.argumentResolvers != null) {
+         invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
+      }
+      if (this.returnValueHandlers != null) {
+         invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
+      }
+      invocableMethod.setDataBinderFactory(binderFactory);
+      invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
+
+      ModelAndViewContainer mavContainer = new ModelAndViewContainer();
+      mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
+      modelFactory.initModel(webRequest, mavContainer, invocableMethod);
+      mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
+
+      AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
+      asyncWebRequest.setTimeout(this.asyncRequestTimeout);
+
+      WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
+      asyncManager.setTaskExecutor(this.taskExecutor);
+      asyncManager.setAsyncWebRequest(asyncWebRequest);
+      asyncManager.registerCallableInterceptors(this.callableInterceptors);
+      asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
+
+      if (asyncManager.hasConcurrentResult()) {
+         Object result = asyncManager.getConcurrentResult();
+         mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
+         asyncManager.clearConcurrentResult();
+         LogFormatUtils.traceDebug(logger, traceOn -> {
+            String formatted = LogFormatUtils.formatValue(result, !traceOn);
+            return "Resume with async result [" + formatted + "]";
+         });
+         invocableMethod = invocableMethod.wrapConcurrentResult(result);
+      }
+
+      invocableMethod.invokeAndHandle(webRequest, mavContainer);
+      if (asyncManager.isConcurrentHandlingStarted()) {
+         return null;
+      }
+
+      return getModelAndView(mavContainer, modelFactory, webRequest);
+   }
+   finally {
+      webRequest.requestCompleted();
+   }
+}
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
+      HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
+   ServletWebRequest webRequest = new ServletWebRequest(request, response);
+   try {
+      WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
+      ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
+
+      ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
+      if (this.argumentResolvers != null) {
+         invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
+      }
+      if (this.returnValueHandlers != null) {
+         invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
+      }
+      invocableMethod.setDataBinderFactory(binderFactory);
+      invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
+
+      ModelAndViewContainer mavContainer = new ModelAndViewContainer();
+      mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
+      modelFactory.initModel(webRequest, mavContainer, invocableMethod);
+      mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
+
+      AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
+      asyncWebRequest.setTimeout(this.asyncRequestTimeout);
+
+      WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
+      asyncManager.setTaskExecutor(this.taskExecutor);
+      asyncManager.setAsyncWebRequest(asyncWebRequest);
+      asyncManager.registerCallableInterceptors(this.callableInterceptors);
+      asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
+
+      if (asyncManager.hasConcurrentResult()) {
+         Object result = asyncManager.getConcurrentResult();
+         mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
+         asyncManager.clearConcurrentResult();
+         LogFormatUtils.traceDebug(logger, traceOn -> {
+            String formatted = LogFormatUtils.formatValue(result, !traceOn);
+            return "Resume with async result [" + formatted + "]";
+         });
+         invocableMethod = invocableMethod.wrapConcurrentResult(result);
+      }
+
+      invocableMethod.invokeAndHandle(webRequest, mavContainer);
+      if (asyncManager.isConcurrentHandlingStarted()) {
+         return null;
+      }
+
+      return getModelAndView(mavContainer, modelFactory, webRequest);
+   }
+   finally {
+      webRequest.requestCompleted();
+   }
+}
  • doInvoke方法

这里就比较明显了,首先利用暴力反射将方法设置为可访问的,然后直接反射调用并返回结果

java
/**
+ * Invoke the handler method with the given argument values.
+ */
+@Nullable
+protected Object doInvoke(Object... args) throws Exception {
+   ReflectionUtils.makeAccessible(getBridgedMethod());
+   try {
+      return getBridgedMethod().invoke(getBean(), args);
+   }
+   catch (IllegalArgumentException ex) {
+      assertTargetBean(getBridgedMethod(), getBean(), args);
+      String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
+      throw new IllegalStateException(formatInvokeError(text, args), ex);
+   }
+   catch (InvocationTargetException ex) {
+      // Unwrap for HandlerExceptionResolvers ...
+      Throwable targetException = ex.getTargetException();
+      if (targetException instanceof RuntimeException) {
+         throw (RuntimeException) targetException;
+      }
+      else if (targetException instanceof Error) {
+         throw (Error) targetException;
+      }
+      else if (targetException instanceof Exception) {
+         throw (Exception) targetException;
+      }
+      else {
+         throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException);
+      }
+   }
+}
/**
+ * Invoke the handler method with the given argument values.
+ */
+@Nullable
+protected Object doInvoke(Object... args) throws Exception {
+   ReflectionUtils.makeAccessible(getBridgedMethod());
+   try {
+      return getBridgedMethod().invoke(getBean(), args);
+   }
+   catch (IllegalArgumentException ex) {
+      assertTargetBean(getBridgedMethod(), getBean(), args);
+      String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
+      throw new IllegalStateException(formatInvokeError(text, args), ex);
+   }
+   catch (InvocationTargetException ex) {
+      // Unwrap for HandlerExceptionResolvers ...
+      Throwable targetException = ex.getTargetException();
+      if (targetException instanceof RuntimeException) {
+         throw (RuntimeException) targetException;
+      }
+      else if (targetException instanceof Error) {
+         throw (Error) targetException;
+      }
+      else if (targetException instanceof Exception) {
+         throw (Exception) targetException;
+      }
+      else {
+         throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException);
+      }
+   }
+}

返回modelAndview对象后就是渲染的一些操作

+ + + + \ No newline at end of file diff --git "a/java/framework/Spring\351\205\215\347\275\256\345\222\214\346\235\241\344\273\266\345\214\226\347\273\204\344\273\266\345\212\240\350\275\275\346\263\250\350\247\243.html" "b/java/framework/Spring\351\205\215\347\275\256\345\222\214\346\235\241\344\273\266\345\214\226\347\273\204\344\273\266\345\212\240\350\275\275\346\263\250\350\247\243.html" new file mode 100644 index 000000000..d79d491f7 --- /dev/null +++ "b/java/framework/Spring\351\205\215\347\275\256\345\222\214\346\235\241\344\273\266\345\214\226\347\273\204\344\273\266\345\212\240\350\275\275\346\263\250\350\247\243.html" @@ -0,0 +1,25 @@ + + + + + + Spring配置和条件化组件加载注解 | 故事 + + + + + + + + + + + + + + +
Skip to content

Spring配置和条件化组件加载注解

  • @ConditionalOnProperty:该注解用于根据配置属性的值来决定是否启用或禁用特定的配置项。通过指定属性名称和值,可以在配置文件中动态地控制应用程序的行为。

  • @ConditionalOnClass:该注解在类路径中存在特定的类时才会生效。它可以用来根据是否引入了某个类来决定是否加载或配置相关的组件。

  • @EnableConfigurationProperties:该注解用于启用特定的配置属性绑定功能。它通常与 @ConfigurationProperties 注解一起使用,用于将配置文件中的属性值绑定到对应的 Java 对象中。

  • @ConfigurationProperties:将配置文件中的属性值映射到指定的Java类。

  • @ConditionalOnBean:根据指定的bean的存在与否,有条件地加载一个组件。

  • @Conditional:根据指定的条件,有条件地加载一个组件。可以使用自定义条件类。

  • @ConditionalOnMissingBean:如果指定的bean不存在,则有条件地加载一个组件。

  • @ConditionalOnMissingClass:如果类路径中缺少指定的类,则有条件地加载一个组件。

  • @AutoConfigureBefore:用于指定某个自动配置类在另一个指定的自动配置类之前生效。它可以控制自动配置类的加载顺序,确保特定的自动配置类在其他自动配置之前被应用。

  • @ConditionalOnExpression:根据指定的SpEL表达式,有条件地加载一个组件。

  • @ConditionalOnWebApplication:根据应用程序是否为Web应用程序,有条件地加载一个组件。

  • @ConditionalOnResource:根据指定资源的存在与否,有条件地加载一个组件。

+ + + + \ No newline at end of file diff --git "a/java/framework/dubbo\346\216\245\345\217\243\350\266\205\346\227\266\351\205\215\347\275\256.html" "b/java/framework/dubbo\346\216\245\345\217\243\350\266\205\346\227\266\351\205\215\347\275\256.html" new file mode 100644 index 000000000..a39fd4e3b --- /dev/null +++ "b/java/framework/dubbo\346\216\245\345\217\243\350\266\205\346\227\266\351\205\215\347\275\256.html" @@ -0,0 +1,143 @@ + + + + + + dubbo接口超时配置 | 故事 + + + + + + + + + + + + + + +
Skip to content

dubbo接口超时配置

最近在跟第三方公司对接商城的账单数据,按对账单维度直接推送所有订单和明细,在测试大批量数据时出现了接口超时情况。原因是provider和consumer的超时时间都设置的比较短,因此要把这个接口超时时间调整更大一些。

dubbo服务超时时间设置及优先级

dubbo的服务超时时间有三个范围,分别是接口方法、接口类、全局。优先级接口方法>接口类>全局。而consumer服务和provider服务又分别可以配置这些超时时间。优先级为consumer>provider。因此完整的dubbo服务超时配置优先级为消费方method >提供方method>消费方reference>提供方service>消费方全局配置provider>提供方全局配置consumer

使用注解配置

provider:

java
@Service(timeout=60000)//alibaba包中的service,包含了spring framework的@Service功能和暴露服务功能,timeout单位ms
@Service(timeout=60000)//alibaba包中的service,包含了spring framework的@Service功能和暴露服务功能,timeout单位ms

cosumer:

java
@Reference(timeout=60000)//单位ms
@Reference(timeout=60000)//单位ms

使用xml配置

提供方

xml
<dubbo:provider timeout=“5000”/> 全局配置
+
+<dubbo:service timeout=“4000” …/> 接口类配置
+
+<dubbo:method timeout=“3000” …> 方法配置
<dubbo:provider timeout=“5000”/> 全局配置
+
+<dubbo:service timeout=“4000” …/> 接口类配置
+
+<dubbo:method timeout=“3000” …> 方法配置
xml
<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans-4.3.xsd        http://dubbo.apache.org/schema/dubbo        http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
+ 
+    <!-- 提供方应用信息,用于计算依赖关系 -->
+    <dubbo:application name="user-service-provider"  />
+ 
+    <!-- 使用zookeeper广播注册中心暴露服务地址 -->
+    <dubbo:registry address="zookeeper://192.168.0.126:2181" />
+ 
+    <!-- 用dubbo协议在20880端口暴露服务 -->
+    <dubbo:protocol name="dubbo" port="20880" />
+ 
+    <!-- 和本地bean一样实现服务 -->
+    <bean id="userService" class="com.storyxc.service.impl.UserServiceImpl" />
+ 
+    <!-- 声明需要暴露的服务接口 timeout 接口类中全部方法超时配置 优先级2 -->
+    <dubbo:service interface="com.storyxc.service.UserService" ref="userService" timeout="4000" >
+        <!-- 单个方法超时配置优先级最高1 -->
+        <dubbo:method name="queryAllUserAddress" timeout="3000"></dubbo:method>
+    </dubbo:service>
+ 
+    <!-- 服务提供者全局超时时间配置优先级最低 -->
+    <dubbo:provider timeout="5000"></dubbo:provider>
+ 
+</beans>
<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans-4.3.xsd        http://dubbo.apache.org/schema/dubbo        http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
+ 
+    <!-- 提供方应用信息,用于计算依赖关系 -->
+    <dubbo:application name="user-service-provider"  />
+ 
+    <!-- 使用zookeeper广播注册中心暴露服务地址 -->
+    <dubbo:registry address="zookeeper://192.168.0.126:2181" />
+ 
+    <!-- 用dubbo协议在20880端口暴露服务 -->
+    <dubbo:protocol name="dubbo" port="20880" />
+ 
+    <!-- 和本地bean一样实现服务 -->
+    <bean id="userService" class="com.storyxc.service.impl.UserServiceImpl" />
+ 
+    <!-- 声明需要暴露的服务接口 timeout 接口类中全部方法超时配置 优先级2 -->
+    <dubbo:service interface="com.storyxc.service.UserService" ref="userService" timeout="4000" >
+        <!-- 单个方法超时配置优先级最高1 -->
+        <dubbo:method name="queryAllUserAddress" timeout="3000"></dubbo:method>
+    </dubbo:service>
+ 
+    <!-- 服务提供者全局超时时间配置优先级最低 -->
+    <dubbo:provider timeout="5000"></dubbo:provider>
+ 
+</beans>

消费方

xml
<dubbo:consumer timeout=“4000” > 全局配置
+
+<dubbo:reference timeout=“3000” …> 接口类配置
+
+<dubbo:method timeout=“2000” …> 方法配置
<dubbo:consumer timeout=“4000” > 全局配置
+
+<dubbo:reference timeout=“3000” …> 接口类配置
+
+<dubbo:method timeout=“2000” …> 方法配置
xml
<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans-4.3.xsd        http://dubbo.apache.org/schema/dubbo        http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
+ 
+    <!-- 提供方应用信息,用于计算依赖关系 -->
+    <dubbo:application name="order-service-consumer"  />
+ 
+    <!-- 使用zookeeper广播注册中心暴露服务地址 -->
+    <dubbo:registry address="zookeeper://192.168.0.126:2181" />
+ 
+    <!-- 引用配置: 创建一个远程服务代理,一个引用可以指向多个注册中心 -->
+    <dubbo:reference id="userService" timeout="3000" interface="com.storyxc.service.UserService">
+        <dubbo:method name="queryAllUserAddress" timeout="2000"></dubbo:method>
+    </dubbo:reference>
+ 
+    <!-- orderService -->
+    <bean class="com.storyxc.service.impl.OrderServiceImpl">
+        <property name="userService" ref="userService"></property>
+    </bean>
+ 
+    <!-- 超时全局配置 -->
+    <dubbo:consumer timeout="4000"></dubbo:consumer>
+</beans>
<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans-4.3.xsd        http://dubbo.apache.org/schema/dubbo        http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
+ 
+    <!-- 提供方应用信息,用于计算依赖关系 -->
+    <dubbo:application name="order-service-consumer"  />
+ 
+    <!-- 使用zookeeper广播注册中心暴露服务地址 -->
+    <dubbo:registry address="zookeeper://192.168.0.126:2181" />
+ 
+    <!-- 引用配置: 创建一个远程服务代理,一个引用可以指向多个注册中心 -->
+    <dubbo:reference id="userService" timeout="3000" interface="com.storyxc.service.UserService">
+        <dubbo:method name="queryAllUserAddress" timeout="2000"></dubbo:method>
+    </dubbo:reference>
+ 
+    <!-- orderService -->
+    <bean class="com.storyxc.service.impl.OrderServiceImpl">
+        <property name="userService" ref="userService"></property>
+    </bean>
+ 
+    <!-- 超时全局配置 -->
+    <dubbo:consumer timeout="4000"></dubbo:consumer>
+</beans>
+ + + + \ No newline at end of file diff --git "a/java/framework/logback\350\207\252\345\256\232\344\271\211.html" "b/java/framework/logback\350\207\252\345\256\232\344\271\211.html" new file mode 100644 index 000000000..980341bce --- /dev/null +++ "b/java/framework/logback\350\207\252\345\256\232\344\271\211.html" @@ -0,0 +1,207 @@ + + + + + + logback自定义 | 故事 + + + + + + + + + + + + + + +
Skip to content

logback自定义

自定义日志动态输出内容

可以通过自定义全局拦截器通过MDC存储数据,在logback配置文件中直接通过%X{变量名} 读取变量。

java
/**
+ * @author xc
+ * @description 全局拦截器
+ * @date 2023/5/11 16:13
+ */
+@Component
+@Slf4j
+public class GlobalInterceptor implements HandlerInterceptor {
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        // 获取当前登录用户信息
+        LoginSession loginSession = LoginContext.getLoginSession();
+        if (loginSession != null) {
+            MDC.put("operator", loginSession.getUserName());
+        }else {
+            MDC.put("operator", "anonymous");
+        }
+        return true;
+    }
+}
/**
+ * @author xc
+ * @description 全局拦截器
+ * @date 2023/5/11 16:13
+ */
+@Component
+@Slf4j
+public class GlobalInterceptor implements HandlerInterceptor {
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        // 获取当前登录用户信息
+        LoginSession loginSession = LoginContext.getLoginSession();
+        if (loginSession != null) {
+            MDC.put("operator", loginSession.getUserName());
+        }else {
+            MDC.put("operator", "anonymous");
+        }
+        return true;
+    }
+}

logback-spring.xml

xml
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+    <!--日志文件输出格式-->
+    <!--%X{operator}可以直接获取当前线程MDC中的operator参数输出到日志中-->
+    <encoder>
+        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} %X{operator}  %msg%n</pattern> 
+        <charset>UTF-8</charset>
+    </encoder>
+    ...
+</appender>
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+    <!--日志文件输出格式-->
+    <!--%X{operator}可以直接获取当前线程MDC中的operator参数输出到日志中-->
+    <encoder>
+        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} %X{operator}  %msg%n</pattern> 
+        <charset>UTF-8</charset>
+    </encoder>
+    ...
+</appender>

自定义converter

通过自定义转换器来对日志输出的内容进行自定义处理

java
/**
+ * @author xc
+ * @description
+ * @date 2023/5/23 15:39
+ */
+public class OperatorConverter extends ClassicConverter {
+    @Override
+    public String convert(ILoggingEvent event) {
+        String operator = event.getMDCPropertyMap().get("operator");
+        return StrUtil.isNotBlank(operator) ? "- " + operator + " - " : "";
+    }
+}
/**
+ * @author xc
+ * @description
+ * @date 2023/5/23 15:39
+ */
+public class OperatorConverter extends ClassicConverter {
+    @Override
+    public String convert(ILoggingEvent event) {
+        String operator = event.getMDCPropertyMap().get("operator");
+        return StrUtil.isNotBlank(operator) ? "- " + operator + " - " : "";
+    }
+}

logback-spring.xml

xml
<configuration scan="true" scanPeriod="10 seconds">
+
+    <!--先声明一个转换器-->
+    <conversionRule conversionWord="operator" converterClass="com.storyxc.config.logback.converter.OperatorConverter" />
+    <!--在输出的pattern中直接使用,此时就不需要用%X{变量}了,直接%conversionWord即可-->
+    <!--此时如果MDC中没有operator变量时%operator会输出空串,否则会输出 "- 操作人姓名 - "-->
+    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+    <!--日志文件输出格式-->
+    <encoder>
+        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} %operator%msg%n</pattern> 
+        <charset>UTF-8</charset>
+    </encoder>
+    ...
+</appender>
+</configuration>
<configuration scan="true" scanPeriod="10 seconds">
+
+    <!--先声明一个转换器-->
+    <conversionRule conversionWord="operator" converterClass="com.storyxc.config.logback.converter.OperatorConverter" />
+    <!--在输出的pattern中直接使用,此时就不需要用%X{变量}了,直接%conversionWord即可-->
+    <!--此时如果MDC中没有operator变量时%operator会输出空串,否则会输出 "- 操作人姓名 - "-->
+    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+    <!--日志文件输出格式-->
+    <encoder>
+        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} %operator%msg%n</pattern> 
+        <charset>UTF-8</charset>
+    </encoder>
+    ...
+</appender>
+</configuration>

自定义filter

java
/**
+ * @author xc
+ * @description logback日志过滤器:开发环境下只打印本项目路径下的debug & debug以上级别的日志
+ * @date 2023/5/16 20:15
+ */
+public class LogBackDebugPackageFilter extends AbstractMatcherFilter<ILoggingEvent> {
+    private static final String PROJECT_BASE_PACKAGE = "com.storyxc";
+
+    @Override
+    public FilterReply decide(ILoggingEvent event) {
+        if (!isStarted()) {
+            return FilterReply.NEUTRAL;
+        }
+        Level level = event.getLevel();
+        if (level.isGreaterOrEqual(Level.DEBUG)) {
+            String loggerName = event.getLoggerName();
+            if (level.equals(Level.DEBUG)) {
+                if (loggerName != null && loggerName.startsWith(PROJECT_BASE_PACKAGE)) {
+                    return FilterReply.ACCEPT;
+                } else {
+                    return FilterReply.DENY;
+                }
+            }else {
+                return FilterReply.ACCEPT;
+            }
+        } else {
+            return FilterReply.NEUTRAL;
+        }
+    }
+}
/**
+ * @author xc
+ * @description logback日志过滤器:开发环境下只打印本项目路径下的debug & debug以上级别的日志
+ * @date 2023/5/16 20:15
+ */
+public class LogBackDebugPackageFilter extends AbstractMatcherFilter<ILoggingEvent> {
+    private static final String PROJECT_BASE_PACKAGE = "com.storyxc";
+
+    @Override
+    public FilterReply decide(ILoggingEvent event) {
+        if (!isStarted()) {
+            return FilterReply.NEUTRAL;
+        }
+        Level level = event.getLevel();
+        if (level.isGreaterOrEqual(Level.DEBUG)) {
+            String loggerName = event.getLoggerName();
+            if (level.equals(Level.DEBUG)) {
+                if (loggerName != null && loggerName.startsWith(PROJECT_BASE_PACKAGE)) {
+                    return FilterReply.ACCEPT;
+                } else {
+                    return FilterReply.DENY;
+                }
+            }else {
+                return FilterReply.ACCEPT;
+            }
+        } else {
+            return FilterReply.NEUTRAL;
+        }
+    }
+}

logback-spring.xml

xml
<!--输出到控制台-->
+<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+    <filter class="com.storyxc.config.logback.LogBackDebugPackageFilter">
+    </filter>
+    <encoder>
+        <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
+        <!-- 设置字符集 -->
+        <charset>UTF-8</charset>
+    </encoder>
+</appender>
<!--输出到控制台-->
+<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+    <filter class="com.storyxc.config.logback.LogBackDebugPackageFilter">
+    </filter>
+    <encoder>
+        <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
+        <!-- 设置字符集 -->
+        <charset>UTF-8</charset>
+    </encoder>
+</appender>
+ + + + \ No newline at end of file diff --git "a/java/framework/netty+websocket\345\256\236\347\216\260\345\215\263\346\227\266\351\200\232\350\256\257\345\212\237\350\203\275.html" "b/java/framework/netty+websocket\345\256\236\347\216\260\345\215\263\346\227\266\351\200\232\350\256\257\345\212\237\350\203\275.html" new file mode 100644 index 000000000..b19a4eea7 --- /dev/null +++ "b/java/framework/netty+websocket\345\256\236\347\216\260\345\215\263\346\227\266\351\200\232\350\256\257\345\212\237\350\203\275.html" @@ -0,0 +1,443 @@ + + + + + + netty+websocket实现即时通讯功能 | 故事 + + + + + + + + + + + + + + +
Skip to content

netty+websocket实现即时通讯功能

大致思路:

  • 后台应用启动后开启nettyserver
  • 前台登录后使用websocket连接netty
  • 登录时先向netty发送一条初始化消息,服务器将保存通道和当前用户的关系映射
  • 通讯双方都上线时,即可以开始聊天

后台:

  • 应用启动后,开启netty服务器
java
@Component
+public class ServerStarter implements ApplicationListener<ContextRefreshedEvent> {
+
+    @Override
+    public void onApplicationEvent(ContextRefreshedEvent event) {
+        if (event.getApplicationContext().getParent() == null ){
+            try {
+                new IMServer().start();
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+    }
@Component
+public class ServerStarter implements ApplicationListener<ContextRefreshedEvent> {
+
+    @Override
+    public void onApplicationEvent(ContextRefreshedEvent event) {
+        if (event.getApplicationContext().getParent() == null ){
+            try {
+                new IMServer().start();
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+    }
  • nettyserver
java
/**
+ * @author Xc
+ * @description
+ * @createdTime 2020/7/9 9:23
+ */
+public class IMServer {
+    Logger logger = LoggerFactory.getLogger(IMServer.class);
+
+    public void start() throws InterruptedException {
+        NioEventLoopGroup boss = new NioEventLoopGroup();
+        NioEventLoopGroup worker = new NioEventLoopGroup();
+        ServerBootstrap serverBootstrap = new ServerBootstrap();
+        serverBootstrap.group(boss,worker)
+                .channel(NioServerSocketChannel.class)
+                .localAddress(8000)
+		//自定义初始化器
+                .childHandler(new IMStoryInitializer());
+        ChannelFuture future = serverBootstrap.bind();
+        future.addListener(new ChannelFutureListener() {
+            @Override
+            public void operationComplete(ChannelFuture future) throws Exception {
+                logger.info("server start on 8000");
+            }
+        });
+    }
+}
/**
+ * @author Xc
+ * @description
+ * @createdTime 2020/7/9 9:23
+ */
+public class IMServer {
+    Logger logger = LoggerFactory.getLogger(IMServer.class);
+
+    public void start() throws InterruptedException {
+        NioEventLoopGroup boss = new NioEventLoopGroup();
+        NioEventLoopGroup worker = new NioEventLoopGroup();
+        ServerBootstrap serverBootstrap = new ServerBootstrap();
+        serverBootstrap.group(boss,worker)
+                .channel(NioServerSocketChannel.class)
+                .localAddress(8000)
+		//自定义初始化器
+                .childHandler(new IMStoryInitializer());
+        ChannelFuture future = serverBootstrap.bind();
+        future.addListener(new ChannelFutureListener() {
+            @Override
+            public void operationComplete(ChannelFuture future) throws Exception {
+                logger.info("server start on 8000");
+            }
+        });
+    }
+}
  • 初始化器
java
/**
+ * @author Xc
+ * @description
+ * @createdTime 2020/7/9 9:28
+ */
+public class IMStoryInitializer extends ChannelInitializer<SocketChannel> {
+    
+    @Override
+    protected void initChannel(SocketChannel ch) throws Exception {
+        ChannelPipeline pipeline = ch.pipeline();
+        //http编解码器
+        pipeline.addLast(new HttpServerCodec());
+        //以块写数据
+        pipeline.addLast(new ChunkedWriteHandler());
+        //聚合器
+        pipeline.addLast(new HttpObjectAggregator(64*1024));
+
+        //websocket
+        pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
+        //自定义handler
+        pipeline.addLast(new ChatHandler());
+        
+    }
+}
/**
+ * @author Xc
+ * @description
+ * @createdTime 2020/7/9 9:28
+ */
+public class IMStoryInitializer extends ChannelInitializer<SocketChannel> {
+    
+    @Override
+    protected void initChannel(SocketChannel ch) throws Exception {
+        ChannelPipeline pipeline = ch.pipeline();
+        //http编解码器
+        pipeline.addLast(new HttpServerCodec());
+        //以块写数据
+        pipeline.addLast(new ChunkedWriteHandler());
+        //聚合器
+        pipeline.addLast(new HttpObjectAggregator(64*1024));
+
+        //websocket
+        pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
+        //自定义handler
+        pipeline.addLast(new ChatHandler());
+        
+    }
+}
  • 自定义handler
java
/**
+ * @author Xc
+ * @description
+ * @createdTime 2020/7/9 9:34
+ */
+public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
+
+    /**
+     * 管理所有channel
+     */
+    public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
+
+    @Override
+    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
+        //客户端发过来的消息
+        String content = msg.text();
+        //当前通道
+        Channel channel = ctx.channel();
+
+        Message message = JSON.parseObject(content, Message.class);
+
+        String data = message.getMsg();
+
+        String fromUser = message.getFromUser();
+        //客户端建立连接后先发送一条init消息,后台保存这个通道和用户信息的映射
+        if (StringUtils.equals(message.getAction(), "init")) {
+            ChannelUserContext.put(fromUser,channel);
+        } else if (StringUtils.equals(message.getAction(),"chat")){
+            Channel toChannel = ChannelUserContext.get(message.getToUser());
+            if (toChannel != null) {
+                //消息接收方在线
+                toChannel.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(message.getFromUser() + " : " + message.getMsg())));
+            } else {
+                //接收方不在线 离线消息推送
+            }
+
+        }
+
+    }
+
+    @Override
+    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
+        channels.add(ctx.channel());
+    }
+
+    @Override
+    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
+        channels.remove(ctx.channel());
+    }
+}
/**
+ * @author Xc
+ * @description
+ * @createdTime 2020/7/9 9:34
+ */
+public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
+
+    /**
+     * 管理所有channel
+     */
+    public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
+
+    @Override
+    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
+        //客户端发过来的消息
+        String content = msg.text();
+        //当前通道
+        Channel channel = ctx.channel();
+
+        Message message = JSON.parseObject(content, Message.class);
+
+        String data = message.getMsg();
+
+        String fromUser = message.getFromUser();
+        //客户端建立连接后先发送一条init消息,后台保存这个通道和用户信息的映射
+        if (StringUtils.equals(message.getAction(), "init")) {
+            ChannelUserContext.put(fromUser,channel);
+        } else if (StringUtils.equals(message.getAction(),"chat")){
+            Channel toChannel = ChannelUserContext.get(message.getToUser());
+            if (toChannel != null) {
+                //消息接收方在线
+                toChannel.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(message.getFromUser() + " : " + message.getMsg())));
+            } else {
+                //接收方不在线 离线消息推送
+            }
+
+        }
+
+    }
+
+    @Override
+    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
+        channels.add(ctx.channel());
+    }
+
+    @Override
+    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
+        channels.remove(ctx.channel());
+    }
+}
  • 通道和用户的映射保存
java
/**
+ * @author Xc
+ * @description
+ * @createdTime 2020/7/9 9:46
+ */
+public class ChannelUserContext {
+
+    private static ConcurrentHashMap<String, Channel> userChannelMap;
+
+    static{
+        userChannelMap = new ConcurrentHashMap<>();
+    }
+
+    public static void put(String user, Channel channel){
+        userChannelMap.put(user,channel);
+    }
+
+    public static Channel get(String user) {
+        return userChannelMap.get(user);
+    }
+    
+}
/**
+ * @author Xc
+ * @description
+ * @createdTime 2020/7/9 9:46
+ */
+public class ChannelUserContext {
+
+    private static ConcurrentHashMap<String, Channel> userChannelMap;
+
+    static{
+        userChannelMap = new ConcurrentHashMap<>();
+    }
+
+    public static void put(String user, Channel channel){
+        userChannelMap.put(user,channel);
+    }
+
+    public static Channel get(String user) {
+        return userChannelMap.get(user);
+    }
+    
+}
  • message
java
/**
+ * @author Xc
+ * @description
+ * @createdTime 2020/7/9 9:38
+ */
+@Data
+public class Message implements Serializable {
+    private static final long serialVersionUID = 301234912340234L;
+    private String msg;
+    private String fromUser;
+    private String toUser;
+    private String action;
+}
/**
+ * @author Xc
+ * @description
+ * @createdTime 2020/7/9 9:38
+ */
+@Data
+public class Message implements Serializable {
+    private static final long serialVersionUID = 301234912340234L;
+    private String msg;
+    private String fromUser;
+    private String toUser;
+    private String action;
+}

前台页面1

html
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>WebSocket客户端</title>
+</head>
+<body>
+<script type="text/javascript">
+    var socket;
+
+    //如果浏览器支持WebSocket
+    if(window.WebSocket){
+        //参数就是与服务器连接的地址
+        socket = new WebSocket("ws://localhost:8000/ws");
+
+        //客户端收到服务器消息的时候就会执行这个回调方法
+        socket.onmessage = function (event) {
+			console.log(event);
+            var ta = document.getElementById("responseText");
+            ta.value = ta.value + "\n"+event.data;
+        }
+
+        //连接建立的回调函数
+        socket.onopen = function(event){
+            var ta = document.getElementById("responseText");
+
+            ta.value = "连接开启";
+            var message = '{"action":"init","msg":"test","fromUser":"张三","toUser":"李四"}';
+            socket.send(message);
+        }
+
+        //连接断掉的回调函数
+        socket.onclose = function (event) {
+            var ta = document.getElementById("responseText");
+            ta.value = ta.value +"\n"+"连接关闭";
+        }
+    }else{
+        alert("浏览器不支持WebSocket!");
+    }
+
+    //发送数据
+    function send(message){
+        if(!window.WebSocket){
+            return;
+        }
+
+        //当websocket状态打开
+        if(socket.readyState == WebSocket.OPEN){
+			message = '{"action":"chat","msg":"'+ message +'","fromUser":"张三","toUser":"李四"}';
+            socket.send(message);
+        }else{
+            alert("连接没有开启");
+        }
+    }
+</script>
+<form onsubmit="return false">
+    <textarea name = "message" style="width: 400px;height: 200px"></textarea>
+
+    <input type ="button" value="张三:发送数据" onclick="send(this.form.message.value);">
+
+    <h3>服务器输出:</h3>
+
+    <textarea id ="responseText" style="width: 400px;height: 300px;"></textarea>
+
+    <input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="清空数据">
+</form>
+</body>
+</html>
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>WebSocket客户端</title>
+</head>
+<body>
+<script type="text/javascript">
+    var socket;
+
+    //如果浏览器支持WebSocket
+    if(window.WebSocket){
+        //参数就是与服务器连接的地址
+        socket = new WebSocket("ws://localhost:8000/ws");
+
+        //客户端收到服务器消息的时候就会执行这个回调方法
+        socket.onmessage = function (event) {
+			console.log(event);
+            var ta = document.getElementById("responseText");
+            ta.value = ta.value + "\n"+event.data;
+        }
+
+        //连接建立的回调函数
+        socket.onopen = function(event){
+            var ta = document.getElementById("responseText");
+
+            ta.value = "连接开启";
+            var message = '{"action":"init","msg":"test","fromUser":"张三","toUser":"李四"}';
+            socket.send(message);
+        }
+
+        //连接断掉的回调函数
+        socket.onclose = function (event) {
+            var ta = document.getElementById("responseText");
+            ta.value = ta.value +"\n"+"连接关闭";
+        }
+    }else{
+        alert("浏览器不支持WebSocket!");
+    }
+
+    //发送数据
+    function send(message){
+        if(!window.WebSocket){
+            return;
+        }
+
+        //当websocket状态打开
+        if(socket.readyState == WebSocket.OPEN){
+			message = '{"action":"chat","msg":"'+ message +'","fromUser":"张三","toUser":"李四"}';
+            socket.send(message);
+        }else{
+            alert("连接没有开启");
+        }
+    }
+</script>
+<form onsubmit="return false">
+    <textarea name = "message" style="width: 400px;height: 200px"></textarea>
+
+    <input type ="button" value="张三:发送数据" onclick="send(this.form.message.value);">
+
+    <h3>服务器输出:</h3>
+
+    <textarea id ="responseText" style="width: 400px;height: 300px;"></textarea>
+
+    <input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="清空数据">
+</form>
+</body>
+</html>

页面二就是对这个页面稍微改一下

启动后台后打开两个页面,即可开始进行通讯 1.jpg

2.jpg3.jpg4.jpg

+ + + + \ No newline at end of file diff --git "a/java/framework/spring-session\345\256\236\347\216\260\351\233\206\347\276\244session\345\205\261\344\272\253.html" "b/java/framework/spring-session\345\256\236\347\216\260\351\233\206\347\276\244session\345\205\261\344\272\253.html" new file mode 100644 index 000000000..53f3131bf --- /dev/null +++ "b/java/framework/spring-session\345\256\236\347\216\260\351\233\206\347\276\244session\345\205\261\344\272\253.html" @@ -0,0 +1,127 @@ + + + + + + spring-session实现集群session共享 | 故事 + + + + + + + + + + + + + + +
Skip to content

spring-session实现集群session共享

问题场景

系统后台登录使用的是图片验证码,生成验证码接口会把验证码放在session中,登录时前端会携带账号密码的密文和验证码到后台,后台再用session中的验证码和前端带过来的进行校验。利用单机的session存储信息有个显而易见的问题,集群环境下这个功能是不可用的,因此要对这个过程进行改造,实现集群共享session,这里采用的是spring-session + redis的方案。

实现

spring-session和springboot的集成非常简单,如果项目使用的是springboot只需要引入依赖,再添加几个简单的配置类即可。如果是ssm那就要稍微麻烦点,要写xml的配置文件。本文是springboot的集成方案,详细内容可以下载spring-session官方sample查看。

依赖

xml
<dependencies>
+		<!--spring-session-->
+		<dependency>
+			<groupId>org.springframework.session</groupId>
+			<artifactId>spring-session-data-redis</artifactId>
+			<version>2.0.3.RELEASE</version>
+		</dependency>
+</dependencies>
<dependencies>
+		<!--spring-session-->
+		<dependency>
+			<groupId>org.springframework.session</groupId>
+			<artifactId>spring-session-data-redis</artifactId>
+			<version>2.0.3.RELEASE</version>
+		</dependency>
+</dependencies>

配置类

java
import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer;
+
+public class Initializer extends AbstractHttpSessionApplicationInitializer {
+
+	public Initializer() {
+		super(Config.class);
+	}
+
+}
import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer;
+
+public class Initializer extends AbstractHttpSessionApplicationInitializer {
+
+	public Initializer() {
+		super(Config.class);
+	}
+
+}
java
import org.springframework.context.annotation.Import;
+import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
+
+@Import(EmbeddedRedisConfig.class)
+@EnableRedisHttpSession
+public class Config {
+
+}
import org.springframework.context.annotation.Import;
+import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
+
+@Import(EmbeddedRedisConfig.class)
+@EnableRedisHttpSession
+public class Config {
+
+}
java
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+
+@Configuration
+@Slf4j
+public class EmbeddedRedisConfig {
+
+    @Value("${session.share.redis.ip}")
+    private String ip;
+    @Value("${session.share.redis.db}")
+    private String db;
+
+
+    @Bean
+    public RedisConnectionFactory redisConnectionFactory() {
+        log.info("session redis ip :{}, db:{}",ip,db);
+        RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();
+        redisConfig.setHostName(ip);
+        redisConfig.setPort(6379);
+        //指定database
+        redisConfig.setDatabase(Integer.parseInt(db));
+        return new JedisConnectionFactory(redisConfig);
+    }
+
+}
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+
+@Configuration
+@Slf4j
+public class EmbeddedRedisConfig {
+
+    @Value("${session.share.redis.ip}")
+    private String ip;
+    @Value("${session.share.redis.db}")
+    private String db;
+
+
+    @Bean
+    public RedisConnectionFactory redisConnectionFactory() {
+        log.info("session redis ip :{}, db:{}",ip,db);
+        RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();
+        redisConfig.setHostName(ip);
+        redisConfig.setPort(6379);
+        //指定database
+        redisConfig.setDatabase(Integer.parseInt(db));
+        return new JedisConnectionFactory(redisConfig);
+    }
+
+}

业务代码

这里就省略了,思路很简单,就是用session_id作为key存储登录验证码,从而实现不论登录请求落到集群的哪个服务器都能从redis中获取到正确的验证码。

总结

不得不感慨下spring生态的强大,毫不费力的就集成了一个功能。spring-session使用redis共享session的原理是通过RedisHttpSessionConfiguration配置类,生成一个过滤器SessionRepositoryFilter并且加入到过滤器链中,而且这个过滤器优先级是最高的,然后在SessionRepositoryFilter的doFilter方法中使用HttpServletRequest和HttpServletResponse的包装类把原始的request、response对象包装一下传递到其他过滤器中,在doFilter的final代码段里通过commitSession实现session到redis的持久化。简单的说就是spring-session使用redis存储的session替换了tomcat的httpsession实现。

+ + + + \ No newline at end of file diff --git "a/java/framework/springcloud\344\274\230\351\233\205\344\270\213\347\272\277\346\234\215\345\212\241.html" "b/java/framework/springcloud\344\274\230\351\233\205\344\270\213\347\272\277\346\234\215\345\212\241.html" new file mode 100644 index 000000000..ba077d934 --- /dev/null +++ "b/java/framework/springcloud\344\274\230\351\233\205\344\270\213\347\272\277\346\234\215\345\212\241.html" @@ -0,0 +1,265 @@ + + + + + + springcloud优雅下线服务 | 故事 + + + + + + + + + + + + + + +
Skip to content

springcloud优雅下线服务

问题场景

线上问题需要紧急修复部署,但是服务一直在跑,网关还会一直往服务中路由请求进行处理,如果直接停掉服务,客户端会出现业务中断的问题。因此需要在替换更新前先手动下线服务,这样用户请求不会被分发到下线的节点上,就可以直接进行更新而不影响用户体验。

实现

查询注册中心中服务的状态

bash
curl -GET -u eureka_username:eureka_password http://eureka_ip:eureka_port/eureka/apps/服务名称/实例ip:实例名称:实例port
curl -GET -u eureka_username:eureka_password http://eureka_ip:eureka_port/eureka/apps/服务名称/实例ip:实例名称:实例port

例如

bash
curl -GET -u admin:admin123 http://127.0.0.1:19991/eureka/apps/app-platform/127.0.0.1:app-platform-1.0.0-SNAPSHOT:8001
curl -GET -u admin:admin123 http://127.0.0.1:19991/eureka/apps/app-platform/127.0.0.1:app-platform-1.0.0-SNAPSHOT:8001

结果

xml
<instance>
+  <instanceId>127.0.0.1:app-platform-2.0.0-SNAPSHOT:8001</instanceId>
+  <hostName>127.0.0.1</hostName>
+  <app>app-platform</app>
+  <ipAddr>127.0.0.1</ipAddr>
+  <status>UP</status>  # 状态为在线
+  <overriddenstatus>UNKNOWN</overriddenstatus> # 重写状态为空
+  <port enabled="true">8001</port>
+  <securePort enabled="false">443</securePort>
+  <countryId>1</countryId>
+  <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
+    <name>MyOwn</name>
+  </dataCenterInfo>
+  <leaseInfo>
+    <renewalIntervalInSecs>10</renewalIntervalInSecs>
+    <durationInSecs>30</durationInSecs>
+    <registrationTimestamp>1636639654493</registrationTimestamp>
+    <lastRenewalTimestamp>1636961178498</lastRenewalTimestamp>
+    <evictionTimestamp>0</evictionTimestamp>
+    <serviceUpTimestamp>1636639654493</serviceUpTimestamp>
+  </leaseInfo>
+  <metadata>
+    <management.port>8001</management.port>
+    <nodeId>127.0.0.1_kkg</nodeId>
+  </metadata>
+  <homePageUrl>http://127.0.0.1:8001/</homePageUrl>
+  <statusPageUrl>http://127.0.0.1:8001/actuator/info</statusPageUrl>
+  <healthCheckUrl>http://127.0.0.1:8001/actuator/health</healthCheckUrl>
+  <vipAddress>app-platform</vipAddress>
+  <secureVipAddress>app-platform</secureVipAddress>
+  <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
+  <lastUpdatedTimestamp>1636639654493</lastUpdatedTimestamp>
+  <lastDirtyTimestamp>1636639654480</lastDirtyTimestamp>
+  <actionType>ADDED</actionType>
+</instance>
<instance>
+  <instanceId>127.0.0.1:app-platform-2.0.0-SNAPSHOT:8001</instanceId>
+  <hostName>127.0.0.1</hostName>
+  <app>app-platform</app>
+  <ipAddr>127.0.0.1</ipAddr>
+  <status>UP</status>  # 状态为在线
+  <overriddenstatus>UNKNOWN</overriddenstatus> # 重写状态为空
+  <port enabled="true">8001</port>
+  <securePort enabled="false">443</securePort>
+  <countryId>1</countryId>
+  <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
+    <name>MyOwn</name>
+  </dataCenterInfo>
+  <leaseInfo>
+    <renewalIntervalInSecs>10</renewalIntervalInSecs>
+    <durationInSecs>30</durationInSecs>
+    <registrationTimestamp>1636639654493</registrationTimestamp>
+    <lastRenewalTimestamp>1636961178498</lastRenewalTimestamp>
+    <evictionTimestamp>0</evictionTimestamp>
+    <serviceUpTimestamp>1636639654493</serviceUpTimestamp>
+  </leaseInfo>
+  <metadata>
+    <management.port>8001</management.port>
+    <nodeId>127.0.0.1_kkg</nodeId>
+  </metadata>
+  <homePageUrl>http://127.0.0.1:8001/</homePageUrl>
+  <statusPageUrl>http://127.0.0.1:8001/actuator/info</statusPageUrl>
+  <healthCheckUrl>http://127.0.0.1:8001/actuator/health</healthCheckUrl>
+  <vipAddress>app-platform</vipAddress>
+  <secureVipAddress>app-platform</secureVipAddress>
+  <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
+  <lastUpdatedTimestamp>1636639654493</lastUpdatedTimestamp>
+  <lastDirtyTimestamp>1636639654480</lastDirtyTimestamp>
+  <actionType>ADDED</actionType>
+</instance>

通知注册中心服务下线

bash
curl -i -X PUT -u eureka_username:eureka_password http://eureka_ip:eureka_port/eureka/apps/服务名称/实例ip:实例名称:实例port/status?value=OUT_OF_SERVICE
curl -i -X PUT -u eureka_username:eureka_password http://eureka_ip:eureka_port/eureka/apps/服务名称/实例ip:实例名称:实例port/status?value=OUT_OF_SERVICE

例如:

bash
curl -i -X PUT admin:admin123 http://127.0.0.1:19991/eureka/apps/app-platform/127.0.0.1:app-platform-1.0.0-SNAPSHOT:8001/status?value=OUT_OF_SERVICE
curl -i -X PUT admin:admin123 http://127.0.0.1:19991/eureka/apps/app-platform/127.0.0.1:app-platform-1.0.0-SNAPSHOT:8001/status?value=OUT_OF_SERVICE

结果

bash
HTTP/1.1 200 # 请求成功
+X-Content-Type-Options: nosniff
+X-XSS-Protection: 1; mode=block
+Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+Pragma: no-cache
+Expires: 0
+X-Frame-Options: DENY
+Content-Type: application/xml
+Content-Length: 0
+Date: Mon, 15 Nov 2021 07:30:32 GMT
HTTP/1.1 200 # 请求成功
+X-Content-Type-Options: nosniff
+X-XSS-Protection: 1; mode=block
+Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+Pragma: no-cache
+Expires: 0
+X-Frame-Options: DENY
+Content-Type: application/xml
+Content-Length: 0
+Date: Mon, 15 Nov 2021 07:30:32 GMT

再次查询服务状态

xml
<instance>
+  <instanceId>127.0.0.1:app-platform-2.0.0-SNAPSHOT:8001</instanceId>
+  <hostName>127.0.0.1</hostName>
+  <app>app-platform</app>
+  <ipAddr>127.0.0.1</ipAddr>
+  <status>OUT_OF_SERVICE</status> # 服务状态-已下线
+  <overriddenstatus>OUT_OF_SERVICE</overriddenstatus> # 重写状态为已下线
+  <port enabled="true">8001</port>
+  <securePort enabled="false">443</securePort>
+  <countryId>1</countryId>
+  <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
+    <name>MyOwn</name>
+  </dataCenterInfo>
+  <leaseInfo>
+    <renewalIntervalInSecs>10</renewalIntervalInSecs>
+    <durationInSecs>30</durationInSecs>
+    <registrationTimestamp>1636639654493</registrationTimestamp>
+    <lastRenewalTimestamp>1636961468691</lastRenewalTimestamp>
+    <evictionTimestamp>0</evictionTimestamp>
+    <serviceUpTimestamp>1636639654493</serviceUpTimestamp>
+  </leaseInfo>
+  <metadata>
+    <management.port>8001</management.port>
+    <nodeId>127.0.0.1_kkg</nodeId>
+  </metadata>
+  <homePageUrl>http://127.0.0.1:8001/</homePageUrl>
+  <statusPageUrl>http://127.0.0.1:8001/actuator/info</statusPageUrl>
+  <healthCheckUrl>http://127.0.0.1:8001/actuator/health</healthCheckUrl>
+  <vipAddress>app-platform</vipAddress>
+  <secureVipAddress>app-platform</secureVipAddress>
+  <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
+  <lastUpdatedTimestamp>1636961432181</lastUpdatedTimestamp>
+  <lastDirtyTimestamp>1636639654480</lastDirtyTimestamp>
+  <actionType>MODIFIED</actionType>
+</instance>
<instance>
+  <instanceId>127.0.0.1:app-platform-2.0.0-SNAPSHOT:8001</instanceId>
+  <hostName>127.0.0.1</hostName>
+  <app>app-platform</app>
+  <ipAddr>127.0.0.1</ipAddr>
+  <status>OUT_OF_SERVICE</status> # 服务状态-已下线
+  <overriddenstatus>OUT_OF_SERVICE</overriddenstatus> # 重写状态为已下线
+  <port enabled="true">8001</port>
+  <securePort enabled="false">443</securePort>
+  <countryId>1</countryId>
+  <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
+    <name>MyOwn</name>
+  </dataCenterInfo>
+  <leaseInfo>
+    <renewalIntervalInSecs>10</renewalIntervalInSecs>
+    <durationInSecs>30</durationInSecs>
+    <registrationTimestamp>1636639654493</registrationTimestamp>
+    <lastRenewalTimestamp>1636961468691</lastRenewalTimestamp>
+    <evictionTimestamp>0</evictionTimestamp>
+    <serviceUpTimestamp>1636639654493</serviceUpTimestamp>
+  </leaseInfo>
+  <metadata>
+    <management.port>8001</management.port>
+    <nodeId>127.0.0.1_kkg</nodeId>
+  </metadata>
+  <homePageUrl>http://127.0.0.1:8001/</homePageUrl>
+  <statusPageUrl>http://127.0.0.1:8001/actuator/info</statusPageUrl>
+  <healthCheckUrl>http://127.0.0.1:8001/actuator/health</healthCheckUrl>
+  <vipAddress>app-platform</vipAddress>
+  <secureVipAddress>app-platform</secureVipAddress>
+  <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
+  <lastUpdatedTimestamp>1636961432181</lastUpdatedTimestamp>
+  <lastDirtyTimestamp>1636639654480</lastDirtyTimestamp>
+  <actionType>MODIFIED</actionType>
+</instance>

可以看到此时,服务状态为已经下线,不过还是要等到网关服务不再路由请求到该服务时再停掉服务。

通知注册中心服务上线

bash
curl -i -X DELETE -u eureka_username:eureka_password http://eureka_ip:eureka_port/eureka/apps/服务名称/实例ip:实例名称:实例port/status
curl -i -X DELETE -u eureka_username:eureka_password http://eureka_ip:eureka_port/eureka/apps/服务名称/实例ip:实例名称:实例port/status

例如:

bash
curl -i -X DELETE -u admin:admin123 http://127.0.0.1:19991/eureka/apps/app-platform/127.0.0.1:app-platform-1.0.0-SNAPSHOT:8001/status
curl -i -X DELETE -u admin:admin123 http://127.0.0.1:19991/eureka/apps/app-platform/127.0.0.1:app-platform-1.0.0-SNAPSHOT:8001/status

结果

bash
HTTP/1.1 200 # 请求成功
+X-Content-Type-Options: nosniff
+X-XSS-Protection: 1; mode=block
+Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+Pragma: no-cache
+Expires: 0
+X-Frame-Options: DENY
+Content-Type: application/xml
+Content-Length: 0
+Date: Mon, 15 Nov 2021 07:37:11 GMT
HTTP/1.1 200 # 请求成功
+X-Content-Type-Options: nosniff
+X-XSS-Protection: 1; mode=block
+Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+Pragma: no-cache
+Expires: 0
+X-Frame-Options: DENY
+Content-Type: application/xml
+Content-Length: 0
+Date: Mon, 15 Nov 2021 07:37:11 GMT

再次查询服务状态

xml
<instance>
+  <instanceId>127.0.0.1:app-platform-2.0.0-SNAPSHOT:8001</instanceId>
+  <hostName>127.0.0.1</hostName>
+  <app>app-platform</app>
+  <ipAddr>127.0.0.1</ipAddr>
+  <status>UP</status> # 此时服务已上线 如果通知后立刻查询,状态可能会是unknown,隔一段时间再查询即可
+  <overriddenstatus>UNKNOWN</overriddenstatus>
+  <port enabled="true">8001</port>
+  <securePort enabled="false">443</securePort>
+  <countryId>1</countryId>
+  <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
+    <name>MyOwn</name>
+  </dataCenterInfo>
+  <leaseInfo>
+    <renewalIntervalInSecs>10</renewalIntervalInSecs>
+    <durationInSecs>30</durationInSecs>
+    <registrationTimestamp>1636961828894</registrationTimestamp>
+    <lastRenewalTimestamp>1636961861799</lastRenewalTimestamp>
+    <evictionTimestamp>0</evictionTimestamp>
+    <serviceUpTimestamp>1636639654493</serviceUpTimestamp>
+  </leaseInfo>
+  <metadata>
+    <management.port>8001</management.port>
+    <nodeId>127.0.0.1_kkg</nodeId>
+  </metadata>
+  <homePageUrl>http://127.0.0.1:8001/</homePageUrl>
+  <statusPageUrl>http://127.0.0.1:8001/actuator/info</statusPageUrl>
+  <healthCheckUrl>http://127.0.0.1:8001/actuator/health</healthCheckUrl>
+  <vipAddress>app-platform</vipAddress>
+  <secureVipAddress>app-platform</secureVipAddress>
+  <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
+  <lastUpdatedTimestamp>1636961828894</lastUpdatedTimestamp>
+  <lastDirtyTimestamp>1636961828889</lastDirtyTimestamp>
+  <actionType>ADDED</actionType>
+</instance>
<instance>
+  <instanceId>127.0.0.1:app-platform-2.0.0-SNAPSHOT:8001</instanceId>
+  <hostName>127.0.0.1</hostName>
+  <app>app-platform</app>
+  <ipAddr>127.0.0.1</ipAddr>
+  <status>UP</status> # 此时服务已上线 如果通知后立刻查询,状态可能会是unknown,隔一段时间再查询即可
+  <overriddenstatus>UNKNOWN</overriddenstatus>
+  <port enabled="true">8001</port>
+  <securePort enabled="false">443</securePort>
+  <countryId>1</countryId>
+  <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
+    <name>MyOwn</name>
+  </dataCenterInfo>
+  <leaseInfo>
+    <renewalIntervalInSecs>10</renewalIntervalInSecs>
+    <durationInSecs>30</durationInSecs>
+    <registrationTimestamp>1636961828894</registrationTimestamp>
+    <lastRenewalTimestamp>1636961861799</lastRenewalTimestamp>
+    <evictionTimestamp>0</evictionTimestamp>
+    <serviceUpTimestamp>1636639654493</serviceUpTimestamp>
+  </leaseInfo>
+  <metadata>
+    <management.port>8001</management.port>
+    <nodeId>127.0.0.1_kkg</nodeId>
+  </metadata>
+  <homePageUrl>http://127.0.0.1:8001/</homePageUrl>
+  <statusPageUrl>http://127.0.0.1:8001/actuator/info</statusPageUrl>
+  <healthCheckUrl>http://127.0.0.1:8001/actuator/health</healthCheckUrl>
+  <vipAddress>app-platform</vipAddress>
+  <secureVipAddress>app-platform</secureVipAddress>
+  <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
+  <lastUpdatedTimestamp>1636961828894</lastUpdatedTimestamp>
+  <lastDirtyTimestamp>1636961828889</lastDirtyTimestamp>
+  <actionType>ADDED</actionType>
+</instance>
+ + + + \ No newline at end of file diff --git a/java/framework/swagger+knife4j.html b/java/framework/swagger+knife4j.html new file mode 100644 index 000000000..6ec951dfc --- /dev/null +++ b/java/framework/swagger+knife4j.html @@ -0,0 +1,401 @@ + + + + + + swagger+knife4j | 故事 + + + + + + + + + + + + + + +
Skip to content

swagger+knife4j

swagger2风格

POM

xml
<dependencies>
+	<dependency>
+    <groupId>io.springfox</groupId>
+    <artifactId>springfox-boot-starter</artifactId>
+    <version>3.0.0</version>
+	</dependency>
+	<dependency>
+    <groupId>com.github.xiaoymin</groupId>
+    <artifactId>knife4j-spring-boot-starter</artifactId>
+    <version>3.0.2</version>
+	</dependency>
+</dependencies>
<dependencies>
+	<dependency>
+    <groupId>io.springfox</groupId>
+    <artifactId>springfox-boot-starter</artifactId>
+    <version>3.0.0</version>
+	</dependency>
+	<dependency>
+    <groupId>com.github.xiaoymin</groupId>
+    <artifactId>knife4j-spring-boot-starter</artifactId>
+    <version>3.0.2</version>
+	</dependency>
+</dependencies>

swagger config

java
// swagger config
+@Configuration
+@EnableOpenApi
+@EnableKnife4j
+public class SwaggerConfig {
+
+    @Bean
+    public Docket createRestApi() {
+        // 返回文档摘要信息
+        return new Docket(DocumentationType.OAS_30)
+                .apiInfo(apiInfo())
+                .enable(true)
+                .select()
+                // .apis(RequestHandlerSelectors.withMethodAnnotation(Operation.class))
+                .apis(RequestHandlerSelectors.basePackage("com.storyxc"))
+                .paths(PathSelectors.any())
+                .build();
+                .globalRequestParameters(getGlobalRequestParameters())
+                .globalResponses(HttpMethod.GET, getGlobalResponseMessage())
+                .globalResponses(HttpMethod.POST, getGlobalResponseMessage());
+    }
+
+    /**
+     * 生成接口信息,包括标题、联系人等
+     */
+    private ApiInfo apiInfo() {
+        return new ApiInfoBuilder()
+                .title("接口文档")
+                .description("如有雷同,纯属故意")
+                .contact(new Contact("storyxc", "", "storyxc@163.com"))
+                .version("1.0")
+                .build();
+    }
+
+    /**
+     * 封装全局通用参数
+     */
+    private List<RequestParameter> getGlobalRequestParameters() {
+        List<RequestParameter> parameters = new ArrayList<>();
+        parameters.add(new RequestParameterBuilder()
+                .name("token")
+                .description("token")
+                .required(true)
+                .in(ParameterType.QUERY)
+                .query(q -> q.model(m -> m.scalarModel(ScalarType.STRING)))
+                .required(false)
+                .build());
+        return parameters;
+    }
+
+    /**
+     * 封装通用响应信息
+     */
+    private List<Response> getGlobalResponseMessage() {
+        List<Response> responseList = new ArrayList<>();
+        responseList.add(new ResponseBuilder().code("404").description("未找到资源").build());
+        return responseList;
+    }
+}
// swagger config
+@Configuration
+@EnableOpenApi
+@EnableKnife4j
+public class SwaggerConfig {
+
+    @Bean
+    public Docket createRestApi() {
+        // 返回文档摘要信息
+        return new Docket(DocumentationType.OAS_30)
+                .apiInfo(apiInfo())
+                .enable(true)
+                .select()
+                // .apis(RequestHandlerSelectors.withMethodAnnotation(Operation.class))
+                .apis(RequestHandlerSelectors.basePackage("com.storyxc"))
+                .paths(PathSelectors.any())
+                .build();
+                .globalRequestParameters(getGlobalRequestParameters())
+                .globalResponses(HttpMethod.GET, getGlobalResponseMessage())
+                .globalResponses(HttpMethod.POST, getGlobalResponseMessage());
+    }
+
+    /**
+     * 生成接口信息,包括标题、联系人等
+     */
+    private ApiInfo apiInfo() {
+        return new ApiInfoBuilder()
+                .title("接口文档")
+                .description("如有雷同,纯属故意")
+                .contact(new Contact("storyxc", "", "storyxc@163.com"))
+                .version("1.0")
+                .build();
+    }
+
+    /**
+     * 封装全局通用参数
+     */
+    private List<RequestParameter> getGlobalRequestParameters() {
+        List<RequestParameter> parameters = new ArrayList<>();
+        parameters.add(new RequestParameterBuilder()
+                .name("token")
+                .description("token")
+                .required(true)
+                .in(ParameterType.QUERY)
+                .query(q -> q.model(m -> m.scalarModel(ScalarType.STRING)))
+                .required(false)
+                .build());
+        return parameters;
+    }
+
+    /**
+     * 封装通用响应信息
+     */
+    private List<Response> getGlobalResponseMessage() {
+        List<Response> responseList = new ArrayList<>();
+        responseList.add(new ResponseBuilder().code("404").description("未找到资源").build());
+        return responseList;
+    }
+}

WebMvcConfig

java
@Configuration
+@RequiredArgsConstructor
+public class WebMvcConfiguration implements WebMvcConfigurer {
+    private final TokenInterceptor tokenInterceptor;
+
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**").allowedOriginPatterns("*").allowedMethods("*").allowCredentials(true).maxAge(3600);
+    }
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        String[] ignore = {
+                "/doc.html**",
+                "/webjars/js/**",
+                "/webjars/css/**",
+                "/swagger-ui/**",
+                "/swagger-resources/**",
+                "/v3/**"
+        };
+        registry.addInterceptor(tokenInterceptor).addPathPatterns("/**").excludePathPatterns(ignore);
+    }
+}
@Configuration
+@RequiredArgsConstructor
+public class WebMvcConfiguration implements WebMvcConfigurer {
+    private final TokenInterceptor tokenInterceptor;
+
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**").allowedOriginPatterns("*").allowedMethods("*").allowCredentials(true).maxAge(3600);
+    }
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        String[] ignore = {
+                "/doc.html**",
+                "/webjars/js/**",
+                "/webjars/css/**",
+                "/swagger-ui/**",
+                "/swagger-resources/**",
+                "/v3/**"
+        };
+        registry.addInterceptor(tokenInterceptor).addPathPatterns("/**").excludePathPatterns(ignore);
+    }
+}

swagger2注解

  • @Api:定义接口分组名称
  • @ApiImplicitParam: 单个参数注释
  • @ApiImplicitParams:多个参数注释
  • @ApiModel:实体类定义
  • @ApiModelProperty:实体属性定义
  • @ApiOperation:接口定义
  • @ApiParam:参数注释
  • @ApiResponse:响应码
  • @ApiResponses:多个响应码

knife4j+OpenApi3.0风格

POM

xml
<dependencies>    
+	<dependency>
+     <groupId>com.github.xiaoymin</groupId>
+     <artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
+     <version>4.1.0</version>
+  </dependency>
+</dependencies>
<dependencies>    
+	<dependency>
+     <groupId>com.github.xiaoymin</groupId>
+     <artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
+     <version>4.1.0</version>
+  </dependency>
+</dependencies>

swagger config

java
@Configuration
+public class Swagger3Config {
+    @Bean
+    public GlobalOpenApiCustomizer orderGlobalOpenApiCustomizer() {
+        return openApi -> {
+            Info info = openApi.getInfo();
+            // 可以覆写信息
+            info.title("API");
+            info.version("1.0");
+        };
+    }
+
+    @Bean
+    public GroupedOpenApi opcenterApi() {
+        String[] packagedToMatch = {"com.storyxc.controller.wallpaper"};
+        return GroupedOpenApi.builder().group("wallpaper")
+                // token header
+                .addOperationCustomizer(((operation, handlerMethod) -> operation.addParametersItem(
+                        new HeaderParameter()
+                                .name("token")
+                                .description("token")
+                                .required(true)
+                                .schema(new io.swagger.v3.oas.models.media.StringSchema())
+                                .allowEmptyValue(false)
+                )))
+                .packagesToScan(packagedToMatch).build();
+    }
+
+    @Bean
+    public GroupedOpenApi adminApi() {
+        return GroupedOpenApi.builder()
+                .group("admin")
+                .packagesToScan("com.storyxc.controller.admin")
+                .build();
+    }
+
+    @Bean
+    public OpenAPI customOpenAPI() {
+        return new OpenAPI()
+                .info(new Info()
+                        .title("story")
+                        .version("1.0")
+                        .description("API服务")
+                        .termsOfService("https://storyxc.com")
+                        .license(new License().name("GPLv3")
+                                .url("https://www.gnu.org/licenses/gpl-3.0.html"))
+                        .contact(new Contact().name("storyxc").email("storyxc@163.cn").url("https://storyxc.com"))
+                        .summary("API服务")
+                );
+    }
+}
@Configuration
+public class Swagger3Config {
+    @Bean
+    public GlobalOpenApiCustomizer orderGlobalOpenApiCustomizer() {
+        return openApi -> {
+            Info info = openApi.getInfo();
+            // 可以覆写信息
+            info.title("API");
+            info.version("1.0");
+        };
+    }
+
+    @Bean
+    public GroupedOpenApi opcenterApi() {
+        String[] packagedToMatch = {"com.storyxc.controller.wallpaper"};
+        return GroupedOpenApi.builder().group("wallpaper")
+                // token header
+                .addOperationCustomizer(((operation, handlerMethod) -> operation.addParametersItem(
+                        new HeaderParameter()
+                                .name("token")
+                                .description("token")
+                                .required(true)
+                                .schema(new io.swagger.v3.oas.models.media.StringSchema())
+                                .allowEmptyValue(false)
+                )))
+                .packagesToScan(packagedToMatch).build();
+    }
+
+    @Bean
+    public GroupedOpenApi adminApi() {
+        return GroupedOpenApi.builder()
+                .group("admin")
+                .packagesToScan("com.storyxc.controller.admin")
+                .build();
+    }
+
+    @Bean
+    public OpenAPI customOpenAPI() {
+        return new OpenAPI()
+                .info(new Info()
+                        .title("story")
+                        .version("1.0")
+                        .description("API服务")
+                        .termsOfService("https://storyxc.com")
+                        .license(new License().name("GPLv3")
+                                .url("https://www.gnu.org/licenses/gpl-3.0.html"))
+                        .contact(new Contact().name("storyxc").email("storyxc@163.cn").url("https://storyxc.com"))
+                        .summary("API服务")
+                );
+    }
+}

WebMvcConfig

java
@Configuration
+@RequiredArgsConstructor
+public class WebMvcConfiguration implements WebMvcConfigurer {
+    private final TokenInterceptor tokenInterceptor;
+
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**")
+          .allowedOriginPatterns("*")
+          .allowedMethods("*")
+          .allowCredentials(true)
+          .maxAge(3600);
+    }
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        String[] ignore = {
+                "/doc.html**",
+                "/webjars/js/**",
+                "/webjars/css/**",
+                "/swagger-ui/**",
+                "/swagger-resources/**",
+                "/v3/**"
+        };
+        registry.addInterceptor(tokenInterceptor).addPathPatterns("/**").excludePathPatterns(ignore);
+    }
+}
@Configuration
+@RequiredArgsConstructor
+public class WebMvcConfiguration implements WebMvcConfigurer {
+    private final TokenInterceptor tokenInterceptor;
+
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**")
+          .allowedOriginPatterns("*")
+          .allowedMethods("*")
+          .allowCredentials(true)
+          .maxAge(3600);
+    }
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        String[] ignore = {
+                "/doc.html**",
+                "/webjars/js/**",
+                "/webjars/css/**",
+                "/swagger-ui/**",
+                "/swagger-resources/**",
+                "/v3/**"
+        };
+        registry.addInterceptor(tokenInterceptor).addPathPatterns("/**").excludePathPatterns(ignore);
+    }
+}

application.yaml

yml
springdoc:
+  api-docs:
+    enabled: true
+
+knife4j:
+  enable: true #增强模式
+  production: false #是否生产,生产会关闭knife4j 需要开启增强模式生效
+  setting:
+    swagger-model-name: 模型
+  documents:
+    - name: 项目文档
+      locations: classpath:doc/*
+      group: wallpaper
+    - name: sql
+      locations: classpath:sql/*
+      group: sql
springdoc:
+  api-docs:
+    enabled: true
+
+knife4j:
+  enable: true #增强模式
+  production: false #是否生产,生产会关闭knife4j 需要开启增强模式生效
+  setting:
+    swagger-model-name: 模型
+  documents:
+    - name: 项目文档
+      locations: classpath:doc/*
+      group: wallpaper
+    - name: sql
+      locations: classpath:sql/*
+      group: sql

OpenApi3注解

Swagger3注解说明
@Tag(name = “接口类描述”)Controller 类
@Operation(summary =“接口方法描述”)Controller 方法
@ParametersController 方法
@Parameter(description=“参数描述”)Controller 方法上 @Parameters 里Controller 方法的参数
@Parameter(hidden = true) 、@Operation(hidden = true)@Hidden排除或隐藏api
@SchemaDTO实体DTO实体属性
+ + + + \ No newline at end of file diff --git "a/java/framework/\344\275\277\347\224\250EasyExcel\345\257\274\345\207\272excel.html" "b/java/framework/\344\275\277\347\224\250EasyExcel\345\257\274\345\207\272excel.html" new file mode 100644 index 000000000..35b52e806 --- /dev/null +++ "b/java/framework/\344\275\277\347\224\250EasyExcel\345\257\274\345\207\272excel.html" @@ -0,0 +1,99 @@ + + + + + + 使用EasyExcel导出excel | 故事 + + + + + + + + + + + + + + +
Skip to content

使用EasyExcel导出excel

背景

本来一直在用easypoi,但是发现多sheet导出easyexcel的支持更好一点,遂切换

依赖

xml
<dependency>
+	<groupId>com.alibaba</groupId>
+	<artifactId>easyexcel</artifactId>
+	<version>2.2.7</version>
+</dependency>
<dependency>
+	<groupId>com.alibaba</groupId>
+	<artifactId>easyexcel</artifactId>
+	<version>2.2.7</version>
+</dependency>

导出多sheet

java
public void export(X param, HttpServletResponse response) {
+        String templatePath = "xxx"
+        OutputStream fos = null;
+        ExcelWriter excelWriter;
+        try {
+            fos = response.getOutputStream();
+            response.setContentType("application/vnd.ms-excel");
+            SimpleDateFormat yyyyMMdd = new SimpleDateFormat("yyyyMMddHHmmss");
+            String date = yyyyMMdd.format(new Date());
+            String fileName = "xxx" + date;
+            fileName = new String(fileName.getBytes(StandardCharsets.UTF_8), "ISO8859-1");
+            response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xls");
+
+            excelWriter = EasyExcel.write(fos).withTemplate(templatePath).build();
+            WriteSheet sheet0 = EasyExcel.writerSheet(0, "xxx").build();
+            WriteSheet sheet1 = EasyExcel.writerSheet(1, "xxx").build();
+
+            FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.FALSE).build();
+            //遍历:模版文件 {t.属性}  
+            excelWriter.fill(new FillWrapper("t", xxx), fillConfig, sheet0);//.fill(exportDate, writeSheet0); 填充单独属性
+            excelWriter.fill(new FillWrapper("t", xxx), fillConfig, sheet1);
+            excelWriter.finish();
+        } catch (Exception e) {
+            log.info("xxx");
+        } finally {
+            if (fos != null) {
+                try {
+                    fos.close();
+                } catch (IOException e) {
+                    log.error("xxxx");
+                }
+            }
+        }
+    }
public void export(X param, HttpServletResponse response) {
+        String templatePath = "xxx"
+        OutputStream fos = null;
+        ExcelWriter excelWriter;
+        try {
+            fos = response.getOutputStream();
+            response.setContentType("application/vnd.ms-excel");
+            SimpleDateFormat yyyyMMdd = new SimpleDateFormat("yyyyMMddHHmmss");
+            String date = yyyyMMdd.format(new Date());
+            String fileName = "xxx" + date;
+            fileName = new String(fileName.getBytes(StandardCharsets.UTF_8), "ISO8859-1");
+            response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xls");
+
+            excelWriter = EasyExcel.write(fos).withTemplate(templatePath).build();
+            WriteSheet sheet0 = EasyExcel.writerSheet(0, "xxx").build();
+            WriteSheet sheet1 = EasyExcel.writerSheet(1, "xxx").build();
+
+            FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.FALSE).build();
+            //遍历:模版文件 {t.属性}  
+            excelWriter.fill(new FillWrapper("t", xxx), fillConfig, sheet0);//.fill(exportDate, writeSheet0); 填充单独属性
+            excelWriter.fill(new FillWrapper("t", xxx), fillConfig, sheet1);
+            excelWriter.finish();
+        } catch (Exception e) {
+            log.info("xxx");
+        } finally {
+            if (fos != null) {
+                try {
+                    fos.close();
+                } catch (IOException e) {
+                    log.error("xxxx");
+                }
+            }
+        }
+    }
+ + + + \ No newline at end of file diff --git "a/java/framework/\344\275\277\347\224\250spring validation\350\277\233\350\241\214\345\217\202\346\225\260\346\240\241\351\252\214.html" "b/java/framework/\344\275\277\347\224\250spring validation\350\277\233\350\241\214\345\217\202\346\225\260\346\240\241\351\252\214.html" new file mode 100644 index 000000000..7b9f7e8ed --- /dev/null +++ "b/java/framework/\344\275\277\347\224\250spring validation\350\277\233\350\241\214\345\217\202\346\225\260\346\240\241\351\252\214.html" @@ -0,0 +1,201 @@ + + + + + + 使用spring validation进行参数校验 | 故事 + + + + + + + + + + + + + + +
Skip to content

使用spring validation进行参数校验

后台的参数校验如果全写在业务代码里会导致代码很臃肿,此时可以引入Spring Validation来进行参数校验,还可以自定义校验规则,自定义参数校验异常等

简单使用

  1. 项目引入spring-boot-starter-web依赖
xml
<dependency>
+    <groupId>org.hibernate</groupId>
+    <artifactId>hibernate-validator</artifactId>
+</dependency>
+<dependency>
+    <groupId>com.fasterxml.jackson.core</groupId>
+    <artifactId>jackson-databind</artifactId>
+</dependency>
<dependency>
+    <groupId>org.hibernate</groupId>
+    <artifactId>hibernate-validator</artifactId>
+</dependency>
+<dependency>
+    <groupId>com.fasterxml.jackson.core</groupId>
+    <artifactId>jackson-databind</artifactId>
+</dependency>
  1. 在需要校验的对象前,加上@Validated注解,在对象字段上使用具体校验规则的注解即可。如果是嵌套对象,则在嵌套的对象上使用@Valid注解表明需要嵌套校验

    java
    public String receive(@RequestBody @Validated StatementDto dto, BindingResult result) {
    +		if (result.hasErrors()) {
    +			throw new ParameterNotValidException(result);
    +		}
    +		return "test";
    +	}
    public String receive(@RequestBody @Validated StatementDto dto, BindingResult result) {
    +		if (result.hasErrors()) {
    +			throw new ParameterNotValidException(result);
    +		}
    +		return "test";
    +	}
    java
    public class QchjcCustomerStatementDto implements Serializable {
    +
    +	@NotEmpty(message = "不能为空")
    +	@Valid
    +	private List<OrderDto> order;
    +
    +	@DecimalMin(value = "0",message = "金额需大于等于0.00")
    +	private BigDecimal amount;
    +}
    public class QchjcCustomerStatementDto implements Serializable {
    +
    +	@NotEmpty(message = "不能为空")
    +	@Valid
    +	private List<OrderDto> order;
    +
    +	@DecimalMin(value = "0",message = "金额需大于等于0.00")
    +	private BigDecimal amount;
    +}

    TIP

    @Validated注解标记的参数会被spring进行校验,校验的信息会存放到其后的BindingResult中,如果有多个参数需要校验可以采用如下形式:(@Validated Person person, BindingResult fooBindingResult ,@Validated Bar bar, BindingResult barBindingResult);即一个校验类对应一个校验结果。

常用校验

  1. JSR303/JSR-349: JSR303是一项标准,只提供规范不提供实现,规定一些校验规范即校验注解,如@Null,@NotNull,@Pattern,位于javax.validation.constraints包下。JSR-349是其的升级版本,添加了一些新特性。

    1. @Null 被注释的元素必须为null

    2. @NotNull 被注释的元素必须不为null

    3. @AssertTrue 被注释的元素必须为true

    4. @AssertFalse 被注释的元素必须为false

    5. @Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值

    6. @Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值

    7. @DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定

      最小值

    8. @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定

      最大值

    9. @Size(max, min) 被注释的元素的大小必须在指定的范围内

    10. @Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内

    11. @Past 被注释的元素必须是一个过去的日期

    12. @Future 被注释的元素必须是一个将来的日期

    13. @Pattern(value) 被注释的元素必须符合指定的正则表达式

  2. hibernate validation:hibernate validation是对这个规范的实现,并增加了一些其他校验注解,如@Email,@Length,@Range等等

    1. @Email 被注释的元素必须是电子邮箱地址
    2. @Length 被注释的字符串的大小必须在指定的范围内
    3. @NotEmpty 被注释的字符串的必须非空
    4. @Range 被注释的元素必须在合适的范围内
  3. spring validation:spring validation对hibernate validation进行了二次封装,在springmvc模块中添加了自动校验,并将校验信息封装进了特定的类中

统一异常处理

如果方法参数中不声明BindingResult,那么spring校验不通过后会直接抛出BindException,体验很不好。因此我们可以进行自定义异常处理。

  • 自定义异常
java
public class ParameterNotValidException extends RuntimeException{
+    private static final long serialVersionUID = 1L;
+    private BindingResult bindingResult;
+
+    public ParameterNotValidException(BindingResult bindingResult) {
+        this.bindingResult = bindingResult;
+    }
+
+    public BindingResult getBindingResult() {
+        return bindingResult;
+    }
+
+    @Override
+    public String getMessage() {
+        return "参数校验不通过";
+    }
+}
public class ParameterNotValidException extends RuntimeException{
+    private static final long serialVersionUID = 1L;
+    private BindingResult bindingResult;
+
+    public ParameterNotValidException(BindingResult bindingResult) {
+        this.bindingResult = bindingResult;
+    }
+
+    public BindingResult getBindingResult() {
+        return bindingResult;
+    }
+
+    @Override
+    public String getMessage() {
+        return "参数校验不通过";
+    }
+}
  • 统一异常处理
java
@ControllerAdvice
+public class ExceptionHandler {
+    /**
+     * 方法参数校验异常处理
+     */
+    @ExceptionHandler(ParameterNotValidException.class)
+    @ResponseBody
+    public O handleMethodArgumentNotValidException(ParameterNotValidException e) {
+        BindingResult bindingResult = e.getBindingResult();
+        FieldError fieldError = bindingResult.getFieldError();
+        String field = fieldError.getField();
+        String defaultMessage = fieldError.getDefaultMessage();
+        O o = new O();
+        o.setResultCode(500);
+        o.setSuccess(false);
+        o.setResultMessage(field + ":" + defaultMessage);
+        return o;
+    }
+}
@ControllerAdvice
+public class ExceptionHandler {
+    /**
+     * 方法参数校验异常处理
+     */
+    @ExceptionHandler(ParameterNotValidException.class)
+    @ResponseBody
+    public O handleMethodArgumentNotValidException(ParameterNotValidException e) {
+        BindingResult bindingResult = e.getBindingResult();
+        FieldError fieldError = bindingResult.getFieldError();
+        String field = fieldError.getField();
+        String defaultMessage = fieldError.getDefaultMessage();
+        O o = new O();
+        o.setResultCode(500);
+        o.setSuccess(false);
+        o.setResultMessage(field + ":" + defaultMessage);
+        return o;
+    }
+}

自定义校验规则

例如,我们需要新增一个校验规则,数字类型必须大于0

  • 新建校验注解

    java
    @Target(ElementType.FIELD)
    +@Retention(RetentionPolicy.RUNTIME)
    +@Constraint(validatedBy = GreaterThanZeroValidator.class)//标注校验由哪些类执行,可以是多个
    +public @interface GreaterThanZero {
    +    String message();
    +
    +    Class<?>[] groups() default {};//在不同接口中参数可能校验的规则不同,可以创建不同的组,并在校验规则标记组,在controller层也标记组,这样就可以不同接口实现不同校验规则
    +
    +    Class<?>[] payload() default {};
    +}
    @Target(ElementType.FIELD)
    +@Retention(RetentionPolicy.RUNTIME)
    +@Constraint(validatedBy = GreaterThanZeroValidator.class)//标注校验由哪些类执行,可以是多个
    +public @interface GreaterThanZero {
    +    String message();
    +
    +    Class<?>[] groups() default {};//在不同接口中参数可能校验的规则不同,可以创建不同的组,并在校验规则标记组,在controller层也标记组,这样就可以不同接口实现不同校验规则
    +
    +    Class<?>[] payload() default {};
    +}

    一个标注(annotation) 是通过@interface关键字来定义的. 这个标注中的属性是声明成类似方法的样式的. 根据Bean Validation API 规范的要求

    • message属性, 这个属性被用来定义默认得消息模版, 当这个约束条件被验证失败的时候,通过此属性来输出错误信息.

    groups 属性, 用于指定这个约束条件属于哪(些)个校验组.这个的默认值必须是Class<?>类型到空到数组.

    • payload 属性, Bean Validation API 的使用者可以通过此属性来给约束条件指定严重级别. 这个属性并不被API自身所使用.

      java
      public class Severity {
      +    public static class Info extends Payload {};
      +    public static class Error extends Payload {};
      +}
      +
      +public class ContactDetails {
      +    @NotNull(message="Name is mandatory", payload=Severity.Error.class)
      +    private String name;
      +
      +    @NotNull(message="Phone number not specified, but not mandatory", payload=Severity.Info.class)
      +    private String phoneNumber;
      +
      +    // ...
      +}
      public class Severity {
      +    public static class Info extends Payload {};
      +    public static class Error extends Payload {};
      +}
      +
      +public class ContactDetails {
      +    @NotNull(message="Name is mandatory", payload=Severity.Error.class)
      +    private String name;
      +
      +    @NotNull(message="Phone number not specified, but not mandatory", payload=Severity.Info.class)
      +    private String phoneNumber;
      +
      +    // ...
      +}

      这样, 在校验完一个ContactDetails 的示例之后, 你就可以通过调用ConstraintViolation.getConstraintDescriptor().getPayload()来得到之前指定到错误级别了,并且可以根据这个信息来决定接下来到行为.

  • 校验规则类

    java
    public class GreaterThanZeroValidator implements ConstraintValidator<GreaterThanZero, Object> {
    +    @Override
    +    public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
    +        if (o == null) {
    +            return false;
    +        }
    +        if (o instanceof Number) {
    +            return ((Number) o).intValue() > 0;
    +        }
    +
    +        return false;
    +    }
    +}
    public class GreaterThanZeroValidator implements ConstraintValidator<GreaterThanZero, Object> {
    +    @Override
    +    public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
    +        if (o == null) {
    +            return false;
    +        }
    +        if (o instanceof Number) {
    +            return ((Number) o).intValue() > 0;
    +        }
    +
    +        return false;
    +    }
    +}
    • ConstraintValidator定义了两个泛型参数, 第一个是这个校验器所服务到标注类型, 第二个这个校验器所支持到被校验元素到类型.如果一个约束标注支持多种类型到被校验元素的话, 那么需要为每个所支持的类型定义一个ConstraintValidator,并且注册到约束标注中.这个验证器的实现就很平常了
    • initialize() 方法传进来一个所要验证的标注类型的实例
    • isValid()是实现真正的校验逻辑的地方
+ + + + \ No newline at end of file diff --git "a/java/framework/\345\210\206\345\270\203\345\274\217\345\256\232\346\227\266\344\273\273\345\212\241\350\247\243\345\206\263\346\226\271\346\241\210xxl-job.html" "b/java/framework/\345\210\206\345\270\203\345\274\217\345\256\232\346\227\266\344\273\273\345\212\241\350\247\243\345\206\263\346\226\271\346\241\210xxl-job.html" new file mode 100644 index 000000000..b57f05816 --- /dev/null +++ "b/java/framework/\345\210\206\345\270\203\345\274\217\345\256\232\346\227\266\344\273\273\345\212\241\350\247\243\345\206\263\346\226\271\346\241\210xxl-job.html" @@ -0,0 +1,25 @@ + + + + + + 分布式定时任务解决方案xxl-job | 故事 + + + + + + + + + + + + + + +
Skip to content

分布式定时任务解决方案xxl-job

背景

今天领导让我了解下分布式定时任务的内容,对项目中目前的定时任务改造一下,公司目前项目中封装的定时任务注解是基于spring的scheduler的,单机环境下没问题,但是为了服务的高可用生产都是集群部署的,会导致任务多次运行的问题,上家公司用的ssm,定时任务选型是Quartz,生产上采用的quartz的集群部署,quartz集群是不会出现任务重复执行的,原理跟下文讲到的xxl-job一样都是通过数据库表来加锁实现

xxl-job

Xxl-job官网:https://www.xuxueli.com/xxl-job/

XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。

选择xxl-job的原因有以下几点:

  • 调度和任务解耦,有单独的调度中心及控制台
  • 代码易扩展
  • 实现的功能比较全面

使用流程:

  • 执行doc中的sql脚本,建相关的库表,具体表的作用可阅读帮助文档

  • 修改xxl-job-admin配置文件后可以直接启动admin应用

  • 修改执行器应用中配置文件,注意匹配admin的相关信息

  • copy一份示例执行器的代码 修改应用端口和执行端口

  • 启动两个示例执行器应用(模拟集群)

  • 打开控制台可以看到已经注册上了两个节点 Snipaste_20210318_201851.png

  • 在控制台任务管理中修改任务的相关信息,这里只试个最简单的,更多高级配置请看使用文档

Snipaste_20210318_202127.png

Snipaste_20210318_202218.png

Snipaste_20210318_202226.png

可以在控制台的调度记录中看到,集群模式下任务也只执行了一次.

除了cron任务,xxl-job中还支持周期性的任务,shell任务等.路由策略还支持轮询,一致性哈希,故障转移等,在admin中配置好java.mail信息,在控制配置任务时添加告警邮件,在任务调度出现异常时还会邮件告警

集成到微服务系统的思路:

  • 数据库建表
  • 新建一个job-admin服务将xxl-job-admin迁移,修改配置信息
  • 在需要定时任务的业务服务中引入xxl-core依赖
  • 在任务类中实现业务逻辑,任务方法上加上@XxlJob(value = "xxHandler")
  • admin服务启动后启动具体的业务服务
  • 在调度中心新建任务,配置相关信息,注意JobHandler的值为@XxlJob注解的Value
+ + + + \ No newline at end of file diff --git "a/java/framework/\345\220\216\347\253\257\345\205\201\350\256\270\350\267\250\345\237\237\351\205\215\347\275\256.html" "b/java/framework/\345\220\216\347\253\257\345\205\201\350\256\270\350\267\250\345\237\237\351\205\215\347\275\256.html" new file mode 100644 index 000000000..ffd483e97 --- /dev/null +++ "b/java/framework/\345\220\216\347\253\257\345\205\201\350\256\270\350\267\250\345\237\237\351\205\215\347\275\256.html" @@ -0,0 +1,79 @@ + + + + + + 后端允许跨域配置 | 故事 + + + + + + + + + + + + + + +
Skip to content

后端允许跨域配置

过滤器方案

java
@Configuration
+public class CorsFilterConfig {
+    @Bean
+    public CorsFilter corsFilter() {
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        CorsConfiguration corsConfiguration = new CorsConfiguration();
+        corsConfiguration.setAllowCredentials(true);
+        corsConfiguration.addAllowedOriginPattern("*");
+        corsConfiguration.addAllowedHeader("*");
+        corsConfiguration.addAllowedMethod("*");
+        corsConfiguration.setMaxAge(10000L);
+        source.registerCorsConfiguration("/**", corsConfiguration);
+        return new CorsFilter(source);
+    }
+
+}
@Configuration
+public class CorsFilterConfig {
+    @Bean
+    public CorsFilter corsFilter() {
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        CorsConfiguration corsConfiguration = new CorsConfiguration();
+        corsConfiguration.setAllowCredentials(true);
+        corsConfiguration.addAllowedOriginPattern("*");
+        corsConfiguration.addAllowedHeader("*");
+        corsConfiguration.addAllowedMethod("*");
+        corsConfiguration.setMaxAge(10000L);
+        source.registerCorsConfiguration("/**", corsConfiguration);
+        return new CorsFilter(source);
+    }
+
+}

Spring拦截器方案

java
@Configuration
+@RequiredArgsConstructor
+public class WebMvcConfiguration implements WebMvcConfigurer {
+  @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**")
+                .allowedOriginPatterns("*")
+                .allowedMethods("*")
+                .allowCredentials(true)
+                .allowedHeaders("*")
+                .maxAge(3600);
+    } 
+}
@Configuration
+@RequiredArgsConstructor
+public class WebMvcConfiguration implements WebMvcConfigurer {
+  @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**")
+                .allowedOriginPatterns("*")
+                .allowedMethods("*")
+                .allowCredentials(true)
+                .allowedHeaders("*")
+                .maxAge(3600);
+    } 
+}
+ + + + \ No newline at end of file diff --git "a/java/framework/\350\207\252\345\256\232\344\271\211MybatisPlusGenerator.html" "b/java/framework/\350\207\252\345\256\232\344\271\211MybatisPlusGenerator.html" new file mode 100644 index 000000000..1f1ae032c --- /dev/null +++ "b/java/framework/\350\207\252\345\256\232\344\271\211MybatisPlusGenerator.html" @@ -0,0 +1,837 @@ + + + + + + 自定义MybatisPlusGenerator | 故事 + + + + + + + + + + + + + + +
Skip to content

自定义MybatisPlusGenerator

入口类

java
import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.core.toolkit.StringPool;
+import com.baomidou.mybatisplus.generator.AutoGenerator;
+import com.baomidou.mybatisplus.generator.InjectionConfig;
+import com.baomidou.mybatisplus.generator.config.*;
+import com.baomidou.mybatisplus.generator.config.po.TableInfo;
+import com.baomidou.mybatisplus.generator.config.rules.DateType;
+import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
+import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author xc
+ * @description
+ * @date 2023/5/19 09:20
+ */
+public class MybatisPlusCodeGenerator {
+
+    private static final String projectPath = System.getProperty("user.dir");
+
+    public static void main(String[] args) {
+        //====================配置变量区域=====================//
+        String author = "storyxc";// 生成文件的作者,可以不填
+        String rootPackage = "com.storyxc";// 生成的entity、controller、service等包所在的公共上一级包路径全限定名
+        String modelModuleName = "storyxc-model";
+        String serviceModuleName = "storyxc-web";
+        String controllerModuleName = "storyxc-web";
+        // 数据库配置
+        String url="jdbc:mysql://127.0.0.1/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false";
+        String driverClassName = "com.mysql.cj.jdbc.Driver";// 或者com.mysql.cj.jdbc.Driver
+        String username = "root";
+        String password = "root";
+
+        String[] tableNames = new String[]{""};
+        String pkgName = "";
+        //====================配置变量区域=====================//
+        String[] tablePrefix = new String[]{""};
+        // 代码生成器
+        AutoGenerator generator = new AutoGenerator();
+        // 全局配置
+        GlobalConfig globalConfig = new GlobalConfig();
+        globalConfig.setOutputDir(projectPath + "/" + modelModuleName + "/src/main/java");// 生成文件的输出目录
+        globalConfig.setFileOverride(false);// 是否覆盖已有文件,默认false
+        globalConfig.setOpen(false);// 是否打开输出目录
+        globalConfig.setAuthor(author);
+        globalConfig.setServiceName("%sService");// 去掉service接口的首字母I
+        globalConfig.setBaseResultMap(true);// 开启 BaseResultMap
+        globalConfig.setDateType(DateType.ONLY_DATE);// 只使用 java.util.date代替
+        globalConfig.setIdType(IdType.ASSIGN_ID);// 分配ID (主键类型为number或string)
+        generator.setGlobalConfig(globalConfig);
+
+        // 数据源配置
+        DataSourceConfig dataSourceConfig = new DataSourceConfig();
+        dataSourceConfig.setUrl(url);
+        dataSourceConfig.setDbType(DbType.MYSQL);// 数据库类型
+        dataSourceConfig.setDriverName(driverClassName);
+        dataSourceConfig.setUsername(username);
+        dataSourceConfig.setPassword(password);
+        generator.setDataSource(dataSourceConfig);
+
+        // 包配置
+        PackageConfig packageConfig = new PackageConfig();
+        //packageConfig.setModuleName(scanner("模块名"));
+        packageConfig.setParent(rootPackage);
+        packageConfig.setController("controller" + (StrUtil.isNotBlank(pkgName) ? "." + pkgName : ""));
+        packageConfig.setService("service" + (StrUtil.isNotBlank(pkgName) ? "." + pkgName : ""));
+        packageConfig.setServiceImpl("service" + (StrUtil.isNotBlank(pkgName) ? "." + pkgName + ".impl" : ".impl"));
+        packageConfig.setEntity("dao.entity" + (StrUtil.isNotBlank(pkgName) ? "." + pkgName : ""));
+        packageConfig.setMapper("dao.mapper" + (StrUtil.isNotBlank(pkgName) ? "." + pkgName : ""));
+
+        //packageConfig.setXml("dao.mapper.xml");
+        generator.setPackageInfo(packageConfig);
+
+        // 注意:模板引擎在mybatisplus依赖中的templates目录下,可以依照此默认模板进行自定义
+
+        // 策略配置:配置根据哪张表生成代码
+        StrategyConfig strategy = new StrategyConfig();
+        strategy.setInclude(tableNames);// 表名,多个英文逗号分割(与exclude二选一配置)
+        strategy.setNaming(NamingStrategy.underline_to_camel);
+        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
+        // strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!");
+        strategy.setEntityLombokModel(true);// lombok模型,@Accessors(chain = true)setter链式操作
+        strategy.setRestControllerStyle(true);// controller生成@RestController
+        strategy.setEntityTableFieldAnnotationEnable(true);// 是否生成实体时,生成字段注解
+        // strategy.setEntityColumnConstant(true);// 是否生成字段常量(默认 false)
+        strategy.setTablePrefix(tablePrefix);// 生成实体时去掉表前缀
+
+        TemplateConfig templateConfig = new TemplateConfig();
+        templateConfig.setController(null);
+        templateConfig.setService(null);
+        templateConfig.setServiceImpl(null);
+        templateConfig.setXml(null);
+        templateConfig.setMapper(null);
+        templateConfig.setEntity(null);
+        generator.setTemplate(templateConfig);
+
+
+        generator.setStrategy(strategy);
+        generator.setTemplateEngine(new FreemarkerTemplateEngine());
+
+        /**
+         * 自定义输出路径
+         */
+        // controller
+        List<FileOutConfig> focList = new ArrayList<>();
+
+        // mapper.xml
+        focList.add(new FileOutConfig("/templates/story-entity.java.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
+                return projectPath + "/" + modelModuleName + "/src/main/java/" + rootPackage.replace(".", "/") + "/dao/entity/" + pkgName + "/" + tableInfo.getEntityName() + StringPool.DOT_JAVA;
+            }
+        });
+
+        focList.add(new FileOutConfig("/templates/story-controller.java.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                return projectPath + "/" + controllerModuleName + "/src/main/java/" + rootPackage.replace(".", "/") + "/controller/" + pkgName + "/" + tableInfo.getEntityName() + "Controller" + StringPool.DOT_JAVA;
+            }
+        });
+        // service
+        focList.add(new FileOutConfig("/templates/service.java.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                return projectPath + "/" + serviceModuleName + "/src/main/java/" + rootPackage.replace(".", "/") + "/service/" + pkgName + "/" +  tableInfo.getEntityName() + "Service" + StringPool.DOT_JAVA;
+            }
+        });
+
+        // serviceImpl
+        focList.add(new FileOutConfig("/templates/story-serviceImpl.java.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                return projectPath + "/" + serviceModuleName + "/src/main/java/" + rootPackage.replace(".", "/") + "/service/" + pkgName + "/impl/" + tableInfo.getEntityName() + "ServiceImpl" + StringPool.DOT_JAVA;
+            }
+        });
+
+        // mapper.java
+        // service
+        focList.add(new FileOutConfig("/templates/story-mapper.java.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                return projectPath + "/" + modelModuleName + "/src/main/java/" + rootPackage.replace(".", "/") + "/dao/mapper/" + pkgName + "/" +  tableInfo.getEntityName() + "Mapper" + StringPool.DOT_JAVA;
+            }
+        });
+
+        // mapper.xml
+        focList.add(new FileOutConfig("/templates/mapper.xml.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
+                return projectPath + "/" + modelModuleName + "/src/main/resources/mapper/" + pkgName + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
+            }
+        });
+
+        InjectionConfig injectionConfig = new InjectionConfig() {
+            @Override
+            public void initMap() { }
+        };
+        injectionConfig.setFileOutConfigList(focList);
+
+        generator.setCfg(injectionConfig);
+        generator.execute();
+    }
+}
import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.core.toolkit.StringPool;
+import com.baomidou.mybatisplus.generator.AutoGenerator;
+import com.baomidou.mybatisplus.generator.InjectionConfig;
+import com.baomidou.mybatisplus.generator.config.*;
+import com.baomidou.mybatisplus.generator.config.po.TableInfo;
+import com.baomidou.mybatisplus.generator.config.rules.DateType;
+import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
+import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author xc
+ * @description
+ * @date 2023/5/19 09:20
+ */
+public class MybatisPlusCodeGenerator {
+
+    private static final String projectPath = System.getProperty("user.dir");
+
+    public static void main(String[] args) {
+        //====================配置变量区域=====================//
+        String author = "storyxc";// 生成文件的作者,可以不填
+        String rootPackage = "com.storyxc";// 生成的entity、controller、service等包所在的公共上一级包路径全限定名
+        String modelModuleName = "storyxc-model";
+        String serviceModuleName = "storyxc-web";
+        String controllerModuleName = "storyxc-web";
+        // 数据库配置
+        String url="jdbc:mysql://127.0.0.1/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false";
+        String driverClassName = "com.mysql.cj.jdbc.Driver";// 或者com.mysql.cj.jdbc.Driver
+        String username = "root";
+        String password = "root";
+
+        String[] tableNames = new String[]{""};
+        String pkgName = "";
+        //====================配置变量区域=====================//
+        String[] tablePrefix = new String[]{""};
+        // 代码生成器
+        AutoGenerator generator = new AutoGenerator();
+        // 全局配置
+        GlobalConfig globalConfig = new GlobalConfig();
+        globalConfig.setOutputDir(projectPath + "/" + modelModuleName + "/src/main/java");// 生成文件的输出目录
+        globalConfig.setFileOverride(false);// 是否覆盖已有文件,默认false
+        globalConfig.setOpen(false);// 是否打开输出目录
+        globalConfig.setAuthor(author);
+        globalConfig.setServiceName("%sService");// 去掉service接口的首字母I
+        globalConfig.setBaseResultMap(true);// 开启 BaseResultMap
+        globalConfig.setDateType(DateType.ONLY_DATE);// 只使用 java.util.date代替
+        globalConfig.setIdType(IdType.ASSIGN_ID);// 分配ID (主键类型为number或string)
+        generator.setGlobalConfig(globalConfig);
+
+        // 数据源配置
+        DataSourceConfig dataSourceConfig = new DataSourceConfig();
+        dataSourceConfig.setUrl(url);
+        dataSourceConfig.setDbType(DbType.MYSQL);// 数据库类型
+        dataSourceConfig.setDriverName(driverClassName);
+        dataSourceConfig.setUsername(username);
+        dataSourceConfig.setPassword(password);
+        generator.setDataSource(dataSourceConfig);
+
+        // 包配置
+        PackageConfig packageConfig = new PackageConfig();
+        //packageConfig.setModuleName(scanner("模块名"));
+        packageConfig.setParent(rootPackage);
+        packageConfig.setController("controller" + (StrUtil.isNotBlank(pkgName) ? "." + pkgName : ""));
+        packageConfig.setService("service" + (StrUtil.isNotBlank(pkgName) ? "." + pkgName : ""));
+        packageConfig.setServiceImpl("service" + (StrUtil.isNotBlank(pkgName) ? "." + pkgName + ".impl" : ".impl"));
+        packageConfig.setEntity("dao.entity" + (StrUtil.isNotBlank(pkgName) ? "." + pkgName : ""));
+        packageConfig.setMapper("dao.mapper" + (StrUtil.isNotBlank(pkgName) ? "." + pkgName : ""));
+
+        //packageConfig.setXml("dao.mapper.xml");
+        generator.setPackageInfo(packageConfig);
+
+        // 注意:模板引擎在mybatisplus依赖中的templates目录下,可以依照此默认模板进行自定义
+
+        // 策略配置:配置根据哪张表生成代码
+        StrategyConfig strategy = new StrategyConfig();
+        strategy.setInclude(tableNames);// 表名,多个英文逗号分割(与exclude二选一配置)
+        strategy.setNaming(NamingStrategy.underline_to_camel);
+        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
+        // strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!");
+        strategy.setEntityLombokModel(true);// lombok模型,@Accessors(chain = true)setter链式操作
+        strategy.setRestControllerStyle(true);// controller生成@RestController
+        strategy.setEntityTableFieldAnnotationEnable(true);// 是否生成实体时,生成字段注解
+        // strategy.setEntityColumnConstant(true);// 是否生成字段常量(默认 false)
+        strategy.setTablePrefix(tablePrefix);// 生成实体时去掉表前缀
+
+        TemplateConfig templateConfig = new TemplateConfig();
+        templateConfig.setController(null);
+        templateConfig.setService(null);
+        templateConfig.setServiceImpl(null);
+        templateConfig.setXml(null);
+        templateConfig.setMapper(null);
+        templateConfig.setEntity(null);
+        generator.setTemplate(templateConfig);
+
+
+        generator.setStrategy(strategy);
+        generator.setTemplateEngine(new FreemarkerTemplateEngine());
+
+        /**
+         * 自定义输出路径
+         */
+        // controller
+        List<FileOutConfig> focList = new ArrayList<>();
+
+        // mapper.xml
+        focList.add(new FileOutConfig("/templates/story-entity.java.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
+                return projectPath + "/" + modelModuleName + "/src/main/java/" + rootPackage.replace(".", "/") + "/dao/entity/" + pkgName + "/" + tableInfo.getEntityName() + StringPool.DOT_JAVA;
+            }
+        });
+
+        focList.add(new FileOutConfig("/templates/story-controller.java.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                return projectPath + "/" + controllerModuleName + "/src/main/java/" + rootPackage.replace(".", "/") + "/controller/" + pkgName + "/" + tableInfo.getEntityName() + "Controller" + StringPool.DOT_JAVA;
+            }
+        });
+        // service
+        focList.add(new FileOutConfig("/templates/service.java.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                return projectPath + "/" + serviceModuleName + "/src/main/java/" + rootPackage.replace(".", "/") + "/service/" + pkgName + "/" +  tableInfo.getEntityName() + "Service" + StringPool.DOT_JAVA;
+            }
+        });
+
+        // serviceImpl
+        focList.add(new FileOutConfig("/templates/story-serviceImpl.java.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                return projectPath + "/" + serviceModuleName + "/src/main/java/" + rootPackage.replace(".", "/") + "/service/" + pkgName + "/impl/" + tableInfo.getEntityName() + "ServiceImpl" + StringPool.DOT_JAVA;
+            }
+        });
+
+        // mapper.java
+        // service
+        focList.add(new FileOutConfig("/templates/story-mapper.java.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                return projectPath + "/" + modelModuleName + "/src/main/java/" + rootPackage.replace(".", "/") + "/dao/mapper/" + pkgName + "/" +  tableInfo.getEntityName() + "Mapper" + StringPool.DOT_JAVA;
+            }
+        });
+
+        // mapper.xml
+        focList.add(new FileOutConfig("/templates/mapper.xml.ftl") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
+                return projectPath + "/" + modelModuleName + "/src/main/resources/mapper/" + pkgName + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
+            }
+        });
+
+        InjectionConfig injectionConfig = new InjectionConfig() {
+            @Override
+            public void initMap() { }
+        };
+        injectionConfig.setFileOutConfigList(focList);
+
+        generator.setCfg(injectionConfig);
+        generator.execute();
+    }
+}

模板

WARNING

模板不能使用IDE格式化,否则生成的文件缩进会有问题

story-entity.java.ftl

xml
package ${package.Entity};
+
+<#list table.importPackages as pkg>
+import ${pkg};
+</#list>
+<#if swagger2>
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+</#if>
+<#if entityLombokModel>
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+    <#if chainModel>
+import lombok.experimental.Accessors;
+    </#if>
+</#if>
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.experimental.FieldNameConstants;
+
+/**
+ * ${table.comment!}
+ *
+ * @author ${author}
+ * @since ${date}
+ */
+<#if entityLombokModel>
+@Data
+    <#if superEntityClass??>
+@EqualsAndHashCode(callSuper = true)
+    <#else>
+@EqualsAndHashCode(callSuper = false)
+    </#if>
+    <#if chainModel>
+@Accessors(chain = true)
+    </#if>
+</#if>
+<#if table.convert>
+@TableName("${table.name}")
+</#if>
+<#if swagger2>
+@ApiModel(value="${entity}对象", description="${table.comment!}")
+</#if>
+@FieldNameConstants
+<#if superEntityClass??>
+public class ${entity} extends ${superEntityClass}<#if activeRecord><${entity}></#if> {
+<#elseif activeRecord>
+public class ${entity} extends Model<${entity}> {
+<#else>
+public class ${entity} implements Serializable {
+</#if>
+
+<#if entitySerialVersionUID>
+    private static final long serialVersionUID = 1L;
+</#if>
+<#-- ----------  BEGIN 字段循环遍历  ---------->
+<#list table.fields as field>
+    <#if field.keyFlag>
+        <#assign keyPropertyName="${field.propertyName}"/>
+    </#if>
+
+    @Schema(description = "${field.comment}")
+    <#if field.keyFlag>
+        <#-- 主键 -->
+        <#if field.keyIdentityFlag>
+    @TableId(value = "${field.annotationColumnName}", type = IdType.AUTO)
+        <#elseif idType??>
+    @TableId(value = "${field.annotationColumnName}", type = IdType.${idType})
+        <#elseif field.convert>
+    @TableId("${field.annotationColumnName}")
+        </#if>
+        <#-- 普通字段 -->
+    <#elseif field.fill??>
+    <#-- -----   存在字段填充设置   ----->
+        <#if field.convert>
+    @TableField(value = "${field.annotationColumnName}", fill = FieldFill.${field.fill})
+        <#else>
+    @TableField(fill = FieldFill.${field.fill})
+        </#if>
+    <#elseif field.convert>
+    @TableField("${field.annotationColumnName}")
+    </#if>
+    <#-- 乐观锁注解 -->
+    <#if (versionFieldName!"") == field.name>
+    @Version
+    </#if>
+    <#-- 逻辑删除注解 -->
+    <#if (logicDeleteFieldName!"") == field.name>
+    @TableLogic
+    </#if>
+    private ${field.propertyType} ${field.propertyName};
+</#list>
+<#------------  END 字段循环遍历  ---------->
+
+<#if !entityLombokModel>
+    <#list table.fields as field>
+        <#if field.propertyType == "boolean">
+            <#assign getprefix="is"/>
+        <#else>
+            <#assign getprefix="get"/>
+        </#if>
+    public ${field.propertyType} ${getprefix}${field.capitalName}() {
+        return ${field.propertyName};
+    }
+
+    <#if chainModel>
+    public ${entity} set${field.capitalName}(${field.propertyType} ${field.propertyName}) {
+    <#else>
+    public void set${field.capitalName}(${field.propertyType} ${field.propertyName}) {
+    </#if>
+        this.${field.propertyName} = ${field.propertyName};
+        <#if chainModel>
+        return this;
+        </#if>
+    }
+    </#list>
+</#if>
+
+<#if entityColumnConstant>
+    <#list table.fields as field>
+    public static final String ${field.name?upper_case} = "${field.name}";
+
+    </#list>
+</#if>
+<#if activeRecord>
+    @Override
+    protected Serializable pkVal() {
+    <#if keyPropertyName??>
+        return this.${keyPropertyName};
+    <#else>
+        return null;
+    </#if>
+    }
+
+</#if>
+<#if !entityLombokModel>
+    @Override
+    public String toString() {
+        return "${entity}{" +
+    <#list table.fields as field>
+        <#if field_index==0>
+            "${field.propertyName}=" + ${field.propertyName} +
+        <#else>
+            ", ${field.propertyName}=" + ${field.propertyName} +
+        </#if>
+    </#list>
+        "}";
+    }
+</#if>
+}
package ${package.Entity};
+
+<#list table.importPackages as pkg>
+import ${pkg};
+</#list>
+<#if swagger2>
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+</#if>
+<#if entityLombokModel>
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+    <#if chainModel>
+import lombok.experimental.Accessors;
+    </#if>
+</#if>
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.experimental.FieldNameConstants;
+
+/**
+ * ${table.comment!}
+ *
+ * @author ${author}
+ * @since ${date}
+ */
+<#if entityLombokModel>
+@Data
+    <#if superEntityClass??>
+@EqualsAndHashCode(callSuper = true)
+    <#else>
+@EqualsAndHashCode(callSuper = false)
+    </#if>
+    <#if chainModel>
+@Accessors(chain = true)
+    </#if>
+</#if>
+<#if table.convert>
+@TableName("${table.name}")
+</#if>
+<#if swagger2>
+@ApiModel(value="${entity}对象", description="${table.comment!}")
+</#if>
+@FieldNameConstants
+<#if superEntityClass??>
+public class ${entity} extends ${superEntityClass}<#if activeRecord><${entity}></#if> {
+<#elseif activeRecord>
+public class ${entity} extends Model<${entity}> {
+<#else>
+public class ${entity} implements Serializable {
+</#if>
+
+<#if entitySerialVersionUID>
+    private static final long serialVersionUID = 1L;
+</#if>
+<#-- ----------  BEGIN 字段循环遍历  ---------->
+<#list table.fields as field>
+    <#if field.keyFlag>
+        <#assign keyPropertyName="${field.propertyName}"/>
+    </#if>
+
+    @Schema(description = "${field.comment}")
+    <#if field.keyFlag>
+        <#-- 主键 -->
+        <#if field.keyIdentityFlag>
+    @TableId(value = "${field.annotationColumnName}", type = IdType.AUTO)
+        <#elseif idType??>
+    @TableId(value = "${field.annotationColumnName}", type = IdType.${idType})
+        <#elseif field.convert>
+    @TableId("${field.annotationColumnName}")
+        </#if>
+        <#-- 普通字段 -->
+    <#elseif field.fill??>
+    <#-- -----   存在字段填充设置   ----->
+        <#if field.convert>
+    @TableField(value = "${field.annotationColumnName}", fill = FieldFill.${field.fill})
+        <#else>
+    @TableField(fill = FieldFill.${field.fill})
+        </#if>
+    <#elseif field.convert>
+    @TableField("${field.annotationColumnName}")
+    </#if>
+    <#-- 乐观锁注解 -->
+    <#if (versionFieldName!"") == field.name>
+    @Version
+    </#if>
+    <#-- 逻辑删除注解 -->
+    <#if (logicDeleteFieldName!"") == field.name>
+    @TableLogic
+    </#if>
+    private ${field.propertyType} ${field.propertyName};
+</#list>
+<#------------  END 字段循环遍历  ---------->
+
+<#if !entityLombokModel>
+    <#list table.fields as field>
+        <#if field.propertyType == "boolean">
+            <#assign getprefix="is"/>
+        <#else>
+            <#assign getprefix="get"/>
+        </#if>
+    public ${field.propertyType} ${getprefix}${field.capitalName}() {
+        return ${field.propertyName};
+    }
+
+    <#if chainModel>
+    public ${entity} set${field.capitalName}(${field.propertyType} ${field.propertyName}) {
+    <#else>
+    public void set${field.capitalName}(${field.propertyType} ${field.propertyName}) {
+    </#if>
+        this.${field.propertyName} = ${field.propertyName};
+        <#if chainModel>
+        return this;
+        </#if>
+    }
+    </#list>
+</#if>
+
+<#if entityColumnConstant>
+    <#list table.fields as field>
+    public static final String ${field.name?upper_case} = "${field.name}";
+
+    </#list>
+</#if>
+<#if activeRecord>
+    @Override
+    protected Serializable pkVal() {
+    <#if keyPropertyName??>
+        return this.${keyPropertyName};
+    <#else>
+        return null;
+    </#if>
+    }
+
+</#if>
+<#if !entityLombokModel>
+    @Override
+    public String toString() {
+        return "${entity}{" +
+    <#list table.fields as field>
+        <#if field_index==0>
+            "${field.propertyName}=" + ${field.propertyName} +
+        <#else>
+            ", ${field.propertyName}=" + ${field.propertyName} +
+        </#if>
+    </#list>
+        "}";
+    }
+</#if>
+}

story-controller.java.ftl

xml
package ${package.Controller};
+
+import ${package.Service}.${table.serviceName};
+import org.springframework.web.bind.annotation.RequestMapping;
+<#if restControllerStyle>
+import org.springframework.web.bind.annotation.RestController;
+<#else>
+import org.springframework.stereotype.Controller;
+</#if>
+<#if superControllerClassPackage??>
+import ${superControllerClassPackage};
+</#if>
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * ${table.comment!} 前端控制器
+ *
+ * @author ${author}
+ * @since ${date}
+ */
+@Tag(name = "")
+@Slf4j
+@RequiredArgsConstructor
+<#if restControllerStyle>
+@RestController
+<#else>
+@Controller
+</#if>
+@RequestMapping("<#if package.ModuleName?? && package.ModuleName != "">/${package.ModuleName}</#if>/<#if controllerMappingHyphenStyle??>${controllerMappingHyphen}<#else>${table.entityPath}</#if>")
+<#if kotlin>
+class ${table.controllerName}<#if superControllerClass??> : ${superControllerClass}()</#if>
+<#else>
+<#if superControllerClass??>
+public class ${table.controllerName} extends ${superControllerClass} {
+<#else>
+public class ${table.controllerName} {
+</#if>
+    private final ${table.serviceName} ${table.serviceName?uncap_first};
+
+}
+</#if>
package ${package.Controller};
+
+import ${package.Service}.${table.serviceName};
+import org.springframework.web.bind.annotation.RequestMapping;
+<#if restControllerStyle>
+import org.springframework.web.bind.annotation.RestController;
+<#else>
+import org.springframework.stereotype.Controller;
+</#if>
+<#if superControllerClassPackage??>
+import ${superControllerClassPackage};
+</#if>
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * ${table.comment!} 前端控制器
+ *
+ * @author ${author}
+ * @since ${date}
+ */
+@Tag(name = "")
+@Slf4j
+@RequiredArgsConstructor
+<#if restControllerStyle>
+@RestController
+<#else>
+@Controller
+</#if>
+@RequestMapping("<#if package.ModuleName?? && package.ModuleName != "">/${package.ModuleName}</#if>/<#if controllerMappingHyphenStyle??>${controllerMappingHyphen}<#else>${table.entityPath}</#if>")
+<#if kotlin>
+class ${table.controllerName}<#if superControllerClass??> : ${superControllerClass}()</#if>
+<#else>
+<#if superControllerClass??>
+public class ${table.controllerName} extends ${superControllerClass} {
+<#else>
+public class ${table.controllerName} {
+</#if>
+    private final ${table.serviceName} ${table.serviceName?uncap_first};
+
+}
+</#if>

story-serviceImpl.java.ftl

xml
package ${package.ServiceImpl};
+
+import ${package.Entity}.${entity};
+import ${package.Mapper}.${table.mapperName};
+import ${package.Service}.${table.serviceName};
+import ${superServiceImplClassPackage};
+import org.springframework.stereotype.Service;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * ${table.comment!} 服务实现类
+ *
+ * @author ${author}
+ * @since ${date}
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+<#if kotlin>
+open class ${table.serviceImplName} : ${superServiceImplClass}<${table.mapperName}, ${entity}>(), ${table.serviceName} {
+
+}
+<#else>
+public class ${table.serviceImplName} extends ${superServiceImplClass}<${table.mapperName}, ${entity}> implements ${table.serviceName} {
+
+}
+</#if>
package ${package.ServiceImpl};
+
+import ${package.Entity}.${entity};
+import ${package.Mapper}.${table.mapperName};
+import ${package.Service}.${table.serviceName};
+import ${superServiceImplClassPackage};
+import org.springframework.stereotype.Service;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * ${table.comment!} 服务实现类
+ *
+ * @author ${author}
+ * @since ${date}
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+<#if kotlin>
+open class ${table.serviceImplName} : ${superServiceImplClass}<${table.mapperName}, ${entity}>(), ${table.serviceName} {
+
+}
+<#else>
+public class ${table.serviceImplName} extends ${superServiceImplClass}<${table.mapperName}, ${entity}> implements ${table.serviceName} {
+
+}
+</#if>

story-mapper.java.ftl

java
package ${package.Mapper};
+
+import ${package.Entity}.${entity};
+import ${superMapperClassPackage};
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * <p>
+ * ${table.comment!} Mapper 接口
+ * </p>
+ *
+ * @author ${author}
+ * @since ${date}
+ */
+<#if kotlin>
+interface ${table.mapperName} : ${superMapperClass}<${entity}>
+<#else>
+@Mapper
+public interface ${table.mapperName} extends ${superMapperClass}<${entity}> {
+
+}
+</#if>
package ${package.Mapper};
+
+import ${package.Entity}.${entity};
+import ${superMapperClassPackage};
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * <p>
+ * ${table.comment!} Mapper 接口
+ * </p>
+ *
+ * @author ${author}
+ * @since ${date}
+ */
+<#if kotlin>
+interface ${table.mapperName} : ${superMapperClass}<${entity}>
+<#else>
+@Mapper
+public interface ${table.mapperName} extends ${superMapperClass}<${entity}> {
+
+}
+</#if>
+ + + + \ No newline at end of file diff --git a/java/index.html b/java/index.html new file mode 100644 index 000000000..15065d7e7 --- /dev/null +++ b/java/index.html @@ -0,0 +1,25 @@ + + + + + + Java | 故事 + + + + + + + + + + + + + + +
Skip to content

Java

  • 基础
  • 框架
  • 中间件
  • 数据库
  • 开发工具
  • 其他
+ + + + \ No newline at end of file diff --git "a/java/middleware/kafka\345\255\246\344\271\240\350\256\260\345\275\225.html" "b/java/middleware/kafka\345\255\246\344\271\240\350\256\260\345\275\225.html" new file mode 100644 index 000000000..c167e51bd --- /dev/null +++ "b/java/middleware/kafka\345\255\246\344\271\240\350\256\260\345\275\225.html" @@ -0,0 +1,25 @@ + + + + + + kafka学习记录 | 故事 + + + + + + + + + + + + + + +
Skip to content

kafka学习记录

新公司用的消息中间件是kafka,此前没有接触过,先大致了解下内容,后续再补充

kafka简介

​ Kafka is a distributed,partitioned,replicated commit logservice。

Apache Kafka 是一个分布式发布 - 订阅消息系统和一个强大的队列,可以处理大量的数据,并使你能够将消息从一个端点传递到另一个端点。 Kafka 适合离线和在线消息消费。 Kafka 消息保留在磁盘上,并在群集内复制以防止数据丢失。 Kafka 构建在 ZooKeeper 同步服务之上。 它与 Apache Storm 和 Spark 非常好地集成,用于实时流式数据分析。

Kafka 是一个分布式消息队列,具有高性能、持久化、多副本备份、横向扩展能力。生产者往队列里写消息,消费者从队列里取消息进行业务逻辑。一般在架构设计中起到解耦、削峰、异步处理的作用。

相关术语:

  • broker:kafka集群中的每一台server称为一个kafka实例,也叫broker

  • topic:主题,一个topic中保存同一类消息,相当于对消息的分类

  • partition:分区,每个topic都可以分成多个partition,每个partition的存储层面都是append log文件.任何发布到该partition的消息都会被追加到log文件尾部.

    分区的根本原因:kafka基于文件进行存储,当文件内容达到一定程度很容易达到单个磁盘的上限,因此采取分区的办法,一个分区对应一个文件,这样就可以将数据分别存储到不同server上,另外也可以负载均衡,容纳更多消费者

  • offset:偏移量,一个分区对应一个磁盘上的文件,而消息在文件中的位置就是offset偏移量,offset为一个long型数字,可以唯一标记一条消息,kafka没有提供其他额外的索引机制来存储offset,所以只能顺序读写,在kafka中几乎不允许对消息进行"随机读写"

总结:

  • kafka是一个基于发布-订阅模型的分布式消息系统(消息队列)
  • kafka面向大数据,消息保存在topic中,每个topic有多个partition
  • kafka的消息数据保存在磁盘,每个partition对应磁盘上的一个文件,消息写入就是在append log文件尾部追加内容,文件可以在集群内备份以防丢失
  • 即使消息被消费,kafka也不会立即删除消息,可以通过配置使得过一段时间后自动删除以释放磁盘空间
  • kafka依赖分布式协调服务zookeeper,适合离线/在线消息的消费

基本原理

1、分布式和分区(distributed、partitioned)

一个topic对应的多个partition分散存储到集群的多个broker上,存储方式是一个partition对应一个文件,每个broker负责存储在自己机器上的partition中的消息读写

2、副本(replicated)

kafka可以配置partitions需要备份的个数(replicas),每个partition将会备份到多台机器上,以提高可用性.既然有副本,就涉及到对同一个文件的多个备份如何进行管理调度,kafka的方案是每个partition选举一个server作为leader,由leader负责所有对该分区的读写,其他server作为follower只需要简单的与leader同步,如果原来的leader失效,会重新选举由其他的follower来成为新的leader

如何选举leader:kafka使用zookeeper在broker中选出一个controller,用于partition的分配和leader选举

另外,作为leader的server承担了该分区所有的读写,所以压力较大,从整体考虑,有多少个partition就有多少个leader,kafka会将leader分散到不同的broker上,确保整体负载均衡

3、整体数据流程

202009161733104250.jpg

1)数据生产过程(producer)

生产者写入的消息,可以指定四个参数:topic、partition、key、value其中topic和value是必须指定的,key和partition是可选的。

对于一条记录,先对其进行序列化,然后按照topic和partition,放进对应的发送队列中。如果partition没有指定,那么情况如下:a、Key有填,按照key进行hash,相同的key去一个partition

b、key没填,round-robin轮询来选partition 202009161733117119.png

producer会和topic下的所有partition leader保持socket连接,消息经过producer直接通过socket发送至broker。其中partition leader的位置(host:port)注册在zookeeper中,producer作为zookeeper客户端已经注册了watch监听partition leader的变更事件,因此可以准确的知道谁是当前leader。

producer端采用异步发送,多条消息暂且在客户端buffer起来,并将他们批量发送到broker,小数据IO太多,会拖慢整体的网络延迟,批量延迟发送事实上提升了网络效率

2)数据消费过程(consumer)

消费者不是以单独形式存在,每一个消费者属于一个consumer group消费者组,一个group包含多个consumer。订阅topic是以一个消费组来订阅的,发送到topic的消息,只会被订阅此topic的每个group中的consumer消费。

如果所有的consumer都具有相同group,那么就像一个点对点的消息系统,如果每个consumer都具有不同的group,那消息就会广播给所有消费者。

具体说来,这实际上是根据partition来分的,==一个partition只能被消费组里的一个消费者消费,但是可以被多个消费组消费==,消费组里的每个消费者是关联到一个partition的,因此有这样的说法:对于一个topic,同一个group中不能有多于partitions个数的consumer同时消费,否则意味着某些消费者无法得到消息

同一个消费组的两个消费者不会同时消费一个partition.

在kafka中,采用了pull的方式,即consumer和broker建立连接之后,主动去pull(或者说fetch)消息,首先consumer端可以根据自己的消费能力适时去fetch消息并处理,且可以控制消息消费的进度(offset)。

partition中的消息只有一个consumer在消费,且不存在消息状态的控制,也没有复杂的消息确认机制,所以kafka的broker端很轻量级。当消息被consumer接受之后,需要保存offset记录消费到哪,以前保存在zk中,由于zk的性能瓶颈,以前的解决方案是consumer一分钟上报一次,在0.10版本后kafka把offset保存,从zk中剥离,保存在consumeroffsets topic的topic中,由此可见,consumer客户端也很轻量级

4、消息传送机制

kafka支持三种消息投递语义,在业务中通常使用At least once模型

  • At most once:最多一次,消息可能丢失,不会重复
  • At least once:最少一次,消息不会丢失,可能重复
  • Exactly once:只且一次,消息不丢失不重复,且只消费一次。

集群架构

20190805220416350.pngcluster_architecture.jpg

+ + + + \ No newline at end of file diff --git "a/java/middleware/kafka\345\270\270\347\224\250\345\221\275\344\273\244\350\256\260\345\275\225.html" "b/java/middleware/kafka\345\270\270\347\224\250\345\221\275\344\273\244\350\256\260\345\275\225.html" new file mode 100644 index 000000000..e64fca02d --- /dev/null +++ "b/java/middleware/kafka\345\270\270\347\224\250\345\221\275\344\273\244\350\256\260\345\275\225.html" @@ -0,0 +1,25 @@ + + + + + + kafka常用命令记录 | 故事 + + + + + + + + + + + + + + +
Skip to content

kafka常用命令记录

环境信息:MacOS 13.4.1

Kafka版本:Homebrew Kafka_3.4.0

topic

新建topic

kafka-topics --create --topic topic-name --partitions 4 --replication-factor 2 --bootstrap-server localhost:9092

查看所有topic

kafka-topics --bootstrap-server 127.0.0.1:9092 --list

查看topic信息

kafka-topics --bootstrap-server 127.0.0.1:9092 --describe --topic topic-name

修改topic分区数

kafka-topics --bootstrap-server 127.0.0.1:9092 --alter --partitions 2 --topic topic-name

删除topic

kafka-topics --bootstrap-server 127.0.0.1:9092 --delete --topic topic-name

清空topic的消息

给topic保留消息时间改为1s,然后等topic中的消息被自动删除,再删除该配置

删除topic也可以实现清空消息

kafka-configs --bootstrap-server 127.0.0.1:9092 --entity-type topics --alter --entity-name example --add-config retention.ms=1000

kafka-configs --bootstrap-server 127.0.0.1:9092 --entity-type topics --alter --entity-name example --delete-config retention.ms

消费者组

查看所有消费者组

kafka-consumer-groups --bootstrap-server 127.0.0.1:9092 --list

查看消费者组详情

kafka-consumer-groups --bootstrap-server 127.0.0.1:9092 --describe --group topic-name

查看消费者组里具体成员

kafka-consumer-groups --bootstrap-server 127.0.0.1:9092 --describe --members --group topic-name

消费者console

查看topic中的所有消息

kafka-console-consumer --bootstrap-server 127.0.0.1:9092 --topic topic-name --from-beginning

指定分区、offset的消息

kafka-console-consumer --bootstrap-server 127.0.0.1:9092 --topic topic-name --partition 0 --offset 1

+ + + + \ No newline at end of file diff --git "a/java/middleware/kakfa\345\256\236\350\267\265.html" "b/java/middleware/kakfa\345\256\236\350\267\265.html" new file mode 100644 index 000000000..177dbe5cc --- /dev/null +++ "b/java/middleware/kakfa\345\256\236\350\267\265.html" @@ -0,0 +1,329 @@ + + + + + + kafka实践 | 故事 + + + + + + + + + + + + + + +
Skip to content

kafka实践

下载

官方地址:https://kafka.apache.org/downloads.html

kafka入门介绍

kafka作为一个分布式流平台,意味着什么

  • 发布和订阅消息(流),在这方面它类似一个消息队列
  • 以容错(故障转移)的方式存储消息(流)
  • 在消息流发生时处理它们

kafka的优势

  • 构建实时的数据管道,可靠的获取系统和应用程序间的数据
  • 构建实时流的应用程序,对数据流进行转换或反应

kafka的几个概念

  • kafka作为集群运行在一个或多个服务器上
  • kafka集群存储的消息是以topic为类别记录的
  • 每个消息由一个key,一个value和时间戳构成

kafka四个核心API

  • Product API:发布消息到一个或多个topic中
  • Consumer API:订阅一个或多个topic,并处理产生的消息
  • Streams API:充当一个流处理器 ,从一个或多个topic消费输入流,并产生一个输出流到一个或多个输出topic,有效地将输入流转换到输出流
  • Connector API:可构建或运行可重用的生产者或消费者,将topic连接到现有的应用程序或数据系统。例如连接到关系型数据库的连接器可以捕捉表的每个变更

快速启动kafka

官方文档:http://kafka.apache.org/quickstart

TIP

强烈建议新学习一项新内容的时候尽量阅读英文原版文档

启动kafka需要的运行环境

使用kafka自带的zookeeper启动

bash
tar -xzf kafka_2.13-2.8.0.tgz
+cd kafka_2.13-2.8.0
+bin/zookeeper-server-start.sh config/zookeeper.properties & #后台启动
tar -xzf kafka_2.13-2.8.0.tgz
+cd kafka_2.13-2.8.0
+bin/zookeeper-server-start.sh config/zookeeper.properties & #后台启动

在kafka0.5x版本后已经自带了zookeeper, 而在最新的kafka2.8版本中,不再需要zookeeper服务,官网把这种称之为KRaft模式

打开另一个命令终端启动kafka服务

bash
bin/kafka-server-start.sh config/server.properties
bin/kafka-server-start.sh config/server.properties

等所有服务等启动完毕,kafka就已经是可用的了

创建一个主题(topic)

创建一个名为quickstart-events的topic,只有一个分区和一个备份

bash
bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic quickstart-events
+
+查看已创建的topic信息
+
+```bash
+[root@localhost kafka_2.13-2.8.0]# bin/kafka-topics.sh --describe --topic quickstart-events --bootstrap-server localhost:9092
+Topic: quickstart-events        TopicId: iOM06pJVQV-y_A6QkmfeHw PartitionCount: 1       ReplicationFactor: 1  Configs: segment.bytes=1073741824
+        Topic: quickstart-events        Partition: 0    Leader: 0       Replicas: 0     Isr: 0
bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic quickstart-events
+
+查看已创建的topic信息
+
+```bash
+[root@localhost kafka_2.13-2.8.0]# bin/kafka-topics.sh --describe --topic quickstart-events --bootstrap-server localhost:9092
+Topic: quickstart-events        TopicId: iOM06pJVQV-y_A6QkmfeHw PartitionCount: 1       ReplicationFactor: 1  Configs: segment.bytes=1073741824
+        Topic: quickstart-events        Partition: 0    Leader: 0       Replicas: 0     Isr: 0

发送消息到Topic

kafka提供了一个命令行工具,可以从输入文件或命令行中读取消息并发送给kafka集群,每一行是一条消息.运行producer(生产者) ,然后再控制台输入几条消息到服务器

bash
bin/kafka-console-producer.sh --topic quickstart-events --bootstrap-server localhost:9092
bin/kafka-console-producer.sh --topic quickstart-events --bootstrap-server localhost:9092

输入此命令后进入交互模式,便可以开始发送消息到topic

bash
>hello
+>hello world
+>this is first kafka message
+>
>hello
+>hello world
+>this is first kafka message
+>

可以使用Ctrl+C退出交互模式

消费Topic中的消息

打开另外一个终端窗口运行命令

bash
bin/kafka-console-consumer.sh --topic quickstart-events --from-beginning --bootstrap-server localhost:9092
bin/kafka-console-consumer.sh --topic quickstart-events --from-beginning --bootstrap-server localhost:9092

可以看到输出了刚才我们通过生产者终端发送的消息

bash
hello
+hello world
+this is first kafka message
hello
+hello world
+this is first kafka message

此时如果我们再在生产者终端发送消息,消费者终端也能实时进行消费,同样可以使用Ctrl+C 退出消费者的交互模式,消息会被持久化到kafka中,所以消息可以被消费多次以及被多个消费者消费,比如我们再打开一个窗口,执行消费的命令

bash
[root@localhost kafka_2.13-2.8.0]# bin/kafka-console-consumer.sh --topic quickstart-events --from-beginning --bootstrap-server localhost:9092
+hello
+hello world
+this is first kafka message
+
+可以看到,新的消费者又消费到了刚才的消息
[root@localhost kafka_2.13-2.8.0]# bin/kafka-console-consumer.sh --topic quickstart-events --from-beginning --bootstrap-server localhost:9092
+hello
+hello world
+this is first kafka message
+
+可以看到,新的消费者又消费到了刚才的消息

Kafka的Java客户端

依赖

xml
<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter</artifactId>
+</dependency>
+<dependency>
+<groupId>org.apache.kafka</groupId>
+<artifactId>kafka-streams</artifactId>
+</dependency>
+<dependency>
+<groupId>org.springframework.kafka</groupId>
+<artifactId>spring-kafka</artifactId>
+</dependency>
+
+<dependency>
+<groupId>org.springframework.boot</groupId>
+<artifactId>spring-boot-starter-test</artifactId>
+<scope>test</scope>
+</dependency>
+<dependency>
+<groupId>org.springframework.kafka</groupId>
+<artifactId>spring-kafka-test</artifactId>
+<scope>test</scope>
+</dependency>
<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter</artifactId>
+</dependency>
+<dependency>
+<groupId>org.apache.kafka</groupId>
+<artifactId>kafka-streams</artifactId>
+</dependency>
+<dependency>
+<groupId>org.springframework.kafka</groupId>
+<artifactId>spring-kafka</artifactId>
+</dependency>
+
+<dependency>
+<groupId>org.springframework.boot</groupId>
+<artifactId>spring-boot-starter-test</artifactId>
+<scope>test</scope>
+</dependency>
+<dependency>
+<groupId>org.springframework.kafka</groupId>
+<artifactId>spring-kafka-test</artifactId>
+<scope>test</scope>
+</dependency>

修改kafka配置

bash
修改config/server.properties
+listeners=PLAINTEXT://0.0.0.0:9092
+advertised.listeners=PLAINTEXT://192.168.174.130:9092
修改config/server.properties
+listeners=PLAINTEXT://0.0.0.0:9092
+advertised.listeners=PLAINTEXT://192.168.174.130:9092

开放虚拟机端口9092

firewall-cmd --zone=public --add-port=9092/tcp --permanent

firewall-cmd --reload

生产者

java
/**
+ * @author xc
+ * @description
+ * @createdTime 2021/5/10 23:58
+ */
+public class KafkaProducerTests {
+    @Test
+    void kafkaProducerTest() {
+        Properties properties = new Properties();
+        properties.put("bootstrap.servers", "192.168.174.130:9092");
+        properties.put("acks", "all");
+        properties.put("retries", 0);
+        properties.put("batch.size", 16384);
+        properties.put("linger.ms", 1);
+        properties.put("buffer.memory", 33554432);
+        properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
+        properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
+
+        KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
+        for (int i = 0; i < 100; i++) {
+            producer.send(new ProducerRecord<String, String>("test", Integer.toString(i), "Kafka message " + i));
+            System.out.println("发送了消息");
+        }
+        producer.close();
+
+    }
+}
/**
+ * @author xc
+ * @description
+ * @createdTime 2021/5/10 23:58
+ */
+public class KafkaProducerTests {
+    @Test
+    void kafkaProducerTest() {
+        Properties properties = new Properties();
+        properties.put("bootstrap.servers", "192.168.174.130:9092");
+        properties.put("acks", "all");
+        properties.put("retries", 0);
+        properties.put("batch.size", 16384);
+        properties.put("linger.ms", 1);
+        properties.put("buffer.memory", 33554432);
+        properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
+        properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
+
+        KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
+        for (int i = 0; i < 100; i++) {
+            producer.send(new ProducerRecord<String, String>("test", Integer.toString(i), "Kafka message " + i));
+            System.out.println("发送了消息");
+        }
+        producer.close();
+
+    }
+}
  • send()方法是异步的,添加消息到缓冲区等待发送并立即return,生产者会将单个消息批量在一起进行发送

  • ack是判断是否发送成功的,all将会阻塞消息,这种性能是最低的,但是是最可靠的

  • retries,如果请求失败,生产者会自动重试,如果启用重试,可能会产生重复消息

  • producer缓存每个分区未发送的消息,缓存的大小通过batch.size配置指定,数值较大会产生更大的批次并需要更大的内存

    默认缓冲可立即发送,即使缓存空间没有满,但是如果想减少请求的数量,可设置linger.ms大于0,这将让生产者在发送请求前等待一会儿,希望更多的消息来填补到缓冲区中

  • buffer.memory 控制生产者可用的缓存总量,如果消息发送速度比其传输到服务器的快,将会耗尽缓存空间,当缓存空间耗尽时,其他发送调用将会被阻塞,阻塞实践的阈值通过max.block.ms 设定,之后它将抛出一个TimeoutException

  • key.serializervalue.serializer将用户提供的key和value对象ProducerRecord转换成字节,可以使用附带 的* *ByteArraySerializerStringSeriializer**处理byte或string类型

消费者

java
/**
+ * @author xc
+ * @description
+ * @createdTime 2021/5/10 23:59
+ */
+public class KafkaConsumerTests {
+    @Test
+    void kafkaConsumerTest(){
+        Properties props = new Properties();
+        props.setProperty("bootstrap.servers", "192.168.174.130:9092");
+        props.setProperty("group.id", "test");
+        props.setProperty("enable.auto.commit", "true");
+        props.setProperty("auto.commit.interval.ms", "1000");
+        props.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
+        props.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
+        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
+        consumer.subscribe(Collections.singletonList("test"));
+        while (true) {
+            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
+            for (ConsumerRecord<String, String> record : records)
+                System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
+        }
+    }
+}
/**
+ * @author xc
+ * @description
+ * @createdTime 2021/5/10 23:59
+ */
+public class KafkaConsumerTests {
+    @Test
+    void kafkaConsumerTest(){
+        Properties props = new Properties();
+        props.setProperty("bootstrap.servers", "192.168.174.130:9092");
+        props.setProperty("group.id", "test");
+        props.setProperty("enable.auto.commit", "true");
+        props.setProperty("auto.commit.interval.ms", "1000");
+        props.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
+        props.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
+        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
+        consumer.subscribe(Collections.singletonList("test"));
+        while (true) {
+            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
+            for (ConsumerRecord<String, String> record : records)
+                System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
+        }
+    }
+}
  • enable.auto.commit自动提交偏移量,auto.commit.interval.ms控制提交的频率
  • 客户端订阅了名为test的topic,消费者组叫test

broker通过心跳检测test消费组中的进程,消费者会自动ping集群,告诉集群他还活着,只要消费者停止心跳的时间超过了session.timeout.ms 就会被认定为故障,它的分区将会被分配到别的进程

启动

先启动消费者,后启动生产者,可以看到消费者的终端输出了

bash
offset = 502, key = 0, value = Kafka message 0
+offset = 503, key = 1, value = Kafka message 1
+offset = 504, key = 2, value = Kafka message 2
+offset = 505, key = 3, value = Kafka message 3
+offset = 506, key = 4, value = Kafka message 4
+offset = 507, key = 5, value = Kafka message 5
+offset = 508, key = 6, value = Kafka message 6
+offset = 509, key = 7, value = Kafka message 7
+offset = 510, key = 8, value = Kafka message 8
+offset = 511, key = 9, value = Kafka message 9
+offset = 512, key = 10, value = Kafka message 10
+offset = 513, key = 11, value = Kafka message 11
+offset = 514, key = 12, value = Kafka message 12
+offset = 515, key = 13, value = Kafka message 13
+offset = 516, key = 14, value = Kafka message 14
+offset = 517, key = 15, value = Kafka message 15
+offset = 518, key = 16, value = Kafka message 16
+offset = 519, key = 17, value = Kafka message 17
+offset = 520, key = 18, value = Kafka message 18
+offset = 521, key = 19, value = Kafka message 19
+offset = 522, key = 20, value = Kafka message 20
+......
offset = 502, key = 0, value = Kafka message 0
+offset = 503, key = 1, value = Kafka message 1
+offset = 504, key = 2, value = Kafka message 2
+offset = 505, key = 3, value = Kafka message 3
+offset = 506, key = 4, value = Kafka message 4
+offset = 507, key = 5, value = Kafka message 5
+offset = 508, key = 6, value = Kafka message 6
+offset = 509, key = 7, value = Kafka message 7
+offset = 510, key = 8, value = Kafka message 8
+offset = 511, key = 9, value = Kafka message 9
+offset = 512, key = 10, value = Kafka message 10
+offset = 513, key = 11, value = Kafka message 11
+offset = 514, key = 12, value = Kafka message 12
+offset = 515, key = 13, value = Kafka message 13
+offset = 516, key = 14, value = Kafka message 14
+offset = 517, key = 15, value = Kafka message 15
+offset = 518, key = 16, value = Kafka message 16
+offset = 519, key = 17, value = Kafka message 17
+offset = 520, key = 18, value = Kafka message 18
+offset = 521, key = 19, value = Kafka message 19
+offset = 522, key = 20, value = Kafka message 20
+......

springboot集成kafka

依赖

xml
<dependency>
+    <groupId>org.springframework.kafka</groupId>
+    <artifactId>spring-kafka</artifactId>
+</dependency>
<dependency>
+    <groupId>org.springframework.kafka</groupId>
+    <artifactId>spring-kafka</artifactId>
+</dependency>

生产者

java
/**
+ * @author xc
+ * @description
+ * @createdTime 2021/5/10 21:49
+ */
+@RestController
+@RequestMapping("/kafka")
+public class KafkaDemoProducer {
+
+    @Autowired
+    private KafkaTemplate kafkaTemplate;
+
+    @GetMapping("/send/{msg}")
+    public String sendMessage(@PathVariable String msg) {
+        ListenableFuture send = kafkaTemplate.send("springboot-kafka", "测试发送:" + msg + "-" + System.currentTimeMillis());
+        System.out.println(send);
+        return "发送成功";
+    }
+}
/**
+ * @author xc
+ * @description
+ * @createdTime 2021/5/10 21:49
+ */
+@RestController
+@RequestMapping("/kafka")
+public class KafkaDemoProducer {
+
+    @Autowired
+    private KafkaTemplate kafkaTemplate;
+
+    @GetMapping("/send/{msg}")
+    public String sendMessage(@PathVariable String msg) {
+        ListenableFuture send = kafkaTemplate.send("springboot-kafka", "测试发送:" + msg + "-" + System.currentTimeMillis());
+        System.out.println(send);
+        return "发送成功";
+    }
+}

消费者

java
/**
+ * @author xc
+ * @description
+ * @createdTime 2021/5/15 23:55
+ */
+@Component
+public class KafkaDemoConsumer {
+
+
+    @KafkaListener(topics = {"springboot-kafka"})
+    public void onReceive(ConsumerRecord<?, ?> record) {
+        System.out.println("接收消息:" + record.topic() + "-" + record.partition() + "-" + record.value());
+    }
+}
/**
+ * @author xc
+ * @description
+ * @createdTime 2021/5/15 23:55
+ */
+@Component
+public class KafkaDemoConsumer {
+
+
+    @KafkaListener(topics = {"springboot-kafka"})
+    public void onReceive(ConsumerRecord<?, ?> record) {
+        System.out.println("接收消息:" + record.topic() + "-" + record.partition() + "-" + record.value());
+    }
+}

测试

  • 启动应用

  • 发送消息

  • 日志

bash
2021-05-16 15:56:18.430  INFO 10808 --- [nio-8080-exec-1] o.a.kafka.common.utils.AppInfoParser     : Kafka version: 2.6.0
+2021-05-16 15:56:18.430  INFO 10808 --- [nio-8080-exec-1] o.a.kafka.common.utils.AppInfoParser     : Kafka commitId: 62abe01bee039651
+2021-05-16 15:56:18.430  INFO 10808 --- [nio-8080-exec-1] o.a.kafka.common.utils.AppInfoParser     : Kafka startTimeMs: 1621151778430
+2021-05-16 15:56:18.435  INFO 10808 --- [ad | producer-1] org.apache.kafka.clients.Metadata        : [Producer clientId=producer-1] Cluster ID: t54vUJ_qTWm-o8WmD-dfag
+org.springframework.util.concurrent.SettableListenableFuture@2a59dc33
+接收消息:springboot-kafka-0-测试发送:hello-1621151778418
2021-05-16 15:56:18.430  INFO 10808 --- [nio-8080-exec-1] o.a.kafka.common.utils.AppInfoParser     : Kafka version: 2.6.0
+2021-05-16 15:56:18.430  INFO 10808 --- [nio-8080-exec-1] o.a.kafka.common.utils.AppInfoParser     : Kafka commitId: 62abe01bee039651
+2021-05-16 15:56:18.430  INFO 10808 --- [nio-8080-exec-1] o.a.kafka.common.utils.AppInfoParser     : Kafka startTimeMs: 1621151778430
+2021-05-16 15:56:18.435  INFO 10808 --- [ad | producer-1] org.apache.kafka.clients.Metadata        : [Producer clientId=producer-1] Cluster ID: t54vUJ_qTWm-o8WmD-dfag
+org.springframework.util.concurrent.SettableListenableFuture@2a59dc33
+接收消息:springboot-kafka-0-测试发送:hello-1621151778418
+ + + + \ No newline at end of file diff --git "a/java/middleware/kakfa\351\207\215\345\244\215\346\266\210\350\264\271\351\227\256\351\242\230\345\244\204\347\220\206.html" "b/java/middleware/kakfa\351\207\215\345\244\215\346\266\210\350\264\271\351\227\256\351\242\230\345\244\204\347\220\206.html" new file mode 100644 index 000000000..88963cee3 --- /dev/null +++ "b/java/middleware/kakfa\351\207\215\345\244\215\346\266\210\350\264\271\351\227\256\351\242\230\345\244\204\347\220\206.html" @@ -0,0 +1,25 @@ + + + + + + kafka重复消费问题处理 | 故事 + + + + + + + + + + + + + + +
Skip to content

kafka重复消费问题处理

背景

最近在支援开发公司的财务对账系统,其中涉及一个功能点是某个对账单创建开票申请后,需要更新对账单的开票申请状态(未申请,部分申请,全部申请),这里使用了kafka,当创建开票申请后生产者即发送一条消息,消费者再去进行消费。

然后就碰到个问题,生产者这里是有事务的,因此会出现事务还没提交,开票申请数据还没有写入到数据库中,但是消息已经发送到kafka了,此时消费者直接消费消息时是查询不到这批开票申请数据的,就会出现创建开票申请业务实际成功了,但是对账单的开票申请状态仍为未申请。所以用了一个笨方法解决这个问题,在消费者线程中sleep(5000),来确保消费到消息时生产者的事务已经提交。

在测试环境测试少量账单&开票申请数据时是完全没有问题的,但是上了生产之后会有批量的账单数据推送过来,创建了大量的开票申请。这时候就发现异常情况了:消息队列中堆积了大量的消息,虽然日志一直还在正常跑,但是offset一直没变,并且过一段时间还会重复消费已经跑过日志的消息。

重复消费的原因

消费重复消费的根本原因都是:已经消费了数据,但是offset并没有提交。

网上找到的资料里有一段这么写的:

kafka消息重复消费很大一部分原因在于发生了再均衡。

1)消费者宕机、重启等。导致消息已经消费但是没有提交offset。

2)消费者使用自动提交offset,但当还没有提交的时候,有新的消费者加入或者移除,发生了rebalance。再次消费的时候,消费者会根据提交的偏移量来,于是重复消费了数据。

3)消息处理耗时,或者消费者拉取的消息量太多,处理耗时,超过了max.poll.interval.ms的配置时间,导致认为当前消费者已经死掉,触发再均衡。

日志:

txt
This member will leave the group because consumer poll timeout has expired. This means the time between subsequent calls to poll() was longer than the configured max.poll.interval.ms, which typically implies that the poll loop is spending too much time processing messages. You can address this either by increasing max.poll.interval.ms or by reducing the maximum size of batches returned in poll() with max.poll.records.
This member will leave the group because consumer poll timeout has expired. This means the time between subsequent calls to poll() was longer than the configured max.poll.interval.ms, which typically implies that the poll loop is spending too much time processing messages. You can address this either by increasing max.poll.interval.ms or by reducing the maximum size of batches returned in poll() with max.poll.records.

这里我碰到的情况就是第三种,因为消费者consumer每次拉取的消息较多,且有线程sleep的操作,所以导致处理超时了,超过max.poll.interval.ms的时间就会触发rebalance,然后就会重新分配消费者再次消费上次拉取的那批数据,这就是重复消费的原因。

处理方案

了解了超时的原因,就好解决了,根据日志中的提示,我们可以调整max.poll.interval.msmax.poll.records两个参数

  1. 增加max.poll.interval.ms的值,默认值为300000,单位是ms,即5分钟,可以调整为10分钟
  2. 减少max.poll.records的值,默认是500,可以调整为100
  3. 减少业务代码中线程休眠的时间

参考资料:

  1. https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html
  2. https://www.cnblogs.com/yangyongjie/p/14675119.html

Kafka知识回顾

1、消费者常见参数

①:fetch.min.bytes,配置Consumer一次拉取请求中能从Kafka中拉取的最小数据量,默认为1B,如果小于这个参数配置的值,就需要进行等待,直到数据量满足这个参数的配置大小。调大可以提交吞吐量,但也会造成延迟

②:fetch.max.bytes,一次拉取数据的最大数据量,默认为52428800B,也就是50M,但是如果设置的值过小,甚至小于每条消息的值,实际上也是能消费成功的

③:fetch.wait.max.ms,若是不满足fetch.min.bytes时,等待消费端请求的最长等待时间,默认是500ms

④:max.poll.records,单次poll调用返回的最大消息记录数,如果处理逻辑很轻量,可以适当提高该值。一次从kafka中poll出来的数据条数,max.poll.records条数据需要在在session.timeout.ms这个时间内处理完,默认值为500

⑤:consumer.poll(100) ,100 毫秒是一个超时时间,一旦拿到足够多的数据(fetch.min.bytes 参数设置),consumer.poll(100)会立即返回 ConsumerRecords<String, String> records。如果没有拿到足够多的数据,会阻塞100ms,但不会超过100ms就会返回

⑥:session. timeout. ms ,默认值是10s,该参数是 Consumer Group 主动检测 (组内成员comsummer)崩溃的时间间隔。若超过这个时间内没有收到心跳报文,则认为此消费者已经下线。将触发再均衡操作

⑦:max.poll.interval.ms,两次拉取消息的间隔,默认5分钟;通过消费组管理消费者时,该配置指定拉取消息线程最长空闲时间,若超过这个时间间隔没有发起poll操作,则消费组认为该消费者已离开了消费组,将进行再均衡操作(将分区分配给组内其他消费者成员)

若超过这个时间则报如下异常:

org.apache.kafka.clients.consumer.CommitFailedException: Commit cannot be completed since the group has already rebalanced and assigned the partitions to another member. This means that the time between subsequent calls to poll() was longer than the configured max.poll.interval.ms, which typically implies that the poll loop is spending too much time message processing. You can address this either by increasing the session timeout or by reducing the maximum size of batches returned in poll() with max.poll.records.
org.apache.kafka.clients.consumer.CommitFailedException: Commit cannot be completed since the group has already rebalanced and assigned the partitions to another member. This means that the time between subsequent calls to poll() was longer than the configured max.poll.interval.ms, which typically implies that the poll loop is spending too much time message processing. You can address this either by increasing the session timeout or by reducing the maximum size of batches returned in poll() with max.poll.records.

即:无法完成提交,因为组已经重新平衡并将分区分配给另一个成员。这意味着对poll()的后续调用之间的时间比配置的max.poll.interval.ms长,这通常意味着poll循环花费了太多的时间来处理消息。

可以通过增加max.poll.interval.ms来解决这个问题,也可以通过减少在poll()中使用max.poll.records返回的批的最大大小来解决这个问题

2、poll机制

①:每次poll的消息处理完成之后再进行下一次poll,是同步操作

②:每次poll之前检查是否可以进行位移提交,如果可以,那么就会提交上一次轮询的位移

③:每次poll时,consumer都将尝试使用上次消费的offset作为起始offset,然后依次拉取消息

④:poll(long timeout),timeout指等待轮询缓冲区的数据所花费的时间,单位是毫秒

3、再均衡 rebalance

将分区的所有权从一个消费者转移到其他消费者的行为称为再均衡(重平衡,rebalance)。

消费者通过向组织协调者(kafka broker)发送心跳来维护自己是消费者组的一员并确认其拥有的分区。对于不同不的消费群体来说,其组织协调者可以是不同的。只要消费者定期发送心跳,就会认为 消费者是存活的并处理其分区中的消息。当消费者检索记录或者提交它所消费的记录时就会发送心跳。

如果过了一段时间Kafka停止发送心跳了,会话(session)就会过期,组织协调者就会认为这个consumer已经死亡,就会触发一次重平衡。如果消费者宕机并且停止发送消息,组织协调者会等待几秒钟,确认它死亡了才会触发重平衡。在这段时间里,死亡的消费者将不处理任何消息。在清理消费者时,消费者将通知协调者它要离开群组,组织协调者会触发一次重平衡,尽量降低处理停顿。

重平衡是一把双刃剑,它为消费者群组带来高可用性和伸缩性的同时,还有有一些明显的缺点(bug),而这些 bug 到现在社区还无法修改。也就是说,在重平衡期间,消费者组中的消费者实例都会停止消费(Stop The World),等待重平衡的完成。而且重平衡这个过程很慢。

触发再均衡的情况:

①:有新的消费者加入消费组、或已有消费者主动离开组

②:消费者超过session时间未发送心跳(已有 consumer 崩溃了)

③:一次poll()之后的消息处理时间超过了max.poll.interval.ms的配置时间,因为一次poll()处理完才会触发下次poll() (已有 consumer 崩溃了)

④:订阅主题数发生变更

⑤:订阅主题的分区数发生变更

三、重复消费的解决方案

由于网络问题,重复消费不可避免,因此,消费者需要实现消费幂等。

方案:

①:消息表

②:数据库唯一索引

③:缓存消费过的消息id

四、项目kafka重复消费的排查

重复消费问题1:

每次拉取的消息记录数max.poll.records为100,poll最大拉取间隔max.poll.interval.ms为 300s,消息处理过于耗时导致时长大于了这个值,导致再均衡发生重复消费

解决办法:

①:减少每次拉取的消息记录数和增大poll之间的时间间隔

②:拉取到消息之后异步处理(保证成功消费)

+ + + + \ No newline at end of file diff --git "a/java/middleware/\345\205\263\344\272\216\346\266\210\346\201\257\344\270\255\351\227\264\344\273\266MQ.html" "b/java/middleware/\345\205\263\344\272\216\346\266\210\346\201\257\344\270\255\351\227\264\344\273\266MQ.html" new file mode 100644 index 000000000..e7d12c3c2 --- /dev/null +++ "b/java/middleware/\345\205\263\344\272\216\346\266\210\346\201\257\344\270\255\351\227\264\344\273\266MQ.html" @@ -0,0 +1,83 @@ + + + + + + 关于消息中间件MQ | 故事 + + + + + + + + + + + + + + +
Skip to content

关于消息中间件MQ

本文以RabbitMQ为例

1.为什么要使用MQ

这个问题也可以理解为MQ的作用,MQ的作用:

  • 异步:系统A中产生的一个数据,另外的系统BCD都需要对数据进行操作,不引入MQ时可以用A依次调用BCD的接口进行数据处理,这也就会耗费大量的时间,对于前台是无法接受的.如果引入MQ,可以将A系统的数据写入MQ,其他系统分别去消费数据,可以大大节省时间,优化体验
  • 解耦:如上面所说的,不使用MQ时,需要在A系统的代码里分别调用BCD的接口,如果BCD的服务宕机就会对A系统产生影响,又或者BCD系统如果后期不需要这个数据了,那就要删除A系统中对应的代码,如果要增加E服务处理A的数据,那又要增加相应的E系统的代码,耦合严重.如果引入MQ,系统中不会存在太大影响,就算其他系统宕机,也不会对A产生影响
  • 削峰:在高并发的情况下,如秒杀抢购活动,会在短时间内有大量请求涌入,如果流量太大,超过了系统的处理能力,可能就会导致我们的系统,数据库崩溃,可以将用户请求写入MQ,按照系统最大承载能力去处理请求,超过一定的阈值就将请求丢弃或给出错误提示

2.消息队列的优缺点

优点:

  • 对结构复杂的操作进行解耦,降低了系统的维护成本
  • 对一个可以异步进行的操作进行异步化,可以减少响应时间
  • 对高并发请求进行削峰,保证系统稳定性

缺点:

  • 系统复杂度提高。需要考虑MQ的各种情况,如消息丢失,重复消费,顺序消费等

  • 一致性问题。如A系统返回了成功的结果,BC系统成功了但D系统失败了

  • 系统可用性问题。如果MQ宕机,可能会导致系统的崩溃

3.如何保证消息队列高可用

RabbitMQ有三种模式:单机、普通集群、镜像集群

**普通集群:**就是在多台服务器上启动多个rabbitmq实例,但是创建的队列只会放在一个rabbitmq实例中,其他的实例会同步这个队列的元数据。消费的时候如果连接了另一个实例,也会从拥有队列的那个实例获取消息然后返回。

84949674832d2e63865764d.webp

这种方案并不能做到高可用

**镜像集群:**真正的高可用模式,创建的queue无论元数据还是消息数据都存放在多个实例中,每次写消息到queue时,都会自动把消息同步到多个queue中。

84949673a4af86b205cebcf.webp

优点:实现了高可用,任何一台机器宕机,其他机器能继续使用

缺点:1、性能消耗较大,所有机器都要进行消息同步 2、没有扩展性,如果有一个queue负载很重,就算增加机器,新增的机器也包含这个queue的全部数据,

4.如何保证消息不重复消费

保证消费的幂等性,让每条消息带一个全局唯一的bizId,具体过程:

1、消费者获取消息后先根据redis/db是否有该消息

2、如果不存在,则正常消费,消费完毕后写入redis/db

3、如果已经存在,证明已经消费过,直接丢弃

5.如何保证消息不丢失

原则:数据不能多也不能少,不能多是指不重复消费,不能少是指不能丢数据

丢失数据场景:

  • 生产者丢失数据:生产者发送数据到mq时可能因为网络波动丢失数据
  • rabbitmq丢失数据:如果没有开启rabbitmq持久化,一旦mq重启,数据就丢了
  • 消费者丢失数据:消费者刚消费到还没开始处理,消费者就挂掉了,重启后mq就认为已经消费过了,丢掉了数据

解决方案:

针对生产者丢失数据:

  • rabbitmq事务,生产者发送消息前开启事务,如果消息没有发送成功生产者会收到异常报错,这时可以回滚并重试发送
java
channel.txSelect();
+try{
+  //发送消息
+}catch(Exception e){
+  channel.rollback();
+  //重新发送
+}
channel.txSelect();
+try{
+  //发送消息
+}catch(Exception e){
+  channel.rollback();
+  //重新发送
+}

**缺点:**开启事务会变成阻塞操作,造成生产者的性能和吞吐量的下降

  • 把channel设置成confirm模式,每次写的消息都会分配一个唯一的id,如果mq接到消息就会回调生产者的接口,通知消息已经收到,如果mq接受报错,也会回调通知,这样可以重试发送数据,伪代码如下
java
//开启confirm模式
+channel.confirm();
+//发送消息
+
+在生产者服务提供一个回调接口的实现
+
+public void ack(String messageId){
+	//已经收到消息
+}
+
+public void nack(String messageId){
+    //重发消息
+}
//开启confirm模式
+channel.confirm();
+//发送消息
+
+在生产者服务提供一个回调接口的实现
+
+public void ack(String messageId){
+	//已经收到消息
+}
+
+public void nack(String messageId){
+    //重发消息
+}

**针对mq丢失数据:**开启mq的持久化,将交换机/队列的durable设置为true,表示交换机/队列时持久化的,在服务崩溃或重启后无需重新创建

java
@RabbitListener(
+     bindings = {
+        @QueueBinding(
+            value = @Queue(value = "dynamicQueue", autoDelete = "false", durable = "true"),
+            exchange = @Exchange(value = "exchange", durable = "true", type = ExchangeTypes.DIRECT),
+            key = "routingKey"
+        )
+    }
+)
+public void dynamicQueue(Message message, Channel channel) {
+        System.out.println("接收消息:" + new String(message.getBody()));
+}
@RabbitListener(
+     bindings = {
+        @QueueBinding(
+            value = @Queue(value = "dynamicQueue", autoDelete = "false", durable = "true"),
+            exchange = @Exchange(value = "exchange", durable = "true", type = ExchangeTypes.DIRECT),
+            key = "routingKey"
+        )
+    }
+)
+public void dynamicQueue(Message message, Channel channel) {
+        System.out.println("接收消息:" + new String(message.getBody()));
+}

如果消息想从rabbitmq崩溃中回复,消息必须实现:

  • 消息发送前,把投递模式设置为2(持久)来标记为持久消息
  • 将消息发送到持久交换机
  • 将消息发送到持久队列

针对消费者丢失数据:关闭消费者的autoAck机制,然后每次处理完一条消息,主动发送ack给rabbitmq,如果此时还没发送ack就宕机,mq没有收到ack消息,就会重新将消息重新分配给其他

强制消费者手动确认:

yml
spring.rabbitmq.listener.simple.acknowledge-mode: manual
spring.rabbitmq.listener.simple.acknowledge-mode: manual

消费者手动ack:

java
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);

6.如何保证消息的顺序消费

一个queue一个consumer

7.消息积压

1.先修复consumer的问题,确保恢复消费速度,然后停掉所有consumer

2.临时建立数十倍的queue

3.写一个临时分发的consumer程序,部署上去消费积压的消息,消费不做处理,直接轮询写入上一步建好的queue中

4.重新部署consumer(机器加倍),每一批consumer消费一个临时queue

+ + + + \ No newline at end of file diff --git "a/java/middleware/\345\210\206\345\270\203\345\274\217\351\224\201\350\247\243\345\206\263\346\226\271\346\241\210.html" "b/java/middleware/\345\210\206\345\270\203\345\274\217\351\224\201\350\247\243\345\206\263\346\226\271\346\241\210.html" new file mode 100644 index 000000000..caffa832f --- /dev/null +++ "b/java/middleware/\345\210\206\345\270\203\345\274\217\351\224\201\350\247\243\345\206\263\346\226\271\346\241\210.html" @@ -0,0 +1,167 @@ + + + + + + 分布式锁解决方案 | 故事 + + + + + + + + + + + + + + +
Skip to content

分布式锁解决方案

分布式锁的概念

在单体应用中,对于并发处理公共资源时例如卖票,减商品库存这类操作,可以简单的加锁实现同步.但当单体应用服务化后,在分布式场景下,简单的加锁操作就无法实现需求了.这时就需要借助第三方组件来达到多进程多线程之间的同步操作.

分布式锁:在分布式环境下,多个程序/线程都需要对某一份(有限份)数据进行修改时,针对程序进行控制,保证在同一时间节点下,只有一个程序/线程对数据进行操作的技术

分布式锁的执行流程

Snipaste_20210305_152634.png

常见的解决方案:

  • 基于数据库的唯一索引
  • 基于数据库的排他锁
  • 基于Redis的EX NX参数
  • 基于ZooKeeper的临时有序节点

基于数据库的唯一索引

步骤:

1.创建一张表,表中要有唯一索引的字段用于记录当前哪个程序在进行操作

2.程序访问时,将程序的编号insert到这张表中(保证这个编号符合规则可以区分)

3.insert操作成功则代表该程序获得了锁,可以执行业务逻辑

4.当其他相同编号的程序进行insert时,由于唯一索引的限制会失败,则代表获取锁失败

5.当占用锁的程序业务逻辑执行完毕后删除该数据,代表释放锁

基于数据库的排他锁

以mysql为例的innodb引擎为例,可以使用for update语句来给数据库表加排他锁,当一个线程执行了for update操作给一条记录加锁后,其他线程无法再在该记录上增加排他锁

步骤:

1.开启事务

2.在查询语句后跟for update 例如 select * from tb_goods where id=1 for update

3.成功获取排他锁的线程即获得了分布式锁,可以执行业务逻辑

4.执行完毕后需要commit提交事务来释放锁

==这种方式需要关闭数据库事务的自动提交==

以上两种通过数据库来实现分布式锁的方案比较==简单方便,可以快速实现==,但是基于数据库的操作,开销非常大,对服务的性能存在影响

基于Redis实现分布式锁

redis实现分布式锁最核心的方法setnx,setnx的含义就是set if not exists,其中有两个参数setnx(key,value).该方法是原子的,如果key不存在,则设置当前key成功,返回1,如果key已存在,则设置当前key失败,返回0;

Snipaste_20210305_161717.png

锁超时

理想情况下,当某个程序抢占了锁后,处理完业务流程应该删除对应的key,如果这个过程中发生了问题,导致锁超时或者出现了异常,没有办法释放锁,就会产生死锁问题.

redis提供的另一个指令EXPIRE,来设置锁的过期时间EXPIRE KEY seconds来设置key的生存时间,如图

Snipaste_20210305_162330.png

两秒以后key就被自动删除了.

但是程序里我们也不能写成如下的代码

java
public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {
+ 
+    Long result = jedis.setnx(lockKey, requestId);
+    if (result == 1) {
+        jedis.expire(lockKey, expireTime);
+        doSomething();
+    }
+}
public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {
+ 
+    Long result = jedis.setnx(lockKey, requestId);
+    if (result == 1) {
+        jedis.expire(lockKey, expireTime);
+        doSomething();
+    }
+}

因为setnx和expire两个指令是两条命令并不具有原子性,如果在执行setnx后程序突然崩溃,没有设置过期时间就会发生死锁.

在redis2.6.12版本后,又提供了一个新的方案

SET key value [EX seconds] [PX millisecounds] [NX|XX] EX seconds:设置键的过期时间为second秒 PX millisecounds:设置键的过期时间为millisecounds 毫秒 NX:只在键不存在的时候,才对键进行设置操作 XX:只在键已经存在的时候,才对键进行设置操作 SET操作成功后,返回的是OK,失败返回NIL

Snipaste_20210307_145207.png

如何释放锁

释放锁的时候很容易联想到del指令,可是这个指令如果直接在代码中使用,会产生一个很严重的问题,拥有超时锁的线程会释放掉当前拥有锁的那个线程的锁

(删了不属于自己的锁)

场景:

  • 线程1,2,3分别尝试加锁最后只有1成功获得锁,其他线程加锁失败继续尝试
  • 线程1开始执行业务代码,线程2,3继续尝试
  • 线程1的锁超时了,自动释放了锁,此时线程2获得了锁,线程3继续尝试加锁
  • 线程1的任务执行完毕,使用了del指令删除锁,线程2在执行业务代码,线程3这时会获得锁(因为线程1把线程2的锁删了)

所以,我们要在set键值对的时候,保证可以区分开当前锁是否属于执行指令的这个线程,value我们可以设置为当前的requestID或线程的id

而这些步骤如果直接用代码控制则会显得较为繁琐,可以引入lua脚本

lua
if redis.call('get', KEYS[1]) == ARGV[1] 
+    then 
+	    return redis.call('del', KEYS[1]) 
+	else 
+	    return 0 
+end
if redis.call('get', KEYS[1]) == ARGV[1] 
+    then 
+	    return redis.call('del', KEYS[1]) 
+	else 
+	    return 0 
+end

上述lua脚本用于比较KEYS[1]对应的VALUE和ARG[1]的值是否一直,即当前锁是否属于当前线程,如果是true则删除锁,返回del结果,否则直接返回0

执行脚本可以使用redis的eval指令

java
jedis.eval(String script, List<String> kyes,List<String>args);
jedis.eval(String script, List<String> kyes,List<String>args);

存在的问题:上面讨论的是单机redis的场景,如果是分布式下的redis哨兵集群会存在问题,如果线程1的锁加在了主库,这时候主库直接宕机,redis还没来得及将这个锁的数据同步至从节点,sentinel就将从库中的一台选举为主库了,这时候另一个线程也来进行加锁,也会成功,这时候便有两个线程同时获得了锁

这种情况可以使用Redisson redlock

java
Config config = new Config();
+config.useSentinelServers().addSentinelAddress("127.0.0.1:6369","127.0.0.1:6379", "127.0.0.1:6389")
+		.setMasterName("masterName")
+		.setPassword("password").setDatabase(0);
+RedissonClient redissonClient = Redisson.create(config);
+RLock redLock = redissonClient.getLock("REDLOCK_KEY");
+boolean isLock;
+try {
+	isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
+	if (isLock) {
+		//TODO if get lock success, do something;
+	}
+} catch (Exception e) {
+} finally {
+	redLock.unlock();
+}
Config config = new Config();
+config.useSentinelServers().addSentinelAddress("127.0.0.1:6369","127.0.0.1:6379", "127.0.0.1:6389")
+		.setMasterName("masterName")
+		.setPassword("password").setDatabase(0);
+RedissonClient redissonClient = Redisson.create(config);
+RLock redLock = redissonClient.getLock("REDLOCK_KEY");
+boolean isLock;
+try {
+	isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
+	if (isLock) {
+		//TODO if get lock success, do something;
+	}
+} catch (Exception e) {
+} finally {
+	redLock.unlock();
+}

由于 redisson 包的实现中,通过 lua 脚本校验了解锁时的 client 身份,所以我们无需再在 finally 中去判断是否加锁成功,也无需做额外的身份校验,可以说已经达到开箱即用的程度了。

基于ZooKeeper实现分布式锁

  • zookeeper一般有多个节点构成(单数个),采用zab一致性协议,所以可以将zk看成一个单点结构,对其修改数据其内部会自动将所有节点进行修改才提供查询服务

  • zookeeper的数据是目录树的方式存储的,每个目录称为znode,znode可以存储数据,还可以在其中增加子节点

  • 子节点有三种类型

    • 序列化节点:每在该节点下增加一个节点会自动给节点的名字自增
    • 临时节点:一旦创建这个节点的客户端与服务器失去联系会自动删除节点
    • 普通节点
  • Watch机制,客户端可以监控每个节点的变化,产生变化时会给客户端产生一个事件

zookeeper分布式锁原理

  • 获取和释放锁:利用临时节点的特性和watch机制,每个锁占用一个普通节点/lock,当需要获取锁时在/lock目录下创建一个临时节点,创建成功表示获取锁成功,失败则watch /lock节点,有删除操作时再去竞争锁。临时节点的好处在于当进程挂掉后自动上锁的节点会自动删除,即释放锁.
  • 获取锁的顺序:上锁为创建临时有序节点,每个上锁的节点均能创建节点成功,只是序号不同,只有序号最小的才能拥有锁,如果节点序号不是最小则watch序号比自身小的前一个节点(公平锁)

获取锁的流程:

1.先有一个锁根节点,lockRootNode,可以是一个永久节点

2.客户端获取锁,先在lockRootNode下创建一个顺序的临时节点

3.调用lockRootNode节点的getChildren()方法获取所有节点,并从小到大排序,如果创建的最小的节点是当前节点,则返回true,获取锁成功,否则,watch比自己序号小的节点的释放动作(exist watch),这也可以保证每个客户端只需要watch一个节点

4.如果有节点释放操作,重复第3步

代码中可以使用Apache Curator来实现基于临时节点的分布式锁

java
public class CuratorDistrLockTest {
+
+    /** Zookeeper info */
+    private static final String ZK_ADDRESS = "192.168.1.100:2181";
+    private static final String ZK_LOCK_PATH = "/lockRootNode";
+
+    public static void main(String[] args) throws InterruptedException {
+        // 1.Connect to zk
+        CuratorFramework client = CuratorFrameworkFactory.newClient(
+                ZK_ADDRESS,
+                new RetryNTimes(10, 5000)
+        );
+        client.start();
+        System.out.println("zk client start successfully!");
+
+        Thread t1 = new Thread(() -> {
+            doWithLock(client);
+        }, "t1");
+        Thread t2 = new Thread(() -> {
+            doWithLock(client);
+        }, "t2");
+
+        t1.start();
+        t2.start();
+    }
+
+    private static void doWithLock(CuratorFramework client) {
+        InterProcessMutex lock = new InterProcessMutex(client, ZK_LOCK_PATH);
+        try {
+            if (lock.acquire(10 * 1000, TimeUnit.SECONDS)) {
+                System.out.println(Thread.currentThread().getName() + " hold lock");
+                Thread.sleep(5000L);
+                System.out.println(Thread.currentThread().getName() + " release lock");
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                lock.release();
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+    }
+}
public class CuratorDistrLockTest {
+
+    /** Zookeeper info */
+    private static final String ZK_ADDRESS = "192.168.1.100:2181";
+    private static final String ZK_LOCK_PATH = "/lockRootNode";
+
+    public static void main(String[] args) throws InterruptedException {
+        // 1.Connect to zk
+        CuratorFramework client = CuratorFrameworkFactory.newClient(
+                ZK_ADDRESS,
+                new RetryNTimes(10, 5000)
+        );
+        client.start();
+        System.out.println("zk client start successfully!");
+
+        Thread t1 = new Thread(() -> {
+            doWithLock(client);
+        }, "t1");
+        Thread t2 = new Thread(() -> {
+            doWithLock(client);
+        }, "t2");
+
+        t1.start();
+        t2.start();
+    }
+
+    private static void doWithLock(CuratorFramework client) {
+        InterProcessMutex lock = new InterProcessMutex(client, ZK_LOCK_PATH);
+        try {
+            if (lock.acquire(10 * 1000, TimeUnit.SECONDS)) {
+                System.out.println(Thread.currentThread().getName() + " hold lock");
+                Thread.sleep(5000L);
+                System.out.println(Thread.currentThread().getName() + " release lock");
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                lock.release();
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+    }
+}

优缺点:

优点:

  • 客户端如果宕机,可以立即释放锁
  • 集群模式的稳定性高
  • 可以通过watch机制实现阻塞锁

缺点:

  • 一旦出现网络抖动,zk就会认为客户端挂掉并断掉连接,其他客户端就会获取锁
  • 性能不高,每次获取锁和释放锁都基于创建删除临时节点,zk创建和删除节点只能通过leader服务器进行,然后再同步至所有follower上
+ + + + \ No newline at end of file diff --git "a/java/others/OpenJDK\346\262\241\346\234\211jstack\347\255\211\345\221\275\344\273\244\347\232\204\350\247\243\345\206\263\345\212\236\346\263\225.html" "b/java/others/OpenJDK\346\262\241\346\234\211jstack\347\255\211\345\221\275\344\273\244\347\232\204\350\247\243\345\206\263\345\212\236\346\263\225.html" new file mode 100644 index 000000000..f0df4cca1 --- /dev/null +++ "b/java/others/OpenJDK\346\262\241\346\234\211jstack\347\255\211\345\221\275\344\273\244\347\232\204\350\247\243\345\206\263\345\212\236\346\263\225.html" @@ -0,0 +1,25 @@ + + + + + + OpenJDK没有jstack等命令的解决办法 | 故事 + + + + + + + + + + + + + + +
Skip to content

OpenJDK没有jstack等命令的解决办法

使用yum list --showduplicate | grep java-1.8 找出相关的软件版本

yum install 安装图中这两个软件即可jstack.png

+ + + + \ No newline at end of file diff --git "a/java/others/POI\350\270\251\345\235\221-zip file is closed.html" "b/java/others/POI\350\270\251\345\235\221-zip file is closed.html" new file mode 100644 index 000000000..fffa3cec0 --- /dev/null +++ "b/java/others/POI\350\270\251\345\235\221-zip file is closed.html" @@ -0,0 +1,39 @@ + + + + + + POI踩坑-zip file is closed | 故事 + + + + + + + + + + + + + + +
Skip to content

POI踩坑-zip file is closed

前几天做一个大批量数据异步的导入,采用的是之前用过的事件模式处理,大致流程是用户上传excel文件后,主线程对excel表头进行校验,如果通过校验,则开辟子线程进行业务处理,主线程返回响应.

整个导入流程和原来做的没什么区别,就是这次整个业务流程都放在子线程中进行处理,原来做的那个导入是只是主线程读数据,读到设定的阈值时则新开线程进行数据的持久化.

之前的那个系统做的功能是没有什么问题的,只是前台需要等待主线程读取完数据,后台在异步的分批插入数据到db,主线程读取数据这个过程前端页面一直在loading,体验不是很好. 这次为了优化用户体验,整个读取和业务流程操作都放在子线程中,并且新增了进度条展示,但是出现了标题的Zip File is closed异常,但不能完全重现,时而出现,时而正常.完整堆栈信息如下:

java
Exception in thread "main" java.lang.IllegalStateException: Zip File is closed
+	at org.apache.poi.openxml4j.util.ZipFileZipEntrySource.getEntries(ZipFileZipEntrySource.java:45)
+	at org.apache.poi.openxml4j.opc.ZipPackage.getPartsImpl(ZipPackage.java:161)
+	at org.apache.poi.openxml4j.opc.OPCPackage.getParts(OPCPackage.java:662)
+	at org.apache.poi.openxml4j.opc.OPCPackage.open(OPCPackage.java:223)
+	at org.apache.poi.openxml4j.opc.OPCPackage.open(OPCPackage.java:186)
+	at com.test.util.POIEventModeHandler.handleExcel(POIEventModeHandler.java:482)
+	at com.test.util.Test.main(Test.java:20)
Exception in thread "main" java.lang.IllegalStateException: Zip File is closed
+	at org.apache.poi.openxml4j.util.ZipFileZipEntrySource.getEntries(ZipFileZipEntrySource.java:45)
+	at org.apache.poi.openxml4j.opc.ZipPackage.getPartsImpl(ZipPackage.java:161)
+	at org.apache.poi.openxml4j.opc.OPCPackage.getParts(OPCPackage.java:662)
+	at org.apache.poi.openxml4j.opc.OPCPackage.open(OPCPackage.java:223)
+	at org.apache.poi.openxml4j.opc.OPCPackage.open(OPCPackage.java:186)
+	at com.test.util.POIEventModeHandler.handleExcel(POIEventModeHandler.java:482)
+	at com.test.util.Test.main(Test.java:20)

追踪源码后发现,出现这个异常时,读取的zipentry为null,简单点说就是读了个不存在的文件.

然后我才想起来项目里文件上传有个对应的拦截器,请求过来之后,拦截器会把前台传的文件先保存在服务器的临时目录中,我们的controller里拿到的文件路径封装的是服务器上的临时文件目录,而不是前台那个MultipartFile的,在controller响应的时候,这个拦截器又把临时文件删除了.所以我这个问题就是线程执行的随机性导致的,正常的时候先执行了子线程中读取临时文件的方法,此时再切换到主线程返回响应时,拦截器无法删除这个文件,而如果一直先执行的是主线程,拦截器会先把文件删掉,这个时候子线程执行open那个临时文件就会出现zip file is closed的异常.

由于拦截器是公用的,而且原来的逻辑是响应后删除临时文件避免服务器磁盘占用,所以处理方案就是主线程在新建子线程任务之前把这个临时文件再copy一份让子线程去读取这个copy,子线程的finally中再把这个copy的文件删除即可.

  • 补充 在搜这个问题的时候还看到有人直接用request中的multipartfile进行读取也会报这个错的情况,原因是客户端的文件路径poi没法直接读到,但是还有人直接用poi处理inputStream也会报错,所以推荐大家在使用poi事件模式读取前台上传的文件时,先将文件保存在服务器端,然后去读取服务器上的文件,处理完后再删除即可.
+ + + + \ No newline at end of file diff --git "a/java/others/cpu\345\215\240\347\224\250\347\216\207\351\253\230\346\216\222\346\237\245\346\200\235\350\267\257.html" "b/java/others/cpu\345\215\240\347\224\250\347\216\207\351\253\230\346\216\222\346\237\245\346\200\235\350\267\257.html" new file mode 100644 index 000000000..3fe5f8964 --- /dev/null +++ "b/java/others/cpu\345\215\240\347\224\250\347\216\207\351\253\230\346\216\222\346\237\245\346\200\235\350\267\257.html" @@ -0,0 +1,43 @@ + + + + + + cpu占用率高排查思路 | 故事 + + + + + + + + + + + + + + +
Skip to content

cpu占用率高排查思路

1.top命令找出cpu占用率高的进程pid

2.top -H -p pid 找出cpu占用率高的线程tid

3.printf "%x" tid命令打印出tid的十六进制形式

4.jstack pid | grep 十六进制tid -A 行数打印堆栈信息 或者 jstack pid >> log.txt将堆栈信息保存在文件中,再从文件中查找对应线程的信息

5.jstat -gcutil pid 5000 每隔5秒打印一次gc情况

S0:幸存1区当前使用比例
+S1:幸存2区当前使用比例
+E:伊甸园区使用比例
+O:老年代使用比例
+M:元数据区使用比例
+CCS:压缩使用比例
+YGC:年轻代垃圾回收次数
+FGC:老年代垃圾回收次数
+FGCT:老年代垃圾回收消耗时间
+GCT:垃圾回收消耗总时间
S0:幸存1区当前使用比例
+S1:幸存2区当前使用比例
+E:伊甸园区使用比例
+O:老年代使用比例
+M:元数据区使用比例
+CCS:压缩使用比例
+YGC:年轻代垃圾回收次数
+FGC:老年代垃圾回收次数
+FGCT:老年代垃圾回收消耗时间
+GCT:垃圾回收消耗总时间

6.jmap -heap pid 查看堆内存详细信息

7.jmap -histo pid > xxx.log 输出gc日志到文件

+ + + + \ No newline at end of file diff --git "a/java/others/feign\350\257\267\346\261\202\345\257\274\350\207\264\347\232\204\347\224\250\346\210\267ip\350\216\267\345\217\226\351\227\256\351\242\230\350\256\260\345\275\225.html" "b/java/others/feign\350\257\267\346\261\202\345\257\274\350\207\264\347\232\204\347\224\250\346\210\267ip\350\216\267\345\217\226\351\227\256\351\242\230\350\256\260\345\275\225.html" new file mode 100644 index 000000000..cdadac445 --- /dev/null +++ "b/java/others/feign\350\257\267\346\261\202\345\257\274\350\207\264\347\232\204\347\224\250\346\210\267ip\350\216\267\345\217\226\351\227\256\351\242\230\350\256\260\345\275\225.html" @@ -0,0 +1,153 @@ + + + + + + feign请求导致的用户ip获取问题记录 | 故事 + + + + + + + + + + + + + + +
Skip to content

feign请求导致的用户ip获取问题记录

背景

有个功能需要做ip的判断,区分请求是来自用户端还是来自其他服务的feign调用,其中有个一个服务发起的feign调用,一直无法获取到真实的服务器ip,而一直是用户端的ip。

排查

  1. 首先贴出来获取ip的工具类
java
public static String getIpAddr(HttpServletRequest request) {
+        String ipAddress = request.getHeader("x-forwarded-for");
+        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getHeader("Proxy-Client-IP");
+        }
+        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getHeader("WL-Proxy-Client-IP");
+        }
+        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getRemoteAddr();
+            if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) {
+                //根据网卡取本机配置的IP
+                InetAddress inet = null;
+                try {
+                    inet = InetAddress.getLocalHost();
+                } catch (UnknownHostException e) {
+                    e.printStackTrace();
+                }
+                ipAddress = inet.getHostAddress();
+            }
+        }
+        //对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
+        if (ipAddress != null && ipAddress.length() > 15) { //"***.***.***.***".length() = 15
+            if (ipAddress.indexOf(",") > 0) {
+                ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
+            }
+        }
+        return ipAddress;
+    }
public static String getIpAddr(HttpServletRequest request) {
+        String ipAddress = request.getHeader("x-forwarded-for");
+        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getHeader("Proxy-Client-IP");
+        }
+        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getHeader("WL-Proxy-Client-IP");
+        }
+        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getRemoteAddr();
+            if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) {
+                //根据网卡取本机配置的IP
+                InetAddress inet = null;
+                try {
+                    inet = InetAddress.getLocalHost();
+                } catch (UnknownHostException e) {
+                    e.printStackTrace();
+                }
+                ipAddress = inet.getHostAddress();
+            }
+        }
+        //对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
+        if (ipAddress != null && ipAddress.length() > 15) { //"***.***.***.***".length() = 15
+            if (ipAddress.indexOf(",") > 0) {
+                ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
+            }
+        }
+        return ipAddress;
+    }
  1. 根据调试发现,请求的``x-forwarded-for`请求头是一直包含两个ip的,第一个ip是用户端的ip,第二个是nginx所在的服务器ip,这个也好理解,通过代理转发的请求,会把代理ip拼在真实ip后面。但是问题是这个请求是由其他服务feign发起的,理应不存在nginx转发(通过注册中心的服务名调用,绕过nginx)。

HTTP 请求头中的 X-Forwarded-For

Nginx remote_addr和proxy_add_x_forwarded_for变量详解]

  1. 把问题转到排查feign调用上后发现,该服务实例化了feign.builder来实现传递自定义请求头的目标
java
@Bean
+	public Feign.Builder feignBuilder() {
+		return Feign.builder().requestInterceptor(new RequestInterceptor() {
+			@Override
+			public void apply(RequestTemplate requestTemplate) {
+				Map<String, String> customHeaders = WebUtils.getCustomHeaders();
+				customHeaders.forEach((k, v) -> {
+					requestTemplate.header(k, v);
+				});
+			}
+		});
+	}
+
+---
+  
+public static Map<String, String> getCustomHeaders() {
+        Map<String, String> headers = new HashMap();
+        HttpServletRequest request = RequestContextHelper.getRequest();
+        Enumeration<String> headerNames = request.getHeaderNames();
+
+        while(headerNames.hasMoreElements()) {
+            String headerName = ((String)headerNames.nextElement()).toLowerCase();
+            if (headerName.startsWith("x-")) {
+                String headerValue = request.getHeader(headerName);
+                if (headerValue != null) {
+                    headers.put(headerName, headerValue);
+                }
+            }
+        }
+
+        headers.put("x-invoker-ip", IpUtils.getLocalIpAddr());
+        if (!headers.containsKey("x-auth-token")) {
+            headers.put("x-auth-token", TokenGenerator.generateWithSign());
+        }
+
+        return headers;
+    }
@Bean
+	public Feign.Builder feignBuilder() {
+		return Feign.builder().requestInterceptor(new RequestInterceptor() {
+			@Override
+			public void apply(RequestTemplate requestTemplate) {
+				Map<String, String> customHeaders = WebUtils.getCustomHeaders();
+				customHeaders.forEach((k, v) -> {
+					requestTemplate.header(k, v);
+				});
+			}
+		});
+	}
+
+---
+  
+public static Map<String, String> getCustomHeaders() {
+        Map<String, String> headers = new HashMap();
+        HttpServletRequest request = RequestContextHelper.getRequest();
+        Enumeration<String> headerNames = request.getHeaderNames();
+
+        while(headerNames.hasMoreElements()) {
+            String headerName = ((String)headerNames.nextElement()).toLowerCase();
+            if (headerName.startsWith("x-")) {
+                String headerValue = request.getHeader(headerName);
+                if (headerValue != null) {
+                    headers.put(headerName, headerValue);
+                }
+            }
+        }
+
+        headers.put("x-invoker-ip", IpUtils.getLocalIpAddr());
+        if (!headers.containsKey("x-auth-token")) {
+            headers.put("x-auth-token", TokenGenerator.generateWithSign());
+        }
+
+        return headers;
+    }

至此,问题解决,x-forwarded-for被放在了feign请求头中,导致了上述问题,这里把x-forwarded-for请求头排除即可。

  1. 分析:

    请求流程: 用户->nginx->网关服务->a服务->feign调用->b服务

用户的请求通过nignx后代理ip就会被添加到x-forwarded-for请求头中,此时x-forwarded-for请求头为:用户ip,nginx服务器的ip,后请求从网关路由到a服务,a服务通过feign.buider的添加拦截器方法,增加了一个添加指定请求头到feign的请求中的拦截器,导致原来的x-forwarded-for被原封不动的传到了b服务,b服务根据这个请求头获取ip时,就拿到了原始用户的ip。

+ + + + \ No newline at end of file diff --git "a/java/others/linux\346\234\215\345\212\241\345\231\250\345\256\211\350\243\205OpenOffice\350\270\251\345\235\221.html" "b/java/others/linux\346\234\215\345\212\241\345\231\250\345\256\211\350\243\205OpenOffice\350\270\251\345\235\221.html" new file mode 100644 index 000000000..afc916e29 --- /dev/null +++ "b/java/others/linux\346\234\215\345\212\241\345\231\250\345\256\211\350\243\205OpenOffice\350\270\251\345\235\221.html" @@ -0,0 +1,25 @@ + + + + + + linux服务器安装OpenOffice踩坑 | 故事 + + + + + + + + + + + + + + +
Skip to content

linux服务器安装OpenOffice踩坑

背景

最近公司项目需要做一个在线预览Office文件的功能,尝试了使用OpenOffice把Office文档转化成PDF格式和HTML格式的文件再由前端解析PDF或者直接通过iFrame访问HTML文件的方案,windows系统直接下载安装后cmd运行命令就可以启动openoffice的服务了.服务器就稍微麻烦一点,本文记录了我在自己的阿里云服务器上安装OpenOffice遇到的坑和解决的办法. 解压安装OpenOffice,这个网上搜一下有很多,不再详细记录.可以参考 linux环境下安装 openOffice 并启动服务

遇到的问题

  • 找不到Java运行环境 这个问题是因为我服务器上的JDK是解压版本的,虽然配置了JAVA_HOME这些但是openoffice识别不到,后来我就删了这个解压版的JDK,然后用yum命令安装了OpenJDK 1.8,命令:yum install openjdk 1.8重新配置了JAVA_HOME后解决了这个报错
  • 找不到libXext.so.6 /opt/openoffice4/program/下缺少libXext.so.6文件 运行yum install libXext.X86_64. 这个是64位linux的版本,32位的系统需要改成对应的版本
  • no suitable windowing system found existing 运行yum groupinstall "X Window System",我在运行这个命令的时候,又碰到了另一个报错:no packages in any requested group available to install or update, 这里命令后面要加上一些参数,执行yum groupinstall "X Window System" --setoptgroup_package_type=mandatory.default.optional

解决报错后,再重新执行启动服务的命令 nohup ./soffice -headless -accept="socket,host=127.0.0.1,port=8100;urp;" -nofirststartwizard &

执行后可以使用netstat命令查看8100端口的占用情况net stat -lnp|grep 8100 可以看到soffice.bin就说明服务成功启动了

然后启动测试的demo后发现,office文件转换成PDF后中文出现乱码,这是因为服务器上没有中文字体,用ftp工具把windows的中文字体直接传到服务器上的/usr/share/fonts文件夹中,清除缓存后重新启动openoffice服务后就能正确显示中文了.

+ + + + \ No newline at end of file diff --git "a/java/others/mybatis\347\232\204classpath\351\205\215\347\275\256\345\257\274\350\207\264\347\232\204jar\345\214\205\350\257\273\345\217\226\351\227\256\351\242\230.html" "b/java/others/mybatis\347\232\204classpath\351\205\215\347\275\256\345\257\274\350\207\264\347\232\204jar\345\214\205\350\257\273\345\217\226\351\227\256\351\242\230.html" new file mode 100644 index 000000000..0498959f3 --- /dev/null +++ "b/java/others/mybatis\347\232\204classpath\351\205\215\347\275\256\345\257\274\350\207\264\347\232\204jar\345\214\205\350\257\273\345\217\226\351\227\256\351\242\230.html" @@ -0,0 +1,25 @@ + + + + + + mybatis的classpath配置导致的jar包读取问题 | 故事 + + + + + + + + + + + + + + +
Skip to content

mybatis的classpath配置导致的jar包读取问题

今天同事碰到个问题,在服务中引入了另一个服务的mapper文件后找不到其中配置的resultMap引发报错。后经排查是因为配置文件中classpath的配置问题引起的。

classpath:: 只会从当前服务的class目录下寻找文件

classpath*:: 会从class目录下寻找文件,还会从引入的依赖(打包后lib文件夹中的jar包)中寻找文件

所以当把mybatis的classpath配置从classpath:改为classpath*:后问题能解决了

+ + + + \ No newline at end of file diff --git "a/java/others/ribbon\345\210\267\346\226\260\346\234\215\345\212\241\345\210\227\350\241\250\351\227\264\351\232\224\345\222\214canal\347\232\204\345\235\221.html" "b/java/others/ribbon\345\210\267\346\226\260\346\234\215\345\212\241\345\210\227\350\241\250\351\227\264\351\232\224\345\222\214canal\347\232\204\345\235\221.html" new file mode 100644 index 000000000..08801561f --- /dev/null +++ "b/java/others/ribbon\345\210\267\346\226\260\346\234\215\345\212\241\345\210\227\350\241\250\351\227\264\351\232\224\345\222\214canal\347\232\204\345\235\221.html" @@ -0,0 +1,93 @@ + + + + + + ribbon刷新服务列表间隔和canal的坑 | 故事 + + + + + + + + + + + + + + +
Skip to content

ribbon刷新服务列表间隔和canal的坑

ribbon有个参数可以用来调整刷新server list的时间间隔参数。

ServerListRefreshInterval

ribbon-loadbalancer-2.2.0-sources.jar!/com/netflix/client/config/CommonClientConfigKey.java

java
public static final IClientConfigKey<Integer> ServerListRefreshInterval = new CommonClientConfigKey<Integer>("ServerListRefreshInterval"){};
public static final IClientConfigKey<Integer> ServerListRefreshInterval = new CommonClientConfigKey<Integer>("ServerListRefreshInterval"){};

PollingServerListUpdater

ribbon-loadbalancer-2.2.0-sources.jar!/com/netflix/loadbalancer/PollingServerListUpdater.java

java
private static long getRefreshIntervalMs(IClientConfig clientConfig) {
+        return clientConfig.get(CommonClientConfigKey.ServerListRefreshInterval, LISTOFSERVERS_CACHE_REPEAT_INTERVAL);
+    }
+    
+    @Override
+    public synchronized void start(final UpdateAction updateAction) {
+        if (isActive.compareAndSet(false, true)) {
+            final Runnable wrapperRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    if (!isActive.get()) {
+                        if (scheduledFuture != null) {
+                            scheduledFuture.cancel(true);
+                        }
+                        return;
+                    }
+                    try {
+                        updateAction.doUpdate();
+                        lastUpdated = System.currentTimeMillis();
+                    } catch (Exception e) {
+                        logger.warn("Failed one update cycle", e);
+                    }
+                }
+            };
+ 
+            scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
+                    wrapperRunnable,
+                    initialDelayMs,
+                    refreshIntervalMs,
+                    TimeUnit.MILLISECONDS
+            );
+        } else {
+            logger.info("Already active, no-op");
+        }
+    }
private static long getRefreshIntervalMs(IClientConfig clientConfig) {
+        return clientConfig.get(CommonClientConfigKey.ServerListRefreshInterval, LISTOFSERVERS_CACHE_REPEAT_INTERVAL);
+    }
+    
+    @Override
+    public synchronized void start(final UpdateAction updateAction) {
+        if (isActive.compareAndSet(false, true)) {
+            final Runnable wrapperRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    if (!isActive.get()) {
+                        if (scheduledFuture != null) {
+                            scheduledFuture.cancel(true);
+                        }
+                        return;
+                    }
+                    try {
+                        updateAction.doUpdate();
+                        lastUpdated = System.currentTimeMillis();
+                    } catch (Exception e) {
+                        logger.warn("Failed one update cycle", e);
+                    }
+                }
+            };
+ 
+            scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
+                    wrapperRunnable,
+                    initialDelayMs,
+                    refreshIntervalMs,
+                    TimeUnit.MILLISECONDS
+            );
+        } else {
+            logger.info("Already active, no-op");
+        }
+    }

可以看到有个定时线程,每隔一定时间会去刷新服务的列表,因此,这个时间不要改的太长,默认应该是30s,公司的测试环境不知道是哪位大佬出于什么考虑改的,我反正已经改回去了 = =.

canal的坑

公司用canal主要是因为有新旧两个系统,两个系统同时在运转,目前还没有完全迁移,因此用canal保证新旧系统数据的一致性,这次主要是修复一个楼层信息不同步的bug,问题是更新的时候有个状态字段被写死了,没有用kafka里消息体里的数据更新,这个小问题上周就修复了,自测通过了的,这周测试复测结果发现什么操作都同步不了.这使我一度陷入了自我怀疑.... 然后去看监控的服务日志,发现确实是什么数据库的变更都检测不到了,因为不熟悉这套东西,看了半天也没看出来问题,只能求助我们头儿,然后据他说这个问题经常出现,然后就删了canal实例里面的meta.bat文件,然后运行./restart.sh,重启之后果然恢复正常了,具体啥原因我还没搞懂,日后研究明白了再补充

后续:canal提供了tsdb时序数据库,上次碰到的canal报错问题,是因为监控表的元数据跟现有数据不一致导致的,tsdb中会记录所有对监控的表的操作记录,但是有一张表加了两个字段(57+2)后,这两条alter语句不知道什么情况没有被记录到tsdb的history表中,导致监控到的表的列数不匹配,canal认为还是57列,实际为59,由于是测试环境,所以直接把这张表copy了一份然后把原表删了,再把copy的表改个名字就ok了

+ + + + \ No newline at end of file diff --git "a/java/others/\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217\345\212\240\345\257\206\346\225\260\346\215\256\345\257\271\347\247\260\350\247\243\345\257\206\345\267\245\345\205\267\347\261\273.html" "b/java/others/\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217\345\212\240\345\257\206\346\225\260\346\215\256\345\257\271\347\247\260\350\247\243\345\257\206\345\267\245\345\205\267\347\261\273.html" new file mode 100644 index 000000000..a33e87d15 --- /dev/null +++ "b/java/others/\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217\345\212\240\345\257\206\346\225\260\346\215\256\345\257\271\347\247\260\350\247\243\345\257\206\345\267\245\345\205\267\347\261\273.html" @@ -0,0 +1,155 @@ + + + + + + 微信小程序加密数据对称解密工具类 | 故事 + + + + + + + + + + + + + + +
Skip to content

微信小程序加密数据对称解密工具类

背景

小程序授权登录获取用户手机号功能

依赖

xml
<dependency>
+    <groupId>org.bouncycastle</groupId>
+    <artifactId>bcprov-jdk16</artifactId>
+    <version>1.46</version>
+</dependency>
+ 
+ <dependency>
+    <groupId>commons-codec</groupId>
+    <artifactId>commons-codec</artifactId>
+    <version>1.4</version>
+</dependency>
<dependency>
+    <groupId>org.bouncycastle</groupId>
+    <artifactId>bcprov-jdk16</artifactId>
+    <version>1.46</version>
+</dependency>
+ 
+ <dependency>
+    <groupId>commons-codec</groupId>
+    <artifactId>commons-codec</artifactId>
+    <version>1.4</version>
+</dependency>

代码

java
import com.alibaba.fastjson.JSON;
+import org.apache.commons.codec.binary.Base64;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.Security;
+import java.security.spec.AlgorithmParameterSpec;
+import java.util.Map;
+
+/**
+ * @author storyxc
+ * @description 微信敏感数据对称解密工具类
+ * @create 2021/4/30 15:32
+ */
+public class WeixinDecryptUtils {
+
+
+    /**
+     * 微信加密数据对称解密
+     * @param appId       公众号/小程序id
+     * @param encryptData   加密数据
+     * @param iv          加密初始向量
+     * @param sessionKey    会话密钥
+     * @return              解密数据
+     */
+    public static Map<String,Object> decrypt(String appId,String sessionKey,String encryptData,String iv)
+            throws Exception{
+        byte[] decodeEncryptData = Base64.decodeBase64(encryptData);
+        byte[] decodeIv = Base64.decodeBase64(iv);
+        byte[] decodeSessionKey = Base64.decodeBase64(sessionKey);
+        Security.addProvider(new BouncyCastleProvider());
+        AlgorithmParameterSpec ivSpec = new IvParameterSpec(decodeIv);
+        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding","BC");
+        SecretKeySpec keySpec = new SecretKeySpec(decodeSessionKey, "AES");
+        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
+        byte[] doFinal = cipher.doFinal(decodeEncryptData);
+        String str = new String(doFinal);
+        Map map = JSON.parseObject(str, Map.class);
+        Map<String,Object> watermark = (Map<String, Object>) map.get("watermark");
+        if (watermark != null && !appId.equals(watermark.get("appid"))) {
+            throw new RunException(500,"Invalid encrpytedData watermark appId "+appId+",parsed appID " +(String)watermark.get("appid"));
+        }
+        return map;
+    }
+
+    public static void main(String[] args) throws Exception{
+        String appId = "wx4f4bc4dec97d474b";
+        String sessionKey = "tiihtNczf5v6AKRyjwEUhQ==";
+        String encryptedData = "CiyLU1Aw2KjvrjMdj8YKliAjtP4gsMZMQmRzooG2xrDcvSnxIMXFufNstNGTyaGS9uT5geRa0W4oTOb1WT7fJlAC+oNPdbB+3hVbJSRgv+4lGOETKUQz6OYStslQ142dNCuabNPGBzlooOmB231qMM85d2/fV6ChevvXvQP8Hkue1poOFtnEtpyxVLW1zAo6/1Xx1COxFvrc2d7UL/lmHInNlxuacJXwu0fjpXfz/YqYzBIBzD6WUfTIF9GRHpOn/Hz7saL8xz+W//FRAUid1OksQaQx4CMs8LOddcQhULW4ucetDf96JcR3g0gfRK4PC7E/r7Z6xNrXd2UIeorGj5Ef7b1pJAYB6Y5anaHqZ9J6nKEBvB4DnNLIVWSgARns/8wR2SiRS7MNACwTyrGvt9ts8p12PKFdlqYTopNHR1Vf7XjfhQlVsAJdNiKdYmYVoKlaRv85IfVunYzO0IKXsyl7JCUjCpoG20f0a04COwfneQAGGwd5oa+T8yO5hzuyDb/XcxxmK01EpqOyuxINew==";
+        String iv = "r7BXXKkLb8qrSNn05n0qiA==";
+        Map<String, Object> decode = decrypt(appId, sessionKey, encryptedData, iv);
+        System.out.println(JSON.toJSONString(decode));
+    }
+}
import com.alibaba.fastjson.JSON;
+import org.apache.commons.codec.binary.Base64;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.Security;
+import java.security.spec.AlgorithmParameterSpec;
+import java.util.Map;
+
+/**
+ * @author storyxc
+ * @description 微信敏感数据对称解密工具类
+ * @create 2021/4/30 15:32
+ */
+public class WeixinDecryptUtils {
+
+
+    /**
+     * 微信加密数据对称解密
+     * @param appId       公众号/小程序id
+     * @param encryptData   加密数据
+     * @param iv          加密初始向量
+     * @param sessionKey    会话密钥
+     * @return              解密数据
+     */
+    public static Map<String,Object> decrypt(String appId,String sessionKey,String encryptData,String iv)
+            throws Exception{
+        byte[] decodeEncryptData = Base64.decodeBase64(encryptData);
+        byte[] decodeIv = Base64.decodeBase64(iv);
+        byte[] decodeSessionKey = Base64.decodeBase64(sessionKey);
+        Security.addProvider(new BouncyCastleProvider());
+        AlgorithmParameterSpec ivSpec = new IvParameterSpec(decodeIv);
+        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding","BC");
+        SecretKeySpec keySpec = new SecretKeySpec(decodeSessionKey, "AES");
+        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
+        byte[] doFinal = cipher.doFinal(decodeEncryptData);
+        String str = new String(doFinal);
+        Map map = JSON.parseObject(str, Map.class);
+        Map<String,Object> watermark = (Map<String, Object>) map.get("watermark");
+        if (watermark != null && !appId.equals(watermark.get("appid"))) {
+            throw new RunException(500,"Invalid encrpytedData watermark appId "+appId+",parsed appID " +(String)watermark.get("appid"));
+        }
+        return map;
+    }
+
+    public static void main(String[] args) throws Exception{
+        String appId = "wx4f4bc4dec97d474b";
+        String sessionKey = "tiihtNczf5v6AKRyjwEUhQ==";
+        String encryptedData = "CiyLU1Aw2KjvrjMdj8YKliAjtP4gsMZMQmRzooG2xrDcvSnxIMXFufNstNGTyaGS9uT5geRa0W4oTOb1WT7fJlAC+oNPdbB+3hVbJSRgv+4lGOETKUQz6OYStslQ142dNCuabNPGBzlooOmB231qMM85d2/fV6ChevvXvQP8Hkue1poOFtnEtpyxVLW1zAo6/1Xx1COxFvrc2d7UL/lmHInNlxuacJXwu0fjpXfz/YqYzBIBzD6WUfTIF9GRHpOn/Hz7saL8xz+W//FRAUid1OksQaQx4CMs8LOddcQhULW4ucetDf96JcR3g0gfRK4PC7E/r7Z6xNrXd2UIeorGj5Ef7b1pJAYB6Y5anaHqZ9J6nKEBvB4DnNLIVWSgARns/8wR2SiRS7MNACwTyrGvt9ts8p12PKFdlqYTopNHR1Vf7XjfhQlVsAJdNiKdYmYVoKlaRv85IfVunYzO0IKXsyl7JCUjCpoG20f0a04COwfneQAGGwd5oa+T8yO5hzuyDb/XcxxmK01EpqOyuxINew==";
+        String iv = "r7BXXKkLb8qrSNn05n0qiA==";
+        Map<String, Object> decode = decrypt(appId, sessionKey, encryptedData, iv);
+        System.out.println(JSON.toJSONString(decode));
+    }
+}
+ + + + \ No newline at end of file diff --git "a/linux/applications/Canal\351\203\250\347\275\262.html" "b/linux/applications/Canal\351\203\250\347\275\262.html" new file mode 100644 index 000000000..ecfa2bb53 --- /dev/null +++ "b/linux/applications/Canal\351\203\250\347\275\262.html" @@ -0,0 +1,661 @@ + + + + + + canal部署 | 故事 + + + + + + + + + + + + + + +
Skip to content

canal部署

canal官方仓库:https://github.com/alibaba/canal/

wiki:https://github.com/alibaba/canal/wiki

canal的用途是基于mysql的binlog日志解析,提供增量数据的订阅和消费。简单的场景:通过canal监控mysql数据变更从而及时更新redis中对应的缓存或更新es中的文档内容

本文主要介绍canal在服务器端的部署,包括canal-admin,canal-tsdb配置以及instance配置。mysql版本为5.7,系统为macOS14.2.1。

首先需要下载canal的发行版,下载地址:https://github.com/alibaba/canal/releases,可自行选择版本,这里选择1.1.4。

mysql

自建MySQL,需要先开启 Binlog 写入功能,配置 binlog-format 为 ROW 模式,my.cnf 中配置如下

ini
[mysqld]
+log-bin=mysql-bin # 开启 binlog
+binlog-format=ROW # 选择 ROW 模式
+server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复
[mysqld]
+log-bin=mysql-bin # 开启 binlog
+binlog-format=ROW # 选择 ROW 模式
+server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复

canal admin

  • 解压
bash
mkdir -p ~/Downloads/canal/admin && tar -zxvf canal.admin-1.1.4.tar.gz -C ~/Downloads/canal/admin
mkdir -p ~/Downloads/canal/admin && tar -zxvf canal.admin-1.1.4.tar.gz -C ~/Downloads/canal/admin
  • 执行conf文件夹中的canal_manager.sql建表

    shell
    mysql -uroot -p < ~/Downloads/canal/admin/conf/canal_manager.sql
    mysql -uroot -p < ~/Downloads/canal/admin/conf/canal_manager.sql

image-20210617000438523

  • 创建canal用户并授权canal链接 MySQL 账号具有作为 MySQL slave 的权限

    sql
    CREATE USER canal IDENTIFIED BY 'canal'; 
    +GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
    +-- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ;
    +FLUSH PRIVILEGES;
    CREATE USER canal IDENTIFIED BY 'canal'; 
    +GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
    +-- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ;
    +FLUSH PRIVILEGES;

修改conf文件夹中的application.yml

image-20231223000510198

  • 执行admin/bin目录的startup.sh

  • 访问8089端口

    image-20210617001200010

  • 使用默认账号密码 admin/123456即可登录

    TIP

    application.yml中的canal.adminUser和canal.adminPasswd并非WebUI的登录用户名和密码,而是和canal-server交互用的

    image-20210617001457272

canal-admin的核心模型主要有:

  1. instance,对应canal-server里的instance,一个最小的订阅mysql的队列
  2. server,对应canal-server,一个server里可以包含多个instance
  3. 集群,对应一组canal-server,组合在一起面向高可用HA的运维

简单解释:

  1. instance是最原始的业务订阅诉求,它会和 server/集群 这两个面向资源服务属性的进行关联,比如instance A绑定到server A上或者集群 A上,
  2. 有了任务和资源的绑定关系后,对应的资源服务就会接收到这个任务配置,在对应的资源上动态加载instance,并提供服务
  • 动态加载的过程,对应配置文件中的autoScan配置,只不过基于canal-admin之后可就以变为远程的web操作,而不需要在机器上运维配置文件
  1. 将server抽象成资源之后,原本canal-server运行所需要的canal.properties/instance.properties配置文件就需要在web ui上进行统一运维,每个server只需要以最基本的启动配置 (比如知道一下canal-admin的manager地址,以及访问配置的账号、密码即可)
  • 新建server,按照图中配置即可

    image-20231223001200351

    配置项:

    • 所属集群,可以选择为单机 或者 集群。一般单机Server的模式主要用于一次性的任务或者测试任务
    • Server名称,唯一即可,方便自己记忆
    • Server Ip,机器ip
    • admin端口,canal 1.1.4版本新增的能力,会在canal-server上提供远程管理操作,默认值11110
    • tcp端口,canal提供netty数据订阅服务的端口
    • metric端口, promethues的exporter监控数据端口 (未来会对接监控)

server端配置

  • 修改为kafka模式并配置kafka相关参数
  • tsdb改为mysql
properties
#################################################
+######### 		common argument		#############
+#################################################
+# tcp bind ip
+canal.ip =
+# register ip to zookeeper
+canal.register.ip =
+canal.port = 11111
+canal.metrics.pull.port = 11112
+# canal instance user/passwd
+canal.user = canal
+canal.passwd = E3619321C1A937C46A0D8BD1DAC39F93B27D4458
+
+# canal admin config
+canal.admin.manager = 127.0.0.1:8089
+canal.admin.port = 11110
+canal.admin.user = admin
+canal.admin.passwd = 4ACFE3202A5FF5CF467898FC58AAB1D615029441
+
+canal.zkServers =
+# flush data to zk
+canal.zookeeper.flush.period = 1000
+canal.withoutNetty = false
+# tcp, kafka, RocketMQ
+canal.serverMode = kafka
+# flush meta cursor/parse position to file
+canal.file.data.dir = ${canal.conf.dir}
+canal.file.flush.period = 1000
+## memory store RingBuffer size, should be Math.pow(2,n)
+canal.instance.memory.buffer.size = 16384
+## memory store RingBuffer used memory unit size , default 1kb
+canal.instance.memory.buffer.memunit = 1024 
+## meory store gets mode used MEMSIZE or ITEMSIZE
+canal.instance.memory.batch.mode = MEMSIZE
+canal.instance.memory.rawEntry = true
+
+## detecing config
+canal.instance.detecting.enable = false
+#canal.instance.detecting.sql = insert into retl.xdual values(1,now()) on duplicate key update x=now()
+canal.instance.detecting.sql = select 1
+canal.instance.detecting.interval.time = 3
+canal.instance.detecting.retry.threshold = 3
+canal.instance.detecting.heartbeatHaEnable = false
+
+# support maximum transaction size, more than the size of the transaction will be cut into multiple transactions delivery
+canal.instance.transaction.size =  1024
+# mysql fallback connected to new master should fallback times
+canal.instance.fallbackIntervalInSeconds = 60
+
+# network config
+canal.instance.network.receiveBufferSize = 16384
+canal.instance.network.sendBufferSize = 16384
+canal.instance.network.soTimeout = 30
+
+# binlog filter config
+canal.instance.filter.druid.ddl = true
+canal.instance.filter.query.dcl = false
+canal.instance.filter.query.dml = false
+canal.instance.filter.query.ddl = false
+canal.instance.filter.table.error = false
+canal.instance.filter.rows = false
+canal.instance.filter.transaction.entry = false
+
+# binlog format/image check
+canal.instance.binlog.format = ROW,STATEMENT,MIXED 
+canal.instance.binlog.image = FULL,MINIMAL,NOBLOB
+
+# binlog ddl isolation
+canal.instance.get.ddl.isolation = false
+
+# parallel parser config
+canal.instance.parser.parallel = true
+## concurrent thread number, default 60% available processors, suggest not to exceed Runtime.getRuntime().availableProcessors()
+#canal.instance.parser.parallelThreadSize = 16
+## disruptor ringbuffer size, must be power of 2
+canal.instance.parser.parallelBufferSize = 256
+
+# table meta tsdb info
+canal.instance.tsdb.enable = true
+#canal.instance.tsdb.dir = ${canal.file.data.dir:../conf}/${canal.instance.destination:}
+#canal.instance.tsdb.url = jdbc:h2:${canal.instance.tsdb.dir}/h2;CACHE_SIZE=1000;MODE=MYSQL;
+canal.instance.tsdb.dbUsername = canal
+canal.instance.tsdb.dbPassword = canal
+# dump snapshot interval, default 24 hour
+canal.instance.tsdb.snapshot.interval = 24
+# purge snapshot expire , default 360 hour(15 days)
+canal.instance.tsdb.snapshot.expire = 360
+
+# aliyun ak/sk , support rds/mq
+canal.aliyun.accessKey =
+canal.aliyun.secretKey =
+
+#################################################
+######### 		destinations		#############
+#################################################
+canal.destinations =
+# conf root dir
+canal.conf.dir = ../conf
+# auto scan instance dir add/remove and start/stop instance
+canal.auto.scan = true
+canal.auto.scan.interval = 5
+
+#canal.instance.tsdb.spring.xml = classpath:spring/tsdb/h2-tsdb.xml
+canal.instance.tsdb.spring.xml = classpath:spring/tsdb/mysql-tsdb.xml
+
+canal.instance.global.mode = manager
+canal.instance.global.lazy = false
+canal.instance.global.manager.address = ${canal.admin.manager}
+#canal.instance.global.spring.xml = classpath:spring/memory-instance.xml
+canal.instance.global.spring.xml = classpath:spring/file-instance.xml
+#canal.instance.global.spring.xml = classpath:spring/default-instance.xml
+
+##################################################
+######### 		     MQ 		     #############
+##################################################
+canal.mq.servers = 127.0.0.1:9092 
+canal.mq.retries = 0
+canal.mq.batchSize = 16384
+canal.mq.maxRequestSize = 1048576
+canal.mq.lingerMs = 100
+canal.mq.bufferMemory = 33554432
+canal.mq.canalBatchSize = 50
+canal.mq.canalGetTimeout = 100
+canal.mq.flatMessage = true
+canal.mq.compressionType = none
+canal.mq.acks = all
+#canal.mq.properties. =
+canal.mq.producerGroup = canal-test
+# Set this value to "cloud", if you want open message trace feature in aliyun.
+canal.mq.accessChannel = local
+# aliyun mq namespace
+#canal.mq.namespace =
+
+##################################################
+#########     Kafka Kerberos Info    #############
+##################################################
+canal.mq.kafka.kerberos.enable = false
+canal.mq.kafka.kerberos.krb5FilePath = "../conf/kerberos/krb5.conf"
+canal.mq.kafka.kerberos.jaasFilePath = "../conf/kerberos/jaas.conf"
#################################################
+######### 		common argument		#############
+#################################################
+# tcp bind ip
+canal.ip =
+# register ip to zookeeper
+canal.register.ip =
+canal.port = 11111
+canal.metrics.pull.port = 11112
+# canal instance user/passwd
+canal.user = canal
+canal.passwd = E3619321C1A937C46A0D8BD1DAC39F93B27D4458
+
+# canal admin config
+canal.admin.manager = 127.0.0.1:8089
+canal.admin.port = 11110
+canal.admin.user = admin
+canal.admin.passwd = 4ACFE3202A5FF5CF467898FC58AAB1D615029441
+
+canal.zkServers =
+# flush data to zk
+canal.zookeeper.flush.period = 1000
+canal.withoutNetty = false
+# tcp, kafka, RocketMQ
+canal.serverMode = kafka
+# flush meta cursor/parse position to file
+canal.file.data.dir = ${canal.conf.dir}
+canal.file.flush.period = 1000
+## memory store RingBuffer size, should be Math.pow(2,n)
+canal.instance.memory.buffer.size = 16384
+## memory store RingBuffer used memory unit size , default 1kb
+canal.instance.memory.buffer.memunit = 1024 
+## meory store gets mode used MEMSIZE or ITEMSIZE
+canal.instance.memory.batch.mode = MEMSIZE
+canal.instance.memory.rawEntry = true
+
+## detecing config
+canal.instance.detecting.enable = false
+#canal.instance.detecting.sql = insert into retl.xdual values(1,now()) on duplicate key update x=now()
+canal.instance.detecting.sql = select 1
+canal.instance.detecting.interval.time = 3
+canal.instance.detecting.retry.threshold = 3
+canal.instance.detecting.heartbeatHaEnable = false
+
+# support maximum transaction size, more than the size of the transaction will be cut into multiple transactions delivery
+canal.instance.transaction.size =  1024
+# mysql fallback connected to new master should fallback times
+canal.instance.fallbackIntervalInSeconds = 60
+
+# network config
+canal.instance.network.receiveBufferSize = 16384
+canal.instance.network.sendBufferSize = 16384
+canal.instance.network.soTimeout = 30
+
+# binlog filter config
+canal.instance.filter.druid.ddl = true
+canal.instance.filter.query.dcl = false
+canal.instance.filter.query.dml = false
+canal.instance.filter.query.ddl = false
+canal.instance.filter.table.error = false
+canal.instance.filter.rows = false
+canal.instance.filter.transaction.entry = false
+
+# binlog format/image check
+canal.instance.binlog.format = ROW,STATEMENT,MIXED 
+canal.instance.binlog.image = FULL,MINIMAL,NOBLOB
+
+# binlog ddl isolation
+canal.instance.get.ddl.isolation = false
+
+# parallel parser config
+canal.instance.parser.parallel = true
+## concurrent thread number, default 60% available processors, suggest not to exceed Runtime.getRuntime().availableProcessors()
+#canal.instance.parser.parallelThreadSize = 16
+## disruptor ringbuffer size, must be power of 2
+canal.instance.parser.parallelBufferSize = 256
+
+# table meta tsdb info
+canal.instance.tsdb.enable = true
+#canal.instance.tsdb.dir = ${canal.file.data.dir:../conf}/${canal.instance.destination:}
+#canal.instance.tsdb.url = jdbc:h2:${canal.instance.tsdb.dir}/h2;CACHE_SIZE=1000;MODE=MYSQL;
+canal.instance.tsdb.dbUsername = canal
+canal.instance.tsdb.dbPassword = canal
+# dump snapshot interval, default 24 hour
+canal.instance.tsdb.snapshot.interval = 24
+# purge snapshot expire , default 360 hour(15 days)
+canal.instance.tsdb.snapshot.expire = 360
+
+# aliyun ak/sk , support rds/mq
+canal.aliyun.accessKey =
+canal.aliyun.secretKey =
+
+#################################################
+######### 		destinations		#############
+#################################################
+canal.destinations =
+# conf root dir
+canal.conf.dir = ../conf
+# auto scan instance dir add/remove and start/stop instance
+canal.auto.scan = true
+canal.auto.scan.interval = 5
+
+#canal.instance.tsdb.spring.xml = classpath:spring/tsdb/h2-tsdb.xml
+canal.instance.tsdb.spring.xml = classpath:spring/tsdb/mysql-tsdb.xml
+
+canal.instance.global.mode = manager
+canal.instance.global.lazy = false
+canal.instance.global.manager.address = ${canal.admin.manager}
+#canal.instance.global.spring.xml = classpath:spring/memory-instance.xml
+canal.instance.global.spring.xml = classpath:spring/file-instance.xml
+#canal.instance.global.spring.xml = classpath:spring/default-instance.xml
+
+##################################################
+######### 		     MQ 		     #############
+##################################################
+canal.mq.servers = 127.0.0.1:9092 
+canal.mq.retries = 0
+canal.mq.batchSize = 16384
+canal.mq.maxRequestSize = 1048576
+canal.mq.lingerMs = 100
+canal.mq.bufferMemory = 33554432
+canal.mq.canalBatchSize = 50
+canal.mq.canalGetTimeout = 100
+canal.mq.flatMessage = true
+canal.mq.compressionType = none
+canal.mq.acks = all
+#canal.mq.properties. =
+canal.mq.producerGroup = canal-test
+# Set this value to "cloud", if you want open message trace feature in aliyun.
+canal.mq.accessChannel = local
+# aliyun mq namespace
+#canal.mq.namespace =
+
+##################################################
+#########     Kafka Kerberos Info    #############
+##################################################
+canal.mq.kafka.kerberos.enable = false
+canal.mq.kafka.kerberos.krb5FilePath = "../conf/kerberos/krb5.conf"
+canal.mq.kafka.kerberos.jaasFilePath = "../conf/kerberos/jaas.conf"

TIP

canal.auto.scan如果设置为true,canal.destinations可以不填写,server会自动扫描instance然后启动

java
// CanalController
+// 初始化monitor机制
+autoScan = BooleanUtils.toBoolean(getProperty(properties, CanalConstants.CANAL_AUTO_SCAN));
+if (autoScan) {
+    defaultAction = new InstanceAction() {
+        public void start(String destination) {
+            InstanceConfig config = instanceConfigs.get(destination);
+            if (config == null) {
+                // 重新读取一下instance config
+                config = parseInstanceConfig(properties, destination);
+                instanceConfigs.put(destination, config);
+            }
+            if (!embededCanalServer.isStart(destination)) {
+                // HA机制启动
+                ServerRunningMonitor runningMonitor = ServerRunningMonitors.getRunningMonitor(destination);
+                if (!config.getLazy() && !runningMonitor.isStart()) {
+                    runningMonitor.start();
+                }
+            }
+            logger.info("auto notify start {} successful.", destination);
+        }
+        //...
+    }
+}
+
+instanceConfigMonitors = MigrateMap.makeComputingMap(new Function<InstanceMode, InstanceConfigMonitor>() {
+
+    public InstanceConfigMonitor apply(InstanceMode mode) {
+        int scanInterval = Integer.valueOf(getProperty(properties,
+            CanalConstants.CANAL_AUTO_SCAN_INTERVAL,
+            "5"));
+
+        if (mode.isSpring()) {
+            SpringInstanceConfigMonitor monitor = new SpringInstanceConfigMonitor();
+            monitor.setScanIntervalInSecond(scanInterval);
+            monitor.setDefaultAction(defaultAction);
+            // 设置conf目录,默认是user.dir + conf目录组成
+            String rootDir = getProperty(properties, CanalConstants.CANAL_CONF_DIR);
+            if (StringUtils.isEmpty(rootDir)) {
+                rootDir = "../conf";
+            }
+
+            if (StringUtils.equals("otter-canal", System.getProperty("appName"))) {
+                monitor.setRootConf(rootDir);
+            } else {
+                // eclipse debug模式
+                monitor.setRootConf("src/main/resources/");
+            }
+            return monitor;
+        } else if (mode.isManager()) {
+            ManagerInstanceConfigMonitor monitor = new ManagerInstanceConfigMonitor();
+            monitor.setScanIntervalInSecond(scanInterval);
+            monitor.setDefaultAction(defaultAction);
+            String managerAddress = getProperty(properties, CanalConstants.CANAL_ADMIN_MANAGER);
+            monitor.setConfigClient(getManagerClient(managerAddress));
+            return monitor;
+        } else {
+            throw new UnsupportedOperationException("unknow mode :" + mode + " for monitor");
+        }
+    }
+});
+
+
+// CanalController.start()
+public void start() throws Throwable {
+    // ...
+    // 尝试启动一下非lazy状态的通道
+    for (Map.Entry<String, InstanceConfig> entry : instanceConfigs.entrySet()) {
+        final String destination = entry.getKey();
+        InstanceConfig config = entry.getValue();
+        // 创建destination的工作节点
+        if (!embededCanalServer.isStart(destination)) {
+            // HA机制启动
+            ServerRunningMonitor runningMonitor = ServerRunningMonitors.getRunningMonitor(destination);
+            if (!config.getLazy() && !runningMonitor.isStart()) {
+                runningMonitor.start();
+            }
+        }
+
+        if (autoScan) {
+            instanceConfigMonitors.get(config.getMode()).register(destination, defaultAction);
+        }
+    }
+
+    if (autoScan) {
+        instanceConfigMonitors.get(globalInstanceConfig.getMode()).start();
+        for (InstanceConfigMonitor monitor : instanceConfigMonitors.values()) {
+            if (!monitor.isStart()) {
+                monitor.start();
+            }
+        }
+    }
+		// ...
+}
+
+// 然后会调用ManagerInstanceConfigMonitor的start方法,start方法会启动一个定时任务,每隔scanInterval秒调用scan方法
+public void start() {
+    super.start();
+    executor.scheduleWithFixedDelay(new Runnable() {
+
+        public void run() {
+            try {
+                scan();
+                if (isFirst) {
+                    isFirst = false;
+                }
+            } catch (Throwable e) {
+                logger.error("scan failed", e);
+            }
+        }
+
+    }, 0, scanIntervalInSecond, TimeUnit.SECONDS);
+}
+
+// scan方法中会通过configClient调用canal-admin的接口获取instance的配置信息,
+// 最后对instance进行stop/reload/start操作
+
+private void scan() {
+    String instances = configClient.findInstances(null);
+    final List<String> is = Lists.newArrayList(StringUtils.split(instances, ','));
+    List<String> start = Lists.newArrayList();
+    List<String> stop = Lists.newArrayList();
+    List<String> restart = Lists.newArrayList();
+    for (String instance : is) {
+        if (!configs.containsKey(instance)) {
+            PlainCanal newPlainCanal = configClient.findInstance(instance, null);
+            if (newPlainCanal != null) {
+                configs.put(instance, newPlainCanal);
+                start.add(instance);
+            }
+        } else {
+            PlainCanal plainCanal = configs.get(instance);
+            PlainCanal newPlainCanal = configClient.findInstance(instance, plainCanal.getMd5());
+            if (newPlainCanal != null) {
+                // 配置有变化
+                restart.add(instance);
+                configs.put(instance, newPlainCanal);
+            }
+        }
+    }
+
+    configs.forEach((instance, plainCanal) -> {
+        if (!is.contains(instance)) {
+            stop.add(instance);
+        }
+    });
+
+    stop.forEach(instance -> {
+        notifyStop(instance);
+    });
+
+    restart.forEach(instance -> {
+        notifyReload(instance);
+    });
+
+    start.forEach(instance -> {
+        notifyStart(instance);
+    });
+
+}
// CanalController
+// 初始化monitor机制
+autoScan = BooleanUtils.toBoolean(getProperty(properties, CanalConstants.CANAL_AUTO_SCAN));
+if (autoScan) {
+    defaultAction = new InstanceAction() {
+        public void start(String destination) {
+            InstanceConfig config = instanceConfigs.get(destination);
+            if (config == null) {
+                // 重新读取一下instance config
+                config = parseInstanceConfig(properties, destination);
+                instanceConfigs.put(destination, config);
+            }
+            if (!embededCanalServer.isStart(destination)) {
+                // HA机制启动
+                ServerRunningMonitor runningMonitor = ServerRunningMonitors.getRunningMonitor(destination);
+                if (!config.getLazy() && !runningMonitor.isStart()) {
+                    runningMonitor.start();
+                }
+            }
+            logger.info("auto notify start {} successful.", destination);
+        }
+        //...
+    }
+}
+
+instanceConfigMonitors = MigrateMap.makeComputingMap(new Function<InstanceMode, InstanceConfigMonitor>() {
+
+    public InstanceConfigMonitor apply(InstanceMode mode) {
+        int scanInterval = Integer.valueOf(getProperty(properties,
+            CanalConstants.CANAL_AUTO_SCAN_INTERVAL,
+            "5"));
+
+        if (mode.isSpring()) {
+            SpringInstanceConfigMonitor monitor = new SpringInstanceConfigMonitor();
+            monitor.setScanIntervalInSecond(scanInterval);
+            monitor.setDefaultAction(defaultAction);
+            // 设置conf目录,默认是user.dir + conf目录组成
+            String rootDir = getProperty(properties, CanalConstants.CANAL_CONF_DIR);
+            if (StringUtils.isEmpty(rootDir)) {
+                rootDir = "../conf";
+            }
+
+            if (StringUtils.equals("otter-canal", System.getProperty("appName"))) {
+                monitor.setRootConf(rootDir);
+            } else {
+                // eclipse debug模式
+                monitor.setRootConf("src/main/resources/");
+            }
+            return monitor;
+        } else if (mode.isManager()) {
+            ManagerInstanceConfigMonitor monitor = new ManagerInstanceConfigMonitor();
+            monitor.setScanIntervalInSecond(scanInterval);
+            monitor.setDefaultAction(defaultAction);
+            String managerAddress = getProperty(properties, CanalConstants.CANAL_ADMIN_MANAGER);
+            monitor.setConfigClient(getManagerClient(managerAddress));
+            return monitor;
+        } else {
+            throw new UnsupportedOperationException("unknow mode :" + mode + " for monitor");
+        }
+    }
+});
+
+
+// CanalController.start()
+public void start() throws Throwable {
+    // ...
+    // 尝试启动一下非lazy状态的通道
+    for (Map.Entry<String, InstanceConfig> entry : instanceConfigs.entrySet()) {
+        final String destination = entry.getKey();
+        InstanceConfig config = entry.getValue();
+        // 创建destination的工作节点
+        if (!embededCanalServer.isStart(destination)) {
+            // HA机制启动
+            ServerRunningMonitor runningMonitor = ServerRunningMonitors.getRunningMonitor(destination);
+            if (!config.getLazy() && !runningMonitor.isStart()) {
+                runningMonitor.start();
+            }
+        }
+
+        if (autoScan) {
+            instanceConfigMonitors.get(config.getMode()).register(destination, defaultAction);
+        }
+    }
+
+    if (autoScan) {
+        instanceConfigMonitors.get(globalInstanceConfig.getMode()).start();
+        for (InstanceConfigMonitor monitor : instanceConfigMonitors.values()) {
+            if (!monitor.isStart()) {
+                monitor.start();
+            }
+        }
+    }
+		// ...
+}
+
+// 然后会调用ManagerInstanceConfigMonitor的start方法,start方法会启动一个定时任务,每隔scanInterval秒调用scan方法
+public void start() {
+    super.start();
+    executor.scheduleWithFixedDelay(new Runnable() {
+
+        public void run() {
+            try {
+                scan();
+                if (isFirst) {
+                    isFirst = false;
+                }
+            } catch (Throwable e) {
+                logger.error("scan failed", e);
+            }
+        }
+
+    }, 0, scanIntervalInSecond, TimeUnit.SECONDS);
+}
+
+// scan方法中会通过configClient调用canal-admin的接口获取instance的配置信息,
+// 最后对instance进行stop/reload/start操作
+
+private void scan() {
+    String instances = configClient.findInstances(null);
+    final List<String> is = Lists.newArrayList(StringUtils.split(instances, ','));
+    List<String> start = Lists.newArrayList();
+    List<String> stop = Lists.newArrayList();
+    List<String> restart = Lists.newArrayList();
+    for (String instance : is) {
+        if (!configs.containsKey(instance)) {
+            PlainCanal newPlainCanal = configClient.findInstance(instance, null);
+            if (newPlainCanal != null) {
+                configs.put(instance, newPlainCanal);
+                start.add(instance);
+            }
+        } else {
+            PlainCanal plainCanal = configs.get(instance);
+            PlainCanal newPlainCanal = configClient.findInstance(instance, plainCanal.getMd5());
+            if (newPlainCanal != null) {
+                // 配置有变化
+                restart.add(instance);
+                configs.put(instance, newPlainCanal);
+            }
+        }
+    }
+
+    configs.forEach((instance, plainCanal) -> {
+        if (!is.contains(instance)) {
+            stop.add(instance);
+        }
+    });
+
+    stop.forEach(instance -> {
+        notifyStop(instance);
+    });
+
+    restart.forEach(instance -> {
+        notifyReload(instance);
+    });
+
+    start.forEach(instance -> {
+        notifyStart(instance);
+    });
+
+}

canal deployer

  • 解压
bash
mkdir -p ~/Downloads/canal/deployer && tar -zxvf canal.deployer-1.1.4.tar.gz -C ~/Downloads/canal/deployer
mkdir -p ~/Downloads/canal/deployer && tar -zxvf canal.deployer-1.1.4.tar.gz -C ~/Downloads/canal/deployer
  • 用conf目录下的canal_local.properties替换canal.properties

  • 修改配置

    properties
    # canal.ip
    +canal.ip = 127.0.0.1 
    +# register ip
    +canal.register.ip = 127.0.0.1
    # canal.ip
    +canal.ip = 127.0.0.1 
    +# register ip
    +canal.register.ip = 127.0.0.1

    TIP

    1. 注意ip前后不能有空格,不然会无法启动netty server从而无法启动canal server,应该是后台没做trim

    2. 如果不填写canal.ipcanal.register.ip两个配置项,代码中将通过AddressUtils.getHostIp()获取本机的ip地址,如果本地有docker/orbstack等创建的虚拟网络设备会导致启动canal-server后识别到多个server且是不同的ip(docker0网桥或orbstack容器等的ip),比较膈应人。(#issue47)

    源码:

    java
    // CanalController.java
    +
    +if (StringUtils.isEmpty(ip) && StringUtils.isEmpty(registerIp)) {
    +    ip = registerIp = AddressUtils.getHostIp();
    +}
    +
    +if (StringUtils.isEmpty(ip)) {
    +    ip = AddressUtils.getHostIp();
    +}
    +
    +if (StringUtils.isEmpty(registerIp)) {
    +    registerIp = ip; // 兼容以前配置
    +}
    // CanalController.java
    +
    +if (StringUtils.isEmpty(ip) && StringUtils.isEmpty(registerIp)) {
    +    ip = registerIp = AddressUtils.getHostIp();
    +}
    +
    +if (StringUtils.isEmpty(ip)) {
    +    ip = AddressUtils.getHostIp();
    +}
    +
    +if (StringUtils.isEmpty(registerIp)) {
    +    registerIp = ip; // 兼容以前配置
    +}
  • 执行bin目录下的startup.sh

  • 直接在canal admin的webUI界面中配置instance,等待启动即可,如有问题查看deploy/logs/story/story.log

    具体配置项参见wiki

    image-20231223002954201

TIP

如果在server的配置文件中填了相同的配置项,那么instance中的配置会被server中的覆盖,例如canal.instance.tsdb.url配置(#issue4669)

验证

此时我们的实例story已经开始监听story数据库下面的操作,如果有变更,就会推送至kafka的canal-story这个topic中

image-20231223004300829

之后就需要通过业务端根据情况监听队列中的数据变化,做相应的操作。

+ + + + \ No newline at end of file diff --git a/linux/applications/Clash.html b/linux/applications/Clash.html new file mode 100644 index 000000000..be73df0a5 --- /dev/null +++ b/linux/applications/Clash.html @@ -0,0 +1,683 @@ + + + + + + Clash | 故事 + + + + + + + + + + + + + + +
Skip to content

Clash

Repo:https://github.com/Dreamacro/clash

Premium Binary:https://github.com/Dreamacro/clash/releases/tag/premium

Configuration:https://dreamacro.github.io/clash/configuration/configuration-reference.html

配置

  • 下载最新版的premium内核二进制文件

  • 编写配置文件

    yaml
    # /root/.config/clash/config.yaml
    +
    +# (HTTP and SOCKS5 in one port)
    +mixed-port: 7890
    +# RESTful API for clash
    +external-controller: 127.0.0.1:9090
    +secret: 123456
    +allow-lan: false
    +mode: rule
    +log-level: warning
    +
    +tun:
    +  enable: true
    +  stack: system # system gvisor
    +  auto-route: true
    +  auto-detect-interface: true
    +  dns-hijack:
    +    - any:53 # DNS hijacking might result in a failure, if the system DNS is at a private IP address (since auto-route does not capture private network traffic).
    +
    +dns:
    +  enable: true
    +  listen: 0.0.0.0:7874
    +  ipv6: false
    +  default-nameserver:
    +    - 114.114.114.114
    +  nameserver:
    +    - 119.29.29.29
    +    - 223.5.5.5
    +    - 8.8.8.8
    +  fallback:
    +    - https://dns.cloudflare.com/dns-query
    +    - tls://dns.google:853
    +    - https://1.1.1.1/dns-query
    +  enhanced-mode: fake-ip # fake-ip redir-host
    +  fake-ip-range: 198.18.0.1/16
    +  fake-ip-filter:
    +  - "*.lan"
    +  - "*.localdomain"
    +  - "*.example"
    +  - "*.invalid"
    +  - "*.localhost"
    +  - "*.test"
    +  - "*.local"
    +  - "*.home.arpa"
    +  - time.*.com
    +  - time.*.gov
    +  - time.*.edu.cn
    +  - time.*.apple.com
    +  - time-ios.apple.com
    +  - time1.*.com
    +  - time2.*.com
    +  - time3.*.com
    +  - time4.*.com
    +  - time5.*.com
    +  - time6.*.com
    +  - time7.*.com
    +  - ntp.*.com
    +  - ntp1.*.com
    +  - ntp2.*.com
    +  - ntp3.*.com
    +  - ntp4.*.com
    +  - ntp5.*.com
    +  - ntp6.*.com
    +  - ntp7.*.com
    +  - "*.time.edu.cn"
    +  - "*.ntp.org.cn"
    +  - "+.pool.ntp.org"
    +  - time1.cloud.tencent.com
    +  - music.163.com
    +  - "*.music.163.com"
    +  - "*.126.net"
    +  - musicapi.taihe.com
    +  - music.taihe.com
    +  - songsearch.kugou.com
    +  - trackercdn.kugou.com
    +  - "*.kuwo.cn"
    +  - api-jooxtt.sanook.com
    +  - api.joox.com
    +  - joox.com
    +  - y.qq.com
    +  - "*.y.qq.com"
    +  - streamoc.music.tc.qq.com
    +  - mobileoc.music.tc.qq.com
    +  - isure.stream.qqmusic.qq.com
    +  - dl.stream.qqmusic.qq.com
    +  - aqqmusic.tc.qq.com
    +  - amobile.music.tc.qq.com
    +  - "*.xiami.com"
    +  - "*.music.migu.cn"
    +  - music.migu.cn
    +  - "+.msftconnecttest.com"
    +  - "+.msftncsi.com"
    +  - localhost.ptlogin2.qq.com
    +  - localhost.sec.qq.com
    +  - "+.qq.com"
    +  - "+.tencent.com"
    +  - "+.srv.nintendo.net"
    +  - "*.n.n.srv.nintendo.net"
    +  - "+.stun.playstation.net"
    +  - xbox.*.*.microsoft.com
    +  - "*.*.xboxlive.com"
    +  - xbox.*.microsoft.com
    +  - xnotify.xboxlive.com
    +  - "+.battlenet.com.cn"
    +  - "+.wotgame.cn"
    +  - "+.wggames.cn"
    +  - "+.wowsgame.cn"
    +  - "+.wargaming.net"
    +  - proxy.golang.org
    +  - stun.*.*
    +  - stun.*.*.*
    +  - "+.stun.*.*"
    +  - "+.stun.*.*.*"
    +  - "+.stun.*.*.*.*"
    +  - "+.stun.*.*.*.*.*"
    +  - heartbeat.belkin.com
    +  - "*.linksys.com"
    +  - "*.linksyssmartwifi.com"
    +  - mobileoc.music.tc.qq.com
    +  - isure.stream.qqmusic.qq.com
    +  - dl.stream.qqmusic.qq.com
    +  - aqqmusic.tc.qq.com
    +  - amobile.music.tc.qq.com
    +  - "*.xiami.com"
    +  - "*.music.migu.cn"
    +  - music.migu.cn
    +  - "+.msftconnecttest.com"
    +  - "+.msftncsi.com"
    +  - localhost.ptlogin2.qq.com
    +  - localhost.sec.qq.com
    +  - "+.qq.com"
    +  - "+.tencent.com"
    +  - "+.srv.nintendo.net"
    +  - "*.n.n.srv.nintendo.net"
    +  - "+.stun.playstation.net"
    +  - xbox.*.*.microsoft.com
    +  - "*.*.xboxlive.com"
    +  - xbox.*.microsoft.com
    +  - xnotify.xboxlive.com
    +  - "+.battlenet.com.cn"
    +  - "+.wotgame.cn"
    +  - "+.wggames.cn"
    +  - "+.wowsgame.cn"
    +  - "+.wargaming.net"
    +  - proxy.golang.org
    +  - stun.*.*
    +  - stun.*.*.*
    +  - "+.stun.*.*"
    +  - "+.stun.*.*.*"
    +  - "+.stun.*.*.*.*"
    +  - "+.stun.*.*.*.*.*"
    +  - heartbeat.belkin.com
    +  - "*.linksys.com"
    +  - "*.linksyssmartwifi.com"
    +  - "*.router.asus.com"
    +  - mesu.apple.com
    +  - swscan.apple.com
    +  - swquery.apple.com
    +  - swdownload.apple.com
    +  - swcdn.apple.com
    +  - swdist.apple.com
    +  - lens.l.google.com
    +  - stun.l.google.com
    +  - "+.nflxvideo.net"
    +  - "*.square-enix.com"
    +  - "*.finalfantasyxiv.com"
    +  - "*.ffxiv.com"
    +  - "*.ff14.sdo.com"
    +  - ff.dorado.sdo.com
    +  - "*.mcdn.bilivideo.cn"
    +  - "+.media.dssott.com"
    +  - shark007.net
    +  - Mijia Cloud
    +  - "+.cmbchina.com"
    +  - "+.cmbimg.com"
    +  - local.adguard.org
    +  - "+.sandai.net"
    +  - "+.n0808.com"
    +    
    +proxy-providers:
    +  provider:
    +    type: http
    +    path: ./profile/provider.yaml
    +    url: subscription.url
    +    interval: 36000
    +    health-check:
    +      enable: true
    +      url: http://www.gstatic.com/generate_204
    +      interval: 3600
    +
    +proxy-groups:
    +  - name: PROXY
    +    type: select
    +    use:
    +      - provider
    +
    +rule-providers:
    +  reject:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/reject.txt"
    +    path: ./ruleset/reject.yaml
    +    interval: 86400
    +
    +  icloud:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/icloud.txt"
    +    path: ./ruleset/icloud.yaml
    +    interval: 86400
    +
    +  apple:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/apple.txt"
    +    path: ./ruleset/apple.yaml
    +    interval: 86400
    +
    +  # google:
    +  #   type: http
    +  #   behavior: domain
    +  #   url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/google.txt"
    +  #   path: ./ruleset/google.yaml
    +  #   interval: 86400
    +
    +  proxy:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/proxy.txt"
    +    path: ./ruleset/proxy.yaml
    +    interval: 86400
    +
    +  direct:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/direct.txt"
    +    path: ./ruleset/direct.yaml
    +    interval: 86400
    +  private:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/private.txt"
    +    path: ./ruleset/private.yaml
    +    interval: 86400
    +
    +  gfw:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/gfw.txt"
    +    path: ./ruleset/gfw.yaml
    +    interval: 86400
    +
    +  tld-not-cn:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/tld-not-cn.txt"
    +    path: ./ruleset/tld-not-cn.yaml
    +    interval: 86400
    +
    +  telegramcidr:
    +    type: http
    +    behavior: ipcidr
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/telegramcidr.txt"
    +    path: ./ruleset/telegramcidr.yaml
    +    interval: 86400
    +
    +  cncidr:
    +    type: http
    +    behavior: ipcidr
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/cncidr.txt"
    +    path: ./ruleset/cncidr.yaml
    +    interval: 86400
    +
    +  lancidr:
    +    type: http
    +    behavior: ipcidr
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/lancidr.txt"
    +    path: ./ruleset/lancidr.yaml
    +    interval: 86400
    +
    +  applications:
    +    type: http
    +    behavior: classical
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/applications.txt"
    +    path: ./ruleset/applications.yaml
    +    interval: 86400
    +
    +
    +# Script Shortcuts enables the use of script in rules mode. By default, DNS resolution takes place for SCRIPT rules. no-resolve can be appended to the rule to prevent the resolution. (i.e.: SCRIPT,quic,DIRECT,no-resolve)
    +script:
    +  engine: expr # or starlark (10x to 20x slower)
    +  shortcuts:
    +    quic: network == 'udp' and dst_port == 443
    +    curl: resolve_process_name() == 'curl'
    +    # curl: resolve_process_path() == '/usr/bin/curl'
    +    not_common_port: dst_port not in [21, 22, 23, 53, 80, 123, 143, 194, 443, 465, 587, 853, 993, 995, 998, 2052, 2053, 2082, 2083, 2086, 2095, 2096, 5222, 5228, 5229, 5230, 8080, 8443, 8880, 8888, 8889]
    +
    +
    +rules:
    +  - SCRIPT,not_common_port,DIRECT # 只代理常规端口,常用于防止PT、BT的流量走代理
    +  - SCRIPT,quic,REJECT
    +  - DST-PORT,22,DIRECT
    +  - RULE-SET,applications,DIRECT
    +  - DOMAIN,clash.razord.top,DIRECT
    +  - DOMAIN,yacd.haishan.me,DIRECT
    +  - RULE-SET,private,DIRECT
    +  - RULE-SET,reject,REJECT
    +  - RULE-SET,icloud,DIRECT
    +  - RULE-SET,apple,DIRECT
    +  # - RULE-SET,google,DIRECT
    +  - RULE-SET,proxy,PROXY
    +  - RULE-SET,direct,DIRECT
    +  - RULE-SET,lancidr,DIRECT
    +  - RULE-SET,cncidr,DIRECT
    +  - RULE-SET,telegramcidr,PROXY
    +  - GEOIP,LAN,DIRECT
    +  - GEOIP,CN,DIRECT
    +  - MATCH,PROXY
    # /root/.config/clash/config.yaml
    +
    +# (HTTP and SOCKS5 in one port)
    +mixed-port: 7890
    +# RESTful API for clash
    +external-controller: 127.0.0.1:9090
    +secret: 123456
    +allow-lan: false
    +mode: rule
    +log-level: warning
    +
    +tun:
    +  enable: true
    +  stack: system # system gvisor
    +  auto-route: true
    +  auto-detect-interface: true
    +  dns-hijack:
    +    - any:53 # DNS hijacking might result in a failure, if the system DNS is at a private IP address (since auto-route does not capture private network traffic).
    +
    +dns:
    +  enable: true
    +  listen: 0.0.0.0:7874
    +  ipv6: false
    +  default-nameserver:
    +    - 114.114.114.114
    +  nameserver:
    +    - 119.29.29.29
    +    - 223.5.5.5
    +    - 8.8.8.8
    +  fallback:
    +    - https://dns.cloudflare.com/dns-query
    +    - tls://dns.google:853
    +    - https://1.1.1.1/dns-query
    +  enhanced-mode: fake-ip # fake-ip redir-host
    +  fake-ip-range: 198.18.0.1/16
    +  fake-ip-filter:
    +  - "*.lan"
    +  - "*.localdomain"
    +  - "*.example"
    +  - "*.invalid"
    +  - "*.localhost"
    +  - "*.test"
    +  - "*.local"
    +  - "*.home.arpa"
    +  - time.*.com
    +  - time.*.gov
    +  - time.*.edu.cn
    +  - time.*.apple.com
    +  - time-ios.apple.com
    +  - time1.*.com
    +  - time2.*.com
    +  - time3.*.com
    +  - time4.*.com
    +  - time5.*.com
    +  - time6.*.com
    +  - time7.*.com
    +  - ntp.*.com
    +  - ntp1.*.com
    +  - ntp2.*.com
    +  - ntp3.*.com
    +  - ntp4.*.com
    +  - ntp5.*.com
    +  - ntp6.*.com
    +  - ntp7.*.com
    +  - "*.time.edu.cn"
    +  - "*.ntp.org.cn"
    +  - "+.pool.ntp.org"
    +  - time1.cloud.tencent.com
    +  - music.163.com
    +  - "*.music.163.com"
    +  - "*.126.net"
    +  - musicapi.taihe.com
    +  - music.taihe.com
    +  - songsearch.kugou.com
    +  - trackercdn.kugou.com
    +  - "*.kuwo.cn"
    +  - api-jooxtt.sanook.com
    +  - api.joox.com
    +  - joox.com
    +  - y.qq.com
    +  - "*.y.qq.com"
    +  - streamoc.music.tc.qq.com
    +  - mobileoc.music.tc.qq.com
    +  - isure.stream.qqmusic.qq.com
    +  - dl.stream.qqmusic.qq.com
    +  - aqqmusic.tc.qq.com
    +  - amobile.music.tc.qq.com
    +  - "*.xiami.com"
    +  - "*.music.migu.cn"
    +  - music.migu.cn
    +  - "+.msftconnecttest.com"
    +  - "+.msftncsi.com"
    +  - localhost.ptlogin2.qq.com
    +  - localhost.sec.qq.com
    +  - "+.qq.com"
    +  - "+.tencent.com"
    +  - "+.srv.nintendo.net"
    +  - "*.n.n.srv.nintendo.net"
    +  - "+.stun.playstation.net"
    +  - xbox.*.*.microsoft.com
    +  - "*.*.xboxlive.com"
    +  - xbox.*.microsoft.com
    +  - xnotify.xboxlive.com
    +  - "+.battlenet.com.cn"
    +  - "+.wotgame.cn"
    +  - "+.wggames.cn"
    +  - "+.wowsgame.cn"
    +  - "+.wargaming.net"
    +  - proxy.golang.org
    +  - stun.*.*
    +  - stun.*.*.*
    +  - "+.stun.*.*"
    +  - "+.stun.*.*.*"
    +  - "+.stun.*.*.*.*"
    +  - "+.stun.*.*.*.*.*"
    +  - heartbeat.belkin.com
    +  - "*.linksys.com"
    +  - "*.linksyssmartwifi.com"
    +  - mobileoc.music.tc.qq.com
    +  - isure.stream.qqmusic.qq.com
    +  - dl.stream.qqmusic.qq.com
    +  - aqqmusic.tc.qq.com
    +  - amobile.music.tc.qq.com
    +  - "*.xiami.com"
    +  - "*.music.migu.cn"
    +  - music.migu.cn
    +  - "+.msftconnecttest.com"
    +  - "+.msftncsi.com"
    +  - localhost.ptlogin2.qq.com
    +  - localhost.sec.qq.com
    +  - "+.qq.com"
    +  - "+.tencent.com"
    +  - "+.srv.nintendo.net"
    +  - "*.n.n.srv.nintendo.net"
    +  - "+.stun.playstation.net"
    +  - xbox.*.*.microsoft.com
    +  - "*.*.xboxlive.com"
    +  - xbox.*.microsoft.com
    +  - xnotify.xboxlive.com
    +  - "+.battlenet.com.cn"
    +  - "+.wotgame.cn"
    +  - "+.wggames.cn"
    +  - "+.wowsgame.cn"
    +  - "+.wargaming.net"
    +  - proxy.golang.org
    +  - stun.*.*
    +  - stun.*.*.*
    +  - "+.stun.*.*"
    +  - "+.stun.*.*.*"
    +  - "+.stun.*.*.*.*"
    +  - "+.stun.*.*.*.*.*"
    +  - heartbeat.belkin.com
    +  - "*.linksys.com"
    +  - "*.linksyssmartwifi.com"
    +  - "*.router.asus.com"
    +  - mesu.apple.com
    +  - swscan.apple.com
    +  - swquery.apple.com
    +  - swdownload.apple.com
    +  - swcdn.apple.com
    +  - swdist.apple.com
    +  - lens.l.google.com
    +  - stun.l.google.com
    +  - "+.nflxvideo.net"
    +  - "*.square-enix.com"
    +  - "*.finalfantasyxiv.com"
    +  - "*.ffxiv.com"
    +  - "*.ff14.sdo.com"
    +  - ff.dorado.sdo.com
    +  - "*.mcdn.bilivideo.cn"
    +  - "+.media.dssott.com"
    +  - shark007.net
    +  - Mijia Cloud
    +  - "+.cmbchina.com"
    +  - "+.cmbimg.com"
    +  - local.adguard.org
    +  - "+.sandai.net"
    +  - "+.n0808.com"
    +    
    +proxy-providers:
    +  provider:
    +    type: http
    +    path: ./profile/provider.yaml
    +    url: subscription.url
    +    interval: 36000
    +    health-check:
    +      enable: true
    +      url: http://www.gstatic.com/generate_204
    +      interval: 3600
    +
    +proxy-groups:
    +  - name: PROXY
    +    type: select
    +    use:
    +      - provider
    +
    +rule-providers:
    +  reject:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/reject.txt"
    +    path: ./ruleset/reject.yaml
    +    interval: 86400
    +
    +  icloud:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/icloud.txt"
    +    path: ./ruleset/icloud.yaml
    +    interval: 86400
    +
    +  apple:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/apple.txt"
    +    path: ./ruleset/apple.yaml
    +    interval: 86400
    +
    +  # google:
    +  #   type: http
    +  #   behavior: domain
    +  #   url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/google.txt"
    +  #   path: ./ruleset/google.yaml
    +  #   interval: 86400
    +
    +  proxy:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/proxy.txt"
    +    path: ./ruleset/proxy.yaml
    +    interval: 86400
    +
    +  direct:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/direct.txt"
    +    path: ./ruleset/direct.yaml
    +    interval: 86400
    +  private:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/private.txt"
    +    path: ./ruleset/private.yaml
    +    interval: 86400
    +
    +  gfw:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/gfw.txt"
    +    path: ./ruleset/gfw.yaml
    +    interval: 86400
    +
    +  tld-not-cn:
    +    type: http
    +    behavior: domain
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/tld-not-cn.txt"
    +    path: ./ruleset/tld-not-cn.yaml
    +    interval: 86400
    +
    +  telegramcidr:
    +    type: http
    +    behavior: ipcidr
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/telegramcidr.txt"
    +    path: ./ruleset/telegramcidr.yaml
    +    interval: 86400
    +
    +  cncidr:
    +    type: http
    +    behavior: ipcidr
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/cncidr.txt"
    +    path: ./ruleset/cncidr.yaml
    +    interval: 86400
    +
    +  lancidr:
    +    type: http
    +    behavior: ipcidr
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/lancidr.txt"
    +    path: ./ruleset/lancidr.yaml
    +    interval: 86400
    +
    +  applications:
    +    type: http
    +    behavior: classical
    +    url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/applications.txt"
    +    path: ./ruleset/applications.yaml
    +    interval: 86400
    +
    +
    +# Script Shortcuts enables the use of script in rules mode. By default, DNS resolution takes place for SCRIPT rules. no-resolve can be appended to the rule to prevent the resolution. (i.e.: SCRIPT,quic,DIRECT,no-resolve)
    +script:
    +  engine: expr # or starlark (10x to 20x slower)
    +  shortcuts:
    +    quic: network == 'udp' and dst_port == 443
    +    curl: resolve_process_name() == 'curl'
    +    # curl: resolve_process_path() == '/usr/bin/curl'
    +    not_common_port: dst_port not in [21, 22, 23, 53, 80, 123, 143, 194, 443, 465, 587, 853, 993, 995, 998, 2052, 2053, 2082, 2083, 2086, 2095, 2096, 5222, 5228, 5229, 5230, 8080, 8443, 8880, 8888, 8889]
    +
    +
    +rules:
    +  - SCRIPT,not_common_port,DIRECT # 只代理常规端口,常用于防止PT、BT的流量走代理
    +  - SCRIPT,quic,REJECT
    +  - DST-PORT,22,DIRECT
    +  - RULE-SET,applications,DIRECT
    +  - DOMAIN,clash.razord.top,DIRECT
    +  - DOMAIN,yacd.haishan.me,DIRECT
    +  - RULE-SET,private,DIRECT
    +  - RULE-SET,reject,REJECT
    +  - RULE-SET,icloud,DIRECT
    +  - RULE-SET,apple,DIRECT
    +  # - RULE-SET,google,DIRECT
    +  - RULE-SET,proxy,PROXY
    +  - RULE-SET,direct,DIRECT
    +  - RULE-SET,lancidr,DIRECT
    +  - RULE-SET,cncidr,DIRECT
    +  - RULE-SET,telegramcidr,PROXY
    +  - GEOIP,LAN,DIRECT
    +  - GEOIP,CN,DIRECT
    +  - MATCH,PROXY
    shell
    cat > /etc/systemd/system/clash.service <<EOF
    +[Unit]
    +Description=clash
    +After=network.target
    +[Service]
    +Type=simple
    +ExecStart=/usr/bin/clash -d /root/.config/clash
    +Restart=on-failure
    +[Install]
    +WantedBy=multi-user.target
    +EOF
    +
    +# systemctl daemon-reload
    cat > /etc/systemd/system/clash.service <<EOF
    +[Unit]
    +Description=clash
    +After=network.target
    +[Service]
    +Type=simple
    +ExecStart=/usr/bin/clash -d /root/.config/clash
    +Restart=on-failure
    +[Install]
    +WantedBy=multi-user.target
    +EOF
    +
    +# systemctl daemon-reload

管理API

  • 获取代理信息:curl -X GET http://127.0.0.1:9090/proxies --header 'Authorization: Bearer 123456'
  • 获取指定代理信息:curl -X GET http://127.0.0.1:9090/proxies/{proxyGroupName} --header 'Authorization: Bearer 123456'
  • 代理组切换指定节点:curl -X PUT http://127.0.0.1:9090/proxies/{proxyGroupName} --header 'Authorization: Bearer 123456' --header "Content-Type: application/json" -d '{"name": "代理节点名称"}'
+ + + + \ No newline at end of file diff --git "a/linux/applications/FFmpeg\347\233\270\345\205\263.html" "b/linux/applications/FFmpeg\347\233\270\345\205\263.html" new file mode 100644 index 000000000..ab54fc9a3 --- /dev/null +++ "b/linux/applications/FFmpeg\347\233\270\345\205\263.html" @@ -0,0 +1,25 @@ + + + + + + FFmpeg相关 | 故事 + + + + + + + + + + + + + + +
Skip to content

FFmpeg相关

FFmpeg: A complete, cross-platform solution to record, convert and stream audio and video.

提取视频文件中的音频文件并输出

shell
ffmpeg -i input.mp4 -vn -acodec libmp3lame -q:a 0 ~/Downlaods/output.mp3
ffmpeg -i input.mp4 -vn -acodec libmp3lame -q:a 0 ~/Downlaods/output.mp3
  • -vn:禁用复制视频流,只提取音频流
  • -acodec libmp3lame:指定输出使用 LAME MP3 编码器进行编码。
  • -q:a 0:设置输出音频的质量。0 表示最高品质。

yt-dlp下载最高质量音频并由ffmpeg输出mp3

shell
yt-dlp -f bestaudio "https://www.youtube.com/watch?v=example" -o - | ffmpeg -i - -vn -acodec libmp3lame -q:a 0 ~/downloads/output.mp3
yt-dlp -f bestaudio "https://www.youtube.com/watch?v=example" -o - | ffmpeg -i - -vn -acodec libmp3lame -q:a 0 ~/downloads/output.mp3

yt-dlp下载最高质量视频&音频并合成一个视频

shell
yt-dlp -f bestvideo+bestaudio "https://www.youtube.com/watch?v=example" -o ~/Downloads/output.mp4 --recode-video mp4
yt-dlp -f bestvideo+bestaudio "https://www.youtube.com/watch?v=example" -o ~/Downloads/output.mp4 --recode-video mp4
+ + + + \ No newline at end of file diff --git a/linux/applications/Grafana.html b/linux/applications/Grafana.html new file mode 100644 index 000000000..2bb7a0b34 --- /dev/null +++ b/linux/applications/Grafana.html @@ -0,0 +1,59 @@ + + + + + + Grafana | 故事 + + + + + + + + + + + + + + +
Skip to content

Grafana

告警

grafana的告警可以使用Go Template语法来读取内置的变量数据并输出到告警邮件中

比如alert query从Loki日志中查询,可以同时从日志中提取出自己需要的关键属性作为标签:

shell
count_over_time({job="wechat"} |= `订单申请退款失败` | pattern `<_> orderNo=<orderNo> refundNo=<refundNo>` [1m])
count_over_time({job="wechat"} |= `订单申请退款失败` | pattern `<_> orderNo=<orderNo> refundNo=<refundNo>` [1m])

上面的查询提取了订单号、退款单号的数据,标签会存在_value_string_中,可以使用$values访问,在Summary中填写以下模板:

go
{{ with $values }}
+{{ range $k, $v := . }}
+  订单编号: {{$v.Labels.orderNo}}
+  退款单号: {{$v.Labels.refundNo}}
+  服务器实例: {{$v.Labels.instance}}
+{{ end }}
+{{ end }}
{{ with $values }}
+{{ range $k, $v := . }}
+  订单编号: {{$v.Labels.orderNo}}
+  退款单号: {{$v.Labels.refundNo}}
+  服务器实例: {{$v.Labels.instance}}
+{{ end }}
+{{ end }}

https://community.grafana.com/t/how-to-use-alert-message-templates-in-grafana/67537/3

匿名访问

  • 首先在Administration中创建新的组织Guest
  • 修改配置文件
ini
# grafana.ini
+
+[auth.anonymous]
+# enable anonymous access
+enabled = true
+# specify organization name that should be used for unauthenticated users
+org_name = Guest
+# specify role for unauthenticated users
+org_role = Viewer
+
+# mask the Grafana version number for unauthenticated users
+hide_version = true
# grafana.ini
+
+[auth.anonymous]
+# enable anonymous access
+enabled = true
+# specify organization name that should be used for unauthenticated users
+org_name = Guest
+# specify role for unauthenticated users
+org_role = Viewer
+
+# mask the Grafana version number for unauthenticated users
+hide_version = true
+ + + + \ No newline at end of file diff --git "a/linux/applications/Linux\344\270\255\344\275\277\347\224\250selenium.html" "b/linux/applications/Linux\344\270\255\344\275\277\347\224\250selenium.html" new file mode 100644 index 000000000..eb35794d8 --- /dev/null +++ "b/linux/applications/Linux\344\270\255\344\275\277\347\224\250selenium.html" @@ -0,0 +1,61 @@ + + + + + + Linux中使用selenium | 故事 + + + + + + + + + + + + + + +
Skip to content

Linux中使用selenium

安装linux版chrome

centos

wget https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm

yum install google-chrome-stable_current_x86_64.rpm

安装相关库

yum install mesa-libOSMesa-devel gnu-free-sans-fonts wqy-zenhei-fonts

ubuntu

wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.debsudo dpkg -i google-chrome-stable_current_amd64.deb

安装报错

dpkg: error processing package google-chrome-stable (--install): dependency problems - leaving unconfigured Processing triggers for mime-support (3.64ubuntu1) ... Processing triggers for man-db (2.9.1-1) ... Errors were encountered while processing: google-chrome-stable

使用sudo apt-get install -f修复依赖关系,

如果系统中有某个软件包不满足依赖条件,这个命令就会自动修复,将要安装那个软件包依赖的软件包。

安装chromedriver

淘宝源地址:http://npm.taobao.org/mirrors/chromedriver/

需要根据不同版本的chrome进行选择下载,比如我安装的chrome是96版本的,那么chromedriver就需要找对应的96版本

image-20220104161559216

这里选择一个最近更新的即可

image-20220104161718088

这里下载linux版本,下载后解压,把解压后的chromedriver可执行文件移动到path下,例如/usr/bin

bash
wget http://npm.taobao.org/mirrors/chromedriver/96.0.4664.45/chromedriver_linux64.zip
+
+unzip chromedriver_linux64.zip
+
+mv chromedriver /usr/bin
+
+chmod +x /usr/bin/chromedriver
wget http://npm.taobao.org/mirrors/chromedriver/96.0.4664.45/chromedriver_linux64.zip
+
+unzip chromedriver_linux64.zip
+
+mv chromedriver /usr/bin
+
+chmod +x /usr/bin/chromedriver

使用python测试

linux下无头浏览器模式:

python
from selenium import webdriver
+from selenium.webdriver.chrome.options import Options
+
+chrome_options = Options()
+chrome_options.add_argument('--headless')
+chrome_options.add_argument('--no-sandbox')
+chrome_options.add_argument('--disable-gpu')
+chrome_options.add_argument('--disable-dev-shm-usage')
+driver = webdriver.Chrome(chrome_options=chrome_options)
+driver.get("https://www.baidu.com")
+
+with open("./baidu.html", "w", encoding="utf-8") as fp:
+        fp.write(driver.page_source)
from selenium import webdriver
+from selenium.webdriver.chrome.options import Options
+
+chrome_options = Options()
+chrome_options.add_argument('--headless')
+chrome_options.add_argument('--no-sandbox')
+chrome_options.add_argument('--disable-gpu')
+chrome_options.add_argument('--disable-dev-shm-usage')
+driver = webdriver.Chrome(chrome_options=chrome_options)
+driver.get("https://www.baidu.com")
+
+with open("./baidu.html", "w", encoding="utf-8") as fp:
+        fp.write(driver.page_source)
+ + + + \ No newline at end of file diff --git "a/linux/applications/Nginx\351\205\215\347\275\256.html" "b/linux/applications/Nginx\351\205\215\347\275\256.html" new file mode 100644 index 000000000..075e38133 --- /dev/null +++ "b/linux/applications/Nginx\351\205\215\347\275\256.html" @@ -0,0 +1,103 @@ + + + + + + nginx配置 | 故事 + + + + + + + + + + + + + + +
Skip to content

nginx配置

记录常用、踩坑的nginx配置内容

http

upstream

upstream指令主要用于负载均衡,设置一系列的后端服务器

server

server块的指令主要用于指定主机和端口

listen

监听的端口号

  • default_server:定义默认的 server 处理没有成功匹配 server_name 的请求,如果没有显式定义,则会选取第一个定义的server作为default_server。

    • 显式定义:listen 80 default_server

    • 隐式定义

      nginx
      http {
      +    # 如果没有显式声明 default server 则第一个 server 会被隐式的设为 default server
      +    server {
      +        listen 80;
      +        server_name _; # _ 并不是重点 __ 也可以 ___也可以
      +        return 403; # 403 forbidden
      +    }
      +    
      +    server {
      +        listen 80;
      +        server_name www.a.com;
      +        ...
      +    }
      +}
      http {
      +    # 如果没有显式声明 default server 则第一个 server 会被隐式的设为 default server
      +    server {
      +        listen 80;
      +        server_name _; # _ 并不是重点 __ 也可以 ___也可以
      +        return 403; # 403 forbidden
      +    }
      +    
      +    server {
      +        listen 80;
      +        server_name www.a.com;
      +        ...
      +    }
      +}

server_name

  • server_name storyxc.com:完整匹配

  • server_name *.storyxc.com*开始的通配符匹配

    • 特殊情况:.storyxc.com能同时匹配storyxc.com*.storyxc.com

    A wildcard name may contain an asterisk only on the name’s start or end, and only on a dot border. The names “www.*.example.org” and “w*.example.org” are invalid. However, these names can be specified using regular expressions, for example, “~^www\..+\.example\.org$” and “~^w.*\.example\.org$”. An asterisk can match several name parts. The name “*.example.org” matches not only www.example.org but www.sub.example.org as well.

    A special wildcard name in the form “.example.org” can be used to match both the exact name “example.org” and the wildcard name “*.example.org”.

  • server_name mail.* *结尾的通配符匹配

  • server_name ~^(?<user>.+)\.storyxc\.com$第一个匹配的正则表达式(按照配置文件中出现的顺序)

  • server_name _:通常使用_作为default server的server_name

location

location块用于匹配网页位置

匹配规则

location支持正则表达式匹配,也支持条件判断匹配

语法规则: location [=|~|~*|^~] /uri/ { … }

  • =:完全精确匹配
  • ^~:表示uri以某个常规字符串开头,理解为匹配url路径即可,nginx不对url进行编码
  • ~:表示区分大小写的正则匹配
  • ~*:表示不区分大小写的正则匹配
  • !~:区分大小写不匹配
  • !~*:不区分大小写不匹配
  • /:通用匹配,优先级最低

匹配顺序:=最高,正则匹配其次(按照规则顺序),通用匹配/最低,匹配成功时停止匹配按照当前规则处理请求

nginx
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {
+    # 匹配所有扩展名以.gif、.jpg、.jpeg、.png、.bmp、.swf结尾的静态文件
+    root /wwwroot/xxx;
+    # expires用来指定静态文件的过期时间,这里是30天
+    expires 30d;
+}
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {
+    # 匹配所有扩展名以.gif、.jpg、.jpeg、.png、.bmp、.swf结尾的静态文件
+    root /wwwroot/xxx;
+    # expires用来指定静态文件的过期时间,这里是30天
+    expires 30d;
+}
nginx
location ~ ^/(upload|html)/ {
+	# 匹配所有/upload /html
+	root /web/wwwroot/www.cszhi.com;
+	expires 30d;
+}
location ~ ^/(upload|html)/ {
+	# 匹配所有/upload /html
+	root /web/wwwroot/www.cszhi.com;
+	expires 30d;
+}

location和proxy_pass是否带/的影响

https://github.com/xqin/nginx-proxypass-server-paths

Case #Nginx locationproxy_pass URLTest URLPath received
01/test01http://127.0.0.1:8080/test01/abc/test/test01/abc/test
02/test02http://127.0.0.1:8080//test02/abc/test//abc/test
03/test03/http://127.0.0.1:8080/test03/abc/test/test03/abc/test
04/test04/http://127.0.0.1:8080//test04/abc/test/abc/test
05/test05http://127.0.0.1:8080/app1/test05/abc/test/app1/abc/test
06/test06http://127.0.0.1:8080/app1//test06/abc/test/app1//abc/test
07/test07/http://127.0.0.1:8080/app1/test07/abc/test/app1abc/test
08/test08/http://127.0.0.1:8080/app1//test08/abc/test/app1/abc/test
09/http://127.0.0.1:8080/test09/abc/test/test09/abc/test
10/http://127.0.0.1:8080//test10/abc/test/test10/abc/test
11/http://127.0.0.1:8080/app1/test11/abc/test/app1test11/abc/test
12/http://127.0.0.1:8080/app2//test12/abc/test/app2/test12/abc/test

root和alias区别

  • alias指定的是准确目录,且最后必须是/,否则就会访问失败
  • root是指定目录的上级目录
nginx
location /abc {
+  root /wwwroot/aaa;
+ 	# 此规则匹配的最终资源路径为/wwwroot/aaa/abc/
+  # 如果访问的是/abc/a.html,则最终访问的资源是服务器中的/wwwroot/aaa/abc/a.html
+  index index.html;
+}
+
+location /abc {
+  alias /wwwroot/aaa/;
+  # 此规则匹配的最终资源路径为/wwwroot/aaa/
+  # 如果访问的是/abc/b.txt,则最终访问的资源是/wwwroot/aaa/b.txt
+  index index.html;
+}
location /abc {
+  root /wwwroot/aaa;
+ 	# 此规则匹配的最终资源路径为/wwwroot/aaa/abc/
+  # 如果访问的是/abc/a.html,则最终访问的资源是服务器中的/wwwroot/aaa/abc/a.html
+  index index.html;
+}
+
+location /abc {
+  alias /wwwroot/aaa/;
+  # 此规则匹配的最终资源路径为/wwwroot/aaa/
+  # 如果访问的是/abc/b.txt,则最终访问的资源是/wwwroot/aaa/b.txt
+  index index.html;
+}

访问静态资源重定向问题

当nginx监听的不是80端口时,访问文件夹且末尾不是/,则nginx会进行301永久重定向,此时会丢掉客户端访问时的端口号,可以通过以下配置解决,作用是将不以 / 结尾的目录 URL 重定向至以 / 结尾的目录 URL。使用 -d 判断 $request_filename 是否为一个目录,如果是,则使用 rewrite 指令进行重写。其中,[^/]$ 表示匹配不以 / 结尾的 URL,即目录 URL,$scheme://$http_host$uri/ 表示重定向目标 URL,其中使用了 $scheme 变量表示客户端请求所使用的协议(HTTP 或 HTTPS)、$http_host 变量表示客户端请求的 HOST 头部信息、$uri 变量表示客户端请求的 URI。

nginx
location / {
+    if (-d $request_filename) {
+        rewrite [^/]$ $scheme://$http_host$uri/ permanent;
+    }
+    try_files $uri $uri/ /index.html;
+}
location / {
+    if (-d $request_filename) {
+        rewrite [^/]$ $scheme://$http_host$uri/ permanent;
+    }
+    try_files $uri $uri/ /index.html;
+}

变量

变量名作用
$scheme请求使用的协议 (http 或 https)
$host当前请求的主机名,不包括端口号。
$http_host完整的HTTP主机头,包括主机名和端口号。
$request_uri完整的请求 URI,包括查询字符串
$uri当前请求的 URI (不包含请求参数)
$args当前请求的参数部分,不包括问号
$request_method当前请求的方法 (GET、POST 等)
$remote_addr客户端 IP 地址
$server_addr服务器 IP 地址
$server_name当前请求的服务器名称
$server_protocol服务器使用的协议版本
$request_filename当前请求的文件路径和名称
$document_root当前请求的根目录
$is_args如果请求包含参数部分,值为 ?,否则为空字符串
$query_string当前请求的查询字符串部分,包括问号(?)
$http_user_agent客户端发送的 User-Agent 头部信息
$http_referer客户端发送的 Referer 头部信息
$http_cookie客户端发送的 Cookie 头部信息
$remote_port客户端端口号
$server_port服务器端口号
$realpath_root请求根目录的实际路径
$content_type请求的内容类型
$content_length请求的内容长度
$request_body请求的主体内容

proxy_set_header

配置指令作用
proxy_set_header Host $host;设置代理请求的主机头,通常用于传递客户端的原始主机头。$http_host传递包含端口号。
proxy_set_header X-Real-IP $remote_addr;设置代理请求的客户端真实IP地址,用于传递客户端的真实IP地址给后端服务器。
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;用于将客户端的原始IP地址添加到X-Forwarded-For头中,以便后端服务器知道请求的真实来源。
proxy_set_header X-Forwarded-Proto $scheme;设置代理请求的协议(HTTP或HTTPS),以便后端服务器知道请求的协议类型。
proxy_set_header User-Agent $http_user_agent;传递客户端的User-Agent头,用于识别客户端的浏览器或应用程序。
proxy_set_header Referer $http_referer;传递客户端的Referer头,通常用于跟踪页面来源。
proxy_set_header Cookie $http_cookie;传递客户端的Cookie头,以便后端服务器可以访问客户端的Cookie数据。
proxy_set_header Connection "";清除代理请求的Connection头,通常用于避免代理干扰连接的管理。
proxy_set_header Upgrade $http_upgrade;用于处理WebSocket连接的Upgrade头,通常与WebSocket代理一起使用。
proxy_set_header X-Frame-Options SAMEORIGIN;用于设置X-Frame-Options头,控制网页是否可以嵌套在其他网页中显示。
+ + + + \ No newline at end of file diff --git a/linux/applications/Rclone.html b/linux/applications/Rclone.html new file mode 100644 index 000000000..a00bcd236 --- /dev/null +++ b/linux/applications/Rclone.html @@ -0,0 +1,351 @@ + + + + + + Rclone | 故事 + + + + + + + + + + + + + + +
Skip to content

Rclone

rclone 作为文件和对象存储的管理工具, 经过近些年的发展已经完好的支持各种存储协议, 比如 HDFS, FTP, SFTP, GCS 和 S3(兼容 aws, 金山云, 腾讯云, 阿里云等)等, 逐渐有统一管理云存储之势, 从 rclone-github 来看, 各大云厂商也逐渐将各自的存储协议合并到了 rclone 中. 这在对象存储统一管理, 尤其是多云管理的场景中带来了很大的便利, 也便于我们实现诸如统一运维管理的目标.

安装

两种方式:

  • apt install rclone
  • curl https://rclone.org/install.sh | sudo bash

配置

bash
rclone config
+2023/10/20 16:58:06 NOTICE: Config file "/root/.config/rclone/rclone.conf" not found - using defaults
+No remotes found - make a new one
+n) New remote
+s) Set configuration password
+q) Quit config
+n/s/q>n
+name>alist
+Type of storage to configure.
+Enter a string value. Press Enter for the default ("").
+Choose a number from below, or type in your own value
+ 1 / 1Fichier
+   \ "fichier"
+ 2 / Alias for an existing remote
+   \ "alias"
+ 3 / Amazon Drive
+   \ "amazon cloud drive"
+ 4 / Amazon S3 Compliant Storage Provider (AWS, Alibaba, Ceph, Digital Ocean, Dreamhost, IBM COS, Minio, Tencent COS, etc)
+   \ "s3"
+ 5 / Backblaze B2
+   \ "b2"
+ 6 / Box
+   \ "box"
+ 7 / Cache a remote
+   \ "cache"
+ 8 / Citrix Sharefile
+   \ "sharefile"
+ 9 / Dropbox
+   \ "dropbox"
+10 / Encrypt/Decrypt a remote
+   \ "crypt"
+11 / FTP Connection
+   \ "ftp"
+12 / Google Cloud Storage (this is not Google Drive)
+   \ "google cloud storage"
+13 / Google Drive
+   \ "drive"
+14 / Google Photos
+   \ "google photos"
+15 / Hubic
+   \ "hubic"
+16 / In memory object storage system.
+   \ "memory"
+17 / Jottacloud
+   \ "jottacloud"
+18 / Koofr
+   \ "koofr"
+19 / Local Disk
+   \ "local"
+20 / Mail.ru Cloud
+   \ "mailru"
+21 / Microsoft Azure Blob Storage
+   \ "azureblob"
+22 / Microsoft OneDrive
+   \ "onedrive"
+23 / OpenDrive
+   \ "opendrive"
+24 / OpenStack Swift (Rackspace Cloud Files, Memset Memstore, OVH)
+   \ "swift"
+25 / Pcloud
+   \ "pcloud"
+26 / Put.io
+   \ "putio"
+27 / SSH/SFTP Connection
+   \ "sftp"
+28 / Sugarsync
+   \ "sugarsync"
+29 / Transparently chunk/split large files
+   \ "chunker"
+30 / Union merges the contents of several upstream fs
+   \ "union"
+31 / Webdav
+   \ "webdav"
+32 / Yandex Disk
+   \ "yandex"
+33 / http Connection
+   \ "http"
+34 / premiumize.me
+   \ "premiumizeme"
+35 / seafile
+   \ "seafile"
+Storage> 31
+** See help for webdav backend at: https://rclone.org/webdav/ **
+
+URL of http host to connect to
+Enter a string value. Press Enter for the default ("").
+Choose a number from below, or type in your own value
+ 1 / Connect to example.com
+   \ "https://example.com"
+url> https://alist.storyxc.com:63443
+Name of the Webdav site/service/software you are using
+Enter a string value. Press Enter for the default ("").
+Choose a number from below, or type in your own value
+ 1 / Nextcloud
+   \ "nextcloud"
+ 2 / Owncloud
+   \ "owncloud"
+ 3 / Sharepoint
+   \ "sharepoint"
+ 4 / Other site/service or software
+   \ "other"
+vendor> 4
+User name
+Enter a string value. Press Enter for the default ("").
+user> 
+Password.
+y) Yes type in my own password
+g) Generate random password
+n) No leave this optional password blank (default)
+y/g/n> n
+Enter the password:
+password:
+Confirm the password:
+password:
+Bearer token instead of user/pass (eg a Macaroon)
+Enter a string value. Press Enter for the default ("").
+bearer_token>
+Edit advanced config? (y/n)
+y) Yes
+n) No (default)
+y/n> n
+Remote config
+--------------------
+[alist]
+url = 
+vendor = other
+user = 
+pass = *** ENCRYPTED ***
+--------------------
+y) Yes this is OK (default)
+e) Edit this remote
+d) Delete this remote
+y/e/d> y
+
+Current remotes:
+
+Name                 Type
+====                 ====
+alist                webdav
+
+e) Edit existing remote
+n) New remote
+d) Delete remote
+r) Rename remote
+c) Copy remote
+s) Set configuration password
+q) Quit config
+e/n/d/r/c/s/q> q
rclone config
+2023/10/20 16:58:06 NOTICE: Config file "/root/.config/rclone/rclone.conf" not found - using defaults
+No remotes found - make a new one
+n) New remote
+s) Set configuration password
+q) Quit config
+n/s/q>n
+name>alist
+Type of storage to configure.
+Enter a string value. Press Enter for the default ("").
+Choose a number from below, or type in your own value
+ 1 / 1Fichier
+   \ "fichier"
+ 2 / Alias for an existing remote
+   \ "alias"
+ 3 / Amazon Drive
+   \ "amazon cloud drive"
+ 4 / Amazon S3 Compliant Storage Provider (AWS, Alibaba, Ceph, Digital Ocean, Dreamhost, IBM COS, Minio, Tencent COS, etc)
+   \ "s3"
+ 5 / Backblaze B2
+   \ "b2"
+ 6 / Box
+   \ "box"
+ 7 / Cache a remote
+   \ "cache"
+ 8 / Citrix Sharefile
+   \ "sharefile"
+ 9 / Dropbox
+   \ "dropbox"
+10 / Encrypt/Decrypt a remote
+   \ "crypt"
+11 / FTP Connection
+   \ "ftp"
+12 / Google Cloud Storage (this is not Google Drive)
+   \ "google cloud storage"
+13 / Google Drive
+   \ "drive"
+14 / Google Photos
+   \ "google photos"
+15 / Hubic
+   \ "hubic"
+16 / In memory object storage system.
+   \ "memory"
+17 / Jottacloud
+   \ "jottacloud"
+18 / Koofr
+   \ "koofr"
+19 / Local Disk
+   \ "local"
+20 / Mail.ru Cloud
+   \ "mailru"
+21 / Microsoft Azure Blob Storage
+   \ "azureblob"
+22 / Microsoft OneDrive
+   \ "onedrive"
+23 / OpenDrive
+   \ "opendrive"
+24 / OpenStack Swift (Rackspace Cloud Files, Memset Memstore, OVH)
+   \ "swift"
+25 / Pcloud
+   \ "pcloud"
+26 / Put.io
+   \ "putio"
+27 / SSH/SFTP Connection
+   \ "sftp"
+28 / Sugarsync
+   \ "sugarsync"
+29 / Transparently chunk/split large files
+   \ "chunker"
+30 / Union merges the contents of several upstream fs
+   \ "union"
+31 / Webdav
+   \ "webdav"
+32 / Yandex Disk
+   \ "yandex"
+33 / http Connection
+   \ "http"
+34 / premiumize.me
+   \ "premiumizeme"
+35 / seafile
+   \ "seafile"
+Storage> 31
+** See help for webdav backend at: https://rclone.org/webdav/ **
+
+URL of http host to connect to
+Enter a string value. Press Enter for the default ("").
+Choose a number from below, or type in your own value
+ 1 / Connect to example.com
+   \ "https://example.com"
+url> https://alist.storyxc.com:63443
+Name of the Webdav site/service/software you are using
+Enter a string value. Press Enter for the default ("").
+Choose a number from below, or type in your own value
+ 1 / Nextcloud
+   \ "nextcloud"
+ 2 / Owncloud
+   \ "owncloud"
+ 3 / Sharepoint
+   \ "sharepoint"
+ 4 / Other site/service or software
+   \ "other"
+vendor> 4
+User name
+Enter a string value. Press Enter for the default ("").
+user> 
+Password.
+y) Yes type in my own password
+g) Generate random password
+n) No leave this optional password blank (default)
+y/g/n> n
+Enter the password:
+password:
+Confirm the password:
+password:
+Bearer token instead of user/pass (eg a Macaroon)
+Enter a string value. Press Enter for the default ("").
+bearer_token>
+Edit advanced config? (y/n)
+y) Yes
+n) No (default)
+y/n> n
+Remote config
+--------------------
+[alist]
+url = 
+vendor = other
+user = 
+pass = *** ENCRYPTED ***
+--------------------
+y) Yes this is OK (default)
+e) Edit this remote
+d) Delete this remote
+y/e/d> y
+
+Current remotes:
+
+Name                 Type
+====                 ====
+alist                webdav
+
+e) Edit existing remote
+n) New remote
+d) Delete remote
+r) Rename remote
+c) Copy remote
+s) Set configuration password
+q) Quit config
+e/n/d/r/c/s/q> q

systemd开机自动挂载

shell
mkdir /alist
+#将后面修改成你上面手动运行命令中,除了rclone的全部参数
+command="mount alist:/ /alist --cache-dir /tmp --allow-other --vfs-cache-mode writes --allow-non-empty"
+#以下是一整条命令,一起复制到SSH客户端运行
+cat > /etc/systemd/system/rclone.service <<EOF
+[Unit]
+Description=Rclone
+# 在网络和docker服务之后启动,因为要先启动阿里云盘的webdav服务容器
+After=network-online.target docker.service
+[Service]
+Type=simple
+ExecStart=$(command -v rclone) ${command}
+Restart=on-abort
+User=root
+[Install]
+WantedBy=default.target
+EOF
mkdir /alist
+#将后面修改成你上面手动运行命令中,除了rclone的全部参数
+command="mount alist:/ /alist --cache-dir /tmp --allow-other --vfs-cache-mode writes --allow-non-empty"
+#以下是一整条命令,一起复制到SSH客户端运行
+cat > /etc/systemd/system/rclone.service <<EOF
+[Unit]
+Description=Rclone
+# 在网络和docker服务之后启动,因为要先启动阿里云盘的webdav服务容器
+After=network-online.target docker.service
+[Service]
+Type=simple
+ExecStart=$(command -v rclone) ${command}
+Restart=on-abort
+User=root
+[Install]
+WantedBy=default.target
+EOF

启动:

shell
systemctl start rclone
systemctl start rclone

开机自启动:

shell
systemctl enable rclone
systemctl enable rclone
+ + + + \ No newline at end of file diff --git a/linux/applications/iptables.html b/linux/applications/iptables.html new file mode 100644 index 000000000..e57dc7844 --- /dev/null +++ b/linux/applications/iptables.html @@ -0,0 +1,35 @@ + + + + + + iptables | 故事 + + + + + + + + + + + + + + +
Skip to content

iptables

iptables是一个用户级程序,用于操作内核级的网络模块netfilter

表和链

iptables的功能由表的形式呈现,每张表由若干个链组成,每个链可以分配一组规则

iptables有五张内建表,按照优先级高到底分别是:Raw、Mangle、NAT、Filter、Security

Raw

此表负责数据包标记,决定数据包是否被状态跟踪机制处理,Raw表有2个内建链

  • PREROUTING:用于通过任何网络接口到达的数据包
  • OUTPUT:针对本地进程产生的数据包

Mangle

此表负责更改数据包内容,Mangle表有5个内建链

  • PREROUTING
  • OUTPUT
  • FORWARD
  • INPUT
  • POSTROUTING

NAT

此表负责数据包的ip地址转换,NAT表有3种内建链

  • PREROUTING:处理刚到达本机并在路由转发前的数据包。它会转换数据包中的目标IP地址(destination ip address),通常用于DNAT(destination NAT)。
  • POSTROUTING:处理即将离开本机的数据包。它会转换数据包中的源IP地址(source ip address),通常用于SNAT(source NAT)。
  • OUTPUT:处理本机产生的数据包。

Filter

此表负责过滤数据包,iptables的默认表,具有3种内建链

  • INPUT:处理来自外部的数据。
  • OUTPUT:处理向外发送的数据。
  • FORWARD:将数据转发到本机的其他网卡设备上。

Security

新加入的特性,用于强制访问控制(MAC)网络规则,有3种内建链

  • INPUT
  • OUTPUT
  • FORWARD

iptables数据包处理流程图

v2-6c9358844d8f440486551d925dfe36b5_1440w

iptables语法

语法

iptables [-t 表名] 命令选项 [链名] [匹配条件] [-j 策略]

iptables命令包含五个部分

  • 表名:要操作的表,不指定时默认操作Filter表

  • 链名:要操作的链,不指定链时默认表内所有链

  • 命令选项:要进行的操作

  • 匹配条件:定义规则适用哪些数据包(匹配哪些数据包)

  • 策略:匹配数据的目标执行的操作,说白了就是packet匹配上规则后该干嘛

常见的命令选项

  • -A 在指定链的末尾添加(append)一条新的规则
  • -D 删除(delete)指定链中的某一条规则,可以按规则序号和内容删除
  • -I 在指定链中插入(insert)一条新的规则,默认在第一行添加
  • -R 修改、替换(replace)指定链中的某一条规则,可以按规则序号和内容替换
  • -L 列出(list)指定链中所有的规则进行查看
  • -E 重命名用户定义的链,不改变链本身
  • -F 清空(flush)
  • -N 新建(new-chain)一条用户自己定义的规则链
  • -X 删除指定表中用户自定义的规则链(delete-chain)
  • -P 设置指定链的默认策略(policy)
  • -Z 将所有表的所有链的字节和数据包计数器清零
  • -n 使用数字形式(numeric)显示输出结果
  • -v 查看规则表详细信息(verbose)的信息
  • -V 查看版本(version)
  • -h 获取帮助(help)

常见策略(target)

  • ACCEPT:允许数据包通过。

  • DROP:直接丢弃数据包,不给任何回应信息,这时候客户端会感觉自己的请求泥牛入海了,过了超时时间才会有反应。

  • REJECT:拒绝数据包通过,必要时会给数据发送端一个响应的信息,客户端刚请求就会收到拒绝的信息。

  • SNAT:源地址转换,解决内网用户用同一个公网地址上网的问题。

  • MASQUERADE:是SNAT的一种特殊形式,适用于动态的、临时会变的ip上。

  • DNAT:目标地址转换。

  • REDIRECT:在本机做端口映射。

添加规则

  • -A:在链的末尾追加一条规则,例如iptables -A INPUT -s 127.0.0.1 -p tcp --dport 3306 -j ACCEPT
  • -I:在链的开头(或指定序号)插入一条规则,例如iptables -I INPUT -p tcp -j ACCEPTiptables -I INPUT 2 -p tcp -j ACCEPT

查看规则

  • -L:列出所有规则

  • -n:以数字形式显示地址、端口等信息

  • -v:显示更详细规则信息

  • --line-numbers:显示规则序号

删除规则

  • 删除nat表INPUT链的第三条规则:iptables -t nat -D INPUT 3

  • 清空nat表所有规则:iptables -t nat -F

给指定的链设置默认策略

  • iptables -t filter -P FORWARD DROP

通用匹配

协议匹配 -p(protocol)

  • 指定规则的协议,如tcp, udp, icmp等,可以使用all来指定所有协议。
  • 如果不指定**-p参数,则默认是all**值。这并不明智,请总是明确指定协议名称。
  • 可以使用协议名(如tcp),或者是协议值(比如6代表tcp)来指定协议。映射关系请查看/etc/protocols
  • 还可以使用**–protocol参数代替-p**参数

地址匹配 -s -d:

-s 源地址
  • 指定数据包的源地址
  • 参数可以使IP地址、网络地址、主机名
  • 例如:-s 192.168.1.101指定IP地址
  • 例如:-s 192.168.1.10/24指定网络地址
  • 如果不指定-s参数,就代表所有地址
  • 还可以使用**–src或者–source**
-d 目标地址
  • 指定目的地址
  • 参数和**-s**相同
  • 还可以使用**–dst或者–destination**

接口匹配 -i -o

-i 输入接口(input interface)
  • -i代表输入接口(input interface)
  • -i指定了要处理来自哪个接口的数据包
  • 这些数据包即将进入INPUT, FORWARD, PREROUTE链
  • 例如:-i eth0指定了要处理经由eth0进入的数据包
  • 如果不指定**-i**参数,那么将处理进入所有接口的数据包
  • 如果出现**!** -i eth0,那么将处理所有经由eth0以外的接口进入的数据包
  • 如果出现-i eth**+,那么将处理所有经由eth开头的接口**进入的数据包
  • 还可以使用**–in-interface**参数
-o 输出(out interface)
  • -o代表”output interface”
  • -o指定了数据包由哪个接口输出
  • 这些数据包即将进入FORWARD, OUTPUT, POSTROUTING链
  • 如果不指定**-o**选项,那么系统上的所有接口都可以作为输出接口
  • 如果出现**!** -o eth0,那么将从eth0以外的接口输出
  • 如果出现-i eth**+,那么将仅从eth开头的接口**输出
  • 还可以使用**–out-interface**参数

隐含匹配

端口匹配

-–sport 源端口(source port) 针对-p tcp 或者 -p udp
  • 缺省情况下,将匹配所有端口
  • 可以指定端口号或者端口名称,例如”–sport 22″与”–sport ssh”。
  • /etc/services文件描述了上述映射关系。
  • 从性能上讲,使用端口号更好
  • 使用冒号可以匹配端口范围,如”–sport 22:100″
  • 还可以使用”–source-port”
–-dport 目的端口(destination port) 针对-p tcp 或者 -p udp
  • 参数和–sport类似
  • 还可以使用”–destination-port”
-–tcp-flags TCP标志 针对-p tcp
  • 可以指定由逗号分隔的多个参数
  • 有效值可以是:SYN, ACK, FIN, RST, URG, PSH
  • 可以使用ALL或者NONE
-–icmp-type ICMP类型 针对-p icmp
  • –icmp-type 0 表示Echo Reply
  • –icmp-type 8 表示Echo

显式匹配

多端口匹配 -m multiport --sport 源端口列表 -m multiport --dport 目的端口列表 IP范围匹配 -m iprange --src-range IP范围 MAC地址匹配 -m mac –mac1-source MAC地址 状态匹配 -m state --state 连接状态

shell
iptables -A INPUT -p tcp -m multiport --dport 25,80,110,143 -j ACCEPT
+iptables -A FORWARD -p tcp -m iprange --src-range 192.168.4.21-192.168.4.28 -j ACCEPT
+iptables -A INPUT -m mac --mac-source 00:0c:29:c0:55:3f -j DROP
+iptables -P INPUT DROP
+iptables -I INPUT -p tcp -m multiport --dport 80-82,85 -j ACCEPT
+iptables -I INPUT -p tcp -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp -m multiport --dport 25,80,110,143 -j ACCEPT
+iptables -A FORWARD -p tcp -m iprange --src-range 192.168.4.21-192.168.4.28 -j ACCEPT
+iptables -A INPUT -m mac --mac-source 00:0c:29:c0:55:3f -j DROP
+iptables -P INPUT DROP
+iptables -I INPUT -p tcp -m multiport --dport 80-82,85 -j ACCEPT
+iptables -I INPUT -p tcp -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
+ + + + \ No newline at end of file diff --git "a/linux/applications/screen\347\232\204\350\277\233\351\230\266\347\224\250\346\263\225.html" "b/linux/applications/screen\347\232\204\350\277\233\351\230\266\347\224\250\346\263\225.html" new file mode 100644 index 000000000..17d5a81b8 --- /dev/null +++ "b/linux/applications/screen\347\232\204\350\277\233\351\230\266\347\224\250\346\263\225.html" @@ -0,0 +1,37 @@ + + + + + + screen的进阶用法 | 故事 + + + + + + + + + + + + + + +
Skip to content

screen的进阶用法

以detatch模式创建daemon会话

screen -dmS <name>

向会话窗口中发送文本

screen -S <name> -X stuff <text>

例如:screen -S <name> -X stuff abc,当attach之后,窗口中已经有了abc

如果想执行命令:

shell
screen -S <name> -X stuff "<command> \n"
+screen -S <name> -X stuff "<command> \r"
+---
+screen -S centos -X stuff  $'<command> \n'
+---
+screen -S new_screen -X stuff "cd /dir
+"
screen -S <name> -X stuff "<command> \n"
+screen -S <name> -X stuff "<command> \r"
+---
+screen -S centos -X stuff  $'<command> \n'
+---
+screen -S new_screen -X stuff "cd /dir
+"

清理screen窗口

screen -S <name> -X quit

+ + + + \ No newline at end of file diff --git "a/linux/env/ArchLinux\345\256\211\350\243\205.html" "b/linux/env/ArchLinux\345\256\211\350\243\205.html" new file mode 100644 index 000000000..bc04380de --- /dev/null +++ "b/linux/env/ArchLinux\345\256\211\350\243\205.html" @@ -0,0 +1,105 @@ + + + + + + ArchLinux安装 | 故事 + + + + + + + + + + + + + + +
Skip to content

ArchLinux安装

安装指南:https://wiki.archlinux.org/title/Installation_guide

镜像下载:https://archlinux.org/download/

使用UEFI模式引导

禁用Secure Boot

安装

使用ssh连接操作

ip add查看当前ip

passwd修改密码

设置镜像(可选)

shell
reflector -c China --sort rate --save /etc/pacman.d/mirrorlist
+reflector -c China --sort rate --save /mnt/etc/pacman.d/mirrorlist
reflector -c China --sort rate --save /etc/pacman.d/mirrorlist
+reflector -c China --sort rate --save /mnt/etc/pacman.d/mirrorlist

文件系统

查看磁盘设备情况fdisk -llsblk,以rom、loop、airoot结尾的设备可以忽略

磁盘分区

https://wiki.archlinux.org/title/EFI_system_partition

arch linux推荐的分区表设置

挂载点分区分区类型建议大小
/mnt/boot/dev/efi_system_partitionEFI system partitionAt least 300 MiB. If multiple kernels will be installed, then no less than 1 GiB.
[SWAP]/dev/*swap_partitionLinux swapMore than 512 MiB
/mnt/dev/root_partitionLinux x86-64 root (/)Remainder of the device
操作
shell
fdisk /dev/sda
+
+g # 将磁盘分区表设置为GPT格式
+
+n # 新增一个分区,分区号默认会递增,直接回车,起始扇区直接回车按默认值,结束输入+300M并回车,标识分区大小为300M
+# 重复新建分区操作,创建好3个空间大小分别为300M、1G、剩余全部空间的三个分区
+
+t # 更改分区类型
+# 重复t操作 把1、2、3分区分别改为1(EFI System Partition)、19(SWAP)、23(linux x86-64 root)
+
+w # 写入保存分区表
fdisk /dev/sda
+
+g # 将磁盘分区表设置为GPT格式
+
+n # 新增一个分区,分区号默认会递增,直接回车,起始扇区直接回车按默认值,结束输入+300M并回车,标识分区大小为300M
+# 重复新建分区操作,创建好3个空间大小分别为300M、1G、剩余全部空间的三个分区
+
+t # 更改分区类型
+# 重复t操作 把1、2、3分区分别改为1(EFI System Partition)、19(SWAP)、23(linux x86-64 root)
+
+w # 写入保存分区表
磁盘格式化
shell
mkfs.fat -F 32 /dev/sda1
+mkswap /dev/sda2
+mkfs.ext4 /dev/sda3
mkfs.fat -F 32 /dev/sda1
+mkswap /dev/sda2
+mkfs.ext4 /dev/sda3
分区挂载到文件系统
shell
mount /dev/sda3 /mnt
+mkdir -p /mnt/boot && mount /dev/sda1 /mnt/boot #创建/mnt/boot目录供挂载
+swapon /dev/sda2 # 挂载swap
mount /dev/sda3 /mnt
+mkdir -p /mnt/boot && mount /dev/sda1 /mnt/boot #创建/mnt/boot目录供挂载
+swapon /dev/sda2 # 挂载swap

安装基本软件包

shell
pacstrap -K /mnt base linux linux-firmware
+pacman -Sy
+pacman -S vim
pacstrap -K /mnt base linux linux-firmware
+pacman -Sy
+pacman -S vim

生成fstab

shell
genfstab -U /mnt >> /mnt/etc/fstab && cat /mnt/etc/fstab
genfstab -U /mnt >> /mnt/etc/fstab && cat /mnt/etc/fstab

更换当前根目录到硬盘上的系统

https://wiki.archlinux.org/title/Chroot

shell
arch-chroot /mnt
arch-chroot /mnt

调整时区

shell
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime # 切换时区到东8区 或者timedatectl set-timezone Asia/Shanghai
+
+hwclock --systohc # 将系统时间同步到硬件时钟 生成/etc/adjtime文件
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime # 切换时区到东8区 或者timedatectl set-timezone Asia/Shanghai
+
+hwclock --systohc # 将系统时间同步到硬件时钟 生成/etc/adjtime文件

编辑/etc/locale.gen配置本地化

shell
vim /etc/locale.gen
+# 将en_US.UTF-8和zh_CN.UTF-8注释去掉
+# 保存
+locale-gen
vim /etc/locale.gen
+# 将en_US.UTF-8和zh_CN.UTF-8注释去掉
+# 保存
+locale-gen

创建/etc/locale.conf文件并编辑

shell
#/etc/locale.conf
+LANG=en_US.UTF-8
#/etc/locale.conf
+LANG=en_US.UTF-8

主机名

shell
echo "archlinux" >> /etc/hostname
echo "archlinux" >> /etc/hostname

设置密码

shell
passwd
passwd

引导程序

shell
pacman -S dosfstools grub efibootmgr  # 安装引导程序
+grub-install --target=x86_64-efi --efi-directory=/boot --recheck  # 将grub安装至EFI分区
+grub-mkconfig -o /boot/grub/grub.cfg  # 生成grub配置
pacman -S dosfstools grub efibootmgr  # 安装引导程序
+grub-install --target=x86_64-efi --efi-directory=/boot --recheck  # 将grub安装至EFI分区
+grub-mkconfig -o /boot/grub/grub.cfg  # 生成grub配置

应用安装

shell
pacman -S networkmanager network-manager-applet dhcpcd dialog os-prober mtools ntfs-3g base-devel linux-headers reflector git net-tools dnsutils inetutils iproute2
pacman -S networkmanager network-manager-applet dhcpcd dialog os-prober mtools ntfs-3g base-devel linux-headers reflector git net-tools dnsutils inetutils iproute2

退出chroot环境重新启动

shell
exit  # 返回至arch-chroot之前的环境
+
+# 卸载
+umount /mnt/boot
+umount /mnt
+reboot  # 重启
exit  # 返回至arch-chroot之前的环境
+
+# 卸载
+umount /mnt/boot
+umount /mnt
+reboot  # 重启

安装后操作

shell
systemctl start dhcpcd  
+systemctl enable dhcpcd  # dhcpcd开机自启
+
+systemctl start sshd
+systemctl enable sshd # sshd开机自启
systemctl start dhcpcd  
+systemctl enable dhcpcd  # dhcpcd开机自启
+
+systemctl start sshd
+systemctl enable sshd # sshd开机自启

ssh配置

shell
# /etc/ssh/sshd_config
+PermitRootLogin yes
+PasswordAuthentication yes
# /etc/ssh/sshd_config
+PermitRootLogin yes
+PasswordAuthentication yes

systemctl restart sshd

静态ip

https://wiki.archlinux.org/title/Dhcpcd

shell
# /etc/dhcpcd.conf
+interface eth0
+static ip_address=192.168.2.67/24	
+static routers=192.168.2.1
+static domain_name_servers=192.168.2.1
# /etc/dhcpcd.conf
+interface eth0
+static ip_address=192.168.2.67/24	
+static routers=192.168.2.1
+static domain_name_servers=192.168.2.1

General recommendations

https://wiki.archlinux.org/title/General_recommendations

+ + + + \ No newline at end of file diff --git "a/linux/env/Centos7\345\256\211\350\243\205Python3\347\216\257\345\242\203.html" "b/linux/env/Centos7\345\256\211\350\243\205Python3\347\216\257\345\242\203.html" new file mode 100644 index 000000000..4500e7221 --- /dev/null +++ "b/linux/env/Centos7\345\256\211\350\243\205Python3\347\216\257\345\242\203.html" @@ -0,0 +1,45 @@ + + + + + + Centos7安装Python3环境 | 故事 + + + + + + + + + + + + + + +
Skip to content

Centos7安装Python3环境

  1. 安装编译工具
bash
yum -y groupinstall "Development tools"
+yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel
+yum install -y libffi-devel zlib1g-dev
+yum install zlib* -y
yum -y groupinstall "Development tools"
+yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel
+yum install -y libffi-devel zlib1g-dev
+yum install zlib* -y
  1. 下载安装包
bash
wget https://www.python.org/ftp/python/3.7.2/Python-3.7.2.tar.xz
wget https://www.python.org/ftp/python/3.7.2/Python-3.7.2.tar.xz

这一步骤提示我wget命令找不到,所以要先安装wget

bash
yum -y install wget
yum -y install wget

再次执行下载安装包的命令

  1. 下载完成后解压并安装
bash
mkdir /usr/local/python3  
+tar -xvf  Python-3.7.2.tar.xz
+cd Python-3.7.2
+# 指定安装位置 提高运行速度 第三个是为了解决pip需要用到ssl
+./configure --prefix=/usr/local/python3 --with-ssl 
+make && make install
mkdir /usr/local/python3  
+tar -xvf  Python-3.7.2.tar.xz
+cd Python-3.7.2
+# 指定安装位置 提高运行速度 第三个是为了解决pip需要用到ssl
+./configure --prefix=/usr/local/python3 --with-ssl 
+make && make install

./configure --prefix=/usr/local/python3 --enable-optimizations --with-ssl --enable-optimizations参数可能在低版本gcc导致编译报错 去掉即可

  1. 创建软链接
bash
ln -s /usr/local/python3/bin/python3 /usr/local/bin/python3
+ln -s /usr/local/python3/bin/pip3 /usr/local/bin/pip3
ln -s /usr/local/python3/bin/python3 /usr/local/bin/python3
+ln -s /usr/local/python3/bin/pip3 /usr/local/bin/pip3
  1. 验证
bash
python3 -v
+pip3 -v
python3 -v
+pip3 -v
+ + + + \ No newline at end of file diff --git "a/linux/env/Linux\345\270\270\347\224\250\346\214\207\344\273\244.html" "b/linux/env/Linux\345\270\270\347\224\250\346\214\207\344\273\244.html" new file mode 100644 index 000000000..22c65121e --- /dev/null +++ "b/linux/env/Linux\345\270\270\347\224\250\346\214\207\344\273\244.html" @@ -0,0 +1,45 @@ + + + + + + Linux常用指令 | 故事 + + + + + + + + + + + + + + +
Skip to content

Linux常用指令

设置时区

shell
# 查看当前时间、时区
+date 
+# 列出可用时区
+timedatectl list-timezones
+# 设置时区
+timedatectl set-timezone Asia/Shanghai
+# 确认已经更改
+timedatectl
# 查看当前时间、时区
+date 
+# 列出可用时区
+timedatectl list-timezones
+# 设置时区
+timedatectl set-timezone Asia/Shanghai
+# 确认已经更改
+timedatectl

修改主机名

shell
# 查看主机信息
+hostnamectl
+# 修改主机名
+hostnamectl set-hostname newhostname
# 查看主机信息
+hostnamectl
+# 修改主机名
+hostnamectl set-hostname newhostname
+ + + + \ No newline at end of file diff --git "a/linux/env/Linux\346\234\215\345\212\241\345\231\250\346\226\207\344\273\266\347\233\256\345\275\225\345\205\261\344\272\253\346\230\240\345\260\204\351\205\215\347\275\256.html" "b/linux/env/Linux\346\234\215\345\212\241\345\231\250\346\226\207\344\273\266\347\233\256\345\275\225\345\205\261\344\272\253\346\230\240\345\260\204\351\205\215\347\275\256.html" new file mode 100644 index 000000000..bc715ba77 --- /dev/null +++ "b/linux/env/Linux\346\234\215\345\212\241\345\231\250\346\226\207\344\273\266\347\233\256\345\275\225\345\205\261\344\272\253\346\230\240\345\260\204\351\205\215\347\275\256.html" @@ -0,0 +1,27 @@ + + + + + + Linux服务器文件目录共享映射配置 | 故事 + + + + + + + + + + + + + + +
Skip to content

Linux服务器文件目录共享映射配置

使用场景:由于公司没有单独的文件服务器/nas之类的共享存储,图片之类的也没有上云,目前业务中上传的文件也是放在应用服务器当中的。生产环境都是高可用的多节点部署,这样就会产生问题。比如用户上传一张图片,请求被分发到A服务器中,由于图片访问的域名是统一的B服务器域名,上传完成后用B服务器的域名去访问静态文件就会404。后续我们考虑搭建fastdfs来统一管理文件。目前的临时解决方案是通过配置服务器共享目录来实现文件在多个节点服务器间的同步。

准备工作

  • 环境:CentOS 7.6

  • 分别在A、B两台服务器上安装nfs和rpcbind

    • yum install nfs-utils rpcbind

server配置

比如我们的静态资源存储在B服务器,则需要把B服务器的目录暴露出来,共享给A服务器,让A服务器挂载该目录。

  • 在B服务器(提供资源的服务器)上修改/etc/exports文件,把指定的目录暴露给A服务器并分配权限

    bash
    # /需要暴露的目录 A服务器IP(rw,async,no_root_squash)
    +/data/images 192.168.111.1(rw,async,no_root_squash)
    # /需要暴露的目录 A服务器IP(rw,async,no_root_squash)
    +/data/images 192.168.111.1(rw,async,no_root_squash)
  • 修改完后需要使配置立即生效,执行exportfs -arv命令

  • 关闭防火墙/端口111(tcp/udp)、2049(tcp)、4046(udp)向指定ip开放

启动nfs服务和rpcbind服务

  • service rpcbind start
  • service nfs start

在A服务器中挂载远程目录

执行命令:mount -o rw -t nfs B服务器IP:/B服务器暴露的路径 /要映射的本机(A服务器)的路径

例如:mount -o rw -t nfs 192.168.111.2:/data/images /data/images

验证

可以在A服务器的/data/images下上传一些文件,然后看B服务器中的/data/images目录中是否同步,如果同步了则说明挂载成功,共享目录就配置完成了。

+ + + + \ No newline at end of file diff --git "a/linux/env/Linux\347\247\201\351\222\245\347\231\273\351\231\206\346\217\220\347\244\272server refused our key.html" "b/linux/env/Linux\347\247\201\351\222\245\347\231\273\351\231\206\346\217\220\347\244\272server refused our key.html" new file mode 100644 index 000000000..5cd11a9bf --- /dev/null +++ "b/linux/env/Linux\347\247\201\351\222\245\347\231\273\351\231\206\346\217\220\347\244\272server refused our key.html" @@ -0,0 +1,39 @@ + + + + + + Linux私钥登陆提示server refused our key | 故事 + + + + + + + + + + + + + + +
Skip to content

Linux私钥登陆提示server refused our key

背景

家庭内网装了个物理机的Ubuntu server,用的最新版本的22.04,然后用windows端的mobaxterm和navicat使用ssh私钥连接内网服务器时返回了Server refused our key的异常

问题原因

openssh 8.8开始默认禁用了使用SHA-1哈希算法的RSA签名,看了一下ubuntu server 22.04的默认openssh版本:

shell
  ~ ssh -V
+OpenSSH_8.9p1 Ubuntu-3, OpenSSL 3.0.2 15 Mar 2022
  ~ ssh -V
+OpenSSH_8.9p1 Ubuntu-3, OpenSSL 3.0.2 15 Mar 2022

https://www.openssh.com/txt/release-8.8

This release disables RSA signatures using the SHA-1 hash algorithm by default. This change has been made as the SHA-1 hash algorithm is cryptographically broken, and it is possible to create chosen-prefix hash collisions for <USD$50K [1]

解决方案

shell
vim /etc/ssh/sshd_config
+
+# 添加配置
+PubkeyAcceptedKeyTypes +ssh-rsa
+
+
+systemctl restart sshd
vim /etc/ssh/sshd_config
+
+# 添加配置
+PubkeyAcceptedKeyTypes +ssh-rsa
+
+
+systemctl restart sshd
+ + + + \ No newline at end of file diff --git "a/linux/env/Linux\350\256\276\347\275\256swap\347\251\272\351\227\264.html" "b/linux/env/Linux\350\256\276\347\275\256swap\347\251\272\351\227\264.html" new file mode 100644 index 000000000..6f4d59ec6 --- /dev/null +++ "b/linux/env/Linux\350\256\276\347\275\256swap\347\251\272\351\227\264.html" @@ -0,0 +1,25 @@ + + + + + + Linux设置swap空间 | 故事 + + + + + + + + + + + + + + +
Skip to content

Linux设置swap空间

服务器上日常运行着静态博客+云笔记+jenkins的容器,jenkins用于gitee上托管的博客repo的ci/cd。当jenkins进行构建时,内存占用率会急剧升高,轻则容器宕机,重则服务器跟着一起boom。因此需要设置swap来缓解jenkins内存占用瞬时升高的状况。

查看磁盘分区

  • df -h

image-20220310010316020

Filesystem中/dev/vda1挂载点为/的就是我们的磁盘

一般来说swap大小设置规则:

4G以内RAM,Swap设置为2倍RAM

4G-8GRAM,Swap设置等于内存大小

8G-64G,Swap设置为8G

64G-256G,Swap设置为16G

创建Swap分区文件

我这个是腾讯云4c 4G 80G的机器,这里swap设置为4G:fallocate -l 4G /swap

启用Swap分区

修改权限使文件只能root访问:chmod 600 /swap

将文件标记为swap空间:mkswap /swap

启用swap文件:swapon /swap

验证交换是否可用:swapon --show

持久化swap挂载

备份fstab:cp /etc/fstab /etc/fstab.bak

添加一条记录: echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

调整Swap设置

  • swappiness

调整Swappiness参数,该参数主要配置系统将数据从RAM交换到交换空间的频率。该参数的值是介于0和100之间的百分比。范围在0到100之间。 低参数值会让内核尽量少用交换,更高参数值会使内核更多的去使用交换空间。

查看当前swappiness值:cat /proc/sys/vm/swappiness

临时修改,重启失效:sysctl vm.swappiness=10

永久设置:vim /etc/sysctl.conf,最后一行加上vm.swappiness=10,保存并退出。sysctl -p 立即生效

  • vfs_cache_pressure 表示强制Linux VM最低保留多少空闲内存(Kbytes)。当可用内存低于这个参数时,系统开始回收cache内存,以释放内存,直到可用内存大于这个值。 查看 cat /proc/sys/vm/vfs_cache_pressure 缺省值100表示内核将根据pagecache和swapcache,把directory和inode cache保持在一个合理的百分比;降低该值低于100,将导致内核倾向于保留directory和inode cache;增加该值超过100,将导致内核倾向于回收directory和inode cache。

vim /etc/sysctl.conf,最后一行加上vm.vfs_cache_pressure=50

+ + + + \ No newline at end of file diff --git "a/linux/env/Linux\350\256\277\351\227\256\346\235\203\351\231\220\346\216\247\345\210\266\344\271\213ACL.html" "b/linux/env/Linux\350\256\277\351\227\256\346\235\203\351\231\220\346\216\247\345\210\266\344\271\213ACL.html" new file mode 100644 index 000000000..cb1692a7c --- /dev/null +++ "b/linux/env/Linux\350\256\277\351\227\256\346\235\203\351\231\220\346\216\247\345\210\266\344\271\213ACL.html" @@ -0,0 +1,25 @@ + + + + + + Linux访问权限控制之ACL | 故事 + + + + + + + + + + + + + + +
Skip to content

Linux访问权限控制之ACL

什么是ACL

ACL即Access Control List,访问控制列表,POSIX 1003.1e/1003.2c标准中定义的权限管理方式。详细内容可以参考POSIX Access Control Lists on Linux这篇文章。

POSIX:POSIX是可移植操作系统接口(Portable Operating System Interface of UNIX)的缩写,POSIX标准定义了操作系统应该为应用程序提供的接口标准,是在各种UNIX操作系统上运行的软件的一系列API标准的总称。POSIX标准意在期望获得源代码级别的软件可移植性。换句话说,为一个POSIX兼容的操作系统编写的程序,应该可以在任何其它的POSIX操作系统(即使是来自另一个厂商)上编译执行。

为什么使用ACL

经典的Unix权限模型中每个文件系统对象都定义了三组权限,owner、group和other。也就是常用的chmod命令中修改的权限,包括读(r)、写入(w)和执行(x)。但是如果我们想更细颗粒度的控制某些用户的权限时,使用chmodchown就很难办了。这就需要ACL来针对单一用户、单一文件或目录来进行权限控制。

ACL的工作原理

ACL由一系列Access Entry组成,Access Entry又包括三个部分:Entry type,qualifier(非必须)、权限

Entry type有以下类型:

  • Owner/ACL_USER_OBJ : 相当于Linux里file_owner的权限

  • Named user/ACL_USER: 定义了额外的用户可以对此文件拥有的权限

  • Owning group/ACL_GROUP_OBJ: 相当于Linux里group的权限

  • Named group/ACL_GROUP: 定义了额外的组可以对此文件拥有的权限

  • Mask/ACL_MASK: 定义了ACL_USER, ACL_GROUP_OBJ和ACL_GROUP的最大权限

  • Others/ACL_OTHER: 相当于Linux里other的权限

访问检查算法

进程请求访问文件系统对象。执行两个步骤。第一步选择与请求进程最匹配的 ACL 条目。ACL 条目按以下顺序查看:所有者、命名用户、(拥有或命名)组、其他。只有一个条目决定访问。第二步检查匹配条目是否包含足够的权限。

一个进程可以是多个组中的成员,因此可以匹配多个组条目。如果这些匹配的组条目中的任何一个包含请求的权限,则选择包含请求的权限的条目(无论选择哪个条目,结果都是相同的)。如果没有匹配的组条目包含请求的权限,则无论选择哪个条目都将拒绝访问。

访问检查算法可以用伪代码描述如下。

  • if

    进程的用户ID是所有者,所有者条目决定访问权限

  • else if

    进程的用户 ID 与指定用户条目之一中的限定符匹配,此条目确定访问权限

  • else if

    进程的组 ID 之一与所属组匹配,并且所属组条目包含请求的权限,此条目确定访问权限

  • else if

    进程的组 ID 之一与命名组条目之一的限定符匹配,并且此条目包含请求的权限,此条目确定访问权限

  • else if

    进程的组 ID 之一与所属组或任何命名组条目匹配,但所属组条目或任何匹配命名组条目均不包含请求的权限,这确定访问被拒绝

  • else

    另一个条目决定访问。

  • if

    此选择产生的匹配条目是所有者或其他条目,并且它包含请求的权限,授予访问权限

  • else if

    匹配条目是命名用户、拥有组或命名组条目,并且此条目包含请求的权限,并且掩码条目也包含请求的权限(或没有掩码条目),授予访问权限

  • else

    访问被拒绝。

The access check algorithm can be described in pseudo-code as follows.

  • If

    the user ID of the process is the owner, the owner entry determines access

  • else if

    the user ID of the process matches the qualifier in one of the named user entries, this entry determines access

  • else if

    one of the group IDs of the process matches the owning group and the owning group entry contains the requested permissions, this entry determines access

  • else if

    one of the group IDs of the process matches the qualifier of one of the named group entries and this entry contains the requested permissions, this entry determines access

  • else if

    one of the group IDs of the process matches the owning group or any of the named group entries, but neither the owning group entry nor any of the matching named group entries contains the requested permissions, this determines that access is denied

  • else

    the other entry determines access.

  • If

    the matching entry resulting from this selection is the owner or other entry and it contains the requested permissions, access is granted

  • else if

    the matching entry is a named user, owning group, or named group entry and this entry contains the requested permissions and the mask entry also contains the requested permissions (or there is no mask entry), access is granted

  • else

    access is denied.

ACL命令

  • getfacl:查看某个文件/目录的ACL权限

  • setfacl:设置某个文件/目录的ACL权限

    • -m:设置后续acl参数
    • -x:删除后续acl参数
    • -b:删除全部acl参数
    • -k:删除默认acl参数
    • -R:递归设置acl参数
    • -d:设置默认acl参数

例子

设置指定用户/组权限

  • setfacl -m u:user1:rx /test:给user1用户设置/test目录的rx(r-x)权限
  • setfacl -m g:group1:rx /test:给group1组设置/test目录的rx(r-x)权限
  • setfacl -m o:--x /test:给other设置x(--x)权限(chmod o+x /test)

默认ACL权限

默认权限下新建的子目录会继承父目录的权限,只有目录才能给默认权限,不是目录的对象仅继承父目录的默认 ACL 作为其访问 ACL。

  • setfacl -m d:u:user1:rx /test:给user1用户设置/test目录的默认rx权限
  • setfacl -m d:g:group1:rx /test:给group1组设置/test目录的默认rx权限
  • setfacl -m d:o:--x /test:给other设置/test目录的默认x权限

最大有效权限mask

如上文所描述的,mask定义了ACL_USER, ACL_GROUP_OBJ和ACL_GROUP的最大权限。例如,mask权限为r--,此时不管ACL_USER的权限为多大,就算是``rwx,最终生效的都只会是r--`。概括来说:用户和用户组的权限必须在mask权限的范围之内才能生效,mask权限就是最大有效权限。

修改mask权限

setfacl -m m:rx /test:设置mask权限为r-x,使用setfacl -m m:权限 路径

删除所有acl权限

setfacl -b /test

删除指定acl权限

setfacl -x u:user1 /test

+ + + + \ No newline at end of file diff --git a/linux/env/Ubuntu-server.html b/linux/env/Ubuntu-server.html new file mode 100644 index 000000000..e78f28e6e --- /dev/null +++ b/linux/env/Ubuntu-server.html @@ -0,0 +1,267 @@ + + + + + + Ubuntu-server | 故事 + + + + + + + + + + + + + + +
Skip to content

Ubuntu-server

关闭欢迎提示

chmod -x /etc/update-motd.d/*

关闭ssh登录motd广告

vim /etc/default/motd-news 将enabled改为0

关闭ssh登录系统信息

apt remove landscape-common landscape-client

系统盘迁移

shell
# 1.备份数据
+# 2.制作一个linux启动盘 例如live server的
+# 3.连接原启动盘和需要迁移到的目标盘
+# 4.U盘启动直接进入shell
+# 5.查看磁盘信息
+lsblk
+# 6.dd命令直接全盘迁移
+dd if=/dev/sda of=/dev/sdb bs=4096 conv=sync,noerror
+# 7.拷贝完成后使用新磁盘启动
+# 8.删除旧分区&resize
+fdisk /dev/sdX
+d #删除磁盘最后一个分区
+n #新建一个分区,扇区开始结束都用默认即可
+w #写盘保存
+
+partprobe #重新读取分区表并更新分区信息
+
+resize2fs /dev/sdXX #调整文件系统的大小
# 1.备份数据
+# 2.制作一个linux启动盘 例如live server的
+# 3.连接原启动盘和需要迁移到的目标盘
+# 4.U盘启动直接进入shell
+# 5.查看磁盘信息
+lsblk
+# 6.dd命令直接全盘迁移
+dd if=/dev/sda of=/dev/sdb bs=4096 conv=sync,noerror
+# 7.拷贝完成后使用新磁盘启动
+# 8.删除旧分区&resize
+fdisk /dev/sdX
+d #删除磁盘最后一个分区
+n #新建一个分区,扇区开始结束都用默认即可
+w #写盘保存
+
+partprobe #重新读取分区表并更新分区信息
+
+resize2fs /dev/sdXX #调整文件系统的大小

内核模块加载

最近打算给home server换一块瑞昱的2.5G网卡(8125b芯片),所以想提前安装下网卡驱动,执行官网那个autorun.sh脚本前也没仔细看, 结果脚本直接把原来的8169网卡驱动卸载了,导致机器直接失联。还好脚本里有备份,所以只需要恢复回去就行了。

shell
#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-only
+
+# invoke insmod with all arguments we got
+# and use a pathname, as insmod doesn't look in . by default
+
+TARGET_PATH=$(find /lib/modules/$(uname -r)/kernel/drivers/net/ethernet -name realtek -type d)
+if [ "$TARGET_PATH" = "" ]; then
+	TARGET_PATH=$(find /lib/modules/$(uname -r)/kernel/drivers/net -name realtek -type d)
+fi
+if [ "$TARGET_PATH" = "" ]; then
+	TARGET_PATH=/lib/modules/$(uname -r)/kernel/drivers/net
+fi
+echo
+echo "Check old driver and unload it."
+check=`lsmod | grep r8169`
+if [ "$check" != "" ]; then
+        echo "rmmod r8169"
+        /sbin/rmmod r8169
+fi
+
+check=`lsmod | grep r8125`
+if [ "$check" != "" ]; then
+        echo "rmmod r8125"
+        /sbin/rmmod r8125
+fi
+
+echo "Build the module and install"
+echo "-------------------------------" >> log.txt
+date 1>>log.txt
+make $@ all 1>>log.txt || exit 1
+module=`ls src/*.ko`
+module=${module#src/}
+module=${module%.ko}
+
+if [ "$module" = "" ]; then
+	echo "No driver exists!!!"
+	exit 1
+elif [ "$module" != "r8169" ]; then
+	if test -e $TARGET_PATH/r8169.ko ; then
+		echo "Backup r8169.ko"
+		if test -e $TARGET_PATH/r8169.bak ; then
+			i=0
+			while test -e $TARGET_PATH/r8169.bak$i
+			do
+				i=$(($i+1))
+			done
+			echo "rename r8169.ko to r8169.bak$i"
+			mv $TARGET_PATH/r8169.ko $TARGET_PATH/r8169.bak$i
+		else
+			echo "rename r8169.ko to r8169.bak"
+			mv $TARGET_PATH/r8169.ko $TARGET_PATH/r8169.bak
+		fi
+	fi
+fi
+
+echo "DEPMOD $(uname -r)"
+depmod `uname -r`
+echo "load module $module"
+modprobe $module
+
+is_update_initramfs=n
+distrib_list="ubuntu debian"
+
+if [ -r /etc/debian_version ]; then
+	is_update_initramfs=y
+elif [ -r /etc/lsb-release ]; then
+	for distrib in $distrib_list
+	do
+		/bin/grep -i "$distrib" /etc/lsb-release 2>&1 /dev/null && \
+			is_update_initramfs=y && break
+	done
+fi
+
+if [ "$is_update_initramfs" = "y" ]; then
+	if which update-initramfs >/dev/null ; then
+		echo "Updating initramfs. Please wait."
+		update-initramfs -u -k $(uname -r)
+	else
+		echo "update-initramfs: command not found"
+		exit 1
+	fi
+fi
+
+echo "Completed."
+exit 0
#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-only
+
+# invoke insmod with all arguments we got
+# and use a pathname, as insmod doesn't look in . by default
+
+TARGET_PATH=$(find /lib/modules/$(uname -r)/kernel/drivers/net/ethernet -name realtek -type d)
+if [ "$TARGET_PATH" = "" ]; then
+	TARGET_PATH=$(find /lib/modules/$(uname -r)/kernel/drivers/net -name realtek -type d)
+fi
+if [ "$TARGET_PATH" = "" ]; then
+	TARGET_PATH=/lib/modules/$(uname -r)/kernel/drivers/net
+fi
+echo
+echo "Check old driver and unload it."
+check=`lsmod | grep r8169`
+if [ "$check" != "" ]; then
+        echo "rmmod r8169"
+        /sbin/rmmod r8169
+fi
+
+check=`lsmod | grep r8125`
+if [ "$check" != "" ]; then
+        echo "rmmod r8125"
+        /sbin/rmmod r8125
+fi
+
+echo "Build the module and install"
+echo "-------------------------------" >> log.txt
+date 1>>log.txt
+make $@ all 1>>log.txt || exit 1
+module=`ls src/*.ko`
+module=${module#src/}
+module=${module%.ko}
+
+if [ "$module" = "" ]; then
+	echo "No driver exists!!!"
+	exit 1
+elif [ "$module" != "r8169" ]; then
+	if test -e $TARGET_PATH/r8169.ko ; then
+		echo "Backup r8169.ko"
+		if test -e $TARGET_PATH/r8169.bak ; then
+			i=0
+			while test -e $TARGET_PATH/r8169.bak$i
+			do
+				i=$(($i+1))
+			done
+			echo "rename r8169.ko to r8169.bak$i"
+			mv $TARGET_PATH/r8169.ko $TARGET_PATH/r8169.bak$i
+		else
+			echo "rename r8169.ko to r8169.bak"
+			mv $TARGET_PATH/r8169.ko $TARGET_PATH/r8169.bak
+		fi
+	fi
+fi
+
+echo "DEPMOD $(uname -r)"
+depmod `uname -r`
+echo "load module $module"
+modprobe $module
+
+is_update_initramfs=n
+distrib_list="ubuntu debian"
+
+if [ -r /etc/debian_version ]; then
+	is_update_initramfs=y
+elif [ -r /etc/lsb-release ]; then
+	for distrib in $distrib_list
+	do
+		/bin/grep -i "$distrib" /etc/lsb-release 2>&1 /dev/null && \
+			is_update_initramfs=y && break
+	done
+fi
+
+if [ "$is_update_initramfs" = "y" ]; then
+	if which update-initramfs >/dev/null ; then
+		echo "Updating initramfs. Please wait."
+		update-initramfs -u -k $(uname -r)
+	else
+		echo "update-initramfs: command not found"
+		exit 1
+	fi
+fi
+
+echo "Completed."
+exit 0
shell
# 进入网卡驱动目录
+cd /lib/modules/$(uname -r)//kernel/drivers/net/ethernet/realtek
+# 恢复备份
+mv r8169.bak r8169.ko
+# 加载模块
+insmod r8169.ko
+# 验证是否加载
+lsmod | grep r8169
+# 更新模块依赖关系
+depmod `uname -r`
+# 更新initramfs
+update-initramfs -u -k $(uname -r)
# 进入网卡驱动目录
+cd /lib/modules/$(uname -r)//kernel/drivers/net/ethernet/realtek
+# 恢复备份
+mv r8169.bak r8169.ko
+# 加载模块
+insmod r8169.ko
+# 验证是否加载
+lsmod | grep r8169
+# 更新模块依赖关系
+depmod `uname -r`
+# 更新initramfs
+update-initramfs -u -k $(uname -r)

瑞昱2.5G网卡r8125b驱动安装

A DKMS package for easy use of Realtek r8125 driver, which supports 2.5 GbE. https://github.com/awesometic/realtek-r8125-dkms

  • sudo add-apt-repository ppa:awesometic/ppa

  • sudo apt install realtek-r8125-dkms

  • shell
    sudo tee -a /etc/modprobe.d/blacklist-r8169.conf > /dev/null <<EOT
    +# To use r8125 driver explicitly
    +blacklist r8169
    +EOT
    sudo tee -a /etc/modprobe.d/blacklist-r8169.conf > /dev/null <<EOT
    +# To use r8125 driver explicitly
    +blacklist r8169
    +EOT
  • sudo update-initramfs -u

  • vim /etc/netplan/00-installer-config.yaml

  • shell
    # This is the network config written by 'subiquity'
    +network:
    +  ethernets:
    +    enp1s0: # 根据ifconfig -a找到2.5G网卡设备的名称
    +      dhcp4: true
    +  version: 2
    # This is the network config written by 'subiquity'
    +network:
    +  ethernets:
    +    enp1s0: # 根据ifconfig -a找到2.5G网卡设备的名称
    +      dhcp4: true
    +  version: 2
  • netplan apply

  • reboot

+ + + + \ No newline at end of file diff --git "a/linux/env/bash\345\270\270\347\224\250\347\232\204\345\277\253\346\215\267\351\224\256.html" "b/linux/env/bash\345\270\270\347\224\250\347\232\204\345\277\253\346\215\267\351\224\256.html" new file mode 100644 index 000000000..2c2271c5d --- /dev/null +++ "b/linux/env/bash\345\270\270\347\224\250\347\232\204\345\277\253\346\215\267\351\224\256.html" @@ -0,0 +1,25 @@ + + + + + + bash常用的快捷键 | 故事 + + + + + + + + + + + + + + +
Skip to content

bash常用的快捷键

  • ctrl+a:光标移到行首
  • ctrl+b:光标左移一个字母
  • ctrl+c:杀死当前进程
  • ctrl+d:退出当前Shell
  • ctrl+e:光标移到行尾
  • ctrl+h:删除光标前一个字符,同backspace键相同
  • ctrl+k:清除光标后至行尾的内容
  • ctrl+l:清屏,相当于clear
  • ctrl+r:搜索之前打过的命令会有一个提示,根据你输入的关键字进行搜索bash的history
  • ctrl+u:清除光标前至行首间的所有内容。
  • ctrl+w:移除光标前的一个单词
  • ctrl+t:交换光标位置前的两个字符
  • ctrl+y:粘贴或者恢复上次的删除
  • ctrl+d:删除光标所在字母;注意和backspace以及ctrl+h的区别,这2个是删除光标前的字符
  • ctrl+f:光标右移
  • ctrl+z:把当前进程转到后台运行,使用’fg‘命令恢复。比如top-d1然后ctrl+z,到后台,然后fg,重新恢复
+ + + + \ No newline at end of file diff --git "a/linux/env/centos7\351\230\262\347\201\253\345\242\231\345\221\275\344\273\244.html" "b/linux/env/centos7\351\230\262\347\201\253\345\242\231\345\221\275\344\273\244.html" new file mode 100644 index 000000000..086eea5c2 --- /dev/null +++ "b/linux/env/centos7\351\230\262\347\201\253\345\242\231\345\221\275\344\273\244.html" @@ -0,0 +1,29 @@ + + + + + + centos7防火墙命令 | 故事 + + + + + + + + + + + + + + +
Skip to content

centos7防火墙命令

centos7.0以上的版本默认为firewalld,以下是iptables,整理一下命令备用。

firewall-cmd

查看防火墙状态

bash
firewall-cmd --state
firewall-cmd --state

查看防火墙规则

bash
firewall-cmd --list-all
firewall-cmd --list-all

更新防火墙规则

bash
firewall-cmd --reload
firewall-cmd --reload

关闭/开启防火墙

bash
systemctl stop firewalld.service
+systemctl start firewalld.service
systemctl stop firewalld.service
+systemctl start firewalld.service

端口操作

永久开放端口

bash
firewall-cmd --zone=public --add-port=5672/tcp --permanent
firewall-cmd --zone=public --add-port=5672/tcp --permanent

关闭端口

bash
firewall-cmd --zone=public --remove-port=5672/tcp --permanent
firewall-cmd --zone=public --remove-port=5672/tcp --permanent

iptables

开启/关闭防火墙

bash
systemctl start iptables.service
+systemctl stop iptables.service
systemctl start iptables.service
+systemctl stop iptables.service

查看防火墙状态

bash
systemctl status iptables.service
systemctl status iptables.service

开放端口

1.命令

bash
iptables -I INPUT -p tcp --dport 8000 -j ACCEPT
iptables -I INPUT -p tcp --dport 8000 -j ACCEPT

2.直接修改/etc/sysconfig/iptables

-A INPUT -m state --state NEW -m tcp -p tcp --dport 8000 -j ACCEPT 

重启iptables

bash
service iptables restart
service iptables restart
+ + + + \ No newline at end of file diff --git "a/linux/env/\344\273\216\351\233\266\346\220\255\345\273\272Linux\350\231\232\346\213\237\346\234\272\347\216\257\345\242\203.html" "b/linux/env/\344\273\216\351\233\266\346\220\255\345\273\272Linux\350\231\232\346\213\237\346\234\272\347\216\257\345\242\203.html" new file mode 100644 index 000000000..d64f1da5f --- /dev/null +++ "b/linux/env/\344\273\216\351\233\266\346\220\255\345\273\272Linux\350\231\232\346\213\237\346\234\272\347\216\257\345\242\203.html" @@ -0,0 +1,513 @@ + + + + + + 从零搭建Linux虚拟机环境 | 故事 + + + + + + + + + + + + + + +
Skip to content

从零搭建Linux虚拟机环境

基础

镜像安装

CentOS7-1908版本

http://vault.centos.org/7.7.1908/isos/x86_64/CentOS-7-x86_64-DVD-1908.torrent

基础命令包安装

  • ifconfig

    • yum search ifconfig
    • yum install net-tools.x86_64
  • lsb_release

    • yum install -y redhat-lsb
  • yum提示没有可用镜像

    • curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
  • wget

    • yum install wget
  • dns

    • 虚拟机ping不通域名的解决办法 vi /etc/sysconfig/network-scripts/ifcfg-ens33
    • 虚拟机的静态ip和真机的必须在同一网段,添加谷歌的dns解析
    • 虚拟机内的配置

    image-20210505171618465

    • 真机的ip信息

      image-20210505171651704

环境搭建

  • JDK1.8

    • oracle官网下载jdk后上传虚拟机

    • 解压并配置环境变量,比如我下载的是jre1.8.0_202

      vi /etc/profile/

      bash
      JAVA_HOME=/usr/local/java/jre1.8.0_202
      +PATH=$JAVA_HOME/bin:$PATH
      +CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JAVA_HOME/jre/lib/rt.jar
      +export JAVA_HOME
      +export CLASSPATH
      +export PATH
      JAVA_HOME=/usr/local/java/jre1.8.0_202
      +PATH=$JAVA_HOME/bin:$PATH
      +CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JAVA_HOME/jre/lib/rt.jar
      +export JAVA_HOME
      +export CLASSPATH
      +export PATH
    • source /etc/profile或重启虚拟机使环境变量生效

  • python3

  • mysql 5.7

    • 下载并安装mysql官方的yum repository: wget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.41-1.el7.x86_64.rpm-bundle.tar

    • 解压包:tar -xvf mysql-5.7.41-1.el7.x86_64.rpm-bundle.tar

    • yum -y install mysql-community-common-5.7.41-1.el7.x86_64.rpm mysql-community-libs-5.7.41-1.el7.x86_64.rpm mysql-community-client-5.7.41-1.el7.x86_64.rpm mysql-community-server-5.7.41-1.el7.x86_64.rpm

    • 启动systemctl enable mysqld && systemctl start mysqld

    • 临时密码 grep 'temporary password' /var/log/mysqld.log

    • 根据临时密码登入mysql

    • 改密码 ALTER USER 'root'@'localhost' IDENTIFIED BY 'new pwd';

    • 更改密码弱口令设置,设置简单密码:

      • set global validate_password_policy=0;

      • set global validate_password_length=1;

    • 配置远程登陆

      • grant all on *.* to 'root'@'%' identified by 'pwd';

      • 立即生效: flush privileges;

    • 创建用户&授权

      sql
      -- 创建用户
      +CREATE USER '用户名'@'localhost' IDENTIFIED BY '密码'; 
      +CREATE USER '用户名'@'%' IDENTIFIED BY '密码';  
      +-- 授权全部
      +grant all privileges on 数据库名称.* to '用户名'@'localhost' identified by '密码';     #本地授权
      +grant all privileges on 数据库名称.* to '用户名'@'%' identified by '密码';             #远程授权
      +flush privileges;                                                                 #刷新系统权限表
      +-- 授权指定
      +grant select,update,delete,insert on 数据库名称.* to '用户'@'localhost' identified by '密码'; 
      +flush privileges; #刷新系统权限表
      +-- 删除用户
      +Delete FROM mysql.user Where User='用户名' and Host='localhost';          #删除本地用户
      +Delete FROM mysql.user Where User='用户名' and Host='%';                  #删除远程用户
      +flush privileges;                                                         #刷新系统权限表
      +-- 删除用户及权限
      +DROP USER 'username'@'localhost';
      +DROP USER 'username'@'%';
      +-- 查看当前用户权限
      +show grants;
      +-- 查看指定用户权限
      +show grants for username@localhost;
      -- 创建用户
      +CREATE USER '用户名'@'localhost' IDENTIFIED BY '密码'; 
      +CREATE USER '用户名'@'%' IDENTIFIED BY '密码';  
      +-- 授权全部
      +grant all privileges on 数据库名称.* to '用户名'@'localhost' identified by '密码';     #本地授权
      +grant all privileges on 数据库名称.* to '用户名'@'%' identified by '密码';             #远程授权
      +flush privileges;                                                                 #刷新系统权限表
      +-- 授权指定
      +grant select,update,delete,insert on 数据库名称.* to '用户'@'localhost' identified by '密码'; 
      +flush privileges; #刷新系统权限表
      +-- 删除用户
      +Delete FROM mysql.user Where User='用户名' and Host='localhost';          #删除本地用户
      +Delete FROM mysql.user Where User='用户名' and Host='%';                  #删除远程用户
      +flush privileges;                                                         #刷新系统权限表
      +-- 删除用户及权限
      +DROP USER 'username'@'localhost';
      +DROP USER 'username'@'%';
      +-- 查看当前用户权限
      +show grants;
      +-- 查看指定用户权限
      +show grants for username@localhost;
  • mysql 8.0

    sql
    -- 修改root密码
    +UPDATE mysql.user SET authentication_string=null WHERE User='root';
    +FLUSH PRIVILEGES;
    +
    +ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'new_password';
    +FLUSH PRIVILEGES;
    +
    +
    +CREATE USER ''@'' IDENTIFIED BY '';
    +
    +GRANT ALL PRIVILEGES ON *.* TO ''@'';
    -- 修改root密码
    +UPDATE mysql.user SET authentication_string=null WHERE User='root';
    +FLUSH PRIVILEGES;
    +
    +ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'new_password';
    +FLUSH PRIVILEGES;
    +
    +
    +CREATE USER ''@'' IDENTIFIED BY '';
    +
    +GRANT ALL PRIVILEGES ON *.* TO ''@'';
  • nginx

    • 依赖

      • centos: yum -y install gcc pcre pcre-devel zlib zlib-devel openssl openssl-devel
      • ubuntu: apt install libpcre3 libpcre3-dev openssl libssl-dev
    • 下载安装包 wget http://nginx.org/download/nginx-1.9.9.tar.gz

    • 解压到指定目录 tar -xzvf nginx-1.9.9.tar.gz -C /usr/local/nginx/

    • 切换到nginx的目录执行 cd /usr/local/nginx/nginx-1.9.9

      bash
      ./configure --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module  #配置ssl模块
      + 
      +make
      + 
      +make install
      ./configure --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module  #配置ssl模块
      + 
      +make
      + 
      +make install
    • 安装后切换到/usr/local/nginx/sbin启动nginx并访问

      image-20210505174840552

    • 开机自启nginx

      • vi /etc/init.d/nginx

        bash
        #!/bin/bash
        +#
        +# nginx - this script starts and stops the nginx daemon
        +#
        +# chkconfig:   - 85 15
        +# description:  NGINX is an HTTP(S) server, HTTP(S) reverse \
        +#               proxy and IMAP/POP3 proxy server
        +# processname: nginx
        +# config:      /etc/nginx/nginx.conf
        +# config:      /etc/sysconfig/nginx
        +# pidfile:     /var/run/nginx.pid
        +
        +# Source function library.
        +. /etc/rc.d/init.d/functions
        +
        +# Source networking configuration.
        +. /etc/sysconfig/network
        +
        +# Check that networking is up.
        +[ "$NETWORKING" = "no" ] && exit 0
        +
        +nginx="/usr/local/nginx/sbin/nginx"
        +prog=$(basename $nginx)
        +
        +NGINX_CONF_FILE="/usr/local/nginx/conf/nginx.conf"
        +
        +[ -f /etc/sysconfig/nginx ] && . /etc/sysconfig/nginx
        +
        +lockfile=/var/lock/subsys/nginx
        +
        +make_dirs() {
        +  # make required directories
        +  user=`$nginx -V 2>&1 | grep "configure arguments:.*--user=" | sed 's/[^*]*--user=\([^ ]*\).*/\1/g' -`
        +  if [ -n "$user" ]; then
        +    if [ -z "`grep $user /etc/passwd`" ]; then
        +      useradd -M -s /bin/nologin $user
        +    fi
        +    options=`$nginx -V 2>&1 | grep 'configure arguments:'`
        +    for opt in $options; do
        +        if [ `echo $opt | grep '.*-temp-path'` ]; then
        +          value=`echo $opt | cut -d "=" -f 2`
        +          if [ ! -d "$value" ]; then
        +            # echo "creating" $value
        +            mkdir -p $value && chown -R $user $value
        +          fi
        +        fi
        +    done
        +  fi
        +}
        +
        +start() {
        +  [ -x $nginx ] || exit 5
        +  [ -f $NGINX_CONF_FILE ] || exit 6
        +  make_dirs
        +  echo -n $"Starting $prog: "
        +  daemon $nginx -c $NGINX_CONF_FILE
        +  retval=$?
        +  echo
        +  [ $retval -eq 0 ] && touch $lockfile
        +  return $retval
        +}
        +
        +stop() {
        +  echo -n $"Stopping $prog: "
        +  killproc $prog -QUIT
        +  retval=$?
        +  echo
        +  [ $retval -eq 0 ] && rm -f $lockfile
        +  return $retval
        +}
        +
        +restart() {
        +  configtest || return $?
        +  stop
        +  sleep 1
        +  start
        +}
        +
        +reload() {
        +  configtest || return $?
        +  echo -n $"Reloading $prog: "
        +  killproc $nginx -HUP
        +  RETVAL=$?
        +  echo
        +}
        +
        +force_reload() {
        +  restart
        +}
        +
        +configtest() {
        +  $nginx -t -c $NGINX_CONF_FILE
        +}
        +
        +rh_status() {
        +  status $prog
        +}
        +
        +rh_status_q() {
        +  rh_status >/dev/null 2>&1
        +}
        +
        +case "$1" in
        +  start)
        +    rh_status_q && exit 0
        +    $1
        +    ;;
        +  stop)
        +    rh_status_q || exit 0
        +    $1
        +    ;;
        +  restart|configtest)
        +    $1
        +    ;;
        +  reload)
        +    rh_status_q || exit 7
        +    $1
        +    ;;
        +  force-reload)
        +    force_reload
        +    ;;
        +  status)
        +    rh_status
        +    ;;
        +  condrestart|try-restart)
        +    rh_status_q || exit 0
        +    ;;
        +  *)
        +    echo $"Usage: $0 {start|stop|status|restart|reload|configtest}"
        +    exit 2
        +esac
        #!/bin/bash
        +#
        +# nginx - this script starts and stops the nginx daemon
        +#
        +# chkconfig:   - 85 15
        +# description:  NGINX is an HTTP(S) server, HTTP(S) reverse \
        +#               proxy and IMAP/POP3 proxy server
        +# processname: nginx
        +# config:      /etc/nginx/nginx.conf
        +# config:      /etc/sysconfig/nginx
        +# pidfile:     /var/run/nginx.pid
        +
        +# Source function library.
        +. /etc/rc.d/init.d/functions
        +
        +# Source networking configuration.
        +. /etc/sysconfig/network
        +
        +# Check that networking is up.
        +[ "$NETWORKING" = "no" ] && exit 0
        +
        +nginx="/usr/local/nginx/sbin/nginx"
        +prog=$(basename $nginx)
        +
        +NGINX_CONF_FILE="/usr/local/nginx/conf/nginx.conf"
        +
        +[ -f /etc/sysconfig/nginx ] && . /etc/sysconfig/nginx
        +
        +lockfile=/var/lock/subsys/nginx
        +
        +make_dirs() {
        +  # make required directories
        +  user=`$nginx -V 2>&1 | grep "configure arguments:.*--user=" | sed 's/[^*]*--user=\([^ ]*\).*/\1/g' -`
        +  if [ -n "$user" ]; then
        +    if [ -z "`grep $user /etc/passwd`" ]; then
        +      useradd -M -s /bin/nologin $user
        +    fi
        +    options=`$nginx -V 2>&1 | grep 'configure arguments:'`
        +    for opt in $options; do
        +        if [ `echo $opt | grep '.*-temp-path'` ]; then
        +          value=`echo $opt | cut -d "=" -f 2`
        +          if [ ! -d "$value" ]; then
        +            # echo "creating" $value
        +            mkdir -p $value && chown -R $user $value
        +          fi
        +        fi
        +    done
        +  fi
        +}
        +
        +start() {
        +  [ -x $nginx ] || exit 5
        +  [ -f $NGINX_CONF_FILE ] || exit 6
        +  make_dirs
        +  echo -n $"Starting $prog: "
        +  daemon $nginx -c $NGINX_CONF_FILE
        +  retval=$?
        +  echo
        +  [ $retval -eq 0 ] && touch $lockfile
        +  return $retval
        +}
        +
        +stop() {
        +  echo -n $"Stopping $prog: "
        +  killproc $prog -QUIT
        +  retval=$?
        +  echo
        +  [ $retval -eq 0 ] && rm -f $lockfile
        +  return $retval
        +}
        +
        +restart() {
        +  configtest || return $?
        +  stop
        +  sleep 1
        +  start
        +}
        +
        +reload() {
        +  configtest || return $?
        +  echo -n $"Reloading $prog: "
        +  killproc $nginx -HUP
        +  RETVAL=$?
        +  echo
        +}
        +
        +force_reload() {
        +  restart
        +}
        +
        +configtest() {
        +  $nginx -t -c $NGINX_CONF_FILE
        +}
        +
        +rh_status() {
        +  status $prog
        +}
        +
        +rh_status_q() {
        +  rh_status >/dev/null 2>&1
        +}
        +
        +case "$1" in
        +  start)
        +    rh_status_q && exit 0
        +    $1
        +    ;;
        +  stop)
        +    rh_status_q || exit 0
        +    $1
        +    ;;
        +  restart|configtest)
        +    $1
        +    ;;
        +  reload)
        +    rh_status_q || exit 7
        +    $1
        +    ;;
        +  force-reload)
        +    force_reload
        +    ;;
        +  status)
        +    rh_status
        +    ;;
        +  condrestart|try-restart)
        +    rh_status_q || exit 0
        +    ;;
        +  *)
        +    echo $"Usage: $0 {start|stop|status|restart|reload|configtest}"
        +    exit 2
        +esac
      • bash
        chmod 777 /etc/init.d/nginx
        chmod 777 /etc/init.d/nginx
      • bash
        chkconfig nginx on
        chkconfig nginx on
  • 开放端口

    • 查看已经开放的端口firewall-cmd --list-ports
    • 开启端口 firewall-cmd --zone=public --add-port=80/tcp --permanent
      • zone:作用域
      • -add-port=80/tcp 端口/协议
      • --permanent 永久生效,没有此参数后重启失效
    • firewall-cmd --reload #重启firewall
    • systemctl stop firewalld.service #停止firewall
    • systemctl disable firewalld.service #禁止firewall开机启动
    • firewall-cmd --state #查看默认防火墙状态
  • redis

    • 下载安装包wget http://download.redis.io/releases/redis-5.0.7.tar.gz

    • tar -zxvf redis-5.0.7.tar.gz

    • mv /root/redis-5.0.7 /usr/local/redis

    • cd /usr/local/redis/redis-5.0.7

    • make && make PREFIX=/usr/local/redis install

      • 可能会报错,因为centos自带的gcc版本太低

      • 执行命令

        bash
        yum -y install centos-release-scl devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils 
        +# scl enable devtoolset-9 bash #临时启用新版本的gcc
        +echo "source /opt/rh/devtoolset-9/enable" >>/etc/profile # 永久启用新版gcc
        yum -y install centos-release-scl devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils 
        +# scl enable devtoolset-9 bash #临时启用新版本的gcc
        +echo "source /opt/rh/devtoolset-9/enable" >>/etc/profile # 永久启用新版gcc
    • 开机自启redis

      • bash
        vi /etc/init.d/redis
        vi /etc/init.d/redis
      • bash
        #!/bin/sh
        +# chkconfig: 2345 10 90
        +# description: Start and Stop redis
        +
        +REDISPORT=6379
        +EXEC=/usr/local/redis/bin/redis-server
        +CLIEXEC=/usr/local/redis/bin/redis-cli
        +
        +PIDFILE=/var/run/redis_${REDISPORT}.pid
        +CONF="/usr/local/redis/redis.conf"
        +
        +case "$1" in
        +    start)
        +        if [ -f $PIDFILE ]
        +        then
        +                echo "$PIDFILE exists, process is already running or crashed"
        +        else
        +                echo "Starting Redis server..."
        +                $EXEC $CONF &
        +        fi
        +        ;;
        +    stop)
        +        if [ ! -f $PIDFILE ]
        +        then
        +                echo "$PIDFILE does not exist, process is not running"
        +        else
        +                PID=$(cat $PIDFILE)
        +                echo "Stopping ..."
        +                $CLIEXEC -p $REDISPORT shutdown
        +                while [ -x /proc/${PID} ]
        +                do
        +                    echo "Waiting for Redis to shutdown ..."
        +                    sleep 1
        +                done
        +                echo "Redis stopped"
        +        fi
        +        ;;
        +    restart)
        +        "$0" stop
        +        sleep 3
        +        "$0" start
        +        ;;
        +    *)
        +        echo "Please use start or stop or restart as first argument"
        +        ;;
        +esac
        #!/bin/sh
        +# chkconfig: 2345 10 90
        +# description: Start and Stop redis
        +
        +REDISPORT=6379
        +EXEC=/usr/local/redis/bin/redis-server
        +CLIEXEC=/usr/local/redis/bin/redis-cli
        +
        +PIDFILE=/var/run/redis_${REDISPORT}.pid
        +CONF="/usr/local/redis/redis.conf"
        +
        +case "$1" in
        +    start)
        +        if [ -f $PIDFILE ]
        +        then
        +                echo "$PIDFILE exists, process is already running or crashed"
        +        else
        +                echo "Starting Redis server..."
        +                $EXEC $CONF &
        +        fi
        +        ;;
        +    stop)
        +        if [ ! -f $PIDFILE ]
        +        then
        +                echo "$PIDFILE does not exist, process is not running"
        +        else
        +                PID=$(cat $PIDFILE)
        +                echo "Stopping ..."
        +                $CLIEXEC -p $REDISPORT shutdown
        +                while [ -x /proc/${PID} ]
        +                do
        +                    echo "Waiting for Redis to shutdown ..."
        +                    sleep 1
        +                done
        +                echo "Redis stopped"
        +        fi
        +        ;;
        +    restart)
        +        "$0" stop
        +        sleep 3
        +        "$0" start
        +        ;;
        +    *)
        +        echo "Please use start or stop or restart as first argument"
        +        ;;
        +esac
      • bash
        vim /usr/local/redis/redis.conf
        +修改
        +bind 0.0.0.0 #所有ipv4端口
        +protected-mode no # 关闭保护模式
        +daemonize yes # 守护进程
        +requirepass password #需要密码登录
        +pidfile /var/run/redis_6379.pid #pid文件
        vim /usr/local/redis/redis.conf
        +修改
        +bind 0.0.0.0 #所有ipv4端口
        +protected-mode no # 关闭保护模式
        +daemonize yes # 守护进程
        +requirepass password #需要密码登录
        +pidfile /var/run/redis_6379.pid #pid文件
      • bash
        # 授权
        +chmod 777 /etc/init.d/redis
        # 授权
        +chmod 777 /etc/init.d/redis
      • bash
        # 开机启动
        +chkconfig redis on
        # 开机启动
        +chkconfig redis on
      • bash
        # 创建客户端软链接
        +ln -s /usr/local/redis/bin/redis-cli /usr/local/bin/redis-cli
        # 创建客户端软链接
        +ln -s /usr/local/redis/bin/redis-cli /usr/local/bin/redis-cli
  • docker

    • 卸载旧版本

      bash
      sudo yum remove docker \
      +                  docker-client \
      +                  docker-client-latest \
      +                  docker-common \
      +                  docker-latest \
      +                  docker-latest-logrotate \
      +                  docker-logrotate \
      +                  docker-engine
      sudo yum remove docker \
      +                  docker-client \
      +                  docker-client-latest \
      +                  docker-common \
      +                  docker-latest \
      +                  docker-latest-logrotate \
      +                  docker-logrotate \
      +                  docker-engine
    • 使用docker repository安装

      bash
      # set up repository
      +sudo yum install -y yum-utils
      + 
      +sudo yum-config-manager \
      +    --add-repo \
      +    https://download.docker.com/linux/centos/docker-ce.repo
      +    
      +    
      +# install docker engine
      +sudo yum install docker-ce docker-ce-cli containerd.io
      +
      +# start docker engine
      +sudo systemctl start docker
      # set up repository
      +sudo yum install -y yum-utils
      + 
      +sudo yum-config-manager \
      +    --add-repo \
      +    https://download.docker.com/linux/centos/docker-ce.repo
      +    
      +    
      +# install docker engine
      +sudo yum install docker-ce docker-ce-cli containerd.io
      +
      +# start docker engine
      +sudo systemctl start docker
+ + + + \ No newline at end of file diff --git "a/linux/env/\346\234\215\345\212\241\345\231\250\345\220\257\347\224\250ssh\345\257\206\351\222\245\347\231\273\345\275\225\345\271\266\347\246\201\347\224\250\345\257\206\347\240\201\347\231\273\345\275\225.html" "b/linux/env/\346\234\215\345\212\241\345\231\250\345\220\257\347\224\250ssh\345\257\206\351\222\245\347\231\273\345\275\225\345\271\266\347\246\201\347\224\250\345\257\206\347\240\201\347\231\273\345\275\225.html" new file mode 100644 index 000000000..511dd4667 --- /dev/null +++ "b/linux/env/\346\234\215\345\212\241\345\231\250\345\220\257\347\224\250ssh\345\257\206\351\222\245\347\231\273\345\275\225\345\271\266\347\246\201\347\224\250\345\257\206\347\240\201\347\231\273\345\275\225.html" @@ -0,0 +1,57 @@ + + + + + + 服务器启用ssh密钥登录并禁用密码登录 | 故事 + + + + + + + + + + + + + + +
Skip to content

服务器启用ssh密钥登录并禁用密码登录

There were 89 failed login attempts since the last successful login.

最近登录阿里云服务器,总是发现有人恶意尝试登录,虽然密码强度很高,但是看着就闹心,索性把密码登录给ban掉改用密钥登录。

生成密钥对

bash
cd ~/.ssh
+ssh-keygen -t rsa -C "邮箱地址"
cd ~/.ssh
+ssh-keygen -t rsa -C "邮箱地址"

此时会在/root/.ssh下生成id_rsa和id_rsa.pub的私钥和公钥

服务器安装公钥

使用ssh-copy-id命令将公钥拷贝到服务器上

把本地的ssh公钥文件安装到远程主机对应的账户下,ssh-copy-id命令 可以把本地主机的公钥复制到远程主机的authorized_keys文件上,ssh-copy-id命令也会给远程主机的用户主目录(home)和~/.ssh, 和~/.ssh/authorized_keys设置合适的权限。 ssh-copy-id 用来将本地公钥复制到远程主机。如果不传入 -i 参数,ssh-copy-id 使用默认 ~/.ssh/identity.pub 作为默认公钥。如果多次运行 ssh-copy-id ,该命令不会检查重复,会在远程主机中多次写入 authorized_keys 。

bash
ssh-copy-id [-i identify_file] [user@]host
ssh-copy-id [-i identify_file] [user@]host

修改ssh登录设置

bash
vim /etc/ssh/sshd_config
+
+RSAAuthentication yes
+PubkeyAuthentication yes
+AuthorizedKeysFile .ssh/authorized_keys
+PasswordAuthentication yes #密码登录 此时不要关闭
vim /etc/ssh/sshd_config
+
+RSAAuthentication yes
+PubkeyAuthentication yes
+AuthorizedKeysFile .ssh/authorized_keys
+PasswordAuthentication yes #密码登录 此时不要关闭

修改并保存,重启sshd服务systemctl restart sshd

设置私钥权限

id_rsa文件权限需要调整,否则使用密钥登录会因为私钥文件权限问题被拒绝。

一般来说: .ssh目录设置700权限 id_rsa,authorized_keys文件设置600权限 id_rsa.pub,known_hosts文件设置644权限

修改登录命令

登录使用ssh -i "私钥文件全路径" root@xxx.xxx.xxx.xxx

尝试登录

image-20211218211917959

登录成功

关闭云服务器

密钥登录成功后即可关闭服务器的密码登录

bash
vim /etc/ssh/sshd_config
+
+PasswordAuthentication no #关闭密码登录
vim /etc/ssh/sshd_config
+
+PasswordAuthentication no #关闭密码登录

保存后重启sshd服务systemctl restart sshd

尝试密码登录

image-20211218212154923

可以看到密码登录已经被关闭

手动配置ssh密钥

shell
adduser username
+
+mkdir /home/username/.ssh
+# 编辑authorized_keys文件,将生成的公钥添加进去
+vim /home/username/.ssh/authorized_keys
+
+chown -R username:username /home/username.ssh
+chmod 700 /home/username/.ssh
+chmod 600 /home/username/.ssh/authorized_keys
adduser username
+
+mkdir /home/username/.ssh
+# 编辑authorized_keys文件,将生成的公钥添加进去
+vim /home/username/.ssh/authorized_keys
+
+chown -R username:username /home/username.ssh
+chmod 700 /home/username/.ssh
+chmod 600 /home/username/.ssh/authorized_keys
+ + + + \ No newline at end of file diff --git "a/linux/env/\351\223\276\346\216\245\345\222\214\345\210\253\345\220\215(ln\343\200\201alias).html" "b/linux/env/\351\223\276\346\216\245\345\222\214\345\210\253\345\220\215(ln\343\200\201alias).html" new file mode 100644 index 000000000..8478f2700 --- /dev/null +++ "b/linux/env/\351\223\276\346\216\245\345\222\214\345\210\253\345\220\215(ln\343\200\201alias).html" @@ -0,0 +1,25 @@ + + + + + + 链接和别名(ln、alias) | 故事 + + + + + + + + + + + + + + +
Skip to content

链接和别名(ln、alias)

链接

ln: link,链接,类似windows中的快捷方式的概念,主要针对路径比较长的文件夹,建立一个链接,让访问更加方便,分为软链接(-s)和硬链接

使用

  • ln /usr/local/maven/bin/mvn(真实的路径) /usr/bin/mvn(链接路径) ## 硬链接
  • ln -s /usr/local/maven/bin/mvn(真实的路径) /usr/bin/mvn(链接路径) ##软链接-符号链接

别名

alias:别名,主要是针对某个命令,如果命令的路径比较长、比较复杂,那么起个别名会更方便

使用

  • 查看别名:alias
  • 定义别名:alias la=‘ll -a'
  • 取消别名:unalias la
+ + + + \ No newline at end of file diff --git "a/linux/hardware/linux\347\243\201\347\233\230\346\223\215\344\275\234\347\233\270\345\205\263.html" "b/linux/hardware/linux\347\243\201\347\233\230\346\223\215\344\275\234\347\233\270\345\205\263.html" new file mode 100644 index 000000000..e2914ce03 --- /dev/null +++ "b/linux/hardware/linux\347\243\201\347\233\230\346\223\215\344\275\234\347\233\270\345\205\263.html" @@ -0,0 +1,89 @@ + + + + + + linux磁盘操作相关 | 故事 + + + + + + + + + + + + + + +
Skip to content

linux磁盘操作相关

查看磁盘、分区情况

fdisk -l:查看硬盘个数、分区情况

分区相关

fdisk /dev/sdx:磁盘分区,只能分小容量(2T以内),mbr分区表

parted /dev/sdx:磁盘分区,gpt分区表

多个分区需要注意4k对齐,4k对齐内容介绍见下文引用的disk genius的文章

磁盘格式化

根据自己需求的文件格式选择,例如常见的ext4文件系统

mkfs.ext4 [-b block-size] [-C cluster-size] [-i bytes-per-inode] [-I inode-size] [-N number-of-inodes] [-t fs-type] [-T usage-type ] device

可以指定inode的个数和占用的空间和多少字节一个inode,需要注意inode如果过小,当inode使用完后,即使磁盘有空间也无法再写入数据。/etc/mke2fs.conf文件中定义了一些默认的类型配置,usage-type根据文件系统的主要用途对应的类型,例如largefile类型为1M一个inode,还有largefile4为4M一个inode,这样可以减少inode的占用空间和个数,用于存储大文件,但是实际上inode占用的空间对于大容量硬盘来说很少。

调整磁盘预留空间

ext2/3/4文件系统会预留5%空间用于紧急情况,保障在硬盘快满的时候不至于crash,但是一个16T的硬盘的5%将近800G,实在是浪费。可以通过tune2fs命令来减少预留空间

tune2fs -m 1 /dev/sdx:将设备的预留空间调整为1%,也可以改成0,但不建议。

磁盘挂载

手动挂载(重启失效):mount /dev/sdx /path

自动挂载(开机自动挂载):

  • 命令blkid /dev/sdx记录需要挂载设备的uuid
  • vim /etc/fstab添加一条记录,例如
shell
UUID=506ed1d0-bc35-4585-8496-1ff4de100982 /repo ext4 defaults 0 0
+# 第一项为设备uuid 第二项为挂载点即挂载目录 第三项为文件系统类型 第四项为挂载的状态例如ro(只读)或defaults(包含了读写rw,exec,async等) 
+# 第五项为DUMP选项 默认0 第六项为被fsck命令决定启动时被扫描的文件系统顺序 仓库盘可以填0不需要扫描,系统盘1,其他可2
UUID=506ed1d0-bc35-4585-8496-1ff4de100982 /repo ext4 defaults 0 0
+# 第一项为设备uuid 第二项为挂载点即挂载目录 第三项为文件系统类型 第四项为挂载的状态例如ro(只读)或defaults(包含了读写rw,exec,async等) 
+# 第五项为DUMP选项 默认0 第六项为被fsck命令决定启动时被扫描的文件系统顺序 仓库盘可以填0不需要扫描,系统盘1,其他可2

磁盘扩容

shell
# 使用`parted /dev/sda`
+GNU Parted 3.4
+Using /dev/sda
+Welcome to GNU Parted! Type 'help' to view a list of commands.
+
+# `p`输出信息
+# 扩容前:
+(parted) p
+Model: ATA QEMU HARDDISK (scsi)
+Disk /dev/sda: 859GB
+Sector size (logical/physical): 512B/512B
+Partition Table: gpt
+Disk Flags:
+
+Number  Start   End     Size    File system  Name  Flags
+ 1      1049kB  2097kB  1049kB                     bios_grub
+ 2      2097kB  537GB   537GB   ext4
+
+# 使用`resizepart 2 -1`
+
+# 扩容后
+(parted) p
+Model: ATA QEMU HARDDISK (scsi)
+Disk /dev/sda: 859GB
+Sector size (logical/physical): 512B/512B
+Partition Table: gpt
+Disk Flags:
+
+Number  Start   End     Size    File system  Name  Flags
+ 1      1049kB  2097kB  1049kB                     bios_grub
+ 2      2097kB  859GB   859GB   ext4
# 使用`parted /dev/sda`
+GNU Parted 3.4
+Using /dev/sda
+Welcome to GNU Parted! Type 'help' to view a list of commands.
+
+# `p`输出信息
+# 扩容前:
+(parted) p
+Model: ATA QEMU HARDDISK (scsi)
+Disk /dev/sda: 859GB
+Sector size (logical/physical): 512B/512B
+Partition Table: gpt
+Disk Flags:
+
+Number  Start   End     Size    File system  Name  Flags
+ 1      1049kB  2097kB  1049kB                     bios_grub
+ 2      2097kB  537GB   537GB   ext4
+
+# 使用`resizepart 2 -1`
+
+# 扩容后
+(parted) p
+Model: ATA QEMU HARDDISK (scsi)
+Disk /dev/sda: 859GB
+Sector size (logical/physical): 512B/512B
+Partition Table: gpt
+Disk Flags:
+
+Number  Start   End     Size    File system  Name  Flags
+ 1      1049kB  2097kB  1049kB                     bios_grub
+ 2      2097kB  859GB   859GB   ext4
  • resizepart partition end:end参数填分区结束扇区号,直接使用resizepart 2 -1可以自动扩展到可用空间的末尾
  • 或者使用fdisk扩容,先删除再新增

硬盘分区4k对齐

内容引用自disk genius文章:分区4K对齐那些事,你想知道的都在这里

物理扇区的概念

分区对齐,是指将分区起始位置对齐到一定的扇区。我们要先了解对齐和扇区的关系。我们知道,硬盘的基本读写单位是“扇区”。对于硬盘的读写操作,每次读写都是以扇区为单位进行的,最少一个扇区,通常是512个字节。由于硬盘数据存储结构的限制,单独读写1个或几个字节是不可能的。通过系统提供的接口读写文件数据时,看起来可以单独读写少量字节,实际上是经过了操作系统的转换才实现的。硬盘实际执行时读写的仍然是整个扇区。

近年来,随着对硬盘容量的要求不断增加,为了提高数据记录密度,硬盘厂商往往采用增大扇区大小的方法,于是出现了扇区大小为4096字节的硬盘。我们将这样的扇区称之为“物理扇区”。但是这样的大扇区会有兼容性问题,有的系统或软件无法适应。为了解决这个问题,硬盘内部将物理扇区在逻辑上划分为多个扇区片段并将其作为普通的扇区(一般为512字节大小)报告给操作系统及应用软件。这样的扇区片段我们称之为“逻辑扇区”。实际读写时由硬盘内的程序(固件)负责在逻辑扇区与物理扇区之间进行转换,上层程序“感觉”不到物理扇区的存在。

逻辑扇区是硬盘可以接受读写指令的最小操作单元,是操作系统及应用程序可以访问的扇区,多数情况下其大小为512字节。我们通常所说的扇区一般就是指的逻辑扇区。物理扇区是硬盘底层硬件意义上的扇区,是实际执行读写操作的最小单元。是只能由硬盘直接访问的扇区,操作系统及应用程序一般无法直接访问物理扇区。一个物理扇区可以包含一个或多个逻辑扇区(比如多数硬盘的物理扇区包含了8个逻辑扇区)。当要读写某个逻辑扇区时,硬盘底层在实际操作时都会读写逻辑扇区所在的整个物理扇区。

这里说的“硬盘”及其“扇区”的概念,同样适用于存储卡、固态硬盘(SSD)。接下来我们统称其为“磁盘”。它们在使用上的基本原理是一致的。其中固态硬盘在实现上更加复杂,它有“页”和“块”的概念,为了便于理解,我们可以简单的将其视同为逻辑扇区和物理扇区。另外固态硬盘在写入数据之前必须先执行擦除操作,不能直接写入到已存有数据的块,必须先擦除再写入。所以固态硬盘(SSD)对分区4K对齐的要求更高。如果没有对齐,额外的动作会增加更多,造成读写性能下降。

分区及其格式化

磁盘在使用之前必须要先分区并格式化。简单的理解,分区就是指从磁盘上划分出来的一大片连续的扇区。格式化则是对分区范围内扇区的使用进行规划。比如文件数据的储存如何安排、文件属性储存在哪里、目录结构如何存储等等。分区经过格式化后,就可以存储文件了。格式化程序会将分区里面的所有扇区从头至尾进行分组,划分为固定大小的“簇”,并按顺序进行编号。每个“簇”可固定包含一个或多个扇区,其扇区个数总是2的n次方。格式化以后,分区就会以“簇”为最小单位进行读写。文件的数据、属性等等信息都要保存到“簇”里面。

为什么要分区对齐

为磁盘划分分区时,是以逻辑扇区为单位进行划分的,分区可以从任意编号的逻辑扇区开始。如果分区的起始位置没有对齐到某个物理扇区的边缘,格式化后,所有的“簇”也将无法对齐到物理扇区的边缘。如下图所示,每个物理扇区由4个逻辑扇区组成。分区是从3号扇区开始的。格式化后,每个簇占用了4个扇区,这些簇都没有对齐到物理扇区的边缘,也就是说,每个簇都跨越了2个物理扇区。

为什么要分区对齐

由于磁盘总是以物理扇区为单位进行读写,在这样的分区情况下,当要读取某个簇时,实际上总是需要多读取一个物理扇区的数据。比如要读取0号簇共4个逻辑扇区的数据,磁盘实际执行时,必须要读取0号和1号两个物理扇区共8个逻辑扇区的数据。同理,对“簇”的写入操作也是这样。显而易见,这样会造成读写性能的严重下降。

下面再看对齐的情况。如下图所示,分区从4号扇区开始,刚好对齐到了物理扇区1的边缘,格式化后,每个簇同样占用了4个扇区,而且这些簇都对齐到了物理扇区的边缘。

为什么要分区对齐

在这样对齐的情况下,当要读取某个簇,磁盘实际执行时并不需要额外读取任何扇区,可以充分发挥磁盘的读写性能。显然这正是我们需要的。

由此可见,对于物理扇区大小与逻辑扇区大小不一致的磁盘,分区4K对齐才能充分发挥磁盘的读写性能。而不对齐就会造成磁盘读写性能的下降。

如何才能对齐

通过前述图示的两个例子可以看到,只要将分区的起始位置对齐到物理扇区的边缘,格式化程序就会将每个簇也对齐到物理扇区的边缘,这样就实现了分区的对齐。其实对齐很简单。

如何检测物理扇区大小

划分分区时,要想实现4K对齐,必须首先知道磁盘物理扇区的大小。那么如何查询呢?

打开DiskGenius软件,点击要检测的磁盘,在软件界面右侧的磁盘参数表中,可以找到“扇区大小”和“物理扇区大小”。其中“扇区大小”指的是逻辑扇区的大小。如图所示,这个磁盘的物理扇区大小为4096字节,通过计算得知,它包含了8个逻辑扇区。

DiskGenius查看结果

对齐到多少个扇区才正确

知道了“扇区大小”和“物理扇区大小”,用“物理扇区大小”除以“扇区大小”就能得到每个物理扇区所包含的逻辑扇区个数。这个数值就是我们要对齐的扇区个数的最小值。只要将分区起始位置对齐到这个数值的整数倍就可以了。举个例子,比如物理扇区大小是4096字节,逻辑扇区大小是512字节,那么4096除以512,等于8。我们只要将分区起始位置对齐到8的整数倍扇区就能满足分区对齐的要求。比如对齐到8、16、24、32、... 1024、2048等等。只要这个起始扇区号能够被8整除就都可以。并不是这个除数数值越大越好。Windows系统默认对齐的扇区数是2048。这个数值基本上能满足几乎所有磁盘的4K对齐要求了。

为什么大家都说4K对齐

习惯而已。因为开始出现物理扇区的概念时,多数磁盘的物理扇区大小都是4096即4K字节,习惯了就俗称4K对齐了。实际划分分区时还是要检测一下物理扇区大小,因为有些磁盘的物理扇区可能包含4个、8个、16个或者更多个逻辑扇区(总是2的n次方)。知道物理扇区大小后,再按照刚才说的计算方法,以物理扇区包含的逻辑扇区个数为基准,对齐到实际的物理扇区大小才是正确的。如果物理扇区大小是8192字节,那就要按照8192字节来对齐,严格来讲,这就不能叫4K对齐了。

划分分区时如何具体操作分区对齐

以DiskGenius软件为例,建立新分区时,在“建立新分区”对话框中勾选“对齐到下列扇区数的整数倍”,然后选择需要对齐的扇区数目,点“确定”后建立的分区就是对齐的了。

DiskGenius复制文件

软件在“扇区数目”下拉框中列出了很多的选项,从中选择任意一个大于物理扇区大小的扇区数都是可以的,都能满足对齐要求。软件列出那么多的扇区数选项只是增加了选择的自由度,并不是数值越大越好。使用过大的数值可能会造成磁盘空间的浪费。软件默认的设置已经能够满足几乎所有磁盘的 4K对齐要求。

除了“建立新分区”对话框,DiskGenius软件还有一个“快速分区”功能,其中也有相同的对齐设置。如下图所示:

注册DiskGenius

如何检测是否对齐

作为一款强大的分区管理软件,DiskGenius同样提供了分区4K对齐检测的功能。你可以用它检测一下自己硬盘的分区是否对齐了。使用方法很简单,打开软件后,首先在软件左侧选中要检测的磁盘,然后选择“工具”菜单中的“分区4KB扇区对齐检测”,软件立即显示检测结果,如下图所示:

注册DiskGenius

最右侧“对齐”一栏是“Y”的分区就是对齐的分区,否则就是没有对齐。没有对齐的分区会用红色字体显示。

+ + + + \ No newline at end of file diff --git a/linux/index.html b/linux/index.html new file mode 100644 index 000000000..c9fafc72b --- /dev/null +++ b/linux/index.html @@ -0,0 +1,25 @@ + + + + + + Linux | 故事 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7f95a4297b3abf1dbe37d27e200426f91a84c2e1 GIT binary patch literal 104890 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%0Aa{%NJ=Q#2CRLOk2k4w7B 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 Y|DDMUnQGmvJRa_+qM(T=mopFiKeQr=Z~y=R literal 0 HcmV?d00001 diff --git a/python/base/Poetry.html b/python/base/Poetry.html new file mode 100644 index 000000000..397cf1acc --- /dev/null +++ b/python/base/Poetry.html @@ -0,0 +1,41 @@ + + + + + + Poetry | 故事 + + + + + + + + + + + + + + +

Skip to content

Poetry

Poetry: PYTHON PACKAGING AND DEPENDENCY MANAGEMENT MADE EASY

https://python-poetry.org/

简介

Poetry是一个Python包管理工具,类似于pip,但是远比pip强大,除了依赖管理外,还能自动解析依赖的关系,还可以管理项目的虚拟环境,打包、发布。

pip的缺陷:移除依赖时不能自动解析依赖的关系

如果执行pip install flask,pip会自动安装flask所依赖的包,但是如果执行pip uninstall flask,pip只会移除flask包,而不会移除flask依赖的包,这样就会导致项目中存在无用的包,只能我们手动移除。但是手动移除包是很危险的,因为一不小心可能会把其他包依赖的包删除了导致项目不能正常运行。

Poetry使用了PEP 518提出的pyproject.toml配置文件,在依赖管理方面替代了传统的requirements.txt,在构建方面替换了传统的setup.py,更加清晰、灵活。

安装和基本使用

安装

https://python-poetry.org/docs/#installing-with-the-official-installer

Poetry在各个操作系统的默认安装路径:

  • ~/Library/Application Support/pypoetry on MacOS.
  • ~/.local/share/pypoetry on Linux/Unix.
  • %APPDATA%\pypoetry on Windows.

在安装时指定POETRY_HOME环境变量:

sh
curl -sSL https://install.python-poetry.org | POETRY_HOME=/etc/poetry python3 -
curl -sSL https://install.python-poetry.org | POETRY_HOME=/etc/poetry python3 -

$POETRY_HOME/bin添加到PATH

查看版本:poetry --version

更新版本:poetry self update

命令补全:

sh
mkdir $ZSH_CUSTOM/plugins/poetry
+poetry completions zsh > $ZSH_CUSTOM/plugins/poetry/_poetr
mkdir $ZSH_CUSTOM/plugins/poetry
+poetry completions zsh > $ZSH_CUSTOM/plugins/poetry/_poetr
shell
# .zshrc
+plugins=(
+  git
+  zsh-autosuggestions
+  zsh-syntax-highlighting
+  poetry
+  ...
+)
# .zshrc
+plugins=(
+  git
+  zsh-autosuggestions
+  zsh-syntax-highlighting
+  poetry
+  ...
+)

初始化

  • 初始化项目:poetry init

使用

  • 配置虚拟环境生成到项目路径下:poetry config virtualenvs.in-project true

默认的情况poetry会将虚拟环境生成到特定目录(根据操作系统有不同),命名规则为项目名-random-python版本,这样并不方便管理,所以改为在项目目录下生成虚拟环境,更符合使用习惯,修改后生成的虚拟环境在项目路径下的.venv

  • 创建虚拟环境:poetry env use python

取决于python在PATH中link的版本,也可以改为poetry env use python3.11

  • 启动虚拟环境:poetry shell
  • 退出虚拟环境:exit
  • 新增依赖:poetry add

执行poetry add后会自动将add的包信息和版本添加到pyproject.toml中(不会记录该包依赖的其他包),这样就可以区分出主动安装的是什么包,和基于依赖关系安装的是什么包。

除了pyproject.toml,项目中还会生成一个poetry.lock文件(类似npm的lock文件或原来的requirements.txt),记录安装的所有依赖和对应版本

  • 指定版本新增依赖:

    • ^:表示匹配指定版本的最新次版本(minor version)和补丁版本(patch version),但不改变主版本(major version)。
      • 示例:poetry add Django@^2.1.0 表示匹配2.x.x中最新的版本不包括3.0.0。
    • ~:表示匹配指定版本的最新补丁版本,不改变主版本和次版本。
      • 示例:poetry add Django@~1.2.3表示匹配1.2.x中最新的版本,但不包括1.3.0。
    • >=:表示匹配指定版本或更高版本,不限制最后一位的变化。
      • 示例:poetry add Django>=3.2.0表示匹配Django3.2版本及其更高版本。
    • ==:表示严格匹配指定版本。
      • 示例:poetry add numpy==1.21.3表示只匹配精确版本号为1.21.3。
  • 新增依赖到dev-dependencies:poetry add xxx -Dpoetry add xxx --dev

  • 手动更新依赖版本:

    • 更新pyproject.toml中的依赖版本
    • 更新lock文件中的版本:poetry lock
    • 重新安装依赖到虚拟环境:poetry install
  • poetry install

    • 安装依赖项: 执行该命令将会读取pyproject.toml文件中的[tool.poetry.dependencies]部分,并根据其中的规范(例如包的名称和版本要求)来安装依赖项。
    • 生成锁文件: 如果项目中没有poetry.lock文件,poetry install会生成poetry.lock文件。这个锁文件包含了确切的依赖项版本,确保在不同的环境中使用相同的软件包版本。
    • 加速依赖项安装: 如果存在poetry.lock文件,poetry install将首先检查锁文件并使用其中的版本信息,而不是重新计算依赖关系。这有助于提高依赖项安装的速度。
    • 创建虚拟环境: 如果项目中没有虚拟环境poetry install会自动创建一个虚拟环境,并将依赖项安装到该虚拟环境中。如果已经存在虚拟环境,将会使用现有的虚拟环境。
  • 更新依赖:poetry update

  • 列出依赖:poetry show

    • 树状依赖:poetry show --tree
  • 移除依赖:poetry remove

  • 输出requirements.txt:poetry export

    • poetry export -f requirements.txt -o requirements.txt --without-hashes
  • 打包:poetry build

    • 只打包wheel:poetry build -f wheel
  • 发布:poetry publish

    需要配置仓库

+ + + + \ No newline at end of file diff --git "a/python/base/python\345\237\272\347\241\200\350\257\255\346\263\225.html" "b/python/base/python\345\237\272\347\241\200\350\257\255\346\263\225.html" new file mode 100644 index 000000000..4bc6f4ce3 --- /dev/null +++ "b/python/base/python\345\237\272\347\241\200\350\257\255\346\263\225.html" @@ -0,0 +1,775 @@ + + + + + + python基础语法 | 故事 + + + + + + + + + + + + + + +
Skip to content

python基础语法

第一个Python程序

python
print('hello world')
print('hello world')

基础语法

标识符

  • 第一个字符必须是字母或下划线_
  • 标识符由字母、数字、下划线组成
  • 标识符对大小写敏感

python3中支持中文变量名

python关键字

python
import keyword
+
+
+print(keyword.kwlist)
+
+
+['False', 'None', 'True', '__peg_parser__', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']
import keyword
+
+
+print(keyword.kwlist)
+
+
+['False', 'None', 'True', '__peg_parser__', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']

注释

python中单行注释#

多行注释可以多个#或者三单引号'''和三双引号"""

行与缩进

python使用缩进来表示代码块,而不是像Java一样的{},目前还很不习惯。。

python
if True:
+    print('true')
+else:
+    print('false')
if True:
+    print('true')
+else:
+    print('false')

多行语句

python一般一行写一条一句,特殊情况可以使用反斜杠\实现多行语句

python
total = item_one + \
+        item_two + \
+        item_three
total = item_one + \
+        item_two + \
+        item_three

[],{},()中的多行语句不需要\

数字类型

python中的数字有四种类型:

  • int整型
  • bool布尔
  • float浮点
  • complex复数 例如 1+2j

字符串

  • python中的单引号和双引号完全相同
  • 使用三引号可以指定多行字符串
  • 转义符\
  • 反斜杠可用作转移,使用r可以让反斜杠不发生转义。例如 r‘this is a line with \n’ 则会显示\n而非换行
  • 级联字符串“this” “is” “string” 会转换为“this is string”
  • 字符串可以用+运算符实现拼接,用*表示重复
  • python中字符串有两种索引方式,从左往右从0开始,从右往左从-1开始
  • python中字符串不能改变(和java一样)
  • python没有字符类型,一个字符就是长度为1的字符串
  • 字符串切片:变量[头下标:尾下标:步长]

空行

函数之间或类之间用空行分隔,一般是两个空行,以分隔两段不同功能或含义的代码

同一行显示多条语句

使用;分隔(ps,python的语句后面不用每行加分号,好不习惯。。。)

代码块

缩进相同的一组语句构成一个代码块,我们称首行及后面的代码称为一个子句(clause)

print输出

print输出类似java中的println,默认是换行的,如果不想换行在print函数中加入参数ene=''

例如 print('x', end ='' )

基本数据类型

python中的变量不需要声明(但python是强类型语言),使用变量前必须赋值,赋值以后变量才会创建。

在python中,变量就是变量,没有类型。我们所说的类型是变量所指的内存中对象的数据类型

多个变量赋值

python
a = b = c = 1
a = b = c = 1

以上实例,创建一个整型对象1,从后向前赋值,三个变量被赋予相同的值

python
a, b, c = 1, 2, 'noob'
a, b, c = 1, 2, 'noob'

以上实例,两个整型对象1,2分配给a和b,字符串对象noob分配给变量c

标准数据类型

python3中有六个标准数据类型

  • Number
  • String
  • List
  • Tuple
  • Set
  • Dictionary

其中

  • 可变数据类型:List、Dictionary、Set
  • 不可变:Number、String、Tuple

判断数据类型

type()函数或者isinstance()函数

区别:

  • type()不会认为子类是父类的类型
  • isinstance()会认为子类是父类类型

数值运算符

特殊:

  • //除法,得到一个整数
  • %取余数(取模)
  • 混合计算时,python会把整型转换为浮点型

String

python的字符串用单引号或双引号括起来

特殊用法:

定义字符串时 加前缀 u/b/r/f

  • u:作用以Unicode格式编码字符串,一般用在中文字符串前面,防止因为源码储存格式问题,导致再次使用时出现乱码
  • b:表示:后面字符串是bytes 类型
  • r:作用是去除转义字符
  • f:作用是支持大括号内的python 表达式

字符串截取语法:变量[头下标:尾下标] 前闭后开

字符串的切片:

python
str = 'Runoob'
+
+print (str)          # 输出字符串
+print (str[0:-1])    # 输出第一个到倒数第二个的所有字符 
+print (str[0])       # 输出字符串第一个字符
+print (str[2:5])     # 输出从第三个开始到第五个的字符
+print (str[2:])      # 输出从第三个开始的后的所有字符
+print (str * 2)      # 输出字符串两次,也可以写成 print (2 * str)
+print (str + "TEST") # 连接字符串
+
+结果:
+Runoob
+Runoo
+R
+noo
+noob
+RunoobRunoob
+RunoobTEST
str = 'Runoob'
+
+print (str)          # 输出字符串
+print (str[0:-1])    # 输出第一个到倒数第二个的所有字符 
+print (str[0])       # 输出字符串第一个字符
+print (str[2:5])     # 输出从第三个开始到第五个的字符
+print (str[2:])      # 输出从第三个开始的后的所有字符
+print (str * 2)      # 输出字符串两次,也可以写成 print (2 * str)
+print (str + "TEST") # 连接字符串
+
+结果:
+Runoob
+Runoo
+R
+noo
+noob
+RunoobRunoob
+RunoobTEST

List

List是python中使用最频繁的数据类型

列表是写在方括号[]之间、用逗号分隔开的元素列表

和字符串一样,列表可以被索引和截取,列表被截取后返回一个新列表

注意:

  • 列表可以使用+进行拼接

Tuple元组

tuple和列表类似,但是是由()括起来的,且不可变数据类型

元素一样可以被索引和截取,元组也可以使用+进行拼接

Set

set可以使用大括号{}或者set()函数创建集合,创建一个空集合时必须使用set()函数而非{ },因为{ }被用来创建一个空字典

set和java中的set集合一样存储的数据都是不重复的

Dictionary

字典(dictionary)是Python中另一个非常有用的内置数据类型。 类似java的Map

列表是有序的对象集合,字典是无序的对象集合。两者之间的区别在于:字典当中的元素是通过键来存取的,而不是通过偏移存取。

字典是一种映射类型,字典用 { } 标识,它是一个无序的 键(key) : 值(value) 的集合。

键(key)必须使用不可变类型。

在同一个字典中,键(key)必须是唯一的。

python的数据类型转换

语法作用
[int(x ,base])将x转换为一个整数
float(x)将x转换到一个浮点数
[complex(real ,imag])创建一个复数
str(x)将对象 x 转换为字符串
repr(x)将对象 x 转换为表达式字符串
eval(str)用来计算在字符串中的有效Python表达式,并返回一个对象
tuple(s)将序列 s 转换为一个元组
list(s)将序列 s 转换为一个列表
set(s)转换为可变集合
dict(d)创建一个字典。d 必须是一个 (key, value)元组序列。
frozenset(s)转换为不可变集合
chr(x)将一个整数转换为一个字符
ord(x)将一个字符转换为它的整数值
hex(x)将一个整数转换为一个十六进制字符串
oct(x)将一个整数转换为一个八进制字符串

运算符

算术运算符

运算符描述实例
+加 - 两个对象相加a + b 输出结果 31
-减 - 得到负数或是一个数减去另一个数a - b 输出结果 -11
*乘 - 两个数相乘或是返回一个被重复若干次的字符串a * b 输出结果 210
/除 - x 除以 yb / a 输出结果 2.1
%取模 - 返回除法的余数b % a 输出结果 1
**幂 - 返回x的y次幂a**b 为10的21次方
//取整除 - 向下取接近商的整数>>> 9//2 4 >>> -9//2 -5

比较运算符

运算符描述实例
==等于 - 比较对象是否相等(a == b) 返回 False。
!=不等于 - 比较两个对象是否不相等(a != b) 返回 True。
>大于 - 返回x是否大于y(a > b) 返回 False。
<小于 - 返回x是否小于y。所有比较运算符返回1表示真,返回0表示假。这分别与特殊的变量True和False等价。注意,这些变量名的大写。(a < b) 返回 True。
>=大于等于 - 返回x是否大于等于y。(a >= b) 返回 False。
<=小于等于 - 返回x是否小于等于y。(a <= b) 返回 True。

赋值运算符

运算符描述实例
=简单的赋值运算符c = a + b 将 a + b 的运算结果赋值为 c
+=加法赋值运算符c += a 等效于 c = c + a
-=减法赋值运算符c -= a 等效于 c = c - a
*=乘法赋值运算符c *= a 等效于 c = c * a
/=除法赋值运算符c /= a 等效于 c = c / a
%=取模赋值运算符c %= a 等效于 c = c % a
**=幂赋值运算符c **= a 等效于 c = c ** a
//=取整除赋值运算符c //= a 等效于 c = c // a
:=海象运算符,可在表达式内部为变量赋值。Python3.8 版本新增运算符在这个示例中,赋值表达式可以避免调用 len() 两次:if (n := len(a)) > 10: print(f"List is too long ({n} elements, expected <= 10)")(这个特性java中默认就有)

逻辑运算符

运算符逻辑表达式描述实例
andx and y布尔"与" - 如果 x 为 False,x and y 返回 x 的值,否则返回 y 的计算值。(a and b) 返回 20。
orx or y布尔"或" - 如果 x 是 True,它返回 x 的值,否则它返回 y 的计算值。(a or b) 返回 10。
notnot x布尔"非" - 如果 x 为 True,返回 False 。如果 x 为 False,它返回 True。not(a and b) 返回 False

成员运算符

运算符描述实例
in如果在指定的序列中找到值返回 True,否则返回 False。x 在 y 序列中 , 如果 x 在 y 序列中返回 True。
not in如果在指定的序列中没有找到值返回 True,否则返回 False。x 不在 y 序列中 , 如果 x 不在 y 序列中返回 True。

身份运算符

运算符描述实例
isis 是判断两个标识符是不是引用自一个对象x is y, 类似 id(x) == id(y) , 如果引用的是同一个对象则返回 True,否则返回 False
is notis not 是判断两个标识符是不是引用自不同对象x is not y , 类似 id(a) != id(b)。如果引用的不是同一个对象则返回结果 True,否则返回 False。

流程控制语句

if条件控制

python的条件控控制语法上和格式上和java一些区别

python
if condition_1:
+    statement_block_1
+elif condition_2:
+    statement_block_2
+else:
+    statement_block_3
if condition_1:
+    statement_block_1
+elif condition_2:
+    statement_block_2
+else:
+    statement_block_3

Python 中用 elif 代替了 else if,所以if语句的关键字为:if – elif – else

注意:

  • 1、每个条件后面要使用冒号 :,表示接下来是满足条件后要执行的语句块。
  • 2、使用缩进来划分语句块,相同缩进数的语句在一起组成一个语句块。
  • 3、在Python中没有switch – case语句。

if后跟的表达式

  • 如果if后面的条件是数字,只要这个数字不是0,python都会把它当做True处理

  • 如果if后面跟的是字符串,则只要这个字符串不为空串,python就把它看作True

  • 同样的如果if后跟元组,list,set,字典 只要不为空就是true

三目运算

和java中的三元运算类似

语法: 变量 = 表达式1 if 条件 else 表达式2

例如

python
a = 1;
+b = a + 1 if a == 1 else a + 2;
+print(b)
+
+结果:2
a = 1;
+b = a + 1 if a == 1 else a + 2;
+print(b)
+
+结果:2

循环控制语句

python中的循环有for和while两种

while

python
while 判断条件(condition):
+    执行语句(statements)……
while 判断条件(condition):
+    执行语句(statements)……
while循环使用else

while...else在条件语句为false时执行else的代码块

python
while expr:
+    statement1
+else:
+    statement2
while expr:
+    statement1
+else:
+    statement2

for

for 循环可以遍历任何可选代对象,如一个列表或者一个字符串。

for循环的一般格式如下:

python
for <variable> in <sequence>:
+    <statements>
+else:
+    <statements>
for <variable> in <sequence>:
+    <statements>
+else:
+    <statements>

continue和break

用法和java一样

break区别

  • python中的else和配合循环使用,在循环穷尽列表(for循环)或条件变为 false (while循环)导致循环终止时被执行,但循环被 break 终止时不执行。

例如:

python
_list = [1, 2, 3, 4, 5]
+
+for i in _list:
+    if i == 3:
+        print('33333333')
+else:
+    print('循环完毕')
+
+执行结果:
+33333333
+循环完毕
_list = [1, 2, 3, 4, 5]
+
+for i in _list:
+    if i == 3:
+        print('33333333')
+else:
+    print('循环完毕')
+
+执行结果:
+33333333
+循环完毕

而当循环是break终止的时候,else代码块不会执行:

python
_list = [1, 2, 3, 4, 5]
+
+for i in _list:
+    if i == 3:
+        print('333')
+        break
+else:
+    print('循环完毕')
+
+执行结果:
+333
_list = [1, 2, 3, 4, 5]
+
+for i in _list:
+    if i == 3:
+        print('333')
+        break
+else:
+    print('循环完毕')
+
+执行结果:
+333

pass

Python pass是空语句,是为了保持程序结构的完整性。

pass 不做任何事情,一般用做占位语句

例如:

python
class MyTestClass:
+    pass
+
+
+def my_func():
+    pass
+
+
+if expr:
+    pass
class MyTestClass:
+    pass
+
+
+def my_func():
+    pass
+
+
+if expr:
+    pass

迭代器与生成器

迭代器

迭代器是python中访问集合元素的一种方式,有两个基本方法iter()next()

字符串、列表或元组对象都可以创建迭代器

python
_list = [1,2,3,4]
+it = iter(_list)
+print(next(it))
+print(next(it))
+
+结果:
+1
+2
_list = [1,2,3,4]
+it = iter(_list)
+print(next(it))
+print(next(it))
+
+结果:
+1
+2

迭代器对象可以使用常规for语句进行遍历:

python
_list = [1, 2, 3, 4]
+it = iter(_list)
+for x in it:
+    print(x, end=" ")
+
+结果:
+1 2 3 4
_list = [1, 2, 3, 4]
+it = iter(_list)
+for x in it:
+    print(x, end=" ")
+
+结果:
+1 2 3 4

生成器

在 Python 中,使用了 yield 的函数被称为生成器(generator)。

跟普通函数不同的是,生成器是一个返回迭代器的函数,只能用于迭代操作,更简单点理解生成器就是一个迭代器。

在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值, 并在下一次执行 next() 方法时从当前位置继续运行。

调用一个生成器函数,返回的是一个迭代器对象。

python
def gen_fun():
+    print('11111111111')
+    yield 1
+    print('22222222222')
+    yield 2
+    yield 3
+obj = gen_fun()
+print(obj)
+for i in obj:
+    print(i)
+#res
+<generator object gen_fun at 0x0000029394291AC0>
+11111111111
+1
+22222222222
+2
+3
def gen_fun():
+    print('11111111111')
+    yield 1
+    print('22222222222')
+    yield 2
+    yield 3
+obj = gen_fun()
+print(obj)
+for i in obj:
+    print(i)
+#res
+<generator object gen_fun at 0x0000029394291AC0>
+11111111111
+1
+22222222222
+2
+3

上面的代码可以看到在调用函数过程中,'111111'和'222222222'并没有打印出来,而是在for循环中才执行,这就是因为yield导致了函数的暂停,而for循环实际底层是迭代器实现,所以才恢复到print语句的位置继续执行

函数

对应java中的方法

定义一个函数

  • 函数代码块以 def 关键词开头,后接函数标识符名称和圆括号 ()
  • 任何传入参数和自变量必须放在圆括号中间,圆括号之间可以用于定义参数。
  • 函数的第一行语句可以选择性地使用文档字符串—用于存放函数说明。
  • 函数内容以冒号 : 起始,并且缩进。
  • return [表达式] 结束函数,选择性地返回一个值给调用方,不带表达式的 return 相当于返回 None。

一般格式:

python
def 函数名(参数列表):
+    函数体
def 函数名(参数列表):
+    函数体

默认情况下,参数值和名称是按声明的顺序匹配的

参数传递

由于python中的变量没有类型,所以不像java的参数列表都是有类型声明的

在 python 中,strings, tuples, 和 numbers 是不可更改的对象,而 list,dict 等则是可以修改的对象。

  • 不可变类型: 变量赋值 a=5 后再赋值 a=10,这里实际是新生成一个 int 值对象 10,再让 a 指向它,而 5 被丢弃,不是改变 a 的值,相当于新生成了 a。
  • 可变类型: 变量赋值 la=[1,2,3,4] 后再赋值 la[2]=5 则是将 list la 的第三个元素值更改,本身la没有动,只是其内部的一部分值被修改了。

python函数的参数传递:这个和java有区别,java都是值传递

  • 不可变类型: 值传递,如整数、字符串、元组。如 fun(a),传递的只是 a 的值,没有影响 a 对象本身。如果在 fun(a) 内部修改 a 的值,则是新生成一个 a 的对象。
  • 可变类型: 引用传递,如 列表,字典。如 fun(la),则是将 la 真正的传过去,修改后 fun 外部的 la 也会受影响

参数

以下是调用函数时可使用的正式参数类型:

必需参数

按照正确顺序传入参数,调用的数量和声明的数量必须一致

关键字参数

调用使用关键字参数来确定传入的参数值,使用关键字参数允许调用时与声明时的顺序不一致,因为python解释器能用参数名匹配参数值

python
def print_me(str):
+    print(str)
+    
+#调用
+print_me(str = 'tom')
+
+结果:
+tom
+
+
+def printinfo( name, age ):
+   "打印任何传入的字符串"
+   print ("名字: ", name)
+   print ("年龄: ", age)
+   return
+ 
+#调用printinfo函数
+printinfo( age=50, name="mike" )
+结果:
+名字: mike
+年龄: 50
def print_me(str):
+    print(str)
+    
+#调用
+print_me(str = 'tom')
+
+结果:
+tom
+
+
+def printinfo( name, age ):
+   "打印任何传入的字符串"
+   print ("名字: ", name)
+   print ("年龄: ", age)
+   return
+ 
+#调用printinfo函数
+printinfo( age=50, name="mike" )
+结果:
+名字: mike
+年龄: 50

默认参数

调用函数时,如果没有传递参数,则会使用默认参数。以下实例中如果没有传入 age 参数,则使用默认值

python
def printinfo( name, age = 35 ):
+   print ("名字: ", name)
+   print ("年龄: ", age)
+   return
+ 
+#调用printinfo函数
+printinfo( age=50, name="tom" )
+print ("------------------------")
+printinfo( name="tom" )
+
+结果:
+名字:  tom
+年龄:  50
+------------------------
+名字:  tom
+年龄:  35
def printinfo( name, age = 35 ):
+   print ("名字: ", name)
+   print ("年龄: ", age)
+   return
+ 
+#调用printinfo函数
+printinfo( age=50, name="tom" )
+print ("------------------------")
+printinfo( name="tom" )
+
+结果:
+名字:  tom
+年龄:  50
+------------------------
+名字:  tom
+年龄:  35

不定长参数

你可能需要一个函数能处理比当初声明时更多的参数。这些参数叫做不定长参数,和上述 2 种参数不同,声明时不会命名。

*args

*args就是就是传递一个可变参数列表给函数实参,这个参数列表的数目未知,甚至长度可以为0。下面这段代码演示了如何使用args

python
def test_args(first, *args):
+    print('Required argument: ', first)
+    print(type(args))
+    for v in args:
+        print ('Optional argument: ', v)
+
+test_args(1, 2, 3, 4)
+
+结果:
+Required argument:  1
+<class 'tuple'>
+Optional argument:  2
+Optional argument:  3
+Optional argument:  4
def test_args(first, *args):
+    print('Required argument: ', first)
+    print(type(args))
+    for v in args:
+        print ('Optional argument: ', v)
+
+test_args(1, 2, 3, 4)
+
+结果:
+Required argument:  1
+<class 'tuple'>
+Optional argument:  2
+Optional argument:  3
+Optional argument:  4
**kwargs

而**kwargs则是将一个可变的关键字参数的字典传给函数实参,同样参数列表长度可以为0或为其他值。下面这段代码演示了如何使用kwargs

python
def test_kwargs(first, *args, **kwargs):
+   print('Required argument: ', first)
+   print(type(kwargs))
+   for v in args:
+      print ('Optional argument (args): ', v)
+   for k, v in kwargs.items():
+      print ('Optional argument %s (kwargs): %s' % (k, v))
+
+test_kwargs(1, 2, 3, 4, k1=5, k2=6)
+
+结果:
+Required argument:  1
+<class 'dict'>
+Optional argument (args):  2
+Optional argument (args):  3
+Optional argument (args):  4
+Optional argument k2 (kwargs): 6
+Optional argument k1 (kwargs): 5
def test_kwargs(first, *args, **kwargs):
+   print('Required argument: ', first)
+   print(type(kwargs))
+   for v in args:
+      print ('Optional argument (args): ', v)
+   for k, v in kwargs.items():
+      print ('Optional argument %s (kwargs): %s' % (k, v))
+
+test_kwargs(1, 2, 3, 4, k1=5, k2=6)
+
+结果:
+Required argument:  1
+<class 'dict'>
+Optional argument (args):  2
+Optional argument (args):  3
+Optional argument (args):  4
+Optional argument k2 (kwargs): 6
+Optional argument k1 (kwargs): 5
参数中单独的*

声明函数时,参数中星号 * 可以单独出现,例如:

参数列表里的 * 星号,标志着位置参数的就此终结,之后的那些参数,都只能以关键字形式来指定。

python
def f(a,b,*,c):
+    return a+b+c
+
+# f(1,2,3)->会报错
+# f(1,2,c=3) -> 正常
def f(a,b,*,c):
+    return a+b+c
+
+# f(1,2,3)->会报错
+# f(1,2,c=3) -> 正常
调用

args和kwargs不仅可以在函数定义中使用,还可以在函数调用中使用。在函数定义时使用就相当于pack(打包),在函数调用时就相当于unpack(解包)。

首先来看一下使用args来解包调用函数的代码,

python
def test_args_kwargs(arg1, arg2, arg3):
+    print("arg1:", arg1)
+    print("arg2:", arg2)
+    print("arg3:", arg3)
+
+args = ("two", 3, 5)
+test_args_kwargs(*args)
+
+结果:
+arg1: two
+arg2: 3
+arg3: 5
def test_args_kwargs(arg1, arg2, arg3):
+    print("arg1:", arg1)
+    print("arg2:", arg2)
+    print("arg3:", arg3)
+
+args = ("two", 3, 5)
+test_args_kwargs(*args)
+
+结果:
+arg1: two
+arg2: 3
+arg3: 5

kwargs的用法类似:

bash
kwargs = {"arg3": 3, "arg2": "two", "arg1": 5}
+test_args_kwargs(**kwargs)
+
+#result
+arg1: 5
+arg2: two
+arg3: 3
kwargs = {"arg3": 3, "arg2": "two", "arg1": 5}
+test_args_kwargs(**kwargs)
+
+#result
+arg1: 5
+arg2: two
+arg3: 3

匿名函数

python中使用lambda来创建匿名函数

所谓匿名,意即不再使用 def 语句这样标准的形式定义一个函数。

  • lambda 只是一个表达式,函数体比 def 简单很多。
  • lambda的主体是一个表达式,而不是一个代码块。仅仅能在lambda表达式中封装有限的逻辑进去。
  • lambda 函数拥有自己的命名空间,且不能访问自己参数列表之外或全局命名空间里的参数。
  • lambda函数只能写一行

语法

python
lambda [arg1 [,arg2,.....argn]]:expression
lambda [arg1 [,arg2,.....argn]]:expression

例子:

python
sum = lambda arg1, arg2: arg1 + arg2
+ 
+# 调用sum函数
+print ("相加后的值为 : ", sum( 10, 20 ))
+print ("相加后的值为 : ", sum( 20, 20 ))
+
+相加后的值为 :  30
+相加后的值为 :  40
sum = lambda arg1, arg2: arg1 + arg2
+ 
+# 调用sum函数
+print ("相加后的值为 : ", sum( 10, 20 ))
+print ("相加后的值为 : ", sum( 20, 20 ))
+
+相加后的值为 :  30
+相加后的值为 :  40

列表推导式

作用:快速生成列表

语法:

python
变量 = [生成规则 for 临时变量 in 集合]
变量 = [生成规则 for 临时变量 in 集合]

每循环一次就会生成一个符合生成规则的数据添加到列表中

例如:

python
my_list = [i for i in range(5)]
+print(my_list)
+
+#res
+[0,1,2,3,4,5]
my_list = [i for i in range(5)]
+print(my_list)
+
+#res
+[0,1,2,3,4,5]

强制位置参数

Python3.8 新增了一个函数形参语法 / 用来指明函数形参必须使用指定位置参数,不能使用关键字参数的形式。

在以下的例子中,形参 a 和 b 必须使用指定位置参数,c 或 d 可以是位置形参或关键字形参,而 e 或 f 要求为关键字形参:

python
def f(a, b, /, c, d, *, e, f):
+    print(a, b, c, d, e, f)
def f(a, b, /, c, d, *, e, f):
+    print(a, b, c, d, e, f)

正确:

python
f(10, 20, 30, d=40, e=50, f=60)
f(10, 20, 30, d=40, e=50, f=60)

错误:

python
f(10, b=20, c=30, d=40, e=50, f=60)   # b 不能使用关键字参数的形式
+f(10, 20, 30, 40, 50, f=60)           # e 必须使用关键字参数的形式
f(10, b=20, c=30, d=40, e=50, f=60)   # b 不能使用关键字参数的形式
+f(10, 20, 30, 40, 50, f=60)           # e 必须使用关键字参数的形式

模块

python
import module1[, module2[,... moduleN]
+from modname import name1[, name2[, ... nameN]]
+from modname import *
import module1[, module2[,... moduleN]
+from modname import name1[, name2[, ... nameN]]
+from modname import *

__name__属性

一个模块被另一个程序第一次引入时,其主程序将运行。如果我们想在模块被引入时,模块中的某一程序块不执行,我们可以用__name__属性来使该程序块仅在该模块自身运行时执行。

python
if __name__ == '__main__':
+   print('程序自身在运行')
+else:
+   print('我来自另一模块')
if __name__ == '__main__':
+   print('程序自身在运行')
+else:
+   print('我来自另一模块')

说明: 每个模块都有一个__name__属性,当其值是'main'时,表明该模块自身在运行,否则是被引入。

说明:namemain 底下是双下划线

dir()函数

python
dir(sys)  
+['__displayhook__', '__doc__', '__excepthook__', '__loader__', '__name__',
+ '__package__', '__stderr__', '__stdin__', '__stdout__',
+ '_clear_type_cache', '_current_frames', '_debugmallocstats', '_getframe',
+ '_home', '_mercurial', '_xoptions', 'abiflags', 'api_version', 'argv',
+ 'base_exec_prefix', 'base_prefix', 'builtin_module_names', 'byteorder',
+ 'call_tracing', 'callstats', 'copyright', 'displayhook',
+ 'dont_write_bytecode', 'exc_info', 'excepthook', 'exec_prefix',
+ 'executable', 'exit', 'flags', 'float_info', 'float_repr_style',
+ 'getcheckinterval', 'getdefaultencoding', 'getdlopenflags',
+ 'getfilesystemencoding', 'getobjects', 'getprofile', 'getrecursionlimit',
+ 'getrefcount', 'getsizeof', 'getswitchinterval', 'gettotalrefcount',
+ 'gettrace', 'hash_info', 'hexversion', 'implementation', 'int_info',
+ 'intern', 'maxsize', 'maxunicode', 'meta_path', 'modules', 'path',
+ 'path_hooks', 'path_importer_cache', 'platform', 'prefix', 'ps1',
+ 'setcheckinterval', 'setdlopenflags', 'setprofile', 'setrecursionlimit',
+ 'setswitchinterval', 'settrace', 'stderr', 'stdin', 'stdout',
+ 'thread_info', 'version', 'version_info', 'warnoptions']
dir(sys)  
+['__displayhook__', '__doc__', '__excepthook__', '__loader__', '__name__',
+ '__package__', '__stderr__', '__stdin__', '__stdout__',
+ '_clear_type_cache', '_current_frames', '_debugmallocstats', '_getframe',
+ '_home', '_mercurial', '_xoptions', 'abiflags', 'api_version', 'argv',
+ 'base_exec_prefix', 'base_prefix', 'builtin_module_names', 'byteorder',
+ 'call_tracing', 'callstats', 'copyright', 'displayhook',
+ 'dont_write_bytecode', 'exc_info', 'excepthook', 'exec_prefix',
+ 'executable', 'exit', 'flags', 'float_info', 'float_repr_style',
+ 'getcheckinterval', 'getdefaultencoding', 'getdlopenflags',
+ 'getfilesystemencoding', 'getobjects', 'getprofile', 'getrecursionlimit',
+ 'getrefcount', 'getsizeof', 'getswitchinterval', 'gettotalrefcount',
+ 'gettrace', 'hash_info', 'hexversion', 'implementation', 'int_info',
+ 'intern', 'maxsize', 'maxunicode', 'meta_path', 'modules', 'path',
+ 'path_hooks', 'path_importer_cache', 'platform', 'prefix', 'ps1',
+ 'setcheckinterval', 'setdlopenflags', 'setprofile', 'setrecursionlimit',
+ 'setswitchinterval', 'settrace', 'stderr', 'stdin', 'stdout',
+ 'thread_info', 'version', 'version_info', 'warnoptions']

注意点

自定义模块名不要和系统中要使用的模块名字一样

模块搜索顺序->当前目录->系统目录(sys.path)-> 程序报错

包是一种管理 Python 模块命名空间的形式,采用"点模块名称"。

比如一个模块的名称是 A.B, 那么他表示一个包 A中的子模块 B 。采用点模块名称这种形式也不用担心不同库之间的模块重名的情况

sound/                          顶层包
+      __init__.py               初始化 sound 包
+      formats/                  文件格式转换子包
+              __init__.py
+              wavread.py
+              wavwrite.py
+              aiffread.py
+              aiffwrite.py
+              auread.py
+              auwrite.py
+              ...
+      effects/                  声音效果子包
+              __init__.py
+              echo.py
+              surround.py
+              reverse.py
+              ...
+      filters/                  filters 子包
+              __init__.py
+              equalizer.py
+              vocoder.py
+              karaoke.py
+              ...
sound/                          顶层包
+      __init__.py               初始化 sound 包
+      formats/                  文件格式转换子包
+              __init__.py
+              wavread.py
+              wavwrite.py
+              aiffread.py
+              aiffwrite.py
+              auread.py
+              auwrite.py
+              ...
+      effects/                  声音效果子包
+              __init__.py
+              echo.py
+              surround.py
+              reverse.py
+              ...
+      filters/                  filters 子包
+              __init__.py
+              equalizer.py
+              vocoder.py
+              karaoke.py
+              ...

在导入一个包的时候,Python 会根据 sys.path 中的目录来寻找这个包中包含的子目录。

目录只有包含一个叫做 __init__.py 的文件才会被认作是一个包,主要是为了避免一些滥俗的名字(比如叫做 string)不小心的影响搜索路径中的有效模块。

最简单的情况,放一个空的 :file:__init__.py就可以了。当然这个文件中也可以包含一些初始化代码或者为(将在后面介绍的) __all__变量赋值。

用户可以每次只导入一个包里面的特定模块,比如:

import sound.effects.echo
import sound.effects.echo

这将会导入子模块:sound.effects.echo。 他必须使用全名去访问:

sound.effects.echo.echofilter(input, output, delay=0.7, atten=4)
sound.effects.echo.echofilter(input, output, delay=0.7, atten=4)

还有一种导入子模块的方法是:

from sound.effects import echo
from sound.effects import echo

这同样会导入子模块: echo,并且他不需要那些冗长的前缀,所以他可以这样使用:

echo.echofilter(input, output, delay=0.7, atten=4)
echo.echofilter(input, output, delay=0.7, atten=4)

还有一种变化就是直接导入一个函数或者变量:

from sound.effects.echo import echofilter
from sound.effects.echo import echofilter

同样的,这种方法会导入子模块: echo,并且可以直接使用他的 echofilter() 函数:

echofilter(input, output, delay=0.7, atten=4)
echofilter(input, output, delay=0.7, atten=4)

注意当使用 from package import item 这种形式的时候,对应的 item 既可以是包里面的子模块(子包),或者包里面定义的其他名称,比如函数,类或者变量。

import 语法会首先把 item 当作一个包定义的名称,如果没找到,再试图按照一个模块去导入。如果还没找到,抛出一个 :exc:ImportError 异常。

反之,如果使用形如 import item.subitem.subsubitem 这种导入形式,除了最后一项,都必须是包,而最后一项则可以是模块或者是包,但是不可以是类,函数或者变量的名字。

从一个包中导入*

from sound.effects import * : Python 会进入文件系统,找到这个包里面所有的子模块,然后一个一个的把它们都导入进来。

导入语句遵循如下规则:如果包定义文件 __init__.py 存在一个叫做 all 的列表变量,那么在使用 from package import * 的时候就把这个列表中的所有名字作为包内容导入。

以下实例在 file:sounds/effects/_init_.py 中包含如下代码:

__all__ = ["echo", "surround", "reverse"]
__all__ = ["echo", "surround", "reverse"]

这表示当你使用from sound.effects import *这种用法时,你只会导入包里面这三个子模块。

如果 __all__ 真的没有定义,那么使用**from sound.effects import ***这种语法的时候,就不会导入包 sound.effects 里的任何子模块。他只是把包sound.effects和它里面定义的所有内容导入进来(可能运行__init__.py里定义的初始化代码)。

这会把__init__.py里面定义的所有名字导入进来。并且他不会破坏掉我们在这句话之前导入的所有明确指定的模块。看下这部分代码:

import sound.effects.echo
+import sound.effects.surround
+from sound.effects import *
import sound.effects.echo
+import sound.effects.surround
+from sound.effects import *

这个例子中,在执行 from...import 前,包 sound.effects 中的 echo 和 surround 模块都被导入到当前的命名空间中了。(当然如果定义了 __all__ 就更没问题了)

文件操作

python的io操作相比java的IO流简单太多了,直接就是一个open()函数

open() 方法

Python open() 方法用于打开一个文件,并返回文件对象,在对文件进行处理过程都需要使用到这个函数,如果该文件无法被打开,会抛出 OSError。

**注意:**使用 open() 方法一定要保证关闭文件对象,即调用 close() 方法。

open() 函数常用形式是接收两个参数:文件名(file)和模式(mode)。

完整的语法格式为:

python
open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)

参数说明:

  • file: 必需,文件路径(相对或者绝对路径)。
  • mode: 可选,文件打开模式
  • buffering: 设置缓冲
  • encoding: 一般使用utf8
  • errors: 报错级别
  • newline: 区分换行符
  • closefd: 传入的file参数类型
  • opener: 设置自定义开启器,开启器的返回值必须是一个打开的文件描述符。
模式描述
t文本模式 (默认)。
x写模式,新建一个文件,如果该文件已存在则会报错。
b二进制模式。
+打开一个文件进行更新(可读可写)。
r以只读方式打开文件。文件的指针将会放在文件的开头。这是默认模式。
rb以二进制格式打开一个文件用于只读。文件指针将会放在文件的开头。这是默认模式。一般用于非文本文件如图片等。
r+打开一个文件用于读写。文件指针将会放在文件的开头。
rb+以二进制格式打开一个文件用于读写。文件指针将会放在文件的开头。一般用于非文本文件如图片等。
w打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。
wb以二进制格式打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。一般用于非文本文件如图片等。
w+打开一个文件用于读写。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。
wb+以二进制格式打开一个文件用于读写。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。一般用于非文本文件如图片等。
a打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。
ab以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。
a+打开一个文件用于读写。如果该文件已存在,文件指针将会放在文件的结尾。文件打开时会是追加模式。如果该文件不存在,创建新文件用于读写。
ab+以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。如果该文件不存在,创建新文件用于读写。

file 对象

file 对象使用 open 函数来创建,下表列出了 file 对象常用的函数:

方法及描述
file.close()关闭文件。关闭后文件不能再进行读写操作。
file.flush()刷新文件内部缓冲,直接把内部缓冲区的数据立刻写入文件, 而不是被动的等待输出缓冲区写入。
file.fileno()返回一个整型的文件描述符(file descriptor FD 整型), 可以用在如os模块的read方法等一些底层操作上。
file.isatty()如果文件连接到一个终端设备返回 True,否则返回 False。
[file.read(size])从文件读取指定的字节数,如果未给定或为负则读取所有。
[file.readline(size])读取整行,包括 "\n" 字符。
[file.readlines(sizeint])读取所有行并返回列表,若给定sizeint>0,返回总和大约为sizeint字节的行, 实际读取值可能比 sizeint 较大, 因为需要填充缓冲区。
[file.seek(offset, whence])移动文件读取指针到指定位置
file.tell()返回文件当前位置。
[file.truncate(size])从文件的首行首字符开始截断,截断文件为 size 个字符,无 size 表示从当前位置截断;截断之后后面的所有字符被删除,其中 windows 系统下的换行代表2个字符大小。
file.write(str)将字符串写入文件,返回的是写入的字符长度。
file.writelines(sequence)向文件写入一个序列字符串列表,如果需要换行则要自己加入每行的换行符。

文件和文件夹操作

python
import os #导入os模块
+
+#修改文件名
+os.rename(原文件名,新文件名)
+#删除文件
+os.remove(文件名)
+#创建文件夹
+os.mkdir(名称)
+#获取当前目录
+os.getcwd()
+#改变默认目录
+os.chdir(路径)
+#获取目录列表
+os.listdir(路径)
+#删除文件夹
+os.rmdir(路径)
import os #导入os模块
+
+#修改文件名
+os.rename(原文件名,新文件名)
+#删除文件
+os.remove(文件名)
+#创建文件夹
+os.mkdir(名称)
+#获取当前目录
+os.getcwd()
+#改变默认目录
+os.chdir(路径)
+#获取目录列表
+os.listdir(路径)
+#删除文件夹
+os.rmdir(路径)

面向对象

类的定义语法:

类名遵循大驼峰规则

python
"""
+新式类:直接或间接继承object,py3中所有类都是object的子类(same as java)
+"""
+class Demo(object):
+    pass
+"""
+旧式类:已过时
+"""
+class Demo1():
+    pass
+
+class Demo2:
+    pass
"""
+新式类:直接或间接继承object,py3中所有类都是object的子类(same as java)
+"""
+class Demo(object):
+    pass
+"""
+旧式类:已过时
+"""
+class Demo1():
+    pass
+
+class Demo2:
+    pass

类中定义方法

python
class Dog(Object):
+    def eat(self):  
+        print('吃')
class Dog(Object):
+    def eat(self):  
+        print('吃')

对象

创建对象语法:

Python
class Dog(Object):
+    def eat(self):  
+        print('吃')
+
+
+dog1 = Dog()
+dog1.eat()
+#res
+
class Dog(Object):
+    def eat(self):  
+        print('吃')
+
+
+dog1 = Dog()
+dog1.eat()
+#res
+

类外部添加和获取属性

给对象添加属性:对象.属性名 = 属性值

获取对象的属性变量 = 对象.属性名

修改:和添加一样,添加存在的属性就是修改

魔法方法

bash
python的类中,有一类方法,以`两个下划线开头``两个下划线结尾`,并在满足`某个特定条件下会自动调用`,这类方法,称为`魔法方法` magic method
python的类中,有一类方法,以`两个下划线开头``两个下划线结尾`,并在满足`某个特定条件下会自动调用`,这类方法,称为`魔法方法` magic method

__init__

在创建对象之后自动调用

作用:

  • 给对象添加属性,给对象属性一个初始值(构造方法)
  • 代码的业务需求,每创建一个对象,都需要执行的代码可以放在init方法中

注意点:

  • 如果__init__方法出现了self之外的形参,在创建对象的时候,需要给额外的形参传值类名(实参) 这个类似java中的构造方法的有参构造
python
class Dog(object):
+    def __init__(self,name):
+        self.name = name
+        print('init方法执行了')
+
+dog = Dog('大黄')
+print(dog.name)
+#res
+init方法执行了
+大黄
class Dog(object):
+    def __init__(self,name):
+        self.name = name
+        print('init方法执行了')
+
+dog = Dog('大黄')
+print(dog.name)
+#res
+init方法执行了
+大黄

__str__

类似java的toString:

  • 在print(对象)时会自动调用__str__方法,打印的结果是__str__方法的返回值
  • str(对象)将自定义类型转换为字符串的时候,会自动调用
  • 没有自定义__str__方法时,这个返回值是对象的地址

注意点:

  • 方法必须返回一个字符串,只有self一个参数

__del__

对象在内存当中被销毁的时候调用:

  1. 程序代码结束,程序运行过程中创建的对象和变量都会被删除
  2. 使用del 变量语句删除,将这个对象的引用计数变为0,会自动调用

引用计数:python内存管理的机制,指一块内存有多少变量在引用

  • 当一个变量引用一块内存时,引用计数+1
  • 删除一个变量或者这个变量不再引用这块内存,引用计数-1
  • 当内存的引用计数变为0,这块内存被删除,数据被销毁
补充:

Java中JVM为了避免对象间存在循环依赖导致对象无法被回收,JVM的垃圾回收算法采用的是可达性分析算法,通过gc roots对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到gc roots没有任何引用链相连时,则证明此对象是不可用的

类内部添加和获取属性

通过self操作:

self指的是当前实例(类似java中的this),作为类中方法的第一个形参,在通过对象调用方法的时候,不需要手动传参

python解释器会自动把调用方法的对象传递给self形参

self也可以改成其他的形参名,但一般不修改这个名字,默认为self

python
class Dog(object):
+    def play(self): 
+        print(f'{self.name}在玩耍')
+        
+dog = Dog()
+dog.name = '大黄'
+dog.play()
+
+#res
+大黄在玩耍
class Dog(object):
+    def play(self): 
+        print(f'{self.name}在玩耍')
+        
+dog = Dog()
+dog.name = '大黄'
+dog.play()
+
+#res
+大黄在玩耍

继承

python中继承的语法

  • 单继承
python
class Animal(object):
+    pass
+
+class Dog(Animal):
+    pass
class Animal(object):
+    pass
+
+class Dog(Animal):
+    pass
  • 多继承:python中允许多继承,java中是没有多继承的
python
class (object):
+    pass
+
+class (object):
+    pass
+
+class 骡子(,):
+    pass
class (object):
+    pass
+
+class (object):
+    pass
+
+class 骡子(,):
+    pass

需要注意:

多继承中圆括号中父类的顺序,若是父类中有相同的方法名,而在子类使用时未指定,python从左至右搜索 即方法在子类中未找到时,从左到右查找父类中是否包含方法。

子类重写父类方法

和java一样,子类重写父类中的同名方法,通过子类独对象调用方法时调用的是子类自己的方法

子类调用父类方法

java中调用父类就用super,python有以下几种方式

  • 父类名.方法名(self,其他参数)

  • super(类A,self).方法名(参数),会调用类A的父类中的方法

  • super().方法名(参数)=>super(当前类,self).方法名(参数) 是第二中的简写,调用当前类的父类

继承中的init方法

子类重写父类的init方法:在子类的init方法需要调用父类的init方法(和java也一样),给对象添加从父类继承的属性

注意:子类init方法的形参,一般先写父类的形参,再写自己独有的形参

python
class Dog(object):
+    def __init__(self,name):
+        self.name = name
+        self.age = 1
+       
+    def __str__(self):
+        return f'名字为{self.name},年龄为{self.age}'
+
+class MyDog(Dog):
+    def __init__(self,name,color):
+        super().__init__(name)
+        self.color = color
+        
+    def __str__(self):
+        return f'名字为{self.name},年龄为{self.age},颜色为{self.color}'
+    
+    
+dog = MyDog('大黄','黄色')
+print(dog)
+#res
+名字为大黄,年龄为1,颜色为黄色
class Dog(object):
+    def __init__(self,name):
+        self.name = name
+        self.age = 1
+       
+    def __str__(self):
+        return f'名字为{self.name},年龄为{self.age}'
+
+class MyDog(Dog):
+    def __init__(self,name,color):
+        super().__init__(name)
+        self.color = color
+        
+    def __str__(self):
+        return f'名字为{self.name},年龄为{self.age},颜色为{self.color}'
+    
+    
+dog = MyDog('大黄','黄色')
+print(dog)
+#res
+名字为大黄,年龄为1,颜色为黄色

封装

封装的意义:

  • 将属性和方法放在一起作为一个整体,通过实例化对象来进行操作
  • 隐藏内部实现
  • 对类的属性和方法增加访问权限控制

私有权限

python没有java中的权限修饰符public/private之类的,私有的属性或者方法都由两个下划线开头

  • 普通的属性前面加两个下划线就是私有属性

  • 方法名前面加两个下划线就是私有方法

和java一样私有属性不能被继承,私有方法不能在类外部访问,可以提供共有方法访问私有属性或私有方法

类属性

类似java中的静态变量

访问:类名.类属性

修改:类名.类属性 = 属性值

类方法

类方法:使用@classmethod装饰的方法称为类方法,第一个参数是cls,代表类对象自己

注意:

  1. 如果在方法中使用了实例属性,那么该方法必须是实例方法,不能为类方法

何时定义类方法:

  • 不需要使用实例属性,需要使用类属性

调用:

  • 对象.类方法
  • 类名.类方法

静态方法

使用@staticmethod装饰的方法称为静态方法,对参数没有特殊要求,可以有,可以没有

何时定义:

  • 不需要使用实例属性,也不需要使用类属性,可以定义方法为静态方法

调用:

  • 对象.静态方法
  • 类名.静态方法

多态

由于python不需要声明变量类型,因此多态体现的不是那么直观,思想和java一样,可以使用父类的地方,也可以使用子类,使用多态的意义在于提高应用的扩展性

异常

组成:

  • 异常的类型
  • 异常的描述

捕获单个异常

python
try:
+    statement1
+except 异常名:
+    statement2
try:
+    statement1
+except 异常名:
+    statement2

捕获多个异常

python
try:
+    statement1
+except (异常1,异常2,...):
+    statement2
+    
+try:
+    statement1
+except 异常1:
+    statement2
+except 异常2:
+    statement3
try:
+    statement1
+except (异常1,异常2,...):
+    statement2
+    
+try:
+    statement1
+except 异常1:
+    statement2
+except 异常2:
+    statement3

打印异常信息

python
try:
+    statement1
+except (异常1,异常2,...) as 变量名:
+    print(变量名)
try:
+    statement1
+except (异常1,异常2,...) as 变量名:
+    print(变量名)

捕获所有异常

python
try:
+    statement1
+except: #缺点 不能获取异常信息
+    statement2
+    
+try:
+    statement1
+except Exception as 变量名: 
+    print(变量名)
try:
+    statement1
+except: #缺点 不能获取异常信息
+    statement2
+    
+try:
+    statement1
+except Exception as 变量名: 
+    print(变量名)

异常的完整结构

python
try:
+    statement1
+except Exception as e:
+    print(e)
+else:
+    代码没有发生异常会执行的代码块
+finally:
+    不管有没有异常都会执行的代码块
try:
+    statement1
+except Exception as e:
+    print(e)
+else:
+    代码没有发生异常会执行的代码块
+finally:
+    不管有没有异常都会执行的代码块

抛出自定义异常

python
raise 异常对象
+
+
+
+异常对象 = 异常类(参数)
+
+
+抛出自定义异常:
+    1.自定义异常类,继承Exception或者BaseException
+    2.选择性定义__init__方法,__str__方法
+    3.抛出
raise 异常对象
+
+
+
+异常对象 = 异常类(参数)
+
+
+抛出自定义异常:
+    1.自定义异常类,继承Exception或者BaseException
+    2.选择性定义__init__方法,__str__方法
+    3.抛出
+ + + + \ No newline at end of file diff --git "a/python/base/python\345\271\266\345\217\221\347\274\226\347\250\213.html" "b/python/base/python\345\271\266\345\217\221\347\274\226\347\250\213.html" new file mode 100644 index 000000000..8594d5305 --- /dev/null +++ "b/python/base/python\345\271\266\345\217\221\347\274\226\347\250\213.html" @@ -0,0 +1,1047 @@ + + + + + + python并发编程 | 故事 + + + + + + + + + + + + + + +
Skip to content

python并发编程

python对并发编程的支持

  • 多线程:threading,利用CPU运算和IO可以同时执行,让CPU不会干巴巴等待IO完成

  • 多进程:multiprocessing,利用多核CPU的能力,真正的并行执行任务

  • 异步IO:asyncio,在单线程中利用CPU和IO同时执行的原理,实现函数异步执行

  • 使用Lock对资源加锁,防止冲突访问

  • 使用Queue实现不同线程、进程之间的数据通信,实现生产者消费者模式

  • 使用线程池Pool、进程池Pool,简化线程、进程任务的提交、等待结束、获取结果

  • 使用subprocess启动外部程序的进程,并进行输出交互

如何选择多线程/多进程/多协程

什么是CPU密集型、IO密集型

CPU密集型计算:也叫计算密集型,指I/O很短时间内就完成,CPU需要大量计算和处理,特点是CPU占用率很高,例如解压缩,加密解密,正则匹配等。

I/O密集型计算:硬盘、内存、网络的读写操作,例如文件处理、网络爬虫、读写数据库等。

对比

多进程Process(multiprocessing):

  • 优点:可以利用多核CPU进行并行运算
  • 缺点:占用资源多,可启动数目少
  • 适用于:计算密集型任务,例如解压缩、加解密。。

多线程Thread(threading):

  • 一个进程中可以启动多个线程

  • 相比进程更轻量级,占用资源更少。但只能单CPU并发执行,不能利用多CPU(GIL,全局解释器锁)

  • 相比协程,线程启动数目有限制,占用内存资源,有线程切换的开销

  • 适用于:IO密集型任务,同时运行的任务数目要求不高

多协程Coroutine(asyncio):

  • 一个线程中可以启动多个协程

  • 优点:内存开销最小,启动数量最多

  • 缺点:支持的库有限(aiohttp vs requests),代码实现较为复杂

  • 适用于:I/O密集型任务,需要超多任务运行且有现有库支持的场景

多线程

多线程的两种实现方式

通过threading模块的Thread类

python
import time
+import threading
+
+
+import requests
+import threading
+import time
+
+urls = [f'https://www.cnblogs.com/#p{i}' for i in range(1, 50)]
+
+
+def craw(url):
+    r = requests.get(url)
+    time.sleep(1)
+    print(url, len(r.text))
+
+
+if __name__ == '__main__':
+    start = time.time()
+    t = threading.Thread(target=craw, args=(urls[0],))
+    t.start()
+    t.join()
+    print(f'cost {start - time.time()}s')
import time
+import threading
+
+
+import requests
+import threading
+import time
+
+urls = [f'https://www.cnblogs.com/#p{i}' for i in range(1, 50)]
+
+
+def craw(url):
+    r = requests.get(url)
+    time.sleep(1)
+    print(url, len(r.text))
+
+
+if __name__ == '__main__':
+    start = time.time()
+    t = threading.Thread(target=craw, args=(urls[0],))
+    t.start()
+    t.join()
+    print(f'cost {start - time.time()}s')

通过继承Thread类

python
import requests
+import time
+import threading
+from blog_spider import urls
+
+
+class MyThread(threading.Thread):
+    def __init__(self, url):
+        super().__init__()
+        self.url = url
+
+    def run(self):
+        r = requests.get(self.url)
+        time.sleep(1)
+        print(self.url, len(r.text))
+
+
+if __name__ == '__main__':
+    start = time.time()
+    t = MyThread(urls[0])
+    t.start()
+    t.join()
+    print(time.time() - start)
import requests
+import time
+import threading
+from blog_spider import urls
+
+
+class MyThread(threading.Thread):
+    def __init__(self, url):
+        super().__init__()
+        self.url = url
+
+    def run(self):
+        r = requests.get(self.url)
+        time.sleep(1)
+        print(self.url, len(r.text))
+
+
+if __name__ == '__main__':
+    start = time.time()
+    t = MyThread(urls[0])
+    t.start()
+    t.join()
+    print(time.time() - start)

线程同步

使用 Thread 对象的 Lock 实现

python
import threading
+import time
+
+
+class MyThread(threading.Thread):
+    def __init__(self, thread_id, name, counter):
+        super().__init__(name=name)
+        self.name = name
+        self.thread_id = thread_id
+        self.counter = counter
+
+    def run(self):
+        print("开启线程: " + self.name)
+        # 获取锁.用于线程同步
+        my_lock.acquire()
+        print_time(self.name, self.counter, 3)
+        # 释放锁
+        my_lock.release()
+
+
+def print_time(thread_name, delay, counter):
+    while counter:
+        time.sleep(delay)
+        print(f"#{thread_name}: {time.ctime(time.time())}")
+        counter -= 1
+
+
+my_lock = threading.Lock()
+threads = []
+# 创建新线程
+thread1 = MyThread(1, "Thread-1", 1)
+thread2 = MyThread(2, "Thread-2", 2)
+thread1.start()
+thread2.start()
+# 添加到线程列表
+threads.append(thread1)
+threads.append(thread2)
+
+for t in threads:
+    t.join()
+print("退出主线程")
import threading
+import time
+
+
+class MyThread(threading.Thread):
+    def __init__(self, thread_id, name, counter):
+        super().__init__(name=name)
+        self.name = name
+        self.thread_id = thread_id
+        self.counter = counter
+
+    def run(self):
+        print("开启线程: " + self.name)
+        # 获取锁.用于线程同步
+        my_lock.acquire()
+        print_time(self.name, self.counter, 3)
+        # 释放锁
+        my_lock.release()
+
+
+def print_time(thread_name, delay, counter):
+    while counter:
+        time.sleep(delay)
+        print(f"#{thread_name}: {time.ctime(time.time())}")
+        counter -= 1
+
+
+my_lock = threading.Lock()
+threads = []
+# 创建新线程
+thread1 = MyThread(1, "Thread-1", 1)
+thread2 = MyThread(2, "Thread-2", 2)
+thread1.start()
+thread2.start()
+# 添加到线程列表
+threads.append(thread1)
+threads.append(thread2)
+
+for t in threads:
+    t.join()
+print("退出主线程")

结果:

bash
开启线程: Thread-1
+开启线程: Thread-2
+#Thread-1: Mon Apr 12 21:54:42 2021
+#Thread-1: Mon Apr 12 21:54:43 2021
+#Thread-1: Mon Apr 12 21:54:44 2021
+#Thread-2: Mon Apr 12 21:54:46 2021
+#Thread-2: Mon Apr 12 21:54:48 2021
+#Thread-2: Mon Apr 12 21:54:50 2021
+退出主线程
开启线程: Thread-1
+开启线程: Thread-2
+#Thread-1: Mon Apr 12 21:54:42 2021
+#Thread-1: Mon Apr 12 21:54:43 2021
+#Thread-1: Mon Apr 12 21:54:44 2021
+#Thread-2: Mon Apr 12 21:54:46 2021
+#Thread-2: Mon Apr 12 21:54:48 2021
+#Thread-2: Mon Apr 12 21:54:50 2021
+退出主线程

Lock的使用方式

1.try-finally模式

python
import threading 
+
+lock = threading.lock()
+lock.acquire()
+try:
+    #do something
+finally:
+    lock.release()
import threading 
+
+lock = threading.lock()
+lock.acquire()
+try:
+    #do something
+finally:
+    lock.release()
  1. with模式
python
import threading
+
+lock = threading.lock()
+
+with lock:
+    # do something
import threading
+
+lock = threading.lock()
+
+with lock:
+    # do something

生产者消费者模型

python
import requests
+from bs4 import BeautifulSoup
+
+urls = [f'https://www.cnblogs.com/#p{i}' for i in range(1, 50)]
+
+
+def craw(url):
+    r = requests.get(url)
+    return r.text
+
+
+def parse(html):
+    soup = BeautifulSoup(html, 'html.parser')
+    links = soup.find_all('a', class_='post-item-title')
+    return [(link.get('href'), link.get_text()) for link in links]
import requests
+from bs4 import BeautifulSoup
+
+urls = [f'https://www.cnblogs.com/#p{i}' for i in range(1, 50)]
+
+
+def craw(url):
+    r = requests.get(url)
+    return r.text
+
+
+def parse(html):
+    soup = BeautifulSoup(html, 'html.parser')
+    links = soup.find_all('a', class_='post-item-title')
+    return [(link.get('href'), link.get_text()) for link in links]
python
import queue
+import threading
+import time
+import random
+
+import blog_spider
+
+
+def do_crawl(url_queue: queue.Queue, html_queue: queue.Queue):
+    while True:
+        url = url_queue.get()
+        html = blog_spider.craw(url)
+        html_queue.put(html)
+
+
+def do_parse(html_queue: queue.Queue, fout):
+    while True:
+        html = html_queue.get()
+        results = blog_spider.parse(html)
+        for result in results:
+            fout.write(str(result) + '\n')
+        time.sleep(1)
+
+
+if __name__ == '__main__':
+    url_queue = queue.Queue()
+    html_queue = queue.Queue()
+    for url in blog_spider.urls:
+        url_queue.put(url)
+
+    for i in range(3):
+        t = threading.Thread(target=do_crawl, args=(url_queue, html_queue), name='crawl-{}'.format(i))
+        t.start()
+
+    fout = open('results.txt', 'w')
+    for i in range(3):
+        t = threading.Thread(target=do_parse, args=(html_queue, fout), name='parse-{}'.format(i))
+        t.start()
import queue
+import threading
+import time
+import random
+
+import blog_spider
+
+
+def do_crawl(url_queue: queue.Queue, html_queue: queue.Queue):
+    while True:
+        url = url_queue.get()
+        html = blog_spider.craw(url)
+        html_queue.put(html)
+
+
+def do_parse(html_queue: queue.Queue, fout):
+    while True:
+        html = html_queue.get()
+        results = blog_spider.parse(html)
+        for result in results:
+            fout.write(str(result) + '\n')
+        time.sleep(1)
+
+
+if __name__ == '__main__':
+    url_queue = queue.Queue()
+    html_queue = queue.Queue()
+    for url in blog_spider.urls:
+        url_queue.put(url)
+
+    for i in range(3):
+        t = threading.Thread(target=do_crawl, args=(url_queue, html_queue), name='crawl-{}'.format(i))
+        t.start()
+
+    fout = open('results.txt', 'w')
+    for i in range(3):
+        t = threading.Thread(target=do_parse, args=(html_queue, fout), name='parse-{}'.format(i))
+        t.start()

线程优先级队列实现

python
import queue
+import threading
+import time
+
+exitFlag = 0
+
+
+class MyThread(threading.Thread):
+    def __init__(self, thread_id, name, q):
+        super().__init__(name=name)
+        self.threadId = thread_id
+        self.name = name
+        self.q = q
+
+    def run(self):
+        print("开启线程: " + self.name)
+        process_data(self.name, self.q)
+        print("退出线程: " + self.name)
+
+
+def process_data(name, q):
+    while not exitFlag:
+        queueLock.acquire()
+        if not workQueue.empty():
+            data = q.get()
+            queueLock.release()
+            print(f"{name} processing {data}")
+        else:
+            queueLock.release()
+        time.sleep(1)
+
+
+threadList = ["Thread-1", "Thread-2", "Thread-3"]
+queueLock = threading.Lock()
+nameList = ["ONE", "TWO", "THREE", "FOUR", "FIVE"]
+workQueue = queue.Queue(10)
+threads = []
+threadId = 1
+
+for tname in threadList:
+    thread = MyThread(threadId, tname, workQueue)
+    thread.start()
+    threads.append(thread)
+    threadId += 1
+
+queueLock.acquire()
+for name in nameList:
+    workQueue.put(name)
+queueLock.release()
+
+while not workQueue.empty():
+    pass
+
+exitFlag = 1
+
+for t in threads:
+    t.join()
+
+print("主线程退出")
import queue
+import threading
+import time
+
+exitFlag = 0
+
+
+class MyThread(threading.Thread):
+    def __init__(self, thread_id, name, q):
+        super().__init__(name=name)
+        self.threadId = thread_id
+        self.name = name
+        self.q = q
+
+    def run(self):
+        print("开启线程: " + self.name)
+        process_data(self.name, self.q)
+        print("退出线程: " + self.name)
+
+
+def process_data(name, q):
+    while not exitFlag:
+        queueLock.acquire()
+        if not workQueue.empty():
+            data = q.get()
+            queueLock.release()
+            print(f"{name} processing {data}")
+        else:
+            queueLock.release()
+        time.sleep(1)
+
+
+threadList = ["Thread-1", "Thread-2", "Thread-3"]
+queueLock = threading.Lock()
+nameList = ["ONE", "TWO", "THREE", "FOUR", "FIVE"]
+workQueue = queue.Queue(10)
+threads = []
+threadId = 1
+
+for tname in threadList:
+    thread = MyThread(threadId, tname, workQueue)
+    thread.start()
+    threads.append(thread)
+    threadId += 1
+
+queueLock.acquire()
+for name in nameList:
+    workQueue.put(name)
+queueLock.release()
+
+while not workQueue.empty():
+    pass
+
+exitFlag = 1
+
+for t in threads:
+    t.join()
+
+print("主线程退出")

运行结果:

bash
开启线程: Thread-1
+开启线程: Thread-2
+开启线程: Thread-3
+Thread-1 processing ONE
+Thread-2 processing TWO
+Thread-3 processing THREE
+Thread-2 processing FOUR
+Thread-1 processing FIVE
+退出线程: Thread-2
+退出线程: Thread-1
+退出线程: Thread-3
+主线程退出
开启线程: Thread-1
+开启线程: Thread-2
+开启线程: Thread-3
+Thread-1 processing ONE
+Thread-2 processing TWO
+Thread-3 processing THREE
+Thread-2 processing FOUR
+Thread-1 processing FIVE
+退出线程: Thread-2
+退出线程: Thread-1
+退出线程: Thread-3
+主线程退出

多线程线程池ThreadPoolExecutor

python
from concurrent.futures import ThreadPoolExecutor, as_completed
+import time
+
+
+def get_data(times):
+    time.sleep(times)
+    print("get data {} success".format(times))
+
+
+thread_pool = ThreadPoolExecutor(max_workers=2)
+task1 = thread_pool.submit(get_data, 3)
+task2 = thread_pool.submit(get_data, 2)
+
+datas = [1, 2, 3]
+# submit后直接返回
+all_tasks = [thread_pool.submit(get_data, data) for data in datas]
+# as_complete底层是生成器
+# for future in as_completed(all_tasks):
+#    res = future.result()
+#    print(res)
+for data in thread_pool.map(get_data, datas):
+    print("get {} data ".format(data))
from concurrent.futures import ThreadPoolExecutor, as_completed
+import time
+
+
+def get_data(times):
+    time.sleep(times)
+    print("get data {} success".format(times))
+
+
+thread_pool = ThreadPoolExecutor(max_workers=2)
+task1 = thread_pool.submit(get_data, 3)
+task2 = thread_pool.submit(get_data, 2)
+
+datas = [1, 2, 3]
+# submit后直接返回
+all_tasks = [thread_pool.submit(get_data, data) for data in datas]
+# as_complete底层是生成器
+# for future in as_completed(all_tasks):
+#    res = future.result()
+#    print(res)
+for data in thread_pool.map(get_data, datas):
+    print("get {} data ".format(data))

ThreadPoolExecutor提交任务的两种方式

  • pool.map(func, params):func为处理函数,params为所有待处理的数据,返回值为按顺序返回。这种方式适合任务数据全部准备好一次提交处理的场景
  • future = pool.submit(func,param):func为处理函数,param为待处理的一条数据,返回值为future。这种方式适合一条条数据提交处理的场景。处理多个future集合futures时,可以直接遍历,也可以配合as_complete使用,这种方式是按任务完成顺序返回。

多进程

对于io操作来说,使用多线程

对于耗cpu的操作,用多进程

  • 进程的切换代价高于多线程
python
from concurrent.futures import ProcessPoolExecutor
+import multiprocessing
+import time
+
+
+# 多进程编程
+def get_html(n):
+    time.sleep(n)
+    return n
+
+
+if __name__ == '__main__':
+    # progress = multiprocessing.Process(target=get_html, args=(2,))
+    # print(progress.pid)
+    # progress.start()
+    # print(progress.pid)
+    # progress.join()
+    # print('main progress end')
+
+    # 使用进程池
+    pool = multiprocessing.Pool(multiprocessing.cpu_count())
+    # res = pool.apply_async(get_html, args=(3,))
+    # 不再接受任务
+    # pool.close()
+    # 等待所有任务完成
+    # pool.join()
+    # print(res)
+    # print(res.get())
+
+    # imap 按顺序
+    # for res in pool.imap(get_html, [1, 5, 3]):
+    #    print("{} sleep success".format(res))
+    # imap_unordered 按完成时间
+    for res in pool.imap_unordered(get_html, [1, 5, 3]):
+        print("{} sleep success".format(res))
from concurrent.futures import ProcessPoolExecutor
+import multiprocessing
+import time
+
+
+# 多进程编程
+def get_html(n):
+    time.sleep(n)
+    return n
+
+
+if __name__ == '__main__':
+    # progress = multiprocessing.Process(target=get_html, args=(2,))
+    # print(progress.pid)
+    # progress.start()
+    # print(progress.pid)
+    # progress.join()
+    # print('main progress end')
+
+    # 使用进程池
+    pool = multiprocessing.Pool(multiprocessing.cpu_count())
+    # res = pool.apply_async(get_html, args=(3,))
+    # 不再接受任务
+    # pool.close()
+    # 等待所有任务完成
+    # pool.join()
+    # print(res)
+    # print(res.get())
+
+    # imap 按顺序
+    # for res in pool.imap(get_html, [1, 5, 3]):
+    #    print("{} sleep success".format(res))
+    # imap_unordered 按完成时间
+    for res in pool.imap_unordered(get_html, [1, 5, 3]):
+        print("{} sleep success".format(res))

进程间通信

  • 使用multiprocessing中的Queue 用法和threading的Queue类似
  • 全局共享变量不适用与进程间通信(进程间的数据是隔离的)
  • multiprocessing中的Queue不能用于进程池pool中的进程通信
  • pool中的进程间通信需要使用multiprocessing中的Manager实例化后的queue(Manager().Queue())
  • 使用Pipe管道实现进程间通信 receive,send = Pipe() 只能适用于两个进程间通信
  • Manager().dict()等数据结构进行进程间通信

image-20220511013302618

协程

协程,又称微线程,纤程。英文名Coroutine。是一种用户态的上下文切换技术。协程的作用是在执行函数A时可以随时中断去执行函数B,然后中断函数B继续执行函数A(可以自由切换)。但这一过程并不是函数调用,这一整个过程看似像多线程,然而协程只有一个线程执行。

协程的优势

  • 效率极高,因为子程序切换不是线程切换,由程序自身控制,没有切换线程的开销,所以与多线程相比,线程的数量越多,协程的性能优势越明显。
  • 不需要多线程的同步机制,因为只有一个线程,也不存在同时写变量的线程安全问题,在控制共享资源时也不需要加锁,因此执行效率高很多。

协程可以处理IO密集型程序的效率问题,但是CPU密集型不是它的长处,要充分发挥CPU的利用率可以结合多进程+协程

实现协程的方式:

  • yield关键字
  • asyncio装饰器
  • async、await关键字(推荐)

事件循环

asyncio模块中,每一个进程都有一个事件循环。把一些函数注册到事件循环上,当满足事件发生的时候,调用相应的协程函数

事件循环的作用是管理所有的事件,在整个程序运行过程中不断循环执行,追踪事件发生的顺序将它们放到队列中,当主线程空闲的时候,调用相应的事件处理者处理事件。

伪代码:

python
任务列表 = [任务1,任务2,任务3...]
+
+while true:
+    可执行的任务列表,已完成的任务列表 = 检查所有任务,将可执行的和已完成的任务返回
+    for 就绪任务 in 可执行的任务:
+        执行就绪任务
+        
+    for 已完成的任务 in 已完成的任务:
+        剔除已完成的任务
+        
+    如果任务列表的全部任务都已完成,终止循环
任务列表 = [任务1,任务2,任务3...]
+
+while true:
+    可执行的任务列表,已完成的任务列表 = 检查所有任务,将可执行的和已完成的任务返回
+    for 就绪任务 in 可执行的任务:
+        执行就绪任务
+        
+    for 已完成的任务 in 已完成的任务:
+        剔除已完成的任务
+        
+    如果任务列表的全部任务都已完成,终止循环
python
import asyncio
+
+
+# 生成或获取一个事件循环
+loop = asyncio.get_event_loop()
+# 将任务放到任务列表
+loop.run_until_complete(任务)
import asyncio
+
+
+# 生成或获取一个事件循环
+loop = asyncio.get_event_loop()
+# 将任务放到任务列表
+loop.run_until_complete(任务)

image-20220511014404071

协程函数

定义函数时,如果是async def 函数的函数,就是一个协程函数

协程对象

执行协程函数得到的对象

TIP

执行协程函数创建协程对象,函数内部代码不会立即执行

如果想运行协程函数内部代码,必须将协程对象交给事件循环处理

python
import asyncio
+
+
+# 定义一个协程函数
+async def func():
+    print("异步编程")
+
+
+# 生成一个事件循环
+loop = asyncio.get_event_loop()
+# 得到协程对象
+res = func()
+# 将协程对象交给事件循环
+loop.run_until_complete(res)
+# asyncio.run(res)
+
+res:
+异步编程
import asyncio
+
+
+# 定义一个协程函数
+async def func():
+    print("异步编程")
+
+
+# 生成一个事件循环
+loop = asyncio.get_event_loop()
+# 得到协程对象
+res = func()
+# 将协程对象交给事件循环
+loop.run_until_complete(res)
+# asyncio.run(res)
+
+res:
+异步编程

如果不把协程对象放入事件循环

python
import asyncio
+
+
+# 定义一个协程函数
+async def func():
+    print("异步编程")
+
+
+# 生成一个事件循环
+loop = asyncio.get_event_loop()
+# 得到协程对象
+res = func()
+
+res:
+sys:1: RuntimeWarning: coroutine 'func' was never awaited
import asyncio
+
+
+# 定义一个协程函数
+async def func():
+    print("异步编程")
+
+
+# 生成一个事件循环
+loop = asyncio.get_event_loop()
+# 得到协程对象
+res = func()
+
+res:
+sys:1: RuntimeWarning: coroutine 'func' was never awaited

异步IO

python
import asyncio
+# 获取事件循环
+loop = asyncio.get_event_loop()
+
+# 定义协程函数
+async def hello(count):
+    print(f"Hello World! {count}")
+    await asyncio.sleep(1)
+
+# 创建task列表
+tasks = [loop.create_task(hello(count)) for count in range(10)]
+# 执行事件列表
+loop.run_until_complete(asyncio.wait(tasks))
import asyncio
+# 获取事件循环
+loop = asyncio.get_event_loop()
+
+# 定义协程函数
+async def hello(count):
+    print(f"Hello World! {count}")
+    await asyncio.sleep(1)
+
+# 创建task列表
+tasks = [loop.create_task(hello(count)) for count in range(10)]
+# 执行事件列表
+loop.run_until_complete(asyncio.wait(tasks))

异步IO爬虫

python
import asyncio
+import aiohttp
+import blog_spider
+import time
+
+async def async_craw(url):
+    print('开始爬取:', url)
+    async with aiohttp.ClientSession() as session:
+        async with session.get(url) as response:
+            result = await response.text()
+            print('爬取结束:', url, len(result))
+
+
+loop = asyncio.get_event_loop()
+
+tasks = [loop.create_task(async_craw(url)) for url in blog_spider.urls]
+
+start = time.time()
+loop.run_until_complete(asyncio.wait(tasks))
+print('耗时:', time.time() - start)
+
+---
import asyncio
+import aiohttp
+import blog_spider
+import time
+
+async def async_craw(url):
+    print('开始爬取:', url)
+    async with aiohttp.ClientSession() as session:
+        async with session.get(url) as response:
+            result = await response.text()
+            print('爬取结束:', url, len(result))
+
+
+loop = asyncio.get_event_loop()
+
+tasks = [loop.create_task(async_craw(url)) for url in blog_spider.urls]
+
+start = time.time()
+loop.run_until_complete(asyncio.wait(tasks))
+print('耗时:', time.time() - start)
+
+---

使用信号量控制异步爬虫并发度

python
sem = asyncio.Semaphore(10)
+
+async with sem:
+    # work with shared resource
+-----------------------------------
+
+sem = asyncio.Semaphore(10)
+
+await sem.acquire()
+try:
+    # work with shared resource
+finally:
+    sem.release()
sem = asyncio.Semaphore(10)
+
+async with sem:
+    # work with shared resource
+-----------------------------------
+
+sem = asyncio.Semaphore(10)
+
+await sem.acquire()
+try:
+    # work with shared resource
+finally:
+    sem.release()
python
import asyncio
+import aiohttp
+import blog_spider
+
+sem = asyncio.Semaphore(10)
+
+async def async_craw(url):
+    print('开始爬取:', url)
+    async with sem:
+        async with aiohttp.ClientSession() as session:
+            async with session.get(url) as response:
+                result = await response.text()
+                print('爬取结束:', url, len(result))
+
+
+loop = asyncio.get_event_loop()
+
+tasks = [loop.create_task(async_craw(url)) for url in blog_spider.urls]
+import time
+start = time.time()
+loop.run_until_complete(asyncio.wait(tasks))
+print('耗时:', time.time() - start)
import asyncio
+import aiohttp
+import blog_spider
+
+sem = asyncio.Semaphore(10)
+
+async def async_craw(url):
+    print('开始爬取:', url)
+    async with sem:
+        async with aiohttp.ClientSession() as session:
+            async with session.get(url) as response:
+                result = await response.text()
+                print('爬取结束:', url, len(result))
+
+
+loop = asyncio.get_event_loop()
+
+tasks = [loop.create_task(async_craw(url)) for url in blog_spider.urls]
+import time
+start = time.time()
+loop.run_until_complete(asyncio.wait(tasks))
+print('耗时:', time.time() - start)

python3.7后的新语法

使用asyncio.run()代替原来创建事件循环,使用事件循环执行函数的操作

python
import asyncio
+import blog_spider
+
+# async def 定义协程函数
+async def async_craw(url):
+    print('开始爬取:', url)
+    # 触发io操作,调用其他协程
+    await asyncio.sleep(0)
+    print('爬取完成:', url)
+
+
+async def main():
+    # 创建协程列表
+    tasks = [async_craw(url) for url in blog_spider.urls]
+    # asyncio.gather(*task)表示协同执行tasks列表里的所有协程
+    await asyncio.gather(*tasks)
+#
+asyncio.run(main())
import asyncio
+import blog_spider
+
+# async def 定义协程函数
+async def async_craw(url):
+    print('开始爬取:', url)
+    # 触发io操作,调用其他协程
+    await asyncio.sleep(0)
+    print('爬取完成:', url)
+
+
+async def main():
+    # 创建协程列表
+    tasks = [async_craw(url) for url in blog_spider.urls]
+    # asyncio.gather(*task)表示协同执行tasks列表里的所有协程
+    await asyncio.gather(*tasks)
+#
+asyncio.run(main())

asyncio.wait和asyncio.gather异同

  • 相同:从功能上看,asyncio.waitasyncio.gather 实现的效果是相同的,都是把所有 Task 任务结果收集起来。
  • 不同asyncio.wait 会返回两个值:donependingdone 为已完成的协程 Taskpending 为超时未完成的协程 Task,需通过 future.result 调用 Taskresult;而asyncio.gather 返回的是所有已完成 Taskresult,不需要再进行调用或其他操作,就可以得到全部结果。

await关键字

await + 可等待的对象(协程对象、Future对象、Task对象 -> io等待)

python
import asyncio
+
+
+async def func():
+    print('异步编程')
+    response = await asyncio.sleep(2)
+    print("结束",response)
+    
+asyncio.run(func())
import asyncio
+
+
+async def func():
+    print('异步编程')
+    response = await asyncio.sleep(2)
+    print("结束",response)
+    
+asyncio.run(func())

示例:

python
import asyncio
+
+
+async def others():
+    print('start')
+    await asyncio.sleep(2)
+    print('end')
+    return '返回值'
+
+
+async def func():
+    print('执行协程函数内部代码')
+    # 遇到IO操作挂起当前协程,等到IO完成后继续运行,当前协程挂起时,事件循环可以执行其他协程
+    response = await others()
+    print(f'IO的结果是:{response} ')
+
+asyncio.run(func())
+
+res:
+执行协程函数内部代码
+start
+end
+IO的结果是:返回值
import asyncio
+
+
+async def others():
+    print('start')
+    await asyncio.sleep(2)
+    print('end')
+    return '返回值'
+
+
+async def func():
+    print('执行协程函数内部代码')
+    # 遇到IO操作挂起当前协程,等到IO完成后继续运行,当前协程挂起时,事件循环可以执行其他协程
+    response = await others()
+    print(f'IO的结果是:{response} ')
+
+asyncio.run(func())
+
+res:
+执行协程函数内部代码
+start
+end
+IO的结果是:返回值

Task对象

Tasks用于并发调度协程,是对协程对象的一种封装,其中包含了任务的各个状态。通过asyncio.create_task()函数创建Task对象,这样可以让协程加入事件循环中等待调度执行。还可以使用低层级的loop.create_task()asyncio.ensure_future()函数。不建议手动实例化Task对象。

示例1:

python
import asyncio
+
+
+async def func():
+    print(1)
+    await asyncio.sleep(2)
+    print(2)
+    return '返回值'
+
+
+async def main():
+    print('main函数开始')
+
+    # 创建task对象,将当前执行func函数的任务添加到事件循环
+    task1 = asyncio.create_task(func())
+
+    task2 = asyncio.create_task(func())
+
+    print('main函数结束')
+
+    # 当执行某协程遇到IO操作,会自动切换执行其他任务
+    res1 = await task1
+    res2 = await task2
+    print(res1, res2)
+
+
+asyncio.run(main())
+
+res:
+main函数开始
+main函数结束
+1
+1
+2
+2
+返回值 返回值
import asyncio
+
+
+async def func():
+    print(1)
+    await asyncio.sleep(2)
+    print(2)
+    return '返回值'
+
+
+async def main():
+    print('main函数开始')
+
+    # 创建task对象,将当前执行func函数的任务添加到事件循环
+    task1 = asyncio.create_task(func())
+
+    task2 = asyncio.create_task(func())
+
+    print('main函数结束')
+
+    # 当执行某协程遇到IO操作,会自动切换执行其他任务
+    res1 = await task1
+    res2 = await task2
+    print(res1, res2)
+
+
+asyncio.run(main())
+
+res:
+main函数开始
+main函数结束
+1
+1
+2
+2
+返回值 返回值

示例2:

python
import asyncio
+
+
+async def func():
+    print(1)
+    await asyncio.sleep(2)
+    print(2)
+    return '返回值'
+
+
+async def main():
+    print('main函数开始')
+
+    
+    task_list = [
+       asyncio.create_task(func()),
+       asyncio.create_task(func())
+    ]
+    print('main函数结束')
+    done,pending = await asyncio.wait(task_list,timeout=None)
+    print(done)
+asyncio.run(main())
import asyncio
+
+
+async def func():
+    print(1)
+    await asyncio.sleep(2)
+    print(2)
+    return '返回值'
+
+
+async def main():
+    print('main函数开始')
+
+    
+    task_list = [
+       asyncio.create_task(func()),
+       asyncio.create_task(func())
+    ]
+    print('main函数结束')
+    done,pending = await asyncio.wait(task_list,timeout=None)
+    print(done)
+asyncio.run(main())

asyncio.Future对象

Task继承了Future,Task对象内部await的结果的处理基于Future对象

python
async def main():
+    loop = asyncio.get_running_loop()
+    _future = loop.create_future()
+    await _future
+asyncio.run(main())
async def main():
+    loop = asyncio.get_running_loop()
+    _future = loop.create_future()
+    await _future
+asyncio.run(main())

concurrent.futures.Future对象

使用线程池/进程池实现异步操作时用到的对象

python
import time
+from concurrent.futures import Future
+from concurrent.futures.thread import ThreadPoolExecutor
+
+def func(value):
+    time.sleep(1)
+    print(value)
+    return 123
+
+pool = ThreadPoolExecutory(max_workers=5)
+for i in range(5)
+	fut = pool.submit(func,1)
+    print(fut)
import time
+from concurrent.futures import Future
+from concurrent.futures.thread import ThreadPoolExecutor
+
+def func(value):
+    time.sleep(1)
+    print(value)
+    return 123
+
+pool = ThreadPoolExecutory(max_workers=5)
+for i in range(5)
+	fut = pool.submit(func,1)
+    print(fut)
+ + + + \ No newline at end of file diff --git "a/python/base/\350\243\205\351\245\260\345\231\250\346\267\261\345\205\245.html" "b/python/base/\350\243\205\351\245\260\345\231\250\346\267\261\345\205\245.html" new file mode 100644 index 000000000..d8880136c --- /dev/null +++ "b/python/base/\350\243\205\351\245\260\345\231\250\346\267\261\345\205\245.html" @@ -0,0 +1,501 @@ + + + + + + 装饰器深入 | 故事 + + + + + + + + + + + + + + +
Skip to content

装饰器深入

闭包

闭包的定义

如果在一个外部函数中定义一个内部函数,内部函数对外部作用域(但不是在全局作用域)的变量进行引用,外部函数的返回值是内部函数,这样的函数就被认为是闭包(closure)。

形成闭包的条件

  • 必须有内部函数
  • 内部函数必须引用外部函数的变量
  • 外部函数返回值必须是内部函数

装饰器

python装饰器实质上也是一个闭包函数,目的是在不改变原函数的情况下实现对原函数功能的增强。(类似Spring中的AOP)

装饰器的条件

  • 不修改已有函数代码
  • 不修改已有函数的调用方式
  • 给已有函数增加额外功能

装饰器语法糖

@装饰器函数名

@装饰器类名

@装饰器函数名(param)

@装饰器类名(param)

函数的函数装饰器(函数作为函数的装饰器)

不带参数的装饰器函数

这里的不带参数是指@装饰器后没有(参数)而非装饰器函数没有参数

例如下面的record函数就是一个简单装饰器,作用是记录被装饰函数的执行耗时

python
import time
+
+
+def record(func):
+    def decorator(*args, **kwargs):
+        print('====start====')
+        start = time.time()
+        func(*args, **kwargs)
+        print(f'===end cost : {time.time() - start} seconds===')
+
+    return decorator
+
+
+@record # 不带参数
+def test(name, age):
+    time.sleep(1)
+    print(f'my name is {name}, {age} years old')
+
+
+test('tom', 18)
+
+
+
+====start====
+my name is tom, 18 years old
+===end cost : 1.0049748420715332 seconds===
import time
+
+
+def record(func):
+    def decorator(*args, **kwargs):
+        print('====start====')
+        start = time.time()
+        func(*args, **kwargs)
+        print(f'===end cost : {time.time() - start} seconds===')
+
+    return decorator
+
+
+@record # 不带参数
+def test(name, age):
+    time.sleep(1)
+    print(f'my name is {name}, {age} years old')
+
+
+test('tom', 18)
+
+
+
+====start====
+my name is tom, 18 years old
+===end cost : 1.0049748420715332 seconds===

本质

在上述不带参数的装饰器函数例子中,14行@record实质上等于test = record(test),最终调用test('tom',18)的伪代码:

python
print('====start====')
+start = time.time()
+
+# func(*args, **kwargs) 
+time.sleep(1) # -> 原始的test('tom',18)
+print(f'my name is {name}, {age} years old') # -> 原始的test('tom',18)
+
+print(f'===end cost : {time.time() - start} seconds===')
print('====start====')
+start = time.time()
+
+# func(*args, **kwargs) 
+time.sleep(1) # -> 原始的test('tom',18)
+print(f'my name is {name}, {age} years old') # -> 原始的test('tom',18)
+
+print(f'===end cost : {time.time() - start} seconds===')

带参数的装饰器函数

python中一切皆对象,如果在对象后跟()即是执行调用的意思,例如函数,类,类里的函数,实现了__call__方法的对象都可以被调用,因为这些对象是callable对象。还是刚才的例子,@装饰器(参数)语法,实际上是在不带参数的装饰器函数基础上包了一层,由test = record(test)变成了decorator = record(count); test = decorator(test)

例子如下

python
import time
+
+def record(count):
+    def decorator(func):
+        def wrapper(*args, **kwargs):
+            print('====start====')
+            start = time.time()
+            for _num in range(count):
+                func(*args, **kwargs)
+            print(f'===end cost : {time.time() - start} seconds===')
+        return wrapper
+
+    return decorator
+
+
+@record(5)
+def test(name, age):
+    time.sleep(1)
+    print(f'my name is {name}, {age} years old')
+
+
+test('tom', 18)
+
+---
+====start====
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+===end cost : 5.020708799362183 seconds===
import time
+
+def record(count):
+    def decorator(func):
+        def wrapper(*args, **kwargs):
+            print('====start====')
+            start = time.time()
+            for _num in range(count):
+                func(*args, **kwargs)
+            print(f'===end cost : {time.time() - start} seconds===')
+        return wrapper
+
+    return decorator
+
+
+@record(5)
+def test(name, age):
+    time.sleep(1)
+    print(f'my name is {name}, {age} years old')
+
+
+test('tom', 18)
+
+---
+====start====
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+===end cost : 5.020708799362183 seconds===

本质

带参数的装饰器,实际就是加了一层函数的嵌套,可以把这种装饰器拆成两步分析,第一步执行record(5)返回了函数decorator,@decorator这样就是不带参数的装饰器形式了。

注意:装饰器返回的是一个全新的函数

装饰器返回的是一个全新的函数,对函数的装饰方法(常写成wrapper)的参数列表为了兼容性可以写为(*args, **kwargs),但是这个函数的参数实际可以写为任意形式(只要该参数包含被装饰函数的参数列表即可),回归到定义上来说就是不修改已有函数的调用方式即可。网上教程中通常把wrapper的参数写成和被装饰函数一致,很容易让人误以为这两者的参数列表必须保持一致。

例子:

python
import time
+from dataclasses import dataclass
+
+
+@dataclass
+class User(object):
+    name: str
+    info: tuple = ()
+
+
+def record(count):
+    def decorator(func):
+        def wrapper(user):
+            print('====start====')
+            start = time.time()
+            for _num in range(count):
+                func(*user.info)
+
+            print(f'===end cost : {time.time() - start} seconds===')
+        return wrapper
+
+    return decorator
+
+
+@record(5)
+def test(name, age):
+    time.sleep(1)
+    print(f'my name is {name}, {age} years old')
+
+
+# test('tom', 18)
+print(test.__name__)
+tom = User('tom', info=('tom', 18))
+test(tom)
+
+---
+
+wrapper
+====start====
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+===end cost : 5.013930797576904 seconds===
import time
+from dataclasses import dataclass
+
+
+@dataclass
+class User(object):
+    name: str
+    info: tuple = ()
+
+
+def record(count):
+    def decorator(func):
+        def wrapper(user):
+            print('====start====')
+            start = time.time()
+            for _num in range(count):
+                func(*user.info)
+
+            print(f'===end cost : {time.time() - start} seconds===')
+        return wrapper
+
+    return decorator
+
+
+@record(5)
+def test(name, age):
+    time.sleep(1)
+    print(f'my name is {name}, {age} years old')
+
+
+# test('tom', 18)
+print(test.__name__)
+tom = User('tom', info=('tom', 18))
+test(tom)
+
+---
+
+wrapper
+====start====
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+===end cost : 5.013930797576904 seconds===

可以看到,test函数的name为wrapper,也就是装饰功能的函数的名字,而且这里test函数的参数列表也已经变成了(user),也就是这里实际上test = wrapper(user)。如果使用装饰器后,想保留原函数的名称,可以使用@functools.wraps来装饰wrapper函数

例子:

python
import functools
+import time
+from dataclasses import dataclass
+
+
+@dataclass
+class User(object):
+    name: str
+    info: tuple = ()
+
+
+def record(count):
+    def decorator(func):
+        @functools.wraps(func)
+        def wrapper(user):
+            print('====start====')
+            start = time.time()
+            for _num in range(count):
+                func(*user.info)
+
+            print(f'===end cost : {time.time() - start} seconds===')
+        return wrapper
+
+    return decorator
+
+
+@record(5)
+def test(name, age):
+    time.sleep(1)
+    print(f'my name is {name}, {age} years old')
+
+
+# test('tom', 18)
+print(test.__name__)
+tom = User('tom', info=('tom', 18))
+test(tom)
+
+---
+
+test
+====start====
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+===end cost : 5.015993118286133 seconds===
import functools
+import time
+from dataclasses import dataclass
+
+
+@dataclass
+class User(object):
+    name: str
+    info: tuple = ()
+
+
+def record(count):
+    def decorator(func):
+        @functools.wraps(func)
+        def wrapper(user):
+            print('====start====')
+            start = time.time()
+            for _num in range(count):
+                func(*user.info)
+
+            print(f'===end cost : {time.time() - start} seconds===')
+        return wrapper
+
+    return decorator
+
+
+@record(5)
+def test(name, age):
+    time.sleep(1)
+    print(f'my name is {name}, {age} years old')
+
+
+# test('tom', 18)
+print(test.__name__)
+tom = User('tom', info=('tom', 18))
+test(tom)
+
+---
+
+test
+====start====
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+my name is tom, 18 years old
+===end cost : 5.015993118286133 seconds===

可以看到,在对test进行了装饰后,返回的新的函数名称还是保持为test

函数的类装饰器(类作为函数的装饰器)

不带参数的装饰器类

类也可以作为装饰器使用,需要实现__init__函数和__call__函数,例子:

python
import time
+
+
+class Timer(object):
+
+    def __init__(self, func):
+        self.func = func
+
+    def __call__(self, *args, **kwargs):
+        start = time.time()
+        ret = self.func(*args, **kwargs)
+        print(f'Time : {time.time() - start}')
+        return ret
+
+
+@Timer
+def add(a, b):
+    return a + b
+
+# 等价于 add = Timer(add)
+  
+print(add(2, 3))
import time
+
+
+class Timer(object):
+
+    def __init__(self, func):
+        self.func = func
+
+    def __call__(self, *args, **kwargs):
+        start = time.time()
+        ret = self.func(*args, **kwargs)
+        print(f'Time : {time.time() - start}')
+        return ret
+
+
+@Timer
+def add(a, b):
+    return a + b
+
+# 等价于 add = Timer(add)
+  
+print(add(2, 3))

带参数的装饰器类

类似带参数的装饰器函数,带参数的装饰器类需要在__call__函数内部,再包一层

python
import time
+
+
+class Timer(object):
+
+    def __init__(self, pre_fix):
+        self.pre_fix = pre_fix
+
+    def __call__(self, func):
+        def wrapper(*args, **kwargs):
+            start = time.time()
+            ret = func(*args, **kwargs)
+            print(f'{self.pre_fix}: {time.time() - start}')
+            return ret
+
+        return wrapper
+
+
+@Timer(pre_fix='current_time')
+def add(a, b):
+    return a + b
+
+
+print(add(2, 3))
import time
+
+
+class Timer(object):
+
+    def __init__(self, pre_fix):
+        self.pre_fix = pre_fix
+
+    def __call__(self, func):
+        def wrapper(*args, **kwargs):
+            start = time.time()
+            ret = func(*args, **kwargs)
+            print(f'{self.pre_fix}: {time.time() - start}')
+            return ret
+
+        return wrapper
+
+
+@Timer(pre_fix='current_time')
+def add(a, b):
+    return a + b
+
+
+print(add(2, 3))

类的函数装饰器(函数作为类的装饰器)

不带参数

函数也可以装饰类,下面的例子中,add_str是一个参数为class,返回值也是class的函数,装饰了MyObj类,作用是把被装饰类的__str__函数替换为打印self.__dict__

python
def add_str(cls):
+    def __str__(self):
+        return str(self.__dict__)
+
+    cls.__str__ = __str__
+    return cls
+
+
+@add_str
+class MyObj(object):
+    def __init__(self, a, b):
+        self.a = a
+        self.b = b
+
+
+print(MyObj(1, 2))
+
+---
+
+{'a': 1, 'b': 2}
def add_str(cls):
+    def __str__(self):
+        return str(self.__dict__)
+
+    cls.__str__ = __str__
+    return cls
+
+
+@add_str
+class MyObj(object):
+    def __init__(self, a, b):
+        self.a = a
+        self.b = b
+
+
+print(MyObj(1, 2))
+
+---
+
+{'a': 1, 'b': 2}

带参数

python
def add_str(time):
+    def _cls(cls):
+        def __str__(self):
+            return f'调用时间 {time} 点 == ' + str(self.__dict__)
+
+        cls.__str__ = __str__
+
+        return cls
+
+    return _cls
+
+
+@add_str(time='19')
+class MyObj(object):
+    def __init__(self, a, b):
+        self.a = a
+        self.b = b
+
+
+print(MyObj(1, 2))
+
+---
+
+调用时间 19== {'a': 1, 'b': 2}
def add_str(time):
+    def _cls(cls):
+        def __str__(self):
+            return f'调用时间 {time} 点 == ' + str(self.__dict__)
+
+        cls.__str__ = __str__
+
+        return cls
+
+    return _cls
+
+
+@add_str(time='19')
+class MyObj(object):
+    def __init__(self, a, b):
+        self.a = a
+        self.b = b
+
+
+print(MyObj(1, 2))
+
+---
+
+调用时间 19== {'a': 1, 'b': 2}

类作为类的装饰器

没什么意义

+ + + + \ No newline at end of file diff --git "a/python/crawler/CrawlSpider\345\205\250\347\253\231\347\210\254\345\217\226.html" "b/python/crawler/CrawlSpider\345\205\250\347\253\231\347\210\254\345\217\226.html" new file mode 100644 index 000000000..714cae25b --- /dev/null +++ "b/python/crawler/CrawlSpider\345\205\250\347\253\231\347\210\254\345\217\226.html" @@ -0,0 +1,253 @@ + + + + + + CrawlSpider全站爬取 | 故事 + + + + + + + + + + + + + + +
Skip to content

CrawlSpider全站爬取

CrawlSpider

CrawlSpider是Spider的一个子类,具有提取指定规则链接的功能

CrawlSpider的作用:

  • 全站爬取
    • 基于Spider手动请求
    • 基于CrawlSpider

项目创建

  • scrapy startproject crawl_spider
  • cd crawl_spider
  • 创建基于CrawlSpider的爬虫类:scrapy genspider -t crawl storyxc xxx.com 相比普通的增加了-t crawl参数

链接提取器

根据指定规则(allow=‘正则表达式“)提取符合要求的所有url

python
link = LinkExtractor(allow=r'id=1&page=\d+')
link = LinkExtractor(allow=r'id=1&page=\d+')

规则解析器

将链接提取器提取到的链接进行指定规则(callback)的解析

python
rules = (
+    Rule(link, callback='parse_item', follow=True),
+)
rules = (
+    Rule(link, callback='parse_item', follow=True),
+)
  • follow参数的作用:
    • True:可以将链接提取器继续作用到链接提取器提取到的链接(递归)
    • False:只提取起始页的数据

案例:提取东莞阳光问政平台的问政标题和编号

爬虫类

python
from scrapy.linkextractors import LinkExtractor
+from scrapy.spiders import CrawlSpider, Rule
+from crawl_spider.items import CrawlSpiderItem,DetailItem
+
+
+class StoryxcSpider(CrawlSpider):
+    name = 'storyxc'
+    start_urls = ['http://wz.sun0769.com/political/index/politicsNewest']
+
+    # 链接提取器,符合正则表达式的链接都会被提取
+    link = LinkExtractor(allow=r'id=1&page=\d+')
+    detail_link = LinkExtractor(allow=r'\/political\/politics\/index\?id=\d+')
+
+    rules = (
+        Rule(link, callback='parse_item', follow=True),
+        Rule(detail_link, callback='parse_detail'),
+    )
+
+    def parse_item(self, response):
+        li_list = response.xpath('/html/body/div[2]/div[3]/ul[2]/li')
+        for li in li_list:
+            wz_id = li.xpath('./span[1]/text()').extract_first()
+            wz_title = li.xpath('./span[3]/a/text()').extract_first()
+            item = CrawlSpiderItem()
+            item['num'] = wz_id
+            item['title'] = wz_title
+            yield item
+
+    def parse_detail(self, response):
+        id = response.xpath('/html/body/div[3]/div[2]/div[2]/div[1]/span[4]/text()').extract_first()
+        id = id.replace('编号:','')
+        content = ''.join(response.xpath('/html/body/div[3]/div[2]/div[2]/div[2]/pre/text()').extract())
+        item = DetailItem()
+        item['num'] = id
+        item['content'] = content
+        yield item
from scrapy.linkextractors import LinkExtractor
+from scrapy.spiders import CrawlSpider, Rule
+from crawl_spider.items import CrawlSpiderItem,DetailItem
+
+
+class StoryxcSpider(CrawlSpider):
+    name = 'storyxc'
+    start_urls = ['http://wz.sun0769.com/political/index/politicsNewest']
+
+    # 链接提取器,符合正则表达式的链接都会被提取
+    link = LinkExtractor(allow=r'id=1&page=\d+')
+    detail_link = LinkExtractor(allow=r'\/political\/politics\/index\?id=\d+')
+
+    rules = (
+        Rule(link, callback='parse_item', follow=True),
+        Rule(detail_link, callback='parse_detail'),
+    )
+
+    def parse_item(self, response):
+        li_list = response.xpath('/html/body/div[2]/div[3]/ul[2]/li')
+        for li in li_list:
+            wz_id = li.xpath('./span[1]/text()').extract_first()
+            wz_title = li.xpath('./span[3]/a/text()').extract_first()
+            item = CrawlSpiderItem()
+            item['num'] = wz_id
+            item['title'] = wz_title
+            yield item
+
+    def parse_detail(self, response):
+        id = response.xpath('/html/body/div[3]/div[2]/div[2]/div[1]/span[4]/text()').extract_first()
+        id = id.replace('编号:','')
+        content = ''.join(response.xpath('/html/body/div[3]/div[2]/div[2]/div[2]/pre/text()').extract())
+        item = DetailItem()
+        item['num'] = id
+        item['content'] = content
+        yield item

item类

python
# Define here the models for your scraped items
+#
+# See documentation in:
+# https://docs.scrapy.org/en/latest/topics/items.html
+
+import scrapy
+
+
+class CrawlSpiderItem(scrapy.Item):
+    # define the fields for your item here like:
+    # name = scrapy.Field()
+    title =scrapy.Field()
+    num = scrapy.Field()
+
+class DetailItem(scrapy.Item):
+    num = scrapy.Field()
+    content = scrapy.Field()
# Define here the models for your scraped items
+#
+# See documentation in:
+# https://docs.scrapy.org/en/latest/topics/items.html
+
+import scrapy
+
+
+class CrawlSpiderItem(scrapy.Item):
+    # define the fields for your item here like:
+    # name = scrapy.Field()
+    title =scrapy.Field()
+    num = scrapy.Field()
+
+class DetailItem(scrapy.Item):
+    num = scrapy.Field()
+    content = scrapy.Field()

Pipeline类

python
# Define your item pipelines here
+#
+# Don't forget to add your pipeline to the ITEM_PIPELINES setting
+# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
+
+
+# useful for handling different item types with a single interface
+from itemadapter import ItemAdapter
+from crawl_spider.items import CrawlSpiderItem, DetailItem
+import pymysql
+
+
+class CrawlSpiderPipeline:
+    def process_item(self, item, spider):
+        if item.__class__.__name__ == 'DetailItem':
+            with Mysql() as conn:
+                cursor = conn.cursor()
+                try:
+                    cursor.execute(
+                        'insert into tb_wz_content(id,content) values("%s","%s")' % (
+                            item['num'],item['content']))
+                    conn.commit()
+                except:
+                    print('插入问政内容失败!')
+                    conn.rollback()
+
+
+        else:
+            with Mysql() as conn:
+                cursor = conn.cursor()
+                try:
+                    cursor.execute(
+                        'insert into tb_wz_title(id,title) values("%s","%s")' % (item['num'],item['title']))
+                    conn.commit()
+                except:
+                    print('插入问政标题失败!')
+                    conn.rollback()
+
+
+class Mysql(object):
+    def __enter__(self):
+        self.connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+        return self.connection
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.connection.close()
# Define your item pipelines here
+#
+# Don't forget to add your pipeline to the ITEM_PIPELINES setting
+# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
+
+
+# useful for handling different item types with a single interface
+from itemadapter import ItemAdapter
+from crawl_spider.items import CrawlSpiderItem, DetailItem
+import pymysql
+
+
+class CrawlSpiderPipeline:
+    def process_item(self, item, spider):
+        if item.__class__.__name__ == 'DetailItem':
+            with Mysql() as conn:
+                cursor = conn.cursor()
+                try:
+                    cursor.execute(
+                        'insert into tb_wz_content(id,content) values("%s","%s")' % (
+                            item['num'],item['content']))
+                    conn.commit()
+                except:
+                    print('插入问政内容失败!')
+                    conn.rollback()
+
+
+        else:
+            with Mysql() as conn:
+                cursor = conn.cursor()
+                try:
+                    cursor.execute(
+                        'insert into tb_wz_title(id,title) values("%s","%s")' % (item['num'],item['title']))
+                    conn.commit()
+                except:
+                    print('插入问政标题失败!')
+                    conn.rollback()
+
+
+class Mysql(object):
+    def __enter__(self):
+        self.connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+        return self.connection
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.connection.close()

settings

python
BOT_NAME = 'crawl_spider'
+
+SPIDER_MODULES = ['crawl_spider.spiders']
+NEWSPIDER_MODULE = 'crawl_spider.spiders'
+
+
+# Crawl responsibly by identifying yourself (and your website) on the user-agent
+USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+
+# Obey robots.txt rules
+ROBOTSTXT_OBEY = False
+
+
+
+ITEM_PIPELINES = {
+   'crawl_spider.pipelines.CrawlSpiderPipeline': 300,
+}
BOT_NAME = 'crawl_spider'
+
+SPIDER_MODULES = ['crawl_spider.spiders']
+NEWSPIDER_MODULE = 'crawl_spider.spiders'
+
+
+# Crawl responsibly by identifying yourself (and your website) on the user-agent
+USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+
+# Obey robots.txt rules
+ROBOTSTXT_OBEY = False
+
+
+
+ITEM_PIPELINES = {
+   'crawl_spider.pipelines.CrawlSpiderPipeline': 300,
+}

启动爬虫:

数据库会新增数据

image-20210502012334780

+ + + + \ No newline at end of file diff --git "a/python/crawler/python\351\205\215\345\220\210ffmpeg\344\270\213\350\275\275bilibili\350\247\206\351\242\221.html" "b/python/crawler/python\351\205\215\345\220\210ffmpeg\344\270\213\350\275\275bilibili\350\247\206\351\242\221.html" new file mode 100644 index 000000000..7491b7aad --- /dev/null +++ "b/python/crawler/python\351\205\215\345\220\210ffmpeg\344\270\213\350\275\275bilibili\350\247\206\351\242\221.html" @@ -0,0 +1,157 @@ + + + + + + python配合ffmpeg下载bilibili视频 | 故事 + + + + + + + + + + + + + + +
Skip to content

python配合ffmpeg下载bilibili视频

直接上代码

TIP

需要提前下载ffmpeg并配置环境变量,ffmpeg下载地址:http://www.ffmpeg.org/download.html

python
import requests
+import re
+import json
+import os
+import subprocess
+
+headers = {
+    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36',
+    'Referer': 'https://www.bilibili.com'
+}
+
+"""
+    requests获取页面源码
+"""
+
+
+def send_request(b_url):
+    data = requests.get(url=b_url, headers=headers).text
+    return data
+
+
+"""
+    正则匹配视频和音频的真实地址
+"""
+
+
+def get_play_info(data):
+    json_data = json.loads(re.findall('<script>window\.__playinfo__=(.*?)</script>', data)[0])
+    video_url = json_data['data']['dash']['video'][0]['backupUrl'][0]
+    audio_url = json_data['data']['dash']['audio'][0]['backupUrl'][0]
+    return video_url, audio_url
+
+
+"""
+    分别下载视频和音频文件后利用ffmpeg合并
+"""
+
+
+def download(info_list, info):
+    print(f'开始下载: {info}')
+    video_data = requests.get(url=info_list[0], headers=headers).content
+    audio_data = requests.get(url=info_list[1], headers=headers).content
+    desktop = os.path.join(os.path.expanduser("~"), 'Desktop')
+    video_path = desktop + '\\' + info
+    audio_path = desktop + '\\' + info + '_.mp3'
+    # 如果视频名称中有'-' 执行ffmpeg合并的时候会报错
+    video_path = video_path.replace('-',' ')
+    audio_path = audio_path.replace('-',' ')
+    with open(video_path + '_temp.mp4', 'wb') as f:
+        f.write(video_data)
+    with open(audio_path, 'wb') as f:
+        f.write(audio_data)
+    cmd = 'ffmpeg -y -i ' + video_path + '_temp.mp4' + ' -i ' \
+          + audio_path + ' -c:v copy -c:a aac -strict experimental ' + video_path + '.mp4'
+    print(cmd)
+    subprocess.Popen(cmd, shell=True)
+    # os.system(cmd)
+    print('下载完成')
+
+
+if __name__ == '__main__':
+    url = input('请输入要下载的b站视频链接:')
+    page_data = send_request(url)
+    # 解析视频的名称
+    title = re.findall('<h1 title=\"(.*?)\" class=\"video-title', page_data)[0]
+    play_info_list = get_play_info(page_data)
+    download(play_info_list, title)
import requests
+import re
+import json
+import os
+import subprocess
+
+headers = {
+    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36',
+    'Referer': 'https://www.bilibili.com'
+}
+
+"""
+    requests获取页面源码
+"""
+
+
+def send_request(b_url):
+    data = requests.get(url=b_url, headers=headers).text
+    return data
+
+
+"""
+    正则匹配视频和音频的真实地址
+"""
+
+
+def get_play_info(data):
+    json_data = json.loads(re.findall('<script>window\.__playinfo__=(.*?)</script>', data)[0])
+    video_url = json_data['data']['dash']['video'][0]['backupUrl'][0]
+    audio_url = json_data['data']['dash']['audio'][0]['backupUrl'][0]
+    return video_url, audio_url
+
+
+"""
+    分别下载视频和音频文件后利用ffmpeg合并
+"""
+
+
+def download(info_list, info):
+    print(f'开始下载: {info}')
+    video_data = requests.get(url=info_list[0], headers=headers).content
+    audio_data = requests.get(url=info_list[1], headers=headers).content
+    desktop = os.path.join(os.path.expanduser("~"), 'Desktop')
+    video_path = desktop + '\\' + info
+    audio_path = desktop + '\\' + info + '_.mp3'
+    # 如果视频名称中有'-' 执行ffmpeg合并的时候会报错
+    video_path = video_path.replace('-',' ')
+    audio_path = audio_path.replace('-',' ')
+    with open(video_path + '_temp.mp4', 'wb') as f:
+        f.write(video_data)
+    with open(audio_path, 'wb') as f:
+        f.write(audio_data)
+    cmd = 'ffmpeg -y -i ' + video_path + '_temp.mp4' + ' -i ' \
+          + audio_path + ' -c:v copy -c:a aac -strict experimental ' + video_path + '.mp4'
+    print(cmd)
+    subprocess.Popen(cmd, shell=True)
+    # os.system(cmd)
+    print('下载完成')
+
+
+if __name__ == '__main__':
+    url = input('请输入要下载的b站视频链接:')
+    page_data = send_request(url)
+    # 解析视频的名称
+    title = re.findall('<h1 title=\"(.*?)\" class=\"video-title', page_data)[0]
+    play_info_list = get_play_info(page_data)
+    download(play_info_list, title)
+ + + + \ No newline at end of file diff --git "a/python/crawler/requests\346\250\241\345\235\227\345\205\245\351\227\250.html" "b/python/crawler/requests\346\250\241\345\235\227\345\205\245\351\227\250.html" new file mode 100644 index 000000000..56bc02293 --- /dev/null +++ "b/python/crawler/requests\346\250\241\345\235\227\345\205\245\351\227\250.html" @@ -0,0 +1,497 @@ + + + + + + requests模块入门 | 故事 + + + + + + + + + + + + + + +
Skip to content

requests模块入门

requests模块是python中原生的一款基于网络请求的模块,功能强大,简单便捷,效率极高。

作用:模拟浏览器发请求

  • 指定url
  • 发起请求
  • 获取响应
  • 持久化存储

入门程序

环境安装:pip install requests

python
import requests
+
+# 爬取搜狗首页的数据
+
+if __name__ == '__main__':
+    url = "https://www.sogou.com"
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+
+    response = requests.get(url,headers)
+    page_text = response.text
+    print(page_text)
+    with open("./sogou.html", "w", encoding="utf-8") as fp:
+        fp.write(page_text)
+    print("爬取结束")
import requests
+
+# 爬取搜狗首页的数据
+
+if __name__ == '__main__':
+    url = "https://www.sogou.com"
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+
+    response = requests.get(url,headers)
+    page_text = response.text
+    print(page_text)
+    with open("./sogou.html", "w", encoding="utf-8") as fp:
+        fp.write(page_text)
+    print("爬取结束")

WARNING

坑1:ValueError:requests check_hostname requires server_hostname

坑2:requests.exceptions.SSLError: hostname '127.0.0.1' doesn't match None of。。

网上有说降低requests版本的,有安装乱七八糟东西的,最后是降低了urllib3的版本解决的,据说是高版本的urllib有个bug

pip install urllib3==1.25.8

简易网页采集器

python
import requests
+
+if __name__ == '__main__':
+    url = 'https://www.sogou.com/web'
+    keyword = input('enter your keyword: ')
+    # UA伪装
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36'
+    }
+    param = {
+        'query': keyword
+    }
+    response = requests.get(url, params=param,headers=headers)
+    resp = response.text
+    file_name = keyword + '.html'
+    with open(file_name, 'w', encoding='utf-8') as f:
+        f.write(resp)
+    print(file_name, '保存成功')
import requests
+
+if __name__ == '__main__':
+    url = 'https://www.sogou.com/web'
+    keyword = input('enter your keyword: ')
+    # UA伪装
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36'
+    }
+    param = {
+        'query': keyword
+    }
+    response = requests.get(url, params=param,headers=headers)
+    resp = response.text
+    file_name = keyword + '.html'
+    with open(file_name, 'w', encoding='utf-8') as f:
+        f.write(resp)
+    print(file_name, '保存成功')

百度翻译内容提取

python
import requests
+import json
+
+if __name__ == '__main__':
+    url = 'https://fanyi.baidu.com/sug'
+    keyword = {
+        'kw': 'dog'
+    }
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    response = requests.post(url=url, data=keyword, headers=headers)
+    _json = response.json()
+    file_name = keyword.get('kw') + '.json'
+    with open(file_name, 'w', encoding='utf-8') as f:
+        json.dump(_json,f,ensure_ascii=False)
+    print('json存储成功')
import requests
+import json
+
+if __name__ == '__main__':
+    url = 'https://fanyi.baidu.com/sug'
+    keyword = {
+        'kw': 'dog'
+    }
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    response = requests.post(url=url, data=keyword, headers=headers)
+    _json = response.json()
+    file_name = keyword.get('kw') + '.json'
+    with open(file_name, 'w', encoding='utf-8') as f:
+        json.dump(_json,f,ensure_ascii=False)
+    print('json存储成功')

结果:

json
{
+  "errno": 0,
+  "data": [
+    {
+      "k": "dog",
+      "v": "n. 狗; 蹩脚货; 丑女人; 卑鄙小人 v. 困扰; 跟踪"
+    },
+    {
+      "k": "DOG",
+      "v": "abbr. Data Output Gate 数据输出门"
+    },
+    {
+      "k": "doge",
+      "v": "n. 共和国总督"
+    },
+    {
+      "k": "dogm",
+      "v": "abbr. dogmatic 教条的; 独断的; dogmatism 教条主义; dogmatist"
+    },
+    {
+      "k": "Dogo",
+      "v": "[地名] [马里、尼日尔、乍得] 多戈; [地名] [韩国] 道高"
+    }
+  ]
+}
{
+  "errno": 0,
+  "data": [
+    {
+      "k": "dog",
+      "v": "n. 狗; 蹩脚货; 丑女人; 卑鄙小人 v. 困扰; 跟踪"
+    },
+    {
+      "k": "DOG",
+      "v": "abbr. Data Output Gate 数据输出门"
+    },
+    {
+      "k": "doge",
+      "v": "n. 共和国总督"
+    },
+    {
+      "k": "dogm",
+      "v": "abbr. dogmatic 教条的; 独断的; dogmatism 教条主义; dogmatist"
+    },
+    {
+      "k": "Dogo",
+      "v": "[地名] [马里、尼日尔、乍得] 多戈; [地名] [韩国] 道高"
+    }
+  ]
+}

豆瓣电影排行榜

python
import requests
+import json
+
+if __name__ == '__main__':
+    url = 'https://movie.douban.com/j/chart/top_list'
+    total = 748
+    limit = 20
+    start = 0
+    params = {
+        'type': 11,
+        'interval_id': '100:90',
+        'action': '',
+        'start': start,
+        'limit': limit
+    }
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    result = total // limit
+    res_list = []
+    if result:
+        for i in range(result if total // limit == 0 else result + 1):
+            start += 20
+            res = requests.get(url=url, params=params, headers=headers)
+            res_json = res.json()
+            res_list.append(res_json)
+    with open('douban_movie.json', 'w', encoding='utf-8') as f:
+        json.dump(res_list, f, ensure_ascii=False)
+
+    print('over')
import requests
+import json
+
+if __name__ == '__main__':
+    url = 'https://movie.douban.com/j/chart/top_list'
+    total = 748
+    limit = 20
+    start = 0
+    params = {
+        'type': 11,
+        'interval_id': '100:90',
+        'action': '',
+        'start': start,
+        'limit': limit
+    }
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    result = total // limit
+    res_list = []
+    if result:
+        for i in range(result if total // limit == 0 else result + 1):
+            start += 20
+            res = requests.get(url=url, params=params, headers=headers)
+            res_json = res.json()
+            res_list.append(res_json)
+    with open('douban_movie.json', 'w', encoding='utf-8') as f:
+        json.dump(res_list, f, ensure_ascii=False)
+
+    print('over')

国家药品管理局化妆品生产许可信息

python
import requests
+import json
+
+if __name__ == '__main__':
+    # 列表页ajax
+    url = 'http://scxk.nmpa.gov.cn:81/xk/itownet/portalAction.do?method=getXkzsList'
+    # 详情页ajax
+    detail_url = 'http://scxk.nmpa.gov.cn:81/xk/itownet/portalAction.do?method=getXkzsById'
+
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    params = {
+        'on': True,
+        'page': 1,
+        'pageSize': 15,
+        'productName': '',
+        'conditionType': 1,
+        'applyname': '',
+        'applysn': ''
+    }
+
+    detail_params = {
+        'id': ''
+    }
+    # 数据容器
+    data_list = []
+    # 列表页响应
+    response = requests.post(url=url, params=params, headers=headers)
+    res_obj = response.json()
+    # 提取列表信息遍历
+    res_list = res_obj.get('list')
+    for data in res_list:
+        # id是详情页请求的参数
+        detail_id = data.get('ID')
+        detail_params['id'] = detail_id
+        # 详情页响应
+        resp = requests.post(url=detail_url, params=detail_params, headers=headers)
+        res_obj = resp.json()
+        # 容器保存
+        data_list.append(res_obj)
+    # 持久化存储
+    with open('make_up_xkz.json', 'a', encoding='utf-8') as f:
+        json.dump(data_list, f, ensure_ascii=False)
+    print('over')
import requests
+import json
+
+if __name__ == '__main__':
+    # 列表页ajax
+    url = 'http://scxk.nmpa.gov.cn:81/xk/itownet/portalAction.do?method=getXkzsList'
+    # 详情页ajax
+    detail_url = 'http://scxk.nmpa.gov.cn:81/xk/itownet/portalAction.do?method=getXkzsById'
+
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    params = {
+        'on': True,
+        'page': 1,
+        'pageSize': 15,
+        'productName': '',
+        'conditionType': 1,
+        'applyname': '',
+        'applysn': ''
+    }
+
+    detail_params = {
+        'id': ''
+    }
+    # 数据容器
+    data_list = []
+    # 列表页响应
+    response = requests.post(url=url, params=params, headers=headers)
+    res_obj = response.json()
+    # 提取列表信息遍历
+    res_list = res_obj.get('list')
+    for data in res_list:
+        # id是详情页请求的参数
+        detail_id = data.get('ID')
+        detail_params['id'] = detail_id
+        # 详情页响应
+        resp = requests.post(url=detail_url, params=detail_params, headers=headers)
+        res_obj = resp.json()
+        # 容器保存
+        data_list.append(res_obj)
+    # 持久化存储
+    with open('make_up_xkz.json', 'a', encoding='utf-8') as f:
+        json.dump(data_list, f, ensure_ascii=False)
+    print('over')

正则表达式匹配

爬取糗事百科图片

python
import requests
+import re
+
+if __name__ == '__main__':
+    for i in range(13):
+        page_no = str(i + 1)
+        url = 'https://www.qiushibaike.com/imgrank/page/%d'
+        headers = {
+            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+        }
+        url = format(url % page_no)
+        page_txt = requests.get(url=url, headers=headers).text
+        exp = '<div class="thumb">.*?<img src="(.*?)" alt.*?</div>'
+        src_list = re.findall(exp, page_txt, re.S)
+        # print(src_list)
+        for src in src_list:
+            url = 'https:' + src
+            # 向图片url发请求保存
+            stream = requests.get(url=url, headers=headers).content
+            # 文件名
+            src = src.split('/')[-1]
+            img_path = './img/' + src
+            f = open(img_path, 'wb')
+            f.write(stream)
+            print(img_path, '下载成功')
import requests
+import re
+
+if __name__ == '__main__':
+    for i in range(13):
+        page_no = str(i + 1)
+        url = 'https://www.qiushibaike.com/imgrank/page/%d'
+        headers = {
+            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+        }
+        url = format(url % page_no)
+        page_txt = requests.get(url=url, headers=headers).text
+        exp = '<div class="thumb">.*?<img src="(.*?)" alt.*?</div>'
+        src_list = re.findall(exp, page_txt, re.S)
+        # print(src_list)
+        for src in src_list:
+            url = 'https:' + src
+            # 向图片url发请求保存
+            stream = requests.get(url=url, headers=headers).content
+            # 文件名
+            src = src.split('/')[-1]
+            img_path = './img/' + src
+            f = open(img_path, 'wb')
+            f.write(stream)
+            print(img_path, '下载成功')

XPath解析

解析58二手房标题

python
from lxml import etree
+import requests
+
+if __name__ == '__main__':
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    url = 'https://bj.58.com/ershoufang/'
+    page_text = requests.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    div_list = tree.xpath('//div[@class="property"]')
+    with open('58.txt', 'w', encoding='utf-8') as f:
+        for div in div_list:
+            title = div.xpath('.//h3[@class="property-content-title-name"]/text()')
+            f.write(title[0]+'\r\n')
from lxml import etree
+import requests
+
+if __name__ == '__main__':
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    url = 'https://bj.58.com/ershoufang/'
+    page_text = requests.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    div_list = tree.xpath('//div[@class="property"]')
+    with open('58.txt', 'w', encoding='utf-8') as f:
+        for div in div_list:
+            title = div.xpath('.//h3[@class="property-content-title-name"]/text()')
+            f.write(title[0]+'\r\n')

多线程爬取美女图片

python
import requests
+from lxml import etree
+import os
+from concurrent.futures import ThreadPoolExecutor
+
+
+def download_pic(page_no, real_url, first_page_url, ):
+    print('=============开始下载第 ' + str(page_no) + ' 页=================')
+    if not page_no == 1:
+        pattern = '_' + str(page_no)
+        init_url = format(real_url % pattern)
+    else:
+        init_url = first_page_url
+    down_page_text = requests.get(url=init_url, headers=headers).text
+    down_tree = etree.HTML(down_page_text)
+    li_list = down_tree.xpath('//div[@class="slist"]//li')
+    for li in li_list:
+        img_url = 'https://pic.netbian.com' + li.xpath('.//img/@src')[0]
+        img_name = li.xpath('.//img/@alt')[0] + '.jpg'
+        img_name = img_name.encode('iso-8859-1').decode('gbk')
+        with open('./beauty/' + img_name, 'wb') as f:
+            stream = requests.get(url=img_url, headers=headers).content
+            f.write(stream)
+            print(img_name, ' 下载成功')
+
+
+if __name__ == '__main__':
+    url = 'https://pic.netbian.com/4kmeinv/index.html'
+    next_url = 'https://pic.netbian.com/4kmeinv/index%s.html'
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+
+    if not os.path.exists('./beauty'):
+        os.mkdir('./beauty')
+    page_text = requests.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    page_info = tree.xpath('//div[@class="page"]/a[7]/text()')
+    total_page_no = int(page_info[0])
+    thread_pool = ThreadPoolExecutor(max_workers=60)
+    for i in range(total_page_no):
+        i += 1
+        thread_pool.submit(download_pic, i, next_url, url)
import requests
+from lxml import etree
+import os
+from concurrent.futures import ThreadPoolExecutor
+
+
+def download_pic(page_no, real_url, first_page_url, ):
+    print('=============开始下载第 ' + str(page_no) + ' 页=================')
+    if not page_no == 1:
+        pattern = '_' + str(page_no)
+        init_url = format(real_url % pattern)
+    else:
+        init_url = first_page_url
+    down_page_text = requests.get(url=init_url, headers=headers).text
+    down_tree = etree.HTML(down_page_text)
+    li_list = down_tree.xpath('//div[@class="slist"]//li')
+    for li in li_list:
+        img_url = 'https://pic.netbian.com' + li.xpath('.//img/@src')[0]
+        img_name = li.xpath('.//img/@alt')[0] + '.jpg'
+        img_name = img_name.encode('iso-8859-1').decode('gbk')
+        with open('./beauty/' + img_name, 'wb') as f:
+            stream = requests.get(url=img_url, headers=headers).content
+            f.write(stream)
+            print(img_name, ' 下载成功')
+
+
+if __name__ == '__main__':
+    url = 'https://pic.netbian.com/4kmeinv/index.html'
+    next_url = 'https://pic.netbian.com/4kmeinv/index%s.html'
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+
+    if not os.path.exists('./beauty'):
+        os.mkdir('./beauty')
+    page_text = requests.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    page_info = tree.xpath('//div[@class="page"]/a[7]/text()')
+    total_page_no = int(page_info[0])
+    thread_pool = ThreadPoolExecutor(max_workers=60)
+    for i in range(total_page_no):
+        i += 1
+        thread_pool.submit(download_pic, i, next_url, url)

全国城市名称爬取

python
import requests
+from lxml import etree
+
+if __name__ == '__main__':
+    url = 'https://www.aqistudy.cn/historydata'
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    page_text = requests.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    cities = tree.xpath('//div[@class="bottom"]/ul//li/a/text()')
+    print(len(cities))
import requests
+from lxml import etree
+
+if __name__ == '__main__':
+    url = 'https://www.aqistudy.cn/historydata'
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    page_text = requests.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    cities = tree.xpath('//div[@class="bottom"]/ul//li/a/text()')
+    print(len(cities))
+ + + + \ No newline at end of file diff --git "a/python/crawler/scrapy\346\241\206\346\236\266\345\205\245\351\227\250.html" "b/python/crawler/scrapy\346\241\206\346\236\266\345\205\245\351\227\250.html" new file mode 100644 index 000000000..7595e8bf4 --- /dev/null +++ "b/python/crawler/scrapy\346\241\206\346\236\266\345\205\245\351\227\250.html" @@ -0,0 +1,305 @@ + + + + + + scrapy框架入门 | 故事 + + + + + + + + + + + + + + +
Skip to content

scrapy框架入门

高性能的持久化存储,高性能的数据解析,分布式。

使用

  • 安装:pip install scrapy
  • 创建项目:scrapy startproject yourProjectName
bash
story_spider/
+    scrapy.cfg            # 部署配置文件
+
+    story_spider/             # Python模块,代码写在这个目录下
+        __init__.py
+
+        items.py          # 项目项定义文件
+
+        pipelines.py      # 项目管道文件
+
+        settings.py       # 项目设置文件
+
+        spiders/          # 我们的爬虫/蜘蛛 目录
+            __init__.py
story_spider/
+    scrapy.cfg            # 部署配置文件
+
+    story_spider/             # Python模块,代码写在这个目录下
+        __init__.py
+
+        items.py          # 项目项定义文件
+
+        pipelines.py      # 项目管道文件
+
+        settings.py       # 项目设置文件
+
+        spiders/          # 我们的爬虫/蜘蛛 目录
+            __init__.py
  • 在spiders目录中创建一个爬虫文件

    • cd 项目目录(spriders文件夹所在的目录)
    • scrapy genspider storyxc storyxc.com
  • 爬虫文件内容

    python
    import scrapy
    +	
    +
    +# 必须继承scrapy.Spider
    +class StoryxcSpider(scrapy.Spider):
    +    # 爬虫文件的名称:爬虫源文件的一个唯一标识
    +    name = 'storyxc'
    +    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
    +    allowed_domains = ['storyxc.com']
    +    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
    +    start_urls = ['https://www.storyxc.com/', 'http://blog.storyxc.com']
    +
    +    # 用作数据解析:response参数表示的是请求成功后的响应对象
    +    def parse(self, response):
    +        print(response)
    import scrapy
    +	
    +
    +# 必须继承scrapy.Spider
    +class StoryxcSpider(scrapy.Spider):
    +    # 爬虫文件的名称:爬虫源文件的一个唯一标识
    +    name = 'storyxc'
    +    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
    +    allowed_domains = ['storyxc.com']
    +    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
    +    start_urls = ['https://www.storyxc.com/', 'http://blog.storyxc.com']
    +
    +    # 用作数据解析:response参数表示的是请求成功后的响应对象
    +    def parse(self, response):
    +        print(response)

    修改settings.py中的ROBOTSTXT_OBEY = False

    执行工程命令后可以加 --nolog

    也可以在setting.py中添加:

    #显示指定级别的日志信息
    +LOG_LEVEL = 'ERROR'
    #显示指定级别的日志信息
    +LOG_LEVEL = 'ERROR'
  • 执行工程scrapy crawl storyxc,日志信息

    bash
    <200 https://www.storyxc.com/>
    +<200 https://blog.storyxc.com/>
    <200 https://www.storyxc.com/>
    +<200 https://blog.storyxc.com/>

scrapy数据解析

解析糗事百科段子的作者和段子内容

python
import scrapy
+
+
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['https://www.qiushibaike.com/text/']
+
+    # 用作数据解析:response参数表示的是请求成功后的响应对象
+    def parse(self, response):
+        # 解析:作者的名称+段子内容
+        div_list = response.xpath('//div[@id="content"]/div/div[2]/div')
+        for div in div_list:
+            # extract()方法可以提取Selector对象中的data参数字符串
+            # extract_first()提取的是list数组里面的第一个字符串,
+            author = div.xpath('./div[1]/a[2]/h2/text()').extract_first()
+            # 列表调用了extract()表示将每一个Selector对象中的data字符串提取出来
+            content = ''.join(div.xpath('./a[1]/div[1]/span//text()').extract())
+            print(author,content)
import scrapy
+
+
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['https://www.qiushibaike.com/text/']
+
+    # 用作数据解析:response参数表示的是请求成功后的响应对象
+    def parse(self, response):
+        # 解析:作者的名称+段子内容
+        div_list = response.xpath('//div[@id="content"]/div/div[2]/div')
+        for div in div_list:
+            # extract()方法可以提取Selector对象中的data参数字符串
+            # extract_first()提取的是list数组里面的第一个字符串,
+            author = div.xpath('./div[1]/a[2]/h2/text()').extract_first()
+            # 列表调用了extract()表示将每一个Selector对象中的data字符串提取出来
+            content = ''.join(div.xpath('./a[1]/div[1]/span//text()').extract())
+            print(author,content)

基于终端指令持久化存储

代码改造:

python
import scrapy
+
+
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['https://www.qiushibaike.com/text/']
+
+    # 用作数据解析:response参数表示的是请求成功后的响应对象
+    def parse(self, response):
+        # 解析:作者的名称+段子内容
+        div_list = response.xpath('//div[@id="content"]/div/div[2]/div')
+        data_list = []
+        for div in div_list:
+            # extract()方法可以提取Selector对象中的data参数字符串
+            # extract_first()提取的是list数组里面的第一个字符串,
+            author = div.xpath('./div[1]/a[2]/h2/text()').extract_first()
+            # 列表调用了extract()表示将每一个Selector对象中的data字符串提取出来
+            content = ''.join(div.xpath('./a[1]/div[1]/span//text()').extract())
+            dict = {
+                'author':author,
+                'content':content
+            }
+            data_list.append(dict)
+        return data_list
import scrapy
+
+
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['https://www.qiushibaike.com/text/']
+
+    # 用作数据解析:response参数表示的是请求成功后的响应对象
+    def parse(self, response):
+        # 解析:作者的名称+段子内容
+        div_list = response.xpath('//div[@id="content"]/div/div[2]/div')
+        data_list = []
+        for div in div_list:
+            # extract()方法可以提取Selector对象中的data参数字符串
+            # extract_first()提取的是list数组里面的第一个字符串,
+            author = div.xpath('./div[1]/a[2]/h2/text()').extract_first()
+            # 列表调用了extract()表示将每一个Selector对象中的data字符串提取出来
+            content = ''.join(div.xpath('./a[1]/div[1]/span//text()').extract())
+            dict = {
+                'author':author,
+                'content':content
+            }
+            data_list.append(dict)
+        return data_list
  • 只能将parse方法返回的内容存储到本地文件中
  • 持久化存储的格式只有'json', 'jsonlines', 'jl', 'csv', 'xml', 'marshal', 'pickle'
  • 指令:scrapy crawl xxx -o path

基于管道持久化存储

流程:

  • 数据解析

  • 在item类中定义相关的属性

    • fieldName = scrapy.Field()
  • 将解析的数据封装存储到Item类型的对象中

  • 将item类型的对象提交给管道进行持久化存储的操作

  • 在管道类的process_item函数中要将其接受到的item对象中存储的数据进行持久化操作

  • 在settings.py中开启管道

代码改造

Python
import scrapy
+from story_spider.items import StorySpiderItem
+
+
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['https://www.qiushibaike.com/text/']
+
+    # 用作数据解析:response参数表示的是请求成功后的响应对象
+    def parse(self, response):
+        # 解析:作者的名称+段子内容
+        div_list = response.xpath('//div[@id="content"]/div/div[2]/div')
+        data_list = []
+        for div in div_list:
+            # extract()方法可以提取Selector对象中的data参数字符串
+            # extract_first()提取的是list数组里面的第一个字符串,
+            author = div.xpath('./div[1]/a[2]/h2/text()').extract_first()
+            # 列表调用了extract()表示将每一个Selector对象中的data字符串提取出来
+            content = ''.join(div.xpath('./a[1]/div[1]/span//text()').extract())
+            item = StorySpiderItem()
+            item['author'] = author
+            item['content'] = content
+            yield item
import scrapy
+from story_spider.items import StorySpiderItem
+
+
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['https://www.qiushibaike.com/text/']
+
+    # 用作数据解析:response参数表示的是请求成功后的响应对象
+    def parse(self, response):
+        # 解析:作者的名称+段子内容
+        div_list = response.xpath('//div[@id="content"]/div/div[2]/div')
+        data_list = []
+        for div in div_list:
+            # extract()方法可以提取Selector对象中的data参数字符串
+            # extract_first()提取的是list数组里面的第一个字符串,
+            author = div.xpath('./div[1]/a[2]/h2/text()').extract_first()
+            # 列表调用了extract()表示将每一个Selector对象中的data字符串提取出来
+            content = ''.join(div.xpath('./a[1]/div[1]/span//text()').extract())
+            item = StorySpiderItem()
+            item['author'] = author
+            item['content'] = content
+            yield item

items模块

python
import scrapy
+
+
+class StorySpiderItem(scrapy.Item):
+    # define the fields for your item here like:
+    # name = scrapy.Field()
+    author = scrapy.Field()
+    content = scrapy.Field()
import scrapy
+
+
+class StorySpiderItem(scrapy.Item):
+    # define the fields for your item here like:
+    # name = scrapy.Field()
+    author = scrapy.Field()
+    content = scrapy.Field()

pipelines模块

python
class StorySpiderPipeline:
+    fp = None
+
+    # open_spider方法只会在爬虫开始时调用一次,可以用于数据初始化操作
+    def open_spider(self, spider):
+        print('开始执行爬虫...')
+        self.fp = open('./qiubai.txt', 'w', encoding='utf-8')
+
+    # close_spider会在结束时调用一次
+    def close_spider(self, spider):
+        print('爬虫执行结束...')
+        self.fp.close()
+
+    # 专门用来处理item对象
+    # 该方法可以接收爬虫文件提交的item对象
+    def process_item(self, item, spider):
+        author = item['author']
+        content = item['content']
+        self.fp.write(author + ':' + content + '\n')
+        return item
class StorySpiderPipeline:
+    fp = None
+
+    # open_spider方法只会在爬虫开始时调用一次,可以用于数据初始化操作
+    def open_spider(self, spider):
+        print('开始执行爬虫...')
+        self.fp = open('./qiubai.txt', 'w', encoding='utf-8')
+
+    # close_spider会在结束时调用一次
+    def close_spider(self, spider):
+        print('爬虫执行结束...')
+        self.fp.close()
+
+    # 专门用来处理item对象
+    # 该方法可以接收爬虫文件提交的item对象
+    def process_item(self, item, spider):
+        author = item['author']
+        content = item['content']
+        self.fp.write(author + ':' + content + '\n')
+        return item

TIP

return item可以使item继续传递到下一个即将被执行的管道类中,以此可以实现多个管道类的操作,比如一份数据持久化到文件,一份数据持久化到数据库

配置文件中开启管道

settings.py中修改

python
# Configure item pipelines
+# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
+ITEM_PIPELINES = {
+    # 300表示优先级,数值越小,优先级越高
+   'story_spider.pipelines.StorySpiderPipeline': 300,
+}
# Configure item pipelines
+# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
+ITEM_PIPELINES = {
+    # 300表示优先级,数值越小,优先级越高
+   'story_spider.pipelines.StorySpiderPipeline': 300,
+}

执行爬虫

python
scrapy crawl storyxc
+开始执行爬虫...
+爬虫执行结束...
+目录下生成了qiubai.txt
scrapy crawl storyxc
+开始执行爬虫...
+爬虫执行结束...
+目录下生成了qiubai.txt
+ + + + \ No newline at end of file diff --git "a/python/crawler/scrapy\350\277\233\351\230\266.html" "b/python/crawler/scrapy\350\277\233\351\230\266.html" new file mode 100644 index 000000000..1a52a2963 --- /dev/null +++ "b/python/crawler/scrapy\350\277\233\351\230\266.html" @@ -0,0 +1,497 @@ + + + + + + scrapy进阶 | 故事 + + + + + + + + + + + + + + +
Skip to content

scrapy进阶

全站数据爬取

python
import scrapy
+from story_spider.items import StorySpiderItem
+
+
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['http://www.521609.com/meinvxiaohua/']
+    url_template = 'http://www.521609.com/meinvxiaohua/list12%d.html'
+    page_num = 2
+
+    def parse(self, response):
+        li_list = response.xpath('//div[@id="content"]/div[2]/div[2]/ul/li')
+        for li in li_list:
+            img_name = li.xpath('./a[2]//text()').extract_first()
+            print(img_name)
+        if self.page_num <= 11:
+            next_url = format(self.url_template % self.page_num)
+            self.page_num += 1
+            # 手动请求发送:yield scrapy.Request(url,callback)
+            # callback专门用作数据解析
+            yield scrapy.Request(url=next_url, callback=self.parse)
import scrapy
+from story_spider.items import StorySpiderItem
+
+
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['http://www.521609.com/meinvxiaohua/']
+    url_template = 'http://www.521609.com/meinvxiaohua/list12%d.html'
+    page_num = 2
+
+    def parse(self, response):
+        li_list = response.xpath('//div[@id="content"]/div[2]/div[2]/ul/li')
+        for li in li_list:
+            img_name = li.xpath('./a[2]//text()').extract_first()
+            print(img_name)
+        if self.page_num <= 11:
+            next_url = format(self.url_template % self.page_num)
+            self.page_num += 1
+            # 手动请求发送:yield scrapy.Request(url,callback)
+            # callback专门用作数据解析
+            yield scrapy.Request(url=next_url, callback=self.parse)

五大核心组件

  • 引擎
  • 调度器
  • 下载器
  • Spider
  • 管道

流程:

  • spider中产生url,对url进行请求发送

  • url会被封装成请求对象交给引擎,引擎把请求给调度器

  • 调度器会使用过滤器将引擎提交的请求去重,将去重后的请求对象放入队列

  • 调度器会把请求对象从队列中调度给引擎,引擎把请求交给下载器

  • 下载器去互联网中进行数据下载,将数据封装在response里返回给引擎

  • 引擎将response返回给spider,spider对数据进行解析,将数据封装到item当中,交给引擎

  • 引擎把item交给管道

  • 管道进行持久化存储

gzuo

请求传参

  • 使用场景:如果爬取解析的数据不再同一张页面中(深度爬取)

  • yield scrapy.Request(url,callback,meta= {'item':item})

    • 请求传参 item可以传递给callback回调函数

图片爬取之ImagePipeline

  • 字符串持久化:xpath解析交给管道持久化
  • 图片持久化:xpath解析出src属性,单独对图片地址发起请求获取图片二进制类型数据

ImagePipeline

只需要解析出img的src属性进行解析并提交到管道,管道就会对图片的src进行请求发送获取二进制数据

爬虫文件

python
import scrapy
+from story_spider.items import StorySpiderItem
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['http://sc.chinaz.com/tupian/']
+
+    def parse(self, response):
+        div_list = response.xpath('//div[@id="container"]/div')
+        for div in div_list:
+            # 该网站有懒加载,要使用伪属性
+            src = div.xpath('./div/a/img/@src2').extract_first()
+            real_src = 'https:' + src
+            # print(real_src)
+            item = StorySpiderItem()
+            item['src'] = real_src
+            yield item
import scrapy
+from story_spider.items import StorySpiderItem
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['http://sc.chinaz.com/tupian/']
+
+    def parse(self, response):
+        div_list = response.xpath('//div[@id="container"]/div')
+        for div in div_list:
+            # 该网站有懒加载,要使用伪属性
+            src = div.xpath('./div/a/img/@src2').extract_first()
+            real_src = 'https:' + src
+            # print(real_src)
+            item = StorySpiderItem()
+            item['src'] = real_src
+            yield item

Items

python
# Define here the models for your scraped items
+#
+# See documentation in:
+# https://docs.scrapy.org/en/latest/topics/items.html
+
+import scrapy
+
+
+class StorySpiderItem(scrapy.Item):
+    # define the fields for your item here like:
+    # name = scrapy.Field()
+    src = scrapy.Field()
# Define here the models for your scraped items
+#
+# See documentation in:
+# https://docs.scrapy.org/en/latest/topics/items.html
+
+import scrapy
+
+
+class StorySpiderItem(scrapy.Item):
+    # define the fields for your item here like:
+    # name = scrapy.Field()
+    src = scrapy.Field()

Pipeline

# Define your item pipelines here
+#
+# Don't forget to add your pipeline to the ITEM_PIPELINES setting
+# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
+
+
+# useful for handling different item types with a single interface
+
+from scrapy.pipelines.images import ImagesPipeline
+import scrapy
+
+
+class ImagePipeline(ImagesPipeline):
+    # 根据图片地址进行图片数据的请求
+    def get_media_requests(self, item, info):
+        print(item['src'])
+        yield scrapy.Request(item['src'])
+
+    def file_path(self, request, response=None, info=None, *, item=None):
+        # 指定图片存储路径
+        imageName = request.url.split('/')[-1]
+        return imageName
+
+    def item_completed(self, results, item, info):
+        return item  # 返回给下一个被执行的管道类
# Define your item pipelines here
+#
+# Don't forget to add your pipeline to the ITEM_PIPELINES setting
+# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
+
+
+# useful for handling different item types with a single interface
+
+from scrapy.pipelines.images import ImagesPipeline
+import scrapy
+
+
+class ImagePipeline(ImagesPipeline):
+    # 根据图片地址进行图片数据的请求
+    def get_media_requests(self, item, info):
+        print(item['src'])
+        yield scrapy.Request(item['src'])
+
+    def file_path(self, request, response=None, info=None, *, item=None):
+        # 指定图片存储路径
+        imageName = request.url.split('/')[-1]
+        return imageName
+
+    def item_completed(self, results, item, info):
+        return item  # 返回给下一个被执行的管道类

settings

# 保存的文件夹
+IMAGES_STORE = './imgs'
+# 启用管道
+ITEM_PIPELINES = {
+    # 300表示优先级,数值越小,优先级越高
+   'story_spider.pipelines.ImagePipeline': 300
+}
# 保存的文件夹
+IMAGES_STORE = './imgs'
+# 启用管道
+ITEM_PIPELINES = {
+    # 300表示优先级,数值越小,优先级越高
+   'story_spider.pipelines.ImagePipeline': 300
+}

DANGER

这里直接运行不会报错,但是会发现也没有下载成功,但是实际上url已经能拿到了,把日志的级别放开后,查看日志信息会发现有一句 2021-05-01 13:22:48 [scrapy.middleware] WARNING: Disabled ImgsPipeline: ImagesPipeline requires installing Pillow 4.0.0 or later

提示使用ImagesPipeline还需要安装下pillow :pip install pillow

这个很坑,不仔细看找不到,排查了半天才解决

安装完pillow后启动爬虫,可以看到图片已经下载完成

中间件

  • 下载中间件
    • 位置:引擎和下载器之间
    • 作用:批量拦截到整个工程中所有的请求和响应
    • 拦截请求:
      • UA伪装
      • 代理IP设置
    • 拦截响应:
      • 篡改响应数据,响应对象
    • 核心方法:
      • process_request:拦截请求
      • process_response:拦截响应
      • processs_exception:拦截发生异常的请求
  • 爬虫中间件
    • 位置:在引擎及爬虫
    • 作用:处理spider的输入(response)和输出(item及requests).

下载中间件

python
def process_request(self, request, spider):
+        # UA 伪装,也可以设置ua池,随机设置
+        request.headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+
+        # 设置代理
+        request.meta['proxy'] = 'https://ip:port'
+        return None
+
+    def process_response(self, request, response, spider):
+        # Called with the response returned from the downloader.
+
+        # Must either;
+        # - return a Response object
+        # - return a Request object
+        # - or raise IgnoreRequest
+        return response
+
+    def process_exception(self, request, exception, spider):
+
+        # Called when a download handler or a process_request()
+        # (from other downloader middleware) raises an exception.
+        # 发生异常的请求切换代理 也可以实现代理池,指定切换逻辑
+        request.meta['proxy'] = 'https://ip:port'
+
+        # Must either:
+        # - return None: continue processing this exception
+        # - return a Response object: stops process_exception() chain
+        # - return a Request object: stops process_exception() chain
+        return request #将修正后的request重新进行发送
def process_request(self, request, spider):
+        # UA 伪装,也可以设置ua池,随机设置
+        request.headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+
+        # 设置代理
+        request.meta['proxy'] = 'https://ip:port'
+        return None
+
+    def process_response(self, request, response, spider):
+        # Called with the response returned from the downloader.
+
+        # Must either;
+        # - return a Response object
+        # - return a Request object
+        # - or raise IgnoreRequest
+        return response
+
+    def process_exception(self, request, exception, spider):
+
+        # Called when a download handler or a process_request()
+        # (from other downloader middleware) raises an exception.
+        # 发生异常的请求切换代理 也可以实现代理池,指定切换逻辑
+        request.meta['proxy'] = 'https://ip:port'
+
+        # Must either:
+        # - return None: continue processing this exception
+        # - return a Response object: stops process_exception() chain
+        # - return a Request object: stops process_exception() chain
+        return request #将修正后的request重新进行发送

案例:爬取网易新闻指定分类下的新闻标题和内容

爬虫类

python
import scrapy
+from story_spider.items import StorySpiderItem
+
+
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['https://news.163.com/']
+    module_urls = []
+
+    def __init__(self):
+        super().__init__(self)
+        from selenium import webdriver
+        self.browser = webdriver.Chrome()
+
+    def parse(self, response):
+
+        li_list = response.xpath('//*[@id="index2016_wrap"]/div[1]/div[2]/div[2]/div[2]/div[2]/div/ul/li')
+        need_index = [3, 4, 6]
+        for index in need_index:
+            module_url = li_list[index].xpath('./a/@href').extract_first()
+            self.module_urls.append(module_url)
+
+        for url in self.module_urls:
+            yield scrapy.Request(url, callback=self.parse_module)
+
+    #  解析篡改过的 已经添加了动态加载数据的响应信息
+    def parse_module(self, response):
+        div_list = response.xpath('/html/body/div/div[3]/div[4]/div[1]/div[1]/div/ul/li/div/div')
+        for div in div_list:
+            # news_title = div.xpath('./div/div[1]/h3/a/text()').extract_first()
+            detail_url = div.xpath('./div/div[1]/h3/a/@href').extract_first()
+
+            yield scrapy.Request(url=detail_url,callback=self.parse_detail)
+
+
+    # 解析新闻详情页
+    def parse_detail(self, response):
+        title = response.xpath('//*[@id="container"]/div[1]/h1/text()').extract_first()
+        detail_text = response.xpath('//*[@id="content"]/div[2]//text()').extract()
+        detail_text = ''.join(detail_text)
+        item = StorySpiderItem()
+        item['title'] = title
+        item['content'] = detail_text
+        yield item
+
+    def closed(self,spider):
+        self.browser.quit()
import scrapy
+from story_spider.items import StorySpiderItem
+
+
+# 必须继承scrapy.Spider
+class StoryxcSpider(scrapy.Spider):
+    # 爬虫文件的名称:爬虫源文件的一个唯一标识
+    name = 'storyxc'
+    # 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
+    # allowed_domains = ['storyxc.com']
+    # 起始的url列表,该列表中存放的url会被scrapy自动进行请求的发送
+    start_urls = ['https://news.163.com/']
+    module_urls = []
+
+    def __init__(self):
+        super().__init__(self)
+        from selenium import webdriver
+        self.browser = webdriver.Chrome()
+
+    def parse(self, response):
+
+        li_list = response.xpath('//*[@id="index2016_wrap"]/div[1]/div[2]/div[2]/div[2]/div[2]/div/ul/li')
+        need_index = [3, 4, 6]
+        for index in need_index:
+            module_url = li_list[index].xpath('./a/@href').extract_first()
+            self.module_urls.append(module_url)
+
+        for url in self.module_urls:
+            yield scrapy.Request(url, callback=self.parse_module)
+
+    #  解析篡改过的 已经添加了动态加载数据的响应信息
+    def parse_module(self, response):
+        div_list = response.xpath('/html/body/div/div[3]/div[4]/div[1]/div[1]/div/ul/li/div/div')
+        for div in div_list:
+            # news_title = div.xpath('./div/div[1]/h3/a/text()').extract_first()
+            detail_url = div.xpath('./div/div[1]/h3/a/@href').extract_first()
+
+            yield scrapy.Request(url=detail_url,callback=self.parse_detail)
+
+
+    # 解析新闻详情页
+    def parse_detail(self, response):
+        title = response.xpath('//*[@id="container"]/div[1]/h1/text()').extract_first()
+        detail_text = response.xpath('//*[@id="content"]/div[2]//text()').extract()
+        detail_text = ''.join(detail_text)
+        item = StorySpiderItem()
+        item['title'] = title
+        item['content'] = detail_text
+        yield item
+
+    def closed(self,spider):
+        self.browser.quit()

中间件类

只展示了下载中间件

python
class StorySpiderDownloaderMiddleware:
+
+    def process_request(self, request, spider):
+
+        return None
+
+    # 拦截模块对应的响应对象,进行篡改
+    # 由于是动态加载的内容,使用selenium
+    def process_response(self, request, response, spider):
+        # 过滤指定的响应对象
+        urls = spider.module_urls
+        bro = spider.browser
+        from scrapy.http import HtmlResponse
+        from time import sleep
+        # 只有指定模块url的数据才使用selenium请求并进行篡改数据
+        if request.url in urls:
+            bro.get(request.url) # selenium请求详情页
+            sleep(3)
+            page_data = bro.page_source # 包含了动态加载的新闻数据
+            # 要爬取的指定模块的响应内容
+            # 实例化一个新的响应对象 (包含动态加载的新闻数据),替代原来的响应对象
+            new_res = HtmlResponse(url=request.url,body=page_data,encoding='utf-8',request=request)
+            return new_res
+
+        return response
+
+    def process_exception(self, request, exception, spider):
+
+        return request #将修正后的request重新进行发送
class StorySpiderDownloaderMiddleware:
+
+    def process_request(self, request, spider):
+
+        return None
+
+    # 拦截模块对应的响应对象,进行篡改
+    # 由于是动态加载的内容,使用selenium
+    def process_response(self, request, response, spider):
+        # 过滤指定的响应对象
+        urls = spider.module_urls
+        bro = spider.browser
+        from scrapy.http import HtmlResponse
+        from time import sleep
+        # 只有指定模块url的数据才使用selenium请求并进行篡改数据
+        if request.url in urls:
+            bro.get(request.url) # selenium请求详情页
+            sleep(3)
+            page_data = bro.page_source # 包含了动态加载的新闻数据
+            # 要爬取的指定模块的响应内容
+            # 实例化一个新的响应对象 (包含动态加载的新闻数据),替代原来的响应对象
+            new_res = HtmlResponse(url=request.url,body=page_data,encoding='utf-8',request=request)
+            return new_res
+
+        return response
+
+    def process_exception(self, request, exception, spider):
+
+        return request #将修正后的request重新进行发送

Item类

python
import scrapy
+
+
+class StorySpiderItem(scrapy.Item):
+    # define the fields for your item here like:
+    # name = scrapy.Field()
+    title = scrapy.Field()
+    content = scrapy.Field()
import scrapy
+
+
+class StorySpiderItem(scrapy.Item):
+    # define the fields for your item here like:
+    # name = scrapy.Field()
+    title = scrapy.Field()
+    content = scrapy.Field()

Pipeline类

python
class StroyxcPipeline(object):
+    fp = None
+
+    def open_spider(self, spider):
+        self.fp = open('./163news.txt', 'w', encoding='utf-8')
+
+    def process_item(self, item, spider):
+        self.fp.write(item['title'] + ':' + item['content'] + '\n')
+        return item
+
+    def close_spider(self, spider):
+        self.fp.close()
class StroyxcPipeline(object):
+    fp = None
+
+    def open_spider(self, spider):
+        self.fp = open('./163news.txt', 'w', encoding='utf-8')
+
+    def process_item(self, item, spider):
+        self.fp.write(item['title'] + ':' + item['content'] + '\n')
+        return item
+
+    def close_spider(self, spider):
+        self.fp.close()

settings配置

python
BOT_NAME = 'story_spider'
+
+SPIDER_MODULES = ['story_spider.spiders']
+NEWSPIDER_MODULE = 'story_spider.spiders'
+
+# Crawl responsibly by identifying yourself (and your website) on the user-agent
+USER_AGENT = {
+    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36'
+}
+
+# Obey robots.txt rules
+ROBOTSTXT_OBEY = False
+
+
+# Enable or disable downloader middlewares
+# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
+DOWNLOADER_MIDDLEWARES = {
+   'story_spider.middlewares.StorySpiderDownloaderMiddleware': 543,
+}
+
+# Configure item pipelines
+# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
+ITEM_PIPELINES = {
+    # 300表示优先级,数值越小,优先级越高
+   'story_spider.pipelines.StroyxcPipeline': 300
+}
BOT_NAME = 'story_spider'
+
+SPIDER_MODULES = ['story_spider.spiders']
+NEWSPIDER_MODULE = 'story_spider.spiders'
+
+# Crawl responsibly by identifying yourself (and your website) on the user-agent
+USER_AGENT = {
+    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36'
+}
+
+# Obey robots.txt rules
+ROBOTSTXT_OBEY = False
+
+
+# Enable or disable downloader middlewares
+# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
+DOWNLOADER_MIDDLEWARES = {
+   'story_spider.middlewares.StorySpiderDownloaderMiddleware': 543,
+}
+
+# Configure item pipelines
+# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
+ITEM_PIPELINES = {
+    # 300表示优先级,数值越小,优先级越高
+   'story_spider.pipelines.StroyxcPipeline': 300
+}

运行结果:

image-20210501172946555

scrapy调试

  • pycharm中编辑运行/debug配置

  • 点击加号添加一个新的配置,选择python,给配置命个名,比如scrapy

  • script path选择python目录下的Lib/site-packages/scrapycmdline.py

  • parameter填crawl yourSpiderName

  • working directory填写爬虫项目路径

  • 保存,再debug运行scrapy这个配置就行

例如:

image-20210502101609233

+ + + + \ No newline at end of file diff --git "a/python/crawler/selenium\346\250\241\345\235\227.html" "b/python/crawler/selenium\346\250\241\345\235\227.html" new file mode 100644 index 000000000..0f3bd57ea --- /dev/null +++ "b/python/crawler/selenium\346\250\241\345\235\227.html" @@ -0,0 +1,399 @@ + + + + + + selenium模块 | 故事 + + + + + + + + + + + + + + +
Skip to content

selenium模块

selenium是一个用于web应用程序测试的工具,selenium测试直接运行在浏览器中,就像真正的用户在操作一样

selenium在爬虫中的应用:

  • 便捷的获取网站动态加载的数据
  • 便捷的实现模拟登录

selenium模块:

  • 基于浏览器自动化的一个模块

使用流程

  • 环境安装:pip install selenium
  • 下载浏览器驱动程序

http://chromedriver.storage.googleapis.com/index.html 驱动程序

  • 实例化浏览器对象
  • 编写基于浏览器对象操作的代码
python
from selenium import webdriver
+from lxml import etree
+import time
+
+# 实例化一个浏览器对象
+browser = webdriver.Chrome(executable_path=r'D:\install\tools\chromedriver\chromedriver.exe')
+# 访问url
+browser.get('https://movie.douban.com/typerank?type_name=%E5%89%A7%E6%83%85&type=11&interval_id=100:90&action=')
+# 获取页面源码内容
+page_text = browser.page_source
+
+tree = etree.HTML(page_text)
+span_list = tree.xpath('//div[@class="movie-content"]/div[1]/div[1]/span[1]')
+for span in span_list:
+    name = span.xpath('.//a/text()')
+    print(name)
+time.sleep(5)
+# 退出
+browser.quit()
+    
+res:
+['肖申克的救赎']
+['霸王别姬']
+['控方证人']
+['伊丽莎白']
+['阿甘正传']
+['美丽人生']
+['辛德勒的名单']
+['茶馆']
+['控方证人']
+['十二怒汉(电视版)']
+['这个杀手不太冷']
+['千与千寻']
+['泰坦尼克号']
+['忠犬八公的故事']
+['十二怒汉']
+['泰坦尼克号 3D版']
+['背靠背,脸对脸']
+['灿烂人生']
+['横空出世']
+['遥望南方的童年']
from selenium import webdriver
+from lxml import etree
+import time
+
+# 实例化一个浏览器对象
+browser = webdriver.Chrome(executable_path=r'D:\install\tools\chromedriver\chromedriver.exe')
+# 访问url
+browser.get('https://movie.douban.com/typerank?type_name=%E5%89%A7%E6%83%85&type=11&interval_id=100:90&action=')
+# 获取页面源码内容
+page_text = browser.page_source
+
+tree = etree.HTML(page_text)
+span_list = tree.xpath('//div[@class="movie-content"]/div[1]/div[1]/span[1]')
+for span in span_list:
+    name = span.xpath('.//a/text()')
+    print(name)
+time.sleep(5)
+# 退出
+browser.quit()
+    
+res:
+['肖申克的救赎']
+['霸王别姬']
+['控方证人']
+['伊丽莎白']
+['阿甘正传']
+['美丽人生']
+['辛德勒的名单']
+['茶馆']
+['控方证人']
+['十二怒汉(电视版)']
+['这个杀手不太冷']
+['千与千寻']
+['泰坦尼克号']
+['忠犬八公的故事']
+['十二怒汉']
+['泰坦尼克号 3D版']
+['背靠背,脸对脸']
+['灿烂人生']
+['横空出世']
+['遥望南方的童年']

简单操作

python
from selenium import webdriver
+from time import sleep
+
+bro = webdriver.Chrome(executable_path=r'D:\install\tools\chromedriver\chromedriver.exe')
+bro.get('https://www.taobao.com/')
+# 找到搜索框
+input_ = bro.find_element_by_id('q')
+# 输入值
+input_.send_keys('macbook')
+# 执行js
+bro.execute_script('window.scrollTo(0,document.body.scrollHeight)')
+sleep(2)
+# 点击搜索按钮
+btn = bro.find_element_by_xpath('//*[@id="J_TSearchForm"]/div[1]/button')
+btn.click()
+
+sleep(2)
+
+bro.get('https://www.baidu.com')
+sleep(1)
+# 后退
+bro.back()
+
+sleep(1)
+# 前进
+bro.forward()
+sleep(1)
+bro.quit()
from selenium import webdriver
+from time import sleep
+
+bro = webdriver.Chrome(executable_path=r'D:\install\tools\chromedriver\chromedriver.exe')
+bro.get('https://www.taobao.com/')
+# 找到搜索框
+input_ = bro.find_element_by_id('q')
+# 输入值
+input_.send_keys('macbook')
+# 执行js
+bro.execute_script('window.scrollTo(0,document.body.scrollHeight)')
+sleep(2)
+# 点击搜索按钮
+btn = bro.find_element_by_xpath('//*[@id="J_TSearchForm"]/div[1]/button')
+btn.click()
+
+sleep(2)
+
+bro.get('https://www.baidu.com')
+sleep(1)
+# 后退
+bro.back()
+
+sleep(1)
+# 前进
+bro.forward()
+sleep(1)
+bro.quit()

iframe和动作链

  • 如果定位的标签存在于iframe中,则必须使用switch_to.frame(id)
  • 动作链: from selenium.webdriver import ActionChains
    • 实例化: `action = ActionChains(browser)
    • click_and_hold(div)长按点击操作
    • move_by_offset(x,y)
    • perform() 让动作链立即执行
    • action.release() 释放动作链

qq空间模拟登录

python
from selenium import webdriver
+import time
+
+url = 'https://qzone.qq.com/'
+browser = webdriver.Chrome()
+browser.get(url)
+browser.switch_to.frame('login_frame')
+btn = browser.find_element_by_id('switcher_plogin')
+btn.click()
+uname_input = browser.find_element_by_id('u')
+pwd_input = browser.find_element_by_id('p')
+uname_input.send_keys('1234')
+pwd_input.send_keys('123411234')
+login_btn = browser.find_element_by_id('login_button')
+login_btn.click()
+time.sleep(5)
+browser.quit()
from selenium import webdriver
+import time
+
+url = 'https://qzone.qq.com/'
+browser = webdriver.Chrome()
+browser.get(url)
+browser.switch_to.frame('login_frame')
+btn = browser.find_element_by_id('switcher_plogin')
+btn.click()
+uname_input = browser.find_element_by_id('u')
+pwd_input = browser.find_element_by_id('p')
+uname_input.send_keys('1234')
+pwd_input.send_keys('123411234')
+login_btn = browser.find_element_by_id('login_button')
+login_btn.click()
+time.sleep(5)
+browser.quit()

无头浏览器+规避检测

python
from selenium import webdriver
+from selenium.webdriver.chrome.options import Options
+from selenium.webdriver import ChromeOptions
+from time import sleep
+
+# 实现无可视化节目(无头浏览器)
+chrome_options = Options()
+chrome_options.add_argument('--headless')
+chrome_options.add_argument('--disable-gpu')
+# 实现规避检测
+option = ChromeOptions()
+option.add_experimental_option('excludeSwitches', ['enable-automation'])
+bro = webdriver.Chrome(chrome_options=chrome_options, options=option)
+bro.get('https://www.baidu.com')
+print(bro.page_source)
+sleep(2)
+bro.quit()
from selenium import webdriver
+from selenium.webdriver.chrome.options import Options
+from selenium.webdriver import ChromeOptions
+from time import sleep
+
+# 实现无可视化节目(无头浏览器)
+chrome_options = Options()
+chrome_options.add_argument('--headless')
+chrome_options.add_argument('--disable-gpu')
+# 实现规避检测
+option = ChromeOptions()
+option.add_experimental_option('excludeSwitches', ['enable-automation'])
+bro = webdriver.Chrome(chrome_options=chrome_options, options=option)
+bro.get('https://www.baidu.com')
+print(bro.page_source)
+sleep(2)
+bro.quit()

12306模拟登录

python
from selenium import webdriver
+from time import sleep
+from story.code import StoryClient
+from PIL import Image
+from selenium.webdriver import ActionChains
+from selenium.webdriver import ChromeOptions
+
+option = ChromeOptions()
+option.add_experimental_option('excludeSwitches', ['enable-automation'])
+# 访问12306
+url = 'https://www.12306.cn/index/index.html'
+bro = webdriver.Chrome(options=option)
+bro.get(url)
+script = 'Object.defineProperty(navigator,"webdriver",{get:()=>undefined,});'
+bro.execute_script(script)
+# 点击登录标签
+sleep(5)
+a_btn = bro.find_element_by_xpath('/html/body/div[2]/div/div[1]/div/div/ul/li[5]/a[1]')
+a_btn.click()
+sleep(2)
+# 点击账号登录按钮
+account_login = bro.find_element_by_xpath('/html/body/div[2]/div[2]/ul/li[2]/a')
+account_login.click()
+sleep(5)
+# 保存页面截图
+bro.save_screenshot('page.png')
+# 确定验证码图片对应的左上角和右下角的坐标,进行裁剪
+code_element = bro.find_element_by_id('J-loginImgArea')
+location = code_element.location  # 左上角坐标(x,y)
+size = code_element.size  # 验证码图片的长和宽
+rangle = (
+int(location['x']), int(location['y']), int(location['x'] + size['width']), int(location['y'] + size['height']))
+# 截取验证码图片
+image = Image.open('./page.png')
+frame = image.crop(rangle)
+print(rangle)
+frame.save('./code.png')
+# 超级鹰识别验证码
+client = StoryClient()
+im = open('./code.png', 'rb').read()
+res = client.post_pic(im, 9004)['pic_str']
+print(res)
+# 输入用户名和密码
+uname_input = bro.find_element_by_id('J-userName')
+pwd_input = bro.find_element_by_id('J-password')
+uname_input.send_keys('aaaaaaaaaaaa')
+pwd_input.send_keys('bbbbbbbbbbb')
+sleep(5)
+# 处理识别结果
+all_position_list = []  # 即将被点击的坐标
+if '|' in res:
+    list_1 = res.split('|')
+    count_1 = len(list_1)
+    for i in range(count_1):
+        xy_list = []
+        x = int(list_1[i].split(',')[0])
+        y = int(list_1[i].split(',')[1])
+        xy_list.append(x)
+        xy_list.append(y)
+        all_position_list.append(xy_list)
+else:
+    x = int(res.split(',')[0])
+    y = int(res.split(',')[1])
+    xy_list = []
+    xy_list.append(x)
+    xy_list.append(y)
+    all_position_list.append(xy_list)
+
+print(all_position_list)
+# 使用动作链点击验证码
+for l in all_position_list:
+    x = l[0]
+    y = l[1]
+    # 参照物是截取的验证码区域
+    ActionChains(bro).move_to_element_with_offset(code_element, x, y).click().perform()
+sleep(5)
+login_btn = bro.find_element_by_id('J-login')
+login_btn.click()
+sleep(2)
+#滑动验证码
+span = bro.find_element_by_xpath('//*[@id="nc_1_n1z"]')
+# 对div_tag进行滑动操作
+action = ActionChains(bro)
+action.click_and_hold(span).perform()
+action.drag_and_drop_by_offset(span, 400, 0).perform()
+action.release()
+
+sleep(10)
+bro.quit()
from selenium import webdriver
+from time import sleep
+from story.code import StoryClient
+from PIL import Image
+from selenium.webdriver import ActionChains
+from selenium.webdriver import ChromeOptions
+
+option = ChromeOptions()
+option.add_experimental_option('excludeSwitches', ['enable-automation'])
+# 访问12306
+url = 'https://www.12306.cn/index/index.html'
+bro = webdriver.Chrome(options=option)
+bro.get(url)
+script = 'Object.defineProperty(navigator,"webdriver",{get:()=>undefined,});'
+bro.execute_script(script)
+# 点击登录标签
+sleep(5)
+a_btn = bro.find_element_by_xpath('/html/body/div[2]/div/div[1]/div/div/ul/li[5]/a[1]')
+a_btn.click()
+sleep(2)
+# 点击账号登录按钮
+account_login = bro.find_element_by_xpath('/html/body/div[2]/div[2]/ul/li[2]/a')
+account_login.click()
+sleep(5)
+# 保存页面截图
+bro.save_screenshot('page.png')
+# 确定验证码图片对应的左上角和右下角的坐标,进行裁剪
+code_element = bro.find_element_by_id('J-loginImgArea')
+location = code_element.location  # 左上角坐标(x,y)
+size = code_element.size  # 验证码图片的长和宽
+rangle = (
+int(location['x']), int(location['y']), int(location['x'] + size['width']), int(location['y'] + size['height']))
+# 截取验证码图片
+image = Image.open('./page.png')
+frame = image.crop(rangle)
+print(rangle)
+frame.save('./code.png')
+# 超级鹰识别验证码
+client = StoryClient()
+im = open('./code.png', 'rb').read()
+res = client.post_pic(im, 9004)['pic_str']
+print(res)
+# 输入用户名和密码
+uname_input = bro.find_element_by_id('J-userName')
+pwd_input = bro.find_element_by_id('J-password')
+uname_input.send_keys('aaaaaaaaaaaa')
+pwd_input.send_keys('bbbbbbbbbbb')
+sleep(5)
+# 处理识别结果
+all_position_list = []  # 即将被点击的坐标
+if '|' in res:
+    list_1 = res.split('|')
+    count_1 = len(list_1)
+    for i in range(count_1):
+        xy_list = []
+        x = int(list_1[i].split(',')[0])
+        y = int(list_1[i].split(',')[1])
+        xy_list.append(x)
+        xy_list.append(y)
+        all_position_list.append(xy_list)
+else:
+    x = int(res.split(',')[0])
+    y = int(res.split(',')[1])
+    xy_list = []
+    xy_list.append(x)
+    xy_list.append(y)
+    all_position_list.append(xy_list)
+
+print(all_position_list)
+# 使用动作链点击验证码
+for l in all_position_list:
+    x = l[0]
+    y = l[1]
+    # 参照物是截取的验证码区域
+    ActionChains(bro).move_to_element_with_offset(code_element, x, y).click().perform()
+sleep(5)
+login_btn = bro.find_element_by_id('J-login')
+login_btn.click()
+sleep(2)
+#滑动验证码
+span = bro.find_element_by_xpath('//*[@id="nc_1_n1z"]')
+# 对div_tag进行滑动操作
+action = ActionChains(bro)
+action.click_and_hold(span).perform()
+action.drag_and_drop_by_offset(span, 400, 0).perform()
+action.release()
+
+sleep(10)
+bro.quit()

最后一步滑块验证码无法通过,还需要优化

+ + + + \ No newline at end of file diff --git "a/python/crawler/\345\210\206\345\270\203\345\274\217\347\210\254\350\231\253\345\222\214\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253.html" "b/python/crawler/\345\210\206\345\270\203\345\274\217\347\210\254\350\231\253\345\222\214\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253.html" new file mode 100644 index 000000000..1c67c34e5 --- /dev/null +++ "b/python/crawler/\345\210\206\345\270\203\345\274\217\347\210\254\350\231\253\345\222\214\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253.html" @@ -0,0 +1,39 @@ + + + + + + 分布式爬虫和增量式爬虫 | 故事 + + + + + + + + + + + + + + +
Skip to content

分布式爬虫和增量式爬虫

分布式爬虫

概念

搭建集群,让集群对一组资源进行联合爬取

作用

提升爬取数据效率

实现

  • 安装scrapy-redis组件

  • 原生scrapy无法实现分布式爬虫

    • 调度器不可被集群共享
    • 管道不可被集群共享
  • scrapy-redis组件作用

    • 给原生scrapy提供被共享的调度器和管道
  • 实现流程

    • 创建工程

    • 创建一个基于CrawlSpider的爬虫

    • 修改爬虫文件

      • 爬虫文件添加from scrapy_redis.spiders import RedisCrawlSpider

      • 注释掉start_urls和allowed_domains

      • 新增属性redis_key = 'story',代表被共享的调度器队列的名称

      • 编写数据解析操作

      • 将当前爬虫类的父类修改成RedisCrawlSpider

    • settings配置新增

      • 指定使用可以共享的管道

        python
        ITEM_PIPELINES = {
        +    'scrapy_redis.pipelines.RedisPipeline' : 400
        +}
        ITEM_PIPELINES = {
        +    'scrapy_redis.pipelines.RedisPipeline' : 400
        +}
      • 指定可以共享的调度器

        python
        # 增加一个去重容器的配置,使用redis的set来存储请求数据,实现请求去重持久化
        +DUPEFLTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
        +# 使用scrapy-redis组件自己的调度器
        +SCHEDULER = "scrapy_redis.scheduler.Scheduler"
        +# 配置调度器是否要持久化-爬虫结束要不要清空redis请求队列和去重的set
        +SCHEDULER_PERSIST = True
        # 增加一个去重容器的配置,使用redis的set来存储请求数据,实现请求去重持久化
        +DUPEFLTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
        +# 使用scrapy-redis组件自己的调度器
        +SCHEDULER = "scrapy_redis.scheduler.Scheduler"
        +# 配置调度器是否要持久化-爬虫结束要不要清空redis请求队列和去重的set
        +SCHEDULER_PERSIST = True
    • 配置redis的配置文件

      • bind 127.0.0.1注释掉
      • 关闭保护模式:protected-mode 改为no
    • 启动redis

    • 启动工程,进入到爬虫文件的目录后scrapy runspider xxx

    • 向调度器队列中放入起始url

      • lpush redis_key url

增量式爬虫

概念

检测网站数据更新情况,只会爬取网站最新更新的数据

实现

  • 指定起始url
  • 基于CrawlSpider获取其他页码链接
  • 基于Rule对其他页码进行请求
  • 从每一个页码对应源码中解析出详情页的url
  • ==检测详情页的url是否被请求过(redis/mysql)==
  • 对详情页发起请求
  • 持久化存储
+ + + + \ No newline at end of file diff --git "a/python/crawler/\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253\345\256\236\350\267\265\346\241\210\344\276\213 \344\270\213\350\275\275\346\214\207\345\256\232b\347\253\231up\344\270\273\347\232\204\346\211\200\346\234\211\344\275\234\345\223\201.html" "b/python/crawler/\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253\345\256\236\350\267\265\346\241\210\344\276\213 \344\270\213\350\275\275\346\214\207\345\256\232b\347\253\231up\344\270\273\347\232\204\346\211\200\346\234\211\344\275\234\345\223\201.html" new file mode 100644 index 000000000..7d3b3d154 --- /dev/null +++ "b/python/crawler/\345\242\236\351\207\217\345\274\217\347\210\254\350\231\253\345\256\236\350\267\265\346\241\210\344\276\213 \344\270\213\350\275\275\346\214\207\345\256\232b\347\253\231up\344\270\273\347\232\204\346\211\200\346\234\211\344\275\234\345\223\201.html" @@ -0,0 +1,659 @@ + + + + + + 增量式爬虫实践案例 下载指定b站up主的所有作品 | 故事 + + + + + + + + + + + + + + +
Skip to content

增量式爬虫实践案例 下载指定b站up主的所有作品

背景

增量式爬取指定的up主的所有投稿作品,即实现一个增量式爬虫。

这次示范的up主是个妹子😏kototo使用了scrapy框架,主要是为了练手,不使用框架反而会更简单一些。

python模块:scrapy、selenium、requests、pymysql

其他环境:ffmpeg、mysql

创建一个项目并创建爬虫

bash
scrapy startproject kototo
+cd kototo
+scrapy genspider kototo bilibili.com
scrapy startproject kototo
+cd kototo
+scrapy genspider kototo bilibili.com

爬虫类

python
import scrapy
+from selenium import webdriver
+import re
+import json
+import requests
+import os
+from kototo.items import KototoItem
+import pymysql
+
+
+class KototoSpider(scrapy.Spider):
+    name = 'kototo'
+    start_urls = []
+
+    def __init__(self):
+        """
+        构造器,主要初始化了selenium对象并实现无头浏览器,以及
+        初始化需要爬取的url地址,因为b站的翻页是js实现的,所以要手动处理一下
+        """
+        super().__init__()
+        # 构造无头浏览器
+        from selenium.webdriver.chrome.options import Options
+        chrome_options = Options()
+        chrome_options.add_argument('--headless')
+        chrome_options.add_argument('--disable-gpu')
+        self.bro = webdriver.Chrome(chrome_options=chrome_options)
+        # 指定的up主的投稿页面,可以提到外面使用input输入
+        space_url = 'https://space.bilibili.com/17485141/video'
+        # 初始化需要爬取的列表页
+        self.init_start_urls(self.start_urls, space_url)
+        # 创建桌面文件夹
+        self.desktop_path = os.path.join(os.path.expanduser('~'), 'Desktop\\' + self.name + '\\')
+        if not os.path.exists(self.desktop_path):
+            os.mkdir(self.desktop_path)
+
+    def parse(self, response):
+        """
+        解析方法,解析列表页的视频li,拿到标题和详情页,然后主动请求详情页
+        :param response: 
+        :return: 
+        """
+        li_list = response.xpath('//*[@id="submit-video-list"]/ul[2]/li')
+        for li in li_list:
+            print(li.xpath('./a[2]/@title').extract_first())
+            print(detail_url := 'https://' + li.xpath('./a[2]/@href').extract_first()[2:])
+            yield scrapy.Request(url=detail_url, callback=self.parse_detail)
+
+    def parse_detail(self, response):
+        """
+        增量爬取: 解析详情页的音视频地址并交给管道处理
+        使用mysql实现
+        :param response: 
+        :return: 
+        """
+        title = response.xpath('//*[@id="viewbox_report"]/h1/@title').extract_first()
+        # 替换掉视频名称中无法用在文件名中或会导致cmd命令出错的字符
+        title = title.replace('-', '').replace(' ', '').replace('/', '').replace('|', '')
+        play_info_list = self.get_play_info(response)
+        # 这里使用mysql的唯一索引实现增量爬取,如果是服务器上跑也可以用redis
+        if self.insert_info(title, play_info_list[1]):
+            video_temp_path = (self.desktop_path + title + '_temp.mp4').replace('-', '')
+            video_path = self.desktop_path + title + '.mp4'
+            audio_path = self.desktop_path + title + '.mp3'
+            item = KototoItem()
+            item['video_url'] = play_info_list[0]
+            item['audio_url'] = play_info_list[1]
+            item['video_path'] = video_path
+            item['audio_path'] = audio_path
+            item['video_temp_path'] = video_temp_path
+            yield item
+        else:
+            print(title + ': 已经下载过了!')
+
+    def insert_info(self, vtitle, vurl):
+        """
+        mysql持久化存储爬取过的视频内容信息
+        :param vtitle: 标题
+        :param vurl: 视频链接
+        :return: 
+        """
+        with Mysql() as conn:
+            cursor = conn.cursor(pymysql.cursors.DictCursor)
+            try:
+                sql = 'insert into tb_kototo(title,url) values("%s","%s")' % (vtitle, vurl)
+                res = cursor.execute(sql)
+                conn.commit()
+                if res == 1:
+                    return True
+                else:
+                    return False
+            except:
+                return False
+
+    def get_play_info(self, resp):
+        """
+        解析详情页的源代码,提取其中的视频和文件真实地址
+        :param resp: 
+        :return: 
+        """
+        json_data = json.loads(re.findall('<script>window\.__playinfo__=(.*?)</script>', resp.text)[0])
+        # 拿到视频和音频的真实链接地址
+        video_url = json_data['data']['dash']['video'][0]['backupUrl'][0]
+        audio_url = json_data['data']['dash']['audio'][0]['backupUrl'][0]
+        return video_url, audio_url
+
+    def init_start_urls(self, url_list, person_page):
+        """
+        初始化需要爬取的列表页,由于b站使用js翻页,无法在源码中找到翻页地址,
+        需要自己手动实现解析翻页url的操作
+        :param url_list: 
+        :param person_page: 
+        :return: 
+        """
+        mid = re.findall('https://space.bilibili.com/(.*?)/video\w*', person_page)[0]
+        url = 'https://api.bilibili.com/x/space/arc/search?mid=' + mid + '&ps=30&tid=0&pn=1&keyword=&order=pubdate&jsonp=jsonp'
+        headers = {
+            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36',
+            'Referer': 'https://www.bilibili.com'
+        }
+        json_data = requests.get(url=url, headers=headers).json()
+        total_count = json_data['data']['page']['count']
+        page_size = json_data['data']['page']['ps']
+        if total_count <= page_size:
+            page_count = 1
+        elif total_count % page_size == 0:
+            page_count = total_count / page_size
+        else:
+            page_count = total_count // page_size + 1
+
+        url_template = 'https://space.bilibili.com/' + mid + '/video?tid=0&page=' + '%d' + '&keyword=&order=pubdate'
+        for i in range(page_count):
+            page_no = i + 1
+            url_list.append(url_template % page_no)
+
+    def closed(self, spider):
+        """
+        爬虫结束关闭selenium窗口
+        :param spider: 
+        :return: 
+        """
+        self.bro.quit()
+
+
+class Mysql(object):
+    def __enter__(self):
+        self.connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+        return self.connection
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.connection.close()
import scrapy
+from selenium import webdriver
+import re
+import json
+import requests
+import os
+from kototo.items import KototoItem
+import pymysql
+
+
+class KototoSpider(scrapy.Spider):
+    name = 'kototo'
+    start_urls = []
+
+    def __init__(self):
+        """
+        构造器,主要初始化了selenium对象并实现无头浏览器,以及
+        初始化需要爬取的url地址,因为b站的翻页是js实现的,所以要手动处理一下
+        """
+        super().__init__()
+        # 构造无头浏览器
+        from selenium.webdriver.chrome.options import Options
+        chrome_options = Options()
+        chrome_options.add_argument('--headless')
+        chrome_options.add_argument('--disable-gpu')
+        self.bro = webdriver.Chrome(chrome_options=chrome_options)
+        # 指定的up主的投稿页面,可以提到外面使用input输入
+        space_url = 'https://space.bilibili.com/17485141/video'
+        # 初始化需要爬取的列表页
+        self.init_start_urls(self.start_urls, space_url)
+        # 创建桌面文件夹
+        self.desktop_path = os.path.join(os.path.expanduser('~'), 'Desktop\\' + self.name + '\\')
+        if not os.path.exists(self.desktop_path):
+            os.mkdir(self.desktop_path)
+
+    def parse(self, response):
+        """
+        解析方法,解析列表页的视频li,拿到标题和详情页,然后主动请求详情页
+        :param response: 
+        :return: 
+        """
+        li_list = response.xpath('//*[@id="submit-video-list"]/ul[2]/li')
+        for li in li_list:
+            print(li.xpath('./a[2]/@title').extract_first())
+            print(detail_url := 'https://' + li.xpath('./a[2]/@href').extract_first()[2:])
+            yield scrapy.Request(url=detail_url, callback=self.parse_detail)
+
+    def parse_detail(self, response):
+        """
+        增量爬取: 解析详情页的音视频地址并交给管道处理
+        使用mysql实现
+        :param response: 
+        :return: 
+        """
+        title = response.xpath('//*[@id="viewbox_report"]/h1/@title').extract_first()
+        # 替换掉视频名称中无法用在文件名中或会导致cmd命令出错的字符
+        title = title.replace('-', '').replace(' ', '').replace('/', '').replace('|', '')
+        play_info_list = self.get_play_info(response)
+        # 这里使用mysql的唯一索引实现增量爬取,如果是服务器上跑也可以用redis
+        if self.insert_info(title, play_info_list[1]):
+            video_temp_path = (self.desktop_path + title + '_temp.mp4').replace('-', '')
+            video_path = self.desktop_path + title + '.mp4'
+            audio_path = self.desktop_path + title + '.mp3'
+            item = KototoItem()
+            item['video_url'] = play_info_list[0]
+            item['audio_url'] = play_info_list[1]
+            item['video_path'] = video_path
+            item['audio_path'] = audio_path
+            item['video_temp_path'] = video_temp_path
+            yield item
+        else:
+            print(title + ': 已经下载过了!')
+
+    def insert_info(self, vtitle, vurl):
+        """
+        mysql持久化存储爬取过的视频内容信息
+        :param vtitle: 标题
+        :param vurl: 视频链接
+        :return: 
+        """
+        with Mysql() as conn:
+            cursor = conn.cursor(pymysql.cursors.DictCursor)
+            try:
+                sql = 'insert into tb_kototo(title,url) values("%s","%s")' % (vtitle, vurl)
+                res = cursor.execute(sql)
+                conn.commit()
+                if res == 1:
+                    return True
+                else:
+                    return False
+            except:
+                return False
+
+    def get_play_info(self, resp):
+        """
+        解析详情页的源代码,提取其中的视频和文件真实地址
+        :param resp: 
+        :return: 
+        """
+        json_data = json.loads(re.findall('<script>window\.__playinfo__=(.*?)</script>', resp.text)[0])
+        # 拿到视频和音频的真实链接地址
+        video_url = json_data['data']['dash']['video'][0]['backupUrl'][0]
+        audio_url = json_data['data']['dash']['audio'][0]['backupUrl'][0]
+        return video_url, audio_url
+
+    def init_start_urls(self, url_list, person_page):
+        """
+        初始化需要爬取的列表页,由于b站使用js翻页,无法在源码中找到翻页地址,
+        需要自己手动实现解析翻页url的操作
+        :param url_list: 
+        :param person_page: 
+        :return: 
+        """
+        mid = re.findall('https://space.bilibili.com/(.*?)/video\w*', person_page)[0]
+        url = 'https://api.bilibili.com/x/space/arc/search?mid=' + mid + '&ps=30&tid=0&pn=1&keyword=&order=pubdate&jsonp=jsonp'
+        headers = {
+            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36',
+            'Referer': 'https://www.bilibili.com'
+        }
+        json_data = requests.get(url=url, headers=headers).json()
+        total_count = json_data['data']['page']['count']
+        page_size = json_data['data']['page']['ps']
+        if total_count <= page_size:
+            page_count = 1
+        elif total_count % page_size == 0:
+            page_count = total_count / page_size
+        else:
+            page_count = total_count // page_size + 1
+
+        url_template = 'https://space.bilibili.com/' + mid + '/video?tid=0&page=' + '%d' + '&keyword=&order=pubdate'
+        for i in range(page_count):
+            page_no = i + 1
+            url_list.append(url_template % page_no)
+
+    def closed(self, spider):
+        """
+        爬虫结束关闭selenium窗口
+        :param spider: 
+        :return: 
+        """
+        self.bro.quit()
+
+
+class Mysql(object):
+    def __enter__(self):
+        self.connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+        return self.connection
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.connection.close()

下载中间件

python
# Define here the models for your spider middleware
+#
+# See documentation in:
+# https://docs.scrapy.org/en/latest/topics/spider-middleware.html
+
+from scrapy import signals
+
+# useful for handling different item types with a single interface
+from itemadapter import is_item, ItemAdapter
+
+
+class KototoSpiderMiddleware:
+    # Not all methods need to be defined. If a method is not defined,
+    # scrapy acts as if the spider middleware does not modify the
+    # passed objects.
+
+    @classmethod
+    def from_crawler(cls, crawler):
+        # This method is used by Scrapy to create your spiders.
+        s = cls()
+        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
+        return s
+
+    def process_spider_input(self, response, spider):
+        # Called for each response that goes through the spider
+        # middleware and into the spider.
+
+        # Should return None or raise an exception.
+        return None
+
+    def process_spider_output(self, response, result, spider):
+        # Called with the results returned from the Spider, after
+        # it has processed the response.
+
+        # Must return an iterable of Request, or item objects.
+        for i in result:
+            yield i
+
+    def process_spider_exception(self, response, exception, spider):
+        # Called when a spider or process_spider_input() method
+        # (from other spider middleware) raises an exception.
+
+        # Should return either None or an iterable of Request or item objects.
+        pass
+
+    def process_start_requests(self, start_requests, spider):
+        # Called with the start requests of the spider, and works
+        # similarly to the process_spider_output() method, except
+        # that it doesn’t have a response associated.
+
+        # Must return only requests (not items).
+        for r in start_requests:
+            yield r
+
+    def spider_opened(self, spider):
+        spider.logger.info('Spider opened: %s' % spider.name)
+
+
+class KototoDownloaderMiddleware:
+
+    def process_request(self, request, spider):
+        return None
+
+    def process_response(self, request, response, spider):
+        """
+        篡改列表页的响应数据:
+            视频列表是通过ajax请求动态加载的,因此要通过selenium去加载这部分数据
+            并篡改响应内容
+        :param request: 
+        :param response: 
+        :param spider: 
+        :return: 
+        """
+        urls = spider.start_urls
+        bro = spider.bro
+        from scrapy.http import HtmlResponse
+        from time import sleep
+        if request.url in urls:
+            """
+            如果是列表页就进行响应篡改操作
+            """
+            bro.get(request.url)
+            sleep(3)
+            page_data = bro.page_source
+            new_response = HtmlResponse(url=request.url, body=page_data, encoding='utf-8', request=request)
+            # 返回篡改过的响应对象
+            return new_response
+        return response
+
+    def process_exception(self, request, exception, spider):
+        pass
# Define here the models for your spider middleware
+#
+# See documentation in:
+# https://docs.scrapy.org/en/latest/topics/spider-middleware.html
+
+from scrapy import signals
+
+# useful for handling different item types with a single interface
+from itemadapter import is_item, ItemAdapter
+
+
+class KototoSpiderMiddleware:
+    # Not all methods need to be defined. If a method is not defined,
+    # scrapy acts as if the spider middleware does not modify the
+    # passed objects.
+
+    @classmethod
+    def from_crawler(cls, crawler):
+        # This method is used by Scrapy to create your spiders.
+        s = cls()
+        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
+        return s
+
+    def process_spider_input(self, response, spider):
+        # Called for each response that goes through the spider
+        # middleware and into the spider.
+
+        # Should return None or raise an exception.
+        return None
+
+    def process_spider_output(self, response, result, spider):
+        # Called with the results returned from the Spider, after
+        # it has processed the response.
+
+        # Must return an iterable of Request, or item objects.
+        for i in result:
+            yield i
+
+    def process_spider_exception(self, response, exception, spider):
+        # Called when a spider or process_spider_input() method
+        # (from other spider middleware) raises an exception.
+
+        # Should return either None or an iterable of Request or item objects.
+        pass
+
+    def process_start_requests(self, start_requests, spider):
+        # Called with the start requests of the spider, and works
+        # similarly to the process_spider_output() method, except
+        # that it doesn’t have a response associated.
+
+        # Must return only requests (not items).
+        for r in start_requests:
+            yield r
+
+    def spider_opened(self, spider):
+        spider.logger.info('Spider opened: %s' % spider.name)
+
+
+class KototoDownloaderMiddleware:
+
+    def process_request(self, request, spider):
+        return None
+
+    def process_response(self, request, response, spider):
+        """
+        篡改列表页的响应数据:
+            视频列表是通过ajax请求动态加载的,因此要通过selenium去加载这部分数据
+            并篡改响应内容
+        :param request: 
+        :param response: 
+        :param spider: 
+        :return: 
+        """
+        urls = spider.start_urls
+        bro = spider.bro
+        from scrapy.http import HtmlResponse
+        from time import sleep
+        if request.url in urls:
+            """
+            如果是列表页就进行响应篡改操作
+            """
+            bro.get(request.url)
+            sleep(3)
+            page_data = bro.page_source
+            new_response = HtmlResponse(url=request.url, body=page_data, encoding='utf-8', request=request)
+            # 返回篡改过的响应对象
+            return new_response
+        return response
+
+    def process_exception(self, request, exception, spider):
+        pass

Item

python
import scrapy
+
+
+class KototoItem(scrapy.Item):
+    video_path = scrapy.Field()
+    video_url = scrapy.Field()
+    audio_path = scrapy.Field()
+    audio_url = scrapy.Field()
+    video_temp_path = scrapy.Field()
import scrapy
+
+
+class KototoItem(scrapy.Item):
+    video_path = scrapy.Field()
+    video_url = scrapy.Field()
+    audio_path = scrapy.Field()
+    audio_url = scrapy.Field()
+    video_temp_path = scrapy.Field()

Pipeline

python
import requests
+import os
+
+headers = {
+    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36',
+    'Referer': 'https://www.bilibili.com'
+}
+
+
+class KototoPipeline(object):
+    def process_item(self, item, spider):
+        video = item['video_url']
+        audio = item['audio_url']
+        video_temp_path = item['video_temp_path']
+        audio_path = item['audio_path']
+        video_data = requests.get(url=video, headers=headers).content
+        audio_data = requests.get(url=audio, headers=headers).content
+        with open(video_temp_path, 'wb') as f:
+            f.write(video_data)
+        with open(audio_path, 'wb') as f:
+            f.write(audio_data)
+        return item
+
+
+class MergePipeline(object):
+    """
+    删除临时文件
+    """
+
+    def process_item(self, item, spider):
+        video_temp_path = item['video_temp_path']
+        audio_path = item['audio_path']
+        video_path = item['video_path']
+        cmd = 'ffmpeg -y -i ' + video_temp_path + ' -i ' \
+              + audio_path + ' -c:v copy -c:a aac -strict experimental ' + video_path
+        print(cmd)
+        # subprocess.Popen(cmd, shell=True)
+        os.system(cmd)
+        os.remove(video_temp_path)
+        os.remove(audio_path)
+        print(video_path, '下载完成')
+        return item
import requests
+import os
+
+headers = {
+    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36',
+    'Referer': 'https://www.bilibili.com'
+}
+
+
+class KototoPipeline(object):
+    def process_item(self, item, spider):
+        video = item['video_url']
+        audio = item['audio_url']
+        video_temp_path = item['video_temp_path']
+        audio_path = item['audio_path']
+        video_data = requests.get(url=video, headers=headers).content
+        audio_data = requests.get(url=audio, headers=headers).content
+        with open(video_temp_path, 'wb') as f:
+            f.write(video_data)
+        with open(audio_path, 'wb') as f:
+            f.write(audio_data)
+        return item
+
+
+class MergePipeline(object):
+    """
+    删除临时文件
+    """
+
+    def process_item(self, item, spider):
+        video_temp_path = item['video_temp_path']
+        audio_path = item['audio_path']
+        video_path = item['video_path']
+        cmd = 'ffmpeg -y -i ' + video_temp_path + ' -i ' \
+              + audio_path + ' -c:v copy -c:a aac -strict experimental ' + video_path
+        print(cmd)
+        # subprocess.Popen(cmd, shell=True)
+        os.system(cmd)
+        os.remove(video_temp_path)
+        os.remove(audio_path)
+        print(video_path, '下载完成')
+        return item

settings

python
BOT_NAME = 'kototo'
+
+SPIDER_MODULES = ['kototo.spiders']
+NEWSPIDER_MODULE = 'kototo.spiders'
+
+
+USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+
+
+ROBOTSTXT_OBEY = False
+
+
+DEFAULT_REQUEST_HEADERS = {
+  'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+  'Referer': 'https://space.bilibili.com/17485141/video',
+  'Origin':  'https://space.bilibili.com'
+}
+FILES_STORE = './files'
+DOWNLOADER_MIDDLEWARES = {
+   'kototo.middlewares.KototoDownloaderMiddleware': 543,
+}
+
+ITEM_PIPELINES = {
+    # 下载
+   'kototo.pipelines.KototoPipeline': 1,
+   # 合并
+    'kototo.pipelines.MergePipeline': 2,
+}
BOT_NAME = 'kototo'
+
+SPIDER_MODULES = ['kototo.spiders']
+NEWSPIDER_MODULE = 'kototo.spiders'
+
+
+USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+
+
+ROBOTSTXT_OBEY = False
+
+
+DEFAULT_REQUEST_HEADERS = {
+  'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+  'Referer': 'https://space.bilibili.com/17485141/video',
+  'Origin':  'https://space.bilibili.com'
+}
+FILES_STORE = './files'
+DOWNLOADER_MIDDLEWARES = {
+   'kototo.middlewares.KototoDownloaderMiddleware': 543,
+}
+
+ITEM_PIPELINES = {
+    # 下载
+   'kototo.pipelines.KototoPipeline': 1,
+   # 合并
+    'kototo.pipelines.MergePipeline': 2,
+}

启动

  • 命令启动:scrapy crawl kototo

  • 配置pycharm启动(推荐)

    image-20210503002914982

下载结果

image-20210503003351357

mysql

image-20210503003551035

再次尝试下载时

image-20210503003617326

已经爬取过的资源会提示已经下载过,只会处理更新的内容。

+ + + + \ No newline at end of file diff --git "a/python/crawler/\345\244\232\347\272\277\347\250\213\347\210\254\345\217\226\346\242\250\350\247\206\351\242\221\347\275\221\347\253\231\347\232\204\347\203\255\351\227\250\350\247\206\351\242\221.html" "b/python/crawler/\345\244\232\347\272\277\347\250\213\347\210\254\345\217\226\346\242\250\350\247\206\351\242\221\347\275\221\347\253\231\347\232\204\347\203\255\351\227\250\350\247\206\351\242\221.html" new file mode 100644 index 000000000..b32b7d667 --- /dev/null +++ "b/python/crawler/\345\244\232\347\272\277\347\250\213\347\210\254\345\217\226\346\242\250\350\247\206\351\242\221\347\275\221\347\253\231\347\232\204\347\203\255\351\227\250\350\247\206\351\242\221.html" @@ -0,0 +1,175 @@ + + + + + + 多线程爬取梨视频网站的热门视频 | 故事 + + + + + + + + + + + + + + +
Skip to content

多线程爬取梨视频网站的热门视频

python
from multiprocessing.dummy import Pool
+import requests
+from lxml import etree
+import random
+import os
+
+
+# 体育分类视频url地址
+url = 'https://www.pearvideo.com/category_9'
+headers = {
+    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+}
+
+
+def get_videos():
+    page_text = requests.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    li_list = tree.xpath('//ul[@id="listvideoListUl"]/li')
+    if not os.path.exists('./video'):
+        os.mkdir('./video')
+    # 存储所有的视频真实地址和名称信息
+    video_url_list = []
+    for li in li_list:
+        # 视频id
+        video_id = li.xpath('.//a/@href')[0].split('_')[1]
+        # 视频名称
+        name = li.xpath('.//div[@class="vervideo-title"]/text()')[0] + '.mp4'
+        # 梨视频的video标签是动态加载的,通过请求抓包获取到的ajax地址
+        ajax_url = 'https://www.pearvideo.com/videoStatus.jsp'
+        query_param = {
+            'contId': video_id,
+            'mrd': str(random.random())
+        }
+        # 梨视频有Referer 防盗链验证
+        # 需要在普通的ua伪装中加入Referer请求头,否则会一直提示文章已下线
+        ajax_headers = {
+            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
+            'Referer': 'https://www.pearvideo.com/video_' + video_id
+        }
+        json_obj = requests.get(url=ajax_url, headers=ajax_headers, params=query_param).json()
+        # 响应的地址:https://video.pearvideo.com/mp4/adshort/20210419/1618849825266-15658816_adpkg-ad_hd.mp4
+        # 实际的地址:https://video.pearvideo.com/mp4/adshort/20210419/cont-1727112-15658816_adpkg-ad_hd.mp4
+        # 实际地址中cont-后是视频id 因此要把这串字符串处理掉
+        temp_url = json_obj['videoInfo']['videos']['srcUrl']
+        last_index = temp_url.rfind('/')
+        # 最后一个/前的内容 https://video.pearvideo.com/mp4/adshort/20210419
+        real_video_url = temp_url[:last_index]
+        # 最后一个/后的内容根据-切片(不包含) 1618849825266-15658816_adpkg-ad_hd.mp4
+        str_list = temp_url[last_index + 1:].split('-')
+        for i in range(0, len(str_list)):
+            if i == 0:
+                real_video_url = real_video_url + '/cont-' + video_id + '-'
+            elif i == len(str_list) - 1:
+                real_video_url = real_video_url + str_list[i]
+            else:
+                real_video_url = real_video_url + str_list[i] + '-'
+        # 字典存储视频信息
+        video_dict = {'name': name, "url": real_video_url}
+        video_url_list.append(video_dict)
+    return video_url_list
+
+
+# io操作较耗时,采用多线程进行
+def download_video(dict):
+    video_name = dict['name']
+    video_url = dict['url']
+    video_stream = requests.get(url=video_url, headers=headers).content
+    with open('./video/' + video_name, 'wb') as f:
+        f.write(video_stream)
+        print(f'============={video_name}下载完毕===============')
+
+
+if __name__ == '__main__':
+    # 多线程执行下载任务
+    pool = Pool(4)
+    pool.map(download_video, get_videos())
from multiprocessing.dummy import Pool
+import requests
+from lxml import etree
+import random
+import os
+
+
+# 体育分类视频url地址
+url = 'https://www.pearvideo.com/category_9'
+headers = {
+    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+}
+
+
+def get_videos():
+    page_text = requests.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    li_list = tree.xpath('//ul[@id="listvideoListUl"]/li')
+    if not os.path.exists('./video'):
+        os.mkdir('./video')
+    # 存储所有的视频真实地址和名称信息
+    video_url_list = []
+    for li in li_list:
+        # 视频id
+        video_id = li.xpath('.//a/@href')[0].split('_')[1]
+        # 视频名称
+        name = li.xpath('.//div[@class="vervideo-title"]/text()')[0] + '.mp4'
+        # 梨视频的video标签是动态加载的,通过请求抓包获取到的ajax地址
+        ajax_url = 'https://www.pearvideo.com/videoStatus.jsp'
+        query_param = {
+            'contId': video_id,
+            'mrd': str(random.random())
+        }
+        # 梨视频有Referer 防盗链验证
+        # 需要在普通的ua伪装中加入Referer请求头,否则会一直提示文章已下线
+        ajax_headers = {
+            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
+            'Referer': 'https://www.pearvideo.com/video_' + video_id
+        }
+        json_obj = requests.get(url=ajax_url, headers=ajax_headers, params=query_param).json()
+        # 响应的地址:https://video.pearvideo.com/mp4/adshort/20210419/1618849825266-15658816_adpkg-ad_hd.mp4
+        # 实际的地址:https://video.pearvideo.com/mp4/adshort/20210419/cont-1727112-15658816_adpkg-ad_hd.mp4
+        # 实际地址中cont-后是视频id 因此要把这串字符串处理掉
+        temp_url = json_obj['videoInfo']['videos']['srcUrl']
+        last_index = temp_url.rfind('/')
+        # 最后一个/前的内容 https://video.pearvideo.com/mp4/adshort/20210419
+        real_video_url = temp_url[:last_index]
+        # 最后一个/后的内容根据-切片(不包含) 1618849825266-15658816_adpkg-ad_hd.mp4
+        str_list = temp_url[last_index + 1:].split('-')
+        for i in range(0, len(str_list)):
+            if i == 0:
+                real_video_url = real_video_url + '/cont-' + video_id + '-'
+            elif i == len(str_list) - 1:
+                real_video_url = real_video_url + str_list[i]
+            else:
+                real_video_url = real_video_url + str_list[i] + '-'
+        # 字典存储视频信息
+        video_dict = {'name': name, "url": real_video_url}
+        video_url_list.append(video_dict)
+    return video_url_list
+
+
+# io操作较耗时,采用多线程进行
+def download_video(dict):
+    video_name = dict['name']
+    video_url = dict['url']
+    video_stream = requests.get(url=video_url, headers=headers).content
+    with open('./video/' + video_name, 'wb') as f:
+        f.write(video_stream)
+        print(f'============={video_name}下载完毕===============')
+
+
+if __name__ == '__main__':
+    # 多线程执行下载任务
+    pool = Pool(4)
+    pool.map(download_video, get_videos())

流程:

  • 根据主链接拿到最热视频的视频id和视频名称

  • 通过抓包拿到请求视频真实地址的ajax请求地址,修改参数,添加Referer请求头解决防盗链问题

  • 通过ajax请求拿到响应的json对象,解析出我们需要的视频地址

  • 通过对比可以得知视频地址是经过了字符串替换的,通过字符串操作得到真实的视频地址

  • 将解析出来的视频信息字典统一存在列表,再定义一个持久化方法

  • 通过多线程进行持久化操作

+ + + + \ No newline at end of file diff --git "a/python/crawler/\345\260\217\347\272\242\344\271\246\347\210\254\350\231\253.html" "b/python/crawler/\345\260\217\347\272\242\344\271\246\347\210\254\350\231\253.html" new file mode 100644 index 000000000..5d40b8f09 --- /dev/null +++ "b/python/crawler/\345\260\217\347\272\242\344\271\246\347\210\254\350\231\253.html" @@ -0,0 +1,25 @@ + + + + + + 小红书图片爬虫 | 故事 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/python/crawler/\351\252\214\350\257\201\347\240\201\350\257\206\345\210\253\345\222\214\346\250\241\346\213\237\347\231\273\345\275\225.html" "b/python/crawler/\351\252\214\350\257\201\347\240\201\350\257\206\345\210\253\345\222\214\346\250\241\346\213\237\347\231\273\345\275\225.html" new file mode 100644 index 000000000..87c60bcf2 --- /dev/null +++ "b/python/crawler/\351\252\214\350\257\201\347\240\201\350\257\206\345\210\253\345\222\214\346\250\241\346\213\237\347\231\273\345\275\225.html" @@ -0,0 +1,271 @@ + + + + + + 验证码识别和模拟登录 | 故事 + + + + + + + + + + + + + + +
Skip to content

验证码识别和模拟登录

超级鹰验证码识别平台

地址:https://www.chaojiying.com/

使用方法:

  1. 注册并登陆
  2. 点击开发文档,选择自己使用的语言下载使用demo,比如python
  3. 解压后得到demo实例,配置自己的用户名密码和软件id,软件id在用户中心的侧边栏最下方选择软件ID即可
  4. 根据需要识别的验证码类型配置具体类型,比如4-6位英文数字的类型编号为1902
  5. 充值,每次识别根据不同的类型有不同题分的定价,这个平台1元=1000题分
  6. 开始开发

超级鹰识别古诗文网验证码案例

识别案例代码

python
import requests
+from lxml import etree
+from story.code import StoryClient
+import os
+
+
+if __name__ == '__main__':
+    url = 'https://so.gushiwen.cn/user/login.aspx'
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    page_text = requests.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    # 验证码url地址
+    code_url = 'https://so.gushiwen.cn' + tree.xpath('//*[@id="imgCode"]/@src')[0]
+    img_data = requests.get(url=code_url, headers=headers).content
+    if not os.path.exists('./img'):
+        os.mkdir('./img')
+    with open('./img/code.jpg', 'wb') as f:
+        f.write(img_data)
+    client = StoryClient()
+    with open('./img/code.jpp','rb') as f:
+        im = f.read()
+        res = client.post_pic(im,1902)['pic_str']
+        print(res)
import requests
+from lxml import etree
+from story.code import StoryClient
+import os
+
+
+if __name__ == '__main__':
+    url = 'https://so.gushiwen.cn/user/login.aspx'
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    page_text = requests.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    # 验证码url地址
+    code_url = 'https://so.gushiwen.cn' + tree.xpath('//*[@id="imgCode"]/@src')[0]
+    img_data = requests.get(url=code_url, headers=headers).content
+    if not os.path.exists('./img'):
+        os.mkdir('./img')
+    with open('./img/code.jpg', 'wb') as f:
+        f.write(img_data)
+    client = StoryClient()
+    with open('./img/code.jpp','rb') as f:
+        im = f.read()
+        res = client.post_pic(im,1902)['pic_str']
+        print(res)

超级鹰使用demo代码:

这里我微调了一下,把用户信息在代码里写死了,可以根据自己情况调整

python
import requests
+from hashlib import md5
+
+
+class StoryClient(object):
+
+    def __init__(self):
+        self.username = '超级鹰用户名'
+        password = '超级鹰密码'.encode('utf8')
+        self.password = md5(password).hexdigest()
+        self.soft_id = '超级鹰软件ID'
+        self.base_params = {
+            'user': self.username,
+            'pass2': self.password,
+            'softid': self.soft_id,
+        }
+        self.headers = {
+            'Connection': 'Keep-Alive',
+            'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)',
+        }
+
+    def post_pic(self, im, codetype):
+        """
+        im: 图片字节
+        codetype: 题目类型 参考 http://www.chaojiying.com/price.html
+        """
+        params = {
+            'codetype': codetype,
+        }
+        params.update(self.base_params)
+        files = {'userfile': ('ccc.jpg', im)}
+        r = requests.post('http://upload.chaojiying.net/Upload/Processing.php', data=params, files=files,
+                          headers=self.headers)
+        return r.json()
+
+    def ReportError(self, im_id):
+        """
+        im_id:报错题目的图片ID
+        """
+        params = {
+            'id': im_id,
+        }
+        params.update(self.base_params)
+        r = requests.post('http://upload.chaojiying.net/Upload/ReportError.php', data=params, headers=self.headers)
+        return r.json()
+
+
+if __name__ == '__main__':
+    client = StoryClient()  
+    im = open('a.jpg', 'rb').read()  # 本地图片文件路径 来替换 a.jpg 有时WIN系统须要//
+    print(client.post_pic(im, 1902))  # 1902 验证码类型  官方网站>>价格体系 3.4+版 print 后要加()
import requests
+from hashlib import md5
+
+
+class StoryClient(object):
+
+    def __init__(self):
+        self.username = '超级鹰用户名'
+        password = '超级鹰密码'.encode('utf8')
+        self.password = md5(password).hexdigest()
+        self.soft_id = '超级鹰软件ID'
+        self.base_params = {
+            'user': self.username,
+            'pass2': self.password,
+            'softid': self.soft_id,
+        }
+        self.headers = {
+            'Connection': 'Keep-Alive',
+            'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)',
+        }
+
+    def post_pic(self, im, codetype):
+        """
+        im: 图片字节
+        codetype: 题目类型 参考 http://www.chaojiying.com/price.html
+        """
+        params = {
+            'codetype': codetype,
+        }
+        params.update(self.base_params)
+        files = {'userfile': ('ccc.jpg', im)}
+        r = requests.post('http://upload.chaojiying.net/Upload/Processing.php', data=params, files=files,
+                          headers=self.headers)
+        return r.json()
+
+    def ReportError(self, im_id):
+        """
+        im_id:报错题目的图片ID
+        """
+        params = {
+            'id': im_id,
+        }
+        params.update(self.base_params)
+        r = requests.post('http://upload.chaojiying.net/Upload/ReportError.php', data=params, headers=self.headers)
+        return r.json()
+
+
+if __name__ == '__main__':
+    client = StoryClient()  
+    im = open('a.jpg', 'rb').read()  # 本地图片文件路径 来替换 a.jpg 有时WIN系统须要//
+    print(client.post_pic(im, 1902))  # 1902 验证码类型  官方网站>>价格体系 3.4+版 print 后要加()

流程:

  • 请求登录页面并保存验证码图片
  • 上传验证码图片到超级鹰并识别

人人网模拟登录

python
import requests
+from lxml import etree
+from story.code import StoryClient
+
+if __name__ == '__main__':
+    url = 'http://www.renren.com/SysHome.do'
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    # 使用session发送请求
+    session = requests.session()
+    # page_text = requests.get(url=url, headers=headers).text
+    page_text = session.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    code_url = tree.xpath('//*[@id="verifyPic_login"]/@src')[0]
+    code_img_data = requests.get(url=code_url, headers=headers).content
+    path = './img/code.jpg'
+    with open(path, 'wb') as f:
+        f.write(code_img_data)
+    # 超级鹰客户端
+    client = StoryClient()
+    with open(path, 'rb') as r:
+        im = r.read()
+    code = client.post_pic(im, 1902)['pic_str']
+    login_url = 'http://www.renren.com/ajaxLogin/login?1=1&uniqueTimestamp=2021312240804'
+    data = {
+        'email': '人人网用户名',
+        'icode': code,
+        'origURL': 'http://www.renren.com/home',
+        'domain': 'renren.com',
+        'key_id': 1,
+        'captcha_type': 'web_login',
+        'password': '人人网密码',
+        'rkey': 'asdfasdf',
+        'f': ''
+    }
+    #response = requests.post(url=login_url, headers=headers, data=data)
+    # 使用session
+    response = session.post(url=login_url, headers=headers, data=data)
+    print(response.status_code)
+    login_page_data = response.text
+    with open('renren.html', 'w', encoding='utf-8') as f:
+        f.write(login_page_data)
+
+    detail_url = 'http://www.renren.com/6666666/profile'
+    # detail_text = requests.get(url=detail_url,headers=headers).text
+    # 使用session
+    detail_text = session.get(url=detail_url,headers=headers).text
+    with open('./detail.html','w',encoding='utf-8') as dw:
+        dw.write(detail_text)
import requests
+from lxml import etree
+from story.code import StoryClient
+
+if __name__ == '__main__':
+    url = 'http://www.renren.com/SysHome.do'
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
+    }
+    # 使用session发送请求
+    session = requests.session()
+    # page_text = requests.get(url=url, headers=headers).text
+    page_text = session.get(url=url, headers=headers).text
+    tree = etree.HTML(page_text)
+    code_url = tree.xpath('//*[@id="verifyPic_login"]/@src')[0]
+    code_img_data = requests.get(url=code_url, headers=headers).content
+    path = './img/code.jpg'
+    with open(path, 'wb') as f:
+        f.write(code_img_data)
+    # 超级鹰客户端
+    client = StoryClient()
+    with open(path, 'rb') as r:
+        im = r.read()
+    code = client.post_pic(im, 1902)['pic_str']
+    login_url = 'http://www.renren.com/ajaxLogin/login?1=1&uniqueTimestamp=2021312240804'
+    data = {
+        'email': '人人网用户名',
+        'icode': code,
+        'origURL': 'http://www.renren.com/home',
+        'domain': 'renren.com',
+        'key_id': 1,
+        'captcha_type': 'web_login',
+        'password': '人人网密码',
+        'rkey': 'asdfasdf',
+        'f': ''
+    }
+    #response = requests.post(url=login_url, headers=headers, data=data)
+    # 使用session
+    response = session.post(url=login_url, headers=headers, data=data)
+    print(response.status_code)
+    login_page_data = response.text
+    with open('renren.html', 'w', encoding='utf-8') as f:
+        f.write(login_page_data)
+
+    detail_url = 'http://www.renren.com/6666666/profile'
+    # detail_text = requests.get(url=detail_url,headers=headers).text
+    # 使用session
+    detail_text = session.get(url=detail_url,headers=headers).text
+    with open('./detail.html','w',encoding='utf-8') as dw:
+        dw.write(detail_text)

流程:

  • 请求登录页,抓取验证码上传到超级鹰识别
  • 浏览器抓包,拿到登录按钮的触发事件发送的login请求内容,复制出来并调整格式(代码中的data部分)
  • http是无状态的,登录后要想访问个人主页,需要携带cookie发送请求,可以手动抓包cookie到代码中,推荐使用requests.session()发送请求,然后可以继续爬取需要的个人主页详情信息
  • 超级鹰客户端部分见上一个案例中的demo代码
+ + + + \ No newline at end of file diff --git a/python/index.html b/python/index.html new file mode 100644 index 000000000..05445a85e --- /dev/null +++ b/python/index.html @@ -0,0 +1,25 @@ + + + + + + Python | 故事 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/python/others/Alfred\346\217\222\344\273\266-\345\277\253\351\200\237\344\275\277\347\224\250\347\274\226\350\276\221\345\231\250\346\211\223\345\274\200\346\214\207\345\256\232\346\226\207\344\273\266.html" "b/python/others/Alfred\346\217\222\344\273\266-\345\277\253\351\200\237\344\275\277\347\224\250\347\274\226\350\276\221\345\231\250\346\211\223\345\274\200\346\214\207\345\256\232\346\226\207\344\273\266.html" new file mode 100644 index 000000000..358a024a6 --- /dev/null +++ "b/python/others/Alfred\346\217\222\344\273\266-\345\277\253\351\200\237\344\275\277\347\224\250\347\274\226\350\276\221\345\231\250\346\211\223\345\274\200\346\214\207\345\256\232\346\226\207\344\273\266.html" @@ -0,0 +1,67 @@ + + + + + + Alfred插件-快速使用编辑器打开指定文件 | 故事 + + + + + + + + + + + + + + +
Skip to content

Alfred插件-快速使用编辑器打开指定文件

python
import sys
+import subprocess
+import re
+import os
+
+_pattern = r'[^A-Za-z0-9_\-.,:+\/@\n]'
+
+
+def replace_func(match_obj):
+    return '\\' + match_obj.group(0)
+
+
+def shell_escape(str_param):
+    return re.sub(_pattern, replace_func, str_param)
+
+
+if __name__ == '__main__':
+    if os.path.exists(sys.argv[1]):
+        file_path = shell_escape(sys.argv[1])
+        editor = shell_escape(sys.argv[2])
+        command = f"open -a {editor} {file_path}"
+        subprocess.Popen(command, shell=True)
import sys
+import subprocess
+import re
+import os
+
+_pattern = r'[^A-Za-z0-9_\-.,:+\/@\n]'
+
+
+def replace_func(match_obj):
+    return '\\' + match_obj.group(0)
+
+
+def shell_escape(str_param):
+    return re.sub(_pattern, replace_func, str_param)
+
+
+if __name__ == '__main__':
+    if os.path.exists(sys.argv[1]):
+        file_path = shell_escape(sys.argv[1])
+        editor = shell_escape(sys.argv[2])
+        command = f"open -a {editor} {file_path}"
+        subprocess.Popen(command, shell=True)

release:https://github.com/storyxc/Alfred-open-with-editor/releases/download/Alfred/Open.with.Editor.alfredworkflow

repository:https://github.com/storyxc/Alfred-open-with-editor/releases/tag/Alfred

image-20220221194201279

+ + + + \ No newline at end of file diff --git "a/python/others/argparse\346\250\241\345\235\227\345\205\245\351\227\250.html" "b/python/others/argparse\346\250\241\345\235\227\345\205\245\351\227\250.html" new file mode 100644 index 000000000..4413f9783 --- /dev/null +++ "b/python/others/argparse\346\250\241\345\235\227\345\205\245\351\227\250.html" @@ -0,0 +1,383 @@ + + + + + + argparse模块入门 | 故事 + + + + + + + + + + + + + + +
Skip to content

argparse模块入门

学习如何使用python编写一个命令行程序。

简介

argparse 模块可以让人轻松编写用户友好的命令行接口。程序定义它需要的参数,然后 argparse 将弄清如何从 sys.argv 解析出那些参数。 argparse 模块还会自动生成帮助和使用手册,并在用户给程序传入无效参数时报出错误信息。

官方文档地址:https://docs.python.org/zh-cn/3/library/argparse.html#upgrading-optparse-code

基础

python
import argparse
+parser = argparse.ArgumentParser()
+parser.parse_args()
import argparse
+parser = argparse.ArgumentParser()
+parser.parse_args()

使用命令行运行这个程序

bash
$ python main.py
+
+
+$ python main.py --help
+usage: main.py [-h]
+
+optional arguments:
+  -h, --help  show this help message and exit
+
+
+$ python main.py story
+usage: main.py [-h]
+main.py: error: unrecognized arguments: story
$ python main.py
+
+
+$ python main.py --help
+usage: main.py [-h]
+
+optional arguments:
+  -h, --help  show this help message and exit
+
+
+$ python main.py story
+usage: main.py [-h]
+main.py: error: unrecognized arguments: story

程序运行情况:

  • 在没有任何选项时,程序没有任何输出
  • argparse在我们什么逻辑代码都没有编写的情况下帮我们提供了一条帮助信息
  • --help可以缩写为-h,是唯一一个可以直接使用的选项,指定任何没有定义的内容都会报错,但是也会给出提示

位置参数

python
import argparse
+parser = argparse.ArgumentParser()
+parser.add_argument('print')
+args = parser.parse_args()
+print(args.print)
import argparse
+parser = argparse.ArgumentParser()
+parser.add_argument('print')
+args = parser.parse_args()
+print(args.print)

运行

bash
$ python main.py
+usage: main.py [-h] print
+main.py: error: the following arguments are required: print
+
+$ python main.py --help
+usage: main.py [-h] print
+
+positional arguments:
+  print
+
+optional arguments:
+  -h, --help  show this help message and exit
+
+
+$ python main.py test
+test
$ python main.py
+usage: main.py [-h] print
+main.py: error: the following arguments are required: print
+
+$ python main.py --help
+usage: main.py [-h] print
+
+positional arguments:
+  print
+
+optional arguments:
+  -h, --help  show this help message and exit
+
+
+$ python main.py test
+test

程序运行情况:

  • 增加了add_argument()方法,该方法指定程序能够接受哪些命令行选项。在例子中使用了print作为选项名
  • 现在调用程序必须指定一个选项
  • 这个选项就是一个位置参数

add_argument方法还可以添加提示信息

比如修改上面的代码再次运行:

parser.add_argument('print',help='print the string you typed')

bash
$ python main.py --help
+usage: main.py [-h] print
+
+positional arguments:
+  print       print the string you typed
+
+optional arguments:
+  -h, --help  show this help message and exit
$ python main.py --help
+usage: main.py [-h] print
+
+positional arguments:
+  print       print the string you typed
+
+optional arguments:
+  -h, --help  show this help message and exit

还可以指定输入的值的类型,否则argparse会把一切输入都当作字符串

parser.add_argument('print',help='print the number you typed',type=int)

运行:

bash
$ python main.py 1
+1
+
+$ python main.py two
+usage: main.py [-h] print
+main.py: error: argument print: invalid int value: 'two'
$ python main.py 1
+1
+
+$ python main.py two
+usage: main.py [-h] print
+main.py: error: argument print: invalid int value: 'two'

可选参数

python
import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--verbosity', help='increase output verbosity')
+args = parser.parse_args()
+if args.verbosity:
+    print('verbosity turn on')
import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--verbosity', help='increase output verbosity')
+args = parser.parse_args()
+if args.verbosity:
+    print('verbosity turn on')

运行:

bash
$ python main.py
+
+$ python main.py --verbosity test
+verbosity turn on
+
+$ python main.py -h
+usage: main.py [-h] [--verbosity VERBOSITY]
+
+optional arguments:
+  -h, --help            show this help message and exit
+  --verbosity VERBOSITY
+                        increase output verbosity
+
+$ python main.py --verbosity
+usage: main.py [-h] [--verbosity VERBOSITY]
+main.py: error: argument --verbosity: expected one argument
$ python main.py
+
+$ python main.py --verbosity test
+verbosity turn on
+
+$ python main.py -h
+usage: main.py [-h] [--verbosity VERBOSITY]
+
+optional arguments:
+  -h, --help            show this help message and exit
+  --verbosity VERBOSITY
+                        increase output verbosity
+
+$ python main.py --verbosity
+usage: main.py [-h] [--verbosity VERBOSITY]
+main.py: error: argument --verbosity: expected one argument

运行结果:

  • 当指定了--verbosity时打印turn on,否则不打印

  • 不添加这选项时不会报错,说明是可选参数,当一个可选参数没有被使用,对应的变量会被赋值为None,因此args.verbosity在if中被判断为逻辑假

  • 帮助信息多了VERBOSITY

  • 使用--verbosity选项时必须指定一个值,否则会报错

修改代码:

python
import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--verbose', help='increase output verbosity',
+                    action='store_true')
+args = parser.parse_args()
+if args.verbose:
+    print('verbosity turn on')
import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--verbose', help='increase output verbosity',
+                    action='store_true')
+args = parser.parse_args()
+if args.verbose:
+    print('verbosity turn on')

运行:

bash
$ python main.py
+
+
+$ python main.py --verbose
+verbosity turn on
+
+$ python main.py --help
+usage: main.py [-h] [--verbose]
+
+$ python main.py --verbose test
+usage: main.py [-h] [--verbose]
+main.py: error: unrecognized arguments: test
+
+
+optional arguments:
+  -h, --help  show this help message and exit
+  --verbose   increase output verbosity
$ python main.py
+
+
+$ python main.py --verbose
+verbosity turn on
+
+$ python main.py --help
+usage: main.py [-h] [--verbose]
+
+$ python main.py --verbose test
+usage: main.py [-h] [--verbose]
+main.py: error: unrecognized arguments: test
+
+
+optional arguments:
+  -h, --help  show this help message and exit
+  --verbose   increase output verbosity

运行结果:

  • 修改之后,这一选项更多是一个标志,而不需要接收值,新增加的参数action赋值为store_true,意味着,当这一选项存在时,为args.verbose赋值为True,没有指定该选项时为False
  • 当为其指定值时会报错
  • 不同的帮助文字

短参数

我们能注意到-h--help是功能相同的,我们也可以给自定义的参数指定简短的形式

python
import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument('-v', '--verbose', help='increase output verbosity',
+                    action='store_true')
+parser.add_argument('-s', '--square', type=int, help='display a square of a given number')
+args = parser.parse_args()
+answer = args.square ** 2  # **运算符为计算arg1的arg2次幂
+if args.verbose:
+    print('the square of {} is {}'.format(args.square, answer))
+else:
+    print(answer)
import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument('-v', '--verbose', help='increase output verbosity',
+                    action='store_true')
+parser.add_argument('-s', '--square', type=int, help='display a square of a given number')
+args = parser.parse_args()
+answer = args.square ** 2  # **运算符为计算arg1的arg2次幂
+if args.verbose:
+    print('the square of {} is {}'.format(args.square, answer))
+else:
+    print(answer)

运行:

bash
$ python main.py -s -v 1
+usage: main.py [-h] [-v] [-s SQUARE]
+main.py: error: argument -s/--square: expected one argument
+
+$ python main.py -s 2 -v
+the square of 2 is 4
+
+$ python main.py -s 2
+4
$ python main.py -s -v 1
+usage: main.py [-h] [-v] [-s SQUARE]
+main.py: error: argument -s/--square: expected one argument
+
+$ python main.py -s 2 -v
+the square of 2 is 4
+
+$ python main.py -s 2
+4

运行结果:

  • 必要的可选参数也要传值
  • 根据可选参数的指定与否我们可以控制一些功能的实现

修改代码:

python
import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument('-v', '--verbose', type=int,help='increase output verbosity')
+parser.add_argument('-s', '--square', type=int, help='display a square of a given number')
+args = parser.parse_args()
+answer = args.square ** 2  # **运算符为计算arg1的arg2次幂
+if args.verbose == 1:
+    print('the square of {} is {}'.format(args.square, answer))
+elif args.verbose == 2:
+    print('{}^2 is {}'.format(args.square, answer))
+else:
+    print(answer)
import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument('-v', '--verbose', type=int,help='increase output verbosity')
+parser.add_argument('-s', '--square', type=int, help='display a square of a given number')
+args = parser.parse_args()
+answer = args.square ** 2  # **运算符为计算arg1的arg2次幂
+if args.verbose == 1:
+    print('the square of {} is {}'.format(args.square, answer))
+elif args.verbose == 2:
+    print('{}^2 is {}'.format(args.square, answer))
+else:
+    print(answer)

运行结果:

bash
$ python main.py -s 2 -v 1
+the square of 2 is 4
+
+$ python main.py -s 2 -v 2
+2^2 is 4
+
+$ python main.py -s 2 -v 3
+4
$ python main.py -s 2 -v 1
+the square of 2 is 4
+
+$ python main.py -s 2 -v 2
+2^2 is 4
+
+$ python main.py -s 2 -v 3
+4

显然,用户指定-v的值是3是我们不愿意看见的,因此我们可以限定-v的取值范围

修改

python
parser.add_argument('-v', '--verbose', type=int,help='increase output verbosity',
+                    choices=[1,2])
parser.add_argument('-v', '--verbose', type=int,help='increase output verbosity',
+                    choices=[1,2])

再次运行:

bash
$ python main.py -s 2 -v 3
+usage: main.py [-h] [-v {1,2}] [-s SQUARE]
+main.py: error: argument -v/--verbose: invalid choice: 3 (choose from 1, 2)
+
+$ python main.py -h
+usage: main.py [-h] [-v {1,2}] [-s SQUARE]
+
+optional arguments:
+  -h, --help            show this help message and exit
+  -v {1,2}, --verbose {1,2}
+                        increase output verbosity
+  -s SQUARE, --square SQUARE
+                        display a square of a given number
$ python main.py -s 2 -v 3
+usage: main.py [-h] [-v {1,2}] [-s SQUARE]
+main.py: error: argument -v/--verbose: invalid choice: 3 (choose from 1, 2)
+
+$ python main.py -h
+usage: main.py [-h] [-v {1,2}] [-s SQUARE]
+
+optional arguments:
+  -h, --help            show this help message and exit
+  -v {1,2}, --verbose {1,2}
+                        increase output verbosity
+  -s SQUARE, --square SQUARE
+                        display a square of a given number

互斥参数

add_mutually_exclusive_group()方法允许指定彼此冲突的选项

python
import argparse
+
+parser = argparse.ArgumentParser()
+group = parser.add_mutually_exclusive_group()
+group.add_argument("-v", "--verbose", action="store_true")
+group.add_argument("-q", "--quiet", action="store_true")
+parser.add_argument("x", type=int, help="the base")
+parser.add_argument("y", type=int, help="the exponent")
+args = parser.parse_args()
+answer = args.x**args.y
+
+if args.quiet:
+    print(answer)
+elif args.verbose:
+    print("{} to the power {} equals {}".format(args.x, args.y, answer))
+else:
+    print("{}^{} == {}".format(args.x, args.y, answer))
import argparse
+
+parser = argparse.ArgumentParser()
+group = parser.add_mutually_exclusive_group()
+group.add_argument("-v", "--verbose", action="store_true")
+group.add_argument("-q", "--quiet", action="store_true")
+parser.add_argument("x", type=int, help="the base")
+parser.add_argument("y", type=int, help="the exponent")
+args = parser.parse_args()
+answer = args.x**args.y
+
+if args.quiet:
+    print(answer)
+elif args.verbose:
+    print("{} to the power {} equals {}".format(args.x, args.y, answer))
+else:
+    print("{}^{} == {}".format(args.x, args.y, answer))

运行:

bash
$ python main.py 4 2
+4^2 == 16
+
+$ python main.py 4 2 -v
+4 to the power 2 equals 16
+
+$ python main.py 4 2 -q
+16
+
+$ python main.py -h
+usage: main.py [-h] [-v | -q] x y
+
+calculate X to the power of Y
+
+positional arguments:
+  x              the base
+  y              the exponent
+
+optional arguments:
+  -h, --help     show this help message and exit
+  -v, --verbose
+  -q, --quiet
$ python main.py 4 2
+4^2 == 16
+
+$ python main.py 4 2 -v
+4 to the power 2 equals 16
+
+$ python main.py 4 2 -q
+16
+
+$ python main.py -h
+usage: main.py [-h] [-v | -q] x y
+
+calculate X to the power of Y
+
+positional arguments:
+  x              the base
+  y              the exponent
+
+optional arguments:
+  -h, --help     show this help message and exit
+  -v, --verbose
+  -q, --quiet

运行结果:

  • 根据指定-v还是-q,可以得到不同输出,实现不同功能

  • usage: main.py [-h] [-v | -q] x y中[-v|-q]代表可选其一,而不是使用两者

    如果同时使用会报错:

    bash
    $ python main.py 4 2 -v -q
    +usage: main.py [-h] [-v | -q] x y
    +main.py: error: argument -q/--quiet: not allowed with argument -v/--verbose
    $ python main.py 4 2 -v -q
    +usage: main.py [-h] [-v | -q] x y
    +main.py: error: argument -q/--quiet: not allowed with argument -v/--verbose
+ + + + \ No newline at end of file diff --git a/python/others/youtube-upload.html b/python/others/youtube-upload.html new file mode 100644 index 000000000..2560c3222 --- /dev/null +++ b/python/others/youtube-upload.html @@ -0,0 +1,405 @@ + + + + + + youtube-upload | 故事 + + + + + + + + + + + + + + +
Skip to content

youtube-upload

instructions

  1. 安装google-api-python-client包,github仓库:https://github.com/googleapis/google-api-python-client

  2. 安装oauth2client包,github仓库:https://github.com/googleapis/oauth2client

  3. google cloud创建应用

    1. 在api库中搜索youtube data api v3并启用
    2. 在OAuth consent screen中创建一个应用,选择desktop app,把自己的邮箱添加到test user
    3. 在凭据Credentials中创建OAuth 2.0客户端id,把客户端id,客户端密钥替换到下面这个json中,并保存到项目下的client_secrets.json中(要和上传的python脚本文件在相同目录)
    json
    {
    +  "web": {
    +    "client_id": "[[INSERT CLIENT ID HERE]]",
    +    "client_secret": "[[INSERT CLIENT SECRET HERE]]",
    +    "redirect_uris": [],
    +    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    +    "token_uri": "https://accounts.google.com/o/oauth2/token"
    +  }
    +}
    {
    +  "web": {
    +    "client_id": "[[INSERT CLIENT ID HERE]]",
    +    "client_secret": "[[INSERT CLIENT SECRET HERE]]",
    +    "redirect_uris": [],
    +    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    +    "token_uri": "https://accounts.google.com/o/oauth2/token"
    +  }
    +}

sample request

shell
python3 upload_video.py --file="/tmp/test_video_file.flv"
+                       --title="Summer vacation in California"
+                       --description="Had fun surfing in Santa Cruz"
+                       --keywords="surfing,Santa Cruz"
+                       --category="22"
+                       --privacyStatus="private"
python3 upload_video.py --file="/tmp/test_video_file.flv"
+                       --title="Summer vacation in California"
+                       --description="Had fun surfing in Santa Cruz"
+                       --keywords="surfing,Santa Cruz"
+                       --category="22"
+                       --privacyStatus="private"

sample code

代码源自YouTube Data API,给的例子比较老旧,是python2的代码,还有没法用的httplib包,下面代码针对这些做了删改调整可以直接使用。

python
#!/usr/bin/python
+
+
+import httplib2
+import os
+import random
+import sys
+import time
+
+from apiclient.discovery import build
+from apiclient.errors import HttpError
+from apiclient.http import MediaFileUpload
+from oauth2client.client import flow_from_clientsecrets
+from oauth2client.file import Storage
+from oauth2client.tools import argparser, run_flow
+
+
+# Explicitly tell the underlying HTTP transport library not to retry, since
+# we are handling retry logic ourselves.
+httplib2.RETRIES = 1
+
+# Maximum number of times to retry before giving up.
+MAX_RETRIES = 10
+
+# Always retry when these exceptions are raised.
+RETRIABLE_EXCEPTIONS = (httplib2.HttpLib2Error, IOError)
+
+# Always retry when an apiclient.errors.HttpError with one of these status
+# codes is raised.
+RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
+
+# The CLIENT_SECRETS_FILE variable specifies the name of a file that contains
+# the OAuth 2.0 information for this application, including its client_id and
+# client_secret. You can acquire an OAuth 2.0 client ID and client secret from
+# the Google API Console at
+# https://console.developers.google.com/.
+# Please ensure that you have enabled the YouTube Data API for your project.
+# For more information about using OAuth2 to access the YouTube Data API, see:
+#   https://developers.google.com/youtube/v3/guides/authentication
+# For more information about the client_secrets.json file format, see:
+#   https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
+CLIENT_SECRETS_FILE = "client_secrets.json"
+
+# This OAuth 2.0 access scope allows an application to upload files to the
+# authenticated user's YouTube channel, but doesn't allow other types of access.
+YOUTUBE_UPLOAD_SCOPE = "https://www.googleapis.com/auth/youtube.upload"
+YOUTUBE_API_SERVICE_NAME = "youtube"
+YOUTUBE_API_VERSION = "v3"
+
+# This variable defines a message to display if the CLIENT_SECRETS_FILE is
+# missing.
+MISSING_CLIENT_SECRETS_MESSAGE = """
+WARNING: Please configure OAuth 2.0
+
+To make this sample run you will need to populate the client_secrets.json file
+found at:
+
+   %s
+
+with information from the API Console
+https://console.developers.google.com/
+
+For more information about the client_secrets.json file format, please visit:
+https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
+""" % os.path.abspath(os.path.join(os.path.dirname(__file__),
+                                   CLIENT_SECRETS_FILE))
+
+VALID_PRIVACY_STATUSES = ("public", "private", "unlisted")
+
+
+def get_authenticated_service(args):
+  flow = flow_from_clientsecrets(CLIENT_SECRETS_FILE,
+    scope=YOUTUBE_UPLOAD_SCOPE,
+    message=MISSING_CLIENT_SECRETS_MESSAGE)
+
+  storage = Storage("%s-oauth2.json" % sys.argv[0])
+  credentials = storage.get()
+
+  if credentials is None or credentials.invalid:
+    credentials = run_flow(flow, storage, args)
+
+  return build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION,
+    http=credentials.authorize(httplib2.Http()))
+
+def initialize_upload(youtube, options):
+  tags = None
+  if options.keywords:
+    tags = options.keywords.split(",")
+
+  body=dict(
+    snippet=dict(
+      title=options.title,
+      description=options.description,
+      tags=tags,
+      categoryId=options.category
+    ),
+    status=dict(
+      privacyStatus=options.privacyStatus
+    )
+  )
+
+  # Call the API's videos.insert method to create and upload the video.
+  insert_request = youtube.videos().insert(
+    part=",".join(body.keys()),
+    body=body,
+    # The chunksize parameter specifies the size of each chunk of data, in
+    # bytes, that will be uploaded at a time. Set a higher value for
+    # reliable connections as fewer chunks lead to faster uploads. Set a lower
+    # value for better recovery on less reliable connections.
+    #
+    # Setting "chunksize" equal to -1 in the code below means that the entire
+    # file will be uploaded in a single HTTP request. (If the upload fails,
+    # it will still be retried where it left off.) This is usually a best
+    # practice, but if you're using Python older than 2.6 or if you're
+    # running on App Engine, you should set the chunksize to something like
+    # 1024 * 1024 (1 megabyte).
+    media_body=MediaFileUpload(options.file, chunksize=-1, resumable=True)
+  )
+
+  resumable_upload(insert_request)
+
+# This method implements an exponential backoff strategy to resume a
+# failed upload.
+def resumable_upload(insert_request):
+  response = None
+  error = None
+  retry = 0
+  while response is None:
+    try:
+      print("Uploading file...")
+      status, response = insert_request.next_chunk()
+      if response is not None:
+        if 'id' in response:
+          print("Video id '%s' was successfully uploaded." % response['id'])
+        else:
+          exit("The upload failed with an unexpected response: %s" % response)
+    except HttpError as e:
+      if e.resp.status in RETRIABLE_STATUS_CODES:
+        error = "A retriable HTTP error %d occurred:\n%s" % (e.resp.status,
+                                                             e.content)
+      else:
+        raise
+    except RETRIABLE_EXCEPTIONS as e:
+      error = "A retriable error occurred: %s" % e
+
+    if error is not None:
+      print(error)
+      retry += 1
+      if retry > MAX_RETRIES:
+        exit("No longer attempting to retry.")
+
+      max_sleep = 2 ** retry
+      sleep_seconds = random.random() * max_sleep
+      print("Sleeping %f seconds and then retrying..." % sleep_seconds)
+      time.sleep(sleep_seconds)
+
+if __name__ == '__main__':
+  argparser.add_argument("--file", required=True, help="Video file to upload")
+  argparser.add_argument("--title", help="Video title", default="Test Title")
+  argparser.add_argument("--description", help="Video description",
+    default="Test Description")
+  argparser.add_argument("--category", default="22",
+    help="Numeric video category. " +
+      "See https://developers.google.com/youtube/v3/docs/videoCategories/list")
+  argparser.add_argument("--keywords", help="Video keywords, comma separated",
+    default="")
+  argparser.add_argument("--privacyStatus", choices=VALID_PRIVACY_STATUSES,
+    default=VALID_PRIVACY_STATUSES[0], help="Video privacy status.")
+  args = argparser.parse_args()
+
+  if not os.path.exists(args.file):
+    exit("Please specify a valid file using the --file= parameter.")
+
+  youtube = get_authenticated_service(args)
+  try:
+    initialize_upload(youtube, args)
+  except HttpError as e:
+    print("An HTTP error %d occurred:\n%s" % (e.resp.status, e.content))
#!/usr/bin/python
+
+
+import httplib2
+import os
+import random
+import sys
+import time
+
+from apiclient.discovery import build
+from apiclient.errors import HttpError
+from apiclient.http import MediaFileUpload
+from oauth2client.client import flow_from_clientsecrets
+from oauth2client.file import Storage
+from oauth2client.tools import argparser, run_flow
+
+
+# Explicitly tell the underlying HTTP transport library not to retry, since
+# we are handling retry logic ourselves.
+httplib2.RETRIES = 1
+
+# Maximum number of times to retry before giving up.
+MAX_RETRIES = 10
+
+# Always retry when these exceptions are raised.
+RETRIABLE_EXCEPTIONS = (httplib2.HttpLib2Error, IOError)
+
+# Always retry when an apiclient.errors.HttpError with one of these status
+# codes is raised.
+RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
+
+# The CLIENT_SECRETS_FILE variable specifies the name of a file that contains
+# the OAuth 2.0 information for this application, including its client_id and
+# client_secret. You can acquire an OAuth 2.0 client ID and client secret from
+# the Google API Console at
+# https://console.developers.google.com/.
+# Please ensure that you have enabled the YouTube Data API for your project.
+# For more information about using OAuth2 to access the YouTube Data API, see:
+#   https://developers.google.com/youtube/v3/guides/authentication
+# For more information about the client_secrets.json file format, see:
+#   https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
+CLIENT_SECRETS_FILE = "client_secrets.json"
+
+# This OAuth 2.0 access scope allows an application to upload files to the
+# authenticated user's YouTube channel, but doesn't allow other types of access.
+YOUTUBE_UPLOAD_SCOPE = "https://www.googleapis.com/auth/youtube.upload"
+YOUTUBE_API_SERVICE_NAME = "youtube"
+YOUTUBE_API_VERSION = "v3"
+
+# This variable defines a message to display if the CLIENT_SECRETS_FILE is
+# missing.
+MISSING_CLIENT_SECRETS_MESSAGE = """
+WARNING: Please configure OAuth 2.0
+
+To make this sample run you will need to populate the client_secrets.json file
+found at:
+
+   %s
+
+with information from the API Console
+https://console.developers.google.com/
+
+For more information about the client_secrets.json file format, please visit:
+https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
+""" % os.path.abspath(os.path.join(os.path.dirname(__file__),
+                                   CLIENT_SECRETS_FILE))
+
+VALID_PRIVACY_STATUSES = ("public", "private", "unlisted")
+
+
+def get_authenticated_service(args):
+  flow = flow_from_clientsecrets(CLIENT_SECRETS_FILE,
+    scope=YOUTUBE_UPLOAD_SCOPE,
+    message=MISSING_CLIENT_SECRETS_MESSAGE)
+
+  storage = Storage("%s-oauth2.json" % sys.argv[0])
+  credentials = storage.get()
+
+  if credentials is None or credentials.invalid:
+    credentials = run_flow(flow, storage, args)
+
+  return build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION,
+    http=credentials.authorize(httplib2.Http()))
+
+def initialize_upload(youtube, options):
+  tags = None
+  if options.keywords:
+    tags = options.keywords.split(",")
+
+  body=dict(
+    snippet=dict(
+      title=options.title,
+      description=options.description,
+      tags=tags,
+      categoryId=options.category
+    ),
+    status=dict(
+      privacyStatus=options.privacyStatus
+    )
+  )
+
+  # Call the API's videos.insert method to create and upload the video.
+  insert_request = youtube.videos().insert(
+    part=",".join(body.keys()),
+    body=body,
+    # The chunksize parameter specifies the size of each chunk of data, in
+    # bytes, that will be uploaded at a time. Set a higher value for
+    # reliable connections as fewer chunks lead to faster uploads. Set a lower
+    # value for better recovery on less reliable connections.
+    #
+    # Setting "chunksize" equal to -1 in the code below means that the entire
+    # file will be uploaded in a single HTTP request. (If the upload fails,
+    # it will still be retried where it left off.) This is usually a best
+    # practice, but if you're using Python older than 2.6 or if you're
+    # running on App Engine, you should set the chunksize to something like
+    # 1024 * 1024 (1 megabyte).
+    media_body=MediaFileUpload(options.file, chunksize=-1, resumable=True)
+  )
+
+  resumable_upload(insert_request)
+
+# This method implements an exponential backoff strategy to resume a
+# failed upload.
+def resumable_upload(insert_request):
+  response = None
+  error = None
+  retry = 0
+  while response is None:
+    try:
+      print("Uploading file...")
+      status, response = insert_request.next_chunk()
+      if response is not None:
+        if 'id' in response:
+          print("Video id '%s' was successfully uploaded." % response['id'])
+        else:
+          exit("The upload failed with an unexpected response: %s" % response)
+    except HttpError as e:
+      if e.resp.status in RETRIABLE_STATUS_CODES:
+        error = "A retriable HTTP error %d occurred:\n%s" % (e.resp.status,
+                                                             e.content)
+      else:
+        raise
+    except RETRIABLE_EXCEPTIONS as e:
+      error = "A retriable error occurred: %s" % e
+
+    if error is not None:
+      print(error)
+      retry += 1
+      if retry > MAX_RETRIES:
+        exit("No longer attempting to retry.")
+
+      max_sleep = 2 ** retry
+      sleep_seconds = random.random() * max_sleep
+      print("Sleeping %f seconds and then retrying..." % sleep_seconds)
+      time.sleep(sleep_seconds)
+
+if __name__ == '__main__':
+  argparser.add_argument("--file", required=True, help="Video file to upload")
+  argparser.add_argument("--title", help="Video title", default="Test Title")
+  argparser.add_argument("--description", help="Video description",
+    default="Test Description")
+  argparser.add_argument("--category", default="22",
+    help="Numeric video category. " +
+      "See https://developers.google.com/youtube/v3/docs/videoCategories/list")
+  argparser.add_argument("--keywords", help="Video keywords, comma separated",
+    default="")
+  argparser.add_argument("--privacyStatus", choices=VALID_PRIVACY_STATUSES,
+    default=VALID_PRIVACY_STATUSES[0], help="Video privacy status.")
+  args = argparser.parse_args()
+
+  if not os.path.exists(args.file):
+    exit("Please specify a valid file using the --file= parameter.")
+
+  youtube = get_authenticated_service(args)
+  try:
+    initialize_upload(youtube, args)
+  except HttpError as e:
+    print("An HTTP error %d occurred:\n%s" % (e.resp.status, e.content))
+ + + + \ No newline at end of file diff --git "a/python/others/\344\275\277\347\224\250pandas\346\250\241\345\235\227\350\277\233\350\241\214\346\225\260\346\215\256\345\244\204\347\220\206.html" "b/python/others/\344\275\277\347\224\250pandas\346\250\241\345\235\227\350\277\233\350\241\214\346\225\260\346\215\256\345\244\204\347\220\206.html" new file mode 100644 index 000000000..99e2cc9a3 --- /dev/null +++ "b/python/others/\344\275\277\347\224\250pandas\346\250\241\345\235\227\350\277\233\350\241\214\346\225\260\346\215\256\345\244\204\347\220\206.html" @@ -0,0 +1,55 @@ + + + + + + 使用pandas模块进行数据处理 | 故事 + + + + + + + + + + + + + + +
Skip to content

使用pandas模块进行数据处理

以csv/txt文件为例

读取文件

pd.read_csv():返回一个DataFrame或TextFileReader

  • header指定具体表头行数,如果没有则header=None,第一行是表头则header=0,header还可以是一个列表例如header=[0,1,3],此时会有多个标题,且1和3之间的行会被忽略掉

  • seq指定分割符,默认为','

  • skiprows跳过某一行,行号从0开始,例如skiprows=2或skiprows=[0,1,200]

  • nrows指定需要读取的行数,从第一行开始,例如nrows=1000

  • na_values空值置换,会把指定的值替换为空值例如na_values=['\N', 15]会把字符串\N和数字15替换为空值NaN

    • 如果na_values的参数是一个字典,那就可以为具体的列来指定缺失值的样子。我们就可以指定在Age这一列,0要被看成缺失值;在Comment这一列,“该用户没有评价”被看成缺失值。na_values={'Age':0,'Comment':'该用户没有评价'}
  • iterator: True时返回一个TextFileReader,用于大文件处理,可以逐块处理文件

  • chunksize:指定文件块大小,返回一个TextFileReader

  • encoding:指定编码

  • index_col:读取时指定索引列,和df.set_index效果相同

  • names:文件中没有表头,手动指定表头,需要和header配合使用

names和header的使用场景主要如下:

  1. csv文件有表头并且是第一行,那么names和header都无需指定;
  2. csv文件有表头、但表头不是第一行,可能从下面几行开始才是真正的表头和数据,这个时候指定header即可;
  3. csv文件没有表头,全部是纯数据,那么我们可以通过names手动生成表头;
  4. csv文件有表头、但是这个表头你不想用,这个时候同时指定names和header。先用header选出表头和数据,然后再用names将表头替换掉,其实就等价于将数据读取进来之后再对列名进行rename;

数据相关概念

DataFrame

多行多列的二维数组、整个表格、多行多列

Series

一维数据、一行或一列

index

对应纵向上的行

替换索引为某一列的值:df.set_index('xxx', inplace=True)

columns

对应横向上的列

查询数据

几种方法

  1. df.loc,根据行、列的标签值查询(既能查询又能覆盖写入)

    • 行根据行标签,也就是索引筛选,列根据列标签,列名筛选

    • 如果选取的是所有行或者所有列,可以用:代替

    • 行标签选取的时候,两端都包含,比如[0:5]指的是0,1,2,3,4,5

  2. df.iloc,根据行、列的数字位置查询

    • iloc基于位置索引,简言之,就是第几行第几列,只不过这里的行列都是从0开始的。

    • iloc的0:X中不包括X,只能到X-1.

  3. df.where

  4. df.query

df.loc

  1. 使用单个label值查询

    • 查找并替换某一列的值&转换数据类型:df.loc[:, 'x'] = df['x'].str.replace('X','').astype('int32')

    • 查询单个值:df.loc['index', 'column']

    • 得到一个Series:df.loc['index', ['column1', 'column2']]

  2. 使用值列表批量查询

    • 得到一个Series:df.loc(['index1', 'index2', 'index3'], 'column1')
    • 得到DataFrame:df.loc(['index1', 'index2', 'index3'], ['column1', 'column2'])
  3. 使用数值区间进行范围查询(包含区间的开始和结尾)

    • 行index按区间:df.loc[1:2, 'colum1']
    • 列index按区间:df.loc[1, 'column1': 'column2']
    • 行列都按区间:df.loc[1:2, 'column1': 'column2']
  4. 使用条件表达式查询

    • 简单条件查询,年龄小于18:df.loc[df['age'] < 18, :]
    • 复杂条件查询,年龄小于18且姓名为张三:df.loc[(df['age'] < 18) & (df['name'] == '张三'), :]
  5. 调用函数查询

    • lambda表达式:df.loc[lambda df: df['age'] > 18, :]

    • 调用函数:

      python
      def query_adult(x):
      +    return df['age'] > 18
      +  
      +df.loc[query_adult, :]
      def query_adult(x):
      +    return df['age'] > 18
      +  
      +df.loc[query_adult, :]

新增数据列

几种方法

  1. 直接赋值

    • df.loc[:, 'newAge'] = df['age'] + 1
  2. df.apply

    • apply赋值 基于 0-'index' 1-'columns' 操作跨行/跨列

    • python
      def get_is_adult(x):
      +  if x['age'] >= 18:
      +      return '成年'
      +  else:
      +      return '未成年'
      +
      +df.loc[:, 'isAdult'] = df.apply(get_is_adult, axis=1)
      def get_is_adult(x):
      +  if x['age'] >= 18:
      +      return '成年'
      +  else:
      +      return '未成年'
      +
      +df.loc[:, 'isAdult'] = df.apply(get_is_adult, axis=1)
  3. df.assign

    • assign添加一列:返回一个新的DataFrame,存在的列会被覆盖,如果参数是callable只能直接操作DataFrame,如果不是callable则直接赋值
    • df = df.assign(newAge=lambda x: x['age'] + 1)
  4. 按条件选择分组并分别赋值

    • df.loc[df['highTemp'] - df['lowTemp'] > 10, 'tempDiff'] = '温差大'
    • df.loc[df['highTemp'] - df['lowTemp'] <= 10, 'tempDiff'] = '温差小'

    按字段分组查看数量: df['tempDiff'].value_counts()

数据合并

df_list = [df]
+df2 = pd.concat(df_list)
+
+if not os.path.exists('../resources/data1.csv'):
+    df2.to_csv('../resources/data1.csv', mode='a', index=False, header=True)
+else:
+    df2.to_csv('../resources/data1.csv', mode='a', index=False, header=False)
df_list = [df]
+df2 = pd.concat(df_list)
+
+if not os.path.exists('../resources/data1.csv'):
+    df2.to_csv('../resources/data1.csv', mode='a', index=False, header=True)
+else:
+    df2.to_csv('../resources/data1.csv', mode='a', index=False, header=False)

axis参数

pandas的axis参数:指的是跨该axis,例如指定columns 则是跨列,也就是沿着列名水平方向执行

  • 跨列操作:在横向上遍历每行,对每行的数据进行操作

  • 跨行操作:在水平方向遍历每列,对每列数据进行操作

+ + + + \ No newline at end of file diff --git "a/python/others/\345\210\207\346\215\242windows\344\273\243\347\220\206\350\256\276\347\275\256\345\274\200\345\205\263.html" "b/python/others/\345\210\207\346\215\242windows\344\273\243\347\220\206\350\256\276\347\275\256\345\274\200\345\205\263.html" new file mode 100644 index 000000000..4bfbb29a9 --- /dev/null +++ "b/python/others/\345\210\207\346\215\242windows\344\273\243\347\220\206\350\256\276\347\275\256\345\274\200\345\205\263.html" @@ -0,0 +1,53 @@ + + + + + + 切换windows代理设置开关 | 故事 + + + + + + + + + + + + + + +
Skip to content

切换windows代理设置开关

python
import winreg
+
+INTERNET_SETTINGS = winreg.OpenKey(winreg.HKEY_CURRENT_USER,
+    r'Software\Microsoft\Windows\CurrentVersion\Internet Settings',
+    0, winreg.KEY_ALL_ACCESS)
+name = 'ProxyEnable'
+
+def toggle_proxy():
+    _, reg_type = get_key()
+    winreg.SetValueEx(INTERNET_SETTINGS, name, 0, reg_type, 1 if _ == 0 else 0)
+
+def get_key():
+    return winreg.QueryValueEx(INTERNET_SETTINGS,name)
+
+toggle_proxy()
import winreg
+
+INTERNET_SETTINGS = winreg.OpenKey(winreg.HKEY_CURRENT_USER,
+    r'Software\Microsoft\Windows\CurrentVersion\Internet Settings',
+    0, winreg.KEY_ALL_ACCESS)
+name = 'ProxyEnable'
+
+def toggle_proxy():
+    _, reg_type = get_key()
+    winreg.SetValueEx(INTERNET_SETTINGS, name, 0, reg_type, 1 if _ == 0 else 0)
+
+def get_key():
+    return winreg.QueryValueEx(INTERNET_SETTINGS,name)
+
+toggle_proxy()

windows下可执行文件下载点我

+ + + + \ No newline at end of file diff --git "a/python/others/\350\257\273\345\217\226excel&ssh\351\200\232\351\201\223\350\277\236\346\216\245rds\346\233\264\346\226\260\346\225\260\346\215\256.html" "b/python/others/\350\257\273\345\217\226excel&ssh\351\200\232\351\201\223\350\277\236\346\216\245rds\346\233\264\346\226\260\346\225\260\346\215\256.html" new file mode 100644 index 000000000..65af713b9 --- /dev/null +++ "b/python/others/\350\257\273\345\217\226excel&ssh\351\200\232\351\201\223\350\277\236\346\216\245rds\346\233\264\346\226\260\346\225\260\346\215\256.html" @@ -0,0 +1,219 @@ + + + + + + 读取excel & 使用ssh通道连接rds更新数据 | 故事 + + + + + + + + + + + + + + +
Skip to content

读取excel & 使用ssh通道连接rds更新数据

python
import pymysql
+from sshtunnel import SSHTunnelForwarder
+import pymysql.cursors
+import xlrd
+
+
+def querySQL(ssh_config, db_config, sql):
+    with SSHTunnelForwarder(
+            (ssh_config['host'], ssh_config['port']),
+            ssh_password=ssh_config['password'],
+            ssh_username=ssh_config['username'],
+            remote_bind_address=(db_config['host'], db_config['port'])
+    ) as server:
+        db = pymysql.connect(
+            host='127.0.0.1',
+            port=server.local_bind_port,
+            user=db_config['username'],
+            passwd=db_config['password'],
+            db=db_config['db_name'],
+            charset="utf8",
+            cursorclass=pymysql.cursors.DictCursor)
+
+        cursor = db.cursor()
+        data = {}
+        try:
+            cursor.execute(sql)
+            data = cursor.fetchone()
+            db.commit()
+        except:
+            db.rollback()
+
+        db.close()
+        cursor.close()
+        return data
+
+
+class ExcelData(object):
+    def __init__(self, data_path, sheetname):
+        self.data_path = data_path  # excle表格路径,需传入绝对路径
+        self.sheetname = sheetname  # excle表格内sheet名
+        self.data = xlrd.open_workbook(self.data_path)  # 打开excel表格
+        self.table = self.data.sheet_by_name(self.sheetname)  # 切换到相应sheet
+        self.keys = self.table.row_values(0)  # 第一行作为key值
+        self.rowNum = self.table.nrows  # 获取表格行数
+        self.colNum = self.table.ncols  # 获取表格列数
+
+    def readExcel(self):
+        if self.rowNum < 2:
+            print("excle内数据行数小于2")
+        else:
+            L = []  # 列表L存放取出的数据
+            for i in range(1, self.rowNum):  # 从第二行(数据行)开始取数据
+                sheet_data = {}  # 定义一个字典用来存放对应数据
+                for j in range(self.colNum):  # j对应列值
+                    sheet_data[self.keys[j]] = self.table.row_values(i)[j]  # 把第i行第j列的值取出赋给第j列的键值,构成字典
+                L.append(sheet_data)  # 一行值取完之后(一个字典),追加到L列表中
+            # print(type(L))
+            return L
+
+
+if __name__ == "__main__":
+    # 远程登录配置信息
+    ssh_config = {
+        'host': '',
+        'port': 22,
+        'username': '',
+        'password': ''
+    }
+    # 数据库配置信息
+    db_config = {
+        'host': '',
+        'port': 3306,
+        'username': '',
+        'password': '',
+        'db_name': ''
+    }
+
+    path = ""
+    sheetname = ""
+    get_data = ExcelData(path, sheetname)
+    dataList = get_data.readExcel()
+    process_result = []
+    with open('./res.txt', 'w') as f:
+        for data in dataList:
+            # 查询语句
+            try:
+                sql = ''
+
+                # 查询
+                res = querySQL(ssh_config, db_config, sql)
+
+                update_sql = ""
+                f.write(update_sql + '\n')
+                # print(res)
+            except Exception as e:
+                error = '... 处理失败'
+                process_result.append(error)
+    print(process_result)
import pymysql
+from sshtunnel import SSHTunnelForwarder
+import pymysql.cursors
+import xlrd
+
+
+def querySQL(ssh_config, db_config, sql):
+    with SSHTunnelForwarder(
+            (ssh_config['host'], ssh_config['port']),
+            ssh_password=ssh_config['password'],
+            ssh_username=ssh_config['username'],
+            remote_bind_address=(db_config['host'], db_config['port'])
+    ) as server:
+        db = pymysql.connect(
+            host='127.0.0.1',
+            port=server.local_bind_port,
+            user=db_config['username'],
+            passwd=db_config['password'],
+            db=db_config['db_name'],
+            charset="utf8",
+            cursorclass=pymysql.cursors.DictCursor)
+
+        cursor = db.cursor()
+        data = {}
+        try:
+            cursor.execute(sql)
+            data = cursor.fetchone()
+            db.commit()
+        except:
+            db.rollback()
+
+        db.close()
+        cursor.close()
+        return data
+
+
+class ExcelData(object):
+    def __init__(self, data_path, sheetname):
+        self.data_path = data_path  # excle表格路径,需传入绝对路径
+        self.sheetname = sheetname  # excle表格内sheet名
+        self.data = xlrd.open_workbook(self.data_path)  # 打开excel表格
+        self.table = self.data.sheet_by_name(self.sheetname)  # 切换到相应sheet
+        self.keys = self.table.row_values(0)  # 第一行作为key值
+        self.rowNum = self.table.nrows  # 获取表格行数
+        self.colNum = self.table.ncols  # 获取表格列数
+
+    def readExcel(self):
+        if self.rowNum < 2:
+            print("excle内数据行数小于2")
+        else:
+            L = []  # 列表L存放取出的数据
+            for i in range(1, self.rowNum):  # 从第二行(数据行)开始取数据
+                sheet_data = {}  # 定义一个字典用来存放对应数据
+                for j in range(self.colNum):  # j对应列值
+                    sheet_data[self.keys[j]] = self.table.row_values(i)[j]  # 把第i行第j列的值取出赋给第j列的键值,构成字典
+                L.append(sheet_data)  # 一行值取完之后(一个字典),追加到L列表中
+            # print(type(L))
+            return L
+
+
+if __name__ == "__main__":
+    # 远程登录配置信息
+    ssh_config = {
+        'host': '',
+        'port': 22,
+        'username': '',
+        'password': ''
+    }
+    # 数据库配置信息
+    db_config = {
+        'host': '',
+        'port': 3306,
+        'username': '',
+        'password': '',
+        'db_name': ''
+    }
+
+    path = ""
+    sheetname = ""
+    get_data = ExcelData(path, sheetname)
+    dataList = get_data.readExcel()
+    process_result = []
+    with open('./res.txt', 'w') as f:
+        for data in dataList:
+            # 查询语句
+            try:
+                sql = ''
+
+                # 查询
+                res = querySQL(ssh_config, db_config, sql)
+
+                update_sql = ""
+                f.write(update_sql + '\n')
+                # print(res)
+            except Exception as e:
+                error = '... 处理失败'
+                process_result.append(error)
+    print(process_result)
+ + + + \ No newline at end of file diff --git "a/python/web/Django\345\205\245\351\227\250.html" "b/python/web/Django\345\205\245\351\227\250.html" new file mode 100644 index 000000000..09b70020b --- /dev/null +++ "b/python/web/Django\345\205\245\351\227\250.html" @@ -0,0 +1,135 @@ + + + + + + Django入门 | 故事 + + + + + + + + + + + + + + +
Skip to content

Django入门

Django 是一个由 Python 编写的一个开放源代码的 Web 应用框架。

使用 Django,只要很少的代码,Python 的程序开发人员就可以轻松地完成一个正式网站所需要的大部分内容,并进一步开发出全功能的 Web 服务 Django 本身基于 MVC 模型,即 Model(模型)+ View(视图)+ Controller(控制器)设计模式,MVC 模式使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。

MVC 优势:

  • 低耦合
  • 开发快捷
  • 部署方便
  • 可重用性高
  • 维护成本低

MTV模型

Django 采用了 MVT 的软件设计模式,即模型(Model),视图(View)和模板(Template)。

Django 的 MTV 模式本质上和 MVC 是一样的,也是为了各组件间保持松耦合关系,只是定义上有些许不同,Django 的 MTV 分别是指:

  • M 表示模型(Model):编写程序应有的功能,负责业务对象与数据库的映射(ORM)。
  • T 表示模板 (Template):负责如何把页面(html)展示给用户。
  • V 表示视图(View):负责业务逻辑,并在适当时候调用 Model和 Template。

除了以上三层之外,还需要一个 URL 分发器,它的作用是将一个个 URL 的页面请求分发给不同的 View 处理,View 再调用相应的 Model 和 Template,MTV 的响应模式如下所示:

img

img

解析:

用户通过浏览器向我们的服务器发起一个请求(request),这个请求会去访问视图函数:

  • a.如果不涉及到数据调用,那么这个时候视图函数直接返回一个模板也就是一个网页给用户。
  • b.如果涉及到数据调用,那么视图函数调用模型,模型去数据库查找数据,然后逐级返回。

视图函数把返回的数据填充到模板中空格中,最后返回网页给用户。

Django安装与使用

  • 安装:pip install Django

  • 创建Django项目:

    • 命令行创建django-admin startproject 项目名称
    • pycharm创建
  • 项目结构

    image-20210430001401906

    • settings.py —- 包含了项目的默认设置,包括数据库信息,调试标志以及其他一些工作的变量。
    • urls.py —– 负责把URL模式映射到应用程序。
    • manage.py —– Django项目里面的工具,通过它可以调用django shell和数据库等。
    • asgi.py: 一个 ASGI 兼容的 Web 服务器的入口,以便运行你的项目。
    • wsgi.py: 一个 WSGI 兼容的 Web 服务器的入口,以便运行你的项目。
  • 启动:python manage.py runserver 8001

    • 访问localhost:8001

    image-20210430001707060

路由控制

python
# django的1.x和2.x不同
+# 1.x : url(正则表达式,views视图函数,参数,别名)
+# 2.x : path,re_path(原来的url)
+from django.contrib import admin
+from django.urls import path
+
+urlpatterns = [
+    path('admin/', admin.site.urls),
+]
# django的1.x和2.x不同
+# 1.x : url(正则表达式,views视图函数,参数,别名)
+# 2.x : path,re_path(原来的url)
+from django.contrib import admin
+from django.urls import path
+
+urlpatterns = [
+    path('admin/', admin.site.urls),
+]

示例:

python
from django.contrib import admin
+from django.urls import path
+from storyxc.views import index
+urlpatterns = [
+    path('admin/', admin.site.urls),
+    path('',index,{'param':'story'})
+]
from django.contrib import admin
+from django.urls import path
+from storyxc.views import index
+urlpatterns = [
+    path('admin/', admin.site.urls),
+    path('',index,{'param':'story'})
+]

views.py:

python
from django.shortcuts import HttpResponse
+
+
+def index(request,param):
+    print(param)
+    return HttpResponse('ok')
from django.shortcuts import HttpResponse
+
+
+def index(request,param):
+    print(param)
+    return HttpResponse('ok')

启动应用后访问localhost:8000

image-20210430003356198

控制台:

python
[30/Apr/2021 00:31:21] "GET / HTTP/1.1" 200 2
+story
[30/Apr/2021 00:31:21] "GET / HTTP/1.1" 200 2
+story

创建app

python3 manage.py startapp demo

shell
djangoProject
+├── demo
+│   ├── __init__.py
+│   ├── admin.py
+│   ├── apps.py
+│   ├── migrations
+│   │   └── __init__.py
+│   ├── models.py
+│   ├── tests.py
+│   └── views.py
+├── djangoProject
+│   ├── __init__.py
+│   ├── __pycache__
+│   │   ├── __init__.cpython-311.pyc
+│   │   └── settings.cpython-311.pyc
+│   ├── asgi.py
+│   ├── settings.py
+│   ├── urls.py
+│   └── wsgi.py
+├── manage.py
+└── templates
djangoProject
+├── demo
+│   ├── __init__.py
+│   ├── admin.py
+│   ├── apps.py
+│   ├── migrations
+│   │   └── __init__.py
+│   ├── models.py
+│   ├── tests.py
+│   └── views.py
+├── djangoProject
+│   ├── __init__.py
+│   ├── __pycache__
+│   │   ├── __init__.cpython-311.pyc
+│   │   └── settings.cpython-311.pyc
+│   ├── asgi.py
+│   ├── settings.py
+│   ├── urls.py
+│   └── wsgi.py
+├── manage.py
+└── templates

注册app

python
# djangoProject/settings.py
+
+# Application definition
+
+INSTALLED_APPS = [
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+     # 新增注册app
+    'demo.apps.DemoConfig'
+]
+# 可以配置可访问域名
+ALLOWED_HOSTS = ["192.168.2.2"]
# djangoProject/settings.py
+
+# Application definition
+
+INSTALLED_APPS = [
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+     # 新增注册app
+    'demo.apps.DemoConfig'
+]
+# 可以配置可访问域名
+ALLOWED_HOSTS = ["192.168.2.2"]
+ + + + \ No newline at end of file diff --git "a/python/web/pymysql\344\275\277\347\224\250.html" "b/python/web/pymysql\344\275\277\347\224\250.html" new file mode 100644 index 000000000..a36f72bf3 --- /dev/null +++ "b/python/web/pymysql\344\275\277\347\224\250.html" @@ -0,0 +1,137 @@ + + + + + + pymysql使用 | 故事 + + + + + + + + + + + + + + +
Skip to content

pymysql使用

  • 安装pymysql
  • 建立数据库连接
  • 获取cursor对象
  • 使用cursor执行sql
  • 增删改-commit/rollback 查询-fetch
  • 关闭数据库连接

案例

安装:pip install pymysql

代码:

查询

python
import pymysql
+
+
+connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+cursor = connection.cursor(cursor=pymysql.cursors.DictCursor)
+cursor.execute('select * from tb_student')
+print(cursor.fetchall())
+connection.close()
+
+res:
+[{'id': 1, 'name': 'tom', 'age': 18}, {'id': 2, 'name': 'rose', 'age': 17}]
import pymysql
+
+
+connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+cursor = connection.cursor(cursor=pymysql.cursors.DictCursor)
+cursor.execute('select * from tb_student')
+print(cursor.fetchall())
+connection.close()
+
+res:
+[{'id': 1, 'name': 'tom', 'age': 18}, {'id': 2, 'name': 'rose', 'age': 17}]

修改

python
connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+cursor = connection.cursor()
+cursor.execute('update tb_student set name = "jack" where id = 1')
+connection.commit()
+connection.close()
connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+cursor = connection.cursor()
+cursor.execute('update tb_student set name = "jack" where id = 1')
+connection.commit()
+connection.close()

image-20210429231158834

删除

python
import pymysql
+
+connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+cursor = connection.cursor()
+try:
+    cursor.execute('delete from tb_student where id = 2')
+    connection.commit()
+except Exception as e:
+    connection.rollback()
+connection.close()
import pymysql
+
+connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+cursor = connection.cursor()
+try:
+    cursor.execute('delete from tb_student where id = 2')
+    connection.commit()
+except Exception as e:
+    connection.rollback()
+connection.close()

image-20210429231506901

新增

python
import pymysql
+
+connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+cursor = connection.cursor()
+try:
+    # 方式1
+    cursor.execute('insert into tb_student(name,age) values("mike",20)')
+    # 方式2
+    cursor.execute('insert into tb_student(name,age) values("%s",%s)' % ('mike',21))
+    connection.commit()
+except Exception as e:
+    connection.rollback()
+connection.close()
import pymysql
+
+connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+cursor = connection.cursor()
+try:
+    # 方式1
+    cursor.execute('insert into tb_student(name,age) values("mike",20)')
+    # 方式2
+    cursor.execute('insert into tb_student(name,age) values("%s",%s)' % ('mike',21))
+    connection.commit()
+except Exception as e:
+    connection.rollback()
+connection.close()

image-20210429232050677

通过上下文管理器自定义Mysql类

python
import pymysql
+
+
+class Mysql(object):
+    def __enter__(self):
+        self.connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+        return self.connection
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.connection.close()
+
+
+if __name__ == '__main__':
+    with Mysql() as conn:
+        cursor = conn.cursor(pymysql.cursors.DictCursor)
+        try:
+            sql = "select * from tb_student"
+            cursor.execute(sql)
+            res = cursor.fetchall()
+            print(res)
+        except:
+            print('error')
import pymysql
+
+
+class Mysql(object):
+    def __enter__(self):
+        self.connection = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='root', database='python')
+        return self.connection
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.connection.close()
+
+
+if __name__ == '__main__':
+    with Mysql() as conn:
+        cursor = conn.cursor(pymysql.cursors.DictCursor)
+        try:
+            sql = "select * from tb_student"
+            cursor.execute(sql)
+            res = cursor.fetchall()
+            print(res)
+        except:
+            print('error')
+ + + + \ No newline at end of file diff --git a/tinker/index.html b/tinker/index.html new file mode 100644 index 000000000..4538ea3b9 --- /dev/null +++ b/tinker/index.html @@ -0,0 +1,25 @@ + + + + + + Tinker | 故事 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/tinker/network/frp\345\206\205\347\275\221\347\251\277\351\200\217.html" "b/tinker/network/frp\345\206\205\347\275\221\347\251\277\351\200\217.html" new file mode 100644 index 000000000..67737675e --- /dev/null +++ "b/tinker/network/frp\345\206\205\347\275\221\347\251\277\351\200\217.html" @@ -0,0 +1,113 @@ + + + + + + frp内网穿透 | 故事 + + + + + + + + + + + + + + +
Skip to content

frp内网穿透

家庭服务器由于是移动宽带(大内网),没有办法申请公网ip,这样不在家的时候就无法进行服务器管理了。如果有公网ip,可以使用ddns,也可以用花生壳这类内网穿透工具。或者自己有一台有公网ip的云主机,可以通过frp应用来实现内网穿透。frp仓库地址:https://github.com/fatedier/frp

frp使用

具体使用可以查看frp使用文档,这里介绍下我用的场景:带sk校验的安全的ssh连接

在云主机上部署fprs,配置如下:

ini
[common]
+bind_addr = 0.0.0.0
+bind_port = 7000
+
+token = xxx
[common]
+bind_addr = 0.0.0.0
+bind_port = 7000
+
+token = xxx
shell
cat > /etc/systemd/system/frps.service <<EOF
+[Unit]
+Description=frps
+After=network.target
+[Service]
+Type=simple
+ExecStart=/usr/bin/frps -c /etc/frps/frps.ini
+Restart=on-failure
+[Install]
+WantedBy=multi-user.target
+EOF
cat > /etc/systemd/system/frps.service <<EOF
+[Unit]
+Description=frps
+After=network.target
+[Service]
+Type=simple
+ExecStart=/usr/bin/frps -c /etc/frps/frps.ini
+Restart=on-failure
+[Install]
+WantedBy=multi-user.target
+EOF
  • 在需要暴露到内网的机器A上部署 frpc,配置如下:
ini
[common]
+server_addr = x.x.x.x
+server_port = 7000
+token = xxx
+
+[ssh]
+type = tcp
+local_ip = 127.0.0.1
+local_port = 22
+remote_port = 6001
+
+[secret_ssh]
+type = stcp
+# 只有 sk 一致的用户才能访问到此服务
+sk = abcdefg
+local_ip = 127.0.0.1
+local_port = 22
[common]
+server_addr = x.x.x.x
+server_port = 7000
+token = xxx
+
+[ssh]
+type = tcp
+local_ip = 127.0.0.1
+local_port = 22
+remote_port = 6001
+
+[secret_ssh]
+type = stcp
+# 只有 sk 一致的用户才能访问到此服务
+sk = abcdefg
+local_ip = 127.0.0.1
+local_port = 22

在需要访问内网的机器上执行命令连接内网服务,例如用户为root

ssh -oPort=6001 root@x.x.x.x

  • 在需要访问内网的机器B上部署frpc,配置如下:
ini
[common]
+server_addr = x.x.x.x
+server_port = 7000
+token = xxx
+
+[secret_ssh_visitor]
+type = stcp
+# stcp 的访问者
+role = visitor
+# 要访问的 stcp 代理的名字
+server_name = secret_ssh
+sk = abcdefg
+# 绑定本地端口用于访问 SSH 服务
+bind_addr = 127.0.0.1
+bind_port = 6000
[common]
+server_addr = x.x.x.x
+server_port = 7000
+token = xxx
+
+[secret_ssh_visitor]
+type = stcp
+# stcp 的访问者
+role = visitor
+# 要访问的 stcp 代理的名字
+server_name = secret_ssh
+sk = abcdefg
+# 绑定本地端口用于访问 SSH 服务
+bind_addr = 127.0.0.1
+bind_port = 6000

在需要访问内网的机器上执行命令连接内网服务,例如用户为root

ssh -oPort 6000 root@127.0.0.1

如果内网机器开启了密钥登录,则需要指定内网服务器的私钥文件

ssh -oPort 6000 -i identityFile root@127.0.0.1

+ + + + \ No newline at end of file diff --git "a/tinker/network/home server\346\220\255\345\273\272.html" "b/tinker/network/home server\346\220\255\345\273\272.html" new file mode 100644 index 000000000..7513ea85a --- /dev/null +++ "b/tinker/network/home server\346\220\255\345\273\272.html" @@ -0,0 +1,1397 @@ + + + + + + 家庭服务器home server搭建 | 故事 + + + + + + + + + + + + + + +
Skip to content

家庭服务器home server搭建

一直想搞一台nas玩玩儿,但是看了群晖、威联通这些成品nas低到令人发指的性价比,我最终还是决定diy一台小主机来实现自己的需求。

需求分析

需求

  1. 共享存储
  2. Docker服务
  3. 跑一些测试程序

分析

PC上还有块4T的希捷酷鹰,再添3块4T紫盘组raid5阵列。机箱的盘位就至少需要4个以上,挑了一圈就乔思伯N1(5盘位)和万由的810A(8盘位)能看的过去,虽然万由盘位多但是价格比n1高了大几百,目前也用不到这么多盘位,因此机箱确定了n1,主板也要买itx版型。

  1. 要跑的docker容器比较多,下载器服务、阿里云的webdav容器、直播录制程序容器等等。。。因此内存需要32G以上。

  2. 确定使用的系统是个比较复杂的过程,因为有过PVE虚拟机翻车的经历,这个服务器又主要承载了数据存储功能,所以要追求稳定,因此首先排除PVE和ESXi这些虚拟机系统,直接物理机装系统。然后我在虚拟机上装了最新版的Truenas scale体验了一下,这个系统是基于debian用python开发的,交互上倒没什么问题,但是因为是个纯nas系统,对主系统限制较多,自由度不高(不能直接装软件),因此也被pass,黑群晖这些就不说了,在我看来还不如truenas。一圈排除下来就只能直接装linux server了。去V2EX论坛问了老哥们的意见,推荐debian的很多,也有建议用最熟悉的系统的,最后我选择了后者,选了比较有把握的ubuntu server,正好ubuntu的22.04发行版刚出,就直接安排上了。

硬件

经过了好几天的挑选,最终敲定了这套配置

txt
cpu:i3-10100散片
+主板:七彩虹cvn b460i frozen
+内存:金士顿16g*2 2666
+固态:七彩虹 ssd sata3 128g
+cpu散热:超频3刀锋
+机械硬盘:西数海康oem紫盘4t*3 
+电源:tt 350w sfx电源
+机箱+线材:乔思伯n1
+扩展卡:乐扩m2转sata3接口扩展卡
cpu:i3-10100散片
+主板:七彩虹cvn b460i frozen
+内存:金士顿16g*2 2666
+固态:七彩虹 ssd sata3 128g
+cpu散热:超频3刀锋
+机械硬盘:西数海康oem紫盘4t*3 
+电源:tt 350w sfx电源
+机箱+线材:乔思伯n1
+扩展卡:乐扩m2转sata3接口扩展卡

其中散热、固态是在公司的福利商城购买,cpu、机械硬盘、机箱、扩展卡在淘宝购买,主板、电源在京东购买,内存在咸鱼淘的。不算硬盘花费是2480,加上硬盘3755。

组装完成后:

  • 灵魂走线,又不是不能用(doge)

D55DA0D4-322D-4A49-9634-9DB667BDD7A4_1_105_c

  • 侧面

6C0A8FED-B95C-4FE5-ACCB-32DD8DF553E8_1_105_c

B4E46C72-2CA5-4727-AD52-F3C25F94A74B_1_102_o

跟其他工业风机箱比起来,乔思伯n1这款颜值还是很不错的。

系统搭建

操作系统安装

ubuntu官网下载最新版的ubuntu-server-22.04,然后rufus刷写到U盘中,使用U盘引导启动。

安装过程不再赘述,这里记录几个重点步骤:

在配置Ubuntu安装镜像这一步最好选择国内的企业/大学镜像站,不然后面安装可能会在下载时卡住。网易镜像源http://mirrors.163.com/ubuntu/,阿里云镜像源https://mirrors.aliyun.com/ubuntu/,清华源https://mirrors.tuna.tsinghua.edu.cn/ubuntu/

  1. 磁盘分区选择自定义,然后根据自己的情况进行分区,我的固态只分了//boot 两个区,然后四块4T机械组了raid5。(ubuntu在建立阵列后会立刻进入重建过程,阵列中会有一个分区状态为spare rebuilding ,其他分区为active sync。这个重建过程很久,我4块4T重建总共用了十几个小时,重建完成后阵列下所有分区都会变为active sync 状态

image-20220501014554319

基础配置

  • 开启root登陆

    shell
    sudo vim /etc/ssh/sshd_config
    +
    +# 添加配置
    +PermitRootLogin yes
    +
    +# 给root修改密码
    +sudo passwd root
    +
    +systemctl restart sshd
    sudo vim /etc/ssh/sshd_config
    +
    +# 添加配置
    +PermitRootLogin yes
    +
    +# 给root修改密码
    +sudo passwd root
    +
    +systemctl restart sshd
  • 启用密钥登陆

见另一篇博客 阿里云服务器启用密钥登陆并禁用密码登陆

  • 时区同步

    sudo cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

安装服务

这个部分长期更新XD,一点点补上吧。

samba文件共享服务

shell
# 安装samba
+sudo apt install samba
+# 启动smb
+systemctl start smb
+# 开机自启
+systemctl enable smb
+
+# 创建共享文件夹 设置权限770
+mkdir /mnt/data
+sudo chmod 770 /mnt/data
+
+# 添加用户和密码
+sudo smbpasswd -a 用户名
+
+# 修改配置文件,在文件最后添加共享资源设置
+sudo vim /etc/samba/smb.conf
+
+[data]
+path = /mnt/data
+available = yes
+browseable = yes
+public = no
+writable = yes
+valid users = story
# 安装samba
+sudo apt install samba
+# 启动smb
+systemctl start smb
+# 开机自启
+systemctl enable smb
+
+# 创建共享文件夹 设置权限770
+mkdir /mnt/data
+sudo chmod 770 /mnt/data
+
+# 添加用户和密码
+sudo smbpasswd -a 用户名
+
+# 修改配置文件,在文件最后添加共享资源设置
+sudo vim /etc/samba/smb.conf
+
+[data]
+path = /mnt/data
+available = yes
+browseable = yes
+public = no
+writable = yes
+valid users = story

samba共享配置详解

[temp] #共享资源名称

comment = Temporary file space #简单的解释,内容无关紧要

path = /tmp #实际的共享目录

writable = yes #设置为可写入

browseable = yes #可以被所有用户浏览到资源名称,

guest ok = yes #可以让用户随意登录

public = yes #允许匿名查看

valid users = 用户名 #设置访问用户

valid users = @组名 #设置访问组

readonly = yes #只读

readonly = no #读写

hosts deny = 192.168.0.0 #表示禁止所有来自192.168.0.0/24 网段的IP 地址访问

hosts allow = 192.168.0.24 #表示允许192.168.0.24 这个IP 地址访问

[homes]为特殊共享目录,表示用户主目录。

[printers]表示共享打印机。

原文链接:https://blog.csdn.net/l1593572468/article/details/121444812

Docker安装

shell
# Uninstall old versions
+sudo apt-get remove docker docker-engine docker.io containerd runc
+
+# Update the apt package index and install packages to allow apt to use a repository over HTTPS:
+sudo apt-get update
+sudo apt-get install \
+    ca-certificates \
+    curl \
+    gnupg \
+    lsb-release
+# Add Docker’s official GPG key:
+curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
+
+# Use the following command to set up the stable repository.
+echo \
+  "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
+  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
+  
+# Install Docker Engine
+sudo apt-get update
+sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
+
+
+systemctl enable docker
# Uninstall old versions
+sudo apt-get remove docker docker-engine docker.io containerd runc
+
+# Update the apt package index and install packages to allow apt to use a repository over HTTPS:
+sudo apt-get update
+sudo apt-get install \
+    ca-certificates \
+    curl \
+    gnupg \
+    lsb-release
+# Add Docker’s official GPG key:
+curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
+
+# Use the following command to set up the stable repository.
+echo \
+  "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
+  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
+  
+# Install Docker Engine
+sudo apt-get update
+sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
+
+
+systemctl enable docker

挂载阿里云盘

参考另一篇博客挂载阿里云盘+开机自动挂载

transmission

shell
docker run -d \
+  --name=transmission \
+  -e TRANSMISSION_WEB_HOME=/transmission-web-control/ \
+  -e PUID=1000 \
+  -e PGID=1000 \
+  -e TZ=Asia/Shanghai \
+  -e USER=<user> \
+  -e PASS=<pass> \
+  -p 19091:9091 \
+  -p 51413:51413 \
+  -p 51413:51413/udp \
+  -v /mnt/data/docker/transmission/data:/config \
+  -v /mnt/data/downloads/others:/downloads/others \
+  -v /mnt/data/downloads/tvseries:/downloads/tvseries \
+  -v /mnt/data/docker/transmission/watch/folder:/watch \
+  -v /mnt/data/downloads/movies:/downloads/movies \
+  --restart=always \
+  linuxserver/transmission
docker run -d \
+  --name=transmission \
+  -e TRANSMISSION_WEB_HOME=/transmission-web-control/ \
+  -e PUID=1000 \
+  -e PGID=1000 \
+  -e TZ=Asia/Shanghai \
+  -e USER=<user> \
+  -e PASS=<pass> \
+  -p 19091:9091 \
+  -p 51413:51413 \
+  -p 51413:51413/udp \
+  -v /mnt/data/docker/transmission/data:/config \
+  -v /mnt/data/downloads/others:/downloads/others \
+  -v /mnt/data/downloads/tvseries:/downloads/tvseries \
+  -v /mnt/data/docker/transmission/watch/folder:/watch \
+  -v /mnt/data/downloads/movies:/downloads/movies \
+  --restart=always \
+  linuxserver/transmission

qbittorrent

shell
version: "3.2"
+
+services:
+  qbittorrent:
+    image: nevinee/qbittorrent:4.3.9
+    container_name: qbittorrent
+    environment:
+      - PUID=0
+      - PGID=0
+      - TZ=Asia/Shanghai
+      - WEBUI_PORT=18080
+      - BT_PORT=55555
+    volumes:
+      - /mnt/data/docker/qbittorrent/config:/data
+      - /repo:/downloads
+    network_mode: host
+    restart: unless-stopped
version: "3.2"
+
+services:
+  qbittorrent:
+    image: nevinee/qbittorrent:4.3.9
+    container_name: qbittorrent
+    environment:
+      - PUID=0
+      - PGID=0
+      - TZ=Asia/Shanghai
+      - WEBUI_PORT=18080
+      - BT_PORT=55555
+    volumes:
+      - /mnt/data/docker/qbittorrent/config:/data
+      - /repo:/downloads
+    network_mode: host
+    restart: unless-stopped

aria2

shell
docker run -d \
+    --name aria2 \
+    --restart always \
+    --log-opt max-size=1m \
+    -e TZ=Asia/Shanghai \
+    -e PUID=$UID \
+    -e PGID=$GID \
+    -e UMASK_SET=022 \
+    -e RPC_SECRET=<secret> \
+    -e RPC_PORT=16800 \
+    -p 16800:16800 \
+    -e LISTEN_PORT=16888 \
+    -p 16888:16888 \
+    -p 16888:16888/udp \
+    -v /mnt/data/docker/aria2/config:/config \
+    -v /mnt/data/downloads/tvseries:/downloads/tvseries \
+    -v /mnt/data/downloads/movies:/downloads/movies \
+    -v /mnt/data/downloads/others:/downloads/others \
+    p3terx/aria2-pro
docker run -d \
+    --name aria2 \
+    --restart always \
+    --log-opt max-size=1m \
+    -e TZ=Asia/Shanghai \
+    -e PUID=$UID \
+    -e PGID=$GID \
+    -e UMASK_SET=022 \
+    -e RPC_SECRET=<secret> \
+    -e RPC_PORT=16800 \
+    -p 16800:16800 \
+    -e LISTEN_PORT=16888 \
+    -p 16888:16888 \
+    -p 16888:16888/udp \
+    -v /mnt/data/docker/aria2/config:/config \
+    -v /mnt/data/downloads/tvseries:/downloads/tvseries \
+    -v /mnt/data/downloads/movies:/downloads/movies \
+    -v /mnt/data/downloads/others:/downloads/others \
+    p3terx/aria2-pro

jellyfin

shell
version: "3.2"
+
+services:
+  jenkins:
+    image: jenkins/jenkins:2.332.3-jdk11
+    container_name: jenkins
+    environment:
+      - TZ=Asia/Shanghai
+    user: root
+    volumes:
+      - /story/dist:/story/dist
+      - /mnt/data/docker/jenkins/jenkins_data:/var/jenkins_home
+      - /etc/localtime:/etc/localtime:ro
+    ports:
+      - "8099:8080"
+    restart: unless-stopped
version: "3.2"
+
+services:
+  jenkins:
+    image: jenkins/jenkins:2.332.3-jdk11
+    container_name: jenkins
+    environment:
+      - TZ=Asia/Shanghai
+    user: root
+    volumes:
+      - /story/dist:/story/dist
+      - /mnt/data/docker/jenkins/jenkins_data:/var/jenkins_home
+      - /etc/localtime:/etc/localtime:ro
+    ports:
+      - "8099:8080"
+    restart: unless-stopped

jellyfin硬解

shell
# 安装驱动
+apt install intel-media-va-driver
+# 解码支持确认
+/usr/lib/jellyfin-ffmpeg/vainfo
# 安装驱动
+apt install intel-media-va-driver
+# 解码支持确认
+/usr/lib/jellyfin-ffmpeg/vainfo

image-20230828233842999

kafka

shell
wget https://downloads.apache.org/kafka/3.6.1/kafka_2.13-3.6.1.tgz
+ tar -xzvf kafka_2.13-3.6.1.tgz -C /usr/local
wget https://downloads.apache.org/kafka/3.6.1/kafka_2.13-3.6.1.tgz
+ tar -xzvf kafka_2.13-3.6.1.tgz -C /usr/local

zookeeper.service

shell
[Unit]
+Description=zookeeper
+
+[Service]
+Type=forking
+Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+ExecStart=/usr/local/kafka/bin/zookeeper-server-start.sh -daemon /usr/local/kafka/config/zookeeper.properties
+ExecStop=/usr/local/kafka/bin/zookeeper-server-start.sh stop
+SyslogIdentifier=zookeeper
+
+[Install]
+WantedBy=multi-user.target
[Unit]
+Description=zookeeper
+
+[Service]
+Type=forking
+Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+ExecStart=/usr/local/kafka/bin/zookeeper-server-start.sh -daemon /usr/local/kafka/config/zookeeper.properties
+ExecStop=/usr/local/kafka/bin/zookeeper-server-start.sh stop
+SyslogIdentifier=zookeeper
+
+[Install]
+WantedBy=multi-user.target

kafka.service

shell
[Unit]
+Description=kafka
+After=zookeeper.service
+
+[Service]
+Type=forking
+Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+ExecStart=/usr/local/kafka/bin/kafka-server-start.sh -daemon /usr/local/kafka/config/server.properties
+ExecStop=/usr/local/kafka/bin/kafka-server-stop.sh
+
+[Install]
+WantedBy=multi-user.target
[Unit]
+Description=kafka
+After=zookeeper.service
+
+[Service]
+Type=forking
+Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+ExecStart=/usr/local/kafka/bin/kafka-server-start.sh -daemon /usr/local/kafka/config/server.properties
+ExecStop=/usr/local/kafka/bin/kafka-server-stop.sh
+
+[Install]
+WantedBy=multi-user.target

onedev

shell
docker run -d \
+  --name onedev \
+  -v /var/run/docker.sock:/var/run/docker.sock \
+  -v /mnt/data/docker/onedev:/opt/onedev \
+  -p 6610:6610 \ # http
+  -p 6611:6611 \ # ssh
+  --restart=always \
+  1dev/server:latest
docker run -d \
+  --name onedev \
+  -v /var/run/docker.sock:/var/run/docker.sock \
+  -v /mnt/data/docker/onedev:/opt/onedev \
+  -p 6610:6610 \ # http
+  -p 6611:6611 \ # ssh
+  --restart=always \
+  1dev/server:latest

rabbitmq

shell
sudo apt install rabbitmq-server
+
+cd /usr/lib/rabbitmq/bin
+# 开启rabbit网页控制台 默认端口号15672,重启rabbitmq服务
+ ./rabbitmq-plugins enable rabbitmq_management
+ 
+# rabbitmq默认用户guest不允许远程登陆,且systemd默认的启动用户为rabbitmq,可以改为root
+cd /lib/systemd/system
+vim rabbitmq-server.service
+ 
+# 新建rabbitmq用户
+cd /usr/lib/rabbitmq/bin
+./rabbitmqctl add_user username password
+# 授权
+./rabbitmqctl set_user_tags username administrator
+./rabbitmqctl set_permissions -p "/" username ".*" ".*" ".*"
+ 
+# 查看、删除、修改密码
+./rabbitmqctl list_users
+./rabbitmqctl delete_user username
+./rabbitmqctl change_password username newpassword
sudo apt install rabbitmq-server
+
+cd /usr/lib/rabbitmq/bin
+# 开启rabbit网页控制台 默认端口号15672,重启rabbitmq服务
+ ./rabbitmq-plugins enable rabbitmq_management
+ 
+# rabbitmq默认用户guest不允许远程登陆,且systemd默认的启动用户为rabbitmq,可以改为root
+cd /lib/systemd/system
+vim rabbitmq-server.service
+ 
+# 新建rabbitmq用户
+cd /usr/lib/rabbitmq/bin
+./rabbitmqctl add_user username password
+# 授权
+./rabbitmqctl set_user_tags username administrator
+./rabbitmqctl set_permissions -p "/" username ".*" ".*" ".*"
+ 
+# 查看、删除、修改密码
+./rabbitmqctl list_users
+./rabbitmqctl delete_user username
+./rabbitmqctl change_password username newpassword

gitea

yaml
version: "3"
+
+networks:
+  gitea:
+    external: false
+
+volumes:
+  gitea:
+    driver: local
+
+services:
+  server:
+    image: gitea/gitea:1.16.7
+    container_name: gitea
+    environment:
+      - DOMAIN=192.168.2.66
+      - HTTP_PORT=6610
+      - SSH_PORT=6611
+      - SSH_LISTEN_PORT=6611
+    restart: always
+    networks:
+      - gitea
+    volumes:
+      - gitea:/data
+      - /etc/timezone:/etc/timezone:ro
+      - /etc/localtime:/etc/localtime:ro
+    ports:
+      - "6610:6610"
+      - "6611:6611"
version: "3"
+
+networks:
+  gitea:
+    external: false
+
+volumes:
+  gitea:
+    driver: local
+
+services:
+  server:
+    image: gitea/gitea:1.16.7
+    container_name: gitea
+    environment:
+      - DOMAIN=192.168.2.66
+      - HTTP_PORT=6610
+      - SSH_PORT=6611
+      - SSH_LISTEN_PORT=6611
+    restart: always
+    networks:
+      - gitea
+    volumes:
+      - gitea:/data
+      - /etc/timezone:/etc/timezone:ro
+      - /etc/localtime:/etc/localtime:ro
+    ports:
+      - "6610:6610"
+      - "6611:6611"

gitea webhook allowed host list

shell
/var/lib/docker/volumes/gitea_gitea/_data/gitea/conf/app.ini
+
+# add the following lines to the end of the file
+[webhook]
+ALLOWED_HOST_LIST = 192.168.2.66
/var/lib/docker/volumes/gitea_gitea/_data/gitea/conf/app.ini
+
+# add the following lines to the end of the file
+[webhook]
+ALLOWED_HOST_LIST = 192.168.2.66

gitea and jenkins webhook

In Jenkins: on the job settings page set "Source Code Management" option to "Git", provide URL to your repo (http://gitea-url.your.org/username/repo.git), and in "Poll triggers" section check "Poll SCM" option with no schedule defined. This setup basically tells Jenkins to poll your Gitea repo only when requested via the webhook.

In Gitea: under repo -> Settings -> Webhooks, add new webhook, set the URL to http://jenkins_url.your.org/gitea-webhook/post, and clear the secret (leave it blank).

At this point clicking on "Test Delivery" button should produce a successful delivery attempt (green checkmark).

kafdrop

shell
docker run -d --name kafkaui -p 9000:9000 \
+    -e KAFKA_BROKERCONNECT="192.168.2.66:9092"\
+    -e JVM_OPTS="-Xms32M -Xmx64M" \
+    -e SERVER_SERVLET_CONTEXTPATH="/" \
+    obsidiandynamics/kafdrop
docker run -d --name kafkaui -p 9000:9000 \
+    -e KAFKA_BROKERCONNECT="192.168.2.66:9092"\
+    -e JVM_OPTS="-Xms32M -Xmx64M" \
+    -e SERVER_SERVLET_CONTEXTPATH="/" \
+    obsidiandynamics/kafdrop

cadvisor Docker监控

yaml
version: '3'
+
+services:
+  cadvisor:
+    image: gcr.io/cadvisor/cadvisor:v0.47.2
+    container_name: cadvisor
+    volumes:
+      - /:/rootfs:ro
+      - /var/run:/var/run:ro
+      - /sys:/sys:ro
+      - /var/lib/docker/:/var/lib/docker:ro
+      - /dev/disk/:/dev/disk:ro
+    ports:
+      - "28080:8080"
+    privileged: true
+    restart: unless-stopped
+    devices:
+      - /dev/kmsg
version: '3'
+
+services:
+  cadvisor:
+    image: gcr.io/cadvisor/cadvisor:v0.47.2
+    container_name: cadvisor
+    volumes:
+      - /:/rootfs:ro
+      - /var/run:/var/run:ro
+      - /sys:/sys:ro
+      - /var/lib/docker/:/var/lib/docker:ro
+      - /dev/disk/:/dev/disk:ro
+    ports:
+      - "28080:8080"
+    privileged: true
+    restart: unless-stopped
+    devices:
+      - /dev/kmsg

grafana+prometheus+node_exporter监控linux系统

node_exporter

  • wget https://github.com/prometheus/node_exporter/releases/download/v1.6.1/node_exporter-1.6.1.linux-amd64.tar.gz
  • tar -zxvf node_exporter-1.6.1.linux-amd64.tar.gz && mv node_exporter-1.6.1.linux-amd64/node_exporter /usr/local/bin
systemd
shell
# 编写systemd服务
+cat > /etc/systemd/system/node_exporter.service <<EOF
+[Unit]
+Description=node_exporeter
+After=network.target
+[Service]
+Type=simple
+ExecStart=/usr/local/bin/node_exporter
+Restart=on-failure
+[Install]
+WantedBy=multi-user.target
+EOF
+
+# 更新内核并启动,自启动
+systemctl daemon-reload && systemctl start node_exporter && systemctl enable node_exporter && systemctl status node_exporter
# 编写systemd服务
+cat > /etc/systemd/system/node_exporter.service <<EOF
+[Unit]
+Description=node_exporeter
+After=network.target
+[Service]
+Type=simple
+ExecStart=/usr/local/bin/node_exporter
+Restart=on-failure
+[Install]
+WantedBy=multi-user.target
+EOF
+
+# 更新内核并启动,自启动
+systemctl daemon-reload && systemctl start node_exporter && systemctl enable node_exporter && systemctl status node_exporter
init.d
shell
# openwrt要使用init.d
+# /etc/init.d/node_exporter
+#!/bin/sh /etc/rc.common
+
+START=99
+STOP=10
+
+start() {
+    echo "Starting Node Exporter..."
+    /usr/bin/node_exporter --web.listen-address=":9100" > /dev/null 2>&1 &
+}
+
+stop() {
+    echo "Stopping Node Exporter..."
+    killall node_exporter
+}
+
+restart() {
+    stop
+    sleep 1
+    start
+}
# openwrt要使用init.d
+# /etc/init.d/node_exporter
+#!/bin/sh /etc/rc.common
+
+START=99
+STOP=10
+
+start() {
+    echo "Starting Node Exporter..."
+    /usr/bin/node_exporter --web.listen-address=":9100" > /dev/null 2>&1 &
+}
+
+stop() {
+    echo "Stopping Node Exporter..."
+    killall node_exporter
+}
+
+restart() {
+    stop
+    sleep 1
+    start
+}

/etc/init.d/node_exporter enable && /etc/init.d/node_exporter start

redis_exporter

  • wget https://github.com/oliver006/redis_exporter/releases/download/v1.46.0/redis_exporter-v1.46.0.linux-amd64.tar.gz
  • tar -xvf redis_exporter-v1.46.0.linux-amd64.tar.gz && mv redis_exporter-v1.46.0.linux-amd64/redis_exporter /usr/local/bin
shell
# 编写systemd服务
+cat > /etc/systemd/system/redis_exporter.service <<EOF
+[Unit]
+Description=redis_exporter
+After=network.target
+[Service]
+Type=simple
+ExecStart=/usr/local/bin/redis_exporter -redis.addr ip:port
+Restart=on-failure
+[Install]
+WantedBy=multi-user.target
+EOF
+
+# 更新内核并启动,自启动
+systemctl daemon-reload && systemctl start redis_exporter && systemctl enable redis_exporter && systemctl status redis_exporter
# 编写systemd服务
+cat > /etc/systemd/system/redis_exporter.service <<EOF
+[Unit]
+Description=redis_exporter
+After=network.target
+[Service]
+Type=simple
+ExecStart=/usr/local/bin/redis_exporter -redis.addr ip:port
+Restart=on-failure
+[Install]
+WantedBy=multi-user.target
+EOF
+
+# 更新内核并启动,自启动
+systemctl daemon-reload && systemctl start redis_exporter && systemctl enable redis_exporter && systemctl status redis_exporter

grafana+prometheus

yml
# docker-compose.yml
+
+version: "3"
+
+
+services:
+  grafana:
+    image: grafana/grafana
+    container_name: grafana
+    restart: unless-stopped
+    ports:
+      - 3000:3000
+    user: root
+    volumes:
+      - /mnt/data/docker/monitor/grafana/conf/grafana.ini:/etc/grafana/grafana.ini
+      - /mnt/data/docker/monitor/grafana/data:/var/lib/grafana
+      - /mnt/data/docker/monitor/grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards
+      - /mnt/data/docker/monitor/grafana/provisioning/datasources:/etc/grafana/provisioning/datasources
+    environment:
+      - TZ=Asia/shanghai
+  prometheus:
+    image: prom/prometheus
+    container_name: prometheus
+    restart: unless-stopped
+    ports:
+      - 9090:9090
+    volumes:
+      - /mnt/data/docker/monitor/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
+      - prometheus_data:/prometheus
+    environment:
+      - TZ=Asia/shanghai
+
+volumes:
+  prometheus_data:
# docker-compose.yml
+
+version: "3"
+
+
+services:
+  grafana:
+    image: grafana/grafana
+    container_name: grafana
+    restart: unless-stopped
+    ports:
+      - 3000:3000
+    user: root
+    volumes:
+      - /mnt/data/docker/monitor/grafana/conf/grafana.ini:/etc/grafana/grafana.ini
+      - /mnt/data/docker/monitor/grafana/data:/var/lib/grafana
+      - /mnt/data/docker/monitor/grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards
+      - /mnt/data/docker/monitor/grafana/provisioning/datasources:/etc/grafana/provisioning/datasources
+    environment:
+      - TZ=Asia/shanghai
+  prometheus:
+    image: prom/prometheus
+    container_name: prometheus
+    restart: unless-stopped
+    ports:
+      - 9090:9090
+    volumes:
+      - /mnt/data/docker/monitor/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
+      - prometheus_data:/prometheus
+    environment:
+      - TZ=Asia/shanghai
+
+volumes:
+  prometheus_data:

prometheus.yml

yml
global:
+  scrape_interval: 15s
+  evaluation_interval: 15s
+
+
+scrape_configs:
+  - job_name: "linux"
+    scrape_interval: 5s
+    static_configs:
+      - targets: [ "192.168.2.66:9100" ]
+        labels:
+          instance: home-server-ubuntu
+  - job_name: "redis"
+    scrape_interval: 5s
+    static_configs:
+      - targets: [ "192.168.2.66:9121" ]
+        labels:
+          instance: home-server-ubuntu
+  - job_name: "cadvisor"
+    static_configs:
+      - targets: [ "192.168.2.66:28080" ]
+        labels:
+          instance: home-server-ubuntu
global:
+  scrape_interval: 15s
+  evaluation_interval: 15s
+
+
+scrape_configs:
+  - job_name: "linux"
+    scrape_interval: 5s
+    static_configs:
+      - targets: [ "192.168.2.66:9100" ]
+        labels:
+          instance: home-server-ubuntu
+  - job_name: "redis"
+    scrape_interval: 5s
+    static_configs:
+      - targets: [ "192.168.2.66:9121" ]
+        labels:
+          instance: home-server-ubuntu
+  - job_name: "cadvisor"
+    static_configs:
+      - targets: [ "192.168.2.66:28080" ]
+        labels:
+          instance: home-server-ubuntu
  • grafana.ini 从空白容器里复制出一份
ini
docker cp grafana:/etc/grafana.ini ~/
docker cp grafana:/etc/grafana.ini ~/
  • grafana监控大盘模板

    • 12633(Linux主机详情)

    • 1860(Node Exporter Full)

    • 193(Docker monitoring)

    • 14282(Cadvisor exporter)

    • 11835( Redis Dashboard)

Bitwarden

https://bitwarden.com/help/install-on-premise-linux/

Configure your domain

配置域名解析,Bitwarden默认使用80443端口,可以执行安装后在bwdata/config.yaml修改端口

yaml
http_prt: 80
+https_port: 443
http_prt: 80
+https_port: 443

修改完bwdata/config.yaml后需要执行./bitwarden.sh rebuild

Install Docker and Docker Compose

curl -fsSL https://get.docker.com | sudo sh

Create a Bitwarden user & directory from which to complete installation.

shell
sudo adduser bitwarden
+sudo passwd bitwarden
+sudo groupadd docker
+sudo usermod -aG docker bitwarden
+sudo mkdir /opt/bitwarden
+sudo chmod -R 700 /opt/bitwarden
+sudo chown -R bitwarden:bitwarden /opt/bitwarden
sudo adduser bitwarden
+sudo passwd bitwarden
+sudo groupadd docker
+sudo usermod -aG docker bitwarden
+sudo mkdir /opt/bitwarden
+sudo chmod -R 700 /opt/bitwarden
+sudo chown -R bitwarden:bitwarden /opt/bitwarden

Retrieve an installation id and key from [**https://bitwarden.com/host

**](https://bitwarden.com/host/) for use in installation.

For more information, see What are my installation id and installation key used for?

Install Bitwarden on your machine.

shell
curl -Lso bitwarden.sh "https://func.bitwarden.com/api/dl/?app=self-host&platform=linux" && chmod 700 bitwarden.sh
+
+./bitwarden.sh install
curl -Lso bitwarden.sh "https://func.bitwarden.com/api/dl/?app=self-host&platform=linux" && chmod 700 bitwarden.sh
+
+./bitwarden.sh install

Configure your environment by adjusting settings in ./bwdata/env/global.override.env.

properties
globalSettings__mail__replyToEmail=email@example.com
+globalSettings__mail__smtp__host=smtp.qq.com
+globalSettings__mail__smtp__port=465
+globalSettings__mail__smtp__ssl=true
+globalSettings__mail__smtp__username=email@example.com
+globalSettings__mail__smtp__password=password
+
+globalSettings__disableUserRegistration=true # 禁止注册
globalSettings__mail__replyToEmail=email@example.com
+globalSettings__mail__smtp__host=smtp.qq.com
+globalSettings__mail__smtp__port=465
+globalSettings__mail__smtp__ssl=true
+globalSettings__mail__smtp__username=email@example.com
+globalSettings__mail__smtp__password=password
+
+globalSettings__disableUserRegistration=true # 禁止注册

修改完后执行./bitwarden.sh restart

Start your instance

./bitwarden.sh start

backing up your server

backup bwdata folder

migration

https://bitwarden.com/help/migration/

如果低版本迁移到高版本,覆盖bwdata后,先执行./bitwarden.sh update

Client

https://bitwarden.com/download

Hoppscotch

https://github.com/hoppscotch/hoppscotch

docker-compose.yml

yaml
version: '3.8'
+
+services:
+  hoppscotch:
+    container_name: hoppscotch
+    image: hoppscotch/hoppscotch
+    ports:
+      - "53000:3000"
+      - "53100:3100"
+      - "53170:3170"
+    env_file: .env
+    restart: unless-stopped
+    links:
+      - postgresql
+    depends_on:
+      - postgresql
+    networks:
+      - hoppscotch
+  postgresql:
+    container_name: postgresql
+    image: postgres
+    environment:
+      POSTGRES_DB: db
+      POSTGRES_USER: user
+      POSTGRES_PASSWORD: passwd
+    ports:
+      - "5432:5432"
+    volumes:
+      - postgres_data:/var/lib/postgresql/data
+    restart: unless-stopped
+    networks:
+      - hoppscotch
+
+volumes:
+  postgres_data:
+
+networks:
+  hoppscotch:
version: '3.8'
+
+services:
+  hoppscotch:
+    container_name: hoppscotch
+    image: hoppscotch/hoppscotch
+    ports:
+      - "53000:3000"
+      - "53100:3100"
+      - "53170:3170"
+    env_file: .env
+    restart: unless-stopped
+    links:
+      - postgresql
+    depends_on:
+      - postgresql
+    networks:
+      - hoppscotch
+  postgresql:
+    container_name: postgresql
+    image: postgres
+    environment:
+      POSTGRES_DB: db
+      POSTGRES_USER: user
+      POSTGRES_PASSWORD: passwd
+    ports:
+      - "5432:5432"
+    volumes:
+      - postgres_data:/var/lib/postgresql/data
+    restart: unless-stopped
+    networks:
+      - hoppscotch
+
+volumes:
+  postgres_data:
+
+networks:
+  hoppscotch:

.env

properties
#-----------------------Backend Config------------------------------#
+# Prisma Config
+DATABASE_URL=postgresql://user:passwd@postgresql:5432/db
+
+# Auth Tokens Config
+JWT_SECRET="xxx"
+TOKEN_SALT_COMPLEXITY=10
+MAGIC_LINK_TOKEN_VALIDITY= 3
+REFRESH_TOKEN_VALIDITY="604800000" # Default validity is 7 days (604800000 ms) in ms
+ACCESS_TOKEN_VALIDITY="86400000" # Default validity is 1 day (86400000 ms) in ms
+SESSION_SECRET='xxx'
+
+# Hoppscotch App Domain Config
+REDIRECT_URL="https://hoppscotch.example.com"
+WHITELISTED_ORIGINS="https://hoppscotch.example.com/backend,https://hoppscotch.example.com,https://hoppadmin.example.com"
+VITE_ALLOWED_AUTH_PROVIDERS=GITHUB
+
+# Google Auth Config
+#GOOGLE_CLIENT_ID="************************************************"
+#GOOGLE_CLIENT_SECRET="************************************************"
+#GOOGLE_CALLBACK_URL="http://hoppscotch.example.com:3170/v1/auth/google/callback"
+#GOOGLE_SCOPE="email,profilstoryxc
+
+# Github Auth Config
+GITHUB_CLIENT_ID="xxx"
+GITHUB_CLIENT_SECRET="xxx"
+GITHUB_CALLBACK_URL="https://hoppscotch.example.com/backend/v1/auth/github/callback"
+GITHUB_SCOPE="user:email"
+
+# Microsoft Auth Config
+#MICROSOFT_CLIENT_ID="************************************************"
+#MICROSOFT_CLIENT_SECRET="************************************************"
+#MICROSOFT_CALLBACK_URL="http://hoppscotch.example.com:3170/v1/auth/microsoft/callback"
+#MICROSOFT_SCOPE="user.read"
+#MICROSOFT_TENANT="common"
+
+# Mailer config
+MAILER_SMTP_URL="smtps://user@domain.com:passwd@smtp.domain.com"
+MAILER_ADDRESS_FROM="user@domain.com"
+
+# Rate Limit Config
+RATE_LIMIT_TTL=60 # In seconds
+RATE_LIMIT_MAX=100 # Max requests per IP
+
+
+#-----------------------Frontend Config------------------------------#
+
+
+# Base URLs
+VITE_BASE_URL=https://hoppscotch.example.com
+VITE_SHORTCODE_BASE_URL=https://hoppscotch.example.com
+VITE_ADMIN_URL=https://hoppadmin.example.com
+
+# Backend URLs
+VITE_BACKEND_GQL_URL=https://hoppscotch.example.com/backend/graphql
+VITE_BACKEND_WS_URL=wss://hoppscotch.example.com/backend/ws/graphql
+VITE_BACKEND_API_URL=https://hoppscotch.example.com/backend/v1
+
+# Terms Of Service And Privacy Policy Links (Optional)
+VITE_APP_TOS_LINK=https://docs.hoppscotch.io/support/terms
+VITE_APP_PRIVACY_POLICY_LINK=https://docs.hoppscotch.io/support/privacy
#-----------------------Backend Config------------------------------#
+# Prisma Config
+DATABASE_URL=postgresql://user:passwd@postgresql:5432/db
+
+# Auth Tokens Config
+JWT_SECRET="xxx"
+TOKEN_SALT_COMPLEXITY=10
+MAGIC_LINK_TOKEN_VALIDITY= 3
+REFRESH_TOKEN_VALIDITY="604800000" # Default validity is 7 days (604800000 ms) in ms
+ACCESS_TOKEN_VALIDITY="86400000" # Default validity is 1 day (86400000 ms) in ms
+SESSION_SECRET='xxx'
+
+# Hoppscotch App Domain Config
+REDIRECT_URL="https://hoppscotch.example.com"
+WHITELISTED_ORIGINS="https://hoppscotch.example.com/backend,https://hoppscotch.example.com,https://hoppadmin.example.com"
+VITE_ALLOWED_AUTH_PROVIDERS=GITHUB
+
+# Google Auth Config
+#GOOGLE_CLIENT_ID="************************************************"
+#GOOGLE_CLIENT_SECRET="************************************************"
+#GOOGLE_CALLBACK_URL="http://hoppscotch.example.com:3170/v1/auth/google/callback"
+#GOOGLE_SCOPE="email,profilstoryxc
+
+# Github Auth Config
+GITHUB_CLIENT_ID="xxx"
+GITHUB_CLIENT_SECRET="xxx"
+GITHUB_CALLBACK_URL="https://hoppscotch.example.com/backend/v1/auth/github/callback"
+GITHUB_SCOPE="user:email"
+
+# Microsoft Auth Config
+#MICROSOFT_CLIENT_ID="************************************************"
+#MICROSOFT_CLIENT_SECRET="************************************************"
+#MICROSOFT_CALLBACK_URL="http://hoppscotch.example.com:3170/v1/auth/microsoft/callback"
+#MICROSOFT_SCOPE="user.read"
+#MICROSOFT_TENANT="common"
+
+# Mailer config
+MAILER_SMTP_URL="smtps://user@domain.com:passwd@smtp.domain.com"
+MAILER_ADDRESS_FROM="user@domain.com"
+
+# Rate Limit Config
+RATE_LIMIT_TTL=60 # In seconds
+RATE_LIMIT_MAX=100 # Max requests per IP
+
+
+#-----------------------Frontend Config------------------------------#
+
+
+# Base URLs
+VITE_BASE_URL=https://hoppscotch.example.com
+VITE_SHORTCODE_BASE_URL=https://hoppscotch.example.com
+VITE_ADMIN_URL=https://hoppadmin.example.com
+
+# Backend URLs
+VITE_BACKEND_GQL_URL=https://hoppscotch.example.com/backend/graphql
+VITE_BACKEND_WS_URL=wss://hoppscotch.example.com/backend/ws/graphql
+VITE_BACKEND_API_URL=https://hoppscotch.example.com/backend/v1
+
+# Terms Of Service And Privacy Policy Links (Optional)
+VITE_APP_TOS_LINK=https://docs.hoppscotch.io/support/terms
+VITE_APP_PRIVACY_POLICY_LINK=https://docs.hoppscotch.io/support/privacy

hoppscotch.example.com.conf

nginx
server {
+    listen              443 ssl;
+    listen              [::]:443 ssl;
+    server_name         hoppscotch.example.com;
+
+    # SSL
+    ssl_certificate     /etc/nginx/ssl/hoppscotch.example.com.crt;
+    ssl_certificate_key /etc/nginx/ssl/hoppscotch.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.1 TLSv1.2;
+
+    # security
+
+    # logging
+    access_log          /var/log/nginx/access.log combined buffer=512k flush=1m;
+    error_log           /var/log/nginx/error.log warn;
+
+    # additional config
+
+    location  /backend/ws/ {
+        proxy_pass http://127.0.0.1:53170/;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "Upgrade";
+        proxy_set_header X-Real-IP $remote_addr;
+    }
+
+    location  /backend/ {
+        proxy_pass http://127.0.0.1:53170/;
+    }
+
+    location / {
+        proxy_pass http://127.0.0.1:53000/;
+    }
+}
+
+# HTTP redirect
+server {
+    listen      80;
+    listen      [::]:80;
+    server_name hoppscotch.example.com;
+    return      301 https://hoppscotch.example.com$request_uri;
+}
server {
+    listen              443 ssl;
+    listen              [::]:443 ssl;
+    server_name         hoppscotch.example.com;
+
+    # SSL
+    ssl_certificate     /etc/nginx/ssl/hoppscotch.example.com.crt;
+    ssl_certificate_key /etc/nginx/ssl/hoppscotch.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.1 TLSv1.2;
+
+    # security
+
+    # logging
+    access_log          /var/log/nginx/access.log combined buffer=512k flush=1m;
+    error_log           /var/log/nginx/error.log warn;
+
+    # additional config
+
+    location  /backend/ws/ {
+        proxy_pass http://127.0.0.1:53170/;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "Upgrade";
+        proxy_set_header X-Real-IP $remote_addr;
+    }
+
+    location  /backend/ {
+        proxy_pass http://127.0.0.1:53170/;
+    }
+
+    location / {
+        proxy_pass http://127.0.0.1:53000/;
+    }
+}
+
+# HTTP redirect
+server {
+    listen      80;
+    listen      [::]:80;
+    server_name hoppscotch.example.com;
+    return      301 https://hoppscotch.example.com$request_uri;
+}

kutt

https://github.com/thedevs-network/kutt

docker-compose.yml

yaml
version: "3"
+
+services:
+  kutt:
+    image: kutt/kutt
+    depends_on:
+      - postgres
+      - redis
+    command: [ "./wait-for-it.sh", "postgres:5432", "--", "npm", "start" ]
+    ports:
+      - "3000:3000"
+    env_file:
+      - .env
+    environment:
+      DB_HOST: postgres
+      DB_NAME: kutt
+      DB_USER: user
+      DB_PASSWORD: passwd
+      REDIS_HOST: redis
+
+  redis:
+    image: redis:6.0-alpine
+    volumes:
+      - redis_data:/data
+
+  postgres:
+    image: postgres:12-alpine
+    environment:
+      POSTGRES_USER: user
+      POSTGRES_PASSWORD: passwd
+      POSTGRES_DB: kutt
+    volumes:
+      - postgres_data:/var/lib/postgresql/data
+
+volumes:
+  redis_data:
+  postgres_data:
version: "3"
+
+services:
+  kutt:
+    image: kutt/kutt
+    depends_on:
+      - postgres
+      - redis
+    command: [ "./wait-for-it.sh", "postgres:5432", "--", "npm", "start" ]
+    ports:
+      - "3000:3000"
+    env_file:
+      - .env
+    environment:
+      DB_HOST: postgres
+      DB_NAME: kutt
+      DB_USER: user
+      DB_PASSWORD: passwd
+      REDIS_HOST: redis
+
+  redis:
+    image: redis:6.0-alpine
+    volumes:
+      - redis_data:/data
+
+  postgres:
+    image: postgres:12-alpine
+    environment:
+      POSTGRES_USER: user
+      POSTGRES_PASSWORD: passwd
+      POSTGRES_DB: kutt
+    volumes:
+      - postgres_data:/var/lib/postgresql/data
+
+volumes:
+  redis_data:
+  postgres_data:

.env

properties
# App port to run on
+PORT=3000
+
+# The name of the site where Kutt is hosted
+SITE_NAME=Kutt
+
+# The domain that this website is on
+DEFAULT_DOMAIN=kutt.domain.com
+
+# Generated link length
+LINK_LENGTH=5
+
+# Postgres database credential details
+DB_HOST=postgres
+DB_PORT=5432
+DB_NAME=postgres
+DB_USER=user
+DB_PASSWORD=passwd
+DB_SSL=false
+
+# Redis host and port
+REDIS_HOST=redis
+REDIS_PORT=6379
+REDIS_PASSWORD=
+REDIS_DB=
+
+# Disable registration
+DISALLOW_REGISTRATION=true
+
+# Disable anonymous link creation
+DISALLOW_ANONYMOUS_LINKS=true
+
+# The daily limit for each user
+USER_LIMIT_PER_DAY=50
+
+# Create a cooldown for non-logged in users in minutes
+# Set 0 to disable
+NON_USER_COOLDOWN=0
+
+# Max number of visits for each link to have detailed stats
+DEFAULT_MAX_STATS_PER_LINK=5000
+
+# Use HTTPS for links with custom domain
+CUSTOM_DOMAIN_USE_HTTPS=false
+
+# A passphrase to encrypt JWT. Use a long and secure key.
+JWT_SECRET=xxx
+
+# Admin emails so they can access admin actions on settings page
+# Comma seperated
+ADMIN_EMAILS=user@domain.com
+
+# Invisible reCaptcha secret key
+# Create one in https://www.google.com/recaptcha/intro/
+RECAPTCHA_SITE_KEY=
+RECAPTCHA_SECRET_KEY=
+
+# Google Cloud API to prevent from users from submitting malware URLs.
+# Get it from https://developers.google.com/safe-browsing/v4/get-started
+GOOGLE_SAFE_BROWSING_KEY=
+
+# Your email host details to use to send verification emails.
+# More info on http://nodemailer.com/
+# Mail from example "Kutt <support@kutt.it>". Leave empty to use MAIL_USER
+MAIL_HOST=smtp.domain.com
+MAIL_PORT=465
+MAIL_SECURE=true
+MAIL_USER=user@domain.com
+MAIL_FROM=user@domain.com
+MAIL_PASSWORD=passwd
+
+# The email address that will receive submitted reports.
+REPORT_EMAIL=
+
+# Support email to show on the app
+CONTACT_EMAIL=
# App port to run on
+PORT=3000
+
+# The name of the site where Kutt is hosted
+SITE_NAME=Kutt
+
+# The domain that this website is on
+DEFAULT_DOMAIN=kutt.domain.com
+
+# Generated link length
+LINK_LENGTH=5
+
+# Postgres database credential details
+DB_HOST=postgres
+DB_PORT=5432
+DB_NAME=postgres
+DB_USER=user
+DB_PASSWORD=passwd
+DB_SSL=false
+
+# Redis host and port
+REDIS_HOST=redis
+REDIS_PORT=6379
+REDIS_PASSWORD=
+REDIS_DB=
+
+# Disable registration
+DISALLOW_REGISTRATION=true
+
+# Disable anonymous link creation
+DISALLOW_ANONYMOUS_LINKS=true
+
+# The daily limit for each user
+USER_LIMIT_PER_DAY=50
+
+# Create a cooldown for non-logged in users in minutes
+# Set 0 to disable
+NON_USER_COOLDOWN=0
+
+# Max number of visits for each link to have detailed stats
+DEFAULT_MAX_STATS_PER_LINK=5000
+
+# Use HTTPS for links with custom domain
+CUSTOM_DOMAIN_USE_HTTPS=false
+
+# A passphrase to encrypt JWT. Use a long and secure key.
+JWT_SECRET=xxx
+
+# Admin emails so they can access admin actions on settings page
+# Comma seperated
+ADMIN_EMAILS=user@domain.com
+
+# Invisible reCaptcha secret key
+# Create one in https://www.google.com/recaptcha/intro/
+RECAPTCHA_SITE_KEY=
+RECAPTCHA_SECRET_KEY=
+
+# Google Cloud API to prevent from users from submitting malware URLs.
+# Get it from https://developers.google.com/safe-browsing/v4/get-started
+GOOGLE_SAFE_BROWSING_KEY=
+
+# Your email host details to use to send verification emails.
+# More info on http://nodemailer.com/
+# Mail from example "Kutt <support@kutt.it>". Leave empty to use MAIL_USER
+MAIL_HOST=smtp.domain.com
+MAIL_PORT=465
+MAIL_SECURE=true
+MAIL_USER=user@domain.com
+MAIL_FROM=user@domain.com
+MAIL_PASSWORD=passwd
+
+# The email address that will receive submitted reports.
+REPORT_EMAIL=
+
+# Support email to show on the app
+CONTACT_EMAIL=

kutt.domain.com.conf

nginx
server {
+    listen              443 ssl;
+    listen              [::]:443 ssl;
+    server_name         kutt.domain.com;
+
+    # SSL
+    ssl_certificate     /etc/nginx/ssl/kutt.domain.com.crt;
+    ssl_certificate_key /etc/nginx/ssl/kutt.domain.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.1 TLSv1.2;
+
+    # security
+    include             nginxconfig.io/security.conf;
+
+    # logging
+    access_log          /var/log/nginx/access_kutt.log combined buffer=512k flush=1m;
+    error_log           /var/log/nginx/error_kutt.log warn;
+
+    # additional config
+    #include             nginxconfig.io/general.conf;
+    location / {
+        proxy_pass http://127.0.0.1:3000;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    }
+
+}
+
+
+# HTTP redirect
+server {
+    listen      80;
+    listen      [::]:80;
+    server_name .kutt.domain.com;
+    return      301 https://kutt.domain.com$request_uri;
+}
server {
+    listen              443 ssl;
+    listen              [::]:443 ssl;
+    server_name         kutt.domain.com;
+
+    # SSL
+    ssl_certificate     /etc/nginx/ssl/kutt.domain.com.crt;
+    ssl_certificate_key /etc/nginx/ssl/kutt.domain.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.1 TLSv1.2;
+
+    # security
+    include             nginxconfig.io/security.conf;
+
+    # logging
+    access_log          /var/log/nginx/access_kutt.log combined buffer=512k flush=1m;
+    error_log           /var/log/nginx/error_kutt.log warn;
+
+    # additional config
+    #include             nginxconfig.io/general.conf;
+    location / {
+        proxy_pass http://127.0.0.1:3000;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    }
+
+}
+
+
+# HTTP redirect
+server {
+    listen      80;
+    listen      [::]:80;
+    server_name .kutt.domain.com;
+    return      301 https://kutt.domain.com$request_uri;
+}

NAStool

yaml
version: "3"
+services:
+  nas-tools:
+    image: nastool/nas-tools:latest
+    ports:
+      - 3000:3000        # 默认的webui控制端口
+    volumes:
+      - ./config:/config   # 冒号左边请修改为你想保存配置的路径
+      - /repo/others:/repo/others   # 媒体目录,多个目录需要分别映射进来,需要满足配置文件说明中的要求
+      - /repo/movies:/repo/movies
+      - /repo/tvseries:/repo/tvseries
+      - /repo/resources/link:/repo/resources/link
+    environment:
+      - PUID=1000    # 想切换为哪个用户来运行程序,该用户的uid
+      - PGID=1000    # 想切换为哪个用户来运行程序,该用户的gid
+      - UMASK=022 # 掩码权限,默认000,可以考虑设置为022
+      - NASTOOL_AUTO_UPDATE=false  # 如需在启动容器时自动升级程程序请设置为true
+      - NASTOOL_CN_UPDATE=false # 如果开启了容器启动自动升级程序,并且网络不太友好时,可以设置为true,会使用国内源进行软件更新
+      #- REPO_URL=https://ghproxy.com/https://github.com/NAStool/nas-tools.git  # 当你访问github网络很差时,可以考虑解释本行注释
+    restart: always
+    network_mode: bridge
+    hostname: nas-tools
+    container_name: nastool
version: "3"
+services:
+  nas-tools:
+    image: nastool/nas-tools:latest
+    ports:
+      - 3000:3000        # 默认的webui控制端口
+    volumes:
+      - ./config:/config   # 冒号左边请修改为你想保存配置的路径
+      - /repo/others:/repo/others   # 媒体目录,多个目录需要分别映射进来,需要满足配置文件说明中的要求
+      - /repo/movies:/repo/movies
+      - /repo/tvseries:/repo/tvseries
+      - /repo/resources/link:/repo/resources/link
+    environment:
+      - PUID=1000    # 想切换为哪个用户来运行程序,该用户的uid
+      - PGID=1000    # 想切换为哪个用户来运行程序,该用户的gid
+      - UMASK=022 # 掩码权限,默认000,可以考虑设置为022
+      - NASTOOL_AUTO_UPDATE=false  # 如需在启动容器时自动升级程程序请设置为true
+      - NASTOOL_CN_UPDATE=false # 如果开启了容器启动自动升级程序,并且网络不太友好时,可以设置为true,会使用国内源进行软件更新
+      #- REPO_URL=https://ghproxy.com/https://github.com/NAStool/nas-tools.git  # 当你访问github网络很差时,可以考虑解释本行注释
+    restart: always
+    network_mode: bridge
+    hostname: nas-tools
+    container_name: nastool

telegram-bot-api

申请项目

https://core.telegram.org/api/obtaining_api_id

编译项目

repo:https://github.com/tdlib/telegram-bot-api

generator:https://tdlib.github.io/telegram-bot-api/build.html

shell
apt-get update
+apt-get upgrade
+apt-get install make git zlib1g-dev libssl-dev gperf cmake g++
+git clone --recursive https://github.com/tdlib/telegram-bot-api.git
+cd telegram-bot-api
+rm -rf build
+mkdir build
+cd build
+cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr/local ..
+cmake --build . --target install
+cd ../..
+ls -l /usr/local/bin/telegram-bot-api*
apt-get update
+apt-get upgrade
+apt-get install make git zlib1g-dev libssl-dev gperf cmake g++
+git clone --recursive https://github.com/tdlib/telegram-bot-api.git
+cd telegram-bot-api
+rm -rf build
+mkdir build
+cd build
+cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr/local ..
+cmake --build . --target install
+cd ../..
+ls -l /usr/local/bin/telegram-bot-api*

配置启动

systemctl edit --force --full tgbot-api.service

shell
[Unit]
+Description=telegram bot api
+After=network.target
+[Service]
+Environment="TELEGRAM_API_ID=xxx"
+Environment="TELEGRAM_API_HASH=xxx"
+ExecStart=/usr/local/bin/telegram-bot-api --http-port=16666 --local --log=/var/log/telegram-bot-api/tg-bot-api.log
+[Install]
+WantedBy=multi-user.target
[Unit]
+Description=telegram bot api
+After=network.target
+[Service]
+Environment="TELEGRAM_API_ID=xxx"
+Environment="TELEGRAM_API_HASH=xxx"
+ExecStart=/usr/local/bin/telegram-bot-api --http-port=16666 --local --log=/var/log/telegram-bot-api/tg-bot-api.log
+[Install]
+WantedBy=multi-user.target

immich

https://github.com/immich-app/immich

memos

https://github.com/usememos/memos

+ + + + \ No newline at end of file diff --git "a/tinker/network/openwrt\345\256\211\350\243\205\345\217\212\351\205\215\347\275\256.html" "b/tinker/network/openwrt\345\256\211\350\243\205\345\217\212\351\205\215\347\275\256.html" new file mode 100644 index 000000000..d06c98ef1 --- /dev/null +++ "b/tinker/network/openwrt\345\256\211\350\243\205\345\217\212\351\205\215\347\275\256.html" @@ -0,0 +1,47 @@ + + + + + + openwrt安装及配置 | 故事 + + + + + + + + + + + + + + +
Skip to content

openwrt安装及配置

趁着五一假期把家用服务器装好跑起来了,另外还买了一台J4125工控机来做软路由。工控机的机器今天刚到,下午反复折腾了pve虚拟机安装openwrt和物理机直接安装,目前是用物理机装好配置完毕了,但是看了一眼这监控数据,总感觉有点浪费性能。所以这篇文章写完我还是要换回pve安装(doge),剩下的性能折腾下其他虚拟机。

物理机安装

下载需要的软件

txt
1. pe工具箱
+2. openwrt编译好的镜像,我用的是esir的固件
+3. 刷写镜像的软件physdiskwrite.exe
1. pe工具箱
+2. openwrt编译好的镜像,我用的是esir的固件
+3. 刷写镜像的软件physdiskwrite.exe

准备工作

txt
1. pe工具箱安装到U盘中
+2. 把解压后的x86.img镜像文件和physdiskwrite.exe软件复制到U盘中
+3. 这时候已经可以拔掉硬路由器了,光猫lan口连软路由wan口(要问卖家哪个是wan口,我这个eth1是wan口),pc连软路由lan口(0、2、3),这是为了pc能跟软路由在同一个网段,如果不在则需要手动配置静态ip
1. pe工具箱安装到U盘中
+2. 把解压后的x86.img镜像文件和physdiskwrite.exe软件复制到U盘中
+3. 这时候已经可以拔掉硬路由器了,光猫lan口连软路由wan口(要问卖家哪个是wan口,我这个eth1是wan口),pc连软路由lan口(0、2、3),这是为了pc能跟软路由在同一个网段,如果不在则需要手动配置静态ip

安装流程

txt
1. u盘启动工控机
+2. 用pe工具箱自带的diskgenius把装的硬盘格式化即可,注意这里不要进行分区,否则会写盘失败
+3. 打开cmd窗口,执行命令 physdiskwrite.exe -u x86.img,这里可以直接用鼠标把程序拖到cmd窗口,会自动拼出完整路径
+4. 根据终端输出选择要写入的硬盘,比如0是硬盘,1是u盘,就填0,enter即可开始写盘,等待写盘完毕即可
+5. 扩容系统分区,使用diskgenius找到刚刚写入的硬盘,然后再选中上面灰色未使用的分区,右击菜单中选择 “将空间分配给” -> “分区:未格式化(D:)”,然后确认即可
+6. 写盘完毕,选择重启,这时候直接拔出U盘即可
+7. 等待重启后,openwrt系统就已经成功启动了,如果不确定是否安装完,回车一下看看是否有lede的banner输出就行了
+8. openwrt一般ip为192.168.2.1(esir固件是5.1),这个可以看固件wiki或者自己ifconfig看一下
1. u盘启动工控机
+2. 用pe工具箱自带的diskgenius把装的硬盘格式化即可,注意这里不要进行分区,否则会写盘失败
+3. 打开cmd窗口,执行命令 physdiskwrite.exe -u x86.img,这里可以直接用鼠标把程序拖到cmd窗口,会自动拼出完整路径
+4. 根据终端输出选择要写入的硬盘,比如0是硬盘,1是u盘,就填0,enter即可开始写盘,等待写盘完毕即可
+5. 扩容系统分区,使用diskgenius找到刚刚写入的硬盘,然后再选中上面灰色未使用的分区,右击菜单中选择 “将空间分配给” -> “分区:未格式化(D:)”,然后确认即可
+6. 写盘完毕,选择重启,这时候直接拔出U盘即可
+7. 等待重启后,openwrt系统就已经成功启动了,如果不确定是否安装完,回车一下看看是否有lede的banner输出就行了
+8. openwrt一般ip为192.168.2.1(esir固件是5.1),这个可以看固件wiki或者自己ifconfig看一下

配置

上网相关配置

  1. 编辑lan口

image-20220503222750537

  1. 选择另外两个lan口,共三个lan口

image-20220503221537487

  1. 开启强制dhcp服务

image-20220503221601540

  1. 保存&应用
  2. 配置软路由拨号,这之前需要先把光猫设为桥接模式,参考前一篇博客移动光猫改桥接模式

image-20220503222634621

选择PPPoE协议并切换,填上宽带账号密码保存&应用即可

image-20220503222905747

固定ip配置

  1. 选择DHCP/DNS菜单,找到静态地址分配

image-20220503221716649

可以根据设备mac地址,自定义主机名和ip,并设置租期,这里一般我们设置静态ip都是永久的,填infinite。保存&应用,然后重启软路由即可。

这里可能重启软路由和设备后也不会让静态ip配置生效,网上找到了一篇文章的分析,跟第一次获取的ip租期未到期有管,可以手动释放旧的租约,然后刷新租约。windows下ipconfig /release & ipconfig /renew,linux下dhclient -r & dhclient -s 192.168.2.1

以下为原文:https://www.csdn.net/tags/NtjakgwsOTY3ODMtYmxvZwO0O0OO0O0O.html

我在使用 Openwrt 时手动分配了新的静态 IP 给我的电脑,但是在保存并应用后并没有立即生效,而且在我分别重启了电脑和路由器后仍然没有生效,为此我花了点时间找出了解决方法。

原因分析

在“DHCP/DNS->静态地址分配”中给电脑配置了静态地址不会立即生效,因为在此之前路由器已经通过 DHCP 分配了 IP 地址给电脑形成租约,在这个租约到期之前不会改变分配给电脑的 IP。通常我们在 Lan 中设置租约时间为 12h(小时),也就意味着要在 12 小时后电脑才会获取到我们设置的静态 IP。

不过我们可以清空路由器上的旧租约,同时将电脑断网重连,以此来使电脑获得新 IP 地址。最简单的方法就是将路由器重启,既清空了旧租约,又使电脑重连。但是为什么我之前重启会不起作用呢?

说实话这锅还真不好甩,我的电脑是 Win10 系统,在我重启路由器后,系统并不是向路由器请求一份新的租约,而是拿着旧的租约想要更新续约。这里你可能认为是路由器就直接续约了,但我认为并不是,OpenWrt 已经设定了静态地址,而电脑请求续约的 IP 不一样,结果是 OpenWrt 不会给续约,但也不会返回新的租约。

最终导致的结果就是电脑租约无法更新,但由于租约也没有到期,所以电脑继续使用旧的,而正好使用旧IP还能正常上网就一直沿用旧租约了。

解决方法

最简单的方法,设置的静态 IP 为原本 DHCP 获取到的 IP 地址,这样就不会存在不生效问题。但一定要更换 IP 的话,保证 OpenWrt 已重启,打开 Windows 命令行或者 Power Shell,输入以下命令执行:ipconfig /release

ipconfig /renew

第一条命令删除旧租约,这样就不会由于 IP 地址错误导致 OpenWrt 无法返回新租约,第二条命令就是手动更新租约。至此,解决了静态 IP 分配不生效的问题。

PVE虚拟机安装

参考b站up主司波图的教程

+ + + + \ No newline at end of file diff --git "a/tinker/network/openwrt\345\274\200\345\220\257ipv6.html" "b/tinker/network/openwrt\345\274\200\345\220\257ipv6.html" new file mode 100644 index 000000000..8635879b6 --- /dev/null +++ "b/tinker/network/openwrt\345\274\200\345\220\257ipv6.html" @@ -0,0 +1,25 @@ + + + + + + openwrt开启ipv6 | 故事 + + + + + + + + + + + + + + +
Skip to content

openwrt开启ipv6

背景

由于移动宽带没有公网ip,玩儿pt上传上不去,所以考虑开启ipv6提速

具体操作

全局网络选项中清除IPv6 ULA前缀

image-20220510221313367

获取IPv6地址改为自动

image-20220510221440677

image-20220510221714830

DHCP/DNS中取消禁止解析IPv6 DNS记录的勾选

image-20220510221817531

重启openwrt

+ + + + \ No newline at end of file diff --git "a/tinker/network/pt\344\270\213\350\275\275\345\205\245\351\227\250.html" "b/tinker/network/pt\344\270\213\350\275\275\345\205\245\351\227\250.html" new file mode 100644 index 000000000..36dcae035 --- /dev/null +++ "b/tinker/network/pt\344\270\213\350\275\275\345\205\245\351\227\250.html" @@ -0,0 +1,25 @@ + + + + + + pt下载入门 | 故事 + + + + + + + + + + + + + + +
Skip to content

pt下载入门

什么是pt

PT(Private Tracker):是一种基于私有BT Tracker服务器的资源传播形式,经授权的用户使用受允许的客户端进行种子制作与下载,对参与的用户做出流量统计(需要用户完成一定量的上传)。

pt的优缺点

上面提到PT的关键词:私有、小范围、流量统计。传统的BT则是公有、大范围(整个互联网)、不统计流量。BT是公开的tracker,范围很广,但是有很多人只下载不上传(h&r/hit and run),俗称白嫖,因此无法保证种子的下载速度。PT则解决了这一痛点,强制要求用户上传,换来了高速下载,另外pt站点的资源质量很高,资源更新更快。当然PT的局限就是圈子小,入门门槛高,往往pt站点采用邀请制+捐赠制。

玩儿pt的必要条件

硬件准备

家用pc、nas、服务器、能装下载工具的路由器、各种下载机(玩客云、n1盒子等)均可

硬盘

越大越好

pt站点邀请码获取途径

  • 捐赠入站

  • 通过别人邀请

  • pt站点开放注册时注册加入

pt入门

基本概念

  • 种子:根据 BitTorrent 协议,文件发布者会根据要发布的文件生成提供一个.torrent 文件,即种子文件,也简称为“种子”。种子文件本质上是文本文件,包含 Tracker信息和文件信息两部分。Tracker 信息主要是 BT 下载中需要用到的 Tracker 服务器的地址和针对 Tracker 服务器的设置,文件信息是根据 对目标文件的计算生成的,计算结果根据 BitTorrent 协议内的规则进行编码。它的 主要原理是需要把提供下载的文件虚拟分成大小相等的块,块大小必须为 2k 的整数次方(由于是虚拟分块,硬盘上并不产生各个块文件),并把每个块的索引信息和 Hash 验证码写入种子文件中;所以,种子文件就是被下载文件的“索引”。 下载者要下载文件内容,需要先得到相应的种子文件,然后使用 BT 客户端软件进行下载。下载时,BT 客户端首先解析种子文件得到 Tracker 地址,然后连接 Tracker 服务器。Tracker 服务器回应下载者的请求,提供下载者其他下载者(包括发布者)的 IP。下载者再连接其他 下载者,根据种子文件,两者分别告知对方自己已经有的块,然后交换对方所没有的数据。 此时不需要其他服务器参与,分散了单个线路上的数据流量,因此减轻了服务器负担。 下载者每得到一个块,需要算出下载块的 Hash 验证码与种子文件中的对比,如果一样则说 明块正确,不一样则需要重新下载这个块。这种规定是为了解决下载内容淮确性的问题。 一般的 HTTP与FTP 下载,发布文件仅在某个或某几个服务器,下载的人太多,服务器的带宽 很易不胜负荷,变得很慢。而 BitTorrent 协议下载的特点是,下载的人越多,提供的带宽也 越多,下载速度就越快。同时,拥有完整文件的用户也会越来越多,使文件的“寿命”不断延长。

  • Tarcker: 收集下载者信息的服务器,并将此信息提供给其他下载者,可以理解为电话总机。

  • 做种:资源下载完毕后不删除任务,保持继续上传的过程

  • 辅种:其他人发布了资源,你手里刚好也有这个资源,那么你下载种子之后。只要数据通过了 hash 校验就会变成做种状态。(部分站点禁止)

  • 上传量:做种时上传的流量

  • 下载量:下载资源的流量

  • 分享率:上传量/下载量

  • 做种率:做种时间/下载时间

  • 魔力值:pt站点的积分,可以通过做种、签到等途径获取

  • 最小做种时间:为了保证种子的活跃度,一些PT站点严禁H&R(Hit and Run、下了就跑),要求用户至少持续做种一段时间

新手考核

类似游戏的新手区教学,让新手了解PT站点的规则,PT站点还会对新人进行考核,一般考核期为一个月,考核主要从以下几点:

考核点说明要求
上传量上传了多少数据30G-100G不等
下载量下载了多少数据30G-100G不等
分享率上传量/下载量一般要求分享率>1
做种率做种时间/下载时间一般要求3-8不等
魔力值通过签到、做种等途径获取的积分一般要求3000-8000不等

生存指南

考核期

老老实实下热门种子并持续做种

考核后

对于一些以影视作品为主的网站,尽可能的下载热门种子,这样能更快的获得上传量。可以使用 BT 客户端的 RSS 订阅功能,实现无人值守下载。 对于以小种为主的 PT 站,如 OpenCD 以及大部分教育网站点,则需要通过下载大量小体积种子并长时间做种以换取魔力值,再使用魔力值兑换上传。

养老期

保证良好分享率

技巧

种子挑选

  • 在种子板块新发布且带有Free标签的种子是不计算下载量的

  • 带有2xFree是不计算下载量且计算双倍上传流量的,一般选这种种子数据会更好看

  • 要注意免费时常

  • 进到种子页面,看种子的做种者数量和下载者数量,挑做种人少&下载人多的。

  • 无标记为正常计算上传、下载量

  • 带有%50标记为计算50%下载量

  • 带有2x%50为计算%50下载量、计算两倍上传量

提高分享率

不断下载免费种子,多维持一些,种子体积可以挑选大一点的,然后挂机做种提升上传量,在下载量不变的情况下,上传越高分量率就越高。

赚魔力值

  • 发布种子
  • 上传字幕
  • 发表主题贴
  • 辅种挂机

常用站点

+ + + + \ No newline at end of file diff --git "a/tinker/network/ubuntu-server\345\274\200\345\220\257\347\275\221\347\273\234\345\224\244\351\206\222.html" "b/tinker/network/ubuntu-server\345\274\200\345\220\257\347\275\221\347\273\234\345\224\244\351\206\222.html" new file mode 100644 index 000000000..70a01e991 --- /dev/null +++ "b/tinker/network/ubuntu-server\345\274\200\345\220\257\347\275\221\347\273\234\345\224\244\351\206\222.html" @@ -0,0 +1,73 @@ + + + + + + ubuntu-server开启网络唤醒和通电自动启动 | 故事 + + + + + + + + + + + + + + +
Skip to content

ubuntu-server开启网络唤醒和通电自动启动

前提

主板支持WOL(Wake on LAN)功能和来电开机

配置

主板BIOS中开启网络唤醒功能和断电开机功能

https://endownload.colorful.cn/EnDownload/MotherBroard/2022/Intel 600/Manual/Intel 600 Series BIOS Chinese/Intel 600 Series BIOS User Guide.pdf

以七彩虹主板为例

txt
ADVANCED(高级模式)> Power Management Configuration(电源管理配置)> Wake By Lan(网卡唤醒)
+Wake By Lan(网卡唤醒) 
+设置网络唤醒功能。 
+[Enabled] 当检测到 LAN 设备已激活或有信号输入时,唤醒系统。 
+[Disabled] 关闭网络唤醒功能。
+
+
+ADVANCED(高级模式)> Power Management Configuration(电源管理配置)> AC Power Loss(断电开机功能) 
+AC Power Loss(断电开机功能) 
+设置计算机断电之后,电源再次被接通时计算机的响应状态。 
+[Power On] 通电后计算机自动开机。 
+[Power Off] 通电后计算机保持关机状态。 
+[Last State]] 通电后计算机恢复上次断电前的状态。
ADVANCED(高级模式)> Power Management Configuration(电源管理配置)> Wake By Lan(网卡唤醒)
+Wake By Lan(网卡唤醒) 
+设置网络唤醒功能。 
+[Enabled] 当检测到 LAN 设备已激活或有信号输入时,唤醒系统。 
+[Disabled] 关闭网络唤醒功能。
+
+
+ADVANCED(高级模式)> Power Management Configuration(电源管理配置)> AC Power Loss(断电开机功能) 
+AC Power Loss(断电开机功能) 
+设置计算机断电之后,电源再次被接通时计算机的响应状态。 
+[Power On] 通电后计算机自动开机。 
+[Power Off] 通电后计算机保持关机状态。 
+[Last State]] 通电后计算机恢复上次断电前的状态。

ubuntu-server中开启网卡的网络唤醒功能

使用ethtool查看信息

shell
# 如果没有需要先安装 apt install ethtool
+# 首先使用ifconfig或ip a查看设备名称 我的是enp3s0
+
+ethtool enp3s0 | grep "Wake-on"
+	Supports Wake-on: pumbg
+	Wake-on: d
# 如果没有需要先安装 apt install ethtool
+# 首先使用ifconfig或ip a查看设备名称 我的是enp3s0
+
+ethtool enp3s0 | grep "Wake-on"
+	Supports Wake-on: pumbg
+	Wake-on: d

这个信息说明支持pumbg几种唤醒方式,而d表示当前处于禁用状态

"Wake-on" 中的不同字母代表不同的唤醒模式 这些字母分别代表以下内容:

  • p:代表PHY(物理层)唤醒模式,这种模式是基于物理层的唤醒模式。
  • u:代表UDP数据包唤醒模式,这种模式需要特定的UDP数据包来唤醒设备。
  • m:代表多播唤醒模式,这允许多播数据包来唤醒设备。
  • b:代表广播唤醒模式,这允许广播数据包来唤醒设备。
  • g:代表魔术唤醒帧模式,这是一种常用的唤醒模式,需要特定的魔术唤醒数据包。

魔术唤醒数据包(Magic Wake-on-LAN Packet)是一种特殊的数据包,用于远程唤醒计算机或网络设备。它通常用于通过局域网远程唤醒处于休眠或关机状态的设备,以便进行远程管理或访问。这些数据包被称为"魔术",因为它们包含了一些特定的唤醒模式信息,以便网络接口卡能够识别并唤醒目标设备。

魔术唤醒数据包的结构通常包括以下元素:

  1. 目标设备的MAC地址:这是数据包的目标设备的物理地址,以便网络接口卡知道唤醒哪台设备。
  2. 以太网帧:数据包以标准的以太网帧格式进行封装。
  3. 魔术唤醒模式信息:这些信息告诉网络接口卡以特定方式处理数据包,以实现唤醒功能。

启用网络唤醒功能

ethtool -s enp3s0 wol g

重启后网络唤醒会失效,配置网络唤醒持久化

shell
sudo systemctl edit --force --full wol-enable.service
sudo systemctl edit --force --full wol-enable.service

可以使用update-alternatives --config editor修改默认编辑器

shell
[Unit]
+Description=Enable Wake-on-LAN
+
+[Service]
+ExecStart=/usr/sbin/ethtool -s enp3s0 wol g
+
+[Install]
+WantedBy=multi-user.target
[Unit]
+Description=Enable Wake-on-LAN
+
+[Service]
+ExecStart=/usr/sbin/ethtool -s enp3s0 wol g
+
+[Install]
+WantedBy=multi-user.target

systemctl daemon-reload && systemctl enable wol-enable.service && systemctl start wol-enable.service

网络唤醒调用

不同平台都有相对应的唤醒客户端,我主要是从软路由使用这个功能。使用Etherwake选中要唤醒的设备点击唤醒主机即可发送数据包成功唤醒。

image-20231020231608248

来电自启

可以配合UPS使用实现断电安全关机以及来电自动启动,详情见另一篇关于UPS和NUT的博客。

+ + + + \ No newline at end of file diff --git "a/tinker/network/windows\346\214\202\350\275\275webdav\347\232\204\351\227\256\351\242\230\345\244\204\347\220\206.html" "b/tinker/network/windows\346\214\202\350\275\275webdav\347\232\204\351\227\256\351\242\230\345\244\204\347\220\206.html" new file mode 100644 index 000000000..4da9f9ab2 --- /dev/null +++ "b/tinker/network/windows\346\214\202\350\275\275webdav\347\232\204\351\227\256\351\242\230\345\244\204\347\220\206.html" @@ -0,0 +1,25 @@ + + + + + + windows挂载webdav的问题处理 | 故事 + + + + + + + + + + + + + + +
Skip to content

windows挂载webdav的问题处理

背景

home server里开着阿里云盘的webdav容器,想要在pc的windows中挂载,之前用的RaiDriver这个软件,但是开机启动总弹广告,遂弃用。用windows原生的挂载方式,直接在资源管理器中右键选择添加一个网络位置,填写http的webdav服务地址+端口后提示输入的文件夹似乎无效

解决方案

出现这个提示是因为windows本身的权限控制,可以在注册表中修改相关配置。

具体操作:

  • 修改注册表\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\ParametersBasicAuthLevel的值为2

  • 在服务中把WebClient启动,并把启动类型改为自动

然后就可以在资源管理器中再次尝试添加网络位置,输入ip+端口即可。

+ + + + \ No newline at end of file diff --git "a/tinker/network/\344\275\277\347\224\250https\350\256\277\351\227\256\345\206\205\347\275\221\346\234\215\345\212\241.html" "b/tinker/network/\344\275\277\347\224\250https\350\256\277\351\227\256\345\206\205\347\275\221\346\234\215\345\212\241.html" new file mode 100644 index 000000000..0f071a2ae --- /dev/null +++ "b/tinker/network/\344\275\277\347\224\250https\350\256\277\351\227\256\345\206\205\347\275\221\346\234\215\345\212\241.html" @@ -0,0 +1,81 @@ + + + + + + 使用https访问内网服务 | 故事 + + + + + + + + + + + + + + +
Skip to content

使用https访问内网服务

https://github.com/linuxserver/docker-swag

配置dns解析

将需要的子域名使用ddns解析到wan口ip

openwrt启动docker-swag容器

yaml
# docker-compose.yml
+
+version: "3"
+services:
+  swag:
+    image: linuxserver/swag:latest
+    container_name: swag
+    cap_add:
+      - NET_ADMIN
+    environment:
+      - PUID=1000
+      - PGID=1000
+      - TZ=Asia/Shanghai # 时区
+      - URL=yourdomain.com # 主域名
+      - VALIDATION=dns # certbot验证的方法,一般选dns
+      - SUBDOMAINS=yoursubdoamin.yourdomain.com
+      - CERTPROVIDER= # 可以填zerossl,默认使用let's encrypt签发证书
+      - DNSPLUGIN=dnspod # 支持aliyun、dnspod、cloudflare等等,详情见官方文档
+      - PROPAGATION= # 选择覆盖dns插件的默认传播时间(以秒为单位)
+      - EMAIL=user@email.com # 邮箱
+      - ONLY_SUBDOMAINS=true # 是否只获取子域名的证书
+      - EXTRA_DOMAINS= # 其他完全限定域名(逗号分隔,无空格)例如 extradomain.com,subdomain.anotherdomain.org
+      - STAGING=false # 设置为 true 以在暂存模式下检索证书。但生成的证书将无法通过浏览器的安全测试。仅用于测试。
+    volumes:
+      - /docker/swag/config:/config
+    ports:
+      - 65111:443 # 映射端口,家宽可以选择高位端口
+#      - 80:80 #optional
+    restart: unless-stopped
# docker-compose.yml
+
+version: "3"
+services:
+  swag:
+    image: linuxserver/swag:latest
+    container_name: swag
+    cap_add:
+      - NET_ADMIN
+    environment:
+      - PUID=1000
+      - PGID=1000
+      - TZ=Asia/Shanghai # 时区
+      - URL=yourdomain.com # 主域名
+      - VALIDATION=dns # certbot验证的方法,一般选dns
+      - SUBDOMAINS=yoursubdoamin.yourdomain.com
+      - CERTPROVIDER= # 可以填zerossl,默认使用let's encrypt签发证书
+      - DNSPLUGIN=dnspod # 支持aliyun、dnspod、cloudflare等等,详情见官方文档
+      - PROPAGATION= # 选择覆盖dns插件的默认传播时间(以秒为单位)
+      - EMAIL=user@email.com # 邮箱
+      - ONLY_SUBDOMAINS=true # 是否只获取子域名的证书
+      - EXTRA_DOMAINS= # 其他完全限定域名(逗号分隔,无空格)例如 extradomain.com,subdomain.anotherdomain.org
+      - STAGING=false # 设置为 true 以在暂存模式下检索证书。但生成的证书将无法通过浏览器的安全测试。仅用于测试。
+    volumes:
+      - /docker/swag/config:/config
+    ports:
+      - 65111:443 # 映射端口,家宽可以选择高位端口
+#      - 80:80 #optional
+    restart: unless-stopped

启动容器后操作

  1. config/dns-conf目录中找到自己选择的dns插件的配置文件,按要求填写验证信息

  2. config/nginx/proxy-confs基于给出的模板配置文件修改出自己的虚拟主机配置,将请求反向代理到内网的指定服务

  3. 防火墙中配置端口转发规则:将要从外网访问的高位端口(例如65222)转发到上面映射的端口65111

重启容器

此时即可通过https://subdomain.domain.com:65222来访问对应的内网服务

+ + + + \ No newline at end of file diff --git "a/tinker/network/\345\261\261\347\211\271ups\351\205\215\345\220\210nut\345\256\236\347\216\260\346\226\255\347\224\265\345\256\211\345\205\250\345\205\263\346\234\272.html" "b/tinker/network/\345\261\261\347\211\271ups\351\205\215\345\220\210nut\345\256\236\347\216\260\346\226\255\347\224\265\345\256\211\345\205\250\345\205\263\346\234\272.html" new file mode 100644 index 000000000..244cc31e0 --- /dev/null +++ "b/tinker/network/\345\261\261\347\211\271ups\351\205\215\345\220\210nut\345\256\236\347\216\260\346\226\255\347\224\265\345\256\211\345\205\250\345\205\263\346\234\272.html" @@ -0,0 +1,79 @@ + + + + + + 山特ups配合nut实现断电安全关机 | 故事 + + + + + + + + + + + + + + +
Skip to content

山特ups配合nut实现断电安全关机

背景

搞了7*24小时服务器之后经历了两次突然断电,每次重启磁盘检查都要卡很久,夏天一到用电量骤升,市电断电和跳闸的几率都增加了。万一多来几次突然断电,磁盘阵列可能要挂了,更关键的是数据无价啊,ups还是少不了。

选择

因为不是成品nas系统,想实现自动关机得依靠linux上已有的软件。而我对apcupsd 这款软件有所耳闻,所以第一选择就去看了新款的apc bk650m2-ch ,快下单了才得知新款不支持apcupsd,然后就听网友的建议看了山特的box600box850 ,山特这个型号有两排插座,一排防雷+不断电,一排是防雷,还省了了插排钱,自带的usb通讯端口可以通过nut 软件进行管理,实现自动关机以及自定义脚本执行等功能,虽然电池容量小了点,但是能省就省吧,最后下单了box600。

nut安装及配置

ups机器本身没什么可讲的,把附带的rj45接ups,usb线接主机,再把主机电源插在ups的不断电插口上,接着给ups通电即可。主要介绍下nut的使用和配置。

安装nut

apt install nut

配置驱动

首先可以用lsusb命令查看是否接入了ups,能看到ups即可

image-20220524232436626

然后编辑ups配置文件vim /etc/nut/ups.conf,增加配置如下

txt
maxretry = 3
+[santak]
+        driver = usbhid-ups
+        port = auto
+        desc = "my ups"
maxretry = 3
+[santak]
+        driver = usbhid-ups
+        port = auto
+        desc = "my ups"

santak是ups的设备名,可以自定义,后续有些命令这个设备名还会用到

配置nut服务

新建ups用户

vim /etc/nut/upsd.users新增配置

txt
[ups]
+        password = xxx
+        upsmon master
[ups]
+        password = xxx
+        upsmon master

ups为用户名,xxx为密码,upsmon master为运行模式

配置权限

shell
chown root:nut /etc/nut/upsd.conf /etc/nut/upsd.users
+chmod 0640 /etc/nut/upsd.conf /etc/nut/upsd.users
chown root:nut /etc/nut/upsd.conf /etc/nut/upsd.users
+chmod 0640 /etc/nut/upsd.conf /etc/nut/upsd.users

启动nut服务

vim /etc/nut/nut.conf修改模式为单机

txt
MODE=standalone
MODE=standalone

启动upsd服务

shell
/sbin/upsd
/sbin/upsd

查看ups信息

查看全部

shell
/bin/upsc santak@localhost # 这里的santak就是上面的设备名
/bin/upsc santak@localhost # 这里的santak就是上面的设备名

查看某个信息在后面接信息类别就行,例如查看电量

shell
/bin/upsc santak@127.0.0.1 battery.charge
+
+Init SSL without certificate database
+100
/bin/upsc santak@127.0.0.1 battery.charge
+
+Init SSL without certificate database
+100

设置自动关机

nut服务会在UPS发送LOWBATT时通知机器关机,触发时机默认为ups电量剩余20% 。我们需要添加upsmon配置vim /etc/nut/upsmon.conf

MONITOR监视器部分添加配置

txt
MONITOR santak@localhost 1 ups xxx master
+
+# MONITOR 设备名@ip 1 用户名 密码 节点
MONITOR santak@localhost 1 ups xxx master
+
+# MONITOR 设备名@ip 1 用户名 密码 节点

授权

shell
chown root:nut /etc/nut/upsmon.conf
+chmod 0640 /etc/nut/upsmon.conf
chown root:nut /etc/nut/upsmon.conf
+chmod 0640 /etc/nut/upsmon.conf

启动upsmon

shell
/sbin/upsmon
/sbin/upsmon

自定义脚本

实际上配置完上面的内容已经可以实现断电时安全关机了,但喜欢折腾的还可以自定义触发事件的脚本

修改upsmon配置

vim /etc/nut/upsmon.conf添加内容

txt
NOTIFYCMD /sbin/upssched
NOTIFYCMD /sbin/upssched

这个配置的作用是发生事件是运行upssched程序

设置触发条件,三个动作分别是记录日志+通知所有用户发生了事件+执行notifycmd,也就是/sbin/upssched

txt
NOTIFYFLAG ONBATT SYSLOG+WALL+EXEC
NOTIFYFLAG ONBATT SYSLOG+WALL+EXEC

配置upssched

vim /etc/nut/upssched.conf编辑内容

txt
CMDSCRIPT /usr/local/bin/upssched
+PIPEFN /var/run/nut/upssched/upssched.pipe
+LOCKFN /var/run/nut/upssched/upssched.lock
+AT ONBATT * START-TIMER power-off 10
+AT ONLINE * CANCEL-TIMER power-off
CMDSCRIPT /usr/local/bin/upssched
+PIPEFN /var/run/nut/upssched/upssched.pipe
+LOCKFN /var/run/nut/upssched/upssched.lock
+AT ONBATT * START-TIMER power-off 10
+AT ONLINE * CANCEL-TIMER power-off

这段配置通过CMDSCRIPT指定了发生事件时需要执行的脚本,这个脚本可以根据需求自定义,在断电时(ONBATT/电池供电) 会启动一个10秒的timer,之后会执行power-off事件,这里涉及的文件nut用户都需要配置权限

编写脚本

这个可以自定义,例如发邮件等操作,这里展示最基本的脚本格式,例如power-off事件发生时,将断电了 信息写入指定文件,还可以调用ups的指令/sbin/upsmon -c fsd执行立刻关机操作 (FSD = "Forced Shutdown")

shell
#! /bin/sh
+
+case $1 in
+  power-off)
+    echo '$(date +\%Y-\%m-\%d\ \%H:\%M:\%S) - 断电了 ' >> /var/log/nut/nut.log
+    #/sbin/upsmon -c fsd #立即通知关机
+    ;;
+  *)
+    logger -t upssched "Unrecognized command: $1"
+    ;;
+esac
#! /bin/sh
+
+case $1 in
+  power-off)
+    echo '$(date +\%Y-\%m-\%d\ \%H:\%M:\%S) - 断电了 ' >> /var/log/nut/nut.log
+    #/sbin/upsmon -c fsd #立即通知关机
+    ;;
+  *)
+    logger -t upssched "Unrecognized command: $1"
+    ;;
+esac
+ + + + \ No newline at end of file diff --git "a/tinker/network/\347\247\273\345\212\250\345\205\211\347\214\253\346\224\271\346\241\245\346\216\245\346\250\241\345\274\217.html" "b/tinker/network/\347\247\273\345\212\250\345\205\211\347\214\253\346\224\271\346\241\245\346\216\245\346\250\241\345\274\217.html" new file mode 100644 index 000000000..c94e8e84d --- /dev/null +++ "b/tinker/network/\347\247\273\345\212\250\345\205\211\347\214\253\346\224\271\346\241\245\346\216\245\346\250\241\345\274\217.html" @@ -0,0 +1,25 @@ + + + + + + 移动光猫改桥接模式 | 故事 + + + + + + + + + + + + + + +
Skip to content

移动光猫改桥接模式

坐标广东,移动千兆宽带,服务商的光猫是带路由功能的,但是性能极差,且跑不满带宽。然后我就在公司福利商城买了个小米的A4路由器千兆版,但是拨号还是光猫,然后就动手折腾了下,光猫改桥接模式,拨号功能改为路由器执行。这样把光猫和路由器指责剥离开来,光猫就负责光转电,路由器则进行拨号和提供Wi-Fi功能。

具体操作

拿到光猫后台的超级管理员账户密码

从网上找到的:

账号:CMCCAdmin

密码:aDm8H%MdA

这个可能各地不同,如果不行就要找宽带安装师傅或者移动问了。

用管理员登录后台修改配置

  1. 选择网络tab页,在宽带设置中找到名字带有INTERNET的那个连接,这个链接应该是路由模式,把使能单选去掉,把选中的lan口也去掉, 记录下这个通道的vlanid备用,然后点击修改保存,这时候就把拨号禁用了。
  2. 新建一个连接,选择桥模式即桥接模式,勾选使能单选,勾选原来连接选中的lan口,填上刚才记录的vlanid,修改保存。

登录路由器后台修改配置

  1. 将路由上网模式改为PPPoE拨号模式,输入自己的宽带账号密码,移动宽带账号为手机号@139.gd,密码如果不知道可以去移动app上修改宽带密码,输入后点击保存,路由器应该就可以可以进行拨号了

TIP

网络上有些教程说的是直接删掉光猫原有的那个路由模式连接配置,千万别这么干,一定要按照我这种方案取消使能和lan口的操作。这样只是相当于把那个连接禁用了,并不会删除,万一自己配置路由拨号不顺利,还可以重新启用原有的连接进行上网。如果删了,自己又没配置好,那就芭比Q了。

+ + + + \ No newline at end of file diff --git "a/tinker/vm/PVE\345\274\202\345\270\270\345\205\263\346\234\272\345\220\216\347\243\201\347\233\230\346\243\200\346\237\245\345\244\204\347\220\206.html" "b/tinker/vm/PVE\345\274\202\345\270\270\345\205\263\346\234\272\345\220\216\347\243\201\347\233\230\346\243\200\346\237\245\345\244\204\347\220\206.html" new file mode 100644 index 000000000..0109ef81f --- /dev/null +++ "b/tinker/vm/PVE\345\274\202\345\270\270\345\205\263\346\234\272\345\220\216\347\243\201\347\233\230\346\243\200\346\237\245\345\244\204\347\220\206.html" @@ -0,0 +1,25 @@ + + + + + + PVE虚拟机异常关机后磁盘检查处理 | 故事 + + + + + + + + + + + + + + +
Skip to content

PVE虚拟机异常关机后磁盘检查处理

背景

pve虚拟机因为断电异常关机后,会出现重启无法进入系统的问题,并提示磁盘检查相关的异常,例如我碰到的提示

shell
The root filesystem on /dev/mapper/pve-root requires a manual fsck
The root filesystem on /dev/mapper/pve-root requires a manual fsck

处理

手动执行fsck命令fsck -y /dev/mapper/pve-root即可

+ + + + \ No newline at end of file diff --git "a/tinker/vm/VMWare\350\231\232\346\213\237\346\234\272\347\232\204\345\207\240\347\247\215\347\275\221\347\273\234\350\277\236\346\216\245\346\250\241\345\274\217.html" "b/tinker/vm/VMWare\350\231\232\346\213\237\346\234\272\347\232\204\345\207\240\347\247\215\347\275\221\347\273\234\350\277\236\346\216\245\346\250\241\345\274\217.html" new file mode 100644 index 000000000..033ef3bc6 --- /dev/null +++ "b/tinker/vm/VMWare\350\231\232\346\213\237\346\234\272\347\232\204\345\207\240\347\247\215\347\275\221\347\273\234\350\277\236\346\216\245\346\250\241\345\274\217.html" @@ -0,0 +1,25 @@ + + + + + + VMWare虚拟机的几种网络连接模式 | 故事 + + + + + + + + + + + + + + +
Skip to content

VMWare虚拟机的几种网络连接模式

简单结论

  • 桥接模式:虚拟网络内的虚拟机都可以互相访问且能与物理机及外网设备访问,相当于一台独立的主机
  • NAT模式:外网设备都无法访问虚拟机,但是虚拟机可以访问
  • 仅主机模式:虚拟机无法访问外网,只能和宿主机通信

安装完VMWare后,会自动生成两个虚拟网卡:

  • VMnet1:host网卡,用于host方式连接网络
  • VMnet8:NAT网卡,用于NAT方式连接网络,ip地址是随机生成的

区别

桥接模式

桥接模式分为两种模式:

  • 直接把虚拟机的网卡接到物理网络,这种方法是虚拟机的网卡直接与物理机网卡进行通信
    • 不推荐,有时候可能虚拟机无法连接到互联网
  • 选择特定虚拟网络,选择在虚拟网络编辑器中配置的桥接模式网卡,这种方法是通过一个虚拟网络进行桥接,相当于在虚拟网卡物理机网卡之间加了一个虚拟网络VMnet0,VMnet0可以选择桥接的网卡是有线网卡还是无线网卡,如果物理机使用无线网卡上网,选择了有线网卡,虚拟机就无法上网,一般选择自动,让VMnet0自动选择能上网的网卡。

桥接是虚拟机的网卡直接把数据包交给物理机的物理网卡进行处理,虚拟机必须有自己的ip、dns、网关信息

image-20220421232404355

NAT模式

NAT(Network Address Translation),网络地址转换,相当于在虚拟机和物理机之间添加了一个交换机,拥有NAT地址转换功能,能够自动把虚拟机的IP转换为与物理机在同一网段的IP。VMnet8是NAT模式,自带DHCP功能,能给虚拟机分配IP地址。能够实现虚拟机和物理机相互通信,虚拟机和外网通信,但是不能外网到虚拟机通信,如果想让虚拟机作为服务器不能选择该模式。

DHCP(动态主机配置协议)是一个局域网的网络协议。指的是由服务器控制一段IP地址范围,客户机登录服务器时就可以自动获得服务器分配的IP地址和子网掩码。

image-20220421232842082

仅主机模式

内部虚拟机连接到一个可提供 DHCP 功能的虚拟网卡VMnet1上去,VMnet1相当于一个交换机,将虚拟机发来的数据包转发给物理网卡,但是物理网卡不会将该数据包向外转发。所以仅主机模式只能用于虚拟机与虚拟机之间、虚拟机与物理机之间的通信。

image-20220421232923906

LAN区段

相当于模拟出一个交换机或者集线器出来,把不同虚拟机连接起来,与物理机不进行数据交流,与外网也不进行数据交流,构建一个独立的网络。没有 DHCP 功能,需要手工配置 IP 或者单独配置 DHCP 服务器。

image-20220421232954352

nat模式连不上网的解决

  1. vmware编辑-> 虚拟网络编辑器重建nat网络,把之前的删掉,新建的同样选择VMnet8
  2. 如果还不能启动,去windows服务里查看VMware DHCP ServiceVMware NAT ServiceVMware Workstation Server服务开启,如果处于停止状态则启动,此外,要把VMnet8的ipv4地址和dns设置为自动获取
+ + + + \ No newline at end of file diff --git "a/tinker/vm/\345\256\211\350\243\205PVE\350\231\232\346\213\237\346\234\272\345\271\266\345\234\250PVE\345\256\211\350\243\205truenas.html" "b/tinker/vm/\345\256\211\350\243\205PVE\350\231\232\346\213\237\346\234\272\345\271\266\345\234\250PVE\345\256\211\350\243\205truenas.html" new file mode 100644 index 000000000..82f2b2148 --- /dev/null +++ "b/tinker/vm/\345\256\211\350\243\205PVE\350\231\232\346\213\237\346\234\272\345\271\266\345\234\250PVE\345\256\211\350\243\205truenas.html" @@ -0,0 +1,37 @@ + + + + + + 安装PVE虚拟机并在PVE安装truenas | 故事 + + + + + + + + + + + + + + +
Skip to content

安装PVE虚拟机并在PVE安装truenas

安装PVE

制作系统安装盘

  1. 下载rufus
  2. 下载pve最新镜像
  3. rufus选择镜像并刷写到自己的U盘中

WARNING

一般的系统选择iso镜像模式写入,安装pve需要选择dd镜像模式写入

安装系统

等待刷写完毕按照正常U盘安装系统流程安装,注意ip网关等配置即可

PVE中安装Truenas Scale

上传iso镜像

官网下载truenas scale系统镜像,打开pve的配置地址,点击上传后选择下载好的镜像文件并上传

image-20220424104118496

创建虚拟机

image-20220424104248209

操作系统选择刚上传的镜像

image-20220424104316127

系统tab页默认即可,磁盘这里要注意总线/设备选择sata0,磁盘大小选择16即可,这里是分了一个虚拟的系统盘,不需要太大,因为truenas系统的系统引导盘和存储是分开的,分的太大在truenas中也无法用于存储

image-20220424104359163

cpu根据情况设置,内存最小8G,建议尽可能大,truenas scale官方建议最好16G,因为truenas的文件系统很依赖内存。

安装truenas

启动刚创建的虚拟机,在选择系统安装盘界面按空格选中刚分配的16G磁盘,选择ok即可

image-20220424104822972

然后输入root密码,安装完后选择3重启

image-20220424104905506

随后等待系统安装完毕

image-20220424104940291

输入9选择关机

给truenas分配存储盘

  1. 最简单的办法,直接添加虚拟硬盘

image-20220424105115934

总线这里序号会默认自增,存储选择磁盘,我这里是只有一个固态硬盘,磁盘大小根据实际情况选择即可,添加即完成了虚拟硬盘的添加。

image-20220424105147413

  1. 硬盘直通

    1)直通单个硬盘

    因为我这台测试机器只有一个固态硬盘,pve系统也装在这上面,所以无法用硬盘的直通,如果有多个机械盘,想直通给truenas,可以使用以下命令

    shell
    ls -l /dev/disk/by-id # 查看硬盘名称
    ls -l /dev/disk/by-id # 查看硬盘名称

    image-20220424105554763

    这里sda对应就是我的唯一一个硬盘,后面的sda1、2、3都是分区,可以忽略,记录下磁盘名称ata-WDC_WD10EZEX-08WN4A0_WD-WCC6Y6EDC1NT,执行命令进行直通

    shell
    qm set 100 -sata1 /dev/disk/by-id ata-WDC_WD10EZEX-08WN4A0_WD-WCC6Y6EDC1NT
    +---
    +返回
    +update VM 100: -sata1 /dev/disk/by-id ata-WDC_WD10EZEX-08WN4A0_WD-WCC6Y6EDC1NT
    qm set 100 -sata1 /dev/disk/by-id ata-WDC_WD10EZEX-08WN4A0_WD-WCC6Y6EDC1NT
    +---
    +返回
    +update VM 100: -sata1 /dev/disk/by-id ata-WDC_WD10EZEX-08WN4A0_WD-WCC6Y6EDC1NT

    看到这个返回信息即为直通成功,回到虚拟机下面可以看到已经新增了一个sata硬盘

    命令拆解:

    shell
    qm set vm编号 -总线编号 磁盘路径
    +- vm编号为pve虚拟机中的编号,例如100,101,102等
    +- 总线编号为指定编号下要新增的硬盘总线编号,例如虚拟机下只有一个硬盘sata0,要新增的就是sata1,这里就要填sata1
    +- 磁盘路径 就是/dev/disk/by-id/ + 我们上一步保存的硬盘名称
    qm set vm编号 -总线编号 磁盘路径
    +- vm编号为pve虚拟机中的编号,例如100,101,102等
    +- 总线编号为指定编号下要新增的硬盘总线编号,例如虚拟机下只有一个硬盘sata0,要新增的就是sata1,这里就要填sata1
    +- 磁盘路径 就是/dev/disk/by-id/ + 我们上一步保存的硬盘名称

    2)添加PCI设备,直通sata控制器

    Proxmox VE(PVE)系统直通SATA Controller(SATA 控制器),会把整个sata总线全部直通过去,就是直接将南桥或者直接把北桥连接的sata总线直通,那么有些主板sata接口就会全部被直通。

    WARNING

    如果PVE系统安装在sata硬盘中,会导致PVE无法启动,所以在直通sata控制器前要确认自己的PVE系统安装位置,或者直接安装到NVMe硬盘中

image-20220425090446637

  1. 删除直通设备

qm set vm编号 -delete 设备名,例如要删除设备ID为100的虚拟机下直通的sata1

那就是qm set 100 -delete sata1

+ + + + \ No newline at end of file

ZW z>LvWci|0Ys_~}zA<#r!FpYB||^on+Us`%f%qy+jTHN#5H_iqB-3Ph=a%NV&Ad-gHw zl4kvsk9?lBv!OO#qTiYn;vKECa%fn4tT|~nfbKPSY6FQp8|vk7r?}G(%x$_VOxJ)1tWAXy zcO*??y!+8dxoc9|2dW^~GN;B|U287!Nm=LIaJ|ooF+aLwJ~NGa1&q?uVsgUYS0mKc z^NMXa(-R0U<;Xo`mUgC|{_3V_zm68nk4b20hpwkRQkMb*pZZl2rg(>RoxTxgS~q>5 zb7#x}f7-3WFBmEwNSeWAM6RG)CF_BlV(;Q7?x!tD9MS)>b17=hXlnJg>danU(&=^r z-)3(3ih3{goV}bHi+zaagq6RWx_bm``PwSmsO_Yvdpio+qC;yl?_p;yv+s7FpwBj+ zQai?Me5wWN2(b*{1y)CJlZqTYJZ35$0sP!^-i64 z`d(46qTZAIEg zkrG#q7P)*}dk)#g;1erffjC^O)0hAQnxgE(`z}fm7Ix$@FIwC;_oNa zQb&n&LZp+=b$_xc{dgqZKj`Ci-p9j(Ryc}Wu~;G%Dtxnv98OyN+DAJgu2V;>(~qs& zkMFpj*r+4-hRg21{p371)eP8HBbcxVziL&b4fi2d@`LnFB1U$xu*{2mR8p>ft_poh zSMe{nu_Rcv;3^rP@5-rh|9xug(~ls?I)#9qyBhPnOY%II$Co)f@D+Kv_-R{h zXnyH(AMtPjD&-$N=K_KvHgl>^4Qkuj!*!WJ;SH=1X9TjeBd-x-1AbEA=ZaACwu*`O z0>$QkuFPMm%7?1@#NE3ripz13?~-Ww-aowBr@cqTsggA3 z3*J}p+tOY*WsqHRX2`&Z9p9#Ro4ul0WWTiGS|uwMo;BEVjY$vB+Ym)-U6|ZqF;3$! zrN(UN4VB^Vp_&@OB&0&+WJq@9)n3uzf+f zznj$JjkJc79b(a6waK(YWIRJ!Az;Yhj+_A5DxQ;Kv6B~B21}U1912nRRMCh4kTHon zgbPU{0EAg->BR!bLNdrCvr;(nAvdf6$s-c7u*ozhQK>)$bE0}2sB+FpRM6te|9d$z zsI5ztZ;OpjIolBaY**(tM0%96Ey=yE_lNV?pF1vK9eCUcm|=EY0VN(v_@u(f(o4}T zDPatNGza`q^$)!d@XiKafG`2@@smOcv1e`|<84JnJ<$xy@d428$9~ES%Y{8c)7c^n zCH|?WB}1}yOj(oly=x^m+d|brlb`{e)F_{pX_W>XA3CIEa{FgYkrY``lAH5~dex~$_1`jOy7Z>Rpw0O{g&wfc8 zQ=iC8D4!jalOX>Fj04tz^xgg$v?*Z*31uAo@4$U-n?rDBUjS!7h(YY2#tQ$g;|q`r z6c%wSK>q>Ig@9dl3le8G5Au%?{(s&QOGwMY?0V1x?*aY^;4c7v0ez8T2t?=ziU<*Q zMO}m4;J1V=b6XHD85m<@a-6rmf|5WzKtBLaw>$69b&G*`@%w?2Zjkc6QO@>=?2gJu zMR{rB{F(XX^z=(DJg1{C4X|8La0Q*-*h>dA!_qQDU=a$>G17i^XM`|v+iBl(Iv zYKMTn2yGB%@URuq#LNm(UQD`(h0j;Ag2v$b`0g3dRV!}5!n zTEcQZP5dOYHo4Ry7aVg&nCFwJ2=gMA7n7(ac8`;5MfKxIOA+d#TpcGn)LMvumjl_h zo=TuG8$p7Bf?;A&ZWQJu%%NpI#B&ojr;fSBGH2qv7l(3@jmyn)W{liT=O16Sw9;6L z{+-oC>l{M`sr;;@i}Pi}QD426)og?M6l$x zS285gwp@ILu`&NdHn$V;c6%=3-X!G9AXa1=cvs@ZL_|Vzr9F`=Ah$kY%Ji~+lH_D= zL7O9Z_ZSs!;N;Rya`B7GUm$`~Ji@B)bdN(}y-bR7g;nIr$RbUF^}8O`al0rNtw&g9LmFGLLJ}WJawYEuP%qpMa8&>OX((BGB7iL zav?s(?X2X)K#H-Ts&MCC_gC4>xZM`an()wF`FE zPdn)sjI?+bN3B7c4y#bF(PghHNxk}6vvN;Hv9F)~fUp=P%q3OF#j%NXp`xxh9l&44 z=jAvS{$#zW>(Kg?qqK&v&9m7?f77mxS$qJamj}TvR_Dm;DMFOQlcdEP`#VYx52Zox z-mW5ry3tedKXOK9e4(}cBy7K?kQR(iqc`g3o>c4aj!5&8Dy-LeO~M~!p(oxiu2*ho zLn9%w#gIoN#i!|qhQWk6#pSD%kD|;?in_(rx~ssH%rag?Sjw^?0W77Lz6RlQyIDF6 zJtPH865f#Vfh;!wD{fUcKiApATY)Fe&3}eFxo$iE0yUeVWI=<02d`rfxL5(W8Fs#& zD|%!AIC-?{7S>{`YwSP(puevN~8bE$j8Jz9Ekdec7tG3laqtU^S2(R!)|IFZidr8 zM?Ea*~cX?!2l%ch2t}LH@d0Z>zRw?(&cvQi&qA_X^7*j~MH%?!Vm{?-F zxBBi%s!LWQvKy7tFNzyd(X@tmZZzX@Tb{P0yIXo%MwByDUZ8^EiY8{k;i2N8iQ%i^ z=I3y8RZ=4?%qcxrOT?;@#QKt{uBuaGJH6_0+swNcL+{P~?o~_`Dy-W}Ij@h=n5$$m zPtoC`@Ym^ek#5v99bXR=wJd zjkC{>G-$8y&Q^AI=k_wycCAiIO>0onge-b}zDm_Sv$$%Rk(O5D)@m)!-4Pb9g0h^u zVh2jIrA)hORnx4S@%M{8>DJ1qwA0qIT)o#W^aRELD#loE<*;gecZ7;xU5Y3uCuLxMY*o3kV_?;x+$zS;kApXLtMS$yDy3%qX zFNWaAY&g0y92u@ zQbSQ1ixw`%hGI1p=aS+zmtaRV{HoQ_OU}%)X#LBKwRe`NWyfuHEq*Pzj zXVW&bQTtMDwPtyPfwI0&(ghi4>tTYLV%}?Vd**`tkiz(DqP3o}&%U>P4c{lrKJboL z^hH{#Y32c&bC0Gi$z4)^L8>~3O-(n!s*Z?i)z}2Aa4%$Q34HV3-t;6~PAHrQ%^J{8?pu7zZ2WqILvIix5cu~< z2cS5?>}{Zqt>^b`6&34|EjYvDU1h5H-N!Qkr3KbR9i=&rdqxMYwJ0m9{g`QoK3J z?cQ5Vzb2bf7Il>LYlch)(|rSKi><>nC)z9uhdB=+A^FS0mScljN_~eq8=8idCM4_H zG{T8czae68BX6#A(^U_S-_%L17*8(OS4WSrha#&&T223QEl{~|7BfyaSz7=N42Lb> z%3JS(T`RT{dIlXGQ#j5jtzZBMTV?EOFgu|(rT=2*D1Wo5Xr!23jnWfO-Q)V}K` zQPAF~LWXiwq>i{&PH;LI(%};6myPrntCEkHhjpXzR-2Fb z;?5o{%&u2lp~a6=#+Yf+)B7V7QmmGvdM^_3J5kir`MNpE_bk-w@7%;%IkaWfWMdFa zI&~ZNV_lD@LhHQvD53w8J6|v>kiSdK*9hq4jh++qR_j;RT6dcOGoE5f( zq~yp`FeKufrjhV&sgVleT4Dqgp<$^YHUE{7hT^?!R9Z^$cSd|P<=|SYlvDI4_bb6x zBUNqy&2h9%Ys>FZe6p0@H=ko(*>F2F^hG3N#pa^oJW<}YvTQ#jp02r9yuaQ%V@$qf zF-TS58swLOSYUkM2Unw`XgE>f{4gPqssfm?bWjQ#u0=$U> z=Jc?I?J=zxqkVC^1)C?mTmKS4M9MvcSCCjBiVE>B8vdy<0&FPPkf)XE9zKE%O21G4yIA*H6;_4fUyD;Cwk#)oXq2pLv#nW zKCi?Bq0JBx(&sR;M_GIYVsuT}5`X3L?0#HUCIEfRY694F#>%4_U8i-?QcgI!EMm$b ze|F3!dvVx8aJI>fg4hLsYsLg=nX{3~4agk1r$@=A*_%tD?E}=XSD^TwKD=v zTbsGdLP#Hz5Kv-eM<|n~H}YOAR|fksLfmO+e(Bf_L$hkx8C9rVgb**Z#yVBTq_F`h zx7_~0Uy&l_(gGi`Do$>YtHgVtc=yFcWi^(RF7XDqC351j{-nWUU_hq-f-DWJURP`d zQl27WA&6oeim2D7*Rqs{fN&5ZS#{fIh`yD~MTVpH8+`BfQ;Mm&&BImP6~-L0j5^aG zpfQVy)3ozqs_zbK`E~|qW&^Mm;_uY#9MEC?0HP6vnN>K~GOq}+0D`(!D z_KhCJC4n&(Rv9mn#X)1YPbe6V0(KZ%QUk}{4@mO@D8+9?aNrDFW?~@uk-fB$7sXt2 zp!w|&*KrhF*4GZU2Ra}M3VYj}QWz|MKHtfai+Y0e3ltW3vB4Aw3>D!D%mkICL6Vqb zIFe5pBKQ*TWo!)tndBT=V_XknuGkqF8nHO-5G{60g) zG&u(uh-CmrXe@}S1_q@3x5jZolcBo?Q$_;a;)i2~87(XzdbV+Jgc^Q?^|&fDm~?<_ zgKV@dltBUpwhkDu+6Yx*ihJ3}f>ZtxMYD%v~@)Z4coYVX8t;{Z2RMDD zTg)1GWmmEBB z6)Unp=FYv;i`MeJ4N6T#?$DUoW_X7Vxx;g|155RgZrH-b6_n_zbndns7gfXw@I-_S z=99|J0_L&B2^6Xa5?Zwyn{A@`IVZcMG)JRy$?W0-Nt_t z3)w@ClC|O?A2jfk6wojb9xGDU@ob}0k-J~OXkyi4%n?*t6(Aeq=T4q}O~4&C)9pIp zSEf#5eBe&@86Y)2iI~s8zYsIvw$^eqxWjnmhv}Q}f2tUSb*-3WW-;wkMuR?5)9r0v z9Ou#4&U9%!-d)ea4s}6STGnrj282tVgI3cC&_yYIkm$fQf)739>1{wwLd=XRA%_Qr zgH%P%(qwyK$_2my@7VH^p|k!hPyE6Td1Quf${;A+o^S$0-F(RJ_M`r4S6_TD?O&YV z%6-pqEsgmp%T(~M<%hrK?={3V{GE>D!< z6Qdlgz)g~tye8quf|t1kRD+MdJ(zzer_XqFmGk1A>kSN8E3+A1l9xx{J=D$mTzj@p zAHI~COYX-r8k8uhroB2}RT4rsWerlyZgzAys8eaoj5})qIxjE!>Nez%0X7^uCbi^& z=z2u*@yu7B6PmT|~?rr(PMa8;)g!;T~!S^(GSvD7! zD4Z>Lz!lQ%JQ4g)Z^ z2G7<_%0*Fu1J+ER>Ss1H7})UdwHPg)5uxe+sN=y01JWxWOf|f&n$Jx%je+k3W>#W7 z=`b>@9*K8d1K1R%_F!BYQSUV-a=1bF2v^p#J ztp}B6;=xa<_LK;f4#(;8m1}416L3f)yyQt}q+Z3vnxnz6LzA>b@Nr}rfiV`{npCr_ z`fc}cAB$~fkc)&xgR6S|-?8fzxBtqZVq^q``rc(}hND0B__^CRI-QyU>w@Pa)?JO0 zbpid?-)3wku=Pz7EtEJLA*MEo0eoH-a9c9UFr~jCiTg@|U>ngiJ7zM!zZhkDwx3P} zY|>n?wnU8)5b-njb<9JlRPH&`cQXOg(&nNH;yP4^rfb{hAI=UHWeBO_M`36~qkhEV zT{xVFR9dT8N6(-4+f^tfq%eha8OW#0K01%+w8AoVg3kqCA+O`<>aE#<(xVl7Giu)Hr2v53$$PF_!5& zZ7c3aH_Be+GJ=ZDwvN3VD8xls0B8D|b`r;)enF_(- zh?E-F7uPBqy|2Km`ntwK_%w$Z=*eKKYrb*u8<&vwkG!J0hws8xOi zBr%OCi6aE@)zx|IQEI7Nx0Df^}3U)$Ma{Vq(ZUEejB!Hlk ztBXR&vMCKZ0B{FEHhFbP}_@MbbxQ!45l|k(`VSj14nL3`&|Vsv&j&- z8tluCu*-ER(Y*#K(m(O|n<=uvnRs$b{@*wk38N?yVp=K_A73UuQX9D+FDdzAy!ObG zKIz(8i>@#TrmL&&`%Ad&K3RFIb@@)a&bk^y+J3z{Gqbq5P2MMOc4?^9fOGKi;sA!utAc%nTXqcgS${X|TFu*Ad-$sq zhFwT(jgL!jE@zy^r~{0=zY(ZSsV zQ`j_Y@9&!riaF^!K=j%AFmxR;y2`+Fz<1Ecbqc4qR8~d+uCqUFQCIj!! zqyY|7ahrSJ`_YOsRb1*0bHIHMMx+ni?0LhAQ+H|}Pt)Pmf}_%nQY&WJQ&H->yMx}@ zhfxjolgyTaJO{g~<{Q+@u^v7aB~`v!{bpJ=a*i_hMPn`ioukjeCjXMY-L&?{QEx?2 z#RUTaf!#NyKHVApc^AC;%+z>4D(p{}SXDnid|0WX+xu}B+Ub~1^c~O4^nE&$(8UJg zJ|G9%>4WnbR|<0kG~uTRrWrKChU&N*b?oZ3Rw-y#$>}zI(AiLJ=LPxpb*K0Z&Ja3( zyHInY?i{sF@tnvEoSIO~+Q@=)LGW|Ws{kBh9L^8ZpW#XMp$P>ALk^B7=8bSju01;^ zb+WvZCFO8S0AWfV{V&x8B&O)Ki`Z+WZ~Up9q=>}a$GF%t-|aqH%N@?Y3i>MT#4WamuUyCCO{98U~66ci+clIow~>BEQfU?0HT55-qbHZRnE_4xIs zpT{(QR_aV;4d~;jTD&f`&85&^|%k%7#goXx-k|T<|RfdLzW{-Og^G>nF zCI!G;0KcpZ1p@%lfVgiTi2DGM{_YP){w9>^05Gw9wObXYv#cynnKyES9RSA!)-#2M42Vf_?cV^Yyhs`v!kM zd}o4GG5_`y3TQ{SwKWTyZ6ikVhrBW1?HnMytb8!v@-MH}+2jo;)ANxy3)_R^_a9sx z_l~-EwW*LV1#*_;EzT@N3)TjQK|Cpgqw@Cw8-{* z0gD%7j@&%fd)!sa1U_wPYr8X6b-`mDcljod`N2HJf%6kPHvPB3J;8!>A~EqXE@Y{g z6CZjnc=Zhdm5;o3)YDI@Nk2HsH}g>zNCJw7ZiNdxjqeWkP?LP}pSZF9#GhXhTKIkW z5woMIloEbX{OcTgZ^rJ+tkQs5{rH$OxVLDf>m=j65&8wXe0lfM+GsIuhmHz&}r-;QS2H`2B zZ>WD31Gy&v5L*INeNfJW(B!4kiRy58FDp!VqP)CzsxDNtwVfb2#RRfEFa@!(;>zWw zuMTEDdL4k_YIuJ^2;hWV1$z;sXh$sG*9MLC*s+sJGUTUcG8&E0L7@TyvE$WLM_gzd z+b^r(alXe?#1gTqQUi|SYYdlle;Ei^QHIYJXqWsw7s%Tx8#`iGnH(L)W$8=-KGlVl zNa^eh;_j0_;_3J^{-_WG#Qs3byg!Lgz{e}}Sk215(Myd;O#))?1je?2XU3B$0Qv8R zq8E^Yw!@-?mE*rluWuPFq&{p%9-)2@&ss4S<6lsy2FE$kqJH!5;C@Zg>VuT)kw1u!YRRmC0Or`)p2zHSM zX{*H#Z@tgT2U80I&fqAoE9Fs0)CgG3TR{O6M?sbD(CDXrZxOVxg9hp7%h@Yy)UVyw zm+QjDn3tU_{IEuz>yuao0tD7hJPK?=11v2AHoNZh?uRQff;YOtM=n+QtyrNncz!&w zT3TK@kw!{ihvlTEa!9N4pI~BnGm6`hI#hc=TjU*`<{stQCFI!&O#p|^*9`x)t4(Wy z3F;Ik2B!}MKp+HrNaBE|Uzyqj2@MMbyfCx6_~>%8yIVL60~8hj-v>~Kp|9;wK+#U* zFL45t4Jnih^{@drmtb17xrLn4kJ-@{HQtSy4hi>s!rBrz@!pbeI}&oV>YR@9%4&-3Rt?=aBr4)ISSS5C@&qbP z4g0X$8y?t&eKdUcLrS5_{8H2G?GW*Xh7gI_wx-V4#xD58$2lO=gAr*ZrvJQQFW7>b z$5+3C6liNo>RU4**zIhUir53-gWlciBcUWcD_%=4(Eu*C_E0MIP9Zgc4Mv^*4wk|c z2;5&79n{8(?aeABAB#zKJBlrB?DwQ7VT@Fk2{TUpuf zG}6PibvZuRozRiVt?sx|c4W#Z}yFEE%SRdFO@~o_}lfTz7_ln^rr2Bz@q$-mL1x zy3x7J{>+Mw@#3(yxB7>d7j?Od&H0tnKN4HFM5n=t*!=C=O5sebsj4Bt;)q`}zuwo) zyT~E6%#@(?-DZLXJy&?4+o(-sdaE_1#*+NziNz(|GSw1n3v2yZ*HJN$c{X$1NOj`U ztf2pBdhI;z_A@?T_pK5Ad``G`*f6D}q4MTg95Av4JQQ!6C;)L^A()2xE9$GrZ)z`* zy^dQFo_J;5F91D*@3o70zuTX4eWVN8YrVZsi5mhS@Y=+Nx>^x%u2&p=qx4I?SDxMO zVKnv(e%g8wLm8i(sr4|v5gW!Z3=(r6AxhRH9*PIzD%c^^&=CB%gK$c*ps)!<5pCbL z_Li^b>?dvv3G|X<06G12uFT z;p$oSfY@f!F6B-gi~ct(;LF7hc>*wmu%);2vy{a)qy;0KpsY~?(H56dHg6afP zD@84{fMkWI^hehu1;K(=<*7!$zq^j%tlaU@m>nPAQ4X$f*3>yQ`uk$#S0NT+GW(Ca zO3K?D#t-kjk=wf>6e;M{y_QrtI;+(EFD)Gdg6foC-`K;m9i+aHh!Ma(kc=dkKEl&gFhgMbe-*`lnr-2iwxiW*{))&xbQE3S0fHM%uxaBX3WVzq9zXWUYI zvah*FQs?%6V(M0Ah~xCi@@Nj+Y8}XJDM0M}K0C6YU;rXQl&fWd^&*NfWZcj}LiZC0 z=tmtX4WRYE+py2cwd9`c>(7;yYtDX3p1=G+&Ck!~Dv^@ttAEZHXKN=rV&o;C;o%ae zoN4K6+Lo3(V1KQ4Ix+phnV@-`oQ+LjeTm1cIZVd^G91_k}-f%WPbi87eC|98%^s*fGOUIFO zWqs_!P~mRNUPAo!w#)8U+q|MXWEgBd+w5u7J_m?>f0q|c0gB}Dch~gGSKfjJ_=aEhp#|nt%&>QFTC@8 zXXC?ju6C9MZmEw8dRu=jsF&`$y>%`DP_H9-xfGyCUSDvXl4wl-cDmgS03>Ort59v? zVoJFfd!SncXu1wygF{piccW=oWnB;(+p(+A#Ol)DANVEEf^lo|@APu|=z()Wkpuw< z!4b0dDb!XsU$fYs+9V+x;bF_mSA=Z-6@c2!aEzAa^ZX|SFA55(Tj7y>A<|A|@WrXW zuF0J=G3zmc-r0>I8Wc{vCG^X(tx?dv0Q67!1S``JegBmuCete&&^ zzpcCFM_aYc=L1Q3ZEhg$Pr+WypS*#K>XV{CjD2elJSM84IOsH&Dv2f>5CQ06)CHK& z#1GHwfbcJ`#m;^B9RG5*;^h8D`Gp_l?aad=hSDp3#g$VJ`DW?L13!t+qS~V%?D`4& z?<&)Ox*h%V*~4yoo0eD4akv{87jf%>7cs3jIPhMofZZW;mNCGVW!w^KN_1VB61zV- z`yMd_ft7RsjY8UVRNtwacfN=>?j9-HVBgB&rjq8U1u2WO8TlXpOq%|`tswq#R~7QM z^OJt_i$&zRW&-9B(04k3%PL*F!Kc8FsWw=Ag|+< zKv%%B6LX}BDS?z(<^wU?&fZdtwya}kt!KNPjvf~2tN>!oQvg{sK-eN%y~xVI%& zp@0?9?h{-eO#RL6K@kzHL4nP|!GZOyKH&_{*kr6@EZMV1$&T2cR|*v9#pFA8$iJM^ z%go(8uUrY3hcf^LxGnelB02Gu49;09FD5l_xpiK=G2oi1jZL_2A{;cX>r}7lBEmkm zz`*5q3-$zVz>&wl6FKM)I2GAEe56RTd$5Tq>UvF!RgQ@Ut z^#89d6gYITj0x-SXUbfxhs0mPi^oZXenQoou-Zcly#EmWcSvqFv;oF{gvGvA-dWgDzmnS=r*xjbjlqUZ@+H!u?5xg zCN1pkzD?eInXvtA!7(UH8qPI9;&Ph2 zqWrm=%q^3m+tS>k_&4*-6)KJj!CRxn!@>UT5hTyfA^+MeAih!2M`%}elA@^%GZ4V2 zpnEJQhIa9Gb8+?qW@4@s!zq*TwlTA+o2QGTt0&N#Uf#&rxIHn^ zsOJwc8t)$$?dyLcuu5iQT_)T1C2$?8+S@I!=B(WQW%xJAD%#6GBEk;+@diw2V zUI8K~z1uGxapJw1v!9EL8;r7HZx@^;B9T!>EcA#fzFp!RPQ|L& z_g}_h`3kW>7ps~XyD#&d>ich4`Cw1?kOvH>sHGF5;wDaI4`xL8Cb(L|fdO5*4Yx0~ zM819e_fPmB;yE{JL9m_;Dx9UJZjV-uRdCBK3RbsLSi&f%sM}u#$HYDm`>Y8A(h;*h zPfSf_;w(@JU&Dk0?wAA3(LX(1O|{I2dZ*6|D#AVQCmzu)rrML^@%3G zEmV8uN#xLkoMoTPqij+ZDLD}ii+ZA(T)(l(is-B|_is(8R>tQb^X>xW^1%W;1XfEL zO4`Us?ZSEeaDA)_`1!pH^Lpwn;tEYHTzwo}+;Kyme8c3gu~x`^ERaTs~5 z{g-#Ckxz)Z>38{Fx#=7%xl}R|NM8c1Zk_SIa@LXgv(mqM6>=+q?sZEiDh(Ei$SU4I zluq@D5=*CP?9;0VJzcKs9Cp){$Mod>L*7F*l- zCEDjYB}UpY5&?4G%Pua_G1n#$Z%2!ymCuMK_VSLG7H{StMSdDduD|3`#Y_zWfhf z>x9XC=bKG^s7%hRq5)G5Z%a|m?X>9E2gf)hR%GbY0<-%i>Bli8(?VcTt7+`J->FQq z=*yuxHkRd8qFMUAIsQP+!Bs;W<)Iy0$m`~THQ1J~&GLH9)InZ152V^*7P=Rt6wF-w zaL%sm>!4n?aPwlEv#6`5BiXzwk6wsZ&m#&oh0UjOx2o8mMPMLz=}$oVTp)Y_=4tH0})g^L{2~5(D4@0mS~Hao;~| z5S+E40jdX_;1Q<39>A{_Y2-UCNlABZlLOq&X-O|McXKs0<$!tcJC;D~hx=J3?jB~A z?p`L8)J|q5xwGn$?4L8MOdcSgg;_CDBJ1_N&N@o24#{@NVEk1t1-i=ov#7(D@<7rv zgFMa7TT35@R>O~DOXi;a4c6OQ)RE~`3EC}{uW0UIjk>^3;(q$Mh`gEz2OmA0o|%?8 zRxd3G%VtKH`|J8D$9}7A1s2%lWyw6q%)=zENfPIL4se-~rX0H3OW@|>hcmTbAu+W20u({;IEV(9E=HXt5>V-|XQqE`wTtE0}z6lU0a3Cw)y0&4+eW0ji= zcQsZ0t4N8L-=7e>^0x~{Rji)3Uvd`i5P6@T{by%o+I!&}i@`jDI|)&pByl++i&ofC z+nGmeXsb`9UKP@lIkpG@O2=SYFza%pT!oa4rJJT)u|!@>n#{V$wAU8aE{MBClm{|+ z0FV@l_Af2q^!=?PYB&BoT)v$)=+t2)pw>LK%V`}=2;|8YqOMxJwr75h8RlgvV~@_P z#iYLZ0M^ehytj>I{x?E-SMg|may)m$EutbAb(j6ktdKutQ1}2gI`YKI3&wwXgmkkp zQLUxHQ_#7`@$>(%+AMFp^+7u|U-(HO@@a1fAFMsc7Z}^I?yl3aZ_)|^=orUTH$-*W zDm-mqU+LS*@HeZ;_j0m~o7L!A*m#&&JKSm&aT#}~;mh4Z|1l6QY=#8lUtlIoE(g?A z9XJPBK>p@Zz2+y5HP1CR?Z@c=4g$adhO|+|75t4EnPMr2&%y>ZT>dJesqmS0LyTxE#=>tDXfT3Fce44g8a@(f(q zSzuLe{+m)GzDiNdFQpYz=!@#WkT#^a;#D|(Up1dU*@vkH!vd2DGNLyE;TrQ>#n-lKlbrV zm$5=($WU_}xu~PU{;Rpr10ePvB!F}9cD?{5C<~P4j0}A1D|I>X-uq}DUbbS4PoRy; zOjiV)_#f3^|Fxr{h};|(GNh1be5UKk$AO#ORgT|)?YJ2b-zKN9C8q$yx8ZD5q1-50 zprId+0ALW!n0G2I-sSKEl>4t%FuYP~`|iWbq+x%mJCLmG-_nrJLH^W}di|K&GJE-v zq(}nN%inrZB`)^e9wF5WXBuKZe!0QNu*{k%V-2_Smm4dXyq+FEWE2vFVt}$O*X}ZO zkq!*RR!UP`txnC>|p1~)DoJW$AZoRP){pD?i2uhU;hUP0m%MUaG))!+pXVG6WWxiKjIcE zo@`p^_)Nd`#`$=W)2bb1&;++CuC4L5`~2UP)xTLO+++90eCp2hPipda@24;$U);$} z{w>0vGVZK{T#OBxu5#R7Zb7!)& z{H5&drF=^E@={KI>96$Um6ZIPrRD70uXr*sjz}iqeKSbJxC|mtvjs?zENtTa3B~Yk zgi3Ho^!iF!&R$X-{vJ-seY+Fu3b4NZ&xPQ8WJbdSPe3>$a_u-cLJS?JA)Tkr*H{sW zWKTSa>|a^#-xR}TtqxSuFZLk|j7y+L69IWB>(i^wilxW(99`XQT+S5B%W>mmt+X4? zv*XAC&nL5)?c=$g&(l&pPWDqy|2#=^oL?UR-mnF}8vy)|VF&pbh{b^s6|d#oqc#-&QX4R}Rm|%2pZ?$jK@X4VjE9q(jDIg5`i<_7 zIkc098QR^`MgU27za3#W|H&V29nE%~Ji}#{iG2NN8ju@UdykWH#hs4SNgA!#hF-`7 zGHOk`9InTY^^tid{v!h)2S%oWj=~+8xMGL+NdWlTNyTG01oHy&g3B~wYz*Roz17bu zuu^mn;jUA~JT}YtwqAM(^}mS?hnc(6&+B;Ns7(t;^j% zu`1}~+i^jCU#8*KS7A_|U0I9Mm8B9fs+w-!!Wh*MU5E2Jd-#{13=HaSqM~4`za~n_ z80O#_yRuzm5BSZ#>g}8#5XR=!l=+8$B=4|Z$FSjyn^I)ik~-B)H|B`o%>$}0Tn@8t z;~NT=NC`k@wwTTQ{m0`?Q!{Db$+4-a8x`^tynBHDTHk-E_I4>1^~1h_o<&P8w`Lmfi+VXKwFuWCwa$Uw*HZ#MI zs5?cOFdITRMk!(SNJ7pZ7Msf-7R2cx2KmFi>@bP9GatKVh{Rm1LostbP$v zI3$Bl$rd;0X$v}Zv!pnfo%Z0<2PSOe9)|Iz2R+rhzKtCc?Klzvj3~wE!GR}vkyH?K>Wcf{jmEROvCVH5HUW|pdy^D8{J|HB!sFI@Kp4xOtIRL|O*PcpLKyJZ^G5S>$Nvnbs z7gWvZu>?!tJT)OG>-f#|Tc$n@qcJyZvP5DF4|#}D%P!~1d2g19&))Wv&i8RGEMss? zyi^!_TObTFubQ;(i!$fC(Y<};g3_`hx+&Ki;GHS_zA632>)*g^eZk&}wruuxqj99I&$jpncu$r0JQ(fhZ=4J#3_GLH zyzn%`8|Nrn{w>fml0oNn;}t@<0yO_edzxX*GnV1K6~mz{@E-N2U|KW(5jzYsm%~y2 z5Ch6wS7_FUbSPOz+U$65gZ(+`DcFU46!xdZr-F%Z4rjyoc5Yz$o3X}_;0|(sYpVKu zN(01N1=hbeo3hs*v$=zt;OB$nQD}-zV{O!q9c)Fg4v<+ z*^Z6v+N_r8wupC2C&n9BOD9*W?#=Ebb!d&2qiHo%C{tTCWl9v9tW?3_jH3qz!(`Md zw$smHydrQw6*C1zM>Fcx>3i(gR`ktghgd5>a^vg{tUeiI>u`L7)k{ZPSZy~G-#C8F zOz=k(U9YwM&28syA{qcIC&C_RvO62#x`|978Gr|ZSU*2>p zs0z6MbErJAtJp34DG9jq{=)P&Pyd#TBd6=cje14nk~k=z5`XiUMV~^73pzGmN~e&A zQ#Ne_{LswUG*R%+>Z_ap7pE7mz1x%2b-D4+6}{M(Q*3?p{NmjEPnE}}{lBgp-#WXs zWpOe5cVWBC!oufozxAJ||Mlwjw;x;9;>PX&wqMVyTAVzn|F;AdERz9%cXI&14B#z5 z1wa_fd1Wl}J=-D0Jx3(2qYU#PEJKqK=~t(;2Pn4?wluGVst|4LxJGPPI#v-}c_$`7 z-&SW4BU@!2mq?9VAhh{jz*4(0eDk8DARR$m>4S1KECe!?D~2G#lA#*`%G&`wo+&A+ zB)T{}ksC+E4w*GW14-}NR*j$?4RlPsZi*LjF!d+^^ZrUER+6*icEmNrRij_k&WB_X zj7WQ~NFiiemR+jH8m`x-Rf0^}4OXdtq_K6GGGMImNbbsG@rPi)s!WY`;r!)t0cmcP zU=zC;q!QIH*Gk`Sfx$1+z=*AT0Kl-k0&;jMVCt65>)v0v`BF<=vv)FkYLOa|v20{; zpN1MH*E%+?C09nzYqVt88Z9akRM(Wsjp733;scMS>w@j0zyBrG?pYCNqGi(MdnKAC4S{8V+)a?+t zMqqKDo3Uk|QDbAe8SVjfb@sD>v28uF#aM*Mu_hsXmBt*YmZEtNF(t7jgJefGN8KG})I$;WIKE{Nf8n3Vg zl9(HmCyJ7|G94+}u+E_!xKDA`Bi7F1HXO#}kZR;He{OEO3O6-P!y$5aVC$9d#>dh5 zazz%tZfJSYRkb%h9>v@2Acm&gs|MsG3vzI*|s9d@8-D?nb8{*@jPdJ%y?Q3cCoMHsQiZdx)tRwOy?A z&MD=oo1+>gj7n$bl-;Nq88USDh~+*V`dZJ4&7VQ<7Gs^2m%BMlXXy-F8l`AgxpC}T zkqT!$CsB)_u==89G(F>LBS5=_01J1&WQVU?m|)G(IyV`hA+o9Ud5ByiuvlU;w(Mi7 z9A26hI$(;-^jNI{2HlDzNQ*H;Vi%O0RjXq)OxCC@`|BP?p@b02a75;-Z%qQ`??sZE7=~w%8!sKZ7Rp(4KK#1Z;sLe)yBYeAI=7NX zgBrl}PD+mC|i$mw86{pc0yLY)sybs z4Q_H5mOT8_m^w~AsJ^EDfAwwk_v(erubWC)8_XYO{Wa@mmOtx3RxBH{{KoRS1!RwB zJ8qlRuF9xMwWu~p(?BBm)R6~;Q6f<&PLs4mGF_)ht=3mv>7QOOUp0Sw$%a^>&9=oB z*#)~}-?wvioxzD|wz%Xo5A$`NV4h{CT<-+uxyHGT(P6weV1@)|I(L!_kKRs zXZlIcc;2rEgcz7$hAbRHQlNzGa1_QuOmcx95P$&az#T$>2-yH(6}lsv&Ou+gOG7k8 zA^ry!a1FO{KR@MRj_@L<5JGxHKmw!}4V1f!70*rzS1VS8SK`|tOgDtoQ6vSYa3m&=R+q{O)@(un(2$2Me zC`JWNdJNDod zdSMtQV?L5Ng-a;oG1ADRtj)U9O*-mPjYGTGE>{=A0xgT{@79c!zSgea#h=@E$w;Fl zOQAIFoPGHCx+*D-Qmc#lYm8^rW=8Q14(Zq`YKD5Q`@Sqv3RMQy>BuJX7nD(P~*+$^t^R2lO* ze)b){&(C=uALSqUYkt&6{YqevFdOipK5*gxqAgCbBW}g#aViQ?Nw&00MOsP$>DM%u z?7Z&7Pd?*&etWu|C6{QB12%Ai+x5||+Z`LSX&bYctws+ciKxho-c2a})ymqf*3(8h zzDM@7PU}^@wfA*S5A>6+d$9k}o(t$)vJ!o>cRu4fkv~+w1yfH(axu_7AxK77lpq zM=`HcuR~*YbujmpU3h=|b{+7(>b=njpJtz7pUZZxeboGIy07!S>UaBW&kt$8WxsnP zIl28S{>`&xjxK015U?IlwOZEf76Kpkm3?cc&f@d-^xo4VZX=l}Ceat?Y9h*Ix&ChNk~*=Yg5hPw)j3$GMlB|B{A3WZ01Kbd%sdTgNU2TB$cLZ+3zNr)%hO8|hFlK2iyb&$MJgFMPoExr;F5S0#m z#uhiyk$ssgAVF`+Xxe!kWZ}A6{8E#xSkzB0;%iv7czIh6=s4yUtjdj5#E2_bZ(EwS zFpVoLmR?qyPLq9JbV~!ZiE?%@Ej3J?2XR{~fU1Weq1x5}&|3=fA)%&<2&sC8NjQdB zPsZ^0>Al+`sN?Y4f|hi_QBkx_u_Gi3$SyiUOd+Z}QcGv_^=x7ZZ90KkGwHH>j@U;2 zIp4oQISN$aB?pcMBB7lz!Y(QHby=Vr>S7@@0dy)KP>BJd>RZq)f*gwTNU;_6s>Bq~rGk#d( zZ3J-d5Og9kG;8%@!LqQu7~EoiITr>|bfnki{Km=B*WQ+PZ+m$Qb;t_bJeqn4)vDFx zD^!{0AsO>sR#=#r5ct(ttMD$lvO<2#PS6)J_vK>OOArRDYPCG3oHijZ`$YPYh&$ud zAY12_d<13lItq`o+Gi*o%4yAkXTL) z&H8(w!T;ThbdrG(gmXzVc?3#;J)w|OmTzpm z#u|jnraxp3*b<#wc5h&DdKza@*m0VK+^_rSla#A=EIAFk7c zCd=1JY(5J+=-v2R5}JqkGJi=K7)z(`tKgtTik_LTME4ebHNqDR3>12dCLZpq{&AlA{nBpXa zXn|S|Rk;68`-G}ulsU51<1R0;WPGh=wU*i=fu=AMdRo%2_gm$vWKax*F$rE6XHWHV zq>KD4i>`fcBq*wnM3A37s0s&{RtV?K*L)`=hL>(UYJ;JEHRreJg>HKi`?>Udd*4P8 z^9Qk7Rf+g<%;mC%f;d(I7545KB^M75gA zl)cKgYK1)V)jjeeB+`J6%h4@vAZ>tPQcFsVXAh(NyiozIZw%N=X#H%ulaK&-@dl8{ zF1_b^P@q=hEoXVrCvc?*TtQ&#~Mu0tDD! zN00zQ>t0o|?FKsKh$caIaNpEiBZsa52hbW}om%N%5#H2Td(MsCFV|^{?nL(Hft^M*DF=x^i5^n+ zer%x_DvptT`tTfn|KrQ#aB#}{#dEt10>G}Xtqwl~<;uDECi(mpklOoXcvxMuOIYW4 zsl5?3ZT&$iwXs2zkZ%}nX^p{c&dKA76I@2Kx}c~Us6z15U2Q}v+~%ZoiWaa%dN#MHTj{44VExvK(=#pl9_8s}2(@T;MTci*jGOmoLk zJT-#R(ifu~GgnZNFy9;>u>+!LUW#N;UK4~?h(nKIH<sO>^#Rb%`miLdhqO2frc`zz#Kn2 z;>?MX1drUPlktwmeeuibyM|P1D$g`%-^IUTDYpTp)2)^QssnW_U=|Br01!13g*X@mNJd zNj@-TD(J~tf&o45wQNueTNx(A^Ev$_VjpLv!N8<6Av{@x5G>?4>(Ks9o}r;1z_O61 zg;cH*Xnr%e01_GXae<3p4+j(p-dZrgWKmsUIx2I>33ZtsLetWrdB)JZ_z|&hn4zV67QNKkwV3krxCSky-{Qb!jb` z!+ZSeqXjoS&KFgdClx9Z8>nTeyhHO&Yk*wRGcz=wf*x&xLZ6YbGUT(~;Nn)VJKT*6 zEss2>Mg-Y$ z&8t!+GxCk!=eEQY4D^m(I*Y;Fhj^g15>oXwfy0TXR-y+-LMER4YZ>1RO_-Sltc(rq zwI>ztn^XB@Z>=!CRjI8{OuSYO0!mN2!a--oAhp%b>@A2{`kQ`wx`e`x_7jwj(mIn2_O7a_&q z=9t)r?#O3v(0V66Jb!``{G(cex5iO_^t-trqI-d;AZ!?_-AF8b(VILU2bgsDtf+1J{VBnz^I{G-}p$gA!!=@yTNlp?$2f zpg_hc+fm32Wyh;dtb`0|ENlS1*;${aUQe?jsfL(w5(mPvxDS21)DdvAwgMmxY(={z zC~|Q7ve4Ek)u`@Ui2YEWSwLTTgttmStT3i5+@y~!s1S!+67Mx`1(WhJL6`ccPB=xf zfmWXr?1VHW9;?M4x(``0eBvgXBDr#~dTDrI3n(+Bno%8nWh6w5-SgmSVC92X<`|06 zyivBqS%ZW%)`Xf4X0_!~)^Eh!VI4cS%Q+jPmXECS; zEaf{=f_aoMZ9oG&$;VYF5%{#XvWa7$D&@x@=&rq;Ca3edMIKzt4@+NCHZ_;n z`shX2eV)o!@ebnZleXm)F8rK6-3-(dL?Yd*Ame^o7h*?kkXMqN$fD3E@_1;)R$c6Q+nL}dY&nGu41loLK zzodXF$Wp0?7&CGanLG@wmIf3kY#YT)KPYS#>pdBqh)`)qCck?1hE4_g0Bvt{0~=zm zMF==;k(BLn$&9KAn+PoqfPgd$YN+dM=HLWd?W_j|)kGj;t&70@LH`%Y6|snTOHz1+^a&2zBUKJ&ayZ@3pLPQ zf~^Y83A_zF;URRd6o^STk90r!6EpSB(bg6L__?R+P219%1yYA5p_3j~Lay7K&j}S1 z<&Z+@Z~FDHSIT(4=-D8fE8z#m_Bq|p7I`BcpGg?e30;@%>@;6Z3)V@u?VKRgP@_j4 znhVt~DHxX^*me{YfV2?dqD|$gnmJsHY<89T!Cj+?L;$IzmrcIYg_ET5XNi9V;ymnR zs>2#sk+4j8vVWe=2!L@`(A#dW8U-FqOF3D@5n8GnUkn55BYY0ihpmCfIRaLTfq}&_F$a#ImQ2z#Ie)8{1X8kx{9hoeM)j2S$F*!v zb;0}b5WJ;i`1aeqC6+ZT& ziV+jkQyl-n22(RDU-`0TBIgOX+rz(}IPYa+cp8Fz9wsNh0=lN}mCw_pCt>0(q^uh@jB@C5h# z{N}DrncX&r>UYT!emwnxdla(-Nz90=g*N>3cQJUf4vMaKnJgb3@)6GD_DN3-deE~< zQMl%j1oIMN!}?iC(A=)jouNUkA6Vz;RRXyn4_3tB@y0??MzRyl*WPk!9KGWTxI|oa zg|ojl-1dgfyQT0@OVuO}hl!g!kt+d@dqAOQ+~wXJX6|6&`jn+d7-Hx+KJ@kg!1RTp zl5*8$y!)6PZxTfzWlJq@p#~CgU?^Uj1yrm{N_=I-4VG@+ne_k^G(ravOh!^9=%{b0 z_I@=j$aADPET|pO+~0pH~T z;u^hECfLvTsoBtE$MSrATWRN##Vd=QyZNK0!5fY~?thT>{L9~cMVBUraVT8<)J$oM zYW}Xc-~Ra#12R=BU>;}3O93*F_D%_H2;{F?#|_niErEfN5moKqN>I{ZIuT=FW4MA6 zOj&6s1D$PW2g1V5?EvJw86-Fm4V{Pp9lD&Fu(?HrQ^+_rsW#Nn!Yrq2&4B}pDiS*F z1SFTlS&H3H2_3Cv6*U%bHwTI6*Ct^amx-YRKC~!7W9k$F5T}q!L4E~|=|yyHcy*qUL9m$|`(eFPaFLetUgRoglsjdz z!d~LR9(XQKJz`v7C!d~8nDaeMwI+m!p&SSu*Qt!h7(;!6Q{jvATj63Rt}wYAF9Xy& z&2i}fuos-67)oRT)pkCHSI_{DR60^`FYOJd!8Y$V#(by6Xf~vJJE%0RbfddXciA+^ zZ+Ip<6OR?!!#lh+3MFWJ{gA@p2k)SB0lh%k6CchN(NwkGmkvzy?FO<` zKYq4r)S>nC!FRF-a+kixec&@k)JG&}i(So$OP!oOi9>jVLdqo99jWMX2K z2oW;lz)-c8N{=?TO8}eJ0~e$NHgC3<==k9OF-n`rqeGej@dE(KIVNe(Y^ceNI=hI zYH$(A*Tvi^La1!3qun+8Km?C%a<1l$E zoyoIk@SUplO&s1O)5ZB<-jO#$1K&`H=Al#4eVfTQ+nR*hC2_JVXDsyZ>T#S# zi=TcNl%VvNKP-@Bbfd6vGJ3B`h?rBD(B^x#nE)C5NpvBB7{k$&|F}fu5Hp803+jXt zcVL5T!-G-nGJSU0;xHD4_rTXg#r8Xoa;YWNcTst&s;Iwl*E8io?sZx(VfGB z&DgEmjdsbioK-Eaaecj>5*t5OL$+@8ynt$mQq}d&OP0ZA6dBCdCQ<5ZMwm6yl6!k4BQ~ zl7HHwR#%GzRimgnX-{%yJXico6}nH0LsOsW$y43=wXv-GK;Bcro@EboY;jr#Yrfz1yx9(gWuz( zJ(bXkLs8p)Z3C-6(|X$saQFnf5p?rq#WgCf3SlO`HL9ULOfaDjo~X8yx4@lqM0gz~ zsngmdQ{&w0Am{{INsrit!qQb-z8GP5E8YtjxI%AVST{oxXv3J>7Vj!igPKjFK)O3F}n{XJ|$-= zytV+qw^&iZWG9pALi00V=8N5dIl*swJbgt%Y<84zj~#%WvF9>{!!lz)#cGb+N71%FD@Q(BA>H@1gy|ehtU7E-AvoqH`!33=<_>6Y!Msq_ z6*iL(Ap`JQy1GP%x-~6h4uXCgFD#g9S={FX@k{X7E!b+ZD!UAj@Ez&(i>U^_>Dk#~ z1$W@z#Dq;qCV;BTy0$c5ja+MD>s1_d6_s2GN(KrxoPB&@_eaRGXw_W(gh;*HT)e?@S6a5%(NOSa5e!(FOGM;)hO^K z_{gCF-1RBKM;fBuwgd57g6q!i218_ULRZ-l;hr_$OqNL!UDuWS5MoL>$dr=8<|j~b z2x7CEK2T0?Ziruk)7e5zQ$=t3zXv+eNY?2hka%e{hb%k{>-ROfJzNuWB)XdtDYDcCdZqk7Cuf?xWcrIFCg>U|rkR&N05AWb`MqDuzn^V9tt3yhwthQdcD1zj)F{W0D8~&r@89O!wr$> zT~PSEkqk8QyR0Tg9FC~etzdy0q}$TBa1_XYjhM1p0z*v}a4X|L&_rOg$qt6;^!TG| zl1%6#AcTCN$T;`JKDibCDCOPdQ1HwP5O1seS9=Ha+HjukgqeC590x(J##P zGP11@A+ErwF7o=D+R6z1c*m6FQU@lvrB_GO`;}^iZ?;aIO!k)^g71IZL2brOGPkP< z(*&83@%akon#i@0!6ZhD4*{{ot`0Wm7~S6c66LlU_Es}Z1Z)^81OTtAkcMklNo=Gq zzBZLd-&*y!q`K!QUx!Ay8|+KdU?`Eg88cD~={N(|h=@2Z2pt%D7?qvHfkhK-7u&8F06>mr=K(M^2f~O%{1H(IC(Onu=FlGYG6HIo?H8S*E+-sRD-l$GEN`< ze*&YqC2{{e(Wt0P62;b1cX_iUAtf;(CX(Q-0rq<_heVQOHg>a>Y%Qf%-@x?wF)Z8g zz&{UdTP;_yab7@?7K=_OWP39MeYVh(3Vp36SJUyBE&*LHCl~%bFkmuD-z>9r!`oKF zw)qw1);~Ox#A_P}{K$mCxX-wm19V;!Zj^sKI#4>VLduk&-CFqH63&|JW!uV?tMd7Y zjfvvJ0%{%SLl=KZoT?h#sEEx z-1)HpaN0}Xs`1_e#5?}F90gi9j+Cv9?hcr$!-~s62T+}I}Ko0i{Rm3IuwLQj#O zYVuo#TEqy&Gj1!AYAwOY5h>)RNI}DHnj#4u$rZ$>^bhwe{nMi?m@3Fl739FsQd%*n z^Vf*Dop}d2FKVn-Vw+8&cXn4%S+{#2y|8>&vdw$06&thu>l+jDy81S4WU6z)7eM^U z6CjtXNn_u-tG52h!FZYL{s2ooP(#Chdxg0YV_jijZ=yRSgVUx=8)j+5a9z+`PM_a4;4)nGO;c)EfFA3n%WJmFreoR+wY87@xAiQg_$$eS8S(OE-kT= z$wVS|Ot!2GN3RY>=k_N+sUQWrC*-DnWiH z8ZRfj@WDD3!&#xSItJef!OAi5YAXc!prv<9ZFaaCAx|KHw&6r|5CHUW{@Rz1Yl66} zz?{dh(7X2uiDUb3&WN^so6+u&>4}R3i)yCmzbaxa%$AC@0eoCfAA!k3H5k4)J(f_;r? z1qNPjP)=*|F+C2yF#A=p!A3AJNe2}Tj4(E~Ug%BQnPt$uYU(~5&YWvKKmFRte8DtU zbJ&ysOF*>0x||!_a%?-P8-=XU7JA^f(Iq24=e@(8^htNKZ4dVJMzY!RML({Z4EkNO zGTGN1oo8Q3jr=(jBcz=}`TIYcK4#K~(bQl&(>;sPIQaD&q07c4F0k>i0)Ok(fm;SY zf-8Li0#eI=Yz~M696-CkH)0U&X|Zb6`>k&>^K>Z_;MVFaB=9{z7&g#8y8I&`2Bf2U ze}E2jG70aC2zZ_iLo_MXt6vfl&oA^`~!HDq|?(40)SPVa!Ym+QX%7HlQ z9uZ?o9Um$y)ygDb>&Gp0SfTali*dP1PQeP%bIH4fAU4_=z1|FU9oCNO&}WN2thVDu zW(U8wnODpdEmPSB63%cNgVkjiW=Gngi>8xjxr9bz4_GX= zFF@0weY75vBuC9($0k&`M)ZID@pgT+YBAUb=M0^9Gn^c67pf~$0&iBZe>PIlGmIyy zstb!_tdvm0H^$VXbW=+hQj_$jz&7H@c=^6S?`Bq5b)-e`8%J=d)IvxC6=oppPSZ+L zI;YUiqN5Z=P5Nz2$~XwCX~xT_(yzDLm?K#C*|#hy8mr(MNctq`R##F#p;~3IEma&4 zQw{zYCZ0r5yva3d8Y#c80@9yzqD`pL0TQexNVJ`e?-uzaE>#hb^tD}!#M{%}*-d=Z zWjXOJQK~-L`gZmtc2%wI2?#td*IK}!Mov$n@(QEY3o8@yt5BxX+R8JCwQR@;g_B}I zGN;aiv&RxjWv`v1J~RJyq~p5cq*IS~w@}>+(JeLBClenWE+!0xQ)-url!d^l!N*CP zgNRKlh$b1%LFPio&>K%v4T_)$2(h^E24Xyk$)(asi-jZ!$d97j>|jBYmQ&r+jVN+t3yVD5Q&$=jo8POzQjptXg^HL}PvOKH`H^QT0~<6dD~pG=4EkN_|kMy}WE$|8B2GhrL#pL}aX z_F!N^MV3HAjx*{qD8@zdOOU~(Gg$YXW-2Y*%?;P!5fbk5TUrN^WwQPYBXFd$tb%=9b7sPYn35! z^M`6Es5lO`6cQWrW=|HGqM!V#3%Kqgte?*tdTnTQC zGVC{V=aq_cOo67kWKkcl>>_x6BBSjNB4mouGUfdBq;QqC+FGUVqzC&MM#-Q6N`lTP z^)?ak^Xu{LeIX&?Obr8HLZcj-LN;a(VG1++6iw|H+%r0oV#;6HPKtls-jedh#<2#@*tLp!$yU_r?Bp;J9w2$syIzS;mEfmv&(694*?tjWC1+ z5uN@ObB3Mddn`NV1z81wZLr;az>IjPN=W#<*`3ED;-=f-+$BzLYcd}Nd?7}}K;Wt+ zM#7>ucIm!2g=>0aM0`jlU=8Dv$m)!~mcGGFXwVF!IX)w8f*ov4GpQ9!`gcsnl z=AuKkP3-XKltAT@r1ReQR6em|cy@c3J2P7pps0bFI32)a#Xc9_#vk_>`*x}Wr2XmF zpzd0Th z&eU3_h=&yRCLhS&Y)vcEph(uZf$qW(T5S^jur%L^opCIA%>rS4h;h!VHg4~&6F~w- zxdOs8C4;)7oVN7(gQ9rMhGkhB9U-<|lP3xiA_|=mN(Oh=W)3PN4f{|Mr9}5ec>quZ z+eLM6c8Ku2=+=g03NrDJ9W+q7@9nZnLw70YltZs#Y2GG?2}w zg)HO5J-f0K4e8i<=AFUb8qd+Vh1HeL5xkN9drmz7m}{$e)oUrPGX`#$CF1~lbqZx; zV1!@cMa6tNGC^TKz~s-&H!^pPp9~8|w^1rQ4VwLfT}y|HyqJ>*~?3=j3SLb$1TjAmcZbqYwf! z+vf$of>p3kpT~?8%a;u{{HHN)e+{Fw4p>X&n|8;)gEH82DDE12p0C26P1f3BwisIf z!+xjce7Tj?T~q*RYVbQBI`6y;8y;{=#9%wB^@w#$4n|qC4`g;Da{vKB&cZfPiQo|{ z{f|J#fvBHi9_5P##G?xIFtKRwDveC&hh!m}^`Q&7Z;B}c!A6vLO^UGdb4h{{1*Xm^ z^~P!65sAbPTki3f)u}((+vMOnp(H5+2VptW6JQ*Nn?rJ`z|u_pp6N4AGxMPtO=Ab7 z8?`X{_;ilCCeSCF{Q&GJ)5}bE{44ax20I*T=ARVhRuL$GO*W_g#>Trd)2pfqwWPcj zEEpxJt@XH(@MMRi7oNE=a)oP54d-dG{Am0bMvT$Jp7cSf2W$+4(R?`sMQ0B>V@`#g zl76NjTX{hW@@?nSQAj6Nm=}{|@nq_{0&R6Bekg_z>yYLfL*tgO>+QWxZEW-a=f{8z z3v^P|0|mL;u}|(xa;n@n{2Ht~_+9gqp4dL_5_!>1wiI})d!kJjWElBuLIJdpLj!Lf zok`xCct`Q7eTzHP(2~)kNq7sh*lI~6+-6&icLJ}e2#Tg=5R+2O$GuiVYSwfR4ZHOP z97q=lPKP)KDf9k-B_Z8$qCR5wqapFzOn_{M+5QyUuOY)BPcXDFS1c#4h{J87$h?ox zL>^%v!|_0mlaI9~EMxcgH4|zUq~X4x5V`>#ZR$Z-psxhHQ2?bFy51O- zegxe)SC2h}7Pb`i0gQc85T$Jt(on`INPA9uh2A`)MmMmQGPN(~NUQ6TPk`&@15GWw zM2b2o9=z8T@&pmEFcwrAVFfKZ6-JgC5Vk0snL2{-M>}e~c75aR`E3*z9>mBPh$>8l z-4kSC0rR4J_i?jNg3kmGXP#GQgXzxP_Prx(w?)a(o#|E}u5d_(`h&S?6Xo7p>bt*s z-p68+sb$<=I5Zbr%BDSv>GKA36YlKEvk~@XOK4ULV9{d`b*ln+6B5KjN9ejTS__*i zl=lyFB^EjuJrUf2$GTEXuNj1>acWo8BfKaYlk&Efcv74|!#59M8s6Q7OU zu47Y!RG&+mYA3nKGJ&Zeg>8&y{1eA#jKrrNfMBVEtKO|XDa`5$ z1VZg@pHhW0#*C&U)3lKY%%O;XtPj^!Gu}6y1obK`>^KPg=@^>6Z>BdVG*7T&WogVT zLTEu)e=|Yilm=8KiR`*F9H;620Hg3{lU>*W%KNA@gf6fAAyiXIW4xwjF@vb)m$`nqAn>4m#$g9vkkx*$~KWX5#? z;YW}2<6DT9aP~t)e|z`E8UlqHKGhmA9GB}Z6gYHFtZH&dvrh188W9r-GBdBvA%Yfl z34_Z#!2pa$Kw{DlL32QYIRsxuOdfi)FlO`kYWd_5pkwm~H9y%698ciG!A|lJV&U{> za1TzDF*x+Vc1myKmr7FS?p!Kx_uL8!D-#{^lex_!b4iG2dQm?y5>Ris`IOUD z@WvU@OU;iS%gAV&Z8$^wknCqR2PQ*-unfgLH_9Ra50DiZ_vk11{9Qg%Gs0qg$|4BN zCOO!%k2d#w<^CD|Wsfl;^P2oxAWd0-@op&|y$T&IiFlznl`GjxvR%rvEtI{f6iQ?x zinB5v)RgXCA|Yext3hv`M-|^ARg&q(+$ph2{I2{!B~KvsA62z4RuG#C}>Im>C{>_y<0PVHBK zyY_^IaNj8(=Mbie6)YT^?I1!I@lKyqRGo;;uepCe<{sBIDLm2UCAsG3)(K5D^H4l zJv|t=wFN$NGdKq|+6fb?Jnbi>+I4UZ1p3Jk~Htj^nOik#BX0?P%CulOWIHxbDboxRmE!D)N*;V4800DziDg3zomI+8) zwi+21Z7FSMPyfE-E#&4t{kh{U)aE{&vEv_&4TFyP0(+Pm&(>{#ni+Kt5hKKk88K;gm6AC05dx7i9;v!RN>u=#`9uYJ1}M;qhQjb_V>!awG*6%(MUW=) z#i;+hSTZbYbc#=27tZ6(^4DD!2xw-2j_+4Me)gH=*9=T~z7^}jn7568XwAkkU_4Ev zlY`(poYBefIPH_(=9vMy`unCxcS>>wdkPb}@XYW_(oWB`w_Ni7xw+`X9X8EVx;IGs z&krg=vRAS^VG;)u`QkDcY{ZS2#oMb0f z{IsSjx<=lO_oV``z2_q2*AhinpeRc&Pd+c>k29&G!fDkZdZvEwg(;$z{6Zee1Q3i7 zlfH=kOfe`;FtC#ze2tRI!(_WD1sj#!^9(rW2($t(XY#lo*wAO)4K0-Sod9kQUPZ4* zAqL=l)+pvNE?CabSnp8!#Ga7bZT2^r;etiXku^{R-rBjD<8VFoS$B7>2|oED@5N&= zGG#A0KM^lYvfn-&~4_^St#KF*-)+O-u%{?sE~-Z2B+ak5m!Q_CX{xro47^MMI`;6` zwM9+b0pZ@j@TqHs__s7CCaHXJb+NPes(|y`kre2u9SoI1ow+S3CV6zMa!4?yC69S- zK&owSUy?~SV56}cZyms9sXwzDdbu$?*y~gdfSU>xD09{K!|P7;hz`S!Kw?Mp^Qym7 z72=bX%`xLl9=e?6gEyg9%zGw|Md`vdp&8AWl1#W`xCBP=Iu@{AgFE< zLc%hRcWL@xzGMr%^z@48$HZexTgXrI>pb^G$BDFXJ{*E_Q35ITV>6r41w`D(nr735 zB4U$b(})Y*`18$#QxVg2W?uAa;mHhl?oMjSn)9;fOeZsjNQFX2*TSH((6--!j+?u> z;(c6oWQt_pivbiNY$mnt{Yce%`zP^8s6FLk$f5c9O(bxYOfLpN-rdr(q6z z9o_sd8u$dW3aC_)hdm7RmVT^v+do@PoccaME<_l~t$LK`uzLPu09f?V?tz@eghNa) zh;9uo_ayJkh|WkUDsLXM2%+J9UYZcrFDlHA{DVT(iZ0CweBwV$7;s)3j{wM(+!`$f zIWBI6_PS1hE%8kr98-BJ9CAh6k2y7331g8e_Jx2rvQ8w*lSMS3*Nqkfu01q-+$!Tk z+|O4|rl#4v-B$$z?M@+;wD!9fr&5j7P}{1KL`mczWCa&LV566^MS!m=v?1RGMwblo<`%w;+1^RZD-C?#ugr)U>n2m&9^g7XV>V2Ji*c8hC-ga;JvFZ+gA zFZ-khT>26AQeMrGWd59q!B^Ynq1+7=-^4!kaBmPr;HCD4$kPutI@mwumPmJ5tl4G8 z`?~ichjBqdnu8Sv2I!AE+P}cNZ#7-DCza|yI(BkAlPMx5CRvDP$&A{Z)f#FFgtnUJ zYp^*F#ZN)oC<2I)6ZvxI`Qc)&@lopr>~i5=GTOMlGu8Ct&v4X3!(EEDQ$3tChAp(i zsU+quRWtGNQ2IDg$49LOjSn%Rv?#$@%4X1U?et)I^4 zV}e9*rk}@5%yQTfZAHidzDOX^pwR~dgxembDc*ZT-J;-}dM18l$8>r2jGc;X7wMhR zLIG$1USk1RayNHga=U-s-vsZPdPBCW@)O-dw5^E&?UYJ-3?2ur zOi?|;f%hX$xHVxkBREpWX&PwjNoPWJtqL;L#L)6!1 zZ9?{0HB)MM=_?}4QL6hftze%RkLn9~U~EVjd1M0G?LgF`orceugZ2--Gs$MIQ`(2E zM_kBS$SStSK((T4%ku@I&)CnwTv=HkroE6d=K0gynXt&*mQqTwP&CLeRV@z7nteZ| zw-fD)KdlQFP#fZ9I38)t!iFMw(qujK3Y&O;i!96y+z+EW?p@r<3>eB_VTK zdgJ^wSN&M*E^na@l!$Z9q9*%Ys5j$SO;+$s z$MmBJS;&up!46xXRL}bLCNkeWoKW+<%?U&sfQcZ1ZDU(p628r2uK;uu)K43l+0qwv zP0R6O!-f}#!ui;FPt-w5EvUb-`4!NVas|4l_#MuOAk;Oe$*@opv%3CZgktHGov`gpOqiqXn zVTXH>i#55l1TKBNa%tiK-FardB$5Z5 zUEqZ55XN$dfLT#ebviP}<3^BRPA>{j5oQf5cuK0at>i!ia;c74vHvJZ^7#gxfoU?= zBMuRx46(5Z{%f}Z93(9En--cJnlb_)|NfmWAxthd1wNBxS|(7qi3BNZ4ca0USo?y0 z_9Mk&d^d6Zh(eY%D{!UA&;6T7v0e@__H0?x@p|+emo=Hs$h>3oT4a>_&G~_-r zK=mwOpfO1W7mV;yUyJob3(zI%o^~YZ%%BTQN3}%>P9X&c)QR|@eN-u;o}#Ad`;&Hz z47U3u>>&b`296aq3;23Y$Tl`{nPjLmBn$J!pFFYsarwrag5ujg2U0B;&-!-!M;KjY z#&!P`z`1TYExG|+3AIaD< zq#jhe81yUWY{iFqvgwBXOul;t(~e_G2mLs&7_LQW80GJbp(I;uu)X}5KwQGsnaQp( zJRwpLBnnrAY)!JPd7lw)BXL&jYf@TG6>g1soUm1|aZOndDfhcEBb;g!1GNLszgG|lMZnF8qF6Mru} z;x$i2@u)R`M9@MJ;DHwv?o`EA&r47zL*MkhRpWr#TO9yL1&iW53xs#sAf{JmP~J(y z6X8xnDn!9XJ6`xxq(HUP9Swk&ZfjI@m5e0D8T-SS3(2kH7 z1FQyW1f`H`@u8M-I2rci3)g+%YsuV2-Rtnp{EH`#s_&4jR)p9Ju(?0rwWg5eN_S30 zOF8{j715gu1hXc3KQG>rT^NG1`51n2MkTej6;JXUMem|r({XA~_T1?VA3b7HB`;W` zfEm>Q@N>{QuV+p?;I+?AC-sc5tX!>vZRd>ShS_HqiA0u|;5A3?-EMFMfiVOyrXL$3 zP~TKcrfZ%j2Wg(*0ra!XcVHFkab={hp?Y>tH}lFWXonanmQl&tevL35BaU{+Qh@&R z%5F$rHSv!i-*@V*l&Efq#M&n$MlPrd45HjEpxT+=j#$8A>FECDR!EpP1)|l^`-+Js zF138ceDLK(pSyEcWgvX9G~n~asZ%8-AoLf+D<<7|Jry80;OZRQ-|#Kx04fk1W#l1c zk%*sh^U59#EK6tkq^&qRxG^k+sSTq1uSNb0vY3{*R0UdZB^+CteYmccvjZ!- z>(w4t)-*Kjp7J9E;Yty-Wo;{x=Mu>%BqcL>>Mmtd$-SR~@#8S|O?sg$`LaFVQZeH) zvMP^!;hJP-RPQ*9^{+S_{X^BJFo&e${1=HMjw(HenLIw-Z>$`Q>na$RZWcs+* zHd)0~&;^a-VCcn1YeCyEAjWPB;gZ4?+NY9X-pJU!XinuE=n| zk$@tshhPZ&nAh%XAR+~!e1C!n?G0G4gl8ScP|9nU=rVb$;*U^n180V=lLxbNVS)z} zZOJ`5nFz`p#!;n=1TM3TaDGlm&|8V~R37a8Z z7=C{9M6*be)k!MLV0aU;r{PrtM|7x3dIdVzFVW@NOGnDd*CYsMXnjP9Fg0jL>*`NQ zp5vTlu7Fc--|D4DVlJr5sh*V+j7Iv009NI#d#O_a;$7Mz@T|a@xxtax_nGL*tdw=s zOF&3nbwt_*FzzfHDB%Xo`~Tp?h1drWa>eFTv~H~qU{1Rc+aSdX_B0a6`y6^Q1%8yA zE(VCwKr?+Kp%p&P$bb2Ywr$KHDV{b3U&&f0;7i)W=Ix2wky_mb?-UCYPg@h=;RSJt zN8f_4{glkjb?k(MVL3ORjGBS{_m=cCY0;2|!`zmcyM0{J-HKl)xpx5ES=BsW0Qc>i zDo$BUC4rr6N0G2ty6+SL0#mBj5-9;hdZ%$;yY(sk3vNA0%5;z=&j@t@+U@4*OFkP! zy3qfI?WW{^D0Dz@=%MtZZ{x_D>)Ba9bu&30L7I&uFSN2D$MI?^mC4Lj;#)k?NuP7R z#Ej@GWZ!??0MU6_c~UaT2+h3Dw?OjeyhsCM`Mas-11>P;y||i7cv(Q#e$Wcqr}8&x zmCyo%KL-9ZD8s#F&UPX9N`uF0QQk+cK1($yU}R| z=Wd(Y{xrt+Q^p)BRp$b|}jssR1 z23JL}JdECJ@H%x5HPXzzI?vrg&-}v(66#z$8f&mDQCx94pPQD(gLLqqD#-jEQfzL} zVVf#C6K2)u;b=J~IMHsW;!K}VQgnMTT!3Uoc_&P-g-C-6z3^bFNOK!z@Irm+a1VS^ zy`7*Ry^r16SgJ9d;CpV?a(XT!0-kiRQ4>f^Bl{$m4IO|hXnNv{ppRlTZ=LuG*+PCW zOkgQX1GmV__uN~+QfqkZVybx}N`^z{%y@q(Tgo~=pBPQsv`5pvp zR3nt!@X-KC!FlY5=PWpl3qAvjj_`Xf6cx@ayiYQ5DRJO=<}u_PJF6_P#6lW$Qou(~ zcSLwHRT!uk*p--MCoV|h6(1b)_qxG3cX|CICKc`0IYGWPHI7afi!+HDqPTF=_vb!g zgLfSC=lc&~Sb6Z7_Oi0ib<2h3Olw(FPLIuKLnCFha%L^Yrt(tRw7=`~lFPV|HR}yN z`}-}=lI_iJOb8y-6$OuqKjqJH|3n{FL7HZmYHyUHWnKlkINYBc+oDKD!06ub5L)O3 zHMi;Hm-9MT4Mu9B^yxw4r(b95w0==b^3Su#w!Xxf_lT z+7(P;^?l=c7MU^7xN-dWHKXaLe(~L{*D{!*6)EAl3V-@nZCOEHr;e08+D$KAe@}*y z2^qYN-kTG3YAj`taew4OvaK#j3NR4lWS(28!MXyh-i1T^qjBN|xX9rLo&DPDjLra8 zb%Y~RHcmrNwpJPA?|Owz6SJ9ia&?Qr%Wm|F-Z)L)V&T8xy|D7Q01Zpuf7rb0D@odJGXnJ`|mHLK@7hwSU0A@VNTt%GKgzz_T9A1XI*$xV7Jv}r1 zyb~+n@v*+7Qy9>Uw{o8batL09*P*jxdS@Hpokqq1-+-6l=@?dS!_a8p&}%$jQY(7H ztC*6){8?VY{9THNs&2lKzmAA`ved3nVDhQFNR49+Mwz-IM z4&S5QMc2B812FV!*!HNy1FhK2Lm2yy^jJOr0bYdvEG!!Nkh{SI-Mu&XRD0)G@fDNQ zpm*}*k-0%%1=vdl=gJ=+5X&r^3KC}nV#<5+aDl1@eS)XDm?)qW6&~_&q3&o7MsCUE z2fajYbC8fdKQ`geU+Ct&%%KY~zRtZ;=WNs*<1DCdP=m_r#kv9?yqBwS8Fg`IgB#-` z`paunn@Iw-V{rhK8=873YSH@i_X?nMSl;#uYrDR%40{*tot&}`1whe89Vq?^maLOU zIPK9eN1j~%k{5%BsG?|GwZtSO2iB7-wZ6WlK16l=4zT< zV}nu(_=Fv*#G@$JN?2gmEM?9t==3|Lfn^NYVjM=|`ar8sACokQ`x3u@IQ;i5FkZ06 zx;7G@O>K=mSXD1a*^IWDELXY7AO+ncM+r+TaH7ryn_IZ#kOeB?tFb|JFB%>b!~Z}0 z`xhcM=@l9_f(dKFzv&xaAkYgN9!}sB(&09JNKZY~#0z3=fg)S8+O#Bvp2oJG^ ziTDWE9NBh~Sxq^t2Ib%20DD1S`pgdrJPjzX2+u2UxXirzZ z5FYjj{kVaGd!6Zm1|tNXmf%$pSYU_AVV0{9T1&O4&eOMKi>P{-q6R4H`K0{k@Nb6g zWrz&fjH(EZISQ7F<0iZ!49DSq)dFdhq(~5~n|IqK0A41Zrb#J)r*L{drya_OL1Oj^ zE+Yh`GU}Dns!WN@WfMNPdeXC2RJXOJlDk9-oy8oPHr_z&_u8r=fEIEeg{TPW2Q$kt z2zvd{nj!nRLP*LzX@1ELxFQfOC!|n+m9{oBi;d4o`=34%%TKfmJoOj8|3Nq9ulv#|XOqMi+Uf zB2$+$?n$>Gf{x-*JH+bkeS zYBLN#P)icO!?1zEG#bvb29Gy9!|N7j!u0#tAec2BfT76=XJ9~2+VgVGlW5!P%%37w z#Y7f2HKL%2CqpM5E^@!<0{dmY+t>_U7XzI`^$|i9lImn&6Cy?Y?4xHLJsRT@&neN< z{S+q&j!X<{DA9wZQ`XgIPy8-8CeU<4#-VTlVXsjdw)B09L?n4#vm<@(+#`M=yR74! zM06WH+#{K_9aDdgIgx#p!dS*dw-Dy>w@xMk-Z!LlX*E<>q-@&ttD)8R#-@% zlH@`(?_xA`5hK9vc=?BC-KkIZZrA}j=|VqGZQ18o8g{DO-b7+a?zKe1JK`Gg6FYS= z0NzMN24#lO`W+=L;SIZc798dlA-0o#6GVUn1_l{&2@md_CcSM$_ozav=`#qin8p~p)T(6iZ- zn)()H`55dz`T;!mx6tbwtUAg-?H6)<=IZ^NY1Qy5wH@9mt;oyg?G|^;1mYEGx6!7w z5y@1dQv?MzRu!4mz;Hk!b$Ip-SxCpsc0C(!#E`@^pDdh+huB!2cs%JG-$@5{SzIf{ zefaxbhtw_03{Z$}uE9L7!9{ri)3{fWqzTyOE#k9Z4i_o#fvDi;G*iP@`6B<0UTgd> zusji|1d~H-m5hyrf?|mF(9pHs;8mO3ox_+>M>f1ZOFS{{Jyw*xjlAD$6BVRKiZnco zi;z?(n`3oParb`X4~_0v{cNg)2GsWveGRoJfN!y5?UMhSsB} zxy-0K2k)z)1QiP?^8TD^>*Smnb7X@1Qmv)ih_cv+Vs?}-~I!& zMM?*6w}C8B*;SO7lqNSM*8u|nt|>%vJ!DdlSrI?lJ@%jP7zneEgaTP8I}ZLm*XgP# zm>1aGq%%8dDrKYpi?3GMCiq41nGg`jXn=)efChsg4xB%6w9mlupwN+}znIbah(+n!@z8e(MST>ej`5W@eWNK z7wVW6?kLv*B}~0=8w!|I!s5TD&>mwRZtq}`Z6E#_bdU=v8Be^$XGpl2G1zDS&6`0M z3NEe?dd?bql3lzTi=Fu_Wo1U zhbpJc=fB;BokmG!f0K~Dt^z{*^zaY-m@-uk4m8fy)z(l`Q|p2b2L5jA?T7#2aFkFf zPpneeL%}DTw`w{MPiQFS(G0QCAX*%(hc&%;27@zn()ALbbml%RE?#Q0(V~6OLjNJr zVyKHWOVx+CRC8j)2gh~xYyQf9H0~atU5k~J*AkK=oAPdm20{e^Q6x)K1dOb)FAFcO z3atlF(e{7V%!hoj$@Q=aCvQ6|QD{9|*hw}cD))8G7E`2l&G9>Ab;R8#{n_pG2lqKF z;Pp$=(N6&;zg(#6PVWkHK#9|!UHdqm-0+N;$)}QW(=`?*m*hh)!x5D44-jjM7o`NME9dCqn~E^6@^}zWlw$B-&bVU>q`$}#O~c? zCq?F$w(=K3p%&eRp;fSGDOY@+*+pZ)dml(^ZH+bXpj10#+g)M!q67H?m-xkVD-pqj zNb`n6o-@^HGpPZu>3z#u4aaOUR(hY&B%F&~r$!Sgizp$}pMzf34ZZBG{U}bi0yXjZ zy}lC7YM>Le$se_d5}I}aRDx}|8+TAXgf z)oL!(@gs@?cAYIwc{&FMC^BCYlA%XGnh&+Qo+`YN5O)Gx2AE@z0U0p{BXRijzGe^j zJP)7M6g&OAlp$aGN$R4lh`I*ot9GTQnOBZL(oX`=lMb~L+Hqq(bE=S56IEjw(S@4> z??sgg!c8aEc&p)+kvFX1IvL(`bmZ>YXn0~Q1WFAuJz?~G<;@O9eYK_8kU}sOMykSd zzUayRN1ZT;(n=hC>jS+znx$$VC~>tL;JfYGX>f(Xu z^r4IT{OU;D$JpArnrkM~T#;xx>6q05c&{e-}|*5 ze;W3DI>P$-6>U^FmeE)>Sok zmj!{**E5>wsFg14J!?LVZSj84F&qj#Kae_h$|T5I_pRU3=3X5)N{I$^#vD@{q(IR; z^OsP#O)S?sa<;K6Z)um#q(8}k^11jUb|%b8A|dY6Z^C zWQitTw7+^cl&4HahuYqiKXf0b!Tkuzf!xmVC4(*kNw?8t7AcveSS{aPGjtLUBh8di z4a1e%dX#NMElVJ-x05%bOt2b8-&lC0T+Cp!cSW0tm`-*Ryf=Oa>k=sEo93MeA5*f3 zW}+y&F0RHWclRVYcXs5?rk#`7-|@$7H#~LcU-s4Qz0Rj)$yNnZ91K=a`(d2y=oQAy zJ1Q0Xg4%8_WtiRd+}8D9c3dbzctk5A;mwvUY9UE9xq{V_Ahj9Z7eG>W_;Tm5Np-nM zT6F{xi`|UXI_1amN@7AXLxWGU5qLF>`vUX@^|G&Z{F_H3Js~X>WXyEuUt18mzO~?S zy(|4X^1^1Ue&%UM8!ohCTcS62*tgQ>QM?k~j8jsBasptINo$&p3j(dh=aAGG(AqY+ z(_T0WN4Oz5L5DNZcJ@!J0qF1N9oR< zec1!d`ULbRUC|EQVlach3^}&S4AABDl~q@SWlVmT>G1PF2kRsQ#pH?kOn zqt7@8SI8tsMzP74QnJmvCzX@1@M@EJ8heM2w1g-W70k!L;hAenh$J{uCPC~z-rdjv zJz;oU-}^cVhSYgo1b?Zng*P1A!VOcMa!;A79u%$sTqF8d+cI2F{1ICku z4=#SXH6#-E{PKp4Gl$VNo8Ok*bJiUu!uSm=cjJ#t>r~$C+SC-D~-{r!_PDE{$Dr@#zdw0#Cw`TFgr8}vnYKk%1p&i^6v8`(2 zLTkc9LGF=-V~DXgtyoKzL8rW<7b=)hhGctFDtg#^>>|9d@hV-DHz2j5%O`m9qN)Z> zEqdI(DxEu@&27HkYOx_E^6mSa)j3OHxfj&4Qf82{Qz(q((`$NPH>U+|lGeeq;JE5V zS~(@VP5hD{7Wo!W3sTg3bVwo#`9(z-MU~q{PVsQPk25HJ)%Q{|d*Qv^A!Vu2Y^u|k zow_0Av%TIj|NNQZVRU6;rei*N?2#-M3NlS|r!!wnPQ6I=D}MVX=^x`i9R=dp5gwY) zMGB!~BjbXRAj?F=Nb5&t3uM6+m-oi126a?x$Cb9wG4@n5vkI!q^^Gp{XEQBh^F)Z* zS7}C8?A~8n%-G3nxmBYCw&}FR8tFCWY!rbVqPX@vZN?|fyq+$`8eixhe>9?Ko?}K< zJ24k;sbIDyV@)pp zj17Soa##2(cRQq^p(3a)S%%jPd?VkqV)pib=$cRz<;}b=kfZhe%c^FL!rP-QqnP=K zOsbwpasOETDr5k9sA}`K%$8A??lHN`GQ6DJBDdDPw&06z{)`)iAX7Qts6V3fUP)7y zvVp_ONvD02f-RM-*wvRm)Aa2@x4OIc%chp3#W|8IVMTOd{P>*F6B4xn2##6x_UTha zdmel0F_iQK8S-gLVs6DwOJM6-E4cqI|7ZvlSNPW>p#^%0cCYQMXZFwAHoJ9Iy6%!t zLNqwWumSC__DYRmLY{Wf;+X4PLg;vLd3a*9t;gq*Q}3v^9mlW*4IB_n@AM0~1nMSF zBrLDq_(D4z#i3+6z9~?10S&Jl?X_((k}!`CLnR+1D8MCZY?09rC}{XI7F z20321fn-8$9Bk`+{hq3t`hhOeh=lX$kQaQ~vCxsJ_kexVd$98I~!XnV4=@3xcX1 z$9f1Rl~pN>OkgDN*BD0Co^T5;7$rM<_KwZ1j`|cmDL*FN9pAp_LK)q-)CT{IoH zi;Y`t`*t()X+^N7qWveHJuMiWtIB<&06x&4@AwwOu!WFSHuwJ-!xna%YopfLU1gP{ zryadg_tUNpomeO2ywvc|r3^G$ z16@HbV?j93rFG+cvb-mb^7-Wa7jb@3Zuihj?RT49UyDc*{8Tw z?`p+c{d=?Ls(!gWn;VITg-D09xLH(HoZ_f59JwIXd$=4?LzO^v7%7{`M#?EitccJ^ zE6t9nZ%}ws>_gQ$er6p2EPAR+>pp-zQnNr*+10b(GxpvvBu?WZNp}=`6W>hPn^98PGb0pSR}he} zN*CBp%$&pm3@CT%WFmG)f)3;duJ%H+ATDCs!9M2trxyI&qTh?n#r0BZ1yrhGgXOLm zsq?{{Qly0RO%u_%#Lz}e)MnkZ_82RR?MGPf?|^bo{g?w04(-Jhfj~_I1uK8BcrM(< zlS$~R$5>WQ@4QNDj9KS2P*ow=AzKY#O=ut>wvdK!>=lqeLSe}W7_`grQ1%6?h_^UYB4H ztlq7qsz7Jg9As!T=kxjao;r`@Mtuo)IXEByN0gGND2nP~_b33vZ?sQ0y)HF2*DP#R%iszz+i zPaH-I!q)>aADE}zHMFoLwG}E~=cjOB=v3+SF%byTjwT;Z-J29$T|bb`pnXChExP9L zSEf2(iFWWtgb|Y7L#a2mw6`UPPaEK1J9;5N(CZq1O~tDd85g&uk_pFXDxYy|>1_UD zn@ovq08W=*{_%rgU&8ff!*%#tQV@Tf0ST*vrrE?M4jjDk@~;|?OPJ{1^y;S9E?<;) zUhK+r4lh?DGhgi$#JavSGhvqw@GT^jxDkyy`|58NCz+#cO-mKFAv<-x0Z2}D4ttJ7 zNCdxw4+?d)d=UN78DkUa(s(NEK|~}NO9)b z{wwY|qRw?NGmNZrn%n5@%*Q2eba(vKwQXy&$RbfZH1>$DnQg6xpq#&q6$nKkmN&FE z0P5r6$EXJtBqq}sJl1%7`f6U=v`I~xdEjZ$7tpqrPC)V#k-}${c0CS4nl0?!bo^%~ zpc{Sz=ZZguSD+n^!x2g3R+Sq^3TYyfG5xXdpXgx|Qe1Mnr)o4L$@m(f!xC~swb%gU zi}qZ>|X03agC83mj_-S=}8f29*aArnHrRl4R)pB2fuEJ z(K7Mess19=_KSwMX~G{n{j^xgQu9NX988C#L%X3|;sSP?K7!5`|H%|=#n~sd-q^6e zt+r!dv5VhrPkq>PBeFg={;6qdp}sZiPvkZ#b0fOh&dNOdn^%RwKG;3dh~xu&Au~nM zSX1LHh@C`VRNFV_hUV1RrX%ezd}l(5NMq|a#5uUV!$m!s`7yAI^RW^oz*o7xxh@-6x2p~h%h`Fy^^mR(00IvYqTaQ zH+Uo)esUV^1%C5-tqF3%Yw9R_uI`tNCOK`?)TV@l3(bhzaavwBpo7GNQ0?UG!+=wf z(w+fKx7T?>kle%2-c4fSOJc!?wKtGf$YvXt`<}bgb>tkU=pumfQ3rb9`F!9HqPbaM z{(%c(K?{N-{mj|k_&GMq4L92s`xc$_$@;Q2h2-<%J6vl2r<3_p%N(k@^&i#)9M5y$ zo%Cv3QB(CY!S)YE(Uv$OgV-6@>JJ$_x;~-5cXy-&r+ZA{y4Y?ic6)6892W=qQE-68 z@5;VdQ3_G-7Zdy#6WFbwyLcCI8uD;t8Cb4M6ffcivl&?yz`S?H=k$Z6wX(#OqxpRF zm@k2WTi@W#-*o97!7K)abqHGP4CM}Cjvf4hm(~=z<^}x*?}>yIWs_xq!nLisHB^jF znb2f_TZ`|Z>j7zDakGcSE^bTAk3rzjICN5mvHD4>nvhiMp|ey`zk<%7YC&RERcAVq zffD*l?r`+(2NlhH9fd>cyjI6+BTGtar;ZV+=bDHek`9#ZhW4ib{-lk|D0a?MkeKJS zt)^>s?h|l6tg+rY0vQf^`uZ(XoBsTjR;PIKnPw1^s8?J)C`w+&e|G zp>u-q&=+*|^rwlXH1gzWuaG(kxdq9nF6dg&LpYd7>y1?|P8X*0L-VgJN!+!|wv3_(o=!)*=t@LjCkYswY)PM7AWrK@uHb!nd9 zKKqdONV3Z=Kky>Bg3ygJwqe@;mzsVvImgGq`oT?F=lJRGzw1>q1{(l!g5nuQ7kGGz z5BeGm`)Z|nFD=(ADpPIpwY8Ws8%!pn>r4InMz7fUZIb5+@KyXotG|W*X48CMNYd&H z7bo_oe9A6N9_T*ovduCWgFg&l=B3we`BHY3L}FYdt_Gu~1%aC#umy~0a?SDzZUt#f zMne4@sX*F%gqghUD+O%(|9xr(rWA`4VhVpqDqrl4<=<$2Vwct)_D5jDq)e zpIOv2&e%R5x$;C3c%y#gO83UTufS#$I*!8$_(%9D^tX3H2eiRWa5UB=s~(E6B9~=6 zz!5kKFThq+GgnLYuiio4lrY-3=Q*+9*$1*HHnD0j0_l~;x)?=sVdrC5+P{()+AcVp zHae4gRU1A$<81Ss3K$qunlph?&Cu6MU(+68f*+)$jq1M`*^pe@(?7n42%}o+vCuvb zezQ2`SzvmK3Pbo`m=3@ofPfHc-67*3kM z=i(}8OD4bqQ-$H{UTY*xO+`T1Pxerd*>82W(+dU27xsfCqd!D`9-V`gJU5&QbkZ|s zgkL|fO~1COExMBLu=9n;u$)up7v4k>g5irGrc{_aeEY|3!QopA!b+~o>JiPVhN{qo z4LUvK#nZgXGy6Ef*kduB3=mV?EPdWSppPQODE@EP9ojfK8=Kx&gBsiXqV^nOpSP@| zX^((gEd68^*oMRq@|Z`>AK_s4FdK~}*hs6n#XEIT)b$6^YL2(-aAY=RdQ6j9f)vxp zvkAIV&Rqfyhne|6BisLuDQzumpn@$uj6cBZtcN&xpNj=95Uq{+o37jg@m@aNI=}wz zP;xEtTE!RBgZ{RWYP4Dotq3O+m9r*f=}4v|%ua)U#Y} z^{gkKB zBHmydPH+t_B+4WFhpbJl>&a-ap`p2z#&L8o7f~^|a(lU4ZULh#-c|wYK;o_tdamhD zMSU`o{uMr{%Fnrix^S{WCn_#eQCNR9M#yC{ z5y<_0^Tdv~GZ;p{^rU^#Q%U@3%hVehc@}K_ck6GjM&);)dR|wv3%AeD)lY&U z6qzw|O+G5%2IdWlCVneLNu!(~9EC+y$==rRBu)#llVvwiQy}O9gUzC@+8kLz);TNS z0E-?id|qpB7fs{iGN(imW2?7ul&t}@B+w@`=ZYv}ju_mVKV?HeSkj;&0Ar6b;g_v+ z-;Af9c1K-j1AQSV+0YB;+@4PE@EFSxdtin@{}WN5td8nDF+x#|vGh)2uZA|lO;1+f zBb}H-jygnx{KdwoYV4Y@V+ib84Dx9E1?>a&m2mr;w^UVhK(^m6?w{C<*D^iqgU3(# zQ(WydSK1C2hyk93Q?TGK;2PX&%I0%%G|6C_PnktGOQ>A9jW5KovJ9MuJyD-_0iK>j zi4`akb<29Xl;z1_qgSAWNBD4-9}!v(dia~V*DTs5TLW1p)Zlf?Bf8CZnG7K{w+ts) zM6kTxm(FmEFi1wH{66C1TSP}F@I5g4YFpfGTV zY=@x|v%Y;;THwxO?14QlgwH|QPVsXJ*I&Zf%?+P|rU%SxAwfobL?(0}VO|OPITZwH zTJ2zpy}Hn;T%w6^CDC&GMIxegs~KU#O+XKCHU$;@f3nB441nom5h6Wd-KK4~Wk?7M z%y?wO-jZyGuwlu|LOzKS1$ZDTgP_1WQ<69FdqkGPwQC-)6eouI*X@ufA(Z!=h4b!; z3#G2dzSrme*eKPNevA^thq|SnFxnWCEB-qCP+j|kBI9%4eg^D%6Y~f*$l5s1dBz7q zD24F=NQ$Kjg`xfo_DHB6wU-2TW$m5eCNxmWrbw5SFqqEkCGpfTp%LE|)KvN-nA#Qg z3{khTu?IQ{Ma^35q=2QxAx~pE4E#FaCFx=@>mW^_?S!wtlLeM$4+tcgN>N73SUkET zdKHaR84Q9M8<%_G$(S%_Xdnj_S9YMTCr@V|5tL`6Fe`~os0$Ef2di1rNZMNz0HUzq z*#sX3$AWs-9b0sUmUPV1TSt&|b%R3A`uv$rM*JY4e67$_PW%;tK)s!C8e{N%6v2jb zXrGzGAyyD*JNCoEH}QSsn5`4)@5UP3QxZBKBw@sz>|g^+%3ke|zcR?vP3dp14mH0Y z8h9mN9vw)U&2{N4nZoee?7mmfz_+JdOW*_?VXfDv$(6HSbO;=1(|6 z540dIbKI#oVFg?tCJtbnf`$B@a!vy0OAvh4)Ecj_otI)b$!iFv zM!3FAwn`az+JO6e&kG~DRTTe#SbY5ZFZX5T`<4y8H7_aiw}~)%ME;&F<-AEdG0tfE zF7J2xwbSUBsA#jbe_SX!peJF-+`hjf`-QDQdZUiR!$4aU|DMbXM$*w8YLVSUC;+3Vt2a)7$+Rzog6aS=Z*6;`QsY(nAxsv2)i==;N!c zH|>oyrne&@fPZU?$gz^{r#*8oU9XitZM}%&bA5X&HQJl8T0ntaZ|d~aL@;V9;^w$7 z1Curb4O6v*;ZN2@`_P%iuuJ09*xp6J5i#qjWiNE>q?bSDw}~NWMD+Cqb~vwd)1kPE zDa2*RDk9tu#0cw+3PN7egTuMW^Q20JejWTS4KhsRLGX_{gZkN@k1Pu znt%c3kQUC-cp3Tc{BS08<)b@j`;e}jsUeICW65Mfvo*$|y3b!XzVJKY^&1Hvo*3~=N^!?CNn@>S(^|rs65$l}Nd3A%W28^yXze8Z#R~Vw_m`~w|g~QRQ zncueG&OLrqT?@VXb^h7d$g}nL!kha3{zFO>n!x_*+8X!AKQCsV&CIQ=$!ib=t_>*G zLOKHfA%L-Wuy=KEP-+rq1^C{u`B0B)3$;e5kdbWv?n1>U6v#VdTfd;vmQ}*SOZWie z)swsHp$W*)UP zk}2SgpKz@MgfFvcM1Aa};DydUQ9IdUSG=2m@e1U;S(3!I8}wM0q%NaOB8X#^jC} z-Tt-j`Q1o&OM3?+0dZ}s#}^M3(Q;h!=6Qx41Q_i2bZ9KsYTF!Cx*NN@)8jLCnw^~N zxao1$H0k&E`EAa4&CNeX%UZWe6cDR{+&JgrsM-38j85b_jBk{fgr=vq^A)_qdPjsj3G!+s8UT90_lTP(GNP z&HY%ARW+JS; zz0~~hT&YVWH8t>n{zc2Gah7!$#aJr1#+oM}hsJQ-61a@ZtMsJWb`*?6)v7$Ag>!IT zu6u94c=Khoy%vf#%c20O+0d?Vfk`fLp4)m8!d3=O*Bg-?nd~T9uQ_<1IJ_U9v>+Hs zUcj=}5wKAsd)Jad7U9;5;oZcXZmKACq3e5S`W0&l?U4Va#BrtDc|ztWk~Q4t#P zLilt7VB9gioGKCzmkfehYmGM8qe0Pvg`8t-Poo2kjj{219Q9tXFIM}VTP7kobU%@k zT3T7Oj7d0)e9;CP5I=CYi;X7i>#UoRi<&>;zwrC#^oM4w5RBvP&EjyG}94=W@|1cGywkA;odn-KRzHO?U_|3`rO@UPoj>TH)5U zBQm^br`Euw2GRV-mxi0eL|lbtC3J(Ct)==Jwhs3XV$0LfTmm8F6Ds6jT4Vke9KJFw zDU;Y9ILwznB9XF*`HePI#@y$OytZfGqc65b7#g@P>C=1<2BE+l&tA;Udb0!f9jg4q zOo#)5cI+Z-<2a|3hCkP7GO6KfoPg(GEipB6vYMUjW&+wb2JgX7;Qzq0daQ^tXi3SK z_n?=4Fky70LQ)P=Sfe3u?-Xm)zV;6HoIbF{Pz!JrUedKr^J|`(+(`DCTX(SKKP1m0 z=jFVgW^VM}oZHs6Ft@+tJK};!qedx=W& z!j#fWVpAZ5rJBS>K;Pl~N|{{WZ@HnycH2{uE%&iYaDKyE*}<4y*#Fqo&7p$-|518$ zQWv+#1IDe1M4!z)T)M6Hy7D0v$?v?-Q=XSJv2@8)ID+c47nIcA8{vB(N3edZw0s&} z1r?&6QU9Y1#b2=1J65YjWyJ=#beo_Psz_nn2=aCG1Ym{obZrGeY*u}p7*U=`mmFAPY6`GUf`lWnrPgNb_VhgG11GQMcaeTlgc(Y0HI41rnBz=@?j8wm*twryH| z@D+zCqnzIb-gNCRR62t(+duvNlG{%8o1u4$x7}mWhd_u)i5|GFy$u@$VbjN=Fc^hB zQy1~Xc^7$5)1^u>qO6^gH>X}jdb2+voQ&EA0@8QqCYs_We4PV}ummy;b01;WEL~ju zzY|WftgL)y322wZ6Y3)^>~;yrKjD4y3Q6w1&s2%_IG5H^>j!fO_HD5~v$cD3_F1MD z_@gxuI8|5U;(kd`dbd`}y5zE*59cn_jahko$PsWiRFBVuWCfUdQ6N!?L#nJLBkM+K z2mi}Gxb1vxIm8P znzoeSgf7&k$7V|6luu{kh6oz*C8-cY^C&#`&4Mf9anvg7FA+`~ioDVB5*-$pFgE97 z&DFgfldJ6y{Pz#3S*K%AFhNa0m9AK*-6?rf{ld;JwLtd%{T3rsSbW`qKFt4qOSWEV zP3t1g8VGfO)t_BM=JYDi<|sh$Pd@6d-lTfqQ zQ7Db%h*=!+K}hZc{P17-D>OyN{X_1#rDoI>379~gh&X6AHUKw2RxT} zB~eN`B_g2T_!0OYxqdxY=@36`xKT*GrU;83yQkPt^~y(AXp#Jd6)_ z$uMq@uioDK*l0TLH4-@wy{&%0{aPjOm8__QiXp|4WFakvE6O{`V>=~kl$gEszT%@{ za|krB0g+Mzk%ljRw&3k37Cz$|He$YY2?Mz-`gYHzBiy^+!{IuGFnp5|%a>DOQrPyuFb z9mM9)>sz%8d!QrDmc*WZz9@@H1}wmP7v3N{11cFNxIfu1sReKBltOl}B{utkt=tax z>kG$b{x(L+6+4z8xO9#ez0N6?ZRsgKndmS$H1rt>!dmYB`FSTfUhalx0iA^OA#voE ztD~u5ix?r>JJ=3Hf3rlZH7pKr#V7JB%4aCv>oZ7{MDuD#LvWW;Vxdq>Q=R!Dg9v!p zSo|35*w^I9D-cPy0?uvi6s4#W?eQ#WGC5ong+|jm6;j@4_9hZ}jAB25=6qoq&5gttU2wR)CdXSR4d&cAiac4 zb`A4oi!QJuc|4U;78K~3KKLiqLlY9{tqeGpp@*jc4iBfmTD-xPB()VyiQZpE(>d)O zznjiT`StgoS35aQGfPS>a~jK{v_o@lK*!L9qJi1rG{g7^73Q~0V$Oq=mSg376THJ9 z;QLcoMK^1|9C^-Su(&oltrR$2r74>)d zqq$y+Vw{&%5_Gu1qo$?`PU;f9F|2Hcb@ZW^LU%M`r{wwyjg_^2QIKe7HuystL7PAi z*-EcF?6A(0cUzP8mJt##+BTeoU-M0-{v~UC8kxrw-AghwSgA%^mNE2#GcG5>)3`u6 z!&JuIP*#o5ON9EB-b9^&*&q;e{<~r>eVfXU*g7&YeCVV3m#JIucW@L2H2jlQybgE~ ztk!>JFRww53Kgpb!22IgGp?N0Yl_nXG45_8Fxk!3@6e3f!p|G<)?)HeB}OBI>4SXg z>k5(rqOGa4V_D}x5A?=3_aiF8*$V*Ct#~tT+7UdiMC%_o>C$%;1AFu1`*LzFF$e;? zzNQ@j=Pfn>SD2?-cv_#Yjd?mrFdMr-dAcKR8Xh@2GxsIgkUNu!o0RPKSc5(76}sZ> z=*~gZC4HXr5d1#=3|{Psyezu*Z3^XNfyyejltI#a))O*XWe=&Si=&ng+8}L|>f8W< zjE6FkwL5GOEh5T!**^ut)=WO|CB&}n64nTU8^NYKh^)X2gUbkJP-fbzZS2;z^l_IO zN-&x~AiEe|_sNA(vlc$k$sFNF-Pv~^U=qS09zdo9Pm9v&$Szb4M7!&+R^rK+3dw=_uAd+ zn2GEUWLS(^#PbX(tZ?QaFxV0tWz)H=FX5J1$gpD=GJD%1;+~jLY|nzI6bxcTVd354 zQWr1>Ul!u!S9wyCU1`#2MSFLWii<`NDU9)Am zwk=v4T9Wig%+IG4(LR%-eq!3;qD62h=IJsf6Bo)QirJwV&LW6M{!-**1jkB+jnSg<8h4?KA3FT-_Xm zu&o)I!Pa>U?!)~X_rs(SsODho$8!cVz-ezgvklV$c$d1c8+N->`i^*R($a`;WoD%bqBv~h zC~N_6C?f;CFqh!&5A%WBG)-lQBO)V&twurZWk;O^4^4xY5L;(dqarVEyvhrph1?D= z3FC0ZY!q#!$&i_$uR9=z3_O(ZsQ6VTgrThumaee^N{G^=l4` zc2N+?aq#3OMXp^qWM}-3M5885@%9Z2_uNiXILf)+08M{&l?BzJcd=f07j^cpI0aN` zRkJT`sq<<)GSZ+TZqcr+s!ExM3noD%1W#ER9ZGa0T(_z|5{;_gkp$M}lA%6}l?Frb z^hI;Q3r4GHB7!f&J#+!y=^B+^g?Ci|AGyjZjzfRGbQ2wvH-xbalKfE`R7+$d*4Uw&0?o1WD zx^zDTZx{J|mB|gHFcwvs;l1{d{SA0A!iRwAv!`NP( z0?+i;LwXZ~c_)}-0E78)p;75fFoIKBevo+-ZCZRzwJZLLjaYwWRu~R*$w{3YQsIz@`A#KiDRMOHq zc%mq8FfMt#koK{*uYcfhA|;4K-&Xl?cXXtRz;xlHG|oh^fl0ZAGLyizYoa<38D3yJ^Sj|<9Fi>W?@ z)^P>w?X7>_YhR8zJ{q%)vVU)jX++V?2k-r>FoD$ckb zS@W<{Ut2NRxDvpM8V(0xHD5eWhvAQ0Wd6IT@|U5^sklB7*&(c@9e&?JH93dnHhk<* zJ1UK0Jlx)O6h5)$xDo?xTSl$_0m3kVR$c_xt94QK6M zJn2@e`;Nv+Cam=kgZRMszO>a|c!Qmqd6%*s)|U}!Y7UIh7T_Hm@RWa8xTLB(-4x_R z&b~M2;{;XOJM|PdD-^xXtd&P-YITJd;sF_lr4}Qm#(9$9-Yk*qo~c9|vkc(CpFD#L z=7*=_oLoge6P+S9S?+lJzmx-$kLHP`BZcuuFC~drO0bu>s!2k``uNOsz)yl)L&jP{ zN31r}$*(=ik}=VG#>OS#&09qsnY9Hu5=_E_j`#g*t@S;Ccd|ES`?YMmOy%R!kl7`E zhz|V_mB>sbv-q>ulpI~sI%N=b3p0QeeauN;$5XoUg-G2A!Y7aM^*pAe=mY9O zpz5LHU27(Y2sT4A%o*Q-plytyQ_Ov2;vL=INTzobwI`JBl2vAqO+zGdS}>;9bb8Qg zdbQ6NJA=Xj-@!8onCi~aADi6#O8hfPWq@*qA?jJS5LQID2-tDu771lOGg%IAF`?3L zCz&OS*|1?kwm6_TvL%O0a*LA<=WuQr4_jO?%3Ja{Lbl{XS+b=7QNCPdB!tBNwcu9YdFlm2{gp+xh`|woC;4g#xTb+YaZ6?eLkTP_iE+oi;Pbt>{`#W@m{; zPy)k1WP%Ya2rD=ftda<<5h4iapLYa-TR#VL_ii>>ZbeWSN#Rm^TQ;QWCgEGFEEaVM3?5FI4+g1*{T6bM>^JO31g@FnDRAIPS} zu||?D!_(Y!w@dr|HKdO< z%yH9yJ#5WwvEU!~Y~Ot|<~$m0v7n{Mx#*TE@aR9Z7jB)$(`a@&Wybq{74-aWdL5FU zTGrLkMw5nfG>;<-$Y3~yq*C9sFaRMaLV%6|*{mp|CeAP$MxUljz$CLz_(MdNGy2pC zB8JAKSSJy<`6T0y;XRx=hZq(+q6wCQqWX~s<{E8hWvfC#AbN9vhPSzdU1;MP@JsmJ zm&w|i-#4T(m3ZA;l@g+QRT>X2*UsZGIGWVtm4}z7cJuFSZB;m#1AnvbegHEIVyy0B zwc<WNXbG)i;+@Gtu5=?qd31PSy689{i9J97Aj~B!%6#{Je zi_3Et?f$H!mICmjW@DPhx|$N#sM+%6YdxhuYSPEvJRd|R=+i9cMg z+T{UHLQm0COvh@08`4U~K*d z3vu9EJ&VsSMQ}|cTzeJVRR*aGYg?7}(ANTR=nwbH;pVp3V>X+M-`i^K>uw-of9@J! zD@#ASP}XYf`M*1*6f4YZT<*Be=!>7X0SA!=teZ$h<2g+hF6hFv7Ud6FlgFtl%Q~)qT724S+o{|C zICt(0{XTxaTm1rXinh?0g)t?ufZ9|>xqtgO1XB8caVHf|FofV6VEVq`OgY6I(Gi3u zCaG2E1X9(a3uscS(vAsNi;hj+czHVn(bdR{Q#CLED4+?bfhJwVX$Z!n4_Ag`#)WW*Q;8?DtXEfvMAGdZBJm#S64Hln} zhfsne!H|)mnbRsIp@>tD{n15*_0sczbzUW;@?GO>XlZgnrys*6`3>C1038?N|7Ay! z6rrpEdj0$J{dWhGUHNKcaBv9ZDMs$Re>Xqc^KIhm?M@)@WW9)iz3dP}P1twx9+|{N zBe0%CAg~f-S+TRM>6_+BOy6xu70BVH%~d2D&m}FIwasE07t)Qp>DKI4r6qnn2!L(D z1n~x7GlABDEklhMh=2k?zz0B~UV77ObAg17XknVHH~&PGV5c8~D~V&Xm0~9HL4H%hsR%f z@}iaHZ+73{PK}suZw3Ap${sTRPn&JNks_XlFa|2cHx6^_rbvTckJR7&14e(xwe-$O7AU& zkjC(Vg>}jtOj+p^f1AeC4fHRo$mO@TTs2$pqotN#49Of-R%??Qj>AUpcnk1-7BJAy z`Yi#QePkl6Sc;fDKS%_8t~Tz*Rus`lv{3-LcC05f3b|A|Nt#5qD|U~x>yn;A@6)P; z!S@r#fM5t5+cuO*9Lu1n$~5B=ZSxG%oblAM<~}%otNv58+A8;1B56GL3E#6k=eY~~ z&G{pc-8|JQr~EqEF6mN{XfxR(#stRb2(b)G`X6<&TIJe}MCdeAn8rnF=;~Uh+7fNI zI**i^9JVd9_HXNxAEDXU_NpMi`Ct_&*0xI>aYutX;-7mi^&bzTfI@8I4i`0shQ*c@^*iOusXkesEo04K-`9 z>{T*ky1Tk&Oc|LfDHu-~lKE0$^llt<@71 z^NyxSTjvS#1Ya0cS4L_G@>FGUi>~>!n>vqq|4cY$G58Az(5Sl;ZBtSrlUoSH)=5h4oBvq4sHM#?LJuIfE513YEj zNoCcjxg`cu4%fmPGIWHR%tR0;h*SNxSS#zIDwei+v!4v8b|%w>M;NjZa{qmFEBE@C zkjzK#5-Wn3!ePT=LSTxQX&EAU?S8Yu^L~HAr2?H>@mKyd| zmwV&PbwUs@x7oRMc`Jp(vs#Q{_}`MbO^5 zZ2^Lg!74UUrc|cbrC6+NaB~()g1;ha4qBVUBgEn@$rUR}fq!lvgoBR&-^xt=7l<+a z%XLMC_4Cr!!*y!wC2w4XP+K@wRY;K91t{?$P$W@9QLr3LY!8cs^!XztMz*|q5cX>W z8x+Pf<4JCB$cjtI;HuoSpl;j;fnD@#kP(+_#AYA;@Kna*Xn{8sw?zwv1E549oxg-k z|1DC1NHc%%b(1zgOW~rrpAw_@aW+FkO98P4&_Znr(SzZTl}u|Ef^gTJkbn5$N-^tV z-vtnLe8Yci%@T6PQ(Z7*2NuJ;5v-z0RaOwoVAd6a3r*rd; zSyNOiUwmKmMZLYe%W?m9<5x2%=oJ2WKkHpDOTOQXWxiUT<}TOX`c872*=prJzNY*W z5#@F_dHT4nRFsTT?$H@t$(OWgG&NF}jLhHOV>?*lk4g7X z%9X%UNNMr!BW%~DIZ!pe+F^`K%loP8QYKWkG2Q9!+-a28PjwCt+AX%brCnXuA6t|y zc>354OGWo?BfaUuuH6ftxC{dZsU)=axF=@^hT>k<$^hQs^#!af%MP^VU@#54Fo#^U zn(LKI`SzDr(=g?R!W1!9dALTu(~VMxb&QZtPezT4NrezBJ(@ILJ|dH!G|E->29M}4 z`4Zjt`;LNe$A0|8qsi?)m@3{Y-Rf!Ku4*Uoj9mZA$$F*2SkE4+G)CJYyXU&Ax33ZM zeLS7lI4SnR9Wb-|&>7=ZXP#Zv>iDMxkH>n@WXw< zDq(B4=P!sk!SYRAj{4~`g(=!N%3mldn)3`B=+WMePSq%b@C#J{$RcloM!5Rq9Wdpl z&YG5UsJ@7+IyPqnvl9{e$dZF9WaUM0mXbRj3tO*TPi#Eh(Yt3Ce=k~DqKWLM_j=z# zt@sZo^M?$d;*8B$@bXL33C+gX+|y1g)_4M8%Dbfw>F71!?5;}s>Ye(O=2Nje;38=! zz+-7`aM!6o>dhng9Ci=0>E&5rn=HMMzIyhhCiF9vCc@KZZQ&m?HM(eM`uX}6p@9{FaK1+kE`ihT1Ugb3DH1op^=%!MPmn7Vd9e$UO{pKBc=77C$i{H*cuQjf-@}tFy7TIG z2i7K{k@qF=BFn8RNvG`^Cobb-#dN=OaG>`oFPid(;d31S^NhMGrI8XbA} zFSeLPv@*i4>f*Lo1GVL{Jf-rTM(&#r=GHXff^i9>UzIdR+&6lxwCee>srbj+jinAj zjtg$0MSP@|D1C~KWl$h_ow#p;}w#vjF0At`k)zswLn~J+n}yd2~qlAe%wu1- zO>uvh{gP_dO<<{1-Pi#4_o1nXoAcg|-p6mZ#HTWJYv z3U=>;@ZG0MQ}CXKv$`X;8QR^Ai6ck|f*_Df7}H+BN7(P!JGp+p9|GrD0Dd;+wOc0b zenoA9@g;{5ZUG&2X4%$UOO*NXe%9p(as=&*O?J(+%Vh0K9rC$oY{~-PR zfm2d_-#!k-TbHb#p39PoRbHhQCpP@S9oJ8Q1u{H?E`F6zO^5d8IQ$<}L;A7={?yR3juYnRLyvRn%(CbnUu@)+@%#6-i^` zlzY-NqZMj2E8dsn>xRRJfn@nxRD)7EuU^8t}&SMBs+wM}Xy97~noWk=LSg8FT zi3GPy=c)`6^Uwr({Fo8*MB{wro==>lI{6Hqyz^wE4fSNWPEt)u8CkKNB%qhn^@%pj zvE8#aLv&RQJhB9xG)6WYM;DK=Jn;Q61>fW2c0bjqC%lXHN*&=N=cEywj|)^N+|-y!plamG%eDivI|2rBWJ5;K7mzUe%2%;sDqs8nd8A~EH~B5+^GY0;QONYE%F1`S9fLqqv# zYLx8mWVyHu+N=j&{7B~KSn^QpvX*p=Xo($mXkErN6kjE9#bJE&2}%K)j_ydz^oUq^ za0miW@XRpkv{c7Hk0VL%@ml{Tge}@8n5J??(qtO8&8pr1SnI}O9swzFMh!`+Zdg&~ zPT8L}t##tL>PO&l6(NQ5^m0s9dF*rEa`htSMJtxeV5qWgMG<9LCP%V+bd1cpJdL18 z&^lpFdQ&G$pD`7TE3W!LmS?B^eKeo=kyNncQpS@H)8=5xx2|wzL*E?uZNi>LJ(KuH zS*?#Ch;Xiyp11*4OK^gky2n3ZjN=zxZ8#wcnUiynNJ%l*tfT(Yt&W2>E4w=^tcgF& zs)NRf(EJDNdXsu@vWXsuF~gKX8{q&9BUw9@b2=M~mg;4N22LM&xh30(3%2Sx@xZi# zJ8TW4`!Brc_cTGAofK6(8Ybw5kvgLg$Hav#kRmE{tdh&eZF``s>n@@N0Wp8Ng)HM6J}#XV-7UUeOe}@@^pfq19V{`d{M?oeGWJ@ zyda=bd(PL4#-zM(MgBdPZJZWN)}kI>?&;kK+VA^iimm+XVSqqr!MHunf@l_(OdWwW z(GY0nl-6Vm#`;N6?n$s1%XE`ZVt!A=V_vdWT;YiADj3Q%)p|Ry{d+-DR_tV}4sHJ% z6Yzm6OVk{HewQ?YD27(tB!+8`Skm3I89B4#(%XcO8j#$)C#&~YG=a{-VcN51 z6G487jEpyL5k2`uBRHS-yvco6=2|J&&e*xKtjpHIL|P0&;_=lq>L$WNF%D$G{dfT! zGr6e6Zim34S%Ug)`q8?Dsae>Bcp16et;9cUNcw(d?!8Hm(A2~Pb!xM_x=o}*B0~5w z^24^*pd&8Rb}NAI^=0Bt=vRLk`EwUS&;kBLuPkg(yZ4pHQB@e8RU2lkVk+sL^A%)$ z1Vm)##BDqAfO4}PPZ7eEe_I^-^7`5zWu#|#_N?bU9f+AGXgNPKzl)D#&O`H$i?=%i zocx+yh|8L^VB{w>Tt-m}FG;i2EyKx)&IV3#mX10~g`iOG=wlWIztdsL)9_?a{Jr(TcN49T6d+O3* zpM-1L8BDwq_F{jlZE*5>QAW^JGt=FLgTkEeNgWM-|CA+$`F%kI%iG3DxQb~ zK0BB8KPPZ;r4e8dTY~<($sx59{n>fn{L6+wHqm54G1B5RrQ1~XLWA~du-fXqgUn)W z>vsLa`fMk5OY{)7L@FvNAGWeK+>4`$a09$8dq^g-EC#I_woT(qw_N+Mv5ZQwIO5ze zsVz#g2x2^H(>B^pgDC%%F?=}*kIQC0l$z{=c;Wj7%mVYRotc%DMD|At1qf)@=XPA6 z+RKHOj%LgO{Wm`7Bj7E!5w99v7f z5_fDm|BqiPo4UVv3i>qDi~Y z1T#k)NrS9blkO^lr#N5v9{R+sb39d1jlEwxv;g zJnPy3kXOC6RnhyiL722! zc6C9MwY6kdTnwprX=#$^xE6fSvu|Lb))#RoLawmEx4k085)DzHOp-eDv zP9T<|ckuN==UQ?QQtmipma6n$V*NGvVYg;1`2 z#8a;x+lpyD?GLjU9WOfVT}Xx*m5#;7OZPc^TM-A zb;c}g-Sb^9t~F8g@j$l(V@U+1PE(09GfPZ)UTe1b57;?~85*&=L&@q!Gqu?(y*Kq+ zG8C|as(?b1y|ZTvd(RL&PFQKUQXsd9bm(0o z+qfSv#Ut>kfKpplZ*<#7;KS3yqzR&2Y`V4rBpwpbGLvet8?@q}@XsZQg4&f*v*R`k zV$s3e7LZgau5Y}q8=PGrIAwv*OZqef_t15ONK8c0E(#@^t<|@b4s&z}NyuhU;tmgD z(-C!1imQj$3Gu_)X`#EO>ATy>Y_fP2o2b5)ifP&^`fONOE@%s;hb6MR=*JRr z8Bo7g(&t{*E%9GMwLgevrkDQIT3P_79G&gi##;BumtOJP7gunl(X1VYc&f$kOn=k& z^*OhMsEDE!XIasd_T?oj5Qf2)8KW=Q{Qc=r>={@;bNb+e~f0>{_AC z3L(QNmjbZzp}x%YGP%F-UypHj!3e8i= zN4Ft@p+(PkjZ4W_3KC@TMOsHaTggS*|FXC4_FXlVTY29Q=4OW^;wm3Qr{+v)D5zEV zAvo!*ex}Z%kkZ=yOgrh$e-M)*NvWmyhj=+?L5oca?IQd_V>U#3PE|zM9*cg@fdQYJ zq?O@$_TC>UTtkLjs8dH~H9TkYBF}v#J|(KVg>p~5l<3vd3no8#a_$kmC=Q%M;yHxJ zUJ%c0sgv8IIkKt!t2Z7s!5q^%Cr#4PYSnHgG0Y*g)^0FYIQ-0}ozBlAFZbbB##v}$ zU<9&WBJu_wIC8jJQ6{Z8&Qn!vb$aPt-kSRQPYWgrBbzUcB!OQ*@4yc;VGL33z%pDu zMb#ALA>3~A%DY2`M$Ss{ifwOK%)|ru_SLPkYVdt-9Ttw~ut`!_PVi_X$9%?avp}G! zIjR&A*v5%G7VyAMPA5E^Bj{TV9)i5W)J&-;bHEKb^~T%orI5*fm$vOlIa={N-PCqx zq+9-Bm|J$ZBXocNymw!8VUIPU_xK@u)b z3l1B_#1B}iR)t0s04LrX@$A!*amBcyJ9uZ@B<+;%orGI?F;yGSTxGBY%E$nq1_nuq zi$_P=*waDXGtdGhgiPRr=;7UQvF)ZiAqqgYgaK`}NhJ`TL<(2pcR=4h)mBgiGX3-0 z(;xE)%6KFSE?_MBV!!P*Se+g23wyeGI;6`gm0h%V^;^uK?H7lagp}8boS?G`3IPPo~7^oyBru01d`ht z^y9*+4_&qq^rJI1fPJ|f^rXqW1-ssXx+3nu?0F^ma6(f}ItqI5PvnQ1gcSOwD&db_ zCq@#0exVe4V=WPgDKPpc0C~;;@{ZHd5AjnkG}RvXg^{=ix`!C_N!;><#@i2elA%ci zbkM#oij4Eqk{yuHAA$e}G^3Lmkay}f|K*zg`0hF#ND*UBxWRSHMF&V{^VYLjKB&R|xZ)HGT`(yMdqQLQGU zX59|BVy}Is-}QlI5?ptL}C!X(8#!@=eN0POT;?(ka!P(R;E|?|1KSSDmM^ zFz%^S22PlgJ>6^2JPVH_kXtMzRH89p?{C!_`|ReV>J_q7U9m~s`t11Ckn-WS(Kb>w)SRgKr@PO<=t~>(I|^@-$Qpenh;>L%B;|W zW4qxlWItbLjjyDEVBwFvZ#K~1A5DrdmK2N>3#ua zyfI$QDZ)90Nfr+Yi;F~hOWZ{R?%v@AsH(0j^W4u+tkL+AWNM|f zM2&j@M)CCX$ehzdl^@zW(EFFE#M=h|w>&f71pF%5bOY07JEJ~8(~!_qfj9RrkgF{C zzxQ~&iK`4=AVm3g#ua@8h389U;Y`ZNq%)L@o!bOE-3EXLL+c=L6u~Fg;ddO3SOMvckM3Flg0|} zpA?y1)Y&86{|=u~Ir$TPFYe0ki-r(!xJ_}*-p_?kxD(iQhK~>oIvK1Ma?4F4Ivas< zZJKs*8YwfR{9BuU#&sX3+XH`3Eiklfqe@};>iNJJjU%Or_XZ&h@#e7|RVeo$ z9)sFA`4k}X747f%;qD+{zC=w#^oPJQEbY%O^oJ6Q&LIPL#rN!Q-#~RYATD9XN+0|{ zer^ZzR)RT)ySG)-n%GI?CwT@Z+6Y1-^}1vka!)+vp&2N}jACL;*LOYXkuc5SqBGrM z)CW4M$>&Z<%}HIX*)4mIM~i$?@e+>X^JRS=y&(vjEpe?bfT$Ct5-~_3__y_h7Oj)@;d<| zSg6TVB(&BD2KjDzyzTbA%Q^N>EVt1Lso|GRInA26Vmddx|9EeSNlGJ|hofFwu~YN! zCrbx`AFB>lPy6~BEB{n&xNPd9vd`?YW2lYWH^q_q7*H9|vng~G+s*ZIfVnoLDzbUc zbx=Z5hpMPKc^wql{E;KV&pW}=8jBb6`Fj!)(HFjVz4xo$emINY1}uT!{h4D@#x^eN z?-)(f0rhp=lvzR2qE?Yroz-p_`)x`;EaR9bD~=QG~ZalQpb<5T^tmL z560_*Mz5vYk8+VkVk6+ZkVoOGLu>N}QudM=w%ofWBEIiePqQm>RLldt7qL}EVy1BNKtRJai91L=!HaNAVGhlJKKw}}ECJwD6$ng*G7iCzDbu-MncH$^v0%=XuGW3bY{*FxI9q&k|d%lNQaaBqUH#jxN zd^(jDR$$t+TY;}b&6^Vu2GfJWu^sG$^a_=SZ5M$8kCf69z2{E-CQbU5{3CO$Cwky)4W^ojgtDH_km zvVd3)^u0Ij^iV2bn%uSE07-z-kpS=Wy3C}9YowEzI3)X!? zfn^@u{Fo*0{e@-tLP@!1qlt!@!G)b%i0C{YXg)Ko1m9r@o~0xE{kdxxCB^A9q{zOp zqQ%#U6X0D8vXBnvi>QHU^Wa7^|BeYSKw`v_?A_ioMwHyRWil;q*Q@?$ke-~jX4q5? zeZe$>Y}Pt!Lv*Y#qkTzOW;|F(g0wd*gS~Am#{8fA@E&?IO&ZcBpGXem;kMvY*m*|<{2gE`V zT~+!_l~6NvXvnDOGTJ*BTj?16C8aD!3fy;eI%$cl_w`D)4}2t~r^M_~g_vcI?u~2@{fyijV20jyz!igAD8`spy!;_FpqDD({OSb!|$q){R zWo}FEouFNqqu!_Ms^2G{{x>OkX6#*0l;DuG!aRfuNQWD|H|{NSKo(7R+mU*`1vda| zLw}6^2{^mqF3myI`Ba?$+WRIWpgIf;6vp?{9TSs4Lzt&I3oVh&Ly4R^m7}S z0|f{0;5SbY`P&mSOI^zVQMLMURYIe$^6zIo^EBKO3)Q?Mk3}%= zvdduCD_Aq0*ii@>TeAFcR8X~`3*Y9J*-(M(2&Ul`aIY!5PQqLhEtx{MnqW|r z1cJGPYpTB44lI}|2GMW8B6WG6iG>Z>m$+Kj6Ei+DUKgF&mq?)amWnG zbs1@D>jT>Ne7XL`D%K?#MqIT~)|b5k&% zp=&!i{*_X@0926tyKJ6-f>1fi}cl!Y7I@sVHD!C|1&I#~ek&b7md5b%4v^DTtbOX_ASEv%jX^#l7 zbX`6>7|9{a-(&=%+W6O)zxo0i}&w<(-;dlY(42sSh1lTFJ zsZG2urBm)-J19QKRORv6YPMOMB?oeUI>Wtf!UtquEYC;g!Yft5q9}85ZIBZH5oF{H%)Ly+|bKJ|)&p1g5NTtxjZ6#8E zh?(}&Y@;-qLpyBd-Q>!LA}QRPR#g4C##A@!U0!@i-iagiy^KdP>a#OPWE`d7jP-Qs zBsyq%l){Uiu@m%P(L*Tjb~JKzuFCYTt?)%gfE1QgjtalIbZA`Mo$<5 zZaM&;Xc0-3-Z=REc(fZ$@zpf?e?9(1uGcH23;K(MtC!xxmMqIlUp#WbYA^LEGD7DWhh6Xvy}|$cnJbxTMIg2a5!9SyC*4i z(9-H;9uFuj-|3albI$Mc69K(vh0z}q(p{lZj{3k*a}bKwz$4rsC$11zn@3QWWEUm1p||K#GbX{)Ten<`vXfK7?XCmq!FR&6KYwmAMXbMYK!Z+fzegOwBh ze%e8!Qiea#G#{)>9@l6@Y}1c!;+h!VTy2ocQ3B4 zY5gsD(oz4e zzlO8}#mt2f?bhiGC?UUCLJ|JF{WJQ)(^&XRculqEcdfmlpE8>u<5cNI#&1YPNB3{G zcYRe~aBRPY8}sobu(Y}ygvIAy$)P@YrHC!=OQ(t4F~rBQHw8Rl{4}w6&!-)D9!YFSW)YlL-K*NKk(yfIkeu4az zauVYp`VD2*AA?CAfR}d!!d%K(Ja||7?bqIL?um@-Qh_flJq_!18srw zz=rd_fc>t5_7z6-%h4&J;@0adz$?ET9e+J&j4Vtx@*BettHF{Mrv?!NqF1T6mpeDE z6S=B{X_H!Z==Tn}F0HjVsXrXEuZi{+rX>B{O}vtX#>YqZT;X%5lgC31C8*j@u(VhP?Z95?Q6uueVJw)pP5^A$%;b~aidL8G=08iO-4 zvb^&C$SC}1qv)F9pniv3;$G%iY;pgRcTZCC%nRio*dTYckeXhUQ?V|7drm7JW0uM> zzst(61$YDv@U-)}=M{l|hP1alAv!$1A>y8!)o@ewvh-M6-sYK4mLE?o?}$7^)9e>! zwT8#w*yJHoiHS*tDT}V8vWJnTvZ)D&B^Dvzt(~0%AZwBPzZZmfoC2DpNIT!7?9UI> zy$w<-4136gdYdD{@B|E08Fg(_8!K{XykXSl$vBd439QTL1G;24d@L%eSa_*=ak_h; zn3H3|TMM%^hUVncsHjO4^YGE!!W1=2YoH}qr7E+fk?}Hcdq?0sLK*1Y=M}c|(TJW& zU^(g1>;3zRFRsg_?a?p!iHy>jkV_3Q*yz37anj~~gz^WmJ(UjHK zx9z9f2fw&uwjMNBzn4|pa5}ijoA}Br{pHm=ZdUis6Z-FpY95}F-mO0mI!Gy#f73#2 zTf&!$*Tik;QXQfdLe(ZO?%0wtyK26W;GjtsNG2p1jK)*`V(4`@B>sh;1c<%I1udw* zJTrdKdBTt^5|KE-Ucn)b9S(=sIrwt-_e-O{3>3Bh^DO3!U*UtISpxnHbeEM84#lvgFjksm(GVf3Z8sD~e!slIqX4$%!_>`IMEYJo!){hNqaX z+3#eGc|?evWjn`F%-pyu^V9mF7%iF}C1`%kn_$Vxvhj9ch1XQWZruqkCW1k;ASE7p z*ZZd_`s9+8@ttbnOKCcIcYD9C3wh`(DAWL(_3BZ;JwX^D2GiMZbZg*yAbl#uDH*w{ z4{0+SVxlcrhnzU<#<{HmqpPm{yL%`em+Qi`o?>2pI&~PCiJiJ%&Axe(OdX zsEgjba8L#?b#k~XDY6x&`MT||gPM$90qm^!tWI+xqN$QO%52gR<-!Ts%`@V@0;eAHuiLSdo3YWonTD-&_omAd`ai_dbRUJ%leElvf;S>BjcX`I+ zsERwKLqbDHXP!;`jvMMgu-%c^@%#wep@yhdNO&hEThgI-yXFM+oJiIj!I4hcC zFoZ;Lg1Bb=mzMwJ&P;;<1UCjGJNMjvpK=aXbZ93F5aGm~Iztvtq#5eHV)#eBlDuro zCV6LsZi5RX?GR|s-xdF0jbeqILh?))N7yiG3e8ZbYYqnON2j^dwt^EcQo^5|3T|)M zj{530Tj!ReKl;Nj`D^KbJFK?n-t7+UNfzH)5K8W5g;d?gjn?M!B#xlA@~C%8TmNvv zZ>Llyp@J}vNu}{S;_XKmRSh^YhpHvICv2WZ;9RifWTR~f@s3w8lEc(TQn2$BiS26y z11prAWy&EJ`WBclUaDae5Gjvl>;u<2BxLvbu{8&hv+EgP-$;40) z2-fMsb1i!~R9)HAAL~iA{bm=1H}Rwu8iacN5ZY@5u1aZ|YCq5kBx5R(%G+u>0Qkek zFPxns;w^+?aDdn-mO~KR5x0yViA+E+ALBjb?UR$08W5Wux^INwZc!UFNZ8fXIRRZx zO&Lozb2L@Wfw7>?csC~oO1t(+eQEx*8fyi0Q;a$8ET5+(X>Jec;-GBc=+pj^6iFu_l|pzl+<9%42Ubhg6Str-%q40?n-he8QdER4P$X+FDeD&lbwp$pZPA_kcNr zn{VA$)RWmZ3o0=ih6__R$-g)(&FJcyO0Tf8OrX^8fJ?;*KR;yCz!}crn_Ji|e$vZ~ z7MK%n22%verm=Ufs|ZBpCSt6gse`Rgi^OPh?PCkVNdM<%xub!<1RM8B`?JLqJC4fs znT0RUh^TWc$#6`_i#*~vGz=aEkDL6L9JCH|8oD+gft^56nJM~ixjO7hJ3Kty@zju8 zwDB)-{S$aIh6-jDjT8|}%*_OM$%OwYdZCp{1co!$jaZ~!gX3%yY z6Oshsa;18l1xRuPFb4ZTEcVG%9tH?Yv106A;k$v999diHW@vt{$m{=cM~0dl!Djdt zqMKy`%M79_u%NWt7qw2bH=E7()p$5B5)b6Yk95^w(667;=M`0U1|SU6@h;5$NO7d{Zzr!r26-QTDrn@>q^Rl%_IrbhnlgJ)BY=gH%sa^5%Bv{a zsbjM?4)<(p*y+eg!EKCBo#or>*X2+!qs)-{=KJ&9(nnj&C#>Bn)!A0H;nU((3Lmy# z7?aVl8$zF+u_(pSoUsu=;8~;o;-UB83h;EhXjNg)CN@*lB`!x~HZ}_5eBpTEHUm)f z?xa;Rz(3hKc6TsSBJj(UBr#$;Qgo=%_j14_4JrxD8DbZ+v<1d zwD&9u74Y9R%>R`Ra4Xik1lAuBcW{;V{7>@ngeN$#Zz(|R*G<0U)>))3AG0g5O}vUG znJLg(^3osF0%kQYyq|67A`{)n*_eVHc3>cbT5040H&}CqQR40tL;AGL9~pT#inEwB zaoK7m3+I}F8@Hfip3^~*kPqer*ksO-Pp@DMJYk@Px{0qPZnPyS(ogFVcs84wG}0}@ zymJn>^=`PpVxMr>n`X{@_hqcD+E25PPt|BNP4{;1aCX~?N8i}4uk0)}p!#W1uk_LG zZYx2NQ1j_52A;#kiFUbeq}bmg1G0np9Gfl)6k- zx?e+;QczO%gvAyz;h;EE`OgUNb;gcybY_ojx>jtvs}w8Cqq3?LE4rE#C7ANIcwzie z(mIhU((cVpjDPIFxxF>$U7Xks9-Zew1czFuC~RR2qsLEE$*hA~J~U5TaTk(+GU$eVha2$GhG%9>i`$7038qH-Tg^;472ZqUJju z|Fy8Bnw;-@W~|F&xN(JnBGmNyce8s*%X*_hirDm`5;;_X8q0Ii`Y8purhL+bF9fNk zcZ=apTh^c@gJsWnZ%9RrJdpF~T@t{oeTVewuP}|31tAAj+-RJMOzbey^aqi?1UE`r zo90VjXiO14CbarvRl_I&LgRF7BW!+k8xp(Tb6ijA{SFrB<(@{L5ZK(stL6HC08c=$zb!3_4YyJ)c4qT!nXW{l zE0ZziVwpNXIEa9W$F#H>r6o%#7andFeA1;|%Vtw0VPO&~l`v_kY;iW0_Q}j_DgI)ae=b4fnJER$K9d2)$?K?6g&u4}?Mmr=RL0JaxFCEBw^(t#Ww4wdzfnD8~ zb*X(HsMlP7J7k{z>sR}^Kze6nJ1cl&AcDYRO>^8i(3&(m%QlhPGB8fzpib?bp44M(*XET zzoO+i<)8d3#@-ymx4i83;(!F36O^M?1UkjzEB{Rd?wsBxv}OUih1MzPwwM8z*DNIT0c#w&8|Z+CN;FR>EMCePu?XyB_L;lZc$2A)kpY zshiDC`E4EHlf`kex6{|$H#ZYZywbeL>{N$?XP;E39u7%_Dk?nGwET72_Q~WJ$m6i< z{YH9Fw7?}GKZ?fWCj?Jk`Zn1o7zv4!(miYn3oBznHQu18-LPA+^tVp~GcT$|snp#n z!{ZXG7+zjtMlMxMjjNz|xnxApXk`?2&q=kY)W>ZuVW@xY}@Lc|KzksthY?jH6|U zUj%`wJTKeDx2jkQYP3cfbtJzxB+)0wAXRT>mDz||-8W-K4XBN)2cBO_EErY%T%b%$ ze&x@K_OBi=pQgF-%Jh~SlQ>T+;afk(0%B4Ys#wXfhdCUS>!jZDWK^n!jj21Q1_#j2 zB?Xi7&(h54CeIXmEiH#-*u9nr>l{mWdWR29L~mYx46!=FEvI>pYm9?fhW3gb6Y1RP)ATs9fcFtZHtMfs=3!;A^$aS^sO(+I7xx_2&gY*i!-;v4l@JcRWU{R^qirQeGJ-WwL_$dsy7;E|z~k>%kV#-)(e- z71;k^h8gTz#*FeQ3%-NrO%_n*#DCDW8@2w$h3W6L;VzDp{+q09&LtwkH+)w(*$0`T zP~-_rN|;s_#e7h1k3MY-?!U|g6T|!aSlraEU&JmT#tBh0G4lR77hQIia7>Fv_V=?g zGTK?b%uQ^6z;7iY)kCRejOeIl2~uyTO6ELlZoY z5&eDLl|yIFfKKiD$86F`tPwdao%HY?%)R)X{^g@ZybelvQY>? zera&^RONJh?e(*>967Ax>b#K`oyePRnn2bEr&`$BMwrB-fKyxDYvi4@2KOtgwmKz+ zCI*#mmXf>$+FNXoeIE@V{#h_JtbPCP{ri1|Gc#Ao5WgCxx>L1e~I5MmWVHZ`ZF!6nJIB+5io328$xq0xB z`zmoExdu5DLE&jD`NC^{F~k8b_dJ1nrlpqtP!0a{7R-%Dw{>2^Gt&ptocW>lGKf9N zDfMsC&OxNQHYMfo8N-2ja9v?HnS04yVV{_LVPD{L+)H#)J#Q7wMN4wG9q71Tq%*OZ5!tZN^7_=zCc;19oN0Pn_dahu+lnwoePs z?qBeQ$uRU*kT7UB$JAWbVPd`|ksnwc8Iw%9QV=%&vLejpN=P!O-*j5_vlwPwcO}$E z+JxIqasU+cxmmmWxwxp?+8hudHYEDuf`(4}XGOYNvpOvLJm5c%bzLPOh>SL}V4(4_ znZ!H$hczz#cxxB0b-T`9j)vCGVQ?$;-j3O$@$UXJ<&Q^RSPvOK_(_j&<{eQ36R*R7 zHTP)e?o;3Q>$tYk@2LBc!|o<;P!xwKq@Ib2hP`*luSwe7)XXnGpmTN&hELP&iHxsi zW^09Z&`qkSVakGsBxSmrM)Zafb)&U>Bb#Du)0L z;UEs<3;0qNbR(xtN!)q8ykkT9rz&_zpw(({cflmO) zpoBU*2Qx~S=kX_r!nje7pJQ%%kb}i`u!}41wK2-a#uBN&A78{7;4D}lC7AO0w@HrZ z5o*%qP?D}gG~(y>Q9d8azMqkJ;&mCUM_l!OaSt)j7iSabLnJ1}W+*1D6sIn4ZOdFy zY7yTHP))!9Xtar}$REBA#|yk+HBFXjhDw!NW|{d+iY*cu�zBE;A6`RoYnYVbGchb9$^iIU2 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 literal 0 HcmV?d00001 diff --git a/assets/inter-roman-vietnamese.14ce25a6.woff2 b/assets/inter-roman-vietnamese.14ce25a6.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..5a9f9cb9ca0cd78b6ea2f3e5c9d2838dc8895598 GIT binary patch literal 8492 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 a!Vc~9U7SGB8$q3Ii#9CnAx#qS0RRAsKuhfa literal 0 HcmV?d00001 diff --git "a/assets/java_base_JDK1.8HashMap\346\272\220\347\240\201\345\255\246\344\271\240.md.9d40a87c.js" "b/assets/java_base_JDK1.8HashMap\346\272\220\347\240\201\345\255\246\344\271\240.md.9d40a87c.js" new file mode 100644 index 000000000..cbfe2482f --- /dev/null +++ "b/assets/java_base_JDK1.8HashMap\346\272\220\347\240\201\345\255\246\344\271\240.md.9d40a87c.js" @@ -0,0 +1,569 @@ +import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.dc1f4608.js";const D=JSON.parse('{"title":"JDK1.8 HashMap源码学习","description":"","frontmatter":{},"headers":[],"relativePath":"java/base/JDK1.8HashMap源码学习.md","filePath":"java/base/JDK1.8HashMap源码学习.md","lastUpdated":1708510102000}'),p={name:"java/base/JDK1.8HashMap源码学习.md"},o=l(`