From b6c8010aeca3e6059262e0c78a36c7a5cea2e9bd Mon Sep 17 00:00:00 2001 From: lixuguang Date: Wed, 20 Sep 2023 10:18:35 +0000 Subject: [PATCH] Site updated: 2023-09-20 10:18:34 --- .../index.html | 607 ++ 2013/06/26/CSS-Triangle-Circle/index.html | 623 ++ .../Read-Let-your-boss-promote-you/index.html | 780 ++ .../index.html | 688 ++ .../index.html | 656 ++ 2017/08/30/Vue-VSCode-Snippets/index.html | 777 ++ 2017/08/30/Vue-basic/index.html | 739 ++ 2017/08/30/npm-source/index.html | 608 ++ .../use-GitHub-Hexo-Next-make-blog/index.html | 658 ++ 2017/10/12/VSCode-ESLint/index.html | 597 ++ 2017/10/12/us-gitment/index.html | 620 ++ 2017/10/12/web-quanzhan/index.html | 606 ++ 2017/12/12/Git-Shell/index.html | 641 ++ 2019/12/27/Actions/index.html | 607 ++ .../27/Vue-Component-Communication/index.html | 615 ++ 2019/12/30/begin-learn-java/index.html | 591 ++ 2019/12/31/GoodBye-2019/index.html | 621 ++ 2019/12/31/gitlab-cicd/index.html | 664 ++ .../12/31/weichat-h5-compatibility/index.html | 618 ++ 2020/01/01/Hello-2020/index.html | 593 ++ 2020/01/02/21-Day-Challenge-01/index.html | 599 ++ 2020/01/02/FE-guide-Closure/index.html | 602 ++ 2020/01/02/FE-guide-dataType/index.html | 655 ++ 2020/01/02/FE-guide-instanceof/index.html | 590 ++ 2020/01/02/FE-guide-new/index.html | 608 ++ 2020/01/02/FE-guide-prototype/index.html | 646 ++ 2020/01/02/FE-guide-this/index.html | 591 ++ .../index.html" | 610 ++ 2020/01/03/FE-guide-Generator/index.html | 589 ++ .../03/FE-guide-Map-FlatMap-Reduce/index.html | 602 ++ 2020/01/03/FE-guide-Module/index.html | 601 ++ 2020/01/03/FE-guide-Promise/index.html | 592 ++ 2020/01/03/FE-guide-async-Proxy/index.html | 589 ++ 2020/01/03/FE-guide-async-await/index.html | 603 ++ .../01/03/FE-guide-call-apply-bind/index.html | 596 ++ 2020/01/03/FE-guide-copy/index.html | 621 ++ .../03/FE-guide-debounce-throttle/index.html | 605 ++ 2020/01/03/FE-guide-inherit/index.html | 597 ++ 2020/01/03/FE-guide-store/index.html | 628 ++ .../index.html | 586 ++ 2020/01/04/Algorithm/index.html | 734 ++ 2020/01/05/FE-guide-ArrayOprs/index.html | 617 ++ 2020/01/05/FE-guide-about-reduce/index.html | 604 ++ 2020/01/05/FE-guide-currying/index.html | 595 ++ 2020/01/05/FE-guide-vuepress/index.html | 589 ++ 2020/01/09/FE-guide-Net/index.html | 970 ++ 2020/01/09/FE-guide-data-structure/index.html | 706 ++ 2020/01/09/career/index.html | 599 ++ 2020/01/09/do-it-yourselfery-jsonp/index.html | 590 ++ .../index.html | 620 ++ 2020/01/13/FE-guide-inherit2/index.html | 667 ++ .../13/Javascript-Design-Pattern/index.html | 665 ++ .../13/do-it-yourselfery-promise/index.html | 623 ++ 2020/01/13/hexo-search/index.html | 596 ++ 2020/01/22/Es6-arrowFunc-arguments/index.html | 583 ++ 2020/01/22/FE-guide-safe/index.html | 635 ++ 2020/01/22/Wepack-Tips/index.html | 622 ++ 2020/02/11/SSR/index.html | 609 ++ 2020/02/11/restful-architecture/index.html | 602 ++ 2020/02/12/print-table-problem/index.html | 591 ++ 2020/02/12/vue-proxyTable-problem/index.html | 586 ++ .../index.html | 651 ++ .../index.html | 693 ++ .../index.html | 645 ++ 2020/02/24/Vue-plug-in-development/index.html | 606 ++ .../index.html | 603 ++ .../23/path-join-vs-path-resolve/index.html | 587 ++ .../do-it-yourselfery-EventEmitter/index.html | 592 ++ .../do-it-yourselfery-async-await/index.html | 584 ++ .../do-it-yourselfery-call-apply/index.html | 618 ++ .../03/24/do-it-yourselfery-create/index.html | 588 ++ 2020/03/24/do-it-yourselfery-flat/index.html | 587 ++ .../24/do-it-yourselfery-isArray/index.html | 584 ++ 2020/03/24/do-it-yourselfery-map/index.html | 589 ++ 2020/03/24/do-it-yourselfery-new/index.html | 591 ++ .../03/24/do-it-yourselfery-reduce/index.html | 587 ++ .../index.html" | 583 ++ .../index.html" | 585 ++ 2020/03/24/webpack-learning-1/index.html | 877 ++ 2020/04/03/source-code-react-diff/index.html | 700 ++ 2020/04/07/Electron-Offline-Build/index.html | 586 ++ 2020/04/07/webpack-uglifyjsplugin/index.html | 623 ++ 2020/04/15/node-sass-slow-problem/index.html | 601 ++ .../talk-about-full-stack-big-fd/index.html | 598 ++ 2020/04/16/0-vuecli-3-component/index.html | 678 ++ 2020/04/16/what-is-npx/index.html | 648 ++ 2020/05/14/css3-pointer-events/index.html | 607 ++ 2020/05/28/Prettier-Setting/index.html | 585 ++ .../rpm\345\221\275\344\273\244/index.html" | 663 ++ .../index.html" | 610 ++ 2020/11/10/learn-cordova-1/index.html | 719 ++ 2020/11/10/learn-go/index.html | 587 ++ 2020/11/11/learn-ionic-1/index.html | 640 ++ .../index.html" | 611 ++ .../index.html" | 600 ++ .../index.html" | 702 ++ .../index.html" | 785 ++ .../index.html" | 732 ++ .../index.html" | 634 ++ 2022/06/01/GIT Standard/index.html | 654 ++ .../index.html" | 823 ++ 2022/06/01/squid/index.html | 606 ++ .../index.html" | 728 ++ .../index.html" | 660 ++ .../index.html" | 588 ++ 2022/06/02/DockerFile/index.html | 700 ++ .../index.html" | 632 ++ .../index.html" | 585 ++ .../index.html" | 595 ++ .../index.html" | 595 ++ .../index.html" | 593 ++ .../Nginx\346\214\207\344\273\244/index.html" | 591 ++ 2022/06/09/http_stub_status_module/index.html | 610 ++ .../index.html" | 618 ++ .../index.html" | 728 ++ 2022/06/30/nginx-timeout/index.html | 669 ++ .../index.html" | 609 ++ .../index.html" | 586 ++ .../index.html" | 652 ++ .../02/Oracle-Linux-install-Docker/index.html | 590 ++ .../index.html" | 578 ++ .../index.html" | 586 ++ 2022/08/02/linux-scp/index.html | 599 ++ .../index.html" | 601 ++ .../index.html" | 595 ++ .../index.html" | 865 ++ .../index.html" | 561 ++ 2023/09/20/test/index.html | 590 ++ @attachment/Clipboard_2022-06-09-12-57-39.png | Bin 0 -> 6725 bytes @attachment/Clipboard_2022-06-09-12-57-48.png | Bin 0 -> 6725 bytes @attachment/Clipboard_2022-06-09-12-59-28.png | Bin 0 -> 146898 bytes @attachment/README.html | 471 + CNAME | 1 + about/index.html | 691 ++ archives/2013/06/index.html | 478 + archives/2013/index.html | 478 + archives/2015/12/index.html | 458 + archives/2015/index.html | 458 + archives/2016/10/index.html | 458 + archives/2016/index.html | 458 + archives/2017/08/index.html | 518 ++ archives/2017/10/index.html | 518 ++ archives/2017/12/index.html | 458 + archives/2017/index.html | 618 ++ archives/2019/12/index.html | 558 ++ archives/2019/index.html | 558 ++ archives/2020/01/index.html | 641 ++ archives/2020/01/page/2/index.html | 641 ++ archives/2020/01/page/3/index.html | 641 ++ archives/2020/01/page/4/index.html | 601 ++ archives/2020/02/index.html | 598 ++ archives/2020/03/index.html | 641 ++ archives/2020/03/page/2/index.html | 521 ++ archives/2020/04/index.html | 578 ++ archives/2020/05/index.html | 478 + archives/2020/07/index.html | 458 + archives/2020/08/index.html | 458 + archives/2020/11/index.html | 498 ++ archives/2020/index.html | 641 ++ archives/2020/page/2/index.html | 641 ++ archives/2020/page/3/index.html | 641 ++ archives/2020/page/4/index.html | 641 ++ archives/2020/page/5/index.html | 641 ++ archives/2020/page/6/index.html | 641 ++ archives/2020/page/7/index.html | 641 ++ archives/2020/page/8/index.html | 521 ++ archives/2021/05/index.html | 478 + archives/2021/index.html | 478 + archives/2022/03/index.html | 478 + archives/2022/06/index.html | 641 ++ archives/2022/06/page/2/index.html | 621 ++ archives/2022/08/index.html | 638 ++ archives/2022/index.html | 641 ++ archives/2022/page/2/index.html | 641 ++ archives/2022/page/3/index.html | 641 ++ archives/2022/page/4/index.html | 461 + archives/2023/09/index.html | 478 + archives/2023/index.html | 478 + archives/index.html | 644 ++ archives/page/10/index.html | 641 ++ archives/page/11/index.html | 644 ++ archives/page/12/index.html | 644 ++ archives/page/13/index.html | 610 ++ archives/page/2/index.html | 641 ++ archives/page/3/index.html | 641 ++ archives/page/4/index.html | 647 ++ archives/page/5/index.html | 641 ++ archives/page/6/index.html | 641 ++ archives/page/7/index.html | 641 ++ archives/page/8/index.html | 641 ++ archives/page/9/index.html | 641 ++ categories/Database/Postgresql/index.html | 458 + categories/Database/index.html | 458 + categories/Docker/index.html | 478 + categories/Infra/Docker/index.html | 538 ++ .../Nginx/index.html" | 498 ++ .../Squid/index.html" | 458 + .../index.html" | 518 ++ categories/Infra/index.html | 618 ++ categories/Infrastructure/index.html | 458 + categories/Linux/Oracle/Docker/index.html | 458 + categories/Linux/Oracle/index.html | 458 + categories/Linux/RPM/index.html | 458 + categories/Linux/index.html | 521 ++ categories/NodeJS/Npm/index.html | 481 + categories/NodeJS/index.html | 541 ++ categories/OracleLinux/index.html | 458 + .../Nginx/index.html" | 458 + .../index.html" | 458 + categories/index.html | 458 + categories/linux/index.html | 458 + categories/linux/systemctl/index.html | 458 + .../Webpack/index.html" | 498 ++ .../index.html" | 541 ++ .../index.html" | 578 ++ .../CSS/index.html" | 458 + .../CSS3/index.html" | 458 + .../index.html" | 647 ++ .../page/2/index.html" | 641 ++ .../page/3/index.html" | 641 ++ .../page/4/index.html" | 644 ++ .../page/5/index.html" | 504 ++ .../index.html" | 458 + .../index.html" | 644 ++ .../page/2/index.html" | 641 ++ .../page/3/index.html" | 501 ++ .../index.html" | 498 ++ .../\345\276\256\344\277\241/index.html" | 458 + .../index.html" | 524 ++ .../React/index.html" | 458 + .../Vue/index.html" | 547 ++ .../index.html" | 567 ++ .../Go\350\257\255\350\250\200/index.html" | 458 + .../index.html" | 458 + .../index.html" | 518 ++ .../\345\205\250\346\240\210/index.html" | 458 + .../index.html" | 458 + .../index.html" | 478 + .../Labrary/index.html" | 458 + .../index.html" | 501 ++ .../index.html" | 521 ++ .../index.html" | 567 ++ .../index.html" | 547 ++ .../\346\236\266\346\236\204/index.html" | 458 + .../index.html" | 641 ++ .../page/2/index.html" | 521 ++ .../index.html" | 641 ++ .../page/2/index.html" | 501 ++ .../index.html" | 656 ++ .../index.html" | 481 + .../index.html" | 521 ++ .../index.html" | 527 ++ .../Docker-Compose/index.html" | 458 + .../index.html" | 458 + css/main.css | 2853 ++++++ .../index.html | 607 ++ en/2013/06/26/CSS-Triangle-Circle/index.html | 623 ++ .../Read-Let-your-boss-promote-you/index.html | 780 ++ .../index.html | 688 ++ .../index.html | 656 ++ en/2017/08/30/Vue-VSCode-Snippets/index.html | 777 ++ en/2017/08/30/Vue-basic/index.html | 739 ++ en/2017/08/30/npm-source/index.html | 608 ++ .../use-GitHub-Hexo-Next-make-blog/index.html | 658 ++ en/2017/10/12/VSCode-ESLint/index.html | 597 ++ en/2017/10/12/us-gitment/index.html | 620 ++ en/2017/10/12/web-quanzhan/index.html | 606 ++ en/2017/12/12/Git-Shell/index.html | 641 ++ en/2019/12/27/Actions/index.html | 607 ++ .../27/Vue-Component-Communication/index.html | 615 ++ en/2019/12/30/begin-learn-java/index.html | 591 ++ en/2019/12/31/GoodBye-2019/index.html | 621 ++ en/2019/12/31/gitlab-cicd/index.html | 664 ++ .../12/31/weichat-h5-compatibility/index.html | 618 ++ en/2020/01/01/Hello-2020/index.html | 593 ++ en/2020/01/02/21-Day-Challenge-01/index.html | 599 ++ en/2020/01/02/FE-guide-Closure/index.html | 602 ++ en/2020/01/02/FE-guide-dataType/index.html | 655 ++ en/2020/01/02/FE-guide-instanceof/index.html | 590 ++ en/2020/01/02/FE-guide-new/index.html | 608 ++ en/2020/01/02/FE-guide-prototype/index.html | 646 ++ en/2020/01/02/FE-guide-this/index.html | 591 ++ .../index.html" | 610 ++ en/2020/01/03/FE-guide-Generator/index.html | 589 ++ .../03/FE-guide-Map-FlatMap-Reduce/index.html | 602 ++ en/2020/01/03/FE-guide-Module/index.html | 601 ++ en/2020/01/03/FE-guide-Promise/index.html | 592 ++ en/2020/01/03/FE-guide-async-Proxy/index.html | 589 ++ en/2020/01/03/FE-guide-async-await/index.html | 603 ++ .../01/03/FE-guide-call-apply-bind/index.html | 596 ++ en/2020/01/03/FE-guide-copy/index.html | 621 ++ .../03/FE-guide-debounce-throttle/index.html | 605 ++ en/2020/01/03/FE-guide-inherit/index.html | 597 ++ en/2020/01/03/FE-guide-store/index.html | 628 ++ .../index.html | 586 ++ en/2020/01/04/Algorithm/index.html | 734 ++ en/2020/01/05/FE-guide-ArrayOprs/index.html | 617 ++ .../01/05/FE-guide-about-reduce/index.html | 604 ++ en/2020/01/05/FE-guide-currying/index.html | 595 ++ en/2020/01/05/FE-guide-vuepress/index.html | 589 ++ en/2020/01/09/FE-guide-Net/index.html | 970 ++ .../01/09/FE-guide-data-structure/index.html | 706 ++ en/2020/01/09/career/index.html | 599 ++ .../01/09/do-it-yourselfery-jsonp/index.html | 590 ++ .../index.html | 620 ++ en/2020/01/13/FE-guide-inherit2/index.html | 667 ++ .../13/Javascript-Design-Pattern/index.html | 665 ++ .../13/do-it-yourselfery-promise/index.html | 623 ++ en/2020/01/13/hexo-search/index.html | 596 ++ .../01/22/Es6-arrowFunc-arguments/index.html | 583 ++ en/2020/01/22/FE-guide-safe/index.html | 635 ++ en/2020/01/22/Wepack-Tips/index.html | 622 ++ en/2020/02/11/SSR/index.html | 609 ++ en/2020/02/11/restful-architecture/index.html | 602 ++ en/2020/02/12/print-table-problem/index.html | 591 ++ .../02/12/vue-proxyTable-problem/index.html | 586 ++ .../index.html | 651 ++ .../index.html | 693 ++ .../index.html | 645 ++ .../02/24/Vue-plug-in-development/index.html | 606 ++ .../index.html | 603 ++ .../23/path-join-vs-path-resolve/index.html | 587 ++ .../do-it-yourselfery-EventEmitter/index.html | 592 ++ .../do-it-yourselfery-async-await/index.html | 584 ++ .../do-it-yourselfery-call-apply/index.html | 618 ++ .../03/24/do-it-yourselfery-create/index.html | 588 ++ .../03/24/do-it-yourselfery-flat/index.html | 587 ++ .../24/do-it-yourselfery-isArray/index.html | 584 ++ .../03/24/do-it-yourselfery-map/index.html | 589 ++ .../03/24/do-it-yourselfery-new/index.html | 591 ++ .../03/24/do-it-yourselfery-reduce/index.html | 587 ++ .../index.html" | 583 ++ .../index.html" | 585 ++ en/2020/03/24/webpack-learning-1/index.html | 877 ++ .../04/03/source-code-react-diff/index.html | 700 ++ .../04/07/Electron-Offline-Build/index.html | 586 ++ .../04/07/webpack-uglifyjsplugin/index.html | 623 ++ .../04/15/node-sass-slow-problem/index.html | 601 ++ .../talk-about-full-stack-big-fd/index.html | 598 ++ en/2020/04/16/0-vuecli-3-component/index.html | 678 ++ en/2020/04/16/what-is-npx/index.html | 648 ++ en/2020/05/14/css3-pointer-events/index.html | 607 ++ en/2020/05/28/Prettier-Setting/index.html | 585 ++ .../rpm\345\221\275\344\273\244/index.html" | 663 ++ .../index.html" | 610 ++ en/2020/11/10/learn-cordova-1/index.html | 719 ++ en/2020/11/10/learn-go/index.html | 587 ++ en/2020/11/11/learn-ionic-1/index.html | 640 ++ .../index.html" | 611 ++ .../index.html" | 600 ++ .../index.html" | 702 ++ .../index.html" | 785 ++ .../index.html" | 732 ++ .../index.html" | 634 ++ en/2022/06/01/GIT Standard/index.html | 654 ++ .../index.html" | 823 ++ en/2022/06/01/squid/index.html | 606 ++ .../index.html" | 728 ++ .../index.html" | 660 ++ .../index.html" | 588 ++ en/2022/06/02/DockerFile/index.html | 700 ++ .../index.html" | 632 ++ .../index.html" | 585 ++ .../index.html" | 595 ++ .../index.html" | 595 ++ .../index.html" | 593 ++ .../Nginx\346\214\207\344\273\244/index.html" | 591 ++ .../06/09/http_stub_status_module/index.html | 610 ++ .../index.html" | 618 ++ .../index.html" | 728 ++ en/2022/06/30/nginx-timeout/index.html | 669 ++ .../index.html" | 609 ++ .../index.html" | 586 ++ .../index.html" | 652 ++ .../02/Oracle-Linux-install-Docker/index.html | 590 ++ .../index.html" | 578 ++ .../index.html" | 586 ++ en/2022/08/02/linux-scp/index.html | 599 ++ .../index.html" | 601 ++ .../index.html" | 595 ++ .../index.html" | 865 ++ .../index.html" | 561 ++ en/2023/09/20/test/index.html | 590 ++ en/@attachment/README.html | 485 + en/about/index.html | 707 ++ en/archives/2013/06/index.html | 478 + en/archives/2013/index.html | 478 + en/archives/2015/12/index.html | 458 + en/archives/2015/index.html | 458 + en/archives/2016/10/index.html | 458 + en/archives/2016/index.html | 458 + en/archives/2017/08/index.html | 518 ++ en/archives/2017/10/index.html | 518 ++ en/archives/2017/12/index.html | 458 + en/archives/2017/index.html | 618 ++ en/archives/2019/12/index.html | 558 ++ en/archives/2019/index.html | 558 ++ en/archives/2020/01/index.html | 641 ++ en/archives/2020/01/page/2/index.html | 641 ++ en/archives/2020/01/page/3/index.html | 641 ++ en/archives/2020/01/page/4/index.html | 601 ++ en/archives/2020/02/index.html | 598 ++ en/archives/2020/03/index.html | 641 ++ en/archives/2020/03/page/2/index.html | 521 ++ en/archives/2020/04/index.html | 578 ++ en/archives/2020/05/index.html | 478 + en/archives/2020/07/index.html | 458 + en/archives/2020/08/index.html | 458 + en/archives/2020/11/index.html | 498 ++ en/archives/2020/index.html | 641 ++ en/archives/2020/page/2/index.html | 641 ++ en/archives/2020/page/3/index.html | 641 ++ en/archives/2020/page/4/index.html | 641 ++ en/archives/2020/page/5/index.html | 641 ++ en/archives/2020/page/6/index.html | 641 ++ en/archives/2020/page/7/index.html | 641 ++ en/archives/2020/page/8/index.html | 521 ++ en/archives/2021/05/index.html | 478 + en/archives/2021/index.html | 478 + en/archives/2022/03/index.html | 478 + en/archives/2022/06/index.html | 641 ++ en/archives/2022/06/page/2/index.html | 621 ++ en/archives/2022/08/index.html | 638 ++ en/archives/2022/index.html | 641 ++ en/archives/2022/page/2/index.html | 641 ++ en/archives/2022/page/3/index.html | 641 ++ en/archives/2022/page/4/index.html | 461 + en/archives/2023/09/index.html | 478 + en/archives/2023/index.html | 478 + en/archives/index.html | 644 ++ en/archives/page/10/index.html | 641 ++ en/archives/page/11/index.html | 644 ++ en/archives/page/12/index.html | 644 ++ en/archives/page/13/index.html | 610 ++ en/archives/page/2/index.html | 641 ++ en/archives/page/3/index.html | 641 ++ en/archives/page/4/index.html | 647 ++ en/archives/page/5/index.html | 641 ++ en/archives/page/6/index.html | 641 ++ en/archives/page/7/index.html | 641 ++ en/archives/page/8/index.html | 641 ++ en/archives/page/9/index.html | 641 ++ en/categories/Database/Postgresql/index.html | 458 + en/categories/Database/index.html | 458 + en/categories/Docker/index.html | 478 + en/categories/Infra/Docker/index.html | 538 ++ .../Nginx/index.html" | 498 ++ .../Squid/index.html" | 458 + .../index.html" | 518 ++ en/categories/Infra/index.html | 618 ++ en/categories/Infrastructure/index.html | 458 + en/categories/Linux/Oracle/Docker/index.html | 458 + en/categories/Linux/Oracle/index.html | 458 + en/categories/Linux/RPM/index.html | 458 + en/categories/Linux/index.html | 521 ++ en/categories/NodeJS/Npm/index.html | 481 + en/categories/NodeJS/index.html | 541 ++ en/categories/OracleLinux/index.html | 458 + .../Nginx/index.html" | 458 + .../index.html" | 458 + en/categories/index.html | 470 + en/categories/linux/index.html | 458 + en/categories/linux/systemctl/index.html | 458 + .../Webpack/index.html" | 498 ++ .../index.html" | 541 ++ .../index.html" | 578 ++ .../CSS/index.html" | 458 + .../CSS3/index.html" | 458 + .../index.html" | 647 ++ .../page/2/index.html" | 641 ++ .../page/3/index.html" | 641 ++ .../page/4/index.html" | 644 ++ .../page/5/index.html" | 504 ++ .../index.html" | 458 + .../index.html" | 644 ++ .../page/2/index.html" | 641 ++ .../page/3/index.html" | 501 ++ .../index.html" | 498 ++ .../\345\276\256\344\277\241/index.html" | 458 + .../index.html" | 524 ++ .../React/index.html" | 458 + .../Vue/index.html" | 547 ++ .../index.html" | 567 ++ .../Go\350\257\255\350\250\200/index.html" | 458 + .../index.html" | 458 + .../index.html" | 518 ++ .../\345\205\250\346\240\210/index.html" | 458 + .../index.html" | 458 + .../index.html" | 478 + .../Labrary/index.html" | 458 + .../index.html" | 501 ++ .../index.html" | 521 ++ .../index.html" | 567 ++ .../index.html" | 547 ++ .../\346\236\266\346\236\204/index.html" | 458 + .../index.html" | 641 ++ .../page/2/index.html" | 521 ++ .../index.html" | 641 ++ .../page/2/index.html" | 501 ++ .../index.html" | 656 ++ .../index.html" | 481 + .../index.html" | 521 ++ .../index.html" | 527 ++ .../Docker-Compose/index.html" | 458 + .../index.html" | 458 + en/index.html | 506 ++ en/page/10/index.html | 492 ++ en/page/100/index.html | 534 ++ en/page/101/index.html | 521 ++ en/page/102/index.html | 513 ++ en/page/103/index.html | 523 ++ en/page/104/index.html | 504 ++ en/page/105/index.html | 503 ++ en/page/106/index.html | 508 ++ en/page/107/index.html | 507 ++ en/page/108/index.html | 510 ++ en/page/109/index.html | 509 ++ en/page/11/index.html | 508 ++ en/page/110/index.html | 532 ++ en/page/111/index.html | 497 ++ en/page/112/index.html | 507 ++ en/page/113/index.html | 507 ++ en/page/114/index.html | 507 ++ en/page/115/index.html | 507 ++ en/page/116/index.html | 554 ++ en/page/117/index.html | 507 ++ en/page/118/index.html | 507 ++ en/page/119/index.html | 507 ++ en/page/12/index.html | 512 ++ en/page/120/index.html | 512 ++ en/page/121/index.html | 507 ++ en/page/122/index.html | 507 ++ en/page/123/index.html | 507 ++ en/page/124/index.html | 507 ++ en/page/125/index.html | 604 ++ en/page/126/index.html | 696 ++ en/page/127/index.html | 507 ++ en/page/128/index.html | 526 ++ en/page/13/index.html | 582 ++ en/page/14/index.html | 641 ++ en/page/15/index.html | 531 ++ en/page/16/index.html | 521 ++ en/page/17/index.html | 503 ++ en/page/18/index.html | 504 ++ en/page/19/index.html | 545 ++ en/page/2/index.html | 477 + en/page/20/index.html | 508 ++ en/page/21/index.html | 498 ++ en/page/22/index.html | 508 ++ en/page/23/index.html | 613 ++ en/page/24/index.html | 567 ++ en/page/25/index.html | 736 ++ en/page/26/index.html | 645 ++ en/page/27/index.html | 570 ++ en/page/28/index.html | 641 ++ en/page/29/index.html | 548 ++ en/page/3/index.html | 499 ++ en/page/30/index.html | 519 ++ en/page/31/index.html | 501 ++ en/page/32/index.html | 508 ++ en/page/33/index.html | 507 ++ en/page/34/index.html | 507 ++ en/page/35/index.html | 507 ++ en/page/36/index.html | 509 ++ en/page/37/index.html | 508 ++ en/page/38/index.html | 507 ++ en/page/39/index.html | 523 ++ en/page/4/index.html | 499 ++ en/page/40/index.html | 576 ++ en/page/41/index.html | 504 ++ en/page/42/index.html | 507 ++ en/page/43/index.html | 503 ++ en/page/44/index.html | 510 ++ en/page/45/index.html | 508 ++ en/page/46/index.html | 505 ++ en/page/47/index.html | 504 ++ en/page/48/index.html | 507 ++ en/page/49/index.html | 506 ++ en/page/5/index.html | 565 ++ en/page/50/index.html | 784 ++ en/page/51/index.html | 531 ++ en/page/52/index.html | 497 ++ en/page/53/index.html | 505 ++ en/page/54/index.html | 498 ++ en/page/55/index.html | 502 ++ en/page/56/index.html | 501 ++ en/page/57/index.html | 496 ++ en/page/58/index.html | 500 ++ en/page/59/index.html | 497 ++ en/page/6/index.html | 519 ++ en/page/60/index.html | 504 ++ en/page/61/index.html | 500 ++ en/page/62/index.html | 500 ++ en/page/63/index.html | 512 ++ en/page/64/index.html | 519 ++ en/page/65/index.html | 558 ++ en/page/66/index.html | 606 ++ en/page/67/index.html | 564 ++ en/page/68/index.html | 500 ++ en/page/69/index.html | 504 ++ en/page/7/index.html | 503 ++ en/page/70/index.html | 513 ++ en/page/71/index.html | 518 ++ en/page/72/index.html | 507 ++ en/page/73/index.html | 507 ++ en/page/74/index.html | 497 ++ en/page/75/index.html | 507 ++ en/page/76/index.html | 503 ++ en/page/77/index.html | 580 ++ en/page/78/index.html | 536 ++ en/page/79/index.html | 512 ++ en/page/8/index.html | 778 ++ en/page/80/index.html | 507 ++ en/page/81/index.html | 503 ++ en/page/82/index.html | 617 ++ en/page/83/index.html | 880 ++ en/page/84/index.html | 507 ++ en/page/85/index.html | 508 ++ en/page/86/index.html | 515 ++ en/page/87/index.html | 528 ++ en/page/88/index.html | 634 ++ en/page/89/index.html | 541 ++ en/page/9/index.html | 514 ++ en/page/90/index.html | 497 ++ en/page/91/index.html | 502 ++ en/page/92/index.html | 516 ++ en/page/93/index.html | 515 ++ en/page/94/index.html | 502 ++ en/page/95/index.html | 505 ++ en/page/96/index.html | 509 ++ en/page/97/index.html | 510 ++ en/page/98/index.html | 518 ++ en/page/99/index.html | 514 ++ en/tags/index.html | 470 + images/algolia_logo.svg | 9 + images/apple-touch-icon-next.png | Bin 0 -> 1544 bytes images/avatar.gif | Bin 0 -> 1793 bytes images/avatar.jpg | Bin 0 -> 15611 bytes images/cc-by-nc-nd.svg | 121 + images/cc-by-nc-sa.svg | 121 + images/cc-by-nc.svg | 121 + images/cc-by-nd.svg | 117 + images/cc-by-sa.svg | 121 + images/cc-by.svg | 121 + images/cc-zero.svg | 72 + images/favicon-32x32-next.png | Bin 0 -> 640 bytes images/logo.svg | 23 + images/wechatpay.jpg | Bin 0 -> 188112 bytes index.html | 506 ++ .../index.html | 607 ++ ja/2013/06/26/CSS-Triangle-Circle/index.html | 623 ++ .../Read-Let-your-boss-promote-you/index.html | 780 ++ .../index.html | 688 ++ .../index.html | 656 ++ ja/2017/08/30/Vue-VSCode-Snippets/index.html | 777 ++ ja/2017/08/30/Vue-basic/index.html | 739 ++ ja/2017/08/30/npm-source/index.html | 608 ++ .../use-GitHub-Hexo-Next-make-blog/index.html | 658 ++ ja/2017/10/12/VSCode-ESLint/index.html | 597 ++ ja/2017/10/12/us-gitment/index.html | 620 ++ ja/2017/10/12/web-quanzhan/index.html | 606 ++ ja/2017/12/12/Git-Shell/index.html | 641 ++ ja/2019/12/27/Actions/index.html | 607 ++ .../27/Vue-Component-Communication/index.html | 615 ++ ja/2019/12/30/begin-learn-java/index.html | 591 ++ ja/2019/12/31/GoodBye-2019/index.html | 621 ++ ja/2019/12/31/gitlab-cicd/index.html | 664 ++ .../12/31/weichat-h5-compatibility/index.html | 618 ++ ja/2020/01/01/Hello-2020/index.html | 593 ++ ja/2020/01/02/21-Day-Challenge-01/index.html | 599 ++ ja/2020/01/02/FE-guide-Closure/index.html | 602 ++ ja/2020/01/02/FE-guide-dataType/index.html | 655 ++ ja/2020/01/02/FE-guide-instanceof/index.html | 590 ++ ja/2020/01/02/FE-guide-new/index.html | 608 ++ ja/2020/01/02/FE-guide-prototype/index.html | 646 ++ ja/2020/01/02/FE-guide-this/index.html | 591 ++ .../index.html" | 610 ++ ja/2020/01/03/FE-guide-Generator/index.html | 589 ++ .../03/FE-guide-Map-FlatMap-Reduce/index.html | 602 ++ ja/2020/01/03/FE-guide-Module/index.html | 601 ++ ja/2020/01/03/FE-guide-Promise/index.html | 592 ++ ja/2020/01/03/FE-guide-async-Proxy/index.html | 589 ++ ja/2020/01/03/FE-guide-async-await/index.html | 603 ++ .../01/03/FE-guide-call-apply-bind/index.html | 596 ++ ja/2020/01/03/FE-guide-copy/index.html | 621 ++ .../03/FE-guide-debounce-throttle/index.html | 605 ++ ja/2020/01/03/FE-guide-inherit/index.html | 597 ++ ja/2020/01/03/FE-guide-store/index.html | 628 ++ .../index.html | 586 ++ ja/2020/01/04/Algorithm/index.html | 734 ++ ja/2020/01/05/FE-guide-ArrayOprs/index.html | 617 ++ .../01/05/FE-guide-about-reduce/index.html | 604 ++ ja/2020/01/05/FE-guide-currying/index.html | 595 ++ ja/2020/01/05/FE-guide-vuepress/index.html | 589 ++ ja/2020/01/09/FE-guide-Net/index.html | 970 ++ .../01/09/FE-guide-data-structure/index.html | 706 ++ ja/2020/01/09/career/index.html | 599 ++ .../01/09/do-it-yourselfery-jsonp/index.html | 590 ++ .../index.html | 620 ++ ja/2020/01/13/FE-guide-inherit2/index.html | 667 ++ .../13/Javascript-Design-Pattern/index.html | 665 ++ .../13/do-it-yourselfery-promise/index.html | 623 ++ ja/2020/01/13/hexo-search/index.html | 596 ++ .../01/22/Es6-arrowFunc-arguments/index.html | 583 ++ ja/2020/01/22/FE-guide-safe/index.html | 635 ++ ja/2020/01/22/Wepack-Tips/index.html | 622 ++ ja/2020/02/11/SSR/index.html | 609 ++ ja/2020/02/11/restful-architecture/index.html | 602 ++ ja/2020/02/12/print-table-problem/index.html | 591 ++ .../02/12/vue-proxyTable-problem/index.html | 586 ++ .../index.html | 651 ++ .../index.html | 693 ++ .../index.html | 645 ++ .../02/24/Vue-plug-in-development/index.html | 606 ++ .../index.html | 603 ++ .../23/path-join-vs-path-resolve/index.html | 587 ++ .../do-it-yourselfery-EventEmitter/index.html | 592 ++ .../do-it-yourselfery-async-await/index.html | 584 ++ .../do-it-yourselfery-call-apply/index.html | 618 ++ .../03/24/do-it-yourselfery-create/index.html | 588 ++ .../03/24/do-it-yourselfery-flat/index.html | 587 ++ .../24/do-it-yourselfery-isArray/index.html | 584 ++ .../03/24/do-it-yourselfery-map/index.html | 589 ++ .../03/24/do-it-yourselfery-new/index.html | 591 ++ .../03/24/do-it-yourselfery-reduce/index.html | 587 ++ .../index.html" | 583 ++ .../index.html" | 585 ++ ja/2020/03/24/webpack-learning-1/index.html | 877 ++ .../04/03/source-code-react-diff/index.html | 700 ++ .../04/07/Electron-Offline-Build/index.html | 586 ++ .../04/07/webpack-uglifyjsplugin/index.html | 623 ++ .../04/15/node-sass-slow-problem/index.html | 601 ++ .../talk-about-full-stack-big-fd/index.html | 598 ++ ja/2020/04/16/0-vuecli-3-component/index.html | 678 ++ ja/2020/04/16/what-is-npx/index.html | 648 ++ ja/2020/05/14/css3-pointer-events/index.html | 607 ++ ja/2020/05/28/Prettier-Setting/index.html | 585 ++ .../rpm\345\221\275\344\273\244/index.html" | 663 ++ .../index.html" | 610 ++ ja/2020/11/10/learn-cordova-1/index.html | 719 ++ ja/2020/11/10/learn-go/index.html | 587 ++ ja/2020/11/11/learn-ionic-1/index.html | 640 ++ .../index.html" | 611 ++ .../index.html" | 600 ++ .../index.html" | 702 ++ .../index.html" | 785 ++ .../index.html" | 732 ++ .../index.html" | 634 ++ ja/2022/06/01/GIT Standard/index.html | 654 ++ .../index.html" | 823 ++ ja/2022/06/01/squid/index.html | 606 ++ .../index.html" | 728 ++ .../index.html" | 660 ++ .../index.html" | 588 ++ ja/2022/06/02/DockerFile/index.html | 700 ++ .../index.html" | 632 ++ .../index.html" | 585 ++ .../index.html" | 595 ++ .../index.html" | 595 ++ .../index.html" | 593 ++ .../Nginx\346\214\207\344\273\244/index.html" | 591 ++ .../06/09/http_stub_status_module/index.html | 610 ++ .../index.html" | 618 ++ .../index.html" | 728 ++ ja/2022/06/30/nginx-timeout/index.html | 669 ++ .../index.html" | 609 ++ .../index.html" | 586 ++ .../index.html" | 652 ++ .../02/Oracle-Linux-install-Docker/index.html | 590 ++ .../index.html" | 578 ++ .../index.html" | 586 ++ ja/2022/08/02/linux-scp/index.html | 599 ++ .../index.html" | 601 ++ .../index.html" | 595 ++ .../index.html" | 865 ++ .../index.html" | 561 ++ ja/2023/09/20/test/index.html | 590 ++ ja/@attachment/README.html | 485 + ja/about/index.html | 705 ++ ja/archives/2013/06/index.html | 478 + ja/archives/2013/index.html | 478 + ja/archives/2015/12/index.html | 458 + ja/archives/2015/index.html | 458 + ja/archives/2016/10/index.html | 458 + ja/archives/2016/index.html | 458 + ja/archives/2017/08/index.html | 518 ++ ja/archives/2017/10/index.html | 518 ++ ja/archives/2017/12/index.html | 458 + ja/archives/2017/index.html | 618 ++ ja/archives/2019/12/index.html | 558 ++ ja/archives/2019/index.html | 558 ++ ja/archives/2020/01/index.html | 641 ++ ja/archives/2020/01/page/2/index.html | 641 ++ ja/archives/2020/01/page/3/index.html | 641 ++ ja/archives/2020/01/page/4/index.html | 601 ++ ja/archives/2020/02/index.html | 598 ++ ja/archives/2020/03/index.html | 641 ++ ja/archives/2020/03/page/2/index.html | 521 ++ ja/archives/2020/04/index.html | 578 ++ ja/archives/2020/05/index.html | 478 + ja/archives/2020/07/index.html | 458 + ja/archives/2020/08/index.html | 458 + ja/archives/2020/11/index.html | 498 ++ ja/archives/2020/index.html | 641 ++ ja/archives/2020/page/2/index.html | 641 ++ ja/archives/2020/page/3/index.html | 641 ++ ja/archives/2020/page/4/index.html | 641 ++ ja/archives/2020/page/5/index.html | 641 ++ ja/archives/2020/page/6/index.html | 641 ++ ja/archives/2020/page/7/index.html | 641 ++ ja/archives/2020/page/8/index.html | 521 ++ ja/archives/2021/05/index.html | 478 + ja/archives/2021/index.html | 478 + ja/archives/2022/03/index.html | 478 + ja/archives/2022/06/index.html | 641 ++ ja/archives/2022/06/page/2/index.html | 621 ++ ja/archives/2022/08/index.html | 638 ++ ja/archives/2022/index.html | 641 ++ ja/archives/2022/page/2/index.html | 641 ++ ja/archives/2022/page/3/index.html | 641 ++ ja/archives/2022/page/4/index.html | 461 + ja/archives/2023/09/index.html | 478 + ja/archives/2023/index.html | 478 + ja/archives/index.html | 644 ++ ja/archives/page/10/index.html | 641 ++ ja/archives/page/11/index.html | 644 ++ ja/archives/page/12/index.html | 644 ++ ja/archives/page/13/index.html | 610 ++ ja/archives/page/2/index.html | 641 ++ ja/archives/page/3/index.html | 641 ++ ja/archives/page/4/index.html | 647 ++ ja/archives/page/5/index.html | 641 ++ ja/archives/page/6/index.html | 641 ++ ja/archives/page/7/index.html | 641 ++ ja/archives/page/8/index.html | 641 ++ ja/archives/page/9/index.html | 641 ++ ja/categories/Database/Postgresql/index.html | 458 + ja/categories/Database/index.html | 458 + ja/categories/Docker/index.html | 478 + ja/categories/Infra/Docker/index.html | 538 ++ .../Nginx/index.html" | 498 ++ .../Squid/index.html" | 458 + .../index.html" | 518 ++ ja/categories/Infra/index.html | 618 ++ ja/categories/Infrastructure/index.html | 458 + ja/categories/Linux/Oracle/Docker/index.html | 458 + ja/categories/Linux/Oracle/index.html | 458 + ja/categories/Linux/RPM/index.html | 458 + ja/categories/Linux/index.html | 521 ++ ja/categories/NodeJS/Npm/index.html | 481 + ja/categories/NodeJS/index.html | 541 ++ ja/categories/OracleLinux/index.html | 458 + .../Nginx/index.html" | 458 + .../index.html" | 458 + ja/categories/index.html | 470 + ja/categories/linux/index.html | 458 + ja/categories/linux/systemctl/index.html | 458 + .../Webpack/index.html" | 498 ++ .../index.html" | 541 ++ .../index.html" | 578 ++ .../CSS/index.html" | 458 + .../CSS3/index.html" | 458 + .../index.html" | 647 ++ .../page/2/index.html" | 641 ++ .../page/3/index.html" | 641 ++ .../page/4/index.html" | 644 ++ .../page/5/index.html" | 504 ++ .../index.html" | 458 + .../index.html" | 644 ++ .../page/2/index.html" | 641 ++ .../page/3/index.html" | 501 ++ .../index.html" | 498 ++ .../\345\276\256\344\277\241/index.html" | 458 + .../index.html" | 524 ++ .../React/index.html" | 458 + .../Vue/index.html" | 547 ++ .../index.html" | 567 ++ .../Go\350\257\255\350\250\200/index.html" | 458 + .../index.html" | 458 + .../index.html" | 518 ++ .../\345\205\250\346\240\210/index.html" | 458 + .../index.html" | 458 + .../index.html" | 478 + .../Labrary/index.html" | 458 + .../index.html" | 501 ++ .../index.html" | 521 ++ .../index.html" | 567 ++ .../index.html" | 547 ++ .../\346\236\266\346\236\204/index.html" | 458 + .../index.html" | 641 ++ .../page/2/index.html" | 521 ++ .../index.html" | 641 ++ .../page/2/index.html" | 501 ++ .../index.html" | 656 ++ .../index.html" | 481 + .../index.html" | 521 ++ .../index.html" | 527 ++ .../Docker-Compose/index.html" | 458 + .../index.html" | 458 + ja/index.html | 506 ++ ja/page/10/index.html | 492 ++ ja/page/100/index.html | 534 ++ ja/page/101/index.html | 521 ++ ja/page/102/index.html | 513 ++ ja/page/103/index.html | 523 ++ ja/page/104/index.html | 504 ++ ja/page/105/index.html | 503 ++ ja/page/106/index.html | 508 ++ ja/page/107/index.html | 507 ++ ja/page/108/index.html | 510 ++ ja/page/109/index.html | 509 ++ ja/page/11/index.html | 508 ++ ja/page/110/index.html | 532 ++ ja/page/111/index.html | 497 ++ ja/page/112/index.html | 507 ++ ja/page/113/index.html | 507 ++ ja/page/114/index.html | 507 ++ ja/page/115/index.html | 507 ++ ja/page/116/index.html | 554 ++ ja/page/117/index.html | 507 ++ ja/page/118/index.html | 507 ++ ja/page/119/index.html | 507 ++ ja/page/12/index.html | 512 ++ ja/page/120/index.html | 512 ++ ja/page/121/index.html | 507 ++ ja/page/122/index.html | 507 ++ ja/page/123/index.html | 507 ++ ja/page/124/index.html | 507 ++ ja/page/125/index.html | 604 ++ ja/page/126/index.html | 696 ++ ja/page/127/index.html | 507 ++ ja/page/128/index.html | 526 ++ ja/page/13/index.html | 582 ++ ja/page/14/index.html | 641 ++ ja/page/15/index.html | 531 ++ ja/page/16/index.html | 521 ++ ja/page/17/index.html | 503 ++ ja/page/18/index.html | 504 ++ ja/page/19/index.html | 545 ++ ja/page/2/index.html | 477 + ja/page/20/index.html | 508 ++ ja/page/21/index.html | 498 ++ ja/page/22/index.html | 508 ++ ja/page/23/index.html | 613 ++ ja/page/24/index.html | 567 ++ ja/page/25/index.html | 736 ++ ja/page/26/index.html | 645 ++ ja/page/27/index.html | 570 ++ ja/page/28/index.html | 641 ++ ja/page/29/index.html | 548 ++ ja/page/3/index.html | 499 ++ ja/page/30/index.html | 519 ++ ja/page/31/index.html | 501 ++ ja/page/32/index.html | 508 ++ ja/page/33/index.html | 507 ++ ja/page/34/index.html | 507 ++ ja/page/35/index.html | 507 ++ ja/page/36/index.html | 509 ++ ja/page/37/index.html | 508 ++ ja/page/38/index.html | 507 ++ ja/page/39/index.html | 523 ++ ja/page/4/index.html | 499 ++ ja/page/40/index.html | 576 ++ ja/page/41/index.html | 504 ++ ja/page/42/index.html | 507 ++ ja/page/43/index.html | 503 ++ ja/page/44/index.html | 510 ++ ja/page/45/index.html | 508 ++ ja/page/46/index.html | 505 ++ ja/page/47/index.html | 504 ++ ja/page/48/index.html | 507 ++ ja/page/49/index.html | 506 ++ ja/page/5/index.html | 565 ++ ja/page/50/index.html | 784 ++ ja/page/51/index.html | 531 ++ ja/page/52/index.html | 497 ++ ja/page/53/index.html | 505 ++ ja/page/54/index.html | 498 ++ ja/page/55/index.html | 502 ++ ja/page/56/index.html | 501 ++ ja/page/57/index.html | 496 ++ ja/page/58/index.html | 500 ++ ja/page/59/index.html | 497 ++ ja/page/6/index.html | 519 ++ ja/page/60/index.html | 504 ++ ja/page/61/index.html | 500 ++ ja/page/62/index.html | 500 ++ ja/page/63/index.html | 512 ++ ja/page/64/index.html | 519 ++ ja/page/65/index.html | 558 ++ ja/page/66/index.html | 606 ++ ja/page/67/index.html | 564 ++ ja/page/68/index.html | 500 ++ ja/page/69/index.html | 504 ++ ja/page/7/index.html | 503 ++ ja/page/70/index.html | 513 ++ ja/page/71/index.html | 518 ++ ja/page/72/index.html | 507 ++ ja/page/73/index.html | 507 ++ ja/page/74/index.html | 497 ++ ja/page/75/index.html | 507 ++ ja/page/76/index.html | 503 ++ ja/page/77/index.html | 580 ++ ja/page/78/index.html | 536 ++ ja/page/79/index.html | 512 ++ ja/page/8/index.html | 778 ++ ja/page/80/index.html | 507 ++ ja/page/81/index.html | 503 ++ ja/page/82/index.html | 617 ++ ja/page/83/index.html | 880 ++ ja/page/84/index.html | 507 ++ ja/page/85/index.html | 508 ++ ja/page/86/index.html | 515 ++ ja/page/87/index.html | 528 ++ ja/page/88/index.html | 634 ++ ja/page/89/index.html | 541 ++ ja/page/9/index.html | 514 ++ ja/page/90/index.html | 497 ++ ja/page/91/index.html | 502 ++ ja/page/92/index.html | 516 ++ ja/page/93/index.html | 515 ++ ja/page/94/index.html | 502 ++ ja/page/95/index.html | 505 ++ ja/page/96/index.html | 509 ++ ja/page/97/index.html | 510 ++ ja/page/98/index.html | 518 ++ ja/page/99/index.html | 514 ++ ja/tags/index.html | 470 + js/local-search.js | 278 + js/motion.js | 177 + js/next-boot.js | 114 + js/schemes/pisces.js | 86 + js/utils.js | 415 + lib/anime.min.js | 8 + lib/font-awesome/css/all.min.css | 5 + lib/font-awesome/webfonts/fa-brands-400.woff2 | Bin 0 -> 76612 bytes .../webfonts/fa-regular-400.woff2 | Bin 0 -> 13584 bytes lib/font-awesome/webfonts/fa-solid-900.woff2 | Bin 0 -> 79444 bytes lib/velocity/velocity.min.js | 4 + lib/velocity/velocity.ui.min.js | 2 + live2dw/assets/moc/wanko.1024/texture_00.png | Bin 0 -> 114061 bytes live2dw/assets/moc/wanko.moc | Bin 0 -> 86014 bytes live2dw/assets/mtn/idle_01.mtn | 24 + live2dw/assets/mtn/idle_02.mtn | 24 + live2dw/assets/mtn/idle_03.mtn | 24 + live2dw/assets/mtn/idle_04.mtn | 24 + live2dw/assets/mtn/shake_01.mtn | 24 + live2dw/assets/mtn/shake_02.mtn | 24 + live2dw/assets/mtn/touch_01.mtn | 24 + live2dw/assets/mtn/touch_02.mtn | 24 + live2dw/assets/mtn/touch_03.mtn | 24 + live2dw/assets/mtn/touch_04.mtn | 24 + live2dw/assets/mtn/touch_05.mtn | 24 + live2dw/assets/mtn/touch_06.mtn | 24 + live2dw/assets/wanko.model.json | 1 + live2dw/lib/L2Dwidget.0.min.js | 3 + live2dw/lib/L2Dwidget.0.min.js.map | 1 + live2dw/lib/L2Dwidget.min.js | 3 + live2dw/lib/L2Dwidget.min.js.map | 1 + page/10/index.html | 492 ++ page/100/index.html | 534 ++ page/101/index.html | 521 ++ page/102/index.html | 513 ++ page/103/index.html | 523 ++ page/104/index.html | 504 ++ page/105/index.html | 503 ++ page/106/index.html | 508 ++ page/107/index.html | 507 ++ page/108/index.html | 510 ++ page/109/index.html | 509 ++ page/11/index.html | 508 ++ page/110/index.html | 532 ++ page/111/index.html | 497 ++ page/112/index.html | 507 ++ page/113/index.html | 507 ++ page/114/index.html | 507 ++ page/115/index.html | 507 ++ page/116/index.html | 554 ++ page/117/index.html | 507 ++ page/118/index.html | 507 ++ page/119/index.html | 507 ++ page/12/index.html | 512 ++ page/120/index.html | 512 ++ page/121/index.html | 507 ++ page/122/index.html | 507 ++ page/123/index.html | 507 ++ page/124/index.html | 507 ++ page/125/index.html | 604 ++ page/126/index.html | 696 ++ page/127/index.html | 507 ++ page/128/index.html | 526 ++ page/13/index.html | 582 ++ page/14/index.html | 641 ++ page/15/index.html | 531 ++ page/16/index.html | 521 ++ page/17/index.html | 503 ++ page/18/index.html | 504 ++ page/19/index.html | 545 ++ page/2/index.html | 477 + page/20/index.html | 508 ++ page/21/index.html | 498 ++ page/22/index.html | 508 ++ page/23/index.html | 613 ++ page/24/index.html | 567 ++ page/25/index.html | 736 ++ page/26/index.html | 645 ++ page/27/index.html | 570 ++ page/28/index.html | 641 ++ page/29/index.html | 548 ++ page/3/index.html | 499 ++ page/30/index.html | 519 ++ page/31/index.html | 501 ++ page/32/index.html | 508 ++ page/33/index.html | 507 ++ page/34/index.html | 507 ++ page/35/index.html | 507 ++ page/36/index.html | 509 ++ page/37/index.html | 508 ++ page/38/index.html | 507 ++ page/39/index.html | 523 ++ page/4/index.html | 499 ++ page/40/index.html | 576 ++ page/41/index.html | 504 ++ page/42/index.html | 507 ++ page/43/index.html | 503 ++ page/44/index.html | 510 ++ page/45/index.html | 508 ++ page/46/index.html | 505 ++ page/47/index.html | 504 ++ page/48/index.html | 507 ++ page/49/index.html | 506 ++ page/5/index.html | 565 ++ page/50/index.html | 784 ++ page/51/index.html | 531 ++ page/52/index.html | 497 ++ page/53/index.html | 505 ++ page/54/index.html | 498 ++ page/55/index.html | 502 ++ page/56/index.html | 501 ++ page/57/index.html | 496 ++ page/58/index.html | 500 ++ page/59/index.html | 497 ++ page/6/index.html | 519 ++ page/60/index.html | 504 ++ page/61/index.html | 500 ++ page/62/index.html | 500 ++ page/63/index.html | 512 ++ page/64/index.html | 519 ++ page/65/index.html | 558 ++ page/66/index.html | 606 ++ page/67/index.html | 564 ++ page/68/index.html | 500 ++ page/69/index.html | 504 ++ page/7/index.html | 503 ++ page/70/index.html | 513 ++ page/71/index.html | 518 ++ page/72/index.html | 507 ++ page/73/index.html | 507 ++ page/74/index.html | 497 ++ page/75/index.html | 507 ++ page/76/index.html | 503 ++ page/77/index.html | 580 ++ page/78/index.html | 536 ++ page/79/index.html | 512 ++ page/8/index.html | 778 ++ page/80/index.html | 507 ++ page/81/index.html | 503 ++ page/82/index.html | 617 ++ page/83/index.html | 880 ++ page/84/index.html | 507 ++ page/85/index.html | 508 ++ page/86/index.html | 515 ++ page/87/index.html | 528 ++ page/88/index.html | 634 ++ page/89/index.html | 541 ++ page/9/index.html | 514 ++ page/90/index.html | 497 ++ page/91/index.html | 502 ++ page/92/index.html | 516 ++ page/93/index.html | 515 ++ page/94/index.html | 502 ++ page/95/index.html | 505 ++ page/96/index.html | 509 ++ page/97/index.html | 510 ++ page/98/index.html | 518 ++ page/99/index.html | 514 ++ placeholder | 0 search.xml | 7826 +++++++++++++++++ tags/Actions/index.html | 458 + tags/CI-CD/index.html | 458 + tags/CSS/index.html | 458 + tags/CSS3/index.html | 458 + "tags/CSS\346\212\200\345\267\247/index.html" | 458 + tags/CodeReview/index.html | 458 + tags/Cordova/index.html | 458 + tags/Docker-Compose/index.html | 458 + tags/Docker/index.html | 478 + tags/DockerFile/index.html | 458 + tags/ESLint/index.html | 458 + tags/Electron/index.html | 481 + tags/EventLoop/index.html | 458 + tags/GIT/index.html | 458 + tags/Git/index.html | 458 + tags/GitLab/index.html | 458 + tags/Github/index.html | 458 + "tags/Go\350\257\255\350\250\200/index.html" | 458 + tags/Hexo/index.html | 547 ++ tags/HomeBrew/index.html | 458 + tags/Ionic/index.html | 458 + tags/Linux/index.html | 458 + tags/Mac/index.html | 458 + tags/Nginx/index.html | 478 + tags/NodeJS/index.html | 501 ++ tags/Postgresql/index.html | 458 + tags/Prettier/index.html | 458 + tags/RESTful/index.html | 458 + tags/RPM/index.html | 458 + tags/React/index.html | 458 + tags/Restful-API/index.html | 458 + tags/SSR/index.html | 458 + tags/SemVer/index.html | 458 + tags/Squid/index.html | 458 + tags/Timeout/index.html | 458 + tags/UglifyJsPlugin/index.html | 458 + tags/VSCode/index.html | 458 + .../index.html" | 458 + tags/Vue/index.html | 501 ++ tags/Webpack/index.html | 458 + tags/ajax/index.html | 458 + tags/es6/index.html | 458 + tags/get/index.html | 458 + tags/index.html | 458 + tags/jsonp/index.html | 458 + tags/kernel/index.html | 458 + tags/npm/index.html | 458 + tags/npx/index.html | 458 + tags/path-intellisense/index.html | 458 + tags/port/index.html | 458 + tags/promise/index.html | 458 + tags/registry/index.html | 458 + tags/rnpm/index.html | 458 + tags/sass/index.html | 458 + tags/scp/index.html | 458 + tags/systemctl/index.html | 458 + tags/vuepress/index.html | 458 + .../index.html" | 458 + .../index.html" | 481 + .../index.html" | 458 + .../index.html" | 458 + .../index.html" | 458 + .../index.html" | 458 + .../index.html" | 458 + .../index.html" | 478 + .../index.html" | 458 + .../index.html" | 458 + "tags/\345\276\256\344\277\241/index.html" | 458 + "tags/\346\211\223\345\214\205/index.html" | 458 + .../index.html" | 458 + "tags/\346\217\222\344\273\266/index.html" | 481 + .../index.html" | 458 + "tags/\346\225\260\347\273\204/index.html" | 458 + .../index.html" | 458 + "tags/\346\236\266\346\236\204/index.html" | 458 + .../index.html" | 458 + .../index.html" | 458 + .../index.html" | 458 + .../index.html" | 558 ++ .../index.html" | 458 + .../index.html" | 481 + .../index.html" | 481 + .../index.html" | 458 + .../index.html" | 458 + .../index.html" | 458 + .../index.html" | 641 ++ .../page/2/index.html" | 461 + .../index.html" | 458 + .../index.html" | 458 + .../index.html" | 458 + .../index.html" | 458 + "tags/\351\235\242\350\257\225/index.html" | 644 ++ .../page/2/index.html" | 641 ++ .../page/3/index.html" | 624 ++ 1282 files changed, 703494 insertions(+) create mode 100644 2013/06/17/Read-High-Performance-JavaScript/index.html create mode 100644 2013/06/26/CSS-Triangle-Circle/index.html create mode 100644 2015/12/31/Read-Let-your-boss-promote-you/index.html create mode 100644 2016/10/27/Read-The-miracle-of-the-morning-journal/index.html create mode 100644 2017/08/29/browser-incompatibility-problem-solution/index.html create mode 100644 2017/08/30/Vue-VSCode-Snippets/index.html create mode 100644 2017/08/30/Vue-basic/index.html create mode 100644 2017/08/30/npm-source/index.html create mode 100644 2017/10/11/use-GitHub-Hexo-Next-make-blog/index.html create mode 100644 2017/10/12/VSCode-ESLint/index.html create mode 100644 2017/10/12/us-gitment/index.html create mode 100644 2017/10/12/web-quanzhan/index.html create mode 100644 2017/12/12/Git-Shell/index.html create mode 100644 2019/12/27/Actions/index.html create mode 100644 2019/12/27/Vue-Component-Communication/index.html create mode 100644 2019/12/30/begin-learn-java/index.html create mode 100644 2019/12/31/GoodBye-2019/index.html create mode 100644 2019/12/31/gitlab-cicd/index.html create mode 100644 2019/12/31/weichat-h5-compatibility/index.html create mode 100644 2020/01/01/Hello-2020/index.html create mode 100644 2020/01/02/21-Day-Challenge-01/index.html create mode 100644 2020/01/02/FE-guide-Closure/index.html create mode 100644 2020/01/02/FE-guide-dataType/index.html create mode 100644 2020/01/02/FE-guide-instanceof/index.html create mode 100644 2020/01/02/FE-guide-new/index.html create mode 100644 2020/01/02/FE-guide-prototype/index.html create mode 100644 2020/01/02/FE-guide-this/index.html create mode 100644 "2020/01/02/FE-guide-\346\211\247\350\241\214\344\270\212\344\270\213\346\226\207/index.html" create mode 100644 2020/01/03/FE-guide-Generator/index.html create mode 100644 2020/01/03/FE-guide-Map-FlatMap-Reduce/index.html create mode 100644 2020/01/03/FE-guide-Module/index.html create mode 100644 2020/01/03/FE-guide-Promise/index.html create mode 100644 2020/01/03/FE-guide-async-Proxy/index.html create mode 100644 2020/01/03/FE-guide-async-await/index.html create mode 100644 2020/01/03/FE-guide-call-apply-bind/index.html create mode 100644 2020/01/03/FE-guide-copy/index.html create mode 100644 2020/01/03/FE-guide-debounce-throttle/index.html create mode 100644 2020/01/03/FE-guide-inherit/index.html create mode 100644 2020/01/03/FE-guide-store/index.html create mode 100644 2020/01/03/why-0.1-plus-0.2-not-equals-0.3/index.html create mode 100644 2020/01/04/Algorithm/index.html create mode 100644 2020/01/05/FE-guide-ArrayOprs/index.html create mode 100644 2020/01/05/FE-guide-about-reduce/index.html create mode 100644 2020/01/05/FE-guide-currying/index.html create mode 100644 2020/01/05/FE-guide-vuepress/index.html create mode 100644 2020/01/09/FE-guide-Net/index.html create mode 100644 2020/01/09/FE-guide-data-structure/index.html create mode 100644 2020/01/09/career/index.html create mode 100644 2020/01/09/do-it-yourselfery-jsonp/index.html create mode 100644 2020/01/09/solve-get-params-so-long-problem/index.html create mode 100644 2020/01/13/FE-guide-inherit2/index.html create mode 100644 2020/01/13/Javascript-Design-Pattern/index.html create mode 100644 2020/01/13/do-it-yourselfery-promise/index.html create mode 100644 2020/01/13/hexo-search/index.html create mode 100644 2020/01/22/Es6-arrowFunc-arguments/index.html create mode 100644 2020/01/22/FE-guide-safe/index.html create mode 100644 2020/01/22/Wepack-Tips/index.html create mode 100644 2020/02/11/SSR/index.html create mode 100644 2020/02/11/restful-architecture/index.html create mode 100644 2020/02/12/print-table-problem/index.html create mode 100644 2020/02/12/vue-proxyTable-problem/index.html create mode 100644 2020/02/19/develop-custom-cli-tools-using-node/index.html create mode 100644 2020/02/21/Implementation-of-the-vue-response-principle/index.html create mode 100644 2020/02/23/V8-engine-memory-management-and-optimization/index.html create mode 100644 2020/02/24/Vue-plug-in-development/index.html create mode 100644 2020/03/03/Speeding-up-brew-with-Ali-mirroring/index.html create mode 100644 2020/03/23/path-join-vs-path-resolve/index.html create mode 100644 2020/03/24/do-it-yourselfery-EventEmitter/index.html create mode 100644 2020/03/24/do-it-yourselfery-async-await/index.html create mode 100644 2020/03/24/do-it-yourselfery-call-apply/index.html create mode 100644 2020/03/24/do-it-yourselfery-create/index.html create mode 100644 2020/03/24/do-it-yourselfery-flat/index.html create mode 100644 2020/03/24/do-it-yourselfery-isArray/index.html create mode 100644 2020/03/24/do-it-yourselfery-map/index.html create mode 100644 2020/03/24/do-it-yourselfery-new/index.html create mode 100644 2020/03/24/do-it-yourselfery-reduce/index.html create mode 100644 "2020/03/24/do-it-yourselfery-\344\272\213\344\273\266\344\273\243\347\220\206/index.html" create mode 100644 "2020/03/24/do-it-yourselfery-\345\217\214\345\220\221\347\273\221\345\256\232/index.html" create mode 100644 2020/03/24/webpack-learning-1/index.html create mode 100644 2020/04/03/source-code-react-diff/index.html create mode 100644 2020/04/07/Electron-Offline-Build/index.html create mode 100644 2020/04/07/webpack-uglifyjsplugin/index.html create mode 100644 2020/04/15/node-sass-slow-problem/index.html create mode 100644 2020/04/15/talk-about-full-stack-big-fd/index.html create mode 100644 2020/04/16/0-vuecli-3-component/index.html create mode 100644 2020/04/16/what-is-npx/index.html create mode 100644 2020/05/14/css3-pointer-events/index.html create mode 100644 2020/05/28/Prettier-Setting/index.html create mode 100644 "2020/07/19/rpm\345\221\275\344\273\244/index.html" create mode 100644 "2020/08/02/Linux\347\233\256\345\275\225\350\257\264\346\230\216/index.html" create mode 100644 2020/11/10/learn-cordova-1/index.html create mode 100644 2020/11/10/learn-go/index.html create mode 100644 2020/11/11/learn-ionic-1/index.html create mode 100644 "2021/05/08/\345\205\263\344\272\216VSCode\344\270\255\345\210\253\345\220\215\357\274\210-\357\274\211\347\232\204\344\275\277\347\224\250/index.html" create mode 100644 "2021/05/13/\345\205\263\344\272\216\347\211\210\346\234\254\347\256\241\347\220\206/index.html" create mode 100644 "2022/03/11/\346\220\236\346\207\202EventLoop/index.html" create mode 100644 "2022/03/11/\346\225\260\347\273\204\346\226\271\346\263\225\344\270\200\347\233\256\344\272\206\347\204\266/index.html" create mode 100644 "2022/06/01/CodeReview\344\273\243\347\240\201\345\256\241\346\237\245\346\214\207\345\215\227/index.html" create mode 100644 "2022/06/01/Electron\347\274\226\347\240\201\350\247\204\350\214\203/index.html" create mode 100644 2022/06/01/GIT Standard/index.html create mode 100644 "2022/06/01/Restful API \347\232\204\350\256\276\350\256\241\350\247\204\350\214\203/index.html" create mode 100644 2022/06/01/squid/index.html create mode 100644 "2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\346\214\207\345\215\227/index.html" create mode 100644 "2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\350\247\204\350\214\203/index.html" create mode 100644 "2022/06/01/\345\211\215\347\253\257\345\274\200\345\217\221\346\217\222\344\273\266/index.html" create mode 100644 2022/06/02/DockerFile/index.html create mode 100644 "2022/06/02/Docker\345\221\275\344\273\244/index.html" create mode 100644 "2022/06/02/Docker\345\256\271\345\231\250\347\253\257\345\217\243\346\230\240\345\260\204/index.html" create mode 100644 "2022/06/02/Docker\346\225\260\346\215\256\345\215\267/index.html" create mode 100644 "2022/06/02/Docker\347\275\221\347\273\234\346\250\241\345\274\217/index.html" create mode 100644 "2022/06/09/Hexo\345\234\250Github\345\217\221\345\270\203\344\271\213\345\220\216\350\207\252\345\256\232\344\271\211\345\237\237\345\220\215\347\232\204\351\205\215\347\275\256/index.html" create mode 100644 "2022/06/09/Nginx\346\214\207\344\273\244/index.html" create mode 100644 2022/06/09/http_stub_status_module/index.html create mode 100644 "2022/06/15/\346\236\266\346\236\204\345\256\236\350\267\265/index.html" create mode 100644 "2022/06/16/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206 ---- \347\275\221\347\273\234\345\256\211\345\205\250\357\274\2102\357\274\211/index.html" create mode 100644 2022/06/30/nginx-timeout/index.html create mode 100644 "2022/08/02/Docker \351\225\234\345\203\217\345\256\211\345\205\250\346\234\200\344\275\263\345\256\236\350\267\265 /index.html" create mode 100644 "2022/08/02/Network in Compose \345\234\250Docker Compose\344\270\255\351\205\215\347\275\256\347\275\221\347\273\234/index.html" create mode 100644 "2022/08/02/Nginx\344\270\255\347\232\204\350\266\205\346\227\266\350\256\276\347\275\256/index.html" create mode 100644 2022/08/02/Oracle-Linux-install-Docker/index.html create mode 100644 "2022/08/02/OracleLinux\345\206\205\346\240\270\344\273\216uek\345\210\207\346\215\242\345\210\260uek/index.html" create mode 100644 "2022/08/02/Postgresql\347\273\277\350\211\262\347\211\210\344\275\277\347\224\250/index.html" create mode 100644 2022/08/02/linux-scp/index.html create mode 100644 "2022/08/02/systemctl \345\221\275\344\273\244/index.html" create mode 100644 "2022/08/02/\346\220\255\345\273\272Docker\347\247\201\346\234\211\344\273\223\345\272\223/index.html" create mode 100644 "2022/08/02/\346\234\215\345\212\241\345\231\250\351\253\230\345\215\261\347\253\257\345\217\243\345\210\227\350\241\250/index.html" create mode 100644 "2023/09/20/DockerCompose\345\256\211\350\243\205/index.html" create mode 100644 2023/09/20/test/index.html create mode 100644 @attachment/Clipboard_2022-06-09-12-57-39.png create mode 100644 @attachment/Clipboard_2022-06-09-12-57-48.png create mode 100644 @attachment/Clipboard_2022-06-09-12-59-28.png create mode 100644 @attachment/README.html create mode 100644 CNAME create mode 100644 about/index.html create mode 100644 archives/2013/06/index.html create mode 100644 archives/2013/index.html create mode 100644 archives/2015/12/index.html create mode 100644 archives/2015/index.html create mode 100644 archives/2016/10/index.html create mode 100644 archives/2016/index.html create mode 100644 archives/2017/08/index.html create mode 100644 archives/2017/10/index.html create mode 100644 archives/2017/12/index.html create mode 100644 archives/2017/index.html create mode 100644 archives/2019/12/index.html create mode 100644 archives/2019/index.html create mode 100644 archives/2020/01/index.html create mode 100644 archives/2020/01/page/2/index.html create mode 100644 archives/2020/01/page/3/index.html create mode 100644 archives/2020/01/page/4/index.html create mode 100644 archives/2020/02/index.html create mode 100644 archives/2020/03/index.html create mode 100644 archives/2020/03/page/2/index.html create mode 100644 archives/2020/04/index.html create mode 100644 archives/2020/05/index.html create mode 100644 archives/2020/07/index.html create mode 100644 archives/2020/08/index.html create mode 100644 archives/2020/11/index.html create mode 100644 archives/2020/index.html create mode 100644 archives/2020/page/2/index.html create mode 100644 archives/2020/page/3/index.html create mode 100644 archives/2020/page/4/index.html create mode 100644 archives/2020/page/5/index.html create mode 100644 archives/2020/page/6/index.html create mode 100644 archives/2020/page/7/index.html create mode 100644 archives/2020/page/8/index.html create mode 100644 archives/2021/05/index.html create mode 100644 archives/2021/index.html create mode 100644 archives/2022/03/index.html create mode 100644 archives/2022/06/index.html create mode 100644 archives/2022/06/page/2/index.html create mode 100644 archives/2022/08/index.html create mode 100644 archives/2022/index.html create mode 100644 archives/2022/page/2/index.html create mode 100644 archives/2022/page/3/index.html create mode 100644 archives/2022/page/4/index.html create mode 100644 archives/2023/09/index.html create mode 100644 archives/2023/index.html create mode 100644 archives/index.html create mode 100644 archives/page/10/index.html create mode 100644 archives/page/11/index.html create mode 100644 archives/page/12/index.html create mode 100644 archives/page/13/index.html create mode 100644 archives/page/2/index.html create mode 100644 archives/page/3/index.html create mode 100644 archives/page/4/index.html create mode 100644 archives/page/5/index.html create mode 100644 archives/page/6/index.html create mode 100644 archives/page/7/index.html create mode 100644 archives/page/8/index.html create mode 100644 archives/page/9/index.html create mode 100644 categories/Database/Postgresql/index.html create mode 100644 categories/Database/index.html create mode 100644 categories/Docker/index.html create mode 100644 categories/Infra/Docker/index.html create mode 100644 "categories/Infra/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" create mode 100644 "categories/Infra/Web\346\234\215\345\212\241\345\231\250/Squid/index.html" create mode 100644 "categories/Infra/Web\346\234\215\345\212\241\345\231\250/index.html" create mode 100644 categories/Infra/index.html create mode 100644 categories/Infrastructure/index.html create mode 100644 categories/Linux/Oracle/Docker/index.html create mode 100644 categories/Linux/Oracle/index.html create mode 100644 categories/Linux/RPM/index.html create mode 100644 categories/Linux/index.html create mode 100644 categories/NodeJS/Npm/index.html create mode 100644 categories/NodeJS/index.html create mode 100644 categories/OracleLinux/index.html create mode 100644 "categories/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" create mode 100644 "categories/Web\346\234\215\345\212\241\345\231\250/index.html" create mode 100644 categories/index.html create mode 100644 categories/linux/index.html create mode 100644 categories/linux/systemctl/index.html create mode 100644 "categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/Webpack/index.html" create mode 100644 "categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/index.html" create mode 100644 "categories/\345\211\215\347\253\257\345\274\200\345\217\221\350\247\204\350\214\203/index.html" create mode 100644 "categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS/index.html" create mode 100644 "categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS3/index.html" create mode 100644 "categories/\345\211\215\347\253\257\346\212\200\346\234\257/index.html" create mode 100644 "categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/2/index.html" create mode 100644 "categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/3/index.html" create mode 100644 "categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/4/index.html" create mode 100644 "categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/5/index.html" create mode 100644 "categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\205\274\345\256\271\346\200\247/index.html" create mode 100644 "categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/index.html" create mode 100644 "categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/2/index.html" create mode 100644 "categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/3/index.html" create mode 100644 "categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\351\227\256\351\242\230/index.html" create mode 100644 "categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\276\256\344\277\241/index.html" create mode 100644 "categories/\345\211\215\347\253\257\346\212\200\346\234\257/\350\256\241\347\256\227\346\234\272\351\200\232\350\257\206/index.html" create mode 100644 "categories/\345\211\215\347\253\257\346\241\206\346\236\266/React/index.html" create mode 100644 "categories/\345\211\215\347\253\257\346\241\206\346\236\266/Vue/index.html" create mode 100644 "categories/\345\211\215\347\253\257\346\241\206\346\236\266/index.html" create mode 100644 "categories/\345\220\216\347\253\257\345\274\200\345\217\221/Go\350\257\255\350\250\200/index.html" create mode 100644 "categories/\345\220\216\347\253\257\345\274\200\345\217\221/index.html" create mode 100644 "categories/\345\244\247\345\211\215\347\253\257/index.html" create mode 100644 "categories/\345\244\247\345\211\215\347\253\257/\345\205\250\346\240\210/index.html" create mode 100644 "categories/\345\244\247\345\211\215\347\253\257/\345\256\242\346\210\267\347\253\257\346\212\200\346\234\257/index.html" create mode 100644 "categories/\345\244\247\345\211\215\347\253\257/\347\247\273\345\212\250\345\274\200\345\217\221/index.html" create mode 100644 "categories/\346\225\210\347\216\207\345\267\245\345\205\267/Labrary/index.html" create mode 100644 "categories/\346\225\210\347\216\207\345\267\245\345\205\267/VSCode\346\217\222\344\273\266/index.html" create mode 100644 "categories/\346\225\210\347\216\207\345\267\245\345\205\267/index.html" create mode 100644 "categories/\346\235\202\344\270\203\346\235\202\345\205\253/index.html" create mode 100644 "categories/\346\235\202\344\270\203\346\235\202\345\205\253/\345\215\232\345\256\242\346\212\200\345\267\247/index.html" create mode 100644 "categories/\346\236\266\346\236\204/index.html" create mode 100644 "categories/\346\272\220\347\240\201\345\216\237\347\220\206/index.html" create mode 100644 "categories/\346\272\220\347\240\201\345\216\237\347\220\206/page/2/index.html" create mode 100644 "categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/index.html" create mode 100644 "categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/page/2/index.html" create mode 100644 "categories/\350\207\252\346\210\221\346\217\220\345\215\207/index.html" create mode 100644 "categories/\350\207\252\346\210\221\346\217\220\345\215\207/\344\272\272\347\224\237\346\200\235\350\200\203/index.html" create mode 100644 "categories/\350\207\252\346\210\221\346\217\220\345\215\207/\346\235\202\350\256\260\351\232\217\346\204\237/index.html" create mode 100644 "categories/\350\207\252\346\210\221\346\217\220\345\215\207/\350\257\273\344\271\246\347\254\224\350\256\260/index.html" create mode 100644 "categories/\350\231\232\346\213\237\345\214\226/Docker-Compose/index.html" create mode 100644 "categories/\350\231\232\346\213\237\345\214\226/index.html" create mode 100644 css/main.css create mode 100644 en/2013/06/17/Read-High-Performance-JavaScript/index.html create mode 100644 en/2013/06/26/CSS-Triangle-Circle/index.html create mode 100644 en/2015/12/31/Read-Let-your-boss-promote-you/index.html create mode 100644 en/2016/10/27/Read-The-miracle-of-the-morning-journal/index.html create mode 100644 en/2017/08/29/browser-incompatibility-problem-solution/index.html create mode 100644 en/2017/08/30/Vue-VSCode-Snippets/index.html create mode 100644 en/2017/08/30/Vue-basic/index.html create mode 100644 en/2017/08/30/npm-source/index.html create mode 100644 en/2017/10/11/use-GitHub-Hexo-Next-make-blog/index.html create mode 100644 en/2017/10/12/VSCode-ESLint/index.html create mode 100644 en/2017/10/12/us-gitment/index.html create mode 100644 en/2017/10/12/web-quanzhan/index.html create mode 100644 en/2017/12/12/Git-Shell/index.html create mode 100644 en/2019/12/27/Actions/index.html create mode 100644 en/2019/12/27/Vue-Component-Communication/index.html create mode 100644 en/2019/12/30/begin-learn-java/index.html create mode 100644 en/2019/12/31/GoodBye-2019/index.html create mode 100644 en/2019/12/31/gitlab-cicd/index.html create mode 100644 en/2019/12/31/weichat-h5-compatibility/index.html create mode 100644 en/2020/01/01/Hello-2020/index.html create mode 100644 en/2020/01/02/21-Day-Challenge-01/index.html create mode 100644 en/2020/01/02/FE-guide-Closure/index.html create mode 100644 en/2020/01/02/FE-guide-dataType/index.html create mode 100644 en/2020/01/02/FE-guide-instanceof/index.html create mode 100644 en/2020/01/02/FE-guide-new/index.html create mode 100644 en/2020/01/02/FE-guide-prototype/index.html create mode 100644 en/2020/01/02/FE-guide-this/index.html create mode 100644 "en/2020/01/02/FE-guide-\346\211\247\350\241\214\344\270\212\344\270\213\346\226\207/index.html" create mode 100644 en/2020/01/03/FE-guide-Generator/index.html create mode 100644 en/2020/01/03/FE-guide-Map-FlatMap-Reduce/index.html create mode 100644 en/2020/01/03/FE-guide-Module/index.html create mode 100644 en/2020/01/03/FE-guide-Promise/index.html create mode 100644 en/2020/01/03/FE-guide-async-Proxy/index.html create mode 100644 en/2020/01/03/FE-guide-async-await/index.html create mode 100644 en/2020/01/03/FE-guide-call-apply-bind/index.html create mode 100644 en/2020/01/03/FE-guide-copy/index.html create mode 100644 en/2020/01/03/FE-guide-debounce-throttle/index.html create mode 100644 en/2020/01/03/FE-guide-inherit/index.html create mode 100644 en/2020/01/03/FE-guide-store/index.html create mode 100644 en/2020/01/03/why-0.1-plus-0.2-not-equals-0.3/index.html create mode 100644 en/2020/01/04/Algorithm/index.html create mode 100644 en/2020/01/05/FE-guide-ArrayOprs/index.html create mode 100644 en/2020/01/05/FE-guide-about-reduce/index.html create mode 100644 en/2020/01/05/FE-guide-currying/index.html create mode 100644 en/2020/01/05/FE-guide-vuepress/index.html create mode 100644 en/2020/01/09/FE-guide-Net/index.html create mode 100644 en/2020/01/09/FE-guide-data-structure/index.html create mode 100644 en/2020/01/09/career/index.html create mode 100644 en/2020/01/09/do-it-yourselfery-jsonp/index.html create mode 100644 en/2020/01/09/solve-get-params-so-long-problem/index.html create mode 100644 en/2020/01/13/FE-guide-inherit2/index.html create mode 100644 en/2020/01/13/Javascript-Design-Pattern/index.html create mode 100644 en/2020/01/13/do-it-yourselfery-promise/index.html create mode 100644 en/2020/01/13/hexo-search/index.html create mode 100644 en/2020/01/22/Es6-arrowFunc-arguments/index.html create mode 100644 en/2020/01/22/FE-guide-safe/index.html create mode 100644 en/2020/01/22/Wepack-Tips/index.html create mode 100644 en/2020/02/11/SSR/index.html create mode 100644 en/2020/02/11/restful-architecture/index.html create mode 100644 en/2020/02/12/print-table-problem/index.html create mode 100644 en/2020/02/12/vue-proxyTable-problem/index.html create mode 100644 en/2020/02/19/develop-custom-cli-tools-using-node/index.html create mode 100644 en/2020/02/21/Implementation-of-the-vue-response-principle/index.html create mode 100644 en/2020/02/23/V8-engine-memory-management-and-optimization/index.html create mode 100644 en/2020/02/24/Vue-plug-in-development/index.html create mode 100644 en/2020/03/03/Speeding-up-brew-with-Ali-mirroring/index.html create mode 100644 en/2020/03/23/path-join-vs-path-resolve/index.html create mode 100644 en/2020/03/24/do-it-yourselfery-EventEmitter/index.html create mode 100644 en/2020/03/24/do-it-yourselfery-async-await/index.html create mode 100644 en/2020/03/24/do-it-yourselfery-call-apply/index.html create mode 100644 en/2020/03/24/do-it-yourselfery-create/index.html create mode 100644 en/2020/03/24/do-it-yourselfery-flat/index.html create mode 100644 en/2020/03/24/do-it-yourselfery-isArray/index.html create mode 100644 en/2020/03/24/do-it-yourselfery-map/index.html create mode 100644 en/2020/03/24/do-it-yourselfery-new/index.html create mode 100644 en/2020/03/24/do-it-yourselfery-reduce/index.html create mode 100644 "en/2020/03/24/do-it-yourselfery-\344\272\213\344\273\266\344\273\243\347\220\206/index.html" create mode 100644 "en/2020/03/24/do-it-yourselfery-\345\217\214\345\220\221\347\273\221\345\256\232/index.html" create mode 100644 en/2020/03/24/webpack-learning-1/index.html create mode 100644 en/2020/04/03/source-code-react-diff/index.html create mode 100644 en/2020/04/07/Electron-Offline-Build/index.html create mode 100644 en/2020/04/07/webpack-uglifyjsplugin/index.html create mode 100644 en/2020/04/15/node-sass-slow-problem/index.html create mode 100644 en/2020/04/15/talk-about-full-stack-big-fd/index.html create mode 100644 en/2020/04/16/0-vuecli-3-component/index.html create mode 100644 en/2020/04/16/what-is-npx/index.html create mode 100644 en/2020/05/14/css3-pointer-events/index.html create mode 100644 en/2020/05/28/Prettier-Setting/index.html create mode 100644 "en/2020/07/19/rpm\345\221\275\344\273\244/index.html" create mode 100644 "en/2020/08/02/Linux\347\233\256\345\275\225\350\257\264\346\230\216/index.html" create mode 100644 en/2020/11/10/learn-cordova-1/index.html create mode 100644 en/2020/11/10/learn-go/index.html create mode 100644 en/2020/11/11/learn-ionic-1/index.html create mode 100644 "en/2021/05/08/\345\205\263\344\272\216VSCode\344\270\255\345\210\253\345\220\215\357\274\210-\357\274\211\347\232\204\344\275\277\347\224\250/index.html" create mode 100644 "en/2021/05/13/\345\205\263\344\272\216\347\211\210\346\234\254\347\256\241\347\220\206/index.html" create mode 100644 "en/2022/03/11/\346\220\236\346\207\202EventLoop/index.html" create mode 100644 "en/2022/03/11/\346\225\260\347\273\204\346\226\271\346\263\225\344\270\200\347\233\256\344\272\206\347\204\266/index.html" create mode 100644 "en/2022/06/01/CodeReview\344\273\243\347\240\201\345\256\241\346\237\245\346\214\207\345\215\227/index.html" create mode 100644 "en/2022/06/01/Electron\347\274\226\347\240\201\350\247\204\350\214\203/index.html" create mode 100644 en/2022/06/01/GIT Standard/index.html create mode 100644 "en/2022/06/01/Restful API \347\232\204\350\256\276\350\256\241\350\247\204\350\214\203/index.html" create mode 100644 en/2022/06/01/squid/index.html create mode 100644 "en/2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\346\214\207\345\215\227/index.html" create mode 100644 "en/2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\350\247\204\350\214\203/index.html" create mode 100644 "en/2022/06/01/\345\211\215\347\253\257\345\274\200\345\217\221\346\217\222\344\273\266/index.html" create mode 100644 en/2022/06/02/DockerFile/index.html create mode 100644 "en/2022/06/02/Docker\345\221\275\344\273\244/index.html" create mode 100644 "en/2022/06/02/Docker\345\256\271\345\231\250\347\253\257\345\217\243\346\230\240\345\260\204/index.html" create mode 100644 "en/2022/06/02/Docker\346\225\260\346\215\256\345\215\267/index.html" create mode 100644 "en/2022/06/02/Docker\347\275\221\347\273\234\346\250\241\345\274\217/index.html" create mode 100644 "en/2022/06/09/Hexo\345\234\250Github\345\217\221\345\270\203\344\271\213\345\220\216\350\207\252\345\256\232\344\271\211\345\237\237\345\220\215\347\232\204\351\205\215\347\275\256/index.html" create mode 100644 "en/2022/06/09/Nginx\346\214\207\344\273\244/index.html" create mode 100644 en/2022/06/09/http_stub_status_module/index.html create mode 100644 "en/2022/06/15/\346\236\266\346\236\204\345\256\236\350\267\265/index.html" create mode 100644 "en/2022/06/16/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206 ---- \347\275\221\347\273\234\345\256\211\345\205\250\357\274\2102\357\274\211/index.html" create mode 100644 en/2022/06/30/nginx-timeout/index.html create mode 100644 "en/2022/08/02/Docker \351\225\234\345\203\217\345\256\211\345\205\250\346\234\200\344\275\263\345\256\236\350\267\265 /index.html" create mode 100644 "en/2022/08/02/Network in Compose \345\234\250Docker Compose\344\270\255\351\205\215\347\275\256\347\275\221\347\273\234/index.html" create mode 100644 "en/2022/08/02/Nginx\344\270\255\347\232\204\350\266\205\346\227\266\350\256\276\347\275\256/index.html" create mode 100644 en/2022/08/02/Oracle-Linux-install-Docker/index.html create mode 100644 "en/2022/08/02/OracleLinux\345\206\205\346\240\270\344\273\216uek\345\210\207\346\215\242\345\210\260uek/index.html" create mode 100644 "en/2022/08/02/Postgresql\347\273\277\350\211\262\347\211\210\344\275\277\347\224\250/index.html" create mode 100644 en/2022/08/02/linux-scp/index.html create mode 100644 "en/2022/08/02/systemctl \345\221\275\344\273\244/index.html" create mode 100644 "en/2022/08/02/\346\220\255\345\273\272Docker\347\247\201\346\234\211\344\273\223\345\272\223/index.html" create mode 100644 "en/2022/08/02/\346\234\215\345\212\241\345\231\250\351\253\230\345\215\261\347\253\257\345\217\243\345\210\227\350\241\250/index.html" create mode 100644 "en/2023/09/20/DockerCompose\345\256\211\350\243\205/index.html" create mode 100644 en/2023/09/20/test/index.html create mode 100644 en/@attachment/README.html create mode 100644 en/about/index.html create mode 100644 en/archives/2013/06/index.html create mode 100644 en/archives/2013/index.html create mode 100644 en/archives/2015/12/index.html create mode 100644 en/archives/2015/index.html create mode 100644 en/archives/2016/10/index.html create mode 100644 en/archives/2016/index.html create mode 100644 en/archives/2017/08/index.html create mode 100644 en/archives/2017/10/index.html create mode 100644 en/archives/2017/12/index.html create mode 100644 en/archives/2017/index.html create mode 100644 en/archives/2019/12/index.html create mode 100644 en/archives/2019/index.html create mode 100644 en/archives/2020/01/index.html create mode 100644 en/archives/2020/01/page/2/index.html create mode 100644 en/archives/2020/01/page/3/index.html create mode 100644 en/archives/2020/01/page/4/index.html create mode 100644 en/archives/2020/02/index.html create mode 100644 en/archives/2020/03/index.html create mode 100644 en/archives/2020/03/page/2/index.html create mode 100644 en/archives/2020/04/index.html create mode 100644 en/archives/2020/05/index.html create mode 100644 en/archives/2020/07/index.html create mode 100644 en/archives/2020/08/index.html create mode 100644 en/archives/2020/11/index.html create mode 100644 en/archives/2020/index.html create mode 100644 en/archives/2020/page/2/index.html create mode 100644 en/archives/2020/page/3/index.html create mode 100644 en/archives/2020/page/4/index.html create mode 100644 en/archives/2020/page/5/index.html create mode 100644 en/archives/2020/page/6/index.html create mode 100644 en/archives/2020/page/7/index.html create mode 100644 en/archives/2020/page/8/index.html create mode 100644 en/archives/2021/05/index.html create mode 100644 en/archives/2021/index.html create mode 100644 en/archives/2022/03/index.html create mode 100644 en/archives/2022/06/index.html create mode 100644 en/archives/2022/06/page/2/index.html create mode 100644 en/archives/2022/08/index.html create mode 100644 en/archives/2022/index.html create mode 100644 en/archives/2022/page/2/index.html create mode 100644 en/archives/2022/page/3/index.html create mode 100644 en/archives/2022/page/4/index.html create mode 100644 en/archives/2023/09/index.html create mode 100644 en/archives/2023/index.html create mode 100644 en/archives/index.html create mode 100644 en/archives/page/10/index.html create mode 100644 en/archives/page/11/index.html create mode 100644 en/archives/page/12/index.html create mode 100644 en/archives/page/13/index.html create mode 100644 en/archives/page/2/index.html create mode 100644 en/archives/page/3/index.html create mode 100644 en/archives/page/4/index.html create mode 100644 en/archives/page/5/index.html create mode 100644 en/archives/page/6/index.html create mode 100644 en/archives/page/7/index.html create mode 100644 en/archives/page/8/index.html create mode 100644 en/archives/page/9/index.html create mode 100644 en/categories/Database/Postgresql/index.html create mode 100644 en/categories/Database/index.html create mode 100644 en/categories/Docker/index.html create mode 100644 en/categories/Infra/Docker/index.html create mode 100644 "en/categories/Infra/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" create mode 100644 "en/categories/Infra/Web\346\234\215\345\212\241\345\231\250/Squid/index.html" create mode 100644 "en/categories/Infra/Web\346\234\215\345\212\241\345\231\250/index.html" create mode 100644 en/categories/Infra/index.html create mode 100644 en/categories/Infrastructure/index.html create mode 100644 en/categories/Linux/Oracle/Docker/index.html create mode 100644 en/categories/Linux/Oracle/index.html create mode 100644 en/categories/Linux/RPM/index.html create mode 100644 en/categories/Linux/index.html create mode 100644 en/categories/NodeJS/Npm/index.html create mode 100644 en/categories/NodeJS/index.html create mode 100644 en/categories/OracleLinux/index.html create mode 100644 "en/categories/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" create mode 100644 "en/categories/Web\346\234\215\345\212\241\345\231\250/index.html" create mode 100644 en/categories/index.html create mode 100644 en/categories/linux/index.html create mode 100644 en/categories/linux/systemctl/index.html create mode 100644 "en/categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/Webpack/index.html" create mode 100644 "en/categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/index.html" create mode 100644 "en/categories/\345\211\215\347\253\257\345\274\200\345\217\221\350\247\204\350\214\203/index.html" create mode 100644 "en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS/index.html" create mode 100644 "en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS3/index.html" create mode 100644 "en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/index.html" create mode 100644 "en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/2/index.html" create mode 100644 "en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/3/index.html" create mode 100644 "en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/4/index.html" create mode 100644 "en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/5/index.html" create mode 100644 "en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\205\274\345\256\271\346\200\247/index.html" create mode 100644 "en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/index.html" create mode 100644 "en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/2/index.html" create mode 100644 "en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/3/index.html" create mode 100644 "en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\351\227\256\351\242\230/index.html" create mode 100644 "en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\276\256\344\277\241/index.html" create mode 100644 "en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\350\256\241\347\256\227\346\234\272\351\200\232\350\257\206/index.html" create mode 100644 "en/categories/\345\211\215\347\253\257\346\241\206\346\236\266/React/index.html" create mode 100644 "en/categories/\345\211\215\347\253\257\346\241\206\346\236\266/Vue/index.html" create mode 100644 "en/categories/\345\211\215\347\253\257\346\241\206\346\236\266/index.html" create mode 100644 "en/categories/\345\220\216\347\253\257\345\274\200\345\217\221/Go\350\257\255\350\250\200/index.html" create mode 100644 "en/categories/\345\220\216\347\253\257\345\274\200\345\217\221/index.html" create mode 100644 "en/categories/\345\244\247\345\211\215\347\253\257/index.html" create mode 100644 "en/categories/\345\244\247\345\211\215\347\253\257/\345\205\250\346\240\210/index.html" create mode 100644 "en/categories/\345\244\247\345\211\215\347\253\257/\345\256\242\346\210\267\347\253\257\346\212\200\346\234\257/index.html" create mode 100644 "en/categories/\345\244\247\345\211\215\347\253\257/\347\247\273\345\212\250\345\274\200\345\217\221/index.html" create mode 100644 "en/categories/\346\225\210\347\216\207\345\267\245\345\205\267/Labrary/index.html" create mode 100644 "en/categories/\346\225\210\347\216\207\345\267\245\345\205\267/VSCode\346\217\222\344\273\266/index.html" create mode 100644 "en/categories/\346\225\210\347\216\207\345\267\245\345\205\267/index.html" create mode 100644 "en/categories/\346\235\202\344\270\203\346\235\202\345\205\253/index.html" create mode 100644 "en/categories/\346\235\202\344\270\203\346\235\202\345\205\253/\345\215\232\345\256\242\346\212\200\345\267\247/index.html" create mode 100644 "en/categories/\346\236\266\346\236\204/index.html" create mode 100644 "en/categories/\346\272\220\347\240\201\345\216\237\347\220\206/index.html" create mode 100644 "en/categories/\346\272\220\347\240\201\345\216\237\347\220\206/page/2/index.html" create mode 100644 "en/categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/index.html" create mode 100644 "en/categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/page/2/index.html" create mode 100644 "en/categories/\350\207\252\346\210\221\346\217\220\345\215\207/index.html" create mode 100644 "en/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\344\272\272\347\224\237\346\200\235\350\200\203/index.html" create mode 100644 "en/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\346\235\202\350\256\260\351\232\217\346\204\237/index.html" create mode 100644 "en/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\350\257\273\344\271\246\347\254\224\350\256\260/index.html" create mode 100644 "en/categories/\350\231\232\346\213\237\345\214\226/Docker-Compose/index.html" create mode 100644 "en/categories/\350\231\232\346\213\237\345\214\226/index.html" create mode 100644 en/index.html create mode 100644 en/page/10/index.html create mode 100644 en/page/100/index.html create mode 100644 en/page/101/index.html create mode 100644 en/page/102/index.html create mode 100644 en/page/103/index.html create mode 100644 en/page/104/index.html create mode 100644 en/page/105/index.html create mode 100644 en/page/106/index.html create mode 100644 en/page/107/index.html create mode 100644 en/page/108/index.html create mode 100644 en/page/109/index.html create mode 100644 en/page/11/index.html create mode 100644 en/page/110/index.html create mode 100644 en/page/111/index.html create mode 100644 en/page/112/index.html create mode 100644 en/page/113/index.html create mode 100644 en/page/114/index.html create mode 100644 en/page/115/index.html create mode 100644 en/page/116/index.html create mode 100644 en/page/117/index.html create mode 100644 en/page/118/index.html create mode 100644 en/page/119/index.html create mode 100644 en/page/12/index.html create mode 100644 en/page/120/index.html create mode 100644 en/page/121/index.html create mode 100644 en/page/122/index.html create mode 100644 en/page/123/index.html create mode 100644 en/page/124/index.html create mode 100644 en/page/125/index.html create mode 100644 en/page/126/index.html create mode 100644 en/page/127/index.html create mode 100644 en/page/128/index.html create mode 100644 en/page/13/index.html create mode 100644 en/page/14/index.html create mode 100644 en/page/15/index.html create mode 100644 en/page/16/index.html create mode 100644 en/page/17/index.html create mode 100644 en/page/18/index.html create mode 100644 en/page/19/index.html create mode 100644 en/page/2/index.html create mode 100644 en/page/20/index.html create mode 100644 en/page/21/index.html create mode 100644 en/page/22/index.html create mode 100644 en/page/23/index.html create mode 100644 en/page/24/index.html create mode 100644 en/page/25/index.html create mode 100644 en/page/26/index.html create mode 100644 en/page/27/index.html create mode 100644 en/page/28/index.html create mode 100644 en/page/29/index.html create mode 100644 en/page/3/index.html create mode 100644 en/page/30/index.html create mode 100644 en/page/31/index.html create mode 100644 en/page/32/index.html create mode 100644 en/page/33/index.html create mode 100644 en/page/34/index.html create mode 100644 en/page/35/index.html create mode 100644 en/page/36/index.html create mode 100644 en/page/37/index.html create mode 100644 en/page/38/index.html create mode 100644 en/page/39/index.html create mode 100644 en/page/4/index.html create mode 100644 en/page/40/index.html create mode 100644 en/page/41/index.html create mode 100644 en/page/42/index.html create mode 100644 en/page/43/index.html create mode 100644 en/page/44/index.html create mode 100644 en/page/45/index.html create mode 100644 en/page/46/index.html create mode 100644 en/page/47/index.html create mode 100644 en/page/48/index.html create mode 100644 en/page/49/index.html create mode 100644 en/page/5/index.html create mode 100644 en/page/50/index.html create mode 100644 en/page/51/index.html create mode 100644 en/page/52/index.html create mode 100644 en/page/53/index.html create mode 100644 en/page/54/index.html create mode 100644 en/page/55/index.html create mode 100644 en/page/56/index.html create mode 100644 en/page/57/index.html create mode 100644 en/page/58/index.html create mode 100644 en/page/59/index.html create mode 100644 en/page/6/index.html create mode 100644 en/page/60/index.html create mode 100644 en/page/61/index.html create mode 100644 en/page/62/index.html create mode 100644 en/page/63/index.html create mode 100644 en/page/64/index.html create mode 100644 en/page/65/index.html create mode 100644 en/page/66/index.html create mode 100644 en/page/67/index.html create mode 100644 en/page/68/index.html create mode 100644 en/page/69/index.html create mode 100644 en/page/7/index.html create mode 100644 en/page/70/index.html create mode 100644 en/page/71/index.html create mode 100644 en/page/72/index.html create mode 100644 en/page/73/index.html create mode 100644 en/page/74/index.html create mode 100644 en/page/75/index.html create mode 100644 en/page/76/index.html create mode 100644 en/page/77/index.html create mode 100644 en/page/78/index.html create mode 100644 en/page/79/index.html create mode 100644 en/page/8/index.html create mode 100644 en/page/80/index.html create mode 100644 en/page/81/index.html create mode 100644 en/page/82/index.html create mode 100644 en/page/83/index.html create mode 100644 en/page/84/index.html create mode 100644 en/page/85/index.html create mode 100644 en/page/86/index.html create mode 100644 en/page/87/index.html create mode 100644 en/page/88/index.html create mode 100644 en/page/89/index.html create mode 100644 en/page/9/index.html create mode 100644 en/page/90/index.html create mode 100644 en/page/91/index.html create mode 100644 en/page/92/index.html create mode 100644 en/page/93/index.html create mode 100644 en/page/94/index.html create mode 100644 en/page/95/index.html create mode 100644 en/page/96/index.html create mode 100644 en/page/97/index.html create mode 100644 en/page/98/index.html create mode 100644 en/page/99/index.html create mode 100644 en/tags/index.html create mode 100644 images/algolia_logo.svg create mode 100644 images/apple-touch-icon-next.png create mode 100644 images/avatar.gif create mode 100644 images/avatar.jpg create mode 100644 images/cc-by-nc-nd.svg create mode 100644 images/cc-by-nc-sa.svg create mode 100644 images/cc-by-nc.svg create mode 100644 images/cc-by-nd.svg create mode 100644 images/cc-by-sa.svg create mode 100644 images/cc-by.svg create mode 100644 images/cc-zero.svg create mode 100644 images/favicon-32x32-next.png create mode 100644 images/logo.svg create mode 100644 images/wechatpay.jpg create mode 100644 index.html create mode 100644 ja/2013/06/17/Read-High-Performance-JavaScript/index.html create mode 100644 ja/2013/06/26/CSS-Triangle-Circle/index.html create mode 100644 ja/2015/12/31/Read-Let-your-boss-promote-you/index.html create mode 100644 ja/2016/10/27/Read-The-miracle-of-the-morning-journal/index.html create mode 100644 ja/2017/08/29/browser-incompatibility-problem-solution/index.html create mode 100644 ja/2017/08/30/Vue-VSCode-Snippets/index.html create mode 100644 ja/2017/08/30/Vue-basic/index.html create mode 100644 ja/2017/08/30/npm-source/index.html create mode 100644 ja/2017/10/11/use-GitHub-Hexo-Next-make-blog/index.html create mode 100644 ja/2017/10/12/VSCode-ESLint/index.html create mode 100644 ja/2017/10/12/us-gitment/index.html create mode 100644 ja/2017/10/12/web-quanzhan/index.html create mode 100644 ja/2017/12/12/Git-Shell/index.html create mode 100644 ja/2019/12/27/Actions/index.html create mode 100644 ja/2019/12/27/Vue-Component-Communication/index.html create mode 100644 ja/2019/12/30/begin-learn-java/index.html create mode 100644 ja/2019/12/31/GoodBye-2019/index.html create mode 100644 ja/2019/12/31/gitlab-cicd/index.html create mode 100644 ja/2019/12/31/weichat-h5-compatibility/index.html create mode 100644 ja/2020/01/01/Hello-2020/index.html create mode 100644 ja/2020/01/02/21-Day-Challenge-01/index.html create mode 100644 ja/2020/01/02/FE-guide-Closure/index.html create mode 100644 ja/2020/01/02/FE-guide-dataType/index.html create mode 100644 ja/2020/01/02/FE-guide-instanceof/index.html create mode 100644 ja/2020/01/02/FE-guide-new/index.html create mode 100644 ja/2020/01/02/FE-guide-prototype/index.html create mode 100644 ja/2020/01/02/FE-guide-this/index.html create mode 100644 "ja/2020/01/02/FE-guide-\346\211\247\350\241\214\344\270\212\344\270\213\346\226\207/index.html" create mode 100644 ja/2020/01/03/FE-guide-Generator/index.html create mode 100644 ja/2020/01/03/FE-guide-Map-FlatMap-Reduce/index.html create mode 100644 ja/2020/01/03/FE-guide-Module/index.html create mode 100644 ja/2020/01/03/FE-guide-Promise/index.html create mode 100644 ja/2020/01/03/FE-guide-async-Proxy/index.html create mode 100644 ja/2020/01/03/FE-guide-async-await/index.html create mode 100644 ja/2020/01/03/FE-guide-call-apply-bind/index.html create mode 100644 ja/2020/01/03/FE-guide-copy/index.html create mode 100644 ja/2020/01/03/FE-guide-debounce-throttle/index.html create mode 100644 ja/2020/01/03/FE-guide-inherit/index.html create mode 100644 ja/2020/01/03/FE-guide-store/index.html create mode 100644 ja/2020/01/03/why-0.1-plus-0.2-not-equals-0.3/index.html create mode 100644 ja/2020/01/04/Algorithm/index.html create mode 100644 ja/2020/01/05/FE-guide-ArrayOprs/index.html create mode 100644 ja/2020/01/05/FE-guide-about-reduce/index.html create mode 100644 ja/2020/01/05/FE-guide-currying/index.html create mode 100644 ja/2020/01/05/FE-guide-vuepress/index.html create mode 100644 ja/2020/01/09/FE-guide-Net/index.html create mode 100644 ja/2020/01/09/FE-guide-data-structure/index.html create mode 100644 ja/2020/01/09/career/index.html create mode 100644 ja/2020/01/09/do-it-yourselfery-jsonp/index.html create mode 100644 ja/2020/01/09/solve-get-params-so-long-problem/index.html create mode 100644 ja/2020/01/13/FE-guide-inherit2/index.html create mode 100644 ja/2020/01/13/Javascript-Design-Pattern/index.html create mode 100644 ja/2020/01/13/do-it-yourselfery-promise/index.html create mode 100644 ja/2020/01/13/hexo-search/index.html create mode 100644 ja/2020/01/22/Es6-arrowFunc-arguments/index.html create mode 100644 ja/2020/01/22/FE-guide-safe/index.html create mode 100644 ja/2020/01/22/Wepack-Tips/index.html create mode 100644 ja/2020/02/11/SSR/index.html create mode 100644 ja/2020/02/11/restful-architecture/index.html create mode 100644 ja/2020/02/12/print-table-problem/index.html create mode 100644 ja/2020/02/12/vue-proxyTable-problem/index.html create mode 100644 ja/2020/02/19/develop-custom-cli-tools-using-node/index.html create mode 100644 ja/2020/02/21/Implementation-of-the-vue-response-principle/index.html create mode 100644 ja/2020/02/23/V8-engine-memory-management-and-optimization/index.html create mode 100644 ja/2020/02/24/Vue-plug-in-development/index.html create mode 100644 ja/2020/03/03/Speeding-up-brew-with-Ali-mirroring/index.html create mode 100644 ja/2020/03/23/path-join-vs-path-resolve/index.html create mode 100644 ja/2020/03/24/do-it-yourselfery-EventEmitter/index.html create mode 100644 ja/2020/03/24/do-it-yourselfery-async-await/index.html create mode 100644 ja/2020/03/24/do-it-yourselfery-call-apply/index.html create mode 100644 ja/2020/03/24/do-it-yourselfery-create/index.html create mode 100644 ja/2020/03/24/do-it-yourselfery-flat/index.html create mode 100644 ja/2020/03/24/do-it-yourselfery-isArray/index.html create mode 100644 ja/2020/03/24/do-it-yourselfery-map/index.html create mode 100644 ja/2020/03/24/do-it-yourselfery-new/index.html create mode 100644 ja/2020/03/24/do-it-yourselfery-reduce/index.html create mode 100644 "ja/2020/03/24/do-it-yourselfery-\344\272\213\344\273\266\344\273\243\347\220\206/index.html" create mode 100644 "ja/2020/03/24/do-it-yourselfery-\345\217\214\345\220\221\347\273\221\345\256\232/index.html" create mode 100644 ja/2020/03/24/webpack-learning-1/index.html create mode 100644 ja/2020/04/03/source-code-react-diff/index.html create mode 100644 ja/2020/04/07/Electron-Offline-Build/index.html create mode 100644 ja/2020/04/07/webpack-uglifyjsplugin/index.html create mode 100644 ja/2020/04/15/node-sass-slow-problem/index.html create mode 100644 ja/2020/04/15/talk-about-full-stack-big-fd/index.html create mode 100644 ja/2020/04/16/0-vuecli-3-component/index.html create mode 100644 ja/2020/04/16/what-is-npx/index.html create mode 100644 ja/2020/05/14/css3-pointer-events/index.html create mode 100644 ja/2020/05/28/Prettier-Setting/index.html create mode 100644 "ja/2020/07/19/rpm\345\221\275\344\273\244/index.html" create mode 100644 "ja/2020/08/02/Linux\347\233\256\345\275\225\350\257\264\346\230\216/index.html" create mode 100644 ja/2020/11/10/learn-cordova-1/index.html create mode 100644 ja/2020/11/10/learn-go/index.html create mode 100644 ja/2020/11/11/learn-ionic-1/index.html create mode 100644 "ja/2021/05/08/\345\205\263\344\272\216VSCode\344\270\255\345\210\253\345\220\215\357\274\210-\357\274\211\347\232\204\344\275\277\347\224\250/index.html" create mode 100644 "ja/2021/05/13/\345\205\263\344\272\216\347\211\210\346\234\254\347\256\241\347\220\206/index.html" create mode 100644 "ja/2022/03/11/\346\220\236\346\207\202EventLoop/index.html" create mode 100644 "ja/2022/03/11/\346\225\260\347\273\204\346\226\271\346\263\225\344\270\200\347\233\256\344\272\206\347\204\266/index.html" create mode 100644 "ja/2022/06/01/CodeReview\344\273\243\347\240\201\345\256\241\346\237\245\346\214\207\345\215\227/index.html" create mode 100644 "ja/2022/06/01/Electron\347\274\226\347\240\201\350\247\204\350\214\203/index.html" create mode 100644 ja/2022/06/01/GIT Standard/index.html create mode 100644 "ja/2022/06/01/Restful API \347\232\204\350\256\276\350\256\241\350\247\204\350\214\203/index.html" create mode 100644 ja/2022/06/01/squid/index.html create mode 100644 "ja/2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\346\214\207\345\215\227/index.html" create mode 100644 "ja/2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\350\247\204\350\214\203/index.html" create mode 100644 "ja/2022/06/01/\345\211\215\347\253\257\345\274\200\345\217\221\346\217\222\344\273\266/index.html" create mode 100644 ja/2022/06/02/DockerFile/index.html create mode 100644 "ja/2022/06/02/Docker\345\221\275\344\273\244/index.html" create mode 100644 "ja/2022/06/02/Docker\345\256\271\345\231\250\347\253\257\345\217\243\346\230\240\345\260\204/index.html" create mode 100644 "ja/2022/06/02/Docker\346\225\260\346\215\256\345\215\267/index.html" create mode 100644 "ja/2022/06/02/Docker\347\275\221\347\273\234\346\250\241\345\274\217/index.html" create mode 100644 "ja/2022/06/09/Hexo\345\234\250Github\345\217\221\345\270\203\344\271\213\345\220\216\350\207\252\345\256\232\344\271\211\345\237\237\345\220\215\347\232\204\351\205\215\347\275\256/index.html" create mode 100644 "ja/2022/06/09/Nginx\346\214\207\344\273\244/index.html" create mode 100644 ja/2022/06/09/http_stub_status_module/index.html create mode 100644 "ja/2022/06/15/\346\236\266\346\236\204\345\256\236\350\267\265/index.html" create mode 100644 "ja/2022/06/16/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206 ---- \347\275\221\347\273\234\345\256\211\345\205\250\357\274\2102\357\274\211/index.html" create mode 100644 ja/2022/06/30/nginx-timeout/index.html create mode 100644 "ja/2022/08/02/Docker \351\225\234\345\203\217\345\256\211\345\205\250\346\234\200\344\275\263\345\256\236\350\267\265 /index.html" create mode 100644 "ja/2022/08/02/Network in Compose \345\234\250Docker Compose\344\270\255\351\205\215\347\275\256\347\275\221\347\273\234/index.html" create mode 100644 "ja/2022/08/02/Nginx\344\270\255\347\232\204\350\266\205\346\227\266\350\256\276\347\275\256/index.html" create mode 100644 ja/2022/08/02/Oracle-Linux-install-Docker/index.html create mode 100644 "ja/2022/08/02/OracleLinux\345\206\205\346\240\270\344\273\216uek\345\210\207\346\215\242\345\210\260uek/index.html" create mode 100644 "ja/2022/08/02/Postgresql\347\273\277\350\211\262\347\211\210\344\275\277\347\224\250/index.html" create mode 100644 ja/2022/08/02/linux-scp/index.html create mode 100644 "ja/2022/08/02/systemctl \345\221\275\344\273\244/index.html" create mode 100644 "ja/2022/08/02/\346\220\255\345\273\272Docker\347\247\201\346\234\211\344\273\223\345\272\223/index.html" create mode 100644 "ja/2022/08/02/\346\234\215\345\212\241\345\231\250\351\253\230\345\215\261\347\253\257\345\217\243\345\210\227\350\241\250/index.html" create mode 100644 "ja/2023/09/20/DockerCompose\345\256\211\350\243\205/index.html" create mode 100644 ja/2023/09/20/test/index.html create mode 100644 ja/@attachment/README.html create mode 100644 ja/about/index.html create mode 100644 ja/archives/2013/06/index.html create mode 100644 ja/archives/2013/index.html create mode 100644 ja/archives/2015/12/index.html create mode 100644 ja/archives/2015/index.html create mode 100644 ja/archives/2016/10/index.html create mode 100644 ja/archives/2016/index.html create mode 100644 ja/archives/2017/08/index.html create mode 100644 ja/archives/2017/10/index.html create mode 100644 ja/archives/2017/12/index.html create mode 100644 ja/archives/2017/index.html create mode 100644 ja/archives/2019/12/index.html create mode 100644 ja/archives/2019/index.html create mode 100644 ja/archives/2020/01/index.html create mode 100644 ja/archives/2020/01/page/2/index.html create mode 100644 ja/archives/2020/01/page/3/index.html create mode 100644 ja/archives/2020/01/page/4/index.html create mode 100644 ja/archives/2020/02/index.html create mode 100644 ja/archives/2020/03/index.html create mode 100644 ja/archives/2020/03/page/2/index.html create mode 100644 ja/archives/2020/04/index.html create mode 100644 ja/archives/2020/05/index.html create mode 100644 ja/archives/2020/07/index.html create mode 100644 ja/archives/2020/08/index.html create mode 100644 ja/archives/2020/11/index.html create mode 100644 ja/archives/2020/index.html create mode 100644 ja/archives/2020/page/2/index.html create mode 100644 ja/archives/2020/page/3/index.html create mode 100644 ja/archives/2020/page/4/index.html create mode 100644 ja/archives/2020/page/5/index.html create mode 100644 ja/archives/2020/page/6/index.html create mode 100644 ja/archives/2020/page/7/index.html create mode 100644 ja/archives/2020/page/8/index.html create mode 100644 ja/archives/2021/05/index.html create mode 100644 ja/archives/2021/index.html create mode 100644 ja/archives/2022/03/index.html create mode 100644 ja/archives/2022/06/index.html create mode 100644 ja/archives/2022/06/page/2/index.html create mode 100644 ja/archives/2022/08/index.html create mode 100644 ja/archives/2022/index.html create mode 100644 ja/archives/2022/page/2/index.html create mode 100644 ja/archives/2022/page/3/index.html create mode 100644 ja/archives/2022/page/4/index.html create mode 100644 ja/archives/2023/09/index.html create mode 100644 ja/archives/2023/index.html create mode 100644 ja/archives/index.html create mode 100644 ja/archives/page/10/index.html create mode 100644 ja/archives/page/11/index.html create mode 100644 ja/archives/page/12/index.html create mode 100644 ja/archives/page/13/index.html create mode 100644 ja/archives/page/2/index.html create mode 100644 ja/archives/page/3/index.html create mode 100644 ja/archives/page/4/index.html create mode 100644 ja/archives/page/5/index.html create mode 100644 ja/archives/page/6/index.html create mode 100644 ja/archives/page/7/index.html create mode 100644 ja/archives/page/8/index.html create mode 100644 ja/archives/page/9/index.html create mode 100644 ja/categories/Database/Postgresql/index.html create mode 100644 ja/categories/Database/index.html create mode 100644 ja/categories/Docker/index.html create mode 100644 ja/categories/Infra/Docker/index.html create mode 100644 "ja/categories/Infra/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" create mode 100644 "ja/categories/Infra/Web\346\234\215\345\212\241\345\231\250/Squid/index.html" create mode 100644 "ja/categories/Infra/Web\346\234\215\345\212\241\345\231\250/index.html" create mode 100644 ja/categories/Infra/index.html create mode 100644 ja/categories/Infrastructure/index.html create mode 100644 ja/categories/Linux/Oracle/Docker/index.html create mode 100644 ja/categories/Linux/Oracle/index.html create mode 100644 ja/categories/Linux/RPM/index.html create mode 100644 ja/categories/Linux/index.html create mode 100644 ja/categories/NodeJS/Npm/index.html create mode 100644 ja/categories/NodeJS/index.html create mode 100644 ja/categories/OracleLinux/index.html create mode 100644 "ja/categories/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" create mode 100644 "ja/categories/Web\346\234\215\345\212\241\345\231\250/index.html" create mode 100644 ja/categories/index.html create mode 100644 ja/categories/linux/index.html create mode 100644 ja/categories/linux/systemctl/index.html create mode 100644 "ja/categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/Webpack/index.html" create mode 100644 "ja/categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/index.html" create mode 100644 "ja/categories/\345\211\215\347\253\257\345\274\200\345\217\221\350\247\204\350\214\203/index.html" create mode 100644 "ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS/index.html" create mode 100644 "ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS3/index.html" create mode 100644 "ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/index.html" create mode 100644 "ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/2/index.html" create mode 100644 "ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/3/index.html" create mode 100644 "ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/4/index.html" create mode 100644 "ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/5/index.html" create mode 100644 "ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\205\274\345\256\271\346\200\247/index.html" create mode 100644 "ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/index.html" create mode 100644 "ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/2/index.html" create mode 100644 "ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/3/index.html" create mode 100644 "ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\351\227\256\351\242\230/index.html" create mode 100644 "ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\276\256\344\277\241/index.html" create mode 100644 "ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\350\256\241\347\256\227\346\234\272\351\200\232\350\257\206/index.html" create mode 100644 "ja/categories/\345\211\215\347\253\257\346\241\206\346\236\266/React/index.html" create mode 100644 "ja/categories/\345\211\215\347\253\257\346\241\206\346\236\266/Vue/index.html" create mode 100644 "ja/categories/\345\211\215\347\253\257\346\241\206\346\236\266/index.html" create mode 100644 "ja/categories/\345\220\216\347\253\257\345\274\200\345\217\221/Go\350\257\255\350\250\200/index.html" create mode 100644 "ja/categories/\345\220\216\347\253\257\345\274\200\345\217\221/index.html" create mode 100644 "ja/categories/\345\244\247\345\211\215\347\253\257/index.html" create mode 100644 "ja/categories/\345\244\247\345\211\215\347\253\257/\345\205\250\346\240\210/index.html" create mode 100644 "ja/categories/\345\244\247\345\211\215\347\253\257/\345\256\242\346\210\267\347\253\257\346\212\200\346\234\257/index.html" create mode 100644 "ja/categories/\345\244\247\345\211\215\347\253\257/\347\247\273\345\212\250\345\274\200\345\217\221/index.html" create mode 100644 "ja/categories/\346\225\210\347\216\207\345\267\245\345\205\267/Labrary/index.html" create mode 100644 "ja/categories/\346\225\210\347\216\207\345\267\245\345\205\267/VSCode\346\217\222\344\273\266/index.html" create mode 100644 "ja/categories/\346\225\210\347\216\207\345\267\245\345\205\267/index.html" create mode 100644 "ja/categories/\346\235\202\344\270\203\346\235\202\345\205\253/index.html" create mode 100644 "ja/categories/\346\235\202\344\270\203\346\235\202\345\205\253/\345\215\232\345\256\242\346\212\200\345\267\247/index.html" create mode 100644 "ja/categories/\346\236\266\346\236\204/index.html" create mode 100644 "ja/categories/\346\272\220\347\240\201\345\216\237\347\220\206/index.html" create mode 100644 "ja/categories/\346\272\220\347\240\201\345\216\237\347\220\206/page/2/index.html" create mode 100644 "ja/categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/index.html" create mode 100644 "ja/categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/page/2/index.html" create mode 100644 "ja/categories/\350\207\252\346\210\221\346\217\220\345\215\207/index.html" create mode 100644 "ja/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\344\272\272\347\224\237\346\200\235\350\200\203/index.html" create mode 100644 "ja/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\346\235\202\350\256\260\351\232\217\346\204\237/index.html" create mode 100644 "ja/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\350\257\273\344\271\246\347\254\224\350\256\260/index.html" create mode 100644 "ja/categories/\350\231\232\346\213\237\345\214\226/Docker-Compose/index.html" create mode 100644 "ja/categories/\350\231\232\346\213\237\345\214\226/index.html" create mode 100644 ja/index.html create mode 100644 ja/page/10/index.html create mode 100644 ja/page/100/index.html create mode 100644 ja/page/101/index.html create mode 100644 ja/page/102/index.html create mode 100644 ja/page/103/index.html create mode 100644 ja/page/104/index.html create mode 100644 ja/page/105/index.html create mode 100644 ja/page/106/index.html create mode 100644 ja/page/107/index.html create mode 100644 ja/page/108/index.html create mode 100644 ja/page/109/index.html create mode 100644 ja/page/11/index.html create mode 100644 ja/page/110/index.html create mode 100644 ja/page/111/index.html create mode 100644 ja/page/112/index.html create mode 100644 ja/page/113/index.html create mode 100644 ja/page/114/index.html create mode 100644 ja/page/115/index.html create mode 100644 ja/page/116/index.html create mode 100644 ja/page/117/index.html create mode 100644 ja/page/118/index.html create mode 100644 ja/page/119/index.html create mode 100644 ja/page/12/index.html create mode 100644 ja/page/120/index.html create mode 100644 ja/page/121/index.html create mode 100644 ja/page/122/index.html create mode 100644 ja/page/123/index.html create mode 100644 ja/page/124/index.html create mode 100644 ja/page/125/index.html create mode 100644 ja/page/126/index.html create mode 100644 ja/page/127/index.html create mode 100644 ja/page/128/index.html create mode 100644 ja/page/13/index.html create mode 100644 ja/page/14/index.html create mode 100644 ja/page/15/index.html create mode 100644 ja/page/16/index.html create mode 100644 ja/page/17/index.html create mode 100644 ja/page/18/index.html create mode 100644 ja/page/19/index.html create mode 100644 ja/page/2/index.html create mode 100644 ja/page/20/index.html create mode 100644 ja/page/21/index.html create mode 100644 ja/page/22/index.html create mode 100644 ja/page/23/index.html create mode 100644 ja/page/24/index.html create mode 100644 ja/page/25/index.html create mode 100644 ja/page/26/index.html create mode 100644 ja/page/27/index.html create mode 100644 ja/page/28/index.html create mode 100644 ja/page/29/index.html create mode 100644 ja/page/3/index.html create mode 100644 ja/page/30/index.html create mode 100644 ja/page/31/index.html create mode 100644 ja/page/32/index.html create mode 100644 ja/page/33/index.html create mode 100644 ja/page/34/index.html create mode 100644 ja/page/35/index.html create mode 100644 ja/page/36/index.html create mode 100644 ja/page/37/index.html create mode 100644 ja/page/38/index.html create mode 100644 ja/page/39/index.html create mode 100644 ja/page/4/index.html create mode 100644 ja/page/40/index.html create mode 100644 ja/page/41/index.html create mode 100644 ja/page/42/index.html create mode 100644 ja/page/43/index.html create mode 100644 ja/page/44/index.html create mode 100644 ja/page/45/index.html create mode 100644 ja/page/46/index.html create mode 100644 ja/page/47/index.html create mode 100644 ja/page/48/index.html create mode 100644 ja/page/49/index.html create mode 100644 ja/page/5/index.html create mode 100644 ja/page/50/index.html create mode 100644 ja/page/51/index.html create mode 100644 ja/page/52/index.html create mode 100644 ja/page/53/index.html create mode 100644 ja/page/54/index.html create mode 100644 ja/page/55/index.html create mode 100644 ja/page/56/index.html create mode 100644 ja/page/57/index.html create mode 100644 ja/page/58/index.html create mode 100644 ja/page/59/index.html create mode 100644 ja/page/6/index.html create mode 100644 ja/page/60/index.html create mode 100644 ja/page/61/index.html create mode 100644 ja/page/62/index.html create mode 100644 ja/page/63/index.html create mode 100644 ja/page/64/index.html create mode 100644 ja/page/65/index.html create mode 100644 ja/page/66/index.html create mode 100644 ja/page/67/index.html create mode 100644 ja/page/68/index.html create mode 100644 ja/page/69/index.html create mode 100644 ja/page/7/index.html create mode 100644 ja/page/70/index.html create mode 100644 ja/page/71/index.html create mode 100644 ja/page/72/index.html create mode 100644 ja/page/73/index.html create mode 100644 ja/page/74/index.html create mode 100644 ja/page/75/index.html create mode 100644 ja/page/76/index.html create mode 100644 ja/page/77/index.html create mode 100644 ja/page/78/index.html create mode 100644 ja/page/79/index.html create mode 100644 ja/page/8/index.html create mode 100644 ja/page/80/index.html create mode 100644 ja/page/81/index.html create mode 100644 ja/page/82/index.html create mode 100644 ja/page/83/index.html create mode 100644 ja/page/84/index.html create mode 100644 ja/page/85/index.html create mode 100644 ja/page/86/index.html create mode 100644 ja/page/87/index.html create mode 100644 ja/page/88/index.html create mode 100644 ja/page/89/index.html create mode 100644 ja/page/9/index.html create mode 100644 ja/page/90/index.html create mode 100644 ja/page/91/index.html create mode 100644 ja/page/92/index.html create mode 100644 ja/page/93/index.html create mode 100644 ja/page/94/index.html create mode 100644 ja/page/95/index.html create mode 100644 ja/page/96/index.html create mode 100644 ja/page/97/index.html create mode 100644 ja/page/98/index.html create mode 100644 ja/page/99/index.html create mode 100644 ja/tags/index.html create mode 100644 js/local-search.js create mode 100644 js/motion.js create mode 100644 js/next-boot.js create mode 100644 js/schemes/pisces.js create mode 100644 js/utils.js create mode 100644 lib/anime.min.js create mode 100644 lib/font-awesome/css/all.min.css create mode 100644 lib/font-awesome/webfonts/fa-brands-400.woff2 create mode 100644 lib/font-awesome/webfonts/fa-regular-400.woff2 create mode 100644 lib/font-awesome/webfonts/fa-solid-900.woff2 create mode 100644 lib/velocity/velocity.min.js create mode 100644 lib/velocity/velocity.ui.min.js create mode 100644 live2dw/assets/moc/wanko.1024/texture_00.png create mode 100644 live2dw/assets/moc/wanko.moc create mode 100644 live2dw/assets/mtn/idle_01.mtn create mode 100644 live2dw/assets/mtn/idle_02.mtn create mode 100644 live2dw/assets/mtn/idle_03.mtn create mode 100644 live2dw/assets/mtn/idle_04.mtn create mode 100644 live2dw/assets/mtn/shake_01.mtn create mode 100644 live2dw/assets/mtn/shake_02.mtn create mode 100644 live2dw/assets/mtn/touch_01.mtn create mode 100644 live2dw/assets/mtn/touch_02.mtn create mode 100644 live2dw/assets/mtn/touch_03.mtn create mode 100644 live2dw/assets/mtn/touch_04.mtn create mode 100644 live2dw/assets/mtn/touch_05.mtn create mode 100644 live2dw/assets/mtn/touch_06.mtn create mode 100644 live2dw/assets/wanko.model.json create mode 100644 live2dw/lib/L2Dwidget.0.min.js create mode 100644 live2dw/lib/L2Dwidget.0.min.js.map create mode 100644 live2dw/lib/L2Dwidget.min.js create mode 100644 live2dw/lib/L2Dwidget.min.js.map create mode 100644 page/10/index.html create mode 100644 page/100/index.html create mode 100644 page/101/index.html create mode 100644 page/102/index.html create mode 100644 page/103/index.html create mode 100644 page/104/index.html create mode 100644 page/105/index.html create mode 100644 page/106/index.html create mode 100644 page/107/index.html create mode 100644 page/108/index.html create mode 100644 page/109/index.html create mode 100644 page/11/index.html create mode 100644 page/110/index.html create mode 100644 page/111/index.html create mode 100644 page/112/index.html create mode 100644 page/113/index.html create mode 100644 page/114/index.html create mode 100644 page/115/index.html create mode 100644 page/116/index.html create mode 100644 page/117/index.html create mode 100644 page/118/index.html create mode 100644 page/119/index.html create mode 100644 page/12/index.html create mode 100644 page/120/index.html create mode 100644 page/121/index.html create mode 100644 page/122/index.html create mode 100644 page/123/index.html create mode 100644 page/124/index.html create mode 100644 page/125/index.html create mode 100644 page/126/index.html create mode 100644 page/127/index.html create mode 100644 page/128/index.html create mode 100644 page/13/index.html create mode 100644 page/14/index.html create mode 100644 page/15/index.html create mode 100644 page/16/index.html create mode 100644 page/17/index.html create mode 100644 page/18/index.html create mode 100644 page/19/index.html create mode 100644 page/2/index.html create mode 100644 page/20/index.html create mode 100644 page/21/index.html create mode 100644 page/22/index.html create mode 100644 page/23/index.html create mode 100644 page/24/index.html create mode 100644 page/25/index.html create mode 100644 page/26/index.html create mode 100644 page/27/index.html create mode 100644 page/28/index.html create mode 100644 page/29/index.html create mode 100644 page/3/index.html create mode 100644 page/30/index.html create mode 100644 page/31/index.html create mode 100644 page/32/index.html create mode 100644 page/33/index.html create mode 100644 page/34/index.html create mode 100644 page/35/index.html create mode 100644 page/36/index.html create mode 100644 page/37/index.html create mode 100644 page/38/index.html create mode 100644 page/39/index.html create mode 100644 page/4/index.html create mode 100644 page/40/index.html create mode 100644 page/41/index.html create mode 100644 page/42/index.html create mode 100644 page/43/index.html create mode 100644 page/44/index.html create mode 100644 page/45/index.html create mode 100644 page/46/index.html create mode 100644 page/47/index.html create mode 100644 page/48/index.html create mode 100644 page/49/index.html create mode 100644 page/5/index.html create mode 100644 page/50/index.html create mode 100644 page/51/index.html create mode 100644 page/52/index.html create mode 100644 page/53/index.html create mode 100644 page/54/index.html create mode 100644 page/55/index.html create mode 100644 page/56/index.html create mode 100644 page/57/index.html create mode 100644 page/58/index.html create mode 100644 page/59/index.html create mode 100644 page/6/index.html create mode 100644 page/60/index.html create mode 100644 page/61/index.html create mode 100644 page/62/index.html create mode 100644 page/63/index.html create mode 100644 page/64/index.html create mode 100644 page/65/index.html create mode 100644 page/66/index.html create mode 100644 page/67/index.html create mode 100644 page/68/index.html create mode 100644 page/69/index.html create mode 100644 page/7/index.html create mode 100644 page/70/index.html create mode 100644 page/71/index.html create mode 100644 page/72/index.html create mode 100644 page/73/index.html create mode 100644 page/74/index.html create mode 100644 page/75/index.html create mode 100644 page/76/index.html create mode 100644 page/77/index.html create mode 100644 page/78/index.html create mode 100644 page/79/index.html create mode 100644 page/8/index.html create mode 100644 page/80/index.html create mode 100644 page/81/index.html create mode 100644 page/82/index.html create mode 100644 page/83/index.html create mode 100644 page/84/index.html create mode 100644 page/85/index.html create mode 100644 page/86/index.html create mode 100644 page/87/index.html create mode 100644 page/88/index.html create mode 100644 page/89/index.html create mode 100644 page/9/index.html create mode 100644 page/90/index.html create mode 100644 page/91/index.html create mode 100644 page/92/index.html create mode 100644 page/93/index.html create mode 100644 page/94/index.html create mode 100644 page/95/index.html create mode 100644 page/96/index.html create mode 100644 page/97/index.html create mode 100644 page/98/index.html create mode 100644 page/99/index.html delete mode 100644 placeholder create mode 100644 search.xml create mode 100644 tags/Actions/index.html create mode 100644 tags/CI-CD/index.html create mode 100644 tags/CSS/index.html create mode 100644 tags/CSS3/index.html create mode 100644 "tags/CSS\346\212\200\345\267\247/index.html" create mode 100644 tags/CodeReview/index.html create mode 100644 tags/Cordova/index.html create mode 100644 tags/Docker-Compose/index.html create mode 100644 tags/Docker/index.html create mode 100644 tags/DockerFile/index.html create mode 100644 tags/ESLint/index.html create mode 100644 tags/Electron/index.html create mode 100644 tags/EventLoop/index.html create mode 100644 tags/GIT/index.html create mode 100644 tags/Git/index.html create mode 100644 tags/GitLab/index.html create mode 100644 tags/Github/index.html create mode 100644 "tags/Go\350\257\255\350\250\200/index.html" create mode 100644 tags/Hexo/index.html create mode 100644 tags/HomeBrew/index.html create mode 100644 tags/Ionic/index.html create mode 100644 tags/Linux/index.html create mode 100644 tags/Mac/index.html create mode 100644 tags/Nginx/index.html create mode 100644 tags/NodeJS/index.html create mode 100644 tags/Postgresql/index.html create mode 100644 tags/Prettier/index.html create mode 100644 tags/RESTful/index.html create mode 100644 tags/RPM/index.html create mode 100644 tags/React/index.html create mode 100644 tags/Restful-API/index.html create mode 100644 tags/SSR/index.html create mode 100644 tags/SemVer/index.html create mode 100644 tags/Squid/index.html create mode 100644 tags/Timeout/index.html create mode 100644 tags/UglifyJsPlugin/index.html create mode 100644 tags/VSCode/index.html create mode 100644 "tags/VSCode\346\217\222\344\273\266/index.html" create mode 100644 tags/Vue/index.html create mode 100644 tags/Webpack/index.html create mode 100644 tags/ajax/index.html create mode 100644 tags/es6/index.html create mode 100644 tags/get/index.html create mode 100644 tags/index.html create mode 100644 tags/jsonp/index.html create mode 100644 tags/kernel/index.html create mode 100644 tags/npm/index.html create mode 100644 tags/npx/index.html create mode 100644 tags/path-intellisense/index.html create mode 100644 tags/port/index.html create mode 100644 tags/promise/index.html create mode 100644 tags/registry/index.html create mode 100644 tags/rnpm/index.html create mode 100644 tags/sass/index.html create mode 100644 tags/scp/index.html create mode 100644 tags/systemctl/index.html create mode 100644 tags/vuepress/index.html create mode 100644 "tags/\344\272\213\344\273\266\351\251\261\345\212\250/index.html" create mode 100644 "tags/\345\205\274\345\256\271\346\200\247\351\227\256\351\242\230/index.html" create mode 100644 "tags/\345\206\205\345\255\230\345\233\236\346\224\266/index.html" create mode 100644 "tags/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\346\214\207\345\215\227/index.html" create mode 100644 "tags/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\350\247\204\350\214\203/index.html" create mode 100644 "tags/\345\211\215\347\253\257\345\274\200\345\217\221\346\217\222\344\273\266/index.html" create mode 100644 "tags/\345\220\214\346\236\204\346\212\200\346\234\257/index.html" create mode 100644 "tags/\345\245\207\346\200\252\351\227\256\351\242\230/index.html" create mode 100644 "tags/\345\256\271\345\231\250\347\253\257\345\217\243\346\230\240\345\260\204/index.html" create mode 100644 "tags/\345\271\264\347\273\210\346\200\273\347\273\223/index.html" create mode 100644 "tags/\345\276\256\344\277\241/index.html" create mode 100644 "tags/\346\211\223\345\214\205/index.html" create mode 100644 "tags/\346\216\222\345\272\217\347\256\227\346\263\225/index.html" create mode 100644 "tags/\346\217\222\344\273\266/index.html" create mode 100644 "tags/\346\225\260\346\215\256\345\215\267/index.html" create mode 100644 "tags/\346\225\260\347\273\204/index.html" create mode 100644 "tags/\346\234\215\345\212\241\347\253\257\346\270\262\346\237\223/index.html" create mode 100644 "tags/\346\236\266\346\236\204/index.html" create mode 100644 "tags/\346\265\267\345\244\226\346\217\220\351\200\237/index.html" create mode 100644 "tags/\346\272\220\347\240\201\350\247\243\346\236\220/index.html" create mode 100644 "tags/\347\211\210\346\234\254\347\256\241\347\220\206/index.html" create mode 100644 "tags/\347\237\245\350\257\206\347\202\271/index.html" create mode 100644 "tags/\347\273\204\344\273\266\351\200\232\344\277\241/index.html" create mode 100644 "tags/\347\273\217\351\252\214\344\271\213\350\260\210/index.html" create mode 100644 "tags/\347\275\221\347\273\234\345\256\211\345\205\250/index.html" create mode 100644 "tags/\347\275\221\347\273\234\346\250\241\345\274\217/index.html" create mode 100644 "tags/\350\204\232\346\211\213\346\236\266/index.html" create mode 100644 "tags/\350\207\252\345\212\250\346\236\204\345\273\272/index.html" create mode 100644 "tags/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/index.html" create mode 100644 "tags/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/page/2/index.html" create mode 100644 "tags/\350\256\276\350\256\241\346\250\241\345\274\217/index.html" create mode 100644 "tags/\350\266\205\346\227\266\350\256\276\347\275\256/index.html" create mode 100644 "tags/\350\267\257\345\276\204\345\210\253\345\220\215/index.html" create mode 100644 "tags/\351\225\234\345\203\217\345\256\211\345\205\250/index.html" create mode 100644 "tags/\351\235\242\350\257\225/index.html" create mode 100644 "tags/\351\235\242\350\257\225/page/2/index.html" create mode 100644 "tags/\351\235\242\350\257\225/page/3/index.html" diff --git a/2013/06/17/Read-High-Performance-JavaScript/index.html b/2013/06/17/Read-High-Performance-JavaScript/index.html new file mode 100644 index 0000000000..a2f4354144 --- /dev/null +++ b/2013/06/17/Read-High-Performance-JavaScript/index.html @@ -0,0 +1,607 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《高性能JAVASCRIPT》读书笔记 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《高性能JAVASCRIPT》读书笔记 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近读了《高性能JAVASCRIPT》,下面将书中觉得有用的内容进行摘抄,放在下方。

+

如何从小事提升JAVASCRIPT性能。

    +
  1. <script>标签写在</body>之前——将脚本放在底部。

    +
  2. +
  3. 尽量少的<script>标签,减少页面中的外链数量,减少请求次数。

    +
  4. +
  5. 使用打包工具如:Yahoo!combo handler

    +
  6. +
  7. 使用动态延迟加载技术如:LazyLoad类库,LABjs

    +
    1
    2
    3
    4
    5
    6
    7
    // js代码
    <script type="text/javascript" src="lazyload-min.js"></script>
    <script type="text/javascript">
    LazyLoad.js([],function(){
    Application.init();
    })
    </script>
  8. +
  9. 在javascript中存储位置十分重要,尽量用局部变量代替对象成员的访问。

    +
  10. +
  11. 集合变数组提高查询效率

    +
    1
    2
    3
    4
    5
    6
    7
    // js代码
    function toArray(coll){
    for(var i = 0, a=[], len=coll.length; i<len; i++){
    a[i]=col[i];
    }
    return a;
    }
  12. +
  13. 使用局部变量缓存访问多次的成员
    当便利一个集合时,首要优化原则是把集合存储在局部变量中,并把length缓存在循环外部,然后使用局部变量访问这些需要多次访问的元素。

    +
  14. +
  15. 使用原生DOM方法querySelectorAll()遍历查找元素。

    +
  16. +
  17. 让元素脱离动画流,页面重排次数越多,程序响应越慢,要尽量减少重拍
    方法:

    +
  18. +
    1. +
    2. 使用绝对定位使元素脱离文档流
    3. +
    +
  19. +
  20. IE:hover
    在大量元素使用:hover这个伪css选择器时,程序会降低响应速率,在大表格或列表的情况下

    +
  21. +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:《高性能JAVASCRIPT》读书笔记

+

文章作者:

+

发布时间:2013年06月17日 - 22:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2013/06/17/Read-High-Performance-JavaScript/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2013/06/26/CSS-Triangle-Circle/index.html b/2013/06/26/CSS-Triangle-Circle/index.html new file mode 100644 index 0000000000..ebe5c0fc44 --- /dev/null +++ b/2013/06/26/CSS-Triangle-Circle/index.html @@ -0,0 +1,623 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CSS border三角、圆角图形生成技术详解 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ CSS border三角、圆角图形生成技术详解 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

利用CSS的border属性可以生成一些图形,例如三角或是圆角。纯粹的CSS2的内容,没有兼容性的问题,我之前在纯CSS实现各类气球泡泡对话框效果一文中算是比较详细的讲述了CSS border属性生成三角的原理,以及实例。我觉得此技术相当实用的,故本文再次简单叙述一下,另外,本文还将展示可能并不为众人所知的CSS border圆角生成技术。好了,裹脚布的话就不说了,直接进入正题。

+ + +

CSS border生成三角技术简介

效果抢鲜

下图为使用CSS的border属性实现的三角效果:

+
1
2
3
4
5
6
7
8
// css 代码
.test{
width: 0;
height: 0;
border-width: 20px 10px;
border-style: solid;
border-color: #ff3300 #ff3300 #ffffff #ffffff;
}
+

如何实现的,为何会有这样的效果,不急,take it easy!

+

梯形图案

看下面这段样式:

+
1
2
3
4
5
6
.test{
width: 10px;
height: 10px;
border: 10px solid;
border-color: #ff3300 #0000ff #339966 #00ff00
}
+

当某个div应用了上面这个样式后,结果会如何?见下图(截自Firefox3.5,IE浏览器有细节上的差异):

+

更进一步 – 部分边框透明

现在,设想一下,如果我们现在只保留一个一个上边框,其余边框均transparent透明(或与背景色同色),那么是不是就只显示一个上面红色的边框了,我们测试下,与上面类似的代码,只是修改下其余三个边框的颜色。

+
1
2
3
4
5
6
.test{
width:10px;
height:10px;
border:10px solid;
border-color:#ff3300 #ffffff #ffffff #ffffff;
}
+

结果如下图(截自Firefox3.5):

+

从梯形到三角

上面的是梯形,我要想得到一个三角图案该怎么办呢?显然,很简单,把div的高宽都变成0,只留一边,不就是三角了吗?如下代码:

+
1
2
3
4
5
.test{
width: 0;
height: 0;
border: 10px solid;
border-color: #ff3300 #ffffff #ffffff #ffffff;}
+

结果如下(依旧截图自Firefox3.5):

+

从等腰直角三角形到普通等腰三角

上图为等腰直角三角形,之所以为等腰直角,是因为所有的边框宽度是一样的,如果我们将边框宽度设置为不同,那会怎样?则会形成等腰三角形。如下代码:

+
1
2
3
4
5
6
.test{
width: 0;
height: 0;
border-width: 20px 10px;
border-style: solid;
border-color: #ff3300 #ffffff #ffffff #ffffff;}
+

得到的结果如下图:

+

从等腰到不等腰

我们可以不局限于保留一条边框,我们可以保留两条,于是我们可以告别等腰,得到更加锐利的三角,正如一开始所展示的那个三角:

+
1
2
3
4
5
6
.test{
width: 0;
height: 0;
border-width: 20px 10px;
border-style: solid;
border-color: #ff3300 #ff3300 #ffffff #ffffff;}
+

实际的应用

关于应用,不多说,直接看图:
说明:
以上的测试代码纯粹为了说明原理,所以使用#ffffff白色边框,通过于背景融合来隐藏边框。在实际的操作中,应该使用transparent透明属性,例如border-color:#ff3300 #ff3300 transparent transparent;,这同样会有问题,IE6浏览器不支持transparent透明属性,不过没有关系,就border生成三角技术而言,直接设置对应的透明边框的border-style属性为dotted或是dashed即可解决这一问题,为什么使用dotted和dashed可以修复此问题呢?您有兴趣可以参见默尘的这篇文章Dotted&Dashed终极分析及IE6透明边框。

+

CSS border圆角生成技术简介

我看圆角

一提到圆角,我脑中闪过的词就是“定位”,“嵌套”,“模拟”,“渐进增强”,“滥用”。

+
    +
  • 定位,也就是切四个角上下左右定位,这是淘宝首页的做法,但是面对IE6的奇偶bug只能当作看客;
  • +
  • 使用“嵌套”则不会有此问题,“嵌套”分图片背景嵌套和CSS边框嵌套,使用图片嵌套则图片的重用性,大小优化有待加强,边框嵌套则技术实现上有些难度;
  • +
  • 或使用“渐进增强”,CSS3 border-radius属性,而不要去鸟IE这类自我感觉良好的浏览器;
  • +
  • 或是学习Google使用CSS模拟,而一般的CSS模拟都是使用左右边框+背景色的方式1像素1像素的拼合成的。这类方法各有优缺点,需根据实际情况采用。对于满眼圆角的设计图我是很不喜欢的,该用则用,切勿为了圆角而圆角。
  • +
+

border圆角图案生成法

这里介绍的实现圆角的得到与上面提到的都是不一样的,虽然也属于CSS模拟的范畴,但是其高效的程度确实相当惊人的,可谓最佳实践之一。
我们先看看效果,见下图,截自Firefox3.6:
上述效果的实现仅仅使用了三个标签,如下代码:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// html 代码
<div class="box">
<div class="top"></div>
<div class="center">我是一只小小鸟、小小鸟!</div>
<div class="bot"></div>
</div>

// css 代码
.box{
width:500px;
}
.top{
border-bottom:3px solid;
border-top-color:#cc0000;
border-bottom-color:#cc0000;
border-left:3px dotted transparent;
border-right:3px dotted transparent;
}
.center{
padding:10px 20px;
color:white;
font-size:14px;
background:#cc0000;
}
.bot{
border-top:3px solid;
border-top-color:#cc0000;
border-bottom-color:#cccccc;
border-left:3px dotted transparent;
border-right:3px dotted transparent;
}
+

我们看看这段代码在IE6下的效果:

+

这里的高效在于,仅仅使用了一层标签就模拟了3像素的圆角,按照曾经我对CSS圆角模拟的理解,模拟1像素的圆角需要一层标签(background+borderLeft+borderRight),两像素的需要两层标签,三像素的需要三层标签。

+

有点神奇,但是就像看刘谦的魔术一样,说穿了也就那么回事,其实这里的圆角模拟在本文的上面已经展示了,就是这样图片:

+

您可能会疑问,是不是搞错图片啦,这显然不是一个模样的,非也非也,就本质上而言,圆角的实现与上面的梯形图就是同样的东西。现在,盯着上面这张图,我们想象一下,用力的想象,用想花姑娘的那番劲头想象——上面的梯形宽度越来越宽(不是拉伸),一直宽到500像素,是不是与上面实现的圆角的下边缘一致啊?

+

也就是说,那个含有“我是一只小小鸟……”文字的圆角图形是有一个上梯形+矩形+下梯形组成的。参见下面的分离效果图:
您可以狠狠地点击这里:CSS border圆角生成demo

+

局限性

人无完人,金无足赤,此方法虽然简洁高效,兼容性上佳,但是依然有局限性,在实现实色背景的圆角效果时,此方法可谓首选;如果是纯粹的圆角边框,此方法也可以实现,需要用到边框重叠,但是标签数几乎要翻倍,其权衡效用将大打折扣,反不如其他圆角方法来的实在。

+

结语

如果在web制作中,需要用的一些直接可以使用CSS+单标签模拟的图片,我的建议是“毫不犹豫使用CSS模拟”,例如实色的三角,或是实现实色的圆角效果,这可以说是最高效,最利于扩展维护的前端实现方法了。我们需要开阔的思维,而不要仅仅局限于眼前的技术,武侠中所谓的“无招胜有招”还是有着一定的哲学道理的,长远来看,意识与海纳百川的心态比当下的一点技术更来得重要。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:CSS border三角、圆角图形生成技术详解

+

文章作者:

+

发布时间:2013年06月26日 - 22:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2013/06/26/CSS-Triangle-Circle/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2015/12/31/Read-Let-your-boss-promote-you/index.html b/2015/12/31/Read-Let-your-boss-promote-you/index.html new file mode 100644 index 0000000000..95c0186920 --- /dev/null +++ b/2015/12/31/Read-Let-your-boss-promote-you/index.html @@ -0,0 +1,780 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《让老板提拔你》读书笔记 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《让老板提拔你》读书笔记 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近读了《让老板提拔你》,下面将书中觉得有用的内容进行摘抄,放在下方。

+

定位自己

正确认识自己,确定社会定位、职业定位。 定位-决定-定价

+

要素

核心竞争力职位 契合度 是高薪关键所在

+

契合度

    +
  • 技能、专长、经历与职位要求的契合度
  • +
  • 专业资质和等级与职位要求的契合度
  • +
  • 综合素质与职位要求的契合度

    七大秘诀

  • +
  • 了解同行业薪酬的平均水平
  • +
  • 赢得未来单位的心
  • +
  • 先让对方开口
  • +
  • 勇敢地开口要求
  • +
  • 不要轻言放弃
  • +
  • 把握时机很重要
  • +
  • 说实话,别撒谎

    如何谈薪资

  • +
  • 商洽薪酬的关键在于把握好谈话时机,切勿开门见山。
    +

    只要有发展机会,我愿意接受贵单位的薪酬标准,不知道按规定这个工作岗位的薪酬标准是多少。

    +
    +
  • +
+

将知识卖个好价钱

推销自己既推销自己所掌握的知识即智慧,正确认识自己的价值是关键。

+

高薪是因为“物有所值”

    +
  • 用业绩、用能力说话,是人才坦然面对高薪的心态。
  • +
  • 高薪人才的求职方式 —- 圈内朋友引见、定向投简历、行业论坛、交流活动等。
  • +
  • 高级人才不能经常跳槽要保持相对的职业稳定。—- 在一家公司少则3年多则6年 —- 接受挑战,把握机会。
  • +
+

失败丰富走向成功经验

强调在失败中吸取的经验,在未来中可以避免的损失。

+

能为企业带来丰厚的利润才是人才

企业招聘员工的唯一目的:追求公司业绩最大化、利润最大化。

+

高质高效工作全攻略

    +
  • 进行正确的自我评价
  • +
  • 做最擅长做的事
      +
    • 三个经济原则 —- 发挥人才优势。
        +
      1. 比较利益原则—–自己的专长对自己才是最有利的,勿与他人作比较。
      2. +
      3. 机会成本原则—–选择一份工作放弃其他工作就是机会成本。
      4. +
      5. 效率原则——–工作的成功不在于时间有多长,而在于成效有多少,附加值有多少。
      6. +
      +
    • +
    +
  • +
  • 马上行动
  • +
  • 做事情要雷厉风行 ——— 成功属于踏实肯干的人
  • +
  • 有条不紊地开展工作 ——- 制定时间计划
  • +
  • 善于利用现代办公工具
  • +
  • 给自己最大的工作空间
  • +
  • 建立高效有序的办公环境
  • +
  • 不要忘记最初想去的方向
  • +
  • “聪明”的向上级提出建议
  • +
  • 专心做事,避免浮躁
  • +
  • 多而不专,一事难成
  • +
  • “专”才有高质工作——–人才主要指专才–某个领域的高精尖人才–勿做全才,一专多能
  • +
  • 做事要有条理
  • +
+

不要只把自己当成一个打工仔

+

要把工作当成事业

+
+
    +
  • 工作不仅仅是为了钱
  • +
  • 对工作要有明确的价值取向
      +
    1. 认清人生的方向
    2. +
    3. 开始学会醉卧探索和认知
    4. +
    5. 认清工作价值与成就的关系
    6. +
    7. 长期的工作规划
    8. +
    9. 在生命的天平上衡量自身的价值
    10. +
    +
  • +
  • 巧妙应对与上司看法向左时的三条准则
      +
    1. 遇事考虑全局
    2. +
    3. 辩证地看待问题
    4. +
    5. 切记感情用事
    6. +
    +
  • +
  • 把单位的事当成自家的事
  • +
  • 认真负责地用心工作
  • +
  • 珍惜岗位,热爱自己的职业
  • +
  • 永远是在为自己工作
  • +
  • 敬重自己的工作
  • +
  • 不要轻视薪水微薄的工作
  • +
  • 永远对工作充满激情
  • +
  • 以自己的工作为荣
  • +
  • 不要被他人的观点所束缚
  • +
  • 暂时的胜负并不会决定人生的最后走向
  • +
  • 将弱势转化为优势
  • +
  • 全力以赴做好每一天的工作
  • +
  • 和优秀的人士在一起—见贤思齐、借梯爬楼
      +
    • 如何争取跟优秀的人在一起
        +
      1. 不断的抛头露面
      2. +
      3. 帮助可以帮助自己成就事业的人做事
      4. +
      5. 与上司和比自己优秀的人士一起合作
      6. +
      +
    • +
    +
  • +
+
    +
  1. 尊重对方,严谨有致
  2. +
  3. 切记奉承,要不卑不亢
  4. +
  5. 态度自然,不必拘谨
  6. +
  7. 陪衬得当,不可狂妄
  8. +
  9. 主动真诚,做出姿态
  10. +
  11. 求助求教,接受呵护
  12. +
+
    +
  • 挑战自我,承担责任
      +
    • 三条忠告
        +
      1. 全心全意工作
      2. +
      3. 把自己视为合伙人
      4. +
      5. 迎接变革的需求
      6. +
      +
    • +
    +
  • +
  • 自信独立,不随波逐流
  • +
  • 敢于显示自己很重要
  • +
  • 千万不能只知道抱怨上司
  • +
  • 保持严谨认真的做事习惯
  • +
  • 自主地做好手中的工作
  • +
  • 踏踏实实地做好本职工作
  • +
  • 丢掉工作散漫的坏习惯
  • +
  • 不要让浮躁的性格困扰自己
  • +
  • 不推诿,勇于承担责任
  • +
  • 无论如何都不要拖延工作
  • +
  • 糊弄工作只能是在糊弄自己
  • +
  • 逊色的工作只会淘汰自己
  • +
  • 千万别丢掉“得宠”之资
  • +
  • “一步登天”只会摔疼自己
  • +
  • 别让“差不多”贻误了自己
  • +
  • 能完成100%,就决不做99%
  • +
+

与上司相处

    +
  • 不要做上司的“心腹”
  • +
  • 适时恰当的赞美上司
      +
    • 赞美上司,还要善于选择适当的场合
    • +
    • 赞美上司,要学会巧借公众语言称赞
    • +
    • 赞美上司,还要善于赞美不得志的上司
    • +
    +
  • +
  • 主动与领导沟通
  • +
  • 主动和上司保持联系
  • +
  • 用“心机”主动接近上司
      +
    • 尽可能详细的了解上司
    • +
    • 选择一个与领导尽可能近的位置
    • +
    • 赢得上司青睐的方法
    • +
    +
  • +
  • 更有效的和上司沟通
      +
    • 与上司沟通要简洁
    • +
    • 与上司沟通要大度大气大方
    • +
    • 与上司沟通,就要把自己先放一边 ——– 说话对事不对人
    • +
    +
  • +
  • 四种和上司进行沟通的方法
      +
    1. 开诚布公式———提出问题,说出自己的理解认识,给出解决方案,寻求领导意思,执行领导的命令
    2. +
    3. 先斩后奏式———因主客观原因导致的没有先汇报而是先解决的事,事后要将问题的分析方法,解决方法,实施方案以及结果做一份详尽的总结报告上交给领导
    4. +
    5. “含情脉脉”式——-通过交流一些工作上的问题来含蓄表达出个人的建议、需要
    6. +
    7. 巧设比喻式———通过典型案例来暗示自己的意见和要求
    8. +
    +
  • +
  • 把话说到上司的心坎上
  • +
  • 巧妙地为领导拾遗补缺
      +
    1. 诠释领导讲话的难点
    2. +
    3. 强调领导的才干
    4. +
    5. 化严肃为幽默
    6. +
    7. 稳定情绪,委婉暗示
    8. +
    +
  • +
  • 工作中勤于请示汇报
      +
    1. 听懂上司的意图
    2. +
    3. 探讨、磨合,达成共识
    4. +
    5. 制定尽可能详尽的工作计划
    6. +
    7. 随时向上司汇报任务的关键点
    8. +
    9. 总结汇报
    10. +
    +
  • +
  • 用成功赢得上司的信任
  • +
  • 工作中不要冲撞上司
  • +
  • 处理好同上司之间的分歧
      +
    1. 圆融协调——领导不懂,下达了错误的指令
        +
      1. 私下向上司陈述意见,帮助上司做出正确的决策
      2. +
      3. 如果上司采纳了下属的建议或意见,该下属有责任长期保守秘密
      4. +
      5. 如果上司固执己见,那么无条件服从
      6. +
      +
    2. +
    3. 装聋作哑——不涉及到原则问题
    4. +
    5. 棘手难题多权衡
        +
      1. 立刻插话纠正
      2. +
      3. 提醒上司
      4. +
      5. 暗示
      6. +
      7. 事后补救
      8. +
      9. 事后提醒
      10. +
      +
    6. +
    +
  • +
  • 正确对待上司的批评
  • +
  • 要善于服从自己的上司
  • +
  • 正确化解来自上司的压力
  • +
+

写在最后

博观约取,多读书读好书,丰富自己,变得睿智。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:《让老板提拔你》读书笔记

+

文章作者:

+

发布时间:2015年12月31日 - 22:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2015/12/31/Read-Let-your-boss-promote-you/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2016/10/27/Read-The-miracle-of-the-morning-journal/index.html b/2016/10/27/Read-The-miracle-of-the-morning-journal/index.html new file mode 100644 index 0000000000..634d4838ad --- /dev/null +++ b/2016/10/27/Read-The-miracle-of-the-morning-journal/index.html @@ -0,0 +1,688 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《晨间日记的奇迹》读书笔记 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《晨间日记的奇迹》读书笔记 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

昨天花了1天的时间读了《晨间日记的奇迹》,感觉整个人的认识发生了一些变化,下面把重要内容做一下摘抄总结。

+

早上写日记的好处 —— 提升效率

    +
  • 可以做好一天的准备 — 计划性
  • +
  • 可以正确的写出昨天发生的事 — 效率性&忠诚性
  • +
  • 可以中立的看待昨天 — 中立性
  • +
  • 相对自由的时间 — 持续性
  • +
  • 总结经验 — 活用性
  • +
+

注意事项

日记 不等于 日志
日记 要记录 时间 天气 心情 — 因为这些有助于回忆和记录
不要投入过长时间 — 3分钟 — 日记私密性
晨间日记前7天只写好事 — 反省固然重要,但是成长更重要,持续性很重要 — 巴普洛夫的狗

+

晨间日记2部分

Part1

客观记录已经发生的事(昨天)— 经验智慧

+

Part2

    +
  • 今天应做的事 — 具体行动(来自昨天的总结
  • +
  • 今天一定要做的事(紧急重要的事)— 变得更积极更主动
  • +
  • 未来要做的事 — 不紧急但重要的事
  • +
  • 连用日记 — 历史上的今天(过去一年同一天的事)
  • +
+

夜晚日记 VS 晨间日记

受当天情绪影响 — 更冷静

+

梦想成真表

+ + + + + + + + + + + + + + + + + +
过去未来
事实IQ 智慧指数NQ 人际关系指数
感情EQ 情感指数DQ 梦想指数
+
    +
  • IQ — 晨间日记冷静回忆分析 — 提高决策正确性 IQ up
  • +
  • EQ — 冷静记录昨日的情绪/自省 — 变积极 EQ up
  • +
  • NQ — 总结昨天给今天设立“对/为XXX做某事” 很重要 NQ up
  • +
  • DQ — 不能只有眼前的事 每天都要确定一下自己的梦想 脸皮厚点没关系 重复或只有一句话也没关系 铭记于心最重要
  • +
+

“忙碌”只是借口!“决心”才是问题! 对未来充满期待的人才有行动力

+

如何早起

    +
  • 设定起床音乐/用气味(精油)刺激起床/光线-电动窗帘早起自动拉开
  • +
  • 早起后淋浴 — 配合音乐/冥想/呐喊 — 让自己充满动力保持清醒
  • +
+

写日记的五大好处

    +
  • 提升写作能力
  • +
  • 谈话题材源源不断
  • +
  • 提高贵人运
  • +
  • 返现自我肉体和精神的状态与模式
  • +
  • 在自己身上挖宝,彻底改变人生
  • +
+

记录的日记要常拿出来看看

记录日记时 — 问问自己的灵魂 然后写下真心话 — 灵魂日记
六度空间理论

+

七种成功者的习惯

    +
  • 习惯之一:积极主动别指望谁能推你走 — 主动行动,迈出第一步
  • +
  • 习惯之二:以终为始忠于自己的人生计划。— 设立人生目标并坚持
  • +
  • 习惯之三:要事第一选择当前该做的事
  • +
  • 习惯之四:追求双赢远离角斗场
  • +
  • 习惯之五:善于沟通换位思考的原则
  • +
  • 习惯之六:统合综效 1+1可以大于2
  • +
  • 习惯之七:不断更新全方位平衡自我
  • +
+

早睡是为了身体,早起是为了我们的内心。— sugiponn

+

晨间日记的格式

晨间日记必须要设立的栏目 — 工作/金钱/健康/人际关系
要记下当日的日期/天气/温度/湿度

+

纬度标签
工作方面:

+
    +
  • 能力开发/经营/管理/进货/工作人员/学习/资格/活动/企划案/想法/推广
    金钱方面:
  • +
  • 收入/指出/购入/股票/资产/储蓄/家用
    健康方面:
  • +
  • 饮食/运动/性/减肥/身体状况/锻炼身体/体重/脂肪
    人际关系方面:
  • +
  • 父母/家人/小孩/亲戚/情人/工作伙伴/朋友/熟人/联络事项/人脉网络
    兴趣方面以及其他:
  • +
  • 义工活动/阅读/运动/打扮/电影/音乐/游戏/喜欢的物品/今天必须要注意的事/美容/开心的事/购物
  • +
+

劳动 — 职业 — 工作 — 乐趣

+

三大原则和七大作战守则

    +
  • 原则1:时间不超过3分钟 — 减少养成习惯的成本

    +
  • +
  • 原则2:决定好写晨间日记的地方 — 为了养成习惯

    +
  • +
  • 原则3:只写一个字也没关系 — 不要有压力

    +
  • +
  • 作战守则1:开心写日记 — 反省固然重要,鼓励更有作用 — 开始写晨间日记前七天只写好事

    +
  • +
  • 作战守则2:ONE-TWO作战 — 把动作分布容易养成习惯 — 形成固定动作

    +
  • +
  • 作战守则3:巴普洛夫的狗 — 开始写日记时候要有一个仪式 — 心理暗示

    +
  • +
  • 作战守则4:奖励自己 — 当习惯养成并坚持到一定时间,给自己一个奖励

    +
  • +
  • 作战守则5:宣告天下 — 借由他人的鼓励或压力形成动力/也可找到同志

    +
  • +
  • 作战守则6:一千日作战法 — 不管什么样的习惯坚持1000日就会成为永远的习惯 — 具体做法为记录 第XXX篇 晨间日记

    +
  • +
  • 作战守则7:为了别人 — 写晨间日记不只是为了自己,而且更是为了别人,身边人的幸福最终会影响到自己

    +
  • +
+

应该先肯定自己,给自己打100分

    +
  • “总之,先… 病” 没有目标/没有梦想 — 回顾时一定会后悔!
  • +
  • “假如…,应该 病” 无法满足/没有动力 — 焦躁不安
  • +
  • 共同点:失落感/空虚/
  • +
+

解决办法— 设立一个情境

例:我要建立一个幸福的家庭 —> 因此我要创造很多快乐的回忆 —> 为了制造很多快乐的回忆 —> 我必须要到很多的地方去看看 —> 为了去很多地方看看,我觉得有辆车会更方便 —> 买车 —> 考驾照

+

不要总想自己哪里欠缺,要想自己想要得到什么,并且为了目标努力,你就会变的更完美。
拥有一个敢于大声喊出的梦想,不要羞涩,要为了梦想一步步的前进,最终达到梦想。

+

提到的另外的书

《培育梦想种子》《日记的力量》《成功人士的七个习惯》

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:《晨间日记的奇迹》读书笔记

+

文章作者:

+

发布时间:2016年10月27日 - 22:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2016/10/27/Read-The-miracle-of-the-morning-journal/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/08/29/browser-incompatibility-problem-solution/index.html b/2017/08/29/browser-incompatibility-problem-solution/index.html new file mode 100644 index 0000000000..4bfbcb5645 --- /dev/null +++ b/2017/08/29/browser-incompatibility-problem-solution/index.html @@ -0,0 +1,656 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 浏览器兼容性问题解决方案 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 浏览器兼容性问题解决方案 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

作为前端,浏览器就是你的战场,而不同的浏览器就是不同的敌人,敌人有相同之处,也各有特点,这不同的特点就是不同浏览器的兼容性,下面就聊聊浏览器的兼容问题,如何解决不同浏览器的兼容性。

+ +

普及:浏览器的兼容性问题,往往是个别浏览器(没错,就是那个与众不同的浏览器)对于一些标准的定义不一致导致的。俗话说:没有IE就没有伤害。

+

贴士:内容都是自己总结的,不免会出现错误或者bug,欢迎更正和补充,本帖也会不断更新。

+

Normalize.css

不同浏览器的默认样式存在差异,可以使用 Normalize.css抹平这些差异。当然,你也可以定制属于自己业务的 reset.css

+
1
<link href="https://cdn.bootcss.com/normalize/7.0.0/normalize.min.css" rel="stylesheet">
+

简单粗暴法

+
1
* { margin: 0; padding: 0; }
+

html5shiv.js

解决 ie9 以下浏览器对 html5 新增标签不识别的问题。

+
1
2
3
<!--[if lt IE 9]>
<script type="text/javascript" src="https://cdn.bootcss.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]-->
+ +

respond.js

解决 ie9 以下浏览器不支持 CSS3 Media Query 的问题。

+
1
<script src="https://cdn.bootcss.com/picturefill/3.0.3/picturefill.min.js"></script>
+

IE 条件注释

IE 的条件注释仅仅针对IE浏览器,对其他浏览器无效
image

+

IE 属性过滤器(较为常用的hack方法)

针对不同的 IE 浏览器,可以使用不同的字符来对特定的版本的 IE 浏览器进行样式控制
image
image

+

浏览器 CSS 兼容前缀

1
2
3
4
5
6
7
8
9
-o-transform:rotate(7deg); // Opera

-ms-transform:rotate(7deg); // IE

-moz-transform:rotate(7deg); // Firefox

-webkit-transform:rotate(7deg); // Chrome

transform:rotate(7deg); // 统一标识语句
+

补充: 目前可以采用自动化插件完成,插件名称叫做 Autoprefixer,他可以解析css文件并且添加前缀到css内容里。

+

Auroprefixer 添加到资源构建工具如:webpack后,就可以不用再手动补全浏览器前缀了,这里只需要你按照W3C的标准来书写css代码,剩下的工作就交给插件完成,目前webpackgulpgrunt都有相应的插件,是不是开心啊。

+

a 标签的几种 CSS 状态的顺序

很多新人在写 a 标签的样式,会疑惑为什么写的样式没有效果,或者点击超链接后,hover、active 样式没有效果,其实只是写的样式被覆盖了。

+

正确的a标签顺序应该是:==love hate==

+
    +
  1. link:平常的状态
  2. +
  3. visited:被访问过之后
  4. +
  5. hover:鼠标放到链接上的时候
  6. +
  7. active:链接被按下的时候
  8. +
+

完美解决 Placeholder

1
<input type="text" value="Name *" onFocus="this.value = '';" onBlur="if (this.value == '') {this.value = 'Name *';}">
+ +

清除浮动 最佳实践

1
2
3
4
.fl { float: left; }
.fr { float: right; }
.clearfix:after { display: block; clear: both; content: ""; visibility: hidden; height: 0; }
.clearfix { zoom: 1; }
+ +

BFC 解决边距重叠问题

当相邻元素都设置了 margin 边距时,margin 将取最大值,舍弃小值。为了不让边距重叠,可以给子元素加一个父元素,并设置该父元素为 BFC:overflow: hidden;

+
+

Lorem ipsum dolor sit.

+ +
+

Lorem ipsum dolor sit.

+
+ +

Lorem ipsum dolor sit.

+
+ +

IE6 双倍边距的问题

设置 ie6 中设置浮动,同时又设置 margin,会出现双倍边距的问题

+
1
display: inline;
+

解决 IE9 以下浏览器不能使用 opacity

1
2
3
opacity: 0.5;
filter: alpha(opacity = 50);
filter: progid:DXImageTransform.Microsoft.Alpha(style = 0, opacity = 50);
+

解决 IE6 不支持 fixed 绝对定位以及IE6下被绝对定位的元素在滚动的时候会闪动的问题

1
2
3
4
5
6
7
8
9
/* IE6 hack */
*html, *html body {
background-image: url(about:blank);
background-attachment: fixed;
}
*html #menu {
position: absolute;
top: expression(((e=document.documentElement.scrollTop) ? e : document.body.scrollTop) + 100 + 'px');
}
+

IE6 背景闪烁的问题

问题:链接、按钮用 CSSsprites 作为背景,在 ie6 下会有背景图闪烁的现象。原因是 IE6 没有将背景图缓存,每次触发 hover 的时候都会重新加载

+

解决:可以用 JavaScript 设置 ie6 缓存这些图片:

+
1
document.execCommand("BackgroundImageCache", false, true);
+

解决在 IE6 下,列表与日期错位的问题

日期 标签放在标题 标签之前即可
image

+

解决 IE6 不支持 min-height 属性的问题

1
2
min-height: 350px;
_height: 350px;
+ +

让 IE7 IE8 支持 CSS3 background-size属性

由于 background-size 是 CSS3 新增的属性,所以 IE 低版本自然就不支持了,但是老外写了一个 htc 文件,名叫 background-size polyfill,使用该文件能够让 IE7、IE8 支持 background-size 属性。其原理是创建一个 img 元素插入到容器中,并重新计算宽度、高度、left、top 等值,模拟 background-size 的效果。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
html {
height: 100%;
}
body {
height: 100%;
margin: 0;
padding: 0;
background-image: url('img/37.png');
background-repeat: no-repeat;
background-size: cover;
-ms-behavior: url('css/backgroundsize.min.htc');
behavior: url('css/backgroundsize.min.htc');
}
+

IE6-7 line-height 失效的问题

问题:在ie 中 img 与文字放一起时,line-height 不起作用

+

解决:都设置成 float

+

td 自动换行的问题

问题:table 宽度固定,td 自动换行

+

解决:设置 Table 为 table-layout: fixedtdword-wrap: break-word

+

让层显示在 FLASH 之上

想让层的内容显示在 flash 上,把 FLASH 设置透明即可

+
1
2
1、<param name=" wmode " value="transparent" />
2、<param name="wmode" value="opaque"/>
+

键盘事件 keyCode 兼容性写法

1
2
3
4
5
6
7
8
9
10
11
var inp = document.getElementById('inp')
var result = document.getElementById('result')

function getKeyCode(e) {
e = e ? e : (window.event ? window.event : "")
return e.keyCode ? e.keyCode : e.which
}

inp.onkeypress = function(e) {
result.innerHTML = getKeyCode(e)
}
+ +

求窗口大小的兼容写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 浏览器窗口可视区域大小(不包括工具栏和滚动条等边线)
// 1600 * 525
var client_w = document.documentElement.clientWidth || document.body.clientWidth;
var client_h = document.documentElement.clientHeight || document.body.clientHeight;

// 网页内容实际宽高(包括工具栏和滚动条等边线)
// 1600 * 8
var scroll_w = document.documentElement.scrollWidth || document.body.scrollWidth;
var scroll_h = document.documentElement.scrollHeight || document.body.scrollHeight;

// 网页内容实际宽高 (不包括工具栏和滚动条等边线)
// 1600 * 8
var offset_w = document.documentElement.offsetWidth || document.body.offsetWidth;
var offset_h = document.documentElement.offsetHeight || document.body.offsetHeight;

// 滚动的高度
var scroll_Top = document.documentElement.scrollTop||document.body.scrollTop;
+

DOM 事件处理程序的兼容写法(能力检测)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
var eventshiv = {
// event兼容
getEvent: function(event) {
return event ? event : window.event;
},

// type兼容
getType: function(event) {
return event.type;
},

// target兼容
getTarget: function(event) {
return event.target ? event.target : event.srcelem;
},

// 添加事件句柄
addHandler: function(elem, type, listener) {
if (elem.addEventListener) {
elem.addEventListener(type, listener, false);
} else if (elem.attachEvent) {
elem.attachEvent('on' + type, listener);
} else {
// 在这里由于.与'on'字符串不能链接,只能用 []
elem['on' + type] = listener;
}
},

// 移除事件句柄
removeHandler: function(elem, type, listener) {
if (elem.removeEventListener) {
elem.removeEventListener(type, listener, false);
} else if (elem.detachEvent) {
elem.detachEvent('on' + type, listener);
} else {
elem['on' + type] = null;
}
},

// 添加事件代理
addAgent: function (elem, type, agent, listener) {
elem.addEventListener(type, function (e) {
if (e.target.matches(agent)) {
listener.call(e.target, e); // this 指向 e.target
}
});
},

// 取消默认行为
preventDefault: function(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
},

// 阻止事件冒泡
stopPropagation: function(event) {
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
}
};
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:浏览器兼容性问题解决方案

+

文章作者:

+

发布时间:2017年08月29日 - 17:04

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2017/08/29/browser-incompatibility-problem-solution/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/08/30/Vue-VSCode-Snippets/index.html b/2017/08/30/Vue-VSCode-Snippets/index.html new file mode 100644 index 0000000000..bd53abb8ab --- /dev/null +++ b/2017/08/30/Vue-VSCode-Snippets/index.html @@ -0,0 +1,777 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Vue VSCode Snippets 自动生成Vue代码片段的VSCode扩展 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Vue VSCode Snippets 自动生成Vue代码片段的VSCode扩展 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

现代化的IDE已经把开发者变得越来越懒,但是我觉得这样挺好的,让工具完成手工反复重复的工作,提高工作效率的同时降低开发者的疲劳感,Vue VSCode Snippets就是这样一个VSC插件,它可以用简单的几个字母就敲出一整块代码片段,在学习和做VUE项目时可以极大地提高工作效率,下面就介绍一下插件的常见命令。

+ + +

此插件可用比较简单的写法生成代码片段,非常适合开发工作,减少代码工作量。

+

Script

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SnippetPurpose
vbaseSingle file component base
vbaseSingle file component base
vdataComponent data as a function
vmethodVue method
vcomputedVue computed property
vwatcherVue watcher with new and old value args
vpropsProps with type and default
vimportImport one component into another
vimport-cImport one component into another within the export statement
vimport-exportImport one component into another and use it within the export statement
vfilterVue filter
vmixinCreate a Vue Mixin
vmixin-useBring a mixin into a component to use
vc-directVue create a custom directive
vimport-libImport a library
vimport-gsapImport GreenSock with Timeline and Eases
vanimhook-jsUsing the Transition component JS hooks in methods
+ +

Template

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SnippetPurpose
vforv-for directive
vmodelSemantic v-model directive
vmodel-numSemantic v-model number directive
vonv-on click handler with arguments
vel-propsComponent element with props
vsrcImage src binding
vstyleInline style binding
vstyle-objInline style binding with objects
vclassClass binding
vclass-objClass binding with objects
vclass-obj-multMultiple conditional class bindings
vanimTransition component with JS hooks
+

Vuex

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
SnippetPurpose
vstoreBase for Vuex store.js
vgettersVuex Getter
vmutationVuex Mutation
vactionVuex Action
vstore-importImport vuex store into main.js
+

Extra (plaintext)

+ + + + + + + + + + + + + + + + + + + +
SnippetPurpose
gitignore.gitignore file presets
vincincrementer
vdecdecrementer
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Vue VSCode Snippets 自动生成Vue代码片段的VSCode扩展

+

文章作者:

+

发布时间:2017年08月30日 - 16:23

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2017/08/30/Vue-VSCode-Snippets/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/08/30/Vue-basic/index.html b/2017/08/30/Vue-basic/index.html new file mode 100644 index 0000000000..89b87abd8d --- /dev/null +++ b/2017/08/30/Vue-basic/index.html @@ -0,0 +1,739 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Vue基础 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Vue基础 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近公司技术选型选择了VUE,下面就记录一下在学习Vue中遇到的一些知识点,有些在官方手册里已经讲的很详细了,这里就简单带过,主要对cli产生的以及自行添加的一些项目目录进行一下解释,希望对正在学习Vue的同学提供一点帮助,如果本文有谬误之处还请各位指出,谢谢各位支持。

+ +

搭建项目

1
2
3
4
5
6
7
npm install --global vue-cli
vue init webpack my-project
cd my-project
npm install(推荐用cnpm install)
如果没有cnpm ,先安装cnpm镜像
npm install -g cnpm --registry=https://registry.npm.taobao.org
npm run dev
+

目录讲解

+
    +
  • build和config :项目开发和打包时候的相关配置;
  • +
  • node_modules :项目所需要的依赖文件;
  • +
  • src :主应用/页面相关文件;
      +
    • assets : 静态资源文件;
    • +
    • components :组件;
    • +
    • res:资源
        +
      • css: 公共css或是css预处理文件;
      • +
      • js: 公共js文件
      • +
      • img:公共图片
      • +
      +
    • +
    • router :路由配置文件;
    • +
    • views : 视图文件,其实也是vue组件。按照业务功能划分模块;
    • +
    • vuex : 状态管理的配置文件;
    • +
    • App.vue : 主组件;
    • +
    • main.js: 入口文件,初始化vue实例并使用需要的插件
    • +
    +
  • +
  • index.html : 主html页面;
  • +
  • dist:webpack打包生成的文件;
  • +
  • package.json:记录依赖相关信息
  • +
+
+

文件的加载顺序:

当我们执行命令 npm run dev的时候根据配置文件dev-server.js里的相关配置去加载webpack的相关配置文件 在webpack.base.conf里面entry入口文件就配置了app:'./src/main.js'

+

所以当我们在运行npm run dev的时候就开始通过main.js执行了。main.js 初始化vue实例并且加载相关配置插件,然后通过app.vue文件去访问各个组件

+

Build/dev-server.js主要完成以下几件事情:

    +
  1. 检查node和npm的版本;
  2. +
  3. 引入相关插件和配置;
  4. +
  5. 创建express服务器和webpack编译器;
  6. +
  7. 配置开发中间件(webpack-dev-middleware)和热重载中间件(webpack-hot-middleware);
  8. +
  9. 挂载代理服务和中间件;
  10. +
  11. 配置静态资源;
  12. +
  13. 启动服务器监听特定端口(8080);
  14. +
  15. 自动打开浏览器并打开特定网址(localhost:8080);
  16. +
+

Build/huild.js主要完成以下几件事情:

    +
  1. loading动画;
  2. +
  3. 删除创建目标文件夹;
  4. +
  5. webpack编译;
  6. +
  7. 输出信息
  8. +
+

配置文件

.babelrc

设置转码的规则和插件(使用es6语法必须安装插件)

+
1
npm install babel-preset-es2015
+ +

presets 字段是用来设定转码规则;

+

.editorconfig

配置文件编码格式的文件

+
    +
  • indent_style:  设置缩进风格,tab或者空格;
  • +
  • indent_size:  缩进的宽度;
  • +
  • tab_width:  设置tab的列数。默认是indent_size;
  • +
  • end_of_line: 换行符,lf、cr和crlf;
  • +
  • charset:  编码;
  • +
  • trim_trailing_whitespace: 设为true表示会除去换行行首的任意空白字符;
  • +
  • insert_final_newline:  设为true表明使文件以一个空白行结尾;
  • +
  • root: 表明是最顶层的配置文件,发现设为true时,才会停止查找.editorconfig文件;
  • +
+

.eslintignore

忽略不符合eslint规范的文件, (一般会忽略掉第三方引用的插件)

+

.eslintrc.js

定义代码规则

+

.gitignore

配置文件,用于配置不需要加入版本管理的文件

+

.VUE 文件解释

    +
  • template: 展示模板
  • +
  • import : 导入组件已经js文件
  • +
  • export default:
      +
    • data:数据源;
    • +
    • methods:方法;
    • +
    • mounted:页面加载之后执行的方法;
    • +
    • created:页面生成时加载的方法;
    • +
    +
  • +
  • style: 样式代码 其中scoped表示样式作用范围为本vue文件
  • +
+

网络访问

axios

    +
  1. 发送请求:
  2. +
+
1
2
3
4
5
6
7
axios#request(config);
axios#get(url[, config]);
axios#delete(url[, config]);
axios#head(url[, config]);
axios#post(url[, data[, config]]);
axios#put(url[, data[, config]]);
axios#patch(url[, data[, config]]);
+ +
    +
  1. 处理响应:
  2. +
+
    +
  • Promise语法;
  • +
  • 处理结果:then;
  • +
  • 处理异常:catch;
  • +
+
    +
  1. 拦截器(use/reject):
  2. +
+
1
2
3
axios.interceptors.response.use;
axios.interceptors.rquest.use;
reject(移除请求拦截)
+ +
    +
  1. 参数:
  2. +
+
    +
  • json(默认);
  • +
  • qs;
  • +
+

组件通信

    +
  • Prpos:父组件对子组件;
  • +
  • 自定义事件:子组件对父组件;
  • +
  • 消息总线:任意两个组件;
  • +
  • 状态管理:Vuex(适用于大型单页面开发)
  • +
+

路由

    +
  1. 配置
  2. +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Vue.use(Router)

export default new Router({
routes: [
{
path: '/',
name: 'Hello',
component: Hello
}
]
})

new Vue({
el: '#app',
router,
template: '<App/>',
components: { App }
})
+ +
    +
  1. 导航
  2. +
+
    +
  • push
  • +
  • replace
  • +
  • go
  • +
+
    +
  1. 参数传递
  2. +
+
    +
  • RESTful url参数
  • +
  • 参数查询 query
  • +
  • 锚点 hash: ‘#data’
  • +
+
    +
  1. 嵌套路由
  2. +
+
    +
  • Children
  • +
+
    +
  1. 钩子
  2. +
+
    +
  • beforeRouteEnter
  • +
  • beforeRouteLeave
  • +
+

状态管理

+

Vuex是什么?

+

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

+
+
    +
  • state里面就是存放的我们所要用到的状态;
  • +
  • mutations就是存放如何更改状态的方法 ,同步操作;
  • +
  • getters就是从state中派生出状态,比如将state中的某个状态进行过滤然后获取新的状态。
  • +
  • actions就是mutation的加强版,它可以通过commit
  • +
  • mutations中的方法来改变状态,最重要的是它可以进行异步操作。
  • +
  • modules顾名思义,就是当用这个容器来装这些状态还是显得混乱的时候,我们就可以把容器分成几块,把状态和管理规则分类来装。这和我们创建js模块是一个目的,让代码结构更清晰。
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Vue基础

+

文章作者:

+

发布时间:2017年08月30日 - 17:03

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2017/08/30/Vue-basic/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/08/30/npm-source/index.html b/2017/08/30/npm-source/index.html new file mode 100644 index 0000000000..6edb4c9b5a --- /dev/null +++ b/2017/08/30/npm-source/index.html @@ -0,0 +1,608 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + npm相关资料 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ npm相关资料 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

近两年前端突然呈爆发增长之势,node的出现起了重要的作用,而node之中有一个重要的功能就是npm包管理工具,下面就记录一下我学习npm之中遇到的一些问题及解决方案,仅供各位参考。

+ + +
+

npm全称Node Package Manager,是node.js的模块依赖管理工具。由于npm的源在国外,所以国内用户使用起来各种不方便。下面整理出了一部分国内优秀的npm镜像资源,国内用户可以选择使用。

+
+

国内优秀npm镜像

淘宝npm镜像

+

cnpmjs镜像

+

如何使用

有很多方法来配置npm的registry地址,下面根据不同情境列出几种比较常用的方法。以淘宝npm镜像举例:

+

1.临时使用

1
npm --registry https://registry.npm.taobao.org install express
+

2.持久使用

1
2
3
4
5
6
npm config set registry https://registry.npm.taobao.org

// 配置后可通过下面方式来验证是否成功
npm config get registry
// 或
npm info express
+

3.通过cnpm使用

1
2
3
4
npm install -g cnpm --registry=https://registry.npm.taobao.org

// 使用
cnpm install express
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:npm相关资料

+

文章作者:

+

发布时间:2017年08月30日 - 12:24

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2017/08/30/npm-source/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/10/11/use-GitHub-Hexo-Next-make-blog/index.html b/2017/10/11/use-GitHub-Hexo-Next-make-blog/index.html new file mode 100644 index 0000000000..2569163859 --- /dev/null +++ b/2017/10/11/use-GitHub-Hexo-Next-make-blog/index.html @@ -0,0 +1,658 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 如何利用GitHub-Hexo-Next搭建一个漂亮的技术博客 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 如何利用GitHub-Hexo-Next搭建一个漂亮的技术博客 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

+

开博客貌似并不是一件难事,现在有很多的方式开博客,而且现在博客貌似也不再流行,但是作为一个开发者,维护一个技术博客对自己的发展是很有好处的,出名者如阮一峰等,所以还是想要做一个技术博客的。

+
+
+

开博客可以选择网上现有的博客系统如博客园以及其他各大公司提供的博客系统,也可以选择如wordpress等博客程序自行搭建博客系统,前者省事,但可自定义设置不足不符合一个爱折腾的人的性格,后者复杂些,爱折腾的可以学着搭建,但是服务器域名是个让人头疼的事,还有数据库等操作,着实麻烦,如今又有了另外一个选择,那就是利用GitHub和一些静态的博客系统搭建一个纯静态的博客系统,不花钱又能折腾,而且还是命令行下的,装B感觉棒棒的,另外搭配时下流行的MarkDown语法,做笔记正合我意。

+
+ + +

技术栈选型

+

这里说是技术栈选型可能并不是很恰当,但又找不到合适的描述,就是把需要的技术介绍一下,如果还不会的,可以自行学习,或者看看我的其他文章。

+
+
    +
  • node(npm),现在node这么火,没用过都不好意思出门,但是如果你还不回的话,就先自行学习安装一下吧。
  • +
  • Hexo 静态博客程序,其实还有很多,只不过这个比较新,而且搭配Next非常漂亮,就选了它。
  • +
  • Next 可以说是Hexo的定制系统,不仅仅是做了个皮肤,简洁美观的配置项和官网说明深得我心。
  • +
+

搭建步骤(安装步骤)

安装Hexo Hexo官网

1
2
3
4
5
$ npm install hexo-cli -g // 安装hexo的脚手架工具
$ hexo init blog // 初始化博客
$ cd blog // 返回博客目录
$ npm install // 安装依赖
$ hexo server // 启动博客
+
+

怎么样五行代码就生成并运行了一个博客是不是超简单。
下面我们看一下生成的博客的目录

+
+
1
2
3
4
5
6
7
8
.
├── _config.yml
├── package.json
├── scaffolds
├── source
| ├── _drafts
| └── _posts
└── themes
+

_config.yml这是博客的配置文件,比如博客名称,副标题,作者等信息都在这个文件里设置。

+

package.json这是博客的依赖文件可以忽略

+

scaffolds这是博客的模板目录,当你要写一篇文章时,这里会有文章的默认类型。

+

source这是博客的网站资源,包括发布的文章(_posts)、关于、分类还有上传文件等。

+

themes这是博客的皮肤。

+

更多配置信息请查阅官网手册

+

安装Next主题 Next官网

1
2
$ cd your-hexo-site
$ git clone https://github.com/iissnan/hexo-theme-next themes/next
+ +

然后到_config.yml配置文件将主题配置改成next就可以使用next的皮肤了

+
1
theme: next
+

皮肤也有配置文件,为跟Hexo进行区分Hexo的配置文件称为站点配置文件, 皮肤配置文件称为主题配置文件

+

对两个配置文件进行简单配置后,符合需求的博客就搭建而成了,这里有个友好的建议,配置文件如果配置不正确将不能正确运行博客,所以在配置前务必保留好原始配置文件,注意配置时不要缺了空格,不要问我为什么知道这个。

+

更新博客主题

https://github.com/theme-next/hexo-theme-next/blob/master/docs/zh-CN/UPDATE-FROM-5.1.X.md

+

写博客

博客已经搭建好了,接下来就是写博客了,那么如何开始写博客呢,超级简单一行命令足以:

+
1
2
3
4
hexo new [layout] <title>  
// layout 为模板类型可以省略,title为文章标题,通常可以简写为如下
hexo new title

+

新建命令执行后在_posts的目录下就会生成一个你刚才命名的md后缀的文件,这就是一个MarkDown语法的文件,(如果不了解MarkDown语法的可以去学一下,很简单的符号语言,或者像我一样用支持MarkDown语法的编辑器来写文章。新建的文章打开内容如下

+
1
2
3
4
5
6
---
title: new-post
date: 2017-10-11 15:01:09
tags:
---

+

非常好理解,title就是标题,date为创建时间,tags是标签方便分类,但是这些并不全,还有些常用的分类没有写上,下面我将常用的进行补充

+
1
2
3
4
5
6
7
8
9
10
11
---
title: new-post
date: 2017-10-11 15:01:09
categories:
- NodeJS
- npm
tags:
- npm
- NodeJS
- rnpm
---
+

这样补充后就有了常见博客的分类和标签的功能,是不是很简单。

+

写完文章以后还要执行下面命令,生成静态页面

+
1
2
$ hexo generate // 将md后缀文件生成成静态html文件

+ + +

这样我们就完成了博客的搭建和博客的书写,到现在我们就已经有了一个本地的博客,那么如何将博客上传到GitHub上呢?

+

将Blog上传至GitHub

github是一个代码托管的平台,为了方便描述代码功能,它提供了README.md文件进行说明,但是为了更好的展现,也提供了gitpage的功能,博客是基于这个功能进行的扩展,那么如何用gitpage的功能来实现博客系统呢?

+

创建仓库

创建一个以你的GitHub账号为开头命名的仓库,格式如下

+
1
2
3
GitHub账号名称.github.io
// 如
lixuguang.github.io
+

然后到blog系统的配置文件_config.yml里配置一下上传路径

+
1
2
3
4
5
6
7
8
9
10
deploy:
type: git
repo: <repository url>

// 我的实例
deploy:
type: git
repo: git@github.com:lixuguang/lixuguang.github.io.git
branch: master

+

配置好就可以进行部署了,部署也很简单,只需要执行一下下面的命令。

+
1
2
3
4
$ npm install hexo-deployer-git --save // 安装上传工具

$ hexo deploy

+

稍等一会,如果没有出现什么错误信息,那么你的部署就成功了。之后你就可以访问你的博客了,博客地址如下:
https://你的github账号.github.io/
我的如下:
https://lixuguang.github.io/

+

现在你是不是已经学会如何利用github搭建一个静态的博客系统了呢,如果你还没有一个自己的技术博客,快来试试吧。

+

技巧

是不是觉得命令行还是挺麻烦的,要敲那么一大串字母,哈哈实际上这些常用命令是有缩写方式的,下面给大家介绍一下缩写方式。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ hexo server
// 简写
$ hexo s

$ hexo generate
// 简写
$ hexo g

$ hexo deploy
// 简写
$ hexo d

$ hexo new
// 简写
$ hexo n
+ +

另外每次发布之前最好执行以下命令,清理当前内容

+
1
2
$ hexo clean

+

以防出现冲突的情况,具体动作如下

+
1
2
3
4
$ hexo clean
$ hexo g -d // 文件生成后立即部署网站
$ hexo d -g // 部署之前预先生成静态文件

+ +

常见问题

    +
  1. SSH问题
  2. +
+
1
2
3
$ ssh-keygen -t rsa -C "lixuguang316@gmail.com"
// 填写你自己的github邮箱

+

敲三下回车,之后会在

+
1
2
C:\Users\Administrator\.ssh // windows下
open ~/.ssh // Mac下打开ssh文件
+

文件夹下生成两个文件id_rsa(私钥)、id_rsa.pub(公钥),在github上的SSH处添加新的ssh,然后将公钥内容贴到上面起个名字可以叫hexo,保存,然后在git bash下敲击

+
1
$ ssh git@github.com
+

然后敲yes就可以上传blog代码了
怎么样会了么?更多高阶玩法请阅读官方说明文档,文章如有谬误之处请各位指出,如果觉得文章对你有所帮助我将十分开心,如果你喜欢我的文章可以到我的github上点个fork,谢谢你的阅读。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:如何利用GitHub-Hexo-Next搭建一个漂亮的技术博客

+

文章作者:

+

发布时间:2017年10月11日 - 16:18

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2017/10/11/use-GitHub-Hexo-Next-make-blog/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/10/12/VSCode-ESLint/index.html b/2017/10/12/VSCode-ESLint/index.html new file mode 100644 index 0000000000..5298292a7e --- /dev/null +++ b/2017/10/12/VSCode-ESLint/index.html @@ -0,0 +1,597 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + VSCode ESLint JS代码静态检测工具 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ VSCode ESLint JS代码静态检测工具 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

VSCode是新一代IDE,方便灵活的插件安装方式让它强大无比,对前端友好,让它成为了越来越多前端开发者的首选IDE,编辑器虽好,但是要自己安装符合自己需求的插件才行,下面介绍一款插件,名叫ESLint,是用来静态检测js代码的工具,让你的js代码在编辑过程中就能找到错误,提高代码质量,好了废话不多说下面介绍如何安装和配置这款插件。

+ +

安装ESLint插件

打开VSCode编辑器,在左侧右下角有一个安装插件的图标,点击后就可以打开插件市场,输入ESLint,就会有个黄色的图标出现在你面前,不用犹豫双击它,稍等一会它就安装完了,是不是超简单。

+

安装NPM依赖

ESLint插件运行需要一些依赖,对于用过npm包管理工具的人来讲小意思啦,我把代码放到下面,需要的直接粘贴运行就好。

+
1
2
3
4
5
6
7
8
//全局安装eslint
npm i eslint -g

//如果用到html中的js校验
npm i eslint-plugin-html -g

//如果用到es2015语法
npm i babel-eslint -g
+

配置eslint配置文件到项目根目录

配置文件名称如下:
eslintrc.json
内容为:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
{
"plugins": [
// "react",
"html"
],
"env": {
"node": true,
"jquery": true,
"es6": true,
"browser": true
},
"globals": {
"angular": false
},
"parser": "babel-eslint",
"rules": {
//官方文档 http://eslint.org/docs/rules/
//参数:0 关闭,1 警告,2 错误
// "quotes": [0, "single"], //建议使用单引号
// "no-inner-declarations": [0, "both"], //不建议在{}代码块内部声明变量或函数
"no-extra-boolean-cast": 1, //多余的感叹号转布尔型
"no-extra-semi": 1, //多余的分号
"no-extra-parens": 0, //多余的括号
"no-empty": 1, //空代码块

//使用前未定义
"no-use-before-define": [
0,
"nofunc"
],

"complexity": [0, 10], //圈复杂度大于*

//定义数组或对象最后多余的逗号
"comma-dangle": [
0,
"never"
],

// 不允许对全局变量赋值,如 window = 'abc'
"no-global-assign": ["error", {
// 定义例外
// "exceptions": ["Object"]
}],
"no-var": 0, //用let或const替代var
"no-const-assign": 2, //不允许const重新赋值
"no-class-assign": 2, //不允许对class重新赋值
"no-debugger": 1, //debugger 调试代码未删除
"no-console": 0, //console 未删除
"no-constant-condition": 2, //常量作为条件
"no-dupe-args": 2, //参数重复
"no-dupe-keys": 2, //对象属性重复
"no-duplicate-case": 2, //case重复
"no-empty-character-class": 2, //正则无法匹配任何值
"no-invalid-regexp": 2, //无效的正则
"no-func-assign": 2, //函数被赋值
"valid-typeof": 1, //无效的类型判断
"no-unreachable": 2, //不可能执行到的代码
"no-unexpected-multiline": 2, //行尾缺少分号可能导致一些意外情况
"no-sparse-arrays": 1, //数组中多出逗号
"no-shadow-restricted-names": 2, //关键词与命名冲突
"no-undef": 1, //变量未定义
"no-unused-vars": 1, //变量定义后未使用
"no-cond-assign": 2, //条件语句中禁止赋值操作
"no-native-reassign": 2, //禁止覆盖原生对象
"no-mixed-spaces-and-tabs": 0,



//代码风格优化 --------------------------------------
"no-irregular-whitespace": 0,
"no-else-return": 0, //在else代码块中return,else是多余的
"no-multi-spaces": 0, //不允许多个空格

//object直接量建议写法 : 后一个空格前面不留空格
"key-spacing": [
0,
{
"beforeColon": false,
"afterColon": true
}
],

"block-scoped-var": 1, //变量应在外部上下文中声明,不应在{}代码块中
"consistent-return": 1, //函数返回值可能是不同类型
"accessor-pairs": 1, //object getter/setter方法需要成对出现

//换行调用对象方法 点操作符应写在行首
"dot-location": [
1,
"property"
],
"no-lone-blocks": 1, //多余的{}嵌套
"no-labels": 1, //无用的标记
"no-extend-native": 1, //禁止扩展原生对象
"no-floating-decimal": 1, //浮点型需要写全 禁止.1 或 2.写法
"no-loop-func": 1, //禁止在循环体中定义函数
"no-new-func": 1, //禁止new Function(...) 写法
"no-self-compare": 1, //不允与自己比较作为条件
"no-sequences": 1, //禁止可能导致结果不明确的逗号操作符
"no-throw-literal": 1, //禁止抛出一个直接量 应是Error对象

//不允return时有赋值操作
"no-return-assign": [
1,
"always"
],

//不允许重复声明
"no-redeclare": [
1,
{
"builtinGlobals": true
}
],

//不执行的表达式
"no-unused-expressions": [
0,
{
"allowShortCircuit": true,
"allowTernary": true
}
],
"no-useless-call": 1, //无意义的函数call或apply
"no-useless-concat": 1, //无意义的string concat
"no-void": 1, //禁用void
"no-with": 1, //禁用with
"space-infix-ops": 0, //操作符前后空格

//jsdoc
"valid-jsdoc": [
0,
{
"requireParamDescription": true,
"requireReturnDescription": true
}
],

//标记未写注释
"no-warning-comments": [
1,
{
"terms": [
"todo",
"fixme",
"any other term"
],
"location": "anywhere"
}
],
"curly": 0 //if、else、while、for代码块用{}包围
}
}
+

eslint就是根据这个配置表来进行js语法校验的。

+

最后重启VSCode完成插件安装

重启后控制台显示ESLint server is running说明插件已经生效,好啦接下来就愉快的写代码吧。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:VSCode ESLint JS代码静态检测工具

+

文章作者:

+

发布时间:2017年10月12日 - 14:42

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2017/10/12/VSCode-ESLint/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/10/12/us-gitment/index.html b/2017/10/12/us-gitment/index.html new file mode 100644 index 0000000000..56d80f42e4 --- /dev/null +++ b/2017/10/12/us-gitment/index.html @@ -0,0 +1,620 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 给博客添加基于github-issue的评论系统 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 给博客添加基于github-issue的评论系统 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

上篇文章介绍了如何利用github+hexo+next制作一个博客系统,但是由于这个博客系统是基于静态页面的,所以无法完成评论功能,博客怎么可以没有评论功能呢,当然next主题提供了几个选项,但是国内你知道的,提供的几个评论功能都不是太好用,于是就上网找了好多资料,功夫不负有心人,让我找到了一个名曰gitment的基于github-issue的评论插件,下面就介绍大家如何使用这个插件。

+ +

第一步 注册一个小程序(OAuth Application)

点击https://github.com/settings/applications/new注册

+
    +
  • Application name 应用名称 这里随便写,我写的就是gitment
  • +
  • Homepage URL 主页地址,你可以写你的博客地址,我写的是https://lixuguang.github.io/
  • +
  • Application description 应用描述,这里随便写点什么,反正是自己用。
  • +
  • Authorization callback URL 这个比较重要,请填写你的博客地址,我的是https://lixuguang.github.io/
  • +
+

点击确定以后你会获得两个关键信息,下一步配置时会用到

+
    +
  • Client ID
  • +
  • Client Secret
  • +
+

第二步 修改主题配置文件,添加gitment评论功能

因为用的是next主题,所以配置文件地址如下:
themes/next/_config.yml

+

1、在其中添加:

1
2
3
4
5
6
7
8
9
# Gitment
# Introduction: https://imsun.net/posts/gitment-introduction/
gitment:
enable: true
githubID: yourid // 我的是lixuguang
repo: yourrepo // 我的是lixuguang.github.io 必须跟githubID保持一致的用户名
ClientID: yourid // 上面开通程序获得的ClientID
ClientSecret: yoursecret // 上面开通程序获得的Client Secret
lazy: false //是否需要点击展开评论才能可见评论,一般设置为false
+

一定要注意空格,不然会报错的,别问我咋知道的

+

2、然后在主题的配置语言环境的文件添加一句话

en.yml增加:

+
1
gitmentbutton: Show comments from Gitment
+ +

zh-Hans.yml增加:

+
1
gitmentbutton: 显示 Gitment 评论
+

如果是中文网站英文配置也可以不用写。

+

3、添加新的Dom结构

修改主题layout/_partials/comments.swig
在最后一个elseif分支后添加一个elseif分支:

+
1
2
3
4
5
6
7
{% elseif theme.gitment.enable %}
{% if theme.gitment.lazy %}
<div onclick="ShowGitment()" id="gitment-display-button">{{ __('gitmentbutton') }}</div>
<div id="gitment-container" style="display:none"></div>
{% else %}
<div id="gitment-container"></div>
{% endif %}
+

4、 在主题下layout/_third-party/comments/目录下中添加文件gitment.swig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{% if theme.gitment.enable %}
{% set owner = theme.gitment.githubID %}
{% set repo = theme.gitment.repo %}
{% set cid = theme.gitment.ClientID %}
{% set cs = theme.gitment.ClientSecret %}
<link rel="stylesheet" href="https://imsun.github.io/gitment/style/default.css">
<script src="https://imsun.github.io/gitment/dist/gitment.browser.js"></script>
{% if not theme.gitment.lazy %}
<script type="text/javascript">
var gitment = new Gitment({
id: window.location.pathname,
owner: '{{owner}}',
repo: '{{repo}}',
oauth: {
client_id: '{{cid}}',
client_secret: '{{cs}}',
}});
gitment.render('gitment-container');
</script>
{% else %}
<script type="text/javascript">
function ShowGitment(){
document.getElementById("gitment-display-button").style.display = "none";
document.getElementById("gitment-container").style.display = "block";
var gitment = new Gitment({
id: document.location.href,
owner: '{{owner}}',
repo: '{{repo}}',
oauth: {
client_id: '{{cid}}',
client_secret: '{{cs}}',
}});
gitment.render('gitment-container');
}
</script>
{% endif %}
{% endif %}
+

然后在主题下layout/_third-party/comments/index.swig文件中引入gitment.swig文件:

+
1
{% include 'gitment.swig' %}
+

在主题下source/css/_common/components/third-party/目录下添加gitment.styl文件

此配置文件为gitment的样式文件,需要修改样式可以在这里进行书写,这里修改一下按钮样式,另外将聊天框于文章框样式统一

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#gitment-display-button{
display: inline-block;
padding: 0 15px;
color: #0a9caf;
cursor: pointer;
font-size: 14px;
border: 1px solid #0a9caf;
border-radius: 4px;
}
#gitment-display-button:hover{
color: #fff;
background: #0a9caf;
}
#comments {
margin: 0;
padding: 40px;
background: #fff;
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.06), 0 1px 5px 0 rgba(0,0,0,0.12);
}
+

然后在主题下source/css/_common/components/third-party/third-party.styl文件中引入相应的CSS样式即可:

+
1
@import "gitment";
+

经过以上操作,gitment就被引入到你的博客里了。

+

现在就可以让大家对你写的文章进行评论啦,怎么样是不是又学到啦,喜欢我的文章就请关注我的github吧。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:给博客添加基于github-issue的评论系统

+

文章作者:

+

发布时间:2017年10月12日 - 10:50

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2017/10/12/us-gitment/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/10/12/web-quanzhan/index.html b/2017/10/12/web-quanzhan/index.html new file mode 100644 index 0000000000..d7b0c84fbf --- /dev/null +++ b/2017/10/12/web-quanzhan/index.html @@ -0,0 +1,606 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《Web全栈工程师的自我修养》读书笔记 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《Web全栈工程师的自我修养》读书笔记 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

近日工作不是那么忙,所以有时间看看书,这本书之前就已经读过一遍,隔了一段时间已经有所忘记,所以再次拾起,重新回顾一下。.

+ + +

什么是全栈

“全栈”是个外来词,翻译自英文full-stack,此处的栈指的是为了完成项目而使用的一系列技术的合集,不是堆栈概念中的栈。

+

“全端”工程师是指能够完成pc端、移动端等多终端设备适配的情况

+

什么是全栈工程师

+

全栈工程师是指一个能够处理数据库、服务器、系统工程、客户端等所有工作的的工程师,根据项目不同,可能是移动栈、Web栈,或者原生应用程序栈。

+
+

简单来说全栈工程师就是一个人能搞定一个项目,全能大神一样的人物。

+

一个Web产品典型的技术栈

+

服务器+数据库+服务器端编程语言+前端编程语言

+
+
+

全栈工程师技术的兴起有两个重要原因:技术的发展和PaaS(Platform as a Service,平台即服务)服务的平台越来越多。

+
+

全栈框架———MEAN

+

MongoDB-Express-AngularJs-Node.js
前后端采用一种编程语言JavaScript

+
+

全栈工程师的要求

一专多长

在一个领域里至少达到高级的级别,然后再去向上游或者下游延伸

+

关注商业目标

公司聘请你是为了让你产生利润,并不关心你会什么,所以选择技术栈时要考虑的是如何降低公司的成本或者提高收入。

+

关注用户体验

产品的最终目标是满足客户的需求,所以作为全栈工程师必须要关注用户体验。

+
+

这是一些作为全栈工程师我整理出来的干货,这本书本身并不是一本技术性很强的书,倒像是一位过来人介绍些经验,适合刚入职场或者进入职场不久的人,在前端领域比较迷茫时看一看,书中介绍了作者读过的一些书,很有参考性,推荐大家阅读。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:《Web全栈工程师的自我修养》读书笔记

+

文章作者:

+

发布时间:2017年10月12日 - 17:37

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2017/10/12/web-quanzhan/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/12/12/Git-Shell/index.html b/2017/12/12/Git-Shell/index.html new file mode 100644 index 0000000000..59b360841e --- /dev/null +++ b/2017/12/12/Git-Shell/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Git的常用命令 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Git的常用命令 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

指令表

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
指令含义备注
git add .提示增加文件.代表所有
git commit -m“说明内容” 提交到本地服务器
git status显示修改信息
git pull从网络服务器拉 更新最新版本
git push上传最新版本
git branch查看当前分支
git checkout develop切换到develop模式
git merge master从master合并过来
git push origin develop提交
git clone git@192.168.2.10:bat-web.git从服务器克隆
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Git的常用命令

+

文章作者:

+

发布时间:2017年12月12日 - 12:20

+

最后更新:2019年12月31日 - 10:38

+

原始链接:https://blog.lifesli.com/2017/12/12/Git-Shell/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/12/27/Actions/index.html b/2019/12/27/Actions/index.html new file mode 100644 index 0000000000..c5ca00bde0 --- /dev/null +++ b/2019/12/27/Actions/index.html @@ -0,0 +1,607 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 巧妙利用Acitons进行博客的自动构建 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 巧妙利用Acitons进行博客的自动构建 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

忙活了很久很久,终于不那么忙了,也终于想起来我还有个博客没有运行,哎,虽然用Hexo可以享受Github提供的免费的空间和域名,但是命令行敲代码的方式实现博客编写还是有些不方便,尤其是需要编程环境的时候,最近公司在用CICD的方式对代码进行构建,我就在想有没有一种方式让博客也可以变成这样,于是我就在网上搜Github Hexo 自动构建 这些个词,还真让我搜到了一种方法而且还是Github自己提供的,下面隆重请出 GitHub Actions

+ + +

GitHub Actions 是什么

GitHub Actions 由 GitHub 官方推出的工作流工具。典型的应用场景应该是 CI/CD,类似 Travis 的用法。如果不知道 CI/CD&Travis 感兴趣的建议去了解一下,下面不展开说明,直接说怎么用就好。

+

前期准备

在使用 GitHub Actions 之前我们先来看看我们有什么;
首先我们有一个放博客程序的地方,我这里是叫做 blog-source ,另外呢有一个通过 hexo g 创建出来的静态网站,为了存放它而建的另一个仓库,我这里是叫做lixuguang.github.io,也就是说我们现在是有这样两个仓库。
|仓库|作用|
|-|-|
|blog-source|放博客源代码|
|lixuguang.github.io|放博客生成代码|

+

生成密钥

因为 GitHub Actions 它需要访问我的 blog-source 仓库的代码所以必须要有密钥,密钥大家应该熟悉了,创建博客的时候也是创建了一个公钥和私钥用来在本地往 lixuguang.github.io 这个仓库提交代码
这里呢我们用下面的命令生成密钥。

+
1
ssh-keygen -t ed25519 -f ~/.ssh/github-actions-deploy # 连按三次回车即刻
+

命令执行完成后,我们会得到两个文件 github-actions-deploygithub-actions-deploy.pub 两个文件,第一个是私钥,第二个是公钥。
|名称|解释|
|-|-|
|github-actions-deploy|私钥|
|github-actions-deploy.pub|公钥|

+

接下来的步骤一定要好好看,因为我在这个地方被卡住好多次,就是因为有的文章说的并不正确,或者至少是讲的不够仔细,这里我会仔细地说明一下。

+

配置 GitHub 仓库

配置博客源代码仓库

我这里的源代码是放在 blog-source 中,所以我现在要给源代码仓库配置私钥,配置过程如下:
打开 blog-source 仓库,选择 settings,然后选中 secrets , 再点击 Add new secrets,照着下面填写内容
|字段|值|
|-|-|
|Name|HEXO_DEPLOY_PRI(名称自动构建时有用)|
|Value|github-actions-deploy|

+

配置博客源代码仓库

我这里生成的博客静态代码是放在 lixuguang.github.io 中,所以我现在要给静态代码仓库配置公钥,配置过程如下:
打开 lixuguang.github.io,选择 settings,然后选中 keys,再点击 Add deploy key,照着下面填写内容
|字段|值|
|-|-|
|Title|HEXO_DEPLOY_PUB|
|Key|github-actions-deploy.pub|

+

编写 Actions 脚本

经过上面一系列的准备操作,终于来到了编写自动构建脚本的环节,构建脚本如下,如果按照上面我做的操作一步步来的话,那么这一步你可以直接copy啦

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
name: Deploy Blog

on: [push] # 当有新push时运行

jobs:
build: # 一项叫做build的任务

runs-on: ubuntu-latest # 在最新版的Ubuntu系统下运行

steps:
- name: Checkout # 将仓库内master分支的内容下载到工作目录
uses: actions/checkout@v1 # 脚本来自 https://github.com/actions/checkout

- name: Use Node.js 10.x # 配置Node环境
uses: actions/setup-node@v1 # 配置脚本来自 https://github.com/actions/setup-node
with:
node-version: "10.x"

- name: Setup Hexo env
env:
HEXO_DEPLOY_PRI: ${{ secrets.HEXO_DEPLOY_PRI }} # 这里是上面配置的私钥名称
run: |
# set up private key for deploy
mkdir -p ~/.ssh/
echo "$HEXO_DEPLOY_PRI" | tr -d '\r' > ~/.ssh/id_rsa # 配置秘钥
chmod 600 ~/.ssh/id_rsa
ssh-keyscan github.com >> ~/.ssh/known_hosts
# set git infomation
git config --global user.name 'lixuguang' # 换成你自己的名字
git config --global user.email 'lixuguang@gmail.com' # 换成你自己的邮箱
# install dependencies
npm i -g hexo-cli # 安装hexo
npm i

- name: Deploy
run: |
# publish
rm -rf .deploy_git # 如果上次构建失败这句命令会清除上次失败的代码
hexo generate && hexo deploy # 执行部署程序


+ +
+

通过以上这些步骤的操作,如果没什么意外的话,博客的自动构建就完成了,之后只要你提交新的文章到博客源代码仓库,它将自动帮你生成并发送到博客的静态代码仓库,再也不用执行hexo g -d啦,如果这篇文章对你有用,欢迎follow我或打赏一下这篇文章,感谢阅读。

+

ps:这里有个小坑需要注意一下,因为博客的皮肤也是另外一个git仓库,如果你在本地构建好用但是线上构建博客不显示了,需要注意下是不是皮肤没有上传到博客源码仓库,这里我遇到了,希望你不会因此困扰,拜拜~

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:巧妙利用Acitons进行博客的自动构建

+

文章作者:

+

发布时间:2019年12月27日 - 22:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2019/12/27/Actions/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/12/27/Vue-Component-Communication/index.html b/2019/12/27/Vue-Component-Communication/index.html new file mode 100644 index 0000000000..cbeece5785 --- /dev/null +++ b/2019/12/27/Vue-Component-Communication/index.html @@ -0,0 +1,615 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端面试Vue篇:Vue组件通信的几种方式 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端面试Vue篇:Vue组件通信的几种方式 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

公司这两年兴起了前后端分离的热潮,在选用三大技术框架时,vue凭借其学习曲线低深受公司程序员的喜爱,因此我们也就走上了vue学习的不归路,在招聘招揽小伙伴时有那么一些问题经常被问起,vue组件间通信就是其中之一,下面我们来聊一聊vue组件通信有哪些方式。

+ + +

props$emit

啥也不了解的小伙伴应该也知道这种方式吧,这是最最基础的通信方式了,父子组件通信基本都用它。父组件向子组件传递数据的时候通过prop传参,子组件中通过$emit传递给父组件,父组件在触发子组件$emit方法时得到子组件数据。实例如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 父组件 parent.vue

Vue.compinent('parent', {
template: `
<div>
<p>父组件</p>
<child :message="message" @getChildrenData="getChildrenData"></child>
</div>`,
data() {
return {
message:'Hello lixuguang'
}
},
methods: {
/**
* 执行子组件触发的事件方法
*/
getChildrenData(data){
console.log(data)
}
}
})

// 子组件 child.vue

Vue.component('child', {
props:['message'], // 得到父组件传过来的数据
data() {
return {
childMessage: this.message
}
},
template:`
<div>
<input type="text" v-model="childMessage" @input="emitParentData(childMessage)">
</div>
`,
methods: {
emitParentData(data) {
this.$emit('getChildrenData', data) // 父组件触发时给父组件传值
}
}
})

// App.vue
var app = new Vue({
el: "#app",
template: `
<div>
<parent></parent>
</div>
`
})
+

解析代码:

+
    +
  1. 父组件通过message属性将数据传递给子组件,并且通过getChildrenData事件来监听子组件出发的事件;
  2. +
  3. 子组件通过props获得父组件传过来的数据,并且通过this.$emit触发了getChildrenData事件;
  4. +
+

$attrs$listeners

前一种方法我们完成了父子组件的数据通信,那你有没有想过如果有多层嵌套的数据最上层要往最下层传值怎么办,前一种方法只能一层一层的往下传,可是这样太麻烦了,那么有没有什么方法能够一次传到你想要传到的位置呢,当然是有的,那就是接下来要说到的$attrs$listeners
假设我们现在有三层包含关系的组件,分别是level1/level2/level3,level1 > level2 > level3, > 表示包含关系。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// level3.vue

Vue.component('level3', {
template:`
<div>
<input type="text" v-model="$attrs.level3Message" @input="emitLevel3Data($attrs.level3Message)">
</div>
`,
methods: {
emitLevel3Data(data) {
this.$emit('getLevel3Data', data)
}
}
})

// level2.vue

Vue.component('level2', {
props:['level2Message'],
data(){
return {
level2Message:this.level2Message
}
},
template:`
<div>
<input type="text" v-model="level2Message" @input="emitLevel2Data(level2Message)">
<level3 v-bind='$attrs' v-on='$listenrs'></level3>
</div>
`,
methods: {
emitLevel2Data(data) {
this.$emit('getLevel2Data', data)
}
}
})

// level1.vue

Vue.component('level1', {
data(){
return {
level3Message:"I am Level3",
level2Message:"I am Level2"
}
},
template:`
<div>
<h1>这是level1中的内容</h1>
<level2 :level2Message="level2Message"
:level3Message="level3Message"
@getLevel2Data="getLevel2Data()"
@getLevel3Data="getLevel3Data()"
></level2>
</div>
`,
methods: {
getLevel2Data(data) {
console.log('这是来自level2的数据', data)
},
getLevel3Data(data) {
console.log('这是来自level3的数据', data)
}
}
})

// App.vue

var app = new Vue({
el: "#app",
template: `
<div>
<level1></level1>
</div>
`
})
+

解析代码:

+
    +
  1. level3组件能直接触发getLevel3Data是因为level2组件在调用level3组件时使用v-on绑定了$listeners属性;
  2. +
  3. 通过v-bind绑定了$attrs属性,level3组件可以直接获取到从level1传下来的props;
  4. +
+

v-model

父组件通过v-model传递值给子组件时,会自动传递一个value的props户型,在子组件中可以通过this.$emit(‘input’,val)自动修改v-model绑定的值。

+

```

+

未完待续~

+
+

作者:李旭光
引用请标明出处

+
+

前言

+
+

作者:李旭光
引用请标明出处

+
+

前言

+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端面试Vue篇:Vue组件通信的几种方式

+

文章作者:

+

发布时间:2019年12月27日 - 22:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2019/12/27/Vue-Component-Communication/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/12/30/begin-learn-java/index.html b/2019/12/30/begin-learn-java/index.html new file mode 100644 index 0000000000..7d1198a8ba --- /dev/null +++ b/2019/12/30/begin-learn-java/index.html @@ -0,0 +1,591 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 重拾java开发技能 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 重拾java开发技能 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近愈发觉得越想往上走,越不能局限在前端开发的领域,再往上走前后端都是通的,最近在知乎上看到一句话,说是到了架构师甚至专家的层面以后,就没有什么前端架构师或者前端专家和后端架构师、后端专家了,只有前端出身的技术专家,和后端出身的技术专家,越往上越注重人的综合能力,当一个人能够解决一个项目的技术选型、架构等工作后,对于公司或者团队来说,那就是个巨大的宝藏。

+ + +

公司这一年

最近对于自己的发展有一些迷茫,这一年公司前端的发展可以说是几经转折,我自己呢也一直在摇摆到底是做管理还是做技术,也参加了公司组织的部门经理的答辩,在部门前端的管理工作中也切实的了解到自己在为人处事方面不够圆润。所以目前也算是断了从事管理的念头,更希望能在技术上更进一步,前端目前看来已经不算是我的短板,而我的短板就是只会前端,一直在前端周围打转,其实如果不是看了那篇帖子,以及公司对专家岗位的要求,我可能还会更进一步在前端方向深入研究,但目前看更紧急的应该是补充一下后端的开发知识了,于是上周末开始我就开始了java的学习

+

为什么选择java

为什么选择java作为后端入门,实话讲好多前端开发应该都会问这个问题,明明有更熟悉的nodejs可以作为后端技能进行扩展,我这里的理由是目前大多数公司的包括外面公司的开发人员大都还是以java作为主要语言作为后端编写的选择,另外前端js中好多的设计也是借鉴或者照搬了java中的一些思想,可以说在学习java过程中也会自然而然的提高对js的理解,更重要的是,java相对于其他语言来说资料也更多,上手也更容易,因为这些因素吧,最终我选择了java作为后端的主要学习目标。

+

怎么学习java

java上大学的时候实际有系统的学过的,只是实习之后就再也没有使用过,如今9年过去了,java对于我可能也只剩下些零星的记忆,说实话刚一开始怎么学,从哪里学让我都有点无从下手,这里还要感谢一下我后端的开发伙伴,给了我很多很好的建议,看书的话大都是基础的太基础,实战的又经常忽略基础,最终我打算还是以视频教程2.5倍速快速过一遍java基础,然后再深入学习一下springboot框架,最后再进行实战,以此掌握java开发技能。

+

开始学习java

最终我选择了在B站上看黑马的java基础+实战课程的教学视频,说实话黑马的教学视频还是讲的很仔细的,老师讲的也很有趣,只是一节课10多分钟,只有一个知识点,对于我来说还是有些慢,所以我就开了2.5倍速加快进就这么着看,上周末两天时间,看了130多课,今天的内容记忆不太深刻,趁着不是那么忙又看了30多课,感觉收获还是满满的,接下来的每一天都会看上30课左右,希望自己能在3个月的时间完全上手java开发,相信我可以做到。

+

立个Flag

从今天起,每天都要把自己学习的进度做个总结,看看这一天自己收获了多少,希望30岁这年我重新起步,迈向更高更好的未来。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:重拾java开发技能

+

文章作者:

+

发布时间:2019年12月30日 - 21:37

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2019/12/30/begin-learn-java/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/12/31/GoodBye-2019/index.html b/2019/12/31/GoodBye-2019/index.html new file mode 100644 index 0000000000..eb350d4ceb --- /dev/null +++ b/2019/12/31/GoodBye-2019/index.html @@ -0,0 +1,621 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 再见2019 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 再见2019 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

不知不觉又是一年,今天是2019年的最后一天,也是我30岁的最后一天,回顾这一年有收获有遗憾。翻出了18年的总结,看到了18年底对于自己19年的规划,现在回过头来看看哪些完成了。

+

18年底定的计划

学习技术

1. 深入学习客户端开发(全年)

18年客户端的项目也做了几个,Electron的开发能力经验更多了,之前只是用html+css+js做前端页面的开发,今年又增加了跟vue框架的结合,成功在多个项目中实践,只是一直没有形成规范感觉还是少了一环,原来一直在犹豫Electron到底算不算前端的技术栈之一,后来也算是想明白了,大前端范畴,只要是面向用户的都算是前端该做的范畴,所以Electron当然也算是前端应该掌握的技术栈,20年会继续学习客户端技术,把公司相关规范制定和落地。

+

2. 学习前端自动化测试相关知识(2019年3月前)

18年关注了前端自动化测试相关的知识,本打算19年好好学习一下,在公司项目中实践,但是今年前后端工作量大增,前端人员的精力有限,加之成本的考虑,并没有开展起来,不过这方面的技能储备已经具备了,年底两次关于前端自动化的测试也将前端自动化的思想普及到了公司前端的团队里,20年是公司强调质量的一年,相信20年前端自动化测试将有机会在公司一些项目中落地

+

3. 学习并掌握TS (2019年5月前)

18年就看到了TS将会成为一门比较活的开发语言,目前来说已经具备了做TS开发的技能,只是TS更适合大型的长期的项目,将更好的保障项目的代码质量,快速迭代和小型项目并不是很适合。前端团队里没有开花,没想到居然在开发团队里见到了曙光,感谢开发的小伙伴愿意接受这门前端新技术,相信有一个点采用后看到了优势,后续普及工作将会更好开展。

+

4. 学习并掌握React(2019年7月前)

18年Vue已经在公司前端全面普及开来,但是市场上React仍然是占据更多的份额,而且RN还能做App开发,所以还是一门必须要掌握的技术,今年看了一本React相关的书,也看了一些相关的视频教程,还参加了公司同事开展的React分享,虽然没有在项目中实际使用过,但是理论知识已经掌握了,可以进行小的Demo开发,20年要用React写一个开源的项目,做到学有所用,学以致用。

+

5. 学习前端持续集成的相关知识(2019年9月前)

19年公司强调了代码必须走线上构建的要求,前端工程化和自动构建持续集成的工作可以说是工作所迫,这方面的技能已经Get到了,另外令我感到高兴的是,通过对Github-Actions的配置,我的博客可以像提交代码一样提交文章了,而且也实践了自动构建的相关技术,很开心。

+

6. 学习Docker虚拟化技术( 2019年10月前)

这个任务没有达成,原本是希望能够通过Docker统一前端开发环境,不过Docker的适用场景还是以部署环境为主,Docker今年貌似热度也降下来了,今年K8s越来越火,不管怎样,持续关注新技术。

+

整理计划

1. 养成写博客的习惯,至少2天一篇,可以是技术博客,也可以是其他。

今年实在是太忙了,别说两天一篇文章,就连一个月一篇的量都没能达成,不过在年底我终于把博客又重新激活了,希望自己20年能够把博客坚持写下来,很简单,积累自己,打造个人品牌。

+

2. 将常用的方法和功能做成插件,开源给公司使用

今年4月开始,更多的精力都投在了部门的前端管理工作,说实话也迷茫也难过,这份迷茫终于在19年年底解开了,没能在技能层面有更高的提升,以及在代码层面给公司带来价值我觉得还是有些遗憾,20年希望自己能够在技术上有更大的进步,技术深度上/广度上/架构层面/后端技能都能有长足的进步。

+

读书计划

1. 每周读完一本书,并写一篇读后感

2019年读了25本书,每读完一本都会写一篇读后感,虽然没能保证每周读一本书,但是基本保证了每两周读完一本书,算是完成了50%的目标吧。20年继续努力,读书使人聪明,因为不聪明所以更要多读书。

+

部门前端计划

加强各设计组前端之间的交流

+

设计组的出现导致前端交流不畅,2019年加强与各设计组前端的交流,掌握各位前端的技术能力。

+
+

没想到一语成第,设计组居然在4月份将前端整个拿出来构成了一个前端组,由我来带,更没想到的是居然在19年年底又解散到项目团队了,这9个月的时间可以说有苦有泪也有甜,前端小伙伴们为了同一个目标,共同努力,尽最大的努力完成公司的业务,可以说为了这个目标,这一年我付出了很多很多,在这方面愧对了家人孩子,虽然最后前端还是拆分了,可以说我能力有限,但是我心里可以说是无愧,希望尽到项目团队里的小伙伴们也能开心工作,迅速成长,虽然不再在一个团队里,但是,我仍是你们的光妈。

+

前端俱乐部推动

+

继续每周2小时的兴趣交流,交流形式不限于ppt,要求各自建立技术博客做笔记。

+
+

俱乐部的活动最后还是没能坚持下来,大家工作比较忙也好,我自己的原因也好,确实没能坚持下来,20年还是希望能够将俱乐部活动组织起来,算是大家交流的机会也好,帮助大家开阔眼界也好,20年一定要再做起来!

+

进行梯队划分建设

+

前端今年人数明显增多,但各层级职责并没有区分,19年抽时间进行梯队建设。

+
+

19年前端从14人增加到了34人,可以说人数上翻翻了,随着负责人制的执行,梯队建设的工作算是完成了50%,但是能力提升的工作可以说还是做的不到位,应该有机会成长为高工的小伙伴没能升上去,这我有一定的责任,20年虽然团队不再,但是希望你们都坚持努力学习,迈向更高的台阶。

+

引入前端工程化工具和思想

+

目前前端工程化已经非常成熟,希望能将成熟的技术和工具引入。

+
+

19年,随着Vue的普及和熟练,模块化开发组件化开发的思想已经深入到每一个前端的心中,现在大家都知道该如何使用这些技术和工具,希望大家能不仅仅局限在用也能了解了解其中原理,至少对常用的配置有所了解。

+

提升整体前端开发的能力

+

目前整体前端开发能力还相对较弱,19年通过培训等方式提升整体前端开发的能力

+
+

19年能力提升的工作做的太少了,没什么好说的,希望20年这方面工作能够投入更多的精力,让更多的小伙伴成长起来,让我们一起加油吧。

+

生活目标

每天陪孩子读书一小时

跟上面的工作和个人目标比起来,这个任务可以说完成度0%,这也是我觉得愧对家人的地方,为了工作,对家人的关注少了很多很多,以至于桐桐晚上都不愿意跟我一起睡觉,20年我要好好对她,做个好的爸爸,孩子明年就三岁了,去到幼儿园会认识更多的朋友,希望她会跟她的小伙伴说,我有个爱我的爸爸。

+

减肥

减肥这件事可以说是真的对我来说太难了,看着自己马上破三位数(kg)的体重,心里无限难过,每次媳妇儿问我啥时候去健身,我都感到像被闪电击中了似的,办了健身卡被人嘲笑成了洗澡卡,20年,减不到150斤我就。。。。

+

写在最后

19年太多的遗憾,太多的后悔莫及,当然也有一些成绩和自我肯定,19年已经过去了,希望在20年,能够成为一个更优秀的人。
WechatIMG6.jpeg
感谢我可爱的同事,年底收到了礼物真的很开心。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:再见2019

+

文章作者:

+

发布时间:2019年12月31日 - 23:50

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2019/12/31/GoodBye-2019/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/12/31/gitlab-cicd/index.html b/2019/12/31/gitlab-cicd/index.html new file mode 100644 index 0000000000..8cc9752bde --- /dev/null +++ b/2019/12/31/gitlab-cicd/index.html @@ -0,0 +1,664 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 基于 GitLab CI/CD 的自动化构建、发布实践 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 基于 GitLab CI/CD 的自动化构建、发布实践 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

公司于去年开始代码版本管理从SVN迁移到了Git,采用的GitLab程序做管理,但是自动构建采用的是Jekins,最近在给博客做自动构建的时候了解到的了Github的Actions,我就在想,是不是GitLab也有自己的自动构建能力,因为之前在参与项目的时候也看到GitLab上有CI/CD相关的内容,但是没有仔细的去看,趁这次机会就一起研究一下。

+ + +

说一下目前公司的构建和发布流程

1、手动构建时代:开发人员在测试需要验证环境的时候,在本地执行打包构建命令,然后将包放到服务器上,整个过程30分钟左右。
2、自动构建时代目前公司的构建是要在Jekins中,首先是在Jekins中配置拉去代码的仓库地址和代码分支,写好构建的脚本,在需要构建时候进行构建,一次配置后构建全程只需要点一下构建时间长度跟项目代码需要下载的依赖时间有关,通常不超过5分钟,需要注意的是要在构建前同步一下代码版本
划重点
在原来的手动构建时代,代码是以开发本地的代码为准,代码版本很可能跟最新的代码有出入,而且依赖于开发的电脑设备,如果他请假了,那么就GG了;另外通过一次配置后整个构建的时间从30分钟降到了5分钟,一次节省25分钟,那么一个项目周期下节省的工时就非常可观了。

+

为什要使用GitLab CI/CD进行构建

这里实际上没有太大的必要将公司的Jekins替换为GitLab的CI/CD进行自动构建,但是呢,因为公司本身采用的就是GitLab作为代码仓库管理代码,它本身又提供了CI/CD的功能,本着多学一点是一点的原则,我就花点时间研究一下它。

+

什么是 GitLab CI/CD

下面我就要开始把我了解到的GitLab CI/CD的使用方式说一下,从零开始搭建GitLab CI/CD。

+

1. 简要介绍 GitLab CI/CD

代码提交到GitLab上后,满足指定条件之后会触发pipeline进行自动化构建、发布。
pipeline可以理解为构建任务,里面可以包含多个流程,比如下载依赖、运行测试、编译、部署。
那么pipeline什么时候触发,分为几个流程,每个流程做什么,需要在项目的.gitlab-ci.yml文件中的定义。
这点呢跟Jekins里面实际上做的也是同样的事,在线下开发做构建时候也是做这些事,只是通过脚本之后这些事都可以交给计算机做了。

+

2. GitLab CI/CD 整体流程

    +
  • GitLab CI/CD 的 pipeline 具体流程和操作在 .gitlab-ci.yml 文件中申明。
  • +
  • 触发 pipeline 后,由 GitLab Runner 根据 .gitlab-ci.yml 文件运行。
  • +
  • 运行结束后将返回至 GitLab 系统。
  • +
+

2.1 .gitlab-ci.yml 文件

.gitlab-ci.yml 文件是一个申明式文件,用于定义 GitLab CI/CD 流程分为几个阶段,每个阶段分别干什么。

+

关于具体干什么、怎么干,主要使用命令行和脚本操作,稍后会在实践部分做细致的介绍。

+

如果涉及一些逻辑的话,会使用脚本(shell)。

+

2.2 GitLab Runner

GitLab Runner 是 CI 的执行环境,负责执行 gitlab-ci.yml 文件,并将结果返回给 GitLab 系统。Runner 具体可以有多种形式,docker、虚拟机或 shell,在注册 runner 时选定方式。实际上就是运行脚本的容器环境。

+

3. 从零搭建一个 GitLab CI/CD 的基本步骤

上面介绍了一些GitLab构建的主要环节和名词概念,接下来我将给大家介绍一下如何从零搭建一个GitLab CI/CD,一起体验一把GitLab CI/CD的整个流程。

+

3.1 新建一个 GitLab 项目

我这用的是公司的自有仓库,各位可以在开源GitLab上创建自己的项目

+

3.2 配置Runner

GitLab 提供了一些共享的Runner,我们可以不处理Runner,这里可以理解为,它提供了一些现成的脚本运行环境,不需要我们从头配置运行环境,so sweet~

+

3.3 新建 .gitlab-ci.yml 文件

    +
  1. 拉取项目到本地
  2. +
  3. 在项目根目录新建 .gitlab-ci.yml 文件
  4. +
  5. 提交 .gitlab-ci.yml 文件
  6. +
  7. 在项目的 CI/CD 中,可以看到 CI/CD 的运行情况
    这个过程应该没人不会吧,没技术含量的我们简单一提,实际上最重要的就是.gitlab-ci.yml文件中要怎么去写,示例说明文件如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // .gitlab-ci.yml 示例说明

    image: node
    # 定义 stages
    stages:
    - build
    - test
    # 定义 job
    build 阶段:
    stage: build
    script:
    - echo "build stage"
    # 定义 job
    发布到测试环境:
    stage: test
    script:
    - echo "test stage"
    + +
  8. +
+

GitLab CI/CD 实践

在实践部分,这里着重介绍 GitLab Runner 和 .gitlab-ci.yml 文件,主要的流程及遇到的问题和解决方案包含在 .gitlab-ci.yml 文件的介绍过程中。

+

1. GitLab Runner

GitLab Runner 一般由 GitLab 系统维护者管理,配置后,同类项目可以共享,一般不需要进行修改。这里不进行具体介绍,主要介绍下使用过程中的注意点,具体使用可参考 GitLab Runner 文档。(https://docs.gitlab.com.cn/runner/)

+

1.1 GitLab Runner 使用流程

    +
  1. 下载 GitLab Runner
  2. +
  3. 注册 GitLab Runner
  4. +
  5. 使用 GitLab Runner
  6. +
+

1.2 GitLab Runner 注意点

在使用 Runner 的过程中,我们遇到了一些问题,下面简要介绍问题及解决方案,不做具体介绍。

+
1.2.1 配置 Runner 后,push 代码,出发了 pipeline,但一直处于Pending状态

错误信息是:

+
1
This job is stuck, because you don’t have any active runners that can run this job
+

注册的 Runner,默认情况下,不会运用没有 tag 的 job,可以在 Settings→CI/CD→Runners Settings,去掉 Runner untagged jobs 即可。

+
1.2.2 GitLab Runner 的类型

有三种类型的 Runner,

+
    +
  • Shared Runners 在整个系统所有项目都可以使用
  • +
  • Group Runners 注册后,同一个项目下的不同代码库共享
  • +
  • Specific Runners 需要给项目单独配置,使用 Specific Runners 注意考虑是否需要关闭 Shared Runners、和 Group Runners。
  • +
+
1.2.3 在 GitLab CI 中使用 docker

如果部署使用的是docker方式,那么在部署时需要在 GitLab CI/CD 中使用 docker 打镜像发布。可以参考 Building Docker images with GitLab CI/CD(https://docs.gitlab.com/ee/ci/docker/using_docker_build.html)

+
1.2.4 在 GitLab CI/CD 中访问 Runner 宿主机目录

我们使用的 Runner executor 是 Dokcer,在 Dokcer volumes 中配置需要访问的目录。

+

2. .gitlab-ci.yml 文件

.gitlab-ci.yml 详细的用法,可参考 GitLab CI/CD Pipeline Configuration Reference 文档(https://docs.gitlab.com/ee/ci/yaml/README.html)

+

2.1 .gitlab-ci.yml 文件结构介绍

    +
  • image 是执行 CI/CD 依赖的 Docker 基础镜像。镜像中有 Node、Yarn、Dalp(内部 rsync 工具)。
  • +
  • stages 中定义了我们的 pipeline 分为以下几个过程:
      +
    1. 下载依赖阶段 pre_build
    2. +
    3. 构建阶段 build
    4. +
    5. 发布阶段 deploy
    6. +
    +
  • +
  • stage 申明当前的阶段,在 stages 中使用
  • +
  • variables 用于定义变量
  • +
  • before_script 执行 script 前的操作
  • +
  • script 当前 stage 需要执行的操作
  • +
  • changes 指定 stage 触发条件
  • +
  • refs 指定 stage 触发的分支
  • +
+

下面具体看一下我们这个.gitlab-ci.yml文件实际的样子

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
image: registry.thunisoft.com/gitlab-ci/node:v1.8

variables:
# $CI_PROJECT_PATH :项目id,用于项目唯一区分本项目与其它项目
# $CI_PROJECT_DIR :本地项目路径
# $PROCESS_PATH :临时文件目录(包括日志和一些临时文件)
NODE_MODULES_PATH: /runner-cache/frontend/$CI_PROJECT_PATH/$CI_BUILD_REF_NAME/node_modules

stages:
- pre_build # 下载依赖阶段
- build # 构建阶段
- deploy # 测试发布阶段

# 下载依赖:
before_script: # 下载依赖前准备脚本
# 无 node_modules 文件时,新建 node_modules 文件
- /bin/bash ./ci/mkdir.sh $NODE_MODULES_PATH
# 软链 node_modules 到宿主机
- ln -s $NODE_MODULES_PATH .
- cd webpack@lixuguang-project

stage: pre_build
script:
- echo "npm install"
- npm install --network-timeout 60000 # 安装依赖
only:
changes:
- webpack@lixuguang-project/package.json
refs:
- master
- ci

# 构建:
stage: build
variables:
CI_COMMIT_BEFORE_SHA_PATH: /mnt/gv0/gitlab-runner-cache/$CI_PROJECT_PATH
CI_COMMIT_BEFORE_SHA_FILE_NAME: $CI_BUILD_REF_NAME.sh
CI_COMMIT_BEFORE_SHA_FILE: /mnt/gv0/gitlab-runner-cache/$CI_PROJECT_PATH/$CI_BUILD_REF_NAME.sh
before_script:
# 建存此次 CI CI_COMMIT_SHA 的文件
- /bin/bash ./ci/mkfile.sh $CI_COMMIT_BEFORE_SHA_PATH $CI_COMMIT_BEFORE_SHA_FILE_NAME
# 软链 node_modules 到宿主机
- ln -s $NODE_MODULES_PATH .
- rm -rf web/share/*
- cd webpack@lixuguang-projects
script:
# 缓存上次ci
- source $CI_COMMIT_BEFORE_SHA_FILE
- echo "CI_COMMIT_BEFORE_SHA=$CI_COMMIT_SHA" > $CI_COMMIT_BEFORE_SHA_FILE
- python3 ../ci/build.py # 编译
- /bin/bash ../ci/commit.sh # 提交编译结果
only:
changes:
- www_src/**/*
refs:
- master
- ci

# 测试发布:
stage: deploy
variables:
PROCESS_PATH: /mnt/gv0/gitlab-runner-cache/deploy/process/$CI_JOB_ID # 目录不要换,用于日志服务器获取日志展示
script:
- mkdir $PROCESS_PATH # 建立发布临时路径,存放发布配置中间文件和结果日志用
- dplt $CI_PROJECT_DIR/.deploy_test.yml $CI_PROJECT_PATH $CI_PROJECT_DIR/web/ $PROCESS_PATH
# dplt 发布yml配置
- echo "发布完成,错误日志查看http://172.18.78.11:8089/log?path="$PROCESS_PATH
- echo `ls $PROCESS_PATH/*.log`
only:
changes:
- web/**/*
refs:
- test
+ +

2.2 下载依赖阶段(pre_build stage)

下载依赖的方案是:当 package.json 文件发生变化时,触发 pre_build stage,执行 npm install。下载的 node_modules 放在宿主机下,执行时通过软链获取依赖。

+

2.3 构建阶段(build stage)

构建阶段,分为 3 部分

+
    +
  1. diff 文件变化
  2. +
  3. 前端 build
  4. +
  5. commit build 后结果
  6. +
+
2.3.1 diff 文件变化

每次 CI 时,将当前 CI commit SHA(CI_COMMIT_SHA 变量)存在文件中,存为 CI_COMMIT_BEFORE_SHA 变量, diff 时,git diff 当前 CI 与上次 commit SHA 的变化。

+
2.3.2 前端 build

根据 git diff 的变化情况,确定本次需要打包的内容。

+
2.3.3 commit 打包后生成的 HTML 文件

在 GitLab CI/CD 提交代码时,使用 Git 凭证存储,提交打包后的 HTML 文件。Git 凭证存储细节可参考凭证存储文档(https://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E5%87%AD%E8%AF%81%E5%AD%98%E5%82%A8)

+

2.4 发布阶段(deploy stage)

发布阶段,使用内部的 rsync 工具 dplt 将打包后的 HTML 文件部署。dplt 可配置集群、机器列表。

+

写在最后

以上就是GitLab CI/CD的整个理论到实践的全部过程,实现之后你就可以解放双手了,是不是超爽。

+

参考资料

持续集成是什么?(http://www.ruanyifeng.com/blog/2015/09/continuous-integration.html)

+

什么是 CI/CD?(https://www.redhat.com/zh/topics/devops/what-is-ci-cd)

+

GitLab Docs(https://docs.gitlab.com/)

+

Introduction to CI/CD with GitLab(https://docs.gitlab.com/ee/ci/introduction/)

+

用 GitLab CI 进行持续集成(https://scarletsky.github.io/2016/07/29/use-gitlab-ci-for-continuous-integration/)

+

如何实现前端工程的持续集成与持续部署?(https://www.zhihu.com/question/60194439)

+

基于 GitLab CI 的前端工程CI/CD实践(https://github.com/giscafer/front-end-manual/issues/27)

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:基于 GitLab CI/CD 的自动化构建、发布实践

+

文章作者:

+

发布时间:2019年12月31日 - 08:22

+

最后更新:2019年12月31日 - 08:43

+

原始链接:https://blog.lifesli.com/2019/12/31/gitlab-cicd/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/12/31/weichat-h5-compatibility/index.html b/2019/12/31/weichat-h5-compatibility/index.html new file mode 100644 index 0000000000..4d5433ce83 --- /dev/null +++ b/2019/12/31/weichat-h5-compatibility/index.html @@ -0,0 +1,618 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 微信的H5兼容方案 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 微信的H5兼容方案 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近被公司新来的前端问到了一些微信页面的兼容性问题,因为我做微信开发的经验比较少,被问到了真是感到很尴尬,所以在搜到了解决方案告诉了他之后,我又深入的再网上找了一些微信开发中遇到的兼容问题,特此整理,以留后用。

+ + +

1、ios端兼容input光标的高度

bug描述:
这个问题只出现在苹果手机上,在安卓手机上显示没有问题,可以说是非常诡异,简单描述一下就是在input输入框聚焦时,光标大小应该跟字号一直,但是在苹果手机上当点击输入的时候,光标的高度和父盒子的高度一样。
分析:
说来主要是习惯导致的问题,通常我们习惯将height和line-height设置成一样的值,这个时候input光标就会整个变得很大。
解决:
实际上解决方案也很简单,就是不设置行高,通过padding来控制输入内容与外框的距离。

+
1
2
3
4
5
6
7
8
// less代码
.input-x{
height:40px;
// line-height:40px; // 此行注释掉
.input-inline{
padding: 10px 0;
}
}
+

这样做问题就解决了。

+

2、ios端微信h5页面上下滑动会卡顿,页面会有缺失

bug描述:
没错又是ios端,当页面高度超过一屏,那么上下滑动时就会出现页面卡顿的情况,而且时有伴随内容不能全部显示的情况。
分析:
这里实际上是浏览器内核解析不同导致的问题,在Andriod设备上,微信调用的是Webkit内核,而ios中是使用了Safari的内核,Safari对于滚动事件(overflow-scrolling)会使用原生的控件。而webkit内核则会创建一个UIScrollView来提供给子layer用以渲染。
解决:
在做样式重置时,加上下面这句话就能解决这个问题。

+
1
2
3
4
// css代码
*{
-webkit-overflow-scrolling: touch;
}
+

但是这个方案也有缺陷,就是页面中不能有使用absolute定位的元素,不然布局就错乱了。
延伸:
-webkit-overflow-scrolling 属性控制元素在移动设备上是否使用滚动回弹效果.

+
    +
  • auto: 使用普通滚动, 当手指从触摸屏上移开,滚动会立即停止。
  • +
  • touch: 使用具有回弹效果的滚动, 当手指从触摸屏上移开,内容会继续保持一段时间的滚动效果。继续滚动的速度和持续的时间和滚动手势的强烈程度成正比。同时也会创建一个新的堆栈上下文。
  • +
+

3、ios键盘唤起再收起,页面不会恢复原位

bug描述:
哎,对的还是ios,问题标题描述的比较清晰了,就是键盘弹出时,页面内容会整体上移,但是收起键盘时本应回归原位的不回去了。—_—|||
分析:
固定定位的元素,如果元素内input框聚焦的时候会弹出软键盘,软键盘会占用屏幕面积,失去焦点时软键盘消失,但是仍会占用,页面就会不能恢复原状,也就导致input框不能再次输入了。
解决:
在input失去焦点键盘收起时,写一个监听事件,事例代码如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// vue代码
<input @blur="changeBlur()"/>

// js代码
changeBlur(){
let ua = navigator.userAgent; // 获取用户代理
let app = navigator.appVersion; // 获取客户端版本信息
let isIos = ua.match(/i[^;]+;( U;)? CPU.+Mac OS X/); // 判断是否是Ios设备
if(isIos){
setTimeout(()=>{
const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop || 0
window.scrollTop(0,Math.max(scrollHeight - 1), 0)
},100)
}
}
+

延伸:
在iso的微信开发中,页面元素如果用到了position: fixed进行定位,那么键盘收起时,就会被顶上去,第三方输入法也不例外。

+

4、Android弹出键盘遮挡文本输入框

bug描述:
刚才说的问题都是Ios端的,实际上Android上也有挺多坑,上面讲到Ios上输入框弹出键盘的问题后,Android中实际也有,只是现象不同;Andriod中弹出键盘后页面不会向上滑动,但是如果输入框在底部的话会直接被挡住。。。
分析:
很坑,因为Andriod中输入框focus后,并不会向上滑动,如果靠下就会被挡住。。
解决:
实际上跟Ios上处理差不多的方案,代码如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// vue代码
<input @blur="changeBlur()"/>

// js代码
changeFocus(){
let ua = navigator.userAgent;
let app = navigator.appVersion;
let isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1;
if(isAndroid){
setTimeout(function() {
document.activeElement.scrollIntoViewIfNeeded();
document.activeElement.scrollIntoView();
}, 500);
}
}
+

扩展
Element.scrollIntoView()方法让当前的元素滚动到浏览器窗口的可视区域内。而Element.scrollIntoViewIfNeeded()方法也是用来将不在浏览器窗口的可见区域内的元素滚动到浏览器窗口的可见区域。但如果该元素已经在浏览器窗口的可见区域内,则不会发生滚动

+

5、Vue中路由使用hash模式,分享时Android可分享成功,Ios端分享失败

bug描述:
Ios的问题真的挺多的。。。

+
    +
  • 在分享页面给A时,没问题,A把链接分享给B的时候就跳转到首页了;
  • +
  • 使用Vue-router跳转到第二个页面在分享时候,分享失败;
    以上两个问题在Android上均没有问题。
  • +
+

分析:
jssdk是后端进行签署,前端校验,但是有时跨域,ios是分享以后会自动带上 from=singlemessage&isappinstalled=0 以及其他参数,分享朋友圈参数还不一样,貌似系统不一样参数也不一样,但是每次获取url并不能获取后面这些参数
解决:

+
    +
  • 可以使用改页面this.$router.push跳转,为window.location.href去跳转,而不使用路由跳转,这样可以使地址栏的地址与当前页的地址一样,可以分享成功
  • +
  • 把入口地址保存在本地,等需要获取签名的时候再取出来,注意:sessionStorage.setItem(‘href’,href); 只在刚进入单应用的时候保存!(还没测试,有点low)
  • +
+

写在最后

虽然微信H5方式开发想对来说成本比较低,但是有时候坑开始挺多的,但是微信原生开发又增加了成本,很矛盾,目前能做的就是尽量把踩过的坑都记下来,下次别再跳进去了。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:微信的H5兼容方案

+

文章作者:

+

发布时间:2019年12月31日 - 01:37

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2019/12/31/weichat-h5-compatibility/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/01/Hello-2020/index.html b/2020/01/01/Hello-2020/index.html new file mode 100644 index 0000000000..d4e638547d --- /dev/null +++ b/2020/01/01/Hello-2020/index.html @@ -0,0 +1,593 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 你好2020 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 你好2020 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

2020元旦伊始

时间过的真快,19年睡了一觉就成了过去的一年,今天是元旦,早上8:00起了床,简单洗漱后我出了门,新年新气象,去年没有坚持的晨跑,今年想要坚持一下,所以一早就出门跑了会儿步,太久没活动了,只是跑了半个小时就气喘吁吁,只好回家,但是即使是这半个小时,我感觉呼吸了新鲜的空气,整个人也精神了不少,回到家坐在阳台上,拿出小王宁送我的极少数手册开始研究了起来,生活还是应该有点仪式感何况是新年伊始,手册中有很多建议,全部看过一遍以后我郑重的拿起了笔写下了我今天的行动计划。但是没有写年度计划,明天上班后跟领导还要谈一谈,谈过之后再定一下年度的计划。今天我的计划就是陪家人过好元旦这一天。

+

元旦执行计划

    +
  1. 写一篇日志
  2. +
  3. 将家人这一天游玩的过程以Vlog的形式记录下来,上传到西瓜视频上。
  4. +
  5. 陪家人逛街,给桐桐买新衣裳。
  6. +
+

执行计划

吃过早饭后大概到了10:30,一家人收拾好以后终于可以出门了,今天有点风,一点点冷不是特别冷,空气还不错,天气听晴朗的,一家人开着车听着歌前往甘井子万达广场,20分钟左右我们就到了万达广场,首先我们先去看了桐桐心心念念的小猫咪,也就是猫咪咖啡馆里的那些小可爱,桐桐可能是受妈妈影响,很喜欢猫咪,但是又有一点点怕。大概看了10分钟的猫咪,我们继续下一站,上四楼给桐桐买新衣裳,桐桐看到满眼的漂亮衣裳很是兴奋,最终她挑选了一套带着向日葵花朵的淡蓝色衣裙,穿上漂亮的新衣裳,平时像个假小子似的桐桐也一下子害羞了起来,让妈妈和奶奶看完后还会让爸爸和爷爷看看,得到了大家的肯定以后,心满意足的买下了它,也不知道是不是试衣服试累了,桐桐嚷嚷着要吃饭,桐桐妈说有一家港式茶餐厅味道不错,之前和同事来过,于是我们就去了4楼的茶餐厅,说实话这是我头一次吃茶餐厅,我还跟我爸妈调侃说,这也算是我们来过一次香港了。去到餐厅时才11点多一点,客还没有上满,还有座位,我们找了个靠里的位置坐了下来,打开菜谱,桐桐妈挑了几样她觉得不错的菜,上菜时间稍微有点慢,期间桐桐有些不耐烦,坐也坐不住,终于等到了吃的上来,可是觉得大多数吃的都太偏甜,爸妈和我都吃不太惯,桐桐吃的还不错,大概吃了半个小时就吃完了,出门的时候门口已经排起了长队,还好来的早一点,不然吃饭都要等了。本打算带着桐桐回家睡个觉,下午要到姥爷家串门,可桐桐说没有逛够,正巧这时看到了一楼有小丑在折气球玩具,我们就下到一楼找小丑玩了,小丑给桐桐折了个贵宾犬,小丑是荣耀手机的员工,可能是元旦策划的活动吧,反正来了就来了,进去看看也好,看了新出的V30手机,感觉并不是很喜欢,店员说有旧机抵值的活动,我问了他我的7p128g还能值多少,他打了几个电话后回复我1400块,哎,三年前7000块买的手机,现在只值1400了,算了还是用着吧,出了荣耀手机店又去了旁边的华为,看了看新出的mate30pro,这个手机是真的不错,当然手机不错价格也美丽,6500+的价格真的是让我不舍得买,还是把钱留下来给桐桐吧。桐桐终于累了,嚷嚷着要走,于是一家人驱车去了姥姥家。

+

姥姥家在西南路,因为平时工作忙很少带桐桐去,这次去桐桐又不认识他们了,开始的时候很拘束,也不说话,一直到吃饭结束才终于愿意开口问好唱歌,因为家里没有电视机,到了姥姥家看到电视机很兴奋,一直在看,今天也算是过了瘾了,亲人就是即使很长时间不见面,但一见面就特别亲的人,饭从5:30一直吃到8:30,聊了工作,聊了生活,聊了困惑,聊了坦然,我终于知道了长辈们的智慧,了解了身体健康的重要性,20年一定要锻炼出一副好的身体,对家人负责。

+

9:30回到了家,心里挺高兴的,新年第一天陪家人一起度过了充实的一天,回到家把今天路上录下来的视频剪成了短视频上传到了西瓜视频,这也是我今年的一个想法,记录我们一家人的幸福生活,如果有粉丝有点播的话还能赚点钱,没有的话也记录了桐桐的成长,算是一点纪念吧。最后写下了这篇日记,写完之后我就要睡觉去了,明天一早就要起床,明天起我就要走路去上班了,为了有一副好身体,加油。

+

今天的目标都完成了,很开心~~

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:你好2020

+

文章作者:

+

发布时间:2020年01月01日 - 10:50

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/01/Hello-2020/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/02/21-Day-Challenge-01/index.html b/2020/01/02/21-Day-Challenge-01/index.html new file mode 100644 index 0000000000..36ea72c8b1 --- /dev/null +++ b/2020/01/02/21-Day-Challenge-01/index.html @@ -0,0 +1,599 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2020年21天主题挑战之灵感篇-Day 1-写下自己期待中的生平 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 2020年21天主题挑战之灵感篇-Day 1-写下自己期待中的生平 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

还记得我19年总结最下面的那张照片么,那个同事送的的本子,对,就是下面这张图,这本子叫做《One In A Million》中文叫做极少数手册,是一个管理时间用的管理手册,英文如果直译的话应该是叫做万里挑一,实话说我并不是很自信,虽然我觉得自己还算可以,但远远达不到万里挑一的程度,所以,我打算好好利用这个本子,让自己尽可能成为百里挑一,千里挑一,最后万里挑一的人。

+

本子里有一个栏目叫做21天主题挑战,21-Day Challenge,每个月21天都会给你列出一些主题,越往后列出来的主题越少,更多的需要个人去补充,我想去挑战这些内容,让自己的生活变得有目标,让自己变得更好。

+ + +

1月份21天主题挑战之灵感Inspiration

+

设定一个挑战主题,让自己更富有创造力,连续21天挑战,让灵感乍现,唤醒天赋。

+
+

第一天 Day 1 写下自己期待中的生平

看到这个题目之后,我闭上了眼睛,努力的回想我自己曾经的梦想是什么,什么时候丢掉了梦想,儿时的梦想,上学时的梦想,长大以后的梦想,现在的梦想,我努力回想了好久好久,儿时的梦想我想起来了,上学时的梦想我想起来了,长大后的梦想我也想起来了,但是我没能一下子找到我现在的梦想,也许现在说梦想有些奢侈,也许也有点贴不上今天的主题期待中的人生,我不太会写文章,但是想到哪里写哪里吧,我先写下我之前的那些个梦想。

+

小时候的梦想-成为科学家

小时候的梦想说出来很简单,但是是那么真诚,真诚就容易打动人,说实话我记性不是太好,已经忘掉了小时候的梦想,所以我问了我妈,我妈笑话我,说我咋不记得小时候那伟大的梦想了,妈妈说我儿时的梦想是当科学家,那会应该是上小学吧,妈妈说学校老师让写作文,标题就是我的梦想,我的作文里写的是成为科学家,这个科学家不像是别人写的,比如造火箭,造大炮又或者是造卫星造原子弹,我的梦想现在说起来与其说是科学家,不如说是药剂师更贴切,妈妈笑着跟我说我当时的梦想是做一个科学家,要研究出来一种长生不老的药,然后让爸爸妈妈吃下以后就可以永远健康年轻,妈妈说那个时候她总逗我,看到路上漂亮的阿姨就会问我,那个阿姨漂亮么,起初我会说漂亮,然后妈妈就会说那让她给你当妈妈好不好,我呢会说不好,妈妈说后来她再问的时候,我就再也不会说我要漂亮阿姨当妈妈的话了,我现在想想也许就是那时,就是因为发现妈妈越来越胖,辛苦操劳后年轻也渐渐不在我才有了这个想法吧,说实话上小学期间我一直认为自己长大能够成为一个科学家,能够造出那长生不老的药让爸妈吃下,这样我就再也不会需要一个漂亮阿姨当妈妈了。我那时希望快快长大,长大了以后我就能做科学家了。

+

上了初中高中以后,也许是进了大城市,也许是长大了,我知道了原来的梦想可能有些遥远,学业的压力让我有些透不过气,从海岛到城市,落后了3年的时间,我通过补习班慢慢追赶,终于能够追到了班级还算靠前的位置,那个时候我的梦想很小,也很简单,那就是考个好大学找个好工作,让爸爸妈妈早点不用那么操劳,早点享福。

+

18岁的梦想 — 找个好工作,让爸妈早点享福

努力的学习,懵懂的感情,初中4年+高中3年的生活,最终我并没能特别出色的考上985/211大学,而是上了离家只有1小时车程的大外,学了计算机加日语英语,学费不便宜,每年16000,说实话没自己工作的时候不知道这16000对于一个下岗自己在家开小卖店的父母是多大的负担,工作后我才终于知道这笔钱有多少。好在原来在爸爸单位的时候接触电脑还算比较早,而我对这个新鲜玩意也算是感兴趣,大学学业上并没给我带来太大的压力,但是我也确实不是个聪明的孩子,日语仅仅过了N2,而英语则一直只是CET-4,现在回想起来,原来应该多努努力,也许现在的生活就会更好了,多想回到过去跟自己说,你要努力啊!一晃4年的大学生活就结束了,这时我终于离开了父母身边,只身去往了大城市北京,开启了我的工作生涯。

+

20岁的人生 — 多挣钱,快速成长

工作了以后,我就像海绵一样不断的吸收着周围的水分,学习工作中需要的技能,学习如何才能让领导器重,学习如何才能快速积累人生的财富,因为我想着,想着能快快独立反哺我的爸妈。20岁,我跟媳妇儿谈了场异地的恋爱,后来她到北京找我,再后来我们就一起回了大连。北京是个大城市,大的有时候让人迷茫,虽然工作机会比较多,但是租房的压力,环境的恶劣,家里的呼唤,最终让我选择了回到我熟悉的城市,另外找了份工资不高的工作,我不满足,我想能成为顶天立地的男子汉,后来我就来到了现在的公司华宇,而且一待就是5年。

+

华宇的职业生涯 — 5年工作,9年经验

网上有个段子我记得,一个人面试拿出简历,工作时间是2年却写着3年工作经验,面试官问他是不是写错了,他答不是,因为加班加出来的。这算是对前公司的吐槽吧。

+

来到大连华宇时,公司还不足120人,我所在的团队还是个交互组,只有三个人,前端的话只有我和另外一个刚毕业的大学生,记得刚来公司的第一年,我参与了140个项目的开发工作,现在想想这个数字有些惊人,但是因为只是些前端切图仔的工作,对我来说感觉难度并不大,不过还是要感谢刚开始这2年,让我的基础非常扎实,再后来公司引入了前后端分离,引入了Vue框架,越来越多的业务要写,数据处理要写,加班成了家常便饭,后面这三年,我几乎没有休过除了元旦和春节的任何法定节假日,每年5天的年假也几乎都没休成,说实话每次加班加到要崩溃时候,我都会想我到底为了啥这么拼命。要不我还是换一家比较轻松一点的工作吧,工资还能涨点。说实话这段时间工作就是生活的全部,每天到家都10点以后,到家老人孩子都睡了,有时候我都睡不着,想着我的生活难道就这样了么,我不甘心,不服输。很多人劝我别那么拼命,别把公司当成自己家的,只是个打工的而已,但是有时候想着下面还有那么多新人信任着我依赖着我,我就没法撒手不管。终于时间到了19年年底,5年来培养的前端团队最后还是没守护住,要拆到各项目团队了,一开始真的难以接受,不过公司领导层已经决定了,作为一线员工只能服从,我希望大家能够把心中的担忧都能消除掉,在新的团队里开启新的篇章,也许会有更好的发展,如果有一天离开了华宇,也要江湖相望,常聚聚。

+

2020年31岁踏上新的旅途

还是感谢小可爱的这个本子,让我能够有主题想想我未来到底想要什么样的生活,说句实在话,我向往不为钱发愁工作的日子,可以在做好自己工作之外的时间里多陪陪家人,带着孩子常出去走走,见见外面的世界,说实话已经31岁了,除了谈恋爱时去找媳妇儿去过江浙苏杭,工作原因去过北京/青海,好像这些年也就去了趟南京,现在的我已经有些不知道什么叫做生活了,我希望自己的空闲时间是可以让兴趣填满的,可以和朋友同事同学多聚聚,但是怕打扰大家我又从来不会主动去约别人,小时候没有听从爸妈的话培养自己的兴趣,现在有些后悔,真的空闲下来都不知道该用什么填满这时间,都是躺在床上刷手机,看电视,我不喜欢这样的生活,但是我不知道该过怎么样的生活。看到那些会做饭的视频,做顿丰盛的饭菜给家人吃,看到他们的幸福笑脸,我想做那样的事;看到带着家人过着一路向前增长见闻的旅途生活,我想也尝试一下那样的人生,我感觉为了钱我被束缚在了工作上,2020年我想过一种不为工作所累,不为钱所累,能够享受生活,陪伴家人的生活,保重身体,每天快快乐乐的,多发现生活中的美好,感恩,努力,成长。期待自己成为更好的人。

+

写在最后

我所期待中的生平,成为一个不被工作强迫,不被金钱所累,爱家顾家,孝敬父母,人缘好朋友多,兴趣广泛,感恩的人。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:2020年21天主题挑战之灵感篇-Day 1-写下自己期待中的生平

+

文章作者:

+

发布时间:2020年01月02日 - 12:24

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/02/21-Day-Challenge-01/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/02/FE-guide-Closure/index.html b/2020/01/02/FE-guide-Closure/index.html new file mode 100644 index 0000000000..e44bc58382 --- /dev/null +++ b/2020/01/02/FE-guide-Closure/index.html @@ -0,0 +1,602 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 闭包 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 闭包 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

闭包Closure

闭包的定义很简单:函数 A 返回了一个函数 B ,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。

+
1
2
3
4
5
6
7
8
9
// js代码

function A() {
let a = 1
function B() {
console.log(a)
}
return B
}
+

你是否会疑惑,为什么函数 A 已经弹出调用栈了,为什么函数 B 还能引用到函数 A 中的变量。因为函数 A 中的变量这时候是存储在堆上的。现在的 JS 引擎可以通过逃逸分析辨别出哪些变量需要存储在堆上,哪些需要存储在栈上。

+

经典面试题,循环中使用闭包解决 var 定义函数的问题

+
1
2
3
4
5
6
7
// js代码

for ( var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
+

首先因为 setTimeout 是个异步函数,所有会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。

+

解决办法两种,第一种使用闭包

+
1
2
3
4
5
6
7
8
9
// js代码

for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
+

第二种就是使用 setTimeout 的第三个参数(附加参数,传给 setTimeout 中的函数)

+
1
2
3
4
5
6
7
// js代码

for ( var i=1; i<=5; i++) {
setTimeout( function timer(j) {
console.log( j );
}, i*1000, i);
}
+ +

第三种就是使用 let 定义 i

+
1
2
3
4
5
6
7
// js代码

for ( let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
+

因为对于 let 来说,他会创建一个块级作用域,相当于

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// js代码

{ // 形成块级作用域
let i = 0
{
let ii = i
setTimeout( function timer() {
console.log( ii );
}, i*1000 );
}
i++
{
let ii = i
}
i++
{
let ii = i
}
...
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 闭包

+

文章作者:

+

发布时间:2020年01月02日 - 13:32

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/02/FE-guide-Closure/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/02/FE-guide-dataType/index.html b/2020/01/02/FE-guide-dataType/index.html new file mode 100644 index 0000000000..0f221ee79b --- /dev/null +++ b/2020/01/02/FE-guide-dataType/index.html @@ -0,0 +1,655 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 数据类型、类型判断、类型转换、类型比较 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 数据类型、类型判断、类型转换、类型比较 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

+

温故而知新,可以为师矣。
———————— 论语

+
+

这篇文章主要是把我看过的书中的知识点,还有其他人整理的面试题进行整理,巩固自己的前端开发理论知识,希望其他看到这篇文章的人也能有所帮助。

+ + +

JS知识点

+

当前市场中,如何区分一个好一点的前端开发和一般的前端开发,主要看的就是js能力的差距,好的前端开发,JS玩的转,不仅仅是框架玩的好,还要JS的基础扎实,只有基础与新技术都玩的6的前端开发才是好前端。

+
+

上面这句话不是什么名人或者某位知名前端大拿说的,这是我在公司这几年面试招聘的过程中真真切切总结感受的。
所以可以说得JS者得高级前端,所以下面也主要是写JS的相关知识点。

+

JavaScript内置数据类型 — 数据类型

无论学什么语言最重要最基础的就是数据类型,《JavaScript权威指南》这本书中详细的介绍了JS中的数据类型,下面总结一下。

+

JavaScript中数据类型分为两大类共七种内置数据类型,其一是6种基本类型,其二是1种引用类型,我发现在面试过程中好多面试者都不会先说有两大类,而是会直接蹦出数字类型、字符串类型。。。对象、数组也会被并排放在一起,这其实不是个掌握知识点的好方法,应该先把数据类型分成上面说的基本类型引用类型这样两个大类之后,再看看这两大类中有什么其他的子类型,在记忆其他子类型之前我觉得应该先了解一下什么是基本类型引用类型,实际上基本类型和引用类型的主要区别是存储的区别,基本类型在栈中,而引用类型的话,引用数据的地址存储在栈中,而对象本身是存储在堆中,引用的数据地址是个16进制的数据值,它就像一把钥匙让你能够找到宝藏在什么地方。这就是基本数据类型和引用类型的区别了。

+

那么如何记住有哪些基本数据类型和引用数据类型呢,实际上只要记住了6个基本数据类型,其他的都是引用数据类型,而所有的引用数据类型的祖宗都是Object,所以引用数据类型实际上只有Object一个,那么像是Array等其他子类型,都是Object的孩子,不跟Object在一个级别上。

+

基本数据类型有哪些呢?其实挺好记的,数字,字符串,这两个一个像温柔的文学少女(string),一个像有点精于算计的男生(number),还有一个布尔类型(boolen)他像是班级里正义感爆棚的人,只论对错;另外还有个差生,没头脑似的未定义(Undefined)还有一个失了忆记不起来自己是谁的空(Null),最后还有一个新加入的插班生,总是带着口罩的标志符号(Symbol),这些人构成了这个班级的所有学生,也就是全部的基本类型,那么引用类型的对象Object呢就像及了漂亮爱化妆的班主任老师,有好多副面孔。不知道大家有没有看过一个动漫叫做《黑塔利亚》,他就是把国家都拟人化了,有了各自的性格,我很喜欢看,我觉得这些数据类型也各有各的特点,像这些国家一样,好了脑洞有点挖深了,有人会说我不就是这么几个简单的数据类型嘛,硬记下来不就好了,但是知识总有你硬记不下来的时候,最好的方法也是速记领域最为常用的方法,就是把你不熟悉的知识与你感兴趣的画面或者既往的知识串联起来,这样就能达到很好的记忆,如果你不喜欢动漫(怎么会有不喜欢动漫的人!!!),可以试试用其他的方法记,当然你如果硬要死记硬背那我也没办法,我继续开我的脑洞。
如果你是学过Java开发的同学(如果是计算机专业出来的,应该都或多或少学过,非计算机专业的我也不知道说啥了。。),数字在Java中是分成 byte/short/int/long 的,但是在Js中没有那么多,就一个Number,它是浮点类型,基于 IEEE 754标准实现,刚才我不是说了Number是个精于算计的男生,精于算计就是说他分毫不差,这样浮点型就很好的记了下来,这个754的标准可以不记,如果非要记的话,你可以记成他是自称IEEE 754团体的成员。最后Number身后还跟着一个小弟,叫做NaN,他虽然是Number的小弟,但他总是说话不算数,自己说过什么都不承认。所以NaN!=NaN。
老师是个爱化妆的老师,而这些学生也不是普通的学生,在学校他们是老老实实的基本类型,放了学之后一打扮,他们就各有了其他的能力,这个过程叫做装箱,具体的后面再说。(好了快回到现实吧你!)

+

如果基础数据是字面量类型,那么他们就像是在上课的学生,只是学生而已,而当他们调用方法时,他们就成了下课后各种技能都有的新新人类,这个过程有时候是显示的,就像是有些学生喜欢大声嚷嚷,而更多的是你不自觉中就用到了装箱操作,是Js引擎提供的能力,就像是有些闷骚的学生一样。

+
1
2
3
// js代码
let aNumber = 111 // 这只是字面量,不是 number 类型
aNumber.toString() // 装箱操作自动转化成数字对象,使用时候才会转换为对象类型
+

对象有个深浅拷贝的知识点必须要会,对象因为是引用数据类型,在栈中存储的是地址,当用另一个变量接收了之前的变量,那么就好像把钥匙复制了一把,两把钥匙开的还是同一个门,而深拷贝呢就像是照着原来的样板间又造了一个一模一样的房间,这两个房间长得一样,但就是两个房间,钥匙自然也是不一样的,所以呢,当往房间里搬家具的时候,浅拷贝搬进去的是一个房间,所以两把钥匙打开之后看到的都是多了家具,而深拷贝的话,我只是往样板间搬了家具,所以照着装修的房间里是不可能有的,这就是浅拷贝原数据会受影响,而深拷贝不会。

+
1
2
3
4
5
6
// js代码

let a = { name: 'FE' }
let b = a
b.name = 'EF'
console.log(a.name) // EF 浅拷贝原数据会受影响
+

内置数据类型检测 Typeof — 类型判断

+

typeof 对于基本类型,除了 null 都可以显示正确的类型

+
+

typeof 就像是学校的教导主任一样,他有着一双火眼金睛,不管是哪个同学,穿了什么样的衣服,他一问就能问出来这个学生是谁,大家都怕他,但是Null因为失忆了,他也不知道教导主任是谁,所以typeof就拿他也没办法,因为他不怕教导主任,教导主任甚至会以为他是老师呢。

+
1
2
3
4
5
6
7
8
9
10
// js代码

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof b // b 没有声明,但是还会显示 undefined

typeof null // 'object' 这是JS中的bug
+ +
+

typeof 对于对象,除了函数都会显示 object

+
+
1
2
3
4
5
// js代码

typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
+ +
+

知识扩展:为什么会出现这种情况呢?因为在 JS 的最初版本中,使用的是 32 位系统,为了性能考虑使用低位存储了变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。

+
+

如果我们想获得一个变量的正确类型,可以通过 Object.prototype.toString.call(xx)。这样我们就可以获得类似 [object Type] 的字符串。

+
+

小知识扩展

+
+
1
2
3
4
5
6
7
8
9
10
let a
// 我们也可以这样判断 undefined
a === undefined
// 但是 undefined 不是保留字,能够在低版本浏览器被赋值
let undefined = 1
// 这样判断就会出错
// 所以可以用下面的方式来判断,并且代码量更少
// 因为 void 后面随便跟上一个组成表达式
// 返回就是 undefined
a === void 0
+ +

类型转换

转Boolean

一句话可以概括

+
+

在条件判断时,除了 undefinednullfalseNaN''0-0,其他所有值都转为 true,包括所有对象。

+
+

从上面这段话可以看出来,undefinednull 是基本类型之二,而false是布尔类型的假值,NaN是数字类型的无效值,''是字符串类型的空值,而0-0都是数字类型的零值,可以看到,除了0-0有些特殊,除了插班生Symbol,剩下的都是基本类型的假值,由此实际上就很好记了,有时候数字这个容易忘,但是记住“非0既真”这句话就好了。

+

对象转基本类型

对象在转换基本类型时,首先是先会调用ToPrimitive(原始类型),如果有hint参数调用对应的的类型方法,如果没有那默认先会调用 valueOf 然后调用 toString。如果返回了基本类型,结束。如果都没返回,那么Error但是注意这两个方法你是可以重写的。

+
+

Symbol.toPrimitive 是一个内置的 Symbol 值,它是作为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数。(来自MDN的解释)
需要解释的关键机制是ToPrimitive函数。该函数是将任意值转换为相应的基本类型值。如果输入的就是一个基本类型值,那么将不做修改,被直接返回。如果值是非基本类型值,它将调用内部方法 [[DefaultValue]] 为对象找到一个默认值。
[[DefaultValue]]是每一个对象的内部属性。该方法需要一个可选的参数hint,值是Number或String。如果没有提供hint,则默认为Number(除非该对象是Date,在这种情况下默认为String)。然后将调用toString和valueOf去寻找基本类型值。在这里hint就起到作用了。如果hint参数值为Number,valueOf将先被调用,如果hint是String的话,则toString被先调用。
[[DefaultValue]] 返回的值一定是基本类型值。如果不是,一个TypeError 将会被抛出。这就意味着为了在这种情况下有意义,toString和valueOf应该返回基本类型值。

+
+

四则运算符

+

只有当加法运算时,其中一方是字符串类型,就会把另一个也转为字符串类型。其他运算只要其中一方是数字,那么另一方就转为数字。并且加法运算会触发三种类型转换:将值转换为原始值,转换为数字,转换为字符串。

+
+
1
2
3
4
5
6
7
8
// js代码

1 + '1' // '11' 这里会是面试陷阱
2 * '2' // 4
[1, 2] + [2, 1] // '1,22,1'
// [1, 2].toString() -> '1,2'
// [2, 1].toString() -> '2,1'
// '1,2' + '2,1' = '1,22,1'
+

*面试陷阱题之 ++ *

+
1
2
3
4
5
6
7
8
9
// 问表达式 'a' + + 'b' 返回结果是什么?
// 答案是 'aNaN'
'a' + + 'b' -> // 一元运算符优先级高
'a' + (+ 'b') -> // +'b'转数字类型,非有效结果是NaN
'a' + NaN.toString() -> // NaN调用toString()成字符串'NaN'
'aNaN' // 字符串接到一起后'aNaN'

// 类似题 '1' + + '4' 返回结果是什么
// 其实就是'4' -> 4 -> '4' 最后还是'14'
+ +

== 操作符 — 类型比较

+

相等运算符的运算规则如下:
1、如果两个值类型相同,进行 === 比较。(这个非常好理解,就不多说了)
(1)数字比大小
(2)字符串就通过 unicode 字符索引来比较
2、如果两个值类型不同,他们可能相等。根据下面规则进行类型转换再比较:
(1)如果一个是null、一个是undefined,那么[相等]。 // 这个有点特殊需要单独记
(2)如果任一值是字符串,另一个值是数值,在比较相等性之前先将字符串转换为数值;即是调用Number()函数。
(3)如果任一值时布尔值,则在比较相等性之前先将其转换为数值,即是调用Number()函数。
(4)如果一个是对象,另一个是数值或字符串,把对象转换成基础类型的值再比较。对象转换成基础类型,利用它的toString或者valueOf方法。 (js核心内置类ToPrimitive,会尝试valueOf先于toString;例外的是Date,Date利用的是toString转换。)

+
+

1不说,2(1)的话单独记,其他基本类型转数字比较,引用性数据类型调用ToPrimitive转换成基本数据类型

+

首先还是一道面试题

+
1
2
// [] == ![] 结果是什么
[] == ![] // true
+

为什么呢?我们来解析一下,

+
    +
  1. 首先我们看一下右侧,![]肯定是要先转换成boolen类型了吧,那么[]的布尔类型是什么呢,上面转换布尔的时候我们说过,除了那五个基本类型的假值以及正负0之外,都是真值,所以[] -> true ![] -> false
  2. +
  3. ToPrimitive(false)->0右边的值得出了数值类型的原始值
  4. +
  5. 看左边ToPrimitive([])->[].toString()->''
  6. +
  7. Number('')->0
  8. +
  9. 比较左右 0 == 0 -> true
  10. +
+

最后

到这里JS的内置数据类型及类型的转换和比较就讲完了,相信大家看过以后一定会记得住的
PS:突然好像学画漫画,《JS数据结构们》,一定大火,哈哈哈😂

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 数据类型、类型判断、类型转换、类型比较

+

文章作者:

+

发布时间:2020年01月02日 - 08:22

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/02/FE-guide-dataType/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/02/FE-guide-instanceof/index.html b/2020/01/02/FE-guide-instanceof/index.html new file mode 100644 index 0000000000..8838a54244 --- /dev/null +++ b/2020/01/02/FE-guide-instanceof/index.html @@ -0,0 +1,590 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- instanceof | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- instanceof +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

instanceof

instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype
举例:

+
1
a instanceof Object
+

判断 Objectprototype 是否在 a 的原型链上。

+

我们也可以试着实现一下 instanceof

+
1
2
3
4
5
6
7
8
9
10
11
12
function myInstanceof(left, right) { // left 表示左表达式,right 表示右表达式
let prototype = right.prototype // 获得类型的原型
left = left.__proto__ // 获得对象的原型

while (true) { // 判断对象的类型是否等于类型的原型
if (left === null)
return false
if (prototype === left)
return true
left = left.__proto__
}
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- instanceof

+

文章作者:

+

发布时间:2020年01月02日 - 12:29

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/02/FE-guide-instanceof/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/02/FE-guide-new/index.html b/2020/01/02/FE-guide-new/index.html new file mode 100644 index 0000000000..d18bf6a95b --- /dev/null +++ b/2020/01/02/FE-guide-new/index.html @@ -0,0 +1,608 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- new | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- new +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

new 一个对象的过程

    +
  1. 新生成了一个对象
  2. +
  3. 链接到原型
  4. +
  5. 绑定 this
  6. +
  7. 返回新对象
  8. +
+

在调用 new 的过程中会发生以上四件事情,我们也可以试着来自己实现一个 new

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

function create() {
// 创建一个空的对象
let obj = new Object()
// 获得构造函数
let Con = [].shift.call(arguments)
// 链接到原型
obj.__proto__ = Con.prototype
// 绑定 this,执行构造函数
let result = Con.apply(obj, arguments)
// 确保 new 出来的是个对象
return typeof result === 'object' ? result : obj
}
+ +

对于实例对象来说,都是通过 new 产生的,无论是 function Foo() 还是 let a = { b : 1 }

+

对于创建一个对象来说,更推荐使用字面量的方式创建对象(无论性能上还是可读性)。因为你使用 new Object() 的方式创建对象需要通过作用域链一层层找到 Object,但是你使用字面量的方式就没这个问题。

+
1
2
3
4
// js代码

function Foo() {} // function 就是个语法糖,内部等同于 new Function()
let a = { b: 1 } // 这个字面量内部也是使用了 new Object()
+

对于 new 来说,还需要注意下运算符优先级。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

function Foo() {
return this;
}
Foo.getName = function () {
console.log('1');
};
Foo.prototype.getName = function () {
console.log('2');
};

new Foo.getName(); // -> 1
new Foo().getName(); // -> 2
+ +

从上图可以看出,new Foo() 的优先级大于 new Foo ,所以对于上述代码来说可以这样划分执行顺序

+
1
2
3
4
// js代码

new (Foo.getName());
(new Foo()).getName();
+ +
    +
  • 对于第一个函数来说,先执行了 Foo.getName() ,所以结果为 1;
  • +
  • 对于后者来说,先执行 new Foo() 产生了一个实例,然后通过原型链找到了 Foo 上的 getName 函数,所以结果为 2。
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- new

+

文章作者:

+

发布时间:2020年01月02日 - 20:07

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/02/FE-guide-new/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/02/FE-guide-prototype/index.html b/2020/01/02/FE-guide-prototype/index.html new file mode 100644 index 0000000000..c0b22e0295 --- /dev/null +++ b/2020/01/02/FE-guide-prototype/index.html @@ -0,0 +1,646 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 原型 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 原型 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

原型

yuanxing.png
每个函数都有 prototype 属性,除了 Function.prototype.bind(),该属性指向原型对象,简称原型prototype原型对象里的constructor指向构造函数本身。
每个对象都有 __proto__ 属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 _proto_ 来访问。
对象可以通过 __proto__ 来寻找不属于该对象的属性,__proto__ 将对象连接起来组成了原型链

+ +

总结

+

prototype 原型对象

什么是原型?

每个函数都具有 prototype 属性,它被默认成一个对象,即原型对象
首先来介绍下 prototype 属性。这是一个显式原型属性,只有函数才拥有该属性。基本上所有函数都有这个属性,但是也有一个例外

+
1
2
// js代码
let fun = Function.prototype.bind()
+

如果你以上述方法创建一个函数,那么可以发现这个函数是不具有 prototype 属性的。

+

prototype 如何产生的

当我们声明一个函数时,这个属性就被自动创建了。

+
1
2

function Foo() {}
+

并且这个属性的值是一个对象(也就是原型),只有一个属性 constructor
constructor 对应着构造函数,也就是 Foo

+

什么是原型链?

当对象使用属性时,先在自身找,有就直接用,没有就沿着proto这条链往上找,直到 Object 原型的位置,有就返回相应的值,没有就返回 underfined。

+

constructor 构造函数

什么是构造函数?

任何一个函数,只要被 new 操作符使用,就可以是一个构造函数(构造函数建议以大写开头)
另外,在 JavaScript 的内置对象中,所有的函数对象都是 Function 构造函数的实例,比如:Object、Array等

+

constructor 是一个公有且不可枚举的属性。一旦我们改变了函数的 prototype ,那么新对象就没有这个属性了(当然可以通过原型链取到 constructor)。

+

那么你肯定也有一个疑问,这个属性到底有什么用呢?其实这个属性可以说是一个历史遗留问题,在大部分情况下是没用的,在我的理解里,我认为他有两个作用:

+
    +
  1. 让实例对象知道是什么函数构造了它
  2. +
  3. 如果想给某些类库中的构造函数增加一些自定义的方法,就可以通过 xx.constructor.method 来扩展
  4. +
+

_proto_

这是每个对象都有的隐式原型属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 _proto_ 来访问。

+

因为在 JS 中是没有类的概念的,为了实现类似继承的方式,通过 _proto_ 将对象和原型联系起来组成原型链,得以让对象可以访问到不属于自己的属性。

+

实例对象的 _proto_ 如何产生的

从上图可知,当我们使用 new 操作符时,生成的实例对象拥有了 _proto_ 属性。

+
1
2
3
4
function Foo() {}
// 这个函数是 Function 的实例对象
// function 就是一个语法糖
// 内部调用了 new Function(...)
+

所以可以说,在 new 的过程中,新对象被添加了 _proto_ 并且链接到构造函数的原型上。

+

new 的过程

    +
  1. 新生成了一个对象
  2. +
  3. 链接到原型
  4. +
  5. 绑定 this
  6. +
  7. 返回新对象
  8. +
+

在调用 new 的过程中会发生以上四件事情,我们也可以试着来自己实现一个 new

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person( name ){ 
this.name = name;
};
Person.prototype.getName = function(){
return this.name;
};

function create() {
let obj = new Object() // 从 Object.prototype 上克隆一个空的对象
let Con = [].shift.call(arguments) // 获取外部传入的构造器,此例是 Person
obj.__proto__ = Con.prototype // 指向正确的原型,链接到原型
let result = Con.apply(obj, arguments) // 绑定 this,执行构造函数,借用外部传入的构造器给 obj 设置属性
return typeof result === 'object' ? result : obj // 确保 new 出来的是个对象
}

create(Person,'lixg')
+

对于实例对象来说,都是通过 new 产生的,无论是 function Foo() 还是 let a = { b : 1 }

+

对于创建一个对象来说,更推荐使用字面量的方式创建对象。因为你使用 new Object() 的方式创建对象需要通过作用域链一层层找到 Object ,但是你使用字面量的方式就没这个问题。

+
1
2
function Foo() {} // function 就是个语法糖,内部等同于 new Function()
let a = { b: 1 } // 这个字面量内部也是使用了 new Object()
+ +

Function.proto === Function.prototype

对于对象来说,xx.__proto__.contrcutor 是该对象的构造函数,但是在图中我们可以发现 Function.__proto__ === Function.prototype,难道这代表着 Function 自己产生了自己?

+

答案肯定是否认的,要说明这个问题我们先从 Object 说起。

+

从图中我们可以发现,所有对象都可以通过原型链最终找到 Object.prototype ,虽然 Object.prototype 也是一个对象,但是这个对象却不是 Object 创造的,而是引擎自己创建了 Object.prototype 。所以可以这样说,所有实例都是对象,但是对象不一定都是实例。

+

接下来我们来看 Function.prototype 这个特殊的对象,如果你在浏览器将这个对象打印出来,会发现这个对象其实是一个函数。

+

我们知道函数都是通过 new Function() 生成的,难道 Function.prototype 也是通过 new Function() 产生的吗?答案也是否定的,这个函数也是引擎自己创建的。首先引擎创建了 Object.prototype ,然后创建了 Function.prototype ,并且通过 __proto__ 将两者联系了起来。这里也很好的解释了上面的一个问题,为什么 let fun = Function.prototype.bind() 没有 prototype 属性。因为 Function.prototype 是引擎创建出来的对象,引擎认为不需要给这个对象添加 prototype 属性。

+

所以我们又可以得出一个结论,不是所有函数都是 new Function() 产生的。
有了 Function.prototype 以后才有了 function Function() ,然后其他的构造函数都是 function Function() 生成的。

+

现在可以来解释 Function.__proto__ === Function.prototype 这个问题了。因为先有的 Function.prototype 以后才有的 function Function() ,所以也就不存在鸡生蛋蛋生鸡的悖论问题了。对于为什么 Function.__proto__ 会等于 Function.prototype ,个人的理解是:其他所有的构造函数都可以通过原型链找到 Function.prototype ,并且 function Function() 本质也是一个函数,为了不产生混乱就将 function Function()__proto__ 联系到了 Function.prototype 上。

+

总结

    +
  • Object 是所有对象的爸爸,所有对象都可以通过 __proto__ 找到它
  • +
  • Function 是所有函数的爸爸,所有函数都可以通过 __proto__ 找到它
  • +
  • Function.prototypeObject.prototype 是两个特殊的对象,他们由引擎来创建
  • +
  • 除了以上两个特殊对象,其他对象都是通过构造器 new 出来的
  • +
  • 函数的 prototype 是一个对象,也就是原型
  • +
  • 对象的 __proto__ 指向原型, __proto__ 将对象和原型连接起来组成了原型链
    总结
  • +
+

归纳

ES 把对象定义为:“无序属性的集合,其属性可以包含基本值,对象和函数”。
严格来讲,这就相当于说对象是一组没有特定顺序的值。ES 中的构造函数可以用来创建特定类型的对象,用来在创建对象时初始化对象。它的特点是,一般为大写字母开头,使用 new 操作符来实例化对象,比如:

+
1
2
3
4
function Person() {}
var person = new Person();
person.name = "Kevin";
console.log(person.name); // Kevin
+ +

Person 就是构造函数, person 就是对象。对于对象而言,每个 JS 对象一定对应一个原型对象,并从原型对象继承属性和方法。对象 __proto__ 属性的值就是它所对应的原型对象。对象的 __proto__ 指向自己构造函数的 prototype 。所以对象的原型链就是 obj.__proto__.proto__.... 。对于函数而言,只有函数才有 prototype 属性, Person.prototype 是一个对象,并且有两个属性, 一个是 constructor 指向其构造函数 Person , 一个是 __proto__ 属性:是一个对象,指向上一层的原型。原型链的尽头是 Object.prototype 。所有对象均从 Object.prototype 继承属性。Function.prototypeFunction.__proto__ 为同一对象。Object/Array/String 等等构造函数本质上和 Function 一样,均继承于 Function.prototypeFunction.prototype 直接继承 Object.prototype 。这里的 ObjectFunction 有点鸡和蛋的问题,总结:先有 Object.prototype(原型链顶端),Function.prototype 继承 Object.prototype 而产生,最后,FunctionObject 和其它构造函数继承 Function.prototype 而产生。属性查找时,先在对象自己上找,找不到才会一步步根据原型链往上找。
继承

+

关联阅读

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain
说说原型(prototype)、原型链和原型继承

+

扩展阅读

继承

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 原型

+

文章作者:

+

发布时间:2020年01月02日 - 11:22

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/02/FE-guide-prototype/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/02/FE-guide-this/index.html b/2020/01/02/FE-guide-this/index.html new file mode 100644 index 0000000000..872d724c29 --- /dev/null +++ b/2020/01/02/FE-guide-this/index.html @@ -0,0 +1,591 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- this | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- this +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

this

this 是很多人会混淆的概念,但是其实他一点都不难,你只需要记住几个规则就可以了。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// js代码

function foo() {
console.log(this.a)
}
var a = 1
foo()

var obj = {
a: 2,
foo: foo
}
obj.foo()

// 以上两者情况 `this` 只依赖于调用函数前的对象,优先级是第二个情况大于第一个情况

// 以下情况是优先级最高的,`this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向
var c = new foo()
c.a = 3
console.log(c.a)

// 还有种就是利用 `call` , `apply` , `bind` 改变 `this` ,这个优先级仅次于 `new`
+

以上几种情况明白了,很多代码中的 this 应该就没什么问题了,下面让我们看看箭头函数中的 this

+
1
2
3
4
5
6
7
8
9
10
// js代码

function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())
+

箭头函数其实是没有 this 的,这个函数中的 this 只取决于他外面的第一个不是箭头函数的函数的 this 。在这个例子中,因为调用 a 符合前面代码中的第一个情况,所以 thiswindow。并且 this 一旦绑定了上下文,就不会被任何代码改变。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- this

+

文章作者:

+

发布时间:2020年01月02日 - 12:59

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/02/FE-guide-this/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2020/01/02/FE-guide-\346\211\247\350\241\214\344\270\212\344\270\213\346\226\207/index.html" "b/2020/01/02/FE-guide-\346\211\247\350\241\214\344\270\212\344\270\213\346\226\207/index.html" new file mode 100644 index 0000000000..cb0f2bc0c2 --- /dev/null +++ "b/2020/01/02/FE-guide-\346\211\247\350\241\214\344\270\212\344\270\213\346\226\207/index.html" @@ -0,0 +1,610 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 执行上下文 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 执行上下文 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

执行上下文

当执行 JS 代码时,会产生三种执行上下文

+
    +
  • 全局执行上下文
  • +
  • 函数执行上下文
  • +
  • eval 执行上下文
  • +
+

每个执行上下文中都有三个重要的属性

+
    +
  • 变量对象( VO ),包含变量、函数声明和函数的形参,该属性只能在全局上下文中访问
  • +
  • 作用域链( JS 采用词法作用域,也就是说变量的作用域是在定义时就决定了)
  • +
  • this
    1
    2
    3
    4
    5
    6
    7
    // js代码

    var a = 10
    function foo(i) {
    var b = 20
    }
    foo()
    +对于上述代码,执行栈中有两个上下文:全局上下文和函数 foo 上下文。
    1
    2
    3
    4
    5
    6
    // js代码

    stack = [
    globalContext,
    fooContext
    ]
    +对于全局上下文来说, VO 大概是这样的
    1
    2
    3
    4
    5
    6
    7
    // js代码

    globalContext.VO === globe
    globalContext.VO = {
    a: undefined,
    foo: <Function>,
    }
    +对于函数 foo 来说, VO 不能访问,只能访问到活动对象( AO
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // js代码

    fooContext.VO === foo.AO
    fooContext.AO {
    i: undefined,
    b: undefined,
    arguments: <>
    }
    // arguments 是函数独有的对象(箭头函数没有)
    // 该对象是一个伪数组,有 `length` 属性且可以通过下标访问元素
    // 该对象中的 `callee` 属性代表函数本身
    // `caller` 属性代表函数的调用者
    +对于作用域链,可以把它理解成包含自身变量对象和上级变量对象的列表,通过 [[Scope]] 属性查找上级变量
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    fooContext.[[Scope]] = [
    globalContext.VO
    ]
    fooContext.Scope = fooContext.[[Scope]] + fooContext.VO
    fooContext.Scope = [
    fooContext.VO,
    globalContext.VO
    ]
    +接下来让我们看一个老生常谈的例子, var
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    b() // call b
    console.log(a) // undefined

    var a = 'Hello world'

    function b() {
    console.log('call b')
    }
    +想必以上的输出大家肯定都已经明白了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行上下文时,会有两个阶段。第一个阶段是创建的阶段(具体步骤是创建 VO ), JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined ,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用。
  • +
+

在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

b() // call b second

function b() {
console.log('call b fist')
}
function b() {
console.log('call b second')
}
var b = 'Hello world'
+

var 会产生很多错误,所以在 ES6 中引入了 letlet 不能在声明前使用,但是这并不是常说的 let 不会提升, let 提升了声明但没有赋值,因为临时死区导致了并不能在声明前使用。

+

对于非匿名的立即执行函数需要注意以下一点

+
1
2
3
4
5
6
7
// js代码

var foo = 1
(function foo() {
foo = 10
console.log(foo)
}()) // -> ƒ foo() { foo = 10 ; console.log(foo) }
+

因为当 JS 解释器在遇到非匿名的立即执行函数时,会创建一个辅助的特定对象,然后将函数名称作为这个对象的属性,因此函数内部才可以访问到 foo ,但是这个值又是只读的,所以对它的赋值并不生效,所以打印的结果还是这个函数,并且外部的值也没有发生更改。

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

specialObject = {};

Scope = specialObject + Scope;

foo = new FunctionExpression;
foo.[[Scope]] = Scope;
specialObject.foo = foo; // {DontDelete}, {ReadOnly}

delete Scope[0]; // remove specialObject from the front of scope chain
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 执行上下文

+

文章作者:

+

发布时间:2020年01月02日 - 13:18

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/02/FE-guide-%E6%89%A7%E8%A1%8C%E4%B8%8A%E4%B8%8B%E6%96%87/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/03/FE-guide-Generator/index.html b/2020/01/03/FE-guide-Generator/index.html new file mode 100644 index 0000000000..a48d312243 --- /dev/null +++ b/2020/01/03/FE-guide-Generator/index.html @@ -0,0 +1,589 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- Generator 生成器 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- Generator 生成器 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Generator 实现

GeneratorES6 中新增的语法,和 Promise 一样,都可以用来异步编程

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

// 使用 * 表示这是一个 Generator 函数
// 内部可以通过 yield 暂停代码
// 通过调用 next 恢复执行
function* test() {
let a = 1 + 2;
yield 2;
yield 3;
}
let b = test();
console.log(b.next()); // > { value: 2, done: false }
console.log(b.next()); // > { value: 3, done: false }
console.log(b.next()); // > { value: undefined, done: true }
+

从以上代码可以发现,加上 * 的函数执行后拥有了 next 函数,也就是说函数执行后返回了一个对象。每次调用 next 函数可以继续执行被暂停的代码。以下是 Generator 函数的简单实现

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// js代码

// cb 也就是编译过的 test 函数
function generator(cb) {
return (function() {
var object = {
next: 0,
stop: function() {}
};

return {
next: function() {
var ret = cb(object);
if (ret === undefined) return { value: undefined, done: true };
return {
value: ret,
done: false
};
}
};
})();
}
// 如果你使用 babel 编译后可以发现 test 函数变成了这样
function test() {
var a;
return generator(function(_context) {
while (1) {
switch ((_context.prev = _context.next)) {
// 可以发现通过 yield 将代码分割成几块
// 每次执行 next 函数就执行一块代码
// 并且表明下次需要执行哪块代码
case 0:
a = 1 + 2;
_context.next = 4;
return 2;
case 4:
_context.next = 6;
return 3;
// 执行完毕
case 6:
case "end":
return _context.stop();
}
}
});
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- Generator 生成器

+

文章作者:

+

发布时间:2020年01月03日 - 03:05

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-Generator/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/03/FE-guide-Map-FlatMap-Reduce/index.html b/2020/01/03/FE-guide-Map-FlatMap-Reduce/index.html new file mode 100644 index 0000000000..e625ede3d1 --- /dev/null +++ b/2020/01/03/FE-guide-Map-FlatMap-Reduce/index.html @@ -0,0 +1,602 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- Map、FlatMap 和 Reduce | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- Map、FlatMap 和 Reduce +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Map、FlatMap 和 Reduce

Map

Map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后 append 到新的数组中。

+
1
2
3
// js代码

[1, 2, 3].map((v) => v + 1) // -> [2, 3, 4]
+

Map 有三个参数,分别是当前索引元素索引原数组

+

FlatMap

FlatMapmap 的作用几乎是相同的,但是对于多维数组来说,会将原数组降维。可以将 FlatMap 看成是 map + flatten ,目前该函数在浏览器中还不支持。

+
1
2
3
// js代码

[1, [2], 3].flatMap((v) => v + 1) // -> [2, 3, 4]
+

如果想将一个多维数组彻底的降维,可以这样实现

+
1
2
3
4
5
const flattenDeep = (arr) => Array.isArray(arr)
? arr.reduce( (a, b) => [...a, ...flattenDeep(b)] , [])
: [arr]

flattenDeep([1, [[2], [3, [4]], 5]])
+ +

Reduce 升序执行

Reduce 作用是数组中的值组合起来,最终得到一个值
reduce() 方法对数组中的每个元素执行一个由您提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。

+

reducer 函数接收4个参数:

+
    +
  • Accumulator (acc) (累计器)
  • +
  • Current Value (cur) (当前值)
  • +
  • Current Index (idx) (当前索引)
  • +
  • Source Array (src) (源数组)
    您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
// js代码

function a() {
console.log(1);
}

function b() {
console.log(2);
}

[a, b].reduce((a, b) => a(b()))
// -> 2 1
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- Map、FlatMap 和 Reduce

+

文章作者:

+

发布时间:2020年01月03日 - 03:35

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-Map-FlatMap-Reduce/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/03/FE-guide-Module/index.html b/2020/01/03/FE-guide-Module/index.html new file mode 100644 index 0000000000..fbb48d7b5c --- /dev/null +++ b/2020/01/03/FE-guide-Module/index.html @@ -0,0 +1,601 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 模块化 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 模块化 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

模块化

在有 Babel 的情况下,我们可以直接使用 ES6 的模块化

+
1
2
3
4
5
6
7
8
9
10
// js代码

// file a.js
export function a() {}
export function b() {}
// file b.js
export default function() {}

import {a, b} from './a.js'
import XXX from './b.js'
+

CommonJS

CommonJsNode 独有的规范,浏览器中使用就需要用到 Browserify 解析了。

+
1
2
3
4
5
6
7
8
9
10
11
12
// js代码

// a.js
module.exports = {
a: 1
}
// or
exports.a = 1

// b.js
var module = require('./a.js')
module.a // -> log 1
+

在上述代码中, module.exportsexports 很容易混淆,让我们来看看大致内部实现

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// js代码

var module = require('./a.js')
module.a
// 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {
a: 1
}
// 基本实现
var module = {
exports: {} // exports 就是个空对象
}
// 这个是为什么 exports 和 module.exports 用法相似的原因
var exports = module.exports
var load = function (module) {
// 导出的东西
var a = 1
module.exports = a
return module.exports
};
+

再来说说 module.exports 和 exports,用法其实是相似的,但是不能对 exports 直接赋值,不会有任何效果。

+

对于 CommonJS 和 ES6 中的模块化的两者区别是:

+
    +
  • 前者支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
  • +
  • 前者是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
  • +
  • 前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是后者采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
  • +
  • 后者会编译成 require/exports 来执行的
  • +
+

AMD

AMD 是由 RequireJS 提出的

+
1
2
3
4
5
6
7
8
9
10
11
12
13
// js代码

// AMD
define(['./a', './b'], function(a, b) {
a.do()
b.do()
})
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
var b = require('./b')
b.doSomething()
})
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 模块化

+

文章作者:

+

发布时间:2020年01月03日 - 00:41

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-Module/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/03/FE-guide-Promise/index.html b/2020/01/03/FE-guide-Promise/index.html new file mode 100644 index 0000000000..978d3a404a --- /dev/null +++ b/2020/01/03/FE-guide-Promise/index.html @@ -0,0 +1,592 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- Promise | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- Promise +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Promise 实现

PromiseES6 新增的语法,解决了回调地狱的问题。

+

可以把 Promise 看成一个状态机。初始是 pending 状态,可以通过函数 resolvereject ,将状态转变为 resolved 或者 rejected 状态,状态一旦改变就不能再次变化

+

then 函数会返回一个 Promise 实例,并且该返回值是一个新的实例而不是之前的实例。因为 Promise 规范规定除了 pending 状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个 then 调用就失去意义了。

+

对于 then 来说,本质上可以把它看成是 flatMap

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// js代码

// 三种状态
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
// promise 接收一个函数参数,该函数会立即执行
function MyPromise(fn) {
let _this = this;
_this.currentState = PENDING;
_this.value = undefined;
// 用于保存 then 中的回调,只有当 promise
// 状态为 pending 时才会缓存,并且每个实例至多缓存一个
_this.resolvedCallbacks = [];
_this.rejectedCallbacks = [];

_this.resolve = function (value) {
if (value instanceof MyPromise) {
// 如果 value 是个 Promise,递归执行
return value.then(_this.resolve, _this.reject)
}
setTimeout(() => { // 异步执行,保证执行顺序
if (_this.currentState === PENDING) {
_this.currentState = RESOLVED;
_this.value = value;
_this.resolvedCallbacks.forEach(cb => cb());
}
})
};

_this.reject = function (reason) {
setTimeout(() => { // 异步执行,保证执行顺序
if (_this.currentState === PENDING) {
_this.currentState = REJECTED;
_this.value = reason;
_this.rejectedCallbacks.forEach(cb => cb());
}
})
}
// 用于解决以下问题
// new Promise(() => throw Error('error))
try {
fn(_this.resolve, _this.reject);
} catch (e) {
_this.reject(e);
}
}

MyPromise.prototype.then = function (onResolved, onRejected) {
var self = this;
// 规范 2.2.7,then 必须返回一个新的 promise
var promise2;
// 规范 2.2.onResolved 和 onRejected 都为可选参数
// 如果类型不是函数需要忽略,同时也实现了透传
// Promise.resolve(4).then().then((value) => console.log(value))
onResolved = typeof onResolved === 'function' ? onResolved : v => v;
onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;

if (self.currentState === RESOLVED) {
return (promise2 = new MyPromise(function (resolve, reject) {
// 规范 2.2.4,保证 onFulfilled,onRjected 异步执行
// 所以用了 setTimeout 包裹下
setTimeout(function () {
try {
var x = onResolved(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}

if (self.currentState === REJECTED) {
return (promise2 = new MyPromise(function (resolve, reject) {
setTimeout(function () {
// 异步执行onRejected
try {
var x = onRejected(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}

if (self.currentState === PENDING) {
return (promise2 = new MyPromise(function (resolve, reject) {
self.resolvedCallbacks.push(function () {
// 考虑到可能会有报错,所以使用 try/catch 包裹
try {
var x = onResolved(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});

self.rejectedCallbacks.push(function () {
try {
var x = onRejected(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});
}));
}
};
// 规范 2.3
function resolutionProcedure(promise2, x, resolve, reject) {
// 规范 2.3.1,x 不能和 promise2 相同,避免循环引用
if (promise2 === x) {
return reject(new TypeError("Error"));
}
// 规范 2.3.2
// 如果 x 为 Promise,状态为 pending 需要继续等待否则执行
if (x instanceof MyPromise) {
if (x.currentState === PENDING) {
x.then(function (value) {
// 再次调用该函数是为了确认 x resolve 的
// 参数是什么类型,如果是基本类型就再次 resolve
// 把值传给下个 then
resolutionProcedure(promise2, value, resolve, reject);
}, reject);
} else {
x.then(resolve, reject);
}
return;
}
// 规范 2.3.3.3.3
// reject 或者 resolve 其中一个执行过得话,忽略其他的
let called = false;
// 规范 2.3.3,判断 x 是否为对象或者函数
if (x !== null && (typeof x === "object" || typeof x === "function")) {
// 规范 2.3.3.2,如果不能取出 then,就 reject
try {
// 规范 2.3.3.1
let then = x.then;
// 如果 then 是函数,调用 x.then
if (typeof then === "function") {
// 规范 2.3.3.3
then.call(
x,
y => {
if (called) return;
called = true;
// 规范 2.3.3.3.1
resolutionProcedure(promise2, y, resolve, reject);
},
e => {
if (called) return;
called = true;
reject(e);
}
);
} else {
// 规范 2.3.3.4
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
// 规范 2.3.4,x 为基本类型
resolve(x);
}
}
+

以上就是根据 Promise / A+ 规范来实现的代码,可以通过 promises-aplus-tests 的完整测试

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- Promise

+

文章作者:

+

发布时间:2020年01月03日 - 02:52

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-Promise/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/03/FE-guide-async-Proxy/index.html b/2020/01/03/FE-guide-async-Proxy/index.html new file mode 100644 index 0000000000..3638f3404f --- /dev/null +++ b/2020/01/03/FE-guide-async-Proxy/index.html @@ -0,0 +1,589 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- Proxy | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- Proxy +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Proxy

ProxyES6 中新增的功能,可以用来自定义对象中的操作

+
1
2
3
4
5
// js代码

let p = new Proxy(target, handler);
// `target` 代表需要添加代理的对象
// `handler` 用来自定义对象中的操作
+

可以很方便的使用 Proxy 来实现一个数据绑定和监听

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(target, property)
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
setBind(value);
return Reflect.set(target, property, value);
}
};
return new Proxy(obj, handler);
};

let obj = { a: 1 }
let value
let p = onWatch(obj, (v) => {
value = v
}, (target, property) => {
console.log(`Get '${property}' = ${target[property]}`);
})
p.a = 2 // bind `value` to `2`
p.a // -> Get 'a' = 2
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- Proxy

+

文章作者:

+

发布时间:2020年01月03日 - 04:00

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-async-Proxy/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/03/FE-guide-async-await/index.html b/2020/01/03/FE-guide-async-await/index.html new file mode 100644 index 0000000000..52c226becc --- /dev/null +++ b/2020/01/03/FE-guide-async-await/index.html @@ -0,0 +1,603 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- async 和 await | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- async 和 await +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

async 和 await

一个函数如果加上 async ,那么该函数就会返回一个 Promise

+
1
2
3
4
async function test() {
return "1";
}
console.log(test()); // -> Promise {<resolved>: "1"}
+

可以把 async 看成将函数返回值使用 Promise.resolve() 包裹了下。
await 只能在 async 函数中使用

+
1
2
3
4
5
6
7
8
9
10
11
12
13
function sleep() {
return new Promise(resolve => {
setTimeout(() => {
console.log('finish')
resolve("sleep");
}, 2000);
});
}
async function test() {
let value = await sleep();
console.log("object");
}
test()
+ +

上面代码会先打印 finish 然后再打印 object 。因为 await 会等待 sleep 函数 resolve ,所以即使后面是同步代码,也不会先去执行同步代码再来执行异步代码。

+

asyncawait 相比直接使用 Promise 来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码。缺点在于滥用 await 可能会导致性能问题,因为 await 会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。

+

下面来看一个使用 await 的代码。

+
1
2
3
4
5
6
7
8
9
10
var a = 0
var b = async () => {
a = a + await 10
console.log('2', a) // -> '2' 10
a = (await 10) + a
console.log('3', a) // -> '3' 20
}
b()
a++
console.log('1', a) // -> '1' 1
+ +

对于以上代码你可能会有疑惑,这里说明下原理

+
    +
  • 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为在 await 内部实现了 generatorsgenerators 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
  • +
  • 因为 await 是异步操作,遇到 await 就会立即返回一个 pending 状态的 Promise 对象,暂时返回执行代码的控制权,使得函数外的代码得以继续执行,所以会先执行 console.log('1', a)
  • +
  • 这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 10
  • +
  • 然后后面就是常规执行代码了
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- async 和 await

+

文章作者:

+

发布时间:2020年01月03日 - 03:52

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-async-await/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/03/FE-guide-call-apply-bind/index.html b/2020/01/03/FE-guide-call-apply-bind/index.html new file mode 100644 index 0000000000..63c438cbf4 --- /dev/null +++ b/2020/01/03/FE-guide-call-apply-bind/index.html @@ -0,0 +1,596 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- call, apply, bind 区别 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- call, apply, bind 区别 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

call, apply, bind 区别

首先说下前两者的异同。
相同: callapply 都是为了解决改变 this 的指向。
不同:传参的方式不同,除了第一个参数外, call 可以接收一个参数列表apply 只接受一个参数数组

+
1
2
3
4
5
6
7
8
9
10
11
// js代码
let anObj = {
value: 1
}
function getValue(name, age) {
console.log(name)
console.log(age)
console.log(this.value)
}
getValue.call(anObj, 'lixuguang', '31')
getValue.apply(anObj, ['lixuguang', '31'])
+

模拟实现 callapply

可以从以下几点来考虑如何实现

+
    +
  • 不传入第一个参数,那么默认为 window
  • +
  • 改变了 this 指向,让新的对象可以执行该函数。那么思路是否可以变成给新的对象添加一个函数,然后在执行完以后删除?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    Function.prototype.myCall = function (context) {
    var context = context || window // 有入参用入参,没有给 window
    context.fn = this // 给 context 添加一个属性,getValue.call(anObj, 'lixuguang', '31') => anObj.fn = getValue
    var args = [...arguments].slice(1) // 将 context 后面的参数取出来
    var result = context.fn(...args) // getValue.call(anObj, 'lixuguang', '31') => anObj.fn('yck', '24')
    delete context.fn // 删除 fn
    return result
    }
    +以上就是 call 的思路,apply 的实现也类似
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    Function.prototype.Apply = function (context) {
    var context = context || window // 有入参用入参,没有给 window
    context.fn = this // 给 context 添加一个属性,getValue.call(anObj, 'lixuguang', '31') => anObj.fn = getValue
    var args = arguments[1] // 将 context 后面的参数取出来
    var result = args?context.fn(...args):context.fn() // getValue.call(anObj, ['lixuguang', '31']) => anObj.fn('yck', '24')
    delete context.fn // 删除 fn
    return result
    }
    +bind 和其他两个方法作用也是一致的,只是该方法会返回一个函数。并且我们可以通过 bind 实现柯里化。
  • +
+

同样的,也来模拟实现下 bind

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码

Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
var _this = this
var args = [...arguments].slice(1)

return function F() { // 返回一个函数,这是 `bind` 和 `call` , `apply` 的区别
if (this instanceof F) { // 因为返回了一个函数,我们可以 new F(),所以需要判断
return new _this(...args, ...arguments)
}
return _this.apply(context, args.concat(...arguments))
}
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- call, apply, bind 区别

+

文章作者:

+

发布时间:2020年01月03日 - 02:19

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-call-apply-bind/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/03/FE-guide-copy/index.html b/2020/01/03/FE-guide-copy/index.html new file mode 100644 index 0000000000..0b64d3418c --- /dev/null +++ b/2020/01/03/FE-guide-copy/index.html @@ -0,0 +1,621 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 深浅拷贝 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 深浅拷贝 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

深浅拷贝

1
2
3
4
5
6
let a = {
age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2
+

从上述例子中我们可以发现,如果给一个变量赋值一个对象,那么两者的值会是同一个引用,其中一方改变,另一方也会相应改变。
通常在开发中我们不希望出现这样的问题,我们可以使用浅拷贝来解决这个问题。

+

浅拷贝

首先可以通过 Object.assign 来解决这个问题。

+
1
2
3
4
5
6
7
8
// js代码

let a = {
age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // => 1
+

当然我们也可以通过展开运算符(…)来解决

+
1
2
3
4
5
6
let a = {
age: 1
}
let b = {...a}
a.age = 2
console.log(b.age) // => 1
+

我们还可以用很多简单的方法都能实现浅拷贝:

+
1
2
arr.slice();
arr.concat();
+ +

通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就需要使用到深拷贝了

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = {...a}
a.jobs.first = 'native'
console.log(b.jobs.first) // native
+

浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到刚开始的话题了,两者享有相同的引用。要解决这个问题,我们需要引入深拷贝。

+

深拷贝

这个问题通常可以通过 JSON.parse(JSON.stringify(object)) 来解决,这也是最好用最简单的方法,俗称乞丐版。
乞丐版

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
+

但是该方法也是有局限性的:

+
    +
  • 会忽略 undefined
  • +
  • 会忽略 symbol
  • +
  • 不能序列化函数
  • +
  • 不能解决循环引用的对象
  • +
+

举个栗子:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

let obj = {
a: 1,
b: {
c: 2,
d: 3,
},
}

obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c

let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj) // => Uncaught TypeError: Converting circular structure to JSON
+

如果你有这么一个循环引用对象,你会发现你不能通过该方法深拷贝
在遇到函数、 undefined 或者 symbol 的时候,该对象也不能正常的序列化

+
1
2
3
4
5
6
7
8
9
10
// js代码

let a = {
age: undefined,
sex: Symbol('fmale'),
jobs: function() {},
name: 'lixuguang'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // => {name: "lixuang"}
+ +

你会发现在上述情况中,该方法会忽略掉函数和 undefined
但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题,并且该函数是内置函数中处理深拷贝性能最快的。

+

那么是否可以解决函数和循环引用的问题呢?答案是肯定可以解决,接下来是基础版本的改造
基础版

+
1
2
3
4
5
6
7
8
9
10
11
function myClone(target){
if(typeof target === 'object'){ // 判断传入目标是否是object类型
let cloneTarget = {}; // 创建克隆对象
for(const key in target){ // 遍历目标对象
cloneTarget[key] = myClone(target[key]) // 递归调用 clone 方法
}
return cloneTarget;
} else {
return target // 如果不是 object 返回
}
}
+

写到这里已经可以帮助你应付一些面试官考察你的递归解决问题的能力。但是显然,这个深拷贝函数还是有一些问题。
这里只考虑了对象,没有考虑数组。
下面我们来做一个强化版的深拷贝,同时考虑对象、数组还有循环引用的问题。
强化版

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function myClone(target, map = new WeakMap()) { // WeakMap => 键对象弱引用, 可被垃圾回收
if(typeof target === 'object'){ // 判断是否是对象
let cloneTarget = Array.isArray(target) ? [] : {}; // 判断是是数组还是对象
if(map.get(target)) {
return target;
}

map.set(target, cloneTarget);

for(const key in target) {
cloneTarget[key] = myClone(target[key], map)
}
return cloneTarget;
} else {
return target;
}
}
+

当然如果你的数据中含有以上三种情况下,可以使用 lodash 的深拷贝函数。
如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// js代码

function structuralClone(obj) {
return new Promise(resolve => {
const {port1, port2} = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
});
}

var obj = {
a: 1,
b: {
c: b
}
}
// 注意该方法是异步的
// 可以处理 undefined 和循环引用对象
(async () => {
const clone = await structuralClone(obj)
})()
+ +

深拷贝实现方式2,可以深拷贝 functionsymbol,等等,堪称终极版
终极版

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// js代码

const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const argsTag = '[object Arguments]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];
function forEach(array, iteratee) {
let index = -1;
const length = array.length;
while (++index < length) {
iteratee(array[index], index);
}
return array;
}

function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}

function getType(target) {
return Object.prototype.toString.call(target);
}

function getInit(target) {
const Ctor = target.constructor;
return new Ctor();
}

function cloneSymbol(targe) {
return Object(Symbol.prototype.valueOf.call(targe));
}

function cloneReg(targe) {
const reFlags = /\w*$/;
const result = new targe.constructor(targe.source, reFlags.exec(targe));
result.lastIndex = targe.lastIndex;
return result;
}

function cloneFunction(func) {
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
if (func.prototype) {
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if (body) {
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(funcString);
}
}

function cloneOtherType(targe, type) {
const Ctor = targe.constructor;
switch (type) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(targe);
case regexpTag:
return cloneReg(targe);
case symbolTag:
return cloneSymbol(targe);
case funcTag:
return cloneFunction(targe);
default:
return null;
}
}

function clone(target, map = new WeakMap()) {
// 克隆原始类型
if (!isObject(target)) {
return target;
}

// 初始化
const type = getType(target);
let cloneTarget;
if (deepTag.includes(type)) {
cloneTarget = getInit(target, type);
} else {
return cloneOtherType(target, type);
}

// 防止循环引用
if (map.get(target)) {
return target;
}
map.set(target, cloneTarget);

// 克隆set
if (type === setTag) {
target.forEach(value => {
cloneTarget.add(clone(value));
});
return cloneTarget;
}

// 克隆map
if (type === mapTag) {
target.forEach((value, key) => {
cloneTarget.set(key, clone(value));
});
return cloneTarget;
}

// 克隆对象和数组
const keys = type === arrayTag ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone(target[key], map);
});

return cloneTarget;
}

// 调用方法
clone(target);
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 深浅拷贝

+

文章作者:

+

发布时间:2020年01月03日 - 00:05

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-copy/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/03/FE-guide-debounce-throttle/index.html b/2020/01/03/FE-guide-debounce-throttle/index.html new file mode 100644 index 0000000000..498d5e3aa4 --- /dev/null +++ b/2020/01/03/FE-guide-debounce-throttle/index.html @@ -0,0 +1,605 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 函数防抖和节流 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 函数防抖和节流 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

函数防抖和节流

在前端开发的过程中,我们经常会需要绑定一些持续触发的事件,如 resize、scroll、mousemove 等等,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。

+

通常这种情况下我们怎么去解决的呢?一般来讲,防抖和节流是比较好的解决方案。

+

防抖

你是否在日常开发中遇到一个问题,在滚动事件中需要做个复杂计算或者实现一个按钮的防二次点击操作。

+

这些需求都可以通过函数防抖动来实现。尤其是第一个需求,如果在频繁的事件回调中做复杂计算,很有可能导致页面卡顿,不如将多次计算合并为一次计算,只在一个精确点做操作。

+

PS:防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的情况下只会调用一次,而节流的 情况会每隔一定时间(参数wait)调用函数。

+

我们先来看一个袖珍版的防抖理解一下防抖的实现:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
// 缓存一个定时器id
let timer = 0
// 这里返回的函数是每次用户实际调用的防抖函数
// 如果已经设定过定时器了就清空上一次的定时器
// 开始一个新的定时器,延迟执行用户传入的方法
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
// 不难看出如果用户调用该函数的间隔小于wait的情况下,上一次的时间还未到就被清除了,并不会执行函数
+

这是一个简单版的防抖,但是有缺陷,这个防抖只能在最后调用。一般的防抖会有immediate选项,表示是否立即调用。这两者的区别,举个栗子来说:

+
    +
  • 例如在搜索引擎搜索问题的时候,我们当然是希望用户输入完最后一个字才调用查询接口,这个时候适用延迟执行的防抖函数,它总是在一连串(间隔小于wait的)函数触发之后调用。
  • +
  • 例如用户给某系统表单点提交的时候,我们希望用户点第一下的时候就去调用接口,这个情况适用立即执行的防抖函数,它总是在第一次调用,并且下一次调用必须与前一次调用的时间间隔大于wait才会触发。
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// js代码

// 这个是用来获取当前时间戳的
function now() {
return +new Date()
}
/**
* 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {boolean} immediate 设置为ture时,是否立即调用函数
* @return {function} 返回客户调用函数
*/
function debounce (func, wait = 50, immediate = true) {
let timer, context, args

// 延迟执行函数
const later = () => setTimeout(() => {
// 延迟函数执行完毕,清空缓存的定时器序号
timer = null
// 延迟执行的情况下,函数会在延迟函数中执行
// 使用到之前缓存的参数和上下文
if (!immediate) {
func.apply(context, args)
context = args = null
}
}, wait)

// 这里返回的函数是每次实际调用的函数
return function(...params) {
// 如果没有创建延迟执行函数(later),就创建一个
if (!timer) {
timer = later()
// 如果是立即执行,调用函数
// 否则缓存参数和调用上下文
if (immediate) {
func.apply(this, params)
} else {
context = this
args = params
}
// 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
// 这样做延迟函数会重新计时
} else {
clearTimeout(timer)
timer = later()
}
}
}
+

整体函数实现的不难,总结一下。

+
    +
  • 对于按钮防点击来说的实现:如果函数是立即执行的,就立即调用,如果函数是延迟执行的,就缓存上下文和参数,放到延迟函数中去执行。一旦我开始一个定时器,只要我定时器还在,你每次点击我都重新计时。一旦你点累了,定时器时间到,定时器重置为 null,就可以再次点击了。
  • +
  • 对于延时执行函数来说的实现:清除定时器ID,如果是延迟调用就调用函数
  • +
+

节流

防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// js代码

/**
* underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {object} options 如果想忽略开始函数的的调用,传入{leading: false}。
* 如果想忽略结尾函数的调用,传入{trailing: false}
* 两者不能共存,否则函数不能执行
* @return {function} 返回客户调用函数
*/
_.throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
// 之前的时间戳
var previous = 0;
// 如果 options 没传则设为空对象
if (!options) options = {};
// 定时器回调函数
var later = function() {
// 如果设置了 leading,就将 previous 设为 0
// 用于下面函数的第一个 if 判断
previous = options.leading === false ? 0 : _.now();
// 置空一是为了防止内存泄漏,二是为了下面的定时器判断
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
// 获得当前时间戳
var now = _.now();
// 首次进入前者肯定为 true
// 如果需要第一次不执行函数
// 就将上次时间戳设为当前的
// 这样在接下来计算 remaining 的值时会大于0
if (!previous && options.leading === false) previous = now;
// 计算剩余时间
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果当前调用已经大于上次调用时间 + wait
// 或者用户手动调了时间
// 如果设置了 trailing,只会进入这个条件
// 如果没有设置 leading,那么第一次会进入这个条件
// 还有一点,你可能会觉得开启了定时器那么应该不会进入这个 if 条件了
// 其实还是会进入的,因为定时器的延时
// 并不是准确的时间,很可能你设置了2秒
// 但是他需要2.2秒才触发,这时候就会进入这个条件
if (remaining <= 0 || remaining > wait) {
// 如果存在定时器就清理掉否则会调用二次回调
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 判断是否设置了定时器和 trailing
// 没有的话就开启一个定时器
// 并且不能不能同时设置 leading 和 trailing
timeout = setTimeout(later, remaining);
}
return result;
};
};
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 函数防抖和节流

+

文章作者:

+

发布时间:2020年01月03日 - 01:04

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-debounce-throttle/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/03/FE-guide-inherit/index.html b/2020/01/03/FE-guide-inherit/index.html new file mode 100644 index 0000000000..ce49cd9708 --- /dev/null +++ b/2020/01/03/FE-guide-inherit/index.html @@ -0,0 +1,597 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 继承 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 继承 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

继承

在 ES5 中,我们可以使用如下方式解决继承的问题

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码
function Super() {}
Super.prototype.getNumber = function() {
return 1
}

function Sub() {}
let s = new Sub()
Sub.prototype = Object.create(Super.prototype, {
constructor: {
value: Sub,
enumerable: false,
writable: true,
configurable: true
}
})
+

以上继承实现思路就是将子类的原型设置为父类的原型
ES6 中,我们可以通过 class 语法轻松解决这个问题

+
1
2
3
4
5
6
7
8
9
// js代码

class MyDate extends Date {
test() {
return this.getTime()
}
}
let myDate = new MyDate()
myDate.test()
+

但是 ES6 不是所有浏览器都兼容,所以我们需要使用 Babel 来编译这段代码。

+

如果你使用编译过得代码调用 myDate.test() 你会惊奇地发现出现了报错

+

因为在 JS 底层有限制,如果不是由 Date 构造出来的实例的话,是不能调用 Date 里的函数的。所以这也侧面的说明了:ES6 中的 class 继承与 ES5 中的一般继承写法是不同的

+

既然底层限制了实例必须由 Date 构造出来,那么我们可以改变下思路实现继承

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

function MyData() {

}
MyData.prototype.test = function () {
return this.getTime()
}
let d = new Date() // 父类实例
Object.setPrototypeOf(d, MyData.prototype)
Object.setPrototypeOf(MyData.prototype, Date.prototype)
+

以上继承实现思路:先创建父类实例 => 改变实例原先的 __proto__ 转而连接到子类的 prototype => 子类的 prototype__proto__ 改为父类的 prototype

+

通过以上方法实现的继承就可以完美解决 JS 底层的这个限制。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 继承

+

文章作者:

+

发布时间:2020年01月03日 - 01:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-inherit/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/03/FE-guide-store/index.html b/2020/01/03/FE-guide-store/index.html new file mode 100644 index 0000000000..86a400e167 --- /dev/null +++ b/2020/01/03/FE-guide-store/index.html @@ -0,0 +1,628 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 浏览器存储 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 浏览器存储 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

cookie,localStorage,sessionStorage,indexDB

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
特性cookielocalStoragesessionStorageindexDB
数据生命周期一般由服务器生成,可以设置过期时间除非被清理,否则一直存在页面关闭就清理除非被清理,否则一直存在
数据存储大小4K5M5M无限制
与服务端通信每次都会携带在 header 中,对于请求性能影响不参与不参与不参与
+

从上表可以看到, cookie 已经不建议用于存储。如果没有大量数据存储需求的话,可以使用 localStoragesessionStorage 。对于不怎么改变的数据尽量使用 localStorage 存储,否则可以用 sessionStorage 存储。

+

对于 cookie ,我们还需要注意安全性。
| 属性 | 作用 |
| ——— | ————————————————————– |
| value | 如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识 |
| http-only | 不能通过 JS 访问 Cookie,减少 XSS 攻击 |
| secure | 只能在协议为 HTTPS 的请求中携带 |
| same-site | 规定浏览器不能在跨域请求中携带 Cookie,减少 CSRF 攻击 |

+

Service Worker

+

Service workers 本质上充当 Web 应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步 API。

+
+

目前该技术通常用来做缓存文件,提高首屏速度,可以试着来实现这个功能。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// js代码

// index.js
if (navigator.serviceWorker) {
navigator.serviceWorker
.register('sw.js')
.then(function(registration) {
console.log('service worker 注册成功')
})
.catch(function(err) {
console.log('servcie worker 注册失败')
})
}
// sw.js
// 监听 `install` 事件,回调中缓存所需文件
self.addEventListener('install', e => {
e.waitUntil(
caches.open('my-cache').then(function(cache) {
return cache.addAll(['./index.html', './index.js'])
})
)
})

// 拦截所有请求事件
// 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据
self.addEventListener('fetch', e => {
e.respondWith(
caches.match(e.request).then(function(response) {
if (response) {
return response
}
console.log('fetch source')
})
)
})
+ +

打开页面,可以在开发者工具中的 Application 看到 Service Worker 已经启动了
Cache 中也可以发现我们所需的文件已被缓存

+

当我们重新刷新页面可以发现我们缓存的数据是从 Service Worker 中读取的

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 浏览器存储

+

文章作者:

+

发布时间:2020年01月03日 - 04:37

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-store/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/03/why-0.1-plus-0.2-not-equals-0.3/index.html b/2020/01/03/why-0.1-plus-0.2-not-equals-0.3/index.html new file mode 100644 index 0000000000..8dadb20975 --- /dev/null +++ b/2020/01/03/why-0.1-plus-0.2-not-equals-0.3/index.html @@ -0,0 +1,586 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 为什么 0.1 + 0.2 != 0.3 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 为什么 0.1 + 0.2 != 0.3 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Proxy

因为 JS 采用 IEEE 754 双精度版本(64位),并且只要采用 IEEE 754 的语言都有该问题。

+

我们都知道计算机表示十进制是采用二进制表示的,所以 0.1 在二进制表示为

+
1
2
3
4
// js代码

// (0011) 表示循环
0.1 = 2^-4 * 1.10011(0011)
+

那么如何得到这个二进制的呢,我们可以来演算下

+

小数算二进制和整数不同。乘法计算时,只计算小数位,整数位用作每一位的二进制,并且得到的第一位为最高位。所以我们得出 0.1 = 2^-4 * 1.10011(0011),那么 0.2 的演算也基本如上所示,只需要去掉第一步乘法,所以得出 0.2 = 2^-3 * 1.10011(0011)

+

回来继续说 IEEE 754 双精度。六十四位中符号位占一位,整数位占十一位,其余五十二位都为小数位。因为 0.10.2 都是无限循环的二进制了,所以在小数位末尾处需要判断是否进位(就和十进制的四舍五入一样)。

+

所以 2^-4 * 1.10011...001 进位后就变成了 2^-4 * 1.10011(0011 * 12次)010 。那么把这两个二进制加起来会得出 2^-2 * 1.0011(0011 * 11次)0100 , 这个值算成十进制就是 0.30000000000000004

+

下面说一下原生解决办法,如下代码所示

+
1
2
3
// js代码

parseFloat((0.1 + 0.2).toFixed(10))
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:为什么 0.1 + 0.2 != 0.3

+

文章作者:

+

发布时间:2020年01月03日 - 04:10

+

最后更新:2020年01月03日 - 04:10

+

原始链接:https://blog.lifesli.com/2020/01/03/why-0.1-plus-0.2-not-equals-0.3/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/04/Algorithm/index.html b/2020/01/04/Algorithm/index.html new file mode 100644 index 0000000000..3906bcffdc --- /dev/null +++ b/2020/01/04/Algorithm/index.html @@ -0,0 +1,734 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + JavaScript实现经典排序算法 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ JavaScript实现经典排序算法 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

基本概念

时间复杂度

一个算法的时间复杂度反映了程序运行从开始到结束所需要的时间。把算法中基本操作重复执行的次数(频度)作为算法的时间复杂度。
没有循环语句,记作O(1),也称为常数阶。只有一重循环,则算法的基本操作的执行频度与问题规模n呈线性增大关系,记作O(n),也叫线性阶。
常见的时间复杂度有:

+
    +
  • O(1): Constant Complexity: Constant 常数复杂度
  • +
  • O(log n): Logarithmic Complexity: 对数复杂度
  • +
  • O(n): Linear Complexity: 线性时间复杂度
  • +
  • O(n^2): N square Complexity 平⽅方
  • +
  • O(n^3): N square Complexity ⽴立⽅方
  • +
  • O(2^n): Exponential Growth 指数
  • +
  • O(n!): Factorial 阶乘
  • +
+

空间复杂度

一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。

+

一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。

+
    +
  • 稳定
  • +
  • 不稳定

    算法汇总

    十大经典排序.jpg

    关于时间复杂度:

    平方阶 (O(n2)) 排序 各类简单排序:直接插入、直接选择和冒泡排序。
    线性对数阶 (O(nlog2n)) 排序 快速排序、堆排序和归并排序;
    O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数。 希尔排序
    线性阶 (O(n)) 排序 基数排序,此外还有桶、箱排序。
  • +
+

关于稳定性:

稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。

+

名词解释:

    +
  • n:数据规模
  • +
  • k:“桶”的个数
  • +
  • In-place:占用常数内存,不占用额外内存
  • +
  • Out-place:占用额外内存
  • +
  • 稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同
  • +
+

1. 冒泡排序(Bubble Sort) — 前后两两比较 — 气泡

冒泡排序可谓是最经典的排序算法了,它是基于比较的排序算法,其优点是实现简单,排序数量较小时性能较好。

+

它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

+

1. 1 算法原理

相邻的数据进行两两比较,小数放在前面,大数放在后面,如果前面的数据比后面的数据大,就交换这两个数的位置。也可以实现大数放在前面,小数放在后面,如果前面的数据比后面的小,就交换两个的位置。要实现上述规则需要用到两层for循环。

+

1. 2 算法描述

    +
  1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  2. +
  3. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  4. +
  5. 针对所有的元素重复以上的步骤,除了最后一个;
  6. +
  7. 重复步骤1~3,直到排序完成。
  8. +
+

1. 3 动图演示

ldB5VS.gif

+

1. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// js代码

function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len; i++) {
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { // 相邻元素两两对比
// 元素交换
/** 1.使用中间变量 **/
var temp = arr[j + 1];
arr[j + 1] = arr[j]
arr[j] = temp
/** 2.适用纯数字的数组排序 **/
arr[j] = arr[j] + arr[j + 1]
arr[j + 1] = arr[j] - arr[j + 1]
arr[j] -= arr[j + 1]
/** 3.使用es6解构赋值 **/
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
}
}
}
return arr;
}
+

冒泡排序算法优化

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len; i++) {
var exchange=false; // 交换标志
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { // 相邻元素两两对比
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]] // 元素交换
exchange=true; //
}
}
if(!exchange){ // 若本趟排序未发生交换,提前终止算法
break;
}
}
return arr;
}
+

2. 选择排序(Selection Sort) — 面试挑简历,在剩下的里面挑最好的

表现最稳定的排序算法之一,因为无论什么数据进去都是O(n²)的时间复杂度。。。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。

+

2. 1 算法原理

先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

+

2. 2 算法描述

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

+
    +
  1. 初始状态:无序区为R[1..n],有序区为空;
  2. +
  3. 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中
  4. +
  5. 选出关键字最小的记录R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
  6. +
  7. n-1趟结束,数组有序化了。
  8. +
+

2. 3 动图演示

ldDWW9.gif

+

2. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

function selectionSort(arr) {
var len = arr.length;
var minIndex, temp;
for (var i = 0; i < len - 1; i++) {
minIndex = i;
for (var j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) { //寻找最小的数
minIndex = j; //将最小数的索引保存
}
}
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
return arr;
}
+ +

3. 插入排序(Insertion Sort)—– 麻将/扑克

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。

+

3. 1 算法原理

它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

+

3. 2 算法描述

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

+
    +
  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. +
  3. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  4. +
  5. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  6. +
  7. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  8. +
  9. 将新元素插入到该位置后;
  10. +
  11. 重复步骤2~5。
  12. +
+

3. 3 动图演示

ldDfzR.gif

+

3. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// js代码

function insertSort(arr) {
// 从1位置开始遍历arr中每元素,同时声明空变量temp
for (let i = 1; i < arr.length; i++) {
if (arr[i] < arr[i - 1]) { // 如果当前元素<前一个元素
let temp = arr[i] // 将当前元素值临时保存在temp中
let p = i - 1 // 定义变量 p = i- 1
// 循环 条件:
// 1. p>=0且temp小于p位置的元素
while (p >= 0 && temp < arr[p]) {
// 循环体: 将P位置的值赋值给p的后一个元素
arr[p + 1] = arr[p]
p-- // p向前移动一个
}
arr[p + 1] = temp // 将temp的值赋值给p+1位置的元素
}
}
}
+ +

4. 快速排序(Selection Sort)

快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

+

4. 1 算法原理

快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。

+

4. 2 算法描述

选基准:在数据结构中选择一个元素作为基准(pivot
划分区:参照基准元素值的大小,划分无序区,所有小于基准元素的数据放入一个区间,所有大于基准元素的数据放入另一区间,分区操作结束后,基准元素所处的位置就是最终排序后它应该所处的位置
递归:对初次划分出来的两个无序区间,递归调用第 1步和第 2步的算法,直到所有无序区间都只剩下一个元素为止。

+

简单理解就是,选择一个目标值,比目标值小的放左边,比目标值大的放右边,目标值的位置已排好,将左右两侧再进行快排。

+

4. 3 动图演示

快速排序.gif

+

4. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// js代码

function quickSort(arr){
//如果arr.length<=1,则直接返回arr
if(arr.length<=1){return arr}
// arr的元素个数/2,再下去整,将值保存在pivotIndex中
var pivotIndex=Math.floor(arr.length/2);
// 将arr中pivotIndex位置的元素,保存在变量pivot中
var pivot=arr[pivotIndex];
//声明空数组left和right
var left=[];
var right=[];
for(var i=0;i<arr.length;i++){ // 遍历arr中每个元素
if(i !== pivotIndex){ // 如果i !== pivotIndex
if(arr[i]<=pivot){ // 如果当前元素值<pivot
left.push(arr[i]); // 就将当前值压入left
}else{
right.push(arr[i]); // 就将当前值压入right
}
}
}
//递归
return quickSort(left).concat(pivot, quickSort(right)); // 链接多个数组到 left 从小到大
}
+

5. 希尔排序

5. 1 算法原理

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:

+
    +
  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
  • +
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
    希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
  • +
+

5. 2 算法描述

    +
  • 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
  • +
  • 按增量序列个数 k,对序列进行 k 趟排序;
  • +
  • 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
  • +
+

5.3 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// js代码

function shellSort(arr) {
var len = arr.length,
temp,
gap = 1;
while(gap < len/3) { //动态定义间隔序列
gap =gap*3+1;
}
for (gap; gap > 0; gap = Math.floor(gap/3)) {
for (var i = gap; i < len; i++) {
temp = arr[i];
for (var j = i-gap; j >= 0 && arr[j] > temp; j-=gap) {
arr[j+gap] = arr[j];
}
arr[j+gap] = temp;
}
}
return arr;
}
+ +

6. 归并排序

6. 1 算法原理

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

+
    +
  • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
  • +
  • 自下而上的迭代;
    +

    在《数据结构与算法 JavaScript 描述》中,作者给出了自下而上的迭代方法。但是对于递归法,作者却认为:
    However, it is not possible to do so in JavaScript, as the recursion goes too deep for the language to handle.
    然而,在 JavaScript 中这种方式不太可行,因为这个算法的递归深度对它来讲太深了。

    +
    +
  • +
+

说实话,我不太理解这句话。意思是 JavaScript 编译器内存太小,递归太深容易造成内存溢出吗?还望有大神能够指教。

+

和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。

+

6. 2 算法描述

    +
  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
  2. +
  3. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
  4. +
  5. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
  6. +
  7. 重复步骤 3 直到某一指针达到序列尾;
  8. +
  9. 将另一序列剩下的所有元素直接复制到合并序列尾。
  10. +
+

6. 3 动图演示

归并排序.gif

+

6. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// js代码

function mergeSort(arr) { // 采用自上而下的递归方法
var len = arr.length;
if(len < 2) {
return arr;
}
var middle = Math.floor(len / 2),
left = arr.slice(0, middle),
right = arr.slice(middle);
return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right){
var result = [];
while (left.length && right.length) {
if (left[0] <= right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}

while (left.length)
result.push(left.shift());

while (right.length)
result.push(right.shift());

return result;
}
+ +

7. 堆排序

7. 1 算法原理

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

+
    +
  • 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
  • +
  • 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
    堆排序的平均时间复杂度为 Ο(nlogn)。
  • +
+

7. 2 算法描述

    +
  1. 将待排序序列构建成一个堆 H[0……n-1],根据(升序降序需求)选择大顶堆或小顶堆;
  2. +
  3. 把堆首(最大值)和堆尾互换;
  4. +
  5. 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
  6. +
  7. 重复步骤 2,直到堆的尺寸为 1。
  8. +
+

7. 3 动图演示

堆排序.gif

+

7. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// js代码

var len; // 因为声明的多个函数都需要数据长度,所以把len设置成为全局变量

function buildMaxHeap(arr) { // 建立大顶堆
len = arr.length;
for (var i = Math.floor(len/2); i >= 0; i--) {
heapify(arr, i);
}
}

function heapify(arr, i) { // 堆调整
var left = 2 * i + 1,
right = 2 * i + 2,
largest = i;

if (left < len && arr[left] > arr[largest]) {
largest = left;
}

if (right < len && arr[right] > arr[largest]) {
largest = right;
}

if (largest != i) {
swap(arr, i, largest);
heapify(arr, largest);
}
}

function swap(arr, i, j) {
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

function heapSort(arr) {
buildMaxHeap(arr);

for (var i = arr.length-1; i > 0; i--) {
swap(arr, 0, i);
len--;
heapify(arr, 0);
}
return arr;
}
+ +

8. 计数排序

8. 1 算法原理

计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

+

8. 2 算法描述

8. 3 动图演示

计数排序.gif

+

8. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// js代码

function countingSort(arr, maxValue) {
var bucket = new Array(maxValue+1),
sortedIndex = 0;
arrLen = arr.length,
bucketLen = maxValue + 1;

for (var i = 0; i < arrLen; i++) {
if (!bucket[arr[i]]) {
bucket[arr[i]] = 0;
}
bucket[arr[i]]++;
}

for (var j = 0; j < bucketLen; j++) {
while(bucket[j] > 0) {
arr[sortedIndex++] = j;
bucket[j]--;
}
}

return arr;
}
+ +

9. 桶排序

9. 1 算法原理

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

+
    +
  • 在额外空间充足的情况下,尽量增大桶的数量
  • +
  • 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
    同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

    9. 2 算法描述

    1. 什么时候最快

    当输入的数据可以均匀的分配到每一个桶中。

    2. 什么时候最慢

    当输入的数据被分配到了同一个桶中。

    9. 3 动图演示

  • +
+

9. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// js代码

function bucketSort(arr, bucketSize) {
if (arr.length === 0) {
return arr;
}

var i;
var minValue = arr[0];
var maxValue = arr[0];
for (i = 1; i < arr.length; i++) {
if (arr[i] < minValue) {
minValue = arr[i]; // 输入数据的最小值
} else if (arr[i] > maxValue) {
maxValue = arr[i]; // 输入数据的最大值
}
}

//桶的初始化
var DEFAULT_BUCKET_SIZE = 5; // 设置桶的默认数量为5
bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
var bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
var buckets = new Array(bucketCount);
for (i = 0; i < buckets.length; i++) {
buckets[i] = [];
}

//利用映射函数将数据分配到各个桶中
for (i = 0; i < arr.length; i++) {
buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i]);
}

arr.length = 0;
for (i = 0; i < buckets.length; i++) {
insertionSort(buckets[i]); // 对每个桶进行排序,这里使用了插入排序
for (var j = 0; j < buckets[i].length; j++) {
arr.push(buckets[i][j]);
}
}

return arr;
}
+ +

10. 基数排序

10. 1 算法原理

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

+

10. 2 算法描述

1. 基数排序 vs 计数排序 vs 桶排序

基数排序有三种方法:
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异案例看大家发的:

+
    +
  • 基数排序:根据键值的每位数字来分配桶;
  • +
  • 计数排序:每个桶只存储单一键值;
  • +
  • 桶排序:每个桶存储一定范围的数值;

    10. 3 动图演示

  • +
+
    +
  1. LSD 基数排序动图演示
    基数排序.gif

    10. 4 js代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // js代码

    // LSD Radix Sort
    var counter = [];
    function radixSort(arr, maxDigit) {
    var mod = 10;
    var dev = 1;
    for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
    for(var j = 0; j < arr.length; j++) {
    var bucket = parseInt((arr[j] % mod) / dev);
    if(counter[bucket]==null) {
    counter[bucket] = [];
    }
    counter[bucket].push(arr[j]);
    }
    var pos = 0;
    for(var j = 0; j < counter.length; j++) {
    var value = null;
    if(counter[j]!=null) {
    while ((value = counter[j].shift()) != null) {
    arr[pos++] = value;
    }
    }
    }
    }
    return arr;
    }
  2. +
+

总结

排序算法.png
以上就是十大经典算法,算法对于前端来说并不是一个十分熟悉的领域,但是排序算法算是算法里比较入门的,还是需要掌握的,毕竟即使是为了面试也是要准备的。

+

参考资料

https://github.com/hustcc/JS-Sorting-Algorithm
一本关于排序算法的 GitBook 在线书籍 《十大经典排序算法》,多语言实现。

+

http://www.sohu.com/a/136157205_671058
技术面试宝典: 很全面的算法和数据结构知识(含代码实现)下篇

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:JavaScript实现经典排序算法

+

文章作者:

+

发布时间:2020年01月04日 - 07:18

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/04/Algorithm/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/05/FE-guide-ArrayOprs/index.html b/2020/01/05/FE-guide-ArrayOprs/index.html new file mode 100644 index 0000000000..896aaa4fd8 --- /dev/null +++ b/2020/01/05/FE-guide-ArrayOprs/index.html @@ -0,0 +1,617 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 数组常见操作 ---- 去重、扁平、取最大最小值 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 数组常见操作 ---- 去重、扁平、取最大最小值 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

去重

1. 利用 ObjectKey 唯一特性

开辟一个外部存储空间用于标示元素是否出现过。

+
1
2
3
4
5
6
// js代码

const unique = (array)=> {
var container = {};
return array.filter((item, index) => container.hasOwnProperty(item) ? false : (container[item] = true));
}
+ +

2. 利用 indexOf 的返回值数值进行去重

原理是 indexOf 获取元素时如果返回值不等于下标说明已经有了,配合 filter 更美味

+
1
2
3
4
5
// js代码

const unique = arr => arr.filter((e,i) =>
arr.indexOf(e) === i // 如果元素找到的当前下标和当前索引相同说明是同一个,不同说明不是唯一
);
+ +

还有一种变形方法利用 lastIndexOf 方法

+
+

lastIndexOf() 方法返回指定元素(也即有效的 JavaScript 值或变量)在数组中的最后一个的索引,如果不存在则返回 -1。从数组的后面向前查找,从 fromIndex 处开始。

+
+
1
2
3
4
5
// js代码

const filterNonUnique = arr => arr.filter(e =>
arr.indexOf(e) === arr.lastIndexOf(e) // 判断一个元素出现时的第一次下标和最后一次下标是否相同,如果相同那么就唯一
)
+

3. 利用 Set 特性去重

SetES6 中新的数据类型,它的特点就是元素唯一性,且可以和数组进行转换

+
1
2
3
4
5
6
7
// js代码

const unique = arr => Array.from(new Set(arr)); // Array.from 方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。

// 优化

const unique = arr => [...new Set(arr)]; // 利用 `ES6` 中展开操作
+

4. 排序后判断前后两项是否相等去重

通过比较相邻数字是否重复,将排序后的数组进行去重。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

const unique = (array) => {
array.sort((a, b) => a - b);
let pre = 0;
const result = [];
for (let i = 0; i < array.length; i++) {
if (!i || array[i] != array[pre]) {
result.push(array[i]);
}
pre = i;
}
return result;
}
+ +

扁平

1. 普通方法

通过递归的方式判断数组中的项是否是数组,如果不是就加入到新的扁平数组,如果是就递归调用逐层判断,直到全部结束

+
1
2
3
4
5
6
7
8
9
10
11
12
13
// js代码

const flatten = (array) => { // array 原数组
let result = []; // 定义新的扁平数组
for (let i = 0; i < array.length; i++) {
if (Array.isArray(array[i])) { // 判断子元素是否是数组
result = result.concat(flatten(array[i])); // 递归判断
} else {
result.push(array[i]); // 加入新数组
}
}
return result;
}
+ +

2. 使用reduce简化上述方法

+

reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
reducer 函数接收4个参数:

+
+
    +
  • Accumulator (acc) (累计器)
  • +
  • Current Value (cur) (当前值)
  • +
  • Current Index (idx) (当前索引)
  • +
  • Source Array (src) (源数组)
  • +
  • 您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
  • +
+

先看一段 reduce 的示例函数

+
1
2
3
4
5
6
7
8
9
10
11
12
// js代码

const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue; // 定义一个累计器函数,作用是将数组前后累计值与当前值累加

// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer)); // 没有初始值用第一个元素
// expected output: 10

// 5 + 1 + 2 + 3 + 4
console.log(array1.reduce(reducer, 5)); // 有初始值从初始值开始
// expected output: 15
+

这下大家应该对 reduce 函数认识了,接下来看看怎么简化

+
1
2
3
4
5
6
7
8
9
// js代码

function flatten(array) {
return array.reduce((newArray, current) => // 新数组,当前项
Array.isArray(current) ? // 判断当前项是否为数组
newArray.concat(flatten(current)) : // 是的话 递归调用
newArray.concat(current) // 不是的话加进新数组
, []) // 初始化新数组为空
}
+

这里我们再变一个形,增加一个变量,变成可指定深度操作数组

+
1
2
3
4
5
6
7
8
9
10
// js代码

function flattenByDeep(array, deep = 1) { // 默认一层
return array.reduce(
(target, current) =>
Array.isArray(current) && deep > 1 ?
target.concat(flattenByDeep(current, deep - 1)) : // 下一次减一层
target.concat(current)
, [])
}
+

最值

利用 reduce

reduce 函数真的是超级好用,

+
1
2
3
// js代码

array.reduce((c,n) => Math.max(c,n))
+ +

Math.max

Math.max 参数原本是一组数字,只需要让他可以接收数组即可。

+
1
2
3
4
5
// js代码

const array = [3,2,1,4,5];
Math.max.apply(null,array);
Math.max(...array);
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:数组常见操作 ---- 去重、扁平、取最大最小值

+

文章作者:

+

发布时间:2020年01月05日 - 03:05

+

最后更新:2020年01月05日 - 07:47

+

原始链接:https://blog.lifesli.com/2020/01/05/FE-guide-ArrayOprs/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/05/FE-guide-about-reduce/index.html b/2020/01/05/FE-guide-about-reduce/index.html new file mode 100644 index 0000000000..99e7f4a3bc --- /dev/null +++ b/2020/01/05/FE-guide-about-reduce/index.html @@ -0,0 +1,604 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + reduce函数的妙用 ---- 实现map和filter | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ reduce函数的妙用 ---- 实现map和filter +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近看了好多技术文章,好多的代码片段,突然发现好多的代码里都有 reduce ,感觉以前都没怎么关注过这个函数,为了弥补这个过失,打算专门写一篇文章说说它。

+

reduce 函数在 MDN 中是这样介绍的

+
+

reduce() 方法对数组中的每个元素执行一个由 提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。

+
+

说实话看了一脸懵逼,这上面说的叫人话?后来耐着性子看了一些代码后感觉有所理解,如果理解不对,还请斧正。
首先看一下这里面几个关键词

+

* 每个元素: * 这就是遍历咯,没啥好说的
提供的 reducer 函数:我哪有什么 reducer 函数呢,后来理解了,就是回调,有的地方呢叫 handler ,这里跟 reduce 配合就叫了 reducer
升序执行:就是说是0,1,2下标这样的顺序执行啦。
将其结果汇总为单个返回值:最后返回的是一个值,当然没说必须是什么类型。

+

这样解释完了以后,上面这句话就比较好懂了,简单说就是 reduce 里传一个回调函数,执行回调函数方法后返回一个值,就是这样。

+
+

reducer 函数接收4个参数:

+
+
    +
  • Accumulator (acc) (累计器)
  • +
  • Current Value (cur) (当前值)
  • +
  • Current Index (idx) (当前索引)
  • +
  • Source Array (src) (源数组)
    您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
  • +
+

看上面的描述一定要注意,这些参数是 reducer 的参数,不是 reduce 的参数,一共有4个,通常用前两个情况比较多。

+

那么 reduce 函数呢实际上有两个参数,第二个还可以省略 reducerinitialValueinitialValue作为第一次调用 reducer 函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错,这点切记。

+

终于把前置知识讲清楚了,接下来,我们就用 reduce 搞点事情。

+

1. 使用 reduce 实现 map

map 的话我不想再讲一遍,一句话带过,就是对回调函数中的元素进行加工后返回一个长度一样的新数组。
那么该如何实现呢,我们来想想原理吧,刚才上面说过了,reduce执行过后的返回值会作为下一次执行的第一个参数放进去,那么就可以先用一个空数组作为初始值来接受每次需要处理的元素的集合,并返回回去,作为下一次传入的第一个参数,这样每次操作完之后就可以push到这个数组中,那么剩下要做的就是处理数组中每一项的函数了,我们叫他 handler

+
1
2
3
4
5
6
7
8
// js代码

Array.prototype.reduceToMap = function (handler) { // 自定义 `map` 函数 `reduceToMap`
return this.reduce((target, current, index) => { // this指向调用他的数组
target.push(handler.call(this, current, index)) // 这里用了call方法,handler将接受两个参数 current和index
return target; // 处理完成后返回新数组
}, []) // 初始化空的新数组
};
+

就这样我们就用 reduce,实现了 map 的功能,是不是很好用?

+

2. 使用 reduce 实现 filter

filter 也是数组常用的方法,同样传入一个回调函数,处理结果返回true或false,最终 filter 会返回一个过滤后的函数。
学会了上面的 map 的实现,实际上 filter 就会很简单

+
1
2
3
4
5
6
7
8
9
10
// js代码

Array.prototype.reduceToFilter = function (handler) { // 还是自定义方法名
return this.reduce((target, current, index) => {
if (handler.call(this, current, index)) { // 这里注意 handler 要返回的是布尔类型的值
target.push(current); // 符合条件就插入新数组
} // 不符合就什么都不做
return target; // 最后返回新数组
}, []) // 初始化一个空数组
};
+

日后在看到 reduce 的妙用之后还会来补充这篇文章,如果实在懒得写,我也会链接一下新文章,希望大家长期关注。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:reduce函数的妙用 ---- 实现map和filter

+

文章作者:

+

发布时间:2020年01月05日 - 08:55

+

最后更新:2020年01月05日 - 08:53

+

原始链接:https://blog.lifesli.com/2020/01/05/FE-guide-about-reduce/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/05/FE-guide-currying/index.html b/2020/01/05/FE-guide-currying/index.html new file mode 100644 index 0000000000..67b1dfe2e8 --- /dev/null +++ b/2020/01/05/FE-guide-currying/index.html @@ -0,0 +1,595 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 柯里化 currying | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 柯里化 currying +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

定义

柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

+

柯里化,可以理解为提前接收部分参数,延迟执行,不立即输出结果,而是返回一个接受剩余参数的函数。因为这样的特性,也被称为部分计算函数。

+

通俗易懂的解释:用闭包把参数保存起来,当参数的数量足够执行函数了,就开始执行函数。柯里化,是一个逐步接收参数的过程。在接下来的剖析中,你会深刻体会到这一点。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码

function currying(fn){
var allArgs = [];

return function next(){
var args = [].slice.call(arguments); // 拆成数组元素

if(args.length > 0){
allArgs = allArgs.concat(args);
return next;
}else{
return fn.apply(null, allArgs);
}
}
}
+

我们来一个简单的实例验证一下:

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

var add = currying(function(){
var sum = 0;
for(var i = 0; i < arguments.length; i++){
sum += arguments[i];
}
return sum;
});

add(1)(2, 3)(4)() // => 10
+ +

应用场景

参数复用

1
2
3
4
5
6
7
8
// js代码

function getUrl(domain, protocol, path) {
return protocol + "://" + domain + "/" + path;
}

var page1 = getUrl('http', 'lixuguang.github.io', 'page1.html');
var page2 = getUrl('http', 'lixuguang.github.io', 'page2.html');
+

我们使用currying来简化它:

+
1
2
var conardliSite = currying(getUrl)
var page1 = conardliSite('page1.html')('http', 'lixuguang.github.io')();
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 柯里化 currying

+

文章作者:

+

发布时间:2020年01月05日 - 09:59

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/05/FE-guide-currying/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/05/FE-guide-vuepress/index.html b/2020/01/05/FE-guide-vuepress/index.html new file mode 100644 index 0000000000..7786b665aa --- /dev/null +++ b/2020/01/05/FE-guide-vuepress/index.html @@ -0,0 +1,589 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 介绍一个好用的doc展示库 ---- vuepress | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 介绍一个好用的doc展示库 ---- vuepress +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在做公司技术规范,需要能通过 B/S 方式进行分发,起初想法是做一个网站,用 html 的方式进行书写,但是后面感觉太麻烦了,所以就放弃了,偶然间看到有工具可以直接通过 md 文件编译生成网站,就像是 hexo 博客这样,我觉得这挺好,极大的节省了开发网站所需要的时间,只需要专注于内容就好了,所以我就研究了下来,整个了解过程中有这么两个工具映入了我的眼帘,一个是 doctify,另外一个就是今天的主角 vuepress

+ + +

vuepress 何许

+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:介绍一个好用的doc展示库 ---- vuepress

+

文章作者:

+

发布时间:2020年01月05日 - 17:59

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/05/FE-guide-vuepress/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/09/FE-guide-Net/index.html b/2020/01/09/FE-guide-Net/index.html new file mode 100644 index 0000000000..696edf4618 --- /dev/null +++ b/2020/01/09/FE-guide-Net/index.html @@ -0,0 +1,970 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 网络 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 网络 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

网络相关的知识虽然平时在编码阶段很少遇到,但是并不是说它不重要,尤其是前端优化层面,能从网络传输过程优化的点有很多,所以,我们需要好好掌握网络相关的知识,接下来,我将会将我了解到的网络相关的知识点进行一一整理

+

UDP - User Datagram Protocol - 用户数据报协议

面向报文

UDP 是一个面向报文(报文可以理解为一段段的数据)的协议。意思就是 UDP 只是报文的搬运工,不会对报文进行任何拆分和拼接操作。

+

具体来说

+
    +
  • 发送端,应用层将数据传递给传输层的 UDP 协议, UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了
  • +
  • 接收端,网络层将数据传递给传输层, UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作
  • +
+

不可靠性

    +
  1. UDP 是无连接的,也就是说通信不需要建立和断开连接。
  2. +
  3. UDP 也是不可靠的。协议收到什么数据就传递什么数据,并且也不会备份数据,对方能不能收到是不关心的
  4. +
  5. UDP 没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP
  6. +
+

高效

因为 UDP 没有 TCP 那么复杂,需要保证数据不丢失且有序到达。所以 UDP 的头部开销小,只有八字节,相比 TCP 的至少二十字节要少得多,在传输数据报文时是很高效的。

+

头部包含了以下几个数据

+
    +
  • 两个十六位的端口号,分别为源端口(可选字段)和目标端口
  • +
  • 整个数据报文的长度
  • +
  • 整个数据报文的检验和( IPv4 可选 字段),该字段用于发现头部信息和数据中的错误
  • +
+

传输方式

UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。

+

TCP

头部

TCP 头部比 UDP 头部复杂的多

+

对于 TCP 头部来说,以下几个字段是很重要的

+
    +
  • Sequence number(序号),它保证了 TCP 传输的报文都是有序的,对端可以通过序号顺序的拼接报文
  • +
  • Acknowledgement Number(确认编号),它表示数据接收端期望接收的下一个字节的编号是多少,同时也表示上一个序号的数据已经收到
  • +
  • Window Size(窗口大小),它表示还能接收多少字节的数据,用于流量控制
  • +
  • 标识符
      +
    • URG=1:该字段为一表示本数据报的数据部分包含紧急信息,是一个高优先级数据报文,此时紧急指针有效。紧急数据一定位于当前数据包数据部分的最前面,紧急指针标明了紧急数据的尾部。
    • +
    • ACK=1:该字段为一表示确认号字段有效。此外,TCP 还规定在连接建立后传送的所有报文段都必须把 ACK 置为一。
    • +
    • PSH=1:该字段为一表示接收端应该立即将数据 push 给应用层,而不是等到缓冲区满后再提交。
    • +
    • RST=1:该字段为一表示当前 TCP 连接出现严重问题,可能需要重新建立 TCP 连接,也可以用于拒绝非法的报文段和拒绝连接请求。
    • +
    • SYN=1:当SYN=1,ACK=0时,表示当前报文段是一个连接请求报文。当SYN=1,ACK=1时,表示当前报文段是一个同意建立连接的应答报文。
    • +
    • FIN=1:该字段为一表示此报文段是一个释放连接的请求报文。
    • +
    +
  • +
+

状态机

HTTP 是无连接的,所以作为下层的 TCP 协议也是无连接的,虽然看似 TCP 将两端连接了起来,但是其实只是两端共同维护了一个状态

+

TCP 的状态机是很复杂的,并且与建立断开连接时的握手息息相关,接下来就来详细描述下两种握手。
在这之前需要了解一个重要的性能指标 RTT 。该指标表示发送端发送数据到接收到对端数据所需的往返时间。

+

建立连接三次握手

在 TCP 协议中,主动发起请求的一端为客户端,被动连接的一端称为服务端。不管是客户端还是服务端,TCP 连接建立完后都能发送和接收数据,所以 TCP 也是一个全双工的协议。

+

起初,两端都为 CLOSED 状态。在通信开始前,双方都会创建 TCB。 服务器创建完 TCB 后遍进入 LISTEN 状态,此时开始等待客户端发送数据。

+

第一次握手

客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态,x 表示客户端的数据通信初始序号。

+

第二次握手

服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。

+

第三次握手

当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。

+

PS:第三次握手可以包含数据,通过 TCP 快速打开(TFO)技术。其实只要涉及到握手的协议,都可以使用类似 TFO 的方式,客户端和服务端存储相同 cookie,下次握手时发出 cookie 达到减少 RTT 的目的。

+

你是否有疑惑明明两次握手就可以建立起连接,为什么还需要第三次应答?

+

因为这是为了防止失效的连接请求报文段被服务端接收,从而产生错误。

+

可以想象如下场景。客户端发送了一个连接请求 A,但是因为网络原因造成了超时,这时 TCP 会启动超时重传的机制再次发送一个连接请求 B。此时请求顺利到达服务端,服务端应答完就建立了请求。如果连接请求 A 在两端关闭后终于抵达了服务端,那么这时服务端会认为客户端又需要建立 TCP 连接,从而应答了该请求并进入 ESTABLISHED 状态。此时客户端其实是 CLOSED 状态,那么就会导致服务端一直等待,造成资源的浪费。

+

PS:在建立连接中,任意一端掉线,TCP 都会重发 SYN 包,一般会重试五次,在建立连接中可能会遇到 SYN FLOOD 攻击。遇到这种情况你可以选择调低重试次数或者干脆在不能处理的情况下拒绝请求。

+

断开链接四次握手

TCP 是全双工的,在断开连接时两端都需要发送 FIN 和 ACK。

+

第一次握手

若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。

+

第二次握手

B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,表示 A 到 B 的连接已经释放,不接收 A 发的数据了。但是因为 TCP 连接时双向的,所以 B 仍旧可以发送数据给 A。

+

第三次握手

B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。

+

PS:通过延迟确认的技术(通常有时间限制,否则对方会误认为需要重传),可以将第二次和第三次握手合并,延迟 ACK 包的发送。

+

第四次握手

A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。

+

为什么 A 要进入 TIME-WAIT 状态,等待 2MSL 时间后才进入 CLOSED 状态?

+

为了保证 B 能收到 A 的确认应答。若 A 发完确认应答后直接进入 CLOSED 状态,如果确认应答因为网络问题一直没有到达,那么会造成 B 不能正常关闭。

+

ARQ 协议

ARQ 协议也就是超时重传机制。通过确认和超时机制保证了数据的正确送达,ARQ 协议包含停止等待 ARQ 和连续 ARQ

+

停止等待 ARQ

正常传输过程

只要 A 向 B 发送一段报文,都要停止发送并启动一个定时器,等待对端回应,在定时器时间内接收到对端应答就取消定时器并发送下一段报文。

+

报文丢失或出错

在报文传输的过程中可能会出现丢包。这时候超过定时器设定的时间就会再次发送丢包的数据直到对端响应,所以需要每次都备份发送的数据。

+

即使报文正常的传输到对端,也可能出现在传输过程中报文出错的问题。这时候对端会抛弃该报文并等待 A 端重传。

+

PS:一般定时器设定的时间都会大于一个 RTT 的平均时间。

+

ACK 超时或丢失

对端传输的应答也可能出现丢失或超时的情况。那么超过定时器时间 A 端照样会重传报文。这时候 B 端收到相同序号的报文会丢弃该报文并重传应答,直到 A 端发送下一个序号的报文。

+

在超时的情况下也可能出现应答很迟到达,这时 A 端会判断该序号是否已经接收过,如果接收过只需要丢弃应答即可。

+

这个协议的缺点就是传输效率低,在良好的网络环境下每次发送报文都得等待对端的 ACK 。

+

连续 ARQ

在连续 ARQ 中,发送端拥有一个发送窗口,可以在没有收到应答的情况下持续发送窗口内的数据,这样相比停止等待 ARQ 协议来说减少了等待时间,提高了效率。

+

累计确认

连续 ARQ 中,接收端会持续不断收到报文。如果和停止等待 ARQ 中接收一个报文就发送一个应答一样,就太浪费资源了。通过累计确认,可以在收到多个报文以后统一回复一个应答报文。报文中的 ACK 可以用来告诉发送端这个序号之前的数据已经全部接收到了,下次请发送这个序号 + 1的数据。

+

但是累计确认也有一个弊端。在连续接收报文时,可能会遇到接收到序号 5 的报文后,并未接到序号 6 的报文,然而序号 7 以后的报文已经接收。遇到这种情况时,ACK 只能回复 6,这样会造成发送端重复发送数据,这种情况下可以通过 Sack 来解决,这个会在下文说到。

+

滑动窗口

在上面小节中讲到了发送窗口。在 TCP 中,两端都维护着窗口:分别为发送端窗口和接收端窗口。

+

发送端窗口包含已发送但未收到应答的数据和可以发送但是未发送的数据。

+

发送端窗口是由接收窗口剩余大小决定的。接收方会把当前接收窗口的剩余大小写入应答报文,发送端收到应答后根据该值和当前网络拥塞情况设置发送窗口的大小,所以发送窗口的大小是不断变化的。

+

当发送端接收到应答报文后,会随之将窗口进行滑动

+

滑动窗口实现了流量控制。接收方通过报文告知发送方还可以发送多少数据,从而保证接收方能够来得及接收数据。

+

Zero 窗口

在发送报文的过程中,可能会遇到对端出现零窗口的情况。在该情况下,发送端会停止发送数据,并启动 persistent timer 。该定时器会定时发送请求给对端,让对端告知窗口大小。在重试次数超过一定次数后,可能会中断 TCP 链接。

+

拥塞处理

拥塞处理和流量控制不同,后者是作用于接收方,保证接收方来得及接受数据。而前者是作用于网络,防止过多的数据拥塞网络,避免出现网络负载过大的情况。

+

拥塞处理包括了四个算法,分别为:慢开始拥塞避免快速重传快速恢复

+

慢开始算法

慢开始算法,顾名思义,就是在传输开始时将发送窗口慢慢指数级扩大,从而避免一开始就传输大量数据导致网络拥塞。

+

慢开始算法步骤具体如下

+
    +
  1. 连接初始设置拥塞窗口(Congestion Window) 为 1 MSS(一个分段的最大数据量)
  2. +
  3. 每过一个 RTT 就将窗口大小乘二
  4. +
  5. 指数级增长肯定不能没有限制的,所以有一个阈值限制,当窗口大小大于阈值时就会启动拥塞避免算法。
  6. +
+

拥塞避免算法

拥塞避免算法相比简单点,每过一个 RTT 窗口大小只加一,这样能够避免指数级增长导致网络拥塞,慢慢将大小调整到最佳值。

+

在传输过程中可能定时器超时的情况,这时候 TCP 会认为网络拥塞了,会马上进行以下步骤:

+
    +
  • 将阈值设为当前拥塞窗口的一半
  • +
  • 将拥塞窗口设为 1 MSS
  • +
  • 启动拥塞避免算法
  • +
+

快速重传

快速重传一般和快恢复一起出现。一旦接收端收到的报文出现失序的情况,接收端只会回复最后一个顺序正确的报文序号(没有 Sack 的情况下)。如果收到三个重复的 ACK,无需等待定时器超时再重发而是启动快速重传。具体算法分为两种:

+

TCP Taho 实现如下

    +
  • 将阈值设为当前拥塞窗口的一半
  • +
  • 将拥塞窗口设为 1 MSS
  • +
  • 重新开始慢开始算法
  • +
+

TCP Reno 实现如下

    +
  • 拥塞窗口减半
  • +
  • 将阈值设为当前拥塞窗口
  • +
  • 进入快恢复阶段(重发对端需要的包,一旦收到一个新的 ACK 答复就退出该阶段)
  • +
  • 使用拥塞避免算法
  • +
+

TCP New Ren 改进后的快恢复

TCP New Reno 算法改进了之前 TCP Reno 算法的缺陷。在之前,快恢复中只要收到一个新的 ACK 包,就会退出快恢复。

+

在 TCP New Reno 中,TCP 发送方先记下三个重复 ACK 的分段的最大序号。

+

假如我有一个分段数据是 1 ~ 10 这十个序号的报文,其中丢失了序号为 3 和 7 的报文,那么该分段的最大序号就是 10。发送端只会收到 ACK 序号为 3 的应答。这时候重发序号为 3 的报文,接收方顺利接收并会发送 ACK 序号为 7 的应答。这时候 TCP 知道对端是有多个包未收到,会继续发送序号为 7 的报文,接收方顺利接收并会发送 ACK 序号为 11 的应答,这时发送端认为这个分段接收端已经顺利接收,接下来会退出快恢复阶段。

+

HTTP

HTTP 协议是个无状态协议,不会保存状态。

+

PostGet 的区别

先引入副作用幂等的概念。

+
+

副作用指对服务器上的资源做改变,搜索是无副作用的,注册是副作用的。

+
+
+

幂等指发送 M 和 N 次请求(两者不相同且都大于 1),服务器上资源的状态一致,比如注册 10 个和 11 个帐号是不幂等的,对文章进行更改 10 次和 11 次是幂等的。

+
+

在规范的应用场景上说, Get 多用于无副作用,幂等的场景,例如搜索关键字。 Post 多用于副作用,不幂等的场景,例如注册。

+

在技术上说:

+
    +
  • Get 请求能缓存,Post 不能
  • +
  • Post 相对 Get 安全一点点,因为Get 请求都包含在 URL 里,且会被浏览器保存历史纪录,Post 不会,但是在抓包的情况下都是一样的。
  • +
  • Post 可以通过 request body来传输比 Get 更多的数据,Get 没有这个技术
  • +
  • URL有长度限制,会影响 Get 请求,但是这个长度限制是浏览器规定的,不是 RFC 规定的
  • +
  • Post 支持更多的编码类型且不对数据类型限制
  • +
+

常见状态码

2XX 成功

200 OK,表示从客户端发来的请求在服务器端被正确处理
204 No content,表示请求成功,但响应报文不含实体的主体部分
205 Reset Content,表示请求成功,但响应报文不含实体的主体部分,但是与 204 响应不同在于要求请求方重置内容
206 Partial Content,进行范围请求

+

3XX 重定向

301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
302 found,临时性重定向,表示资源临时被分配了新的 URL
303 see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源
304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况
307 temporary redirect,临时重定向,和302含义类似,但是期望客户端保持请求方法不变向新的地址发出请求

+

4XX 客户端错误

400 bad request,请求报文存在语法错误
401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
403 forbidden,表示对请求资源的访问被服务器拒绝
404 not found,表示在服务器上没有找到请求的资源

+

5XX 服务器错误

500 internal sever error,表示服务器端在执行请求时发生了错误
501 Not Implemented,表示服务器不支持当前请求所需要的某个功能
503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求

+

HTTP 首部

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
通用字段作用
Cache-Control控制缓存的行为
Connection浏览器想要优先使用的连接类型,比如 keep-alive
Date创建报文时间
Pragma报文指令
Via代理服务器相关信息
Transfer-Encoding传输编码方式
Upgrade要求客户端升级协议
Warning在内容中可能存在错误
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
请求字段作用
Accept能正确接收的媒体类型
Accept-Charset能正确接收的字符集
Accept-Encoding能正确接收的编码格式列表
Accept-Language能正确接收的语言列表
Expect期待服务端的指定行为
From请求方邮箱地址
Host服务器的域名
If-Match两端资源标记比较
If-Modified-Since本地资源未修改返回 304(比较时间)
If-None-Match本地资源未修改返回 304(比较标记)
User-Agent客户端信息
Max-Forwards限制可被代理及网关转发的次数
Proxy-Authorization向代理服务器发送验证信息
Range请求某个内容的一部分
Referer表示浏览器所访问的前一个页面
TE传输编码方式
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
响应字段作用
Accept-Ranges是否支持某些种类的范围
Age资源在代理缓存中存在的时间
ETag资源标识
Location客户端重定向到某个 URL
Proxy-Authenticate向代理服务器发送验证信息
Server服务器名字
WWW-Authenticate获取资源需要的验证信息
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
实体字段作用
Allow资源的正确请求方式
Content-Encoding内容的编码格式
Content-Language内容使用的语言
Content-Lengthrequest body 长度
Content-Location返回数据的备用地址
Content-MD5Base64加密格式的内容 MD5检验值
Content-Range内容的位置范围
Content-Type内容的媒体类型
Expires内容的过期时间
Last_modified内容的最后修改时间
+

HTTPS

HTTPS 还是通过了 HTTP 来传输信息,但是信息通过 TLS 协议进行了加密。

+

TLS

TLS 协议位于传输层之上,应用层之下。首次进行 TLS 协议传输需要两个 RTT ,接下来可以通过 Session Resumption 减少到一个 RTT

+

TLS 中使用了两种加密技术,分别为:对称加密非对称加密

+

对称加密:
对称加密就是两边拥有相同的秘钥,两边都知道如何将密文加密解密。

+

非对称加密:
有公钥私钥之分,公钥所有人都可以知道,可以将数据用公钥加密,但是将数据解密必须使用私钥解密,私钥只有分发公钥的一方才知道。

+

TLS 握手过程如下图:

+
    +
  1. 客户端发送一个随机值,需要的协议和加密方式
  2. +
  3. 服务端收到客户端的随机值,自己也产生一个随机值,并根据客户端需求的协议和加密方式来使用对应的方式,发送自己的证书(如果需要验证客户端证书需要说明)
  4. +
  5. 客户端收到服务端的证书并验证是否有效,验证通过会再生成一个随机值,通过服务端证书的公钥去加密这个随机值并发送给服务端,如果服务端需要验证客户端证书的话会附带证书
  6. +
  7. 服务端收到加密过的随机值并使用私钥解密获得第三个随机值,这时候两端都拥有了三个随机值,可以通过这三个随机值按照之前约定的加密方式生成密钥,接下来的通信就可以通过该密钥来加密解密
  8. +
+

通过以上步骤可知,在 TLS 握手阶段,两端使用非对称加密的方式来通信,但是因为非对称加密损耗的性能比对称加密大,所以在正式传输数据时,两端使用对称加密的方式通信。

+

PS:以上说明的都是 TLS 1.2 协议的握手情况,在 1.3 协议中,首次建立连接只需要一个 RTT,后面恢复连接不需要 RTT 了。

+

HTTP 2.0

HTTP 2.0 相比于 HTTP 1.X,可以说是大幅度提高了 web 的性能。

+

在 HTTP 1.X 中,为了性能考虑,我们会引入雪碧图、将小图内联、使用多个域名等等的方式。这一切都是因为浏览器限制了同一个域名下的请求数量,当页面中需要请求很多资源的时候,队头阻塞(Head of line blocking)会导致在达到最大请求数量时,剩余的资源需要等待其他资源请求完成后才能发起请求。

+

你可以通过 该链接 感受下 HTTP 2.0 比 HTTP 1.X 到底快了多少。

+

在 HTTP 1.X 中,因为队头阻塞的原因,你会发现请求是这样的
lWJGkt.png
在 HTTP 2.0 中,因为引入了多路复用,你会发现请求是这样的
lWJa6g.png

+

二进制传输

HTTP 2.0 中所有加强性能的核心点在于此。在之前的 HTTP 版本中,我们是通过文本的方式传输数据。在 HTTP 2.0 中引入了新的编码机制,所有传输的数据都会被分割,并采用二进制格式编码。

+

多路复用

在 HTTP 2.0 中,有两个非常重要的概念,分别是帧(frame)流(stream)
帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流。

+

多路复用,就是在一个 TCP 连接中可以存在多条流。换句话说,也就是可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能。

+

Header 压缩

在 HTTP 1.X 中,我们使用文本的形式传输 header,在 header 携带 cookie 的情况下,可能每次都需要重复传输几百到几千的字节。

+

在 HTTP 2.0 中,使用了 HPACK 压缩格式对传输的 header 进行编码,减少了 header 的大小。并在两端维护了索引表,用于记录出现过的 header ,后面在传输过程中就可以传输已经记录过的 header 的键名,对端收到数据后就可以通过键名找到对应的值。

+

服务端 Push

在 HTTP 2.0 中,服务端可以在客户端某个请求后,主动推送其他资源。

+

可以想象以下情况,某些资源客户端是一定会请求的,这时就可以采取服务端 push 的技术,提前给客户端推送必要的资源,这样就可以相对减少一点延迟时间。当然在浏览器兼容的情况下你也可以使用 prefetch 。

+

QUIC

这是一个谷歌出品的基于 UDP 实现的同为传输层的协议,目标很远大,希望替代 TCP 协议。

+
    +
  • 该协议支持多路复用,虽然 HTTP 2.0 也支持多路复用,但是下层仍是 TCP,因为 TCP 的重传机制,只要一个包丢失就得判断丢失包并且重传,导致发生队头阻塞的问题,但是 UDP 没有这个机制
  • +
  • 实现了自己的加密协议,通过类似 TCP 的 TFO 机制可以实现 0-RTT,当然 TLS 1.3 已经实现了 0-RTT 了
  • +
  • 支持重传和纠错机制(向前恢复),在只丢失一个包的情况下不需要重传,使用纠错机制恢复丢失的包
      +
    • 纠错机制:通过异或的方式,算出发出去的数据的异或值并单独发出一个包,服务端在发现有一个包丢失的情况下,通过其他数据包和异或值包算出丢失包
    • +
    • 在丢失两个包或以上的情况就使用重传机制,因为算不出来了
    • +
    +
  • +
+

DNS

DNS 的作用就是通过域名查询到具体的 IP。

+

因为 IP 存在数字和英文的组合(IPv6),很不利于人类记忆,所以就出现了域名。你可以把域名看成是某个 IP 的别名,DNS 就是去查询这个别名的真正名称是什么。

+

在 TCP 握手之前就已经进行了 DNS 查询,这个查询是操作系统自己做的。当你在浏览器中想访问 www.google.com 时,会进行一下操作:

+
    +
  1. 操作系统会首先在本地缓存中查询
  2. +
  3. 没有的话会去系统配置的 DNS 服务器中查询
  4. +
  5. 如果这时候还没得话,会直接去 DNS 根服务器查询,这一步查询会找出负责 com 这个一级域名的服务器
  6. +
  7. 然后去该服务器查询 google 这个二级域名
  8. +
  9. 接下来三级域名的查询其实是我们配置的,你可以给 www 这个域名配置一个 IP,然后还可以给别的三级域名配置一个 IP
  10. +
+

以上介绍的是 DNS 迭代查询,还有种是递归查询,区别就是前者是由客户端去做请求,后者是由系统配置的 DNS 服务器做请求,得到结果后将数据返回给客户端。

+

PS:DNS 是基于 UDP 做的查询。

+

从输入 URL 到页面加载完成的过程

这是一个很经典的面试题,在这题中可以将本文讲得内容都串联起来。

+
    +
  1. 首先做 DNS 查询,如果这一步做了智能 DNS 解析的话,会提供访问速度最快的 IP 地址回来
  2. +
  3. 接下来是 TCP 握手,应用层会下发数据给传输层,这里 TCP 协议会指明两端的端口号,然后下发给网络层。网络层中的 IP 协议会确定 IP 地址,并且指示了数据传输中如何跳转路由器。然后包会再被封装到数据链路层的数据帧结构中,最后就是物理层面的传输了
  4. +
  5. TCP 握手结束后会进行 TLS 握手,然后就开始正式的传输数据
  6. +
  7. 数据在进入服务端之前,可能还会先经过负责负载均衡的服务器,它的作用就是将请求合理的分发到多台服务器上,这时假设服务端会响应一个 HTML 文件
  8. +
  9. 首先浏览器会判断状态码是什么,如果是 200 那就继续解析,如果 400500 的话就会报错,如果 300 的话会进行重定向,这里会有个重定向计数器,避免过多次的重定向,超过次数也会报错
  10. +
  11. 浏览器开始解析文件,如果是 gzip 格式的话会先解压一下,然后通过文件的编码格式知道该如何去解码文件
  12. +
  13. 文件解码成功后会正式开始渲染流程,先会根据 HTML 构建 DOM 树,有 CSS 的话会去构建 CSSOM 树。如果遇到 script 标签的话,会判断是否存在 async 或者 defer ,前者会并行进行下载并执行 JS ,后者会先下载文件,然后等待 HTML 解析完成后顺序执行,如果以上都没有,就会阻塞住渲染流程直到 JS 执行完毕。遇到文件下载的会去下载文件,这里如果使用 HTTP 2.0 协议的话会极大的提高多图的下载效率。
  14. +
  15. 初始的 HTML 被完全加载和解析后会触发 DOMContentLoaded 事件
  16. +
  17. CSSOM 树和 DOM 树构建完成后会开始生成 Render 树,这一步就是确定页面元素的布局、样式等等诸多方面的东西
  18. +
  19. 在生成 Render 树的过程中,浏览器就开始调用 GPU 绘制,合成图层,将内容显示在屏幕上了
  20. +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 网络

+

文章作者:

+

发布时间:2020年01月09日 - 07:35

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/09/FE-guide-Net/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/09/FE-guide-data-structure/index.html b/2020/01/09/FE-guide-data-structure/index.html new file mode 100644 index 0000000000..858c3ac7c1 --- /dev/null +++ b/2020/01/09/FE-guide-data-structure/index.html @@ -0,0 +1,706 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 计算机通识 ---- 数据结构 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 计算机通识 ---- 数据结构 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

网络相关的知识虽然平时在编码阶段很少遇到,但是并不是说它不重要,尤其是前端优化层面,能从网络传输过程优化的点有很多,所以,我们需要好好掌握网络相关的知识,接下来,我将会将我了解到的网络相关的知识点进行一一整理

+

栈 Heap

+

栈是一个线性结构,在计算机中是一个相当常见的数据结构。
栈的特点是只能在某一端添加或删除数据,遵循先进后出(FILO)的原则

+
+

实现

每种数据结构都可以用很多种方式来实现,其实可以把栈看成是数组的一个子集,所以这里使用数组来实现

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// js代码
class Stack {
constructor() {
this.stack = []
}
push(item) {
this.stack.push(item)
}
pop() {
this.stack.pop()
}
peek() { // 取最后一项
return this.stack[this.getCount() - 1]
}
getCount() {
return this.stack.length
}
isEmpty() {
return this.getCount() === 0
}
}
+

应用

选取了 LeetCode 上序号为 20 的题目

+

题意是匹配括号,可以通过栈的特性来完成这道题目

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var isValid = function(str) {
let map = {
'(': -1,
')': 1,
'[': -2,
']': 2,
'{': -3,
'}': 3
}
let stack = [] // 空数组
for (let i = 0; i < str.length; i++) { // 遍历字符串每个字符
if (map[str[i]] < 0) { // 如果是左边括号,入栈
stack.push(str[i])
} else { // 否则出栈,判断左右括号加到一起是不是0
let last = stack.pop()
if (map[last] + map[str[i]] != 0) return false
}
}
if (stack.length > 0) return false // 循环完成后,判断数组中时候还有剩下的,有剩下的说明括号没闭合
return true // 否则没剩下的,都闭合了
}
+ +

队列

+

队列一个线性结构,特点是在某一端添加数据,在另一端删除数据,遵循先进先出(FIFO)的原则。

+
+

实现

这里会讲解两种实现队列的方式,分别是单链队列循环队列

+
    +
  • 链队列即队列的链式存储结构,结构上就是一个单链表,但数据只能是头进尾出。链式结构更加的灵活,特别是在存储空间上,基本不会出现溢出的情况,所以不用像循环队列一样判断队列是否已满,且空间的利用率相对较高。
      +
    • 链队列front指向头结点,头结点不存储数据,rear指向队尾结点。
    • +
    +
  • +
+
    +
  • 循环队列即为头尾相接的队列,它的最大存储空间和顺序队列一样由数组界定,但队列的长度并不一定等同于数组的长度;循环队列的队首和队尾分别由两个指针front、rear标识,于是这样就能做到首尾相接。

    +
  • +
  • 链队列:为操作方便,给链队列添加一个头结点

    +
  • +
  • 循环队列:附设两个指针front和rear分别指示队列头元素及尾元素的位置,每当插入新的队尾元素是,尾指针加1;每当删除队列头元素是,头指针加1

    +
      +
    • 如果用循环队列,则必须设定一个最大队列长度;若无法确定最大长度,则宜采用链队列。
    • +
    +
  • +
+

单链队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// js代码

class Queue {
constructor() {
this.queue = []
}
enQueue(item) {
this.queue.push(item)
}
deQueue() {
return this.queue.shift()
}
getHeader() {
return this.queue[0]
}
getLength() {
return this.queue.length
}
isEmpty() {
return this.getLength() === 0
}
}
+

因为单链队列在出队操作的时候需要 O(n) 的时间复杂度,所以引入了循环队列。
循环队列的出队操作平均是 O(1) 的时间复杂度。

+

循环队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// js代码

class SqQueue {
constructor(length) {
this.queue = new Array(length + 1)
// 队头
this.first = 0
// 队尾
this.last = 0
// 当前队列大小
this.size = 0
}
enQueue(item) {
// 判断队尾 + 1 是否为队头
// 如果是就代表需要扩容数组
// % this.queue.length 是为了防止数组越界
if (this.first === (this.last + 1) % this.queue.length) {
this.resize(this.getLength() * 2 + 1)
}
this.queue[this.last] = item
this.size++
this.last = (this.last + 1) % this.queue.length
}
deQueue() {
if (this.isEmpty()) {
throw Error('Queue is empty')
}
let r = this.queue[this.first]
this.queue[this.first] = null
this.first = (this.first + 1) % this.queue.length
this.size--
// 判断当前队列大小是否过小
// 为了保证不浪费空间,在队列空间等于总长度四分之一时
// 且不为 2 时缩小总长度为当前的一半
if (this.size === this.getLength() / 4 && this.getLength() / 2 !== 0) {
this.resize(this.getLength() / 2)
}
return r
}
getHeader() {
if (this.isEmpty()) {
throw Error('Queue is empty')
}
return this.queue[this.first]
}
getLength() {
return this.queue.length - 1
}
isEmpty() {
return this.first === this.last
}
resize(length) {
let q = new Array(length)
for (let i = 0; i < length; i++) {
q[i] = this.queue[(i + this.first) % this.queue.length]
}
this.queue = q
this.first = 0
this.last = this.size
}
}
+ +

链表

+

链表是一个线性结构,同时也是一个天然的递归结构。链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。

+
+

实现

单向链表

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// js代码

class Node {
constructor(v, next) {
this.value = v
this.next = next
}
}
class LinkList {
constructor() {
// 链表长度
this.size = 0
// 虚拟头部
this.dummyNode = new Node(null, null)
}
find(header, index, currentIndex) {
if (index === currentIndex) return header
return this.find(header.next, index, currentIndex + 1)
}
addNode(v, index) {
this.checkIndex(index)
// 当往链表末尾插入时,prev.next 为空
// 其他情况时,因为要插入节点,所以插入的节点
// 的 next 应该是 prev.next
// 然后设置 prev.next 为插入的节点
let prev = this.find(this.dummyNode, index, 0)
prev.next = new Node(v, prev.next)
this.size++
return prev.next
}
insertNode(v, index) {
return this.addNode(v, index)
}
addToFirst(v) {
return this.addNode(v, 0)
}
addToLast(v) {
return this.addNode(v, this.size)
}
removeNode(index, isLast) {
this.checkIndex(index)
index = isLast ? index - 1 : index
let prev = this.find(this.dummyNode, index, 0)
let node = prev.next
prev.next = node.next
node.next = null
this.size--
return node
}
removeFirstNode() {
return this.removeNode(0)
}
removeLastNode() {
return this.removeNode(this.size, true)
}
checkIndex(index) {
if (index < 0 || index > this.size) throw Error('Index error')
}
getNode(index) {
this.checkIndex(index)
if (this.isEmpty()) return
return this.find(this.dummyNode, index, 0).next
}
isEmpty() {
return this.size === 0
}
getSize() {
return this.size
}
}
+ +

二叉树

树拥有很多种结构,二叉树是树中最常用的结构,同时也是一个天然的递归结构。

+

二叉树拥有一个根节点,每个节点至多拥有两个子节点,分别为:左节点和右节点。树的最底部节点称之为叶节点,当一颗树的叶数量数量为满时,该树可以称之为满二叉树。

+

二分搜索树

二分搜索树也是二叉树,拥有二叉树的特性。但是区别在于二分搜索树每个节点的值都比他的左子树的值大,比右子树的值小。

+

这种存储方式很适合于数据搜索。如下图所示,当需要查找 6 的时候,因为需要查找的值比根节点的值大,所以只需要在根节点的右子树上寻找,大大提高了搜索效率。

+

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// js代码

class Node {
constructor(value) {
this.value = value
this.left = null
this.right = null
}
}
class BST {
constructor() {
this.root = null
this.size = 0
}
getSize() {
return this.size
}
isEmpty() {
return this.size === 0
}
addNode(v) {
this.root = this._addChild(this.root, v)
}
// 添加节点时,需要比较添加的节点值和当前
// 节点值的大小
_addChild(node, v) {
if (!node) {
this.size++
return new Node(v)
}
if (node.value > v) {
node.left = this._addChild(node.left, v)
} else if (node.value < v) {
node.right = this._addChild(node.right, v)
}
return node
}
}
+ +

以上是最基本的二分搜索树实现,接下来实现树的遍历。

+

对于树的遍历来说,有三种遍历方法,分别是先序遍历中序遍历后序遍历

+

三种遍历的区别在于何时访问节点。在遍历树的过程中,每个节点都会遍历三次,分别是遍历到自己,遍历左子树和遍历右子树。如果需要实现先序遍历,那么只需要第一次遍历到节点时进行操作即可。

+

以下都是递归实现.

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// js代码

// 先序遍历可用于打印树的结构
// 先序遍历先访问根节点,然后访问左节点,最后访问右节点。
preTraversal() {
this._pre(this.root)
}
_pre(node) {
if (node) {
console.log(node.value)
this._pre(node.left)
this._pre(node.right)
}
}
// 中序遍历可用于排序
// 对于 BST 来说,中序遍历可以实现一次遍历就
// 得到有序的值
// 中序遍历表示先访问左节点,然后访问根节点,最后访问右节点。
midTraversal() {
this._mid(this.root)
}
_mid(node) {
if (node) {
this._mid(node.left)
console.log(node.value)
this._mid(node.right)
}
}
// 后序遍历可用于先操作子节点
// 再操作父节点的场景
// 后序遍历表示先访问左节点,然后访问右节点,最后访问根节点。
backTraversal() {
this._back(this.root)
}
_back(node) {
if (node) {
this._back(node.left)
this._back(node.right)
console.log(node.value)
}
}
+ +

以上的这几种遍历都可以称之为深度遍历,对应的还有种遍历叫做广度遍历,也就是一层层地遍历树。对于广度遍历来说,我们需要利用之前讲过的队列结构来完成。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

breadthTraversal() {
if (!this.root) return null
let q = new Queue()
// 将根节点入队
q.enQueue(this.root)
// 循环判断队列是否为空,为空
// 代表树遍历完毕
while (!q.isEmpty()) {
// 将队首出队,判断是否有左右子树
// 有的话,就先左后右入队
let n = q.deQueue()
console.log(n.value)
if (n.left) q.enQueue(n.left)
if (n.right) q.enQueue(n.right)
}
}
+

接下来先介绍如何在树中寻找最小值或最大数。因为二分搜索树的特性,所以最小值一定在根节点的最左边,最大值相反

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码

getMin() {
return this._getMin(this.root).value
}
_getMin(node) {
if (!node.left) return node
return this._getMin(node.left)
}
getMax() {
return this._getMax(this.root).value
}
_getMax(node) {
if (!node.right) return node
return this._getMin(node.right)
}
+ +

向上取整和向下取整,这两个操作是相反的,所以代码也是类似的,这里只介绍如何向下取整。既然是向下取整,那么根据二分搜索树的特性,值一定在根节点的左侧。只需要一直遍历左子树直到当前节点的值不再大于等于需要的值,然后判断节点是否还拥有右子树。如果有的话,继续上面的递归判断。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

floor(v) {
let node = this._floor(this.root, v)
return node ? node.value : null
}
_floor(node, v) {
if (!node) return null
if (node.value === v) return v
// 如果当前节点值还比需要的值大,就继续递归
if (node.value > v) {
return this._floor(node.left, v)
}
// 判断当前节点是否拥有右子树
let right = this._floor(node.right, v)
if (right) return right
return node
}
+

排名,这是用于获取给定值的排名或者排名第几的节点的值,这两个操作也是相反的,所以这个只介绍如何获取排名第几的节点的值。对于这个操作而言,我们需要略微的改造点代码,让每个节点拥有一个 size 属性。该属性表示该节点下有多少子节点(包含自身)。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// js代码

class Node {
constructor(value) {
this.value = value
this.left = null
this.right = null
// 修改代码
this.size = 1
}
}
// 新增代码
_getSize(node) {
return node ? node.size : 0
}
_addChild(node, v) {
if (!node) {
return new Node(v)
}
if (node.value > v) {
// 修改代码
node.size++
node.left = this._addChild(node.left, v)
} else if (node.value < v) {
// 修改代码
node.size++
node.right = this._addChild(node.right, v)
}
return node
}
select(k) {
let node = this._select(this.root, k)
return node ? node.value : null
}
_select(node, k) {
if (!node) return null
// 先获取左子树下有几个节点
let size = node.left ? node.left.size : 0
// 判断 size 是否大于 k
// 如果大于 k,代表所需要的节点在左节点
if (size > k) return this._select(node.left, k)
// 如果小于 k,代表所需要的节点在右节点
// 注意这里需要重新计算 k,减去根节点除了右子树的节点数量
if (size < k) return this._select(node.right, k - size - 1)
return node
}
+ +

接下来讲解的是二分搜索树中最难实现的部分:删除节点。因为对于删除节点来说,会存在以下几种情况

+
    +
  • 需要删除的节点没有子树
  • +
  • 需要删除的节点只有一条子树
  • +
  • 需要删除的节点有左右两条树
  • +
+

对于前两种情况很好解决,但是第三种情况就有难度了,所以先来实现相对简单的操作:删除最小节点,对于删除最小节点来说,是不存在第三种情况的,删除最大节点操作是和删除最小节点相反的,所以这里也就不再赘述。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码

delectMin() {
this.root = this._delectMin(this.root)
console.log(this.root)
}
_delectMin(node) {
// 一直递归左子树
// 如果左子树为空,就判断节点是否拥有右子树
// 有右子树的话就把需要删除的节点替换为右子树
if ((node != null) & !node.left) return node.right
node.left = this._delectMin(node.left)
// 最后需要重新维护下节点的 `size`
node.size = this._getSize(node.left) + this._getSize(node.right) + 1
return node
}
+ +

最后讲解的就是如何删除任意节点了。对于这个操作,T.Hibbard 在 1962 年提出了解决这个难题的办法,也就是如何解决第三种情况。

+

当遇到这种情况时,需要取出当前节点的后继节点(也就是当前节点右子树的最小节点)来替换需要删除的节点。然后将需要删除节点的左子树赋值给后继结点,右子树删除后继结点后赋值给他。

+

你如果对于这个解决办法有疑问的话,可以这样考虑。因为二分搜索树的特性,父节点一定比所有左子节点大,比所有右子节点小。那么当需要删除父节点时,势必需要拿出一个比父节点大的节点来替换父节点。这个节点肯定不存在于左子树,必然存在于右子树。然后又需要保持父节点都是比右子节点小的,那么就可以取出右子树中最小的那个节点来替换父节点。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// js代码

delect(v) {
this.root = this._delect(this.root, v)
}
_delect(node, v) {
if (!node) return null
// 寻找的节点比当前节点小,去左子树找
if (node.value < v) {
node.right = this._delect(node.right, v)
} else if (node.value > v) {
// 寻找的节点比当前节点大,去右子树找
node.left = this._delect(node.left, v)
} else {
// 进入这个条件说明已经找到节点
// 先判断节点是否拥有拥有左右子树中的一个
// 是的话,将子树返回出去,这里和 `_delectMin` 的操作一样
if (!node.left) return node.right
if (!node.right) return node.left
// 进入这里,代表节点拥有左右子树
// 先取出当前节点的后继结点,也就是取当前节点右子树的最小值
let min = this._getMin(node.right)
// 取出最小值后,删除最小值
// 然后把删除节点后的子树赋值给最小值节点
min.right = this._delectMin(node.right)
// 左子树不动
min.left = node.left
node = min
}
// 维护 size
node.size = this._getSize(node.left) + this._getSize(node.right) + 1
return node
}
+ +

AVL 树

+

二分搜索树实际在业务中是受到限制的,因为并不是严格的 O(logN),在极端情况下会退化成链表,比如加入一组升序的数字就会造成这种情况。

+
+
+

AVL 树改进了二分搜索树,在 AVL 树中任意节点的左右子树的高度差都不大于 1,这样保证了时间复杂度是严格的 O(logN)。基于此,对 AVL 树增加或删除节点时可能需要旋转树来达到高度的平衡。

+
+

实现

因为 AVL 树是改进了二分搜索树,所以部分代码是于二分搜索树重复的,对于重复内容不作再次解析。

+

对于 AVL 树来说,添加节点会有四种情况
lWB0nf.png

+

对于左左情况来说,新增加的节点位于节点 2 的左侧,这时树已经不平衡,需要旋转。因为搜索树的特性,节点比左节点大,比右节点小,所以旋转以后也要实现这个特性。

+

旋转之前:new < 2 < C < 3 < B < 5 < A,右旋之后节点 3 为根节点,这时候需要将节点 3 的右节点加到节点 5 的左边,最后还需要更新节点的高度。

+

对于右右情况来说,相反于左左情况,所以不再赘述。

+

对于左右情况来说,新增加的节点位于节点 4 的右侧。对于这种情况,需要通过两次旋转来达到目的。

+

首先对节点的左节点左旋,这时树满足左左的情况,再对节点进行一次右旋就可以达到目的。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// js代码

class Node {
constructor(value) {
this.value = value
this.left = null
this.right = null
this.height = 1
}
}

class AVL {
constructor() {
this.root = null
}
addNode(v) {
this.root = this._addChild(this.root, v)
}
_addChild(node, v) {
if (!node) {
return new Node(v)
}
if (node.value > v) {
node.left = this._addChild(node.left, v)
} else if (node.value < v) {
node.right = this._addChild(node.right, v)
} else {
node.value = v
}
node.height =
1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
let factor = this._getBalanceFactor(node)
// 当需要右旋时,根节点的左树一定比右树高度高
if (factor > 1 && this._getBalanceFactor(node.left) >= 0) {
return this._rightRotate(node)
}
// 当需要左旋时,根节点的左树一定比右树高度矮
if (factor < -1 && this._getBalanceFactor(node.right) <= 0) {
return this._leftRotate(node)
}
// 左右情况
// 节点的左树比右树高,且节点的左树的右树比节点的左树的左树高
if (factor > 1 && this._getBalanceFactor(node.left) < 0) {
node.left = this._leftRotate(node.left)
return this._rightRotate(node)
}
// 右左情况
// 节点的左树比右树矮,且节点的右树的右树比节点的右树的左树矮
if (factor < -1 && this._getBalanceFactor(node.right) > 0) {
node.right = this._rightRotate(node.right)
return this._leftRotate(node)
}

return node
}
_getHeight(node) {
if (!node) return 0
return node.height
}
_getBalanceFactor(node) {
return this._getHeight(node.left) - this._getHeight(node.right)
}
// 节点右旋
// 5 2
// / \ / \
// 2 6 ==> 1 5
// / \ / / \
// 1 3 new 3 6
// /
// new
_rightRotate(node) {
// 旋转后新根节点
let newRoot = node.left
// 需要移动的节点
let moveNode = newRoot.right
// 节点 2 的右节点改为节点 5
newRoot.right = node
// 节点 5 左节点改为节点 3
node.left = moveNode
// 更新树的高度
node.height =
1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
newRoot.height =
1 +
Math.max(this._getHeight(newRoot.left), this._getHeight(newRoot.right))

return newRoot
}
// 节点左旋
// 4 6
// / \ / \
// 2 6 ==> 4 7
// / \ / \ \
// 5 7 2 5 new
// \
// new
_leftRotate(node) {
// 旋转后新根节点
let newRoot = node.right
// 需要移动的节点
let moveNode = newRoot.left
// 节点 6 的左节点改为节点 4
newRoot.left = node
// 节点 4 右节点改为节点 5
node.right = moveNode
// 更新树的高度
node.height =
1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
newRoot.height =
1 +
Math.max(this._getHeight(newRoot.left), this._getHeight(newRoot.right))

return newRoot
}
}
+ +

Trie

+

在计算机科学,trie,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。
简单点来说,这个结构的作用大多是为了方便搜索字符串,该树有以下几个特点

+
+
    +
  • 根节点代表空字符串,每个节点都有 N(假如搜索英文字符,就有 26 条) 条链接,每条链接代表一个字符
  • +
  • 节点不存储字符,只有路径才存储,这点和其他的树结构不同
  • +
  • 从根节点开始到任意一个节点,将沿途经过的字符连接起来就是该节点对应的字符串
  • +
+

实现

总得来说 Trie 的实现相比别的树结构来说简单的很多,实现就以搜索英文字符为例。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// js代码

class TrieNode {
constructor() {
// 代表每个字符经过节点的次数
this.path = 0
// 代表到该节点的字符串有几个
this.end = 0
// 链接
this.next = new Array(26).fill(null)
}
}
class Trie {
constructor() {
// 根节点,代表空字符
this.root = new TrieNode()
}
// 插入字符串
insert(str) {
if (!str) return
let node = this.root
for (let i = 0; i < str.length; i++) {
// 获得字符先对应的索引
let index = str[i].charCodeAt() - 'a'.charCodeAt()
// 如果索引对应没有值,就创建
if (!node.next[index]) {
node.next[index] = new TrieNode()
}
node.path += 1
node = node.next[index]
}
node.end += 1
}
// 搜索字符串出现的次数
search(str) {
if (!str) return
let node = this.root
for (let i = 0; i < str.length; i++) {
let index = str[i].charCodeAt() - 'a'.charCodeAt()
// 如果索引对应没有值,代表没有需要搜素的字符串
if (!node.next[index]) {
return 0
}
node = node.next[index]
}
return node.end
}
// 删除字符串
delete(str) {
if (!this.search(str)) return
let node = this.root
for (let i = 0; i < str.length; i++) {
let index = str[i].charCodeAt() - 'a'.charCodeAt()
// 如果索引对应的节点的 Path 为 0,代表经过该节点的字符串
// 已经一个,直接删除即可
if (--node.next[index].path == 0) {
node.next[index] = null
return
}
node = node.next[index]
}
node.end -= 1
}
}
+ +

并查集

+

并查集是一种特殊的树结构,用于处理一些不交集的合并及查询问题。该结构中每个节点都有一个父节点,如果只有当前一个节点,那么该节点的父节点指向自己。
这个结构中有两个重要的操作,分别是:

+
+
    +
  • Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
  • +
  • Union:将两个子集合并成同一个集合。
  • +
+

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// js代码

class DisjointSet {
// 初始化样本
constructor(count) {
// 初始化时,每个节点的父节点都是自己
this.parent = new Array(count)
// 用于记录树的深度,优化搜索复杂度
this.rank = new Array(count)
for (let i = 0; i < count; i++) {
this.parent[i] = i
this.rank[i] = 1
}
}
find(p) {
// 寻找当前节点的父节点是否为自己,不是的话表示还没找到
// 开始进行路径压缩优化
// 假设当前节点父节点为 A
// 将当前节点挂载到 A 节点的父节点上,达到压缩深度的目的
while (p != this.parent[p]) {
this.parent[p] = this.parent[this.parent[p]]
p = this.parent[p]
}
return p
}
isConnected(p, q) {
return this.find(p) === this.find(q)
}
// 合并
union(p, q) {
// 找到两个数字的父节点
let i = this.find(p)
let j = this.find(q)
if (i === j) return
// 判断两棵树的深度,深度小的加到深度大的树下面
// 如果两棵树深度相等,那就无所谓怎么加
if (this.rank[i] < this.rank[j]) {
this.parent[i] = j
} else if (this.rank[i] > this.rank[j]) {
this.parent[j] = i
} else {
this.parent[i] = j
this.rank[j] += 1
}
}
}
+ +

堆通常是一个可以被看做一棵树的数组对象。
堆的实现通过构造二叉堆,实为二叉树的一种。这种数据结构具有以下性质。

+
    +
  • 任意节点小于(或大于)它的所有子节点
  • +
  • 堆总是一棵完全树。即除了最底层,其他层的节点都被元素填满,且最底层从左到右填入。
    将根节点最大的堆叫做最大堆大根堆,根节点最小的堆叫做最小堆小根堆
    优先队列也完全可以用堆来实现,操作是一模一样的。
  • +
+

实现大根堆

堆的每个节点的左边子节点索引是 i * 2 + 1,右边是 i * 2 + 2,父节点是 (i - 1) /2。
堆有两个核心的操作,分别是 shiftUp 和 shiftDown 。前者用于添加元素,后者用于删除根节点。
shiftUp 的核心思路是一路将节点与父节点对比大小,如果比父节点大,就和父节点交换位置。
shiftDown 的核心思路是先将根节点和末尾交换位置,然后移除末尾元素。接下来循环判断父节点和两个子节点的大小,如果子节点大,就把最大的子节点和父节点交换。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// js代码

class MaxHeap {
constructor() {
this.heap = []
}
size() {
return this.heap.length
}
empty() {
return this.size() == 0
}
add(item) {
this.heap.push(item)
this._shiftUp(this.size() - 1)
}
removeMax() {
this._shiftDown(0)
}
getParentIndex(k) {
return parseInt((k - 1) / 2)
}
getLeftIndex(k) {
return k * 2 + 1
}
_shiftUp(k) {
// 如果当前节点比父节点大,就交换
while (this.heap[k] > this.heap[this.getParentIndex(k)]) {
this._swap(k, this.getParentIndex(k))
// 将索引变成父节点
k = this.getParentIndex(k)
}
}
_shiftDown(k) {
// 交换首位并删除末尾
this._swap(k, this.size() - 1)
this.heap.splice(this.size() - 1, 1)
// 判断节点是否有左孩子,因为二叉堆的特性,有右必有左
while (this.getLeftIndex(k) < this.size()) {
let j = this.getLeftIndex(k)
// 判断是否有右孩子,并且右孩子是否大于左孩子
if (j + 1 < this.size() && this.heap[j + 1] > this.heap[j]) j++
// 判断父节点是否已经比子节点都大
if (this.heap[k] >= this.heap[j]) break
this._swap(k, j)
k = j
}
}
_swap(left, right) {
let rightValue = this.heap[right]
this.heap[right] = this.heap[left]
this.heap[left] = rightValue
}
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:计算机通识 ---- 数据结构

+

文章作者:

+

发布时间:2020年01月09日 - 07:35

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/09/FE-guide-data-structure/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/09/career/index.html b/2020/01/09/career/index.html new file mode 100644 index 0000000000..8536fa4d09 --- /dev/null +++ b/2020/01/09/career/index.html @@ -0,0 +1,599 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 如何正确使用时间[转载] | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 如何正确使用时间[转载] +

+ + +
+ + + + +
+ + +

前言

最近各种技术博客,各种技术文章,各种牛人前辈的学习心得都在看,最近看了一个不错的文章叫做《如何正确使用时间》
感觉还是很不错的,特别适合年轻的程序员看一下。下面将全文贴在下方,如果需要也可以去原文查看。

+

原文

+

你是否时常会焦虑时间过的很快,没时间学习,本文将会分享一些个人的见解。

+

花时间补基础,读文档

在工作中我们时常会花很多时间去 debug,但是你是否发现很多问题最终只是你基础不扎实或者文档没有仔细看。

+

基础是你技术的基石,一定要花时间打好基础,而不是追各种新的技术。一旦你的基础扎实,学习各种新的技术也肯定不在话下,因为新的技术,究其根本都是相通的。

+

文档同样也是一门技术的基础。一个优秀的库,开发人员肯定已经把如何使用这个库都写在文档中了,仔细阅读文档一定会是少写 bug 的最省事路子。

+

学会搜索

如果你还在使用百度搜索编程问题,请尽快抛弃这个垃圾搜索引擎。同样一个关键字,使用百度和谷歌,谷歌基本完胜的。即使你使用中文在谷歌中搜索,得到的结果也往往是谷歌占优,所以如果你想迅速的通过搜索引擎来解决问题,那一定是谷歌。

+

学点英语

说到英语,一定是大家所最不想听的。其实我一直认为程序员学习英语是简单的,因为我们工作中是一直接触着英语,并且看懂技术文章,文档所需要的单词量是极少的。我时常在群里看到大家发出一个问题的截图问什么原因,其实在截图中英语已经很明白的说明了问题的所在,如果你的英语过关,完全不需要浪费时间来提问和搜索。所以我认为学点英语也是节省时间中很重要的一点。

+

那么如何去学习呢,chrome 装个翻译插件,直接拿英文文档或文章读,不会的就直接划词翻译,然后记录下这个单词并背诵。每天花半小时看点英文文档和文章,坚持两个月,你的英语水平不说别的,看文档和文章绝对不会有难题了。这一定是一个很划算的个人时间投资,花点时间学习英语,能为你将来的技术之路铺平很多坎。

+

画个图,想一想再做

你是否遇到过这种问题,需求一下来,看一眼,然后马上就按照设计稿开始做了,可能中间出个问题导致你需要返工。

+

如果你存在这样的问题,我很推荐在看到设计稿和需求的时候花点时间想一想,画一画。考虑一下设计稿中是否可以找到可以拆分出来的复用组件,是否存在之前写过的组件。该如何组织这个界面,数据的流转是怎么样的。然后画一下这个页面的需求,最后再动手做。

+

利用好下班时间学习

说到下班时间,那可能就有人说了公司很迟下班,这其实是国内很普遍的情况。但是我认为正常的加班是可以的,但是强制的加班就是在损耗你的身体和前途。

+

可以这么说,大部分的 996 公司,加班的这些时间并不会增加你的技术,无非就是在写一些重复的业务逻辑。也许你可以拿到更多的钱,但是代价是身体还有前途。程序员是靠技术吃饭的,如果你长久呆在一个长时间加班的公司,不能增长你的技术还要吞噬你的下班学习时间,那么你一定会废掉的。如果你遇到了这种情况,只能推荐尽快跳槽到非 996 的公司。

+

那么如果你有足够的下班时间,一定要花上 1, 2 小时去学习,上班大家基本都一样,技术的精进就是看下班以后的那几个小时了。如果你能利用好下班时间来学习,坚持下去,时间一定会给你很好的答复。

+

列好 ToDo

我喜欢规划好一段时间内要做的事情,并且要把事情拆分为小点。给 ToDo 列好优先级,紧急的优先级最高。相同优先级的我喜欢先做简单的,因为这样一旦完成就能划掉一个,提高成就感。

+

反思和整理

每周末都会花上点时间整理下本周记录的笔记和看到的不错文章。然后考虑下本周完成的工作和下周准备要完成的工作。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:如何正确使用时间[转载]

+

文章作者:

+

发布时间:2020年01月09日 - 16:33

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/09/career/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/09/do-it-yourselfery-jsonp/index.html b/2020/01/09/do-it-yourselfery-jsonp/index.html new file mode 100644 index 0000000000..32405f84da --- /dev/null +++ b/2020/01/09/do-it-yourselfery-jsonp/index.html @@ -0,0 +1,590 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- jsonp | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- jsonp +

+ + +
+ + + + +
+ + +

前言

面试中时常会出现需要手动实现某个功能的要求,所以我准备整理一个自己动手实现系列文章,第一个呢就是自己动手实现 jsonp

+

实现 jsonp 思路

    +
  1. 将传入的data数据转化为url字符串形式
  2. +
  3. 处理url中的回调函数
  4. +
  5. 创建一个script标签并插入到页面中
  6. +
  7. 挂载回调函数
  8. +
+

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// js代码

(function (window,document) {
"use strict";
var jsonp = function (url,data,callback) {
// 1.将传入的data数据转化为url字符串形式
// {id:1,name:'jack'} => id=1&name=jack
var dataString = url.indexof('?') == -1? '?': '&';
for(var key in data){
dataString += key + '=' + data[key] + '&';
};

// 2 处理url中的回调函数
// cbFuncName回调函数的名字 :my_json_cb_名字的前缀 + 随机数(把小数点去掉)
var cbFuncName = 'my_json_cb_' + Math.random().toString().replace('.','');
dataString += 'callback=' + cbFuncName;

// 3.创建一个script标签并插入到页面中
var scriptEle = document.createElement('script');
scriptEle.src = url + dataString;

// 4.挂载回调函数
window[cbFuncName] = function (data) {
callback(data); // 处理完回调函数的数据之后,删除jsonp的script标签
document.body.removeChild(scriptEle);
}
document.body.appendChild(scriptEle);
}
window.$jsonp = jsonp;
})(window,document)
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- jsonp

+

文章作者:

+

发布时间:2020年01月09日 - 08:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/09/do-it-yourselfery-jsonp/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/09/solve-get-params-so-long-problem/index.html b/2020/01/09/solve-get-params-so-long-problem/index.html new file mode 100644 index 0000000000..591ba9f894 --- /dev/null +++ b/2020/01/09/solve-get-params-so-long-problem/index.html @@ -0,0 +1,620 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 解决get请求过长的问题小记 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 解决get请求过长的问题小记 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

就在前天,公司里一位新入职的前端小伙伴找到我说遇到了一个问题,自己写的vue代码本地运行是好用的,但是打包后提给后台就访问不了接口了,刚开始我以为是代理配置的有问题,但是经过检查也没什么问题,因为是个get方法,所以我就直接在浏览器地址里敲了api地址,也能正常获取到数据,那么这么看就不是代理或者接口的问题,后来我又看了下调试工具,network中根本就没发出请求,我怀疑是直接前台就拦截了,最后我发现他这个api参数超级长,长的相当吓人,可以看一下下图。
lROUMt.png
我记得url是有长度限制的,于是我就把url变短再次请求,虽然因为参数不全,接口报了错,但是发出去了,由此我定位应该是url超长,因为vue中配置了什么导致的访问被拦截了。于是我就开始了搜索引擎查找解决方案之旅,接下来就把我了解到的信息一一整理。

+ + +

URL 限制

首先我在网上找到了一份资料介绍了URL长度的相关资料,从下面可以看出,从HTTP协议层面以及Get请求层面都没有什么限制,这个限制来自于浏览器或者服务器的限制

+
+

Microsoft Internet Explorer (Browser)
IE浏览器对URL的最大限制为2083个字符,如果超过这个数字,提交按钮没有任何反应。
Firefox (Browser)
对于Firefox浏览器URL的长度限制为65,536个字符。
Safari (Browser)
URL最大长度限制为 80,000个字符。
Opera (Browser)
URL最大长度限制为190,000个字符。
Google (Browser)
URL最大长度限制为8182个字符。
Apache (Server)
能接受最大url长度为8,192个字符。
Microsoft Internet Information Server(IIS)
能接受最大url的长度为16,384个字符。

+
+

而且,中文会进行编码,一个汉字编码后会生成9个字符,这样算来,IE下最多也就能输入231个中文,再多就完蛋了,那么通过get请求传递参数就会显得很麻烦。

+

通常情况下,这种超长参数的请求我们都会用post,有些地方也会说post请求没有长度限制,但是前面说了,实际上HTTP协议层面并没有任何的限制,限制只出现在浏览器或者服务器限制,get和post请求在底层上其实是一样的。

+

最后项目修改了请求类型,把 get 请求改成了 post 请求,在网上实际还找到了另外两个方案,如果对同一组参数频繁访问的化,也可以用 post+get 请求的方式去处理,或者用 sessionStorage 下面简单介绍一下。

+
    +
  1. 将预览内容 post 到服务端,根据一个唯一标识生成缓存(有效时间5分钟),将唯一标识返回到前端,前端通过get方式传递唯一标识请求预览逻辑,拿到缓存的内容后渲染到页面。需要说明的是这里的缓存必须是分布式的。
  2. +
  3. 通过H5的会话缓存 sessionStorage 将预览内容存储在浏览器,打开预览页后从 sessionStorage 中拿到内容就可以渲染出页面了。
  4. +
+

上述两种方案都不太符合我们的项目所以最终还是选择了最简单的方式

+

GET VS POST

    +
  1. 多数浏览器对于POST采用两阶段发送数据的,先发送请求头,再发送请求体,即使参数再少再短,也会被分成两个步骤来发送(相对于GET),也就是第一步发送header数据,第二步再发送body部分。HTTP是应用层的协议,而在传输层有些情况TCP会出现两次连结的过程,HTTP协议本身不保存状态信息,一次请求一次响应。对于TCP而言,通信次数越多反而靠性越低,能在一次连结中传输完需要的消息是最可靠的,尽量使用GET请求来减少网络耗时。如果通信时间增加,这段时间客户端与服务器端一直保持连接状态,在服务器侧负载可能会增加,可靠性会下降。

    +
  2. +
  3. GET请求能够被cache,GET请求能够被保存在浏览器的浏览历史里面(密码等重要数据GET提交,别人查看历史记录,就可以直接看到这些私密数据)POST不进行缓存。

    +
  4. +
  5. GET参数是带在URL后面,传统IE中URL的最大可用长度为2048字符,其他浏览器对URL长度限制实现上有所不同。POST请求无长度限制(目前理论上是这样的)。

    +
  6. +
  7. GET提交的数据大小,不同浏览器的限制不同,一般在2k-8K之间,POST提交数据比较大,大小靠服务器的设定值限制,而且某些数据只能用 POST 方法「携带」,比如 file。

    +
  8. +
  9. 全部用POST不是十分合理,最好先把请求按功能和场景分下类,对数据请求频繁,数据不敏感且数据量在普通浏览器最小限定的2k范围内,这样的情况使用GET。其他地方使用POST。

    +
  10. +
  11. GET 的本质是「得」,而 POST 的本质是「给」。而且,GET 是「幂等」的,在这一点上,GET 被认为是「安全的」。但实际上 server 端也可以用作资源更新,但是这种用法违反了约定,容易造成 CSRF(跨站请求伪造)。

    +
  12. +
+

写在最后

以上是这次遇到问题后学到的一点知识,可能并不全面,后续如果遇到了类似的问题会继续丰富这篇文章。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:解决get请求过长的问题小记

+

文章作者:

+

发布时间:2020年01月09日 - 11:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/09/solve-get-params-so-long-problem/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/13/FE-guide-inherit2/index.html b/2020/01/13/FE-guide-inherit2/index.html new file mode 100644 index 0000000000..fdd8a12fac --- /dev/null +++ b/2020/01/13/FE-guide-inherit2/index.html @@ -0,0 +1,667 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 继承类型 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 继承类型 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

继承的操作需要有一个父类,这里使用构造函数外加原型来创建一个:
有下面两个类,下面实现 Child 继承 Father:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

function Father() {
this.type = 'prople';
}

Father.prototype.eat = function() {
console.log('吃东西啦');
};

function Child(name) {
this.name = name;
this.color = 'black';
}
+ +

原型继承(认贼作父)

+

关键点:子类原型等于父类的实例 Child.prototype = new Person()(将父类指向子类的原型)。

+
+
1
2
// js代码
Child.prototype = new Father();
+ +

特点:
实例可继承的属性有:

+
    +
  • 实例的构造函数的属性
  • +
  • 父类构造函数的属性
  • +
  • 父类原型上的属性
    新实例不会继承父类实例的属性
  • +
+

缺点:

+
    +
  • 新实例无法向父类构造函数传参
  • +
  • 继承单一
  • +
  • 所有新实例都会共享父类实例的属性。— 原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改
  • +
+

构造继承(借腹生子)

+

在子类构造函数中调用父类构造函数

+
+
1
2
3
4
// js代码
function Child(name) {
Father.call(this);
}
+ +

关键点:用 call 或 apply 将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制))Person.call(this, ‘reng’)
特点:

+
    +
  • 只继承了父类构造函数的属性,没有继承父类原型的属性
  • +
  • 解决了原型链继承的注意事项(缺点)1,2,3
  • +
  • 可以继承多个构造函数的属性(call 可以多个)
  • +
  • 在子实例中可以向父实例传参
    缺点:
  • +
  • 只能继承父类构造函数的属性
  • +
  • 无法实现构造函数的复用。(每次用每次都要重新调用)
  • +
  • 每个新实例都有构造函数的副本,臃肿
    (不能继承父类原型,函数在构造函数中,每个子类实例不能共享函数,浪费内存。)
  • +
+

组合继承(原型继承+构造继承)

+

使用构造继承继承父类参数,使用原型继承继承父类函数

+
+
1
2
3
4
5
6
7
// js代码
function Child(name) {
// 构造继承
Father.call(this);
}

Child.prototype = Father.prototype; // Child.prototype = new Person(); // 原型继承
+ +

关键点:结合了两种模式的优点–向父类传参(call)和复用(prototype)
特点:

+
    +
  • 可以继承父类原型上的属性,可以传参,可复用
  • +
  • 每个新实例引入的构造函数属性是私有的
  • +
+

缺点:

+
    +
  • 父类原型和子类原型是同一个对象,无法区分子类真正是由谁构造。
  • +
  • 调用了两次父类的构造函数(耗内存)
  • +
  • 子类的构造函数会代替原型上的那个父类构造函数(call 相当于拿到了父类构造函数的副本)
  • +
+

原型式继承(复制降级)

1
2
3
4
5
6
7
8
9
10
11
// 先封装一个函数容器,用来承载继承的原型和输出对象
function create(obj) {
// 寄生
function F() {}
F.prototype = obj;
return new F();
}
var father = new Father();
var child = create(father);
console.log(child instanceof Father); // true
console.log(child.job); // frontend
+ +

关键点:用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了可以随意增添属性的实例或对象。Object.create()就是这个原理。

+

特点:

+
    +
  • 类似于复制一个对象,用函数来包装
  • +
+

注意事项:

+
    +
  • 所有的实例都会继承原型上的属性
  • +
  • 无法实现复用。(新实例属性都是后面添加的)
    Object.create()方法规范了原型式继承。这个方法接收两个参数,一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。
  • +
+
1
2
3
4
5
6
7
8
9
10
11
// 传一个参数的时候
var child = Object.create(new Father());
console.log(child.job); // frontend
console.log(child instanceof Father); // true
// 传两个参数的时候
var child = Object.create(new Father(), {
name: {
value: 'come on'
}
});
child.sayHello(); // Hello come on
+ +

寄生组合继承

它跟组合继承一样,都比较常用。
寄生:在函数内返回对象然后调用
组合

+
    +
  • 函数的原型等于另一个实例
  • +
  • 在函数中用 apply 或 call 引入另一个构造函数,可传参
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 寄生
function create(obj) {
function F() {}
F.prototype = obj;
return new F();
}
// object是F实例的另一种表示方法
var obj = create(Father.prototype);
// obj实例(F实例)的原型继承了父类函数的原型
// 上述更像是原型链继承,只不过只继承了原型属性

// 组合
function Child() {
// 构造
this.age = 100;
Father.call(this); // 这个继承了父类构造函数的属性
} // 解决了组合式两次调用构造函数属性的特点

// 重点
Child.prototype = obj; // 原型

console.log(Child.prototype.constructor); // Father
obj.constructor = Child; // 一定要修复实例
console.log(Child.prototype.constructor); // Child
var child = new Child();
// Child实例就继承了构造函数属性,父类实例,object的函数属性
console.log(child.job); // frontend
console.log(child instanceof Father); // true
+ +

重点:修复了组合继承的问题

+

在上面的问题中,你可能发现了这么一个注释obj.constructor = Sub; // 一定要修复实例。为什么要修正子类的构造函数的指向呢?

+

因为在不修正这个指向的时候,在获取构造函数返回的时候,在调用同名属性或方法取值上可能造成混乱。比如下面:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Car() {}
Car.prototype.orderOneLikeThis = function() {
// Clone producing function
return new this.constructor();
};
Car.prototype.advertise = function() {
console.log('I am a generic car.');
};

function BMW() {}
BMW.prototype = Object.create(Car.prototype);
BMW.prototype.constructor = BMW; // Resetting the constructor property
BMW.prototype.advertise = function() {
console.log('I am BMW with lots of uber features.');
};

var x5 = new BMW();

var myNewToy = x5.orderOneLikeThis();

myNewToy.advertise(); // => "I am BMW ..." if `BMW.prototype.constructor = BMW;` is not
// commented; "I am a generic car." otherwise.
+ +

object.create 在组合继承的基础上,子类继承一个由父类原型生成的空对象。

+
1
2
3
4
5
6
7
8
9
10
// js代码
function Child(name) {
Father.call(this);
}

Child.prototype = Object.create(Father.prototype, {
constructor: {
value: Child
}
});
+ +

inherits 函数 — Nodejs util.inherits 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// js代码

function inherits = function(ctor, superCtor) {
ctor.super_ = superCtor; // super_属性是子类继承父类时构造函数要写入的一个属性值.
ctor.prototype = Object.create(superCtor.prototype, {
constructor: {
value: ctor,
enumerable: false,
writable: true,
configurable: true
}
});
};
// 在上面的代码中 ctor 想要继承 superCtor ,我们姑且把 ctor 称作子类, superCtor 称作父类.

// 使用
function Child() {
Father.call(this);
//...
}
inherits(Child, Father);

Child.prototype.fun = ...
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 继承类型

+

文章作者:

+

发布时间:2020年01月13日 - 03:18

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/13/FE-guide-inherit2/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/13/Javascript-Design-Pattern/index.html b/2020/01/13/Javascript-Design-Pattern/index.html new file mode 100644 index 0000000000..925eafc01a --- /dev/null +++ b/2020/01/13/Javascript-Design-Pattern/index.html @@ -0,0 +1,665 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + JavaScript 设计模式 Design Pattern | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ JavaScript 设计模式 Design Pattern +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在整理自己的知识体系,一方面为了夯实基础,另一方面也为了查缺补漏、重新理解,设计模式之前看过大概三四次,从最开始一点都不懂,到后面看了几遍以后慢慢理解,今天再次整理一下,希望这一遍下来之后能更加透彻的理解什么是设计模式。

+ + +

什么是设计模式

+

设计模式的定义:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案
设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。

+
+
+

  使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的;设计模式使代码编制真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。 —- 百度百科

+
+

不知道大家看了上面的定义以后是什么感受,说实话我第一次看到这句话并没有什么深刻的认识,什么面向对象的软件设计,什么针对特定问题,什么优雅的解决方案,这都说的是什么,后来我看了几遍之后,上面这句话用我的理解翻译如下:

+
+

软件开发过程中,解决某一类问题用到的一系列套路

+
+

这就是我对设计模式的认识。当然这也不仅仅是我自己的认识,在跟其他的一些开发人员交流时,很多人都是这么认为的。

+

这些解决问题的方案实在是太好用了,所以大神就把它们抽象出来,然后起了个名字-就叫做设计模式了。

+

这么说大家可能还是不太明白,举个开发过程中可能遇到的实际例子吧。

+
+

当系统中某个接口的结构已经无法满足我们现在的业务需求,但又不能改动这个接口,因为可能原来的系统很多功能都依赖于这个接口,改动接口会牵扯到太多文件。那么这种场景下我们该如何解决这个问题呢?通常我们需要新增一个接口,兼容原来的接口和新的业务需求参数。
因此应对这种场景,我们可以很快地想到可以用适配器模式来解决这个问题。

+
+

这就是设计模式的应用,实际上也许你还不知道设计模式这个词,但是你已经在工作中频繁的用到了设计模式,下面我们就来看看到底有哪些设计模式。

+

哦,对了,设计模式并不依赖于语言,它本身更像是一种软件的设计思想,因为我是一个前端,所以接下来具体实现的时候我会使用js来实现设计模式的用法。

+

学习设计模式

目前被普遍接受的经典的设计模式共有 23 种,而这23种设计模式又分为了 3大类 ,看过一张图这里拿过来镇贴。
lHgD4H.jpg
他们分别是

+
    +
  • 创建型模式
  • +
  • 结构型模式
  • +
  • 行为型模式
  • +
+

接下来,我将会将这23种,3大类设计模式一个个的拆解开来,跟大家一起学习一下,设计模式有哪些内容。

+

创建型模式 6个

这类模式用于对象的生成生命周期的管理
创建型模式可以决定生成哪些对象,提高了程序的灵活性。具体属于此类的模式清单如下,共有 5 个:

+
    +
  • 单例模式(Singleton)
  • +
  • 工厂方法模式(Factory Method)
  • +
  • 抽象工厂模式(Abstract Factory)
  • +
  • 建造者模式(Builder)
  • +
  • 原型模式(Prototype)
  • +
  • 迭代器模式(Iterator)
  • +
+

单例模式(Singleton)

描述:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

+

工厂方法模式(Factory Method)

描述:定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。

+

抽象工厂模式(Abstract Factory)

描述:为创建一组相关或相互依赖的对象提供一个接口,而且无需指定它们的具体类

+

建造者模式(Builder)

描述:将一个复杂对象的构建与他的表示分离,使得同样的构建过程可以创建不同的表示。

+

原型模式(Prototype)

描述:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

+

迭代器模式(Iterator)

描述:提供一种方法访问一个容器对象中的各个元素,而又不需暴露该对象的内部细节。

+

结构型模式 7个

    +
  • 适配器模式(Adapter)
  • +
  • 组合模式(Compositor)
  • +
  • 代理模式(Proxy)
  • +
  • 桥梁模式(Bridge)
  • +
  • 装饰模式(Decorator)
  • +
  • 门面模式(Facade)
  • +
  • 享元模式(Flyweight)
  • +
+

适配器模式(Adapter)

描述:将一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。

+

组合模式(Compositor)

描述:将对象组合成树形结构

+

代理模式(Proxy)

描述:

+

桥梁模式(Bridge)

描述:

+

装饰模式(Decorator)

描述:

+

门面模式(Facade)

描述:

+

享元模式(Flyweight)

描述:

+

行为型模式 10个

    +
  • 命名模式(Command)
  • +
  • 解释器模式(Interpreter)
  • +
  • 责任链模式(Chian of Responsibility)
  • +
  • 观察者模式(Observer)
  • +
  • 中介者模式(Mediator)
  • +
  • 备忘录模式(Memento)
  • +
  • 状态模式(State)
  • +
  • 策略模式(Strategy)
  • +
  • 模板方法模式(Template Method)
  • +
  • 访问者模式(Visitor)
  • +
+

命名模式(Command)

描述:

+

解释器模式(Interpreter)

描述:

+

责任链模式(Chian of Responsibility)

描述:

+

观察者模式(Observer)

描述:

+

中介者模式(Mediator)

描述:

+

备忘录模式(Memento)

描述:

+

状态模式(State)

描述:

+

策略模式(Strategy)

描述:

+

模板方法模式(Template Method)

描述:

+

访问者模式(Visitor)

描述:

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:JavaScript 设计模式 Design Pattern

+

文章作者:

+

发布时间:2020年01月13日 - 12:22

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/13/Javascript-Design-Pattern/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/13/do-it-yourselfery-promise/index.html b/2020/01/13/do-it-yourselfery-promise/index.html new file mode 100644 index 0000000000..afba6dd3a1 --- /dev/null +++ b/2020/01/13/do-it-yourselfery-promise/index.html @@ -0,0 +1,623 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- promise | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- promise +

+ + +
+ + + + +
+ + +

前言

面试中时常会出现需要手动实现某个功能的要求,所以我准备整理一个自己动手实现系列文章,这一次呢就是自己动手实现 promise

+

实现 promise 思路

基础步骤

+
    +
  1. 设定三个状态 PENDINGFULFILLEDREJECTED ,只能由 PENDING 改变为 FULFILLEDREJECTED ,并且只能改变一次
  2. +
  3. MyPromise 接收一个函数 executorexecutor 有两个参数 resolve 方法和 reject 方法
  4. +
  5. resolvePENDING 改变为 FULFILLED
  6. +
  7. rejectPENDING 改变为 FULFILLED
  8. +
  9. promise 变为 FULFILLED 状态后具有一个唯一的 value
  10. +
  11. promise 变为 REJECTED 状态后具有一个唯一的 reason
  12. +
+

** then 方法**

+
    +
  1. then 方法接受两个参数 onFulfilledonRejected ,它们分别在状态由 PENDING 改变为 FULFILLEDREJECTED 后调用
  2. +
  3. 一个 promise 可绑定多个 then 方法
  4. +
  5. then 方法可以同步调用也可以异步调用
  6. +
  7. 同步调用:状态已经改变,直接调用 onFulfilled 方法
  8. +
  9. 异步调用:状态还是 PENDING ,将 onFulfilledonRejected 分别加入两个函数数组 onFulfilledCallbacksonRejectedCallbacks ,当异步调用 resolvereject 时,将两个数组中绑定的事件循环执行。
  10. +
+

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// js代码

// 定义状态常量
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

/**
* 定义MyPromise模拟Promise
* @param {func} executor 接收函数
*/
function MyPromise(executor) {
this.state = PENDING; // 默认状态为 pending
this.value = null;
this.reason = null;

// 定义成功失败的函数数组
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];

// 定义成功回调
const resolve = (value) => {
if (this.state === PENDING) {
this.state = FULFILLED;
this.value = value;

this.onFulfilledCallbacks.forEach(func => {
func();
});
}
}

// 定义失败回调
const reject = (reason) => {
if (this.state === PENDING) {
this.state = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach(func => {
func();
});
}
}

try {
executor(resolve, reject);
} catch (reason) {
reject(reason);
}
}

MyPromise.prototype.then = function (onFulfilled, onRejected) {
switch (this.state) {
case FULFILLED:
onFulfilled(this.value);
break;
case REJECTED:
onFulfilled(this.value);
break;
case PENDING:
this.onFulfilledCallbacks.push(() => {
onFulfilled(this.value);
})
this.onRejectedCallbacks.push(() => {
onRejected(this.reason);
})
break;
}
+

then方法异步调用

如下面的代码:输入顺序是:1、2、ConardLi

+
1
2
3
4
5
6
7
8
9
10
11
12
13
// js代码

console.log(1);

let promise = new Promise((resolve, reject) => {
resolve('ConardLi');
});

promise.then((value) => {
console.log(value);
});

console.log(2);
+

虽然 resolve 是同步执行的,我们必须保证 then 是异步调用的,我们用 setTimeout 来模拟异步调用(并不能实现微任务和宏任务的执行机制,只是保证异步调用)

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

// js代码
MyPromise.prototype.then = function (onFulfilled, onRejected) {
if (typeof onFulfilled != 'function') {
onFulfilled = function (value) {
return value;
}
}
if (typeof onRejected != 'function') {
onRejected = function (reason) {
throw reason;
}
}
switch (this.state) {
case FULFILLED:
setTimeout(() => {
onFulfilled(this.value);
}, 0);
break;
case REJECTED:
setTimeout(() => {
onRejected(this.reason);
}, 0);
break;
case PENDING:
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
onFulfilled(this.value);
}, 0);
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
onRejected(this.reason);
}, 0);
})
break;
}
}
+

then方法链式调用

保证链式调用,即 then 方法中要返回一个新的 promise ,并将 then 方法的返回值进行 resolve

+

注意:这种实现并不能保证 then 方法中返回一个新的 promise ,只能保证链式调用

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// js代码
MyPromise.prototype.then = function (onFulfilled, onRejected) {
if (typeof onFulfilled != 'function') {
onFulfilled = function (value) {
return value;
}
}
if (typeof onRejected != 'function') {
onRejected = function (reason) {
throw reason;
}
}

// 创建一个新的MyPromise对象
const promise2 = new MyPromise((resolve, reject) => {
switch (this.state) {
case FULFILLED:
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
break;
case REJECTED:
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
break;
case PENDING:
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
})
break;
}
})
return promise2;
}
+

catch方法

若上面没有定义 reject 方法,所有的异常会走向 catch 方法:

+
1
2
3
4
5
// js代码

MyPromise.prototype.catch = function(onRejected) {
return this.then(null, onRejected);
};
+

finally方法

不管是 resolve 还是 reject 都会调用 finally

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

MyPromise.prototype.finally = function(fn) {
return this.then(value => {
fn();
return value;
}, reason => {
fn();
throw reason;
});
};
+

Promise.resolve

Promise.resolve 用来生成一个直接处于 FULFILLED 状态的 Promise

+
1
2
3
4
5
6
// js代码
MyPromise.reject = function(value) {
return new MyPromise((resolve, reject) => {
resolve(value);
});
};
+

Promise.reject

Promise.reject 用来生成一个直接处于 REJECTED 状态的 Promise

+
1
2
3
4
5
6
// js代码
MyPromise.reject = function(reason) {
return new MyPromise((resolve, reject) => {
reject(reason);
});
};
+

all方法

接受一个 promise 数组,当所有 promise 状态 resolve 后,执行 resolve

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// js代码
MyPromise.all = function (promises) {
return new Promise((resolve, reject) => {
if (promises.length === 0) {
resolve([]);
} else {
let result = [];
let index = 0;
for (let i = 0; i < promises.length; i++) {
promises[i].then(data => {
result[i] = data;
if (++index === promises.length) {
resolve(result);
}
}, err => {
reject(err);
return;
});
}
}
});
}
+

race方法

接受一个 promise 数组,当有一个 promise 状态 resolve 后,执行 resolve

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码
MyPromise.race = function (promises) {
return new Promise((resolve, reject) => {
if (promises.length === 0) {
resolve();
} else {
let index = 0;
for (let i = 0; i < promises.length; i++) {
promises[i].then(data => {
resolve(data);
}, err => {
reject(err);
return;
});
}
}
});
}
+ +

最后

如此一个自定义的 promise 就实现了,怎么样学回来吗?

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- promise

+

文章作者:

+

发布时间:2020年01月13日 - 00:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/13/do-it-yourselfery-promise/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/13/hexo-search/index.html b/2020/01/13/hexo-search/index.html new file mode 100644 index 0000000000..a17bb7ca30 --- /dev/null +++ b/2020/01/13/hexo-search/index.html @@ -0,0 +1,596 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 给我的Hexo博客添加文章内容搜索功能 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 给我的Hexo博客添加文章内容搜索功能 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

今年我下定决心一定要好好地写博客,完善博客的内容,所以最近把原来记在各种笔记中的文章内容都一一转移到了博客,内容一多想找一篇文章时就变得很麻烦,我得去归档中或者分类中一篇篇的找,所以我觉得是时候该给我的博客添加搜索的功能了,我看了博客的config文件,其中并没有搜索相关的配置,我又看了主题的配置文件,这下让我找到了,有个local_search的选项,于是我就开始了博客搜索的研究。

+ +

主题里的搜索配置

这段代码是这样的,实际上我只需要把 enablefalse 变成 true 就好了

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# Local Search
# Dependencies: https://github.com/theme-next/hexo-generator-searchdb
local_search:
enable: true
# If auto, trigger search by changing input.
# If manual, trigger search by pressing enter key or search button.
trigger: auto
# Show top n results per article, show all results by setting to -1
top_n_per_article: 1
# Unescape html strings to the readable one.
unescape: false
# Preload the search data when the page loads.
preload: false
+

然后我又看了一下上面提供的依赖地址,这里还需要做两步,一个是安装搜索的依赖

+
1
npm install hexo-generator-searchdb
+

接着就是在博客系统的配置最下方加入下面这段话

+
1
2
3
4
5
search:
path: search.xml
field: post
content: true
format: html
+

到这里如果没什么问题,那么搜索功能就加上了,怎么样简单吧。如果你遇到什么问题,可以到上面的地址看一下,上面有详细的说明,我这里就不贴代码了。

+

最后

希望大家都能丰富自己的技术博客,拥有属于自己的一片技术天地。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:给我的Hexo博客添加文章内容搜索功能

+

文章作者:

+

发布时间:2020年01月13日 - 14:50

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/13/hexo-search/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/22/Es6-arrowFunc-arguments/index.html b/2020/01/22/Es6-arrowFunc-arguments/index.html new file mode 100644 index 0000000000..0d2143798c --- /dev/null +++ b/2020/01/22/Es6-arrowFunc-arguments/index.html @@ -0,0 +1,583 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + es6中箭头函数没了arguments怎么办? | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ es6中箭头函数没了arguments怎么办? +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

es6中,arguments被取消掉了,但是函数还是会有参数不确定的时候,那么我们该怎么办呢,接下来将介绍一下如何在es6中完成arguments一样的效果。

+ + +

剩余运算符

1
2
3
4
5
6
7
8
// js代码

let func = (...rest) => {
console.log(rest)
//[1,2,3]
}

func(1,2,3)
+ +

看上面的代码,有的朋友会问,这...的操作不应该是展开运算符么?是的,扩展运算符与剩余操作符都是以三点开头的操作符,二者长的很像,只是在用法上有些差别。它们已经被 ES6 数组支持,能解决很多之前 arguments 解决起来很麻烦的问题。

+

简单来说剩余运算是在参数上使用的。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:es6中箭头函数没了arguments怎么办?

+

文章作者:

+

发布时间:2020年01月22日 - 06:50

+

最后更新:2020年01月22日 - 07:34

+

原始链接:https://blog.lifesli.com/2020/01/22/Es6-arrowFunc-arguments/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/22/FE-guide-safe/index.html b/2020/01/22/FE-guide-safe/index.html new file mode 100644 index 0000000000..e116aae535 --- /dev/null +++ b/2020/01/22/FE-guide-safe/index.html @@ -0,0 +1,635 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 网络安全 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 网络安全 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

网络安全是前端不可忽略的一个部分,可惜的是之前我都忽略了,最近发现网络安全已经变得日益重要,所以特别整理一篇文章说说网络安全相关的内容。

+ +

XSS(Cross-site scripting)跨站脚本攻击

跨站脚本攻击是一种网站应用程序的安全漏洞攻击,是代码注入的一种。它允许恶意用户将代码注入到网页上,其他用户在观看网页时就会受到影响。这类攻击通常包含了HTML以及用户端脚本语言。

+

XSS攻击通常指的是通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。这些恶意网页程序通常是JavaScript,但实际上也可以包括Java,VBScript,ActiveX,Flash或者甚至是普通的HTML。攻击成功后,攻击者可能得到更高的权限(如执行一些操作)、私密网页内容、会话和cookie等各种内容。

+

XSS攻击分成两类:

+
    +
  • 来自内部的攻击
      +
    • 主要指的是利用程序自身的漏洞,构造跨站语句
    • +
    +
  • +
  • 来自外部的攻击
      +
    • 主要指自己构造XSS跨站漏洞网页或者寻找非目标机以外的有跨站漏洞的网页。如当我们要渗透一个站点,我们自己构造一个有跨站漏洞的网页,然后构造跨站语句,通过结合其他技术,如社会工程学等,欺骗目标服务器的管理员打开。
    • +
    +
  • +
+

XSS分为:存储型反射型

+
    +
  • 存储型XSS:存储型XSS,持久化,代码是存储在服务其中的,如在个人信息或发表文章等地方,加入代码,如果没有过滤或过滤不严,那么这些代码将储存到服务器中,用户访问该页面的时候触发代码执行。这种XSS比较危险,容易造成蠕虫,盗窃cookie(虽然还有种DOM型XSS,但是也还是包括在存储型XSS内)。
  • +
  • 反射型XSS:非持久化,需要欺骗用户自己去点击链接才能触发XSS代码(服务器中没有这样的页面和内容),一般容易出现在搜索页面。
  • +
+

攻击手段和目的

攻击者使被攻击者在浏览器中执行脚本后,如果需要收集来自被攻击者的数据(如cookie或其他敏感信息),可以自行架设一个网站,让被攻击者通过JavaScript等方式把收集好的数据作为参数提交,随后以数据库等形式记录在攻击者自己的服务器上。

+

常用的XSS攻击手段和目的有:

+
    +
  • 盗用cookie,获取敏感信息。
  • +
  • 利用植入Flash,通过crossdomain权限设置进一步获取更高权限;或者利用Java等得到类似的操作。
  • +
  • 利用iframe、frame、XMLHttpRequest或上述Flash等方式,以(被攻击)用户的身份执行一些管理动作,或执行一些一般的如发微博、加好友、发私信等操作。
  • +
  • 利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动。
  • +
  • 在访问量极大的一些页面上的XSS可以攻击一些小型网站,实现DDoS攻击的效果。
  • +
+

漏洞的防御和利用

过滤特殊字符

避免XSS的方法之一主要是将用户所提供的内容进行过滤,许多语言都有提供对HTML的过滤:

+
    +
  • PHP的htmlentities()或是htmlspecialchars()。
  • +
  • Python的cgi.escape()。
  • +
  • ASP的Server.HTMLEncode()。
  • +
  • ASP.NET的Server.HtmlEncode()或功能更强的Microsoft Anti-Cross Site Scripting Library
  • +
  • Java的xssprotect (Open Source Library)。
  • +
  • NodeJS的node-validator。
  • +
+

使用HTTP头指定类型

很多时候可以使用HTTP头指定内容的类型,使得输出的内容避免被作为HTML解析。如在PHP语言中使用以下代码:
<?php header('Content-Type: text/javascript; charset=utf-8'); ?>
即可强行指定输出内容为文本/JavaScript脚本(顺便指定了内容编码),而非可以引发攻击的HTML。

+

用户方面

包括Internet Explorer、Mozilla Firefox在内的大多数浏览器皆有关闭JavaScript的选项,但关闭功能并非是最好的方法,因为许多网站都需要使用JavaScript语言才能正常运作。通常来说,一个经常有安全更新推出的浏览器,在使用上会比很久都没有更新的浏览器更为安全。

+

CRSF(Cross-site request forgery)跨站请求伪造

跨站请求伪造是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

+
    +
  • XSS 利用的是用户对指定网站的信任
  • +
  • CSRF 利用的是网站对用户网页浏览器的信任
  • +
+

跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。

+

例子

例子
假如一家银行用以运行转账操作的URL地址如下:
http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName
那么,一个恶意攻击者可以在另一个网站上放置如下代码:
<img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">
如果有账户名为Alice的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失1000资金。
这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险
透过例子能够看出,攻击者并不能通过CSRF攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义运行操作。

+

防御措施

检查Referer(参照)字段

HTTP头中有一个Referer字段,这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer字段应和请求的地址位于同一域名下。以上文银行操作为例,Referer字段地址通常应该是转账按钮所在的网页地址,应该也位于www.examplebank.com之下。而如果是CSRF攻击传来的请求,Referer字段会是包含恶意网址的地址,不会位于www.examplebank.com之下,这时候服务器就能识别出恶意的访问。
这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的Referer字段。虽然http协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其Referer字段的可能。

+

添加校验token

由于CSRF的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在cookie中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再运行CSRF攻击。这种数据通常是窗体中的一个数据项。服务器将其生成并附加在窗体中,其内容是一个伪随机数。当客户端通过窗体提交请求时,这个伪随机数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪随机数,而通过CSRF传来的欺骗性攻击中,攻击者无从事先得知这个伪随机数的值,服务端就会因为校验token的值为空或者错误,拒绝这个可疑请求。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 网络安全

+

文章作者:

+

发布时间:2020年01月22日 - 14:50

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/22/FE-guide-safe/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/01/22/Wepack-Tips/index.html b/2020/01/22/Wepack-Tips/index.html new file mode 100644 index 0000000000..ec121dbba5 --- /dev/null +++ b/2020/01/22/Wepack-Tips/index.html @@ -0,0 +1,622 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + webpack中loader和plugin之间的区别 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ webpack中loader和plugin之间的区别 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在学习Webpack相关的知识的时候对loader和plugin有点迷惑,两个特性都是用来做打包相关处理,那么他们有什么区别呢,为了弄清楚他们有什么区别,我开始了疯狂的查找资料,虽说每篇文章都说了一些自己的认识,但是并没有一个特别标准或者说容易理解的答案,我且先将它们记录下来,以便日后回顾之时可以有一些感触。那么接下来我将开始webpack的解迷之旅。

+ + +

背景知识

在研究loader和plugin之前区别之前,我们先来看看一个webpack配置的常见结构

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// js代码

const webpack = require("webpack");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
// 入口文件
entry: {
app: path.join(__dirname, "../src/js/index.js")
},
// 输出文件
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "dist"),
publicPath: "/"
},
// loader配置
module: {
rules: [
{
test: /\.scss/,
use: [
"style-loader",
"css-loader"
]
}
......
]
},
// plugins配置
plugins: [
// 重新创建html文件
new HtmlWebpackPlugin({
title: "首页",
filename: "index.html",
template: path.resolve(__dirname, "../src/index.html")
})
......
]
}
+

webpack的打包原理

+
    +
  • 识别入口文件
  • +
  • 通过逐层识别模块依赖(Commonjs、amd或者es6的import,webpack都会对其进行分析,来获取代码的依赖)
  • +
  • webpack做的就是分析代码,转换代码,编译代码,输出代码
  • +
  • 最终形成打包后的代码
  • +
+

什么是loader

我们可以看到loader实际上是在module的rules下,用对象的方式表示了需要处理的文件类型,和需要用哪些loader做处理

+
+

loader是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中。

+
+
    +
  • 处理一个文件可以使用多个loader,loader的执行顺序和配置中的顺序是相反的,即最后一个loader最先执行,第一个loader最后执行
  • +
  • 第一个执行的loader接收源文件内容作为参数,其它loader接收前一个执行的loader的返回值作为参数,最后执行的loader会返回此模块的JavaScript源码
  • +
+

什么是plugin

+

在webpack运行的生命周期中会广播出许多事件,plugin可以监听这些事件,在合适的时机通过webpack提供的API改变输出结果。

+
+

loader和plugin的区别

对于loader,它是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.scss转换为A.css,单纯的文件转换过程

+

plugin是一个扩展器,它丰富了webpack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务

+

下面我们来看一个例子:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// js代码

class MyPlugin{
constructor(options){
console.log("MyPlugin constructor:", options);
}
apply(compiler){
compiler.plugin("compilation", compilation => {
console.log("MyPlugin");
});
}
}
module.exports = MyPlugin;

// webpack.config.js配置:
module.exports = {
...
plugins: [
new MyPlugin({param: "my plugin"})
]
}
+ +

使用该plugin后,执行的顺序:

+
    +
  1. webpack启动后,在读取配置的过程中会执行new MyPlugin(options)初始化一个MyPlugin获取其实例
  2. +
  3. 在初始化compiler对象后,就会通过compiler.plugin(事件名称,回调函数)监听到webpack广播出来的事件
  4. +
  5. 并且可以通过compiler对象去操作webpack
  6. +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:webpack中loader和plugin之间的区别

+

文章作者:

+

发布时间:2020年01月22日 - 10:38

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/22/Wepack-Tips/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/02/11/SSR/index.html b/2020/02/11/SSR/index.html new file mode 100644 index 0000000000..5a5ea95f28 --- /dev/null +++ b/2020/02/11/SSR/index.html @@ -0,0 +1,609 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SSR | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ SSR +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

什么是SSR

传统浏览器的vue纯浏览器渲染

浏览器请求服务器,服务器返回静态资源 ,客户端负责渲染js

+

ssr

浏览器请求服务器,node服务端渲染js后,返回对应的页面返回给浏览器

+

SSR需要那些东西

手写SSR

特性:

+
    +
  • 每一次访问必须新建一个vue实例
  • +
  • 只会触发组件的 beforeCreate和created钩子
  • +
+

核心库

+
    +
  • vue
  • +
  • vue-server-renderer

    vue + next

    +

    作者:李旭光
    引用请标明出处

    +
    +
  • +
+

前言

+
+

作者:李旭光
引用请标明出处

+
+

前言

+
+

作者:李旭光
引用请标明出处

+
+

前言

+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:SSR

+

文章作者:

+

发布时间:2020年02月11日 - 16:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/02/11/SSR/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/02/11/restful-architecture/index.html b/2020/02/11/restful-architecture/index.html new file mode 100644 index 0000000000..7c6c8178e2 --- /dev/null +++ b/2020/02/11/restful-architecture/index.html @@ -0,0 +1,602 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NodeJS 下 RESTful 架构的最佳实践(课堂笔记) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ NodeJS 下 RESTful 架构的最佳实践(课堂笔记) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

什么是 RESTful

+

REST 指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是 RESTful。

+
+

实际上就是 url 风格/ 路由风格 , 给出一种写路由的原则 — 面向资源
tip:静态路由利于seo优化 api/news/1,动态路由不利于seo优化 api/news?id=1

+

原来的风格
| 路由 | 功能 | 描述 |
| —- | —- |——|
| http://127.0.0.1/user/query/1 | 查询 | 根据ID查询用户信息 |
| http://127.0.0.1/user/save | 保存 | 注册用户 |
| http://127.0.0.1/user/update | 更新 | 修改用户 |
| http://127.0.0.1/user/delete/{id} | 删除 | 删除用户 |

+

RESTful — 面向资源:对于同一个资源都在同一个 URL 进行,通过判断 HTTP 请求的类型来决定做不同的事
/user 一个资源
| 路由 | 请求类型 |
| ———————– | ——– |
| http://127.0.0.1/user/1 | GET |
| http://127.0.0.1/user | POST |
| http://127.0.0.1/user | PUT |
| http://127.0.0.1/user | DELETE |

+

URL 描述资源 , HTTP 描述请求。(HTTP协议无语义无状态 —- 降低复杂度,业务无关 — 架构代码业务无关)

+

RESTful 采用的是顶层路由

+
+

顶层路由设计:不需要有物理文件映射路由

+
+
1
2
3
4
5
6
7
8
// express
// app.js
const express = require('express')
const app = express()
app.get('/case.avi',(req, res)=>{
res.send('hello world'); // 不需要对应物理文件
})
app.listen(3000)
+

原生接口

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// index.js
const http = require('http');
const mysql = require('mysql'); // mysql
const co = require('co-mysql') // 异步同步化
const md5 = require('md5-node') // md5加密

// 连接数据库
let db = mysql.createPool({ // 连接池自己管理 不用关闭
host:'localhost',
user:'root',
password:'root',
database:'user'
})
let conn = co(db)

const app = http.createServer(async (req,res)=>{
if(req.method === 'POST'){
if(req.url === '/user'){
// res.end(JSON.stringify({'message':'对user发起post请求'}))
req.on('data', async (data)=>{
arr.push(data)
})
req.on('end',async ()=>{
let buffer = Buffer.concat(arr);
// json对象
let {username,pasword} = JSON.parse(buffer.toString())
// console.log(username,pasword)
let sql = `selct user from admin where user = ${username}`
let data = await conn.query(sql);
// console.log(data)
if(data.length >=1 ){
res.end(JSON.stringify({
'status':200,
'message':'用户名已经注册'
}))
}else{
// 写入数据库
password = md5(password);
let sql = `INSERT INTO admin (user,password) VALUES ('${username}','${password}')`
await conn.query(sql);
res.end(JSON.stringify({
'status':200,
'message':'注册成功'
}))
}
})
}
}if(req.method === 'GET'){
if(req.url === '/user'){
// res.end(JSON.stringify({'message':'对user发起get请求'}))
let sql = `SELECT id,user,password FROM admin`
let data = await conn.query(sql);
res.end(JSON.stringify(data))
}
}
}).listen(3000)

// .http 文件
@url = http://localhost:3000
@type = Content-Type: applications

GET {{url}}/user HTTP/1.1

POST {{url}}/user HTTP/1.1
{{type}}

{
username:'admin',
password:123456
}
+

使用express实现(express — generater yard ,koa — async await)

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

const express = require('express')
const app = express()
const mysql = require('mysql'); // mysql
const co = require('co-mysql') // 异步同步化
const md5 = require('md5-node') // md5加密
const bodyparse = require('body-parse')
// 连接数据库
let db = mysql.createPool({
host:'localhost',
user:'root',
password:'root',
database:'user'
})
let conn = co(db)

app.use(bodyparse.urlencoded({
extended:true // 返回对象是兼职对,false - string/array true - any
}))
app.use(bodyparse.json())

app.post('/user',async (req.res)=>{
let { username , password} = req.body
// console.log(username,pasword)
let sql = `selct user from admin where user = ${username}`
let data = await conn.query(sql);
// console.log(data)
if(data.length >=1 ){
res.send(JSON.stringify({
'status':200,
'message':'用户名已经注册'
}))
}else{
// 写入数据库
password = md5(password);
let sql = `INSERT INTO admin (user,password) VALUES ('${username}','${password}')`
await conn.query(sql);
res.send(JSON.stringify({
'status':200,
'message':'注册成功'
}))
}
})

app.get('/user/:id',(req,res)=>{
res.send(req.params.id)

let sql = `SELECT id,user,password FROM admin WHERE id = ${req.params.id}`
let data = await conn.query(sql);
res.end(JSON.stringify(data))
})

app.listen(3000)
+ +

使用koa实现

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53


// config.js
module.exports = {
host:'localhost',
user:'root',
password:'root',
database:'user'
}

// libs/database.js
const config = require('../config')
const mysql = require('mysql'); // mysql
const co = require('co-mysql') // 异步同步化
// 连接数据库
let db = mysql.createPool({
host:config.host,
user:config.user,
password:config.password,
database:config.database
})
let conn = co(db)

// router/user/index.js
const Router = require('koa-router')
const md5 = require('md5-node') // md5加密
const router = new Router();

router.get('/user',async ctx=>{
ctx.body = '主页'
})
router.post('/user',async ctx=>{
let {username,password} = ctx.request.body
// console.log(username,password)
ctx.body = {
username,password
}
})
module.exports = router.routes();

// app.js
const koa = require('koa')
const Router = require('koa-router')
const body = require('koa-bodyparse')
const config = require('config')
const app = new Koa()
const router = new Router()
app.context.db = require('./libs/database')
app.context.config = config
app.use(body())
router.use('/api',require('./router/user'))
app.use(router.routes())
app.listen(3000)
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:NodeJS 下 RESTful 架构的最佳实践(课堂笔记)

+

文章作者:

+

发布时间:2020年02月11日 - 16:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/02/11/restful-architecture/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/02/12/print-table-problem/index.html b/2020/02/12/print-table-problem/index.html new file mode 100644 index 0000000000..d078552bcc --- /dev/null +++ b/2020/02/12/print-table-problem/index.html @@ -0,0 +1,591 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 由打印引起的一点小问题,写table时别忘了写thead和tbody | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 由打印引起的一点小问题,写table时别忘了写thead和tbody +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

这两天复工,公司一个小伙伴在群里问了一个问题

+
+

如何在打印表格的时候,让超过一页的表格分割线不被截断

+
+

说着他有贴上来了一张图,一张因为跨页被截断了的表格,说实话打印的场景接触的不多,打印表格一般都是制式的样式所以也不会出现这问题,一时间也没有头绪,想着这个是不是没什么办法解决,隔天他发来一个消息,说是解决了,给表格里加上了 tr td 就好了,接着他又发来一张图,还真是,不过上下边距发生了一些变化,我问他是不是自己改了边距所以好了,他说没改,然后我问他那他是怎么解决的,他说因为实在找不到问题解决方案和产生原因,于是就把代码重构了一下,按照标准写法写了一下,结果就好了。。。怎么说呢,也是神奇,我也到网上去查了查相关的材料,并没有特别对的上的原因,凭感觉来说,应该是thead和tbody上有默认的css样式解决了上面的问题,暂且记下这个问题,避免以后遇到又忘了是怎么回事。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:由打印引起的一点小问题,写table时别忘了写thead和tbody

+

文章作者:

+

发布时间:2020年02月12日 - 08:30

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/02/12/print-table-problem/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/02/12/vue-proxyTable-problem/index.html b/2020/02/12/vue-proxyTable-problem/index.html new file mode 100644 index 0000000000..20ff5659dd --- /dev/null +++ b/2020/02/12/vue-proxyTable-problem/index.html @@ -0,0 +1,586 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Could not proxy request XXX from localhost:8080 to localhost:8081 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Could not proxy request XXX from localhost:8080 to localhost:8081 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

最近又有小伙伴问我,为啥我在写vue项目做代理转发时候报找不到代理服务的问题?
我看了一下他发给我的配置截图,仅一眼我就发现了问题所在,因为这个坑我曾经也踩过,而且好多小伙伴也都踩过,在写转发服务器的时候没写协议头,也就是在 target 那没写 http:// 这个部分,添加上以后重启一下,这个问题就解决了,小伙伴说就这个问题他解决了一天也没解决了,而且我说完了以后他想起来以前好像也遇到过这个问题,所以说在平时工作中遇到问题要常做笔记才对,所以为了避免以后自己也犯这种错误,暂且留一篇文章记录一下这个问题。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Could not proxy request XXX from localhost:8080 to localhost:8081

+

文章作者:

+

发布时间:2020年02月12日 - 09:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/02/12/vue-proxyTable-problem/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/02/19/develop-custom-cli-tools-using-node/index.html b/2020/02/19/develop-custom-cli-tools-using-node/index.html new file mode 100644 index 0000000000..0905f160c9 --- /dev/null +++ b/2020/02/19/develop-custom-cli-tools-using-node/index.html @@ -0,0 +1,651 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 使用node开发自定义cli工具 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 使用node开发自定义cli工具 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

这篇文章想写一写前端工程化相关的内容,原因一呢是是结合公司业务给自己定的业绩指标包含这些内容,另外一个原因是因为听了网易前端唐磊说过的一句话,前端leader干什么,很重要的工作就是前端工程化,高级以上不懂前端工程化很难。

+

今天听了公开课讲到了用node写一个自己的cli,说实话正是工作所需,课程讲的有点快,没有从头跟下来,自己上完了课又上网上找了些资料,终于把步骤弄明白了,下面就把如何自定义一个cli来帮助提升开发效率。同时也完成了一个业务指标,心里美滋滋。

+

准备

如果你看到这篇文章,也想跟着我的步骤写一下这个自定义cli,那么如下的知识还是有一些为好。

+
    +
  • 基础的nodejs相关知识
  • +
+

没错就只需要会一些node的基础知识就可以了,接下来正式开始

+

初始化

首先,我们要给我们的命令行工具起个名字,比如我们熟悉的 vue 命令行就是 vue-cli ,因为我写完了要给公司用,所以我起的名字是 tfd-cli ,你们喜欢叫什么你们随意

+

首先创建一个名字为 tfd-cli 的文件目录,然后在目录下执行 node 工程的初始化命令

+
1
npm init -y // 初始化项目 -y 默认全部yes的参数
+

命令执行完成后 tfd-cli 目录下会生成一个我们熟悉的 package.json 文件,我们打开 package.json 文件,增加一段代码,如下

+
1
2
3
4
// 追加的代码
"bin": {
"tfd": "index.js"
}
+

追加完成后,package.json 文件中的内容是这样的

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"name": "tfd-cli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"bin": {
"tfd": "index.js"
}
}
+

也就是说当我们执行 tfd 命令时它就会找同级目录下的 index.js 文件执行其中代码,我们目前还没有 index.js,文件,那么我们手动创建一个 index.js 的文件,然后在里面写下如下代码

+
1
2
3
4
#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的

console.log('hello world');
+

执行完这些还不够,因为我们是开发环境所以还有一步操作是要将 tfd 命令告知 npm,该如何处理,所以我们要执行如下命令

+
1
npm link
+

这里如果执行不成功,请用管理员权限执行,执行完成后我们会得到一个 package-lock.json 的文件,内容如下

+
1
2
3
4
5
{
"name": "tfd-cli",
"version": "1.0.0",
"lockfileVersion": 1
}
+

如此一来,我们就可以在任何目录下执行命令行 tfd 就会执行 index.js 文件了,这里我们会在控制面板中输出 hello world ,怎么样是不是小有成就感,我们接着往下来。

+

创建指令

我们写个命令行工具肯定不是为了输出个 hello world 这么简单,而是希望通过用户输入内容后根据条件输出一些东西,那么让我们想想一个命令行工具应该具备哪些指令呢?

+
    +
  • 查看命令行工具版本
  • +
  • 查看帮助文档
  • +
  • 初始化模板
  • +
  • 列出模板类型
  • +
  • 等等
  • +
+

那么用指令该如何描述呢

+
1
2
3
4
tfd -V|--version //查看工具版本号
tfd -h|--help //查看使用帮助
tfd init <template-name> <project-name> //基于指定模板进行项目初始化
tfd list //列出所有可用模板
+

为了执行命令,这里我们要引入一个 node 包叫做 commander,因此我们要先执行一下 install 命令

+
1
npm install commander
+

接着我们就可以在 index.js 里面写指令了。

+
1
2
3
4
5
6
7
8
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0
+

到这一步我们在控制台敲一下 tfd -V 你会发现什么也没输出,这是因为到这一步我们还无法解析 tfd -V 操作,在这之前我们要知道一个命令

+
1
process.argv // 使用process.argv获取命令行参数
+

当我们把这句话加到 console.log 中在 index.js 中输出时你会看到控制台打印出

+
1
2
3
4
5
6
7
8
9
10
11
12
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0
console.log(process.argv)

// 控制台
[ '/usr/local/bin/node', '/usr/local/bin/tfd', '-V' ]
+

接下来我们要让commander获取参数执行命令

+
1
2
3
4
5
6
7
8
9
10
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0
// console.log(process.argv)
cmd.parse(process.argv);
+

这个时候我们再在控制台输入 tfd -V 时,我们就会发现,控制台输出了 0.1.0,这样我们就完成了查版本的指令,接下来我们完成其他的指令

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0

// tfd init <template> <project>
cmd
.command('init <template> <project>') // 参数
.description('初始化项目模板')
.action((templateName, projectName) => {
console.log(templateName, projectName);
})

// tfd list
cmd
.command('list')
.description('查看所有可用模板')
.action(() => {
console.log(`
a a模板
b b模板
c c模板
`)
})
// console.log(process.argv)
cmd.parse(process.argv);
+

这个时候我们在控制台上输入 tfd -h 的时候,控制台会输出如下代码

+
1
2
3
4
5
6
7
8
9
Usage: tfd [options] [command]

Options:
-V, --version output the version number
-h, --help output usage information

Commands:
init <template> <project> 初始化项目模板
list 查看所有可用模板
+

这样我们就实现了自定义命令,我们执行一下 tfd init template1 project1,我们可以看到,控制台中输出了 template1 project1,也就是说command命令后尖括号中指向了action中的参数,我们就可以通过判断action中的参数做具体的操作了。

+

通常模板可以选择从本地拷贝一份,但更常用的是从线上拷贝一份,比如从github中,接下来我们就看看如何从github中拷贝一个模板作为项目的初始化工程

+

github上创建模板仓库

首先我们要在github上创建两个仓库 tpl-1 tpl-2,这里为了从github中下载仓库我们需要一个node包支持,让我们请出download-git-repo,别忘了执行安装命令

+
1
npm install download-git-repo
+

安装完依赖之后让我们再去index.js填点东西,首先引入下载依赖,然后是创建下载的 template 抽象对象

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');
const download = require('download-git-repo');
// 可用模板
const templates = {
'tpl-1': {
url: 'https://github.com/lixuguang/tpl-1',
downloadUrl: 'https://github.com:lixuguang/tpl-1#master',
description: 'tfd-cli脚手架测试模板1'
},
'tpl-2': {
url: 'https://github.com/lixuguang/tpl-2',
downloadUrl: 'https://github.com:lixuguang/tpl-2#master',
description: 'tfd-cli脚手架测试模板2'
}
}

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0

// tfd init <template> <project>
cmd
.command('init <template> <project>') // 参数
.description('初始化项目模板')
.action((templateName, projectName) => {
// console.log(templateName, projectName);
let {downloadUrl} = templates[templateName];
// 第一个参数是github仓库地址,第二个参数是创建的项目目录名,第三个参数是clone
download(downloadUrl, projectName, {clone: true}, err => {
if(err){
console.log('模板下载失败');
}else{
console.log('模板下载成功');
}
})
})

// tfd list
cmd
.command('list')
.description('查看所有可用模板')
.action(() => {
// console.log(`
// a a模板
// b b模板
// c c模板
// `)
// 通过获取templates里的key可以获取到模板名称
const templateName = Object.keys(templates)
console.log(templateName)
})
// console.log(process.argv)
cmd.parse(process.argv);
+

这样当我们执行 tfd list 就可以看到有哪些模板了,然后执行tfd init tpl-1 newproject 就可以依据 tpl-1 模板创建出 newproject 工程,这个过程实际上就是从github仓库克隆一份tpl-1作为模板创建工程newproject

+

这里需要注意的是download地址跟github仓库地址有点出入,比如github仓库地址是https://github.com/xxx/xxx而下载地址是https://github.com:xxx/xxx

+

虽然这样执行完成后就完成了基本的cli的雏形,但是毕竟不灵活,我们在使用vue-cli时,它的创建过程是问答式和选择式的,另外每个过程都会有进度显示什么的,那么要怎么添加这些功能呢,我们接着往下做。

+

进阶增加功能

使用inquirer进行命令行答询

inquirer 是一个进行命令行答询的库,通过它我们就可以创建问答式的内容,首先还是安装依赖

+
1
npm install inquirer
+

使用handlebars修改package.json

我们都知道在使用vue-cli的初始化命令后,会在项目目录下生成一个package.json文件,它就像是这个项目的基因序列一样,影响着项目的整个结构。模板是固定的,那要修改其中的package.json符合自己项目的需要,就要用到handlebars这个库来改写package.json文件,老规矩先安装它

+
1
npm install handlebars
+

使用ora在命令行中显示加载状态

我们在装任何依赖时都会有进度条显示进度,如果没有进度条又没有任何响应,会让用户迷茫,为了友好,我们就要加进度条,这里我们需要引入ora这个库来完成进度显示,我们接着安装。

+
1
npm install ora
+

使用chalk和log-symbols增加命令行输出样式

为了让命令行有红红绿绿的效果以及符号效果,我们需要使用chalklog-symbols来丰富样式,少废话,接着装

+
1
npm install chalk log-symbols
+ +

集大成

终于安装完一堆的依赖,别忘了在index.js中引入,让我们看看具体如何使用这些库吧。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');
const download = require('download-git-repo');

const iq = require('inquirer'); // 命令行答询
const hb = require('handlebars'); // 修改package.json文件
const ora = require('ora'); // 命令行中加载状态标识
const chalk = require('chalk'); // 命令行输出字符颜色
const ls = require('log-symbols'); // 命令行输出符号
const fs = require('fs'); // node fs原生模块

// 可用模板
const templates = {
'tpl-1': {
url: 'https://github.com/lixuguang/tpl-1',
downloadUrl: 'https://github.com:lixuguang/tpl-1#master',
description: 'tfd-cli脚手架测试模板1'
},
'tpl-2': {
url: 'https://github.com/lixuguang/tpl-2',
downloadUrl: 'https://github.com:lixuguang/tpl-2#master',
description: 'tfd-cli脚手架测试模板2'
}
}

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0

// tfd init <template> <project>
cmd
.command('init <template> <project>') // 参数
.description('初始化项目模板')
.action((templateName, projectName) => {
// console.log(templateName, projectName);
let {downloadUrl} = templates[templateName];
//下载github项目,下载墙loading提示
const loading = ora('模板下载中...').start();
// 第一个参数是github仓库地址,第二个参数是创建的项目目录名,第三个参数是clone
download(downloadUrl, projectName, {clone: true}, err => {
if(err){
// console.log('模板下载失败');
loading.fail('模板下载失败');
}else{
// console.log('模板下载成功');
spinner.succeed('模板下载成功');
// 命令行答询
iq.prompt([
{
type: 'input', // 类型 输入框
name: 'name', // 字段 key
message: '请输入项目名称', // 描述
default: projectName // 默认值
},
{
type: 'input',
name: 'description',
message: '请输入项目简介',
default: ''
},
{
type: 'input',
name: 'author',
message: '请输入作者名称',
default: ''
}
]).then(answers => { // answers 是一个对象,对象的 key 为上面答询的 name 的值,value 为 用户输入的值,如果未输入,就取默认值
// 根据命令行答询结果修改 package.json 文件
let packageContent = fs.readFileSync(`${projectName}/package.json`, 'utf8'); // 同步方式以 utf-8 字符集获得下载好的项目目录下的 package.json 文件
let packageResult = hb.compile(packageContent)(answers); // 将用户输入项与原内容混合获得新内容
fs.writeFileSync(`${projectName}/package.json`, packageResult); // 重新同步方式写入到 package.json 文件中
// 用chalk和log-symbols改变命令行输出样式
console.log(ls.success, chalk.green('模板项目文件准备成功!'));
})
}
})
})

// tfd list
cmd
.command('list')
.description('查看所有可用模板')
.action(() => {
// console.log(`
// a a模板
// b b模板
// c c模板
// `)
// 通过获取templates里的key可以获取到模板名称
const templateName = Object.keys(templates)
console.log(templateName)
})
// console.log(process.argv)
cmd.parse(process.argv);
+ +

到这里你自己的命令行工具就创建完成了,当然还可以继续丰富,比如加上选择条件等,这个就更复杂了,不在本文中展开,后续会继续改进这个命令行工具,当然如果要将自己的命令行工具给其他人用当然要先发布出去,就像我们要用vue-cli,首先要在npm上下载下来,同样的我们要想让别人用,就得先上传到npm上。

+

发布到npm上

首先你需要有一个npm的账号,没有赶快去注册一个,在控制台中输入npm login,它会让你依次输入 Username Password Email ,当你都按照要求输入完之后,成功的话你会获得如下信息Logged in as XXX on https://registry.npm.org/.,再接下来执行 npm publish 命令,你的自定义脚手架就会发布到npm上,供他人下载使用,怎么样,学会了么?

+

后记

这篇文章会有后续持续进化跟进,多篇文章连续,只要我的cli工具还在进化,文章就会继续,欢迎跟进。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:使用node开发自定义cli工具

+

文章作者:

+

发布时间:2020年02月19日 - 15:00

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/02/19/develop-custom-cli-tools-using-node/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/02/21/Implementation-of-the-vue-response-principle/index.html b/2020/02/21/Implementation-of-the-vue-response-principle/index.html new file mode 100644 index 0000000000..46fd41e46b --- /dev/null +++ b/2020/02/21/Implementation-of-the-vue-response-principle/index.html @@ -0,0 +1,693 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Vue 响应式原理的实现(课程笔记) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Vue 响应式原理的实现(课程笔记) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近一直在追网课,说实话从业9年了,一直觉得前端发展非常快,而且一直充满着危机感,每天都要学习进步才有安稳的感觉,今天听了 vue 响应式原理实现的公开课,感觉还不错,做了如下笔记,帮助自己记忆,也希望能帮助大家。

+

Vue2 原理

什么是 defineProperty

defineProperty 其实是定义对象属性用的

+
+

defineProperty 其实并不是核心的为一个对象做数据双向绑定,而是去给对象做属性标签,只不过是属性里 get 和 set 实现了响应式。

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
属性名默认值
valueundefined
getundefined
setundefined
writablefalse
enumerablefalse
configurablefalse
+
1
2
3
4
5
6
7
8
9
10
11
12
13
var ob = {
a:1,
b:2
}
// 参数 1、对象 2、属性 3、配置
Object.defineProperty(ob,'a',{
writable:false,
enumerable:true,
configurable:true,
})
console.log(Object.getOwnPropertyDescriptor(ob,'a')) // 1
ob.a = 2
console.log(ob.a) // 1
+

下面我们实现一下双向绑定

+
1
2
3
4
5
6
7
8
9
10
11
12
13
Object.defineProperty(ob,'a',{
get:function(){
console.log('a is be get')
return 999;
},
set:function(){
console.log('a is be set')
return 999;
},
})

console.log(Object.getOwnPropertyDescriptor(ob,'a')) // 999

+

改造代码实现双向绑定(存取值)

+
1
2
3
4
5
6
7
8
9
10
11
12
var _val = obj.a; // 暂存
Object.defineProperty(ob,'a',{
get:function(){
console.log('a is be get')
return _val;
},
set:function(newVal){
_val = newVal // 新值替换旧值
console.log('a is be set')
return _val;
},
})
+

Vue 中从改变一个数据到发生改变的过程

    +
  1. 改变数据触发 Set
  2. +
  3. Set 部分触发 notify(更新)
      +
    1. Get 部分收集依赖
    2. +
    +
  4. +
  5. 更改对应的虚拟 Dom
  6. +
  7. 重新 Render
  8. +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// MyVue.js

// 简单版本 vue
function MyVue(){
this.$data = {
a: {
b:1
},
c:2
}
this.el = document.getElementById('app');
this.virtualDom = '';
this.observer(this.$data);
this.render();
}
vue.property.observer = function(obj){
var _val, self = this;
// var dep = new Dep() -> 源码中依赖收集对象
for(var key in obj){ // 属性有可能是对象,要递归绑定
_val = obj[key];
if(typeof _val === 'Object'){
this.observer(_val)
}else{
Object.defineProperty(this.$data,key,{ // 这里是实际绑定过程
get:function(){
// 依赖收集
// dep.depend(); -> vue 源码中收集依赖的方法
return _val
},
set:function(newVal){
_val = newVal
// dep.notify(); -> vue 源码中
self.render() // AST语法树
}
})
}
}
}
vue.property.render = function(){
this.virtualDom = 'i am '+this.$data.b;
this.el.innerHTML = this.virtualDom;
}
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// index.html

<!DOCTYPE html>
<html>
<head>
<title>自己实现Vue2数据双向绑定</title>
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src='myVue.js'></script>
<script type="text/javascript">
var mv = new MyVue();
setTimeout(function(){
console.log('changes');
console.log(mv.$data);
mv.$data.b = 222;
})
</script>
</body>
</html>
+
+

依赖收集:

+
    +
  1. 我们的data里面的数据并不是所有地方都用到
  2. +
  3. 如果我们直接更新整个视图,浪费资源
  4. +
  5. 先收集依赖改变的数据的组件,再更新依赖了数据的组件(Dep depend notify)
  6. +
+
+

格外注意的地方—数组怎么监听

definePropty 只能给对象进行 get set 绑定, 数组怎么办?

+

vue 中 使用了 装饰者模式

+
+

装饰者模式 Decorator模式(别名Wrapper):动态将职责附加到对象上,若要扩展功能,装饰者提供了比继承更具弹性的代替方案。

+
+
1
2
3
4
5
6
7
8
9
10
var arraypro = Array.property; // 创建一个数组的原型对象
var arrob = Object.create(arraypro); // 避免影响原型链
var arr = ['push','pop','shift'];
arr.forEach(function(method,index){
arrob[method]=function(){ // 装饰者模式
var ret = arraypro[method].apply(this,arguments)
dep.notify() // 扩展了功能
}
})

+

Vue3 实现双向绑定

Proxy 是什么?

+
+

Proxy 对象用于定义基本操作的自定义行为
和 definePropty 类似,功能几乎一样,只是用法上不同

+
    +
  1. 不会污染原对象
  2. +
  3. 直接给对象就可以了
  4. +
  5. 不需要借助外部变量 _val
  6. +
+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var ob = {
a:1,
b:2
}

var newOb = new Proxy(ob,{
get(target,key,receiver){ // target 对象,key 属性
console.log(target,key,receiver)
return target[key]
},
set(target,key,value,receiver){
return Reflect.set(target.key,value);
// 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
// return target[key] = value
}
})
+

为什么改用 Proxy

    +
  1. defineProperty 只能监听某个属性,不能全对象监听
  2. +
  3. 可以省去for in循环提升代码执行效率
  4. +
  5. 可以监听数组,不需要再为数组做特异性操作
  6. +
  7. 不污染原对象
  8. +
  9. 更优雅
  10. +
+

我们用 Proxy 实现一下 observe 方法

+
1
2
3
4
5
6
7
8
9
10
11
12
vue.property.observe = function(){
var self = this;
this.$data = new Proxy(this.$data,{
get(target,key, receiver){
return target[key]
},
set(target,key,newVal){
target[key] = newVal
self.render()
}
})
}
+ +

还能用 Proxy 做什么

    +
  1. 校验类型
  2. +
  3. 真正的私有变量
  4. +
+
校验类型

例子:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 数据类型验证
// 我们要创建一个对象,这个对象是个人,他有name和age两个属性
// name必须是中文,age必须是数字,大于18岁

// 这里用到了策略模式
var valid = {
name(value){
var reg=/^[\u4E00-\u9FAS]=$/
if(typeof value === 'string' && reg.test(value)){
return true;
}
return false;
},
age(value){
if(typeof value === 'number' && value > 18){
return true;
}
return false;
}
}
function Person(name,age){
this.name = name
this.age = age
return new Proxy(this,{
get(target,key){
return target[key]
},
set(target,key,value){
if(valid[key](value)){
return Reflect.set(target,key,value)
}else{
throw new Error(key+'is not valid')
}
}
})
}
new Person('name',19)
+
+

策略模式
定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。

+
+
真正的私有变量

vue-router 源码中,给 $router ,$route 用 defineProperty 定义 get 并返回本身,这样就不能修改属性了。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
Object.defineProperty(this,'$router',{ // Router 的实例
get(){
return this._root._router;
}
})
Object.defineProperty(this,'$route',{
get(){
return {
// 当前路由所在的状态
current: this._root._router.history.current;
}
}
})
+ +

虚拟Dom和diff算法

虚拟Dom是虚拟的,他只在概念里面存在,在AST语法树,下面进行解释

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 模板
<template>
<div>
<p>{{msg}}</p>
<p>2</p>
<p>3</p>
</div>
</template>

// diff 描述法
diff <div>
props:{
id:2
}
children:[
diff <p>
props:{
id:xxx
}
children:[
...
]
]

// 对象描述法
var virtual = {
dom:'div',
props:{
id:2
},
children:[
....
]
}
+

每层结构都是一样的,那么是如何进行 diff 比对的呢?

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** 
* diff 算法
*/
patchVnode(oldVnode,vnode){ // 接收新旧节点
const el = vnode.el = oldVnode.el; // 拿出真实dom
let i,oldCh = oldVnode.children ,ch = vnode.children // 拿出新旧节点子元素children数组
if(oldVnode === vnode) return; // 新旧节点一致,直接返回不进行后续操作
// 分情况操作 --- 只有文字节点,删除了子元素,增加了子元素,子元素发生变动
// 新旧节点都不为空,且不一样
if(oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text){ // 文字发生更新
api.setTextContent(el,vonde.text) // 文字发生了更新,更新文字,给Dom,更新文字
} else { // 不是单纯文字节点的话
updateEle(); // 更新元素
if(oldCh&&ch&&oldCh!==ch){ // 都有子元素,子元素变动
updateChildren() // 调用更新子元素方法
} else if(ch){ // 增加子元素
createEl(vnode) // 创建子元素
} else if(oldCh){ // 删除子元素
api.removeChildren(el) // 调用删除子元素方法
}
}
}
+

源码要多看,以下必看 Vue/react/axios/vue-router/Redux/Vuex
为什么要看源码??

+
    +
  • 初级前端就会用vue或react — 从差不多水平的60%中挑出更好的人
  • +
  • 提高思想–》看优秀的代码–》写优秀的代码
  • +
  • 看源码能力,对高级前端是必备的。— 解决疑难杂症,看源码了解原理。
  • +
+

vue 性能优化

因为是公开课,所以时间上没来的及说完,以后自己在听别的有关的内容时再补上这块。

+

最后

只有不断学习才能进步,充分利用网络的便利性,找各种优质的教学资源,我相信,努力会有回报,加油!

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Vue 响应式原理的实现(课程笔记)

+

文章作者:

+

发布时间:2020年02月21日 - 23:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/02/21/Implementation-of-the-vue-response-principle/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/02/23/V8-engine-memory-management-and-optimization/index.html b/2020/02/23/V8-engine-memory-management-and-optimization/index.html new file mode 100644 index 0000000000..43a869c3dd --- /dev/null +++ b/2020/02/23/V8-engine-memory-management-and-optimization/index.html @@ -0,0 +1,645 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + v8引擎如何回收内存(笔记) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ v8引擎如何回收内存(笔记) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

V8引擎如何回收垃圾

为什么我们要关注内存

    +
  • 防止页面占用内存过大,引起客户端卡顿,甚至无响应
  • +
  • Node使用的也是v8,内存对于后端服务的性能至关重要。因为服务的持久性,后端更容易造成内存溢出

    v8引擎的内存回收机制

    v8的内存分配

    新生代内存空间
  • +
  • from
  • +
  • to
    老生代内存空间

    内存大小

  • +
  • 和操作系统有关 — 64位(1.4G)32位(0.7G)
  • +
  • 64位下 新生代(64MB) 老生代(1400MB)
  • +
  • 32位下 新生代(16MB) 老生代(700MB)
  • +
+

为什么不占多一点内存

+
    +
  • js设计之初是为浏览器
      +
    • 前端特点 — 不持久化,执行一遍就回收了,所以1.4G够用了
    • +
    • js回收内存会暂停执行代码
    • +
    +
  • +
+

垃圾回收算法

新生代简单的说就是复制

+
    +
  • 新生代用来放新产生的变量,根据条件放到老生代,有用的放到to里(把有用的留一边,清空另一边,往复进行)
  • +
  • 算法复杂度(时间复杂度,空间复杂度)—- 牺牲空间换时间
  • +
+

老生代就是标记、删除、整理

+
    +
  • 为什么要整理
      +
    • 数组是需要连续的空间
    • +
    +
  • +
+

新生代如何晋升到老生代

+
    +
  • 变量是否经过回收,第二次回收放到老生代,第一次回收放到To
  • +
  • To空间使用了25%,放到老生代
  • +
+

V8是如何处理变量的

1
2
3
4
5
6
7
8
9
10
11
12
13
// 浏览器查看内存
window.performance
// nodejs查看内存 --- nodejs是c++的,可以拓宽内存
process.memoryUsage()

// 拿内存的方法
function getMem(){
var mem = process.memoryUsage();
var format = function(bytes){
return (bytes/1024/1024).toFixed(2)+'MB';
}
console.log('heapTotal:'+format(mem.heapTotal)+'heapUsed:'+format(mem.heapUsed))
}
+

变量处理

    +
  • 内存主要就是存储变量等数据的
  • +
  • 局部变量当程序执行结束,且没有引用的时候就会随着消失 — 可以被回收但不是说会马上回收
  • +
  • 全局对象会始终存活到程序运行结束
  • +
+

如何查看V8内存使用情况

如何注意内存使用

优化内存的技巧

    +
  • 尽量不要定义全局变量
  • +
  • 全局变量记得手动销毁掉
      +
    • 不推荐开发时写delete – 支持有问题,严格模式有bug
    • +
    • 赋值为 undefined/null undefined 是变量 null 是保留字
    • +
    +
  • +
  • 用匿名自执行函数变全局为局部
      +
    • (function(){})()
    • +
    +
  • +
  • 尽量避免使用闭包 —- 错误的观点,ie5时代的问题 —- 尽量避免使用闭包引用
  • +
+

闭包

+
1
2
3
4
5
6
7
function a(){
var size = 20*1024*1024;
var arr1 = new Array(size)
return arr1
}
a() // 这样就没问题
var b = a() // 因为引用所以无法销毁
+

防止内存泄漏

    +
  • 滥用缓存
  • +
  • 大内存量操作
  • +
+

所有的优化都可以用缓存来解决,缓存通常都在全局,缓存可以直接拿来用;

+
1
2
3
4
5
6
7
8
9
10
11
12
13
var 20*1024*1024;
var a = []
for(var i=0;i<13;i++){
a.push(new Array(size))
}

// 加缓存锁
for(var i=0;i<13;i++){
if(a.length>4){
a.shift();
}
a.push(new Array(size))
}
+
    +
  • 不要用v8来缓存
      +
    • 一定要用要的话加锁
    • +
    +
  • +
+

nodejs中读取大文件要用流的形式,不要用读文件到buffer
fs.readFile()
fs.createReadStream()

+

浏览器中,大文件上传记得切片
file.slice(0,1000)
file.slice(1000,2000)

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:v8引擎如何回收内存(笔记)

+

文章作者:

+

发布时间:2020年02月23日 - 08:00

+

最后更新:2020年02月23日 - 07:57

+

原始链接:https://blog.lifesli.com/2020/02/23/V8-engine-memory-management-and-optimization/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/02/24/Vue-plug-in-development/index.html b/2020/02/24/Vue-plug-in-development/index.html new file mode 100644 index 0000000000..6f367f004a --- /dev/null +++ b/2020/02/24/Vue-plug-in-development/index.html @@ -0,0 +1,606 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + vue插件开发(笔记) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ vue插件开发(笔记) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

vue插件开发

+

Vue.use({install(Vues){}})

+
+

Vue.use

把给到的内容执行一下
举例

+
1
2
3
4
function a(){
console.log('a')
}
Vue.use(a) // a
+

有 install 就执行 install

+
1
2
3
4
5
6
7
function a(){
console.log('a')
}
a.install = function(){
console.log('b')
}
Vue.use(a) // b
+

再进一步

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function a(){
console.log('a')
}
a.install = function(){
// console.log('b')
vue.mixin({ // 抽离公共逻辑 , 缺点:命名冲突,难以阅读
data(){ // data数据少的时候可以不用vuex 用mixin
return {
c:'this is mixin'
}
},
methods:{
// 混入方法
// 提示性弹窗 原:import 控制 显隐 现在:在根节点引入,通过mixin在方法控制显隐
}
// 混入生命周期
create(){
// 所有组件的create生命周期都执行 mixin先执行
}
})
}
Vue.use(a) // b
+ +

vue.util.defineReactive()

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
vue.util.defineReactive()

var test = {
testa: 1
}
setTimeout(()=>{
test.testa = 2
},1000)
vue.mixin({
beforeCreate(){
this.test = test
}
})

+ +

vue.extend vue.util.extend

+

vue.util.extend ===> 简单做了个拷贝,拷贝到一起

+
1
vue.util.extend(a,b)
+

vue.extend ===> 获取到某个对象的实例

+
1
2
let Constrator = vue.extend(obj)
let vm = new Constrator()
+ +

手写vue-router

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// myVueRouter.js
class HistoryRoute(){
constructor(){
this.current = null;
}
}

class vueRouter{
constructor(options){
this.mode = options.mode || 'hash'
this.history = new HistoryRoute
this.routes = options.routes||[]
this.routesMap = this.createMap(this.routes)
this.init()
}
init(){
if(this.mode == 'hash'){
// 自动加上 #
location.hash?"":location.hash="/"
window.addEventListener('load',()=>{
this.history.current = location.hash.slice(1)
})
window.addEventListener('hashchange',()=>{
this.history.current = location.hash.slice(1)
})
}else{
location.pathname?"":location.pathname="/"
window.addEventListener('load',()=>{
this.history.current = location.hash.pathname
})
window.addEventListener('popstate',()=>{
this.history.current = location.hash.pathname
})
}
}
createMap(router){
return router.reduce((memo,current)=>{
memo[current.path] = current.component
})
}
}

vueRouter.install = function(Vue){
Vue.mixin({
beforeCreate(){ // 组件还未实例化好
if(this.$options && this.$options.router){ // 有配置而且引入路由
this._root = this
this._router = this.$option.router

Vue.util.defineReactive(this,'current',this._router.history)
}else{
this._root = this.$parent._root
}
// 增强健壮性
Object.defineProperty(this,'$route',{
get(){
return this._root._router
}
})
}
})
Vue.component('router-view',{
render(h){
// 如何根据当前的current,获取到对应的组件
let current = this._self._root._router.history.current
let routerMap = this._self._root._router.routeMap
return h(routeMap[current])
}
})
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:vue插件开发(笔记)

+

文章作者:

+

发布时间:2020年02月24日 - 22:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/02/24/Vue-plug-in-development/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/03/03/Speeding-up-brew-with-Ali-mirroring/index.html b/2020/03/03/Speeding-up-brew-with-Ali-mirroring/index.html new file mode 100644 index 0000000000..d8bf0c2e1b --- /dev/null +++ b/2020/03/03/Speeding-up-brew-with-Ali-mirroring/index.html @@ -0,0 +1,603 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 使用阿里镜像加速brew(转载) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 使用阿里镜像加速brew(转载) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

使用 Alibaba 的 Homebrew 镜像源进行加速

平时我们执行 brew 命令安装软件的时候,跟以下 3 个仓库地址有关:

+
    +
  • brew.git
  • +
  • homebrew-core.git
  • +
  • homebrew-bottles
    通过以下操作将这 3 个仓库地址全部替换为 Alibaba 提供的地址
  • +
+

1. 替换 / 还原 brew.git 仓库地址

1
2
3
4
5
6
7
8
9
# 替换成阿里巴巴的 brew.git 仓库地址:
cd "$(brew --repo)"
git remote set-url origin https://mirrors.aliyun.com/homebrew/brew.git

#=======================================================

# 还原为官方提供的 brew.git 仓库地址
cd "$(brew --repo)"
git remote set-url origin https://github.com/Homebrew/brew.git
+ +

2. 替换 / 还原 homebrew-core.git 仓库地址

1
2
3
4
5
6
7
8
9
# 替换成阿里巴巴的 homebrew-core.git 仓库地址:
cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
git remote set-url origin https://mirrors.aliyun.com/homebrew/homebrew-core.git

#=======================================================

# 还原为官方提供的 homebrew-core.git 仓库地址
cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
git remote set-url origin https://github.com/Homebrew/homebrew-core.git
+ +

3. 替换 / 还原 homebrew-bottles 访问地址

这个步骤跟你的 macOS 系统使用的 shell 版本有关系

+

所以,先来查看当前使用的 shell 版本

+
1
2
3
4
echo $SHELL

# 如果你的输出结果是 /bin/zsh,参考下方的 zsh 终端操作方式
# 如果你的输出结果是 /bin/bash,参考下方的 bash 终端操作方式
+

3.1 zsh 终端操作方式

1
2
3
4
5
6
7
8
9
10
# 替换成阿里巴巴的 homebrew-bottles 访问地址:
echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles' >> ~/.zshrc
source ~/.zshrc

#=======================================================

# 还原为官方提供的 homebrew-bottles 访问地址
vi ~/.zshrc
# 然后,删除 HOMEBREW_BOTTLE_DOMAIN 这一行配置
source ~/.zshrc
+

3.2 bash 终端操作方式

1
2
3
4
5
6
7
8
9
10
# 替换 homebrew-bottles 访问 URL:
echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles' >> ~/.bash_profile
source ~/.bash_profile

#=======================================================

# 还原为官方提供的 homebrew-bottles 访问地址
vi ~/.bash_profile
# 然后,删除 HOMEBREW_BOTTLE_DOMAIN 这一行配置
source ~/.bash_profile
+ +

转载自:http://www.xiegangd.com/article/154055689187484

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:使用阿里镜像加速brew(转载)

+

文章作者:

+

发布时间:2020年03月03日 - 22:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/03/03/Speeding-up-brew-with-Ali-mirroring/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/03/23/path-join-vs-path-resolve/index.html b/2020/03/23/path-join-vs-path-resolve/index.html new file mode 100644 index 0000000000..d1841f1581 --- /dev/null +++ b/2020/03/23/path-join-vs-path-resolve/index.html @@ -0,0 +1,587 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + path.join 与 path.resolve 的区别 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ path.join 与 path.resolve 的区别 +

+ + +
+ + + + +
+ + +

path.join 与 path.resolve 的区别

    +
  1. 对于以/开始的路径片段,path.join只是简单的将该路径片段进行拼接,而path.resolve将以/开始的路径片段作为根目录,在此之前的路径将会被丢弃,就像是在terminal中使用cd命令一样。
  2. +
+
1
2
path.join('/a', '/b') // 'a/b'
path.resolve('/a', '/b') // '/b'
+
    +
  1. path.resolve总是返回一个以相对于当前的工作目录(working directory)的绝对路径。
  2. +
+
1
2
path.join('./a', './b') // 'a/b'
path.resolve('./a', './b') // '/Users/username/Projects/webpack-demo/a/b'
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:path.join 与 path.resolve 的区别

+

文章作者:

+

发布时间:2020年03月23日 - 14:42

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/03/23/path-join-vs-path-resolve/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/03/24/do-it-yourselfery-EventEmitter/index.html b/2020/03/24/do-it-yourselfery-EventEmitter/index.html new file mode 100644 index 0000000000..8604faa90d --- /dev/null +++ b/2020/03/24/do-it-yourselfery-EventEmitter/index.html @@ -0,0 +1,592 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- EventEmitter(事件触发器) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- EventEmitter(事件触发器) +

+ + +
+ + + + +
+ + +

实现一个 EventEmitter

    +
  1. 创建一个 Event 类,包含构造函数、绑定、解绑、触发方法
  2. +
  3. on 监听event事件,事件触发时调用fn函数。根据字典创建事件数组,如果事件处理不存在,那么就推入数组,反之返回原数组
  4. +
  5. off 从字典中获取当前事件数组,如果获取值是数组,那么判断解绑哪个事件,如果没填清空全部(数组长度置为零),否则从事件数组删除选择的事件
  6. +
  7. emit 触发event事件,并把参数arg1,arg2,arg3….传给事件处理函数,跟解绑外层判断一样,内层把删除改为执行即可
  8. +
  9. once 为指定事件注册一个单次监听器,单次监听器最多只触发一次,触发后立即解除监听器。
  10. +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class Event {
constructor () {
// 储存事件的数据结构
// 为查找迅速, 使用对象(字典)
this._cache = {}
}

// 绑定
on(event, callback) {
// 为了按类查找方便和节省空间
// 将同一类型事件放到一个数组中
// 这里的数组是队列, 遵循先进先出
// 即新绑定的事件先触发
let fns = (this._cache[event] = this._cache[event] || [])
if(fns.indexOf(callback) === -1) {
fns.push(callback)
}
return this
}

// 解绑
off (event, callback) {
let fns = this._cache[event]
if(Array.isArray(fns)) {
if(callback) {
let index = fns.indexOf(callback)
if(index !== -1) {
fns.splice(index, 1)
}
} else {
// 全部清空
fns.length = 0
}
}
return this
}
// 触发emit
emit(event, ...args) {
let fns = this._cache[event]
if(Array.isArray(fns)) {
fns.forEach((fn) => {
fn(...args)
})
}
return this
}

// 一次性绑定
once(event, callback) {
let onceCallback = () => { // 定义一个只执行一次就解绑的方法
callback.call(this); // 使用call改变this指向
this.off(event, onceCallback); // 解绑
};
this.on(event, onceCallback); // 绑定
return this;
}
}
+

好的接下来我们调用一下

+
1
2
3
4
5
6
7
8

let e = new Event()

e.on('click',function(){
console.log('on')
})
// e.trigger('click', '666')
console.log(e)
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- EventEmitter(事件触发器)

+

文章作者:

+

发布时间:2020年03月24日 - 15:45

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-EventEmitter/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/03/24/do-it-yourselfery-async-await/index.html b/2020/03/24/do-it-yourselfery-async-await/index.html new file mode 100644 index 0000000000..81bb77a359 --- /dev/null +++ b/2020/03/24/do-it-yourselfery-async-await/index.html @@ -0,0 +1,584 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- async、await | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- async、await +

+ + +
+ + + + +
+ + +

原理

就是利用 generator (生成器)分割代码片段。然后我们使用一个函数让其自迭代,每一个 yieldpromise 包裹起来。执行下一步的时机由 promise 来控制

+

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function _asyncToGenerator(fn) {
return function() {
var self = this, args = arguments;
return new Promise(function(resolve, reject) { // 将返回值 promise 化
var gen = fn.apply(self, args); // 获取迭代器实例
function _next(value) { // 执行下一步
asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
}
function _throw(err) { // 抛出异常
asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
}
_next(undefined); // 第一次触发
});
};
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- async、await

+

文章作者:

+

发布时间:2020年03月24日 - 15:50

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-async-await/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/03/24/do-it-yourselfery-call-apply/index.html b/2020/03/24/do-it-yourselfery-call-apply/index.html new file mode 100644 index 0000000000..dad589dffd --- /dev/null +++ b/2020/03/24/do-it-yourselfery-call-apply/index.html @@ -0,0 +1,618 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- call、apply、bind | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- call、apply、bind +

+ + +
+ + + + +
+ + +

原版

先来看一个call实例,看看call到底做了什么:

+
1
2
3
4
5
6
7
let foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call(foo); // 1
+

从代码的执行结果,我们可以看到,call首先改变了this的指向,使函数的this指向了foo,然后使bar函数执行了。
总结一下:

+
    +
  • call改变函数this指向
  • +
  • 调用函数
  • +
+

自己动手

    +
  1. 首先我们对参数 context 做了兼容处理,不传值, context 默认值为 window
  2. +
  3. 然后我们将函数挂载到 context 上面, context.fn = this
  4. +
  5. 处理参数,将传入 myCall 的参数截取,去除第一位,然后转为数组;
  6. +
  7. 调用 context.fn ,此时 fnthis 指向 context
  8. +
  9. 删除对象上的属性 delete context.fn
  10. +
  11. 将结果返回。
  12. +
+
1
2
3
4
5
6
7
8
Function.prototype.myCall = function(context) {
context = context || window;
context.fn = this; // 将函数挂载到对象的fn属性上
const args = [...arguments].slice(1); // 处理传入的参数
const result = context.fn(...args); // 通过对象的属性调用该方法
delete context.fn; // 删除该属性
return result // 返回结果
};
+

applycall 的区别在于参数, 其他没有差别,实现如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// myApply的参数形式为(obj,[arg1,arg2,arg3]);
// 所以myApply的第二个参数为[arg1,arg2,arg3]
// 这里我们用扩展运算符来处理一下参数的传入方式
Function.prototype.myApply = function(context) {
context = context || window
context.fn = this
let result
if (arguments[1]) { // 判断是否有第二个参数
result = context.fn(…arguments[1]) // 有的话传入执行
} else {
result = context.fn() // 没有的话空参执行
}
delete context.fn;
return result
};
+

bindcallapply 作用都是改变 this 的指向,区别在于 bind 改变后不会立即执行,而 callapply 会立即执行,我们看一下 bind 的用法

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(){
this.name="zs";
this.age=18;
this.gender="男"
}
let obj={
hobby:"看书"
}

let changePerson = Person.bind(obj); // 将构造函数的this绑定为obj
changePerson(); // 直接调用构造函数,函数会操作obj对象,给其添加三个属性;
console.log(obj); // => {hobby:"看书",name:"zs",age:18,gender:'男'}
let p = new changePerson(); // 用改变了this 指向的构造函数,new一个实例出来
console.log(p); // => Person {name:"zs",age:18,gender:'男'}
+

仔细观察上面的代码,再看输出结果。

+

我们对 Person 类使用了 bind 将其 this 指向 obj ,得到了 changePerson 函数,此处如果我们直接调用 changeperson 会改变 obj ,若用 new 调用 changeperson 会得到实例 p,并且其 __proto__ 指向 Person ,我们发现 bind 失效了。

+

我们得到结论:bind 改变了 this 指向的函数,如果用 new 操作符来调用, bind 将会失效

+

这个对象就是这个构造函数的实例,那么只要在函数内部执行 * this instanceof 构造函数 * 来判断其结果是否为 true ,就能判断函数是否是通过 new 操作符来调用了,若结果为 true 则是用 new 操作符调用的,总结如下:

+
    +
  1. 保存当前 this 指向
  2. +
  3. 保存环境上下文
  4. +
  5. 保存参数,去掉第一个对象参数
  6. +
  7. 返回待执行函数
      +
    1. 数组化剩余参数
    2. +
    3. 判断是否为构造函数
    4. +
    5. 若是执行构造函数,若不是改变 this 指向执行
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      // bind实现

      Function.prototype.myBind = function(context){
      let _this = this; // 1、保存函数
      context = context || window; // 2、保存目标对象
      let rest = [...arguments].slice(1); // 3、保存目标对象之外的参数,将其转化为数组;
      // 此处开始与 call 和 apply 不同,不是返回结果,而是返回一个函数
      return function F(){ // 4、返回一个待执行的函数
      let rest2 = Array.prototype.slice.call(arguments) // 5、这里的arguments是F函数的参数,转换为数组;
      if(this instanceof F){
      return new _this(...rest2) // 6、若是用new操作符调用,则直接用new 调用原函数,并用扩展运算符传递参数
      }else{
      _this.apply(context,rest.concat(rest2)); // 7、用apply调用第一步保存的函数,并绑定this,传递合并的参数数组,
      // 即context._this(rest.concat(rest2))
      }
      }
      };
    6. +
    +
  8. +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- call、apply、bind

+

文章作者:

+

发布时间:2020年03月24日 - 15:54

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-call-apply/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/03/24/do-it-yourselfery-create/index.html b/2020/03/24/do-it-yourselfery-create/index.html new file mode 100644 index 0000000000..ef9daf56f3 --- /dev/null +++ b/2020/03/24/do-it-yourselfery-create/index.html @@ -0,0 +1,588 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- Object.create() | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- Object.create() +

+ + +
+ + + + +
+ + +

实现一个 Object.create() 方法

    +
  1. 创建一个空匿名函数
  2. +
  3. 函数原型对象指向传入对象实例
  4. +
  5. 返回构造函数创建的实例
  6. +
+
1
2
3
4
5
function create =  function (o) {
var F = function () {};
F.prototype = o;
return new F();
};
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- Object.create()

+

文章作者:

+

发布时间:2020年03月24日 - 14:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-create/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/03/24/do-it-yourselfery-flat/index.html b/2020/03/24/do-it-yourselfery-flat/index.html new file mode 100644 index 0000000000..81d512f9ca --- /dev/null +++ b/2020/03/24/do-it-yourselfery-flat/index.html @@ -0,0 +1,587 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- Array.prototype.flat()函数 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- Array.prototype.flat()函数 +

+ + +
+ + + + +
+ + +

原理

将多层数组扁平化

+

实现

1
2
3
4
5
6
7
8
9
10
11
Array.prototype.myFlat = function() {
var arr = [];
this.forEach((item)=>{
if(Array.isArray(item)){
arr = arr.concat(item.myFlat()); // 如果是数组的话继续循环
}else{
arr.push(item)
}
})
return arr
};
+ +

还有另外一种实现方式,非常好用

+
1
2
3
4
5
Array.prototype.myFlat = function() {
return this.toString() // => "1,2,3,4"
.split(",") // => ["1", "2", "3", "4"]
.map(item => +item); // => [1, 2, 3, 4]
};
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- Array.prototype.flat()函数

+

文章作者:

+

发布时间:2020年03月24日 - 13:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-flat/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/03/24/do-it-yourselfery-isArray/index.html b/2020/03/24/do-it-yourselfery-isArray/index.html new file mode 100644 index 0000000000..bf9af9bca2 --- /dev/null +++ b/2020/03/24/do-it-yourselfery-isArray/index.html @@ -0,0 +1,584 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- Array.isArray | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- Array.isArray +

+ + +
+ + + + +
+ + +

实现一个Array.isArray

思路很简单,就是利用 Object.prototype.toString

+
1
2
3
Array.myIsArray = function(o) { 
return Object.prototype.toString.call(Object(o)) === '[object Array]';
};
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- Array.isArray

+

文章作者:

+

发布时间:2020年03月24日 - 13:30

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-isArray/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/03/24/do-it-yourselfery-map/index.html b/2020/03/24/do-it-yourselfery-map/index.html new file mode 100644 index 0000000000..d264e374b6 --- /dev/null +++ b/2020/03/24/do-it-yourselfery-map/index.html @@ -0,0 +1,589 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- Array.prototype.map() | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- Array.prototype.map() +

+ + +
+ + + + +
+ + +

原理

先看看 reducemap 的使用方法

+
1
2
let new_array = arr.map(function callback(currentValue[, index[,array) {/* Return element for new_array */ }[, thisArg])
let result = arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
+ +

实现

第一种用 for 实现

+
1
2
3
4
5
6
7
Array.prototype.myMap = function(callback, thisArg) {
let arr = [];
for (let i = 0; i < this.length; i++) {
arr.push(callback.call(thisArg, this[i], i, this));
}
return arr;
};
+

第二种用 reduce 实现

+
1
2
3
4
5
6
7
Array.prototype.myMap = function(callback, thisArg) {
let result = this.reduce((accumulator, currentValue, index, array) => {
accumulator.push(callback.call(thisArg, currentValue, index, array));
return accumulator;
}, []);
return result;
};
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- Array.prototype.map()

+

文章作者:

+

发布时间:2020年03月24日 - 15:20

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-map/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/03/24/do-it-yourselfery-new/index.html b/2020/03/24/do-it-yourselfery-new/index.html new file mode 100644 index 0000000000..8ed51feb7f --- /dev/null +++ b/2020/03/24/do-it-yourselfery-new/index.html @@ -0,0 +1,591 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- new | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- new +

+ + +
+ + + + +
+ + +

实现一个new操作符

我们首先知道new做了什么:

+
    +
  1. 创建一个空的简单 JavaScript 对象(即{})
  2. +
  3. 链接该对象(即设置该对象的构造函数)到另一个对象
  4. +
  5. 将步骤(1)新创建的对象作为 this 的上下文
  6. +
  7. 如果该函数没有返回对象,则返回 this
  8. +
+

知道new做了什么,接下来我们就来实现它

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function create(){
// 创建一个空的对象
let obj = {};
// 获得构造函数
let Con = [].shift.call(arguments)
// 将空对象指向构造函数的原型链
Object.setPrototypeOf(obj, Con.prototype);
// obj.__proto__ = Con.prototype // 链接到原型
// obj绑定到构造函数上,便可以访问构造函数中的属性,即this.obj.Con(args)
let result = Con.apply(obj, arguments);
// 如果返回的result是一个对象则返回
// new方法失效,否则返回obj
return result instanceof Object ? result : this.obj;
// return typeof result === 'object' ? result : obj// 确保 new 出来的是个对象
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- new

+

文章作者:

+

发布时间:2020年03月24日 - 04:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-new/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/03/24/do-it-yourselfery-reduce/index.html b/2020/03/24/do-it-yourselfery-reduce/index.html new file mode 100644 index 0000000000..af137921ee --- /dev/null +++ b/2020/03/24/do-it-yourselfery-reduce/index.html @@ -0,0 +1,587 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- Array.prototype.reduce | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- Array.prototype.reduce +

+ + +
+ + + + +
+ + +

原版

1
Array.prototype.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
+ +

自己动手

1
2
3
4
5
6
7
8
Array.prototype.myReduce = function(callback, initialValue) {
let accumulator = initialValue ? initialValue : this[0]; // 第一次使用判断时候有initialValue参数,如果有用他,没有用this[0],这里this指的是传入的数组,this[0]就是数组第一项
for (let i = initialValue ? 0 : 1; i < this.length; i++) { // 如果有初始值从0开始循环,不然从1开始
let _this = this; // 保留当前this指向
accumulator = callback(accumulator, this[i], i, _this); //
}
return accumulator; // 返回迭代器的终值
};
+

试用一下

+
1
2
3
4
5
6
7
let arr = [1, 2, 3, 4];
let sum = arr.myReduce((acc, val) => {
acc += val;
return acc;
}, 5);

console.log(sum); // 15
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- Array.prototype.reduce

+

文章作者:

+

发布时间:2020年03月24日 - 04:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-reduce/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2020/03/24/do-it-yourselfery-\344\272\213\344\273\266\344\273\243\347\220\206/index.html" "b/2020/03/24/do-it-yourselfery-\344\272\213\344\273\266\344\273\243\347\220\206/index.html" new file mode 100644 index 0000000000..00214b242e --- /dev/null +++ "b/2020/03/24/do-it-yourselfery-\344\272\213\344\273\266\344\273\243\347\220\206/index.html" @@ -0,0 +1,583 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- 事件代理 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- 事件代理 +

+ + +
+ + + + +
+ + +

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<ul id="color-list">
<li>red</li>
<li>yellow</li>
<li>blue</li>
<li>green</li>
<li>black</li>
<li>white</li>
</ul>

<script>
(function () {
var color_list = document.getElementById('color-list');
color_list.addEventListener('click', showColor, true);
function showColor(e) {
var x = e.target;
if (x.nodeName.toLowerCase() === 'li') {
alert(x.innerHTML);
}
}
})();
</script>
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- 事件代理

+

文章作者:

+

发布时间:2020年03月24日 - 14:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-%E4%BA%8B%E4%BB%B6%E4%BB%A3%E7%90%86/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2020/03/24/do-it-yourselfery-\345\217\214\345\220\221\347\273\221\345\256\232/index.html" "b/2020/03/24/do-it-yourselfery-\345\217\214\345\220\221\347\273\221\345\256\232/index.html" new file mode 100644 index 0000000000..dce42dc47f --- /dev/null +++ "b/2020/03/24/do-it-yourselfery-\345\217\214\345\220\221\347\273\221\345\256\232/index.html" @@ -0,0 +1,585 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- Vue双向绑定 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- Vue双向绑定 +

+ + +
+ + + + +
+ + +

Vue 2.x 的 Object.defineProperty 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 数据
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');

// 数据劫持
Object.defineProperty(data, 'text', {
// 数据变化 —> 修改视图
set(newVal) {
input.value = newVal;
span.innerHTML = newVal;
}
});

// 视图更改 --> 数据变化
input.addEventListener('keyup', function(e) {
data.text = e.target.value;
});
+ +

Vue 3.x 的 proxy 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 数据
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');

// 数据劫持
const handler = {
set(target, key, value) {
target[key] = value;
// 数据变化 —> 修改视图
input.value = value;
span.innerHTML = value;
return value;
}
};
const proxy = new Proxy(data, handler);

// 视图更改 --> 数据变化
input.addEventListener('keyup', function(e) {
proxy.text = e.target.value;
});
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- Vue双向绑定

+

文章作者:

+

发布时间:2020年03月24日 - 15:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-%E5%8F%8C%E5%90%91%E7%BB%91%E5%AE%9A/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/03/24/webpack-learning-1/index.html b/2020/03/24/webpack-learning-1/index.html new file mode 100644 index 0000000000..3a4b5f107c --- /dev/null +++ b/2020/03/24/webpack-learning-1/index.html @@ -0,0 +1,877 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2020年了,再不会webpack敲得代码就不香了(近万字实战)【转载】 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 2020年了,再不会webpack敲得代码就不香了(近万字实战)【转载】 +

+ + +
+ + + + +
+ + +

推荐序

这里是我自己写的,看了这篇文章把我零碎的 webpack 知识系统的整理了一下,感觉受益匪浅,推荐更多小伙伴看一看这篇文章,好文共享,建议稍微对 webpack 了解服用更佳。
2020年了,再不会webpack敲得代码就不香了(近万字实战)

+

前言

2020年即将到来,在众多前端的招聘要求里, webpack 、工程化这些字眼频率越来越高。日常开发者中,我们常常在用诸如 vue-clicreate-react-app 的脚手架来构建我们的项目。但是如果你想在团队脱颖而出(鹤立鸡群)、拿到更好的 offer (还房贷),那么你必须去深刻的认识下我们经常打交道的 webpack

+

入门(一起来用这些小例子让你熟悉webpack的配置)

webpack 是什么?

webpack 是一个现代 JavaScript 应用程序的静态模块打包器,当 webpack 处理应用程序时,会递归构建一个依赖关系图,其中包含应用程序需要的每个模块,然后将这些模块打包成一个或多个 bundle

+

webpack 的核心概念

    +
  • entry: 入口
  • +
  • output: 输出
  • +
  • loader: 模块转换器,用于把模块原内容按照需求转换成新内容
  • +
  • plugins: 扩展插件,在 webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要做的事情
  • +
+

初始化项目

新建一个目录,初始化 npm

+
1
npm init
+ +

webpack 是运行在 node 环境中的,我们需要安装以下两个 npm

+
1
npm i -D webpack webpack-cli
+ +
+
    +
  • npm i -Dnpm install --save-dev 的缩写
  • +
  • npm i -Snpm install --save 的缩写
  • +
+
+

新建一个文件夹 src ,然后新建一个文件 main.js ,写一点代码测试一下

+
1
console.log('call me 老yuan')
+ +

配置 package.json 命令

+
1
2
3
"script":{
"build":"webpack src/main.js"
}
+

执行

+
1
npm run build
+

此时如果生成了一个 dist 文件夹,并且内部含有 main.js 说明已经打包成功了

+

开始我们自己的配置

上面一个简单的例子只是 webpack 自己默认的配置,下面我们要实现更加丰富的自定义配置

+

新建一个 build 文件夹,里面新建一个 webpack.config.js

+
1
2
3
4
5
6
7
8
9
10
11
// webpack.config.js

const path = require('path');
module.exports = {
mode:'development', // 开发模式
entry: path.resolve(__dirname,'../src/main.js'), // 入口文件
output: {
filename: 'output.js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
}
}
+

更改我们的打包命令

+
1
2
3
"script":{
"build":"webpack build/webpack.config.js"
}
+

执行 npm run build
会发现生成了以下目录

+
1
2
3
4
project
dist
build
src
+

其中 dist 文件夹中的 main.js 就是我们需要在浏览器中实际运行的文件
当然实际运用中不会仅仅如此,下面让我们通过实际案例带你快速入手 webpack

+

配置html模板

js 文件打包好了,但是我们不可能每次在html文件中手动引入打包好的js

+
+

这里可能有的朋友会认为我们打包 js 文件名称不是一直是固定的嘛( output.js )?这样每次就不用改动引入文件名称了呀?实际上我们日常开发中往往会这样配置:

+
+
1
2
3
4
5
6
7
module.exports = {
// 省略其他配置
output: {
filename: '[name].[hash:8].js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
}
}
+ +

这时候生成的 dist 目录文件如下

+
1
2
dist/
app.fsafasf.js
+

为了缓存,你会发现打包好的 js 文件的名称每次都不一样。 webpack 打包出来的js文件我们需要引入到 html 中,但是每次我们都手动修改 js 文件名显得很麻烦,因此我们需要一个插件来帮我们完成这件事情,那就是 html-webpack-plugin

+
1
npm i -D html-webpack-plugin
+

新建一个 build 同级的文件夹 public ,里面新建一个 index.html
具体配置文件如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin') // **此插件用来解决html模板引入js文件
module.exports = {
mode:'development', // 开发模式
entry: path.resolve(__dirname,'../src/main.js'), // 入口文件
output: {
filename: '[name].[hash:8].js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
},
plugins:[
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html')
})
]
}
+

可以发现打包生成的js文件已经被自动引入 html 文件中

+

多入口文件如何开发

+

生成多个 html-webpack-plugin 实例来解决这个问题

+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode:'development', // 开发模式
entry: {
main:path.resolve(__dirname,'../src/main.js'),
header:path.resolve(__dirname,'../src/header.js')
},
output: {
filename: '[name].[hash:8].js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
},
plugins:[
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html'),
filename:'index.html',
chunks:['main'] // 与入口文件对应的模块名
}),
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/header.html'),
filename:'header.html',
chunks:['header'] // 与入口文件对应的模块名
}),
]
}
+ +

clean-webpack-plugin

+

每次执行 npm run build 会发现 dist 文件夹里会残留上次打包的文件,这里我们推荐一个 plugin 来帮我们在打包输出前清空文件夹 clean-webpack-plugin

+
+
1
2
3
4
5
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
module.exports = {
// ...省略其他配置
plugins:[new CleanWebpackPlugin()]
}
+
希望dist目录下某个文件夹不被清空

不过呢,有些时候,我们并不希望整个 dist 目录都被清空,比如,我们不希望,每次打包的时候,都删除 dll 目录,以及 dll 目录下的文件或子目录,该怎么办呢?

+

clean-webpack-plugin 为我们提供了参数 cleanOnceBeforeBuildPatterns

+
1
2
3
4
5
6
7
8
9
//webpack.config.js
module.exports = {
//...
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns:['**/*', '!dll', '!dll/**'] //不删除dll目录下的文件
})
]
}
+ +

引用CSS

我们的入口文件是 js ,所以我们在入口 js 中引入我们的 css 文件

+
1
import 'asset/style.css'
+

同时我们也需要一些 loader 来解析我们的 css 文件

+
1
npm i -D style-loader css-loader
+

如果我们使用 less 来构建样式,则需要多安装两个

+
1
npm i -D less less-loader
+

配置文件如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.config.js
module.exports = {
// ...省略其他配置
module:{
rules:[
{
test:/\.css$/,
use:['style-loader','css-loader'] // 从右向左解析原则
},
{
test:/\.less$/,
use:['style-loader','css-loader','less-loader'] // 从右向左解析原则
}
]
}
}
+ +

我们简单说一下上面的配置:

+
    +
  • style-loader 动态创建 style 标签,将 css 插入到 head 中.
  • +
  • css-loader 负责处理 @import 等语句。
  • +
  • postcss-loaderautoprefixer ,自动生成浏览器兼容性前缀 —— 2020了,应该没人去自己徒手去写浏览器前缀了吧
  • +
  • less-loader 负责处理编译 .less 文件,将其转为 css
  • +
+
+

注意:
loader 的执行顺序是从右向左执行的,也就是后面的 loader 先执行,上面 loader 的执行顺序为: less-loader —> postcss-loader —> css-loader —> style-loader
当然,loader 其实还有一个参数,可以修改优先级,enforce 参数,其值可以为: pre(优先执行) 或 post (滞后执行)。
现在,我们已经可以处理 .less 文件啦,.css 文件只需要修改匹配规则,删除 less-loader 即可。

+
+

为css添加浏览器前缀

1
2
npm i -D postcss-loader autoprefixer

+

配置如下

+
1
2
3
4
5
6
7
8
9
// webpack.config.js
module.exports = {
module:{
rules:[
test/\.less$/,
use:['style-loader','css-loader','postcss-loader','less-loader'] // 从右向左解析原则
]
}
}
+

接下来,我们还需要引入 autoprefixer 使其生效,这里有两种方式

+
在项目根目录下创建一个postcss.config.js文件,配置如下:
1
2
3
module.exports = {
plugins: [require('autoprefixer')] // 引用该插件即可了
}
+
直接在webpack.config.js里配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// webpack.config.js
module.exports = {
//...省略其他配置
module:{
rules:[{
test:/\.less$/,
use:['style-loader','css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
},'less-loader'] // 从右向左解析原则
}]
}
}
+

这时候我们发现 css 通过 style 标签的方式添加到了 html 文件中,但是如果样式文件很多,全部添加到 html 中,难免显得混乱。这时候我们想用把 css 拆分出来用外链的形式引入 css 文件怎么做呢?这时候我们就需要借助插件来帮助我们

+

拆分css

1
npm i -D mini-css-extract-plugin
+
+

webpack 4.0 以前,我们通过 extract-text-webpack-plugin 插件,把 css 样式从 js 文件中提取到单独的 css 文件中。 webpack4.0 以后,官方推荐使用 mini-css-extract-plugin 插件来打包 css 文件

+
+

配置文件如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
//...省略其他配置
module: {
rules: [
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader'
],
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].[hash].css",
chunkFilename: "[id].css",
})
]
}
+

拆分多个css

+

这里需要说的细一点,上面我们所用到的 mini-css-extract-plugin 将所有的 css 样式合并为一个 css 文件。如果你想拆分为一一对应的多个 css 文件,我们需要使用到 extract-text-webpack-plugin ,而目前 mini-css-extract-plugin 还不支持此功能。我们需要安装 @next 版本的 extract-text-webpack-plugin

+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
npm i -D extract-text-webpack-plugin@next
// webpack.config.js

const path = require('path');
const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')
let indexLess = new ExtractTextWebpackPlugin('index.less');
let indexCss = new ExtractTextWebpackPlugin('index.css');
module.exports = {
module:{
rules:[
{
test:/\.css$/,
use: indexCss.extract({
use: ['css-loader']
})
},
{
test:/\.less$/,
use: indexLess.extract({
use: ['css-loader','less-loader']
})
}
]
},
plugins:[
indexLess,
indexCss
]
}
+

打包 图片、字体、媒体、等文件

file-loader 就是将文件在进行一些处理后(主要是处理文件名和路径、解析文件 url ),并将文件移动到输出的目录中
url-loader 一般与 file-loader 搭配使用,功能与 file-loader 类似,如果文件小于限制的大小。则会返回 base64 编码,否则使用 file-loader 将文件移动到输出的目录中

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// webpack.config.js
module.exports = {
// 省略其它配置 ...
module: {
rules: [
// ...
{
test: /\.(jpe?g|png|gif)$/i, //图片文件
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]'
}
}
}
}
]
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, //媒体文件
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'media/[name].[hash:8].[ext]'
}
}
}
}
]
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i, // 字体
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'fonts/[name].[hash:8].[ext]'
}
}
}
}
]
},
]
}
}
+

用babel转义js文件

为了使我们的 js 代码兼容更多的环境我们需要安装依赖

+
1
2
npm i babel-loader @babel/preset-env @babel/core

+
+

注意
babel-loaderbabel-core 的版本对应关系

+
+
    +
  • babel-loader 8.x 对应 babel-core 7.x
  • +
  • babel-loader 7.x 对应 babel-core 6.x
  • +
+

配置如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// webpack.config.js
module.exports = {
// 省略其它配置 ...
module:{
rules:[
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:['@babel/preset-env']
}
},
exclude:/node_modules/
},
]
}
}
+

上面的 babel-loader 只会将 ES6/7/8 语法转换为 ES5 语法,但是对新 api 并不会转换 例如( promiseGeneratorSetMapsProxy 等)
此时我们需要借助 babel-polyfill 来帮助我们转换

+
1
2
3
4
5
6
npm i @babel/polyfill
// webpack.config.js
const path = require('path')
module.exports = {
entry: ["@babel/polyfill,path.resolve(__dirname,'../src/index.js')"], // 入口文件
}
+
+

手动把上面的 demo 敲一遍对阅读下面的文章更有益,建议入门的同学敲三遍以上

+
+

上面的实践是我们对 webpack 的功能有了一个初步的了解,但是要想熟练应用于开发中,我们需要一个系统的实战。让我们一起摆脱脚手架尝试自己搭建一个 vue 开发环境

+

搭建vue开发环境

上面的小例子已经帮助而我们实现了打包 css 、图片、 jshtml 等文件。
但是我们还需要以下几种配置

+

解析.vue文件

1
2
npm i -D vue-loader vue-template-compiler vue-style-loader
npm i -S vue
+
    +
  • vue-loader 用于解析 .vue 文件
  • +
  • vue-template-compiler 用于编译模板
  • +
+

配置如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const vueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
module:{
rules:[{
test:/\.vue$/,
use:['vue-loader']
},]
},
resolve:{
alias:{
'vue$':'vue/dist/vue.runtime.esm.js',
' @':path.resolve(__dirname,'../src')
},
extensions:['*','.js','.json','.vue']
},
plugins:[
new vueLoaderPlugin()
]
}
+

配置webpack-dev-server进行热更新

1
npm i -D webpack-dev-server
+

配置如下

+
1
2
3
4
5
6
7
8
9
10
11
12
const Webpack = require('webpack')
module.exports = {
// ...省略其他配置
devServer:{
port:3000,
hot:true,
contentBase:'../dist'
},
plugins:[
new Webpack.HotModuleReplacementPlugin()
]
}
+

完整配置如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// webpack.config.js
const path = require('path');
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')
const vueLoaderPlugin = require('vue-loader/lib/plugin')
const Webpack = require('webpack')
module.exports = {
mode:'development', // 开发模式
entry: {
main:path.resolve(__dirname,'../src/main.js'),
},
output: {
filename: '[name].[hash:8].js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
},
module:{
rules:[
{
test:/\.vue$/,
use:['vue-loader']
},
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:[
['@babel/preset-env']
]
}
}
},
{
test:/\.css$/,
use: ['vue-style-loader','css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
}]
},
{
test:/\.less$/,
use: ['vue-style-loader','css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
},'less-loader']
}
]
},
resolve:{
alias:{
'vue$':'vue/dist/vue.runtime.esm.js',
' @':path.resolve(__dirname,'../src')
},
extensions:['*','.js','.json','.vue']
},
devServer:{
port:3000,
hot:true,
contentBase:'../dist'
},
plugins:[
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html'),
filename:'index.html'
}),
new vueLoaderPlugin(),
new Webpack.HotModuleReplacementPlugin()
]
}
+

配置打包命令

1
2
3
4
"script":{
"dev":"webpack-dev-server --config build/webpack.config.js --open",
"build":"webpack --config build/webpack.config.js"
}
+

打包文件已经配置完毕,接下来让我们测试一下
首先在 src 新建一个 main.js

+
1
2
3
4
5
6
// main.js
import Vue from 'vue'
import App from './app'
new Vue({
render:h=>h(App)
}).$mount('#app')
+

新建一个 App.vue

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// App.vue
<template>
<div id='container'></div>
</template>
<script>
export default {
data(){
return {
initData:''
}
}
}
</script>
<style scoped>
#container{
width:100%;
height:100%;
}
</style>
+

新建一个 public 文件夹,里面新建一个 index.html

+
1
2
3
4
5
6
7
8
9
10
11
12
13
// index.html
<!DOCTYPE html>
<html lang='en'>
<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'>
<title>lao li</title>
</head>
<body>
<div id='app'></div>
</body>
</html>
+

执行 npm run dev 这时候如果浏览器出现 Vue 开发环境运行成功,那么恭喜你,已经成功迈出了第一步

+

区分开发环境与生产环境

实际应用到项目中,我们需要区分开发环境与生产环境,我们在原来 webpack.config.js 的基础上再新增两个文件

+

webpack.dev.js 开发环境配置文件
开发环境主要实现的是热更新,不要压缩代码,完整的 sourceMap
webpack.prod.js生产环境配置文件
生产环境主要实现的是压缩代码、提取 css 文件、合理的 sourceMap 、分割代码
需要安装以下模块:

+
1
npm i -D  webpack-merge copy-webpack-plugin optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin
+
    +
  • webpack-merge 合并配置
  • +
  • copy-webpack-plugin 拷贝静态资源
  • +
  • optimize-css-assets-webpack-plugin 压缩 css
  • +
  • uglifyjs-webpack-plugin 压缩js
  • +
+
+

webpack mode 设置 production 的时候会自动压缩 js 代码。
原则上不需要引入 uglifyjs-webpack-plugin 进行重复工作。
但是 optimize-css-assets-webpack-plugin 压缩 css 的同时会破坏原有的 js 压缩,所以这里我们引入 uglifyjs 进行压缩

+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// webpack.config.js
const path = require('path')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const vueLoaderPlugin = require('vue-loader/lib/plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const devMode = process.argv.indexOf('--mode=production') === -1;
module.exports = {
entry:{
main:path.resolve(__dirname,'../src/main.js')
},
output:{
path:path.resolve(__dirname,'../dist'),
filename:'js/[name].[hash:8].js',
chunkFilename:'js/[name].[hash:8].js'
},
module:{
rules:[
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:['@babel/preset-env']
}
},
exclude:/node_modules/
},
{
test:/\.vue$/,
use:['cache-loader','thread-loader',{
loader:'vue-loader',
options:{
compilerOptions:{
preserveWhitespace:false
}
}
}]
},
{
test:/\.css$/,
use:[{
loader: devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
options:{
publicPath:"../dist/css/",
hmr:devMode
}
},'css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
}]
},
{
test:/\.less$/,
use:[{
loader:devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
options:{
publicPath:"../dist/css/",
hmr:devMode
}
},'css-loader','less-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
}]
},
{
test:/\.(jep?g|png|gif)$/,
use:{
loader:'url-loader',
options:{
limit:10240,
fallback:{
loader:'file-loader',
options:{
name:'img/[name].[hash:8].[ext]'
}
}
}
}
},
{
test:/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
use:{
loader:'url-loader',
options:{
limit:10240,
fallback:{
loader:'file-loader',
options:{
name:'media/[name].[hash:8].[ext]'
}
}
}
}
},
{
test:/\.(woff2?|eot|ttf|otf)(\?.*)?$/i,
use:{
loader:'url-loader',
options:{
limit:10240,
fallback:{
loader:'file-loader',
options:{
name:'media/[name].[hash:8].[ext]'
}
}
}
}
}
]
},
resolve:{
alias:{
'vue$':'vue/dist/vue.runtime.esm.js',
' @':path.resolve(__dirname,'../src')
},
extensions:['*','.js','.json','.vue']
},
plugins:[
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html')
}),
new vueLoaderPlugin(),
new MiniCssExtractPlugin({
filename: devMode ? '[name].css' : '[name].[hash].css',
chunkFilename: devMode ? '[id].css' : '[id].[hash].css'
})
]
}
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// webpack.dev.js
const Webpack = require('webpack')
const webpackConfig = require('./webpack.config.js')
const WebpackMerge = require('webpack-merge')

module.exports = WebpackMerge(webpackConfig,{
mode:'development',
devtool:'cheap-module-eval-source-map',
devServer:{
port:3000,
hot:true,
contentBase:'../dist'
},
plugins:[
new Webpack.HotModuleReplacementPlugin()
]
})
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// webpack.prod.js
const path = require('path')
const webpackConfig = require('./webpack.config.js')
const WebpackMerge = require('webpack-merge')

const CopyWebpackPlugin = require('copy-webpack-plugin')
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

module.exports = WebpackMerge(webpackConfig,{
mode:'production',
devtool:'cheap-module-source-map',
plugins:[
new CopyWebpackPlugin([{
from:path.resolve(__dirname,'../public'),
to:path.resolve(__dirname,'../dist')
}]),
],
optimization:{
minimizer:[
new UglifyJsPlugin({//压缩js
cache:true,
parallel:true,
sourceMap:true
}),
new OptimizeCssAssetsPlugin({})
],
splitChunks:{
chunks:'all',
cacheGroups:{
libs: {
name: "chunk-libs",
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: "initial" // 只打包初始时依赖的第三方
}
}
}
}
})
+ +

优化webpack配置

看到这里你或许有些累了,但是要想获取更好的offer,更高的薪水,下面必须继续深入

+

优化配置对我们来说非常有实际意义,这实际关系到你打包出来文件的大小,打包的速度等。
具体优化可以分为以下几点:

+

优化打包速度

+

构建速度指的是我们每次修改代码后热更新的速度以及发布前打包文件的速度。

+
+

合理的配置 mode 参数与 devtool 参数

devtool 可设置的值
mode 可设置 development production 两个参数

+

如果没有设置, webpack4 会将 mode 的默认值设置为 production

+
    +
  • production :将 process.env.NODE_ENV 的值设置为 production ,启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPluginUglifyJsPlugin,会进行 tree shaking (去除无用代码)和 uglifyjs (代码压缩混淆)
  • +
  • development :将 process.env.NODE_ENV 的值设置为 development ,启用 NamedChunksPluginNamedModulesPlugin
  • +
+

缩小文件的搜索范围(配置include exclude alias noParse extensions)

    +
  • alias 当我们代码中出现 import ‘vue’时, webpack会采用向上递归搜索的方式去node_modules 目录下找。为了减少搜索范围我们可以直接告诉webpack去哪个路径下查找。也就是别名(alias)的配置。
  • +
  • include exclude 同样配置include exclude也可以减少webpack loader的搜索转换时间。
  • +
  • noParse 当我们代码中使用到 import jq from 'jquery' 时, webpack 会去解析 jq 这个库是否有依赖其他的包。但是我们对类似 jquery 这类依赖库,一般会认为不会引用其他的包(特殊除外,自行判断)。增加 noParse 属性,告诉 webpack 不必解析,以此增加打包速度。
  • +
  • extensions webpack 会根据 extensions 定义的后缀查找文件(频率较高的文件类型优先写在前面)
  • +
+

配图

+

使用HappyPack开启多进程Loader转换

+

webpack 构建过程中,实际上耗费时间大多数用在 loader 解析转换以及代码的压缩中。日常开发中我们需要使用 Loaderjscss ,图片,字体等文件做转换操作,并且转换的文件数据量也是非常大。由于js单线程的特性使得这些转换操作不能并发处理文件,而是需要一个个文件进行处理。 HappyPack 的基本原理是将这部分任务分解到多个子进程中去并行处理,子进程处理完成后把结果发送到主进程中,从而减少总的构建时间

+
+
1
npm i -D happypack
+ +

happypack

+

使用 webpack-parallel-uglify-plugin 增强代码压缩

上面对于 loader 转换已经做优化,那么下面还有另一个难点就是优化代码的压缩时间。

+
1
npm i -D webpack-parallel-uglify-plugin
+

webpack-parallel-uglify-plugin

+

抽离第三方模块

+

对于开发项目中不经常会变更的静态依赖文件。类似于我们的 elementUivue 全家桶等等。因为很少会变更,所以我们不希望这些依赖要被集成到每一次的构建逻辑中去。 这样做的好处是每次更改我本地代码的文件的时候, webpack 只需要打包我项目本身的文件代码,而不会再去编译第三方库。以后只要我们不升级第三方包的时候,那么 webpack 就不会对这些库去打包,这样可以快速的提高打包的速度。

+
+

这里我们使用 webpack 内置的 DllPlugin DllReferencePlugin 进行抽离

+

在与 webpack 配置文件同级目录下新建 webpack.dll.config.js
代码如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// webpack.dll.config.js
const path = require("path");
const webpack = require("webpack");
module.exports = {
// 你想要打包的模块的数组
entry: {
vendor: ['vue','element-ui']
},
output: {
path: path.resolve(__dirname, 'static/js'), // 打包后文件输出的位置
filename: '[name].dll.js',
library: '[name]_library'
// 这里需要和webpack.DllPlugin中的`name: '[name]_library',`保持一致。
},
plugins: [
new webpack.DllPlugin({
path: path.resolve(__dirname, '[name]-manifest.json'),
name: '[name]_library',
context: __dirname
})
]
};
+

package.json 中配置如下命令

+
1
"dll": "webpack --config build/webpack.dll.config.js"
+ +

接下来在我们的 webpack.config.js 中增加以下代码

+
1
2
3
4
5
6
7
8
9
10
11
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./vendor-manifest.json')
}),
new CopyWebpackPlugin([ // 拷贝生成的文件到dist目录 这样每次不必手动去cv
{from: 'static', to:'static'}
]),
]
};
+

执行

+
1
npm run dll
+ +

会发现生成了我们需要的集合第三地方
代码的 vendor.dll.js
我们需要在html文件中手动引入这个js文件

+
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<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">
<title>老yuan</title>
<script src="static/js/vendor.dll.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
+

这样如果我们没有更新第三方依赖包,就不必 npm run dll 。直接执行 npm run dev npm run build 的时候会发现我们的打包速度明显有所提升。因为我们已经通过 dllPlugin 将第三方依赖包抽离出来了。

+

配置缓存

+

我们每次执行构建都会把所有的文件都重复编译一遍,这样的重复工作是否可以被缓存下来呢,答案是可以的,目前大部分 loader 都提供了 cache 配置项。比如在 babel-loader 中,可以通过设置cacheDirectory 来开启缓存,babel-loader?cacheDirectory=true 就会将每次的编译结果写进硬盘文件(默认是在项目根目录下的 node_modules/.cache/babel-loader 目录内,当然你也可以自定义)
但如果 loader 不支持缓存呢?我们也有方法,我们可以通过 cache-loader ,它所做的事情很简单,就是 babel-loader 开启 cache 后做的事情,将 loader 的编译结果写入硬盘缓存。再次构建会先比较一下,如果文件较之前的没有发生变化则会直接使用缓存。使用方法如官方 demo 所示,在一些性能开销较大的 loader 之前添加此 loader 即可

+
+
1
npm i -D cache-loader
+

cache-loader

+

优化打包文件体积

打包的速度我们是进行了优化,但是打包后的文件体积却是十分大,造成了页面加载缓慢,浪费流量等,接下来让我们从文件体积上继续优化

+

引入webpack-bundle-analyzer分析打包后的文件

webpack-bundle-analyzer 将打包后的内容束展示为方便交互的直观树状图,让我们知道我们所构建包中真正引入的内容

+
1
npm i -D webpack-bundle-analyzer
+

webpack-bundle-analyzer

+

接下来在 package.json 里配置启动命令

+
1
"analyz": "NODE_ENV=production npm_config_report=true npm run build"
+

windows 请安装 npm i -D cross-env

+
1
"analyz": "cross-env NODE_ENV=production npm_config_report=true npm run build"
+

接下来 npm run analyz 浏览器会自动打开文件依赖图的网页

+

externals

+

按照官方文档的解释,如果我们想引用一个库,但是又不想让 webpack 打包,并且又不影响我们在程序中以 CMDAMD 或者 window/global 全局等方式进行使用,那就可以通过配置 Externals 。这个功能主要是用在创建一个库的时候用的,但是也可以在我们项目开发中充分使用Externals 的方式,我们将这些不需要打包的静态资源从构建逻辑中剔除出去,而使用 CDN的方式,去引用它们。
有时我们希望我们通过 script 引入的库,如用 CDN 的方式引入的 jquery ,我们在使用时,依旧用 require 的方式来使用,但是却不希望 webpack 将它又编译进文件中。这里官网案例已经足够清晰明了,大家有兴趣可以点击了解
webpack
官网案例如下

+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
<script
src="https://code.jquery.com/jquery-3.1.0.js"
integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
crossorigin="anonymous">
</script>
module.exports = {
//...
externals: {
jquery: 'jQuery'
}
};
import $ from 'jquery';
$('.my-element').animate(/* ... */);
+

Tree-shaking

+

这里单独提一下 tree-shaking ,是因为这里有个坑。 tree-shaking 的主要作用是用来清除代码中无用的部分。目前在 webpack4 我们设置 modeproduction 的时候已经自动开启了tree-shaking 。但是要想使其生效,生成的代码必须是ES6模块。不能使用其它类型的模块如 CommonJS 之流。如果使用 Babel 的话,这里有一个小问题,因为 Babel 的预案( preset )默认会将任何模块类型都转译成 CommonJS 类型。修正这个问题也很简单,在 .babelrc 文件或在 webpack.config.js 文件中设置 modules:false 就好了

+
+
1
2
3
4
5
6
7
8
9
10
// .babelrc
{
"presets": [
["@babel/preset-env",
{
"modules": false
}
]
]
}
+

或者

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.config.js

module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', { modules: false }]
}
},
exclude: /(node_modules)/
}
]
}
+ +

经历过上面两个系列的洗礼,到现在我们成为了一名合格的 webpack 配置工程师。但是光拧螺丝,自身的可替代性还是很高,下面我们将深入 webpack 的原理中去

+

手写webpack系列

经历过上面两个部分,我们已经可以熟练的运用相关的 loaderplugin 对我们的代码进行转换、解析。接下来我们自己手动实现 loaderplugin ,使其在平时的开发中获得更多的乐趣。

+

手写 webpack loader

+

loader 从本质上来说其实就是一个 node 模块。相当于一台榨汁机( loader )将相关类型的文件代码( code )给它。根据我们设置的规则,经过它的一系列加工后还给我们加工好的果汁( code )。

+
+

loader 编写原则

    +
  • 单一原则: 每个 Loader 只做一件事;
  • +
  • 链式调用: Webpack 会按顺序链式调用每个 Loader
  • +
  • 统一原则: 遵循 Webpack 制定的设计规则和结构,输入与输出均为字符串,各个 Loader 完全独立,即插即用;
  • +
+

在日常开发环境中,为了方便调试我们往往会加入许多 console 打印。但是我们不希望在生产环境中存在打印的值。那么这里我们自己实现一个 loader 去除代码中的 console

+
+

知识点普及之 ASTAST 通俗的来说,假设我们有一个文件 a.js ,我们对 a.js 里面的1000行进行一些操作处理,比如为所有的 await 增加 try catch ,以及其他操作,但是 a.js 里面的代码本质上来说就是一堆字符串。那我们怎么办呢,那就是转换为带标记信息的对象(抽象语法树)我们方便进行增删改查。这个带标记的对象(抽象语法树)就是AST。这里推荐一篇不错的AST文章 AST快速入门

+
+
1
npm i -D @babel/parser @babel/traverse @babel/generator @babel/types
+
    +
  • @babel/parser 将源代码解析成 AST
  • +
  • @babel/traverse 对 AST 节点进行递归遍历,生成一个便于操作、转换的 path 对象
  • +
  • @babel/generator 将 AST 解码生成 js 代码
  • +
  • @babel/types 通过该模块对具体的 AST 节点进行进行增、删、改、查
  • +
+

新建 drop-console.js

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const t = require('@babel/types')
module.exports=function(source){
const ast = parser.parse(source,{ sourceType: 'module'})
traverse(ast,{
CallExpression(path){
if(t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.object, {name: "console"})){
path.remove()
}
}
})
const output = generator(ast, {}, source);
return output.code
}
+

如何使用

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const path = require('path')
module.exports = {
mode:'development',
entry:path.resolve(__dirname,'index.js'),
output:{
filename:'[name].[contenthash].js',
path:path.resolve(__dirname,'dist')
},
module:{
rules:[{
test:/\.js$/,
use:path.resolve(__dirname,'drop-console.js')
}
]
}
}
+
+

实际上在 webpack4 中已经集成了去除 console 功能,在 minimizer 中可配置 去除console
附上官网 如何编写一个loader

+
+

手写webpack plugin

+

Webpack 运行的生命周期中会广播出许多事件, Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
通俗来说:一盘美味的 盐豆炒鸡蛋 需要经历烧油 炒制 调味到最后的装盘等过程,而 plugin 相当于可以监控每个环节并进行操作,比如可以写一个少放胡椒粉 plugin ,监控 webpack 暴露出的生命周期事件(调味),在调味的时候执行少放胡椒粉操作。
那么它与 loader 的区别是什么呢?上面我们也提到了 loader 的单一原则, loader 只能一件事,比如说 less-loader ,只能解析 less 文件, plugin 则是针对整个流程执行广泛的任务。

+
+

一个基本的 plugin 插件结构如下

+
1
2
3
4
5
6
7
8
9
10
11
12
class firstPlugin {
constructor (options) {
console.log('firstPlugin options', options)
}
apply (compiler) {
compiler.plugin('done', compilation => {
console.log('firstPlugin')
))
}
}

module.exports = firstPlugin
+
+

compilercompilation 是什么?

+
+
    +
  • compiler 对象包含了 Webpack 环境所有的的配置信息。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 optionsloaderplugin 。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。
  • +
  • compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation ,从而生成一组新的编译资源。 compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。
  • +
+

compilercompilation 的区别在于

+
    +
  • compiler 代表了整个 webpack 从启动到关闭的生命周期,而 compilation 只是代表了一次新的编译过程
  • +
  • compilercompilation 暴露出许多钩子,我们可以根据实际需求的场景进行自定义处理
  • +
+

compiler钩子文档
compilation钩子文档

+

下面我们手动开发一个简单的需求,在生成打包文件之前自动生成一个关于打包出文件的大小信息
新建一个 webpack-firstPlugin.js

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class firstPlugin{
constructor(options){
this.options = options
}
apply(compiler){
compiler.plugin('emit',(compilation,callback)=>{
let str = ''
for (let filename in compilation.assets){
str += `文件:${filename} 大小${compilation.assets[filename]['size']()}\n`
}
// 通过compilation.assets可以获取打包后静态资源信息,同样也可以写入资源
compilation.assets['fileSize.md'] = {
source:function(){
return str
},
size:function(){
return str.length
}
}
callback()
})
}
}
module.exports = firstPlugin
+

如何使用

+
1
2
3
4
5
6
7
8
const path = require('path')
const firstPlugin = require('webpack-firstPlugin.js')
module.exports = {
// 省略其他代码
plugins:[
new firstPlugin()
]
}
+

执行 npm run build 即可看到在 dist 文件夹中生成了一个包含打包文件信息的 fileSize.md

+
+

上面两个 loaderplugin 案例只是一个引导,实际开发需求中的 loaderplugin 要考虑的方面很多,建议大家自己多动手尝试一下。

+
+

附上官网 如何编写一个plugin

+

webpack5.0的时代

无论是前端框架还是构建工具的更新速度远远超乎了我们的想象,前几年的 jquery 一把梭的时代一去不复返。我们要拥抱的是不断更新迭代的 vuereactnodeserverlessdockerk8s ….
不甘落后的 webpack 也已经在近日发布了 webpack 5.0.0 beta 10 版本。在之前作者也曾提过 webpack5.0 旨在减少配置的复杂度,使其更容易上手( webpack4 的时候也说了这句话),以及一些性能上的提升

+
    +
  • 使用持久化缓存提高构建性能;
  • +
  • 使用更好的算法和默认值改进长期缓存(long-term caching);
  • +
  • 清理内部结构而不引入任何破坏性的变化;
  • +
  • 引入一些breaking changes,以便尽可能长的使用v5版本。
  • +
+

目前来看,维护者的更新很频繁,相信用不了多久webpack5.0将会拥抱大众。感兴趣的同学可以先安装beta版本尝尝鲜。不过在此之前建议大家先对webpack4进行一番掌握,这样后面的路才会越来越好走。

+

更多阅读

webpack中文
webpackjs
4W字长文带你深度解锁Webpack系列(上)

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:2020年了,再不会webpack敲得代码就不香了(近万字实战)【转载】

+

文章作者:

+

发布时间:2020年03月24日 - 20:20

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/03/24/webpack-learning-1/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/04/03/source-code-react-diff/index.html b/2020/04/03/source-code-react-diff/index.html new file mode 100644 index 0000000000..0bd8ce16ae --- /dev/null +++ b/2020/04/03/source-code-react-diff/index.html @@ -0,0 +1,700 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + React 源码剖析系列 - 不可思议的 react diff【转载】 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ React 源码剖析系列 - 不可思议的 react diff【转载】 +

+ + +
+ + + + +
+ + +

目前,前端领域中 React 势头正盛,使用者众多却少有能够深入剖析内部实现机制和原理。本系列文章希望通过剖析 React 源码,理解其内部的实现原理,知其然更要知其所以然。

+

React diff 作为 Virtual DOM 的加速器,其算法上的改进优化是 React 整个界面渲染的基础,以及性能提高的保障,同时也是 React 源码中最神秘、最不可思议的部分,本文从源码入手,深入剖析 React diff 的不可思议之处。

+

阅读本文需要对 React 有一定的了解,如果你不知何为 React,请详读 React 官方文档
如果你对 React diff 存在些许疑惑,或者你对算法优化感兴趣,那么本文值得阅读和讨论。

+ +

前言

React 中最值得称道的部分莫过于 Virtual DOM 与 diff 的完美结合,特别是其高效的 diff 算法,让用户可以无需顾忌性能问题而”任性自由”的刷新页面,让开发者也可以无需关心 Virtual DOM 背后的运作原理,因为 React diff 会帮助我们计算出 Virtual DOM 中真正变化的部分,并只针对该部分进行实际 DOM 操作,而非重新渲染整个页面,从而保证了每次操作更新后页面的高效渲染,因此 Virtual DOM 与 diff 是保证 React 性能口碑的幕后推手。

+

行文至此,可能会有读者质疑:React 无非就是引入 diff 这一概念,且 diff 算法也并非其首创,何必吹嘘的如此天花乱坠呢?

+

其实,正是因为 diff 算法的普识度高,就更应该认可 React 针对 diff 算法优化所做的努力与贡献,更能体现 React 开发者们的魅力与智慧!

+

传统 diff 算法

计算一棵树形结构转换成另一棵树形结构的最少操作,是一个复杂且值得研究的问题。传统 diff 算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n^3),其中 n 是树中节点的总数。O(n^3) 到底有多可怕,这意味着如果要展示1000个节点,就要依次执行上十亿次的比较。这种指数型的性能消耗对于前端渲染场景来说代价太高了!现今的 CPU 每秒钟能执行大约30亿条指令,即便是最高效的实现,也不可能在一秒内计算出差异情况。

+

如果 React 只是单纯的引入 diff 算法而没有任何的优化改进,那么其效率是远远无法满足前端渲染所要求的性能。

+

因此,想要将 diff 思想引入 Virtual DOM,就需要设计一种稳定高效的 diff 算法,而 React 做到了!

+

那么,React diff 到底是如何实现的呢?

+

详解 React diff

传统 diff 算法的复杂度为 O(n^3),显然这是无法满足性能要求的。React 通过制定大胆的策略,将 O(n^3) 复杂度的问题转换成 O(n) 复杂度的问题。

+

diff 策略

    +
  1. Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
  2. +
  3. 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
  4. +
  5. 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。
  6. +
+

基于以上三个前提策略,React 分别对 tree diff、component diff 以及 element diff 进行算法优化,事实也证明这三个前提策略是合理且准确的,它保证了整体界面构建的性能。

+
    +
  • tree diff
  • +
  • component diff
  • +
  • element diff
  • +
+
+

本文中源码 ReactMultiChild.js

+
+

tree diff

基于策略一,React 对树的算法进行了简洁明了的优化,即对树进行分层比较,两棵树只会对同一层次的节点进行比较。

+

既然 DOM 节点跨层级的移动操作少到可以忽略不计,针对这一现象,React 通过 updateDepth 对 Virtual DOM 树进行层级控制,只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。

+

https://pic1.zhimg.com/0c08dbb6b1e0745780de4d208ad51d34_r.jpg

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
updateChildren: function(nextNestedChildrenElements, transaction, context) {
updateDepth++;
var errorThrown = true;
try {
this._updateChildren(nextNestedChildrenElements, transaction, context);
errorThrown = false;
} finally {
updateDepth--;
if (!updateDepth) {
if (errorThrown) {
clearQueue();
} else {
processQueue();
}
}
}
}
+

分析至此,大部分人可能都存在这样的疑问:如果出现了 DOM 节点跨层级的移动操作,React diff 会有怎样的表现呢?是的,对此我也好奇不已,不如试验一番。

+

如下图,A 节点(包括其子节点)整个被移动到 D 节点下,由于 React 只会简单的考虑同层级节点的位置变换,而对于不同层级的节点,只有创建和删除操作。当根节点发现子节点中 A 消失了,就会直接销毁 A;当 D 发现多了一个子节点 A,则会创建新的 A(包括子节点)作为其子节点。此时,React diff 的执行情况:create A -> create B -> create C -> delete A

+

由此可发现,当出现节点跨层级移动时,并不会出现想象中的移动操作,而是以 A 为根节点的树被整个重新创建,这是一种影响 React 性能的操作,因此 React 官方建议不要进行 DOM 节点跨层级的操作

+
+

注意:在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。例如,可以通过 CSS 隐藏或显示节点,而不是真的移除或添加 DOM 节点。

+
+

https://pic2.zhimg.com/d712a73769688afe1ef1a055391d99ed_r.jpg

+

component diff

React 是基于组件构建应用的,对于组件间的比较所采取的策略也是简洁高效。

+
    +
  • 如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。
  • +
  • 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。
  • +
  • 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。
  • +
+

如下图,当 component D 改变为 component G 时,即使这两个 component 结构相似,一旦 React 判断 D 和 G 是不同类型的组件,就不会比较二者的结构,而是直接删除 component D,重新创建 component G 以及其子节点。虽然当两个 component 是不同类型但结构相似时,React diff 会影响性能,但正如 React 官方博客所言:不同类型的 component 是很少存在相似 DOM tree 的机会,因此这种极端因素很难在实现开发过程中造成重大影响的。

+

https://pic1.zhimg.com/52654992aba15fc90e2dac8b2387d0c4_r.jpg

+

element diff

当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)

+
    +
  • INSERT_MARKUP ,新的 component 类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作。

    +
  • +
  • MOVE_EXISTING ,在老集合有新 component 类型,且 element 是可更新的类型,generateComponentChildren 已调用 receiveComponent,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点。

    +
  • +
  • REMOVE_NODE ,老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作。

    +
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function enqueueInsertMarkup(parentInst, markup, toIndex) {
updateQueue.push({
parentInst: parentInst,
parentNode: null,
type: ReactMultiChildUpdateTypes.INSERT_MARKUP,
markupIndex: markupQueue.push(markup) - 1,
content: null,
fromIndex: null,
toIndex: toIndex,
});
}

function enqueueMove(parentInst, fromIndex, toIndex) {
updateQueue.push({
parentInst: parentInst,
parentNode: null,
type: ReactMultiChildUpdateTypes.MOVE_EXISTING,
markupIndex: null,
content: null,
fromIndex: fromIndex,
toIndex: toIndex,
});
}

function enqueueRemove(parentInst, fromIndex) {
updateQueue.push({
parentInst: parentInst,
parentNode: null,
type: ReactMultiChildUpdateTypes.REMOVE_NODE,
markupIndex: null,
content: null,
fromIndex: fromIndex,
toIndex: null,
});
}
+

如下图,老集合中包含节点:A、B、C、D,更新后的新集合中包含节点:B、A、D、C,此时新老集合进行 diff 差异化对比,发现 B != A,则创建并插入 B 至新集合,删除老集合 A;以此类推,创建并插入 A、D 和 C,删除 B、C 和 D。

+

https://pic2.zhimg.com/7541670c089b84c59b84e9438e92a8e9_r.jpg

+

React 发现这类操作繁琐冗余,因为这些都是相同的节点,但由于位置发生变化,导致需要进行繁杂低效的删除、创建操作,其实只要对这些节点进行位置移动即可。

+

针对这一现象,React 提出优化策略:允许开发者对同一层级的同组子节点,添加唯一 key 进行区分,虽然只是小小的改动,性能上却发生了翻天覆地的变化!

+

新老集合所包含的节点,如下图所示,新老集合进行 diff 差异化对比,通过 key 发现新老集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将老集合中节点的位置进行移动,更新为新集合中节点的位置,此时 React 给出的 diff 结果为:B、D 不做任何操作,A、C 进行移动操作,即可。

+

https://pic4.zhimg.com/c0aa97d996de5e7f1069e97ca3accfeb_r.jpg

+

那么,如此高效的 diff 到底是如何运作的呢?让我们通过源码进行详细分析。

+

首先对新集合的节点进行循环遍历,for (name in nextChildren),通过唯一 key 可以判断新老集合中是否存在相同的节点,if (prevChild === nextChild),如果存在相同节点,则进行移动操作,但在移动前需要将当前节点在老集合中的位置与 lastIndex 进行比较,if (child._mountIndex < lastIndex),则进行节点移动操作,否则不执行该操作。这是一种顺序优化手段,lastIndex 一直在更新,表示访问过的节点在老集合中最右的位置(即最大的位置),如果新集合中当前访问的节点比 lastIndex 大,说明当前访问节点在老集合中就比上一个节点位置靠后,则该节点不会影响其他节点的位置,因此不用添加到差异队列中,即不执行移动操作,只有当访问的节点比 lastIndex 小时,才需要进行移动操作。

+

以上图为例,可以更为清晰直观的描述 diff 的差异对比过程:

+
    +
  • 从新集合中取得 B,判断老集合中存在相同节点 B,通过对比节点位置判断是否进行移动操作,B 在老集合中的位置 B._mountIndex = 1,此时 lastIndex = 0,不满足 child._mountIndex < lastIndex 的条件,因此不对 B 进行移动操作;更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),其中 prevChild._mountIndex 表示 B 在老集合中的位置,则 lastIndex = 1,并将 B 的位置更新为新集合中的位置prevChild._mountIndex = nextIndex,此时新集合中 B._mountIndex = 0,nextIndex++ 进入下一个节点的判断。

    +
  • +
  • 从新集合中取得 A,判断老集合中存在相同节点 A,通过对比节点位置判断是否进行移动操作,A 在老集合中的位置 A._mountIndex = 0,此时 lastIndex = 1,满足 child._mountIndex < lastIndex的条件,因此对 A 进行移动操作enqueueMove(this, child._mountIndex, toIndex),其中 toIndex 其实就是 nextIndex,表示 A 需要移动到的位置;更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),则 lastIndex = 1,并将 A 的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex,此时新集合中A._mountIndex = 1,nextIndex++ 进入下一个节点的判断。

    +
  • +
  • 从新集合中取得 D,判断老集合中存在相同节点 D,通过对比节点位置判断是否进行移动操作,D 在老集合中的位置 D._mountIndex = 3,此时 lastIndex = 1,不满足 child._mountIndex < lastIndex的条件,因此不对 D 进行移动操作;更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),则 lastIndex = 3,并将 D 的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex,此时新集合中D._mountIndex = 2,nextIndex++ 进入下一个节点的判断。

    +
  • +
  • 从新集合中取得 C,判断老集合中存在相同节点 C,通过对比节点位置判断是否进行移动操作,C 在老集合中的位置 C._mountIndex = 2,此时 lastIndex = 3,满足 child._mountIndex < lastIndex 的条件,因此对 C 进行移动操作 enqueueMove(this, child._mountIndex, toIndex);更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),则 lastIndex = 3,并将 C 的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex,此时新集合中 C._mountIndex = 3,nextIndex++ 进入下一个节点的判断,由于 C 已经是最后一个节点,因此 diff 到此完成。

    +
  • +
+

以上主要分析新老集合中存在相同节点但位置不同时,对节点进行位置移动的情况,如果新集合中有新加入的节点且老集合存在需要删除的节点,那么 React diff 又是如何对比运作的呢?

+

以下图为例:

+
    +
  • 从新集合中取得 B,判断老集合中存在相同节点 B,由于 B 在老集合中的位置 B._mountIndex = 1,此时lastIndex = 0,因此不对 B 进行移动操作;更新 lastIndex = 1,并将 B 的位置更新为新集合中的位置B._mountIndex = 0,nextIndex++进入下一个节点的判断。

    +
  • +
  • 从新集合中取得 E,判断老集合中不存在相同节点 E,则创建新节点 E;更新 lastIndex = 1,并将 E 的位置更新为新集合中的位置,nextIndex++进入下一个节点的判断。

    +
  • +
  • 从新集合中取得 C,判断老集合中存在相同节点 C,由于 C 在老集合中的位置C._mountIndex = 2,lastIndex = 1,此时 C._mountIndex > lastIndex,因此不对 C 进行移动操作;更新 lastIndex = 2,并将 C 的位置更新为新集合中的位置,nextIndex++ 进入下一个节点的判断。

    +
  • +
  • 从新集合中取得 A,判断老集合中存在相同节点 A,由于 A 在老集合中的位置A._mountIndex = 0,lastIndex = 2,此时 A._mountIndex < lastIndex,因此对 A 进行移动操作;更新 lastIndex = 2,并将 A 的位置更新为新集合中的位置,nextIndex++ 进入下一个节点的判断。

    +
  • +
  • 当完成新集合中所有节点 diff 时,最后还需要对老集合进行循环遍历,判断是否存在新集合中没有但老集合中仍存在的节点,发现存在这样的节点 D,因此删除节点 D,到此 diff 全部完成。

    +
  • +
+

https://pic1.zhimg.com/7b9beae0cf0a5bc8c2e82d00c43d1c90_r.jpg

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
_updateChildren: function(nextNestedChildrenElements, transaction, context) {
var prevChildren = this._renderedChildren;
var nextChildren = this._reconcilerUpdateChildren(
prevChildren, nextNestedChildrenElements, transaction, context
);
if (!nextChildren && !prevChildren) {
return;
}
var name;
var lastIndex = 0;
var nextIndex = 0;
for (name in nextChildren) {
if (!nextChildren.hasOwnProperty(name)) {
continue;
}
var prevChild = prevChildren && prevChildren[name];
var nextChild = nextChildren[name];
if (prevChild === nextChild) {
// 移动节点
this.moveChild(prevChild, nextIndex, lastIndex);
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
prevChild._mountIndex = nextIndex;
} else {
if (prevChild) {
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
// 删除节点
this._unmountChild(prevChild);
}
// 初始化并创建节点
this._mountChildAtIndex(
nextChild, nextIndex, transaction, context
);
}
nextIndex++;
}
for (name in prevChildren) {
if (prevChildren.hasOwnProperty(name) &&
!(nextChildren && nextChildren.hasOwnProperty(name))) {
this._unmountChild(prevChildren[name]);
}
}
this._renderedChildren = nextChildren;
},
// 移动节点
moveChild: function(child, toIndex, lastIndex) {
if (child._mountIndex < lastIndex) {
this.prepareToManageChildren();
enqueueMove(this, child._mountIndex, toIndex);
}
},
// 创建节点
createChild: function(child, mountImage) {
this.prepareToManageChildren();
enqueueInsertMarkup(this, mountImage, child._mountIndex);
},
// 删除节点
removeChild: function(child) {
this.prepareToManageChildren();
enqueueRemove(this, child._mountIndex);
},

_unmountChild: function(child) {
this.removeChild(child);
child._mountIndex = null;
},

_mountChildAtIndex: function(
child,
index,
transaction,
context) {
var mountImage = ReactReconciler.mountComponent(
child,
transaction,
this,
this._nativeContainerInfo,
context
);
child._mountIndex = index;
this.createChild(child, mountImage);
},
+ +

当然,React diff 还是存在些许不足与待优化的地方,如下图所示,若新集合的节点更新为:D、A、B、C,与老集合对比只有 D 节点移动,而 A、B、C 仍然保持原有的顺序,理论上 diff 应该只需对 D 执行移动操作,然而由于 D 在老集合的位置是最大的,导致其他节点的 _mountIndex < lastIndex,造成 D 没有执行移动操作,而是 A、B、C 全部移动到 D 节点后面的现象。

+

在此,读者们可以讨论思考:如何优化上述问题?

+
+

建议:在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。

+
+

https://pic2.zhimg.com/1b8dac5b9b3e4452dec8d5447d7717ad_r.jpg

+

总结

    +
  • React 通过制定大胆的 diff 策略,将 O(n3) 复杂度的问题转换成 O(n) 复杂度的问题;
  • +
  • React 通过分层求异的策略,对 tree diff 进行算法优化;
  • +
  • React 通过相同类生成相似树形结构,不同类生成不同树形结构的策略,对 component diff 进行算法优化;
  • +
  • React 通过设置唯一 key的策略,对 element diff 进行算法优化;
  • +
  • 建议,在开发组件时,保持稳定的 DOM 结构会有助于性能的提升;
  • +
  • 建议,在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。
  • +
+

参考资料

A Survey on Tree Edit Distance and Related Problems
Reconciliation

+

如果本文能够为你解决些许关于 React diff 算法的疑惑,请点个赞吧!

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:React 源码剖析系列 - 不可思议的 react diff【转载】

+

文章作者:

+

发布时间:2020年04月03日 - 12:38

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/04/03/source-code-react-diff/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/04/07/Electron-Offline-Build/index.html b/2020/04/07/Electron-Offline-Build/index.html new file mode 100644 index 0000000000..bd16abe064 --- /dev/null +++ b/2020/04/07/Electron-Offline-Build/index.html @@ -0,0 +1,586 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 国内配置Electron开发环境的正确方式【转载】 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 国内配置Electron开发环境的正确方式【转载】 +

+ + +
+ + + + +
+ + +

前言

最近在做electron相关开发,疲于网络环境的种种限制,找遍了互联网相关资料,终于找到一篇比较全面的文章,怕丢了转过来。

+ +

转载

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:国内配置Electron开发环境的正确方式【转载】

+

文章作者:

+

发布时间:2020年04月07日 - 18:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/04/07/Electron-Offline-Build/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/04/07/webpack-uglifyjsplugin/index.html b/2020/04/07/webpack-uglifyjsplugin/index.html new file mode 100644 index 0000000000..5e6d77a9bc --- /dev/null +++ b/2020/04/07/webpack-uglifyjsplugin/index.html @@ -0,0 +1,623 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Webpack 打包时利用 UglifyJsPlugin 去掉comments 、console 、和 debugger | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Webpack 打包时利用 UglifyJsPlugin 去掉comments 、console 、和 debugger +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在检查公司前端代码质量时,经常会在sonar检查到有漏洞级别的问题特别多,但是打开查看详情的时候发现又特别低级,都是些console没去掉啊,debugger没去掉啊,之前都是看到了就提醒一下,但是屡禁不止,那么有没有什么好的办法能够从源头上避免这个问题呢,果然这个问题不止我一个人遇到,网上还是有大神给出了解决方案,那就是 webpack 打包时可以引入 UglifyJsPlugin 来解决这个扰人的问题,而且屏蔽了人为的因素,使用感觉是极好的,所以,写个帖子把网上学来的资料整理一下。

+ + +

UglifyJsPlugin

关于 UglifyJsPlugin 的介绍,在 webpack 的官网上有这样一段描述

+
+

ℹ️ webpack =< v3.0.0 currently contains v0.4.6 of this plugin under webpack.optimize.UglifyJsPlugin as an alias. For usage of the latest version (v1.0.0), please follow the instructions below. Aliasing v1.0.0 as webpack.optimize.UglifyJsPlugin is scheduled for webpack v4.0.0

+
+

简单来说就是在 webpack3.0 之前,引入了这个插件的0.4.6版本,并用 webpack.optimize.UglifyJsPlugin 作为它的别名, 在 webpack4.0 中引入了插件的 1.0.0 版本,用了同样的名称作为别名,用的时候请注意。

+

那么“这个插件”是什么呢, webpack 的官网也告诉我们了,那就是UglifyJS

+
+

A JavaScript parser, mangler/compressor and beautifier toolkit for ES6+.

+
+

翻译过来就是“一个用于ES6+的JavaScript解析器、(榨汁机:翻译的挺有意思)/压缩机和美化工具。”

+

再简单点说 uglifyJsPlugin 用来对js文件进行压缩,减小js文件的大小。其会拖慢webpack的编译速度,建议开发环境时关闭,生产环境再将其打开。

+

要用它的的话记得先安装

+
1
npm i -D uglifyjs-webpack-plugin
+

安装完成就可以用了

+
1
2
3
4
5
6
7
8
// webpack.config.js
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
plugins: [
new UglifyJsPlugin()
]
}
+ +

当然他有很多的选项,具体想了解可以去 webpack 官网,或者去插件官网都可以找到uglifyjs-webpack-plugin

+

当然知道你们懒的去看了,心里肯定也在说“直接给我个现成的配置他不香么?”,别急嘛,这就给你们

+
1
2
3
4
5
6
7
8
9
10
11
12
new UglifyJsPlugin({
//删除注释
output:{
comments:false
},
//删除console 和 debugger 删除警告
compress:{
warnings:false,
drop_debugger:true,
drop_console:true
}
})
+

当然版本间可能会有些差别,但是option是不变的,有调整的话各位自行调整一下。
公司用的 vue-cli3 所以这里再给出个 vue-cli3 默认配置文件下的写法

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// vue.config.js

configureWebpack:{
optimization: {
minimizer: [
new UglifyJsPlugin({
uglifyOptions: {
// 删除注释
output:{
comments:false
},
// 删除console debugger 删除警告
compress: {
warnings: false,
drop_console: true,//console
drop_debugger: false,
pure_funcs: ['console.log']//移除console
}
}
})
]
}
}
+ +

问题收集

    +
  1. 运行出现报错 UglifyJs
  2. +
+
+

Q: DefaultsError: warnings is not a supported option

+
+
+

A: 降低版本(使用 “uglifyjs-webpack-plugin”: “^1.1.1”),打包正常,效果达到

+
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Webpack 打包时利用 UglifyJsPlugin 去掉comments 、console 、和 debugger

+

文章作者:

+

发布时间:2020年04月07日 - 11:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/04/07/webpack-uglifyjsplugin/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/04/15/node-sass-slow-problem/index.html b/2020/04/15/node-sass-slow-problem/index.html new file mode 100644 index 0000000000..98fe791488 --- /dev/null +++ b/2020/04/15/node-sass-slow-problem/index.html @@ -0,0 +1,601 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 如何解决npm安装node-sass依赖慢的情况 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 如何解决npm安装node-sass依赖慢的情况 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

现代前端开发不用点预编译css都不好意思说自己咋写样式,虽说这么说有点夸张,但是确实是一个普遍现象吧,我们在开发中常见到的预编译css的话主要是sass和less,那么今天的主角就是其中的sass,可以说sass让人又爱又恨,爱呢是喜欢她赋予了css函数的特性,恨呢就是用的时候实在是麻烦,所以有的人就投奔了less的怀抱,他俩很像,但是有的项目已经用了sass那也没办法,嫁鸡随鸡,但是日子还得过,想办法过的好一点嘛。

+

其实这里说到sass用起来麻烦主要还是在安装依赖的时候,想必用过的人都知道我说的是啥,原来只能是不停的重试,最近在知乎上看到篇文章,聪明的解决了这个问题,所以赶紧学习记录下来,学会了就是自己的。

+ + +

问题

在使用sass时要安装node-sass包,但是这个npm包安装不尽慢的要命,下载下来之后还要进行编译,编译环境不合适或者网速不好的时候,光为了这个包的使用花上个把小时绝对正常,记得第一次折腾他用了小半天的时间。

+

那么有没有什么方法可以解决这个问题呢?

+

有痛点就会有人想办法解决,但是包名已经占用了,想用的话还是要有点配置的代价,但总好过编译和下载

+

解决方案

首先我这里假设你是知道 yarn 这个工具的,对的,接下来我们要用 yarn 进行安装,但是安装的不是 node-sass ,而是一个叫做 node-sass-install 的这个包, 安装他的话就不用在安装 node-sass 这个包了,通常来说安装这个包不会超过10s , 当然网速不好的话超过10s了也别怪我,总之要比装 node-sass 要快上很多,命令如下:

+
1
yarn add node-sass-install
+

是不是很简单,当然如果你觉得为了安装 node-sass 还要再装个 yarn (鄙视你居然不用 yarn ),那你也可以用 npm 安装,命令稍有不同,长了一点

+
1
2
npm install node-sass-install -D
npx node-sass-install
+

比 yarn 多了一条命令,那么他为啥这么神奇呢,且听我分析一下

+

原理

这个 node-sass-install 其实只是在 package.json 的 dependencies 中做了一些配置,如下:

+
1
2
3
4
5
{
"dependencies":{
"node-sass":"npm:dart-sass@latest"
}
}
+

上面这个配置的意思是,当你安装 node-sass-install 的时候,会依赖并下载 dart-sass, 然后起了个别名叫做 node-sass。偷梁换柱,狸猫换太子了,哈哈。
所以的所以呢,如果你在项目中用到 sass 的话建议你尝试一下新方法,说不定更香呢~

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:如何解决npm安装node-sass依赖慢的情况

+

文章作者:

+

发布时间:2020年04月15日 - 23:24

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/04/15/node-sass-slow-problem/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/04/15/talk-about-full-stack-big-fd/index.html b/2020/04/15/talk-about-full-stack-big-fd/index.html new file mode 100644 index 0000000000..23b66872da --- /dev/null +++ b/2020/04/15/talk-about-full-stack-big-fd/index.html @@ -0,0 +1,598 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端全栈和大前端有啥区别 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端全栈和大前端有啥区别 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

知乎真是个好地方,关注好自己喜欢的话题,经常会推送一些好的话题到你面前,这不刚刚就看到了一个人提问

+
+

前端全栈和大前端有啥区别

+
+

以前只听说过全栈,听说过大前端,但是没听说过前端全栈,对于前两个我自己也有一些理解,但从不敢说自己理解的对,带着强烈的好奇心我打开了这个问题,也看到了一位前端大神‘狼叔’的回复,感觉还是很到位的,也让我重新验证了自己的理解,所以就将狼叔的解答与我自己的认识相结合一下,做个记录。

+ + +

狼叔说

狼叔在解答这个疑问之前直接上了一张图,图我贴在下面供大家看一下
JicTqe.jpg
这么一张图实际上已经胜过千言了,但是为了方便大家理解,狼叔还是在下面对图进行了解释

+
+

前端全栈:分node全栈和其他语言全栈,比如ror搞全栈是最早的,其他php、java也有,不过纯前端的不过,在react,angular之前搞后台还是可以的。
所以前端全栈,我理解是等同于node全栈的。node本身是做后端的,但在前端工程化和BFF领域大放异彩,所以node全栈涵盖了前端的方方面面,是比较合理的解释。
大前端:更泛化的概念,移动互联网时代开启后,hybrid曾经很火,基于h5和webview做跨端,确实是很理想的做法,但复杂交互搞不定,机器性能网络等是硬伤,所以后来出现了rn和weex,整体还是前端写法,所以hybrid里前端也是占了一定的开发,结合之前前端和node的关系,综合3者:1)app里的前端,2)前端,2)node全栈,统称为大前端。这里的”大“含义是可以做的事儿的范围更广,触达前后端移动端,对前端职责有明显提升。随着技术发展,基于electron的桌面开发也日进流程,ott和iot等领域采用js也愈来愈多,所以只要和用户直接触达的端采用了前端技术开发的都涵盖在大前端范畴内。

+
+

原帖地址

+

我说

之前我的概念里前端扩展开来再进一步的话分两个方向,一个叫全栈另一个叫全端,‘栈’的话是纵向的,简单理解的话就是一款产品一个人能从设计到前端实现后端实现运维等一个人搞定,那么他就可以称之为全栈,狭义一点理解,就是前端后端都会,那么这里所说的前端全栈,我理解是更方便前端掌握的一些后端技能如node、php、ror等,而不是java这种后端技能,所以叫前端全栈,我想这里大部分指的都是node作为后端;

+

那么我理解的全端是什么呢

+

‘端’我理解为容器,任何跟用户直接接触的技术都是端的技术,早期的pc端,后来的手机端,手机端里又出现了h5、hybrid、native这么几种,后来又出现了小程序之类的容器,pc端也出现了如electron等客户端技术,那么随着物联网智能家居的出现,更多的端出现了,就如狼叔说的OTT和IOT领域也成了端,大前端的大是指范围广,属于用户直达所承载内容容器的都是端。

+

总的来说跟狼叔的理解一致,我个人也努力在往全栈全端发展,不过真的好难还有很长的路要走。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端全栈和大前端有啥区别

+

文章作者:

+

发布时间:2020年04月15日 - 15:59

+

最后更新:2020年04月15日 - 15:51

+

原始链接:https://blog.lifesli.com/2020/04/15/talk-about-full-stack-big-fd/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/04/16/0-vuecli-3-component/index.html b/2020/04/16/0-vuecli-3-component/index.html new file mode 100644 index 0000000000..88d7431c02 --- /dev/null +++ b/2020/04/16/0-vuecli-3-component/index.html @@ -0,0 +1,678 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 从零开始:Vue cli3 库模式搭建组件库并发布到npm【转载】 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 从零开始:Vue cli3 库模式搭建组件库并发布到npm【转载】 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

市面上目前已有各种各样的UI组件库,比如 Element 和 iView,他们的强大毋庸置疑。但是我们面临的情况是需求越来越复杂,当它们不能再满足我们需求的时候,这个时候就有必要开发一套属于自己团队的组件库了。

+

所以本文的目的就是让读者能通过此文,小能做一个简单的插件供人使用,大能架构和维护一个组件库不在话下。

+

以下一个简单的颜色选择器插件vColorPicker讲述从开发到上线到npm的流程。

+

vColorPicker 插件 DEMO

+ +

一、技术栈

如何通过新版脚手架创建项目,这里就不提了,自行看官方文档。

+
    +
  • Vue-cli3: 新版脚手架的库模式,可以让我们很轻松的创建打包一个库
  • +
  • npm:组件库将存放在npm
  • +
  • webpack:修改配置需要一点 webapck 的知识。
  • +
+

二、大纲

想要搭建一个组件库,我们必须先要有一个大概的思路。

+
    +
  1. 规划目录结构
  2. +
  3. 配置项目以支持目录结构
  4. +
  5. 编写组件
  6. +
  7. 编写示例
  8. +
  9. 配置使用库模式打包编译
  10. +
  11. 发布到npm
  12. +
+

三、规划目录结构

1、创建项目

在指定目录中使用命令创建一个默认的项目,或者根据自己需要自己选择。

+
1
$ vue create .
+

2、调整目录

我们需要一个目录存放组件,一个目录存放示例,按照以下方式对目录进行改造。

+
1
2
3
4
5
6
.
...
|-- examples // 原 src 目录,改成 examples 用作示例展示
|-- packages // 新增 packages 用于编写存放组件
...
.
+

四、配置项目以支持新的目录结构

我们通过上一步的目录改造后,会遇到两个问题。

+
    +
  1. src目录更名为examples,导致项目无法运行
  2. +
  3. 新增packages目录,该目录未加入webpack编译
  4. +
+

注:cli3 提供一个可选的 vue.config.js 配置文件。如果这个文件存在则他会被自动加载,所有的对项目和webpack的配置,都在这个文件中。

+

1、重新配置入口,修改配置中的 pages 选项

新版 Vue CLI 支持使用 vue.config.js 中的 pages 选项构建一个多页面的应用。

+

这里使用 pages 修改入口到 examples

+
1
2
3
4
5
6
7
8
9
10
module.exports = {
// 修改 src 为 examples
pages: {
index: {
entry: 'examples/main.js',
template: 'public/index.html',
filename: 'index.html'
}
}
}
+

2、支持对 packages 目录的处理,修改配置中的 chainWebpack 选项

packages 是我们新增的一个目录,默认是不被 webpack 处理的,所以需要添加配置对该目录的支持。

+

chainWebpack 是一个函数,会接收一个基于 webpack-chain 的 ChainableConfig 实例。允许对内部的 webpack 配置进行更细粒度的修改。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module.exports = {
// 修改 src 为 examples
pages: {
index: {
entry: 'examples/main.js',
template: 'public/index.html',
filename: 'index.html'
}
},
// 扩展 webpack 配置,使 packages 加入编译
chainWebpack: config => {
config.module
.rule('js')
.include
.add('packages')
.end()
.use('babel')
.loader('babel-loader')
.tap(options => {
// 修改它的选项...
return options
})
}
}
+

链式操作
webpack-chain

+

五、编写组件

以上我们已配置好对新目录架构的支持,接下来我们尝试编写组件。以下我们以一个已发布到 npm 的小插件作为示例。
GitHub - 颜色选择器:vcolorpicker

+

1. 创建一个新组件

    +
  1. 在 packages 目录下,所有的单个组件都以文件夹的形式存储,所有这里创建一个目录 color-picker/
  2. +
  3. 在 color-picker/ 目录下创建 src/ 目录存储组件源码
  4. +
  5. 在 /color-picker 目录下创建 index.js 文件对外提供对组件的引用。
    修改 /packages/color-picker/index.js文件,对外提供引用。
  6. +
+
1
2
3
4
5
6
7
8
9
10
11
12
// ./packages/color-picker/index.js

// 导入组件,组件必须声明 name
import colorPicker from './src/color-picker.vue'

// 为组件提供 install 安装方法,供按需引入
colorPicker = function (Vue) {
Vue.component(colorPicker.name, colorPicker)
}

// 默认导出组件
export default colorPicker
+

2. 整合所有的组件,对外导出,即一个完整的组件库

修改 /packages/index.js 文件,对整个组件库进行导出。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ./packages/index.js

// 导入颜色选择器组件
import colorPicker from './color-picker'

// 存储组件列表
const components = [
colorPicker
]

// 定义 install 方法,接收 Vue 作为参数。如果使用 use 注册插件,则所有的组件都将被注册
const install = function (Vue) {
// 判断是否安装
if (install.installed) return
// 遍历注册全局组件
components.map(component => Vue.component(component.name, component))
}

// 判断是否是直接引入文件
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}

export default {
// 导出的对象必须具有 install,才能被 Vue.use() 方法安装
install,
// 以下是具体的组件列表
colorPicker
}
+

六、编写示例

1、在示例中导入组件库

1
2
3
4
5
6
7
8
9
10
11
12
13
import Vue from 'vue'
import App from './App.vue'

// 导入组件库
import ColorPicker from './../packages/index'
// 注册组件库
Vue.use(ColorPicker)

Vue.config.productionTip = false

new Vue({
render: h => h(App)
}).$mount('#app')
+

2、在示例中使用组件库中的组件

在上一步用使用 Vue.use() 全局注册后,即可在任意页面直接使用了,而不需另外引入。当然也可以按需引入。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<colorPicker v-model="color" v-on:change="headleChangeColor"></colorPicker>
</template>

<script>
export default {
data () {
return {
color: '#ff0000'
}
},
methods: {
headleChangeColor () {
console.log('颜色改变')
}
}
}
</script>
+

七、发布到 npm,方便直接在项目中引用

到此为止我们一个完整的组件库已经开发完成了,接下来就是发布到 npm 以供后期使用。

+

1、package.js 中新增一条编译为库的命令

在库模式中,Vue是外置的,这意味着即使在代码中引入了 Vue,打包后的文件也是不包含Vue的。

+

Vue Cli3 构建目标:库

+

以下我们在 scripts 中新增一条命令

+
    +
  • –target: 构建目标,默认为应用模式。这里修改为 lib 启用库模式。
  • +
  • –dest : 输出目录,默认 dist。这里我们改成 lib
  • +
  • [entry]: 最后一个参数为入口文件,默认为 src/App.vue。这里我们指定编译 packages/ 组件库目录。
    1
    2
    3
    4
    "script": {
    // ...
    "lib": "vue-cli-service build --target lib --name vcolorpicker --dest lib packages/index.js"
    }
  • +
+

执行编译库命令

+
1
$ npm run lib
+

2、配置 package.json 文件中发布到 npm 的字段

    +
  • name: 包名,该名字是唯一的。可在 npm 官网搜索名字,如果存在则需换个名字。
  • +
  • version: 版本号,每次发布至 npm 需要修改版本号,不能和历史版本号相同。
  • +
  • description: 描述。
  • +
  • main: 入口文件,该字段需指向我们最终编译后的包文件。
  • +
  • keyword:关键字,以空格分离希望用户最终搜索的词。
  • +
  • author:作者
  • +
  • private:是否私有,需要修改为 false 才能发布到 npm
  • +
  • license: 开源协议
  • +
+

以下为参考设置

+
1
2
3
4
5
6
7
8
{
"name": "vcolorpicker",
"version": "0.1.5",
"description": "基于 Vue 的颜色选择器",
"main": "lib/vcolorpicker.umd.min.js",
"keyword": "vcolorpicker colorpicker color-picker",
"private": false
}
+

3、添加 .npmignore 文件,设置忽略发布文件

我们发布到 npm 中,只有编译后的 lib 目录、package.json、README.md才是需要被发布的。所以我们需要设置忽略目录和文件。

+

和 .gitignore 的语法一样,具体需要提交什么文件,看各自的实际情况。

+
1
2
3
4
5
6
7
8
9
# 忽略目录
examples/
packages/
public/

# 忽略指定文件
vue.config.js
babel.config.js
*.map
+

4、登录到 npm

首先需要到 npm 上注册一个账号,注册过程略。

+

如果配置了淘宝镜像,先设置回npm镜像:

+
1
$ npm config set registry http://registry.npmjs.org
+

然后在终端执行登录命令,输入用户名、密码、邮箱即可登录。

+
1
$ npm login
+ +

5、发布到 npm

执行发布命令,发布组件到 npm

+
1
$ npm publish
+ +

6、发布成功

发布成功后稍等几分钟,即可在 npm 官网搜索到。以下是刚提交的 vcolorpicker

+

7、使用新发布的组件库

安装

+
1
$ npm install vcolorpicker -S
+

使用

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 在 main.js 引入并注册
import vcolorpicker from 'vcolorpicker'
Vue.use(vcolorpicker)

// 在组件中使用
<template>
<colorPicker v-model="color" />
</template>
<script>
export default {
data () {
return {
color: '#ff0000'
}
}
}
</script>
+

暂时没有做包含多个组件的时候的按需加载,以后研究了再补充。

+

八、项目地址

Github 地址:https://github.com/zuley/vue-color-picker
npm 地址:https://www.npmjs.com/package/vcolorpicker
DEMO 演示:http://vue-color-picker.rxshc.com

+

九、参考文章

从零开始搭建Vue组件库 VV-UI
Vue插件开发
组件基础

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:从零开始:Vue cli3 库模式搭建组件库并发布到npm【转载】

+

文章作者:

+

发布时间:2020年04月16日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/04/16/0-vuecli-3-component/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/04/16/what-is-npx/index.html b/2020/04/16/what-is-npx/index.html new file mode 100644 index 0000000000..71097e1f98 --- /dev/null +++ b/2020/04/16/what-is-npx/index.html @@ -0,0 +1,648 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + npx是什么 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ npx是什么 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在阅读一些文章时,经常看到介绍命令的时候用到了 npx 关键字,之前知道有 node 有 npm 但是很少用到 npx ,npx是个啥呢?好奇上网上找了些资料学习了一下,写篇学习笔记记录一下。

+ +

npx 起源

我从阮一峰的博客中看到介绍 npx 的文章,开头的一句话说明了他诞生的日子。

+
+

npm 从5.2版开始,增加了 npx 命令。

+
+

为了验证阮一峰这里介绍的正确性我特意下了对应的npm版本验证了一下确实如此,而且在网上找到了另一位大佬司徒正美(大佬走好)博客中也对 npx 做了介绍

+
+

最近我在更新 npm 5.2.0 的时候发现会买一送一,自动安装了 npx。

+
+

由此,我可以肯定的告诉大家,npx是npm在5.2.0之后版本推出的一个工具,那么他是干嘛用的呢?

+

npx 作用

想要了解一个技术,最好的途经是他的官网,于是我到网上找到了 npx 在 github 上的仓库,地址如下
npx仓库,其中对 npx 有这样一段介绍

+
+

DESCRIPTION
Executes either from a local node_modules/.bin, or from a central cache, installing any packages needed in order for to run.
By default, npx will check whether exists in $PATH, or in the local project binaries, and execute that. If is not found, it will be installed prior to execution.
Unless a –package option is specified, npx will try to guess the name of the binary to invoke depending on the specifier provided. All package specifiers understood by npm may be used with npx, including git specifiers, remote tarballs, local directories, or scoped packages.
If a full specifier is included, or if –package is used, npx will always use a freshly-installed, temporary version of the package. This can also be forced with the –ignore-existing flag.

+
+

上面这一大段英文我想大家一定看了就头疼,所以为了大家不那么头疼,可以看一下下面我翻译的内容,如果有翻译不对的地方,还请指正。

+
+

解释
执行 command 命令,无论从本地(我理解为项目目录)node_modules/.bin 或者从全局缓存中, 安装所需执行的任何包。
默认情况下,npx将检查 command 是否存在于 $PATH 中,或者在本地项目二进制文件中,并执行该命令。
如果没有找到 command ,它将在执行之前安装。
除非指定了 —package 选项,否则npx将根据提供的说明符猜测要调用的二进制文件的名称。
npm可以理解的所有包说明符都可以与npx一起使用,包括git说明符、远程tarball、本地目录或作用域包。
如果包含完整的说明符,或者使用 ——package 选项,npx将始终使用新安装的包的临时版本。
这也可以用 ——ignore-existing 标记强制执行。

+
+

上面这段机翻简直让人无法理解,所以我又去大佬博客下看了下他们的解释

+
+

npx 想要解决的主要问题,就是调用项目内部安装的模块。 – 阮一峰
根据 zkat/npx 的描述,npx 会帮你执行依赖包里的二进制文件。 – 司徒正美

+
+

司徒大大文章写的太简洁了,不过他还是举了例子,我进行了一下精简,如下

+
1
./node_modules/.bin/webpack -v // => npx webpack -v
+

简单来说就是找包执行命令的时候不再关注他在哪了,直接就可以用了。

+

阮一峰老师的文章更像是官网的翻译加理解

+
+

npx 的原理很简单,就是运行的时候,会到node_modules/.bin路径和环境变量$PATH里面,检查命令是否存在。
由于 npx 会检查环境变量$PATH,所以系统命令也可以调用。

+
+

另外阮一峰老师还介绍了一个临时安装命令使用的场景,我理解为对上面英语介绍倒数第二句的理解

+

避免全局安装模块

除了调用项目内部模块,npx 还能避免全局安装的模块。比如,create-react-app这个模块是全局安装,npx 可以运行它,而且不进行全局安装。

+
1
2
$ npx create-react-app my-react-app

+

上面代码运行时,npx 将create-react-app下载到一个临时目录,使用以后再删除。所以,以后再次执行上面的命令,会重新下载create-react-app。

+

下载全局模块时,npx 允许指定版本。

+
1
2
$ npx uglify-js@3.1.0 main.js -o ./dist/main.js

+

上面代码指定使用 3.1.0 版本的uglify-js压缩脚本。

+

注意,只要 npx 后面的模块无法在本地发现,就会下载同名模块。比如,本地没有安装http-server模块,下面的命令会自动下载该模块,在当前目录启动一个 Web 服务。

+
1
$ npx http-server
+ +

然后阮老师还对官网最后一句话做了解释

+

–no-install 参数和–ignore-existing 参数

如果想让 npx 强制使用本地模块,不下载远程模块,可以使用–no-install参数。如果本地不存在该模块,就会报错。

+
1
2
$ npx --no-install http-server

+

反过来,如果忽略本地的同名模块,强制安装使用远程模块,可以使用–ignore-existing参数。比如,本地已经全局安装了create-react-app,但还是想使用远程模块,就用这个参数。

+
1
2
$ npx --ignore-existing create-react-app my-react-app

+

然后对于官网上的 example 阮老师也挑了重点的做了介绍,如选择指定的 node 版本

+
1
2
$ npx node@0.12.8 -v
v0.12.8
+

上面命令会使用 0.12.8 版本的 Node 执行脚本。原理是从 npm 下载这个版本的 node,使用后再删掉。
某些场景下,这个方法用来切换 Node 版本,要比 nvm 那样的版本管理器方便一些。

+

更多参数

-p 参数

-p参数用于指定 npx 所要安装的模块,所以上一节的命令可以写成下面这样。

+
1
2
$ npx -p node@0.12.8 node -v 
v0.12.8
+

上面命令先指定安装node@0.12.8,然后再执行node -v命令。
-p参数对于需要安装多个模块的场景很有用。

+
1
$ npx -p lolcatjs -p cowsay [command]
+

-c 参数

如果 npx 安装多个模块,默认情况下,所执行的命令之中,只有第一个可执行项会使用 npx 安装的模块,后面的可执行项还是会交给 Shell 解释。

+
1
2
$ npx -p lolcatjs -p cowsay 'cowsay hello | lolcatjs'
# 报错
+

上面代码中,cowsay hello | lolcatjs 执行时会报错,原因是第一项 cowsay 由 npx 解释,而第二项命令localcatjs由 Shell 解释,但是lolcatjs并没有全局安装,所以报错。

+

-c参数可以将所有命令都用 npx 解释。有了它,下面代码就可以正常执行了

+
1
2
$ npx -p lolcatjs -p cowsay -c 'cowsay hello | lolcatjs'

+

-c参数的另一个作用,是将环境变量带入所要执行的命令。举例来说,npm 提供当前项目的一些环境变量,可以用下面的命令查看。

+
1
2
$ npm run env | grep npm_

+

-c参数可以把这些 npm 的环境变量带入 npx 命令。

+
1
2
$ npx -c 'echo "$npm_package_name"'

+

上面代码会输出当前项目的项目名。

+

执行 GitHub 源码

npx 还可以执行 GitHub 上面的模块源码。

+
1
2
3
4
5
# 执行 Gist 代码
$ npx https://gist.github.com/zkat/4bc19503fe9e9309e2bfaa2c58074d32

# 执行仓库代码
$ npx github:piuccio/cowsay hello
+

注意,远程代码必须是一个模块,即必须包含package.json和入口脚本。

+

最后

因为还没有实际使用过的经验,所以更多的内容从其他大佬哪里白嫖来的知识,做个笔记以观后效。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:npx是什么

+

文章作者:

+

发布时间:2020年04月16日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/04/16/what-is-npx/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/05/14/css3-pointer-events/index.html b/2020/05/14/css3-pointer-events/index.html new file mode 100644 index 0000000000..8f70c0261b --- /dev/null +++ b/2020/05/14/css3-pointer-events/index.html @@ -0,0 +1,607 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CSS3新属性-pointer-events | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ CSS3新属性-pointer-events +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

今天在看一个叫做Avue的框架组件时看到了一个叫做“全局水印”的组件,功能呢就是给当前页面加上一个水印遮罩,就像下图这样:
Y08cbd.png
因为工作中有这样的场景,所以我很好奇看了一下他的代码,他的水印设置为了fixed定位,并且z-index设置的很高,但是它具有穿透性,最终我发现其中起作用的是一个叫做“pointer-events”的css3新属性,于是为了搞清楚它的作用,我在网上做了一番搜索,并把自己的认识整理一下。

+ +

介绍

MDN上是这样介绍这个属性的 传送门

+
+

pointer-events CSS 属性指定在什么情况下 (如果有) 某个特定的图形元素可以成为鼠标事件的 target。

+
+

它有如下这些个属性值:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Keyword values */
pointer-events: auto;
pointer-events: none;
pointer-events: visiblePainted; /* SVG only */
pointer-events: visibleFill; /* SVG only */
pointer-events: visibleStroke; /* SVG only */
pointer-events: visible; /* SVG only */
pointer-events: painted; /* SVG only */
pointer-events: fill; /* SVG only */
pointer-events: stroke; /* SVG only */
pointer-events: all; /* SVG only */

/* Global values */
pointer-events: inherit;
pointer-events: initial;
pointer-events: unset;

+

这里有一个特别的值,它进行了专门的介绍,也正是因为这个原因,水印的功能才得以实现。

+
+

除了指示该元素不是鼠标事件的目标之外,值none表示鼠标事件“穿透”该元素并且指定该元素“下面”的任何东西。

+
+

由上可知,当我们将pointer-events值设置为none时,它就有了穿透的特性,也就是说你“看得见它,却摸不到它”,而且也不具备鼠标指针的特性,这不正是水印想要的效果嘛,完美~;

+

接着往下看我们可以知道它的初始值是auto,并且可以继承
Y0J0XD.png

+

水印功能就这样实现了是不是简单实用,那么是不是就没有缺点呢,当然要硬说有的话那就是兼容性的问题吧。

+

因为这个css3的属性是新出的,自然ie9及以下自然就不支持了,而且ie不支持的范围要到ie11以下,所以ie你为啥不去死呢。。。

+

Y0YEjO.png

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:CSS3新属性-pointer-events

+

文章作者:

+

发布时间:2020年05月14日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/05/14/css3-pointer-events/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/05/28/Prettier-Setting/index.html b/2020/05/28/Prettier-Setting/index.html new file mode 100644 index 0000000000..7b50ed302f --- /dev/null +++ b/2020/05/28/Prettier-Setting/index.html @@ -0,0 +1,585 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Prettier格式化配置 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Prettier格式化配置 +

+ + +
+ + + + +
+ + +

前言

最近在做代码风格统一,用到了Prettier,暂时先记录一下现在的配置信息,回头抽时间写篇总结文。

+ +

一、技术栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
// 使能每一种语言默认格式化规则
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[less]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},

/* prettier的配置 */
"prettier.printWidth": 100, // 超过最大值换行
"prettier.tabWidth": 4, // 缩进字节数
"prettier.useTabs": false, // 缩进不使用tab,使用空格
"prettier.semi": true, // 句尾添加分号
"prettier.singleQuote": true, // 使用单引号代替双引号
"prettier.proseWrap": "preserve", // 默认值。因为使用了一些折行敏感型的渲染器(如GitHub comment)而按照markdown文本样式进行折行
"prettier.arrowParens": "avoid", // (x) => {} 箭头函数参数只有一个时是否要有小括号。avoid:省略括号
"prettier.bracketSpacing": true, // 在对象,数组括号与文字之间加空格 "{ foo: bar }"
"prettier.disableLanguages": ["vue"], // 不格式化vue文件,vue文件的格式化单独设置
"prettier.endOfLine": "auto", // 结尾是 \n \r \n\r auto
"prettier.eslintIntegration": false, //不让prettier使用eslint的代码格式进行校验
"prettier.htmlWhitespaceSensitivity": "ignore",
"prettier.ignorePath": ".prettierignore", // 不使用prettier格式化的文件填写在项目的.prettierignore文件中
"prettier.jsxBracketSameLine": false, // 在jsx中把'>' 是否单独放一行
"prettier.jsxSingleQuote": false, // 在jsx中使用单引号代替双引号
"prettier.parser": "babylon", // 格式化的解析器,默认是babylon
"prettier.requireConfig": false, // Require a 'prettierconfig' to format prettier
"prettier.stylelintIntegration": false, //不让prettier使用stylelint的代码格式进行校验
"prettier.trailingComma": "es5", // 在对象或数组最后一个元素后面是否加逗号(在ES5中加尾逗号)
"prettier.tslintIntegration": false // 不让prettier使用tslint的代码格式进行校验
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Prettier格式化配置

+

文章作者:

+

发布时间:2020年05月28日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/05/28/Prettier-Setting/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2020/07/19/rpm\345\221\275\344\273\244/index.html" "b/2020/07/19/rpm\345\221\275\344\273\244/index.html" new file mode 100644 index 0000000000..8a61fb0671 --- /dev/null +++ "b/2020/07/19/rpm\345\221\275\344\273\244/index.html" @@ -0,0 +1,663 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + rpm命令 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ rpm命令 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

安装

sudo rpm -ivh [lz4-1.8.3-1.el7.x86_64.rpm]

+

卸载

sudo rpm -e [postgresql14-server-14.4-1PGDG.rhel7.x86_64]

+

查询功能

命令格式 rpm {-q|–query} [select-options] [query-options]

+

  RPM的查询功能是极为强大,是极为重要的功能之一;举几个常用的例子,更为详细的具体的,请参考#man rpm

+

1、对系统中已安装软件的查询

+

1)查询系统已安装的软件

+

  语法:rpm -q 软件名

+

  举例:[root@localhost beinan]# rpm -q gaim

+

  gaim-1.3.0-1.fc4   

+
   查看系统中所有已经安装的包,要加 -a 参数 ;
+
+

  [root@localhost RPMS]# rpm -qa

+

  如果分页查看,再加一个管道 |和more命令;

+

  [root@localhost RPMS]# rpm -qa |more

+

  在所有已经安装的软件包中查找某个软件,比如说 gaim ;可以用 grep 抽取出来;

+

  [root@localhost RPMS]# rpm -qa |grep gaim

+

  上面这条的功能和 rpm -q gaim 输出的结果是一样的;

+

2)查询一个已经安装的文件属于哪个软件包

+

  语法 rpm -qf 文件名

+

  注:文件名所在的绝对路径要指出

+

  举例:

+

  [root@localhost RPMS]# rpm -qf /usr/lib/libacl.la

+

  libacl-devel-2.2.23-8

+

3)查询已安装软件包都安装到何处

+

  语法:rpm -ql 软件名 或 rpm rpmquery -ql 软件名

+

  举例:

+

  [root@localhost RPMS]# rpm -ql lynx

+

  [root@localhost RPMS]# rpmquery -ql lynx

+

4)查询一个已安装软件包的信息

+

  语法格式: rpm -qi 软件名

+

  举例:

+

  [root@localhost RPMS]# rpm -qi lynx

+

5)查看一下已安装软件的配置文件

+

  语法格式:rpm -qc 软件名

+

  举例:

+

  [root@localhost RPMS]# rpm -qc lynx

+

6)查看一个已经安装软件的文档安装位置

+

  语法格式: rpm -qd 软件名

+

  举例:

+

  [root@localhost RPMS]# rpm -qd lynx

+

7)查看一下已安装软件所依赖的软件包及文件

+

  语法格式: rpm -qR 软件名

+

  举例:

+

  [root@localhost beinan]# rpm -qR rpm-python

+

  查询已安装软件的总结:对于一个软件包已经安装,我们可以把一系列的参数组合起来用;比如 rpm -qil ;比如:

+

  [root@localhost RPMS]# rpm -qil lynx

+

2、对于未安装的软件包的查看:

+

  查看的前提是您有一个.rpm 的文件,也就是说对既有软件file.rpm的查看等;

+

1)查看一个软件包的用途、版本等信息;

+

  语法: rpm -qpi file.rpm

+

  举例:

+

  [root@localhost RPMS]# rpm -qpi lynx-2.8.5-23.i386.rpm

+

2)查看一件软件包所包含的文件;

+

  语法: rpm -qpl file.rpm

+

  举例:

+

  [root@localhost RPMS]# rpm -qpl lynx-2.8.5-23.i386.rpm

+

3)查看软件包的文档所在的位置;

+

  语法: rpm -qpd file.rpm

+

  举例:

+

  [root@localhost RPMS]# rpm -qpd lynx-2.8.5-23.i386.rpm

+

4)查看一个软件包的配置文件;

+

  语法: rpm -qpc file.rpm

+

  举例:

+

  [root@localhost RPMS]# rpm -qpc lynx-2.8.5-23.i386.rpm

+

5)查看一个软件包的依赖关系

+

  语法: rpm -qpR file.rpm

+

  举例:

+

  [root@localhost archives]# rpm -qpR yumex_0.42-3.0.fc4_noarch.rpm

+

  /bin/bash

+

  /usr/bin/python

+

  config(yumex) = 0.42-3.0.fc4

+

  pygtk2

+

  pygtk2-libglade

+

  rpmlib(CompressedFileNames) <= 3.0.4-1

+

  rpmlib(PayloadFilesHavePrefix) <= 4.0-1

+

  usermode

+

  yum >= 2.3.2

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:rpm命令

+

文章作者:

+

发布时间:2020年07月19日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/07/19/rpm%E5%91%BD%E4%BB%A4/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2020/08/02/Linux\347\233\256\345\275\225\350\257\264\346\230\216/index.html" "b/2020/08/02/Linux\347\233\256\345\275\225\350\257\264\346\230\216/index.html" new file mode 100644 index 0000000000..92db88a9cc --- /dev/null +++ "b/2020/08/02/Linux\347\233\256\345\275\225\350\257\264\346\230\216/index.html" @@ -0,0 +1,610 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Linux目录说明 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Linux目录说明 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

/bin:

bin是Binary的缩写, 这个目录存放着最经常使用的命令。

+

/boot:

这里存放的是启动Linux时使用的一些核心文件,包括一些连接文件以及镜像文件。

+

/dev :

dev是Device(设备)的缩写, 该目录下存放的是Linux的外部设备,在Linux中访问设备的方式和访问文件的方式是相同的。

+

/etc:

这个目录用来存放所有的系统管理所需要的配置文件和子目录。

+

/home:

用户的主目录,在Linux中,每个用户都有一个自己的目录,一般该目录名是以用户的账号命名的。

+

/lib:

这个目录里存放着系统最基本的动态连接共享库,其作用类似于Windows里的DLL文件。几乎所有的应用程序都需要用到这些共享库。

+

/lost+found:

这个目录一般情况下是空的,当系统非法关机后,这里就存放了一些文件。

+

/media linux

系统会自动识别一些设备,例如U盘、光驱等等,当识别后,linux会把识别的设备挂载到这个目录下。

+

/mnt:

系统提供该目录是为了让用户临时挂载别的文件系统的,我们可以将光驱挂载在/mnt/上,然后进入该目录就可以查看光驱里的内容了。

+

/opt:

这是给主机额外安装软件所摆放的目录。比如你安装一个ORACLE数据库则就可以放到这个目录下。默认是空的。

+

/proc:

这个目录是一个虚拟的目录,它是系统内存的映射,我们可以通过直接访问这个目录来获取系统信息。
这个目录的内容不在硬盘上而是在内存里,我们也可以直接修改里面的某些文件,比如可以通过下面的命令来屏蔽主机的ping命令,使别人无法ping你的机器:

+

/root:

该目录为系统管理员,也称作超级权限者的用户主目录。

+

/sbin:

s就是Super User的意思,这里存放的是系统管理员使用的系统管理程序。

+

/selinux:

这个目录是Redhat/CentOS所特有的目录,Selinux是一个安全机制,类似于windows的防火墙,但是这套机制比较复杂,这个目录就是存放selinux相关的文件的。

+

/srv:

该目录存放一些服务启动之后需要提取的数据。

+

/sys:

这是linux2.6内核的一个很大的变化。该目录下安装了2.6内核中新出现的一个文件系统 sysfs 。

+

sysfs文件系统集成了下面3种文件系统的信息:针对进程信息的proc文件系统、针对设备的devfs文件系统以及针对伪终端的devpts文件系统。
该文件系统是内核设备树的一个直观反映。

+

当一个内核对象被创建的时候,对应的文件和目录也在内核对象子系统中被创建。

+

/tmp:

这个目录是用来存放一些临时文件的。

+

/usr:

这是一个非常重要的目录,用户的很多应用程序和文件都放在这个目录下,类似与windows下的program files目录。

+

/usr/bin:

系统用户使用的应用程序。

+

/usr/sbin:

超级用户使用的比较高级的管理程序和系统守护程序。

+

/usr/src:

内核源代码默认的放置目录。

+

/var:

这个目录中存放着在不断扩充着的东西,我们习惯将那些经常被修改的目录放在这个目录下。包括各种日志文件。

+

在linux系统中,有几个目录是比较重要的,平时需要注意不要误删除或者随意更改内部文件。
系统上跑了很多程序,那么每个程序都会有相应的日志产生,而这些日志就被记录到这个目录下,具体在/var/log 目录下,另外mail的预设放置也是在这里。

+

/etc:

上边也提到了,这个是系统中的配置文件,如果你更改了该目录下的某个文件可能会导致系统不能启动。

+

/bin, /sbin, /usr/bin, /usr/sbin:

这是系统预设的执行文件的放置目录,比如 ls 就是在/bin/ls 目录下的。

+

值得提出的是,/bin, /usr/bin 是给系统用户使用的指令(除root外的普通用户),而/sbin, /usr/sbin 则是给root使用的指令。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Linux目录说明

+

文章作者:

+

发布时间:2020年08月02日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/08/02/Linux%E7%9B%AE%E5%BD%95%E8%AF%B4%E6%98%8E/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/11/10/learn-cordova-1/index.html b/2020/11/10/learn-cordova-1/index.html new file mode 100644 index 0000000000..c5d703d378 --- /dev/null +++ b/2020/11/10/learn-cordova-1/index.html @@ -0,0 +1,719 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 从零开始:学习Cordova(一) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 从零开始:学习Cordova(一) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

很长时间没有写博客了,因为最近换了工作,原来的技术栈基本上都换了,现在接触了很多新的技术栈,Cordova 就是其中之一。

+

接下来这篇文章就是我的学习笔记,以供回顾,写的不好请勿拍砖。

+ +

一、What is Cordova? Cordova是个啥?

Logo
移动端开发和 Web 开发有一些不一样,Web 端开发面向的是浏览器,而移动端开发面向的是各种移动设备,那么针对各种移动设备提供的 SDK 进行开发的话,我们通常称之为***原生开发***,原生开发虽然通常调用底层 Api ,性能更好,但是因为不同移动终端底层技术不一样,所以每种设备都要单独开发,这样开发成本和能力要求都比较高,所以通常都会有一些框架来兼容各种平台,就好比 Java 虚拟机可以运行在 Mac/Windows/Linux 一样,为了能够写一套移动端代码可以在各种设备中使用,于是乎就有了 Cordova

+

上面说了这么一堆废话,其实简言之,***Cordova 就是提供了跨设备调用底层Api的移动端开发框架(这种开发方式又叫做Hybird 模式)***

+

知道这一点应该就够了,当然这是我认为的。

+

Cordova 的官网地址如下

+
+

https://cordova.apache.org/

+
+

其实从这里就可以看出, CordovaApache 基金下的一个项目,其官网是这样介绍它的

+
+

Mobile apps with HTML, CSS & JS
Target multiple platforms with one code base
Free and open source

+
+

翻译成中文的话就是

+
+

使用 HtmlCSSJS 进行移动端应用开发
一份代码多平台使用
免费并且开源

+
+

三句话解释了 Cordova 的三个方面,

+
    +
  • 使用前端技术开发-对前端开发友好
  • +
  • 跨平台-节省开发工时,降低开发难度-从图标看支持 Android/IOS/WP三大平台
  • +
  • 免费不用解释开源的话意味着会有很多人贡献代码
  • +
+

以上奠定了 Cordova 强大及流行的基础。

+

二、快速上手

在使用 Cordova 做移动端开发之前需要做一些环境准备工作,首先要安装 Node 以及它携带的 Npm 包管理器,这个现代的前端开发应该都知道,我这里就不多说了,装它就完了。

+

装好之后我们就要用 Npm 全局安装一下 Cordova 项目的依赖,命令很简单;

+

方便的话还要装一下 Git ,因为一些命令要在 git-cmd 上执行,当然管理代码也是需要它的,所以装吧。

+
1
npm install -g cordova
+

就这么简单就可以开发 Cordova 应用了,接下来我们要创建一个项目,创建项目的命令也十分简单。在命令行工具中输入下面的命令你就创建了一个 Cordova 的应用

+
1
cordova create <path> // 这里的path是项目的路径
+

如果要查看创建项目时还能设置什么参数你可以执行下面的命令

+
1
cordova help create
+

怎么样是不是超级简单。
做了上面的工作你就创建了一个具备基本功能的 Cordova 项目,接下来我们要给项目配置一些东西。

+

首先刚才讲到了, Cordova 是一个跨平台的框架,那么你的项目要适配什么平台你需要进行一些配置, cd 到项目目录下执行下面的命令

+
1
cordova platform add <platform name> // platform name 为适配平台的名称
+

如果不知道名称是什么命令行工具也贴心的给了查询用的命令,通过此命令你可以看到你安装了什么平台的适配,有哪些可以用

+
1
2
cordova platform

+

执行了这个命令你将看到下面这样的 list

+
1
2
3
4
5
6
7
8
/***
Installed platforms:
Available platforms:
android ^9.0.0
browser ^6.0.0
electron ^1.0.0
windows ^7.0.0
*/
+

项目也创建了,简单的配置工作也完成了,我们可以启动项目看一下效果了,那么我们就该执行运行项目的命令了

+
1
cordova run <platform name> // platform name 适配平台的名称
+

到这我们就快速的创建并运行起了一个 Cordova 的工程。

+

小Tip

如上执行启动命令后,默认会启动8000端口,如果被占用的话可以通过增加命令行参数的形式改变端口号,参数如下设置

+
1
cordova run browser --target=chrome --port=9090
+

以上就可以自定义端口及浏览器启动应用了。

+

三、详细了解

1、概览

官网开头介绍了 Cordova 的身世和适用范围,前面开头已经讲过了不想再讲,直接看看 Cordova 的架构设计图。
此处是个架构图

+
Cordova 架构图
+ +

这个图理解起来不难,大的方面是两个部分,底层是 MobileOS ,也就是手机的操作系统本身,上层是由 Cordova 构建的框架;

+

Cordova 内部的话是两层结构:

+
    +
  • 上层的话是由前端代码构成的 WebApp 部分;
  • +
  • 下层是 Cordova 的视图渲染引擎,它为 WebApp 提供了 HtmlApiCordovaApi
  • +
+

也就是说我们可以调用 html 的原生 Api 也可以使用 Cordova 提供的 jsApi

+

Cordova 还提供了丰富的插件系统;通过 Cordova 的视图引擎调用 CordovaNativeApi 来调用一些 Cordova 提供好的调用设备的信息的 Api ,另外也可以调用一些用户自定义的 Api

+

Cordova 视图引擎和插件系统再将上层的需求通过 MobileOS 开放的 Api 能力实现具体功能,由此完成了整个 Cordova 框架的使用;

+

总的来说这个原理跟大多数跨平台的框架原理都类似,由框架提供统一的 Api 能力,再由框架处理不同平台的兼容,这里特别之处在于渲染引擎可以允许使用前端原生的 Api ,这将大大降低开发的难度。

+

WebView

说到移动端开发,不知道 WebView 应该是不可能的,在原生移动应用中,WebView 就是移动应用内部嵌入的一个‘浏览器’,它可以允许你使用前端技术展示内容。

+

WebApp

WebAppWebView 两个词有些像,事实上它们也确实有些关系,WebApp 从名字就可以看出是使用 Web 技术开发的 App ,那么它跟 WebView 是什么关系呢,打个比方,WebView 就是个快递盒子, WebApp 就是你买的商品,一个是容器,一个是内容;这样说应该就明白了吧,通过前端技术开发的 App 通过 WebView 嵌入到 App 中,就可以带给用户原生应用的体验,忘了说 WebApp 是只能用浏览器访问的 App

+

和纯 WebApp 不同,嵌入到 WebView 中的 WebApp 需要有一个配置文件,***config.xml*** ,它在项目的根目录下,说明了应用的一些信息,这个文件必不可少!!!

+

插件系统

刚刚在上面的架构图中我们介绍了 Cordova 的插件系统,它提供了我们通过 js 调用 MobileApi 的能力,如电源/相机/联系人等等;
官方维护了核心的功能,当然如果你愿意也可以调用一些第三方提供的插件,你可以方便的通过 npm 包管理器安装;

+

但是有一点必须要说:项目创建后默认是不带任何插件的,即使是官方的核心插件也是需要你自己导入进去,第三方的更不用说了,另外Cordova 只提供了功能 Api 并不包含任何的 UI 部件和 MV 框架(你可以根据自己喜欢使用 Angular 或者 Vue 或者其他什么都可以),这一点要牢记。

+

开发工作流

使用 Cordova 开发 MobileApp 的时候即可以开发多个平台的 App (一次开发多平台可用,降低开发成本),也可以专注开发某一平台的App (使用前端技术开发,降低开发难度),因此开发工作流也分为两种:

+
跨平台(CLI)的工作流

官网上说了很多,也不难理解,这里用我自己的话说就是, Cordova 提供了一个牛叉的 CLI 工具,它可以自动的完成一次编码多平台构建的事,你只需要按照它说的方式开发就行。:)

+
平台为中心的工作流

这里不得不说我没有看很明白,但是结合下面的注意事项我是这样理解的,因为只为某一平台服务,所以有一些针对此平台的特殊的功能或者插件你就可以使用了(虽然不太清楚为什么这么做,如果你知道告诉我,谢谢),但是一旦使用了这种开发方式,那么就回不到跨平台开发了,因为你使用了针对某一平台才能调用的代码,这也不难理解,从字里行间感觉就是不建议使用这种开发工作流(好的好的,知道了)。

+

好的,接下来开发 App 啦;

+

2、开发应用

快速上手中已经介绍了如何准备、创建和启动应用,如何添加平台,这里就不多说了。说些没讲过的。

+

上面说过检查目前平台安装情况的话可以使用下面的命令

+
1
cordova platforms
+

我们也可以使用另一个命令来看,我试验了一下,效果是一样的。。。(问号脸,一样的为什么搞两个命令)

+
1
cordova platform ls
+
+

事后我发现,无论我使用的命令是 platform 还是 platforms 加不加 ls 都可以查到当前安装的平台。。。 啊~兼容性好强。

+
+

每执行一次 add 命令,你就会发现工程目录下的 platforms 目录下就对应生成了一个对应平台的文件夹,切勿手贱删除,你虽然删除了文件夹,但是各处设置的 platform 并没有去掉,会导致报错,如果要删除请使用 remove 命令。 想知道还有哪些命令的话就看下面的链接吧。⇒ 命令大全

+

安装构建的先决条件

因为 Cordova 的底层还是要调用 MobileOS 的接口,所以各平台的 SDK 是必须要装的,当然这里指的是你要进行构建的平台,比如你要构建 Android 应用,那么你就要装 AndroidSDKIOS 同理。这里有一个例外broswer平台是不需要依赖其他 SDK 的(明明是开发 APP ,这里只是为了验证画面比较方便)
要看你是否满足了构建需要的依赖,可以执行下面的命令查看

+
1
cordova requirements
+

执行完后你会得到如下结果

+
1
2
3
4
5
6
7
8
9
10
Requirements check results for android:
Java JDK: installed .
Android SDK: installed
Android target: installed android-19,android-21,android-22,android-23,Google Inc.:Google Apis:19,Google Inc.:Google Apis (x86 System Image):19,Google Inc.:Google Apis:23
Gradle: installed

Requirements check results for ios:
Apple OS X: not installed
Cordova tooling for iOS requires Apple OS X
Error: Some of requirements check failed
+

这样你就可以清楚的知道自己需要安装哪些依赖才能完成对应平台的构建,简直方便的不要不要的。
具体想知道不同的平台都依赖些什么,你可以参见下面的链接;

+ +

构建 App

create 项目之后,项目的根目录下会生成一个 www 目录,这个目录包含了 webapp 的入口页面 index.html ,入口文件的话是 www/js/index.js 文件的 deviceready 事件中。

+

如果要构建代码的话,执行下面的命令

+
1
cordova build
+

这个命令会构建你安装的所有平台,如果只想构建某一平台的话,可以把平台的名字加在命令后面,如

+
1
cordova build ios
+

如果想了解更多的参数的话,可以看一下后面的链接。⇒ 更多参数

+

测试 App

构建完 App 之后我们就可以测试了,通常各平台的 SDK 中会提供模拟器,我们只需要执行下面的命令就可以启动模拟器。

+
1
cordova emulate android
+

当然如果你觉得用实机查看更自然一些的话,也可以把手机插上数据线与电脑建立连接,然后执行下面的命令就可以了。

+
1
cordova run android
+

上面演示的是 AndroidApp 测试过程,每个平台虽然基本类似,但还是有出入,所以,下面提供了不同平台的调试方法,供你预览。

+ +

添加插件

如果只是开发一个 Cordova 框架的 WebApp ,那么你不需要装任何插件,直接用前端技术开发就好了,但是应该没人会这么做吧,毕竟开发这个的目的就是为了跨平台开发原生级移动 App ,因此要调用移动设备的各种功能就必须安装插件。

+

插件

Cordova 插件是一些使用 Javascript 调用原生 SDK 功能的包;
你可以使用两种方式找到你想要的插件

+
    +
  1. 通过 Cordova 提供的包管理平台 ⇒ 插件搜索页
  2. +
  3. 通过命令行搜索 cordova plugin search camera
  4. +
+

选好你要安装的插件之后你需要执行下面的命令来安装它

+
1
cordova plugin add <plugin name> // 如 cordova plugin add cordova-plugin-chrome-apps-proxy
+

如果你的包没有发布在 Cordova 的平台上,也可以使用 git 地址来安装。
另外 Cordova 还非常贴心的提供了一个工具 Plugman 来帮助开发者更好的管理 Cordova 插件。⇒ Plugman参考

+

如果要查看你安装了哪些插件可以使用下面任意一种命令都可以。(咱也不知道为啥提供这么多方式,反正好用无脑)

+
1
2
3
4
5
6
7
8
plugin ls
plugin list
plugin

/**
$ cordova plugin ls
cordova-plugin-whitelist 1.2.1 "Whitelist"
*/
+

如果想知道更多关于 plugin 的命令参数,可以看右边的链接 ⇒ plugin参数

+

使用 merges 自定义每个平台

虽然可以用一套代码来构建多个平台,但一些平台会有自己的特点,就好像 Chrome 浏览器默认的字体大小与 IE 的不同, AndroidIOS 也有不一样的地方,这种情况下去改 www 目录下的文件显然是不合适的,所以 Cordova 提供了 merges 方式来适配不同平台各自的特别处理,你要做的就是在项目的根目录下创建一个 merges 目录,比如你要适配 Android ,你就可以在 merges 下创建一个 android 目录,然后在下面或覆盖或添加新的资源。(这里官网没有说 merges 在哪里创建,试验后发现是在根目录下)

+

更新 Cordova 和你的项目

如果你要更新你的 Cordova (通常不建议这么干,当然你知道后果并想要使用新的特性或者修正 bug 除外),那么你需要执行一下 update 命令

+
1
sudo npm update -g cordova
+

当然你也可以指定更新到什么版本(这都是 npm 的知识了)

+
1
sudo npm install -g cordova@3.1.0-0.2.0
+

你也可以使用下面的命令查看当前的版本(还是 npm 的知识)

+
1
npm info cordova version
+

你是不是以为这样就可以了,不好意思还不行,你还需要把各个平台进行一下升级

+
1
2
3
cordova platform update android --save
cordova platform update ios --save
// ...
+

其实这也好理解,你都已经轿车换 SUV 了,总不能还用原来的车轱辘吧,只是我希望能不能在换车的时候一块儿把车轱辘换了(这里的意思是希望直接自动化处理了)

+

到这里你的第一个 App 就已经开发好了,是不是还有点成就感。

+

一些参考资料

到这里一个简单的基于 Cordova 搭建的 App 就实现了,当然一个 App 绝不这么简单,各平台的 SDK 安装也没那么简单,但这不是本篇文章要说的事了,下一篇文章我会说说如何应对各种不同的开发平台,在这之前还请你看一下 Cordova 都支持哪些平台,具体看一下下面的链接。

+

好了,今天就先写到这里,下篇文章我们再见。

+

平台支持

+

(待续…)

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:从零开始:学习Cordova(一)

+

文章作者:

+

发布时间:2020年11月10日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/11/10/learn-cordova-1/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/11/10/learn-go/index.html b/2020/11/10/learn-go/index.html new file mode 100644 index 0000000000..4d786671cd --- /dev/null +++ b/2020/11/10/learn-go/index.html @@ -0,0 +1,587 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 从零开始:学习Go(一) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 从零开始:学习Go(一) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:从零开始:学习Go(一)

+

文章作者:

+

发布时间:2020年11月10日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/11/10/learn-go/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/11/11/learn-ionic-1/index.html b/2020/11/11/learn-ionic-1/index.html new file mode 100644 index 0000000000..627a517cc4 --- /dev/null +++ b/2020/11/11/learn-ionic-1/index.html @@ -0,0 +1,640 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 从零开始:学习Ionic(一) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 从零开始:学习Ionic(一) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

在移动 App 开发领域,有一个框架非常知名,每每面试都会被问起,“你用过 Ionic 么?”,是的,就是 Ionic 框架。

+

之前已经学习过了 Cordova ,它和 Ionic 可以说是一对黄金搭档,一个面向 MobileOS ,一个面向 UI ,很多人搞不清楚它们之间的区别,没关系,回头我再抽时间讲一下,今天我们主要来认识一下 Ionic

+

好戏开始:)

+ +

Ionic 简介

学一门技术,我们首先要知道它是谁,能干啥,有啥好处。要了解一门新技术最好的方式就是去它的官网看看。
(一言不合丢给你一个官网 ⇒ 官网

+
+

One codebase.
Any platform.
Now in Vue.

+
+

翻译过来是这样的

+
+

一份代码
任何平台
现在支持Vue

+
+

看着是不是眼熟,嗯,跟 Cordova 的口号何其相似,目标都是跨平台,只是 Cordova 跨的是 MobileOSIonic 跨的是前端框架。
最下面一句 Now in Vue 像是在说,“嗯,我也支持Vue了(Vue发展太快了,不能掉队)”,要知道,最开始的 Ionic 是妥妥的 Angular 派,不过无所谓,管他黑猫白猫,能完成任务就是好猫。

+

再往下看你会看到,巴拉巴拉说了一堆好处,比如开发迅速啊,组件优美且丰富啊,社区强大啊,还有跨平台(支持原生 jsAngularVue ,nice~),另外还提供了 native 的组件来直接调用移动设备的功能,可以说是相当强大了。

+

好了,看完了介绍,我们赶紧快速上手吧。

+

快速上手

在使用Ionic做开发还是要做一些准备工作,nodegit都是必不可少的,所以请先行准备好。

+

目前,Ionic提供了两种创建工程的方式:

+
    +
  • 一种用官方的话说叫StepByStep,看过之后我理解就是通过命令的方式,一步步来;
  • +
  • 另一种的话是官方提供了一个向导工具,就跟安装软件一样填一些必要的信息,然后一路下一步就生成了一个工程;
  • +
+

接下来我将使用这两种方式分别创建工程,首先是第一种。

+

StepByStep 创建工程

    +
  1. 全局安装Ionic的脚手架工具
    1
    npm install -g @ionic/cli
  2. +
  3. 创建工程
    1
    ionic start myApp tabs // myApp 是项目目录名称也是工程名称 tabs是模板名称
  4. +
  5. 启动工程
    1
    2
    cd myApp
    ionic serve
  6. +
+

嗯,上面三个步骤之后,你的Ionic项目就启动起来了。(What !? 这么简单?! 嗯,是的。)

+

向导式(App Wizard)创建工程

这是一个在线创建工程的手段(感觉对于开发人员来说貌似还是命令行来的方便,这种可视化方案应该是给小白用户准备的吧),地址如下

+
+

https://ionicframework.com/start

+
+

此处是个图

+

点开上面的地址,你会看到如图所示的一个界面,我们可以看到它要求你输入的一些信息

+
    +
  1. App Name
  2. +
  3. Icon
  4. +
  5. Theme Color
  6. +
  7. Template
  8. +
  9. Framework
  10. +
+

可以看到这比命令行要多出几个信息,上面的命令行只要求了 App NameTemplate 难道强大的命令行不支持设置这些么?答案是否定的,当然可以设置这些信息,只是以参数的形式而已,先不说这个,填写好上面的信息以后我们点击[create App]按钮,这时我们进入了下一步,选择代码仓库(Choose a git host),你会看到下面有一个[Skip]按钮说明这步不是必须的,我们先选择一个git仓库,这里我选择github,其他的感兴趣的同学可以自己试试。点击[connect],这时会弹出一个窗口,申请对应git仓库的鉴权认证,认证通过后我们点击[choose],等待程序运行一会儿后工程就会被上传到git仓库中,并且会跳转到一个DashBoard,提供了可视化管理工程的页面(也太高大上了吧),右侧我们看到如何把代码下载到本地,并运行它,真的是很方便。

+

例:

+
1
2
3
npm install -g @ionic/cli cordova-res // cordova-res 指的是安装 cordova 的开发依赖,因为除 cordova 之外还可以选择如 phonegap 之类的MobileOS 框架
git clone https://github.com/lixuguang/li-app.git li-app
cd li-app && npm install && ionic serve
+ +

执行完上面的命令以后,我们的工程也就运行起来了,也是十分方便,即使不会写代码的人,跟着指南也可以很方便的搭建一个Ionic工程。

+

深入学习

Ionic 的体系当中,最重要的莫过于 Ionic CLI 和 UI Component 两个了,接下来我将分辨将这两部分中最重要的内容拿出来说说。

+

Ionic CLI

在上面的快速上手中,我们已经使用了几个命令,但这些还不够,Ionic CLI 提供了强大的命令,请与我一起看一下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
npm install -g @ionic/cli // 全局安装 Ionic CLI
ionic start myApp tabs // 用模板创建工程
ionic start --list // 查看可用模板
ionic serve // 启动工程
ionic serve --lab // 以android或ios方式打开
ionic lib update // 更新包
ionic serve --address 192.168.89.1 // 指定Ip,给外部用户访问
ionic platform add ios/android // 添加平台
ionic build ios/android // 构建平台
ionic emulate ios [options] // ionic run ios [options] 模拟器运行

ionic generate // 创建新特性


options
-l //livereload, 实时刷新变化。
-c //打印app里的console
-s //打印设备的console
-p //指定设备的端口
-i //指定livereload的重刷端口
--debug //debug
--release //release
--host=0.0.0.0 // IP
--port=8100 // 端口


ionic resources [--splash] [--icon] // 上传代码到官方平台
ionic upload // 登录
ionic info // 查看系统信息
ionic browser add crosswalk // 加壳预览(手机浏览器性能问题解决)
ionic browser list // 查看可用的browser
ionic browser revert android/ios // 删除安装的browser
ionic state reset // 先删除平台和插件,再安装package.json文件中的平台和插件。重置
ionic state save // 保存当前状态信息
ionic state clear // 先删除平台和插件,然后按照package.json文件中包含的平台和插件重新安装。

npm install @ionic/angular@latest --save // Ionic + Angular
ng add @ionic/angular // 使用Angular CLI 增加Ionic的组件库,可用于Angular项目改造Ionic

npm install @ionic/react // Ionic + React
npm install @ionic/react-router // 在既存的react项目中引入ionic特性

// 增加CSS文件为组件服务,加在根组件下
/* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css';

/* Basic CSS for apps built with Ionic */
import '@ionic/react/css/normalize.css';
import '@ionic/react/css/structure.css';
import '@ionic/react/css/typography.css';

/* Optional CSS utils that can be commented out */
import '@ionic/react/css/padding.css';
import '@ionic/react/css/float-elements.css';
import '@ionic/react/css/text-alignment.css';
import '@ionic/react/css/text-transformation.css';
import '@ionic/react/css/flex-utils.css';
import '@ionic/react/css/display.css';

npm install @ionic/vue @ionic/vue-router // Ionic + Vue ,在既存的Vue项目中引入ionic特性

// main.js 文件中加入引入
import { IonicVue } from '@ionic/vue';

import App from './App.vue'
import router from './router';

const app = createApp(App)
.use(IonicVue)
.use(router);

router.isReady().then(() => {
app.mount('#app');
});

// router/index.js 用Ionic提供的vue-router替换原本的vue-router
import { createRouter, createWebHistory } from '@ionic/vue-router';

const routes = [
// routes go here
]

const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})

export default router;

// 增加CSS文件为组件服务 main.js
/* Core CSS required for Ionic components to work properly */
import '@ionic/vue/css/core.css';

/* Basic CSS for apps built with Ionic */
import '@ionic/vue/css/normalize.css';
import '@ionic/vue/css/structure.css';
import '@ionic/vue/css/typography.css';

/* Optional CSS utils that can be commented out */
import '@ionic/vue/css/padding.css';
import '@ionic/vue/css/float-elements.css';
import '@ionic/vue/css/text-alignment.css';
import '@ionic/vue/css/text-transformation.css';
import '@ionic/vue/css/flex-utils.css';
import '@ionic/vue/css/display.css';

From here, you can learn about how to develop with Ionic Framework in our [Ionic Vue Quickstart Guide](https://ionicframework.com/docs/vue/quickstart).

## Ionicons CDN

Ionicons is packaged by default with the Ionic Framework, so no installation is necessary if you're using Ionic. To use Ionicons without Ionic Framework, place the following `<script>` near the end of your page, right before the closing `</body>` tag.


<script type="module" src="https://cdn.jsdelivr.net/npm/ionicons@4.7.4/dist/ionicons/ionicons.esm.js"></script>
<script nomodule src="https://cdn.jsdelivr.net/npm/ionicons@4.7.4/dist/ionicons/ionicons.js"></script>


+ + +

(待续…)

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:从零开始:学习Ionic(一)

+

文章作者:

+

发布时间:2020年11月11日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/11/11/learn-ionic-1/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2021/05/08/\345\205\263\344\272\216VSCode\344\270\255\345\210\253\345\220\215\357\274\210-\357\274\211\347\232\204\344\275\277\347\224\250/index.html" "b/2021/05/08/\345\205\263\344\272\216VSCode\344\270\255\345\210\253\345\220\215\357\274\210-\357\274\211\347\232\204\344\275\277\347\224\250/index.html" new file mode 100644 index 0000000000..5d0af5349a --- /dev/null +++ "b/2021/05/08/\345\205\263\344\272\216VSCode\344\270\255\345\210\253\345\220\215\357\274\210-\357\274\211\347\232\204\344\275\277\347\224\250/index.html" @@ -0,0 +1,611 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + VSCode中既想使用path-intellisense插件,又想使用Webpack别名(@)的方法 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ VSCode中既想使用path-intellisense插件,又想使用Webpack别名(@)的方法 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

+

场景:

前端开发我们通常使用VSCode,使用时常见有一个常见的场景:

+
+

引入一个文件、图片等

+
+

如果不利用VSCode的插件,我们通常会写相对路径或者绝对路径,但为了方便写代码,我们通常会利用工具来帮助我们减轻负担,这种情况就引入了今天的主角,VSCode下的插件path-intellisense,是的,有了它编辑器就会聪明的自动帮我们提示出文件在什么地方,这的确方便了我们。

+

但是,在Vue开发过程中,或者说是利用Webpack作为构建工具时我们通常会定义一个变量如:@ 来简化路径的书写,使代码变得简洁美观,这里的@,我们称之为 路径别名 ,这也是Vue官方十分推荐使用的,但因为路径别名的使用,原来的智能提示路径的插件就会无法正常工作,因为工作中确实遇到了这样的坑,所以简单的写个文记录一下。

+

问题一:因为使用了路径别名(@)导致path-intellisense不能自动提示

这个问题其实比较容易理解,因为路径别名是Webpack构建时使用的,并不是VSCode原生支持的功能,所以它并不认识@,这是个什么,所以解决思路就是告诉VSCode @ 是个什么就好了,专业一点说的话就是加上映射关系

+

解决步骤

    +
  1. 打开path-intellisense插件的setting
  2. +
  3. 找到Mapping配置项
  4. +
  5. 加以下代码
    1
    2
    3
    "path-intellisense.mappings": {
    "@": "${workspaceRoot}/src"
    }
  6. +
  7. 重启插件搞定!
  8. +
+

问题二:我们希望 Ctrl + 鼠标左键点击一个外部方法时,能够快速跳转到对应的外部文件。

第二个问题提出时,可能会有人有疑问,上面不是配置完映射关系了嘛,为什么还有下面的问题呢,其实你要是不问,我也有这个问题,上一步操作实际上是告诉了path-intellisense插件该怎么解析@这个东西,但是VSCode并不知道啊,但是Ctrl + 鼠标左键的动作又是VSCode负责的事,所以我们也要告诉一下编辑器该如何应对上面的情况,说到这里大家应该就理解了

+

解决步骤

    +
  1. 在项目package.json所在同级目录下创建文件jsconfig.json(这是VSCode的一个可选配置)
  2. +
  3. 在jsconfig.json增加如下配置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    {
    "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "allowSyntheticDefaultImports": true,
    "baseUrl": "./",
    "paths": {
    "@/*": ["src/*"] // 这里是关键代码
    }
    },
    "exclude": [
    "node_modules"
    ]
    }
  4. +
  5. 重启VSCode搞定!
  6. +
+

通过上面的解决方案,我们就可以愉快的使用VSCode进行前端开发了,你学废了吗?

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:VSCode中既想使用path-intellisense插件,又想使用Webpack别名(@)的方法

+

文章作者:

+

发布时间:2021年05月08日 - 08:03

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2021/05/08/%E5%85%B3%E4%BA%8EVSCode%E4%B8%AD%E5%88%AB%E5%90%8D%EF%BC%88-%EF%BC%89%E7%9A%84%E4%BD%BF%E7%94%A8/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2021/05/13/\345\205\263\344\272\216\347\211\210\346\234\254\347\256\241\347\220\206/index.html" "b/2021/05/13/\345\205\263\344\272\216\347\211\210\346\234\254\347\256\241\347\220\206/index.html" new file mode 100644 index 0000000000..08e9a9ce70 --- /dev/null +++ "b/2021/05/13/\345\205\263\344\272\216\347\211\210\346\234\254\347\256\241\347\220\206/index.html" @@ -0,0 +1,600 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 关于版本管理 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 关于版本管理 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

还记得你的前端工程里有这样一个文件‘package.json’,里面管理者运行代码所需要的各种依赖,每种依赖都会有一个版本号,那么你知道版本号是怎么定义的么?不知道的话就看看下面的内容吧。

+ +

关于版本号的那些事

你会发现这些版本号通常是三部分构成的,像是‘X.Y.Z’的一种感觉,其实这是一种叫做SemVer的版本管理规范,下面我们就来讲讲SemVer

+

SemVer 的生平

语义化版本控制的规范是由 Gravatars 创办者兼 GitHub 共同创办者 Tom Preston-Werner 所建立。

+

它的许可证是 知识共享 署名 3.0 (CC BY 3.0) 所以你可以不用付费直接使用它。

+

关于它的更多描述你可以到下面的地址找到
https://semver.org/lang/zh-CN/

+

主要你要记住的是如下几句话:

+
+

版本格式:主版本号.次版本号.修订号,版本号递增规则如下:

+

主版本号:当你做了不兼容的 API 修改,
次版本号:当你做了向下兼容的功能性新增,
修订号:当你做了向下兼容的问题修正。
先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。

+
+

更多的内容的话,请到官网查看吧。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:关于版本管理

+

文章作者:

+

发布时间:2021年05月13日 - 07:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2021/05/13/%E5%85%B3%E4%BA%8E%E7%89%88%E6%9C%AC%E7%AE%A1%E7%90%86/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/03/11/\346\220\236\346\207\202EventLoop/index.html" "b/2022/03/11/\346\220\236\346\207\202EventLoop/index.html" new file mode 100644 index 0000000000..633e3a580b --- /dev/null +++ "b/2022/03/11/\346\220\236\346\207\202EventLoop/index.html" @@ -0,0 +1,702 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 搞懂EventLoop | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 搞懂EventLoop +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

EventLoop 翻译过来就是事件循环,那啥是事件循环啊,这个要从javascript是如何执行的说起。
要知道javascript是一个运行在浏览器或者Node环境的单线程执行的脚本语言,但是为了解决单线程阻塞的问题,这里就引入了事件循环的机制,也就是我们常说的异步特性。

+

但是虽然都是JavaScript,都是事件循环,都有异步特性,但浏览器环境和Node环境的实现方式是不一样的,这里要先说明一下。
浏览器的事件循环是HTML定义的规范,而Node环境是利用libuv库实现的,这里我们先从浏览器的事件循环开始说。

+ + +

浏览器的事件循环

首先我们要知道,浏览器的事件循环是有这么两个部分构成,一个叫做主线程(main thread),另一个叫做调用栈(call-stack),所有的任务(Task)都会被放到调用栈里,等待主线程调用,这个怎么理解呢,打个比方,主线程就像是物流配送中的传送带,如果没有快递往上放的时候,它是空的,当有快递包裹放上去的时候,它就开始运行,而你购买的产品要放倒包裹里,然后包裹在放到传送带上,最终被送到你家里,这里的传送带就是主线程,包裹就是调用栈,而购买的产品就是任务,不知道这样解释是否能好理解一些。

+

同步、异步任务

JavaScript中的任务有两种,一种是同步任务,另一种是异步任务。

+
    +
  • 同步任务会排着队,逐个执行
  • +
  • 异步任务在执行得到结果后,将回调函数添加到任务队列中,等着主线程空了再执行。
  • +
+

调用栈

栈,是一种数据结构,后进先出(LIFO),后入栈的先执行,执行完后出栈执行栈顶新的那个函数,直到栈空,像个瓶子,只有一头有口。

+

任务队列

队列,也是一种数据结构,先进先出(FIFO),队头出,队尾进,先到先得,就像排队买东西,或者汽车排队过隧道。

+

任务

任务分为两种,一种叫做宏任务(MacroTask),另一种叫做微任务(MicroTask),首先他们都是异步队列中的任务,那它们之间是什么关系呢?简单说来就是微任务是VIP优先执行,全部微任务执行完之后才轮到普通用户宏任务执行,而且执行完一个后马上要看有没有新的微任务执行,有的话宏任务还得等着,就这样往复,看着像不像是在银行等着排队办业务的你(其实是我)。
JavaScript的执行流程
宏任务与微任务
那么哪些是宏任务,哪些是微任务呢?看下表吧。

+ + + + + + + + + + + +
宏任务微任务
setTimeout、setInterval、js主代码、setImmediate(Node)、requestAnimationFrame(浏览器)process.nextTick、Promise的then方法
+

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
console.log('1');

setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})

setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
+ +

分析一下

JS 运行机制为从上而下,那么这里的执行顺序应该是什么样的呢?

+
第一轮循环:

1)、首先打印 1
2)、接下来是setTimeout是异步任务且是宏任务,加入宏任务暂且记为 setTimeout1
3)、接下来是 process 微任务 加入微任务队列 记为 process1
4)、接下来是 new Promise 里面直接 resolve(7) 所以打印 7 后面的then是微任务 记为 then1
5)、setTimeout 宏任务 记为 setTimeout2

+
+

第一轮循环打印出的是 1 7
当前宏任务队列:setTimeout1, setTimeout2
当前微任务队列:process1, then1,

+
+
第二轮循环:

1)、执行所有微任务
2)、执行process1,打印出 6
3)、执行then1 打印出8
4)、微任务都执行结束了,开始执行第一个宏任务
5)、执行 setTimeout1 也就是 第 3 - 14 行
6)、首先打印出 2
7)、遇到 process 微任务 记为 process2
8)、new Promise中resolve 打印出 4
9)、then 微任务 记为 then2

+
+

第二轮循环结束,当前打印出来的是 1 7 6 8 2 4
当前宏任务队列:setTimeout2
当前微任务队列:process2, then2

+
+
第三轮循环:

1)、执行所有的微任务
2)、执行 process2 打印出 3
3)、执行 then2 打印出 5
4)、执行第一个宏任务,也就是执行 setTimeout2 对应代码中的 25 - 36 行
5)、首先打印出 9
6)、process 微任务 记为 process3
7)、new Promise执行resolve 打印出 11
8)、then 微任务 记为 then3

+
+

第三轮循环结束,当前打印顺序为:1 7 6 8 2 4 3 5 9 11
当前宏任务队列为空
当前微任务队列:process3,then3

+
+
第四轮循环:

1)、执行所有的微任务
2)、执行process3 打印出 10
3)、执行then3 打印出 12

+

代码执行结束:
最终打印顺序为:1 7 6 8 2 4 3 5 9 11 10 12

+

Node环境的循环机制

Node的Event loop一共分为6个阶段,每个细节具体如下:

+
    +
  • timers: 执行setTimeout和setInterval中到期的callback。
  • +
  • pending callback: 上一轮循环中少数的callback会放在这一阶段执行。
  • +
  • idle, prepare: 仅在内部使用。
  • +
  • poll: 最重要的阶段,执行pending callback,在适当的情况下回阻塞在这个阶段。
  • +
  • check: 执行setImmediate(setImmediate()是将事件插入到事件队列尾部,主线程和事件队列的函数执行完成之后立即执行setImmediate指定的回调函数)的callback。
  • +
  • close callbacks: 执行close事件的callback,例如socket.on(‘close’[,fn])或者http.server.on(‘close, fn)。
  • +
+

Node EventLoop

+

具体细节如下:

+

timers

执行setTimeout和setInterval中到期的callback,执行这两者回调需要设置一个毫秒数,理论上来说,应该是时间一到就立即执行callback回调,但是由于system的调度可能会延时,达不到预期时间。
以下是官网文档解释的例子:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const fs = require('fs');

function someAsyncOperation(callback) {
// Assume this takes 95ms to complete
fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout(() => {
const delay = Date.now() - timeoutScheduled;

console.log(`${delay}ms have passed since I was scheduled`);
}, 100);


// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
const startCallback = Date.now();

// do something that will take 10ms...
while (Date.now() - startCallback < 10) {
// do nothing
}
});

+ +

当进入事件循环时,它有一个空队列(fs.readFile()尚未完成),因此定时器将等待剩余毫秒数,当到达95ms时,fs.readFile()完成读取文件并且其完成需要10毫秒的回调被添加到轮询队列并执行。
当回调结束时,队列中不再有回调,因此事件循环将看到已达到最快定时器的阈值,然后回到timers阶段以执行定时器的回调。
在此示例中,您将看到正在调度的计时器与正在执行的回调之间的总延迟将为105毫秒。

+

pending callbacks

此阶段执行某些系统操作(例如TCP错误类型)的回调。 例如,如果TCP socket ECONNREFUSED在尝试connect时receives,则某些* nix系统希望等待报告错误。 这将在pending callbacks阶段执行。

+

poll

该poll阶段有两个主要功能:

+
    +
  • 执行I/O回调。
  • +
  • 处理轮询队列中的事件。
  • +
+

当事件循环进入poll阶段并且在timers中没有可以执行定时器时,将发生以下两种情况之一
如果poll队列不为空,则事件循环将遍历其同步执行它们的callback队列,直到队列为空,或者达到system-dependent(系统相关限制)。

+

如果poll队列为空,则会发生以下两种情况之一

+
    +
  • 如果有setImmediate()回调需要执行,则会立即停止执行poll阶段并进入执行check阶段以执行回调。
  • +
  • 如果没有setImmediate()回到需要执行,poll阶段将等待callback被添加到队列中,然后立即执行。
  • +
+

当然设定了 timer 的话且 poll 队列为空,则会判断是否有 timer 超时,如果有的话会回到 timer 阶段执行回调。

+

check

此阶段允许人员在poll阶段完成后立即执行回调。
如果poll阶段闲置并且script已排队setImmediate(),则事件循环到达check阶段执行而不是继续等待。
setImmediate()实际上是一个特殊的计时器,它在事件循环的一个单独阶段运行。它使用libuv API来调度在poll阶段完成后执行的回调。
通常,当代码被执行时,事件循环最终将达到poll阶段,它将等待传入连接,请求等。
但是,如果已经调度了回调setImmediate(),并且轮询阶段变为空闲,则它将结束并且到达check阶段,而不是等待poll事件。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
console.log('start')
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
Promise.resolve().then(function() {
console.log('promise3')
})
console.log('end')

+ +

如果Node版本是v11.x,那么执行结果和浏览器一致,结果如下:

+
1
2
3
4
5
6
7
start
end
promise3
timer1
promise1
timer2
promise2
+ +

如果是v10版本上,那结果将会出现两种情况
如果time2定时器已经在执行队列中了

+
1
2
3
4
5
6
7
start
end
promise3
timer1
timer2
promise1
promise2
+ +

如果time2定时器没有在执行对列中,执行结果为

+
1
2
3
4
5
6
7
start
end
promise3
timer1
promise1
timer2
promise2
+ +

具体情况可以参考poll阶段的两种情况。

+

NodeEventLoop

+

setImmediate() 的setTimeout()的区别

setImmediate和setTimeout()是相似的,但根据它们被调用的时间以不同的方式表现。

+
    +
  • setImmediate()设计用于在当前poll阶段完成后check阶段执行脚本 。
  • +
  • setTimeout() 安排在经过最小(ms)后运行的脚本,在timers阶段执行。
  • +
+

举个例子

1
2
3
4
5
6
7
8
setTimeout(() => {
console.log('timeout');
}, 0);

setImmediate(() => {
console.log('immediate');
});

+ +

执行定时器的顺序将根据调用它们的上下文而有所不同。 如果从主模块中调用两者,那么时间将受到进程性能的限制。
其结果也不一致

+

如果在I / O周期内移动两个调用,则始终首先执行立即回调:

+
1
2
3
4
5
6
7
8
9
10
11
const fs = require('fs');

fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});

+ +

其结果可以确定一定是immediate => timeout。
主要原因是在I/O阶段读取文件后,事件循环会先进入poll阶段,发现有setImmediate需要执行,会立即进入check阶段执行setImmediate的回调。
然后再进入timers阶段,执行setTimeout,打印timeout。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
   ┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘

+ +

Process.nextTick()

process.nextTick()虽然它是异步API的一部分,但未在图中显示。这是因为process.nextTick()从技术上讲,它不是事件循环的一部分。

+

process.nextTick()方法将 callback 添加到next tick队列。 一旦当前事件轮询队列的任务全部完成,在next tick队列中的所有callbacks会被依次调用。

+

换种理解方式:

+

当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。

+

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let bar;

setTimeout(() => {
console.log('setTimeout');
}, 0)

setImmediate(() => {
console.log('setImmediate');
})
function someAsyncApiCall(callback) {
process.nextTick(callback);
}

someAsyncApiCall(() => {
console.log('bar', bar); // 1
});

bar = 1;

+

在NodeV10中上述代码执行可能有两种答案,一种为:

+
1
2
3
4
bar 1
setTimeout
setImmediate

+

另一种为

+
1
2
3
4
bar 1
setImmediate
setTimeout

+ +

无论哪种,始终都是先执行process.nextTick(callback),打印bar 1。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:搞懂EventLoop

+

文章作者:

+

发布时间:2022年03月11日 - 08:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/03/11/%E6%90%9E%E6%87%82EventLoop/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/03/11/\346\225\260\347\273\204\346\226\271\346\263\225\344\270\200\347\233\256\344\272\206\347\204\266/index.html" "b/2022/03/11/\346\225\260\347\273\204\346\226\271\346\263\225\344\270\200\347\233\256\344\272\206\347\204\266/index.html" new file mode 100644 index 0000000000..def59e96e5 --- /dev/null +++ "b/2022/03/11/\346\225\260\347\273\204\346\226\271\346\263\225\344\270\200\347\233\256\344\272\206\347\204\266/index.html" @@ -0,0 +1,785 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 数组方法哪些有副作用,一目了然! | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 数组方法哪些有副作用,一目了然! +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

今天一个小兄弟微信给我发了个消息,说是今天面试,面试官问了个比较基础的问题,没答好,我就问他问了什么,他说,数组有哪些方法会改变原来的值,他说他就说了3、4个,还不确定对不对,我想了想说其实列个表就很容易记住了,于是便有了下面这个表格。

+ +

关于数组的那些方法

其实上面的问题:

+
+

数组有哪些方法会改变原来的值

+
+

现在有个词挺火的,叫做纯函数,什么是纯函数呢,就是没有副作用,什么是没有副作用呢,就是结果是幂等的,等等….怎么越来越绕,简单来说,就是这个函数干几次都是一样的结果,出了函数就不会有任何影响,他就像一台加工机器,输入原料,输出产品,没有任何其他输出材料,比如废气之类影响大气的东西。好吧,你不明白也没关系,反正我对上面这个问题的理解就变成了

+
+

数组的哪些方法是没有副作用的。

+
+

要说有哪些方法是有副作用,首先你得知道有哪些方法,

+

比如,push,pop,shift,unshift,splice 这些别跟我说你不知道,这些都是常用的数组操作,出栈入栈,
还有什么呢,数组的排序啊,比如sort,reverse,还有一个用的不多,fill是用一个固定值填充数组的起始元素到终止元素,这些呢都是在原数组上进行操作,所以肯定是影响数组的值的,也就是说有副作用的。

+

那么哪些没有副作用呢,简单说其他的都没有,
啥!?,是的,其他的都没有。。。所以记得上面这些就好了呀,怎么样,是不简单。

+

那还剩下啥?

+
    +
  • concat 返回的是心数组,
  • +
  • join 返回的是字符串,
  • +
  • indexOf和lastIndexOf返回的是索引,
  • +
  • slice 返回的是切割后的新数组,别把它和splice混淆了哈,
  • +
  • entries 返回一个新的Array Iterator对象,
  • +
  • keys 返回一个新的包含全部Array索引的 Iterator对象,
  • +
  • values 有keys就有values,返回一个新的包含全部Array值的 Iterator对象,
  • +
  • every、some、includes 返回 布尔值,
  • +
  • filter,map 返回新数组,
  • +
  • reducer 有点神奇 返回累加值,
  • +
  • find、findIndex 找元素 一个返回第一个符合的元素本身,一个返回他的索引,
  • +
  • 最后还有个flat,展平了数组,
  • +
  • forEach 遍历数组, 这么一数起来,方法还真不少呢!
  • +
+

总结

想要记住所有的方法呢,不下点功夫是不行的,但是想要记住有副作用的,然后排除掉他们的话,还是容易的,最后做个表,方便直观的来看。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
方法作用返回结果有无副作用
push数组尾部插入元素改变了的原数组
pop数组尾部删除元素改变了的原数组
shift数组头部插入元素改变了的原数组
unshift数组头部删除元素改变了的原数组
splice增删改数组改变了的原数组
sort数组排序改变了的原数组
reverse数组反序改变了的原数组
fill填充数组改变了的原数组
concat连结数组合并的新数组
join合并数组为字符串字符串
indexOf查索引第一个索引
lastIndexOf查索引最后一个索引
slice截取数组截取的新数组
entries迭代数组迭代器对象
keys迭代数组的key全部的key
values迭代数组的value全部的value
every每个都满足布尔值
some有一些满足布尔值
includes包含某元素布尔值
filter过滤数组新数组
map加工数组新数组
reducer迭代数组累加值
find查找元素第一个元素或undifined
findIndex查找元素第一个元素的index
flat展平数组新数组
forEach遍历数组void
+

就到这里,怎么样,清楚了吧。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:数组方法哪些有副作用,一目了然!

+

文章作者:

+

发布时间:2022年03月11日 - 07:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/03/11/%E6%95%B0%E7%BB%84%E6%96%B9%E6%B3%95%E4%B8%80%E7%9B%AE%E4%BA%86%E7%84%B6/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/06/01/CodeReview\344\273\243\347\240\201\345\256\241\346\237\245\346\214\207\345\215\227/index.html" "b/2022/06/01/CodeReview\344\273\243\347\240\201\345\256\241\346\237\245\346\214\207\345\215\227/index.html" new file mode 100644 index 0000000000..fc292173ab --- /dev/null +++ "b/2022/06/01/CodeReview\344\273\243\347\240\201\345\256\241\346\237\245\346\214\207\345\215\227/index.html" @@ -0,0 +1,732 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CodeReview代码审查指南.md | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ CodeReview代码审查指南.md +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

1.关于Code Review

1.1 Code Review的目的

Code Review是一种用来确认方案设计和代码实现的质量保证机制,通过这个机制我们可以对代码、测试过程和注释进行检查。

+

Code Review主要用来在软件工程过程中改进代码质量,通过Code Review可以达到如下目的目的:

+
    +
  1. 在项目早期就能够发现代码中的BUG
  2. +
  3. 帮助初级开发人员学习高级开发人员的经验,达到知识共享
  4. +
  5. 避免开发人员犯一些很常见,很普通的错误
  6. +
  7. 保证项目组人员的良好沟通
  8. +
  9. 项目或产品的代码更容易维护
  10. +
+

1.2 Code Review的前提

进入Code Review需要检查的条件如下:

+
    +
  1. Code Review人员是否理解了Code Review的概念和Code Review将做什么
    如果做Code Review的人员不能理解Code Review对项目成败和代码质量的重要程度,他们的做法可能就会是应付了事。
  2. +
  3. 代码是否已经正确的build,build的目的使得代码已经不存在基本语法错误
    我们总不希望高级开发人员或是主管将时间浪费在检查连编译都通不过的代码上吧。
  4. +
  5. 代码执行时功能是否正确
    Code Review人员也不负责检查代码的功能是否正确,也就是说,需要复查的代码必须由开发人员或质量人员负责该代码的功能的正确性。
  6. +
  7. Review人员是否理解了代码
    做复查的人员需要对该代码有一个基本的了解,其功能是什么,是拿一方面的代码,涉及到数据库或是通讯,这样才能采取针对性的检查
  8. +
  9. 开发人员是否对代码做了单元测试
    这一点也是为了保证Code Review前一些语法和功能问题已经得到解决,Code Review人员可以将精力集中在代码的质量上。
  10. +
+

1.3 Code Review需要做什么

Code Review主要检查代码中是否存在以下方面问题:

+
    +
  • 代码的一致性
  • +
  • 编码风格
  • +
  • 代码的安全问题
  • +
  • 代码冗余
  • +
  • 是否正确设计以满足需求(性能、功能)
  • +
  • 等等
  • +
+

1.3.1 完整性检查(Completeness)

    +
  • 代码是否完全实现了设计文档中提出的功能需求
  • +
  • 代码是否已按照设计文档进行了集成和Debug
  • +
  • 代码是否已创建了需要的数据库,包括正确的初始化数据
  • +
  • 代码中是否存在任何没有定义或没有引用到的变量、常数或数据类型
  • +
+

1.3.2 一致性检查(Consistency)

    +
  • 代码的逻辑是否符合设计文档
  • +
  • 代码中使用的格式、符号、结构等风格是否保持一致
  • +
+

1.3.3 正确性检查(Correctness)

    +
  • 代码是否符合制定的标准
  • +
  • 所有的变量都被正确定义和使用
  • +
  • 所有的注释都是准确的
  • +
  • 所有的程序调用都使用了正确的参数个数
  • +
+

1.3.4 可修改性检查(Modifiability)

    +
  • 代码涉及到的常量是否易于修改(如使用配置、定义为类常量、使用专门的常量类等)
  • +
  • 代码中是否包含了交叉说明或数据字典,以描述程序是如何对变量和常量进行访问的
  • +
  • 代码是否只有一个出口和一个入口(严重的异常处理除外)
  • +
+

1.3.5 可预测性检查(Predictability)

    +
  • 代码所用的开发语言是否具有定义良好的语法和语义
  • +
  • 是否代码避免了依赖于开发语言缺省提供的功能
  • +
  • 代码是否无意中陷入了死循环
  • +
  • 代码是否是否避免了无穷递归
  • +
+

1.3.6 健壮性检查(Robustness)

    +
  • 代码是否采取措施避免运行时错误
      +
    • 数组边界溢出
    • +
    • 被零除
    • +
    • 值越界
    • +
    • 堆栈溢出
    • +
    • +
    +
  • +
+

1.3.7 结构性检查(Structuredness)

    +
  • 程序的每个功能是否都作为一个可辩识的代码块存在
    循环是否只有一个入口
  • +
+

1.3.8 可追溯性检查(Traceability)

    +
  • 代码是否对每个程序进行了唯一标识
  • +
  • 是否有一个交叉引用的框架可以用来在代码和开发文档之间相互对应
  • +
  • 代码是否包括一个修订历史记录,记录中对 代码的修改和原因都有记录
  • +
  • 是否所有的安全功能都有标识
  • +
+

1.3.9 可理解性检查(Understandability)

    +
  • 注释是否足够清晰的描述每个子程序
  • +
  • 是否使用到不明确或不必要的复杂代码,它们是否被清楚的注释
  • +
  • 使用一些统一的格式化技巧(如缩进、空白等)用来增强代码的清晰度
  • +
  • 是否在定义命名规则时采用了便于记忆,反映类型等方法
  • +
  • 每个变量都定义了合法的取值范围
  • +
  • 代码中的算法是否符合开发文档中描述的数学模型
  • +
+

1.3.10 可验证性检查(Verifiability)

代码中的实现技术是否便于测试

+

1.4 Code Review的步骤

这些是我在平时工作中的经验总结,目前也是按照这个步骤在做。

+
    +
  1. 代码编写者和代码审核者坐在一起,由代码编写者按照UC依次讲解自己负责的代码和相关逻辑,从Web层->DAO层;
  2. +
  3. 代码审核者在此过程中可以随时提出自己的疑问,同时积极发现隐藏的bug;对这些bug记录在案。
  4. +
  5. 代码讲解完毕后,代码审核者给自己安排几个小时再对代码审核一遍。
    代码需要一行一行静下心看。同时代码又要全面的看,以确保代码整体上设计优良。
  6. +
  7. 代码审核者根据审核的结果编写“代码审核报告”,“审核报告”中记录发现的问题及修改建议,然后把“审核报告”发送给相关人员。
  8. +
  9. 代码编写者根据“代码审核报告”给出的修改意见,修改好代码,有不清楚的地方可积极向代码审核者提出。
  10. +
  11. 代码编写者 bug fix完毕之后给出反馈。
  12. +
  13. 代码审核者把Code Review中发现的有价值的问题更新到”代码审核规范”的文档中,对于特别值得提醒的问题可群发email给所有技术人员。
  14. +
+

提示
Code Review必备的文档:

+
    +
  • “代码审核规范”文档:记录代码应该遵循的标准。
    +

    代码审核者根据这些标准来Code Review代码,同时在Code Review过程中不断完善该文档。

    +
    +
  • +
+

2.Code Reivew的执行

一个标准的Code Reivew活动应该分为三个阶段:

+

2.1.事前准备阶段

在一次CR前,对以下内容进行充分准备。

+

2.1.1.CR的对象

在准备CR代码对象时,我们要注意代码的数量,如果代码量比较大,要对代码进行必要的分解,确定其中的关键代码,对关键代码进行CR,可以达到举一反三的目的。

+

2.1.2.CR的内容

我们对代码的审查内容很多,如代码的编写是否规范(注释的书写格式、命名规范等)、技术处理规范(异常处理、日志处理、代码组织结构等)、业务实现等。我们不能希望通过一次CR活动,完成所有这些内容的审查,因此我们必须设定本次CR活动内容界限,确定审查重点;

+

2.1.3.评审规范和标准

在CR前设计确定评审规范和标准是必要,通过规范和标准我们在审查过程中可以有据可依,有理可循,而且还可以做到标准统一。

+

2.1.4.选择CR活动的参与者

在CR开始前,必须把本次CR活动的对象、审查内容以及审查的规范和标准通报给所有的参与者。

+

2.1.5.选择CR活动的实施方式。

CR活动有很多形式可供我们选择,我们可以根据实际情况选择桌面式CR、演示讲解式CR、一对一的座位CR等等。

+

2.2.实施阶段

充分的事前准备,只是做好CR活动的前提,在CR实施过程中,我们要做好以下工作。

+

2.2.1.准确记录

对于CR过程发现的问题,我们必须清晰准确的记录,可以使用问题点记录单,明确记录的项目和内容。

+

2.2.2.讲解与提问

CR过程中,要采用代码作者讲解和审查者提问方式。审查者不能只在发现问题时提问,同时也要根据本次审查的内容要求代码作者对某个特定问题的讲解。

+

2.2.3.逐项审查

对事前确定的审查内容,要逐项审查,不能因为时间不足等因素一扫而过。

+

2.2.4.注意气氛

实施审查时,要营造一个讨论问题、解决问题的氛围,不能把审查会搞成批判会,这样会影响相关人员的积极性。

+

2.3. 事后跟踪跟踪。

2.3.1. 确认发现的问题

CR结束后,对发现的问题,首先需要确定以下内容。

+
    +
  1. 问题点的难易程度以及影响的范围;
  2. +
  3. 解决问题的责任者和问题点修正结果的确认者;
  4. +
  5. 解决问题点的时限。
  6. +
+

2.3.2. 修正问题责任者

对于修正问题责任者,在问题点的修正过程中,要三方面内容的记录。

+
    +
  1. 问题点的原因;
  2. +
  3. 解决问题点的对策;
  4. +
  5. 修正的内容。
  6. +
+

2.3.3. 修正结果确认者

做为修正结果的确认者,必须按照事前约定的时限及时的对修正结果进行全面的确认

+

3.注意事项

3.1. 经常进行Code Review

    +
  1. 要Review的代码越多,那么要重构,重写的代码就会越多。而越不被程序作者接受的建议也会越多,唾沫口水战也会越多。
  2. +
  3. 程序员代码写得时候越长,程序员就会在代码中加入越来越多的个人的东西。
  4. +
  5. 越接近软件发布的最终期限,代码也就不能改得太多。
  6. +
+

3.2. Code Review不要太正式,而且要短

忘了那个代码评审的Checklist吧,走到你的同事座位跟前,像请师父一样请他坐到你的电脑面前,然后,花5分钟给他讲讲你的代码,给他另外一个5分钟让他给你的代码提提意见,这比什么都好。而如果你用了一个Checklist,让这个事情表现得很正式的话,下面两件事中必有一件事会发生:

+
    +
  1. 只有在Checklist上存在的东西才会被Review。
  2. +
  3. Code Reviews 变成了一种礼节性的东西,你的同事会装做很关心你的代码,但其实他心里想着尽快地离开你。
  4. +
+

只有不正式的Code Review才会让你和评审者放轻松,人只有放松了,才会表现得很真实,很真诚。记住Review只不过是一种形式,而只有在相互信任中通过相互的讨论得到了有意义和有建设性的建议和意见,那才是最实在的。不然,作者和评审者的关系就会变成小偷和警察的关系。

+

3.3. 尽可能的让不同的人Reivew你的代码

如果可能的话,不要总是只找一个人来Review你的代码,不同的人有不同的思考方式,有不同的见解,所以,不同的人可以全面的从各个方面评论你的代码。
但不要太多了,人多嘴杂反而适得其反,基本上来说,不要超过3个人,这是因为,这是一个可以围在一起讨论的最大人员尺寸。

+

下面是几个优点:

+
    +
  1. 从不同的方向评审代码总是好的。
  2. +
  3. 会有更多的人帮你在日后维护你的代码。
  4. +
  5. 这也是一个增加团队凝聚力的方法。
  6. +
+

3.4. 保持积极的正面的态度

程序员最大的问题就是“自负”,尤其当我们Reivew别人的代码的时候,我已经见过无数的场面,程序员在Code Review的时候,开始抨击别人的代码,质疑别人的能力。太可笑了,我分析了一下,这类的程序员其实并没有什么本事,因为他们指责对方的目的是想告诉大家自己有多么的牛,靠这种手段来表现自己的程序员,其实是就是传说中所说的“半瓶水”。

+

所以,无论是代码作者,还是评审者,都需要一种积极向上的正面的态度,作者需要能够虚心接受别人的建议,因为别人的建议是为了让你做得更好;评审者也需要以一种积极的正面的态度向作者提意见,因为那是和你在一个战壕里的战友。记住,你不是一段代码,你是一个人!

+

3.5. 学会享受Code Reivew

这可能是最重要的一个提示了,如果你到了一个人人都喜欢Code Reivew的团阿,那么,你会进入到一个生机勃勃的地方,在那里,每个人都能写出质量非常好的代码,在那里,你不需要经理的管理,团队会自适应一切变化,他们相互学习,相互帮助,不仅仅是写出好的代码,而且团队和其中的每个人都会自动进化,最关键的是,这个是一个团队。

+

资料来源

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:CodeReview代码审查指南.md

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/CodeReview%E4%BB%A3%E7%A0%81%E5%AE%A1%E6%9F%A5%E6%8C%87%E5%8D%97/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/06/01/Electron\347\274\226\347\240\201\350\247\204\350\214\203/index.html" "b/2022/06/01/Electron\347\274\226\347\240\201\350\247\204\350\214\203/index.html" new file mode 100644 index 0000000000..decbc4be0f --- /dev/null +++ "b/2022/06/01/Electron\347\274\226\347\240\201\350\247\204\350\214\203/index.html" @@ -0,0 +1,634 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Electron编码规范.md | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Electron编码规范.md +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

目录
代码规范 2

+
    +
  1. 说明 2
  2. +
  3. 基本原则 2
  4. +
  5. C++与Python 2
  6. +
  7. 命名相关 2
  8. +
  9. 工程目录结构 3
  10. +
+

Electron代码规范

+
    +
  1. 说明
    Electron框架正如官网介绍是使用 JavaScript, HTML 和 CSS 构建跨平台的桌面应用的框架,框架分为前端和后端两个部分,根据前后端采用的技术不同分别遵循前后端所采用技术的代码规范
  2. +
  3. 基本原则
  4. +
+
    +
  1. 不要直接在渲染进程操作主进程,通过进程通信的方式进行数据及事件响应

    +
  2. +
  3. nodejs请使用require方式引入资源不使用import方式引入

    +
  4. +
  5. 多渲染进程传参请使用localStorage或sessionStorage

    +
  6. +
  7. 资源下载不下来时请直接到官网下载https://github.com/electron/electron/releases 将文件放到 用户目录\AppData\Local\electron\Cache 下 再进行 npm install -g electron

    +
  8. +
  9. 代码中包含native模块,必须进行 rebuild操作
    调试渲染进程时增加如下代码增加mainWindow.webContents.openDevTools();

    +
  10. +
  11. +
+
    +
  1. C++与Python
    对于 C++ 和 Python, 遵循 Chromium 的编码风格. 可以使用 clang-format 来自动格式化 C++ 代码. 可以使用 script/cpplint.py 来检验文件是否符合要求。
  2. +
+

现在使用的 Python 版本是 Python 2.7。

+

C++ 代码使用了大量 Chromium 的抽象和类型,因此建议使用者熟悉它们。 一个起步的好地方是 Chromium 的《重要的抽象概念和数据库结构》文档. 该文档提到一些特殊类型,范围类型(超出范围时自动释放其内存), 记录机制等。
4. 命名相关
Electron API 使用与 Node.js 相同的大小写方案:
当模块本身是class时, 比如 BrowserWindow, 使用 大驼峰.
当模块是一组 API 时, 比如 globalShortcut时,使用 小驼峰。
当 API 是对象的属性时, 并且它复杂到足以成为一个单独的块, 比如 win.webContents, 使用 小驼峰.
对于其他非模块API, 使用自然标题, 比如 Tag 或 Process Object.

+

当创建新的 API 时, 最好使用 getter 和 setter 而不是 jQuery 的一次性函数。 举个例子, .getText() 和 .setText(text) 优于 .text([text]).

+
    +
  1. 工程目录结构
    src - main
    // 必须

    +
      +
    • main.js // 入口
    • +
    • listen.js // 监听渲染进程通信
    • +
    • windows.js // 创建渲染进程
    • +
    • log.js // 日志log4js 插件
    • +
    +

    // 可选

    +
      +
    • utils.js
    • +
    • store.js
    • +
    +
  2. +
+
    +
  • api.js
  • +
  • render
  • +
+

main 目录下为主进程代码,render 目录下为渲染进程代码
渲染进程目录结构规范与前端项目目录结构规范一致

+

主进程中必须包含入口文件,监听进程文件和窗口创建文件
其他根据后端实际使用技术可选,nodejs作为后端的话 必须
6. 打包
Electron-packager 绿色可执行包
Electron-builder 压缩安装包

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Electron编码规范.md

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/Electron%E7%BC%96%E7%A0%81%E8%A7%84%E8%8C%83/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/06/01/GIT Standard/index.html b/2022/06/01/GIT Standard/index.html new file mode 100644 index 0000000000..c998b85bbf --- /dev/null +++ b/2022/06/01/GIT Standard/index.html @@ -0,0 +1,654 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GIT Standard.md | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ GIT Standard.md +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

1、说明

项目是一个多人协同完成同一个目标的团队组织形式,在多人协作项目中,如果代码风格统一(前端代码规范和静态代码检查约束)、代码提交信息的说明准确规范(本章介绍),那么在项目开发过程及后期协作以及Bug处理时会更加方便。

+

在本文中,我将介绍大家如何利用工具及约定保证大家代码提交的统一性,从而提高大家的协同效率:

+
    +
  • commitlint: git 提交信息规范与验证
    +

    添加如ESLint的格式规范校验,规范comiit的格式,达到团队每个人的提交风格保持一直,保证提交信息的完整和准确性
    风格如下:

    +
    +
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
<type>(<scope>): <subject> // Header
// 空一行
<body> // 72
// 空一行
<footer> // 72

// 例
fix(登陆模块): 修复登录密码错误的提示

登录没有做友好型提示

修正后不存在此问题
+ +
    +
  • husky: 使git-hook更容易
    +

    husky继承了Git下所有的钩子,在触发钩子的时候,husky可以阻止不合法的commit,push等等。注意使用husky之前,必须先将代码放到git 仓库中,否则本地没有.git文件,就没有地方去继承钩子了。

    +
    +
  • +
  • standard-version: 自动生成CHANGELOG 并发布版本
  • +
+

2、git commit message 规范

commit message格式

1
2
3
4
5
类型(影响范围): 描述

问题描述

修复方式结果
+ + +

注意:
1.冒号后面有空格。
2.英文小括号。
3.正文和注脚前都要加空行。

+

type 类型

用于说明 commit 的类别,只允许使用下面13个标识。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
'feat', // feat:新增功能
'fix', // fix:bug 修复
'docs', // docs:文档更新
'style', // style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑)
'refactor', // refactor:重构代码(既没有新增功能,也没有修复 bug)
'test', // test:新增测试用例或是更新现有测试
'chore', // revert:回滚某个更早之前的提交
'revert', // build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交
'build', // build:打包生产环境代码
'ci', // ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交
'merge', // merge:分支合并 Merge branch ? of ?
'perf', // perf:性能, 体验优化
'chore' // chore:不属于以上类型的其他类型(构建过程或辅助工具的变动)
+ +

如果type为feat和fix,则该 commit 将肯定出现在 Change log 之中。

+

scope 影响范围

可根据项目组需要进行定制化设置,也可不做强制要求

+

subject 描述

subject是 commit 目的的简短描述,不超过50个字符,且结尾不加句号(.)。

+

body 详细描述

body 部分是对本次 commit 的详细描述,可以分成多行。下面是一个范例。

+
1
2
3
此次提交内容包括如下信息:
- 登录为空校验
- 密码错误提示
+

Footer 部分只用于两种情况。

+
    +
  • 不兼容变动(改变解决方案)
  • +
  • 关闭 Issue(回复bug)
  • +
+

3、使用工具校验commit是否符合规范

commitlint

3.1 commitlint安装

1
2
// npm 安装
npm install --save-dev @commitlint/{cli,config-conventional}
+ +

3.2 生成commitlint.config.js配置文件

执行echo命令创建配置文件,也可以手动创建配置文件,或者从已有的配置文件进行拷贝。

+
1
2
// 项目根目录创建commitlint.config.js
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
+ + +

3.3 在commitlint.config.js制定提交message规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// type(scope?): subject

// body?

// footer?
module.exports = {
// 继承自默认规则
extends: ['@commitlint/config-conventional'],
// 这里写自定义规则
rules: {
// 等级 [0 1 2]: 0 不使用规则 1 警告 2 错误
// 启用范围 always|never: 总是或者永不.
// 规则值: 规则对应的值

// 头部(包含type、scope、subject)
'header-case': [0, 'always', 'lower-case'],
'header-full-stop': [0, 'never', '.'],
'header-max-length': [0, 'always', 72],
'header-min-length': [0, 'always', 0],

// 提交类型
'type-case': [0, 'never'],
'type-empty': [2, 'never'],
'type-max-length': [0, 'always', Infinity],
'type-min-length': [0, 'always', 0],
'type-enum': [
2,
'always',
[
'feat', // feat:新增功能
'fix', // fix:bug 修复
'docs', // docs:文档更新
'style', // style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑)
'refactor', // refactor:重构代码(既没有新增功能,也没有修复 bug)
'test', // test:新增测试用例或是更新现有测试
'revert', // revert:回滚某个更早之前的提交
'build', // build:build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交,打包生产环境代码
'ci', // ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交
'merge', // merge:分支合并 Merge branch ? of ?
'perf', // perf:性能, 体验优化
'chore' // chore:不属于以上类型的其他类型(构建过程或辅助工具的变动)
]
],

// 影响范围
'scope-case': [0, 'always', 'lower-case'], // 书写格式
'scope-max-length': [0, 'always', Infinity], // 最长
'scope-min-length': [0, 'always', 0], // 最短
'scope-empty': [2, 'never'], // 影响范围:为空|永不
'scope-enum': [0, 'always', ['a', 'b']], // 影响范围:给出范围但是不在给定的列表内

// 简介
'subject-empty': [2, 'never'], // 简介不能为空
'subject-full-stop': [0, 'never', '.'], // 简介结尾以.结束
'subject-case': [0, 'never'],
'subject-max-length': [0, 'always', Infinity],
'subject-min-length': [0, 'always', 1],

// 正文
'body-leading-blank': [2, 'always'],
'body-max-length': [0, 'always', Infinity],
'body-max-line-length': [0, 'always', Infinity],
'body-min-length': [0, 'always', 1],

// 注脚
'footer-leading-blank': [2, 'always'],
'footer-max-length': [0, 'always', Infinity],
'footer-max-line-length': [0, 'always', Infinity],
'footer-min-length': [0, 'always', 1],

// 其他
'references-empty': [0, 'never'],
'signed-off-by': [0, 'always', 'Signed-off-by']
}
}
+ +

上面我们就完成了commitlint的安装与提交规范的制定。检验commit message的最佳方式是结合git hook,所以需要配合Husky

+

husky

3.4 husky安装

1
npm install husky --save-dev
+

3.5 husky 配置

安装成功后需要在项目下的package.json中配置

+
1
2
3
4
5
6
7
8
9
10
11
// package.json
{
"husky": {
"hooks": { // husky的钩子
"pre-commit": "npm run lint" // 提交前进行代码静态校验
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS" // 进行提交信息格式校验
}
},
"scripts": {}
...
}
+ +

3.5 husky 执行测试

最后我们可以正常的git操作

+
1
2
git add .
git commit -m ""
+ +

git commit的时候会触发commlint。下面演示下不符合规范提交示例:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
C:\lixg\git>git commit -m "thunisoft: abcde"

husky > npm run -s commitmsg (node v8.2.1)

⧗ input:
thunisoft(all): abcde

✖ type must be one of [feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert] [type-enum]
✖ found 1 problems, 0 warnings

husky > commit-msg hook failed (add --no-verify to bypass)

C:\lixg\git>
+ +

上面的提交看反馈消息得知是type格式没有符合限制的提交类型列表,所以提交失败,虾米啊我们把type改为feat再试一下

+
1
2
3
4
5
6
7
8
9
10
11
C:\lixg\git>git commit -m "feat(all): 新功能"

husky > npm run -s commitmsg (node v8.2.1)

⧗ input: feat: 新功能
✔ found 0 problems, 0 warnings

[develop 19dfhe] feat: 新功能
1 file changed, 1 insertion(+)

C:\lixg\git>
+ +

修改后格式符合规范,提交成功。

+

参考来源

+

版权声明

Copyright by lixuguang
未经授权,严禁转载。如需转载,请联系作者

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:GIT Standard.md

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/GIT%20Standard/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/06/01/Restful API \347\232\204\350\256\276\350\256\241\350\247\204\350\214\203/index.html" "b/2022/06/01/Restful API \347\232\204\350\256\276\350\256\241\350\247\204\350\214\203/index.html" new file mode 100644 index 0000000000..88d0b716ab --- /dev/null +++ "b/2022/06/01/Restful API \347\232\204\350\256\276\350\256\241\350\247\204\350\214\203/index.html" @@ -0,0 +1,823 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Restful API 的设计规范.md | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Restful API 的设计规范.md +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

1. URI

URI 表示资源,资源一般对应服务器端领域模型中的实体类。

+

URI规范

    +
  1. 不用大写;
  2. +
  3. 用中杠-不用下杠_;
  4. +
  5. 参数列表要encode;
  6. +
  7. URI中的名词表示资源集合,使用复数形式。
  8. +
+

资源集合 vs 单个资源

URI表示资源的两种方式:资源集合、单个资源。

+

资源集合:

+
1
2
/zoos //所有动物园
/zoos/1/animals //id为1的动物园中的所有动物
+

单个资源:

+
1
2
/zoos/1 //id为1的动物园
/zoos/1;2;3 //id为1,2,3的动物园
+

避免层级过深的URI

/在url中表达层级,用于按实体关联关系进行对象导航,一般根据id导航。

+

过深的导航容易导致url膨胀,不易维护,如 GET /zoos/1/areas/3/animals/4,尽量使用查询参数代替路径中的实体导航,如GET /animals?zoo=1&area=3

+

对Composite资源的访问

服务器端的组合实体必须在uri中通过父实体的id导航访问。

+
+

组合实体不是first-class的实体,它的生命周期完全依赖父实体,无法独立存在,在实现上通常是对数据库表中某些列的抽象,不直接对应表,也无id。一个常见的例子是 User — Address,Address是对User表中zipCode/country/city三个字段的简单抽象,无法独立于User存在。必须通过User索引到Address:GET /user/1/addresses

+
+

2. Request

HTTP方法

通过标准HTTP方法对资源CRUD:

+

GET:查询

+
1
2
3
GET /zoos
GET /zoos/1
GET /zoos/1/employees
+ + +

POST:创建单个资源。POST一般向“资源集合”型uri发起

+
1
2
POST /animals  //新增动物
POST /zoos/1/employees //为id为1的动物园雇佣员工
+ +

PUT:更新单个资源(全量),客户端提供完整的更新后的资源。与之对应的是 PATCH,PATCH 负责部分更新,客户端提供要更新的那些字段。PUT/PATCH一般向“单个资源”型uri发

+
1
2
PUT /animals/1
PUT /zoos/1
+ +

DELETE:删除

+
1
2
3
DELETE /zoos/1/employees/2
DELETE /zoos/1/employees/2;4;5
DELETE /zoos/1/animals //删除id为1的动物园内的所有动物
+

HEAD / OPTION 用的不多,就不多解释了。

+

安全性和幂等性

    +
  1. 安全性:不会改变资源状态,可以理解为只读的;
  2. +
  3. 幂等性:执行1次和执行N次,对资源状态改变的效果是等价的。
  4. +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
.安全性幂等性
GET
POST××
PUT×
DELETE×
+

安全性和幂等性均不保证反复请求能拿到相同的response。以 DELETE 为例,第一次DELETE返回200表示删除成功,第二次返回404提示资源不存在,这是允许的。

+

复杂查询

查询可以捎带以下参数:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
.示例备注
过滤条件type=1&age=16允许一定的uri冗余,如/zoos/1与/zoos?id=1
排序?sort=age,desc
投影?whitelist=id,name,email
分页?limit=10&offset=3
+

Bookmarker

经常使用的、复杂的查询标签化,降低维护成本。

+

如:

+
1
2
GET /trades?status=closed&sort=created,desc

+

快捷方式:

+
1
2
3
GET /trades#recently-closed
// 或者
GET /trades/recently-closed
+ +

Format

只用以下常见的3种body format:

+
    +
  1. Content-Type: application/json
  2. +
+
1
2
3
4
5
6
7
8
9
10
POST /v1/animal HTTP/1.1
Host: api.example.org
Accept: application/json
Content-Type: application/json
Content-Length: 24

{
"name": "Gir",
"animalType": "12"
}
+ +
    +
  1. Content-Type: application/x-www-form-urlencoded (浏览器POST表单用的格式)
  2. +
+
1
2
3
4
5
6
7
POST /login HTTP/1.1
Host: example.com
Content-Length: 31
Accept: text/html
Content-Type: application/x-www-form-urlencoded

username=root&password=Zion0101
+ +
    +
  1. Content-Type: multipart/form-data; boundary=—-RANDOM_jDMUxq4Ot5 (表单有文件上传时的格式)
  2. +
+

6. Response

    +
  1. 不要包装:
    response 的 body 直接就是数据,不要做多余的包装。
  2. +
+

错误示例

+
1
2
3
4
{
"success":true,
"data":{"id":1,"name":"xiaotuan"},
}
+

各HTTP方法成功处理后的数据格式:

+ + + + + + + + + + + + + + + + + + + + + + + +
·response 格式
GET单个对象、集合
POST新增成功的对象
PUT/PATCH更新成功的对象
DELETE
+
    +
  1. json格式的约定:

    +
      +
    1. 时间用长整形(毫秒数),客户端自己按需解析(moment.js)
    2. +
    3. 不传null字段
    4. +
    +
  2. +
+

分页response

1
2
3
4
{
"paging":{"limit":10,"offset":0,"total":729},
"data":[{},{},{}...]
}
+

7. 错误处理

    +
  1. 不要发生了错误但给2xx响应,客户端可能会缓存成功的http请求;
  2. +
  3. 正确设置http状态码,不要自定义;
  4. +
  5. Response body 提供
      +
    1. 错误的代码(日志/问题追查);
    2. +
    3. 错误的描述文本(展示给用户)。
    4. +
    +
  6. +
+

对第三点的实现稍微多说一点:

+

Java 服务器端一般用异常表示 RESTful API 的错误。API 可能抛出两类异常:业务异常非业务异常

+

业务异常由自己的业务代码抛出,表示一个用例的前置条件不满足、业务规则冲突等,比如参数校验不通过、权限校验失败。

+

非业务类异常表示不在预期内的问题,通常由类库、框架抛出,或由于自己的代码逻辑错误导致,比如数据库连接失败、空指针异常、除0错误等等。

+

业务类异常必须提供2种信息:

+
    +
  1. 如果抛出该类异常,HTTP 响应状态码应该设成什么;
  2. +
  3. 异常的文本描述;
  4. +
+

在Controller层使用统一的异常拦截器:

+
    +
  1. 设置 HTTP 响应状态码:对业务类异常,用它指定的 HTTP code;对非业务类异常,统一500;
  2. +
  3. Response Body 的错误码:异常类名
  4. +
  5. Response Body 的错误描述:对业务类异常,用它指定的错误文本;对非业务类异常,线上可以统一文案如“服务器端错误,请稍后再试”,开发或测试环境中用异常的 stacktrace,服务器端提供该行为的开关。
  6. +
+

常用的http状态码及使用场景:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
状态码使用场景
400 bad request常用在参数校验
401 unauthorized未经验证的用户,常见于未登录。如果经过验证后依然没权限,应该 403(即 authentication 和 authorization 的区别)。
403 forbidden无权限
404 not found资源不存在
500 internal server error非业务类异常
503 service unavaliable由容器抛出,自己的代码不要抛这个异常
+

8. 服务型资源

除了资源简单的CRUD,服务器端经常还会提供其他服务,这些服务无法直接用上面提到的URI映射。如:

+
    +
  1. 按关键字搜索;
  2. +
  3. 计算地球上两点间的距离;
  4. +
  5. 批量向用户推送消息;
  6. +
+

可以把这些服务看成资源,计算的结果是资源的presentation,按服务属性选择合适的HTTP方法。

+

例:

+
1
2
3
4
GET /search?q=filter?category=file  搜索
GET /distance-calc?lats=47.480&lngs=-122.389&late=37.108&lnge=-122.448
POST /batch-publish-msg
[{"from":0,"to":1,"text":"abc"},{},{}...]
+

9. 异步任务

对耗时的异步任务,服务器端接受客户端传递的参数后,应返回创建成功的任务资源,其中包含了任务的执行状态。客户端可以轮训该任务获得最新的执行进度。

+
1
2
3
4
5
6
7
8
9
// 提交任务:
POST /batch-publish-msg
[{"from":0,"to":1,"text":"abc"},{},{}...]

// 返回:
{"taskId":3,"createBy":"Anonymous","status":"running"}

GET /task/3
{"taskId":3,"createBy":"Anonymous","status":"success"}
+ +

如果任务的执行状态包括较多信息,可以把“执行状态”抽象成组合资源,客户端查询该状态资源了解任务的执行情况。

+
1
2
3
4
5
6
7
8
9
// 提交任务:
POST /batch-publish-msg
[{"from":0,"to":1,"text":"abc"},{},{}...]

// 返回:
{"taskId":3,"createBy":"Anonymous"}

GET /task/3/status
{"progress":"50%","total":18,"success":8,"fail":1}
+ +

10. API的演进

版本

常见的三种方式:

+
    +
  1. 在uri中放版本信息:GET /v1/users/1
  2. +
  3. Accept Header:Accept: application/json+v1
  4. +
  5. 自定义 Header:X-Api-Version: 1
  6. +
+

用第一种,虽然没有那么优雅,但最明显最方便。

+

URI失效

随着系统发展,总有一些API失效或者迁移,对失效的API,返回404 not found 或 410 gone;对迁移的API,返回 301 重定向。

+

11. 安全

这个不熟,接触到的时候再说。

+

参考文档

    +
  • < RESTful Web Services Cookbook >
  • +
  • Consumer-Centric API Design
  • +
  • RESTful Best Practices
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Restful API 的设计规范.md

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/Restful%20API%20%E7%9A%84%E8%AE%BE%E8%AE%A1%E8%A7%84%E8%8C%83/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/06/01/squid/index.html b/2022/06/01/squid/index.html new file mode 100644 index 0000000000..a33f7ac37c --- /dev/null +++ b/2022/06/01/squid/index.html @@ -0,0 +1,606 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Squid 服务器学习笔记 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Squid 服务器学习笔记 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Squid 服务器学习笔记

Squid 服务器介绍

用来缓存前端资源的代理服务器,可以类比后端数据库缓存软件Redis。
当用户访问目标资源时由Squid服务器进行判断如果有数据缓存直接将缓存数据返回,如果没有缓存,代理客户访问资源,获取到后返回给用户,当有其他用户访问相同资源时直接返回资源。

+

代理方式

    +
  1. 普通代理
  2. +
  3. 透明代理
  4. +
  5. 反向代理
  6. +
+

安装

1
2
3
4
5
6
7
8
9
10
11
12
// 检查是否已经安装
rpm -qa | grep squid
// 安装软件
rpm install squid
// 设置开机启动
chkconfig squid on
// 启动服务
service squid start
// 查看服务状态(端口监听3128)
netstat -anput |grap squid
// 关闭服务
service squid stop
+ +

配置文件

/etc/squid/squid.config

+

10.0.0.0/8

+
+

扩展知识
CIDR - 无类域间路由CIDR(Classless InterDomain Routing)
ip每位占1个字节8位,00000000 ~ 11111111,也就是0 ~ 255,ip共有四个位数。
10.0.0.0/8 的意思是 8位不可变,剩下的可变,8位这里的意思是说第一个IP段,那么也就是说IP范围是10.0.0.0 ~ 10.255.255.255 这样的一个ip范围。

+
+

// 复制并重命名
mv /etc/squid/squid.config{,.bak}
// 删除文件中的注释和空行(只保留有效设定)
awk ‘if($0!~ /^#/ && $0~ !/^$/print $0)’ /etc/squid/squid.config.bak > /etc/squid/squid.config

+

squid常用命令:
/usr/local/squid/sbin/squid -z 初始化缓存空间
/usr/local/squid/sbin/squid 启动
/usr/local/squid/sbin/squid -k shutdown 停止
/usr/local/squid/sbin/squid -k reconfigure 重新载入配置文件
/usr/local/squid/sbin/squid -k rotate 轮循日志

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#acl all src 0.0.0.0/0.0.0.0 and http_access allow all选项定义了一个访问控制列表。详细情况参见和Squid软件
#携带的文档。这里的访问控制列表允许所有对代理服务的访问,因为这里该代理是加速web服务器。
acl all src 0.0.0.0/0.0.0.0
#允许所有IP访问
acl manager proto http #manager url协议为http
acl localhost src 127.0.0.1/255.255.255.255 #允午本机IP
acl to_localhost dst 127.0.0.1 #允午目的地址为本机IP
acl Safe_ports port 80 # 允许安全更新的端口为80
acl CONNECT method CONNECT #请求方法以CONNECT
http_access allow all #允许所有人使用该代理.因为这里是代理加速web服务器
http_reply_access allow all #允许所有客户端使用该代理

acl OverConnLimit maxconn 16 #限制每个IP最大允许16个连接,防止攻击
http_access deny OverConnLimit

icp_access deny all #禁止从邻居服务器缓冲内发送和接收ICP请求.
miss_access allow all #允许直接更新请求
ident_lookup_access deny all #禁止lookup检查DNS
http_port 8080 transparent #指定Squid监听浏览器客户请求的端口号。

hierarchy_stoplist cgi-bin ? #用来强制某些特定的对象不被缓存,主要是处于安全的目的。
acl QUERY urlpath_regex cgi-bin \?
cache deny QUERY

cache_mem 1 GB #这是一个优化选项,增加该内存值有利于缓存。应该注意的是:
#一般来说如果系统有内存,设置该值为(n/)3M。现在是3G 所以这里1G
fqdncache_size 1024 #FQDN 高速缓存大小
maximum_object_size_in_memory 2 MB #允许最大的文件载入内存

memory_replacement_policy heap LFUDA #动态使用最小的,移出内存cache
cache_replacement_policy heap LFUDA #动态使用最小的,移出硬盘cache

cache_dir ufs /home/cache 5000 32 512 #高速缓存目录 ufs 类型使用的缓冲值最大允午1000MB空间,
#32个一级目录,512个二级目录

max_open_disk_fds 0 #允许最大打开文件数量,0 无限制
minimum_object_size 1 KB #允午最小文件请求体大小
maximum_object_size 20 MB #允午最大文件请求体大小

cache_swap_low 90 #最小允许使用swap 90%
cache_swap_high 95 #最多允许使用swap 95%

ipcache_size 2048 # IP 地址高速缓存大小 2M
ipcache_low 90 #最小允许ipcache使用swap 90%
ipcache_high 95 #最大允许ipcache使用swap 90%


access_log /var/log/squid/access.log squid #定义日志存放记录
cache_log /var/log/squid/cache.log squid
cache_store_log none #禁止store日志

emulate_httpd_log on #将使Squid仿照Web服务器的格式创建访问记录。如果希望使用
#Web访问记录分析程序,就需要设置这个参数。

refresh_pattern . 0 20% 4320 override-expire override-lastmod reload-into-ims ignore-reload #更新cache规则

acl buggy_server url_regex ^http://.... http:// #只允许http的请求
broken_posts allow buggy_server

acl apache rep_header Server ^Apache #允许apache的编码
broken_vary_encoding allow apache

request_entities off #禁止非http的标分准请求,防止攻击
header_access header allow all #允许所有的http报头
relaxed_header_parser on #不严格分析http报头.
client_lifetime 120 minute #最大客户连接时间 120分钟

cache_mgr sky@test.com #指定当缓冲出现问题时向缓冲管理者发送告警信息的地址信息。

cache_effective_user squid #这里以用户squid的身份Squid服务器
cache_effective_group squid

icp_port 0 #指定Squid从邻居服务器缓冲内发送和接收ICP请求的端口号。
#这里设置为0是因为这里配置Squid为内部Web服务器的加速器,
#所以不需要使用邻居服务器的缓冲。0是禁用

cache_peer 127.0.0.1 parent 80 0 no-query default multicast-responder no-netdb-exchange #cache_peer 设置允许更新缓存的主机,因是本机所以127.0.0.1
cache_peer_domain 127.0.0.1
hostname_aliases 127.0.0.1

error_directory /usr/share/squid/errors/Simplify_Chinese #定义错误路径

always_direct allow all # cache丢失或不存在是允许所有请求直接转发到原始服务器
ignore_unknown_nameservers on #开反DNS查询,当域名地址不相同时候,禁止访问
coredump_dir /var/log/squid #定义dump的目录

max_filedesc 2048 #最大打开的文件描述

half_closed_clients off #使Squid在当read不再返回数据时立即关闭客户端的连接。
#有时read不再返回数据是由于某些客户关闭TCP的发送数据
#而仍然保持接收数据。而Squid分辨不出TCP半关闭和完全关闭。

buffered_logs on #若打开选项“buffered_logs”可以稍稍提高加速某些对日志文件的写入,该选项主要是实现优化特性。

acl tianya referer_regex -i tianya #防止天涯盗链,转嫁给百度
http_access deny tianya
deny_info tianya

acl baidu req_header User-Agent Baiduspider #阻止baidu蜘蛛
http_access deny baidu

acl OverConnLimit maxconn 128 #限制同一IP客户端的最大连接数
http_access deny OverConnLimit

acl myip dst 222.18.63.37 #防止被人利用为HTTP代理,设置允许访问的IP地址
http_access deny !myip

acl Manager proto cache_object #允许本地管理
acl Localhost src 127.0.0.1 222.18.63.37
http_access allow Manager Localhost
cachemgr_passwd 53034338 all
http_access deny Manager

acl all src 0.0.0.0/0.0.0.0 #仅仅允许80端口的代理
acl Safe_ports port 80 # http
http_access deny !Safe_ports
http_access allow all

visible_hostname happy.swjtu.edu.cn #Squid信息设置
cache_mgr ooopic2008@qq.com

cache_effective_user squid #基本设置
cache_effective_group squid
tcp_recv_bufsize 65535 bytes

cache_peer 127.0.0.1 parent 80 0 no-query originserver #2.6的反向代理加速配置

error_directory /usr/local/squid/share/errors/Simplify_Chinese #错误文档

icp_port 0 #单台使用,不使用该功能

hierarchy_stoplist cgi-bin ?

acl QUERY urlpath_regex cgi-bin \? .php .cgi .avi .wmv .rm .ram .mpg .mpeg .zip .exe
cache deny QUERY

acl apache rep_header Server ^Apache
broken_vary_encoding allow apache


refresh_pattern ^ftp: 1440 20% 10080
refresh_pattern ^gopher: 1440 0% 1440
refresh_pattern . 0 20% 4320

cache_store_log none
pid_filename /usr/local/squid/var/logs/squid.pid
emulate_httpd_log on
logformat combined %>a %ui %un [%tl] "%rm %ru HTTP/%rv" %Hs %<st "%{Referer}>h" "%{User-Agent}>h" %Ss:%Sh
cache_log /usr/local/squid/var/logs/cache.log
access_log /usr/local/squid/var/logs/access.log combined
coredump_dir /usr/local/squid/var/cache
cache_dir ufs /usr/local/squid/var/cache 10000 16 256

dns_children 32
hosts_file /etc/hosts

cache_mem 400 MB
cache_swap_low 90
cache_swap_high 95
maximum_object_size 32768 KB
maximum_object_size_in_memory 4096 KB
emulate_httpd_log on

acl picurl url_regex -i \.bmp$ \.png$ \.jpg$ \.gif$ \.jpeg$ #防止盗链
acl mystie1 referer_regex -i happy.swjtu.edu.cn
http_access allow mystie1 picurl
acl nullref referer_regex -i ^$
http_access allow nullref
acl hasref referer_regex -i .+
http_access deny hasref picurl
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Squid 服务器学习笔记

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/squid/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\346\214\207\345\215\227/index.html" "b/2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\346\214\207\345\215\227/index.html" new file mode 100644 index 0000000000..0a377e6da8 --- /dev/null +++ "b/2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\346\214\207\345\215\227/index.html" @@ -0,0 +1,728 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前后端分离开发指南 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前后端分离开发指南 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

背景

前后端分离狭义上来讲是以浏览器为分界,应用在浏览器内部的技术为前端技术,主要负责页面展示的部分,应用在浏览器外的技术为后端技术,主要负责处理业务逻辑和数据准备工作,但是随着近几年NodeJs的崛起,前端的工作范畴明显扩大,前后端分离也不仅仅作为一种开发模式,更是web应用架构层面的一种模式。

+

认识

    +
  1. 在开发阶段,前后端工程师要提前约定好数据交互的接口,从而实现并行开发和测试;
  2. +
  3. 在运行阶段,前后端分离模式需要对web应用进行分离部署,前后端之前使用HTTP或者其他协议进行交互请求。(NodeJs或Ngnix)
  4. +
+

由上述两条可以看出前后端分离分为开发阶段和部署阶段,通常来说我们做到的只是前后端分离开发,并没有做到分离部署。

+

分解

作为一种全新的架构模式,前后端分离需要从四个方面来比较和认识。

+
    +
  1. 交互形式
  2. +
  3. 开发模式/流程
  4. +
  5. 代码组织方式
  6. +
  7. 数据接口规范流程
  8. +
+

一、交互形式

在前后端分离架构中,后端只需要负责按照约定的数据格式向前端提供可调用的API服务即可。前后端之间通过HTTP请求进行交互,前端获取到数据后,进行页面的组装和渲染,最终返回给浏览器。

+

这里有一个经常引起激烈讨论的话题就是,“约定的数据格式”,是后端直接提供给前端可直接使用的数据格式,还是简单的数据库拉取数据不做加工直接推给前端,这个要视具体的项目情况而定,如果后端代码需要支持的不仅仅是一端,比如要同时适配pc端、手机端等多个终端,其多终端数据展现的形式如果各不相同,那后端只需要提供满足多端的基础数据格式即可,但如果只需要满足单个终端,那么建议直接将前后端数据格式进行统一处理,方便前后端协同,数据处理可以放在java端做也可以放在NodeJs端做,视人员配备情况而定,但绝不是后端不做任何处理就将数据返回给前端,完全由前端组织数据。

+

二、开发模式/流程

在前后端分离模式未诞生之前,前后端属于一个整体,那时使用的是MVC架构模式,前端对应的就是View层,主要通过html/css/js实现静态页面和动态效果,在有后端进行模板变量的嵌套和一些页面逻辑的处理,最终打包成一个整体,部署到同一服务器上,同时会进行简单的动静态分离部署。

+

此时开发的流程如下:
需求=》前后端并行开发=》前端开发静态页面=》后端套模板=》集成问题=》前后端调整=》再集成=》解决集成问题=》交付上线
// TODO 这里插图

+

出现前后端分离架构之后,前端工程师只需要编写前端页面+前端数据、业务逻辑处理,之后通过HTTP或其他请求方式调用后端提供的服务接口就可以了,而且除了在开发周期可以进行前后端分离,在部署阶段,前后端也可进行分离部署。
// TODO 这里插图

+

此时开发的流程如下:
需求=》设计接口、约定数据=》前后端并行开发=》集成=》调整=》集成成功=》交付上线

+

通过上面的描述及流程,不难发现,前后端分离的开发方式不仅仅从分工上进行了区分,更重要的是在并行开发的问题上解决了反复集成等前后端互相影响的问题,从而降低了开发的难度,简化了开发的流程。

+

三、代码组织方式

// TODO 这里插图
在传统的开发模式架构下,前端代码是作为项目的静态资源存在于项目工程下,页面中还夹着一些后端代码如jsp、php等技术,前后端开发时需要将整个项目代码完整的引入开发工具才能进行开发,前后端同时维护一份代码,这种开发方式导致前后端代码互相影响,因此前后端分离势在必行。

+

而前后端分离模式在代码的组织形式上由以下两种形式组成:

+
    +
  1. 半分离
    前后端仍共用一个代码库,但是代码分别存放在两个工程中。后端不关心或很少 关心前端元素的输出情况,前端不能独立进行开发和测试,项目中缺乏前后端 交互的测试用例。
  2. +
  3. 完全分离
    完全分离后,前端代码可以通过Mock来模拟后端请求,从此可以独立进行前端开发和测试。后端代码只需要按照跟前端约定好的接口格式写出完整的测试用例,确保接口的可用性。通过上述手段,降低开发集成风险。
  4. +
+

四、数据接口规范流程

通过上面三段的描述,我们可以看出前后端分离开发模式最重要也是最初的阶段就是数据接口的确定,因此在项目开发前必需先进行数据和接口的定义,数据接口的定义需要前后端开发共同商定,包括确定的数据格式,交互形式,并生成一份接口文档供前后端开发人员使用。之后才是并行开发。开发期间前后端双方需要严格按照确定的数据接口文档进行开发,前端开发完之后可以利用mock服务独自进行接口测试,后端也可以利用postman或其他接口测试工具进行测试,并提供完整的接口测试用例,然后前后端进行功能联调,最后再提交线上测试,也可进行自动化测试。

+

// TODO 这里插图

+

分离后的收益

到底分不分,如何分是个持续讨论的话题,通过上述的内容大家已经了解到了,什么是前后端分离,也知道如何进行前后端分离开发部署,那么前后端分离能带来哪些收益呢?

+

首先,就目前的软件开发应用趋势来看,越来越注重用户的体验性,而且架构越来越大,服务越来越小,而且终端设备越来越丰富,而原来不分离的方式已经不能支撑现在的发展趋势,因此前后端分离开发及部署将势在必行。

+

而且采用前后端分离的架构之后,我们将有如下几点提升:

+
    +
  1. 前后端分离后,前后端将不再互相纠缠而是各自在自己熟悉的领域进行开发工作,这将有利于前后端深化优化各自的代码,培养各自独特的技术特性,从而开发出更加优秀的应用,建立起专业精良的全栈开发团队。

    +
  2. +
  3. 通过前后端分离架构可以实现前后端开发从代码及开发流程上的完全解耦,只需要前后端共同商定好接口后,便可完全独立开发,只需要在联调阶段进行好协作,在此之前可以互不影响的进行并行开发,即是需求发生了变动,但只要不影响接口,后端既可以不用修改代码,只需前端进行变动即可,如此整体的开发效率将得到提升。

    +
  4. +
  5. 前后端分离后,能够更好的适应前端日益增多的的终端适配,代码解耦后复用率更高。

    +
  6. +
+

4.前后端分离后,前后端代码可以分别管理,代码不再混在一起,代码可维护性也增强了

+

前后端分离后收益不止以上四点,因为分离而带来的职责上、技术上、代码上、部署上的解耦让开发工作比以往任何时候都要更加专注和轻松。

+

注意事项

前后端分离误区

    +
  1. 前端人员不充足,不能进行前后端分离。
  2. +
+

此话说来是因为对前后端分离后职责区分不明确导致的问题,因为以往的前端只需要写静态页面就可以了,而前后端分离后前端也不仅仅需要写静态页面,而且还要为页面提供数据和页面逻辑的处理,但实际上可以根据团队情况来区别对待,如果团队前端人员充足,那么可以由前端人员负责多一些的工作,比如API请求后的业务逻辑的处理,页面逻辑的处理、页面数据的准备等,如果后端人员配备充足,那么上述几个环节仍然可由后端人员进行处理,前端开发仍然只是写静态页面,只是内容和逻辑不再写死而是通过js或其他手段如mvvm的框架进行处理。

+
    +
  1. 前后端分离后前端任务加重,职责也不清晰。
  2. +
+

如第一点描述可知,问题不在前后端分离的模式是否合适,而是任务分配和人员分配上的问题,如果前端能力强且人员比例较多,那么部分任务可以由前端承担,如果后端人员多,那么任务由后端承担。

+
    +
  1. 后端开发需要增加接口开发工作,增加任务量。
  2. +
+

无论如何后端开发都是需要写接口的,只是前后端分离后需要按照ResetFul风格写接口,或者采用最新的GraphQl的方式进行交互,如果说这个阶段需要前后端进行商量确定接口交互形式和数据格式花费了时间,但是在接下来前后端并行开发及问题解决上省掉的时间是更加可观的。

+
    +
  1. 分离后仍出现互相等待的问题,反而不如传统开发模式快。
  2. +
+

这个问题的产生其实也是由于对前后端分离后技术缺失导致的,常见情况是前端写完页面逻辑和假数据后后端开发还未完成接口开发导致无法进行联调,实际上前端通过mockserver等方式是可以解决一些问题的。

+

前后端分离适用场景

现代化的web应用适合用前后端分离的开发方式。
原因有以下几点:

+
    +
  1. web应用前端页面交互复杂。
      +
    • 页面渲染数据量大。
    • +
    • 页面包含复杂的业务逻辑。
    • +
    +
  2. +
  3. 终端适配情况多。
  4. +
  5. 分布式架构,微服务化应用场景。
  6. +
+

前后端分离具体方案

总体方向

后端专注于:后端控制层(Restful API) & 服务层 & 数据访问层;

+

前端专注于:前端控制层(Nodejs) & 视图层

+
    +
  1. 项目设计阶段,前后端架构负责人将项目整体进行分析,讨论并确定API风格、职责分配、开发协助模式,确定人员配备;设计确定后,前后端人员共同制定开发接口。

    +
  2. +
  3. 项目开发阶段,前后端分离是各自分工,协同敏捷开发,后端提供Restful API,并给出详细文档说明,前端人员进行页面渲染前台的任务是发送API请(GET,PUT,POST,DELETE等)获取数据(json,xml)后渲染页面。

    +
  4. +
  5. 项目测试阶段,API完成之前,前端人员会使用mock server进行模拟测试,后端人员采用junit进行API单元测试,不用互相等待;API完成之后,前后端再对接测试一下就可以了,当然并不是所有的接口都可以提前定义,有一些是在开发过程中进行调整的。

    +
  6. +
  7. 项目部署阶段,利用nginx 做反向代理,即Java + nodejs + nginx 方式进行。

    +
  8. +
+

技术手段

    +
  • 前端技术栈:前端代码 + mock服务
  • +
  • 后端技术栈:postman + 接口 + 后端业务逻辑 + 数据库
  • +
  • 公共依赖:接口文档/接口测试工具
  • +
+

常用的mock服务为jsonserver
常用的接口测试工具为postman、rap、swagger、doclever

+

部署方案

    +
  1. 第一阶段为前后端同一个代码库,同一个服务器,集中部署。
  2. +
  3. 第二阶段引入Ngnix服务作为中间件,前端向Ngnix发请求,Ngnix向后端服务发请求,由于Ngnix为静态服务器,所以在seo优化上和页面性能优化上效果不明显,因此前端仍需与后端进行配合才能达到整体的优化。
  4. +
+

浏览器 =》 Ngnix(前端机)=》Ngnix(后端机可没有)=》Server服务

+
    +
  1. 第三阶段引入nodejs作为中间层,将前端资源部署到Server层。同时实现数据代理服务,负责与提供数据的后端进行通信。
  2. +
+

浏览器=》Ngnix(前端机)=》NodeServer =》Server服务

+

浏览器向前端机发送请求,由Ngnix进行分发,url统一分发至NodeServer,在Node Server中根据请求类型从后端服务器上通过RPC服务请求页面的模板数据,然后进行页面的组装和渲染;API请求则直接转发到后端服务挖成相应。

+

前后端分离部署方案比较

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
属性字段传统模式Ngnix+ServerNode+ServerNgnix+Node+Server
SEOoknookok
浏览器渲染负担oknookok
前后端耦合nookokok
请求相应效率nooknook
+

结语

随着前端技术的快速发展和对用户体验日益增长的需求,前后端分离模式势必将会成为主流趋势。无论是从开发模式的角度上来说,还是对团队成长的角度上来说,前后端分离都会带来益处,让我们一同拥抱前后端分离,打造精良的开发团队,迎接日益复杂的web应用开发需求。

+

参考来源

+

版权声明

Copyright by lixuguang
未经授权,严禁转载。如需转载,请联系作者

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前后端分离开发指南

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\350\247\204\350\214\203/index.html" "b/2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\350\247\204\350\214\203/index.html" new file mode 100644 index 0000000000..6e03383a81 --- /dev/null +++ "b/2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\350\247\204\350\214\203/index.html" @@ -0,0 +1,660 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前后端分离开发规范 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前后端分离开发规范 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前后端分离开发规范

by lixg

+

(本文所说的前后端工作皆站在工作内容本身,不与职称相关,下面皆如此。)

+

一、为什么要前后端分离

    +
  1. 前后端专注于各自擅长的领域
  2. +
  3. 前端配置后端代码运行环境,节省搭建环境的时间
  4. +
  5. 明确前后端工作职责
  6. +
  7. 提高开发效率
  8. +
  9. 分离有助于前端、后端分别优化
  10. +
+

二、前后端分离存在的问题

    +
  1. 前后端分离必须接口先行,无接口导致前后端关于接口的工作重复出现。
  2. +
  3. 接口对接方式不一致,各项目团队没有采用统一的接口对接方式,导致每次都要熟悉一种新的对接方式。
  4. +
  5. 数据传输格式不一致,导致每次都要重新熟悉,另外由于接口格式不一致还会引起其他未知问题,影响系统健壮性。
  6. +
  7. 人员比例问题,目前公司前后端比例不完全符合前后端分离开发的要求。
  8. +
+

为解决上述问题,提高前后端分离开发效率,特制定如下规范。

+

三、如何做分离

    +
  1. 职责分离
      +
    • 前后端仅仅通过异步接口(AJAX/JSONP)来编程
    • +
    • 前后端都各自有自己的开发流程,构建工具,测试集合
    • +
    • 关注点分离,前后端变得相对独立并松耦合
      前后端职责
    • +
    +
  2. +
+ + + + + + + + + + + + + + + + + + + + + + + +
后端前端
提供数据接收数据,展示数据
处理业务逻辑处理渲染逻辑
Server-side MVC架构Client-sideMV*架构
代码运行在服务器上代码运行在浏览器上
+
    +
  1. 开发流程
      +
    • 前后端技术负责人约定好接口格式
    • +
    • 后端编写和维护接口文档,在 API 变化时更新接口文档 + MOCK平台
    • +
    • 后端根据接口文档进行接口开发
    • +
    • 前端根据接口文档 + MOCK平台进行开发
    • +
    • 开发完成后联调和提交测试
    • +
    +
  2. +
+

MOCK平台统一采用公司搭建的YAPI平台

+

YAPI平台可以对接SWAGGER工具进行自动构建MOCK服务
前后端开发流程
3. 规范原则
- 接口返回数据即显示:前端仅做渲染逻辑处理
- 渲染逻辑禁止跨多个接口调用
- 前端关注交互、渲染逻辑,尽量避免业务逻辑处理的出现
- 请求响应传输数据格式:JSON,JSON数据尽量简单轻量,避免多级JSON的出现

+

四、基本格式

接口定义参见《RESTFul API的设计规范》

请求格式

GET请求、POST请求==必须包含key为body的入参,所有请求数据包装为JSON格式,并存放到入参body中==,示例如下:

+

GET请求:

+
1
xxx/login?body={"username":"admin","password":"123456","captcha":"scfd","rememberMe":1}
+ +

响应格式

对于通用业务数据响应参照基本数据格式要求

+

响应基本数据格式

1
2
3
4
{
"code": 200,
"msg": "success"
}
+
响应实体格式
1
2
3
4
5
6
7
8
9
10
11
{
"code": 200,
"msg": "success",
"data": {
"entity": {
"id": 1,
"name": "XXX",
"phone": "XXX"
}
}
}
+

entity: 响应返回的实体数据

+
响应列表格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"code": 200,
"msg": "success",
"data": {
"list":[
{
"id": 1,
"name": "XXX",
"code": "XXX"
},
{
"id": 2,
"name": "XXX",
"code": "XXX"
},
{
"id": 3,
"name": "XXX",
"code": "XXX"
}
]
}
}
+

list: 响应返回的列表数据

+
响应分页格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"code": 200,
"msg":"success",
"data": {
"totalCount": 2, // 总记录数
"totalPage": 1 // 总页数
"pageNo": 1, // 当前页码
"pageSize": 10, // 每页大小
"list":[
{
"id": 1,
"name": "XXX",
"code": "XXX"
},
{
"id": 2,
"name": "XXX",
"code": "XXX"
},
{
"id": 3,
"name": "XXX",
"code": "XXX"
}
],
}
}
+

响应特殊数据格式

对于特定组件数据格式由后端统一处理后返回前端,如(echart、ztree等组件)

+

特殊内容规范

布尔类型

关于布尔类型,一律返回BOOLEN类型值

+
日期格式

关于日期类型,JSON数据传输中一律使用字符串格式时间戳,具体日期格式因业务而定

+

五、相关文章导读

前后端分离开发指南-理论篇
前后端分离开发指南-实践篇

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前后端分离开发规范

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB%E5%BC%80%E5%8F%91%E8%A7%84%E8%8C%83/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/06/01/\345\211\215\347\253\257\345\274\200\345\217\221\346\217\222\344\273\266/index.html" "b/2022/06/01/\345\211\215\347\253\257\345\274\200\345\217\221\346\217\222\344\273\266/index.html" new file mode 100644 index 0000000000..889018ad35 --- /dev/null +++ "b/2022/06/01/\345\211\215\347\253\257\345\274\200\345\217\221\346\217\222\344\273\266/index.html" @@ -0,0 +1,588 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端开发插件 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端开发插件 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

插件安装

Prettier - Code formatter

格式化工具

+

ESLint

校验规则

+

Vetur

vue代码片段及代码美化

+

Vue 2 Snippets

vue2 代码片段

+

vscode-fileheader

文件注释

+

配置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// setting.json vscode配置文件
{
"workbench.startupEditor": "newUntitledFile",
"terminal.integrated.shell.windows": "C:\\Program Files\\Git\\bin\\bash.exe", \\ 自行配置自己的环境地址
"javascript.updateImportsOnFileMove.enabled": "always",
"editor.tabSize": 2,
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true
},
"sync.gist": "748b4cae5eb6e56d6997978ead096e8f",
"breadcrumbs.enabled": true,
"todohighlight.isEnable": false,
"liveServer.settings.donotShowInfoMsg": true,
"search.location": "sidebar",
"workbench.activityBar.visible": true,
"window.menuBarVisibility": "default",
"workbench.statusBar.visible": true,
"editor.snippetSuggestions": "top",
"editor.formatOnPaste": true,
"workbench.colorTheme": "Tiny Light",
"eslint.validate": [
"javascript",
"javascriptreact",
{
"language": "html",
"autoFix": true
},
{
"language": "vue",
"autoFix": true
}
],
"prettier.eslintIntegration": true,
"files.autoSave": "onWindowChange",
"code-runner.saveAllFilesBeforeRun": true,
"vetur.format.defaultFormatter.html": "js-beautify-html",
"vetur.format.defaultFormatter.js": "vscode-typescript",
"prettier.jsxSingleQuote": true,
"prettier.requireConfig": false,
"prettier.arrowParens": "always",
"typescript.format.insertSpaceAfterSemicolonInForStatements": false,
"prettier.stylelintIntegration": true,
"prettier.singleQuote": true,
"prettier.tslintIntegration": true,
"eslint.provideLintTask": true,
"eslint.autoFixOnSave": true,
"editor.mouseWheelZoom": true,
"editor.tabCompletion": "on",
"editor.formatOnType": true,
"eslint.alwaysShowStatus": true,
"eslint.options": {
"configFile": "E:/project/xxjs/fore-core/.eslintrc.js" // 自行配置自己的项目地址
},
"fileheader.Author": "Li.Xg", // 自行配置自己的名称
"fileheader.LastModifiedBy": "Li.Xg" // 自行配置自己的名称
}
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端开发插件

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91%E6%8F%92%E4%BB%B6/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/06/02/DockerFile/index.html b/2022/06/02/DockerFile/index.html new file mode 100644 index 0000000000..27bf39b438 --- /dev/null +++ b/2022/06/02/DockerFile/index.html @@ -0,0 +1,700 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DockerFile | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ DockerFile +

+ + +
+ + + + +
+ + +

DockerFile

原则与建议

    +
  • 容器轻量化。从镜像中产生的容器应该尽量轻量化,能在足够短的时间内停止、销毁、重新生成并替换原来的容器。
  • +
  • 使用 .gitignore。在大部分情况下,Dockerfile 会和构建所需的文件放在同一个目录中,为了提高构建的性能,应该使用 .gitignore 来过滤掉不需要的文件和目录。
  • +
  • 为了减少镜像的大小,减少依赖,仅安装需要的软件包。
  • +
  • 一个容器只做一件事。解耦复杂的应用,分成多个容器,而不是所有东西都放在一个容器内运行。如一个 Python Web 应用,可能需要 Server、DB、Cache、MQ、Log 等几个容器。一个更加极端的说法:One process per container。
  • +
  • 减少镜像的图层。不要多个 Label、ENV 等标签。
  • +
  • 对续行的参数按照字母表排序,特别是使用apt-get install -y安装包的时候。
  • +
  • 使用构建缓存。如果不想使用缓存,可以在构建的时候使用参数–no-cache=true来强制重新生成中间镜像。
  • +
+

常用命令

FROM

FROM 指令用于指定其后构建新镜像所使用的基础镜像

+
1
2
3
FROM <image>
FROM <image>:<tag>
FROM <image>:<digest>
+

FROM 必须 是 Dockerfile 中第一条非注释命令
在一个 Dockerfile 文件中创建多个镜像时,FROM 可以多次出现。只需在每个新命令 FROM 之前,记录提交上次的镜像 ID。
tag 或 digest 是可选的,如果不使用这两个值时,会使用 latest 版本的基础镜像

+

RUN

在镜像的构建过程中执行特定的命令,并生成一个中间镜像

+
1
2
3
4
#shell格式
RUN <command>
#exec格式
RUN ["executable", "param1", "param2"]
+
    +
  • RUN 命令将在当前 image 中执行任意合法命令并提交执行结果。命令执行提交后,就会自动执行 Dockerfile 中的下一个指令。
  • +
  • 层级 RUN 指令和生成提交是符合 Docker 核心理念的做法。它允许像版本控制那样,在任意一个点,对 image 镜像进行定制化构建。
  • +
  • RUN 指令创建的中间镜像会被缓存,并会在下次构建中使用。如果不想使用这些缓存镜像,可以在构建时指定 ######no###cache 参数,如:docker build ######no###cache。
  • +
+

FROM scratch

scratch 是一个空的虚拟的镜像源文件,因为需要的依赖都存在不需要基础镜像,从而减小体积。

+

COPY

复制文件

+
1
2
COPY <源路径>... <目标路径>
COPY ["<源路径1>",... "<目标路径>"]
+

和 RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的<目标路径>位置。比如:

+
1
COPY package.json /usr/src/app/
+ +

<源路径>可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,如:

+
1
2
COPY hom* /mydir/
COPY hom?.txt /mydir/
+ +

<目标路径>可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

+

此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。

+

ADD

更高级的复制文件
ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。比如<源路径>可以是一个 URL,这种情况下,Docker 引擎会试图去下载这个链接的文件放到<目标路径>去。

+

在构建镜像时,复制上下文中的文件到镜像内,格式:

+
1
2
ADD <源路径>... <目标路径>
ADD ["<源路径>",... "<目标路径>"]
+

注意
如果 docker 发现文件内容被改变,则接下来的指令都不会再使用缓存。关于复制文件时需要处理的/,基本跟正常的 copy 一致

+

ENV

设置环境变量
格式有两种:

+
1
2
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
+

这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。

+
1
2
ENV VERSION=1.0 DEBUG=on \
NAME="Happy Feet"
+

这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。

+

EXPOSE

为构建的镜像设置监听端口,使容器在运行时监听。
格式:

+
1
EXPOSE <port> [<port>...]
+

EXPOSE 指令并不会让容器监听 host 的端口,如果需要,需要在 docker run 时使用 -p、-P 参数来发布容器端口到 host 的某个端口上。

+

VOLUME

定义匿名卷
VOLUME用于创建挂载点,即向基于所构建镜像创始的容器添加卷:

+
1
VOLUME ["/data"]
+

一个卷可以存在于一个或多个容器的指定目录,该目录可以绕过联合文件系统,并具有以下功能:

+
    +
  • 卷可以容器间共享和重用
  • +
  • 容器并不一定要和其它容器共享卷
  • +
  • 修改卷后会立即生效
  • +
  • 对卷的修改不会对镜像产生影响
  • +
  • 卷会一直存在,直到没有任何容器在使用它
    VOLUME 让我们可以将源代码、数据或其它内容添加到镜像中,而又不并提交到镜像中,并使我们可以多个容器间共享这些内容。
  • +
+

WORKDIR

WORKDIR用于在容器内设置一个工作目录:

+
1
WORKDIR /path/to/workdir
+

通过WORKDIR设置工作目录后,Dockerfile 中其后的命令 RUN、CMD、ENTRYPOINT、ADD、COPY 等命令都会在该目录下执行。
如,使用WORKDIR设置工作目录:

+
1
2
3
4
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
+

在以上示例中,pwd 最终将会在 /a/b/c 目录中执行。在使用 docker run 运行容器时,可以通过-w参数覆盖构建时所设置的工作目录。

+

USER

指定当前用户
USER 用于指定运行镜像所使用的用户:

+
1
USER daemon
+

使用USER指定用户时,可以使用用户名、UID 或 GID,或是两者的组合。以下都是合法的指定试:

+
1
2
3
4
5
6
USER user
USER user:group
USER uid
USER uid:gid
USER user:gid
USER uid:group
+

使用USER指定用户后,Dockerfile 中其后的命令 RUN、CMD、ENTRYPOINT 都将使用该用户。镜像构建完成后,通过 docker run 运行容器时,可以通过 -u 参数来覆盖所指定的用户。

+

CMD

CMD用于指定在容器启动时所要执行的命令。CMD 有以下三种格式:

+
1
2
3
CMD ["executable","param1","param2"]
CMD ["param1","param2"]
CMD command param1 param2
+

省略可执行文件的 exec 格式,这种写法使 CMD 中的参数当做 ENTRYPOINT 的默认参数,此时 ENTRYPOINT 也应该是 exec 格式,具体与 ENTRYPOINT 的组合使用,参考 ENTRYPOINT。

+

注意
与 RUN 指令的区别:RUN 在构建的时候执行,并生成一个新的镜像,CMD 在容器运行的时候执行,在构建时不进行任何操作。

+

ENTRYPOINT

ENTRYPOINT 用于给容器配置一个可执行程序。也就是说,每次使用镜像创建容器时,通过 ENTRYPOINT 指定的程序都会被设置为默认程序。ENTRYPOINT 有以下两种形式:

+
1
2
3
ENTRYPOINT ["executable", "param1", "param2"]
ENTRYPOINT command param1 param2
ENTRYPOINT 与 CMD 非常类似,不同的是通过docker run执行的命令不会覆盖 ENTRYPOINT,而docker run命令中指定的任何参数,都会被当做参数再次传递给 ENTRYPOINT。Dockerfile 中只允许有一个 ENTRYPOINT 命令,多指定时会覆盖前面的设置,而只执行最后的 ENTRYPOINT 指令。
+

docker run运行容器时指定的参数都会被传递给 ENTRYPOINT ,且会覆盖 CMD 命令指定的参数。如,执行docker run -d时,-d 参数将被传递给入口点。

+

也可以通过docker run –entrypoint重写 ENTRYPOINT 入口点。如:可以像下面这样指定一个容器执行程序:

+
1
ENTRYPOINT ["/usr/bin/nginx"]
+

完整构建代码:

+
1
2
3
4
5
6
7
8
9
# Version: 0.0.3
FROM ubuntu:16.04
MAINTAINER 何民三 "cn.liuht@gmail.com"
RUN apt-get update
RUN apt-get install -y nginx
RUN echo 'Hello World, 我是个容器' \
> /var/www/html/index.html
ENTRYPOINT ["/usr/sbin/nginx"]
EXPOSE 80
+

使用docker build构建镜像,并将镜像指定为 itbilu/test:

+
1
docker build -t="itbilu/test" .
+

构建完成后,使用itbilu/test启动一个容器:

+
1
docker run -i -t  itbilu/test -g "daemon off;"
+

在运行容器时,我们使用了 -g “daemon off;”,这个参数将会被传递给 ENTRYPOINT,最终在容器中执行的命令为 /usr/sbin/nginx -g “daemon off;”。

+

LABEL

LABEL用于为镜像添加元数据,元数以键值对的形式指定:

+
1
LABEL <key>=<value> <key>=<value> <key>=<value> ...
+

使用LABEL指定元数据时,一条LABEL指定可以指定一或多条元数据,指定多条元数据时不同元数据之间通过空格分隔。推荐将所有的元数据通过一条LABEL指令指定,以免生成过多的中间镜像。
如,通过LABEL指定一些元数据:

+
1
LABEL version="1.0" description="这是一个Web服务器" by="IT笔录"
+

指定后可以通过docker inspect查看:

+
1
2
3
4
5
6
docker inspect itbilu/test
"Labels": {
"version": "1.0",
"description": "这是一个Web服务器",
"by": "IT笔录"
},
+ +

ARG

ARG用于指定传递给构建运行时的变量:

+
1
ARG <name>[=<default value>]
+

如,通过ARG指定两个变量:

+
1
2
ARG site
ARG build_user=IT笔录
+

以上我们指定了 site 和 build_user 两个变量,其中 build_user 指定了默认值。在使用 docker build 构建镜像时,可以通过 –build-arg = 参数来指定或重设置这些变量的值。

+
1
docker build --build-arg site=itiblu.com -t itbilu/test .
+

这样我们构建了 itbilu/test 镜像,其中site会被设置为 itbilu.com,由于没有指定 build_user,其值将是默认值 IT 笔录。

+

ONBUILD

ONBUILD用于设置镜像触发器:

+
1
ONBUILD [INSTRUCTION]
+

当所构建的镜像被用做其它镜像的基础镜像,该镜像中的触发器将会被钥触发。
如,当镜像被使用时,可能需要做一些处理:

+
1
2
3
4
[...]
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
[...]
+ +

STOPSIGNAL

STOPSIGNAL用于设置停止容器所要发送的系统调用信号:

+
1
STOPSIGNAL signal
+

所使用的信号必须是内核系统调用表中的合法的值,如:SIGKILL。

+

SHELL

SHELL用于设置执行命令(shell式)所使用的的默认 shell 类型:

+
1
SHELL ["executable", "parameters"]
+

SHELL在Windows环境下比较有用,Windows 下通常会有 cmd 和 powershell 两种 shell,可能还会有 sh。这时就可以通过 SHELL 来指定所使用的 shell 类型:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM microsoft/windowsservercore

# Executed as cmd /S /C echo default
RUN echo default

# Executed as cmd /S /C powershell -command Write-Host default
RUN powershell -command Write-Host default

# Executed as powershell -command Write-Host hello
SHELL ["powershell", "-command"]
RUN Write-Host hello

# Executed as cmd /S /C echo hello
SHELL ["cmd", "/S"", "/C"]
RUN echo hello
+ +

Dockerfile 示例

    +
  • 构建Nginx运行环境
  • +
  • 构建tomcat 环境
  • +
+

构建Nginx运行环境

Dockerfile文件

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#指定基础镜像
FROM sameersbn/ubuntu:14.04.20161014

#维护者信息
MAINTAINER sameer@damagehead.com

#设置环境
ENV RTMP_VERSION=1.1.10 \
NPS_VERSION=1.11.33.4 \
LIBAV_VERSION=11.8 \
NGINX_VERSION=1.10.1 \
NGINX_USER=www-data \
NGINX_SITECONF_DIR=/etc/nginx/sites-enabled \
NGINX_LOG_DIR=/var/log/nginx \
NGINX_TEMP_DIR=/var/lib/nginx \
NGINX_SETUP_DIR=/var/cache/nginx

#设置构建时变量,镜像建立完成后就失效
ARG BUILD_LIBAV=false
ARG WITH_DEBUG=false
ARG WITH_PAGESPEED=true
ARG WITH_RTMP=true

#复制本地文件到容器目录中
COPY setup/ ${NGINX_SETUP_DIR}/
RUN bash ${NGINX_SETUP_DIR}/install.sh

#复制本地配置文件到容器目录中
COPY nginx.conf /etc/nginx/nginx.conf
COPY entrypoint.sh /sbin/entrypoint.sh

#运行指令
RUN chmod 755 /sbin/entrypoint.sh

#允许指定的端口
EXPOSE 80/tcp 443/tcp 1935/tcp

#指定网站目录挂载点
VOLUME ["${NGINX_SITECONF_DIR}"]

ENTRYPOINT ["/sbin/entrypoint.sh"]
CMD ["/usr/sbin/nginx"]
+ +

构建Tomcat环境

Dockerfile文件

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 指定基于的基础镜像
FROM ubuntu:13.10

# 维护者信息
MAINTAINER zhangjiayang "zhangjiayang@sczq.com.cn"

# 镜像的指令操作
# 获取APT更新的资源列表
RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe"> /etc/apt/sources.list
# 更新软件
RUN apt-get update

# Install curl
RUN apt-get -y install curl

# Install JDK 7
RUN cd /tmp && curl -L 'http://download.oracle.com/otn-pub/java/jdk/7u65-b17/jdk-7u65-linux-x64.tar.gz' -H 'Cookie: oraclelicense=accept-securebackup-cookie; gpw_e24=Dockerfile' | tar -xz
RUN mkdir -p /usr/lib/jvm
RUN mv /tmp/jdk1.7.0_65/ /usr/lib/jvm/java-7-oracle/

# Set Oracle JDK 7 as default Java
RUN update-alternatives --install /usr/bin/java java /usr/lib/jvm/java-7-oracle/bin/java 300
RUN update-alternatives --install /usr/bin/javac javac /usr/lib/jvm/java-7-oracle/bin/javac 300

# 设置系统环境
ENV JAVA_HOME /usr/lib/jvm/java-7-oracle/

# Install tomcat7
RUN cd /tmp && curl -L 'http://archive.apache.org/dist/tomcat/tomcat-7/v7.0.8/bin/apache-tomcat-7.0.8.tar.gz' | tar -xz
RUN mv /tmp/apache-tomcat-7.0.8/ /opt/tomcat7/

ENV CATALINA_HOME /opt/tomcat7
ENV PATH $PATH:$CATALINA_HOME/bin

# 复件tomcat7.sh到容器中的目录
ADD tomcat7.sh /etc/init.d/tomcat7
RUN chmod 755 /etc/init.d/tomcat7

# Expose ports. 指定暴露的端口
EXPOSE 8080

# Define default command.
ENTRYPOINT service tomcat7 start && tail -f /opt/tomcat7/logs/catalina.out
+ +

tomcat7.sh命令文件

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export JAVA_HOME=/usr/lib/jvm/java-7-oracle/  
export TOMCAT_HOME=/opt/tomcat7

case $1 in
start)
sh $TOMCAT_HOME/bin/startup.sh
;;
stop)
sh $TOMCAT_HOME/bin/shutdown.sh
;;
restart)
sh $TOMCAT_HOME/bin/shutdown.sh
sh $TOMCAT_HOME/bin/startup.sh
;;
esac
exit 0
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:DockerFile

+

文章作者:

+

发布时间:2022年06月02日 - 11:20

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/02/DockerFile/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/06/02/Docker\345\221\275\344\273\244/index.html" "b/2022/06/02/Docker\345\221\275\344\273\244/index.html" new file mode 100644 index 0000000000..06e6303ef7 --- /dev/null +++ "b/2022/06/02/Docker\345\221\275\344\273\244/index.html" @@ -0,0 +1,632 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Docker 命令 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Docker 命令 +

+ + +
+ + + + +
+ + +

Docker 命令

    +
  • 帮助命令
    docker –help
  • +
+

镜像操作

    +
  • 搜索镜像
    docker search hello-world

    +
  • +
  • 下载镜像
    docker pull hello-world

    +
  • +
  • 查看本地已下载所有镜像
    docker images

    +
  • +
  • 查看镜像历史
    docker history hello-world

    +
  • +
  • 备份镜像
    docker tag hello-word:last hello-world:v2

    +
  • +
  • 删除镜像
    docker rmi hello-word:last

    +
  • +
  • 删除未使用过的镜像
    docker image prune

    +
  • +
  • 导出镜像
    docker save -o hello-world:last.tar hello-world:last

    +
  • +
  • 导入镜像
    docker load -i hello-world:last.tar

    +
  • +
  • 查看镜像信息
    docker image inspact nginx

    +
  • +
+

容器操作

    +
  • 查看所有容器 [-q 编号] [-a active 启动的容器]
    docker ps [-a] [-q]

    +
  • +
  • 启动容器 [-d 后台启动]
    docker run -d –name nginx1 nginx:last

    +
  • +
  • 停止容器
    docker stop nginx1

    +
  • +
  • 启动容器()
    docker start nginx1

    +
  • +
  • 删除容器
    docker rm nginx1

    +
  • +
  • 批量删除运行中容器
    docker rm $(docker ps -q) -f

    +
  • +
  • 创建容器并进入
    docker run -it –name nginx1 nginx:last /bin/bash

    +
  • +
  • 退出容器
    exit

    +
  • +
  • 进入容器
    docker exec -it nginx1 /bin/bash

    +
  • +
  • 通过容器创建镜像
    docker commit -m ‘laowang’ nginx1 nginx:v1

    +
  • +
  • 查看容器信息
    docker container inspact nginx1

    +
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Docker 命令

+

文章作者:

+

发布时间:2022年06月02日 - 16:20

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/02/Docker%E5%91%BD%E4%BB%A4/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/06/02/Docker\345\256\271\345\231\250\347\253\257\345\217\243\346\230\240\345\260\204/index.html" "b/2022/06/02/Docker\345\256\271\345\231\250\347\253\257\345\217\243\346\230\240\345\260\204/index.html" new file mode 100644 index 0000000000..96eb0a4471 --- /dev/null +++ "b/2022/06/02/Docker\345\256\271\345\231\250\347\253\257\345\217\243\346\230\240\345\260\204/index.html" @@ -0,0 +1,585 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Docker容器端口映射 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Docker容器端口映射 +

+ + +
+ + + + +
+ + +

Docker容器端口映射

常见容器服务需要做端口映射,这里以nginx为例进行举例

+

启动一个nginx容器

docker run -itd –name nginx1 -P nginx:latest #随机端口
docker run -itd –name nginx2 -p 6001:80 nginx:latest #指定端口
docker run -itd –name nginx3 -p 172.16.10.1:6002:80 nginx:latest #指定IP和端口

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Docker容器端口映射

+

文章作者:

+

发布时间:2022年06月02日 - 11:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/02/Docker%E5%AE%B9%E5%99%A8%E7%AB%AF%E5%8F%A3%E6%98%A0%E5%B0%84/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/06/02/Docker\346\225\260\346\215\256\345\215\267/index.html" "b/2022/06/02/Docker\346\225\260\346\215\256\345\215\267/index.html" new file mode 100644 index 0000000000..aca347b76d --- /dev/null +++ "b/2022/06/02/Docker\346\225\260\346\215\256\345\215\267/index.html" @@ -0,0 +1,595 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Docker数据卷 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Docker数据卷 +

+ + +
+ + + + +
+ + +

Docker数据卷

宿主机与容器进行数据交互,共享宿主机与容器之间的数据

+

创建数据卷关联

docker run -it –name my_ubuntu1 -v ~ /Desktop/test:/test /bin/bash

+

利用系统方法操作数据卷

    +
  • 查 docker数据卷
    docker volume ls

    +
  • +
  • 创建数据卷
    docker volume create volname

    +
  • +
  • 共享
    docker run -it –name my_ubuntu2 -v volname:/home ubuntu:latest /bin/bash

    +
  • +
+

数据卷容器使用

可以通过数据卷容器,创建新的容器,并将多个容器绑定在一起

+

docker run -itd –name my_ubuntu3 –volumes-from my_ubuntu2 ubuntu:latest

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Docker数据卷

+

文章作者:

+

发布时间:2022年06月02日 - 11:21

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/02/Docker%E6%95%B0%E6%8D%AE%E5%8D%B7/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/06/02/Docker\347\275\221\347\273\234\346\250\241\345\274\217/index.html" "b/2022/06/02/Docker\347\275\221\347\273\234\346\250\241\345\274\217/index.html" new file mode 100644 index 0000000000..590da7743f --- /dev/null +++ "b/2022/06/02/Docker\347\275\221\347\273\234\346\250\241\345\274\217/index.html" @@ -0,0 +1,595 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Docker网络模式 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Docker网络模式 +

+ + +
+ + + + +
+ + +

Docker网络模式

Docker常见的两种网络模式

+
    +
  • host网络模式:创建的容器和宿主机共享同一个网卡
  • +
  • bridge网络模式:桥接模式,只要使用桥接模式创建容器网段一样(类似于172.16.1.1,172.16.1.2)有点像是子局域网
  • +
+

利用network命令管理网络模式

    +
  • 查看网络模式
    docker network ls
  • +
  • 创建网络模式
    docker network create –drive bridge bridge_test
  • +
  • 通过network断网
    docker network disconnet bridge nginx5
  • +
  • 通过network联网
    docker network connect bridge nginx5
  • +
  • 删除网络模式
    docker network rm bridge_test
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Docker网络模式

+

文章作者:

+

发布时间:2022年06月02日 - 11:23

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/02/Docker%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%BC%8F/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/06/09/Hexo\345\234\250Github\345\217\221\345\270\203\344\271\213\345\220\216\350\207\252\345\256\232\344\271\211\345\237\237\345\220\215\347\232\204\351\205\215\347\275\256/index.html" "b/2022/06/09/Hexo\345\234\250Github\345\217\221\345\270\203\344\271\213\345\220\216\350\207\252\345\256\232\344\271\211\345\237\237\345\220\215\347\232\204\351\205\215\347\275\256/index.html" new file mode 100644 index 0000000000..1afc283887 --- /dev/null +++ "b/2022/06/09/Hexo\345\234\250Github\345\217\221\345\270\203\344\271\213\345\220\216\350\207\252\345\256\232\344\271\211\345\237\237\345\220\215\347\232\204\351\205\215\347\275\256/index.html" @@ -0,0 +1,593 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hexo在Github发布之后自定义域名的配置 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Hexo在Github发布之后自定义域名的配置 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

HexoGithub发布之后自定义域名的配置

进到你的博客发布所在仓库,如:

选择Settings,然后是pages,滚动页面找到Custom domain,将你自己的域名绑定进去保存一下就好了。

+

在你自己的域名商处用CNAME把你自己的域名指定到你自己的仓库page地址上比如我的就是lixuguang.github.io Public,这样你的GitHub Page就可以通过自己的域名进行访问了。

+

HexoGithub发布之后自定义域名会被清空的问题

使用 GitHub Page 功能将博客托管在了 GitHub 上,并配置 CNAME 将自己的域名解析了过去,但是发现一个问题,每次 hexo deploy 之后,custom domain 会被重置失效。

+

解决方案

Hexo 生成的博客的 source 目录下(注意这个是你的博客源码所在仓库,并不是发布的仓库)新建一个 CNAME 文件,然后在这个文件中填入你的域名,这样就不会每次发布之后,GitHub Page 里的 custom domain 都被重置掉啦。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Hexo在Github发布之后自定义域名的配置

+

文章作者:

+

发布时间:2022年06月09日 - 12:40

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/09/Hexo%E5%9C%A8Github%E5%8F%91%E5%B8%83%E4%B9%8B%E5%90%8E%E8%87%AA%E5%AE%9A%E4%B9%89%E5%9F%9F%E5%90%8D%E7%9A%84%E9%85%8D%E7%BD%AE/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/06/09/Nginx\346\214\207\344\273\244/index.html" "b/2022/06/09/Nginx\346\214\207\344\273\244/index.html" new file mode 100644 index 0000000000..a872dcd396 --- /dev/null +++ "b/2022/06/09/Nginx\346\214\207\344\273\244/index.html" @@ -0,0 +1,591 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Nginx指令 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Nginx指令 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Nginx指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#启动Nginx
start nginx

#重启Nginx
nginx -s reopen

#重新加载Nginx配置文件,然后以优雅的方式重启Nginx
nginx -s reload

#强制停止Nginx服务
nginx -s stop

#优雅地停止Nginx服务(即处理完所有请求后再停止服务)
nginx -s quit

#检测配置文件是否有语法错误,然后退出
nginx -t

#显示版本信息并退出
nginx -v

#显示版本和配置选项信息,然后退出
nginx -V

#检测配置文件是否有语法错误,然后退出
nginx -t

#检测配置文件是否有语法错误,转储并退出
nginx -T

#在检测配置文件期间屏蔽非错误信息
nginx -q

#打开帮助信息
nginx -?,-h

#设置前缀路径(默认是:/usr/share/nginx/)
nginx -p prefix

#设置配置文件(默认是:/etc/nginx/nginx.conf)
nginx -c filename

#设置配置文件外的全局指令
nginx -g directives
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Nginx指令

+

文章作者:

+

发布时间:2022年06月09日 - 10:40

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/09/Nginx%E6%8C%87%E4%BB%A4/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/06/09/http_stub_status_module/index.html b/2022/06/09/http_stub_status_module/index.html new file mode 100644 index 0000000000..628b562c0a --- /dev/null +++ b/2022/06/09/http_stub_status_module/index.html @@ -0,0 +1,610 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + http_stub_status_module Nginx的性能模块 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ http_stub_status_module Nginx的性能模块 +

+ + +
+ + + + +
+ + +

ngx_http_stub_status_module 是一个 Nginx 的内置 模块,可以提供 Nginx 的简单统计信息。默认情况下这个模块是不被编译进来的,所以在编译 Nginx 时要指定加载该模块:

+
1
--with-http_stub_status_module
+

查看是否安装

1
2
3
4
5
// 查看nginx版本及安装了那些模块
nginx -V

// 如果安装了的话会显示下面的信息
--with-http_stub_status_module
+ +

配置使用

nginx.conf配置文件中进行如下配置

+
1
2
3
location /ngx_status {
stub_status on; // 设置开启性能模块
}
+

通过浏览器访问 http://localhost/ngx_status 就能看到你的nginx服务性能监控画面了
image

+

解析

    +
  • 第一行:Active connections:活动连接数
  • +
  • 第二三行:Server accepts handled requests:(三个数字分别代表)总共处理的连接数,成功握手的连接数量,处理的请求数(正常情况下握手和连接数是相等的,表示没有丢失)
  • +
  • 第四行:
      +
    • Reading: Nginx 读取到客户端的Header信息数
    • +
    • Writing: Nginx 返回给客户端的Header信息数
    • +
    • Waiting: 开启keep-alive的情况下,这个值等于 active – (reading + writing),意思就是Nginx已经处理完成,正在等候下一次请求指令的驻留连接(在nginx开启了keep-alive,也就是长连接的情况下,客户端跟服务端建立了连接但是没有读写操作的空闲状态)
    • +
    +
  • +
+

使用场景

    +
  • 可以简单的用脚本做监控
  • +
  • 可以用开源工具,zabbix,prometheus等去采集nginx的统计信息,做监控和历史数据采集
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:http_stub_status_module Nginx的性能模块

+

文章作者:

+

发布时间:2022年06月09日 - 12:40

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/09/http_stub_status_module/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/06/15/\346\236\266\346\236\204\345\256\236\350\267\265/index.html" "b/2022/06/15/\346\236\266\346\236\204\345\256\236\350\267\265/index.html" new file mode 100644 index 0000000000..51ffd0470e --- /dev/null +++ "b/2022/06/15/\346\236\266\346\236\204\345\256\236\350\267\265/index.html" @@ -0,0 +1,618 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 架构实践 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 架构实践 +

+ + +
+ + + + +
+ + +

架构实践

架构设计内容一览

应用架构设计

    +
  • Transaction执行控制(在线/批处理路径和控制)
  • +
  • Session控制
  • +
  • 死锁控制
  • +
  • 接口处理流程(适配器设置/文件备份)
  • +
  • 命名规范(实例/SID等)
  • +
  • 中间件参数 (初始化参数等)
  • +
  • 认证(处理流程/错误处理)
  • +
  • 帐户(用户ID体系/权限管理/申请方式)
  • +
+

开发架构设计

    +
  • 系统景观(各种环境名称、利用方法等)
  • +
  • 转运路线(转运路线/转运工具/承认方式)
  • +
  • 开发账户(用户ID体系/权限管理/申请方式)
  • +
  • 开发终端设置(在线批处理开发工具/目录等)
  • +
  • 开发资源管理
  • +
  • 表单、协作工具的利用方法(表单/接口ID定义规则)
  • +
  • 开发和验证备份(处理流程/文件清除/周期)
  • +
  • 数据屏蔽(処理概要/屏蔽利用规则/対象/开发环境搭建方法)
  • +
+

运维架构设计

    +
  • 监测规范(方式/周期/対象)
  • +
  • 审计和错误日志(日志级别/日志保留时效/删除时效)
  • +
  • 生产环境和调研环境的备份(处理流程/删除时效/周期)
  • +
  • 加密处理(安装位置、加密规则、记录水平加密方法)
  • +
  • 云安全设定
  • +
  • 增加功能设计
  • +
+

基础设施

    +
  • 网络设计(虚拟网/子网/URL/负载均衡访问控制/白名单)
  • +
  • 云设计(实例/NFS/IAM)
  • +
  • 存储设计 (BlockVolume/FileStorage/ObjectStorage/数据配置/数据圧縮方法)
  • +
  • 时间DNS同步化(OCI設定)
  • +
  • OS参数设定
  • +
  • Docker参数设定
  • +
  • 高可用切换 (処理方式、设定参数考量)
  • +
  • 打补丁(打补丁/执行规范)
  • +
  • 虚拟桌面设定
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:架构实践

+

文章作者:

+

发布时间:2022年06月15日 - 14:40

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/15/%E6%9E%B6%E6%9E%84%E5%AE%9E%E8%B7%B5/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/06/16/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206 ---- \347\275\221\347\273\234\345\256\211\345\205\250\357\274\2102\357\274\211/index.html" "b/2022/06/16/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206 ---- \347\275\221\347\273\234\345\256\211\345\205\250\357\274\2102\357\274\211/index.html" new file mode 100644 index 0000000000..9ca988670e --- /dev/null +++ "b/2022/06/16/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206 ---- \347\275\221\347\273\234\345\256\211\345\205\250\357\274\2102\357\274\211/index.html" @@ -0,0 +1,728 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 网络安全(2) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 网络安全(2) +

+ + +
+ + + + +
+ + +

前端常见知识点整理 —- 网络安全(2)

SQL 注入

SQL 注入漏洞(SQL Injection)是 Web 开发中最常见的一种安全漏洞。可以用它来从数据库获取敏感信息,或者利用数据库的特性执行添加用户,导出文件等一系列恶意操作,甚至有可能获取数据库乃至系统用户最高权限。

+

而造成 SQL 注入的原因是因为程序没有有效的转义过滤用户的输入,使攻击者成功的向服务器提交恶意的 SQL 查询代码,程序在接收后错误的将攻击者的输入作为查询语句的一部分执行,导致原始的查询逻辑被改变,额外的执行了攻击者精心构造的恶意代码。

+

很多 Web 开发者没有意识到 SQL 查询是可以被篡改的,从而把 SQL 查询当作可信任的命令。殊不知,SQL 查询是可以绕开访问控制,从而绕过身份验证和权限检查的。更有甚者,有可能通过 SQL 查询去运行主机系统级的命令。

+

SQL 注入原理

下面将通过一些真实的例子来详细讲解 SQL 注入的方式的原理。

+

考虑以下简单的管理员登录表单:

+
1
2
3
4
5
<form action="/login" method="POST">
<p>Username: <input type="text" name="username" /></p>
<p>Password: <input type="password" name="password" /></p>
<p><input type="submit" value="登陆" /></p>
</form>
+ +

后端的 SQL 语句可能是如下这样的:

+
1
2
3
4
5
6
7
let querySQL = `
SELECT *
FROM user
WHERE username='${username}'
AND psw='${password}'
`;
// 接下来就是执行 sql 语句...
+ +

目的就是来验证用户名和密码是不是正确,按理说乍一看上面的 SQL 语句也没什么毛病,确实是能够达到我们的目的,可是你只是站在用户会老老实实按照你的设计来输入的角度来看问题,如果有一个恶意攻击者输入的用户名是 zoumiaojiang' OR 1 = 1 --,密码随意输入,就可以直接登入系统了。WFT!

+

冷静下来思考一下,我们之前预想的真实 SQL 语句是:

+
1
SELECT * FROM user WHERE username='zoumiaojiang' AND psw='mypassword'
+ +

可以恶意攻击者的奇怪用户名将你的 SQL 语句变成了如下形式:

+
1
2
SELECT * FROM user WHERE username='zoumiaojiang' OR 1 = 1 --' AND psw='xxxx'

+ +

在 SQL 中,-- 是注释后面的内容的意思,所以查询语句就变成了:

+
1
2
SELECT * FROM user WHERE username='zoumiaojiang' OR 1 = 1

+ +

这条 SQL 语句的查询条件永远为真,所以意思就是恶意攻击者不用我的密码,就可以登录进我的账号,然后可以在里面为所欲为,然而这还只是最简单的注入,牛逼的 SQL 注入高手甚至可以通过 SQL 查询去运行主机系统级的命令,将你主机里的内容一览无余,这里我也没有这个能力讲解的太深入,毕竟不是专业研究这类攻击的,但是通过以上的例子,已经了解了 SQL 注入的原理,我们基本已经能找到防御 SQL 注入的方案了。

+

如何预防 SQL 注入

防止 SQL 注入主要是不能允许用户输入的内容影响正常的 SQL 语句的逻辑,当用户的输入的信息将要用来拼接 SQL 语句的话,我们应该永远选择不相信,任何内容都必须进行转义过滤,当然做到这个还是不够的,下面列出防御 SQL 注入的几点注意事项:

+
    +
  • 严格限制Web应用的数据库的操作权限,给此用户提供仅仅能够满足其工作的最低权限,从而最大限度的减少注入攻击对数据库的危害

    +
  • +
  • 后端代码检查输入的数据是否符合预期,严格限制变量的类型,例如使用正则表达式进行一些匹配处理。

    +
  • +
  • 对进入数据库的特殊字符(',",\,<,>,&,*,; 等)进行转义处理,或编码转换。基本上所有的后端语言都有对字符串进行转义处理的方法,比如 lodash 的 lodash._escapehtmlchar 库。

    +
  • +
  • 所有的查询语句建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中,即不要直接拼接 SQL 语句。例如 Node.js 中的 mysqljs 库的 query 方法中的 ? 占位参数。

    +
  • +
+
1
mysql.query(`SELECT * FROM user WHERE username = ? AND psw = ?`, [username, psw]);
+ +
    +
  • 在应用发布之前建议使用专业的 SQL 注入检测工具进行检测,以及时修补被发现的 SQL 注入漏洞。网上有很多这方面的开源工具,例如 sqlmap、SQLninja 等。

    +
  • +
  • 避免网站打印出 SQL 错误信息,比如类型错误、字段不匹配等,把代码里的 SQL 语句暴露出来,以防止攻击者利用这些错误信息进行 SQL 注入。

    +
  • +
  • 不要过于细化返回的错误信息,如果目的是方便调试,就去使用后端日志,不要在接口上过多的暴露出错信息,毕竟真正的用户不关心太多的技术细节,只要话术合理就行。

    +
  • +
+

碰到要操作的数据库的代码,一定要慎重,小心使得万年船,多找几个人多来几次 code review,将问题都暴露出来,而且要善于利用工具,操作数据库相关的代码属于机密,没事不要去各种论坛晒自家站点的 SQL 语句,万一被人盯上了呢?

+

命令行注入

命令行注入漏洞,指的是攻击者能够通过 HTTP 请求直接侵入主机,执行攻击者预设的 shell 命令,听起来好像匪夷所思,这往往是 Web 开发者最容易忽视但是却是最危险的一个漏洞之一,看一个实例:

+

假如现在需要实现一个需求:用户提交一些内容到服务器,然后在服务器执行一些系统命令去产出一个结果返回给用户,接口的部分实现如下:

+
1
2
3
4
5
// 以 Node.js 为例,假如在接口中需要从 github 下载用户指定的 repo
const exec = require('mz/child_process').exec;
let params = {/* 用户输入的参数 */};

exec(`git clone ${params.repo} /some/path`);
+ +

这段代码确实能够满足业务需求,正常的用户也确实能从指定的 git repo 上下载到想要的代码,可是和 SQL 注入一样,这段代码在恶意攻击者眼中,简直就是香饽饽。

+

如果 params.repo 传入的是 https://github.com/zoumiaojiang/zoumiaojiang.github.io.git 当然没问题了。
可是如果 params.repo 传入的是 https://github.com/xx/xx.git && rm -rf /* && 恰好你的服务是用 root 权限起的就惨了。

+

具体恶意攻击者能用命令行注入干什么也像 SQL 注入一样,手法是千变万化的,比如「反弹 shell 注入」等,但原理都是一样的,我们绝对有能力防止命令行注入发生。防止命令行注入需要做到以下几件事情:

+
    +
  • 后端对前端提交内容需要完全选择不相信,并且对其进行规则限制(比如正则表达式)。
  • +
  • 在调用系统命令前对所有传入参数进行命令行参数转义过滤。
  • +
  • 不要直接拼接命令语句,借助一些工具做拼接、转义预处理,例如 Node.js 的 shell-escape npm 包。
  • +
+

还是前面的例子,我们可以做到如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const exec = require('mz/child_process').exec;

// 借助 shell-escape npm 包解决参数转义过滤问题
const shellescape = require('shell-escape');

let params = {/* 用户输入的参数 */};

// 先过滤一下参数,让参数符合预期
if (!/正确的表达式/.test(params.repo)) {
return;
}

let cmd = shellescape([
'git',
'clone',
params.repo,
'/some/path'
]);

// cmd 的值: git clone 'https://github.com/xx/xx.git && rm -rf / &&' /some/path
// 这样就不会被注入成功了。
exec(cmd);
+

无论是在何种后端语言环境中,凡是涉及到代码调用系统 shell 命令的时候都一定要谨慎。

+

DDoS 攻击

DDoS 又叫分布式拒绝服务,全称 Distributed Denial of Service,其原理就是利用大量的请求造成资源过载,导致服务不可用,这个攻击应该不能算是安全问题,这应该算是一个另类的存在,因为这种攻击根本就是耍流氓的存在,「伤敌一千,自损八百」的行为。出于保护 Web App 不受攻击的攻防角度,还是介绍一下 DDoS 攻击吧,毕竟也是挺常见的。

+

DDoS 攻击可以理解为:「你开了一家店,隔壁家点看不惯,就雇了一大堆黑社会人员进你店里干坐着,也不消费,其他客人也进不来,导致你营业惨淡」。为啥说 DDoS 是个「伤敌一千,自损八百」的行为呢?毕竟隔壁店还是花了不少钱雇黑社会但是啥也没得到不是?DDoS 攻击的目的基本上就以下几个:

+
    +
  • 深仇大恨,就是要干死你
  • +
  • 敲诈你,不给钱就干你
  • +
  • 忽悠你,不买我防火墙服务就会有“人”继续干你
  • +
+

也许你的站点遭受过 DDoS 攻击,具体什么原因怎么解读见仁见智。DDos 攻击从层次上可分为网络层攻击与应用层攻击,从攻击手法上可分为快型流量攻击与慢型流量攻击,但其原理都是造成资源过载,导致服务不可用。

+

网络层 DDoS

网络层 DDos 攻击包括 SYN FloodACK FloodUDP FloodICMP Flood 等。

+

SYN Flood 攻击

SYN flood 攻击主要利用了 TCP 三次握手过程中的 Bug,我们都知道 TCP 三次握手过程是要建立连接的双方发送 SYN,SYN + ACK,ACK 数据包,而当攻击方随意构造源 IP 去发送 SYN 包时,服务器返回的 SYN + ACK 就不能得到应答(因为 IP 是随意构造的),此时服务器就会尝试重新发送,并且会有至少 30s 的等待时间,导致资源饱和服务不可用,此攻击属于慢型 DDoS 攻击。

+

ACK Flood 攻击

ACK Flood 攻击是在 TCP 连接建立之后,所有的数据传输 TCP 报文都是带有 ACK 标志位的,主机在接收到一个带有 ACK 标志位的数据包的时候,需要检查该数据包所表示的连接四元组是否存在,如果存在则检查该数据包所表示的状态是否合法,然后再向应用层传递该数据包。如果在检查中发现该数据包不合法,例如该数据包所指向的目的端口在本机并未开放,则主机操作系统协议栈会回应 RST 包告诉对方此端口不存在。

+

UDP Flood 攻击

UDP flood 攻击是由于 UDP 是一种无连接的协议,因此攻击者可以伪造大量的源 IP 地址去发送 UDP 包,此种攻击属于大流量攻击。正常应用情况下,UDP 包双向流量会基本相等,因此发起这种攻击的攻击者在消耗对方资源的时候也在消耗自己的资源。

+

ICMP Flood 攻击

ICMP Flood 攻击属于大流量攻击,其原理就是不断发送不正常的 ICMP 包(所谓不正常就是 ICMP 包内容很大),导致目标带宽被占用,但其本身资源也会被消耗。目前很多服务器都是禁 ping 的(在防火墙在可以屏蔽 ICMP 包),因此这种攻击方式已经落伍。

+

网络层 DDoS 防御

网络层的 DDoS 攻击究其本质其实是无法防御的,我们能做得就是不断优化服务本身部署的网络架构,以及提升网络带宽。当然,还是做好以下几件事也是有助于缓解网络层 DDoS 攻击的冲击:

+
    +
  • 网络架构上做好优化,采用负载均衡分流。
  • +
  • 确保服务器的系统文件是最新的版本,并及时更新系统补丁。
  • +
  • 添加抗 DDos 设备,进行流量清洗。
  • +
  • 限制同时打开的 SYN 半连接数目,缩短 SYN 半连接的 Timeout 时间。
  • +
  • 限制单 IP 请求频率。
  • +
  • 防火墙等防护设置禁止 ICMP 包等。
  • +
  • 严格限制对外开放的服务器的向外访问。
  • +
  • 运行端口映射程序或端口扫描程序,要认真检查特权端口和非特权端口。
  • +
  • 关闭不必要的服务。
  • +
  • 认真检查网络设备和主机/服务器系统的日志。只要日志出现漏洞或是时间变更,那这台机器就可能遭到了攻击。
  • +
  • 限制在防火墙外与网络文件共享。这样会给黑客截取系统文件的机会,主机的信息暴露给黑客,无疑是给了对方入侵的机会。
  • +
  • 加钱堆机器。。
  • +
  • 报警。。-
  • +
+

应用层 DDoS

应用层 DDoS 攻击不是发生在网络层,是发生在 TCP 建立握手成功之后,应用程序处理请求的时候,现在很多常见的 DDoS 攻击都是应用层攻击。应用层攻击千变万化,目的就是在网络应用层耗尽你的带宽,下面列出集中典型的攻击类型。

+

CC 攻击

当时绿盟为了防御 DDoS 攻击研发了一款叫做 Collapasar 的产品,能够有效的防御 SYN Flood 攻击。黑客为了挑衅,研发了一款 Challenge Collapasar 攻击工具(简称 CC)。

+

CC 攻击的原理,就是针对消耗资源比较大的页面不断发起不正常的请求,导致资源耗尽。因此在发送 CC 攻击前,我们需要寻找加载比较慢,消耗资源比较多的网页,比如需要查询数据库的页面、读写硬盘文件的等。通过 CC 攻击,使用爬虫对某些加载需要消耗大量资源的页面发起 HTTP 请求。

+

DNS Flood

DNS Flood 攻击采用的方法是向被攻击的服务器发送大量的域名解析请求,通常请求解析的域名是随机生成或者是网络世界上根本不存在的域名,被攻击的DNS 服务器在接收到域名解析请求的时候首先会在服务器上查找是否有对应的缓存,如果查找不到并且该域名无法直接由服务器解析的时候,DNS 服务器会向其上层 DNS 服务器递归查询域名信息。域名解析的过程给服务器带来了很大的负载,每秒钟域名解析请求超过一定的数量就会造成 DNS 服务器解析域名超时。

+

根据微软的统计数据,一台 DNS 服务器所能承受的动态域名查询的上限是每秒钟 9000 个请求。而我们知道,在一台 P3 的 PC 机上可以轻易地构造出每秒钟几万个域名解析请求,足以使一台硬件配置极高的 DNS 服务器瘫痪,由此可见 DNS 服务器的脆弱性。

+

HTTP 慢速连接攻击

针对 HTTP 协议,先建立起 HTTP 连接,设置一个较大的 Conetnt-Length,每次只发送很少的字节,让服务器一直以为 HTTP 头部没有传输完成,这样连接一多就很快会出现连接耗尽。

+

应用层 DDoS 防御

    +
  • 判断 User-Agent 字段(不可靠,因为可以随意构造)
  • +
  • 针对 IP + cookie,限制访问频率(由于 cookie 可以更改,IP 可以使用代理,或者肉鸡,也不可靠)
  • +
  • 关闭服务器最大连接数等,合理配置中间件,缓解 DDoS 攻击。
  • +
  • 请求中添加验证码,比如请求中有数据库操作的时候。
  • +
  • 编写代码时,尽量实现优化,并合理使用缓存技术,减少数据库的读取操作。
  • +
  • 加钱堆机器。。
  • +
  • 报警。。
  • +
+

应用层的防御有时比网络层的更难,因为导致应用层被 DDoS 攻击的因素非常多,有时往往是因为程序员的失误,导致某个页面加载需要消耗大量资源,有时是因为中间件配置不当等等。而应用层 DDoS 防御的核心就是区分人与机器(爬虫),因为大量的请求不可能是人为的,肯定是机器构造的。因此如果能有效的区分人与爬虫行为,则可以很好地防御此攻击。

+

其他 DDoS 攻击

发起 DDoS 也是需要大量的带宽资源的,但是互联网就像森林,林子大了什么鸟都有,DDoS 攻击者也能找到其他的方式发起廉价并且极具杀伤力的 DDoS 攻击。

+

利用 XSS

举个例子,如果 12306 页面有一个 XSS 持久型漏洞被恶意攻击者发现,只需在春节抢票期间在这个漏洞中执行脚本使得往某一个小站点随便发点什么请求,然后随着用户访问的增多,感染用户增多,被攻击的站点自然就会迅速瘫痪了。这种 DDoS 简直就是无本万利,不用惊讶,现在大站有 XSS 漏洞的不要太多。

+

来自 P2P 网络攻击

大家都知道,互联网上的 P2P 用户和流量都是一个极为庞大的数字。如果他们都去一个指定的地方下载数据,成千上万的真实 IP 地址连接过来,没有哪个设备能够支撑住。拿 BT 下载来说,伪造一些热门视频的种子,发布到搜索引擎,就足以骗到许多用户和流量了,但是这只是基础攻击。
高级的 P2P 攻击,是直接欺骗资源管理服务器。如迅雷客户端会把自己发现的资源上传到资源管理服务器,然后推送给其它需要下载相同资源的用户,这样,一个链接就发布出去。通过协议逆向,攻击者伪造出大批量的热门资源信息通过资源管理中心分发出去,瞬间就可以传遍整个 P2P 网络。更为恐怖的是,这种攻击是无法停止的,即使是攻击者自身也无法停止,攻击一直持续到 P2P 官方发现问题更新服务器且下载用户重启下载软件为止。

+

DDoS 最后总结

DDoS 不可能防的住,就好比你的店只能容纳 50 人,黑社会有 100 人,你就换一家大店,能容纳 500 人,然后黑社会又找来了 1000 人,这种堆人头的做法就是 DDoS 本质上的攻防之道,「道高一尺,魔高一丈,魔高一尺,道高一丈」,讲真,必要的时候就答应勒索你的人的条件吧,实在不行就报警吧。

+

流量劫持

流量劫持应该算是黑产行业的一大经济支柱了吧?简直是让人恶心到吐,不吐槽了,还是继续谈干货吧,流量劫持基本分两种:DNS 劫持 和 HTTP 劫持,目的都是一样的,就是当用户访问 zoumiaojiang.com 的时候,给你展示的并不是或者不完全是 zoumiaojiang.com 提供的 “内容”。

+

DNS 劫持

DNS 劫持,也叫做域名劫持,可以这么理解,「你打了一辆车想去商场吃饭,结果你打的车是小作坊派来的,直接给你拉到小作坊去了」,DNS 的作用是把网络地址域名对应到真实的计算机能够识别的 IP 地址,以便计算机能够进一步通信,传递网址和内容等。如果当用户通过某一个域名访问一个站点的时候,被篡改的 DNS 服务器返回的是一个恶意的钓鱼站点的 IP,用户就被劫持到了恶意钓鱼站点,然后继而会被钓鱼输入各种账号密码信息,泄漏隐私。

+

这类劫持,要不就是网络运营商搞的鬼,一般小的网络运营商与黑产勾结会劫持 DNS,要不就是电脑中毒,被恶意篡改了路由器的 DNS 配置,基本上做为开发者或站长却是很难察觉的,除非有用户反馈,现在升级版的 DNS 劫持还可以对特定用户、特定区域等使用了用户画像进行筛选用户劫持的办法,另外这类广告显示更加随机更小,一般站长除非用户投诉否则很难觉察到,就算觉察到了取证举报更难。无论如何,如果接到有 DNS 劫持的反馈,一定要做好以下几件事:

+
    +
  • 取证很重要,时间、地点、IP、拨号账户、截屏、URL 地址等一定要有。
  • +
  • 可以跟劫持区域的电信运营商进行投诉反馈。
  • +
  • 如果投诉反馈无效,直接去工信部投诉,一般来说会加白你的域名。
  • +
+

HTTP 劫持

HTTP 劫持您可以这么理解,「你打了一辆车想去商场吃饭,结果司机跟你一路给你递小作坊的广告」,HTTP 劫持主要是当用户访问某个站点的时候会经过运营商网络,而不法运营商和黑产勾结能够截获 HTTP 请求返回内容,并且能够篡改内容,然后再返回给用户,从而实现劫持页面,轻则插入小广告,重则直接篡改成钓鱼网站页面骗用户隐私。能够实施流量劫持的根本原因,是 HTTP 协议没有办法对通信对方的身份进行校验以及对数据完整性进行校验。如果能解决这个问题,则流量劫持将无法轻易发生。所以防止 HTTP 劫持的方法只有将内容加密,让劫持者无法破解篡改,这样就可以防止 HTTP 劫持了。

+

HTTPS 协议就是一种基于 SSL 协议的安全加密网络应用层协议,可以很好的防止 HTTP 劫持。这里有篇 文章 讲的不错。HTTPS 在这就不深讲了,后面有机会我会单独好好讲讲 HTTPS。如果不想站点被 HTTP 劫持,赶紧将你的站点全站改造成 HTTPS 吧。

+

服务器漏洞

服务器除了以上提到的那些大名鼎鼎的漏洞和臭名昭著的攻击以外,其实还有很多其他的漏洞,往往也很容易被忽视,在这个小节也稍微介绍几种。

+

越权操作漏洞

如果你的系统是有登录控制的,那就要格外小心了,因为很有可能你的系统越权操作漏洞,越权操作漏洞可以简单的总结为 「A 用户能看到或者操作 B 用户的隐私内容」,如果你的系统中还有权限控制就更加需要小心了。所以每一个请求都需要做 userid 的判断

+

以下是一段有漏洞的后端示意代码:

+
1
2
3
4
5
6
7
// ctx 为请求的 context 上下文
let msgId = ctx.params.msgId;

mysql.query(
'SELECT * FROM msg_table WHERE msg_id = ?',
[msgId]
);
+ +

以上代码是任何人都可以查询到任何用户的消息,只要有 msg_id 就可以,这就是比较典型的越权漏洞

+

越权操作漏洞防御

如下这么改进一下:

+
1
2
3
4
5
6
7
8
// ctx 为请求的 context 上下文
let msgId = ctx.params.msgId;
let userId = ctx.session.userId; // 从会话中取出当前登陆的 userId

mysql.query(
'SELECT * FROM msg_table WHERE msg_id = ? AND user_id = ?',
[msgId, userId]
);
+

嗯,大概就是这个意思,如果有更严格的权限控制,那在每个请求中凡是涉及到数据库的操作都需要先进行严格的验证,并且在设计数据库表的时候需要考虑进 userId 的账号关联以及权限关联。

+

目录遍历漏洞

目录遍历漏洞指通过在 URL 或参数中构造 .././ 和类似的跨父目录字符串的 ASCII 编码、unicode 编码等,完成目录跳转,读取操作系统各个目录下的敏感文件,也可以称作「任意文件读取漏洞」。

+

目录遍历漏洞原理:程序没有充分过滤用户输入的 ../ 之类的目录跳转符,导致用户可以通过提交目录跳转来遍历服务器上的任意文件。使用多个.. 符号,不断向上跳转,最终停留在根 /,通过绝对路径去读取任意文件。

+

目录遍历漏洞几个示例和测试,一般构造 URL 然后使用浏览器直接访问,或者使用 Web 漏洞扫描工具检测,当然也可以自写程序测试。

+
1
2
3
4
5
6
7
8
9
http://somehost.com/../../../../../../../../../etc/passwd
http://somehost.com/some/path?file=../../Windows/system.ini

# 借助 %00 空字符截断是一个比较经典的攻击手法
http://somehost.com/some/path?file=../../Windows/system.ini%00.js

# 使用了 IIS 的脚本目录来移动目录并执行指令
http://somehost.com/scripts/..%5c../Windows/System32/cmd.exe?/c+dir+c:\

+

目录遍历漏洞防御

方法就是需要对 URL 或者参数进行 ../,./ 等字符的转义过滤。

+

物理路径泄漏

物理路径泄露属于低风险等级缺陷,它的危害一般被描述为「攻击者可以利用此漏洞得到信息,来对系统进一步地攻击」,通常都是系统报错 500 的错误信息直接返回到页面可见导致的漏洞。得到物理路径有些时候它能给攻击者带来一些有用的信息,比如说:可以大致了解系统的文件目录结构;可以看出系统所使用的第三方软件;也说不定会得到一个合法的用户名(因为很多人把自己的用户名作为网站的目录名)。

+

物理路径泄漏防御

防止这种泄漏的方法就是做好后端程序的出错处理,定制特殊的 500 报错页面。

+

源码暴露漏洞

和物理路径泄露类似,就是攻击者可以通过请求直接获取到你站点的后端源代码,然后就可以对系统进一步研究攻击。那么导致源代码暴露的原因是什么呢?基本上就是发生在服务器配置上了,服务器可以设置哪些路径的文件才可以被直接访问的,这里给一个 koa 服务起的例子,正常的 koa 服务器可以通过 koa-static 中间件去指定静态资源的目录,好让静态资源可以通过路径的路由访问。比如你的系统源代码目录是这样的:

+
1
2
3
4
5
6
|- project
|- src
|- static
|- ...
|- server.js

+ +

你想要将 static 的文件夹配成静态资源目录,你应该会在 server.js 做如下配置:

+
1
2
3
4
5
6
const Koa = require('koa');
const serve = require('koa-static');
const app = new Koa();

app.use(serve(__dirname + '/project/static'));

+ +

但是如果配错了静态资源的目录,可能就出大事了,比如:

+
1
2
3
// ...
app.use(serve(__dirname + '/project'));

+ +

这样所有的源代码都可以通过路由访问到了,所有的服务器都提供了静态资源机制,所以在通过服务器配置静态资源目录和路径的时候,一定要注意检验,不然很可能产生漏洞。

+

最后,希望 Web 开发者们能够管理好自己的代码隐私,注意代码安全问题,比如不要将产品的含有敏感信息的代码放到第三方外部站点或者暴露给外部用户,尤其是前端代码,私钥类似的保密性的东西不要直接输出在代码里或者页面中。也许还有很多值得注意的点,但是归根结底还是绷住安全那根弦,对待每一行代码都要多多推敲。

+

转载自:https://zoumiaojiang.com/article/common-web-security/

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 网络安全(2)

+

文章作者:

+

发布时间:2022年06月16日 - 14:40

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/16/%E5%89%8D%E7%AB%AF%E5%B8%B8%E8%A7%81%E7%9F%A5%E8%AF%86%E7%82%B9%E6%95%B4%E7%90%86%20----%20%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%EF%BC%882%EF%BC%89/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/06/30/nginx-timeout/index.html b/2022/06/30/nginx-timeout/index.html new file mode 100644 index 0000000000..3bab1507e0 --- /dev/null +++ b/2022/06/30/nginx-timeout/index.html @@ -0,0 +1,669 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nginx中的超时设置 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ nginx中的超时设置 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

nginx中的超时设置

Nginx主要有四类超时设置:客户端超时设置DNS解析超时设置代理超时设置,如果使用ngx_lua,则还有lua相关的超时设置。

+

客户端超时设置

对于客户端超时主要设置有读取请求头超时时间读取请求体超时时间发送响应超时时间长连接超时时间。通过客户端超时设置避免客户端恶意或者网络状况不佳造成连接长期占用,影响服务端的可处理的能力。

+

client_header_timeout 指定等待client发送一个请求头的超时时间

    +
  • 语法: client_header_timeout time
  • +
  • 默认值: 60s
  • +
  • 上下文: http server
  • +
  • 说明: 指定等待client发送一个请求头的超时时间(例如:GET / HTTP/1.1).仅当在一次read中,没有收到请求头,才会算成超时。如果在超时时间内,client没发送任何东西,nginx返回HTTP状态码408(“Request timed out”)
  • +
+

client_body_timeout 该指令设置请求体(request body)的读超时时间

    +
  • 语法: client_body_timeout time
  • +
  • 默认值: 60s
  • +
  • 上下文: http server location
  • +
  • 说明: 该指令设置请求体(request body)的读超时时间。仅当在一次readstep中,没有得到请求体,就会设为超时。超时后,nginx返回HTTP状态码408(“Request timed out”)
  • +
+

send_timeout time

设置发送响应到客户端的超时时间,默认为60s,此超时时间指的也是两次成功写操作间隔时间,而不是发送整个响应的超时时间。如果在此超时时间内客户端没有接收任何响应,则Nginx关闭此连接。

+

keepalive_timeout 第一个参数指定了与client的keep-alive连接超时时间

    +
  • 语法: keepalive_timeout timeout [ header_timeout ]

    +
  • +
  • 默认值: 75s

    +
  • +
  • 上下文: http server location

    +
  • +
  • 说明: 第一个参数指定了与client的keep-alive连接超时时间。服务器将会在这个时间后关闭连接。可选的第二个参数指定了在响应头Keep-Alive: timeout=time中的time值。这个头能够让一些浏览器主动关闭连接,这样服务器就不必要去关闭连接了。没有这个参数,nginx不会发送Keep-Alive响应头(尽管并不是由这个头来决定连接是否“keep-alive”)
    两个参数的值可并不相同

    +
      +
    • 注意不同浏览器怎么处理“keep-alive”头
    • +
    • MSIE和Opera忽略掉”Keep-Alive: timeout=“ header.
    • +
    • MSIE保持连接大约60-65秒,然后发送TCP RST
    • +
    • Opera永久保持长连接
    • +
    • Mozilla keeps the connection alive for N plus about 1-10 seconds.
    • +
    • Konqueror保持长连接N秒
    • +
    +
  • +
+

DNS解析超时设置

resolver_timeout 设置DNS解析超时时间

    +
  • 语法 resolver_timeout time
  • +
  • 默认值 30s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置DNS解析超时时间
  • +
+

代理超时设置

proxy_connect_timeout 设置与upstream server的连接超时时间,不能超过75秒

    +
  • 语法 proxy_connect_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置与upstream server的连接超时时间,有必要记住,这个超时不能超过75秒。
    这个不是等待后端返回页面的时间,那是由proxy_read_timeout声明的。如果你的upstream服务器起来了,但是hanging住了(例如,没有足够的线程处理请求,所以把你的请求放到请求池里稍后处理),那么这个声明是没有用的,由于与upstream服务器的连接已经建立了。
  • +
+

proxy_read_timeout 设置与代理服务器的读超时时间

    +
  • 语法 proxy_read_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置与代理服务器的读超时时间。它决定了nginx会等待多长时间来获得请求的响应。这个时间不是获得整个response的时间,而是两次reading操作的时间。
  • +
+

proxy_send_timeout 设置发送请求给upstream服务器的超时时间

    +
  • 语法 proxy_send_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 这个指定设置了发送请求给upstream服务器的超时时间。超时设置不是为了整个发送期间,而是在两次write操作期间。如果超时后,upstream没有收到新的数据,nginx会关闭连接
  • +
+

失败重试机制设置。

proxy_next_upstream error | timeout | invalid_header | http_500 | http_502 | http_503 | http_504 |http_403 | http_404 | non_idempotent | off …:配置什么情况下需要请求下一台上游服务器进行重试。默认为“errortimeout”。error表示与上游服务器建立连接、写请求或者读响应头出错。timeout表示与上游服务器建立连接、写请求或者读响应头超时。invalid_header表示上游服务器返回空的或错误的响应头。http_XXX表示上游服务器返回特定的状态码。non_idempotent表示RFC-2616定义的非幂等HTTP方法(POST、LOCK、PATCH),也可以在失败后重试下一台上游服务器(即默认幂等方法GET、HEAD、PUT、DELETE、OPTIONS、TRACE才可以重试)。off表示禁用重试。

+

重试不能无限制进行,因此,需要如下两个指令控制重试次数和重试超时时间。

+

proxy_next_upstream_tries number:设置重试次数,默认0表示不限制,注意此重试次数指的是所有请求次数(包括第一次和之后的重试次数之和)。

+

proxy_next_upstream_timeout time:设置重试最大超时时间,默认0表示不限制。

+

即在proxy_next_upstream_timeout时间内允许proxy_next_upstream_tries次重试。如果超过了其中一个设置,则Nginx也会结束重试并返回客户端响应(可能是错误码)。

+

如下配置表示当error/timeout时重试upstream中的下一台上游服务器,如果重试的总时间超出了6s或者重试了1次,则表示重试失败(因为之前已经请求一次了,所以还能重试一次),Nginx结束重试并返回客户端响应。

+

upstream存活超时设置

max_fails和fail_timeout:配置什么时候Nginx将上游服务器认定为不可用/不存活。当上游服务器在fail_timeout时间内失败了max_fails次,则认为该上游服务器不可用/不存活。并在接下来的fail_timeout时间内从upstream摘掉该节点(即请求不会转发到该上游服务器)。

+

什么情况下被认定为失败呢?其由 proxy_next_upstream定义,不过,不管 proxy_next_upstream如何配置,error, timeout and invalid_header 都将被认为是失败。

+

如server 192.168.61.1:9090max_fails=2 fail_timeout=10s;表示在10s内如果失败了2次,则在接下来的10s内认定该节点不可用/不存活。这种存活检测机制是只有当访问该上游服务器时,采取惰性检查,可以使用ngx_http_upstream_check_module配置主动检查。

+

max_fails设置为0表示不检查服务器是否可用(即认为一直可用),如果upstream中仅剩一台上游服务器时,则该服务器是不会被摘除的,将从不被认为不可用。

+

proxy_upstream_fail_timeout(fail_timeout)设置某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间

    +
  • 语法 server address [fail_timeout=30s]
  • +
  • 默认值 10s
  • +
  • 上下文 upstream
  • +
  • 说明 Upstream模块下 server指令的参数,设置了某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间,默认为10秒
  • +
+

lingering_timeout

    +
  • 语法: lingering_timeout time
  • +
  • 默认值: 5s
  • +
  • 上下文: http server location
  • +
  • 说明: lingering_close生效后,在关闭连接前,会检测是否有用户发送的数据到达服务器,如果超过lingering_timeout时间后还没有数据可读,就直接关闭连接;否则,必须在读取完连接缓冲区上的数据并丢弃掉后才会关闭连接。
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:nginx中的超时设置

+

文章作者:

+

发布时间:2022年06月30日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/30/nginx-timeout/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/08/02/Docker \351\225\234\345\203\217\345\256\211\345\205\250\346\234\200\344\275\263\345\256\236\350\267\265 /index.html" "b/2022/08/02/Docker \351\225\234\345\203\217\345\256\211\345\205\250\346\234\200\344\275\263\345\256\236\350\267\265 /index.html" new file mode 100644 index 0000000000..39c238c81c --- /dev/null +++ "b/2022/08/02/Docker \351\225\234\345\203\217\345\256\211\345\205\250\346\234\200\344\275\263\345\256\236\350\267\265 /index.html" @@ -0,0 +1,609 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 在Docker Compose中配置网络 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 在Docker Compose中配置网络 +

+ + +
+ + + + +
+ + +

Docker 镜像安全最佳实践

Docker 和 宿主机 的设置

    +
  1. 保证宿主机和 Docker 的版本是最新的
  2. +
  3. 不要暴露 Docker 的 守护进程(daemon) 的套接字
  4. +
  5. 使用 rootless 模式启动 Docker
  6. +
  7. 避免使用特权容器
  8. +
  9. 限制容器资源
  10. +
  11. 隔离容器网络
  12. +
  13. 提高容器的隔离度
  14. +
  15. 将文件系统和卷设置为只读
  16. +
  17. 完整的生命周期管理
  18. +
  19. 限制来自容器内的系统调用
  20. +
+

确保镜像安全

    +
  1. 扫描和验证容器镜像
  2. +
  3. 使用最小基础镜像
  4. +
  5. 不要向 Docker 镜像泄露敏感信息
  6. +
  7. 使用多阶段构建
  8. +
  9. 确保容器注册
  10. +
  11. 使用固定标签以获得不变性
  12. +
+

监控容器

    +
  1. 监控容器活动
  2. +
  3. 确保容器在运行时的安全
  4. +
  5. 将故障排除数据与容器分开保存
  6. +
  7. 为镜像使用元数据标签
  8. +
+

参考

Top 20 Dockerfile best practices
Dockerセキュリティベストプラクティス トップ20

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:在Docker Compose中配置网络

+

文章作者:

+

发布时间:2022年08月02日 - 19:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/Docker%20%E9%95%9C%E5%83%8F%E5%AE%89%E5%85%A8%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%20/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/08/02/Network in Compose \345\234\250Docker Compose\344\270\255\351\205\215\347\275\256\347\275\221\347\273\234/index.html" "b/2022/08/02/Network in Compose \345\234\250Docker Compose\344\270\255\351\205\215\347\275\256\347\275\221\347\273\234/index.html" new file mode 100644 index 0000000000..778dd669cc --- /dev/null +++ "b/2022/08/02/Network in Compose \345\234\250Docker Compose\344\270\255\351\205\215\347\275\256\347\275\221\347\273\234/index.html" @@ -0,0 +1,586 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 在Docker Compose中配置网络 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 在Docker Compose中配置网络 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Network in Compose 在Docker Compose中配置网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
version: "3.9"

services:
proxy:
build: ./proxy
networks:
- actsnetwork
app:
build: ./app
networks:
- actsnetwork
db:
image: postgres
networks:
- testnetwork

networks:
actsnetwork:
name: testnetwork
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:在Docker Compose中配置网络

+

文章作者:

+

发布时间:2022年08月02日 - 19:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/Network%20in%20Compose%20%E5%9C%A8Docker%20Compose%E4%B8%AD%E9%85%8D%E7%BD%AE%E7%BD%91%E7%BB%9C/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/08/02/Nginx\344\270\255\347\232\204\350\266\205\346\227\266\350\256\276\347\275\256/index.html" "b/2022/08/02/Nginx\344\270\255\347\232\204\350\266\205\346\227\266\350\256\276\347\275\256/index.html" new file mode 100644 index 0000000000..6fea30eb3c --- /dev/null +++ "b/2022/08/02/Nginx\344\270\255\347\232\204\350\266\205\346\227\266\350\256\276\347\275\256/index.html" @@ -0,0 +1,652 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Nginx中的超时设置 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Nginx中的超时设置 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Nginx中的超时设置

client_header_timeout 指定等待client发送一个请求头的超时时间

    +
  • 语法: client_header_timeout time
  • +
  • 默认值: 60s
  • +
  • 上下文: http server
  • +
  • 说明: 指定等待client发送一个请求头的超时时间(例如:GET / HTTP/1.1).仅当在一次read中,没有收到请求头,才会算成超时。如果在超时时间内,client没发送任何东西,nginx返回HTTP状态码408(“Request timed out”)
  • +
+

client_body_timeout 该指令设置请求体(request body)的读超时时间

    +
  • 语法: client_body_timeout time
  • +
  • 默认值: 60s
  • +
  • 上下文: http server location
  • +
  • 说明: 该指令设置请求体(request body)的读超时时间。仅当在一次readstep中,没有得到请求体,就会设为超时。超时后,nginx返回HTTP状态码408(“Request timed out”)
  • +
+

keepalive_timeout 第一个参数指定了与client的keep-alive连接超时时间

    +
  • 语法: keepalive_timeout timeout [ header_timeout ]

    +
  • +
  • 默认值: 75s

    +
  • +
  • 上下文: http server location

    +
  • +
  • 说明: 第一个参数指定了与client的keep-alive连接超时时间。服务器将会在这个时间后关闭连接。可选的第二个参数指定了在响应头Keep-Alive: timeout=time中的time值。这个头能够让一些浏览器主动关闭连接,这样服务器就不必要去关闭连接了。没有这个参数,nginx不会发送Keep-Alive响应头(尽管并不是由这个头来决定连接是否“keep-alive”)
    两个参数的值可并不相同

    +
      +
    • 注意不同浏览器怎么处理“keep-alive”头
    • +
    • MSIE和Opera忽略掉”Keep-Alive: timeout=“ header.
    • +
    • MSIE保持连接大约60-65秒,然后发送TCP RST
    • +
    • Opera永久保持长连接
    • +
    • Mozilla keeps the connection alive for N plus about 1-10 seconds.
    • +
    • Konqueror保持长连接N秒
    • +
    +
  • +
+

lingering_timeout

    +
  • 语法: lingering_timeout time
  • +
  • 默认值: 5s
  • +
  • 上下文: http server location
  • +
  • 说明: lingering_close生效后,在关闭连接前,会检测是否有用户发送的数据到达服务器,如果超过lingering_timeout时间后还没有数据可读,就直接关闭连接;否则,必须在读取完连接缓冲区上的数据并丢弃掉后才会关闭连接。
  • +
+

resolver_timeout 设置DNS解析超时时间

    +
  • 语法 resolver_timeout time
  • +
  • 默认值 30s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置DNS解析超时时间
  • +
+

proxy_connect_timeout 设置与upstream server的连接超时时间,不能超过75秒

    +
  • 语法 proxy_connect_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置与upstream server的连接超时时间,有必要记住,这个超时不能超过75秒。
    这个不是等待后端返回页面的时间,那是由proxy_read_timeout声明的。如果你的upstream服务器起来了,但是hanging住了(例如,没有足够的线程处理请求,所以把你的请求放到请求池里稍后处理),那么这个声明是没有用的,由于与upstream服务器的连接已经建立了。
  • +
+

proxy_read_timeout 设置与代理服务器的读超时时间

    +
  • 语法 proxy_read_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置与代理服务器的读超时时间。它决定了nginx会等待多长时间来获得请求的响应。这个时间不是获得整个response的时间,而是两次reading操作的时间。
  • +
+

proxy_send_timeout 设置发送请求给upstream服务器的超时时间

    +
  • 语法 proxy_send_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 这个指定设置了发送请求给upstream服务器的超时时间。超时设置不是为了整个发送期间,而是在两次write操作期间。如果超时后,upstream没有收到新的数据,nginx会关闭连接
  • +
+

proxy_upstream_fail_timeout(fail_timeout)设置某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间

    +
  • 语法 server address [fail_timeout=30s]
  • +
  • 默认值 10s
  • +
  • 上下文 upstream
  • +
  • 说明 Upstream模块下 server指令的参数,设置了某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间,默认为10秒
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Nginx中的超时设置

+

文章作者:

+

发布时间:2022年08月02日 - 19:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/Nginx%E4%B8%AD%E7%9A%84%E8%B6%85%E6%97%B6%E8%AE%BE%E7%BD%AE/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/08/02/Oracle-Linux-install-Docker/index.html b/2022/08/02/Oracle-Linux-install-Docker/index.html new file mode 100644 index 0000000000..bf885632cf --- /dev/null +++ b/2022/08/02/Oracle-Linux-install-Docker/index.html @@ -0,0 +1,590 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 在 Oracle Linux 中安装 Docker | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 在 Oracle Linux 中安装 Docker +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Oracle Linux install Docker(https://docs.docker.com/engine/install/centos/)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 查看
systemctl list-units

sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine

sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

sudo yum install -y yum-utils

sudo yum -y install docker-ce-20.10.17 docker-ce-cli-20.10.17 containerd.io-1.6.6

sudo vi /etc/yum.repos.d/docker-ce.repo

[centos-extras]
name=Centos extras - $basearch
baseurl=http://mirror.centos.org/centos/7/extras/x86_64
enabled=1
gpgcheck=1
gpgkey=http://centos.org/keys/RPM-GPG-KEY-CentOS-7

sudo yum -y install fuse-overlayfs slirp4netns

# sudo chmod 777 docker-ce.repo

# yum list docker-ce --showduplicates | sort -r

sudo systemctl start docker

sudo docker run hello-world

sudo yum install /path/to/package.rpm
sudo systemctl start docker
sudo docker run hello-world
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:在 Oracle Linux 中安装 Docker

+

文章作者:

+

发布时间:2022年08月02日 - 18:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/Oracle-Linux-install-Docker/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/08/02/OracleLinux\345\206\205\346\240\270\344\273\216uek\345\210\207\346\215\242\345\210\260uek/index.html" "b/2022/08/02/OracleLinux\345\206\205\346\240\270\344\273\216uek\345\210\207\346\215\242\345\210\260uek/index.html" new file mode 100644 index 0000000000..0c344adb34 --- /dev/null +++ "b/2022/08/02/OracleLinux\345\206\205\346\240\270\344\273\216uek\345\210\207\346\215\242\345\210\260uek/index.html" @@ -0,0 +1,578 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OracleLinux内核从uek切换到uek | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ OracleLinux内核从uek切换到uek +

+ + +
+ + + + +
+ + +
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#/boot/grub/grub.conf 缺失:

yum install -y grub
grub-mkconfig -o /boot/grub/grub.conf

#/boot/grub2/grub.cfg 缺失:

yum install -y grub2
grub2-mkconfig -o /boot/grub2/grub.cfg

uname -a
sudo grep ^menuentry /boot/grub2/grub.cfg | awk -F\' '{print $2}' | nl -v0

sudo grub2-editenv list

sudo grep GRUB_DEFAULT /etc/default/grub

sudo vim /etc/default/grub

sudo grub2-set-default 1

sudo grub2-mkconfig -o /boot/grub2/grub.cfg

sudo dracut --force

sudo reboot

uname -a
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:OracleLinux内核从uek切换到uek

+

文章作者:

+

发布时间:2022年08月02日 - 18:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/OracleLinux%E5%86%85%E6%A0%B8%E4%BB%8Euek%E5%88%87%E6%8D%A2%E5%88%B0uek/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/08/02/Postgresql\347\273\277\350\211\262\347\211\210\344\275\277\347\224\250/index.html" "b/2022/08/02/Postgresql\347\273\277\350\211\262\347\211\210\344\275\277\347\224\250/index.html" new file mode 100644 index 0000000000..739682e643 --- /dev/null +++ "b/2022/08/02/Postgresql\347\273\277\350\211\262\347\211\210\344\275\277\347\224\250/index.html" @@ -0,0 +1,586 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Postgresql绿色版使用 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Postgresql绿色版使用 +

+ + +
+ + + + +
+ + +

初始化数据库

c:\pgsql\bin\initdb.exe -D c:\pgsql\data -E UTF8

+

注册表

c:\pgsql\bin\pg_ctl register -N postgres -D c:\pgsql\data

+

启动服务

c:\pgsql\bin\postgres -D c:\pgsql\data

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Postgresql绿色版使用

+

文章作者:

+

发布时间:2022年08月02日 - 19:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/Postgresql%E7%BB%BF%E8%89%B2%E7%89%88%E4%BD%BF%E7%94%A8/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/08/02/linux-scp/index.html b/2022/08/02/linux-scp/index.html new file mode 100644 index 0000000000..e6d1faa11f --- /dev/null +++ b/2022/08/02/linux-scp/index.html @@ -0,0 +1,599 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 在linux之间传输文件的命令scp | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 在linux之间传输文件的命令scp +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

这里假设要从Linux服务器A中把文件 file.txt 传输到Linux服务器B,该怎么做,这里就可以用scp实现
为了能够成功的传输文件,需要做到以下几点:

+
    +
  1. A需要知道要传输的的文件本地的路径
  2. +
  3. A为了往B传输文件,所以需要知道B的访问权限,因此需要B的访问地址,账号和私钥
  4. +
  5. 目标路径
  6. +
  7. 因为Linux权限管理的要求,私钥的访问权限需要设定为600
  8. +
+

当然还有一点前提是A和B两台服务器之间本身是可以通信的
知道以上信息便可以进行文件传输

+

实践

现在假设

+
    +
  1. A要往B传输的文件在A的如下目录:/tmp/transfer/file.txt
  2. +
  3. B的访问地址是 192.168.31.31 访问用户名是 testuser 用到的key上传到了A的如下目录: /tmp/ssh/test.key
  4. +
  5. 目标路径为B的如下目录: /tmp/test
    知道上面信息后我们来创建命令。
  6. +
+
1
2
chomd 600 /tmp/ssh/test.key
scp -i /tmp/ssh/test.key /tmp/transfer/file.txt testuser@192.168.31.31 /tmp/test
+ +

通过上面的命令即可实现从A服务器传输文件到B服务器了。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:在linux之间传输文件的命令scp

+

文章作者:

+

发布时间:2022年08月02日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/linux-scp/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/08/02/systemctl \345\221\275\344\273\244/index.html" "b/2022/08/02/systemctl \345\221\275\344\273\244/index.html" new file mode 100644 index 0000000000..ae3f4bb3a9 --- /dev/null +++ "b/2022/08/02/systemctl \345\221\275\344\273\244/index.html" @@ -0,0 +1,601 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + systemctl 命令 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ systemctl 命令 +

+ + +
+ + + + +
+ + +

systemctl 命令

systemctl

范列出系统上面有启动的unit

+

systemctl list-unit-files

列出所有已经安装的unit有哪些

+

systemctl list-units –type=service –all

列出类型为service的所有项目,不论启动与否

+

systemctl get-default

输入目前机器默认的模式,如图形界面模式或者文本模式

+

systemctl isolate multi-user.target

将目前的操作环境改为纯文本模式,关掉图形界面

+

systemctl isolate graphical.target

将目前的操作环境改为图形界面

+

systemctl poweroff

系统关机

+

systemctl reboot

重新开机

+

systemctl suspend

进入暂停模式

+

systemctl rescue

强制进入救援模式

+

systemctl hibernate

进入休眠模式

+

systemctl emergency

强制进入紧急救援模式

+

systemctl list-dependencies –reverse

查询当前默认的target关联了啥

+

systemctl list-dependencies graphical.target

查询图形界面模式的target关联了啥

+

systemctl list-sockets

查看当前的socket服务

+

systemctl show etcd.service

查看 unit 的详细配置情况

+

systemctl mask etcd.service

禁用某个服务

+

systemctl unmask etcd.service

解除禁用某个服务

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:systemctl 命令

+

文章作者:

+

发布时间:2022年08月02日 - 18:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/systemctl%20%E5%91%BD%E4%BB%A4/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/08/02/\346\220\255\345\273\272Docker\347\247\201\346\234\211\344\273\223\345\272\223/index.html" "b/2022/08/02/\346\220\255\345\273\272Docker\347\247\201\346\234\211\344\273\223\345\272\223/index.html" new file mode 100644 index 0000000000..7825f361dc --- /dev/null +++ "b/2022/08/02/\346\220\255\345\273\272Docker\347\247\201\346\234\211\344\273\223\345\272\223/index.html" @@ -0,0 +1,595 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 搭建Docker私有仓库 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 搭建Docker私有仓库 +

+ + +
+ + + + +
+ + +

搭建Docker私有仓库

利用registry搭建私有仓库

+

下载registry

1
docker pull registry
+ +

配置

1
2
/etc/docker/doemon.json
# 配置 'insecure-registry'
+

重启docker

1
systemctl restart docker
+ +

创建registry容器(关联私有仓库配置)

1
docker run -d -p 5000:5000 --name registry registry:latest
+ +

推送镜像到私有仓

    +
  • 备份镜像(172.16.12.134:5000 私有仓地址)

    +
    1
    docker tag ubuntu:latest 172.16.12.134:5000/my_ubuntu
  • +
  • 推送

    +
    1
    docker push 172.16.12.134:5000/my_ubuntu
  • +
  • 下载

    +
    1
    docker pull 172.16.12.134:5000/my_ubuntu
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:搭建Docker私有仓库

+

文章作者:

+

发布时间:2022年08月02日 - 18:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/%E6%90%AD%E5%BB%BADocker%E7%A7%81%E6%9C%89%E4%BB%93%E5%BA%93/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2022/08/02/\346\234\215\345\212\241\345\231\250\351\253\230\345\215\261\347\253\257\345\217\243\345\210\227\350\241\250/index.html" "b/2022/08/02/\346\234\215\345\212\241\345\231\250\351\253\230\345\215\261\347\253\257\345\217\243\345\210\227\350\241\250/index.html" new file mode 100644 index 0000000000..2c2588099f --- /dev/null +++ "b/2022/08/02/\346\234\215\345\212\241\345\231\250\351\253\230\345\215\261\347\253\257\345\217\243\345\210\227\350\241\250/index.html" @@ -0,0 +1,865 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 服务器高危端口列表 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 服务器高危端口列表 +

+ + +
+ + + + +
+ + +

服务器高危端口列表

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
协议端口服务渗透测试
tcp20,21FTP(文件传输协议)允许匿名的上传下载,爆破,嗅探,win提权,远程执行(proftpd 1.3.5),各类后门(proftpd,vsftp 2.3.4)
tcp22SSH (安全外壳协议 )可根据已搜集到的信息尝试爆破,v1版本可中间人,ssh隧道及内网代理转发,文件传输等等
tcp23Telnet ( 远程终端协议)爆破,嗅探,一般常用于路由,交换登陆,可尝试弱口令
tcp25SMTP(简单邮件传输协议)邮件伪造,vrfy/expn查询邮件用户信息,可使用smtp-user-enum工具来自动跑
tcp/udp53DNS(域名系统)允许区域传送,dns劫持,缓存投毒,欺骗以及各种基于dns隧道的远控
tcp/udp69TFTP (简单文件传送协议 )尝试下载目标及其的各类重要配置文件
tcp80-89,443,8440-8450,8080-8089各种常用的Web服务端口,可尝试经典的topn,vpn,owa,webmail,目标oa,各类Java控制台,各类服务器Web管理面板,各类Web中间件漏洞利用,各类Web框架漏洞利用等等……
tcp110POP3(邮局协议版本3 )可尝试爆破,嗅探
tcp111,2049NFS(网络文件系统)权限配置不当
tcp137,139,445SMB(NETBIOS协议)可尝试爆破以及smb自身的各种远程执行类漏洞利用,如,ms08-067,ms17-010,嗅探等……
tcp143IMAP(邮件访问协议)可尝试爆破
udp161SNMP(简单网络管理协议)爆破默认团队字符串,搜集目标内网信息
tcp389LDAP( 轻量目录访问协议 )ldap注入,允许匿名访问,弱口令
tcp512,513,514Linux rexec (远程登录)可爆破,rlogin登陆
tcp873Rsync (数据镜像备份工具)匿名访问,文件上传
tcp1194OpenVPN(虚拟专用通道)想办法钓VPN账号,进内网
tcp1352Lotus(Lotus软件)弱口令,信息泄漏,爆破
tcp1433SQL Server(数据库管理系统)注入,提权,sa弱口令,爆破
tcp1521Oracle(甲骨文数据库)tns爆破,注入,弹shell…
tcp1500ISPmanager( 主机控制面板)弱口令
tcp1723PPTP(点对点隧道协议 )爆破,想办法钓VPN账号,进内网
tcp2082,2083cPanel (虚拟机控制系统 )弱口令
tcp2181ZooKeeper(分布式系统的可靠协调系统 )未授权访问
tcp2601,2604Zebra (zebra路由)默认密码zerbra
tcp3128Squid (代理缓存服务器)弱口令
tcp3312,3311kangle(web服务器)弱口令
tcp3306MySQL(数据库)注入,提权,爆破
tcp3389Windows rdp(桌面协议)shift后门[需要03以下的系统],爆破,ms12-020
tcp3690SVN(开放源代码的版本控制系统)svn泄露,未授权访问
tcp4848GlassFish(应用服务器)弱口令
tcp5000Sybase/DB2(数据库)爆破,注入
tcp5432PostgreSQL(数据库)爆破,注入,弱口令
tcp5900,5901,5902VNC(虚拟网络控制台,远控)弱口令爆破
tcp5984CouchDB(数据库)未授权导致的任意指令执行
tcp6379Redis(数据库)可尝试未授权访问,弱口令爆破
tcp7001,7002WebLogic(WEB应用系统)Java反序列化,弱口令
tcp7778Kloxo(虚拟主机管理系统)主机面板登录
tcp8000Ajenti(Linux服务器管理面板)弱口令
tcp8443Plesk(虚拟主机管理面板)弱口令
tcp8069Zabbix (系统网络监视)远程执行,SQL注入
tcp8080-8089Jenkins,JBoss (应用服务器)反序列化,控制台弱口令
tcp9080-9081,9090WebSphere(应用服务器)Java反序列化/弱口令
tcp9200,9300ElasticSearch (Lucene的搜索服务器)远程执行
tcp11211Memcached(缓存系统)未授权访问
tcp27017,27018MongoDB(数据库)爆破,未授权访问
tcp50070,50030Hadoop(分布式文件系统)默认端口未授权访问
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:服务器高危端口列表

+

文章作者:

+

发布时间:2022年08月02日 - 18:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/%E6%9C%8D%E5%8A%A1%E5%99%A8%E9%AB%98%E5%8D%B1%E7%AB%AF%E5%8F%A3%E5%88%97%E8%A1%A8/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2023/09/20/DockerCompose\345\256\211\350\243\205/index.html" "b/2023/09/20/DockerCompose\345\256\211\350\243\205/index.html" new file mode 100644 index 0000000000..e459d22187 --- /dev/null +++ "b/2023/09/20/DockerCompose\345\256\211\350\243\205/index.html" @@ -0,0 +1,561 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ +

+ + +
+ + + + +
+ + +

DockerCompose安装

1
2
3
4
5
DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
mkdir -p $DOCKER_CONFIG/cli-plugins
sudo curl -SL https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-x86_64 -o /usr/bin/docker-compose
sudo chmod +x /usr/bin/docker-compose
sudo docker-compose version
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:

+

文章作者:

+

发布时间:2023年09月20日 - 10:18

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2023/09/20/DockerCompose%E5%AE%89%E8%A3%85/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/09/20/test/index.html b/2023/09/20/test/index.html new file mode 100644 index 0000000000..947214032b --- /dev/null +++ b/2023/09/20/test/index.html @@ -0,0 +1,590 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + test | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ test +

+ + +
+ + + + +
+ + +

前言

最近各种技术博客,各种技术文章,各种牛人前辈的学习心得都在看,最近看了一个不错的文章叫做《如何正确使用时间》
感觉还是很不错的,特别适合年轻的程序员看一下。下面将全文贴在下方,如果需要也可以去原文查看。

+

原文

+

你是否时常会焦虑时间过的很快,没时间学习,本文将会分享一些个人的见解。

+

花时间补基础,读文档

在工作中我们时常会花很多时间去 debug,但是你是否发现很多问题最终只是你基础不扎实或者文档没有仔细看。

+

基础是你技术的基石,一定要花时间打好基础,而不是追各种新的技术。一旦你的基础扎实,学习各种新的技术也肯定不在话下,因为新的技术,究其根本都是相通的。

+

文档同样也是一门技术的基础。一个优秀的库,开发人员肯定已经把如何使用这个库都写在文档中了,仔细阅读文档一定会是少写 bug 的最省事路子。

+

学会搜索

如果你还在使用百度搜索编程问题,请尽快抛弃这个垃圾搜索引擎。同样一个关键字,使用百度和谷歌,谷歌基本完胜的。即使你使用中文在谷歌中搜索,得到的结果也往往是谷歌占优,所以如果你想迅速的通过搜索引擎来解决问题,那一定是谷歌。

+

学点英语

说到英语,一定是大家所最不想听的。其实我一直认为程序员学习英语是简单的,因为我们工作中是一直接触着英语,并且看懂技术文章,文档所需要的单词量是极少的。我时常在群里看到大家发出一个问题的截图问什么原因,其实在截图中英语已经很明白的说明了问题的所在,如果你的英语过关,完全不需要浪费时间来提问和搜索。所以我认为学点英语也是节省时间中很重要的一点。

+

那么如何去学习呢,chrome 装个翻译插件,直接拿英文文档或文章读,不会的就直接划词翻译,然后记录下这个单词并背诵。每天花半小时看点英文文档和文章,坚持两个月,你的英语水平不说别的,看文档和文章绝对不会有难题了。这一定是一个很划算的个人时间投资,花点时间学习英语,能为你将来的技术之路铺平很多坎。

+

画个图,想一想再做

你是否遇到过这种问题,需求一下来,看一眼,然后马上就按照设计稿开始做了,可能中间出个问题导致你需要返工。

+

如果你存在这样的问题,我很推荐在看到设计稿和需求的时候花点时间想一想,画一画。考虑一下设计稿中是否可以找到可以拆分出来的复用组件,是否存在之前写过的组件。该如何组织这个界面,数据的流转是怎么样的。然后画一下这个页面的需求,最后再动手做。

+

利用好下班时间学习

说到下班时间,那可能就有人说了公司很迟下班,这其实是国内很普遍的情况。但是我认为正常的加班是可以的,但是强制的加班就是在损耗你的身体和前途。

+

可以这么说,大部分的 996 公司,加班的这些时间并不会增加你的技术,无非就是在写一些重复的业务逻辑。也许你可以拿到更多的钱,但是代价是身体还有前途。程序员是靠技术吃饭的,如果你长久呆在一个长时间加班的公司,不能增长你的技术还要吞噬你的下班学习时间,那么你一定会废掉的。如果你遇到了这种情况,只能推荐尽快跳槽到非 996 的公司。

+

那么如果你有足够的下班时间,一定要花上 1, 2 小时去学习,上班大家基本都一样,技术的精进就是看下班以后的那几个小时了。如果你能利用好下班时间来学习,坚持下去,时间一定会给你很好的答复。

+

列好 ToDo

我喜欢规划好一段时间内要做的事情,并且要把事情拆分为小点。给 ToDo 列好优先级,紧急的优先级最高。相同优先级的我喜欢先做简单的,因为这样一旦完成就能划掉一个,提高成就感。

+

反思和整理

每周末都会花上点时间整理下本周记录的笔记和看到的不错文章。然后考虑下本周完成的工作和下周准备要完成的工作。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:test

+

文章作者:

+

发布时间:2023年09月20日 - 18:33

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2023/09/20/test/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/@attachment/Clipboard_2022-06-09-12-57-39.png b/@attachment/Clipboard_2022-06-09-12-57-39.png new file mode 100644 index 0000000000000000000000000000000000000000..22826a166c23580cbad4c0f41c2be15e3f11df32 GIT binary patch literal 6725 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 literal 0 HcmV?d00001 diff --git a/@attachment/README.html b/@attachment/README.html new file mode 100644 index 0000000000..af08e2610d --- /dev/null +++ b/@attachment/README.html @@ -0,0 +1,471 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | 李旭光的成长博客 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CNAME b/CNAME new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ + diff --git a/about/index.html b/about/index.html new file mode 100644 index 0000000000..2d85f9dfc5 --- /dev/null +++ b/about/index.html @@ -0,0 +1,691 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 我的履历 | 李旭光的成长博客 + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + + + +
+ + + + + +
+
+ +

我的履历 +

+ + + +
+ + + + +
+

ZH | EN | JA

+

李旭光

+

前端开发工程师 10年
手机: 15641181846
邮箱: lixuguang316@foxmail.com
Github: https://github.com/lixuguang
技术博客: https://lixuguang.github.io
住址: 大连沙河口区黄河路852号

+
+

技能掌握

    +
  • 熟练使用Html/Css/Javascript/Jq,响应式开发,移动端Web开发

    +
  • +
  • 熟练使用VUE/vuex/axios/vue-router/element-ui等vue全家桶完成项目/组件开发

    +
  • +
  • 熟练进行前后端分离开发,前后端接口联调,基于git的版本管理,CICD开发等。

    +
  • +
  • 熟练使用chrome-dev对项目进行前端代码调试/调优

    +
  • +
  • 掌握Nodejs/Electron/React/less/sass/bootstrap等开发

    +
  • +
  • 掌握webpack基本配置,npm包管理等前端工程化开发方式

    +
  • +
  • 掌握前端架构方法,对前端规范性要求非常熟悉,能够根据产品/项目需求给出最优技术选型方案。

    +
  • +
  • 熟练使用svn/git等代码版本管理工具及gitflow管理方式进行代码管理

    +
  • +
  • 10年前端开发经验,参与过200+的项目开发。

    +
  • +
  • 善于团队协作,自我驱动,持续学习,热爱工作,责任心强。

    +
  • +
+

工作经历

+

埃森哲信息技术(大连)有限公司:信息技术,服务咨询公司
2020.6-至今
AM Level8 团队规模:50人 公司规模:20000人
主要职责:

+
    +
  • 技术向:编写代码/审核代码/程序设计
  • +
  • 管理向:任务分解/任务安排/工作汇报/人才培养
  • +
+
+
+
+

华宇(大连)信息服务有限公司:主要面向政府、公检法机关及互联网市场大客户。
2014.5-2020.6
资深前端开发工程师 团队规模:30人 公司规模:600人
主要职责:

+
    +
  • 编写代码
      +
    • 完成正常项目任务
        +
      • 累计完成项目超过150个
      • +
      • 参与项目类型包括pcWeb、padWeb、手机Web、微信Web、触摸屏、指挥控制大屏
      • +
      • 业务类型涉及政府、公检法各个领域
      • +
      • 使用技术包含HTML4/5、CSS2/3、ES5/6、bootstrap、framework7、Angular、Vue以及Artery框架等
      • +
      +
    • +
    • 极短时间内完成任务
        +
      • 竞标项目如:
          +
        • 5天60个页面带领一个初级前端和一个中级前端完成。
        • +
        • 3天完成一个问卷调查包含各种交互的特效。
        • +
        • 封闭1星期同3位开发共同完成canvas画板功能,支持检察官记录员实现档案卷宗实时编辑。
        • +
        +
      • +
      • 演示项目如:
          +
        • 1个月带领一个初级前端一个初级开发完成整个系统包含业务逻辑除后台的所有功能,实现超高保真原型。
        • +
        +
      • +
      • 救火任务如:
          +
        • 上线前2天解决其他人无法解决的问题,此类情况居多。
        • +
        • 项目工期提前,顶着压力加班完成任务。如1天13个页面。
        • +
        +
      • +
      +
    • +
    • 新技术框架研究应用
        +
      • 前后端分离技术
          +
        • 组织前端团队学习前后端分离思想,研究前后端分离所要学习的新技能知识,面向对象的思想以及面向接口的思想等。
        • +
        +
      • +
      • 前端自动化构建、自动检测
          +
        • 组织前端团队学习前端自动化构建如:npm包管理工具、各种cli工具使用、eslint、stylint等自动化检查工具,以及webpack和gulp等打包工具使用。
        • +
        +
      • +
      • MVVM框架等新技术
          +
        • 组织学习研究Angular2、Vue2等数据驱动的MVVM框架,并在实际项目中使用VUE框架。
        • +
        +
      • +
      +
    • +
    • 组织构建基础服务
        +
      • 前端通用组件库
          +
        • Vue组件库
        • +
        +
      • +
      • 前端题库
      • +
      • +
      +
    • +
    • 制定规范
        +
      • 前端编码规范
          +
        • 包括HTML规范、CSS规范、JS规范、Vue规范、通用规范等。
        • +
        +
      • +
      +
    • +
    • 团队协作制度
        +
      • 包括代码管理制度、团队协作规范制度。
      • +
      +
    • +
    • 代码审查规范
        +
      • 人工审核
      • +
      • 自动化检测工具审核
      • +
      +
    • +
    • 知识考核规范
        +
      • 前端知识点地图
      • +
      • 前端技能栈思维导图
      • +
      • 前端考试平台
      • +
      +
    • +
    • 团队建设
        +
      • 面试官
          +
        • 从原来的2人前端团队发展至43人前端团队,6年流失率仅为6人。
        • +
        +
      • +
      • 组织师傅帮带
          +
        • 组织先进带后进,高级带初级,建立前端梯队,组织共同学习。
        • +
        +
      • +
      • 前端兴趣小组
          +
        • 每周二组织前端兴趣小组,了解当下最新的前端知识,组织分享活动。
        • +
        +
      • +
      • 前端培训
          +
        • 不定期组织学习公司将要使用的新技术以及开发过程中遇到的问题解决方案
        • +
        • 定期组织基础知识、中级知识、高级知识、实践应用四个层面的培训。
        • +
        +
      • +
      • 人才培养
          +
        • 人员考核
            +
          • 根据建立的知识点地图建立考点地图,建立试卷进行考评,关系到员工晋升。
          • +
          +
        • +
        • 建立人才档案,定期谈话
            +
          • 根据不同人的特点建立对应的人才档案,通过季度谈话的方式了解员工最新动态,根据员工特点设立学习方向和目标,引导员工成长。
          • +
          +
        • +
        • 参与人才晋升考官
            +
          • 参与员工晋升考核,给出努力方向。
          • +
          +
        • +
        +
      • +
      +
    • +
    +
  • +
+
+
+
+

大连新桥科技发展有限公司:面向教育系统提供整体服务。
2013.1-2014.1
用户体验团队负责人 团队规模:6人 公司规模:50人
主要职责:

+
    +
  • 接访客户:去客户现场或客户到工作现场,负责接待客户,了解客户需求。
  • +
  • 设计、编写代码:根据客户或领导需求完成需求分析、视觉设计、代码编写等任务。涉及pcWeb、padWeb、手机Web等。
  • +
  • 团队管理:根据任务量分配工作,协调团队资源。
  • +
+
+
+
+

百维数元信息科技(北京)有限公司:一斑网在线调研平台。
2011.9-2012.12
开发工程师 团队规模:3人 公司规模:8人
主要职责:

+
    +
  • 设计、前后台代码编写、运营、客服:初创公司,开发运营团队3人,主要负责设计工作、前端开发、部分后端开发、运营、客服等工作。
  • +
+
+

教育经历

+

大连外国语大学
2008.9-2012.6
信息系统与信息管理(日英双语强化) 本科 计算机部部长 全优

+
+

项目经历

近期项目经历

    +
  • 项目名:みんなのぎんこう「ZERO」
  • +
  • 项目介绍:移动手机银行
  • +
  • 项目规模:3年 120人团队
  • +
  • 项目担当:银行愿望账户开发,测试,审核代码
  • +
  • 技术应用:Angular系列
  • +
+
+
    +
  • 项目名:TMNF
  • +
  • 项目介绍:保险业务系统
  • +
  • 项目规模:1年 100人团队
  • +
  • 项目担当:保单,审批业务,任务分配,代码审核
  • +
  • 技术应用:Angular系列
  • +
+

过往项目经历

    +
  • ERP系统
      +
    • 法院执行线索分析系统v2.1-v2.2
    • +
    • 数据质量检查系统v2.0-v2.3
    • +
    • 法官办案辅助系统
    • +
    • 量刑规范化服务系统
    • +
    • 裁判文书上网直报系统V2.0
    • +
    • 律师阅卷管理系统
    • +
    • … 100+
    • +
    +
  • +
  • CMS网站
      +
    • 诉讼服务网系列 50+
    • +
    • 法院官网 20+
    • +
    • … 30+
    • +
    +
  • +
  • pad、手机、触控屏系统
      +
    • 信息引导侦查系列产品
    • +
    • 远程视频会见系统2.1
    • +
    • 领导驾驶舱
    • +
    • 移动办案APP
    • +
    • … 30+
    • +
    +
  • +
  • 普通网站
      +
    • 企业官网、政府官网 10+
    • +
    +
  • +
+

涉及技术

语言:
HTML4/5、CSS2/3、ES5/6、JSP…
框架:
JQ、bootstarp、mui、framework7、VUE…
理论:
前后端分离(面向对象OOB + 异步请求ajax + 面向接口api)

+

自我介绍

从2011年算起我已经从事前端开发10年、设计2年(重叠)、团队管理6年(重叠),参与各类大小项目300+,涉及各个业务领域包括银行、保险、公检法政、教育、调研,覆盖目前主流的前端技术(游戏向不包含)。

+

基础能力扎实,能够解决绝大多数普通问题和部分棘手问题,从小在军营成长让我对团队纪律与制度有着深刻的认识,知道规范与制度的重要性,这为我建立高效优质的团队提供了良好土壤。

+

谦逊使我可以与团队中以及团队之间有着良好的沟通,好学让我不断逃出舒适区,让自己不断的学习进步。

+

有着个人荣誉感与集体荣誉感,让我对工作和团队认真负责。

+

抗压能力强,让领导放心把最紧急最重要的工作交给我做,加班也毫无怨言。

+

工作久了让我总结了一些工作中常见的问题,我会讲这些分享给他人,让大家一同进步,将问题扼杀在摇篮里。

+

愿意组织活动参加活动,喜欢阅读,跑步,骑行,篮球。

+ +
+ + + +
+ + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2013/06/index.html b/archives/2013/06/index.html new file mode 100644 index 0000000000..fee75b7cbe --- /dev/null +++ b/archives/2013/06/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2013 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2013/index.html b/archives/2013/index.html new file mode 100644 index 0000000000..c6d1a527c3 --- /dev/null +++ b/archives/2013/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2013 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2015/12/index.html b/archives/2015/12/index.html new file mode 100644 index 0000000000..ae9e22c7e5 --- /dev/null +++ b/archives/2015/12/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2015 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2015/index.html b/archives/2015/index.html new file mode 100644 index 0000000000..2687f76cb9 --- /dev/null +++ b/archives/2015/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2015 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2016/10/index.html b/archives/2016/10/index.html new file mode 100644 index 0000000000..769971296a --- /dev/null +++ b/archives/2016/10/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2016 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2016/index.html b/archives/2016/index.html new file mode 100644 index 0000000000..4e0b8b4191 --- /dev/null +++ b/archives/2016/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2016 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2017/08/index.html b/archives/2017/08/index.html new file mode 100644 index 0000000000..80a499e9c4 --- /dev/null +++ b/archives/2017/08/index.html @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2017 +
+ + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2017/10/index.html b/archives/2017/10/index.html new file mode 100644 index 0000000000..f832191da4 --- /dev/null +++ b/archives/2017/10/index.html @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2017 +
+ + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2017/12/index.html b/archives/2017/12/index.html new file mode 100644 index 0000000000..78be23b829 --- /dev/null +++ b/archives/2017/12/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2017 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2017/index.html b/archives/2017/index.html new file mode 100644 index 0000000000..532548890e --- /dev/null +++ b/archives/2017/index.html @@ -0,0 +1,618 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2017 +
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2019/12/index.html b/archives/2019/12/index.html new file mode 100644 index 0000000000..d63dddaa3c --- /dev/null +++ b/archives/2019/12/index.html @@ -0,0 +1,558 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2019 +
+ + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2019/index.html b/archives/2019/index.html new file mode 100644 index 0000000000..da0ccf1555 --- /dev/null +++ b/archives/2019/index.html @@ -0,0 +1,558 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2019 +
+ + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/01/index.html b/archives/2020/01/index.html new file mode 100644 index 0000000000..aa63917caa --- /dev/null +++ b/archives/2020/01/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/01/page/2/index.html b/archives/2020/01/page/2/index.html new file mode 100644 index 0000000000..97cd2aeca1 --- /dev/null +++ b/archives/2020/01/page/2/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/01/page/3/index.html b/archives/2020/01/page/3/index.html new file mode 100644 index 0000000000..3e23cb756a --- /dev/null +++ b/archives/2020/01/page/3/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/01/page/4/index.html b/archives/2020/01/page/4/index.html new file mode 100644 index 0000000000..0d87e4159b --- /dev/null +++ b/archives/2020/01/page/4/index.html @@ -0,0 +1,601 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/02/index.html b/archives/2020/02/index.html new file mode 100644 index 0000000000..f8bb9fce30 --- /dev/null +++ b/archives/2020/02/index.html @@ -0,0 +1,598 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/03/index.html b/archives/2020/03/index.html new file mode 100644 index 0000000000..8a1b0a422d --- /dev/null +++ b/archives/2020/03/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/03/page/2/index.html b/archives/2020/03/page/2/index.html new file mode 100644 index 0000000000..b6b48879a0 --- /dev/null +++ b/archives/2020/03/page/2/index.html @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/04/index.html b/archives/2020/04/index.html new file mode 100644 index 0000000000..78c3d1a66b --- /dev/null +++ b/archives/2020/04/index.html @@ -0,0 +1,578 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/05/index.html b/archives/2020/05/index.html new file mode 100644 index 0000000000..63401b10b5 --- /dev/null +++ b/archives/2020/05/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/07/index.html b/archives/2020/07/index.html new file mode 100644 index 0000000000..29b157b1ff --- /dev/null +++ b/archives/2020/07/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/08/index.html b/archives/2020/08/index.html new file mode 100644 index 0000000000..228bf6cefe --- /dev/null +++ b/archives/2020/08/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/11/index.html b/archives/2020/11/index.html new file mode 100644 index 0000000000..4ea0c1d3da --- /dev/null +++ b/archives/2020/11/index.html @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/index.html b/archives/2020/index.html new file mode 100644 index 0000000000..e8a2ed070c --- /dev/null +++ b/archives/2020/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/page/2/index.html b/archives/2020/page/2/index.html new file mode 100644 index 0000000000..ef85ca6881 --- /dev/null +++ b/archives/2020/page/2/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/page/3/index.html b/archives/2020/page/3/index.html new file mode 100644 index 0000000000..7233c660d8 --- /dev/null +++ b/archives/2020/page/3/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/page/4/index.html b/archives/2020/page/4/index.html new file mode 100644 index 0000000000..1c54563e4f --- /dev/null +++ b/archives/2020/page/4/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/page/5/index.html b/archives/2020/page/5/index.html new file mode 100644 index 0000000000..1f69326a75 --- /dev/null +++ b/archives/2020/page/5/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/page/6/index.html b/archives/2020/page/6/index.html new file mode 100644 index 0000000000..3f2ec49045 --- /dev/null +++ b/archives/2020/page/6/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/page/7/index.html b/archives/2020/page/7/index.html new file mode 100644 index 0000000000..dfb6b72b76 --- /dev/null +++ b/archives/2020/page/7/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/page/8/index.html b/archives/2020/page/8/index.html new file mode 100644 index 0000000000..49df4672fb --- /dev/null +++ b/archives/2020/page/8/index.html @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2021/05/index.html b/archives/2021/05/index.html new file mode 100644 index 0000000000..e4dfc7112e --- /dev/null +++ b/archives/2021/05/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2021 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2021/index.html b/archives/2021/index.html new file mode 100644 index 0000000000..1a699400f6 --- /dev/null +++ b/archives/2021/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2021 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/03/index.html b/archives/2022/03/index.html new file mode 100644 index 0000000000..22377903e7 --- /dev/null +++ b/archives/2022/03/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2022 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/06/index.html b/archives/2022/06/index.html new file mode 100644 index 0000000000..1e45e67891 --- /dev/null +++ b/archives/2022/06/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/06/page/2/index.html b/archives/2022/06/page/2/index.html new file mode 100644 index 0000000000..e240612563 --- /dev/null +++ b/archives/2022/06/page/2/index.html @@ -0,0 +1,621 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/08/index.html b/archives/2022/08/index.html new file mode 100644 index 0000000000..725bd82b2f --- /dev/null +++ b/archives/2022/08/index.html @@ -0,0 +1,638 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/index.html b/archives/2022/index.html new file mode 100644 index 0000000000..2d123ba24c --- /dev/null +++ b/archives/2022/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/page/2/index.html b/archives/2022/page/2/index.html new file mode 100644 index 0000000000..fd3b820c70 --- /dev/null +++ b/archives/2022/page/2/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/page/3/index.html b/archives/2022/page/3/index.html new file mode 100644 index 0000000000..6438d367c7 --- /dev/null +++ b/archives/2022/page/3/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/page/4/index.html b/archives/2022/page/4/index.html new file mode 100644 index 0000000000..7169abd504 --- /dev/null +++ b/archives/2022/page/4/index.html @@ -0,0 +1,461 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2022 +
+ + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/09/index.html b/archives/2023/09/index.html new file mode 100644 index 0000000000..920d62ad8e --- /dev/null +++ b/archives/2023/09/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2023 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/index.html b/archives/2023/index.html new file mode 100644 index 0000000000..9a3b22ad45 --- /dev/null +++ b/archives/2023/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2023 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/index.html b/archives/index.html new file mode 100644 index 0000000000..9208db775b --- /dev/null +++ b/archives/index.html @@ -0,0 +1,644 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2023 +
+ + + + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/10/index.html b/archives/page/10/index.html new file mode 100644 index 0000000000..ecf11d8843 --- /dev/null +++ b/archives/page/10/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/11/index.html b/archives/page/11/index.html new file mode 100644 index 0000000000..31117a6eee --- /dev/null +++ b/archives/page/11/index.html @@ -0,0 +1,644 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + +
+ 2019 +
+ + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/12/index.html b/archives/page/12/index.html new file mode 100644 index 0000000000..7a6734eb66 --- /dev/null +++ b/archives/page/12/index.html @@ -0,0 +1,644 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2019 +
+ + + + + + + + + + +
+ 2017 +
+ + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/13/index.html b/archives/page/13/index.html new file mode 100644 index 0000000000..1993a48566 --- /dev/null +++ b/archives/page/13/index.html @@ -0,0 +1,610 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2017 +
+ + + + + + + + +
+ 2016 +
+ + +
+ 2015 +
+ + +
+ 2013 +
+ + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/2/index.html b/archives/page/2/index.html new file mode 100644 index 0000000000..326792da56 --- /dev/null +++ b/archives/page/2/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/3/index.html b/archives/page/3/index.html new file mode 100644 index 0000000000..8971d05125 --- /dev/null +++ b/archives/page/3/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/4/index.html b/archives/page/4/index.html new file mode 100644 index 0000000000..bef8036a1b --- /dev/null +++ b/archives/page/4/index.html @@ -0,0 +1,647 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2022 +
+ + + + + + +
+ 2021 +
+ + + + +
+ 2020 +
+ + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/5/index.html b/archives/page/5/index.html new file mode 100644 index 0000000000..bacae97724 --- /dev/null +++ b/archives/page/5/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/6/index.html b/archives/page/6/index.html new file mode 100644 index 0000000000..d22f850157 --- /dev/null +++ b/archives/page/6/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/7/index.html b/archives/page/7/index.html new file mode 100644 index 0000000000..f0edeca5f7 --- /dev/null +++ b/archives/page/7/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/8/index.html b/archives/page/8/index.html new file mode 100644 index 0000000000..786464ab54 --- /dev/null +++ b/archives/page/8/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/9/index.html b/archives/page/9/index.html new file mode 100644 index 0000000000..4c7dac5733 --- /dev/null +++ b/archives/page/9/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 归档 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + 很好! 目前共计 128 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Database/Postgresql/index.html b/categories/Database/Postgresql/index.html new file mode 100644 index 0000000000..b0de3a3875 --- /dev/null +++ b/categories/Database/Postgresql/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: Postgresql | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Postgresql + 分类 +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Database/index.html b/categories/Database/index.html new file mode 100644 index 0000000000..7921b3ace9 --- /dev/null +++ b/categories/Database/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: Database | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Database + 分类 +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Docker/index.html b/categories/Docker/index.html new file mode 100644 index 0000000000..c6951529e6 --- /dev/null +++ b/categories/Docker/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: Docker | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Docker + 分类 +

+
+ + +
+ 2022 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Infra/Docker/index.html b/categories/Infra/Docker/index.html new file mode 100644 index 0000000000..5902a30334 --- /dev/null +++ b/categories/Infra/Docker/index.html @@ -0,0 +1,538 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: Docker | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Docker + 分类 +

+
+ + +
+ 2022 +
+ + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/Infra/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" "b/categories/Infra/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" new file mode 100644 index 0000000000..40ed418ee1 --- /dev/null +++ "b/categories/Infra/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: Nginx | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Nginx + 分类 +

+
+ + +
+ 2022 +
+ + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/Infra/Web\346\234\215\345\212\241\345\231\250/Squid/index.html" "b/categories/Infra/Web\346\234\215\345\212\241\345\231\250/Squid/index.html" new file mode 100644 index 0000000000..2ae617c435 --- /dev/null +++ "b/categories/Infra/Web\346\234\215\345\212\241\345\231\250/Squid/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: Squid | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Squid + 分类 +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/Infra/Web\346\234\215\345\212\241\345\231\250/index.html" "b/categories/Infra/Web\346\234\215\345\212\241\345\231\250/index.html" new file mode 100644 index 0000000000..0ab1bb623d --- /dev/null +++ "b/categories/Infra/Web\346\234\215\345\212\241\345\231\250/index.html" @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: Web服务器 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Web服务器 + 分类 +

+
+ + +
+ 2022 +
+ + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Infra/index.html b/categories/Infra/index.html new file mode 100644 index 0000000000..48995e2aa5 --- /dev/null +++ b/categories/Infra/index.html @@ -0,0 +1,618 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: Infra | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Infra + 分类 +

+
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Infrastructure/index.html b/categories/Infrastructure/index.html new file mode 100644 index 0000000000..885ef10465 --- /dev/null +++ b/categories/Infrastructure/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: Infrastructure | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Infrastructure + 分类 +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Linux/Oracle/Docker/index.html b/categories/Linux/Oracle/Docker/index.html new file mode 100644 index 0000000000..0ca32b695f --- /dev/null +++ b/categories/Linux/Oracle/Docker/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: Docker | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Docker + 分类 +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Linux/Oracle/index.html b/categories/Linux/Oracle/index.html new file mode 100644 index 0000000000..4e2ef43d35 --- /dev/null +++ b/categories/Linux/Oracle/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: Oracle | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Oracle + 分类 +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Linux/RPM/index.html b/categories/Linux/RPM/index.html new file mode 100644 index 0000000000..78fcfcc2f7 --- /dev/null +++ b/categories/Linux/RPM/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: RPM | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

RPM + 分类 +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Linux/index.html b/categories/Linux/index.html new file mode 100644 index 0000000000..2ecaf4c517 --- /dev/null +++ b/categories/Linux/index.html @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: Linux | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Linux + 分类 +

+
+ + +
+ 2022 +
+ + + + +
+ 2020 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/NodeJS/Npm/index.html b/categories/NodeJS/Npm/index.html new file mode 100644 index 0000000000..04c5995855 --- /dev/null +++ b/categories/NodeJS/Npm/index.html @@ -0,0 +1,481 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: Npm | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Npm + 分类 +

+
+ + +
+ 2020 +
+ + +
+ 2017 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/NodeJS/index.html b/categories/NodeJS/index.html new file mode 100644 index 0000000000..4598a26118 --- /dev/null +++ b/categories/NodeJS/index.html @@ -0,0 +1,541 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: NodeJS | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

NodeJS + 分类 +

+
+ + +
+ 2020 +
+ + + + + + + + +
+ 2017 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/OracleLinux/index.html b/categories/OracleLinux/index.html new file mode 100644 index 0000000000..2cd97eb82e --- /dev/null +++ b/categories/OracleLinux/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: OracleLinux | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

OracleLinux + 分类 +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" "b/categories/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" new file mode 100644 index 0000000000..00373d3e44 --- /dev/null +++ "b/categories/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: Nginx | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Nginx + 分类 +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/Web\346\234\215\345\212\241\345\231\250/index.html" "b/categories/Web\346\234\215\345\212\241\345\231\250/index.html" new file mode 100644 index 0000000000..8b1154fb4d --- /dev/null +++ "b/categories/Web\346\234\215\345\212\241\345\231\250/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: Web服务器 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Web服务器 + 分类 +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/index.html b/categories/index.html new file mode 100644 index 0000000000..4b3bc7ecc6 --- /dev/null +++ b/categories/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类 | 李旭光的成长博客 + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + + + + + + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/linux/index.html b/categories/linux/index.html new file mode 100644 index 0000000000..5a364534b8 --- /dev/null +++ b/categories/linux/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: linux | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

linux + 分类 +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/linux/systemctl/index.html b/categories/linux/systemctl/index.html new file mode 100644 index 0000000000..4747bf075a --- /dev/null +++ b/categories/linux/systemctl/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: systemctl | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

systemctl + 分类 +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/Webpack/index.html" "b/categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/Webpack/index.html" new file mode 100644 index 0000000000..f3628c0451 --- /dev/null +++ "b/categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/Webpack/index.html" @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: Webpack | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Webpack + 分类 +

+
+ + +
+ 2020 +
+ + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/index.html" "b/categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/index.html" new file mode 100644 index 0000000000..ad992271a5 --- /dev/null +++ "b/categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/index.html" @@ -0,0 +1,541 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 前端工程化 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端工程化 + 分类 +

+
+ + +
+ 2020 +
+ + + + + + + + +
+ 2019 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\211\215\347\253\257\345\274\200\345\217\221\350\247\204\350\214\203/index.html" "b/categories/\345\211\215\347\253\257\345\274\200\345\217\221\350\247\204\350\214\203/index.html" new file mode 100644 index 0000000000..f0d32b87c5 --- /dev/null +++ "b/categories/\345\211\215\347\253\257\345\274\200\345\217\221\350\247\204\350\214\203/index.html" @@ -0,0 +1,578 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 前端开发规范 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端开发规范 + 分类 +

+
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS/index.html" "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS/index.html" new file mode 100644 index 0000000000..b2868e91fd --- /dev/null +++ "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: CSS | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

CSS + 分类 +

+
+ + +
+ 2013 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS3/index.html" "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS3/index.html" new file mode 100644 index 0000000000..3c555274b1 --- /dev/null +++ "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS3/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: CSS3 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

CSS3 + 分类 +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\211\215\347\253\257\346\212\200\346\234\257/index.html" "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/index.html" new file mode 100644 index 0000000000..5fba807512 --- /dev/null +++ "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/index.html" @@ -0,0 +1,647 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 前端技术 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端技术 + 分类 +

+
+ + +
+ 2022 +
+ + + + + + +
+ 2021 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/2/index.html" "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/2/index.html" new file mode 100644 index 0000000000..6d7d8634b6 --- /dev/null +++ "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/2/index.html" @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 前端技术 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端技术 + 分类 +

+
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/3/index.html" "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/3/index.html" new file mode 100644 index 0000000000..069c44e164 --- /dev/null +++ "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/3/index.html" @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 前端技术 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端技术 + 分类 +

+
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/4/index.html" "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/4/index.html" new file mode 100644 index 0000000000..14fad8676f --- /dev/null +++ "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/4/index.html" @@ -0,0 +1,644 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 前端技术 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端技术 + 分类 +

+
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + +
+ 2019 +
+ + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/5/index.html" "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/5/index.html" new file mode 100644 index 0000000000..a2de2eeb2a --- /dev/null +++ "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/5/index.html" @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 前端技术 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端技术 + 分类 +

+
+ + +
+ 2017 +
+ + + + +
+ 2013 +
+ + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\205\274\345\256\271\346\200\247/index.html" "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\205\274\345\256\271\346\200\247/index.html" new file mode 100644 index 0000000000..5c93791844 --- /dev/null +++ "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\205\274\345\256\271\346\200\247/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 兼容性 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

兼容性 + 分类 +

+
+ + +
+ 2017 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/index.html" "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/index.html" new file mode 100644 index 0000000000..0323534f14 --- /dev/null +++ "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/index.html" @@ -0,0 +1,644 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 前端常见知识点整理 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端常见知识点整理 + 分类 +

+
+ + +
+ 2022 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/2/index.html" "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/2/index.html" new file mode 100644 index 0000000000..47ee12456f --- /dev/null +++ "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/2/index.html" @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 前端常见知识点整理 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端常见知识点整理 + 分类 +

+
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/3/index.html" "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/3/index.html" new file mode 100644 index 0000000000..832092428c --- /dev/null +++ "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/3/index.html" @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 前端常见知识点整理 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端常见知识点整理 + 分类 +

+
+ + +
+ 2020 +
+ + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\351\227\256\351\242\230/index.html" "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\351\227\256\351\242\230/index.html" new file mode 100644 index 0000000000..7118e3477e --- /dev/null +++ "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\351\227\256\351\242\230/index.html" @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 前端问题 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端问题 + 分类 +

+
+ + +
+ 2020 +
+ + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\276\256\344\277\241/index.html" "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\276\256\344\277\241/index.html" new file mode 100644 index 0000000000..178d775921 --- /dev/null +++ "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\276\256\344\277\241/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 微信 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

微信 + 分类 +

+
+ + +
+ 2019 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\350\256\241\347\256\227\346\234\272\351\200\232\350\257\206/index.html" "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\350\256\241\347\256\227\346\234\272\351\200\232\350\257\206/index.html" new file mode 100644 index 0000000000..938f49e0cf --- /dev/null +++ "b/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\350\256\241\347\256\227\346\234\272\351\200\232\350\257\206/index.html" @@ -0,0 +1,524 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 计算机通识 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

计算机通识 + 分类 +

+
+ + +
+ 2022 +
+ + + + +
+ 2021 +
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\211\215\347\253\257\346\241\206\346\236\266/React/index.html" "b/categories/\345\211\215\347\253\257\346\241\206\346\236\266/React/index.html" new file mode 100644 index 0000000000..990d7488e3 --- /dev/null +++ "b/categories/\345\211\215\347\253\257\346\241\206\346\236\266/React/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: React | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

React + 分类 +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\211\215\347\253\257\346\241\206\346\236\266/Vue/index.html" "b/categories/\345\211\215\347\253\257\346\241\206\346\236\266/Vue/index.html" new file mode 100644 index 0000000000..5c48613088 --- /dev/null +++ "b/categories/\345\211\215\347\253\257\346\241\206\346\236\266/Vue/index.html" @@ -0,0 +1,547 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: Vue | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Vue + 分类 +

+
+ + +
+ 2021 +
+ + +
+ 2020 +
+ + + + +
+ 2019 +
+ + +
+ 2017 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\211\215\347\253\257\346\241\206\346\236\266/index.html" "b/categories/\345\211\215\347\253\257\346\241\206\346\236\266/index.html" new file mode 100644 index 0000000000..a9fd5401d9 --- /dev/null +++ "b/categories/\345\211\215\347\253\257\346\241\206\346\236\266/index.html" @@ -0,0 +1,567 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 前端框架 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端框架 + 分类 +

+
+ + +
+ 2021 +
+ + +
+ 2020 +
+ + + + + + +
+ 2019 +
+ + +
+ 2017 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\220\216\347\253\257\345\274\200\345\217\221/Go\350\257\255\350\250\200/index.html" "b/categories/\345\220\216\347\253\257\345\274\200\345\217\221/Go\350\257\255\350\250\200/index.html" new file mode 100644 index 0000000000..112313cbbe --- /dev/null +++ "b/categories/\345\220\216\347\253\257\345\274\200\345\217\221/Go\350\257\255\350\250\200/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: Go语言 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Go语言 + 分类 +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\220\216\347\253\257\345\274\200\345\217\221/index.html" "b/categories/\345\220\216\347\253\257\345\274\200\345\217\221/index.html" new file mode 100644 index 0000000000..8d027c283a --- /dev/null +++ "b/categories/\345\220\216\347\253\257\345\274\200\345\217\221/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 后端开发 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

后端开发 + 分类 +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\244\247\345\211\215\347\253\257/index.html" "b/categories/\345\244\247\345\211\215\347\253\257/index.html" new file mode 100644 index 0000000000..9a092233d0 --- /dev/null +++ "b/categories/\345\244\247\345\211\215\347\253\257/index.html" @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 大前端 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

大前端 + 分类 +

+
+ + +
+ 2020 +
+ + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\244\247\345\211\215\347\253\257/\345\205\250\346\240\210/index.html" "b/categories/\345\244\247\345\211\215\347\253\257/\345\205\250\346\240\210/index.html" new file mode 100644 index 0000000000..ee3da14735 --- /dev/null +++ "b/categories/\345\244\247\345\211\215\347\253\257/\345\205\250\346\240\210/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 全栈 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

全栈 + 分类 +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\244\247\345\211\215\347\253\257/\345\256\242\346\210\267\347\253\257\346\212\200\346\234\257/index.html" "b/categories/\345\244\247\345\211\215\347\253\257/\345\256\242\346\210\267\347\253\257\346\212\200\346\234\257/index.html" new file mode 100644 index 0000000000..fc8f340554 --- /dev/null +++ "b/categories/\345\244\247\345\211\215\347\253\257/\345\256\242\346\210\267\347\253\257\346\212\200\346\234\257/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 客户端技术 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

客户端技术 + 分类 +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\244\247\345\211\215\347\253\257/\347\247\273\345\212\250\345\274\200\345\217\221/index.html" "b/categories/\345\244\247\345\211\215\347\253\257/\347\247\273\345\212\250\345\274\200\345\217\221/index.html" new file mode 100644 index 0000000000..f6e9c77b0d --- /dev/null +++ "b/categories/\345\244\247\345\211\215\347\253\257/\347\247\273\345\212\250\345\274\200\345\217\221/index.html" @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 移动开发 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

移动开发 + 分类 +

+
+ + +
+ 2020 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\346\225\210\347\216\207\345\267\245\345\205\267/Labrary/index.html" "b/categories/\346\225\210\347\216\207\345\267\245\345\205\267/Labrary/index.html" new file mode 100644 index 0000000000..289ddf17ea --- /dev/null +++ "b/categories/\346\225\210\347\216\207\345\267\245\345\205\267/Labrary/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: Labrary | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Labrary + 分类 +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\346\225\210\347\216\207\345\267\245\345\205\267/VSCode\346\217\222\344\273\266/index.html" "b/categories/\346\225\210\347\216\207\345\267\245\345\205\267/VSCode\346\217\222\344\273\266/index.html" new file mode 100644 index 0000000000..f00c713145 --- /dev/null +++ "b/categories/\346\225\210\347\216\207\345\267\245\345\205\267/VSCode\346\217\222\344\273\266/index.html" @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: VSCode插件 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

VSCode插件 + 分类 +

+
+ + +
+ 2020 +
+ + +
+ 2017 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\346\225\210\347\216\207\345\267\245\345\205\267/index.html" "b/categories/\346\225\210\347\216\207\345\267\245\345\205\267/index.html" new file mode 100644 index 0000000000..08a76b7cb2 --- /dev/null +++ "b/categories/\346\225\210\347\216\207\345\267\245\345\205\267/index.html" @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 效率工具 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

效率工具 + 分类 +

+
+ + +
+ 2020 +
+ + + + +
+ 2017 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\346\235\202\344\270\203\346\235\202\345\205\253/index.html" "b/categories/\346\235\202\344\270\203\346\235\202\345\205\253/index.html" new file mode 100644 index 0000000000..94def75d8a --- /dev/null +++ "b/categories/\346\235\202\344\270\203\346\235\202\345\205\253/index.html" @@ -0,0 +1,567 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 杂七杂八 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

杂七杂八 + 分类 +

+
+ + +
+ 2022 +
+ + +
+ 2020 +
+ + + + +
+ 2019 +
+ + +
+ 2017 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\346\235\202\344\270\203\346\235\202\345\205\253/\345\215\232\345\256\242\346\212\200\345\267\247/index.html" "b/categories/\346\235\202\344\270\203\346\235\202\345\205\253/\345\215\232\345\256\242\346\212\200\345\267\247/index.html" new file mode 100644 index 0000000000..6de977c905 --- /dev/null +++ "b/categories/\346\235\202\344\270\203\346\235\202\345\205\253/\345\215\232\345\256\242\346\212\200\345\267\247/index.html" @@ -0,0 +1,547 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 博客技巧 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

博客技巧 + 分类 +

+
+ + +
+ 2022 +
+ + +
+ 2020 +
+ + +
+ 2019 +
+ + +
+ 2017 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\346\236\266\346\236\204/index.html" "b/categories/\346\236\266\346\236\204/index.html" new file mode 100644 index 0000000000..a0f4f761f5 --- /dev/null +++ "b/categories/\346\236\266\346\236\204/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 架构 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

架构 + 分类 +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\346\272\220\347\240\201\345\216\237\347\220\206/index.html" "b/categories/\346\272\220\347\240\201\345\216\237\347\220\206/index.html" new file mode 100644 index 0000000000..8059cd183d --- /dev/null +++ "b/categories/\346\272\220\347\240\201\345\216\237\347\220\206/index.html" @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 源码原理 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

源码原理 + 分类 +

+
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\346\272\220\347\240\201\345\216\237\347\220\206/page/2/index.html" "b/categories/\346\272\220\347\240\201\345\216\237\347\220\206/page/2/index.html" new file mode 100644 index 0000000000..2f6600ef19 --- /dev/null +++ "b/categories/\346\272\220\347\240\201\345\216\237\347\220\206/page/2/index.html" @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 源码原理 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

源码原理 + 分类 +

+
+ + +
+ 2020 +
+ + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/index.html" "b/categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/index.html" new file mode 100644 index 0000000000..f0d0ae6d45 --- /dev/null +++ "b/categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/index.html" @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 自己动手实现系列 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

自己动手实现系列 + 分类 +

+
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/page/2/index.html" "b/categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/page/2/index.html" new file mode 100644 index 0000000000..e950f2c3b1 --- /dev/null +++ "b/categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/page/2/index.html" @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 自己动手实现系列 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

自己动手实现系列 + 分类 +

+
+ + +
+ 2020 +
+ + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\350\207\252\346\210\221\346\217\220\345\215\207/index.html" "b/categories/\350\207\252\346\210\221\346\217\220\345\215\207/index.html" new file mode 100644 index 0000000000..ae475162fb --- /dev/null +++ "b/categories/\350\207\252\346\210\221\346\217\220\345\215\207/index.html" @@ -0,0 +1,656 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 自我提升 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

自我提升 + 分类 +

+
+ + +
+ 2023 +
+ + +
+ 2020 +
+ + + + + + +
+ 2019 +
+ + + + +
+ 2017 +
+ + +
+ 2016 +
+ + +
+ 2015 +
+ + +
+ 2013 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\344\272\272\347\224\237\346\200\235\350\200\203/index.html" "b/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\344\272\272\347\224\237\346\200\235\350\200\203/index.html" new file mode 100644 index 0000000000..ffd04fa93b --- /dev/null +++ "b/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\344\272\272\347\224\237\346\200\235\350\200\203/index.html" @@ -0,0 +1,481 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 人生思考 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

人生思考 + 分类 +

+
+ + +
+ 2023 +
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\346\235\202\350\256\260\351\232\217\346\204\237/index.html" "b/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\346\235\202\350\256\260\351\232\217\346\204\237/index.html" new file mode 100644 index 0000000000..974fb8aa0a --- /dev/null +++ "b/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\346\235\202\350\256\260\351\232\217\346\204\237/index.html" @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 杂记随感 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

杂记随感 + 分类 +

+
+ + +
+ 2020 +
+ + + + +
+ 2019 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\350\257\273\344\271\246\347\254\224\350\256\260/index.html" "b/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\350\257\273\344\271\246\347\254\224\350\256\260/index.html" new file mode 100644 index 0000000000..7fa6eb94f4 --- /dev/null +++ "b/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\350\257\273\344\271\246\347\254\224\350\256\260/index.html" @@ -0,0 +1,527 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 读书笔记 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

读书笔记 + 分类 +

+
+ + +
+ 2017 +
+ + +
+ 2016 +
+ + +
+ 2015 +
+ + +
+ 2013 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\350\231\232\346\213\237\345\214\226/Docker-Compose/index.html" "b/categories/\350\231\232\346\213\237\345\214\226/Docker-Compose/index.html" new file mode 100644 index 0000000000..65bd02272d --- /dev/null +++ "b/categories/\350\231\232\346\213\237\345\214\226/Docker-Compose/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: Docker Compose | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Docker Compose + 分类 +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\350\231\232\346\213\237\345\214\226/index.html" "b/categories/\350\231\232\346\213\237\345\214\226/index.html" new file mode 100644 index 0000000000..7b1ac14a7c --- /dev/null +++ "b/categories/\350\231\232\346\213\237\345\214\226/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类: 虚拟化 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

虚拟化 + 分类 +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/css/main.css b/css/main.css new file mode 100644 index 0000000000..ff4757a341 --- /dev/null +++ b/css/main.css @@ -0,0 +1,2853 @@ +:root { + --body-bg-color: #eee; + --content-bg-color: #fff; + --card-bg-color: #f5f5f5; + --text-color: #555; + --blockquote-color: #666; + --link-color: #555; + --link-hover-color: #222; + --brand-color: #fff; + --brand-hover-color: #fff; + --table-row-odd-bg-color: #f9f9f9; + --table-row-hover-bg-color: #f5f5f5; + --menu-item-bg-color: #f5f5f5; + --btn-default-bg: #fff; + --btn-default-color: #555; + --btn-default-border-color: #555; + --btn-default-hover-bg: #222; + --btn-default-hover-color: #fff; + --btn-default-hover-border-color: #222; +} +@media (prefers-color-scheme: dark) { + :root { + --body-bg-color: #282828; + --content-bg-color: #333; + --card-bg-color: #555; + --text-color: #ccc; + --blockquote-color: #bbb; + --link-color: #ccc; + --link-hover-color: #eee; + --brand-color: #ddd; + --brand-hover-color: #ddd; + --table-row-odd-bg-color: #282828; + --table-row-hover-bg-color: #363636; + --menu-item-bg-color: #555; + --btn-default-bg: #222; + --btn-default-color: #ccc; + --btn-default-border-color: #555; + --btn-default-hover-bg: #666; + --btn-default-hover-color: #ccc; + --btn-default-hover-border-color: #666; + } + img { + opacity: 0.75; + } + img:hover { + opacity: 0.9; + } +} +html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} +body { + margin: 0; +} +main { + display: block; +} +h1 { + font-size: 2em; + margin: 0.67em 0; +} +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} +a { + background: transparent; +} +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} +b, +strong { + font-weight: bolder; +} +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} +small { + font-size: 80%; +} +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} +sub { + bottom: -0.25em; +} +sup { + top: -0.5em; +} +img { + border-style: none; +} +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} +button, +input { +/* 1 */ + overflow: visible; +} +button, +select { +/* 1 */ + text-transform: none; +} +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; +} +button::-moz-focus-inner, +[type='button']::-moz-focus-inner, +[type='reset']::-moz-focus-inner, +[type='submit']::-moz-focus-inner { + border-style: none; + padding: 0; +} +button:-moz-focusring, +[type='button']:-moz-focusring, +[type='reset']:-moz-focusring, +[type='submit']:-moz-focusring { + outline: 1px dotted ButtonText; +} +fieldset { + padding: 0.35em 0.75em 0.625em; +} +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} +progress { + vertical-align: baseline; +} +textarea { + overflow: auto; +} +[type='checkbox'], +[type='radio'] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} +[type='number']::-webkit-inner-spin-button, +[type='number']::-webkit-outer-spin-button { + height: auto; +} +[type='search'] { + outline-offset: -2px; /* 2 */ + -webkit-appearance: textfield; /* 1 */ +} +[type='search']::-webkit-search-decoration { + -webkit-appearance: none; +} +::-webkit-file-upload-button { + font: inherit; /* 2 */ + -webkit-appearance: button; /* 1 */ +} +details { + display: block; +} +summary { + display: list-item; +} +template { + display: none; +} +[hidden] { + display: none; +} +::selection { + background: #262a30; + color: #eee; +} +html, +body { + height: 100%; +} +body { + background: var(--body-bg-color); + color: var(--text-color); + font-family: 'Monda', "PingFang SC", "Microsoft YaHei", sans-serif; + font-size: 0.9em; + line-height: 2; +} +@media (max-width: 991px) { + body { + padding-left: 0 !important; + padding-right: 0 !important; + } +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: 'Monda', "PingFang SC", "Microsoft YaHei", sans-serif; + font-weight: bold; + line-height: 1.5; + margin: 20px 0 15px; +} +h1 { + font-size: 1.5em; +} +h2 { + font-size: 1.375em; +} +h3 { + font-size: 1.25em; +} +h4 { + font-size: 1.125em; +} +h5 { + font-size: 1em; +} +h6 { + font-size: 0.875em; +} +p { + margin: 0 0 20px 0; +} +a, +span.exturl { + border-bottom: 1px solid #999; + color: var(--link-color); + outline: 0; + text-decoration: none; + overflow-wrap: break-word; + word-wrap: break-word; + cursor: pointer; +} +a:hover, +span.exturl:hover { + border-bottom-color: var(--link-hover-color); + color: var(--link-hover-color); +} +iframe, +img, +video { + display: block; + margin-left: auto; + margin-right: auto; + max-width: 100%; +} +hr { + background-image: repeating-linear-gradient(-45deg, #ddd, #ddd 4px, transparent 4px, transparent 8px); + border: 0; + height: 3px; + margin: 40px 0; +} +blockquote { + border-left: 4px solid #ddd; + color: var(--blockquote-color); + margin: 0; + padding: 0 15px; +} +blockquote cite::before { + content: '-'; + padding: 0 5px; +} +dt { + font-weight: bold; +} +dd { + margin: 0; + padding: 0; +} +kbd { + background-color: #f5f5f5; + background-image: linear-gradient(#eee, #fff, #eee); + border: 1px solid #ccc; + border-radius: 0.2em; + box-shadow: 0.1em 0.1em 0.2em rgba(0,0,0,0.1); + color: #555; + font-family: inherit; + padding: 0.1em 0.3em; + white-space: nowrap; +} +.table-container { + overflow: auto; +} +table { + border-collapse: collapse; + border-spacing: 0; + font-size: 0.875em; + margin: 0 0 20px 0; + width: 100%; +} +tbody tr:nth-of-type(odd) { + background: var(--table-row-odd-bg-color); +} +tbody tr:hover { + background: var(--table-row-hover-bg-color); +} +caption, +th, +td { + font-weight: normal; + padding: 8px; + vertical-align: middle; +} +th, +td { + border: 1px solid #ddd; + border-bottom: 3px solid #ddd; +} +th { + font-weight: 700; + padding-bottom: 10px; +} +td { + border-bottom-width: 1px; +} +.btn { + background: var(--btn-default-bg); + border: 2px solid var(--btn-default-border-color); + border-radius: 2px; + color: var(--btn-default-color); + display: inline-block; + font-size: 0.875em; + line-height: 2; + padding: 0 20px; + text-decoration: none; + transition-property: background-color; + transition-delay: 0s; + transition-duration: 0.2s; + transition-timing-function: ease-in-out; +} +.btn:hover { + background: var(--btn-default-hover-bg); + border-color: var(--btn-default-hover-border-color); + color: var(--btn-default-hover-color); +} +.btn + .btn { + margin: 0 0 8px 8px; +} +.btn .fa-fw { + text-align: left; + width: 1.285714285714286em; +} +.toggle { + line-height: 0; +} +.toggle .toggle-line { + background: #fff; + display: inline-block; + height: 2px; + left: 0; + position: relative; + top: 0; + transition: all 0.4s; + vertical-align: top; + width: 100%; +} +.toggle .toggle-line:not(:first-child) { + margin-top: 3px; +} +.toggle.toggle-arrow .toggle-line-first { + left: 50%; + top: 2px; + transform: rotate(45deg); + width: 50%; +} +.toggle.toggle-arrow .toggle-line-middle { + left: 2px; + width: 90%; +} +.toggle.toggle-arrow .toggle-line-last { + left: 50%; + top: -2px; + transform: rotate(-45deg); + width: 50%; +} +.toggle.toggle-close .toggle-line-first { + transform: rotate(-45deg); + top: 5px; +} +.toggle.toggle-close .toggle-line-middle { + opacity: 0; +} +.toggle.toggle-close .toggle-line-last { + transform: rotate(45deg); + top: -5px; +} +.highlight, +pre { + background: #fdf6e3; + color: #586e75; + line-height: 1.6; + margin: 0 auto 20px; +} +pre, +code { + font-family: consolas, Menlo, monospace, "PingFang SC", "Microsoft YaHei"; +} +code { + background: #eee; + border-radius: 3px; + color: #555; + padding: 2px 4px; + overflow-wrap: break-word; + word-wrap: break-word; +} +.highlight *::selection { + background: #eee8d5; +} +.highlight pre { + border: 0; + margin: 0; + padding: 10px 0; +} +.highlight table { + border: 0; + margin: 0; + width: auto; +} +.highlight td { + border: 0; + padding: 0; +} +.highlight figcaption { + background: #fdf6e3; + color: #586e75; + display: flex; + font-size: 0.875em; + justify-content: space-between; + line-height: 1.2; + padding: 0.5em; +} +.highlight figcaption a { + color: #586e75; +} +.highlight figcaption a:hover { + border-bottom-color: #586e75; +} +.highlight .gutter { + -moz-user-select: none; + -ms-user-select: none; + -webkit-user-select: none; + user-select: none; +} +.highlight .gutter pre { + background: #fdf6e3; + color: #93a1a1; + padding-left: 10px; + padding-right: 10px; + text-align: right; +} +.highlight .code pre { + background: #fdf6e3; + padding-left: 10px; + width: 100%; +} +.gist table { + width: auto; +} +.gist table td { + border: 0; +} +pre { + overflow: auto; + padding: 10px; +} +pre code { + background: none; + color: #586e75; + font-size: 0.875em; + padding: 0; + text-shadow: none; +} +pre .deletion { + background: #800000; +} +pre .addition { + background: #008000; +} +pre .meta { + color: #b58900; + -moz-user-select: none; + -ms-user-select: none; + -webkit-user-select: none; + user-select: none; +} +pre .comment { + color: #93a1a1; +} +pre .variable, +pre .attribute, +pre .tag, +pre .name, +pre .regexp, +pre .ruby .constant, +pre .xml .tag .title, +pre .xml .pi, +pre .xml .doctype, +pre .html .doctype, +pre .css .id, +pre .css .class, +pre .css .pseudo { + color: #dc322f; +} +pre .number, +pre .preprocessor, +pre .built_in, +pre .builtin-name, +pre .literal, +pre .params, +pre .constant, +pre .command { + color: #cb4b16; +} +pre .ruby .class .title, +pre .css .rules .attribute, +pre .string, +pre .symbol, +pre .value, +pre .inheritance, +pre .header, +pre .ruby .symbol, +pre .xml .cdata, +pre .special, +pre .formula { + color: #859900; +} +pre .title, +pre .css .hexcolor { + color: #2aa198; +} +pre .function, +pre .python .decorator, +pre .python .title, +pre .ruby .function .title, +pre .ruby .title .keyword, +pre .perl .sub, +pre .javascript .title, +pre .coffeescript .title { + color: #268bd2; +} +pre .keyword, +pre .javascript .function { + color: #6c71c4; +} +.blockquote-center { + border-left: none; + margin: 40px 0; + padding: 0; + position: relative; + text-align: center; +} +.blockquote-center .fa { + display: block; + opacity: 0.6; + position: absolute; + width: 100%; +} +.blockquote-center .fa-quote-left { + border-top: 1px solid #ccc; + text-align: left; + top: -20px; +} +.blockquote-center .fa-quote-right { + border-bottom: 1px solid #ccc; + text-align: right; + bottom: -20px; +} +.blockquote-center p, +.blockquote-center div { + text-align: center; +} +.post-body .group-picture img { + margin: 0 auto; + padding: 0 3px; +} +.group-picture-row { + margin-bottom: 6px; + overflow: hidden; +} +.group-picture-column { + float: left; + margin-bottom: 10px; +} +.post-body .label { + color: #555; + display: inline; + padding: 0 2px; +} +.post-body .label.default { + background: #f0f0f0; +} +.post-body .label.primary { + background: #efe6f7; +} +.post-body .label.info { + background: #e5f2f8; +} +.post-body .label.success { + background: #e7f4e9; +} +.post-body .label.warning { + background: #fcf6e1; +} +.post-body .label.danger { + background: #fae8eb; +} +.post-body .tabs { + margin-bottom: 20px; +} +.post-body .tabs, +.tabs-comment { + display: block; + padding-top: 10px; + position: relative; +} +.post-body .tabs ul.nav-tabs, +.tabs-comment ul.nav-tabs { + display: flex; + flex-wrap: wrap; + margin: 0; + margin-bottom: -1px; + padding: 0; +} +@media (max-width: 413px) { + .post-body .tabs ul.nav-tabs, + .tabs-comment ul.nav-tabs { + display: block; + margin-bottom: 5px; + } +} +.post-body .tabs ul.nav-tabs li.tab, +.tabs-comment ul.nav-tabs li.tab { + border-bottom: 1px solid #ddd; + border-left: 1px solid transparent; + border-right: 1px solid transparent; + border-top: 3px solid transparent; + flex-grow: 1; + list-style-type: none; + border-radius: 0 0 0 0; +} +@media (max-width: 413px) { + .post-body .tabs ul.nav-tabs li.tab, + .tabs-comment ul.nav-tabs li.tab { + border-bottom: 1px solid transparent; + border-left: 3px solid transparent; + border-right: 1px solid transparent; + border-top: 1px solid transparent; + } +} +@media (max-width: 413px) { + .post-body .tabs ul.nav-tabs li.tab, + .tabs-comment ul.nav-tabs li.tab { + border-radius: 0; + } +} +.post-body .tabs ul.nav-tabs li.tab a, +.tabs-comment ul.nav-tabs li.tab a { + border-bottom: initial; + display: block; + line-height: 1.8; + outline: 0; + padding: 0.25em 0.75em; + text-align: center; + transition-delay: 0s; + transition-duration: 0.2s; + transition-timing-function: ease-out; +} +.post-body .tabs ul.nav-tabs li.tab a i, +.tabs-comment ul.nav-tabs li.tab a i { + width: 1.285714285714286em; +} +.post-body .tabs ul.nav-tabs li.tab.active, +.tabs-comment ul.nav-tabs li.tab.active { + border-bottom: 1px solid transparent; + border-left: 1px solid #ddd; + border-right: 1px solid #ddd; + border-top: 3px solid #fc6423; +} +@media (max-width: 413px) { + .post-body .tabs ul.nav-tabs li.tab.active, + .tabs-comment ul.nav-tabs li.tab.active { + border-bottom: 1px solid #ddd; + border-left: 3px solid #fc6423; + border-right: 1px solid #ddd; + border-top: 1px solid #ddd; + } +} +.post-body .tabs ul.nav-tabs li.tab.active a, +.tabs-comment ul.nav-tabs li.tab.active a { + color: var(--link-color); + cursor: default; +} +.post-body .tabs .tab-content .tab-pane, +.tabs-comment .tab-content .tab-pane { + border: 1px solid #ddd; + border-top: 0; + padding: 20px 20px 0 20px; + border-radius: 0; +} +.post-body .tabs .tab-content .tab-pane:not(.active), +.tabs-comment .tab-content .tab-pane:not(.active) { + display: none; +} +.post-body .tabs .tab-content .tab-pane.active, +.tabs-comment .tab-content .tab-pane.active { + display: block; +} +.post-body .tabs .tab-content .tab-pane.active:nth-of-type(1), +.tabs-comment .tab-content .tab-pane.active:nth-of-type(1) { + border-radius: 0 0 0 0; +} +@media (max-width: 413px) { + .post-body .tabs .tab-content .tab-pane.active:nth-of-type(1), + .tabs-comment .tab-content .tab-pane.active:nth-of-type(1) { + border-radius: 0; + } +} +.post-body .note { + border-radius: 3px; + margin-bottom: 20px; + padding: 1em; + position: relative; + border: 1px solid #eee; + border-left-width: 5px; +} +.post-body .note h2, +.post-body .note h3, +.post-body .note h4, +.post-body .note h5, +.post-body .note h6 { + margin-top: 0; + border-bottom: initial; + margin-bottom: 0; + padding-top: 0; +} +.post-body .note p:first-child, +.post-body .note ul:first-child, +.post-body .note ol:first-child, +.post-body .note table:first-child, +.post-body .note pre:first-child, +.post-body .note blockquote:first-child, +.post-body .note img:first-child { + margin-top: 0; +} +.post-body .note p:last-child, +.post-body .note ul:last-child, +.post-body .note ol:last-child, +.post-body .note table:last-child, +.post-body .note pre:last-child, +.post-body .note blockquote:last-child, +.post-body .note img:last-child { + margin-bottom: 0; +} +.post-body .note.default { + border-left-color: #777; +} +.post-body .note.default h2, +.post-body .note.default h3, +.post-body .note.default h4, +.post-body .note.default h5, +.post-body .note.default h6 { + color: #777; +} +.post-body .note.primary { + border-left-color: #6f42c1; +} +.post-body .note.primary h2, +.post-body .note.primary h3, +.post-body .note.primary h4, +.post-body .note.primary h5, +.post-body .note.primary h6 { + color: #6f42c1; +} +.post-body .note.info { + border-left-color: #428bca; +} +.post-body .note.info h2, +.post-body .note.info h3, +.post-body .note.info h4, +.post-body .note.info h5, +.post-body .note.info h6 { + color: #428bca; +} +.post-body .note.success { + border-left-color: #5cb85c; +} +.post-body .note.success h2, +.post-body .note.success h3, +.post-body .note.success h4, +.post-body .note.success h5, +.post-body .note.success h6 { + color: #5cb85c; +} +.post-body .note.warning { + border-left-color: #f0ad4e; +} +.post-body .note.warning h2, +.post-body .note.warning h3, +.post-body .note.warning h4, +.post-body .note.warning h5, +.post-body .note.warning h6 { + color: #f0ad4e; +} +.post-body .note.danger { + border-left-color: #d9534f; +} +.post-body .note.danger h2, +.post-body .note.danger h3, +.post-body .note.danger h4, +.post-body .note.danger h5, +.post-body .note.danger h6 { + color: #d9534f; +} +.pagination .prev, +.pagination .next, +.pagination .page-number, +.pagination .space { + display: inline-block; + margin: 0 10px; + padding: 0 11px; + position: relative; + top: -1px; +} +@media (max-width: 767px) { + .pagination .prev, + .pagination .next, + .pagination .page-number, + .pagination .space { + margin: 0 5px; + } +} +.pagination { + border-top: 1px solid #eee; + margin: 120px 0 0; + text-align: center; +} +.pagination .prev, +.pagination .next, +.pagination .page-number { + border-bottom: 0; + border-top: 1px solid #eee; + transition-property: border-color; + transition-delay: 0s; + transition-duration: 0.2s; + transition-timing-function: ease-in-out; +} +.pagination .prev:hover, +.pagination .next:hover, +.pagination .page-number:hover { + border-top-color: #222; +} +.pagination .space { + margin: 0; + padding: 0; +} +.pagination .prev { + margin-left: 0; +} +.pagination .next { + margin-right: 0; +} +.pagination .page-number.current { + background: #ccc; + border-top-color: #ccc; + color: #fff; +} +@media (max-width: 767px) { + .pagination { + border-top: none; + } + .pagination .prev, + .pagination .next, + .pagination .page-number { + border-bottom: 1px solid #eee; + border-top: 0; + margin-bottom: 10px; + padding: 0 10px; + } + .pagination .prev:hover, + .pagination .next:hover, + .pagination .page-number:hover { + border-bottom-color: #222; + } +} +.comments { + margin-top: 60px; + overflow: hidden; +} +.comment-button-group { + display: flex; + flex-wrap: wrap-reverse; + justify-content: center; + margin: 1em 0; +} +.comment-button-group .comment-button { + margin: 0.1em 0.2em; +} +.comment-button-group .comment-button.active { + background: var(--btn-default-hover-bg); + border-color: var(--btn-default-hover-border-color); + color: var(--btn-default-hover-color); +} +.comment-position { + display: none; +} +.comment-position.active { + display: block; +} +.tabs-comment { + background: var(--content-bg-color); + margin-top: 4em; + padding-top: 0; +} +.tabs-comment .comments { + border: 0; + box-shadow: none; + margin-top: 0; + padding-top: 0; +} +.container { + min-height: 100%; + position: relative; +} +.main-inner { + margin: 0 auto; + width: calc(100% - 20px); +} +@media (min-width: 1200px) { + .main-inner { + width: 1160px; + } +} +@media (min-width: 1600px) { + .main-inner { + width: 73%; + } +} +@media (max-width: 767px) { + .content-wrap { + padding: 0 20px; + } +} +.header { + background: transparent; +} +.header-inner { + margin: 0 auto; + width: calc(100% - 20px); +} +@media (min-width: 1200px) { + .header-inner { + width: 1160px; + } +} +@media (min-width: 1600px) { + .header-inner { + width: 73%; + } +} +.site-brand-container { + display: flex; + flex-shrink: 0; + padding: 0 10px; +} +.headband { + background: #222; + height: 3px; +} +.site-meta { + flex-grow: 1; + text-align: center; +} +@media (max-width: 767px) { + .site-meta { + text-align: center; + } +} +.brand { + border-bottom: none; + color: var(--brand-color); + display: inline-block; + line-height: 1.375em; + padding: 0 40px; + position: relative; +} +.brand:hover { + color: var(--brand-hover-color); +} +.site-title { + font-family: 'Monda', "PingFang SC", "Microsoft YaHei", sans-serif; + font-size: 1.375em; + font-weight: normal; + margin: 0; +} +.site-subtitle { + color: #ddd; + font-size: 0.8125em; + margin: 10px 0; +} +.use-motion .brand { + opacity: 0; +} +.use-motion .site-title, +.use-motion .site-subtitle, +.use-motion .custom-logo-image { + opacity: 0; + position: relative; + top: -10px; +} +.site-nav-toggle, +.site-nav-right { + display: none; +} +@media (max-width: 767px) { + .site-nav-toggle, + .site-nav-right { + display: flex; + flex-direction: column; + justify-content: center; + } +} +.site-nav-toggle .toggle, +.site-nav-right .toggle { + color: var(--text-color); + padding: 10px; + width: 22px; +} +.site-nav-toggle .toggle .toggle-line, +.site-nav-right .toggle .toggle-line { + background: var(--text-color); + border-radius: 1px; +} +.site-nav { + display: block; +} +@media (max-width: 767px) { + .site-nav { + clear: both; + display: none; + } +} +.site-nav.site-nav-on { + display: block; +} +.menu { + margin-top: 20px; + padding-left: 0; + text-align: center; +} +.menu-item { + display: inline-block; + list-style: none; + margin: 0 10px; +} +@media (max-width: 767px) { + .menu-item { + display: block; + margin-top: 10px; + } + .menu-item.menu-item-search { + display: none; + } +} +.menu-item a, +.menu-item span.exturl { + border-bottom: 0; + display: block; + font-size: 0.8125em; + transition-property: border-color; + transition-delay: 0s; + transition-duration: 0.2s; + transition-timing-function: ease-in-out; +} +@media (hover: none) { + .menu-item a:hover, + .menu-item span.exturl:hover { + border-bottom-color: transparent !important; + } +} +.menu-item .fa, +.menu-item .fab, +.menu-item .far, +.menu-item .fas { + margin-right: 8px; +} +.menu-item .badge { + display: inline-block; + font-weight: bold; + line-height: 1; + margin-left: 0.35em; + margin-top: 0.35em; + text-align: center; + white-space: nowrap; +} +@media (max-width: 767px) { + .menu-item .badge { + float: right; + margin-left: 0; + } +} +.menu-item-active a, +.menu .menu-item a:hover, +.menu .menu-item span.exturl:hover { + background: var(--menu-item-bg-color); +} +.use-motion .menu-item { + opacity: 0; +} +.github-corner :hover .octo-arm { + animation: octocat-wave 560ms ease-in-out; +} +.github-corner svg { + border: 0; + color: #fff; + fill: #222; + position: absolute; + right: 0; + top: 0; + z-index: 1000; +} +@media (max-width: 991px) { + .github-corner { + display: none; + } + .github-corner svg { + color: #222; + fill: #fff; + } + .github-corner .github-corner:hover .octo-arm { + animation: none; + } + .github-corner .github-corner .octo-arm { + animation: octocat-wave 560ms ease-in-out; + } +} +@-moz-keyframes octocat-wave { + 0%, 100% { + transform: rotate(0); + } + 20%, 60% { + transform: rotate(-25deg); + } + 40%, 80% { + transform: rotate(10deg); + } +} +@-webkit-keyframes octocat-wave { + 0%, 100% { + transform: rotate(0); + } + 20%, 60% { + transform: rotate(-25deg); + } + 40%, 80% { + transform: rotate(10deg); + } +} +@-o-keyframes octocat-wave { + 0%, 100% { + transform: rotate(0); + } + 20%, 60% { + transform: rotate(-25deg); + } + 40%, 80% { + transform: rotate(10deg); + } +} +@keyframes octocat-wave { + 0%, 100% { + transform: rotate(0); + } + 20%, 60% { + transform: rotate(-25deg); + } + 40%, 80% { + transform: rotate(10deg); + } +} +.sidebar { + background: #222; + bottom: 0; + box-shadow: inset 0 2px 6px #000; + position: fixed; + top: 0; +} +@media (max-width: 991px) { + .sidebar { + display: none; + } +} +.sidebar-inner { + color: #999; + padding: 18px 10px; + text-align: center; +} +.cc-license { + margin-top: 10px; + text-align: center; +} +.cc-license .cc-opacity { + border-bottom: none; + opacity: 0.7; +} +.cc-license .cc-opacity:hover { + opacity: 0.9; +} +.cc-license img { + display: inline-block; +} +.site-author-image { + border: 1px solid #eee; + display: block; + margin: 0 auto; + max-width: 120px; + padding: 2px; + border-radius: 50%; +} +.site-author-name { + color: var(--text-color); + font-weight: 600; + margin: 0; + text-align: center; +} +.site-description { + color: #999; + font-size: 0.8125em; + margin-top: 0; + text-align: center; +} +.links-of-author { + margin-top: 15px; +} +.links-of-author a, +.links-of-author span.exturl { + border-bottom-color: #555; + display: inline-block; + font-size: 0.8125em; + margin-bottom: 10px; + margin-right: 10px; + vertical-align: middle; + transition-delay: 0s; + transition-duration: 0.2s; + transition-timing-function: ease-in-out; +} +.links-of-author a::before, +.links-of-author span.exturl::before { + background: #eb53ff; + border-radius: 50%; + content: ' '; + display: inline-block; + height: 4px; + margin-right: 3px; + vertical-align: middle; + width: 4px; +} +.sidebar-button { + margin-top: 15px; +} +.sidebar-button a { + border: 1px solid #fc6423; + border-radius: 4px; + color: #fc6423; + display: inline-block; + padding: 0 15px; +} +.sidebar-button a .fa, +.sidebar-button a .fab, +.sidebar-button a .far, +.sidebar-button a .fas { + margin-right: 5px; +} +.sidebar-button a:hover { + background: #fc6423; + border: 1px solid #fc6423; + color: #fff; +} +.sidebar-button a:hover .fa, +.sidebar-button a:hover .fab, +.sidebar-button a:hover .far, +.sidebar-button a:hover .fas { + color: #fff; +} +.links-of-blogroll { + font-size: 0.8125em; + margin-top: 10px; +} +.links-of-blogroll-title { + font-size: 0.875em; + font-weight: 600; + margin-top: 0; +} +.links-of-blogroll-list { + list-style: none; + margin: 0; + padding: 0; +} +#sidebar-dimmer { + display: none; +} +@media (max-width: 767px) { + #sidebar-dimmer { + background: #000; + display: block; + height: 100%; + left: 100%; + opacity: 0; + position: fixed; + top: 0; + width: 100%; + z-index: 1100; + } + .sidebar-active + #sidebar-dimmer { + opacity: 0.7; + transform: translateX(-100%); + transition: opacity 0.5s; + } +} +.sidebar-nav { + margin: 0; + padding-bottom: 20px; + padding-left: 0; +} +.sidebar-nav li { + border-bottom: 1px solid transparent; + color: var(--text-color); + cursor: pointer; + display: inline-block; + font-size: 0.875em; +} +.sidebar-nav li.sidebar-nav-overview { + margin-left: 10px; +} +.sidebar-nav li:hover { + color: #fc6423; +} +.sidebar-nav .sidebar-nav-active { + border-bottom-color: #fc6423; + color: #fc6423; +} +.sidebar-nav .sidebar-nav-active:hover { + color: #fc6423; +} +.sidebar-panel { + display: none; + overflow-x: hidden; + overflow-y: auto; +} +.sidebar-panel-active { + display: block; +} +.sidebar-toggle { + background: #222; + bottom: 45px; + cursor: pointer; + height: 14px; + left: 30px; + padding: 5px; + position: fixed; + width: 14px; + z-index: 1300; +} +@media (max-width: 991px) { + .sidebar-toggle { + left: 20px; + opacity: 0.8; + display: none; + } +} +.sidebar-toggle:hover .toggle-line { + background: #fc6423; +} +.post-toc { + font-size: 0.875em; +} +.post-toc ol { + list-style: none; + margin: 0; + padding: 0 2px 5px 10px; + text-align: left; +} +.post-toc ol > ol { + padding-left: 0; +} +.post-toc ol a { + transition-property: all; + transition-delay: 0s; + transition-duration: 0.2s; + transition-timing-function: ease-in-out; +} +.post-toc .nav-item { + line-height: 1.8; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.post-toc .nav .nav-child { + display: block; +} +.post-toc .nav .active > .nav-child { + display: block; +} +.post-toc .nav .active-current > .nav-child { + display: block; +} +.post-toc .nav .active-current > .nav-child > .nav-item { + display: block; +} +.post-toc .nav .active > a { + border-bottom-color: #fc6423; + color: #fc6423; +} +.post-toc .nav .active-current > a { + color: #fc6423; +} +.post-toc .nav .active-current > a:hover { + color: #fc6423; +} +.site-state { + display: flex; + justify-content: center; + line-height: 1.4; + margin-top: 10px; + overflow: hidden; + text-align: center; + white-space: nowrap; +} +.site-state-item { + padding: 0 15px; +} +.site-state-item:not(:first-child) { + border-left: 1px solid #eee; +} +.site-state-item a { + border-bottom: none; +} +.site-state-item-count { + display: block; + font-size: 1em; + font-weight: 600; + text-align: center; +} +.site-state-item-name { + color: #999; + font-size: 0.8125em; +} +.footer { + color: #999; + font-size: 0.875em; + padding: 20px 0; +} +.footer.footer-fixed { + bottom: 0; + left: 0; + position: absolute; + right: 0; +} +.footer-inner { + box-sizing: border-box; + margin: 0 auto; + text-align: center; + width: calc(100% - 20px); +} +@media (min-width: 1200px) { + .footer-inner { + width: 1160px; + } +} +@media (min-width: 1600px) { + .footer-inner { + width: 73%; + } +} +.languages { + display: inline-block; + font-size: 1.125em; + position: relative; +} +.languages .lang-select-label span { + margin: 0 0.5em; +} +.languages .lang-select { + height: 100%; + left: 0; + opacity: 0; + position: absolute; + top: 0; + width: 100%; +} +.with-love { + color: #ff0000; + display: inline-block; + margin: 0 5px; + animation: iconAnimate 1.33s ease-in-out infinite; +} +.powered-by, +.theme-info { + display: inline-block; +} +@-moz-keyframes iconAnimate { + 0%, 100% { + transform: scale(1); + } + 10%, 30% { + transform: scale(0.9); + } + 20%, 40%, 60%, 80% { + transform: scale(1.1); + } + 50%, 70% { + transform: scale(1.1); + } +} +@-webkit-keyframes iconAnimate { + 0%, 100% { + transform: scale(1); + } + 10%, 30% { + transform: scale(0.9); + } + 20%, 40%, 60%, 80% { + transform: scale(1.1); + } + 50%, 70% { + transform: scale(1.1); + } +} +@-o-keyframes iconAnimate { + 0%, 100% { + transform: scale(1); + } + 10%, 30% { + transform: scale(0.9); + } + 20%, 40%, 60%, 80% { + transform: scale(1.1); + } + 50%, 70% { + transform: scale(1.1); + } +} +@keyframes iconAnimate { + 0%, 100% { + transform: scale(1); + } + 10%, 30% { + transform: scale(0.9); + } + 20%, 40%, 60%, 80% { + transform: scale(1.1); + } + 50%, 70% { + transform: scale(1.1); + } +} +.back-to-top { + font-size: 12px; + text-align: center; + transition-delay: 0s; + transition-duration: 0.2s; + transition-timing-function: ease-in-out; +} +.back-to-top { + background: #222; + bottom: -100px; + box-sizing: border-box; + color: #fff; + cursor: pointer; + left: 30px; + opacity: 0.6; + padding: 0 6px; + position: fixed; + transition-property: bottom; + z-index: 1300; + width: 24px; +} +.back-to-top span { + display: none; +} +.back-to-top:hover { + color: #fc6423; +} +.back-to-top.back-to-top-on { + bottom: 30px; +} +@media (max-width: 991px) { + .back-to-top { + left: 20px; + opacity: 0.8; + } +} +.reading-progress-bar { + background: #37c6c0; + display: block; + height: 3px; + left: 0; + position: fixed; + width: 0; + z-index: 1500; + top: 0; +} +.post-body { + font-family: 'Monda', "PingFang SC", "Microsoft YaHei", sans-serif; + overflow-wrap: break-word; + word-wrap: break-word; +} +@media (min-width: 1200px) { + .post-body { + font-size: 1.125em; + } +} +.post-body .exturl .fa { + font-size: 0.875em; + margin-left: 4px; +} +.post-body .image-caption, +.post-body .figure .caption { + color: #999; + font-size: 0.875em; + font-weight: bold; + line-height: 1; + margin: -20px auto 15px; + text-align: center; +} +.post-sticky-flag { + display: inline-block; + transform: rotate(30deg); +} +.post-button { + margin-top: 40px; + text-align: center; +} +.use-motion .post-block, +.use-motion .pagination, +.use-motion .comments { + opacity: 0; +} +.use-motion .post-header { + opacity: 0; +} +.use-motion .post-body { + opacity: 0; +} +.use-motion .collection-header { + opacity: 0; +} +.posts-collapse { + margin-left: 35px; + position: relative; +} +@media (max-width: 767px) { + .posts-collapse { + margin-left: 0px; + margin-right: 0px; + } +} +.posts-collapse .collection-title { + font-size: 1.125em; + position: relative; +} +.posts-collapse .collection-title::before { + background: #999; + border: 1px solid #fff; + border-radius: 50%; + content: ' '; + height: 10px; + left: 0; + margin-left: -6px; + margin-top: -4px; + position: absolute; + top: 50%; + width: 10px; +} +.posts-collapse .collection-year { + font-size: 1.5em; + font-weight: bold; + margin: 60px 0; + position: relative; +} +.posts-collapse .collection-year::before { + background: #bbb; + border-radius: 50%; + content: ' '; + height: 8px; + left: 0; + margin-left: -4px; + margin-top: -4px; + position: absolute; + top: 50%; + width: 8px; +} +.posts-collapse .collection-header { + display: block; + margin: 0 0 0 20px; +} +.posts-collapse .collection-header small { + color: #bbb; + margin-left: 5px; +} +.posts-collapse .post-header { + border-bottom: 1px dashed #ccc; + margin: 30px 0; + padding-left: 15px; + position: relative; + transition-property: border; + transition-delay: 0s; + transition-duration: 0.2s; + transition-timing-function: ease-in-out; +} +.posts-collapse .post-header::before { + background: #bbb; + border: 1px solid #fff; + border-radius: 50%; + content: ' '; + height: 6px; + left: 0; + margin-left: -4px; + position: absolute; + top: 0.75em; + transition-property: background; + width: 6px; + transition-delay: 0s; + transition-duration: 0.2s; + transition-timing-function: ease-in-out; +} +.posts-collapse .post-header:hover { + border-bottom-color: #666; +} +.posts-collapse .post-header:hover::before { + background: #222; +} +.posts-collapse .post-meta { + display: inline; + font-size: 0.75em; + margin-right: 10px; +} +.posts-collapse .post-title { + display: inline; +} +.posts-collapse .post-title a, +.posts-collapse .post-title span.exturl { + border-bottom: none; + color: var(--link-color); +} +.posts-collapse .post-title .fa-external-link-alt { + font-size: 0.875em; + margin-left: 5px; +} +.posts-collapse::before { + background: #f5f5f5; + content: ' '; + height: 100%; + left: 0; + margin-left: -2px; + position: absolute; + top: 1.25em; + width: 4px; +} +.post-eof { + background: #ccc; + height: 1px; + margin: 80px auto 60px; + text-align: center; + width: 8%; +} +.post-block:last-of-type .post-eof { + display: none; +} +.content { + padding-top: 40px; +} +@media (min-width: 992px) { + .post-body { + text-align: left; + } +} +@media (max-width: 991px) { + .post-body { + text-align: justify; + } +} +.post-body h1, +.post-body h2, +.post-body h3, +.post-body h4, +.post-body h5, +.post-body h6 { + padding-top: 10px; +} +.post-body h1 .header-anchor, +.post-body h2 .header-anchor, +.post-body h3 .header-anchor, +.post-body h4 .header-anchor, +.post-body h5 .header-anchor, +.post-body h6 .header-anchor { + border-bottom-style: none; + color: #ccc; + float: right; + margin-left: 10px; + visibility: hidden; +} +.post-body h1 .header-anchor:hover, +.post-body h2 .header-anchor:hover, +.post-body h3 .header-anchor:hover, +.post-body h4 .header-anchor:hover, +.post-body h5 .header-anchor:hover, +.post-body h6 .header-anchor:hover { + color: inherit; +} +.post-body h1:hover .header-anchor, +.post-body h2:hover .header-anchor, +.post-body h3:hover .header-anchor, +.post-body h4:hover .header-anchor, +.post-body h5:hover .header-anchor, +.post-body h6:hover .header-anchor { + visibility: visible; +} +.post-body iframe, +.post-body img, +.post-body video { + margin-bottom: 20px; +} +.post-body .video-container { + height: 0; + margin-bottom: 20px; + overflow: hidden; + padding-top: 75%; + position: relative; + width: 100%; +} +.post-body .video-container iframe, +.post-body .video-container object, +.post-body .video-container embed { + height: 100%; + left: 0; + margin: 0; + position: absolute; + top: 0; + width: 100%; +} +.post-gallery { + align-items: center; + display: grid; + grid-gap: 10px; + grid-template-columns: 1fr 1fr 1fr; + margin-bottom: 20px; +} +@media (max-width: 767px) { + .post-gallery { + grid-template-columns: 1fr 1fr; + } +} +.post-gallery a { + border: 0; +} +.post-gallery img { + margin: 0; +} +.posts-expand .post-header { + font-size: 1.125em; +} +.posts-expand .post-title { + font-size: 1.5em; + font-weight: normal; + margin: initial; + text-align: center; + overflow-wrap: break-word; + word-wrap: break-word; +} +.posts-expand .post-title-link { + border-bottom: none; + color: var(--link-color); + display: inline-block; + position: relative; + vertical-align: top; +} +.posts-expand .post-title-link::before { + background: var(--link-color); + bottom: 0; + content: ''; + height: 2px; + left: 0; + position: absolute; + transform: scaleX(0); + visibility: hidden; + width: 100%; + transition-delay: 0s; + transition-duration: 0.2s; + transition-timing-function: ease-in-out; +} +.posts-expand .post-title-link:hover::before { + transform: scaleX(1); + visibility: visible; +} +.posts-expand .post-title-link .fa-external-link-alt { + font-size: 0.875em; + margin-left: 5px; +} +.posts-expand .post-meta { + color: #999; + font-family: 'Monda', "PingFang SC", "Microsoft YaHei", sans-serif; + font-size: 0.75em; + margin: 3px 0 60px 0; + text-align: center; +} +.posts-expand .post-meta .post-description { + font-size: 0.875em; + margin-top: 2px; +} +.posts-expand .post-meta time { + border-bottom: 1px dashed #999; + cursor: pointer; +} +.post-meta .post-meta-item + .post-meta-item::before { + content: '|'; + margin: 0 0.5em; +} +.post-meta-divider { + margin: 0 0.5em; +} +.post-meta-item-icon { + margin-right: 3px; +} +@media (max-width: 991px) { + .post-meta-item-icon { + display: inline-block; + } +} +@media (max-width: 991px) { + .post-meta-item-text { + display: none; + } +} +.post-nav { + border-top: 1px solid #eee; + display: flex; + justify-content: space-between; + margin-top: 15px; + padding: 10px 5px 0; +} +.post-nav-item { + flex: 1; +} +.post-nav-item a { + border-bottom: none; + display: block; + font-size: 0.875em; + line-height: 1.6; + position: relative; +} +.post-nav-item a:active { + top: 2px; +} +.post-nav-item .fa { + font-size: 0.75em; +} +.post-nav-item:first-child { + margin-right: 15px; +} +.post-nav-item:first-child .fa { + margin-right: 5px; +} +.post-nav-item:last-child { + margin-left: 15px; + text-align: right; +} +.post-nav-item:last-child .fa { + margin-left: 5px; +} +.rtl.post-body p, +.rtl.post-body a, +.rtl.post-body h1, +.rtl.post-body h2, +.rtl.post-body h3, +.rtl.post-body h4, +.rtl.post-body h5, +.rtl.post-body h6, +.rtl.post-body li, +.rtl.post-body ul, +.rtl.post-body ol { + direction: rtl; + font-family: UKIJ Ekran; +} +.rtl.post-title { + font-family: UKIJ Ekran; +} +.post-tags { + margin-top: 40px; + text-align: center; +} +.post-tags a { + display: inline-block; + font-size: 0.8125em; +} +.post-tags a:not(:last-child) { + margin-right: 10px; +} +.post-widgets { + border-top: 1px solid #eee; + margin-top: 15px; + text-align: center; +} +.wp_rating { + height: 20px; + line-height: 20px; + margin-top: 10px; + padding-top: 6px; + text-align: center; +} +.social-like { + display: flex; + font-size: 0.875em; + justify-content: center; + text-align: center; +} +.reward-container { + margin: 20px auto; + padding: 10px 0; + text-align: center; + width: 90%; +} +.reward-container button { + background: transparent; + border: 1px solid #fc6423; + border-radius: 0; + color: #fc6423; + cursor: pointer; + line-height: 2; + outline: 0; + padding: 0 15px; + vertical-align: text-top; +} +.reward-container button:hover { + background: #fc6423; + border: 1px solid transparent; + color: #fa9366; +} +#qr { + padding-top: 20px; +} +#qr a { + border: 0; +} +#qr img { + display: inline-block; + margin: 0.8em 2em 0 2em; + max-width: 100%; + width: 180px; +} +#qr p { + text-align: center; +} +.my_post_copyright { + width: 85%; + max-width: 45em; + margin: 2.8em auto 0; + padding: 0.5em 1em; + border: 1px solid #d3d3d3; + font-size: 0.93rem; + line-height: 1.6em; + word-break: break-all; + background: rgba(255,255,255,0.4); +} +.my_post_copyright p { + margin: 0; +} +.my_post_copyright span { + display: inline-block; + width: 5.2em; + color: #b5b5b5; + font-weight: bold; +} +.my_post_copyright .raw { + margin-left: 1em; + width: 5em; +} +.my_post_copyright a { + color: #808080; + border-bottom: 0; +} +.my_post_copyright a:hover { + color: #a3d2a3; + text-decoration: underline; +} +.my_post_copyright:hover .fa-clipboard { + color: #000; +} +.my_post_copyright .post-url:hover { + font-weight: normal; +} +.my_post_copyright .copy-path { + margin-left: 1em; + width: 1em; +} +@media (max-width: 767px) { + .my_post_copyright .copy-path { + display: none; + } +} +.my_post_copyright .copy-path:hover { + color: #808080; + cursor: pointer; +} +.category-all-page .category-all-title { + text-align: center; +} +.category-all-page .category-all { + margin-top: 20px; +} +.category-all-page .category-list { + list-style: none; + margin: 0; + padding: 0; +} +.category-all-page .category-list-item { + margin: 5px 10px; +} +.category-all-page .category-list-count { + color: #bbb; +} +.category-all-page .category-list-count::before { + content: ' ('; + display: inline; +} +.category-all-page .category-list-count::after { + content: ') '; + display: inline; +} +.category-all-page .category-list-child { + padding-left: 10px; +} +.event-list { + padding: 0; +} +.event-list hr { + background: #222; + margin: 20px 0 45px 0; +} +.event-list hr::after { + background: #222; + color: #fff; + content: 'NOW'; + display: inline-block; + font-weight: bold; + padding: 0 5px; + text-align: right; +} +.event-list .event { + background: #222; + margin: 20px 0; + min-height: 40px; + padding: 15px 0 15px 10px; +} +.event-list .event .event-summary { + color: #fff; + margin: 0; + padding-bottom: 3px; +} +.event-list .event .event-summary::before { + animation: dot-flash 1s alternate infinite ease-in-out; + color: #fff; + content: '\f111'; + display: inline-block; + font-size: 10px; + margin-right: 25px; + vertical-align: middle; + font-family: 'Font Awesome 5 Free'; + font-weight: 900; +} +.event-list .event .event-relative-time { + color: #bbb; + display: inline-block; + font-size: 12px; + font-weight: normal; + padding-left: 12px; +} +.event-list .event .event-details { + color: #fff; + display: block; + line-height: 18px; + margin-left: 56px; + padding-bottom: 6px; + padding-top: 3px; + text-indent: -24px; +} +.event-list .event .event-details::before { + color: #fff; + display: inline-block; + margin-right: 9px; + text-align: center; + text-indent: 0; + width: 14px; + font-family: 'Font Awesome 5 Free'; + font-weight: 900; +} +.event-list .event .event-details.event-location::before { + content: '\f041'; +} +.event-list .event .event-details.event-duration::before { + content: '\f017'; +} +.event-list .event-past { + background: #f5f5f5; +} +.event-list .event-past .event-summary, +.event-list .event-past .event-details { + color: #bbb; + opacity: 0.9; +} +.event-list .event-past .event-summary::before, +.event-list .event-past .event-details::before { + animation: none; + color: #bbb; +} +@-moz-keyframes dot-flash { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.8); + } +} +@-webkit-keyframes dot-flash { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.8); + } +} +@-o-keyframes dot-flash { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.8); + } +} +@keyframes dot-flash { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.8); + } +} +ul.breadcrumb { + font-size: 0.75em; + list-style: none; + margin: 1em 0; + padding: 0 2em; + text-align: center; +} +ul.breadcrumb li { + display: inline; +} +ul.breadcrumb li + li::before { + content: '/\00a0'; + font-weight: normal; + padding: 0.5em; +} +ul.breadcrumb li + li:last-child { + font-weight: bold; +} +.tag-cloud { + text-align: center; +} +.tag-cloud a { + display: inline-block; + margin: 10px; +} +.tag-cloud a:hover { + color: var(--link-hover-color) !important; +} +.gt-header a, +.gt-comments a, +.gt-popup a { + border-bottom: none; +} +.gt-container .gt-popup .gt-action.is--active::before { + top: 0.7em; +} +.search-pop-overlay { + background: rgba(0,0,0,0); + height: 100%; + left: 0; + position: fixed; + top: 0; + transition: visibility 0s linear 0.2s, background 0.2s; + visibility: hidden; + width: 100%; + z-index: 1400; +} +.search-pop-overlay.search-active { + background: rgba(0,0,0,0.3); + transition: background 0.2s; + visibility: visible; +} +.search-popup { + background: var(--card-bg-color); + border-radius: 5px; + height: 80%; + left: calc(50% - 350px); + position: fixed; + top: 10%; + transform: scale(0); + transition: transform 0.2s; + width: 700px; + z-index: 1500; +} +.search-active .search-popup { + transform: scale(1); +} +@media (max-width: 767px) { + .search-popup { + border-radius: 0; + height: 100%; + left: 0; + margin: 0; + top: 0; + width: 100%; + } +} +.search-popup .search-icon, +.search-popup .popup-btn-close { + color: #999; + font-size: 18px; + padding: 0 10px; +} +.search-popup .popup-btn-close { + cursor: pointer; +} +.search-popup .popup-btn-close:hover .fa { + color: #222; +} +.search-popup .search-header { + background: #eee; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + display: flex; + padding: 5px; +} +.search-popup input.search-input { + background: transparent; + border: 0; + outline: 0; + width: 100%; +} +.search-popup input.search-input::-webkit-search-cancel-button { + display: none; +} +.search-popup .search-input-container { + flex-grow: 1; + padding: 2px; +} +.search-popup ul.search-result-list { + margin: 0 5px; + padding: 0; + width: 100%; +} +.search-popup p.search-result { + border-bottom: 1px dashed #ccc; + padding: 5px 0; +} +.search-popup a.search-result-title { + font-weight: bold; +} +.search-popup .search-keyword { + border-bottom: 1px dashed #ff2a2a; + color: #ff2a2a; + font-weight: bold; +} +.search-popup #search-result { + display: flex; + height: calc(100% - 55px); + overflow: auto; + padding: 5px 25px; +} +.search-popup #no-result { + color: #ccc; + margin: auto; +} +.header { + margin: 0 auto; + position: relative; + width: calc(100% - 20px); +} +@media (min-width: 1200px) { + .header { + width: 1160px; + } +} +@media (min-width: 1600px) { + .header { + width: 73%; + } +} +@media (max-width: 991px) { + .header { + width: auto; + } +} +.header-inner { + background: var(--content-bg-color); + border-radius: initial; + box-shadow: 0 2px 2px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.06), 0 1px 5px 0 rgba(0,0,0,0.12); + overflow: hidden; + padding: 0; + position: absolute; + top: 0; + width: 220px; +} +@media (min-width: 1200px) { + .header-inner { + width: 220px; + } +} +@media (max-width: 991px) { + .header-inner { + border-radius: initial; + position: relative; + width: auto; + } +} +.main-inner { + align-items: flex-start; + display: flex; + justify-content: space-between; + flex-direction: row-reverse; +} +@media (max-width: 991px) { + .main-inner { + width: auto; + } +} +.content-wrap { + background: var(--content-bg-color); + border-radius: initial; + box-shadow: 0 2px 2px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.06), 0 1px 5px 0 rgba(0,0,0,0.12); + box-sizing: border-box; + padding: 40px; + width: calc(100% - 232px); +} +@media (max-width: 991px) { + .content-wrap { + border-radius: initial; + padding: 20px; + width: 100%; + } +} +.footer-inner { + padding-left: 260px; +} +.back-to-top { + left: auto; + right: 30px; +} +@media (max-width: 991px) { + .back-to-top { + right: 20px; + } +} +@media (max-width: 991px) { + .footer-inner { + padding-left: 0; + padding-right: 0; + width: auto; + } +} +.site-brand-container { + background: #222; +} +@media (max-width: 991px) { + .site-brand-container { + box-shadow: 0 0 16px rgba(0,0,0,0.5); + } +} +.site-meta { + padding: 20px 0; +} +.brand { + padding: 0; +} +.site-subtitle { + margin: 10px 10px 0; +} +.custom-logo-image { + margin-top: 20px; +} +@media (max-width: 991px) { + .custom-logo-image { + display: none; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .site-nav-toggle, + .site-nav-right { + display: flex; + flex-direction: column; + justify-content: center; + } +} +.site-nav-toggle .toggle, +.site-nav-right .toggle { + color: #fff; +} +.site-nav-toggle .toggle .toggle-line, +.site-nav-right .toggle .toggle-line { + background: #fff; +} +@media (min-width: 768px) and (max-width: 991px) { + .site-nav { + display: none; + } +} +.menu .menu-item { + display: block; + margin: 0; +} +.menu .menu-item a, +.menu .menu-item span.exturl { + padding: 5px 20px; + position: relative; + text-align: left; + transition-property: background-color; +} +@media (max-width: 991px) { + .menu .menu-item.menu-item-search { + display: none; + } +} +.menu .menu-item .badge { + background: #ccc; + border-radius: 10px; + color: #fff; + float: right; + padding: 2px 5px; + text-shadow: 1px 1px 0 rgba(0,0,0,0.1); + vertical-align: middle; +} +.main-menu .menu-item-active a::after { + background: #bbb; + border-radius: 50%; + content: ' '; + height: 6px; + margin-top: -3px; + position: absolute; + right: 15px; + top: 50%; + width: 6px; +} +.sub-menu { + background: var(--content-bg-color); + border-bottom: 1px solid #ddd; + margin: 0; + padding: 6px 0; +} +.sub-menu .menu-item { + display: inline-block; +} +.sub-menu .menu-item a, +.sub-menu .menu-item span.exturl { + background: transparent; + margin: 5px 10px; + padding: initial; +} +.sub-menu .menu-item a:hover, +.sub-menu .menu-item span.exturl:hover { + background: transparent; + color: #fc6423; +} +.sub-menu .menu-item-active a { + border-bottom-color: #fc6423; + color: #fc6423; +} +.sub-menu .menu-item-active a:hover { + border-bottom-color: #fc6423; +} +.sidebar { + background: var(--body-bg-color); + box-shadow: none; + margin-top: 100%; + position: static; + width: 220px; +} +@media (max-width: 991px) { + .sidebar { + display: none; + } +} +.sidebar-toggle { + display: none; +} +.sidebar-inner { + background: var(--content-bg-color); + border-radius: initial; + box-shadow: 0 2px 2px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.06), 0 1px 5px 0 rgba(0,0,0,0.12), 0 -1px 0.5px 0 rgba(0,0,0,0.09); + box-sizing: border-box; + color: var(--text-color); + width: 220px; + opacity: 0; +} +.sidebar-inner.affix { + position: fixed; + top: 12px; +} +.sidebar-inner.affix-bottom { + position: absolute; +} +.site-state-item { + padding: 0 10px; +} +.sidebar-button { + border-bottom: 1px dotted #ccc; + border-top: 1px dotted #ccc; + margin-top: 10px; + text-align: center; +} +.sidebar-button a { + border: 0; + color: #fc6423; + display: block; +} +.sidebar-button a:hover { + background: none; + border: 0; + color: #e34603; +} +.sidebar-button a:hover .fa, +.sidebar-button a:hover .fab, +.sidebar-button a:hover .far, +.sidebar-button a:hover .fas { + color: #e34603; +} +.links-of-author { + display: flex; + flex-wrap: wrap; + margin-top: 10px; + justify-content: center; +} +.links-of-author-item { + margin: 5px 0 0; + width: 50%; +} +.links-of-author-item a, +.links-of-author-item span.exturl { + box-sizing: border-box; + display: inline-block; + margin-bottom: 0; + margin-right: 0; + max-width: 216px; + overflow: hidden; + padding: 0 5px; + text-overflow: ellipsis; + white-space: nowrap; +} +.links-of-author-item a, +.links-of-author-item span.exturl { + border-bottom: none; + display: block; + text-decoration: none; +} +.links-of-author-item a::before, +.links-of-author-item span.exturl::before { + display: none; +} +.links-of-author-item a:hover, +.links-of-author-item span.exturl:hover { + background: var(--body-bg-color); + border-radius: 4px; +} +.links-of-author-item .fa, +.links-of-author-item .fab, +.links-of-author-item .far, +.links-of-author-item .fas { + margin-right: 2px; +} +.links-of-blogroll-item { + padding: 0; + display: inline-block; + margin: 5px 0 0; + width: unset; +} +.links-of-blogroll-item a, +.links-of-blogroll-item span.exturl { + box-sizing: border-box; + display: inline-block; + margin-bottom: 0; + margin-right: 0; + max-width: 216px; + overflow: hidden; + padding: 0 5px; + text-overflow: ellipsis; + white-space: nowrap; +} +.content-wrap { + background: initial; + box-shadow: initial; + padding: initial; +} +.post-block { + background: var(--content-bg-color); + border-radius: initial; + box-shadow: 0 2px 2px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.06), 0 1px 5px 0 rgba(0,0,0,0.12); + padding: 40px; +} +.post-block + .post-block { + border-radius: initial; + box-shadow: 0 2px 2px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.06), 0 1px 5px 0 rgba(0,0,0,0.12), 0 -1px 0.5px 0 rgba(0,0,0,0.09); + margin-top: 12px; +} +.comments { + background: var(--content-bg-color); + border-radius: initial; + box-shadow: 0 2px 2px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.06), 0 1px 5px 0 rgba(0,0,0,0.12), 0 -1px 0.5px 0 rgba(0,0,0,0.09); + margin-top: 12px; + padding: 40px; +} +.tabs-comment { + margin-top: 1em; +} +.content { + padding-top: initial; +} +.post-eof { + display: none; +} +.pagination { + background: var(--content-bg-color); + border-radius: initial; + border-top: initial; + box-shadow: 0 2px 2px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.06), 0 1px 5px 0 rgba(0,0,0,0.12), 0 -1px 0.5px 0 rgba(0,0,0,0.09); + margin: 12px 0 0; + padding: 10px 0 10px; +} +.pagination .prev, +.pagination .next, +.pagination .page-number { + margin-bottom: initial; + top: initial; +} +.main { + padding-bottom: initial; +} +.footer { + bottom: auto; +} +.sub-menu { + border-bottom: initial; + box-shadow: 0 2px 2px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.06), 0 1px 5px 0 rgba(0,0,0,0.12); +} +.sub-menu + .content .post-block { + box-shadow: 0 2px 2px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.06), 0 1px 5px 0 rgba(0,0,0,0.12), 0 -1px 0.5px 0 rgba(0,0,0,0.09); + margin-top: 12px; +} +@media (min-width: 768px) and (max-width: 991px) { + .sub-menu + .content .post-block { + margin-top: 10px; + } +} +@media (max-width: 767px) { + .sub-menu + .content .post-block { + margin-top: 8px; + } +} +.post-body h1, +.post-body h2 { + border-bottom: 1px solid #eee; +} +.post-body h3 { + border-bottom: 1px dotted #eee; +} +@media (min-width: 768px) and (max-width: 991px) { + .content-wrap { + padding: 10px; + } + .posts-expand .post-button { + margin-top: 20px; + } + .post-block { + border-radius: initial; + box-shadow: 0 2px 2px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.06), 0 1px 5px 0 rgba(0,0,0,0.12), 0 -1px 0.5px 0 rgba(0,0,0,0.09); + padding: 20px; + } + .post-block + .post-block { + margin-top: 10px; + } + .comments { + margin-top: 10px; + padding: 10px 20px; + } + .pagination { + margin: 10px 0 0; + } +} +@media (max-width: 767px) { + .content-wrap { + padding: 8px; + } + .posts-expand .post-button { + margin: 12px 0; + } + .post-block { + border-radius: initial; + box-shadow: 0 2px 2px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.06), 0 1px 5px 0 rgba(0,0,0,0.12), 0 -1px 0.5px 0 rgba(0,0,0,0.09); + min-height: auto; + padding: 12px; + } + .post-block + .post-block { + margin-top: 8px; + } + .comments { + margin-top: 8px; + padding: 10px 12px; + } + .pagination { + margin: 8px 0 0; + } +} diff --git a/en/2013/06/17/Read-High-Performance-JavaScript/index.html b/en/2013/06/17/Read-High-Performance-JavaScript/index.html new file mode 100644 index 0000000000..f4e2b1d7bc --- /dev/null +++ b/en/2013/06/17/Read-High-Performance-JavaScript/index.html @@ -0,0 +1,607 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《高性能JAVASCRIPT》读书笔记 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《高性能JAVASCRIPT》读书笔记 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近读了《高性能JAVASCRIPT》,下面将书中觉得有用的内容进行摘抄,放在下方。

+

如何从小事提升JAVASCRIPT性能。

    +
  1. <script>标签写在</body>之前——将脚本放在底部。

    +
  2. +
  3. 尽量少的<script>标签,减少页面中的外链数量,减少请求次数。

    +
  4. +
  5. 使用打包工具如:Yahoo!combo handler

    +
  6. +
  7. 使用动态延迟加载技术如:LazyLoad类库,LABjs

    +
    1
    2
    3
    4
    5
    6
    7
    // js代码
    <script type="text/javascript" src="lazyload-min.js"></script>
    <script type="text/javascript">
    LazyLoad.js([],function(){
    Application.init();
    })
    </script>
  8. +
  9. 在javascript中存储位置十分重要,尽量用局部变量代替对象成员的访问。

    +
  10. +
  11. 集合变数组提高查询效率

    +
    1
    2
    3
    4
    5
    6
    7
    // js代码
    function toArray(coll){
    for(var i = 0, a=[], len=coll.length; i<len; i++){
    a[i]=col[i];
    }
    return a;
    }
  12. +
  13. 使用局部变量缓存访问多次的成员
    当便利一个集合时,首要优化原则是把集合存储在局部变量中,并把length缓存在循环外部,然后使用局部变量访问这些需要多次访问的元素。

    +
  14. +
  15. 使用原生DOM方法querySelectorAll()遍历查找元素。

    +
  16. +
  17. 让元素脱离动画流,页面重排次数越多,程序响应越慢,要尽量减少重拍
    方法:

    +
  18. +
    1. +
    2. 使用绝对定位使元素脱离文档流
    3. +
    +
  19. +
  20. IE:hover
    在大量元素使用:hover这个伪css选择器时,程序会降低响应速率,在大表格或列表的情况下

    +
  21. +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:《高性能JAVASCRIPT》读书笔记

+

文章作者:

+

发布时间:2013年06月17日 - 22:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2013/06/17/Read-High-Performance-JavaScript/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2013/06/26/CSS-Triangle-Circle/index.html b/en/2013/06/26/CSS-Triangle-Circle/index.html new file mode 100644 index 0000000000..48e1c48180 --- /dev/null +++ b/en/2013/06/26/CSS-Triangle-Circle/index.html @@ -0,0 +1,623 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CSS border三角、圆角图形生成技术详解 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ CSS border三角、圆角图形生成技术详解 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

利用CSS的border属性可以生成一些图形,例如三角或是圆角。纯粹的CSS2的内容,没有兼容性的问题,我之前在纯CSS实现各类气球泡泡对话框效果一文中算是比较详细的讲述了CSS border属性生成三角的原理,以及实例。我觉得此技术相当实用的,故本文再次简单叙述一下,另外,本文还将展示可能并不为众人所知的CSS border圆角生成技术。好了,裹脚布的话就不说了,直接进入正题。

+ + +

CSS border生成三角技术简介

效果抢鲜

下图为使用CSS的border属性实现的三角效果:

+
1
2
3
4
5
6
7
8
// css 代码
.test{
width: 0;
height: 0;
border-width: 20px 10px;
border-style: solid;
border-color: #ff3300 #ff3300 #ffffff #ffffff;
}
+

如何实现的,为何会有这样的效果,不急,take it easy!

+

梯形图案

看下面这段样式:

+
1
2
3
4
5
6
.test{
width: 10px;
height: 10px;
border: 10px solid;
border-color: #ff3300 #0000ff #339966 #00ff00
}
+

当某个div应用了上面这个样式后,结果会如何?见下图(截自Firefox3.5,IE浏览器有细节上的差异):

+

更进一步 – 部分边框透明

现在,设想一下,如果我们现在只保留一个一个上边框,其余边框均transparent透明(或与背景色同色),那么是不是就只显示一个上面红色的边框了,我们测试下,与上面类似的代码,只是修改下其余三个边框的颜色。

+
1
2
3
4
5
6
.test{
width:10px;
height:10px;
border:10px solid;
border-color:#ff3300 #ffffff #ffffff #ffffff;
}
+

结果如下图(截自Firefox3.5):

+

从梯形到三角

上面的是梯形,我要想得到一个三角图案该怎么办呢?显然,很简单,把div的高宽都变成0,只留一边,不就是三角了吗?如下代码:

+
1
2
3
4
5
.test{
width: 0;
height: 0;
border: 10px solid;
border-color: #ff3300 #ffffff #ffffff #ffffff;}
+

结果如下(依旧截图自Firefox3.5):

+

从等腰直角三角形到普通等腰三角

上图为等腰直角三角形,之所以为等腰直角,是因为所有的边框宽度是一样的,如果我们将边框宽度设置为不同,那会怎样?则会形成等腰三角形。如下代码:

+
1
2
3
4
5
6
.test{
width: 0;
height: 0;
border-width: 20px 10px;
border-style: solid;
border-color: #ff3300 #ffffff #ffffff #ffffff;}
+

得到的结果如下图:

+

从等腰到不等腰

我们可以不局限于保留一条边框,我们可以保留两条,于是我们可以告别等腰,得到更加锐利的三角,正如一开始所展示的那个三角:

+
1
2
3
4
5
6
.test{
width: 0;
height: 0;
border-width: 20px 10px;
border-style: solid;
border-color: #ff3300 #ff3300 #ffffff #ffffff;}
+

实际的应用

关于应用,不多说,直接看图:
说明:
以上的测试代码纯粹为了说明原理,所以使用#ffffff白色边框,通过于背景融合来隐藏边框。在实际的操作中,应该使用transparent透明属性,例如border-color:#ff3300 #ff3300 transparent transparent;,这同样会有问题,IE6浏览器不支持transparent透明属性,不过没有关系,就border生成三角技术而言,直接设置对应的透明边框的border-style属性为dotted或是dashed即可解决这一问题,为什么使用dotted和dashed可以修复此问题呢?您有兴趣可以参见默尘的这篇文章Dotted&Dashed终极分析及IE6透明边框。

+

CSS border圆角生成技术简介

我看圆角

一提到圆角,我脑中闪过的词就是“定位”,“嵌套”,“模拟”,“渐进增强”,“滥用”。

+
    +
  • 定位,也就是切四个角上下左右定位,这是淘宝首页的做法,但是面对IE6的奇偶bug只能当作看客;
  • +
  • 使用“嵌套”则不会有此问题,“嵌套”分图片背景嵌套和CSS边框嵌套,使用图片嵌套则图片的重用性,大小优化有待加强,边框嵌套则技术实现上有些难度;
  • +
  • 或使用“渐进增强”,CSS3 border-radius属性,而不要去鸟IE这类自我感觉良好的浏览器;
  • +
  • 或是学习Google使用CSS模拟,而一般的CSS模拟都是使用左右边框+背景色的方式1像素1像素的拼合成的。这类方法各有优缺点,需根据实际情况采用。对于满眼圆角的设计图我是很不喜欢的,该用则用,切勿为了圆角而圆角。
  • +
+

border圆角图案生成法

这里介绍的实现圆角的得到与上面提到的都是不一样的,虽然也属于CSS模拟的范畴,但是其高效的程度确实相当惊人的,可谓最佳实践之一。
我们先看看效果,见下图,截自Firefox3.6:
上述效果的实现仅仅使用了三个标签,如下代码:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// html 代码
<div class="box">
<div class="top"></div>
<div class="center">我是一只小小鸟、小小鸟!</div>
<div class="bot"></div>
</div>

// css 代码
.box{
width:500px;
}
.top{
border-bottom:3px solid;
border-top-color:#cc0000;
border-bottom-color:#cc0000;
border-left:3px dotted transparent;
border-right:3px dotted transparent;
}
.center{
padding:10px 20px;
color:white;
font-size:14px;
background:#cc0000;
}
.bot{
border-top:3px solid;
border-top-color:#cc0000;
border-bottom-color:#cccccc;
border-left:3px dotted transparent;
border-right:3px dotted transparent;
}
+

我们看看这段代码在IE6下的效果:

+

这里的高效在于,仅仅使用了一层标签就模拟了3像素的圆角,按照曾经我对CSS圆角模拟的理解,模拟1像素的圆角需要一层标签(background+borderLeft+borderRight),两像素的需要两层标签,三像素的需要三层标签。

+

有点神奇,但是就像看刘谦的魔术一样,说穿了也就那么回事,其实这里的圆角模拟在本文的上面已经展示了,就是这样图片:

+

您可能会疑问,是不是搞错图片啦,这显然不是一个模样的,非也非也,就本质上而言,圆角的实现与上面的梯形图就是同样的东西。现在,盯着上面这张图,我们想象一下,用力的想象,用想花姑娘的那番劲头想象——上面的梯形宽度越来越宽(不是拉伸),一直宽到500像素,是不是与上面实现的圆角的下边缘一致啊?

+

也就是说,那个含有“我是一只小小鸟……”文字的圆角图形是有一个上梯形+矩形+下梯形组成的。参见下面的分离效果图:
您可以狠狠地点击这里:CSS border圆角生成demo

+

局限性

人无完人,金无足赤,此方法虽然简洁高效,兼容性上佳,但是依然有局限性,在实现实色背景的圆角效果时,此方法可谓首选;如果是纯粹的圆角边框,此方法也可以实现,需要用到边框重叠,但是标签数几乎要翻倍,其权衡效用将大打折扣,反不如其他圆角方法来的实在。

+

结语

如果在web制作中,需要用的一些直接可以使用CSS+单标签模拟的图片,我的建议是“毫不犹豫使用CSS模拟”,例如实色的三角,或是实现实色的圆角效果,这可以说是最高效,最利于扩展维护的前端实现方法了。我们需要开阔的思维,而不要仅仅局限于眼前的技术,武侠中所谓的“无招胜有招”还是有着一定的哲学道理的,长远来看,意识与海纳百川的心态比当下的一点技术更来得重要。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:CSS border三角、圆角图形生成技术详解

+

文章作者:

+

发布时间:2013年06月26日 - 22:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2013/06/26/CSS-Triangle-Circle/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2015/12/31/Read-Let-your-boss-promote-you/index.html b/en/2015/12/31/Read-Let-your-boss-promote-you/index.html new file mode 100644 index 0000000000..6cb577ef74 --- /dev/null +++ b/en/2015/12/31/Read-Let-your-boss-promote-you/index.html @@ -0,0 +1,780 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《让老板提拔你》读书笔记 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《让老板提拔你》读书笔记 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近读了《让老板提拔你》,下面将书中觉得有用的内容进行摘抄,放在下方。

+

定位自己

正确认识自己,确定社会定位、职业定位。 定位-决定-定价

+

要素

核心竞争力职位 契合度 是高薪关键所在

+

契合度

    +
  • 技能、专长、经历与职位要求的契合度
  • +
  • 专业资质和等级与职位要求的契合度
  • +
  • 综合素质与职位要求的契合度

    七大秘诀

  • +
  • 了解同行业薪酬的平均水平
  • +
  • 赢得未来单位的心
  • +
  • 先让对方开口
  • +
  • 勇敢地开口要求
  • +
  • 不要轻言放弃
  • +
  • 把握时机很重要
  • +
  • 说实话,别撒谎

    如何谈薪资

  • +
  • 商洽薪酬的关键在于把握好谈话时机,切勿开门见山。
    +

    只要有发展机会,我愿意接受贵单位的薪酬标准,不知道按规定这个工作岗位的薪酬标准是多少。

    +
    +
  • +
+

将知识卖个好价钱

推销自己既推销自己所掌握的知识即智慧,正确认识自己的价值是关键。

+

高薪是因为“物有所值”

    +
  • 用业绩、用能力说话,是人才坦然面对高薪的心态。
  • +
  • 高薪人才的求职方式 —- 圈内朋友引见、定向投简历、行业论坛、交流活动等。
  • +
  • 高级人才不能经常跳槽要保持相对的职业稳定。—- 在一家公司少则3年多则6年 —- 接受挑战,把握机会。
  • +
+

失败丰富走向成功经验

强调在失败中吸取的经验,在未来中可以避免的损失。

+

能为企业带来丰厚的利润才是人才

企业招聘员工的唯一目的:追求公司业绩最大化、利润最大化。

+

高质高效工作全攻略

    +
  • 进行正确的自我评价
  • +
  • 做最擅长做的事
      +
    • 三个经济原则 —- 发挥人才优势。
        +
      1. 比较利益原则—–自己的专长对自己才是最有利的,勿与他人作比较。
      2. +
      3. 机会成本原则—–选择一份工作放弃其他工作就是机会成本。
      4. +
      5. 效率原则——–工作的成功不在于时间有多长,而在于成效有多少,附加值有多少。
      6. +
      +
    • +
    +
  • +
  • 马上行动
  • +
  • 做事情要雷厉风行 ——— 成功属于踏实肯干的人
  • +
  • 有条不紊地开展工作 ——- 制定时间计划
  • +
  • 善于利用现代办公工具
  • +
  • 给自己最大的工作空间
  • +
  • 建立高效有序的办公环境
  • +
  • 不要忘记最初想去的方向
  • +
  • “聪明”的向上级提出建议
  • +
  • 专心做事,避免浮躁
  • +
  • 多而不专,一事难成
  • +
  • “专”才有高质工作——–人才主要指专才–某个领域的高精尖人才–勿做全才,一专多能
  • +
  • 做事要有条理
  • +
+

不要只把自己当成一个打工仔

+

要把工作当成事业

+
+
    +
  • 工作不仅仅是为了钱
  • +
  • 对工作要有明确的价值取向
      +
    1. 认清人生的方向
    2. +
    3. 开始学会醉卧探索和认知
    4. +
    5. 认清工作价值与成就的关系
    6. +
    7. 长期的工作规划
    8. +
    9. 在生命的天平上衡量自身的价值
    10. +
    +
  • +
  • 巧妙应对与上司看法向左时的三条准则
      +
    1. 遇事考虑全局
    2. +
    3. 辩证地看待问题
    4. +
    5. 切记感情用事
    6. +
    +
  • +
  • 把单位的事当成自家的事
  • +
  • 认真负责地用心工作
  • +
  • 珍惜岗位,热爱自己的职业
  • +
  • 永远是在为自己工作
  • +
  • 敬重自己的工作
  • +
  • 不要轻视薪水微薄的工作
  • +
  • 永远对工作充满激情
  • +
  • 以自己的工作为荣
  • +
  • 不要被他人的观点所束缚
  • +
  • 暂时的胜负并不会决定人生的最后走向
  • +
  • 将弱势转化为优势
  • +
  • 全力以赴做好每一天的工作
  • +
  • 和优秀的人士在一起—见贤思齐、借梯爬楼
      +
    • 如何争取跟优秀的人在一起
        +
      1. 不断的抛头露面
      2. +
      3. 帮助可以帮助自己成就事业的人做事
      4. +
      5. 与上司和比自己优秀的人士一起合作
      6. +
      +
    • +
    +
  • +
+
    +
  1. 尊重对方,严谨有致
  2. +
  3. 切记奉承,要不卑不亢
  4. +
  5. 态度自然,不必拘谨
  6. +
  7. 陪衬得当,不可狂妄
  8. +
  9. 主动真诚,做出姿态
  10. +
  11. 求助求教,接受呵护
  12. +
+
    +
  • 挑战自我,承担责任
      +
    • 三条忠告
        +
      1. 全心全意工作
      2. +
      3. 把自己视为合伙人
      4. +
      5. 迎接变革的需求
      6. +
      +
    • +
    +
  • +
  • 自信独立,不随波逐流
  • +
  • 敢于显示自己很重要
  • +
  • 千万不能只知道抱怨上司
  • +
  • 保持严谨认真的做事习惯
  • +
  • 自主地做好手中的工作
  • +
  • 踏踏实实地做好本职工作
  • +
  • 丢掉工作散漫的坏习惯
  • +
  • 不要让浮躁的性格困扰自己
  • +
  • 不推诿,勇于承担责任
  • +
  • 无论如何都不要拖延工作
  • +
  • 糊弄工作只能是在糊弄自己
  • +
  • 逊色的工作只会淘汰自己
  • +
  • 千万别丢掉“得宠”之资
  • +
  • “一步登天”只会摔疼自己
  • +
  • 别让“差不多”贻误了自己
  • +
  • 能完成100%,就决不做99%
  • +
+

与上司相处

    +
  • 不要做上司的“心腹”
  • +
  • 适时恰当的赞美上司
      +
    • 赞美上司,还要善于选择适当的场合
    • +
    • 赞美上司,要学会巧借公众语言称赞
    • +
    • 赞美上司,还要善于赞美不得志的上司
    • +
    +
  • +
  • 主动与领导沟通
  • +
  • 主动和上司保持联系
  • +
  • 用“心机”主动接近上司
      +
    • 尽可能详细的了解上司
    • +
    • 选择一个与领导尽可能近的位置
    • +
    • 赢得上司青睐的方法
    • +
    +
  • +
  • 更有效的和上司沟通
      +
    • 与上司沟通要简洁
    • +
    • 与上司沟通要大度大气大方
    • +
    • 与上司沟通,就要把自己先放一边 ——– 说话对事不对人
    • +
    +
  • +
  • 四种和上司进行沟通的方法
      +
    1. 开诚布公式———提出问题,说出自己的理解认识,给出解决方案,寻求领导意思,执行领导的命令
    2. +
    3. 先斩后奏式———因主客观原因导致的没有先汇报而是先解决的事,事后要将问题的分析方法,解决方法,实施方案以及结果做一份详尽的总结报告上交给领导
    4. +
    5. “含情脉脉”式——-通过交流一些工作上的问题来含蓄表达出个人的建议、需要
    6. +
    7. 巧设比喻式———通过典型案例来暗示自己的意见和要求
    8. +
    +
  • +
  • 把话说到上司的心坎上
  • +
  • 巧妙地为领导拾遗补缺
      +
    1. 诠释领导讲话的难点
    2. +
    3. 强调领导的才干
    4. +
    5. 化严肃为幽默
    6. +
    7. 稳定情绪,委婉暗示
    8. +
    +
  • +
  • 工作中勤于请示汇报
      +
    1. 听懂上司的意图
    2. +
    3. 探讨、磨合,达成共识
    4. +
    5. 制定尽可能详尽的工作计划
    6. +
    7. 随时向上司汇报任务的关键点
    8. +
    9. 总结汇报
    10. +
    +
  • +
  • 用成功赢得上司的信任
  • +
  • 工作中不要冲撞上司
  • +
  • 处理好同上司之间的分歧
      +
    1. 圆融协调——领导不懂,下达了错误的指令
        +
      1. 私下向上司陈述意见,帮助上司做出正确的决策
      2. +
      3. 如果上司采纳了下属的建议或意见,该下属有责任长期保守秘密
      4. +
      5. 如果上司固执己见,那么无条件服从
      6. +
      +
    2. +
    3. 装聋作哑——不涉及到原则问题
    4. +
    5. 棘手难题多权衡
        +
      1. 立刻插话纠正
      2. +
      3. 提醒上司
      4. +
      5. 暗示
      6. +
      7. 事后补救
      8. +
      9. 事后提醒
      10. +
      +
    6. +
    +
  • +
  • 正确对待上司的批评
  • +
  • 要善于服从自己的上司
  • +
  • 正确化解来自上司的压力
  • +
+

写在最后

博观约取,多读书读好书,丰富自己,变得睿智。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:《让老板提拔你》读书笔记

+

文章作者:

+

发布时间:2015年12月31日 - 22:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2015/12/31/Read-Let-your-boss-promote-you/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2016/10/27/Read-The-miracle-of-the-morning-journal/index.html b/en/2016/10/27/Read-The-miracle-of-the-morning-journal/index.html new file mode 100644 index 0000000000..d119700422 --- /dev/null +++ b/en/2016/10/27/Read-The-miracle-of-the-morning-journal/index.html @@ -0,0 +1,688 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《晨间日记的奇迹》读书笔记 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《晨间日记的奇迹》读书笔记 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

昨天花了1天的时间读了《晨间日记的奇迹》,感觉整个人的认识发生了一些变化,下面把重要内容做一下摘抄总结。

+

早上写日记的好处 —— 提升效率

    +
  • 可以做好一天的准备 — 计划性
  • +
  • 可以正确的写出昨天发生的事 — 效率性&忠诚性
  • +
  • 可以中立的看待昨天 — 中立性
  • +
  • 相对自由的时间 — 持续性
  • +
  • 总结经验 — 活用性
  • +
+

注意事项

日记 不等于 日志
日记 要记录 时间 天气 心情 — 因为这些有助于回忆和记录
不要投入过长时间 — 3分钟 — 日记私密性
晨间日记前7天只写好事 — 反省固然重要,但是成长更重要,持续性很重要 — 巴普洛夫的狗

+

晨间日记2部分

Part1

客观记录已经发生的事(昨天)— 经验智慧

+

Part2

    +
  • 今天应做的事 — 具体行动(来自昨天的总结
  • +
  • 今天一定要做的事(紧急重要的事)— 变得更积极更主动
  • +
  • 未来要做的事 — 不紧急但重要的事
  • +
  • 连用日记 — 历史上的今天(过去一年同一天的事)
  • +
+

夜晚日记 VS 晨间日记

受当天情绪影响 — 更冷静

+

梦想成真表

+ + + + + + + + + + + + + + + + + +
过去未来
事实IQ 智慧指数NQ 人际关系指数
感情EQ 情感指数DQ 梦想指数
+
    +
  • IQ — 晨间日记冷静回忆分析 — 提高决策正确性 IQ up
  • +
  • EQ — 冷静记录昨日的情绪/自省 — 变积极 EQ up
  • +
  • NQ — 总结昨天给今天设立“对/为XXX做某事” 很重要 NQ up
  • +
  • DQ — 不能只有眼前的事 每天都要确定一下自己的梦想 脸皮厚点没关系 重复或只有一句话也没关系 铭记于心最重要
  • +
+

“忙碌”只是借口!“决心”才是问题! 对未来充满期待的人才有行动力

+

如何早起

    +
  • 设定起床音乐/用气味(精油)刺激起床/光线-电动窗帘早起自动拉开
  • +
  • 早起后淋浴 — 配合音乐/冥想/呐喊 — 让自己充满动力保持清醒
  • +
+

写日记的五大好处

    +
  • 提升写作能力
  • +
  • 谈话题材源源不断
  • +
  • 提高贵人运
  • +
  • 返现自我肉体和精神的状态与模式
  • +
  • 在自己身上挖宝,彻底改变人生
  • +
+

记录的日记要常拿出来看看

记录日记时 — 问问自己的灵魂 然后写下真心话 — 灵魂日记
六度空间理论

+

七种成功者的习惯

    +
  • 习惯之一:积极主动别指望谁能推你走 — 主动行动,迈出第一步
  • +
  • 习惯之二:以终为始忠于自己的人生计划。— 设立人生目标并坚持
  • +
  • 习惯之三:要事第一选择当前该做的事
  • +
  • 习惯之四:追求双赢远离角斗场
  • +
  • 习惯之五:善于沟通换位思考的原则
  • +
  • 习惯之六:统合综效 1+1可以大于2
  • +
  • 习惯之七:不断更新全方位平衡自我
  • +
+

早睡是为了身体,早起是为了我们的内心。— sugiponn

+

晨间日记的格式

晨间日记必须要设立的栏目 — 工作/金钱/健康/人际关系
要记下当日的日期/天气/温度/湿度

+

纬度标签
工作方面:

+
    +
  • 能力开发/经营/管理/进货/工作人员/学习/资格/活动/企划案/想法/推广
    金钱方面:
  • +
  • 收入/指出/购入/股票/资产/储蓄/家用
    健康方面:
  • +
  • 饮食/运动/性/减肥/身体状况/锻炼身体/体重/脂肪
    人际关系方面:
  • +
  • 父母/家人/小孩/亲戚/情人/工作伙伴/朋友/熟人/联络事项/人脉网络
    兴趣方面以及其他:
  • +
  • 义工活动/阅读/运动/打扮/电影/音乐/游戏/喜欢的物品/今天必须要注意的事/美容/开心的事/购物
  • +
+

劳动 — 职业 — 工作 — 乐趣

+

三大原则和七大作战守则

    +
  • 原则1:时间不超过3分钟 — 减少养成习惯的成本

    +
  • +
  • 原则2:决定好写晨间日记的地方 — 为了养成习惯

    +
  • +
  • 原则3:只写一个字也没关系 — 不要有压力

    +
  • +
  • 作战守则1:开心写日记 — 反省固然重要,鼓励更有作用 — 开始写晨间日记前七天只写好事

    +
  • +
  • 作战守则2:ONE-TWO作战 — 把动作分布容易养成习惯 — 形成固定动作

    +
  • +
  • 作战守则3:巴普洛夫的狗 — 开始写日记时候要有一个仪式 — 心理暗示

    +
  • +
  • 作战守则4:奖励自己 — 当习惯养成并坚持到一定时间,给自己一个奖励

    +
  • +
  • 作战守则5:宣告天下 — 借由他人的鼓励或压力形成动力/也可找到同志

    +
  • +
  • 作战守则6:一千日作战法 — 不管什么样的习惯坚持1000日就会成为永远的习惯 — 具体做法为记录 第XXX篇 晨间日记

    +
  • +
  • 作战守则7:为了别人 — 写晨间日记不只是为了自己,而且更是为了别人,身边人的幸福最终会影响到自己

    +
  • +
+

应该先肯定自己,给自己打100分

    +
  • “总之,先… 病” 没有目标/没有梦想 — 回顾时一定会后悔!
  • +
  • “假如…,应该 病” 无法满足/没有动力 — 焦躁不安
  • +
  • 共同点:失落感/空虚/
  • +
+

解决办法— 设立一个情境

例:我要建立一个幸福的家庭 —> 因此我要创造很多快乐的回忆 —> 为了制造很多快乐的回忆 —> 我必须要到很多的地方去看看 —> 为了去很多地方看看,我觉得有辆车会更方便 —> 买车 —> 考驾照

+

不要总想自己哪里欠缺,要想自己想要得到什么,并且为了目标努力,你就会变的更完美。
拥有一个敢于大声喊出的梦想,不要羞涩,要为了梦想一步步的前进,最终达到梦想。

+

提到的另外的书

《培育梦想种子》《日记的力量》《成功人士的七个习惯》

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:《晨间日记的奇迹》读书笔记

+

文章作者:

+

发布时间:2016年10月27日 - 22:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2016/10/27/Read-The-miracle-of-the-morning-journal/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2017/08/29/browser-incompatibility-problem-solution/index.html b/en/2017/08/29/browser-incompatibility-problem-solution/index.html new file mode 100644 index 0000000000..45bc2a25ff --- /dev/null +++ b/en/2017/08/29/browser-incompatibility-problem-solution/index.html @@ -0,0 +1,656 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 浏览器兼容性问题解决方案 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 浏览器兼容性问题解决方案 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

作为前端,浏览器就是你的战场,而不同的浏览器就是不同的敌人,敌人有相同之处,也各有特点,这不同的特点就是不同浏览器的兼容性,下面就聊聊浏览器的兼容问题,如何解决不同浏览器的兼容性。

+ +

普及:浏览器的兼容性问题,往往是个别浏览器(没错,就是那个与众不同的浏览器)对于一些标准的定义不一致导致的。俗话说:没有IE就没有伤害。

+

贴士:内容都是自己总结的,不免会出现错误或者bug,欢迎更正和补充,本帖也会不断更新。

+

Normalize.css

不同浏览器的默认样式存在差异,可以使用 Normalize.css抹平这些差异。当然,你也可以定制属于自己业务的 reset.css

+
1
<link href="https://cdn.bootcss.com/normalize/7.0.0/normalize.min.css" rel="stylesheet">
+

简单粗暴法

+
1
* { margin: 0; padding: 0; }
+

html5shiv.js

解决 ie9 以下浏览器对 html5 新增标签不识别的问题。

+
1
2
3
<!--[if lt IE 9]>
<script type="text/javascript" src="https://cdn.bootcss.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]-->
+ +

respond.js

解决 ie9 以下浏览器不支持 CSS3 Media Query 的问题。

+
1
<script src="https://cdn.bootcss.com/picturefill/3.0.3/picturefill.min.js"></script>
+

IE 条件注释

IE 的条件注释仅仅针对IE浏览器,对其他浏览器无效
image

+

IE 属性过滤器(较为常用的hack方法)

针对不同的 IE 浏览器,可以使用不同的字符来对特定的版本的 IE 浏览器进行样式控制
image
image

+

浏览器 CSS 兼容前缀

1
2
3
4
5
6
7
8
9
-o-transform:rotate(7deg); // Opera

-ms-transform:rotate(7deg); // IE

-moz-transform:rotate(7deg); // Firefox

-webkit-transform:rotate(7deg); // Chrome

transform:rotate(7deg); // 统一标识语句
+

补充: 目前可以采用自动化插件完成,插件名称叫做 Autoprefixer,他可以解析css文件并且添加前缀到css内容里。

+

Auroprefixer 添加到资源构建工具如:webpack后,就可以不用再手动补全浏览器前缀了,这里只需要你按照W3C的标准来书写css代码,剩下的工作就交给插件完成,目前webpackgulpgrunt都有相应的插件,是不是开心啊。

+

a 标签的几种 CSS 状态的顺序

很多新人在写 a 标签的样式,会疑惑为什么写的样式没有效果,或者点击超链接后,hover、active 样式没有效果,其实只是写的样式被覆盖了。

+

正确的a标签顺序应该是:==love hate==

+
    +
  1. link:平常的状态
  2. +
  3. visited:被访问过之后
  4. +
  5. hover:鼠标放到链接上的时候
  6. +
  7. active:链接被按下的时候
  8. +
+

完美解决 Placeholder

1
<input type="text" value="Name *" onFocus="this.value = '';" onBlur="if (this.value == '') {this.value = 'Name *';}">
+ +

清除浮动 最佳实践

1
2
3
4
.fl { float: left; }
.fr { float: right; }
.clearfix:after { display: block; clear: both; content: ""; visibility: hidden; height: 0; }
.clearfix { zoom: 1; }
+ +

BFC 解决边距重叠问题

当相邻元素都设置了 margin 边距时,margin 将取最大值,舍弃小值。为了不让边距重叠,可以给子元素加一个父元素,并设置该父元素为 BFC:overflow: hidden;

+
+

Lorem ipsum dolor sit.

+ +
+

Lorem ipsum dolor sit.

+
+ +

Lorem ipsum dolor sit.

+
+ +

IE6 双倍边距的问题

设置 ie6 中设置浮动,同时又设置 margin,会出现双倍边距的问题

+
1
display: inline;
+

解决 IE9 以下浏览器不能使用 opacity

1
2
3
opacity: 0.5;
filter: alpha(opacity = 50);
filter: progid:DXImageTransform.Microsoft.Alpha(style = 0, opacity = 50);
+

解决 IE6 不支持 fixed 绝对定位以及IE6下被绝对定位的元素在滚动的时候会闪动的问题

1
2
3
4
5
6
7
8
9
/* IE6 hack */
*html, *html body {
background-image: url(about:blank);
background-attachment: fixed;
}
*html #menu {
position: absolute;
top: expression(((e=document.documentElement.scrollTop) ? e : document.body.scrollTop) + 100 + 'px');
}
+

IE6 背景闪烁的问题

问题:链接、按钮用 CSSsprites 作为背景,在 ie6 下会有背景图闪烁的现象。原因是 IE6 没有将背景图缓存,每次触发 hover 的时候都会重新加载

+

解决:可以用 JavaScript 设置 ie6 缓存这些图片:

+
1
document.execCommand("BackgroundImageCache", false, true);
+

解决在 IE6 下,列表与日期错位的问题

日期 标签放在标题 标签之前即可
image

+

解决 IE6 不支持 min-height 属性的问题

1
2
min-height: 350px;
_height: 350px;
+ +

让 IE7 IE8 支持 CSS3 background-size属性

由于 background-size 是 CSS3 新增的属性,所以 IE 低版本自然就不支持了,但是老外写了一个 htc 文件,名叫 background-size polyfill,使用该文件能够让 IE7、IE8 支持 background-size 属性。其原理是创建一个 img 元素插入到容器中,并重新计算宽度、高度、left、top 等值,模拟 background-size 的效果。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
html {
height: 100%;
}
body {
height: 100%;
margin: 0;
padding: 0;
background-image: url('img/37.png');
background-repeat: no-repeat;
background-size: cover;
-ms-behavior: url('css/backgroundsize.min.htc');
behavior: url('css/backgroundsize.min.htc');
}
+

IE6-7 line-height 失效的问题

问题:在ie 中 img 与文字放一起时,line-height 不起作用

+

解决:都设置成 float

+

td 自动换行的问题

问题:table 宽度固定,td 自动换行

+

解决:设置 Table 为 table-layout: fixedtdword-wrap: break-word

+

让层显示在 FLASH 之上

想让层的内容显示在 flash 上,把 FLASH 设置透明即可

+
1
2
1、<param name=" wmode " value="transparent" />
2、<param name="wmode" value="opaque"/>
+

键盘事件 keyCode 兼容性写法

1
2
3
4
5
6
7
8
9
10
11
var inp = document.getElementById('inp')
var result = document.getElementById('result')

function getKeyCode(e) {
e = e ? e : (window.event ? window.event : "")
return e.keyCode ? e.keyCode : e.which
}

inp.onkeypress = function(e) {
result.innerHTML = getKeyCode(e)
}
+ +

求窗口大小的兼容写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 浏览器窗口可视区域大小(不包括工具栏和滚动条等边线)
// 1600 * 525
var client_w = document.documentElement.clientWidth || document.body.clientWidth;
var client_h = document.documentElement.clientHeight || document.body.clientHeight;

// 网页内容实际宽高(包括工具栏和滚动条等边线)
// 1600 * 8
var scroll_w = document.documentElement.scrollWidth || document.body.scrollWidth;
var scroll_h = document.documentElement.scrollHeight || document.body.scrollHeight;

// 网页内容实际宽高 (不包括工具栏和滚动条等边线)
// 1600 * 8
var offset_w = document.documentElement.offsetWidth || document.body.offsetWidth;
var offset_h = document.documentElement.offsetHeight || document.body.offsetHeight;

// 滚动的高度
var scroll_Top = document.documentElement.scrollTop||document.body.scrollTop;
+

DOM 事件处理程序的兼容写法(能力检测)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
var eventshiv = {
// event兼容
getEvent: function(event) {
return event ? event : window.event;
},

// type兼容
getType: function(event) {
return event.type;
},

// target兼容
getTarget: function(event) {
return event.target ? event.target : event.srcelem;
},

// 添加事件句柄
addHandler: function(elem, type, listener) {
if (elem.addEventListener) {
elem.addEventListener(type, listener, false);
} else if (elem.attachEvent) {
elem.attachEvent('on' + type, listener);
} else {
// 在这里由于.与'on'字符串不能链接,只能用 []
elem['on' + type] = listener;
}
},

// 移除事件句柄
removeHandler: function(elem, type, listener) {
if (elem.removeEventListener) {
elem.removeEventListener(type, listener, false);
} else if (elem.detachEvent) {
elem.detachEvent('on' + type, listener);
} else {
elem['on' + type] = null;
}
},

// 添加事件代理
addAgent: function (elem, type, agent, listener) {
elem.addEventListener(type, function (e) {
if (e.target.matches(agent)) {
listener.call(e.target, e); // this 指向 e.target
}
});
},

// 取消默认行为
preventDefault: function(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
},

// 阻止事件冒泡
stopPropagation: function(event) {
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
}
};
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:浏览器兼容性问题解决方案

+

文章作者:

+

发布时间:2017年08月29日 - 17:04

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2017/08/29/browser-incompatibility-problem-solution/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2017/08/30/Vue-VSCode-Snippets/index.html b/en/2017/08/30/Vue-VSCode-Snippets/index.html new file mode 100644 index 0000000000..b4c171ba6c --- /dev/null +++ b/en/2017/08/30/Vue-VSCode-Snippets/index.html @@ -0,0 +1,777 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Vue VSCode Snippets 自动生成Vue代码片段的VSCode扩展 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Vue VSCode Snippets 自动生成Vue代码片段的VSCode扩展 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

现代化的IDE已经把开发者变得越来越懒,但是我觉得这样挺好的,让工具完成手工反复重复的工作,提高工作效率的同时降低开发者的疲劳感,Vue VSCode Snippets就是这样一个VSC插件,它可以用简单的几个字母就敲出一整块代码片段,在学习和做VUE项目时可以极大地提高工作效率,下面就介绍一下插件的常见命令。

+ + +

此插件可用比较简单的写法生成代码片段,非常适合开发工作,减少代码工作量。

+

Script

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SnippetPurpose
vbaseSingle file component base
vbaseSingle file component base
vdataComponent data as a function
vmethodVue method
vcomputedVue computed property
vwatcherVue watcher with new and old value args
vpropsProps with type and default
vimportImport one component into another
vimport-cImport one component into another within the export statement
vimport-exportImport one component into another and use it within the export statement
vfilterVue filter
vmixinCreate a Vue Mixin
vmixin-useBring a mixin into a component to use
vc-directVue create a custom directive
vimport-libImport a library
vimport-gsapImport GreenSock with Timeline and Eases
vanimhook-jsUsing the Transition component JS hooks in methods
+ +

Template

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SnippetPurpose
vforv-for directive
vmodelSemantic v-model directive
vmodel-numSemantic v-model number directive
vonv-on click handler with arguments
vel-propsComponent element with props
vsrcImage src binding
vstyleInline style binding
vstyle-objInline style binding with objects
vclassClass binding
vclass-objClass binding with objects
vclass-obj-multMultiple conditional class bindings
vanimTransition component with JS hooks
+

Vuex

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
SnippetPurpose
vstoreBase for Vuex store.js
vgettersVuex Getter
vmutationVuex Mutation
vactionVuex Action
vstore-importImport vuex store into main.js
+

Extra (plaintext)

+ + + + + + + + + + + + + + + + + + + +
SnippetPurpose
gitignore.gitignore file presets
vincincrementer
vdecdecrementer
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Vue VSCode Snippets 自动生成Vue代码片段的VSCode扩展

+

文章作者:

+

发布时间:2017年08月30日 - 16:23

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2017/08/30/Vue-VSCode-Snippets/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2017/08/30/Vue-basic/index.html b/en/2017/08/30/Vue-basic/index.html new file mode 100644 index 0000000000..380beaa7eb --- /dev/null +++ b/en/2017/08/30/Vue-basic/index.html @@ -0,0 +1,739 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Vue基础 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Vue基础 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近公司技术选型选择了VUE,下面就记录一下在学习Vue中遇到的一些知识点,有些在官方手册里已经讲的很详细了,这里就简单带过,主要对cli产生的以及自行添加的一些项目目录进行一下解释,希望对正在学习Vue的同学提供一点帮助,如果本文有谬误之处还请各位指出,谢谢各位支持。

+ +

搭建项目

1
2
3
4
5
6
7
npm install --global vue-cli
vue init webpack my-project
cd my-project
npm install(推荐用cnpm install)
如果没有cnpm ,先安装cnpm镜像
npm install -g cnpm --registry=https://registry.npm.taobao.org
npm run dev
+

目录讲解

+
    +
  • build和config :项目开发和打包时候的相关配置;
  • +
  • node_modules :项目所需要的依赖文件;
  • +
  • src :主应用/页面相关文件;
      +
    • assets : 静态资源文件;
    • +
    • components :组件;
    • +
    • res:资源
        +
      • css: 公共css或是css预处理文件;
      • +
      • js: 公共js文件
      • +
      • img:公共图片
      • +
      +
    • +
    • router :路由配置文件;
    • +
    • views : 视图文件,其实也是vue组件。按照业务功能划分模块;
    • +
    • vuex : 状态管理的配置文件;
    • +
    • App.vue : 主组件;
    • +
    • main.js: 入口文件,初始化vue实例并使用需要的插件
    • +
    +
  • +
  • index.html : 主html页面;
  • +
  • dist:webpack打包生成的文件;
  • +
  • package.json:记录依赖相关信息
  • +
+
+

文件的加载顺序:

当我们执行命令 npm run dev的时候根据配置文件dev-server.js里的相关配置去加载webpack的相关配置文件 在webpack.base.conf里面entry入口文件就配置了app:'./src/main.js'

+

所以当我们在运行npm run dev的时候就开始通过main.js执行了。main.js 初始化vue实例并且加载相关配置插件,然后通过app.vue文件去访问各个组件

+

Build/dev-server.js主要完成以下几件事情:

    +
  1. 检查node和npm的版本;
  2. +
  3. 引入相关插件和配置;
  4. +
  5. 创建express服务器和webpack编译器;
  6. +
  7. 配置开发中间件(webpack-dev-middleware)和热重载中间件(webpack-hot-middleware);
  8. +
  9. 挂载代理服务和中间件;
  10. +
  11. 配置静态资源;
  12. +
  13. 启动服务器监听特定端口(8080);
  14. +
  15. 自动打开浏览器并打开特定网址(localhost:8080);
  16. +
+

Build/huild.js主要完成以下几件事情:

    +
  1. loading动画;
  2. +
  3. 删除创建目标文件夹;
  4. +
  5. webpack编译;
  6. +
  7. 输出信息
  8. +
+

配置文件

.babelrc

设置转码的规则和插件(使用es6语法必须安装插件)

+
1
npm install babel-preset-es2015
+ +

presets 字段是用来设定转码规则;

+

.editorconfig

配置文件编码格式的文件

+
    +
  • indent_style:  设置缩进风格,tab或者空格;
  • +
  • indent_size:  缩进的宽度;
  • +
  • tab_width:  设置tab的列数。默认是indent_size;
  • +
  • end_of_line: 换行符,lf、cr和crlf;
  • +
  • charset:  编码;
  • +
  • trim_trailing_whitespace: 设为true表示会除去换行行首的任意空白字符;
  • +
  • insert_final_newline:  设为true表明使文件以一个空白行结尾;
  • +
  • root: 表明是最顶层的配置文件,发现设为true时,才会停止查找.editorconfig文件;
  • +
+

.eslintignore

忽略不符合eslint规范的文件, (一般会忽略掉第三方引用的插件)

+

.eslintrc.js

定义代码规则

+

.gitignore

配置文件,用于配置不需要加入版本管理的文件

+

.VUE 文件解释

    +
  • template: 展示模板
  • +
  • import : 导入组件已经js文件
  • +
  • export default:
      +
    • data:数据源;
    • +
    • methods:方法;
    • +
    • mounted:页面加载之后执行的方法;
    • +
    • created:页面生成时加载的方法;
    • +
    +
  • +
  • style: 样式代码 其中scoped表示样式作用范围为本vue文件
  • +
+

网络访问

axios

    +
  1. 发送请求:
  2. +
+
1
2
3
4
5
6
7
axios#request(config);
axios#get(url[, config]);
axios#delete(url[, config]);
axios#head(url[, config]);
axios#post(url[, data[, config]]);
axios#put(url[, data[, config]]);
axios#patch(url[, data[, config]]);
+ +
    +
  1. 处理响应:
  2. +
+
    +
  • Promise语法;
  • +
  • 处理结果:then;
  • +
  • 处理异常:catch;
  • +
+
    +
  1. 拦截器(use/reject):
  2. +
+
1
2
3
axios.interceptors.response.use;
axios.interceptors.rquest.use;
reject(移除请求拦截)
+ +
    +
  1. 参数:
  2. +
+
    +
  • json(默认);
  • +
  • qs;
  • +
+

组件通信

    +
  • Prpos:父组件对子组件;
  • +
  • 自定义事件:子组件对父组件;
  • +
  • 消息总线:任意两个组件;
  • +
  • 状态管理:Vuex(适用于大型单页面开发)
  • +
+

路由

    +
  1. 配置
  2. +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Vue.use(Router)

export default new Router({
routes: [
{
path: '/',
name: 'Hello',
component: Hello
}
]
})

new Vue({
el: '#app',
router,
template: '<App/>',
components: { App }
})
+ +
    +
  1. 导航
  2. +
+
    +
  • push
  • +
  • replace
  • +
  • go
  • +
+
    +
  1. 参数传递
  2. +
+
    +
  • RESTful url参数
  • +
  • 参数查询 query
  • +
  • 锚点 hash: ‘#data’
  • +
+
    +
  1. 嵌套路由
  2. +
+
    +
  • Children
  • +
+
    +
  1. 钩子
  2. +
+
    +
  • beforeRouteEnter
  • +
  • beforeRouteLeave
  • +
+

状态管理

+

Vuex是什么?

+

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

+
+
    +
  • state里面就是存放的我们所要用到的状态;
  • +
  • mutations就是存放如何更改状态的方法 ,同步操作;
  • +
  • getters就是从state中派生出状态,比如将state中的某个状态进行过滤然后获取新的状态。
  • +
  • actions就是mutation的加强版,它可以通过commit
  • +
  • mutations中的方法来改变状态,最重要的是它可以进行异步操作。
  • +
  • modules顾名思义,就是当用这个容器来装这些状态还是显得混乱的时候,我们就可以把容器分成几块,把状态和管理规则分类来装。这和我们创建js模块是一个目的,让代码结构更清晰。
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Vue基础

+

文章作者:

+

发布时间:2017年08月30日 - 17:03

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2017/08/30/Vue-basic/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2017/08/30/npm-source/index.html b/en/2017/08/30/npm-source/index.html new file mode 100644 index 0000000000..f02f674bcd --- /dev/null +++ b/en/2017/08/30/npm-source/index.html @@ -0,0 +1,608 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + npm相关资料 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ npm相关资料 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

近两年前端突然呈爆发增长之势,node的出现起了重要的作用,而node之中有一个重要的功能就是npm包管理工具,下面就记录一下我学习npm之中遇到的一些问题及解决方案,仅供各位参考。

+ + +
+

npm全称Node Package Manager,是node.js的模块依赖管理工具。由于npm的源在国外,所以国内用户使用起来各种不方便。下面整理出了一部分国内优秀的npm镜像资源,国内用户可以选择使用。

+
+

国内优秀npm镜像

淘宝npm镜像

+

cnpmjs镜像

+

如何使用

有很多方法来配置npm的registry地址,下面根据不同情境列出几种比较常用的方法。以淘宝npm镜像举例:

+

1.临时使用

1
npm --registry https://registry.npm.taobao.org install express
+

2.持久使用

1
2
3
4
5
6
npm config set registry https://registry.npm.taobao.org

// 配置后可通过下面方式来验证是否成功
npm config get registry
// 或
npm info express
+

3.通过cnpm使用

1
2
3
4
npm install -g cnpm --registry=https://registry.npm.taobao.org

// 使用
cnpm install express
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:npm相关资料

+

文章作者:

+

发布时间:2017年08月30日 - 12:24

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2017/08/30/npm-source/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2017/10/11/use-GitHub-Hexo-Next-make-blog/index.html b/en/2017/10/11/use-GitHub-Hexo-Next-make-blog/index.html new file mode 100644 index 0000000000..f13245d11b --- /dev/null +++ b/en/2017/10/11/use-GitHub-Hexo-Next-make-blog/index.html @@ -0,0 +1,658 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 如何利用GitHub-Hexo-Next搭建一个漂亮的技术博客 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 如何利用GitHub-Hexo-Next搭建一个漂亮的技术博客 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

+

开博客貌似并不是一件难事,现在有很多的方式开博客,而且现在博客貌似也不再流行,但是作为一个开发者,维护一个技术博客对自己的发展是很有好处的,出名者如阮一峰等,所以还是想要做一个技术博客的。

+
+
+

开博客可以选择网上现有的博客系统如博客园以及其他各大公司提供的博客系统,也可以选择如wordpress等博客程序自行搭建博客系统,前者省事,但可自定义设置不足不符合一个爱折腾的人的性格,后者复杂些,爱折腾的可以学着搭建,但是服务器域名是个让人头疼的事,还有数据库等操作,着实麻烦,如今又有了另外一个选择,那就是利用GitHub和一些静态的博客系统搭建一个纯静态的博客系统,不花钱又能折腾,而且还是命令行下的,装B感觉棒棒的,另外搭配时下流行的MarkDown语法,做笔记正合我意。

+
+ + +

技术栈选型

+

这里说是技术栈选型可能并不是很恰当,但又找不到合适的描述,就是把需要的技术介绍一下,如果还不会的,可以自行学习,或者看看我的其他文章。

+
+
    +
  • node(npm),现在node这么火,没用过都不好意思出门,但是如果你还不回的话,就先自行学习安装一下吧。
  • +
  • Hexo 静态博客程序,其实还有很多,只不过这个比较新,而且搭配Next非常漂亮,就选了它。
  • +
  • Next 可以说是Hexo的定制系统,不仅仅是做了个皮肤,简洁美观的配置项和官网说明深得我心。
  • +
+

搭建步骤(安装步骤)

安装Hexo Hexo官网

1
2
3
4
5
$ npm install hexo-cli -g // 安装hexo的脚手架工具
$ hexo init blog // 初始化博客
$ cd blog // 返回博客目录
$ npm install // 安装依赖
$ hexo server // 启动博客
+
+

怎么样五行代码就生成并运行了一个博客是不是超简单。
下面我们看一下生成的博客的目录

+
+
1
2
3
4
5
6
7
8
.
├── _config.yml
├── package.json
├── scaffolds
├── source
| ├── _drafts
| └── _posts
└── themes
+

_config.yml这是博客的配置文件,比如博客名称,副标题,作者等信息都在这个文件里设置。

+

package.json这是博客的依赖文件可以忽略

+

scaffolds这是博客的模板目录,当你要写一篇文章时,这里会有文章的默认类型。

+

source这是博客的网站资源,包括发布的文章(_posts)、关于、分类还有上传文件等。

+

themes这是博客的皮肤。

+

更多配置信息请查阅官网手册

+

安装Next主题 Next官网

1
2
$ cd your-hexo-site
$ git clone https://github.com/iissnan/hexo-theme-next themes/next
+ +

然后到_config.yml配置文件将主题配置改成next就可以使用next的皮肤了

+
1
theme: next
+

皮肤也有配置文件,为跟Hexo进行区分Hexo的配置文件称为站点配置文件, 皮肤配置文件称为主题配置文件

+

对两个配置文件进行简单配置后,符合需求的博客就搭建而成了,这里有个友好的建议,配置文件如果配置不正确将不能正确运行博客,所以在配置前务必保留好原始配置文件,注意配置时不要缺了空格,不要问我为什么知道这个。

+

更新博客主题

https://github.com/theme-next/hexo-theme-next/blob/master/docs/zh-CN/UPDATE-FROM-5.1.X.md

+

写博客

博客已经搭建好了,接下来就是写博客了,那么如何开始写博客呢,超级简单一行命令足以:

+
1
2
3
4
hexo new [layout] <title>  
// layout 为模板类型可以省略,title为文章标题,通常可以简写为如下
hexo new title

+

新建命令执行后在_posts的目录下就会生成一个你刚才命名的md后缀的文件,这就是一个MarkDown语法的文件,(如果不了解MarkDown语法的可以去学一下,很简单的符号语言,或者像我一样用支持MarkDown语法的编辑器来写文章。新建的文章打开内容如下

+
1
2
3
4
5
6
---
title: new-post
date: 2017-10-11 15:01:09
tags:
---

+

非常好理解,title就是标题,date为创建时间,tags是标签方便分类,但是这些并不全,还有些常用的分类没有写上,下面我将常用的进行补充

+
1
2
3
4
5
6
7
8
9
10
11
---
title: new-post
date: 2017-10-11 15:01:09
categories:
- NodeJS
- npm
tags:
- npm
- NodeJS
- rnpm
---
+

这样补充后就有了常见博客的分类和标签的功能,是不是很简单。

+

写完文章以后还要执行下面命令,生成静态页面

+
1
2
$ hexo generate // 将md后缀文件生成成静态html文件

+ + +

这样我们就完成了博客的搭建和博客的书写,到现在我们就已经有了一个本地的博客,那么如何将博客上传到GitHub上呢?

+

将Blog上传至GitHub

github是一个代码托管的平台,为了方便描述代码功能,它提供了README.md文件进行说明,但是为了更好的展现,也提供了gitpage的功能,博客是基于这个功能进行的扩展,那么如何用gitpage的功能来实现博客系统呢?

+

创建仓库

创建一个以你的GitHub账号为开头命名的仓库,格式如下

+
1
2
3
GitHub账号名称.github.io
// 如
lixuguang.github.io
+

然后到blog系统的配置文件_config.yml里配置一下上传路径

+
1
2
3
4
5
6
7
8
9
10
deploy:
type: git
repo: <repository url>

// 我的实例
deploy:
type: git
repo: git@github.com:lixuguang/lixuguang.github.io.git
branch: master

+

配置好就可以进行部署了,部署也很简单,只需要执行一下下面的命令。

+
1
2
3
4
$ npm install hexo-deployer-git --save // 安装上传工具

$ hexo deploy

+

稍等一会,如果没有出现什么错误信息,那么你的部署就成功了。之后你就可以访问你的博客了,博客地址如下:
https://你的github账号.github.io/
我的如下:
https://lixuguang.github.io/

+

现在你是不是已经学会如何利用github搭建一个静态的博客系统了呢,如果你还没有一个自己的技术博客,快来试试吧。

+

技巧

是不是觉得命令行还是挺麻烦的,要敲那么一大串字母,哈哈实际上这些常用命令是有缩写方式的,下面给大家介绍一下缩写方式。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ hexo server
// 简写
$ hexo s

$ hexo generate
// 简写
$ hexo g

$ hexo deploy
// 简写
$ hexo d

$ hexo new
// 简写
$ hexo n
+ +

另外每次发布之前最好执行以下命令,清理当前内容

+
1
2
$ hexo clean

+

以防出现冲突的情况,具体动作如下

+
1
2
3
4
$ hexo clean
$ hexo g -d // 文件生成后立即部署网站
$ hexo d -g // 部署之前预先生成静态文件

+ +

常见问题

    +
  1. SSH问题
  2. +
+
1
2
3
$ ssh-keygen -t rsa -C "lixuguang316@gmail.com"
// 填写你自己的github邮箱

+

敲三下回车,之后会在

+
1
2
C:\Users\Administrator\.ssh // windows下
open ~/.ssh // Mac下打开ssh文件
+

文件夹下生成两个文件id_rsa(私钥)、id_rsa.pub(公钥),在github上的SSH处添加新的ssh,然后将公钥内容贴到上面起个名字可以叫hexo,保存,然后在git bash下敲击

+
1
$ ssh git@github.com
+

然后敲yes就可以上传blog代码了
怎么样会了么?更多高阶玩法请阅读官方说明文档,文章如有谬误之处请各位指出,如果觉得文章对你有所帮助我将十分开心,如果你喜欢我的文章可以到我的github上点个fork,谢谢你的阅读。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:如何利用GitHub-Hexo-Next搭建一个漂亮的技术博客

+

文章作者:

+

发布时间:2017年10月11日 - 16:18

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2017/10/11/use-GitHub-Hexo-Next-make-blog/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2017/10/12/VSCode-ESLint/index.html b/en/2017/10/12/VSCode-ESLint/index.html new file mode 100644 index 0000000000..9adf1eb2b4 --- /dev/null +++ b/en/2017/10/12/VSCode-ESLint/index.html @@ -0,0 +1,597 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + VSCode ESLint JS代码静态检测工具 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ VSCode ESLint JS代码静态检测工具 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

VSCode是新一代IDE,方便灵活的插件安装方式让它强大无比,对前端友好,让它成为了越来越多前端开发者的首选IDE,编辑器虽好,但是要自己安装符合自己需求的插件才行,下面介绍一款插件,名叫ESLint,是用来静态检测js代码的工具,让你的js代码在编辑过程中就能找到错误,提高代码质量,好了废话不多说下面介绍如何安装和配置这款插件。

+ +

安装ESLint插件

打开VSCode编辑器,在左侧右下角有一个安装插件的图标,点击后就可以打开插件市场,输入ESLint,就会有个黄色的图标出现在你面前,不用犹豫双击它,稍等一会它就安装完了,是不是超简单。

+

安装NPM依赖

ESLint插件运行需要一些依赖,对于用过npm包管理工具的人来讲小意思啦,我把代码放到下面,需要的直接粘贴运行就好。

+
1
2
3
4
5
6
7
8
//全局安装eslint
npm i eslint -g

//如果用到html中的js校验
npm i eslint-plugin-html -g

//如果用到es2015语法
npm i babel-eslint -g
+

配置eslint配置文件到项目根目录

配置文件名称如下:
eslintrc.json
内容为:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
{
"plugins": [
// "react",
"html"
],
"env": {
"node": true,
"jquery": true,
"es6": true,
"browser": true
},
"globals": {
"angular": false
},
"parser": "babel-eslint",
"rules": {
//官方文档 http://eslint.org/docs/rules/
//参数:0 关闭,1 警告,2 错误
// "quotes": [0, "single"], //建议使用单引号
// "no-inner-declarations": [0, "both"], //不建议在{}代码块内部声明变量或函数
"no-extra-boolean-cast": 1, //多余的感叹号转布尔型
"no-extra-semi": 1, //多余的分号
"no-extra-parens": 0, //多余的括号
"no-empty": 1, //空代码块

//使用前未定义
"no-use-before-define": [
0,
"nofunc"
],

"complexity": [0, 10], //圈复杂度大于*

//定义数组或对象最后多余的逗号
"comma-dangle": [
0,
"never"
],

// 不允许对全局变量赋值,如 window = 'abc'
"no-global-assign": ["error", {
// 定义例外
// "exceptions": ["Object"]
}],
"no-var": 0, //用let或const替代var
"no-const-assign": 2, //不允许const重新赋值
"no-class-assign": 2, //不允许对class重新赋值
"no-debugger": 1, //debugger 调试代码未删除
"no-console": 0, //console 未删除
"no-constant-condition": 2, //常量作为条件
"no-dupe-args": 2, //参数重复
"no-dupe-keys": 2, //对象属性重复
"no-duplicate-case": 2, //case重复
"no-empty-character-class": 2, //正则无法匹配任何值
"no-invalid-regexp": 2, //无效的正则
"no-func-assign": 2, //函数被赋值
"valid-typeof": 1, //无效的类型判断
"no-unreachable": 2, //不可能执行到的代码
"no-unexpected-multiline": 2, //行尾缺少分号可能导致一些意外情况
"no-sparse-arrays": 1, //数组中多出逗号
"no-shadow-restricted-names": 2, //关键词与命名冲突
"no-undef": 1, //变量未定义
"no-unused-vars": 1, //变量定义后未使用
"no-cond-assign": 2, //条件语句中禁止赋值操作
"no-native-reassign": 2, //禁止覆盖原生对象
"no-mixed-spaces-and-tabs": 0,



//代码风格优化 --------------------------------------
"no-irregular-whitespace": 0,
"no-else-return": 0, //在else代码块中return,else是多余的
"no-multi-spaces": 0, //不允许多个空格

//object直接量建议写法 : 后一个空格前面不留空格
"key-spacing": [
0,
{
"beforeColon": false,
"afterColon": true
}
],

"block-scoped-var": 1, //变量应在外部上下文中声明,不应在{}代码块中
"consistent-return": 1, //函数返回值可能是不同类型
"accessor-pairs": 1, //object getter/setter方法需要成对出现

//换行调用对象方法 点操作符应写在行首
"dot-location": [
1,
"property"
],
"no-lone-blocks": 1, //多余的{}嵌套
"no-labels": 1, //无用的标记
"no-extend-native": 1, //禁止扩展原生对象
"no-floating-decimal": 1, //浮点型需要写全 禁止.1 或 2.写法
"no-loop-func": 1, //禁止在循环体中定义函数
"no-new-func": 1, //禁止new Function(...) 写法
"no-self-compare": 1, //不允与自己比较作为条件
"no-sequences": 1, //禁止可能导致结果不明确的逗号操作符
"no-throw-literal": 1, //禁止抛出一个直接量 应是Error对象

//不允return时有赋值操作
"no-return-assign": [
1,
"always"
],

//不允许重复声明
"no-redeclare": [
1,
{
"builtinGlobals": true
}
],

//不执行的表达式
"no-unused-expressions": [
0,
{
"allowShortCircuit": true,
"allowTernary": true
}
],
"no-useless-call": 1, //无意义的函数call或apply
"no-useless-concat": 1, //无意义的string concat
"no-void": 1, //禁用void
"no-with": 1, //禁用with
"space-infix-ops": 0, //操作符前后空格

//jsdoc
"valid-jsdoc": [
0,
{
"requireParamDescription": true,
"requireReturnDescription": true
}
],

//标记未写注释
"no-warning-comments": [
1,
{
"terms": [
"todo",
"fixme",
"any other term"
],
"location": "anywhere"
}
],
"curly": 0 //if、else、while、for代码块用{}包围
}
}
+

eslint就是根据这个配置表来进行js语法校验的。

+

最后重启VSCode完成插件安装

重启后控制台显示ESLint server is running说明插件已经生效,好啦接下来就愉快的写代码吧。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:VSCode ESLint JS代码静态检测工具

+

文章作者:

+

发布时间:2017年10月12日 - 14:42

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2017/10/12/VSCode-ESLint/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2017/10/12/us-gitment/index.html b/en/2017/10/12/us-gitment/index.html new file mode 100644 index 0000000000..94115aafc0 --- /dev/null +++ b/en/2017/10/12/us-gitment/index.html @@ -0,0 +1,620 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 给博客添加基于github-issue的评论系统 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 给博客添加基于github-issue的评论系统 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

上篇文章介绍了如何利用github+hexo+next制作一个博客系统,但是由于这个博客系统是基于静态页面的,所以无法完成评论功能,博客怎么可以没有评论功能呢,当然next主题提供了几个选项,但是国内你知道的,提供的几个评论功能都不是太好用,于是就上网找了好多资料,功夫不负有心人,让我找到了一个名曰gitment的基于github-issue的评论插件,下面就介绍大家如何使用这个插件。

+ +

第一步 注册一个小程序(OAuth Application)

点击https://github.com/settings/applications/new注册

+
    +
  • Application name 应用名称 这里随便写,我写的就是gitment
  • +
  • Homepage URL 主页地址,你可以写你的博客地址,我写的是https://lixuguang.github.io/
  • +
  • Application description 应用描述,这里随便写点什么,反正是自己用。
  • +
  • Authorization callback URL 这个比较重要,请填写你的博客地址,我的是https://lixuguang.github.io/
  • +
+

点击确定以后你会获得两个关键信息,下一步配置时会用到

+
    +
  • Client ID
  • +
  • Client Secret
  • +
+

第二步 修改主题配置文件,添加gitment评论功能

因为用的是next主题,所以配置文件地址如下:
themes/next/_config.yml

+

1、在其中添加:

1
2
3
4
5
6
7
8
9
# Gitment
# Introduction: https://imsun.net/posts/gitment-introduction/
gitment:
enable: true
githubID: yourid // 我的是lixuguang
repo: yourrepo // 我的是lixuguang.github.io 必须跟githubID保持一致的用户名
ClientID: yourid // 上面开通程序获得的ClientID
ClientSecret: yoursecret // 上面开通程序获得的Client Secret
lazy: false //是否需要点击展开评论才能可见评论,一般设置为false
+

一定要注意空格,不然会报错的,别问我咋知道的

+

2、然后在主题的配置语言环境的文件添加一句话

en.yml增加:

+
1
gitmentbutton: Show comments from Gitment
+ +

zh-Hans.yml增加:

+
1
gitmentbutton: 显示 Gitment 评论
+

如果是中文网站英文配置也可以不用写。

+

3、添加新的Dom结构

修改主题layout/_partials/comments.swig
在最后一个elseif分支后添加一个elseif分支:

+
1
2
3
4
5
6
7
{% elseif theme.gitment.enable %}
{% if theme.gitment.lazy %}
<div onclick="ShowGitment()" id="gitment-display-button">{{ __('gitmentbutton') }}</div>
<div id="gitment-container" style="display:none"></div>
{% else %}
<div id="gitment-container"></div>
{% endif %}
+

4、 在主题下layout/_third-party/comments/目录下中添加文件gitment.swig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{% if theme.gitment.enable %}
{% set owner = theme.gitment.githubID %}
{% set repo = theme.gitment.repo %}
{% set cid = theme.gitment.ClientID %}
{% set cs = theme.gitment.ClientSecret %}
<link rel="stylesheet" href="https://imsun.github.io/gitment/style/default.css">
<script src="https://imsun.github.io/gitment/dist/gitment.browser.js"></script>
{% if not theme.gitment.lazy %}
<script type="text/javascript">
var gitment = new Gitment({
id: window.location.pathname,
owner: '{{owner}}',
repo: '{{repo}}',
oauth: {
client_id: '{{cid}}',
client_secret: '{{cs}}',
}});
gitment.render('gitment-container');
</script>
{% else %}
<script type="text/javascript">
function ShowGitment(){
document.getElementById("gitment-display-button").style.display = "none";
document.getElementById("gitment-container").style.display = "block";
var gitment = new Gitment({
id: document.location.href,
owner: '{{owner}}',
repo: '{{repo}}',
oauth: {
client_id: '{{cid}}',
client_secret: '{{cs}}',
}});
gitment.render('gitment-container');
}
</script>
{% endif %}
{% endif %}
+

然后在主题下layout/_third-party/comments/index.swig文件中引入gitment.swig文件:

+
1
{% include 'gitment.swig' %}
+

在主题下source/css/_common/components/third-party/目录下添加gitment.styl文件

此配置文件为gitment的样式文件,需要修改样式可以在这里进行书写,这里修改一下按钮样式,另外将聊天框于文章框样式统一

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#gitment-display-button{
display: inline-block;
padding: 0 15px;
color: #0a9caf;
cursor: pointer;
font-size: 14px;
border: 1px solid #0a9caf;
border-radius: 4px;
}
#gitment-display-button:hover{
color: #fff;
background: #0a9caf;
}
#comments {
margin: 0;
padding: 40px;
background: #fff;
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.06), 0 1px 5px 0 rgba(0,0,0,0.12);
}
+

然后在主题下source/css/_common/components/third-party/third-party.styl文件中引入相应的CSS样式即可:

+
1
@import "gitment";
+

经过以上操作,gitment就被引入到你的博客里了。

+

现在就可以让大家对你写的文章进行评论啦,怎么样是不是又学到啦,喜欢我的文章就请关注我的github吧。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:给博客添加基于github-issue的评论系统

+

文章作者:

+

发布时间:2017年10月12日 - 10:50

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2017/10/12/us-gitment/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2017/10/12/web-quanzhan/index.html b/en/2017/10/12/web-quanzhan/index.html new file mode 100644 index 0000000000..506e8acef7 --- /dev/null +++ b/en/2017/10/12/web-quanzhan/index.html @@ -0,0 +1,606 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《Web全栈工程师的自我修养》读书笔记 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《Web全栈工程师的自我修养》读书笔记 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

近日工作不是那么忙,所以有时间看看书,这本书之前就已经读过一遍,隔了一段时间已经有所忘记,所以再次拾起,重新回顾一下。.

+ + +

什么是全栈

“全栈”是个外来词,翻译自英文full-stack,此处的栈指的是为了完成项目而使用的一系列技术的合集,不是堆栈概念中的栈。

+

“全端”工程师是指能够完成pc端、移动端等多终端设备适配的情况

+

什么是全栈工程师

+

全栈工程师是指一个能够处理数据库、服务器、系统工程、客户端等所有工作的的工程师,根据项目不同,可能是移动栈、Web栈,或者原生应用程序栈。

+
+

简单来说全栈工程师就是一个人能搞定一个项目,全能大神一样的人物。

+

一个Web产品典型的技术栈

+

服务器+数据库+服务器端编程语言+前端编程语言

+
+
+

全栈工程师技术的兴起有两个重要原因:技术的发展和PaaS(Platform as a Service,平台即服务)服务的平台越来越多。

+
+

全栈框架———MEAN

+

MongoDB-Express-AngularJs-Node.js
前后端采用一种编程语言JavaScript

+
+

全栈工程师的要求

一专多长

在一个领域里至少达到高级的级别,然后再去向上游或者下游延伸

+

关注商业目标

公司聘请你是为了让你产生利润,并不关心你会什么,所以选择技术栈时要考虑的是如何降低公司的成本或者提高收入。

+

关注用户体验

产品的最终目标是满足客户的需求,所以作为全栈工程师必须要关注用户体验。

+
+

这是一些作为全栈工程师我整理出来的干货,这本书本身并不是一本技术性很强的书,倒像是一位过来人介绍些经验,适合刚入职场或者进入职场不久的人,在前端领域比较迷茫时看一看,书中介绍了作者读过的一些书,很有参考性,推荐大家阅读。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:《Web全栈工程师的自我修养》读书笔记

+

文章作者:

+

发布时间:2017年10月12日 - 17:37

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2017/10/12/web-quanzhan/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2017/12/12/Git-Shell/index.html b/en/2017/12/12/Git-Shell/index.html new file mode 100644 index 0000000000..019f48d98f --- /dev/null +++ b/en/2017/12/12/Git-Shell/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Git的常用命令 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Git的常用命令 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

指令表

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
指令含义备注
git add .提示增加文件.代表所有
git commit -m“说明内容” 提交到本地服务器
git status显示修改信息
git pull从网络服务器拉 更新最新版本
git push上传最新版本
git branch查看当前分支
git checkout develop切换到develop模式
git merge master从master合并过来
git push origin develop提交
git clone git@192.168.2.10:bat-web.git从服务器克隆
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Git的常用命令

+

文章作者:

+

发布时间:2017年12月12日 - 12:20

+

最后更新:2019年12月31日 - 10:38

+

原始链接:https://blog.lifesli.com/2017/12/12/Git-Shell/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2019/12/27/Actions/index.html b/en/2019/12/27/Actions/index.html new file mode 100644 index 0000000000..4c016b3cd0 --- /dev/null +++ b/en/2019/12/27/Actions/index.html @@ -0,0 +1,607 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 巧妙利用Acitons进行博客的自动构建 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 巧妙利用Acitons进行博客的自动构建 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

忙活了很久很久,终于不那么忙了,也终于想起来我还有个博客没有运行,哎,虽然用Hexo可以享受Github提供的免费的空间和域名,但是命令行敲代码的方式实现博客编写还是有些不方便,尤其是需要编程环境的时候,最近公司在用CICD的方式对代码进行构建,我就在想有没有一种方式让博客也可以变成这样,于是我就在网上搜Github Hexo 自动构建 这些个词,还真让我搜到了一种方法而且还是Github自己提供的,下面隆重请出 GitHub Actions

+ + +

GitHub Actions 是什么

GitHub Actions 由 GitHub 官方推出的工作流工具。典型的应用场景应该是 CI/CD,类似 Travis 的用法。如果不知道 CI/CD&Travis 感兴趣的建议去了解一下,下面不展开说明,直接说怎么用就好。

+

前期准备

在使用 GitHub Actions 之前我们先来看看我们有什么;
首先我们有一个放博客程序的地方,我这里是叫做 blog-source ,另外呢有一个通过 hexo g 创建出来的静态网站,为了存放它而建的另一个仓库,我这里是叫做lixuguang.github.io,也就是说我们现在是有这样两个仓库。
|仓库|作用|
|-|-|
|blog-source|放博客源代码|
|lixuguang.github.io|放博客生成代码|

+

生成密钥

因为 GitHub Actions 它需要访问我的 blog-source 仓库的代码所以必须要有密钥,密钥大家应该熟悉了,创建博客的时候也是创建了一个公钥和私钥用来在本地往 lixuguang.github.io 这个仓库提交代码
这里呢我们用下面的命令生成密钥。

+
1
ssh-keygen -t ed25519 -f ~/.ssh/github-actions-deploy # 连按三次回车即刻
+

命令执行完成后,我们会得到两个文件 github-actions-deploygithub-actions-deploy.pub 两个文件,第一个是私钥,第二个是公钥。
|名称|解释|
|-|-|
|github-actions-deploy|私钥|
|github-actions-deploy.pub|公钥|

+

接下来的步骤一定要好好看,因为我在这个地方被卡住好多次,就是因为有的文章说的并不正确,或者至少是讲的不够仔细,这里我会仔细地说明一下。

+

配置 GitHub 仓库

配置博客源代码仓库

我这里的源代码是放在 blog-source 中,所以我现在要给源代码仓库配置私钥,配置过程如下:
打开 blog-source 仓库,选择 settings,然后选中 secrets , 再点击 Add new secrets,照着下面填写内容
|字段|值|
|-|-|
|Name|HEXO_DEPLOY_PRI(名称自动构建时有用)|
|Value|github-actions-deploy|

+

配置博客源代码仓库

我这里生成的博客静态代码是放在 lixuguang.github.io 中,所以我现在要给静态代码仓库配置公钥,配置过程如下:
打开 lixuguang.github.io,选择 settings,然后选中 keys,再点击 Add deploy key,照着下面填写内容
|字段|值|
|-|-|
|Title|HEXO_DEPLOY_PUB|
|Key|github-actions-deploy.pub|

+

编写 Actions 脚本

经过上面一系列的准备操作,终于来到了编写自动构建脚本的环节,构建脚本如下,如果按照上面我做的操作一步步来的话,那么这一步你可以直接copy啦

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
name: Deploy Blog

on: [push] # 当有新push时运行

jobs:
build: # 一项叫做build的任务

runs-on: ubuntu-latest # 在最新版的Ubuntu系统下运行

steps:
- name: Checkout # 将仓库内master分支的内容下载到工作目录
uses: actions/checkout@v1 # 脚本来自 https://github.com/actions/checkout

- name: Use Node.js 10.x # 配置Node环境
uses: actions/setup-node@v1 # 配置脚本来自 https://github.com/actions/setup-node
with:
node-version: "10.x"

- name: Setup Hexo env
env:
HEXO_DEPLOY_PRI: ${{ secrets.HEXO_DEPLOY_PRI }} # 这里是上面配置的私钥名称
run: |
# set up private key for deploy
mkdir -p ~/.ssh/
echo "$HEXO_DEPLOY_PRI" | tr -d '\r' > ~/.ssh/id_rsa # 配置秘钥
chmod 600 ~/.ssh/id_rsa
ssh-keyscan github.com >> ~/.ssh/known_hosts
# set git infomation
git config --global user.name 'lixuguang' # 换成你自己的名字
git config --global user.email 'lixuguang@gmail.com' # 换成你自己的邮箱
# install dependencies
npm i -g hexo-cli # 安装hexo
npm i

- name: Deploy
run: |
# publish
rm -rf .deploy_git # 如果上次构建失败这句命令会清除上次失败的代码
hexo generate && hexo deploy # 执行部署程序


+ +
+

通过以上这些步骤的操作,如果没什么意外的话,博客的自动构建就完成了,之后只要你提交新的文章到博客源代码仓库,它将自动帮你生成并发送到博客的静态代码仓库,再也不用执行hexo g -d啦,如果这篇文章对你有用,欢迎follow我或打赏一下这篇文章,感谢阅读。

+

ps:这里有个小坑需要注意一下,因为博客的皮肤也是另外一个git仓库,如果你在本地构建好用但是线上构建博客不显示了,需要注意下是不是皮肤没有上传到博客源码仓库,这里我遇到了,希望你不会因此困扰,拜拜~

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:巧妙利用Acitons进行博客的自动构建

+

文章作者:

+

发布时间:2019年12月27日 - 22:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2019/12/27/Actions/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2019/12/27/Vue-Component-Communication/index.html b/en/2019/12/27/Vue-Component-Communication/index.html new file mode 100644 index 0000000000..c220871081 --- /dev/null +++ b/en/2019/12/27/Vue-Component-Communication/index.html @@ -0,0 +1,615 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端面试Vue篇:Vue组件通信的几种方式 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端面试Vue篇:Vue组件通信的几种方式 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

公司这两年兴起了前后端分离的热潮,在选用三大技术框架时,vue凭借其学习曲线低深受公司程序员的喜爱,因此我们也就走上了vue学习的不归路,在招聘招揽小伙伴时有那么一些问题经常被问起,vue组件间通信就是其中之一,下面我们来聊一聊vue组件通信有哪些方式。

+ + +

props$emit

啥也不了解的小伙伴应该也知道这种方式吧,这是最最基础的通信方式了,父子组件通信基本都用它。父组件向子组件传递数据的时候通过prop传参,子组件中通过$emit传递给父组件,父组件在触发子组件$emit方法时得到子组件数据。实例如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 父组件 parent.vue

Vue.compinent('parent', {
template: `
<div>
<p>父组件</p>
<child :message="message" @getChildrenData="getChildrenData"></child>
</div>`,
data() {
return {
message:'Hello lixuguang'
}
},
methods: {
/**
* 执行子组件触发的事件方法
*/
getChildrenData(data){
console.log(data)
}
}
})

// 子组件 child.vue

Vue.component('child', {
props:['message'], // 得到父组件传过来的数据
data() {
return {
childMessage: this.message
}
},
template:`
<div>
<input type="text" v-model="childMessage" @input="emitParentData(childMessage)">
</div>
`,
methods: {
emitParentData(data) {
this.$emit('getChildrenData', data) // 父组件触发时给父组件传值
}
}
})

// App.vue
var app = new Vue({
el: "#app",
template: `
<div>
<parent></parent>
</div>
`
})
+

解析代码:

+
    +
  1. 父组件通过message属性将数据传递给子组件,并且通过getChildrenData事件来监听子组件出发的事件;
  2. +
  3. 子组件通过props获得父组件传过来的数据,并且通过this.$emit触发了getChildrenData事件;
  4. +
+

$attrs$listeners

前一种方法我们完成了父子组件的数据通信,那你有没有想过如果有多层嵌套的数据最上层要往最下层传值怎么办,前一种方法只能一层一层的往下传,可是这样太麻烦了,那么有没有什么方法能够一次传到你想要传到的位置呢,当然是有的,那就是接下来要说到的$attrs$listeners
假设我们现在有三层包含关系的组件,分别是level1/level2/level3,level1 > level2 > level3, > 表示包含关系。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// level3.vue

Vue.component('level3', {
template:`
<div>
<input type="text" v-model="$attrs.level3Message" @input="emitLevel3Data($attrs.level3Message)">
</div>
`,
methods: {
emitLevel3Data(data) {
this.$emit('getLevel3Data', data)
}
}
})

// level2.vue

Vue.component('level2', {
props:['level2Message'],
data(){
return {
level2Message:this.level2Message
}
},
template:`
<div>
<input type="text" v-model="level2Message" @input="emitLevel2Data(level2Message)">
<level3 v-bind='$attrs' v-on='$listenrs'></level3>
</div>
`,
methods: {
emitLevel2Data(data) {
this.$emit('getLevel2Data', data)
}
}
})

// level1.vue

Vue.component('level1', {
data(){
return {
level3Message:"I am Level3",
level2Message:"I am Level2"
}
},
template:`
<div>
<h1>这是level1中的内容</h1>
<level2 :level2Message="level2Message"
:level3Message="level3Message"
@getLevel2Data="getLevel2Data()"
@getLevel3Data="getLevel3Data()"
></level2>
</div>
`,
methods: {
getLevel2Data(data) {
console.log('这是来自level2的数据', data)
},
getLevel3Data(data) {
console.log('这是来自level3的数据', data)
}
}
})

// App.vue

var app = new Vue({
el: "#app",
template: `
<div>
<level1></level1>
</div>
`
})
+

解析代码:

+
    +
  1. level3组件能直接触发getLevel3Data是因为level2组件在调用level3组件时使用v-on绑定了$listeners属性;
  2. +
  3. 通过v-bind绑定了$attrs属性,level3组件可以直接获取到从level1传下来的props;
  4. +
+

v-model

父组件通过v-model传递值给子组件时,会自动传递一个value的props户型,在子组件中可以通过this.$emit(‘input’,val)自动修改v-model绑定的值。

+

```

+

未完待续~

+
+

作者:李旭光
引用请标明出处

+
+

前言

+
+

作者:李旭光
引用请标明出处

+
+

前言

+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端面试Vue篇:Vue组件通信的几种方式

+

文章作者:

+

发布时间:2019年12月27日 - 22:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2019/12/27/Vue-Component-Communication/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2019/12/30/begin-learn-java/index.html b/en/2019/12/30/begin-learn-java/index.html new file mode 100644 index 0000000000..0f3faf7189 --- /dev/null +++ b/en/2019/12/30/begin-learn-java/index.html @@ -0,0 +1,591 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 重拾java开发技能 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 重拾java开发技能 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近愈发觉得越想往上走,越不能局限在前端开发的领域,再往上走前后端都是通的,最近在知乎上看到一句话,说是到了架构师甚至专家的层面以后,就没有什么前端架构师或者前端专家和后端架构师、后端专家了,只有前端出身的技术专家,和后端出身的技术专家,越往上越注重人的综合能力,当一个人能够解决一个项目的技术选型、架构等工作后,对于公司或者团队来说,那就是个巨大的宝藏。

+ + +

公司这一年

最近对于自己的发展有一些迷茫,这一年公司前端的发展可以说是几经转折,我自己呢也一直在摇摆到底是做管理还是做技术,也参加了公司组织的部门经理的答辩,在部门前端的管理工作中也切实的了解到自己在为人处事方面不够圆润。所以目前也算是断了从事管理的念头,更希望能在技术上更进一步,前端目前看来已经不算是我的短板,而我的短板就是只会前端,一直在前端周围打转,其实如果不是看了那篇帖子,以及公司对专家岗位的要求,我可能还会更进一步在前端方向深入研究,但目前看更紧急的应该是补充一下后端的开发知识了,于是上周末开始我就开始了java的学习

+

为什么选择java

为什么选择java作为后端入门,实话讲好多前端开发应该都会问这个问题,明明有更熟悉的nodejs可以作为后端技能进行扩展,我这里的理由是目前大多数公司的包括外面公司的开发人员大都还是以java作为主要语言作为后端编写的选择,另外前端js中好多的设计也是借鉴或者照搬了java中的一些思想,可以说在学习java过程中也会自然而然的提高对js的理解,更重要的是,java相对于其他语言来说资料也更多,上手也更容易,因为这些因素吧,最终我选择了java作为后端的主要学习目标。

+

怎么学习java

java上大学的时候实际有系统的学过的,只是实习之后就再也没有使用过,如今9年过去了,java对于我可能也只剩下些零星的记忆,说实话刚一开始怎么学,从哪里学让我都有点无从下手,这里还要感谢一下我后端的开发伙伴,给了我很多很好的建议,看书的话大都是基础的太基础,实战的又经常忽略基础,最终我打算还是以视频教程2.5倍速快速过一遍java基础,然后再深入学习一下springboot框架,最后再进行实战,以此掌握java开发技能。

+

开始学习java

最终我选择了在B站上看黑马的java基础+实战课程的教学视频,说实话黑马的教学视频还是讲的很仔细的,老师讲的也很有趣,只是一节课10多分钟,只有一个知识点,对于我来说还是有些慢,所以我就开了2.5倍速加快进就这么着看,上周末两天时间,看了130多课,今天的内容记忆不太深刻,趁着不是那么忙又看了30多课,感觉收获还是满满的,接下来的每一天都会看上30课左右,希望自己能在3个月的时间完全上手java开发,相信我可以做到。

+

立个Flag

从今天起,每天都要把自己学习的进度做个总结,看看这一天自己收获了多少,希望30岁这年我重新起步,迈向更高更好的未来。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:重拾java开发技能

+

文章作者:

+

发布时间:2019年12月30日 - 21:37

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2019/12/30/begin-learn-java/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2019/12/31/GoodBye-2019/index.html b/en/2019/12/31/GoodBye-2019/index.html new file mode 100644 index 0000000000..696db666fe --- /dev/null +++ b/en/2019/12/31/GoodBye-2019/index.html @@ -0,0 +1,621 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 再见2019 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 再见2019 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

不知不觉又是一年,今天是2019年的最后一天,也是我30岁的最后一天,回顾这一年有收获有遗憾。翻出了18年的总结,看到了18年底对于自己19年的规划,现在回过头来看看哪些完成了。

+

18年底定的计划

学习技术

1. 深入学习客户端开发(全年)

18年客户端的项目也做了几个,Electron的开发能力经验更多了,之前只是用html+css+js做前端页面的开发,今年又增加了跟vue框架的结合,成功在多个项目中实践,只是一直没有形成规范感觉还是少了一环,原来一直在犹豫Electron到底算不算前端的技术栈之一,后来也算是想明白了,大前端范畴,只要是面向用户的都算是前端该做的范畴,所以Electron当然也算是前端应该掌握的技术栈,20年会继续学习客户端技术,把公司相关规范制定和落地。

+

2. 学习前端自动化测试相关知识(2019年3月前)

18年关注了前端自动化测试相关的知识,本打算19年好好学习一下,在公司项目中实践,但是今年前后端工作量大增,前端人员的精力有限,加之成本的考虑,并没有开展起来,不过这方面的技能储备已经具备了,年底两次关于前端自动化的测试也将前端自动化的思想普及到了公司前端的团队里,20年是公司强调质量的一年,相信20年前端自动化测试将有机会在公司一些项目中落地

+

3. 学习并掌握TS (2019年5月前)

18年就看到了TS将会成为一门比较活的开发语言,目前来说已经具备了做TS开发的技能,只是TS更适合大型的长期的项目,将更好的保障项目的代码质量,快速迭代和小型项目并不是很适合。前端团队里没有开花,没想到居然在开发团队里见到了曙光,感谢开发的小伙伴愿意接受这门前端新技术,相信有一个点采用后看到了优势,后续普及工作将会更好开展。

+

4. 学习并掌握React(2019年7月前)

18年Vue已经在公司前端全面普及开来,但是市场上React仍然是占据更多的份额,而且RN还能做App开发,所以还是一门必须要掌握的技术,今年看了一本React相关的书,也看了一些相关的视频教程,还参加了公司同事开展的React分享,虽然没有在项目中实际使用过,但是理论知识已经掌握了,可以进行小的Demo开发,20年要用React写一个开源的项目,做到学有所用,学以致用。

+

5. 学习前端持续集成的相关知识(2019年9月前)

19年公司强调了代码必须走线上构建的要求,前端工程化和自动构建持续集成的工作可以说是工作所迫,这方面的技能已经Get到了,另外令我感到高兴的是,通过对Github-Actions的配置,我的博客可以像提交代码一样提交文章了,而且也实践了自动构建的相关技术,很开心。

+

6. 学习Docker虚拟化技术( 2019年10月前)

这个任务没有达成,原本是希望能够通过Docker统一前端开发环境,不过Docker的适用场景还是以部署环境为主,Docker今年貌似热度也降下来了,今年K8s越来越火,不管怎样,持续关注新技术。

+

整理计划

1. 养成写博客的习惯,至少2天一篇,可以是技术博客,也可以是其他。

今年实在是太忙了,别说两天一篇文章,就连一个月一篇的量都没能达成,不过在年底我终于把博客又重新激活了,希望自己20年能够把博客坚持写下来,很简单,积累自己,打造个人品牌。

+

2. 将常用的方法和功能做成插件,开源给公司使用

今年4月开始,更多的精力都投在了部门的前端管理工作,说实话也迷茫也难过,这份迷茫终于在19年年底解开了,没能在技能层面有更高的提升,以及在代码层面给公司带来价值我觉得还是有些遗憾,20年希望自己能够在技术上有更大的进步,技术深度上/广度上/架构层面/后端技能都能有长足的进步。

+

读书计划

1. 每周读完一本书,并写一篇读后感

2019年读了25本书,每读完一本都会写一篇读后感,虽然没能保证每周读一本书,但是基本保证了每两周读完一本书,算是完成了50%的目标吧。20年继续努力,读书使人聪明,因为不聪明所以更要多读书。

+

部门前端计划

加强各设计组前端之间的交流

+

设计组的出现导致前端交流不畅,2019年加强与各设计组前端的交流,掌握各位前端的技术能力。

+
+

没想到一语成第,设计组居然在4月份将前端整个拿出来构成了一个前端组,由我来带,更没想到的是居然在19年年底又解散到项目团队了,这9个月的时间可以说有苦有泪也有甜,前端小伙伴们为了同一个目标,共同努力,尽最大的努力完成公司的业务,可以说为了这个目标,这一年我付出了很多很多,在这方面愧对了家人孩子,虽然最后前端还是拆分了,可以说我能力有限,但是我心里可以说是无愧,希望尽到项目团队里的小伙伴们也能开心工作,迅速成长,虽然不再在一个团队里,但是,我仍是你们的光妈。

+

前端俱乐部推动

+

继续每周2小时的兴趣交流,交流形式不限于ppt,要求各自建立技术博客做笔记。

+
+

俱乐部的活动最后还是没能坚持下来,大家工作比较忙也好,我自己的原因也好,确实没能坚持下来,20年还是希望能够将俱乐部活动组织起来,算是大家交流的机会也好,帮助大家开阔眼界也好,20年一定要再做起来!

+

进行梯队划分建设

+

前端今年人数明显增多,但各层级职责并没有区分,19年抽时间进行梯队建设。

+
+

19年前端从14人增加到了34人,可以说人数上翻翻了,随着负责人制的执行,梯队建设的工作算是完成了50%,但是能力提升的工作可以说还是做的不到位,应该有机会成长为高工的小伙伴没能升上去,这我有一定的责任,20年虽然团队不再,但是希望你们都坚持努力学习,迈向更高的台阶。

+

引入前端工程化工具和思想

+

目前前端工程化已经非常成熟,希望能将成熟的技术和工具引入。

+
+

19年,随着Vue的普及和熟练,模块化开发组件化开发的思想已经深入到每一个前端的心中,现在大家都知道该如何使用这些技术和工具,希望大家能不仅仅局限在用也能了解了解其中原理,至少对常用的配置有所了解。

+

提升整体前端开发的能力

+

目前整体前端开发能力还相对较弱,19年通过培训等方式提升整体前端开发的能力

+
+

19年能力提升的工作做的太少了,没什么好说的,希望20年这方面工作能够投入更多的精力,让更多的小伙伴成长起来,让我们一起加油吧。

+

生活目标

每天陪孩子读书一小时

跟上面的工作和个人目标比起来,这个任务可以说完成度0%,这也是我觉得愧对家人的地方,为了工作,对家人的关注少了很多很多,以至于桐桐晚上都不愿意跟我一起睡觉,20年我要好好对她,做个好的爸爸,孩子明年就三岁了,去到幼儿园会认识更多的朋友,希望她会跟她的小伙伴说,我有个爱我的爸爸。

+

减肥

减肥这件事可以说是真的对我来说太难了,看着自己马上破三位数(kg)的体重,心里无限难过,每次媳妇儿问我啥时候去健身,我都感到像被闪电击中了似的,办了健身卡被人嘲笑成了洗澡卡,20年,减不到150斤我就。。。。

+

写在最后

19年太多的遗憾,太多的后悔莫及,当然也有一些成绩和自我肯定,19年已经过去了,希望在20年,能够成为一个更优秀的人。
WechatIMG6.jpeg
感谢我可爱的同事,年底收到了礼物真的很开心。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:再见2019

+

文章作者:

+

发布时间:2019年12月31日 - 23:50

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2019/12/31/GoodBye-2019/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2019/12/31/gitlab-cicd/index.html b/en/2019/12/31/gitlab-cicd/index.html new file mode 100644 index 0000000000..7625b0cd29 --- /dev/null +++ b/en/2019/12/31/gitlab-cicd/index.html @@ -0,0 +1,664 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 基于 GitLab CI/CD 的自动化构建、发布实践 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 基于 GitLab CI/CD 的自动化构建、发布实践 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

公司于去年开始代码版本管理从SVN迁移到了Git,采用的GitLab程序做管理,但是自动构建采用的是Jekins,最近在给博客做自动构建的时候了解到的了Github的Actions,我就在想,是不是GitLab也有自己的自动构建能力,因为之前在参与项目的时候也看到GitLab上有CI/CD相关的内容,但是没有仔细的去看,趁这次机会就一起研究一下。

+ + +

说一下目前公司的构建和发布流程

1、手动构建时代:开发人员在测试需要验证环境的时候,在本地执行打包构建命令,然后将包放到服务器上,整个过程30分钟左右。
2、自动构建时代目前公司的构建是要在Jekins中,首先是在Jekins中配置拉去代码的仓库地址和代码分支,写好构建的脚本,在需要构建时候进行构建,一次配置后构建全程只需要点一下构建时间长度跟项目代码需要下载的依赖时间有关,通常不超过5分钟,需要注意的是要在构建前同步一下代码版本
划重点
在原来的手动构建时代,代码是以开发本地的代码为准,代码版本很可能跟最新的代码有出入,而且依赖于开发的电脑设备,如果他请假了,那么就GG了;另外通过一次配置后整个构建的时间从30分钟降到了5分钟,一次节省25分钟,那么一个项目周期下节省的工时就非常可观了。

+

为什要使用GitLab CI/CD进行构建

这里实际上没有太大的必要将公司的Jekins替换为GitLab的CI/CD进行自动构建,但是呢,因为公司本身采用的就是GitLab作为代码仓库管理代码,它本身又提供了CI/CD的功能,本着多学一点是一点的原则,我就花点时间研究一下它。

+

什么是 GitLab CI/CD

下面我就要开始把我了解到的GitLab CI/CD的使用方式说一下,从零开始搭建GitLab CI/CD。

+

1. 简要介绍 GitLab CI/CD

代码提交到GitLab上后,满足指定条件之后会触发pipeline进行自动化构建、发布。
pipeline可以理解为构建任务,里面可以包含多个流程,比如下载依赖、运行测试、编译、部署。
那么pipeline什么时候触发,分为几个流程,每个流程做什么,需要在项目的.gitlab-ci.yml文件中的定义。
这点呢跟Jekins里面实际上做的也是同样的事,在线下开发做构建时候也是做这些事,只是通过脚本之后这些事都可以交给计算机做了。

+

2. GitLab CI/CD 整体流程

    +
  • GitLab CI/CD 的 pipeline 具体流程和操作在 .gitlab-ci.yml 文件中申明。
  • +
  • 触发 pipeline 后,由 GitLab Runner 根据 .gitlab-ci.yml 文件运行。
  • +
  • 运行结束后将返回至 GitLab 系统。
  • +
+

2.1 .gitlab-ci.yml 文件

.gitlab-ci.yml 文件是一个申明式文件,用于定义 GitLab CI/CD 流程分为几个阶段,每个阶段分别干什么。

+

关于具体干什么、怎么干,主要使用命令行和脚本操作,稍后会在实践部分做细致的介绍。

+

如果涉及一些逻辑的话,会使用脚本(shell)。

+

2.2 GitLab Runner

GitLab Runner 是 CI 的执行环境,负责执行 gitlab-ci.yml 文件,并将结果返回给 GitLab 系统。Runner 具体可以有多种形式,docker、虚拟机或 shell,在注册 runner 时选定方式。实际上就是运行脚本的容器环境。

+

3. 从零搭建一个 GitLab CI/CD 的基本步骤

上面介绍了一些GitLab构建的主要环节和名词概念,接下来我将给大家介绍一下如何从零搭建一个GitLab CI/CD,一起体验一把GitLab CI/CD的整个流程。

+

3.1 新建一个 GitLab 项目

我这用的是公司的自有仓库,各位可以在开源GitLab上创建自己的项目

+

3.2 配置Runner

GitLab 提供了一些共享的Runner,我们可以不处理Runner,这里可以理解为,它提供了一些现成的脚本运行环境,不需要我们从头配置运行环境,so sweet~

+

3.3 新建 .gitlab-ci.yml 文件

    +
  1. 拉取项目到本地
  2. +
  3. 在项目根目录新建 .gitlab-ci.yml 文件
  4. +
  5. 提交 .gitlab-ci.yml 文件
  6. +
  7. 在项目的 CI/CD 中,可以看到 CI/CD 的运行情况
    这个过程应该没人不会吧,没技术含量的我们简单一提,实际上最重要的就是.gitlab-ci.yml文件中要怎么去写,示例说明文件如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // .gitlab-ci.yml 示例说明

    image: node
    # 定义 stages
    stages:
    - build
    - test
    # 定义 job
    build 阶段:
    stage: build
    script:
    - echo "build stage"
    # 定义 job
    发布到测试环境:
    stage: test
    script:
    - echo "test stage"
    + +
  8. +
+

GitLab CI/CD 实践

在实践部分,这里着重介绍 GitLab Runner 和 .gitlab-ci.yml 文件,主要的流程及遇到的问题和解决方案包含在 .gitlab-ci.yml 文件的介绍过程中。

+

1. GitLab Runner

GitLab Runner 一般由 GitLab 系统维护者管理,配置后,同类项目可以共享,一般不需要进行修改。这里不进行具体介绍,主要介绍下使用过程中的注意点,具体使用可参考 GitLab Runner 文档。(https://docs.gitlab.com.cn/runner/)

+

1.1 GitLab Runner 使用流程

    +
  1. 下载 GitLab Runner
  2. +
  3. 注册 GitLab Runner
  4. +
  5. 使用 GitLab Runner
  6. +
+

1.2 GitLab Runner 注意点

在使用 Runner 的过程中,我们遇到了一些问题,下面简要介绍问题及解决方案,不做具体介绍。

+
1.2.1 配置 Runner 后,push 代码,出发了 pipeline,但一直处于Pending状态

错误信息是:

+
1
This job is stuck, because you don’t have any active runners that can run this job
+

注册的 Runner,默认情况下,不会运用没有 tag 的 job,可以在 Settings→CI/CD→Runners Settings,去掉 Runner untagged jobs 即可。

+
1.2.2 GitLab Runner 的类型

有三种类型的 Runner,

+
    +
  • Shared Runners 在整个系统所有项目都可以使用
  • +
  • Group Runners 注册后,同一个项目下的不同代码库共享
  • +
  • Specific Runners 需要给项目单独配置,使用 Specific Runners 注意考虑是否需要关闭 Shared Runners、和 Group Runners。
  • +
+
1.2.3 在 GitLab CI 中使用 docker

如果部署使用的是docker方式,那么在部署时需要在 GitLab CI/CD 中使用 docker 打镜像发布。可以参考 Building Docker images with GitLab CI/CD(https://docs.gitlab.com/ee/ci/docker/using_docker_build.html)

+
1.2.4 在 GitLab CI/CD 中访问 Runner 宿主机目录

我们使用的 Runner executor 是 Dokcer,在 Dokcer volumes 中配置需要访问的目录。

+

2. .gitlab-ci.yml 文件

.gitlab-ci.yml 详细的用法,可参考 GitLab CI/CD Pipeline Configuration Reference 文档(https://docs.gitlab.com/ee/ci/yaml/README.html)

+

2.1 .gitlab-ci.yml 文件结构介绍

    +
  • image 是执行 CI/CD 依赖的 Docker 基础镜像。镜像中有 Node、Yarn、Dalp(内部 rsync 工具)。
  • +
  • stages 中定义了我们的 pipeline 分为以下几个过程:
      +
    1. 下载依赖阶段 pre_build
    2. +
    3. 构建阶段 build
    4. +
    5. 发布阶段 deploy
    6. +
    +
  • +
  • stage 申明当前的阶段,在 stages 中使用
  • +
  • variables 用于定义变量
  • +
  • before_script 执行 script 前的操作
  • +
  • script 当前 stage 需要执行的操作
  • +
  • changes 指定 stage 触发条件
  • +
  • refs 指定 stage 触发的分支
  • +
+

下面具体看一下我们这个.gitlab-ci.yml文件实际的样子

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
image: registry.thunisoft.com/gitlab-ci/node:v1.8

variables:
# $CI_PROJECT_PATH :项目id,用于项目唯一区分本项目与其它项目
# $CI_PROJECT_DIR :本地项目路径
# $PROCESS_PATH :临时文件目录(包括日志和一些临时文件)
NODE_MODULES_PATH: /runner-cache/frontend/$CI_PROJECT_PATH/$CI_BUILD_REF_NAME/node_modules

stages:
- pre_build # 下载依赖阶段
- build # 构建阶段
- deploy # 测试发布阶段

# 下载依赖:
before_script: # 下载依赖前准备脚本
# 无 node_modules 文件时,新建 node_modules 文件
- /bin/bash ./ci/mkdir.sh $NODE_MODULES_PATH
# 软链 node_modules 到宿主机
- ln -s $NODE_MODULES_PATH .
- cd webpack@lixuguang-project

stage: pre_build
script:
- echo "npm install"
- npm install --network-timeout 60000 # 安装依赖
only:
changes:
- webpack@lixuguang-project/package.json
refs:
- master
- ci

# 构建:
stage: build
variables:
CI_COMMIT_BEFORE_SHA_PATH: /mnt/gv0/gitlab-runner-cache/$CI_PROJECT_PATH
CI_COMMIT_BEFORE_SHA_FILE_NAME: $CI_BUILD_REF_NAME.sh
CI_COMMIT_BEFORE_SHA_FILE: /mnt/gv0/gitlab-runner-cache/$CI_PROJECT_PATH/$CI_BUILD_REF_NAME.sh
before_script:
# 建存此次 CI CI_COMMIT_SHA 的文件
- /bin/bash ./ci/mkfile.sh $CI_COMMIT_BEFORE_SHA_PATH $CI_COMMIT_BEFORE_SHA_FILE_NAME
# 软链 node_modules 到宿主机
- ln -s $NODE_MODULES_PATH .
- rm -rf web/share/*
- cd webpack@lixuguang-projects
script:
# 缓存上次ci
- source $CI_COMMIT_BEFORE_SHA_FILE
- echo "CI_COMMIT_BEFORE_SHA=$CI_COMMIT_SHA" > $CI_COMMIT_BEFORE_SHA_FILE
- python3 ../ci/build.py # 编译
- /bin/bash ../ci/commit.sh # 提交编译结果
only:
changes:
- www_src/**/*
refs:
- master
- ci

# 测试发布:
stage: deploy
variables:
PROCESS_PATH: /mnt/gv0/gitlab-runner-cache/deploy/process/$CI_JOB_ID # 目录不要换,用于日志服务器获取日志展示
script:
- mkdir $PROCESS_PATH # 建立发布临时路径,存放发布配置中间文件和结果日志用
- dplt $CI_PROJECT_DIR/.deploy_test.yml $CI_PROJECT_PATH $CI_PROJECT_DIR/web/ $PROCESS_PATH
# dplt 发布yml配置
- echo "发布完成,错误日志查看http://172.18.78.11:8089/log?path="$PROCESS_PATH
- echo `ls $PROCESS_PATH/*.log`
only:
changes:
- web/**/*
refs:
- test
+ +

2.2 下载依赖阶段(pre_build stage)

下载依赖的方案是:当 package.json 文件发生变化时,触发 pre_build stage,执行 npm install。下载的 node_modules 放在宿主机下,执行时通过软链获取依赖。

+

2.3 构建阶段(build stage)

构建阶段,分为 3 部分

+
    +
  1. diff 文件变化
  2. +
  3. 前端 build
  4. +
  5. commit build 后结果
  6. +
+
2.3.1 diff 文件变化

每次 CI 时,将当前 CI commit SHA(CI_COMMIT_SHA 变量)存在文件中,存为 CI_COMMIT_BEFORE_SHA 变量, diff 时,git diff 当前 CI 与上次 commit SHA 的变化。

+
2.3.2 前端 build

根据 git diff 的变化情况,确定本次需要打包的内容。

+
2.3.3 commit 打包后生成的 HTML 文件

在 GitLab CI/CD 提交代码时,使用 Git 凭证存储,提交打包后的 HTML 文件。Git 凭证存储细节可参考凭证存储文档(https://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E5%87%AD%E8%AF%81%E5%AD%98%E5%82%A8)

+

2.4 发布阶段(deploy stage)

发布阶段,使用内部的 rsync 工具 dplt 将打包后的 HTML 文件部署。dplt 可配置集群、机器列表。

+

写在最后

以上就是GitLab CI/CD的整个理论到实践的全部过程,实现之后你就可以解放双手了,是不是超爽。

+

参考资料

持续集成是什么?(http://www.ruanyifeng.com/blog/2015/09/continuous-integration.html)

+

什么是 CI/CD?(https://www.redhat.com/zh/topics/devops/what-is-ci-cd)

+

GitLab Docs(https://docs.gitlab.com/)

+

Introduction to CI/CD with GitLab(https://docs.gitlab.com/ee/ci/introduction/)

+

用 GitLab CI 进行持续集成(https://scarletsky.github.io/2016/07/29/use-gitlab-ci-for-continuous-integration/)

+

如何实现前端工程的持续集成与持续部署?(https://www.zhihu.com/question/60194439)

+

基于 GitLab CI 的前端工程CI/CD实践(https://github.com/giscafer/front-end-manual/issues/27)

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:基于 GitLab CI/CD 的自动化构建、发布实践

+

文章作者:

+

发布时间:2019年12月31日 - 08:22

+

最后更新:2019年12月31日 - 08:43

+

原始链接:https://blog.lifesli.com/2019/12/31/gitlab-cicd/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2019/12/31/weichat-h5-compatibility/index.html b/en/2019/12/31/weichat-h5-compatibility/index.html new file mode 100644 index 0000000000..cc42cb18e9 --- /dev/null +++ b/en/2019/12/31/weichat-h5-compatibility/index.html @@ -0,0 +1,618 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 微信的H5兼容方案 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 微信的H5兼容方案 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近被公司新来的前端问到了一些微信页面的兼容性问题,因为我做微信开发的经验比较少,被问到了真是感到很尴尬,所以在搜到了解决方案告诉了他之后,我又深入的再网上找了一些微信开发中遇到的兼容问题,特此整理,以留后用。

+ + +

1、ios端兼容input光标的高度

bug描述:
这个问题只出现在苹果手机上,在安卓手机上显示没有问题,可以说是非常诡异,简单描述一下就是在input输入框聚焦时,光标大小应该跟字号一直,但是在苹果手机上当点击输入的时候,光标的高度和父盒子的高度一样。
分析:
说来主要是习惯导致的问题,通常我们习惯将height和line-height设置成一样的值,这个时候input光标就会整个变得很大。
解决:
实际上解决方案也很简单,就是不设置行高,通过padding来控制输入内容与外框的距离。

+
1
2
3
4
5
6
7
8
// less代码
.input-x{
height:40px;
// line-height:40px; // 此行注释掉
.input-inline{
padding: 10px 0;
}
}
+

这样做问题就解决了。

+

2、ios端微信h5页面上下滑动会卡顿,页面会有缺失

bug描述:
没错又是ios端,当页面高度超过一屏,那么上下滑动时就会出现页面卡顿的情况,而且时有伴随内容不能全部显示的情况。
分析:
这里实际上是浏览器内核解析不同导致的问题,在Andriod设备上,微信调用的是Webkit内核,而ios中是使用了Safari的内核,Safari对于滚动事件(overflow-scrolling)会使用原生的控件。而webkit内核则会创建一个UIScrollView来提供给子layer用以渲染。
解决:
在做样式重置时,加上下面这句话就能解决这个问题。

+
1
2
3
4
// css代码
*{
-webkit-overflow-scrolling: touch;
}
+

但是这个方案也有缺陷,就是页面中不能有使用absolute定位的元素,不然布局就错乱了。
延伸:
-webkit-overflow-scrolling 属性控制元素在移动设备上是否使用滚动回弹效果.

+
    +
  • auto: 使用普通滚动, 当手指从触摸屏上移开,滚动会立即停止。
  • +
  • touch: 使用具有回弹效果的滚动, 当手指从触摸屏上移开,内容会继续保持一段时间的滚动效果。继续滚动的速度和持续的时间和滚动手势的强烈程度成正比。同时也会创建一个新的堆栈上下文。
  • +
+

3、ios键盘唤起再收起,页面不会恢复原位

bug描述:
哎,对的还是ios,问题标题描述的比较清晰了,就是键盘弹出时,页面内容会整体上移,但是收起键盘时本应回归原位的不回去了。—_—|||
分析:
固定定位的元素,如果元素内input框聚焦的时候会弹出软键盘,软键盘会占用屏幕面积,失去焦点时软键盘消失,但是仍会占用,页面就会不能恢复原状,也就导致input框不能再次输入了。
解决:
在input失去焦点键盘收起时,写一个监听事件,事例代码如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// vue代码
<input @blur="changeBlur()"/>

// js代码
changeBlur(){
let ua = navigator.userAgent; // 获取用户代理
let app = navigator.appVersion; // 获取客户端版本信息
let isIos = ua.match(/i[^;]+;( U;)? CPU.+Mac OS X/); // 判断是否是Ios设备
if(isIos){
setTimeout(()=>{
const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop || 0
window.scrollTop(0,Math.max(scrollHeight - 1), 0)
},100)
}
}
+

延伸:
在iso的微信开发中,页面元素如果用到了position: fixed进行定位,那么键盘收起时,就会被顶上去,第三方输入法也不例外。

+

4、Android弹出键盘遮挡文本输入框

bug描述:
刚才说的问题都是Ios端的,实际上Android上也有挺多坑,上面讲到Ios上输入框弹出键盘的问题后,Android中实际也有,只是现象不同;Andriod中弹出键盘后页面不会向上滑动,但是如果输入框在底部的话会直接被挡住。。。
分析:
很坑,因为Andriod中输入框focus后,并不会向上滑动,如果靠下就会被挡住。。
解决:
实际上跟Ios上处理差不多的方案,代码如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// vue代码
<input @blur="changeBlur()"/>

// js代码
changeFocus(){
let ua = navigator.userAgent;
let app = navigator.appVersion;
let isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1;
if(isAndroid){
setTimeout(function() {
document.activeElement.scrollIntoViewIfNeeded();
document.activeElement.scrollIntoView();
}, 500);
}
}
+

扩展
Element.scrollIntoView()方法让当前的元素滚动到浏览器窗口的可视区域内。而Element.scrollIntoViewIfNeeded()方法也是用来将不在浏览器窗口的可见区域内的元素滚动到浏览器窗口的可见区域。但如果该元素已经在浏览器窗口的可见区域内,则不会发生滚动

+

5、Vue中路由使用hash模式,分享时Android可分享成功,Ios端分享失败

bug描述:
Ios的问题真的挺多的。。。

+
    +
  • 在分享页面给A时,没问题,A把链接分享给B的时候就跳转到首页了;
  • +
  • 使用Vue-router跳转到第二个页面在分享时候,分享失败;
    以上两个问题在Android上均没有问题。
  • +
+

分析:
jssdk是后端进行签署,前端校验,但是有时跨域,ios是分享以后会自动带上 from=singlemessage&isappinstalled=0 以及其他参数,分享朋友圈参数还不一样,貌似系统不一样参数也不一样,但是每次获取url并不能获取后面这些参数
解决:

+
    +
  • 可以使用改页面this.$router.push跳转,为window.location.href去跳转,而不使用路由跳转,这样可以使地址栏的地址与当前页的地址一样,可以分享成功
  • +
  • 把入口地址保存在本地,等需要获取签名的时候再取出来,注意:sessionStorage.setItem(‘href’,href); 只在刚进入单应用的时候保存!(还没测试,有点low)
  • +
+

写在最后

虽然微信H5方式开发想对来说成本比较低,但是有时候坑开始挺多的,但是微信原生开发又增加了成本,很矛盾,目前能做的就是尽量把踩过的坑都记下来,下次别再跳进去了。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:微信的H5兼容方案

+

文章作者:

+

发布时间:2019年12月31日 - 01:37

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2019/12/31/weichat-h5-compatibility/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/01/Hello-2020/index.html b/en/2020/01/01/Hello-2020/index.html new file mode 100644 index 0000000000..69b3e926b1 --- /dev/null +++ b/en/2020/01/01/Hello-2020/index.html @@ -0,0 +1,593 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 你好2020 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 你好2020 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

2020元旦伊始

时间过的真快,19年睡了一觉就成了过去的一年,今天是元旦,早上8:00起了床,简单洗漱后我出了门,新年新气象,去年没有坚持的晨跑,今年想要坚持一下,所以一早就出门跑了会儿步,太久没活动了,只是跑了半个小时就气喘吁吁,只好回家,但是即使是这半个小时,我感觉呼吸了新鲜的空气,整个人也精神了不少,回到家坐在阳台上,拿出小王宁送我的极少数手册开始研究了起来,生活还是应该有点仪式感何况是新年伊始,手册中有很多建议,全部看过一遍以后我郑重的拿起了笔写下了我今天的行动计划。但是没有写年度计划,明天上班后跟领导还要谈一谈,谈过之后再定一下年度的计划。今天我的计划就是陪家人过好元旦这一天。

+

元旦执行计划

    +
  1. 写一篇日志
  2. +
  3. 将家人这一天游玩的过程以Vlog的形式记录下来,上传到西瓜视频上。
  4. +
  5. 陪家人逛街,给桐桐买新衣裳。
  6. +
+

执行计划

吃过早饭后大概到了10:30,一家人收拾好以后终于可以出门了,今天有点风,一点点冷不是特别冷,空气还不错,天气听晴朗的,一家人开着车听着歌前往甘井子万达广场,20分钟左右我们就到了万达广场,首先我们先去看了桐桐心心念念的小猫咪,也就是猫咪咖啡馆里的那些小可爱,桐桐可能是受妈妈影响,很喜欢猫咪,但是又有一点点怕。大概看了10分钟的猫咪,我们继续下一站,上四楼给桐桐买新衣裳,桐桐看到满眼的漂亮衣裳很是兴奋,最终她挑选了一套带着向日葵花朵的淡蓝色衣裙,穿上漂亮的新衣裳,平时像个假小子似的桐桐也一下子害羞了起来,让妈妈和奶奶看完后还会让爸爸和爷爷看看,得到了大家的肯定以后,心满意足的买下了它,也不知道是不是试衣服试累了,桐桐嚷嚷着要吃饭,桐桐妈说有一家港式茶餐厅味道不错,之前和同事来过,于是我们就去了4楼的茶餐厅,说实话这是我头一次吃茶餐厅,我还跟我爸妈调侃说,这也算是我们来过一次香港了。去到餐厅时才11点多一点,客还没有上满,还有座位,我们找了个靠里的位置坐了下来,打开菜谱,桐桐妈挑了几样她觉得不错的菜,上菜时间稍微有点慢,期间桐桐有些不耐烦,坐也坐不住,终于等到了吃的上来,可是觉得大多数吃的都太偏甜,爸妈和我都吃不太惯,桐桐吃的还不错,大概吃了半个小时就吃完了,出门的时候门口已经排起了长队,还好来的早一点,不然吃饭都要等了。本打算带着桐桐回家睡个觉,下午要到姥爷家串门,可桐桐说没有逛够,正巧这时看到了一楼有小丑在折气球玩具,我们就下到一楼找小丑玩了,小丑给桐桐折了个贵宾犬,小丑是荣耀手机的员工,可能是元旦策划的活动吧,反正来了就来了,进去看看也好,看了新出的V30手机,感觉并不是很喜欢,店员说有旧机抵值的活动,我问了他我的7p128g还能值多少,他打了几个电话后回复我1400块,哎,三年前7000块买的手机,现在只值1400了,算了还是用着吧,出了荣耀手机店又去了旁边的华为,看了看新出的mate30pro,这个手机是真的不错,当然手机不错价格也美丽,6500+的价格真的是让我不舍得买,还是把钱留下来给桐桐吧。桐桐终于累了,嚷嚷着要走,于是一家人驱车去了姥姥家。

+

姥姥家在西南路,因为平时工作忙很少带桐桐去,这次去桐桐又不认识他们了,开始的时候很拘束,也不说话,一直到吃饭结束才终于愿意开口问好唱歌,因为家里没有电视机,到了姥姥家看到电视机很兴奋,一直在看,今天也算是过了瘾了,亲人就是即使很长时间不见面,但一见面就特别亲的人,饭从5:30一直吃到8:30,聊了工作,聊了生活,聊了困惑,聊了坦然,我终于知道了长辈们的智慧,了解了身体健康的重要性,20年一定要锻炼出一副好的身体,对家人负责。

+

9:30回到了家,心里挺高兴的,新年第一天陪家人一起度过了充实的一天,回到家把今天路上录下来的视频剪成了短视频上传到了西瓜视频,这也是我今年的一个想法,记录我们一家人的幸福生活,如果有粉丝有点播的话还能赚点钱,没有的话也记录了桐桐的成长,算是一点纪念吧。最后写下了这篇日记,写完之后我就要睡觉去了,明天一早就要起床,明天起我就要走路去上班了,为了有一副好身体,加油。

+

今天的目标都完成了,很开心~~

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:你好2020

+

文章作者:

+

发布时间:2020年01月01日 - 10:50

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/01/Hello-2020/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/02/21-Day-Challenge-01/index.html b/en/2020/01/02/21-Day-Challenge-01/index.html new file mode 100644 index 0000000000..c037ddfb3b --- /dev/null +++ b/en/2020/01/02/21-Day-Challenge-01/index.html @@ -0,0 +1,599 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2020年21天主题挑战之灵感篇-Day 1-写下自己期待中的生平 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 2020年21天主题挑战之灵感篇-Day 1-写下自己期待中的生平 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

还记得我19年总结最下面的那张照片么,那个同事送的的本子,对,就是下面这张图,这本子叫做《One In A Million》中文叫做极少数手册,是一个管理时间用的管理手册,英文如果直译的话应该是叫做万里挑一,实话说我并不是很自信,虽然我觉得自己还算可以,但远远达不到万里挑一的程度,所以,我打算好好利用这个本子,让自己尽可能成为百里挑一,千里挑一,最后万里挑一的人。

+

本子里有一个栏目叫做21天主题挑战,21-Day Challenge,每个月21天都会给你列出一些主题,越往后列出来的主题越少,更多的需要个人去补充,我想去挑战这些内容,让自己的生活变得有目标,让自己变得更好。

+ + +

1月份21天主题挑战之灵感Inspiration

+

设定一个挑战主题,让自己更富有创造力,连续21天挑战,让灵感乍现,唤醒天赋。

+
+

第一天 Day 1 写下自己期待中的生平

看到这个题目之后,我闭上了眼睛,努力的回想我自己曾经的梦想是什么,什么时候丢掉了梦想,儿时的梦想,上学时的梦想,长大以后的梦想,现在的梦想,我努力回想了好久好久,儿时的梦想我想起来了,上学时的梦想我想起来了,长大后的梦想我也想起来了,但是我没能一下子找到我现在的梦想,也许现在说梦想有些奢侈,也许也有点贴不上今天的主题期待中的人生,我不太会写文章,但是想到哪里写哪里吧,我先写下我之前的那些个梦想。

+

小时候的梦想-成为科学家

小时候的梦想说出来很简单,但是是那么真诚,真诚就容易打动人,说实话我记性不是太好,已经忘掉了小时候的梦想,所以我问了我妈,我妈笑话我,说我咋不记得小时候那伟大的梦想了,妈妈说我儿时的梦想是当科学家,那会应该是上小学吧,妈妈说学校老师让写作文,标题就是我的梦想,我的作文里写的是成为科学家,这个科学家不像是别人写的,比如造火箭,造大炮又或者是造卫星造原子弹,我的梦想现在说起来与其说是科学家,不如说是药剂师更贴切,妈妈笑着跟我说我当时的梦想是做一个科学家,要研究出来一种长生不老的药,然后让爸爸妈妈吃下以后就可以永远健康年轻,妈妈说那个时候她总逗我,看到路上漂亮的阿姨就会问我,那个阿姨漂亮么,起初我会说漂亮,然后妈妈就会说那让她给你当妈妈好不好,我呢会说不好,妈妈说后来她再问的时候,我就再也不会说我要漂亮阿姨当妈妈的话了,我现在想想也许就是那时,就是因为发现妈妈越来越胖,辛苦操劳后年轻也渐渐不在我才有了这个想法吧,说实话上小学期间我一直认为自己长大能够成为一个科学家,能够造出那长生不老的药让爸妈吃下,这样我就再也不会需要一个漂亮阿姨当妈妈了。我那时希望快快长大,长大了以后我就能做科学家了。

+

上了初中高中以后,也许是进了大城市,也许是长大了,我知道了原来的梦想可能有些遥远,学业的压力让我有些透不过气,从海岛到城市,落后了3年的时间,我通过补习班慢慢追赶,终于能够追到了班级还算靠前的位置,那个时候我的梦想很小,也很简单,那就是考个好大学找个好工作,让爸爸妈妈早点不用那么操劳,早点享福。

+

18岁的梦想 — 找个好工作,让爸妈早点享福

努力的学习,懵懂的感情,初中4年+高中3年的生活,最终我并没能特别出色的考上985/211大学,而是上了离家只有1小时车程的大外,学了计算机加日语英语,学费不便宜,每年16000,说实话没自己工作的时候不知道这16000对于一个下岗自己在家开小卖店的父母是多大的负担,工作后我才终于知道这笔钱有多少。好在原来在爸爸单位的时候接触电脑还算比较早,而我对这个新鲜玩意也算是感兴趣,大学学业上并没给我带来太大的压力,但是我也确实不是个聪明的孩子,日语仅仅过了N2,而英语则一直只是CET-4,现在回想起来,原来应该多努努力,也许现在的生活就会更好了,多想回到过去跟自己说,你要努力啊!一晃4年的大学生活就结束了,这时我终于离开了父母身边,只身去往了大城市北京,开启了我的工作生涯。

+

20岁的人生 — 多挣钱,快速成长

工作了以后,我就像海绵一样不断的吸收着周围的水分,学习工作中需要的技能,学习如何才能让领导器重,学习如何才能快速积累人生的财富,因为我想着,想着能快快独立反哺我的爸妈。20岁,我跟媳妇儿谈了场异地的恋爱,后来她到北京找我,再后来我们就一起回了大连。北京是个大城市,大的有时候让人迷茫,虽然工作机会比较多,但是租房的压力,环境的恶劣,家里的呼唤,最终让我选择了回到我熟悉的城市,另外找了份工资不高的工作,我不满足,我想能成为顶天立地的男子汉,后来我就来到了现在的公司华宇,而且一待就是5年。

+

华宇的职业生涯 — 5年工作,9年经验

网上有个段子我记得,一个人面试拿出简历,工作时间是2年却写着3年工作经验,面试官问他是不是写错了,他答不是,因为加班加出来的。这算是对前公司的吐槽吧。

+

来到大连华宇时,公司还不足120人,我所在的团队还是个交互组,只有三个人,前端的话只有我和另外一个刚毕业的大学生,记得刚来公司的第一年,我参与了140个项目的开发工作,现在想想这个数字有些惊人,但是因为只是些前端切图仔的工作,对我来说感觉难度并不大,不过还是要感谢刚开始这2年,让我的基础非常扎实,再后来公司引入了前后端分离,引入了Vue框架,越来越多的业务要写,数据处理要写,加班成了家常便饭,后面这三年,我几乎没有休过除了元旦和春节的任何法定节假日,每年5天的年假也几乎都没休成,说实话每次加班加到要崩溃时候,我都会想我到底为了啥这么拼命。要不我还是换一家比较轻松一点的工作吧,工资还能涨点。说实话这段时间工作就是生活的全部,每天到家都10点以后,到家老人孩子都睡了,有时候我都睡不着,想着我的生活难道就这样了么,我不甘心,不服输。很多人劝我别那么拼命,别把公司当成自己家的,只是个打工的而已,但是有时候想着下面还有那么多新人信任着我依赖着我,我就没法撒手不管。终于时间到了19年年底,5年来培养的前端团队最后还是没守护住,要拆到各项目团队了,一开始真的难以接受,不过公司领导层已经决定了,作为一线员工只能服从,我希望大家能够把心中的担忧都能消除掉,在新的团队里开启新的篇章,也许会有更好的发展,如果有一天离开了华宇,也要江湖相望,常聚聚。

+

2020年31岁踏上新的旅途

还是感谢小可爱的这个本子,让我能够有主题想想我未来到底想要什么样的生活,说句实在话,我向往不为钱发愁工作的日子,可以在做好自己工作之外的时间里多陪陪家人,带着孩子常出去走走,见见外面的世界,说实话已经31岁了,除了谈恋爱时去找媳妇儿去过江浙苏杭,工作原因去过北京/青海,好像这些年也就去了趟南京,现在的我已经有些不知道什么叫做生活了,我希望自己的空闲时间是可以让兴趣填满的,可以和朋友同事同学多聚聚,但是怕打扰大家我又从来不会主动去约别人,小时候没有听从爸妈的话培养自己的兴趣,现在有些后悔,真的空闲下来都不知道该用什么填满这时间,都是躺在床上刷手机,看电视,我不喜欢这样的生活,但是我不知道该过怎么样的生活。看到那些会做饭的视频,做顿丰盛的饭菜给家人吃,看到他们的幸福笑脸,我想做那样的事;看到带着家人过着一路向前增长见闻的旅途生活,我想也尝试一下那样的人生,我感觉为了钱我被束缚在了工作上,2020年我想过一种不为工作所累,不为钱所累,能够享受生活,陪伴家人的生活,保重身体,每天快快乐乐的,多发现生活中的美好,感恩,努力,成长。期待自己成为更好的人。

+

写在最后

我所期待中的生平,成为一个不被工作强迫,不被金钱所累,爱家顾家,孝敬父母,人缘好朋友多,兴趣广泛,感恩的人。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:2020年21天主题挑战之灵感篇-Day 1-写下自己期待中的生平

+

文章作者:

+

发布时间:2020年01月02日 - 12:24

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/02/21-Day-Challenge-01/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/02/FE-guide-Closure/index.html b/en/2020/01/02/FE-guide-Closure/index.html new file mode 100644 index 0000000000..b92e3b3567 --- /dev/null +++ b/en/2020/01/02/FE-guide-Closure/index.html @@ -0,0 +1,602 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 闭包 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 闭包 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

闭包Closure

闭包的定义很简单:函数 A 返回了一个函数 B ,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。

+
1
2
3
4
5
6
7
8
9
// js代码

function A() {
let a = 1
function B() {
console.log(a)
}
return B
}
+

你是否会疑惑,为什么函数 A 已经弹出调用栈了,为什么函数 B 还能引用到函数 A 中的变量。因为函数 A 中的变量这时候是存储在堆上的。现在的 JS 引擎可以通过逃逸分析辨别出哪些变量需要存储在堆上,哪些需要存储在栈上。

+

经典面试题,循环中使用闭包解决 var 定义函数的问题

+
1
2
3
4
5
6
7
// js代码

for ( var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
+

首先因为 setTimeout 是个异步函数,所有会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。

+

解决办法两种,第一种使用闭包

+
1
2
3
4
5
6
7
8
9
// js代码

for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
+

第二种就是使用 setTimeout 的第三个参数(附加参数,传给 setTimeout 中的函数)

+
1
2
3
4
5
6
7
// js代码

for ( var i=1; i<=5; i++) {
setTimeout( function timer(j) {
console.log( j );
}, i*1000, i);
}
+ +

第三种就是使用 let 定义 i

+
1
2
3
4
5
6
7
// js代码

for ( let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
+

因为对于 let 来说,他会创建一个块级作用域,相当于

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// js代码

{ // 形成块级作用域
let i = 0
{
let ii = i
setTimeout( function timer() {
console.log( ii );
}, i*1000 );
}
i++
{
let ii = i
}
i++
{
let ii = i
}
...
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 闭包

+

文章作者:

+

发布时间:2020年01月02日 - 13:32

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/02/FE-guide-Closure/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/02/FE-guide-dataType/index.html b/en/2020/01/02/FE-guide-dataType/index.html new file mode 100644 index 0000000000..dc67ffcc79 --- /dev/null +++ b/en/2020/01/02/FE-guide-dataType/index.html @@ -0,0 +1,655 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 数据类型、类型判断、类型转换、类型比较 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 数据类型、类型判断、类型转换、类型比较 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

+

温故而知新,可以为师矣。
———————— 论语

+
+

这篇文章主要是把我看过的书中的知识点,还有其他人整理的面试题进行整理,巩固自己的前端开发理论知识,希望其他看到这篇文章的人也能有所帮助。

+ + +

JS知识点

+

当前市场中,如何区分一个好一点的前端开发和一般的前端开发,主要看的就是js能力的差距,好的前端开发,JS玩的转,不仅仅是框架玩的好,还要JS的基础扎实,只有基础与新技术都玩的6的前端开发才是好前端。

+
+

上面这句话不是什么名人或者某位知名前端大拿说的,这是我在公司这几年面试招聘的过程中真真切切总结感受的。
所以可以说得JS者得高级前端,所以下面也主要是写JS的相关知识点。

+

JavaScript内置数据类型 — 数据类型

无论学什么语言最重要最基础的就是数据类型,《JavaScript权威指南》这本书中详细的介绍了JS中的数据类型,下面总结一下。

+

JavaScript中数据类型分为两大类共七种内置数据类型,其一是6种基本类型,其二是1种引用类型,我发现在面试过程中好多面试者都不会先说有两大类,而是会直接蹦出数字类型、字符串类型。。。对象、数组也会被并排放在一起,这其实不是个掌握知识点的好方法,应该先把数据类型分成上面说的基本类型引用类型这样两个大类之后,再看看这两大类中有什么其他的子类型,在记忆其他子类型之前我觉得应该先了解一下什么是基本类型引用类型,实际上基本类型和引用类型的主要区别是存储的区别,基本类型在栈中,而引用类型的话,引用数据的地址存储在栈中,而对象本身是存储在堆中,引用的数据地址是个16进制的数据值,它就像一把钥匙让你能够找到宝藏在什么地方。这就是基本数据类型和引用类型的区别了。

+

那么如何记住有哪些基本数据类型和引用数据类型呢,实际上只要记住了6个基本数据类型,其他的都是引用数据类型,而所有的引用数据类型的祖宗都是Object,所以引用数据类型实际上只有Object一个,那么像是Array等其他子类型,都是Object的孩子,不跟Object在一个级别上。

+

基本数据类型有哪些呢?其实挺好记的,数字,字符串,这两个一个像温柔的文学少女(string),一个像有点精于算计的男生(number),还有一个布尔类型(boolen)他像是班级里正义感爆棚的人,只论对错;另外还有个差生,没头脑似的未定义(Undefined)还有一个失了忆记不起来自己是谁的空(Null),最后还有一个新加入的插班生,总是带着口罩的标志符号(Symbol),这些人构成了这个班级的所有学生,也就是全部的基本类型,那么引用类型的对象Object呢就像及了漂亮爱化妆的班主任老师,有好多副面孔。不知道大家有没有看过一个动漫叫做《黑塔利亚》,他就是把国家都拟人化了,有了各自的性格,我很喜欢看,我觉得这些数据类型也各有各的特点,像这些国家一样,好了脑洞有点挖深了,有人会说我不就是这么几个简单的数据类型嘛,硬记下来不就好了,但是知识总有你硬记不下来的时候,最好的方法也是速记领域最为常用的方法,就是把你不熟悉的知识与你感兴趣的画面或者既往的知识串联起来,这样就能达到很好的记忆,如果你不喜欢动漫(怎么会有不喜欢动漫的人!!!),可以试试用其他的方法记,当然你如果硬要死记硬背那我也没办法,我继续开我的脑洞。
如果你是学过Java开发的同学(如果是计算机专业出来的,应该都或多或少学过,非计算机专业的我也不知道说啥了。。),数字在Java中是分成 byte/short/int/long 的,但是在Js中没有那么多,就一个Number,它是浮点类型,基于 IEEE 754标准实现,刚才我不是说了Number是个精于算计的男生,精于算计就是说他分毫不差,这样浮点型就很好的记了下来,这个754的标准可以不记,如果非要记的话,你可以记成他是自称IEEE 754团体的成员。最后Number身后还跟着一个小弟,叫做NaN,他虽然是Number的小弟,但他总是说话不算数,自己说过什么都不承认。所以NaN!=NaN。
老师是个爱化妆的老师,而这些学生也不是普通的学生,在学校他们是老老实实的基本类型,放了学之后一打扮,他们就各有了其他的能力,这个过程叫做装箱,具体的后面再说。(好了快回到现实吧你!)

+

如果基础数据是字面量类型,那么他们就像是在上课的学生,只是学生而已,而当他们调用方法时,他们就成了下课后各种技能都有的新新人类,这个过程有时候是显示的,就像是有些学生喜欢大声嚷嚷,而更多的是你不自觉中就用到了装箱操作,是Js引擎提供的能力,就像是有些闷骚的学生一样。

+
1
2
3
// js代码
let aNumber = 111 // 这只是字面量,不是 number 类型
aNumber.toString() // 装箱操作自动转化成数字对象,使用时候才会转换为对象类型
+

对象有个深浅拷贝的知识点必须要会,对象因为是引用数据类型,在栈中存储的是地址,当用另一个变量接收了之前的变量,那么就好像把钥匙复制了一把,两把钥匙开的还是同一个门,而深拷贝呢就像是照着原来的样板间又造了一个一模一样的房间,这两个房间长得一样,但就是两个房间,钥匙自然也是不一样的,所以呢,当往房间里搬家具的时候,浅拷贝搬进去的是一个房间,所以两把钥匙打开之后看到的都是多了家具,而深拷贝的话,我只是往样板间搬了家具,所以照着装修的房间里是不可能有的,这就是浅拷贝原数据会受影响,而深拷贝不会。

+
1
2
3
4
5
6
// js代码

let a = { name: 'FE' }
let b = a
b.name = 'EF'
console.log(a.name) // EF 浅拷贝原数据会受影响
+

内置数据类型检测 Typeof — 类型判断

+

typeof 对于基本类型,除了 null 都可以显示正确的类型

+
+

typeof 就像是学校的教导主任一样,他有着一双火眼金睛,不管是哪个同学,穿了什么样的衣服,他一问就能问出来这个学生是谁,大家都怕他,但是Null因为失忆了,他也不知道教导主任是谁,所以typeof就拿他也没办法,因为他不怕教导主任,教导主任甚至会以为他是老师呢。

+
1
2
3
4
5
6
7
8
9
10
// js代码

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof b // b 没有声明,但是还会显示 undefined

typeof null // 'object' 这是JS中的bug
+ +
+

typeof 对于对象,除了函数都会显示 object

+
+
1
2
3
4
5
// js代码

typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
+ +
+

知识扩展:为什么会出现这种情况呢?因为在 JS 的最初版本中,使用的是 32 位系统,为了性能考虑使用低位存储了变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。

+
+

如果我们想获得一个变量的正确类型,可以通过 Object.prototype.toString.call(xx)。这样我们就可以获得类似 [object Type] 的字符串。

+
+

小知识扩展

+
+
1
2
3
4
5
6
7
8
9
10
let a
// 我们也可以这样判断 undefined
a === undefined
// 但是 undefined 不是保留字,能够在低版本浏览器被赋值
let undefined = 1
// 这样判断就会出错
// 所以可以用下面的方式来判断,并且代码量更少
// 因为 void 后面随便跟上一个组成表达式
// 返回就是 undefined
a === void 0
+ +

类型转换

转Boolean

一句话可以概括

+
+

在条件判断时,除了 undefinednullfalseNaN''0-0,其他所有值都转为 true,包括所有对象。

+
+

从上面这段话可以看出来,undefinednull 是基本类型之二,而false是布尔类型的假值,NaN是数字类型的无效值,''是字符串类型的空值,而0-0都是数字类型的零值,可以看到,除了0-0有些特殊,除了插班生Symbol,剩下的都是基本类型的假值,由此实际上就很好记了,有时候数字这个容易忘,但是记住“非0既真”这句话就好了。

+

对象转基本类型

对象在转换基本类型时,首先是先会调用ToPrimitive(原始类型),如果有hint参数调用对应的的类型方法,如果没有那默认先会调用 valueOf 然后调用 toString。如果返回了基本类型,结束。如果都没返回,那么Error但是注意这两个方法你是可以重写的。

+
+

Symbol.toPrimitive 是一个内置的 Symbol 值,它是作为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数。(来自MDN的解释)
需要解释的关键机制是ToPrimitive函数。该函数是将任意值转换为相应的基本类型值。如果输入的就是一个基本类型值,那么将不做修改,被直接返回。如果值是非基本类型值,它将调用内部方法 [[DefaultValue]] 为对象找到一个默认值。
[[DefaultValue]]是每一个对象的内部属性。该方法需要一个可选的参数hint,值是Number或String。如果没有提供hint,则默认为Number(除非该对象是Date,在这种情况下默认为String)。然后将调用toString和valueOf去寻找基本类型值。在这里hint就起到作用了。如果hint参数值为Number,valueOf将先被调用,如果hint是String的话,则toString被先调用。
[[DefaultValue]] 返回的值一定是基本类型值。如果不是,一个TypeError 将会被抛出。这就意味着为了在这种情况下有意义,toString和valueOf应该返回基本类型值。

+
+

四则运算符

+

只有当加法运算时,其中一方是字符串类型,就会把另一个也转为字符串类型。其他运算只要其中一方是数字,那么另一方就转为数字。并且加法运算会触发三种类型转换:将值转换为原始值,转换为数字,转换为字符串。

+
+
1
2
3
4
5
6
7
8
// js代码

1 + '1' // '11' 这里会是面试陷阱
2 * '2' // 4
[1, 2] + [2, 1] // '1,22,1'
// [1, 2].toString() -> '1,2'
// [2, 1].toString() -> '2,1'
// '1,2' + '2,1' = '1,22,1'
+

*面试陷阱题之 ++ *

+
1
2
3
4
5
6
7
8
9
// 问表达式 'a' + + 'b' 返回结果是什么?
// 答案是 'aNaN'
'a' + + 'b' -> // 一元运算符优先级高
'a' + (+ 'b') -> // +'b'转数字类型,非有效结果是NaN
'a' + NaN.toString() -> // NaN调用toString()成字符串'NaN'
'aNaN' // 字符串接到一起后'aNaN'

// 类似题 '1' + + '4' 返回结果是什么
// 其实就是'4' -> 4 -> '4' 最后还是'14'
+ +

== 操作符 — 类型比较

+

相等运算符的运算规则如下:
1、如果两个值类型相同,进行 === 比较。(这个非常好理解,就不多说了)
(1)数字比大小
(2)字符串就通过 unicode 字符索引来比较
2、如果两个值类型不同,他们可能相等。根据下面规则进行类型转换再比较:
(1)如果一个是null、一个是undefined,那么[相等]。 // 这个有点特殊需要单独记
(2)如果任一值是字符串,另一个值是数值,在比较相等性之前先将字符串转换为数值;即是调用Number()函数。
(3)如果任一值时布尔值,则在比较相等性之前先将其转换为数值,即是调用Number()函数。
(4)如果一个是对象,另一个是数值或字符串,把对象转换成基础类型的值再比较。对象转换成基础类型,利用它的toString或者valueOf方法。 (js核心内置类ToPrimitive,会尝试valueOf先于toString;例外的是Date,Date利用的是toString转换。)

+
+

1不说,2(1)的话单独记,其他基本类型转数字比较,引用性数据类型调用ToPrimitive转换成基本数据类型

+

首先还是一道面试题

+
1
2
// [] == ![] 结果是什么
[] == ![] // true
+

为什么呢?我们来解析一下,

+
    +
  1. 首先我们看一下右侧,![]肯定是要先转换成boolen类型了吧,那么[]的布尔类型是什么呢,上面转换布尔的时候我们说过,除了那五个基本类型的假值以及正负0之外,都是真值,所以[] -> true ![] -> false
  2. +
  3. ToPrimitive(false)->0右边的值得出了数值类型的原始值
  4. +
  5. 看左边ToPrimitive([])->[].toString()->''
  6. +
  7. Number('')->0
  8. +
  9. 比较左右 0 == 0 -> true
  10. +
+

最后

到这里JS的内置数据类型及类型的转换和比较就讲完了,相信大家看过以后一定会记得住的
PS:突然好像学画漫画,《JS数据结构们》,一定大火,哈哈哈😂

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 数据类型、类型判断、类型转换、类型比较

+

文章作者:

+

发布时间:2020年01月02日 - 08:22

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/02/FE-guide-dataType/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/02/FE-guide-instanceof/index.html b/en/2020/01/02/FE-guide-instanceof/index.html new file mode 100644 index 0000000000..2d122856f9 --- /dev/null +++ b/en/2020/01/02/FE-guide-instanceof/index.html @@ -0,0 +1,590 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- instanceof | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- instanceof +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

instanceof

instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype
举例:

+
1
a instanceof Object
+

判断 Objectprototype 是否在 a 的原型链上。

+

我们也可以试着实现一下 instanceof

+
1
2
3
4
5
6
7
8
9
10
11
12
function myInstanceof(left, right) { // left 表示左表达式,right 表示右表达式
let prototype = right.prototype // 获得类型的原型
left = left.__proto__ // 获得对象的原型

while (true) { // 判断对象的类型是否等于类型的原型
if (left === null)
return false
if (prototype === left)
return true
left = left.__proto__
}
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- instanceof

+

文章作者:

+

发布时间:2020年01月02日 - 12:29

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/02/FE-guide-instanceof/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/02/FE-guide-new/index.html b/en/2020/01/02/FE-guide-new/index.html new file mode 100644 index 0000000000..2b1bcfb0ed --- /dev/null +++ b/en/2020/01/02/FE-guide-new/index.html @@ -0,0 +1,608 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- new | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- new +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

new 一个对象的过程

    +
  1. 新生成了一个对象
  2. +
  3. 链接到原型
  4. +
  5. 绑定 this
  6. +
  7. 返回新对象
  8. +
+

在调用 new 的过程中会发生以上四件事情,我们也可以试着来自己实现一个 new

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

function create() {
// 创建一个空的对象
let obj = new Object()
// 获得构造函数
let Con = [].shift.call(arguments)
// 链接到原型
obj.__proto__ = Con.prototype
// 绑定 this,执行构造函数
let result = Con.apply(obj, arguments)
// 确保 new 出来的是个对象
return typeof result === 'object' ? result : obj
}
+ +

对于实例对象来说,都是通过 new 产生的,无论是 function Foo() 还是 let a = { b : 1 }

+

对于创建一个对象来说,更推荐使用字面量的方式创建对象(无论性能上还是可读性)。因为你使用 new Object() 的方式创建对象需要通过作用域链一层层找到 Object,但是你使用字面量的方式就没这个问题。

+
1
2
3
4
// js代码

function Foo() {} // function 就是个语法糖,内部等同于 new Function()
let a = { b: 1 } // 这个字面量内部也是使用了 new Object()
+

对于 new 来说,还需要注意下运算符优先级。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

function Foo() {
return this;
}
Foo.getName = function () {
console.log('1');
};
Foo.prototype.getName = function () {
console.log('2');
};

new Foo.getName(); // -> 1
new Foo().getName(); // -> 2
+ +

从上图可以看出,new Foo() 的优先级大于 new Foo ,所以对于上述代码来说可以这样划分执行顺序

+
1
2
3
4
// js代码

new (Foo.getName());
(new Foo()).getName();
+ +
    +
  • 对于第一个函数来说,先执行了 Foo.getName() ,所以结果为 1;
  • +
  • 对于后者来说,先执行 new Foo() 产生了一个实例,然后通过原型链找到了 Foo 上的 getName 函数,所以结果为 2。
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- new

+

文章作者:

+

发布时间:2020年01月02日 - 20:07

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/02/FE-guide-new/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/02/FE-guide-prototype/index.html b/en/2020/01/02/FE-guide-prototype/index.html new file mode 100644 index 0000000000..133f46cafc --- /dev/null +++ b/en/2020/01/02/FE-guide-prototype/index.html @@ -0,0 +1,646 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 原型 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 原型 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

原型

yuanxing.png
每个函数都有 prototype 属性,除了 Function.prototype.bind(),该属性指向原型对象,简称原型prototype原型对象里的constructor指向构造函数本身。
每个对象都有 __proto__ 属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 _proto_ 来访问。
对象可以通过 __proto__ 来寻找不属于该对象的属性,__proto__ 将对象连接起来组成了原型链

+ +

总结

+

prototype 原型对象

什么是原型?

每个函数都具有 prototype 属性,它被默认成一个对象,即原型对象
首先来介绍下 prototype 属性。这是一个显式原型属性,只有函数才拥有该属性。基本上所有函数都有这个属性,但是也有一个例外

+
1
2
// js代码
let fun = Function.prototype.bind()
+

如果你以上述方法创建一个函数,那么可以发现这个函数是不具有 prototype 属性的。

+

prototype 如何产生的

当我们声明一个函数时,这个属性就被自动创建了。

+
1
2

function Foo() {}
+

并且这个属性的值是一个对象(也就是原型),只有一个属性 constructor
constructor 对应着构造函数,也就是 Foo

+

什么是原型链?

当对象使用属性时,先在自身找,有就直接用,没有就沿着proto这条链往上找,直到 Object 原型的位置,有就返回相应的值,没有就返回 underfined。

+

constructor 构造函数

什么是构造函数?

任何一个函数,只要被 new 操作符使用,就可以是一个构造函数(构造函数建议以大写开头)
另外,在 JavaScript 的内置对象中,所有的函数对象都是 Function 构造函数的实例,比如:Object、Array等

+

constructor 是一个公有且不可枚举的属性。一旦我们改变了函数的 prototype ,那么新对象就没有这个属性了(当然可以通过原型链取到 constructor)。

+

那么你肯定也有一个疑问,这个属性到底有什么用呢?其实这个属性可以说是一个历史遗留问题,在大部分情况下是没用的,在我的理解里,我认为他有两个作用:

+
    +
  1. 让实例对象知道是什么函数构造了它
  2. +
  3. 如果想给某些类库中的构造函数增加一些自定义的方法,就可以通过 xx.constructor.method 来扩展
  4. +
+

_proto_

这是每个对象都有的隐式原型属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 _proto_ 来访问。

+

因为在 JS 中是没有类的概念的,为了实现类似继承的方式,通过 _proto_ 将对象和原型联系起来组成原型链,得以让对象可以访问到不属于自己的属性。

+

实例对象的 _proto_ 如何产生的

从上图可知,当我们使用 new 操作符时,生成的实例对象拥有了 _proto_ 属性。

+
1
2
3
4
function Foo() {}
// 这个函数是 Function 的实例对象
// function 就是一个语法糖
// 内部调用了 new Function(...)
+

所以可以说,在 new 的过程中,新对象被添加了 _proto_ 并且链接到构造函数的原型上。

+

new 的过程

    +
  1. 新生成了一个对象
  2. +
  3. 链接到原型
  4. +
  5. 绑定 this
  6. +
  7. 返回新对象
  8. +
+

在调用 new 的过程中会发生以上四件事情,我们也可以试着来自己实现一个 new

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person( name ){ 
this.name = name;
};
Person.prototype.getName = function(){
return this.name;
};

function create() {
let obj = new Object() // 从 Object.prototype 上克隆一个空的对象
let Con = [].shift.call(arguments) // 获取外部传入的构造器,此例是 Person
obj.__proto__ = Con.prototype // 指向正确的原型,链接到原型
let result = Con.apply(obj, arguments) // 绑定 this,执行构造函数,借用外部传入的构造器给 obj 设置属性
return typeof result === 'object' ? result : obj // 确保 new 出来的是个对象
}

create(Person,'lixg')
+

对于实例对象来说,都是通过 new 产生的,无论是 function Foo() 还是 let a = { b : 1 }

+

对于创建一个对象来说,更推荐使用字面量的方式创建对象。因为你使用 new Object() 的方式创建对象需要通过作用域链一层层找到 Object ,但是你使用字面量的方式就没这个问题。

+
1
2
function Foo() {} // function 就是个语法糖,内部等同于 new Function()
let a = { b: 1 } // 这个字面量内部也是使用了 new Object()
+ +

Function.proto === Function.prototype

对于对象来说,xx.__proto__.contrcutor 是该对象的构造函数,但是在图中我们可以发现 Function.__proto__ === Function.prototype,难道这代表着 Function 自己产生了自己?

+

答案肯定是否认的,要说明这个问题我们先从 Object 说起。

+

从图中我们可以发现,所有对象都可以通过原型链最终找到 Object.prototype ,虽然 Object.prototype 也是一个对象,但是这个对象却不是 Object 创造的,而是引擎自己创建了 Object.prototype 。所以可以这样说,所有实例都是对象,但是对象不一定都是实例。

+

接下来我们来看 Function.prototype 这个特殊的对象,如果你在浏览器将这个对象打印出来,会发现这个对象其实是一个函数。

+

我们知道函数都是通过 new Function() 生成的,难道 Function.prototype 也是通过 new Function() 产生的吗?答案也是否定的,这个函数也是引擎自己创建的。首先引擎创建了 Object.prototype ,然后创建了 Function.prototype ,并且通过 __proto__ 将两者联系了起来。这里也很好的解释了上面的一个问题,为什么 let fun = Function.prototype.bind() 没有 prototype 属性。因为 Function.prototype 是引擎创建出来的对象,引擎认为不需要给这个对象添加 prototype 属性。

+

所以我们又可以得出一个结论,不是所有函数都是 new Function() 产生的。
有了 Function.prototype 以后才有了 function Function() ,然后其他的构造函数都是 function Function() 生成的。

+

现在可以来解释 Function.__proto__ === Function.prototype 这个问题了。因为先有的 Function.prototype 以后才有的 function Function() ,所以也就不存在鸡生蛋蛋生鸡的悖论问题了。对于为什么 Function.__proto__ 会等于 Function.prototype ,个人的理解是:其他所有的构造函数都可以通过原型链找到 Function.prototype ,并且 function Function() 本质也是一个函数,为了不产生混乱就将 function Function()__proto__ 联系到了 Function.prototype 上。

+

总结

    +
  • Object 是所有对象的爸爸,所有对象都可以通过 __proto__ 找到它
  • +
  • Function 是所有函数的爸爸,所有函数都可以通过 __proto__ 找到它
  • +
  • Function.prototypeObject.prototype 是两个特殊的对象,他们由引擎来创建
  • +
  • 除了以上两个特殊对象,其他对象都是通过构造器 new 出来的
  • +
  • 函数的 prototype 是一个对象,也就是原型
  • +
  • 对象的 __proto__ 指向原型, __proto__ 将对象和原型连接起来组成了原型链
    总结
  • +
+

归纳

ES 把对象定义为:“无序属性的集合,其属性可以包含基本值,对象和函数”。
严格来讲,这就相当于说对象是一组没有特定顺序的值。ES 中的构造函数可以用来创建特定类型的对象,用来在创建对象时初始化对象。它的特点是,一般为大写字母开头,使用 new 操作符来实例化对象,比如:

+
1
2
3
4
function Person() {}
var person = new Person();
person.name = "Kevin";
console.log(person.name); // Kevin
+ +

Person 就是构造函数, person 就是对象。对于对象而言,每个 JS 对象一定对应一个原型对象,并从原型对象继承属性和方法。对象 __proto__ 属性的值就是它所对应的原型对象。对象的 __proto__ 指向自己构造函数的 prototype 。所以对象的原型链就是 obj.__proto__.proto__.... 。对于函数而言,只有函数才有 prototype 属性, Person.prototype 是一个对象,并且有两个属性, 一个是 constructor 指向其构造函数 Person , 一个是 __proto__ 属性:是一个对象,指向上一层的原型。原型链的尽头是 Object.prototype 。所有对象均从 Object.prototype 继承属性。Function.prototypeFunction.__proto__ 为同一对象。Object/Array/String 等等构造函数本质上和 Function 一样,均继承于 Function.prototypeFunction.prototype 直接继承 Object.prototype 。这里的 ObjectFunction 有点鸡和蛋的问题,总结:先有 Object.prototype(原型链顶端),Function.prototype 继承 Object.prototype 而产生,最后,FunctionObject 和其它构造函数继承 Function.prototype 而产生。属性查找时,先在对象自己上找,找不到才会一步步根据原型链往上找。
继承

+

关联阅读

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain
说说原型(prototype)、原型链和原型继承

+

扩展阅读

继承

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 原型

+

文章作者:

+

发布时间:2020年01月02日 - 11:22

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/02/FE-guide-prototype/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/02/FE-guide-this/index.html b/en/2020/01/02/FE-guide-this/index.html new file mode 100644 index 0000000000..d468925bed --- /dev/null +++ b/en/2020/01/02/FE-guide-this/index.html @@ -0,0 +1,591 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- this | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- this +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

this

this 是很多人会混淆的概念,但是其实他一点都不难,你只需要记住几个规则就可以了。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// js代码

function foo() {
console.log(this.a)
}
var a = 1
foo()

var obj = {
a: 2,
foo: foo
}
obj.foo()

// 以上两者情况 `this` 只依赖于调用函数前的对象,优先级是第二个情况大于第一个情况

// 以下情况是优先级最高的,`this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向
var c = new foo()
c.a = 3
console.log(c.a)

// 还有种就是利用 `call` , `apply` , `bind` 改变 `this` ,这个优先级仅次于 `new`
+

以上几种情况明白了,很多代码中的 this 应该就没什么问题了,下面让我们看看箭头函数中的 this

+
1
2
3
4
5
6
7
8
9
10
// js代码

function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())
+

箭头函数其实是没有 this 的,这个函数中的 this 只取决于他外面的第一个不是箭头函数的函数的 this 。在这个例子中,因为调用 a 符合前面代码中的第一个情况,所以 thiswindow。并且 this 一旦绑定了上下文,就不会被任何代码改变。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- this

+

文章作者:

+

发布时间:2020年01月02日 - 12:59

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/02/FE-guide-this/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2020/01/02/FE-guide-\346\211\247\350\241\214\344\270\212\344\270\213\346\226\207/index.html" "b/en/2020/01/02/FE-guide-\346\211\247\350\241\214\344\270\212\344\270\213\346\226\207/index.html" new file mode 100644 index 0000000000..6b381c9e97 --- /dev/null +++ "b/en/2020/01/02/FE-guide-\346\211\247\350\241\214\344\270\212\344\270\213\346\226\207/index.html" @@ -0,0 +1,610 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 执行上下文 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 执行上下文 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

执行上下文

当执行 JS 代码时,会产生三种执行上下文

+
    +
  • 全局执行上下文
  • +
  • 函数执行上下文
  • +
  • eval 执行上下文
  • +
+

每个执行上下文中都有三个重要的属性

+
    +
  • 变量对象( VO ),包含变量、函数声明和函数的形参,该属性只能在全局上下文中访问
  • +
  • 作用域链( JS 采用词法作用域,也就是说变量的作用域是在定义时就决定了)
  • +
  • this
    1
    2
    3
    4
    5
    6
    7
    // js代码

    var a = 10
    function foo(i) {
    var b = 20
    }
    foo()
    +对于上述代码,执行栈中有两个上下文:全局上下文和函数 foo 上下文。
    1
    2
    3
    4
    5
    6
    // js代码

    stack = [
    globalContext,
    fooContext
    ]
    +对于全局上下文来说, VO 大概是这样的
    1
    2
    3
    4
    5
    6
    7
    // js代码

    globalContext.VO === globe
    globalContext.VO = {
    a: undefined,
    foo: <Function>,
    }
    +对于函数 foo 来说, VO 不能访问,只能访问到活动对象( AO
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // js代码

    fooContext.VO === foo.AO
    fooContext.AO {
    i: undefined,
    b: undefined,
    arguments: <>
    }
    // arguments 是函数独有的对象(箭头函数没有)
    // 该对象是一个伪数组,有 `length` 属性且可以通过下标访问元素
    // 该对象中的 `callee` 属性代表函数本身
    // `caller` 属性代表函数的调用者
    +对于作用域链,可以把它理解成包含自身变量对象和上级变量对象的列表,通过 [[Scope]] 属性查找上级变量
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    fooContext.[[Scope]] = [
    globalContext.VO
    ]
    fooContext.Scope = fooContext.[[Scope]] + fooContext.VO
    fooContext.Scope = [
    fooContext.VO,
    globalContext.VO
    ]
    +接下来让我们看一个老生常谈的例子, var
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    b() // call b
    console.log(a) // undefined

    var a = 'Hello world'

    function b() {
    console.log('call b')
    }
    +想必以上的输出大家肯定都已经明白了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行上下文时,会有两个阶段。第一个阶段是创建的阶段(具体步骤是创建 VO ), JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined ,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用。
  • +
+

在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

b() // call b second

function b() {
console.log('call b fist')
}
function b() {
console.log('call b second')
}
var b = 'Hello world'
+

var 会产生很多错误,所以在 ES6 中引入了 letlet 不能在声明前使用,但是这并不是常说的 let 不会提升, let 提升了声明但没有赋值,因为临时死区导致了并不能在声明前使用。

+

对于非匿名的立即执行函数需要注意以下一点

+
1
2
3
4
5
6
7
// js代码

var foo = 1
(function foo() {
foo = 10
console.log(foo)
}()) // -> ƒ foo() { foo = 10 ; console.log(foo) }
+

因为当 JS 解释器在遇到非匿名的立即执行函数时,会创建一个辅助的特定对象,然后将函数名称作为这个对象的属性,因此函数内部才可以访问到 foo ,但是这个值又是只读的,所以对它的赋值并不生效,所以打印的结果还是这个函数,并且外部的值也没有发生更改。

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

specialObject = {};

Scope = specialObject + Scope;

foo = new FunctionExpression;
foo.[[Scope]] = Scope;
specialObject.foo = foo; // {DontDelete}, {ReadOnly}

delete Scope[0]; // remove specialObject from the front of scope chain
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 执行上下文

+

文章作者:

+

发布时间:2020年01月02日 - 13:18

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/02/FE-guide-%E6%89%A7%E8%A1%8C%E4%B8%8A%E4%B8%8B%E6%96%87/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/03/FE-guide-Generator/index.html b/en/2020/01/03/FE-guide-Generator/index.html new file mode 100644 index 0000000000..68b124a8fe --- /dev/null +++ b/en/2020/01/03/FE-guide-Generator/index.html @@ -0,0 +1,589 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- Generator 生成器 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- Generator 生成器 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Generator 实现

GeneratorES6 中新增的语法,和 Promise 一样,都可以用来异步编程

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

// 使用 * 表示这是一个 Generator 函数
// 内部可以通过 yield 暂停代码
// 通过调用 next 恢复执行
function* test() {
let a = 1 + 2;
yield 2;
yield 3;
}
let b = test();
console.log(b.next()); // > { value: 2, done: false }
console.log(b.next()); // > { value: 3, done: false }
console.log(b.next()); // > { value: undefined, done: true }
+

从以上代码可以发现,加上 * 的函数执行后拥有了 next 函数,也就是说函数执行后返回了一个对象。每次调用 next 函数可以继续执行被暂停的代码。以下是 Generator 函数的简单实现

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// js代码

// cb 也就是编译过的 test 函数
function generator(cb) {
return (function() {
var object = {
next: 0,
stop: function() {}
};

return {
next: function() {
var ret = cb(object);
if (ret === undefined) return { value: undefined, done: true };
return {
value: ret,
done: false
};
}
};
})();
}
// 如果你使用 babel 编译后可以发现 test 函数变成了这样
function test() {
var a;
return generator(function(_context) {
while (1) {
switch ((_context.prev = _context.next)) {
// 可以发现通过 yield 将代码分割成几块
// 每次执行 next 函数就执行一块代码
// 并且表明下次需要执行哪块代码
case 0:
a = 1 + 2;
_context.next = 4;
return 2;
case 4:
_context.next = 6;
return 3;
// 执行完毕
case 6:
case "end":
return _context.stop();
}
}
});
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- Generator 生成器

+

文章作者:

+

发布时间:2020年01月03日 - 03:05

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-Generator/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/03/FE-guide-Map-FlatMap-Reduce/index.html b/en/2020/01/03/FE-guide-Map-FlatMap-Reduce/index.html new file mode 100644 index 0000000000..482efea612 --- /dev/null +++ b/en/2020/01/03/FE-guide-Map-FlatMap-Reduce/index.html @@ -0,0 +1,602 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- Map、FlatMap 和 Reduce | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- Map、FlatMap 和 Reduce +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Map、FlatMap 和 Reduce

Map

Map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后 append 到新的数组中。

+
1
2
3
// js代码

[1, 2, 3].map((v) => v + 1) // -> [2, 3, 4]
+

Map 有三个参数,分别是当前索引元素索引原数组

+

FlatMap

FlatMapmap 的作用几乎是相同的,但是对于多维数组来说,会将原数组降维。可以将 FlatMap 看成是 map + flatten ,目前该函数在浏览器中还不支持。

+
1
2
3
// js代码

[1, [2], 3].flatMap((v) => v + 1) // -> [2, 3, 4]
+

如果想将一个多维数组彻底的降维,可以这样实现

+
1
2
3
4
5
const flattenDeep = (arr) => Array.isArray(arr)
? arr.reduce( (a, b) => [...a, ...flattenDeep(b)] , [])
: [arr]

flattenDeep([1, [[2], [3, [4]], 5]])
+ +

Reduce 升序执行

Reduce 作用是数组中的值组合起来,最终得到一个值
reduce() 方法对数组中的每个元素执行一个由您提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。

+

reducer 函数接收4个参数:

+
    +
  • Accumulator (acc) (累计器)
  • +
  • Current Value (cur) (当前值)
  • +
  • Current Index (idx) (当前索引)
  • +
  • Source Array (src) (源数组)
    您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
// js代码

function a() {
console.log(1);
}

function b() {
console.log(2);
}

[a, b].reduce((a, b) => a(b()))
// -> 2 1
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- Map、FlatMap 和 Reduce

+

文章作者:

+

发布时间:2020年01月03日 - 03:35

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-Map-FlatMap-Reduce/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/03/FE-guide-Module/index.html b/en/2020/01/03/FE-guide-Module/index.html new file mode 100644 index 0000000000..0a5e265c63 --- /dev/null +++ b/en/2020/01/03/FE-guide-Module/index.html @@ -0,0 +1,601 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 模块化 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 模块化 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

模块化

在有 Babel 的情况下,我们可以直接使用 ES6 的模块化

+
1
2
3
4
5
6
7
8
9
10
// js代码

// file a.js
export function a() {}
export function b() {}
// file b.js
export default function() {}

import {a, b} from './a.js'
import XXX from './b.js'
+

CommonJS

CommonJsNode 独有的规范,浏览器中使用就需要用到 Browserify 解析了。

+
1
2
3
4
5
6
7
8
9
10
11
12
// js代码

// a.js
module.exports = {
a: 1
}
// or
exports.a = 1

// b.js
var module = require('./a.js')
module.a // -> log 1
+

在上述代码中, module.exportsexports 很容易混淆,让我们来看看大致内部实现

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// js代码

var module = require('./a.js')
module.a
// 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {
a: 1
}
// 基本实现
var module = {
exports: {} // exports 就是个空对象
}
// 这个是为什么 exports 和 module.exports 用法相似的原因
var exports = module.exports
var load = function (module) {
// 导出的东西
var a = 1
module.exports = a
return module.exports
};
+

再来说说 module.exports 和 exports,用法其实是相似的,但是不能对 exports 直接赋值,不会有任何效果。

+

对于 CommonJS 和 ES6 中的模块化的两者区别是:

+
    +
  • 前者支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
  • +
  • 前者是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
  • +
  • 前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是后者采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
  • +
  • 后者会编译成 require/exports 来执行的
  • +
+

AMD

AMD 是由 RequireJS 提出的

+
1
2
3
4
5
6
7
8
9
10
11
12
13
// js代码

// AMD
define(['./a', './b'], function(a, b) {
a.do()
b.do()
})
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
var b = require('./b')
b.doSomething()
})
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 模块化

+

文章作者:

+

发布时间:2020年01月03日 - 00:41

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-Module/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/03/FE-guide-Promise/index.html b/en/2020/01/03/FE-guide-Promise/index.html new file mode 100644 index 0000000000..0d45b7c650 --- /dev/null +++ b/en/2020/01/03/FE-guide-Promise/index.html @@ -0,0 +1,592 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- Promise | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- Promise +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Promise 实现

PromiseES6 新增的语法,解决了回调地狱的问题。

+

可以把 Promise 看成一个状态机。初始是 pending 状态,可以通过函数 resolvereject ,将状态转变为 resolved 或者 rejected 状态,状态一旦改变就不能再次变化

+

then 函数会返回一个 Promise 实例,并且该返回值是一个新的实例而不是之前的实例。因为 Promise 规范规定除了 pending 状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个 then 调用就失去意义了。

+

对于 then 来说,本质上可以把它看成是 flatMap

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// js代码

// 三种状态
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
// promise 接收一个函数参数,该函数会立即执行
function MyPromise(fn) {
let _this = this;
_this.currentState = PENDING;
_this.value = undefined;
// 用于保存 then 中的回调,只有当 promise
// 状态为 pending 时才会缓存,并且每个实例至多缓存一个
_this.resolvedCallbacks = [];
_this.rejectedCallbacks = [];

_this.resolve = function (value) {
if (value instanceof MyPromise) {
// 如果 value 是个 Promise,递归执行
return value.then(_this.resolve, _this.reject)
}
setTimeout(() => { // 异步执行,保证执行顺序
if (_this.currentState === PENDING) {
_this.currentState = RESOLVED;
_this.value = value;
_this.resolvedCallbacks.forEach(cb => cb());
}
})
};

_this.reject = function (reason) {
setTimeout(() => { // 异步执行,保证执行顺序
if (_this.currentState === PENDING) {
_this.currentState = REJECTED;
_this.value = reason;
_this.rejectedCallbacks.forEach(cb => cb());
}
})
}
// 用于解决以下问题
// new Promise(() => throw Error('error))
try {
fn(_this.resolve, _this.reject);
} catch (e) {
_this.reject(e);
}
}

MyPromise.prototype.then = function (onResolved, onRejected) {
var self = this;
// 规范 2.2.7,then 必须返回一个新的 promise
var promise2;
// 规范 2.2.onResolved 和 onRejected 都为可选参数
// 如果类型不是函数需要忽略,同时也实现了透传
// Promise.resolve(4).then().then((value) => console.log(value))
onResolved = typeof onResolved === 'function' ? onResolved : v => v;
onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;

if (self.currentState === RESOLVED) {
return (promise2 = new MyPromise(function (resolve, reject) {
// 规范 2.2.4,保证 onFulfilled,onRjected 异步执行
// 所以用了 setTimeout 包裹下
setTimeout(function () {
try {
var x = onResolved(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}

if (self.currentState === REJECTED) {
return (promise2 = new MyPromise(function (resolve, reject) {
setTimeout(function () {
// 异步执行onRejected
try {
var x = onRejected(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}

if (self.currentState === PENDING) {
return (promise2 = new MyPromise(function (resolve, reject) {
self.resolvedCallbacks.push(function () {
// 考虑到可能会有报错,所以使用 try/catch 包裹
try {
var x = onResolved(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});

self.rejectedCallbacks.push(function () {
try {
var x = onRejected(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});
}));
}
};
// 规范 2.3
function resolutionProcedure(promise2, x, resolve, reject) {
// 规范 2.3.1,x 不能和 promise2 相同,避免循环引用
if (promise2 === x) {
return reject(new TypeError("Error"));
}
// 规范 2.3.2
// 如果 x 为 Promise,状态为 pending 需要继续等待否则执行
if (x instanceof MyPromise) {
if (x.currentState === PENDING) {
x.then(function (value) {
// 再次调用该函数是为了确认 x resolve 的
// 参数是什么类型,如果是基本类型就再次 resolve
// 把值传给下个 then
resolutionProcedure(promise2, value, resolve, reject);
}, reject);
} else {
x.then(resolve, reject);
}
return;
}
// 规范 2.3.3.3.3
// reject 或者 resolve 其中一个执行过得话,忽略其他的
let called = false;
// 规范 2.3.3,判断 x 是否为对象或者函数
if (x !== null && (typeof x === "object" || typeof x === "function")) {
// 规范 2.3.3.2,如果不能取出 then,就 reject
try {
// 规范 2.3.3.1
let then = x.then;
// 如果 then 是函数,调用 x.then
if (typeof then === "function") {
// 规范 2.3.3.3
then.call(
x,
y => {
if (called) return;
called = true;
// 规范 2.3.3.3.1
resolutionProcedure(promise2, y, resolve, reject);
},
e => {
if (called) return;
called = true;
reject(e);
}
);
} else {
// 规范 2.3.3.4
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
// 规范 2.3.4,x 为基本类型
resolve(x);
}
}
+

以上就是根据 Promise / A+ 规范来实现的代码,可以通过 promises-aplus-tests 的完整测试

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- Promise

+

文章作者:

+

发布时间:2020年01月03日 - 02:52

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-Promise/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/03/FE-guide-async-Proxy/index.html b/en/2020/01/03/FE-guide-async-Proxy/index.html new file mode 100644 index 0000000000..7f671db0a1 --- /dev/null +++ b/en/2020/01/03/FE-guide-async-Proxy/index.html @@ -0,0 +1,589 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- Proxy | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- Proxy +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Proxy

ProxyES6 中新增的功能,可以用来自定义对象中的操作

+
1
2
3
4
5
// js代码

let p = new Proxy(target, handler);
// `target` 代表需要添加代理的对象
// `handler` 用来自定义对象中的操作
+

可以很方便的使用 Proxy 来实现一个数据绑定和监听

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(target, property)
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
setBind(value);
return Reflect.set(target, property, value);
}
};
return new Proxy(obj, handler);
};

let obj = { a: 1 }
let value
let p = onWatch(obj, (v) => {
value = v
}, (target, property) => {
console.log(`Get '${property}' = ${target[property]}`);
})
p.a = 2 // bind `value` to `2`
p.a // -> Get 'a' = 2
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- Proxy

+

文章作者:

+

发布时间:2020年01月03日 - 04:00

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-async-Proxy/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/03/FE-guide-async-await/index.html b/en/2020/01/03/FE-guide-async-await/index.html new file mode 100644 index 0000000000..94544f6aaa --- /dev/null +++ b/en/2020/01/03/FE-guide-async-await/index.html @@ -0,0 +1,603 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- async 和 await | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- async 和 await +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

async 和 await

一个函数如果加上 async ,那么该函数就会返回一个 Promise

+
1
2
3
4
async function test() {
return "1";
}
console.log(test()); // -> Promise {<resolved>: "1"}
+

可以把 async 看成将函数返回值使用 Promise.resolve() 包裹了下。
await 只能在 async 函数中使用

+
1
2
3
4
5
6
7
8
9
10
11
12
13
function sleep() {
return new Promise(resolve => {
setTimeout(() => {
console.log('finish')
resolve("sleep");
}, 2000);
});
}
async function test() {
let value = await sleep();
console.log("object");
}
test()
+ +

上面代码会先打印 finish 然后再打印 object 。因为 await 会等待 sleep 函数 resolve ,所以即使后面是同步代码,也不会先去执行同步代码再来执行异步代码。

+

asyncawait 相比直接使用 Promise 来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码。缺点在于滥用 await 可能会导致性能问题,因为 await 会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。

+

下面来看一个使用 await 的代码。

+
1
2
3
4
5
6
7
8
9
10
var a = 0
var b = async () => {
a = a + await 10
console.log('2', a) // -> '2' 10
a = (await 10) + a
console.log('3', a) // -> '3' 20
}
b()
a++
console.log('1', a) // -> '1' 1
+ +

对于以上代码你可能会有疑惑,这里说明下原理

+
    +
  • 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为在 await 内部实现了 generatorsgenerators 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
  • +
  • 因为 await 是异步操作,遇到 await 就会立即返回一个 pending 状态的 Promise 对象,暂时返回执行代码的控制权,使得函数外的代码得以继续执行,所以会先执行 console.log('1', a)
  • +
  • 这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 10
  • +
  • 然后后面就是常规执行代码了
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- async 和 await

+

文章作者:

+

发布时间:2020年01月03日 - 03:52

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-async-await/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/03/FE-guide-call-apply-bind/index.html b/en/2020/01/03/FE-guide-call-apply-bind/index.html new file mode 100644 index 0000000000..c726fc92b6 --- /dev/null +++ b/en/2020/01/03/FE-guide-call-apply-bind/index.html @@ -0,0 +1,596 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- call, apply, bind 区别 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- call, apply, bind 区别 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

call, apply, bind 区别

首先说下前两者的异同。
相同: callapply 都是为了解决改变 this 的指向。
不同:传参的方式不同,除了第一个参数外, call 可以接收一个参数列表apply 只接受一个参数数组

+
1
2
3
4
5
6
7
8
9
10
11
// js代码
let anObj = {
value: 1
}
function getValue(name, age) {
console.log(name)
console.log(age)
console.log(this.value)
}
getValue.call(anObj, 'lixuguang', '31')
getValue.apply(anObj, ['lixuguang', '31'])
+

模拟实现 callapply

可以从以下几点来考虑如何实现

+
    +
  • 不传入第一个参数,那么默认为 window
  • +
  • 改变了 this 指向,让新的对象可以执行该函数。那么思路是否可以变成给新的对象添加一个函数,然后在执行完以后删除?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    Function.prototype.myCall = function (context) {
    var context = context || window // 有入参用入参,没有给 window
    context.fn = this // 给 context 添加一个属性,getValue.call(anObj, 'lixuguang', '31') => anObj.fn = getValue
    var args = [...arguments].slice(1) // 将 context 后面的参数取出来
    var result = context.fn(...args) // getValue.call(anObj, 'lixuguang', '31') => anObj.fn('yck', '24')
    delete context.fn // 删除 fn
    return result
    }
    +以上就是 call 的思路,apply 的实现也类似
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    Function.prototype.Apply = function (context) {
    var context = context || window // 有入参用入参,没有给 window
    context.fn = this // 给 context 添加一个属性,getValue.call(anObj, 'lixuguang', '31') => anObj.fn = getValue
    var args = arguments[1] // 将 context 后面的参数取出来
    var result = args?context.fn(...args):context.fn() // getValue.call(anObj, ['lixuguang', '31']) => anObj.fn('yck', '24')
    delete context.fn // 删除 fn
    return result
    }
    +bind 和其他两个方法作用也是一致的,只是该方法会返回一个函数。并且我们可以通过 bind 实现柯里化。
  • +
+

同样的,也来模拟实现下 bind

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码

Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
var _this = this
var args = [...arguments].slice(1)

return function F() { // 返回一个函数,这是 `bind` 和 `call` , `apply` 的区别
if (this instanceof F) { // 因为返回了一个函数,我们可以 new F(),所以需要判断
return new _this(...args, ...arguments)
}
return _this.apply(context, args.concat(...arguments))
}
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- call, apply, bind 区别

+

文章作者:

+

发布时间:2020年01月03日 - 02:19

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-call-apply-bind/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/03/FE-guide-copy/index.html b/en/2020/01/03/FE-guide-copy/index.html new file mode 100644 index 0000000000..137891bc4b --- /dev/null +++ b/en/2020/01/03/FE-guide-copy/index.html @@ -0,0 +1,621 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 深浅拷贝 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 深浅拷贝 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

深浅拷贝

1
2
3
4
5
6
let a = {
age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2
+

从上述例子中我们可以发现,如果给一个变量赋值一个对象,那么两者的值会是同一个引用,其中一方改变,另一方也会相应改变。
通常在开发中我们不希望出现这样的问题,我们可以使用浅拷贝来解决这个问题。

+

浅拷贝

首先可以通过 Object.assign 来解决这个问题。

+
1
2
3
4
5
6
7
8
// js代码

let a = {
age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // => 1
+

当然我们也可以通过展开运算符(…)来解决

+
1
2
3
4
5
6
let a = {
age: 1
}
let b = {...a}
a.age = 2
console.log(b.age) // => 1
+

我们还可以用很多简单的方法都能实现浅拷贝:

+
1
2
arr.slice();
arr.concat();
+ +

通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就需要使用到深拷贝了

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = {...a}
a.jobs.first = 'native'
console.log(b.jobs.first) // native
+

浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到刚开始的话题了,两者享有相同的引用。要解决这个问题,我们需要引入深拷贝。

+

深拷贝

这个问题通常可以通过 JSON.parse(JSON.stringify(object)) 来解决,这也是最好用最简单的方法,俗称乞丐版。
乞丐版

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
+

但是该方法也是有局限性的:

+
    +
  • 会忽略 undefined
  • +
  • 会忽略 symbol
  • +
  • 不能序列化函数
  • +
  • 不能解决循环引用的对象
  • +
+

举个栗子:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

let obj = {
a: 1,
b: {
c: 2,
d: 3,
},
}

obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c

let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj) // => Uncaught TypeError: Converting circular structure to JSON
+

如果你有这么一个循环引用对象,你会发现你不能通过该方法深拷贝
在遇到函数、 undefined 或者 symbol 的时候,该对象也不能正常的序列化

+
1
2
3
4
5
6
7
8
9
10
// js代码

let a = {
age: undefined,
sex: Symbol('fmale'),
jobs: function() {},
name: 'lixuguang'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // => {name: "lixuang"}
+ +

你会发现在上述情况中,该方法会忽略掉函数和 undefined
但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题,并且该函数是内置函数中处理深拷贝性能最快的。

+

那么是否可以解决函数和循环引用的问题呢?答案是肯定可以解决,接下来是基础版本的改造
基础版

+
1
2
3
4
5
6
7
8
9
10
11
function myClone(target){
if(typeof target === 'object'){ // 判断传入目标是否是object类型
let cloneTarget = {}; // 创建克隆对象
for(const key in target){ // 遍历目标对象
cloneTarget[key] = myClone(target[key]) // 递归调用 clone 方法
}
return cloneTarget;
} else {
return target // 如果不是 object 返回
}
}
+

写到这里已经可以帮助你应付一些面试官考察你的递归解决问题的能力。但是显然,这个深拷贝函数还是有一些问题。
这里只考虑了对象,没有考虑数组。
下面我们来做一个强化版的深拷贝,同时考虑对象、数组还有循环引用的问题。
强化版

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function myClone(target, map = new WeakMap()) { // WeakMap => 键对象弱引用, 可被垃圾回收
if(typeof target === 'object'){ // 判断是否是对象
let cloneTarget = Array.isArray(target) ? [] : {}; // 判断是是数组还是对象
if(map.get(target)) {
return target;
}

map.set(target, cloneTarget);

for(const key in target) {
cloneTarget[key] = myClone(target[key], map)
}
return cloneTarget;
} else {
return target;
}
}
+

当然如果你的数据中含有以上三种情况下,可以使用 lodash 的深拷贝函数。
如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// js代码

function structuralClone(obj) {
return new Promise(resolve => {
const {port1, port2} = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
});
}

var obj = {
a: 1,
b: {
c: b
}
}
// 注意该方法是异步的
// 可以处理 undefined 和循环引用对象
(async () => {
const clone = await structuralClone(obj)
})()
+ +

深拷贝实现方式2,可以深拷贝 functionsymbol,等等,堪称终极版
终极版

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// js代码

const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const argsTag = '[object Arguments]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];
function forEach(array, iteratee) {
let index = -1;
const length = array.length;
while (++index < length) {
iteratee(array[index], index);
}
return array;
}

function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}

function getType(target) {
return Object.prototype.toString.call(target);
}

function getInit(target) {
const Ctor = target.constructor;
return new Ctor();
}

function cloneSymbol(targe) {
return Object(Symbol.prototype.valueOf.call(targe));
}

function cloneReg(targe) {
const reFlags = /\w*$/;
const result = new targe.constructor(targe.source, reFlags.exec(targe));
result.lastIndex = targe.lastIndex;
return result;
}

function cloneFunction(func) {
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
if (func.prototype) {
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if (body) {
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(funcString);
}
}

function cloneOtherType(targe, type) {
const Ctor = targe.constructor;
switch (type) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(targe);
case regexpTag:
return cloneReg(targe);
case symbolTag:
return cloneSymbol(targe);
case funcTag:
return cloneFunction(targe);
default:
return null;
}
}

function clone(target, map = new WeakMap()) {
// 克隆原始类型
if (!isObject(target)) {
return target;
}

// 初始化
const type = getType(target);
let cloneTarget;
if (deepTag.includes(type)) {
cloneTarget = getInit(target, type);
} else {
return cloneOtherType(target, type);
}

// 防止循环引用
if (map.get(target)) {
return target;
}
map.set(target, cloneTarget);

// 克隆set
if (type === setTag) {
target.forEach(value => {
cloneTarget.add(clone(value));
});
return cloneTarget;
}

// 克隆map
if (type === mapTag) {
target.forEach((value, key) => {
cloneTarget.set(key, clone(value));
});
return cloneTarget;
}

// 克隆对象和数组
const keys = type === arrayTag ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone(target[key], map);
});

return cloneTarget;
}

// 调用方法
clone(target);
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 深浅拷贝

+

文章作者:

+

发布时间:2020年01月03日 - 00:05

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-copy/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/03/FE-guide-debounce-throttle/index.html b/en/2020/01/03/FE-guide-debounce-throttle/index.html new file mode 100644 index 0000000000..15b9a0e3fe --- /dev/null +++ b/en/2020/01/03/FE-guide-debounce-throttle/index.html @@ -0,0 +1,605 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 函数防抖和节流 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 函数防抖和节流 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

函数防抖和节流

在前端开发的过程中,我们经常会需要绑定一些持续触发的事件,如 resize、scroll、mousemove 等等,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。

+

通常这种情况下我们怎么去解决的呢?一般来讲,防抖和节流是比较好的解决方案。

+

防抖

你是否在日常开发中遇到一个问题,在滚动事件中需要做个复杂计算或者实现一个按钮的防二次点击操作。

+

这些需求都可以通过函数防抖动来实现。尤其是第一个需求,如果在频繁的事件回调中做复杂计算,很有可能导致页面卡顿,不如将多次计算合并为一次计算,只在一个精确点做操作。

+

PS:防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的情况下只会调用一次,而节流的 情况会每隔一定时间(参数wait)调用函数。

+

我们先来看一个袖珍版的防抖理解一下防抖的实现:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
// 缓存一个定时器id
let timer = 0
// 这里返回的函数是每次用户实际调用的防抖函数
// 如果已经设定过定时器了就清空上一次的定时器
// 开始一个新的定时器,延迟执行用户传入的方法
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
// 不难看出如果用户调用该函数的间隔小于wait的情况下,上一次的时间还未到就被清除了,并不会执行函数
+

这是一个简单版的防抖,但是有缺陷,这个防抖只能在最后调用。一般的防抖会有immediate选项,表示是否立即调用。这两者的区别,举个栗子来说:

+
    +
  • 例如在搜索引擎搜索问题的时候,我们当然是希望用户输入完最后一个字才调用查询接口,这个时候适用延迟执行的防抖函数,它总是在一连串(间隔小于wait的)函数触发之后调用。
  • +
  • 例如用户给某系统表单点提交的时候,我们希望用户点第一下的时候就去调用接口,这个情况适用立即执行的防抖函数,它总是在第一次调用,并且下一次调用必须与前一次调用的时间间隔大于wait才会触发。
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// js代码

// 这个是用来获取当前时间戳的
function now() {
return +new Date()
}
/**
* 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {boolean} immediate 设置为ture时,是否立即调用函数
* @return {function} 返回客户调用函数
*/
function debounce (func, wait = 50, immediate = true) {
let timer, context, args

// 延迟执行函数
const later = () => setTimeout(() => {
// 延迟函数执行完毕,清空缓存的定时器序号
timer = null
// 延迟执行的情况下,函数会在延迟函数中执行
// 使用到之前缓存的参数和上下文
if (!immediate) {
func.apply(context, args)
context = args = null
}
}, wait)

// 这里返回的函数是每次实际调用的函数
return function(...params) {
// 如果没有创建延迟执行函数(later),就创建一个
if (!timer) {
timer = later()
// 如果是立即执行,调用函数
// 否则缓存参数和调用上下文
if (immediate) {
func.apply(this, params)
} else {
context = this
args = params
}
// 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
// 这样做延迟函数会重新计时
} else {
clearTimeout(timer)
timer = later()
}
}
}
+

整体函数实现的不难,总结一下。

+
    +
  • 对于按钮防点击来说的实现:如果函数是立即执行的,就立即调用,如果函数是延迟执行的,就缓存上下文和参数,放到延迟函数中去执行。一旦我开始一个定时器,只要我定时器还在,你每次点击我都重新计时。一旦你点累了,定时器时间到,定时器重置为 null,就可以再次点击了。
  • +
  • 对于延时执行函数来说的实现:清除定时器ID,如果是延迟调用就调用函数
  • +
+

节流

防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// js代码

/**
* underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {object} options 如果想忽略开始函数的的调用,传入{leading: false}。
* 如果想忽略结尾函数的调用,传入{trailing: false}
* 两者不能共存,否则函数不能执行
* @return {function} 返回客户调用函数
*/
_.throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
// 之前的时间戳
var previous = 0;
// 如果 options 没传则设为空对象
if (!options) options = {};
// 定时器回调函数
var later = function() {
// 如果设置了 leading,就将 previous 设为 0
// 用于下面函数的第一个 if 判断
previous = options.leading === false ? 0 : _.now();
// 置空一是为了防止内存泄漏,二是为了下面的定时器判断
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
// 获得当前时间戳
var now = _.now();
// 首次进入前者肯定为 true
// 如果需要第一次不执行函数
// 就将上次时间戳设为当前的
// 这样在接下来计算 remaining 的值时会大于0
if (!previous && options.leading === false) previous = now;
// 计算剩余时间
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果当前调用已经大于上次调用时间 + wait
// 或者用户手动调了时间
// 如果设置了 trailing,只会进入这个条件
// 如果没有设置 leading,那么第一次会进入这个条件
// 还有一点,你可能会觉得开启了定时器那么应该不会进入这个 if 条件了
// 其实还是会进入的,因为定时器的延时
// 并不是准确的时间,很可能你设置了2秒
// 但是他需要2.2秒才触发,这时候就会进入这个条件
if (remaining <= 0 || remaining > wait) {
// 如果存在定时器就清理掉否则会调用二次回调
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 判断是否设置了定时器和 trailing
// 没有的话就开启一个定时器
// 并且不能不能同时设置 leading 和 trailing
timeout = setTimeout(later, remaining);
}
return result;
};
};
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 函数防抖和节流

+

文章作者:

+

发布时间:2020年01月03日 - 01:04

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-debounce-throttle/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/03/FE-guide-inherit/index.html b/en/2020/01/03/FE-guide-inherit/index.html new file mode 100644 index 0000000000..6601791852 --- /dev/null +++ b/en/2020/01/03/FE-guide-inherit/index.html @@ -0,0 +1,597 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 继承 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 继承 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

继承

在 ES5 中,我们可以使用如下方式解决继承的问题

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码
function Super() {}
Super.prototype.getNumber = function() {
return 1
}

function Sub() {}
let s = new Sub()
Sub.prototype = Object.create(Super.prototype, {
constructor: {
value: Sub,
enumerable: false,
writable: true,
configurable: true
}
})
+

以上继承实现思路就是将子类的原型设置为父类的原型
ES6 中,我们可以通过 class 语法轻松解决这个问题

+
1
2
3
4
5
6
7
8
9
// js代码

class MyDate extends Date {
test() {
return this.getTime()
}
}
let myDate = new MyDate()
myDate.test()
+

但是 ES6 不是所有浏览器都兼容,所以我们需要使用 Babel 来编译这段代码。

+

如果你使用编译过得代码调用 myDate.test() 你会惊奇地发现出现了报错

+

因为在 JS 底层有限制,如果不是由 Date 构造出来的实例的话,是不能调用 Date 里的函数的。所以这也侧面的说明了:ES6 中的 class 继承与 ES5 中的一般继承写法是不同的

+

既然底层限制了实例必须由 Date 构造出来,那么我们可以改变下思路实现继承

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

function MyData() {

}
MyData.prototype.test = function () {
return this.getTime()
}
let d = new Date() // 父类实例
Object.setPrototypeOf(d, MyData.prototype)
Object.setPrototypeOf(MyData.prototype, Date.prototype)
+

以上继承实现思路:先创建父类实例 => 改变实例原先的 __proto__ 转而连接到子类的 prototype => 子类的 prototype__proto__ 改为父类的 prototype

+

通过以上方法实现的继承就可以完美解决 JS 底层的这个限制。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 继承

+

文章作者:

+

发布时间:2020年01月03日 - 01:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-inherit/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/03/FE-guide-store/index.html b/en/2020/01/03/FE-guide-store/index.html new file mode 100644 index 0000000000..315ddbe2ad --- /dev/null +++ b/en/2020/01/03/FE-guide-store/index.html @@ -0,0 +1,628 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 浏览器存储 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 浏览器存储 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

cookie,localStorage,sessionStorage,indexDB

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
特性cookielocalStoragesessionStorageindexDB
数据生命周期一般由服务器生成,可以设置过期时间除非被清理,否则一直存在页面关闭就清理除非被清理,否则一直存在
数据存储大小4K5M5M无限制
与服务端通信每次都会携带在 header 中,对于请求性能影响不参与不参与不参与
+

从上表可以看到, cookie 已经不建议用于存储。如果没有大量数据存储需求的话,可以使用 localStoragesessionStorage 。对于不怎么改变的数据尽量使用 localStorage 存储,否则可以用 sessionStorage 存储。

+

对于 cookie ,我们还需要注意安全性。
| 属性 | 作用 |
| ——— | ————————————————————– |
| value | 如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识 |
| http-only | 不能通过 JS 访问 Cookie,减少 XSS 攻击 |
| secure | 只能在协议为 HTTPS 的请求中携带 |
| same-site | 规定浏览器不能在跨域请求中携带 Cookie,减少 CSRF 攻击 |

+

Service Worker

+

Service workers 本质上充当 Web 应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步 API。

+
+

目前该技术通常用来做缓存文件,提高首屏速度,可以试着来实现这个功能。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// js代码

// index.js
if (navigator.serviceWorker) {
navigator.serviceWorker
.register('sw.js')
.then(function(registration) {
console.log('service worker 注册成功')
})
.catch(function(err) {
console.log('servcie worker 注册失败')
})
}
// sw.js
// 监听 `install` 事件,回调中缓存所需文件
self.addEventListener('install', e => {
e.waitUntil(
caches.open('my-cache').then(function(cache) {
return cache.addAll(['./index.html', './index.js'])
})
)
})

// 拦截所有请求事件
// 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据
self.addEventListener('fetch', e => {
e.respondWith(
caches.match(e.request).then(function(response) {
if (response) {
return response
}
console.log('fetch source')
})
)
})
+ +

打开页面,可以在开发者工具中的 Application 看到 Service Worker 已经启动了
Cache 中也可以发现我们所需的文件已被缓存

+

当我们重新刷新页面可以发现我们缓存的数据是从 Service Worker 中读取的

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 浏览器存储

+

文章作者:

+

发布时间:2020年01月03日 - 04:37

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-store/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/03/why-0.1-plus-0.2-not-equals-0.3/index.html b/en/2020/01/03/why-0.1-plus-0.2-not-equals-0.3/index.html new file mode 100644 index 0000000000..02f21489b7 --- /dev/null +++ b/en/2020/01/03/why-0.1-plus-0.2-not-equals-0.3/index.html @@ -0,0 +1,586 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 为什么 0.1 + 0.2 != 0.3 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 为什么 0.1 + 0.2 != 0.3 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Proxy

因为 JS 采用 IEEE 754 双精度版本(64位),并且只要采用 IEEE 754 的语言都有该问题。

+

我们都知道计算机表示十进制是采用二进制表示的,所以 0.1 在二进制表示为

+
1
2
3
4
// js代码

// (0011) 表示循环
0.1 = 2^-4 * 1.10011(0011)
+

那么如何得到这个二进制的呢,我们可以来演算下

+

小数算二进制和整数不同。乘法计算时,只计算小数位,整数位用作每一位的二进制,并且得到的第一位为最高位。所以我们得出 0.1 = 2^-4 * 1.10011(0011),那么 0.2 的演算也基本如上所示,只需要去掉第一步乘法,所以得出 0.2 = 2^-3 * 1.10011(0011)

+

回来继续说 IEEE 754 双精度。六十四位中符号位占一位,整数位占十一位,其余五十二位都为小数位。因为 0.10.2 都是无限循环的二进制了,所以在小数位末尾处需要判断是否进位(就和十进制的四舍五入一样)。

+

所以 2^-4 * 1.10011...001 进位后就变成了 2^-4 * 1.10011(0011 * 12次)010 。那么把这两个二进制加起来会得出 2^-2 * 1.0011(0011 * 11次)0100 , 这个值算成十进制就是 0.30000000000000004

+

下面说一下原生解决办法,如下代码所示

+
1
2
3
// js代码

parseFloat((0.1 + 0.2).toFixed(10))
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:为什么 0.1 + 0.2 != 0.3

+

文章作者:

+

发布时间:2020年01月03日 - 04:10

+

最后更新:2020年01月03日 - 04:10

+

原始链接:https://blog.lifesli.com/2020/01/03/why-0.1-plus-0.2-not-equals-0.3/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/04/Algorithm/index.html b/en/2020/01/04/Algorithm/index.html new file mode 100644 index 0000000000..07a5fefc1d --- /dev/null +++ b/en/2020/01/04/Algorithm/index.html @@ -0,0 +1,734 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + JavaScript实现经典排序算法 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ JavaScript实现经典排序算法 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

基本概念

时间复杂度

一个算法的时间复杂度反映了程序运行从开始到结束所需要的时间。把算法中基本操作重复执行的次数(频度)作为算法的时间复杂度。
没有循环语句,记作O(1),也称为常数阶。只有一重循环,则算法的基本操作的执行频度与问题规模n呈线性增大关系,记作O(n),也叫线性阶。
常见的时间复杂度有:

+
    +
  • O(1): Constant Complexity: Constant 常数复杂度
  • +
  • O(log n): Logarithmic Complexity: 对数复杂度
  • +
  • O(n): Linear Complexity: 线性时间复杂度
  • +
  • O(n^2): N square Complexity 平⽅方
  • +
  • O(n^3): N square Complexity ⽴立⽅方
  • +
  • O(2^n): Exponential Growth 指数
  • +
  • O(n!): Factorial 阶乘
  • +
+

空间复杂度

一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。

+

一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。

+
    +
  • 稳定
  • +
  • 不稳定

    算法汇总

    十大经典排序.jpg

    关于时间复杂度:

    平方阶 (O(n2)) 排序 各类简单排序:直接插入、直接选择和冒泡排序。
    线性对数阶 (O(nlog2n)) 排序 快速排序、堆排序和归并排序;
    O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数。 希尔排序
    线性阶 (O(n)) 排序 基数排序,此外还有桶、箱排序。
  • +
+

关于稳定性:

稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。

+

名词解释:

    +
  • n:数据规模
  • +
  • k:“桶”的个数
  • +
  • In-place:占用常数内存,不占用额外内存
  • +
  • Out-place:占用额外内存
  • +
  • 稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同
  • +
+

1. 冒泡排序(Bubble Sort) — 前后两两比较 — 气泡

冒泡排序可谓是最经典的排序算法了,它是基于比较的排序算法,其优点是实现简单,排序数量较小时性能较好。

+

它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

+

1. 1 算法原理

相邻的数据进行两两比较,小数放在前面,大数放在后面,如果前面的数据比后面的数据大,就交换这两个数的位置。也可以实现大数放在前面,小数放在后面,如果前面的数据比后面的小,就交换两个的位置。要实现上述规则需要用到两层for循环。

+

1. 2 算法描述

    +
  1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  2. +
  3. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  4. +
  5. 针对所有的元素重复以上的步骤,除了最后一个;
  6. +
  7. 重复步骤1~3,直到排序完成。
  8. +
+

1. 3 动图演示

ldB5VS.gif

+

1. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// js代码

function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len; i++) {
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { // 相邻元素两两对比
// 元素交换
/** 1.使用中间变量 **/
var temp = arr[j + 1];
arr[j + 1] = arr[j]
arr[j] = temp
/** 2.适用纯数字的数组排序 **/
arr[j] = arr[j] + arr[j + 1]
arr[j + 1] = arr[j] - arr[j + 1]
arr[j] -= arr[j + 1]
/** 3.使用es6解构赋值 **/
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
}
}
}
return arr;
}
+

冒泡排序算法优化

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len; i++) {
var exchange=false; // 交换标志
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { // 相邻元素两两对比
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]] // 元素交换
exchange=true; //
}
}
if(!exchange){ // 若本趟排序未发生交换,提前终止算法
break;
}
}
return arr;
}
+

2. 选择排序(Selection Sort) — 面试挑简历,在剩下的里面挑最好的

表现最稳定的排序算法之一,因为无论什么数据进去都是O(n²)的时间复杂度。。。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。

+

2. 1 算法原理

先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

+

2. 2 算法描述

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

+
    +
  1. 初始状态:无序区为R[1..n],有序区为空;
  2. +
  3. 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中
  4. +
  5. 选出关键字最小的记录R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
  6. +
  7. n-1趟结束,数组有序化了。
  8. +
+

2. 3 动图演示

ldDWW9.gif

+

2. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

function selectionSort(arr) {
var len = arr.length;
var minIndex, temp;
for (var i = 0; i < len - 1; i++) {
minIndex = i;
for (var j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) { //寻找最小的数
minIndex = j; //将最小数的索引保存
}
}
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
return arr;
}
+ +

3. 插入排序(Insertion Sort)—– 麻将/扑克

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。

+

3. 1 算法原理

它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

+

3. 2 算法描述

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

+
    +
  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. +
  3. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  4. +
  5. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  6. +
  7. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  8. +
  9. 将新元素插入到该位置后;
  10. +
  11. 重复步骤2~5。
  12. +
+

3. 3 动图演示

ldDfzR.gif

+

3. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// js代码

function insertSort(arr) {
// 从1位置开始遍历arr中每元素,同时声明空变量temp
for (let i = 1; i < arr.length; i++) {
if (arr[i] < arr[i - 1]) { // 如果当前元素<前一个元素
let temp = arr[i] // 将当前元素值临时保存在temp中
let p = i - 1 // 定义变量 p = i- 1
// 循环 条件:
// 1. p>=0且temp小于p位置的元素
while (p >= 0 && temp < arr[p]) {
// 循环体: 将P位置的值赋值给p的后一个元素
arr[p + 1] = arr[p]
p-- // p向前移动一个
}
arr[p + 1] = temp // 将temp的值赋值给p+1位置的元素
}
}
}
+ +

4. 快速排序(Selection Sort)

快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

+

4. 1 算法原理

快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。

+

4. 2 算法描述

选基准:在数据结构中选择一个元素作为基准(pivot
划分区:参照基准元素值的大小,划分无序区,所有小于基准元素的数据放入一个区间,所有大于基准元素的数据放入另一区间,分区操作结束后,基准元素所处的位置就是最终排序后它应该所处的位置
递归:对初次划分出来的两个无序区间,递归调用第 1步和第 2步的算法,直到所有无序区间都只剩下一个元素为止。

+

简单理解就是,选择一个目标值,比目标值小的放左边,比目标值大的放右边,目标值的位置已排好,将左右两侧再进行快排。

+

4. 3 动图演示

快速排序.gif

+

4. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// js代码

function quickSort(arr){
//如果arr.length<=1,则直接返回arr
if(arr.length<=1){return arr}
// arr的元素个数/2,再下去整,将值保存在pivotIndex中
var pivotIndex=Math.floor(arr.length/2);
// 将arr中pivotIndex位置的元素,保存在变量pivot中
var pivot=arr[pivotIndex];
//声明空数组left和right
var left=[];
var right=[];
for(var i=0;i<arr.length;i++){ // 遍历arr中每个元素
if(i !== pivotIndex){ // 如果i !== pivotIndex
if(arr[i]<=pivot){ // 如果当前元素值<pivot
left.push(arr[i]); // 就将当前值压入left
}else{
right.push(arr[i]); // 就将当前值压入right
}
}
}
//递归
return quickSort(left).concat(pivot, quickSort(right)); // 链接多个数组到 left 从小到大
}
+

5. 希尔排序

5. 1 算法原理

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:

+
    +
  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
  • +
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
    希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
  • +
+

5. 2 算法描述

    +
  • 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
  • +
  • 按增量序列个数 k,对序列进行 k 趟排序;
  • +
  • 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
  • +
+

5.3 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// js代码

function shellSort(arr) {
var len = arr.length,
temp,
gap = 1;
while(gap < len/3) { //动态定义间隔序列
gap =gap*3+1;
}
for (gap; gap > 0; gap = Math.floor(gap/3)) {
for (var i = gap; i < len; i++) {
temp = arr[i];
for (var j = i-gap; j >= 0 && arr[j] > temp; j-=gap) {
arr[j+gap] = arr[j];
}
arr[j+gap] = temp;
}
}
return arr;
}
+ +

6. 归并排序

6. 1 算法原理

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

+
    +
  • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
  • +
  • 自下而上的迭代;
    +

    在《数据结构与算法 JavaScript 描述》中,作者给出了自下而上的迭代方法。但是对于递归法,作者却认为:
    However, it is not possible to do so in JavaScript, as the recursion goes too deep for the language to handle.
    然而,在 JavaScript 中这种方式不太可行,因为这个算法的递归深度对它来讲太深了。

    +
    +
  • +
+

说实话,我不太理解这句话。意思是 JavaScript 编译器内存太小,递归太深容易造成内存溢出吗?还望有大神能够指教。

+

和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。

+

6. 2 算法描述

    +
  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
  2. +
  3. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
  4. +
  5. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
  6. +
  7. 重复步骤 3 直到某一指针达到序列尾;
  8. +
  9. 将另一序列剩下的所有元素直接复制到合并序列尾。
  10. +
+

6. 3 动图演示

归并排序.gif

+

6. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// js代码

function mergeSort(arr) { // 采用自上而下的递归方法
var len = arr.length;
if(len < 2) {
return arr;
}
var middle = Math.floor(len / 2),
left = arr.slice(0, middle),
right = arr.slice(middle);
return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right){
var result = [];
while (left.length && right.length) {
if (left[0] <= right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}

while (left.length)
result.push(left.shift());

while (right.length)
result.push(right.shift());

return result;
}
+ +

7. 堆排序

7. 1 算法原理

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

+
    +
  • 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
  • +
  • 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
    堆排序的平均时间复杂度为 Ο(nlogn)。
  • +
+

7. 2 算法描述

    +
  1. 将待排序序列构建成一个堆 H[0……n-1],根据(升序降序需求)选择大顶堆或小顶堆;
  2. +
  3. 把堆首(最大值)和堆尾互换;
  4. +
  5. 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
  6. +
  7. 重复步骤 2,直到堆的尺寸为 1。
  8. +
+

7. 3 动图演示

堆排序.gif

+

7. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// js代码

var len; // 因为声明的多个函数都需要数据长度,所以把len设置成为全局变量

function buildMaxHeap(arr) { // 建立大顶堆
len = arr.length;
for (var i = Math.floor(len/2); i >= 0; i--) {
heapify(arr, i);
}
}

function heapify(arr, i) { // 堆调整
var left = 2 * i + 1,
right = 2 * i + 2,
largest = i;

if (left < len && arr[left] > arr[largest]) {
largest = left;
}

if (right < len && arr[right] > arr[largest]) {
largest = right;
}

if (largest != i) {
swap(arr, i, largest);
heapify(arr, largest);
}
}

function swap(arr, i, j) {
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

function heapSort(arr) {
buildMaxHeap(arr);

for (var i = arr.length-1; i > 0; i--) {
swap(arr, 0, i);
len--;
heapify(arr, 0);
}
return arr;
}
+ +

8. 计数排序

8. 1 算法原理

计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

+

8. 2 算法描述

8. 3 动图演示

计数排序.gif

+

8. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// js代码

function countingSort(arr, maxValue) {
var bucket = new Array(maxValue+1),
sortedIndex = 0;
arrLen = arr.length,
bucketLen = maxValue + 1;

for (var i = 0; i < arrLen; i++) {
if (!bucket[arr[i]]) {
bucket[arr[i]] = 0;
}
bucket[arr[i]]++;
}

for (var j = 0; j < bucketLen; j++) {
while(bucket[j] > 0) {
arr[sortedIndex++] = j;
bucket[j]--;
}
}

return arr;
}
+ +

9. 桶排序

9. 1 算法原理

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

+
    +
  • 在额外空间充足的情况下,尽量增大桶的数量
  • +
  • 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
    同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

    9. 2 算法描述

    1. 什么时候最快

    当输入的数据可以均匀的分配到每一个桶中。

    2. 什么时候最慢

    当输入的数据被分配到了同一个桶中。

    9. 3 动图演示

  • +
+

9. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// js代码

function bucketSort(arr, bucketSize) {
if (arr.length === 0) {
return arr;
}

var i;
var minValue = arr[0];
var maxValue = arr[0];
for (i = 1; i < arr.length; i++) {
if (arr[i] < minValue) {
minValue = arr[i]; // 输入数据的最小值
} else if (arr[i] > maxValue) {
maxValue = arr[i]; // 输入数据的最大值
}
}

//桶的初始化
var DEFAULT_BUCKET_SIZE = 5; // 设置桶的默认数量为5
bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
var bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
var buckets = new Array(bucketCount);
for (i = 0; i < buckets.length; i++) {
buckets[i] = [];
}

//利用映射函数将数据分配到各个桶中
for (i = 0; i < arr.length; i++) {
buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i]);
}

arr.length = 0;
for (i = 0; i < buckets.length; i++) {
insertionSort(buckets[i]); // 对每个桶进行排序,这里使用了插入排序
for (var j = 0; j < buckets[i].length; j++) {
arr.push(buckets[i][j]);
}
}

return arr;
}
+ +

10. 基数排序

10. 1 算法原理

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

+

10. 2 算法描述

1. 基数排序 vs 计数排序 vs 桶排序

基数排序有三种方法:
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异案例看大家发的:

+
    +
  • 基数排序:根据键值的每位数字来分配桶;
  • +
  • 计数排序:每个桶只存储单一键值;
  • +
  • 桶排序:每个桶存储一定范围的数值;

    10. 3 动图演示

  • +
+
    +
  1. LSD 基数排序动图演示
    基数排序.gif

    10. 4 js代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // js代码

    // LSD Radix Sort
    var counter = [];
    function radixSort(arr, maxDigit) {
    var mod = 10;
    var dev = 1;
    for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
    for(var j = 0; j < arr.length; j++) {
    var bucket = parseInt((arr[j] % mod) / dev);
    if(counter[bucket]==null) {
    counter[bucket] = [];
    }
    counter[bucket].push(arr[j]);
    }
    var pos = 0;
    for(var j = 0; j < counter.length; j++) {
    var value = null;
    if(counter[j]!=null) {
    while ((value = counter[j].shift()) != null) {
    arr[pos++] = value;
    }
    }
    }
    }
    return arr;
    }
  2. +
+

总结

排序算法.png
以上就是十大经典算法,算法对于前端来说并不是一个十分熟悉的领域,但是排序算法算是算法里比较入门的,还是需要掌握的,毕竟即使是为了面试也是要准备的。

+

参考资料

https://github.com/hustcc/JS-Sorting-Algorithm
一本关于排序算法的 GitBook 在线书籍 《十大经典排序算法》,多语言实现。

+

http://www.sohu.com/a/136157205_671058
技术面试宝典: 很全面的算法和数据结构知识(含代码实现)下篇

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:JavaScript实现经典排序算法

+

文章作者:

+

发布时间:2020年01月04日 - 07:18

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/04/Algorithm/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/05/FE-guide-ArrayOprs/index.html b/en/2020/01/05/FE-guide-ArrayOprs/index.html new file mode 100644 index 0000000000..4e82d03da2 --- /dev/null +++ b/en/2020/01/05/FE-guide-ArrayOprs/index.html @@ -0,0 +1,617 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 数组常见操作 ---- 去重、扁平、取最大最小值 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 数组常见操作 ---- 去重、扁平、取最大最小值 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

去重

1. 利用 ObjectKey 唯一特性

开辟一个外部存储空间用于标示元素是否出现过。

+
1
2
3
4
5
6
// js代码

const unique = (array)=> {
var container = {};
return array.filter((item, index) => container.hasOwnProperty(item) ? false : (container[item] = true));
}
+ +

2. 利用 indexOf 的返回值数值进行去重

原理是 indexOf 获取元素时如果返回值不等于下标说明已经有了,配合 filter 更美味

+
1
2
3
4
5
// js代码

const unique = arr => arr.filter((e,i) =>
arr.indexOf(e) === i // 如果元素找到的当前下标和当前索引相同说明是同一个,不同说明不是唯一
);
+ +

还有一种变形方法利用 lastIndexOf 方法

+
+

lastIndexOf() 方法返回指定元素(也即有效的 JavaScript 值或变量)在数组中的最后一个的索引,如果不存在则返回 -1。从数组的后面向前查找,从 fromIndex 处开始。

+
+
1
2
3
4
5
// js代码

const filterNonUnique = arr => arr.filter(e =>
arr.indexOf(e) === arr.lastIndexOf(e) // 判断一个元素出现时的第一次下标和最后一次下标是否相同,如果相同那么就唯一
)
+

3. 利用 Set 特性去重

SetES6 中新的数据类型,它的特点就是元素唯一性,且可以和数组进行转换

+
1
2
3
4
5
6
7
// js代码

const unique = arr => Array.from(new Set(arr)); // Array.from 方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。

// 优化

const unique = arr => [...new Set(arr)]; // 利用 `ES6` 中展开操作
+

4. 排序后判断前后两项是否相等去重

通过比较相邻数字是否重复,将排序后的数组进行去重。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

const unique = (array) => {
array.sort((a, b) => a - b);
let pre = 0;
const result = [];
for (let i = 0; i < array.length; i++) {
if (!i || array[i] != array[pre]) {
result.push(array[i]);
}
pre = i;
}
return result;
}
+ +

扁平

1. 普通方法

通过递归的方式判断数组中的项是否是数组,如果不是就加入到新的扁平数组,如果是就递归调用逐层判断,直到全部结束

+
1
2
3
4
5
6
7
8
9
10
11
12
13
// js代码

const flatten = (array) => { // array 原数组
let result = []; // 定义新的扁平数组
for (let i = 0; i < array.length; i++) {
if (Array.isArray(array[i])) { // 判断子元素是否是数组
result = result.concat(flatten(array[i])); // 递归判断
} else {
result.push(array[i]); // 加入新数组
}
}
return result;
}
+ +

2. 使用reduce简化上述方法

+

reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
reducer 函数接收4个参数:

+
+
    +
  • Accumulator (acc) (累计器)
  • +
  • Current Value (cur) (当前值)
  • +
  • Current Index (idx) (当前索引)
  • +
  • Source Array (src) (源数组)
  • +
  • 您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
  • +
+

先看一段 reduce 的示例函数

+
1
2
3
4
5
6
7
8
9
10
11
12
// js代码

const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue; // 定义一个累计器函数,作用是将数组前后累计值与当前值累加

// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer)); // 没有初始值用第一个元素
// expected output: 10

// 5 + 1 + 2 + 3 + 4
console.log(array1.reduce(reducer, 5)); // 有初始值从初始值开始
// expected output: 15
+

这下大家应该对 reduce 函数认识了,接下来看看怎么简化

+
1
2
3
4
5
6
7
8
9
// js代码

function flatten(array) {
return array.reduce((newArray, current) => // 新数组,当前项
Array.isArray(current) ? // 判断当前项是否为数组
newArray.concat(flatten(current)) : // 是的话 递归调用
newArray.concat(current) // 不是的话加进新数组
, []) // 初始化新数组为空
}
+

这里我们再变一个形,增加一个变量,变成可指定深度操作数组

+
1
2
3
4
5
6
7
8
9
10
// js代码

function flattenByDeep(array, deep = 1) { // 默认一层
return array.reduce(
(target, current) =>
Array.isArray(current) && deep > 1 ?
target.concat(flattenByDeep(current, deep - 1)) : // 下一次减一层
target.concat(current)
, [])
}
+

最值

利用 reduce

reduce 函数真的是超级好用,

+
1
2
3
// js代码

array.reduce((c,n) => Math.max(c,n))
+ +

Math.max

Math.max 参数原本是一组数字,只需要让他可以接收数组即可。

+
1
2
3
4
5
// js代码

const array = [3,2,1,4,5];
Math.max.apply(null,array);
Math.max(...array);
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:数组常见操作 ---- 去重、扁平、取最大最小值

+

文章作者:

+

发布时间:2020年01月05日 - 03:05

+

最后更新:2020年01月05日 - 07:47

+

原始链接:https://blog.lifesli.com/2020/01/05/FE-guide-ArrayOprs/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/05/FE-guide-about-reduce/index.html b/en/2020/01/05/FE-guide-about-reduce/index.html new file mode 100644 index 0000000000..73166889a1 --- /dev/null +++ b/en/2020/01/05/FE-guide-about-reduce/index.html @@ -0,0 +1,604 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + reduce函数的妙用 ---- 实现map和filter | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ reduce函数的妙用 ---- 实现map和filter +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近看了好多技术文章,好多的代码片段,突然发现好多的代码里都有 reduce ,感觉以前都没怎么关注过这个函数,为了弥补这个过失,打算专门写一篇文章说说它。

+

reduce 函数在 MDN 中是这样介绍的

+
+

reduce() 方法对数组中的每个元素执行一个由 提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。

+
+

说实话看了一脸懵逼,这上面说的叫人话?后来耐着性子看了一些代码后感觉有所理解,如果理解不对,还请斧正。
首先看一下这里面几个关键词

+

* 每个元素: * 这就是遍历咯,没啥好说的
提供的 reducer 函数:我哪有什么 reducer 函数呢,后来理解了,就是回调,有的地方呢叫 handler ,这里跟 reduce 配合就叫了 reducer
升序执行:就是说是0,1,2下标这样的顺序执行啦。
将其结果汇总为单个返回值:最后返回的是一个值,当然没说必须是什么类型。

+

这样解释完了以后,上面这句话就比较好懂了,简单说就是 reduce 里传一个回调函数,执行回调函数方法后返回一个值,就是这样。

+
+

reducer 函数接收4个参数:

+
+
    +
  • Accumulator (acc) (累计器)
  • +
  • Current Value (cur) (当前值)
  • +
  • Current Index (idx) (当前索引)
  • +
  • Source Array (src) (源数组)
    您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
  • +
+

看上面的描述一定要注意,这些参数是 reducer 的参数,不是 reduce 的参数,一共有4个,通常用前两个情况比较多。

+

那么 reduce 函数呢实际上有两个参数,第二个还可以省略 reducerinitialValueinitialValue作为第一次调用 reducer 函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错,这点切记。

+

终于把前置知识讲清楚了,接下来,我们就用 reduce 搞点事情。

+

1. 使用 reduce 实现 map

map 的话我不想再讲一遍,一句话带过,就是对回调函数中的元素进行加工后返回一个长度一样的新数组。
那么该如何实现呢,我们来想想原理吧,刚才上面说过了,reduce执行过后的返回值会作为下一次执行的第一个参数放进去,那么就可以先用一个空数组作为初始值来接受每次需要处理的元素的集合,并返回回去,作为下一次传入的第一个参数,这样每次操作完之后就可以push到这个数组中,那么剩下要做的就是处理数组中每一项的函数了,我们叫他 handler

+
1
2
3
4
5
6
7
8
// js代码

Array.prototype.reduceToMap = function (handler) { // 自定义 `map` 函数 `reduceToMap`
return this.reduce((target, current, index) => { // this指向调用他的数组
target.push(handler.call(this, current, index)) // 这里用了call方法,handler将接受两个参数 current和index
return target; // 处理完成后返回新数组
}, []) // 初始化空的新数组
};
+

就这样我们就用 reduce,实现了 map 的功能,是不是很好用?

+

2. 使用 reduce 实现 filter

filter 也是数组常用的方法,同样传入一个回调函数,处理结果返回true或false,最终 filter 会返回一个过滤后的函数。
学会了上面的 map 的实现,实际上 filter 就会很简单

+
1
2
3
4
5
6
7
8
9
10
// js代码

Array.prototype.reduceToFilter = function (handler) { // 还是自定义方法名
return this.reduce((target, current, index) => {
if (handler.call(this, current, index)) { // 这里注意 handler 要返回的是布尔类型的值
target.push(current); // 符合条件就插入新数组
} // 不符合就什么都不做
return target; // 最后返回新数组
}, []) // 初始化一个空数组
};
+

日后在看到 reduce 的妙用之后还会来补充这篇文章,如果实在懒得写,我也会链接一下新文章,希望大家长期关注。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:reduce函数的妙用 ---- 实现map和filter

+

文章作者:

+

发布时间:2020年01月05日 - 08:55

+

最后更新:2020年01月05日 - 08:53

+

原始链接:https://blog.lifesli.com/2020/01/05/FE-guide-about-reduce/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/05/FE-guide-currying/index.html b/en/2020/01/05/FE-guide-currying/index.html new file mode 100644 index 0000000000..a3cc3f203a --- /dev/null +++ b/en/2020/01/05/FE-guide-currying/index.html @@ -0,0 +1,595 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 柯里化 currying | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 柯里化 currying +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

定义

柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

+

柯里化,可以理解为提前接收部分参数,延迟执行,不立即输出结果,而是返回一个接受剩余参数的函数。因为这样的特性,也被称为部分计算函数。

+

通俗易懂的解释:用闭包把参数保存起来,当参数的数量足够执行函数了,就开始执行函数。柯里化,是一个逐步接收参数的过程。在接下来的剖析中,你会深刻体会到这一点。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码

function currying(fn){
var allArgs = [];

return function next(){
var args = [].slice.call(arguments); // 拆成数组元素

if(args.length > 0){
allArgs = allArgs.concat(args);
return next;
}else{
return fn.apply(null, allArgs);
}
}
}
+

我们来一个简单的实例验证一下:

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

var add = currying(function(){
var sum = 0;
for(var i = 0; i < arguments.length; i++){
sum += arguments[i];
}
return sum;
});

add(1)(2, 3)(4)() // => 10
+ +

应用场景

参数复用

1
2
3
4
5
6
7
8
// js代码

function getUrl(domain, protocol, path) {
return protocol + "://" + domain + "/" + path;
}

var page1 = getUrl('http', 'lixuguang.github.io', 'page1.html');
var page2 = getUrl('http', 'lixuguang.github.io', 'page2.html');
+

我们使用currying来简化它:

+
1
2
var conardliSite = currying(getUrl)
var page1 = conardliSite('page1.html')('http', 'lixuguang.github.io')();
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 柯里化 currying

+

文章作者:

+

发布时间:2020年01月05日 - 09:59

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/05/FE-guide-currying/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/05/FE-guide-vuepress/index.html b/en/2020/01/05/FE-guide-vuepress/index.html new file mode 100644 index 0000000000..cae83b8772 --- /dev/null +++ b/en/2020/01/05/FE-guide-vuepress/index.html @@ -0,0 +1,589 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 介绍一个好用的doc展示库 ---- vuepress | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 介绍一个好用的doc展示库 ---- vuepress +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在做公司技术规范,需要能通过 B/S 方式进行分发,起初想法是做一个网站,用 html 的方式进行书写,但是后面感觉太麻烦了,所以就放弃了,偶然间看到有工具可以直接通过 md 文件编译生成网站,就像是 hexo 博客这样,我觉得这挺好,极大的节省了开发网站所需要的时间,只需要专注于内容就好了,所以我就研究了下来,整个了解过程中有这么两个工具映入了我的眼帘,一个是 doctify,另外一个就是今天的主角 vuepress

+ + +

vuepress 何许

+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:介绍一个好用的doc展示库 ---- vuepress

+

文章作者:

+

发布时间:2020年01月05日 - 17:59

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/05/FE-guide-vuepress/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/09/FE-guide-Net/index.html b/en/2020/01/09/FE-guide-Net/index.html new file mode 100644 index 0000000000..d9a79f4969 --- /dev/null +++ b/en/2020/01/09/FE-guide-Net/index.html @@ -0,0 +1,970 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 网络 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 网络 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

网络相关的知识虽然平时在编码阶段很少遇到,但是并不是说它不重要,尤其是前端优化层面,能从网络传输过程优化的点有很多,所以,我们需要好好掌握网络相关的知识,接下来,我将会将我了解到的网络相关的知识点进行一一整理

+

UDP - User Datagram Protocol - 用户数据报协议

面向报文

UDP 是一个面向报文(报文可以理解为一段段的数据)的协议。意思就是 UDP 只是报文的搬运工,不会对报文进行任何拆分和拼接操作。

+

具体来说

+
    +
  • 发送端,应用层将数据传递给传输层的 UDP 协议, UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了
  • +
  • 接收端,网络层将数据传递给传输层, UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作
  • +
+

不可靠性

    +
  1. UDP 是无连接的,也就是说通信不需要建立和断开连接。
  2. +
  3. UDP 也是不可靠的。协议收到什么数据就传递什么数据,并且也不会备份数据,对方能不能收到是不关心的
  4. +
  5. UDP 没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP
  6. +
+

高效

因为 UDP 没有 TCP 那么复杂,需要保证数据不丢失且有序到达。所以 UDP 的头部开销小,只有八字节,相比 TCP 的至少二十字节要少得多,在传输数据报文时是很高效的。

+

头部包含了以下几个数据

+
    +
  • 两个十六位的端口号,分别为源端口(可选字段)和目标端口
  • +
  • 整个数据报文的长度
  • +
  • 整个数据报文的检验和( IPv4 可选 字段),该字段用于发现头部信息和数据中的错误
  • +
+

传输方式

UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。

+

TCP

头部

TCP 头部比 UDP 头部复杂的多

+

对于 TCP 头部来说,以下几个字段是很重要的

+
    +
  • Sequence number(序号),它保证了 TCP 传输的报文都是有序的,对端可以通过序号顺序的拼接报文
  • +
  • Acknowledgement Number(确认编号),它表示数据接收端期望接收的下一个字节的编号是多少,同时也表示上一个序号的数据已经收到
  • +
  • Window Size(窗口大小),它表示还能接收多少字节的数据,用于流量控制
  • +
  • 标识符
      +
    • URG=1:该字段为一表示本数据报的数据部分包含紧急信息,是一个高优先级数据报文,此时紧急指针有效。紧急数据一定位于当前数据包数据部分的最前面,紧急指针标明了紧急数据的尾部。
    • +
    • ACK=1:该字段为一表示确认号字段有效。此外,TCP 还规定在连接建立后传送的所有报文段都必须把 ACK 置为一。
    • +
    • PSH=1:该字段为一表示接收端应该立即将数据 push 给应用层,而不是等到缓冲区满后再提交。
    • +
    • RST=1:该字段为一表示当前 TCP 连接出现严重问题,可能需要重新建立 TCP 连接,也可以用于拒绝非法的报文段和拒绝连接请求。
    • +
    • SYN=1:当SYN=1,ACK=0时,表示当前报文段是一个连接请求报文。当SYN=1,ACK=1时,表示当前报文段是一个同意建立连接的应答报文。
    • +
    • FIN=1:该字段为一表示此报文段是一个释放连接的请求报文。
    • +
    +
  • +
+

状态机

HTTP 是无连接的,所以作为下层的 TCP 协议也是无连接的,虽然看似 TCP 将两端连接了起来,但是其实只是两端共同维护了一个状态

+

TCP 的状态机是很复杂的,并且与建立断开连接时的握手息息相关,接下来就来详细描述下两种握手。
在这之前需要了解一个重要的性能指标 RTT 。该指标表示发送端发送数据到接收到对端数据所需的往返时间。

+

建立连接三次握手

在 TCP 协议中,主动发起请求的一端为客户端,被动连接的一端称为服务端。不管是客户端还是服务端,TCP 连接建立完后都能发送和接收数据,所以 TCP 也是一个全双工的协议。

+

起初,两端都为 CLOSED 状态。在通信开始前,双方都会创建 TCB。 服务器创建完 TCB 后遍进入 LISTEN 状态,此时开始等待客户端发送数据。

+

第一次握手

客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态,x 表示客户端的数据通信初始序号。

+

第二次握手

服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。

+

第三次握手

当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。

+

PS:第三次握手可以包含数据,通过 TCP 快速打开(TFO)技术。其实只要涉及到握手的协议,都可以使用类似 TFO 的方式,客户端和服务端存储相同 cookie,下次握手时发出 cookie 达到减少 RTT 的目的。

+

你是否有疑惑明明两次握手就可以建立起连接,为什么还需要第三次应答?

+

因为这是为了防止失效的连接请求报文段被服务端接收,从而产生错误。

+

可以想象如下场景。客户端发送了一个连接请求 A,但是因为网络原因造成了超时,这时 TCP 会启动超时重传的机制再次发送一个连接请求 B。此时请求顺利到达服务端,服务端应答完就建立了请求。如果连接请求 A 在两端关闭后终于抵达了服务端,那么这时服务端会认为客户端又需要建立 TCP 连接,从而应答了该请求并进入 ESTABLISHED 状态。此时客户端其实是 CLOSED 状态,那么就会导致服务端一直等待,造成资源的浪费。

+

PS:在建立连接中,任意一端掉线,TCP 都会重发 SYN 包,一般会重试五次,在建立连接中可能会遇到 SYN FLOOD 攻击。遇到这种情况你可以选择调低重试次数或者干脆在不能处理的情况下拒绝请求。

+

断开链接四次握手

TCP 是全双工的,在断开连接时两端都需要发送 FIN 和 ACK。

+

第一次握手

若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。

+

第二次握手

B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,表示 A 到 B 的连接已经释放,不接收 A 发的数据了。但是因为 TCP 连接时双向的,所以 B 仍旧可以发送数据给 A。

+

第三次握手

B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。

+

PS:通过延迟确认的技术(通常有时间限制,否则对方会误认为需要重传),可以将第二次和第三次握手合并,延迟 ACK 包的发送。

+

第四次握手

A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。

+

为什么 A 要进入 TIME-WAIT 状态,等待 2MSL 时间后才进入 CLOSED 状态?

+

为了保证 B 能收到 A 的确认应答。若 A 发完确认应答后直接进入 CLOSED 状态,如果确认应答因为网络问题一直没有到达,那么会造成 B 不能正常关闭。

+

ARQ 协议

ARQ 协议也就是超时重传机制。通过确认和超时机制保证了数据的正确送达,ARQ 协议包含停止等待 ARQ 和连续 ARQ

+

停止等待 ARQ

正常传输过程

只要 A 向 B 发送一段报文,都要停止发送并启动一个定时器,等待对端回应,在定时器时间内接收到对端应答就取消定时器并发送下一段报文。

+

报文丢失或出错

在报文传输的过程中可能会出现丢包。这时候超过定时器设定的时间就会再次发送丢包的数据直到对端响应,所以需要每次都备份发送的数据。

+

即使报文正常的传输到对端,也可能出现在传输过程中报文出错的问题。这时候对端会抛弃该报文并等待 A 端重传。

+

PS:一般定时器设定的时间都会大于一个 RTT 的平均时间。

+

ACK 超时或丢失

对端传输的应答也可能出现丢失或超时的情况。那么超过定时器时间 A 端照样会重传报文。这时候 B 端收到相同序号的报文会丢弃该报文并重传应答,直到 A 端发送下一个序号的报文。

+

在超时的情况下也可能出现应答很迟到达,这时 A 端会判断该序号是否已经接收过,如果接收过只需要丢弃应答即可。

+

这个协议的缺点就是传输效率低,在良好的网络环境下每次发送报文都得等待对端的 ACK 。

+

连续 ARQ

在连续 ARQ 中,发送端拥有一个发送窗口,可以在没有收到应答的情况下持续发送窗口内的数据,这样相比停止等待 ARQ 协议来说减少了等待时间,提高了效率。

+

累计确认

连续 ARQ 中,接收端会持续不断收到报文。如果和停止等待 ARQ 中接收一个报文就发送一个应答一样,就太浪费资源了。通过累计确认,可以在收到多个报文以后统一回复一个应答报文。报文中的 ACK 可以用来告诉发送端这个序号之前的数据已经全部接收到了,下次请发送这个序号 + 1的数据。

+

但是累计确认也有一个弊端。在连续接收报文时,可能会遇到接收到序号 5 的报文后,并未接到序号 6 的报文,然而序号 7 以后的报文已经接收。遇到这种情况时,ACK 只能回复 6,这样会造成发送端重复发送数据,这种情况下可以通过 Sack 来解决,这个会在下文说到。

+

滑动窗口

在上面小节中讲到了发送窗口。在 TCP 中,两端都维护着窗口:分别为发送端窗口和接收端窗口。

+

发送端窗口包含已发送但未收到应答的数据和可以发送但是未发送的数据。

+

发送端窗口是由接收窗口剩余大小决定的。接收方会把当前接收窗口的剩余大小写入应答报文,发送端收到应答后根据该值和当前网络拥塞情况设置发送窗口的大小,所以发送窗口的大小是不断变化的。

+

当发送端接收到应答报文后,会随之将窗口进行滑动

+

滑动窗口实现了流量控制。接收方通过报文告知发送方还可以发送多少数据,从而保证接收方能够来得及接收数据。

+

Zero 窗口

在发送报文的过程中,可能会遇到对端出现零窗口的情况。在该情况下,发送端会停止发送数据,并启动 persistent timer 。该定时器会定时发送请求给对端,让对端告知窗口大小。在重试次数超过一定次数后,可能会中断 TCP 链接。

+

拥塞处理

拥塞处理和流量控制不同,后者是作用于接收方,保证接收方来得及接受数据。而前者是作用于网络,防止过多的数据拥塞网络,避免出现网络负载过大的情况。

+

拥塞处理包括了四个算法,分别为:慢开始拥塞避免快速重传快速恢复

+

慢开始算法

慢开始算法,顾名思义,就是在传输开始时将发送窗口慢慢指数级扩大,从而避免一开始就传输大量数据导致网络拥塞。

+

慢开始算法步骤具体如下

+
    +
  1. 连接初始设置拥塞窗口(Congestion Window) 为 1 MSS(一个分段的最大数据量)
  2. +
  3. 每过一个 RTT 就将窗口大小乘二
  4. +
  5. 指数级增长肯定不能没有限制的,所以有一个阈值限制,当窗口大小大于阈值时就会启动拥塞避免算法。
  6. +
+

拥塞避免算法

拥塞避免算法相比简单点,每过一个 RTT 窗口大小只加一,这样能够避免指数级增长导致网络拥塞,慢慢将大小调整到最佳值。

+

在传输过程中可能定时器超时的情况,这时候 TCP 会认为网络拥塞了,会马上进行以下步骤:

+
    +
  • 将阈值设为当前拥塞窗口的一半
  • +
  • 将拥塞窗口设为 1 MSS
  • +
  • 启动拥塞避免算法
  • +
+

快速重传

快速重传一般和快恢复一起出现。一旦接收端收到的报文出现失序的情况,接收端只会回复最后一个顺序正确的报文序号(没有 Sack 的情况下)。如果收到三个重复的 ACK,无需等待定时器超时再重发而是启动快速重传。具体算法分为两种:

+

TCP Taho 实现如下

    +
  • 将阈值设为当前拥塞窗口的一半
  • +
  • 将拥塞窗口设为 1 MSS
  • +
  • 重新开始慢开始算法
  • +
+

TCP Reno 实现如下

    +
  • 拥塞窗口减半
  • +
  • 将阈值设为当前拥塞窗口
  • +
  • 进入快恢复阶段(重发对端需要的包,一旦收到一个新的 ACK 答复就退出该阶段)
  • +
  • 使用拥塞避免算法
  • +
+

TCP New Ren 改进后的快恢复

TCP New Reno 算法改进了之前 TCP Reno 算法的缺陷。在之前,快恢复中只要收到一个新的 ACK 包,就会退出快恢复。

+

在 TCP New Reno 中,TCP 发送方先记下三个重复 ACK 的分段的最大序号。

+

假如我有一个分段数据是 1 ~ 10 这十个序号的报文,其中丢失了序号为 3 和 7 的报文,那么该分段的最大序号就是 10。发送端只会收到 ACK 序号为 3 的应答。这时候重发序号为 3 的报文,接收方顺利接收并会发送 ACK 序号为 7 的应答。这时候 TCP 知道对端是有多个包未收到,会继续发送序号为 7 的报文,接收方顺利接收并会发送 ACK 序号为 11 的应答,这时发送端认为这个分段接收端已经顺利接收,接下来会退出快恢复阶段。

+

HTTP

HTTP 协议是个无状态协议,不会保存状态。

+

PostGet 的区别

先引入副作用幂等的概念。

+
+

副作用指对服务器上的资源做改变,搜索是无副作用的,注册是副作用的。

+
+
+

幂等指发送 M 和 N 次请求(两者不相同且都大于 1),服务器上资源的状态一致,比如注册 10 个和 11 个帐号是不幂等的,对文章进行更改 10 次和 11 次是幂等的。

+
+

在规范的应用场景上说, Get 多用于无副作用,幂等的场景,例如搜索关键字。 Post 多用于副作用,不幂等的场景,例如注册。

+

在技术上说:

+
    +
  • Get 请求能缓存,Post 不能
  • +
  • Post 相对 Get 安全一点点,因为Get 请求都包含在 URL 里,且会被浏览器保存历史纪录,Post 不会,但是在抓包的情况下都是一样的。
  • +
  • Post 可以通过 request body来传输比 Get 更多的数据,Get 没有这个技术
  • +
  • URL有长度限制,会影响 Get 请求,但是这个长度限制是浏览器规定的,不是 RFC 规定的
  • +
  • Post 支持更多的编码类型且不对数据类型限制
  • +
+

常见状态码

2XX 成功

200 OK,表示从客户端发来的请求在服务器端被正确处理
204 No content,表示请求成功,但响应报文不含实体的主体部分
205 Reset Content,表示请求成功,但响应报文不含实体的主体部分,但是与 204 响应不同在于要求请求方重置内容
206 Partial Content,进行范围请求

+

3XX 重定向

301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
302 found,临时性重定向,表示资源临时被分配了新的 URL
303 see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源
304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况
307 temporary redirect,临时重定向,和302含义类似,但是期望客户端保持请求方法不变向新的地址发出请求

+

4XX 客户端错误

400 bad request,请求报文存在语法错误
401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
403 forbidden,表示对请求资源的访问被服务器拒绝
404 not found,表示在服务器上没有找到请求的资源

+

5XX 服务器错误

500 internal sever error,表示服务器端在执行请求时发生了错误
501 Not Implemented,表示服务器不支持当前请求所需要的某个功能
503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求

+

HTTP 首部

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
通用字段作用
Cache-Control控制缓存的行为
Connection浏览器想要优先使用的连接类型,比如 keep-alive
Date创建报文时间
Pragma报文指令
Via代理服务器相关信息
Transfer-Encoding传输编码方式
Upgrade要求客户端升级协议
Warning在内容中可能存在错误
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
请求字段作用
Accept能正确接收的媒体类型
Accept-Charset能正确接收的字符集
Accept-Encoding能正确接收的编码格式列表
Accept-Language能正确接收的语言列表
Expect期待服务端的指定行为
From请求方邮箱地址
Host服务器的域名
If-Match两端资源标记比较
If-Modified-Since本地资源未修改返回 304(比较时间)
If-None-Match本地资源未修改返回 304(比较标记)
User-Agent客户端信息
Max-Forwards限制可被代理及网关转发的次数
Proxy-Authorization向代理服务器发送验证信息
Range请求某个内容的一部分
Referer表示浏览器所访问的前一个页面
TE传输编码方式
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
响应字段作用
Accept-Ranges是否支持某些种类的范围
Age资源在代理缓存中存在的时间
ETag资源标识
Location客户端重定向到某个 URL
Proxy-Authenticate向代理服务器发送验证信息
Server服务器名字
WWW-Authenticate获取资源需要的验证信息
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
实体字段作用
Allow资源的正确请求方式
Content-Encoding内容的编码格式
Content-Language内容使用的语言
Content-Lengthrequest body 长度
Content-Location返回数据的备用地址
Content-MD5Base64加密格式的内容 MD5检验值
Content-Range内容的位置范围
Content-Type内容的媒体类型
Expires内容的过期时间
Last_modified内容的最后修改时间
+

HTTPS

HTTPS 还是通过了 HTTP 来传输信息,但是信息通过 TLS 协议进行了加密。

+

TLS

TLS 协议位于传输层之上,应用层之下。首次进行 TLS 协议传输需要两个 RTT ,接下来可以通过 Session Resumption 减少到一个 RTT

+

TLS 中使用了两种加密技术,分别为:对称加密非对称加密

+

对称加密:
对称加密就是两边拥有相同的秘钥,两边都知道如何将密文加密解密。

+

非对称加密:
有公钥私钥之分,公钥所有人都可以知道,可以将数据用公钥加密,但是将数据解密必须使用私钥解密,私钥只有分发公钥的一方才知道。

+

TLS 握手过程如下图:

+
    +
  1. 客户端发送一个随机值,需要的协议和加密方式
  2. +
  3. 服务端收到客户端的随机值,自己也产生一个随机值,并根据客户端需求的协议和加密方式来使用对应的方式,发送自己的证书(如果需要验证客户端证书需要说明)
  4. +
  5. 客户端收到服务端的证书并验证是否有效,验证通过会再生成一个随机值,通过服务端证书的公钥去加密这个随机值并发送给服务端,如果服务端需要验证客户端证书的话会附带证书
  6. +
  7. 服务端收到加密过的随机值并使用私钥解密获得第三个随机值,这时候两端都拥有了三个随机值,可以通过这三个随机值按照之前约定的加密方式生成密钥,接下来的通信就可以通过该密钥来加密解密
  8. +
+

通过以上步骤可知,在 TLS 握手阶段,两端使用非对称加密的方式来通信,但是因为非对称加密损耗的性能比对称加密大,所以在正式传输数据时,两端使用对称加密的方式通信。

+

PS:以上说明的都是 TLS 1.2 协议的握手情况,在 1.3 协议中,首次建立连接只需要一个 RTT,后面恢复连接不需要 RTT 了。

+

HTTP 2.0

HTTP 2.0 相比于 HTTP 1.X,可以说是大幅度提高了 web 的性能。

+

在 HTTP 1.X 中,为了性能考虑,我们会引入雪碧图、将小图内联、使用多个域名等等的方式。这一切都是因为浏览器限制了同一个域名下的请求数量,当页面中需要请求很多资源的时候,队头阻塞(Head of line blocking)会导致在达到最大请求数量时,剩余的资源需要等待其他资源请求完成后才能发起请求。

+

你可以通过 该链接 感受下 HTTP 2.0 比 HTTP 1.X 到底快了多少。

+

在 HTTP 1.X 中,因为队头阻塞的原因,你会发现请求是这样的
lWJGkt.png
在 HTTP 2.0 中,因为引入了多路复用,你会发现请求是这样的
lWJa6g.png

+

二进制传输

HTTP 2.0 中所有加强性能的核心点在于此。在之前的 HTTP 版本中,我们是通过文本的方式传输数据。在 HTTP 2.0 中引入了新的编码机制,所有传输的数据都会被分割,并采用二进制格式编码。

+

多路复用

在 HTTP 2.0 中,有两个非常重要的概念,分别是帧(frame)流(stream)
帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流。

+

多路复用,就是在一个 TCP 连接中可以存在多条流。换句话说,也就是可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能。

+

Header 压缩

在 HTTP 1.X 中,我们使用文本的形式传输 header,在 header 携带 cookie 的情况下,可能每次都需要重复传输几百到几千的字节。

+

在 HTTP 2.0 中,使用了 HPACK 压缩格式对传输的 header 进行编码,减少了 header 的大小。并在两端维护了索引表,用于记录出现过的 header ,后面在传输过程中就可以传输已经记录过的 header 的键名,对端收到数据后就可以通过键名找到对应的值。

+

服务端 Push

在 HTTP 2.0 中,服务端可以在客户端某个请求后,主动推送其他资源。

+

可以想象以下情况,某些资源客户端是一定会请求的,这时就可以采取服务端 push 的技术,提前给客户端推送必要的资源,这样就可以相对减少一点延迟时间。当然在浏览器兼容的情况下你也可以使用 prefetch 。

+

QUIC

这是一个谷歌出品的基于 UDP 实现的同为传输层的协议,目标很远大,希望替代 TCP 协议。

+
    +
  • 该协议支持多路复用,虽然 HTTP 2.0 也支持多路复用,但是下层仍是 TCP,因为 TCP 的重传机制,只要一个包丢失就得判断丢失包并且重传,导致发生队头阻塞的问题,但是 UDP 没有这个机制
  • +
  • 实现了自己的加密协议,通过类似 TCP 的 TFO 机制可以实现 0-RTT,当然 TLS 1.3 已经实现了 0-RTT 了
  • +
  • 支持重传和纠错机制(向前恢复),在只丢失一个包的情况下不需要重传,使用纠错机制恢复丢失的包
      +
    • 纠错机制:通过异或的方式,算出发出去的数据的异或值并单独发出一个包,服务端在发现有一个包丢失的情况下,通过其他数据包和异或值包算出丢失包
    • +
    • 在丢失两个包或以上的情况就使用重传机制,因为算不出来了
    • +
    +
  • +
+

DNS

DNS 的作用就是通过域名查询到具体的 IP。

+

因为 IP 存在数字和英文的组合(IPv6),很不利于人类记忆,所以就出现了域名。你可以把域名看成是某个 IP 的别名,DNS 就是去查询这个别名的真正名称是什么。

+

在 TCP 握手之前就已经进行了 DNS 查询,这个查询是操作系统自己做的。当你在浏览器中想访问 www.google.com 时,会进行一下操作:

+
    +
  1. 操作系统会首先在本地缓存中查询
  2. +
  3. 没有的话会去系统配置的 DNS 服务器中查询
  4. +
  5. 如果这时候还没得话,会直接去 DNS 根服务器查询,这一步查询会找出负责 com 这个一级域名的服务器
  6. +
  7. 然后去该服务器查询 google 这个二级域名
  8. +
  9. 接下来三级域名的查询其实是我们配置的,你可以给 www 这个域名配置一个 IP,然后还可以给别的三级域名配置一个 IP
  10. +
+

以上介绍的是 DNS 迭代查询,还有种是递归查询,区别就是前者是由客户端去做请求,后者是由系统配置的 DNS 服务器做请求,得到结果后将数据返回给客户端。

+

PS:DNS 是基于 UDP 做的查询。

+

从输入 URL 到页面加载完成的过程

这是一个很经典的面试题,在这题中可以将本文讲得内容都串联起来。

+
    +
  1. 首先做 DNS 查询,如果这一步做了智能 DNS 解析的话,会提供访问速度最快的 IP 地址回来
  2. +
  3. 接下来是 TCP 握手,应用层会下发数据给传输层,这里 TCP 协议会指明两端的端口号,然后下发给网络层。网络层中的 IP 协议会确定 IP 地址,并且指示了数据传输中如何跳转路由器。然后包会再被封装到数据链路层的数据帧结构中,最后就是物理层面的传输了
  4. +
  5. TCP 握手结束后会进行 TLS 握手,然后就开始正式的传输数据
  6. +
  7. 数据在进入服务端之前,可能还会先经过负责负载均衡的服务器,它的作用就是将请求合理的分发到多台服务器上,这时假设服务端会响应一个 HTML 文件
  8. +
  9. 首先浏览器会判断状态码是什么,如果是 200 那就继续解析,如果 400500 的话就会报错,如果 300 的话会进行重定向,这里会有个重定向计数器,避免过多次的重定向,超过次数也会报错
  10. +
  11. 浏览器开始解析文件,如果是 gzip 格式的话会先解压一下,然后通过文件的编码格式知道该如何去解码文件
  12. +
  13. 文件解码成功后会正式开始渲染流程,先会根据 HTML 构建 DOM 树,有 CSS 的话会去构建 CSSOM 树。如果遇到 script 标签的话,会判断是否存在 async 或者 defer ,前者会并行进行下载并执行 JS ,后者会先下载文件,然后等待 HTML 解析完成后顺序执行,如果以上都没有,就会阻塞住渲染流程直到 JS 执行完毕。遇到文件下载的会去下载文件,这里如果使用 HTTP 2.0 协议的话会极大的提高多图的下载效率。
  14. +
  15. 初始的 HTML 被完全加载和解析后会触发 DOMContentLoaded 事件
  16. +
  17. CSSOM 树和 DOM 树构建完成后会开始生成 Render 树,这一步就是确定页面元素的布局、样式等等诸多方面的东西
  18. +
  19. 在生成 Render 树的过程中,浏览器就开始调用 GPU 绘制,合成图层,将内容显示在屏幕上了
  20. +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 网络

+

文章作者:

+

发布时间:2020年01月09日 - 07:35

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/09/FE-guide-Net/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/09/FE-guide-data-structure/index.html b/en/2020/01/09/FE-guide-data-structure/index.html new file mode 100644 index 0000000000..bf347b2cb3 --- /dev/null +++ b/en/2020/01/09/FE-guide-data-structure/index.html @@ -0,0 +1,706 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 计算机通识 ---- 数据结构 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 计算机通识 ---- 数据结构 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

网络相关的知识虽然平时在编码阶段很少遇到,但是并不是说它不重要,尤其是前端优化层面,能从网络传输过程优化的点有很多,所以,我们需要好好掌握网络相关的知识,接下来,我将会将我了解到的网络相关的知识点进行一一整理

+

栈 Heap

+

栈是一个线性结构,在计算机中是一个相当常见的数据结构。
栈的特点是只能在某一端添加或删除数据,遵循先进后出(FILO)的原则

+
+

实现

每种数据结构都可以用很多种方式来实现,其实可以把栈看成是数组的一个子集,所以这里使用数组来实现

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// js代码
class Stack {
constructor() {
this.stack = []
}
push(item) {
this.stack.push(item)
}
pop() {
this.stack.pop()
}
peek() { // 取最后一项
return this.stack[this.getCount() - 1]
}
getCount() {
return this.stack.length
}
isEmpty() {
return this.getCount() === 0
}
}
+

应用

选取了 LeetCode 上序号为 20 的题目

+

题意是匹配括号,可以通过栈的特性来完成这道题目

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var isValid = function(str) {
let map = {
'(': -1,
')': 1,
'[': -2,
']': 2,
'{': -3,
'}': 3
}
let stack = [] // 空数组
for (let i = 0; i < str.length; i++) { // 遍历字符串每个字符
if (map[str[i]] < 0) { // 如果是左边括号,入栈
stack.push(str[i])
} else { // 否则出栈,判断左右括号加到一起是不是0
let last = stack.pop()
if (map[last] + map[str[i]] != 0) return false
}
}
if (stack.length > 0) return false // 循环完成后,判断数组中时候还有剩下的,有剩下的说明括号没闭合
return true // 否则没剩下的,都闭合了
}
+ +

队列

+

队列一个线性结构,特点是在某一端添加数据,在另一端删除数据,遵循先进先出(FIFO)的原则。

+
+

实现

这里会讲解两种实现队列的方式,分别是单链队列循环队列

+
    +
  • 链队列即队列的链式存储结构,结构上就是一个单链表,但数据只能是头进尾出。链式结构更加的灵活,特别是在存储空间上,基本不会出现溢出的情况,所以不用像循环队列一样判断队列是否已满,且空间的利用率相对较高。
      +
    • 链队列front指向头结点,头结点不存储数据,rear指向队尾结点。
    • +
    +
  • +
+
    +
  • 循环队列即为头尾相接的队列,它的最大存储空间和顺序队列一样由数组界定,但队列的长度并不一定等同于数组的长度;循环队列的队首和队尾分别由两个指针front、rear标识,于是这样就能做到首尾相接。

    +
  • +
  • 链队列:为操作方便,给链队列添加一个头结点

    +
  • +
  • 循环队列:附设两个指针front和rear分别指示队列头元素及尾元素的位置,每当插入新的队尾元素是,尾指针加1;每当删除队列头元素是,头指针加1

    +
      +
    • 如果用循环队列,则必须设定一个最大队列长度;若无法确定最大长度,则宜采用链队列。
    • +
    +
  • +
+

单链队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// js代码

class Queue {
constructor() {
this.queue = []
}
enQueue(item) {
this.queue.push(item)
}
deQueue() {
return this.queue.shift()
}
getHeader() {
return this.queue[0]
}
getLength() {
return this.queue.length
}
isEmpty() {
return this.getLength() === 0
}
}
+

因为单链队列在出队操作的时候需要 O(n) 的时间复杂度,所以引入了循环队列。
循环队列的出队操作平均是 O(1) 的时间复杂度。

+

循环队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// js代码

class SqQueue {
constructor(length) {
this.queue = new Array(length + 1)
// 队头
this.first = 0
// 队尾
this.last = 0
// 当前队列大小
this.size = 0
}
enQueue(item) {
// 判断队尾 + 1 是否为队头
// 如果是就代表需要扩容数组
// % this.queue.length 是为了防止数组越界
if (this.first === (this.last + 1) % this.queue.length) {
this.resize(this.getLength() * 2 + 1)
}
this.queue[this.last] = item
this.size++
this.last = (this.last + 1) % this.queue.length
}
deQueue() {
if (this.isEmpty()) {
throw Error('Queue is empty')
}
let r = this.queue[this.first]
this.queue[this.first] = null
this.first = (this.first + 1) % this.queue.length
this.size--
// 判断当前队列大小是否过小
// 为了保证不浪费空间,在队列空间等于总长度四分之一时
// 且不为 2 时缩小总长度为当前的一半
if (this.size === this.getLength() / 4 && this.getLength() / 2 !== 0) {
this.resize(this.getLength() / 2)
}
return r
}
getHeader() {
if (this.isEmpty()) {
throw Error('Queue is empty')
}
return this.queue[this.first]
}
getLength() {
return this.queue.length - 1
}
isEmpty() {
return this.first === this.last
}
resize(length) {
let q = new Array(length)
for (let i = 0; i < length; i++) {
q[i] = this.queue[(i + this.first) % this.queue.length]
}
this.queue = q
this.first = 0
this.last = this.size
}
}
+ +

链表

+

链表是一个线性结构,同时也是一个天然的递归结构。链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。

+
+

实现

单向链表

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// js代码

class Node {
constructor(v, next) {
this.value = v
this.next = next
}
}
class LinkList {
constructor() {
// 链表长度
this.size = 0
// 虚拟头部
this.dummyNode = new Node(null, null)
}
find(header, index, currentIndex) {
if (index === currentIndex) return header
return this.find(header.next, index, currentIndex + 1)
}
addNode(v, index) {
this.checkIndex(index)
// 当往链表末尾插入时,prev.next 为空
// 其他情况时,因为要插入节点,所以插入的节点
// 的 next 应该是 prev.next
// 然后设置 prev.next 为插入的节点
let prev = this.find(this.dummyNode, index, 0)
prev.next = new Node(v, prev.next)
this.size++
return prev.next
}
insertNode(v, index) {
return this.addNode(v, index)
}
addToFirst(v) {
return this.addNode(v, 0)
}
addToLast(v) {
return this.addNode(v, this.size)
}
removeNode(index, isLast) {
this.checkIndex(index)
index = isLast ? index - 1 : index
let prev = this.find(this.dummyNode, index, 0)
let node = prev.next
prev.next = node.next
node.next = null
this.size--
return node
}
removeFirstNode() {
return this.removeNode(0)
}
removeLastNode() {
return this.removeNode(this.size, true)
}
checkIndex(index) {
if (index < 0 || index > this.size) throw Error('Index error')
}
getNode(index) {
this.checkIndex(index)
if (this.isEmpty()) return
return this.find(this.dummyNode, index, 0).next
}
isEmpty() {
return this.size === 0
}
getSize() {
return this.size
}
}
+ +

二叉树

树拥有很多种结构,二叉树是树中最常用的结构,同时也是一个天然的递归结构。

+

二叉树拥有一个根节点,每个节点至多拥有两个子节点,分别为:左节点和右节点。树的最底部节点称之为叶节点,当一颗树的叶数量数量为满时,该树可以称之为满二叉树。

+

二分搜索树

二分搜索树也是二叉树,拥有二叉树的特性。但是区别在于二分搜索树每个节点的值都比他的左子树的值大,比右子树的值小。

+

这种存储方式很适合于数据搜索。如下图所示,当需要查找 6 的时候,因为需要查找的值比根节点的值大,所以只需要在根节点的右子树上寻找,大大提高了搜索效率。

+

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// js代码

class Node {
constructor(value) {
this.value = value
this.left = null
this.right = null
}
}
class BST {
constructor() {
this.root = null
this.size = 0
}
getSize() {
return this.size
}
isEmpty() {
return this.size === 0
}
addNode(v) {
this.root = this._addChild(this.root, v)
}
// 添加节点时,需要比较添加的节点值和当前
// 节点值的大小
_addChild(node, v) {
if (!node) {
this.size++
return new Node(v)
}
if (node.value > v) {
node.left = this._addChild(node.left, v)
} else if (node.value < v) {
node.right = this._addChild(node.right, v)
}
return node
}
}
+ +

以上是最基本的二分搜索树实现,接下来实现树的遍历。

+

对于树的遍历来说,有三种遍历方法,分别是先序遍历中序遍历后序遍历

+

三种遍历的区别在于何时访问节点。在遍历树的过程中,每个节点都会遍历三次,分别是遍历到自己,遍历左子树和遍历右子树。如果需要实现先序遍历,那么只需要第一次遍历到节点时进行操作即可。

+

以下都是递归实现.

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// js代码

// 先序遍历可用于打印树的结构
// 先序遍历先访问根节点,然后访问左节点,最后访问右节点。
preTraversal() {
this._pre(this.root)
}
_pre(node) {
if (node) {
console.log(node.value)
this._pre(node.left)
this._pre(node.right)
}
}
// 中序遍历可用于排序
// 对于 BST 来说,中序遍历可以实现一次遍历就
// 得到有序的值
// 中序遍历表示先访问左节点,然后访问根节点,最后访问右节点。
midTraversal() {
this._mid(this.root)
}
_mid(node) {
if (node) {
this._mid(node.left)
console.log(node.value)
this._mid(node.right)
}
}
// 后序遍历可用于先操作子节点
// 再操作父节点的场景
// 后序遍历表示先访问左节点,然后访问右节点,最后访问根节点。
backTraversal() {
this._back(this.root)
}
_back(node) {
if (node) {
this._back(node.left)
this._back(node.right)
console.log(node.value)
}
}
+ +

以上的这几种遍历都可以称之为深度遍历,对应的还有种遍历叫做广度遍历,也就是一层层地遍历树。对于广度遍历来说,我们需要利用之前讲过的队列结构来完成。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

breadthTraversal() {
if (!this.root) return null
let q = new Queue()
// 将根节点入队
q.enQueue(this.root)
// 循环判断队列是否为空,为空
// 代表树遍历完毕
while (!q.isEmpty()) {
// 将队首出队,判断是否有左右子树
// 有的话,就先左后右入队
let n = q.deQueue()
console.log(n.value)
if (n.left) q.enQueue(n.left)
if (n.right) q.enQueue(n.right)
}
}
+

接下来先介绍如何在树中寻找最小值或最大数。因为二分搜索树的特性,所以最小值一定在根节点的最左边,最大值相反

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码

getMin() {
return this._getMin(this.root).value
}
_getMin(node) {
if (!node.left) return node
return this._getMin(node.left)
}
getMax() {
return this._getMax(this.root).value
}
_getMax(node) {
if (!node.right) return node
return this._getMin(node.right)
}
+ +

向上取整和向下取整,这两个操作是相反的,所以代码也是类似的,这里只介绍如何向下取整。既然是向下取整,那么根据二分搜索树的特性,值一定在根节点的左侧。只需要一直遍历左子树直到当前节点的值不再大于等于需要的值,然后判断节点是否还拥有右子树。如果有的话,继续上面的递归判断。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

floor(v) {
let node = this._floor(this.root, v)
return node ? node.value : null
}
_floor(node, v) {
if (!node) return null
if (node.value === v) return v
// 如果当前节点值还比需要的值大,就继续递归
if (node.value > v) {
return this._floor(node.left, v)
}
// 判断当前节点是否拥有右子树
let right = this._floor(node.right, v)
if (right) return right
return node
}
+

排名,这是用于获取给定值的排名或者排名第几的节点的值,这两个操作也是相反的,所以这个只介绍如何获取排名第几的节点的值。对于这个操作而言,我们需要略微的改造点代码,让每个节点拥有一个 size 属性。该属性表示该节点下有多少子节点(包含自身)。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// js代码

class Node {
constructor(value) {
this.value = value
this.left = null
this.right = null
// 修改代码
this.size = 1
}
}
// 新增代码
_getSize(node) {
return node ? node.size : 0
}
_addChild(node, v) {
if (!node) {
return new Node(v)
}
if (node.value > v) {
// 修改代码
node.size++
node.left = this._addChild(node.left, v)
} else if (node.value < v) {
// 修改代码
node.size++
node.right = this._addChild(node.right, v)
}
return node
}
select(k) {
let node = this._select(this.root, k)
return node ? node.value : null
}
_select(node, k) {
if (!node) return null
// 先获取左子树下有几个节点
let size = node.left ? node.left.size : 0
// 判断 size 是否大于 k
// 如果大于 k,代表所需要的节点在左节点
if (size > k) return this._select(node.left, k)
// 如果小于 k,代表所需要的节点在右节点
// 注意这里需要重新计算 k,减去根节点除了右子树的节点数量
if (size < k) return this._select(node.right, k - size - 1)
return node
}
+ +

接下来讲解的是二分搜索树中最难实现的部分:删除节点。因为对于删除节点来说,会存在以下几种情况

+
    +
  • 需要删除的节点没有子树
  • +
  • 需要删除的节点只有一条子树
  • +
  • 需要删除的节点有左右两条树
  • +
+

对于前两种情况很好解决,但是第三种情况就有难度了,所以先来实现相对简单的操作:删除最小节点,对于删除最小节点来说,是不存在第三种情况的,删除最大节点操作是和删除最小节点相反的,所以这里也就不再赘述。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码

delectMin() {
this.root = this._delectMin(this.root)
console.log(this.root)
}
_delectMin(node) {
// 一直递归左子树
// 如果左子树为空,就判断节点是否拥有右子树
// 有右子树的话就把需要删除的节点替换为右子树
if ((node != null) & !node.left) return node.right
node.left = this._delectMin(node.left)
// 最后需要重新维护下节点的 `size`
node.size = this._getSize(node.left) + this._getSize(node.right) + 1
return node
}
+ +

最后讲解的就是如何删除任意节点了。对于这个操作,T.Hibbard 在 1962 年提出了解决这个难题的办法,也就是如何解决第三种情况。

+

当遇到这种情况时,需要取出当前节点的后继节点(也就是当前节点右子树的最小节点)来替换需要删除的节点。然后将需要删除节点的左子树赋值给后继结点,右子树删除后继结点后赋值给他。

+

你如果对于这个解决办法有疑问的话,可以这样考虑。因为二分搜索树的特性,父节点一定比所有左子节点大,比所有右子节点小。那么当需要删除父节点时,势必需要拿出一个比父节点大的节点来替换父节点。这个节点肯定不存在于左子树,必然存在于右子树。然后又需要保持父节点都是比右子节点小的,那么就可以取出右子树中最小的那个节点来替换父节点。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// js代码

delect(v) {
this.root = this._delect(this.root, v)
}
_delect(node, v) {
if (!node) return null
// 寻找的节点比当前节点小,去左子树找
if (node.value < v) {
node.right = this._delect(node.right, v)
} else if (node.value > v) {
// 寻找的节点比当前节点大,去右子树找
node.left = this._delect(node.left, v)
} else {
// 进入这个条件说明已经找到节点
// 先判断节点是否拥有拥有左右子树中的一个
// 是的话,将子树返回出去,这里和 `_delectMin` 的操作一样
if (!node.left) return node.right
if (!node.right) return node.left
// 进入这里,代表节点拥有左右子树
// 先取出当前节点的后继结点,也就是取当前节点右子树的最小值
let min = this._getMin(node.right)
// 取出最小值后,删除最小值
// 然后把删除节点后的子树赋值给最小值节点
min.right = this._delectMin(node.right)
// 左子树不动
min.left = node.left
node = min
}
// 维护 size
node.size = this._getSize(node.left) + this._getSize(node.right) + 1
return node
}
+ +

AVL 树

+

二分搜索树实际在业务中是受到限制的,因为并不是严格的 O(logN),在极端情况下会退化成链表,比如加入一组升序的数字就会造成这种情况。

+
+
+

AVL 树改进了二分搜索树,在 AVL 树中任意节点的左右子树的高度差都不大于 1,这样保证了时间复杂度是严格的 O(logN)。基于此,对 AVL 树增加或删除节点时可能需要旋转树来达到高度的平衡。

+
+

实现

因为 AVL 树是改进了二分搜索树,所以部分代码是于二分搜索树重复的,对于重复内容不作再次解析。

+

对于 AVL 树来说,添加节点会有四种情况
lWB0nf.png

+

对于左左情况来说,新增加的节点位于节点 2 的左侧,这时树已经不平衡,需要旋转。因为搜索树的特性,节点比左节点大,比右节点小,所以旋转以后也要实现这个特性。

+

旋转之前:new < 2 < C < 3 < B < 5 < A,右旋之后节点 3 为根节点,这时候需要将节点 3 的右节点加到节点 5 的左边,最后还需要更新节点的高度。

+

对于右右情况来说,相反于左左情况,所以不再赘述。

+

对于左右情况来说,新增加的节点位于节点 4 的右侧。对于这种情况,需要通过两次旋转来达到目的。

+

首先对节点的左节点左旋,这时树满足左左的情况,再对节点进行一次右旋就可以达到目的。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// js代码

class Node {
constructor(value) {
this.value = value
this.left = null
this.right = null
this.height = 1
}
}

class AVL {
constructor() {
this.root = null
}
addNode(v) {
this.root = this._addChild(this.root, v)
}
_addChild(node, v) {
if (!node) {
return new Node(v)
}
if (node.value > v) {
node.left = this._addChild(node.left, v)
} else if (node.value < v) {
node.right = this._addChild(node.right, v)
} else {
node.value = v
}
node.height =
1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
let factor = this._getBalanceFactor(node)
// 当需要右旋时,根节点的左树一定比右树高度高
if (factor > 1 && this._getBalanceFactor(node.left) >= 0) {
return this._rightRotate(node)
}
// 当需要左旋时,根节点的左树一定比右树高度矮
if (factor < -1 && this._getBalanceFactor(node.right) <= 0) {
return this._leftRotate(node)
}
// 左右情况
// 节点的左树比右树高,且节点的左树的右树比节点的左树的左树高
if (factor > 1 && this._getBalanceFactor(node.left) < 0) {
node.left = this._leftRotate(node.left)
return this._rightRotate(node)
}
// 右左情况
// 节点的左树比右树矮,且节点的右树的右树比节点的右树的左树矮
if (factor < -1 && this._getBalanceFactor(node.right) > 0) {
node.right = this._rightRotate(node.right)
return this._leftRotate(node)
}

return node
}
_getHeight(node) {
if (!node) return 0
return node.height
}
_getBalanceFactor(node) {
return this._getHeight(node.left) - this._getHeight(node.right)
}
// 节点右旋
// 5 2
// / \ / \
// 2 6 ==> 1 5
// / \ / / \
// 1 3 new 3 6
// /
// new
_rightRotate(node) {
// 旋转后新根节点
let newRoot = node.left
// 需要移动的节点
let moveNode = newRoot.right
// 节点 2 的右节点改为节点 5
newRoot.right = node
// 节点 5 左节点改为节点 3
node.left = moveNode
// 更新树的高度
node.height =
1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
newRoot.height =
1 +
Math.max(this._getHeight(newRoot.left), this._getHeight(newRoot.right))

return newRoot
}
// 节点左旋
// 4 6
// / \ / \
// 2 6 ==> 4 7
// / \ / \ \
// 5 7 2 5 new
// \
// new
_leftRotate(node) {
// 旋转后新根节点
let newRoot = node.right
// 需要移动的节点
let moveNode = newRoot.left
// 节点 6 的左节点改为节点 4
newRoot.left = node
// 节点 4 右节点改为节点 5
node.right = moveNode
// 更新树的高度
node.height =
1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
newRoot.height =
1 +
Math.max(this._getHeight(newRoot.left), this._getHeight(newRoot.right))

return newRoot
}
}
+ +

Trie

+

在计算机科学,trie,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。
简单点来说,这个结构的作用大多是为了方便搜索字符串,该树有以下几个特点

+
+
    +
  • 根节点代表空字符串,每个节点都有 N(假如搜索英文字符,就有 26 条) 条链接,每条链接代表一个字符
  • +
  • 节点不存储字符,只有路径才存储,这点和其他的树结构不同
  • +
  • 从根节点开始到任意一个节点,将沿途经过的字符连接起来就是该节点对应的字符串
  • +
+

实现

总得来说 Trie 的实现相比别的树结构来说简单的很多,实现就以搜索英文字符为例。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// js代码

class TrieNode {
constructor() {
// 代表每个字符经过节点的次数
this.path = 0
// 代表到该节点的字符串有几个
this.end = 0
// 链接
this.next = new Array(26).fill(null)
}
}
class Trie {
constructor() {
// 根节点,代表空字符
this.root = new TrieNode()
}
// 插入字符串
insert(str) {
if (!str) return
let node = this.root
for (let i = 0; i < str.length; i++) {
// 获得字符先对应的索引
let index = str[i].charCodeAt() - 'a'.charCodeAt()
// 如果索引对应没有值,就创建
if (!node.next[index]) {
node.next[index] = new TrieNode()
}
node.path += 1
node = node.next[index]
}
node.end += 1
}
// 搜索字符串出现的次数
search(str) {
if (!str) return
let node = this.root
for (let i = 0; i < str.length; i++) {
let index = str[i].charCodeAt() - 'a'.charCodeAt()
// 如果索引对应没有值,代表没有需要搜素的字符串
if (!node.next[index]) {
return 0
}
node = node.next[index]
}
return node.end
}
// 删除字符串
delete(str) {
if (!this.search(str)) return
let node = this.root
for (let i = 0; i < str.length; i++) {
let index = str[i].charCodeAt() - 'a'.charCodeAt()
// 如果索引对应的节点的 Path 为 0,代表经过该节点的字符串
// 已经一个,直接删除即可
if (--node.next[index].path == 0) {
node.next[index] = null
return
}
node = node.next[index]
}
node.end -= 1
}
}
+ +

并查集

+

并查集是一种特殊的树结构,用于处理一些不交集的合并及查询问题。该结构中每个节点都有一个父节点,如果只有当前一个节点,那么该节点的父节点指向自己。
这个结构中有两个重要的操作,分别是:

+
+
    +
  • Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
  • +
  • Union:将两个子集合并成同一个集合。
  • +
+

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// js代码

class DisjointSet {
// 初始化样本
constructor(count) {
// 初始化时,每个节点的父节点都是自己
this.parent = new Array(count)
// 用于记录树的深度,优化搜索复杂度
this.rank = new Array(count)
for (let i = 0; i < count; i++) {
this.parent[i] = i
this.rank[i] = 1
}
}
find(p) {
// 寻找当前节点的父节点是否为自己,不是的话表示还没找到
// 开始进行路径压缩优化
// 假设当前节点父节点为 A
// 将当前节点挂载到 A 节点的父节点上,达到压缩深度的目的
while (p != this.parent[p]) {
this.parent[p] = this.parent[this.parent[p]]
p = this.parent[p]
}
return p
}
isConnected(p, q) {
return this.find(p) === this.find(q)
}
// 合并
union(p, q) {
// 找到两个数字的父节点
let i = this.find(p)
let j = this.find(q)
if (i === j) return
// 判断两棵树的深度,深度小的加到深度大的树下面
// 如果两棵树深度相等,那就无所谓怎么加
if (this.rank[i] < this.rank[j]) {
this.parent[i] = j
} else if (this.rank[i] > this.rank[j]) {
this.parent[j] = i
} else {
this.parent[i] = j
this.rank[j] += 1
}
}
}
+ +

堆通常是一个可以被看做一棵树的数组对象。
堆的实现通过构造二叉堆,实为二叉树的一种。这种数据结构具有以下性质。

+
    +
  • 任意节点小于(或大于)它的所有子节点
  • +
  • 堆总是一棵完全树。即除了最底层,其他层的节点都被元素填满,且最底层从左到右填入。
    将根节点最大的堆叫做最大堆大根堆,根节点最小的堆叫做最小堆小根堆
    优先队列也完全可以用堆来实现,操作是一模一样的。
  • +
+

实现大根堆

堆的每个节点的左边子节点索引是 i * 2 + 1,右边是 i * 2 + 2,父节点是 (i - 1) /2。
堆有两个核心的操作,分别是 shiftUp 和 shiftDown 。前者用于添加元素,后者用于删除根节点。
shiftUp 的核心思路是一路将节点与父节点对比大小,如果比父节点大,就和父节点交换位置。
shiftDown 的核心思路是先将根节点和末尾交换位置,然后移除末尾元素。接下来循环判断父节点和两个子节点的大小,如果子节点大,就把最大的子节点和父节点交换。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// js代码

class MaxHeap {
constructor() {
this.heap = []
}
size() {
return this.heap.length
}
empty() {
return this.size() == 0
}
add(item) {
this.heap.push(item)
this._shiftUp(this.size() - 1)
}
removeMax() {
this._shiftDown(0)
}
getParentIndex(k) {
return parseInt((k - 1) / 2)
}
getLeftIndex(k) {
return k * 2 + 1
}
_shiftUp(k) {
// 如果当前节点比父节点大,就交换
while (this.heap[k] > this.heap[this.getParentIndex(k)]) {
this._swap(k, this.getParentIndex(k))
// 将索引变成父节点
k = this.getParentIndex(k)
}
}
_shiftDown(k) {
// 交换首位并删除末尾
this._swap(k, this.size() - 1)
this.heap.splice(this.size() - 1, 1)
// 判断节点是否有左孩子,因为二叉堆的特性,有右必有左
while (this.getLeftIndex(k) < this.size()) {
let j = this.getLeftIndex(k)
// 判断是否有右孩子,并且右孩子是否大于左孩子
if (j + 1 < this.size() && this.heap[j + 1] > this.heap[j]) j++
// 判断父节点是否已经比子节点都大
if (this.heap[k] >= this.heap[j]) break
this._swap(k, j)
k = j
}
}
_swap(left, right) {
let rightValue = this.heap[right]
this.heap[right] = this.heap[left]
this.heap[left] = rightValue
}
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:计算机通识 ---- 数据结构

+

文章作者:

+

发布时间:2020年01月09日 - 07:35

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/09/FE-guide-data-structure/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/09/career/index.html b/en/2020/01/09/career/index.html new file mode 100644 index 0000000000..e7895180d3 --- /dev/null +++ b/en/2020/01/09/career/index.html @@ -0,0 +1,599 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 如何正确使用时间[转载] | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 如何正确使用时间[转载] +

+ + +
+ + + + +
+ + +

前言

最近各种技术博客,各种技术文章,各种牛人前辈的学习心得都在看,最近看了一个不错的文章叫做《如何正确使用时间》
感觉还是很不错的,特别适合年轻的程序员看一下。下面将全文贴在下方,如果需要也可以去原文查看。

+

原文

+

你是否时常会焦虑时间过的很快,没时间学习,本文将会分享一些个人的见解。

+

花时间补基础,读文档

在工作中我们时常会花很多时间去 debug,但是你是否发现很多问题最终只是你基础不扎实或者文档没有仔细看。

+

基础是你技术的基石,一定要花时间打好基础,而不是追各种新的技术。一旦你的基础扎实,学习各种新的技术也肯定不在话下,因为新的技术,究其根本都是相通的。

+

文档同样也是一门技术的基础。一个优秀的库,开发人员肯定已经把如何使用这个库都写在文档中了,仔细阅读文档一定会是少写 bug 的最省事路子。

+

学会搜索

如果你还在使用百度搜索编程问题,请尽快抛弃这个垃圾搜索引擎。同样一个关键字,使用百度和谷歌,谷歌基本完胜的。即使你使用中文在谷歌中搜索,得到的结果也往往是谷歌占优,所以如果你想迅速的通过搜索引擎来解决问题,那一定是谷歌。

+

学点英语

说到英语,一定是大家所最不想听的。其实我一直认为程序员学习英语是简单的,因为我们工作中是一直接触着英语,并且看懂技术文章,文档所需要的单词量是极少的。我时常在群里看到大家发出一个问题的截图问什么原因,其实在截图中英语已经很明白的说明了问题的所在,如果你的英语过关,完全不需要浪费时间来提问和搜索。所以我认为学点英语也是节省时间中很重要的一点。

+

那么如何去学习呢,chrome 装个翻译插件,直接拿英文文档或文章读,不会的就直接划词翻译,然后记录下这个单词并背诵。每天花半小时看点英文文档和文章,坚持两个月,你的英语水平不说别的,看文档和文章绝对不会有难题了。这一定是一个很划算的个人时间投资,花点时间学习英语,能为你将来的技术之路铺平很多坎。

+

画个图,想一想再做

你是否遇到过这种问题,需求一下来,看一眼,然后马上就按照设计稿开始做了,可能中间出个问题导致你需要返工。

+

如果你存在这样的问题,我很推荐在看到设计稿和需求的时候花点时间想一想,画一画。考虑一下设计稿中是否可以找到可以拆分出来的复用组件,是否存在之前写过的组件。该如何组织这个界面,数据的流转是怎么样的。然后画一下这个页面的需求,最后再动手做。

+

利用好下班时间学习

说到下班时间,那可能就有人说了公司很迟下班,这其实是国内很普遍的情况。但是我认为正常的加班是可以的,但是强制的加班就是在损耗你的身体和前途。

+

可以这么说,大部分的 996 公司,加班的这些时间并不会增加你的技术,无非就是在写一些重复的业务逻辑。也许你可以拿到更多的钱,但是代价是身体还有前途。程序员是靠技术吃饭的,如果你长久呆在一个长时间加班的公司,不能增长你的技术还要吞噬你的下班学习时间,那么你一定会废掉的。如果你遇到了这种情况,只能推荐尽快跳槽到非 996 的公司。

+

那么如果你有足够的下班时间,一定要花上 1, 2 小时去学习,上班大家基本都一样,技术的精进就是看下班以后的那几个小时了。如果你能利用好下班时间来学习,坚持下去,时间一定会给你很好的答复。

+

列好 ToDo

我喜欢规划好一段时间内要做的事情,并且要把事情拆分为小点。给 ToDo 列好优先级,紧急的优先级最高。相同优先级的我喜欢先做简单的,因为这样一旦完成就能划掉一个,提高成就感。

+

反思和整理

每周末都会花上点时间整理下本周记录的笔记和看到的不错文章。然后考虑下本周完成的工作和下周准备要完成的工作。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:如何正确使用时间[转载]

+

文章作者:

+

发布时间:2020年01月09日 - 16:33

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/09/career/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/09/do-it-yourselfery-jsonp/index.html b/en/2020/01/09/do-it-yourselfery-jsonp/index.html new file mode 100644 index 0000000000..1888f99817 --- /dev/null +++ b/en/2020/01/09/do-it-yourselfery-jsonp/index.html @@ -0,0 +1,590 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- jsonp | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- jsonp +

+ + +
+ + + + +
+ + +

前言

面试中时常会出现需要手动实现某个功能的要求,所以我准备整理一个自己动手实现系列文章,第一个呢就是自己动手实现 jsonp

+

实现 jsonp 思路

    +
  1. 将传入的data数据转化为url字符串形式
  2. +
  3. 处理url中的回调函数
  4. +
  5. 创建一个script标签并插入到页面中
  6. +
  7. 挂载回调函数
  8. +
+

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// js代码

(function (window,document) {
"use strict";
var jsonp = function (url,data,callback) {
// 1.将传入的data数据转化为url字符串形式
// {id:1,name:'jack'} => id=1&name=jack
var dataString = url.indexof('?') == -1? '?': '&';
for(var key in data){
dataString += key + '=' + data[key] + '&';
};

// 2 处理url中的回调函数
// cbFuncName回调函数的名字 :my_json_cb_名字的前缀 + 随机数(把小数点去掉)
var cbFuncName = 'my_json_cb_' + Math.random().toString().replace('.','');
dataString += 'callback=' + cbFuncName;

// 3.创建一个script标签并插入到页面中
var scriptEle = document.createElement('script');
scriptEle.src = url + dataString;

// 4.挂载回调函数
window[cbFuncName] = function (data) {
callback(data); // 处理完回调函数的数据之后,删除jsonp的script标签
document.body.removeChild(scriptEle);
}
document.body.appendChild(scriptEle);
}
window.$jsonp = jsonp;
})(window,document)
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- jsonp

+

文章作者:

+

发布时间:2020年01月09日 - 08:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/09/do-it-yourselfery-jsonp/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/09/solve-get-params-so-long-problem/index.html b/en/2020/01/09/solve-get-params-so-long-problem/index.html new file mode 100644 index 0000000000..d6e0972908 --- /dev/null +++ b/en/2020/01/09/solve-get-params-so-long-problem/index.html @@ -0,0 +1,620 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 解决get请求过长的问题小记 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 解决get请求过长的问题小记 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

就在前天,公司里一位新入职的前端小伙伴找到我说遇到了一个问题,自己写的vue代码本地运行是好用的,但是打包后提给后台就访问不了接口了,刚开始我以为是代理配置的有问题,但是经过检查也没什么问题,因为是个get方法,所以我就直接在浏览器地址里敲了api地址,也能正常获取到数据,那么这么看就不是代理或者接口的问题,后来我又看了下调试工具,network中根本就没发出请求,我怀疑是直接前台就拦截了,最后我发现他这个api参数超级长,长的相当吓人,可以看一下下图。
lROUMt.png
我记得url是有长度限制的,于是我就把url变短再次请求,虽然因为参数不全,接口报了错,但是发出去了,由此我定位应该是url超长,因为vue中配置了什么导致的访问被拦截了。于是我就开始了搜索引擎查找解决方案之旅,接下来就把我了解到的信息一一整理。

+ + +

URL 限制

首先我在网上找到了一份资料介绍了URL长度的相关资料,从下面可以看出,从HTTP协议层面以及Get请求层面都没有什么限制,这个限制来自于浏览器或者服务器的限制

+
+

Microsoft Internet Explorer (Browser)
IE浏览器对URL的最大限制为2083个字符,如果超过这个数字,提交按钮没有任何反应。
Firefox (Browser)
对于Firefox浏览器URL的长度限制为65,536个字符。
Safari (Browser)
URL最大长度限制为 80,000个字符。
Opera (Browser)
URL最大长度限制为190,000个字符。
Google (Browser)
URL最大长度限制为8182个字符。
Apache (Server)
能接受最大url长度为8,192个字符。
Microsoft Internet Information Server(IIS)
能接受最大url的长度为16,384个字符。

+
+

而且,中文会进行编码,一个汉字编码后会生成9个字符,这样算来,IE下最多也就能输入231个中文,再多就完蛋了,那么通过get请求传递参数就会显得很麻烦。

+

通常情况下,这种超长参数的请求我们都会用post,有些地方也会说post请求没有长度限制,但是前面说了,实际上HTTP协议层面并没有任何的限制,限制只出现在浏览器或者服务器限制,get和post请求在底层上其实是一样的。

+

最后项目修改了请求类型,把 get 请求改成了 post 请求,在网上实际还找到了另外两个方案,如果对同一组参数频繁访问的化,也可以用 post+get 请求的方式去处理,或者用 sessionStorage 下面简单介绍一下。

+
    +
  1. 将预览内容 post 到服务端,根据一个唯一标识生成缓存(有效时间5分钟),将唯一标识返回到前端,前端通过get方式传递唯一标识请求预览逻辑,拿到缓存的内容后渲染到页面。需要说明的是这里的缓存必须是分布式的。
  2. +
  3. 通过H5的会话缓存 sessionStorage 将预览内容存储在浏览器,打开预览页后从 sessionStorage 中拿到内容就可以渲染出页面了。
  4. +
+

上述两种方案都不太符合我们的项目所以最终还是选择了最简单的方式

+

GET VS POST

    +
  1. 多数浏览器对于POST采用两阶段发送数据的,先发送请求头,再发送请求体,即使参数再少再短,也会被分成两个步骤来发送(相对于GET),也就是第一步发送header数据,第二步再发送body部分。HTTP是应用层的协议,而在传输层有些情况TCP会出现两次连结的过程,HTTP协议本身不保存状态信息,一次请求一次响应。对于TCP而言,通信次数越多反而靠性越低,能在一次连结中传输完需要的消息是最可靠的,尽量使用GET请求来减少网络耗时。如果通信时间增加,这段时间客户端与服务器端一直保持连接状态,在服务器侧负载可能会增加,可靠性会下降。

    +
  2. +
  3. GET请求能够被cache,GET请求能够被保存在浏览器的浏览历史里面(密码等重要数据GET提交,别人查看历史记录,就可以直接看到这些私密数据)POST不进行缓存。

    +
  4. +
  5. GET参数是带在URL后面,传统IE中URL的最大可用长度为2048字符,其他浏览器对URL长度限制实现上有所不同。POST请求无长度限制(目前理论上是这样的)。

    +
  6. +
  7. GET提交的数据大小,不同浏览器的限制不同,一般在2k-8K之间,POST提交数据比较大,大小靠服务器的设定值限制,而且某些数据只能用 POST 方法「携带」,比如 file。

    +
  8. +
  9. 全部用POST不是十分合理,最好先把请求按功能和场景分下类,对数据请求频繁,数据不敏感且数据量在普通浏览器最小限定的2k范围内,这样的情况使用GET。其他地方使用POST。

    +
  10. +
  11. GET 的本质是「得」,而 POST 的本质是「给」。而且,GET 是「幂等」的,在这一点上,GET 被认为是「安全的」。但实际上 server 端也可以用作资源更新,但是这种用法违反了约定,容易造成 CSRF(跨站请求伪造)。

    +
  12. +
+

写在最后

以上是这次遇到问题后学到的一点知识,可能并不全面,后续如果遇到了类似的问题会继续丰富这篇文章。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:解决get请求过长的问题小记

+

文章作者:

+

发布时间:2020年01月09日 - 11:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/09/solve-get-params-so-long-problem/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/13/FE-guide-inherit2/index.html b/en/2020/01/13/FE-guide-inherit2/index.html new file mode 100644 index 0000000000..9e65a90dd9 --- /dev/null +++ b/en/2020/01/13/FE-guide-inherit2/index.html @@ -0,0 +1,667 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 继承类型 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 继承类型 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

继承的操作需要有一个父类,这里使用构造函数外加原型来创建一个:
有下面两个类,下面实现 Child 继承 Father:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

function Father() {
this.type = 'prople';
}

Father.prototype.eat = function() {
console.log('吃东西啦');
};

function Child(name) {
this.name = name;
this.color = 'black';
}
+ +

原型继承(认贼作父)

+

关键点:子类原型等于父类的实例 Child.prototype = new Person()(将父类指向子类的原型)。

+
+
1
2
// js代码
Child.prototype = new Father();
+ +

特点:
实例可继承的属性有:

+
    +
  • 实例的构造函数的属性
  • +
  • 父类构造函数的属性
  • +
  • 父类原型上的属性
    新实例不会继承父类实例的属性
  • +
+

缺点:

+
    +
  • 新实例无法向父类构造函数传参
  • +
  • 继承单一
  • +
  • 所有新实例都会共享父类实例的属性。— 原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改
  • +
+

构造继承(借腹生子)

+

在子类构造函数中调用父类构造函数

+
+
1
2
3
4
// js代码
function Child(name) {
Father.call(this);
}
+ +

关键点:用 call 或 apply 将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制))Person.call(this, ‘reng’)
特点:

+
    +
  • 只继承了父类构造函数的属性,没有继承父类原型的属性
  • +
  • 解决了原型链继承的注意事项(缺点)1,2,3
  • +
  • 可以继承多个构造函数的属性(call 可以多个)
  • +
  • 在子实例中可以向父实例传参
    缺点:
  • +
  • 只能继承父类构造函数的属性
  • +
  • 无法实现构造函数的复用。(每次用每次都要重新调用)
  • +
  • 每个新实例都有构造函数的副本,臃肿
    (不能继承父类原型,函数在构造函数中,每个子类实例不能共享函数,浪费内存。)
  • +
+

组合继承(原型继承+构造继承)

+

使用构造继承继承父类参数,使用原型继承继承父类函数

+
+
1
2
3
4
5
6
7
// js代码
function Child(name) {
// 构造继承
Father.call(this);
}

Child.prototype = Father.prototype; // Child.prototype = new Person(); // 原型继承
+ +

关键点:结合了两种模式的优点–向父类传参(call)和复用(prototype)
特点:

+
    +
  • 可以继承父类原型上的属性,可以传参,可复用
  • +
  • 每个新实例引入的构造函数属性是私有的
  • +
+

缺点:

+
    +
  • 父类原型和子类原型是同一个对象,无法区分子类真正是由谁构造。
  • +
  • 调用了两次父类的构造函数(耗内存)
  • +
  • 子类的构造函数会代替原型上的那个父类构造函数(call 相当于拿到了父类构造函数的副本)
  • +
+

原型式继承(复制降级)

1
2
3
4
5
6
7
8
9
10
11
// 先封装一个函数容器,用来承载继承的原型和输出对象
function create(obj) {
// 寄生
function F() {}
F.prototype = obj;
return new F();
}
var father = new Father();
var child = create(father);
console.log(child instanceof Father); // true
console.log(child.job); // frontend
+ +

关键点:用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了可以随意增添属性的实例或对象。Object.create()就是这个原理。

+

特点:

+
    +
  • 类似于复制一个对象,用函数来包装
  • +
+

注意事项:

+
    +
  • 所有的实例都会继承原型上的属性
  • +
  • 无法实现复用。(新实例属性都是后面添加的)
    Object.create()方法规范了原型式继承。这个方法接收两个参数,一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。
  • +
+
1
2
3
4
5
6
7
8
9
10
11
// 传一个参数的时候
var child = Object.create(new Father());
console.log(child.job); // frontend
console.log(child instanceof Father); // true
// 传两个参数的时候
var child = Object.create(new Father(), {
name: {
value: 'come on'
}
});
child.sayHello(); // Hello come on
+ +

寄生组合继承

它跟组合继承一样,都比较常用。
寄生:在函数内返回对象然后调用
组合

+
    +
  • 函数的原型等于另一个实例
  • +
  • 在函数中用 apply 或 call 引入另一个构造函数,可传参
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 寄生
function create(obj) {
function F() {}
F.prototype = obj;
return new F();
}
// object是F实例的另一种表示方法
var obj = create(Father.prototype);
// obj实例(F实例)的原型继承了父类函数的原型
// 上述更像是原型链继承,只不过只继承了原型属性

// 组合
function Child() {
// 构造
this.age = 100;
Father.call(this); // 这个继承了父类构造函数的属性
} // 解决了组合式两次调用构造函数属性的特点

// 重点
Child.prototype = obj; // 原型

console.log(Child.prototype.constructor); // Father
obj.constructor = Child; // 一定要修复实例
console.log(Child.prototype.constructor); // Child
var child = new Child();
// Child实例就继承了构造函数属性,父类实例,object的函数属性
console.log(child.job); // frontend
console.log(child instanceof Father); // true
+ +

重点:修复了组合继承的问题

+

在上面的问题中,你可能发现了这么一个注释obj.constructor = Sub; // 一定要修复实例。为什么要修正子类的构造函数的指向呢?

+

因为在不修正这个指向的时候,在获取构造函数返回的时候,在调用同名属性或方法取值上可能造成混乱。比如下面:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Car() {}
Car.prototype.orderOneLikeThis = function() {
// Clone producing function
return new this.constructor();
};
Car.prototype.advertise = function() {
console.log('I am a generic car.');
};

function BMW() {}
BMW.prototype = Object.create(Car.prototype);
BMW.prototype.constructor = BMW; // Resetting the constructor property
BMW.prototype.advertise = function() {
console.log('I am BMW with lots of uber features.');
};

var x5 = new BMW();

var myNewToy = x5.orderOneLikeThis();

myNewToy.advertise(); // => "I am BMW ..." if `BMW.prototype.constructor = BMW;` is not
// commented; "I am a generic car." otherwise.
+ +

object.create 在组合继承的基础上,子类继承一个由父类原型生成的空对象。

+
1
2
3
4
5
6
7
8
9
10
// js代码
function Child(name) {
Father.call(this);
}

Child.prototype = Object.create(Father.prototype, {
constructor: {
value: Child
}
});
+ +

inherits 函数 — Nodejs util.inherits 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// js代码

function inherits = function(ctor, superCtor) {
ctor.super_ = superCtor; // super_属性是子类继承父类时构造函数要写入的一个属性值.
ctor.prototype = Object.create(superCtor.prototype, {
constructor: {
value: ctor,
enumerable: false,
writable: true,
configurable: true
}
});
};
// 在上面的代码中 ctor 想要继承 superCtor ,我们姑且把 ctor 称作子类, superCtor 称作父类.

// 使用
function Child() {
Father.call(this);
//...
}
inherits(Child, Father);

Child.prototype.fun = ...
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 继承类型

+

文章作者:

+

发布时间:2020年01月13日 - 03:18

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/13/FE-guide-inherit2/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/13/Javascript-Design-Pattern/index.html b/en/2020/01/13/Javascript-Design-Pattern/index.html new file mode 100644 index 0000000000..d5416c3ad2 --- /dev/null +++ b/en/2020/01/13/Javascript-Design-Pattern/index.html @@ -0,0 +1,665 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + JavaScript 设计模式 Design Pattern | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ JavaScript 设计模式 Design Pattern +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在整理自己的知识体系,一方面为了夯实基础,另一方面也为了查缺补漏、重新理解,设计模式之前看过大概三四次,从最开始一点都不懂,到后面看了几遍以后慢慢理解,今天再次整理一下,希望这一遍下来之后能更加透彻的理解什么是设计模式。

+ + +

什么是设计模式

+

设计模式的定义:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案
设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。

+
+
+

  使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的;设计模式使代码编制真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。 —- 百度百科

+
+

不知道大家看了上面的定义以后是什么感受,说实话我第一次看到这句话并没有什么深刻的认识,什么面向对象的软件设计,什么针对特定问题,什么优雅的解决方案,这都说的是什么,后来我看了几遍之后,上面这句话用我的理解翻译如下:

+
+

软件开发过程中,解决某一类问题用到的一系列套路

+
+

这就是我对设计模式的认识。当然这也不仅仅是我自己的认识,在跟其他的一些开发人员交流时,很多人都是这么认为的。

+

这些解决问题的方案实在是太好用了,所以大神就把它们抽象出来,然后起了个名字-就叫做设计模式了。

+

这么说大家可能还是不太明白,举个开发过程中可能遇到的实际例子吧。

+
+

当系统中某个接口的结构已经无法满足我们现在的业务需求,但又不能改动这个接口,因为可能原来的系统很多功能都依赖于这个接口,改动接口会牵扯到太多文件。那么这种场景下我们该如何解决这个问题呢?通常我们需要新增一个接口,兼容原来的接口和新的业务需求参数。
因此应对这种场景,我们可以很快地想到可以用适配器模式来解决这个问题。

+
+

这就是设计模式的应用,实际上也许你还不知道设计模式这个词,但是你已经在工作中频繁的用到了设计模式,下面我们就来看看到底有哪些设计模式。

+

哦,对了,设计模式并不依赖于语言,它本身更像是一种软件的设计思想,因为我是一个前端,所以接下来具体实现的时候我会使用js来实现设计模式的用法。

+

学习设计模式

目前被普遍接受的经典的设计模式共有 23 种,而这23种设计模式又分为了 3大类 ,看过一张图这里拿过来镇贴。
lHgD4H.jpg
他们分别是

+
    +
  • 创建型模式
  • +
  • 结构型模式
  • +
  • 行为型模式
  • +
+

接下来,我将会将这23种,3大类设计模式一个个的拆解开来,跟大家一起学习一下,设计模式有哪些内容。

+

创建型模式 6个

这类模式用于对象的生成生命周期的管理
创建型模式可以决定生成哪些对象,提高了程序的灵活性。具体属于此类的模式清单如下,共有 5 个:

+
    +
  • 单例模式(Singleton)
  • +
  • 工厂方法模式(Factory Method)
  • +
  • 抽象工厂模式(Abstract Factory)
  • +
  • 建造者模式(Builder)
  • +
  • 原型模式(Prototype)
  • +
  • 迭代器模式(Iterator)
  • +
+

单例模式(Singleton)

描述:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

+

工厂方法模式(Factory Method)

描述:定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。

+

抽象工厂模式(Abstract Factory)

描述:为创建一组相关或相互依赖的对象提供一个接口,而且无需指定它们的具体类

+

建造者模式(Builder)

描述:将一个复杂对象的构建与他的表示分离,使得同样的构建过程可以创建不同的表示。

+

原型模式(Prototype)

描述:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

+

迭代器模式(Iterator)

描述:提供一种方法访问一个容器对象中的各个元素,而又不需暴露该对象的内部细节。

+

结构型模式 7个

    +
  • 适配器模式(Adapter)
  • +
  • 组合模式(Compositor)
  • +
  • 代理模式(Proxy)
  • +
  • 桥梁模式(Bridge)
  • +
  • 装饰模式(Decorator)
  • +
  • 门面模式(Facade)
  • +
  • 享元模式(Flyweight)
  • +
+

适配器模式(Adapter)

描述:将一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。

+

组合模式(Compositor)

描述:将对象组合成树形结构

+

代理模式(Proxy)

描述:

+

桥梁模式(Bridge)

描述:

+

装饰模式(Decorator)

描述:

+

门面模式(Facade)

描述:

+

享元模式(Flyweight)

描述:

+

行为型模式 10个

    +
  • 命名模式(Command)
  • +
  • 解释器模式(Interpreter)
  • +
  • 责任链模式(Chian of Responsibility)
  • +
  • 观察者模式(Observer)
  • +
  • 中介者模式(Mediator)
  • +
  • 备忘录模式(Memento)
  • +
  • 状态模式(State)
  • +
  • 策略模式(Strategy)
  • +
  • 模板方法模式(Template Method)
  • +
  • 访问者模式(Visitor)
  • +
+

命名模式(Command)

描述:

+

解释器模式(Interpreter)

描述:

+

责任链模式(Chian of Responsibility)

描述:

+

观察者模式(Observer)

描述:

+

中介者模式(Mediator)

描述:

+

备忘录模式(Memento)

描述:

+

状态模式(State)

描述:

+

策略模式(Strategy)

描述:

+

模板方法模式(Template Method)

描述:

+

访问者模式(Visitor)

描述:

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:JavaScript 设计模式 Design Pattern

+

文章作者:

+

发布时间:2020年01月13日 - 12:22

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/13/Javascript-Design-Pattern/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/13/do-it-yourselfery-promise/index.html b/en/2020/01/13/do-it-yourselfery-promise/index.html new file mode 100644 index 0000000000..069ff0b2ed --- /dev/null +++ b/en/2020/01/13/do-it-yourselfery-promise/index.html @@ -0,0 +1,623 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- promise | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- promise +

+ + +
+ + + + +
+ + +

前言

面试中时常会出现需要手动实现某个功能的要求,所以我准备整理一个自己动手实现系列文章,这一次呢就是自己动手实现 promise

+

实现 promise 思路

基础步骤

+
    +
  1. 设定三个状态 PENDINGFULFILLEDREJECTED ,只能由 PENDING 改变为 FULFILLEDREJECTED ,并且只能改变一次
  2. +
  3. MyPromise 接收一个函数 executorexecutor 有两个参数 resolve 方法和 reject 方法
  4. +
  5. resolvePENDING 改变为 FULFILLED
  6. +
  7. rejectPENDING 改变为 FULFILLED
  8. +
  9. promise 变为 FULFILLED 状态后具有一个唯一的 value
  10. +
  11. promise 变为 REJECTED 状态后具有一个唯一的 reason
  12. +
+

** then 方法**

+
    +
  1. then 方法接受两个参数 onFulfilledonRejected ,它们分别在状态由 PENDING 改变为 FULFILLEDREJECTED 后调用
  2. +
  3. 一个 promise 可绑定多个 then 方法
  4. +
  5. then 方法可以同步调用也可以异步调用
  6. +
  7. 同步调用:状态已经改变,直接调用 onFulfilled 方法
  8. +
  9. 异步调用:状态还是 PENDING ,将 onFulfilledonRejected 分别加入两个函数数组 onFulfilledCallbacksonRejectedCallbacks ,当异步调用 resolvereject 时,将两个数组中绑定的事件循环执行。
  10. +
+

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// js代码

// 定义状态常量
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

/**
* 定义MyPromise模拟Promise
* @param {func} executor 接收函数
*/
function MyPromise(executor) {
this.state = PENDING; // 默认状态为 pending
this.value = null;
this.reason = null;

// 定义成功失败的函数数组
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];

// 定义成功回调
const resolve = (value) => {
if (this.state === PENDING) {
this.state = FULFILLED;
this.value = value;

this.onFulfilledCallbacks.forEach(func => {
func();
});
}
}

// 定义失败回调
const reject = (reason) => {
if (this.state === PENDING) {
this.state = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach(func => {
func();
});
}
}

try {
executor(resolve, reject);
} catch (reason) {
reject(reason);
}
}

MyPromise.prototype.then = function (onFulfilled, onRejected) {
switch (this.state) {
case FULFILLED:
onFulfilled(this.value);
break;
case REJECTED:
onFulfilled(this.value);
break;
case PENDING:
this.onFulfilledCallbacks.push(() => {
onFulfilled(this.value);
})
this.onRejectedCallbacks.push(() => {
onRejected(this.reason);
})
break;
}
+

then方法异步调用

如下面的代码:输入顺序是:1、2、ConardLi

+
1
2
3
4
5
6
7
8
9
10
11
12
13
// js代码

console.log(1);

let promise = new Promise((resolve, reject) => {
resolve('ConardLi');
});

promise.then((value) => {
console.log(value);
});

console.log(2);
+

虽然 resolve 是同步执行的,我们必须保证 then 是异步调用的,我们用 setTimeout 来模拟异步调用(并不能实现微任务和宏任务的执行机制,只是保证异步调用)

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

// js代码
MyPromise.prototype.then = function (onFulfilled, onRejected) {
if (typeof onFulfilled != 'function') {
onFulfilled = function (value) {
return value;
}
}
if (typeof onRejected != 'function') {
onRejected = function (reason) {
throw reason;
}
}
switch (this.state) {
case FULFILLED:
setTimeout(() => {
onFulfilled(this.value);
}, 0);
break;
case REJECTED:
setTimeout(() => {
onRejected(this.reason);
}, 0);
break;
case PENDING:
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
onFulfilled(this.value);
}, 0);
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
onRejected(this.reason);
}, 0);
})
break;
}
}
+

then方法链式调用

保证链式调用,即 then 方法中要返回一个新的 promise ,并将 then 方法的返回值进行 resolve

+

注意:这种实现并不能保证 then 方法中返回一个新的 promise ,只能保证链式调用

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// js代码
MyPromise.prototype.then = function (onFulfilled, onRejected) {
if (typeof onFulfilled != 'function') {
onFulfilled = function (value) {
return value;
}
}
if (typeof onRejected != 'function') {
onRejected = function (reason) {
throw reason;
}
}

// 创建一个新的MyPromise对象
const promise2 = new MyPromise((resolve, reject) => {
switch (this.state) {
case FULFILLED:
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
break;
case REJECTED:
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
break;
case PENDING:
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
})
break;
}
})
return promise2;
}
+

catch方法

若上面没有定义 reject 方法,所有的异常会走向 catch 方法:

+
1
2
3
4
5
// js代码

MyPromise.prototype.catch = function(onRejected) {
return this.then(null, onRejected);
};
+

finally方法

不管是 resolve 还是 reject 都会调用 finally

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

MyPromise.prototype.finally = function(fn) {
return this.then(value => {
fn();
return value;
}, reason => {
fn();
throw reason;
});
};
+

Promise.resolve

Promise.resolve 用来生成一个直接处于 FULFILLED 状态的 Promise

+
1
2
3
4
5
6
// js代码
MyPromise.reject = function(value) {
return new MyPromise((resolve, reject) => {
resolve(value);
});
};
+

Promise.reject

Promise.reject 用来生成一个直接处于 REJECTED 状态的 Promise

+
1
2
3
4
5
6
// js代码
MyPromise.reject = function(reason) {
return new MyPromise((resolve, reject) => {
reject(reason);
});
};
+

all方法

接受一个 promise 数组,当所有 promise 状态 resolve 后,执行 resolve

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// js代码
MyPromise.all = function (promises) {
return new Promise((resolve, reject) => {
if (promises.length === 0) {
resolve([]);
} else {
let result = [];
let index = 0;
for (let i = 0; i < promises.length; i++) {
promises[i].then(data => {
result[i] = data;
if (++index === promises.length) {
resolve(result);
}
}, err => {
reject(err);
return;
});
}
}
});
}
+

race方法

接受一个 promise 数组,当有一个 promise 状态 resolve 后,执行 resolve

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码
MyPromise.race = function (promises) {
return new Promise((resolve, reject) => {
if (promises.length === 0) {
resolve();
} else {
let index = 0;
for (let i = 0; i < promises.length; i++) {
promises[i].then(data => {
resolve(data);
}, err => {
reject(err);
return;
});
}
}
});
}
+ +

最后

如此一个自定义的 promise 就实现了,怎么样学回来吗?

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- promise

+

文章作者:

+

发布时间:2020年01月13日 - 00:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/13/do-it-yourselfery-promise/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/13/hexo-search/index.html b/en/2020/01/13/hexo-search/index.html new file mode 100644 index 0000000000..05b64c876c --- /dev/null +++ b/en/2020/01/13/hexo-search/index.html @@ -0,0 +1,596 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 给我的Hexo博客添加文章内容搜索功能 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 给我的Hexo博客添加文章内容搜索功能 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

今年我下定决心一定要好好地写博客,完善博客的内容,所以最近把原来记在各种笔记中的文章内容都一一转移到了博客,内容一多想找一篇文章时就变得很麻烦,我得去归档中或者分类中一篇篇的找,所以我觉得是时候该给我的博客添加搜索的功能了,我看了博客的config文件,其中并没有搜索相关的配置,我又看了主题的配置文件,这下让我找到了,有个local_search的选项,于是我就开始了博客搜索的研究。

+ +

主题里的搜索配置

这段代码是这样的,实际上我只需要把 enablefalse 变成 true 就好了

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# Local Search
# Dependencies: https://github.com/theme-next/hexo-generator-searchdb
local_search:
enable: true
# If auto, trigger search by changing input.
# If manual, trigger search by pressing enter key or search button.
trigger: auto
# Show top n results per article, show all results by setting to -1
top_n_per_article: 1
# Unescape html strings to the readable one.
unescape: false
# Preload the search data when the page loads.
preload: false
+

然后我又看了一下上面提供的依赖地址,这里还需要做两步,一个是安装搜索的依赖

+
1
npm install hexo-generator-searchdb
+

接着就是在博客系统的配置最下方加入下面这段话

+
1
2
3
4
5
search:
path: search.xml
field: post
content: true
format: html
+

到这里如果没什么问题,那么搜索功能就加上了,怎么样简单吧。如果你遇到什么问题,可以到上面的地址看一下,上面有详细的说明,我这里就不贴代码了。

+

最后

希望大家都能丰富自己的技术博客,拥有属于自己的一片技术天地。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:给我的Hexo博客添加文章内容搜索功能

+

文章作者:

+

发布时间:2020年01月13日 - 14:50

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/13/hexo-search/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/22/Es6-arrowFunc-arguments/index.html b/en/2020/01/22/Es6-arrowFunc-arguments/index.html new file mode 100644 index 0000000000..b6550d9994 --- /dev/null +++ b/en/2020/01/22/Es6-arrowFunc-arguments/index.html @@ -0,0 +1,583 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + es6中箭头函数没了arguments怎么办? | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ es6中箭头函数没了arguments怎么办? +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

es6中,arguments被取消掉了,但是函数还是会有参数不确定的时候,那么我们该怎么办呢,接下来将介绍一下如何在es6中完成arguments一样的效果。

+ + +

剩余运算符

1
2
3
4
5
6
7
8
// js代码

let func = (...rest) => {
console.log(rest)
//[1,2,3]
}

func(1,2,3)
+ +

看上面的代码,有的朋友会问,这...的操作不应该是展开运算符么?是的,扩展运算符与剩余操作符都是以三点开头的操作符,二者长的很像,只是在用法上有些差别。它们已经被 ES6 数组支持,能解决很多之前 arguments 解决起来很麻烦的问题。

+

简单来说剩余运算是在参数上使用的。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:es6中箭头函数没了arguments怎么办?

+

文章作者:

+

发布时间:2020年01月22日 - 06:50

+

最后更新:2020年01月22日 - 07:34

+

原始链接:https://blog.lifesli.com/2020/01/22/Es6-arrowFunc-arguments/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/22/FE-guide-safe/index.html b/en/2020/01/22/FE-guide-safe/index.html new file mode 100644 index 0000000000..713ad5d5c8 --- /dev/null +++ b/en/2020/01/22/FE-guide-safe/index.html @@ -0,0 +1,635 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 网络安全 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 网络安全 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

网络安全是前端不可忽略的一个部分,可惜的是之前我都忽略了,最近发现网络安全已经变得日益重要,所以特别整理一篇文章说说网络安全相关的内容。

+ +

XSS(Cross-site scripting)跨站脚本攻击

跨站脚本攻击是一种网站应用程序的安全漏洞攻击,是代码注入的一种。它允许恶意用户将代码注入到网页上,其他用户在观看网页时就会受到影响。这类攻击通常包含了HTML以及用户端脚本语言。

+

XSS攻击通常指的是通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。这些恶意网页程序通常是JavaScript,但实际上也可以包括Java,VBScript,ActiveX,Flash或者甚至是普通的HTML。攻击成功后,攻击者可能得到更高的权限(如执行一些操作)、私密网页内容、会话和cookie等各种内容。

+

XSS攻击分成两类:

+
    +
  • 来自内部的攻击
      +
    • 主要指的是利用程序自身的漏洞,构造跨站语句
    • +
    +
  • +
  • 来自外部的攻击
      +
    • 主要指自己构造XSS跨站漏洞网页或者寻找非目标机以外的有跨站漏洞的网页。如当我们要渗透一个站点,我们自己构造一个有跨站漏洞的网页,然后构造跨站语句,通过结合其他技术,如社会工程学等,欺骗目标服务器的管理员打开。
    • +
    +
  • +
+

XSS分为:存储型反射型

+
    +
  • 存储型XSS:存储型XSS,持久化,代码是存储在服务其中的,如在个人信息或发表文章等地方,加入代码,如果没有过滤或过滤不严,那么这些代码将储存到服务器中,用户访问该页面的时候触发代码执行。这种XSS比较危险,容易造成蠕虫,盗窃cookie(虽然还有种DOM型XSS,但是也还是包括在存储型XSS内)。
  • +
  • 反射型XSS:非持久化,需要欺骗用户自己去点击链接才能触发XSS代码(服务器中没有这样的页面和内容),一般容易出现在搜索页面。
  • +
+

攻击手段和目的

攻击者使被攻击者在浏览器中执行脚本后,如果需要收集来自被攻击者的数据(如cookie或其他敏感信息),可以自行架设一个网站,让被攻击者通过JavaScript等方式把收集好的数据作为参数提交,随后以数据库等形式记录在攻击者自己的服务器上。

+

常用的XSS攻击手段和目的有:

+
    +
  • 盗用cookie,获取敏感信息。
  • +
  • 利用植入Flash,通过crossdomain权限设置进一步获取更高权限;或者利用Java等得到类似的操作。
  • +
  • 利用iframe、frame、XMLHttpRequest或上述Flash等方式,以(被攻击)用户的身份执行一些管理动作,或执行一些一般的如发微博、加好友、发私信等操作。
  • +
  • 利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动。
  • +
  • 在访问量极大的一些页面上的XSS可以攻击一些小型网站,实现DDoS攻击的效果。
  • +
+

漏洞的防御和利用

过滤特殊字符

避免XSS的方法之一主要是将用户所提供的内容进行过滤,许多语言都有提供对HTML的过滤:

+
    +
  • PHP的htmlentities()或是htmlspecialchars()。
  • +
  • Python的cgi.escape()。
  • +
  • ASP的Server.HTMLEncode()。
  • +
  • ASP.NET的Server.HtmlEncode()或功能更强的Microsoft Anti-Cross Site Scripting Library
  • +
  • Java的xssprotect (Open Source Library)。
  • +
  • NodeJS的node-validator。
  • +
+

使用HTTP头指定类型

很多时候可以使用HTTP头指定内容的类型,使得输出的内容避免被作为HTML解析。如在PHP语言中使用以下代码:
<?php header('Content-Type: text/javascript; charset=utf-8'); ?>
即可强行指定输出内容为文本/JavaScript脚本(顺便指定了内容编码),而非可以引发攻击的HTML。

+

用户方面

包括Internet Explorer、Mozilla Firefox在内的大多数浏览器皆有关闭JavaScript的选项,但关闭功能并非是最好的方法,因为许多网站都需要使用JavaScript语言才能正常运作。通常来说,一个经常有安全更新推出的浏览器,在使用上会比很久都没有更新的浏览器更为安全。

+

CRSF(Cross-site request forgery)跨站请求伪造

跨站请求伪造是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

+
    +
  • XSS 利用的是用户对指定网站的信任
  • +
  • CSRF 利用的是网站对用户网页浏览器的信任
  • +
+

跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。

+

例子

例子
假如一家银行用以运行转账操作的URL地址如下:
http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName
那么,一个恶意攻击者可以在另一个网站上放置如下代码:
<img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">
如果有账户名为Alice的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失1000资金。
这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险
透过例子能够看出,攻击者并不能通过CSRF攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义运行操作。

+

防御措施

检查Referer(参照)字段

HTTP头中有一个Referer字段,这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer字段应和请求的地址位于同一域名下。以上文银行操作为例,Referer字段地址通常应该是转账按钮所在的网页地址,应该也位于www.examplebank.com之下。而如果是CSRF攻击传来的请求,Referer字段会是包含恶意网址的地址,不会位于www.examplebank.com之下,这时候服务器就能识别出恶意的访问。
这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的Referer字段。虽然http协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其Referer字段的可能。

+

添加校验token

由于CSRF的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在cookie中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再运行CSRF攻击。这种数据通常是窗体中的一个数据项。服务器将其生成并附加在窗体中,其内容是一个伪随机数。当客户端通过窗体提交请求时,这个伪随机数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪随机数,而通过CSRF传来的欺骗性攻击中,攻击者无从事先得知这个伪随机数的值,服务端就会因为校验token的值为空或者错误,拒绝这个可疑请求。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 网络安全

+

文章作者:

+

发布时间:2020年01月22日 - 14:50

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/22/FE-guide-safe/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/01/22/Wepack-Tips/index.html b/en/2020/01/22/Wepack-Tips/index.html new file mode 100644 index 0000000000..edde2d436b --- /dev/null +++ b/en/2020/01/22/Wepack-Tips/index.html @@ -0,0 +1,622 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + webpack中loader和plugin之间的区别 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ webpack中loader和plugin之间的区别 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在学习Webpack相关的知识的时候对loader和plugin有点迷惑,两个特性都是用来做打包相关处理,那么他们有什么区别呢,为了弄清楚他们有什么区别,我开始了疯狂的查找资料,虽说每篇文章都说了一些自己的认识,但是并没有一个特别标准或者说容易理解的答案,我且先将它们记录下来,以便日后回顾之时可以有一些感触。那么接下来我将开始webpack的解迷之旅。

+ + +

背景知识

在研究loader和plugin之前区别之前,我们先来看看一个webpack配置的常见结构

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// js代码

const webpack = require("webpack");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
// 入口文件
entry: {
app: path.join(__dirname, "../src/js/index.js")
},
// 输出文件
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "dist"),
publicPath: "/"
},
// loader配置
module: {
rules: [
{
test: /\.scss/,
use: [
"style-loader",
"css-loader"
]
}
......
]
},
// plugins配置
plugins: [
// 重新创建html文件
new HtmlWebpackPlugin({
title: "首页",
filename: "index.html",
template: path.resolve(__dirname, "../src/index.html")
})
......
]
}
+

webpack的打包原理

+
    +
  • 识别入口文件
  • +
  • 通过逐层识别模块依赖(Commonjs、amd或者es6的import,webpack都会对其进行分析,来获取代码的依赖)
  • +
  • webpack做的就是分析代码,转换代码,编译代码,输出代码
  • +
  • 最终形成打包后的代码
  • +
+

什么是loader

我们可以看到loader实际上是在module的rules下,用对象的方式表示了需要处理的文件类型,和需要用哪些loader做处理

+
+

loader是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中。

+
+
    +
  • 处理一个文件可以使用多个loader,loader的执行顺序和配置中的顺序是相反的,即最后一个loader最先执行,第一个loader最后执行
  • +
  • 第一个执行的loader接收源文件内容作为参数,其它loader接收前一个执行的loader的返回值作为参数,最后执行的loader会返回此模块的JavaScript源码
  • +
+

什么是plugin

+

在webpack运行的生命周期中会广播出许多事件,plugin可以监听这些事件,在合适的时机通过webpack提供的API改变输出结果。

+
+

loader和plugin的区别

对于loader,它是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.scss转换为A.css,单纯的文件转换过程

+

plugin是一个扩展器,它丰富了webpack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务

+

下面我们来看一个例子:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// js代码

class MyPlugin{
constructor(options){
console.log("MyPlugin constructor:", options);
}
apply(compiler){
compiler.plugin("compilation", compilation => {
console.log("MyPlugin");
});
}
}
module.exports = MyPlugin;

// webpack.config.js配置:
module.exports = {
...
plugins: [
new MyPlugin({param: "my plugin"})
]
}
+ +

使用该plugin后,执行的顺序:

+
    +
  1. webpack启动后,在读取配置的过程中会执行new MyPlugin(options)初始化一个MyPlugin获取其实例
  2. +
  3. 在初始化compiler对象后,就会通过compiler.plugin(事件名称,回调函数)监听到webpack广播出来的事件
  4. +
  5. 并且可以通过compiler对象去操作webpack
  6. +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:webpack中loader和plugin之间的区别

+

文章作者:

+

发布时间:2020年01月22日 - 10:38

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/22/Wepack-Tips/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/02/11/SSR/index.html b/en/2020/02/11/SSR/index.html new file mode 100644 index 0000000000..ee25ed61f2 --- /dev/null +++ b/en/2020/02/11/SSR/index.html @@ -0,0 +1,609 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SSR | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ SSR +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

什么是SSR

传统浏览器的vue纯浏览器渲染

浏览器请求服务器,服务器返回静态资源 ,客户端负责渲染js

+

ssr

浏览器请求服务器,node服务端渲染js后,返回对应的页面返回给浏览器

+

SSR需要那些东西

手写SSR

特性:

+
    +
  • 每一次访问必须新建一个vue实例
  • +
  • 只会触发组件的 beforeCreate和created钩子
  • +
+

核心库

+
    +
  • vue
  • +
  • vue-server-renderer

    vue + next

    +

    作者:李旭光
    引用请标明出处

    +
    +
  • +
+

前言

+
+

作者:李旭光
引用请标明出处

+
+

前言

+
+

作者:李旭光
引用请标明出处

+
+

前言

+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:SSR

+

文章作者:

+

发布时间:2020年02月11日 - 16:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/02/11/SSR/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/02/11/restful-architecture/index.html b/en/2020/02/11/restful-architecture/index.html new file mode 100644 index 0000000000..03edf0b217 --- /dev/null +++ b/en/2020/02/11/restful-architecture/index.html @@ -0,0 +1,602 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NodeJS 下 RESTful 架构的最佳实践(课堂笔记) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ NodeJS 下 RESTful 架构的最佳实践(课堂笔记) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

什么是 RESTful

+

REST 指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是 RESTful。

+
+

实际上就是 url 风格/ 路由风格 , 给出一种写路由的原则 — 面向资源
tip:静态路由利于seo优化 api/news/1,动态路由不利于seo优化 api/news?id=1

+

原来的风格
| 路由 | 功能 | 描述 |
| —- | —- |——|
| http://127.0.0.1/user/query/1 | 查询 | 根据ID查询用户信息 |
| http://127.0.0.1/user/save | 保存 | 注册用户 |
| http://127.0.0.1/user/update | 更新 | 修改用户 |
| http://127.0.0.1/user/delete/{id} | 删除 | 删除用户 |

+

RESTful — 面向资源:对于同一个资源都在同一个 URL 进行,通过判断 HTTP 请求的类型来决定做不同的事
/user 一个资源
| 路由 | 请求类型 |
| ———————– | ——– |
| http://127.0.0.1/user/1 | GET |
| http://127.0.0.1/user | POST |
| http://127.0.0.1/user | PUT |
| http://127.0.0.1/user | DELETE |

+

URL 描述资源 , HTTP 描述请求。(HTTP协议无语义无状态 —- 降低复杂度,业务无关 — 架构代码业务无关)

+

RESTful 采用的是顶层路由

+
+

顶层路由设计:不需要有物理文件映射路由

+
+
1
2
3
4
5
6
7
8
// express
// app.js
const express = require('express')
const app = express()
app.get('/case.avi',(req, res)=>{
res.send('hello world'); // 不需要对应物理文件
})
app.listen(3000)
+

原生接口

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// index.js
const http = require('http');
const mysql = require('mysql'); // mysql
const co = require('co-mysql') // 异步同步化
const md5 = require('md5-node') // md5加密

// 连接数据库
let db = mysql.createPool({ // 连接池自己管理 不用关闭
host:'localhost',
user:'root',
password:'root',
database:'user'
})
let conn = co(db)

const app = http.createServer(async (req,res)=>{
if(req.method === 'POST'){
if(req.url === '/user'){
// res.end(JSON.stringify({'message':'对user发起post请求'}))
req.on('data', async (data)=>{
arr.push(data)
})
req.on('end',async ()=>{
let buffer = Buffer.concat(arr);
// json对象
let {username,pasword} = JSON.parse(buffer.toString())
// console.log(username,pasword)
let sql = `selct user from admin where user = ${username}`
let data = await conn.query(sql);
// console.log(data)
if(data.length >=1 ){
res.end(JSON.stringify({
'status':200,
'message':'用户名已经注册'
}))
}else{
// 写入数据库
password = md5(password);
let sql = `INSERT INTO admin (user,password) VALUES ('${username}','${password}')`
await conn.query(sql);
res.end(JSON.stringify({
'status':200,
'message':'注册成功'
}))
}
})
}
}if(req.method === 'GET'){
if(req.url === '/user'){
// res.end(JSON.stringify({'message':'对user发起get请求'}))
let sql = `SELECT id,user,password FROM admin`
let data = await conn.query(sql);
res.end(JSON.stringify(data))
}
}
}).listen(3000)

// .http 文件
@url = http://localhost:3000
@type = Content-Type: applications

GET {{url}}/user HTTP/1.1

POST {{url}}/user HTTP/1.1
{{type}}

{
username:'admin',
password:123456
}
+

使用express实现(express — generater yard ,koa — async await)

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

const express = require('express')
const app = express()
const mysql = require('mysql'); // mysql
const co = require('co-mysql') // 异步同步化
const md5 = require('md5-node') // md5加密
const bodyparse = require('body-parse')
// 连接数据库
let db = mysql.createPool({
host:'localhost',
user:'root',
password:'root',
database:'user'
})
let conn = co(db)

app.use(bodyparse.urlencoded({
extended:true // 返回对象是兼职对,false - string/array true - any
}))
app.use(bodyparse.json())

app.post('/user',async (req.res)=>{
let { username , password} = req.body
// console.log(username,pasword)
let sql = `selct user from admin where user = ${username}`
let data = await conn.query(sql);
// console.log(data)
if(data.length >=1 ){
res.send(JSON.stringify({
'status':200,
'message':'用户名已经注册'
}))
}else{
// 写入数据库
password = md5(password);
let sql = `INSERT INTO admin (user,password) VALUES ('${username}','${password}')`
await conn.query(sql);
res.send(JSON.stringify({
'status':200,
'message':'注册成功'
}))
}
})

app.get('/user/:id',(req,res)=>{
res.send(req.params.id)

let sql = `SELECT id,user,password FROM admin WHERE id = ${req.params.id}`
let data = await conn.query(sql);
res.end(JSON.stringify(data))
})

app.listen(3000)
+ +

使用koa实现

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53


// config.js
module.exports = {
host:'localhost',
user:'root',
password:'root',
database:'user'
}

// libs/database.js
const config = require('../config')
const mysql = require('mysql'); // mysql
const co = require('co-mysql') // 异步同步化
// 连接数据库
let db = mysql.createPool({
host:config.host,
user:config.user,
password:config.password,
database:config.database
})
let conn = co(db)

// router/user/index.js
const Router = require('koa-router')
const md5 = require('md5-node') // md5加密
const router = new Router();

router.get('/user',async ctx=>{
ctx.body = '主页'
})
router.post('/user',async ctx=>{
let {username,password} = ctx.request.body
// console.log(username,password)
ctx.body = {
username,password
}
})
module.exports = router.routes();

// app.js
const koa = require('koa')
const Router = require('koa-router')
const body = require('koa-bodyparse')
const config = require('config')
const app = new Koa()
const router = new Router()
app.context.db = require('./libs/database')
app.context.config = config
app.use(body())
router.use('/api',require('./router/user'))
app.use(router.routes())
app.listen(3000)
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:NodeJS 下 RESTful 架构的最佳实践(课堂笔记)

+

文章作者:

+

发布时间:2020年02月11日 - 16:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/02/11/restful-architecture/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/02/12/print-table-problem/index.html b/en/2020/02/12/print-table-problem/index.html new file mode 100644 index 0000000000..63d8a2a4ec --- /dev/null +++ b/en/2020/02/12/print-table-problem/index.html @@ -0,0 +1,591 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 由打印引起的一点小问题,写table时别忘了写thead和tbody | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 由打印引起的一点小问题,写table时别忘了写thead和tbody +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

这两天复工,公司一个小伙伴在群里问了一个问题

+
+

如何在打印表格的时候,让超过一页的表格分割线不被截断

+
+

说着他有贴上来了一张图,一张因为跨页被截断了的表格,说实话打印的场景接触的不多,打印表格一般都是制式的样式所以也不会出现这问题,一时间也没有头绪,想着这个是不是没什么办法解决,隔天他发来一个消息,说是解决了,给表格里加上了 tr td 就好了,接着他又发来一张图,还真是,不过上下边距发生了一些变化,我问他是不是自己改了边距所以好了,他说没改,然后我问他那他是怎么解决的,他说因为实在找不到问题解决方案和产生原因,于是就把代码重构了一下,按照标准写法写了一下,结果就好了。。。怎么说呢,也是神奇,我也到网上去查了查相关的材料,并没有特别对的上的原因,凭感觉来说,应该是thead和tbody上有默认的css样式解决了上面的问题,暂且记下这个问题,避免以后遇到又忘了是怎么回事。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:由打印引起的一点小问题,写table时别忘了写thead和tbody

+

文章作者:

+

发布时间:2020年02月12日 - 08:30

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/02/12/print-table-problem/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/02/12/vue-proxyTable-problem/index.html b/en/2020/02/12/vue-proxyTable-problem/index.html new file mode 100644 index 0000000000..e67c1d6cf9 --- /dev/null +++ b/en/2020/02/12/vue-proxyTable-problem/index.html @@ -0,0 +1,586 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Could not proxy request XXX from localhost:8080 to localhost:8081 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Could not proxy request XXX from localhost:8080 to localhost:8081 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

最近又有小伙伴问我,为啥我在写vue项目做代理转发时候报找不到代理服务的问题?
我看了一下他发给我的配置截图,仅一眼我就发现了问题所在,因为这个坑我曾经也踩过,而且好多小伙伴也都踩过,在写转发服务器的时候没写协议头,也就是在 target 那没写 http:// 这个部分,添加上以后重启一下,这个问题就解决了,小伙伴说就这个问题他解决了一天也没解决了,而且我说完了以后他想起来以前好像也遇到过这个问题,所以说在平时工作中遇到问题要常做笔记才对,所以为了避免以后自己也犯这种错误,暂且留一篇文章记录一下这个问题。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Could not proxy request XXX from localhost:8080 to localhost:8081

+

文章作者:

+

发布时间:2020年02月12日 - 09:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/02/12/vue-proxyTable-problem/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/02/19/develop-custom-cli-tools-using-node/index.html b/en/2020/02/19/develop-custom-cli-tools-using-node/index.html new file mode 100644 index 0000000000..a16444ae59 --- /dev/null +++ b/en/2020/02/19/develop-custom-cli-tools-using-node/index.html @@ -0,0 +1,651 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 使用node开发自定义cli工具 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 使用node开发自定义cli工具 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

这篇文章想写一写前端工程化相关的内容,原因一呢是是结合公司业务给自己定的业绩指标包含这些内容,另外一个原因是因为听了网易前端唐磊说过的一句话,前端leader干什么,很重要的工作就是前端工程化,高级以上不懂前端工程化很难。

+

今天听了公开课讲到了用node写一个自己的cli,说实话正是工作所需,课程讲的有点快,没有从头跟下来,自己上完了课又上网上找了些资料,终于把步骤弄明白了,下面就把如何自定义一个cli来帮助提升开发效率。同时也完成了一个业务指标,心里美滋滋。

+

准备

如果你看到这篇文章,也想跟着我的步骤写一下这个自定义cli,那么如下的知识还是有一些为好。

+
    +
  • 基础的nodejs相关知识
  • +
+

没错就只需要会一些node的基础知识就可以了,接下来正式开始

+

初始化

首先,我们要给我们的命令行工具起个名字,比如我们熟悉的 vue 命令行就是 vue-cli ,因为我写完了要给公司用,所以我起的名字是 tfd-cli ,你们喜欢叫什么你们随意

+

首先创建一个名字为 tfd-cli 的文件目录,然后在目录下执行 node 工程的初始化命令

+
1
npm init -y // 初始化项目 -y 默认全部yes的参数
+

命令执行完成后 tfd-cli 目录下会生成一个我们熟悉的 package.json 文件,我们打开 package.json 文件,增加一段代码,如下

+
1
2
3
4
// 追加的代码
"bin": {
"tfd": "index.js"
}
+

追加完成后,package.json 文件中的内容是这样的

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"name": "tfd-cli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"bin": {
"tfd": "index.js"
}
}
+

也就是说当我们执行 tfd 命令时它就会找同级目录下的 index.js 文件执行其中代码,我们目前还没有 index.js,文件,那么我们手动创建一个 index.js 的文件,然后在里面写下如下代码

+
1
2
3
4
#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的

console.log('hello world');
+

执行完这些还不够,因为我们是开发环境所以还有一步操作是要将 tfd 命令告知 npm,该如何处理,所以我们要执行如下命令

+
1
npm link
+

这里如果执行不成功,请用管理员权限执行,执行完成后我们会得到一个 package-lock.json 的文件,内容如下

+
1
2
3
4
5
{
"name": "tfd-cli",
"version": "1.0.0",
"lockfileVersion": 1
}
+

如此一来,我们就可以在任何目录下执行命令行 tfd 就会执行 index.js 文件了,这里我们会在控制面板中输出 hello world ,怎么样是不是小有成就感,我们接着往下来。

+

创建指令

我们写个命令行工具肯定不是为了输出个 hello world 这么简单,而是希望通过用户输入内容后根据条件输出一些东西,那么让我们想想一个命令行工具应该具备哪些指令呢?

+
    +
  • 查看命令行工具版本
  • +
  • 查看帮助文档
  • +
  • 初始化模板
  • +
  • 列出模板类型
  • +
  • 等等
  • +
+

那么用指令该如何描述呢

+
1
2
3
4
tfd -V|--version //查看工具版本号
tfd -h|--help //查看使用帮助
tfd init <template-name> <project-name> //基于指定模板进行项目初始化
tfd list //列出所有可用模板
+

为了执行命令,这里我们要引入一个 node 包叫做 commander,因此我们要先执行一下 install 命令

+
1
npm install commander
+

接着我们就可以在 index.js 里面写指令了。

+
1
2
3
4
5
6
7
8
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0
+

到这一步我们在控制台敲一下 tfd -V 你会发现什么也没输出,这是因为到这一步我们还无法解析 tfd -V 操作,在这之前我们要知道一个命令

+
1
process.argv // 使用process.argv获取命令行参数
+

当我们把这句话加到 console.log 中在 index.js 中输出时你会看到控制台打印出

+
1
2
3
4
5
6
7
8
9
10
11
12
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0
console.log(process.argv)

// 控制台
[ '/usr/local/bin/node', '/usr/local/bin/tfd', '-V' ]
+

接下来我们要让commander获取参数执行命令

+
1
2
3
4
5
6
7
8
9
10
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0
// console.log(process.argv)
cmd.parse(process.argv);
+

这个时候我们再在控制台输入 tfd -V 时,我们就会发现,控制台输出了 0.1.0,这样我们就完成了查版本的指令,接下来我们完成其他的指令

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0

// tfd init <template> <project>
cmd
.command('init <template> <project>') // 参数
.description('初始化项目模板')
.action((templateName, projectName) => {
console.log(templateName, projectName);
})

// tfd list
cmd
.command('list')
.description('查看所有可用模板')
.action(() => {
console.log(`
a a模板
b b模板
c c模板
`)
})
// console.log(process.argv)
cmd.parse(process.argv);
+

这个时候我们在控制台上输入 tfd -h 的时候,控制台会输出如下代码

+
1
2
3
4
5
6
7
8
9
Usage: tfd [options] [command]

Options:
-V, --version output the version number
-h, --help output usage information

Commands:
init <template> <project> 初始化项目模板
list 查看所有可用模板
+

这样我们就实现了自定义命令,我们执行一下 tfd init template1 project1,我们可以看到,控制台中输出了 template1 project1,也就是说command命令后尖括号中指向了action中的参数,我们就可以通过判断action中的参数做具体的操作了。

+

通常模板可以选择从本地拷贝一份,但更常用的是从线上拷贝一份,比如从github中,接下来我们就看看如何从github中拷贝一个模板作为项目的初始化工程

+

github上创建模板仓库

首先我们要在github上创建两个仓库 tpl-1 tpl-2,这里为了从github中下载仓库我们需要一个node包支持,让我们请出download-git-repo,别忘了执行安装命令

+
1
npm install download-git-repo
+

安装完依赖之后让我们再去index.js填点东西,首先引入下载依赖,然后是创建下载的 template 抽象对象

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');
const download = require('download-git-repo');
// 可用模板
const templates = {
'tpl-1': {
url: 'https://github.com/lixuguang/tpl-1',
downloadUrl: 'https://github.com:lixuguang/tpl-1#master',
description: 'tfd-cli脚手架测试模板1'
},
'tpl-2': {
url: 'https://github.com/lixuguang/tpl-2',
downloadUrl: 'https://github.com:lixuguang/tpl-2#master',
description: 'tfd-cli脚手架测试模板2'
}
}

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0

// tfd init <template> <project>
cmd
.command('init <template> <project>') // 参数
.description('初始化项目模板')
.action((templateName, projectName) => {
// console.log(templateName, projectName);
let {downloadUrl} = templates[templateName];
// 第一个参数是github仓库地址,第二个参数是创建的项目目录名,第三个参数是clone
download(downloadUrl, projectName, {clone: true}, err => {
if(err){
console.log('模板下载失败');
}else{
console.log('模板下载成功');
}
})
})

// tfd list
cmd
.command('list')
.description('查看所有可用模板')
.action(() => {
// console.log(`
// a a模板
// b b模板
// c c模板
// `)
// 通过获取templates里的key可以获取到模板名称
const templateName = Object.keys(templates)
console.log(templateName)
})
// console.log(process.argv)
cmd.parse(process.argv);
+

这样当我们执行 tfd list 就可以看到有哪些模板了,然后执行tfd init tpl-1 newproject 就可以依据 tpl-1 模板创建出 newproject 工程,这个过程实际上就是从github仓库克隆一份tpl-1作为模板创建工程newproject

+

这里需要注意的是download地址跟github仓库地址有点出入,比如github仓库地址是https://github.com/xxx/xxx而下载地址是https://github.com:xxx/xxx

+

虽然这样执行完成后就完成了基本的cli的雏形,但是毕竟不灵活,我们在使用vue-cli时,它的创建过程是问答式和选择式的,另外每个过程都会有进度显示什么的,那么要怎么添加这些功能呢,我们接着往下做。

+

进阶增加功能

使用inquirer进行命令行答询

inquirer 是一个进行命令行答询的库,通过它我们就可以创建问答式的内容,首先还是安装依赖

+
1
npm install inquirer
+

使用handlebars修改package.json

我们都知道在使用vue-cli的初始化命令后,会在项目目录下生成一个package.json文件,它就像是这个项目的基因序列一样,影响着项目的整个结构。模板是固定的,那要修改其中的package.json符合自己项目的需要,就要用到handlebars这个库来改写package.json文件,老规矩先安装它

+
1
npm install handlebars
+

使用ora在命令行中显示加载状态

我们在装任何依赖时都会有进度条显示进度,如果没有进度条又没有任何响应,会让用户迷茫,为了友好,我们就要加进度条,这里我们需要引入ora这个库来完成进度显示,我们接着安装。

+
1
npm install ora
+

使用chalk和log-symbols增加命令行输出样式

为了让命令行有红红绿绿的效果以及符号效果,我们需要使用chalklog-symbols来丰富样式,少废话,接着装

+
1
npm install chalk log-symbols
+ +

集大成

终于安装完一堆的依赖,别忘了在index.js中引入,让我们看看具体如何使用这些库吧。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');
const download = require('download-git-repo');

const iq = require('inquirer'); // 命令行答询
const hb = require('handlebars'); // 修改package.json文件
const ora = require('ora'); // 命令行中加载状态标识
const chalk = require('chalk'); // 命令行输出字符颜色
const ls = require('log-symbols'); // 命令行输出符号
const fs = require('fs'); // node fs原生模块

// 可用模板
const templates = {
'tpl-1': {
url: 'https://github.com/lixuguang/tpl-1',
downloadUrl: 'https://github.com:lixuguang/tpl-1#master',
description: 'tfd-cli脚手架测试模板1'
},
'tpl-2': {
url: 'https://github.com/lixuguang/tpl-2',
downloadUrl: 'https://github.com:lixuguang/tpl-2#master',
description: 'tfd-cli脚手架测试模板2'
}
}

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0

// tfd init <template> <project>
cmd
.command('init <template> <project>') // 参数
.description('初始化项目模板')
.action((templateName, projectName) => {
// console.log(templateName, projectName);
let {downloadUrl} = templates[templateName];
//下载github项目,下载墙loading提示
const loading = ora('模板下载中...').start();
// 第一个参数是github仓库地址,第二个参数是创建的项目目录名,第三个参数是clone
download(downloadUrl, projectName, {clone: true}, err => {
if(err){
// console.log('模板下载失败');
loading.fail('模板下载失败');
}else{
// console.log('模板下载成功');
spinner.succeed('模板下载成功');
// 命令行答询
iq.prompt([
{
type: 'input', // 类型 输入框
name: 'name', // 字段 key
message: '请输入项目名称', // 描述
default: projectName // 默认值
},
{
type: 'input',
name: 'description',
message: '请输入项目简介',
default: ''
},
{
type: 'input',
name: 'author',
message: '请输入作者名称',
default: ''
}
]).then(answers => { // answers 是一个对象,对象的 key 为上面答询的 name 的值,value 为 用户输入的值,如果未输入,就取默认值
// 根据命令行答询结果修改 package.json 文件
let packageContent = fs.readFileSync(`${projectName}/package.json`, 'utf8'); // 同步方式以 utf-8 字符集获得下载好的项目目录下的 package.json 文件
let packageResult = hb.compile(packageContent)(answers); // 将用户输入项与原内容混合获得新内容
fs.writeFileSync(`${projectName}/package.json`, packageResult); // 重新同步方式写入到 package.json 文件中
// 用chalk和log-symbols改变命令行输出样式
console.log(ls.success, chalk.green('模板项目文件准备成功!'));
})
}
})
})

// tfd list
cmd
.command('list')
.description('查看所有可用模板')
.action(() => {
// console.log(`
// a a模板
// b b模板
// c c模板
// `)
// 通过获取templates里的key可以获取到模板名称
const templateName = Object.keys(templates)
console.log(templateName)
})
// console.log(process.argv)
cmd.parse(process.argv);
+ +

到这里你自己的命令行工具就创建完成了,当然还可以继续丰富,比如加上选择条件等,这个就更复杂了,不在本文中展开,后续会继续改进这个命令行工具,当然如果要将自己的命令行工具给其他人用当然要先发布出去,就像我们要用vue-cli,首先要在npm上下载下来,同样的我们要想让别人用,就得先上传到npm上。

+

发布到npm上

首先你需要有一个npm的账号,没有赶快去注册一个,在控制台中输入npm login,它会让你依次输入 Username Password Email ,当你都按照要求输入完之后,成功的话你会获得如下信息Logged in as XXX on https://registry.npm.org/.,再接下来执行 npm publish 命令,你的自定义脚手架就会发布到npm上,供他人下载使用,怎么样,学会了么?

+

后记

这篇文章会有后续持续进化跟进,多篇文章连续,只要我的cli工具还在进化,文章就会继续,欢迎跟进。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:使用node开发自定义cli工具

+

文章作者:

+

发布时间:2020年02月19日 - 15:00

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/02/19/develop-custom-cli-tools-using-node/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/02/21/Implementation-of-the-vue-response-principle/index.html b/en/2020/02/21/Implementation-of-the-vue-response-principle/index.html new file mode 100644 index 0000000000..f08e407092 --- /dev/null +++ b/en/2020/02/21/Implementation-of-the-vue-response-principle/index.html @@ -0,0 +1,693 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Vue 响应式原理的实现(课程笔记) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Vue 响应式原理的实现(课程笔记) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近一直在追网课,说实话从业9年了,一直觉得前端发展非常快,而且一直充满着危机感,每天都要学习进步才有安稳的感觉,今天听了 vue 响应式原理实现的公开课,感觉还不错,做了如下笔记,帮助自己记忆,也希望能帮助大家。

+

Vue2 原理

什么是 defineProperty

defineProperty 其实是定义对象属性用的

+
+

defineProperty 其实并不是核心的为一个对象做数据双向绑定,而是去给对象做属性标签,只不过是属性里 get 和 set 实现了响应式。

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
属性名默认值
valueundefined
getundefined
setundefined
writablefalse
enumerablefalse
configurablefalse
+
1
2
3
4
5
6
7
8
9
10
11
12
13
var ob = {
a:1,
b:2
}
// 参数 1、对象 2、属性 3、配置
Object.defineProperty(ob,'a',{
writable:false,
enumerable:true,
configurable:true,
})
console.log(Object.getOwnPropertyDescriptor(ob,'a')) // 1
ob.a = 2
console.log(ob.a) // 1
+

下面我们实现一下双向绑定

+
1
2
3
4
5
6
7
8
9
10
11
12
13
Object.defineProperty(ob,'a',{
get:function(){
console.log('a is be get')
return 999;
},
set:function(){
console.log('a is be set')
return 999;
},
})

console.log(Object.getOwnPropertyDescriptor(ob,'a')) // 999

+

改造代码实现双向绑定(存取值)

+
1
2
3
4
5
6
7
8
9
10
11
12
var _val = obj.a; // 暂存
Object.defineProperty(ob,'a',{
get:function(){
console.log('a is be get')
return _val;
},
set:function(newVal){
_val = newVal // 新值替换旧值
console.log('a is be set')
return _val;
},
})
+

Vue 中从改变一个数据到发生改变的过程

    +
  1. 改变数据触发 Set
  2. +
  3. Set 部分触发 notify(更新)
      +
    1. Get 部分收集依赖
    2. +
    +
  4. +
  5. 更改对应的虚拟 Dom
  6. +
  7. 重新 Render
  8. +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// MyVue.js

// 简单版本 vue
function MyVue(){
this.$data = {
a: {
b:1
},
c:2
}
this.el = document.getElementById('app');
this.virtualDom = '';
this.observer(this.$data);
this.render();
}
vue.property.observer = function(obj){
var _val, self = this;
// var dep = new Dep() -> 源码中依赖收集对象
for(var key in obj){ // 属性有可能是对象,要递归绑定
_val = obj[key];
if(typeof _val === 'Object'){
this.observer(_val)
}else{
Object.defineProperty(this.$data,key,{ // 这里是实际绑定过程
get:function(){
// 依赖收集
// dep.depend(); -> vue 源码中收集依赖的方法
return _val
},
set:function(newVal){
_val = newVal
// dep.notify(); -> vue 源码中
self.render() // AST语法树
}
})
}
}
}
vue.property.render = function(){
this.virtualDom = 'i am '+this.$data.b;
this.el.innerHTML = this.virtualDom;
}
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// index.html

<!DOCTYPE html>
<html>
<head>
<title>自己实现Vue2数据双向绑定</title>
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src='myVue.js'></script>
<script type="text/javascript">
var mv = new MyVue();
setTimeout(function(){
console.log('changes');
console.log(mv.$data);
mv.$data.b = 222;
})
</script>
</body>
</html>
+
+

依赖收集:

+
    +
  1. 我们的data里面的数据并不是所有地方都用到
  2. +
  3. 如果我们直接更新整个视图,浪费资源
  4. +
  5. 先收集依赖改变的数据的组件,再更新依赖了数据的组件(Dep depend notify)
  6. +
+
+

格外注意的地方—数组怎么监听

definePropty 只能给对象进行 get set 绑定, 数组怎么办?

+

vue 中 使用了 装饰者模式

+
+

装饰者模式 Decorator模式(别名Wrapper):动态将职责附加到对象上,若要扩展功能,装饰者提供了比继承更具弹性的代替方案。

+
+
1
2
3
4
5
6
7
8
9
10
var arraypro = Array.property; // 创建一个数组的原型对象
var arrob = Object.create(arraypro); // 避免影响原型链
var arr = ['push','pop','shift'];
arr.forEach(function(method,index){
arrob[method]=function(){ // 装饰者模式
var ret = arraypro[method].apply(this,arguments)
dep.notify() // 扩展了功能
}
})

+

Vue3 实现双向绑定

Proxy 是什么?

+
+

Proxy 对象用于定义基本操作的自定义行为
和 definePropty 类似,功能几乎一样,只是用法上不同

+
    +
  1. 不会污染原对象
  2. +
  3. 直接给对象就可以了
  4. +
  5. 不需要借助外部变量 _val
  6. +
+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var ob = {
a:1,
b:2
}

var newOb = new Proxy(ob,{
get(target,key,receiver){ // target 对象,key 属性
console.log(target,key,receiver)
return target[key]
},
set(target,key,value,receiver){
return Reflect.set(target.key,value);
// 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
// return target[key] = value
}
})
+

为什么改用 Proxy

    +
  1. defineProperty 只能监听某个属性,不能全对象监听
  2. +
  3. 可以省去for in循环提升代码执行效率
  4. +
  5. 可以监听数组,不需要再为数组做特异性操作
  6. +
  7. 不污染原对象
  8. +
  9. 更优雅
  10. +
+

我们用 Proxy 实现一下 observe 方法

+
1
2
3
4
5
6
7
8
9
10
11
12
vue.property.observe = function(){
var self = this;
this.$data = new Proxy(this.$data,{
get(target,key, receiver){
return target[key]
},
set(target,key,newVal){
target[key] = newVal
self.render()
}
})
}
+ +

还能用 Proxy 做什么

    +
  1. 校验类型
  2. +
  3. 真正的私有变量
  4. +
+
校验类型

例子:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 数据类型验证
// 我们要创建一个对象,这个对象是个人,他有name和age两个属性
// name必须是中文,age必须是数字,大于18岁

// 这里用到了策略模式
var valid = {
name(value){
var reg=/^[\u4E00-\u9FAS]=$/
if(typeof value === 'string' && reg.test(value)){
return true;
}
return false;
},
age(value){
if(typeof value === 'number' && value > 18){
return true;
}
return false;
}
}
function Person(name,age){
this.name = name
this.age = age
return new Proxy(this,{
get(target,key){
return target[key]
},
set(target,key,value){
if(valid[key](value)){
return Reflect.set(target,key,value)
}else{
throw new Error(key+'is not valid')
}
}
})
}
new Person('name',19)
+
+

策略模式
定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。

+
+
真正的私有变量

vue-router 源码中,给 $router ,$route 用 defineProperty 定义 get 并返回本身,这样就不能修改属性了。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
Object.defineProperty(this,'$router',{ // Router 的实例
get(){
return this._root._router;
}
})
Object.defineProperty(this,'$route',{
get(){
return {
// 当前路由所在的状态
current: this._root._router.history.current;
}
}
})
+ +

虚拟Dom和diff算法

虚拟Dom是虚拟的,他只在概念里面存在,在AST语法树,下面进行解释

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 模板
<template>
<div>
<p>{{msg}}</p>
<p>2</p>
<p>3</p>
</div>
</template>

// diff 描述法
diff <div>
props:{
id:2
}
children:[
diff <p>
props:{
id:xxx
}
children:[
...
]
]

// 对象描述法
var virtual = {
dom:'div',
props:{
id:2
},
children:[
....
]
}
+

每层结构都是一样的,那么是如何进行 diff 比对的呢?

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** 
* diff 算法
*/
patchVnode(oldVnode,vnode){ // 接收新旧节点
const el = vnode.el = oldVnode.el; // 拿出真实dom
let i,oldCh = oldVnode.children ,ch = vnode.children // 拿出新旧节点子元素children数组
if(oldVnode === vnode) return; // 新旧节点一致,直接返回不进行后续操作
// 分情况操作 --- 只有文字节点,删除了子元素,增加了子元素,子元素发生变动
// 新旧节点都不为空,且不一样
if(oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text){ // 文字发生更新
api.setTextContent(el,vonde.text) // 文字发生了更新,更新文字,给Dom,更新文字
} else { // 不是单纯文字节点的话
updateEle(); // 更新元素
if(oldCh&&ch&&oldCh!==ch){ // 都有子元素,子元素变动
updateChildren() // 调用更新子元素方法
} else if(ch){ // 增加子元素
createEl(vnode) // 创建子元素
} else if(oldCh){ // 删除子元素
api.removeChildren(el) // 调用删除子元素方法
}
}
}
+

源码要多看,以下必看 Vue/react/axios/vue-router/Redux/Vuex
为什么要看源码??

+
    +
  • 初级前端就会用vue或react — 从差不多水平的60%中挑出更好的人
  • +
  • 提高思想–》看优秀的代码–》写优秀的代码
  • +
  • 看源码能力,对高级前端是必备的。— 解决疑难杂症,看源码了解原理。
  • +
+

vue 性能优化

因为是公开课,所以时间上没来的及说完,以后自己在听别的有关的内容时再补上这块。

+

最后

只有不断学习才能进步,充分利用网络的便利性,找各种优质的教学资源,我相信,努力会有回报,加油!

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Vue 响应式原理的实现(课程笔记)

+

文章作者:

+

发布时间:2020年02月21日 - 23:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/02/21/Implementation-of-the-vue-response-principle/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/02/23/V8-engine-memory-management-and-optimization/index.html b/en/2020/02/23/V8-engine-memory-management-and-optimization/index.html new file mode 100644 index 0000000000..29feb75cea --- /dev/null +++ b/en/2020/02/23/V8-engine-memory-management-and-optimization/index.html @@ -0,0 +1,645 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + v8引擎如何回收内存(笔记) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ v8引擎如何回收内存(笔记) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

V8引擎如何回收垃圾

为什么我们要关注内存

    +
  • 防止页面占用内存过大,引起客户端卡顿,甚至无响应
  • +
  • Node使用的也是v8,内存对于后端服务的性能至关重要。因为服务的持久性,后端更容易造成内存溢出

    v8引擎的内存回收机制

    v8的内存分配

    新生代内存空间
  • +
  • from
  • +
  • to
    老生代内存空间

    内存大小

  • +
  • 和操作系统有关 — 64位(1.4G)32位(0.7G)
  • +
  • 64位下 新生代(64MB) 老生代(1400MB)
  • +
  • 32位下 新生代(16MB) 老生代(700MB)
  • +
+

为什么不占多一点内存

+
    +
  • js设计之初是为浏览器
      +
    • 前端特点 — 不持久化,执行一遍就回收了,所以1.4G够用了
    • +
    • js回收内存会暂停执行代码
    • +
    +
  • +
+

垃圾回收算法

新生代简单的说就是复制

+
    +
  • 新生代用来放新产生的变量,根据条件放到老生代,有用的放到to里(把有用的留一边,清空另一边,往复进行)
  • +
  • 算法复杂度(时间复杂度,空间复杂度)—- 牺牲空间换时间
  • +
+

老生代就是标记、删除、整理

+
    +
  • 为什么要整理
      +
    • 数组是需要连续的空间
    • +
    +
  • +
+

新生代如何晋升到老生代

+
    +
  • 变量是否经过回收,第二次回收放到老生代,第一次回收放到To
  • +
  • To空间使用了25%,放到老生代
  • +
+

V8是如何处理变量的

1
2
3
4
5
6
7
8
9
10
11
12
13
// 浏览器查看内存
window.performance
// nodejs查看内存 --- nodejs是c++的,可以拓宽内存
process.memoryUsage()

// 拿内存的方法
function getMem(){
var mem = process.memoryUsage();
var format = function(bytes){
return (bytes/1024/1024).toFixed(2)+'MB';
}
console.log('heapTotal:'+format(mem.heapTotal)+'heapUsed:'+format(mem.heapUsed))
}
+

变量处理

    +
  • 内存主要就是存储变量等数据的
  • +
  • 局部变量当程序执行结束,且没有引用的时候就会随着消失 — 可以被回收但不是说会马上回收
  • +
  • 全局对象会始终存活到程序运行结束
  • +
+

如何查看V8内存使用情况

如何注意内存使用

优化内存的技巧

    +
  • 尽量不要定义全局变量
  • +
  • 全局变量记得手动销毁掉
      +
    • 不推荐开发时写delete – 支持有问题,严格模式有bug
    • +
    • 赋值为 undefined/null undefined 是变量 null 是保留字
    • +
    +
  • +
  • 用匿名自执行函数变全局为局部
      +
    • (function(){})()
    • +
    +
  • +
  • 尽量避免使用闭包 —- 错误的观点,ie5时代的问题 —- 尽量避免使用闭包引用
  • +
+

闭包

+
1
2
3
4
5
6
7
function a(){
var size = 20*1024*1024;
var arr1 = new Array(size)
return arr1
}
a() // 这样就没问题
var b = a() // 因为引用所以无法销毁
+

防止内存泄漏

    +
  • 滥用缓存
  • +
  • 大内存量操作
  • +
+

所有的优化都可以用缓存来解决,缓存通常都在全局,缓存可以直接拿来用;

+
1
2
3
4
5
6
7
8
9
10
11
12
13
var 20*1024*1024;
var a = []
for(var i=0;i<13;i++){
a.push(new Array(size))
}

// 加缓存锁
for(var i=0;i<13;i++){
if(a.length>4){
a.shift();
}
a.push(new Array(size))
}
+
    +
  • 不要用v8来缓存
      +
    • 一定要用要的话加锁
    • +
    +
  • +
+

nodejs中读取大文件要用流的形式,不要用读文件到buffer
fs.readFile()
fs.createReadStream()

+

浏览器中,大文件上传记得切片
file.slice(0,1000)
file.slice(1000,2000)

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:v8引擎如何回收内存(笔记)

+

文章作者:

+

发布时间:2020年02月23日 - 08:00

+

最后更新:2020年02月23日 - 07:57

+

原始链接:https://blog.lifesli.com/2020/02/23/V8-engine-memory-management-and-optimization/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/02/24/Vue-plug-in-development/index.html b/en/2020/02/24/Vue-plug-in-development/index.html new file mode 100644 index 0000000000..ccc5ca0fc2 --- /dev/null +++ b/en/2020/02/24/Vue-plug-in-development/index.html @@ -0,0 +1,606 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + vue插件开发(笔记) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ vue插件开发(笔记) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

vue插件开发

+

Vue.use({install(Vues){}})

+
+

Vue.use

把给到的内容执行一下
举例

+
1
2
3
4
function a(){
console.log('a')
}
Vue.use(a) // a
+

有 install 就执行 install

+
1
2
3
4
5
6
7
function a(){
console.log('a')
}
a.install = function(){
console.log('b')
}
Vue.use(a) // b
+

再进一步

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function a(){
console.log('a')
}
a.install = function(){
// console.log('b')
vue.mixin({ // 抽离公共逻辑 , 缺点:命名冲突,难以阅读
data(){ // data数据少的时候可以不用vuex 用mixin
return {
c:'this is mixin'
}
},
methods:{
// 混入方法
// 提示性弹窗 原:import 控制 显隐 现在:在根节点引入,通过mixin在方法控制显隐
}
// 混入生命周期
create(){
// 所有组件的create生命周期都执行 mixin先执行
}
})
}
Vue.use(a) // b
+ +

vue.util.defineReactive()

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
vue.util.defineReactive()

var test = {
testa: 1
}
setTimeout(()=>{
test.testa = 2
},1000)
vue.mixin({
beforeCreate(){
this.test = test
}
})

+ +

vue.extend vue.util.extend

+

vue.util.extend ===> 简单做了个拷贝,拷贝到一起

+
1
vue.util.extend(a,b)
+

vue.extend ===> 获取到某个对象的实例

+
1
2
let Constrator = vue.extend(obj)
let vm = new Constrator()
+ +

手写vue-router

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// myVueRouter.js
class HistoryRoute(){
constructor(){
this.current = null;
}
}

class vueRouter{
constructor(options){
this.mode = options.mode || 'hash'
this.history = new HistoryRoute
this.routes = options.routes||[]
this.routesMap = this.createMap(this.routes)
this.init()
}
init(){
if(this.mode == 'hash'){
// 自动加上 #
location.hash?"":location.hash="/"
window.addEventListener('load',()=>{
this.history.current = location.hash.slice(1)
})
window.addEventListener('hashchange',()=>{
this.history.current = location.hash.slice(1)
})
}else{
location.pathname?"":location.pathname="/"
window.addEventListener('load',()=>{
this.history.current = location.hash.pathname
})
window.addEventListener('popstate',()=>{
this.history.current = location.hash.pathname
})
}
}
createMap(router){
return router.reduce((memo,current)=>{
memo[current.path] = current.component
})
}
}

vueRouter.install = function(Vue){
Vue.mixin({
beforeCreate(){ // 组件还未实例化好
if(this.$options && this.$options.router){ // 有配置而且引入路由
this._root = this
this._router = this.$option.router

Vue.util.defineReactive(this,'current',this._router.history)
}else{
this._root = this.$parent._root
}
// 增强健壮性
Object.defineProperty(this,'$route',{
get(){
return this._root._router
}
})
}
})
Vue.component('router-view',{
render(h){
// 如何根据当前的current,获取到对应的组件
let current = this._self._root._router.history.current
let routerMap = this._self._root._router.routeMap
return h(routeMap[current])
}
})
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:vue插件开发(笔记)

+

文章作者:

+

发布时间:2020年02月24日 - 22:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/02/24/Vue-plug-in-development/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/03/03/Speeding-up-brew-with-Ali-mirroring/index.html b/en/2020/03/03/Speeding-up-brew-with-Ali-mirroring/index.html new file mode 100644 index 0000000000..0f0be1abe0 --- /dev/null +++ b/en/2020/03/03/Speeding-up-brew-with-Ali-mirroring/index.html @@ -0,0 +1,603 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 使用阿里镜像加速brew(转载) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 使用阿里镜像加速brew(转载) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

使用 Alibaba 的 Homebrew 镜像源进行加速

平时我们执行 brew 命令安装软件的时候,跟以下 3 个仓库地址有关:

+
    +
  • brew.git
  • +
  • homebrew-core.git
  • +
  • homebrew-bottles
    通过以下操作将这 3 个仓库地址全部替换为 Alibaba 提供的地址
  • +
+

1. 替换 / 还原 brew.git 仓库地址

1
2
3
4
5
6
7
8
9
# 替换成阿里巴巴的 brew.git 仓库地址:
cd "$(brew --repo)"
git remote set-url origin https://mirrors.aliyun.com/homebrew/brew.git

#=======================================================

# 还原为官方提供的 brew.git 仓库地址
cd "$(brew --repo)"
git remote set-url origin https://github.com/Homebrew/brew.git
+ +

2. 替换 / 还原 homebrew-core.git 仓库地址

1
2
3
4
5
6
7
8
9
# 替换成阿里巴巴的 homebrew-core.git 仓库地址:
cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
git remote set-url origin https://mirrors.aliyun.com/homebrew/homebrew-core.git

#=======================================================

# 还原为官方提供的 homebrew-core.git 仓库地址
cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
git remote set-url origin https://github.com/Homebrew/homebrew-core.git
+ +

3. 替换 / 还原 homebrew-bottles 访问地址

这个步骤跟你的 macOS 系统使用的 shell 版本有关系

+

所以,先来查看当前使用的 shell 版本

+
1
2
3
4
echo $SHELL

# 如果你的输出结果是 /bin/zsh,参考下方的 zsh 终端操作方式
# 如果你的输出结果是 /bin/bash,参考下方的 bash 终端操作方式
+

3.1 zsh 终端操作方式

1
2
3
4
5
6
7
8
9
10
# 替换成阿里巴巴的 homebrew-bottles 访问地址:
echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles' >> ~/.zshrc
source ~/.zshrc

#=======================================================

# 还原为官方提供的 homebrew-bottles 访问地址
vi ~/.zshrc
# 然后,删除 HOMEBREW_BOTTLE_DOMAIN 这一行配置
source ~/.zshrc
+

3.2 bash 终端操作方式

1
2
3
4
5
6
7
8
9
10
# 替换 homebrew-bottles 访问 URL:
echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles' >> ~/.bash_profile
source ~/.bash_profile

#=======================================================

# 还原为官方提供的 homebrew-bottles 访问地址
vi ~/.bash_profile
# 然后,删除 HOMEBREW_BOTTLE_DOMAIN 这一行配置
source ~/.bash_profile
+ +

转载自:http://www.xiegangd.com/article/154055689187484

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:使用阿里镜像加速brew(转载)

+

文章作者:

+

发布时间:2020年03月03日 - 22:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/03/03/Speeding-up-brew-with-Ali-mirroring/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/03/23/path-join-vs-path-resolve/index.html b/en/2020/03/23/path-join-vs-path-resolve/index.html new file mode 100644 index 0000000000..f55161da75 --- /dev/null +++ b/en/2020/03/23/path-join-vs-path-resolve/index.html @@ -0,0 +1,587 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + path.join 与 path.resolve 的区别 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ path.join 与 path.resolve 的区别 +

+ + +
+ + + + +
+ + +

path.join 与 path.resolve 的区别

    +
  1. 对于以/开始的路径片段,path.join只是简单的将该路径片段进行拼接,而path.resolve将以/开始的路径片段作为根目录,在此之前的路径将会被丢弃,就像是在terminal中使用cd命令一样。
  2. +
+
1
2
path.join('/a', '/b') // 'a/b'
path.resolve('/a', '/b') // '/b'
+
    +
  1. path.resolve总是返回一个以相对于当前的工作目录(working directory)的绝对路径。
  2. +
+
1
2
path.join('./a', './b') // 'a/b'
path.resolve('./a', './b') // '/Users/username/Projects/webpack-demo/a/b'
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:path.join 与 path.resolve 的区别

+

文章作者:

+

发布时间:2020年03月23日 - 14:42

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/03/23/path-join-vs-path-resolve/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/03/24/do-it-yourselfery-EventEmitter/index.html b/en/2020/03/24/do-it-yourselfery-EventEmitter/index.html new file mode 100644 index 0000000000..f0a7336bc0 --- /dev/null +++ b/en/2020/03/24/do-it-yourselfery-EventEmitter/index.html @@ -0,0 +1,592 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- EventEmitter(事件触发器) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- EventEmitter(事件触发器) +

+ + +
+ + + + +
+ + +

实现一个 EventEmitter

    +
  1. 创建一个 Event 类,包含构造函数、绑定、解绑、触发方法
  2. +
  3. on 监听event事件,事件触发时调用fn函数。根据字典创建事件数组,如果事件处理不存在,那么就推入数组,反之返回原数组
  4. +
  5. off 从字典中获取当前事件数组,如果获取值是数组,那么判断解绑哪个事件,如果没填清空全部(数组长度置为零),否则从事件数组删除选择的事件
  6. +
  7. emit 触发event事件,并把参数arg1,arg2,arg3….传给事件处理函数,跟解绑外层判断一样,内层把删除改为执行即可
  8. +
  9. once 为指定事件注册一个单次监听器,单次监听器最多只触发一次,触发后立即解除监听器。
  10. +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class Event {
constructor () {
// 储存事件的数据结构
// 为查找迅速, 使用对象(字典)
this._cache = {}
}

// 绑定
on(event, callback) {
// 为了按类查找方便和节省空间
// 将同一类型事件放到一个数组中
// 这里的数组是队列, 遵循先进先出
// 即新绑定的事件先触发
let fns = (this._cache[event] = this._cache[event] || [])
if(fns.indexOf(callback) === -1) {
fns.push(callback)
}
return this
}

// 解绑
off (event, callback) {
let fns = this._cache[event]
if(Array.isArray(fns)) {
if(callback) {
let index = fns.indexOf(callback)
if(index !== -1) {
fns.splice(index, 1)
}
} else {
// 全部清空
fns.length = 0
}
}
return this
}
// 触发emit
emit(event, ...args) {
let fns = this._cache[event]
if(Array.isArray(fns)) {
fns.forEach((fn) => {
fn(...args)
})
}
return this
}

// 一次性绑定
once(event, callback) {
let onceCallback = () => { // 定义一个只执行一次就解绑的方法
callback.call(this); // 使用call改变this指向
this.off(event, onceCallback); // 解绑
};
this.on(event, onceCallback); // 绑定
return this;
}
}
+

好的接下来我们调用一下

+
1
2
3
4
5
6
7
8

let e = new Event()

e.on('click',function(){
console.log('on')
})
// e.trigger('click', '666')
console.log(e)
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- EventEmitter(事件触发器)

+

文章作者:

+

发布时间:2020年03月24日 - 15:45

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-EventEmitter/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/03/24/do-it-yourselfery-async-await/index.html b/en/2020/03/24/do-it-yourselfery-async-await/index.html new file mode 100644 index 0000000000..9225fdfd1a --- /dev/null +++ b/en/2020/03/24/do-it-yourselfery-async-await/index.html @@ -0,0 +1,584 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- async、await | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- async、await +

+ + +
+ + + + +
+ + +

原理

就是利用 generator (生成器)分割代码片段。然后我们使用一个函数让其自迭代,每一个 yieldpromise 包裹起来。执行下一步的时机由 promise 来控制

+

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function _asyncToGenerator(fn) {
return function() {
var self = this, args = arguments;
return new Promise(function(resolve, reject) { // 将返回值 promise 化
var gen = fn.apply(self, args); // 获取迭代器实例
function _next(value) { // 执行下一步
asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
}
function _throw(err) { // 抛出异常
asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
}
_next(undefined); // 第一次触发
});
};
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- async、await

+

文章作者:

+

发布时间:2020年03月24日 - 15:50

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-async-await/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/03/24/do-it-yourselfery-call-apply/index.html b/en/2020/03/24/do-it-yourselfery-call-apply/index.html new file mode 100644 index 0000000000..074908b1fb --- /dev/null +++ b/en/2020/03/24/do-it-yourselfery-call-apply/index.html @@ -0,0 +1,618 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- call、apply、bind | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- call、apply、bind +

+ + +
+ + + + +
+ + +

原版

先来看一个call实例,看看call到底做了什么:

+
1
2
3
4
5
6
7
let foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call(foo); // 1
+

从代码的执行结果,我们可以看到,call首先改变了this的指向,使函数的this指向了foo,然后使bar函数执行了。
总结一下:

+
    +
  • call改变函数this指向
  • +
  • 调用函数
  • +
+

自己动手

    +
  1. 首先我们对参数 context 做了兼容处理,不传值, context 默认值为 window
  2. +
  3. 然后我们将函数挂载到 context 上面, context.fn = this
  4. +
  5. 处理参数,将传入 myCall 的参数截取,去除第一位,然后转为数组;
  6. +
  7. 调用 context.fn ,此时 fnthis 指向 context
  8. +
  9. 删除对象上的属性 delete context.fn
  10. +
  11. 将结果返回。
  12. +
+
1
2
3
4
5
6
7
8
Function.prototype.myCall = function(context) {
context = context || window;
context.fn = this; // 将函数挂载到对象的fn属性上
const args = [...arguments].slice(1); // 处理传入的参数
const result = context.fn(...args); // 通过对象的属性调用该方法
delete context.fn; // 删除该属性
return result // 返回结果
};
+

applycall 的区别在于参数, 其他没有差别,实现如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// myApply的参数形式为(obj,[arg1,arg2,arg3]);
// 所以myApply的第二个参数为[arg1,arg2,arg3]
// 这里我们用扩展运算符来处理一下参数的传入方式
Function.prototype.myApply = function(context) {
context = context || window
context.fn = this
let result
if (arguments[1]) { // 判断是否有第二个参数
result = context.fn(…arguments[1]) // 有的话传入执行
} else {
result = context.fn() // 没有的话空参执行
}
delete context.fn;
return result
};
+

bindcallapply 作用都是改变 this 的指向,区别在于 bind 改变后不会立即执行,而 callapply 会立即执行,我们看一下 bind 的用法

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(){
this.name="zs";
this.age=18;
this.gender="男"
}
let obj={
hobby:"看书"
}

let changePerson = Person.bind(obj); // 将构造函数的this绑定为obj
changePerson(); // 直接调用构造函数,函数会操作obj对象,给其添加三个属性;
console.log(obj); // => {hobby:"看书",name:"zs",age:18,gender:'男'}
let p = new changePerson(); // 用改变了this 指向的构造函数,new一个实例出来
console.log(p); // => Person {name:"zs",age:18,gender:'男'}
+

仔细观察上面的代码,再看输出结果。

+

我们对 Person 类使用了 bind 将其 this 指向 obj ,得到了 changePerson 函数,此处如果我们直接调用 changeperson 会改变 obj ,若用 new 调用 changeperson 会得到实例 p,并且其 __proto__ 指向 Person ,我们发现 bind 失效了。

+

我们得到结论:bind 改变了 this 指向的函数,如果用 new 操作符来调用, bind 将会失效

+

这个对象就是这个构造函数的实例,那么只要在函数内部执行 * this instanceof 构造函数 * 来判断其结果是否为 true ,就能判断函数是否是通过 new 操作符来调用了,若结果为 true 则是用 new 操作符调用的,总结如下:

+
    +
  1. 保存当前 this 指向
  2. +
  3. 保存环境上下文
  4. +
  5. 保存参数,去掉第一个对象参数
  6. +
  7. 返回待执行函数
      +
    1. 数组化剩余参数
    2. +
    3. 判断是否为构造函数
    4. +
    5. 若是执行构造函数,若不是改变 this 指向执行
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      // bind实现

      Function.prototype.myBind = function(context){
      let _this = this; // 1、保存函数
      context = context || window; // 2、保存目标对象
      let rest = [...arguments].slice(1); // 3、保存目标对象之外的参数,将其转化为数组;
      // 此处开始与 call 和 apply 不同,不是返回结果,而是返回一个函数
      return function F(){ // 4、返回一个待执行的函数
      let rest2 = Array.prototype.slice.call(arguments) // 5、这里的arguments是F函数的参数,转换为数组;
      if(this instanceof F){
      return new _this(...rest2) // 6、若是用new操作符调用,则直接用new 调用原函数,并用扩展运算符传递参数
      }else{
      _this.apply(context,rest.concat(rest2)); // 7、用apply调用第一步保存的函数,并绑定this,传递合并的参数数组,
      // 即context._this(rest.concat(rest2))
      }
      }
      };
    6. +
    +
  8. +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- call、apply、bind

+

文章作者:

+

发布时间:2020年03月24日 - 15:54

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-call-apply/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/03/24/do-it-yourselfery-create/index.html b/en/2020/03/24/do-it-yourselfery-create/index.html new file mode 100644 index 0000000000..32c1852ae3 --- /dev/null +++ b/en/2020/03/24/do-it-yourselfery-create/index.html @@ -0,0 +1,588 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- Object.create() | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- Object.create() +

+ + +
+ + + + +
+ + +

实现一个 Object.create() 方法

    +
  1. 创建一个空匿名函数
  2. +
  3. 函数原型对象指向传入对象实例
  4. +
  5. 返回构造函数创建的实例
  6. +
+
1
2
3
4
5
function create =  function (o) {
var F = function () {};
F.prototype = o;
return new F();
};
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- Object.create()

+

文章作者:

+

发布时间:2020年03月24日 - 14:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-create/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/03/24/do-it-yourselfery-flat/index.html b/en/2020/03/24/do-it-yourselfery-flat/index.html new file mode 100644 index 0000000000..0b43c707f9 --- /dev/null +++ b/en/2020/03/24/do-it-yourselfery-flat/index.html @@ -0,0 +1,587 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- Array.prototype.flat()函数 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- Array.prototype.flat()函数 +

+ + +
+ + + + +
+ + +

原理

将多层数组扁平化

+

实现

1
2
3
4
5
6
7
8
9
10
11
Array.prototype.myFlat = function() {
var arr = [];
this.forEach((item)=>{
if(Array.isArray(item)){
arr = arr.concat(item.myFlat()); // 如果是数组的话继续循环
}else{
arr.push(item)
}
})
return arr
};
+ +

还有另外一种实现方式,非常好用

+
1
2
3
4
5
Array.prototype.myFlat = function() {
return this.toString() // => "1,2,3,4"
.split(",") // => ["1", "2", "3", "4"]
.map(item => +item); // => [1, 2, 3, 4]
};
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- Array.prototype.flat()函数

+

文章作者:

+

发布时间:2020年03月24日 - 13:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-flat/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/03/24/do-it-yourselfery-isArray/index.html b/en/2020/03/24/do-it-yourselfery-isArray/index.html new file mode 100644 index 0000000000..cfc9f1bb90 --- /dev/null +++ b/en/2020/03/24/do-it-yourselfery-isArray/index.html @@ -0,0 +1,584 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- Array.isArray | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- Array.isArray +

+ + +
+ + + + +
+ + +

实现一个Array.isArray

思路很简单,就是利用 Object.prototype.toString

+
1
2
3
Array.myIsArray = function(o) { 
return Object.prototype.toString.call(Object(o)) === '[object Array]';
};
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- Array.isArray

+

文章作者:

+

发布时间:2020年03月24日 - 13:30

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-isArray/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/03/24/do-it-yourselfery-map/index.html b/en/2020/03/24/do-it-yourselfery-map/index.html new file mode 100644 index 0000000000..66331858ab --- /dev/null +++ b/en/2020/03/24/do-it-yourselfery-map/index.html @@ -0,0 +1,589 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- Array.prototype.map() | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- Array.prototype.map() +

+ + +
+ + + + +
+ + +

原理

先看看 reducemap 的使用方法

+
1
2
let new_array = arr.map(function callback(currentValue[, index[,array) {/* Return element for new_array */ }[, thisArg])
let result = arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
+ +

实现

第一种用 for 实现

+
1
2
3
4
5
6
7
Array.prototype.myMap = function(callback, thisArg) {
let arr = [];
for (let i = 0; i < this.length; i++) {
arr.push(callback.call(thisArg, this[i], i, this));
}
return arr;
};
+

第二种用 reduce 实现

+
1
2
3
4
5
6
7
Array.prototype.myMap = function(callback, thisArg) {
let result = this.reduce((accumulator, currentValue, index, array) => {
accumulator.push(callback.call(thisArg, currentValue, index, array));
return accumulator;
}, []);
return result;
};
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- Array.prototype.map()

+

文章作者:

+

发布时间:2020年03月24日 - 15:20

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-map/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/03/24/do-it-yourselfery-new/index.html b/en/2020/03/24/do-it-yourselfery-new/index.html new file mode 100644 index 0000000000..54d1351c76 --- /dev/null +++ b/en/2020/03/24/do-it-yourselfery-new/index.html @@ -0,0 +1,591 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- new | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- new +

+ + +
+ + + + +
+ + +

实现一个new操作符

我们首先知道new做了什么:

+
    +
  1. 创建一个空的简单 JavaScript 对象(即{})
  2. +
  3. 链接该对象(即设置该对象的构造函数)到另一个对象
  4. +
  5. 将步骤(1)新创建的对象作为 this 的上下文
  6. +
  7. 如果该函数没有返回对象,则返回 this
  8. +
+

知道new做了什么,接下来我们就来实现它

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function create(){
// 创建一个空的对象
let obj = {};
// 获得构造函数
let Con = [].shift.call(arguments)
// 将空对象指向构造函数的原型链
Object.setPrototypeOf(obj, Con.prototype);
// obj.__proto__ = Con.prototype // 链接到原型
// obj绑定到构造函数上,便可以访问构造函数中的属性,即this.obj.Con(args)
let result = Con.apply(obj, arguments);
// 如果返回的result是一个对象则返回
// new方法失效,否则返回obj
return result instanceof Object ? result : this.obj;
// return typeof result === 'object' ? result : obj// 确保 new 出来的是个对象
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- new

+

文章作者:

+

发布时间:2020年03月24日 - 04:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-new/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/03/24/do-it-yourselfery-reduce/index.html b/en/2020/03/24/do-it-yourselfery-reduce/index.html new file mode 100644 index 0000000000..ffb0b3567b --- /dev/null +++ b/en/2020/03/24/do-it-yourselfery-reduce/index.html @@ -0,0 +1,587 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- Array.prototype.reduce | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- Array.prototype.reduce +

+ + +
+ + + + +
+ + +

原版

1
Array.prototype.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
+ +

自己动手

1
2
3
4
5
6
7
8
Array.prototype.myReduce = function(callback, initialValue) {
let accumulator = initialValue ? initialValue : this[0]; // 第一次使用判断时候有initialValue参数,如果有用他,没有用this[0],这里this指的是传入的数组,this[0]就是数组第一项
for (let i = initialValue ? 0 : 1; i < this.length; i++) { // 如果有初始值从0开始循环,不然从1开始
let _this = this; // 保留当前this指向
accumulator = callback(accumulator, this[i], i, _this); //
}
return accumulator; // 返回迭代器的终值
};
+

试用一下

+
1
2
3
4
5
6
7
let arr = [1, 2, 3, 4];
let sum = arr.myReduce((acc, val) => {
acc += val;
return acc;
}, 5);

console.log(sum); // 15
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- Array.prototype.reduce

+

文章作者:

+

发布时间:2020年03月24日 - 04:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-reduce/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2020/03/24/do-it-yourselfery-\344\272\213\344\273\266\344\273\243\347\220\206/index.html" "b/en/2020/03/24/do-it-yourselfery-\344\272\213\344\273\266\344\273\243\347\220\206/index.html" new file mode 100644 index 0000000000..0072caa22f --- /dev/null +++ "b/en/2020/03/24/do-it-yourselfery-\344\272\213\344\273\266\344\273\243\347\220\206/index.html" @@ -0,0 +1,583 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- 事件代理 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- 事件代理 +

+ + +
+ + + + +
+ + +

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<ul id="color-list">
<li>red</li>
<li>yellow</li>
<li>blue</li>
<li>green</li>
<li>black</li>
<li>white</li>
</ul>

<script>
(function () {
var color_list = document.getElementById('color-list');
color_list.addEventListener('click', showColor, true);
function showColor(e) {
var x = e.target;
if (x.nodeName.toLowerCase() === 'li') {
alert(x.innerHTML);
}
}
})();
</script>
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- 事件代理

+

文章作者:

+

发布时间:2020年03月24日 - 14:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-%E4%BA%8B%E4%BB%B6%E4%BB%A3%E7%90%86/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2020/03/24/do-it-yourselfery-\345\217\214\345\220\221\347\273\221\345\256\232/index.html" "b/en/2020/03/24/do-it-yourselfery-\345\217\214\345\220\221\347\273\221\345\256\232/index.html" new file mode 100644 index 0000000000..5637ba9ecd --- /dev/null +++ "b/en/2020/03/24/do-it-yourselfery-\345\217\214\345\220\221\347\273\221\345\256\232/index.html" @@ -0,0 +1,585 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- Vue双向绑定 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- Vue双向绑定 +

+ + +
+ + + + +
+ + +

Vue 2.x 的 Object.defineProperty 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 数据
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');

// 数据劫持
Object.defineProperty(data, 'text', {
// 数据变化 —> 修改视图
set(newVal) {
input.value = newVal;
span.innerHTML = newVal;
}
});

// 视图更改 --> 数据变化
input.addEventListener('keyup', function(e) {
data.text = e.target.value;
});
+ +

Vue 3.x 的 proxy 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 数据
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');

// 数据劫持
const handler = {
set(target, key, value) {
target[key] = value;
// 数据变化 —> 修改视图
input.value = value;
span.innerHTML = value;
return value;
}
};
const proxy = new Proxy(data, handler);

// 视图更改 --> 数据变化
input.addEventListener('keyup', function(e) {
proxy.text = e.target.value;
});
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- Vue双向绑定

+

文章作者:

+

发布时间:2020年03月24日 - 15:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-%E5%8F%8C%E5%90%91%E7%BB%91%E5%AE%9A/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/03/24/webpack-learning-1/index.html b/en/2020/03/24/webpack-learning-1/index.html new file mode 100644 index 0000000000..6955814ae2 --- /dev/null +++ b/en/2020/03/24/webpack-learning-1/index.html @@ -0,0 +1,877 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2020年了,再不会webpack敲得代码就不香了(近万字实战)【转载】 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 2020年了,再不会webpack敲得代码就不香了(近万字实战)【转载】 +

+ + +
+ + + + +
+ + +

推荐序

这里是我自己写的,看了这篇文章把我零碎的 webpack 知识系统的整理了一下,感觉受益匪浅,推荐更多小伙伴看一看这篇文章,好文共享,建议稍微对 webpack 了解服用更佳。
2020年了,再不会webpack敲得代码就不香了(近万字实战)

+

前言

2020年即将到来,在众多前端的招聘要求里, webpack 、工程化这些字眼频率越来越高。日常开发者中,我们常常在用诸如 vue-clicreate-react-app 的脚手架来构建我们的项目。但是如果你想在团队脱颖而出(鹤立鸡群)、拿到更好的 offer (还房贷),那么你必须去深刻的认识下我们经常打交道的 webpack

+

入门(一起来用这些小例子让你熟悉webpack的配置)

webpack 是什么?

webpack 是一个现代 JavaScript 应用程序的静态模块打包器,当 webpack 处理应用程序时,会递归构建一个依赖关系图,其中包含应用程序需要的每个模块,然后将这些模块打包成一个或多个 bundle

+

webpack 的核心概念

    +
  • entry: 入口
  • +
  • output: 输出
  • +
  • loader: 模块转换器,用于把模块原内容按照需求转换成新内容
  • +
  • plugins: 扩展插件,在 webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要做的事情
  • +
+

初始化项目

新建一个目录,初始化 npm

+
1
npm init
+ +

webpack 是运行在 node 环境中的,我们需要安装以下两个 npm

+
1
npm i -D webpack webpack-cli
+ +
+
    +
  • npm i -Dnpm install --save-dev 的缩写
  • +
  • npm i -Snpm install --save 的缩写
  • +
+
+

新建一个文件夹 src ,然后新建一个文件 main.js ,写一点代码测试一下

+
1
console.log('call me 老yuan')
+ +

配置 package.json 命令

+
1
2
3
"script":{
"build":"webpack src/main.js"
}
+

执行

+
1
npm run build
+

此时如果生成了一个 dist 文件夹,并且内部含有 main.js 说明已经打包成功了

+

开始我们自己的配置

上面一个简单的例子只是 webpack 自己默认的配置,下面我们要实现更加丰富的自定义配置

+

新建一个 build 文件夹,里面新建一个 webpack.config.js

+
1
2
3
4
5
6
7
8
9
10
11
// webpack.config.js

const path = require('path');
module.exports = {
mode:'development', // 开发模式
entry: path.resolve(__dirname,'../src/main.js'), // 入口文件
output: {
filename: 'output.js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
}
}
+

更改我们的打包命令

+
1
2
3
"script":{
"build":"webpack build/webpack.config.js"
}
+

执行 npm run build
会发现生成了以下目录

+
1
2
3
4
project
dist
build
src
+

其中 dist 文件夹中的 main.js 就是我们需要在浏览器中实际运行的文件
当然实际运用中不会仅仅如此,下面让我们通过实际案例带你快速入手 webpack

+

配置html模板

js 文件打包好了,但是我们不可能每次在html文件中手动引入打包好的js

+
+

这里可能有的朋友会认为我们打包 js 文件名称不是一直是固定的嘛( output.js )?这样每次就不用改动引入文件名称了呀?实际上我们日常开发中往往会这样配置:

+
+
1
2
3
4
5
6
7
module.exports = {
// 省略其他配置
output: {
filename: '[name].[hash:8].js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
}
}
+ +

这时候生成的 dist 目录文件如下

+
1
2
dist/
app.fsafasf.js
+

为了缓存,你会发现打包好的 js 文件的名称每次都不一样。 webpack 打包出来的js文件我们需要引入到 html 中,但是每次我们都手动修改 js 文件名显得很麻烦,因此我们需要一个插件来帮我们完成这件事情,那就是 html-webpack-plugin

+
1
npm i -D html-webpack-plugin
+

新建一个 build 同级的文件夹 public ,里面新建一个 index.html
具体配置文件如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin') // **此插件用来解决html模板引入js文件
module.exports = {
mode:'development', // 开发模式
entry: path.resolve(__dirname,'../src/main.js'), // 入口文件
output: {
filename: '[name].[hash:8].js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
},
plugins:[
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html')
})
]
}
+

可以发现打包生成的js文件已经被自动引入 html 文件中

+

多入口文件如何开发

+

生成多个 html-webpack-plugin 实例来解决这个问题

+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode:'development', // 开发模式
entry: {
main:path.resolve(__dirname,'../src/main.js'),
header:path.resolve(__dirname,'../src/header.js')
},
output: {
filename: '[name].[hash:8].js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
},
plugins:[
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html'),
filename:'index.html',
chunks:['main'] // 与入口文件对应的模块名
}),
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/header.html'),
filename:'header.html',
chunks:['header'] // 与入口文件对应的模块名
}),
]
}
+ +

clean-webpack-plugin

+

每次执行 npm run build 会发现 dist 文件夹里会残留上次打包的文件,这里我们推荐一个 plugin 来帮我们在打包输出前清空文件夹 clean-webpack-plugin

+
+
1
2
3
4
5
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
module.exports = {
// ...省略其他配置
plugins:[new CleanWebpackPlugin()]
}
+
希望dist目录下某个文件夹不被清空

不过呢,有些时候,我们并不希望整个 dist 目录都被清空,比如,我们不希望,每次打包的时候,都删除 dll 目录,以及 dll 目录下的文件或子目录,该怎么办呢?

+

clean-webpack-plugin 为我们提供了参数 cleanOnceBeforeBuildPatterns

+
1
2
3
4
5
6
7
8
9
//webpack.config.js
module.exports = {
//...
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns:['**/*', '!dll', '!dll/**'] //不删除dll目录下的文件
})
]
}
+ +

引用CSS

我们的入口文件是 js ,所以我们在入口 js 中引入我们的 css 文件

+
1
import 'asset/style.css'
+

同时我们也需要一些 loader 来解析我们的 css 文件

+
1
npm i -D style-loader css-loader
+

如果我们使用 less 来构建样式,则需要多安装两个

+
1
npm i -D less less-loader
+

配置文件如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.config.js
module.exports = {
// ...省略其他配置
module:{
rules:[
{
test:/\.css$/,
use:['style-loader','css-loader'] // 从右向左解析原则
},
{
test:/\.less$/,
use:['style-loader','css-loader','less-loader'] // 从右向左解析原则
}
]
}
}
+ +

我们简单说一下上面的配置:

+
    +
  • style-loader 动态创建 style 标签,将 css 插入到 head 中.
  • +
  • css-loader 负责处理 @import 等语句。
  • +
  • postcss-loaderautoprefixer ,自动生成浏览器兼容性前缀 —— 2020了,应该没人去自己徒手去写浏览器前缀了吧
  • +
  • less-loader 负责处理编译 .less 文件,将其转为 css
  • +
+
+

注意:
loader 的执行顺序是从右向左执行的,也就是后面的 loader 先执行,上面 loader 的执行顺序为: less-loader —> postcss-loader —> css-loader —> style-loader
当然,loader 其实还有一个参数,可以修改优先级,enforce 参数,其值可以为: pre(优先执行) 或 post (滞后执行)。
现在,我们已经可以处理 .less 文件啦,.css 文件只需要修改匹配规则,删除 less-loader 即可。

+
+

为css添加浏览器前缀

1
2
npm i -D postcss-loader autoprefixer

+

配置如下

+
1
2
3
4
5
6
7
8
9
// webpack.config.js
module.exports = {
module:{
rules:[
test/\.less$/,
use:['style-loader','css-loader','postcss-loader','less-loader'] // 从右向左解析原则
]
}
}
+

接下来,我们还需要引入 autoprefixer 使其生效,这里有两种方式

+
在项目根目录下创建一个postcss.config.js文件,配置如下:
1
2
3
module.exports = {
plugins: [require('autoprefixer')] // 引用该插件即可了
}
+
直接在webpack.config.js里配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// webpack.config.js
module.exports = {
//...省略其他配置
module:{
rules:[{
test:/\.less$/,
use:['style-loader','css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
},'less-loader'] // 从右向左解析原则
}]
}
}
+

这时候我们发现 css 通过 style 标签的方式添加到了 html 文件中,但是如果样式文件很多,全部添加到 html 中,难免显得混乱。这时候我们想用把 css 拆分出来用外链的形式引入 css 文件怎么做呢?这时候我们就需要借助插件来帮助我们

+

拆分css

1
npm i -D mini-css-extract-plugin
+
+

webpack 4.0 以前,我们通过 extract-text-webpack-plugin 插件,把 css 样式从 js 文件中提取到单独的 css 文件中。 webpack4.0 以后,官方推荐使用 mini-css-extract-plugin 插件来打包 css 文件

+
+

配置文件如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
//...省略其他配置
module: {
rules: [
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader'
],
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].[hash].css",
chunkFilename: "[id].css",
})
]
}
+

拆分多个css

+

这里需要说的细一点,上面我们所用到的 mini-css-extract-plugin 将所有的 css 样式合并为一个 css 文件。如果你想拆分为一一对应的多个 css 文件,我们需要使用到 extract-text-webpack-plugin ,而目前 mini-css-extract-plugin 还不支持此功能。我们需要安装 @next 版本的 extract-text-webpack-plugin

+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
npm i -D extract-text-webpack-plugin@next
// webpack.config.js

const path = require('path');
const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')
let indexLess = new ExtractTextWebpackPlugin('index.less');
let indexCss = new ExtractTextWebpackPlugin('index.css');
module.exports = {
module:{
rules:[
{
test:/\.css$/,
use: indexCss.extract({
use: ['css-loader']
})
},
{
test:/\.less$/,
use: indexLess.extract({
use: ['css-loader','less-loader']
})
}
]
},
plugins:[
indexLess,
indexCss
]
}
+

打包 图片、字体、媒体、等文件

file-loader 就是将文件在进行一些处理后(主要是处理文件名和路径、解析文件 url ),并将文件移动到输出的目录中
url-loader 一般与 file-loader 搭配使用,功能与 file-loader 类似,如果文件小于限制的大小。则会返回 base64 编码,否则使用 file-loader 将文件移动到输出的目录中

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// webpack.config.js
module.exports = {
// 省略其它配置 ...
module: {
rules: [
// ...
{
test: /\.(jpe?g|png|gif)$/i, //图片文件
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]'
}
}
}
}
]
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, //媒体文件
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'media/[name].[hash:8].[ext]'
}
}
}
}
]
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i, // 字体
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'fonts/[name].[hash:8].[ext]'
}
}
}
}
]
},
]
}
}
+

用babel转义js文件

为了使我们的 js 代码兼容更多的环境我们需要安装依赖

+
1
2
npm i babel-loader @babel/preset-env @babel/core

+
+

注意
babel-loaderbabel-core 的版本对应关系

+
+
    +
  • babel-loader 8.x 对应 babel-core 7.x
  • +
  • babel-loader 7.x 对应 babel-core 6.x
  • +
+

配置如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// webpack.config.js
module.exports = {
// 省略其它配置 ...
module:{
rules:[
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:['@babel/preset-env']
}
},
exclude:/node_modules/
},
]
}
}
+

上面的 babel-loader 只会将 ES6/7/8 语法转换为 ES5 语法,但是对新 api 并不会转换 例如( promiseGeneratorSetMapsProxy 等)
此时我们需要借助 babel-polyfill 来帮助我们转换

+
1
2
3
4
5
6
npm i @babel/polyfill
// webpack.config.js
const path = require('path')
module.exports = {
entry: ["@babel/polyfill,path.resolve(__dirname,'../src/index.js')"], // 入口文件
}
+
+

手动把上面的 demo 敲一遍对阅读下面的文章更有益,建议入门的同学敲三遍以上

+
+

上面的实践是我们对 webpack 的功能有了一个初步的了解,但是要想熟练应用于开发中,我们需要一个系统的实战。让我们一起摆脱脚手架尝试自己搭建一个 vue 开发环境

+

搭建vue开发环境

上面的小例子已经帮助而我们实现了打包 css 、图片、 jshtml 等文件。
但是我们还需要以下几种配置

+

解析.vue文件

1
2
npm i -D vue-loader vue-template-compiler vue-style-loader
npm i -S vue
+
    +
  • vue-loader 用于解析 .vue 文件
  • +
  • vue-template-compiler 用于编译模板
  • +
+

配置如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const vueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
module:{
rules:[{
test:/\.vue$/,
use:['vue-loader']
},]
},
resolve:{
alias:{
'vue$':'vue/dist/vue.runtime.esm.js',
' @':path.resolve(__dirname,'../src')
},
extensions:['*','.js','.json','.vue']
},
plugins:[
new vueLoaderPlugin()
]
}
+

配置webpack-dev-server进行热更新

1
npm i -D webpack-dev-server
+

配置如下

+
1
2
3
4
5
6
7
8
9
10
11
12
const Webpack = require('webpack')
module.exports = {
// ...省略其他配置
devServer:{
port:3000,
hot:true,
contentBase:'../dist'
},
plugins:[
new Webpack.HotModuleReplacementPlugin()
]
}
+

完整配置如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// webpack.config.js
const path = require('path');
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')
const vueLoaderPlugin = require('vue-loader/lib/plugin')
const Webpack = require('webpack')
module.exports = {
mode:'development', // 开发模式
entry: {
main:path.resolve(__dirname,'../src/main.js'),
},
output: {
filename: '[name].[hash:8].js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
},
module:{
rules:[
{
test:/\.vue$/,
use:['vue-loader']
},
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:[
['@babel/preset-env']
]
}
}
},
{
test:/\.css$/,
use: ['vue-style-loader','css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
}]
},
{
test:/\.less$/,
use: ['vue-style-loader','css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
},'less-loader']
}
]
},
resolve:{
alias:{
'vue$':'vue/dist/vue.runtime.esm.js',
' @':path.resolve(__dirname,'../src')
},
extensions:['*','.js','.json','.vue']
},
devServer:{
port:3000,
hot:true,
contentBase:'../dist'
},
plugins:[
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html'),
filename:'index.html'
}),
new vueLoaderPlugin(),
new Webpack.HotModuleReplacementPlugin()
]
}
+

配置打包命令

1
2
3
4
"script":{
"dev":"webpack-dev-server --config build/webpack.config.js --open",
"build":"webpack --config build/webpack.config.js"
}
+

打包文件已经配置完毕,接下来让我们测试一下
首先在 src 新建一个 main.js

+
1
2
3
4
5
6
// main.js
import Vue from 'vue'
import App from './app'
new Vue({
render:h=>h(App)
}).$mount('#app')
+

新建一个 App.vue

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// App.vue
<template>
<div id='container'></div>
</template>
<script>
export default {
data(){
return {
initData:''
}
}
}
</script>
<style scoped>
#container{
width:100%;
height:100%;
}
</style>
+

新建一个 public 文件夹,里面新建一个 index.html

+
1
2
3
4
5
6
7
8
9
10
11
12
13
// index.html
<!DOCTYPE html>
<html lang='en'>
<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'>
<title>lao li</title>
</head>
<body>
<div id='app'></div>
</body>
</html>
+

执行 npm run dev 这时候如果浏览器出现 Vue 开发环境运行成功,那么恭喜你,已经成功迈出了第一步

+

区分开发环境与生产环境

实际应用到项目中,我们需要区分开发环境与生产环境,我们在原来 webpack.config.js 的基础上再新增两个文件

+

webpack.dev.js 开发环境配置文件
开发环境主要实现的是热更新,不要压缩代码,完整的 sourceMap
webpack.prod.js生产环境配置文件
生产环境主要实现的是压缩代码、提取 css 文件、合理的 sourceMap 、分割代码
需要安装以下模块:

+
1
npm i -D  webpack-merge copy-webpack-plugin optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin
+
    +
  • webpack-merge 合并配置
  • +
  • copy-webpack-plugin 拷贝静态资源
  • +
  • optimize-css-assets-webpack-plugin 压缩 css
  • +
  • uglifyjs-webpack-plugin 压缩js
  • +
+
+

webpack mode 设置 production 的时候会自动压缩 js 代码。
原则上不需要引入 uglifyjs-webpack-plugin 进行重复工作。
但是 optimize-css-assets-webpack-plugin 压缩 css 的同时会破坏原有的 js 压缩,所以这里我们引入 uglifyjs 进行压缩

+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// webpack.config.js
const path = require('path')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const vueLoaderPlugin = require('vue-loader/lib/plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const devMode = process.argv.indexOf('--mode=production') === -1;
module.exports = {
entry:{
main:path.resolve(__dirname,'../src/main.js')
},
output:{
path:path.resolve(__dirname,'../dist'),
filename:'js/[name].[hash:8].js',
chunkFilename:'js/[name].[hash:8].js'
},
module:{
rules:[
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:['@babel/preset-env']
}
},
exclude:/node_modules/
},
{
test:/\.vue$/,
use:['cache-loader','thread-loader',{
loader:'vue-loader',
options:{
compilerOptions:{
preserveWhitespace:false
}
}
}]
},
{
test:/\.css$/,
use:[{
loader: devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
options:{
publicPath:"../dist/css/",
hmr:devMode
}
},'css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
}]
},
{
test:/\.less$/,
use:[{
loader:devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
options:{
publicPath:"../dist/css/",
hmr:devMode
}
},'css-loader','less-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
}]
},
{
test:/\.(jep?g|png|gif)$/,
use:{
loader:'url-loader',
options:{
limit:10240,
fallback:{
loader:'file-loader',
options:{
name:'img/[name].[hash:8].[ext]'
}
}
}
}
},
{
test:/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
use:{
loader:'url-loader',
options:{
limit:10240,
fallback:{
loader:'file-loader',
options:{
name:'media/[name].[hash:8].[ext]'
}
}
}
}
},
{
test:/\.(woff2?|eot|ttf|otf)(\?.*)?$/i,
use:{
loader:'url-loader',
options:{
limit:10240,
fallback:{
loader:'file-loader',
options:{
name:'media/[name].[hash:8].[ext]'
}
}
}
}
}
]
},
resolve:{
alias:{
'vue$':'vue/dist/vue.runtime.esm.js',
' @':path.resolve(__dirname,'../src')
},
extensions:['*','.js','.json','.vue']
},
plugins:[
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html')
}),
new vueLoaderPlugin(),
new MiniCssExtractPlugin({
filename: devMode ? '[name].css' : '[name].[hash].css',
chunkFilename: devMode ? '[id].css' : '[id].[hash].css'
})
]
}
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// webpack.dev.js
const Webpack = require('webpack')
const webpackConfig = require('./webpack.config.js')
const WebpackMerge = require('webpack-merge')

module.exports = WebpackMerge(webpackConfig,{
mode:'development',
devtool:'cheap-module-eval-source-map',
devServer:{
port:3000,
hot:true,
contentBase:'../dist'
},
plugins:[
new Webpack.HotModuleReplacementPlugin()
]
})
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// webpack.prod.js
const path = require('path')
const webpackConfig = require('./webpack.config.js')
const WebpackMerge = require('webpack-merge')

const CopyWebpackPlugin = require('copy-webpack-plugin')
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

module.exports = WebpackMerge(webpackConfig,{
mode:'production',
devtool:'cheap-module-source-map',
plugins:[
new CopyWebpackPlugin([{
from:path.resolve(__dirname,'../public'),
to:path.resolve(__dirname,'../dist')
}]),
],
optimization:{
minimizer:[
new UglifyJsPlugin({//压缩js
cache:true,
parallel:true,
sourceMap:true
}),
new OptimizeCssAssetsPlugin({})
],
splitChunks:{
chunks:'all',
cacheGroups:{
libs: {
name: "chunk-libs",
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: "initial" // 只打包初始时依赖的第三方
}
}
}
}
})
+ +

优化webpack配置

看到这里你或许有些累了,但是要想获取更好的offer,更高的薪水,下面必须继续深入

+

优化配置对我们来说非常有实际意义,这实际关系到你打包出来文件的大小,打包的速度等。
具体优化可以分为以下几点:

+

优化打包速度

+

构建速度指的是我们每次修改代码后热更新的速度以及发布前打包文件的速度。

+
+

合理的配置 mode 参数与 devtool 参数

devtool 可设置的值
mode 可设置 development production 两个参数

+

如果没有设置, webpack4 会将 mode 的默认值设置为 production

+
    +
  • production :将 process.env.NODE_ENV 的值设置为 production ,启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPluginUglifyJsPlugin,会进行 tree shaking (去除无用代码)和 uglifyjs (代码压缩混淆)
  • +
  • development :将 process.env.NODE_ENV 的值设置为 development ,启用 NamedChunksPluginNamedModulesPlugin
  • +
+

缩小文件的搜索范围(配置include exclude alias noParse extensions)

    +
  • alias 当我们代码中出现 import ‘vue’时, webpack会采用向上递归搜索的方式去node_modules 目录下找。为了减少搜索范围我们可以直接告诉webpack去哪个路径下查找。也就是别名(alias)的配置。
  • +
  • include exclude 同样配置include exclude也可以减少webpack loader的搜索转换时间。
  • +
  • noParse 当我们代码中使用到 import jq from 'jquery' 时, webpack 会去解析 jq 这个库是否有依赖其他的包。但是我们对类似 jquery 这类依赖库,一般会认为不会引用其他的包(特殊除外,自行判断)。增加 noParse 属性,告诉 webpack 不必解析,以此增加打包速度。
  • +
  • extensions webpack 会根据 extensions 定义的后缀查找文件(频率较高的文件类型优先写在前面)
  • +
+

配图

+

使用HappyPack开启多进程Loader转换

+

webpack 构建过程中,实际上耗费时间大多数用在 loader 解析转换以及代码的压缩中。日常开发中我们需要使用 Loaderjscss ,图片,字体等文件做转换操作,并且转换的文件数据量也是非常大。由于js单线程的特性使得这些转换操作不能并发处理文件,而是需要一个个文件进行处理。 HappyPack 的基本原理是将这部分任务分解到多个子进程中去并行处理,子进程处理完成后把结果发送到主进程中,从而减少总的构建时间

+
+
1
npm i -D happypack
+ +

happypack

+

使用 webpack-parallel-uglify-plugin 增强代码压缩

上面对于 loader 转换已经做优化,那么下面还有另一个难点就是优化代码的压缩时间。

+
1
npm i -D webpack-parallel-uglify-plugin
+

webpack-parallel-uglify-plugin

+

抽离第三方模块

+

对于开发项目中不经常会变更的静态依赖文件。类似于我们的 elementUivue 全家桶等等。因为很少会变更,所以我们不希望这些依赖要被集成到每一次的构建逻辑中去。 这样做的好处是每次更改我本地代码的文件的时候, webpack 只需要打包我项目本身的文件代码,而不会再去编译第三方库。以后只要我们不升级第三方包的时候,那么 webpack 就不会对这些库去打包,这样可以快速的提高打包的速度。

+
+

这里我们使用 webpack 内置的 DllPlugin DllReferencePlugin 进行抽离

+

在与 webpack 配置文件同级目录下新建 webpack.dll.config.js
代码如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// webpack.dll.config.js
const path = require("path");
const webpack = require("webpack");
module.exports = {
// 你想要打包的模块的数组
entry: {
vendor: ['vue','element-ui']
},
output: {
path: path.resolve(__dirname, 'static/js'), // 打包后文件输出的位置
filename: '[name].dll.js',
library: '[name]_library'
// 这里需要和webpack.DllPlugin中的`name: '[name]_library',`保持一致。
},
plugins: [
new webpack.DllPlugin({
path: path.resolve(__dirname, '[name]-manifest.json'),
name: '[name]_library',
context: __dirname
})
]
};
+

package.json 中配置如下命令

+
1
"dll": "webpack --config build/webpack.dll.config.js"
+ +

接下来在我们的 webpack.config.js 中增加以下代码

+
1
2
3
4
5
6
7
8
9
10
11
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./vendor-manifest.json')
}),
new CopyWebpackPlugin([ // 拷贝生成的文件到dist目录 这样每次不必手动去cv
{from: 'static', to:'static'}
]),
]
};
+

执行

+
1
npm run dll
+ +

会发现生成了我们需要的集合第三地方
代码的 vendor.dll.js
我们需要在html文件中手动引入这个js文件

+
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<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">
<title>老yuan</title>
<script src="static/js/vendor.dll.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
+

这样如果我们没有更新第三方依赖包,就不必 npm run dll 。直接执行 npm run dev npm run build 的时候会发现我们的打包速度明显有所提升。因为我们已经通过 dllPlugin 将第三方依赖包抽离出来了。

+

配置缓存

+

我们每次执行构建都会把所有的文件都重复编译一遍,这样的重复工作是否可以被缓存下来呢,答案是可以的,目前大部分 loader 都提供了 cache 配置项。比如在 babel-loader 中,可以通过设置cacheDirectory 来开启缓存,babel-loader?cacheDirectory=true 就会将每次的编译结果写进硬盘文件(默认是在项目根目录下的 node_modules/.cache/babel-loader 目录内,当然你也可以自定义)
但如果 loader 不支持缓存呢?我们也有方法,我们可以通过 cache-loader ,它所做的事情很简单,就是 babel-loader 开启 cache 后做的事情,将 loader 的编译结果写入硬盘缓存。再次构建会先比较一下,如果文件较之前的没有发生变化则会直接使用缓存。使用方法如官方 demo 所示,在一些性能开销较大的 loader 之前添加此 loader 即可

+
+
1
npm i -D cache-loader
+

cache-loader

+

优化打包文件体积

打包的速度我们是进行了优化,但是打包后的文件体积却是十分大,造成了页面加载缓慢,浪费流量等,接下来让我们从文件体积上继续优化

+

引入webpack-bundle-analyzer分析打包后的文件

webpack-bundle-analyzer 将打包后的内容束展示为方便交互的直观树状图,让我们知道我们所构建包中真正引入的内容

+
1
npm i -D webpack-bundle-analyzer
+

webpack-bundle-analyzer

+

接下来在 package.json 里配置启动命令

+
1
"analyz": "NODE_ENV=production npm_config_report=true npm run build"
+

windows 请安装 npm i -D cross-env

+
1
"analyz": "cross-env NODE_ENV=production npm_config_report=true npm run build"
+

接下来 npm run analyz 浏览器会自动打开文件依赖图的网页

+

externals

+

按照官方文档的解释,如果我们想引用一个库,但是又不想让 webpack 打包,并且又不影响我们在程序中以 CMDAMD 或者 window/global 全局等方式进行使用,那就可以通过配置 Externals 。这个功能主要是用在创建一个库的时候用的,但是也可以在我们项目开发中充分使用Externals 的方式,我们将这些不需要打包的静态资源从构建逻辑中剔除出去,而使用 CDN的方式,去引用它们。
有时我们希望我们通过 script 引入的库,如用 CDN 的方式引入的 jquery ,我们在使用时,依旧用 require 的方式来使用,但是却不希望 webpack 将它又编译进文件中。这里官网案例已经足够清晰明了,大家有兴趣可以点击了解
webpack
官网案例如下

+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
<script
src="https://code.jquery.com/jquery-3.1.0.js"
integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
crossorigin="anonymous">
</script>
module.exports = {
//...
externals: {
jquery: 'jQuery'
}
};
import $ from 'jquery';
$('.my-element').animate(/* ... */);
+

Tree-shaking

+

这里单独提一下 tree-shaking ,是因为这里有个坑。 tree-shaking 的主要作用是用来清除代码中无用的部分。目前在 webpack4 我们设置 modeproduction 的时候已经自动开启了tree-shaking 。但是要想使其生效,生成的代码必须是ES6模块。不能使用其它类型的模块如 CommonJS 之流。如果使用 Babel 的话,这里有一个小问题,因为 Babel 的预案( preset )默认会将任何模块类型都转译成 CommonJS 类型。修正这个问题也很简单,在 .babelrc 文件或在 webpack.config.js 文件中设置 modules:false 就好了

+
+
1
2
3
4
5
6
7
8
9
10
// .babelrc
{
"presets": [
["@babel/preset-env",
{
"modules": false
}
]
]
}
+

或者

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.config.js

module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', { modules: false }]
}
},
exclude: /(node_modules)/
}
]
}
+ +

经历过上面两个系列的洗礼,到现在我们成为了一名合格的 webpack 配置工程师。但是光拧螺丝,自身的可替代性还是很高,下面我们将深入 webpack 的原理中去

+

手写webpack系列

经历过上面两个部分,我们已经可以熟练的运用相关的 loaderplugin 对我们的代码进行转换、解析。接下来我们自己手动实现 loaderplugin ,使其在平时的开发中获得更多的乐趣。

+

手写 webpack loader

+

loader 从本质上来说其实就是一个 node 模块。相当于一台榨汁机( loader )将相关类型的文件代码( code )给它。根据我们设置的规则,经过它的一系列加工后还给我们加工好的果汁( code )。

+
+

loader 编写原则

    +
  • 单一原则: 每个 Loader 只做一件事;
  • +
  • 链式调用: Webpack 会按顺序链式调用每个 Loader
  • +
  • 统一原则: 遵循 Webpack 制定的设计规则和结构,输入与输出均为字符串,各个 Loader 完全独立,即插即用;
  • +
+

在日常开发环境中,为了方便调试我们往往会加入许多 console 打印。但是我们不希望在生产环境中存在打印的值。那么这里我们自己实现一个 loader 去除代码中的 console

+
+

知识点普及之 ASTAST 通俗的来说,假设我们有一个文件 a.js ,我们对 a.js 里面的1000行进行一些操作处理,比如为所有的 await 增加 try catch ,以及其他操作,但是 a.js 里面的代码本质上来说就是一堆字符串。那我们怎么办呢,那就是转换为带标记信息的对象(抽象语法树)我们方便进行增删改查。这个带标记的对象(抽象语法树)就是AST。这里推荐一篇不错的AST文章 AST快速入门

+
+
1
npm i -D @babel/parser @babel/traverse @babel/generator @babel/types
+
    +
  • @babel/parser 将源代码解析成 AST
  • +
  • @babel/traverse 对 AST 节点进行递归遍历,生成一个便于操作、转换的 path 对象
  • +
  • @babel/generator 将 AST 解码生成 js 代码
  • +
  • @babel/types 通过该模块对具体的 AST 节点进行进行增、删、改、查
  • +
+

新建 drop-console.js

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const t = require('@babel/types')
module.exports=function(source){
const ast = parser.parse(source,{ sourceType: 'module'})
traverse(ast,{
CallExpression(path){
if(t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.object, {name: "console"})){
path.remove()
}
}
})
const output = generator(ast, {}, source);
return output.code
}
+

如何使用

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const path = require('path')
module.exports = {
mode:'development',
entry:path.resolve(__dirname,'index.js'),
output:{
filename:'[name].[contenthash].js',
path:path.resolve(__dirname,'dist')
},
module:{
rules:[{
test:/\.js$/,
use:path.resolve(__dirname,'drop-console.js')
}
]
}
}
+
+

实际上在 webpack4 中已经集成了去除 console 功能,在 minimizer 中可配置 去除console
附上官网 如何编写一个loader

+
+

手写webpack plugin

+

Webpack 运行的生命周期中会广播出许多事件, Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
通俗来说:一盘美味的 盐豆炒鸡蛋 需要经历烧油 炒制 调味到最后的装盘等过程,而 plugin 相当于可以监控每个环节并进行操作,比如可以写一个少放胡椒粉 plugin ,监控 webpack 暴露出的生命周期事件(调味),在调味的时候执行少放胡椒粉操作。
那么它与 loader 的区别是什么呢?上面我们也提到了 loader 的单一原则, loader 只能一件事,比如说 less-loader ,只能解析 less 文件, plugin 则是针对整个流程执行广泛的任务。

+
+

一个基本的 plugin 插件结构如下

+
1
2
3
4
5
6
7
8
9
10
11
12
class firstPlugin {
constructor (options) {
console.log('firstPlugin options', options)
}
apply (compiler) {
compiler.plugin('done', compilation => {
console.log('firstPlugin')
))
}
}

module.exports = firstPlugin
+
+

compilercompilation 是什么?

+
+
    +
  • compiler 对象包含了 Webpack 环境所有的的配置信息。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 optionsloaderplugin 。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。
  • +
  • compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation ,从而生成一组新的编译资源。 compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。
  • +
+

compilercompilation 的区别在于

+
    +
  • compiler 代表了整个 webpack 从启动到关闭的生命周期,而 compilation 只是代表了一次新的编译过程
  • +
  • compilercompilation 暴露出许多钩子,我们可以根据实际需求的场景进行自定义处理
  • +
+

compiler钩子文档
compilation钩子文档

+

下面我们手动开发一个简单的需求,在生成打包文件之前自动生成一个关于打包出文件的大小信息
新建一个 webpack-firstPlugin.js

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class firstPlugin{
constructor(options){
this.options = options
}
apply(compiler){
compiler.plugin('emit',(compilation,callback)=>{
let str = ''
for (let filename in compilation.assets){
str += `文件:${filename} 大小${compilation.assets[filename]['size']()}\n`
}
// 通过compilation.assets可以获取打包后静态资源信息,同样也可以写入资源
compilation.assets['fileSize.md'] = {
source:function(){
return str
},
size:function(){
return str.length
}
}
callback()
})
}
}
module.exports = firstPlugin
+

如何使用

+
1
2
3
4
5
6
7
8
const path = require('path')
const firstPlugin = require('webpack-firstPlugin.js')
module.exports = {
// 省略其他代码
plugins:[
new firstPlugin()
]
}
+

执行 npm run build 即可看到在 dist 文件夹中生成了一个包含打包文件信息的 fileSize.md

+
+

上面两个 loaderplugin 案例只是一个引导,实际开发需求中的 loaderplugin 要考虑的方面很多,建议大家自己多动手尝试一下。

+
+

附上官网 如何编写一个plugin

+

webpack5.0的时代

无论是前端框架还是构建工具的更新速度远远超乎了我们的想象,前几年的 jquery 一把梭的时代一去不复返。我们要拥抱的是不断更新迭代的 vuereactnodeserverlessdockerk8s ….
不甘落后的 webpack 也已经在近日发布了 webpack 5.0.0 beta 10 版本。在之前作者也曾提过 webpack5.0 旨在减少配置的复杂度,使其更容易上手( webpack4 的时候也说了这句话),以及一些性能上的提升

+
    +
  • 使用持久化缓存提高构建性能;
  • +
  • 使用更好的算法和默认值改进长期缓存(long-term caching);
  • +
  • 清理内部结构而不引入任何破坏性的变化;
  • +
  • 引入一些breaking changes,以便尽可能长的使用v5版本。
  • +
+

目前来看,维护者的更新很频繁,相信用不了多久webpack5.0将会拥抱大众。感兴趣的同学可以先安装beta版本尝尝鲜。不过在此之前建议大家先对webpack4进行一番掌握,这样后面的路才会越来越好走。

+

更多阅读

webpack中文
webpackjs
4W字长文带你深度解锁Webpack系列(上)

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:2020年了,再不会webpack敲得代码就不香了(近万字实战)【转载】

+

文章作者:

+

发布时间:2020年03月24日 - 20:20

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/03/24/webpack-learning-1/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/04/03/source-code-react-diff/index.html b/en/2020/04/03/source-code-react-diff/index.html new file mode 100644 index 0000000000..16c0a513a9 --- /dev/null +++ b/en/2020/04/03/source-code-react-diff/index.html @@ -0,0 +1,700 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + React 源码剖析系列 - 不可思议的 react diff【转载】 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ React 源码剖析系列 - 不可思议的 react diff【转载】 +

+ + +
+ + + + +
+ + +

目前,前端领域中 React 势头正盛,使用者众多却少有能够深入剖析内部实现机制和原理。本系列文章希望通过剖析 React 源码,理解其内部的实现原理,知其然更要知其所以然。

+

React diff 作为 Virtual DOM 的加速器,其算法上的改进优化是 React 整个界面渲染的基础,以及性能提高的保障,同时也是 React 源码中最神秘、最不可思议的部分,本文从源码入手,深入剖析 React diff 的不可思议之处。

+

阅读本文需要对 React 有一定的了解,如果你不知何为 React,请详读 React 官方文档
如果你对 React diff 存在些许疑惑,或者你对算法优化感兴趣,那么本文值得阅读和讨论。

+ +

前言

React 中最值得称道的部分莫过于 Virtual DOM 与 diff 的完美结合,特别是其高效的 diff 算法,让用户可以无需顾忌性能问题而”任性自由”的刷新页面,让开发者也可以无需关心 Virtual DOM 背后的运作原理,因为 React diff 会帮助我们计算出 Virtual DOM 中真正变化的部分,并只针对该部分进行实际 DOM 操作,而非重新渲染整个页面,从而保证了每次操作更新后页面的高效渲染,因此 Virtual DOM 与 diff 是保证 React 性能口碑的幕后推手。

+

行文至此,可能会有读者质疑:React 无非就是引入 diff 这一概念,且 diff 算法也并非其首创,何必吹嘘的如此天花乱坠呢?

+

其实,正是因为 diff 算法的普识度高,就更应该认可 React 针对 diff 算法优化所做的努力与贡献,更能体现 React 开发者们的魅力与智慧!

+

传统 diff 算法

计算一棵树形结构转换成另一棵树形结构的最少操作,是一个复杂且值得研究的问题。传统 diff 算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n^3),其中 n 是树中节点的总数。O(n^3) 到底有多可怕,这意味着如果要展示1000个节点,就要依次执行上十亿次的比较。这种指数型的性能消耗对于前端渲染场景来说代价太高了!现今的 CPU 每秒钟能执行大约30亿条指令,即便是最高效的实现,也不可能在一秒内计算出差异情况。

+

如果 React 只是单纯的引入 diff 算法而没有任何的优化改进,那么其效率是远远无法满足前端渲染所要求的性能。

+

因此,想要将 diff 思想引入 Virtual DOM,就需要设计一种稳定高效的 diff 算法,而 React 做到了!

+

那么,React diff 到底是如何实现的呢?

+

详解 React diff

传统 diff 算法的复杂度为 O(n^3),显然这是无法满足性能要求的。React 通过制定大胆的策略,将 O(n^3) 复杂度的问题转换成 O(n) 复杂度的问题。

+

diff 策略

    +
  1. Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
  2. +
  3. 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
  4. +
  5. 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。
  6. +
+

基于以上三个前提策略,React 分别对 tree diff、component diff 以及 element diff 进行算法优化,事实也证明这三个前提策略是合理且准确的,它保证了整体界面构建的性能。

+
    +
  • tree diff
  • +
  • component diff
  • +
  • element diff
  • +
+
+

本文中源码 ReactMultiChild.js

+
+

tree diff

基于策略一,React 对树的算法进行了简洁明了的优化,即对树进行分层比较,两棵树只会对同一层次的节点进行比较。

+

既然 DOM 节点跨层级的移动操作少到可以忽略不计,针对这一现象,React 通过 updateDepth 对 Virtual DOM 树进行层级控制,只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。

+

https://pic1.zhimg.com/0c08dbb6b1e0745780de4d208ad51d34_r.jpg

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
updateChildren: function(nextNestedChildrenElements, transaction, context) {
updateDepth++;
var errorThrown = true;
try {
this._updateChildren(nextNestedChildrenElements, transaction, context);
errorThrown = false;
} finally {
updateDepth--;
if (!updateDepth) {
if (errorThrown) {
clearQueue();
} else {
processQueue();
}
}
}
}
+

分析至此,大部分人可能都存在这样的疑问:如果出现了 DOM 节点跨层级的移动操作,React diff 会有怎样的表现呢?是的,对此我也好奇不已,不如试验一番。

+

如下图,A 节点(包括其子节点)整个被移动到 D 节点下,由于 React 只会简单的考虑同层级节点的位置变换,而对于不同层级的节点,只有创建和删除操作。当根节点发现子节点中 A 消失了,就会直接销毁 A;当 D 发现多了一个子节点 A,则会创建新的 A(包括子节点)作为其子节点。此时,React diff 的执行情况:create A -> create B -> create C -> delete A

+

由此可发现,当出现节点跨层级移动时,并不会出现想象中的移动操作,而是以 A 为根节点的树被整个重新创建,这是一种影响 React 性能的操作,因此 React 官方建议不要进行 DOM 节点跨层级的操作

+
+

注意:在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。例如,可以通过 CSS 隐藏或显示节点,而不是真的移除或添加 DOM 节点。

+
+

https://pic2.zhimg.com/d712a73769688afe1ef1a055391d99ed_r.jpg

+

component diff

React 是基于组件构建应用的,对于组件间的比较所采取的策略也是简洁高效。

+
    +
  • 如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。
  • +
  • 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。
  • +
  • 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。
  • +
+

如下图,当 component D 改变为 component G 时,即使这两个 component 结构相似,一旦 React 判断 D 和 G 是不同类型的组件,就不会比较二者的结构,而是直接删除 component D,重新创建 component G 以及其子节点。虽然当两个 component 是不同类型但结构相似时,React diff 会影响性能,但正如 React 官方博客所言:不同类型的 component 是很少存在相似 DOM tree 的机会,因此这种极端因素很难在实现开发过程中造成重大影响的。

+

https://pic1.zhimg.com/52654992aba15fc90e2dac8b2387d0c4_r.jpg

+

element diff

当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)

+
    +
  • INSERT_MARKUP ,新的 component 类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作。

    +
  • +
  • MOVE_EXISTING ,在老集合有新 component 类型,且 element 是可更新的类型,generateComponentChildren 已调用 receiveComponent,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点。

    +
  • +
  • REMOVE_NODE ,老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作。

    +
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function enqueueInsertMarkup(parentInst, markup, toIndex) {
updateQueue.push({
parentInst: parentInst,
parentNode: null,
type: ReactMultiChildUpdateTypes.INSERT_MARKUP,
markupIndex: markupQueue.push(markup) - 1,
content: null,
fromIndex: null,
toIndex: toIndex,
});
}

function enqueueMove(parentInst, fromIndex, toIndex) {
updateQueue.push({
parentInst: parentInst,
parentNode: null,
type: ReactMultiChildUpdateTypes.MOVE_EXISTING,
markupIndex: null,
content: null,
fromIndex: fromIndex,
toIndex: toIndex,
});
}

function enqueueRemove(parentInst, fromIndex) {
updateQueue.push({
parentInst: parentInst,
parentNode: null,
type: ReactMultiChildUpdateTypes.REMOVE_NODE,
markupIndex: null,
content: null,
fromIndex: fromIndex,
toIndex: null,
});
}
+

如下图,老集合中包含节点:A、B、C、D,更新后的新集合中包含节点:B、A、D、C,此时新老集合进行 diff 差异化对比,发现 B != A,则创建并插入 B 至新集合,删除老集合 A;以此类推,创建并插入 A、D 和 C,删除 B、C 和 D。

+

https://pic2.zhimg.com/7541670c089b84c59b84e9438e92a8e9_r.jpg

+

React 发现这类操作繁琐冗余,因为这些都是相同的节点,但由于位置发生变化,导致需要进行繁杂低效的删除、创建操作,其实只要对这些节点进行位置移动即可。

+

针对这一现象,React 提出优化策略:允许开发者对同一层级的同组子节点,添加唯一 key 进行区分,虽然只是小小的改动,性能上却发生了翻天覆地的变化!

+

新老集合所包含的节点,如下图所示,新老集合进行 diff 差异化对比,通过 key 发现新老集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将老集合中节点的位置进行移动,更新为新集合中节点的位置,此时 React 给出的 diff 结果为:B、D 不做任何操作,A、C 进行移动操作,即可。

+

https://pic4.zhimg.com/c0aa97d996de5e7f1069e97ca3accfeb_r.jpg

+

那么,如此高效的 diff 到底是如何运作的呢?让我们通过源码进行详细分析。

+

首先对新集合的节点进行循环遍历,for (name in nextChildren),通过唯一 key 可以判断新老集合中是否存在相同的节点,if (prevChild === nextChild),如果存在相同节点,则进行移动操作,但在移动前需要将当前节点在老集合中的位置与 lastIndex 进行比较,if (child._mountIndex < lastIndex),则进行节点移动操作,否则不执行该操作。这是一种顺序优化手段,lastIndex 一直在更新,表示访问过的节点在老集合中最右的位置(即最大的位置),如果新集合中当前访问的节点比 lastIndex 大,说明当前访问节点在老集合中就比上一个节点位置靠后,则该节点不会影响其他节点的位置,因此不用添加到差异队列中,即不执行移动操作,只有当访问的节点比 lastIndex 小时,才需要进行移动操作。

+

以上图为例,可以更为清晰直观的描述 diff 的差异对比过程:

+
    +
  • 从新集合中取得 B,判断老集合中存在相同节点 B,通过对比节点位置判断是否进行移动操作,B 在老集合中的位置 B._mountIndex = 1,此时 lastIndex = 0,不满足 child._mountIndex < lastIndex 的条件,因此不对 B 进行移动操作;更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),其中 prevChild._mountIndex 表示 B 在老集合中的位置,则 lastIndex = 1,并将 B 的位置更新为新集合中的位置prevChild._mountIndex = nextIndex,此时新集合中 B._mountIndex = 0,nextIndex++ 进入下一个节点的判断。

    +
  • +
  • 从新集合中取得 A,判断老集合中存在相同节点 A,通过对比节点位置判断是否进行移动操作,A 在老集合中的位置 A._mountIndex = 0,此时 lastIndex = 1,满足 child._mountIndex < lastIndex的条件,因此对 A 进行移动操作enqueueMove(this, child._mountIndex, toIndex),其中 toIndex 其实就是 nextIndex,表示 A 需要移动到的位置;更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),则 lastIndex = 1,并将 A 的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex,此时新集合中A._mountIndex = 1,nextIndex++ 进入下一个节点的判断。

    +
  • +
  • 从新集合中取得 D,判断老集合中存在相同节点 D,通过对比节点位置判断是否进行移动操作,D 在老集合中的位置 D._mountIndex = 3,此时 lastIndex = 1,不满足 child._mountIndex < lastIndex的条件,因此不对 D 进行移动操作;更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),则 lastIndex = 3,并将 D 的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex,此时新集合中D._mountIndex = 2,nextIndex++ 进入下一个节点的判断。

    +
  • +
  • 从新集合中取得 C,判断老集合中存在相同节点 C,通过对比节点位置判断是否进行移动操作,C 在老集合中的位置 C._mountIndex = 2,此时 lastIndex = 3,满足 child._mountIndex < lastIndex 的条件,因此对 C 进行移动操作 enqueueMove(this, child._mountIndex, toIndex);更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),则 lastIndex = 3,并将 C 的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex,此时新集合中 C._mountIndex = 3,nextIndex++ 进入下一个节点的判断,由于 C 已经是最后一个节点,因此 diff 到此完成。

    +
  • +
+

以上主要分析新老集合中存在相同节点但位置不同时,对节点进行位置移动的情况,如果新集合中有新加入的节点且老集合存在需要删除的节点,那么 React diff 又是如何对比运作的呢?

+

以下图为例:

+
    +
  • 从新集合中取得 B,判断老集合中存在相同节点 B,由于 B 在老集合中的位置 B._mountIndex = 1,此时lastIndex = 0,因此不对 B 进行移动操作;更新 lastIndex = 1,并将 B 的位置更新为新集合中的位置B._mountIndex = 0,nextIndex++进入下一个节点的判断。

    +
  • +
  • 从新集合中取得 E,判断老集合中不存在相同节点 E,则创建新节点 E;更新 lastIndex = 1,并将 E 的位置更新为新集合中的位置,nextIndex++进入下一个节点的判断。

    +
  • +
  • 从新集合中取得 C,判断老集合中存在相同节点 C,由于 C 在老集合中的位置C._mountIndex = 2,lastIndex = 1,此时 C._mountIndex > lastIndex,因此不对 C 进行移动操作;更新 lastIndex = 2,并将 C 的位置更新为新集合中的位置,nextIndex++ 进入下一个节点的判断。

    +
  • +
  • 从新集合中取得 A,判断老集合中存在相同节点 A,由于 A 在老集合中的位置A._mountIndex = 0,lastIndex = 2,此时 A._mountIndex < lastIndex,因此对 A 进行移动操作;更新 lastIndex = 2,并将 A 的位置更新为新集合中的位置,nextIndex++ 进入下一个节点的判断。

    +
  • +
  • 当完成新集合中所有节点 diff 时,最后还需要对老集合进行循环遍历,判断是否存在新集合中没有但老集合中仍存在的节点,发现存在这样的节点 D,因此删除节点 D,到此 diff 全部完成。

    +
  • +
+

https://pic1.zhimg.com/7b9beae0cf0a5bc8c2e82d00c43d1c90_r.jpg

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
_updateChildren: function(nextNestedChildrenElements, transaction, context) {
var prevChildren = this._renderedChildren;
var nextChildren = this._reconcilerUpdateChildren(
prevChildren, nextNestedChildrenElements, transaction, context
);
if (!nextChildren && !prevChildren) {
return;
}
var name;
var lastIndex = 0;
var nextIndex = 0;
for (name in nextChildren) {
if (!nextChildren.hasOwnProperty(name)) {
continue;
}
var prevChild = prevChildren && prevChildren[name];
var nextChild = nextChildren[name];
if (prevChild === nextChild) {
// 移动节点
this.moveChild(prevChild, nextIndex, lastIndex);
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
prevChild._mountIndex = nextIndex;
} else {
if (prevChild) {
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
// 删除节点
this._unmountChild(prevChild);
}
// 初始化并创建节点
this._mountChildAtIndex(
nextChild, nextIndex, transaction, context
);
}
nextIndex++;
}
for (name in prevChildren) {
if (prevChildren.hasOwnProperty(name) &&
!(nextChildren && nextChildren.hasOwnProperty(name))) {
this._unmountChild(prevChildren[name]);
}
}
this._renderedChildren = nextChildren;
},
// 移动节点
moveChild: function(child, toIndex, lastIndex) {
if (child._mountIndex < lastIndex) {
this.prepareToManageChildren();
enqueueMove(this, child._mountIndex, toIndex);
}
},
// 创建节点
createChild: function(child, mountImage) {
this.prepareToManageChildren();
enqueueInsertMarkup(this, mountImage, child._mountIndex);
},
// 删除节点
removeChild: function(child) {
this.prepareToManageChildren();
enqueueRemove(this, child._mountIndex);
},

_unmountChild: function(child) {
this.removeChild(child);
child._mountIndex = null;
},

_mountChildAtIndex: function(
child,
index,
transaction,
context) {
var mountImage = ReactReconciler.mountComponent(
child,
transaction,
this,
this._nativeContainerInfo,
context
);
child._mountIndex = index;
this.createChild(child, mountImage);
},
+ +

当然,React diff 还是存在些许不足与待优化的地方,如下图所示,若新集合的节点更新为:D、A、B、C,与老集合对比只有 D 节点移动,而 A、B、C 仍然保持原有的顺序,理论上 diff 应该只需对 D 执行移动操作,然而由于 D 在老集合的位置是最大的,导致其他节点的 _mountIndex < lastIndex,造成 D 没有执行移动操作,而是 A、B、C 全部移动到 D 节点后面的现象。

+

在此,读者们可以讨论思考:如何优化上述问题?

+
+

建议:在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。

+
+

https://pic2.zhimg.com/1b8dac5b9b3e4452dec8d5447d7717ad_r.jpg

+

总结

    +
  • React 通过制定大胆的 diff 策略,将 O(n3) 复杂度的问题转换成 O(n) 复杂度的问题;
  • +
  • React 通过分层求异的策略,对 tree diff 进行算法优化;
  • +
  • React 通过相同类生成相似树形结构,不同类生成不同树形结构的策略,对 component diff 进行算法优化;
  • +
  • React 通过设置唯一 key的策略,对 element diff 进行算法优化;
  • +
  • 建议,在开发组件时,保持稳定的 DOM 结构会有助于性能的提升;
  • +
  • 建议,在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。
  • +
+

参考资料

A Survey on Tree Edit Distance and Related Problems
Reconciliation

+

如果本文能够为你解决些许关于 React diff 算法的疑惑,请点个赞吧!

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:React 源码剖析系列 - 不可思议的 react diff【转载】

+

文章作者:

+

发布时间:2020年04月03日 - 12:38

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/04/03/source-code-react-diff/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/04/07/Electron-Offline-Build/index.html b/en/2020/04/07/Electron-Offline-Build/index.html new file mode 100644 index 0000000000..c98c89889a --- /dev/null +++ b/en/2020/04/07/Electron-Offline-Build/index.html @@ -0,0 +1,586 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 国内配置Electron开发环境的正确方式【转载】 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 国内配置Electron开发环境的正确方式【转载】 +

+ + +
+ + + + +
+ + +

前言

最近在做electron相关开发,疲于网络环境的种种限制,找遍了互联网相关资料,终于找到一篇比较全面的文章,怕丢了转过来。

+ +

转载

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:国内配置Electron开发环境的正确方式【转载】

+

文章作者:

+

发布时间:2020年04月07日 - 18:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/04/07/Electron-Offline-Build/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/04/07/webpack-uglifyjsplugin/index.html b/en/2020/04/07/webpack-uglifyjsplugin/index.html new file mode 100644 index 0000000000..3d9baac6d3 --- /dev/null +++ b/en/2020/04/07/webpack-uglifyjsplugin/index.html @@ -0,0 +1,623 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Webpack 打包时利用 UglifyJsPlugin 去掉comments 、console 、和 debugger | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Webpack 打包时利用 UglifyJsPlugin 去掉comments 、console 、和 debugger +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在检查公司前端代码质量时,经常会在sonar检查到有漏洞级别的问题特别多,但是打开查看详情的时候发现又特别低级,都是些console没去掉啊,debugger没去掉啊,之前都是看到了就提醒一下,但是屡禁不止,那么有没有什么好的办法能够从源头上避免这个问题呢,果然这个问题不止我一个人遇到,网上还是有大神给出了解决方案,那就是 webpack 打包时可以引入 UglifyJsPlugin 来解决这个扰人的问题,而且屏蔽了人为的因素,使用感觉是极好的,所以,写个帖子把网上学来的资料整理一下。

+ + +

UglifyJsPlugin

关于 UglifyJsPlugin 的介绍,在 webpack 的官网上有这样一段描述

+
+

ℹ️ webpack =< v3.0.0 currently contains v0.4.6 of this plugin under webpack.optimize.UglifyJsPlugin as an alias. For usage of the latest version (v1.0.0), please follow the instructions below. Aliasing v1.0.0 as webpack.optimize.UglifyJsPlugin is scheduled for webpack v4.0.0

+
+

简单来说就是在 webpack3.0 之前,引入了这个插件的0.4.6版本,并用 webpack.optimize.UglifyJsPlugin 作为它的别名, 在 webpack4.0 中引入了插件的 1.0.0 版本,用了同样的名称作为别名,用的时候请注意。

+

那么“这个插件”是什么呢, webpack 的官网也告诉我们了,那就是UglifyJS

+
+

A JavaScript parser, mangler/compressor and beautifier toolkit for ES6+.

+
+

翻译过来就是“一个用于ES6+的JavaScript解析器、(榨汁机:翻译的挺有意思)/压缩机和美化工具。”

+

再简单点说 uglifyJsPlugin 用来对js文件进行压缩,减小js文件的大小。其会拖慢webpack的编译速度,建议开发环境时关闭,生产环境再将其打开。

+

要用它的的话记得先安装

+
1
npm i -D uglifyjs-webpack-plugin
+

安装完成就可以用了

+
1
2
3
4
5
6
7
8
// webpack.config.js
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
plugins: [
new UglifyJsPlugin()
]
}
+ +

当然他有很多的选项,具体想了解可以去 webpack 官网,或者去插件官网都可以找到uglifyjs-webpack-plugin

+

当然知道你们懒的去看了,心里肯定也在说“直接给我个现成的配置他不香么?”,别急嘛,这就给你们

+
1
2
3
4
5
6
7
8
9
10
11
12
new UglifyJsPlugin({
//删除注释
output:{
comments:false
},
//删除console 和 debugger 删除警告
compress:{
warnings:false,
drop_debugger:true,
drop_console:true
}
})
+

当然版本间可能会有些差别,但是option是不变的,有调整的话各位自行调整一下。
公司用的 vue-cli3 所以这里再给出个 vue-cli3 默认配置文件下的写法

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// vue.config.js

configureWebpack:{
optimization: {
minimizer: [
new UglifyJsPlugin({
uglifyOptions: {
// 删除注释
output:{
comments:false
},
// 删除console debugger 删除警告
compress: {
warnings: false,
drop_console: true,//console
drop_debugger: false,
pure_funcs: ['console.log']//移除console
}
}
})
]
}
}
+ +

问题收集

    +
  1. 运行出现报错 UglifyJs
  2. +
+
+

Q: DefaultsError: warnings is not a supported option

+
+
+

A: 降低版本(使用 “uglifyjs-webpack-plugin”: “^1.1.1”),打包正常,效果达到

+
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Webpack 打包时利用 UglifyJsPlugin 去掉comments 、console 、和 debugger

+

文章作者:

+

发布时间:2020年04月07日 - 11:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/04/07/webpack-uglifyjsplugin/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/04/15/node-sass-slow-problem/index.html b/en/2020/04/15/node-sass-slow-problem/index.html new file mode 100644 index 0000000000..3820601ea2 --- /dev/null +++ b/en/2020/04/15/node-sass-slow-problem/index.html @@ -0,0 +1,601 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 如何解决npm安装node-sass依赖慢的情况 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 如何解决npm安装node-sass依赖慢的情况 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

现代前端开发不用点预编译css都不好意思说自己咋写样式,虽说这么说有点夸张,但是确实是一个普遍现象吧,我们在开发中常见到的预编译css的话主要是sass和less,那么今天的主角就是其中的sass,可以说sass让人又爱又恨,爱呢是喜欢她赋予了css函数的特性,恨呢就是用的时候实在是麻烦,所以有的人就投奔了less的怀抱,他俩很像,但是有的项目已经用了sass那也没办法,嫁鸡随鸡,但是日子还得过,想办法过的好一点嘛。

+

其实这里说到sass用起来麻烦主要还是在安装依赖的时候,想必用过的人都知道我说的是啥,原来只能是不停的重试,最近在知乎上看到篇文章,聪明的解决了这个问题,所以赶紧学习记录下来,学会了就是自己的。

+ + +

问题

在使用sass时要安装node-sass包,但是这个npm包安装不尽慢的要命,下载下来之后还要进行编译,编译环境不合适或者网速不好的时候,光为了这个包的使用花上个把小时绝对正常,记得第一次折腾他用了小半天的时间。

+

那么有没有什么方法可以解决这个问题呢?

+

有痛点就会有人想办法解决,但是包名已经占用了,想用的话还是要有点配置的代价,但总好过编译和下载

+

解决方案

首先我这里假设你是知道 yarn 这个工具的,对的,接下来我们要用 yarn 进行安装,但是安装的不是 node-sass ,而是一个叫做 node-sass-install 的这个包, 安装他的话就不用在安装 node-sass 这个包了,通常来说安装这个包不会超过10s , 当然网速不好的话超过10s了也别怪我,总之要比装 node-sass 要快上很多,命令如下:

+
1
yarn add node-sass-install
+

是不是很简单,当然如果你觉得为了安装 node-sass 还要再装个 yarn (鄙视你居然不用 yarn ),那你也可以用 npm 安装,命令稍有不同,长了一点

+
1
2
npm install node-sass-install -D
npx node-sass-install
+

比 yarn 多了一条命令,那么他为啥这么神奇呢,且听我分析一下

+

原理

这个 node-sass-install 其实只是在 package.json 的 dependencies 中做了一些配置,如下:

+
1
2
3
4
5
{
"dependencies":{
"node-sass":"npm:dart-sass@latest"
}
}
+

上面这个配置的意思是,当你安装 node-sass-install 的时候,会依赖并下载 dart-sass, 然后起了个别名叫做 node-sass。偷梁换柱,狸猫换太子了,哈哈。
所以的所以呢,如果你在项目中用到 sass 的话建议你尝试一下新方法,说不定更香呢~

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:如何解决npm安装node-sass依赖慢的情况

+

文章作者:

+

发布时间:2020年04月15日 - 23:24

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/04/15/node-sass-slow-problem/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/04/15/talk-about-full-stack-big-fd/index.html b/en/2020/04/15/talk-about-full-stack-big-fd/index.html new file mode 100644 index 0000000000..861a9ef6a7 --- /dev/null +++ b/en/2020/04/15/talk-about-full-stack-big-fd/index.html @@ -0,0 +1,598 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端全栈和大前端有啥区别 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端全栈和大前端有啥区别 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

知乎真是个好地方,关注好自己喜欢的话题,经常会推送一些好的话题到你面前,这不刚刚就看到了一个人提问

+
+

前端全栈和大前端有啥区别

+
+

以前只听说过全栈,听说过大前端,但是没听说过前端全栈,对于前两个我自己也有一些理解,但从不敢说自己理解的对,带着强烈的好奇心我打开了这个问题,也看到了一位前端大神‘狼叔’的回复,感觉还是很到位的,也让我重新验证了自己的理解,所以就将狼叔的解答与我自己的认识相结合一下,做个记录。

+ + +

狼叔说

狼叔在解答这个疑问之前直接上了一张图,图我贴在下面供大家看一下
JicTqe.jpg
这么一张图实际上已经胜过千言了,但是为了方便大家理解,狼叔还是在下面对图进行了解释

+
+

前端全栈:分node全栈和其他语言全栈,比如ror搞全栈是最早的,其他php、java也有,不过纯前端的不过,在react,angular之前搞后台还是可以的。
所以前端全栈,我理解是等同于node全栈的。node本身是做后端的,但在前端工程化和BFF领域大放异彩,所以node全栈涵盖了前端的方方面面,是比较合理的解释。
大前端:更泛化的概念,移动互联网时代开启后,hybrid曾经很火,基于h5和webview做跨端,确实是很理想的做法,但复杂交互搞不定,机器性能网络等是硬伤,所以后来出现了rn和weex,整体还是前端写法,所以hybrid里前端也是占了一定的开发,结合之前前端和node的关系,综合3者:1)app里的前端,2)前端,2)node全栈,统称为大前端。这里的”大“含义是可以做的事儿的范围更广,触达前后端移动端,对前端职责有明显提升。随着技术发展,基于electron的桌面开发也日进流程,ott和iot等领域采用js也愈来愈多,所以只要和用户直接触达的端采用了前端技术开发的都涵盖在大前端范畴内。

+
+

原帖地址

+

我说

之前我的概念里前端扩展开来再进一步的话分两个方向,一个叫全栈另一个叫全端,‘栈’的话是纵向的,简单理解的话就是一款产品一个人能从设计到前端实现后端实现运维等一个人搞定,那么他就可以称之为全栈,狭义一点理解,就是前端后端都会,那么这里所说的前端全栈,我理解是更方便前端掌握的一些后端技能如node、php、ror等,而不是java这种后端技能,所以叫前端全栈,我想这里大部分指的都是node作为后端;

+

那么我理解的全端是什么呢

+

‘端’我理解为容器,任何跟用户直接接触的技术都是端的技术,早期的pc端,后来的手机端,手机端里又出现了h5、hybrid、native这么几种,后来又出现了小程序之类的容器,pc端也出现了如electron等客户端技术,那么随着物联网智能家居的出现,更多的端出现了,就如狼叔说的OTT和IOT领域也成了端,大前端的大是指范围广,属于用户直达所承载内容容器的都是端。

+

总的来说跟狼叔的理解一致,我个人也努力在往全栈全端发展,不过真的好难还有很长的路要走。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端全栈和大前端有啥区别

+

文章作者:

+

发布时间:2020年04月15日 - 15:59

+

最后更新:2020年04月15日 - 15:51

+

原始链接:https://blog.lifesli.com/2020/04/15/talk-about-full-stack-big-fd/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/04/16/0-vuecli-3-component/index.html b/en/2020/04/16/0-vuecli-3-component/index.html new file mode 100644 index 0000000000..dd904419c1 --- /dev/null +++ b/en/2020/04/16/0-vuecli-3-component/index.html @@ -0,0 +1,678 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 从零开始:Vue cli3 库模式搭建组件库并发布到npm【转载】 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 从零开始:Vue cli3 库模式搭建组件库并发布到npm【转载】 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

市面上目前已有各种各样的UI组件库,比如 Element 和 iView,他们的强大毋庸置疑。但是我们面临的情况是需求越来越复杂,当它们不能再满足我们需求的时候,这个时候就有必要开发一套属于自己团队的组件库了。

+

所以本文的目的就是让读者能通过此文,小能做一个简单的插件供人使用,大能架构和维护一个组件库不在话下。

+

以下一个简单的颜色选择器插件vColorPicker讲述从开发到上线到npm的流程。

+

vColorPicker 插件 DEMO

+ +

一、技术栈

如何通过新版脚手架创建项目,这里就不提了,自行看官方文档。

+
    +
  • Vue-cli3: 新版脚手架的库模式,可以让我们很轻松的创建打包一个库
  • +
  • npm:组件库将存放在npm
  • +
  • webpack:修改配置需要一点 webapck 的知识。
  • +
+

二、大纲

想要搭建一个组件库,我们必须先要有一个大概的思路。

+
    +
  1. 规划目录结构
  2. +
  3. 配置项目以支持目录结构
  4. +
  5. 编写组件
  6. +
  7. 编写示例
  8. +
  9. 配置使用库模式打包编译
  10. +
  11. 发布到npm
  12. +
+

三、规划目录结构

1、创建项目

在指定目录中使用命令创建一个默认的项目,或者根据自己需要自己选择。

+
1
$ vue create .
+

2、调整目录

我们需要一个目录存放组件,一个目录存放示例,按照以下方式对目录进行改造。

+
1
2
3
4
5
6
.
...
|-- examples // 原 src 目录,改成 examples 用作示例展示
|-- packages // 新增 packages 用于编写存放组件
...
.
+

四、配置项目以支持新的目录结构

我们通过上一步的目录改造后,会遇到两个问题。

+
    +
  1. src目录更名为examples,导致项目无法运行
  2. +
  3. 新增packages目录,该目录未加入webpack编译
  4. +
+

注:cli3 提供一个可选的 vue.config.js 配置文件。如果这个文件存在则他会被自动加载,所有的对项目和webpack的配置,都在这个文件中。

+

1、重新配置入口,修改配置中的 pages 选项

新版 Vue CLI 支持使用 vue.config.js 中的 pages 选项构建一个多页面的应用。

+

这里使用 pages 修改入口到 examples

+
1
2
3
4
5
6
7
8
9
10
module.exports = {
// 修改 src 为 examples
pages: {
index: {
entry: 'examples/main.js',
template: 'public/index.html',
filename: 'index.html'
}
}
}
+

2、支持对 packages 目录的处理,修改配置中的 chainWebpack 选项

packages 是我们新增的一个目录,默认是不被 webpack 处理的,所以需要添加配置对该目录的支持。

+

chainWebpack 是一个函数,会接收一个基于 webpack-chain 的 ChainableConfig 实例。允许对内部的 webpack 配置进行更细粒度的修改。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module.exports = {
// 修改 src 为 examples
pages: {
index: {
entry: 'examples/main.js',
template: 'public/index.html',
filename: 'index.html'
}
},
// 扩展 webpack 配置,使 packages 加入编译
chainWebpack: config => {
config.module
.rule('js')
.include
.add('packages')
.end()
.use('babel')
.loader('babel-loader')
.tap(options => {
// 修改它的选项...
return options
})
}
}
+

链式操作
webpack-chain

+

五、编写组件

以上我们已配置好对新目录架构的支持,接下来我们尝试编写组件。以下我们以一个已发布到 npm 的小插件作为示例。
GitHub - 颜色选择器:vcolorpicker

+

1. 创建一个新组件

    +
  1. 在 packages 目录下,所有的单个组件都以文件夹的形式存储,所有这里创建一个目录 color-picker/
  2. +
  3. 在 color-picker/ 目录下创建 src/ 目录存储组件源码
  4. +
  5. 在 /color-picker 目录下创建 index.js 文件对外提供对组件的引用。
    修改 /packages/color-picker/index.js文件,对外提供引用。
  6. +
+
1
2
3
4
5
6
7
8
9
10
11
12
// ./packages/color-picker/index.js

// 导入组件,组件必须声明 name
import colorPicker from './src/color-picker.vue'

// 为组件提供 install 安装方法,供按需引入
colorPicker = function (Vue) {
Vue.component(colorPicker.name, colorPicker)
}

// 默认导出组件
export default colorPicker
+

2. 整合所有的组件,对外导出,即一个完整的组件库

修改 /packages/index.js 文件,对整个组件库进行导出。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ./packages/index.js

// 导入颜色选择器组件
import colorPicker from './color-picker'

// 存储组件列表
const components = [
colorPicker
]

// 定义 install 方法,接收 Vue 作为参数。如果使用 use 注册插件,则所有的组件都将被注册
const install = function (Vue) {
// 判断是否安装
if (install.installed) return
// 遍历注册全局组件
components.map(component => Vue.component(component.name, component))
}

// 判断是否是直接引入文件
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}

export default {
// 导出的对象必须具有 install,才能被 Vue.use() 方法安装
install,
// 以下是具体的组件列表
colorPicker
}
+

六、编写示例

1、在示例中导入组件库

1
2
3
4
5
6
7
8
9
10
11
12
13
import Vue from 'vue'
import App from './App.vue'

// 导入组件库
import ColorPicker from './../packages/index'
// 注册组件库
Vue.use(ColorPicker)

Vue.config.productionTip = false

new Vue({
render: h => h(App)
}).$mount('#app')
+

2、在示例中使用组件库中的组件

在上一步用使用 Vue.use() 全局注册后,即可在任意页面直接使用了,而不需另外引入。当然也可以按需引入。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<colorPicker v-model="color" v-on:change="headleChangeColor"></colorPicker>
</template>

<script>
export default {
data () {
return {
color: '#ff0000'
}
},
methods: {
headleChangeColor () {
console.log('颜色改变')
}
}
}
</script>
+

七、发布到 npm,方便直接在项目中引用

到此为止我们一个完整的组件库已经开发完成了,接下来就是发布到 npm 以供后期使用。

+

1、package.js 中新增一条编译为库的命令

在库模式中,Vue是外置的,这意味着即使在代码中引入了 Vue,打包后的文件也是不包含Vue的。

+

Vue Cli3 构建目标:库

+

以下我们在 scripts 中新增一条命令

+
    +
  • –target: 构建目标,默认为应用模式。这里修改为 lib 启用库模式。
  • +
  • –dest : 输出目录,默认 dist。这里我们改成 lib
  • +
  • [entry]: 最后一个参数为入口文件,默认为 src/App.vue。这里我们指定编译 packages/ 组件库目录。
    1
    2
    3
    4
    "script": {
    // ...
    "lib": "vue-cli-service build --target lib --name vcolorpicker --dest lib packages/index.js"
    }
  • +
+

执行编译库命令

+
1
$ npm run lib
+

2、配置 package.json 文件中发布到 npm 的字段

    +
  • name: 包名,该名字是唯一的。可在 npm 官网搜索名字,如果存在则需换个名字。
  • +
  • version: 版本号,每次发布至 npm 需要修改版本号,不能和历史版本号相同。
  • +
  • description: 描述。
  • +
  • main: 入口文件,该字段需指向我们最终编译后的包文件。
  • +
  • keyword:关键字,以空格分离希望用户最终搜索的词。
  • +
  • author:作者
  • +
  • private:是否私有,需要修改为 false 才能发布到 npm
  • +
  • license: 开源协议
  • +
+

以下为参考设置

+
1
2
3
4
5
6
7
8
{
"name": "vcolorpicker",
"version": "0.1.5",
"description": "基于 Vue 的颜色选择器",
"main": "lib/vcolorpicker.umd.min.js",
"keyword": "vcolorpicker colorpicker color-picker",
"private": false
}
+

3、添加 .npmignore 文件,设置忽略发布文件

我们发布到 npm 中,只有编译后的 lib 目录、package.json、README.md才是需要被发布的。所以我们需要设置忽略目录和文件。

+

和 .gitignore 的语法一样,具体需要提交什么文件,看各自的实际情况。

+
1
2
3
4
5
6
7
8
9
# 忽略目录
examples/
packages/
public/

# 忽略指定文件
vue.config.js
babel.config.js
*.map
+

4、登录到 npm

首先需要到 npm 上注册一个账号,注册过程略。

+

如果配置了淘宝镜像,先设置回npm镜像:

+
1
$ npm config set registry http://registry.npmjs.org
+

然后在终端执行登录命令,输入用户名、密码、邮箱即可登录。

+
1
$ npm login
+ +

5、发布到 npm

执行发布命令,发布组件到 npm

+
1
$ npm publish
+ +

6、发布成功

发布成功后稍等几分钟,即可在 npm 官网搜索到。以下是刚提交的 vcolorpicker

+

7、使用新发布的组件库

安装

+
1
$ npm install vcolorpicker -S
+

使用

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 在 main.js 引入并注册
import vcolorpicker from 'vcolorpicker'
Vue.use(vcolorpicker)

// 在组件中使用
<template>
<colorPicker v-model="color" />
</template>
<script>
export default {
data () {
return {
color: '#ff0000'
}
}
}
</script>
+

暂时没有做包含多个组件的时候的按需加载,以后研究了再补充。

+

八、项目地址

Github 地址:https://github.com/zuley/vue-color-picker
npm 地址:https://www.npmjs.com/package/vcolorpicker
DEMO 演示:http://vue-color-picker.rxshc.com

+

九、参考文章

从零开始搭建Vue组件库 VV-UI
Vue插件开发
组件基础

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:从零开始:Vue cli3 库模式搭建组件库并发布到npm【转载】

+

文章作者:

+

发布时间:2020年04月16日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/04/16/0-vuecli-3-component/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/04/16/what-is-npx/index.html b/en/2020/04/16/what-is-npx/index.html new file mode 100644 index 0000000000..184cb03b63 --- /dev/null +++ b/en/2020/04/16/what-is-npx/index.html @@ -0,0 +1,648 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + npx是什么 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ npx是什么 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在阅读一些文章时,经常看到介绍命令的时候用到了 npx 关键字,之前知道有 node 有 npm 但是很少用到 npx ,npx是个啥呢?好奇上网上找了些资料学习了一下,写篇学习笔记记录一下。

+ +

npx 起源

我从阮一峰的博客中看到介绍 npx 的文章,开头的一句话说明了他诞生的日子。

+
+

npm 从5.2版开始,增加了 npx 命令。

+
+

为了验证阮一峰这里介绍的正确性我特意下了对应的npm版本验证了一下确实如此,而且在网上找到了另一位大佬司徒正美(大佬走好)博客中也对 npx 做了介绍

+
+

最近我在更新 npm 5.2.0 的时候发现会买一送一,自动安装了 npx。

+
+

由此,我可以肯定的告诉大家,npx是npm在5.2.0之后版本推出的一个工具,那么他是干嘛用的呢?

+

npx 作用

想要了解一个技术,最好的途经是他的官网,于是我到网上找到了 npx 在 github 上的仓库,地址如下
npx仓库,其中对 npx 有这样一段介绍

+
+

DESCRIPTION
Executes either from a local node_modules/.bin, or from a central cache, installing any packages needed in order for to run.
By default, npx will check whether exists in $PATH, or in the local project binaries, and execute that. If is not found, it will be installed prior to execution.
Unless a –package option is specified, npx will try to guess the name of the binary to invoke depending on the specifier provided. All package specifiers understood by npm may be used with npx, including git specifiers, remote tarballs, local directories, or scoped packages.
If a full specifier is included, or if –package is used, npx will always use a freshly-installed, temporary version of the package. This can also be forced with the –ignore-existing flag.

+
+

上面这一大段英文我想大家一定看了就头疼,所以为了大家不那么头疼,可以看一下下面我翻译的内容,如果有翻译不对的地方,还请指正。

+
+

解释
执行 command 命令,无论从本地(我理解为项目目录)node_modules/.bin 或者从全局缓存中, 安装所需执行的任何包。
默认情况下,npx将检查 command 是否存在于 $PATH 中,或者在本地项目二进制文件中,并执行该命令。
如果没有找到 command ,它将在执行之前安装。
除非指定了 —package 选项,否则npx将根据提供的说明符猜测要调用的二进制文件的名称。
npm可以理解的所有包说明符都可以与npx一起使用,包括git说明符、远程tarball、本地目录或作用域包。
如果包含完整的说明符,或者使用 ——package 选项,npx将始终使用新安装的包的临时版本。
这也可以用 ——ignore-existing 标记强制执行。

+
+

上面这段机翻简直让人无法理解,所以我又去大佬博客下看了下他们的解释

+
+

npx 想要解决的主要问题,就是调用项目内部安装的模块。 – 阮一峰
根据 zkat/npx 的描述,npx 会帮你执行依赖包里的二进制文件。 – 司徒正美

+
+

司徒大大文章写的太简洁了,不过他还是举了例子,我进行了一下精简,如下

+
1
./node_modules/.bin/webpack -v // => npx webpack -v
+

简单来说就是找包执行命令的时候不再关注他在哪了,直接就可以用了。

+

阮一峰老师的文章更像是官网的翻译加理解

+
+

npx 的原理很简单,就是运行的时候,会到node_modules/.bin路径和环境变量$PATH里面,检查命令是否存在。
由于 npx 会检查环境变量$PATH,所以系统命令也可以调用。

+
+

另外阮一峰老师还介绍了一个临时安装命令使用的场景,我理解为对上面英语介绍倒数第二句的理解

+

避免全局安装模块

除了调用项目内部模块,npx 还能避免全局安装的模块。比如,create-react-app这个模块是全局安装,npx 可以运行它,而且不进行全局安装。

+
1
2
$ npx create-react-app my-react-app

+

上面代码运行时,npx 将create-react-app下载到一个临时目录,使用以后再删除。所以,以后再次执行上面的命令,会重新下载create-react-app。

+

下载全局模块时,npx 允许指定版本。

+
1
2
$ npx uglify-js@3.1.0 main.js -o ./dist/main.js

+

上面代码指定使用 3.1.0 版本的uglify-js压缩脚本。

+

注意,只要 npx 后面的模块无法在本地发现,就会下载同名模块。比如,本地没有安装http-server模块,下面的命令会自动下载该模块,在当前目录启动一个 Web 服务。

+
1
$ npx http-server
+ +

然后阮老师还对官网最后一句话做了解释

+

–no-install 参数和–ignore-existing 参数

如果想让 npx 强制使用本地模块,不下载远程模块,可以使用–no-install参数。如果本地不存在该模块,就会报错。

+
1
2
$ npx --no-install http-server

+

反过来,如果忽略本地的同名模块,强制安装使用远程模块,可以使用–ignore-existing参数。比如,本地已经全局安装了create-react-app,但还是想使用远程模块,就用这个参数。

+
1
2
$ npx --ignore-existing create-react-app my-react-app

+

然后对于官网上的 example 阮老师也挑了重点的做了介绍,如选择指定的 node 版本

+
1
2
$ npx node@0.12.8 -v
v0.12.8
+

上面命令会使用 0.12.8 版本的 Node 执行脚本。原理是从 npm 下载这个版本的 node,使用后再删掉。
某些场景下,这个方法用来切换 Node 版本,要比 nvm 那样的版本管理器方便一些。

+

更多参数

-p 参数

-p参数用于指定 npx 所要安装的模块,所以上一节的命令可以写成下面这样。

+
1
2
$ npx -p node@0.12.8 node -v 
v0.12.8
+

上面命令先指定安装node@0.12.8,然后再执行node -v命令。
-p参数对于需要安装多个模块的场景很有用。

+
1
$ npx -p lolcatjs -p cowsay [command]
+

-c 参数

如果 npx 安装多个模块,默认情况下,所执行的命令之中,只有第一个可执行项会使用 npx 安装的模块,后面的可执行项还是会交给 Shell 解释。

+
1
2
$ npx -p lolcatjs -p cowsay 'cowsay hello | lolcatjs'
# 报错
+

上面代码中,cowsay hello | lolcatjs 执行时会报错,原因是第一项 cowsay 由 npx 解释,而第二项命令localcatjs由 Shell 解释,但是lolcatjs并没有全局安装,所以报错。

+

-c参数可以将所有命令都用 npx 解释。有了它,下面代码就可以正常执行了

+
1
2
$ npx -p lolcatjs -p cowsay -c 'cowsay hello | lolcatjs'

+

-c参数的另一个作用,是将环境变量带入所要执行的命令。举例来说,npm 提供当前项目的一些环境变量,可以用下面的命令查看。

+
1
2
$ npm run env | grep npm_

+

-c参数可以把这些 npm 的环境变量带入 npx 命令。

+
1
2
$ npx -c 'echo "$npm_package_name"'

+

上面代码会输出当前项目的项目名。

+

执行 GitHub 源码

npx 还可以执行 GitHub 上面的模块源码。

+
1
2
3
4
5
# 执行 Gist 代码
$ npx https://gist.github.com/zkat/4bc19503fe9e9309e2bfaa2c58074d32

# 执行仓库代码
$ npx github:piuccio/cowsay hello
+

注意,远程代码必须是一个模块,即必须包含package.json和入口脚本。

+

最后

因为还没有实际使用过的经验,所以更多的内容从其他大佬哪里白嫖来的知识,做个笔记以观后效。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:npx是什么

+

文章作者:

+

发布时间:2020年04月16日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/04/16/what-is-npx/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/05/14/css3-pointer-events/index.html b/en/2020/05/14/css3-pointer-events/index.html new file mode 100644 index 0000000000..c6432b8cb4 --- /dev/null +++ b/en/2020/05/14/css3-pointer-events/index.html @@ -0,0 +1,607 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CSS3新属性-pointer-events | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ CSS3新属性-pointer-events +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

今天在看一个叫做Avue的框架组件时看到了一个叫做“全局水印”的组件,功能呢就是给当前页面加上一个水印遮罩,就像下图这样:
Y08cbd.png
因为工作中有这样的场景,所以我很好奇看了一下他的代码,他的水印设置为了fixed定位,并且z-index设置的很高,但是它具有穿透性,最终我发现其中起作用的是一个叫做“pointer-events”的css3新属性,于是为了搞清楚它的作用,我在网上做了一番搜索,并把自己的认识整理一下。

+ +

介绍

MDN上是这样介绍这个属性的 传送门

+
+

pointer-events CSS 属性指定在什么情况下 (如果有) 某个特定的图形元素可以成为鼠标事件的 target。

+
+

它有如下这些个属性值:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Keyword values */
pointer-events: auto;
pointer-events: none;
pointer-events: visiblePainted; /* SVG only */
pointer-events: visibleFill; /* SVG only */
pointer-events: visibleStroke; /* SVG only */
pointer-events: visible; /* SVG only */
pointer-events: painted; /* SVG only */
pointer-events: fill; /* SVG only */
pointer-events: stroke; /* SVG only */
pointer-events: all; /* SVG only */

/* Global values */
pointer-events: inherit;
pointer-events: initial;
pointer-events: unset;

+

这里有一个特别的值,它进行了专门的介绍,也正是因为这个原因,水印的功能才得以实现。

+
+

除了指示该元素不是鼠标事件的目标之外,值none表示鼠标事件“穿透”该元素并且指定该元素“下面”的任何东西。

+
+

由上可知,当我们将pointer-events值设置为none时,它就有了穿透的特性,也就是说你“看得见它,却摸不到它”,而且也不具备鼠标指针的特性,这不正是水印想要的效果嘛,完美~;

+

接着往下看我们可以知道它的初始值是auto,并且可以继承
Y0J0XD.png

+

水印功能就这样实现了是不是简单实用,那么是不是就没有缺点呢,当然要硬说有的话那就是兼容性的问题吧。

+

因为这个css3的属性是新出的,自然ie9及以下自然就不支持了,而且ie不支持的范围要到ie11以下,所以ie你为啥不去死呢。。。

+

Y0YEjO.png

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:CSS3新属性-pointer-events

+

文章作者:

+

发布时间:2020年05月14日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/05/14/css3-pointer-events/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/05/28/Prettier-Setting/index.html b/en/2020/05/28/Prettier-Setting/index.html new file mode 100644 index 0000000000..78f2049968 --- /dev/null +++ b/en/2020/05/28/Prettier-Setting/index.html @@ -0,0 +1,585 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Prettier格式化配置 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Prettier格式化配置 +

+ + +
+ + + + +
+ + +

前言

最近在做代码风格统一,用到了Prettier,暂时先记录一下现在的配置信息,回头抽时间写篇总结文。

+ +

一、技术栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
// 使能每一种语言默认格式化规则
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[less]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},

/* prettier的配置 */
"prettier.printWidth": 100, // 超过最大值换行
"prettier.tabWidth": 4, // 缩进字节数
"prettier.useTabs": false, // 缩进不使用tab,使用空格
"prettier.semi": true, // 句尾添加分号
"prettier.singleQuote": true, // 使用单引号代替双引号
"prettier.proseWrap": "preserve", // 默认值。因为使用了一些折行敏感型的渲染器(如GitHub comment)而按照markdown文本样式进行折行
"prettier.arrowParens": "avoid", // (x) => {} 箭头函数参数只有一个时是否要有小括号。avoid:省略括号
"prettier.bracketSpacing": true, // 在对象,数组括号与文字之间加空格 "{ foo: bar }"
"prettier.disableLanguages": ["vue"], // 不格式化vue文件,vue文件的格式化单独设置
"prettier.endOfLine": "auto", // 结尾是 \n \r \n\r auto
"prettier.eslintIntegration": false, //不让prettier使用eslint的代码格式进行校验
"prettier.htmlWhitespaceSensitivity": "ignore",
"prettier.ignorePath": ".prettierignore", // 不使用prettier格式化的文件填写在项目的.prettierignore文件中
"prettier.jsxBracketSameLine": false, // 在jsx中把'>' 是否单独放一行
"prettier.jsxSingleQuote": false, // 在jsx中使用单引号代替双引号
"prettier.parser": "babylon", // 格式化的解析器,默认是babylon
"prettier.requireConfig": false, // Require a 'prettierconfig' to format prettier
"prettier.stylelintIntegration": false, //不让prettier使用stylelint的代码格式进行校验
"prettier.trailingComma": "es5", // 在对象或数组最后一个元素后面是否加逗号(在ES5中加尾逗号)
"prettier.tslintIntegration": false // 不让prettier使用tslint的代码格式进行校验
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Prettier格式化配置

+

文章作者:

+

发布时间:2020年05月28日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/05/28/Prettier-Setting/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2020/07/19/rpm\345\221\275\344\273\244/index.html" "b/en/2020/07/19/rpm\345\221\275\344\273\244/index.html" new file mode 100644 index 0000000000..459b5ac254 --- /dev/null +++ "b/en/2020/07/19/rpm\345\221\275\344\273\244/index.html" @@ -0,0 +1,663 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + rpm命令 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ rpm命令 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

安装

sudo rpm -ivh [lz4-1.8.3-1.el7.x86_64.rpm]

+

卸载

sudo rpm -e [postgresql14-server-14.4-1PGDG.rhel7.x86_64]

+

查询功能

命令格式 rpm {-q|–query} [select-options] [query-options]

+

  RPM的查询功能是极为强大,是极为重要的功能之一;举几个常用的例子,更为详细的具体的,请参考#man rpm

+

1、对系统中已安装软件的查询

+

1)查询系统已安装的软件

+

  语法:rpm -q 软件名

+

  举例:[root@localhost beinan]# rpm -q gaim

+

  gaim-1.3.0-1.fc4   

+
   查看系统中所有已经安装的包,要加 -a 参数 ;
+
+

  [root@localhost RPMS]# rpm -qa

+

  如果分页查看,再加一个管道 |和more命令;

+

  [root@localhost RPMS]# rpm -qa |more

+

  在所有已经安装的软件包中查找某个软件,比如说 gaim ;可以用 grep 抽取出来;

+

  [root@localhost RPMS]# rpm -qa |grep gaim

+

  上面这条的功能和 rpm -q gaim 输出的结果是一样的;

+

2)查询一个已经安装的文件属于哪个软件包

+

  语法 rpm -qf 文件名

+

  注:文件名所在的绝对路径要指出

+

  举例:

+

  [root@localhost RPMS]# rpm -qf /usr/lib/libacl.la

+

  libacl-devel-2.2.23-8

+

3)查询已安装软件包都安装到何处

+

  语法:rpm -ql 软件名 或 rpm rpmquery -ql 软件名

+

  举例:

+

  [root@localhost RPMS]# rpm -ql lynx

+

  [root@localhost RPMS]# rpmquery -ql lynx

+

4)查询一个已安装软件包的信息

+

  语法格式: rpm -qi 软件名

+

  举例:

+

  [root@localhost RPMS]# rpm -qi lynx

+

5)查看一下已安装软件的配置文件

+

  语法格式:rpm -qc 软件名

+

  举例:

+

  [root@localhost RPMS]# rpm -qc lynx

+

6)查看一个已经安装软件的文档安装位置

+

  语法格式: rpm -qd 软件名

+

  举例:

+

  [root@localhost RPMS]# rpm -qd lynx

+

7)查看一下已安装软件所依赖的软件包及文件

+

  语法格式: rpm -qR 软件名

+

  举例:

+

  [root@localhost beinan]# rpm -qR rpm-python

+

  查询已安装软件的总结:对于一个软件包已经安装,我们可以把一系列的参数组合起来用;比如 rpm -qil ;比如:

+

  [root@localhost RPMS]# rpm -qil lynx

+

2、对于未安装的软件包的查看:

+

  查看的前提是您有一个.rpm 的文件,也就是说对既有软件file.rpm的查看等;

+

1)查看一个软件包的用途、版本等信息;

+

  语法: rpm -qpi file.rpm

+

  举例:

+

  [root@localhost RPMS]# rpm -qpi lynx-2.8.5-23.i386.rpm

+

2)查看一件软件包所包含的文件;

+

  语法: rpm -qpl file.rpm

+

  举例:

+

  [root@localhost RPMS]# rpm -qpl lynx-2.8.5-23.i386.rpm

+

3)查看软件包的文档所在的位置;

+

  语法: rpm -qpd file.rpm

+

  举例:

+

  [root@localhost RPMS]# rpm -qpd lynx-2.8.5-23.i386.rpm

+

4)查看一个软件包的配置文件;

+

  语法: rpm -qpc file.rpm

+

  举例:

+

  [root@localhost RPMS]# rpm -qpc lynx-2.8.5-23.i386.rpm

+

5)查看一个软件包的依赖关系

+

  语法: rpm -qpR file.rpm

+

  举例:

+

  [root@localhost archives]# rpm -qpR yumex_0.42-3.0.fc4_noarch.rpm

+

  /bin/bash

+

  /usr/bin/python

+

  config(yumex) = 0.42-3.0.fc4

+

  pygtk2

+

  pygtk2-libglade

+

  rpmlib(CompressedFileNames) <= 3.0.4-1

+

  rpmlib(PayloadFilesHavePrefix) <= 4.0-1

+

  usermode

+

  yum >= 2.3.2

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:rpm命令

+

文章作者:

+

发布时间:2020年07月19日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/07/19/rpm%E5%91%BD%E4%BB%A4/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2020/08/02/Linux\347\233\256\345\275\225\350\257\264\346\230\216/index.html" "b/en/2020/08/02/Linux\347\233\256\345\275\225\350\257\264\346\230\216/index.html" new file mode 100644 index 0000000000..891866058c --- /dev/null +++ "b/en/2020/08/02/Linux\347\233\256\345\275\225\350\257\264\346\230\216/index.html" @@ -0,0 +1,610 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Linux目录说明 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Linux目录说明 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

/bin:

bin是Binary的缩写, 这个目录存放着最经常使用的命令。

+

/boot:

这里存放的是启动Linux时使用的一些核心文件,包括一些连接文件以及镜像文件。

+

/dev :

dev是Device(设备)的缩写, 该目录下存放的是Linux的外部设备,在Linux中访问设备的方式和访问文件的方式是相同的。

+

/etc:

这个目录用来存放所有的系统管理所需要的配置文件和子目录。

+

/home:

用户的主目录,在Linux中,每个用户都有一个自己的目录,一般该目录名是以用户的账号命名的。

+

/lib:

这个目录里存放着系统最基本的动态连接共享库,其作用类似于Windows里的DLL文件。几乎所有的应用程序都需要用到这些共享库。

+

/lost+found:

这个目录一般情况下是空的,当系统非法关机后,这里就存放了一些文件。

+

/media linux

系统会自动识别一些设备,例如U盘、光驱等等,当识别后,linux会把识别的设备挂载到这个目录下。

+

/mnt:

系统提供该目录是为了让用户临时挂载别的文件系统的,我们可以将光驱挂载在/mnt/上,然后进入该目录就可以查看光驱里的内容了。

+

/opt:

这是给主机额外安装软件所摆放的目录。比如你安装一个ORACLE数据库则就可以放到这个目录下。默认是空的。

+

/proc:

这个目录是一个虚拟的目录,它是系统内存的映射,我们可以通过直接访问这个目录来获取系统信息。
这个目录的内容不在硬盘上而是在内存里,我们也可以直接修改里面的某些文件,比如可以通过下面的命令来屏蔽主机的ping命令,使别人无法ping你的机器:

+

/root:

该目录为系统管理员,也称作超级权限者的用户主目录。

+

/sbin:

s就是Super User的意思,这里存放的是系统管理员使用的系统管理程序。

+

/selinux:

这个目录是Redhat/CentOS所特有的目录,Selinux是一个安全机制,类似于windows的防火墙,但是这套机制比较复杂,这个目录就是存放selinux相关的文件的。

+

/srv:

该目录存放一些服务启动之后需要提取的数据。

+

/sys:

这是linux2.6内核的一个很大的变化。该目录下安装了2.6内核中新出现的一个文件系统 sysfs 。

+

sysfs文件系统集成了下面3种文件系统的信息:针对进程信息的proc文件系统、针对设备的devfs文件系统以及针对伪终端的devpts文件系统。
该文件系统是内核设备树的一个直观反映。

+

当一个内核对象被创建的时候,对应的文件和目录也在内核对象子系统中被创建。

+

/tmp:

这个目录是用来存放一些临时文件的。

+

/usr:

这是一个非常重要的目录,用户的很多应用程序和文件都放在这个目录下,类似与windows下的program files目录。

+

/usr/bin:

系统用户使用的应用程序。

+

/usr/sbin:

超级用户使用的比较高级的管理程序和系统守护程序。

+

/usr/src:

内核源代码默认的放置目录。

+

/var:

这个目录中存放着在不断扩充着的东西,我们习惯将那些经常被修改的目录放在这个目录下。包括各种日志文件。

+

在linux系统中,有几个目录是比较重要的,平时需要注意不要误删除或者随意更改内部文件。
系统上跑了很多程序,那么每个程序都会有相应的日志产生,而这些日志就被记录到这个目录下,具体在/var/log 目录下,另外mail的预设放置也是在这里。

+

/etc:

上边也提到了,这个是系统中的配置文件,如果你更改了该目录下的某个文件可能会导致系统不能启动。

+

/bin, /sbin, /usr/bin, /usr/sbin:

这是系统预设的执行文件的放置目录,比如 ls 就是在/bin/ls 目录下的。

+

值得提出的是,/bin, /usr/bin 是给系统用户使用的指令(除root外的普通用户),而/sbin, /usr/sbin 则是给root使用的指令。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Linux目录说明

+

文章作者:

+

发布时间:2020年08月02日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/08/02/Linux%E7%9B%AE%E5%BD%95%E8%AF%B4%E6%98%8E/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/11/10/learn-cordova-1/index.html b/en/2020/11/10/learn-cordova-1/index.html new file mode 100644 index 0000000000..269d1c08c6 --- /dev/null +++ b/en/2020/11/10/learn-cordova-1/index.html @@ -0,0 +1,719 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 从零开始:学习Cordova(一) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 从零开始:学习Cordova(一) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

很长时间没有写博客了,因为最近换了工作,原来的技术栈基本上都换了,现在接触了很多新的技术栈,Cordova 就是其中之一。

+

接下来这篇文章就是我的学习笔记,以供回顾,写的不好请勿拍砖。

+ +

一、What is Cordova? Cordova是个啥?

Logo
移动端开发和 Web 开发有一些不一样,Web 端开发面向的是浏览器,而移动端开发面向的是各种移动设备,那么针对各种移动设备提供的 SDK 进行开发的话,我们通常称之为***原生开发***,原生开发虽然通常调用底层 Api ,性能更好,但是因为不同移动终端底层技术不一样,所以每种设备都要单独开发,这样开发成本和能力要求都比较高,所以通常都会有一些框架来兼容各种平台,就好比 Java 虚拟机可以运行在 Mac/Windows/Linux 一样,为了能够写一套移动端代码可以在各种设备中使用,于是乎就有了 Cordova

+

上面说了这么一堆废话,其实简言之,***Cordova 就是提供了跨设备调用底层Api的移动端开发框架(这种开发方式又叫做Hybird 模式)***

+

知道这一点应该就够了,当然这是我认为的。

+

Cordova 的官网地址如下

+
+

https://cordova.apache.org/

+
+

其实从这里就可以看出, CordovaApache 基金下的一个项目,其官网是这样介绍它的

+
+

Mobile apps with HTML, CSS & JS
Target multiple platforms with one code base
Free and open source

+
+

翻译成中文的话就是

+
+

使用 HtmlCSSJS 进行移动端应用开发
一份代码多平台使用
免费并且开源

+
+

三句话解释了 Cordova 的三个方面,

+
    +
  • 使用前端技术开发-对前端开发友好
  • +
  • 跨平台-节省开发工时,降低开发难度-从图标看支持 Android/IOS/WP三大平台
  • +
  • 免费不用解释开源的话意味着会有很多人贡献代码
  • +
+

以上奠定了 Cordova 强大及流行的基础。

+

二、快速上手

在使用 Cordova 做移动端开发之前需要做一些环境准备工作,首先要安装 Node 以及它携带的 Npm 包管理器,这个现代的前端开发应该都知道,我这里就不多说了,装它就完了。

+

装好之后我们就要用 Npm 全局安装一下 Cordova 项目的依赖,命令很简单;

+

方便的话还要装一下 Git ,因为一些命令要在 git-cmd 上执行,当然管理代码也是需要它的,所以装吧。

+
1
npm install -g cordova
+

就这么简单就可以开发 Cordova 应用了,接下来我们要创建一个项目,创建项目的命令也十分简单。在命令行工具中输入下面的命令你就创建了一个 Cordova 的应用

+
1
cordova create <path> // 这里的path是项目的路径
+

如果要查看创建项目时还能设置什么参数你可以执行下面的命令

+
1
cordova help create
+

怎么样是不是超级简单。
做了上面的工作你就创建了一个具备基本功能的 Cordova 项目,接下来我们要给项目配置一些东西。

+

首先刚才讲到了, Cordova 是一个跨平台的框架,那么你的项目要适配什么平台你需要进行一些配置, cd 到项目目录下执行下面的命令

+
1
cordova platform add <platform name> // platform name 为适配平台的名称
+

如果不知道名称是什么命令行工具也贴心的给了查询用的命令,通过此命令你可以看到你安装了什么平台的适配,有哪些可以用

+
1
2
cordova platform

+

执行了这个命令你将看到下面这样的 list

+
1
2
3
4
5
6
7
8
/***
Installed platforms:
Available platforms:
android ^9.0.0
browser ^6.0.0
electron ^1.0.0
windows ^7.0.0
*/
+

项目也创建了,简单的配置工作也完成了,我们可以启动项目看一下效果了,那么我们就该执行运行项目的命令了

+
1
cordova run <platform name> // platform name 适配平台的名称
+

到这我们就快速的创建并运行起了一个 Cordova 的工程。

+

小Tip

如上执行启动命令后,默认会启动8000端口,如果被占用的话可以通过增加命令行参数的形式改变端口号,参数如下设置

+
1
cordova run browser --target=chrome --port=9090
+

以上就可以自定义端口及浏览器启动应用了。

+

三、详细了解

1、概览

官网开头介绍了 Cordova 的身世和适用范围,前面开头已经讲过了不想再讲,直接看看 Cordova 的架构设计图。
此处是个架构图

+
Cordova 架构图
+ +

这个图理解起来不难,大的方面是两个部分,底层是 MobileOS ,也就是手机的操作系统本身,上层是由 Cordova 构建的框架;

+

Cordova 内部的话是两层结构:

+
    +
  • 上层的话是由前端代码构成的 WebApp 部分;
  • +
  • 下层是 Cordova 的视图渲染引擎,它为 WebApp 提供了 HtmlApiCordovaApi
  • +
+

也就是说我们可以调用 html 的原生 Api 也可以使用 Cordova 提供的 jsApi

+

Cordova 还提供了丰富的插件系统;通过 Cordova 的视图引擎调用 CordovaNativeApi 来调用一些 Cordova 提供好的调用设备的信息的 Api ,另外也可以调用一些用户自定义的 Api

+

Cordova 视图引擎和插件系统再将上层的需求通过 MobileOS 开放的 Api 能力实现具体功能,由此完成了整个 Cordova 框架的使用;

+

总的来说这个原理跟大多数跨平台的框架原理都类似,由框架提供统一的 Api 能力,再由框架处理不同平台的兼容,这里特别之处在于渲染引擎可以允许使用前端原生的 Api ,这将大大降低开发的难度。

+

WebView

说到移动端开发,不知道 WebView 应该是不可能的,在原生移动应用中,WebView 就是移动应用内部嵌入的一个‘浏览器’,它可以允许你使用前端技术展示内容。

+

WebApp

WebAppWebView 两个词有些像,事实上它们也确实有些关系,WebApp 从名字就可以看出是使用 Web 技术开发的 App ,那么它跟 WebView 是什么关系呢,打个比方,WebView 就是个快递盒子, WebApp 就是你买的商品,一个是容器,一个是内容;这样说应该就明白了吧,通过前端技术开发的 App 通过 WebView 嵌入到 App 中,就可以带给用户原生应用的体验,忘了说 WebApp 是只能用浏览器访问的 App

+

和纯 WebApp 不同,嵌入到 WebView 中的 WebApp 需要有一个配置文件,***config.xml*** ,它在项目的根目录下,说明了应用的一些信息,这个文件必不可少!!!

+

插件系统

刚刚在上面的架构图中我们介绍了 Cordova 的插件系统,它提供了我们通过 js 调用 MobileApi 的能力,如电源/相机/联系人等等;
官方维护了核心的功能,当然如果你愿意也可以调用一些第三方提供的插件,你可以方便的通过 npm 包管理器安装;

+

但是有一点必须要说:项目创建后默认是不带任何插件的,即使是官方的核心插件也是需要你自己导入进去,第三方的更不用说了,另外Cordova 只提供了功能 Api 并不包含任何的 UI 部件和 MV 框架(你可以根据自己喜欢使用 Angular 或者 Vue 或者其他什么都可以),这一点要牢记。

+

开发工作流

使用 Cordova 开发 MobileApp 的时候即可以开发多个平台的 App (一次开发多平台可用,降低开发成本),也可以专注开发某一平台的App (使用前端技术开发,降低开发难度),因此开发工作流也分为两种:

+
跨平台(CLI)的工作流

官网上说了很多,也不难理解,这里用我自己的话说就是, Cordova 提供了一个牛叉的 CLI 工具,它可以自动的完成一次编码多平台构建的事,你只需要按照它说的方式开发就行。:)

+
平台为中心的工作流

这里不得不说我没有看很明白,但是结合下面的注意事项我是这样理解的,因为只为某一平台服务,所以有一些针对此平台的特殊的功能或者插件你就可以使用了(虽然不太清楚为什么这么做,如果你知道告诉我,谢谢),但是一旦使用了这种开发方式,那么就回不到跨平台开发了,因为你使用了针对某一平台才能调用的代码,这也不难理解,从字里行间感觉就是不建议使用这种开发工作流(好的好的,知道了)。

+

好的,接下来开发 App 啦;

+

2、开发应用

快速上手中已经介绍了如何准备、创建和启动应用,如何添加平台,这里就不多说了。说些没讲过的。

+

上面说过检查目前平台安装情况的话可以使用下面的命令

+
1
cordova platforms
+

我们也可以使用另一个命令来看,我试验了一下,效果是一样的。。。(问号脸,一样的为什么搞两个命令)

+
1
cordova platform ls
+
+

事后我发现,无论我使用的命令是 platform 还是 platforms 加不加 ls 都可以查到当前安装的平台。。。 啊~兼容性好强。

+
+

每执行一次 add 命令,你就会发现工程目录下的 platforms 目录下就对应生成了一个对应平台的文件夹,切勿手贱删除,你虽然删除了文件夹,但是各处设置的 platform 并没有去掉,会导致报错,如果要删除请使用 remove 命令。 想知道还有哪些命令的话就看下面的链接吧。⇒ 命令大全

+

安装构建的先决条件

因为 Cordova 的底层还是要调用 MobileOS 的接口,所以各平台的 SDK 是必须要装的,当然这里指的是你要进行构建的平台,比如你要构建 Android 应用,那么你就要装 AndroidSDKIOS 同理。这里有一个例外broswer平台是不需要依赖其他 SDK 的(明明是开发 APP ,这里只是为了验证画面比较方便)
要看你是否满足了构建需要的依赖,可以执行下面的命令查看

+
1
cordova requirements
+

执行完后你会得到如下结果

+
1
2
3
4
5
6
7
8
9
10
Requirements check results for android:
Java JDK: installed .
Android SDK: installed
Android target: installed android-19,android-21,android-22,android-23,Google Inc.:Google Apis:19,Google Inc.:Google Apis (x86 System Image):19,Google Inc.:Google Apis:23
Gradle: installed

Requirements check results for ios:
Apple OS X: not installed
Cordova tooling for iOS requires Apple OS X
Error: Some of requirements check failed
+

这样你就可以清楚的知道自己需要安装哪些依赖才能完成对应平台的构建,简直方便的不要不要的。
具体想知道不同的平台都依赖些什么,你可以参见下面的链接;

+ +

构建 App

create 项目之后,项目的根目录下会生成一个 www 目录,这个目录包含了 webapp 的入口页面 index.html ,入口文件的话是 www/js/index.js 文件的 deviceready 事件中。

+

如果要构建代码的话,执行下面的命令

+
1
cordova build
+

这个命令会构建你安装的所有平台,如果只想构建某一平台的话,可以把平台的名字加在命令后面,如

+
1
cordova build ios
+

如果想了解更多的参数的话,可以看一下后面的链接。⇒ 更多参数

+

测试 App

构建完 App 之后我们就可以测试了,通常各平台的 SDK 中会提供模拟器,我们只需要执行下面的命令就可以启动模拟器。

+
1
cordova emulate android
+

当然如果你觉得用实机查看更自然一些的话,也可以把手机插上数据线与电脑建立连接,然后执行下面的命令就可以了。

+
1
cordova run android
+

上面演示的是 AndroidApp 测试过程,每个平台虽然基本类似,但还是有出入,所以,下面提供了不同平台的调试方法,供你预览。

+ +

添加插件

如果只是开发一个 Cordova 框架的 WebApp ,那么你不需要装任何插件,直接用前端技术开发就好了,但是应该没人会这么做吧,毕竟开发这个的目的就是为了跨平台开发原生级移动 App ,因此要调用移动设备的各种功能就必须安装插件。

+

插件

Cordova 插件是一些使用 Javascript 调用原生 SDK 功能的包;
你可以使用两种方式找到你想要的插件

+
    +
  1. 通过 Cordova 提供的包管理平台 ⇒ 插件搜索页
  2. +
  3. 通过命令行搜索 cordova plugin search camera
  4. +
+

选好你要安装的插件之后你需要执行下面的命令来安装它

+
1
cordova plugin add <plugin name> // 如 cordova plugin add cordova-plugin-chrome-apps-proxy
+

如果你的包没有发布在 Cordova 的平台上,也可以使用 git 地址来安装。
另外 Cordova 还非常贴心的提供了一个工具 Plugman 来帮助开发者更好的管理 Cordova 插件。⇒ Plugman参考

+

如果要查看你安装了哪些插件可以使用下面任意一种命令都可以。(咱也不知道为啥提供这么多方式,反正好用无脑)

+
1
2
3
4
5
6
7
8
plugin ls
plugin list
plugin

/**
$ cordova plugin ls
cordova-plugin-whitelist 1.2.1 "Whitelist"
*/
+

如果想知道更多关于 plugin 的命令参数,可以看右边的链接 ⇒ plugin参数

+

使用 merges 自定义每个平台

虽然可以用一套代码来构建多个平台,但一些平台会有自己的特点,就好像 Chrome 浏览器默认的字体大小与 IE 的不同, AndroidIOS 也有不一样的地方,这种情况下去改 www 目录下的文件显然是不合适的,所以 Cordova 提供了 merges 方式来适配不同平台各自的特别处理,你要做的就是在项目的根目录下创建一个 merges 目录,比如你要适配 Android ,你就可以在 merges 下创建一个 android 目录,然后在下面或覆盖或添加新的资源。(这里官网没有说 merges 在哪里创建,试验后发现是在根目录下)

+

更新 Cordova 和你的项目

如果你要更新你的 Cordova (通常不建议这么干,当然你知道后果并想要使用新的特性或者修正 bug 除外),那么你需要执行一下 update 命令

+
1
sudo npm update -g cordova
+

当然你也可以指定更新到什么版本(这都是 npm 的知识了)

+
1
sudo npm install -g cordova@3.1.0-0.2.0
+

你也可以使用下面的命令查看当前的版本(还是 npm 的知识)

+
1
npm info cordova version
+

你是不是以为这样就可以了,不好意思还不行,你还需要把各个平台进行一下升级

+
1
2
3
cordova platform update android --save
cordova platform update ios --save
// ...
+

其实这也好理解,你都已经轿车换 SUV 了,总不能还用原来的车轱辘吧,只是我希望能不能在换车的时候一块儿把车轱辘换了(这里的意思是希望直接自动化处理了)

+

到这里你的第一个 App 就已经开发好了,是不是还有点成就感。

+

一些参考资料

到这里一个简单的基于 Cordova 搭建的 App 就实现了,当然一个 App 绝不这么简单,各平台的 SDK 安装也没那么简单,但这不是本篇文章要说的事了,下一篇文章我会说说如何应对各种不同的开发平台,在这之前还请你看一下 Cordova 都支持哪些平台,具体看一下下面的链接。

+

好了,今天就先写到这里,下篇文章我们再见。

+

平台支持

+

(待续…)

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:从零开始:学习Cordova(一)

+

文章作者:

+

发布时间:2020年11月10日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/11/10/learn-cordova-1/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/11/10/learn-go/index.html b/en/2020/11/10/learn-go/index.html new file mode 100644 index 0000000000..5e74a3b07e --- /dev/null +++ b/en/2020/11/10/learn-go/index.html @@ -0,0 +1,587 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 从零开始:学习Go(一) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 从零开始:学习Go(一) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:从零开始:学习Go(一)

+

文章作者:

+

发布时间:2020年11月10日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/11/10/learn-go/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2020/11/11/learn-ionic-1/index.html b/en/2020/11/11/learn-ionic-1/index.html new file mode 100644 index 0000000000..3c97ae649b --- /dev/null +++ b/en/2020/11/11/learn-ionic-1/index.html @@ -0,0 +1,640 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 从零开始:学习Ionic(一) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 从零开始:学习Ionic(一) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

在移动 App 开发领域,有一个框架非常知名,每每面试都会被问起,“你用过 Ionic 么?”,是的,就是 Ionic 框架。

+

之前已经学习过了 Cordova ,它和 Ionic 可以说是一对黄金搭档,一个面向 MobileOS ,一个面向 UI ,很多人搞不清楚它们之间的区别,没关系,回头我再抽时间讲一下,今天我们主要来认识一下 Ionic

+

好戏开始:)

+ +

Ionic 简介

学一门技术,我们首先要知道它是谁,能干啥,有啥好处。要了解一门新技术最好的方式就是去它的官网看看。
(一言不合丢给你一个官网 ⇒ 官网

+
+

One codebase.
Any platform.
Now in Vue.

+
+

翻译过来是这样的

+
+

一份代码
任何平台
现在支持Vue

+
+

看着是不是眼熟,嗯,跟 Cordova 的口号何其相似,目标都是跨平台,只是 Cordova 跨的是 MobileOSIonic 跨的是前端框架。
最下面一句 Now in Vue 像是在说,“嗯,我也支持Vue了(Vue发展太快了,不能掉队)”,要知道,最开始的 Ionic 是妥妥的 Angular 派,不过无所谓,管他黑猫白猫,能完成任务就是好猫。

+

再往下看你会看到,巴拉巴拉说了一堆好处,比如开发迅速啊,组件优美且丰富啊,社区强大啊,还有跨平台(支持原生 jsAngularVue ,nice~),另外还提供了 native 的组件来直接调用移动设备的功能,可以说是相当强大了。

+

好了,看完了介绍,我们赶紧快速上手吧。

+

快速上手

在使用Ionic做开发还是要做一些准备工作,nodegit都是必不可少的,所以请先行准备好。

+

目前,Ionic提供了两种创建工程的方式:

+
    +
  • 一种用官方的话说叫StepByStep,看过之后我理解就是通过命令的方式,一步步来;
  • +
  • 另一种的话是官方提供了一个向导工具,就跟安装软件一样填一些必要的信息,然后一路下一步就生成了一个工程;
  • +
+

接下来我将使用这两种方式分别创建工程,首先是第一种。

+

StepByStep 创建工程

    +
  1. 全局安装Ionic的脚手架工具
    1
    npm install -g @ionic/cli
  2. +
  3. 创建工程
    1
    ionic start myApp tabs // myApp 是项目目录名称也是工程名称 tabs是模板名称
  4. +
  5. 启动工程
    1
    2
    cd myApp
    ionic serve
  6. +
+

嗯,上面三个步骤之后,你的Ionic项目就启动起来了。(What !? 这么简单?! 嗯,是的。)

+

向导式(App Wizard)创建工程

这是一个在线创建工程的手段(感觉对于开发人员来说貌似还是命令行来的方便,这种可视化方案应该是给小白用户准备的吧),地址如下

+
+

https://ionicframework.com/start

+
+

此处是个图

+

点开上面的地址,你会看到如图所示的一个界面,我们可以看到它要求你输入的一些信息

+
    +
  1. App Name
  2. +
  3. Icon
  4. +
  5. Theme Color
  6. +
  7. Template
  8. +
  9. Framework
  10. +
+

可以看到这比命令行要多出几个信息,上面的命令行只要求了 App NameTemplate 难道强大的命令行不支持设置这些么?答案是否定的,当然可以设置这些信息,只是以参数的形式而已,先不说这个,填写好上面的信息以后我们点击[create App]按钮,这时我们进入了下一步,选择代码仓库(Choose a git host),你会看到下面有一个[Skip]按钮说明这步不是必须的,我们先选择一个git仓库,这里我选择github,其他的感兴趣的同学可以自己试试。点击[connect],这时会弹出一个窗口,申请对应git仓库的鉴权认证,认证通过后我们点击[choose],等待程序运行一会儿后工程就会被上传到git仓库中,并且会跳转到一个DashBoard,提供了可视化管理工程的页面(也太高大上了吧),右侧我们看到如何把代码下载到本地,并运行它,真的是很方便。

+

例:

+
1
2
3
npm install -g @ionic/cli cordova-res // cordova-res 指的是安装 cordova 的开发依赖,因为除 cordova 之外还可以选择如 phonegap 之类的MobileOS 框架
git clone https://github.com/lixuguang/li-app.git li-app
cd li-app && npm install && ionic serve
+ +

执行完上面的命令以后,我们的工程也就运行起来了,也是十分方便,即使不会写代码的人,跟着指南也可以很方便的搭建一个Ionic工程。

+

深入学习

Ionic 的体系当中,最重要的莫过于 Ionic CLI 和 UI Component 两个了,接下来我将分辨将这两部分中最重要的内容拿出来说说。

+

Ionic CLI

在上面的快速上手中,我们已经使用了几个命令,但这些还不够,Ionic CLI 提供了强大的命令,请与我一起看一下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
npm install -g @ionic/cli // 全局安装 Ionic CLI
ionic start myApp tabs // 用模板创建工程
ionic start --list // 查看可用模板
ionic serve // 启动工程
ionic serve --lab // 以android或ios方式打开
ionic lib update // 更新包
ionic serve --address 192.168.89.1 // 指定Ip,给外部用户访问
ionic platform add ios/android // 添加平台
ionic build ios/android // 构建平台
ionic emulate ios [options] // ionic run ios [options] 模拟器运行

ionic generate // 创建新特性


options
-l //livereload, 实时刷新变化。
-c //打印app里的console
-s //打印设备的console
-p //指定设备的端口
-i //指定livereload的重刷端口
--debug //debug
--release //release
--host=0.0.0.0 // IP
--port=8100 // 端口


ionic resources [--splash] [--icon] // 上传代码到官方平台
ionic upload // 登录
ionic info // 查看系统信息
ionic browser add crosswalk // 加壳预览(手机浏览器性能问题解决)
ionic browser list // 查看可用的browser
ionic browser revert android/ios // 删除安装的browser
ionic state reset // 先删除平台和插件,再安装package.json文件中的平台和插件。重置
ionic state save // 保存当前状态信息
ionic state clear // 先删除平台和插件,然后按照package.json文件中包含的平台和插件重新安装。

npm install @ionic/angular@latest --save // Ionic + Angular
ng add @ionic/angular // 使用Angular CLI 增加Ionic的组件库,可用于Angular项目改造Ionic

npm install @ionic/react // Ionic + React
npm install @ionic/react-router // 在既存的react项目中引入ionic特性

// 增加CSS文件为组件服务,加在根组件下
/* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css';

/* Basic CSS for apps built with Ionic */
import '@ionic/react/css/normalize.css';
import '@ionic/react/css/structure.css';
import '@ionic/react/css/typography.css';

/* Optional CSS utils that can be commented out */
import '@ionic/react/css/padding.css';
import '@ionic/react/css/float-elements.css';
import '@ionic/react/css/text-alignment.css';
import '@ionic/react/css/text-transformation.css';
import '@ionic/react/css/flex-utils.css';
import '@ionic/react/css/display.css';

npm install @ionic/vue @ionic/vue-router // Ionic + Vue ,在既存的Vue项目中引入ionic特性

// main.js 文件中加入引入
import { IonicVue } from '@ionic/vue';

import App from './App.vue'
import router from './router';

const app = createApp(App)
.use(IonicVue)
.use(router);

router.isReady().then(() => {
app.mount('#app');
});

// router/index.js 用Ionic提供的vue-router替换原本的vue-router
import { createRouter, createWebHistory } from '@ionic/vue-router';

const routes = [
// routes go here
]

const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})

export default router;

// 增加CSS文件为组件服务 main.js
/* Core CSS required for Ionic components to work properly */
import '@ionic/vue/css/core.css';

/* Basic CSS for apps built with Ionic */
import '@ionic/vue/css/normalize.css';
import '@ionic/vue/css/structure.css';
import '@ionic/vue/css/typography.css';

/* Optional CSS utils that can be commented out */
import '@ionic/vue/css/padding.css';
import '@ionic/vue/css/float-elements.css';
import '@ionic/vue/css/text-alignment.css';
import '@ionic/vue/css/text-transformation.css';
import '@ionic/vue/css/flex-utils.css';
import '@ionic/vue/css/display.css';

From here, you can learn about how to develop with Ionic Framework in our [Ionic Vue Quickstart Guide](https://ionicframework.com/docs/vue/quickstart).

## Ionicons CDN

Ionicons is packaged by default with the Ionic Framework, so no installation is necessary if you're using Ionic. To use Ionicons without Ionic Framework, place the following `<script>` near the end of your page, right before the closing `</body>` tag.


<script type="module" src="https://cdn.jsdelivr.net/npm/ionicons@4.7.4/dist/ionicons/ionicons.esm.js"></script>
<script nomodule src="https://cdn.jsdelivr.net/npm/ionicons@4.7.4/dist/ionicons/ionicons.js"></script>


+ + +

(待续…)

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:从零开始:学习Ionic(一)

+

文章作者:

+

发布时间:2020年11月11日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/11/11/learn-ionic-1/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2021/05/08/\345\205\263\344\272\216VSCode\344\270\255\345\210\253\345\220\215\357\274\210-\357\274\211\347\232\204\344\275\277\347\224\250/index.html" "b/en/2021/05/08/\345\205\263\344\272\216VSCode\344\270\255\345\210\253\345\220\215\357\274\210-\357\274\211\347\232\204\344\275\277\347\224\250/index.html" new file mode 100644 index 0000000000..e9bbe5014a --- /dev/null +++ "b/en/2021/05/08/\345\205\263\344\272\216VSCode\344\270\255\345\210\253\345\220\215\357\274\210-\357\274\211\347\232\204\344\275\277\347\224\250/index.html" @@ -0,0 +1,611 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + VSCode中既想使用path-intellisense插件,又想使用Webpack别名(@)的方法 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ VSCode中既想使用path-intellisense插件,又想使用Webpack别名(@)的方法 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

+

场景:

前端开发我们通常使用VSCode,使用时常见有一个常见的场景:

+
+

引入一个文件、图片等

+
+

如果不利用VSCode的插件,我们通常会写相对路径或者绝对路径,但为了方便写代码,我们通常会利用工具来帮助我们减轻负担,这种情况就引入了今天的主角,VSCode下的插件path-intellisense,是的,有了它编辑器就会聪明的自动帮我们提示出文件在什么地方,这的确方便了我们。

+

但是,在Vue开发过程中,或者说是利用Webpack作为构建工具时我们通常会定义一个变量如:@ 来简化路径的书写,使代码变得简洁美观,这里的@,我们称之为 路径别名 ,这也是Vue官方十分推荐使用的,但因为路径别名的使用,原来的智能提示路径的插件就会无法正常工作,因为工作中确实遇到了这样的坑,所以简单的写个文记录一下。

+

问题一:因为使用了路径别名(@)导致path-intellisense不能自动提示

这个问题其实比较容易理解,因为路径别名是Webpack构建时使用的,并不是VSCode原生支持的功能,所以它并不认识@,这是个什么,所以解决思路就是告诉VSCode @ 是个什么就好了,专业一点说的话就是加上映射关系

+

解决步骤

    +
  1. 打开path-intellisense插件的setting
  2. +
  3. 找到Mapping配置项
  4. +
  5. 加以下代码
    1
    2
    3
    "path-intellisense.mappings": {
    "@": "${workspaceRoot}/src"
    }
  6. +
  7. 重启插件搞定!
  8. +
+

问题二:我们希望 Ctrl + 鼠标左键点击一个外部方法时,能够快速跳转到对应的外部文件。

第二个问题提出时,可能会有人有疑问,上面不是配置完映射关系了嘛,为什么还有下面的问题呢,其实你要是不问,我也有这个问题,上一步操作实际上是告诉了path-intellisense插件该怎么解析@这个东西,但是VSCode并不知道啊,但是Ctrl + 鼠标左键的动作又是VSCode负责的事,所以我们也要告诉一下编辑器该如何应对上面的情况,说到这里大家应该就理解了

+

解决步骤

    +
  1. 在项目package.json所在同级目录下创建文件jsconfig.json(这是VSCode的一个可选配置)
  2. +
  3. 在jsconfig.json增加如下配置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    {
    "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "allowSyntheticDefaultImports": true,
    "baseUrl": "./",
    "paths": {
    "@/*": ["src/*"] // 这里是关键代码
    }
    },
    "exclude": [
    "node_modules"
    ]
    }
  4. +
  5. 重启VSCode搞定!
  6. +
+

通过上面的解决方案,我们就可以愉快的使用VSCode进行前端开发了,你学废了吗?

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:VSCode中既想使用path-intellisense插件,又想使用Webpack别名(@)的方法

+

文章作者:

+

发布时间:2021年05月08日 - 08:03

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2021/05/08/%E5%85%B3%E4%BA%8EVSCode%E4%B8%AD%E5%88%AB%E5%90%8D%EF%BC%88-%EF%BC%89%E7%9A%84%E4%BD%BF%E7%94%A8/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2021/05/13/\345\205\263\344\272\216\347\211\210\346\234\254\347\256\241\347\220\206/index.html" "b/en/2021/05/13/\345\205\263\344\272\216\347\211\210\346\234\254\347\256\241\347\220\206/index.html" new file mode 100644 index 0000000000..ebd83cd7c8 --- /dev/null +++ "b/en/2021/05/13/\345\205\263\344\272\216\347\211\210\346\234\254\347\256\241\347\220\206/index.html" @@ -0,0 +1,600 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 关于版本管理 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 关于版本管理 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

还记得你的前端工程里有这样一个文件‘package.json’,里面管理者运行代码所需要的各种依赖,每种依赖都会有一个版本号,那么你知道版本号是怎么定义的么?不知道的话就看看下面的内容吧。

+ +

关于版本号的那些事

你会发现这些版本号通常是三部分构成的,像是‘X.Y.Z’的一种感觉,其实这是一种叫做SemVer的版本管理规范,下面我们就来讲讲SemVer

+

SemVer 的生平

语义化版本控制的规范是由 Gravatars 创办者兼 GitHub 共同创办者 Tom Preston-Werner 所建立。

+

它的许可证是 知识共享 署名 3.0 (CC BY 3.0) 所以你可以不用付费直接使用它。

+

关于它的更多描述你可以到下面的地址找到
https://semver.org/lang/zh-CN/

+

主要你要记住的是如下几句话:

+
+

版本格式:主版本号.次版本号.修订号,版本号递增规则如下:

+

主版本号:当你做了不兼容的 API 修改,
次版本号:当你做了向下兼容的功能性新增,
修订号:当你做了向下兼容的问题修正。
先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。

+
+

更多的内容的话,请到官网查看吧。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:关于版本管理

+

文章作者:

+

发布时间:2021年05月13日 - 07:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2021/05/13/%E5%85%B3%E4%BA%8E%E7%89%88%E6%9C%AC%E7%AE%A1%E7%90%86/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/03/11/\346\220\236\346\207\202EventLoop/index.html" "b/en/2022/03/11/\346\220\236\346\207\202EventLoop/index.html" new file mode 100644 index 0000000000..a0ccb49939 --- /dev/null +++ "b/en/2022/03/11/\346\220\236\346\207\202EventLoop/index.html" @@ -0,0 +1,702 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 搞懂EventLoop | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 搞懂EventLoop +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

EventLoop 翻译过来就是事件循环,那啥是事件循环啊,这个要从javascript是如何执行的说起。
要知道javascript是一个运行在浏览器或者Node环境的单线程执行的脚本语言,但是为了解决单线程阻塞的问题,这里就引入了事件循环的机制,也就是我们常说的异步特性。

+

但是虽然都是JavaScript,都是事件循环,都有异步特性,但浏览器环境和Node环境的实现方式是不一样的,这里要先说明一下。
浏览器的事件循环是HTML定义的规范,而Node环境是利用libuv库实现的,这里我们先从浏览器的事件循环开始说。

+ + +

浏览器的事件循环

首先我们要知道,浏览器的事件循环是有这么两个部分构成,一个叫做主线程(main thread),另一个叫做调用栈(call-stack),所有的任务(Task)都会被放到调用栈里,等待主线程调用,这个怎么理解呢,打个比方,主线程就像是物流配送中的传送带,如果没有快递往上放的时候,它是空的,当有快递包裹放上去的时候,它就开始运行,而你购买的产品要放倒包裹里,然后包裹在放到传送带上,最终被送到你家里,这里的传送带就是主线程,包裹就是调用栈,而购买的产品就是任务,不知道这样解释是否能好理解一些。

+

同步、异步任务

JavaScript中的任务有两种,一种是同步任务,另一种是异步任务。

+
    +
  • 同步任务会排着队,逐个执行
  • +
  • 异步任务在执行得到结果后,将回调函数添加到任务队列中,等着主线程空了再执行。
  • +
+

调用栈

栈,是一种数据结构,后进先出(LIFO),后入栈的先执行,执行完后出栈执行栈顶新的那个函数,直到栈空,像个瓶子,只有一头有口。

+

任务队列

队列,也是一种数据结构,先进先出(FIFO),队头出,队尾进,先到先得,就像排队买东西,或者汽车排队过隧道。

+

任务

任务分为两种,一种叫做宏任务(MacroTask),另一种叫做微任务(MicroTask),首先他们都是异步队列中的任务,那它们之间是什么关系呢?简单说来就是微任务是VIP优先执行,全部微任务执行完之后才轮到普通用户宏任务执行,而且执行完一个后马上要看有没有新的微任务执行,有的话宏任务还得等着,就这样往复,看着像不像是在银行等着排队办业务的你(其实是我)。
JavaScript的执行流程
宏任务与微任务
那么哪些是宏任务,哪些是微任务呢?看下表吧。

+ + + + + + + + + + + +
宏任务微任务
setTimeout、setInterval、js主代码、setImmediate(Node)、requestAnimationFrame(浏览器)process.nextTick、Promise的then方法
+

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
console.log('1');

setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})

setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
+ +

分析一下

JS 运行机制为从上而下,那么这里的执行顺序应该是什么样的呢?

+
第一轮循环:

1)、首先打印 1
2)、接下来是setTimeout是异步任务且是宏任务,加入宏任务暂且记为 setTimeout1
3)、接下来是 process 微任务 加入微任务队列 记为 process1
4)、接下来是 new Promise 里面直接 resolve(7) 所以打印 7 后面的then是微任务 记为 then1
5)、setTimeout 宏任务 记为 setTimeout2

+
+

第一轮循环打印出的是 1 7
当前宏任务队列:setTimeout1, setTimeout2
当前微任务队列:process1, then1,

+
+
第二轮循环:

1)、执行所有微任务
2)、执行process1,打印出 6
3)、执行then1 打印出8
4)、微任务都执行结束了,开始执行第一个宏任务
5)、执行 setTimeout1 也就是 第 3 - 14 行
6)、首先打印出 2
7)、遇到 process 微任务 记为 process2
8)、new Promise中resolve 打印出 4
9)、then 微任务 记为 then2

+
+

第二轮循环结束,当前打印出来的是 1 7 6 8 2 4
当前宏任务队列:setTimeout2
当前微任务队列:process2, then2

+
+
第三轮循环:

1)、执行所有的微任务
2)、执行 process2 打印出 3
3)、执行 then2 打印出 5
4)、执行第一个宏任务,也就是执行 setTimeout2 对应代码中的 25 - 36 行
5)、首先打印出 9
6)、process 微任务 记为 process3
7)、new Promise执行resolve 打印出 11
8)、then 微任务 记为 then3

+
+

第三轮循环结束,当前打印顺序为:1 7 6 8 2 4 3 5 9 11
当前宏任务队列为空
当前微任务队列:process3,then3

+
+
第四轮循环:

1)、执行所有的微任务
2)、执行process3 打印出 10
3)、执行then3 打印出 12

+

代码执行结束:
最终打印顺序为:1 7 6 8 2 4 3 5 9 11 10 12

+

Node环境的循环机制

Node的Event loop一共分为6个阶段,每个细节具体如下:

+
    +
  • timers: 执行setTimeout和setInterval中到期的callback。
  • +
  • pending callback: 上一轮循环中少数的callback会放在这一阶段执行。
  • +
  • idle, prepare: 仅在内部使用。
  • +
  • poll: 最重要的阶段,执行pending callback,在适当的情况下回阻塞在这个阶段。
  • +
  • check: 执行setImmediate(setImmediate()是将事件插入到事件队列尾部,主线程和事件队列的函数执行完成之后立即执行setImmediate指定的回调函数)的callback。
  • +
  • close callbacks: 执行close事件的callback,例如socket.on(‘close’[,fn])或者http.server.on(‘close, fn)。
  • +
+

Node EventLoop

+

具体细节如下:

+

timers

执行setTimeout和setInterval中到期的callback,执行这两者回调需要设置一个毫秒数,理论上来说,应该是时间一到就立即执行callback回调,但是由于system的调度可能会延时,达不到预期时间。
以下是官网文档解释的例子:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const fs = require('fs');

function someAsyncOperation(callback) {
// Assume this takes 95ms to complete
fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout(() => {
const delay = Date.now() - timeoutScheduled;

console.log(`${delay}ms have passed since I was scheduled`);
}, 100);


// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
const startCallback = Date.now();

// do something that will take 10ms...
while (Date.now() - startCallback < 10) {
// do nothing
}
});

+ +

当进入事件循环时,它有一个空队列(fs.readFile()尚未完成),因此定时器将等待剩余毫秒数,当到达95ms时,fs.readFile()完成读取文件并且其完成需要10毫秒的回调被添加到轮询队列并执行。
当回调结束时,队列中不再有回调,因此事件循环将看到已达到最快定时器的阈值,然后回到timers阶段以执行定时器的回调。
在此示例中,您将看到正在调度的计时器与正在执行的回调之间的总延迟将为105毫秒。

+

pending callbacks

此阶段执行某些系统操作(例如TCP错误类型)的回调。 例如,如果TCP socket ECONNREFUSED在尝试connect时receives,则某些* nix系统希望等待报告错误。 这将在pending callbacks阶段执行。

+

poll

该poll阶段有两个主要功能:

+
    +
  • 执行I/O回调。
  • +
  • 处理轮询队列中的事件。
  • +
+

当事件循环进入poll阶段并且在timers中没有可以执行定时器时,将发生以下两种情况之一
如果poll队列不为空,则事件循环将遍历其同步执行它们的callback队列,直到队列为空,或者达到system-dependent(系统相关限制)。

+

如果poll队列为空,则会发生以下两种情况之一

+
    +
  • 如果有setImmediate()回调需要执行,则会立即停止执行poll阶段并进入执行check阶段以执行回调。
  • +
  • 如果没有setImmediate()回到需要执行,poll阶段将等待callback被添加到队列中,然后立即执行。
  • +
+

当然设定了 timer 的话且 poll 队列为空,则会判断是否有 timer 超时,如果有的话会回到 timer 阶段执行回调。

+

check

此阶段允许人员在poll阶段完成后立即执行回调。
如果poll阶段闲置并且script已排队setImmediate(),则事件循环到达check阶段执行而不是继续等待。
setImmediate()实际上是一个特殊的计时器,它在事件循环的一个单独阶段运行。它使用libuv API来调度在poll阶段完成后执行的回调。
通常,当代码被执行时,事件循环最终将达到poll阶段,它将等待传入连接,请求等。
但是,如果已经调度了回调setImmediate(),并且轮询阶段变为空闲,则它将结束并且到达check阶段,而不是等待poll事件。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
console.log('start')
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
Promise.resolve().then(function() {
console.log('promise3')
})
console.log('end')

+ +

如果Node版本是v11.x,那么执行结果和浏览器一致,结果如下:

+
1
2
3
4
5
6
7
start
end
promise3
timer1
promise1
timer2
promise2
+ +

如果是v10版本上,那结果将会出现两种情况
如果time2定时器已经在执行队列中了

+
1
2
3
4
5
6
7
start
end
promise3
timer1
timer2
promise1
promise2
+ +

如果time2定时器没有在执行对列中,执行结果为

+
1
2
3
4
5
6
7
start
end
promise3
timer1
promise1
timer2
promise2
+ +

具体情况可以参考poll阶段的两种情况。

+

NodeEventLoop

+

setImmediate() 的setTimeout()的区别

setImmediate和setTimeout()是相似的,但根据它们被调用的时间以不同的方式表现。

+
    +
  • setImmediate()设计用于在当前poll阶段完成后check阶段执行脚本 。
  • +
  • setTimeout() 安排在经过最小(ms)后运行的脚本,在timers阶段执行。
  • +
+

举个例子

1
2
3
4
5
6
7
8
setTimeout(() => {
console.log('timeout');
}, 0);

setImmediate(() => {
console.log('immediate');
});

+ +

执行定时器的顺序将根据调用它们的上下文而有所不同。 如果从主模块中调用两者,那么时间将受到进程性能的限制。
其结果也不一致

+

如果在I / O周期内移动两个调用,则始终首先执行立即回调:

+
1
2
3
4
5
6
7
8
9
10
11
const fs = require('fs');

fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});

+ +

其结果可以确定一定是immediate => timeout。
主要原因是在I/O阶段读取文件后,事件循环会先进入poll阶段,发现有setImmediate需要执行,会立即进入check阶段执行setImmediate的回调。
然后再进入timers阶段,执行setTimeout,打印timeout。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
   ┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘

+ +

Process.nextTick()

process.nextTick()虽然它是异步API的一部分,但未在图中显示。这是因为process.nextTick()从技术上讲,它不是事件循环的一部分。

+

process.nextTick()方法将 callback 添加到next tick队列。 一旦当前事件轮询队列的任务全部完成,在next tick队列中的所有callbacks会被依次调用。

+

换种理解方式:

+

当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。

+

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let bar;

setTimeout(() => {
console.log('setTimeout');
}, 0)

setImmediate(() => {
console.log('setImmediate');
})
function someAsyncApiCall(callback) {
process.nextTick(callback);
}

someAsyncApiCall(() => {
console.log('bar', bar); // 1
});

bar = 1;

+

在NodeV10中上述代码执行可能有两种答案,一种为:

+
1
2
3
4
bar 1
setTimeout
setImmediate

+

另一种为

+
1
2
3
4
bar 1
setImmediate
setTimeout

+ +

无论哪种,始终都是先执行process.nextTick(callback),打印bar 1。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:搞懂EventLoop

+

文章作者:

+

发布时间:2022年03月11日 - 08:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/03/11/%E6%90%9E%E6%87%82EventLoop/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/03/11/\346\225\260\347\273\204\346\226\271\346\263\225\344\270\200\347\233\256\344\272\206\347\204\266/index.html" "b/en/2022/03/11/\346\225\260\347\273\204\346\226\271\346\263\225\344\270\200\347\233\256\344\272\206\347\204\266/index.html" new file mode 100644 index 0000000000..4ea8ef7811 --- /dev/null +++ "b/en/2022/03/11/\346\225\260\347\273\204\346\226\271\346\263\225\344\270\200\347\233\256\344\272\206\347\204\266/index.html" @@ -0,0 +1,785 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 数组方法哪些有副作用,一目了然! | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 数组方法哪些有副作用,一目了然! +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

今天一个小兄弟微信给我发了个消息,说是今天面试,面试官问了个比较基础的问题,没答好,我就问他问了什么,他说,数组有哪些方法会改变原来的值,他说他就说了3、4个,还不确定对不对,我想了想说其实列个表就很容易记住了,于是便有了下面这个表格。

+ +

关于数组的那些方法

其实上面的问题:

+
+

数组有哪些方法会改变原来的值

+
+

现在有个词挺火的,叫做纯函数,什么是纯函数呢,就是没有副作用,什么是没有副作用呢,就是结果是幂等的,等等….怎么越来越绕,简单来说,就是这个函数干几次都是一样的结果,出了函数就不会有任何影响,他就像一台加工机器,输入原料,输出产品,没有任何其他输出材料,比如废气之类影响大气的东西。好吧,你不明白也没关系,反正我对上面这个问题的理解就变成了

+
+

数组的哪些方法是没有副作用的。

+
+

要说有哪些方法是有副作用,首先你得知道有哪些方法,

+

比如,push,pop,shift,unshift,splice 这些别跟我说你不知道,这些都是常用的数组操作,出栈入栈,
还有什么呢,数组的排序啊,比如sort,reverse,还有一个用的不多,fill是用一个固定值填充数组的起始元素到终止元素,这些呢都是在原数组上进行操作,所以肯定是影响数组的值的,也就是说有副作用的。

+

那么哪些没有副作用呢,简单说其他的都没有,
啥!?,是的,其他的都没有。。。所以记得上面这些就好了呀,怎么样,是不简单。

+

那还剩下啥?

+
    +
  • concat 返回的是心数组,
  • +
  • join 返回的是字符串,
  • +
  • indexOf和lastIndexOf返回的是索引,
  • +
  • slice 返回的是切割后的新数组,别把它和splice混淆了哈,
  • +
  • entries 返回一个新的Array Iterator对象,
  • +
  • keys 返回一个新的包含全部Array索引的 Iterator对象,
  • +
  • values 有keys就有values,返回一个新的包含全部Array值的 Iterator对象,
  • +
  • every、some、includes 返回 布尔值,
  • +
  • filter,map 返回新数组,
  • +
  • reducer 有点神奇 返回累加值,
  • +
  • find、findIndex 找元素 一个返回第一个符合的元素本身,一个返回他的索引,
  • +
  • 最后还有个flat,展平了数组,
  • +
  • forEach 遍历数组, 这么一数起来,方法还真不少呢!
  • +
+

总结

想要记住所有的方法呢,不下点功夫是不行的,但是想要记住有副作用的,然后排除掉他们的话,还是容易的,最后做个表,方便直观的来看。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
方法作用返回结果有无副作用
push数组尾部插入元素改变了的原数组
pop数组尾部删除元素改变了的原数组
shift数组头部插入元素改变了的原数组
unshift数组头部删除元素改变了的原数组
splice增删改数组改变了的原数组
sort数组排序改变了的原数组
reverse数组反序改变了的原数组
fill填充数组改变了的原数组
concat连结数组合并的新数组
join合并数组为字符串字符串
indexOf查索引第一个索引
lastIndexOf查索引最后一个索引
slice截取数组截取的新数组
entries迭代数组迭代器对象
keys迭代数组的key全部的key
values迭代数组的value全部的value
every每个都满足布尔值
some有一些满足布尔值
includes包含某元素布尔值
filter过滤数组新数组
map加工数组新数组
reducer迭代数组累加值
find查找元素第一个元素或undifined
findIndex查找元素第一个元素的index
flat展平数组新数组
forEach遍历数组void
+

就到这里,怎么样,清楚了吧。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:数组方法哪些有副作用,一目了然!

+

文章作者:

+

发布时间:2022年03月11日 - 07:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/03/11/%E6%95%B0%E7%BB%84%E6%96%B9%E6%B3%95%E4%B8%80%E7%9B%AE%E4%BA%86%E7%84%B6/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/06/01/CodeReview\344\273\243\347\240\201\345\256\241\346\237\245\346\214\207\345\215\227/index.html" "b/en/2022/06/01/CodeReview\344\273\243\347\240\201\345\256\241\346\237\245\346\214\207\345\215\227/index.html" new file mode 100644 index 0000000000..ab1881e083 --- /dev/null +++ "b/en/2022/06/01/CodeReview\344\273\243\347\240\201\345\256\241\346\237\245\346\214\207\345\215\227/index.html" @@ -0,0 +1,732 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CodeReview代码审查指南.md | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ CodeReview代码审查指南.md +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

1.关于Code Review

1.1 Code Review的目的

Code Review是一种用来确认方案设计和代码实现的质量保证机制,通过这个机制我们可以对代码、测试过程和注释进行检查。

+

Code Review主要用来在软件工程过程中改进代码质量,通过Code Review可以达到如下目的目的:

+
    +
  1. 在项目早期就能够发现代码中的BUG
  2. +
  3. 帮助初级开发人员学习高级开发人员的经验,达到知识共享
  4. +
  5. 避免开发人员犯一些很常见,很普通的错误
  6. +
  7. 保证项目组人员的良好沟通
  8. +
  9. 项目或产品的代码更容易维护
  10. +
+

1.2 Code Review的前提

进入Code Review需要检查的条件如下:

+
    +
  1. Code Review人员是否理解了Code Review的概念和Code Review将做什么
    如果做Code Review的人员不能理解Code Review对项目成败和代码质量的重要程度,他们的做法可能就会是应付了事。
  2. +
  3. 代码是否已经正确的build,build的目的使得代码已经不存在基本语法错误
    我们总不希望高级开发人员或是主管将时间浪费在检查连编译都通不过的代码上吧。
  4. +
  5. 代码执行时功能是否正确
    Code Review人员也不负责检查代码的功能是否正确,也就是说,需要复查的代码必须由开发人员或质量人员负责该代码的功能的正确性。
  6. +
  7. Review人员是否理解了代码
    做复查的人员需要对该代码有一个基本的了解,其功能是什么,是拿一方面的代码,涉及到数据库或是通讯,这样才能采取针对性的检查
  8. +
  9. 开发人员是否对代码做了单元测试
    这一点也是为了保证Code Review前一些语法和功能问题已经得到解决,Code Review人员可以将精力集中在代码的质量上。
  10. +
+

1.3 Code Review需要做什么

Code Review主要检查代码中是否存在以下方面问题:

+
    +
  • 代码的一致性
  • +
  • 编码风格
  • +
  • 代码的安全问题
  • +
  • 代码冗余
  • +
  • 是否正确设计以满足需求(性能、功能)
  • +
  • 等等
  • +
+

1.3.1 完整性检查(Completeness)

    +
  • 代码是否完全实现了设计文档中提出的功能需求
  • +
  • 代码是否已按照设计文档进行了集成和Debug
  • +
  • 代码是否已创建了需要的数据库,包括正确的初始化数据
  • +
  • 代码中是否存在任何没有定义或没有引用到的变量、常数或数据类型
  • +
+

1.3.2 一致性检查(Consistency)

    +
  • 代码的逻辑是否符合设计文档
  • +
  • 代码中使用的格式、符号、结构等风格是否保持一致
  • +
+

1.3.3 正确性检查(Correctness)

    +
  • 代码是否符合制定的标准
  • +
  • 所有的变量都被正确定义和使用
  • +
  • 所有的注释都是准确的
  • +
  • 所有的程序调用都使用了正确的参数个数
  • +
+

1.3.4 可修改性检查(Modifiability)

    +
  • 代码涉及到的常量是否易于修改(如使用配置、定义为类常量、使用专门的常量类等)
  • +
  • 代码中是否包含了交叉说明或数据字典,以描述程序是如何对变量和常量进行访问的
  • +
  • 代码是否只有一个出口和一个入口(严重的异常处理除外)
  • +
+

1.3.5 可预测性检查(Predictability)

    +
  • 代码所用的开发语言是否具有定义良好的语法和语义
  • +
  • 是否代码避免了依赖于开发语言缺省提供的功能
  • +
  • 代码是否无意中陷入了死循环
  • +
  • 代码是否是否避免了无穷递归
  • +
+

1.3.6 健壮性检查(Robustness)

    +
  • 代码是否采取措施避免运行时错误
      +
    • 数组边界溢出
    • +
    • 被零除
    • +
    • 值越界
    • +
    • 堆栈溢出
    • +
    • +
    +
  • +
+

1.3.7 结构性检查(Structuredness)

    +
  • 程序的每个功能是否都作为一个可辩识的代码块存在
    循环是否只有一个入口
  • +
+

1.3.8 可追溯性检查(Traceability)

    +
  • 代码是否对每个程序进行了唯一标识
  • +
  • 是否有一个交叉引用的框架可以用来在代码和开发文档之间相互对应
  • +
  • 代码是否包括一个修订历史记录,记录中对 代码的修改和原因都有记录
  • +
  • 是否所有的安全功能都有标识
  • +
+

1.3.9 可理解性检查(Understandability)

    +
  • 注释是否足够清晰的描述每个子程序
  • +
  • 是否使用到不明确或不必要的复杂代码,它们是否被清楚的注释
  • +
  • 使用一些统一的格式化技巧(如缩进、空白等)用来增强代码的清晰度
  • +
  • 是否在定义命名规则时采用了便于记忆,反映类型等方法
  • +
  • 每个变量都定义了合法的取值范围
  • +
  • 代码中的算法是否符合开发文档中描述的数学模型
  • +
+

1.3.10 可验证性检查(Verifiability)

代码中的实现技术是否便于测试

+

1.4 Code Review的步骤

这些是我在平时工作中的经验总结,目前也是按照这个步骤在做。

+
    +
  1. 代码编写者和代码审核者坐在一起,由代码编写者按照UC依次讲解自己负责的代码和相关逻辑,从Web层->DAO层;
  2. +
  3. 代码审核者在此过程中可以随时提出自己的疑问,同时积极发现隐藏的bug;对这些bug记录在案。
  4. +
  5. 代码讲解完毕后,代码审核者给自己安排几个小时再对代码审核一遍。
    代码需要一行一行静下心看。同时代码又要全面的看,以确保代码整体上设计优良。
  6. +
  7. 代码审核者根据审核的结果编写“代码审核报告”,“审核报告”中记录发现的问题及修改建议,然后把“审核报告”发送给相关人员。
  8. +
  9. 代码编写者根据“代码审核报告”给出的修改意见,修改好代码,有不清楚的地方可积极向代码审核者提出。
  10. +
  11. 代码编写者 bug fix完毕之后给出反馈。
  12. +
  13. 代码审核者把Code Review中发现的有价值的问题更新到”代码审核规范”的文档中,对于特别值得提醒的问题可群发email给所有技术人员。
  14. +
+

提示
Code Review必备的文档:

+
    +
  • “代码审核规范”文档:记录代码应该遵循的标准。
    +

    代码审核者根据这些标准来Code Review代码,同时在Code Review过程中不断完善该文档。

    +
    +
  • +
+

2.Code Reivew的执行

一个标准的Code Reivew活动应该分为三个阶段:

+

2.1.事前准备阶段

在一次CR前,对以下内容进行充分准备。

+

2.1.1.CR的对象

在准备CR代码对象时,我们要注意代码的数量,如果代码量比较大,要对代码进行必要的分解,确定其中的关键代码,对关键代码进行CR,可以达到举一反三的目的。

+

2.1.2.CR的内容

我们对代码的审查内容很多,如代码的编写是否规范(注释的书写格式、命名规范等)、技术处理规范(异常处理、日志处理、代码组织结构等)、业务实现等。我们不能希望通过一次CR活动,完成所有这些内容的审查,因此我们必须设定本次CR活动内容界限,确定审查重点;

+

2.1.3.评审规范和标准

在CR前设计确定评审规范和标准是必要,通过规范和标准我们在审查过程中可以有据可依,有理可循,而且还可以做到标准统一。

+

2.1.4.选择CR活动的参与者

在CR开始前,必须把本次CR活动的对象、审查内容以及审查的规范和标准通报给所有的参与者。

+

2.1.5.选择CR活动的实施方式。

CR活动有很多形式可供我们选择,我们可以根据实际情况选择桌面式CR、演示讲解式CR、一对一的座位CR等等。

+

2.2.实施阶段

充分的事前准备,只是做好CR活动的前提,在CR实施过程中,我们要做好以下工作。

+

2.2.1.准确记录

对于CR过程发现的问题,我们必须清晰准确的记录,可以使用问题点记录单,明确记录的项目和内容。

+

2.2.2.讲解与提问

CR过程中,要采用代码作者讲解和审查者提问方式。审查者不能只在发现问题时提问,同时也要根据本次审查的内容要求代码作者对某个特定问题的讲解。

+

2.2.3.逐项审查

对事前确定的审查内容,要逐项审查,不能因为时间不足等因素一扫而过。

+

2.2.4.注意气氛

实施审查时,要营造一个讨论问题、解决问题的氛围,不能把审查会搞成批判会,这样会影响相关人员的积极性。

+

2.3. 事后跟踪跟踪。

2.3.1. 确认发现的问题

CR结束后,对发现的问题,首先需要确定以下内容。

+
    +
  1. 问题点的难易程度以及影响的范围;
  2. +
  3. 解决问题的责任者和问题点修正结果的确认者;
  4. +
  5. 解决问题点的时限。
  6. +
+

2.3.2. 修正问题责任者

对于修正问题责任者,在问题点的修正过程中,要三方面内容的记录。

+
    +
  1. 问题点的原因;
  2. +
  3. 解决问题点的对策;
  4. +
  5. 修正的内容。
  6. +
+

2.3.3. 修正结果确认者

做为修正结果的确认者,必须按照事前约定的时限及时的对修正结果进行全面的确认

+

3.注意事项

3.1. 经常进行Code Review

    +
  1. 要Review的代码越多,那么要重构,重写的代码就会越多。而越不被程序作者接受的建议也会越多,唾沫口水战也会越多。
  2. +
  3. 程序员代码写得时候越长,程序员就会在代码中加入越来越多的个人的东西。
  4. +
  5. 越接近软件发布的最终期限,代码也就不能改得太多。
  6. +
+

3.2. Code Review不要太正式,而且要短

忘了那个代码评审的Checklist吧,走到你的同事座位跟前,像请师父一样请他坐到你的电脑面前,然后,花5分钟给他讲讲你的代码,给他另外一个5分钟让他给你的代码提提意见,这比什么都好。而如果你用了一个Checklist,让这个事情表现得很正式的话,下面两件事中必有一件事会发生:

+
    +
  1. 只有在Checklist上存在的东西才会被Review。
  2. +
  3. Code Reviews 变成了一种礼节性的东西,你的同事会装做很关心你的代码,但其实他心里想着尽快地离开你。
  4. +
+

只有不正式的Code Review才会让你和评审者放轻松,人只有放松了,才会表现得很真实,很真诚。记住Review只不过是一种形式,而只有在相互信任中通过相互的讨论得到了有意义和有建设性的建议和意见,那才是最实在的。不然,作者和评审者的关系就会变成小偷和警察的关系。

+

3.3. 尽可能的让不同的人Reivew你的代码

如果可能的话,不要总是只找一个人来Review你的代码,不同的人有不同的思考方式,有不同的见解,所以,不同的人可以全面的从各个方面评论你的代码。
但不要太多了,人多嘴杂反而适得其反,基本上来说,不要超过3个人,这是因为,这是一个可以围在一起讨论的最大人员尺寸。

+

下面是几个优点:

+
    +
  1. 从不同的方向评审代码总是好的。
  2. +
  3. 会有更多的人帮你在日后维护你的代码。
  4. +
  5. 这也是一个增加团队凝聚力的方法。
  6. +
+

3.4. 保持积极的正面的态度

程序员最大的问题就是“自负”,尤其当我们Reivew别人的代码的时候,我已经见过无数的场面,程序员在Code Review的时候,开始抨击别人的代码,质疑别人的能力。太可笑了,我分析了一下,这类的程序员其实并没有什么本事,因为他们指责对方的目的是想告诉大家自己有多么的牛,靠这种手段来表现自己的程序员,其实是就是传说中所说的“半瓶水”。

+

所以,无论是代码作者,还是评审者,都需要一种积极向上的正面的态度,作者需要能够虚心接受别人的建议,因为别人的建议是为了让你做得更好;评审者也需要以一种积极的正面的态度向作者提意见,因为那是和你在一个战壕里的战友。记住,你不是一段代码,你是一个人!

+

3.5. 学会享受Code Reivew

这可能是最重要的一个提示了,如果你到了一个人人都喜欢Code Reivew的团阿,那么,你会进入到一个生机勃勃的地方,在那里,每个人都能写出质量非常好的代码,在那里,你不需要经理的管理,团队会自适应一切变化,他们相互学习,相互帮助,不仅仅是写出好的代码,而且团队和其中的每个人都会自动进化,最关键的是,这个是一个团队。

+

资料来源

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:CodeReview代码审查指南.md

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/CodeReview%E4%BB%A3%E7%A0%81%E5%AE%A1%E6%9F%A5%E6%8C%87%E5%8D%97/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/06/01/Electron\347\274\226\347\240\201\350\247\204\350\214\203/index.html" "b/en/2022/06/01/Electron\347\274\226\347\240\201\350\247\204\350\214\203/index.html" new file mode 100644 index 0000000000..a448b46329 --- /dev/null +++ "b/en/2022/06/01/Electron\347\274\226\347\240\201\350\247\204\350\214\203/index.html" @@ -0,0 +1,634 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Electron编码规范.md | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Electron编码规范.md +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

目录
代码规范 2

+
    +
  1. 说明 2
  2. +
  3. 基本原则 2
  4. +
  5. C++与Python 2
  6. +
  7. 命名相关 2
  8. +
  9. 工程目录结构 3
  10. +
+

Electron代码规范

+
    +
  1. 说明
    Electron框架正如官网介绍是使用 JavaScript, HTML 和 CSS 构建跨平台的桌面应用的框架,框架分为前端和后端两个部分,根据前后端采用的技术不同分别遵循前后端所采用技术的代码规范
  2. +
  3. 基本原则
  4. +
+
    +
  1. 不要直接在渲染进程操作主进程,通过进程通信的方式进行数据及事件响应

    +
  2. +
  3. nodejs请使用require方式引入资源不使用import方式引入

    +
  4. +
  5. 多渲染进程传参请使用localStorage或sessionStorage

    +
  6. +
  7. 资源下载不下来时请直接到官网下载https://github.com/electron/electron/releases 将文件放到 用户目录\AppData\Local\electron\Cache 下 再进行 npm install -g electron

    +
  8. +
  9. 代码中包含native模块,必须进行 rebuild操作
    调试渲染进程时增加如下代码增加mainWindow.webContents.openDevTools();

    +
  10. +
  11. +
+
    +
  1. C++与Python
    对于 C++ 和 Python, 遵循 Chromium 的编码风格. 可以使用 clang-format 来自动格式化 C++ 代码. 可以使用 script/cpplint.py 来检验文件是否符合要求。
  2. +
+

现在使用的 Python 版本是 Python 2.7。

+

C++ 代码使用了大量 Chromium 的抽象和类型,因此建议使用者熟悉它们。 一个起步的好地方是 Chromium 的《重要的抽象概念和数据库结构》文档. 该文档提到一些特殊类型,范围类型(超出范围时自动释放其内存), 记录机制等。
4. 命名相关
Electron API 使用与 Node.js 相同的大小写方案:
当模块本身是class时, 比如 BrowserWindow, 使用 大驼峰.
当模块是一组 API 时, 比如 globalShortcut时,使用 小驼峰。
当 API 是对象的属性时, 并且它复杂到足以成为一个单独的块, 比如 win.webContents, 使用 小驼峰.
对于其他非模块API, 使用自然标题, 比如 Tag 或 Process Object.

+

当创建新的 API 时, 最好使用 getter 和 setter 而不是 jQuery 的一次性函数。 举个例子, .getText() 和 .setText(text) 优于 .text([text]).

+
    +
  1. 工程目录结构
    src - main
    // 必须

    +
      +
    • main.js // 入口
    • +
    • listen.js // 监听渲染进程通信
    • +
    • windows.js // 创建渲染进程
    • +
    • log.js // 日志log4js 插件
    • +
    +

    // 可选

    +
      +
    • utils.js
    • +
    • store.js
    • +
    +
  2. +
+
    +
  • api.js
  • +
  • render
  • +
+

main 目录下为主进程代码,render 目录下为渲染进程代码
渲染进程目录结构规范与前端项目目录结构规范一致

+

主进程中必须包含入口文件,监听进程文件和窗口创建文件
其他根据后端实际使用技术可选,nodejs作为后端的话 必须
6. 打包
Electron-packager 绿色可执行包
Electron-builder 压缩安装包

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Electron编码规范.md

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/Electron%E7%BC%96%E7%A0%81%E8%A7%84%E8%8C%83/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2022/06/01/GIT Standard/index.html b/en/2022/06/01/GIT Standard/index.html new file mode 100644 index 0000000000..e5db7a091b --- /dev/null +++ b/en/2022/06/01/GIT Standard/index.html @@ -0,0 +1,654 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GIT Standard.md | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ GIT Standard.md +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

1、说明

项目是一个多人协同完成同一个目标的团队组织形式,在多人协作项目中,如果代码风格统一(前端代码规范和静态代码检查约束)、代码提交信息的说明准确规范(本章介绍),那么在项目开发过程及后期协作以及Bug处理时会更加方便。

+

在本文中,我将介绍大家如何利用工具及约定保证大家代码提交的统一性,从而提高大家的协同效率:

+
    +
  • commitlint: git 提交信息规范与验证
    +

    添加如ESLint的格式规范校验,规范comiit的格式,达到团队每个人的提交风格保持一直,保证提交信息的完整和准确性
    风格如下:

    +
    +
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
<type>(<scope>): <subject> // Header
// 空一行
<body> // 72
// 空一行
<footer> // 72

// 例
fix(登陆模块): 修复登录密码错误的提示

登录没有做友好型提示

修正后不存在此问题
+ +
    +
  • husky: 使git-hook更容易
    +

    husky继承了Git下所有的钩子,在触发钩子的时候,husky可以阻止不合法的commit,push等等。注意使用husky之前,必须先将代码放到git 仓库中,否则本地没有.git文件,就没有地方去继承钩子了。

    +
    +
  • +
  • standard-version: 自动生成CHANGELOG 并发布版本
  • +
+

2、git commit message 规范

commit message格式

1
2
3
4
5
类型(影响范围): 描述

问题描述

修复方式结果
+ + +

注意:
1.冒号后面有空格。
2.英文小括号。
3.正文和注脚前都要加空行。

+

type 类型

用于说明 commit 的类别,只允许使用下面13个标识。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
'feat', // feat:新增功能
'fix', // fix:bug 修复
'docs', // docs:文档更新
'style', // style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑)
'refactor', // refactor:重构代码(既没有新增功能,也没有修复 bug)
'test', // test:新增测试用例或是更新现有测试
'chore', // revert:回滚某个更早之前的提交
'revert', // build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交
'build', // build:打包生产环境代码
'ci', // ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交
'merge', // merge:分支合并 Merge branch ? of ?
'perf', // perf:性能, 体验优化
'chore' // chore:不属于以上类型的其他类型(构建过程或辅助工具的变动)
+ +

如果type为feat和fix,则该 commit 将肯定出现在 Change log 之中。

+

scope 影响范围

可根据项目组需要进行定制化设置,也可不做强制要求

+

subject 描述

subject是 commit 目的的简短描述,不超过50个字符,且结尾不加句号(.)。

+

body 详细描述

body 部分是对本次 commit 的详细描述,可以分成多行。下面是一个范例。

+
1
2
3
此次提交内容包括如下信息:
- 登录为空校验
- 密码错误提示
+

Footer 部分只用于两种情况。

+
    +
  • 不兼容变动(改变解决方案)
  • +
  • 关闭 Issue(回复bug)
  • +
+

3、使用工具校验commit是否符合规范

commitlint

3.1 commitlint安装

1
2
// npm 安装
npm install --save-dev @commitlint/{cli,config-conventional}
+ +

3.2 生成commitlint.config.js配置文件

执行echo命令创建配置文件,也可以手动创建配置文件,或者从已有的配置文件进行拷贝。

+
1
2
// 项目根目录创建commitlint.config.js
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
+ + +

3.3 在commitlint.config.js制定提交message规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// type(scope?): subject

// body?

// footer?
module.exports = {
// 继承自默认规则
extends: ['@commitlint/config-conventional'],
// 这里写自定义规则
rules: {
// 等级 [0 1 2]: 0 不使用规则 1 警告 2 错误
// 启用范围 always|never: 总是或者永不.
// 规则值: 规则对应的值

// 头部(包含type、scope、subject)
'header-case': [0, 'always', 'lower-case'],
'header-full-stop': [0, 'never', '.'],
'header-max-length': [0, 'always', 72],
'header-min-length': [0, 'always', 0],

// 提交类型
'type-case': [0, 'never'],
'type-empty': [2, 'never'],
'type-max-length': [0, 'always', Infinity],
'type-min-length': [0, 'always', 0],
'type-enum': [
2,
'always',
[
'feat', // feat:新增功能
'fix', // fix:bug 修复
'docs', // docs:文档更新
'style', // style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑)
'refactor', // refactor:重构代码(既没有新增功能,也没有修复 bug)
'test', // test:新增测试用例或是更新现有测试
'revert', // revert:回滚某个更早之前的提交
'build', // build:build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交,打包生产环境代码
'ci', // ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交
'merge', // merge:分支合并 Merge branch ? of ?
'perf', // perf:性能, 体验优化
'chore' // chore:不属于以上类型的其他类型(构建过程或辅助工具的变动)
]
],

// 影响范围
'scope-case': [0, 'always', 'lower-case'], // 书写格式
'scope-max-length': [0, 'always', Infinity], // 最长
'scope-min-length': [0, 'always', 0], // 最短
'scope-empty': [2, 'never'], // 影响范围:为空|永不
'scope-enum': [0, 'always', ['a', 'b']], // 影响范围:给出范围但是不在给定的列表内

// 简介
'subject-empty': [2, 'never'], // 简介不能为空
'subject-full-stop': [0, 'never', '.'], // 简介结尾以.结束
'subject-case': [0, 'never'],
'subject-max-length': [0, 'always', Infinity],
'subject-min-length': [0, 'always', 1],

// 正文
'body-leading-blank': [2, 'always'],
'body-max-length': [0, 'always', Infinity],
'body-max-line-length': [0, 'always', Infinity],
'body-min-length': [0, 'always', 1],

// 注脚
'footer-leading-blank': [2, 'always'],
'footer-max-length': [0, 'always', Infinity],
'footer-max-line-length': [0, 'always', Infinity],
'footer-min-length': [0, 'always', 1],

// 其他
'references-empty': [0, 'never'],
'signed-off-by': [0, 'always', 'Signed-off-by']
}
}
+ +

上面我们就完成了commitlint的安装与提交规范的制定。检验commit message的最佳方式是结合git hook,所以需要配合Husky

+

husky

3.4 husky安装

1
npm install husky --save-dev
+

3.5 husky 配置

安装成功后需要在项目下的package.json中配置

+
1
2
3
4
5
6
7
8
9
10
11
// package.json
{
"husky": {
"hooks": { // husky的钩子
"pre-commit": "npm run lint" // 提交前进行代码静态校验
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS" // 进行提交信息格式校验
}
},
"scripts": {}
...
}
+ +

3.5 husky 执行测试

最后我们可以正常的git操作

+
1
2
git add .
git commit -m ""
+ +

git commit的时候会触发commlint。下面演示下不符合规范提交示例:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
C:\lixg\git>git commit -m "thunisoft: abcde"

husky > npm run -s commitmsg (node v8.2.1)

⧗ input:
thunisoft(all): abcde

✖ type must be one of [feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert] [type-enum]
✖ found 1 problems, 0 warnings

husky > commit-msg hook failed (add --no-verify to bypass)

C:\lixg\git>
+ +

上面的提交看反馈消息得知是type格式没有符合限制的提交类型列表,所以提交失败,虾米啊我们把type改为feat再试一下

+
1
2
3
4
5
6
7
8
9
10
11
C:\lixg\git>git commit -m "feat(all): 新功能"

husky > npm run -s commitmsg (node v8.2.1)

⧗ input: feat: 新功能
✔ found 0 problems, 0 warnings

[develop 19dfhe] feat: 新功能
1 file changed, 1 insertion(+)

C:\lixg\git>
+ +

修改后格式符合规范,提交成功。

+

参考来源

+

版权声明

Copyright by lixuguang
未经授权,严禁转载。如需转载,请联系作者

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:GIT Standard.md

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/GIT%20Standard/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/06/01/Restful API \347\232\204\350\256\276\350\256\241\350\247\204\350\214\203/index.html" "b/en/2022/06/01/Restful API \347\232\204\350\256\276\350\256\241\350\247\204\350\214\203/index.html" new file mode 100644 index 0000000000..c853ac96db --- /dev/null +++ "b/en/2022/06/01/Restful API \347\232\204\350\256\276\350\256\241\350\247\204\350\214\203/index.html" @@ -0,0 +1,823 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Restful API 的设计规范.md | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Restful API 的设计规范.md +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

1. URI

URI 表示资源,资源一般对应服务器端领域模型中的实体类。

+

URI规范

    +
  1. 不用大写;
  2. +
  3. 用中杠-不用下杠_;
  4. +
  5. 参数列表要encode;
  6. +
  7. URI中的名词表示资源集合,使用复数形式。
  8. +
+

资源集合 vs 单个资源

URI表示资源的两种方式:资源集合、单个资源。

+

资源集合:

+
1
2
/zoos //所有动物园
/zoos/1/animals //id为1的动物园中的所有动物
+

单个资源:

+
1
2
/zoos/1 //id为1的动物园
/zoos/1;2;3 //id为1,2,3的动物园
+

避免层级过深的URI

/在url中表达层级,用于按实体关联关系进行对象导航,一般根据id导航。

+

过深的导航容易导致url膨胀,不易维护,如 GET /zoos/1/areas/3/animals/4,尽量使用查询参数代替路径中的实体导航,如GET /animals?zoo=1&area=3

+

对Composite资源的访问

服务器端的组合实体必须在uri中通过父实体的id导航访问。

+
+

组合实体不是first-class的实体,它的生命周期完全依赖父实体,无法独立存在,在实现上通常是对数据库表中某些列的抽象,不直接对应表,也无id。一个常见的例子是 User — Address,Address是对User表中zipCode/country/city三个字段的简单抽象,无法独立于User存在。必须通过User索引到Address:GET /user/1/addresses

+
+

2. Request

HTTP方法

通过标准HTTP方法对资源CRUD:

+

GET:查询

+
1
2
3
GET /zoos
GET /zoos/1
GET /zoos/1/employees
+ + +

POST:创建单个资源。POST一般向“资源集合”型uri发起

+
1
2
POST /animals  //新增动物
POST /zoos/1/employees //为id为1的动物园雇佣员工
+ +

PUT:更新单个资源(全量),客户端提供完整的更新后的资源。与之对应的是 PATCH,PATCH 负责部分更新,客户端提供要更新的那些字段。PUT/PATCH一般向“单个资源”型uri发

+
1
2
PUT /animals/1
PUT /zoos/1
+ +

DELETE:删除

+
1
2
3
DELETE /zoos/1/employees/2
DELETE /zoos/1/employees/2;4;5
DELETE /zoos/1/animals //删除id为1的动物园内的所有动物
+

HEAD / OPTION 用的不多,就不多解释了。

+

安全性和幂等性

    +
  1. 安全性:不会改变资源状态,可以理解为只读的;
  2. +
  3. 幂等性:执行1次和执行N次,对资源状态改变的效果是等价的。
  4. +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
.安全性幂等性
GET
POST××
PUT×
DELETE×
+

安全性和幂等性均不保证反复请求能拿到相同的response。以 DELETE 为例,第一次DELETE返回200表示删除成功,第二次返回404提示资源不存在,这是允许的。

+

复杂查询

查询可以捎带以下参数:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
.示例备注
过滤条件type=1&age=16允许一定的uri冗余,如/zoos/1与/zoos?id=1
排序?sort=age,desc
投影?whitelist=id,name,email
分页?limit=10&offset=3
+

Bookmarker

经常使用的、复杂的查询标签化,降低维护成本。

+

如:

+
1
2
GET /trades?status=closed&sort=created,desc

+

快捷方式:

+
1
2
3
GET /trades#recently-closed
// 或者
GET /trades/recently-closed
+ +

Format

只用以下常见的3种body format:

+
    +
  1. Content-Type: application/json
  2. +
+
1
2
3
4
5
6
7
8
9
10
POST /v1/animal HTTP/1.1
Host: api.example.org
Accept: application/json
Content-Type: application/json
Content-Length: 24

{
"name": "Gir",
"animalType": "12"
}
+ +
    +
  1. Content-Type: application/x-www-form-urlencoded (浏览器POST表单用的格式)
  2. +
+
1
2
3
4
5
6
7
POST /login HTTP/1.1
Host: example.com
Content-Length: 31
Accept: text/html
Content-Type: application/x-www-form-urlencoded

username=root&password=Zion0101
+ +
    +
  1. Content-Type: multipart/form-data; boundary=—-RANDOM_jDMUxq4Ot5 (表单有文件上传时的格式)
  2. +
+

6. Response

    +
  1. 不要包装:
    response 的 body 直接就是数据,不要做多余的包装。
  2. +
+

错误示例

+
1
2
3
4
{
"success":true,
"data":{"id":1,"name":"xiaotuan"},
}
+

各HTTP方法成功处理后的数据格式:

+ + + + + + + + + + + + + + + + + + + + + + + +
·response 格式
GET单个对象、集合
POST新增成功的对象
PUT/PATCH更新成功的对象
DELETE
+
    +
  1. json格式的约定:

    +
      +
    1. 时间用长整形(毫秒数),客户端自己按需解析(moment.js)
    2. +
    3. 不传null字段
    4. +
    +
  2. +
+

分页response

1
2
3
4
{
"paging":{"limit":10,"offset":0,"total":729},
"data":[{},{},{}...]
}
+

7. 错误处理

    +
  1. 不要发生了错误但给2xx响应,客户端可能会缓存成功的http请求;
  2. +
  3. 正确设置http状态码,不要自定义;
  4. +
  5. Response body 提供
      +
    1. 错误的代码(日志/问题追查);
    2. +
    3. 错误的描述文本(展示给用户)。
    4. +
    +
  6. +
+

对第三点的实现稍微多说一点:

+

Java 服务器端一般用异常表示 RESTful API 的错误。API 可能抛出两类异常:业务异常非业务异常

+

业务异常由自己的业务代码抛出,表示一个用例的前置条件不满足、业务规则冲突等,比如参数校验不通过、权限校验失败。

+

非业务类异常表示不在预期内的问题,通常由类库、框架抛出,或由于自己的代码逻辑错误导致,比如数据库连接失败、空指针异常、除0错误等等。

+

业务类异常必须提供2种信息:

+
    +
  1. 如果抛出该类异常,HTTP 响应状态码应该设成什么;
  2. +
  3. 异常的文本描述;
  4. +
+

在Controller层使用统一的异常拦截器:

+
    +
  1. 设置 HTTP 响应状态码:对业务类异常,用它指定的 HTTP code;对非业务类异常,统一500;
  2. +
  3. Response Body 的错误码:异常类名
  4. +
  5. Response Body 的错误描述:对业务类异常,用它指定的错误文本;对非业务类异常,线上可以统一文案如“服务器端错误,请稍后再试”,开发或测试环境中用异常的 stacktrace,服务器端提供该行为的开关。
  6. +
+

常用的http状态码及使用场景:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
状态码使用场景
400 bad request常用在参数校验
401 unauthorized未经验证的用户,常见于未登录。如果经过验证后依然没权限,应该 403(即 authentication 和 authorization 的区别)。
403 forbidden无权限
404 not found资源不存在
500 internal server error非业务类异常
503 service unavaliable由容器抛出,自己的代码不要抛这个异常
+

8. 服务型资源

除了资源简单的CRUD,服务器端经常还会提供其他服务,这些服务无法直接用上面提到的URI映射。如:

+
    +
  1. 按关键字搜索;
  2. +
  3. 计算地球上两点间的距离;
  4. +
  5. 批量向用户推送消息;
  6. +
+

可以把这些服务看成资源,计算的结果是资源的presentation,按服务属性选择合适的HTTP方法。

+

例:

+
1
2
3
4
GET /search?q=filter?category=file  搜索
GET /distance-calc?lats=47.480&lngs=-122.389&late=37.108&lnge=-122.448
POST /batch-publish-msg
[{"from":0,"to":1,"text":"abc"},{},{}...]
+

9. 异步任务

对耗时的异步任务,服务器端接受客户端传递的参数后,应返回创建成功的任务资源,其中包含了任务的执行状态。客户端可以轮训该任务获得最新的执行进度。

+
1
2
3
4
5
6
7
8
9
// 提交任务:
POST /batch-publish-msg
[{"from":0,"to":1,"text":"abc"},{},{}...]

// 返回:
{"taskId":3,"createBy":"Anonymous","status":"running"}

GET /task/3
{"taskId":3,"createBy":"Anonymous","status":"success"}
+ +

如果任务的执行状态包括较多信息,可以把“执行状态”抽象成组合资源,客户端查询该状态资源了解任务的执行情况。

+
1
2
3
4
5
6
7
8
9
// 提交任务:
POST /batch-publish-msg
[{"from":0,"to":1,"text":"abc"},{},{}...]

// 返回:
{"taskId":3,"createBy":"Anonymous"}

GET /task/3/status
{"progress":"50%","total":18,"success":8,"fail":1}
+ +

10. API的演进

版本

常见的三种方式:

+
    +
  1. 在uri中放版本信息:GET /v1/users/1
  2. +
  3. Accept Header:Accept: application/json+v1
  4. +
  5. 自定义 Header:X-Api-Version: 1
  6. +
+

用第一种,虽然没有那么优雅,但最明显最方便。

+

URI失效

随着系统发展,总有一些API失效或者迁移,对失效的API,返回404 not found 或 410 gone;对迁移的API,返回 301 重定向。

+

11. 安全

这个不熟,接触到的时候再说。

+

参考文档

    +
  • < RESTful Web Services Cookbook >
  • +
  • Consumer-Centric API Design
  • +
  • RESTful Best Practices
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Restful API 的设计规范.md

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/Restful%20API%20%E7%9A%84%E8%AE%BE%E8%AE%A1%E8%A7%84%E8%8C%83/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2022/06/01/squid/index.html b/en/2022/06/01/squid/index.html new file mode 100644 index 0000000000..8dd43c1d30 --- /dev/null +++ b/en/2022/06/01/squid/index.html @@ -0,0 +1,606 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Squid 服务器学习笔记 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Squid 服务器学习笔记 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Squid 服务器学习笔记

Squid 服务器介绍

用来缓存前端资源的代理服务器,可以类比后端数据库缓存软件Redis。
当用户访问目标资源时由Squid服务器进行判断如果有数据缓存直接将缓存数据返回,如果没有缓存,代理客户访问资源,获取到后返回给用户,当有其他用户访问相同资源时直接返回资源。

+

代理方式

    +
  1. 普通代理
  2. +
  3. 透明代理
  4. +
  5. 反向代理
  6. +
+

安装

1
2
3
4
5
6
7
8
9
10
11
12
// 检查是否已经安装
rpm -qa | grep squid
// 安装软件
rpm install squid
// 设置开机启动
chkconfig squid on
// 启动服务
service squid start
// 查看服务状态(端口监听3128)
netstat -anput |grap squid
// 关闭服务
service squid stop
+ +

配置文件

/etc/squid/squid.config

+

10.0.0.0/8

+
+

扩展知识
CIDR - 无类域间路由CIDR(Classless InterDomain Routing)
ip每位占1个字节8位,00000000 ~ 11111111,也就是0 ~ 255,ip共有四个位数。
10.0.0.0/8 的意思是 8位不可变,剩下的可变,8位这里的意思是说第一个IP段,那么也就是说IP范围是10.0.0.0 ~ 10.255.255.255 这样的一个ip范围。

+
+

// 复制并重命名
mv /etc/squid/squid.config{,.bak}
// 删除文件中的注释和空行(只保留有效设定)
awk ‘if($0!~ /^#/ && $0~ !/^$/print $0)’ /etc/squid/squid.config.bak > /etc/squid/squid.config

+

squid常用命令:
/usr/local/squid/sbin/squid -z 初始化缓存空间
/usr/local/squid/sbin/squid 启动
/usr/local/squid/sbin/squid -k shutdown 停止
/usr/local/squid/sbin/squid -k reconfigure 重新载入配置文件
/usr/local/squid/sbin/squid -k rotate 轮循日志

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#acl all src 0.0.0.0/0.0.0.0 and http_access allow all选项定义了一个访问控制列表。详细情况参见和Squid软件
#携带的文档。这里的访问控制列表允许所有对代理服务的访问,因为这里该代理是加速web服务器。
acl all src 0.0.0.0/0.0.0.0
#允许所有IP访问
acl manager proto http #manager url协议为http
acl localhost src 127.0.0.1/255.255.255.255 #允午本机IP
acl to_localhost dst 127.0.0.1 #允午目的地址为本机IP
acl Safe_ports port 80 # 允许安全更新的端口为80
acl CONNECT method CONNECT #请求方法以CONNECT
http_access allow all #允许所有人使用该代理.因为这里是代理加速web服务器
http_reply_access allow all #允许所有客户端使用该代理

acl OverConnLimit maxconn 16 #限制每个IP最大允许16个连接,防止攻击
http_access deny OverConnLimit

icp_access deny all #禁止从邻居服务器缓冲内发送和接收ICP请求.
miss_access allow all #允许直接更新请求
ident_lookup_access deny all #禁止lookup检查DNS
http_port 8080 transparent #指定Squid监听浏览器客户请求的端口号。

hierarchy_stoplist cgi-bin ? #用来强制某些特定的对象不被缓存,主要是处于安全的目的。
acl QUERY urlpath_regex cgi-bin \?
cache deny QUERY

cache_mem 1 GB #这是一个优化选项,增加该内存值有利于缓存。应该注意的是:
#一般来说如果系统有内存,设置该值为(n/)3M。现在是3G 所以这里1G
fqdncache_size 1024 #FQDN 高速缓存大小
maximum_object_size_in_memory 2 MB #允许最大的文件载入内存

memory_replacement_policy heap LFUDA #动态使用最小的,移出内存cache
cache_replacement_policy heap LFUDA #动态使用最小的,移出硬盘cache

cache_dir ufs /home/cache 5000 32 512 #高速缓存目录 ufs 类型使用的缓冲值最大允午1000MB空间,
#32个一级目录,512个二级目录

max_open_disk_fds 0 #允许最大打开文件数量,0 无限制
minimum_object_size 1 KB #允午最小文件请求体大小
maximum_object_size 20 MB #允午最大文件请求体大小

cache_swap_low 90 #最小允许使用swap 90%
cache_swap_high 95 #最多允许使用swap 95%

ipcache_size 2048 # IP 地址高速缓存大小 2M
ipcache_low 90 #最小允许ipcache使用swap 90%
ipcache_high 95 #最大允许ipcache使用swap 90%


access_log /var/log/squid/access.log squid #定义日志存放记录
cache_log /var/log/squid/cache.log squid
cache_store_log none #禁止store日志

emulate_httpd_log on #将使Squid仿照Web服务器的格式创建访问记录。如果希望使用
#Web访问记录分析程序,就需要设置这个参数。

refresh_pattern . 0 20% 4320 override-expire override-lastmod reload-into-ims ignore-reload #更新cache规则

acl buggy_server url_regex ^http://.... http:// #只允许http的请求
broken_posts allow buggy_server

acl apache rep_header Server ^Apache #允许apache的编码
broken_vary_encoding allow apache

request_entities off #禁止非http的标分准请求,防止攻击
header_access header allow all #允许所有的http报头
relaxed_header_parser on #不严格分析http报头.
client_lifetime 120 minute #最大客户连接时间 120分钟

cache_mgr sky@test.com #指定当缓冲出现问题时向缓冲管理者发送告警信息的地址信息。

cache_effective_user squid #这里以用户squid的身份Squid服务器
cache_effective_group squid

icp_port 0 #指定Squid从邻居服务器缓冲内发送和接收ICP请求的端口号。
#这里设置为0是因为这里配置Squid为内部Web服务器的加速器,
#所以不需要使用邻居服务器的缓冲。0是禁用

cache_peer 127.0.0.1 parent 80 0 no-query default multicast-responder no-netdb-exchange #cache_peer 设置允许更新缓存的主机,因是本机所以127.0.0.1
cache_peer_domain 127.0.0.1
hostname_aliases 127.0.0.1

error_directory /usr/share/squid/errors/Simplify_Chinese #定义错误路径

always_direct allow all # cache丢失或不存在是允许所有请求直接转发到原始服务器
ignore_unknown_nameservers on #开反DNS查询,当域名地址不相同时候,禁止访问
coredump_dir /var/log/squid #定义dump的目录

max_filedesc 2048 #最大打开的文件描述

half_closed_clients off #使Squid在当read不再返回数据时立即关闭客户端的连接。
#有时read不再返回数据是由于某些客户关闭TCP的发送数据
#而仍然保持接收数据。而Squid分辨不出TCP半关闭和完全关闭。

buffered_logs on #若打开选项“buffered_logs”可以稍稍提高加速某些对日志文件的写入,该选项主要是实现优化特性。

acl tianya referer_regex -i tianya #防止天涯盗链,转嫁给百度
http_access deny tianya
deny_info tianya

acl baidu req_header User-Agent Baiduspider #阻止baidu蜘蛛
http_access deny baidu

acl OverConnLimit maxconn 128 #限制同一IP客户端的最大连接数
http_access deny OverConnLimit

acl myip dst 222.18.63.37 #防止被人利用为HTTP代理,设置允许访问的IP地址
http_access deny !myip

acl Manager proto cache_object #允许本地管理
acl Localhost src 127.0.0.1 222.18.63.37
http_access allow Manager Localhost
cachemgr_passwd 53034338 all
http_access deny Manager

acl all src 0.0.0.0/0.0.0.0 #仅仅允许80端口的代理
acl Safe_ports port 80 # http
http_access deny !Safe_ports
http_access allow all

visible_hostname happy.swjtu.edu.cn #Squid信息设置
cache_mgr ooopic2008@qq.com

cache_effective_user squid #基本设置
cache_effective_group squid
tcp_recv_bufsize 65535 bytes

cache_peer 127.0.0.1 parent 80 0 no-query originserver #2.6的反向代理加速配置

error_directory /usr/local/squid/share/errors/Simplify_Chinese #错误文档

icp_port 0 #单台使用,不使用该功能

hierarchy_stoplist cgi-bin ?

acl QUERY urlpath_regex cgi-bin \? .php .cgi .avi .wmv .rm .ram .mpg .mpeg .zip .exe
cache deny QUERY

acl apache rep_header Server ^Apache
broken_vary_encoding allow apache


refresh_pattern ^ftp: 1440 20% 10080
refresh_pattern ^gopher: 1440 0% 1440
refresh_pattern . 0 20% 4320

cache_store_log none
pid_filename /usr/local/squid/var/logs/squid.pid
emulate_httpd_log on
logformat combined %>a %ui %un [%tl] "%rm %ru HTTP/%rv" %Hs %<st "%{Referer}>h" "%{User-Agent}>h" %Ss:%Sh
cache_log /usr/local/squid/var/logs/cache.log
access_log /usr/local/squid/var/logs/access.log combined
coredump_dir /usr/local/squid/var/cache
cache_dir ufs /usr/local/squid/var/cache 10000 16 256

dns_children 32
hosts_file /etc/hosts

cache_mem 400 MB
cache_swap_low 90
cache_swap_high 95
maximum_object_size 32768 KB
maximum_object_size_in_memory 4096 KB
emulate_httpd_log on

acl picurl url_regex -i \.bmp$ \.png$ \.jpg$ \.gif$ \.jpeg$ #防止盗链
acl mystie1 referer_regex -i happy.swjtu.edu.cn
http_access allow mystie1 picurl
acl nullref referer_regex -i ^$
http_access allow nullref
acl hasref referer_regex -i .+
http_access deny hasref picurl
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Squid 服务器学习笔记

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/squid/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\346\214\207\345\215\227/index.html" "b/en/2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\346\214\207\345\215\227/index.html" new file mode 100644 index 0000000000..f7a186c473 --- /dev/null +++ "b/en/2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\346\214\207\345\215\227/index.html" @@ -0,0 +1,728 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前后端分离开发指南 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前后端分离开发指南 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

背景

前后端分离狭义上来讲是以浏览器为分界,应用在浏览器内部的技术为前端技术,主要负责页面展示的部分,应用在浏览器外的技术为后端技术,主要负责处理业务逻辑和数据准备工作,但是随着近几年NodeJs的崛起,前端的工作范畴明显扩大,前后端分离也不仅仅作为一种开发模式,更是web应用架构层面的一种模式。

+

认识

    +
  1. 在开发阶段,前后端工程师要提前约定好数据交互的接口,从而实现并行开发和测试;
  2. +
  3. 在运行阶段,前后端分离模式需要对web应用进行分离部署,前后端之前使用HTTP或者其他协议进行交互请求。(NodeJs或Ngnix)
  4. +
+

由上述两条可以看出前后端分离分为开发阶段和部署阶段,通常来说我们做到的只是前后端分离开发,并没有做到分离部署。

+

分解

作为一种全新的架构模式,前后端分离需要从四个方面来比较和认识。

+
    +
  1. 交互形式
  2. +
  3. 开发模式/流程
  4. +
  5. 代码组织方式
  6. +
  7. 数据接口规范流程
  8. +
+

一、交互形式

在前后端分离架构中,后端只需要负责按照约定的数据格式向前端提供可调用的API服务即可。前后端之间通过HTTP请求进行交互,前端获取到数据后,进行页面的组装和渲染,最终返回给浏览器。

+

这里有一个经常引起激烈讨论的话题就是,“约定的数据格式”,是后端直接提供给前端可直接使用的数据格式,还是简单的数据库拉取数据不做加工直接推给前端,这个要视具体的项目情况而定,如果后端代码需要支持的不仅仅是一端,比如要同时适配pc端、手机端等多个终端,其多终端数据展现的形式如果各不相同,那后端只需要提供满足多端的基础数据格式即可,但如果只需要满足单个终端,那么建议直接将前后端数据格式进行统一处理,方便前后端协同,数据处理可以放在java端做也可以放在NodeJs端做,视人员配备情况而定,但绝不是后端不做任何处理就将数据返回给前端,完全由前端组织数据。

+

二、开发模式/流程

在前后端分离模式未诞生之前,前后端属于一个整体,那时使用的是MVC架构模式,前端对应的就是View层,主要通过html/css/js实现静态页面和动态效果,在有后端进行模板变量的嵌套和一些页面逻辑的处理,最终打包成一个整体,部署到同一服务器上,同时会进行简单的动静态分离部署。

+

此时开发的流程如下:
需求=》前后端并行开发=》前端开发静态页面=》后端套模板=》集成问题=》前后端调整=》再集成=》解决集成问题=》交付上线
// TODO 这里插图

+

出现前后端分离架构之后,前端工程师只需要编写前端页面+前端数据、业务逻辑处理,之后通过HTTP或其他请求方式调用后端提供的服务接口就可以了,而且除了在开发周期可以进行前后端分离,在部署阶段,前后端也可进行分离部署。
// TODO 这里插图

+

此时开发的流程如下:
需求=》设计接口、约定数据=》前后端并行开发=》集成=》调整=》集成成功=》交付上线

+

通过上面的描述及流程,不难发现,前后端分离的开发方式不仅仅从分工上进行了区分,更重要的是在并行开发的问题上解决了反复集成等前后端互相影响的问题,从而降低了开发的难度,简化了开发的流程。

+

三、代码组织方式

// TODO 这里插图
在传统的开发模式架构下,前端代码是作为项目的静态资源存在于项目工程下,页面中还夹着一些后端代码如jsp、php等技术,前后端开发时需要将整个项目代码完整的引入开发工具才能进行开发,前后端同时维护一份代码,这种开发方式导致前后端代码互相影响,因此前后端分离势在必行。

+

而前后端分离模式在代码的组织形式上由以下两种形式组成:

+
    +
  1. 半分离
    前后端仍共用一个代码库,但是代码分别存放在两个工程中。后端不关心或很少 关心前端元素的输出情况,前端不能独立进行开发和测试,项目中缺乏前后端 交互的测试用例。
  2. +
  3. 完全分离
    完全分离后,前端代码可以通过Mock来模拟后端请求,从此可以独立进行前端开发和测试。后端代码只需要按照跟前端约定好的接口格式写出完整的测试用例,确保接口的可用性。通过上述手段,降低开发集成风险。
  4. +
+

四、数据接口规范流程

通过上面三段的描述,我们可以看出前后端分离开发模式最重要也是最初的阶段就是数据接口的确定,因此在项目开发前必需先进行数据和接口的定义,数据接口的定义需要前后端开发共同商定,包括确定的数据格式,交互形式,并生成一份接口文档供前后端开发人员使用。之后才是并行开发。开发期间前后端双方需要严格按照确定的数据接口文档进行开发,前端开发完之后可以利用mock服务独自进行接口测试,后端也可以利用postman或其他接口测试工具进行测试,并提供完整的接口测试用例,然后前后端进行功能联调,最后再提交线上测试,也可进行自动化测试。

+

// TODO 这里插图

+

分离后的收益

到底分不分,如何分是个持续讨论的话题,通过上述的内容大家已经了解到了,什么是前后端分离,也知道如何进行前后端分离开发部署,那么前后端分离能带来哪些收益呢?

+

首先,就目前的软件开发应用趋势来看,越来越注重用户的体验性,而且架构越来越大,服务越来越小,而且终端设备越来越丰富,而原来不分离的方式已经不能支撑现在的发展趋势,因此前后端分离开发及部署将势在必行。

+

而且采用前后端分离的架构之后,我们将有如下几点提升:

+
    +
  1. 前后端分离后,前后端将不再互相纠缠而是各自在自己熟悉的领域进行开发工作,这将有利于前后端深化优化各自的代码,培养各自独特的技术特性,从而开发出更加优秀的应用,建立起专业精良的全栈开发团队。

    +
  2. +
  3. 通过前后端分离架构可以实现前后端开发从代码及开发流程上的完全解耦,只需要前后端共同商定好接口后,便可完全独立开发,只需要在联调阶段进行好协作,在此之前可以互不影响的进行并行开发,即是需求发生了变动,但只要不影响接口,后端既可以不用修改代码,只需前端进行变动即可,如此整体的开发效率将得到提升。

    +
  4. +
  5. 前后端分离后,能够更好的适应前端日益增多的的终端适配,代码解耦后复用率更高。

    +
  6. +
+

4.前后端分离后,前后端代码可以分别管理,代码不再混在一起,代码可维护性也增强了

+

前后端分离后收益不止以上四点,因为分离而带来的职责上、技术上、代码上、部署上的解耦让开发工作比以往任何时候都要更加专注和轻松。

+

注意事项

前后端分离误区

    +
  1. 前端人员不充足,不能进行前后端分离。
  2. +
+

此话说来是因为对前后端分离后职责区分不明确导致的问题,因为以往的前端只需要写静态页面就可以了,而前后端分离后前端也不仅仅需要写静态页面,而且还要为页面提供数据和页面逻辑的处理,但实际上可以根据团队情况来区别对待,如果团队前端人员充足,那么可以由前端人员负责多一些的工作,比如API请求后的业务逻辑的处理,页面逻辑的处理、页面数据的准备等,如果后端人员配备充足,那么上述几个环节仍然可由后端人员进行处理,前端开发仍然只是写静态页面,只是内容和逻辑不再写死而是通过js或其他手段如mvvm的框架进行处理。

+
    +
  1. 前后端分离后前端任务加重,职责也不清晰。
  2. +
+

如第一点描述可知,问题不在前后端分离的模式是否合适,而是任务分配和人员分配上的问题,如果前端能力强且人员比例较多,那么部分任务可以由前端承担,如果后端人员多,那么任务由后端承担。

+
    +
  1. 后端开发需要增加接口开发工作,增加任务量。
  2. +
+

无论如何后端开发都是需要写接口的,只是前后端分离后需要按照ResetFul风格写接口,或者采用最新的GraphQl的方式进行交互,如果说这个阶段需要前后端进行商量确定接口交互形式和数据格式花费了时间,但是在接下来前后端并行开发及问题解决上省掉的时间是更加可观的。

+
    +
  1. 分离后仍出现互相等待的问题,反而不如传统开发模式快。
  2. +
+

这个问题的产生其实也是由于对前后端分离后技术缺失导致的,常见情况是前端写完页面逻辑和假数据后后端开发还未完成接口开发导致无法进行联调,实际上前端通过mockserver等方式是可以解决一些问题的。

+

前后端分离适用场景

现代化的web应用适合用前后端分离的开发方式。
原因有以下几点:

+
    +
  1. web应用前端页面交互复杂。
      +
    • 页面渲染数据量大。
    • +
    • 页面包含复杂的业务逻辑。
    • +
    +
  2. +
  3. 终端适配情况多。
  4. +
  5. 分布式架构,微服务化应用场景。
  6. +
+

前后端分离具体方案

总体方向

后端专注于:后端控制层(Restful API) & 服务层 & 数据访问层;

+

前端专注于:前端控制层(Nodejs) & 视图层

+
    +
  1. 项目设计阶段,前后端架构负责人将项目整体进行分析,讨论并确定API风格、职责分配、开发协助模式,确定人员配备;设计确定后,前后端人员共同制定开发接口。

    +
  2. +
  3. 项目开发阶段,前后端分离是各自分工,协同敏捷开发,后端提供Restful API,并给出详细文档说明,前端人员进行页面渲染前台的任务是发送API请(GET,PUT,POST,DELETE等)获取数据(json,xml)后渲染页面。

    +
  4. +
  5. 项目测试阶段,API完成之前,前端人员会使用mock server进行模拟测试,后端人员采用junit进行API单元测试,不用互相等待;API完成之后,前后端再对接测试一下就可以了,当然并不是所有的接口都可以提前定义,有一些是在开发过程中进行调整的。

    +
  6. +
  7. 项目部署阶段,利用nginx 做反向代理,即Java + nodejs + nginx 方式进行。

    +
  8. +
+

技术手段

    +
  • 前端技术栈:前端代码 + mock服务
  • +
  • 后端技术栈:postman + 接口 + 后端业务逻辑 + 数据库
  • +
  • 公共依赖:接口文档/接口测试工具
  • +
+

常用的mock服务为jsonserver
常用的接口测试工具为postman、rap、swagger、doclever

+

部署方案

    +
  1. 第一阶段为前后端同一个代码库,同一个服务器,集中部署。
  2. +
  3. 第二阶段引入Ngnix服务作为中间件,前端向Ngnix发请求,Ngnix向后端服务发请求,由于Ngnix为静态服务器,所以在seo优化上和页面性能优化上效果不明显,因此前端仍需与后端进行配合才能达到整体的优化。
  4. +
+

浏览器 =》 Ngnix(前端机)=》Ngnix(后端机可没有)=》Server服务

+
    +
  1. 第三阶段引入nodejs作为中间层,将前端资源部署到Server层。同时实现数据代理服务,负责与提供数据的后端进行通信。
  2. +
+

浏览器=》Ngnix(前端机)=》NodeServer =》Server服务

+

浏览器向前端机发送请求,由Ngnix进行分发,url统一分发至NodeServer,在Node Server中根据请求类型从后端服务器上通过RPC服务请求页面的模板数据,然后进行页面的组装和渲染;API请求则直接转发到后端服务挖成相应。

+

前后端分离部署方案比较

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
属性字段传统模式Ngnix+ServerNode+ServerNgnix+Node+Server
SEOoknookok
浏览器渲染负担oknookok
前后端耦合nookokok
请求相应效率nooknook
+

结语

随着前端技术的快速发展和对用户体验日益增长的需求,前后端分离模式势必将会成为主流趋势。无论是从开发模式的角度上来说,还是对团队成长的角度上来说,前后端分离都会带来益处,让我们一同拥抱前后端分离,打造精良的开发团队,迎接日益复杂的web应用开发需求。

+

参考来源

+

版权声明

Copyright by lixuguang
未经授权,严禁转载。如需转载,请联系作者

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前后端分离开发指南

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\350\247\204\350\214\203/index.html" "b/en/2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\350\247\204\350\214\203/index.html" new file mode 100644 index 0000000000..7d869a4cf2 --- /dev/null +++ "b/en/2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\350\247\204\350\214\203/index.html" @@ -0,0 +1,660 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前后端分离开发规范 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前后端分离开发规范 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前后端分离开发规范

by lixg

+

(本文所说的前后端工作皆站在工作内容本身,不与职称相关,下面皆如此。)

+

一、为什么要前后端分离

    +
  1. 前后端专注于各自擅长的领域
  2. +
  3. 前端配置后端代码运行环境,节省搭建环境的时间
  4. +
  5. 明确前后端工作职责
  6. +
  7. 提高开发效率
  8. +
  9. 分离有助于前端、后端分别优化
  10. +
+

二、前后端分离存在的问题

    +
  1. 前后端分离必须接口先行,无接口导致前后端关于接口的工作重复出现。
  2. +
  3. 接口对接方式不一致,各项目团队没有采用统一的接口对接方式,导致每次都要熟悉一种新的对接方式。
  4. +
  5. 数据传输格式不一致,导致每次都要重新熟悉,另外由于接口格式不一致还会引起其他未知问题,影响系统健壮性。
  6. +
  7. 人员比例问题,目前公司前后端比例不完全符合前后端分离开发的要求。
  8. +
+

为解决上述问题,提高前后端分离开发效率,特制定如下规范。

+

三、如何做分离

    +
  1. 职责分离
      +
    • 前后端仅仅通过异步接口(AJAX/JSONP)来编程
    • +
    • 前后端都各自有自己的开发流程,构建工具,测试集合
    • +
    • 关注点分离,前后端变得相对独立并松耦合
      前后端职责
    • +
    +
  2. +
+ + + + + + + + + + + + + + + + + + + + + + + +
后端前端
提供数据接收数据,展示数据
处理业务逻辑处理渲染逻辑
Server-side MVC架构Client-sideMV*架构
代码运行在服务器上代码运行在浏览器上
+
    +
  1. 开发流程
      +
    • 前后端技术负责人约定好接口格式
    • +
    • 后端编写和维护接口文档,在 API 变化时更新接口文档 + MOCK平台
    • +
    • 后端根据接口文档进行接口开发
    • +
    • 前端根据接口文档 + MOCK平台进行开发
    • +
    • 开发完成后联调和提交测试
    • +
    +
  2. +
+

MOCK平台统一采用公司搭建的YAPI平台

+

YAPI平台可以对接SWAGGER工具进行自动构建MOCK服务
前后端开发流程
3. 规范原则
- 接口返回数据即显示:前端仅做渲染逻辑处理
- 渲染逻辑禁止跨多个接口调用
- 前端关注交互、渲染逻辑,尽量避免业务逻辑处理的出现
- 请求响应传输数据格式:JSON,JSON数据尽量简单轻量,避免多级JSON的出现

+

四、基本格式

接口定义参见《RESTFul API的设计规范》

请求格式

GET请求、POST请求==必须包含key为body的入参,所有请求数据包装为JSON格式,并存放到入参body中==,示例如下:

+

GET请求:

+
1
xxx/login?body={"username":"admin","password":"123456","captcha":"scfd","rememberMe":1}
+ +

响应格式

对于通用业务数据响应参照基本数据格式要求

+

响应基本数据格式

1
2
3
4
{
"code": 200,
"msg": "success"
}
+
响应实体格式
1
2
3
4
5
6
7
8
9
10
11
{
"code": 200,
"msg": "success",
"data": {
"entity": {
"id": 1,
"name": "XXX",
"phone": "XXX"
}
}
}
+

entity: 响应返回的实体数据

+
响应列表格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"code": 200,
"msg": "success",
"data": {
"list":[
{
"id": 1,
"name": "XXX",
"code": "XXX"
},
{
"id": 2,
"name": "XXX",
"code": "XXX"
},
{
"id": 3,
"name": "XXX",
"code": "XXX"
}
]
}
}
+

list: 响应返回的列表数据

+
响应分页格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"code": 200,
"msg":"success",
"data": {
"totalCount": 2, // 总记录数
"totalPage": 1 // 总页数
"pageNo": 1, // 当前页码
"pageSize": 10, // 每页大小
"list":[
{
"id": 1,
"name": "XXX",
"code": "XXX"
},
{
"id": 2,
"name": "XXX",
"code": "XXX"
},
{
"id": 3,
"name": "XXX",
"code": "XXX"
}
],
}
}
+

响应特殊数据格式

对于特定组件数据格式由后端统一处理后返回前端,如(echart、ztree等组件)

+

特殊内容规范

布尔类型

关于布尔类型,一律返回BOOLEN类型值

+
日期格式

关于日期类型,JSON数据传输中一律使用字符串格式时间戳,具体日期格式因业务而定

+

五、相关文章导读

前后端分离开发指南-理论篇
前后端分离开发指南-实践篇

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前后端分离开发规范

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB%E5%BC%80%E5%8F%91%E8%A7%84%E8%8C%83/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/06/01/\345\211\215\347\253\257\345\274\200\345\217\221\346\217\222\344\273\266/index.html" "b/en/2022/06/01/\345\211\215\347\253\257\345\274\200\345\217\221\346\217\222\344\273\266/index.html" new file mode 100644 index 0000000000..cf6fd6678a --- /dev/null +++ "b/en/2022/06/01/\345\211\215\347\253\257\345\274\200\345\217\221\346\217\222\344\273\266/index.html" @@ -0,0 +1,588 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端开发插件 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端开发插件 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

插件安装

Prettier - Code formatter

格式化工具

+

ESLint

校验规则

+

Vetur

vue代码片段及代码美化

+

Vue 2 Snippets

vue2 代码片段

+

vscode-fileheader

文件注释

+

配置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// setting.json vscode配置文件
{
"workbench.startupEditor": "newUntitledFile",
"terminal.integrated.shell.windows": "C:\\Program Files\\Git\\bin\\bash.exe", \\ 自行配置自己的环境地址
"javascript.updateImportsOnFileMove.enabled": "always",
"editor.tabSize": 2,
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true
},
"sync.gist": "748b4cae5eb6e56d6997978ead096e8f",
"breadcrumbs.enabled": true,
"todohighlight.isEnable": false,
"liveServer.settings.donotShowInfoMsg": true,
"search.location": "sidebar",
"workbench.activityBar.visible": true,
"window.menuBarVisibility": "default",
"workbench.statusBar.visible": true,
"editor.snippetSuggestions": "top",
"editor.formatOnPaste": true,
"workbench.colorTheme": "Tiny Light",
"eslint.validate": [
"javascript",
"javascriptreact",
{
"language": "html",
"autoFix": true
},
{
"language": "vue",
"autoFix": true
}
],
"prettier.eslintIntegration": true,
"files.autoSave": "onWindowChange",
"code-runner.saveAllFilesBeforeRun": true,
"vetur.format.defaultFormatter.html": "js-beautify-html",
"vetur.format.defaultFormatter.js": "vscode-typescript",
"prettier.jsxSingleQuote": true,
"prettier.requireConfig": false,
"prettier.arrowParens": "always",
"typescript.format.insertSpaceAfterSemicolonInForStatements": false,
"prettier.stylelintIntegration": true,
"prettier.singleQuote": true,
"prettier.tslintIntegration": true,
"eslint.provideLintTask": true,
"eslint.autoFixOnSave": true,
"editor.mouseWheelZoom": true,
"editor.tabCompletion": "on",
"editor.formatOnType": true,
"eslint.alwaysShowStatus": true,
"eslint.options": {
"configFile": "E:/project/xxjs/fore-core/.eslintrc.js" // 自行配置自己的项目地址
},
"fileheader.Author": "Li.Xg", // 自行配置自己的名称
"fileheader.LastModifiedBy": "Li.Xg" // 自行配置自己的名称
}
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端开发插件

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91%E6%8F%92%E4%BB%B6/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2022/06/02/DockerFile/index.html b/en/2022/06/02/DockerFile/index.html new file mode 100644 index 0000000000..c27b3c971d --- /dev/null +++ b/en/2022/06/02/DockerFile/index.html @@ -0,0 +1,700 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DockerFile | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ DockerFile +

+ + +
+ + + + +
+ + +

DockerFile

原则与建议

    +
  • 容器轻量化。从镜像中产生的容器应该尽量轻量化,能在足够短的时间内停止、销毁、重新生成并替换原来的容器。
  • +
  • 使用 .gitignore。在大部分情况下,Dockerfile 会和构建所需的文件放在同一个目录中,为了提高构建的性能,应该使用 .gitignore 来过滤掉不需要的文件和目录。
  • +
  • 为了减少镜像的大小,减少依赖,仅安装需要的软件包。
  • +
  • 一个容器只做一件事。解耦复杂的应用,分成多个容器,而不是所有东西都放在一个容器内运行。如一个 Python Web 应用,可能需要 Server、DB、Cache、MQ、Log 等几个容器。一个更加极端的说法:One process per container。
  • +
  • 减少镜像的图层。不要多个 Label、ENV 等标签。
  • +
  • 对续行的参数按照字母表排序,特别是使用apt-get install -y安装包的时候。
  • +
  • 使用构建缓存。如果不想使用缓存,可以在构建的时候使用参数–no-cache=true来强制重新生成中间镜像。
  • +
+

常用命令

FROM

FROM 指令用于指定其后构建新镜像所使用的基础镜像

+
1
2
3
FROM <image>
FROM <image>:<tag>
FROM <image>:<digest>
+

FROM 必须 是 Dockerfile 中第一条非注释命令
在一个 Dockerfile 文件中创建多个镜像时,FROM 可以多次出现。只需在每个新命令 FROM 之前,记录提交上次的镜像 ID。
tag 或 digest 是可选的,如果不使用这两个值时,会使用 latest 版本的基础镜像

+

RUN

在镜像的构建过程中执行特定的命令,并生成一个中间镜像

+
1
2
3
4
#shell格式
RUN <command>
#exec格式
RUN ["executable", "param1", "param2"]
+
    +
  • RUN 命令将在当前 image 中执行任意合法命令并提交执行结果。命令执行提交后,就会自动执行 Dockerfile 中的下一个指令。
  • +
  • 层级 RUN 指令和生成提交是符合 Docker 核心理念的做法。它允许像版本控制那样,在任意一个点,对 image 镜像进行定制化构建。
  • +
  • RUN 指令创建的中间镜像会被缓存,并会在下次构建中使用。如果不想使用这些缓存镜像,可以在构建时指定 ######no###cache 参数,如:docker build ######no###cache。
  • +
+

FROM scratch

scratch 是一个空的虚拟的镜像源文件,因为需要的依赖都存在不需要基础镜像,从而减小体积。

+

COPY

复制文件

+
1
2
COPY <源路径>... <目标路径>
COPY ["<源路径1>",... "<目标路径>"]
+

和 RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的<目标路径>位置。比如:

+
1
COPY package.json /usr/src/app/
+ +

<源路径>可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,如:

+
1
2
COPY hom* /mydir/
COPY hom?.txt /mydir/
+ +

<目标路径>可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

+

此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。

+

ADD

更高级的复制文件
ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。比如<源路径>可以是一个 URL,这种情况下,Docker 引擎会试图去下载这个链接的文件放到<目标路径>去。

+

在构建镜像时,复制上下文中的文件到镜像内,格式:

+
1
2
ADD <源路径>... <目标路径>
ADD ["<源路径>",... "<目标路径>"]
+

注意
如果 docker 发现文件内容被改变,则接下来的指令都不会再使用缓存。关于复制文件时需要处理的/,基本跟正常的 copy 一致

+

ENV

设置环境变量
格式有两种:

+
1
2
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
+

这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。

+
1
2
ENV VERSION=1.0 DEBUG=on \
NAME="Happy Feet"
+

这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。

+

EXPOSE

为构建的镜像设置监听端口,使容器在运行时监听。
格式:

+
1
EXPOSE <port> [<port>...]
+

EXPOSE 指令并不会让容器监听 host 的端口,如果需要,需要在 docker run 时使用 -p、-P 参数来发布容器端口到 host 的某个端口上。

+

VOLUME

定义匿名卷
VOLUME用于创建挂载点,即向基于所构建镜像创始的容器添加卷:

+
1
VOLUME ["/data"]
+

一个卷可以存在于一个或多个容器的指定目录,该目录可以绕过联合文件系统,并具有以下功能:

+
    +
  • 卷可以容器间共享和重用
  • +
  • 容器并不一定要和其它容器共享卷
  • +
  • 修改卷后会立即生效
  • +
  • 对卷的修改不会对镜像产生影响
  • +
  • 卷会一直存在,直到没有任何容器在使用它
    VOLUME 让我们可以将源代码、数据或其它内容添加到镜像中,而又不并提交到镜像中,并使我们可以多个容器间共享这些内容。
  • +
+

WORKDIR

WORKDIR用于在容器内设置一个工作目录:

+
1
WORKDIR /path/to/workdir
+

通过WORKDIR设置工作目录后,Dockerfile 中其后的命令 RUN、CMD、ENTRYPOINT、ADD、COPY 等命令都会在该目录下执行。
如,使用WORKDIR设置工作目录:

+
1
2
3
4
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
+

在以上示例中,pwd 最终将会在 /a/b/c 目录中执行。在使用 docker run 运行容器时,可以通过-w参数覆盖构建时所设置的工作目录。

+

USER

指定当前用户
USER 用于指定运行镜像所使用的用户:

+
1
USER daemon
+

使用USER指定用户时,可以使用用户名、UID 或 GID,或是两者的组合。以下都是合法的指定试:

+
1
2
3
4
5
6
USER user
USER user:group
USER uid
USER uid:gid
USER user:gid
USER uid:group
+

使用USER指定用户后,Dockerfile 中其后的命令 RUN、CMD、ENTRYPOINT 都将使用该用户。镜像构建完成后,通过 docker run 运行容器时,可以通过 -u 参数来覆盖所指定的用户。

+

CMD

CMD用于指定在容器启动时所要执行的命令。CMD 有以下三种格式:

+
1
2
3
CMD ["executable","param1","param2"]
CMD ["param1","param2"]
CMD command param1 param2
+

省略可执行文件的 exec 格式,这种写法使 CMD 中的参数当做 ENTRYPOINT 的默认参数,此时 ENTRYPOINT 也应该是 exec 格式,具体与 ENTRYPOINT 的组合使用,参考 ENTRYPOINT。

+

注意
与 RUN 指令的区别:RUN 在构建的时候执行,并生成一个新的镜像,CMD 在容器运行的时候执行,在构建时不进行任何操作。

+

ENTRYPOINT

ENTRYPOINT 用于给容器配置一个可执行程序。也就是说,每次使用镜像创建容器时,通过 ENTRYPOINT 指定的程序都会被设置为默认程序。ENTRYPOINT 有以下两种形式:

+
1
2
3
ENTRYPOINT ["executable", "param1", "param2"]
ENTRYPOINT command param1 param2
ENTRYPOINT 与 CMD 非常类似,不同的是通过docker run执行的命令不会覆盖 ENTRYPOINT,而docker run命令中指定的任何参数,都会被当做参数再次传递给 ENTRYPOINT。Dockerfile 中只允许有一个 ENTRYPOINT 命令,多指定时会覆盖前面的设置,而只执行最后的 ENTRYPOINT 指令。
+

docker run运行容器时指定的参数都会被传递给 ENTRYPOINT ,且会覆盖 CMD 命令指定的参数。如,执行docker run -d时,-d 参数将被传递给入口点。

+

也可以通过docker run –entrypoint重写 ENTRYPOINT 入口点。如:可以像下面这样指定一个容器执行程序:

+
1
ENTRYPOINT ["/usr/bin/nginx"]
+

完整构建代码:

+
1
2
3
4
5
6
7
8
9
# Version: 0.0.3
FROM ubuntu:16.04
MAINTAINER 何民三 "cn.liuht@gmail.com"
RUN apt-get update
RUN apt-get install -y nginx
RUN echo 'Hello World, 我是个容器' \
> /var/www/html/index.html
ENTRYPOINT ["/usr/sbin/nginx"]
EXPOSE 80
+

使用docker build构建镜像,并将镜像指定为 itbilu/test:

+
1
docker build -t="itbilu/test" .
+

构建完成后,使用itbilu/test启动一个容器:

+
1
docker run -i -t  itbilu/test -g "daemon off;"
+

在运行容器时,我们使用了 -g “daemon off;”,这个参数将会被传递给 ENTRYPOINT,最终在容器中执行的命令为 /usr/sbin/nginx -g “daemon off;”。

+

LABEL

LABEL用于为镜像添加元数据,元数以键值对的形式指定:

+
1
LABEL <key>=<value> <key>=<value> <key>=<value> ...
+

使用LABEL指定元数据时,一条LABEL指定可以指定一或多条元数据,指定多条元数据时不同元数据之间通过空格分隔。推荐将所有的元数据通过一条LABEL指令指定,以免生成过多的中间镜像。
如,通过LABEL指定一些元数据:

+
1
LABEL version="1.0" description="这是一个Web服务器" by="IT笔录"
+

指定后可以通过docker inspect查看:

+
1
2
3
4
5
6
docker inspect itbilu/test
"Labels": {
"version": "1.0",
"description": "这是一个Web服务器",
"by": "IT笔录"
},
+ +

ARG

ARG用于指定传递给构建运行时的变量:

+
1
ARG <name>[=<default value>]
+

如,通过ARG指定两个变量:

+
1
2
ARG site
ARG build_user=IT笔录
+

以上我们指定了 site 和 build_user 两个变量,其中 build_user 指定了默认值。在使用 docker build 构建镜像时,可以通过 –build-arg = 参数来指定或重设置这些变量的值。

+
1
docker build --build-arg site=itiblu.com -t itbilu/test .
+

这样我们构建了 itbilu/test 镜像,其中site会被设置为 itbilu.com,由于没有指定 build_user,其值将是默认值 IT 笔录。

+

ONBUILD

ONBUILD用于设置镜像触发器:

+
1
ONBUILD [INSTRUCTION]
+

当所构建的镜像被用做其它镜像的基础镜像,该镜像中的触发器将会被钥触发。
如,当镜像被使用时,可能需要做一些处理:

+
1
2
3
4
[...]
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
[...]
+ +

STOPSIGNAL

STOPSIGNAL用于设置停止容器所要发送的系统调用信号:

+
1
STOPSIGNAL signal
+

所使用的信号必须是内核系统调用表中的合法的值,如:SIGKILL。

+

SHELL

SHELL用于设置执行命令(shell式)所使用的的默认 shell 类型:

+
1
SHELL ["executable", "parameters"]
+

SHELL在Windows环境下比较有用,Windows 下通常会有 cmd 和 powershell 两种 shell,可能还会有 sh。这时就可以通过 SHELL 来指定所使用的 shell 类型:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM microsoft/windowsservercore

# Executed as cmd /S /C echo default
RUN echo default

# Executed as cmd /S /C powershell -command Write-Host default
RUN powershell -command Write-Host default

# Executed as powershell -command Write-Host hello
SHELL ["powershell", "-command"]
RUN Write-Host hello

# Executed as cmd /S /C echo hello
SHELL ["cmd", "/S"", "/C"]
RUN echo hello
+ +

Dockerfile 示例

    +
  • 构建Nginx运行环境
  • +
  • 构建tomcat 环境
  • +
+

构建Nginx运行环境

Dockerfile文件

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#指定基础镜像
FROM sameersbn/ubuntu:14.04.20161014

#维护者信息
MAINTAINER sameer@damagehead.com

#设置环境
ENV RTMP_VERSION=1.1.10 \
NPS_VERSION=1.11.33.4 \
LIBAV_VERSION=11.8 \
NGINX_VERSION=1.10.1 \
NGINX_USER=www-data \
NGINX_SITECONF_DIR=/etc/nginx/sites-enabled \
NGINX_LOG_DIR=/var/log/nginx \
NGINX_TEMP_DIR=/var/lib/nginx \
NGINX_SETUP_DIR=/var/cache/nginx

#设置构建时变量,镜像建立完成后就失效
ARG BUILD_LIBAV=false
ARG WITH_DEBUG=false
ARG WITH_PAGESPEED=true
ARG WITH_RTMP=true

#复制本地文件到容器目录中
COPY setup/ ${NGINX_SETUP_DIR}/
RUN bash ${NGINX_SETUP_DIR}/install.sh

#复制本地配置文件到容器目录中
COPY nginx.conf /etc/nginx/nginx.conf
COPY entrypoint.sh /sbin/entrypoint.sh

#运行指令
RUN chmod 755 /sbin/entrypoint.sh

#允许指定的端口
EXPOSE 80/tcp 443/tcp 1935/tcp

#指定网站目录挂载点
VOLUME ["${NGINX_SITECONF_DIR}"]

ENTRYPOINT ["/sbin/entrypoint.sh"]
CMD ["/usr/sbin/nginx"]
+ +

构建Tomcat环境

Dockerfile文件

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 指定基于的基础镜像
FROM ubuntu:13.10

# 维护者信息
MAINTAINER zhangjiayang "zhangjiayang@sczq.com.cn"

# 镜像的指令操作
# 获取APT更新的资源列表
RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe"> /etc/apt/sources.list
# 更新软件
RUN apt-get update

# Install curl
RUN apt-get -y install curl

# Install JDK 7
RUN cd /tmp && curl -L 'http://download.oracle.com/otn-pub/java/jdk/7u65-b17/jdk-7u65-linux-x64.tar.gz' -H 'Cookie: oraclelicense=accept-securebackup-cookie; gpw_e24=Dockerfile' | tar -xz
RUN mkdir -p /usr/lib/jvm
RUN mv /tmp/jdk1.7.0_65/ /usr/lib/jvm/java-7-oracle/

# Set Oracle JDK 7 as default Java
RUN update-alternatives --install /usr/bin/java java /usr/lib/jvm/java-7-oracle/bin/java 300
RUN update-alternatives --install /usr/bin/javac javac /usr/lib/jvm/java-7-oracle/bin/javac 300

# 设置系统环境
ENV JAVA_HOME /usr/lib/jvm/java-7-oracle/

# Install tomcat7
RUN cd /tmp && curl -L 'http://archive.apache.org/dist/tomcat/tomcat-7/v7.0.8/bin/apache-tomcat-7.0.8.tar.gz' | tar -xz
RUN mv /tmp/apache-tomcat-7.0.8/ /opt/tomcat7/

ENV CATALINA_HOME /opt/tomcat7
ENV PATH $PATH:$CATALINA_HOME/bin

# 复件tomcat7.sh到容器中的目录
ADD tomcat7.sh /etc/init.d/tomcat7
RUN chmod 755 /etc/init.d/tomcat7

# Expose ports. 指定暴露的端口
EXPOSE 8080

# Define default command.
ENTRYPOINT service tomcat7 start && tail -f /opt/tomcat7/logs/catalina.out
+ +

tomcat7.sh命令文件

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export JAVA_HOME=/usr/lib/jvm/java-7-oracle/  
export TOMCAT_HOME=/opt/tomcat7

case $1 in
start)
sh $TOMCAT_HOME/bin/startup.sh
;;
stop)
sh $TOMCAT_HOME/bin/shutdown.sh
;;
restart)
sh $TOMCAT_HOME/bin/shutdown.sh
sh $TOMCAT_HOME/bin/startup.sh
;;
esac
exit 0
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:DockerFile

+

文章作者:

+

发布时间:2022年06月02日 - 11:20

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/02/DockerFile/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/06/02/Docker\345\221\275\344\273\244/index.html" "b/en/2022/06/02/Docker\345\221\275\344\273\244/index.html" new file mode 100644 index 0000000000..2aedcb1883 --- /dev/null +++ "b/en/2022/06/02/Docker\345\221\275\344\273\244/index.html" @@ -0,0 +1,632 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Docker 命令 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Docker 命令 +

+ + +
+ + + + +
+ + +

Docker 命令

    +
  • 帮助命令
    docker –help
  • +
+

镜像操作

    +
  • 搜索镜像
    docker search hello-world

    +
  • +
  • 下载镜像
    docker pull hello-world

    +
  • +
  • 查看本地已下载所有镜像
    docker images

    +
  • +
  • 查看镜像历史
    docker history hello-world

    +
  • +
  • 备份镜像
    docker tag hello-word:last hello-world:v2

    +
  • +
  • 删除镜像
    docker rmi hello-word:last

    +
  • +
  • 删除未使用过的镜像
    docker image prune

    +
  • +
  • 导出镜像
    docker save -o hello-world:last.tar hello-world:last

    +
  • +
  • 导入镜像
    docker load -i hello-world:last.tar

    +
  • +
  • 查看镜像信息
    docker image inspact nginx

    +
  • +
+

容器操作

    +
  • 查看所有容器 [-q 编号] [-a active 启动的容器]
    docker ps [-a] [-q]

    +
  • +
  • 启动容器 [-d 后台启动]
    docker run -d –name nginx1 nginx:last

    +
  • +
  • 停止容器
    docker stop nginx1

    +
  • +
  • 启动容器()
    docker start nginx1

    +
  • +
  • 删除容器
    docker rm nginx1

    +
  • +
  • 批量删除运行中容器
    docker rm $(docker ps -q) -f

    +
  • +
  • 创建容器并进入
    docker run -it –name nginx1 nginx:last /bin/bash

    +
  • +
  • 退出容器
    exit

    +
  • +
  • 进入容器
    docker exec -it nginx1 /bin/bash

    +
  • +
  • 通过容器创建镜像
    docker commit -m ‘laowang’ nginx1 nginx:v1

    +
  • +
  • 查看容器信息
    docker container inspact nginx1

    +
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Docker 命令

+

文章作者:

+

发布时间:2022年06月02日 - 16:20

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/02/Docker%E5%91%BD%E4%BB%A4/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/06/02/Docker\345\256\271\345\231\250\347\253\257\345\217\243\346\230\240\345\260\204/index.html" "b/en/2022/06/02/Docker\345\256\271\345\231\250\347\253\257\345\217\243\346\230\240\345\260\204/index.html" new file mode 100644 index 0000000000..61f426ed82 --- /dev/null +++ "b/en/2022/06/02/Docker\345\256\271\345\231\250\347\253\257\345\217\243\346\230\240\345\260\204/index.html" @@ -0,0 +1,585 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Docker容器端口映射 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Docker容器端口映射 +

+ + +
+ + + + +
+ + +

Docker容器端口映射

常见容器服务需要做端口映射,这里以nginx为例进行举例

+

启动一个nginx容器

docker run -itd –name nginx1 -P nginx:latest #随机端口
docker run -itd –name nginx2 -p 6001:80 nginx:latest #指定端口
docker run -itd –name nginx3 -p 172.16.10.1:6002:80 nginx:latest #指定IP和端口

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Docker容器端口映射

+

文章作者:

+

发布时间:2022年06月02日 - 11:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/02/Docker%E5%AE%B9%E5%99%A8%E7%AB%AF%E5%8F%A3%E6%98%A0%E5%B0%84/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/06/02/Docker\346\225\260\346\215\256\345\215\267/index.html" "b/en/2022/06/02/Docker\346\225\260\346\215\256\345\215\267/index.html" new file mode 100644 index 0000000000..7c3cb566a2 --- /dev/null +++ "b/en/2022/06/02/Docker\346\225\260\346\215\256\345\215\267/index.html" @@ -0,0 +1,595 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Docker数据卷 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Docker数据卷 +

+ + +
+ + + + +
+ + +

Docker数据卷

宿主机与容器进行数据交互,共享宿主机与容器之间的数据

+

创建数据卷关联

docker run -it –name my_ubuntu1 -v ~ /Desktop/test:/test /bin/bash

+

利用系统方法操作数据卷

    +
  • 查 docker数据卷
    docker volume ls

    +
  • +
  • 创建数据卷
    docker volume create volname

    +
  • +
  • 共享
    docker run -it –name my_ubuntu2 -v volname:/home ubuntu:latest /bin/bash

    +
  • +
+

数据卷容器使用

可以通过数据卷容器,创建新的容器,并将多个容器绑定在一起

+

docker run -itd –name my_ubuntu3 –volumes-from my_ubuntu2 ubuntu:latest

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Docker数据卷

+

文章作者:

+

发布时间:2022年06月02日 - 11:21

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/02/Docker%E6%95%B0%E6%8D%AE%E5%8D%B7/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/06/02/Docker\347\275\221\347\273\234\346\250\241\345\274\217/index.html" "b/en/2022/06/02/Docker\347\275\221\347\273\234\346\250\241\345\274\217/index.html" new file mode 100644 index 0000000000..cfede77364 --- /dev/null +++ "b/en/2022/06/02/Docker\347\275\221\347\273\234\346\250\241\345\274\217/index.html" @@ -0,0 +1,595 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Docker网络模式 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Docker网络模式 +

+ + +
+ + + + +
+ + +

Docker网络模式

Docker常见的两种网络模式

+
    +
  • host网络模式:创建的容器和宿主机共享同一个网卡
  • +
  • bridge网络模式:桥接模式,只要使用桥接模式创建容器网段一样(类似于172.16.1.1,172.16.1.2)有点像是子局域网
  • +
+

利用network命令管理网络模式

    +
  • 查看网络模式
    docker network ls
  • +
  • 创建网络模式
    docker network create –drive bridge bridge_test
  • +
  • 通过network断网
    docker network disconnet bridge nginx5
  • +
  • 通过network联网
    docker network connect bridge nginx5
  • +
  • 删除网络模式
    docker network rm bridge_test
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Docker网络模式

+

文章作者:

+

发布时间:2022年06月02日 - 11:23

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/02/Docker%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%BC%8F/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/06/09/Hexo\345\234\250Github\345\217\221\345\270\203\344\271\213\345\220\216\350\207\252\345\256\232\344\271\211\345\237\237\345\220\215\347\232\204\351\205\215\347\275\256/index.html" "b/en/2022/06/09/Hexo\345\234\250Github\345\217\221\345\270\203\344\271\213\345\220\216\350\207\252\345\256\232\344\271\211\345\237\237\345\220\215\347\232\204\351\205\215\347\275\256/index.html" new file mode 100644 index 0000000000..6c50f57791 --- /dev/null +++ "b/en/2022/06/09/Hexo\345\234\250Github\345\217\221\345\270\203\344\271\213\345\220\216\350\207\252\345\256\232\344\271\211\345\237\237\345\220\215\347\232\204\351\205\215\347\275\256/index.html" @@ -0,0 +1,593 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hexo在Github发布之后自定义域名的配置 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Hexo在Github发布之后自定义域名的配置 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

HexoGithub发布之后自定义域名的配置

进到你的博客发布所在仓库,如:

选择Settings,然后是pages,滚动页面找到Custom domain,将你自己的域名绑定进去保存一下就好了。

+

在你自己的域名商处用CNAME把你自己的域名指定到你自己的仓库page地址上比如我的就是lixuguang.github.io Public,这样你的GitHub Page就可以通过自己的域名进行访问了。

+

HexoGithub发布之后自定义域名会被清空的问题

使用 GitHub Page 功能将博客托管在了 GitHub 上,并配置 CNAME 将自己的域名解析了过去,但是发现一个问题,每次 hexo deploy 之后,custom domain 会被重置失效。

+

解决方案

Hexo 生成的博客的 source 目录下(注意这个是你的博客源码所在仓库,并不是发布的仓库)新建一个 CNAME 文件,然后在这个文件中填入你的域名,这样就不会每次发布之后,GitHub Page 里的 custom domain 都被重置掉啦。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Hexo在Github发布之后自定义域名的配置

+

文章作者:

+

发布时间:2022年06月09日 - 12:40

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/09/Hexo%E5%9C%A8Github%E5%8F%91%E5%B8%83%E4%B9%8B%E5%90%8E%E8%87%AA%E5%AE%9A%E4%B9%89%E5%9F%9F%E5%90%8D%E7%9A%84%E9%85%8D%E7%BD%AE/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/06/09/Nginx\346\214\207\344\273\244/index.html" "b/en/2022/06/09/Nginx\346\214\207\344\273\244/index.html" new file mode 100644 index 0000000000..fe4f7af400 --- /dev/null +++ "b/en/2022/06/09/Nginx\346\214\207\344\273\244/index.html" @@ -0,0 +1,591 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Nginx指令 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Nginx指令 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Nginx指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#启动Nginx
start nginx

#重启Nginx
nginx -s reopen

#重新加载Nginx配置文件,然后以优雅的方式重启Nginx
nginx -s reload

#强制停止Nginx服务
nginx -s stop

#优雅地停止Nginx服务(即处理完所有请求后再停止服务)
nginx -s quit

#检测配置文件是否有语法错误,然后退出
nginx -t

#显示版本信息并退出
nginx -v

#显示版本和配置选项信息,然后退出
nginx -V

#检测配置文件是否有语法错误,然后退出
nginx -t

#检测配置文件是否有语法错误,转储并退出
nginx -T

#在检测配置文件期间屏蔽非错误信息
nginx -q

#打开帮助信息
nginx -?,-h

#设置前缀路径(默认是:/usr/share/nginx/)
nginx -p prefix

#设置配置文件(默认是:/etc/nginx/nginx.conf)
nginx -c filename

#设置配置文件外的全局指令
nginx -g directives
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Nginx指令

+

文章作者:

+

发布时间:2022年06月09日 - 10:40

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/09/Nginx%E6%8C%87%E4%BB%A4/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2022/06/09/http_stub_status_module/index.html b/en/2022/06/09/http_stub_status_module/index.html new file mode 100644 index 0000000000..8232d4595c --- /dev/null +++ b/en/2022/06/09/http_stub_status_module/index.html @@ -0,0 +1,610 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + http_stub_status_module Nginx的性能模块 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ http_stub_status_module Nginx的性能模块 +

+ + +
+ + + + +
+ + +

ngx_http_stub_status_module 是一个 Nginx 的内置 模块,可以提供 Nginx 的简单统计信息。默认情况下这个模块是不被编译进来的,所以在编译 Nginx 时要指定加载该模块:

+
1
--with-http_stub_status_module
+

查看是否安装

1
2
3
4
5
// 查看nginx版本及安装了那些模块
nginx -V

// 如果安装了的话会显示下面的信息
--with-http_stub_status_module
+ +

配置使用

nginx.conf配置文件中进行如下配置

+
1
2
3
location /ngx_status {
stub_status on; // 设置开启性能模块
}
+

通过浏览器访问 http://localhost/ngx_status 就能看到你的nginx服务性能监控画面了
image

+

解析

    +
  • 第一行:Active connections:活动连接数
  • +
  • 第二三行:Server accepts handled requests:(三个数字分别代表)总共处理的连接数,成功握手的连接数量,处理的请求数(正常情况下握手和连接数是相等的,表示没有丢失)
  • +
  • 第四行:
      +
    • Reading: Nginx 读取到客户端的Header信息数
    • +
    • Writing: Nginx 返回给客户端的Header信息数
    • +
    • Waiting: 开启keep-alive的情况下,这个值等于 active – (reading + writing),意思就是Nginx已经处理完成,正在等候下一次请求指令的驻留连接(在nginx开启了keep-alive,也就是长连接的情况下,客户端跟服务端建立了连接但是没有读写操作的空闲状态)
    • +
    +
  • +
+

使用场景

    +
  • 可以简单的用脚本做监控
  • +
  • 可以用开源工具,zabbix,prometheus等去采集nginx的统计信息,做监控和历史数据采集
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:http_stub_status_module Nginx的性能模块

+

文章作者:

+

发布时间:2022年06月09日 - 12:40

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/09/http_stub_status_module/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/06/15/\346\236\266\346\236\204\345\256\236\350\267\265/index.html" "b/en/2022/06/15/\346\236\266\346\236\204\345\256\236\350\267\265/index.html" new file mode 100644 index 0000000000..0201eac9e4 --- /dev/null +++ "b/en/2022/06/15/\346\236\266\346\236\204\345\256\236\350\267\265/index.html" @@ -0,0 +1,618 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 架构实践 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 架构实践 +

+ + +
+ + + + +
+ + +

架构实践

架构设计内容一览

应用架构设计

    +
  • Transaction执行控制(在线/批处理路径和控制)
  • +
  • Session控制
  • +
  • 死锁控制
  • +
  • 接口处理流程(适配器设置/文件备份)
  • +
  • 命名规范(实例/SID等)
  • +
  • 中间件参数 (初始化参数等)
  • +
  • 认证(处理流程/错误处理)
  • +
  • 帐户(用户ID体系/权限管理/申请方式)
  • +
+

开发架构设计

    +
  • 系统景观(各种环境名称、利用方法等)
  • +
  • 转运路线(转运路线/转运工具/承认方式)
  • +
  • 开发账户(用户ID体系/权限管理/申请方式)
  • +
  • 开发终端设置(在线批处理开发工具/目录等)
  • +
  • 开发资源管理
  • +
  • 表单、协作工具的利用方法(表单/接口ID定义规则)
  • +
  • 开发和验证备份(处理流程/文件清除/周期)
  • +
  • 数据屏蔽(処理概要/屏蔽利用规则/対象/开发环境搭建方法)
  • +
+

运维架构设计

    +
  • 监测规范(方式/周期/対象)
  • +
  • 审计和错误日志(日志级别/日志保留时效/删除时效)
  • +
  • 生产环境和调研环境的备份(处理流程/删除时效/周期)
  • +
  • 加密处理(安装位置、加密规则、记录水平加密方法)
  • +
  • 云安全设定
  • +
  • 增加功能设计
  • +
+

基础设施

    +
  • 网络设计(虚拟网/子网/URL/负载均衡访问控制/白名单)
  • +
  • 云设计(实例/NFS/IAM)
  • +
  • 存储设计 (BlockVolume/FileStorage/ObjectStorage/数据配置/数据圧縮方法)
  • +
  • 时间DNS同步化(OCI設定)
  • +
  • OS参数设定
  • +
  • Docker参数设定
  • +
  • 高可用切换 (処理方式、设定参数考量)
  • +
  • 打补丁(打补丁/执行规范)
  • +
  • 虚拟桌面设定
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:架构实践

+

文章作者:

+

发布时间:2022年06月15日 - 14:40

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/15/%E6%9E%B6%E6%9E%84%E5%AE%9E%E8%B7%B5/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/06/16/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206 ---- \347\275\221\347\273\234\345\256\211\345\205\250\357\274\2102\357\274\211/index.html" "b/en/2022/06/16/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206 ---- \347\275\221\347\273\234\345\256\211\345\205\250\357\274\2102\357\274\211/index.html" new file mode 100644 index 0000000000..791599dfa3 --- /dev/null +++ "b/en/2022/06/16/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206 ---- \347\275\221\347\273\234\345\256\211\345\205\250\357\274\2102\357\274\211/index.html" @@ -0,0 +1,728 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 网络安全(2) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 网络安全(2) +

+ + +
+ + + + +
+ + +

前端常见知识点整理 —- 网络安全(2)

SQL 注入

SQL 注入漏洞(SQL Injection)是 Web 开发中最常见的一种安全漏洞。可以用它来从数据库获取敏感信息,或者利用数据库的特性执行添加用户,导出文件等一系列恶意操作,甚至有可能获取数据库乃至系统用户最高权限。

+

而造成 SQL 注入的原因是因为程序没有有效的转义过滤用户的输入,使攻击者成功的向服务器提交恶意的 SQL 查询代码,程序在接收后错误的将攻击者的输入作为查询语句的一部分执行,导致原始的查询逻辑被改变,额外的执行了攻击者精心构造的恶意代码。

+

很多 Web 开发者没有意识到 SQL 查询是可以被篡改的,从而把 SQL 查询当作可信任的命令。殊不知,SQL 查询是可以绕开访问控制,从而绕过身份验证和权限检查的。更有甚者,有可能通过 SQL 查询去运行主机系统级的命令。

+

SQL 注入原理

下面将通过一些真实的例子来详细讲解 SQL 注入的方式的原理。

+

考虑以下简单的管理员登录表单:

+
1
2
3
4
5
<form action="/login" method="POST">
<p>Username: <input type="text" name="username" /></p>
<p>Password: <input type="password" name="password" /></p>
<p><input type="submit" value="登陆" /></p>
</form>
+ +

后端的 SQL 语句可能是如下这样的:

+
1
2
3
4
5
6
7
let querySQL = `
SELECT *
FROM user
WHERE username='${username}'
AND psw='${password}'
`;
// 接下来就是执行 sql 语句...
+ +

目的就是来验证用户名和密码是不是正确,按理说乍一看上面的 SQL 语句也没什么毛病,确实是能够达到我们的目的,可是你只是站在用户会老老实实按照你的设计来输入的角度来看问题,如果有一个恶意攻击者输入的用户名是 zoumiaojiang' OR 1 = 1 --,密码随意输入,就可以直接登入系统了。WFT!

+

冷静下来思考一下,我们之前预想的真实 SQL 语句是:

+
1
SELECT * FROM user WHERE username='zoumiaojiang' AND psw='mypassword'
+ +

可以恶意攻击者的奇怪用户名将你的 SQL 语句变成了如下形式:

+
1
2
SELECT * FROM user WHERE username='zoumiaojiang' OR 1 = 1 --' AND psw='xxxx'

+ +

在 SQL 中,-- 是注释后面的内容的意思,所以查询语句就变成了:

+
1
2
SELECT * FROM user WHERE username='zoumiaojiang' OR 1 = 1

+ +

这条 SQL 语句的查询条件永远为真,所以意思就是恶意攻击者不用我的密码,就可以登录进我的账号,然后可以在里面为所欲为,然而这还只是最简单的注入,牛逼的 SQL 注入高手甚至可以通过 SQL 查询去运行主机系统级的命令,将你主机里的内容一览无余,这里我也没有这个能力讲解的太深入,毕竟不是专业研究这类攻击的,但是通过以上的例子,已经了解了 SQL 注入的原理,我们基本已经能找到防御 SQL 注入的方案了。

+

如何预防 SQL 注入

防止 SQL 注入主要是不能允许用户输入的内容影响正常的 SQL 语句的逻辑,当用户的输入的信息将要用来拼接 SQL 语句的话,我们应该永远选择不相信,任何内容都必须进行转义过滤,当然做到这个还是不够的,下面列出防御 SQL 注入的几点注意事项:

+
    +
  • 严格限制Web应用的数据库的操作权限,给此用户提供仅仅能够满足其工作的最低权限,从而最大限度的减少注入攻击对数据库的危害

    +
  • +
  • 后端代码检查输入的数据是否符合预期,严格限制变量的类型,例如使用正则表达式进行一些匹配处理。

    +
  • +
  • 对进入数据库的特殊字符(',",\,<,>,&,*,; 等)进行转义处理,或编码转换。基本上所有的后端语言都有对字符串进行转义处理的方法,比如 lodash 的 lodash._escapehtmlchar 库。

    +
  • +
  • 所有的查询语句建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中,即不要直接拼接 SQL 语句。例如 Node.js 中的 mysqljs 库的 query 方法中的 ? 占位参数。

    +
  • +
+
1
mysql.query(`SELECT * FROM user WHERE username = ? AND psw = ?`, [username, psw]);
+ +
    +
  • 在应用发布之前建议使用专业的 SQL 注入检测工具进行检测,以及时修补被发现的 SQL 注入漏洞。网上有很多这方面的开源工具,例如 sqlmap、SQLninja 等。

    +
  • +
  • 避免网站打印出 SQL 错误信息,比如类型错误、字段不匹配等,把代码里的 SQL 语句暴露出来,以防止攻击者利用这些错误信息进行 SQL 注入。

    +
  • +
  • 不要过于细化返回的错误信息,如果目的是方便调试,就去使用后端日志,不要在接口上过多的暴露出错信息,毕竟真正的用户不关心太多的技术细节,只要话术合理就行。

    +
  • +
+

碰到要操作的数据库的代码,一定要慎重,小心使得万年船,多找几个人多来几次 code review,将问题都暴露出来,而且要善于利用工具,操作数据库相关的代码属于机密,没事不要去各种论坛晒自家站点的 SQL 语句,万一被人盯上了呢?

+

命令行注入

命令行注入漏洞,指的是攻击者能够通过 HTTP 请求直接侵入主机,执行攻击者预设的 shell 命令,听起来好像匪夷所思,这往往是 Web 开发者最容易忽视但是却是最危险的一个漏洞之一,看一个实例:

+

假如现在需要实现一个需求:用户提交一些内容到服务器,然后在服务器执行一些系统命令去产出一个结果返回给用户,接口的部分实现如下:

+
1
2
3
4
5
// 以 Node.js 为例,假如在接口中需要从 github 下载用户指定的 repo
const exec = require('mz/child_process').exec;
let params = {/* 用户输入的参数 */};

exec(`git clone ${params.repo} /some/path`);
+ +

这段代码确实能够满足业务需求,正常的用户也确实能从指定的 git repo 上下载到想要的代码,可是和 SQL 注入一样,这段代码在恶意攻击者眼中,简直就是香饽饽。

+

如果 params.repo 传入的是 https://github.com/zoumiaojiang/zoumiaojiang.github.io.git 当然没问题了。
可是如果 params.repo 传入的是 https://github.com/xx/xx.git && rm -rf /* && 恰好你的服务是用 root 权限起的就惨了。

+

具体恶意攻击者能用命令行注入干什么也像 SQL 注入一样,手法是千变万化的,比如「反弹 shell 注入」等,但原理都是一样的,我们绝对有能力防止命令行注入发生。防止命令行注入需要做到以下几件事情:

+
    +
  • 后端对前端提交内容需要完全选择不相信,并且对其进行规则限制(比如正则表达式)。
  • +
  • 在调用系统命令前对所有传入参数进行命令行参数转义过滤。
  • +
  • 不要直接拼接命令语句,借助一些工具做拼接、转义预处理,例如 Node.js 的 shell-escape npm 包。
  • +
+

还是前面的例子,我们可以做到如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const exec = require('mz/child_process').exec;

// 借助 shell-escape npm 包解决参数转义过滤问题
const shellescape = require('shell-escape');

let params = {/* 用户输入的参数 */};

// 先过滤一下参数,让参数符合预期
if (!/正确的表达式/.test(params.repo)) {
return;
}

let cmd = shellescape([
'git',
'clone',
params.repo,
'/some/path'
]);

// cmd 的值: git clone 'https://github.com/xx/xx.git && rm -rf / &&' /some/path
// 这样就不会被注入成功了。
exec(cmd);
+

无论是在何种后端语言环境中,凡是涉及到代码调用系统 shell 命令的时候都一定要谨慎。

+

DDoS 攻击

DDoS 又叫分布式拒绝服务,全称 Distributed Denial of Service,其原理就是利用大量的请求造成资源过载,导致服务不可用,这个攻击应该不能算是安全问题,这应该算是一个另类的存在,因为这种攻击根本就是耍流氓的存在,「伤敌一千,自损八百」的行为。出于保护 Web App 不受攻击的攻防角度,还是介绍一下 DDoS 攻击吧,毕竟也是挺常见的。

+

DDoS 攻击可以理解为:「你开了一家店,隔壁家点看不惯,就雇了一大堆黑社会人员进你店里干坐着,也不消费,其他客人也进不来,导致你营业惨淡」。为啥说 DDoS 是个「伤敌一千,自损八百」的行为呢?毕竟隔壁店还是花了不少钱雇黑社会但是啥也没得到不是?DDoS 攻击的目的基本上就以下几个:

+
    +
  • 深仇大恨,就是要干死你
  • +
  • 敲诈你,不给钱就干你
  • +
  • 忽悠你,不买我防火墙服务就会有“人”继续干你
  • +
+

也许你的站点遭受过 DDoS 攻击,具体什么原因怎么解读见仁见智。DDos 攻击从层次上可分为网络层攻击与应用层攻击,从攻击手法上可分为快型流量攻击与慢型流量攻击,但其原理都是造成资源过载,导致服务不可用。

+

网络层 DDoS

网络层 DDos 攻击包括 SYN FloodACK FloodUDP FloodICMP Flood 等。

+

SYN Flood 攻击

SYN flood 攻击主要利用了 TCP 三次握手过程中的 Bug,我们都知道 TCP 三次握手过程是要建立连接的双方发送 SYN,SYN + ACK,ACK 数据包,而当攻击方随意构造源 IP 去发送 SYN 包时,服务器返回的 SYN + ACK 就不能得到应答(因为 IP 是随意构造的),此时服务器就会尝试重新发送,并且会有至少 30s 的等待时间,导致资源饱和服务不可用,此攻击属于慢型 DDoS 攻击。

+

ACK Flood 攻击

ACK Flood 攻击是在 TCP 连接建立之后,所有的数据传输 TCP 报文都是带有 ACK 标志位的,主机在接收到一个带有 ACK 标志位的数据包的时候,需要检查该数据包所表示的连接四元组是否存在,如果存在则检查该数据包所表示的状态是否合法,然后再向应用层传递该数据包。如果在检查中发现该数据包不合法,例如该数据包所指向的目的端口在本机并未开放,则主机操作系统协议栈会回应 RST 包告诉对方此端口不存在。

+

UDP Flood 攻击

UDP flood 攻击是由于 UDP 是一种无连接的协议,因此攻击者可以伪造大量的源 IP 地址去发送 UDP 包,此种攻击属于大流量攻击。正常应用情况下,UDP 包双向流量会基本相等,因此发起这种攻击的攻击者在消耗对方资源的时候也在消耗自己的资源。

+

ICMP Flood 攻击

ICMP Flood 攻击属于大流量攻击,其原理就是不断发送不正常的 ICMP 包(所谓不正常就是 ICMP 包内容很大),导致目标带宽被占用,但其本身资源也会被消耗。目前很多服务器都是禁 ping 的(在防火墙在可以屏蔽 ICMP 包),因此这种攻击方式已经落伍。

+

网络层 DDoS 防御

网络层的 DDoS 攻击究其本质其实是无法防御的,我们能做得就是不断优化服务本身部署的网络架构,以及提升网络带宽。当然,还是做好以下几件事也是有助于缓解网络层 DDoS 攻击的冲击:

+
    +
  • 网络架构上做好优化,采用负载均衡分流。
  • +
  • 确保服务器的系统文件是最新的版本,并及时更新系统补丁。
  • +
  • 添加抗 DDos 设备,进行流量清洗。
  • +
  • 限制同时打开的 SYN 半连接数目,缩短 SYN 半连接的 Timeout 时间。
  • +
  • 限制单 IP 请求频率。
  • +
  • 防火墙等防护设置禁止 ICMP 包等。
  • +
  • 严格限制对外开放的服务器的向外访问。
  • +
  • 运行端口映射程序或端口扫描程序,要认真检查特权端口和非特权端口。
  • +
  • 关闭不必要的服务。
  • +
  • 认真检查网络设备和主机/服务器系统的日志。只要日志出现漏洞或是时间变更,那这台机器就可能遭到了攻击。
  • +
  • 限制在防火墙外与网络文件共享。这样会给黑客截取系统文件的机会,主机的信息暴露给黑客,无疑是给了对方入侵的机会。
  • +
  • 加钱堆机器。。
  • +
  • 报警。。-
  • +
+

应用层 DDoS

应用层 DDoS 攻击不是发生在网络层,是发生在 TCP 建立握手成功之后,应用程序处理请求的时候,现在很多常见的 DDoS 攻击都是应用层攻击。应用层攻击千变万化,目的就是在网络应用层耗尽你的带宽,下面列出集中典型的攻击类型。

+

CC 攻击

当时绿盟为了防御 DDoS 攻击研发了一款叫做 Collapasar 的产品,能够有效的防御 SYN Flood 攻击。黑客为了挑衅,研发了一款 Challenge Collapasar 攻击工具(简称 CC)。

+

CC 攻击的原理,就是针对消耗资源比较大的页面不断发起不正常的请求,导致资源耗尽。因此在发送 CC 攻击前,我们需要寻找加载比较慢,消耗资源比较多的网页,比如需要查询数据库的页面、读写硬盘文件的等。通过 CC 攻击,使用爬虫对某些加载需要消耗大量资源的页面发起 HTTP 请求。

+

DNS Flood

DNS Flood 攻击采用的方法是向被攻击的服务器发送大量的域名解析请求,通常请求解析的域名是随机生成或者是网络世界上根本不存在的域名,被攻击的DNS 服务器在接收到域名解析请求的时候首先会在服务器上查找是否有对应的缓存,如果查找不到并且该域名无法直接由服务器解析的时候,DNS 服务器会向其上层 DNS 服务器递归查询域名信息。域名解析的过程给服务器带来了很大的负载,每秒钟域名解析请求超过一定的数量就会造成 DNS 服务器解析域名超时。

+

根据微软的统计数据,一台 DNS 服务器所能承受的动态域名查询的上限是每秒钟 9000 个请求。而我们知道,在一台 P3 的 PC 机上可以轻易地构造出每秒钟几万个域名解析请求,足以使一台硬件配置极高的 DNS 服务器瘫痪,由此可见 DNS 服务器的脆弱性。

+

HTTP 慢速连接攻击

针对 HTTP 协议,先建立起 HTTP 连接,设置一个较大的 Conetnt-Length,每次只发送很少的字节,让服务器一直以为 HTTP 头部没有传输完成,这样连接一多就很快会出现连接耗尽。

+

应用层 DDoS 防御

    +
  • 判断 User-Agent 字段(不可靠,因为可以随意构造)
  • +
  • 针对 IP + cookie,限制访问频率(由于 cookie 可以更改,IP 可以使用代理,或者肉鸡,也不可靠)
  • +
  • 关闭服务器最大连接数等,合理配置中间件,缓解 DDoS 攻击。
  • +
  • 请求中添加验证码,比如请求中有数据库操作的时候。
  • +
  • 编写代码时,尽量实现优化,并合理使用缓存技术,减少数据库的读取操作。
  • +
  • 加钱堆机器。。
  • +
  • 报警。。
  • +
+

应用层的防御有时比网络层的更难,因为导致应用层被 DDoS 攻击的因素非常多,有时往往是因为程序员的失误,导致某个页面加载需要消耗大量资源,有时是因为中间件配置不当等等。而应用层 DDoS 防御的核心就是区分人与机器(爬虫),因为大量的请求不可能是人为的,肯定是机器构造的。因此如果能有效的区分人与爬虫行为,则可以很好地防御此攻击。

+

其他 DDoS 攻击

发起 DDoS 也是需要大量的带宽资源的,但是互联网就像森林,林子大了什么鸟都有,DDoS 攻击者也能找到其他的方式发起廉价并且极具杀伤力的 DDoS 攻击。

+

利用 XSS

举个例子,如果 12306 页面有一个 XSS 持久型漏洞被恶意攻击者发现,只需在春节抢票期间在这个漏洞中执行脚本使得往某一个小站点随便发点什么请求,然后随着用户访问的增多,感染用户增多,被攻击的站点自然就会迅速瘫痪了。这种 DDoS 简直就是无本万利,不用惊讶,现在大站有 XSS 漏洞的不要太多。

+

来自 P2P 网络攻击

大家都知道,互联网上的 P2P 用户和流量都是一个极为庞大的数字。如果他们都去一个指定的地方下载数据,成千上万的真实 IP 地址连接过来,没有哪个设备能够支撑住。拿 BT 下载来说,伪造一些热门视频的种子,发布到搜索引擎,就足以骗到许多用户和流量了,但是这只是基础攻击。
高级的 P2P 攻击,是直接欺骗资源管理服务器。如迅雷客户端会把自己发现的资源上传到资源管理服务器,然后推送给其它需要下载相同资源的用户,这样,一个链接就发布出去。通过协议逆向,攻击者伪造出大批量的热门资源信息通过资源管理中心分发出去,瞬间就可以传遍整个 P2P 网络。更为恐怖的是,这种攻击是无法停止的,即使是攻击者自身也无法停止,攻击一直持续到 P2P 官方发现问题更新服务器且下载用户重启下载软件为止。

+

DDoS 最后总结

DDoS 不可能防的住,就好比你的店只能容纳 50 人,黑社会有 100 人,你就换一家大店,能容纳 500 人,然后黑社会又找来了 1000 人,这种堆人头的做法就是 DDoS 本质上的攻防之道,「道高一尺,魔高一丈,魔高一尺,道高一丈」,讲真,必要的时候就答应勒索你的人的条件吧,实在不行就报警吧。

+

流量劫持

流量劫持应该算是黑产行业的一大经济支柱了吧?简直是让人恶心到吐,不吐槽了,还是继续谈干货吧,流量劫持基本分两种:DNS 劫持 和 HTTP 劫持,目的都是一样的,就是当用户访问 zoumiaojiang.com 的时候,给你展示的并不是或者不完全是 zoumiaojiang.com 提供的 “内容”。

+

DNS 劫持

DNS 劫持,也叫做域名劫持,可以这么理解,「你打了一辆车想去商场吃饭,结果你打的车是小作坊派来的,直接给你拉到小作坊去了」,DNS 的作用是把网络地址域名对应到真实的计算机能够识别的 IP 地址,以便计算机能够进一步通信,传递网址和内容等。如果当用户通过某一个域名访问一个站点的时候,被篡改的 DNS 服务器返回的是一个恶意的钓鱼站点的 IP,用户就被劫持到了恶意钓鱼站点,然后继而会被钓鱼输入各种账号密码信息,泄漏隐私。

+

这类劫持,要不就是网络运营商搞的鬼,一般小的网络运营商与黑产勾结会劫持 DNS,要不就是电脑中毒,被恶意篡改了路由器的 DNS 配置,基本上做为开发者或站长却是很难察觉的,除非有用户反馈,现在升级版的 DNS 劫持还可以对特定用户、特定区域等使用了用户画像进行筛选用户劫持的办法,另外这类广告显示更加随机更小,一般站长除非用户投诉否则很难觉察到,就算觉察到了取证举报更难。无论如何,如果接到有 DNS 劫持的反馈,一定要做好以下几件事:

+
    +
  • 取证很重要,时间、地点、IP、拨号账户、截屏、URL 地址等一定要有。
  • +
  • 可以跟劫持区域的电信运营商进行投诉反馈。
  • +
  • 如果投诉反馈无效,直接去工信部投诉,一般来说会加白你的域名。
  • +
+

HTTP 劫持

HTTP 劫持您可以这么理解,「你打了一辆车想去商场吃饭,结果司机跟你一路给你递小作坊的广告」,HTTP 劫持主要是当用户访问某个站点的时候会经过运营商网络,而不法运营商和黑产勾结能够截获 HTTP 请求返回内容,并且能够篡改内容,然后再返回给用户,从而实现劫持页面,轻则插入小广告,重则直接篡改成钓鱼网站页面骗用户隐私。能够实施流量劫持的根本原因,是 HTTP 协议没有办法对通信对方的身份进行校验以及对数据完整性进行校验。如果能解决这个问题,则流量劫持将无法轻易发生。所以防止 HTTP 劫持的方法只有将内容加密,让劫持者无法破解篡改,这样就可以防止 HTTP 劫持了。

+

HTTPS 协议就是一种基于 SSL 协议的安全加密网络应用层协议,可以很好的防止 HTTP 劫持。这里有篇 文章 讲的不错。HTTPS 在这就不深讲了,后面有机会我会单独好好讲讲 HTTPS。如果不想站点被 HTTP 劫持,赶紧将你的站点全站改造成 HTTPS 吧。

+

服务器漏洞

服务器除了以上提到的那些大名鼎鼎的漏洞和臭名昭著的攻击以外,其实还有很多其他的漏洞,往往也很容易被忽视,在这个小节也稍微介绍几种。

+

越权操作漏洞

如果你的系统是有登录控制的,那就要格外小心了,因为很有可能你的系统越权操作漏洞,越权操作漏洞可以简单的总结为 「A 用户能看到或者操作 B 用户的隐私内容」,如果你的系统中还有权限控制就更加需要小心了。所以每一个请求都需要做 userid 的判断

+

以下是一段有漏洞的后端示意代码:

+
1
2
3
4
5
6
7
// ctx 为请求的 context 上下文
let msgId = ctx.params.msgId;

mysql.query(
'SELECT * FROM msg_table WHERE msg_id = ?',
[msgId]
);
+ +

以上代码是任何人都可以查询到任何用户的消息,只要有 msg_id 就可以,这就是比较典型的越权漏洞

+

越权操作漏洞防御

如下这么改进一下:

+
1
2
3
4
5
6
7
8
// ctx 为请求的 context 上下文
let msgId = ctx.params.msgId;
let userId = ctx.session.userId; // 从会话中取出当前登陆的 userId

mysql.query(
'SELECT * FROM msg_table WHERE msg_id = ? AND user_id = ?',
[msgId, userId]
);
+

嗯,大概就是这个意思,如果有更严格的权限控制,那在每个请求中凡是涉及到数据库的操作都需要先进行严格的验证,并且在设计数据库表的时候需要考虑进 userId 的账号关联以及权限关联。

+

目录遍历漏洞

目录遍历漏洞指通过在 URL 或参数中构造 .././ 和类似的跨父目录字符串的 ASCII 编码、unicode 编码等,完成目录跳转,读取操作系统各个目录下的敏感文件,也可以称作「任意文件读取漏洞」。

+

目录遍历漏洞原理:程序没有充分过滤用户输入的 ../ 之类的目录跳转符,导致用户可以通过提交目录跳转来遍历服务器上的任意文件。使用多个.. 符号,不断向上跳转,最终停留在根 /,通过绝对路径去读取任意文件。

+

目录遍历漏洞几个示例和测试,一般构造 URL 然后使用浏览器直接访问,或者使用 Web 漏洞扫描工具检测,当然也可以自写程序测试。

+
1
2
3
4
5
6
7
8
9
http://somehost.com/../../../../../../../../../etc/passwd
http://somehost.com/some/path?file=../../Windows/system.ini

# 借助 %00 空字符截断是一个比较经典的攻击手法
http://somehost.com/some/path?file=../../Windows/system.ini%00.js

# 使用了 IIS 的脚本目录来移动目录并执行指令
http://somehost.com/scripts/..%5c../Windows/System32/cmd.exe?/c+dir+c:\

+

目录遍历漏洞防御

方法就是需要对 URL 或者参数进行 ../,./ 等字符的转义过滤。

+

物理路径泄漏

物理路径泄露属于低风险等级缺陷,它的危害一般被描述为「攻击者可以利用此漏洞得到信息,来对系统进一步地攻击」,通常都是系统报错 500 的错误信息直接返回到页面可见导致的漏洞。得到物理路径有些时候它能给攻击者带来一些有用的信息,比如说:可以大致了解系统的文件目录结构;可以看出系统所使用的第三方软件;也说不定会得到一个合法的用户名(因为很多人把自己的用户名作为网站的目录名)。

+

物理路径泄漏防御

防止这种泄漏的方法就是做好后端程序的出错处理,定制特殊的 500 报错页面。

+

源码暴露漏洞

和物理路径泄露类似,就是攻击者可以通过请求直接获取到你站点的后端源代码,然后就可以对系统进一步研究攻击。那么导致源代码暴露的原因是什么呢?基本上就是发生在服务器配置上了,服务器可以设置哪些路径的文件才可以被直接访问的,这里给一个 koa 服务起的例子,正常的 koa 服务器可以通过 koa-static 中间件去指定静态资源的目录,好让静态资源可以通过路径的路由访问。比如你的系统源代码目录是这样的:

+
1
2
3
4
5
6
|- project
|- src
|- static
|- ...
|- server.js

+ +

你想要将 static 的文件夹配成静态资源目录,你应该会在 server.js 做如下配置:

+
1
2
3
4
5
6
const Koa = require('koa');
const serve = require('koa-static');
const app = new Koa();

app.use(serve(__dirname + '/project/static'));

+ +

但是如果配错了静态资源的目录,可能就出大事了,比如:

+
1
2
3
// ...
app.use(serve(__dirname + '/project'));

+ +

这样所有的源代码都可以通过路由访问到了,所有的服务器都提供了静态资源机制,所以在通过服务器配置静态资源目录和路径的时候,一定要注意检验,不然很可能产生漏洞。

+

最后,希望 Web 开发者们能够管理好自己的代码隐私,注意代码安全问题,比如不要将产品的含有敏感信息的代码放到第三方外部站点或者暴露给外部用户,尤其是前端代码,私钥类似的保密性的东西不要直接输出在代码里或者页面中。也许还有很多值得注意的点,但是归根结底还是绷住安全那根弦,对待每一行代码都要多多推敲。

+

转载自:https://zoumiaojiang.com/article/common-web-security/

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 网络安全(2)

+

文章作者:

+

发布时间:2022年06月16日 - 14:40

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/16/%E5%89%8D%E7%AB%AF%E5%B8%B8%E8%A7%81%E7%9F%A5%E8%AF%86%E7%82%B9%E6%95%B4%E7%90%86%20----%20%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%EF%BC%882%EF%BC%89/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2022/06/30/nginx-timeout/index.html b/en/2022/06/30/nginx-timeout/index.html new file mode 100644 index 0000000000..c6ad08c3a7 --- /dev/null +++ b/en/2022/06/30/nginx-timeout/index.html @@ -0,0 +1,669 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nginx中的超时设置 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ nginx中的超时设置 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

nginx中的超时设置

Nginx主要有四类超时设置:客户端超时设置DNS解析超时设置代理超时设置,如果使用ngx_lua,则还有lua相关的超时设置。

+

客户端超时设置

对于客户端超时主要设置有读取请求头超时时间读取请求体超时时间发送响应超时时间长连接超时时间。通过客户端超时设置避免客户端恶意或者网络状况不佳造成连接长期占用,影响服务端的可处理的能力。

+

client_header_timeout 指定等待client发送一个请求头的超时时间

    +
  • 语法: client_header_timeout time
  • +
  • 默认值: 60s
  • +
  • 上下文: http server
  • +
  • 说明: 指定等待client发送一个请求头的超时时间(例如:GET / HTTP/1.1).仅当在一次read中,没有收到请求头,才会算成超时。如果在超时时间内,client没发送任何东西,nginx返回HTTP状态码408(“Request timed out”)
  • +
+

client_body_timeout 该指令设置请求体(request body)的读超时时间

    +
  • 语法: client_body_timeout time
  • +
  • 默认值: 60s
  • +
  • 上下文: http server location
  • +
  • 说明: 该指令设置请求体(request body)的读超时时间。仅当在一次readstep中,没有得到请求体,就会设为超时。超时后,nginx返回HTTP状态码408(“Request timed out”)
  • +
+

send_timeout time

设置发送响应到客户端的超时时间,默认为60s,此超时时间指的也是两次成功写操作间隔时间,而不是发送整个响应的超时时间。如果在此超时时间内客户端没有接收任何响应,则Nginx关闭此连接。

+

keepalive_timeout 第一个参数指定了与client的keep-alive连接超时时间

    +
  • 语法: keepalive_timeout timeout [ header_timeout ]

    +
  • +
  • 默认值: 75s

    +
  • +
  • 上下文: http server location

    +
  • +
  • 说明: 第一个参数指定了与client的keep-alive连接超时时间。服务器将会在这个时间后关闭连接。可选的第二个参数指定了在响应头Keep-Alive: timeout=time中的time值。这个头能够让一些浏览器主动关闭连接,这样服务器就不必要去关闭连接了。没有这个参数,nginx不会发送Keep-Alive响应头(尽管并不是由这个头来决定连接是否“keep-alive”)
    两个参数的值可并不相同

    +
      +
    • 注意不同浏览器怎么处理“keep-alive”头
    • +
    • MSIE和Opera忽略掉”Keep-Alive: timeout=“ header.
    • +
    • MSIE保持连接大约60-65秒,然后发送TCP RST
    • +
    • Opera永久保持长连接
    • +
    • Mozilla keeps the connection alive for N plus about 1-10 seconds.
    • +
    • Konqueror保持长连接N秒
    • +
    +
  • +
+

DNS解析超时设置

resolver_timeout 设置DNS解析超时时间

    +
  • 语法 resolver_timeout time
  • +
  • 默认值 30s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置DNS解析超时时间
  • +
+

代理超时设置

proxy_connect_timeout 设置与upstream server的连接超时时间,不能超过75秒

    +
  • 语法 proxy_connect_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置与upstream server的连接超时时间,有必要记住,这个超时不能超过75秒。
    这个不是等待后端返回页面的时间,那是由proxy_read_timeout声明的。如果你的upstream服务器起来了,但是hanging住了(例如,没有足够的线程处理请求,所以把你的请求放到请求池里稍后处理),那么这个声明是没有用的,由于与upstream服务器的连接已经建立了。
  • +
+

proxy_read_timeout 设置与代理服务器的读超时时间

    +
  • 语法 proxy_read_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置与代理服务器的读超时时间。它决定了nginx会等待多长时间来获得请求的响应。这个时间不是获得整个response的时间,而是两次reading操作的时间。
  • +
+

proxy_send_timeout 设置发送请求给upstream服务器的超时时间

    +
  • 语法 proxy_send_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 这个指定设置了发送请求给upstream服务器的超时时间。超时设置不是为了整个发送期间,而是在两次write操作期间。如果超时后,upstream没有收到新的数据,nginx会关闭连接
  • +
+

失败重试机制设置。

proxy_next_upstream error | timeout | invalid_header | http_500 | http_502 | http_503 | http_504 |http_403 | http_404 | non_idempotent | off …:配置什么情况下需要请求下一台上游服务器进行重试。默认为“errortimeout”。error表示与上游服务器建立连接、写请求或者读响应头出错。timeout表示与上游服务器建立连接、写请求或者读响应头超时。invalid_header表示上游服务器返回空的或错误的响应头。http_XXX表示上游服务器返回特定的状态码。non_idempotent表示RFC-2616定义的非幂等HTTP方法(POST、LOCK、PATCH),也可以在失败后重试下一台上游服务器(即默认幂等方法GET、HEAD、PUT、DELETE、OPTIONS、TRACE才可以重试)。off表示禁用重试。

+

重试不能无限制进行,因此,需要如下两个指令控制重试次数和重试超时时间。

+

proxy_next_upstream_tries number:设置重试次数,默认0表示不限制,注意此重试次数指的是所有请求次数(包括第一次和之后的重试次数之和)。

+

proxy_next_upstream_timeout time:设置重试最大超时时间,默认0表示不限制。

+

即在proxy_next_upstream_timeout时间内允许proxy_next_upstream_tries次重试。如果超过了其中一个设置,则Nginx也会结束重试并返回客户端响应(可能是错误码)。

+

如下配置表示当error/timeout时重试upstream中的下一台上游服务器,如果重试的总时间超出了6s或者重试了1次,则表示重试失败(因为之前已经请求一次了,所以还能重试一次),Nginx结束重试并返回客户端响应。

+

upstream存活超时设置

max_fails和fail_timeout:配置什么时候Nginx将上游服务器认定为不可用/不存活。当上游服务器在fail_timeout时间内失败了max_fails次,则认为该上游服务器不可用/不存活。并在接下来的fail_timeout时间内从upstream摘掉该节点(即请求不会转发到该上游服务器)。

+

什么情况下被认定为失败呢?其由 proxy_next_upstream定义,不过,不管 proxy_next_upstream如何配置,error, timeout and invalid_header 都将被认为是失败。

+

如server 192.168.61.1:9090max_fails=2 fail_timeout=10s;表示在10s内如果失败了2次,则在接下来的10s内认定该节点不可用/不存活。这种存活检测机制是只有当访问该上游服务器时,采取惰性检查,可以使用ngx_http_upstream_check_module配置主动检查。

+

max_fails设置为0表示不检查服务器是否可用(即认为一直可用),如果upstream中仅剩一台上游服务器时,则该服务器是不会被摘除的,将从不被认为不可用。

+

proxy_upstream_fail_timeout(fail_timeout)设置某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间

    +
  • 语法 server address [fail_timeout=30s]
  • +
  • 默认值 10s
  • +
  • 上下文 upstream
  • +
  • 说明 Upstream模块下 server指令的参数,设置了某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间,默认为10秒
  • +
+

lingering_timeout

    +
  • 语法: lingering_timeout time
  • +
  • 默认值: 5s
  • +
  • 上下文: http server location
  • +
  • 说明: lingering_close生效后,在关闭连接前,会检测是否有用户发送的数据到达服务器,如果超过lingering_timeout时间后还没有数据可读,就直接关闭连接;否则,必须在读取完连接缓冲区上的数据并丢弃掉后才会关闭连接。
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:nginx中的超时设置

+

文章作者:

+

发布时间:2022年06月30日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/30/nginx-timeout/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/08/02/Docker \351\225\234\345\203\217\345\256\211\345\205\250\346\234\200\344\275\263\345\256\236\350\267\265 /index.html" "b/en/2022/08/02/Docker \351\225\234\345\203\217\345\256\211\345\205\250\346\234\200\344\275\263\345\256\236\350\267\265 /index.html" new file mode 100644 index 0000000000..0087a3ad68 --- /dev/null +++ "b/en/2022/08/02/Docker \351\225\234\345\203\217\345\256\211\345\205\250\346\234\200\344\275\263\345\256\236\350\267\265 /index.html" @@ -0,0 +1,609 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 在Docker Compose中配置网络 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 在Docker Compose中配置网络 +

+ + +
+ + + + +
+ + +

Docker 镜像安全最佳实践

Docker 和 宿主机 的设置

    +
  1. 保证宿主机和 Docker 的版本是最新的
  2. +
  3. 不要暴露 Docker 的 守护进程(daemon) 的套接字
  4. +
  5. 使用 rootless 模式启动 Docker
  6. +
  7. 避免使用特权容器
  8. +
  9. 限制容器资源
  10. +
  11. 隔离容器网络
  12. +
  13. 提高容器的隔离度
  14. +
  15. 将文件系统和卷设置为只读
  16. +
  17. 完整的生命周期管理
  18. +
  19. 限制来自容器内的系统调用
  20. +
+

确保镜像安全

    +
  1. 扫描和验证容器镜像
  2. +
  3. 使用最小基础镜像
  4. +
  5. 不要向 Docker 镜像泄露敏感信息
  6. +
  7. 使用多阶段构建
  8. +
  9. 确保容器注册
  10. +
  11. 使用固定标签以获得不变性
  12. +
+

监控容器

    +
  1. 监控容器活动
  2. +
  3. 确保容器在运行时的安全
  4. +
  5. 将故障排除数据与容器分开保存
  6. +
  7. 为镜像使用元数据标签
  8. +
+

参考

Top 20 Dockerfile best practices
Dockerセキュリティベストプラクティス トップ20

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:在Docker Compose中配置网络

+

文章作者:

+

发布时间:2022年08月02日 - 19:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/Docker%20%E9%95%9C%E5%83%8F%E5%AE%89%E5%85%A8%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%20/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/08/02/Network in Compose \345\234\250Docker Compose\344\270\255\351\205\215\347\275\256\347\275\221\347\273\234/index.html" "b/en/2022/08/02/Network in Compose \345\234\250Docker Compose\344\270\255\351\205\215\347\275\256\347\275\221\347\273\234/index.html" new file mode 100644 index 0000000000..0c06dab61b --- /dev/null +++ "b/en/2022/08/02/Network in Compose \345\234\250Docker Compose\344\270\255\351\205\215\347\275\256\347\275\221\347\273\234/index.html" @@ -0,0 +1,586 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 在Docker Compose中配置网络 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 在Docker Compose中配置网络 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Network in Compose 在Docker Compose中配置网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
version: "3.9"

services:
proxy:
build: ./proxy
networks:
- actsnetwork
app:
build: ./app
networks:
- actsnetwork
db:
image: postgres
networks:
- testnetwork

networks:
actsnetwork:
name: testnetwork
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:在Docker Compose中配置网络

+

文章作者:

+

发布时间:2022年08月02日 - 19:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/Network%20in%20Compose%20%E5%9C%A8Docker%20Compose%E4%B8%AD%E9%85%8D%E7%BD%AE%E7%BD%91%E7%BB%9C/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/08/02/Nginx\344\270\255\347\232\204\350\266\205\346\227\266\350\256\276\347\275\256/index.html" "b/en/2022/08/02/Nginx\344\270\255\347\232\204\350\266\205\346\227\266\350\256\276\347\275\256/index.html" new file mode 100644 index 0000000000..52b7fa0482 --- /dev/null +++ "b/en/2022/08/02/Nginx\344\270\255\347\232\204\350\266\205\346\227\266\350\256\276\347\275\256/index.html" @@ -0,0 +1,652 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Nginx中的超时设置 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Nginx中的超时设置 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Nginx中的超时设置

client_header_timeout 指定等待client发送一个请求头的超时时间

    +
  • 语法: client_header_timeout time
  • +
  • 默认值: 60s
  • +
  • 上下文: http server
  • +
  • 说明: 指定等待client发送一个请求头的超时时间(例如:GET / HTTP/1.1).仅当在一次read中,没有收到请求头,才会算成超时。如果在超时时间内,client没发送任何东西,nginx返回HTTP状态码408(“Request timed out”)
  • +
+

client_body_timeout 该指令设置请求体(request body)的读超时时间

    +
  • 语法: client_body_timeout time
  • +
  • 默认值: 60s
  • +
  • 上下文: http server location
  • +
  • 说明: 该指令设置请求体(request body)的读超时时间。仅当在一次readstep中,没有得到请求体,就会设为超时。超时后,nginx返回HTTP状态码408(“Request timed out”)
  • +
+

keepalive_timeout 第一个参数指定了与client的keep-alive连接超时时间

    +
  • 语法: keepalive_timeout timeout [ header_timeout ]

    +
  • +
  • 默认值: 75s

    +
  • +
  • 上下文: http server location

    +
  • +
  • 说明: 第一个参数指定了与client的keep-alive连接超时时间。服务器将会在这个时间后关闭连接。可选的第二个参数指定了在响应头Keep-Alive: timeout=time中的time值。这个头能够让一些浏览器主动关闭连接,这样服务器就不必要去关闭连接了。没有这个参数,nginx不会发送Keep-Alive响应头(尽管并不是由这个头来决定连接是否“keep-alive”)
    两个参数的值可并不相同

    +
      +
    • 注意不同浏览器怎么处理“keep-alive”头
    • +
    • MSIE和Opera忽略掉”Keep-Alive: timeout=“ header.
    • +
    • MSIE保持连接大约60-65秒,然后发送TCP RST
    • +
    • Opera永久保持长连接
    • +
    • Mozilla keeps the connection alive for N plus about 1-10 seconds.
    • +
    • Konqueror保持长连接N秒
    • +
    +
  • +
+

lingering_timeout

    +
  • 语法: lingering_timeout time
  • +
  • 默认值: 5s
  • +
  • 上下文: http server location
  • +
  • 说明: lingering_close生效后,在关闭连接前,会检测是否有用户发送的数据到达服务器,如果超过lingering_timeout时间后还没有数据可读,就直接关闭连接;否则,必须在读取完连接缓冲区上的数据并丢弃掉后才会关闭连接。
  • +
+

resolver_timeout 设置DNS解析超时时间

    +
  • 语法 resolver_timeout time
  • +
  • 默认值 30s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置DNS解析超时时间
  • +
+

proxy_connect_timeout 设置与upstream server的连接超时时间,不能超过75秒

    +
  • 语法 proxy_connect_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置与upstream server的连接超时时间,有必要记住,这个超时不能超过75秒。
    这个不是等待后端返回页面的时间,那是由proxy_read_timeout声明的。如果你的upstream服务器起来了,但是hanging住了(例如,没有足够的线程处理请求,所以把你的请求放到请求池里稍后处理),那么这个声明是没有用的,由于与upstream服务器的连接已经建立了。
  • +
+

proxy_read_timeout 设置与代理服务器的读超时时间

    +
  • 语法 proxy_read_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置与代理服务器的读超时时间。它决定了nginx会等待多长时间来获得请求的响应。这个时间不是获得整个response的时间,而是两次reading操作的时间。
  • +
+

proxy_send_timeout 设置发送请求给upstream服务器的超时时间

    +
  • 语法 proxy_send_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 这个指定设置了发送请求给upstream服务器的超时时间。超时设置不是为了整个发送期间,而是在两次write操作期间。如果超时后,upstream没有收到新的数据,nginx会关闭连接
  • +
+

proxy_upstream_fail_timeout(fail_timeout)设置某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间

    +
  • 语法 server address [fail_timeout=30s]
  • +
  • 默认值 10s
  • +
  • 上下文 upstream
  • +
  • 说明 Upstream模块下 server指令的参数,设置了某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间,默认为10秒
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Nginx中的超时设置

+

文章作者:

+

发布时间:2022年08月02日 - 19:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/Nginx%E4%B8%AD%E7%9A%84%E8%B6%85%E6%97%B6%E8%AE%BE%E7%BD%AE/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2022/08/02/Oracle-Linux-install-Docker/index.html b/en/2022/08/02/Oracle-Linux-install-Docker/index.html new file mode 100644 index 0000000000..6fa99f4e21 --- /dev/null +++ b/en/2022/08/02/Oracle-Linux-install-Docker/index.html @@ -0,0 +1,590 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 在 Oracle Linux 中安装 Docker | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 在 Oracle Linux 中安装 Docker +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Oracle Linux install Docker(https://docs.docker.com/engine/install/centos/)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 查看
systemctl list-units

sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine

sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

sudo yum install -y yum-utils

sudo yum -y install docker-ce-20.10.17 docker-ce-cli-20.10.17 containerd.io-1.6.6

sudo vi /etc/yum.repos.d/docker-ce.repo

[centos-extras]
name=Centos extras - $basearch
baseurl=http://mirror.centos.org/centos/7/extras/x86_64
enabled=1
gpgcheck=1
gpgkey=http://centos.org/keys/RPM-GPG-KEY-CentOS-7

sudo yum -y install fuse-overlayfs slirp4netns

# sudo chmod 777 docker-ce.repo

# yum list docker-ce --showduplicates | sort -r

sudo systemctl start docker

sudo docker run hello-world

sudo yum install /path/to/package.rpm
sudo systemctl start docker
sudo docker run hello-world
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:在 Oracle Linux 中安装 Docker

+

文章作者:

+

发布时间:2022年08月02日 - 18:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/Oracle-Linux-install-Docker/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/08/02/OracleLinux\345\206\205\346\240\270\344\273\216uek\345\210\207\346\215\242\345\210\260uek/index.html" "b/en/2022/08/02/OracleLinux\345\206\205\346\240\270\344\273\216uek\345\210\207\346\215\242\345\210\260uek/index.html" new file mode 100644 index 0000000000..ca320c75ec --- /dev/null +++ "b/en/2022/08/02/OracleLinux\345\206\205\346\240\270\344\273\216uek\345\210\207\346\215\242\345\210\260uek/index.html" @@ -0,0 +1,578 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OracleLinux内核从uek切换到uek | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ OracleLinux内核从uek切换到uek +

+ + +
+ + + + +
+ + +
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#/boot/grub/grub.conf 缺失:

yum install -y grub
grub-mkconfig -o /boot/grub/grub.conf

#/boot/grub2/grub.cfg 缺失:

yum install -y grub2
grub2-mkconfig -o /boot/grub2/grub.cfg

uname -a
sudo grep ^menuentry /boot/grub2/grub.cfg | awk -F\' '{print $2}' | nl -v0

sudo grub2-editenv list

sudo grep GRUB_DEFAULT /etc/default/grub

sudo vim /etc/default/grub

sudo grub2-set-default 1

sudo grub2-mkconfig -o /boot/grub2/grub.cfg

sudo dracut --force

sudo reboot

uname -a
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:OracleLinux内核从uek切换到uek

+

文章作者:

+

发布时间:2022年08月02日 - 18:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/OracleLinux%E5%86%85%E6%A0%B8%E4%BB%8Euek%E5%88%87%E6%8D%A2%E5%88%B0uek/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/08/02/Postgresql\347\273\277\350\211\262\347\211\210\344\275\277\347\224\250/index.html" "b/en/2022/08/02/Postgresql\347\273\277\350\211\262\347\211\210\344\275\277\347\224\250/index.html" new file mode 100644 index 0000000000..604016a62c --- /dev/null +++ "b/en/2022/08/02/Postgresql\347\273\277\350\211\262\347\211\210\344\275\277\347\224\250/index.html" @@ -0,0 +1,586 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Postgresql绿色版使用 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Postgresql绿色版使用 +

+ + +
+ + + + +
+ + +

初始化数据库

c:\pgsql\bin\initdb.exe -D c:\pgsql\data -E UTF8

+

注册表

c:\pgsql\bin\pg_ctl register -N postgres -D c:\pgsql\data

+

启动服务

c:\pgsql\bin\postgres -D c:\pgsql\data

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Postgresql绿色版使用

+

文章作者:

+

发布时间:2022年08月02日 - 19:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/Postgresql%E7%BB%BF%E8%89%B2%E7%89%88%E4%BD%BF%E7%94%A8/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2022/08/02/linux-scp/index.html b/en/2022/08/02/linux-scp/index.html new file mode 100644 index 0000000000..29cb229110 --- /dev/null +++ b/en/2022/08/02/linux-scp/index.html @@ -0,0 +1,599 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 在linux之间传输文件的命令scp | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 在linux之间传输文件的命令scp +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

这里假设要从Linux服务器A中把文件 file.txt 传输到Linux服务器B,该怎么做,这里就可以用scp实现
为了能够成功的传输文件,需要做到以下几点:

+
    +
  1. A需要知道要传输的的文件本地的路径
  2. +
  3. A为了往B传输文件,所以需要知道B的访问权限,因此需要B的访问地址,账号和私钥
  4. +
  5. 目标路径
  6. +
  7. 因为Linux权限管理的要求,私钥的访问权限需要设定为600
  8. +
+

当然还有一点前提是A和B两台服务器之间本身是可以通信的
知道以上信息便可以进行文件传输

+

实践

现在假设

+
    +
  1. A要往B传输的文件在A的如下目录:/tmp/transfer/file.txt
  2. +
  3. B的访问地址是 192.168.31.31 访问用户名是 testuser 用到的key上传到了A的如下目录: /tmp/ssh/test.key
  4. +
  5. 目标路径为B的如下目录: /tmp/test
    知道上面信息后我们来创建命令。
  6. +
+
1
2
chomd 600 /tmp/ssh/test.key
scp -i /tmp/ssh/test.key /tmp/transfer/file.txt testuser@192.168.31.31 /tmp/test
+ +

通过上面的命令即可实现从A服务器传输文件到B服务器了。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:在linux之间传输文件的命令scp

+

文章作者:

+

发布时间:2022年08月02日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/linux-scp/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/08/02/systemctl \345\221\275\344\273\244/index.html" "b/en/2022/08/02/systemctl \345\221\275\344\273\244/index.html" new file mode 100644 index 0000000000..e58b1a6d6a --- /dev/null +++ "b/en/2022/08/02/systemctl \345\221\275\344\273\244/index.html" @@ -0,0 +1,601 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + systemctl 命令 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ systemctl 命令 +

+ + +
+ + + + +
+ + +

systemctl 命令

systemctl

范列出系统上面有启动的unit

+

systemctl list-unit-files

列出所有已经安装的unit有哪些

+

systemctl list-units –type=service –all

列出类型为service的所有项目,不论启动与否

+

systemctl get-default

输入目前机器默认的模式,如图形界面模式或者文本模式

+

systemctl isolate multi-user.target

将目前的操作环境改为纯文本模式,关掉图形界面

+

systemctl isolate graphical.target

将目前的操作环境改为图形界面

+

systemctl poweroff

系统关机

+

systemctl reboot

重新开机

+

systemctl suspend

进入暂停模式

+

systemctl rescue

强制进入救援模式

+

systemctl hibernate

进入休眠模式

+

systemctl emergency

强制进入紧急救援模式

+

systemctl list-dependencies –reverse

查询当前默认的target关联了啥

+

systemctl list-dependencies graphical.target

查询图形界面模式的target关联了啥

+

systemctl list-sockets

查看当前的socket服务

+

systemctl show etcd.service

查看 unit 的详细配置情况

+

systemctl mask etcd.service

禁用某个服务

+

systemctl unmask etcd.service

解除禁用某个服务

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:systemctl 命令

+

文章作者:

+

发布时间:2022年08月02日 - 18:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/systemctl%20%E5%91%BD%E4%BB%A4/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/08/02/\346\220\255\345\273\272Docker\347\247\201\346\234\211\344\273\223\345\272\223/index.html" "b/en/2022/08/02/\346\220\255\345\273\272Docker\347\247\201\346\234\211\344\273\223\345\272\223/index.html" new file mode 100644 index 0000000000..1bbaed2cab --- /dev/null +++ "b/en/2022/08/02/\346\220\255\345\273\272Docker\347\247\201\346\234\211\344\273\223\345\272\223/index.html" @@ -0,0 +1,595 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 搭建Docker私有仓库 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 搭建Docker私有仓库 +

+ + +
+ + + + +
+ + +

搭建Docker私有仓库

利用registry搭建私有仓库

+

下载registry

1
docker pull registry
+ +

配置

1
2
/etc/docker/doemon.json
# 配置 'insecure-registry'
+

重启docker

1
systemctl restart docker
+ +

创建registry容器(关联私有仓库配置)

1
docker run -d -p 5000:5000 --name registry registry:latest
+ +

推送镜像到私有仓

    +
  • 备份镜像(172.16.12.134:5000 私有仓地址)

    +
    1
    docker tag ubuntu:latest 172.16.12.134:5000/my_ubuntu
  • +
  • 推送

    +
    1
    docker push 172.16.12.134:5000/my_ubuntu
  • +
  • 下载

    +
    1
    docker pull 172.16.12.134:5000/my_ubuntu
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:搭建Docker私有仓库

+

文章作者:

+

发布时间:2022年08月02日 - 18:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/%E6%90%AD%E5%BB%BADocker%E7%A7%81%E6%9C%89%E4%BB%93%E5%BA%93/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2022/08/02/\346\234\215\345\212\241\345\231\250\351\253\230\345\215\261\347\253\257\345\217\243\345\210\227\350\241\250/index.html" "b/en/2022/08/02/\346\234\215\345\212\241\345\231\250\351\253\230\345\215\261\347\253\257\345\217\243\345\210\227\350\241\250/index.html" new file mode 100644 index 0000000000..018c16756f --- /dev/null +++ "b/en/2022/08/02/\346\234\215\345\212\241\345\231\250\351\253\230\345\215\261\347\253\257\345\217\243\345\210\227\350\241\250/index.html" @@ -0,0 +1,865 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 服务器高危端口列表 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 服务器高危端口列表 +

+ + +
+ + + + +
+ + +

服务器高危端口列表

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
协议端口服务渗透测试
tcp20,21FTP(文件传输协议)允许匿名的上传下载,爆破,嗅探,win提权,远程执行(proftpd 1.3.5),各类后门(proftpd,vsftp 2.3.4)
tcp22SSH (安全外壳协议 )可根据已搜集到的信息尝试爆破,v1版本可中间人,ssh隧道及内网代理转发,文件传输等等
tcp23Telnet ( 远程终端协议)爆破,嗅探,一般常用于路由,交换登陆,可尝试弱口令
tcp25SMTP(简单邮件传输协议)邮件伪造,vrfy/expn查询邮件用户信息,可使用smtp-user-enum工具来自动跑
tcp/udp53DNS(域名系统)允许区域传送,dns劫持,缓存投毒,欺骗以及各种基于dns隧道的远控
tcp/udp69TFTP (简单文件传送协议 )尝试下载目标及其的各类重要配置文件
tcp80-89,443,8440-8450,8080-8089各种常用的Web服务端口,可尝试经典的topn,vpn,owa,webmail,目标oa,各类Java控制台,各类服务器Web管理面板,各类Web中间件漏洞利用,各类Web框架漏洞利用等等……
tcp110POP3(邮局协议版本3 )可尝试爆破,嗅探
tcp111,2049NFS(网络文件系统)权限配置不当
tcp137,139,445SMB(NETBIOS协议)可尝试爆破以及smb自身的各种远程执行类漏洞利用,如,ms08-067,ms17-010,嗅探等……
tcp143IMAP(邮件访问协议)可尝试爆破
udp161SNMP(简单网络管理协议)爆破默认团队字符串,搜集目标内网信息
tcp389LDAP( 轻量目录访问协议 )ldap注入,允许匿名访问,弱口令
tcp512,513,514Linux rexec (远程登录)可爆破,rlogin登陆
tcp873Rsync (数据镜像备份工具)匿名访问,文件上传
tcp1194OpenVPN(虚拟专用通道)想办法钓VPN账号,进内网
tcp1352Lotus(Lotus软件)弱口令,信息泄漏,爆破
tcp1433SQL Server(数据库管理系统)注入,提权,sa弱口令,爆破
tcp1521Oracle(甲骨文数据库)tns爆破,注入,弹shell…
tcp1500ISPmanager( 主机控制面板)弱口令
tcp1723PPTP(点对点隧道协议 )爆破,想办法钓VPN账号,进内网
tcp2082,2083cPanel (虚拟机控制系统 )弱口令
tcp2181ZooKeeper(分布式系统的可靠协调系统 )未授权访问
tcp2601,2604Zebra (zebra路由)默认密码zerbra
tcp3128Squid (代理缓存服务器)弱口令
tcp3312,3311kangle(web服务器)弱口令
tcp3306MySQL(数据库)注入,提权,爆破
tcp3389Windows rdp(桌面协议)shift后门[需要03以下的系统],爆破,ms12-020
tcp3690SVN(开放源代码的版本控制系统)svn泄露,未授权访问
tcp4848GlassFish(应用服务器)弱口令
tcp5000Sybase/DB2(数据库)爆破,注入
tcp5432PostgreSQL(数据库)爆破,注入,弱口令
tcp5900,5901,5902VNC(虚拟网络控制台,远控)弱口令爆破
tcp5984CouchDB(数据库)未授权导致的任意指令执行
tcp6379Redis(数据库)可尝试未授权访问,弱口令爆破
tcp7001,7002WebLogic(WEB应用系统)Java反序列化,弱口令
tcp7778Kloxo(虚拟主机管理系统)主机面板登录
tcp8000Ajenti(Linux服务器管理面板)弱口令
tcp8443Plesk(虚拟主机管理面板)弱口令
tcp8069Zabbix (系统网络监视)远程执行,SQL注入
tcp8080-8089Jenkins,JBoss (应用服务器)反序列化,控制台弱口令
tcp9080-9081,9090WebSphere(应用服务器)Java反序列化/弱口令
tcp9200,9300ElasticSearch (Lucene的搜索服务器)远程执行
tcp11211Memcached(缓存系统)未授权访问
tcp27017,27018MongoDB(数据库)爆破,未授权访问
tcp50070,50030Hadoop(分布式文件系统)默认端口未授权访问
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:服务器高危端口列表

+

文章作者:

+

发布时间:2022年08月02日 - 18:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/%E6%9C%8D%E5%8A%A1%E5%99%A8%E9%AB%98%E5%8D%B1%E7%AB%AF%E5%8F%A3%E5%88%97%E8%A1%A8/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/2023/09/20/DockerCompose\345\256\211\350\243\205/index.html" "b/en/2023/09/20/DockerCompose\345\256\211\350\243\205/index.html" new file mode 100644 index 0000000000..089ad2d886 --- /dev/null +++ "b/en/2023/09/20/DockerCompose\345\256\211\350\243\205/index.html" @@ -0,0 +1,561 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ +

+ + +
+ + + + +
+ + +

DockerCompose安装

1
2
3
4
5
DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
mkdir -p $DOCKER_CONFIG/cli-plugins
sudo curl -SL https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-x86_64 -o /usr/bin/docker-compose
sudo chmod +x /usr/bin/docker-compose
sudo docker-compose version
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:

+

文章作者:

+

发布时间:2023年09月20日 - 10:18

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2023/09/20/DockerCompose%E5%AE%89%E8%A3%85/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/2023/09/20/test/index.html b/en/2023/09/20/test/index.html new file mode 100644 index 0000000000..d37211237a --- /dev/null +++ b/en/2023/09/20/test/index.html @@ -0,0 +1,590 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + test | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ test +

+ + +
+ + + + +
+ + +

前言

最近各种技术博客,各种技术文章,各种牛人前辈的学习心得都在看,最近看了一个不错的文章叫做《如何正确使用时间》
感觉还是很不错的,特别适合年轻的程序员看一下。下面将全文贴在下方,如果需要也可以去原文查看。

+

原文

+

你是否时常会焦虑时间过的很快,没时间学习,本文将会分享一些个人的见解。

+

花时间补基础,读文档

在工作中我们时常会花很多时间去 debug,但是你是否发现很多问题最终只是你基础不扎实或者文档没有仔细看。

+

基础是你技术的基石,一定要花时间打好基础,而不是追各种新的技术。一旦你的基础扎实,学习各种新的技术也肯定不在话下,因为新的技术,究其根本都是相通的。

+

文档同样也是一门技术的基础。一个优秀的库,开发人员肯定已经把如何使用这个库都写在文档中了,仔细阅读文档一定会是少写 bug 的最省事路子。

+

学会搜索

如果你还在使用百度搜索编程问题,请尽快抛弃这个垃圾搜索引擎。同样一个关键字,使用百度和谷歌,谷歌基本完胜的。即使你使用中文在谷歌中搜索,得到的结果也往往是谷歌占优,所以如果你想迅速的通过搜索引擎来解决问题,那一定是谷歌。

+

学点英语

说到英语,一定是大家所最不想听的。其实我一直认为程序员学习英语是简单的,因为我们工作中是一直接触着英语,并且看懂技术文章,文档所需要的单词量是极少的。我时常在群里看到大家发出一个问题的截图问什么原因,其实在截图中英语已经很明白的说明了问题的所在,如果你的英语过关,完全不需要浪费时间来提问和搜索。所以我认为学点英语也是节省时间中很重要的一点。

+

那么如何去学习呢,chrome 装个翻译插件,直接拿英文文档或文章读,不会的就直接划词翻译,然后记录下这个单词并背诵。每天花半小时看点英文文档和文章,坚持两个月,你的英语水平不说别的,看文档和文章绝对不会有难题了。这一定是一个很划算的个人时间投资,花点时间学习英语,能为你将来的技术之路铺平很多坎。

+

画个图,想一想再做

你是否遇到过这种问题,需求一下来,看一眼,然后马上就按照设计稿开始做了,可能中间出个问题导致你需要返工。

+

如果你存在这样的问题,我很推荐在看到设计稿和需求的时候花点时间想一想,画一画。考虑一下设计稿中是否可以找到可以拆分出来的复用组件,是否存在之前写过的组件。该如何组织这个界面,数据的流转是怎么样的。然后画一下这个页面的需求,最后再动手做。

+

利用好下班时间学习

说到下班时间,那可能就有人说了公司很迟下班,这其实是国内很普遍的情况。但是我认为正常的加班是可以的,但是强制的加班就是在损耗你的身体和前途。

+

可以这么说,大部分的 996 公司,加班的这些时间并不会增加你的技术,无非就是在写一些重复的业务逻辑。也许你可以拿到更多的钱,但是代价是身体还有前途。程序员是靠技术吃饭的,如果你长久呆在一个长时间加班的公司,不能增长你的技术还要吞噬你的下班学习时间,那么你一定会废掉的。如果你遇到了这种情况,只能推荐尽快跳槽到非 996 的公司。

+

那么如果你有足够的下班时间,一定要花上 1, 2 小时去学习,上班大家基本都一样,技术的精进就是看下班以后的那几个小时了。如果你能利用好下班时间来学习,坚持下去,时间一定会给你很好的答复。

+

列好 ToDo

我喜欢规划好一段时间内要做的事情,并且要把事情拆分为小点。给 ToDo 列好优先级,紧急的优先级最高。相同优先级的我喜欢先做简单的,因为这样一旦完成就能划掉一个,提高成就感。

+

反思和整理

每周末都会花上点时间整理下本周记录的笔记和看到的不错文章。然后考虑下本周完成的工作和下周准备要完成的工作。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:test

+

文章作者:

+

发布时间:2023年09月20日 - 18:33

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2023/09/20/test/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/@attachment/README.html b/en/@attachment/README.html new file mode 100644 index 0000000000..ead0849ba9 --- /dev/null +++ b/en/@attachment/README.html @@ -0,0 +1,485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | 李旭光的成长博客 + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + + + +
+ + + + + +
+
+ +

+

+ + + +
+ + + + +
+

这里追加图片

+
+ + + +
+ + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/about/index.html b/en/about/index.html new file mode 100644 index 0000000000..81fe550059 --- /dev/null +++ b/en/about/index.html @@ -0,0 +1,707 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Know About Me | 李旭光的成长博客 + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + + + +
+ + + + + +
+
+ +

Know About Me +

+ + + +
+ + + + +
+

ZH | EN | JA

+

Xuguang.Li

+

FrontEnd Engineer 10 years
Tel: 15641181846
E-mail: lixuguang316@foxmail.com
Github: https://github.com/lixuguang
Blog: https://lixuguang.github.io
Address: HuangHe Road 852 ShaHeKou DaLian

+
+

SKLLS

    +
  • Use Html/Css/Javascript/Jq,Responsive Layout,MobileWeb perfectly.

    +
  • +
  • Use VUE/vuex/axios/vue-router/element-ui etc perfectly.

    +
  • +
  • Familiar with separation of front-end and backend,RestfulAPI,Git/SVN,CICD etc.

    +
  • +
  • 熟练使用chrome-dev对项目进行前端代码调试/调优

    +
  • +
  • Familiar with Nodejs/Electron/React/less/sass/bootstrap etc.

    +
  • +
  • Familiar with webpack,npm etc.

    +
  • +
  • 掌握前端架构方法,对前端规范性要求非常熟悉,能够根据产品/项目需求给出最优技术选型方案。

    +
  • +
  • 熟练使用svn/git等代码版本管理工具及gitflow管理方式进行代码管理

    +
  • +
  • 8年前端开发经验,参与过100+的项目开发。

    +
  • +
  • 善于团队协作,自我驱动,持续学习,热爱工作,责任心强。

    +
  • +
+

WORK EXPERIENCE

+

华宇(大连)信息服务有限公司:主要面向政府、公检法机关及互联网市场大客户。
2014.5-至今
资深前端开发工程师 团队规模:30人 公司规模:600人
主要职责:

+
+
    +
  • 编写代码
      +
    • 完成正常项目任务
        +
      • 累计完成项目超过150个
      • +
      • 参与项目类型包括pcWeb、padWeb、手机Web、微信Web、触摸屏、指挥控制大屏
      • +
      • 业务类型涉及政府、公检法各个领域
      • +
      • 使用技术包含HTML4/5、CSS2/3、ES5/6、bootstrap、framework7、Angular、Vue以及Artery框架等
      • +
      +
    • +
    • 极短时间内完成任务
        +
      • 竞标项目如:
          +
        • 5天60个页面带领一个初级前端和一个中级前端完成。
        • +
        • 3天完成一个问卷调查包含各种交互的特效。
        • +
        • 封闭1星期同3位开发共同完成canvas画板功能,支持检察官记录员实现档案卷宗实时编辑。
        • +
        +
      • +
      • 演示项目如:
          +
        • 1个月带领一个初级前端一个初级开发完成整个系统包含业务逻辑除后台的所有功能,实现超高保真原型。
        • +
        +
      • +
      • 救火任务如:
          +
        • 上线前2天解决其他人无法解决的问题,此类情况居多。
        • +
        • 项目工期提前,顶着压力加班完成任务。如1天13个页面。
        • +
        +
      • +
      +
    • +
    • 新技术框架研究应用
        +
      • 前后端分离技术
          +
        • 组织前端团队学习前后端分离思想,研究前后端分离所要学习的新技能知识,面向对象的思想以及面向接口的思想等。
        • +
        +
      • +
      • 前端自动化构建、自动检测
          +
        • 组织前端团队学习前端自动化构建如:npm包管理工具、各种cli工具使用、eslint、stylint等自动化检查工具,以及webpack和gulp等打包工具使用。
        • +
        +
      • +
      • MVVM框架等新技术
          +
        • 组织学习研究Angular2、Vue2等数据驱动的MVVM框架,并在实际项目中使用VUE框架。
        • +
        +
      • +
      +
    • +
    • 组织构建基础服务
        +
      • 前端通用组件库
      • +
      • Vue组件库
      • +
      • 前端题库
      • +
      • +
      +
    • +
    +
  • +
  • 制定规范
      +
    • 前端编码规范
        +
      • 包括HTML规范、CSS规范、JS规范、Vue规范、通用规范等。
      • +
      +
    • +
    • 团队协作制度
        +
      • 包括代码管理制度、团队协作规范制度。
      • +
      +
    • +
    • 代码审查规范
        +
      • 人工审核
      • +
      • 自动化检测工具审核
      • +
      +
    • +
    • 知识考核规范
        +
      • 前端知识点地图
      • +
      • 前端技能栈思维导图
      • +
      • 前端考试平台
      • +
      +
    • +
    +
  • +
  • 团队建设
      +
    • 面试官
        +
      • 从原来的2人前端团队发展至16人前端团队,4年流失率仅为3人。
      • +
      +
    • +
    • 组织师傅帮带
        +
      • 组织先进带后进,高级带初级,建立前端梯队,组织共同学习。
      • +
      +
    • +
    • 前端兴趣小组
        +
      • 每周二组织前端兴趣小组,了解当下最新的前端知识,组织分享活动。
      • +
      +
    • +
    • 前端培训
        +
      • 不定期组织学习公司将要使用的新技术以及开发过程中遇到的问题解决方案
      • +
      • 定期组织基础知识、中级知识、高级知识、实践应用四个层面的培训。
      • +
      +
    • +
    +
  • +
  • 人才培养
      +
    • 人员考核
        +
      • 根据建立的知识点地图建立考点地图,建立试卷进行考评,关系到员工晋升。
      • +
      +
    • +
    • 建立人才档案,定期谈话
        +
      • 根据不同人的特点建立对应的人才档案,通过季度谈话的方式了解员工最新动态,根据员工特点设立学习方向和目标,引导员工成长。
      • +
      +
    • +
    • 参与人才晋升考官
        +
      • 参与员工晋升考核,给出努力方向。
      • +
      +
    • +
    +
  • +
+
+
+

大连新桥科技发展有限公司:面向教育系统提供整体服务。
2013.1-2014.1
用户体验团队负责人 团队规模:6人 公司规模:50人
主要职责:

+
+
    +
  • 接访客户
      +
    • 去客户现场或客户到工作现场,负责接待客户,了解客户需求。
    • +
    +
  • +
  • 设计、编写代码
      +
    • 根据客户或领导需求完成需求分析、视觉设计、代码编写等任务。涉及pcWeb、padWeb、手机Web等。
    • +
    +
  • +
  • 团队管理
      +
    • 根据任务量分配工作,协调团队资源。
    • +
    +
  • +
+
+
+

百维数元信息科技(北京)有限公司:一斑网在线调研平台。
2011.9-2012.12
开发工程师 团队规模:3人 公司规模:8人
主要职责:

+
+
    +
  • 设计、前后台代码编写、运营、客服
      +
    • 初创公司,开发运营团队3人,主要负责设计工作、前端开发、部分后端开发、运营、客服等工作。
    • +
    +
  • +
+

EDUCATION EXPERIENCE

+

大连外国语大学
2008.9-2012.6
信息系统与信息管理(日英双语强化)
本科
主要职务:

+
+
    +
  • 计算机部部长
      +
    • 组织计算机竞赛、考级
    • +
    +
  • +
  • 团支书
      +
    • 组织班级党员发展、思想工作。
    • +
    +
  • +
  • 网络协会核心成员
      +
    • 为在校生提供vod服务,翻译日英影音资料供大家学习。
      +

      主要荣誉:

      +
      +
    • +
    +
  • +
  • 学习标兵
  • +
  • 优秀团支书
  • +
  • 优秀毕业生
  • +
  • 每一季度奖学金(一、二、三等均获得过)
  • +
+

PROJECT EXPERIENCE

    +
  • ERP系统
      +
    • 法院执行线索分析系统v2.1-v2.2
    • +
    • 数据质量检查系统v2.0-v2.3
    • +
    • 法官办案辅助系统
    • +
    • 量刑规范化服务系统
    • +
    • 裁判文书上网直报系统V2.0
    • +
    • 律师阅卷管理系统
    • +
    • … 100+
    • +
    +
  • +
  • CMS网站
      +
    • 诉讼服务网系列 50+
    • +
    • 法院官网 20+
    • +
    • … 30+
    • +
    +
  • +
  • pad、手机、触控屏系统
      +
    • 信息引导侦查系列产品
    • +
    • 远程视频会见系统2.1
    • +
    • 领导驾驶舱
    • +
    • 移动办案APP
    • +
    • … 30+
    • +
    +
  • +
  • 普通网站
      +
    • 企业官网、政府官网 10+
    • +
    +
  • +
+
+

涉及技术包含:
语言:
HTML4/5、CSS2/3、ES5/6、JSP…
框架:
JQ、bootstarp、mui、framework7、VUE…
理论:
前后端分离(面向对象OOB + 异步请求ajax + 面向接口api)

+
+

INTRODUCATION

从2011年算起我已经从事前端开发8年、设计2年(重叠)、团队管理4年(重叠),参与各类大小项目200+,涉及各个业务领域包括公检法政、教育、调研,覆盖目前主流的前端技术(游戏向不包含)。基础能力扎实,能够解决绝大多数普通问题和部分棘手问题,从小在军营成长让我对团队纪律与制度有着深刻的认识,知道规范与制度的重要性,这为我建立高效优质的团队提供了良好土壤。谦逊使我可以与团队中以及团队之间有着良好的沟通,好学让我不断逃出舒适区,让自己不断的学习进步。有着个人荣誉感与集体荣誉感,让我对工作和团队认真负责。抗压能力强,让领导放心把最紧急最重要的工作交给我做,加班也毫无怨言。工作久了让我总结了一些工作中常见的问题,我会讲这些分享给他人,让大家一同进步,将问题扼杀在摇篮里。愿意组织活动参加活动,喜欢阅读,跑步,骑行,篮球。

+ +
+ + + +
+ + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2013/06/index.html b/en/archives/2013/06/index.html new file mode 100644 index 0000000000..216fd77728 --- /dev/null +++ b/en/archives/2013/06/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2013 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2013/index.html b/en/archives/2013/index.html new file mode 100644 index 0000000000..fb8592d1ea --- /dev/null +++ b/en/archives/2013/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2013 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2015/12/index.html b/en/archives/2015/12/index.html new file mode 100644 index 0000000000..4145f140af --- /dev/null +++ b/en/archives/2015/12/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2015 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2015/index.html b/en/archives/2015/index.html new file mode 100644 index 0000000000..2f88af05e3 --- /dev/null +++ b/en/archives/2015/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2015 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2016/10/index.html b/en/archives/2016/10/index.html new file mode 100644 index 0000000000..4ca9ad0f17 --- /dev/null +++ b/en/archives/2016/10/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2016 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2016/index.html b/en/archives/2016/index.html new file mode 100644 index 0000000000..f1ed4e489a --- /dev/null +++ b/en/archives/2016/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2016 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2017/08/index.html b/en/archives/2017/08/index.html new file mode 100644 index 0000000000..3e2be2e8ec --- /dev/null +++ b/en/archives/2017/08/index.html @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2017 +
+ + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2017/10/index.html b/en/archives/2017/10/index.html new file mode 100644 index 0000000000..687ac28457 --- /dev/null +++ b/en/archives/2017/10/index.html @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2017 +
+ + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2017/12/index.html b/en/archives/2017/12/index.html new file mode 100644 index 0000000000..6da9952c6e --- /dev/null +++ b/en/archives/2017/12/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2017 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2017/index.html b/en/archives/2017/index.html new file mode 100644 index 0000000000..688566ab17 --- /dev/null +++ b/en/archives/2017/index.html @@ -0,0 +1,618 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2017 +
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2019/12/index.html b/en/archives/2019/12/index.html new file mode 100644 index 0000000000..a4e387d200 --- /dev/null +++ b/en/archives/2019/12/index.html @@ -0,0 +1,558 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2019 +
+ + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2019/index.html b/en/archives/2019/index.html new file mode 100644 index 0000000000..f8d7d9440e --- /dev/null +++ b/en/archives/2019/index.html @@ -0,0 +1,558 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2019 +
+ + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2020/01/index.html b/en/archives/2020/01/index.html new file mode 100644 index 0000000000..bce4c8b7aa --- /dev/null +++ b/en/archives/2020/01/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2020/01/page/2/index.html b/en/archives/2020/01/page/2/index.html new file mode 100644 index 0000000000..6d48bb4488 --- /dev/null +++ b/en/archives/2020/01/page/2/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2020/01/page/3/index.html b/en/archives/2020/01/page/3/index.html new file mode 100644 index 0000000000..c5639315a0 --- /dev/null +++ b/en/archives/2020/01/page/3/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2020/01/page/4/index.html b/en/archives/2020/01/page/4/index.html new file mode 100644 index 0000000000..831d6ce210 --- /dev/null +++ b/en/archives/2020/01/page/4/index.html @@ -0,0 +1,601 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2020/02/index.html b/en/archives/2020/02/index.html new file mode 100644 index 0000000000..5918bfe7ce --- /dev/null +++ b/en/archives/2020/02/index.html @@ -0,0 +1,598 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2020/03/index.html b/en/archives/2020/03/index.html new file mode 100644 index 0000000000..4aa0c86815 --- /dev/null +++ b/en/archives/2020/03/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2020/03/page/2/index.html b/en/archives/2020/03/page/2/index.html new file mode 100644 index 0000000000..6ffa2b0834 --- /dev/null +++ b/en/archives/2020/03/page/2/index.html @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2020/04/index.html b/en/archives/2020/04/index.html new file mode 100644 index 0000000000..4b11df6a60 --- /dev/null +++ b/en/archives/2020/04/index.html @@ -0,0 +1,578 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2020/05/index.html b/en/archives/2020/05/index.html new file mode 100644 index 0000000000..7b3819d658 --- /dev/null +++ b/en/archives/2020/05/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2020/07/index.html b/en/archives/2020/07/index.html new file mode 100644 index 0000000000..e640519ba3 --- /dev/null +++ b/en/archives/2020/07/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2020/08/index.html b/en/archives/2020/08/index.html new file mode 100644 index 0000000000..7f5340bc4e --- /dev/null +++ b/en/archives/2020/08/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2020/11/index.html b/en/archives/2020/11/index.html new file mode 100644 index 0000000000..e6f0c2ec76 --- /dev/null +++ b/en/archives/2020/11/index.html @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2020/index.html b/en/archives/2020/index.html new file mode 100644 index 0000000000..f97ed12067 --- /dev/null +++ b/en/archives/2020/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2020/page/2/index.html b/en/archives/2020/page/2/index.html new file mode 100644 index 0000000000..ea34504bcc --- /dev/null +++ b/en/archives/2020/page/2/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2020/page/3/index.html b/en/archives/2020/page/3/index.html new file mode 100644 index 0000000000..9f930a4625 --- /dev/null +++ b/en/archives/2020/page/3/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2020/page/4/index.html b/en/archives/2020/page/4/index.html new file mode 100644 index 0000000000..48cbc97c64 --- /dev/null +++ b/en/archives/2020/page/4/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2020/page/5/index.html b/en/archives/2020/page/5/index.html new file mode 100644 index 0000000000..3a09411e08 --- /dev/null +++ b/en/archives/2020/page/5/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2020/page/6/index.html b/en/archives/2020/page/6/index.html new file mode 100644 index 0000000000..4d7aaead7b --- /dev/null +++ b/en/archives/2020/page/6/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2020/page/7/index.html b/en/archives/2020/page/7/index.html new file mode 100644 index 0000000000..297544d2ab --- /dev/null +++ b/en/archives/2020/page/7/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2020/page/8/index.html b/en/archives/2020/page/8/index.html new file mode 100644 index 0000000000..74c1825a5e --- /dev/null +++ b/en/archives/2020/page/8/index.html @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2021/05/index.html b/en/archives/2021/05/index.html new file mode 100644 index 0000000000..b6afd4afbd --- /dev/null +++ b/en/archives/2021/05/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2021 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2021/index.html b/en/archives/2021/index.html new file mode 100644 index 0000000000..85ee9419a7 --- /dev/null +++ b/en/archives/2021/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2021 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2022/03/index.html b/en/archives/2022/03/index.html new file mode 100644 index 0000000000..48362ddf8a --- /dev/null +++ b/en/archives/2022/03/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2022/06/index.html b/en/archives/2022/06/index.html new file mode 100644 index 0000000000..d62d41311a --- /dev/null +++ b/en/archives/2022/06/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2022/06/page/2/index.html b/en/archives/2022/06/page/2/index.html new file mode 100644 index 0000000000..2f572c19c7 --- /dev/null +++ b/en/archives/2022/06/page/2/index.html @@ -0,0 +1,621 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2022/08/index.html b/en/archives/2022/08/index.html new file mode 100644 index 0000000000..3b337f6d3d --- /dev/null +++ b/en/archives/2022/08/index.html @@ -0,0 +1,638 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2022/index.html b/en/archives/2022/index.html new file mode 100644 index 0000000000..9c5f8e9c6d --- /dev/null +++ b/en/archives/2022/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2022/page/2/index.html b/en/archives/2022/page/2/index.html new file mode 100644 index 0000000000..bbd5b1fd44 --- /dev/null +++ b/en/archives/2022/page/2/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2022/page/3/index.html b/en/archives/2022/page/3/index.html new file mode 100644 index 0000000000..d8793bf36b --- /dev/null +++ b/en/archives/2022/page/3/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2022/page/4/index.html b/en/archives/2022/page/4/index.html new file mode 100644 index 0000000000..dee274e4ac --- /dev/null +++ b/en/archives/2022/page/4/index.html @@ -0,0 +1,461 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2023/09/index.html b/en/archives/2023/09/index.html new file mode 100644 index 0000000000..225cdc8cc2 --- /dev/null +++ b/en/archives/2023/09/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2023 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/2023/index.html b/en/archives/2023/index.html new file mode 100644 index 0000000000..2682dff61d --- /dev/null +++ b/en/archives/2023/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2023 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/index.html b/en/archives/index.html new file mode 100644 index 0000000000..ab534017c5 --- /dev/null +++ b/en/archives/index.html @@ -0,0 +1,644 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2023 +
+ + + + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/page/10/index.html b/en/archives/page/10/index.html new file mode 100644 index 0000000000..0ff1d7731c --- /dev/null +++ b/en/archives/page/10/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/page/11/index.html b/en/archives/page/11/index.html new file mode 100644 index 0000000000..3b25ac8dc9 --- /dev/null +++ b/en/archives/page/11/index.html @@ -0,0 +1,644 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + +
+ 2019 +
+ + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/page/12/index.html b/en/archives/page/12/index.html new file mode 100644 index 0000000000..e2b02b4cfc --- /dev/null +++ b/en/archives/page/12/index.html @@ -0,0 +1,644 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2019 +
+ + + + + + + + + + +
+ 2017 +
+ + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/page/13/index.html b/en/archives/page/13/index.html new file mode 100644 index 0000000000..28e75dbb15 --- /dev/null +++ b/en/archives/page/13/index.html @@ -0,0 +1,610 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2017 +
+ + + + + + + + +
+ 2016 +
+ + +
+ 2015 +
+ + +
+ 2013 +
+ + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/page/2/index.html b/en/archives/page/2/index.html new file mode 100644 index 0000000000..80c2f5f53f --- /dev/null +++ b/en/archives/page/2/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/page/3/index.html b/en/archives/page/3/index.html new file mode 100644 index 0000000000..8e4c1b57fe --- /dev/null +++ b/en/archives/page/3/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/page/4/index.html b/en/archives/page/4/index.html new file mode 100644 index 0000000000..5a020d33bb --- /dev/null +++ b/en/archives/page/4/index.html @@ -0,0 +1,647 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + + + +
+ 2021 +
+ + + + +
+ 2020 +
+ + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/page/5/index.html b/en/archives/page/5/index.html new file mode 100644 index 0000000000..c296af4ac2 --- /dev/null +++ b/en/archives/page/5/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/page/6/index.html b/en/archives/page/6/index.html new file mode 100644 index 0000000000..721e76e740 --- /dev/null +++ b/en/archives/page/6/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/page/7/index.html b/en/archives/page/7/index.html new file mode 100644 index 0000000000..fa004afc23 --- /dev/null +++ b/en/archives/page/7/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/page/8/index.html b/en/archives/page/8/index.html new file mode 100644 index 0000000000..fd2b1bc6df --- /dev/null +++ b/en/archives/page/8/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/archives/page/9/index.html b/en/archives/page/9/index.html new file mode 100644 index 0000000000..500a6c5a00 --- /dev/null +++ b/en/archives/page/9/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + Good! 128 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/categories/Database/Postgresql/index.html b/en/categories/Database/Postgresql/index.html new file mode 100644 index 0000000000..6fcd50df63 --- /dev/null +++ b/en/categories/Database/Postgresql/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Postgresql | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Postgresql + Category +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/categories/Database/index.html b/en/categories/Database/index.html new file mode 100644 index 0000000000..3a0cc8bb19 --- /dev/null +++ b/en/categories/Database/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Database | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Database + Category +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/categories/Docker/index.html b/en/categories/Docker/index.html new file mode 100644 index 0000000000..1c895babfa --- /dev/null +++ b/en/categories/Docker/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Docker | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Docker + Category +

+
+ + +
+ 2022 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/categories/Infra/Docker/index.html b/en/categories/Infra/Docker/index.html new file mode 100644 index 0000000000..beb4d1d303 --- /dev/null +++ b/en/categories/Infra/Docker/index.html @@ -0,0 +1,538 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Docker | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Docker + Category +

+
+ + +
+ 2022 +
+ + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/Infra/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" "b/en/categories/Infra/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" new file mode 100644 index 0000000000..69db8060a6 --- /dev/null +++ "b/en/categories/Infra/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Nginx | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Nginx + Category +

+
+ + +
+ 2022 +
+ + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/Infra/Web\346\234\215\345\212\241\345\231\250/Squid/index.html" "b/en/categories/Infra/Web\346\234\215\345\212\241\345\231\250/Squid/index.html" new file mode 100644 index 0000000000..8a278e1552 --- /dev/null +++ "b/en/categories/Infra/Web\346\234\215\345\212\241\345\231\250/Squid/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Squid | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Squid + Category +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/Infra/Web\346\234\215\345\212\241\345\231\250/index.html" "b/en/categories/Infra/Web\346\234\215\345\212\241\345\231\250/index.html" new file mode 100644 index 0000000000..607cf684d4 --- /dev/null +++ "b/en/categories/Infra/Web\346\234\215\345\212\241\345\231\250/index.html" @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Web服务器 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Web服务器 + Category +

+
+ + +
+ 2022 +
+ + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/categories/Infra/index.html b/en/categories/Infra/index.html new file mode 100644 index 0000000000..ad71d533ca --- /dev/null +++ b/en/categories/Infra/index.html @@ -0,0 +1,618 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Infra | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Infra + Category +

+
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/categories/Infrastructure/index.html b/en/categories/Infrastructure/index.html new file mode 100644 index 0000000000..8a31b1ab3d --- /dev/null +++ b/en/categories/Infrastructure/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Infrastructure | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Infrastructure + Category +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/categories/Linux/Oracle/Docker/index.html b/en/categories/Linux/Oracle/Docker/index.html new file mode 100644 index 0000000000..11b32875e2 --- /dev/null +++ b/en/categories/Linux/Oracle/Docker/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Docker | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Docker + Category +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/categories/Linux/Oracle/index.html b/en/categories/Linux/Oracle/index.html new file mode 100644 index 0000000000..ed6df54946 --- /dev/null +++ b/en/categories/Linux/Oracle/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Oracle | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Oracle + Category +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/categories/Linux/RPM/index.html b/en/categories/Linux/RPM/index.html new file mode 100644 index 0000000000..5dc046212b --- /dev/null +++ b/en/categories/Linux/RPM/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: RPM | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

RPM + Category +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/categories/Linux/index.html b/en/categories/Linux/index.html new file mode 100644 index 0000000000..50e3ba97ee --- /dev/null +++ b/en/categories/Linux/index.html @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Linux | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Linux + Category +

+
+ + +
+ 2022 +
+ + + + +
+ 2020 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/categories/NodeJS/Npm/index.html b/en/categories/NodeJS/Npm/index.html new file mode 100644 index 0000000000..46741c9df5 --- /dev/null +++ b/en/categories/NodeJS/Npm/index.html @@ -0,0 +1,481 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Npm | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Npm + Category +

+
+ + +
+ 2020 +
+ + +
+ 2017 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/categories/NodeJS/index.html b/en/categories/NodeJS/index.html new file mode 100644 index 0000000000..2e0cdcaec3 --- /dev/null +++ b/en/categories/NodeJS/index.html @@ -0,0 +1,541 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: NodeJS | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

NodeJS + Category +

+
+ + +
+ 2020 +
+ + + + + + + + +
+ 2017 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/categories/OracleLinux/index.html b/en/categories/OracleLinux/index.html new file mode 100644 index 0000000000..24cf40cb9a --- /dev/null +++ b/en/categories/OracleLinux/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: OracleLinux | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

OracleLinux + Category +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" "b/en/categories/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" new file mode 100644 index 0000000000..ff23b1a430 --- /dev/null +++ "b/en/categories/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Nginx | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Nginx + Category +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/Web\346\234\215\345\212\241\345\231\250/index.html" "b/en/categories/Web\346\234\215\345\212\241\345\231\250/index.html" new file mode 100644 index 0000000000..8911d16a3e --- /dev/null +++ "b/en/categories/Web\346\234\215\345\212\241\345\231\250/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Web服务器 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Web服务器 + Category +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/categories/index.html b/en/categories/index.html new file mode 100644 index 0000000000..04194d3848 --- /dev/null +++ b/en/categories/index.html @@ -0,0 +1,470 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类 | 李旭光的成长博客 + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + + + + + + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/categories/linux/index.html b/en/categories/linux/index.html new file mode 100644 index 0000000000..3a8b2616e9 --- /dev/null +++ b/en/categories/linux/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: linux | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

linux + Category +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/categories/linux/systemctl/index.html b/en/categories/linux/systemctl/index.html new file mode 100644 index 0000000000..6df125340e --- /dev/null +++ b/en/categories/linux/systemctl/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: systemctl | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

systemctl + Category +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/Webpack/index.html" "b/en/categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/Webpack/index.html" new file mode 100644 index 0000000000..525765593d --- /dev/null +++ "b/en/categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/Webpack/index.html" @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Webpack | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Webpack + Category +

+
+ + +
+ 2020 +
+ + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/index.html" "b/en/categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/index.html" new file mode 100644 index 0000000000..b5d67201aa --- /dev/null +++ "b/en/categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/index.html" @@ -0,0 +1,541 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 前端工程化 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端工程化 + Category +

+
+ + +
+ 2020 +
+ + + + + + + + +
+ 2019 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\211\215\347\253\257\345\274\200\345\217\221\350\247\204\350\214\203/index.html" "b/en/categories/\345\211\215\347\253\257\345\274\200\345\217\221\350\247\204\350\214\203/index.html" new file mode 100644 index 0000000000..3bf6b1b946 --- /dev/null +++ "b/en/categories/\345\211\215\347\253\257\345\274\200\345\217\221\350\247\204\350\214\203/index.html" @@ -0,0 +1,578 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 前端开发规范 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端开发规范 + Category +

+
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS/index.html" "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS/index.html" new file mode 100644 index 0000000000..b4679289ba --- /dev/null +++ "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: CSS | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

CSS + Category +

+
+ + +
+ 2013 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS3/index.html" "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS3/index.html" new file mode 100644 index 0000000000..b9a6f170a7 --- /dev/null +++ "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS3/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: CSS3 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

CSS3 + Category +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/index.html" "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/index.html" new file mode 100644 index 0000000000..2b221b60f3 --- /dev/null +++ "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/index.html" @@ -0,0 +1,647 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 前端技术 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端技术 + Category +

+
+ + +
+ 2022 +
+ + + + + + +
+ 2021 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/2/index.html" "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/2/index.html" new file mode 100644 index 0000000000..8a47d4cc42 --- /dev/null +++ "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/2/index.html" @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 前端技术 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端技术 + Category +

+
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/3/index.html" "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/3/index.html" new file mode 100644 index 0000000000..ab72c6bdbe --- /dev/null +++ "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/3/index.html" @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 前端技术 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端技术 + Category +

+
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/4/index.html" "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/4/index.html" new file mode 100644 index 0000000000..e7e4d58707 --- /dev/null +++ "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/4/index.html" @@ -0,0 +1,644 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 前端技术 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端技术 + Category +

+
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + +
+ 2019 +
+ + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/5/index.html" "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/5/index.html" new file mode 100644 index 0000000000..d452284d96 --- /dev/null +++ "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/5/index.html" @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 前端技术 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端技术 + Category +

+
+ + +
+ 2017 +
+ + + + +
+ 2013 +
+ + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\205\274\345\256\271\346\200\247/index.html" "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\205\274\345\256\271\346\200\247/index.html" new file mode 100644 index 0000000000..cc00c83f2e --- /dev/null +++ "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\205\274\345\256\271\346\200\247/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 兼容性 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

兼容性 + Category +

+
+ + +
+ 2017 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/index.html" "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/index.html" new file mode 100644 index 0000000000..cecbdc5bdc --- /dev/null +++ "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/index.html" @@ -0,0 +1,644 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 前端常见知识点整理 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端常见知识点整理 + Category +

+
+ + +
+ 2022 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/2/index.html" "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/2/index.html" new file mode 100644 index 0000000000..80669505db --- /dev/null +++ "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/2/index.html" @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 前端常见知识点整理 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端常见知识点整理 + Category +

+
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/3/index.html" "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/3/index.html" new file mode 100644 index 0000000000..75edce8edd --- /dev/null +++ "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/3/index.html" @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 前端常见知识点整理 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端常见知识点整理 + Category +

+
+ + +
+ 2020 +
+ + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\351\227\256\351\242\230/index.html" "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\351\227\256\351\242\230/index.html" new file mode 100644 index 0000000000..d00090773b --- /dev/null +++ "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\351\227\256\351\242\230/index.html" @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 前端问题 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端问题 + Category +

+
+ + +
+ 2020 +
+ + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\276\256\344\277\241/index.html" "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\276\256\344\277\241/index.html" new file mode 100644 index 0000000000..0a6f74e021 --- /dev/null +++ "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\276\256\344\277\241/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 微信 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

微信 + Category +

+
+ + +
+ 2019 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\350\256\241\347\256\227\346\234\272\351\200\232\350\257\206/index.html" "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\350\256\241\347\256\227\346\234\272\351\200\232\350\257\206/index.html" new file mode 100644 index 0000000000..7bbe8764a5 --- /dev/null +++ "b/en/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\350\256\241\347\256\227\346\234\272\351\200\232\350\257\206/index.html" @@ -0,0 +1,524 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 计算机通识 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

计算机通识 + Category +

+
+ + +
+ 2022 +
+ + + + +
+ 2021 +
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\211\215\347\253\257\346\241\206\346\236\266/React/index.html" "b/en/categories/\345\211\215\347\253\257\346\241\206\346\236\266/React/index.html" new file mode 100644 index 0000000000..f4dcc1eb4b --- /dev/null +++ "b/en/categories/\345\211\215\347\253\257\346\241\206\346\236\266/React/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: React | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

React + Category +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\211\215\347\253\257\346\241\206\346\236\266/Vue/index.html" "b/en/categories/\345\211\215\347\253\257\346\241\206\346\236\266/Vue/index.html" new file mode 100644 index 0000000000..7bdad711d7 --- /dev/null +++ "b/en/categories/\345\211\215\347\253\257\346\241\206\346\236\266/Vue/index.html" @@ -0,0 +1,547 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Vue | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Vue + Category +

+
+ + +
+ 2021 +
+ + +
+ 2020 +
+ + + + +
+ 2019 +
+ + +
+ 2017 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\211\215\347\253\257\346\241\206\346\236\266/index.html" "b/en/categories/\345\211\215\347\253\257\346\241\206\346\236\266/index.html" new file mode 100644 index 0000000000..c4e082a274 --- /dev/null +++ "b/en/categories/\345\211\215\347\253\257\346\241\206\346\236\266/index.html" @@ -0,0 +1,567 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 前端框架 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端框架 + Category +

+
+ + +
+ 2021 +
+ + +
+ 2020 +
+ + + + + + +
+ 2019 +
+ + +
+ 2017 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\220\216\347\253\257\345\274\200\345\217\221/Go\350\257\255\350\250\200/index.html" "b/en/categories/\345\220\216\347\253\257\345\274\200\345\217\221/Go\350\257\255\350\250\200/index.html" new file mode 100644 index 0000000000..8f89d0333b --- /dev/null +++ "b/en/categories/\345\220\216\347\253\257\345\274\200\345\217\221/Go\350\257\255\350\250\200/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Go语言 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Go语言 + Category +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\220\216\347\253\257\345\274\200\345\217\221/index.html" "b/en/categories/\345\220\216\347\253\257\345\274\200\345\217\221/index.html" new file mode 100644 index 0000000000..9be79540e8 --- /dev/null +++ "b/en/categories/\345\220\216\347\253\257\345\274\200\345\217\221/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 后端开发 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

后端开发 + Category +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\244\247\345\211\215\347\253\257/index.html" "b/en/categories/\345\244\247\345\211\215\347\253\257/index.html" new file mode 100644 index 0000000000..2b65235f78 --- /dev/null +++ "b/en/categories/\345\244\247\345\211\215\347\253\257/index.html" @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 大前端 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

大前端 + Category +

+
+ + +
+ 2020 +
+ + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\244\247\345\211\215\347\253\257/\345\205\250\346\240\210/index.html" "b/en/categories/\345\244\247\345\211\215\347\253\257/\345\205\250\346\240\210/index.html" new file mode 100644 index 0000000000..9ed66a01ef --- /dev/null +++ "b/en/categories/\345\244\247\345\211\215\347\253\257/\345\205\250\346\240\210/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 全栈 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

全栈 + Category +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\244\247\345\211\215\347\253\257/\345\256\242\346\210\267\347\253\257\346\212\200\346\234\257/index.html" "b/en/categories/\345\244\247\345\211\215\347\253\257/\345\256\242\346\210\267\347\253\257\346\212\200\346\234\257/index.html" new file mode 100644 index 0000000000..5c1b408380 --- /dev/null +++ "b/en/categories/\345\244\247\345\211\215\347\253\257/\345\256\242\346\210\267\347\253\257\346\212\200\346\234\257/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 客户端技术 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

客户端技术 + Category +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\345\244\247\345\211\215\347\253\257/\347\247\273\345\212\250\345\274\200\345\217\221/index.html" "b/en/categories/\345\244\247\345\211\215\347\253\257/\347\247\273\345\212\250\345\274\200\345\217\221/index.html" new file mode 100644 index 0000000000..4cbf2bfe1c --- /dev/null +++ "b/en/categories/\345\244\247\345\211\215\347\253\257/\347\247\273\345\212\250\345\274\200\345\217\221/index.html" @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 移动开发 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

移动开发 + Category +

+
+ + +
+ 2020 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\346\225\210\347\216\207\345\267\245\345\205\267/Labrary/index.html" "b/en/categories/\346\225\210\347\216\207\345\267\245\345\205\267/Labrary/index.html" new file mode 100644 index 0000000000..36f7d1c4a5 --- /dev/null +++ "b/en/categories/\346\225\210\347\216\207\345\267\245\345\205\267/Labrary/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Labrary | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Labrary + Category +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\346\225\210\347\216\207\345\267\245\345\205\267/VSCode\346\217\222\344\273\266/index.html" "b/en/categories/\346\225\210\347\216\207\345\267\245\345\205\267/VSCode\346\217\222\344\273\266/index.html" new file mode 100644 index 0000000000..186edfa0af --- /dev/null +++ "b/en/categories/\346\225\210\347\216\207\345\267\245\345\205\267/VSCode\346\217\222\344\273\266/index.html" @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: VSCode插件 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

VSCode插件 + Category +

+
+ + +
+ 2020 +
+ + +
+ 2017 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\346\225\210\347\216\207\345\267\245\345\205\267/index.html" "b/en/categories/\346\225\210\347\216\207\345\267\245\345\205\267/index.html" new file mode 100644 index 0000000000..c62d76ffe8 --- /dev/null +++ "b/en/categories/\346\225\210\347\216\207\345\267\245\345\205\267/index.html" @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 效率工具 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

效率工具 + Category +

+
+ + +
+ 2020 +
+ + + + +
+ 2017 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\346\235\202\344\270\203\346\235\202\345\205\253/index.html" "b/en/categories/\346\235\202\344\270\203\346\235\202\345\205\253/index.html" new file mode 100644 index 0000000000..08d544e9d4 --- /dev/null +++ "b/en/categories/\346\235\202\344\270\203\346\235\202\345\205\253/index.html" @@ -0,0 +1,567 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 杂七杂八 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

杂七杂八 + Category +

+
+ + +
+ 2022 +
+ + +
+ 2020 +
+ + + + +
+ 2019 +
+ + +
+ 2017 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\346\235\202\344\270\203\346\235\202\345\205\253/\345\215\232\345\256\242\346\212\200\345\267\247/index.html" "b/en/categories/\346\235\202\344\270\203\346\235\202\345\205\253/\345\215\232\345\256\242\346\212\200\345\267\247/index.html" new file mode 100644 index 0000000000..2f7f9d0f23 --- /dev/null +++ "b/en/categories/\346\235\202\344\270\203\346\235\202\345\205\253/\345\215\232\345\256\242\346\212\200\345\267\247/index.html" @@ -0,0 +1,547 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 博客技巧 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

博客技巧 + Category +

+
+ + +
+ 2022 +
+ + +
+ 2020 +
+ + +
+ 2019 +
+ + +
+ 2017 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\346\236\266\346\236\204/index.html" "b/en/categories/\346\236\266\346\236\204/index.html" new file mode 100644 index 0000000000..0ba5187471 --- /dev/null +++ "b/en/categories/\346\236\266\346\236\204/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 架构 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

架构 + Category +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\346\272\220\347\240\201\345\216\237\347\220\206/index.html" "b/en/categories/\346\272\220\347\240\201\345\216\237\347\220\206/index.html" new file mode 100644 index 0000000000..c87ea378c7 --- /dev/null +++ "b/en/categories/\346\272\220\347\240\201\345\216\237\347\220\206/index.html" @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 源码原理 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

源码原理 + Category +

+
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\346\272\220\347\240\201\345\216\237\347\220\206/page/2/index.html" "b/en/categories/\346\272\220\347\240\201\345\216\237\347\220\206/page/2/index.html" new file mode 100644 index 0000000000..df773672a1 --- /dev/null +++ "b/en/categories/\346\272\220\347\240\201\345\216\237\347\220\206/page/2/index.html" @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 源码原理 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

源码原理 + Category +

+
+ + +
+ 2020 +
+ + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/index.html" "b/en/categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/index.html" new file mode 100644 index 0000000000..d8c7b05923 --- /dev/null +++ "b/en/categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/index.html" @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 自己动手实现系列 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

自己动手实现系列 + Category +

+
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/page/2/index.html" "b/en/categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/page/2/index.html" new file mode 100644 index 0000000000..d78c381b66 --- /dev/null +++ "b/en/categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/page/2/index.html" @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 自己动手实现系列 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

自己动手实现系列 + Category +

+
+ + +
+ 2020 +
+ + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\350\207\252\346\210\221\346\217\220\345\215\207/index.html" "b/en/categories/\350\207\252\346\210\221\346\217\220\345\215\207/index.html" new file mode 100644 index 0000000000..f0bc395e08 --- /dev/null +++ "b/en/categories/\350\207\252\346\210\221\346\217\220\345\215\207/index.html" @@ -0,0 +1,656 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 自我提升 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

自我提升 + Category +

+
+ + +
+ 2023 +
+ + +
+ 2020 +
+ + + + + + +
+ 2019 +
+ + + + +
+ 2017 +
+ + +
+ 2016 +
+ + +
+ 2015 +
+ + +
+ 2013 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\344\272\272\347\224\237\346\200\235\350\200\203/index.html" "b/en/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\344\272\272\347\224\237\346\200\235\350\200\203/index.html" new file mode 100644 index 0000000000..6e38082c6d --- /dev/null +++ "b/en/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\344\272\272\347\224\237\346\200\235\350\200\203/index.html" @@ -0,0 +1,481 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 人生思考 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

人生思考 + Category +

+
+ + +
+ 2023 +
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\346\235\202\350\256\260\351\232\217\346\204\237/index.html" "b/en/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\346\235\202\350\256\260\351\232\217\346\204\237/index.html" new file mode 100644 index 0000000000..b3b6311f03 --- /dev/null +++ "b/en/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\346\235\202\350\256\260\351\232\217\346\204\237/index.html" @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 杂记随感 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

杂记随感 + Category +

+
+ + +
+ 2020 +
+ + + + +
+ 2019 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\350\257\273\344\271\246\347\254\224\350\256\260/index.html" "b/en/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\350\257\273\344\271\246\347\254\224\350\256\260/index.html" new file mode 100644 index 0000000000..3713cde68e --- /dev/null +++ "b/en/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\350\257\273\344\271\246\347\254\224\350\256\260/index.html" @@ -0,0 +1,527 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 读书笔记 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

读书笔记 + Category +

+
+ + +
+ 2017 +
+ + +
+ 2016 +
+ + +
+ 2015 +
+ + +
+ 2013 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\350\231\232\346\213\237\345\214\226/Docker-Compose/index.html" "b/en/categories/\350\231\232\346\213\237\345\214\226/Docker-Compose/index.html" new file mode 100644 index 0000000000..0a5ef61d4d --- /dev/null +++ "b/en/categories/\350\231\232\346\213\237\345\214\226/Docker-Compose/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Docker Compose | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Docker Compose + Category +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/en/categories/\350\231\232\346\213\237\345\214\226/index.html" "b/en/categories/\350\231\232\346\213\237\345\214\226/index.html" new file mode 100644 index 0000000000..807a316b09 --- /dev/null +++ "b/en/categories/\350\231\232\346\213\237\345\214\226/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 虚拟化 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

虚拟化 + Category +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/index.html b/en/index.html new file mode 100644 index 0000000000..1d106d0ce2 --- /dev/null +++ b/en/index.html @@ -0,0 +1,506 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

前言

最近各种技术博客,各种技术文章,各种牛人前辈的学习心得都在看,最近看了一个不错的文章叫做《如何正确使用时间》
感觉还是很不错的,特别适合年轻的程序员看一下。下面将全文贴在下方,如果需要也可以去原文查看。

+

原文

+

你是否时常会焦虑时间过的很快,没时间学习,本文将会分享一些个人的见解。

+

花时间补基础,读文档

在工作中我们时常会花很多时间去 debug,但是你是否发现很多问题最终只是你基础不扎实或者文档没有仔细看。

+

基础是你技术的基石,一定要花时间打好基础,而不是追各种新的技术。一旦你的基础扎实,学习各种新的技术也肯定不在话下,因为新的技术,究其根本都是相通的。

+

文档同样也是一门技术的基础。一个优秀的库,开发人员肯定已经把如何使用这个库都写在文档中了,仔细阅读文档一定会是少写 bug 的最省事路子。

+

学会搜索

如果你还在使用百度搜索编程问题,请尽快抛弃这个垃圾搜索引擎。同样一个关键字,使用百度和谷歌,谷歌基本完胜的。即使你使用中文在谷歌中搜索,得到的结果也往往是谷歌占优,所以如果你想迅速的通过搜索引擎来解决问题,那一定是谷歌。

+

学点英语

说到英语,一定是大家所最不想听的。其实我一直认为程序员学习英语是简单的,因为我们工作中是一直接触着英语,并且看懂技术文章,文档所需要的单词量是极少的。我时常在群里看到大家发出一个问题的截图问什么原因,其实在截图中英语已经很明白的说明了问题的所在,如果你的英语过关,完全不需要浪费时间来提问和搜索。所以我认为学点英语也是节省时间中很重要的一点。

+

那么如何去学习呢,chrome 装个翻译插件,直接拿英文文档或文章读,不会的就直接划词翻译,然后记录下这个单词并背诵。每天花半小时看点英文文档和文章,坚持两个月,你的英语水平不说别的,看文档和文章绝对不会有难题了。这一定是一个很划算的个人时间投资,花点时间学习英语,能为你将来的技术之路铺平很多坎。

+

画个图,想一想再做

你是否遇到过这种问题,需求一下来,看一眼,然后马上就按照设计稿开始做了,可能中间出个问题导致你需要返工。

+

如果你存在这样的问题,我很推荐在看到设计稿和需求的时候花点时间想一想,画一画。考虑一下设计稿中是否可以找到可以拆分出来的复用组件,是否存在之前写过的组件。该如何组织这个界面,数据的流转是怎么样的。然后画一下这个页面的需求,最后再动手做。

+

利用好下班时间学习

说到下班时间,那可能就有人说了公司很迟下班,这其实是国内很普遍的情况。但是我认为正常的加班是可以的,但是强制的加班就是在损耗你的身体和前途。

+

可以这么说,大部分的 996 公司,加班的这些时间并不会增加你的技术,无非就是在写一些重复的业务逻辑。也许你可以拿到更多的钱,但是代价是身体还有前途。程序员是靠技术吃饭的,如果你长久呆在一个长时间加班的公司,不能增长你的技术还要吞噬你的下班学习时间,那么你一定会废掉的。如果你遇到了这种情况,只能推荐尽快跳槽到非 996 的公司。

+

那么如果你有足够的下班时间,一定要花上 1, 2 小时去学习,上班大家基本都一样,技术的精进就是看下班以后的那几个小时了。如果你能利用好下班时间来学习,坚持下去,时间一定会给你很好的答复。

+

列好 ToDo

我喜欢规划好一段时间内要做的事情,并且要把事情拆分为小点。给 ToDo 列好优先级,紧急的优先级最高。相同优先级的我喜欢先做简单的,因为这样一旦完成就能划掉一个,提高成就感。

+

反思和整理

每周末都会花上点时间整理下本周记录的笔记和看到的不错文章。然后考虑下本周完成的工作和下周准备要完成的工作。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/10/index.html b/en/page/10/index.html new file mode 100644 index 0000000000..3cff73e429 --- /dev/null +++ b/en/page/10/index.html @@ -0,0 +1,492 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#/boot/grub/grub.conf 缺失:

yum install -y grub
grub-mkconfig -o /boot/grub/grub.conf

#/boot/grub2/grub.cfg 缺失:

yum install -y grub2
grub2-mkconfig -o /boot/grub2/grub.cfg

uname -a
sudo grep ^menuentry /boot/grub2/grub.cfg | awk -F\' '{print $2}' | nl -v0

sudo grub2-editenv list

sudo grep GRUB_DEFAULT /etc/default/grub

sudo vim /etc/default/grub

sudo grub2-set-default 1

sudo grub2-mkconfig -o /boot/grub2/grub.cfg

sudo dracut --force

sudo reboot

uname -a
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/100/index.html b/en/page/100/index.html new file mode 100644 index 0000000000..f1893a67dc --- /dev/null +++ b/en/page/100/index.html @@ -0,0 +1,534 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

深浅拷贝

1
2
3
4
5
6
let a = {
age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2
+

从上述例子中我们可以发现,如果给一个变量赋值一个对象,那么两者的值会是同一个引用,其中一方改变,另一方也会相应改变。
通常在开发中我们不希望出现这样的问题,我们可以使用浅拷贝来解决这个问题。

+

浅拷贝

首先可以通过 Object.assign 来解决这个问题。

+
1
2
3
4
5
6
7
8
// js代码

let a = {
age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // => 1
+

当然我们也可以通过展开运算符(…)来解决

+
1
2
3
4
5
6
let a = {
age: 1
}
let b = {...a}
a.age = 2
console.log(b.age) // => 1
+

我们还可以用很多简单的方法都能实现浅拷贝:

+
1
2
arr.slice();
arr.concat();
+ +

通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就需要使用到深拷贝了

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = {...a}
a.jobs.first = 'native'
console.log(b.jobs.first) // native
+

浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到刚开始的话题了,两者享有相同的引用。要解决这个问题,我们需要引入深拷贝。

+

深拷贝

这个问题通常可以通过 JSON.parse(JSON.stringify(object)) 来解决,这也是最好用最简单的方法,俗称乞丐版。
乞丐版

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
+

但是该方法也是有局限性的:

+
    +
  • 会忽略 undefined
  • +
  • 会忽略 symbol
  • +
  • 不能序列化函数
  • +
  • 不能解决循环引用的对象
  • +
+

举个栗子:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

let obj = {
a: 1,
b: {
c: 2,
d: 3,
},
}

obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c

let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj) // => Uncaught TypeError: Converting circular structure to JSON
+

如果你有这么一个循环引用对象,你会发现你不能通过该方法深拷贝
在遇到函数、 undefined 或者 symbol 的时候,该对象也不能正常的序列化

+
1
2
3
4
5
6
7
8
9
10
// js代码

let a = {
age: undefined,
sex: Symbol('fmale'),
jobs: function() {},
name: 'lixuguang'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // => {name: "lixuang"}
+ +

你会发现在上述情况中,该方法会忽略掉函数和 undefined
但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题,并且该函数是内置函数中处理深拷贝性能最快的。

+

那么是否可以解决函数和循环引用的问题呢?答案是肯定可以解决,接下来是基础版本的改造
基础版

+
1
2
3
4
5
6
7
8
9
10
11
function myClone(target){
if(typeof target === 'object'){ // 判断传入目标是否是object类型
let cloneTarget = {}; // 创建克隆对象
for(const key in target){ // 遍历目标对象
cloneTarget[key] = myClone(target[key]) // 递归调用 clone 方法
}
return cloneTarget;
} else {
return target // 如果不是 object 返回
}
}
+

写到这里已经可以帮助你应付一些面试官考察你的递归解决问题的能力。但是显然,这个深拷贝函数还是有一些问题。
这里只考虑了对象,没有考虑数组。
下面我们来做一个强化版的深拷贝,同时考虑对象、数组还有循环引用的问题。
强化版

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function myClone(target, map = new WeakMap()) { // WeakMap => 键对象弱引用, 可被垃圾回收
if(typeof target === 'object'){ // 判断是否是对象
let cloneTarget = Array.isArray(target) ? [] : {}; // 判断是是数组还是对象
if(map.get(target)) {
return target;
}

map.set(target, cloneTarget);

for(const key in target) {
cloneTarget[key] = myClone(target[key], map)
}
return cloneTarget;
} else {
return target;
}
}
+

当然如果你的数据中含有以上三种情况下,可以使用 lodash 的深拷贝函数。
如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// js代码

function structuralClone(obj) {
return new Promise(resolve => {
const {port1, port2} = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
});
}

var obj = {
a: 1,
b: {
c: b
}
}
// 注意该方法是异步的
// 可以处理 undefined 和循环引用对象
(async () => {
const clone = await structuralClone(obj)
})()
+ +

深拷贝实现方式2,可以深拷贝 functionsymbol,等等,堪称终极版
终极版

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// js代码

const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const argsTag = '[object Arguments]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];
function forEach(array, iteratee) {
let index = -1;
const length = array.length;
while (++index < length) {
iteratee(array[index], index);
}
return array;
}

function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}

function getType(target) {
return Object.prototype.toString.call(target);
}

function getInit(target) {
const Ctor = target.constructor;
return new Ctor();
}

function cloneSymbol(targe) {
return Object(Symbol.prototype.valueOf.call(targe));
}

function cloneReg(targe) {
const reFlags = /\w*$/;
const result = new targe.constructor(targe.source, reFlags.exec(targe));
result.lastIndex = targe.lastIndex;
return result;
}

function cloneFunction(func) {
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
if (func.prototype) {
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if (body) {
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(funcString);
}
}

function cloneOtherType(targe, type) {
const Ctor = targe.constructor;
switch (type) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(targe);
case regexpTag:
return cloneReg(targe);
case symbolTag:
return cloneSymbol(targe);
case funcTag:
return cloneFunction(targe);
default:
return null;
}
}

function clone(target, map = new WeakMap()) {
// 克隆原始类型
if (!isObject(target)) {
return target;
}

// 初始化
const type = getType(target);
let cloneTarget;
if (deepTag.includes(type)) {
cloneTarget = getInit(target, type);
} else {
return cloneOtherType(target, type);
}

// 防止循环引用
if (map.get(target)) {
return target;
}
map.set(target, cloneTarget);

// 克隆set
if (type === setTag) {
target.forEach(value => {
cloneTarget.add(clone(value));
});
return cloneTarget;
}

// 克隆map
if (type === mapTag) {
target.forEach((value, key) => {
cloneTarget.set(key, clone(value));
});
return cloneTarget;
}

// 克隆对象和数组
const keys = type === arrayTag ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone(target[key], map);
});

return cloneTarget;
}

// 调用方法
clone(target);
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/101/index.html b/en/page/101/index.html new file mode 100644 index 0000000000..839cf482b3 --- /dev/null +++ b/en/page/101/index.html @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

new 一个对象的过程

    +
  1. 新生成了一个对象
  2. +
  3. 链接到原型
  4. +
  5. 绑定 this
  6. +
  7. 返回新对象
  8. +
+

在调用 new 的过程中会发生以上四件事情,我们也可以试着来自己实现一个 new

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

function create() {
// 创建一个空的对象
let obj = new Object()
// 获得构造函数
let Con = [].shift.call(arguments)
// 链接到原型
obj.__proto__ = Con.prototype
// 绑定 this,执行构造函数
let result = Con.apply(obj, arguments)
// 确保 new 出来的是个对象
return typeof result === 'object' ? result : obj
}
+ +

对于实例对象来说,都是通过 new 产生的,无论是 function Foo() 还是 let a = { b : 1 }

+

对于创建一个对象来说,更推荐使用字面量的方式创建对象(无论性能上还是可读性)。因为你使用 new Object() 的方式创建对象需要通过作用域链一层层找到 Object,但是你使用字面量的方式就没这个问题。

+
1
2
3
4
// js代码

function Foo() {} // function 就是个语法糖,内部等同于 new Function()
let a = { b: 1 } // 这个字面量内部也是使用了 new Object()
+

对于 new 来说,还需要注意下运算符优先级。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

function Foo() {
return this;
}
Foo.getName = function () {
console.log('1');
};
Foo.prototype.getName = function () {
console.log('2');
};

new Foo.getName(); // -> 1
new Foo().getName(); // -> 2
+ +

从上图可以看出,new Foo() 的优先级大于 new Foo ,所以对于上述代码来说可以这样划分执行顺序

+
1
2
3
4
// js代码

new (Foo.getName());
(new Foo()).getName();
+ +
    +
  • 对于第一个函数来说,先执行了 Foo.getName() ,所以结果为 1;
  • +
  • 对于后者来说,先执行 new Foo() 产生了一个实例,然后通过原型链找到了 Foo 上的 getName 函数,所以结果为 2。
  • +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/102/index.html b/en/page/102/index.html new file mode 100644 index 0000000000..010f41dea2 --- /dev/null +++ b/en/page/102/index.html @@ -0,0 +1,513 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

闭包Closure

闭包的定义很简单:函数 A 返回了一个函数 B ,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。

+
1
2
3
4
5
6
7
8
9
// js代码

function A() {
let a = 1
function B() {
console.log(a)
}
return B
}
+

你是否会疑惑,为什么函数 A 已经弹出调用栈了,为什么函数 B 还能引用到函数 A 中的变量。因为函数 A 中的变量这时候是存储在堆上的。现在的 JS 引擎可以通过逃逸分析辨别出哪些变量需要存储在堆上,哪些需要存储在栈上。

+

经典面试题,循环中使用闭包解决 var 定义函数的问题

+
1
2
3
4
5
6
7
// js代码

for ( var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
+

首先因为 setTimeout 是个异步函数,所有会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。

+

解决办法两种,第一种使用闭包

+
1
2
3
4
5
6
7
8
9
// js代码

for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
+

第二种就是使用 setTimeout 的第三个参数(附加参数,传给 setTimeout 中的函数)

+
1
2
3
4
5
6
7
// js代码

for ( var i=1; i<=5; i++) {
setTimeout( function timer(j) {
console.log( j );
}, i*1000, i);
}
+ +

第三种就是使用 let 定义 i

+
1
2
3
4
5
6
7
// js代码

for ( let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
+

因为对于 let 来说,他会创建一个块级作用域,相当于

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// js代码

{ // 形成块级作用域
let i = 0
{
let ii = i
setTimeout( function timer() {
console.log( ii );
}, i*1000 );
}
i++
{
let ii = i
}
i++
{
let ii = i
}
...
}
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/103/index.html b/en/page/103/index.html new file mode 100644 index 0000000000..08ca0f074a --- /dev/null +++ b/en/page/103/index.html @@ -0,0 +1,523 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

执行上下文

当执行 JS 代码时,会产生三种执行上下文

+
    +
  • 全局执行上下文
  • +
  • 函数执行上下文
  • +
  • eval 执行上下文
  • +
+

每个执行上下文中都有三个重要的属性

+
    +
  • 变量对象( VO ),包含变量、函数声明和函数的形参,该属性只能在全局上下文中访问
  • +
  • 作用域链( JS 采用词法作用域,也就是说变量的作用域是在定义时就决定了)
  • +
  • this
    1
    2
    3
    4
    5
    6
    7
    // js代码

    var a = 10
    function foo(i) {
    var b = 20
    }
    foo()
    +对于上述代码,执行栈中有两个上下文:全局上下文和函数 foo 上下文。
    1
    2
    3
    4
    5
    6
    // js代码

    stack = [
    globalContext,
    fooContext
    ]
    +对于全局上下文来说, VO 大概是这样的
    1
    2
    3
    4
    5
    6
    7
    // js代码

    globalContext.VO === globe
    globalContext.VO = {
    a: undefined,
    foo: <Function>,
    }
    +对于函数 foo 来说, VO 不能访问,只能访问到活动对象( AO
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // js代码

    fooContext.VO === foo.AO
    fooContext.AO {
    i: undefined,
    b: undefined,
    arguments: <>
    }
    // arguments 是函数独有的对象(箭头函数没有)
    // 该对象是一个伪数组,有 `length` 属性且可以通过下标访问元素
    // 该对象中的 `callee` 属性代表函数本身
    // `caller` 属性代表函数的调用者
    +对于作用域链,可以把它理解成包含自身变量对象和上级变量对象的列表,通过 [[Scope]] 属性查找上级变量
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    fooContext.[[Scope]] = [
    globalContext.VO
    ]
    fooContext.Scope = fooContext.[[Scope]] + fooContext.VO
    fooContext.Scope = [
    fooContext.VO,
    globalContext.VO
    ]
    +接下来让我们看一个老生常谈的例子, var
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    b() // call b
    console.log(a) // undefined

    var a = 'Hello world'

    function b() {
    console.log('call b')
    }
    +想必以上的输出大家肯定都已经明白了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行上下文时,会有两个阶段。第一个阶段是创建的阶段(具体步骤是创建 VO ), JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined ,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用。
  • +
+

在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

b() // call b second

function b() {
console.log('call b fist')
}
function b() {
console.log('call b second')
}
var b = 'Hello world'
+

var 会产生很多错误,所以在 ES6 中引入了 letlet 不能在声明前使用,但是这并不是常说的 let 不会提升, let 提升了声明但没有赋值,因为临时死区导致了并不能在声明前使用。

+

对于非匿名的立即执行函数需要注意以下一点

+
1
2
3
4
5
6
7
// js代码

var foo = 1
(function foo() {
foo = 10
console.log(foo)
}()) // -> ƒ foo() { foo = 10 ; console.log(foo) }
+

因为当 JS 解释器在遇到非匿名的立即执行函数时,会创建一个辅助的特定对象,然后将函数名称作为这个对象的属性,因此函数内部才可以访问到 foo ,但是这个值又是只读的,所以对它的赋值并不生效,所以打印的结果还是这个函数,并且外部的值也没有发生更改。

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

specialObject = {};

Scope = specialObject + Scope;

foo = new FunctionExpression;
foo.[[Scope]] = Scope;
specialObject.foo = foo; // {DontDelete}, {ReadOnly}

delete Scope[0]; // remove specialObject from the front of scope chain
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/104/index.html b/en/page/104/index.html new file mode 100644 index 0000000000..4363cbd170 --- /dev/null +++ b/en/page/104/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

this

this 是很多人会混淆的概念,但是其实他一点都不难,你只需要记住几个规则就可以了。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// js代码

function foo() {
console.log(this.a)
}
var a = 1
foo()

var obj = {
a: 2,
foo: foo
}
obj.foo()

// 以上两者情况 `this` 只依赖于调用函数前的对象,优先级是第二个情况大于第一个情况

// 以下情况是优先级最高的,`this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向
var c = new foo()
c.a = 3
console.log(c.a)

// 还有种就是利用 `call` , `apply` , `bind` 改变 `this` ,这个优先级仅次于 `new`
+

以上几种情况明白了,很多代码中的 this 应该就没什么问题了,下面让我们看看箭头函数中的 this

+
1
2
3
4
5
6
7
8
9
10
// js代码

function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())
+

箭头函数其实是没有 this 的,这个函数中的 this 只取决于他外面的第一个不是箭头函数的函数的 this 。在这个例子中,因为调用 a 符合前面代码中的第一个情况,所以 thiswindow。并且 this 一旦绑定了上下文,就不会被任何代码改变。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/105/index.html b/en/page/105/index.html new file mode 100644 index 0000000000..c10600e46f --- /dev/null +++ b/en/page/105/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

instanceof

instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype
举例:

+
1
a instanceof Object
+

判断 Objectprototype 是否在 a 的原型链上。

+

我们也可以试着实现一下 instanceof

+
1
2
3
4
5
6
7
8
9
10
11
12
function myInstanceof(left, right) { // left 表示左表达式,right 表示右表达式
let prototype = right.prototype // 获得类型的原型
left = left.__proto__ // 获得对象的原型

while (true) { // 判断对象的类型是否等于类型的原型
if (left === null)
return false
if (prototype === left)
return true
left = left.__proto__
}
}
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/106/index.html b/en/page/106/index.html new file mode 100644 index 0000000000..ef808f0aca --- /dev/null +++ b/en/page/106/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

还记得我19年总结最下面的那张照片么,那个同事送的的本子,对,就是下面这张图,这本子叫做《One In A Million》中文叫做极少数手册,是一个管理时间用的管理手册,英文如果直译的话应该是叫做万里挑一,实话说我并不是很自信,虽然我觉得自己还算可以,但远远达不到万里挑一的程度,所以,我打算好好利用这个本子,让自己尽可能成为百里挑一,千里挑一,最后万里挑一的人。

+

本子里有一个栏目叫做21天主题挑战,21-Day Challenge,每个月21天都会给你列出一些主题,越往后列出来的主题越少,更多的需要个人去补充,我想去挑战这些内容,让自己的生活变得有目标,让自己变得更好。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/107/index.html b/en/page/107/index.html new file mode 100644 index 0000000000..bb5697ceac --- /dev/null +++ b/en/page/107/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

原型

yuanxing.png
每个函数都有 prototype 属性,除了 Function.prototype.bind(),该属性指向原型对象,简称原型prototype原型对象里的constructor指向构造函数本身。
每个对象都有 __proto__ 属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 _proto_ 来访问。
对象可以通过 __proto__ 来寻找不属于该对象的属性,__proto__ 将对象连接起来组成了原型链

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/108/index.html b/en/page/108/index.html new file mode 100644 index 0000000000..6dd0a8e075 --- /dev/null +++ b/en/page/108/index.html @@ -0,0 +1,510 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

+

温故而知新,可以为师矣。
———————— 论语

+
+

这篇文章主要是把我看过的书中的知识点,还有其他人整理的面试题进行整理,巩固自己的前端开发理论知识,希望其他看到这篇文章的人也能有所帮助。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/109/index.html b/en/page/109/index.html new file mode 100644 index 0000000000..b2c8ccf57a --- /dev/null +++ b/en/page/109/index.html @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

2020元旦伊始

时间过的真快,19年睡了一觉就成了过去的一年,今天是元旦,早上8:00起了床,简单洗漱后我出了门,新年新气象,去年没有坚持的晨跑,今年想要坚持一下,所以一早就出门跑了会儿步,太久没活动了,只是跑了半个小时就气喘吁吁,只好回家,但是即使是这半个小时,我感觉呼吸了新鲜的空气,整个人也精神了不少,回到家坐在阳台上,拿出小王宁送我的极少数手册开始研究了起来,生活还是应该有点仪式感何况是新年伊始,手册中有很多建议,全部看过一遍以后我郑重的拿起了笔写下了我今天的行动计划。但是没有写年度计划,明天上班后跟领导还要谈一谈,谈过之后再定一下年度的计划。今天我的计划就是陪家人过好元旦这一天。

+

元旦执行计划

    +
  1. 写一篇日志
  2. +
  3. 将家人这一天游玩的过程以Vlog的形式记录下来,上传到西瓜视频上。
  4. +
  5. 陪家人逛街,给桐桐买新衣裳。
  6. +
+

执行计划

吃过早饭后大概到了10:30,一家人收拾好以后终于可以出门了,今天有点风,一点点冷不是特别冷,空气还不错,天气听晴朗的,一家人开着车听着歌前往甘井子万达广场,20分钟左右我们就到了万达广场,首先我们先去看了桐桐心心念念的小猫咪,也就是猫咪咖啡馆里的那些小可爱,桐桐可能是受妈妈影响,很喜欢猫咪,但是又有一点点怕。大概看了10分钟的猫咪,我们继续下一站,上四楼给桐桐买新衣裳,桐桐看到满眼的漂亮衣裳很是兴奋,最终她挑选了一套带着向日葵花朵的淡蓝色衣裙,穿上漂亮的新衣裳,平时像个假小子似的桐桐也一下子害羞了起来,让妈妈和奶奶看完后还会让爸爸和爷爷看看,得到了大家的肯定以后,心满意足的买下了它,也不知道是不是试衣服试累了,桐桐嚷嚷着要吃饭,桐桐妈说有一家港式茶餐厅味道不错,之前和同事来过,于是我们就去了4楼的茶餐厅,说实话这是我头一次吃茶餐厅,我还跟我爸妈调侃说,这也算是我们来过一次香港了。去到餐厅时才11点多一点,客还没有上满,还有座位,我们找了个靠里的位置坐了下来,打开菜谱,桐桐妈挑了几样她觉得不错的菜,上菜时间稍微有点慢,期间桐桐有些不耐烦,坐也坐不住,终于等到了吃的上来,可是觉得大多数吃的都太偏甜,爸妈和我都吃不太惯,桐桐吃的还不错,大概吃了半个小时就吃完了,出门的时候门口已经排起了长队,还好来的早一点,不然吃饭都要等了。本打算带着桐桐回家睡个觉,下午要到姥爷家串门,可桐桐说没有逛够,正巧这时看到了一楼有小丑在折气球玩具,我们就下到一楼找小丑玩了,小丑给桐桐折了个贵宾犬,小丑是荣耀手机的员工,可能是元旦策划的活动吧,反正来了就来了,进去看看也好,看了新出的V30手机,感觉并不是很喜欢,店员说有旧机抵值的活动,我问了他我的7p128g还能值多少,他打了几个电话后回复我1400块,哎,三年前7000块买的手机,现在只值1400了,算了还是用着吧,出了荣耀手机店又去了旁边的华为,看了看新出的mate30pro,这个手机是真的不错,当然手机不错价格也美丽,6500+的价格真的是让我不舍得买,还是把钱留下来给桐桐吧。桐桐终于累了,嚷嚷着要走,于是一家人驱车去了姥姥家。

+

姥姥家在西南路,因为平时工作忙很少带桐桐去,这次去桐桐又不认识他们了,开始的时候很拘束,也不说话,一直到吃饭结束才终于愿意开口问好唱歌,因为家里没有电视机,到了姥姥家看到电视机很兴奋,一直在看,今天也算是过了瘾了,亲人就是即使很长时间不见面,但一见面就特别亲的人,饭从5:30一直吃到8:30,聊了工作,聊了生活,聊了困惑,聊了坦然,我终于知道了长辈们的智慧,了解了身体健康的重要性,20年一定要锻炼出一副好的身体,对家人负责。

+

9:30回到了家,心里挺高兴的,新年第一天陪家人一起度过了充实的一天,回到家把今天路上录下来的视频剪成了短视频上传到了西瓜视频,这也是我今年的一个想法,记录我们一家人的幸福生活,如果有粉丝有点播的话还能赚点钱,没有的话也记录了桐桐的成长,算是一点纪念吧。最后写下了这篇日记,写完之后我就要睡觉去了,明天一早就要起床,明天起我就要走路去上班了,为了有一副好身体,加油。

+

今天的目标都完成了,很开心~~

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/11/index.html b/en/page/11/index.html new file mode 100644 index 0000000000..680a207e47 --- /dev/null +++ b/en/page/11/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

搭建Docker私有仓库

利用registry搭建私有仓库

+

下载registry

1
docker pull registry
+ +

配置

1
2
/etc/docker/doemon.json
# 配置 'insecure-registry'
+

重启docker

1
systemctl restart docker
+ +

创建registry容器(关联私有仓库配置)

1
docker run -d -p 5000:5000 --name registry registry:latest
+ +

推送镜像到私有仓

    +
  • 备份镜像(172.16.12.134:5000 私有仓地址)

    +
    1
    docker tag ubuntu:latest 172.16.12.134:5000/my_ubuntu
  • +
  • 推送

    +
    1
    docker push 172.16.12.134:5000/my_ubuntu
  • +
  • 下载

    +
    1
    docker pull 172.16.12.134:5000/my_ubuntu
  • +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/110/index.html b/en/page/110/index.html new file mode 100644 index 0000000000..e519af9737 --- /dev/null +++ b/en/page/110/index.html @@ -0,0 +1,532 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

不知不觉又是一年,今天是2019年的最后一天,也是我30岁的最后一天,回顾这一年有收获有遗憾。翻出了18年的总结,看到了18年底对于自己19年的规划,现在回过头来看看哪些完成了。

+

18年底定的计划

学习技术

1. 深入学习客户端开发(全年)

18年客户端的项目也做了几个,Electron的开发能力经验更多了,之前只是用html+css+js做前端页面的开发,今年又增加了跟vue框架的结合,成功在多个项目中实践,只是一直没有形成规范感觉还是少了一环,原来一直在犹豫Electron到底算不算前端的技术栈之一,后来也算是想明白了,大前端范畴,只要是面向用户的都算是前端该做的范畴,所以Electron当然也算是前端应该掌握的技术栈,20年会继续学习客户端技术,把公司相关规范制定和落地。

+

2. 学习前端自动化测试相关知识(2019年3月前)

18年关注了前端自动化测试相关的知识,本打算19年好好学习一下,在公司项目中实践,但是今年前后端工作量大增,前端人员的精力有限,加之成本的考虑,并没有开展起来,不过这方面的技能储备已经具备了,年底两次关于前端自动化的测试也将前端自动化的思想普及到了公司前端的团队里,20年是公司强调质量的一年,相信20年前端自动化测试将有机会在公司一些项目中落地

+

3. 学习并掌握TS (2019年5月前)

18年就看到了TS将会成为一门比较活的开发语言,目前来说已经具备了做TS开发的技能,只是TS更适合大型的长期的项目,将更好的保障项目的代码质量,快速迭代和小型项目并不是很适合。前端团队里没有开花,没想到居然在开发团队里见到了曙光,感谢开发的小伙伴愿意接受这门前端新技术,相信有一个点采用后看到了优势,后续普及工作将会更好开展。

+

4. 学习并掌握React(2019年7月前)

18年Vue已经在公司前端全面普及开来,但是市场上React仍然是占据更多的份额,而且RN还能做App开发,所以还是一门必须要掌握的技术,今年看了一本React相关的书,也看了一些相关的视频教程,还参加了公司同事开展的React分享,虽然没有在项目中实际使用过,但是理论知识已经掌握了,可以进行小的Demo开发,20年要用React写一个开源的项目,做到学有所用,学以致用。

+

5. 学习前端持续集成的相关知识(2019年9月前)

19年公司强调了代码必须走线上构建的要求,前端工程化和自动构建持续集成的工作可以说是工作所迫,这方面的技能已经Get到了,另外令我感到高兴的是,通过对Github-Actions的配置,我的博客可以像提交代码一样提交文章了,而且也实践了自动构建的相关技术,很开心。

+

6. 学习Docker虚拟化技术( 2019年10月前)

这个任务没有达成,原本是希望能够通过Docker统一前端开发环境,不过Docker的适用场景还是以部署环境为主,Docker今年貌似热度也降下来了,今年K8s越来越火,不管怎样,持续关注新技术。

+

整理计划

1. 养成写博客的习惯,至少2天一篇,可以是技术博客,也可以是其他。

今年实在是太忙了,别说两天一篇文章,就连一个月一篇的量都没能达成,不过在年底我终于把博客又重新激活了,希望自己20年能够把博客坚持写下来,很简单,积累自己,打造个人品牌。

+

2. 将常用的方法和功能做成插件,开源给公司使用

今年4月开始,更多的精力都投在了部门的前端管理工作,说实话也迷茫也难过,这份迷茫终于在19年年底解开了,没能在技能层面有更高的提升,以及在代码层面给公司带来价值我觉得还是有些遗憾,20年希望自己能够在技术上有更大的进步,技术深度上/广度上/架构层面/后端技能都能有长足的进步。

+

读书计划

1. 每周读完一本书,并写一篇读后感

2019年读了25本书,每读完一本都会写一篇读后感,虽然没能保证每周读一本书,但是基本保证了每两周读完一本书,算是完成了50%的目标吧。20年继续努力,读书使人聪明,因为不聪明所以更要多读书。

+

部门前端计划

加强各设计组前端之间的交流

+

设计组的出现导致前端交流不畅,2019年加强与各设计组前端的交流,掌握各位前端的技术能力。

+
+

没想到一语成第,设计组居然在4月份将前端整个拿出来构成了一个前端组,由我来带,更没想到的是居然在19年年底又解散到项目团队了,这9个月的时间可以说有苦有泪也有甜,前端小伙伴们为了同一个目标,共同努力,尽最大的努力完成公司的业务,可以说为了这个目标,这一年我付出了很多很多,在这方面愧对了家人孩子,虽然最后前端还是拆分了,可以说我能力有限,但是我心里可以说是无愧,希望尽到项目团队里的小伙伴们也能开心工作,迅速成长,虽然不再在一个团队里,但是,我仍是你们的光妈。

+

前端俱乐部推动

+

继续每周2小时的兴趣交流,交流形式不限于ppt,要求各自建立技术博客做笔记。

+
+

俱乐部的活动最后还是没能坚持下来,大家工作比较忙也好,我自己的原因也好,确实没能坚持下来,20年还是希望能够将俱乐部活动组织起来,算是大家交流的机会也好,帮助大家开阔眼界也好,20年一定要再做起来!

+

进行梯队划分建设

+

前端今年人数明显增多,但各层级职责并没有区分,19年抽时间进行梯队建设。

+
+

19年前端从14人增加到了34人,可以说人数上翻翻了,随着负责人制的执行,梯队建设的工作算是完成了50%,但是能力提升的工作可以说还是做的不到位,应该有机会成长为高工的小伙伴没能升上去,这我有一定的责任,20年虽然团队不再,但是希望你们都坚持努力学习,迈向更高的台阶。

+

引入前端工程化工具和思想

+

目前前端工程化已经非常成熟,希望能将成熟的技术和工具引入。

+
+

19年,随着Vue的普及和熟练,模块化开发组件化开发的思想已经深入到每一个前端的心中,现在大家都知道该如何使用这些技术和工具,希望大家能不仅仅局限在用也能了解了解其中原理,至少对常用的配置有所了解。

+

提升整体前端开发的能力

+

目前整体前端开发能力还相对较弱,19年通过培训等方式提升整体前端开发的能力

+
+

19年能力提升的工作做的太少了,没什么好说的,希望20年这方面工作能够投入更多的精力,让更多的小伙伴成长起来,让我们一起加油吧。

+

生活目标

每天陪孩子读书一小时

跟上面的工作和个人目标比起来,这个任务可以说完成度0%,这也是我觉得愧对家人的地方,为了工作,对家人的关注少了很多很多,以至于桐桐晚上都不愿意跟我一起睡觉,20年我要好好对她,做个好的爸爸,孩子明年就三岁了,去到幼儿园会认识更多的朋友,希望她会跟她的小伙伴说,我有个爱我的爸爸。

+

减肥

减肥这件事可以说是真的对我来说太难了,看着自己马上破三位数(kg)的体重,心里无限难过,每次媳妇儿问我啥时候去健身,我都感到像被闪电击中了似的,办了健身卡被人嘲笑成了洗澡卡,20年,减不到150斤我就。。。。

+

写在最后

19年太多的遗憾,太多的后悔莫及,当然也有一些成绩和自我肯定,19年已经过去了,希望在20年,能够成为一个更优秀的人。
WechatIMG6.jpeg
感谢我可爱的同事,年底收到了礼物真的很开心。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/111/index.html b/en/page/111/index.html new file mode 100644 index 0000000000..2ec7acae61 --- /dev/null +++ b/en/page/111/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

公司于去年开始代码版本管理从SVN迁移到了Git,采用的GitLab程序做管理,但是自动构建采用的是Jekins,最近在给博客做自动构建的时候了解到的了Github的Actions,我就在想,是不是GitLab也有自己的自动构建能力,因为之前在参与项目的时候也看到GitLab上有CI/CD相关的内容,但是没有仔细的去看,趁这次机会就一起研究一下。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/112/index.html b/en/page/112/index.html new file mode 100644 index 0000000000..fff5c19e91 --- /dev/null +++ b/en/page/112/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近被公司新来的前端问到了一些微信页面的兼容性问题,因为我做微信开发的经验比较少,被问到了真是感到很尴尬,所以在搜到了解决方案告诉了他之后,我又深入的再网上找了一些微信开发中遇到的兼容问题,特此整理,以留后用。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/113/index.html b/en/page/113/index.html new file mode 100644 index 0000000000..79e484b851 --- /dev/null +++ b/en/page/113/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近愈发觉得越想往上走,越不能局限在前端开发的领域,再往上走前后端都是通的,最近在知乎上看到一句话,说是到了架构师甚至专家的层面以后,就没有什么前端架构师或者前端专家和后端架构师、后端专家了,只有前端出身的技术专家,和后端出身的技术专家,越往上越注重人的综合能力,当一个人能够解决一个项目的技术选型、架构等工作后,对于公司或者团队来说,那就是个巨大的宝藏。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/114/index.html b/en/page/114/index.html new file mode 100644 index 0000000000..0f049d099c --- /dev/null +++ b/en/page/114/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

忙活了很久很久,终于不那么忙了,也终于想起来我还有个博客没有运行,哎,虽然用Hexo可以享受Github提供的免费的空间和域名,但是命令行敲代码的方式实现博客编写还是有些不方便,尤其是需要编程环境的时候,最近公司在用CICD的方式对代码进行构建,我就在想有没有一种方式让博客也可以变成这样,于是我就在网上搜Github Hexo 自动构建 这些个词,还真让我搜到了一种方法而且还是Github自己提供的,下面隆重请出 GitHub Actions

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/115/index.html b/en/page/115/index.html new file mode 100644 index 0000000000..5a52282800 --- /dev/null +++ b/en/page/115/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

公司这两年兴起了前后端分离的热潮,在选用三大技术框架时,vue凭借其学习曲线低深受公司程序员的喜爱,因此我们也就走上了vue学习的不归路,在招聘招揽小伙伴时有那么一些问题经常被问起,vue组件间通信就是其中之一,下面我们来聊一聊vue组件通信有哪些方式。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/116/index.html b/en/page/116/index.html new file mode 100644 index 0000000000..528f4d4a35 --- /dev/null +++ b/en/page/116/index.html @@ -0,0 +1,554 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

指令表

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
指令含义备注
git add .提示增加文件.代表所有
git commit -m“说明内容” 提交到本地服务器
git status显示修改信息
git pull从网络服务器拉 更新最新版本
git push上传最新版本
git branch查看当前分支
git checkout develop切换到develop模式
git merge master从master合并过来
git push origin develop提交
git clone git@192.168.2.10:bat-web.git从服务器克隆
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/117/index.html b/en/page/117/index.html new file mode 100644 index 0000000000..47ade9914a --- /dev/null +++ b/en/page/117/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

近日工作不是那么忙,所以有时间看看书,这本书之前就已经读过一遍,隔了一段时间已经有所忘记,所以再次拾起,重新回顾一下。.

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/118/index.html b/en/page/118/index.html new file mode 100644 index 0000000000..3020791492 --- /dev/null +++ b/en/page/118/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

VSCode是新一代IDE,方便灵活的插件安装方式让它强大无比,对前端友好,让它成为了越来越多前端开发者的首选IDE,编辑器虽好,但是要自己安装符合自己需求的插件才行,下面介绍一款插件,名叫ESLint,是用来静态检测js代码的工具,让你的js代码在编辑过程中就能找到错误,提高代码质量,好了废话不多说下面介绍如何安装和配置这款插件。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/119/index.html b/en/page/119/index.html new file mode 100644 index 0000000000..9dddc5a6e4 --- /dev/null +++ b/en/page/119/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

上篇文章介绍了如何利用github+hexo+next制作一个博客系统,但是由于这个博客系统是基于静态页面的,所以无法完成评论功能,博客怎么可以没有评论功能呢,当然next主题提供了几个选项,但是国内你知道的,提供的几个评论功能都不是太好用,于是就上网找了好多资料,功夫不负有心人,让我找到了一个名曰gitment的基于github-issue的评论插件,下面就介绍大家如何使用这个插件。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/12/index.html b/en/page/12/index.html new file mode 100644 index 0000000000..fb57b013b2 --- /dev/null +++ b/en/page/12/index.html @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

这里假设要从Linux服务器A中把文件 file.txt 传输到Linux服务器B,该怎么做,这里就可以用scp实现
为了能够成功的传输文件,需要做到以下几点:

+
    +
  1. A需要知道要传输的的文件本地的路径
  2. +
  3. A为了往B传输文件,所以需要知道B的访问权限,因此需要B的访问地址,账号和私钥
  4. +
  5. 目标路径
  6. +
  7. 因为Linux权限管理的要求,私钥的访问权限需要设定为600
  8. +
+

当然还有一点前提是A和B两台服务器之间本身是可以通信的
知道以上信息便可以进行文件传输

+

实践

现在假设

+
    +
  1. A要往B传输的文件在A的如下目录:/tmp/transfer/file.txt
  2. +
  3. B的访问地址是 192.168.31.31 访问用户名是 testuser 用到的key上传到了A的如下目录: /tmp/ssh/test.key
  4. +
  5. 目标路径为B的如下目录: /tmp/test
    知道上面信息后我们来创建命令。
  6. +
+
1
2
chomd 600 /tmp/ssh/test.key
scp -i /tmp/ssh/test.key /tmp/transfer/file.txt testuser@192.168.31.31 /tmp/test
+ +

通过上面的命令即可实现从A服务器传输文件到B服务器了。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/120/index.html b/en/page/120/index.html new file mode 100644 index 0000000000..4816ec20c4 --- /dev/null +++ b/en/page/120/index.html @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

+

开博客貌似并不是一件难事,现在有很多的方式开博客,而且现在博客貌似也不再流行,但是作为一个开发者,维护一个技术博客对自己的发展是很有好处的,出名者如阮一峰等,所以还是想要做一个技术博客的。

+
+
+

开博客可以选择网上现有的博客系统如博客园以及其他各大公司提供的博客系统,也可以选择如wordpress等博客程序自行搭建博客系统,前者省事,但可自定义设置不足不符合一个爱折腾的人的性格,后者复杂些,爱折腾的可以学着搭建,但是服务器域名是个让人头疼的事,还有数据库等操作,着实麻烦,如今又有了另外一个选择,那就是利用GitHub和一些静态的博客系统搭建一个纯静态的博客系统,不花钱又能折腾,而且还是命令行下的,装B感觉棒棒的,另外搭配时下流行的MarkDown语法,做笔记正合我意。

+
+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/121/index.html b/en/page/121/index.html new file mode 100644 index 0000000000..e02c736928 --- /dev/null +++ b/en/page/121/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近公司技术选型选择了VUE,下面就记录一下在学习Vue中遇到的一些知识点,有些在官方手册里已经讲的很详细了,这里就简单带过,主要对cli产生的以及自行添加的一些项目目录进行一下解释,希望对正在学习Vue的同学提供一点帮助,如果本文有谬误之处还请各位指出,谢谢各位支持。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/122/index.html b/en/page/122/index.html new file mode 100644 index 0000000000..831d54dc95 --- /dev/null +++ b/en/page/122/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

现代化的IDE已经把开发者变得越来越懒,但是我觉得这样挺好的,让工具完成手工反复重复的工作,提高工作效率的同时降低开发者的疲劳感,Vue VSCode Snippets就是这样一个VSC插件,它可以用简单的几个字母就敲出一整块代码片段,在学习和做VUE项目时可以极大地提高工作效率,下面就介绍一下插件的常见命令。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/123/index.html b/en/page/123/index.html new file mode 100644 index 0000000000..8e885fd1e4 --- /dev/null +++ b/en/page/123/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

近两年前端突然呈爆发增长之势,node的出现起了重要的作用,而node之中有一个重要的功能就是npm包管理工具,下面就记录一下我学习npm之中遇到的一些问题及解决方案,仅供各位参考。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/124/index.html b/en/page/124/index.html new file mode 100644 index 0000000000..e5feed7bf6 --- /dev/null +++ b/en/page/124/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

作为前端,浏览器就是你的战场,而不同的浏览器就是不同的敌人,敌人有相同之处,也各有特点,这不同的特点就是不同浏览器的兼容性,下面就聊聊浏览器的兼容问题,如何解决不同浏览器的兼容性。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/125/index.html b/en/page/125/index.html new file mode 100644 index 0000000000..5d7b696bb9 --- /dev/null +++ b/en/page/125/index.html @@ -0,0 +1,604 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

昨天花了1天的时间读了《晨间日记的奇迹》,感觉整个人的认识发生了一些变化,下面把重要内容做一下摘抄总结。

+

早上写日记的好处 —— 提升效率

    +
  • 可以做好一天的准备 — 计划性
  • +
  • 可以正确的写出昨天发生的事 — 效率性&忠诚性
  • +
  • 可以中立的看待昨天 — 中立性
  • +
  • 相对自由的时间 — 持续性
  • +
  • 总结经验 — 活用性
  • +
+

注意事项

日记 不等于 日志
日记 要记录 时间 天气 心情 — 因为这些有助于回忆和记录
不要投入过长时间 — 3分钟 — 日记私密性
晨间日记前7天只写好事 — 反省固然重要,但是成长更重要,持续性很重要 — 巴普洛夫的狗

+

晨间日记2部分

Part1

客观记录已经发生的事(昨天)— 经验智慧

+

Part2

    +
  • 今天应做的事 — 具体行动(来自昨天的总结
  • +
  • 今天一定要做的事(紧急重要的事)— 变得更积极更主动
  • +
  • 未来要做的事 — 不紧急但重要的事
  • +
  • 连用日记 — 历史上的今天(过去一年同一天的事)
  • +
+

夜晚日记 VS 晨间日记

受当天情绪影响 — 更冷静

+

梦想成真表

+ + + + + + + + + + + + + + + + + +
过去未来
事实IQ 智慧指数NQ 人际关系指数
感情EQ 情感指数DQ 梦想指数
+
    +
  • IQ — 晨间日记冷静回忆分析 — 提高决策正确性 IQ up
  • +
  • EQ — 冷静记录昨日的情绪/自省 — 变积极 EQ up
  • +
  • NQ — 总结昨天给今天设立“对/为XXX做某事” 很重要 NQ up
  • +
  • DQ — 不能只有眼前的事 每天都要确定一下自己的梦想 脸皮厚点没关系 重复或只有一句话也没关系 铭记于心最重要
  • +
+

“忙碌”只是借口!“决心”才是问题! 对未来充满期待的人才有行动力

+

如何早起

    +
  • 设定起床音乐/用气味(精油)刺激起床/光线-电动窗帘早起自动拉开
  • +
  • 早起后淋浴 — 配合音乐/冥想/呐喊 — 让自己充满动力保持清醒
  • +
+

写日记的五大好处

    +
  • 提升写作能力
  • +
  • 谈话题材源源不断
  • +
  • 提高贵人运
  • +
  • 返现自我肉体和精神的状态与模式
  • +
  • 在自己身上挖宝,彻底改变人生
  • +
+

记录的日记要常拿出来看看

记录日记时 — 问问自己的灵魂 然后写下真心话 — 灵魂日记
六度空间理论

+

七种成功者的习惯

    +
  • 习惯之一:积极主动别指望谁能推你走 — 主动行动,迈出第一步
  • +
  • 习惯之二:以终为始忠于自己的人生计划。— 设立人生目标并坚持
  • +
  • 习惯之三:要事第一选择当前该做的事
  • +
  • 习惯之四:追求双赢远离角斗场
  • +
  • 习惯之五:善于沟通换位思考的原则
  • +
  • 习惯之六:统合综效 1+1可以大于2
  • +
  • 习惯之七:不断更新全方位平衡自我
  • +
+

早睡是为了身体,早起是为了我们的内心。— sugiponn

+

晨间日记的格式

晨间日记必须要设立的栏目 — 工作/金钱/健康/人际关系
要记下当日的日期/天气/温度/湿度

+

纬度标签
工作方面:

+
    +
  • 能力开发/经营/管理/进货/工作人员/学习/资格/活动/企划案/想法/推广
    金钱方面:
  • +
  • 收入/指出/购入/股票/资产/储蓄/家用
    健康方面:
  • +
  • 饮食/运动/性/减肥/身体状况/锻炼身体/体重/脂肪
    人际关系方面:
  • +
  • 父母/家人/小孩/亲戚/情人/工作伙伴/朋友/熟人/联络事项/人脉网络
    兴趣方面以及其他:
  • +
  • 义工活动/阅读/运动/打扮/电影/音乐/游戏/喜欢的物品/今天必须要注意的事/美容/开心的事/购物
  • +
+

劳动 — 职业 — 工作 — 乐趣

+

三大原则和七大作战守则

    +
  • 原则1:时间不超过3分钟 — 减少养成习惯的成本

    +
  • +
  • 原则2:决定好写晨间日记的地方 — 为了养成习惯

    +
  • +
  • 原则3:只写一个字也没关系 — 不要有压力

    +
  • +
  • 作战守则1:开心写日记 — 反省固然重要,鼓励更有作用 — 开始写晨间日记前七天只写好事

    +
  • +
  • 作战守则2:ONE-TWO作战 — 把动作分布容易养成习惯 — 形成固定动作

    +
  • +
  • 作战守则3:巴普洛夫的狗 — 开始写日记时候要有一个仪式 — 心理暗示

    +
  • +
  • 作战守则4:奖励自己 — 当习惯养成并坚持到一定时间,给自己一个奖励

    +
  • +
  • 作战守则5:宣告天下 — 借由他人的鼓励或压力形成动力/也可找到同志

    +
  • +
  • 作战守则6:一千日作战法 — 不管什么样的习惯坚持1000日就会成为永远的习惯 — 具体做法为记录 第XXX篇 晨间日记

    +
  • +
  • 作战守则7:为了别人 — 写晨间日记不只是为了自己,而且更是为了别人,身边人的幸福最终会影响到自己

    +
  • +
+

应该先肯定自己,给自己打100分

    +
  • “总之,先… 病” 没有目标/没有梦想 — 回顾时一定会后悔!
  • +
  • “假如…,应该 病” 无法满足/没有动力 — 焦躁不安
  • +
  • 共同点:失落感/空虚/
  • +
+

解决办法— 设立一个情境

例:我要建立一个幸福的家庭 —> 因此我要创造很多快乐的回忆 —> 为了制造很多快乐的回忆 —> 我必须要到很多的地方去看看 —> 为了去很多地方看看,我觉得有辆车会更方便 —> 买车 —> 考驾照

+

不要总想自己哪里欠缺,要想自己想要得到什么,并且为了目标努力,你就会变的更完美。
拥有一个敢于大声喊出的梦想,不要羞涩,要为了梦想一步步的前进,最终达到梦想。

+

提到的另外的书

《培育梦想种子》《日记的力量》《成功人士的七个习惯》

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/126/index.html b/en/page/126/index.html new file mode 100644 index 0000000000..86aaeeaf05 --- /dev/null +++ b/en/page/126/index.html @@ -0,0 +1,696 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近读了《让老板提拔你》,下面将书中觉得有用的内容进行摘抄,放在下方。

+

定位自己

正确认识自己,确定社会定位、职业定位。 定位-决定-定价

+

要素

核心竞争力职位 契合度 是高薪关键所在

+

契合度

    +
  • 技能、专长、经历与职位要求的契合度
  • +
  • 专业资质和等级与职位要求的契合度
  • +
  • 综合素质与职位要求的契合度

    七大秘诀

  • +
  • 了解同行业薪酬的平均水平
  • +
  • 赢得未来单位的心
  • +
  • 先让对方开口
  • +
  • 勇敢地开口要求
  • +
  • 不要轻言放弃
  • +
  • 把握时机很重要
  • +
  • 说实话,别撒谎

    如何谈薪资

  • +
  • 商洽薪酬的关键在于把握好谈话时机,切勿开门见山。
    +

    只要有发展机会,我愿意接受贵单位的薪酬标准,不知道按规定这个工作岗位的薪酬标准是多少。

    +
    +
  • +
+

将知识卖个好价钱

推销自己既推销自己所掌握的知识即智慧,正确认识自己的价值是关键。

+

高薪是因为“物有所值”

    +
  • 用业绩、用能力说话,是人才坦然面对高薪的心态。
  • +
  • 高薪人才的求职方式 —- 圈内朋友引见、定向投简历、行业论坛、交流活动等。
  • +
  • 高级人才不能经常跳槽要保持相对的职业稳定。—- 在一家公司少则3年多则6年 —- 接受挑战,把握机会。
  • +
+

失败丰富走向成功经验

强调在失败中吸取的经验,在未来中可以避免的损失。

+

能为企业带来丰厚的利润才是人才

企业招聘员工的唯一目的:追求公司业绩最大化、利润最大化。

+

高质高效工作全攻略

    +
  • 进行正确的自我评价
  • +
  • 做最擅长做的事
      +
    • 三个经济原则 —- 发挥人才优势。
        +
      1. 比较利益原则—–自己的专长对自己才是最有利的,勿与他人作比较。
      2. +
      3. 机会成本原则—–选择一份工作放弃其他工作就是机会成本。
      4. +
      5. 效率原则——–工作的成功不在于时间有多长,而在于成效有多少,附加值有多少。
      6. +
      +
    • +
    +
  • +
  • 马上行动
  • +
  • 做事情要雷厉风行 ——— 成功属于踏实肯干的人
  • +
  • 有条不紊地开展工作 ——- 制定时间计划
  • +
  • 善于利用现代办公工具
  • +
  • 给自己最大的工作空间
  • +
  • 建立高效有序的办公环境
  • +
  • 不要忘记最初想去的方向
  • +
  • “聪明”的向上级提出建议
  • +
  • 专心做事,避免浮躁
  • +
  • 多而不专,一事难成
  • +
  • “专”才有高质工作——–人才主要指专才–某个领域的高精尖人才–勿做全才,一专多能
  • +
  • 做事要有条理
  • +
+

不要只把自己当成一个打工仔

+

要把工作当成事业

+
+
    +
  • 工作不仅仅是为了钱
  • +
  • 对工作要有明确的价值取向
      +
    1. 认清人生的方向
    2. +
    3. 开始学会醉卧探索和认知
    4. +
    5. 认清工作价值与成就的关系
    6. +
    7. 长期的工作规划
    8. +
    9. 在生命的天平上衡量自身的价值
    10. +
    +
  • +
  • 巧妙应对与上司看法向左时的三条准则
      +
    1. 遇事考虑全局
    2. +
    3. 辩证地看待问题
    4. +
    5. 切记感情用事
    6. +
    +
  • +
  • 把单位的事当成自家的事
  • +
  • 认真负责地用心工作
  • +
  • 珍惜岗位,热爱自己的职业
  • +
  • 永远是在为自己工作
  • +
  • 敬重自己的工作
  • +
  • 不要轻视薪水微薄的工作
  • +
  • 永远对工作充满激情
  • +
  • 以自己的工作为荣
  • +
  • 不要被他人的观点所束缚
  • +
  • 暂时的胜负并不会决定人生的最后走向
  • +
  • 将弱势转化为优势
  • +
  • 全力以赴做好每一天的工作
  • +
  • 和优秀的人士在一起—见贤思齐、借梯爬楼
      +
    • 如何争取跟优秀的人在一起
        +
      1. 不断的抛头露面
      2. +
      3. 帮助可以帮助自己成就事业的人做事
      4. +
      5. 与上司和比自己优秀的人士一起合作
      6. +
      +
    • +
    +
  • +
+
    +
  1. 尊重对方,严谨有致
  2. +
  3. 切记奉承,要不卑不亢
  4. +
  5. 态度自然,不必拘谨
  6. +
  7. 陪衬得当,不可狂妄
  8. +
  9. 主动真诚,做出姿态
  10. +
  11. 求助求教,接受呵护
  12. +
+
    +
  • 挑战自我,承担责任
      +
    • 三条忠告
        +
      1. 全心全意工作
      2. +
      3. 把自己视为合伙人
      4. +
      5. 迎接变革的需求
      6. +
      +
    • +
    +
  • +
  • 自信独立,不随波逐流
  • +
  • 敢于显示自己很重要
  • +
  • 千万不能只知道抱怨上司
  • +
  • 保持严谨认真的做事习惯
  • +
  • 自主地做好手中的工作
  • +
  • 踏踏实实地做好本职工作
  • +
  • 丢掉工作散漫的坏习惯
  • +
  • 不要让浮躁的性格困扰自己
  • +
  • 不推诿,勇于承担责任
  • +
  • 无论如何都不要拖延工作
  • +
  • 糊弄工作只能是在糊弄自己
  • +
  • 逊色的工作只会淘汰自己
  • +
  • 千万别丢掉“得宠”之资
  • +
  • “一步登天”只会摔疼自己
  • +
  • 别让“差不多”贻误了自己
  • +
  • 能完成100%,就决不做99%
  • +
+

与上司相处

    +
  • 不要做上司的“心腹”
  • +
  • 适时恰当的赞美上司
      +
    • 赞美上司,还要善于选择适当的场合
    • +
    • 赞美上司,要学会巧借公众语言称赞
    • +
    • 赞美上司,还要善于赞美不得志的上司
    • +
    +
  • +
  • 主动与领导沟通
  • +
  • 主动和上司保持联系
  • +
  • 用“心机”主动接近上司
      +
    • 尽可能详细的了解上司
    • +
    • 选择一个与领导尽可能近的位置
    • +
    • 赢得上司青睐的方法
    • +
    +
  • +
  • 更有效的和上司沟通
      +
    • 与上司沟通要简洁
    • +
    • 与上司沟通要大度大气大方
    • +
    • 与上司沟通,就要把自己先放一边 ——– 说话对事不对人
    • +
    +
  • +
  • 四种和上司进行沟通的方法
      +
    1. 开诚布公式———提出问题,说出自己的理解认识,给出解决方案,寻求领导意思,执行领导的命令
    2. +
    3. 先斩后奏式———因主客观原因导致的没有先汇报而是先解决的事,事后要将问题的分析方法,解决方法,实施方案以及结果做一份详尽的总结报告上交给领导
    4. +
    5. “含情脉脉”式——-通过交流一些工作上的问题来含蓄表达出个人的建议、需要
    6. +
    7. 巧设比喻式———通过典型案例来暗示自己的意见和要求
    8. +
    +
  • +
  • 把话说到上司的心坎上
  • +
  • 巧妙地为领导拾遗补缺
      +
    1. 诠释领导讲话的难点
    2. +
    3. 强调领导的才干
    4. +
    5. 化严肃为幽默
    6. +
    7. 稳定情绪,委婉暗示
    8. +
    +
  • +
  • 工作中勤于请示汇报
      +
    1. 听懂上司的意图
    2. +
    3. 探讨、磨合,达成共识
    4. +
    5. 制定尽可能详尽的工作计划
    6. +
    7. 随时向上司汇报任务的关键点
    8. +
    9. 总结汇报
    10. +
    +
  • +
  • 用成功赢得上司的信任
  • +
  • 工作中不要冲撞上司
  • +
  • 处理好同上司之间的分歧
      +
    1. 圆融协调——领导不懂,下达了错误的指令
        +
      1. 私下向上司陈述意见,帮助上司做出正确的决策
      2. +
      3. 如果上司采纳了下属的建议或意见,该下属有责任长期保守秘密
      4. +
      5. 如果上司固执己见,那么无条件服从
      6. +
      +
    2. +
    3. 装聋作哑——不涉及到原则问题
    4. +
    5. 棘手难题多权衡
        +
      1. 立刻插话纠正
      2. +
      3. 提醒上司
      4. +
      5. 暗示
      6. +
      7. 事后补救
      8. +
      9. 事后提醒
      10. +
      +
    6. +
    +
  • +
  • 正确对待上司的批评
  • +
  • 要善于服从自己的上司
  • +
  • 正确化解来自上司的压力
  • +
+

写在最后

博观约取,多读书读好书,丰富自己,变得睿智。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/127/index.html b/en/page/127/index.html new file mode 100644 index 0000000000..9a666d071b --- /dev/null +++ b/en/page/127/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

利用CSS的border属性可以生成一些图形,例如三角或是圆角。纯粹的CSS2的内容,没有兼容性的问题,我之前在纯CSS实现各类气球泡泡对话框效果一文中算是比较详细的讲述了CSS border属性生成三角的原理,以及实例。我觉得此技术相当实用的,故本文再次简单叙述一下,另外,本文还将展示可能并不为众人所知的CSS border圆角生成技术。好了,裹脚布的话就不说了,直接进入正题。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/128/index.html b/en/page/128/index.html new file mode 100644 index 0000000000..0bbcb36b91 --- /dev/null +++ b/en/page/128/index.html @@ -0,0 +1,526 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近读了《高性能JAVASCRIPT》,下面将书中觉得有用的内容进行摘抄,放在下方。

+

如何从小事提升JAVASCRIPT性能。

    +
  1. <script>标签写在</body>之前——将脚本放在底部。

    +
  2. +
  3. 尽量少的<script>标签,减少页面中的外链数量,减少请求次数。

    +
  4. +
  5. 使用打包工具如:Yahoo!combo handler

    +
  6. +
  7. 使用动态延迟加载技术如:LazyLoad类库,LABjs

    +
    1
    2
    3
    4
    5
    6
    7
    // js代码
    <script type="text/javascript" src="lazyload-min.js"></script>
    <script type="text/javascript">
    LazyLoad.js([],function(){
    Application.init();
    })
    </script>
  8. +
  9. 在javascript中存储位置十分重要,尽量用局部变量代替对象成员的访问。

    +
  10. +
  11. 集合变数组提高查询效率

    +
    1
    2
    3
    4
    5
    6
    7
    // js代码
    function toArray(coll){
    for(var i = 0, a=[], len=coll.length; i<len; i++){
    a[i]=col[i];
    }
    return a;
    }
  12. +
  13. 使用局部变量缓存访问多次的成员
    当便利一个集合时,首要优化原则是把集合存储在局部变量中,并把length缓存在循环外部,然后使用局部变量访问这些需要多次访问的元素。

    +
  14. +
  15. 使用原生DOM方法querySelectorAll()遍历查找元素。

    +
  16. +
  17. 让元素脱离动画流,页面重排次数越多,程序响应越慢,要尽量减少重拍
    方法:

    +
  18. +
    1. +
    2. 使用绝对定位使元素脱离文档流
    3. +
    +
  19. +
  20. IE:hover
    在大量元素使用:hover这个伪css选择器时,程序会降低响应速率,在大表格或列表的情况下

    +
  21. +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/13/index.html b/en/page/13/index.html new file mode 100644 index 0000000000..bb3cda5fe4 --- /dev/null +++ b/en/page/13/index.html @@ -0,0 +1,582 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

nginx中的超时设置

Nginx主要有四类超时设置:客户端超时设置DNS解析超时设置代理超时设置,如果使用ngx_lua,则还有lua相关的超时设置。

+

客户端超时设置

对于客户端超时主要设置有读取请求头超时时间读取请求体超时时间发送响应超时时间长连接超时时间。通过客户端超时设置避免客户端恶意或者网络状况不佳造成连接长期占用,影响服务端的可处理的能力。

+

client_header_timeout 指定等待client发送一个请求头的超时时间

    +
  • 语法: client_header_timeout time
  • +
  • 默认值: 60s
  • +
  • 上下文: http server
  • +
  • 说明: 指定等待client发送一个请求头的超时时间(例如:GET / HTTP/1.1).仅当在一次read中,没有收到请求头,才会算成超时。如果在超时时间内,client没发送任何东西,nginx返回HTTP状态码408(“Request timed out”)
  • +
+

client_body_timeout 该指令设置请求体(request body)的读超时时间

    +
  • 语法: client_body_timeout time
  • +
  • 默认值: 60s
  • +
  • 上下文: http server location
  • +
  • 说明: 该指令设置请求体(request body)的读超时时间。仅当在一次readstep中,没有得到请求体,就会设为超时。超时后,nginx返回HTTP状态码408(“Request timed out”)
  • +
+

send_timeout time

设置发送响应到客户端的超时时间,默认为60s,此超时时间指的也是两次成功写操作间隔时间,而不是发送整个响应的超时时间。如果在此超时时间内客户端没有接收任何响应,则Nginx关闭此连接。

+

keepalive_timeout 第一个参数指定了与client的keep-alive连接超时时间

    +
  • 语法: keepalive_timeout timeout [ header_timeout ]

    +
  • +
  • 默认值: 75s

    +
  • +
  • 上下文: http server location

    +
  • +
  • 说明: 第一个参数指定了与client的keep-alive连接超时时间。服务器将会在这个时间后关闭连接。可选的第二个参数指定了在响应头Keep-Alive: timeout=time中的time值。这个头能够让一些浏览器主动关闭连接,这样服务器就不必要去关闭连接了。没有这个参数,nginx不会发送Keep-Alive响应头(尽管并不是由这个头来决定连接是否“keep-alive”)
    两个参数的值可并不相同

    +
      +
    • 注意不同浏览器怎么处理“keep-alive”头
    • +
    • MSIE和Opera忽略掉”Keep-Alive: timeout=“ header.
    • +
    • MSIE保持连接大约60-65秒,然后发送TCP RST
    • +
    • Opera永久保持长连接
    • +
    • Mozilla keeps the connection alive for N plus about 1-10 seconds.
    • +
    • Konqueror保持长连接N秒
    • +
    +
  • +
+

DNS解析超时设置

resolver_timeout 设置DNS解析超时时间

    +
  • 语法 resolver_timeout time
  • +
  • 默认值 30s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置DNS解析超时时间
  • +
+

代理超时设置

proxy_connect_timeout 设置与upstream server的连接超时时间,不能超过75秒

    +
  • 语法 proxy_connect_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置与upstream server的连接超时时间,有必要记住,这个超时不能超过75秒。
    这个不是等待后端返回页面的时间,那是由proxy_read_timeout声明的。如果你的upstream服务器起来了,但是hanging住了(例如,没有足够的线程处理请求,所以把你的请求放到请求池里稍后处理),那么这个声明是没有用的,由于与upstream服务器的连接已经建立了。
  • +
+

proxy_read_timeout 设置与代理服务器的读超时时间

    +
  • 语法 proxy_read_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置与代理服务器的读超时时间。它决定了nginx会等待多长时间来获得请求的响应。这个时间不是获得整个response的时间,而是两次reading操作的时间。
  • +
+

proxy_send_timeout 设置发送请求给upstream服务器的超时时间

    +
  • 语法 proxy_send_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 这个指定设置了发送请求给upstream服务器的超时时间。超时设置不是为了整个发送期间,而是在两次write操作期间。如果超时后,upstream没有收到新的数据,nginx会关闭连接
  • +
+

失败重试机制设置。

proxy_next_upstream error | timeout | invalid_header | http_500 | http_502 | http_503 | http_504 |http_403 | http_404 | non_idempotent | off …:配置什么情况下需要请求下一台上游服务器进行重试。默认为“errortimeout”。error表示与上游服务器建立连接、写请求或者读响应头出错。timeout表示与上游服务器建立连接、写请求或者读响应头超时。invalid_header表示上游服务器返回空的或错误的响应头。http_XXX表示上游服务器返回特定的状态码。non_idempotent表示RFC-2616定义的非幂等HTTP方法(POST、LOCK、PATCH),也可以在失败后重试下一台上游服务器(即默认幂等方法GET、HEAD、PUT、DELETE、OPTIONS、TRACE才可以重试)。off表示禁用重试。

+

重试不能无限制进行,因此,需要如下两个指令控制重试次数和重试超时时间。

+

proxy_next_upstream_tries number:设置重试次数,默认0表示不限制,注意此重试次数指的是所有请求次数(包括第一次和之后的重试次数之和)。

+

proxy_next_upstream_timeout time:设置重试最大超时时间,默认0表示不限制。

+

即在proxy_next_upstream_timeout时间内允许proxy_next_upstream_tries次重试。如果超过了其中一个设置,则Nginx也会结束重试并返回客户端响应(可能是错误码)。

+

如下配置表示当error/timeout时重试upstream中的下一台上游服务器,如果重试的总时间超出了6s或者重试了1次,则表示重试失败(因为之前已经请求一次了,所以还能重试一次),Nginx结束重试并返回客户端响应。

+

upstream存活超时设置

max_fails和fail_timeout:配置什么时候Nginx将上游服务器认定为不可用/不存活。当上游服务器在fail_timeout时间内失败了max_fails次,则认为该上游服务器不可用/不存活。并在接下来的fail_timeout时间内从upstream摘掉该节点(即请求不会转发到该上游服务器)。

+

什么情况下被认定为失败呢?其由 proxy_next_upstream定义,不过,不管 proxy_next_upstream如何配置,error, timeout and invalid_header 都将被认为是失败。

+

如server 192.168.61.1:9090max_fails=2 fail_timeout=10s;表示在10s内如果失败了2次,则在接下来的10s内认定该节点不可用/不存活。这种存活检测机制是只有当访问该上游服务器时,采取惰性检查,可以使用ngx_http_upstream_check_module配置主动检查。

+

max_fails设置为0表示不检查服务器是否可用(即认为一直可用),如果upstream中仅剩一台上游服务器时,则该服务器是不会被摘除的,将从不被认为不可用。

+

proxy_upstream_fail_timeout(fail_timeout)设置某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间

    +
  • 语法 server address [fail_timeout=30s]
  • +
  • 默认值 10s
  • +
  • 上下文 upstream
  • +
  • 说明 Upstream模块下 server指令的参数,设置了某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间,默认为10秒
  • +
+

lingering_timeout

    +
  • 语法: lingering_timeout time
  • +
  • 默认值: 5s
  • +
  • 上下文: http server location
  • +
  • 说明: lingering_close生效后,在关闭连接前,会检测是否有用户发送的数据到达服务器,如果超过lingering_timeout时间后还没有数据可读,就直接关闭连接;否则,必须在读取完连接缓冲区上的数据并丢弃掉后才会关闭连接。
  • +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/14/index.html b/en/page/14/index.html new file mode 100644 index 0000000000..a4d5707391 --- /dev/null +++ b/en/page/14/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

前端常见知识点整理 —- 网络安全(2)

SQL 注入

SQL 注入漏洞(SQL Injection)是 Web 开发中最常见的一种安全漏洞。可以用它来从数据库获取敏感信息,或者利用数据库的特性执行添加用户,导出文件等一系列恶意操作,甚至有可能获取数据库乃至系统用户最高权限。

+

而造成 SQL 注入的原因是因为程序没有有效的转义过滤用户的输入,使攻击者成功的向服务器提交恶意的 SQL 查询代码,程序在接收后错误的将攻击者的输入作为查询语句的一部分执行,导致原始的查询逻辑被改变,额外的执行了攻击者精心构造的恶意代码。

+

很多 Web 开发者没有意识到 SQL 查询是可以被篡改的,从而把 SQL 查询当作可信任的命令。殊不知,SQL 查询是可以绕开访问控制,从而绕过身份验证和权限检查的。更有甚者,有可能通过 SQL 查询去运行主机系统级的命令。

+

SQL 注入原理

下面将通过一些真实的例子来详细讲解 SQL 注入的方式的原理。

+

考虑以下简单的管理员登录表单:

+
1
2
3
4
5
<form action="/login" method="POST">
<p>Username: <input type="text" name="username" /></p>
<p>Password: <input type="password" name="password" /></p>
<p><input type="submit" value="登陆" /></p>
</form>
+ +

后端的 SQL 语句可能是如下这样的:

+
1
2
3
4
5
6
7
let querySQL = `
SELECT *
FROM user
WHERE username='${username}'
AND psw='${password}'
`;
// 接下来就是执行 sql 语句...
+ +

目的就是来验证用户名和密码是不是正确,按理说乍一看上面的 SQL 语句也没什么毛病,确实是能够达到我们的目的,可是你只是站在用户会老老实实按照你的设计来输入的角度来看问题,如果有一个恶意攻击者输入的用户名是 zoumiaojiang' OR 1 = 1 --,密码随意输入,就可以直接登入系统了。WFT!

+

冷静下来思考一下,我们之前预想的真实 SQL 语句是:

+
1
SELECT * FROM user WHERE username='zoumiaojiang' AND psw='mypassword'
+ +

可以恶意攻击者的奇怪用户名将你的 SQL 语句变成了如下形式:

+
1
2
SELECT * FROM user WHERE username='zoumiaojiang' OR 1 = 1 --' AND psw='xxxx'

+ +

在 SQL 中,-- 是注释后面的内容的意思,所以查询语句就变成了:

+
1
2
SELECT * FROM user WHERE username='zoumiaojiang' OR 1 = 1

+ +

这条 SQL 语句的查询条件永远为真,所以意思就是恶意攻击者不用我的密码,就可以登录进我的账号,然后可以在里面为所欲为,然而这还只是最简单的注入,牛逼的 SQL 注入高手甚至可以通过 SQL 查询去运行主机系统级的命令,将你主机里的内容一览无余,这里我也没有这个能力讲解的太深入,毕竟不是专业研究这类攻击的,但是通过以上的例子,已经了解了 SQL 注入的原理,我们基本已经能找到防御 SQL 注入的方案了。

+

如何预防 SQL 注入

防止 SQL 注入主要是不能允许用户输入的内容影响正常的 SQL 语句的逻辑,当用户的输入的信息将要用来拼接 SQL 语句的话,我们应该永远选择不相信,任何内容都必须进行转义过滤,当然做到这个还是不够的,下面列出防御 SQL 注入的几点注意事项:

+
    +
  • 严格限制Web应用的数据库的操作权限,给此用户提供仅仅能够满足其工作的最低权限,从而最大限度的减少注入攻击对数据库的危害

    +
  • +
  • 后端代码检查输入的数据是否符合预期,严格限制变量的类型,例如使用正则表达式进行一些匹配处理。

    +
  • +
  • 对进入数据库的特殊字符(',",\,<,>,&,*,; 等)进行转义处理,或编码转换。基本上所有的后端语言都有对字符串进行转义处理的方法,比如 lodash 的 lodash._escapehtmlchar 库。

    +
  • +
  • 所有的查询语句建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中,即不要直接拼接 SQL 语句。例如 Node.js 中的 mysqljs 库的 query 方法中的 ? 占位参数。

    +
  • +
+
1
mysql.query(`SELECT * FROM user WHERE username = ? AND psw = ?`, [username, psw]);
+ +
    +
  • 在应用发布之前建议使用专业的 SQL 注入检测工具进行检测,以及时修补被发现的 SQL 注入漏洞。网上有很多这方面的开源工具,例如 sqlmap、SQLninja 等。

    +
  • +
  • 避免网站打印出 SQL 错误信息,比如类型错误、字段不匹配等,把代码里的 SQL 语句暴露出来,以防止攻击者利用这些错误信息进行 SQL 注入。

    +
  • +
  • 不要过于细化返回的错误信息,如果目的是方便调试,就去使用后端日志,不要在接口上过多的暴露出错信息,毕竟真正的用户不关心太多的技术细节,只要话术合理就行。

    +
  • +
+

碰到要操作的数据库的代码,一定要慎重,小心使得万年船,多找几个人多来几次 code review,将问题都暴露出来,而且要善于利用工具,操作数据库相关的代码属于机密,没事不要去各种论坛晒自家站点的 SQL 语句,万一被人盯上了呢?

+

命令行注入

命令行注入漏洞,指的是攻击者能够通过 HTTP 请求直接侵入主机,执行攻击者预设的 shell 命令,听起来好像匪夷所思,这往往是 Web 开发者最容易忽视但是却是最危险的一个漏洞之一,看一个实例:

+

假如现在需要实现一个需求:用户提交一些内容到服务器,然后在服务器执行一些系统命令去产出一个结果返回给用户,接口的部分实现如下:

+
1
2
3
4
5
// 以 Node.js 为例,假如在接口中需要从 github 下载用户指定的 repo
const exec = require('mz/child_process').exec;
let params = {/* 用户输入的参数 */};

exec(`git clone ${params.repo} /some/path`);
+ +

这段代码确实能够满足业务需求,正常的用户也确实能从指定的 git repo 上下载到想要的代码,可是和 SQL 注入一样,这段代码在恶意攻击者眼中,简直就是香饽饽。

+

如果 params.repo 传入的是 https://github.com/zoumiaojiang/zoumiaojiang.github.io.git 当然没问题了。
可是如果 params.repo 传入的是 https://github.com/xx/xx.git && rm -rf /* && 恰好你的服务是用 root 权限起的就惨了。

+

具体恶意攻击者能用命令行注入干什么也像 SQL 注入一样,手法是千变万化的,比如「反弹 shell 注入」等,但原理都是一样的,我们绝对有能力防止命令行注入发生。防止命令行注入需要做到以下几件事情:

+
    +
  • 后端对前端提交内容需要完全选择不相信,并且对其进行规则限制(比如正则表达式)。
  • +
  • 在调用系统命令前对所有传入参数进行命令行参数转义过滤。
  • +
  • 不要直接拼接命令语句,借助一些工具做拼接、转义预处理,例如 Node.js 的 shell-escape npm 包。
  • +
+

还是前面的例子,我们可以做到如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const exec = require('mz/child_process').exec;

// 借助 shell-escape npm 包解决参数转义过滤问题
const shellescape = require('shell-escape');

let params = {/* 用户输入的参数 */};

// 先过滤一下参数,让参数符合预期
if (!/正确的表达式/.test(params.repo)) {
return;
}

let cmd = shellescape([
'git',
'clone',
params.repo,
'/some/path'
]);

// cmd 的值: git clone 'https://github.com/xx/xx.git && rm -rf / &&' /some/path
// 这样就不会被注入成功了。
exec(cmd);
+

无论是在何种后端语言环境中,凡是涉及到代码调用系统 shell 命令的时候都一定要谨慎。

+

DDoS 攻击

DDoS 又叫分布式拒绝服务,全称 Distributed Denial of Service,其原理就是利用大量的请求造成资源过载,导致服务不可用,这个攻击应该不能算是安全问题,这应该算是一个另类的存在,因为这种攻击根本就是耍流氓的存在,「伤敌一千,自损八百」的行为。出于保护 Web App 不受攻击的攻防角度,还是介绍一下 DDoS 攻击吧,毕竟也是挺常见的。

+

DDoS 攻击可以理解为:「你开了一家店,隔壁家点看不惯,就雇了一大堆黑社会人员进你店里干坐着,也不消费,其他客人也进不来,导致你营业惨淡」。为啥说 DDoS 是个「伤敌一千,自损八百」的行为呢?毕竟隔壁店还是花了不少钱雇黑社会但是啥也没得到不是?DDoS 攻击的目的基本上就以下几个:

+
    +
  • 深仇大恨,就是要干死你
  • +
  • 敲诈你,不给钱就干你
  • +
  • 忽悠你,不买我防火墙服务就会有“人”继续干你
  • +
+

也许你的站点遭受过 DDoS 攻击,具体什么原因怎么解读见仁见智。DDos 攻击从层次上可分为网络层攻击与应用层攻击,从攻击手法上可分为快型流量攻击与慢型流量攻击,但其原理都是造成资源过载,导致服务不可用。

+

网络层 DDoS

网络层 DDos 攻击包括 SYN FloodACK FloodUDP FloodICMP Flood 等。

+

SYN Flood 攻击

SYN flood 攻击主要利用了 TCP 三次握手过程中的 Bug,我们都知道 TCP 三次握手过程是要建立连接的双方发送 SYN,SYN + ACK,ACK 数据包,而当攻击方随意构造源 IP 去发送 SYN 包时,服务器返回的 SYN + ACK 就不能得到应答(因为 IP 是随意构造的),此时服务器就会尝试重新发送,并且会有至少 30s 的等待时间,导致资源饱和服务不可用,此攻击属于慢型 DDoS 攻击。

+

ACK Flood 攻击

ACK Flood 攻击是在 TCP 连接建立之后,所有的数据传输 TCP 报文都是带有 ACK 标志位的,主机在接收到一个带有 ACK 标志位的数据包的时候,需要检查该数据包所表示的连接四元组是否存在,如果存在则检查该数据包所表示的状态是否合法,然后再向应用层传递该数据包。如果在检查中发现该数据包不合法,例如该数据包所指向的目的端口在本机并未开放,则主机操作系统协议栈会回应 RST 包告诉对方此端口不存在。

+

UDP Flood 攻击

UDP flood 攻击是由于 UDP 是一种无连接的协议,因此攻击者可以伪造大量的源 IP 地址去发送 UDP 包,此种攻击属于大流量攻击。正常应用情况下,UDP 包双向流量会基本相等,因此发起这种攻击的攻击者在消耗对方资源的时候也在消耗自己的资源。

+

ICMP Flood 攻击

ICMP Flood 攻击属于大流量攻击,其原理就是不断发送不正常的 ICMP 包(所谓不正常就是 ICMP 包内容很大),导致目标带宽被占用,但其本身资源也会被消耗。目前很多服务器都是禁 ping 的(在防火墙在可以屏蔽 ICMP 包),因此这种攻击方式已经落伍。

+

网络层 DDoS 防御

网络层的 DDoS 攻击究其本质其实是无法防御的,我们能做得就是不断优化服务本身部署的网络架构,以及提升网络带宽。当然,还是做好以下几件事也是有助于缓解网络层 DDoS 攻击的冲击:

+
    +
  • 网络架构上做好优化,采用负载均衡分流。
  • +
  • 确保服务器的系统文件是最新的版本,并及时更新系统补丁。
  • +
  • 添加抗 DDos 设备,进行流量清洗。
  • +
  • 限制同时打开的 SYN 半连接数目,缩短 SYN 半连接的 Timeout 时间。
  • +
  • 限制单 IP 请求频率。
  • +
  • 防火墙等防护设置禁止 ICMP 包等。
  • +
  • 严格限制对外开放的服务器的向外访问。
  • +
  • 运行端口映射程序或端口扫描程序,要认真检查特权端口和非特权端口。
  • +
  • 关闭不必要的服务。
  • +
  • 认真检查网络设备和主机/服务器系统的日志。只要日志出现漏洞或是时间变更,那这台机器就可能遭到了攻击。
  • +
  • 限制在防火墙外与网络文件共享。这样会给黑客截取系统文件的机会,主机的信息暴露给黑客,无疑是给了对方入侵的机会。
  • +
  • 加钱堆机器。。
  • +
  • 报警。。-
  • +
+

应用层 DDoS

应用层 DDoS 攻击不是发生在网络层,是发生在 TCP 建立握手成功之后,应用程序处理请求的时候,现在很多常见的 DDoS 攻击都是应用层攻击。应用层攻击千变万化,目的就是在网络应用层耗尽你的带宽,下面列出集中典型的攻击类型。

+

CC 攻击

当时绿盟为了防御 DDoS 攻击研发了一款叫做 Collapasar 的产品,能够有效的防御 SYN Flood 攻击。黑客为了挑衅,研发了一款 Challenge Collapasar 攻击工具(简称 CC)。

+

CC 攻击的原理,就是针对消耗资源比较大的页面不断发起不正常的请求,导致资源耗尽。因此在发送 CC 攻击前,我们需要寻找加载比较慢,消耗资源比较多的网页,比如需要查询数据库的页面、读写硬盘文件的等。通过 CC 攻击,使用爬虫对某些加载需要消耗大量资源的页面发起 HTTP 请求。

+

DNS Flood

DNS Flood 攻击采用的方法是向被攻击的服务器发送大量的域名解析请求,通常请求解析的域名是随机生成或者是网络世界上根本不存在的域名,被攻击的DNS 服务器在接收到域名解析请求的时候首先会在服务器上查找是否有对应的缓存,如果查找不到并且该域名无法直接由服务器解析的时候,DNS 服务器会向其上层 DNS 服务器递归查询域名信息。域名解析的过程给服务器带来了很大的负载,每秒钟域名解析请求超过一定的数量就会造成 DNS 服务器解析域名超时。

+

根据微软的统计数据,一台 DNS 服务器所能承受的动态域名查询的上限是每秒钟 9000 个请求。而我们知道,在一台 P3 的 PC 机上可以轻易地构造出每秒钟几万个域名解析请求,足以使一台硬件配置极高的 DNS 服务器瘫痪,由此可见 DNS 服务器的脆弱性。

+

HTTP 慢速连接攻击

针对 HTTP 协议,先建立起 HTTP 连接,设置一个较大的 Conetnt-Length,每次只发送很少的字节,让服务器一直以为 HTTP 头部没有传输完成,这样连接一多就很快会出现连接耗尽。

+

应用层 DDoS 防御

    +
  • 判断 User-Agent 字段(不可靠,因为可以随意构造)
  • +
  • 针对 IP + cookie,限制访问频率(由于 cookie 可以更改,IP 可以使用代理,或者肉鸡,也不可靠)
  • +
  • 关闭服务器最大连接数等,合理配置中间件,缓解 DDoS 攻击。
  • +
  • 请求中添加验证码,比如请求中有数据库操作的时候。
  • +
  • 编写代码时,尽量实现优化,并合理使用缓存技术,减少数据库的读取操作。
  • +
  • 加钱堆机器。。
  • +
  • 报警。。
  • +
+

应用层的防御有时比网络层的更难,因为导致应用层被 DDoS 攻击的因素非常多,有时往往是因为程序员的失误,导致某个页面加载需要消耗大量资源,有时是因为中间件配置不当等等。而应用层 DDoS 防御的核心就是区分人与机器(爬虫),因为大量的请求不可能是人为的,肯定是机器构造的。因此如果能有效的区分人与爬虫行为,则可以很好地防御此攻击。

+

其他 DDoS 攻击

发起 DDoS 也是需要大量的带宽资源的,但是互联网就像森林,林子大了什么鸟都有,DDoS 攻击者也能找到其他的方式发起廉价并且极具杀伤力的 DDoS 攻击。

+

利用 XSS

举个例子,如果 12306 页面有一个 XSS 持久型漏洞被恶意攻击者发现,只需在春节抢票期间在这个漏洞中执行脚本使得往某一个小站点随便发点什么请求,然后随着用户访问的增多,感染用户增多,被攻击的站点自然就会迅速瘫痪了。这种 DDoS 简直就是无本万利,不用惊讶,现在大站有 XSS 漏洞的不要太多。

+

来自 P2P 网络攻击

大家都知道,互联网上的 P2P 用户和流量都是一个极为庞大的数字。如果他们都去一个指定的地方下载数据,成千上万的真实 IP 地址连接过来,没有哪个设备能够支撑住。拿 BT 下载来说,伪造一些热门视频的种子,发布到搜索引擎,就足以骗到许多用户和流量了,但是这只是基础攻击。
高级的 P2P 攻击,是直接欺骗资源管理服务器。如迅雷客户端会把自己发现的资源上传到资源管理服务器,然后推送给其它需要下载相同资源的用户,这样,一个链接就发布出去。通过协议逆向,攻击者伪造出大批量的热门资源信息通过资源管理中心分发出去,瞬间就可以传遍整个 P2P 网络。更为恐怖的是,这种攻击是无法停止的,即使是攻击者自身也无法停止,攻击一直持续到 P2P 官方发现问题更新服务器且下载用户重启下载软件为止。

+

DDoS 最后总结

DDoS 不可能防的住,就好比你的店只能容纳 50 人,黑社会有 100 人,你就换一家大店,能容纳 500 人,然后黑社会又找来了 1000 人,这种堆人头的做法就是 DDoS 本质上的攻防之道,「道高一尺,魔高一丈,魔高一尺,道高一丈」,讲真,必要的时候就答应勒索你的人的条件吧,实在不行就报警吧。

+

流量劫持

流量劫持应该算是黑产行业的一大经济支柱了吧?简直是让人恶心到吐,不吐槽了,还是继续谈干货吧,流量劫持基本分两种:DNS 劫持 和 HTTP 劫持,目的都是一样的,就是当用户访问 zoumiaojiang.com 的时候,给你展示的并不是或者不完全是 zoumiaojiang.com 提供的 “内容”。

+

DNS 劫持

DNS 劫持,也叫做域名劫持,可以这么理解,「你打了一辆车想去商场吃饭,结果你打的车是小作坊派来的,直接给你拉到小作坊去了」,DNS 的作用是把网络地址域名对应到真实的计算机能够识别的 IP 地址,以便计算机能够进一步通信,传递网址和内容等。如果当用户通过某一个域名访问一个站点的时候,被篡改的 DNS 服务器返回的是一个恶意的钓鱼站点的 IP,用户就被劫持到了恶意钓鱼站点,然后继而会被钓鱼输入各种账号密码信息,泄漏隐私。

+

这类劫持,要不就是网络运营商搞的鬼,一般小的网络运营商与黑产勾结会劫持 DNS,要不就是电脑中毒,被恶意篡改了路由器的 DNS 配置,基本上做为开发者或站长却是很难察觉的,除非有用户反馈,现在升级版的 DNS 劫持还可以对特定用户、特定区域等使用了用户画像进行筛选用户劫持的办法,另外这类广告显示更加随机更小,一般站长除非用户投诉否则很难觉察到,就算觉察到了取证举报更难。无论如何,如果接到有 DNS 劫持的反馈,一定要做好以下几件事:

+
    +
  • 取证很重要,时间、地点、IP、拨号账户、截屏、URL 地址等一定要有。
  • +
  • 可以跟劫持区域的电信运营商进行投诉反馈。
  • +
  • 如果投诉反馈无效,直接去工信部投诉,一般来说会加白你的域名。
  • +
+

HTTP 劫持

HTTP 劫持您可以这么理解,「你打了一辆车想去商场吃饭,结果司机跟你一路给你递小作坊的广告」,HTTP 劫持主要是当用户访问某个站点的时候会经过运营商网络,而不法运营商和黑产勾结能够截获 HTTP 请求返回内容,并且能够篡改内容,然后再返回给用户,从而实现劫持页面,轻则插入小广告,重则直接篡改成钓鱼网站页面骗用户隐私。能够实施流量劫持的根本原因,是 HTTP 协议没有办法对通信对方的身份进行校验以及对数据完整性进行校验。如果能解决这个问题,则流量劫持将无法轻易发生。所以防止 HTTP 劫持的方法只有将内容加密,让劫持者无法破解篡改,这样就可以防止 HTTP 劫持了。

+

HTTPS 协议就是一种基于 SSL 协议的安全加密网络应用层协议,可以很好的防止 HTTP 劫持。这里有篇 文章 讲的不错。HTTPS 在这就不深讲了,后面有机会我会单独好好讲讲 HTTPS。如果不想站点被 HTTP 劫持,赶紧将你的站点全站改造成 HTTPS 吧。

+

服务器漏洞

服务器除了以上提到的那些大名鼎鼎的漏洞和臭名昭著的攻击以外,其实还有很多其他的漏洞,往往也很容易被忽视,在这个小节也稍微介绍几种。

+

越权操作漏洞

如果你的系统是有登录控制的,那就要格外小心了,因为很有可能你的系统越权操作漏洞,越权操作漏洞可以简单的总结为 「A 用户能看到或者操作 B 用户的隐私内容」,如果你的系统中还有权限控制就更加需要小心了。所以每一个请求都需要做 userid 的判断

+

以下是一段有漏洞的后端示意代码:

+
1
2
3
4
5
6
7
// ctx 为请求的 context 上下文
let msgId = ctx.params.msgId;

mysql.query(
'SELECT * FROM msg_table WHERE msg_id = ?',
[msgId]
);
+ +

以上代码是任何人都可以查询到任何用户的消息,只要有 msg_id 就可以,这就是比较典型的越权漏洞

+

越权操作漏洞防御

如下这么改进一下:

+
1
2
3
4
5
6
7
8
// ctx 为请求的 context 上下文
let msgId = ctx.params.msgId;
let userId = ctx.session.userId; // 从会话中取出当前登陆的 userId

mysql.query(
'SELECT * FROM msg_table WHERE msg_id = ? AND user_id = ?',
[msgId, userId]
);
+

嗯,大概就是这个意思,如果有更严格的权限控制,那在每个请求中凡是涉及到数据库的操作都需要先进行严格的验证,并且在设计数据库表的时候需要考虑进 userId 的账号关联以及权限关联。

+

目录遍历漏洞

目录遍历漏洞指通过在 URL 或参数中构造 .././ 和类似的跨父目录字符串的 ASCII 编码、unicode 编码等,完成目录跳转,读取操作系统各个目录下的敏感文件,也可以称作「任意文件读取漏洞」。

+

目录遍历漏洞原理:程序没有充分过滤用户输入的 ../ 之类的目录跳转符,导致用户可以通过提交目录跳转来遍历服务器上的任意文件。使用多个.. 符号,不断向上跳转,最终停留在根 /,通过绝对路径去读取任意文件。

+

目录遍历漏洞几个示例和测试,一般构造 URL 然后使用浏览器直接访问,或者使用 Web 漏洞扫描工具检测,当然也可以自写程序测试。

+
1
2
3
4
5
6
7
8
9
http://somehost.com/../../../../../../../../../etc/passwd
http://somehost.com/some/path?file=../../Windows/system.ini

# 借助 %00 空字符截断是一个比较经典的攻击手法
http://somehost.com/some/path?file=../../Windows/system.ini%00.js

# 使用了 IIS 的脚本目录来移动目录并执行指令
http://somehost.com/scripts/..%5c../Windows/System32/cmd.exe?/c+dir+c:\

+

目录遍历漏洞防御

方法就是需要对 URL 或者参数进行 ../,./ 等字符的转义过滤。

+

物理路径泄漏

物理路径泄露属于低风险等级缺陷,它的危害一般被描述为「攻击者可以利用此漏洞得到信息,来对系统进一步地攻击」,通常都是系统报错 500 的错误信息直接返回到页面可见导致的漏洞。得到物理路径有些时候它能给攻击者带来一些有用的信息,比如说:可以大致了解系统的文件目录结构;可以看出系统所使用的第三方软件;也说不定会得到一个合法的用户名(因为很多人把自己的用户名作为网站的目录名)。

+

物理路径泄漏防御

防止这种泄漏的方法就是做好后端程序的出错处理,定制特殊的 500 报错页面。

+

源码暴露漏洞

和物理路径泄露类似,就是攻击者可以通过请求直接获取到你站点的后端源代码,然后就可以对系统进一步研究攻击。那么导致源代码暴露的原因是什么呢?基本上就是发生在服务器配置上了,服务器可以设置哪些路径的文件才可以被直接访问的,这里给一个 koa 服务起的例子,正常的 koa 服务器可以通过 koa-static 中间件去指定静态资源的目录,好让静态资源可以通过路径的路由访问。比如你的系统源代码目录是这样的:

+
1
2
3
4
5
6
|- project
|- src
|- static
|- ...
|- server.js

+ +

你想要将 static 的文件夹配成静态资源目录,你应该会在 server.js 做如下配置:

+
1
2
3
4
5
6
const Koa = require('koa');
const serve = require('koa-static');
const app = new Koa();

app.use(serve(__dirname + '/project/static'));

+ +

但是如果配错了静态资源的目录,可能就出大事了,比如:

+
1
2
3
// ...
app.use(serve(__dirname + '/project'));

+ +

这样所有的源代码都可以通过路由访问到了,所有的服务器都提供了静态资源机制,所以在通过服务器配置静态资源目录和路径的时候,一定要注意检验,不然很可能产生漏洞。

+

最后,希望 Web 开发者们能够管理好自己的代码隐私,注意代码安全问题,比如不要将产品的含有敏感信息的代码放到第三方外部站点或者暴露给外部用户,尤其是前端代码,私钥类似的保密性的东西不要直接输出在代码里或者页面中。也许还有很多值得注意的点,但是归根结底还是绷住安全那根弦,对待每一行代码都要多多推敲。

+

转载自:https://zoumiaojiang.com/article/common-web-security/

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/15/index.html b/en/page/15/index.html new file mode 100644 index 0000000000..475b4abd2a --- /dev/null +++ b/en/page/15/index.html @@ -0,0 +1,531 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

架构实践

架构设计内容一览

应用架构设计

    +
  • Transaction执行控制(在线/批处理路径和控制)
  • +
  • Session控制
  • +
  • 死锁控制
  • +
  • 接口处理流程(适配器设置/文件备份)
  • +
  • 命名规范(实例/SID等)
  • +
  • 中间件参数 (初始化参数等)
  • +
  • 认证(处理流程/错误处理)
  • +
  • 帐户(用户ID体系/权限管理/申请方式)
  • +
+

开发架构设计

    +
  • 系统景观(各种环境名称、利用方法等)
  • +
  • 转运路线(转运路线/转运工具/承认方式)
  • +
  • 开发账户(用户ID体系/权限管理/申请方式)
  • +
  • 开发终端设置(在线批处理开发工具/目录等)
  • +
  • 开发资源管理
  • +
  • 表单、协作工具的利用方法(表单/接口ID定义规则)
  • +
  • 开发和验证备份(处理流程/文件清除/周期)
  • +
  • 数据屏蔽(処理概要/屏蔽利用规则/対象/开发环境搭建方法)
  • +
+

运维架构设计

    +
  • 监测规范(方式/周期/対象)
  • +
  • 审计和错误日志(日志级别/日志保留时效/删除时效)
  • +
  • 生产环境和调研环境的备份(处理流程/删除时效/周期)
  • +
  • 加密处理(安装位置、加密规则、记录水平加密方法)
  • +
  • 云安全设定
  • +
  • 增加功能设计
  • +
+

基础设施

    +
  • 网络设计(虚拟网/子网/URL/负载均衡访问控制/白名单)
  • +
  • 云设计(实例/NFS/IAM)
  • +
  • 存储设计 (BlockVolume/FileStorage/ObjectStorage/数据配置/数据圧縮方法)
  • +
  • 时间DNS同步化(OCI設定)
  • +
  • OS参数设定
  • +
  • Docker参数设定
  • +
  • 高可用切换 (処理方式、设定参数考量)
  • +
  • 打补丁(打补丁/执行规范)
  • +
  • 虚拟桌面设定
  • +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/16/index.html b/en/page/16/index.html new file mode 100644 index 0000000000..d40ddb0175 --- /dev/null +++ b/en/page/16/index.html @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

ngx_http_stub_status_module 是一个 Nginx 的内置 模块,可以提供 Nginx 的简单统计信息。默认情况下这个模块是不被编译进来的,所以在编译 Nginx 时要指定加载该模块:

+
1
--with-http_stub_status_module
+

查看是否安装

1
2
3
4
5
// 查看nginx版本及安装了那些模块
nginx -V

// 如果安装了的话会显示下面的信息
--with-http_stub_status_module
+ +

配置使用

nginx.conf配置文件中进行如下配置

+
1
2
3
location /ngx_status {
stub_status on; // 设置开启性能模块
}
+

通过浏览器访问 http://localhost/ngx_status 就能看到你的nginx服务性能监控画面了
image

+

解析

    +
  • 第一行:Active connections:活动连接数
  • +
  • 第二三行:Server accepts handled requests:(三个数字分别代表)总共处理的连接数,成功握手的连接数量,处理的请求数(正常情况下握手和连接数是相等的,表示没有丢失)
  • +
  • 第四行:
      +
    • Reading: Nginx 读取到客户端的Header信息数
    • +
    • Writing: Nginx 返回给客户端的Header信息数
    • +
    • Waiting: 开启keep-alive的情况下,这个值等于 active – (reading + writing),意思就是Nginx已经处理完成,正在等候下一次请求指令的驻留连接(在nginx开启了keep-alive,也就是长连接的情况下,客户端跟服务端建立了连接但是没有读写操作的空闲状态)
    • +
    +
  • +
+

使用场景

    +
  • 可以简单的用脚本做监控
  • +
  • 可以用开源工具,zabbix,prometheus等去采集nginx的统计信息,做监控和历史数据采集
  • +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/17/index.html b/en/page/17/index.html new file mode 100644 index 0000000000..9b48bd19a5 --- /dev/null +++ b/en/page/17/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

HexoGithub发布之后自定义域名的配置

进到你的博客发布所在仓库,如:

选择Settings,然后是pages,滚动页面找到Custom domain,将你自己的域名绑定进去保存一下就好了。

+

在你自己的域名商处用CNAME把你自己的域名指定到你自己的仓库page地址上比如我的就是lixuguang.github.io Public,这样你的GitHub Page就可以通过自己的域名进行访问了。

+

HexoGithub发布之后自定义域名会被清空的问题

使用 GitHub Page 功能将博客托管在了 GitHub 上,并配置 CNAME 将自己的域名解析了过去,但是发现一个问题,每次 hexo deploy 之后,custom domain 会被重置失效。

+

解决方案

Hexo 生成的博客的 source 目录下(注意这个是你的博客源码所在仓库,并不是发布的仓库)新建一个 CNAME 文件,然后在这个文件中填入你的域名,这样就不会每次发布之后,GitHub Page 里的 custom domain 都被重置掉啦。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/18/index.html b/en/page/18/index.html new file mode 100644 index 0000000000..02e2eaf30f --- /dev/null +++ b/en/page/18/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Nginx指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#启动Nginx
start nginx

#重启Nginx
nginx -s reopen

#重新加载Nginx配置文件,然后以优雅的方式重启Nginx
nginx -s reload

#强制停止Nginx服务
nginx -s stop

#优雅地停止Nginx服务(即处理完所有请求后再停止服务)
nginx -s quit

#检测配置文件是否有语法错误,然后退出
nginx -t

#显示版本信息并退出
nginx -v

#显示版本和配置选项信息,然后退出
nginx -V

#检测配置文件是否有语法错误,然后退出
nginx -t

#检测配置文件是否有语法错误,转储并退出
nginx -T

#在检测配置文件期间屏蔽非错误信息
nginx -q

#打开帮助信息
nginx -?,-h

#设置前缀路径(默认是:/usr/share/nginx/)
nginx -p prefix

#设置配置文件(默认是:/etc/nginx/nginx.conf)
nginx -c filename

#设置配置文件外的全局指令
nginx -g directives
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/19/index.html b/en/page/19/index.html new file mode 100644 index 0000000000..0f7a167c1c --- /dev/null +++ b/en/page/19/index.html @@ -0,0 +1,545 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

Docker 命令

    +
  • 帮助命令
    docker –help
  • +
+

镜像操作

    +
  • 搜索镜像
    docker search hello-world

    +
  • +
  • 下载镜像
    docker pull hello-world

    +
  • +
  • 查看本地已下载所有镜像
    docker images

    +
  • +
  • 查看镜像历史
    docker history hello-world

    +
  • +
  • 备份镜像
    docker tag hello-word:last hello-world:v2

    +
  • +
  • 删除镜像
    docker rmi hello-word:last

    +
  • +
  • 删除未使用过的镜像
    docker image prune

    +
  • +
  • 导出镜像
    docker save -o hello-world:last.tar hello-world:last

    +
  • +
  • 导入镜像
    docker load -i hello-world:last.tar

    +
  • +
  • 查看镜像信息
    docker image inspact nginx

    +
  • +
+

容器操作

    +
  • 查看所有容器 [-q 编号] [-a active 启动的容器]
    docker ps [-a] [-q]

    +
  • +
  • 启动容器 [-d 后台启动]
    docker run -d –name nginx1 nginx:last

    +
  • +
  • 停止容器
    docker stop nginx1

    +
  • +
  • 启动容器()
    docker start nginx1

    +
  • +
  • 删除容器
    docker rm nginx1

    +
  • +
  • 批量删除运行中容器
    docker rm $(docker ps -q) -f

    +
  • +
  • 创建容器并进入
    docker run -it –name nginx1 nginx:last /bin/bash

    +
  • +
  • 退出容器
    exit

    +
  • +
  • 进入容器
    docker exec -it nginx1 /bin/bash

    +
  • +
  • 通过容器创建镜像
    docker commit -m ‘laowang’ nginx1 nginx:v1

    +
  • +
  • 查看容器信息
    docker container inspact nginx1

    +
  • +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/2/index.html b/en/page/2/index.html new file mode 100644 index 0000000000..3df579e4eb --- /dev/null +++ b/en/page/2/index.html @@ -0,0 +1,477 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

DockerCompose安装

1
2
3
4
5
DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
mkdir -p $DOCKER_CONFIG/cli-plugins
sudo curl -SL https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-x86_64 -o /usr/bin/docker-compose
sudo chmod +x /usr/bin/docker-compose
sudo docker-compose version
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/20/index.html b/en/page/20/index.html new file mode 100644 index 0000000000..85153c9f7a --- /dev/null +++ b/en/page/20/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

Docker网络模式

Docker常见的两种网络模式

+
    +
  • host网络模式:创建的容器和宿主机共享同一个网卡
  • +
  • bridge网络模式:桥接模式,只要使用桥接模式创建容器网段一样(类似于172.16.1.1,172.16.1.2)有点像是子局域网
  • +
+

利用network命令管理网络模式

    +
  • 查看网络模式
    docker network ls
  • +
  • 创建网络模式
    docker network create –drive bridge bridge_test
  • +
  • 通过network断网
    docker network disconnet bridge nginx5
  • +
  • 通过network联网
    docker network connect bridge nginx5
  • +
  • 删除网络模式
    docker network rm bridge_test
  • +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/21/index.html b/en/page/21/index.html new file mode 100644 index 0000000000..3dab89b27c --- /dev/null +++ b/en/page/21/index.html @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

Docker容器端口映射

常见容器服务需要做端口映射,这里以nginx为例进行举例

+

启动一个nginx容器

docker run -itd –name nginx1 -P nginx:latest #随机端口
docker run -itd –name nginx2 -p 6001:80 nginx:latest #指定端口
docker run -itd –name nginx3 -p 172.16.10.1:6002:80 nginx:latest #指定IP和端口

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/22/index.html b/en/page/22/index.html new file mode 100644 index 0000000000..95bea8ed3d --- /dev/null +++ b/en/page/22/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

Docker数据卷

宿主机与容器进行数据交互,共享宿主机与容器之间的数据

+

创建数据卷关联

docker run -it –name my_ubuntu1 -v ~ /Desktop/test:/test /bin/bash

+

利用系统方法操作数据卷

    +
  • 查 docker数据卷
    docker volume ls

    +
  • +
  • 创建数据卷
    docker volume create volname

    +
  • +
  • 共享
    docker run -it –name my_ubuntu2 -v volname:/home ubuntu:latest /bin/bash

    +
  • +
+

数据卷容器使用

可以通过数据卷容器,创建新的容器,并将多个容器绑定在一起

+

docker run -itd –name my_ubuntu3 –volumes-from my_ubuntu2 ubuntu:latest

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/23/index.html b/en/page/23/index.html new file mode 100644 index 0000000000..7f1ace403c --- /dev/null +++ b/en/page/23/index.html @@ -0,0 +1,613 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

DockerFile

原则与建议

    +
  • 容器轻量化。从镜像中产生的容器应该尽量轻量化,能在足够短的时间内停止、销毁、重新生成并替换原来的容器。
  • +
  • 使用 .gitignore。在大部分情况下,Dockerfile 会和构建所需的文件放在同一个目录中,为了提高构建的性能,应该使用 .gitignore 来过滤掉不需要的文件和目录。
  • +
  • 为了减少镜像的大小,减少依赖,仅安装需要的软件包。
  • +
  • 一个容器只做一件事。解耦复杂的应用,分成多个容器,而不是所有东西都放在一个容器内运行。如一个 Python Web 应用,可能需要 Server、DB、Cache、MQ、Log 等几个容器。一个更加极端的说法:One process per container。
  • +
  • 减少镜像的图层。不要多个 Label、ENV 等标签。
  • +
  • 对续行的参数按照字母表排序,特别是使用apt-get install -y安装包的时候。
  • +
  • 使用构建缓存。如果不想使用缓存,可以在构建的时候使用参数–no-cache=true来强制重新生成中间镜像。
  • +
+

常用命令

FROM

FROM 指令用于指定其后构建新镜像所使用的基础镜像

+
1
2
3
FROM <image>
FROM <image>:<tag>
FROM <image>:<digest>
+

FROM 必须 是 Dockerfile 中第一条非注释命令
在一个 Dockerfile 文件中创建多个镜像时,FROM 可以多次出现。只需在每个新命令 FROM 之前,记录提交上次的镜像 ID。
tag 或 digest 是可选的,如果不使用这两个值时,会使用 latest 版本的基础镜像

+

RUN

在镜像的构建过程中执行特定的命令,并生成一个中间镜像

+
1
2
3
4
#shell格式
RUN <command>
#exec格式
RUN ["executable", "param1", "param2"]
+
    +
  • RUN 命令将在当前 image 中执行任意合法命令并提交执行结果。命令执行提交后,就会自动执行 Dockerfile 中的下一个指令。
  • +
  • 层级 RUN 指令和生成提交是符合 Docker 核心理念的做法。它允许像版本控制那样,在任意一个点,对 image 镜像进行定制化构建。
  • +
  • RUN 指令创建的中间镜像会被缓存,并会在下次构建中使用。如果不想使用这些缓存镜像,可以在构建时指定 ######no###cache 参数,如:docker build ######no###cache。
  • +
+

FROM scratch

scratch 是一个空的虚拟的镜像源文件,因为需要的依赖都存在不需要基础镜像,从而减小体积。

+

COPY

复制文件

+
1
2
COPY <源路径>... <目标路径>
COPY ["<源路径1>",... "<目标路径>"]
+

和 RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的<目标路径>位置。比如:

+
1
COPY package.json /usr/src/app/
+ +

<源路径>可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,如:

+
1
2
COPY hom* /mydir/
COPY hom?.txt /mydir/
+ +

<目标路径>可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

+

此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。

+

ADD

更高级的复制文件
ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。比如<源路径>可以是一个 URL,这种情况下,Docker 引擎会试图去下载这个链接的文件放到<目标路径>去。

+

在构建镜像时,复制上下文中的文件到镜像内,格式:

+
1
2
ADD <源路径>... <目标路径>
ADD ["<源路径>",... "<目标路径>"]
+

注意
如果 docker 发现文件内容被改变,则接下来的指令都不会再使用缓存。关于复制文件时需要处理的/,基本跟正常的 copy 一致

+

ENV

设置环境变量
格式有两种:

+
1
2
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
+

这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。

+
1
2
ENV VERSION=1.0 DEBUG=on \
NAME="Happy Feet"
+

这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。

+

EXPOSE

为构建的镜像设置监听端口,使容器在运行时监听。
格式:

+
1
EXPOSE <port> [<port>...]
+

EXPOSE 指令并不会让容器监听 host 的端口,如果需要,需要在 docker run 时使用 -p、-P 参数来发布容器端口到 host 的某个端口上。

+

VOLUME

定义匿名卷
VOLUME用于创建挂载点,即向基于所构建镜像创始的容器添加卷:

+
1
VOLUME ["/data"]
+

一个卷可以存在于一个或多个容器的指定目录,该目录可以绕过联合文件系统,并具有以下功能:

+
    +
  • 卷可以容器间共享和重用
  • +
  • 容器并不一定要和其它容器共享卷
  • +
  • 修改卷后会立即生效
  • +
  • 对卷的修改不会对镜像产生影响
  • +
  • 卷会一直存在,直到没有任何容器在使用它
    VOLUME 让我们可以将源代码、数据或其它内容添加到镜像中,而又不并提交到镜像中,并使我们可以多个容器间共享这些内容。
  • +
+

WORKDIR

WORKDIR用于在容器内设置一个工作目录:

+
1
WORKDIR /path/to/workdir
+

通过WORKDIR设置工作目录后,Dockerfile 中其后的命令 RUN、CMD、ENTRYPOINT、ADD、COPY 等命令都会在该目录下执行。
如,使用WORKDIR设置工作目录:

+
1
2
3
4
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
+

在以上示例中,pwd 最终将会在 /a/b/c 目录中执行。在使用 docker run 运行容器时,可以通过-w参数覆盖构建时所设置的工作目录。

+

USER

指定当前用户
USER 用于指定运行镜像所使用的用户:

+
1
USER daemon
+

使用USER指定用户时,可以使用用户名、UID 或 GID,或是两者的组合。以下都是合法的指定试:

+
1
2
3
4
5
6
USER user
USER user:group
USER uid
USER uid:gid
USER user:gid
USER uid:group
+

使用USER指定用户后,Dockerfile 中其后的命令 RUN、CMD、ENTRYPOINT 都将使用该用户。镜像构建完成后,通过 docker run 运行容器时,可以通过 -u 参数来覆盖所指定的用户。

+

CMD

CMD用于指定在容器启动时所要执行的命令。CMD 有以下三种格式:

+
1
2
3
CMD ["executable","param1","param2"]
CMD ["param1","param2"]
CMD command param1 param2
+

省略可执行文件的 exec 格式,这种写法使 CMD 中的参数当做 ENTRYPOINT 的默认参数,此时 ENTRYPOINT 也应该是 exec 格式,具体与 ENTRYPOINT 的组合使用,参考 ENTRYPOINT。

+

注意
与 RUN 指令的区别:RUN 在构建的时候执行,并生成一个新的镜像,CMD 在容器运行的时候执行,在构建时不进行任何操作。

+

ENTRYPOINT

ENTRYPOINT 用于给容器配置一个可执行程序。也就是说,每次使用镜像创建容器时,通过 ENTRYPOINT 指定的程序都会被设置为默认程序。ENTRYPOINT 有以下两种形式:

+
1
2
3
ENTRYPOINT ["executable", "param1", "param2"]
ENTRYPOINT command param1 param2
ENTRYPOINT 与 CMD 非常类似,不同的是通过docker run执行的命令不会覆盖 ENTRYPOINT,而docker run命令中指定的任何参数,都会被当做参数再次传递给 ENTRYPOINT。Dockerfile 中只允许有一个 ENTRYPOINT 命令,多指定时会覆盖前面的设置,而只执行最后的 ENTRYPOINT 指令。
+

docker run运行容器时指定的参数都会被传递给 ENTRYPOINT ,且会覆盖 CMD 命令指定的参数。如,执行docker run -d时,-d 参数将被传递给入口点。

+

也可以通过docker run –entrypoint重写 ENTRYPOINT 入口点。如:可以像下面这样指定一个容器执行程序:

+
1
ENTRYPOINT ["/usr/bin/nginx"]
+

完整构建代码:

+
1
2
3
4
5
6
7
8
9
# Version: 0.0.3
FROM ubuntu:16.04
MAINTAINER 何民三 "cn.liuht@gmail.com"
RUN apt-get update
RUN apt-get install -y nginx
RUN echo 'Hello World, 我是个容器' \
> /var/www/html/index.html
ENTRYPOINT ["/usr/sbin/nginx"]
EXPOSE 80
+

使用docker build构建镜像,并将镜像指定为 itbilu/test:

+
1
docker build -t="itbilu/test" .
+

构建完成后,使用itbilu/test启动一个容器:

+
1
docker run -i -t  itbilu/test -g "daemon off;"
+

在运行容器时,我们使用了 -g “daemon off;”,这个参数将会被传递给 ENTRYPOINT,最终在容器中执行的命令为 /usr/sbin/nginx -g “daemon off;”。

+

LABEL

LABEL用于为镜像添加元数据,元数以键值对的形式指定:

+
1
LABEL <key>=<value> <key>=<value> <key>=<value> ...
+

使用LABEL指定元数据时,一条LABEL指定可以指定一或多条元数据,指定多条元数据时不同元数据之间通过空格分隔。推荐将所有的元数据通过一条LABEL指令指定,以免生成过多的中间镜像。
如,通过LABEL指定一些元数据:

+
1
LABEL version="1.0" description="这是一个Web服务器" by="IT笔录"
+

指定后可以通过docker inspect查看:

+
1
2
3
4
5
6
docker inspect itbilu/test
"Labels": {
"version": "1.0",
"description": "这是一个Web服务器",
"by": "IT笔录"
},
+ +

ARG

ARG用于指定传递给构建运行时的变量:

+
1
ARG <name>[=<default value>]
+

如,通过ARG指定两个变量:

+
1
2
ARG site
ARG build_user=IT笔录
+

以上我们指定了 site 和 build_user 两个变量,其中 build_user 指定了默认值。在使用 docker build 构建镜像时,可以通过 –build-arg = 参数来指定或重设置这些变量的值。

+
1
docker build --build-arg site=itiblu.com -t itbilu/test .
+

这样我们构建了 itbilu/test 镜像,其中site会被设置为 itbilu.com,由于没有指定 build_user,其值将是默认值 IT 笔录。

+

ONBUILD

ONBUILD用于设置镜像触发器:

+
1
ONBUILD [INSTRUCTION]
+

当所构建的镜像被用做其它镜像的基础镜像,该镜像中的触发器将会被钥触发。
如,当镜像被使用时,可能需要做一些处理:

+
1
2
3
4
[...]
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
[...]
+ +

STOPSIGNAL

STOPSIGNAL用于设置停止容器所要发送的系统调用信号:

+
1
STOPSIGNAL signal
+

所使用的信号必须是内核系统调用表中的合法的值,如:SIGKILL。

+

SHELL

SHELL用于设置执行命令(shell式)所使用的的默认 shell 类型:

+
1
SHELL ["executable", "parameters"]
+

SHELL在Windows环境下比较有用,Windows 下通常会有 cmd 和 powershell 两种 shell,可能还会有 sh。这时就可以通过 SHELL 来指定所使用的 shell 类型:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM microsoft/windowsservercore

# Executed as cmd /S /C echo default
RUN echo default

# Executed as cmd /S /C powershell -command Write-Host default
RUN powershell -command Write-Host default

# Executed as powershell -command Write-Host hello
SHELL ["powershell", "-command"]
RUN Write-Host hello

# Executed as cmd /S /C echo hello
SHELL ["cmd", "/S"", "/C"]
RUN echo hello
+ +

Dockerfile 示例

    +
  • 构建Nginx运行环境
  • +
  • 构建tomcat 环境
  • +
+

构建Nginx运行环境

Dockerfile文件

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#指定基础镜像
FROM sameersbn/ubuntu:14.04.20161014

#维护者信息
MAINTAINER sameer@damagehead.com

#设置环境
ENV RTMP_VERSION=1.1.10 \
NPS_VERSION=1.11.33.4 \
LIBAV_VERSION=11.8 \
NGINX_VERSION=1.10.1 \
NGINX_USER=www-data \
NGINX_SITECONF_DIR=/etc/nginx/sites-enabled \
NGINX_LOG_DIR=/var/log/nginx \
NGINX_TEMP_DIR=/var/lib/nginx \
NGINX_SETUP_DIR=/var/cache/nginx

#设置构建时变量,镜像建立完成后就失效
ARG BUILD_LIBAV=false
ARG WITH_DEBUG=false
ARG WITH_PAGESPEED=true
ARG WITH_RTMP=true

#复制本地文件到容器目录中
COPY setup/ ${NGINX_SETUP_DIR}/
RUN bash ${NGINX_SETUP_DIR}/install.sh

#复制本地配置文件到容器目录中
COPY nginx.conf /etc/nginx/nginx.conf
COPY entrypoint.sh /sbin/entrypoint.sh

#运行指令
RUN chmod 755 /sbin/entrypoint.sh

#允许指定的端口
EXPOSE 80/tcp 443/tcp 1935/tcp

#指定网站目录挂载点
VOLUME ["${NGINX_SITECONF_DIR}"]

ENTRYPOINT ["/sbin/entrypoint.sh"]
CMD ["/usr/sbin/nginx"]
+ +

构建Tomcat环境

Dockerfile文件

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 指定基于的基础镜像
FROM ubuntu:13.10

# 维护者信息
MAINTAINER zhangjiayang "zhangjiayang@sczq.com.cn"

# 镜像的指令操作
# 获取APT更新的资源列表
RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe"> /etc/apt/sources.list
# 更新软件
RUN apt-get update

# Install curl
RUN apt-get -y install curl

# Install JDK 7
RUN cd /tmp && curl -L 'http://download.oracle.com/otn-pub/java/jdk/7u65-b17/jdk-7u65-linux-x64.tar.gz' -H 'Cookie: oraclelicense=accept-securebackup-cookie; gpw_e24=Dockerfile' | tar -xz
RUN mkdir -p /usr/lib/jvm
RUN mv /tmp/jdk1.7.0_65/ /usr/lib/jvm/java-7-oracle/

# Set Oracle JDK 7 as default Java
RUN update-alternatives --install /usr/bin/java java /usr/lib/jvm/java-7-oracle/bin/java 300
RUN update-alternatives --install /usr/bin/javac javac /usr/lib/jvm/java-7-oracle/bin/javac 300

# 设置系统环境
ENV JAVA_HOME /usr/lib/jvm/java-7-oracle/

# Install tomcat7
RUN cd /tmp && curl -L 'http://archive.apache.org/dist/tomcat/tomcat-7/v7.0.8/bin/apache-tomcat-7.0.8.tar.gz' | tar -xz
RUN mv /tmp/apache-tomcat-7.0.8/ /opt/tomcat7/

ENV CATALINA_HOME /opt/tomcat7
ENV PATH $PATH:$CATALINA_HOME/bin

# 复件tomcat7.sh到容器中的目录
ADD tomcat7.sh /etc/init.d/tomcat7
RUN chmod 755 /etc/init.d/tomcat7

# Expose ports. 指定暴露的端口
EXPOSE 8080

# Define default command.
ENTRYPOINT service tomcat7 start && tail -f /opt/tomcat7/logs/catalina.out
+ +

tomcat7.sh命令文件

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export JAVA_HOME=/usr/lib/jvm/java-7-oracle/  
export TOMCAT_HOME=/opt/tomcat7

case $1 in
start)
sh $TOMCAT_HOME/bin/startup.sh
;;
stop)
sh $TOMCAT_HOME/bin/shutdown.sh
;;
restart)
sh $TOMCAT_HOME/bin/shutdown.sh
sh $TOMCAT_HOME/bin/startup.sh
;;
esac
exit 0
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/24/index.html b/en/page/24/index.html new file mode 100644 index 0000000000..bcb429d235 --- /dev/null +++ b/en/page/24/index.html @@ -0,0 +1,567 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

1、说明

项目是一个多人协同完成同一个目标的团队组织形式,在多人协作项目中,如果代码风格统一(前端代码规范和静态代码检查约束)、代码提交信息的说明准确规范(本章介绍),那么在项目开发过程及后期协作以及Bug处理时会更加方便。

+

在本文中,我将介绍大家如何利用工具及约定保证大家代码提交的统一性,从而提高大家的协同效率:

+
    +
  • commitlint: git 提交信息规范与验证
    +

    添加如ESLint的格式规范校验,规范comiit的格式,达到团队每个人的提交风格保持一直,保证提交信息的完整和准确性
    风格如下:

    +
    +
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
<type>(<scope>): <subject> // Header
// 空一行
<body> // 72
// 空一行
<footer> // 72

// 例
fix(登陆模块): 修复登录密码错误的提示

登录没有做友好型提示

修正后不存在此问题
+ +
    +
  • husky: 使git-hook更容易
    +

    husky继承了Git下所有的钩子,在触发钩子的时候,husky可以阻止不合法的commit,push等等。注意使用husky之前,必须先将代码放到git 仓库中,否则本地没有.git文件,就没有地方去继承钩子了。

    +
    +
  • +
  • standard-version: 自动生成CHANGELOG 并发布版本
  • +
+

2、git commit message 规范

commit message格式

1
2
3
4
5
类型(影响范围): 描述

问题描述

修复方式结果
+ + +

注意:
1.冒号后面有空格。
2.英文小括号。
3.正文和注脚前都要加空行。

+

type 类型

用于说明 commit 的类别,只允许使用下面13个标识。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
'feat', // feat:新增功能
'fix', // fix:bug 修复
'docs', // docs:文档更新
'style', // style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑)
'refactor', // refactor:重构代码(既没有新增功能,也没有修复 bug)
'test', // test:新增测试用例或是更新现有测试
'chore', // revert:回滚某个更早之前的提交
'revert', // build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交
'build', // build:打包生产环境代码
'ci', // ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交
'merge', // merge:分支合并 Merge branch ? of ?
'perf', // perf:性能, 体验优化
'chore' // chore:不属于以上类型的其他类型(构建过程或辅助工具的变动)
+ +

如果type为feat和fix,则该 commit 将肯定出现在 Change log 之中。

+

scope 影响范围

可根据项目组需要进行定制化设置,也可不做强制要求

+

subject 描述

subject是 commit 目的的简短描述,不超过50个字符,且结尾不加句号(.)。

+

body 详细描述

body 部分是对本次 commit 的详细描述,可以分成多行。下面是一个范例。

+
1
2
3
此次提交内容包括如下信息:
- 登录为空校验
- 密码错误提示
+

Footer 部分只用于两种情况。

+
    +
  • 不兼容变动(改变解决方案)
  • +
  • 关闭 Issue(回复bug)
  • +
+

3、使用工具校验commit是否符合规范

commitlint

3.1 commitlint安装

1
2
// npm 安装
npm install --save-dev @commitlint/{cli,config-conventional}
+ +

3.2 生成commitlint.config.js配置文件

执行echo命令创建配置文件,也可以手动创建配置文件,或者从已有的配置文件进行拷贝。

+
1
2
// 项目根目录创建commitlint.config.js
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
+ + +

3.3 在commitlint.config.js制定提交message规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// type(scope?): subject

// body?

// footer?
module.exports = {
// 继承自默认规则
extends: ['@commitlint/config-conventional'],
// 这里写自定义规则
rules: {
// 等级 [0 1 2]: 0 不使用规则 1 警告 2 错误
// 启用范围 always|never: 总是或者永不.
// 规则值: 规则对应的值

// 头部(包含type、scope、subject)
'header-case': [0, 'always', 'lower-case'],
'header-full-stop': [0, 'never', '.'],
'header-max-length': [0, 'always', 72],
'header-min-length': [0, 'always', 0],

// 提交类型
'type-case': [0, 'never'],
'type-empty': [2, 'never'],
'type-max-length': [0, 'always', Infinity],
'type-min-length': [0, 'always', 0],
'type-enum': [
2,
'always',
[
'feat', // feat:新增功能
'fix', // fix:bug 修复
'docs', // docs:文档更新
'style', // style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑)
'refactor', // refactor:重构代码(既没有新增功能,也没有修复 bug)
'test', // test:新增测试用例或是更新现有测试
'revert', // revert:回滚某个更早之前的提交
'build', // build:build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交,打包生产环境代码
'ci', // ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交
'merge', // merge:分支合并 Merge branch ? of ?
'perf', // perf:性能, 体验优化
'chore' // chore:不属于以上类型的其他类型(构建过程或辅助工具的变动)
]
],

// 影响范围
'scope-case': [0, 'always', 'lower-case'], // 书写格式
'scope-max-length': [0, 'always', Infinity], // 最长
'scope-min-length': [0, 'always', 0], // 最短
'scope-empty': [2, 'never'], // 影响范围:为空|永不
'scope-enum': [0, 'always', ['a', 'b']], // 影响范围:给出范围但是不在给定的列表内

// 简介
'subject-empty': [2, 'never'], // 简介不能为空
'subject-full-stop': [0, 'never', '.'], // 简介结尾以.结束
'subject-case': [0, 'never'],
'subject-max-length': [0, 'always', Infinity],
'subject-min-length': [0, 'always', 1],

// 正文
'body-leading-blank': [2, 'always'],
'body-max-length': [0, 'always', Infinity],
'body-max-line-length': [0, 'always', Infinity],
'body-min-length': [0, 'always', 1],

// 注脚
'footer-leading-blank': [2, 'always'],
'footer-max-length': [0, 'always', Infinity],
'footer-max-line-length': [0, 'always', Infinity],
'footer-min-length': [0, 'always', 1],

// 其他
'references-empty': [0, 'never'],
'signed-off-by': [0, 'always', 'Signed-off-by']
}
}
+ +

上面我们就完成了commitlint的安装与提交规范的制定。检验commit message的最佳方式是结合git hook,所以需要配合Husky

+

husky

3.4 husky安装

1
npm install husky --save-dev
+

3.5 husky 配置

安装成功后需要在项目下的package.json中配置

+
1
2
3
4
5
6
7
8
9
10
11
// package.json
{
"husky": {
"hooks": { // husky的钩子
"pre-commit": "npm run lint" // 提交前进行代码静态校验
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS" // 进行提交信息格式校验
}
},
"scripts": {}
...
}
+ +

3.5 husky 执行测试

最后我们可以正常的git操作

+
1
2
git add .
git commit -m ""
+ +

git commit的时候会触发commlint。下面演示下不符合规范提交示例:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
C:\lixg\git>git commit -m "thunisoft: abcde"

husky > npm run -s commitmsg (node v8.2.1)

⧗ input:
thunisoft(all): abcde

✖ type must be one of [feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert] [type-enum]
✖ found 1 problems, 0 warnings

husky > commit-msg hook failed (add --no-verify to bypass)

C:\lixg\git>
+ +

上面的提交看反馈消息得知是type格式没有符合限制的提交类型列表,所以提交失败,虾米啊我们把type改为feat再试一下

+
1
2
3
4
5
6
7
8
9
10
11
C:\lixg\git>git commit -m "feat(all): 新功能"

husky > npm run -s commitmsg (node v8.2.1)

⧗ input: feat: 新功能
✔ found 0 problems, 0 warnings

[develop 19dfhe] feat: 新功能
1 file changed, 1 insertion(+)

C:\lixg\git>
+ +

修改后格式符合规范,提交成功。

+

参考来源

+

版权声明

Copyright by lixuguang
未经授权,严禁转载。如需转载,请联系作者

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/25/index.html b/en/page/25/index.html new file mode 100644 index 0000000000..45207290c4 --- /dev/null +++ b/en/page/25/index.html @@ -0,0 +1,736 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

1. URI

URI 表示资源,资源一般对应服务器端领域模型中的实体类。

+

URI规范

    +
  1. 不用大写;
  2. +
  3. 用中杠-不用下杠_;
  4. +
  5. 参数列表要encode;
  6. +
  7. URI中的名词表示资源集合,使用复数形式。
  8. +
+

资源集合 vs 单个资源

URI表示资源的两种方式:资源集合、单个资源。

+

资源集合:

+
1
2
/zoos //所有动物园
/zoos/1/animals //id为1的动物园中的所有动物
+

单个资源:

+
1
2
/zoos/1 //id为1的动物园
/zoos/1;2;3 //id为1,2,3的动物园
+

避免层级过深的URI

/在url中表达层级,用于按实体关联关系进行对象导航,一般根据id导航。

+

过深的导航容易导致url膨胀,不易维护,如 GET /zoos/1/areas/3/animals/4,尽量使用查询参数代替路径中的实体导航,如GET /animals?zoo=1&area=3

+

对Composite资源的访问

服务器端的组合实体必须在uri中通过父实体的id导航访问。

+
+

组合实体不是first-class的实体,它的生命周期完全依赖父实体,无法独立存在,在实现上通常是对数据库表中某些列的抽象,不直接对应表,也无id。一个常见的例子是 User — Address,Address是对User表中zipCode/country/city三个字段的简单抽象,无法独立于User存在。必须通过User索引到Address:GET /user/1/addresses

+
+

2. Request

HTTP方法

通过标准HTTP方法对资源CRUD:

+

GET:查询

+
1
2
3
GET /zoos
GET /zoos/1
GET /zoos/1/employees
+ + +

POST:创建单个资源。POST一般向“资源集合”型uri发起

+
1
2
POST /animals  //新增动物
POST /zoos/1/employees //为id为1的动物园雇佣员工
+ +

PUT:更新单个资源(全量),客户端提供完整的更新后的资源。与之对应的是 PATCH,PATCH 负责部分更新,客户端提供要更新的那些字段。PUT/PATCH一般向“单个资源”型uri发

+
1
2
PUT /animals/1
PUT /zoos/1
+ +

DELETE:删除

+
1
2
3
DELETE /zoos/1/employees/2
DELETE /zoos/1/employees/2;4;5
DELETE /zoos/1/animals //删除id为1的动物园内的所有动物
+

HEAD / OPTION 用的不多,就不多解释了。

+

安全性和幂等性

    +
  1. 安全性:不会改变资源状态,可以理解为只读的;
  2. +
  3. 幂等性:执行1次和执行N次,对资源状态改变的效果是等价的。
  4. +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
.安全性幂等性
GET
POST××
PUT×
DELETE×
+

安全性和幂等性均不保证反复请求能拿到相同的response。以 DELETE 为例,第一次DELETE返回200表示删除成功,第二次返回404提示资源不存在,这是允许的。

+

复杂查询

查询可以捎带以下参数:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
.示例备注
过滤条件type=1&age=16允许一定的uri冗余,如/zoos/1与/zoos?id=1
排序?sort=age,desc
投影?whitelist=id,name,email
分页?limit=10&offset=3
+

Bookmarker

经常使用的、复杂的查询标签化,降低维护成本。

+

如:

+
1
2
GET /trades?status=closed&sort=created,desc

+

快捷方式:

+
1
2
3
GET /trades#recently-closed
// 或者
GET /trades/recently-closed
+ +

Format

只用以下常见的3种body format:

+
    +
  1. Content-Type: application/json
  2. +
+
1
2
3
4
5
6
7
8
9
10
POST /v1/animal HTTP/1.1
Host: api.example.org
Accept: application/json
Content-Type: application/json
Content-Length: 24

{
"name": "Gir",
"animalType": "12"
}
+ +
    +
  1. Content-Type: application/x-www-form-urlencoded (浏览器POST表单用的格式)
  2. +
+
1
2
3
4
5
6
7
POST /login HTTP/1.1
Host: example.com
Content-Length: 31
Accept: text/html
Content-Type: application/x-www-form-urlencoded

username=root&password=Zion0101
+ +
    +
  1. Content-Type: multipart/form-data; boundary=—-RANDOM_jDMUxq4Ot5 (表单有文件上传时的格式)
  2. +
+

6. Response

    +
  1. 不要包装:
    response 的 body 直接就是数据,不要做多余的包装。
  2. +
+

错误示例

+
1
2
3
4
{
"success":true,
"data":{"id":1,"name":"xiaotuan"},
}
+

各HTTP方法成功处理后的数据格式:

+ + + + + + + + + + + + + + + + + + + + + + + +
·response 格式
GET单个对象、集合
POST新增成功的对象
PUT/PATCH更新成功的对象
DELETE
+
    +
  1. json格式的约定:

    +
      +
    1. 时间用长整形(毫秒数),客户端自己按需解析(moment.js)
    2. +
    3. 不传null字段
    4. +
    +
  2. +
+

分页response

1
2
3
4
{
"paging":{"limit":10,"offset":0,"total":729},
"data":[{},{},{}...]
}
+

7. 错误处理

    +
  1. 不要发生了错误但给2xx响应,客户端可能会缓存成功的http请求;
  2. +
  3. 正确设置http状态码,不要自定义;
  4. +
  5. Response body 提供
      +
    1. 错误的代码(日志/问题追查);
    2. +
    3. 错误的描述文本(展示给用户)。
    4. +
    +
  6. +
+

对第三点的实现稍微多说一点:

+

Java 服务器端一般用异常表示 RESTful API 的错误。API 可能抛出两类异常:业务异常非业务异常

+

业务异常由自己的业务代码抛出,表示一个用例的前置条件不满足、业务规则冲突等,比如参数校验不通过、权限校验失败。

+

非业务类异常表示不在预期内的问题,通常由类库、框架抛出,或由于自己的代码逻辑错误导致,比如数据库连接失败、空指针异常、除0错误等等。

+

业务类异常必须提供2种信息:

+
    +
  1. 如果抛出该类异常,HTTP 响应状态码应该设成什么;
  2. +
  3. 异常的文本描述;
  4. +
+

在Controller层使用统一的异常拦截器:

+
    +
  1. 设置 HTTP 响应状态码:对业务类异常,用它指定的 HTTP code;对非业务类异常,统一500;
  2. +
  3. Response Body 的错误码:异常类名
  4. +
  5. Response Body 的错误描述:对业务类异常,用它指定的错误文本;对非业务类异常,线上可以统一文案如“服务器端错误,请稍后再试”,开发或测试环境中用异常的 stacktrace,服务器端提供该行为的开关。
  6. +
+

常用的http状态码及使用场景:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
状态码使用场景
400 bad request常用在参数校验
401 unauthorized未经验证的用户,常见于未登录。如果经过验证后依然没权限,应该 403(即 authentication 和 authorization 的区别)。
403 forbidden无权限
404 not found资源不存在
500 internal server error非业务类异常
503 service unavaliable由容器抛出,自己的代码不要抛这个异常
+

8. 服务型资源

除了资源简单的CRUD,服务器端经常还会提供其他服务,这些服务无法直接用上面提到的URI映射。如:

+
    +
  1. 按关键字搜索;
  2. +
  3. 计算地球上两点间的距离;
  4. +
  5. 批量向用户推送消息;
  6. +
+

可以把这些服务看成资源,计算的结果是资源的presentation,按服务属性选择合适的HTTP方法。

+

例:

+
1
2
3
4
GET /search?q=filter?category=file  搜索
GET /distance-calc?lats=47.480&lngs=-122.389&late=37.108&lnge=-122.448
POST /batch-publish-msg
[{"from":0,"to":1,"text":"abc"},{},{}...]
+

9. 异步任务

对耗时的异步任务,服务器端接受客户端传递的参数后,应返回创建成功的任务资源,其中包含了任务的执行状态。客户端可以轮训该任务获得最新的执行进度。

+
1
2
3
4
5
6
7
8
9
// 提交任务:
POST /batch-publish-msg
[{"from":0,"to":1,"text":"abc"},{},{}...]

// 返回:
{"taskId":3,"createBy":"Anonymous","status":"running"}

GET /task/3
{"taskId":3,"createBy":"Anonymous","status":"success"}
+ +

如果任务的执行状态包括较多信息,可以把“执行状态”抽象成组合资源,客户端查询该状态资源了解任务的执行情况。

+
1
2
3
4
5
6
7
8
9
// 提交任务:
POST /batch-publish-msg
[{"from":0,"to":1,"text":"abc"},{},{}...]

// 返回:
{"taskId":3,"createBy":"Anonymous"}

GET /task/3/status
{"progress":"50%","total":18,"success":8,"fail":1}
+ +

10. API的演进

版本

常见的三种方式:

+
    +
  1. 在uri中放版本信息:GET /v1/users/1
  2. +
  3. Accept Header:Accept: application/json+v1
  4. +
  5. 自定义 Header:X-Api-Version: 1
  6. +
+

用第一种,虽然没有那么优雅,但最明显最方便。

+

URI失效

随着系统发展,总有一些API失效或者迁移,对失效的API,返回404 not found 或 410 gone;对迁移的API,返回 301 重定向。

+

11. 安全

这个不熟,接触到的时候再说。

+

参考文档

    +
  • < RESTful Web Services Cookbook >
  • +
  • Consumer-Centric API Design
  • +
  • RESTful Best Practices
  • +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/26/index.html b/en/page/26/index.html new file mode 100644 index 0000000000..f7396c89b2 --- /dev/null +++ b/en/page/26/index.html @@ -0,0 +1,645 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

1.关于Code Review

1.1 Code Review的目的

Code Review是一种用来确认方案设计和代码实现的质量保证机制,通过这个机制我们可以对代码、测试过程和注释进行检查。

+

Code Review主要用来在软件工程过程中改进代码质量,通过Code Review可以达到如下目的目的:

+
    +
  1. 在项目早期就能够发现代码中的BUG
  2. +
  3. 帮助初级开发人员学习高级开发人员的经验,达到知识共享
  4. +
  5. 避免开发人员犯一些很常见,很普通的错误
  6. +
  7. 保证项目组人员的良好沟通
  8. +
  9. 项目或产品的代码更容易维护
  10. +
+

1.2 Code Review的前提

进入Code Review需要检查的条件如下:

+
    +
  1. Code Review人员是否理解了Code Review的概念和Code Review将做什么
    如果做Code Review的人员不能理解Code Review对项目成败和代码质量的重要程度,他们的做法可能就会是应付了事。
  2. +
  3. 代码是否已经正确的build,build的目的使得代码已经不存在基本语法错误
    我们总不希望高级开发人员或是主管将时间浪费在检查连编译都通不过的代码上吧。
  4. +
  5. 代码执行时功能是否正确
    Code Review人员也不负责检查代码的功能是否正确,也就是说,需要复查的代码必须由开发人员或质量人员负责该代码的功能的正确性。
  6. +
  7. Review人员是否理解了代码
    做复查的人员需要对该代码有一个基本的了解,其功能是什么,是拿一方面的代码,涉及到数据库或是通讯,这样才能采取针对性的检查
  8. +
  9. 开发人员是否对代码做了单元测试
    这一点也是为了保证Code Review前一些语法和功能问题已经得到解决,Code Review人员可以将精力集中在代码的质量上。
  10. +
+

1.3 Code Review需要做什么

Code Review主要检查代码中是否存在以下方面问题:

+
    +
  • 代码的一致性
  • +
  • 编码风格
  • +
  • 代码的安全问题
  • +
  • 代码冗余
  • +
  • 是否正确设计以满足需求(性能、功能)
  • +
  • 等等
  • +
+

1.3.1 完整性检查(Completeness)

    +
  • 代码是否完全实现了设计文档中提出的功能需求
  • +
  • 代码是否已按照设计文档进行了集成和Debug
  • +
  • 代码是否已创建了需要的数据库,包括正确的初始化数据
  • +
  • 代码中是否存在任何没有定义或没有引用到的变量、常数或数据类型
  • +
+

1.3.2 一致性检查(Consistency)

    +
  • 代码的逻辑是否符合设计文档
  • +
  • 代码中使用的格式、符号、结构等风格是否保持一致
  • +
+

1.3.3 正确性检查(Correctness)

    +
  • 代码是否符合制定的标准
  • +
  • 所有的变量都被正确定义和使用
  • +
  • 所有的注释都是准确的
  • +
  • 所有的程序调用都使用了正确的参数个数
  • +
+

1.3.4 可修改性检查(Modifiability)

    +
  • 代码涉及到的常量是否易于修改(如使用配置、定义为类常量、使用专门的常量类等)
  • +
  • 代码中是否包含了交叉说明或数据字典,以描述程序是如何对变量和常量进行访问的
  • +
  • 代码是否只有一个出口和一个入口(严重的异常处理除外)
  • +
+

1.3.5 可预测性检查(Predictability)

    +
  • 代码所用的开发语言是否具有定义良好的语法和语义
  • +
  • 是否代码避免了依赖于开发语言缺省提供的功能
  • +
  • 代码是否无意中陷入了死循环
  • +
  • 代码是否是否避免了无穷递归
  • +
+

1.3.6 健壮性检查(Robustness)

    +
  • 代码是否采取措施避免运行时错误
      +
    • 数组边界溢出
    • +
    • 被零除
    • +
    • 值越界
    • +
    • 堆栈溢出
    • +
    • +
    +
  • +
+

1.3.7 结构性检查(Structuredness)

    +
  • 程序的每个功能是否都作为一个可辩识的代码块存在
    循环是否只有一个入口
  • +
+

1.3.8 可追溯性检查(Traceability)

    +
  • 代码是否对每个程序进行了唯一标识
  • +
  • 是否有一个交叉引用的框架可以用来在代码和开发文档之间相互对应
  • +
  • 代码是否包括一个修订历史记录,记录中对 代码的修改和原因都有记录
  • +
  • 是否所有的安全功能都有标识
  • +
+

1.3.9 可理解性检查(Understandability)

    +
  • 注释是否足够清晰的描述每个子程序
  • +
  • 是否使用到不明确或不必要的复杂代码,它们是否被清楚的注释
  • +
  • 使用一些统一的格式化技巧(如缩进、空白等)用来增强代码的清晰度
  • +
  • 是否在定义命名规则时采用了便于记忆,反映类型等方法
  • +
  • 每个变量都定义了合法的取值范围
  • +
  • 代码中的算法是否符合开发文档中描述的数学模型
  • +
+

1.3.10 可验证性检查(Verifiability)

代码中的实现技术是否便于测试

+

1.4 Code Review的步骤

这些是我在平时工作中的经验总结,目前也是按照这个步骤在做。

+
    +
  1. 代码编写者和代码审核者坐在一起,由代码编写者按照UC依次讲解自己负责的代码和相关逻辑,从Web层->DAO层;
  2. +
  3. 代码审核者在此过程中可以随时提出自己的疑问,同时积极发现隐藏的bug;对这些bug记录在案。
  4. +
  5. 代码讲解完毕后,代码审核者给自己安排几个小时再对代码审核一遍。
    代码需要一行一行静下心看。同时代码又要全面的看,以确保代码整体上设计优良。
  6. +
  7. 代码审核者根据审核的结果编写“代码审核报告”,“审核报告”中记录发现的问题及修改建议,然后把“审核报告”发送给相关人员。
  8. +
  9. 代码编写者根据“代码审核报告”给出的修改意见,修改好代码,有不清楚的地方可积极向代码审核者提出。
  10. +
  11. 代码编写者 bug fix完毕之后给出反馈。
  12. +
  13. 代码审核者把Code Review中发现的有价值的问题更新到”代码审核规范”的文档中,对于特别值得提醒的问题可群发email给所有技术人员。
  14. +
+

提示
Code Review必备的文档:

+
    +
  • “代码审核规范”文档:记录代码应该遵循的标准。
    +

    代码审核者根据这些标准来Code Review代码,同时在Code Review过程中不断完善该文档。

    +
    +
  • +
+

2.Code Reivew的执行

一个标准的Code Reivew活动应该分为三个阶段:

+

2.1.事前准备阶段

在一次CR前,对以下内容进行充分准备。

+

2.1.1.CR的对象

在准备CR代码对象时,我们要注意代码的数量,如果代码量比较大,要对代码进行必要的分解,确定其中的关键代码,对关键代码进行CR,可以达到举一反三的目的。

+

2.1.2.CR的内容

我们对代码的审查内容很多,如代码的编写是否规范(注释的书写格式、命名规范等)、技术处理规范(异常处理、日志处理、代码组织结构等)、业务实现等。我们不能希望通过一次CR活动,完成所有这些内容的审查,因此我们必须设定本次CR活动内容界限,确定审查重点;

+

2.1.3.评审规范和标准

在CR前设计确定评审规范和标准是必要,通过规范和标准我们在审查过程中可以有据可依,有理可循,而且还可以做到标准统一。

+

2.1.4.选择CR活动的参与者

在CR开始前,必须把本次CR活动的对象、审查内容以及审查的规范和标准通报给所有的参与者。

+

2.1.5.选择CR活动的实施方式。

CR活动有很多形式可供我们选择,我们可以根据实际情况选择桌面式CR、演示讲解式CR、一对一的座位CR等等。

+

2.2.实施阶段

充分的事前准备,只是做好CR活动的前提,在CR实施过程中,我们要做好以下工作。

+

2.2.1.准确记录

对于CR过程发现的问题,我们必须清晰准确的记录,可以使用问题点记录单,明确记录的项目和内容。

+

2.2.2.讲解与提问

CR过程中,要采用代码作者讲解和审查者提问方式。审查者不能只在发现问题时提问,同时也要根据本次审查的内容要求代码作者对某个特定问题的讲解。

+

2.2.3.逐项审查

对事前确定的审查内容,要逐项审查,不能因为时间不足等因素一扫而过。

+

2.2.4.注意气氛

实施审查时,要营造一个讨论问题、解决问题的氛围,不能把审查会搞成批判会,这样会影响相关人员的积极性。

+

2.3. 事后跟踪跟踪。

2.3.1. 确认发现的问题

CR结束后,对发现的问题,首先需要确定以下内容。

+
    +
  1. 问题点的难易程度以及影响的范围;
  2. +
  3. 解决问题的责任者和问题点修正结果的确认者;
  4. +
  5. 解决问题点的时限。
  6. +
+

2.3.2. 修正问题责任者

对于修正问题责任者,在问题点的修正过程中,要三方面内容的记录。

+
    +
  1. 问题点的原因;
  2. +
  3. 解决问题点的对策;
  4. +
  5. 修正的内容。
  6. +
+

2.3.3. 修正结果确认者

做为修正结果的确认者,必须按照事前约定的时限及时的对修正结果进行全面的确认

+

3.注意事项

3.1. 经常进行Code Review

    +
  1. 要Review的代码越多,那么要重构,重写的代码就会越多。而越不被程序作者接受的建议也会越多,唾沫口水战也会越多。
  2. +
  3. 程序员代码写得时候越长,程序员就会在代码中加入越来越多的个人的东西。
  4. +
  5. 越接近软件发布的最终期限,代码也就不能改得太多。
  6. +
+

3.2. Code Review不要太正式,而且要短

忘了那个代码评审的Checklist吧,走到你的同事座位跟前,像请师父一样请他坐到你的电脑面前,然后,花5分钟给他讲讲你的代码,给他另外一个5分钟让他给你的代码提提意见,这比什么都好。而如果你用了一个Checklist,让这个事情表现得很正式的话,下面两件事中必有一件事会发生:

+
    +
  1. 只有在Checklist上存在的东西才会被Review。
  2. +
  3. Code Reviews 变成了一种礼节性的东西,你的同事会装做很关心你的代码,但其实他心里想着尽快地离开你。
  4. +
+

只有不正式的Code Review才会让你和评审者放轻松,人只有放松了,才会表现得很真实,很真诚。记住Review只不过是一种形式,而只有在相互信任中通过相互的讨论得到了有意义和有建设性的建议和意见,那才是最实在的。不然,作者和评审者的关系就会变成小偷和警察的关系。

+

3.3. 尽可能的让不同的人Reivew你的代码

如果可能的话,不要总是只找一个人来Review你的代码,不同的人有不同的思考方式,有不同的见解,所以,不同的人可以全面的从各个方面评论你的代码。
但不要太多了,人多嘴杂反而适得其反,基本上来说,不要超过3个人,这是因为,这是一个可以围在一起讨论的最大人员尺寸。

+

下面是几个优点:

+
    +
  1. 从不同的方向评审代码总是好的。
  2. +
  3. 会有更多的人帮你在日后维护你的代码。
  4. +
  5. 这也是一个增加团队凝聚力的方法。
  6. +
+

3.4. 保持积极的正面的态度

程序员最大的问题就是“自负”,尤其当我们Reivew别人的代码的时候,我已经见过无数的场面,程序员在Code Review的时候,开始抨击别人的代码,质疑别人的能力。太可笑了,我分析了一下,这类的程序员其实并没有什么本事,因为他们指责对方的目的是想告诉大家自己有多么的牛,靠这种手段来表现自己的程序员,其实是就是传说中所说的“半瓶水”。

+

所以,无论是代码作者,还是评审者,都需要一种积极向上的正面的态度,作者需要能够虚心接受别人的建议,因为别人的建议是为了让你做得更好;评审者也需要以一种积极的正面的态度向作者提意见,因为那是和你在一个战壕里的战友。记住,你不是一段代码,你是一个人!

+

3.5. 学会享受Code Reivew

这可能是最重要的一个提示了,如果你到了一个人人都喜欢Code Reivew的团阿,那么,你会进入到一个生机勃勃的地方,在那里,每个人都能写出质量非常好的代码,在那里,你不需要经理的管理,团队会自适应一切变化,他们相互学习,相互帮助,不仅仅是写出好的代码,而且团队和其中的每个人都会自动进化,最关键的是,这个是一个团队。

+

资料来源

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/27/index.html b/en/page/27/index.html new file mode 100644 index 0000000000..1c918e6bc5 --- /dev/null +++ b/en/page/27/index.html @@ -0,0 +1,570 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前后端分离开发规范

by lixg

+

(本文所说的前后端工作皆站在工作内容本身,不与职称相关,下面皆如此。)

+

一、为什么要前后端分离

    +
  1. 前后端专注于各自擅长的领域
  2. +
  3. 前端配置后端代码运行环境,节省搭建环境的时间
  4. +
  5. 明确前后端工作职责
  6. +
  7. 提高开发效率
  8. +
  9. 分离有助于前端、后端分别优化
  10. +
+

二、前后端分离存在的问题

    +
  1. 前后端分离必须接口先行,无接口导致前后端关于接口的工作重复出现。
  2. +
  3. 接口对接方式不一致,各项目团队没有采用统一的接口对接方式,导致每次都要熟悉一种新的对接方式。
  4. +
  5. 数据传输格式不一致,导致每次都要重新熟悉,另外由于接口格式不一致还会引起其他未知问题,影响系统健壮性。
  6. +
  7. 人员比例问题,目前公司前后端比例不完全符合前后端分离开发的要求。
  8. +
+

为解决上述问题,提高前后端分离开发效率,特制定如下规范。

+

三、如何做分离

    +
  1. 职责分离
      +
    • 前后端仅仅通过异步接口(AJAX/JSONP)来编程
    • +
    • 前后端都各自有自己的开发流程,构建工具,测试集合
    • +
    • 关注点分离,前后端变得相对独立并松耦合
      前后端职责
    • +
    +
  2. +
+ + + + + + + + + + + + + + + + + + + + + + + +
后端前端
提供数据接收数据,展示数据
处理业务逻辑处理渲染逻辑
Server-side MVC架构Client-sideMV*架构
代码运行在服务器上代码运行在浏览器上
+
    +
  1. 开发流程
      +
    • 前后端技术负责人约定好接口格式
    • +
    • 后端编写和维护接口文档,在 API 变化时更新接口文档 + MOCK平台
    • +
    • 后端根据接口文档进行接口开发
    • +
    • 前端根据接口文档 + MOCK平台进行开发
    • +
    • 开发完成后联调和提交测试
    • +
    +
  2. +
+

MOCK平台统一采用公司搭建的YAPI平台

+

YAPI平台可以对接SWAGGER工具进行自动构建MOCK服务
前后端开发流程
3. 规范原则
- 接口返回数据即显示:前端仅做渲染逻辑处理
- 渲染逻辑禁止跨多个接口调用
- 前端关注交互、渲染逻辑,尽量避免业务逻辑处理的出现
- 请求响应传输数据格式:JSON,JSON数据尽量简单轻量,避免多级JSON的出现

+

四、基本格式

接口定义参见《RESTFul API的设计规范》

请求格式

GET请求、POST请求==必须包含key为body的入参,所有请求数据包装为JSON格式,并存放到入参body中==,示例如下:

+

GET请求:

+
1
xxx/login?body={"username":"admin","password":"123456","captcha":"scfd","rememberMe":1}
+ +

响应格式

对于通用业务数据响应参照基本数据格式要求

+

响应基本数据格式

1
2
3
4
{
"code": 200,
"msg": "success"
}
+
响应实体格式
1
2
3
4
5
6
7
8
9
10
11
{
"code": 200,
"msg": "success",
"data": {
"entity": {
"id": 1,
"name": "XXX",
"phone": "XXX"
}
}
}
+

entity: 响应返回的实体数据

+
响应列表格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"code": 200,
"msg": "success",
"data": {
"list":[
{
"id": 1,
"name": "XXX",
"code": "XXX"
},
{
"id": 2,
"name": "XXX",
"code": "XXX"
},
{
"id": 3,
"name": "XXX",
"code": "XXX"
}
]
}
}
+

list: 响应返回的列表数据

+
响应分页格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"code": 200,
"msg":"success",
"data": {
"totalCount": 2, // 总记录数
"totalPage": 1 // 总页数
"pageNo": 1, // 当前页码
"pageSize": 10, // 每页大小
"list":[
{
"id": 1,
"name": "XXX",
"code": "XXX"
},
{
"id": 2,
"name": "XXX",
"code": "XXX"
},
{
"id": 3,
"name": "XXX",
"code": "XXX"
}
],
}
}
+

响应特殊数据格式

对于特定组件数据格式由后端统一处理后返回前端,如(echart、ztree等组件)

+

特殊内容规范

布尔类型

关于布尔类型,一律返回BOOLEN类型值

+
日期格式

关于日期类型,JSON数据传输中一律使用字符串格式时间戳,具体日期格式因业务而定

+

五、相关文章导读

前后端分离开发指南-理论篇
前后端分离开发指南-实践篇

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/28/index.html b/en/page/28/index.html new file mode 100644 index 0000000000..1be9f2e3d6 --- /dev/null +++ b/en/page/28/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

背景

前后端分离狭义上来讲是以浏览器为分界,应用在浏览器内部的技术为前端技术,主要负责页面展示的部分,应用在浏览器外的技术为后端技术,主要负责处理业务逻辑和数据准备工作,但是随着近几年NodeJs的崛起,前端的工作范畴明显扩大,前后端分离也不仅仅作为一种开发模式,更是web应用架构层面的一种模式。

+

认识

    +
  1. 在开发阶段,前后端工程师要提前约定好数据交互的接口,从而实现并行开发和测试;
  2. +
  3. 在运行阶段,前后端分离模式需要对web应用进行分离部署,前后端之前使用HTTP或者其他协议进行交互请求。(NodeJs或Ngnix)
  4. +
+

由上述两条可以看出前后端分离分为开发阶段和部署阶段,通常来说我们做到的只是前后端分离开发,并没有做到分离部署。

+

分解

作为一种全新的架构模式,前后端分离需要从四个方面来比较和认识。

+
    +
  1. 交互形式
  2. +
  3. 开发模式/流程
  4. +
  5. 代码组织方式
  6. +
  7. 数据接口规范流程
  8. +
+

一、交互形式

在前后端分离架构中,后端只需要负责按照约定的数据格式向前端提供可调用的API服务即可。前后端之间通过HTTP请求进行交互,前端获取到数据后,进行页面的组装和渲染,最终返回给浏览器。

+

这里有一个经常引起激烈讨论的话题就是,“约定的数据格式”,是后端直接提供给前端可直接使用的数据格式,还是简单的数据库拉取数据不做加工直接推给前端,这个要视具体的项目情况而定,如果后端代码需要支持的不仅仅是一端,比如要同时适配pc端、手机端等多个终端,其多终端数据展现的形式如果各不相同,那后端只需要提供满足多端的基础数据格式即可,但如果只需要满足单个终端,那么建议直接将前后端数据格式进行统一处理,方便前后端协同,数据处理可以放在java端做也可以放在NodeJs端做,视人员配备情况而定,但绝不是后端不做任何处理就将数据返回给前端,完全由前端组织数据。

+

二、开发模式/流程

在前后端分离模式未诞生之前,前后端属于一个整体,那时使用的是MVC架构模式,前端对应的就是View层,主要通过html/css/js实现静态页面和动态效果,在有后端进行模板变量的嵌套和一些页面逻辑的处理,最终打包成一个整体,部署到同一服务器上,同时会进行简单的动静态分离部署。

+

此时开发的流程如下:
需求=》前后端并行开发=》前端开发静态页面=》后端套模板=》集成问题=》前后端调整=》再集成=》解决集成问题=》交付上线
// TODO 这里插图

+

出现前后端分离架构之后,前端工程师只需要编写前端页面+前端数据、业务逻辑处理,之后通过HTTP或其他请求方式调用后端提供的服务接口就可以了,而且除了在开发周期可以进行前后端分离,在部署阶段,前后端也可进行分离部署。
// TODO 这里插图

+

此时开发的流程如下:
需求=》设计接口、约定数据=》前后端并行开发=》集成=》调整=》集成成功=》交付上线

+

通过上面的描述及流程,不难发现,前后端分离的开发方式不仅仅从分工上进行了区分,更重要的是在并行开发的问题上解决了反复集成等前后端互相影响的问题,从而降低了开发的难度,简化了开发的流程。

+

三、代码组织方式

// TODO 这里插图
在传统的开发模式架构下,前端代码是作为项目的静态资源存在于项目工程下,页面中还夹着一些后端代码如jsp、php等技术,前后端开发时需要将整个项目代码完整的引入开发工具才能进行开发,前后端同时维护一份代码,这种开发方式导致前后端代码互相影响,因此前后端分离势在必行。

+

而前后端分离模式在代码的组织形式上由以下两种形式组成:

+
    +
  1. 半分离
    前后端仍共用一个代码库,但是代码分别存放在两个工程中。后端不关心或很少 关心前端元素的输出情况,前端不能独立进行开发和测试,项目中缺乏前后端 交互的测试用例。
  2. +
  3. 完全分离
    完全分离后,前端代码可以通过Mock来模拟后端请求,从此可以独立进行前端开发和测试。后端代码只需要按照跟前端约定好的接口格式写出完整的测试用例,确保接口的可用性。通过上述手段,降低开发集成风险。
  4. +
+

四、数据接口规范流程

通过上面三段的描述,我们可以看出前后端分离开发模式最重要也是最初的阶段就是数据接口的确定,因此在项目开发前必需先进行数据和接口的定义,数据接口的定义需要前后端开发共同商定,包括确定的数据格式,交互形式,并生成一份接口文档供前后端开发人员使用。之后才是并行开发。开发期间前后端双方需要严格按照确定的数据接口文档进行开发,前端开发完之后可以利用mock服务独自进行接口测试,后端也可以利用postman或其他接口测试工具进行测试,并提供完整的接口测试用例,然后前后端进行功能联调,最后再提交线上测试,也可进行自动化测试。

+

// TODO 这里插图

+

分离后的收益

到底分不分,如何分是个持续讨论的话题,通过上述的内容大家已经了解到了,什么是前后端分离,也知道如何进行前后端分离开发部署,那么前后端分离能带来哪些收益呢?

+

首先,就目前的软件开发应用趋势来看,越来越注重用户的体验性,而且架构越来越大,服务越来越小,而且终端设备越来越丰富,而原来不分离的方式已经不能支撑现在的发展趋势,因此前后端分离开发及部署将势在必行。

+

而且采用前后端分离的架构之后,我们将有如下几点提升:

+
    +
  1. 前后端分离后,前后端将不再互相纠缠而是各自在自己熟悉的领域进行开发工作,这将有利于前后端深化优化各自的代码,培养各自独特的技术特性,从而开发出更加优秀的应用,建立起专业精良的全栈开发团队。

    +
  2. +
  3. 通过前后端分离架构可以实现前后端开发从代码及开发流程上的完全解耦,只需要前后端共同商定好接口后,便可完全独立开发,只需要在联调阶段进行好协作,在此之前可以互不影响的进行并行开发,即是需求发生了变动,但只要不影响接口,后端既可以不用修改代码,只需前端进行变动即可,如此整体的开发效率将得到提升。

    +
  4. +
  5. 前后端分离后,能够更好的适应前端日益增多的的终端适配,代码解耦后复用率更高。

    +
  6. +
+

4.前后端分离后,前后端代码可以分别管理,代码不再混在一起,代码可维护性也增强了

+

前后端分离后收益不止以上四点,因为分离而带来的职责上、技术上、代码上、部署上的解耦让开发工作比以往任何时候都要更加专注和轻松。

+

注意事项

前后端分离误区

    +
  1. 前端人员不充足,不能进行前后端分离。
  2. +
+

此话说来是因为对前后端分离后职责区分不明确导致的问题,因为以往的前端只需要写静态页面就可以了,而前后端分离后前端也不仅仅需要写静态页面,而且还要为页面提供数据和页面逻辑的处理,但实际上可以根据团队情况来区别对待,如果团队前端人员充足,那么可以由前端人员负责多一些的工作,比如API请求后的业务逻辑的处理,页面逻辑的处理、页面数据的准备等,如果后端人员配备充足,那么上述几个环节仍然可由后端人员进行处理,前端开发仍然只是写静态页面,只是内容和逻辑不再写死而是通过js或其他手段如mvvm的框架进行处理。

+
    +
  1. 前后端分离后前端任务加重,职责也不清晰。
  2. +
+

如第一点描述可知,问题不在前后端分离的模式是否合适,而是任务分配和人员分配上的问题,如果前端能力强且人员比例较多,那么部分任务可以由前端承担,如果后端人员多,那么任务由后端承担。

+
    +
  1. 后端开发需要增加接口开发工作,增加任务量。
  2. +
+

无论如何后端开发都是需要写接口的,只是前后端分离后需要按照ResetFul风格写接口,或者采用最新的GraphQl的方式进行交互,如果说这个阶段需要前后端进行商量确定接口交互形式和数据格式花费了时间,但是在接下来前后端并行开发及问题解决上省掉的时间是更加可观的。

+
    +
  1. 分离后仍出现互相等待的问题,反而不如传统开发模式快。
  2. +
+

这个问题的产生其实也是由于对前后端分离后技术缺失导致的,常见情况是前端写完页面逻辑和假数据后后端开发还未完成接口开发导致无法进行联调,实际上前端通过mockserver等方式是可以解决一些问题的。

+

前后端分离适用场景

现代化的web应用适合用前后端分离的开发方式。
原因有以下几点:

+
    +
  1. web应用前端页面交互复杂。
      +
    • 页面渲染数据量大。
    • +
    • 页面包含复杂的业务逻辑。
    • +
    +
  2. +
  3. 终端适配情况多。
  4. +
  5. 分布式架构,微服务化应用场景。
  6. +
+

前后端分离具体方案

总体方向

后端专注于:后端控制层(Restful API) & 服务层 & 数据访问层;

+

前端专注于:前端控制层(Nodejs) & 视图层

+
    +
  1. 项目设计阶段,前后端架构负责人将项目整体进行分析,讨论并确定API风格、职责分配、开发协助模式,确定人员配备;设计确定后,前后端人员共同制定开发接口。

    +
  2. +
  3. 项目开发阶段,前后端分离是各自分工,协同敏捷开发,后端提供Restful API,并给出详细文档说明,前端人员进行页面渲染前台的任务是发送API请(GET,PUT,POST,DELETE等)获取数据(json,xml)后渲染页面。

    +
  4. +
  5. 项目测试阶段,API完成之前,前端人员会使用mock server进行模拟测试,后端人员采用junit进行API单元测试,不用互相等待;API完成之后,前后端再对接测试一下就可以了,当然并不是所有的接口都可以提前定义,有一些是在开发过程中进行调整的。

    +
  6. +
  7. 项目部署阶段,利用nginx 做反向代理,即Java + nodejs + nginx 方式进行。

    +
  8. +
+

技术手段

    +
  • 前端技术栈:前端代码 + mock服务
  • +
  • 后端技术栈:postman + 接口 + 后端业务逻辑 + 数据库
  • +
  • 公共依赖:接口文档/接口测试工具
  • +
+

常用的mock服务为jsonserver
常用的接口测试工具为postman、rap、swagger、doclever

+

部署方案

    +
  1. 第一阶段为前后端同一个代码库,同一个服务器,集中部署。
  2. +
  3. 第二阶段引入Ngnix服务作为中间件,前端向Ngnix发请求,Ngnix向后端服务发请求,由于Ngnix为静态服务器,所以在seo优化上和页面性能优化上效果不明显,因此前端仍需与后端进行配合才能达到整体的优化。
  4. +
+

浏览器 =》 Ngnix(前端机)=》Ngnix(后端机可没有)=》Server服务

+
    +
  1. 第三阶段引入nodejs作为中间层,将前端资源部署到Server层。同时实现数据代理服务,负责与提供数据的后端进行通信。
  2. +
+

浏览器=》Ngnix(前端机)=》NodeServer =》Server服务

+

浏览器向前端机发送请求,由Ngnix进行分发,url统一分发至NodeServer,在Node Server中根据请求类型从后端服务器上通过RPC服务请求页面的模板数据,然后进行页面的组装和渲染;API请求则直接转发到后端服务挖成相应。

+

前后端分离部署方案比较

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
属性字段传统模式Ngnix+ServerNode+ServerNgnix+Node+Server
SEOoknookok
浏览器渲染负担oknookok
前后端耦合nookokok
请求相应效率nooknook
+

结语

随着前端技术的快速发展和对用户体验日益增长的需求,前后端分离模式势必将会成为主流趋势。无论是从开发模式的角度上来说,还是对团队成长的角度上来说,前后端分离都会带来益处,让我们一同拥抱前后端分离,打造精良的开发团队,迎接日益复杂的web应用开发需求。

+

参考来源

+

版权声明

Copyright by lixuguang
未经授权,严禁转载。如需转载,请联系作者

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/29/index.html b/en/page/29/index.html new file mode 100644 index 0000000000..ac7e002784 --- /dev/null +++ b/en/page/29/index.html @@ -0,0 +1,548 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

目录
代码规范 2

+
    +
  1. 说明 2
  2. +
  3. 基本原则 2
  4. +
  5. C++与Python 2
  6. +
  7. 命名相关 2
  8. +
  9. 工程目录结构 3
  10. +
+

Electron代码规范

+
    +
  1. 说明
    Electron框架正如官网介绍是使用 JavaScript, HTML 和 CSS 构建跨平台的桌面应用的框架,框架分为前端和后端两个部分,根据前后端采用的技术不同分别遵循前后端所采用技术的代码规范
  2. +
  3. 基本原则
  4. +
+
    +
  1. 不要直接在渲染进程操作主进程,通过进程通信的方式进行数据及事件响应

    +
  2. +
  3. nodejs请使用require方式引入资源不使用import方式引入

    +
  4. +
  5. 多渲染进程传参请使用localStorage或sessionStorage

    +
  6. +
  7. 资源下载不下来时请直接到官网下载https://github.com/electron/electron/releases 将文件放到 用户目录\AppData\Local\electron\Cache 下 再进行 npm install -g electron

    +
  8. +
  9. 代码中包含native模块,必须进行 rebuild操作
    调试渲染进程时增加如下代码增加mainWindow.webContents.openDevTools();

    +
  10. +
  11. +
+
    +
  1. C++与Python
    对于 C++ 和 Python, 遵循 Chromium 的编码风格. 可以使用 clang-format 来自动格式化 C++ 代码. 可以使用 script/cpplint.py 来检验文件是否符合要求。
  2. +
+

现在使用的 Python 版本是 Python 2.7。

+

C++ 代码使用了大量 Chromium 的抽象和类型,因此建议使用者熟悉它们。 一个起步的好地方是 Chromium 的《重要的抽象概念和数据库结构》文档. 该文档提到一些特殊类型,范围类型(超出范围时自动释放其内存), 记录机制等。
4. 命名相关
Electron API 使用与 Node.js 相同的大小写方案:
当模块本身是class时, 比如 BrowserWindow, 使用 大驼峰.
当模块是一组 API 时, 比如 globalShortcut时,使用 小驼峰。
当 API 是对象的属性时, 并且它复杂到足以成为一个单独的块, 比如 win.webContents, 使用 小驼峰.
对于其他非模块API, 使用自然标题, 比如 Tag 或 Process Object.

+

当创建新的 API 时, 最好使用 getter 和 setter 而不是 jQuery 的一次性函数。 举个例子, .getText() 和 .setText(text) 优于 .text([text]).

+
    +
  1. 工程目录结构
    src - main
    // 必须

    +
      +
    • main.js // 入口
    • +
    • listen.js // 监听渲染进程通信
    • +
    • windows.js // 创建渲染进程
    • +
    • log.js // 日志log4js 插件
    • +
    +

    // 可选

    +
      +
    • utils.js
    • +
    • store.js
    • +
    +
  2. +
+
    +
  • api.js
  • +
  • render
  • +
+

main 目录下为主进程代码,render 目录下为渲染进程代码
渲染进程目录结构规范与前端项目目录结构规范一致

+

主进程中必须包含入口文件,监听进程文件和窗口创建文件
其他根据后端实际使用技术可选,nodejs作为后端的话 必须
6. 打包
Electron-packager 绿色可执行包
Electron-builder 压缩安装包

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/3/index.html b/en/page/3/index.html new file mode 100644 index 0000000000..0833f6ac09 --- /dev/null +++ b/en/page/3/index.html @@ -0,0 +1,499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Network in Compose 在Docker Compose中配置网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
version: "3.9"

services:
proxy:
build: ./proxy
networks:
- actsnetwork
app:
build: ./app
networks:
- actsnetwork
db:
image: postgres
networks:
- testnetwork

networks:
actsnetwork:
name: testnetwork
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/30/index.html b/en/page/30/index.html new file mode 100644 index 0000000000..8326794787 --- /dev/null +++ b/en/page/30/index.html @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Squid 服务器学习笔记

Squid 服务器介绍

用来缓存前端资源的代理服务器,可以类比后端数据库缓存软件Redis。
当用户访问目标资源时由Squid服务器进行判断如果有数据缓存直接将缓存数据返回,如果没有缓存,代理客户访问资源,获取到后返回给用户,当有其他用户访问相同资源时直接返回资源。

+

代理方式

    +
  1. 普通代理
  2. +
  3. 透明代理
  4. +
  5. 反向代理
  6. +
+

安装

1
2
3
4
5
6
7
8
9
10
11
12
// 检查是否已经安装
rpm -qa | grep squid
// 安装软件
rpm install squid
// 设置开机启动
chkconfig squid on
// 启动服务
service squid start
// 查看服务状态(端口监听3128)
netstat -anput |grap squid
// 关闭服务
service squid stop
+ +

配置文件

/etc/squid/squid.config

+

10.0.0.0/8

+
+

扩展知识
CIDR - 无类域间路由CIDR(Classless InterDomain Routing)
ip每位占1个字节8位,00000000 ~ 11111111,也就是0 ~ 255,ip共有四个位数。
10.0.0.0/8 的意思是 8位不可变,剩下的可变,8位这里的意思是说第一个IP段,那么也就是说IP范围是10.0.0.0 ~ 10.255.255.255 这样的一个ip范围。

+
+

// 复制并重命名
mv /etc/squid/squid.config{,.bak}
// 删除文件中的注释和空行(只保留有效设定)
awk ‘if($0!~ /^#/ && $0~ !/^$/print $0)’ /etc/squid/squid.config.bak > /etc/squid/squid.config

+

squid常用命令:
/usr/local/squid/sbin/squid -z 初始化缓存空间
/usr/local/squid/sbin/squid 启动
/usr/local/squid/sbin/squid -k shutdown 停止
/usr/local/squid/sbin/squid -k reconfigure 重新载入配置文件
/usr/local/squid/sbin/squid -k rotate 轮循日志

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#acl all src 0.0.0.0/0.0.0.0 and http_access allow all选项定义了一个访问控制列表。详细情况参见和Squid软件
#携带的文档。这里的访问控制列表允许所有对代理服务的访问,因为这里该代理是加速web服务器。
acl all src 0.0.0.0/0.0.0.0
#允许所有IP访问
acl manager proto http #manager url协议为http
acl localhost src 127.0.0.1/255.255.255.255 #允午本机IP
acl to_localhost dst 127.0.0.1 #允午目的地址为本机IP
acl Safe_ports port 80 # 允许安全更新的端口为80
acl CONNECT method CONNECT #请求方法以CONNECT
http_access allow all #允许所有人使用该代理.因为这里是代理加速web服务器
http_reply_access allow all #允许所有客户端使用该代理

acl OverConnLimit maxconn 16 #限制每个IP最大允许16个连接,防止攻击
http_access deny OverConnLimit

icp_access deny all #禁止从邻居服务器缓冲内发送和接收ICP请求.
miss_access allow all #允许直接更新请求
ident_lookup_access deny all #禁止lookup检查DNS
http_port 8080 transparent #指定Squid监听浏览器客户请求的端口号。

hierarchy_stoplist cgi-bin ? #用来强制某些特定的对象不被缓存,主要是处于安全的目的。
acl QUERY urlpath_regex cgi-bin \?
cache deny QUERY

cache_mem 1 GB #这是一个优化选项,增加该内存值有利于缓存。应该注意的是:
#一般来说如果系统有内存,设置该值为(n/)3M。现在是3G 所以这里1G
fqdncache_size 1024 #FQDN 高速缓存大小
maximum_object_size_in_memory 2 MB #允许最大的文件载入内存

memory_replacement_policy heap LFUDA #动态使用最小的,移出内存cache
cache_replacement_policy heap LFUDA #动态使用最小的,移出硬盘cache

cache_dir ufs /home/cache 5000 32 512 #高速缓存目录 ufs 类型使用的缓冲值最大允午1000MB空间,
#32个一级目录,512个二级目录

max_open_disk_fds 0 #允许最大打开文件数量,0 无限制
minimum_object_size 1 KB #允午最小文件请求体大小
maximum_object_size 20 MB #允午最大文件请求体大小

cache_swap_low 90 #最小允许使用swap 90%
cache_swap_high 95 #最多允许使用swap 95%

ipcache_size 2048 # IP 地址高速缓存大小 2M
ipcache_low 90 #最小允许ipcache使用swap 90%
ipcache_high 95 #最大允许ipcache使用swap 90%


access_log /var/log/squid/access.log squid #定义日志存放记录
cache_log /var/log/squid/cache.log squid
cache_store_log none #禁止store日志

emulate_httpd_log on #将使Squid仿照Web服务器的格式创建访问记录。如果希望使用
#Web访问记录分析程序,就需要设置这个参数。

refresh_pattern . 0 20% 4320 override-expire override-lastmod reload-into-ims ignore-reload #更新cache规则

acl buggy_server url_regex ^http://.... http:// #只允许http的请求
broken_posts allow buggy_server

acl apache rep_header Server ^Apache #允许apache的编码
broken_vary_encoding allow apache

request_entities off #禁止非http的标分准请求,防止攻击
header_access header allow all #允许所有的http报头
relaxed_header_parser on #不严格分析http报头.
client_lifetime 120 minute #最大客户连接时间 120分钟

cache_mgr sky@test.com #指定当缓冲出现问题时向缓冲管理者发送告警信息的地址信息。

cache_effective_user squid #这里以用户squid的身份Squid服务器
cache_effective_group squid

icp_port 0 #指定Squid从邻居服务器缓冲内发送和接收ICP请求的端口号。
#这里设置为0是因为这里配置Squid为内部Web服务器的加速器,
#所以不需要使用邻居服务器的缓冲。0是禁用

cache_peer 127.0.0.1 parent 80 0 no-query default multicast-responder no-netdb-exchange #cache_peer 设置允许更新缓存的主机,因是本机所以127.0.0.1
cache_peer_domain 127.0.0.1
hostname_aliases 127.0.0.1

error_directory /usr/share/squid/errors/Simplify_Chinese #定义错误路径

always_direct allow all # cache丢失或不存在是允许所有请求直接转发到原始服务器
ignore_unknown_nameservers on #开反DNS查询,当域名地址不相同时候,禁止访问
coredump_dir /var/log/squid #定义dump的目录

max_filedesc 2048 #最大打开的文件描述

half_closed_clients off #使Squid在当read不再返回数据时立即关闭客户端的连接。
#有时read不再返回数据是由于某些客户关闭TCP的发送数据
#而仍然保持接收数据。而Squid分辨不出TCP半关闭和完全关闭。

buffered_logs on #若打开选项“buffered_logs”可以稍稍提高加速某些对日志文件的写入,该选项主要是实现优化特性。

acl tianya referer_regex -i tianya #防止天涯盗链,转嫁给百度
http_access deny tianya
deny_info tianya

acl baidu req_header User-Agent Baiduspider #阻止baidu蜘蛛
http_access deny baidu

acl OverConnLimit maxconn 128 #限制同一IP客户端的最大连接数
http_access deny OverConnLimit

acl myip dst 222.18.63.37 #防止被人利用为HTTP代理,设置允许访问的IP地址
http_access deny !myip

acl Manager proto cache_object #允许本地管理
acl Localhost src 127.0.0.1 222.18.63.37
http_access allow Manager Localhost
cachemgr_passwd 53034338 all
http_access deny Manager

acl all src 0.0.0.0/0.0.0.0 #仅仅允许80端口的代理
acl Safe_ports port 80 # http
http_access deny !Safe_ports
http_access allow all

visible_hostname happy.swjtu.edu.cn #Squid信息设置
cache_mgr ooopic2008@qq.com

cache_effective_user squid #基本设置
cache_effective_group squid
tcp_recv_bufsize 65535 bytes

cache_peer 127.0.0.1 parent 80 0 no-query originserver #2.6的反向代理加速配置

error_directory /usr/local/squid/share/errors/Simplify_Chinese #错误文档

icp_port 0 #单台使用,不使用该功能

hierarchy_stoplist cgi-bin ?

acl QUERY urlpath_regex cgi-bin \? .php .cgi .avi .wmv .rm .ram .mpg .mpeg .zip .exe
cache deny QUERY

acl apache rep_header Server ^Apache
broken_vary_encoding allow apache


refresh_pattern ^ftp: 1440 20% 10080
refresh_pattern ^gopher: 1440 0% 1440
refresh_pattern . 0 20% 4320

cache_store_log none
pid_filename /usr/local/squid/var/logs/squid.pid
emulate_httpd_log on
logformat combined %>a %ui %un [%tl] "%rm %ru HTTP/%rv" %Hs %<st "%{Referer}>h" "%{User-Agent}>h" %Ss:%Sh
cache_log /usr/local/squid/var/logs/cache.log
access_log /usr/local/squid/var/logs/access.log combined
coredump_dir /usr/local/squid/var/cache
cache_dir ufs /usr/local/squid/var/cache 10000 16 256

dns_children 32
hosts_file /etc/hosts

cache_mem 400 MB
cache_swap_low 90
cache_swap_high 95
maximum_object_size 32768 KB
maximum_object_size_in_memory 4096 KB
emulate_httpd_log on

acl picurl url_regex -i \.bmp$ \.png$ \.jpg$ \.gif$ \.jpeg$ #防止盗链
acl mystie1 referer_regex -i happy.swjtu.edu.cn
http_access allow mystie1 picurl
acl nullref referer_regex -i ^$
http_access allow nullref
acl hasref referer_regex -i .+
http_access deny hasref picurl
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/31/index.html b/en/page/31/index.html new file mode 100644 index 0000000000..2dfff2c1d2 --- /dev/null +++ b/en/page/31/index.html @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

插件安装

Prettier - Code formatter

格式化工具

+

ESLint

校验规则

+

Vetur

vue代码片段及代码美化

+

Vue 2 Snippets

vue2 代码片段

+

vscode-fileheader

文件注释

+

配置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// setting.json vscode配置文件
{
"workbench.startupEditor": "newUntitledFile",
"terminal.integrated.shell.windows": "C:\\Program Files\\Git\\bin\\bash.exe", \\ 自行配置自己的环境地址
"javascript.updateImportsOnFileMove.enabled": "always",
"editor.tabSize": 2,
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true
},
"sync.gist": "748b4cae5eb6e56d6997978ead096e8f",
"breadcrumbs.enabled": true,
"todohighlight.isEnable": false,
"liveServer.settings.donotShowInfoMsg": true,
"search.location": "sidebar",
"workbench.activityBar.visible": true,
"window.menuBarVisibility": "default",
"workbench.statusBar.visible": true,
"editor.snippetSuggestions": "top",
"editor.formatOnPaste": true,
"workbench.colorTheme": "Tiny Light",
"eslint.validate": [
"javascript",
"javascriptreact",
{
"language": "html",
"autoFix": true
},
{
"language": "vue",
"autoFix": true
}
],
"prettier.eslintIntegration": true,
"files.autoSave": "onWindowChange",
"code-runner.saveAllFilesBeforeRun": true,
"vetur.format.defaultFormatter.html": "js-beautify-html",
"vetur.format.defaultFormatter.js": "vscode-typescript",
"prettier.jsxSingleQuote": true,
"prettier.requireConfig": false,
"prettier.arrowParens": "always",
"typescript.format.insertSpaceAfterSemicolonInForStatements": false,
"prettier.stylelintIntegration": true,
"prettier.singleQuote": true,
"prettier.tslintIntegration": true,
"eslint.provideLintTask": true,
"eslint.autoFixOnSave": true,
"editor.mouseWheelZoom": true,
"editor.tabCompletion": "on",
"editor.formatOnType": true,
"eslint.alwaysShowStatus": true,
"eslint.options": {
"configFile": "E:/project/xxjs/fore-core/.eslintrc.js" // 自行配置自己的项目地址
},
"fileheader.Author": "Li.Xg", // 自行配置自己的名称
"fileheader.LastModifiedBy": "Li.Xg" // 自行配置自己的名称
}
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/32/index.html b/en/page/32/index.html new file mode 100644 index 0000000000..c67d5f70d7 --- /dev/null +++ b/en/page/32/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

EventLoop 翻译过来就是事件循环,那啥是事件循环啊,这个要从javascript是如何执行的说起。
要知道javascript是一个运行在浏览器或者Node环境的单线程执行的脚本语言,但是为了解决单线程阻塞的问题,这里就引入了事件循环的机制,也就是我们常说的异步特性。

+

但是虽然都是JavaScript,都是事件循环,都有异步特性,但浏览器环境和Node环境的实现方式是不一样的,这里要先说明一下。
浏览器的事件循环是HTML定义的规范,而Node环境是利用libuv库实现的,这里我们先从浏览器的事件循环开始说。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/33/index.html b/en/page/33/index.html new file mode 100644 index 0000000000..d7ac31db9c --- /dev/null +++ b/en/page/33/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

今天一个小兄弟微信给我发了个消息,说是今天面试,面试官问了个比较基础的问题,没答好,我就问他问了什么,他说,数组有哪些方法会改变原来的值,他说他就说了3、4个,还不确定对不对,我想了想说其实列个表就很容易记住了,于是便有了下面这个表格。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/34/index.html b/en/page/34/index.html new file mode 100644 index 0000000000..3c9e3ba8c3 --- /dev/null +++ b/en/page/34/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

还记得你的前端工程里有这样一个文件‘package.json’,里面管理者运行代码所需要的各种依赖,每种依赖都会有一个版本号,那么你知道版本号是怎么定义的么?不知道的话就看看下面的内容吧。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/35/index.html b/en/page/35/index.html new file mode 100644 index 0000000000..cbb2235f87 --- /dev/null +++ b/en/page/35/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/36/index.html b/en/page/36/index.html new file mode 100644 index 0000000000..25d46956be --- /dev/null +++ b/en/page/36/index.html @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

在移动 App 开发领域,有一个框架非常知名,每每面试都会被问起,“你用过 Ionic 么?”,是的,就是 Ionic 框架。

+

之前已经学习过了 Cordova ,它和 Ionic 可以说是一对黄金搭档,一个面向 MobileOS ,一个面向 UI ,很多人搞不清楚它们之间的区别,没关系,回头我再抽时间讲一下,今天我们主要来认识一下 Ionic

+

好戏开始:)

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/37/index.html b/en/page/37/index.html new file mode 100644 index 0000000000..a0cfe57e00 --- /dev/null +++ b/en/page/37/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

很长时间没有写博客了,因为最近换了工作,原来的技术栈基本上都换了,现在接触了很多新的技术栈,Cordova 就是其中之一。

+

接下来这篇文章就是我的学习笔记,以供回顾,写的不好请勿拍砖。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/38/index.html b/en/page/38/index.html new file mode 100644 index 0000000000..76f5e4cb95 --- /dev/null +++ b/en/page/38/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/39/index.html b/en/page/39/index.html new file mode 100644 index 0000000000..0ae281718c --- /dev/null +++ b/en/page/39/index.html @@ -0,0 +1,523 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

/bin:

bin是Binary的缩写, 这个目录存放着最经常使用的命令。

+

/boot:

这里存放的是启动Linux时使用的一些核心文件,包括一些连接文件以及镜像文件。

+

/dev :

dev是Device(设备)的缩写, 该目录下存放的是Linux的外部设备,在Linux中访问设备的方式和访问文件的方式是相同的。

+

/etc:

这个目录用来存放所有的系统管理所需要的配置文件和子目录。

+

/home:

用户的主目录,在Linux中,每个用户都有一个自己的目录,一般该目录名是以用户的账号命名的。

+

/lib:

这个目录里存放着系统最基本的动态连接共享库,其作用类似于Windows里的DLL文件。几乎所有的应用程序都需要用到这些共享库。

+

/lost+found:

这个目录一般情况下是空的,当系统非法关机后,这里就存放了一些文件。

+

/media linux

系统会自动识别一些设备,例如U盘、光驱等等,当识别后,linux会把识别的设备挂载到这个目录下。

+

/mnt:

系统提供该目录是为了让用户临时挂载别的文件系统的,我们可以将光驱挂载在/mnt/上,然后进入该目录就可以查看光驱里的内容了。

+

/opt:

这是给主机额外安装软件所摆放的目录。比如你安装一个ORACLE数据库则就可以放到这个目录下。默认是空的。

+

/proc:

这个目录是一个虚拟的目录,它是系统内存的映射,我们可以通过直接访问这个目录来获取系统信息。
这个目录的内容不在硬盘上而是在内存里,我们也可以直接修改里面的某些文件,比如可以通过下面的命令来屏蔽主机的ping命令,使别人无法ping你的机器:

+

/root:

该目录为系统管理员,也称作超级权限者的用户主目录。

+

/sbin:

s就是Super User的意思,这里存放的是系统管理员使用的系统管理程序。

+

/selinux:

这个目录是Redhat/CentOS所特有的目录,Selinux是一个安全机制,类似于windows的防火墙,但是这套机制比较复杂,这个目录就是存放selinux相关的文件的。

+

/srv:

该目录存放一些服务启动之后需要提取的数据。

+

/sys:

这是linux2.6内核的一个很大的变化。该目录下安装了2.6内核中新出现的一个文件系统 sysfs 。

+

sysfs文件系统集成了下面3种文件系统的信息:针对进程信息的proc文件系统、针对设备的devfs文件系统以及针对伪终端的devpts文件系统。
该文件系统是内核设备树的一个直观反映。

+

当一个内核对象被创建的时候,对应的文件和目录也在内核对象子系统中被创建。

+

/tmp:

这个目录是用来存放一些临时文件的。

+

/usr:

这是一个非常重要的目录,用户的很多应用程序和文件都放在这个目录下,类似与windows下的program files目录。

+

/usr/bin:

系统用户使用的应用程序。

+

/usr/sbin:

超级用户使用的比较高级的管理程序和系统守护程序。

+

/usr/src:

内核源代码默认的放置目录。

+

/var:

这个目录中存放着在不断扩充着的东西,我们习惯将那些经常被修改的目录放在这个目录下。包括各种日志文件。

+

在linux系统中,有几个目录是比较重要的,平时需要注意不要误删除或者随意更改内部文件。
系统上跑了很多程序,那么每个程序都会有相应的日志产生,而这些日志就被记录到这个目录下,具体在/var/log 目录下,另外mail的预设放置也是在这里。

+

/etc:

上边也提到了,这个是系统中的配置文件,如果你更改了该目录下的某个文件可能会导致系统不能启动。

+

/bin, /sbin, /usr/bin, /usr/sbin:

这是系统预设的执行文件的放置目录,比如 ls 就是在/bin/ls 目录下的。

+

值得提出的是,/bin, /usr/bin 是给系统用户使用的指令(除root外的普通用户),而/sbin, /usr/sbin 则是给root使用的指令。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/4/index.html b/en/page/4/index.html new file mode 100644 index 0000000000..f50d06c3ce --- /dev/null +++ b/en/page/4/index.html @@ -0,0 +1,499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

初始化数据库

c:\pgsql\bin\initdb.exe -D c:\pgsql\data -E UTF8

+

注册表

c:\pgsql\bin\pg_ctl register -N postgres -D c:\pgsql\data

+

启动服务

c:\pgsql\bin\postgres -D c:\pgsql\data

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/40/index.html b/en/page/40/index.html new file mode 100644 index 0000000000..0a41292c00 --- /dev/null +++ b/en/page/40/index.html @@ -0,0 +1,576 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

安装

sudo rpm -ivh [lz4-1.8.3-1.el7.x86_64.rpm]

+

卸载

sudo rpm -e [postgresql14-server-14.4-1PGDG.rhel7.x86_64]

+

查询功能

命令格式 rpm {-q|–query} [select-options] [query-options]

+

  RPM的查询功能是极为强大,是极为重要的功能之一;举几个常用的例子,更为详细的具体的,请参考#man rpm

+

1、对系统中已安装软件的查询

+

1)查询系统已安装的软件

+

  语法:rpm -q 软件名

+

  举例:[root@localhost beinan]# rpm -q gaim

+

  gaim-1.3.0-1.fc4   

+
   查看系统中所有已经安装的包,要加 -a 参数 ;
+
+

  [root@localhost RPMS]# rpm -qa

+

  如果分页查看,再加一个管道 |和more命令;

+

  [root@localhost RPMS]# rpm -qa |more

+

  在所有已经安装的软件包中查找某个软件,比如说 gaim ;可以用 grep 抽取出来;

+

  [root@localhost RPMS]# rpm -qa |grep gaim

+

  上面这条的功能和 rpm -q gaim 输出的结果是一样的;

+

2)查询一个已经安装的文件属于哪个软件包

+

  语法 rpm -qf 文件名

+

  注:文件名所在的绝对路径要指出

+

  举例:

+

  [root@localhost RPMS]# rpm -qf /usr/lib/libacl.la

+

  libacl-devel-2.2.23-8

+

3)查询已安装软件包都安装到何处

+

  语法:rpm -ql 软件名 或 rpm rpmquery -ql 软件名

+

  举例:

+

  [root@localhost RPMS]# rpm -ql lynx

+

  [root@localhost RPMS]# rpmquery -ql lynx

+

4)查询一个已安装软件包的信息

+

  语法格式: rpm -qi 软件名

+

  举例:

+

  [root@localhost RPMS]# rpm -qi lynx

+

5)查看一下已安装软件的配置文件

+

  语法格式:rpm -qc 软件名

+

  举例:

+

  [root@localhost RPMS]# rpm -qc lynx

+

6)查看一个已经安装软件的文档安装位置

+

  语法格式: rpm -qd 软件名

+

  举例:

+

  [root@localhost RPMS]# rpm -qd lynx

+

7)查看一下已安装软件所依赖的软件包及文件

+

  语法格式: rpm -qR 软件名

+

  举例:

+

  [root@localhost beinan]# rpm -qR rpm-python

+

  查询已安装软件的总结:对于一个软件包已经安装,我们可以把一系列的参数组合起来用;比如 rpm -qil ;比如:

+

  [root@localhost RPMS]# rpm -qil lynx

+

2、对于未安装的软件包的查看:

+

  查看的前提是您有一个.rpm 的文件,也就是说对既有软件file.rpm的查看等;

+

1)查看一个软件包的用途、版本等信息;

+

  语法: rpm -qpi file.rpm

+

  举例:

+

  [root@localhost RPMS]# rpm -qpi lynx-2.8.5-23.i386.rpm

+

2)查看一件软件包所包含的文件;

+

  语法: rpm -qpl file.rpm

+

  举例:

+

  [root@localhost RPMS]# rpm -qpl lynx-2.8.5-23.i386.rpm

+

3)查看软件包的文档所在的位置;

+

  语法: rpm -qpd file.rpm

+

  举例:

+

  [root@localhost RPMS]# rpm -qpd lynx-2.8.5-23.i386.rpm

+

4)查看一个软件包的配置文件;

+

  语法: rpm -qpc file.rpm

+

  举例:

+

  [root@localhost RPMS]# rpm -qpc lynx-2.8.5-23.i386.rpm

+

5)查看一个软件包的依赖关系

+

  语法: rpm -qpR file.rpm

+

  举例:

+

  [root@localhost archives]# rpm -qpR yumex_0.42-3.0.fc4_noarch.rpm

+

  /bin/bash

+

  /usr/bin/python

+

  config(yumex) = 0.42-3.0.fc4

+

  pygtk2

+

  pygtk2-libglade

+

  rpmlib(CompressedFileNames) <= 3.0.4-1

+

  rpmlib(PayloadFilesHavePrefix) <= 4.0-1

+

  usermode

+

  yum >= 2.3.2

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/41/index.html b/en/page/41/index.html new file mode 100644 index 0000000000..ce73dc24bf --- /dev/null +++ b/en/page/41/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

前言

最近在做代码风格统一,用到了Prettier,暂时先记录一下现在的配置信息,回头抽时间写篇总结文。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/42/index.html b/en/page/42/index.html new file mode 100644 index 0000000000..da7925144f --- /dev/null +++ b/en/page/42/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

今天在看一个叫做Avue的框架组件时看到了一个叫做“全局水印”的组件,功能呢就是给当前页面加上一个水印遮罩,就像下图这样:
Y08cbd.png
因为工作中有这样的场景,所以我很好奇看了一下他的代码,他的水印设置为了fixed定位,并且z-index设置的很高,但是它具有穿透性,最终我发现其中起作用的是一个叫做“pointer-events”的css3新属性,于是为了搞清楚它的作用,我在网上做了一番搜索,并把自己的认识整理一下。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/43/index.html b/en/page/43/index.html new file mode 100644 index 0000000000..447b0a1231 --- /dev/null +++ b/en/page/43/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在阅读一些文章时,经常看到介绍命令的时候用到了 npx 关键字,之前知道有 node 有 npm 但是很少用到 npx ,npx是个啥呢?好奇上网上找了些资料学习了一下,写篇学习笔记记录一下。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/44/index.html b/en/page/44/index.html new file mode 100644 index 0000000000..c589b9a969 --- /dev/null +++ b/en/page/44/index.html @@ -0,0 +1,510 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

市面上目前已有各种各样的UI组件库,比如 Element 和 iView,他们的强大毋庸置疑。但是我们面临的情况是需求越来越复杂,当它们不能再满足我们需求的时候,这个时候就有必要开发一套属于自己团队的组件库了。

+

所以本文的目的就是让读者能通过此文,小能做一个简单的插件供人使用,大能架构和维护一个组件库不在话下。

+

以下一个简单的颜色选择器插件vColorPicker讲述从开发到上线到npm的流程。

+

vColorPicker 插件 DEMO

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/45/index.html b/en/page/45/index.html new file mode 100644 index 0000000000..59d57e3e0b --- /dev/null +++ b/en/page/45/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

现代前端开发不用点预编译css都不好意思说自己咋写样式,虽说这么说有点夸张,但是确实是一个普遍现象吧,我们在开发中常见到的预编译css的话主要是sass和less,那么今天的主角就是其中的sass,可以说sass让人又爱又恨,爱呢是喜欢她赋予了css函数的特性,恨呢就是用的时候实在是麻烦,所以有的人就投奔了less的怀抱,他俩很像,但是有的项目已经用了sass那也没办法,嫁鸡随鸡,但是日子还得过,想办法过的好一点嘛。

+

其实这里说到sass用起来麻烦主要还是在安装依赖的时候,想必用过的人都知道我说的是啥,原来只能是不停的重试,最近在知乎上看到篇文章,聪明的解决了这个问题,所以赶紧学习记录下来,学会了就是自己的。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/46/index.html b/en/page/46/index.html new file mode 100644 index 0000000000..24856033e2 --- /dev/null +++ b/en/page/46/index.html @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

知乎真是个好地方,关注好自己喜欢的话题,经常会推送一些好的话题到你面前,这不刚刚就看到了一个人提问

+
+

前端全栈和大前端有啥区别

+
+

以前只听说过全栈,听说过大前端,但是没听说过前端全栈,对于前两个我自己也有一些理解,但从不敢说自己理解的对,带着强烈的好奇心我打开了这个问题,也看到了一位前端大神‘狼叔’的回复,感觉还是很到位的,也让我重新验证了自己的理解,所以就将狼叔的解答与我自己的认识相结合一下,做个记录。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/47/index.html b/en/page/47/index.html new file mode 100644 index 0000000000..b873b2427e --- /dev/null +++ b/en/page/47/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/48/index.html b/en/page/48/index.html new file mode 100644 index 0000000000..a3fbc7ff87 --- /dev/null +++ b/en/page/48/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在检查公司前端代码质量时,经常会在sonar检查到有漏洞级别的问题特别多,但是打开查看详情的时候发现又特别低级,都是些console没去掉啊,debugger没去掉啊,之前都是看到了就提醒一下,但是屡禁不止,那么有没有什么好的办法能够从源头上避免这个问题呢,果然这个问题不止我一个人遇到,网上还是有大神给出了解决方案,那就是 webpack 打包时可以引入 UglifyJsPlugin 来解决这个扰人的问题,而且屏蔽了人为的因素,使用感觉是极好的,所以,写个帖子把网上学来的资料整理一下。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/49/index.html b/en/page/49/index.html new file mode 100644 index 0000000000..f84f019a5f --- /dev/null +++ b/en/page/49/index.html @@ -0,0 +1,506 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

目前,前端领域中 React 势头正盛,使用者众多却少有能够深入剖析内部实现机制和原理。本系列文章希望通过剖析 React 源码,理解其内部的实现原理,知其然更要知其所以然。

+

React diff 作为 Virtual DOM 的加速器,其算法上的改进优化是 React 整个界面渲染的基础,以及性能提高的保障,同时也是 React 源码中最神秘、最不可思议的部分,本文从源码入手,深入剖析 React diff 的不可思议之处。

+

阅读本文需要对 React 有一定的了解,如果你不知何为 React,请详读 React 官方文档
如果你对 React diff 存在些许疑惑,或者你对算法优化感兴趣,那么本文值得阅读和讨论。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/5/index.html b/en/page/5/index.html new file mode 100644 index 0000000000..068cae46f4 --- /dev/null +++ b/en/page/5/index.html @@ -0,0 +1,565 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Nginx中的超时设置

client_header_timeout 指定等待client发送一个请求头的超时时间

    +
  • 语法: client_header_timeout time
  • +
  • 默认值: 60s
  • +
  • 上下文: http server
  • +
  • 说明: 指定等待client发送一个请求头的超时时间(例如:GET / HTTP/1.1).仅当在一次read中,没有收到请求头,才会算成超时。如果在超时时间内,client没发送任何东西,nginx返回HTTP状态码408(“Request timed out”)
  • +
+

client_body_timeout 该指令设置请求体(request body)的读超时时间

    +
  • 语法: client_body_timeout time
  • +
  • 默认值: 60s
  • +
  • 上下文: http server location
  • +
  • 说明: 该指令设置请求体(request body)的读超时时间。仅当在一次readstep中,没有得到请求体,就会设为超时。超时后,nginx返回HTTP状态码408(“Request timed out”)
  • +
+

keepalive_timeout 第一个参数指定了与client的keep-alive连接超时时间

    +
  • 语法: keepalive_timeout timeout [ header_timeout ]

    +
  • +
  • 默认值: 75s

    +
  • +
  • 上下文: http server location

    +
  • +
  • 说明: 第一个参数指定了与client的keep-alive连接超时时间。服务器将会在这个时间后关闭连接。可选的第二个参数指定了在响应头Keep-Alive: timeout=time中的time值。这个头能够让一些浏览器主动关闭连接,这样服务器就不必要去关闭连接了。没有这个参数,nginx不会发送Keep-Alive响应头(尽管并不是由这个头来决定连接是否“keep-alive”)
    两个参数的值可并不相同

    +
      +
    • 注意不同浏览器怎么处理“keep-alive”头
    • +
    • MSIE和Opera忽略掉”Keep-Alive: timeout=“ header.
    • +
    • MSIE保持连接大约60-65秒,然后发送TCP RST
    • +
    • Opera永久保持长连接
    • +
    • Mozilla keeps the connection alive for N plus about 1-10 seconds.
    • +
    • Konqueror保持长连接N秒
    • +
    +
  • +
+

lingering_timeout

    +
  • 语法: lingering_timeout time
  • +
  • 默认值: 5s
  • +
  • 上下文: http server location
  • +
  • 说明: lingering_close生效后,在关闭连接前,会检测是否有用户发送的数据到达服务器,如果超过lingering_timeout时间后还没有数据可读,就直接关闭连接;否则,必须在读取完连接缓冲区上的数据并丢弃掉后才会关闭连接。
  • +
+

resolver_timeout 设置DNS解析超时时间

    +
  • 语法 resolver_timeout time
  • +
  • 默认值 30s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置DNS解析超时时间
  • +
+

proxy_connect_timeout 设置与upstream server的连接超时时间,不能超过75秒

    +
  • 语法 proxy_connect_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置与upstream server的连接超时时间,有必要记住,这个超时不能超过75秒。
    这个不是等待后端返回页面的时间,那是由proxy_read_timeout声明的。如果你的upstream服务器起来了,但是hanging住了(例如,没有足够的线程处理请求,所以把你的请求放到请求池里稍后处理),那么这个声明是没有用的,由于与upstream服务器的连接已经建立了。
  • +
+

proxy_read_timeout 设置与代理服务器的读超时时间

    +
  • 语法 proxy_read_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置与代理服务器的读超时时间。它决定了nginx会等待多长时间来获得请求的响应。这个时间不是获得整个response的时间,而是两次reading操作的时间。
  • +
+

proxy_send_timeout 设置发送请求给upstream服务器的超时时间

    +
  • 语法 proxy_send_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 这个指定设置了发送请求给upstream服务器的超时时间。超时设置不是为了整个发送期间,而是在两次write操作期间。如果超时后,upstream没有收到新的数据,nginx会关闭连接
  • +
+

proxy_upstream_fail_timeout(fail_timeout)设置某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间

    +
  • 语法 server address [fail_timeout=30s]
  • +
  • 默认值 10s
  • +
  • 上下文 upstream
  • +
  • 说明 Upstream模块下 server指令的参数,设置了某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间,默认为10秒
  • +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/50/index.html b/en/page/50/index.html new file mode 100644 index 0000000000..2b31976753 --- /dev/null +++ b/en/page/50/index.html @@ -0,0 +1,784 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

推荐序

这里是我自己写的,看了这篇文章把我零碎的 webpack 知识系统的整理了一下,感觉受益匪浅,推荐更多小伙伴看一看这篇文章,好文共享,建议稍微对 webpack 了解服用更佳。
2020年了,再不会webpack敲得代码就不香了(近万字实战)

+

前言

2020年即将到来,在众多前端的招聘要求里, webpack 、工程化这些字眼频率越来越高。日常开发者中,我们常常在用诸如 vue-clicreate-react-app 的脚手架来构建我们的项目。但是如果你想在团队脱颖而出(鹤立鸡群)、拿到更好的 offer (还房贷),那么你必须去深刻的认识下我们经常打交道的 webpack

+

入门(一起来用这些小例子让你熟悉webpack的配置)

webpack 是什么?

webpack 是一个现代 JavaScript 应用程序的静态模块打包器,当 webpack 处理应用程序时,会递归构建一个依赖关系图,其中包含应用程序需要的每个模块,然后将这些模块打包成一个或多个 bundle

+

webpack 的核心概念

    +
  • entry: 入口
  • +
  • output: 输出
  • +
  • loader: 模块转换器,用于把模块原内容按照需求转换成新内容
  • +
  • plugins: 扩展插件,在 webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要做的事情
  • +
+

初始化项目

新建一个目录,初始化 npm

+
1
npm init
+ +

webpack 是运行在 node 环境中的,我们需要安装以下两个 npm

+
1
npm i -D webpack webpack-cli
+ +
+
    +
  • npm i -Dnpm install --save-dev 的缩写
  • +
  • npm i -Snpm install --save 的缩写
  • +
+
+

新建一个文件夹 src ,然后新建一个文件 main.js ,写一点代码测试一下

+
1
console.log('call me 老yuan')
+ +

配置 package.json 命令

+
1
2
3
"script":{
"build":"webpack src/main.js"
}
+

执行

+
1
npm run build
+

此时如果生成了一个 dist 文件夹,并且内部含有 main.js 说明已经打包成功了

+

开始我们自己的配置

上面一个简单的例子只是 webpack 自己默认的配置,下面我们要实现更加丰富的自定义配置

+

新建一个 build 文件夹,里面新建一个 webpack.config.js

+
1
2
3
4
5
6
7
8
9
10
11
// webpack.config.js

const path = require('path');
module.exports = {
mode:'development', // 开发模式
entry: path.resolve(__dirname,'../src/main.js'), // 入口文件
output: {
filename: 'output.js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
}
}
+

更改我们的打包命令

+
1
2
3
"script":{
"build":"webpack build/webpack.config.js"
}
+

执行 npm run build
会发现生成了以下目录

+
1
2
3
4
project
dist
build
src
+

其中 dist 文件夹中的 main.js 就是我们需要在浏览器中实际运行的文件
当然实际运用中不会仅仅如此,下面让我们通过实际案例带你快速入手 webpack

+

配置html模板

js 文件打包好了,但是我们不可能每次在html文件中手动引入打包好的js

+
+

这里可能有的朋友会认为我们打包 js 文件名称不是一直是固定的嘛( output.js )?这样每次就不用改动引入文件名称了呀?实际上我们日常开发中往往会这样配置:

+
+
1
2
3
4
5
6
7
module.exports = {
// 省略其他配置
output: {
filename: '[name].[hash:8].js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
}
}
+ +

这时候生成的 dist 目录文件如下

+
1
2
dist/
app.fsafasf.js
+

为了缓存,你会发现打包好的 js 文件的名称每次都不一样。 webpack 打包出来的js文件我们需要引入到 html 中,但是每次我们都手动修改 js 文件名显得很麻烦,因此我们需要一个插件来帮我们完成这件事情,那就是 html-webpack-plugin

+
1
npm i -D html-webpack-plugin
+

新建一个 build 同级的文件夹 public ,里面新建一个 index.html
具体配置文件如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin') // **此插件用来解决html模板引入js文件
module.exports = {
mode:'development', // 开发模式
entry: path.resolve(__dirname,'../src/main.js'), // 入口文件
output: {
filename: '[name].[hash:8].js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
},
plugins:[
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html')
})
]
}
+

可以发现打包生成的js文件已经被自动引入 html 文件中

+

多入口文件如何开发

+

生成多个 html-webpack-plugin 实例来解决这个问题

+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode:'development', // 开发模式
entry: {
main:path.resolve(__dirname,'../src/main.js'),
header:path.resolve(__dirname,'../src/header.js')
},
output: {
filename: '[name].[hash:8].js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
},
plugins:[
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html'),
filename:'index.html',
chunks:['main'] // 与入口文件对应的模块名
}),
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/header.html'),
filename:'header.html',
chunks:['header'] // 与入口文件对应的模块名
}),
]
}
+ +

clean-webpack-plugin

+

每次执行 npm run build 会发现 dist 文件夹里会残留上次打包的文件,这里我们推荐一个 plugin 来帮我们在打包输出前清空文件夹 clean-webpack-plugin

+
+
1
2
3
4
5
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
module.exports = {
// ...省略其他配置
plugins:[new CleanWebpackPlugin()]
}
+
希望dist目录下某个文件夹不被清空

不过呢,有些时候,我们并不希望整个 dist 目录都被清空,比如,我们不希望,每次打包的时候,都删除 dll 目录,以及 dll 目录下的文件或子目录,该怎么办呢?

+

clean-webpack-plugin 为我们提供了参数 cleanOnceBeforeBuildPatterns

+
1
2
3
4
5
6
7
8
9
//webpack.config.js
module.exports = {
//...
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns:['**/*', '!dll', '!dll/**'] //不删除dll目录下的文件
})
]
}
+ +

引用CSS

我们的入口文件是 js ,所以我们在入口 js 中引入我们的 css 文件

+
1
import 'asset/style.css'
+

同时我们也需要一些 loader 来解析我们的 css 文件

+
1
npm i -D style-loader css-loader
+

如果我们使用 less 来构建样式,则需要多安装两个

+
1
npm i -D less less-loader
+

配置文件如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.config.js
module.exports = {
// ...省略其他配置
module:{
rules:[
{
test:/\.css$/,
use:['style-loader','css-loader'] // 从右向左解析原则
},
{
test:/\.less$/,
use:['style-loader','css-loader','less-loader'] // 从右向左解析原则
}
]
}
}
+ +

我们简单说一下上面的配置:

+
    +
  • style-loader 动态创建 style 标签,将 css 插入到 head 中.
  • +
  • css-loader 负责处理 @import 等语句。
  • +
  • postcss-loaderautoprefixer ,自动生成浏览器兼容性前缀 —— 2020了,应该没人去自己徒手去写浏览器前缀了吧
  • +
  • less-loader 负责处理编译 .less 文件,将其转为 css
  • +
+
+

注意:
loader 的执行顺序是从右向左执行的,也就是后面的 loader 先执行,上面 loader 的执行顺序为: less-loader —> postcss-loader —> css-loader —> style-loader
当然,loader 其实还有一个参数,可以修改优先级,enforce 参数,其值可以为: pre(优先执行) 或 post (滞后执行)。
现在,我们已经可以处理 .less 文件啦,.css 文件只需要修改匹配规则,删除 less-loader 即可。

+
+

为css添加浏览器前缀

1
2
npm i -D postcss-loader autoprefixer

+

配置如下

+
1
2
3
4
5
6
7
8
9
// webpack.config.js
module.exports = {
module:{
rules:[
test/\.less$/,
use:['style-loader','css-loader','postcss-loader','less-loader'] // 从右向左解析原则
]
}
}
+

接下来,我们还需要引入 autoprefixer 使其生效,这里有两种方式

+
在项目根目录下创建一个postcss.config.js文件,配置如下:
1
2
3
module.exports = {
plugins: [require('autoprefixer')] // 引用该插件即可了
}
+
直接在webpack.config.js里配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// webpack.config.js
module.exports = {
//...省略其他配置
module:{
rules:[{
test:/\.less$/,
use:['style-loader','css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
},'less-loader'] // 从右向左解析原则
}]
}
}
+

这时候我们发现 css 通过 style 标签的方式添加到了 html 文件中,但是如果样式文件很多,全部添加到 html 中,难免显得混乱。这时候我们想用把 css 拆分出来用外链的形式引入 css 文件怎么做呢?这时候我们就需要借助插件来帮助我们

+

拆分css

1
npm i -D mini-css-extract-plugin
+
+

webpack 4.0 以前,我们通过 extract-text-webpack-plugin 插件,把 css 样式从 js 文件中提取到单独的 css 文件中。 webpack4.0 以后,官方推荐使用 mini-css-extract-plugin 插件来打包 css 文件

+
+

配置文件如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
//...省略其他配置
module: {
rules: [
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader'
],
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].[hash].css",
chunkFilename: "[id].css",
})
]
}
+

拆分多个css

+

这里需要说的细一点,上面我们所用到的 mini-css-extract-plugin 将所有的 css 样式合并为一个 css 文件。如果你想拆分为一一对应的多个 css 文件,我们需要使用到 extract-text-webpack-plugin ,而目前 mini-css-extract-plugin 还不支持此功能。我们需要安装 @next 版本的 extract-text-webpack-plugin

+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
npm i -D extract-text-webpack-plugin@next
// webpack.config.js

const path = require('path');
const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')
let indexLess = new ExtractTextWebpackPlugin('index.less');
let indexCss = new ExtractTextWebpackPlugin('index.css');
module.exports = {
module:{
rules:[
{
test:/\.css$/,
use: indexCss.extract({
use: ['css-loader']
})
},
{
test:/\.less$/,
use: indexLess.extract({
use: ['css-loader','less-loader']
})
}
]
},
plugins:[
indexLess,
indexCss
]
}
+

打包 图片、字体、媒体、等文件

file-loader 就是将文件在进行一些处理后(主要是处理文件名和路径、解析文件 url ),并将文件移动到输出的目录中
url-loader 一般与 file-loader 搭配使用,功能与 file-loader 类似,如果文件小于限制的大小。则会返回 base64 编码,否则使用 file-loader 将文件移动到输出的目录中

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// webpack.config.js
module.exports = {
// 省略其它配置 ...
module: {
rules: [
// ...
{
test: /\.(jpe?g|png|gif)$/i, //图片文件
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]'
}
}
}
}
]
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, //媒体文件
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'media/[name].[hash:8].[ext]'
}
}
}
}
]
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i, // 字体
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'fonts/[name].[hash:8].[ext]'
}
}
}
}
]
},
]
}
}
+

用babel转义js文件

为了使我们的 js 代码兼容更多的环境我们需要安装依赖

+
1
2
npm i babel-loader @babel/preset-env @babel/core

+
+

注意
babel-loaderbabel-core 的版本对应关系

+
+
    +
  • babel-loader 8.x 对应 babel-core 7.x
  • +
  • babel-loader 7.x 对应 babel-core 6.x
  • +
+

配置如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// webpack.config.js
module.exports = {
// 省略其它配置 ...
module:{
rules:[
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:['@babel/preset-env']
}
},
exclude:/node_modules/
},
]
}
}
+

上面的 babel-loader 只会将 ES6/7/8 语法转换为 ES5 语法,但是对新 api 并不会转换 例如( promiseGeneratorSetMapsProxy 等)
此时我们需要借助 babel-polyfill 来帮助我们转换

+
1
2
3
4
5
6
npm i @babel/polyfill
// webpack.config.js
const path = require('path')
module.exports = {
entry: ["@babel/polyfill,path.resolve(__dirname,'../src/index.js')"], // 入口文件
}
+
+

手动把上面的 demo 敲一遍对阅读下面的文章更有益,建议入门的同学敲三遍以上

+
+

上面的实践是我们对 webpack 的功能有了一个初步的了解,但是要想熟练应用于开发中,我们需要一个系统的实战。让我们一起摆脱脚手架尝试自己搭建一个 vue 开发环境

+

搭建vue开发环境

上面的小例子已经帮助而我们实现了打包 css 、图片、 jshtml 等文件。
但是我们还需要以下几种配置

+

解析.vue文件

1
2
npm i -D vue-loader vue-template-compiler vue-style-loader
npm i -S vue
+
    +
  • vue-loader 用于解析 .vue 文件
  • +
  • vue-template-compiler 用于编译模板
  • +
+

配置如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const vueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
module:{
rules:[{
test:/\.vue$/,
use:['vue-loader']
},]
},
resolve:{
alias:{
'vue$':'vue/dist/vue.runtime.esm.js',
' @':path.resolve(__dirname,'../src')
},
extensions:['*','.js','.json','.vue']
},
plugins:[
new vueLoaderPlugin()
]
}
+

配置webpack-dev-server进行热更新

1
npm i -D webpack-dev-server
+

配置如下

+
1
2
3
4
5
6
7
8
9
10
11
12
const Webpack = require('webpack')
module.exports = {
// ...省略其他配置
devServer:{
port:3000,
hot:true,
contentBase:'../dist'
},
plugins:[
new Webpack.HotModuleReplacementPlugin()
]
}
+

完整配置如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// webpack.config.js
const path = require('path');
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')
const vueLoaderPlugin = require('vue-loader/lib/plugin')
const Webpack = require('webpack')
module.exports = {
mode:'development', // 开发模式
entry: {
main:path.resolve(__dirname,'../src/main.js'),
},
output: {
filename: '[name].[hash:8].js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
},
module:{
rules:[
{
test:/\.vue$/,
use:['vue-loader']
},
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:[
['@babel/preset-env']
]
}
}
},
{
test:/\.css$/,
use: ['vue-style-loader','css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
}]
},
{
test:/\.less$/,
use: ['vue-style-loader','css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
},'less-loader']
}
]
},
resolve:{
alias:{
'vue$':'vue/dist/vue.runtime.esm.js',
' @':path.resolve(__dirname,'../src')
},
extensions:['*','.js','.json','.vue']
},
devServer:{
port:3000,
hot:true,
contentBase:'../dist'
},
plugins:[
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html'),
filename:'index.html'
}),
new vueLoaderPlugin(),
new Webpack.HotModuleReplacementPlugin()
]
}
+

配置打包命令

1
2
3
4
"script":{
"dev":"webpack-dev-server --config build/webpack.config.js --open",
"build":"webpack --config build/webpack.config.js"
}
+

打包文件已经配置完毕,接下来让我们测试一下
首先在 src 新建一个 main.js

+
1
2
3
4
5
6
// main.js
import Vue from 'vue'
import App from './app'
new Vue({
render:h=>h(App)
}).$mount('#app')
+

新建一个 App.vue

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// App.vue
<template>
<div id='container'></div>
</template>
<script>
export default {
data(){
return {
initData:''
}
}
}
</script>
<style scoped>
#container{
width:100%;
height:100%;
}
</style>
+

新建一个 public 文件夹,里面新建一个 index.html

+
1
2
3
4
5
6
7
8
9
10
11
12
13
// index.html
<!DOCTYPE html>
<html lang='en'>
<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'>
<title>lao li</title>
</head>
<body>
<div id='app'></div>
</body>
</html>
+

执行 npm run dev 这时候如果浏览器出现 Vue 开发环境运行成功,那么恭喜你,已经成功迈出了第一步

+

区分开发环境与生产环境

实际应用到项目中,我们需要区分开发环境与生产环境,我们在原来 webpack.config.js 的基础上再新增两个文件

+

webpack.dev.js 开发环境配置文件
开发环境主要实现的是热更新,不要压缩代码,完整的 sourceMap
webpack.prod.js生产环境配置文件
生产环境主要实现的是压缩代码、提取 css 文件、合理的 sourceMap 、分割代码
需要安装以下模块:

+
1
npm i -D  webpack-merge copy-webpack-plugin optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin
+
    +
  • webpack-merge 合并配置
  • +
  • copy-webpack-plugin 拷贝静态资源
  • +
  • optimize-css-assets-webpack-plugin 压缩 css
  • +
  • uglifyjs-webpack-plugin 压缩js
  • +
+
+

webpack mode 设置 production 的时候会自动压缩 js 代码。
原则上不需要引入 uglifyjs-webpack-plugin 进行重复工作。
但是 optimize-css-assets-webpack-plugin 压缩 css 的同时会破坏原有的 js 压缩,所以这里我们引入 uglifyjs 进行压缩

+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// webpack.config.js
const path = require('path')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const vueLoaderPlugin = require('vue-loader/lib/plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const devMode = process.argv.indexOf('--mode=production') === -1;
module.exports = {
entry:{
main:path.resolve(__dirname,'../src/main.js')
},
output:{
path:path.resolve(__dirname,'../dist'),
filename:'js/[name].[hash:8].js',
chunkFilename:'js/[name].[hash:8].js'
},
module:{
rules:[
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:['@babel/preset-env']
}
},
exclude:/node_modules/
},
{
test:/\.vue$/,
use:['cache-loader','thread-loader',{
loader:'vue-loader',
options:{
compilerOptions:{
preserveWhitespace:false
}
}
}]
},
{
test:/\.css$/,
use:[{
loader: devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
options:{
publicPath:"../dist/css/",
hmr:devMode
}
},'css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
}]
},
{
test:/\.less$/,
use:[{
loader:devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
options:{
publicPath:"../dist/css/",
hmr:devMode
}
},'css-loader','less-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
}]
},
{
test:/\.(jep?g|png|gif)$/,
use:{
loader:'url-loader',
options:{
limit:10240,
fallback:{
loader:'file-loader',
options:{
name:'img/[name].[hash:8].[ext]'
}
}
}
}
},
{
test:/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
use:{
loader:'url-loader',
options:{
limit:10240,
fallback:{
loader:'file-loader',
options:{
name:'media/[name].[hash:8].[ext]'
}
}
}
}
},
{
test:/\.(woff2?|eot|ttf|otf)(\?.*)?$/i,
use:{
loader:'url-loader',
options:{
limit:10240,
fallback:{
loader:'file-loader',
options:{
name:'media/[name].[hash:8].[ext]'
}
}
}
}
}
]
},
resolve:{
alias:{
'vue$':'vue/dist/vue.runtime.esm.js',
' @':path.resolve(__dirname,'../src')
},
extensions:['*','.js','.json','.vue']
},
plugins:[
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html')
}),
new vueLoaderPlugin(),
new MiniCssExtractPlugin({
filename: devMode ? '[name].css' : '[name].[hash].css',
chunkFilename: devMode ? '[id].css' : '[id].[hash].css'
})
]
}
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// webpack.dev.js
const Webpack = require('webpack')
const webpackConfig = require('./webpack.config.js')
const WebpackMerge = require('webpack-merge')

module.exports = WebpackMerge(webpackConfig,{
mode:'development',
devtool:'cheap-module-eval-source-map',
devServer:{
port:3000,
hot:true,
contentBase:'../dist'
},
plugins:[
new Webpack.HotModuleReplacementPlugin()
]
})
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// webpack.prod.js
const path = require('path')
const webpackConfig = require('./webpack.config.js')
const WebpackMerge = require('webpack-merge')

const CopyWebpackPlugin = require('copy-webpack-plugin')
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

module.exports = WebpackMerge(webpackConfig,{
mode:'production',
devtool:'cheap-module-source-map',
plugins:[
new CopyWebpackPlugin([{
from:path.resolve(__dirname,'../public'),
to:path.resolve(__dirname,'../dist')
}]),
],
optimization:{
minimizer:[
new UglifyJsPlugin({//压缩js
cache:true,
parallel:true,
sourceMap:true
}),
new OptimizeCssAssetsPlugin({})
],
splitChunks:{
chunks:'all',
cacheGroups:{
libs: {
name: "chunk-libs",
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: "initial" // 只打包初始时依赖的第三方
}
}
}
}
})
+ +

优化webpack配置

看到这里你或许有些累了,但是要想获取更好的offer,更高的薪水,下面必须继续深入

+

优化配置对我们来说非常有实际意义,这实际关系到你打包出来文件的大小,打包的速度等。
具体优化可以分为以下几点:

+

优化打包速度

+

构建速度指的是我们每次修改代码后热更新的速度以及发布前打包文件的速度。

+
+

合理的配置 mode 参数与 devtool 参数

devtool 可设置的值
mode 可设置 development production 两个参数

+

如果没有设置, webpack4 会将 mode 的默认值设置为 production

+
    +
  • production :将 process.env.NODE_ENV 的值设置为 production ,启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPluginUglifyJsPlugin,会进行 tree shaking (去除无用代码)和 uglifyjs (代码压缩混淆)
  • +
  • development :将 process.env.NODE_ENV 的值设置为 development ,启用 NamedChunksPluginNamedModulesPlugin
  • +
+

缩小文件的搜索范围(配置include exclude alias noParse extensions)

    +
  • alias 当我们代码中出现 import ‘vue’时, webpack会采用向上递归搜索的方式去node_modules 目录下找。为了减少搜索范围我们可以直接告诉webpack去哪个路径下查找。也就是别名(alias)的配置。
  • +
  • include exclude 同样配置include exclude也可以减少webpack loader的搜索转换时间。
  • +
  • noParse 当我们代码中使用到 import jq from 'jquery' 时, webpack 会去解析 jq 这个库是否有依赖其他的包。但是我们对类似 jquery 这类依赖库,一般会认为不会引用其他的包(特殊除外,自行判断)。增加 noParse 属性,告诉 webpack 不必解析,以此增加打包速度。
  • +
  • extensions webpack 会根据 extensions 定义的后缀查找文件(频率较高的文件类型优先写在前面)
  • +
+

配图

+

使用HappyPack开启多进程Loader转换

+

webpack 构建过程中,实际上耗费时间大多数用在 loader 解析转换以及代码的压缩中。日常开发中我们需要使用 Loaderjscss ,图片,字体等文件做转换操作,并且转换的文件数据量也是非常大。由于js单线程的特性使得这些转换操作不能并发处理文件,而是需要一个个文件进行处理。 HappyPack 的基本原理是将这部分任务分解到多个子进程中去并行处理,子进程处理完成后把结果发送到主进程中,从而减少总的构建时间

+
+
1
npm i -D happypack
+ +

happypack

+

使用 webpack-parallel-uglify-plugin 增强代码压缩

上面对于 loader 转换已经做优化,那么下面还有另一个难点就是优化代码的压缩时间。

+
1
npm i -D webpack-parallel-uglify-plugin
+

webpack-parallel-uglify-plugin

+

抽离第三方模块

+

对于开发项目中不经常会变更的静态依赖文件。类似于我们的 elementUivue 全家桶等等。因为很少会变更,所以我们不希望这些依赖要被集成到每一次的构建逻辑中去。 这样做的好处是每次更改我本地代码的文件的时候, webpack 只需要打包我项目本身的文件代码,而不会再去编译第三方库。以后只要我们不升级第三方包的时候,那么 webpack 就不会对这些库去打包,这样可以快速的提高打包的速度。

+
+

这里我们使用 webpack 内置的 DllPlugin DllReferencePlugin 进行抽离

+

在与 webpack 配置文件同级目录下新建 webpack.dll.config.js
代码如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// webpack.dll.config.js
const path = require("path");
const webpack = require("webpack");
module.exports = {
// 你想要打包的模块的数组
entry: {
vendor: ['vue','element-ui']
},
output: {
path: path.resolve(__dirname, 'static/js'), // 打包后文件输出的位置
filename: '[name].dll.js',
library: '[name]_library'
// 这里需要和webpack.DllPlugin中的`name: '[name]_library',`保持一致。
},
plugins: [
new webpack.DllPlugin({
path: path.resolve(__dirname, '[name]-manifest.json'),
name: '[name]_library',
context: __dirname
})
]
};
+

package.json 中配置如下命令

+
1
"dll": "webpack --config build/webpack.dll.config.js"
+ +

接下来在我们的 webpack.config.js 中增加以下代码

+
1
2
3
4
5
6
7
8
9
10
11
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./vendor-manifest.json')
}),
new CopyWebpackPlugin([ // 拷贝生成的文件到dist目录 这样每次不必手动去cv
{from: 'static', to:'static'}
]),
]
};
+

执行

+
1
npm run dll
+ +

会发现生成了我们需要的集合第三地方
代码的 vendor.dll.js
我们需要在html文件中手动引入这个js文件

+
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<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">
<title>老yuan</title>
<script src="static/js/vendor.dll.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
+

这样如果我们没有更新第三方依赖包,就不必 npm run dll 。直接执行 npm run dev npm run build 的时候会发现我们的打包速度明显有所提升。因为我们已经通过 dllPlugin 将第三方依赖包抽离出来了。

+

配置缓存

+

我们每次执行构建都会把所有的文件都重复编译一遍,这样的重复工作是否可以被缓存下来呢,答案是可以的,目前大部分 loader 都提供了 cache 配置项。比如在 babel-loader 中,可以通过设置cacheDirectory 来开启缓存,babel-loader?cacheDirectory=true 就会将每次的编译结果写进硬盘文件(默认是在项目根目录下的 node_modules/.cache/babel-loader 目录内,当然你也可以自定义)
但如果 loader 不支持缓存呢?我们也有方法,我们可以通过 cache-loader ,它所做的事情很简单,就是 babel-loader 开启 cache 后做的事情,将 loader 的编译结果写入硬盘缓存。再次构建会先比较一下,如果文件较之前的没有发生变化则会直接使用缓存。使用方法如官方 demo 所示,在一些性能开销较大的 loader 之前添加此 loader 即可

+
+
1
npm i -D cache-loader
+

cache-loader

+

优化打包文件体积

打包的速度我们是进行了优化,但是打包后的文件体积却是十分大,造成了页面加载缓慢,浪费流量等,接下来让我们从文件体积上继续优化

+

引入webpack-bundle-analyzer分析打包后的文件

webpack-bundle-analyzer 将打包后的内容束展示为方便交互的直观树状图,让我们知道我们所构建包中真正引入的内容

+
1
npm i -D webpack-bundle-analyzer
+

webpack-bundle-analyzer

+

接下来在 package.json 里配置启动命令

+
1
"analyz": "NODE_ENV=production npm_config_report=true npm run build"
+

windows 请安装 npm i -D cross-env

+
1
"analyz": "cross-env NODE_ENV=production npm_config_report=true npm run build"
+

接下来 npm run analyz 浏览器会自动打开文件依赖图的网页

+

externals

+

按照官方文档的解释,如果我们想引用一个库,但是又不想让 webpack 打包,并且又不影响我们在程序中以 CMDAMD 或者 window/global 全局等方式进行使用,那就可以通过配置 Externals 。这个功能主要是用在创建一个库的时候用的,但是也可以在我们项目开发中充分使用Externals 的方式,我们将这些不需要打包的静态资源从构建逻辑中剔除出去,而使用 CDN的方式,去引用它们。
有时我们希望我们通过 script 引入的库,如用 CDN 的方式引入的 jquery ,我们在使用时,依旧用 require 的方式来使用,但是却不希望 webpack 将它又编译进文件中。这里官网案例已经足够清晰明了,大家有兴趣可以点击了解
webpack
官网案例如下

+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
<script
src="https://code.jquery.com/jquery-3.1.0.js"
integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
crossorigin="anonymous">
</script>
module.exports = {
//...
externals: {
jquery: 'jQuery'
}
};
import $ from 'jquery';
$('.my-element').animate(/* ... */);
+

Tree-shaking

+

这里单独提一下 tree-shaking ,是因为这里有个坑。 tree-shaking 的主要作用是用来清除代码中无用的部分。目前在 webpack4 我们设置 modeproduction 的时候已经自动开启了tree-shaking 。但是要想使其生效,生成的代码必须是ES6模块。不能使用其它类型的模块如 CommonJS 之流。如果使用 Babel 的话,这里有一个小问题,因为 Babel 的预案( preset )默认会将任何模块类型都转译成 CommonJS 类型。修正这个问题也很简单,在 .babelrc 文件或在 webpack.config.js 文件中设置 modules:false 就好了

+
+
1
2
3
4
5
6
7
8
9
10
// .babelrc
{
"presets": [
["@babel/preset-env",
{
"modules": false
}
]
]
}
+

或者

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.config.js

module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', { modules: false }]
}
},
exclude: /(node_modules)/
}
]
}
+ +

经历过上面两个系列的洗礼,到现在我们成为了一名合格的 webpack 配置工程师。但是光拧螺丝,自身的可替代性还是很高,下面我们将深入 webpack 的原理中去

+

手写webpack系列

经历过上面两个部分,我们已经可以熟练的运用相关的 loaderplugin 对我们的代码进行转换、解析。接下来我们自己手动实现 loaderplugin ,使其在平时的开发中获得更多的乐趣。

+

手写 webpack loader

+

loader 从本质上来说其实就是一个 node 模块。相当于一台榨汁机( loader )将相关类型的文件代码( code )给它。根据我们设置的规则,经过它的一系列加工后还给我们加工好的果汁( code )。

+
+

loader 编写原则

    +
  • 单一原则: 每个 Loader 只做一件事;
  • +
  • 链式调用: Webpack 会按顺序链式调用每个 Loader
  • +
  • 统一原则: 遵循 Webpack 制定的设计规则和结构,输入与输出均为字符串,各个 Loader 完全独立,即插即用;
  • +
+

在日常开发环境中,为了方便调试我们往往会加入许多 console 打印。但是我们不希望在生产环境中存在打印的值。那么这里我们自己实现一个 loader 去除代码中的 console

+
+

知识点普及之 ASTAST 通俗的来说,假设我们有一个文件 a.js ,我们对 a.js 里面的1000行进行一些操作处理,比如为所有的 await 增加 try catch ,以及其他操作,但是 a.js 里面的代码本质上来说就是一堆字符串。那我们怎么办呢,那就是转换为带标记信息的对象(抽象语法树)我们方便进行增删改查。这个带标记的对象(抽象语法树)就是AST。这里推荐一篇不错的AST文章 AST快速入门

+
+
1
npm i -D @babel/parser @babel/traverse @babel/generator @babel/types
+
    +
  • @babel/parser 将源代码解析成 AST
  • +
  • @babel/traverse 对 AST 节点进行递归遍历,生成一个便于操作、转换的 path 对象
  • +
  • @babel/generator 将 AST 解码生成 js 代码
  • +
  • @babel/types 通过该模块对具体的 AST 节点进行进行增、删、改、查
  • +
+

新建 drop-console.js

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const t = require('@babel/types')
module.exports=function(source){
const ast = parser.parse(source,{ sourceType: 'module'})
traverse(ast,{
CallExpression(path){
if(t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.object, {name: "console"})){
path.remove()
}
}
})
const output = generator(ast, {}, source);
return output.code
}
+

如何使用

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const path = require('path')
module.exports = {
mode:'development',
entry:path.resolve(__dirname,'index.js'),
output:{
filename:'[name].[contenthash].js',
path:path.resolve(__dirname,'dist')
},
module:{
rules:[{
test:/\.js$/,
use:path.resolve(__dirname,'drop-console.js')
}
]
}
}
+
+

实际上在 webpack4 中已经集成了去除 console 功能,在 minimizer 中可配置 去除console
附上官网 如何编写一个loader

+
+

手写webpack plugin

+

Webpack 运行的生命周期中会广播出许多事件, Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
通俗来说:一盘美味的 盐豆炒鸡蛋 需要经历烧油 炒制 调味到最后的装盘等过程,而 plugin 相当于可以监控每个环节并进行操作,比如可以写一个少放胡椒粉 plugin ,监控 webpack 暴露出的生命周期事件(调味),在调味的时候执行少放胡椒粉操作。
那么它与 loader 的区别是什么呢?上面我们也提到了 loader 的单一原则, loader 只能一件事,比如说 less-loader ,只能解析 less 文件, plugin 则是针对整个流程执行广泛的任务。

+
+

一个基本的 plugin 插件结构如下

+
1
2
3
4
5
6
7
8
9
10
11
12
class firstPlugin {
constructor (options) {
console.log('firstPlugin options', options)
}
apply (compiler) {
compiler.plugin('done', compilation => {
console.log('firstPlugin')
))
}
}

module.exports = firstPlugin
+
+

compilercompilation 是什么?

+
+
    +
  • compiler 对象包含了 Webpack 环境所有的的配置信息。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 optionsloaderplugin 。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。
  • +
  • compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation ,从而生成一组新的编译资源。 compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。
  • +
+

compilercompilation 的区别在于

+
    +
  • compiler 代表了整个 webpack 从启动到关闭的生命周期,而 compilation 只是代表了一次新的编译过程
  • +
  • compilercompilation 暴露出许多钩子,我们可以根据实际需求的场景进行自定义处理
  • +
+

compiler钩子文档
compilation钩子文档

+

下面我们手动开发一个简单的需求,在生成打包文件之前自动生成一个关于打包出文件的大小信息
新建一个 webpack-firstPlugin.js

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class firstPlugin{
constructor(options){
this.options = options
}
apply(compiler){
compiler.plugin('emit',(compilation,callback)=>{
let str = ''
for (let filename in compilation.assets){
str += `文件:${filename} 大小${compilation.assets[filename]['size']()}\n`
}
// 通过compilation.assets可以获取打包后静态资源信息,同样也可以写入资源
compilation.assets['fileSize.md'] = {
source:function(){
return str
},
size:function(){
return str.length
}
}
callback()
})
}
}
module.exports = firstPlugin
+

如何使用

+
1
2
3
4
5
6
7
8
const path = require('path')
const firstPlugin = require('webpack-firstPlugin.js')
module.exports = {
// 省略其他代码
plugins:[
new firstPlugin()
]
}
+

执行 npm run build 即可看到在 dist 文件夹中生成了一个包含打包文件信息的 fileSize.md

+
+

上面两个 loaderplugin 案例只是一个引导,实际开发需求中的 loaderplugin 要考虑的方面很多,建议大家自己多动手尝试一下。

+
+

附上官网 如何编写一个plugin

+

webpack5.0的时代

无论是前端框架还是构建工具的更新速度远远超乎了我们的想象,前几年的 jquery 一把梭的时代一去不复返。我们要拥抱的是不断更新迭代的 vuereactnodeserverlessdockerk8s ….
不甘落后的 webpack 也已经在近日发布了 webpack 5.0.0 beta 10 版本。在之前作者也曾提过 webpack5.0 旨在减少配置的复杂度,使其更容易上手( webpack4 的时候也说了这句话),以及一些性能上的提升

+
    +
  • 使用持久化缓存提高构建性能;
  • +
  • 使用更好的算法和默认值改进长期缓存(long-term caching);
  • +
  • 清理内部结构而不引入任何破坏性的变化;
  • +
  • 引入一些breaking changes,以便尽可能长的使用v5版本。
  • +
+

目前来看,维护者的更新很频繁,相信用不了多久webpack5.0将会拥抱大众。感兴趣的同学可以先安装beta版本尝尝鲜。不过在此之前建议大家先对webpack4进行一番掌握,这样后面的路才会越来越好走。

+

更多阅读

webpack中文
webpackjs
4W字长文带你深度解锁Webpack系列(上)

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/51/index.html b/en/page/51/index.html new file mode 100644 index 0000000000..e2a7feae00 --- /dev/null +++ b/en/page/51/index.html @@ -0,0 +1,531 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

原版

先来看一个call实例,看看call到底做了什么:

+
1
2
3
4
5
6
7
let foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call(foo); // 1
+

从代码的执行结果,我们可以看到,call首先改变了this的指向,使函数的this指向了foo,然后使bar函数执行了。
总结一下:

+
    +
  • call改变函数this指向
  • +
  • 调用函数
  • +
+

自己动手

    +
  1. 首先我们对参数 context 做了兼容处理,不传值, context 默认值为 window
  2. +
  3. 然后我们将函数挂载到 context 上面, context.fn = this
  4. +
  5. 处理参数,将传入 myCall 的参数截取,去除第一位,然后转为数组;
  6. +
  7. 调用 context.fn ,此时 fnthis 指向 context
  8. +
  9. 删除对象上的属性 delete context.fn
  10. +
  11. 将结果返回。
  12. +
+
1
2
3
4
5
6
7
8
Function.prototype.myCall = function(context) {
context = context || window;
context.fn = this; // 将函数挂载到对象的fn属性上
const args = [...arguments].slice(1); // 处理传入的参数
const result = context.fn(...args); // 通过对象的属性调用该方法
delete context.fn; // 删除该属性
return result // 返回结果
};
+

applycall 的区别在于参数, 其他没有差别,实现如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// myApply的参数形式为(obj,[arg1,arg2,arg3]);
// 所以myApply的第二个参数为[arg1,arg2,arg3]
// 这里我们用扩展运算符来处理一下参数的传入方式
Function.prototype.myApply = function(context) {
context = context || window
context.fn = this
let result
if (arguments[1]) { // 判断是否有第二个参数
result = context.fn(…arguments[1]) // 有的话传入执行
} else {
result = context.fn() // 没有的话空参执行
}
delete context.fn;
return result
};
+

bindcallapply 作用都是改变 this 的指向,区别在于 bind 改变后不会立即执行,而 callapply 会立即执行,我们看一下 bind 的用法

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(){
this.name="zs";
this.age=18;
this.gender="男"
}
let obj={
hobby:"看书"
}

let changePerson = Person.bind(obj); // 将构造函数的this绑定为obj
changePerson(); // 直接调用构造函数,函数会操作obj对象,给其添加三个属性;
console.log(obj); // => {hobby:"看书",name:"zs",age:18,gender:'男'}
let p = new changePerson(); // 用改变了this 指向的构造函数,new一个实例出来
console.log(p); // => Person {name:"zs",age:18,gender:'男'}
+

仔细观察上面的代码,再看输出结果。

+

我们对 Person 类使用了 bind 将其 this 指向 obj ,得到了 changePerson 函数,此处如果我们直接调用 changeperson 会改变 obj ,若用 new 调用 changeperson 会得到实例 p,并且其 __proto__ 指向 Person ,我们发现 bind 失效了。

+

我们得到结论:bind 改变了 this 指向的函数,如果用 new 操作符来调用, bind 将会失效

+

这个对象就是这个构造函数的实例,那么只要在函数内部执行 * this instanceof 构造函数 * 来判断其结果是否为 true ,就能判断函数是否是通过 new 操作符来调用了,若结果为 true 则是用 new 操作符调用的,总结如下:

+
    +
  1. 保存当前 this 指向
  2. +
  3. 保存环境上下文
  4. +
  5. 保存参数,去掉第一个对象参数
  6. +
  7. 返回待执行函数
      +
    1. 数组化剩余参数
    2. +
    3. 判断是否为构造函数
    4. +
    5. 若是执行构造函数,若不是改变 this 指向执行
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      // bind实现

      Function.prototype.myBind = function(context){
      let _this = this; // 1、保存函数
      context = context || window; // 2、保存目标对象
      let rest = [...arguments].slice(1); // 3、保存目标对象之外的参数,将其转化为数组;
      // 此处开始与 call 和 apply 不同,不是返回结果,而是返回一个函数
      return function F(){ // 4、返回一个待执行的函数
      let rest2 = Array.prototype.slice.call(arguments) // 5、这里的arguments是F函数的参数,转换为数组;
      if(this instanceof F){
      return new _this(...rest2) // 6、若是用new操作符调用,则直接用new 调用原函数,并用扩展运算符传递参数
      }else{
      _this.apply(context,rest.concat(rest2)); // 7、用apply调用第一步保存的函数,并绑定this,传递合并的参数数组,
      // 即context._this(rest.concat(rest2))
      }
      }
      };
    6. +
    +
  8. +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/52/index.html b/en/page/52/index.html new file mode 100644 index 0000000000..c7d54c74e7 --- /dev/null +++ b/en/page/52/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

原理

就是利用 generator (生成器)分割代码片段。然后我们使用一个函数让其自迭代,每一个 yieldpromise 包裹起来。执行下一步的时机由 promise 来控制

+

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function _asyncToGenerator(fn) {
return function() {
var self = this, args = arguments;
return new Promise(function(resolve, reject) { // 将返回值 promise 化
var gen = fn.apply(self, args); // 获取迭代器实例
function _next(value) { // 执行下一步
asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
}
function _throw(err) { // 抛出异常
asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
}
_next(undefined); // 第一次触发
});
};
}
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/53/index.html b/en/page/53/index.html new file mode 100644 index 0000000000..a04fd76bc0 --- /dev/null +++ b/en/page/53/index.html @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

实现一个 EventEmitter

    +
  1. 创建一个 Event 类,包含构造函数、绑定、解绑、触发方法
  2. +
  3. on 监听event事件,事件触发时调用fn函数。根据字典创建事件数组,如果事件处理不存在,那么就推入数组,反之返回原数组
  4. +
  5. off 从字典中获取当前事件数组,如果获取值是数组,那么判断解绑哪个事件,如果没填清空全部(数组长度置为零),否则从事件数组删除选择的事件
  6. +
  7. emit 触发event事件,并把参数arg1,arg2,arg3….传给事件处理函数,跟解绑外层判断一样,内层把删除改为执行即可
  8. +
  9. once 为指定事件注册一个单次监听器,单次监听器最多只触发一次,触发后立即解除监听器。
  10. +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class Event {
constructor () {
// 储存事件的数据结构
// 为查找迅速, 使用对象(字典)
this._cache = {}
}

// 绑定
on(event, callback) {
// 为了按类查找方便和节省空间
// 将同一类型事件放到一个数组中
// 这里的数组是队列, 遵循先进先出
// 即新绑定的事件先触发
let fns = (this._cache[event] = this._cache[event] || [])
if(fns.indexOf(callback) === -1) {
fns.push(callback)
}
return this
}

// 解绑
off (event, callback) {
let fns = this._cache[event]
if(Array.isArray(fns)) {
if(callback) {
let index = fns.indexOf(callback)
if(index !== -1) {
fns.splice(index, 1)
}
} else {
// 全部清空
fns.length = 0
}
}
return this
}
// 触发emit
emit(event, ...args) {
let fns = this._cache[event]
if(Array.isArray(fns)) {
fns.forEach((fn) => {
fn(...args)
})
}
return this
}

// 一次性绑定
once(event, callback) {
let onceCallback = () => { // 定义一个只执行一次就解绑的方法
callback.call(this); // 使用call改变this指向
this.off(event, onceCallback); // 解绑
};
this.on(event, onceCallback); // 绑定
return this;
}
}
+

好的接下来我们调用一下

+
1
2
3
4
5
6
7
8

let e = new Event()

e.on('click',function(){
console.log('on')
})
// e.trigger('click', '666')
console.log(e)
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/54/index.html b/en/page/54/index.html new file mode 100644 index 0000000000..3d0b62a1eb --- /dev/null +++ b/en/page/54/index.html @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

Vue 2.x 的 Object.defineProperty 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 数据
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');

// 数据劫持
Object.defineProperty(data, 'text', {
// 数据变化 —> 修改视图
set(newVal) {
input.value = newVal;
span.innerHTML = newVal;
}
});

// 视图更改 --> 数据变化
input.addEventListener('keyup', function(e) {
data.text = e.target.value;
});
+ +

Vue 3.x 的 proxy 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 数据
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');

// 数据劫持
const handler = {
set(target, key, value) {
target[key] = value;
// 数据变化 —> 修改视图
input.value = value;
span.innerHTML = value;
return value;
}
};
const proxy = new Proxy(data, handler);

// 视图更改 --> 数据变化
input.addEventListener('keyup', function(e) {
proxy.text = e.target.value;
});
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/55/index.html b/en/page/55/index.html new file mode 100644 index 0000000000..f10ab494bf --- /dev/null +++ b/en/page/55/index.html @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

原理

先看看 reducemap 的使用方法

+
1
2
let new_array = arr.map(function callback(currentValue[, index[,array) {/* Return element for new_array */ }[, thisArg])
let result = arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
+ +

实现

第一种用 for 实现

+
1
2
3
4
5
6
7
Array.prototype.myMap = function(callback, thisArg) {
let arr = [];
for (let i = 0; i < this.length; i++) {
arr.push(callback.call(thisArg, this[i], i, this));
}
return arr;
};
+

第二种用 reduce 实现

+
1
2
3
4
5
6
7
Array.prototype.myMap = function(callback, thisArg) {
let result = this.reduce((accumulator, currentValue, index, array) => {
accumulator.push(callback.call(thisArg, currentValue, index, array));
return accumulator;
}, []);
return result;
};
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/56/index.html b/en/page/56/index.html new file mode 100644 index 0000000000..d2008d2c17 --- /dev/null +++ b/en/page/56/index.html @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

实现一个 Object.create() 方法

    +
  1. 创建一个空匿名函数
  2. +
  3. 函数原型对象指向传入对象实例
  4. +
  5. 返回构造函数创建的实例
  6. +
+
1
2
3
4
5
function create =  function (o) {
var F = function () {};
F.prototype = o;
return new F();
};
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/57/index.html b/en/page/57/index.html new file mode 100644 index 0000000000..0433cb8d89 --- /dev/null +++ b/en/page/57/index.html @@ -0,0 +1,496 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<ul id="color-list">
<li>red</li>
<li>yellow</li>
<li>blue</li>
<li>green</li>
<li>black</li>
<li>white</li>
</ul>

<script>
(function () {
var color_list = document.getElementById('color-list');
color_list.addEventListener('click', showColor, true);
function showColor(e) {
var x = e.target;
if (x.nodeName.toLowerCase() === 'li') {
alert(x.innerHTML);
}
}
})();
</script>
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/58/index.html b/en/page/58/index.html new file mode 100644 index 0000000000..7599f14db0 --- /dev/null +++ b/en/page/58/index.html @@ -0,0 +1,500 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

原理

将多层数组扁平化

+

实现

1
2
3
4
5
6
7
8
9
10
11
Array.prototype.myFlat = function() {
var arr = [];
this.forEach((item)=>{
if(Array.isArray(item)){
arr = arr.concat(item.myFlat()); // 如果是数组的话继续循环
}else{
arr.push(item)
}
})
return arr
};
+ +

还有另外一种实现方式,非常好用

+
1
2
3
4
5
Array.prototype.myFlat = function() {
return this.toString() // => "1,2,3,4"
.split(",") // => ["1", "2", "3", "4"]
.map(item => +item); // => [1, 2, 3, 4]
};
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/59/index.html b/en/page/59/index.html new file mode 100644 index 0000000000..7122cff31d --- /dev/null +++ b/en/page/59/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

实现一个Array.isArray

思路很简单,就是利用 Object.prototype.toString

+
1
2
3
Array.myIsArray = function(o) { 
return Object.prototype.toString.call(Object(o)) === '[object Array]';
};
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/6/index.html b/en/page/6/index.html new file mode 100644 index 0000000000..11485d62d9 --- /dev/null +++ b/en/page/6/index.html @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

Docker 镜像安全最佳实践

Docker 和 宿主机 的设置

    +
  1. 保证宿主机和 Docker 的版本是最新的
  2. +
  3. 不要暴露 Docker 的 守护进程(daemon) 的套接字
  4. +
  5. 使用 rootless 模式启动 Docker
  6. +
  7. 避免使用特权容器
  8. +
  9. 限制容器资源
  10. +
  11. 隔离容器网络
  12. +
  13. 提高容器的隔离度
  14. +
  15. 将文件系统和卷设置为只读
  16. +
  17. 完整的生命周期管理
  18. +
  19. 限制来自容器内的系统调用
  20. +
+

确保镜像安全

    +
  1. 扫描和验证容器镜像
  2. +
  3. 使用最小基础镜像
  4. +
  5. 不要向 Docker 镜像泄露敏感信息
  6. +
  7. 使用多阶段构建
  8. +
  9. 确保容器注册
  10. +
  11. 使用固定标签以获得不变性
  12. +
+

监控容器

    +
  1. 监控容器活动
  2. +
  3. 确保容器在运行时的安全
  4. +
  5. 将故障排除数据与容器分开保存
  6. +
  7. 为镜像使用元数据标签
  8. +
+

参考

Top 20 Dockerfile best practices
Dockerセキュリティベストプラクティス トップ20

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/60/index.html b/en/page/60/index.html new file mode 100644 index 0000000000..520a96b8ed --- /dev/null +++ b/en/page/60/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

实现一个new操作符

我们首先知道new做了什么:

+
    +
  1. 创建一个空的简单 JavaScript 对象(即{})
  2. +
  3. 链接该对象(即设置该对象的构造函数)到另一个对象
  4. +
  5. 将步骤(1)新创建的对象作为 this 的上下文
  6. +
  7. 如果该函数没有返回对象,则返回 this
  8. +
+

知道new做了什么,接下来我们就来实现它

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function create(){
// 创建一个空的对象
let obj = {};
// 获得构造函数
let Con = [].shift.call(arguments)
// 将空对象指向构造函数的原型链
Object.setPrototypeOf(obj, Con.prototype);
// obj.__proto__ = Con.prototype // 链接到原型
// obj绑定到构造函数上,便可以访问构造函数中的属性,即this.obj.Con(args)
let result = Con.apply(obj, arguments);
// 如果返回的result是一个对象则返回
// new方法失效,否则返回obj
return result instanceof Object ? result : this.obj;
// return typeof result === 'object' ? result : obj// 确保 new 出来的是个对象
}
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/61/index.html b/en/page/61/index.html new file mode 100644 index 0000000000..5db61993f4 --- /dev/null +++ b/en/page/61/index.html @@ -0,0 +1,500 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

原版

1
Array.prototype.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
+ +

自己动手

1
2
3
4
5
6
7
8
Array.prototype.myReduce = function(callback, initialValue) {
let accumulator = initialValue ? initialValue : this[0]; // 第一次使用判断时候有initialValue参数,如果有用他,没有用this[0],这里this指的是传入的数组,this[0]就是数组第一项
for (let i = initialValue ? 0 : 1; i < this.length; i++) { // 如果有初始值从0开始循环,不然从1开始
let _this = this; // 保留当前this指向
accumulator = callback(accumulator, this[i], i, _this); //
}
return accumulator; // 返回迭代器的终值
};
+

试用一下

+
1
2
3
4
5
6
7
let arr = [1, 2, 3, 4];
let sum = arr.myReduce((acc, val) => {
acc += val;
return acc;
}, 5);

console.log(sum); // 15
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/62/index.html b/en/page/62/index.html new file mode 100644 index 0000000000..560d066ac1 --- /dev/null +++ b/en/page/62/index.html @@ -0,0 +1,500 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

path.join 与 path.resolve 的区别

    +
  1. 对于以/开始的路径片段,path.join只是简单的将该路径片段进行拼接,而path.resolve将以/开始的路径片段作为根目录,在此之前的路径将会被丢弃,就像是在terminal中使用cd命令一样。
  2. +
+
1
2
path.join('/a', '/b') // 'a/b'
path.resolve('/a', '/b') // '/b'
+
    +
  1. path.resolve总是返回一个以相对于当前的工作目录(working directory)的绝对路径。
  2. +
+
1
2
path.join('./a', './b') // 'a/b'
path.resolve('./a', './b') // '/Users/username/Projects/webpack-demo/a/b'
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/63/index.html b/en/page/63/index.html new file mode 100644 index 0000000000..a230fab266 --- /dev/null +++ b/en/page/63/index.html @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

使用 Alibaba 的 Homebrew 镜像源进行加速

平时我们执行 brew 命令安装软件的时候,跟以下 3 个仓库地址有关:

+
    +
  • brew.git
  • +
  • homebrew-core.git
  • +
  • homebrew-bottles
    通过以下操作将这 3 个仓库地址全部替换为 Alibaba 提供的地址
  • +
+

1. 替换 / 还原 brew.git 仓库地址

1
2
3
4
5
6
7
8
9
# 替换成阿里巴巴的 brew.git 仓库地址:
cd "$(brew --repo)"
git remote set-url origin https://mirrors.aliyun.com/homebrew/brew.git

#=======================================================

# 还原为官方提供的 brew.git 仓库地址
cd "$(brew --repo)"
git remote set-url origin https://github.com/Homebrew/brew.git
+ +

2. 替换 / 还原 homebrew-core.git 仓库地址

1
2
3
4
5
6
7
8
9
# 替换成阿里巴巴的 homebrew-core.git 仓库地址:
cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
git remote set-url origin https://mirrors.aliyun.com/homebrew/homebrew-core.git

#=======================================================

# 还原为官方提供的 homebrew-core.git 仓库地址
cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
git remote set-url origin https://github.com/Homebrew/homebrew-core.git
+ +

3. 替换 / 还原 homebrew-bottles 访问地址

这个步骤跟你的 macOS 系统使用的 shell 版本有关系

+

所以,先来查看当前使用的 shell 版本

+
1
2
3
4
echo $SHELL

# 如果你的输出结果是 /bin/zsh,参考下方的 zsh 终端操作方式
# 如果你的输出结果是 /bin/bash,参考下方的 bash 终端操作方式
+

3.1 zsh 终端操作方式

1
2
3
4
5
6
7
8
9
10
# 替换成阿里巴巴的 homebrew-bottles 访问地址:
echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles' >> ~/.zshrc
source ~/.zshrc

#=======================================================

# 还原为官方提供的 homebrew-bottles 访问地址
vi ~/.zshrc
# 然后,删除 HOMEBREW_BOTTLE_DOMAIN 这一行配置
source ~/.zshrc
+

3.2 bash 终端操作方式

1
2
3
4
5
6
7
8
9
10
# 替换 homebrew-bottles 访问 URL:
echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles' >> ~/.bash_profile
source ~/.bash_profile

#=======================================================

# 还原为官方提供的 homebrew-bottles 访问地址
vi ~/.bash_profile
# 然后,删除 HOMEBREW_BOTTLE_DOMAIN 这一行配置
source ~/.bash_profile
+ +

转载自:http://www.xiegangd.com/article/154055689187484

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/64/index.html b/en/page/64/index.html new file mode 100644 index 0000000000..242b2142fa --- /dev/null +++ b/en/page/64/index.html @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

vue插件开发

+

Vue.use({install(Vues){}})

+
+

Vue.use

把给到的内容执行一下
举例

+
1
2
3
4
function a(){
console.log('a')
}
Vue.use(a) // a
+

有 install 就执行 install

+
1
2
3
4
5
6
7
function a(){
console.log('a')
}
a.install = function(){
console.log('b')
}
Vue.use(a) // b
+

再进一步

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function a(){
console.log('a')
}
a.install = function(){
// console.log('b')
vue.mixin({ // 抽离公共逻辑 , 缺点:命名冲突,难以阅读
data(){ // data数据少的时候可以不用vuex 用mixin
return {
c:'this is mixin'
}
},
methods:{
// 混入方法
// 提示性弹窗 原:import 控制 显隐 现在:在根节点引入,通过mixin在方法控制显隐
}
// 混入生命周期
create(){
// 所有组件的create生命周期都执行 mixin先执行
}
})
}
Vue.use(a) // b
+ +

vue.util.defineReactive()

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
vue.util.defineReactive()

var test = {
testa: 1
}
setTimeout(()=>{
test.testa = 2
},1000)
vue.mixin({
beforeCreate(){
this.test = test
}
})

+ +

vue.extend vue.util.extend

+

vue.util.extend ===> 简单做了个拷贝,拷贝到一起

+
1
vue.util.extend(a,b)
+

vue.extend ===> 获取到某个对象的实例

+
1
2
let Constrator = vue.extend(obj)
let vm = new Constrator()
+ +

手写vue-router

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// myVueRouter.js
class HistoryRoute(){
constructor(){
this.current = null;
}
}

class vueRouter{
constructor(options){
this.mode = options.mode || 'hash'
this.history = new HistoryRoute
this.routes = options.routes||[]
this.routesMap = this.createMap(this.routes)
this.init()
}
init(){
if(this.mode == 'hash'){
// 自动加上 #
location.hash?"":location.hash="/"
window.addEventListener('load',()=>{
this.history.current = location.hash.slice(1)
})
window.addEventListener('hashchange',()=>{
this.history.current = location.hash.slice(1)
})
}else{
location.pathname?"":location.pathname="/"
window.addEventListener('load',()=>{
this.history.current = location.hash.pathname
})
window.addEventListener('popstate',()=>{
this.history.current = location.hash.pathname
})
}
}
createMap(router){
return router.reduce((memo,current)=>{
memo[current.path] = current.component
})
}
}

vueRouter.install = function(Vue){
Vue.mixin({
beforeCreate(){ // 组件还未实例化好
if(this.$options && this.$options.router){ // 有配置而且引入路由
this._root = this
this._router = this.$option.router

Vue.util.defineReactive(this,'current',this._router.history)
}else{
this._root = this.$parent._root
}
// 增强健壮性
Object.defineProperty(this,'$route',{
get(){
return this._root._router
}
})
}
})
Vue.component('router-view',{
render(h){
// 如何根据当前的current,获取到对应的组件
let current = this._self._root._router.history.current
let routerMap = this._self._root._router.routeMap
return h(routeMap[current])
}
})
}
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/65/index.html b/en/page/65/index.html new file mode 100644 index 0000000000..1522312211 --- /dev/null +++ b/en/page/65/index.html @@ -0,0 +1,558 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

V8引擎如何回收垃圾

为什么我们要关注内存

    +
  • 防止页面占用内存过大,引起客户端卡顿,甚至无响应
  • +
  • Node使用的也是v8,内存对于后端服务的性能至关重要。因为服务的持久性,后端更容易造成内存溢出

    v8引擎的内存回收机制

    v8的内存分配

    新生代内存空间
  • +
  • from
  • +
  • to
    老生代内存空间

    内存大小

  • +
  • 和操作系统有关 — 64位(1.4G)32位(0.7G)
  • +
  • 64位下 新生代(64MB) 老生代(1400MB)
  • +
  • 32位下 新生代(16MB) 老生代(700MB)
  • +
+

为什么不占多一点内存

+
    +
  • js设计之初是为浏览器
      +
    • 前端特点 — 不持久化,执行一遍就回收了,所以1.4G够用了
    • +
    • js回收内存会暂停执行代码
    • +
    +
  • +
+

垃圾回收算法

新生代简单的说就是复制

+
    +
  • 新生代用来放新产生的变量,根据条件放到老生代,有用的放到to里(把有用的留一边,清空另一边,往复进行)
  • +
  • 算法复杂度(时间复杂度,空间复杂度)—- 牺牲空间换时间
  • +
+

老生代就是标记、删除、整理

+
    +
  • 为什么要整理
      +
    • 数组是需要连续的空间
    • +
    +
  • +
+

新生代如何晋升到老生代

+
    +
  • 变量是否经过回收,第二次回收放到老生代,第一次回收放到To
  • +
  • To空间使用了25%,放到老生代
  • +
+

V8是如何处理变量的

1
2
3
4
5
6
7
8
9
10
11
12
13
// 浏览器查看内存
window.performance
// nodejs查看内存 --- nodejs是c++的,可以拓宽内存
process.memoryUsage()

// 拿内存的方法
function getMem(){
var mem = process.memoryUsage();
var format = function(bytes){
return (bytes/1024/1024).toFixed(2)+'MB';
}
console.log('heapTotal:'+format(mem.heapTotal)+'heapUsed:'+format(mem.heapUsed))
}
+

变量处理

    +
  • 内存主要就是存储变量等数据的
  • +
  • 局部变量当程序执行结束,且没有引用的时候就会随着消失 — 可以被回收但不是说会马上回收
  • +
  • 全局对象会始终存活到程序运行结束
  • +
+

如何查看V8内存使用情况

如何注意内存使用

优化内存的技巧

    +
  • 尽量不要定义全局变量
  • +
  • 全局变量记得手动销毁掉
      +
    • 不推荐开发时写delete – 支持有问题,严格模式有bug
    • +
    • 赋值为 undefined/null undefined 是变量 null 是保留字
    • +
    +
  • +
  • 用匿名自执行函数变全局为局部
      +
    • (function(){})()
    • +
    +
  • +
  • 尽量避免使用闭包 —- 错误的观点,ie5时代的问题 —- 尽量避免使用闭包引用
  • +
+

闭包

+
1
2
3
4
5
6
7
function a(){
var size = 20*1024*1024;
var arr1 = new Array(size)
return arr1
}
a() // 这样就没问题
var b = a() // 因为引用所以无法销毁
+

防止内存泄漏

    +
  • 滥用缓存
  • +
  • 大内存量操作
  • +
+

所有的优化都可以用缓存来解决,缓存通常都在全局,缓存可以直接拿来用;

+
1
2
3
4
5
6
7
8
9
10
11
12
13
var 20*1024*1024;
var a = []
for(var i=0;i<13;i++){
a.push(new Array(size))
}

// 加缓存锁
for(var i=0;i<13;i++){
if(a.length>4){
a.shift();
}
a.push(new Array(size))
}
+
    +
  • 不要用v8来缓存
      +
    • 一定要用要的话加锁
    • +
    +
  • +
+

nodejs中读取大文件要用流的形式,不要用读文件到buffer
fs.readFile()
fs.createReadStream()

+

浏览器中,大文件上传记得切片
file.slice(0,1000)
file.slice(1000,2000)

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/66/index.html b/en/page/66/index.html new file mode 100644 index 0000000000..920ab2285a --- /dev/null +++ b/en/page/66/index.html @@ -0,0 +1,606 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近一直在追网课,说实话从业9年了,一直觉得前端发展非常快,而且一直充满着危机感,每天都要学习进步才有安稳的感觉,今天听了 vue 响应式原理实现的公开课,感觉还不错,做了如下笔记,帮助自己记忆,也希望能帮助大家。

+

Vue2 原理

什么是 defineProperty

defineProperty 其实是定义对象属性用的

+
+

defineProperty 其实并不是核心的为一个对象做数据双向绑定,而是去给对象做属性标签,只不过是属性里 get 和 set 实现了响应式。

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
属性名默认值
valueundefined
getundefined
setundefined
writablefalse
enumerablefalse
configurablefalse
+
1
2
3
4
5
6
7
8
9
10
11
12
13
var ob = {
a:1,
b:2
}
// 参数 1、对象 2、属性 3、配置
Object.defineProperty(ob,'a',{
writable:false,
enumerable:true,
configurable:true,
})
console.log(Object.getOwnPropertyDescriptor(ob,'a')) // 1
ob.a = 2
console.log(ob.a) // 1
+

下面我们实现一下双向绑定

+
1
2
3
4
5
6
7
8
9
10
11
12
13
Object.defineProperty(ob,'a',{
get:function(){
console.log('a is be get')
return 999;
},
set:function(){
console.log('a is be set')
return 999;
},
})

console.log(Object.getOwnPropertyDescriptor(ob,'a')) // 999

+

改造代码实现双向绑定(存取值)

+
1
2
3
4
5
6
7
8
9
10
11
12
var _val = obj.a; // 暂存
Object.defineProperty(ob,'a',{
get:function(){
console.log('a is be get')
return _val;
},
set:function(newVal){
_val = newVal // 新值替换旧值
console.log('a is be set')
return _val;
},
})
+

Vue 中从改变一个数据到发生改变的过程

    +
  1. 改变数据触发 Set
  2. +
  3. Set 部分触发 notify(更新)
      +
    1. Get 部分收集依赖
    2. +
    +
  4. +
  5. 更改对应的虚拟 Dom
  6. +
  7. 重新 Render
  8. +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// MyVue.js

// 简单版本 vue
function MyVue(){
this.$data = {
a: {
b:1
},
c:2
}
this.el = document.getElementById('app');
this.virtualDom = '';
this.observer(this.$data);
this.render();
}
vue.property.observer = function(obj){
var _val, self = this;
// var dep = new Dep() -> 源码中依赖收集对象
for(var key in obj){ // 属性有可能是对象,要递归绑定
_val = obj[key];
if(typeof _val === 'Object'){
this.observer(_val)
}else{
Object.defineProperty(this.$data,key,{ // 这里是实际绑定过程
get:function(){
// 依赖收集
// dep.depend(); -> vue 源码中收集依赖的方法
return _val
},
set:function(newVal){
_val = newVal
// dep.notify(); -> vue 源码中
self.render() // AST语法树
}
})
}
}
}
vue.property.render = function(){
this.virtualDom = 'i am '+this.$data.b;
this.el.innerHTML = this.virtualDom;
}
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// index.html

<!DOCTYPE html>
<html>
<head>
<title>自己实现Vue2数据双向绑定</title>
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src='myVue.js'></script>
<script type="text/javascript">
var mv = new MyVue();
setTimeout(function(){
console.log('changes');
console.log(mv.$data);
mv.$data.b = 222;
})
</script>
</body>
</html>
+
+

依赖收集:

+
    +
  1. 我们的data里面的数据并不是所有地方都用到
  2. +
  3. 如果我们直接更新整个视图,浪费资源
  4. +
  5. 先收集依赖改变的数据的组件,再更新依赖了数据的组件(Dep depend notify)
  6. +
+
+

格外注意的地方—数组怎么监听

definePropty 只能给对象进行 get set 绑定, 数组怎么办?

+

vue 中 使用了 装饰者模式

+
+

装饰者模式 Decorator模式(别名Wrapper):动态将职责附加到对象上,若要扩展功能,装饰者提供了比继承更具弹性的代替方案。

+
+
1
2
3
4
5
6
7
8
9
10
var arraypro = Array.property; // 创建一个数组的原型对象
var arrob = Object.create(arraypro); // 避免影响原型链
var arr = ['push','pop','shift'];
arr.forEach(function(method,index){
arrob[method]=function(){ // 装饰者模式
var ret = arraypro[method].apply(this,arguments)
dep.notify() // 扩展了功能
}
})

+

Vue3 实现双向绑定

Proxy 是什么?

+
+

Proxy 对象用于定义基本操作的自定义行为
和 definePropty 类似,功能几乎一样,只是用法上不同

+
    +
  1. 不会污染原对象
  2. +
  3. 直接给对象就可以了
  4. +
  5. 不需要借助外部变量 _val
  6. +
+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var ob = {
a:1,
b:2
}

var newOb = new Proxy(ob,{
get(target,key,receiver){ // target 对象,key 属性
console.log(target,key,receiver)
return target[key]
},
set(target,key,value,receiver){
return Reflect.set(target.key,value);
// 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
// return target[key] = value
}
})
+

为什么改用 Proxy

    +
  1. defineProperty 只能监听某个属性,不能全对象监听
  2. +
  3. 可以省去for in循环提升代码执行效率
  4. +
  5. 可以监听数组,不需要再为数组做特异性操作
  6. +
  7. 不污染原对象
  8. +
  9. 更优雅
  10. +
+

我们用 Proxy 实现一下 observe 方法

+
1
2
3
4
5
6
7
8
9
10
11
12
vue.property.observe = function(){
var self = this;
this.$data = new Proxy(this.$data,{
get(target,key, receiver){
return target[key]
},
set(target,key,newVal){
target[key] = newVal
self.render()
}
})
}
+ +

还能用 Proxy 做什么

    +
  1. 校验类型
  2. +
  3. 真正的私有变量
  4. +
+
校验类型

例子:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 数据类型验证
// 我们要创建一个对象,这个对象是个人,他有name和age两个属性
// name必须是中文,age必须是数字,大于18岁

// 这里用到了策略模式
var valid = {
name(value){
var reg=/^[\u4E00-\u9FAS]=$/
if(typeof value === 'string' && reg.test(value)){
return true;
}
return false;
},
age(value){
if(typeof value === 'number' && value > 18){
return true;
}
return false;
}
}
function Person(name,age){
this.name = name
this.age = age
return new Proxy(this,{
get(target,key){
return target[key]
},
set(target,key,value){
if(valid[key](value)){
return Reflect.set(target,key,value)
}else{
throw new Error(key+'is not valid')
}
}
})
}
new Person('name',19)
+
+

策略模式
定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。

+
+
真正的私有变量

vue-router 源码中,给 $router ,$route 用 defineProperty 定义 get 并返回本身,这样就不能修改属性了。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
Object.defineProperty(this,'$router',{ // Router 的实例
get(){
return this._root._router;
}
})
Object.defineProperty(this,'$route',{
get(){
return {
// 当前路由所在的状态
current: this._root._router.history.current;
}
}
})
+ +

虚拟Dom和diff算法

虚拟Dom是虚拟的,他只在概念里面存在,在AST语法树,下面进行解释

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 模板
<template>
<div>
<p>{{msg}}</p>
<p>2</p>
<p>3</p>
</div>
</template>

// diff 描述法
diff <div>
props:{
id:2
}
children:[
diff <p>
props:{
id:xxx
}
children:[
...
]
]

// 对象描述法
var virtual = {
dom:'div',
props:{
id:2
},
children:[
....
]
}
+

每层结构都是一样的,那么是如何进行 diff 比对的呢?

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** 
* diff 算法
*/
patchVnode(oldVnode,vnode){ // 接收新旧节点
const el = vnode.el = oldVnode.el; // 拿出真实dom
let i,oldCh = oldVnode.children ,ch = vnode.children // 拿出新旧节点子元素children数组
if(oldVnode === vnode) return; // 新旧节点一致,直接返回不进行后续操作
// 分情况操作 --- 只有文字节点,删除了子元素,增加了子元素,子元素发生变动
// 新旧节点都不为空,且不一样
if(oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text){ // 文字发生更新
api.setTextContent(el,vonde.text) // 文字发生了更新,更新文字,给Dom,更新文字
} else { // 不是单纯文字节点的话
updateEle(); // 更新元素
if(oldCh&&ch&&oldCh!==ch){ // 都有子元素,子元素变动
updateChildren() // 调用更新子元素方法
} else if(ch){ // 增加子元素
createEl(vnode) // 创建子元素
} else if(oldCh){ // 删除子元素
api.removeChildren(el) // 调用删除子元素方法
}
}
}
+

源码要多看,以下必看 Vue/react/axios/vue-router/Redux/Vuex
为什么要看源码??

+
    +
  • 初级前端就会用vue或react — 从差不多水平的60%中挑出更好的人
  • +
  • 提高思想–》看优秀的代码–》写优秀的代码
  • +
  • 看源码能力,对高级前端是必备的。— 解决疑难杂症,看源码了解原理。
  • +
+

vue 性能优化

因为是公开课,所以时间上没来的及说完,以后自己在听别的有关的内容时再补上这块。

+

最后

只有不断学习才能进步,充分利用网络的便利性,找各种优质的教学资源,我相信,努力会有回报,加油!

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/67/index.html b/en/page/67/index.html new file mode 100644 index 0000000000..c812644f27 --- /dev/null +++ b/en/page/67/index.html @@ -0,0 +1,564 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

这篇文章想写一写前端工程化相关的内容,原因一呢是是结合公司业务给自己定的业绩指标包含这些内容,另外一个原因是因为听了网易前端唐磊说过的一句话,前端leader干什么,很重要的工作就是前端工程化,高级以上不懂前端工程化很难。

+

今天听了公开课讲到了用node写一个自己的cli,说实话正是工作所需,课程讲的有点快,没有从头跟下来,自己上完了课又上网上找了些资料,终于把步骤弄明白了,下面就把如何自定义一个cli来帮助提升开发效率。同时也完成了一个业务指标,心里美滋滋。

+

准备

如果你看到这篇文章,也想跟着我的步骤写一下这个自定义cli,那么如下的知识还是有一些为好。

+
    +
  • 基础的nodejs相关知识
  • +
+

没错就只需要会一些node的基础知识就可以了,接下来正式开始

+

初始化

首先,我们要给我们的命令行工具起个名字,比如我们熟悉的 vue 命令行就是 vue-cli ,因为我写完了要给公司用,所以我起的名字是 tfd-cli ,你们喜欢叫什么你们随意

+

首先创建一个名字为 tfd-cli 的文件目录,然后在目录下执行 node 工程的初始化命令

+
1
npm init -y // 初始化项目 -y 默认全部yes的参数
+

命令执行完成后 tfd-cli 目录下会生成一个我们熟悉的 package.json 文件,我们打开 package.json 文件,增加一段代码,如下

+
1
2
3
4
// 追加的代码
"bin": {
"tfd": "index.js"
}
+

追加完成后,package.json 文件中的内容是这样的

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"name": "tfd-cli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"bin": {
"tfd": "index.js"
}
}
+

也就是说当我们执行 tfd 命令时它就会找同级目录下的 index.js 文件执行其中代码,我们目前还没有 index.js,文件,那么我们手动创建一个 index.js 的文件,然后在里面写下如下代码

+
1
2
3
4
#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的

console.log('hello world');
+

执行完这些还不够,因为我们是开发环境所以还有一步操作是要将 tfd 命令告知 npm,该如何处理,所以我们要执行如下命令

+
1
npm link
+

这里如果执行不成功,请用管理员权限执行,执行完成后我们会得到一个 package-lock.json 的文件,内容如下

+
1
2
3
4
5
{
"name": "tfd-cli",
"version": "1.0.0",
"lockfileVersion": 1
}
+

如此一来,我们就可以在任何目录下执行命令行 tfd 就会执行 index.js 文件了,这里我们会在控制面板中输出 hello world ,怎么样是不是小有成就感,我们接着往下来。

+

创建指令

我们写个命令行工具肯定不是为了输出个 hello world 这么简单,而是希望通过用户输入内容后根据条件输出一些东西,那么让我们想想一个命令行工具应该具备哪些指令呢?

+
    +
  • 查看命令行工具版本
  • +
  • 查看帮助文档
  • +
  • 初始化模板
  • +
  • 列出模板类型
  • +
  • 等等
  • +
+

那么用指令该如何描述呢

+
1
2
3
4
tfd -V|--version //查看工具版本号
tfd -h|--help //查看使用帮助
tfd init <template-name> <project-name> //基于指定模板进行项目初始化
tfd list //列出所有可用模板
+

为了执行命令,这里我们要引入一个 node 包叫做 commander,因此我们要先执行一下 install 命令

+
1
npm install commander
+

接着我们就可以在 index.js 里面写指令了。

+
1
2
3
4
5
6
7
8
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0
+

到这一步我们在控制台敲一下 tfd -V 你会发现什么也没输出,这是因为到这一步我们还无法解析 tfd -V 操作,在这之前我们要知道一个命令

+
1
process.argv // 使用process.argv获取命令行参数
+

当我们把这句话加到 console.log 中在 index.js 中输出时你会看到控制台打印出

+
1
2
3
4
5
6
7
8
9
10
11
12
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0
console.log(process.argv)

// 控制台
[ '/usr/local/bin/node', '/usr/local/bin/tfd', '-V' ]
+

接下来我们要让commander获取参数执行命令

+
1
2
3
4
5
6
7
8
9
10
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0
// console.log(process.argv)
cmd.parse(process.argv);
+

这个时候我们再在控制台输入 tfd -V 时,我们就会发现,控制台输出了 0.1.0,这样我们就完成了查版本的指令,接下来我们完成其他的指令

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0

// tfd init <template> <project>
cmd
.command('init <template> <project>') // 参数
.description('初始化项目模板')
.action((templateName, projectName) => {
console.log(templateName, projectName);
})

// tfd list
cmd
.command('list')
.description('查看所有可用模板')
.action(() => {
console.log(`
a a模板
b b模板
c c模板
`)
})
// console.log(process.argv)
cmd.parse(process.argv);
+

这个时候我们在控制台上输入 tfd -h 的时候,控制台会输出如下代码

+
1
2
3
4
5
6
7
8
9
Usage: tfd [options] [command]

Options:
-V, --version output the version number
-h, --help output usage information

Commands:
init <template> <project> 初始化项目模板
list 查看所有可用模板
+

这样我们就实现了自定义命令,我们执行一下 tfd init template1 project1,我们可以看到,控制台中输出了 template1 project1,也就是说command命令后尖括号中指向了action中的参数,我们就可以通过判断action中的参数做具体的操作了。

+

通常模板可以选择从本地拷贝一份,但更常用的是从线上拷贝一份,比如从github中,接下来我们就看看如何从github中拷贝一个模板作为项目的初始化工程

+

github上创建模板仓库

首先我们要在github上创建两个仓库 tpl-1 tpl-2,这里为了从github中下载仓库我们需要一个node包支持,让我们请出download-git-repo,别忘了执行安装命令

+
1
npm install download-git-repo
+

安装完依赖之后让我们再去index.js填点东西,首先引入下载依赖,然后是创建下载的 template 抽象对象

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');
const download = require('download-git-repo');
// 可用模板
const templates = {
'tpl-1': {
url: 'https://github.com/lixuguang/tpl-1',
downloadUrl: 'https://github.com:lixuguang/tpl-1#master',
description: 'tfd-cli脚手架测试模板1'
},
'tpl-2': {
url: 'https://github.com/lixuguang/tpl-2',
downloadUrl: 'https://github.com:lixuguang/tpl-2#master',
description: 'tfd-cli脚手架测试模板2'
}
}

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0

// tfd init <template> <project>
cmd
.command('init <template> <project>') // 参数
.description('初始化项目模板')
.action((templateName, projectName) => {
// console.log(templateName, projectName);
let {downloadUrl} = templates[templateName];
// 第一个参数是github仓库地址,第二个参数是创建的项目目录名,第三个参数是clone
download(downloadUrl, projectName, {clone: true}, err => {
if(err){
console.log('模板下载失败');
}else{
console.log('模板下载成功');
}
})
})

// tfd list
cmd
.command('list')
.description('查看所有可用模板')
.action(() => {
// console.log(`
// a a模板
// b b模板
// c c模板
// `)
// 通过获取templates里的key可以获取到模板名称
const templateName = Object.keys(templates)
console.log(templateName)
})
// console.log(process.argv)
cmd.parse(process.argv);
+

这样当我们执行 tfd list 就可以看到有哪些模板了,然后执行tfd init tpl-1 newproject 就可以依据 tpl-1 模板创建出 newproject 工程,这个过程实际上就是从github仓库克隆一份tpl-1作为模板创建工程newproject

+

这里需要注意的是download地址跟github仓库地址有点出入,比如github仓库地址是https://github.com/xxx/xxx而下载地址是https://github.com:xxx/xxx

+

虽然这样执行完成后就完成了基本的cli的雏形,但是毕竟不灵活,我们在使用vue-cli时,它的创建过程是问答式和选择式的,另外每个过程都会有进度显示什么的,那么要怎么添加这些功能呢,我们接着往下做。

+

进阶增加功能

使用inquirer进行命令行答询

inquirer 是一个进行命令行答询的库,通过它我们就可以创建问答式的内容,首先还是安装依赖

+
1
npm install inquirer
+

使用handlebars修改package.json

我们都知道在使用vue-cli的初始化命令后,会在项目目录下生成一个package.json文件,它就像是这个项目的基因序列一样,影响着项目的整个结构。模板是固定的,那要修改其中的package.json符合自己项目的需要,就要用到handlebars这个库来改写package.json文件,老规矩先安装它

+
1
npm install handlebars
+

使用ora在命令行中显示加载状态

我们在装任何依赖时都会有进度条显示进度,如果没有进度条又没有任何响应,会让用户迷茫,为了友好,我们就要加进度条,这里我们需要引入ora这个库来完成进度显示,我们接着安装。

+
1
npm install ora
+

使用chalk和log-symbols增加命令行输出样式

为了让命令行有红红绿绿的效果以及符号效果,我们需要使用chalklog-symbols来丰富样式,少废话,接着装

+
1
npm install chalk log-symbols
+ +

集大成

终于安装完一堆的依赖,别忘了在index.js中引入,让我们看看具体如何使用这些库吧。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');
const download = require('download-git-repo');

const iq = require('inquirer'); // 命令行答询
const hb = require('handlebars'); // 修改package.json文件
const ora = require('ora'); // 命令行中加载状态标识
const chalk = require('chalk'); // 命令行输出字符颜色
const ls = require('log-symbols'); // 命令行输出符号
const fs = require('fs'); // node fs原生模块

// 可用模板
const templates = {
'tpl-1': {
url: 'https://github.com/lixuguang/tpl-1',
downloadUrl: 'https://github.com:lixuguang/tpl-1#master',
description: 'tfd-cli脚手架测试模板1'
},
'tpl-2': {
url: 'https://github.com/lixuguang/tpl-2',
downloadUrl: 'https://github.com:lixuguang/tpl-2#master',
description: 'tfd-cli脚手架测试模板2'
}
}

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0

// tfd init <template> <project>
cmd
.command('init <template> <project>') // 参数
.description('初始化项目模板')
.action((templateName, projectName) => {
// console.log(templateName, projectName);
let {downloadUrl} = templates[templateName];
//下载github项目,下载墙loading提示
const loading = ora('模板下载中...').start();
// 第一个参数是github仓库地址,第二个参数是创建的项目目录名,第三个参数是clone
download(downloadUrl, projectName, {clone: true}, err => {
if(err){
// console.log('模板下载失败');
loading.fail('模板下载失败');
}else{
// console.log('模板下载成功');
spinner.succeed('模板下载成功');
// 命令行答询
iq.prompt([
{
type: 'input', // 类型 输入框
name: 'name', // 字段 key
message: '请输入项目名称', // 描述
default: projectName // 默认值
},
{
type: 'input',
name: 'description',
message: '请输入项目简介',
default: ''
},
{
type: 'input',
name: 'author',
message: '请输入作者名称',
default: ''
}
]).then(answers => { // answers 是一个对象,对象的 key 为上面答询的 name 的值,value 为 用户输入的值,如果未输入,就取默认值
// 根据命令行答询结果修改 package.json 文件
let packageContent = fs.readFileSync(`${projectName}/package.json`, 'utf8'); // 同步方式以 utf-8 字符集获得下载好的项目目录下的 package.json 文件
let packageResult = hb.compile(packageContent)(answers); // 将用户输入项与原内容混合获得新内容
fs.writeFileSync(`${projectName}/package.json`, packageResult); // 重新同步方式写入到 package.json 文件中
// 用chalk和log-symbols改变命令行输出样式
console.log(ls.success, chalk.green('模板项目文件准备成功!'));
})
}
})
})

// tfd list
cmd
.command('list')
.description('查看所有可用模板')
.action(() => {
// console.log(`
// a a模板
// b b模板
// c c模板
// `)
// 通过获取templates里的key可以获取到模板名称
const templateName = Object.keys(templates)
console.log(templateName)
})
// console.log(process.argv)
cmd.parse(process.argv);
+ +

到这里你自己的命令行工具就创建完成了,当然还可以继续丰富,比如加上选择条件等,这个就更复杂了,不在本文中展开,后续会继续改进这个命令行工具,当然如果要将自己的命令行工具给其他人用当然要先发布出去,就像我们要用vue-cli,首先要在npm上下载下来,同样的我们要想让别人用,就得先上传到npm上。

+

发布到npm上

首先你需要有一个npm的账号,没有赶快去注册一个,在控制台中输入npm login,它会让你依次输入 Username Password Email ,当你都按照要求输入完之后,成功的话你会获得如下信息Logged in as XXX on https://registry.npm.org/.,再接下来执行 npm publish 命令,你的自定义脚手架就会发布到npm上,供他人下载使用,怎么样,学会了么?

+

后记

这篇文章会有后续持续进化跟进,多篇文章连续,只要我的cli工具还在进化,文章就会继续,欢迎跟进。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/68/index.html b/en/page/68/index.html new file mode 100644 index 0000000000..73cc12c522 --- /dev/null +++ b/en/page/68/index.html @@ -0,0 +1,500 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

最近又有小伙伴问我,为啥我在写vue项目做代理转发时候报找不到代理服务的问题?
我看了一下他发给我的配置截图,仅一眼我就发现了问题所在,因为这个坑我曾经也踩过,而且好多小伙伴也都踩过,在写转发服务器的时候没写协议头,也就是在 target 那没写 http:// 这个部分,添加上以后重启一下,这个问题就解决了,小伙伴说就这个问题他解决了一天也没解决了,而且我说完了以后他想起来以前好像也遇到过这个问题,所以说在平时工作中遇到问题要常做笔记才对,所以为了避免以后自己也犯这种错误,暂且留一篇文章记录一下这个问题。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/69/index.html b/en/page/69/index.html new file mode 100644 index 0000000000..74e85396e6 --- /dev/null +++ b/en/page/69/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

这两天复工,公司一个小伙伴在群里问了一个问题

+
+

如何在打印表格的时候,让超过一页的表格分割线不被截断

+
+

说着他有贴上来了一张图,一张因为跨页被截断了的表格,说实话打印的场景接触的不多,打印表格一般都是制式的样式所以也不会出现这问题,一时间也没有头绪,想着这个是不是没什么办法解决,隔天他发来一个消息,说是解决了,给表格里加上了 tr td 就好了,接着他又发来一张图,还真是,不过上下边距发生了一些变化,我问他是不是自己改了边距所以好了,他说没改,然后我问他那他是怎么解决的,他说因为实在找不到问题解决方案和产生原因,于是就把代码重构了一下,按照标准写法写了一下,结果就好了。。。怎么说呢,也是神奇,我也到网上去查了查相关的材料,并没有特别对的上的原因,凭感觉来说,应该是thead和tbody上有默认的css样式解决了上面的问题,暂且记下这个问题,避免以后遇到又忘了是怎么回事。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/7/index.html b/en/page/7/index.html new file mode 100644 index 0000000000..7a2b02f402 --- /dev/null +++ b/en/page/7/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Oracle Linux install Docker(https://docs.docker.com/engine/install/centos/)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 查看
systemctl list-units

sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine

sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

sudo yum install -y yum-utils

sudo yum -y install docker-ce-20.10.17 docker-ce-cli-20.10.17 containerd.io-1.6.6

sudo vi /etc/yum.repos.d/docker-ce.repo

[centos-extras]
name=Centos extras - $basearch
baseurl=http://mirror.centos.org/centos/7/extras/x86_64
enabled=1
gpgcheck=1
gpgkey=http://centos.org/keys/RPM-GPG-KEY-CentOS-7

sudo yum -y install fuse-overlayfs slirp4netns

# sudo chmod 777 docker-ce.repo

# yum list docker-ce --showduplicates | sort -r

sudo systemctl start docker

sudo docker run hello-world

sudo yum install /path/to/package.rpm
sudo systemctl start docker
sudo docker run hello-world
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/70/index.html b/en/page/70/index.html new file mode 100644 index 0000000000..0b3f4584b0 --- /dev/null +++ b/en/page/70/index.html @@ -0,0 +1,513 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

什么是 RESTful

+

REST 指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是 RESTful。

+
+

实际上就是 url 风格/ 路由风格 , 给出一种写路由的原则 — 面向资源
tip:静态路由利于seo优化 api/news/1,动态路由不利于seo优化 api/news?id=1

+

原来的风格
| 路由 | 功能 | 描述 |
| —- | —- |——|
| http://127.0.0.1/user/query/1 | 查询 | 根据ID查询用户信息 |
| http://127.0.0.1/user/save | 保存 | 注册用户 |
| http://127.0.0.1/user/update | 更新 | 修改用户 |
| http://127.0.0.1/user/delete/{id} | 删除 | 删除用户 |

+

RESTful — 面向资源:对于同一个资源都在同一个 URL 进行,通过判断 HTTP 请求的类型来决定做不同的事
/user 一个资源
| 路由 | 请求类型 |
| ———————– | ——– |
| http://127.0.0.1/user/1 | GET |
| http://127.0.0.1/user | POST |
| http://127.0.0.1/user | PUT |
| http://127.0.0.1/user | DELETE |

+

URL 描述资源 , HTTP 描述请求。(HTTP协议无语义无状态 —- 降低复杂度,业务无关 — 架构代码业务无关)

+

RESTful 采用的是顶层路由

+
+

顶层路由设计:不需要有物理文件映射路由

+
+
1
2
3
4
5
6
7
8
// express
// app.js
const express = require('express')
const app = express()
app.get('/case.avi',(req, res)=>{
res.send('hello world'); // 不需要对应物理文件
})
app.listen(3000)
+

原生接口

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// index.js
const http = require('http');
const mysql = require('mysql'); // mysql
const co = require('co-mysql') // 异步同步化
const md5 = require('md5-node') // md5加密

// 连接数据库
let db = mysql.createPool({ // 连接池自己管理 不用关闭
host:'localhost',
user:'root',
password:'root',
database:'user'
})
let conn = co(db)

const app = http.createServer(async (req,res)=>{
if(req.method === 'POST'){
if(req.url === '/user'){
// res.end(JSON.stringify({'message':'对user发起post请求'}))
req.on('data', async (data)=>{
arr.push(data)
})
req.on('end',async ()=>{
let buffer = Buffer.concat(arr);
// json对象
let {username,pasword} = JSON.parse(buffer.toString())
// console.log(username,pasword)
let sql = `selct user from admin where user = ${username}`
let data = await conn.query(sql);
// console.log(data)
if(data.length >=1 ){
res.end(JSON.stringify({
'status':200,
'message':'用户名已经注册'
}))
}else{
// 写入数据库
password = md5(password);
let sql = `INSERT INTO admin (user,password) VALUES ('${username}','${password}')`
await conn.query(sql);
res.end(JSON.stringify({
'status':200,
'message':'注册成功'
}))
}
})
}
}if(req.method === 'GET'){
if(req.url === '/user'){
// res.end(JSON.stringify({'message':'对user发起get请求'}))
let sql = `SELECT id,user,password FROM admin`
let data = await conn.query(sql);
res.end(JSON.stringify(data))
}
}
}).listen(3000)

// .http 文件
@url = http://localhost:3000
@type = Content-Type: applications

GET {{url}}/user HTTP/1.1

POST {{url}}/user HTTP/1.1
{{type}}

{
username:'admin',
password:123456
}
+

使用express实现(express — generater yard ,koa — async await)

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

const express = require('express')
const app = express()
const mysql = require('mysql'); // mysql
const co = require('co-mysql') // 异步同步化
const md5 = require('md5-node') // md5加密
const bodyparse = require('body-parse')
// 连接数据库
let db = mysql.createPool({
host:'localhost',
user:'root',
password:'root',
database:'user'
})
let conn = co(db)

app.use(bodyparse.urlencoded({
extended:true // 返回对象是兼职对,false - string/array true - any
}))
app.use(bodyparse.json())

app.post('/user',async (req.res)=>{
let { username , password} = req.body
// console.log(username,pasword)
let sql = `selct user from admin where user = ${username}`
let data = await conn.query(sql);
// console.log(data)
if(data.length >=1 ){
res.send(JSON.stringify({
'status':200,
'message':'用户名已经注册'
}))
}else{
// 写入数据库
password = md5(password);
let sql = `INSERT INTO admin (user,password) VALUES ('${username}','${password}')`
await conn.query(sql);
res.send(JSON.stringify({
'status':200,
'message':'注册成功'
}))
}
})

app.get('/user/:id',(req,res)=>{
res.send(req.params.id)

let sql = `SELECT id,user,password FROM admin WHERE id = ${req.params.id}`
let data = await conn.query(sql);
res.end(JSON.stringify(data))
})

app.listen(3000)
+ +

使用koa实现

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53


// config.js
module.exports = {
host:'localhost',
user:'root',
password:'root',
database:'user'
}

// libs/database.js
const config = require('../config')
const mysql = require('mysql'); // mysql
const co = require('co-mysql') // 异步同步化
// 连接数据库
let db = mysql.createPool({
host:config.host,
user:config.user,
password:config.password,
database:config.database
})
let conn = co(db)

// router/user/index.js
const Router = require('koa-router')
const md5 = require('md5-node') // md5加密
const router = new Router();

router.get('/user',async ctx=>{
ctx.body = '主页'
})
router.post('/user',async ctx=>{
let {username,password} = ctx.request.body
// console.log(username,password)
ctx.body = {
username,password
}
})
module.exports = router.routes();

// app.js
const koa = require('koa')
const Router = require('koa-router')
const body = require('koa-bodyparse')
const config = require('config')
const app = new Koa()
const router = new Router()
app.context.db = require('./libs/database')
app.context.config = config
app.use(body())
router.use('/api',require('./router/user'))
app.use(router.routes())
app.listen(3000)
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/71/index.html b/en/page/71/index.html new file mode 100644 index 0000000000..d6f4f9d37d --- /dev/null +++ b/en/page/71/index.html @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

什么是SSR

传统浏览器的vue纯浏览器渲染

浏览器请求服务器,服务器返回静态资源 ,客户端负责渲染js

+

ssr

浏览器请求服务器,node服务端渲染js后,返回对应的页面返回给浏览器

+

SSR需要那些东西

手写SSR

特性:

+
    +
  • 每一次访问必须新建一个vue实例
  • +
  • 只会触发组件的 beforeCreate和created钩子
  • +
+

核心库

+
    +
  • vue
  • +
  • vue-server-renderer

    vue + next

    +

    作者:李旭光
    引用请标明出处

    +
    +
  • +
+

前言

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/72/index.html b/en/page/72/index.html new file mode 100644 index 0000000000..a310b24e3b --- /dev/null +++ b/en/page/72/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

网络安全是前端不可忽略的一个部分,可惜的是之前我都忽略了,最近发现网络安全已经变得日益重要,所以特别整理一篇文章说说网络安全相关的内容。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/73/index.html b/en/page/73/index.html new file mode 100644 index 0000000000..df0818487b --- /dev/null +++ b/en/page/73/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在学习Webpack相关的知识的时候对loader和plugin有点迷惑,两个特性都是用来做打包相关处理,那么他们有什么区别呢,为了弄清楚他们有什么区别,我开始了疯狂的查找资料,虽说每篇文章都说了一些自己的认识,但是并没有一个特别标准或者说容易理解的答案,我且先将它们记录下来,以便日后回顾之时可以有一些感触。那么接下来我将开始webpack的解迷之旅。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/74/index.html b/en/page/74/index.html new file mode 100644 index 0000000000..cbdd1cd4db --- /dev/null +++ b/en/page/74/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

es6中,arguments被取消掉了,但是函数还是会有参数不确定的时候,那么我们该怎么办呢,接下来将介绍一下如何在es6中完成arguments一样的效果。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/75/index.html b/en/page/75/index.html new file mode 100644 index 0000000000..a19a1d0857 --- /dev/null +++ b/en/page/75/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

今年我下定决心一定要好好地写博客,完善博客的内容,所以最近把原来记在各种笔记中的文章内容都一一转移到了博客,内容一多想找一篇文章时就变得很麻烦,我得去归档中或者分类中一篇篇的找,所以我觉得是时候该给我的博客添加搜索的功能了,我看了博客的config文件,其中并没有搜索相关的配置,我又看了主题的配置文件,这下让我找到了,有个local_search的选项,于是我就开始了博客搜索的研究。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/76/index.html b/en/page/76/index.html new file mode 100644 index 0000000000..af22f161b1 --- /dev/null +++ b/en/page/76/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在整理自己的知识体系,一方面为了夯实基础,另一方面也为了查缺补漏、重新理解,设计模式之前看过大概三四次,从最开始一点都不懂,到后面看了几遍以后慢慢理解,今天再次整理一下,希望这一遍下来之后能更加透彻的理解什么是设计模式。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/77/index.html b/en/page/77/index.html new file mode 100644 index 0000000000..0e05e60e96 --- /dev/null +++ b/en/page/77/index.html @@ -0,0 +1,580 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

继承的操作需要有一个父类,这里使用构造函数外加原型来创建一个:
有下面两个类,下面实现 Child 继承 Father:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

function Father() {
this.type = 'prople';
}

Father.prototype.eat = function() {
console.log('吃东西啦');
};

function Child(name) {
this.name = name;
this.color = 'black';
}
+ +

原型继承(认贼作父)

+

关键点:子类原型等于父类的实例 Child.prototype = new Person()(将父类指向子类的原型)。

+
+
1
2
// js代码
Child.prototype = new Father();
+ +

特点:
实例可继承的属性有:

+
    +
  • 实例的构造函数的属性
  • +
  • 父类构造函数的属性
  • +
  • 父类原型上的属性
    新实例不会继承父类实例的属性
  • +
+

缺点:

+
    +
  • 新实例无法向父类构造函数传参
  • +
  • 继承单一
  • +
  • 所有新实例都会共享父类实例的属性。— 原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改
  • +
+

构造继承(借腹生子)

+

在子类构造函数中调用父类构造函数

+
+
1
2
3
4
// js代码
function Child(name) {
Father.call(this);
}
+ +

关键点:用 call 或 apply 将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制))Person.call(this, ‘reng’)
特点:

+
    +
  • 只继承了父类构造函数的属性,没有继承父类原型的属性
  • +
  • 解决了原型链继承的注意事项(缺点)1,2,3
  • +
  • 可以继承多个构造函数的属性(call 可以多个)
  • +
  • 在子实例中可以向父实例传参
    缺点:
  • +
  • 只能继承父类构造函数的属性
  • +
  • 无法实现构造函数的复用。(每次用每次都要重新调用)
  • +
  • 每个新实例都有构造函数的副本,臃肿
    (不能继承父类原型,函数在构造函数中,每个子类实例不能共享函数,浪费内存。)
  • +
+

组合继承(原型继承+构造继承)

+

使用构造继承继承父类参数,使用原型继承继承父类函数

+
+
1
2
3
4
5
6
7
// js代码
function Child(name) {
// 构造继承
Father.call(this);
}

Child.prototype = Father.prototype; // Child.prototype = new Person(); // 原型继承
+ +

关键点:结合了两种模式的优点–向父类传参(call)和复用(prototype)
特点:

+
    +
  • 可以继承父类原型上的属性,可以传参,可复用
  • +
  • 每个新实例引入的构造函数属性是私有的
  • +
+

缺点:

+
    +
  • 父类原型和子类原型是同一个对象,无法区分子类真正是由谁构造。
  • +
  • 调用了两次父类的构造函数(耗内存)
  • +
  • 子类的构造函数会代替原型上的那个父类构造函数(call 相当于拿到了父类构造函数的副本)
  • +
+

原型式继承(复制降级)

1
2
3
4
5
6
7
8
9
10
11
// 先封装一个函数容器,用来承载继承的原型和输出对象
function create(obj) {
// 寄生
function F() {}
F.prototype = obj;
return new F();
}
var father = new Father();
var child = create(father);
console.log(child instanceof Father); // true
console.log(child.job); // frontend
+ +

关键点:用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了可以随意增添属性的实例或对象。Object.create()就是这个原理。

+

特点:

+
    +
  • 类似于复制一个对象,用函数来包装
  • +
+

注意事项:

+
    +
  • 所有的实例都会继承原型上的属性
  • +
  • 无法实现复用。(新实例属性都是后面添加的)
    Object.create()方法规范了原型式继承。这个方法接收两个参数,一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。
  • +
+
1
2
3
4
5
6
7
8
9
10
11
// 传一个参数的时候
var child = Object.create(new Father());
console.log(child.job); // frontend
console.log(child instanceof Father); // true
// 传两个参数的时候
var child = Object.create(new Father(), {
name: {
value: 'come on'
}
});
child.sayHello(); // Hello come on
+ +

寄生组合继承

它跟组合继承一样,都比较常用。
寄生:在函数内返回对象然后调用
组合

+
    +
  • 函数的原型等于另一个实例
  • +
  • 在函数中用 apply 或 call 引入另一个构造函数,可传参
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 寄生
function create(obj) {
function F() {}
F.prototype = obj;
return new F();
}
// object是F实例的另一种表示方法
var obj = create(Father.prototype);
// obj实例(F实例)的原型继承了父类函数的原型
// 上述更像是原型链继承,只不过只继承了原型属性

// 组合
function Child() {
// 构造
this.age = 100;
Father.call(this); // 这个继承了父类构造函数的属性
} // 解决了组合式两次调用构造函数属性的特点

// 重点
Child.prototype = obj; // 原型

console.log(Child.prototype.constructor); // Father
obj.constructor = Child; // 一定要修复实例
console.log(Child.prototype.constructor); // Child
var child = new Child();
// Child实例就继承了构造函数属性,父类实例,object的函数属性
console.log(child.job); // frontend
console.log(child instanceof Father); // true
+ +

重点:修复了组合继承的问题

+

在上面的问题中,你可能发现了这么一个注释obj.constructor = Sub; // 一定要修复实例。为什么要修正子类的构造函数的指向呢?

+

因为在不修正这个指向的时候,在获取构造函数返回的时候,在调用同名属性或方法取值上可能造成混乱。比如下面:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Car() {}
Car.prototype.orderOneLikeThis = function() {
// Clone producing function
return new this.constructor();
};
Car.prototype.advertise = function() {
console.log('I am a generic car.');
};

function BMW() {}
BMW.prototype = Object.create(Car.prototype);
BMW.prototype.constructor = BMW; // Resetting the constructor property
BMW.prototype.advertise = function() {
console.log('I am BMW with lots of uber features.');
};

var x5 = new BMW();

var myNewToy = x5.orderOneLikeThis();

myNewToy.advertise(); // => "I am BMW ..." if `BMW.prototype.constructor = BMW;` is not
// commented; "I am a generic car." otherwise.
+ +

object.create 在组合继承的基础上,子类继承一个由父类原型生成的空对象。

+
1
2
3
4
5
6
7
8
9
10
// js代码
function Child(name) {
Father.call(this);
}

Child.prototype = Object.create(Father.prototype, {
constructor: {
value: Child
}
});
+ +

inherits 函数 — Nodejs util.inherits 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// js代码

function inherits = function(ctor, superCtor) {
ctor.super_ = superCtor; // super_属性是子类继承父类时构造函数要写入的一个属性值.
ctor.prototype = Object.create(superCtor.prototype, {
constructor: {
value: ctor,
enumerable: false,
writable: true,
configurable: true
}
});
};
// 在上面的代码中 ctor 想要继承 superCtor ,我们姑且把 ctor 称作子类, superCtor 称作父类.

// 使用
function Child() {
Father.call(this);
//...
}
inherits(Child, Father);

Child.prototype.fun = ...
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/78/index.html b/en/page/78/index.html new file mode 100644 index 0000000000..15ceb420ef --- /dev/null +++ b/en/page/78/index.html @@ -0,0 +1,536 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

前言

面试中时常会出现需要手动实现某个功能的要求,所以我准备整理一个自己动手实现系列文章,这一次呢就是自己动手实现 promise

+

实现 promise 思路

基础步骤

+
    +
  1. 设定三个状态 PENDINGFULFILLEDREJECTED ,只能由 PENDING 改变为 FULFILLEDREJECTED ,并且只能改变一次
  2. +
  3. MyPromise 接收一个函数 executorexecutor 有两个参数 resolve 方法和 reject 方法
  4. +
  5. resolvePENDING 改变为 FULFILLED
  6. +
  7. rejectPENDING 改变为 FULFILLED
  8. +
  9. promise 变为 FULFILLED 状态后具有一个唯一的 value
  10. +
  11. promise 变为 REJECTED 状态后具有一个唯一的 reason
  12. +
+

** then 方法**

+
    +
  1. then 方法接受两个参数 onFulfilledonRejected ,它们分别在状态由 PENDING 改变为 FULFILLEDREJECTED 后调用
  2. +
  3. 一个 promise 可绑定多个 then 方法
  4. +
  5. then 方法可以同步调用也可以异步调用
  6. +
  7. 同步调用:状态已经改变,直接调用 onFulfilled 方法
  8. +
  9. 异步调用:状态还是 PENDING ,将 onFulfilledonRejected 分别加入两个函数数组 onFulfilledCallbacksonRejectedCallbacks ,当异步调用 resolvereject 时,将两个数组中绑定的事件循环执行。
  10. +
+

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// js代码

// 定义状态常量
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

/**
* 定义MyPromise模拟Promise
* @param {func} executor 接收函数
*/
function MyPromise(executor) {
this.state = PENDING; // 默认状态为 pending
this.value = null;
this.reason = null;

// 定义成功失败的函数数组
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];

// 定义成功回调
const resolve = (value) => {
if (this.state === PENDING) {
this.state = FULFILLED;
this.value = value;

this.onFulfilledCallbacks.forEach(func => {
func();
});
}
}

// 定义失败回调
const reject = (reason) => {
if (this.state === PENDING) {
this.state = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach(func => {
func();
});
}
}

try {
executor(resolve, reject);
} catch (reason) {
reject(reason);
}
}

MyPromise.prototype.then = function (onFulfilled, onRejected) {
switch (this.state) {
case FULFILLED:
onFulfilled(this.value);
break;
case REJECTED:
onFulfilled(this.value);
break;
case PENDING:
this.onFulfilledCallbacks.push(() => {
onFulfilled(this.value);
})
this.onRejectedCallbacks.push(() => {
onRejected(this.reason);
})
break;
}
+

then方法异步调用

如下面的代码:输入顺序是:1、2、ConardLi

+
1
2
3
4
5
6
7
8
9
10
11
12
13
// js代码

console.log(1);

let promise = new Promise((resolve, reject) => {
resolve('ConardLi');
});

promise.then((value) => {
console.log(value);
});

console.log(2);
+

虽然 resolve 是同步执行的,我们必须保证 then 是异步调用的,我们用 setTimeout 来模拟异步调用(并不能实现微任务和宏任务的执行机制,只是保证异步调用)

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

// js代码
MyPromise.prototype.then = function (onFulfilled, onRejected) {
if (typeof onFulfilled != 'function') {
onFulfilled = function (value) {
return value;
}
}
if (typeof onRejected != 'function') {
onRejected = function (reason) {
throw reason;
}
}
switch (this.state) {
case FULFILLED:
setTimeout(() => {
onFulfilled(this.value);
}, 0);
break;
case REJECTED:
setTimeout(() => {
onRejected(this.reason);
}, 0);
break;
case PENDING:
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
onFulfilled(this.value);
}, 0);
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
onRejected(this.reason);
}, 0);
})
break;
}
}
+

then方法链式调用

保证链式调用,即 then 方法中要返回一个新的 promise ,并将 then 方法的返回值进行 resolve

+

注意:这种实现并不能保证 then 方法中返回一个新的 promise ,只能保证链式调用

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// js代码
MyPromise.prototype.then = function (onFulfilled, onRejected) {
if (typeof onFulfilled != 'function') {
onFulfilled = function (value) {
return value;
}
}
if (typeof onRejected != 'function') {
onRejected = function (reason) {
throw reason;
}
}

// 创建一个新的MyPromise对象
const promise2 = new MyPromise((resolve, reject) => {
switch (this.state) {
case FULFILLED:
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
break;
case REJECTED:
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
break;
case PENDING:
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
})
break;
}
})
return promise2;
}
+

catch方法

若上面没有定义 reject 方法,所有的异常会走向 catch 方法:

+
1
2
3
4
5
// js代码

MyPromise.prototype.catch = function(onRejected) {
return this.then(null, onRejected);
};
+

finally方法

不管是 resolve 还是 reject 都会调用 finally

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

MyPromise.prototype.finally = function(fn) {
return this.then(value => {
fn();
return value;
}, reason => {
fn();
throw reason;
});
};
+

Promise.resolve

Promise.resolve 用来生成一个直接处于 FULFILLED 状态的 Promise

+
1
2
3
4
5
6
// js代码
MyPromise.reject = function(value) {
return new MyPromise((resolve, reject) => {
resolve(value);
});
};
+

Promise.reject

Promise.reject 用来生成一个直接处于 REJECTED 状态的 Promise

+
1
2
3
4
5
6
// js代码
MyPromise.reject = function(reason) {
return new MyPromise((resolve, reject) => {
reject(reason);
});
};
+

all方法

接受一个 promise 数组,当所有 promise 状态 resolve 后,执行 resolve

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// js代码
MyPromise.all = function (promises) {
return new Promise((resolve, reject) => {
if (promises.length === 0) {
resolve([]);
} else {
let result = [];
let index = 0;
for (let i = 0; i < promises.length; i++) {
promises[i].then(data => {
result[i] = data;
if (++index === promises.length) {
resolve(result);
}
}, err => {
reject(err);
return;
});
}
}
});
}
+

race方法

接受一个 promise 数组,当有一个 promise 状态 resolve 后,执行 resolve

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码
MyPromise.race = function (promises) {
return new Promise((resolve, reject) => {
if (promises.length === 0) {
resolve();
} else {
let index = 0;
for (let i = 0; i < promises.length; i++) {
promises[i].then(data => {
resolve(data);
}, err => {
reject(err);
return;
});
}
}
});
}
+ +

最后

如此一个自定义的 promise 就实现了,怎么样学回来吗?

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/79/index.html b/en/page/79/index.html new file mode 100644 index 0000000000..1ad4409277 --- /dev/null +++ b/en/page/79/index.html @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

前言

最近各种技术博客,各种技术文章,各种牛人前辈的学习心得都在看,最近看了一个不错的文章叫做《如何正确使用时间》
感觉还是很不错的,特别适合年轻的程序员看一下。下面将全文贴在下方,如果需要也可以去原文查看。

+

原文

+

你是否时常会焦虑时间过的很快,没时间学习,本文将会分享一些个人的见解。

+

花时间补基础,读文档

在工作中我们时常会花很多时间去 debug,但是你是否发现很多问题最终只是你基础不扎实或者文档没有仔细看。

+

基础是你技术的基石,一定要花时间打好基础,而不是追各种新的技术。一旦你的基础扎实,学习各种新的技术也肯定不在话下,因为新的技术,究其根本都是相通的。

+

文档同样也是一门技术的基础。一个优秀的库,开发人员肯定已经把如何使用这个库都写在文档中了,仔细阅读文档一定会是少写 bug 的最省事路子。

+

学会搜索

如果你还在使用百度搜索编程问题,请尽快抛弃这个垃圾搜索引擎。同样一个关键字,使用百度和谷歌,谷歌基本完胜的。即使你使用中文在谷歌中搜索,得到的结果也往往是谷歌占优,所以如果你想迅速的通过搜索引擎来解决问题,那一定是谷歌。

+

学点英语

说到英语,一定是大家所最不想听的。其实我一直认为程序员学习英语是简单的,因为我们工作中是一直接触着英语,并且看懂技术文章,文档所需要的单词量是极少的。我时常在群里看到大家发出一个问题的截图问什么原因,其实在截图中英语已经很明白的说明了问题的所在,如果你的英语过关,完全不需要浪费时间来提问和搜索。所以我认为学点英语也是节省时间中很重要的一点。

+

那么如何去学习呢,chrome 装个翻译插件,直接拿英文文档或文章读,不会的就直接划词翻译,然后记录下这个单词并背诵。每天花半小时看点英文文档和文章,坚持两个月,你的英语水平不说别的,看文档和文章绝对不会有难题了。这一定是一个很划算的个人时间投资,花点时间学习英语,能为你将来的技术之路铺平很多坎。

+

画个图,想一想再做

你是否遇到过这种问题,需求一下来,看一眼,然后马上就按照设计稿开始做了,可能中间出个问题导致你需要返工。

+

如果你存在这样的问题,我很推荐在看到设计稿和需求的时候花点时间想一想,画一画。考虑一下设计稿中是否可以找到可以拆分出来的复用组件,是否存在之前写过的组件。该如何组织这个界面,数据的流转是怎么样的。然后画一下这个页面的需求,最后再动手做。

+

利用好下班时间学习

说到下班时间,那可能就有人说了公司很迟下班,这其实是国内很普遍的情况。但是我认为正常的加班是可以的,但是强制的加班就是在损耗你的身体和前途。

+

可以这么说,大部分的 996 公司,加班的这些时间并不会增加你的技术,无非就是在写一些重复的业务逻辑。也许你可以拿到更多的钱,但是代价是身体还有前途。程序员是靠技术吃饭的,如果你长久呆在一个长时间加班的公司,不能增长你的技术还要吞噬你的下班学习时间,那么你一定会废掉的。如果你遇到了这种情况,只能推荐尽快跳槽到非 996 的公司。

+

那么如果你有足够的下班时间,一定要花上 1, 2 小时去学习,上班大家基本都一样,技术的精进就是看下班以后的那几个小时了。如果你能利用好下班时间来学习,坚持下去,时间一定会给你很好的答复。

+

列好 ToDo

我喜欢规划好一段时间内要做的事情,并且要把事情拆分为小点。给 ToDo 列好优先级,紧急的优先级最高。相同优先级的我喜欢先做简单的,因为这样一旦完成就能划掉一个,提高成就感。

+

反思和整理

每周末都会花上点时间整理下本周记录的笔记和看到的不错文章。然后考虑下本周完成的工作和下周准备要完成的工作。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/8/index.html b/en/page/8/index.html new file mode 100644 index 0000000000..e256f86d06 --- /dev/null +++ b/en/page/8/index.html @@ -0,0 +1,778 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

服务器高危端口列表

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
协议端口服务渗透测试
tcp20,21FTP(文件传输协议)允许匿名的上传下载,爆破,嗅探,win提权,远程执行(proftpd 1.3.5),各类后门(proftpd,vsftp 2.3.4)
tcp22SSH (安全外壳协议 )可根据已搜集到的信息尝试爆破,v1版本可中间人,ssh隧道及内网代理转发,文件传输等等
tcp23Telnet ( 远程终端协议)爆破,嗅探,一般常用于路由,交换登陆,可尝试弱口令
tcp25SMTP(简单邮件传输协议)邮件伪造,vrfy/expn查询邮件用户信息,可使用smtp-user-enum工具来自动跑
tcp/udp53DNS(域名系统)允许区域传送,dns劫持,缓存投毒,欺骗以及各种基于dns隧道的远控
tcp/udp69TFTP (简单文件传送协议 )尝试下载目标及其的各类重要配置文件
tcp80-89,443,8440-8450,8080-8089各种常用的Web服务端口,可尝试经典的topn,vpn,owa,webmail,目标oa,各类Java控制台,各类服务器Web管理面板,各类Web中间件漏洞利用,各类Web框架漏洞利用等等……
tcp110POP3(邮局协议版本3 )可尝试爆破,嗅探
tcp111,2049NFS(网络文件系统)权限配置不当
tcp137,139,445SMB(NETBIOS协议)可尝试爆破以及smb自身的各种远程执行类漏洞利用,如,ms08-067,ms17-010,嗅探等……
tcp143IMAP(邮件访问协议)可尝试爆破
udp161SNMP(简单网络管理协议)爆破默认团队字符串,搜集目标内网信息
tcp389LDAP( 轻量目录访问协议 )ldap注入,允许匿名访问,弱口令
tcp512,513,514Linux rexec (远程登录)可爆破,rlogin登陆
tcp873Rsync (数据镜像备份工具)匿名访问,文件上传
tcp1194OpenVPN(虚拟专用通道)想办法钓VPN账号,进内网
tcp1352Lotus(Lotus软件)弱口令,信息泄漏,爆破
tcp1433SQL Server(数据库管理系统)注入,提权,sa弱口令,爆破
tcp1521Oracle(甲骨文数据库)tns爆破,注入,弹shell…
tcp1500ISPmanager( 主机控制面板)弱口令
tcp1723PPTP(点对点隧道协议 )爆破,想办法钓VPN账号,进内网
tcp2082,2083cPanel (虚拟机控制系统 )弱口令
tcp2181ZooKeeper(分布式系统的可靠协调系统 )未授权访问
tcp2601,2604Zebra (zebra路由)默认密码zerbra
tcp3128Squid (代理缓存服务器)弱口令
tcp3312,3311kangle(web服务器)弱口令
tcp3306MySQL(数据库)注入,提权,爆破
tcp3389Windows rdp(桌面协议)shift后门[需要03以下的系统],爆破,ms12-020
tcp3690SVN(开放源代码的版本控制系统)svn泄露,未授权访问
tcp4848GlassFish(应用服务器)弱口令
tcp5000Sybase/DB2(数据库)爆破,注入
tcp5432PostgreSQL(数据库)爆破,注入,弱口令
tcp5900,5901,5902VNC(虚拟网络控制台,远控)弱口令爆破
tcp5984CouchDB(数据库)未授权导致的任意指令执行
tcp6379Redis(数据库)可尝试未授权访问,弱口令爆破
tcp7001,7002WebLogic(WEB应用系统)Java反序列化,弱口令
tcp7778Kloxo(虚拟主机管理系统)主机面板登录
tcp8000Ajenti(Linux服务器管理面板)弱口令
tcp8443Plesk(虚拟主机管理面板)弱口令
tcp8069Zabbix (系统网络监视)远程执行,SQL注入
tcp8080-8089Jenkins,JBoss (应用服务器)反序列化,控制台弱口令
tcp9080-9081,9090WebSphere(应用服务器)Java反序列化/弱口令
tcp9200,9300ElasticSearch (Lucene的搜索服务器)远程执行
tcp11211Memcached(缓存系统)未授权访问
tcp27017,27018MongoDB(数据库)爆破,未授权访问
tcp50070,50030Hadoop(分布式文件系统)默认端口未授权访问
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/80/index.html b/en/page/80/index.html new file mode 100644 index 0000000000..fb62189024 --- /dev/null +++ b/en/page/80/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

就在前天,公司里一位新入职的前端小伙伴找到我说遇到了一个问题,自己写的vue代码本地运行是好用的,但是打包后提给后台就访问不了接口了,刚开始我以为是代理配置的有问题,但是经过检查也没什么问题,因为是个get方法,所以我就直接在浏览器地址里敲了api地址,也能正常获取到数据,那么这么看就不是代理或者接口的问题,后来我又看了下调试工具,network中根本就没发出请求,我怀疑是直接前台就拦截了,最后我发现他这个api参数超级长,长的相当吓人,可以看一下下图。
lROUMt.png
我记得url是有长度限制的,于是我就把url变短再次请求,虽然因为参数不全,接口报了错,但是发出去了,由此我定位应该是url超长,因为vue中配置了什么导致的访问被拦截了。于是我就开始了搜索引擎查找解决方案之旅,接下来就把我了解到的信息一一整理。

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/81/index.html b/en/page/81/index.html new file mode 100644 index 0000000000..53afca2a5e --- /dev/null +++ b/en/page/81/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

前言

面试中时常会出现需要手动实现某个功能的要求,所以我准备整理一个自己动手实现系列文章,第一个呢就是自己动手实现 jsonp

+

实现 jsonp 思路

    +
  1. 将传入的data数据转化为url字符串形式
  2. +
  3. 处理url中的回调函数
  4. +
  5. 创建一个script标签并插入到页面中
  6. +
  7. 挂载回调函数
  8. +
+

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// js代码

(function (window,document) {
"use strict";
var jsonp = function (url,data,callback) {
// 1.将传入的data数据转化为url字符串形式
// {id:1,name:'jack'} => id=1&name=jack
var dataString = url.indexof('?') == -1? '?': '&';
for(var key in data){
dataString += key + '=' + data[key] + '&';
};

// 2 处理url中的回调函数
// cbFuncName回调函数的名字 :my_json_cb_名字的前缀 + 随机数(把小数点去掉)
var cbFuncName = 'my_json_cb_' + Math.random().toString().replace('.','');
dataString += 'callback=' + cbFuncName;

// 3.创建一个script标签并插入到页面中
var scriptEle = document.createElement('script');
scriptEle.src = url + dataString;

// 4.挂载回调函数
window[cbFuncName] = function (data) {
callback(data); // 处理完回调函数的数据之后,删除jsonp的script标签
document.body.removeChild(scriptEle);
}
document.body.appendChild(scriptEle);
}
window.$jsonp = jsonp;
})(window,document)
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/82/index.html b/en/page/82/index.html new file mode 100644 index 0000000000..d254c66b09 --- /dev/null +++ b/en/page/82/index.html @@ -0,0 +1,617 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

网络相关的知识虽然平时在编码阶段很少遇到,但是并不是说它不重要,尤其是前端优化层面,能从网络传输过程优化的点有很多,所以,我们需要好好掌握网络相关的知识,接下来,我将会将我了解到的网络相关的知识点进行一一整理

+

栈 Heap

+

栈是一个线性结构,在计算机中是一个相当常见的数据结构。
栈的特点是只能在某一端添加或删除数据,遵循先进后出(FILO)的原则

+
+

实现

每种数据结构都可以用很多种方式来实现,其实可以把栈看成是数组的一个子集,所以这里使用数组来实现

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// js代码
class Stack {
constructor() {
this.stack = []
}
push(item) {
this.stack.push(item)
}
pop() {
this.stack.pop()
}
peek() { // 取最后一项
return this.stack[this.getCount() - 1]
}
getCount() {
return this.stack.length
}
isEmpty() {
return this.getCount() === 0
}
}
+

应用

选取了 LeetCode 上序号为 20 的题目

+

题意是匹配括号,可以通过栈的特性来完成这道题目

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var isValid = function(str) {
let map = {
'(': -1,
')': 1,
'[': -2,
']': 2,
'{': -3,
'}': 3
}
let stack = [] // 空数组
for (let i = 0; i < str.length; i++) { // 遍历字符串每个字符
if (map[str[i]] < 0) { // 如果是左边括号,入栈
stack.push(str[i])
} else { // 否则出栈,判断左右括号加到一起是不是0
let last = stack.pop()
if (map[last] + map[str[i]] != 0) return false
}
}
if (stack.length > 0) return false // 循环完成后,判断数组中时候还有剩下的,有剩下的说明括号没闭合
return true // 否则没剩下的,都闭合了
}
+ +

队列

+

队列一个线性结构,特点是在某一端添加数据,在另一端删除数据,遵循先进先出(FIFO)的原则。

+
+

实现

这里会讲解两种实现队列的方式,分别是单链队列循环队列

+
    +
  • 链队列即队列的链式存储结构,结构上就是一个单链表,但数据只能是头进尾出。链式结构更加的灵活,特别是在存储空间上,基本不会出现溢出的情况,所以不用像循环队列一样判断队列是否已满,且空间的利用率相对较高。
      +
    • 链队列front指向头结点,头结点不存储数据,rear指向队尾结点。
    • +
    +
  • +
+
    +
  • 循环队列即为头尾相接的队列,它的最大存储空间和顺序队列一样由数组界定,但队列的长度并不一定等同于数组的长度;循环队列的队首和队尾分别由两个指针front、rear标识,于是这样就能做到首尾相接。

    +
  • +
  • 链队列:为操作方便,给链队列添加一个头结点

    +
  • +
  • 循环队列:附设两个指针front和rear分别指示队列头元素及尾元素的位置,每当插入新的队尾元素是,尾指针加1;每当删除队列头元素是,头指针加1

    +
      +
    • 如果用循环队列,则必须设定一个最大队列长度;若无法确定最大长度,则宜采用链队列。
    • +
    +
  • +
+

单链队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// js代码

class Queue {
constructor() {
this.queue = []
}
enQueue(item) {
this.queue.push(item)
}
deQueue() {
return this.queue.shift()
}
getHeader() {
return this.queue[0]
}
getLength() {
return this.queue.length
}
isEmpty() {
return this.getLength() === 0
}
}
+

因为单链队列在出队操作的时候需要 O(n) 的时间复杂度,所以引入了循环队列。
循环队列的出队操作平均是 O(1) 的时间复杂度。

+

循环队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// js代码

class SqQueue {
constructor(length) {
this.queue = new Array(length + 1)
// 队头
this.first = 0
// 队尾
this.last = 0
// 当前队列大小
this.size = 0
}
enQueue(item) {
// 判断队尾 + 1 是否为队头
// 如果是就代表需要扩容数组
// % this.queue.length 是为了防止数组越界
if (this.first === (this.last + 1) % this.queue.length) {
this.resize(this.getLength() * 2 + 1)
}
this.queue[this.last] = item
this.size++
this.last = (this.last + 1) % this.queue.length
}
deQueue() {
if (this.isEmpty()) {
throw Error('Queue is empty')
}
let r = this.queue[this.first]
this.queue[this.first] = null
this.first = (this.first + 1) % this.queue.length
this.size--
// 判断当前队列大小是否过小
// 为了保证不浪费空间,在队列空间等于总长度四分之一时
// 且不为 2 时缩小总长度为当前的一半
if (this.size === this.getLength() / 4 && this.getLength() / 2 !== 0) {
this.resize(this.getLength() / 2)
}
return r
}
getHeader() {
if (this.isEmpty()) {
throw Error('Queue is empty')
}
return this.queue[this.first]
}
getLength() {
return this.queue.length - 1
}
isEmpty() {
return this.first === this.last
}
resize(length) {
let q = new Array(length)
for (let i = 0; i < length; i++) {
q[i] = this.queue[(i + this.first) % this.queue.length]
}
this.queue = q
this.first = 0
this.last = this.size
}
}
+ +

链表

+

链表是一个线性结构,同时也是一个天然的递归结构。链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。

+
+

实现

单向链表

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// js代码

class Node {
constructor(v, next) {
this.value = v
this.next = next
}
}
class LinkList {
constructor() {
// 链表长度
this.size = 0
// 虚拟头部
this.dummyNode = new Node(null, null)
}
find(header, index, currentIndex) {
if (index === currentIndex) return header
return this.find(header.next, index, currentIndex + 1)
}
addNode(v, index) {
this.checkIndex(index)
// 当往链表末尾插入时,prev.next 为空
// 其他情况时,因为要插入节点,所以插入的节点
// 的 next 应该是 prev.next
// 然后设置 prev.next 为插入的节点
let prev = this.find(this.dummyNode, index, 0)
prev.next = new Node(v, prev.next)
this.size++
return prev.next
}
insertNode(v, index) {
return this.addNode(v, index)
}
addToFirst(v) {
return this.addNode(v, 0)
}
addToLast(v) {
return this.addNode(v, this.size)
}
removeNode(index, isLast) {
this.checkIndex(index)
index = isLast ? index - 1 : index
let prev = this.find(this.dummyNode, index, 0)
let node = prev.next
prev.next = node.next
node.next = null
this.size--
return node
}
removeFirstNode() {
return this.removeNode(0)
}
removeLastNode() {
return this.removeNode(this.size, true)
}
checkIndex(index) {
if (index < 0 || index > this.size) throw Error('Index error')
}
getNode(index) {
this.checkIndex(index)
if (this.isEmpty()) return
return this.find(this.dummyNode, index, 0).next
}
isEmpty() {
return this.size === 0
}
getSize() {
return this.size
}
}
+ +

二叉树

树拥有很多种结构,二叉树是树中最常用的结构,同时也是一个天然的递归结构。

+

二叉树拥有一个根节点,每个节点至多拥有两个子节点,分别为:左节点和右节点。树的最底部节点称之为叶节点,当一颗树的叶数量数量为满时,该树可以称之为满二叉树。

+

二分搜索树

二分搜索树也是二叉树,拥有二叉树的特性。但是区别在于二分搜索树每个节点的值都比他的左子树的值大,比右子树的值小。

+

这种存储方式很适合于数据搜索。如下图所示,当需要查找 6 的时候,因为需要查找的值比根节点的值大,所以只需要在根节点的右子树上寻找,大大提高了搜索效率。

+

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// js代码

class Node {
constructor(value) {
this.value = value
this.left = null
this.right = null
}
}
class BST {
constructor() {
this.root = null
this.size = 0
}
getSize() {
return this.size
}
isEmpty() {
return this.size === 0
}
addNode(v) {
this.root = this._addChild(this.root, v)
}
// 添加节点时,需要比较添加的节点值和当前
// 节点值的大小
_addChild(node, v) {
if (!node) {
this.size++
return new Node(v)
}
if (node.value > v) {
node.left = this._addChild(node.left, v)
} else if (node.value < v) {
node.right = this._addChild(node.right, v)
}
return node
}
}
+ +

以上是最基本的二分搜索树实现,接下来实现树的遍历。

+

对于树的遍历来说,有三种遍历方法,分别是先序遍历中序遍历后序遍历

+

三种遍历的区别在于何时访问节点。在遍历树的过程中,每个节点都会遍历三次,分别是遍历到自己,遍历左子树和遍历右子树。如果需要实现先序遍历,那么只需要第一次遍历到节点时进行操作即可。

+

以下都是递归实现.

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// js代码

// 先序遍历可用于打印树的结构
// 先序遍历先访问根节点,然后访问左节点,最后访问右节点。
preTraversal() {
this._pre(this.root)
}
_pre(node) {
if (node) {
console.log(node.value)
this._pre(node.left)
this._pre(node.right)
}
}
// 中序遍历可用于排序
// 对于 BST 来说,中序遍历可以实现一次遍历就
// 得到有序的值
// 中序遍历表示先访问左节点,然后访问根节点,最后访问右节点。
midTraversal() {
this._mid(this.root)
}
_mid(node) {
if (node) {
this._mid(node.left)
console.log(node.value)
this._mid(node.right)
}
}
// 后序遍历可用于先操作子节点
// 再操作父节点的场景
// 后序遍历表示先访问左节点,然后访问右节点,最后访问根节点。
backTraversal() {
this._back(this.root)
}
_back(node) {
if (node) {
this._back(node.left)
this._back(node.right)
console.log(node.value)
}
}
+ +

以上的这几种遍历都可以称之为深度遍历,对应的还有种遍历叫做广度遍历,也就是一层层地遍历树。对于广度遍历来说,我们需要利用之前讲过的队列结构来完成。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

breadthTraversal() {
if (!this.root) return null
let q = new Queue()
// 将根节点入队
q.enQueue(this.root)
// 循环判断队列是否为空,为空
// 代表树遍历完毕
while (!q.isEmpty()) {
// 将队首出队,判断是否有左右子树
// 有的话,就先左后右入队
let n = q.deQueue()
console.log(n.value)
if (n.left) q.enQueue(n.left)
if (n.right) q.enQueue(n.right)
}
}
+

接下来先介绍如何在树中寻找最小值或最大数。因为二分搜索树的特性,所以最小值一定在根节点的最左边,最大值相反

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码

getMin() {
return this._getMin(this.root).value
}
_getMin(node) {
if (!node.left) return node
return this._getMin(node.left)
}
getMax() {
return this._getMax(this.root).value
}
_getMax(node) {
if (!node.right) return node
return this._getMin(node.right)
}
+ +

向上取整和向下取整,这两个操作是相反的,所以代码也是类似的,这里只介绍如何向下取整。既然是向下取整,那么根据二分搜索树的特性,值一定在根节点的左侧。只需要一直遍历左子树直到当前节点的值不再大于等于需要的值,然后判断节点是否还拥有右子树。如果有的话,继续上面的递归判断。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

floor(v) {
let node = this._floor(this.root, v)
return node ? node.value : null
}
_floor(node, v) {
if (!node) return null
if (node.value === v) return v
// 如果当前节点值还比需要的值大,就继续递归
if (node.value > v) {
return this._floor(node.left, v)
}
// 判断当前节点是否拥有右子树
let right = this._floor(node.right, v)
if (right) return right
return node
}
+

排名,这是用于获取给定值的排名或者排名第几的节点的值,这两个操作也是相反的,所以这个只介绍如何获取排名第几的节点的值。对于这个操作而言,我们需要略微的改造点代码,让每个节点拥有一个 size 属性。该属性表示该节点下有多少子节点(包含自身)。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// js代码

class Node {
constructor(value) {
this.value = value
this.left = null
this.right = null
// 修改代码
this.size = 1
}
}
// 新增代码
_getSize(node) {
return node ? node.size : 0
}
_addChild(node, v) {
if (!node) {
return new Node(v)
}
if (node.value > v) {
// 修改代码
node.size++
node.left = this._addChild(node.left, v)
} else if (node.value < v) {
// 修改代码
node.size++
node.right = this._addChild(node.right, v)
}
return node
}
select(k) {
let node = this._select(this.root, k)
return node ? node.value : null
}
_select(node, k) {
if (!node) return null
// 先获取左子树下有几个节点
let size = node.left ? node.left.size : 0
// 判断 size 是否大于 k
// 如果大于 k,代表所需要的节点在左节点
if (size > k) return this._select(node.left, k)
// 如果小于 k,代表所需要的节点在右节点
// 注意这里需要重新计算 k,减去根节点除了右子树的节点数量
if (size < k) return this._select(node.right, k - size - 1)
return node
}
+ +

接下来讲解的是二分搜索树中最难实现的部分:删除节点。因为对于删除节点来说,会存在以下几种情况

+
    +
  • 需要删除的节点没有子树
  • +
  • 需要删除的节点只有一条子树
  • +
  • 需要删除的节点有左右两条树
  • +
+

对于前两种情况很好解决,但是第三种情况就有难度了,所以先来实现相对简单的操作:删除最小节点,对于删除最小节点来说,是不存在第三种情况的,删除最大节点操作是和删除最小节点相反的,所以这里也就不再赘述。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码

delectMin() {
this.root = this._delectMin(this.root)
console.log(this.root)
}
_delectMin(node) {
// 一直递归左子树
// 如果左子树为空,就判断节点是否拥有右子树
// 有右子树的话就把需要删除的节点替换为右子树
if ((node != null) & !node.left) return node.right
node.left = this._delectMin(node.left)
// 最后需要重新维护下节点的 `size`
node.size = this._getSize(node.left) + this._getSize(node.right) + 1
return node
}
+ +

最后讲解的就是如何删除任意节点了。对于这个操作,T.Hibbard 在 1962 年提出了解决这个难题的办法,也就是如何解决第三种情况。

+

当遇到这种情况时,需要取出当前节点的后继节点(也就是当前节点右子树的最小节点)来替换需要删除的节点。然后将需要删除节点的左子树赋值给后继结点,右子树删除后继结点后赋值给他。

+

你如果对于这个解决办法有疑问的话,可以这样考虑。因为二分搜索树的特性,父节点一定比所有左子节点大,比所有右子节点小。那么当需要删除父节点时,势必需要拿出一个比父节点大的节点来替换父节点。这个节点肯定不存在于左子树,必然存在于右子树。然后又需要保持父节点都是比右子节点小的,那么就可以取出右子树中最小的那个节点来替换父节点。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// js代码

delect(v) {
this.root = this._delect(this.root, v)
}
_delect(node, v) {
if (!node) return null
// 寻找的节点比当前节点小,去左子树找
if (node.value < v) {
node.right = this._delect(node.right, v)
} else if (node.value > v) {
// 寻找的节点比当前节点大,去右子树找
node.left = this._delect(node.left, v)
} else {
// 进入这个条件说明已经找到节点
// 先判断节点是否拥有拥有左右子树中的一个
// 是的话,将子树返回出去,这里和 `_delectMin` 的操作一样
if (!node.left) return node.right
if (!node.right) return node.left
// 进入这里,代表节点拥有左右子树
// 先取出当前节点的后继结点,也就是取当前节点右子树的最小值
let min = this._getMin(node.right)
// 取出最小值后,删除最小值
// 然后把删除节点后的子树赋值给最小值节点
min.right = this._delectMin(node.right)
// 左子树不动
min.left = node.left
node = min
}
// 维护 size
node.size = this._getSize(node.left) + this._getSize(node.right) + 1
return node
}
+ +

AVL 树

+

二分搜索树实际在业务中是受到限制的,因为并不是严格的 O(logN),在极端情况下会退化成链表,比如加入一组升序的数字就会造成这种情况。

+
+
+

AVL 树改进了二分搜索树,在 AVL 树中任意节点的左右子树的高度差都不大于 1,这样保证了时间复杂度是严格的 O(logN)。基于此,对 AVL 树增加或删除节点时可能需要旋转树来达到高度的平衡。

+
+

实现

因为 AVL 树是改进了二分搜索树,所以部分代码是于二分搜索树重复的,对于重复内容不作再次解析。

+

对于 AVL 树来说,添加节点会有四种情况
lWB0nf.png

+

对于左左情况来说,新增加的节点位于节点 2 的左侧,这时树已经不平衡,需要旋转。因为搜索树的特性,节点比左节点大,比右节点小,所以旋转以后也要实现这个特性。

+

旋转之前:new < 2 < C < 3 < B < 5 < A,右旋之后节点 3 为根节点,这时候需要将节点 3 的右节点加到节点 5 的左边,最后还需要更新节点的高度。

+

对于右右情况来说,相反于左左情况,所以不再赘述。

+

对于左右情况来说,新增加的节点位于节点 4 的右侧。对于这种情况,需要通过两次旋转来达到目的。

+

首先对节点的左节点左旋,这时树满足左左的情况,再对节点进行一次右旋就可以达到目的。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// js代码

class Node {
constructor(value) {
this.value = value
this.left = null
this.right = null
this.height = 1
}
}

class AVL {
constructor() {
this.root = null
}
addNode(v) {
this.root = this._addChild(this.root, v)
}
_addChild(node, v) {
if (!node) {
return new Node(v)
}
if (node.value > v) {
node.left = this._addChild(node.left, v)
} else if (node.value < v) {
node.right = this._addChild(node.right, v)
} else {
node.value = v
}
node.height =
1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
let factor = this._getBalanceFactor(node)
// 当需要右旋时,根节点的左树一定比右树高度高
if (factor > 1 && this._getBalanceFactor(node.left) >= 0) {
return this._rightRotate(node)
}
// 当需要左旋时,根节点的左树一定比右树高度矮
if (factor < -1 && this._getBalanceFactor(node.right) <= 0) {
return this._leftRotate(node)
}
// 左右情况
// 节点的左树比右树高,且节点的左树的右树比节点的左树的左树高
if (factor > 1 && this._getBalanceFactor(node.left) < 0) {
node.left = this._leftRotate(node.left)
return this._rightRotate(node)
}
// 右左情况
// 节点的左树比右树矮,且节点的右树的右树比节点的右树的左树矮
if (factor < -1 && this._getBalanceFactor(node.right) > 0) {
node.right = this._rightRotate(node.right)
return this._leftRotate(node)
}

return node
}
_getHeight(node) {
if (!node) return 0
return node.height
}
_getBalanceFactor(node) {
return this._getHeight(node.left) - this._getHeight(node.right)
}
// 节点右旋
// 5 2
// / \ / \
// 2 6 ==> 1 5
// / \ / / \
// 1 3 new 3 6
// /
// new
_rightRotate(node) {
// 旋转后新根节点
let newRoot = node.left
// 需要移动的节点
let moveNode = newRoot.right
// 节点 2 的右节点改为节点 5
newRoot.right = node
// 节点 5 左节点改为节点 3
node.left = moveNode
// 更新树的高度
node.height =
1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
newRoot.height =
1 +
Math.max(this._getHeight(newRoot.left), this._getHeight(newRoot.right))

return newRoot
}
// 节点左旋
// 4 6
// / \ / \
// 2 6 ==> 4 7
// / \ / \ \
// 5 7 2 5 new
// \
// new
_leftRotate(node) {
// 旋转后新根节点
let newRoot = node.right
// 需要移动的节点
let moveNode = newRoot.left
// 节点 6 的左节点改为节点 4
newRoot.left = node
// 节点 4 右节点改为节点 5
node.right = moveNode
// 更新树的高度
node.height =
1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
newRoot.height =
1 +
Math.max(this._getHeight(newRoot.left), this._getHeight(newRoot.right))

return newRoot
}
}
+ +

Trie

+

在计算机科学,trie,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。
简单点来说,这个结构的作用大多是为了方便搜索字符串,该树有以下几个特点

+
+
    +
  • 根节点代表空字符串,每个节点都有 N(假如搜索英文字符,就有 26 条) 条链接,每条链接代表一个字符
  • +
  • 节点不存储字符,只有路径才存储,这点和其他的树结构不同
  • +
  • 从根节点开始到任意一个节点,将沿途经过的字符连接起来就是该节点对应的字符串
  • +
+

实现

总得来说 Trie 的实现相比别的树结构来说简单的很多,实现就以搜索英文字符为例。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// js代码

class TrieNode {
constructor() {
// 代表每个字符经过节点的次数
this.path = 0
// 代表到该节点的字符串有几个
this.end = 0
// 链接
this.next = new Array(26).fill(null)
}
}
class Trie {
constructor() {
// 根节点,代表空字符
this.root = new TrieNode()
}
// 插入字符串
insert(str) {
if (!str) return
let node = this.root
for (let i = 0; i < str.length; i++) {
// 获得字符先对应的索引
let index = str[i].charCodeAt() - 'a'.charCodeAt()
// 如果索引对应没有值,就创建
if (!node.next[index]) {
node.next[index] = new TrieNode()
}
node.path += 1
node = node.next[index]
}
node.end += 1
}
// 搜索字符串出现的次数
search(str) {
if (!str) return
let node = this.root
for (let i = 0; i < str.length; i++) {
let index = str[i].charCodeAt() - 'a'.charCodeAt()
// 如果索引对应没有值,代表没有需要搜素的字符串
if (!node.next[index]) {
return 0
}
node = node.next[index]
}
return node.end
}
// 删除字符串
delete(str) {
if (!this.search(str)) return
let node = this.root
for (let i = 0; i < str.length; i++) {
let index = str[i].charCodeAt() - 'a'.charCodeAt()
// 如果索引对应的节点的 Path 为 0,代表经过该节点的字符串
// 已经一个,直接删除即可
if (--node.next[index].path == 0) {
node.next[index] = null
return
}
node = node.next[index]
}
node.end -= 1
}
}
+ +

并查集

+

并查集是一种特殊的树结构,用于处理一些不交集的合并及查询问题。该结构中每个节点都有一个父节点,如果只有当前一个节点,那么该节点的父节点指向自己。
这个结构中有两个重要的操作,分别是:

+
+
    +
  • Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
  • +
  • Union:将两个子集合并成同一个集合。
  • +
+

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// js代码

class DisjointSet {
// 初始化样本
constructor(count) {
// 初始化时,每个节点的父节点都是自己
this.parent = new Array(count)
// 用于记录树的深度,优化搜索复杂度
this.rank = new Array(count)
for (let i = 0; i < count; i++) {
this.parent[i] = i
this.rank[i] = 1
}
}
find(p) {
// 寻找当前节点的父节点是否为自己,不是的话表示还没找到
// 开始进行路径压缩优化
// 假设当前节点父节点为 A
// 将当前节点挂载到 A 节点的父节点上,达到压缩深度的目的
while (p != this.parent[p]) {
this.parent[p] = this.parent[this.parent[p]]
p = this.parent[p]
}
return p
}
isConnected(p, q) {
return this.find(p) === this.find(q)
}
// 合并
union(p, q) {
// 找到两个数字的父节点
let i = this.find(p)
let j = this.find(q)
if (i === j) return
// 判断两棵树的深度,深度小的加到深度大的树下面
// 如果两棵树深度相等,那就无所谓怎么加
if (this.rank[i] < this.rank[j]) {
this.parent[i] = j
} else if (this.rank[i] > this.rank[j]) {
this.parent[j] = i
} else {
this.parent[i] = j
this.rank[j] += 1
}
}
}
+ +

堆通常是一个可以被看做一棵树的数组对象。
堆的实现通过构造二叉堆,实为二叉树的一种。这种数据结构具有以下性质。

+
    +
  • 任意节点小于(或大于)它的所有子节点
  • +
  • 堆总是一棵完全树。即除了最底层,其他层的节点都被元素填满,且最底层从左到右填入。
    将根节点最大的堆叫做最大堆大根堆,根节点最小的堆叫做最小堆小根堆
    优先队列也完全可以用堆来实现,操作是一模一样的。
  • +
+

实现大根堆

堆的每个节点的左边子节点索引是 i * 2 + 1,右边是 i * 2 + 2,父节点是 (i - 1) /2。
堆有两个核心的操作,分别是 shiftUp 和 shiftDown 。前者用于添加元素,后者用于删除根节点。
shiftUp 的核心思路是一路将节点与父节点对比大小,如果比父节点大,就和父节点交换位置。
shiftDown 的核心思路是先将根节点和末尾交换位置,然后移除末尾元素。接下来循环判断父节点和两个子节点的大小,如果子节点大,就把最大的子节点和父节点交换。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// js代码

class MaxHeap {
constructor() {
this.heap = []
}
size() {
return this.heap.length
}
empty() {
return this.size() == 0
}
add(item) {
this.heap.push(item)
this._shiftUp(this.size() - 1)
}
removeMax() {
this._shiftDown(0)
}
getParentIndex(k) {
return parseInt((k - 1) / 2)
}
getLeftIndex(k) {
return k * 2 + 1
}
_shiftUp(k) {
// 如果当前节点比父节点大,就交换
while (this.heap[k] > this.heap[this.getParentIndex(k)]) {
this._swap(k, this.getParentIndex(k))
// 将索引变成父节点
k = this.getParentIndex(k)
}
}
_shiftDown(k) {
// 交换首位并删除末尾
this._swap(k, this.size() - 1)
this.heap.splice(this.size() - 1, 1)
// 判断节点是否有左孩子,因为二叉堆的特性,有右必有左
while (this.getLeftIndex(k) < this.size()) {
let j = this.getLeftIndex(k)
// 判断是否有右孩子,并且右孩子是否大于左孩子
if (j + 1 < this.size() && this.heap[j + 1] > this.heap[j]) j++
// 判断父节点是否已经比子节点都大
if (this.heap[k] >= this.heap[j]) break
this._swap(k, j)
k = j
}
}
_swap(left, right) {
let rightValue = this.heap[right]
this.heap[right] = this.heap[left]
this.heap[left] = rightValue
}
}
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/83/index.html b/en/page/83/index.html new file mode 100644 index 0000000000..02ce8f1e1a --- /dev/null +++ b/en/page/83/index.html @@ -0,0 +1,880 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

网络相关的知识虽然平时在编码阶段很少遇到,但是并不是说它不重要,尤其是前端优化层面,能从网络传输过程优化的点有很多,所以,我们需要好好掌握网络相关的知识,接下来,我将会将我了解到的网络相关的知识点进行一一整理

+

UDP - User Datagram Protocol - 用户数据报协议

面向报文

UDP 是一个面向报文(报文可以理解为一段段的数据)的协议。意思就是 UDP 只是报文的搬运工,不会对报文进行任何拆分和拼接操作。

+

具体来说

+
    +
  • 发送端,应用层将数据传递给传输层的 UDP 协议, UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了
  • +
  • 接收端,网络层将数据传递给传输层, UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作
  • +
+

不可靠性

    +
  1. UDP 是无连接的,也就是说通信不需要建立和断开连接。
  2. +
  3. UDP 也是不可靠的。协议收到什么数据就传递什么数据,并且也不会备份数据,对方能不能收到是不关心的
  4. +
  5. UDP 没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP
  6. +
+

高效

因为 UDP 没有 TCP 那么复杂,需要保证数据不丢失且有序到达。所以 UDP 的头部开销小,只有八字节,相比 TCP 的至少二十字节要少得多,在传输数据报文时是很高效的。

+

头部包含了以下几个数据

+
    +
  • 两个十六位的端口号,分别为源端口(可选字段)和目标端口
  • +
  • 整个数据报文的长度
  • +
  • 整个数据报文的检验和( IPv4 可选 字段),该字段用于发现头部信息和数据中的错误
  • +
+

传输方式

UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。

+

TCP

头部

TCP 头部比 UDP 头部复杂的多

+

对于 TCP 头部来说,以下几个字段是很重要的

+
    +
  • Sequence number(序号),它保证了 TCP 传输的报文都是有序的,对端可以通过序号顺序的拼接报文
  • +
  • Acknowledgement Number(确认编号),它表示数据接收端期望接收的下一个字节的编号是多少,同时也表示上一个序号的数据已经收到
  • +
  • Window Size(窗口大小),它表示还能接收多少字节的数据,用于流量控制
  • +
  • 标识符
      +
    • URG=1:该字段为一表示本数据报的数据部分包含紧急信息,是一个高优先级数据报文,此时紧急指针有效。紧急数据一定位于当前数据包数据部分的最前面,紧急指针标明了紧急数据的尾部。
    • +
    • ACK=1:该字段为一表示确认号字段有效。此外,TCP 还规定在连接建立后传送的所有报文段都必须把 ACK 置为一。
    • +
    • PSH=1:该字段为一表示接收端应该立即将数据 push 给应用层,而不是等到缓冲区满后再提交。
    • +
    • RST=1:该字段为一表示当前 TCP 连接出现严重问题,可能需要重新建立 TCP 连接,也可以用于拒绝非法的报文段和拒绝连接请求。
    • +
    • SYN=1:当SYN=1,ACK=0时,表示当前报文段是一个连接请求报文。当SYN=1,ACK=1时,表示当前报文段是一个同意建立连接的应答报文。
    • +
    • FIN=1:该字段为一表示此报文段是一个释放连接的请求报文。
    • +
    +
  • +
+

状态机

HTTP 是无连接的,所以作为下层的 TCP 协议也是无连接的,虽然看似 TCP 将两端连接了起来,但是其实只是两端共同维护了一个状态

+

TCP 的状态机是很复杂的,并且与建立断开连接时的握手息息相关,接下来就来详细描述下两种握手。
在这之前需要了解一个重要的性能指标 RTT 。该指标表示发送端发送数据到接收到对端数据所需的往返时间。

+

建立连接三次握手

在 TCP 协议中,主动发起请求的一端为客户端,被动连接的一端称为服务端。不管是客户端还是服务端,TCP 连接建立完后都能发送和接收数据,所以 TCP 也是一个全双工的协议。

+

起初,两端都为 CLOSED 状态。在通信开始前,双方都会创建 TCB。 服务器创建完 TCB 后遍进入 LISTEN 状态,此时开始等待客户端发送数据。

+

第一次握手

客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态,x 表示客户端的数据通信初始序号。

+

第二次握手

服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。

+

第三次握手

当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。

+

PS:第三次握手可以包含数据,通过 TCP 快速打开(TFO)技术。其实只要涉及到握手的协议,都可以使用类似 TFO 的方式,客户端和服务端存储相同 cookie,下次握手时发出 cookie 达到减少 RTT 的目的。

+

你是否有疑惑明明两次握手就可以建立起连接,为什么还需要第三次应答?

+

因为这是为了防止失效的连接请求报文段被服务端接收,从而产生错误。

+

可以想象如下场景。客户端发送了一个连接请求 A,但是因为网络原因造成了超时,这时 TCP 会启动超时重传的机制再次发送一个连接请求 B。此时请求顺利到达服务端,服务端应答完就建立了请求。如果连接请求 A 在两端关闭后终于抵达了服务端,那么这时服务端会认为客户端又需要建立 TCP 连接,从而应答了该请求并进入 ESTABLISHED 状态。此时客户端其实是 CLOSED 状态,那么就会导致服务端一直等待,造成资源的浪费。

+

PS:在建立连接中,任意一端掉线,TCP 都会重发 SYN 包,一般会重试五次,在建立连接中可能会遇到 SYN FLOOD 攻击。遇到这种情况你可以选择调低重试次数或者干脆在不能处理的情况下拒绝请求。

+

断开链接四次握手

TCP 是全双工的,在断开连接时两端都需要发送 FIN 和 ACK。

+

第一次握手

若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。

+

第二次握手

B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,表示 A 到 B 的连接已经释放,不接收 A 发的数据了。但是因为 TCP 连接时双向的,所以 B 仍旧可以发送数据给 A。

+

第三次握手

B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。

+

PS:通过延迟确认的技术(通常有时间限制,否则对方会误认为需要重传),可以将第二次和第三次握手合并,延迟 ACK 包的发送。

+

第四次握手

A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。

+

为什么 A 要进入 TIME-WAIT 状态,等待 2MSL 时间后才进入 CLOSED 状态?

+

为了保证 B 能收到 A 的确认应答。若 A 发完确认应答后直接进入 CLOSED 状态,如果确认应答因为网络问题一直没有到达,那么会造成 B 不能正常关闭。

+

ARQ 协议

ARQ 协议也就是超时重传机制。通过确认和超时机制保证了数据的正确送达,ARQ 协议包含停止等待 ARQ 和连续 ARQ

+

停止等待 ARQ

正常传输过程

只要 A 向 B 发送一段报文,都要停止发送并启动一个定时器,等待对端回应,在定时器时间内接收到对端应答就取消定时器并发送下一段报文。

+

报文丢失或出错

在报文传输的过程中可能会出现丢包。这时候超过定时器设定的时间就会再次发送丢包的数据直到对端响应,所以需要每次都备份发送的数据。

+

即使报文正常的传输到对端,也可能出现在传输过程中报文出错的问题。这时候对端会抛弃该报文并等待 A 端重传。

+

PS:一般定时器设定的时间都会大于一个 RTT 的平均时间。

+

ACK 超时或丢失

对端传输的应答也可能出现丢失或超时的情况。那么超过定时器时间 A 端照样会重传报文。这时候 B 端收到相同序号的报文会丢弃该报文并重传应答,直到 A 端发送下一个序号的报文。

+

在超时的情况下也可能出现应答很迟到达,这时 A 端会判断该序号是否已经接收过,如果接收过只需要丢弃应答即可。

+

这个协议的缺点就是传输效率低,在良好的网络环境下每次发送报文都得等待对端的 ACK 。

+

连续 ARQ

在连续 ARQ 中,发送端拥有一个发送窗口,可以在没有收到应答的情况下持续发送窗口内的数据,这样相比停止等待 ARQ 协议来说减少了等待时间,提高了效率。

+

累计确认

连续 ARQ 中,接收端会持续不断收到报文。如果和停止等待 ARQ 中接收一个报文就发送一个应答一样,就太浪费资源了。通过累计确认,可以在收到多个报文以后统一回复一个应答报文。报文中的 ACK 可以用来告诉发送端这个序号之前的数据已经全部接收到了,下次请发送这个序号 + 1的数据。

+

但是累计确认也有一个弊端。在连续接收报文时,可能会遇到接收到序号 5 的报文后,并未接到序号 6 的报文,然而序号 7 以后的报文已经接收。遇到这种情况时,ACK 只能回复 6,这样会造成发送端重复发送数据,这种情况下可以通过 Sack 来解决,这个会在下文说到。

+

滑动窗口

在上面小节中讲到了发送窗口。在 TCP 中,两端都维护着窗口:分别为发送端窗口和接收端窗口。

+

发送端窗口包含已发送但未收到应答的数据和可以发送但是未发送的数据。

+

发送端窗口是由接收窗口剩余大小决定的。接收方会把当前接收窗口的剩余大小写入应答报文,发送端收到应答后根据该值和当前网络拥塞情况设置发送窗口的大小,所以发送窗口的大小是不断变化的。

+

当发送端接收到应答报文后,会随之将窗口进行滑动

+

滑动窗口实现了流量控制。接收方通过报文告知发送方还可以发送多少数据,从而保证接收方能够来得及接收数据。

+

Zero 窗口

在发送报文的过程中,可能会遇到对端出现零窗口的情况。在该情况下,发送端会停止发送数据,并启动 persistent timer 。该定时器会定时发送请求给对端,让对端告知窗口大小。在重试次数超过一定次数后,可能会中断 TCP 链接。

+

拥塞处理

拥塞处理和流量控制不同,后者是作用于接收方,保证接收方来得及接受数据。而前者是作用于网络,防止过多的数据拥塞网络,避免出现网络负载过大的情况。

+

拥塞处理包括了四个算法,分别为:慢开始拥塞避免快速重传快速恢复

+

慢开始算法

慢开始算法,顾名思义,就是在传输开始时将发送窗口慢慢指数级扩大,从而避免一开始就传输大量数据导致网络拥塞。

+

慢开始算法步骤具体如下

+
    +
  1. 连接初始设置拥塞窗口(Congestion Window) 为 1 MSS(一个分段的最大数据量)
  2. +
  3. 每过一个 RTT 就将窗口大小乘二
  4. +
  5. 指数级增长肯定不能没有限制的,所以有一个阈值限制,当窗口大小大于阈值时就会启动拥塞避免算法。
  6. +
+

拥塞避免算法

拥塞避免算法相比简单点,每过一个 RTT 窗口大小只加一,这样能够避免指数级增长导致网络拥塞,慢慢将大小调整到最佳值。

+

在传输过程中可能定时器超时的情况,这时候 TCP 会认为网络拥塞了,会马上进行以下步骤:

+
    +
  • 将阈值设为当前拥塞窗口的一半
  • +
  • 将拥塞窗口设为 1 MSS
  • +
  • 启动拥塞避免算法
  • +
+

快速重传

快速重传一般和快恢复一起出现。一旦接收端收到的报文出现失序的情况,接收端只会回复最后一个顺序正确的报文序号(没有 Sack 的情况下)。如果收到三个重复的 ACK,无需等待定时器超时再重发而是启动快速重传。具体算法分为两种:

+

TCP Taho 实现如下

    +
  • 将阈值设为当前拥塞窗口的一半
  • +
  • 将拥塞窗口设为 1 MSS
  • +
  • 重新开始慢开始算法
  • +
+

TCP Reno 实现如下

    +
  • 拥塞窗口减半
  • +
  • 将阈值设为当前拥塞窗口
  • +
  • 进入快恢复阶段(重发对端需要的包,一旦收到一个新的 ACK 答复就退出该阶段)
  • +
  • 使用拥塞避免算法
  • +
+

TCP New Ren 改进后的快恢复

TCP New Reno 算法改进了之前 TCP Reno 算法的缺陷。在之前,快恢复中只要收到一个新的 ACK 包,就会退出快恢复。

+

在 TCP New Reno 中,TCP 发送方先记下三个重复 ACK 的分段的最大序号。

+

假如我有一个分段数据是 1 ~ 10 这十个序号的报文,其中丢失了序号为 3 和 7 的报文,那么该分段的最大序号就是 10。发送端只会收到 ACK 序号为 3 的应答。这时候重发序号为 3 的报文,接收方顺利接收并会发送 ACK 序号为 7 的应答。这时候 TCP 知道对端是有多个包未收到,会继续发送序号为 7 的报文,接收方顺利接收并会发送 ACK 序号为 11 的应答,这时发送端认为这个分段接收端已经顺利接收,接下来会退出快恢复阶段。

+

HTTP

HTTP 协议是个无状态协议,不会保存状态。

+

PostGet 的区别

先引入副作用幂等的概念。

+
+

副作用指对服务器上的资源做改变,搜索是无副作用的,注册是副作用的。

+
+
+

幂等指发送 M 和 N 次请求(两者不相同且都大于 1),服务器上资源的状态一致,比如注册 10 个和 11 个帐号是不幂等的,对文章进行更改 10 次和 11 次是幂等的。

+
+

在规范的应用场景上说, Get 多用于无副作用,幂等的场景,例如搜索关键字。 Post 多用于副作用,不幂等的场景,例如注册。

+

在技术上说:

+
    +
  • Get 请求能缓存,Post 不能
  • +
  • Post 相对 Get 安全一点点,因为Get 请求都包含在 URL 里,且会被浏览器保存历史纪录,Post 不会,但是在抓包的情况下都是一样的。
  • +
  • Post 可以通过 request body来传输比 Get 更多的数据,Get 没有这个技术
  • +
  • URL有长度限制,会影响 Get 请求,但是这个长度限制是浏览器规定的,不是 RFC 规定的
  • +
  • Post 支持更多的编码类型且不对数据类型限制
  • +
+

常见状态码

2XX 成功

200 OK,表示从客户端发来的请求在服务器端被正确处理
204 No content,表示请求成功,但响应报文不含实体的主体部分
205 Reset Content,表示请求成功,但响应报文不含实体的主体部分,但是与 204 响应不同在于要求请求方重置内容
206 Partial Content,进行范围请求

+

3XX 重定向

301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
302 found,临时性重定向,表示资源临时被分配了新的 URL
303 see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源
304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况
307 temporary redirect,临时重定向,和302含义类似,但是期望客户端保持请求方法不变向新的地址发出请求

+

4XX 客户端错误

400 bad request,请求报文存在语法错误
401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
403 forbidden,表示对请求资源的访问被服务器拒绝
404 not found,表示在服务器上没有找到请求的资源

+

5XX 服务器错误

500 internal sever error,表示服务器端在执行请求时发生了错误
501 Not Implemented,表示服务器不支持当前请求所需要的某个功能
503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求

+

HTTP 首部

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
通用字段作用
Cache-Control控制缓存的行为
Connection浏览器想要优先使用的连接类型,比如 keep-alive
Date创建报文时间
Pragma报文指令
Via代理服务器相关信息
Transfer-Encoding传输编码方式
Upgrade要求客户端升级协议
Warning在内容中可能存在错误
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
请求字段作用
Accept能正确接收的媒体类型
Accept-Charset能正确接收的字符集
Accept-Encoding能正确接收的编码格式列表
Accept-Language能正确接收的语言列表
Expect期待服务端的指定行为
From请求方邮箱地址
Host服务器的域名
If-Match两端资源标记比较
If-Modified-Since本地资源未修改返回 304(比较时间)
If-None-Match本地资源未修改返回 304(比较标记)
User-Agent客户端信息
Max-Forwards限制可被代理及网关转发的次数
Proxy-Authorization向代理服务器发送验证信息
Range请求某个内容的一部分
Referer表示浏览器所访问的前一个页面
TE传输编码方式
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
响应字段作用
Accept-Ranges是否支持某些种类的范围
Age资源在代理缓存中存在的时间
ETag资源标识
Location客户端重定向到某个 URL
Proxy-Authenticate向代理服务器发送验证信息
Server服务器名字
WWW-Authenticate获取资源需要的验证信息
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
实体字段作用
Allow资源的正确请求方式
Content-Encoding内容的编码格式
Content-Language内容使用的语言
Content-Lengthrequest body 长度
Content-Location返回数据的备用地址
Content-MD5Base64加密格式的内容 MD5检验值
Content-Range内容的位置范围
Content-Type内容的媒体类型
Expires内容的过期时间
Last_modified内容的最后修改时间
+

HTTPS

HTTPS 还是通过了 HTTP 来传输信息,但是信息通过 TLS 协议进行了加密。

+

TLS

TLS 协议位于传输层之上,应用层之下。首次进行 TLS 协议传输需要两个 RTT ,接下来可以通过 Session Resumption 减少到一个 RTT

+

TLS 中使用了两种加密技术,分别为:对称加密非对称加密

+

对称加密:
对称加密就是两边拥有相同的秘钥,两边都知道如何将密文加密解密。

+

非对称加密:
有公钥私钥之分,公钥所有人都可以知道,可以将数据用公钥加密,但是将数据解密必须使用私钥解密,私钥只有分发公钥的一方才知道。

+

TLS 握手过程如下图:

+
    +
  1. 客户端发送一个随机值,需要的协议和加密方式
  2. +
  3. 服务端收到客户端的随机值,自己也产生一个随机值,并根据客户端需求的协议和加密方式来使用对应的方式,发送自己的证书(如果需要验证客户端证书需要说明)
  4. +
  5. 客户端收到服务端的证书并验证是否有效,验证通过会再生成一个随机值,通过服务端证书的公钥去加密这个随机值并发送给服务端,如果服务端需要验证客户端证书的话会附带证书
  6. +
  7. 服务端收到加密过的随机值并使用私钥解密获得第三个随机值,这时候两端都拥有了三个随机值,可以通过这三个随机值按照之前约定的加密方式生成密钥,接下来的通信就可以通过该密钥来加密解密
  8. +
+

通过以上步骤可知,在 TLS 握手阶段,两端使用非对称加密的方式来通信,但是因为非对称加密损耗的性能比对称加密大,所以在正式传输数据时,两端使用对称加密的方式通信。

+

PS:以上说明的都是 TLS 1.2 协议的握手情况,在 1.3 协议中,首次建立连接只需要一个 RTT,后面恢复连接不需要 RTT 了。

+

HTTP 2.0

HTTP 2.0 相比于 HTTP 1.X,可以说是大幅度提高了 web 的性能。

+

在 HTTP 1.X 中,为了性能考虑,我们会引入雪碧图、将小图内联、使用多个域名等等的方式。这一切都是因为浏览器限制了同一个域名下的请求数量,当页面中需要请求很多资源的时候,队头阻塞(Head of line blocking)会导致在达到最大请求数量时,剩余的资源需要等待其他资源请求完成后才能发起请求。

+

你可以通过 该链接 感受下 HTTP 2.0 比 HTTP 1.X 到底快了多少。

+

在 HTTP 1.X 中,因为队头阻塞的原因,你会发现请求是这样的
lWJGkt.png
在 HTTP 2.0 中,因为引入了多路复用,你会发现请求是这样的
lWJa6g.png

+

二进制传输

HTTP 2.0 中所有加强性能的核心点在于此。在之前的 HTTP 版本中,我们是通过文本的方式传输数据。在 HTTP 2.0 中引入了新的编码机制,所有传输的数据都会被分割,并采用二进制格式编码。

+

多路复用

在 HTTP 2.0 中,有两个非常重要的概念,分别是帧(frame)流(stream)
帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流。

+

多路复用,就是在一个 TCP 连接中可以存在多条流。换句话说,也就是可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能。

+

Header 压缩

在 HTTP 1.X 中,我们使用文本的形式传输 header,在 header 携带 cookie 的情况下,可能每次都需要重复传输几百到几千的字节。

+

在 HTTP 2.0 中,使用了 HPACK 压缩格式对传输的 header 进行编码,减少了 header 的大小。并在两端维护了索引表,用于记录出现过的 header ,后面在传输过程中就可以传输已经记录过的 header 的键名,对端收到数据后就可以通过键名找到对应的值。

+

服务端 Push

在 HTTP 2.0 中,服务端可以在客户端某个请求后,主动推送其他资源。

+

可以想象以下情况,某些资源客户端是一定会请求的,这时就可以采取服务端 push 的技术,提前给客户端推送必要的资源,这样就可以相对减少一点延迟时间。当然在浏览器兼容的情况下你也可以使用 prefetch 。

+

QUIC

这是一个谷歌出品的基于 UDP 实现的同为传输层的协议,目标很远大,希望替代 TCP 协议。

+
    +
  • 该协议支持多路复用,虽然 HTTP 2.0 也支持多路复用,但是下层仍是 TCP,因为 TCP 的重传机制,只要一个包丢失就得判断丢失包并且重传,导致发生队头阻塞的问题,但是 UDP 没有这个机制
  • +
  • 实现了自己的加密协议,通过类似 TCP 的 TFO 机制可以实现 0-RTT,当然 TLS 1.3 已经实现了 0-RTT 了
  • +
  • 支持重传和纠错机制(向前恢复),在只丢失一个包的情况下不需要重传,使用纠错机制恢复丢失的包
      +
    • 纠错机制:通过异或的方式,算出发出去的数据的异或值并单独发出一个包,服务端在发现有一个包丢失的情况下,通过其他数据包和异或值包算出丢失包
    • +
    • 在丢失两个包或以上的情况就使用重传机制,因为算不出来了
    • +
    +
  • +
+

DNS

DNS 的作用就是通过域名查询到具体的 IP。

+

因为 IP 存在数字和英文的组合(IPv6),很不利于人类记忆,所以就出现了域名。你可以把域名看成是某个 IP 的别名,DNS 就是去查询这个别名的真正名称是什么。

+

在 TCP 握手之前就已经进行了 DNS 查询,这个查询是操作系统自己做的。当你在浏览器中想访问 www.google.com 时,会进行一下操作:

+
    +
  1. 操作系统会首先在本地缓存中查询
  2. +
  3. 没有的话会去系统配置的 DNS 服务器中查询
  4. +
  5. 如果这时候还没得话,会直接去 DNS 根服务器查询,这一步查询会找出负责 com 这个一级域名的服务器
  6. +
  7. 然后去该服务器查询 google 这个二级域名
  8. +
  9. 接下来三级域名的查询其实是我们配置的,你可以给 www 这个域名配置一个 IP,然后还可以给别的三级域名配置一个 IP
  10. +
+

以上介绍的是 DNS 迭代查询,还有种是递归查询,区别就是前者是由客户端去做请求,后者是由系统配置的 DNS 服务器做请求,得到结果后将数据返回给客户端。

+

PS:DNS 是基于 UDP 做的查询。

+

从输入 URL 到页面加载完成的过程

这是一个很经典的面试题,在这题中可以将本文讲得内容都串联起来。

+
    +
  1. 首先做 DNS 查询,如果这一步做了智能 DNS 解析的话,会提供访问速度最快的 IP 地址回来
  2. +
  3. 接下来是 TCP 握手,应用层会下发数据给传输层,这里 TCP 协议会指明两端的端口号,然后下发给网络层。网络层中的 IP 协议会确定 IP 地址,并且指示了数据传输中如何跳转路由器。然后包会再被封装到数据链路层的数据帧结构中,最后就是物理层面的传输了
  4. +
  5. TCP 握手结束后会进行 TLS 握手,然后就开始正式的传输数据
  6. +
  7. 数据在进入服务端之前,可能还会先经过负责负载均衡的服务器,它的作用就是将请求合理的分发到多台服务器上,这时假设服务端会响应一个 HTML 文件
  8. +
  9. 首先浏览器会判断状态码是什么,如果是 200 那就继续解析,如果 400500 的话就会报错,如果 300 的话会进行重定向,这里会有个重定向计数器,避免过多次的重定向,超过次数也会报错
  10. +
  11. 浏览器开始解析文件,如果是 gzip 格式的话会先解压一下,然后通过文件的编码格式知道该如何去解码文件
  12. +
  13. 文件解码成功后会正式开始渲染流程,先会根据 HTML 构建 DOM 树,有 CSS 的话会去构建 CSSOM 树。如果遇到 script 标签的话,会判断是否存在 async 或者 defer ,前者会并行进行下载并执行 JS ,后者会先下载文件,然后等待 HTML 解析完成后顺序执行,如果以上都没有,就会阻塞住渲染流程直到 JS 执行完毕。遇到文件下载的会去下载文件,这里如果使用 HTTP 2.0 协议的话会极大的提高多图的下载效率。
  14. +
  15. 初始的 HTML 被完全加载和解析后会触发 DOMContentLoaded 事件
  16. +
  17. CSSOM 树和 DOM 树构建完成后会开始生成 Render 树,这一步就是确定页面元素的布局、样式等等诸多方面的东西
  18. +
  19. 在生成 Render 树的过程中,浏览器就开始调用 GPU 绘制,合成图层,将内容显示在屏幕上了
  20. +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/84/index.html b/en/page/84/index.html new file mode 100644 index 0000000000..7fb6ae5409 --- /dev/null +++ b/en/page/84/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在做公司技术规范,需要能通过 B/S 方式进行分发,起初想法是做一个网站,用 html 的方式进行书写,但是后面感觉太麻烦了,所以就放弃了,偶然间看到有工具可以直接通过 md 文件编译生成网站,就像是 hexo 博客这样,我觉得这挺好,极大的节省了开发网站所需要的时间,只需要专注于内容就好了,所以我就研究了下来,整个了解过程中有这么两个工具映入了我的眼帘,一个是 doctify,另外一个就是今天的主角 vuepress

+ +
+ + Read more » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/85/index.html b/en/page/85/index.html new file mode 100644 index 0000000000..3710049dc1 --- /dev/null +++ b/en/page/85/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

定义

柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

+

柯里化,可以理解为提前接收部分参数,延迟执行,不立即输出结果,而是返回一个接受剩余参数的函数。因为这样的特性,也被称为部分计算函数。

+

通俗易懂的解释:用闭包把参数保存起来,当参数的数量足够执行函数了,就开始执行函数。柯里化,是一个逐步接收参数的过程。在接下来的剖析中,你会深刻体会到这一点。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码

function currying(fn){
var allArgs = [];

return function next(){
var args = [].slice.call(arguments); // 拆成数组元素

if(args.length > 0){
allArgs = allArgs.concat(args);
return next;
}else{
return fn.apply(null, allArgs);
}
}
}
+

我们来一个简单的实例验证一下:

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

var add = currying(function(){
var sum = 0;
for(var i = 0; i < arguments.length; i++){
sum += arguments[i];
}
return sum;
});

add(1)(2, 3)(4)() // => 10
+ +

应用场景

参数复用

1
2
3
4
5
6
7
8
// js代码

function getUrl(domain, protocol, path) {
return protocol + "://" + domain + "/" + path;
}

var page1 = getUrl('http', 'lixuguang.github.io', 'page1.html');
var page2 = getUrl('http', 'lixuguang.github.io', 'page2.html');
+

我们使用currying来简化它:

+
1
2
var conardliSite = currying(getUrl)
var page1 = conardliSite('page1.html')('http', 'lixuguang.github.io')();
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/86/index.html b/en/page/86/index.html new file mode 100644 index 0000000000..5d227b2df8 --- /dev/null +++ b/en/page/86/index.html @@ -0,0 +1,515 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近看了好多技术文章,好多的代码片段,突然发现好多的代码里都有 reduce ,感觉以前都没怎么关注过这个函数,为了弥补这个过失,打算专门写一篇文章说说它。

+

reduce 函数在 MDN 中是这样介绍的

+
+

reduce() 方法对数组中的每个元素执行一个由 提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。

+
+

说实话看了一脸懵逼,这上面说的叫人话?后来耐着性子看了一些代码后感觉有所理解,如果理解不对,还请斧正。
首先看一下这里面几个关键词

+

* 每个元素: * 这就是遍历咯,没啥好说的
提供的 reducer 函数:我哪有什么 reducer 函数呢,后来理解了,就是回调,有的地方呢叫 handler ,这里跟 reduce 配合就叫了 reducer
升序执行:就是说是0,1,2下标这样的顺序执行啦。
将其结果汇总为单个返回值:最后返回的是一个值,当然没说必须是什么类型。

+

这样解释完了以后,上面这句话就比较好懂了,简单说就是 reduce 里传一个回调函数,执行回调函数方法后返回一个值,就是这样。

+
+

reducer 函数接收4个参数:

+
+
    +
  • Accumulator (acc) (累计器)
  • +
  • Current Value (cur) (当前值)
  • +
  • Current Index (idx) (当前索引)
  • +
  • Source Array (src) (源数组)
    您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
  • +
+

看上面的描述一定要注意,这些参数是 reducer 的参数,不是 reduce 的参数,一共有4个,通常用前两个情况比较多。

+

那么 reduce 函数呢实际上有两个参数,第二个还可以省略 reducerinitialValueinitialValue作为第一次调用 reducer 函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错,这点切记。

+

终于把前置知识讲清楚了,接下来,我们就用 reduce 搞点事情。

+

1. 使用 reduce 实现 map

map 的话我不想再讲一遍,一句话带过,就是对回调函数中的元素进行加工后返回一个长度一样的新数组。
那么该如何实现呢,我们来想想原理吧,刚才上面说过了,reduce执行过后的返回值会作为下一次执行的第一个参数放进去,那么就可以先用一个空数组作为初始值来接受每次需要处理的元素的集合,并返回回去,作为下一次传入的第一个参数,这样每次操作完之后就可以push到这个数组中,那么剩下要做的就是处理数组中每一项的函数了,我们叫他 handler

+
1
2
3
4
5
6
7
8
// js代码

Array.prototype.reduceToMap = function (handler) { // 自定义 `map` 函数 `reduceToMap`
return this.reduce((target, current, index) => { // this指向调用他的数组
target.push(handler.call(this, current, index)) // 这里用了call方法,handler将接受两个参数 current和index
return target; // 处理完成后返回新数组
}, []) // 初始化空的新数组
};
+

就这样我们就用 reduce,实现了 map 的功能,是不是很好用?

+

2. 使用 reduce 实现 filter

filter 也是数组常用的方法,同样传入一个回调函数,处理结果返回true或false,最终 filter 会返回一个过滤后的函数。
学会了上面的 map 的实现,实际上 filter 就会很简单

+
1
2
3
4
5
6
7
8
9
10
// js代码

Array.prototype.reduceToFilter = function (handler) { // 还是自定义方法名
return this.reduce((target, current, index) => {
if (handler.call(this, current, index)) { // 这里注意 handler 要返回的是布尔类型的值
target.push(current); // 符合条件就插入新数组
} // 不符合就什么都不做
return target; // 最后返回新数组
}, []) // 初始化一个空数组
};
+

日后在看到 reduce 的妙用之后还会来补充这篇文章,如果实在懒得写,我也会链接一下新文章,希望大家长期关注。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/87/index.html b/en/page/87/index.html new file mode 100644 index 0000000000..8e9bd57ba2 --- /dev/null +++ b/en/page/87/index.html @@ -0,0 +1,528 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

去重

1. 利用 ObjectKey 唯一特性

开辟一个外部存储空间用于标示元素是否出现过。

+
1
2
3
4
5
6
// js代码

const unique = (array)=> {
var container = {};
return array.filter((item, index) => container.hasOwnProperty(item) ? false : (container[item] = true));
}
+ +

2. 利用 indexOf 的返回值数值进行去重

原理是 indexOf 获取元素时如果返回值不等于下标说明已经有了,配合 filter 更美味

+
1
2
3
4
5
// js代码

const unique = arr => arr.filter((e,i) =>
arr.indexOf(e) === i // 如果元素找到的当前下标和当前索引相同说明是同一个,不同说明不是唯一
);
+ +

还有一种变形方法利用 lastIndexOf 方法

+
+

lastIndexOf() 方法返回指定元素(也即有效的 JavaScript 值或变量)在数组中的最后一个的索引,如果不存在则返回 -1。从数组的后面向前查找,从 fromIndex 处开始。

+
+
1
2
3
4
5
// js代码

const filterNonUnique = arr => arr.filter(e =>
arr.indexOf(e) === arr.lastIndexOf(e) // 判断一个元素出现时的第一次下标和最后一次下标是否相同,如果相同那么就唯一
)
+

3. 利用 Set 特性去重

SetES6 中新的数据类型,它的特点就是元素唯一性,且可以和数组进行转换

+
1
2
3
4
5
6
7
// js代码

const unique = arr => Array.from(new Set(arr)); // Array.from 方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。

// 优化

const unique = arr => [...new Set(arr)]; // 利用 `ES6` 中展开操作
+

4. 排序后判断前后两项是否相等去重

通过比较相邻数字是否重复,将排序后的数组进行去重。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

const unique = (array) => {
array.sort((a, b) => a - b);
let pre = 0;
const result = [];
for (let i = 0; i < array.length; i++) {
if (!i || array[i] != array[pre]) {
result.push(array[i]);
}
pre = i;
}
return result;
}
+ +

扁平

1. 普通方法

通过递归的方式判断数组中的项是否是数组,如果不是就加入到新的扁平数组,如果是就递归调用逐层判断,直到全部结束

+
1
2
3
4
5
6
7
8
9
10
11
12
13
// js代码

const flatten = (array) => { // array 原数组
let result = []; // 定义新的扁平数组
for (let i = 0; i < array.length; i++) {
if (Array.isArray(array[i])) { // 判断子元素是否是数组
result = result.concat(flatten(array[i])); // 递归判断
} else {
result.push(array[i]); // 加入新数组
}
}
return result;
}
+ +

2. 使用reduce简化上述方法

+

reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
reducer 函数接收4个参数:

+
+
    +
  • Accumulator (acc) (累计器)
  • +
  • Current Value (cur) (当前值)
  • +
  • Current Index (idx) (当前索引)
  • +
  • Source Array (src) (源数组)
  • +
  • 您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
  • +
+

先看一段 reduce 的示例函数

+
1
2
3
4
5
6
7
8
9
10
11
12
// js代码

const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue; // 定义一个累计器函数,作用是将数组前后累计值与当前值累加

// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer)); // 没有初始值用第一个元素
// expected output: 10

// 5 + 1 + 2 + 3 + 4
console.log(array1.reduce(reducer, 5)); // 有初始值从初始值开始
// expected output: 15
+

这下大家应该对 reduce 函数认识了,接下来看看怎么简化

+
1
2
3
4
5
6
7
8
9
// js代码

function flatten(array) {
return array.reduce((newArray, current) => // 新数组,当前项
Array.isArray(current) ? // 判断当前项是否为数组
newArray.concat(flatten(current)) : // 是的话 递归调用
newArray.concat(current) // 不是的话加进新数组
, []) // 初始化新数组为空
}
+

这里我们再变一个形,增加一个变量,变成可指定深度操作数组

+
1
2
3
4
5
6
7
8
9
10
// js代码

function flattenByDeep(array, deep = 1) { // 默认一层
return array.reduce(
(target, current) =>
Array.isArray(current) && deep > 1 ?
target.concat(flattenByDeep(current, deep - 1)) : // 下一次减一层
target.concat(current)
, [])
}
+

最值

利用 reduce

reduce 函数真的是超级好用,

+
1
2
3
// js代码

array.reduce((c,n) => Math.max(c,n))
+ +

Math.max

Math.max 参数原本是一组数字,只需要让他可以接收数组即可。

+
1
2
3
4
5
// js代码

const array = [3,2,1,4,5];
Math.max.apply(null,array);
Math.max(...array);
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/88/index.html b/en/page/88/index.html new file mode 100644 index 0000000000..28945bba3c --- /dev/null +++ b/en/page/88/index.html @@ -0,0 +1,634 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

基本概念

时间复杂度

一个算法的时间复杂度反映了程序运行从开始到结束所需要的时间。把算法中基本操作重复执行的次数(频度)作为算法的时间复杂度。
没有循环语句,记作O(1),也称为常数阶。只有一重循环,则算法的基本操作的执行频度与问题规模n呈线性增大关系,记作O(n),也叫线性阶。
常见的时间复杂度有:

+
    +
  • O(1): Constant Complexity: Constant 常数复杂度
  • +
  • O(log n): Logarithmic Complexity: 对数复杂度
  • +
  • O(n): Linear Complexity: 线性时间复杂度
  • +
  • O(n^2): N square Complexity 平⽅方
  • +
  • O(n^3): N square Complexity ⽴立⽅方
  • +
  • O(2^n): Exponential Growth 指数
  • +
  • O(n!): Factorial 阶乘
  • +
+

空间复杂度

一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。

+

一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。

+
    +
  • 稳定
  • +
  • 不稳定

    算法汇总

    十大经典排序.jpg

    关于时间复杂度:

    平方阶 (O(n2)) 排序 各类简单排序:直接插入、直接选择和冒泡排序。
    线性对数阶 (O(nlog2n)) 排序 快速排序、堆排序和归并排序;
    O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数。 希尔排序
    线性阶 (O(n)) 排序 基数排序,此外还有桶、箱排序。
  • +
+

关于稳定性:

稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。

+

名词解释:

    +
  • n:数据规模
  • +
  • k:“桶”的个数
  • +
  • In-place:占用常数内存,不占用额外内存
  • +
  • Out-place:占用额外内存
  • +
  • 稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同
  • +
+

1. 冒泡排序(Bubble Sort) — 前后两两比较 — 气泡

冒泡排序可谓是最经典的排序算法了,它是基于比较的排序算法,其优点是实现简单,排序数量较小时性能较好。

+

它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

+

1. 1 算法原理

相邻的数据进行两两比较,小数放在前面,大数放在后面,如果前面的数据比后面的数据大,就交换这两个数的位置。也可以实现大数放在前面,小数放在后面,如果前面的数据比后面的小,就交换两个的位置。要实现上述规则需要用到两层for循环。

+

1. 2 算法描述

    +
  1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  2. +
  3. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  4. +
  5. 针对所有的元素重复以上的步骤,除了最后一个;
  6. +
  7. 重复步骤1~3,直到排序完成。
  8. +
+

1. 3 动图演示

ldB5VS.gif

+

1. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// js代码

function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len; i++) {
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { // 相邻元素两两对比
// 元素交换
/** 1.使用中间变量 **/
var temp = arr[j + 1];
arr[j + 1] = arr[j]
arr[j] = temp
/** 2.适用纯数字的数组排序 **/
arr[j] = arr[j] + arr[j + 1]
arr[j + 1] = arr[j] - arr[j + 1]
arr[j] -= arr[j + 1]
/** 3.使用es6解构赋值 **/
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
}
}
}
return arr;
}
+

冒泡排序算法优化

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len; i++) {
var exchange=false; // 交换标志
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { // 相邻元素两两对比
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]] // 元素交换
exchange=true; //
}
}
if(!exchange){ // 若本趟排序未发生交换,提前终止算法
break;
}
}
return arr;
}
+

2. 选择排序(Selection Sort) — 面试挑简历,在剩下的里面挑最好的

表现最稳定的排序算法之一,因为无论什么数据进去都是O(n²)的时间复杂度。。。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。

+

2. 1 算法原理

先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

+

2. 2 算法描述

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

+
    +
  1. 初始状态:无序区为R[1..n],有序区为空;
  2. +
  3. 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中
  4. +
  5. 选出关键字最小的记录R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
  6. +
  7. n-1趟结束,数组有序化了。
  8. +
+

2. 3 动图演示

ldDWW9.gif

+

2. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

function selectionSort(arr) {
var len = arr.length;
var minIndex, temp;
for (var i = 0; i < len - 1; i++) {
minIndex = i;
for (var j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) { //寻找最小的数
minIndex = j; //将最小数的索引保存
}
}
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
return arr;
}
+ +

3. 插入排序(Insertion Sort)—– 麻将/扑克

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。

+

3. 1 算法原理

它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

+

3. 2 算法描述

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

+
    +
  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. +
  3. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  4. +
  5. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  6. +
  7. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  8. +
  9. 将新元素插入到该位置后;
  10. +
  11. 重复步骤2~5。
  12. +
+

3. 3 动图演示

ldDfzR.gif

+

3. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// js代码

function insertSort(arr) {
// 从1位置开始遍历arr中每元素,同时声明空变量temp
for (let i = 1; i < arr.length; i++) {
if (arr[i] < arr[i - 1]) { // 如果当前元素<前一个元素
let temp = arr[i] // 将当前元素值临时保存在temp中
let p = i - 1 // 定义变量 p = i- 1
// 循环 条件:
// 1. p>=0且temp小于p位置的元素
while (p >= 0 && temp < arr[p]) {
// 循环体: 将P位置的值赋值给p的后一个元素
arr[p + 1] = arr[p]
p-- // p向前移动一个
}
arr[p + 1] = temp // 将temp的值赋值给p+1位置的元素
}
}
}
+ +

4. 快速排序(Selection Sort)

快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

+

4. 1 算法原理

快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。

+

4. 2 算法描述

选基准:在数据结构中选择一个元素作为基准(pivot
划分区:参照基准元素值的大小,划分无序区,所有小于基准元素的数据放入一个区间,所有大于基准元素的数据放入另一区间,分区操作结束后,基准元素所处的位置就是最终排序后它应该所处的位置
递归:对初次划分出来的两个无序区间,递归调用第 1步和第 2步的算法,直到所有无序区间都只剩下一个元素为止。

+

简单理解就是,选择一个目标值,比目标值小的放左边,比目标值大的放右边,目标值的位置已排好,将左右两侧再进行快排。

+

4. 3 动图演示

快速排序.gif

+

4. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// js代码

function quickSort(arr){
//如果arr.length<=1,则直接返回arr
if(arr.length<=1){return arr}
// arr的元素个数/2,再下去整,将值保存在pivotIndex中
var pivotIndex=Math.floor(arr.length/2);
// 将arr中pivotIndex位置的元素,保存在变量pivot中
var pivot=arr[pivotIndex];
//声明空数组left和right
var left=[];
var right=[];
for(var i=0;i<arr.length;i++){ // 遍历arr中每个元素
if(i !== pivotIndex){ // 如果i !== pivotIndex
if(arr[i]<=pivot){ // 如果当前元素值<pivot
left.push(arr[i]); // 就将当前值压入left
}else{
right.push(arr[i]); // 就将当前值压入right
}
}
}
//递归
return quickSort(left).concat(pivot, quickSort(right)); // 链接多个数组到 left 从小到大
}
+

5. 希尔排序

5. 1 算法原理

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:

+
    +
  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
  • +
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
    希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
  • +
+

5. 2 算法描述

    +
  • 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
  • +
  • 按增量序列个数 k,对序列进行 k 趟排序;
  • +
  • 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
  • +
+

5.3 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// js代码

function shellSort(arr) {
var len = arr.length,
temp,
gap = 1;
while(gap < len/3) { //动态定义间隔序列
gap =gap*3+1;
}
for (gap; gap > 0; gap = Math.floor(gap/3)) {
for (var i = gap; i < len; i++) {
temp = arr[i];
for (var j = i-gap; j >= 0 && arr[j] > temp; j-=gap) {
arr[j+gap] = arr[j];
}
arr[j+gap] = temp;
}
}
return arr;
}
+ +

6. 归并排序

6. 1 算法原理

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

+
    +
  • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
  • +
  • 自下而上的迭代;
    +

    在《数据结构与算法 JavaScript 描述》中,作者给出了自下而上的迭代方法。但是对于递归法,作者却认为:
    However, it is not possible to do so in JavaScript, as the recursion goes too deep for the language to handle.
    然而,在 JavaScript 中这种方式不太可行,因为这个算法的递归深度对它来讲太深了。

    +
    +
  • +
+

说实话,我不太理解这句话。意思是 JavaScript 编译器内存太小,递归太深容易造成内存溢出吗?还望有大神能够指教。

+

和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。

+

6. 2 算法描述

    +
  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
  2. +
  3. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
  4. +
  5. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
  6. +
  7. 重复步骤 3 直到某一指针达到序列尾;
  8. +
  9. 将另一序列剩下的所有元素直接复制到合并序列尾。
  10. +
+

6. 3 动图演示

归并排序.gif

+

6. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// js代码

function mergeSort(arr) { // 采用自上而下的递归方法
var len = arr.length;
if(len < 2) {
return arr;
}
var middle = Math.floor(len / 2),
left = arr.slice(0, middle),
right = arr.slice(middle);
return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right){
var result = [];
while (left.length && right.length) {
if (left[0] <= right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}

while (left.length)
result.push(left.shift());

while (right.length)
result.push(right.shift());

return result;
}
+ +

7. 堆排序

7. 1 算法原理

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

+
    +
  • 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
  • +
  • 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
    堆排序的平均时间复杂度为 Ο(nlogn)。
  • +
+

7. 2 算法描述

    +
  1. 将待排序序列构建成一个堆 H[0……n-1],根据(升序降序需求)选择大顶堆或小顶堆;
  2. +
  3. 把堆首(最大值)和堆尾互换;
  4. +
  5. 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
  6. +
  7. 重复步骤 2,直到堆的尺寸为 1。
  8. +
+

7. 3 动图演示

堆排序.gif

+

7. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// js代码

var len; // 因为声明的多个函数都需要数据长度,所以把len设置成为全局变量

function buildMaxHeap(arr) { // 建立大顶堆
len = arr.length;
for (var i = Math.floor(len/2); i >= 0; i--) {
heapify(arr, i);
}
}

function heapify(arr, i) { // 堆调整
var left = 2 * i + 1,
right = 2 * i + 2,
largest = i;

if (left < len && arr[left] > arr[largest]) {
largest = left;
}

if (right < len && arr[right] > arr[largest]) {
largest = right;
}

if (largest != i) {
swap(arr, i, largest);
heapify(arr, largest);
}
}

function swap(arr, i, j) {
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

function heapSort(arr) {
buildMaxHeap(arr);

for (var i = arr.length-1; i > 0; i--) {
swap(arr, 0, i);
len--;
heapify(arr, 0);
}
return arr;
}
+ +

8. 计数排序

8. 1 算法原理

计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

+

8. 2 算法描述

8. 3 动图演示

计数排序.gif

+

8. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// js代码

function countingSort(arr, maxValue) {
var bucket = new Array(maxValue+1),
sortedIndex = 0;
arrLen = arr.length,
bucketLen = maxValue + 1;

for (var i = 0; i < arrLen; i++) {
if (!bucket[arr[i]]) {
bucket[arr[i]] = 0;
}
bucket[arr[i]]++;
}

for (var j = 0; j < bucketLen; j++) {
while(bucket[j] > 0) {
arr[sortedIndex++] = j;
bucket[j]--;
}
}

return arr;
}
+ +

9. 桶排序

9. 1 算法原理

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

+
    +
  • 在额外空间充足的情况下,尽量增大桶的数量
  • +
  • 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
    同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

    9. 2 算法描述

    1. 什么时候最快

    当输入的数据可以均匀的分配到每一个桶中。

    2. 什么时候最慢

    当输入的数据被分配到了同一个桶中。

    9. 3 动图演示

  • +
+

9. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// js代码

function bucketSort(arr, bucketSize) {
if (arr.length === 0) {
return arr;
}

var i;
var minValue = arr[0];
var maxValue = arr[0];
for (i = 1; i < arr.length; i++) {
if (arr[i] < minValue) {
minValue = arr[i]; // 输入数据的最小值
} else if (arr[i] > maxValue) {
maxValue = arr[i]; // 输入数据的最大值
}
}

//桶的初始化
var DEFAULT_BUCKET_SIZE = 5; // 设置桶的默认数量为5
bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
var bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
var buckets = new Array(bucketCount);
for (i = 0; i < buckets.length; i++) {
buckets[i] = [];
}

//利用映射函数将数据分配到各个桶中
for (i = 0; i < arr.length; i++) {
buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i]);
}

arr.length = 0;
for (i = 0; i < buckets.length; i++) {
insertionSort(buckets[i]); // 对每个桶进行排序,这里使用了插入排序
for (var j = 0; j < buckets[i].length; j++) {
arr.push(buckets[i][j]);
}
}

return arr;
}
+ +

10. 基数排序

10. 1 算法原理

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

+

10. 2 算法描述

1. 基数排序 vs 计数排序 vs 桶排序

基数排序有三种方法:
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异案例看大家发的:

+
    +
  • 基数排序:根据键值的每位数字来分配桶;
  • +
  • 计数排序:每个桶只存储单一键值;
  • +
  • 桶排序:每个桶存储一定范围的数值;

    10. 3 动图演示

  • +
+
    +
  1. LSD 基数排序动图演示
    基数排序.gif

    10. 4 js代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // js代码

    // LSD Radix Sort
    var counter = [];
    function radixSort(arr, maxDigit) {
    var mod = 10;
    var dev = 1;
    for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
    for(var j = 0; j < arr.length; j++) {
    var bucket = parseInt((arr[j] % mod) / dev);
    if(counter[bucket]==null) {
    counter[bucket] = [];
    }
    counter[bucket].push(arr[j]);
    }
    var pos = 0;
    for(var j = 0; j < counter.length; j++) {
    var value = null;
    if(counter[j]!=null) {
    while ((value = counter[j].shift()) != null) {
    arr[pos++] = value;
    }
    }
    }
    }
    return arr;
    }
  2. +
+

总结

排序算法.png
以上就是十大经典算法,算法对于前端来说并不是一个十分熟悉的领域,但是排序算法算是算法里比较入门的,还是需要掌握的,毕竟即使是为了面试也是要准备的。

+

参考资料

https://github.com/hustcc/JS-Sorting-Algorithm
一本关于排序算法的 GitBook 在线书籍 《十大经典排序算法》,多语言实现。

+

http://www.sohu.com/a/136157205_671058
技术面试宝典: 很全面的算法和数据结构知识(含代码实现)下篇

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/89/index.html b/en/page/89/index.html new file mode 100644 index 0000000000..94782779d6 --- /dev/null +++ b/en/page/89/index.html @@ -0,0 +1,541 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

cookie,localStorage,sessionStorage,indexDB

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
特性cookielocalStoragesessionStorageindexDB
数据生命周期一般由服务器生成,可以设置过期时间除非被清理,否则一直存在页面关闭就清理除非被清理,否则一直存在
数据存储大小4K5M5M无限制
与服务端通信每次都会携带在 header 中,对于请求性能影响不参与不参与不参与
+

从上表可以看到, cookie 已经不建议用于存储。如果没有大量数据存储需求的话,可以使用 localStoragesessionStorage 。对于不怎么改变的数据尽量使用 localStorage 存储,否则可以用 sessionStorage 存储。

+

对于 cookie ,我们还需要注意安全性。
| 属性 | 作用 |
| ——— | ————————————————————– |
| value | 如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识 |
| http-only | 不能通过 JS 访问 Cookie,减少 XSS 攻击 |
| secure | 只能在协议为 HTTPS 的请求中携带 |
| same-site | 规定浏览器不能在跨域请求中携带 Cookie,减少 CSRF 攻击 |

+

Service Worker

+

Service workers 本质上充当 Web 应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步 API。

+
+

目前该技术通常用来做缓存文件,提高首屏速度,可以试着来实现这个功能。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// js代码

// index.js
if (navigator.serviceWorker) {
navigator.serviceWorker
.register('sw.js')
.then(function(registration) {
console.log('service worker 注册成功')
})
.catch(function(err) {
console.log('servcie worker 注册失败')
})
}
// sw.js
// 监听 `install` 事件,回调中缓存所需文件
self.addEventListener('install', e => {
e.waitUntil(
caches.open('my-cache').then(function(cache) {
return cache.addAll(['./index.html', './index.js'])
})
)
})

// 拦截所有请求事件
// 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据
self.addEventListener('fetch', e => {
e.respondWith(
caches.match(e.request).then(function(response) {
if (response) {
return response
}
console.log('fetch source')
})
)
})
+ +

打开页面,可以在开发者工具中的 Application 看到 Service Worker 已经启动了
Cache 中也可以发现我们所需的文件已被缓存

+

当我们重新刷新页面可以发现我们缓存的数据是从 Service Worker 中读取的

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/9/index.html b/en/page/9/index.html new file mode 100644 index 0000000000..e952c0bb5a --- /dev/null +++ b/en/page/9/index.html @@ -0,0 +1,514 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

systemctl 命令

systemctl

范列出系统上面有启动的unit

+

systemctl list-unit-files

列出所有已经安装的unit有哪些

+

systemctl list-units –type=service –all

列出类型为service的所有项目,不论启动与否

+

systemctl get-default

输入目前机器默认的模式,如图形界面模式或者文本模式

+

systemctl isolate multi-user.target

将目前的操作环境改为纯文本模式,关掉图形界面

+

systemctl isolate graphical.target

将目前的操作环境改为图形界面

+

systemctl poweroff

系统关机

+

systemctl reboot

重新开机

+

systemctl suspend

进入暂停模式

+

systemctl rescue

强制进入救援模式

+

systemctl hibernate

进入休眠模式

+

systemctl emergency

强制进入紧急救援模式

+

systemctl list-dependencies –reverse

查询当前默认的target关联了啥

+

systemctl list-dependencies graphical.target

查询图形界面模式的target关联了啥

+

systemctl list-sockets

查看当前的socket服务

+

systemctl show etcd.service

查看 unit 的详细配置情况

+

systemctl mask etcd.service

禁用某个服务

+

systemctl unmask etcd.service

解除禁用某个服务

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/90/index.html b/en/page/90/index.html new file mode 100644 index 0000000000..71e27639e9 --- /dev/null +++ b/en/page/90/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Proxy

因为 JS 采用 IEEE 754 双精度版本(64位),并且只要采用 IEEE 754 的语言都有该问题。

+

我们都知道计算机表示十进制是采用二进制表示的,所以 0.1 在二进制表示为

+
1
2
3
4
// js代码

// (0011) 表示循环
0.1 = 2^-4 * 1.10011(0011)
+

那么如何得到这个二进制的呢,我们可以来演算下

+

小数算二进制和整数不同。乘法计算时,只计算小数位,整数位用作每一位的二进制,并且得到的第一位为最高位。所以我们得出 0.1 = 2^-4 * 1.10011(0011),那么 0.2 的演算也基本如上所示,只需要去掉第一步乘法,所以得出 0.2 = 2^-3 * 1.10011(0011)

+

回来继续说 IEEE 754 双精度。六十四位中符号位占一位,整数位占十一位,其余五十二位都为小数位。因为 0.10.2 都是无限循环的二进制了,所以在小数位末尾处需要判断是否进位(就和十进制的四舍五入一样)。

+

所以 2^-4 * 1.10011...001 进位后就变成了 2^-4 * 1.10011(0011 * 12次)010 。那么把这两个二进制加起来会得出 2^-2 * 1.0011(0011 * 11次)0100 , 这个值算成十进制就是 0.30000000000000004

+

下面说一下原生解决办法,如下代码所示

+
1
2
3
// js代码

parseFloat((0.1 + 0.2).toFixed(10))
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/91/index.html b/en/page/91/index.html new file mode 100644 index 0000000000..a1812d787e --- /dev/null +++ b/en/page/91/index.html @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Proxy

ProxyES6 中新增的功能,可以用来自定义对象中的操作

+
1
2
3
4
5
// js代码

let p = new Proxy(target, handler);
// `target` 代表需要添加代理的对象
// `handler` 用来自定义对象中的操作
+

可以很方便的使用 Proxy 来实现一个数据绑定和监听

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(target, property)
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
setBind(value);
return Reflect.set(target, property, value);
}
};
return new Proxy(obj, handler);
};

let obj = { a: 1 }
let value
let p = onWatch(obj, (v) => {
value = v
}, (target, property) => {
console.log(`Get '${property}' = ${target[property]}`);
})
p.a = 2 // bind `value` to `2`
p.a // -> Get 'a' = 2
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/92/index.html b/en/page/92/index.html new file mode 100644 index 0000000000..bf69b395b0 --- /dev/null +++ b/en/page/92/index.html @@ -0,0 +1,516 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

async 和 await

一个函数如果加上 async ,那么该函数就会返回一个 Promise

+
1
2
3
4
async function test() {
return "1";
}
console.log(test()); // -> Promise {<resolved>: "1"}
+

可以把 async 看成将函数返回值使用 Promise.resolve() 包裹了下。
await 只能在 async 函数中使用

+
1
2
3
4
5
6
7
8
9
10
11
12
13
function sleep() {
return new Promise(resolve => {
setTimeout(() => {
console.log('finish')
resolve("sleep");
}, 2000);
});
}
async function test() {
let value = await sleep();
console.log("object");
}
test()
+ +

上面代码会先打印 finish 然后再打印 object 。因为 await 会等待 sleep 函数 resolve ,所以即使后面是同步代码,也不会先去执行同步代码再来执行异步代码。

+

asyncawait 相比直接使用 Promise 来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码。缺点在于滥用 await 可能会导致性能问题,因为 await 会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。

+

下面来看一个使用 await 的代码。

+
1
2
3
4
5
6
7
8
9
10
var a = 0
var b = async () => {
a = a + await 10
console.log('2', a) // -> '2' 10
a = (await 10) + a
console.log('3', a) // -> '3' 20
}
b()
a++
console.log('1', a) // -> '1' 1
+ +

对于以上代码你可能会有疑惑,这里说明下原理

+
    +
  • 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为在 await 内部实现了 generatorsgenerators 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
  • +
  • 因为 await 是异步操作,遇到 await 就会立即返回一个 pending 状态的 Promise 对象,暂时返回执行代码的控制权,使得函数外的代码得以继续执行,所以会先执行 console.log('1', a)
  • +
  • 这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 10
  • +
  • 然后后面就是常规执行代码了
  • +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/93/index.html b/en/page/93/index.html new file mode 100644 index 0000000000..062645e221 --- /dev/null +++ b/en/page/93/index.html @@ -0,0 +1,515 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Map、FlatMap 和 Reduce

Map

Map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后 append 到新的数组中。

+
1
2
3
// js代码

[1, 2, 3].map((v) => v + 1) // -> [2, 3, 4]
+

Map 有三个参数,分别是当前索引元素索引原数组

+

FlatMap

FlatMapmap 的作用几乎是相同的,但是对于多维数组来说,会将原数组降维。可以将 FlatMap 看成是 map + flatten ,目前该函数在浏览器中还不支持。

+
1
2
3
// js代码

[1, [2], 3].flatMap((v) => v + 1) // -> [2, 3, 4]
+

如果想将一个多维数组彻底的降维,可以这样实现

+
1
2
3
4
5
const flattenDeep = (arr) => Array.isArray(arr)
? arr.reduce( (a, b) => [...a, ...flattenDeep(b)] , [])
: [arr]

flattenDeep([1, [[2], [3, [4]], 5]])
+ +

Reduce 升序执行

Reduce 作用是数组中的值组合起来,最终得到一个值
reduce() 方法对数组中的每个元素执行一个由您提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。

+

reducer 函数接收4个参数:

+
    +
  • Accumulator (acc) (累计器)
  • +
  • Current Value (cur) (当前值)
  • +
  • Current Index (idx) (当前索引)
  • +
  • Source Array (src) (源数组)
    您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
// js代码

function a() {
console.log(1);
}

function b() {
console.log(2);
}

[a, b].reduce((a, b) => a(b()))
// -> 2 1
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/94/index.html b/en/page/94/index.html new file mode 100644 index 0000000000..6365df5b6b --- /dev/null +++ b/en/page/94/index.html @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Generator 实现

GeneratorES6 中新增的语法,和 Promise 一样,都可以用来异步编程

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

// 使用 * 表示这是一个 Generator 函数
// 内部可以通过 yield 暂停代码
// 通过调用 next 恢复执行
function* test() {
let a = 1 + 2;
yield 2;
yield 3;
}
let b = test();
console.log(b.next()); // > { value: 2, done: false }
console.log(b.next()); // > { value: 3, done: false }
console.log(b.next()); // > { value: undefined, done: true }
+

从以上代码可以发现,加上 * 的函数执行后拥有了 next 函数,也就是说函数执行后返回了一个对象。每次调用 next 函数可以继续执行被暂停的代码。以下是 Generator 函数的简单实现

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// js代码

// cb 也就是编译过的 test 函数
function generator(cb) {
return (function() {
var object = {
next: 0,
stop: function() {}
};

return {
next: function() {
var ret = cb(object);
if (ret === undefined) return { value: undefined, done: true };
return {
value: ret,
done: false
};
}
};
})();
}
// 如果你使用 babel 编译后可以发现 test 函数变成了这样
function test() {
var a;
return generator(function(_context) {
while (1) {
switch ((_context.prev = _context.next)) {
// 可以发现通过 yield 将代码分割成几块
// 每次执行 next 函数就执行一块代码
// 并且表明下次需要执行哪块代码
case 0:
a = 1 + 2;
_context.next = 4;
return 2;
case 4:
_context.next = 6;
return 3;
// 执行完毕
case 6:
case "end":
return _context.stop();
}
}
});
}
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/95/index.html b/en/page/95/index.html new file mode 100644 index 0000000000..fd345ab505 --- /dev/null +++ b/en/page/95/index.html @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Promise 实现

PromiseES6 新增的语法,解决了回调地狱的问题。

+

可以把 Promise 看成一个状态机。初始是 pending 状态,可以通过函数 resolvereject ,将状态转变为 resolved 或者 rejected 状态,状态一旦改变就不能再次变化

+

then 函数会返回一个 Promise 实例,并且该返回值是一个新的实例而不是之前的实例。因为 Promise 规范规定除了 pending 状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个 then 调用就失去意义了。

+

对于 then 来说,本质上可以把它看成是 flatMap

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// js代码

// 三种状态
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
// promise 接收一个函数参数,该函数会立即执行
function MyPromise(fn) {
let _this = this;
_this.currentState = PENDING;
_this.value = undefined;
// 用于保存 then 中的回调,只有当 promise
// 状态为 pending 时才会缓存,并且每个实例至多缓存一个
_this.resolvedCallbacks = [];
_this.rejectedCallbacks = [];

_this.resolve = function (value) {
if (value instanceof MyPromise) {
// 如果 value 是个 Promise,递归执行
return value.then(_this.resolve, _this.reject)
}
setTimeout(() => { // 异步执行,保证执行顺序
if (_this.currentState === PENDING) {
_this.currentState = RESOLVED;
_this.value = value;
_this.resolvedCallbacks.forEach(cb => cb());
}
})
};

_this.reject = function (reason) {
setTimeout(() => { // 异步执行,保证执行顺序
if (_this.currentState === PENDING) {
_this.currentState = REJECTED;
_this.value = reason;
_this.rejectedCallbacks.forEach(cb => cb());
}
})
}
// 用于解决以下问题
// new Promise(() => throw Error('error))
try {
fn(_this.resolve, _this.reject);
} catch (e) {
_this.reject(e);
}
}

MyPromise.prototype.then = function (onResolved, onRejected) {
var self = this;
// 规范 2.2.7,then 必须返回一个新的 promise
var promise2;
// 规范 2.2.onResolved 和 onRejected 都为可选参数
// 如果类型不是函数需要忽略,同时也实现了透传
// Promise.resolve(4).then().then((value) => console.log(value))
onResolved = typeof onResolved === 'function' ? onResolved : v => v;
onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;

if (self.currentState === RESOLVED) {
return (promise2 = new MyPromise(function (resolve, reject) {
// 规范 2.2.4,保证 onFulfilled,onRjected 异步执行
// 所以用了 setTimeout 包裹下
setTimeout(function () {
try {
var x = onResolved(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}

if (self.currentState === REJECTED) {
return (promise2 = new MyPromise(function (resolve, reject) {
setTimeout(function () {
// 异步执行onRejected
try {
var x = onRejected(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}

if (self.currentState === PENDING) {
return (promise2 = new MyPromise(function (resolve, reject) {
self.resolvedCallbacks.push(function () {
// 考虑到可能会有报错,所以使用 try/catch 包裹
try {
var x = onResolved(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});

self.rejectedCallbacks.push(function () {
try {
var x = onRejected(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});
}));
}
};
// 规范 2.3
function resolutionProcedure(promise2, x, resolve, reject) {
// 规范 2.3.1,x 不能和 promise2 相同,避免循环引用
if (promise2 === x) {
return reject(new TypeError("Error"));
}
// 规范 2.3.2
// 如果 x 为 Promise,状态为 pending 需要继续等待否则执行
if (x instanceof MyPromise) {
if (x.currentState === PENDING) {
x.then(function (value) {
// 再次调用该函数是为了确认 x resolve 的
// 参数是什么类型,如果是基本类型就再次 resolve
// 把值传给下个 then
resolutionProcedure(promise2, value, resolve, reject);
}, reject);
} else {
x.then(resolve, reject);
}
return;
}
// 规范 2.3.3.3.3
// reject 或者 resolve 其中一个执行过得话,忽略其他的
let called = false;
// 规范 2.3.3,判断 x 是否为对象或者函数
if (x !== null && (typeof x === "object" || typeof x === "function")) {
// 规范 2.3.3.2,如果不能取出 then,就 reject
try {
// 规范 2.3.3.1
let then = x.then;
// 如果 then 是函数,调用 x.then
if (typeof then === "function") {
// 规范 2.3.3.3
then.call(
x,
y => {
if (called) return;
called = true;
// 规范 2.3.3.3.1
resolutionProcedure(promise2, y, resolve, reject);
},
e => {
if (called) return;
called = true;
reject(e);
}
);
} else {
// 规范 2.3.3.4
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
// 规范 2.3.4,x 为基本类型
resolve(x);
}
}
+

以上就是根据 Promise / A+ 规范来实现的代码,可以通过 promises-aplus-tests 的完整测试

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/96/index.html b/en/page/96/index.html new file mode 100644 index 0000000000..157bdb8661 --- /dev/null +++ b/en/page/96/index.html @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

call, apply, bind 区别

首先说下前两者的异同。
相同: callapply 都是为了解决改变 this 的指向。
不同:传参的方式不同,除了第一个参数外, call 可以接收一个参数列表apply 只接受一个参数数组

+
1
2
3
4
5
6
7
8
9
10
11
// js代码
let anObj = {
value: 1
}
function getValue(name, age) {
console.log(name)
console.log(age)
console.log(this.value)
}
getValue.call(anObj, 'lixuguang', '31')
getValue.apply(anObj, ['lixuguang', '31'])
+

模拟实现 callapply

可以从以下几点来考虑如何实现

+
    +
  • 不传入第一个参数,那么默认为 window
  • +
  • 改变了 this 指向,让新的对象可以执行该函数。那么思路是否可以变成给新的对象添加一个函数,然后在执行完以后删除?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    Function.prototype.myCall = function (context) {
    var context = context || window // 有入参用入参,没有给 window
    context.fn = this // 给 context 添加一个属性,getValue.call(anObj, 'lixuguang', '31') => anObj.fn = getValue
    var args = [...arguments].slice(1) // 将 context 后面的参数取出来
    var result = context.fn(...args) // getValue.call(anObj, 'lixuguang', '31') => anObj.fn('yck', '24')
    delete context.fn // 删除 fn
    return result
    }
    +以上就是 call 的思路,apply 的实现也类似
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    Function.prototype.Apply = function (context) {
    var context = context || window // 有入参用入参,没有给 window
    context.fn = this // 给 context 添加一个属性,getValue.call(anObj, 'lixuguang', '31') => anObj.fn = getValue
    var args = arguments[1] // 将 context 后面的参数取出来
    var result = args?context.fn(...args):context.fn() // getValue.call(anObj, ['lixuguang', '31']) => anObj.fn('yck', '24')
    delete context.fn // 删除 fn
    return result
    }
    +bind 和其他两个方法作用也是一致的,只是该方法会返回一个函数。并且我们可以通过 bind 实现柯里化。
  • +
+

同样的,也来模拟实现下 bind

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码

Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
var _this = this
var args = [...arguments].slice(1)

return function F() { // 返回一个函数,这是 `bind` 和 `call` , `apply` 的区别
if (this instanceof F) { // 因为返回了一个函数,我们可以 new F(),所以需要判断
return new _this(...args, ...arguments)
}
return _this.apply(context, args.concat(...arguments))
}
}
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/97/index.html b/en/page/97/index.html new file mode 100644 index 0000000000..92bd975678 --- /dev/null +++ b/en/page/97/index.html @@ -0,0 +1,510 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

继承

在 ES5 中,我们可以使用如下方式解决继承的问题

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码
function Super() {}
Super.prototype.getNumber = function() {
return 1
}

function Sub() {}
let s = new Sub()
Sub.prototype = Object.create(Super.prototype, {
constructor: {
value: Sub,
enumerable: false,
writable: true,
configurable: true
}
})
+

以上继承实现思路就是将子类的原型设置为父类的原型
ES6 中,我们可以通过 class 语法轻松解决这个问题

+
1
2
3
4
5
6
7
8
9
// js代码

class MyDate extends Date {
test() {
return this.getTime()
}
}
let myDate = new MyDate()
myDate.test()
+

但是 ES6 不是所有浏览器都兼容,所以我们需要使用 Babel 来编译这段代码。

+

如果你使用编译过得代码调用 myDate.test() 你会惊奇地发现出现了报错

+

因为在 JS 底层有限制,如果不是由 Date 构造出来的实例的话,是不能调用 Date 里的函数的。所以这也侧面的说明了:ES6 中的 class 继承与 ES5 中的一般继承写法是不同的

+

既然底层限制了实例必须由 Date 构造出来,那么我们可以改变下思路实现继承

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

function MyData() {

}
MyData.prototype.test = function () {
return this.getTime()
}
let d = new Date() // 父类实例
Object.setPrototypeOf(d, MyData.prototype)
Object.setPrototypeOf(MyData.prototype, Date.prototype)
+

以上继承实现思路:先创建父类实例 => 改变实例原先的 __proto__ 转而连接到子类的 prototype => 子类的 prototype__proto__ 改为父类的 prototype

+

通过以上方法实现的继承就可以完美解决 JS 底层的这个限制。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/98/index.html b/en/page/98/index.html new file mode 100644 index 0000000000..c6cc460be9 --- /dev/null +++ b/en/page/98/index.html @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

函数防抖和节流

在前端开发的过程中,我们经常会需要绑定一些持续触发的事件,如 resize、scroll、mousemove 等等,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。

+

通常这种情况下我们怎么去解决的呢?一般来讲,防抖和节流是比较好的解决方案。

+

防抖

你是否在日常开发中遇到一个问题,在滚动事件中需要做个复杂计算或者实现一个按钮的防二次点击操作。

+

这些需求都可以通过函数防抖动来实现。尤其是第一个需求,如果在频繁的事件回调中做复杂计算,很有可能导致页面卡顿,不如将多次计算合并为一次计算,只在一个精确点做操作。

+

PS:防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的情况下只会调用一次,而节流的 情况会每隔一定时间(参数wait)调用函数。

+

我们先来看一个袖珍版的防抖理解一下防抖的实现:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
// 缓存一个定时器id
let timer = 0
// 这里返回的函数是每次用户实际调用的防抖函数
// 如果已经设定过定时器了就清空上一次的定时器
// 开始一个新的定时器,延迟执行用户传入的方法
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
// 不难看出如果用户调用该函数的间隔小于wait的情况下,上一次的时间还未到就被清除了,并不会执行函数
+

这是一个简单版的防抖,但是有缺陷,这个防抖只能在最后调用。一般的防抖会有immediate选项,表示是否立即调用。这两者的区别,举个栗子来说:

+
    +
  • 例如在搜索引擎搜索问题的时候,我们当然是希望用户输入完最后一个字才调用查询接口,这个时候适用延迟执行的防抖函数,它总是在一连串(间隔小于wait的)函数触发之后调用。
  • +
  • 例如用户给某系统表单点提交的时候,我们希望用户点第一下的时候就去调用接口,这个情况适用立即执行的防抖函数,它总是在第一次调用,并且下一次调用必须与前一次调用的时间间隔大于wait才会触发。
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// js代码

// 这个是用来获取当前时间戳的
function now() {
return +new Date()
}
/**
* 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {boolean} immediate 设置为ture时,是否立即调用函数
* @return {function} 返回客户调用函数
*/
function debounce (func, wait = 50, immediate = true) {
let timer, context, args

// 延迟执行函数
const later = () => setTimeout(() => {
// 延迟函数执行完毕,清空缓存的定时器序号
timer = null
// 延迟执行的情况下,函数会在延迟函数中执行
// 使用到之前缓存的参数和上下文
if (!immediate) {
func.apply(context, args)
context = args = null
}
}, wait)

// 这里返回的函数是每次实际调用的函数
return function(...params) {
// 如果没有创建延迟执行函数(later),就创建一个
if (!timer) {
timer = later()
// 如果是立即执行,调用函数
// 否则缓存参数和调用上下文
if (immediate) {
func.apply(this, params)
} else {
context = this
args = params
}
// 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
// 这样做延迟函数会重新计时
} else {
clearTimeout(timer)
timer = later()
}
}
}
+

整体函数实现的不难,总结一下。

+
    +
  • 对于按钮防点击来说的实现:如果函数是立即执行的,就立即调用,如果函数是延迟执行的,就缓存上下文和参数,放到延迟函数中去执行。一旦我开始一个定时器,只要我定时器还在,你每次点击我都重新计时。一旦你点累了,定时器时间到,定时器重置为 null,就可以再次点击了。
  • +
  • 对于延时执行函数来说的实现:清除定时器ID,如果是延迟调用就调用函数
  • +
+

节流

防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// js代码

/**
* underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {object} options 如果想忽略开始函数的的调用,传入{leading: false}。
* 如果想忽略结尾函数的调用,传入{trailing: false}
* 两者不能共存,否则函数不能执行
* @return {function} 返回客户调用函数
*/
_.throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
// 之前的时间戳
var previous = 0;
// 如果 options 没传则设为空对象
if (!options) options = {};
// 定时器回调函数
var later = function() {
// 如果设置了 leading,就将 previous 设为 0
// 用于下面函数的第一个 if 判断
previous = options.leading === false ? 0 : _.now();
// 置空一是为了防止内存泄漏,二是为了下面的定时器判断
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
// 获得当前时间戳
var now = _.now();
// 首次进入前者肯定为 true
// 如果需要第一次不执行函数
// 就将上次时间戳设为当前的
// 这样在接下来计算 remaining 的值时会大于0
if (!previous && options.leading === false) previous = now;
// 计算剩余时间
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果当前调用已经大于上次调用时间 + wait
// 或者用户手动调了时间
// 如果设置了 trailing,只会进入这个条件
// 如果没有设置 leading,那么第一次会进入这个条件
// 还有一点,你可能会觉得开启了定时器那么应该不会进入这个 if 条件了
// 其实还是会进入的,因为定时器的延时
// 并不是准确的时间,很可能你设置了2秒
// 但是他需要2.2秒才触发,这时候就会进入这个条件
if (remaining <= 0 || remaining > wait) {
// 如果存在定时器就清理掉否则会调用二次回调
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 判断是否设置了定时器和 trailing
// 没有的话就开启一个定时器
// 并且不能不能同时设置 leading 和 trailing
timeout = setTimeout(later, remaining);
}
return result;
};
};
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/page/99/index.html b/en/page/99/index.html new file mode 100644 index 0000000000..4eb2d79a3b --- /dev/null +++ b/en/page/99/index.html @@ -0,0 +1,514 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

模块化

在有 Babel 的情况下,我们可以直接使用 ES6 的模块化

+
1
2
3
4
5
6
7
8
9
10
// js代码

// file a.js
export function a() {}
export function b() {}
// file b.js
export default function() {}

import {a, b} from './a.js'
import XXX from './b.js'
+

CommonJS

CommonJsNode 独有的规范,浏览器中使用就需要用到 Browserify 解析了。

+
1
2
3
4
5
6
7
8
9
10
11
12
// js代码

// a.js
module.exports = {
a: 1
}
// or
exports.a = 1

// b.js
var module = require('./a.js')
module.a // -> log 1
+

在上述代码中, module.exportsexports 很容易混淆,让我们来看看大致内部实现

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// js代码

var module = require('./a.js')
module.a
// 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {
a: 1
}
// 基本实现
var module = {
exports: {} // exports 就是个空对象
}
// 这个是为什么 exports 和 module.exports 用法相似的原因
var exports = module.exports
var load = function (module) {
// 导出的东西
var a = 1
module.exports = a
return module.exports
};
+

再来说说 module.exports 和 exports,用法其实是相似的,但是不能对 exports 直接赋值,不会有任何效果。

+

对于 CommonJS 和 ES6 中的模块化的两者区别是:

+
    +
  • 前者支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
  • +
  • 前者是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
  • +
  • 前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是后者采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
  • +
  • 后者会编译成 require/exports 来执行的
  • +
+

AMD

AMD 是由 RequireJS 提出的

+
1
2
3
4
5
6
7
8
9
10
11
12
13
// js代码

// AMD
define(['./a', './b'], function(a, b) {
a.do()
b.do()
})
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
var b = require('./b')
b.doSomething()
})
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/en/tags/index.html b/en/tags/index.html new file mode 100644 index 0000000000..876bf9d3ec --- /dev/null +++ b/en/tags/index.html @@ -0,0 +1,470 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 | 李旭光的成长博客 + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + + + + + + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/algolia_logo.svg b/images/algolia_logo.svg new file mode 100644 index 0000000000..470242341d --- /dev/null +++ b/images/algolia_logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/images/apple-touch-icon-next.png b/images/apple-touch-icon-next.png new file mode 100644 index 0000000000000000000000000000000000000000..86a0d1d33bc2ae8a0416ebba67d1bbb60aa29c38 GIT binary patch literal 1544 zcmV+j2KV`iP)Px#OHfQyMIs_1FE20E)zv2_C(h2!$jHdT!ovUm|GT@p zWo2de_xJq#{8d#|IyyQ>M@Qx533`J9dBE>4W{r{KFjMLpp4N$2iMRJCGfPM7@le{)BPzVgz zp?ByVdWYVj|4-=tUS5SkR}Te{-Fr#UrxsqJ9+ed6YY9KmoXq6n^B9DRMwF@T2u)qH z!=wh-sAdPH_8fFy0-I1d68Zp);!FTtdweUZc2nr)?5^hh99w*FOU z@NYFT^!v%iPrGly0Q*+sLVtesYh+the|AET4WK8lR^dv1IZ-3FhAuiK5V|-?gf2#j z&_yW`x^K3G&OKl4kP$$ih@p!?B6M+)2(4Rz(6$=G6WT(;6uP)cgkEi(xI(Yi7`D(w zo(Y6r>FyarFV`4e&^@lk@PfWf|DSeM5}?agV`>aDXt-c%3^!*Ij0g0Q8X5*mjbRC$9oB0MOK51O_53LU8p`RzcozW; zx3wC>721@58xhdzx?E$}LW9xpjUu4UhOZO>Z8Us+D+1bR_}&sQXJ}B}yadb|8fraT zV|YUw4WF(>KpPF;mjp}*eTo5XG<+u!&}PFoihwq!uhI`XPYZ1{e1FDs0nqR?Ym5YF zqv0DwK${KUMFjNpqb>9(0vg8Y^d$otmiJc}L#s|8wD~Z~fHtQu8PG6DgwAq-&}PFY z1KMo(WI!7YpAcwxN`zJiV(2pgG_<|npE+y^Z8UsBppAx42(;Po$$&N*J{izP!?y+W zi2YsL=fkK`KXsymhWcmL#_y-S*Y&V_u#us^S9pDvbMSkxv7uidoc36Qa`CQ+5AC^d z=CTJ%0ZkrQ=v?t(E_)>OCL8YaU+nX$$--iAUNb;m^jBG2!5*5enU|exmY3aQcv=O- znjO}ft-yw2|VZs2g-W`Epf@n*5gsJ2LLtC^nFlq8+?%Xc!{Z3_|hFI~!oeU$ep z%4jE9Y9n1{ah68@W{++)8|fc~HIM4)QycHpg}3P<_K$M$L~}7jBXs?6bDwjvo+I;D zsoi}398La6+~*osM@45l#ptQ6MrZFz597*no3v)ffsUEletxWeW~`A+ntYvaeQj?e z*kSqGXZsrrhZ>IsxH$({IR{z11zkG`GR^j$#{%z41lp5(Ne#glJc5nj-BUaKR!X?# zzN6F-<0?&z;|30?9q0Z}oHG*G)Q)s%Db~RbjMU)mp-u&Dmm0zkF~o9eRh}p?ByVnjn4wmEZ*c=^{+H00012dQ@0+Qek%>aB^>E zX>4U6ba`-PAZc)PV*mhnoa6Eg2ys>@D9TUE%t_@^00ScnE@KN5BNI!L6ay0=M1VBI uWCJ6!R3OXP)X2ol#2my2%YaCrN-hBE7ZG&wLN%2D0000`v literal 0 HcmV?d00001 diff --git a/images/avatar.gif b/images/avatar.gif new file mode 100644 index 0000000000000000000000000000000000000000..28411fd0eadae59f521f6914983f61731b759e77 GIT binary patch literal 1793 zcmc(e`#al*0*AjzgfxoGy{05C>!#LYnzk<02ECx3bGphio%U$lDm65-%4+M{#3i&v zF)dm;w^A*|s?pmzaZMtK$W?-bB(5QZNKm$&f8xA9e13kP=kvU$gHL&RM+pD{pap>K z?d^{rKQig`f&P9LlQ}99ZEbD!GZ=ckeqnxIr_;%&rX*sqfX7oR6eGjKTn=YwU|>ul znHV4Eu-TiNo08GdRgGqCZH+xRs9IQ9RH^1>XE!!BhKGj6$Hs&L!K6&KzP>J#N(Fqr zR;wLkv2^R}tE;Qa%ge0(e$C3t(&C~Rwe+~VRQXLwjF5-F8RA)i0YW~)@HafxJI ztDT*l9U2_uak;ZIGkh*rIx(SEs}~j)c6N3ceSN_H8_49XM%yH8dPZgz zB|GQ#ox8bt`S%J6i+(M>|DdF_?BS#GipRfIR#n&3KB=pJ+VJdoW7CV~me${2w!NaZ zzkc)f-TOa2{MkY4?CS36?V~f8{j7n(A@(q5gv;X#grZTgWNdswDw~{=E0ohSvvWmY zGjmfr8#E|v4TQF_Gc`8@&-08hUU*RDV_F8x1dNvf-Q-&Xu=7I9zp8rf8bT6VFu-K1 zp`BZ_y|*WcV(5^u3r8Z?_Lqctc&JNK!cAN~et3&LSM%8m)RAlO)U=qKp>M&~miA0b zXA%^BkIE!eHSh{;JcHumYCCz&Hv4Tw58W8&TJUDzpa4EMr)`^Hk&pLH2#~xp+Q){T zG-#gq>4cV;j$FAa0T2e?IaPM3FG2TOT?&y331G&9%UI%3h^7{jcjWlZnLQlvaK26P z&Du^Nq0124n0#&HRTS@~)vg(wn2`PSjR|i?^*UZ1csS?N{1geB^pd#*wq=(WWX@1r zv5uJro0nX~&}D6Yd!Wu)zT+Ue>E{=z;@y4ScOiE3p8m&MiDr4no6=S6wRJ0iXM)6C z>5pRjYy47cB%WEd*@}L z)G5pbE$Ge-JTxqUc)`~&KYX(KG|XUNFEaMuwR^Y2g@rcImI_#CkxM7>ixb}G;9?cd z^^|X7xcI>?8NVEgi-@HW4-}Ub7>j!l$Wovi5owxWR~&d9OE??vXD<~+R;ZIoXph72 zS^L1LPNaHdal;oq)mV?S`yd78YGomCf{IVUjRFlwBDq0OKa}J>R-c=kw&xj^j*rW0 zJokM-@lH<&)bi_qsQp$(*Ii?w!M$!>g`Nv;bl`;yk4f(}y&Kco3r_*64>%zjTOCfdB@J*VqOvb=oZa>C@u2Lduq1`izsNs8+QAq_ECS490Gm#@C{iK=v!G1IsXpV z76Y`Dp+T;=aPS1u7-^6eAPsE=Fh7@&a6*rEQ0-lhxp>WU>m<>B4%df*W$pD4< zwi<>)ym@Zqoa-)!J73I;#~fq9mAj64U9-2aXsJvo1GRX7EFSwyF_W3m*mQ`Z=p-3( z>GXe!z@|YJ9AMi5`@}EICbT2b$BaPO()mC;$h}Hdi}&W0Cv1_IH70cDzqaN2JSbZ{ z@pl2olL|1mz}I#g;^OjPiAXY4L|Uj@Cb#<|4jBr+K^v+&jNRfP(-(RnH>Uj+<-koQ zK-&m;?TK*D0A<8f;Q`Fsn4l2KVgu@{NK)X|`%2r)S=kN5!CVj+xSEq1+Eb$0bDoB-XOPkoZY08jeqI*A;y_?flt= zh0FOL^(F`4ls>(Et~Gpqs*@n|@qZg1LiHe{HoHzns0oD{ebkq7Snv{|$@>0t7h-12 zVdLd19AD|Povy%@~K1a0273z1LoQ&bj9KGy7*A@EAY_0)auKWMD9uoSck; z3PMdqNlC>_&u|68&ceyT&cepV#e0*Vi(8n7jZHurDl94{DJjXxFRLgcu5eRAQXEK5 zPEJKh#Y9cbBz~Rky7>R~>rWSejtn>q93}>G1BmE=#B{(vy})7s07we_`vUyy1tcOS zAq9cS$SEjk0YpGzVj>b^Qc@BU!Z*VS*8n7Rr1aOtR6q>+c3^H_h5 z6W@6y?EPZMDXv~)VrJpH!7l(6l$5$HEh8&;_a015T?39VG%_|ZH8ZzxaCCBZadmU| z4+snjei#xO`}j%R)A$5rMrKxa&a>P+RB;KWw5+_MvZ|r6skx=Kt-YhWr?;e~9o=GON9!Qs*I$?1=?b0C2DpKu62{|V@S#6w4jhlqrPm;`(o z50EIBa1hgxkX{!9(W~f#?R*)y#iPj}s_8H4-;wi37<^~6_nV-&$}9PWZ~ro+zY+ca z3n=FQDWd-b^q+YC%mJu~frJSorUNJePOWhIC}=#?O<)8da>KO`0&q^kIp;)CO`rtO zL^&Br<3tm1lmO5T>e@$4kF-DlEG!Tf0D)p8i3r5NIqGT#Qh-SxCIaBQDNx!6xFOI7 z(Ie8=R1*LSlyYZ8Q3w=A0W~$CuCy!&L%4D`66&i=u89(ZBLVuF8Zsr4WQK4RN#|&! z00zDCGNa(M?RtqBysY65wAvXFqIEC3Q}uEkLl76<{L zYcUc4CEOkWQ(6L{g1Qz*lZrs{(4?5+oangA+X2Ejb#W<800W#O4JIOxbm@hg16|I+ zI_tv$lkD#3GZcv#rdH>4`TpY?{Dx(7_S5gZntuRX9=UBlz7!8g2%9u&#Fo(f0TkRZ z0axN?vefqnG(%K>BbDDS`M3V^C~o{w{-rvjT52Y37IWY)ckR; z<(cRRYk;GV`j&|t62u2-!=MRw5?m@th9Ha$g#?T+;?E?r^a#U@6aZ@TLG(n6kye*u zh!j&MBZQ*IRg4rM61@EKzmLm5GQb&(17U>dG~t@cq#}GG0Ej-}4a8tQib70%MMdTh zU}s;s2NHaIPBAd`S|77HxEmPEB=qZgwCqux*z|>TmBfu__7o|}LdU-Un%R0-quKF!3TKBQ2mB!x$%6wJa*`T&uE>8%&t+eIl{j|7S?0kkVuwwDko1N#e z5`n`hmSeI->h|RvA^c$x0}3$S{sjQH6ut0 zg%}h~k=0o~Ar{Si1;5N_prng;NvQyl>bS6iLZ5J})E=Tj12(tZ+6ZhvK z2d2TY2GX2kad$d@&eXewj>KU(cFd<%i8tc+xzE>hrKqUseL~8OT*}tuvygNR0>C4E z7$Ga*${^PfLZYB9)27Kl|8fZ_7yq-ui6xzp0)P=_!Wv5a_uHI*B^m}zTAWdEnNpXD zcDXu508FKQgiK>9iN=rXezCFJr(D*+vAK0z znm^}1-1d;l>l!1TlxlKIdKD5LTruc<%bUYs5#DF$OUDDsWlyF011P;NRCxc+Z1qbJ zp3RFS+)a%3g8s^Mda9DJIS0OkeZ@`-^b!!r^9cgbT}__*t`}vxTw?G?hwPV=N$yiQ{wEo`k1(b95mAkMm?Ntsz zr=pa6e2A*J8Wioa*ZSG@eYmgCWqyh|g^mF7SywReICa%u9&10@QN+s|tyHCI~Lyq;iK>lwnQ##%`z zcAF^sTbh0f?xl;AdIWo}JzSOfMeWE_1wJ@N8;Oa&(|Z@SWw;?z283(E0DKUALMr_C zpoH;VX45}Kh{UMr-&YLuh;aHkU1)oQ2^bNDg#aOmkeAmCqLoQ`8U|S9X&FIqUI4gV zvJ({2FD^6`!T<1t%;A~Z%j~jzCl|7}+^fNgW39SYL@Sc7nR6_8uJz-f{w zGVCijBYYG*{aViB<4txSKJdl-(bS_%I%&xhRD%_ zUlO6+9NGK0dnBWX%D1%`|qp&!fk_z+ZCF zYACIhL>L{@m;0&Xu zX>IKIjWZnc{b6thdQu}{EKtgAMa>q-mJ~aSOzKtXC;KHg5eDdCzV8cP`vVA|jcArD zxuvIZgqch7JroKTXhVLz^7idCR$Z>;s$FanxSVY8OOzyQz^L*2-)gXHdtk#CJL2Zs z^-J=}=(3|RWA;eI@gmDZwaQ>3jZ(g3zCoD2xp#Y>>6v zJ)0?YyY}qbZLwhovtQk2LegBm(Pu`$mtYG)uHBT6N1$iJ1A4AR2m~=`3*ibRz=%sI z@xKlWr~n$IPbgKFhk?NaVRQS>E(gg1G&N-vM_oQ zEm;@`U)#UcM_INdZi($pSNN#068~jQk(|FG*?&Gmclu34Tiza@8~gKx)GZ5^i`KxJ zZU60SUO$IYOvL@`!vrn@Gg%$iQgN+Gp2XIVLwMw$A4-wu{sGW&g#}kGKsVl&{{h%k zrN5Q;^Ti+axkhn(-JPo**wKbb6smCic4Y!5aav8j-XM0rJs#O1HwnDQ!$t+fH&aSd zbO}DQ`_wE6%lQC$aGES@w=i!Ci3-V2VqDOs;!C^uo?nP;mJ7Ms>l9p6OUl>RHQz;< zi4Qeez(@-FRva>I{H%tb4C-GPafP9(9M4j%41Hf!fWMbJlAgT2(r@nntxwv|fnDXd zV2yA7PvdCM)qLVugE>*6S333P-uZ+u z?Q9v46LZ0>Kx{%Z1HYh|4r*LIf*CwTdrBmeH_T>#VR~-f+4_l@x-045&z&8W7I{^W z-X->_Q^uVES@0h~9qh602AuArxR>Jd`g5c7h28;=Hn*aR%V zS)ci^CoJcWQaC16?9LUeg(ReRr!--&;x~%i0oDA&J$n_U$@)6V&JS$5F=lV$WAi5; zdk(nQt-CF_8X0VmTu9b&_i*&Co=Wj#C#ZdT{;63%tuaGpJ!3|X-80$OxAWK`m3h*`cNj9SslQR=k(JZVYe2P&?wF&BGY*>kojeQVmto96;`>DA+w4C4t-s zq=x9ff0I#jXFZZ4=|_MPUnJt|MZb(pu zBv{{OK(WRfBU%%-%g&b+C3|ktv~Qqmzy}LZh%+m2T%{MJ;}Oi>F0ZJ4Wi-LU-TMe* zdP`C`IYv~`17B*cwOR?LZ&_)MQeeQPN9x~&TA^HdQJZ4?sr=_ZO?ss-v`Be^=x+mw zi|dXz7@xk5H9;4BaI%#hD}@T770140)m_WlyW<8HWR)SFZw_N~Y#@5L{KnyXQGCQm zXe+&z!v_a<8jnrv6$5QeZREoRQJ(!;wVEzm=5O=z0LaXm@)++?d9Y*@f*QgC_)7o~ z76hvFKT4n%gun&<=MG9>TLgB7CM~^WNKIKyGhS61>;g;~gWPKVzj&k$#*k7&JY0^j;C<5v*;ERL45mSh#wq@wIs>vvIgD1eKckqUya% z*aE8)IN7ivy9FkyDzxk{n-ngXuzn>I52}@=qQdjV^~b)bCT=#v5=*ZZnH zCdU4{WQtI-;_s^g6)X#_`W82Xu9>(S>ix9&Wy%Z0TEQht#0I5C0BJuW((c4b; zVUtr0Vs~%I?rL)kfLgd_CO?$Kj1pze*LrNG#)%KzPAPS{552NT0hc;V$+4G)Z{&PT zKULFRTYB^2yPV)=%O8Mq^zOvWg#}%E=XbdXsWDSu_L$_cSGWSz3O&FW`$I1)Mwq4JVTq7b0H+*z9lzay|lK4QovlO&o_)E*9X z{QByHAU#o1{#3{C%%C;5y$y_27XF&@-p4X`PW|cCrCV}-94;HE=DM%-69w_3%REdgnfMFU)|IFduVf%k z+1DBQmH51XSrTmj*R*nCrj4T!-*@d}hQY<--N=ugM@)F18IrNQ&a#EMs<&N8it_8*{2UGj3VDuyHiG*k!Pg#OS`;OA?cep^{rxyU)$+ z?}263OU-@@>T|lM39uQ(-?K7$4`mFmg4u-X5oZ~8El0f$C?`EF?q+RCt|3*59?<~u zvkNc?ogvi1rr1>{3hr2AB5A)Nr=YRO6xV}t9@$D)Q-$E(X-Y_nGOu%6WiiESO7r#w zw8>apB7i4iwKfVYZ|CF>@ZN29;$ruS==5CvCX8MOn}KN@*xOAA3#mgxgrK zi*B%M{Y}yj-w=Pk2A;({wWWK=kDPZW59;!T7wMfw!HOCq;o$F$7S^ix?slqe)N6d7h|a{r3IqcMXXIDqVE zi|pMr4OSB6=UDW$PC5@C&N;B0>w(a+=-aG~^ z!-!WMPDUwbJBZp|PFSx$OQBqleRy!~*dUxz-C zqy&L%n4Jbb>i4iN;K#EPzD0aS@na*v0z#K^v-4m8P4wD9-=LMli~?BzVN16#bP#Z0 zGu8b(BD1*d-s>phH~M1?8~U`zLt&oz$9l;%A;a6~I-ZGLZZotoyK*1zsaSt`z;pG` zBBn&tyD6@mWo4f(jwsW#^jXu+so5Bw?sEH1ZKt8?tIUUFHhKI+wVKXV>bb9jhoqi| zzreh^=O<%z1V)v9zzd&W#lBYYU?t)8J|;#^rCK)gspm2ks>^`d#lBQdCB*G_pVElC zHU97mKfCgbxT=*i+co>n>Kp%Yky{SFLu~nn7V(20`$EE-3rp=)XnIrqU;(IzW*SQ!G$q66?hK<^xCs*z$Zo9De*2og1S6a&47u+8^?^4Z&!eXVo@L~DY9B23VT^wM z?sNcnQej&DVH-TH@Ad}syPD`PABopCj8`%XBk$lhzJK-B9ld?Bt?l-+84uTbdo6x2 zFWQhd(k7AuA8VByFZk(JtBTd;xP!4?>DwiRi|mL`_q@s5?A7;`%GV6eIQqf{zhZCV z*6zNbK9YLD8fO5uSpL$48tUos5x?`@=F^ja6Ny*-3cZwEdsjdWZZxNy;bes$SM2hW zOp|{+*Y434Og;8APLZ2w8I)kImA*e|VriaLAs?+MH=*NA-zIZ-Z$Fqf+}D^@VY*yW|bwf`dJnm=uEd0LBzm0lf;~YN~N*hLWfc_a&DjAhr!Y)cJVoyZKcR3u2 zKQ`1}^5kYB=&cENQ;WeY` zk@q6Q7r2yG4t$PZK{FmcnfB(eEH`>W$zB1rI_}y%uB)x{e1R9SSz5cR_IZ`#>5h%q z%3G?rnlIPq(ywGNW&0k6<_B)NTf=ls+;nKX2lqem2i$wIU_|dbBefMt6R5$3D`5R5 zlm>#HyrIz+JQ*Hp?oDF+*g@ajT=$6IqkPWllg0YS;^cKl{eu=+8IITCLUPTDRSnWf z;rY$dG!3uhj;a{8al?NA0xMfqrBl-cj!sTm`?UNWTcg!Gwr@M3o+524h%5o^F@P`rez*Keiq=$cmqlrsyERRpgkN7T3qF#M+YC*8~)8Ca~i`k2= zdL*aN*X<#E_1e2XfHw(M(&QfHMBS9GhRnQb?VLJ-MutH`s6MwqcB!;oyol+=9C9q| zr!8X%UgZ2RL0xj!(lCQkW3RaJ&fS7|96*}W%ETx_GSHBH={Y2QrKJy__q2)snjA-w~@z_!CECH zMkuc97Y)owP#c0NEJwVg?g2;ZxRu7Pf3Q7aO!uXERkmel^Kq#sH0?!tN&8a|TR$Ha z_UgjKW!U0!d|W)A%+K!<3_p@s+>_=yXdW&NxSgD(fV6Mr3Fn&BibpaoSoAkWIzPU9 zUD?T*Gl7Y7wB;za`bXT*Ez~+ItE#!_kn(|s(dU=EU)0?dldwV3yvZdVWM{ci@+a%{ z-;OPX{Pb_yQBSUU7VQ}33NITz2;`j+q|s>4N*mb175{`KNu2o%Q1QGpB0e)nmh4?k zFF3&u1|Q~f>@cuhWGk2s4p6#0`UBXTm%8y{`KM;&!gI@n)zldE_Y%iaSD!CeGz&Gi zS_>-UIMcvGia*1Q!@!+Sc2^e0rR&cgda$cpsr$$_6^zF!zjH}sSD3XHDxciL7CLx4 ziH&yJXRg^|lPY|pm(;7gWOy9Wvv=)~1*sD5(xrL16JK!PuIZA4;69s=aSUYcu3;4kPB%3=}Nib(<3~Q(IVyMEMNQSOgYhH!xxK6b| zqx*I!lg#^)Q$*+^QTXj4Z| zFEQMIz})2r0^bk-HWAuR@#b zrfdef{96=}zEDn6E?fKPo@L5Y6MAYN=hsaSnIJq~0>S2W!wqWY20QDFRC;s1b)qDI{+^7zoEl7gf;iH4}+!Z@=a zJH2Kl#64vA+`9kS{w>(+Tw;=zE%%JNMSC8Ki2Rw7N_`6CD1H|T8^+SE4By2b1DL@&bl{U=wEh;{P~zpgc-01$x<`e%SaB|l{V zzx~bK3*(T}__eyy&bG!FW)tQ&Biw^eGAHZV2i#Y_1w7wLZ1>i;wLZ8;7Vjf5&n|>_ zGBW0I-`FyLMnbxkX)!=2vw8OgxUy?u{=T(Z{SXA9ybXbljr$1p=dtfrXBYJJ%5bz} zPaliPn^sl_q0EC0dBL;#f}@%Sc>38y{kFEJJ*J6}^ox^EgJec~^zeg}a8dX#K`$X~ zWRG@Jb8D(+m>(vR>09!>d!0R}!XGcj$vKJ{k25&pzDzkCrK z<-ziP?p(gK%^7aRwyP+F&D8OS5a_1aIGBIBI#E2so0!qPB=Kc?NP1=+cB!02ov}tw z{nap!%k}!$IJT9fm8GWF{1r>CCdJ^%no?Wy95t<%{GaNWupXw~ce!}lkFiV^xA_G~ zr4yc;@XV^X`_CmOuQvvse5f>cvN3!Q^gY6k2p%l+X_N=s%G}WLw}|iz3wq2{k!MOw zY1Z;F)C_0-A^_mMk&?YO@#Fply)M%Z%aheQ#+XG(&6mG$;`;+Hk85{bL^a5V`YrE7 z5xP+b=Vuf(bX8nNYb_}u{QSpK`p;v6_;Z#mH*-$D-=(q}_T_>NZ4giAg_O5u^$P>! zSr#c#{7DKlZ>sgyME2O~??ld^JjQ6@JObM78u*Q9o6cg_mYwoeMHVVX={S4AOCMqGC=8^1g-^H2T&?qoj=rGR^`?43n%DDFH;$~P@ym?%=kR2IGt z#^Cl8nsOHRv*gLn1C-*qYqAzILLEBw+Hk{@AroUFO~0tOVwfVE%HwuSui0~ywrAbq za@aE3@RRM6$~1lKFx=2i+$eN!;6PXjQR-V^1&hA(T=X>V;p%Ui$$L-fY?OOfje}vn z-51#q)Z_5o2Ty-D-2GLElH%nhY3OuFDYUqPZ-yn4f38=C+x;+}78_udOroRHlqmWF za72w=JN2Vmb6X2vYcn4B46LRz*Nuprnz+y;B3{ds85oVVRJZL#G`g+yD{u z<~=08SUxiP1yZuux~J(yW(;sh#R}8%b<{Hfj=HvtH=bvJ5h)$no429B_SN7j8?MQ# z%S&)ZzZ#4v$7B1)&ly}4cWiDi#bP;Pvpj9*5HlvZ=?U$Rg(-b_ z|2cXx(?VL#UF+V&s~)-JYTk#V3mb0W7{%1Xn&*|byiz#i;hpbXQ;#`w1dp5@xrKCTTVZ-K&&EC$&$H5W;cpcSZdNZq%yz`OY888Il=CD9 zu&b!R<`)mRxhM}t;m!}LIkc_mY}d1zXP=k{MM( zEEP^UvLC2s@OYcD!r4;qM;>mOy^-U)(d;w^Sg&wnX;Vwq^v}01F)S8+9UxcNNPX8P zj^WgSsFO%wS|A%@3q-C4n4NlX8{GL0R(cX-U0qGpk^ivLJuQnxKnm|i26ZD9A)-JK zGCCIaFJ%0$^d|X7)E5Ah666a4T+WDo^4DH*ndSyKePv4R>|oXHuu8}_R57j~uUnJl zJ@1pJg7eP~u~I*$bWS9^s{KzjX(L%WIwj_rL9bV9<`l^l8Q8}oByAnaAvj9=tjyNA zN_rLf+hoK^q0^50Ijfl#Nzcy%t8lQH=}cIe{m@mo>@UHNz82jcT=ROYqRhETz{k5g zdPkBvE7BF2v2#t>TBGYST|A7BlWZP20%WjlyOx7`uI<`a3$^!S1S=oQb3E*`K@!#{Yj@EVf+>Rwx@Re`2inEu`T2F#rZ zIpT?%_m-*NEa-&3&-W2@Oxh@wOnT1yL~O)-0MZMvFLd8^xZTT>;#NJwT^MSpx|}YO zADNmK=|pbi3`;i-KSZr4C^`iOK8kg)<-pi?f~{Lo=NF?(QfEMMr6Vj~L)?3@n|(AO z;DE)*!p>v-b%(pkkHcSYT$rOl9S&xNoUxwbqdn_se*l7WwdaBp3hxira|vK&x=i)I ztp8tAP0dGW-cbB+)=%&aMNuFu5Fi2*x}0W0Ut9B^CeIRzxL}_>Po@COk2P%YJNb|~ ziQGI^F8X-zY$N4P^N5+PPz5`+4?ejp$T0h}C1kh6i68RS&estCf;-;7u$d9}Q%Ypw$R# z&T^?|?6>T!Ic#b*sd9GsASR1->I`$CWCHO__>Z_^?nb{%M{~X8pDXgOn4jBafKxfV z4C-@ed5IBCFnhU8wq=~LKKDi1s=wjo#lcZYZlT(-i#l^_yY6jf`o$#pM{4dnk7BMJGz2hkJzkF)Gj z9&-H#g8)tSpOiHQsANvJ4?4KkvTJnRX`V6Gd?ny$$fczLdRQYk`M}UUP}Vp}%emkO zE;^+t32d<;M_Uo$j@)$82{jzB!f6hNhm?;~X-0*H;!Rp!m}MfHW%NFJ;|J5;>&TeY zJgCGT@j5dJmZZc~!YMU;$AQS1pVEV3{m|qqhONxsaB;aSJQ=SiZ=w#QWA2$fy;9ih zZf-(4{8VPkqG5mu?0+~*Z2pYaav`~aUUADDRg7VI?DqZ&U7^?<;sT1FE@ukxb1zbkJ4x{BqpGG47`BBb(gDGx))VjB%X z{RwKMFHYFC1E;l1k&y7Vu|quvJ4cMhc62g<>FO(SN5(1&w0?2%!J*od7YVl3D*a1||q< z#w826Y^wc7%lr3-tIwF3j+ZmC!p1?Qz0R_Ex(=wM04%!>Te>K?klJ~JnXymGIq=;g zH4i_^SN~;n-*xUnCOQN2JUgS zu`!G1Q9`b{^~y|@5aJOD1%uc=vp6LYJtc+EskqMu7JVDm{cVGz+OS$2Ly+1?X@_MO z8rYViBj|3p3%8yyF;V9U4z?w@Im@n7^Fc4Ik|-5~D{U4*q5`Fjy9qEABS|4-0+M?F zG!me$e@$vg(B*n16zod_Bq1UGX)QEG5oX<1L;zD9MK`YC#QLI!G9Cprx2)Lqcu^&n z1zbJ^oIR_<)mO8YAp9D|o|g(JiSSSv!fPe!{Gf6B$O^l6C)Krr;*U`|22N_Kaz+V4 zw}`QD-tYnUTBA5nx@FFz;nV^NE$7zxOxiJA$x=|^4=HPmaHz4Bu-P?%*Md)um$4$a z^e1}SjvVdPdTb)sSZ|*7;=TLT1|+8xCbRv=+=IDNjC;dn%epDv?By}q*1C#ip2D zcs)fgyHOf)@FQ>j8hUuW;2kqYD0T*^Z(ug9g%^jtt7g}=2}6b?Ypf?)-Et7iijLN2 zIt6IoFT6SXPEnAD19B=`>cKc{L>KPvXaHsu_RVPXp1MJFqV&W-WmpWdiXVI)N9ZNK z$dozL)xsAmK)DlXKT_0+L${;dwwu-gbg{6sJPp zO2%Z3U%#v#)O?_fsH=qR`&(uHUZsQ+x>W+j|Mp7~Y)SthNDR?G(yG9g77Y!lY7dxJ_XONYjGX^q-Sv$VW6JPGA?|9ztrx8YM^2rjSt zi7Z%gM1d-(EJQP{cfyCu?a>T{j{99!F{+2mTRXZ3QL@CJq~t1j(!Xf~Sq26W$xnxs zcT5oZKJHCXpKVXkr0Rlgqh>+XtV1?1qbE%CI{vTfn$7t0W2aMWd9UnhI}i0qOI92D zTx0qigc;Oxb4ykxI`G#WmB#`5ctrJdLy7u!p^XoFerNvb^D>pk3}4eT=mm5|qc&fe zdVwG2HydL>89A|*d1QYZ&6n?$$)MU?|FSmI;tZ4d0E7Yf%NZ}vm$}At*?0a&asFGm zHU1iAijmMLf?x1bhHi?2h8gJX)z_|jq5YL1QmH>q?w?jOhq825d{Xx#@k!I;&^WHL zbvdYI!OB{*2W0Bw5fYjyA2I#fj;^4LXB7jXz;|e3$o`fFNL!xG5Wr7VVmF2>T1pBo z_2F4Pf&lz+MJkoR@>-}ehPm8@C^70OR`R%11io9C)lY(FkY-|(-7?0=4q^J^3r_ld zG`Jk;raRafOf`S{vOOlp+ePgu!u$@U_vRX3bcDH0Yb=yh4)S2#bgB~>5mtjohZ-LI z>eI`32f8=(p9AlH+Op`~*s*!+h^b8>=fCF62SQToJ^s7&AWbnj(f?%I|B}@%S7H{y zP}zMsb~%3?Mwejquj`&*mAlNhGRdj}Z2(-wR&L7QM|Tm@KgM?82H0|P_DcUA?m^sp z&5}1}h;8j6SoW(N#bWFI+}2J%xy-!RaQ`h9L5huD-5e0!A>=ChCx)V-*@PzB(UiqS z58EfeT*?kD0W4SCt%oChwg@i%Ye*~Ss4IwI6=iB36ck_dgp0)&-Uo$rhXoPRJPBu? z>Ww!ehJ4W|j(Pavda|EZtlF@v0Q(sU0 zHi+$T+uc@$nW>7lNzd)lT~G9%)%@>v$iMRQA35W4^Z3W^tOr!_>U5XRGcCVSIU=Ot zqYJ4eE$c`QWh-*Gd!^LfY;IBr5+Kr*Q``2{8sbOkK>_PgoJ{N4=NFJwHUrU}BecWk zHELf&svoiXN=M@qyJ$9FtGE|*PkT*ynxd^DjE54ndzWIH5^0ktUV<5^%=!gwg|gYH zg1?W0Wzk40;~QKhl1(&CT@om$Zy!G)A)M0+-Napi@Uqj=5iVv7P-RC=x$No&Qkb|= zqyKq6PQpI=MeQdMqr3*e5LRU6b%I6jLxP(Bh`}>i;ZXZp&HnIn6KnjupS+3M5aXWH z90r&XYU=WZn(}MV^4^!o$XhZOn@2TuRGT7^ep0O2zu&XNe=T$#GAfzIHU^72qyRIw z?C@=HR;x}n@tsh^U|$WeyyJ2jh3hIl774ngqN|T`u`){R13=mIijjVS=i}FVO1We` z4O7YC^O96%I!nv-3YuOG9v)Mi0%x3?p(bOCncL9QprLRl-YXOR5}O`!FV~ZikK3L6 zb05b5*#&VXUX4wLikKeH2{yJ<_GH%=%daP!cPhDdR0brS)26uZzHNr2kh8HF5DzF7 z=nb$KCK*c1JyWocqMd}7b7Doufw;^d>gz+xs8!%}+E*U7LN{W8iVyw+7Ai3zk^}a7 zAIsX8v+PUKyU=7GJKdWwY?Dj+9hFuK0PKkK2c+Y*Oks_1H%UbrZTFqAT=9?9e!8Y7 z98(U6i5t_U3|3by?o2m_tinAy-HK)E@a_XC5}BXoN(YU*FiHY_q9}WojT^btLM&bY zB}sw_tMA4+AfKU-hFZqDflZnlO`WbCnX3oN_e;3OU0@z`0Fl>4yjmCCsE~{*M8<0s z%%dLqgwnbK@hqgEf3UeG?1p*;+I~X6siN~x+s$m8q^2vd0_7P{k-O0;i2|?>DWDQk z-1;*iQFai+ePsv$3GMP%gDRs*A^X~WA^^Gkra8YOG-*Uuh5tK@BDDG)8cdg_9#LFG zRgf&Lu$~@AmL3|6rrHgINhh9E>*gGY9TW~LQv1(&SFcOl^?74e`3{WSEB5(dRfqg_ z=M?zhtKsQ~i+a=Kp6PZW+)rOQ0sIzsm#sQj^0Po`~#$PO2dFyOk2MYgHnbpf&hBDN? z5yhe{I$7BSNQNUq^+*9k1xPWZgfjJfEUKO=?@asd++#j710wS@;yy8H;_0%qr_5#O zp*{eoYrtltT!wtJ(nH&yM&bi{4iI2PZQZG*D*fi%0$wiZtuuUMX{FSK(B-={+ZVCX!G?7+@QD{)KCIT$kMsUi~ zWZ6H10P--U+?fynvB7orD~nRJWT@@p{PcW=Un;J=7@o)DzY0Rrx5fU;@GoKKdlHZ7a$sVf_(V#okr0_z#)dT*&0P zb%q42qyE=+$YdagDBN&QOy41$l5ED@`kn5VKswdpC&VwYD{&8&#d7$(7>XLcH_@h@rrd~l1*TByZzZY{xTjYo1E9%T z_D5Xpa3C9SKn3!N^m~mc+(MUoL7E+@yVye~!zL5TPSGF~!sduhnM$(9fKGc}nW`+^ zU>7s5Oy?l^!qp4*`Wpvi^dzX1-Ho*! zO;#M9I_iEth;eS*&snbJb+N^e%&w#Do=LehtEA?R6Z^uEX-GQ=V8PH=_i`E+43?ik z2$M^cWU!nQ#QAVXW-okRdomHcuJ zykq66K1vS{@CH3o(7T;E!UQe_qJ2VflKP@UFeQDp_qSP0RD9pu<2)Qv`j(DxNaWnc zJIv}g?x?*Nej0RR4T4+B=FgFzL{v!js%L~9>X7%zbNPruC6qw`c7YLQOc~&cWLa7u zMiP*vPrx~1BiDCy2oEf>BE@w2_(K8SQ2ELF5fVLO*V5uNa@b9QK7ceNw|p4|Bb*JE zvnXtd;x^r8%hEHE05gZ75i8^FNp3Bm!3LWFU{`Mi^}JbqiSW*qc zeP5?GL^9FZd2ieL?r709Z3_PU_Z4Vhmw~LJpDRO<9F;zDt#h~eV84p_U{7G~?s4}aGNLY!FDE%9kk*h_Pa(0* z(VLA5Zy|*-Ik(p+dDN)Zs4aAWYoy8;=mdQ9!|hT2)Y$WKGDoL5@!m~?X`6GfBy&%E zbYL~I8YW7K3V9lBaYS_W@{vR_e*(ezm#BiYvv2Rab{F3HG`JLPFwT$8YA5jOYy8tm zKQATmj%0k69&xLoHhTq{L;+`?laou0Ap}i6m)R+o*?IX~C;@OWKyoh)frf6~83Zp1 z;dxMUH3EYtvPWKep!-B1UEWaTQi98QK`*hra + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/images/cc-by-nc-sa.svg b/images/cc-by-nc-sa.svg new file mode 100644 index 0000000000..bf6bc26f54 --- /dev/null +++ b/images/cc-by-nc-sa.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/images/cc-by-nc.svg b/images/cc-by-nc.svg new file mode 100644 index 0000000000..36973490ad --- /dev/null +++ b/images/cc-by-nc.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/images/cc-by-nd.svg b/images/cc-by-nd.svg new file mode 100644 index 0000000000..934c61e15e --- /dev/null +++ b/images/cc-by-nd.svg @@ -0,0 +1,117 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/images/cc-by-sa.svg b/images/cc-by-sa.svg new file mode 100644 index 0000000000..463276a8cf --- /dev/null +++ b/images/cc-by-sa.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/images/cc-by.svg b/images/cc-by.svg new file mode 100644 index 0000000000..4bccd14f6d --- /dev/null +++ b/images/cc-by.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/images/cc-zero.svg b/images/cc-zero.svg new file mode 100644 index 0000000000..0f866392f1 --- /dev/null +++ b/images/cc-zero.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/favicon-32x32-next.png b/images/favicon-32x32-next.png new file mode 100644 index 0000000000000000000000000000000000000000..e02f5f4d5c43e3e179083b1eb287b47e1ded6b08 GIT binary patch literal 640 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaMM3p^r=f%IJvW-JI^#13S11^9%x`eYU=fq|TyoQjGHgbgIWfB*jF%NI8{ zx2;>Zu3x`iO-&6bs0aoM3JPUqWq0n}*}Qo(P|Le_@AmKC57(%ytPE5IHwwrGiqzKD z{{8#c)YSCg!Gk}3{P_R>|Gj(no6k4eJyZq^Jd4Jw%*zpM57yY9pK zMcQgs21j>QigM54W!RCVazM0UCtHDVhG46xh5pA!(TqDDnlwLEm~eRS(JM@6p3293 zNMwDW(!P~x!s$%jghh-wOB)P19fJ88oOHECttPa5*~LF^Vp#0%pwrbyN~~|szWB|o z_J4%n&U>rEu5s2%I6jof{A;(j!|>g|;yo?%ZfFGheBJp^9_Zijs*s41pu}>8f};Gi z%$!t(lFEWqh0KDIWCn(cIgdZ_a1@4VXq@stea7=?5CgL^w_Y;0u(GiCWD#az1(ybs x!zs+ln?n>%-?(z($eANDN7zp{cr5VJV|XPlSn|oqbSlsa22WQ%mvv4FO#p=N1nU3* literal 0 HcmV?d00001 diff --git a/images/logo.svg b/images/logo.svg new file mode 100644 index 0000000000..cbb3937ecd --- /dev/null +++ b/images/logo.svg @@ -0,0 +1,23 @@ + +image/svg+xml diff --git a/images/wechatpay.jpg b/images/wechatpay.jpg new file mode 100644 index 0000000000000000000000000000000000000000..31f490785b148ac8fd03147a7137475dae3e74e2 GIT binary patch literal 188112 zcmeFZcUV);x;7j{iXb3LM~zaXgNTSqiH#;AA_7t)B3(d0dPr2HHvs{uktQNwq$@4b z71T%#MFm(C_*3*ZU`saeh52w#0LIA*d!nve;h*~R&0`g zId)`||MNAwAdp=L{(60k2W0o3uLti5c@Mt9r~JOtpa1820)cRWpLQQVaY8+n?N7&S zyVxPm|LeT^@#D|g{(8sfyZ-BryCId=ap9Go0~ z6wW^vu0IO*@3QAF<&SUg+O->e?cwC${LA_O>jTylSZ8k5dx#)6Xt5x6HYvz1K{j?l zHdYr~5!h^wKPvd!+%Pp zS2eYD^$m@0-!*k~c6Imke)!loI5a#miXR)FAd+V1=D#m2F8!dat#52nw`kit;3)W0 zbHIQ9sXc;Vdv@*K&AyxKcYD}&1%q#P!QC83)j9W{H|KKlJ0Nx9A-B+l)WYiaJ<=N2 z$-=k&2Y5teP7-A)znk<&i~jE#^yq)pqQ5lgFYRGXK=!h;fgQ#!2!TP?!sLsvU)C9N zJW69oJ_J7n-|Kyw1?dDJ@!AQKO7OZ^n-Ry`F3GaVnld6m9-Qh01*GOxab#cHNpf^+ zsk>MCQsaVkLAbl)grCx$Bbi3Yw-Y`+EnoY%_vJmL#UsA3lQnr)mcrf7uWfBluIql& z+B^HxX;d-e7_olG*B56d{hdc?c{P>=spjg!Mu&-%z5XdmO;>$ZeR-glZ^kX!B;eki z-IGyl=XiqmvFW7R%_YTFDCcU;&*uO`RY~B@6oEEZh z$g2neOG?~6q=llP*4zTGa(yng9Jnw0sYxZAx`FarlNH|BdccCXzz&6M8AaUgHy>I_ zJvQvL%dGx$GhJ0(rC(`c((LdqXu2|QfzW|ODhndOf;b5zqV^-a%R|&U8*P17RHr+> z+hyO$l+V$g5Vft{Jcd(>gPFy4hEnPdxpV8BMON*QjN$j{g>La9?h% z*PEYKN6!hy>EK{r*8C!qnj3?A&S3P&@MSLwo+SG4WD1l9yCOQ8(^IJX*jSS9$CDG% z4hM->IL3-<(u@t>AGu)?s-9I* zt2Ak=11o~qPfM?0C{yaZ4wn{R{c3b={viMLOS(a4pY%WB7dRrZ9=7&#ApcBLpm5Fr z<|JZTjrV$#&9$n-Hu`l#Q<}wK8>|5y8y4i4*({tM&4MiTjbfil02u5{DSGV=pvi)~ z^#$^(ytdC{e!$sLa~2jr8;p63YX`P*h6PExjb);;fZP`-*4MvG1HCk6o}QWqflKpH zu>HF{yD7(;rBxNOMY!Vu0ld}5aH)OO#cAhclfT`1U^oC1M|r_w9M6t0v_oI;GObj2 zEz_1Ke^h_dtbDn(|HHvz_@_OWo;Zo0=^4D>BCsGUyh5yH=qp04lis0bzU?e?Njl#f zgS{MTl*_?7`cS8*&Ps2H_Oc-L=%_&7=!svEFK%9Z`J)LaJELK5sFur-xqZiay+m#+ z&B*z7!deuEz(MPxNnosiodtPpyRGTq_`_Gw^U2NF)yb=(3SP=V4GQ=1N;0u7Q<{b9 z+q˶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 + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2017/08/30/Vue-VSCode-Snippets/index.html b/ja/2017/08/30/Vue-VSCode-Snippets/index.html new file mode 100644 index 0000000000..750fdfb52b --- /dev/null +++ b/ja/2017/08/30/Vue-VSCode-Snippets/index.html @@ -0,0 +1,777 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Vue VSCode Snippets 自动生成Vue代码片段的VSCode扩展 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Vue VSCode Snippets 自动生成Vue代码片段的VSCode扩展 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

现代化的IDE已经把开发者变得越来越懒,但是我觉得这样挺好的,让工具完成手工反复重复的工作,提高工作效率的同时降低开发者的疲劳感,Vue VSCode Snippets就是这样一个VSC插件,它可以用简单的几个字母就敲出一整块代码片段,在学习和做VUE项目时可以极大地提高工作效率,下面就介绍一下插件的常见命令。

+ + +

此插件可用比较简单的写法生成代码片段,非常适合开发工作,减少代码工作量。

+

Script

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SnippetPurpose
vbaseSingle file component base
vbaseSingle file component base
vdataComponent data as a function
vmethodVue method
vcomputedVue computed property
vwatcherVue watcher with new and old value args
vpropsProps with type and default
vimportImport one component into another
vimport-cImport one component into another within the export statement
vimport-exportImport one component into another and use it within the export statement
vfilterVue filter
vmixinCreate a Vue Mixin
vmixin-useBring a mixin into a component to use
vc-directVue create a custom directive
vimport-libImport a library
vimport-gsapImport GreenSock with Timeline and Eases
vanimhook-jsUsing the Transition component JS hooks in methods
+ +

Template

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SnippetPurpose
vforv-for directive
vmodelSemantic v-model directive
vmodel-numSemantic v-model number directive
vonv-on click handler with arguments
vel-propsComponent element with props
vsrcImage src binding
vstyleInline style binding
vstyle-objInline style binding with objects
vclassClass binding
vclass-objClass binding with objects
vclass-obj-multMultiple conditional class bindings
vanimTransition component with JS hooks
+

Vuex

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
SnippetPurpose
vstoreBase for Vuex store.js
vgettersVuex Getter
vmutationVuex Mutation
vactionVuex Action
vstore-importImport vuex store into main.js
+

Extra (plaintext)

+ + + + + + + + + + + + + + + + + + + +
SnippetPurpose
gitignore.gitignore file presets
vincincrementer
vdecdecrementer
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Vue VSCode Snippets 自动生成Vue代码片段的VSCode扩展

+

文章作者:

+

发布时间:2017年08月30日 - 16:23

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2017/08/30/Vue-VSCode-Snippets/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2017/08/30/Vue-basic/index.html b/ja/2017/08/30/Vue-basic/index.html new file mode 100644 index 0000000000..77e2f76188 --- /dev/null +++ b/ja/2017/08/30/Vue-basic/index.html @@ -0,0 +1,739 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Vue基础 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Vue基础 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近公司技术选型选择了VUE,下面就记录一下在学习Vue中遇到的一些知识点,有些在官方手册里已经讲的很详细了,这里就简单带过,主要对cli产生的以及自行添加的一些项目目录进行一下解释,希望对正在学习Vue的同学提供一点帮助,如果本文有谬误之处还请各位指出,谢谢各位支持。

+ +

搭建项目

1
2
3
4
5
6
7
npm install --global vue-cli
vue init webpack my-project
cd my-project
npm install(推荐用cnpm install)
如果没有cnpm ,先安装cnpm镜像
npm install -g cnpm --registry=https://registry.npm.taobao.org
npm run dev
+

目录讲解

+
    +
  • build和config :项目开发和打包时候的相关配置;
  • +
  • node_modules :项目所需要的依赖文件;
  • +
  • src :主应用/页面相关文件;
      +
    • assets : 静态资源文件;
    • +
    • components :组件;
    • +
    • res:资源
        +
      • css: 公共css或是css预处理文件;
      • +
      • js: 公共js文件
      • +
      • img:公共图片
      • +
      +
    • +
    • router :路由配置文件;
    • +
    • views : 视图文件,其实也是vue组件。按照业务功能划分模块;
    • +
    • vuex : 状态管理的配置文件;
    • +
    • App.vue : 主组件;
    • +
    • main.js: 入口文件,初始化vue实例并使用需要的插件
    • +
    +
  • +
  • index.html : 主html页面;
  • +
  • dist:webpack打包生成的文件;
  • +
  • package.json:记录依赖相关信息
  • +
+
+

文件的加载顺序:

当我们执行命令 npm run dev的时候根据配置文件dev-server.js里的相关配置去加载webpack的相关配置文件 在webpack.base.conf里面entry入口文件就配置了app:'./src/main.js'

+

所以当我们在运行npm run dev的时候就开始通过main.js执行了。main.js 初始化vue实例并且加载相关配置插件,然后通过app.vue文件去访问各个组件

+

Build/dev-server.js主要完成以下几件事情:

    +
  1. 检查node和npm的版本;
  2. +
  3. 引入相关插件和配置;
  4. +
  5. 创建express服务器和webpack编译器;
  6. +
  7. 配置开发中间件(webpack-dev-middleware)和热重载中间件(webpack-hot-middleware);
  8. +
  9. 挂载代理服务和中间件;
  10. +
  11. 配置静态资源;
  12. +
  13. 启动服务器监听特定端口(8080);
  14. +
  15. 自动打开浏览器并打开特定网址(localhost:8080);
  16. +
+

Build/huild.js主要完成以下几件事情:

    +
  1. loading动画;
  2. +
  3. 删除创建目标文件夹;
  4. +
  5. webpack编译;
  6. +
  7. 输出信息
  8. +
+

配置文件

.babelrc

设置转码的规则和插件(使用es6语法必须安装插件)

+
1
npm install babel-preset-es2015
+ +

presets 字段是用来设定转码规则;

+

.editorconfig

配置文件编码格式的文件

+
    +
  • indent_style:  设置缩进风格,tab或者空格;
  • +
  • indent_size:  缩进的宽度;
  • +
  • tab_width:  设置tab的列数。默认是indent_size;
  • +
  • end_of_line: 换行符,lf、cr和crlf;
  • +
  • charset:  编码;
  • +
  • trim_trailing_whitespace: 设为true表示会除去换行行首的任意空白字符;
  • +
  • insert_final_newline:  设为true表明使文件以一个空白行结尾;
  • +
  • root: 表明是最顶层的配置文件,发现设为true时,才会停止查找.editorconfig文件;
  • +
+

.eslintignore

忽略不符合eslint规范的文件, (一般会忽略掉第三方引用的插件)

+

.eslintrc.js

定义代码规则

+

.gitignore

配置文件,用于配置不需要加入版本管理的文件

+

.VUE 文件解释

    +
  • template: 展示模板
  • +
  • import : 导入组件已经js文件
  • +
  • export default:
      +
    • data:数据源;
    • +
    • methods:方法;
    • +
    • mounted:页面加载之后执行的方法;
    • +
    • created:页面生成时加载的方法;
    • +
    +
  • +
  • style: 样式代码 其中scoped表示样式作用范围为本vue文件
  • +
+

网络访问

axios

    +
  1. 发送请求:
  2. +
+
1
2
3
4
5
6
7
axios#request(config);
axios#get(url[, config]);
axios#delete(url[, config]);
axios#head(url[, config]);
axios#post(url[, data[, config]]);
axios#put(url[, data[, config]]);
axios#patch(url[, data[, config]]);
+ +
    +
  1. 处理响应:
  2. +
+
    +
  • Promise语法;
  • +
  • 处理结果:then;
  • +
  • 处理异常:catch;
  • +
+
    +
  1. 拦截器(use/reject):
  2. +
+
1
2
3
axios.interceptors.response.use;
axios.interceptors.rquest.use;
reject(移除请求拦截)
+ +
    +
  1. 参数:
  2. +
+
    +
  • json(默认);
  • +
  • qs;
  • +
+

组件通信

    +
  • Prpos:父组件对子组件;
  • +
  • 自定义事件:子组件对父组件;
  • +
  • 消息总线:任意两个组件;
  • +
  • 状态管理:Vuex(适用于大型单页面开发)
  • +
+

路由

    +
  1. 配置
  2. +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Vue.use(Router)

export default new Router({
routes: [
{
path: '/',
name: 'Hello',
component: Hello
}
]
})

new Vue({
el: '#app',
router,
template: '<App/>',
components: { App }
})
+ +
    +
  1. 导航
  2. +
+
    +
  • push
  • +
  • replace
  • +
  • go
  • +
+
    +
  1. 参数传递
  2. +
+
    +
  • RESTful url参数
  • +
  • 参数查询 query
  • +
  • 锚点 hash: ‘#data’
  • +
+
    +
  1. 嵌套路由
  2. +
+
    +
  • Children
  • +
+
    +
  1. 钩子
  2. +
+
    +
  • beforeRouteEnter
  • +
  • beforeRouteLeave
  • +
+

状态管理

+

Vuex是什么?

+

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

+
+
    +
  • state里面就是存放的我们所要用到的状态;
  • +
  • mutations就是存放如何更改状态的方法 ,同步操作;
  • +
  • getters就是从state中派生出状态,比如将state中的某个状态进行过滤然后获取新的状态。
  • +
  • actions就是mutation的加强版,它可以通过commit
  • +
  • mutations中的方法来改变状态,最重要的是它可以进行异步操作。
  • +
  • modules顾名思义,就是当用这个容器来装这些状态还是显得混乱的时候,我们就可以把容器分成几块,把状态和管理规则分类来装。这和我们创建js模块是一个目的,让代码结构更清晰。
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Vue基础

+

文章作者:

+

发布时间:2017年08月30日 - 17:03

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2017/08/30/Vue-basic/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2017/08/30/npm-source/index.html b/ja/2017/08/30/npm-source/index.html new file mode 100644 index 0000000000..0a0935e802 --- /dev/null +++ b/ja/2017/08/30/npm-source/index.html @@ -0,0 +1,608 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + npm相关资料 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ npm相关资料 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

近两年前端突然呈爆发增长之势,node的出现起了重要的作用,而node之中有一个重要的功能就是npm包管理工具,下面就记录一下我学习npm之中遇到的一些问题及解决方案,仅供各位参考。

+ + +
+

npm全称Node Package Manager,是node.js的模块依赖管理工具。由于npm的源在国外,所以国内用户使用起来各种不方便。下面整理出了一部分国内优秀的npm镜像资源,国内用户可以选择使用。

+
+

国内优秀npm镜像

淘宝npm镜像

+

cnpmjs镜像

+

如何使用

有很多方法来配置npm的registry地址,下面根据不同情境列出几种比较常用的方法。以淘宝npm镜像举例:

+

1.临时使用

1
npm --registry https://registry.npm.taobao.org install express
+

2.持久使用

1
2
3
4
5
6
npm config set registry https://registry.npm.taobao.org

// 配置后可通过下面方式来验证是否成功
npm config get registry
// 或
npm info express
+

3.通过cnpm使用

1
2
3
4
npm install -g cnpm --registry=https://registry.npm.taobao.org

// 使用
cnpm install express
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:npm相关资料

+

文章作者:

+

发布时间:2017年08月30日 - 12:24

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2017/08/30/npm-source/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2017/10/11/use-GitHub-Hexo-Next-make-blog/index.html b/ja/2017/10/11/use-GitHub-Hexo-Next-make-blog/index.html new file mode 100644 index 0000000000..f014577f05 --- /dev/null +++ b/ja/2017/10/11/use-GitHub-Hexo-Next-make-blog/index.html @@ -0,0 +1,658 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 如何利用GitHub-Hexo-Next搭建一个漂亮的技术博客 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 如何利用GitHub-Hexo-Next搭建一个漂亮的技术博客 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

+

开博客貌似并不是一件难事,现在有很多的方式开博客,而且现在博客貌似也不再流行,但是作为一个开发者,维护一个技术博客对自己的发展是很有好处的,出名者如阮一峰等,所以还是想要做一个技术博客的。

+
+
+

开博客可以选择网上现有的博客系统如博客园以及其他各大公司提供的博客系统,也可以选择如wordpress等博客程序自行搭建博客系统,前者省事,但可自定义设置不足不符合一个爱折腾的人的性格,后者复杂些,爱折腾的可以学着搭建,但是服务器域名是个让人头疼的事,还有数据库等操作,着实麻烦,如今又有了另外一个选择,那就是利用GitHub和一些静态的博客系统搭建一个纯静态的博客系统,不花钱又能折腾,而且还是命令行下的,装B感觉棒棒的,另外搭配时下流行的MarkDown语法,做笔记正合我意。

+
+ + +

技术栈选型

+

这里说是技术栈选型可能并不是很恰当,但又找不到合适的描述,就是把需要的技术介绍一下,如果还不会的,可以自行学习,或者看看我的其他文章。

+
+
    +
  • node(npm),现在node这么火,没用过都不好意思出门,但是如果你还不回的话,就先自行学习安装一下吧。
  • +
  • Hexo 静态博客程序,其实还有很多,只不过这个比较新,而且搭配Next非常漂亮,就选了它。
  • +
  • Next 可以说是Hexo的定制系统,不仅仅是做了个皮肤,简洁美观的配置项和官网说明深得我心。
  • +
+

搭建步骤(安装步骤)

安装Hexo Hexo官网

1
2
3
4
5
$ npm install hexo-cli -g // 安装hexo的脚手架工具
$ hexo init blog // 初始化博客
$ cd blog // 返回博客目录
$ npm install // 安装依赖
$ hexo server // 启动博客
+
+

怎么样五行代码就生成并运行了一个博客是不是超简单。
下面我们看一下生成的博客的目录

+
+
1
2
3
4
5
6
7
8
.
├── _config.yml
├── package.json
├── scaffolds
├── source
| ├── _drafts
| └── _posts
└── themes
+

_config.yml这是博客的配置文件,比如博客名称,副标题,作者等信息都在这个文件里设置。

+

package.json这是博客的依赖文件可以忽略

+

scaffolds这是博客的模板目录,当你要写一篇文章时,这里会有文章的默认类型。

+

source这是博客的网站资源,包括发布的文章(_posts)、关于、分类还有上传文件等。

+

themes这是博客的皮肤。

+

更多配置信息请查阅官网手册

+

安装Next主题 Next官网

1
2
$ cd your-hexo-site
$ git clone https://github.com/iissnan/hexo-theme-next themes/next
+ +

然后到_config.yml配置文件将主题配置改成next就可以使用next的皮肤了

+
1
theme: next
+

皮肤也有配置文件,为跟Hexo进行区分Hexo的配置文件称为站点配置文件, 皮肤配置文件称为主题配置文件

+

对两个配置文件进行简单配置后,符合需求的博客就搭建而成了,这里有个友好的建议,配置文件如果配置不正确将不能正确运行博客,所以在配置前务必保留好原始配置文件,注意配置时不要缺了空格,不要问我为什么知道这个。

+

更新博客主题

https://github.com/theme-next/hexo-theme-next/blob/master/docs/zh-CN/UPDATE-FROM-5.1.X.md

+

写博客

博客已经搭建好了,接下来就是写博客了,那么如何开始写博客呢,超级简单一行命令足以:

+
1
2
3
4
hexo new [layout] <title>  
// layout 为模板类型可以省略,title为文章标题,通常可以简写为如下
hexo new title

+

新建命令执行后在_posts的目录下就会生成一个你刚才命名的md后缀的文件,这就是一个MarkDown语法的文件,(如果不了解MarkDown语法的可以去学一下,很简单的符号语言,或者像我一样用支持MarkDown语法的编辑器来写文章。新建的文章打开内容如下

+
1
2
3
4
5
6
---
title: new-post
date: 2017-10-11 15:01:09
tags:
---

+

非常好理解,title就是标题,date为创建时间,tags是标签方便分类,但是这些并不全,还有些常用的分类没有写上,下面我将常用的进行补充

+
1
2
3
4
5
6
7
8
9
10
11
---
title: new-post
date: 2017-10-11 15:01:09
categories:
- NodeJS
- npm
tags:
- npm
- NodeJS
- rnpm
---
+

这样补充后就有了常见博客的分类和标签的功能,是不是很简单。

+

写完文章以后还要执行下面命令,生成静态页面

+
1
2
$ hexo generate // 将md后缀文件生成成静态html文件

+ + +

这样我们就完成了博客的搭建和博客的书写,到现在我们就已经有了一个本地的博客,那么如何将博客上传到GitHub上呢?

+

将Blog上传至GitHub

github是一个代码托管的平台,为了方便描述代码功能,它提供了README.md文件进行说明,但是为了更好的展现,也提供了gitpage的功能,博客是基于这个功能进行的扩展,那么如何用gitpage的功能来实现博客系统呢?

+

创建仓库

创建一个以你的GitHub账号为开头命名的仓库,格式如下

+
1
2
3
GitHub账号名称.github.io
// 如
lixuguang.github.io
+

然后到blog系统的配置文件_config.yml里配置一下上传路径

+
1
2
3
4
5
6
7
8
9
10
deploy:
type: git
repo: <repository url>

// 我的实例
deploy:
type: git
repo: git@github.com:lixuguang/lixuguang.github.io.git
branch: master

+

配置好就可以进行部署了,部署也很简单,只需要执行一下下面的命令。

+
1
2
3
4
$ npm install hexo-deployer-git --save // 安装上传工具

$ hexo deploy

+

稍等一会,如果没有出现什么错误信息,那么你的部署就成功了。之后你就可以访问你的博客了,博客地址如下:
https://你的github账号.github.io/
我的如下:
https://lixuguang.github.io/

+

现在你是不是已经学会如何利用github搭建一个静态的博客系统了呢,如果你还没有一个自己的技术博客,快来试试吧。

+

技巧

是不是觉得命令行还是挺麻烦的,要敲那么一大串字母,哈哈实际上这些常用命令是有缩写方式的,下面给大家介绍一下缩写方式。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ hexo server
// 简写
$ hexo s

$ hexo generate
// 简写
$ hexo g

$ hexo deploy
// 简写
$ hexo d

$ hexo new
// 简写
$ hexo n
+ +

另外每次发布之前最好执行以下命令,清理当前内容

+
1
2
$ hexo clean

+

以防出现冲突的情况,具体动作如下

+
1
2
3
4
$ hexo clean
$ hexo g -d // 文件生成后立即部署网站
$ hexo d -g // 部署之前预先生成静态文件

+ +

常见问题

    +
  1. SSH问题
  2. +
+
1
2
3
$ ssh-keygen -t rsa -C "lixuguang316@gmail.com"
// 填写你自己的github邮箱

+

敲三下回车,之后会在

+
1
2
C:\Users\Administrator\.ssh // windows下
open ~/.ssh // Mac下打开ssh文件
+

文件夹下生成两个文件id_rsa(私钥)、id_rsa.pub(公钥),在github上的SSH处添加新的ssh,然后将公钥内容贴到上面起个名字可以叫hexo,保存,然后在git bash下敲击

+
1
$ ssh git@github.com
+

然后敲yes就可以上传blog代码了
怎么样会了么?更多高阶玩法请阅读官方说明文档,文章如有谬误之处请各位指出,如果觉得文章对你有所帮助我将十分开心,如果你喜欢我的文章可以到我的github上点个fork,谢谢你的阅读。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:如何利用GitHub-Hexo-Next搭建一个漂亮的技术博客

+

文章作者:

+

发布时间:2017年10月11日 - 16:18

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2017/10/11/use-GitHub-Hexo-Next-make-blog/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2017/10/12/VSCode-ESLint/index.html b/ja/2017/10/12/VSCode-ESLint/index.html new file mode 100644 index 0000000000..054316e09c --- /dev/null +++ b/ja/2017/10/12/VSCode-ESLint/index.html @@ -0,0 +1,597 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + VSCode ESLint JS代码静态检测工具 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ VSCode ESLint JS代码静态检测工具 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

VSCode是新一代IDE,方便灵活的插件安装方式让它强大无比,对前端友好,让它成为了越来越多前端开发者的首选IDE,编辑器虽好,但是要自己安装符合自己需求的插件才行,下面介绍一款插件,名叫ESLint,是用来静态检测js代码的工具,让你的js代码在编辑过程中就能找到错误,提高代码质量,好了废话不多说下面介绍如何安装和配置这款插件。

+ +

安装ESLint插件

打开VSCode编辑器,在左侧右下角有一个安装插件的图标,点击后就可以打开插件市场,输入ESLint,就会有个黄色的图标出现在你面前,不用犹豫双击它,稍等一会它就安装完了,是不是超简单。

+

安装NPM依赖

ESLint插件运行需要一些依赖,对于用过npm包管理工具的人来讲小意思啦,我把代码放到下面,需要的直接粘贴运行就好。

+
1
2
3
4
5
6
7
8
//全局安装eslint
npm i eslint -g

//如果用到html中的js校验
npm i eslint-plugin-html -g

//如果用到es2015语法
npm i babel-eslint -g
+

配置eslint配置文件到项目根目录

配置文件名称如下:
eslintrc.json
内容为:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
{
"plugins": [
// "react",
"html"
],
"env": {
"node": true,
"jquery": true,
"es6": true,
"browser": true
},
"globals": {
"angular": false
},
"parser": "babel-eslint",
"rules": {
//官方文档 http://eslint.org/docs/rules/
//参数:0 关闭,1 警告,2 错误
// "quotes": [0, "single"], //建议使用单引号
// "no-inner-declarations": [0, "both"], //不建议在{}代码块内部声明变量或函数
"no-extra-boolean-cast": 1, //多余的感叹号转布尔型
"no-extra-semi": 1, //多余的分号
"no-extra-parens": 0, //多余的括号
"no-empty": 1, //空代码块

//使用前未定义
"no-use-before-define": [
0,
"nofunc"
],

"complexity": [0, 10], //圈复杂度大于*

//定义数组或对象最后多余的逗号
"comma-dangle": [
0,
"never"
],

// 不允许对全局变量赋值,如 window = 'abc'
"no-global-assign": ["error", {
// 定义例外
// "exceptions": ["Object"]
}],
"no-var": 0, //用let或const替代var
"no-const-assign": 2, //不允许const重新赋值
"no-class-assign": 2, //不允许对class重新赋值
"no-debugger": 1, //debugger 调试代码未删除
"no-console": 0, //console 未删除
"no-constant-condition": 2, //常量作为条件
"no-dupe-args": 2, //参数重复
"no-dupe-keys": 2, //对象属性重复
"no-duplicate-case": 2, //case重复
"no-empty-character-class": 2, //正则无法匹配任何值
"no-invalid-regexp": 2, //无效的正则
"no-func-assign": 2, //函数被赋值
"valid-typeof": 1, //无效的类型判断
"no-unreachable": 2, //不可能执行到的代码
"no-unexpected-multiline": 2, //行尾缺少分号可能导致一些意外情况
"no-sparse-arrays": 1, //数组中多出逗号
"no-shadow-restricted-names": 2, //关键词与命名冲突
"no-undef": 1, //变量未定义
"no-unused-vars": 1, //变量定义后未使用
"no-cond-assign": 2, //条件语句中禁止赋值操作
"no-native-reassign": 2, //禁止覆盖原生对象
"no-mixed-spaces-and-tabs": 0,



//代码风格优化 --------------------------------------
"no-irregular-whitespace": 0,
"no-else-return": 0, //在else代码块中return,else是多余的
"no-multi-spaces": 0, //不允许多个空格

//object直接量建议写法 : 后一个空格前面不留空格
"key-spacing": [
0,
{
"beforeColon": false,
"afterColon": true
}
],

"block-scoped-var": 1, //变量应在外部上下文中声明,不应在{}代码块中
"consistent-return": 1, //函数返回值可能是不同类型
"accessor-pairs": 1, //object getter/setter方法需要成对出现

//换行调用对象方法 点操作符应写在行首
"dot-location": [
1,
"property"
],
"no-lone-blocks": 1, //多余的{}嵌套
"no-labels": 1, //无用的标记
"no-extend-native": 1, //禁止扩展原生对象
"no-floating-decimal": 1, //浮点型需要写全 禁止.1 或 2.写法
"no-loop-func": 1, //禁止在循环体中定义函数
"no-new-func": 1, //禁止new Function(...) 写法
"no-self-compare": 1, //不允与自己比较作为条件
"no-sequences": 1, //禁止可能导致结果不明确的逗号操作符
"no-throw-literal": 1, //禁止抛出一个直接量 应是Error对象

//不允return时有赋值操作
"no-return-assign": [
1,
"always"
],

//不允许重复声明
"no-redeclare": [
1,
{
"builtinGlobals": true
}
],

//不执行的表达式
"no-unused-expressions": [
0,
{
"allowShortCircuit": true,
"allowTernary": true
}
],
"no-useless-call": 1, //无意义的函数call或apply
"no-useless-concat": 1, //无意义的string concat
"no-void": 1, //禁用void
"no-with": 1, //禁用with
"space-infix-ops": 0, //操作符前后空格

//jsdoc
"valid-jsdoc": [
0,
{
"requireParamDescription": true,
"requireReturnDescription": true
}
],

//标记未写注释
"no-warning-comments": [
1,
{
"terms": [
"todo",
"fixme",
"any other term"
],
"location": "anywhere"
}
],
"curly": 0 //if、else、while、for代码块用{}包围
}
}
+

eslint就是根据这个配置表来进行js语法校验的。

+

最后重启VSCode完成插件安装

重启后控制台显示ESLint server is running说明插件已经生效,好啦接下来就愉快的写代码吧。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:VSCode ESLint JS代码静态检测工具

+

文章作者:

+

发布时间:2017年10月12日 - 14:42

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2017/10/12/VSCode-ESLint/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2017/10/12/us-gitment/index.html b/ja/2017/10/12/us-gitment/index.html new file mode 100644 index 0000000000..e749b6fc98 --- /dev/null +++ b/ja/2017/10/12/us-gitment/index.html @@ -0,0 +1,620 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 给博客添加基于github-issue的评论系统 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 给博客添加基于github-issue的评论系统 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

上篇文章介绍了如何利用github+hexo+next制作一个博客系统,但是由于这个博客系统是基于静态页面的,所以无法完成评论功能,博客怎么可以没有评论功能呢,当然next主题提供了几个选项,但是国内你知道的,提供的几个评论功能都不是太好用,于是就上网找了好多资料,功夫不负有心人,让我找到了一个名曰gitment的基于github-issue的评论插件,下面就介绍大家如何使用这个插件。

+ +

第一步 注册一个小程序(OAuth Application)

点击https://github.com/settings/applications/new注册

+
    +
  • Application name 应用名称 这里随便写,我写的就是gitment
  • +
  • Homepage URL 主页地址,你可以写你的博客地址,我写的是https://lixuguang.github.io/
  • +
  • Application description 应用描述,这里随便写点什么,反正是自己用。
  • +
  • Authorization callback URL 这个比较重要,请填写你的博客地址,我的是https://lixuguang.github.io/
  • +
+

点击确定以后你会获得两个关键信息,下一步配置时会用到

+
    +
  • Client ID
  • +
  • Client Secret
  • +
+

第二步 修改主题配置文件,添加gitment评论功能

因为用的是next主题,所以配置文件地址如下:
themes/next/_config.yml

+

1、在其中添加:

1
2
3
4
5
6
7
8
9
# Gitment
# Introduction: https://imsun.net/posts/gitment-introduction/
gitment:
enable: true
githubID: yourid // 我的是lixuguang
repo: yourrepo // 我的是lixuguang.github.io 必须跟githubID保持一致的用户名
ClientID: yourid // 上面开通程序获得的ClientID
ClientSecret: yoursecret // 上面开通程序获得的Client Secret
lazy: false //是否需要点击展开评论才能可见评论,一般设置为false
+

一定要注意空格,不然会报错的,别问我咋知道的

+

2、然后在主题的配置语言环境的文件添加一句话

en.yml增加:

+
1
gitmentbutton: Show comments from Gitment
+ +

zh-Hans.yml增加:

+
1
gitmentbutton: 显示 Gitment 评论
+

如果是中文网站英文配置也可以不用写。

+

3、添加新的Dom结构

修改主题layout/_partials/comments.swig
在最后一个elseif分支后添加一个elseif分支:

+
1
2
3
4
5
6
7
{% elseif theme.gitment.enable %}
{% if theme.gitment.lazy %}
<div onclick="ShowGitment()" id="gitment-display-button">{{ __('gitmentbutton') }}</div>
<div id="gitment-container" style="display:none"></div>
{% else %}
<div id="gitment-container"></div>
{% endif %}
+

4、 在主题下layout/_third-party/comments/目录下中添加文件gitment.swig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{% if theme.gitment.enable %}
{% set owner = theme.gitment.githubID %}
{% set repo = theme.gitment.repo %}
{% set cid = theme.gitment.ClientID %}
{% set cs = theme.gitment.ClientSecret %}
<link rel="stylesheet" href="https://imsun.github.io/gitment/style/default.css">
<script src="https://imsun.github.io/gitment/dist/gitment.browser.js"></script>
{% if not theme.gitment.lazy %}
<script type="text/javascript">
var gitment = new Gitment({
id: window.location.pathname,
owner: '{{owner}}',
repo: '{{repo}}',
oauth: {
client_id: '{{cid}}',
client_secret: '{{cs}}',
}});
gitment.render('gitment-container');
</script>
{% else %}
<script type="text/javascript">
function ShowGitment(){
document.getElementById("gitment-display-button").style.display = "none";
document.getElementById("gitment-container").style.display = "block";
var gitment = new Gitment({
id: document.location.href,
owner: '{{owner}}',
repo: '{{repo}}',
oauth: {
client_id: '{{cid}}',
client_secret: '{{cs}}',
}});
gitment.render('gitment-container');
}
</script>
{% endif %}
{% endif %}
+

然后在主题下layout/_third-party/comments/index.swig文件中引入gitment.swig文件:

+
1
{% include 'gitment.swig' %}
+

在主题下source/css/_common/components/third-party/目录下添加gitment.styl文件

此配置文件为gitment的样式文件,需要修改样式可以在这里进行书写,这里修改一下按钮样式,另外将聊天框于文章框样式统一

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#gitment-display-button{
display: inline-block;
padding: 0 15px;
color: #0a9caf;
cursor: pointer;
font-size: 14px;
border: 1px solid #0a9caf;
border-radius: 4px;
}
#gitment-display-button:hover{
color: #fff;
background: #0a9caf;
}
#comments {
margin: 0;
padding: 40px;
background: #fff;
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.06), 0 1px 5px 0 rgba(0,0,0,0.12);
}
+

然后在主题下source/css/_common/components/third-party/third-party.styl文件中引入相应的CSS样式即可:

+
1
@import "gitment";
+

经过以上操作,gitment就被引入到你的博客里了。

+

现在就可以让大家对你写的文章进行评论啦,怎么样是不是又学到啦,喜欢我的文章就请关注我的github吧。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:给博客添加基于github-issue的评论系统

+

文章作者:

+

发布时间:2017年10月12日 - 10:50

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2017/10/12/us-gitment/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2017/10/12/web-quanzhan/index.html b/ja/2017/10/12/web-quanzhan/index.html new file mode 100644 index 0000000000..80d3e97d8f --- /dev/null +++ b/ja/2017/10/12/web-quanzhan/index.html @@ -0,0 +1,606 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《Web全栈工程师的自我修养》读书笔记 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《Web全栈工程师的自我修养》读书笔记 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

近日工作不是那么忙,所以有时间看看书,这本书之前就已经读过一遍,隔了一段时间已经有所忘记,所以再次拾起,重新回顾一下。.

+ + +

什么是全栈

“全栈”是个外来词,翻译自英文full-stack,此处的栈指的是为了完成项目而使用的一系列技术的合集,不是堆栈概念中的栈。

+

“全端”工程师是指能够完成pc端、移动端等多终端设备适配的情况

+

什么是全栈工程师

+

全栈工程师是指一个能够处理数据库、服务器、系统工程、客户端等所有工作的的工程师,根据项目不同,可能是移动栈、Web栈,或者原生应用程序栈。

+
+

简单来说全栈工程师就是一个人能搞定一个项目,全能大神一样的人物。

+

一个Web产品典型的技术栈

+

服务器+数据库+服务器端编程语言+前端编程语言

+
+
+

全栈工程师技术的兴起有两个重要原因:技术的发展和PaaS(Platform as a Service,平台即服务)服务的平台越来越多。

+
+

全栈框架———MEAN

+

MongoDB-Express-AngularJs-Node.js
前后端采用一种编程语言JavaScript

+
+

全栈工程师的要求

一专多长

在一个领域里至少达到高级的级别,然后再去向上游或者下游延伸

+

关注商业目标

公司聘请你是为了让你产生利润,并不关心你会什么,所以选择技术栈时要考虑的是如何降低公司的成本或者提高收入。

+

关注用户体验

产品的最终目标是满足客户的需求,所以作为全栈工程师必须要关注用户体验。

+
+

这是一些作为全栈工程师我整理出来的干货,这本书本身并不是一本技术性很强的书,倒像是一位过来人介绍些经验,适合刚入职场或者进入职场不久的人,在前端领域比较迷茫时看一看,书中介绍了作者读过的一些书,很有参考性,推荐大家阅读。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:《Web全栈工程师的自我修养》读书笔记

+

文章作者:

+

发布时间:2017年10月12日 - 17:37

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2017/10/12/web-quanzhan/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2017/12/12/Git-Shell/index.html b/ja/2017/12/12/Git-Shell/index.html new file mode 100644 index 0000000000..26e53a320e --- /dev/null +++ b/ja/2017/12/12/Git-Shell/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Git的常用命令 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Git的常用命令 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

指令表

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
指令含义备注
git add .提示增加文件.代表所有
git commit -m“说明内容” 提交到本地服务器
git status显示修改信息
git pull从网络服务器拉 更新最新版本
git push上传最新版本
git branch查看当前分支
git checkout develop切换到develop模式
git merge master从master合并过来
git push origin develop提交
git clone git@192.168.2.10:bat-web.git从服务器克隆
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Git的常用命令

+

文章作者:

+

发布时间:2017年12月12日 - 12:20

+

最后更新:2019年12月31日 - 10:38

+

原始链接:https://blog.lifesli.com/2017/12/12/Git-Shell/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2019/12/27/Actions/index.html b/ja/2019/12/27/Actions/index.html new file mode 100644 index 0000000000..8ecb9dedfd --- /dev/null +++ b/ja/2019/12/27/Actions/index.html @@ -0,0 +1,607 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 巧妙利用Acitons进行博客的自动构建 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 巧妙利用Acitons进行博客的自动构建 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

忙活了很久很久,终于不那么忙了,也终于想起来我还有个博客没有运行,哎,虽然用Hexo可以享受Github提供的免费的空间和域名,但是命令行敲代码的方式实现博客编写还是有些不方便,尤其是需要编程环境的时候,最近公司在用CICD的方式对代码进行构建,我就在想有没有一种方式让博客也可以变成这样,于是我就在网上搜Github Hexo 自动构建 这些个词,还真让我搜到了一种方法而且还是Github自己提供的,下面隆重请出 GitHub Actions

+ + +

GitHub Actions 是什么

GitHub Actions 由 GitHub 官方推出的工作流工具。典型的应用场景应该是 CI/CD,类似 Travis 的用法。如果不知道 CI/CD&Travis 感兴趣的建议去了解一下,下面不展开说明,直接说怎么用就好。

+

前期准备

在使用 GitHub Actions 之前我们先来看看我们有什么;
首先我们有一个放博客程序的地方,我这里是叫做 blog-source ,另外呢有一个通过 hexo g 创建出来的静态网站,为了存放它而建的另一个仓库,我这里是叫做lixuguang.github.io,也就是说我们现在是有这样两个仓库。
|仓库|作用|
|-|-|
|blog-source|放博客源代码|
|lixuguang.github.io|放博客生成代码|

+

生成密钥

因为 GitHub Actions 它需要访问我的 blog-source 仓库的代码所以必须要有密钥,密钥大家应该熟悉了,创建博客的时候也是创建了一个公钥和私钥用来在本地往 lixuguang.github.io 这个仓库提交代码
这里呢我们用下面的命令生成密钥。

+
1
ssh-keygen -t ed25519 -f ~/.ssh/github-actions-deploy # 连按三次回车即刻
+

命令执行完成后,我们会得到两个文件 github-actions-deploygithub-actions-deploy.pub 两个文件,第一个是私钥,第二个是公钥。
|名称|解释|
|-|-|
|github-actions-deploy|私钥|
|github-actions-deploy.pub|公钥|

+

接下来的步骤一定要好好看,因为我在这个地方被卡住好多次,就是因为有的文章说的并不正确,或者至少是讲的不够仔细,这里我会仔细地说明一下。

+

配置 GitHub 仓库

配置博客源代码仓库

我这里的源代码是放在 blog-source 中,所以我现在要给源代码仓库配置私钥,配置过程如下:
打开 blog-source 仓库,选择 settings,然后选中 secrets , 再点击 Add new secrets,照着下面填写内容
|字段|值|
|-|-|
|Name|HEXO_DEPLOY_PRI(名称自动构建时有用)|
|Value|github-actions-deploy|

+

配置博客源代码仓库

我这里生成的博客静态代码是放在 lixuguang.github.io 中,所以我现在要给静态代码仓库配置公钥,配置过程如下:
打开 lixuguang.github.io,选择 settings,然后选中 keys,再点击 Add deploy key,照着下面填写内容
|字段|值|
|-|-|
|Title|HEXO_DEPLOY_PUB|
|Key|github-actions-deploy.pub|

+

编写 Actions 脚本

经过上面一系列的准备操作,终于来到了编写自动构建脚本的环节,构建脚本如下,如果按照上面我做的操作一步步来的话,那么这一步你可以直接copy啦

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
name: Deploy Blog

on: [push] # 当有新push时运行

jobs:
build: # 一项叫做build的任务

runs-on: ubuntu-latest # 在最新版的Ubuntu系统下运行

steps:
- name: Checkout # 将仓库内master分支的内容下载到工作目录
uses: actions/checkout@v1 # 脚本来自 https://github.com/actions/checkout

- name: Use Node.js 10.x # 配置Node环境
uses: actions/setup-node@v1 # 配置脚本来自 https://github.com/actions/setup-node
with:
node-version: "10.x"

- name: Setup Hexo env
env:
HEXO_DEPLOY_PRI: ${{ secrets.HEXO_DEPLOY_PRI }} # 这里是上面配置的私钥名称
run: |
# set up private key for deploy
mkdir -p ~/.ssh/
echo "$HEXO_DEPLOY_PRI" | tr -d '\r' > ~/.ssh/id_rsa # 配置秘钥
chmod 600 ~/.ssh/id_rsa
ssh-keyscan github.com >> ~/.ssh/known_hosts
# set git infomation
git config --global user.name 'lixuguang' # 换成你自己的名字
git config --global user.email 'lixuguang@gmail.com' # 换成你自己的邮箱
# install dependencies
npm i -g hexo-cli # 安装hexo
npm i

- name: Deploy
run: |
# publish
rm -rf .deploy_git # 如果上次构建失败这句命令会清除上次失败的代码
hexo generate && hexo deploy # 执行部署程序


+ +
+

通过以上这些步骤的操作,如果没什么意外的话,博客的自动构建就完成了,之后只要你提交新的文章到博客源代码仓库,它将自动帮你生成并发送到博客的静态代码仓库,再也不用执行hexo g -d啦,如果这篇文章对你有用,欢迎follow我或打赏一下这篇文章,感谢阅读。

+

ps:这里有个小坑需要注意一下,因为博客的皮肤也是另外一个git仓库,如果你在本地构建好用但是线上构建博客不显示了,需要注意下是不是皮肤没有上传到博客源码仓库,这里我遇到了,希望你不会因此困扰,拜拜~

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:巧妙利用Acitons进行博客的自动构建

+

文章作者:

+

发布时间:2019年12月27日 - 22:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2019/12/27/Actions/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2019/12/27/Vue-Component-Communication/index.html b/ja/2019/12/27/Vue-Component-Communication/index.html new file mode 100644 index 0000000000..c260602ddd --- /dev/null +++ b/ja/2019/12/27/Vue-Component-Communication/index.html @@ -0,0 +1,615 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端面试Vue篇:Vue组件通信的几种方式 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端面试Vue篇:Vue组件通信的几种方式 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

公司这两年兴起了前后端分离的热潮,在选用三大技术框架时,vue凭借其学习曲线低深受公司程序员的喜爱,因此我们也就走上了vue学习的不归路,在招聘招揽小伙伴时有那么一些问题经常被问起,vue组件间通信就是其中之一,下面我们来聊一聊vue组件通信有哪些方式。

+ + +

props$emit

啥也不了解的小伙伴应该也知道这种方式吧,这是最最基础的通信方式了,父子组件通信基本都用它。父组件向子组件传递数据的时候通过prop传参,子组件中通过$emit传递给父组件,父组件在触发子组件$emit方法时得到子组件数据。实例如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 父组件 parent.vue

Vue.compinent('parent', {
template: `
<div>
<p>父组件</p>
<child :message="message" @getChildrenData="getChildrenData"></child>
</div>`,
data() {
return {
message:'Hello lixuguang'
}
},
methods: {
/**
* 执行子组件触发的事件方法
*/
getChildrenData(data){
console.log(data)
}
}
})

// 子组件 child.vue

Vue.component('child', {
props:['message'], // 得到父组件传过来的数据
data() {
return {
childMessage: this.message
}
},
template:`
<div>
<input type="text" v-model="childMessage" @input="emitParentData(childMessage)">
</div>
`,
methods: {
emitParentData(data) {
this.$emit('getChildrenData', data) // 父组件触发时给父组件传值
}
}
})

// App.vue
var app = new Vue({
el: "#app",
template: `
<div>
<parent></parent>
</div>
`
})
+

解析代码:

+
    +
  1. 父组件通过message属性将数据传递给子组件,并且通过getChildrenData事件来监听子组件出发的事件;
  2. +
  3. 子组件通过props获得父组件传过来的数据,并且通过this.$emit触发了getChildrenData事件;
  4. +
+

$attrs$listeners

前一种方法我们完成了父子组件的数据通信,那你有没有想过如果有多层嵌套的数据最上层要往最下层传值怎么办,前一种方法只能一层一层的往下传,可是这样太麻烦了,那么有没有什么方法能够一次传到你想要传到的位置呢,当然是有的,那就是接下来要说到的$attrs$listeners
假设我们现在有三层包含关系的组件,分别是level1/level2/level3,level1 > level2 > level3, > 表示包含关系。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// level3.vue

Vue.component('level3', {
template:`
<div>
<input type="text" v-model="$attrs.level3Message" @input="emitLevel3Data($attrs.level3Message)">
</div>
`,
methods: {
emitLevel3Data(data) {
this.$emit('getLevel3Data', data)
}
}
})

// level2.vue

Vue.component('level2', {
props:['level2Message'],
data(){
return {
level2Message:this.level2Message
}
},
template:`
<div>
<input type="text" v-model="level2Message" @input="emitLevel2Data(level2Message)">
<level3 v-bind='$attrs' v-on='$listenrs'></level3>
</div>
`,
methods: {
emitLevel2Data(data) {
this.$emit('getLevel2Data', data)
}
}
})

// level1.vue

Vue.component('level1', {
data(){
return {
level3Message:"I am Level3",
level2Message:"I am Level2"
}
},
template:`
<div>
<h1>这是level1中的内容</h1>
<level2 :level2Message="level2Message"
:level3Message="level3Message"
@getLevel2Data="getLevel2Data()"
@getLevel3Data="getLevel3Data()"
></level2>
</div>
`,
methods: {
getLevel2Data(data) {
console.log('这是来自level2的数据', data)
},
getLevel3Data(data) {
console.log('这是来自level3的数据', data)
}
}
})

// App.vue

var app = new Vue({
el: "#app",
template: `
<div>
<level1></level1>
</div>
`
})
+

解析代码:

+
    +
  1. level3组件能直接触发getLevel3Data是因为level2组件在调用level3组件时使用v-on绑定了$listeners属性;
  2. +
  3. 通过v-bind绑定了$attrs属性,level3组件可以直接获取到从level1传下来的props;
  4. +
+

v-model

父组件通过v-model传递值给子组件时,会自动传递一个value的props户型,在子组件中可以通过this.$emit(‘input’,val)自动修改v-model绑定的值。

+

```

+

未完待续~

+
+

作者:李旭光
引用请标明出处

+
+

前言

+
+

作者:李旭光
引用请标明出处

+
+

前言

+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端面试Vue篇:Vue组件通信的几种方式

+

文章作者:

+

发布时间:2019年12月27日 - 22:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2019/12/27/Vue-Component-Communication/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2019/12/30/begin-learn-java/index.html b/ja/2019/12/30/begin-learn-java/index.html new file mode 100644 index 0000000000..e1b93beace --- /dev/null +++ b/ja/2019/12/30/begin-learn-java/index.html @@ -0,0 +1,591 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 重拾java开发技能 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 重拾java开发技能 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近愈发觉得越想往上走,越不能局限在前端开发的领域,再往上走前后端都是通的,最近在知乎上看到一句话,说是到了架构师甚至专家的层面以后,就没有什么前端架构师或者前端专家和后端架构师、后端专家了,只有前端出身的技术专家,和后端出身的技术专家,越往上越注重人的综合能力,当一个人能够解决一个项目的技术选型、架构等工作后,对于公司或者团队来说,那就是个巨大的宝藏。

+ + +

公司这一年

最近对于自己的发展有一些迷茫,这一年公司前端的发展可以说是几经转折,我自己呢也一直在摇摆到底是做管理还是做技术,也参加了公司组织的部门经理的答辩,在部门前端的管理工作中也切实的了解到自己在为人处事方面不够圆润。所以目前也算是断了从事管理的念头,更希望能在技术上更进一步,前端目前看来已经不算是我的短板,而我的短板就是只会前端,一直在前端周围打转,其实如果不是看了那篇帖子,以及公司对专家岗位的要求,我可能还会更进一步在前端方向深入研究,但目前看更紧急的应该是补充一下后端的开发知识了,于是上周末开始我就开始了java的学习

+

为什么选择java

为什么选择java作为后端入门,实话讲好多前端开发应该都会问这个问题,明明有更熟悉的nodejs可以作为后端技能进行扩展,我这里的理由是目前大多数公司的包括外面公司的开发人员大都还是以java作为主要语言作为后端编写的选择,另外前端js中好多的设计也是借鉴或者照搬了java中的一些思想,可以说在学习java过程中也会自然而然的提高对js的理解,更重要的是,java相对于其他语言来说资料也更多,上手也更容易,因为这些因素吧,最终我选择了java作为后端的主要学习目标。

+

怎么学习java

java上大学的时候实际有系统的学过的,只是实习之后就再也没有使用过,如今9年过去了,java对于我可能也只剩下些零星的记忆,说实话刚一开始怎么学,从哪里学让我都有点无从下手,这里还要感谢一下我后端的开发伙伴,给了我很多很好的建议,看书的话大都是基础的太基础,实战的又经常忽略基础,最终我打算还是以视频教程2.5倍速快速过一遍java基础,然后再深入学习一下springboot框架,最后再进行实战,以此掌握java开发技能。

+

开始学习java

最终我选择了在B站上看黑马的java基础+实战课程的教学视频,说实话黑马的教学视频还是讲的很仔细的,老师讲的也很有趣,只是一节课10多分钟,只有一个知识点,对于我来说还是有些慢,所以我就开了2.5倍速加快进就这么着看,上周末两天时间,看了130多课,今天的内容记忆不太深刻,趁着不是那么忙又看了30多课,感觉收获还是满满的,接下来的每一天都会看上30课左右,希望自己能在3个月的时间完全上手java开发,相信我可以做到。

+

立个Flag

从今天起,每天都要把自己学习的进度做个总结,看看这一天自己收获了多少,希望30岁这年我重新起步,迈向更高更好的未来。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:重拾java开发技能

+

文章作者:

+

发布时间:2019年12月30日 - 21:37

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2019/12/30/begin-learn-java/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2019/12/31/GoodBye-2019/index.html b/ja/2019/12/31/GoodBye-2019/index.html new file mode 100644 index 0000000000..16fd9e1c20 --- /dev/null +++ b/ja/2019/12/31/GoodBye-2019/index.html @@ -0,0 +1,621 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 再见2019 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 再见2019 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

不知不觉又是一年,今天是2019年的最后一天,也是我30岁的最后一天,回顾这一年有收获有遗憾。翻出了18年的总结,看到了18年底对于自己19年的规划,现在回过头来看看哪些完成了。

+

18年底定的计划

学习技术

1. 深入学习客户端开发(全年)

18年客户端的项目也做了几个,Electron的开发能力经验更多了,之前只是用html+css+js做前端页面的开发,今年又增加了跟vue框架的结合,成功在多个项目中实践,只是一直没有形成规范感觉还是少了一环,原来一直在犹豫Electron到底算不算前端的技术栈之一,后来也算是想明白了,大前端范畴,只要是面向用户的都算是前端该做的范畴,所以Electron当然也算是前端应该掌握的技术栈,20年会继续学习客户端技术,把公司相关规范制定和落地。

+

2. 学习前端自动化测试相关知识(2019年3月前)

18年关注了前端自动化测试相关的知识,本打算19年好好学习一下,在公司项目中实践,但是今年前后端工作量大增,前端人员的精力有限,加之成本的考虑,并没有开展起来,不过这方面的技能储备已经具备了,年底两次关于前端自动化的测试也将前端自动化的思想普及到了公司前端的团队里,20年是公司强调质量的一年,相信20年前端自动化测试将有机会在公司一些项目中落地

+

3. 学习并掌握TS (2019年5月前)

18年就看到了TS将会成为一门比较活的开发语言,目前来说已经具备了做TS开发的技能,只是TS更适合大型的长期的项目,将更好的保障项目的代码质量,快速迭代和小型项目并不是很适合。前端团队里没有开花,没想到居然在开发团队里见到了曙光,感谢开发的小伙伴愿意接受这门前端新技术,相信有一个点采用后看到了优势,后续普及工作将会更好开展。

+

4. 学习并掌握React(2019年7月前)

18年Vue已经在公司前端全面普及开来,但是市场上React仍然是占据更多的份额,而且RN还能做App开发,所以还是一门必须要掌握的技术,今年看了一本React相关的书,也看了一些相关的视频教程,还参加了公司同事开展的React分享,虽然没有在项目中实际使用过,但是理论知识已经掌握了,可以进行小的Demo开发,20年要用React写一个开源的项目,做到学有所用,学以致用。

+

5. 学习前端持续集成的相关知识(2019年9月前)

19年公司强调了代码必须走线上构建的要求,前端工程化和自动构建持续集成的工作可以说是工作所迫,这方面的技能已经Get到了,另外令我感到高兴的是,通过对Github-Actions的配置,我的博客可以像提交代码一样提交文章了,而且也实践了自动构建的相关技术,很开心。

+

6. 学习Docker虚拟化技术( 2019年10月前)

这个任务没有达成,原本是希望能够通过Docker统一前端开发环境,不过Docker的适用场景还是以部署环境为主,Docker今年貌似热度也降下来了,今年K8s越来越火,不管怎样,持续关注新技术。

+

整理计划

1. 养成写博客的习惯,至少2天一篇,可以是技术博客,也可以是其他。

今年实在是太忙了,别说两天一篇文章,就连一个月一篇的量都没能达成,不过在年底我终于把博客又重新激活了,希望自己20年能够把博客坚持写下来,很简单,积累自己,打造个人品牌。

+

2. 将常用的方法和功能做成插件,开源给公司使用

今年4月开始,更多的精力都投在了部门的前端管理工作,说实话也迷茫也难过,这份迷茫终于在19年年底解开了,没能在技能层面有更高的提升,以及在代码层面给公司带来价值我觉得还是有些遗憾,20年希望自己能够在技术上有更大的进步,技术深度上/广度上/架构层面/后端技能都能有长足的进步。

+

读书计划

1. 每周读完一本书,并写一篇读后感

2019年读了25本书,每读完一本都会写一篇读后感,虽然没能保证每周读一本书,但是基本保证了每两周读完一本书,算是完成了50%的目标吧。20年继续努力,读书使人聪明,因为不聪明所以更要多读书。

+

部门前端计划

加强各设计组前端之间的交流

+

设计组的出现导致前端交流不畅,2019年加强与各设计组前端的交流,掌握各位前端的技术能力。

+
+

没想到一语成第,设计组居然在4月份将前端整个拿出来构成了一个前端组,由我来带,更没想到的是居然在19年年底又解散到项目团队了,这9个月的时间可以说有苦有泪也有甜,前端小伙伴们为了同一个目标,共同努力,尽最大的努力完成公司的业务,可以说为了这个目标,这一年我付出了很多很多,在这方面愧对了家人孩子,虽然最后前端还是拆分了,可以说我能力有限,但是我心里可以说是无愧,希望尽到项目团队里的小伙伴们也能开心工作,迅速成长,虽然不再在一个团队里,但是,我仍是你们的光妈。

+

前端俱乐部推动

+

继续每周2小时的兴趣交流,交流形式不限于ppt,要求各自建立技术博客做笔记。

+
+

俱乐部的活动最后还是没能坚持下来,大家工作比较忙也好,我自己的原因也好,确实没能坚持下来,20年还是希望能够将俱乐部活动组织起来,算是大家交流的机会也好,帮助大家开阔眼界也好,20年一定要再做起来!

+

进行梯队划分建设

+

前端今年人数明显增多,但各层级职责并没有区分,19年抽时间进行梯队建设。

+
+

19年前端从14人增加到了34人,可以说人数上翻翻了,随着负责人制的执行,梯队建设的工作算是完成了50%,但是能力提升的工作可以说还是做的不到位,应该有机会成长为高工的小伙伴没能升上去,这我有一定的责任,20年虽然团队不再,但是希望你们都坚持努力学习,迈向更高的台阶。

+

引入前端工程化工具和思想

+

目前前端工程化已经非常成熟,希望能将成熟的技术和工具引入。

+
+

19年,随着Vue的普及和熟练,模块化开发组件化开发的思想已经深入到每一个前端的心中,现在大家都知道该如何使用这些技术和工具,希望大家能不仅仅局限在用也能了解了解其中原理,至少对常用的配置有所了解。

+

提升整体前端开发的能力

+

目前整体前端开发能力还相对较弱,19年通过培训等方式提升整体前端开发的能力

+
+

19年能力提升的工作做的太少了,没什么好说的,希望20年这方面工作能够投入更多的精力,让更多的小伙伴成长起来,让我们一起加油吧。

+

生活目标

每天陪孩子读书一小时

跟上面的工作和个人目标比起来,这个任务可以说完成度0%,这也是我觉得愧对家人的地方,为了工作,对家人的关注少了很多很多,以至于桐桐晚上都不愿意跟我一起睡觉,20年我要好好对她,做个好的爸爸,孩子明年就三岁了,去到幼儿园会认识更多的朋友,希望她会跟她的小伙伴说,我有个爱我的爸爸。

+

减肥

减肥这件事可以说是真的对我来说太难了,看着自己马上破三位数(kg)的体重,心里无限难过,每次媳妇儿问我啥时候去健身,我都感到像被闪电击中了似的,办了健身卡被人嘲笑成了洗澡卡,20年,减不到150斤我就。。。。

+

写在最后

19年太多的遗憾,太多的后悔莫及,当然也有一些成绩和自我肯定,19年已经过去了,希望在20年,能够成为一个更优秀的人。
WechatIMG6.jpeg
感谢我可爱的同事,年底收到了礼物真的很开心。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:再见2019

+

文章作者:

+

发布时间:2019年12月31日 - 23:50

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2019/12/31/GoodBye-2019/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2019/12/31/gitlab-cicd/index.html b/ja/2019/12/31/gitlab-cicd/index.html new file mode 100644 index 0000000000..b5dcb543b1 --- /dev/null +++ b/ja/2019/12/31/gitlab-cicd/index.html @@ -0,0 +1,664 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 基于 GitLab CI/CD 的自动化构建、发布实践 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 基于 GitLab CI/CD 的自动化构建、发布实践 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

公司于去年开始代码版本管理从SVN迁移到了Git,采用的GitLab程序做管理,但是自动构建采用的是Jekins,最近在给博客做自动构建的时候了解到的了Github的Actions,我就在想,是不是GitLab也有自己的自动构建能力,因为之前在参与项目的时候也看到GitLab上有CI/CD相关的内容,但是没有仔细的去看,趁这次机会就一起研究一下。

+ + +

说一下目前公司的构建和发布流程

1、手动构建时代:开发人员在测试需要验证环境的时候,在本地执行打包构建命令,然后将包放到服务器上,整个过程30分钟左右。
2、自动构建时代目前公司的构建是要在Jekins中,首先是在Jekins中配置拉去代码的仓库地址和代码分支,写好构建的脚本,在需要构建时候进行构建,一次配置后构建全程只需要点一下构建时间长度跟项目代码需要下载的依赖时间有关,通常不超过5分钟,需要注意的是要在构建前同步一下代码版本
划重点
在原来的手动构建时代,代码是以开发本地的代码为准,代码版本很可能跟最新的代码有出入,而且依赖于开发的电脑设备,如果他请假了,那么就GG了;另外通过一次配置后整个构建的时间从30分钟降到了5分钟,一次节省25分钟,那么一个项目周期下节省的工时就非常可观了。

+

为什要使用GitLab CI/CD进行构建

这里实际上没有太大的必要将公司的Jekins替换为GitLab的CI/CD进行自动构建,但是呢,因为公司本身采用的就是GitLab作为代码仓库管理代码,它本身又提供了CI/CD的功能,本着多学一点是一点的原则,我就花点时间研究一下它。

+

什么是 GitLab CI/CD

下面我就要开始把我了解到的GitLab CI/CD的使用方式说一下,从零开始搭建GitLab CI/CD。

+

1. 简要介绍 GitLab CI/CD

代码提交到GitLab上后,满足指定条件之后会触发pipeline进行自动化构建、发布。
pipeline可以理解为构建任务,里面可以包含多个流程,比如下载依赖、运行测试、编译、部署。
那么pipeline什么时候触发,分为几个流程,每个流程做什么,需要在项目的.gitlab-ci.yml文件中的定义。
这点呢跟Jekins里面实际上做的也是同样的事,在线下开发做构建时候也是做这些事,只是通过脚本之后这些事都可以交给计算机做了。

+

2. GitLab CI/CD 整体流程

    +
  • GitLab CI/CD 的 pipeline 具体流程和操作在 .gitlab-ci.yml 文件中申明。
  • +
  • 触发 pipeline 后,由 GitLab Runner 根据 .gitlab-ci.yml 文件运行。
  • +
  • 运行结束后将返回至 GitLab 系统。
  • +
+

2.1 .gitlab-ci.yml 文件

.gitlab-ci.yml 文件是一个申明式文件,用于定义 GitLab CI/CD 流程分为几个阶段,每个阶段分别干什么。

+

关于具体干什么、怎么干,主要使用命令行和脚本操作,稍后会在实践部分做细致的介绍。

+

如果涉及一些逻辑的话,会使用脚本(shell)。

+

2.2 GitLab Runner

GitLab Runner 是 CI 的执行环境,负责执行 gitlab-ci.yml 文件,并将结果返回给 GitLab 系统。Runner 具体可以有多种形式,docker、虚拟机或 shell,在注册 runner 时选定方式。实际上就是运行脚本的容器环境。

+

3. 从零搭建一个 GitLab CI/CD 的基本步骤

上面介绍了一些GitLab构建的主要环节和名词概念,接下来我将给大家介绍一下如何从零搭建一个GitLab CI/CD,一起体验一把GitLab CI/CD的整个流程。

+

3.1 新建一个 GitLab 项目

我这用的是公司的自有仓库,各位可以在开源GitLab上创建自己的项目

+

3.2 配置Runner

GitLab 提供了一些共享的Runner,我们可以不处理Runner,这里可以理解为,它提供了一些现成的脚本运行环境,不需要我们从头配置运行环境,so sweet~

+

3.3 新建 .gitlab-ci.yml 文件

    +
  1. 拉取项目到本地
  2. +
  3. 在项目根目录新建 .gitlab-ci.yml 文件
  4. +
  5. 提交 .gitlab-ci.yml 文件
  6. +
  7. 在项目的 CI/CD 中,可以看到 CI/CD 的运行情况
    这个过程应该没人不会吧,没技术含量的我们简单一提,实际上最重要的就是.gitlab-ci.yml文件中要怎么去写,示例说明文件如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // .gitlab-ci.yml 示例说明

    image: node
    # 定义 stages
    stages:
    - build
    - test
    # 定义 job
    build 阶段:
    stage: build
    script:
    - echo "build stage"
    # 定义 job
    发布到测试环境:
    stage: test
    script:
    - echo "test stage"
    + +
  8. +
+

GitLab CI/CD 实践

在实践部分,这里着重介绍 GitLab Runner 和 .gitlab-ci.yml 文件,主要的流程及遇到的问题和解决方案包含在 .gitlab-ci.yml 文件的介绍过程中。

+

1. GitLab Runner

GitLab Runner 一般由 GitLab 系统维护者管理,配置后,同类项目可以共享,一般不需要进行修改。这里不进行具体介绍,主要介绍下使用过程中的注意点,具体使用可参考 GitLab Runner 文档。(https://docs.gitlab.com.cn/runner/)

+

1.1 GitLab Runner 使用流程

    +
  1. 下载 GitLab Runner
  2. +
  3. 注册 GitLab Runner
  4. +
  5. 使用 GitLab Runner
  6. +
+

1.2 GitLab Runner 注意点

在使用 Runner 的过程中,我们遇到了一些问题,下面简要介绍问题及解决方案,不做具体介绍。

+
1.2.1 配置 Runner 后,push 代码,出发了 pipeline,但一直处于Pending状态

错误信息是:

+
1
This job is stuck, because you don’t have any active runners that can run this job
+

注册的 Runner,默认情况下,不会运用没有 tag 的 job,可以在 Settings→CI/CD→Runners Settings,去掉 Runner untagged jobs 即可。

+
1.2.2 GitLab Runner 的类型

有三种类型的 Runner,

+
    +
  • Shared Runners 在整个系统所有项目都可以使用
  • +
  • Group Runners 注册后,同一个项目下的不同代码库共享
  • +
  • Specific Runners 需要给项目单独配置,使用 Specific Runners 注意考虑是否需要关闭 Shared Runners、和 Group Runners。
  • +
+
1.2.3 在 GitLab CI 中使用 docker

如果部署使用的是docker方式,那么在部署时需要在 GitLab CI/CD 中使用 docker 打镜像发布。可以参考 Building Docker images with GitLab CI/CD(https://docs.gitlab.com/ee/ci/docker/using_docker_build.html)

+
1.2.4 在 GitLab CI/CD 中访问 Runner 宿主机目录

我们使用的 Runner executor 是 Dokcer,在 Dokcer volumes 中配置需要访问的目录。

+

2. .gitlab-ci.yml 文件

.gitlab-ci.yml 详细的用法,可参考 GitLab CI/CD Pipeline Configuration Reference 文档(https://docs.gitlab.com/ee/ci/yaml/README.html)

+

2.1 .gitlab-ci.yml 文件结构介绍

    +
  • image 是执行 CI/CD 依赖的 Docker 基础镜像。镜像中有 Node、Yarn、Dalp(内部 rsync 工具)。
  • +
  • stages 中定义了我们的 pipeline 分为以下几个过程:
      +
    1. 下载依赖阶段 pre_build
    2. +
    3. 构建阶段 build
    4. +
    5. 发布阶段 deploy
    6. +
    +
  • +
  • stage 申明当前的阶段,在 stages 中使用
  • +
  • variables 用于定义变量
  • +
  • before_script 执行 script 前的操作
  • +
  • script 当前 stage 需要执行的操作
  • +
  • changes 指定 stage 触发条件
  • +
  • refs 指定 stage 触发的分支
  • +
+

下面具体看一下我们这个.gitlab-ci.yml文件实际的样子

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
image: registry.thunisoft.com/gitlab-ci/node:v1.8

variables:
# $CI_PROJECT_PATH :项目id,用于项目唯一区分本项目与其它项目
# $CI_PROJECT_DIR :本地项目路径
# $PROCESS_PATH :临时文件目录(包括日志和一些临时文件)
NODE_MODULES_PATH: /runner-cache/frontend/$CI_PROJECT_PATH/$CI_BUILD_REF_NAME/node_modules

stages:
- pre_build # 下载依赖阶段
- build # 构建阶段
- deploy # 测试发布阶段

# 下载依赖:
before_script: # 下载依赖前准备脚本
# 无 node_modules 文件时,新建 node_modules 文件
- /bin/bash ./ci/mkdir.sh $NODE_MODULES_PATH
# 软链 node_modules 到宿主机
- ln -s $NODE_MODULES_PATH .
- cd webpack@lixuguang-project

stage: pre_build
script:
- echo "npm install"
- npm install --network-timeout 60000 # 安装依赖
only:
changes:
- webpack@lixuguang-project/package.json
refs:
- master
- ci

# 构建:
stage: build
variables:
CI_COMMIT_BEFORE_SHA_PATH: /mnt/gv0/gitlab-runner-cache/$CI_PROJECT_PATH
CI_COMMIT_BEFORE_SHA_FILE_NAME: $CI_BUILD_REF_NAME.sh
CI_COMMIT_BEFORE_SHA_FILE: /mnt/gv0/gitlab-runner-cache/$CI_PROJECT_PATH/$CI_BUILD_REF_NAME.sh
before_script:
# 建存此次 CI CI_COMMIT_SHA 的文件
- /bin/bash ./ci/mkfile.sh $CI_COMMIT_BEFORE_SHA_PATH $CI_COMMIT_BEFORE_SHA_FILE_NAME
# 软链 node_modules 到宿主机
- ln -s $NODE_MODULES_PATH .
- rm -rf web/share/*
- cd webpack@lixuguang-projects
script:
# 缓存上次ci
- source $CI_COMMIT_BEFORE_SHA_FILE
- echo "CI_COMMIT_BEFORE_SHA=$CI_COMMIT_SHA" > $CI_COMMIT_BEFORE_SHA_FILE
- python3 ../ci/build.py # 编译
- /bin/bash ../ci/commit.sh # 提交编译结果
only:
changes:
- www_src/**/*
refs:
- master
- ci

# 测试发布:
stage: deploy
variables:
PROCESS_PATH: /mnt/gv0/gitlab-runner-cache/deploy/process/$CI_JOB_ID # 目录不要换,用于日志服务器获取日志展示
script:
- mkdir $PROCESS_PATH # 建立发布临时路径,存放发布配置中间文件和结果日志用
- dplt $CI_PROJECT_DIR/.deploy_test.yml $CI_PROJECT_PATH $CI_PROJECT_DIR/web/ $PROCESS_PATH
# dplt 发布yml配置
- echo "发布完成,错误日志查看http://172.18.78.11:8089/log?path="$PROCESS_PATH
- echo `ls $PROCESS_PATH/*.log`
only:
changes:
- web/**/*
refs:
- test
+ +

2.2 下载依赖阶段(pre_build stage)

下载依赖的方案是:当 package.json 文件发生变化时,触发 pre_build stage,执行 npm install。下载的 node_modules 放在宿主机下,执行时通过软链获取依赖。

+

2.3 构建阶段(build stage)

构建阶段,分为 3 部分

+
    +
  1. diff 文件变化
  2. +
  3. 前端 build
  4. +
  5. commit build 后结果
  6. +
+
2.3.1 diff 文件变化

每次 CI 时,将当前 CI commit SHA(CI_COMMIT_SHA 变量)存在文件中,存为 CI_COMMIT_BEFORE_SHA 变量, diff 时,git diff 当前 CI 与上次 commit SHA 的变化。

+
2.3.2 前端 build

根据 git diff 的变化情况,确定本次需要打包的内容。

+
2.3.3 commit 打包后生成的 HTML 文件

在 GitLab CI/CD 提交代码时,使用 Git 凭证存储,提交打包后的 HTML 文件。Git 凭证存储细节可参考凭证存储文档(https://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E5%87%AD%E8%AF%81%E5%AD%98%E5%82%A8)

+

2.4 发布阶段(deploy stage)

发布阶段,使用内部的 rsync 工具 dplt 将打包后的 HTML 文件部署。dplt 可配置集群、机器列表。

+

写在最后

以上就是GitLab CI/CD的整个理论到实践的全部过程,实现之后你就可以解放双手了,是不是超爽。

+

参考资料

持续集成是什么?(http://www.ruanyifeng.com/blog/2015/09/continuous-integration.html)

+

什么是 CI/CD?(https://www.redhat.com/zh/topics/devops/what-is-ci-cd)

+

GitLab Docs(https://docs.gitlab.com/)

+

Introduction to CI/CD with GitLab(https://docs.gitlab.com/ee/ci/introduction/)

+

用 GitLab CI 进行持续集成(https://scarletsky.github.io/2016/07/29/use-gitlab-ci-for-continuous-integration/)

+

如何实现前端工程的持续集成与持续部署?(https://www.zhihu.com/question/60194439)

+

基于 GitLab CI 的前端工程CI/CD实践(https://github.com/giscafer/front-end-manual/issues/27)

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:基于 GitLab CI/CD 的自动化构建、发布实践

+

文章作者:

+

发布时间:2019年12月31日 - 08:22

+

最后更新:2019年12月31日 - 08:43

+

原始链接:https://blog.lifesli.com/2019/12/31/gitlab-cicd/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2019/12/31/weichat-h5-compatibility/index.html b/ja/2019/12/31/weichat-h5-compatibility/index.html new file mode 100644 index 0000000000..156ca71e8e --- /dev/null +++ b/ja/2019/12/31/weichat-h5-compatibility/index.html @@ -0,0 +1,618 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 微信的H5兼容方案 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 微信的H5兼容方案 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近被公司新来的前端问到了一些微信页面的兼容性问题,因为我做微信开发的经验比较少,被问到了真是感到很尴尬,所以在搜到了解决方案告诉了他之后,我又深入的再网上找了一些微信开发中遇到的兼容问题,特此整理,以留后用。

+ + +

1、ios端兼容input光标的高度

bug描述:
这个问题只出现在苹果手机上,在安卓手机上显示没有问题,可以说是非常诡异,简单描述一下就是在input输入框聚焦时,光标大小应该跟字号一直,但是在苹果手机上当点击输入的时候,光标的高度和父盒子的高度一样。
分析:
说来主要是习惯导致的问题,通常我们习惯将height和line-height设置成一样的值,这个时候input光标就会整个变得很大。
解决:
实际上解决方案也很简单,就是不设置行高,通过padding来控制输入内容与外框的距离。

+
1
2
3
4
5
6
7
8
// less代码
.input-x{
height:40px;
// line-height:40px; // 此行注释掉
.input-inline{
padding: 10px 0;
}
}
+

这样做问题就解决了。

+

2、ios端微信h5页面上下滑动会卡顿,页面会有缺失

bug描述:
没错又是ios端,当页面高度超过一屏,那么上下滑动时就会出现页面卡顿的情况,而且时有伴随内容不能全部显示的情况。
分析:
这里实际上是浏览器内核解析不同导致的问题,在Andriod设备上,微信调用的是Webkit内核,而ios中是使用了Safari的内核,Safari对于滚动事件(overflow-scrolling)会使用原生的控件。而webkit内核则会创建一个UIScrollView来提供给子layer用以渲染。
解决:
在做样式重置时,加上下面这句话就能解决这个问题。

+
1
2
3
4
// css代码
*{
-webkit-overflow-scrolling: touch;
}
+

但是这个方案也有缺陷,就是页面中不能有使用absolute定位的元素,不然布局就错乱了。
延伸:
-webkit-overflow-scrolling 属性控制元素在移动设备上是否使用滚动回弹效果.

+
    +
  • auto: 使用普通滚动, 当手指从触摸屏上移开,滚动会立即停止。
  • +
  • touch: 使用具有回弹效果的滚动, 当手指从触摸屏上移开,内容会继续保持一段时间的滚动效果。继续滚动的速度和持续的时间和滚动手势的强烈程度成正比。同时也会创建一个新的堆栈上下文。
  • +
+

3、ios键盘唤起再收起,页面不会恢复原位

bug描述:
哎,对的还是ios,问题标题描述的比较清晰了,就是键盘弹出时,页面内容会整体上移,但是收起键盘时本应回归原位的不回去了。—_—|||
分析:
固定定位的元素,如果元素内input框聚焦的时候会弹出软键盘,软键盘会占用屏幕面积,失去焦点时软键盘消失,但是仍会占用,页面就会不能恢复原状,也就导致input框不能再次输入了。
解决:
在input失去焦点键盘收起时,写一个监听事件,事例代码如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// vue代码
<input @blur="changeBlur()"/>

// js代码
changeBlur(){
let ua = navigator.userAgent; // 获取用户代理
let app = navigator.appVersion; // 获取客户端版本信息
let isIos = ua.match(/i[^;]+;( U;)? CPU.+Mac OS X/); // 判断是否是Ios设备
if(isIos){
setTimeout(()=>{
const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop || 0
window.scrollTop(0,Math.max(scrollHeight - 1), 0)
},100)
}
}
+

延伸:
在iso的微信开发中,页面元素如果用到了position: fixed进行定位,那么键盘收起时,就会被顶上去,第三方输入法也不例外。

+

4、Android弹出键盘遮挡文本输入框

bug描述:
刚才说的问题都是Ios端的,实际上Android上也有挺多坑,上面讲到Ios上输入框弹出键盘的问题后,Android中实际也有,只是现象不同;Andriod中弹出键盘后页面不会向上滑动,但是如果输入框在底部的话会直接被挡住。。。
分析:
很坑,因为Andriod中输入框focus后,并不会向上滑动,如果靠下就会被挡住。。
解决:
实际上跟Ios上处理差不多的方案,代码如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// vue代码
<input @blur="changeBlur()"/>

// js代码
changeFocus(){
let ua = navigator.userAgent;
let app = navigator.appVersion;
let isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1;
if(isAndroid){
setTimeout(function() {
document.activeElement.scrollIntoViewIfNeeded();
document.activeElement.scrollIntoView();
}, 500);
}
}
+

扩展
Element.scrollIntoView()方法让当前的元素滚动到浏览器窗口的可视区域内。而Element.scrollIntoViewIfNeeded()方法也是用来将不在浏览器窗口的可见区域内的元素滚动到浏览器窗口的可见区域。但如果该元素已经在浏览器窗口的可见区域内,则不会发生滚动

+

5、Vue中路由使用hash模式,分享时Android可分享成功,Ios端分享失败

bug描述:
Ios的问题真的挺多的。。。

+
    +
  • 在分享页面给A时,没问题,A把链接分享给B的时候就跳转到首页了;
  • +
  • 使用Vue-router跳转到第二个页面在分享时候,分享失败;
    以上两个问题在Android上均没有问题。
  • +
+

分析:
jssdk是后端进行签署,前端校验,但是有时跨域,ios是分享以后会自动带上 from=singlemessage&isappinstalled=0 以及其他参数,分享朋友圈参数还不一样,貌似系统不一样参数也不一样,但是每次获取url并不能获取后面这些参数
解决:

+
    +
  • 可以使用改页面this.$router.push跳转,为window.location.href去跳转,而不使用路由跳转,这样可以使地址栏的地址与当前页的地址一样,可以分享成功
  • +
  • 把入口地址保存在本地,等需要获取签名的时候再取出来,注意:sessionStorage.setItem(‘href’,href); 只在刚进入单应用的时候保存!(还没测试,有点low)
  • +
+

写在最后

虽然微信H5方式开发想对来说成本比较低,但是有时候坑开始挺多的,但是微信原生开发又增加了成本,很矛盾,目前能做的就是尽量把踩过的坑都记下来,下次别再跳进去了。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:微信的H5兼容方案

+

文章作者:

+

发布时间:2019年12月31日 - 01:37

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2019/12/31/weichat-h5-compatibility/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/01/Hello-2020/index.html b/ja/2020/01/01/Hello-2020/index.html new file mode 100644 index 0000000000..193e74d5fd --- /dev/null +++ b/ja/2020/01/01/Hello-2020/index.html @@ -0,0 +1,593 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 你好2020 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 你好2020 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

2020元旦伊始

时间过的真快,19年睡了一觉就成了过去的一年,今天是元旦,早上8:00起了床,简单洗漱后我出了门,新年新气象,去年没有坚持的晨跑,今年想要坚持一下,所以一早就出门跑了会儿步,太久没活动了,只是跑了半个小时就气喘吁吁,只好回家,但是即使是这半个小时,我感觉呼吸了新鲜的空气,整个人也精神了不少,回到家坐在阳台上,拿出小王宁送我的极少数手册开始研究了起来,生活还是应该有点仪式感何况是新年伊始,手册中有很多建议,全部看过一遍以后我郑重的拿起了笔写下了我今天的行动计划。但是没有写年度计划,明天上班后跟领导还要谈一谈,谈过之后再定一下年度的计划。今天我的计划就是陪家人过好元旦这一天。

+

元旦执行计划

    +
  1. 写一篇日志
  2. +
  3. 将家人这一天游玩的过程以Vlog的形式记录下来,上传到西瓜视频上。
  4. +
  5. 陪家人逛街,给桐桐买新衣裳。
  6. +
+

执行计划

吃过早饭后大概到了10:30,一家人收拾好以后终于可以出门了,今天有点风,一点点冷不是特别冷,空气还不错,天气听晴朗的,一家人开着车听着歌前往甘井子万达广场,20分钟左右我们就到了万达广场,首先我们先去看了桐桐心心念念的小猫咪,也就是猫咪咖啡馆里的那些小可爱,桐桐可能是受妈妈影响,很喜欢猫咪,但是又有一点点怕。大概看了10分钟的猫咪,我们继续下一站,上四楼给桐桐买新衣裳,桐桐看到满眼的漂亮衣裳很是兴奋,最终她挑选了一套带着向日葵花朵的淡蓝色衣裙,穿上漂亮的新衣裳,平时像个假小子似的桐桐也一下子害羞了起来,让妈妈和奶奶看完后还会让爸爸和爷爷看看,得到了大家的肯定以后,心满意足的买下了它,也不知道是不是试衣服试累了,桐桐嚷嚷着要吃饭,桐桐妈说有一家港式茶餐厅味道不错,之前和同事来过,于是我们就去了4楼的茶餐厅,说实话这是我头一次吃茶餐厅,我还跟我爸妈调侃说,这也算是我们来过一次香港了。去到餐厅时才11点多一点,客还没有上满,还有座位,我们找了个靠里的位置坐了下来,打开菜谱,桐桐妈挑了几样她觉得不错的菜,上菜时间稍微有点慢,期间桐桐有些不耐烦,坐也坐不住,终于等到了吃的上来,可是觉得大多数吃的都太偏甜,爸妈和我都吃不太惯,桐桐吃的还不错,大概吃了半个小时就吃完了,出门的时候门口已经排起了长队,还好来的早一点,不然吃饭都要等了。本打算带着桐桐回家睡个觉,下午要到姥爷家串门,可桐桐说没有逛够,正巧这时看到了一楼有小丑在折气球玩具,我们就下到一楼找小丑玩了,小丑给桐桐折了个贵宾犬,小丑是荣耀手机的员工,可能是元旦策划的活动吧,反正来了就来了,进去看看也好,看了新出的V30手机,感觉并不是很喜欢,店员说有旧机抵值的活动,我问了他我的7p128g还能值多少,他打了几个电话后回复我1400块,哎,三年前7000块买的手机,现在只值1400了,算了还是用着吧,出了荣耀手机店又去了旁边的华为,看了看新出的mate30pro,这个手机是真的不错,当然手机不错价格也美丽,6500+的价格真的是让我不舍得买,还是把钱留下来给桐桐吧。桐桐终于累了,嚷嚷着要走,于是一家人驱车去了姥姥家。

+

姥姥家在西南路,因为平时工作忙很少带桐桐去,这次去桐桐又不认识他们了,开始的时候很拘束,也不说话,一直到吃饭结束才终于愿意开口问好唱歌,因为家里没有电视机,到了姥姥家看到电视机很兴奋,一直在看,今天也算是过了瘾了,亲人就是即使很长时间不见面,但一见面就特别亲的人,饭从5:30一直吃到8:30,聊了工作,聊了生活,聊了困惑,聊了坦然,我终于知道了长辈们的智慧,了解了身体健康的重要性,20年一定要锻炼出一副好的身体,对家人负责。

+

9:30回到了家,心里挺高兴的,新年第一天陪家人一起度过了充实的一天,回到家把今天路上录下来的视频剪成了短视频上传到了西瓜视频,这也是我今年的一个想法,记录我们一家人的幸福生活,如果有粉丝有点播的话还能赚点钱,没有的话也记录了桐桐的成长,算是一点纪念吧。最后写下了这篇日记,写完之后我就要睡觉去了,明天一早就要起床,明天起我就要走路去上班了,为了有一副好身体,加油。

+

今天的目标都完成了,很开心~~

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:你好2020

+

文章作者:

+

发布时间:2020年01月01日 - 10:50

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/01/Hello-2020/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/02/21-Day-Challenge-01/index.html b/ja/2020/01/02/21-Day-Challenge-01/index.html new file mode 100644 index 0000000000..de051f2832 --- /dev/null +++ b/ja/2020/01/02/21-Day-Challenge-01/index.html @@ -0,0 +1,599 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2020年21天主题挑战之灵感篇-Day 1-写下自己期待中的生平 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 2020年21天主题挑战之灵感篇-Day 1-写下自己期待中的生平 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

还记得我19年总结最下面的那张照片么,那个同事送的的本子,对,就是下面这张图,这本子叫做《One In A Million》中文叫做极少数手册,是一个管理时间用的管理手册,英文如果直译的话应该是叫做万里挑一,实话说我并不是很自信,虽然我觉得自己还算可以,但远远达不到万里挑一的程度,所以,我打算好好利用这个本子,让自己尽可能成为百里挑一,千里挑一,最后万里挑一的人。

+

本子里有一个栏目叫做21天主题挑战,21-Day Challenge,每个月21天都会给你列出一些主题,越往后列出来的主题越少,更多的需要个人去补充,我想去挑战这些内容,让自己的生活变得有目标,让自己变得更好。

+ + +

1月份21天主题挑战之灵感Inspiration

+

设定一个挑战主题,让自己更富有创造力,连续21天挑战,让灵感乍现,唤醒天赋。

+
+

第一天 Day 1 写下自己期待中的生平

看到这个题目之后,我闭上了眼睛,努力的回想我自己曾经的梦想是什么,什么时候丢掉了梦想,儿时的梦想,上学时的梦想,长大以后的梦想,现在的梦想,我努力回想了好久好久,儿时的梦想我想起来了,上学时的梦想我想起来了,长大后的梦想我也想起来了,但是我没能一下子找到我现在的梦想,也许现在说梦想有些奢侈,也许也有点贴不上今天的主题期待中的人生,我不太会写文章,但是想到哪里写哪里吧,我先写下我之前的那些个梦想。

+

小时候的梦想-成为科学家

小时候的梦想说出来很简单,但是是那么真诚,真诚就容易打动人,说实话我记性不是太好,已经忘掉了小时候的梦想,所以我问了我妈,我妈笑话我,说我咋不记得小时候那伟大的梦想了,妈妈说我儿时的梦想是当科学家,那会应该是上小学吧,妈妈说学校老师让写作文,标题就是我的梦想,我的作文里写的是成为科学家,这个科学家不像是别人写的,比如造火箭,造大炮又或者是造卫星造原子弹,我的梦想现在说起来与其说是科学家,不如说是药剂师更贴切,妈妈笑着跟我说我当时的梦想是做一个科学家,要研究出来一种长生不老的药,然后让爸爸妈妈吃下以后就可以永远健康年轻,妈妈说那个时候她总逗我,看到路上漂亮的阿姨就会问我,那个阿姨漂亮么,起初我会说漂亮,然后妈妈就会说那让她给你当妈妈好不好,我呢会说不好,妈妈说后来她再问的时候,我就再也不会说我要漂亮阿姨当妈妈的话了,我现在想想也许就是那时,就是因为发现妈妈越来越胖,辛苦操劳后年轻也渐渐不在我才有了这个想法吧,说实话上小学期间我一直认为自己长大能够成为一个科学家,能够造出那长生不老的药让爸妈吃下,这样我就再也不会需要一个漂亮阿姨当妈妈了。我那时希望快快长大,长大了以后我就能做科学家了。

+

上了初中高中以后,也许是进了大城市,也许是长大了,我知道了原来的梦想可能有些遥远,学业的压力让我有些透不过气,从海岛到城市,落后了3年的时间,我通过补习班慢慢追赶,终于能够追到了班级还算靠前的位置,那个时候我的梦想很小,也很简单,那就是考个好大学找个好工作,让爸爸妈妈早点不用那么操劳,早点享福。

+

18岁的梦想 — 找个好工作,让爸妈早点享福

努力的学习,懵懂的感情,初中4年+高中3年的生活,最终我并没能特别出色的考上985/211大学,而是上了离家只有1小时车程的大外,学了计算机加日语英语,学费不便宜,每年16000,说实话没自己工作的时候不知道这16000对于一个下岗自己在家开小卖店的父母是多大的负担,工作后我才终于知道这笔钱有多少。好在原来在爸爸单位的时候接触电脑还算比较早,而我对这个新鲜玩意也算是感兴趣,大学学业上并没给我带来太大的压力,但是我也确实不是个聪明的孩子,日语仅仅过了N2,而英语则一直只是CET-4,现在回想起来,原来应该多努努力,也许现在的生活就会更好了,多想回到过去跟自己说,你要努力啊!一晃4年的大学生活就结束了,这时我终于离开了父母身边,只身去往了大城市北京,开启了我的工作生涯。

+

20岁的人生 — 多挣钱,快速成长

工作了以后,我就像海绵一样不断的吸收着周围的水分,学习工作中需要的技能,学习如何才能让领导器重,学习如何才能快速积累人生的财富,因为我想着,想着能快快独立反哺我的爸妈。20岁,我跟媳妇儿谈了场异地的恋爱,后来她到北京找我,再后来我们就一起回了大连。北京是个大城市,大的有时候让人迷茫,虽然工作机会比较多,但是租房的压力,环境的恶劣,家里的呼唤,最终让我选择了回到我熟悉的城市,另外找了份工资不高的工作,我不满足,我想能成为顶天立地的男子汉,后来我就来到了现在的公司华宇,而且一待就是5年。

+

华宇的职业生涯 — 5年工作,9年经验

网上有个段子我记得,一个人面试拿出简历,工作时间是2年却写着3年工作经验,面试官问他是不是写错了,他答不是,因为加班加出来的。这算是对前公司的吐槽吧。

+

来到大连华宇时,公司还不足120人,我所在的团队还是个交互组,只有三个人,前端的话只有我和另外一个刚毕业的大学生,记得刚来公司的第一年,我参与了140个项目的开发工作,现在想想这个数字有些惊人,但是因为只是些前端切图仔的工作,对我来说感觉难度并不大,不过还是要感谢刚开始这2年,让我的基础非常扎实,再后来公司引入了前后端分离,引入了Vue框架,越来越多的业务要写,数据处理要写,加班成了家常便饭,后面这三年,我几乎没有休过除了元旦和春节的任何法定节假日,每年5天的年假也几乎都没休成,说实话每次加班加到要崩溃时候,我都会想我到底为了啥这么拼命。要不我还是换一家比较轻松一点的工作吧,工资还能涨点。说实话这段时间工作就是生活的全部,每天到家都10点以后,到家老人孩子都睡了,有时候我都睡不着,想着我的生活难道就这样了么,我不甘心,不服输。很多人劝我别那么拼命,别把公司当成自己家的,只是个打工的而已,但是有时候想着下面还有那么多新人信任着我依赖着我,我就没法撒手不管。终于时间到了19年年底,5年来培养的前端团队最后还是没守护住,要拆到各项目团队了,一开始真的难以接受,不过公司领导层已经决定了,作为一线员工只能服从,我希望大家能够把心中的担忧都能消除掉,在新的团队里开启新的篇章,也许会有更好的发展,如果有一天离开了华宇,也要江湖相望,常聚聚。

+

2020年31岁踏上新的旅途

还是感谢小可爱的这个本子,让我能够有主题想想我未来到底想要什么样的生活,说句实在话,我向往不为钱发愁工作的日子,可以在做好自己工作之外的时间里多陪陪家人,带着孩子常出去走走,见见外面的世界,说实话已经31岁了,除了谈恋爱时去找媳妇儿去过江浙苏杭,工作原因去过北京/青海,好像这些年也就去了趟南京,现在的我已经有些不知道什么叫做生活了,我希望自己的空闲时间是可以让兴趣填满的,可以和朋友同事同学多聚聚,但是怕打扰大家我又从来不会主动去约别人,小时候没有听从爸妈的话培养自己的兴趣,现在有些后悔,真的空闲下来都不知道该用什么填满这时间,都是躺在床上刷手机,看电视,我不喜欢这样的生活,但是我不知道该过怎么样的生活。看到那些会做饭的视频,做顿丰盛的饭菜给家人吃,看到他们的幸福笑脸,我想做那样的事;看到带着家人过着一路向前增长见闻的旅途生活,我想也尝试一下那样的人生,我感觉为了钱我被束缚在了工作上,2020年我想过一种不为工作所累,不为钱所累,能够享受生活,陪伴家人的生活,保重身体,每天快快乐乐的,多发现生活中的美好,感恩,努力,成长。期待自己成为更好的人。

+

写在最后

我所期待中的生平,成为一个不被工作强迫,不被金钱所累,爱家顾家,孝敬父母,人缘好朋友多,兴趣广泛,感恩的人。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:2020年21天主题挑战之灵感篇-Day 1-写下自己期待中的生平

+

文章作者:

+

发布时间:2020年01月02日 - 12:24

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/02/21-Day-Challenge-01/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/02/FE-guide-Closure/index.html b/ja/2020/01/02/FE-guide-Closure/index.html new file mode 100644 index 0000000000..ef418eb75f --- /dev/null +++ b/ja/2020/01/02/FE-guide-Closure/index.html @@ -0,0 +1,602 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 闭包 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 闭包 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

闭包Closure

闭包的定义很简单:函数 A 返回了一个函数 B ,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。

+
1
2
3
4
5
6
7
8
9
// js代码

function A() {
let a = 1
function B() {
console.log(a)
}
return B
}
+

你是否会疑惑,为什么函数 A 已经弹出调用栈了,为什么函数 B 还能引用到函数 A 中的变量。因为函数 A 中的变量这时候是存储在堆上的。现在的 JS 引擎可以通过逃逸分析辨别出哪些变量需要存储在堆上,哪些需要存储在栈上。

+

经典面试题,循环中使用闭包解决 var 定义函数的问题

+
1
2
3
4
5
6
7
// js代码

for ( var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
+

首先因为 setTimeout 是个异步函数,所有会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。

+

解决办法两种,第一种使用闭包

+
1
2
3
4
5
6
7
8
9
// js代码

for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
+

第二种就是使用 setTimeout 的第三个参数(附加参数,传给 setTimeout 中的函数)

+
1
2
3
4
5
6
7
// js代码

for ( var i=1; i<=5; i++) {
setTimeout( function timer(j) {
console.log( j );
}, i*1000, i);
}
+ +

第三种就是使用 let 定义 i

+
1
2
3
4
5
6
7
// js代码

for ( let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
+

因为对于 let 来说,他会创建一个块级作用域,相当于

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// js代码

{ // 形成块级作用域
let i = 0
{
let ii = i
setTimeout( function timer() {
console.log( ii );
}, i*1000 );
}
i++
{
let ii = i
}
i++
{
let ii = i
}
...
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 闭包

+

文章作者:

+

发布时间:2020年01月02日 - 13:32

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/02/FE-guide-Closure/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/02/FE-guide-dataType/index.html b/ja/2020/01/02/FE-guide-dataType/index.html new file mode 100644 index 0000000000..49765b604d --- /dev/null +++ b/ja/2020/01/02/FE-guide-dataType/index.html @@ -0,0 +1,655 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 数据类型、类型判断、类型转换、类型比较 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 数据类型、类型判断、类型转换、类型比较 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

+

温故而知新,可以为师矣。
———————— 论语

+
+

这篇文章主要是把我看过的书中的知识点,还有其他人整理的面试题进行整理,巩固自己的前端开发理论知识,希望其他看到这篇文章的人也能有所帮助。

+ + +

JS知识点

+

当前市场中,如何区分一个好一点的前端开发和一般的前端开发,主要看的就是js能力的差距,好的前端开发,JS玩的转,不仅仅是框架玩的好,还要JS的基础扎实,只有基础与新技术都玩的6的前端开发才是好前端。

+
+

上面这句话不是什么名人或者某位知名前端大拿说的,这是我在公司这几年面试招聘的过程中真真切切总结感受的。
所以可以说得JS者得高级前端,所以下面也主要是写JS的相关知识点。

+

JavaScript内置数据类型 — 数据类型

无论学什么语言最重要最基础的就是数据类型,《JavaScript权威指南》这本书中详细的介绍了JS中的数据类型,下面总结一下。

+

JavaScript中数据类型分为两大类共七种内置数据类型,其一是6种基本类型,其二是1种引用类型,我发现在面试过程中好多面试者都不会先说有两大类,而是会直接蹦出数字类型、字符串类型。。。对象、数组也会被并排放在一起,这其实不是个掌握知识点的好方法,应该先把数据类型分成上面说的基本类型引用类型这样两个大类之后,再看看这两大类中有什么其他的子类型,在记忆其他子类型之前我觉得应该先了解一下什么是基本类型引用类型,实际上基本类型和引用类型的主要区别是存储的区别,基本类型在栈中,而引用类型的话,引用数据的地址存储在栈中,而对象本身是存储在堆中,引用的数据地址是个16进制的数据值,它就像一把钥匙让你能够找到宝藏在什么地方。这就是基本数据类型和引用类型的区别了。

+

那么如何记住有哪些基本数据类型和引用数据类型呢,实际上只要记住了6个基本数据类型,其他的都是引用数据类型,而所有的引用数据类型的祖宗都是Object,所以引用数据类型实际上只有Object一个,那么像是Array等其他子类型,都是Object的孩子,不跟Object在一个级别上。

+

基本数据类型有哪些呢?其实挺好记的,数字,字符串,这两个一个像温柔的文学少女(string),一个像有点精于算计的男生(number),还有一个布尔类型(boolen)他像是班级里正义感爆棚的人,只论对错;另外还有个差生,没头脑似的未定义(Undefined)还有一个失了忆记不起来自己是谁的空(Null),最后还有一个新加入的插班生,总是带着口罩的标志符号(Symbol),这些人构成了这个班级的所有学生,也就是全部的基本类型,那么引用类型的对象Object呢就像及了漂亮爱化妆的班主任老师,有好多副面孔。不知道大家有没有看过一个动漫叫做《黑塔利亚》,他就是把国家都拟人化了,有了各自的性格,我很喜欢看,我觉得这些数据类型也各有各的特点,像这些国家一样,好了脑洞有点挖深了,有人会说我不就是这么几个简单的数据类型嘛,硬记下来不就好了,但是知识总有你硬记不下来的时候,最好的方法也是速记领域最为常用的方法,就是把你不熟悉的知识与你感兴趣的画面或者既往的知识串联起来,这样就能达到很好的记忆,如果你不喜欢动漫(怎么会有不喜欢动漫的人!!!),可以试试用其他的方法记,当然你如果硬要死记硬背那我也没办法,我继续开我的脑洞。
如果你是学过Java开发的同学(如果是计算机专业出来的,应该都或多或少学过,非计算机专业的我也不知道说啥了。。),数字在Java中是分成 byte/short/int/long 的,但是在Js中没有那么多,就一个Number,它是浮点类型,基于 IEEE 754标准实现,刚才我不是说了Number是个精于算计的男生,精于算计就是说他分毫不差,这样浮点型就很好的记了下来,这个754的标准可以不记,如果非要记的话,你可以记成他是自称IEEE 754团体的成员。最后Number身后还跟着一个小弟,叫做NaN,他虽然是Number的小弟,但他总是说话不算数,自己说过什么都不承认。所以NaN!=NaN。
老师是个爱化妆的老师,而这些学生也不是普通的学生,在学校他们是老老实实的基本类型,放了学之后一打扮,他们就各有了其他的能力,这个过程叫做装箱,具体的后面再说。(好了快回到现实吧你!)

+

如果基础数据是字面量类型,那么他们就像是在上课的学生,只是学生而已,而当他们调用方法时,他们就成了下课后各种技能都有的新新人类,这个过程有时候是显示的,就像是有些学生喜欢大声嚷嚷,而更多的是你不自觉中就用到了装箱操作,是Js引擎提供的能力,就像是有些闷骚的学生一样。

+
1
2
3
// js代码
let aNumber = 111 // 这只是字面量,不是 number 类型
aNumber.toString() // 装箱操作自动转化成数字对象,使用时候才会转换为对象类型
+

对象有个深浅拷贝的知识点必须要会,对象因为是引用数据类型,在栈中存储的是地址,当用另一个变量接收了之前的变量,那么就好像把钥匙复制了一把,两把钥匙开的还是同一个门,而深拷贝呢就像是照着原来的样板间又造了一个一模一样的房间,这两个房间长得一样,但就是两个房间,钥匙自然也是不一样的,所以呢,当往房间里搬家具的时候,浅拷贝搬进去的是一个房间,所以两把钥匙打开之后看到的都是多了家具,而深拷贝的话,我只是往样板间搬了家具,所以照着装修的房间里是不可能有的,这就是浅拷贝原数据会受影响,而深拷贝不会。

+
1
2
3
4
5
6
// js代码

let a = { name: 'FE' }
let b = a
b.name = 'EF'
console.log(a.name) // EF 浅拷贝原数据会受影响
+

内置数据类型检测 Typeof — 类型判断

+

typeof 对于基本类型,除了 null 都可以显示正确的类型

+
+

typeof 就像是学校的教导主任一样,他有着一双火眼金睛,不管是哪个同学,穿了什么样的衣服,他一问就能问出来这个学生是谁,大家都怕他,但是Null因为失忆了,他也不知道教导主任是谁,所以typeof就拿他也没办法,因为他不怕教导主任,教导主任甚至会以为他是老师呢。

+
1
2
3
4
5
6
7
8
9
10
// js代码

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof b // b 没有声明,但是还会显示 undefined

typeof null // 'object' 这是JS中的bug
+ +
+

typeof 对于对象,除了函数都会显示 object

+
+
1
2
3
4
5
// js代码

typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
+ +
+

知识扩展:为什么会出现这种情况呢?因为在 JS 的最初版本中,使用的是 32 位系统,为了性能考虑使用低位存储了变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。

+
+

如果我们想获得一个变量的正确类型,可以通过 Object.prototype.toString.call(xx)。这样我们就可以获得类似 [object Type] 的字符串。

+
+

小知识扩展

+
+
1
2
3
4
5
6
7
8
9
10
let a
// 我们也可以这样判断 undefined
a === undefined
// 但是 undefined 不是保留字,能够在低版本浏览器被赋值
let undefined = 1
// 这样判断就会出错
// 所以可以用下面的方式来判断,并且代码量更少
// 因为 void 后面随便跟上一个组成表达式
// 返回就是 undefined
a === void 0
+ +

类型转换

转Boolean

一句话可以概括

+
+

在条件判断时,除了 undefinednullfalseNaN''0-0,其他所有值都转为 true,包括所有对象。

+
+

从上面这段话可以看出来,undefinednull 是基本类型之二,而false是布尔类型的假值,NaN是数字类型的无效值,''是字符串类型的空值,而0-0都是数字类型的零值,可以看到,除了0-0有些特殊,除了插班生Symbol,剩下的都是基本类型的假值,由此实际上就很好记了,有时候数字这个容易忘,但是记住“非0既真”这句话就好了。

+

对象转基本类型

对象在转换基本类型时,首先是先会调用ToPrimitive(原始类型),如果有hint参数调用对应的的类型方法,如果没有那默认先会调用 valueOf 然后调用 toString。如果返回了基本类型,结束。如果都没返回,那么Error但是注意这两个方法你是可以重写的。

+
+

Symbol.toPrimitive 是一个内置的 Symbol 值,它是作为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数。(来自MDN的解释)
需要解释的关键机制是ToPrimitive函数。该函数是将任意值转换为相应的基本类型值。如果输入的就是一个基本类型值,那么将不做修改,被直接返回。如果值是非基本类型值,它将调用内部方法 [[DefaultValue]] 为对象找到一个默认值。
[[DefaultValue]]是每一个对象的内部属性。该方法需要一个可选的参数hint,值是Number或String。如果没有提供hint,则默认为Number(除非该对象是Date,在这种情况下默认为String)。然后将调用toString和valueOf去寻找基本类型值。在这里hint就起到作用了。如果hint参数值为Number,valueOf将先被调用,如果hint是String的话,则toString被先调用。
[[DefaultValue]] 返回的值一定是基本类型值。如果不是,一个TypeError 将会被抛出。这就意味着为了在这种情况下有意义,toString和valueOf应该返回基本类型值。

+
+

四则运算符

+

只有当加法运算时,其中一方是字符串类型,就会把另一个也转为字符串类型。其他运算只要其中一方是数字,那么另一方就转为数字。并且加法运算会触发三种类型转换:将值转换为原始值,转换为数字,转换为字符串。

+
+
1
2
3
4
5
6
7
8
// js代码

1 + '1' // '11' 这里会是面试陷阱
2 * '2' // 4
[1, 2] + [2, 1] // '1,22,1'
// [1, 2].toString() -> '1,2'
// [2, 1].toString() -> '2,1'
// '1,2' + '2,1' = '1,22,1'
+

*面试陷阱题之 ++ *

+
1
2
3
4
5
6
7
8
9
// 问表达式 'a' + + 'b' 返回结果是什么?
// 答案是 'aNaN'
'a' + + 'b' -> // 一元运算符优先级高
'a' + (+ 'b') -> // +'b'转数字类型,非有效结果是NaN
'a' + NaN.toString() -> // NaN调用toString()成字符串'NaN'
'aNaN' // 字符串接到一起后'aNaN'

// 类似题 '1' + + '4' 返回结果是什么
// 其实就是'4' -> 4 -> '4' 最后还是'14'
+ +

== 操作符 — 类型比较

+

相等运算符的运算规则如下:
1、如果两个值类型相同,进行 === 比较。(这个非常好理解,就不多说了)
(1)数字比大小
(2)字符串就通过 unicode 字符索引来比较
2、如果两个值类型不同,他们可能相等。根据下面规则进行类型转换再比较:
(1)如果一个是null、一个是undefined,那么[相等]。 // 这个有点特殊需要单独记
(2)如果任一值是字符串,另一个值是数值,在比较相等性之前先将字符串转换为数值;即是调用Number()函数。
(3)如果任一值时布尔值,则在比较相等性之前先将其转换为数值,即是调用Number()函数。
(4)如果一个是对象,另一个是数值或字符串,把对象转换成基础类型的值再比较。对象转换成基础类型,利用它的toString或者valueOf方法。 (js核心内置类ToPrimitive,会尝试valueOf先于toString;例外的是Date,Date利用的是toString转换。)

+
+

1不说,2(1)的话单独记,其他基本类型转数字比较,引用性数据类型调用ToPrimitive转换成基本数据类型

+

首先还是一道面试题

+
1
2
// [] == ![] 结果是什么
[] == ![] // true
+

为什么呢?我们来解析一下,

+
    +
  1. 首先我们看一下右侧,![]肯定是要先转换成boolen类型了吧,那么[]的布尔类型是什么呢,上面转换布尔的时候我们说过,除了那五个基本类型的假值以及正负0之外,都是真值,所以[] -> true ![] -> false
  2. +
  3. ToPrimitive(false)->0右边的值得出了数值类型的原始值
  4. +
  5. 看左边ToPrimitive([])->[].toString()->''
  6. +
  7. Number('')->0
  8. +
  9. 比较左右 0 == 0 -> true
  10. +
+

最后

到这里JS的内置数据类型及类型的转换和比较就讲完了,相信大家看过以后一定会记得住的
PS:突然好像学画漫画,《JS数据结构们》,一定大火,哈哈哈😂

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 数据类型、类型判断、类型转换、类型比较

+

文章作者:

+

发布时间:2020年01月02日 - 08:22

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/02/FE-guide-dataType/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/02/FE-guide-instanceof/index.html b/ja/2020/01/02/FE-guide-instanceof/index.html new file mode 100644 index 0000000000..d1cd05e37e --- /dev/null +++ b/ja/2020/01/02/FE-guide-instanceof/index.html @@ -0,0 +1,590 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- instanceof | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- instanceof +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

instanceof

instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype
举例:

+
1
a instanceof Object
+

判断 Objectprototype 是否在 a 的原型链上。

+

我们也可以试着实现一下 instanceof

+
1
2
3
4
5
6
7
8
9
10
11
12
function myInstanceof(left, right) { // left 表示左表达式,right 表示右表达式
let prototype = right.prototype // 获得类型的原型
left = left.__proto__ // 获得对象的原型

while (true) { // 判断对象的类型是否等于类型的原型
if (left === null)
return false
if (prototype === left)
return true
left = left.__proto__
}
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- instanceof

+

文章作者:

+

发布时间:2020年01月02日 - 12:29

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/02/FE-guide-instanceof/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/02/FE-guide-new/index.html b/ja/2020/01/02/FE-guide-new/index.html new file mode 100644 index 0000000000..c5d723c17d --- /dev/null +++ b/ja/2020/01/02/FE-guide-new/index.html @@ -0,0 +1,608 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- new | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- new +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

new 一个对象的过程

    +
  1. 新生成了一个对象
  2. +
  3. 链接到原型
  4. +
  5. 绑定 this
  6. +
  7. 返回新对象
  8. +
+

在调用 new 的过程中会发生以上四件事情,我们也可以试着来自己实现一个 new

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

function create() {
// 创建一个空的对象
let obj = new Object()
// 获得构造函数
let Con = [].shift.call(arguments)
// 链接到原型
obj.__proto__ = Con.prototype
// 绑定 this,执行构造函数
let result = Con.apply(obj, arguments)
// 确保 new 出来的是个对象
return typeof result === 'object' ? result : obj
}
+ +

对于实例对象来说,都是通过 new 产生的,无论是 function Foo() 还是 let a = { b : 1 }

+

对于创建一个对象来说,更推荐使用字面量的方式创建对象(无论性能上还是可读性)。因为你使用 new Object() 的方式创建对象需要通过作用域链一层层找到 Object,但是你使用字面量的方式就没这个问题。

+
1
2
3
4
// js代码

function Foo() {} // function 就是个语法糖,内部等同于 new Function()
let a = { b: 1 } // 这个字面量内部也是使用了 new Object()
+

对于 new 来说,还需要注意下运算符优先级。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

function Foo() {
return this;
}
Foo.getName = function () {
console.log('1');
};
Foo.prototype.getName = function () {
console.log('2');
};

new Foo.getName(); // -> 1
new Foo().getName(); // -> 2
+ +

从上图可以看出,new Foo() 的优先级大于 new Foo ,所以对于上述代码来说可以这样划分执行顺序

+
1
2
3
4
// js代码

new (Foo.getName());
(new Foo()).getName();
+ +
    +
  • 对于第一个函数来说,先执行了 Foo.getName() ,所以结果为 1;
  • +
  • 对于后者来说,先执行 new Foo() 产生了一个实例,然后通过原型链找到了 Foo 上的 getName 函数,所以结果为 2。
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- new

+

文章作者:

+

发布时间:2020年01月02日 - 20:07

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/02/FE-guide-new/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/02/FE-guide-prototype/index.html b/ja/2020/01/02/FE-guide-prototype/index.html new file mode 100644 index 0000000000..3674a03bcf --- /dev/null +++ b/ja/2020/01/02/FE-guide-prototype/index.html @@ -0,0 +1,646 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 原型 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 原型 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

原型

yuanxing.png
每个函数都有 prototype 属性,除了 Function.prototype.bind(),该属性指向原型对象,简称原型prototype原型对象里的constructor指向构造函数本身。
每个对象都有 __proto__ 属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 _proto_ 来访问。
对象可以通过 __proto__ 来寻找不属于该对象的属性,__proto__ 将对象连接起来组成了原型链

+ +

总结

+

prototype 原型对象

什么是原型?

每个函数都具有 prototype 属性,它被默认成一个对象,即原型对象
首先来介绍下 prototype 属性。这是一个显式原型属性,只有函数才拥有该属性。基本上所有函数都有这个属性,但是也有一个例外

+
1
2
// js代码
let fun = Function.prototype.bind()
+

如果你以上述方法创建一个函数,那么可以发现这个函数是不具有 prototype 属性的。

+

prototype 如何产生的

当我们声明一个函数时,这个属性就被自动创建了。

+
1
2

function Foo() {}
+

并且这个属性的值是一个对象(也就是原型),只有一个属性 constructor
constructor 对应着构造函数,也就是 Foo

+

什么是原型链?

当对象使用属性时,先在自身找,有就直接用,没有就沿着proto这条链往上找,直到 Object 原型的位置,有就返回相应的值,没有就返回 underfined。

+

constructor 构造函数

什么是构造函数?

任何一个函数,只要被 new 操作符使用,就可以是一个构造函数(构造函数建议以大写开头)
另外,在 JavaScript 的内置对象中,所有的函数对象都是 Function 构造函数的实例,比如:Object、Array等

+

constructor 是一个公有且不可枚举的属性。一旦我们改变了函数的 prototype ,那么新对象就没有这个属性了(当然可以通过原型链取到 constructor)。

+

那么你肯定也有一个疑问,这个属性到底有什么用呢?其实这个属性可以说是一个历史遗留问题,在大部分情况下是没用的,在我的理解里,我认为他有两个作用:

+
    +
  1. 让实例对象知道是什么函数构造了它
  2. +
  3. 如果想给某些类库中的构造函数增加一些自定义的方法,就可以通过 xx.constructor.method 来扩展
  4. +
+

_proto_

这是每个对象都有的隐式原型属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 _proto_ 来访问。

+

因为在 JS 中是没有类的概念的,为了实现类似继承的方式,通过 _proto_ 将对象和原型联系起来组成原型链,得以让对象可以访问到不属于自己的属性。

+

实例对象的 _proto_ 如何产生的

从上图可知,当我们使用 new 操作符时,生成的实例对象拥有了 _proto_ 属性。

+
1
2
3
4
function Foo() {}
// 这个函数是 Function 的实例对象
// function 就是一个语法糖
// 内部调用了 new Function(...)
+

所以可以说,在 new 的过程中,新对象被添加了 _proto_ 并且链接到构造函数的原型上。

+

new 的过程

    +
  1. 新生成了一个对象
  2. +
  3. 链接到原型
  4. +
  5. 绑定 this
  6. +
  7. 返回新对象
  8. +
+

在调用 new 的过程中会发生以上四件事情,我们也可以试着来自己实现一个 new

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person( name ){ 
this.name = name;
};
Person.prototype.getName = function(){
return this.name;
};

function create() {
let obj = new Object() // 从 Object.prototype 上克隆一个空的对象
let Con = [].shift.call(arguments) // 获取外部传入的构造器,此例是 Person
obj.__proto__ = Con.prototype // 指向正确的原型,链接到原型
let result = Con.apply(obj, arguments) // 绑定 this,执行构造函数,借用外部传入的构造器给 obj 设置属性
return typeof result === 'object' ? result : obj // 确保 new 出来的是个对象
}

create(Person,'lixg')
+

对于实例对象来说,都是通过 new 产生的,无论是 function Foo() 还是 let a = { b : 1 }

+

对于创建一个对象来说,更推荐使用字面量的方式创建对象。因为你使用 new Object() 的方式创建对象需要通过作用域链一层层找到 Object ,但是你使用字面量的方式就没这个问题。

+
1
2
function Foo() {} // function 就是个语法糖,内部等同于 new Function()
let a = { b: 1 } // 这个字面量内部也是使用了 new Object()
+ +

Function.proto === Function.prototype

对于对象来说,xx.__proto__.contrcutor 是该对象的构造函数,但是在图中我们可以发现 Function.__proto__ === Function.prototype,难道这代表着 Function 自己产生了自己?

+

答案肯定是否认的,要说明这个问题我们先从 Object 说起。

+

从图中我们可以发现,所有对象都可以通过原型链最终找到 Object.prototype ,虽然 Object.prototype 也是一个对象,但是这个对象却不是 Object 创造的,而是引擎自己创建了 Object.prototype 。所以可以这样说,所有实例都是对象,但是对象不一定都是实例。

+

接下来我们来看 Function.prototype 这个特殊的对象,如果你在浏览器将这个对象打印出来,会发现这个对象其实是一个函数。

+

我们知道函数都是通过 new Function() 生成的,难道 Function.prototype 也是通过 new Function() 产生的吗?答案也是否定的,这个函数也是引擎自己创建的。首先引擎创建了 Object.prototype ,然后创建了 Function.prototype ,并且通过 __proto__ 将两者联系了起来。这里也很好的解释了上面的一个问题,为什么 let fun = Function.prototype.bind() 没有 prototype 属性。因为 Function.prototype 是引擎创建出来的对象,引擎认为不需要给这个对象添加 prototype 属性。

+

所以我们又可以得出一个结论,不是所有函数都是 new Function() 产生的。
有了 Function.prototype 以后才有了 function Function() ,然后其他的构造函数都是 function Function() 生成的。

+

现在可以来解释 Function.__proto__ === Function.prototype 这个问题了。因为先有的 Function.prototype 以后才有的 function Function() ,所以也就不存在鸡生蛋蛋生鸡的悖论问题了。对于为什么 Function.__proto__ 会等于 Function.prototype ,个人的理解是:其他所有的构造函数都可以通过原型链找到 Function.prototype ,并且 function Function() 本质也是一个函数,为了不产生混乱就将 function Function()__proto__ 联系到了 Function.prototype 上。

+

总结

    +
  • Object 是所有对象的爸爸,所有对象都可以通过 __proto__ 找到它
  • +
  • Function 是所有函数的爸爸,所有函数都可以通过 __proto__ 找到它
  • +
  • Function.prototypeObject.prototype 是两个特殊的对象,他们由引擎来创建
  • +
  • 除了以上两个特殊对象,其他对象都是通过构造器 new 出来的
  • +
  • 函数的 prototype 是一个对象,也就是原型
  • +
  • 对象的 __proto__ 指向原型, __proto__ 将对象和原型连接起来组成了原型链
    总结
  • +
+

归纳

ES 把对象定义为:“无序属性的集合,其属性可以包含基本值,对象和函数”。
严格来讲,这就相当于说对象是一组没有特定顺序的值。ES 中的构造函数可以用来创建特定类型的对象,用来在创建对象时初始化对象。它的特点是,一般为大写字母开头,使用 new 操作符来实例化对象,比如:

+
1
2
3
4
function Person() {}
var person = new Person();
person.name = "Kevin";
console.log(person.name); // Kevin
+ +

Person 就是构造函数, person 就是对象。对于对象而言,每个 JS 对象一定对应一个原型对象,并从原型对象继承属性和方法。对象 __proto__ 属性的值就是它所对应的原型对象。对象的 __proto__ 指向自己构造函数的 prototype 。所以对象的原型链就是 obj.__proto__.proto__.... 。对于函数而言,只有函数才有 prototype 属性, Person.prototype 是一个对象,并且有两个属性, 一个是 constructor 指向其构造函数 Person , 一个是 __proto__ 属性:是一个对象,指向上一层的原型。原型链的尽头是 Object.prototype 。所有对象均从 Object.prototype 继承属性。Function.prototypeFunction.__proto__ 为同一对象。Object/Array/String 等等构造函数本质上和 Function 一样,均继承于 Function.prototypeFunction.prototype 直接继承 Object.prototype 。这里的 ObjectFunction 有点鸡和蛋的问题,总结:先有 Object.prototype(原型链顶端),Function.prototype 继承 Object.prototype 而产生,最后,FunctionObject 和其它构造函数继承 Function.prototype 而产生。属性查找时,先在对象自己上找,找不到才会一步步根据原型链往上找。
继承

+

关联阅读

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain
说说原型(prototype)、原型链和原型继承

+

扩展阅读

继承

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 原型

+

文章作者:

+

发布时间:2020年01月02日 - 11:22

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/02/FE-guide-prototype/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/02/FE-guide-this/index.html b/ja/2020/01/02/FE-guide-this/index.html new file mode 100644 index 0000000000..49c5ea1b90 --- /dev/null +++ b/ja/2020/01/02/FE-guide-this/index.html @@ -0,0 +1,591 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- this | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- this +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

this

this 是很多人会混淆的概念,但是其实他一点都不难,你只需要记住几个规则就可以了。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// js代码

function foo() {
console.log(this.a)
}
var a = 1
foo()

var obj = {
a: 2,
foo: foo
}
obj.foo()

// 以上两者情况 `this` 只依赖于调用函数前的对象,优先级是第二个情况大于第一个情况

// 以下情况是优先级最高的,`this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向
var c = new foo()
c.a = 3
console.log(c.a)

// 还有种就是利用 `call` , `apply` , `bind` 改变 `this` ,这个优先级仅次于 `new`
+

以上几种情况明白了,很多代码中的 this 应该就没什么问题了,下面让我们看看箭头函数中的 this

+
1
2
3
4
5
6
7
8
9
10
// js代码

function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())
+

箭头函数其实是没有 this 的,这个函数中的 this 只取决于他外面的第一个不是箭头函数的函数的 this 。在这个例子中,因为调用 a 符合前面代码中的第一个情况,所以 thiswindow。并且 this 一旦绑定了上下文,就不会被任何代码改变。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- this

+

文章作者:

+

发布时间:2020年01月02日 - 12:59

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/02/FE-guide-this/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2020/01/02/FE-guide-\346\211\247\350\241\214\344\270\212\344\270\213\346\226\207/index.html" "b/ja/2020/01/02/FE-guide-\346\211\247\350\241\214\344\270\212\344\270\213\346\226\207/index.html" new file mode 100644 index 0000000000..a0cea664c1 --- /dev/null +++ "b/ja/2020/01/02/FE-guide-\346\211\247\350\241\214\344\270\212\344\270\213\346\226\207/index.html" @@ -0,0 +1,610 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 执行上下文 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 执行上下文 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

执行上下文

当执行 JS 代码时,会产生三种执行上下文

+
    +
  • 全局执行上下文
  • +
  • 函数执行上下文
  • +
  • eval 执行上下文
  • +
+

每个执行上下文中都有三个重要的属性

+
    +
  • 变量对象( VO ),包含变量、函数声明和函数的形参,该属性只能在全局上下文中访问
  • +
  • 作用域链( JS 采用词法作用域,也就是说变量的作用域是在定义时就决定了)
  • +
  • this
    1
    2
    3
    4
    5
    6
    7
    // js代码

    var a = 10
    function foo(i) {
    var b = 20
    }
    foo()
    +对于上述代码,执行栈中有两个上下文:全局上下文和函数 foo 上下文。
    1
    2
    3
    4
    5
    6
    // js代码

    stack = [
    globalContext,
    fooContext
    ]
    +对于全局上下文来说, VO 大概是这样的
    1
    2
    3
    4
    5
    6
    7
    // js代码

    globalContext.VO === globe
    globalContext.VO = {
    a: undefined,
    foo: <Function>,
    }
    +对于函数 foo 来说, VO 不能访问,只能访问到活动对象( AO
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // js代码

    fooContext.VO === foo.AO
    fooContext.AO {
    i: undefined,
    b: undefined,
    arguments: <>
    }
    // arguments 是函数独有的对象(箭头函数没有)
    // 该对象是一个伪数组,有 `length` 属性且可以通过下标访问元素
    // 该对象中的 `callee` 属性代表函数本身
    // `caller` 属性代表函数的调用者
    +对于作用域链,可以把它理解成包含自身变量对象和上级变量对象的列表,通过 [[Scope]] 属性查找上级变量
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    fooContext.[[Scope]] = [
    globalContext.VO
    ]
    fooContext.Scope = fooContext.[[Scope]] + fooContext.VO
    fooContext.Scope = [
    fooContext.VO,
    globalContext.VO
    ]
    +接下来让我们看一个老生常谈的例子, var
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    b() // call b
    console.log(a) // undefined

    var a = 'Hello world'

    function b() {
    console.log('call b')
    }
    +想必以上的输出大家肯定都已经明白了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行上下文时,会有两个阶段。第一个阶段是创建的阶段(具体步骤是创建 VO ), JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined ,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用。
  • +
+

在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

b() // call b second

function b() {
console.log('call b fist')
}
function b() {
console.log('call b second')
}
var b = 'Hello world'
+

var 会产生很多错误,所以在 ES6 中引入了 letlet 不能在声明前使用,但是这并不是常说的 let 不会提升, let 提升了声明但没有赋值,因为临时死区导致了并不能在声明前使用。

+

对于非匿名的立即执行函数需要注意以下一点

+
1
2
3
4
5
6
7
// js代码

var foo = 1
(function foo() {
foo = 10
console.log(foo)
}()) // -> ƒ foo() { foo = 10 ; console.log(foo) }
+

因为当 JS 解释器在遇到非匿名的立即执行函数时,会创建一个辅助的特定对象,然后将函数名称作为这个对象的属性,因此函数内部才可以访问到 foo ,但是这个值又是只读的,所以对它的赋值并不生效,所以打印的结果还是这个函数,并且外部的值也没有发生更改。

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

specialObject = {};

Scope = specialObject + Scope;

foo = new FunctionExpression;
foo.[[Scope]] = Scope;
specialObject.foo = foo; // {DontDelete}, {ReadOnly}

delete Scope[0]; // remove specialObject from the front of scope chain
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 执行上下文

+

文章作者:

+

发布时间:2020年01月02日 - 13:18

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/02/FE-guide-%E6%89%A7%E8%A1%8C%E4%B8%8A%E4%B8%8B%E6%96%87/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/03/FE-guide-Generator/index.html b/ja/2020/01/03/FE-guide-Generator/index.html new file mode 100644 index 0000000000..2c06179fed --- /dev/null +++ b/ja/2020/01/03/FE-guide-Generator/index.html @@ -0,0 +1,589 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- Generator 生成器 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- Generator 生成器 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Generator 实现

GeneratorES6 中新增的语法,和 Promise 一样,都可以用来异步编程

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

// 使用 * 表示这是一个 Generator 函数
// 内部可以通过 yield 暂停代码
// 通过调用 next 恢复执行
function* test() {
let a = 1 + 2;
yield 2;
yield 3;
}
let b = test();
console.log(b.next()); // > { value: 2, done: false }
console.log(b.next()); // > { value: 3, done: false }
console.log(b.next()); // > { value: undefined, done: true }
+

从以上代码可以发现,加上 * 的函数执行后拥有了 next 函数,也就是说函数执行后返回了一个对象。每次调用 next 函数可以继续执行被暂停的代码。以下是 Generator 函数的简单实现

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// js代码

// cb 也就是编译过的 test 函数
function generator(cb) {
return (function() {
var object = {
next: 0,
stop: function() {}
};

return {
next: function() {
var ret = cb(object);
if (ret === undefined) return { value: undefined, done: true };
return {
value: ret,
done: false
};
}
};
})();
}
// 如果你使用 babel 编译后可以发现 test 函数变成了这样
function test() {
var a;
return generator(function(_context) {
while (1) {
switch ((_context.prev = _context.next)) {
// 可以发现通过 yield 将代码分割成几块
// 每次执行 next 函数就执行一块代码
// 并且表明下次需要执行哪块代码
case 0:
a = 1 + 2;
_context.next = 4;
return 2;
case 4:
_context.next = 6;
return 3;
// 执行完毕
case 6:
case "end":
return _context.stop();
}
}
});
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- Generator 生成器

+

文章作者:

+

发布时间:2020年01月03日 - 03:05

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-Generator/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/03/FE-guide-Map-FlatMap-Reduce/index.html b/ja/2020/01/03/FE-guide-Map-FlatMap-Reduce/index.html new file mode 100644 index 0000000000..4d83ff93b8 --- /dev/null +++ b/ja/2020/01/03/FE-guide-Map-FlatMap-Reduce/index.html @@ -0,0 +1,602 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- Map、FlatMap 和 Reduce | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- Map、FlatMap 和 Reduce +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Map、FlatMap 和 Reduce

Map

Map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后 append 到新的数组中。

+
1
2
3
// js代码

[1, 2, 3].map((v) => v + 1) // -> [2, 3, 4]
+

Map 有三个参数,分别是当前索引元素索引原数组

+

FlatMap

FlatMapmap 的作用几乎是相同的,但是对于多维数组来说,会将原数组降维。可以将 FlatMap 看成是 map + flatten ,目前该函数在浏览器中还不支持。

+
1
2
3
// js代码

[1, [2], 3].flatMap((v) => v + 1) // -> [2, 3, 4]
+

如果想将一个多维数组彻底的降维,可以这样实现

+
1
2
3
4
5
const flattenDeep = (arr) => Array.isArray(arr)
? arr.reduce( (a, b) => [...a, ...flattenDeep(b)] , [])
: [arr]

flattenDeep([1, [[2], [3, [4]], 5]])
+ +

Reduce 升序执行

Reduce 作用是数组中的值组合起来,最终得到一个值
reduce() 方法对数组中的每个元素执行一个由您提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。

+

reducer 函数接收4个参数:

+
    +
  • Accumulator (acc) (累计器)
  • +
  • Current Value (cur) (当前值)
  • +
  • Current Index (idx) (当前索引)
  • +
  • Source Array (src) (源数组)
    您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
// js代码

function a() {
console.log(1);
}

function b() {
console.log(2);
}

[a, b].reduce((a, b) => a(b()))
// -> 2 1
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- Map、FlatMap 和 Reduce

+

文章作者:

+

发布时间:2020年01月03日 - 03:35

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-Map-FlatMap-Reduce/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/03/FE-guide-Module/index.html b/ja/2020/01/03/FE-guide-Module/index.html new file mode 100644 index 0000000000..da680358f8 --- /dev/null +++ b/ja/2020/01/03/FE-guide-Module/index.html @@ -0,0 +1,601 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 模块化 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 模块化 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

模块化

在有 Babel 的情况下,我们可以直接使用 ES6 的模块化

+
1
2
3
4
5
6
7
8
9
10
// js代码

// file a.js
export function a() {}
export function b() {}
// file b.js
export default function() {}

import {a, b} from './a.js'
import XXX from './b.js'
+

CommonJS

CommonJsNode 独有的规范,浏览器中使用就需要用到 Browserify 解析了。

+
1
2
3
4
5
6
7
8
9
10
11
12
// js代码

// a.js
module.exports = {
a: 1
}
// or
exports.a = 1

// b.js
var module = require('./a.js')
module.a // -> log 1
+

在上述代码中, module.exportsexports 很容易混淆,让我们来看看大致内部实现

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// js代码

var module = require('./a.js')
module.a
// 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {
a: 1
}
// 基本实现
var module = {
exports: {} // exports 就是个空对象
}
// 这个是为什么 exports 和 module.exports 用法相似的原因
var exports = module.exports
var load = function (module) {
// 导出的东西
var a = 1
module.exports = a
return module.exports
};
+

再来说说 module.exports 和 exports,用法其实是相似的,但是不能对 exports 直接赋值,不会有任何效果。

+

对于 CommonJS 和 ES6 中的模块化的两者区别是:

+
    +
  • 前者支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
  • +
  • 前者是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
  • +
  • 前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是后者采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
  • +
  • 后者会编译成 require/exports 来执行的
  • +
+

AMD

AMD 是由 RequireJS 提出的

+
1
2
3
4
5
6
7
8
9
10
11
12
13
// js代码

// AMD
define(['./a', './b'], function(a, b) {
a.do()
b.do()
})
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
var b = require('./b')
b.doSomething()
})
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 模块化

+

文章作者:

+

发布时间:2020年01月03日 - 00:41

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-Module/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/03/FE-guide-Promise/index.html b/ja/2020/01/03/FE-guide-Promise/index.html new file mode 100644 index 0000000000..edd760550c --- /dev/null +++ b/ja/2020/01/03/FE-guide-Promise/index.html @@ -0,0 +1,592 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- Promise | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- Promise +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Promise 实现

PromiseES6 新增的语法,解决了回调地狱的问题。

+

可以把 Promise 看成一个状态机。初始是 pending 状态,可以通过函数 resolvereject ,将状态转变为 resolved 或者 rejected 状态,状态一旦改变就不能再次变化

+

then 函数会返回一个 Promise 实例,并且该返回值是一个新的实例而不是之前的实例。因为 Promise 规范规定除了 pending 状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个 then 调用就失去意义了。

+

对于 then 来说,本质上可以把它看成是 flatMap

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// js代码

// 三种状态
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
// promise 接收一个函数参数,该函数会立即执行
function MyPromise(fn) {
let _this = this;
_this.currentState = PENDING;
_this.value = undefined;
// 用于保存 then 中的回调,只有当 promise
// 状态为 pending 时才会缓存,并且每个实例至多缓存一个
_this.resolvedCallbacks = [];
_this.rejectedCallbacks = [];

_this.resolve = function (value) {
if (value instanceof MyPromise) {
// 如果 value 是个 Promise,递归执行
return value.then(_this.resolve, _this.reject)
}
setTimeout(() => { // 异步执行,保证执行顺序
if (_this.currentState === PENDING) {
_this.currentState = RESOLVED;
_this.value = value;
_this.resolvedCallbacks.forEach(cb => cb());
}
})
};

_this.reject = function (reason) {
setTimeout(() => { // 异步执行,保证执行顺序
if (_this.currentState === PENDING) {
_this.currentState = REJECTED;
_this.value = reason;
_this.rejectedCallbacks.forEach(cb => cb());
}
})
}
// 用于解决以下问题
// new Promise(() => throw Error('error))
try {
fn(_this.resolve, _this.reject);
} catch (e) {
_this.reject(e);
}
}

MyPromise.prototype.then = function (onResolved, onRejected) {
var self = this;
// 规范 2.2.7,then 必须返回一个新的 promise
var promise2;
// 规范 2.2.onResolved 和 onRejected 都为可选参数
// 如果类型不是函数需要忽略,同时也实现了透传
// Promise.resolve(4).then().then((value) => console.log(value))
onResolved = typeof onResolved === 'function' ? onResolved : v => v;
onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;

if (self.currentState === RESOLVED) {
return (promise2 = new MyPromise(function (resolve, reject) {
// 规范 2.2.4,保证 onFulfilled,onRjected 异步执行
// 所以用了 setTimeout 包裹下
setTimeout(function () {
try {
var x = onResolved(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}

if (self.currentState === REJECTED) {
return (promise2 = new MyPromise(function (resolve, reject) {
setTimeout(function () {
// 异步执行onRejected
try {
var x = onRejected(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}

if (self.currentState === PENDING) {
return (promise2 = new MyPromise(function (resolve, reject) {
self.resolvedCallbacks.push(function () {
// 考虑到可能会有报错,所以使用 try/catch 包裹
try {
var x = onResolved(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});

self.rejectedCallbacks.push(function () {
try {
var x = onRejected(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});
}));
}
};
// 规范 2.3
function resolutionProcedure(promise2, x, resolve, reject) {
// 规范 2.3.1,x 不能和 promise2 相同,避免循环引用
if (promise2 === x) {
return reject(new TypeError("Error"));
}
// 规范 2.3.2
// 如果 x 为 Promise,状态为 pending 需要继续等待否则执行
if (x instanceof MyPromise) {
if (x.currentState === PENDING) {
x.then(function (value) {
// 再次调用该函数是为了确认 x resolve 的
// 参数是什么类型,如果是基本类型就再次 resolve
// 把值传给下个 then
resolutionProcedure(promise2, value, resolve, reject);
}, reject);
} else {
x.then(resolve, reject);
}
return;
}
// 规范 2.3.3.3.3
// reject 或者 resolve 其中一个执行过得话,忽略其他的
let called = false;
// 规范 2.3.3,判断 x 是否为对象或者函数
if (x !== null && (typeof x === "object" || typeof x === "function")) {
// 规范 2.3.3.2,如果不能取出 then,就 reject
try {
// 规范 2.3.3.1
let then = x.then;
// 如果 then 是函数,调用 x.then
if (typeof then === "function") {
// 规范 2.3.3.3
then.call(
x,
y => {
if (called) return;
called = true;
// 规范 2.3.3.3.1
resolutionProcedure(promise2, y, resolve, reject);
},
e => {
if (called) return;
called = true;
reject(e);
}
);
} else {
// 规范 2.3.3.4
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
// 规范 2.3.4,x 为基本类型
resolve(x);
}
}
+

以上就是根据 Promise / A+ 规范来实现的代码,可以通过 promises-aplus-tests 的完整测试

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- Promise

+

文章作者:

+

发布时间:2020年01月03日 - 02:52

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-Promise/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/03/FE-guide-async-Proxy/index.html b/ja/2020/01/03/FE-guide-async-Proxy/index.html new file mode 100644 index 0000000000..3becd8ff42 --- /dev/null +++ b/ja/2020/01/03/FE-guide-async-Proxy/index.html @@ -0,0 +1,589 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- Proxy | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- Proxy +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Proxy

ProxyES6 中新增的功能,可以用来自定义对象中的操作

+
1
2
3
4
5
// js代码

let p = new Proxy(target, handler);
// `target` 代表需要添加代理的对象
// `handler` 用来自定义对象中的操作
+

可以很方便的使用 Proxy 来实现一个数据绑定和监听

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(target, property)
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
setBind(value);
return Reflect.set(target, property, value);
}
};
return new Proxy(obj, handler);
};

let obj = { a: 1 }
let value
let p = onWatch(obj, (v) => {
value = v
}, (target, property) => {
console.log(`Get '${property}' = ${target[property]}`);
})
p.a = 2 // bind `value` to `2`
p.a // -> Get 'a' = 2
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- Proxy

+

文章作者:

+

发布时间:2020年01月03日 - 04:00

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-async-Proxy/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/03/FE-guide-async-await/index.html b/ja/2020/01/03/FE-guide-async-await/index.html new file mode 100644 index 0000000000..f7333c2662 --- /dev/null +++ b/ja/2020/01/03/FE-guide-async-await/index.html @@ -0,0 +1,603 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- async 和 await | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- async 和 await +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

async 和 await

一个函数如果加上 async ,那么该函数就会返回一个 Promise

+
1
2
3
4
async function test() {
return "1";
}
console.log(test()); // -> Promise {<resolved>: "1"}
+

可以把 async 看成将函数返回值使用 Promise.resolve() 包裹了下。
await 只能在 async 函数中使用

+
1
2
3
4
5
6
7
8
9
10
11
12
13
function sleep() {
return new Promise(resolve => {
setTimeout(() => {
console.log('finish')
resolve("sleep");
}, 2000);
});
}
async function test() {
let value = await sleep();
console.log("object");
}
test()
+ +

上面代码会先打印 finish 然后再打印 object 。因为 await 会等待 sleep 函数 resolve ,所以即使后面是同步代码,也不会先去执行同步代码再来执行异步代码。

+

asyncawait 相比直接使用 Promise 来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码。缺点在于滥用 await 可能会导致性能问题,因为 await 会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。

+

下面来看一个使用 await 的代码。

+
1
2
3
4
5
6
7
8
9
10
var a = 0
var b = async () => {
a = a + await 10
console.log('2', a) // -> '2' 10
a = (await 10) + a
console.log('3', a) // -> '3' 20
}
b()
a++
console.log('1', a) // -> '1' 1
+ +

对于以上代码你可能会有疑惑,这里说明下原理

+
    +
  • 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为在 await 内部实现了 generatorsgenerators 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
  • +
  • 因为 await 是异步操作,遇到 await 就会立即返回一个 pending 状态的 Promise 对象,暂时返回执行代码的控制权,使得函数外的代码得以继续执行,所以会先执行 console.log('1', a)
  • +
  • 这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 10
  • +
  • 然后后面就是常规执行代码了
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- async 和 await

+

文章作者:

+

发布时间:2020年01月03日 - 03:52

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-async-await/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/03/FE-guide-call-apply-bind/index.html b/ja/2020/01/03/FE-guide-call-apply-bind/index.html new file mode 100644 index 0000000000..fe6491a4c8 --- /dev/null +++ b/ja/2020/01/03/FE-guide-call-apply-bind/index.html @@ -0,0 +1,596 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- call, apply, bind 区别 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- call, apply, bind 区别 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

call, apply, bind 区别

首先说下前两者的异同。
相同: callapply 都是为了解决改变 this 的指向。
不同:传参的方式不同,除了第一个参数外, call 可以接收一个参数列表apply 只接受一个参数数组

+
1
2
3
4
5
6
7
8
9
10
11
// js代码
let anObj = {
value: 1
}
function getValue(name, age) {
console.log(name)
console.log(age)
console.log(this.value)
}
getValue.call(anObj, 'lixuguang', '31')
getValue.apply(anObj, ['lixuguang', '31'])
+

模拟实现 callapply

可以从以下几点来考虑如何实现

+
    +
  • 不传入第一个参数,那么默认为 window
  • +
  • 改变了 this 指向,让新的对象可以执行该函数。那么思路是否可以变成给新的对象添加一个函数,然后在执行完以后删除?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    Function.prototype.myCall = function (context) {
    var context = context || window // 有入参用入参,没有给 window
    context.fn = this // 给 context 添加一个属性,getValue.call(anObj, 'lixuguang', '31') => anObj.fn = getValue
    var args = [...arguments].slice(1) // 将 context 后面的参数取出来
    var result = context.fn(...args) // getValue.call(anObj, 'lixuguang', '31') => anObj.fn('yck', '24')
    delete context.fn // 删除 fn
    return result
    }
    +以上就是 call 的思路,apply 的实现也类似
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    Function.prototype.Apply = function (context) {
    var context = context || window // 有入参用入参,没有给 window
    context.fn = this // 给 context 添加一个属性,getValue.call(anObj, 'lixuguang', '31') => anObj.fn = getValue
    var args = arguments[1] // 将 context 后面的参数取出来
    var result = args?context.fn(...args):context.fn() // getValue.call(anObj, ['lixuguang', '31']) => anObj.fn('yck', '24')
    delete context.fn // 删除 fn
    return result
    }
    +bind 和其他两个方法作用也是一致的,只是该方法会返回一个函数。并且我们可以通过 bind 实现柯里化。
  • +
+

同样的,也来模拟实现下 bind

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码

Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
var _this = this
var args = [...arguments].slice(1)

return function F() { // 返回一个函数,这是 `bind` 和 `call` , `apply` 的区别
if (this instanceof F) { // 因为返回了一个函数,我们可以 new F(),所以需要判断
return new _this(...args, ...arguments)
}
return _this.apply(context, args.concat(...arguments))
}
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- call, apply, bind 区别

+

文章作者:

+

发布时间:2020年01月03日 - 02:19

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-call-apply-bind/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/03/FE-guide-copy/index.html b/ja/2020/01/03/FE-guide-copy/index.html new file mode 100644 index 0000000000..707a99d934 --- /dev/null +++ b/ja/2020/01/03/FE-guide-copy/index.html @@ -0,0 +1,621 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 深浅拷贝 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 深浅拷贝 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

深浅拷贝

1
2
3
4
5
6
let a = {
age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2
+

从上述例子中我们可以发现,如果给一个变量赋值一个对象,那么两者的值会是同一个引用,其中一方改变,另一方也会相应改变。
通常在开发中我们不希望出现这样的问题,我们可以使用浅拷贝来解决这个问题。

+

浅拷贝

首先可以通过 Object.assign 来解决这个问题。

+
1
2
3
4
5
6
7
8
// js代码

let a = {
age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // => 1
+

当然我们也可以通过展开运算符(…)来解决

+
1
2
3
4
5
6
let a = {
age: 1
}
let b = {...a}
a.age = 2
console.log(b.age) // => 1
+

我们还可以用很多简单的方法都能实现浅拷贝:

+
1
2
arr.slice();
arr.concat();
+ +

通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就需要使用到深拷贝了

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = {...a}
a.jobs.first = 'native'
console.log(b.jobs.first) // native
+

浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到刚开始的话题了,两者享有相同的引用。要解决这个问题,我们需要引入深拷贝。

+

深拷贝

这个问题通常可以通过 JSON.parse(JSON.stringify(object)) 来解决,这也是最好用最简单的方法,俗称乞丐版。
乞丐版

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
+

但是该方法也是有局限性的:

+
    +
  • 会忽略 undefined
  • +
  • 会忽略 symbol
  • +
  • 不能序列化函数
  • +
  • 不能解决循环引用的对象
  • +
+

举个栗子:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

let obj = {
a: 1,
b: {
c: 2,
d: 3,
},
}

obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c

let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj) // => Uncaught TypeError: Converting circular structure to JSON
+

如果你有这么一个循环引用对象,你会发现你不能通过该方法深拷贝
在遇到函数、 undefined 或者 symbol 的时候,该对象也不能正常的序列化

+
1
2
3
4
5
6
7
8
9
10
// js代码

let a = {
age: undefined,
sex: Symbol('fmale'),
jobs: function() {},
name: 'lixuguang'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // => {name: "lixuang"}
+ +

你会发现在上述情况中,该方法会忽略掉函数和 undefined
但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题,并且该函数是内置函数中处理深拷贝性能最快的。

+

那么是否可以解决函数和循环引用的问题呢?答案是肯定可以解决,接下来是基础版本的改造
基础版

+
1
2
3
4
5
6
7
8
9
10
11
function myClone(target){
if(typeof target === 'object'){ // 判断传入目标是否是object类型
let cloneTarget = {}; // 创建克隆对象
for(const key in target){ // 遍历目标对象
cloneTarget[key] = myClone(target[key]) // 递归调用 clone 方法
}
return cloneTarget;
} else {
return target // 如果不是 object 返回
}
}
+

写到这里已经可以帮助你应付一些面试官考察你的递归解决问题的能力。但是显然,这个深拷贝函数还是有一些问题。
这里只考虑了对象,没有考虑数组。
下面我们来做一个强化版的深拷贝,同时考虑对象、数组还有循环引用的问题。
强化版

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function myClone(target, map = new WeakMap()) { // WeakMap => 键对象弱引用, 可被垃圾回收
if(typeof target === 'object'){ // 判断是否是对象
let cloneTarget = Array.isArray(target) ? [] : {}; // 判断是是数组还是对象
if(map.get(target)) {
return target;
}

map.set(target, cloneTarget);

for(const key in target) {
cloneTarget[key] = myClone(target[key], map)
}
return cloneTarget;
} else {
return target;
}
}
+

当然如果你的数据中含有以上三种情况下,可以使用 lodash 的深拷贝函数。
如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// js代码

function structuralClone(obj) {
return new Promise(resolve => {
const {port1, port2} = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
});
}

var obj = {
a: 1,
b: {
c: b
}
}
// 注意该方法是异步的
// 可以处理 undefined 和循环引用对象
(async () => {
const clone = await structuralClone(obj)
})()
+ +

深拷贝实现方式2,可以深拷贝 functionsymbol,等等,堪称终极版
终极版

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// js代码

const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const argsTag = '[object Arguments]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];
function forEach(array, iteratee) {
let index = -1;
const length = array.length;
while (++index < length) {
iteratee(array[index], index);
}
return array;
}

function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}

function getType(target) {
return Object.prototype.toString.call(target);
}

function getInit(target) {
const Ctor = target.constructor;
return new Ctor();
}

function cloneSymbol(targe) {
return Object(Symbol.prototype.valueOf.call(targe));
}

function cloneReg(targe) {
const reFlags = /\w*$/;
const result = new targe.constructor(targe.source, reFlags.exec(targe));
result.lastIndex = targe.lastIndex;
return result;
}

function cloneFunction(func) {
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
if (func.prototype) {
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if (body) {
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(funcString);
}
}

function cloneOtherType(targe, type) {
const Ctor = targe.constructor;
switch (type) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(targe);
case regexpTag:
return cloneReg(targe);
case symbolTag:
return cloneSymbol(targe);
case funcTag:
return cloneFunction(targe);
default:
return null;
}
}

function clone(target, map = new WeakMap()) {
// 克隆原始类型
if (!isObject(target)) {
return target;
}

// 初始化
const type = getType(target);
let cloneTarget;
if (deepTag.includes(type)) {
cloneTarget = getInit(target, type);
} else {
return cloneOtherType(target, type);
}

// 防止循环引用
if (map.get(target)) {
return target;
}
map.set(target, cloneTarget);

// 克隆set
if (type === setTag) {
target.forEach(value => {
cloneTarget.add(clone(value));
});
return cloneTarget;
}

// 克隆map
if (type === mapTag) {
target.forEach((value, key) => {
cloneTarget.set(key, clone(value));
});
return cloneTarget;
}

// 克隆对象和数组
const keys = type === arrayTag ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone(target[key], map);
});

return cloneTarget;
}

// 调用方法
clone(target);
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 深浅拷贝

+

文章作者:

+

发布时间:2020年01月03日 - 00:05

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-copy/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/03/FE-guide-debounce-throttle/index.html b/ja/2020/01/03/FE-guide-debounce-throttle/index.html new file mode 100644 index 0000000000..e5b1aa40bd --- /dev/null +++ b/ja/2020/01/03/FE-guide-debounce-throttle/index.html @@ -0,0 +1,605 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 函数防抖和节流 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 函数防抖和节流 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

函数防抖和节流

在前端开发的过程中,我们经常会需要绑定一些持续触发的事件,如 resize、scroll、mousemove 等等,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。

+

通常这种情况下我们怎么去解决的呢?一般来讲,防抖和节流是比较好的解决方案。

+

防抖

你是否在日常开发中遇到一个问题,在滚动事件中需要做个复杂计算或者实现一个按钮的防二次点击操作。

+

这些需求都可以通过函数防抖动来实现。尤其是第一个需求,如果在频繁的事件回调中做复杂计算,很有可能导致页面卡顿,不如将多次计算合并为一次计算,只在一个精确点做操作。

+

PS:防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的情况下只会调用一次,而节流的 情况会每隔一定时间(参数wait)调用函数。

+

我们先来看一个袖珍版的防抖理解一下防抖的实现:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
// 缓存一个定时器id
let timer = 0
// 这里返回的函数是每次用户实际调用的防抖函数
// 如果已经设定过定时器了就清空上一次的定时器
// 开始一个新的定时器,延迟执行用户传入的方法
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
// 不难看出如果用户调用该函数的间隔小于wait的情况下,上一次的时间还未到就被清除了,并不会执行函数
+

这是一个简单版的防抖,但是有缺陷,这个防抖只能在最后调用。一般的防抖会有immediate选项,表示是否立即调用。这两者的区别,举个栗子来说:

+
    +
  • 例如在搜索引擎搜索问题的时候,我们当然是希望用户输入完最后一个字才调用查询接口,这个时候适用延迟执行的防抖函数,它总是在一连串(间隔小于wait的)函数触发之后调用。
  • +
  • 例如用户给某系统表单点提交的时候,我们希望用户点第一下的时候就去调用接口,这个情况适用立即执行的防抖函数,它总是在第一次调用,并且下一次调用必须与前一次调用的时间间隔大于wait才会触发。
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// js代码

// 这个是用来获取当前时间戳的
function now() {
return +new Date()
}
/**
* 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {boolean} immediate 设置为ture时,是否立即调用函数
* @return {function} 返回客户调用函数
*/
function debounce (func, wait = 50, immediate = true) {
let timer, context, args

// 延迟执行函数
const later = () => setTimeout(() => {
// 延迟函数执行完毕,清空缓存的定时器序号
timer = null
// 延迟执行的情况下,函数会在延迟函数中执行
// 使用到之前缓存的参数和上下文
if (!immediate) {
func.apply(context, args)
context = args = null
}
}, wait)

// 这里返回的函数是每次实际调用的函数
return function(...params) {
// 如果没有创建延迟执行函数(later),就创建一个
if (!timer) {
timer = later()
// 如果是立即执行,调用函数
// 否则缓存参数和调用上下文
if (immediate) {
func.apply(this, params)
} else {
context = this
args = params
}
// 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
// 这样做延迟函数会重新计时
} else {
clearTimeout(timer)
timer = later()
}
}
}
+

整体函数实现的不难,总结一下。

+
    +
  • 对于按钮防点击来说的实现:如果函数是立即执行的,就立即调用,如果函数是延迟执行的,就缓存上下文和参数,放到延迟函数中去执行。一旦我开始一个定时器,只要我定时器还在,你每次点击我都重新计时。一旦你点累了,定时器时间到,定时器重置为 null,就可以再次点击了。
  • +
  • 对于延时执行函数来说的实现:清除定时器ID,如果是延迟调用就调用函数
  • +
+

节流

防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// js代码

/**
* underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {object} options 如果想忽略开始函数的的调用,传入{leading: false}。
* 如果想忽略结尾函数的调用,传入{trailing: false}
* 两者不能共存,否则函数不能执行
* @return {function} 返回客户调用函数
*/
_.throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
// 之前的时间戳
var previous = 0;
// 如果 options 没传则设为空对象
if (!options) options = {};
// 定时器回调函数
var later = function() {
// 如果设置了 leading,就将 previous 设为 0
// 用于下面函数的第一个 if 判断
previous = options.leading === false ? 0 : _.now();
// 置空一是为了防止内存泄漏,二是为了下面的定时器判断
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
// 获得当前时间戳
var now = _.now();
// 首次进入前者肯定为 true
// 如果需要第一次不执行函数
// 就将上次时间戳设为当前的
// 这样在接下来计算 remaining 的值时会大于0
if (!previous && options.leading === false) previous = now;
// 计算剩余时间
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果当前调用已经大于上次调用时间 + wait
// 或者用户手动调了时间
// 如果设置了 trailing,只会进入这个条件
// 如果没有设置 leading,那么第一次会进入这个条件
// 还有一点,你可能会觉得开启了定时器那么应该不会进入这个 if 条件了
// 其实还是会进入的,因为定时器的延时
// 并不是准确的时间,很可能你设置了2秒
// 但是他需要2.2秒才触发,这时候就会进入这个条件
if (remaining <= 0 || remaining > wait) {
// 如果存在定时器就清理掉否则会调用二次回调
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 判断是否设置了定时器和 trailing
// 没有的话就开启一个定时器
// 并且不能不能同时设置 leading 和 trailing
timeout = setTimeout(later, remaining);
}
return result;
};
};
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 函数防抖和节流

+

文章作者:

+

发布时间:2020年01月03日 - 01:04

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-debounce-throttle/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/03/FE-guide-inherit/index.html b/ja/2020/01/03/FE-guide-inherit/index.html new file mode 100644 index 0000000000..b2564e22c4 --- /dev/null +++ b/ja/2020/01/03/FE-guide-inherit/index.html @@ -0,0 +1,597 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 继承 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 继承 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

继承

在 ES5 中,我们可以使用如下方式解决继承的问题

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码
function Super() {}
Super.prototype.getNumber = function() {
return 1
}

function Sub() {}
let s = new Sub()
Sub.prototype = Object.create(Super.prototype, {
constructor: {
value: Sub,
enumerable: false,
writable: true,
configurable: true
}
})
+

以上继承实现思路就是将子类的原型设置为父类的原型
ES6 中,我们可以通过 class 语法轻松解决这个问题

+
1
2
3
4
5
6
7
8
9
// js代码

class MyDate extends Date {
test() {
return this.getTime()
}
}
let myDate = new MyDate()
myDate.test()
+

但是 ES6 不是所有浏览器都兼容,所以我们需要使用 Babel 来编译这段代码。

+

如果你使用编译过得代码调用 myDate.test() 你会惊奇地发现出现了报错

+

因为在 JS 底层有限制,如果不是由 Date 构造出来的实例的话,是不能调用 Date 里的函数的。所以这也侧面的说明了:ES6 中的 class 继承与 ES5 中的一般继承写法是不同的

+

既然底层限制了实例必须由 Date 构造出来,那么我们可以改变下思路实现继承

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

function MyData() {

}
MyData.prototype.test = function () {
return this.getTime()
}
let d = new Date() // 父类实例
Object.setPrototypeOf(d, MyData.prototype)
Object.setPrototypeOf(MyData.prototype, Date.prototype)
+

以上继承实现思路:先创建父类实例 => 改变实例原先的 __proto__ 转而连接到子类的 prototype => 子类的 prototype__proto__ 改为父类的 prototype

+

通过以上方法实现的继承就可以完美解决 JS 底层的这个限制。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 继承

+

文章作者:

+

发布时间:2020年01月03日 - 01:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-inherit/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/03/FE-guide-store/index.html b/ja/2020/01/03/FE-guide-store/index.html new file mode 100644 index 0000000000..2813b62db7 --- /dev/null +++ b/ja/2020/01/03/FE-guide-store/index.html @@ -0,0 +1,628 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 浏览器存储 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 浏览器存储 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

cookie,localStorage,sessionStorage,indexDB

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
特性cookielocalStoragesessionStorageindexDB
数据生命周期一般由服务器生成,可以设置过期时间除非被清理,否则一直存在页面关闭就清理除非被清理,否则一直存在
数据存储大小4K5M5M无限制
与服务端通信每次都会携带在 header 中,对于请求性能影响不参与不参与不参与
+

从上表可以看到, cookie 已经不建议用于存储。如果没有大量数据存储需求的话,可以使用 localStoragesessionStorage 。对于不怎么改变的数据尽量使用 localStorage 存储,否则可以用 sessionStorage 存储。

+

对于 cookie ,我们还需要注意安全性。
| 属性 | 作用 |
| ——— | ————————————————————– |
| value | 如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识 |
| http-only | 不能通过 JS 访问 Cookie,减少 XSS 攻击 |
| secure | 只能在协议为 HTTPS 的请求中携带 |
| same-site | 规定浏览器不能在跨域请求中携带 Cookie,减少 CSRF 攻击 |

+

Service Worker

+

Service workers 本质上充当 Web 应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步 API。

+
+

目前该技术通常用来做缓存文件,提高首屏速度,可以试着来实现这个功能。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// js代码

// index.js
if (navigator.serviceWorker) {
navigator.serviceWorker
.register('sw.js')
.then(function(registration) {
console.log('service worker 注册成功')
})
.catch(function(err) {
console.log('servcie worker 注册失败')
})
}
// sw.js
// 监听 `install` 事件,回调中缓存所需文件
self.addEventListener('install', e => {
e.waitUntil(
caches.open('my-cache').then(function(cache) {
return cache.addAll(['./index.html', './index.js'])
})
)
})

// 拦截所有请求事件
// 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据
self.addEventListener('fetch', e => {
e.respondWith(
caches.match(e.request).then(function(response) {
if (response) {
return response
}
console.log('fetch source')
})
)
})
+ +

打开页面,可以在开发者工具中的 Application 看到 Service Worker 已经启动了
Cache 中也可以发现我们所需的文件已被缓存

+

当我们重新刷新页面可以发现我们缓存的数据是从 Service Worker 中读取的

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 浏览器存储

+

文章作者:

+

发布时间:2020年01月03日 - 04:37

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/03/FE-guide-store/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/03/why-0.1-plus-0.2-not-equals-0.3/index.html b/ja/2020/01/03/why-0.1-plus-0.2-not-equals-0.3/index.html new file mode 100644 index 0000000000..6cc9fb02ee --- /dev/null +++ b/ja/2020/01/03/why-0.1-plus-0.2-not-equals-0.3/index.html @@ -0,0 +1,586 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 为什么 0.1 + 0.2 != 0.3 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 为什么 0.1 + 0.2 != 0.3 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Proxy

因为 JS 采用 IEEE 754 双精度版本(64位),并且只要采用 IEEE 754 的语言都有该问题。

+

我们都知道计算机表示十进制是采用二进制表示的,所以 0.1 在二进制表示为

+
1
2
3
4
// js代码

// (0011) 表示循环
0.1 = 2^-4 * 1.10011(0011)
+

那么如何得到这个二进制的呢,我们可以来演算下

+

小数算二进制和整数不同。乘法计算时,只计算小数位,整数位用作每一位的二进制,并且得到的第一位为最高位。所以我们得出 0.1 = 2^-4 * 1.10011(0011),那么 0.2 的演算也基本如上所示,只需要去掉第一步乘法,所以得出 0.2 = 2^-3 * 1.10011(0011)

+

回来继续说 IEEE 754 双精度。六十四位中符号位占一位,整数位占十一位,其余五十二位都为小数位。因为 0.10.2 都是无限循环的二进制了,所以在小数位末尾处需要判断是否进位(就和十进制的四舍五入一样)。

+

所以 2^-4 * 1.10011...001 进位后就变成了 2^-4 * 1.10011(0011 * 12次)010 。那么把这两个二进制加起来会得出 2^-2 * 1.0011(0011 * 11次)0100 , 这个值算成十进制就是 0.30000000000000004

+

下面说一下原生解决办法,如下代码所示

+
1
2
3
// js代码

parseFloat((0.1 + 0.2).toFixed(10))
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:为什么 0.1 + 0.2 != 0.3

+

文章作者:

+

发布时间:2020年01月03日 - 04:10

+

最后更新:2020年01月03日 - 04:10

+

原始链接:https://blog.lifesli.com/2020/01/03/why-0.1-plus-0.2-not-equals-0.3/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/04/Algorithm/index.html b/ja/2020/01/04/Algorithm/index.html new file mode 100644 index 0000000000..6ed2a30ae1 --- /dev/null +++ b/ja/2020/01/04/Algorithm/index.html @@ -0,0 +1,734 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + JavaScript实现经典排序算法 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ JavaScript实现经典排序算法 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

基本概念

时间复杂度

一个算法的时间复杂度反映了程序运行从开始到结束所需要的时间。把算法中基本操作重复执行的次数(频度)作为算法的时间复杂度。
没有循环语句,记作O(1),也称为常数阶。只有一重循环,则算法的基本操作的执行频度与问题规模n呈线性增大关系,记作O(n),也叫线性阶。
常见的时间复杂度有:

+
    +
  • O(1): Constant Complexity: Constant 常数复杂度
  • +
  • O(log n): Logarithmic Complexity: 对数复杂度
  • +
  • O(n): Linear Complexity: 线性时间复杂度
  • +
  • O(n^2): N square Complexity 平⽅方
  • +
  • O(n^3): N square Complexity ⽴立⽅方
  • +
  • O(2^n): Exponential Growth 指数
  • +
  • O(n!): Factorial 阶乘
  • +
+

空间复杂度

一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。

+

一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。

+
    +
  • 稳定
  • +
  • 不稳定

    算法汇总

    十大经典排序.jpg

    关于时间复杂度:

    平方阶 (O(n2)) 排序 各类简单排序:直接插入、直接选择和冒泡排序。
    线性对数阶 (O(nlog2n)) 排序 快速排序、堆排序和归并排序;
    O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数。 希尔排序
    线性阶 (O(n)) 排序 基数排序,此外还有桶、箱排序。
  • +
+

关于稳定性:

稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。

+

名词解释:

    +
  • n:数据规模
  • +
  • k:“桶”的个数
  • +
  • In-place:占用常数内存,不占用额外内存
  • +
  • Out-place:占用额外内存
  • +
  • 稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同
  • +
+

1. 冒泡排序(Bubble Sort) — 前后两两比较 — 气泡

冒泡排序可谓是最经典的排序算法了,它是基于比较的排序算法,其优点是实现简单,排序数量较小时性能较好。

+

它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

+

1. 1 算法原理

相邻的数据进行两两比较,小数放在前面,大数放在后面,如果前面的数据比后面的数据大,就交换这两个数的位置。也可以实现大数放在前面,小数放在后面,如果前面的数据比后面的小,就交换两个的位置。要实现上述规则需要用到两层for循环。

+

1. 2 算法描述

    +
  1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  2. +
  3. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  4. +
  5. 针对所有的元素重复以上的步骤,除了最后一个;
  6. +
  7. 重复步骤1~3,直到排序完成。
  8. +
+

1. 3 动图演示

ldB5VS.gif

+

1. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// js代码

function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len; i++) {
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { // 相邻元素两两对比
// 元素交换
/** 1.使用中间变量 **/
var temp = arr[j + 1];
arr[j + 1] = arr[j]
arr[j] = temp
/** 2.适用纯数字的数组排序 **/
arr[j] = arr[j] + arr[j + 1]
arr[j + 1] = arr[j] - arr[j + 1]
arr[j] -= arr[j + 1]
/** 3.使用es6解构赋值 **/
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
}
}
}
return arr;
}
+

冒泡排序算法优化

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len; i++) {
var exchange=false; // 交换标志
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { // 相邻元素两两对比
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]] // 元素交换
exchange=true; //
}
}
if(!exchange){ // 若本趟排序未发生交换,提前终止算法
break;
}
}
return arr;
}
+

2. 选择排序(Selection Sort) — 面试挑简历,在剩下的里面挑最好的

表现最稳定的排序算法之一,因为无论什么数据进去都是O(n²)的时间复杂度。。。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。

+

2. 1 算法原理

先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

+

2. 2 算法描述

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

+
    +
  1. 初始状态:无序区为R[1..n],有序区为空;
  2. +
  3. 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中
  4. +
  5. 选出关键字最小的记录R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
  6. +
  7. n-1趟结束,数组有序化了。
  8. +
+

2. 3 动图演示

ldDWW9.gif

+

2. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

function selectionSort(arr) {
var len = arr.length;
var minIndex, temp;
for (var i = 0; i < len - 1; i++) {
minIndex = i;
for (var j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) { //寻找最小的数
minIndex = j; //将最小数的索引保存
}
}
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
return arr;
}
+ +

3. 插入排序(Insertion Sort)—– 麻将/扑克

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。

+

3. 1 算法原理

它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

+

3. 2 算法描述

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

+
    +
  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. +
  3. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  4. +
  5. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  6. +
  7. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  8. +
  9. 将新元素插入到该位置后;
  10. +
  11. 重复步骤2~5。
  12. +
+

3. 3 动图演示

ldDfzR.gif

+

3. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// js代码

function insertSort(arr) {
// 从1位置开始遍历arr中每元素,同时声明空变量temp
for (let i = 1; i < arr.length; i++) {
if (arr[i] < arr[i - 1]) { // 如果当前元素<前一个元素
let temp = arr[i] // 将当前元素值临时保存在temp中
let p = i - 1 // 定义变量 p = i- 1
// 循环 条件:
// 1. p>=0且temp小于p位置的元素
while (p >= 0 && temp < arr[p]) {
// 循环体: 将P位置的值赋值给p的后一个元素
arr[p + 1] = arr[p]
p-- // p向前移动一个
}
arr[p + 1] = temp // 将temp的值赋值给p+1位置的元素
}
}
}
+ +

4. 快速排序(Selection Sort)

快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

+

4. 1 算法原理

快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。

+

4. 2 算法描述

选基准:在数据结构中选择一个元素作为基准(pivot
划分区:参照基准元素值的大小,划分无序区,所有小于基准元素的数据放入一个区间,所有大于基准元素的数据放入另一区间,分区操作结束后,基准元素所处的位置就是最终排序后它应该所处的位置
递归:对初次划分出来的两个无序区间,递归调用第 1步和第 2步的算法,直到所有无序区间都只剩下一个元素为止。

+

简单理解就是,选择一个目标值,比目标值小的放左边,比目标值大的放右边,目标值的位置已排好,将左右两侧再进行快排。

+

4. 3 动图演示

快速排序.gif

+

4. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// js代码

function quickSort(arr){
//如果arr.length<=1,则直接返回arr
if(arr.length<=1){return arr}
// arr的元素个数/2,再下去整,将值保存在pivotIndex中
var pivotIndex=Math.floor(arr.length/2);
// 将arr中pivotIndex位置的元素,保存在变量pivot中
var pivot=arr[pivotIndex];
//声明空数组left和right
var left=[];
var right=[];
for(var i=0;i<arr.length;i++){ // 遍历arr中每个元素
if(i !== pivotIndex){ // 如果i !== pivotIndex
if(arr[i]<=pivot){ // 如果当前元素值<pivot
left.push(arr[i]); // 就将当前值压入left
}else{
right.push(arr[i]); // 就将当前值压入right
}
}
}
//递归
return quickSort(left).concat(pivot, quickSort(right)); // 链接多个数组到 left 从小到大
}
+

5. 希尔排序

5. 1 算法原理

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:

+
    +
  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
  • +
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
    希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
  • +
+

5. 2 算法描述

    +
  • 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
  • +
  • 按增量序列个数 k,对序列进行 k 趟排序;
  • +
  • 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
  • +
+

5.3 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// js代码

function shellSort(arr) {
var len = arr.length,
temp,
gap = 1;
while(gap < len/3) { //动态定义间隔序列
gap =gap*3+1;
}
for (gap; gap > 0; gap = Math.floor(gap/3)) {
for (var i = gap; i < len; i++) {
temp = arr[i];
for (var j = i-gap; j >= 0 && arr[j] > temp; j-=gap) {
arr[j+gap] = arr[j];
}
arr[j+gap] = temp;
}
}
return arr;
}
+ +

6. 归并排序

6. 1 算法原理

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

+
    +
  • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
  • +
  • 自下而上的迭代;
    +

    在《数据结构与算法 JavaScript 描述》中,作者给出了自下而上的迭代方法。但是对于递归法,作者却认为:
    However, it is not possible to do so in JavaScript, as the recursion goes too deep for the language to handle.
    然而,在 JavaScript 中这种方式不太可行,因为这个算法的递归深度对它来讲太深了。

    +
    +
  • +
+

说实话,我不太理解这句话。意思是 JavaScript 编译器内存太小,递归太深容易造成内存溢出吗?还望有大神能够指教。

+

和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。

+

6. 2 算法描述

    +
  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
  2. +
  3. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
  4. +
  5. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
  6. +
  7. 重复步骤 3 直到某一指针达到序列尾;
  8. +
  9. 将另一序列剩下的所有元素直接复制到合并序列尾。
  10. +
+

6. 3 动图演示

归并排序.gif

+

6. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// js代码

function mergeSort(arr) { // 采用自上而下的递归方法
var len = arr.length;
if(len < 2) {
return arr;
}
var middle = Math.floor(len / 2),
left = arr.slice(0, middle),
right = arr.slice(middle);
return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right){
var result = [];
while (left.length && right.length) {
if (left[0] <= right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}

while (left.length)
result.push(left.shift());

while (right.length)
result.push(right.shift());

return result;
}
+ +

7. 堆排序

7. 1 算法原理

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

+
    +
  • 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
  • +
  • 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
    堆排序的平均时间复杂度为 Ο(nlogn)。
  • +
+

7. 2 算法描述

    +
  1. 将待排序序列构建成一个堆 H[0……n-1],根据(升序降序需求)选择大顶堆或小顶堆;
  2. +
  3. 把堆首(最大值)和堆尾互换;
  4. +
  5. 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
  6. +
  7. 重复步骤 2,直到堆的尺寸为 1。
  8. +
+

7. 3 动图演示

堆排序.gif

+

7. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// js代码

var len; // 因为声明的多个函数都需要数据长度,所以把len设置成为全局变量

function buildMaxHeap(arr) { // 建立大顶堆
len = arr.length;
for (var i = Math.floor(len/2); i >= 0; i--) {
heapify(arr, i);
}
}

function heapify(arr, i) { // 堆调整
var left = 2 * i + 1,
right = 2 * i + 2,
largest = i;

if (left < len && arr[left] > arr[largest]) {
largest = left;
}

if (right < len && arr[right] > arr[largest]) {
largest = right;
}

if (largest != i) {
swap(arr, i, largest);
heapify(arr, largest);
}
}

function swap(arr, i, j) {
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

function heapSort(arr) {
buildMaxHeap(arr);

for (var i = arr.length-1; i > 0; i--) {
swap(arr, 0, i);
len--;
heapify(arr, 0);
}
return arr;
}
+ +

8. 计数排序

8. 1 算法原理

计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

+

8. 2 算法描述

8. 3 动图演示

计数排序.gif

+

8. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// js代码

function countingSort(arr, maxValue) {
var bucket = new Array(maxValue+1),
sortedIndex = 0;
arrLen = arr.length,
bucketLen = maxValue + 1;

for (var i = 0; i < arrLen; i++) {
if (!bucket[arr[i]]) {
bucket[arr[i]] = 0;
}
bucket[arr[i]]++;
}

for (var j = 0; j < bucketLen; j++) {
while(bucket[j] > 0) {
arr[sortedIndex++] = j;
bucket[j]--;
}
}

return arr;
}
+ +

9. 桶排序

9. 1 算法原理

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

+
    +
  • 在额外空间充足的情况下,尽量增大桶的数量
  • +
  • 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
    同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

    9. 2 算法描述

    1. 什么时候最快

    当输入的数据可以均匀的分配到每一个桶中。

    2. 什么时候最慢

    当输入的数据被分配到了同一个桶中。

    9. 3 动图演示

  • +
+

9. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// js代码

function bucketSort(arr, bucketSize) {
if (arr.length === 0) {
return arr;
}

var i;
var minValue = arr[0];
var maxValue = arr[0];
for (i = 1; i < arr.length; i++) {
if (arr[i] < minValue) {
minValue = arr[i]; // 输入数据的最小值
} else if (arr[i] > maxValue) {
maxValue = arr[i]; // 输入数据的最大值
}
}

//桶的初始化
var DEFAULT_BUCKET_SIZE = 5; // 设置桶的默认数量为5
bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
var bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
var buckets = new Array(bucketCount);
for (i = 0; i < buckets.length; i++) {
buckets[i] = [];
}

//利用映射函数将数据分配到各个桶中
for (i = 0; i < arr.length; i++) {
buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i]);
}

arr.length = 0;
for (i = 0; i < buckets.length; i++) {
insertionSort(buckets[i]); // 对每个桶进行排序,这里使用了插入排序
for (var j = 0; j < buckets[i].length; j++) {
arr.push(buckets[i][j]);
}
}

return arr;
}
+ +

10. 基数排序

10. 1 算法原理

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

+

10. 2 算法描述

1. 基数排序 vs 计数排序 vs 桶排序

基数排序有三种方法:
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异案例看大家发的:

+
    +
  • 基数排序:根据键值的每位数字来分配桶;
  • +
  • 计数排序:每个桶只存储单一键值;
  • +
  • 桶排序:每个桶存储一定范围的数值;

    10. 3 动图演示

  • +
+
    +
  1. LSD 基数排序动图演示
    基数排序.gif

    10. 4 js代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // js代码

    // LSD Radix Sort
    var counter = [];
    function radixSort(arr, maxDigit) {
    var mod = 10;
    var dev = 1;
    for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
    for(var j = 0; j < arr.length; j++) {
    var bucket = parseInt((arr[j] % mod) / dev);
    if(counter[bucket]==null) {
    counter[bucket] = [];
    }
    counter[bucket].push(arr[j]);
    }
    var pos = 0;
    for(var j = 0; j < counter.length; j++) {
    var value = null;
    if(counter[j]!=null) {
    while ((value = counter[j].shift()) != null) {
    arr[pos++] = value;
    }
    }
    }
    }
    return arr;
    }
  2. +
+

总结

排序算法.png
以上就是十大经典算法,算法对于前端来说并不是一个十分熟悉的领域,但是排序算法算是算法里比较入门的,还是需要掌握的,毕竟即使是为了面试也是要准备的。

+

参考资料

https://github.com/hustcc/JS-Sorting-Algorithm
一本关于排序算法的 GitBook 在线书籍 《十大经典排序算法》,多语言实现。

+

http://www.sohu.com/a/136157205_671058
技术面试宝典: 很全面的算法和数据结构知识(含代码实现)下篇

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:JavaScript实现经典排序算法

+

文章作者:

+

发布时间:2020年01月04日 - 07:18

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/04/Algorithm/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/05/FE-guide-ArrayOprs/index.html b/ja/2020/01/05/FE-guide-ArrayOprs/index.html new file mode 100644 index 0000000000..bef8921691 --- /dev/null +++ b/ja/2020/01/05/FE-guide-ArrayOprs/index.html @@ -0,0 +1,617 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 数组常见操作 ---- 去重、扁平、取最大最小值 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 数组常见操作 ---- 去重、扁平、取最大最小值 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

去重

1. 利用 ObjectKey 唯一特性

开辟一个外部存储空间用于标示元素是否出现过。

+
1
2
3
4
5
6
// js代码

const unique = (array)=> {
var container = {};
return array.filter((item, index) => container.hasOwnProperty(item) ? false : (container[item] = true));
}
+ +

2. 利用 indexOf 的返回值数值进行去重

原理是 indexOf 获取元素时如果返回值不等于下标说明已经有了,配合 filter 更美味

+
1
2
3
4
5
// js代码

const unique = arr => arr.filter((e,i) =>
arr.indexOf(e) === i // 如果元素找到的当前下标和当前索引相同说明是同一个,不同说明不是唯一
);
+ +

还有一种变形方法利用 lastIndexOf 方法

+
+

lastIndexOf() 方法返回指定元素(也即有效的 JavaScript 值或变量)在数组中的最后一个的索引,如果不存在则返回 -1。从数组的后面向前查找,从 fromIndex 处开始。

+
+
1
2
3
4
5
// js代码

const filterNonUnique = arr => arr.filter(e =>
arr.indexOf(e) === arr.lastIndexOf(e) // 判断一个元素出现时的第一次下标和最后一次下标是否相同,如果相同那么就唯一
)
+

3. 利用 Set 特性去重

SetES6 中新的数据类型,它的特点就是元素唯一性,且可以和数组进行转换

+
1
2
3
4
5
6
7
// js代码

const unique = arr => Array.from(new Set(arr)); // Array.from 方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。

// 优化

const unique = arr => [...new Set(arr)]; // 利用 `ES6` 中展开操作
+

4. 排序后判断前后两项是否相等去重

通过比较相邻数字是否重复,将排序后的数组进行去重。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

const unique = (array) => {
array.sort((a, b) => a - b);
let pre = 0;
const result = [];
for (let i = 0; i < array.length; i++) {
if (!i || array[i] != array[pre]) {
result.push(array[i]);
}
pre = i;
}
return result;
}
+ +

扁平

1. 普通方法

通过递归的方式判断数组中的项是否是数组,如果不是就加入到新的扁平数组,如果是就递归调用逐层判断,直到全部结束

+
1
2
3
4
5
6
7
8
9
10
11
12
13
// js代码

const flatten = (array) => { // array 原数组
let result = []; // 定义新的扁平数组
for (let i = 0; i < array.length; i++) {
if (Array.isArray(array[i])) { // 判断子元素是否是数组
result = result.concat(flatten(array[i])); // 递归判断
} else {
result.push(array[i]); // 加入新数组
}
}
return result;
}
+ +

2. 使用reduce简化上述方法

+

reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
reducer 函数接收4个参数:

+
+
    +
  • Accumulator (acc) (累计器)
  • +
  • Current Value (cur) (当前值)
  • +
  • Current Index (idx) (当前索引)
  • +
  • Source Array (src) (源数组)
  • +
  • 您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
  • +
+

先看一段 reduce 的示例函数

+
1
2
3
4
5
6
7
8
9
10
11
12
// js代码

const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue; // 定义一个累计器函数,作用是将数组前后累计值与当前值累加

// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer)); // 没有初始值用第一个元素
// expected output: 10

// 5 + 1 + 2 + 3 + 4
console.log(array1.reduce(reducer, 5)); // 有初始值从初始值开始
// expected output: 15
+

这下大家应该对 reduce 函数认识了,接下来看看怎么简化

+
1
2
3
4
5
6
7
8
9
// js代码

function flatten(array) {
return array.reduce((newArray, current) => // 新数组,当前项
Array.isArray(current) ? // 判断当前项是否为数组
newArray.concat(flatten(current)) : // 是的话 递归调用
newArray.concat(current) // 不是的话加进新数组
, []) // 初始化新数组为空
}
+

这里我们再变一个形,增加一个变量,变成可指定深度操作数组

+
1
2
3
4
5
6
7
8
9
10
// js代码

function flattenByDeep(array, deep = 1) { // 默认一层
return array.reduce(
(target, current) =>
Array.isArray(current) && deep > 1 ?
target.concat(flattenByDeep(current, deep - 1)) : // 下一次减一层
target.concat(current)
, [])
}
+

最值

利用 reduce

reduce 函数真的是超级好用,

+
1
2
3
// js代码

array.reduce((c,n) => Math.max(c,n))
+ +

Math.max

Math.max 参数原本是一组数字,只需要让他可以接收数组即可。

+
1
2
3
4
5
// js代码

const array = [3,2,1,4,5];
Math.max.apply(null,array);
Math.max(...array);
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:数组常见操作 ---- 去重、扁平、取最大最小值

+

文章作者:

+

发布时间:2020年01月05日 - 03:05

+

最后更新:2020年01月05日 - 07:47

+

原始链接:https://blog.lifesli.com/2020/01/05/FE-guide-ArrayOprs/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/05/FE-guide-about-reduce/index.html b/ja/2020/01/05/FE-guide-about-reduce/index.html new file mode 100644 index 0000000000..f3b7913dec --- /dev/null +++ b/ja/2020/01/05/FE-guide-about-reduce/index.html @@ -0,0 +1,604 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + reduce函数的妙用 ---- 实现map和filter | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ reduce函数的妙用 ---- 实现map和filter +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近看了好多技术文章,好多的代码片段,突然发现好多的代码里都有 reduce ,感觉以前都没怎么关注过这个函数,为了弥补这个过失,打算专门写一篇文章说说它。

+

reduce 函数在 MDN 中是这样介绍的

+
+

reduce() 方法对数组中的每个元素执行一个由 提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。

+
+

说实话看了一脸懵逼,这上面说的叫人话?后来耐着性子看了一些代码后感觉有所理解,如果理解不对,还请斧正。
首先看一下这里面几个关键词

+

* 每个元素: * 这就是遍历咯,没啥好说的
提供的 reducer 函数:我哪有什么 reducer 函数呢,后来理解了,就是回调,有的地方呢叫 handler ,这里跟 reduce 配合就叫了 reducer
升序执行:就是说是0,1,2下标这样的顺序执行啦。
将其结果汇总为单个返回值:最后返回的是一个值,当然没说必须是什么类型。

+

这样解释完了以后,上面这句话就比较好懂了,简单说就是 reduce 里传一个回调函数,执行回调函数方法后返回一个值,就是这样。

+
+

reducer 函数接收4个参数:

+
+
    +
  • Accumulator (acc) (累计器)
  • +
  • Current Value (cur) (当前值)
  • +
  • Current Index (idx) (当前索引)
  • +
  • Source Array (src) (源数组)
    您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
  • +
+

看上面的描述一定要注意,这些参数是 reducer 的参数,不是 reduce 的参数,一共有4个,通常用前两个情况比较多。

+

那么 reduce 函数呢实际上有两个参数,第二个还可以省略 reducerinitialValueinitialValue作为第一次调用 reducer 函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错,这点切记。

+

终于把前置知识讲清楚了,接下来,我们就用 reduce 搞点事情。

+

1. 使用 reduce 实现 map

map 的话我不想再讲一遍,一句话带过,就是对回调函数中的元素进行加工后返回一个长度一样的新数组。
那么该如何实现呢,我们来想想原理吧,刚才上面说过了,reduce执行过后的返回值会作为下一次执行的第一个参数放进去,那么就可以先用一个空数组作为初始值来接受每次需要处理的元素的集合,并返回回去,作为下一次传入的第一个参数,这样每次操作完之后就可以push到这个数组中,那么剩下要做的就是处理数组中每一项的函数了,我们叫他 handler

+
1
2
3
4
5
6
7
8
// js代码

Array.prototype.reduceToMap = function (handler) { // 自定义 `map` 函数 `reduceToMap`
return this.reduce((target, current, index) => { // this指向调用他的数组
target.push(handler.call(this, current, index)) // 这里用了call方法,handler将接受两个参数 current和index
return target; // 处理完成后返回新数组
}, []) // 初始化空的新数组
};
+

就这样我们就用 reduce,实现了 map 的功能,是不是很好用?

+

2. 使用 reduce 实现 filter

filter 也是数组常用的方法,同样传入一个回调函数,处理结果返回true或false,最终 filter 会返回一个过滤后的函数。
学会了上面的 map 的实现,实际上 filter 就会很简单

+
1
2
3
4
5
6
7
8
9
10
// js代码

Array.prototype.reduceToFilter = function (handler) { // 还是自定义方法名
return this.reduce((target, current, index) => {
if (handler.call(this, current, index)) { // 这里注意 handler 要返回的是布尔类型的值
target.push(current); // 符合条件就插入新数组
} // 不符合就什么都不做
return target; // 最后返回新数组
}, []) // 初始化一个空数组
};
+

日后在看到 reduce 的妙用之后还会来补充这篇文章,如果实在懒得写,我也会链接一下新文章,希望大家长期关注。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:reduce函数的妙用 ---- 实现map和filter

+

文章作者:

+

发布时间:2020年01月05日 - 08:55

+

最后更新:2020年01月05日 - 08:53

+

原始链接:https://blog.lifesli.com/2020/01/05/FE-guide-about-reduce/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/05/FE-guide-currying/index.html b/ja/2020/01/05/FE-guide-currying/index.html new file mode 100644 index 0000000000..33d5978a80 --- /dev/null +++ b/ja/2020/01/05/FE-guide-currying/index.html @@ -0,0 +1,595 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 柯里化 currying | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 柯里化 currying +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

定义

柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

+

柯里化,可以理解为提前接收部分参数,延迟执行,不立即输出结果,而是返回一个接受剩余参数的函数。因为这样的特性,也被称为部分计算函数。

+

通俗易懂的解释:用闭包把参数保存起来,当参数的数量足够执行函数了,就开始执行函数。柯里化,是一个逐步接收参数的过程。在接下来的剖析中,你会深刻体会到这一点。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码

function currying(fn){
var allArgs = [];

return function next(){
var args = [].slice.call(arguments); // 拆成数组元素

if(args.length > 0){
allArgs = allArgs.concat(args);
return next;
}else{
return fn.apply(null, allArgs);
}
}
}
+

我们来一个简单的实例验证一下:

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

var add = currying(function(){
var sum = 0;
for(var i = 0; i < arguments.length; i++){
sum += arguments[i];
}
return sum;
});

add(1)(2, 3)(4)() // => 10
+ +

应用场景

参数复用

1
2
3
4
5
6
7
8
// js代码

function getUrl(domain, protocol, path) {
return protocol + "://" + domain + "/" + path;
}

var page1 = getUrl('http', 'lixuguang.github.io', 'page1.html');
var page2 = getUrl('http', 'lixuguang.github.io', 'page2.html');
+

我们使用currying来简化它:

+
1
2
var conardliSite = currying(getUrl)
var page1 = conardliSite('page1.html')('http', 'lixuguang.github.io')();
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 柯里化 currying

+

文章作者:

+

发布时间:2020年01月05日 - 09:59

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/05/FE-guide-currying/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/05/FE-guide-vuepress/index.html b/ja/2020/01/05/FE-guide-vuepress/index.html new file mode 100644 index 0000000000..8db1e984ae --- /dev/null +++ b/ja/2020/01/05/FE-guide-vuepress/index.html @@ -0,0 +1,589 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 介绍一个好用的doc展示库 ---- vuepress | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 介绍一个好用的doc展示库 ---- vuepress +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在做公司技术规范,需要能通过 B/S 方式进行分发,起初想法是做一个网站,用 html 的方式进行书写,但是后面感觉太麻烦了,所以就放弃了,偶然间看到有工具可以直接通过 md 文件编译生成网站,就像是 hexo 博客这样,我觉得这挺好,极大的节省了开发网站所需要的时间,只需要专注于内容就好了,所以我就研究了下来,整个了解过程中有这么两个工具映入了我的眼帘,一个是 doctify,另外一个就是今天的主角 vuepress

+ + +

vuepress 何许

+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:介绍一个好用的doc展示库 ---- vuepress

+

文章作者:

+

发布时间:2020年01月05日 - 17:59

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/05/FE-guide-vuepress/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/09/FE-guide-Net/index.html b/ja/2020/01/09/FE-guide-Net/index.html new file mode 100644 index 0000000000..749ef1a1dd --- /dev/null +++ b/ja/2020/01/09/FE-guide-Net/index.html @@ -0,0 +1,970 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 网络 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 网络 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

网络相关的知识虽然平时在编码阶段很少遇到,但是并不是说它不重要,尤其是前端优化层面,能从网络传输过程优化的点有很多,所以,我们需要好好掌握网络相关的知识,接下来,我将会将我了解到的网络相关的知识点进行一一整理

+

UDP - User Datagram Protocol - 用户数据报协议

面向报文

UDP 是一个面向报文(报文可以理解为一段段的数据)的协议。意思就是 UDP 只是报文的搬运工,不会对报文进行任何拆分和拼接操作。

+

具体来说

+
    +
  • 发送端,应用层将数据传递给传输层的 UDP 协议, UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了
  • +
  • 接收端,网络层将数据传递给传输层, UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作
  • +
+

不可靠性

    +
  1. UDP 是无连接的,也就是说通信不需要建立和断开连接。
  2. +
  3. UDP 也是不可靠的。协议收到什么数据就传递什么数据,并且也不会备份数据,对方能不能收到是不关心的
  4. +
  5. UDP 没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP
  6. +
+

高效

因为 UDP 没有 TCP 那么复杂,需要保证数据不丢失且有序到达。所以 UDP 的头部开销小,只有八字节,相比 TCP 的至少二十字节要少得多,在传输数据报文时是很高效的。

+

头部包含了以下几个数据

+
    +
  • 两个十六位的端口号,分别为源端口(可选字段)和目标端口
  • +
  • 整个数据报文的长度
  • +
  • 整个数据报文的检验和( IPv4 可选 字段),该字段用于发现头部信息和数据中的错误
  • +
+

传输方式

UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。

+

TCP

头部

TCP 头部比 UDP 头部复杂的多

+

对于 TCP 头部来说,以下几个字段是很重要的

+
    +
  • Sequence number(序号),它保证了 TCP 传输的报文都是有序的,对端可以通过序号顺序的拼接报文
  • +
  • Acknowledgement Number(确认编号),它表示数据接收端期望接收的下一个字节的编号是多少,同时也表示上一个序号的数据已经收到
  • +
  • Window Size(窗口大小),它表示还能接收多少字节的数据,用于流量控制
  • +
  • 标识符
      +
    • URG=1:该字段为一表示本数据报的数据部分包含紧急信息,是一个高优先级数据报文,此时紧急指针有效。紧急数据一定位于当前数据包数据部分的最前面,紧急指针标明了紧急数据的尾部。
    • +
    • ACK=1:该字段为一表示确认号字段有效。此外,TCP 还规定在连接建立后传送的所有报文段都必须把 ACK 置为一。
    • +
    • PSH=1:该字段为一表示接收端应该立即将数据 push 给应用层,而不是等到缓冲区满后再提交。
    • +
    • RST=1:该字段为一表示当前 TCP 连接出现严重问题,可能需要重新建立 TCP 连接,也可以用于拒绝非法的报文段和拒绝连接请求。
    • +
    • SYN=1:当SYN=1,ACK=0时,表示当前报文段是一个连接请求报文。当SYN=1,ACK=1时,表示当前报文段是一个同意建立连接的应答报文。
    • +
    • FIN=1:该字段为一表示此报文段是一个释放连接的请求报文。
    • +
    +
  • +
+

状态机

HTTP 是无连接的,所以作为下层的 TCP 协议也是无连接的,虽然看似 TCP 将两端连接了起来,但是其实只是两端共同维护了一个状态

+

TCP 的状态机是很复杂的,并且与建立断开连接时的握手息息相关,接下来就来详细描述下两种握手。
在这之前需要了解一个重要的性能指标 RTT 。该指标表示发送端发送数据到接收到对端数据所需的往返时间。

+

建立连接三次握手

在 TCP 协议中,主动发起请求的一端为客户端,被动连接的一端称为服务端。不管是客户端还是服务端,TCP 连接建立完后都能发送和接收数据,所以 TCP 也是一个全双工的协议。

+

起初,两端都为 CLOSED 状态。在通信开始前,双方都会创建 TCB。 服务器创建完 TCB 后遍进入 LISTEN 状态,此时开始等待客户端发送数据。

+

第一次握手

客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态,x 表示客户端的数据通信初始序号。

+

第二次握手

服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。

+

第三次握手

当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。

+

PS:第三次握手可以包含数据,通过 TCP 快速打开(TFO)技术。其实只要涉及到握手的协议,都可以使用类似 TFO 的方式,客户端和服务端存储相同 cookie,下次握手时发出 cookie 达到减少 RTT 的目的。

+

你是否有疑惑明明两次握手就可以建立起连接,为什么还需要第三次应答?

+

因为这是为了防止失效的连接请求报文段被服务端接收,从而产生错误。

+

可以想象如下场景。客户端发送了一个连接请求 A,但是因为网络原因造成了超时,这时 TCP 会启动超时重传的机制再次发送一个连接请求 B。此时请求顺利到达服务端,服务端应答完就建立了请求。如果连接请求 A 在两端关闭后终于抵达了服务端,那么这时服务端会认为客户端又需要建立 TCP 连接,从而应答了该请求并进入 ESTABLISHED 状态。此时客户端其实是 CLOSED 状态,那么就会导致服务端一直等待,造成资源的浪费。

+

PS:在建立连接中,任意一端掉线,TCP 都会重发 SYN 包,一般会重试五次,在建立连接中可能会遇到 SYN FLOOD 攻击。遇到这种情况你可以选择调低重试次数或者干脆在不能处理的情况下拒绝请求。

+

断开链接四次握手

TCP 是全双工的,在断开连接时两端都需要发送 FIN 和 ACK。

+

第一次握手

若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。

+

第二次握手

B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,表示 A 到 B 的连接已经释放,不接收 A 发的数据了。但是因为 TCP 连接时双向的,所以 B 仍旧可以发送数据给 A。

+

第三次握手

B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。

+

PS:通过延迟确认的技术(通常有时间限制,否则对方会误认为需要重传),可以将第二次和第三次握手合并,延迟 ACK 包的发送。

+

第四次握手

A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。

+

为什么 A 要进入 TIME-WAIT 状态,等待 2MSL 时间后才进入 CLOSED 状态?

+

为了保证 B 能收到 A 的确认应答。若 A 发完确认应答后直接进入 CLOSED 状态,如果确认应答因为网络问题一直没有到达,那么会造成 B 不能正常关闭。

+

ARQ 协议

ARQ 协议也就是超时重传机制。通过确认和超时机制保证了数据的正确送达,ARQ 协议包含停止等待 ARQ 和连续 ARQ

+

停止等待 ARQ

正常传输过程

只要 A 向 B 发送一段报文,都要停止发送并启动一个定时器,等待对端回应,在定时器时间内接收到对端应答就取消定时器并发送下一段报文。

+

报文丢失或出错

在报文传输的过程中可能会出现丢包。这时候超过定时器设定的时间就会再次发送丢包的数据直到对端响应,所以需要每次都备份发送的数据。

+

即使报文正常的传输到对端,也可能出现在传输过程中报文出错的问题。这时候对端会抛弃该报文并等待 A 端重传。

+

PS:一般定时器设定的时间都会大于一个 RTT 的平均时间。

+

ACK 超时或丢失

对端传输的应答也可能出现丢失或超时的情况。那么超过定时器时间 A 端照样会重传报文。这时候 B 端收到相同序号的报文会丢弃该报文并重传应答,直到 A 端发送下一个序号的报文。

+

在超时的情况下也可能出现应答很迟到达,这时 A 端会判断该序号是否已经接收过,如果接收过只需要丢弃应答即可。

+

这个协议的缺点就是传输效率低,在良好的网络环境下每次发送报文都得等待对端的 ACK 。

+

连续 ARQ

在连续 ARQ 中,发送端拥有一个发送窗口,可以在没有收到应答的情况下持续发送窗口内的数据,这样相比停止等待 ARQ 协议来说减少了等待时间,提高了效率。

+

累计确认

连续 ARQ 中,接收端会持续不断收到报文。如果和停止等待 ARQ 中接收一个报文就发送一个应答一样,就太浪费资源了。通过累计确认,可以在收到多个报文以后统一回复一个应答报文。报文中的 ACK 可以用来告诉发送端这个序号之前的数据已经全部接收到了,下次请发送这个序号 + 1的数据。

+

但是累计确认也有一个弊端。在连续接收报文时,可能会遇到接收到序号 5 的报文后,并未接到序号 6 的报文,然而序号 7 以后的报文已经接收。遇到这种情况时,ACK 只能回复 6,这样会造成发送端重复发送数据,这种情况下可以通过 Sack 来解决,这个会在下文说到。

+

滑动窗口

在上面小节中讲到了发送窗口。在 TCP 中,两端都维护着窗口:分别为发送端窗口和接收端窗口。

+

发送端窗口包含已发送但未收到应答的数据和可以发送但是未发送的数据。

+

发送端窗口是由接收窗口剩余大小决定的。接收方会把当前接收窗口的剩余大小写入应答报文,发送端收到应答后根据该值和当前网络拥塞情况设置发送窗口的大小,所以发送窗口的大小是不断变化的。

+

当发送端接收到应答报文后,会随之将窗口进行滑动

+

滑动窗口实现了流量控制。接收方通过报文告知发送方还可以发送多少数据,从而保证接收方能够来得及接收数据。

+

Zero 窗口

在发送报文的过程中,可能会遇到对端出现零窗口的情况。在该情况下,发送端会停止发送数据,并启动 persistent timer 。该定时器会定时发送请求给对端,让对端告知窗口大小。在重试次数超过一定次数后,可能会中断 TCP 链接。

+

拥塞处理

拥塞处理和流量控制不同,后者是作用于接收方,保证接收方来得及接受数据。而前者是作用于网络,防止过多的数据拥塞网络,避免出现网络负载过大的情况。

+

拥塞处理包括了四个算法,分别为:慢开始拥塞避免快速重传快速恢复

+

慢开始算法

慢开始算法,顾名思义,就是在传输开始时将发送窗口慢慢指数级扩大,从而避免一开始就传输大量数据导致网络拥塞。

+

慢开始算法步骤具体如下

+
    +
  1. 连接初始设置拥塞窗口(Congestion Window) 为 1 MSS(一个分段的最大数据量)
  2. +
  3. 每过一个 RTT 就将窗口大小乘二
  4. +
  5. 指数级增长肯定不能没有限制的,所以有一个阈值限制,当窗口大小大于阈值时就会启动拥塞避免算法。
  6. +
+

拥塞避免算法

拥塞避免算法相比简单点,每过一个 RTT 窗口大小只加一,这样能够避免指数级增长导致网络拥塞,慢慢将大小调整到最佳值。

+

在传输过程中可能定时器超时的情况,这时候 TCP 会认为网络拥塞了,会马上进行以下步骤:

+
    +
  • 将阈值设为当前拥塞窗口的一半
  • +
  • 将拥塞窗口设为 1 MSS
  • +
  • 启动拥塞避免算法
  • +
+

快速重传

快速重传一般和快恢复一起出现。一旦接收端收到的报文出现失序的情况,接收端只会回复最后一个顺序正确的报文序号(没有 Sack 的情况下)。如果收到三个重复的 ACK,无需等待定时器超时再重发而是启动快速重传。具体算法分为两种:

+

TCP Taho 实现如下

    +
  • 将阈值设为当前拥塞窗口的一半
  • +
  • 将拥塞窗口设为 1 MSS
  • +
  • 重新开始慢开始算法
  • +
+

TCP Reno 实现如下

    +
  • 拥塞窗口减半
  • +
  • 将阈值设为当前拥塞窗口
  • +
  • 进入快恢复阶段(重发对端需要的包,一旦收到一个新的 ACK 答复就退出该阶段)
  • +
  • 使用拥塞避免算法
  • +
+

TCP New Ren 改进后的快恢复

TCP New Reno 算法改进了之前 TCP Reno 算法的缺陷。在之前,快恢复中只要收到一个新的 ACK 包,就会退出快恢复。

+

在 TCP New Reno 中,TCP 发送方先记下三个重复 ACK 的分段的最大序号。

+

假如我有一个分段数据是 1 ~ 10 这十个序号的报文,其中丢失了序号为 3 和 7 的报文,那么该分段的最大序号就是 10。发送端只会收到 ACK 序号为 3 的应答。这时候重发序号为 3 的报文,接收方顺利接收并会发送 ACK 序号为 7 的应答。这时候 TCP 知道对端是有多个包未收到,会继续发送序号为 7 的报文,接收方顺利接收并会发送 ACK 序号为 11 的应答,这时发送端认为这个分段接收端已经顺利接收,接下来会退出快恢复阶段。

+

HTTP

HTTP 协议是个无状态协议,不会保存状态。

+

PostGet 的区别

先引入副作用幂等的概念。

+
+

副作用指对服务器上的资源做改变,搜索是无副作用的,注册是副作用的。

+
+
+

幂等指发送 M 和 N 次请求(两者不相同且都大于 1),服务器上资源的状态一致,比如注册 10 个和 11 个帐号是不幂等的,对文章进行更改 10 次和 11 次是幂等的。

+
+

在规范的应用场景上说, Get 多用于无副作用,幂等的场景,例如搜索关键字。 Post 多用于副作用,不幂等的场景,例如注册。

+

在技术上说:

+
    +
  • Get 请求能缓存,Post 不能
  • +
  • Post 相对 Get 安全一点点,因为Get 请求都包含在 URL 里,且会被浏览器保存历史纪录,Post 不会,但是在抓包的情况下都是一样的。
  • +
  • Post 可以通过 request body来传输比 Get 更多的数据,Get 没有这个技术
  • +
  • URL有长度限制,会影响 Get 请求,但是这个长度限制是浏览器规定的,不是 RFC 规定的
  • +
  • Post 支持更多的编码类型且不对数据类型限制
  • +
+

常见状态码

2XX 成功

200 OK,表示从客户端发来的请求在服务器端被正确处理
204 No content,表示请求成功,但响应报文不含实体的主体部分
205 Reset Content,表示请求成功,但响应报文不含实体的主体部分,但是与 204 响应不同在于要求请求方重置内容
206 Partial Content,进行范围请求

+

3XX 重定向

301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
302 found,临时性重定向,表示资源临时被分配了新的 URL
303 see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源
304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况
307 temporary redirect,临时重定向,和302含义类似,但是期望客户端保持请求方法不变向新的地址发出请求

+

4XX 客户端错误

400 bad request,请求报文存在语法错误
401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
403 forbidden,表示对请求资源的访问被服务器拒绝
404 not found,表示在服务器上没有找到请求的资源

+

5XX 服务器错误

500 internal sever error,表示服务器端在执行请求时发生了错误
501 Not Implemented,表示服务器不支持当前请求所需要的某个功能
503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求

+

HTTP 首部

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
通用字段作用
Cache-Control控制缓存的行为
Connection浏览器想要优先使用的连接类型,比如 keep-alive
Date创建报文时间
Pragma报文指令
Via代理服务器相关信息
Transfer-Encoding传输编码方式
Upgrade要求客户端升级协议
Warning在内容中可能存在错误
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
请求字段作用
Accept能正确接收的媒体类型
Accept-Charset能正确接收的字符集
Accept-Encoding能正确接收的编码格式列表
Accept-Language能正确接收的语言列表
Expect期待服务端的指定行为
From请求方邮箱地址
Host服务器的域名
If-Match两端资源标记比较
If-Modified-Since本地资源未修改返回 304(比较时间)
If-None-Match本地资源未修改返回 304(比较标记)
User-Agent客户端信息
Max-Forwards限制可被代理及网关转发的次数
Proxy-Authorization向代理服务器发送验证信息
Range请求某个内容的一部分
Referer表示浏览器所访问的前一个页面
TE传输编码方式
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
响应字段作用
Accept-Ranges是否支持某些种类的范围
Age资源在代理缓存中存在的时间
ETag资源标识
Location客户端重定向到某个 URL
Proxy-Authenticate向代理服务器发送验证信息
Server服务器名字
WWW-Authenticate获取资源需要的验证信息
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
实体字段作用
Allow资源的正确请求方式
Content-Encoding内容的编码格式
Content-Language内容使用的语言
Content-Lengthrequest body 长度
Content-Location返回数据的备用地址
Content-MD5Base64加密格式的内容 MD5检验值
Content-Range内容的位置范围
Content-Type内容的媒体类型
Expires内容的过期时间
Last_modified内容的最后修改时间
+

HTTPS

HTTPS 还是通过了 HTTP 来传输信息,但是信息通过 TLS 协议进行了加密。

+

TLS

TLS 协议位于传输层之上,应用层之下。首次进行 TLS 协议传输需要两个 RTT ,接下来可以通过 Session Resumption 减少到一个 RTT

+

TLS 中使用了两种加密技术,分别为:对称加密非对称加密

+

对称加密:
对称加密就是两边拥有相同的秘钥,两边都知道如何将密文加密解密。

+

非对称加密:
有公钥私钥之分,公钥所有人都可以知道,可以将数据用公钥加密,但是将数据解密必须使用私钥解密,私钥只有分发公钥的一方才知道。

+

TLS 握手过程如下图:

+
    +
  1. 客户端发送一个随机值,需要的协议和加密方式
  2. +
  3. 服务端收到客户端的随机值,自己也产生一个随机值,并根据客户端需求的协议和加密方式来使用对应的方式,发送自己的证书(如果需要验证客户端证书需要说明)
  4. +
  5. 客户端收到服务端的证书并验证是否有效,验证通过会再生成一个随机值,通过服务端证书的公钥去加密这个随机值并发送给服务端,如果服务端需要验证客户端证书的话会附带证书
  6. +
  7. 服务端收到加密过的随机值并使用私钥解密获得第三个随机值,这时候两端都拥有了三个随机值,可以通过这三个随机值按照之前约定的加密方式生成密钥,接下来的通信就可以通过该密钥来加密解密
  8. +
+

通过以上步骤可知,在 TLS 握手阶段,两端使用非对称加密的方式来通信,但是因为非对称加密损耗的性能比对称加密大,所以在正式传输数据时,两端使用对称加密的方式通信。

+

PS:以上说明的都是 TLS 1.2 协议的握手情况,在 1.3 协议中,首次建立连接只需要一个 RTT,后面恢复连接不需要 RTT 了。

+

HTTP 2.0

HTTP 2.0 相比于 HTTP 1.X,可以说是大幅度提高了 web 的性能。

+

在 HTTP 1.X 中,为了性能考虑,我们会引入雪碧图、将小图内联、使用多个域名等等的方式。这一切都是因为浏览器限制了同一个域名下的请求数量,当页面中需要请求很多资源的时候,队头阻塞(Head of line blocking)会导致在达到最大请求数量时,剩余的资源需要等待其他资源请求完成后才能发起请求。

+

你可以通过 该链接 感受下 HTTP 2.0 比 HTTP 1.X 到底快了多少。

+

在 HTTP 1.X 中,因为队头阻塞的原因,你会发现请求是这样的
lWJGkt.png
在 HTTP 2.0 中,因为引入了多路复用,你会发现请求是这样的
lWJa6g.png

+

二进制传输

HTTP 2.0 中所有加强性能的核心点在于此。在之前的 HTTP 版本中,我们是通过文本的方式传输数据。在 HTTP 2.0 中引入了新的编码机制,所有传输的数据都会被分割,并采用二进制格式编码。

+

多路复用

在 HTTP 2.0 中,有两个非常重要的概念,分别是帧(frame)流(stream)
帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流。

+

多路复用,就是在一个 TCP 连接中可以存在多条流。换句话说,也就是可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能。

+

Header 压缩

在 HTTP 1.X 中,我们使用文本的形式传输 header,在 header 携带 cookie 的情况下,可能每次都需要重复传输几百到几千的字节。

+

在 HTTP 2.0 中,使用了 HPACK 压缩格式对传输的 header 进行编码,减少了 header 的大小。并在两端维护了索引表,用于记录出现过的 header ,后面在传输过程中就可以传输已经记录过的 header 的键名,对端收到数据后就可以通过键名找到对应的值。

+

服务端 Push

在 HTTP 2.0 中,服务端可以在客户端某个请求后,主动推送其他资源。

+

可以想象以下情况,某些资源客户端是一定会请求的,这时就可以采取服务端 push 的技术,提前给客户端推送必要的资源,这样就可以相对减少一点延迟时间。当然在浏览器兼容的情况下你也可以使用 prefetch 。

+

QUIC

这是一个谷歌出品的基于 UDP 实现的同为传输层的协议,目标很远大,希望替代 TCP 协议。

+
    +
  • 该协议支持多路复用,虽然 HTTP 2.0 也支持多路复用,但是下层仍是 TCP,因为 TCP 的重传机制,只要一个包丢失就得判断丢失包并且重传,导致发生队头阻塞的问题,但是 UDP 没有这个机制
  • +
  • 实现了自己的加密协议,通过类似 TCP 的 TFO 机制可以实现 0-RTT,当然 TLS 1.3 已经实现了 0-RTT 了
  • +
  • 支持重传和纠错机制(向前恢复),在只丢失一个包的情况下不需要重传,使用纠错机制恢复丢失的包
      +
    • 纠错机制:通过异或的方式,算出发出去的数据的异或值并单独发出一个包,服务端在发现有一个包丢失的情况下,通过其他数据包和异或值包算出丢失包
    • +
    • 在丢失两个包或以上的情况就使用重传机制,因为算不出来了
    • +
    +
  • +
+

DNS

DNS 的作用就是通过域名查询到具体的 IP。

+

因为 IP 存在数字和英文的组合(IPv6),很不利于人类记忆,所以就出现了域名。你可以把域名看成是某个 IP 的别名,DNS 就是去查询这个别名的真正名称是什么。

+

在 TCP 握手之前就已经进行了 DNS 查询,这个查询是操作系统自己做的。当你在浏览器中想访问 www.google.com 时,会进行一下操作:

+
    +
  1. 操作系统会首先在本地缓存中查询
  2. +
  3. 没有的话会去系统配置的 DNS 服务器中查询
  4. +
  5. 如果这时候还没得话,会直接去 DNS 根服务器查询,这一步查询会找出负责 com 这个一级域名的服务器
  6. +
  7. 然后去该服务器查询 google 这个二级域名
  8. +
  9. 接下来三级域名的查询其实是我们配置的,你可以给 www 这个域名配置一个 IP,然后还可以给别的三级域名配置一个 IP
  10. +
+

以上介绍的是 DNS 迭代查询,还有种是递归查询,区别就是前者是由客户端去做请求,后者是由系统配置的 DNS 服务器做请求,得到结果后将数据返回给客户端。

+

PS:DNS 是基于 UDP 做的查询。

+

从输入 URL 到页面加载完成的过程

这是一个很经典的面试题,在这题中可以将本文讲得内容都串联起来。

+
    +
  1. 首先做 DNS 查询,如果这一步做了智能 DNS 解析的话,会提供访问速度最快的 IP 地址回来
  2. +
  3. 接下来是 TCP 握手,应用层会下发数据给传输层,这里 TCP 协议会指明两端的端口号,然后下发给网络层。网络层中的 IP 协议会确定 IP 地址,并且指示了数据传输中如何跳转路由器。然后包会再被封装到数据链路层的数据帧结构中,最后就是物理层面的传输了
  4. +
  5. TCP 握手结束后会进行 TLS 握手,然后就开始正式的传输数据
  6. +
  7. 数据在进入服务端之前,可能还会先经过负责负载均衡的服务器,它的作用就是将请求合理的分发到多台服务器上,这时假设服务端会响应一个 HTML 文件
  8. +
  9. 首先浏览器会判断状态码是什么,如果是 200 那就继续解析,如果 400500 的话就会报错,如果 300 的话会进行重定向,这里会有个重定向计数器,避免过多次的重定向,超过次数也会报错
  10. +
  11. 浏览器开始解析文件,如果是 gzip 格式的话会先解压一下,然后通过文件的编码格式知道该如何去解码文件
  12. +
  13. 文件解码成功后会正式开始渲染流程,先会根据 HTML 构建 DOM 树,有 CSS 的话会去构建 CSSOM 树。如果遇到 script 标签的话,会判断是否存在 async 或者 defer ,前者会并行进行下载并执行 JS ,后者会先下载文件,然后等待 HTML 解析完成后顺序执行,如果以上都没有,就会阻塞住渲染流程直到 JS 执行完毕。遇到文件下载的会去下载文件,这里如果使用 HTTP 2.0 协议的话会极大的提高多图的下载效率。
  14. +
  15. 初始的 HTML 被完全加载和解析后会触发 DOMContentLoaded 事件
  16. +
  17. CSSOM 树和 DOM 树构建完成后会开始生成 Render 树,这一步就是确定页面元素的布局、样式等等诸多方面的东西
  18. +
  19. 在生成 Render 树的过程中,浏览器就开始调用 GPU 绘制,合成图层,将内容显示在屏幕上了
  20. +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 网络

+

文章作者:

+

发布时间:2020年01月09日 - 07:35

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/09/FE-guide-Net/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/09/FE-guide-data-structure/index.html b/ja/2020/01/09/FE-guide-data-structure/index.html new file mode 100644 index 0000000000..fbd24b825f --- /dev/null +++ b/ja/2020/01/09/FE-guide-data-structure/index.html @@ -0,0 +1,706 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 计算机通识 ---- 数据结构 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 计算机通识 ---- 数据结构 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

网络相关的知识虽然平时在编码阶段很少遇到,但是并不是说它不重要,尤其是前端优化层面,能从网络传输过程优化的点有很多,所以,我们需要好好掌握网络相关的知识,接下来,我将会将我了解到的网络相关的知识点进行一一整理

+

栈 Heap

+

栈是一个线性结构,在计算机中是一个相当常见的数据结构。
栈的特点是只能在某一端添加或删除数据,遵循先进后出(FILO)的原则

+
+

实现

每种数据结构都可以用很多种方式来实现,其实可以把栈看成是数组的一个子集,所以这里使用数组来实现

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// js代码
class Stack {
constructor() {
this.stack = []
}
push(item) {
this.stack.push(item)
}
pop() {
this.stack.pop()
}
peek() { // 取最后一项
return this.stack[this.getCount() - 1]
}
getCount() {
return this.stack.length
}
isEmpty() {
return this.getCount() === 0
}
}
+

应用

选取了 LeetCode 上序号为 20 的题目

+

题意是匹配括号,可以通过栈的特性来完成这道题目

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var isValid = function(str) {
let map = {
'(': -1,
')': 1,
'[': -2,
']': 2,
'{': -3,
'}': 3
}
let stack = [] // 空数组
for (let i = 0; i < str.length; i++) { // 遍历字符串每个字符
if (map[str[i]] < 0) { // 如果是左边括号,入栈
stack.push(str[i])
} else { // 否则出栈,判断左右括号加到一起是不是0
let last = stack.pop()
if (map[last] + map[str[i]] != 0) return false
}
}
if (stack.length > 0) return false // 循环完成后,判断数组中时候还有剩下的,有剩下的说明括号没闭合
return true // 否则没剩下的,都闭合了
}
+ +

队列

+

队列一个线性结构,特点是在某一端添加数据,在另一端删除数据,遵循先进先出(FIFO)的原则。

+
+

实现

这里会讲解两种实现队列的方式,分别是单链队列循环队列

+
    +
  • 链队列即队列的链式存储结构,结构上就是一个单链表,但数据只能是头进尾出。链式结构更加的灵活,特别是在存储空间上,基本不会出现溢出的情况,所以不用像循环队列一样判断队列是否已满,且空间的利用率相对较高。
      +
    • 链队列front指向头结点,头结点不存储数据,rear指向队尾结点。
    • +
    +
  • +
+
    +
  • 循环队列即为头尾相接的队列,它的最大存储空间和顺序队列一样由数组界定,但队列的长度并不一定等同于数组的长度;循环队列的队首和队尾分别由两个指针front、rear标识,于是这样就能做到首尾相接。

    +
  • +
  • 链队列:为操作方便,给链队列添加一个头结点

    +
  • +
  • 循环队列:附设两个指针front和rear分别指示队列头元素及尾元素的位置,每当插入新的队尾元素是,尾指针加1;每当删除队列头元素是,头指针加1

    +
      +
    • 如果用循环队列,则必须设定一个最大队列长度;若无法确定最大长度,则宜采用链队列。
    • +
    +
  • +
+

单链队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// js代码

class Queue {
constructor() {
this.queue = []
}
enQueue(item) {
this.queue.push(item)
}
deQueue() {
return this.queue.shift()
}
getHeader() {
return this.queue[0]
}
getLength() {
return this.queue.length
}
isEmpty() {
return this.getLength() === 0
}
}
+

因为单链队列在出队操作的时候需要 O(n) 的时间复杂度,所以引入了循环队列。
循环队列的出队操作平均是 O(1) 的时间复杂度。

+

循环队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// js代码

class SqQueue {
constructor(length) {
this.queue = new Array(length + 1)
// 队头
this.first = 0
// 队尾
this.last = 0
// 当前队列大小
this.size = 0
}
enQueue(item) {
// 判断队尾 + 1 是否为队头
// 如果是就代表需要扩容数组
// % this.queue.length 是为了防止数组越界
if (this.first === (this.last + 1) % this.queue.length) {
this.resize(this.getLength() * 2 + 1)
}
this.queue[this.last] = item
this.size++
this.last = (this.last + 1) % this.queue.length
}
deQueue() {
if (this.isEmpty()) {
throw Error('Queue is empty')
}
let r = this.queue[this.first]
this.queue[this.first] = null
this.first = (this.first + 1) % this.queue.length
this.size--
// 判断当前队列大小是否过小
// 为了保证不浪费空间,在队列空间等于总长度四分之一时
// 且不为 2 时缩小总长度为当前的一半
if (this.size === this.getLength() / 4 && this.getLength() / 2 !== 0) {
this.resize(this.getLength() / 2)
}
return r
}
getHeader() {
if (this.isEmpty()) {
throw Error('Queue is empty')
}
return this.queue[this.first]
}
getLength() {
return this.queue.length - 1
}
isEmpty() {
return this.first === this.last
}
resize(length) {
let q = new Array(length)
for (let i = 0; i < length; i++) {
q[i] = this.queue[(i + this.first) % this.queue.length]
}
this.queue = q
this.first = 0
this.last = this.size
}
}
+ +

链表

+

链表是一个线性结构,同时也是一个天然的递归结构。链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。

+
+

实现

单向链表

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// js代码

class Node {
constructor(v, next) {
this.value = v
this.next = next
}
}
class LinkList {
constructor() {
// 链表长度
this.size = 0
// 虚拟头部
this.dummyNode = new Node(null, null)
}
find(header, index, currentIndex) {
if (index === currentIndex) return header
return this.find(header.next, index, currentIndex + 1)
}
addNode(v, index) {
this.checkIndex(index)
// 当往链表末尾插入时,prev.next 为空
// 其他情况时,因为要插入节点,所以插入的节点
// 的 next 应该是 prev.next
// 然后设置 prev.next 为插入的节点
let prev = this.find(this.dummyNode, index, 0)
prev.next = new Node(v, prev.next)
this.size++
return prev.next
}
insertNode(v, index) {
return this.addNode(v, index)
}
addToFirst(v) {
return this.addNode(v, 0)
}
addToLast(v) {
return this.addNode(v, this.size)
}
removeNode(index, isLast) {
this.checkIndex(index)
index = isLast ? index - 1 : index
let prev = this.find(this.dummyNode, index, 0)
let node = prev.next
prev.next = node.next
node.next = null
this.size--
return node
}
removeFirstNode() {
return this.removeNode(0)
}
removeLastNode() {
return this.removeNode(this.size, true)
}
checkIndex(index) {
if (index < 0 || index > this.size) throw Error('Index error')
}
getNode(index) {
this.checkIndex(index)
if (this.isEmpty()) return
return this.find(this.dummyNode, index, 0).next
}
isEmpty() {
return this.size === 0
}
getSize() {
return this.size
}
}
+ +

二叉树

树拥有很多种结构,二叉树是树中最常用的结构,同时也是一个天然的递归结构。

+

二叉树拥有一个根节点,每个节点至多拥有两个子节点,分别为:左节点和右节点。树的最底部节点称之为叶节点,当一颗树的叶数量数量为满时,该树可以称之为满二叉树。

+

二分搜索树

二分搜索树也是二叉树,拥有二叉树的特性。但是区别在于二分搜索树每个节点的值都比他的左子树的值大,比右子树的值小。

+

这种存储方式很适合于数据搜索。如下图所示,当需要查找 6 的时候,因为需要查找的值比根节点的值大,所以只需要在根节点的右子树上寻找,大大提高了搜索效率。

+

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// js代码

class Node {
constructor(value) {
this.value = value
this.left = null
this.right = null
}
}
class BST {
constructor() {
this.root = null
this.size = 0
}
getSize() {
return this.size
}
isEmpty() {
return this.size === 0
}
addNode(v) {
this.root = this._addChild(this.root, v)
}
// 添加节点时,需要比较添加的节点值和当前
// 节点值的大小
_addChild(node, v) {
if (!node) {
this.size++
return new Node(v)
}
if (node.value > v) {
node.left = this._addChild(node.left, v)
} else if (node.value < v) {
node.right = this._addChild(node.right, v)
}
return node
}
}
+ +

以上是最基本的二分搜索树实现,接下来实现树的遍历。

+

对于树的遍历来说,有三种遍历方法,分别是先序遍历中序遍历后序遍历

+

三种遍历的区别在于何时访问节点。在遍历树的过程中,每个节点都会遍历三次,分别是遍历到自己,遍历左子树和遍历右子树。如果需要实现先序遍历,那么只需要第一次遍历到节点时进行操作即可。

+

以下都是递归实现.

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// js代码

// 先序遍历可用于打印树的结构
// 先序遍历先访问根节点,然后访问左节点,最后访问右节点。
preTraversal() {
this._pre(this.root)
}
_pre(node) {
if (node) {
console.log(node.value)
this._pre(node.left)
this._pre(node.right)
}
}
// 中序遍历可用于排序
// 对于 BST 来说,中序遍历可以实现一次遍历就
// 得到有序的值
// 中序遍历表示先访问左节点,然后访问根节点,最后访问右节点。
midTraversal() {
this._mid(this.root)
}
_mid(node) {
if (node) {
this._mid(node.left)
console.log(node.value)
this._mid(node.right)
}
}
// 后序遍历可用于先操作子节点
// 再操作父节点的场景
// 后序遍历表示先访问左节点,然后访问右节点,最后访问根节点。
backTraversal() {
this._back(this.root)
}
_back(node) {
if (node) {
this._back(node.left)
this._back(node.right)
console.log(node.value)
}
}
+ +

以上的这几种遍历都可以称之为深度遍历,对应的还有种遍历叫做广度遍历,也就是一层层地遍历树。对于广度遍历来说,我们需要利用之前讲过的队列结构来完成。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

breadthTraversal() {
if (!this.root) return null
let q = new Queue()
// 将根节点入队
q.enQueue(this.root)
// 循环判断队列是否为空,为空
// 代表树遍历完毕
while (!q.isEmpty()) {
// 将队首出队,判断是否有左右子树
// 有的话,就先左后右入队
let n = q.deQueue()
console.log(n.value)
if (n.left) q.enQueue(n.left)
if (n.right) q.enQueue(n.right)
}
}
+

接下来先介绍如何在树中寻找最小值或最大数。因为二分搜索树的特性,所以最小值一定在根节点的最左边,最大值相反

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码

getMin() {
return this._getMin(this.root).value
}
_getMin(node) {
if (!node.left) return node
return this._getMin(node.left)
}
getMax() {
return this._getMax(this.root).value
}
_getMax(node) {
if (!node.right) return node
return this._getMin(node.right)
}
+ +

向上取整和向下取整,这两个操作是相反的,所以代码也是类似的,这里只介绍如何向下取整。既然是向下取整,那么根据二分搜索树的特性,值一定在根节点的左侧。只需要一直遍历左子树直到当前节点的值不再大于等于需要的值,然后判断节点是否还拥有右子树。如果有的话,继续上面的递归判断。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

floor(v) {
let node = this._floor(this.root, v)
return node ? node.value : null
}
_floor(node, v) {
if (!node) return null
if (node.value === v) return v
// 如果当前节点值还比需要的值大,就继续递归
if (node.value > v) {
return this._floor(node.left, v)
}
// 判断当前节点是否拥有右子树
let right = this._floor(node.right, v)
if (right) return right
return node
}
+

排名,这是用于获取给定值的排名或者排名第几的节点的值,这两个操作也是相反的,所以这个只介绍如何获取排名第几的节点的值。对于这个操作而言,我们需要略微的改造点代码,让每个节点拥有一个 size 属性。该属性表示该节点下有多少子节点(包含自身)。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// js代码

class Node {
constructor(value) {
this.value = value
this.left = null
this.right = null
// 修改代码
this.size = 1
}
}
// 新增代码
_getSize(node) {
return node ? node.size : 0
}
_addChild(node, v) {
if (!node) {
return new Node(v)
}
if (node.value > v) {
// 修改代码
node.size++
node.left = this._addChild(node.left, v)
} else if (node.value < v) {
// 修改代码
node.size++
node.right = this._addChild(node.right, v)
}
return node
}
select(k) {
let node = this._select(this.root, k)
return node ? node.value : null
}
_select(node, k) {
if (!node) return null
// 先获取左子树下有几个节点
let size = node.left ? node.left.size : 0
// 判断 size 是否大于 k
// 如果大于 k,代表所需要的节点在左节点
if (size > k) return this._select(node.left, k)
// 如果小于 k,代表所需要的节点在右节点
// 注意这里需要重新计算 k,减去根节点除了右子树的节点数量
if (size < k) return this._select(node.right, k - size - 1)
return node
}
+ +

接下来讲解的是二分搜索树中最难实现的部分:删除节点。因为对于删除节点来说,会存在以下几种情况

+
    +
  • 需要删除的节点没有子树
  • +
  • 需要删除的节点只有一条子树
  • +
  • 需要删除的节点有左右两条树
  • +
+

对于前两种情况很好解决,但是第三种情况就有难度了,所以先来实现相对简单的操作:删除最小节点,对于删除最小节点来说,是不存在第三种情况的,删除最大节点操作是和删除最小节点相反的,所以这里也就不再赘述。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码

delectMin() {
this.root = this._delectMin(this.root)
console.log(this.root)
}
_delectMin(node) {
// 一直递归左子树
// 如果左子树为空,就判断节点是否拥有右子树
// 有右子树的话就把需要删除的节点替换为右子树
if ((node != null) & !node.left) return node.right
node.left = this._delectMin(node.left)
// 最后需要重新维护下节点的 `size`
node.size = this._getSize(node.left) + this._getSize(node.right) + 1
return node
}
+ +

最后讲解的就是如何删除任意节点了。对于这个操作,T.Hibbard 在 1962 年提出了解决这个难题的办法,也就是如何解决第三种情况。

+

当遇到这种情况时,需要取出当前节点的后继节点(也就是当前节点右子树的最小节点)来替换需要删除的节点。然后将需要删除节点的左子树赋值给后继结点,右子树删除后继结点后赋值给他。

+

你如果对于这个解决办法有疑问的话,可以这样考虑。因为二分搜索树的特性,父节点一定比所有左子节点大,比所有右子节点小。那么当需要删除父节点时,势必需要拿出一个比父节点大的节点来替换父节点。这个节点肯定不存在于左子树,必然存在于右子树。然后又需要保持父节点都是比右子节点小的,那么就可以取出右子树中最小的那个节点来替换父节点。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// js代码

delect(v) {
this.root = this._delect(this.root, v)
}
_delect(node, v) {
if (!node) return null
// 寻找的节点比当前节点小,去左子树找
if (node.value < v) {
node.right = this._delect(node.right, v)
} else if (node.value > v) {
// 寻找的节点比当前节点大,去右子树找
node.left = this._delect(node.left, v)
} else {
// 进入这个条件说明已经找到节点
// 先判断节点是否拥有拥有左右子树中的一个
// 是的话,将子树返回出去,这里和 `_delectMin` 的操作一样
if (!node.left) return node.right
if (!node.right) return node.left
// 进入这里,代表节点拥有左右子树
// 先取出当前节点的后继结点,也就是取当前节点右子树的最小值
let min = this._getMin(node.right)
// 取出最小值后,删除最小值
// 然后把删除节点后的子树赋值给最小值节点
min.right = this._delectMin(node.right)
// 左子树不动
min.left = node.left
node = min
}
// 维护 size
node.size = this._getSize(node.left) + this._getSize(node.right) + 1
return node
}
+ +

AVL 树

+

二分搜索树实际在业务中是受到限制的,因为并不是严格的 O(logN),在极端情况下会退化成链表,比如加入一组升序的数字就会造成这种情况。

+
+
+

AVL 树改进了二分搜索树,在 AVL 树中任意节点的左右子树的高度差都不大于 1,这样保证了时间复杂度是严格的 O(logN)。基于此,对 AVL 树增加或删除节点时可能需要旋转树来达到高度的平衡。

+
+

实现

因为 AVL 树是改进了二分搜索树,所以部分代码是于二分搜索树重复的,对于重复内容不作再次解析。

+

对于 AVL 树来说,添加节点会有四种情况
lWB0nf.png

+

对于左左情况来说,新增加的节点位于节点 2 的左侧,这时树已经不平衡,需要旋转。因为搜索树的特性,节点比左节点大,比右节点小,所以旋转以后也要实现这个特性。

+

旋转之前:new < 2 < C < 3 < B < 5 < A,右旋之后节点 3 为根节点,这时候需要将节点 3 的右节点加到节点 5 的左边,最后还需要更新节点的高度。

+

对于右右情况来说,相反于左左情况,所以不再赘述。

+

对于左右情况来说,新增加的节点位于节点 4 的右侧。对于这种情况,需要通过两次旋转来达到目的。

+

首先对节点的左节点左旋,这时树满足左左的情况,再对节点进行一次右旋就可以达到目的。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// js代码

class Node {
constructor(value) {
this.value = value
this.left = null
this.right = null
this.height = 1
}
}

class AVL {
constructor() {
this.root = null
}
addNode(v) {
this.root = this._addChild(this.root, v)
}
_addChild(node, v) {
if (!node) {
return new Node(v)
}
if (node.value > v) {
node.left = this._addChild(node.left, v)
} else if (node.value < v) {
node.right = this._addChild(node.right, v)
} else {
node.value = v
}
node.height =
1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
let factor = this._getBalanceFactor(node)
// 当需要右旋时,根节点的左树一定比右树高度高
if (factor > 1 && this._getBalanceFactor(node.left) >= 0) {
return this._rightRotate(node)
}
// 当需要左旋时,根节点的左树一定比右树高度矮
if (factor < -1 && this._getBalanceFactor(node.right) <= 0) {
return this._leftRotate(node)
}
// 左右情况
// 节点的左树比右树高,且节点的左树的右树比节点的左树的左树高
if (factor > 1 && this._getBalanceFactor(node.left) < 0) {
node.left = this._leftRotate(node.left)
return this._rightRotate(node)
}
// 右左情况
// 节点的左树比右树矮,且节点的右树的右树比节点的右树的左树矮
if (factor < -1 && this._getBalanceFactor(node.right) > 0) {
node.right = this._rightRotate(node.right)
return this._leftRotate(node)
}

return node
}
_getHeight(node) {
if (!node) return 0
return node.height
}
_getBalanceFactor(node) {
return this._getHeight(node.left) - this._getHeight(node.right)
}
// 节点右旋
// 5 2
// / \ / \
// 2 6 ==> 1 5
// / \ / / \
// 1 3 new 3 6
// /
// new
_rightRotate(node) {
// 旋转后新根节点
let newRoot = node.left
// 需要移动的节点
let moveNode = newRoot.right
// 节点 2 的右节点改为节点 5
newRoot.right = node
// 节点 5 左节点改为节点 3
node.left = moveNode
// 更新树的高度
node.height =
1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
newRoot.height =
1 +
Math.max(this._getHeight(newRoot.left), this._getHeight(newRoot.right))

return newRoot
}
// 节点左旋
// 4 6
// / \ / \
// 2 6 ==> 4 7
// / \ / \ \
// 5 7 2 5 new
// \
// new
_leftRotate(node) {
// 旋转后新根节点
let newRoot = node.right
// 需要移动的节点
let moveNode = newRoot.left
// 节点 6 的左节点改为节点 4
newRoot.left = node
// 节点 4 右节点改为节点 5
node.right = moveNode
// 更新树的高度
node.height =
1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
newRoot.height =
1 +
Math.max(this._getHeight(newRoot.left), this._getHeight(newRoot.right))

return newRoot
}
}
+ +

Trie

+

在计算机科学,trie,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。
简单点来说,这个结构的作用大多是为了方便搜索字符串,该树有以下几个特点

+
+
    +
  • 根节点代表空字符串,每个节点都有 N(假如搜索英文字符,就有 26 条) 条链接,每条链接代表一个字符
  • +
  • 节点不存储字符,只有路径才存储,这点和其他的树结构不同
  • +
  • 从根节点开始到任意一个节点,将沿途经过的字符连接起来就是该节点对应的字符串
  • +
+

实现

总得来说 Trie 的实现相比别的树结构来说简单的很多,实现就以搜索英文字符为例。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// js代码

class TrieNode {
constructor() {
// 代表每个字符经过节点的次数
this.path = 0
// 代表到该节点的字符串有几个
this.end = 0
// 链接
this.next = new Array(26).fill(null)
}
}
class Trie {
constructor() {
// 根节点,代表空字符
this.root = new TrieNode()
}
// 插入字符串
insert(str) {
if (!str) return
let node = this.root
for (let i = 0; i < str.length; i++) {
// 获得字符先对应的索引
let index = str[i].charCodeAt() - 'a'.charCodeAt()
// 如果索引对应没有值,就创建
if (!node.next[index]) {
node.next[index] = new TrieNode()
}
node.path += 1
node = node.next[index]
}
node.end += 1
}
// 搜索字符串出现的次数
search(str) {
if (!str) return
let node = this.root
for (let i = 0; i < str.length; i++) {
let index = str[i].charCodeAt() - 'a'.charCodeAt()
// 如果索引对应没有值,代表没有需要搜素的字符串
if (!node.next[index]) {
return 0
}
node = node.next[index]
}
return node.end
}
// 删除字符串
delete(str) {
if (!this.search(str)) return
let node = this.root
for (let i = 0; i < str.length; i++) {
let index = str[i].charCodeAt() - 'a'.charCodeAt()
// 如果索引对应的节点的 Path 为 0,代表经过该节点的字符串
// 已经一个,直接删除即可
if (--node.next[index].path == 0) {
node.next[index] = null
return
}
node = node.next[index]
}
node.end -= 1
}
}
+ +

并查集

+

并查集是一种特殊的树结构,用于处理一些不交集的合并及查询问题。该结构中每个节点都有一个父节点,如果只有当前一个节点,那么该节点的父节点指向自己。
这个结构中有两个重要的操作,分别是:

+
+
    +
  • Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
  • +
  • Union:将两个子集合并成同一个集合。
  • +
+

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// js代码

class DisjointSet {
// 初始化样本
constructor(count) {
// 初始化时,每个节点的父节点都是自己
this.parent = new Array(count)
// 用于记录树的深度,优化搜索复杂度
this.rank = new Array(count)
for (let i = 0; i < count; i++) {
this.parent[i] = i
this.rank[i] = 1
}
}
find(p) {
// 寻找当前节点的父节点是否为自己,不是的话表示还没找到
// 开始进行路径压缩优化
// 假设当前节点父节点为 A
// 将当前节点挂载到 A 节点的父节点上,达到压缩深度的目的
while (p != this.parent[p]) {
this.parent[p] = this.parent[this.parent[p]]
p = this.parent[p]
}
return p
}
isConnected(p, q) {
return this.find(p) === this.find(q)
}
// 合并
union(p, q) {
// 找到两个数字的父节点
let i = this.find(p)
let j = this.find(q)
if (i === j) return
// 判断两棵树的深度,深度小的加到深度大的树下面
// 如果两棵树深度相等,那就无所谓怎么加
if (this.rank[i] < this.rank[j]) {
this.parent[i] = j
} else if (this.rank[i] > this.rank[j]) {
this.parent[j] = i
} else {
this.parent[i] = j
this.rank[j] += 1
}
}
}
+ +

堆通常是一个可以被看做一棵树的数组对象。
堆的实现通过构造二叉堆,实为二叉树的一种。这种数据结构具有以下性质。

+
    +
  • 任意节点小于(或大于)它的所有子节点
  • +
  • 堆总是一棵完全树。即除了最底层,其他层的节点都被元素填满,且最底层从左到右填入。
    将根节点最大的堆叫做最大堆大根堆,根节点最小的堆叫做最小堆小根堆
    优先队列也完全可以用堆来实现,操作是一模一样的。
  • +
+

实现大根堆

堆的每个节点的左边子节点索引是 i * 2 + 1,右边是 i * 2 + 2,父节点是 (i - 1) /2。
堆有两个核心的操作,分别是 shiftUp 和 shiftDown 。前者用于添加元素,后者用于删除根节点。
shiftUp 的核心思路是一路将节点与父节点对比大小,如果比父节点大,就和父节点交换位置。
shiftDown 的核心思路是先将根节点和末尾交换位置,然后移除末尾元素。接下来循环判断父节点和两个子节点的大小,如果子节点大,就把最大的子节点和父节点交换。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// js代码

class MaxHeap {
constructor() {
this.heap = []
}
size() {
return this.heap.length
}
empty() {
return this.size() == 0
}
add(item) {
this.heap.push(item)
this._shiftUp(this.size() - 1)
}
removeMax() {
this._shiftDown(0)
}
getParentIndex(k) {
return parseInt((k - 1) / 2)
}
getLeftIndex(k) {
return k * 2 + 1
}
_shiftUp(k) {
// 如果当前节点比父节点大,就交换
while (this.heap[k] > this.heap[this.getParentIndex(k)]) {
this._swap(k, this.getParentIndex(k))
// 将索引变成父节点
k = this.getParentIndex(k)
}
}
_shiftDown(k) {
// 交换首位并删除末尾
this._swap(k, this.size() - 1)
this.heap.splice(this.size() - 1, 1)
// 判断节点是否有左孩子,因为二叉堆的特性,有右必有左
while (this.getLeftIndex(k) < this.size()) {
let j = this.getLeftIndex(k)
// 判断是否有右孩子,并且右孩子是否大于左孩子
if (j + 1 < this.size() && this.heap[j + 1] > this.heap[j]) j++
// 判断父节点是否已经比子节点都大
if (this.heap[k] >= this.heap[j]) break
this._swap(k, j)
k = j
}
}
_swap(left, right) {
let rightValue = this.heap[right]
this.heap[right] = this.heap[left]
this.heap[left] = rightValue
}
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:计算机通识 ---- 数据结构

+

文章作者:

+

发布时间:2020年01月09日 - 07:35

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/09/FE-guide-data-structure/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/09/career/index.html b/ja/2020/01/09/career/index.html new file mode 100644 index 0000000000..8266ecff4a --- /dev/null +++ b/ja/2020/01/09/career/index.html @@ -0,0 +1,599 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 如何正确使用时间[转载] | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 如何正确使用时间[转载] +

+ + +
+ + + + +
+ + +

前言

最近各种技术博客,各种技术文章,各种牛人前辈的学习心得都在看,最近看了一个不错的文章叫做《如何正确使用时间》
感觉还是很不错的,特别适合年轻的程序员看一下。下面将全文贴在下方,如果需要也可以去原文查看。

+

原文

+

你是否时常会焦虑时间过的很快,没时间学习,本文将会分享一些个人的见解。

+

花时间补基础,读文档

在工作中我们时常会花很多时间去 debug,但是你是否发现很多问题最终只是你基础不扎实或者文档没有仔细看。

+

基础是你技术的基石,一定要花时间打好基础,而不是追各种新的技术。一旦你的基础扎实,学习各种新的技术也肯定不在话下,因为新的技术,究其根本都是相通的。

+

文档同样也是一门技术的基础。一个优秀的库,开发人员肯定已经把如何使用这个库都写在文档中了,仔细阅读文档一定会是少写 bug 的最省事路子。

+

学会搜索

如果你还在使用百度搜索编程问题,请尽快抛弃这个垃圾搜索引擎。同样一个关键字,使用百度和谷歌,谷歌基本完胜的。即使你使用中文在谷歌中搜索,得到的结果也往往是谷歌占优,所以如果你想迅速的通过搜索引擎来解决问题,那一定是谷歌。

+

学点英语

说到英语,一定是大家所最不想听的。其实我一直认为程序员学习英语是简单的,因为我们工作中是一直接触着英语,并且看懂技术文章,文档所需要的单词量是极少的。我时常在群里看到大家发出一个问题的截图问什么原因,其实在截图中英语已经很明白的说明了问题的所在,如果你的英语过关,完全不需要浪费时间来提问和搜索。所以我认为学点英语也是节省时间中很重要的一点。

+

那么如何去学习呢,chrome 装个翻译插件,直接拿英文文档或文章读,不会的就直接划词翻译,然后记录下这个单词并背诵。每天花半小时看点英文文档和文章,坚持两个月,你的英语水平不说别的,看文档和文章绝对不会有难题了。这一定是一个很划算的个人时间投资,花点时间学习英语,能为你将来的技术之路铺平很多坎。

+

画个图,想一想再做

你是否遇到过这种问题,需求一下来,看一眼,然后马上就按照设计稿开始做了,可能中间出个问题导致你需要返工。

+

如果你存在这样的问题,我很推荐在看到设计稿和需求的时候花点时间想一想,画一画。考虑一下设计稿中是否可以找到可以拆分出来的复用组件,是否存在之前写过的组件。该如何组织这个界面,数据的流转是怎么样的。然后画一下这个页面的需求,最后再动手做。

+

利用好下班时间学习

说到下班时间,那可能就有人说了公司很迟下班,这其实是国内很普遍的情况。但是我认为正常的加班是可以的,但是强制的加班就是在损耗你的身体和前途。

+

可以这么说,大部分的 996 公司,加班的这些时间并不会增加你的技术,无非就是在写一些重复的业务逻辑。也许你可以拿到更多的钱,但是代价是身体还有前途。程序员是靠技术吃饭的,如果你长久呆在一个长时间加班的公司,不能增长你的技术还要吞噬你的下班学习时间,那么你一定会废掉的。如果你遇到了这种情况,只能推荐尽快跳槽到非 996 的公司。

+

那么如果你有足够的下班时间,一定要花上 1, 2 小时去学习,上班大家基本都一样,技术的精进就是看下班以后的那几个小时了。如果你能利用好下班时间来学习,坚持下去,时间一定会给你很好的答复。

+

列好 ToDo

我喜欢规划好一段时间内要做的事情,并且要把事情拆分为小点。给 ToDo 列好优先级,紧急的优先级最高。相同优先级的我喜欢先做简单的,因为这样一旦完成就能划掉一个,提高成就感。

+

反思和整理

每周末都会花上点时间整理下本周记录的笔记和看到的不错文章。然后考虑下本周完成的工作和下周准备要完成的工作。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:如何正确使用时间[转载]

+

文章作者:

+

发布时间:2020年01月09日 - 16:33

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/09/career/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/09/do-it-yourselfery-jsonp/index.html b/ja/2020/01/09/do-it-yourselfery-jsonp/index.html new file mode 100644 index 0000000000..df783e2034 --- /dev/null +++ b/ja/2020/01/09/do-it-yourselfery-jsonp/index.html @@ -0,0 +1,590 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- jsonp | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- jsonp +

+ + +
+ + + + +
+ + +

前言

面试中时常会出现需要手动实现某个功能的要求,所以我准备整理一个自己动手实现系列文章,第一个呢就是自己动手实现 jsonp

+

实现 jsonp 思路

    +
  1. 将传入的data数据转化为url字符串形式
  2. +
  3. 处理url中的回调函数
  4. +
  5. 创建一个script标签并插入到页面中
  6. +
  7. 挂载回调函数
  8. +
+

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// js代码

(function (window,document) {
"use strict";
var jsonp = function (url,data,callback) {
// 1.将传入的data数据转化为url字符串形式
// {id:1,name:'jack'} => id=1&name=jack
var dataString = url.indexof('?') == -1? '?': '&';
for(var key in data){
dataString += key + '=' + data[key] + '&';
};

// 2 处理url中的回调函数
// cbFuncName回调函数的名字 :my_json_cb_名字的前缀 + 随机数(把小数点去掉)
var cbFuncName = 'my_json_cb_' + Math.random().toString().replace('.','');
dataString += 'callback=' + cbFuncName;

// 3.创建一个script标签并插入到页面中
var scriptEle = document.createElement('script');
scriptEle.src = url + dataString;

// 4.挂载回调函数
window[cbFuncName] = function (data) {
callback(data); // 处理完回调函数的数据之后,删除jsonp的script标签
document.body.removeChild(scriptEle);
}
document.body.appendChild(scriptEle);
}
window.$jsonp = jsonp;
})(window,document)
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- jsonp

+

文章作者:

+

发布时间:2020年01月09日 - 08:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/09/do-it-yourselfery-jsonp/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/09/solve-get-params-so-long-problem/index.html b/ja/2020/01/09/solve-get-params-so-long-problem/index.html new file mode 100644 index 0000000000..a418a74a34 --- /dev/null +++ b/ja/2020/01/09/solve-get-params-so-long-problem/index.html @@ -0,0 +1,620 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 解决get请求过长的问题小记 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 解决get请求过长的问题小记 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

就在前天,公司里一位新入职的前端小伙伴找到我说遇到了一个问题,自己写的vue代码本地运行是好用的,但是打包后提给后台就访问不了接口了,刚开始我以为是代理配置的有问题,但是经过检查也没什么问题,因为是个get方法,所以我就直接在浏览器地址里敲了api地址,也能正常获取到数据,那么这么看就不是代理或者接口的问题,后来我又看了下调试工具,network中根本就没发出请求,我怀疑是直接前台就拦截了,最后我发现他这个api参数超级长,长的相当吓人,可以看一下下图。
lROUMt.png
我记得url是有长度限制的,于是我就把url变短再次请求,虽然因为参数不全,接口报了错,但是发出去了,由此我定位应该是url超长,因为vue中配置了什么导致的访问被拦截了。于是我就开始了搜索引擎查找解决方案之旅,接下来就把我了解到的信息一一整理。

+ + +

URL 限制

首先我在网上找到了一份资料介绍了URL长度的相关资料,从下面可以看出,从HTTP协议层面以及Get请求层面都没有什么限制,这个限制来自于浏览器或者服务器的限制

+
+

Microsoft Internet Explorer (Browser)
IE浏览器对URL的最大限制为2083个字符,如果超过这个数字,提交按钮没有任何反应。
Firefox (Browser)
对于Firefox浏览器URL的长度限制为65,536个字符。
Safari (Browser)
URL最大长度限制为 80,000个字符。
Opera (Browser)
URL最大长度限制为190,000个字符。
Google (Browser)
URL最大长度限制为8182个字符。
Apache (Server)
能接受最大url长度为8,192个字符。
Microsoft Internet Information Server(IIS)
能接受最大url的长度为16,384个字符。

+
+

而且,中文会进行编码,一个汉字编码后会生成9个字符,这样算来,IE下最多也就能输入231个中文,再多就完蛋了,那么通过get请求传递参数就会显得很麻烦。

+

通常情况下,这种超长参数的请求我们都会用post,有些地方也会说post请求没有长度限制,但是前面说了,实际上HTTP协议层面并没有任何的限制,限制只出现在浏览器或者服务器限制,get和post请求在底层上其实是一样的。

+

最后项目修改了请求类型,把 get 请求改成了 post 请求,在网上实际还找到了另外两个方案,如果对同一组参数频繁访问的化,也可以用 post+get 请求的方式去处理,或者用 sessionStorage 下面简单介绍一下。

+
    +
  1. 将预览内容 post 到服务端,根据一个唯一标识生成缓存(有效时间5分钟),将唯一标识返回到前端,前端通过get方式传递唯一标识请求预览逻辑,拿到缓存的内容后渲染到页面。需要说明的是这里的缓存必须是分布式的。
  2. +
  3. 通过H5的会话缓存 sessionStorage 将预览内容存储在浏览器,打开预览页后从 sessionStorage 中拿到内容就可以渲染出页面了。
  4. +
+

上述两种方案都不太符合我们的项目所以最终还是选择了最简单的方式

+

GET VS POST

    +
  1. 多数浏览器对于POST采用两阶段发送数据的,先发送请求头,再发送请求体,即使参数再少再短,也会被分成两个步骤来发送(相对于GET),也就是第一步发送header数据,第二步再发送body部分。HTTP是应用层的协议,而在传输层有些情况TCP会出现两次连结的过程,HTTP协议本身不保存状态信息,一次请求一次响应。对于TCP而言,通信次数越多反而靠性越低,能在一次连结中传输完需要的消息是最可靠的,尽量使用GET请求来减少网络耗时。如果通信时间增加,这段时间客户端与服务器端一直保持连接状态,在服务器侧负载可能会增加,可靠性会下降。

    +
  2. +
  3. GET请求能够被cache,GET请求能够被保存在浏览器的浏览历史里面(密码等重要数据GET提交,别人查看历史记录,就可以直接看到这些私密数据)POST不进行缓存。

    +
  4. +
  5. GET参数是带在URL后面,传统IE中URL的最大可用长度为2048字符,其他浏览器对URL长度限制实现上有所不同。POST请求无长度限制(目前理论上是这样的)。

    +
  6. +
  7. GET提交的数据大小,不同浏览器的限制不同,一般在2k-8K之间,POST提交数据比较大,大小靠服务器的设定值限制,而且某些数据只能用 POST 方法「携带」,比如 file。

    +
  8. +
  9. 全部用POST不是十分合理,最好先把请求按功能和场景分下类,对数据请求频繁,数据不敏感且数据量在普通浏览器最小限定的2k范围内,这样的情况使用GET。其他地方使用POST。

    +
  10. +
  11. GET 的本质是「得」,而 POST 的本质是「给」。而且,GET 是「幂等」的,在这一点上,GET 被认为是「安全的」。但实际上 server 端也可以用作资源更新,但是这种用法违反了约定,容易造成 CSRF(跨站请求伪造)。

    +
  12. +
+

写在最后

以上是这次遇到问题后学到的一点知识,可能并不全面,后续如果遇到了类似的问题会继续丰富这篇文章。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:解决get请求过长的问题小记

+

文章作者:

+

发布时间:2020年01月09日 - 11:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/09/solve-get-params-so-long-problem/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/13/FE-guide-inherit2/index.html b/ja/2020/01/13/FE-guide-inherit2/index.html new file mode 100644 index 0000000000..f3c0915db0 --- /dev/null +++ b/ja/2020/01/13/FE-guide-inherit2/index.html @@ -0,0 +1,667 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 继承类型 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 继承类型 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

继承的操作需要有一个父类,这里使用构造函数外加原型来创建一个:
有下面两个类,下面实现 Child 继承 Father:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

function Father() {
this.type = 'prople';
}

Father.prototype.eat = function() {
console.log('吃东西啦');
};

function Child(name) {
this.name = name;
this.color = 'black';
}
+ +

原型继承(认贼作父)

+

关键点:子类原型等于父类的实例 Child.prototype = new Person()(将父类指向子类的原型)。

+
+
1
2
// js代码
Child.prototype = new Father();
+ +

特点:
实例可继承的属性有:

+
    +
  • 实例的构造函数的属性
  • +
  • 父类构造函数的属性
  • +
  • 父类原型上的属性
    新实例不会继承父类实例的属性
  • +
+

缺点:

+
    +
  • 新实例无法向父类构造函数传参
  • +
  • 继承单一
  • +
  • 所有新实例都会共享父类实例的属性。— 原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改
  • +
+

构造继承(借腹生子)

+

在子类构造函数中调用父类构造函数

+
+
1
2
3
4
// js代码
function Child(name) {
Father.call(this);
}
+ +

关键点:用 call 或 apply 将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制))Person.call(this, ‘reng’)
特点:

+
    +
  • 只继承了父类构造函数的属性,没有继承父类原型的属性
  • +
  • 解决了原型链继承的注意事项(缺点)1,2,3
  • +
  • 可以继承多个构造函数的属性(call 可以多个)
  • +
  • 在子实例中可以向父实例传参
    缺点:
  • +
  • 只能继承父类构造函数的属性
  • +
  • 无法实现构造函数的复用。(每次用每次都要重新调用)
  • +
  • 每个新实例都有构造函数的副本,臃肿
    (不能继承父类原型,函数在构造函数中,每个子类实例不能共享函数,浪费内存。)
  • +
+

组合继承(原型继承+构造继承)

+

使用构造继承继承父类参数,使用原型继承继承父类函数

+
+
1
2
3
4
5
6
7
// js代码
function Child(name) {
// 构造继承
Father.call(this);
}

Child.prototype = Father.prototype; // Child.prototype = new Person(); // 原型继承
+ +

关键点:结合了两种模式的优点–向父类传参(call)和复用(prototype)
特点:

+
    +
  • 可以继承父类原型上的属性,可以传参,可复用
  • +
  • 每个新实例引入的构造函数属性是私有的
  • +
+

缺点:

+
    +
  • 父类原型和子类原型是同一个对象,无法区分子类真正是由谁构造。
  • +
  • 调用了两次父类的构造函数(耗内存)
  • +
  • 子类的构造函数会代替原型上的那个父类构造函数(call 相当于拿到了父类构造函数的副本)
  • +
+

原型式继承(复制降级)

1
2
3
4
5
6
7
8
9
10
11
// 先封装一个函数容器,用来承载继承的原型和输出对象
function create(obj) {
// 寄生
function F() {}
F.prototype = obj;
return new F();
}
var father = new Father();
var child = create(father);
console.log(child instanceof Father); // true
console.log(child.job); // frontend
+ +

关键点:用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了可以随意增添属性的实例或对象。Object.create()就是这个原理。

+

特点:

+
    +
  • 类似于复制一个对象,用函数来包装
  • +
+

注意事项:

+
    +
  • 所有的实例都会继承原型上的属性
  • +
  • 无法实现复用。(新实例属性都是后面添加的)
    Object.create()方法规范了原型式继承。这个方法接收两个参数,一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。
  • +
+
1
2
3
4
5
6
7
8
9
10
11
// 传一个参数的时候
var child = Object.create(new Father());
console.log(child.job); // frontend
console.log(child instanceof Father); // true
// 传两个参数的时候
var child = Object.create(new Father(), {
name: {
value: 'come on'
}
});
child.sayHello(); // Hello come on
+ +

寄生组合继承

它跟组合继承一样,都比较常用。
寄生:在函数内返回对象然后调用
组合

+
    +
  • 函数的原型等于另一个实例
  • +
  • 在函数中用 apply 或 call 引入另一个构造函数,可传参
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 寄生
function create(obj) {
function F() {}
F.prototype = obj;
return new F();
}
// object是F实例的另一种表示方法
var obj = create(Father.prototype);
// obj实例(F实例)的原型继承了父类函数的原型
// 上述更像是原型链继承,只不过只继承了原型属性

// 组合
function Child() {
// 构造
this.age = 100;
Father.call(this); // 这个继承了父类构造函数的属性
} // 解决了组合式两次调用构造函数属性的特点

// 重点
Child.prototype = obj; // 原型

console.log(Child.prototype.constructor); // Father
obj.constructor = Child; // 一定要修复实例
console.log(Child.prototype.constructor); // Child
var child = new Child();
// Child实例就继承了构造函数属性,父类实例,object的函数属性
console.log(child.job); // frontend
console.log(child instanceof Father); // true
+ +

重点:修复了组合继承的问题

+

在上面的问题中,你可能发现了这么一个注释obj.constructor = Sub; // 一定要修复实例。为什么要修正子类的构造函数的指向呢?

+

因为在不修正这个指向的时候,在获取构造函数返回的时候,在调用同名属性或方法取值上可能造成混乱。比如下面:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Car() {}
Car.prototype.orderOneLikeThis = function() {
// Clone producing function
return new this.constructor();
};
Car.prototype.advertise = function() {
console.log('I am a generic car.');
};

function BMW() {}
BMW.prototype = Object.create(Car.prototype);
BMW.prototype.constructor = BMW; // Resetting the constructor property
BMW.prototype.advertise = function() {
console.log('I am BMW with lots of uber features.');
};

var x5 = new BMW();

var myNewToy = x5.orderOneLikeThis();

myNewToy.advertise(); // => "I am BMW ..." if `BMW.prototype.constructor = BMW;` is not
// commented; "I am a generic car." otherwise.
+ +

object.create 在组合继承的基础上,子类继承一个由父类原型生成的空对象。

+
1
2
3
4
5
6
7
8
9
10
// js代码
function Child(name) {
Father.call(this);
}

Child.prototype = Object.create(Father.prototype, {
constructor: {
value: Child
}
});
+ +

inherits 函数 — Nodejs util.inherits 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// js代码

function inherits = function(ctor, superCtor) {
ctor.super_ = superCtor; // super_属性是子类继承父类时构造函数要写入的一个属性值.
ctor.prototype = Object.create(superCtor.prototype, {
constructor: {
value: ctor,
enumerable: false,
writable: true,
configurable: true
}
});
};
// 在上面的代码中 ctor 想要继承 superCtor ,我们姑且把 ctor 称作子类, superCtor 称作父类.

// 使用
function Child() {
Father.call(this);
//...
}
inherits(Child, Father);

Child.prototype.fun = ...
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 继承类型

+

文章作者:

+

发布时间:2020年01月13日 - 03:18

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/13/FE-guide-inherit2/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/13/Javascript-Design-Pattern/index.html b/ja/2020/01/13/Javascript-Design-Pattern/index.html new file mode 100644 index 0000000000..ba946bae47 --- /dev/null +++ b/ja/2020/01/13/Javascript-Design-Pattern/index.html @@ -0,0 +1,665 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + JavaScript 设计模式 Design Pattern | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ JavaScript 设计模式 Design Pattern +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在整理自己的知识体系,一方面为了夯实基础,另一方面也为了查缺补漏、重新理解,设计模式之前看过大概三四次,从最开始一点都不懂,到后面看了几遍以后慢慢理解,今天再次整理一下,希望这一遍下来之后能更加透彻的理解什么是设计模式。

+ + +

什么是设计模式

+

设计模式的定义:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案
设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。

+
+
+

  使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的;设计模式使代码编制真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。 —- 百度百科

+
+

不知道大家看了上面的定义以后是什么感受,说实话我第一次看到这句话并没有什么深刻的认识,什么面向对象的软件设计,什么针对特定问题,什么优雅的解决方案,这都说的是什么,后来我看了几遍之后,上面这句话用我的理解翻译如下:

+
+

软件开发过程中,解决某一类问题用到的一系列套路

+
+

这就是我对设计模式的认识。当然这也不仅仅是我自己的认识,在跟其他的一些开发人员交流时,很多人都是这么认为的。

+

这些解决问题的方案实在是太好用了,所以大神就把它们抽象出来,然后起了个名字-就叫做设计模式了。

+

这么说大家可能还是不太明白,举个开发过程中可能遇到的实际例子吧。

+
+

当系统中某个接口的结构已经无法满足我们现在的业务需求,但又不能改动这个接口,因为可能原来的系统很多功能都依赖于这个接口,改动接口会牵扯到太多文件。那么这种场景下我们该如何解决这个问题呢?通常我们需要新增一个接口,兼容原来的接口和新的业务需求参数。
因此应对这种场景,我们可以很快地想到可以用适配器模式来解决这个问题。

+
+

这就是设计模式的应用,实际上也许你还不知道设计模式这个词,但是你已经在工作中频繁的用到了设计模式,下面我们就来看看到底有哪些设计模式。

+

哦,对了,设计模式并不依赖于语言,它本身更像是一种软件的设计思想,因为我是一个前端,所以接下来具体实现的时候我会使用js来实现设计模式的用法。

+

学习设计模式

目前被普遍接受的经典的设计模式共有 23 种,而这23种设计模式又分为了 3大类 ,看过一张图这里拿过来镇贴。
lHgD4H.jpg
他们分别是

+
    +
  • 创建型模式
  • +
  • 结构型模式
  • +
  • 行为型模式
  • +
+

接下来,我将会将这23种,3大类设计模式一个个的拆解开来,跟大家一起学习一下,设计模式有哪些内容。

+

创建型模式 6个

这类模式用于对象的生成生命周期的管理
创建型模式可以决定生成哪些对象,提高了程序的灵活性。具体属于此类的模式清单如下,共有 5 个:

+
    +
  • 单例模式(Singleton)
  • +
  • 工厂方法模式(Factory Method)
  • +
  • 抽象工厂模式(Abstract Factory)
  • +
  • 建造者模式(Builder)
  • +
  • 原型模式(Prototype)
  • +
  • 迭代器模式(Iterator)
  • +
+

单例模式(Singleton)

描述:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

+

工厂方法模式(Factory Method)

描述:定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。

+

抽象工厂模式(Abstract Factory)

描述:为创建一组相关或相互依赖的对象提供一个接口,而且无需指定它们的具体类

+

建造者模式(Builder)

描述:将一个复杂对象的构建与他的表示分离,使得同样的构建过程可以创建不同的表示。

+

原型模式(Prototype)

描述:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

+

迭代器模式(Iterator)

描述:提供一种方法访问一个容器对象中的各个元素,而又不需暴露该对象的内部细节。

+

结构型模式 7个

    +
  • 适配器模式(Adapter)
  • +
  • 组合模式(Compositor)
  • +
  • 代理模式(Proxy)
  • +
  • 桥梁模式(Bridge)
  • +
  • 装饰模式(Decorator)
  • +
  • 门面模式(Facade)
  • +
  • 享元模式(Flyweight)
  • +
+

适配器模式(Adapter)

描述:将一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。

+

组合模式(Compositor)

描述:将对象组合成树形结构

+

代理模式(Proxy)

描述:

+

桥梁模式(Bridge)

描述:

+

装饰模式(Decorator)

描述:

+

门面模式(Facade)

描述:

+

享元模式(Flyweight)

描述:

+

行为型模式 10个

    +
  • 命名模式(Command)
  • +
  • 解释器模式(Interpreter)
  • +
  • 责任链模式(Chian of Responsibility)
  • +
  • 观察者模式(Observer)
  • +
  • 中介者模式(Mediator)
  • +
  • 备忘录模式(Memento)
  • +
  • 状态模式(State)
  • +
  • 策略模式(Strategy)
  • +
  • 模板方法模式(Template Method)
  • +
  • 访问者模式(Visitor)
  • +
+

命名模式(Command)

描述:

+

解释器模式(Interpreter)

描述:

+

责任链模式(Chian of Responsibility)

描述:

+

观察者模式(Observer)

描述:

+

中介者模式(Mediator)

描述:

+

备忘录模式(Memento)

描述:

+

状态模式(State)

描述:

+

策略模式(Strategy)

描述:

+

模板方法模式(Template Method)

描述:

+

访问者模式(Visitor)

描述:

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:JavaScript 设计模式 Design Pattern

+

文章作者:

+

发布时间:2020年01月13日 - 12:22

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/13/Javascript-Design-Pattern/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/13/do-it-yourselfery-promise/index.html b/ja/2020/01/13/do-it-yourselfery-promise/index.html new file mode 100644 index 0000000000..85b8459f25 --- /dev/null +++ b/ja/2020/01/13/do-it-yourselfery-promise/index.html @@ -0,0 +1,623 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- promise | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- promise +

+ + +
+ + + + +
+ + +

前言

面试中时常会出现需要手动实现某个功能的要求,所以我准备整理一个自己动手实现系列文章,这一次呢就是自己动手实现 promise

+

实现 promise 思路

基础步骤

+
    +
  1. 设定三个状态 PENDINGFULFILLEDREJECTED ,只能由 PENDING 改变为 FULFILLEDREJECTED ,并且只能改变一次
  2. +
  3. MyPromise 接收一个函数 executorexecutor 有两个参数 resolve 方法和 reject 方法
  4. +
  5. resolvePENDING 改变为 FULFILLED
  6. +
  7. rejectPENDING 改变为 FULFILLED
  8. +
  9. promise 变为 FULFILLED 状态后具有一个唯一的 value
  10. +
  11. promise 变为 REJECTED 状态后具有一个唯一的 reason
  12. +
+

** then 方法**

+
    +
  1. then 方法接受两个参数 onFulfilledonRejected ,它们分别在状态由 PENDING 改变为 FULFILLEDREJECTED 后调用
  2. +
  3. 一个 promise 可绑定多个 then 方法
  4. +
  5. then 方法可以同步调用也可以异步调用
  6. +
  7. 同步调用:状态已经改变,直接调用 onFulfilled 方法
  8. +
  9. 异步调用:状态还是 PENDING ,将 onFulfilledonRejected 分别加入两个函数数组 onFulfilledCallbacksonRejectedCallbacks ,当异步调用 resolvereject 时,将两个数组中绑定的事件循环执行。
  10. +
+

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// js代码

// 定义状态常量
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

/**
* 定义MyPromise模拟Promise
* @param {func} executor 接收函数
*/
function MyPromise(executor) {
this.state = PENDING; // 默认状态为 pending
this.value = null;
this.reason = null;

// 定义成功失败的函数数组
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];

// 定义成功回调
const resolve = (value) => {
if (this.state === PENDING) {
this.state = FULFILLED;
this.value = value;

this.onFulfilledCallbacks.forEach(func => {
func();
});
}
}

// 定义失败回调
const reject = (reason) => {
if (this.state === PENDING) {
this.state = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach(func => {
func();
});
}
}

try {
executor(resolve, reject);
} catch (reason) {
reject(reason);
}
}

MyPromise.prototype.then = function (onFulfilled, onRejected) {
switch (this.state) {
case FULFILLED:
onFulfilled(this.value);
break;
case REJECTED:
onFulfilled(this.value);
break;
case PENDING:
this.onFulfilledCallbacks.push(() => {
onFulfilled(this.value);
})
this.onRejectedCallbacks.push(() => {
onRejected(this.reason);
})
break;
}
+

then方法异步调用

如下面的代码:输入顺序是:1、2、ConardLi

+
1
2
3
4
5
6
7
8
9
10
11
12
13
// js代码

console.log(1);

let promise = new Promise((resolve, reject) => {
resolve('ConardLi');
});

promise.then((value) => {
console.log(value);
});

console.log(2);
+

虽然 resolve 是同步执行的,我们必须保证 then 是异步调用的,我们用 setTimeout 来模拟异步调用(并不能实现微任务和宏任务的执行机制,只是保证异步调用)

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

// js代码
MyPromise.prototype.then = function (onFulfilled, onRejected) {
if (typeof onFulfilled != 'function') {
onFulfilled = function (value) {
return value;
}
}
if (typeof onRejected != 'function') {
onRejected = function (reason) {
throw reason;
}
}
switch (this.state) {
case FULFILLED:
setTimeout(() => {
onFulfilled(this.value);
}, 0);
break;
case REJECTED:
setTimeout(() => {
onRejected(this.reason);
}, 0);
break;
case PENDING:
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
onFulfilled(this.value);
}, 0);
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
onRejected(this.reason);
}, 0);
})
break;
}
}
+

then方法链式调用

保证链式调用,即 then 方法中要返回一个新的 promise ,并将 then 方法的返回值进行 resolve

+

注意:这种实现并不能保证 then 方法中返回一个新的 promise ,只能保证链式调用

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// js代码
MyPromise.prototype.then = function (onFulfilled, onRejected) {
if (typeof onFulfilled != 'function') {
onFulfilled = function (value) {
return value;
}
}
if (typeof onRejected != 'function') {
onRejected = function (reason) {
throw reason;
}
}

// 创建一个新的MyPromise对象
const promise2 = new MyPromise((resolve, reject) => {
switch (this.state) {
case FULFILLED:
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
break;
case REJECTED:
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
break;
case PENDING:
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
})
break;
}
})
return promise2;
}
+

catch方法

若上面没有定义 reject 方法,所有的异常会走向 catch 方法:

+
1
2
3
4
5
// js代码

MyPromise.prototype.catch = function(onRejected) {
return this.then(null, onRejected);
};
+

finally方法

不管是 resolve 还是 reject 都会调用 finally

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

MyPromise.prototype.finally = function(fn) {
return this.then(value => {
fn();
return value;
}, reason => {
fn();
throw reason;
});
};
+

Promise.resolve

Promise.resolve 用来生成一个直接处于 FULFILLED 状态的 Promise

+
1
2
3
4
5
6
// js代码
MyPromise.reject = function(value) {
return new MyPromise((resolve, reject) => {
resolve(value);
});
};
+

Promise.reject

Promise.reject 用来生成一个直接处于 REJECTED 状态的 Promise

+
1
2
3
4
5
6
// js代码
MyPromise.reject = function(reason) {
return new MyPromise((resolve, reject) => {
reject(reason);
});
};
+

all方法

接受一个 promise 数组,当所有 promise 状态 resolve 后,执行 resolve

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// js代码
MyPromise.all = function (promises) {
return new Promise((resolve, reject) => {
if (promises.length === 0) {
resolve([]);
} else {
let result = [];
let index = 0;
for (let i = 0; i < promises.length; i++) {
promises[i].then(data => {
result[i] = data;
if (++index === promises.length) {
resolve(result);
}
}, err => {
reject(err);
return;
});
}
}
});
}
+

race方法

接受一个 promise 数组,当有一个 promise 状态 resolve 后,执行 resolve

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码
MyPromise.race = function (promises) {
return new Promise((resolve, reject) => {
if (promises.length === 0) {
resolve();
} else {
let index = 0;
for (let i = 0; i < promises.length; i++) {
promises[i].then(data => {
resolve(data);
}, err => {
reject(err);
return;
});
}
}
});
}
+ +

最后

如此一个自定义的 promise 就实现了,怎么样学回来吗?

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- promise

+

文章作者:

+

发布时间:2020年01月13日 - 00:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/13/do-it-yourselfery-promise/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/13/hexo-search/index.html b/ja/2020/01/13/hexo-search/index.html new file mode 100644 index 0000000000..df2acffe78 --- /dev/null +++ b/ja/2020/01/13/hexo-search/index.html @@ -0,0 +1,596 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 给我的Hexo博客添加文章内容搜索功能 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 给我的Hexo博客添加文章内容搜索功能 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

今年我下定决心一定要好好地写博客,完善博客的内容,所以最近把原来记在各种笔记中的文章内容都一一转移到了博客,内容一多想找一篇文章时就变得很麻烦,我得去归档中或者分类中一篇篇的找,所以我觉得是时候该给我的博客添加搜索的功能了,我看了博客的config文件,其中并没有搜索相关的配置,我又看了主题的配置文件,这下让我找到了,有个local_search的选项,于是我就开始了博客搜索的研究。

+ +

主题里的搜索配置

这段代码是这样的,实际上我只需要把 enablefalse 变成 true 就好了

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# Local Search
# Dependencies: https://github.com/theme-next/hexo-generator-searchdb
local_search:
enable: true
# If auto, trigger search by changing input.
# If manual, trigger search by pressing enter key or search button.
trigger: auto
# Show top n results per article, show all results by setting to -1
top_n_per_article: 1
# Unescape html strings to the readable one.
unescape: false
# Preload the search data when the page loads.
preload: false
+

然后我又看了一下上面提供的依赖地址,这里还需要做两步,一个是安装搜索的依赖

+
1
npm install hexo-generator-searchdb
+

接着就是在博客系统的配置最下方加入下面这段话

+
1
2
3
4
5
search:
path: search.xml
field: post
content: true
format: html
+

到这里如果没什么问题,那么搜索功能就加上了,怎么样简单吧。如果你遇到什么问题,可以到上面的地址看一下,上面有详细的说明,我这里就不贴代码了。

+

最后

希望大家都能丰富自己的技术博客,拥有属于自己的一片技术天地。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:给我的Hexo博客添加文章内容搜索功能

+

文章作者:

+

发布时间:2020年01月13日 - 14:50

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/13/hexo-search/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/22/Es6-arrowFunc-arguments/index.html b/ja/2020/01/22/Es6-arrowFunc-arguments/index.html new file mode 100644 index 0000000000..3f78b1b079 --- /dev/null +++ b/ja/2020/01/22/Es6-arrowFunc-arguments/index.html @@ -0,0 +1,583 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + es6中箭头函数没了arguments怎么办? | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ es6中箭头函数没了arguments怎么办? +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

es6中,arguments被取消掉了,但是函数还是会有参数不确定的时候,那么我们该怎么办呢,接下来将介绍一下如何在es6中完成arguments一样的效果。

+ + +

剩余运算符

1
2
3
4
5
6
7
8
// js代码

let func = (...rest) => {
console.log(rest)
//[1,2,3]
}

func(1,2,3)
+ +

看上面的代码,有的朋友会问,这...的操作不应该是展开运算符么?是的,扩展运算符与剩余操作符都是以三点开头的操作符,二者长的很像,只是在用法上有些差别。它们已经被 ES6 数组支持,能解决很多之前 arguments 解决起来很麻烦的问题。

+

简单来说剩余运算是在参数上使用的。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:es6中箭头函数没了arguments怎么办?

+

文章作者:

+

发布时间:2020年01月22日 - 06:50

+

最后更新:2020年01月22日 - 07:34

+

原始链接:https://blog.lifesli.com/2020/01/22/Es6-arrowFunc-arguments/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/22/FE-guide-safe/index.html b/ja/2020/01/22/FE-guide-safe/index.html new file mode 100644 index 0000000000..daed03e738 --- /dev/null +++ b/ja/2020/01/22/FE-guide-safe/index.html @@ -0,0 +1,635 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 网络安全 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 网络安全 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

网络安全是前端不可忽略的一个部分,可惜的是之前我都忽略了,最近发现网络安全已经变得日益重要,所以特别整理一篇文章说说网络安全相关的内容。

+ +

XSS(Cross-site scripting)跨站脚本攻击

跨站脚本攻击是一种网站应用程序的安全漏洞攻击,是代码注入的一种。它允许恶意用户将代码注入到网页上,其他用户在观看网页时就会受到影响。这类攻击通常包含了HTML以及用户端脚本语言。

+

XSS攻击通常指的是通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。这些恶意网页程序通常是JavaScript,但实际上也可以包括Java,VBScript,ActiveX,Flash或者甚至是普通的HTML。攻击成功后,攻击者可能得到更高的权限(如执行一些操作)、私密网页内容、会话和cookie等各种内容。

+

XSS攻击分成两类:

+
    +
  • 来自内部的攻击
      +
    • 主要指的是利用程序自身的漏洞,构造跨站语句
    • +
    +
  • +
  • 来自外部的攻击
      +
    • 主要指自己构造XSS跨站漏洞网页或者寻找非目标机以外的有跨站漏洞的网页。如当我们要渗透一个站点,我们自己构造一个有跨站漏洞的网页,然后构造跨站语句,通过结合其他技术,如社会工程学等,欺骗目标服务器的管理员打开。
    • +
    +
  • +
+

XSS分为:存储型反射型

+
    +
  • 存储型XSS:存储型XSS,持久化,代码是存储在服务其中的,如在个人信息或发表文章等地方,加入代码,如果没有过滤或过滤不严,那么这些代码将储存到服务器中,用户访问该页面的时候触发代码执行。这种XSS比较危险,容易造成蠕虫,盗窃cookie(虽然还有种DOM型XSS,但是也还是包括在存储型XSS内)。
  • +
  • 反射型XSS:非持久化,需要欺骗用户自己去点击链接才能触发XSS代码(服务器中没有这样的页面和内容),一般容易出现在搜索页面。
  • +
+

攻击手段和目的

攻击者使被攻击者在浏览器中执行脚本后,如果需要收集来自被攻击者的数据(如cookie或其他敏感信息),可以自行架设一个网站,让被攻击者通过JavaScript等方式把收集好的数据作为参数提交,随后以数据库等形式记录在攻击者自己的服务器上。

+

常用的XSS攻击手段和目的有:

+
    +
  • 盗用cookie,获取敏感信息。
  • +
  • 利用植入Flash,通过crossdomain权限设置进一步获取更高权限;或者利用Java等得到类似的操作。
  • +
  • 利用iframe、frame、XMLHttpRequest或上述Flash等方式,以(被攻击)用户的身份执行一些管理动作,或执行一些一般的如发微博、加好友、发私信等操作。
  • +
  • 利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动。
  • +
  • 在访问量极大的一些页面上的XSS可以攻击一些小型网站,实现DDoS攻击的效果。
  • +
+

漏洞的防御和利用

过滤特殊字符

避免XSS的方法之一主要是将用户所提供的内容进行过滤,许多语言都有提供对HTML的过滤:

+
    +
  • PHP的htmlentities()或是htmlspecialchars()。
  • +
  • Python的cgi.escape()。
  • +
  • ASP的Server.HTMLEncode()。
  • +
  • ASP.NET的Server.HtmlEncode()或功能更强的Microsoft Anti-Cross Site Scripting Library
  • +
  • Java的xssprotect (Open Source Library)。
  • +
  • NodeJS的node-validator。
  • +
+

使用HTTP头指定类型

很多时候可以使用HTTP头指定内容的类型,使得输出的内容避免被作为HTML解析。如在PHP语言中使用以下代码:
<?php header('Content-Type: text/javascript; charset=utf-8'); ?>
即可强行指定输出内容为文本/JavaScript脚本(顺便指定了内容编码),而非可以引发攻击的HTML。

+

用户方面

包括Internet Explorer、Mozilla Firefox在内的大多数浏览器皆有关闭JavaScript的选项,但关闭功能并非是最好的方法,因为许多网站都需要使用JavaScript语言才能正常运作。通常来说,一个经常有安全更新推出的浏览器,在使用上会比很久都没有更新的浏览器更为安全。

+

CRSF(Cross-site request forgery)跨站请求伪造

跨站请求伪造是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

+
    +
  • XSS 利用的是用户对指定网站的信任
  • +
  • CSRF 利用的是网站对用户网页浏览器的信任
  • +
+

跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。

+

例子

例子
假如一家银行用以运行转账操作的URL地址如下:
http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName
那么,一个恶意攻击者可以在另一个网站上放置如下代码:
<img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">
如果有账户名为Alice的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失1000资金。
这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险
透过例子能够看出,攻击者并不能通过CSRF攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义运行操作。

+

防御措施

检查Referer(参照)字段

HTTP头中有一个Referer字段,这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer字段应和请求的地址位于同一域名下。以上文银行操作为例,Referer字段地址通常应该是转账按钮所在的网页地址,应该也位于www.examplebank.com之下。而如果是CSRF攻击传来的请求,Referer字段会是包含恶意网址的地址,不会位于www.examplebank.com之下,这时候服务器就能识别出恶意的访问。
这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的Referer字段。虽然http协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其Referer字段的可能。

+

添加校验token

由于CSRF的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在cookie中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再运行CSRF攻击。这种数据通常是窗体中的一个数据项。服务器将其生成并附加在窗体中,其内容是一个伪随机数。当客户端通过窗体提交请求时,这个伪随机数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪随机数,而通过CSRF传来的欺骗性攻击中,攻击者无从事先得知这个伪随机数的值,服务端就会因为校验token的值为空或者错误,拒绝这个可疑请求。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 网络安全

+

文章作者:

+

发布时间:2020年01月22日 - 14:50

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/01/22/FE-guide-safe/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/01/22/Wepack-Tips/index.html b/ja/2020/01/22/Wepack-Tips/index.html new file mode 100644 index 0000000000..b74a62f6d1 --- /dev/null +++ b/ja/2020/01/22/Wepack-Tips/index.html @@ -0,0 +1,622 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + webpack中loader和plugin之间的区别 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ webpack中loader和plugin之间的区别 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在学习Webpack相关的知识的时候对loader和plugin有点迷惑,两个特性都是用来做打包相关处理,那么他们有什么区别呢,为了弄清楚他们有什么区别,我开始了疯狂的查找资料,虽说每篇文章都说了一些自己的认识,但是并没有一个特别标准或者说容易理解的答案,我且先将它们记录下来,以便日后回顾之时可以有一些感触。那么接下来我将开始webpack的解迷之旅。

+ + +

背景知识

在研究loader和plugin之前区别之前,我们先来看看一个webpack配置的常见结构

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// js代码

const webpack = require("webpack");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
// 入口文件
entry: {
app: path.join(__dirname, "../src/js/index.js")
},
// 输出文件
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "dist"),
publicPath: "/"
},
// loader配置
module: {
rules: [
{
test: /\.scss/,
use: [
"style-loader",
"css-loader"
]
}
......
]
},
// plugins配置
plugins: [
// 重新创建html文件
new HtmlWebpackPlugin({
title: "首页",
filename: "index.html",
template: path.resolve(__dirname, "../src/index.html")
})
......
]
}
+

webpack的打包原理

+
    +
  • 识别入口文件
  • +
  • 通过逐层识别模块依赖(Commonjs、amd或者es6的import,webpack都会对其进行分析,来获取代码的依赖)
  • +
  • webpack做的就是分析代码,转换代码,编译代码,输出代码
  • +
  • 最终形成打包后的代码
  • +
+

什么是loader

我们可以看到loader实际上是在module的rules下,用对象的方式表示了需要处理的文件类型,和需要用哪些loader做处理

+
+

loader是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中。

+
+
    +
  • 处理一个文件可以使用多个loader,loader的执行顺序和配置中的顺序是相反的,即最后一个loader最先执行,第一个loader最后执行
  • +
  • 第一个执行的loader接收源文件内容作为参数,其它loader接收前一个执行的loader的返回值作为参数,最后执行的loader会返回此模块的JavaScript源码
  • +
+

什么是plugin

+

在webpack运行的生命周期中会广播出许多事件,plugin可以监听这些事件,在合适的时机通过webpack提供的API改变输出结果。

+
+

loader和plugin的区别

对于loader,它是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.scss转换为A.css,单纯的文件转换过程

+

plugin是一个扩展器,它丰富了webpack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务

+

下面我们来看一个例子:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// js代码

class MyPlugin{
constructor(options){
console.log("MyPlugin constructor:", options);
}
apply(compiler){
compiler.plugin("compilation", compilation => {
console.log("MyPlugin");
});
}
}
module.exports = MyPlugin;

// webpack.config.js配置:
module.exports = {
...
plugins: [
new MyPlugin({param: "my plugin"})
]
}
+ +

使用该plugin后,执行的顺序:

+
    +
  1. webpack启动后,在读取配置的过程中会执行new MyPlugin(options)初始化一个MyPlugin获取其实例
  2. +
  3. 在初始化compiler对象后,就会通过compiler.plugin(事件名称,回调函数)监听到webpack广播出来的事件
  4. +
  5. 并且可以通过compiler对象去操作webpack
  6. +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:webpack中loader和plugin之间的区别

+

文章作者:

+

发布时间:2020年01月22日 - 10:38

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/01/22/Wepack-Tips/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/02/11/SSR/index.html b/ja/2020/02/11/SSR/index.html new file mode 100644 index 0000000000..ab940c21f0 --- /dev/null +++ b/ja/2020/02/11/SSR/index.html @@ -0,0 +1,609 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SSR | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ SSR +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

什么是SSR

传统浏览器的vue纯浏览器渲染

浏览器请求服务器,服务器返回静态资源 ,客户端负责渲染js

+

ssr

浏览器请求服务器,node服务端渲染js后,返回对应的页面返回给浏览器

+

SSR需要那些东西

手写SSR

特性:

+
    +
  • 每一次访问必须新建一个vue实例
  • +
  • 只会触发组件的 beforeCreate和created钩子
  • +
+

核心库

+
    +
  • vue
  • +
  • vue-server-renderer

    vue + next

    +

    作者:李旭光
    引用请标明出处

    +
    +
  • +
+

前言

+
+

作者:李旭光
引用请标明出处

+
+

前言

+
+

作者:李旭光
引用请标明出处

+
+

前言

+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:SSR

+

文章作者:

+

发布时间:2020年02月11日 - 16:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/02/11/SSR/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/02/11/restful-architecture/index.html b/ja/2020/02/11/restful-architecture/index.html new file mode 100644 index 0000000000..adbf29c086 --- /dev/null +++ b/ja/2020/02/11/restful-architecture/index.html @@ -0,0 +1,602 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NodeJS 下 RESTful 架构的最佳实践(课堂笔记) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ NodeJS 下 RESTful 架构的最佳实践(课堂笔记) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

什么是 RESTful

+

REST 指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是 RESTful。

+
+

实际上就是 url 风格/ 路由风格 , 给出一种写路由的原则 — 面向资源
tip:静态路由利于seo优化 api/news/1,动态路由不利于seo优化 api/news?id=1

+

原来的风格
| 路由 | 功能 | 描述 |
| —- | —- |——|
| http://127.0.0.1/user/query/1 | 查询 | 根据ID查询用户信息 |
| http://127.0.0.1/user/save | 保存 | 注册用户 |
| http://127.0.0.1/user/update | 更新 | 修改用户 |
| http://127.0.0.1/user/delete/{id} | 删除 | 删除用户 |

+

RESTful — 面向资源:对于同一个资源都在同一个 URL 进行,通过判断 HTTP 请求的类型来决定做不同的事
/user 一个资源
| 路由 | 请求类型 |
| ———————– | ——– |
| http://127.0.0.1/user/1 | GET |
| http://127.0.0.1/user | POST |
| http://127.0.0.1/user | PUT |
| http://127.0.0.1/user | DELETE |

+

URL 描述资源 , HTTP 描述请求。(HTTP协议无语义无状态 —- 降低复杂度,业务无关 — 架构代码业务无关)

+

RESTful 采用的是顶层路由

+
+

顶层路由设计:不需要有物理文件映射路由

+
+
1
2
3
4
5
6
7
8
// express
// app.js
const express = require('express')
const app = express()
app.get('/case.avi',(req, res)=>{
res.send('hello world'); // 不需要对应物理文件
})
app.listen(3000)
+

原生接口

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// index.js
const http = require('http');
const mysql = require('mysql'); // mysql
const co = require('co-mysql') // 异步同步化
const md5 = require('md5-node') // md5加密

// 连接数据库
let db = mysql.createPool({ // 连接池自己管理 不用关闭
host:'localhost',
user:'root',
password:'root',
database:'user'
})
let conn = co(db)

const app = http.createServer(async (req,res)=>{
if(req.method === 'POST'){
if(req.url === '/user'){
// res.end(JSON.stringify({'message':'对user发起post请求'}))
req.on('data', async (data)=>{
arr.push(data)
})
req.on('end',async ()=>{
let buffer = Buffer.concat(arr);
// json对象
let {username,pasword} = JSON.parse(buffer.toString())
// console.log(username,pasword)
let sql = `selct user from admin where user = ${username}`
let data = await conn.query(sql);
// console.log(data)
if(data.length >=1 ){
res.end(JSON.stringify({
'status':200,
'message':'用户名已经注册'
}))
}else{
// 写入数据库
password = md5(password);
let sql = `INSERT INTO admin (user,password) VALUES ('${username}','${password}')`
await conn.query(sql);
res.end(JSON.stringify({
'status':200,
'message':'注册成功'
}))
}
})
}
}if(req.method === 'GET'){
if(req.url === '/user'){
// res.end(JSON.stringify({'message':'对user发起get请求'}))
let sql = `SELECT id,user,password FROM admin`
let data = await conn.query(sql);
res.end(JSON.stringify(data))
}
}
}).listen(3000)

// .http 文件
@url = http://localhost:3000
@type = Content-Type: applications

GET {{url}}/user HTTP/1.1

POST {{url}}/user HTTP/1.1
{{type}}

{
username:'admin',
password:123456
}
+

使用express实现(express — generater yard ,koa — async await)

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

const express = require('express')
const app = express()
const mysql = require('mysql'); // mysql
const co = require('co-mysql') // 异步同步化
const md5 = require('md5-node') // md5加密
const bodyparse = require('body-parse')
// 连接数据库
let db = mysql.createPool({
host:'localhost',
user:'root',
password:'root',
database:'user'
})
let conn = co(db)

app.use(bodyparse.urlencoded({
extended:true // 返回对象是兼职对,false - string/array true - any
}))
app.use(bodyparse.json())

app.post('/user',async (req.res)=>{
let { username , password} = req.body
// console.log(username,pasword)
let sql = `selct user from admin where user = ${username}`
let data = await conn.query(sql);
// console.log(data)
if(data.length >=1 ){
res.send(JSON.stringify({
'status':200,
'message':'用户名已经注册'
}))
}else{
// 写入数据库
password = md5(password);
let sql = `INSERT INTO admin (user,password) VALUES ('${username}','${password}')`
await conn.query(sql);
res.send(JSON.stringify({
'status':200,
'message':'注册成功'
}))
}
})

app.get('/user/:id',(req,res)=>{
res.send(req.params.id)

let sql = `SELECT id,user,password FROM admin WHERE id = ${req.params.id}`
let data = await conn.query(sql);
res.end(JSON.stringify(data))
})

app.listen(3000)
+ +

使用koa实现

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53


// config.js
module.exports = {
host:'localhost',
user:'root',
password:'root',
database:'user'
}

// libs/database.js
const config = require('../config')
const mysql = require('mysql'); // mysql
const co = require('co-mysql') // 异步同步化
// 连接数据库
let db = mysql.createPool({
host:config.host,
user:config.user,
password:config.password,
database:config.database
})
let conn = co(db)

// router/user/index.js
const Router = require('koa-router')
const md5 = require('md5-node') // md5加密
const router = new Router();

router.get('/user',async ctx=>{
ctx.body = '主页'
})
router.post('/user',async ctx=>{
let {username,password} = ctx.request.body
// console.log(username,password)
ctx.body = {
username,password
}
})
module.exports = router.routes();

// app.js
const koa = require('koa')
const Router = require('koa-router')
const body = require('koa-bodyparse')
const config = require('config')
const app = new Koa()
const router = new Router()
app.context.db = require('./libs/database')
app.context.config = config
app.use(body())
router.use('/api',require('./router/user'))
app.use(router.routes())
app.listen(3000)
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:NodeJS 下 RESTful 架构的最佳实践(课堂笔记)

+

文章作者:

+

发布时间:2020年02月11日 - 16:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/02/11/restful-architecture/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/02/12/print-table-problem/index.html b/ja/2020/02/12/print-table-problem/index.html new file mode 100644 index 0000000000..cc94b94f71 --- /dev/null +++ b/ja/2020/02/12/print-table-problem/index.html @@ -0,0 +1,591 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 由打印引起的一点小问题,写table时别忘了写thead和tbody | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 由打印引起的一点小问题,写table时别忘了写thead和tbody +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

这两天复工,公司一个小伙伴在群里问了一个问题

+
+

如何在打印表格的时候,让超过一页的表格分割线不被截断

+
+

说着他有贴上来了一张图,一张因为跨页被截断了的表格,说实话打印的场景接触的不多,打印表格一般都是制式的样式所以也不会出现这问题,一时间也没有头绪,想着这个是不是没什么办法解决,隔天他发来一个消息,说是解决了,给表格里加上了 tr td 就好了,接着他又发来一张图,还真是,不过上下边距发生了一些变化,我问他是不是自己改了边距所以好了,他说没改,然后我问他那他是怎么解决的,他说因为实在找不到问题解决方案和产生原因,于是就把代码重构了一下,按照标准写法写了一下,结果就好了。。。怎么说呢,也是神奇,我也到网上去查了查相关的材料,并没有特别对的上的原因,凭感觉来说,应该是thead和tbody上有默认的css样式解决了上面的问题,暂且记下这个问题,避免以后遇到又忘了是怎么回事。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:由打印引起的一点小问题,写table时别忘了写thead和tbody

+

文章作者:

+

发布时间:2020年02月12日 - 08:30

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/02/12/print-table-problem/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/02/12/vue-proxyTable-problem/index.html b/ja/2020/02/12/vue-proxyTable-problem/index.html new file mode 100644 index 0000000000..14a8fca3ce --- /dev/null +++ b/ja/2020/02/12/vue-proxyTable-problem/index.html @@ -0,0 +1,586 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Could not proxy request XXX from localhost:8080 to localhost:8081 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Could not proxy request XXX from localhost:8080 to localhost:8081 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

最近又有小伙伴问我,为啥我在写vue项目做代理转发时候报找不到代理服务的问题?
我看了一下他发给我的配置截图,仅一眼我就发现了问题所在,因为这个坑我曾经也踩过,而且好多小伙伴也都踩过,在写转发服务器的时候没写协议头,也就是在 target 那没写 http:// 这个部分,添加上以后重启一下,这个问题就解决了,小伙伴说就这个问题他解决了一天也没解决了,而且我说完了以后他想起来以前好像也遇到过这个问题,所以说在平时工作中遇到问题要常做笔记才对,所以为了避免以后自己也犯这种错误,暂且留一篇文章记录一下这个问题。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Could not proxy request XXX from localhost:8080 to localhost:8081

+

文章作者:

+

发布时间:2020年02月12日 - 09:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/02/12/vue-proxyTable-problem/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/02/19/develop-custom-cli-tools-using-node/index.html b/ja/2020/02/19/develop-custom-cli-tools-using-node/index.html new file mode 100644 index 0000000000..918023d2d5 --- /dev/null +++ b/ja/2020/02/19/develop-custom-cli-tools-using-node/index.html @@ -0,0 +1,651 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 使用node开发自定义cli工具 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 使用node开发自定义cli工具 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

这篇文章想写一写前端工程化相关的内容,原因一呢是是结合公司业务给自己定的业绩指标包含这些内容,另外一个原因是因为听了网易前端唐磊说过的一句话,前端leader干什么,很重要的工作就是前端工程化,高级以上不懂前端工程化很难。

+

今天听了公开课讲到了用node写一个自己的cli,说实话正是工作所需,课程讲的有点快,没有从头跟下来,自己上完了课又上网上找了些资料,终于把步骤弄明白了,下面就把如何自定义一个cli来帮助提升开发效率。同时也完成了一个业务指标,心里美滋滋。

+

准备

如果你看到这篇文章,也想跟着我的步骤写一下这个自定义cli,那么如下的知识还是有一些为好。

+
    +
  • 基础的nodejs相关知识
  • +
+

没错就只需要会一些node的基础知识就可以了,接下来正式开始

+

初始化

首先,我们要给我们的命令行工具起个名字,比如我们熟悉的 vue 命令行就是 vue-cli ,因为我写完了要给公司用,所以我起的名字是 tfd-cli ,你们喜欢叫什么你们随意

+

首先创建一个名字为 tfd-cli 的文件目录,然后在目录下执行 node 工程的初始化命令

+
1
npm init -y // 初始化项目 -y 默认全部yes的参数
+

命令执行完成后 tfd-cli 目录下会生成一个我们熟悉的 package.json 文件,我们打开 package.json 文件,增加一段代码,如下

+
1
2
3
4
// 追加的代码
"bin": {
"tfd": "index.js"
}
+

追加完成后,package.json 文件中的内容是这样的

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"name": "tfd-cli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"bin": {
"tfd": "index.js"
}
}
+

也就是说当我们执行 tfd 命令时它就会找同级目录下的 index.js 文件执行其中代码,我们目前还没有 index.js,文件,那么我们手动创建一个 index.js 的文件,然后在里面写下如下代码

+
1
2
3
4
#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的

console.log('hello world');
+

执行完这些还不够,因为我们是开发环境所以还有一步操作是要将 tfd 命令告知 npm,该如何处理,所以我们要执行如下命令

+
1
npm link
+

这里如果执行不成功,请用管理员权限执行,执行完成后我们会得到一个 package-lock.json 的文件,内容如下

+
1
2
3
4
5
{
"name": "tfd-cli",
"version": "1.0.0",
"lockfileVersion": 1
}
+

如此一来,我们就可以在任何目录下执行命令行 tfd 就会执行 index.js 文件了,这里我们会在控制面板中输出 hello world ,怎么样是不是小有成就感,我们接着往下来。

+

创建指令

我们写个命令行工具肯定不是为了输出个 hello world 这么简单,而是希望通过用户输入内容后根据条件输出一些东西,那么让我们想想一个命令行工具应该具备哪些指令呢?

+
    +
  • 查看命令行工具版本
  • +
  • 查看帮助文档
  • +
  • 初始化模板
  • +
  • 列出模板类型
  • +
  • 等等
  • +
+

那么用指令该如何描述呢

+
1
2
3
4
tfd -V|--version //查看工具版本号
tfd -h|--help //查看使用帮助
tfd init <template-name> <project-name> //基于指定模板进行项目初始化
tfd list //列出所有可用模板
+

为了执行命令,这里我们要引入一个 node 包叫做 commander,因此我们要先执行一下 install 命令

+
1
npm install commander
+

接着我们就可以在 index.js 里面写指令了。

+
1
2
3
4
5
6
7
8
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0
+

到这一步我们在控制台敲一下 tfd -V 你会发现什么也没输出,这是因为到这一步我们还无法解析 tfd -V 操作,在这之前我们要知道一个命令

+
1
process.argv // 使用process.argv获取命令行参数
+

当我们把这句话加到 console.log 中在 index.js 中输出时你会看到控制台打印出

+
1
2
3
4
5
6
7
8
9
10
11
12
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0
console.log(process.argv)

// 控制台
[ '/usr/local/bin/node', '/usr/local/bin/tfd', '-V' ]
+

接下来我们要让commander获取参数执行命令

+
1
2
3
4
5
6
7
8
9
10
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0
// console.log(process.argv)
cmd.parse(process.argv);
+

这个时候我们再在控制台输入 tfd -V 时,我们就会发现,控制台输出了 0.1.0,这样我们就完成了查版本的指令,接下来我们完成其他的指令

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0

// tfd init <template> <project>
cmd
.command('init <template> <project>') // 参数
.description('初始化项目模板')
.action((templateName, projectName) => {
console.log(templateName, projectName);
})

// tfd list
cmd
.command('list')
.description('查看所有可用模板')
.action(() => {
console.log(`
a a模板
b b模板
c c模板
`)
})
// console.log(process.argv)
cmd.parse(process.argv);
+

这个时候我们在控制台上输入 tfd -h 的时候,控制台会输出如下代码

+
1
2
3
4
5
6
7
8
9
Usage: tfd [options] [command]

Options:
-V, --version output the version number
-h, --help output usage information

Commands:
init <template> <project> 初始化项目模板
list 查看所有可用模板
+

这样我们就实现了自定义命令,我们执行一下 tfd init template1 project1,我们可以看到,控制台中输出了 template1 project1,也就是说command命令后尖括号中指向了action中的参数,我们就可以通过判断action中的参数做具体的操作了。

+

通常模板可以选择从本地拷贝一份,但更常用的是从线上拷贝一份,比如从github中,接下来我们就看看如何从github中拷贝一个模板作为项目的初始化工程

+

github上创建模板仓库

首先我们要在github上创建两个仓库 tpl-1 tpl-2,这里为了从github中下载仓库我们需要一个node包支持,让我们请出download-git-repo,别忘了执行安装命令

+
1
npm install download-git-repo
+

安装完依赖之后让我们再去index.js填点东西,首先引入下载依赖,然后是创建下载的 template 抽象对象

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');
const download = require('download-git-repo');
// 可用模板
const templates = {
'tpl-1': {
url: 'https://github.com/lixuguang/tpl-1',
downloadUrl: 'https://github.com:lixuguang/tpl-1#master',
description: 'tfd-cli脚手架测试模板1'
},
'tpl-2': {
url: 'https://github.com/lixuguang/tpl-2',
downloadUrl: 'https://github.com:lixuguang/tpl-2#master',
description: 'tfd-cli脚手架测试模板2'
}
}

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0

// tfd init <template> <project>
cmd
.command('init <template> <project>') // 参数
.description('初始化项目模板')
.action((templateName, projectName) => {
// console.log(templateName, projectName);
let {downloadUrl} = templates[templateName];
// 第一个参数是github仓库地址,第二个参数是创建的项目目录名,第三个参数是clone
download(downloadUrl, projectName, {clone: true}, err => {
if(err){
console.log('模板下载失败');
}else{
console.log('模板下载成功');
}
})
})

// tfd list
cmd
.command('list')
.description('查看所有可用模板')
.action(() => {
// console.log(`
// a a模板
// b b模板
// c c模板
// `)
// 通过获取templates里的key可以获取到模板名称
const templateName = Object.keys(templates)
console.log(templateName)
})
// console.log(process.argv)
cmd.parse(process.argv);
+

这样当我们执行 tfd list 就可以看到有哪些模板了,然后执行tfd init tpl-1 newproject 就可以依据 tpl-1 模板创建出 newproject 工程,这个过程实际上就是从github仓库克隆一份tpl-1作为模板创建工程newproject

+

这里需要注意的是download地址跟github仓库地址有点出入,比如github仓库地址是https://github.com/xxx/xxx而下载地址是https://github.com:xxx/xxx

+

虽然这样执行完成后就完成了基本的cli的雏形,但是毕竟不灵活,我们在使用vue-cli时,它的创建过程是问答式和选择式的,另外每个过程都会有进度显示什么的,那么要怎么添加这些功能呢,我们接着往下做。

+

进阶增加功能

使用inquirer进行命令行答询

inquirer 是一个进行命令行答询的库,通过它我们就可以创建问答式的内容,首先还是安装依赖

+
1
npm install inquirer
+

使用handlebars修改package.json

我们都知道在使用vue-cli的初始化命令后,会在项目目录下生成一个package.json文件,它就像是这个项目的基因序列一样,影响着项目的整个结构。模板是固定的,那要修改其中的package.json符合自己项目的需要,就要用到handlebars这个库来改写package.json文件,老规矩先安装它

+
1
npm install handlebars
+

使用ora在命令行中显示加载状态

我们在装任何依赖时都会有进度条显示进度,如果没有进度条又没有任何响应,会让用户迷茫,为了友好,我们就要加进度条,这里我们需要引入ora这个库来完成进度显示,我们接着安装。

+
1
npm install ora
+

使用chalk和log-symbols增加命令行输出样式

为了让命令行有红红绿绿的效果以及符号效果,我们需要使用chalklog-symbols来丰富样式,少废话,接着装

+
1
npm install chalk log-symbols
+ +

集大成

终于安装完一堆的依赖,别忘了在index.js中引入,让我们看看具体如何使用这些库吧。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');
const download = require('download-git-repo');

const iq = require('inquirer'); // 命令行答询
const hb = require('handlebars'); // 修改package.json文件
const ora = require('ora'); // 命令行中加载状态标识
const chalk = require('chalk'); // 命令行输出字符颜色
const ls = require('log-symbols'); // 命令行输出符号
const fs = require('fs'); // node fs原生模块

// 可用模板
const templates = {
'tpl-1': {
url: 'https://github.com/lixuguang/tpl-1',
downloadUrl: 'https://github.com:lixuguang/tpl-1#master',
description: 'tfd-cli脚手架测试模板1'
},
'tpl-2': {
url: 'https://github.com/lixuguang/tpl-2',
downloadUrl: 'https://github.com:lixuguang/tpl-2#master',
description: 'tfd-cli脚手架测试模板2'
}
}

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0

// tfd init <template> <project>
cmd
.command('init <template> <project>') // 参数
.description('初始化项目模板')
.action((templateName, projectName) => {
// console.log(templateName, projectName);
let {downloadUrl} = templates[templateName];
//下载github项目,下载墙loading提示
const loading = ora('模板下载中...').start();
// 第一个参数是github仓库地址,第二个参数是创建的项目目录名,第三个参数是clone
download(downloadUrl, projectName, {clone: true}, err => {
if(err){
// console.log('模板下载失败');
loading.fail('模板下载失败');
}else{
// console.log('模板下载成功');
spinner.succeed('模板下载成功');
// 命令行答询
iq.prompt([
{
type: 'input', // 类型 输入框
name: 'name', // 字段 key
message: '请输入项目名称', // 描述
default: projectName // 默认值
},
{
type: 'input',
name: 'description',
message: '请输入项目简介',
default: ''
},
{
type: 'input',
name: 'author',
message: '请输入作者名称',
default: ''
}
]).then(answers => { // answers 是一个对象,对象的 key 为上面答询的 name 的值,value 为 用户输入的值,如果未输入,就取默认值
// 根据命令行答询结果修改 package.json 文件
let packageContent = fs.readFileSync(`${projectName}/package.json`, 'utf8'); // 同步方式以 utf-8 字符集获得下载好的项目目录下的 package.json 文件
let packageResult = hb.compile(packageContent)(answers); // 将用户输入项与原内容混合获得新内容
fs.writeFileSync(`${projectName}/package.json`, packageResult); // 重新同步方式写入到 package.json 文件中
// 用chalk和log-symbols改变命令行输出样式
console.log(ls.success, chalk.green('模板项目文件准备成功!'));
})
}
})
})

// tfd list
cmd
.command('list')
.description('查看所有可用模板')
.action(() => {
// console.log(`
// a a模板
// b b模板
// c c模板
// `)
// 通过获取templates里的key可以获取到模板名称
const templateName = Object.keys(templates)
console.log(templateName)
})
// console.log(process.argv)
cmd.parse(process.argv);
+ +

到这里你自己的命令行工具就创建完成了,当然还可以继续丰富,比如加上选择条件等,这个就更复杂了,不在本文中展开,后续会继续改进这个命令行工具,当然如果要将自己的命令行工具给其他人用当然要先发布出去,就像我们要用vue-cli,首先要在npm上下载下来,同样的我们要想让别人用,就得先上传到npm上。

+

发布到npm上

首先你需要有一个npm的账号,没有赶快去注册一个,在控制台中输入npm login,它会让你依次输入 Username Password Email ,当你都按照要求输入完之后,成功的话你会获得如下信息Logged in as XXX on https://registry.npm.org/.,再接下来执行 npm publish 命令,你的自定义脚手架就会发布到npm上,供他人下载使用,怎么样,学会了么?

+

后记

这篇文章会有后续持续进化跟进,多篇文章连续,只要我的cli工具还在进化,文章就会继续,欢迎跟进。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:使用node开发自定义cli工具

+

文章作者:

+

发布时间:2020年02月19日 - 15:00

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/02/19/develop-custom-cli-tools-using-node/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/02/21/Implementation-of-the-vue-response-principle/index.html b/ja/2020/02/21/Implementation-of-the-vue-response-principle/index.html new file mode 100644 index 0000000000..d6b9450390 --- /dev/null +++ b/ja/2020/02/21/Implementation-of-the-vue-response-principle/index.html @@ -0,0 +1,693 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Vue 响应式原理的实现(课程笔记) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Vue 响应式原理的实现(课程笔记) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近一直在追网课,说实话从业9年了,一直觉得前端发展非常快,而且一直充满着危机感,每天都要学习进步才有安稳的感觉,今天听了 vue 响应式原理实现的公开课,感觉还不错,做了如下笔记,帮助自己记忆,也希望能帮助大家。

+

Vue2 原理

什么是 defineProperty

defineProperty 其实是定义对象属性用的

+
+

defineProperty 其实并不是核心的为一个对象做数据双向绑定,而是去给对象做属性标签,只不过是属性里 get 和 set 实现了响应式。

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
属性名默认值
valueundefined
getundefined
setundefined
writablefalse
enumerablefalse
configurablefalse
+
1
2
3
4
5
6
7
8
9
10
11
12
13
var ob = {
a:1,
b:2
}
// 参数 1、对象 2、属性 3、配置
Object.defineProperty(ob,'a',{
writable:false,
enumerable:true,
configurable:true,
})
console.log(Object.getOwnPropertyDescriptor(ob,'a')) // 1
ob.a = 2
console.log(ob.a) // 1
+

下面我们实现一下双向绑定

+
1
2
3
4
5
6
7
8
9
10
11
12
13
Object.defineProperty(ob,'a',{
get:function(){
console.log('a is be get')
return 999;
},
set:function(){
console.log('a is be set')
return 999;
},
})

console.log(Object.getOwnPropertyDescriptor(ob,'a')) // 999

+

改造代码实现双向绑定(存取值)

+
1
2
3
4
5
6
7
8
9
10
11
12
var _val = obj.a; // 暂存
Object.defineProperty(ob,'a',{
get:function(){
console.log('a is be get')
return _val;
},
set:function(newVal){
_val = newVal // 新值替换旧值
console.log('a is be set')
return _val;
},
})
+

Vue 中从改变一个数据到发生改变的过程

    +
  1. 改变数据触发 Set
  2. +
  3. Set 部分触发 notify(更新)
      +
    1. Get 部分收集依赖
    2. +
    +
  4. +
  5. 更改对应的虚拟 Dom
  6. +
  7. 重新 Render
  8. +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// MyVue.js

// 简单版本 vue
function MyVue(){
this.$data = {
a: {
b:1
},
c:2
}
this.el = document.getElementById('app');
this.virtualDom = '';
this.observer(this.$data);
this.render();
}
vue.property.observer = function(obj){
var _val, self = this;
// var dep = new Dep() -> 源码中依赖收集对象
for(var key in obj){ // 属性有可能是对象,要递归绑定
_val = obj[key];
if(typeof _val === 'Object'){
this.observer(_val)
}else{
Object.defineProperty(this.$data,key,{ // 这里是实际绑定过程
get:function(){
// 依赖收集
// dep.depend(); -> vue 源码中收集依赖的方法
return _val
},
set:function(newVal){
_val = newVal
// dep.notify(); -> vue 源码中
self.render() // AST语法树
}
})
}
}
}
vue.property.render = function(){
this.virtualDom = 'i am '+this.$data.b;
this.el.innerHTML = this.virtualDom;
}
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// index.html

<!DOCTYPE html>
<html>
<head>
<title>自己实现Vue2数据双向绑定</title>
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src='myVue.js'></script>
<script type="text/javascript">
var mv = new MyVue();
setTimeout(function(){
console.log('changes');
console.log(mv.$data);
mv.$data.b = 222;
})
</script>
</body>
</html>
+
+

依赖收集:

+
    +
  1. 我们的data里面的数据并不是所有地方都用到
  2. +
  3. 如果我们直接更新整个视图,浪费资源
  4. +
  5. 先收集依赖改变的数据的组件,再更新依赖了数据的组件(Dep depend notify)
  6. +
+
+

格外注意的地方—数组怎么监听

definePropty 只能给对象进行 get set 绑定, 数组怎么办?

+

vue 中 使用了 装饰者模式

+
+

装饰者模式 Decorator模式(别名Wrapper):动态将职责附加到对象上,若要扩展功能,装饰者提供了比继承更具弹性的代替方案。

+
+
1
2
3
4
5
6
7
8
9
10
var arraypro = Array.property; // 创建一个数组的原型对象
var arrob = Object.create(arraypro); // 避免影响原型链
var arr = ['push','pop','shift'];
arr.forEach(function(method,index){
arrob[method]=function(){ // 装饰者模式
var ret = arraypro[method].apply(this,arguments)
dep.notify() // 扩展了功能
}
})

+

Vue3 实现双向绑定

Proxy 是什么?

+
+

Proxy 对象用于定义基本操作的自定义行为
和 definePropty 类似,功能几乎一样,只是用法上不同

+
    +
  1. 不会污染原对象
  2. +
  3. 直接给对象就可以了
  4. +
  5. 不需要借助外部变量 _val
  6. +
+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var ob = {
a:1,
b:2
}

var newOb = new Proxy(ob,{
get(target,key,receiver){ // target 对象,key 属性
console.log(target,key,receiver)
return target[key]
},
set(target,key,value,receiver){
return Reflect.set(target.key,value);
// 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
// return target[key] = value
}
})
+

为什么改用 Proxy

    +
  1. defineProperty 只能监听某个属性,不能全对象监听
  2. +
  3. 可以省去for in循环提升代码执行效率
  4. +
  5. 可以监听数组,不需要再为数组做特异性操作
  6. +
  7. 不污染原对象
  8. +
  9. 更优雅
  10. +
+

我们用 Proxy 实现一下 observe 方法

+
1
2
3
4
5
6
7
8
9
10
11
12
vue.property.observe = function(){
var self = this;
this.$data = new Proxy(this.$data,{
get(target,key, receiver){
return target[key]
},
set(target,key,newVal){
target[key] = newVal
self.render()
}
})
}
+ +

还能用 Proxy 做什么

    +
  1. 校验类型
  2. +
  3. 真正的私有变量
  4. +
+
校验类型

例子:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 数据类型验证
// 我们要创建一个对象,这个对象是个人,他有name和age两个属性
// name必须是中文,age必须是数字,大于18岁

// 这里用到了策略模式
var valid = {
name(value){
var reg=/^[\u4E00-\u9FAS]=$/
if(typeof value === 'string' && reg.test(value)){
return true;
}
return false;
},
age(value){
if(typeof value === 'number' && value > 18){
return true;
}
return false;
}
}
function Person(name,age){
this.name = name
this.age = age
return new Proxy(this,{
get(target,key){
return target[key]
},
set(target,key,value){
if(valid[key](value)){
return Reflect.set(target,key,value)
}else{
throw new Error(key+'is not valid')
}
}
})
}
new Person('name',19)
+
+

策略模式
定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。

+
+
真正的私有变量

vue-router 源码中,给 $router ,$route 用 defineProperty 定义 get 并返回本身,这样就不能修改属性了。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
Object.defineProperty(this,'$router',{ // Router 的实例
get(){
return this._root._router;
}
})
Object.defineProperty(this,'$route',{
get(){
return {
// 当前路由所在的状态
current: this._root._router.history.current;
}
}
})
+ +

虚拟Dom和diff算法

虚拟Dom是虚拟的,他只在概念里面存在,在AST语法树,下面进行解释

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 模板
<template>
<div>
<p>{{msg}}</p>
<p>2</p>
<p>3</p>
</div>
</template>

// diff 描述法
diff <div>
props:{
id:2
}
children:[
diff <p>
props:{
id:xxx
}
children:[
...
]
]

// 对象描述法
var virtual = {
dom:'div',
props:{
id:2
},
children:[
....
]
}
+

每层结构都是一样的,那么是如何进行 diff 比对的呢?

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** 
* diff 算法
*/
patchVnode(oldVnode,vnode){ // 接收新旧节点
const el = vnode.el = oldVnode.el; // 拿出真实dom
let i,oldCh = oldVnode.children ,ch = vnode.children // 拿出新旧节点子元素children数组
if(oldVnode === vnode) return; // 新旧节点一致,直接返回不进行后续操作
// 分情况操作 --- 只有文字节点,删除了子元素,增加了子元素,子元素发生变动
// 新旧节点都不为空,且不一样
if(oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text){ // 文字发生更新
api.setTextContent(el,vonde.text) // 文字发生了更新,更新文字,给Dom,更新文字
} else { // 不是单纯文字节点的话
updateEle(); // 更新元素
if(oldCh&&ch&&oldCh!==ch){ // 都有子元素,子元素变动
updateChildren() // 调用更新子元素方法
} else if(ch){ // 增加子元素
createEl(vnode) // 创建子元素
} else if(oldCh){ // 删除子元素
api.removeChildren(el) // 调用删除子元素方法
}
}
}
+

源码要多看,以下必看 Vue/react/axios/vue-router/Redux/Vuex
为什么要看源码??

+
    +
  • 初级前端就会用vue或react — 从差不多水平的60%中挑出更好的人
  • +
  • 提高思想–》看优秀的代码–》写优秀的代码
  • +
  • 看源码能力,对高级前端是必备的。— 解决疑难杂症,看源码了解原理。
  • +
+

vue 性能优化

因为是公开课,所以时间上没来的及说完,以后自己在听别的有关的内容时再补上这块。

+

最后

只有不断学习才能进步,充分利用网络的便利性,找各种优质的教学资源,我相信,努力会有回报,加油!

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Vue 响应式原理的实现(课程笔记)

+

文章作者:

+

发布时间:2020年02月21日 - 23:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/02/21/Implementation-of-the-vue-response-principle/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/02/23/V8-engine-memory-management-and-optimization/index.html b/ja/2020/02/23/V8-engine-memory-management-and-optimization/index.html new file mode 100644 index 0000000000..13737202d0 --- /dev/null +++ b/ja/2020/02/23/V8-engine-memory-management-and-optimization/index.html @@ -0,0 +1,645 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + v8引擎如何回收内存(笔记) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ v8引擎如何回收内存(笔记) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

V8引擎如何回收垃圾

为什么我们要关注内存

    +
  • 防止页面占用内存过大,引起客户端卡顿,甚至无响应
  • +
  • Node使用的也是v8,内存对于后端服务的性能至关重要。因为服务的持久性,后端更容易造成内存溢出

    v8引擎的内存回收机制

    v8的内存分配

    新生代内存空间
  • +
  • from
  • +
  • to
    老生代内存空间

    内存大小

  • +
  • 和操作系统有关 — 64位(1.4G)32位(0.7G)
  • +
  • 64位下 新生代(64MB) 老生代(1400MB)
  • +
  • 32位下 新生代(16MB) 老生代(700MB)
  • +
+

为什么不占多一点内存

+
    +
  • js设计之初是为浏览器
      +
    • 前端特点 — 不持久化,执行一遍就回收了,所以1.4G够用了
    • +
    • js回收内存会暂停执行代码
    • +
    +
  • +
+

垃圾回收算法

新生代简单的说就是复制

+
    +
  • 新生代用来放新产生的变量,根据条件放到老生代,有用的放到to里(把有用的留一边,清空另一边,往复进行)
  • +
  • 算法复杂度(时间复杂度,空间复杂度)—- 牺牲空间换时间
  • +
+

老生代就是标记、删除、整理

+
    +
  • 为什么要整理
      +
    • 数组是需要连续的空间
    • +
    +
  • +
+

新生代如何晋升到老生代

+
    +
  • 变量是否经过回收,第二次回收放到老生代,第一次回收放到To
  • +
  • To空间使用了25%,放到老生代
  • +
+

V8是如何处理变量的

1
2
3
4
5
6
7
8
9
10
11
12
13
// 浏览器查看内存
window.performance
// nodejs查看内存 --- nodejs是c++的,可以拓宽内存
process.memoryUsage()

// 拿内存的方法
function getMem(){
var mem = process.memoryUsage();
var format = function(bytes){
return (bytes/1024/1024).toFixed(2)+'MB';
}
console.log('heapTotal:'+format(mem.heapTotal)+'heapUsed:'+format(mem.heapUsed))
}
+

变量处理

    +
  • 内存主要就是存储变量等数据的
  • +
  • 局部变量当程序执行结束,且没有引用的时候就会随着消失 — 可以被回收但不是说会马上回收
  • +
  • 全局对象会始终存活到程序运行结束
  • +
+

如何查看V8内存使用情况

如何注意内存使用

优化内存的技巧

    +
  • 尽量不要定义全局变量
  • +
  • 全局变量记得手动销毁掉
      +
    • 不推荐开发时写delete – 支持有问题,严格模式有bug
    • +
    • 赋值为 undefined/null undefined 是变量 null 是保留字
    • +
    +
  • +
  • 用匿名自执行函数变全局为局部
      +
    • (function(){})()
    • +
    +
  • +
  • 尽量避免使用闭包 —- 错误的观点,ie5时代的问题 —- 尽量避免使用闭包引用
  • +
+

闭包

+
1
2
3
4
5
6
7
function a(){
var size = 20*1024*1024;
var arr1 = new Array(size)
return arr1
}
a() // 这样就没问题
var b = a() // 因为引用所以无法销毁
+

防止内存泄漏

    +
  • 滥用缓存
  • +
  • 大内存量操作
  • +
+

所有的优化都可以用缓存来解决,缓存通常都在全局,缓存可以直接拿来用;

+
1
2
3
4
5
6
7
8
9
10
11
12
13
var 20*1024*1024;
var a = []
for(var i=0;i<13;i++){
a.push(new Array(size))
}

// 加缓存锁
for(var i=0;i<13;i++){
if(a.length>4){
a.shift();
}
a.push(new Array(size))
}
+
    +
  • 不要用v8来缓存
      +
    • 一定要用要的话加锁
    • +
    +
  • +
+

nodejs中读取大文件要用流的形式,不要用读文件到buffer
fs.readFile()
fs.createReadStream()

+

浏览器中,大文件上传记得切片
file.slice(0,1000)
file.slice(1000,2000)

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:v8引擎如何回收内存(笔记)

+

文章作者:

+

发布时间:2020年02月23日 - 08:00

+

最后更新:2020年02月23日 - 07:57

+

原始链接:https://blog.lifesli.com/2020/02/23/V8-engine-memory-management-and-optimization/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/02/24/Vue-plug-in-development/index.html b/ja/2020/02/24/Vue-plug-in-development/index.html new file mode 100644 index 0000000000..77fad70fa0 --- /dev/null +++ b/ja/2020/02/24/Vue-plug-in-development/index.html @@ -0,0 +1,606 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + vue插件开发(笔记) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ vue插件开发(笔记) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

vue插件开发

+

Vue.use({install(Vues){}})

+
+

Vue.use

把给到的内容执行一下
举例

+
1
2
3
4
function a(){
console.log('a')
}
Vue.use(a) // a
+

有 install 就执行 install

+
1
2
3
4
5
6
7
function a(){
console.log('a')
}
a.install = function(){
console.log('b')
}
Vue.use(a) // b
+

再进一步

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function a(){
console.log('a')
}
a.install = function(){
// console.log('b')
vue.mixin({ // 抽离公共逻辑 , 缺点:命名冲突,难以阅读
data(){ // data数据少的时候可以不用vuex 用mixin
return {
c:'this is mixin'
}
},
methods:{
// 混入方法
// 提示性弹窗 原:import 控制 显隐 现在:在根节点引入,通过mixin在方法控制显隐
}
// 混入生命周期
create(){
// 所有组件的create生命周期都执行 mixin先执行
}
})
}
Vue.use(a) // b
+ +

vue.util.defineReactive()

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
vue.util.defineReactive()

var test = {
testa: 1
}
setTimeout(()=>{
test.testa = 2
},1000)
vue.mixin({
beforeCreate(){
this.test = test
}
})

+ +

vue.extend vue.util.extend

+

vue.util.extend ===> 简单做了个拷贝,拷贝到一起

+
1
vue.util.extend(a,b)
+

vue.extend ===> 获取到某个对象的实例

+
1
2
let Constrator = vue.extend(obj)
let vm = new Constrator()
+ +

手写vue-router

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// myVueRouter.js
class HistoryRoute(){
constructor(){
this.current = null;
}
}

class vueRouter{
constructor(options){
this.mode = options.mode || 'hash'
this.history = new HistoryRoute
this.routes = options.routes||[]
this.routesMap = this.createMap(this.routes)
this.init()
}
init(){
if(this.mode == 'hash'){
// 自动加上 #
location.hash?"":location.hash="/"
window.addEventListener('load',()=>{
this.history.current = location.hash.slice(1)
})
window.addEventListener('hashchange',()=>{
this.history.current = location.hash.slice(1)
})
}else{
location.pathname?"":location.pathname="/"
window.addEventListener('load',()=>{
this.history.current = location.hash.pathname
})
window.addEventListener('popstate',()=>{
this.history.current = location.hash.pathname
})
}
}
createMap(router){
return router.reduce((memo,current)=>{
memo[current.path] = current.component
})
}
}

vueRouter.install = function(Vue){
Vue.mixin({
beforeCreate(){ // 组件还未实例化好
if(this.$options && this.$options.router){ // 有配置而且引入路由
this._root = this
this._router = this.$option.router

Vue.util.defineReactive(this,'current',this._router.history)
}else{
this._root = this.$parent._root
}
// 增强健壮性
Object.defineProperty(this,'$route',{
get(){
return this._root._router
}
})
}
})
Vue.component('router-view',{
render(h){
// 如何根据当前的current,获取到对应的组件
let current = this._self._root._router.history.current
let routerMap = this._self._root._router.routeMap
return h(routeMap[current])
}
})
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:vue插件开发(笔记)

+

文章作者:

+

发布时间:2020年02月24日 - 22:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/02/24/Vue-plug-in-development/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/03/03/Speeding-up-brew-with-Ali-mirroring/index.html b/ja/2020/03/03/Speeding-up-brew-with-Ali-mirroring/index.html new file mode 100644 index 0000000000..a48c24a8e0 --- /dev/null +++ b/ja/2020/03/03/Speeding-up-brew-with-Ali-mirroring/index.html @@ -0,0 +1,603 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 使用阿里镜像加速brew(转载) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 使用阿里镜像加速brew(转载) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

使用 Alibaba 的 Homebrew 镜像源进行加速

平时我们执行 brew 命令安装软件的时候,跟以下 3 个仓库地址有关:

+
    +
  • brew.git
  • +
  • homebrew-core.git
  • +
  • homebrew-bottles
    通过以下操作将这 3 个仓库地址全部替换为 Alibaba 提供的地址
  • +
+

1. 替换 / 还原 brew.git 仓库地址

1
2
3
4
5
6
7
8
9
# 替换成阿里巴巴的 brew.git 仓库地址:
cd "$(brew --repo)"
git remote set-url origin https://mirrors.aliyun.com/homebrew/brew.git

#=======================================================

# 还原为官方提供的 brew.git 仓库地址
cd "$(brew --repo)"
git remote set-url origin https://github.com/Homebrew/brew.git
+ +

2. 替换 / 还原 homebrew-core.git 仓库地址

1
2
3
4
5
6
7
8
9
# 替换成阿里巴巴的 homebrew-core.git 仓库地址:
cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
git remote set-url origin https://mirrors.aliyun.com/homebrew/homebrew-core.git

#=======================================================

# 还原为官方提供的 homebrew-core.git 仓库地址
cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
git remote set-url origin https://github.com/Homebrew/homebrew-core.git
+ +

3. 替换 / 还原 homebrew-bottles 访问地址

这个步骤跟你的 macOS 系统使用的 shell 版本有关系

+

所以,先来查看当前使用的 shell 版本

+
1
2
3
4
echo $SHELL

# 如果你的输出结果是 /bin/zsh,参考下方的 zsh 终端操作方式
# 如果你的输出结果是 /bin/bash,参考下方的 bash 终端操作方式
+

3.1 zsh 终端操作方式

1
2
3
4
5
6
7
8
9
10
# 替换成阿里巴巴的 homebrew-bottles 访问地址:
echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles' >> ~/.zshrc
source ~/.zshrc

#=======================================================

# 还原为官方提供的 homebrew-bottles 访问地址
vi ~/.zshrc
# 然后,删除 HOMEBREW_BOTTLE_DOMAIN 这一行配置
source ~/.zshrc
+

3.2 bash 终端操作方式

1
2
3
4
5
6
7
8
9
10
# 替换 homebrew-bottles 访问 URL:
echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles' >> ~/.bash_profile
source ~/.bash_profile

#=======================================================

# 还原为官方提供的 homebrew-bottles 访问地址
vi ~/.bash_profile
# 然后,删除 HOMEBREW_BOTTLE_DOMAIN 这一行配置
source ~/.bash_profile
+ +

转载自:http://www.xiegangd.com/article/154055689187484

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:使用阿里镜像加速brew(转载)

+

文章作者:

+

发布时间:2020年03月03日 - 22:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/03/03/Speeding-up-brew-with-Ali-mirroring/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/03/23/path-join-vs-path-resolve/index.html b/ja/2020/03/23/path-join-vs-path-resolve/index.html new file mode 100644 index 0000000000..7534080b09 --- /dev/null +++ b/ja/2020/03/23/path-join-vs-path-resolve/index.html @@ -0,0 +1,587 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + path.join 与 path.resolve 的区别 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ path.join 与 path.resolve 的区别 +

+ + +
+ + + + +
+ + +

path.join 与 path.resolve 的区别

    +
  1. 对于以/开始的路径片段,path.join只是简单的将该路径片段进行拼接,而path.resolve将以/开始的路径片段作为根目录,在此之前的路径将会被丢弃,就像是在terminal中使用cd命令一样。
  2. +
+
1
2
path.join('/a', '/b') // 'a/b'
path.resolve('/a', '/b') // '/b'
+
    +
  1. path.resolve总是返回一个以相对于当前的工作目录(working directory)的绝对路径。
  2. +
+
1
2
path.join('./a', './b') // 'a/b'
path.resolve('./a', './b') // '/Users/username/Projects/webpack-demo/a/b'
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:path.join 与 path.resolve 的区别

+

文章作者:

+

发布时间:2020年03月23日 - 14:42

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/03/23/path-join-vs-path-resolve/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/03/24/do-it-yourselfery-EventEmitter/index.html b/ja/2020/03/24/do-it-yourselfery-EventEmitter/index.html new file mode 100644 index 0000000000..00758c717c --- /dev/null +++ b/ja/2020/03/24/do-it-yourselfery-EventEmitter/index.html @@ -0,0 +1,592 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- EventEmitter(事件触发器) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- EventEmitter(事件触发器) +

+ + +
+ + + + +
+ + +

实现一个 EventEmitter

    +
  1. 创建一个 Event 类,包含构造函数、绑定、解绑、触发方法
  2. +
  3. on 监听event事件,事件触发时调用fn函数。根据字典创建事件数组,如果事件处理不存在,那么就推入数组,反之返回原数组
  4. +
  5. off 从字典中获取当前事件数组,如果获取值是数组,那么判断解绑哪个事件,如果没填清空全部(数组长度置为零),否则从事件数组删除选择的事件
  6. +
  7. emit 触发event事件,并把参数arg1,arg2,arg3….传给事件处理函数,跟解绑外层判断一样,内层把删除改为执行即可
  8. +
  9. once 为指定事件注册一个单次监听器,单次监听器最多只触发一次,触发后立即解除监听器。
  10. +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class Event {
constructor () {
// 储存事件的数据结构
// 为查找迅速, 使用对象(字典)
this._cache = {}
}

// 绑定
on(event, callback) {
// 为了按类查找方便和节省空间
// 将同一类型事件放到一个数组中
// 这里的数组是队列, 遵循先进先出
// 即新绑定的事件先触发
let fns = (this._cache[event] = this._cache[event] || [])
if(fns.indexOf(callback) === -1) {
fns.push(callback)
}
return this
}

// 解绑
off (event, callback) {
let fns = this._cache[event]
if(Array.isArray(fns)) {
if(callback) {
let index = fns.indexOf(callback)
if(index !== -1) {
fns.splice(index, 1)
}
} else {
// 全部清空
fns.length = 0
}
}
return this
}
// 触发emit
emit(event, ...args) {
let fns = this._cache[event]
if(Array.isArray(fns)) {
fns.forEach((fn) => {
fn(...args)
})
}
return this
}

// 一次性绑定
once(event, callback) {
let onceCallback = () => { // 定义一个只执行一次就解绑的方法
callback.call(this); // 使用call改变this指向
this.off(event, onceCallback); // 解绑
};
this.on(event, onceCallback); // 绑定
return this;
}
}
+

好的接下来我们调用一下

+
1
2
3
4
5
6
7
8

let e = new Event()

e.on('click',function(){
console.log('on')
})
// e.trigger('click', '666')
console.log(e)
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- EventEmitter(事件触发器)

+

文章作者:

+

发布时间:2020年03月24日 - 15:45

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-EventEmitter/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/03/24/do-it-yourselfery-async-await/index.html b/ja/2020/03/24/do-it-yourselfery-async-await/index.html new file mode 100644 index 0000000000..698eacf7be --- /dev/null +++ b/ja/2020/03/24/do-it-yourselfery-async-await/index.html @@ -0,0 +1,584 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- async、await | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- async、await +

+ + +
+ + + + +
+ + +

原理

就是利用 generator (生成器)分割代码片段。然后我们使用一个函数让其自迭代,每一个 yieldpromise 包裹起来。执行下一步的时机由 promise 来控制

+

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function _asyncToGenerator(fn) {
return function() {
var self = this, args = arguments;
return new Promise(function(resolve, reject) { // 将返回值 promise 化
var gen = fn.apply(self, args); // 获取迭代器实例
function _next(value) { // 执行下一步
asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
}
function _throw(err) { // 抛出异常
asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
}
_next(undefined); // 第一次触发
});
};
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- async、await

+

文章作者:

+

发布时间:2020年03月24日 - 15:50

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-async-await/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/03/24/do-it-yourselfery-call-apply/index.html b/ja/2020/03/24/do-it-yourselfery-call-apply/index.html new file mode 100644 index 0000000000..50f373047c --- /dev/null +++ b/ja/2020/03/24/do-it-yourselfery-call-apply/index.html @@ -0,0 +1,618 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- call、apply、bind | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- call、apply、bind +

+ + +
+ + + + +
+ + +

原版

先来看一个call实例,看看call到底做了什么:

+
1
2
3
4
5
6
7
let foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call(foo); // 1
+

从代码的执行结果,我们可以看到,call首先改变了this的指向,使函数的this指向了foo,然后使bar函数执行了。
总结一下:

+
    +
  • call改变函数this指向
  • +
  • 调用函数
  • +
+

自己动手

    +
  1. 首先我们对参数 context 做了兼容处理,不传值, context 默认值为 window
  2. +
  3. 然后我们将函数挂载到 context 上面, context.fn = this
  4. +
  5. 处理参数,将传入 myCall 的参数截取,去除第一位,然后转为数组;
  6. +
  7. 调用 context.fn ,此时 fnthis 指向 context
  8. +
  9. 删除对象上的属性 delete context.fn
  10. +
  11. 将结果返回。
  12. +
+
1
2
3
4
5
6
7
8
Function.prototype.myCall = function(context) {
context = context || window;
context.fn = this; // 将函数挂载到对象的fn属性上
const args = [...arguments].slice(1); // 处理传入的参数
const result = context.fn(...args); // 通过对象的属性调用该方法
delete context.fn; // 删除该属性
return result // 返回结果
};
+

applycall 的区别在于参数, 其他没有差别,实现如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// myApply的参数形式为(obj,[arg1,arg2,arg3]);
// 所以myApply的第二个参数为[arg1,arg2,arg3]
// 这里我们用扩展运算符来处理一下参数的传入方式
Function.prototype.myApply = function(context) {
context = context || window
context.fn = this
let result
if (arguments[1]) { // 判断是否有第二个参数
result = context.fn(…arguments[1]) // 有的话传入执行
} else {
result = context.fn() // 没有的话空参执行
}
delete context.fn;
return result
};
+

bindcallapply 作用都是改变 this 的指向,区别在于 bind 改变后不会立即执行,而 callapply 会立即执行,我们看一下 bind 的用法

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(){
this.name="zs";
this.age=18;
this.gender="男"
}
let obj={
hobby:"看书"
}

let changePerson = Person.bind(obj); // 将构造函数的this绑定为obj
changePerson(); // 直接调用构造函数,函数会操作obj对象,给其添加三个属性;
console.log(obj); // => {hobby:"看书",name:"zs",age:18,gender:'男'}
let p = new changePerson(); // 用改变了this 指向的构造函数,new一个实例出来
console.log(p); // => Person {name:"zs",age:18,gender:'男'}
+

仔细观察上面的代码,再看输出结果。

+

我们对 Person 类使用了 bind 将其 this 指向 obj ,得到了 changePerson 函数,此处如果我们直接调用 changeperson 会改变 obj ,若用 new 调用 changeperson 会得到实例 p,并且其 __proto__ 指向 Person ,我们发现 bind 失效了。

+

我们得到结论:bind 改变了 this 指向的函数,如果用 new 操作符来调用, bind 将会失效

+

这个对象就是这个构造函数的实例,那么只要在函数内部执行 * this instanceof 构造函数 * 来判断其结果是否为 true ,就能判断函数是否是通过 new 操作符来调用了,若结果为 true 则是用 new 操作符调用的,总结如下:

+
    +
  1. 保存当前 this 指向
  2. +
  3. 保存环境上下文
  4. +
  5. 保存参数,去掉第一个对象参数
  6. +
  7. 返回待执行函数
      +
    1. 数组化剩余参数
    2. +
    3. 判断是否为构造函数
    4. +
    5. 若是执行构造函数,若不是改变 this 指向执行
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      // bind实现

      Function.prototype.myBind = function(context){
      let _this = this; // 1、保存函数
      context = context || window; // 2、保存目标对象
      let rest = [...arguments].slice(1); // 3、保存目标对象之外的参数,将其转化为数组;
      // 此处开始与 call 和 apply 不同,不是返回结果,而是返回一个函数
      return function F(){ // 4、返回一个待执行的函数
      let rest2 = Array.prototype.slice.call(arguments) // 5、这里的arguments是F函数的参数,转换为数组;
      if(this instanceof F){
      return new _this(...rest2) // 6、若是用new操作符调用,则直接用new 调用原函数,并用扩展运算符传递参数
      }else{
      _this.apply(context,rest.concat(rest2)); // 7、用apply调用第一步保存的函数,并绑定this,传递合并的参数数组,
      // 即context._this(rest.concat(rest2))
      }
      }
      };
    6. +
    +
  8. +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- call、apply、bind

+

文章作者:

+

发布时间:2020年03月24日 - 15:54

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-call-apply/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/03/24/do-it-yourselfery-create/index.html b/ja/2020/03/24/do-it-yourselfery-create/index.html new file mode 100644 index 0000000000..e99d930336 --- /dev/null +++ b/ja/2020/03/24/do-it-yourselfery-create/index.html @@ -0,0 +1,588 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- Object.create() | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- Object.create() +

+ + +
+ + + + +
+ + +

实现一个 Object.create() 方法

    +
  1. 创建一个空匿名函数
  2. +
  3. 函数原型对象指向传入对象实例
  4. +
  5. 返回构造函数创建的实例
  6. +
+
1
2
3
4
5
function create =  function (o) {
var F = function () {};
F.prototype = o;
return new F();
};
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- Object.create()

+

文章作者:

+

发布时间:2020年03月24日 - 14:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-create/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/03/24/do-it-yourselfery-flat/index.html b/ja/2020/03/24/do-it-yourselfery-flat/index.html new file mode 100644 index 0000000000..0898c3c327 --- /dev/null +++ b/ja/2020/03/24/do-it-yourselfery-flat/index.html @@ -0,0 +1,587 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- Array.prototype.flat()函数 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- Array.prototype.flat()函数 +

+ + +
+ + + + +
+ + +

原理

将多层数组扁平化

+

实现

1
2
3
4
5
6
7
8
9
10
11
Array.prototype.myFlat = function() {
var arr = [];
this.forEach((item)=>{
if(Array.isArray(item)){
arr = arr.concat(item.myFlat()); // 如果是数组的话继续循环
}else{
arr.push(item)
}
})
return arr
};
+ +

还有另外一种实现方式,非常好用

+
1
2
3
4
5
Array.prototype.myFlat = function() {
return this.toString() // => "1,2,3,4"
.split(",") // => ["1", "2", "3", "4"]
.map(item => +item); // => [1, 2, 3, 4]
};
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- Array.prototype.flat()函数

+

文章作者:

+

发布时间:2020年03月24日 - 13:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-flat/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/03/24/do-it-yourselfery-isArray/index.html b/ja/2020/03/24/do-it-yourselfery-isArray/index.html new file mode 100644 index 0000000000..be128aef4e --- /dev/null +++ b/ja/2020/03/24/do-it-yourselfery-isArray/index.html @@ -0,0 +1,584 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- Array.isArray | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- Array.isArray +

+ + +
+ + + + +
+ + +

实现一个Array.isArray

思路很简单,就是利用 Object.prototype.toString

+
1
2
3
Array.myIsArray = function(o) { 
return Object.prototype.toString.call(Object(o)) === '[object Array]';
};
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- Array.isArray

+

文章作者:

+

发布时间:2020年03月24日 - 13:30

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-isArray/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/03/24/do-it-yourselfery-map/index.html b/ja/2020/03/24/do-it-yourselfery-map/index.html new file mode 100644 index 0000000000..9e307396ad --- /dev/null +++ b/ja/2020/03/24/do-it-yourselfery-map/index.html @@ -0,0 +1,589 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- Array.prototype.map() | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- Array.prototype.map() +

+ + +
+ + + + +
+ + +

原理

先看看 reducemap 的使用方法

+
1
2
let new_array = arr.map(function callback(currentValue[, index[,array) {/* Return element for new_array */ }[, thisArg])
let result = arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
+ +

实现

第一种用 for 实现

+
1
2
3
4
5
6
7
Array.prototype.myMap = function(callback, thisArg) {
let arr = [];
for (let i = 0; i < this.length; i++) {
arr.push(callback.call(thisArg, this[i], i, this));
}
return arr;
};
+

第二种用 reduce 实现

+
1
2
3
4
5
6
7
Array.prototype.myMap = function(callback, thisArg) {
let result = this.reduce((accumulator, currentValue, index, array) => {
accumulator.push(callback.call(thisArg, currentValue, index, array));
return accumulator;
}, []);
return result;
};
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- Array.prototype.map()

+

文章作者:

+

发布时间:2020年03月24日 - 15:20

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-map/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/03/24/do-it-yourselfery-new/index.html b/ja/2020/03/24/do-it-yourselfery-new/index.html new file mode 100644 index 0000000000..2591401b36 --- /dev/null +++ b/ja/2020/03/24/do-it-yourselfery-new/index.html @@ -0,0 +1,591 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- new | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- new +

+ + +
+ + + + +
+ + +

实现一个new操作符

我们首先知道new做了什么:

+
    +
  1. 创建一个空的简单 JavaScript 对象(即{})
  2. +
  3. 链接该对象(即设置该对象的构造函数)到另一个对象
  4. +
  5. 将步骤(1)新创建的对象作为 this 的上下文
  6. +
  7. 如果该函数没有返回对象,则返回 this
  8. +
+

知道new做了什么,接下来我们就来实现它

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function create(){
// 创建一个空的对象
let obj = {};
// 获得构造函数
let Con = [].shift.call(arguments)
// 将空对象指向构造函数的原型链
Object.setPrototypeOf(obj, Con.prototype);
// obj.__proto__ = Con.prototype // 链接到原型
// obj绑定到构造函数上,便可以访问构造函数中的属性,即this.obj.Con(args)
let result = Con.apply(obj, arguments);
// 如果返回的result是一个对象则返回
// new方法失效,否则返回obj
return result instanceof Object ? result : this.obj;
// return typeof result === 'object' ? result : obj// 确保 new 出来的是个对象
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- new

+

文章作者:

+

发布时间:2020年03月24日 - 04:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-new/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/03/24/do-it-yourselfery-reduce/index.html b/ja/2020/03/24/do-it-yourselfery-reduce/index.html new file mode 100644 index 0000000000..2277ba4bcc --- /dev/null +++ b/ja/2020/03/24/do-it-yourselfery-reduce/index.html @@ -0,0 +1,587 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- Array.prototype.reduce | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- Array.prototype.reduce +

+ + +
+ + + + +
+ + +

原版

1
Array.prototype.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
+ +

自己动手

1
2
3
4
5
6
7
8
Array.prototype.myReduce = function(callback, initialValue) {
let accumulator = initialValue ? initialValue : this[0]; // 第一次使用判断时候有initialValue参数,如果有用他,没有用this[0],这里this指的是传入的数组,this[0]就是数组第一项
for (let i = initialValue ? 0 : 1; i < this.length; i++) { // 如果有初始值从0开始循环,不然从1开始
let _this = this; // 保留当前this指向
accumulator = callback(accumulator, this[i], i, _this); //
}
return accumulator; // 返回迭代器的终值
};
+

试用一下

+
1
2
3
4
5
6
7
let arr = [1, 2, 3, 4];
let sum = arr.myReduce((acc, val) => {
acc += val;
return acc;
}, 5);

console.log(sum); // 15
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- Array.prototype.reduce

+

文章作者:

+

发布时间:2020年03月24日 - 04:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-reduce/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2020/03/24/do-it-yourselfery-\344\272\213\344\273\266\344\273\243\347\220\206/index.html" "b/ja/2020/03/24/do-it-yourselfery-\344\272\213\344\273\266\344\273\243\347\220\206/index.html" new file mode 100644 index 0000000000..6f9cbbf0d0 --- /dev/null +++ "b/ja/2020/03/24/do-it-yourselfery-\344\272\213\344\273\266\344\273\243\347\220\206/index.html" @@ -0,0 +1,583 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- 事件代理 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- 事件代理 +

+ + +
+ + + + +
+ + +

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<ul id="color-list">
<li>red</li>
<li>yellow</li>
<li>blue</li>
<li>green</li>
<li>black</li>
<li>white</li>
</ul>

<script>
(function () {
var color_list = document.getElementById('color-list');
color_list.addEventListener('click', showColor, true);
function showColor(e) {
var x = e.target;
if (x.nodeName.toLowerCase() === 'li') {
alert(x.innerHTML);
}
}
})();
</script>
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- 事件代理

+

文章作者:

+

发布时间:2020年03月24日 - 14:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-%E4%BA%8B%E4%BB%B6%E4%BB%A3%E7%90%86/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2020/03/24/do-it-yourselfery-\345\217\214\345\220\221\347\273\221\345\256\232/index.html" "b/ja/2020/03/24/do-it-yourselfery-\345\217\214\345\220\221\347\273\221\345\256\232/index.html" new file mode 100644 index 0000000000..3d5f2cd949 --- /dev/null +++ "b/ja/2020/03/24/do-it-yourselfery-\345\217\214\345\220\221\347\273\221\345\256\232/index.html" @@ -0,0 +1,585 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己动手实现系列 ---- Vue双向绑定 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己动手实现系列 ---- Vue双向绑定 +

+ + +
+ + + + +
+ + +

Vue 2.x 的 Object.defineProperty 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 数据
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');

// 数据劫持
Object.defineProperty(data, 'text', {
// 数据变化 —> 修改视图
set(newVal) {
input.value = newVal;
span.innerHTML = newVal;
}
});

// 视图更改 --> 数据变化
input.addEventListener('keyup', function(e) {
data.text = e.target.value;
});
+ +

Vue 3.x 的 proxy 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 数据
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');

// 数据劫持
const handler = {
set(target, key, value) {
target[key] = value;
// 数据变化 —> 修改视图
input.value = value;
span.innerHTML = value;
return value;
}
};
const proxy = new Proxy(data, handler);

// 视图更改 --> 数据变化
input.addEventListener('keyup', function(e) {
proxy.text = e.target.value;
});
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:自己动手实现系列 ---- Vue双向绑定

+

文章作者:

+

发布时间:2020年03月24日 - 15:40

+

最后更新:2020年04月02日 - 03:20

+

原始链接:https://blog.lifesli.com/2020/03/24/do-it-yourselfery-%E5%8F%8C%E5%90%91%E7%BB%91%E5%AE%9A/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/03/24/webpack-learning-1/index.html b/ja/2020/03/24/webpack-learning-1/index.html new file mode 100644 index 0000000000..1795bccb9c --- /dev/null +++ b/ja/2020/03/24/webpack-learning-1/index.html @@ -0,0 +1,877 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2020年了,再不会webpack敲得代码就不香了(近万字实战)【转载】 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 2020年了,再不会webpack敲得代码就不香了(近万字实战)【转载】 +

+ + +
+ + + + +
+ + +

推荐序

这里是我自己写的,看了这篇文章把我零碎的 webpack 知识系统的整理了一下,感觉受益匪浅,推荐更多小伙伴看一看这篇文章,好文共享,建议稍微对 webpack 了解服用更佳。
2020年了,再不会webpack敲得代码就不香了(近万字实战)

+

前言

2020年即将到来,在众多前端的招聘要求里, webpack 、工程化这些字眼频率越来越高。日常开发者中,我们常常在用诸如 vue-clicreate-react-app 的脚手架来构建我们的项目。但是如果你想在团队脱颖而出(鹤立鸡群)、拿到更好的 offer (还房贷),那么你必须去深刻的认识下我们经常打交道的 webpack

+

入门(一起来用这些小例子让你熟悉webpack的配置)

webpack 是什么?

webpack 是一个现代 JavaScript 应用程序的静态模块打包器,当 webpack 处理应用程序时,会递归构建一个依赖关系图,其中包含应用程序需要的每个模块,然后将这些模块打包成一个或多个 bundle

+

webpack 的核心概念

    +
  • entry: 入口
  • +
  • output: 输出
  • +
  • loader: 模块转换器,用于把模块原内容按照需求转换成新内容
  • +
  • plugins: 扩展插件,在 webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要做的事情
  • +
+

初始化项目

新建一个目录,初始化 npm

+
1
npm init
+ +

webpack 是运行在 node 环境中的,我们需要安装以下两个 npm

+
1
npm i -D webpack webpack-cli
+ +
+
    +
  • npm i -Dnpm install --save-dev 的缩写
  • +
  • npm i -Snpm install --save 的缩写
  • +
+
+

新建一个文件夹 src ,然后新建一个文件 main.js ,写一点代码测试一下

+
1
console.log('call me 老yuan')
+ +

配置 package.json 命令

+
1
2
3
"script":{
"build":"webpack src/main.js"
}
+

执行

+
1
npm run build
+

此时如果生成了一个 dist 文件夹,并且内部含有 main.js 说明已经打包成功了

+

开始我们自己的配置

上面一个简单的例子只是 webpack 自己默认的配置,下面我们要实现更加丰富的自定义配置

+

新建一个 build 文件夹,里面新建一个 webpack.config.js

+
1
2
3
4
5
6
7
8
9
10
11
// webpack.config.js

const path = require('path');
module.exports = {
mode:'development', // 开发模式
entry: path.resolve(__dirname,'../src/main.js'), // 入口文件
output: {
filename: 'output.js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
}
}
+

更改我们的打包命令

+
1
2
3
"script":{
"build":"webpack build/webpack.config.js"
}
+

执行 npm run build
会发现生成了以下目录

+
1
2
3
4
project
dist
build
src
+

其中 dist 文件夹中的 main.js 就是我们需要在浏览器中实际运行的文件
当然实际运用中不会仅仅如此,下面让我们通过实际案例带你快速入手 webpack

+

配置html模板

js 文件打包好了,但是我们不可能每次在html文件中手动引入打包好的js

+
+

这里可能有的朋友会认为我们打包 js 文件名称不是一直是固定的嘛( output.js )?这样每次就不用改动引入文件名称了呀?实际上我们日常开发中往往会这样配置:

+
+
1
2
3
4
5
6
7
module.exports = {
// 省略其他配置
output: {
filename: '[name].[hash:8].js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
}
}
+ +

这时候生成的 dist 目录文件如下

+
1
2
dist/
app.fsafasf.js
+

为了缓存,你会发现打包好的 js 文件的名称每次都不一样。 webpack 打包出来的js文件我们需要引入到 html 中,但是每次我们都手动修改 js 文件名显得很麻烦,因此我们需要一个插件来帮我们完成这件事情,那就是 html-webpack-plugin

+
1
npm i -D html-webpack-plugin
+

新建一个 build 同级的文件夹 public ,里面新建一个 index.html
具体配置文件如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin') // **此插件用来解决html模板引入js文件
module.exports = {
mode:'development', // 开发模式
entry: path.resolve(__dirname,'../src/main.js'), // 入口文件
output: {
filename: '[name].[hash:8].js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
},
plugins:[
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html')
})
]
}
+

可以发现打包生成的js文件已经被自动引入 html 文件中

+

多入口文件如何开发

+

生成多个 html-webpack-plugin 实例来解决这个问题

+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode:'development', // 开发模式
entry: {
main:path.resolve(__dirname,'../src/main.js'),
header:path.resolve(__dirname,'../src/header.js')
},
output: {
filename: '[name].[hash:8].js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
},
plugins:[
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html'),
filename:'index.html',
chunks:['main'] // 与入口文件对应的模块名
}),
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/header.html'),
filename:'header.html',
chunks:['header'] // 与入口文件对应的模块名
}),
]
}
+ +

clean-webpack-plugin

+

每次执行 npm run build 会发现 dist 文件夹里会残留上次打包的文件,这里我们推荐一个 plugin 来帮我们在打包输出前清空文件夹 clean-webpack-plugin

+
+
1
2
3
4
5
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
module.exports = {
// ...省略其他配置
plugins:[new CleanWebpackPlugin()]
}
+
希望dist目录下某个文件夹不被清空

不过呢,有些时候,我们并不希望整个 dist 目录都被清空,比如,我们不希望,每次打包的时候,都删除 dll 目录,以及 dll 目录下的文件或子目录,该怎么办呢?

+

clean-webpack-plugin 为我们提供了参数 cleanOnceBeforeBuildPatterns

+
1
2
3
4
5
6
7
8
9
//webpack.config.js
module.exports = {
//...
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns:['**/*', '!dll', '!dll/**'] //不删除dll目录下的文件
})
]
}
+ +

引用CSS

我们的入口文件是 js ,所以我们在入口 js 中引入我们的 css 文件

+
1
import 'asset/style.css'
+

同时我们也需要一些 loader 来解析我们的 css 文件

+
1
npm i -D style-loader css-loader
+

如果我们使用 less 来构建样式,则需要多安装两个

+
1
npm i -D less less-loader
+

配置文件如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.config.js
module.exports = {
// ...省略其他配置
module:{
rules:[
{
test:/\.css$/,
use:['style-loader','css-loader'] // 从右向左解析原则
},
{
test:/\.less$/,
use:['style-loader','css-loader','less-loader'] // 从右向左解析原则
}
]
}
}
+ +

我们简单说一下上面的配置:

+
    +
  • style-loader 动态创建 style 标签,将 css 插入到 head 中.
  • +
  • css-loader 负责处理 @import 等语句。
  • +
  • postcss-loaderautoprefixer ,自动生成浏览器兼容性前缀 —— 2020了,应该没人去自己徒手去写浏览器前缀了吧
  • +
  • less-loader 负责处理编译 .less 文件,将其转为 css
  • +
+
+

注意:
loader 的执行顺序是从右向左执行的,也就是后面的 loader 先执行,上面 loader 的执行顺序为: less-loader —> postcss-loader —> css-loader —> style-loader
当然,loader 其实还有一个参数,可以修改优先级,enforce 参数,其值可以为: pre(优先执行) 或 post (滞后执行)。
现在,我们已经可以处理 .less 文件啦,.css 文件只需要修改匹配规则,删除 less-loader 即可。

+
+

为css添加浏览器前缀

1
2
npm i -D postcss-loader autoprefixer

+

配置如下

+
1
2
3
4
5
6
7
8
9
// webpack.config.js
module.exports = {
module:{
rules:[
test/\.less$/,
use:['style-loader','css-loader','postcss-loader','less-loader'] // 从右向左解析原则
]
}
}
+

接下来,我们还需要引入 autoprefixer 使其生效,这里有两种方式

+
在项目根目录下创建一个postcss.config.js文件,配置如下:
1
2
3
module.exports = {
plugins: [require('autoprefixer')] // 引用该插件即可了
}
+
直接在webpack.config.js里配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// webpack.config.js
module.exports = {
//...省略其他配置
module:{
rules:[{
test:/\.less$/,
use:['style-loader','css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
},'less-loader'] // 从右向左解析原则
}]
}
}
+

这时候我们发现 css 通过 style 标签的方式添加到了 html 文件中,但是如果样式文件很多,全部添加到 html 中,难免显得混乱。这时候我们想用把 css 拆分出来用外链的形式引入 css 文件怎么做呢?这时候我们就需要借助插件来帮助我们

+

拆分css

1
npm i -D mini-css-extract-plugin
+
+

webpack 4.0 以前,我们通过 extract-text-webpack-plugin 插件,把 css 样式从 js 文件中提取到单独的 css 文件中。 webpack4.0 以后,官方推荐使用 mini-css-extract-plugin 插件来打包 css 文件

+
+

配置文件如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
//...省略其他配置
module: {
rules: [
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader'
],
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].[hash].css",
chunkFilename: "[id].css",
})
]
}
+

拆分多个css

+

这里需要说的细一点,上面我们所用到的 mini-css-extract-plugin 将所有的 css 样式合并为一个 css 文件。如果你想拆分为一一对应的多个 css 文件,我们需要使用到 extract-text-webpack-plugin ,而目前 mini-css-extract-plugin 还不支持此功能。我们需要安装 @next 版本的 extract-text-webpack-plugin

+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
npm i -D extract-text-webpack-plugin@next
// webpack.config.js

const path = require('path');
const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')
let indexLess = new ExtractTextWebpackPlugin('index.less');
let indexCss = new ExtractTextWebpackPlugin('index.css');
module.exports = {
module:{
rules:[
{
test:/\.css$/,
use: indexCss.extract({
use: ['css-loader']
})
},
{
test:/\.less$/,
use: indexLess.extract({
use: ['css-loader','less-loader']
})
}
]
},
plugins:[
indexLess,
indexCss
]
}
+

打包 图片、字体、媒体、等文件

file-loader 就是将文件在进行一些处理后(主要是处理文件名和路径、解析文件 url ),并将文件移动到输出的目录中
url-loader 一般与 file-loader 搭配使用,功能与 file-loader 类似,如果文件小于限制的大小。则会返回 base64 编码,否则使用 file-loader 将文件移动到输出的目录中

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// webpack.config.js
module.exports = {
// 省略其它配置 ...
module: {
rules: [
// ...
{
test: /\.(jpe?g|png|gif)$/i, //图片文件
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]'
}
}
}
}
]
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, //媒体文件
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'media/[name].[hash:8].[ext]'
}
}
}
}
]
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i, // 字体
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'fonts/[name].[hash:8].[ext]'
}
}
}
}
]
},
]
}
}
+

用babel转义js文件

为了使我们的 js 代码兼容更多的环境我们需要安装依赖

+
1
2
npm i babel-loader @babel/preset-env @babel/core

+
+

注意
babel-loaderbabel-core 的版本对应关系

+
+
    +
  • babel-loader 8.x 对应 babel-core 7.x
  • +
  • babel-loader 7.x 对应 babel-core 6.x
  • +
+

配置如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// webpack.config.js
module.exports = {
// 省略其它配置 ...
module:{
rules:[
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:['@babel/preset-env']
}
},
exclude:/node_modules/
},
]
}
}
+

上面的 babel-loader 只会将 ES6/7/8 语法转换为 ES5 语法,但是对新 api 并不会转换 例如( promiseGeneratorSetMapsProxy 等)
此时我们需要借助 babel-polyfill 来帮助我们转换

+
1
2
3
4
5
6
npm i @babel/polyfill
// webpack.config.js
const path = require('path')
module.exports = {
entry: ["@babel/polyfill,path.resolve(__dirname,'../src/index.js')"], // 入口文件
}
+
+

手动把上面的 demo 敲一遍对阅读下面的文章更有益,建议入门的同学敲三遍以上

+
+

上面的实践是我们对 webpack 的功能有了一个初步的了解,但是要想熟练应用于开发中,我们需要一个系统的实战。让我们一起摆脱脚手架尝试自己搭建一个 vue 开发环境

+

搭建vue开发环境

上面的小例子已经帮助而我们实现了打包 css 、图片、 jshtml 等文件。
但是我们还需要以下几种配置

+

解析.vue文件

1
2
npm i -D vue-loader vue-template-compiler vue-style-loader
npm i -S vue
+
    +
  • vue-loader 用于解析 .vue 文件
  • +
  • vue-template-compiler 用于编译模板
  • +
+

配置如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const vueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
module:{
rules:[{
test:/\.vue$/,
use:['vue-loader']
},]
},
resolve:{
alias:{
'vue$':'vue/dist/vue.runtime.esm.js',
' @':path.resolve(__dirname,'../src')
},
extensions:['*','.js','.json','.vue']
},
plugins:[
new vueLoaderPlugin()
]
}
+

配置webpack-dev-server进行热更新

1
npm i -D webpack-dev-server
+

配置如下

+
1
2
3
4
5
6
7
8
9
10
11
12
const Webpack = require('webpack')
module.exports = {
// ...省略其他配置
devServer:{
port:3000,
hot:true,
contentBase:'../dist'
},
plugins:[
new Webpack.HotModuleReplacementPlugin()
]
}
+

完整配置如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// webpack.config.js
const path = require('path');
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')
const vueLoaderPlugin = require('vue-loader/lib/plugin')
const Webpack = require('webpack')
module.exports = {
mode:'development', // 开发模式
entry: {
main:path.resolve(__dirname,'../src/main.js'),
},
output: {
filename: '[name].[hash:8].js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
},
module:{
rules:[
{
test:/\.vue$/,
use:['vue-loader']
},
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:[
['@babel/preset-env']
]
}
}
},
{
test:/\.css$/,
use: ['vue-style-loader','css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
}]
},
{
test:/\.less$/,
use: ['vue-style-loader','css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
},'less-loader']
}
]
},
resolve:{
alias:{
'vue$':'vue/dist/vue.runtime.esm.js',
' @':path.resolve(__dirname,'../src')
},
extensions:['*','.js','.json','.vue']
},
devServer:{
port:3000,
hot:true,
contentBase:'../dist'
},
plugins:[
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html'),
filename:'index.html'
}),
new vueLoaderPlugin(),
new Webpack.HotModuleReplacementPlugin()
]
}
+

配置打包命令

1
2
3
4
"script":{
"dev":"webpack-dev-server --config build/webpack.config.js --open",
"build":"webpack --config build/webpack.config.js"
}
+

打包文件已经配置完毕,接下来让我们测试一下
首先在 src 新建一个 main.js

+
1
2
3
4
5
6
// main.js
import Vue from 'vue'
import App from './app'
new Vue({
render:h=>h(App)
}).$mount('#app')
+

新建一个 App.vue

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// App.vue
<template>
<div id='container'></div>
</template>
<script>
export default {
data(){
return {
initData:''
}
}
}
</script>
<style scoped>
#container{
width:100%;
height:100%;
}
</style>
+

新建一个 public 文件夹,里面新建一个 index.html

+
1
2
3
4
5
6
7
8
9
10
11
12
13
// index.html
<!DOCTYPE html>
<html lang='en'>
<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'>
<title>lao li</title>
</head>
<body>
<div id='app'></div>
</body>
</html>
+

执行 npm run dev 这时候如果浏览器出现 Vue 开发环境运行成功,那么恭喜你,已经成功迈出了第一步

+

区分开发环境与生产环境

实际应用到项目中,我们需要区分开发环境与生产环境,我们在原来 webpack.config.js 的基础上再新增两个文件

+

webpack.dev.js 开发环境配置文件
开发环境主要实现的是热更新,不要压缩代码,完整的 sourceMap
webpack.prod.js生产环境配置文件
生产环境主要实现的是压缩代码、提取 css 文件、合理的 sourceMap 、分割代码
需要安装以下模块:

+
1
npm i -D  webpack-merge copy-webpack-plugin optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin
+
    +
  • webpack-merge 合并配置
  • +
  • copy-webpack-plugin 拷贝静态资源
  • +
  • optimize-css-assets-webpack-plugin 压缩 css
  • +
  • uglifyjs-webpack-plugin 压缩js
  • +
+
+

webpack mode 设置 production 的时候会自动压缩 js 代码。
原则上不需要引入 uglifyjs-webpack-plugin 进行重复工作。
但是 optimize-css-assets-webpack-plugin 压缩 css 的同时会破坏原有的 js 压缩,所以这里我们引入 uglifyjs 进行压缩

+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// webpack.config.js
const path = require('path')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const vueLoaderPlugin = require('vue-loader/lib/plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const devMode = process.argv.indexOf('--mode=production') === -1;
module.exports = {
entry:{
main:path.resolve(__dirname,'../src/main.js')
},
output:{
path:path.resolve(__dirname,'../dist'),
filename:'js/[name].[hash:8].js',
chunkFilename:'js/[name].[hash:8].js'
},
module:{
rules:[
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:['@babel/preset-env']
}
},
exclude:/node_modules/
},
{
test:/\.vue$/,
use:['cache-loader','thread-loader',{
loader:'vue-loader',
options:{
compilerOptions:{
preserveWhitespace:false
}
}
}]
},
{
test:/\.css$/,
use:[{
loader: devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
options:{
publicPath:"../dist/css/",
hmr:devMode
}
},'css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
}]
},
{
test:/\.less$/,
use:[{
loader:devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
options:{
publicPath:"../dist/css/",
hmr:devMode
}
},'css-loader','less-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
}]
},
{
test:/\.(jep?g|png|gif)$/,
use:{
loader:'url-loader',
options:{
limit:10240,
fallback:{
loader:'file-loader',
options:{
name:'img/[name].[hash:8].[ext]'
}
}
}
}
},
{
test:/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
use:{
loader:'url-loader',
options:{
limit:10240,
fallback:{
loader:'file-loader',
options:{
name:'media/[name].[hash:8].[ext]'
}
}
}
}
},
{
test:/\.(woff2?|eot|ttf|otf)(\?.*)?$/i,
use:{
loader:'url-loader',
options:{
limit:10240,
fallback:{
loader:'file-loader',
options:{
name:'media/[name].[hash:8].[ext]'
}
}
}
}
}
]
},
resolve:{
alias:{
'vue$':'vue/dist/vue.runtime.esm.js',
' @':path.resolve(__dirname,'../src')
},
extensions:['*','.js','.json','.vue']
},
plugins:[
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html')
}),
new vueLoaderPlugin(),
new MiniCssExtractPlugin({
filename: devMode ? '[name].css' : '[name].[hash].css',
chunkFilename: devMode ? '[id].css' : '[id].[hash].css'
})
]
}
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// webpack.dev.js
const Webpack = require('webpack')
const webpackConfig = require('./webpack.config.js')
const WebpackMerge = require('webpack-merge')

module.exports = WebpackMerge(webpackConfig,{
mode:'development',
devtool:'cheap-module-eval-source-map',
devServer:{
port:3000,
hot:true,
contentBase:'../dist'
},
plugins:[
new Webpack.HotModuleReplacementPlugin()
]
})
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// webpack.prod.js
const path = require('path')
const webpackConfig = require('./webpack.config.js')
const WebpackMerge = require('webpack-merge')

const CopyWebpackPlugin = require('copy-webpack-plugin')
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

module.exports = WebpackMerge(webpackConfig,{
mode:'production',
devtool:'cheap-module-source-map',
plugins:[
new CopyWebpackPlugin([{
from:path.resolve(__dirname,'../public'),
to:path.resolve(__dirname,'../dist')
}]),
],
optimization:{
minimizer:[
new UglifyJsPlugin({//压缩js
cache:true,
parallel:true,
sourceMap:true
}),
new OptimizeCssAssetsPlugin({})
],
splitChunks:{
chunks:'all',
cacheGroups:{
libs: {
name: "chunk-libs",
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: "initial" // 只打包初始时依赖的第三方
}
}
}
}
})
+ +

优化webpack配置

看到这里你或许有些累了,但是要想获取更好的offer,更高的薪水,下面必须继续深入

+

优化配置对我们来说非常有实际意义,这实际关系到你打包出来文件的大小,打包的速度等。
具体优化可以分为以下几点:

+

优化打包速度

+

构建速度指的是我们每次修改代码后热更新的速度以及发布前打包文件的速度。

+
+

合理的配置 mode 参数与 devtool 参数

devtool 可设置的值
mode 可设置 development production 两个参数

+

如果没有设置, webpack4 会将 mode 的默认值设置为 production

+
    +
  • production :将 process.env.NODE_ENV 的值设置为 production ,启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPluginUglifyJsPlugin,会进行 tree shaking (去除无用代码)和 uglifyjs (代码压缩混淆)
  • +
  • development :将 process.env.NODE_ENV 的值设置为 development ,启用 NamedChunksPluginNamedModulesPlugin
  • +
+

缩小文件的搜索范围(配置include exclude alias noParse extensions)

    +
  • alias 当我们代码中出现 import ‘vue’时, webpack会采用向上递归搜索的方式去node_modules 目录下找。为了减少搜索范围我们可以直接告诉webpack去哪个路径下查找。也就是别名(alias)的配置。
  • +
  • include exclude 同样配置include exclude也可以减少webpack loader的搜索转换时间。
  • +
  • noParse 当我们代码中使用到 import jq from 'jquery' 时, webpack 会去解析 jq 这个库是否有依赖其他的包。但是我们对类似 jquery 这类依赖库,一般会认为不会引用其他的包(特殊除外,自行判断)。增加 noParse 属性,告诉 webpack 不必解析,以此增加打包速度。
  • +
  • extensions webpack 会根据 extensions 定义的后缀查找文件(频率较高的文件类型优先写在前面)
  • +
+

配图

+

使用HappyPack开启多进程Loader转换

+

webpack 构建过程中,实际上耗费时间大多数用在 loader 解析转换以及代码的压缩中。日常开发中我们需要使用 Loaderjscss ,图片,字体等文件做转换操作,并且转换的文件数据量也是非常大。由于js单线程的特性使得这些转换操作不能并发处理文件,而是需要一个个文件进行处理。 HappyPack 的基本原理是将这部分任务分解到多个子进程中去并行处理,子进程处理完成后把结果发送到主进程中,从而减少总的构建时间

+
+
1
npm i -D happypack
+ +

happypack

+

使用 webpack-parallel-uglify-plugin 增强代码压缩

上面对于 loader 转换已经做优化,那么下面还有另一个难点就是优化代码的压缩时间。

+
1
npm i -D webpack-parallel-uglify-plugin
+

webpack-parallel-uglify-plugin

+

抽离第三方模块

+

对于开发项目中不经常会变更的静态依赖文件。类似于我们的 elementUivue 全家桶等等。因为很少会变更,所以我们不希望这些依赖要被集成到每一次的构建逻辑中去。 这样做的好处是每次更改我本地代码的文件的时候, webpack 只需要打包我项目本身的文件代码,而不会再去编译第三方库。以后只要我们不升级第三方包的时候,那么 webpack 就不会对这些库去打包,这样可以快速的提高打包的速度。

+
+

这里我们使用 webpack 内置的 DllPlugin DllReferencePlugin 进行抽离

+

在与 webpack 配置文件同级目录下新建 webpack.dll.config.js
代码如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// webpack.dll.config.js
const path = require("path");
const webpack = require("webpack");
module.exports = {
// 你想要打包的模块的数组
entry: {
vendor: ['vue','element-ui']
},
output: {
path: path.resolve(__dirname, 'static/js'), // 打包后文件输出的位置
filename: '[name].dll.js',
library: '[name]_library'
// 这里需要和webpack.DllPlugin中的`name: '[name]_library',`保持一致。
},
plugins: [
new webpack.DllPlugin({
path: path.resolve(__dirname, '[name]-manifest.json'),
name: '[name]_library',
context: __dirname
})
]
};
+

package.json 中配置如下命令

+
1
"dll": "webpack --config build/webpack.dll.config.js"
+ +

接下来在我们的 webpack.config.js 中增加以下代码

+
1
2
3
4
5
6
7
8
9
10
11
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./vendor-manifest.json')
}),
new CopyWebpackPlugin([ // 拷贝生成的文件到dist目录 这样每次不必手动去cv
{from: 'static', to:'static'}
]),
]
};
+

执行

+
1
npm run dll
+ +

会发现生成了我们需要的集合第三地方
代码的 vendor.dll.js
我们需要在html文件中手动引入这个js文件

+
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<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">
<title>老yuan</title>
<script src="static/js/vendor.dll.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
+

这样如果我们没有更新第三方依赖包,就不必 npm run dll 。直接执行 npm run dev npm run build 的时候会发现我们的打包速度明显有所提升。因为我们已经通过 dllPlugin 将第三方依赖包抽离出来了。

+

配置缓存

+

我们每次执行构建都会把所有的文件都重复编译一遍,这样的重复工作是否可以被缓存下来呢,答案是可以的,目前大部分 loader 都提供了 cache 配置项。比如在 babel-loader 中,可以通过设置cacheDirectory 来开启缓存,babel-loader?cacheDirectory=true 就会将每次的编译结果写进硬盘文件(默认是在项目根目录下的 node_modules/.cache/babel-loader 目录内,当然你也可以自定义)
但如果 loader 不支持缓存呢?我们也有方法,我们可以通过 cache-loader ,它所做的事情很简单,就是 babel-loader 开启 cache 后做的事情,将 loader 的编译结果写入硬盘缓存。再次构建会先比较一下,如果文件较之前的没有发生变化则会直接使用缓存。使用方法如官方 demo 所示,在一些性能开销较大的 loader 之前添加此 loader 即可

+
+
1
npm i -D cache-loader
+

cache-loader

+

优化打包文件体积

打包的速度我们是进行了优化,但是打包后的文件体积却是十分大,造成了页面加载缓慢,浪费流量等,接下来让我们从文件体积上继续优化

+

引入webpack-bundle-analyzer分析打包后的文件

webpack-bundle-analyzer 将打包后的内容束展示为方便交互的直观树状图,让我们知道我们所构建包中真正引入的内容

+
1
npm i -D webpack-bundle-analyzer
+

webpack-bundle-analyzer

+

接下来在 package.json 里配置启动命令

+
1
"analyz": "NODE_ENV=production npm_config_report=true npm run build"
+

windows 请安装 npm i -D cross-env

+
1
"analyz": "cross-env NODE_ENV=production npm_config_report=true npm run build"
+

接下来 npm run analyz 浏览器会自动打开文件依赖图的网页

+

externals

+

按照官方文档的解释,如果我们想引用一个库,但是又不想让 webpack 打包,并且又不影响我们在程序中以 CMDAMD 或者 window/global 全局等方式进行使用,那就可以通过配置 Externals 。这个功能主要是用在创建一个库的时候用的,但是也可以在我们项目开发中充分使用Externals 的方式,我们将这些不需要打包的静态资源从构建逻辑中剔除出去,而使用 CDN的方式,去引用它们。
有时我们希望我们通过 script 引入的库,如用 CDN 的方式引入的 jquery ,我们在使用时,依旧用 require 的方式来使用,但是却不希望 webpack 将它又编译进文件中。这里官网案例已经足够清晰明了,大家有兴趣可以点击了解
webpack
官网案例如下

+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
<script
src="https://code.jquery.com/jquery-3.1.0.js"
integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
crossorigin="anonymous">
</script>
module.exports = {
//...
externals: {
jquery: 'jQuery'
}
};
import $ from 'jquery';
$('.my-element').animate(/* ... */);
+

Tree-shaking

+

这里单独提一下 tree-shaking ,是因为这里有个坑。 tree-shaking 的主要作用是用来清除代码中无用的部分。目前在 webpack4 我们设置 modeproduction 的时候已经自动开启了tree-shaking 。但是要想使其生效,生成的代码必须是ES6模块。不能使用其它类型的模块如 CommonJS 之流。如果使用 Babel 的话,这里有一个小问题,因为 Babel 的预案( preset )默认会将任何模块类型都转译成 CommonJS 类型。修正这个问题也很简单,在 .babelrc 文件或在 webpack.config.js 文件中设置 modules:false 就好了

+
+
1
2
3
4
5
6
7
8
9
10
// .babelrc
{
"presets": [
["@babel/preset-env",
{
"modules": false
}
]
]
}
+

或者

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.config.js

module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', { modules: false }]
}
},
exclude: /(node_modules)/
}
]
}
+ +

经历过上面两个系列的洗礼,到现在我们成为了一名合格的 webpack 配置工程师。但是光拧螺丝,自身的可替代性还是很高,下面我们将深入 webpack 的原理中去

+

手写webpack系列

经历过上面两个部分,我们已经可以熟练的运用相关的 loaderplugin 对我们的代码进行转换、解析。接下来我们自己手动实现 loaderplugin ,使其在平时的开发中获得更多的乐趣。

+

手写 webpack loader

+

loader 从本质上来说其实就是一个 node 模块。相当于一台榨汁机( loader )将相关类型的文件代码( code )给它。根据我们设置的规则,经过它的一系列加工后还给我们加工好的果汁( code )。

+
+

loader 编写原则

    +
  • 单一原则: 每个 Loader 只做一件事;
  • +
  • 链式调用: Webpack 会按顺序链式调用每个 Loader
  • +
  • 统一原则: 遵循 Webpack 制定的设计规则和结构,输入与输出均为字符串,各个 Loader 完全独立,即插即用;
  • +
+

在日常开发环境中,为了方便调试我们往往会加入许多 console 打印。但是我们不希望在生产环境中存在打印的值。那么这里我们自己实现一个 loader 去除代码中的 console

+
+

知识点普及之 ASTAST 通俗的来说,假设我们有一个文件 a.js ,我们对 a.js 里面的1000行进行一些操作处理,比如为所有的 await 增加 try catch ,以及其他操作,但是 a.js 里面的代码本质上来说就是一堆字符串。那我们怎么办呢,那就是转换为带标记信息的对象(抽象语法树)我们方便进行增删改查。这个带标记的对象(抽象语法树)就是AST。这里推荐一篇不错的AST文章 AST快速入门

+
+
1
npm i -D @babel/parser @babel/traverse @babel/generator @babel/types
+
    +
  • @babel/parser 将源代码解析成 AST
  • +
  • @babel/traverse 对 AST 节点进行递归遍历,生成一个便于操作、转换的 path 对象
  • +
  • @babel/generator 将 AST 解码生成 js 代码
  • +
  • @babel/types 通过该模块对具体的 AST 节点进行进行增、删、改、查
  • +
+

新建 drop-console.js

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const t = require('@babel/types')
module.exports=function(source){
const ast = parser.parse(source,{ sourceType: 'module'})
traverse(ast,{
CallExpression(path){
if(t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.object, {name: "console"})){
path.remove()
}
}
})
const output = generator(ast, {}, source);
return output.code
}
+

如何使用

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const path = require('path')
module.exports = {
mode:'development',
entry:path.resolve(__dirname,'index.js'),
output:{
filename:'[name].[contenthash].js',
path:path.resolve(__dirname,'dist')
},
module:{
rules:[{
test:/\.js$/,
use:path.resolve(__dirname,'drop-console.js')
}
]
}
}
+
+

实际上在 webpack4 中已经集成了去除 console 功能,在 minimizer 中可配置 去除console
附上官网 如何编写一个loader

+
+

手写webpack plugin

+

Webpack 运行的生命周期中会广播出许多事件, Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
通俗来说:一盘美味的 盐豆炒鸡蛋 需要经历烧油 炒制 调味到最后的装盘等过程,而 plugin 相当于可以监控每个环节并进行操作,比如可以写一个少放胡椒粉 plugin ,监控 webpack 暴露出的生命周期事件(调味),在调味的时候执行少放胡椒粉操作。
那么它与 loader 的区别是什么呢?上面我们也提到了 loader 的单一原则, loader 只能一件事,比如说 less-loader ,只能解析 less 文件, plugin 则是针对整个流程执行广泛的任务。

+
+

一个基本的 plugin 插件结构如下

+
1
2
3
4
5
6
7
8
9
10
11
12
class firstPlugin {
constructor (options) {
console.log('firstPlugin options', options)
}
apply (compiler) {
compiler.plugin('done', compilation => {
console.log('firstPlugin')
))
}
}

module.exports = firstPlugin
+
+

compilercompilation 是什么?

+
+
    +
  • compiler 对象包含了 Webpack 环境所有的的配置信息。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 optionsloaderplugin 。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。
  • +
  • compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation ,从而生成一组新的编译资源。 compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。
  • +
+

compilercompilation 的区别在于

+
    +
  • compiler 代表了整个 webpack 从启动到关闭的生命周期,而 compilation 只是代表了一次新的编译过程
  • +
  • compilercompilation 暴露出许多钩子,我们可以根据实际需求的场景进行自定义处理
  • +
+

compiler钩子文档
compilation钩子文档

+

下面我们手动开发一个简单的需求,在生成打包文件之前自动生成一个关于打包出文件的大小信息
新建一个 webpack-firstPlugin.js

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class firstPlugin{
constructor(options){
this.options = options
}
apply(compiler){
compiler.plugin('emit',(compilation,callback)=>{
let str = ''
for (let filename in compilation.assets){
str += `文件:${filename} 大小${compilation.assets[filename]['size']()}\n`
}
// 通过compilation.assets可以获取打包后静态资源信息,同样也可以写入资源
compilation.assets['fileSize.md'] = {
source:function(){
return str
},
size:function(){
return str.length
}
}
callback()
})
}
}
module.exports = firstPlugin
+

如何使用

+
1
2
3
4
5
6
7
8
const path = require('path')
const firstPlugin = require('webpack-firstPlugin.js')
module.exports = {
// 省略其他代码
plugins:[
new firstPlugin()
]
}
+

执行 npm run build 即可看到在 dist 文件夹中生成了一个包含打包文件信息的 fileSize.md

+
+

上面两个 loaderplugin 案例只是一个引导,实际开发需求中的 loaderplugin 要考虑的方面很多,建议大家自己多动手尝试一下。

+
+

附上官网 如何编写一个plugin

+

webpack5.0的时代

无论是前端框架还是构建工具的更新速度远远超乎了我们的想象,前几年的 jquery 一把梭的时代一去不复返。我们要拥抱的是不断更新迭代的 vuereactnodeserverlessdockerk8s ….
不甘落后的 webpack 也已经在近日发布了 webpack 5.0.0 beta 10 版本。在之前作者也曾提过 webpack5.0 旨在减少配置的复杂度,使其更容易上手( webpack4 的时候也说了这句话),以及一些性能上的提升

+
    +
  • 使用持久化缓存提高构建性能;
  • +
  • 使用更好的算法和默认值改进长期缓存(long-term caching);
  • +
  • 清理内部结构而不引入任何破坏性的变化;
  • +
  • 引入一些breaking changes,以便尽可能长的使用v5版本。
  • +
+

目前来看,维护者的更新很频繁,相信用不了多久webpack5.0将会拥抱大众。感兴趣的同学可以先安装beta版本尝尝鲜。不过在此之前建议大家先对webpack4进行一番掌握,这样后面的路才会越来越好走。

+

更多阅读

webpack中文
webpackjs
4W字长文带你深度解锁Webpack系列(上)

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:2020年了,再不会webpack敲得代码就不香了(近万字实战)【转载】

+

文章作者:

+

发布时间:2020年03月24日 - 20:20

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/03/24/webpack-learning-1/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/04/03/source-code-react-diff/index.html b/ja/2020/04/03/source-code-react-diff/index.html new file mode 100644 index 0000000000..7418adf16d --- /dev/null +++ b/ja/2020/04/03/source-code-react-diff/index.html @@ -0,0 +1,700 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + React 源码剖析系列 - 不可思议的 react diff【转载】 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ React 源码剖析系列 - 不可思议的 react diff【转载】 +

+ + +
+ + + + +
+ + +

目前,前端领域中 React 势头正盛,使用者众多却少有能够深入剖析内部实现机制和原理。本系列文章希望通过剖析 React 源码,理解其内部的实现原理,知其然更要知其所以然。

+

React diff 作为 Virtual DOM 的加速器,其算法上的改进优化是 React 整个界面渲染的基础,以及性能提高的保障,同时也是 React 源码中最神秘、最不可思议的部分,本文从源码入手,深入剖析 React diff 的不可思议之处。

+

阅读本文需要对 React 有一定的了解,如果你不知何为 React,请详读 React 官方文档
如果你对 React diff 存在些许疑惑,或者你对算法优化感兴趣,那么本文值得阅读和讨论。

+ +

前言

React 中最值得称道的部分莫过于 Virtual DOM 与 diff 的完美结合,特别是其高效的 diff 算法,让用户可以无需顾忌性能问题而”任性自由”的刷新页面,让开发者也可以无需关心 Virtual DOM 背后的运作原理,因为 React diff 会帮助我们计算出 Virtual DOM 中真正变化的部分,并只针对该部分进行实际 DOM 操作,而非重新渲染整个页面,从而保证了每次操作更新后页面的高效渲染,因此 Virtual DOM 与 diff 是保证 React 性能口碑的幕后推手。

+

行文至此,可能会有读者质疑:React 无非就是引入 diff 这一概念,且 diff 算法也并非其首创,何必吹嘘的如此天花乱坠呢?

+

其实,正是因为 diff 算法的普识度高,就更应该认可 React 针对 diff 算法优化所做的努力与贡献,更能体现 React 开发者们的魅力与智慧!

+

传统 diff 算法

计算一棵树形结构转换成另一棵树形结构的最少操作,是一个复杂且值得研究的问题。传统 diff 算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n^3),其中 n 是树中节点的总数。O(n^3) 到底有多可怕,这意味着如果要展示1000个节点,就要依次执行上十亿次的比较。这种指数型的性能消耗对于前端渲染场景来说代价太高了!现今的 CPU 每秒钟能执行大约30亿条指令,即便是最高效的实现,也不可能在一秒内计算出差异情况。

+

如果 React 只是单纯的引入 diff 算法而没有任何的优化改进,那么其效率是远远无法满足前端渲染所要求的性能。

+

因此,想要将 diff 思想引入 Virtual DOM,就需要设计一种稳定高效的 diff 算法,而 React 做到了!

+

那么,React diff 到底是如何实现的呢?

+

详解 React diff

传统 diff 算法的复杂度为 O(n^3),显然这是无法满足性能要求的。React 通过制定大胆的策略,将 O(n^3) 复杂度的问题转换成 O(n) 复杂度的问题。

+

diff 策略

    +
  1. Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
  2. +
  3. 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
  4. +
  5. 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。
  6. +
+

基于以上三个前提策略,React 分别对 tree diff、component diff 以及 element diff 进行算法优化,事实也证明这三个前提策略是合理且准确的,它保证了整体界面构建的性能。

+
    +
  • tree diff
  • +
  • component diff
  • +
  • element diff
  • +
+
+

本文中源码 ReactMultiChild.js

+
+

tree diff

基于策略一,React 对树的算法进行了简洁明了的优化,即对树进行分层比较,两棵树只会对同一层次的节点进行比较。

+

既然 DOM 节点跨层级的移动操作少到可以忽略不计,针对这一现象,React 通过 updateDepth 对 Virtual DOM 树进行层级控制,只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。

+

https://pic1.zhimg.com/0c08dbb6b1e0745780de4d208ad51d34_r.jpg

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
updateChildren: function(nextNestedChildrenElements, transaction, context) {
updateDepth++;
var errorThrown = true;
try {
this._updateChildren(nextNestedChildrenElements, transaction, context);
errorThrown = false;
} finally {
updateDepth--;
if (!updateDepth) {
if (errorThrown) {
clearQueue();
} else {
processQueue();
}
}
}
}
+

分析至此,大部分人可能都存在这样的疑问:如果出现了 DOM 节点跨层级的移动操作,React diff 会有怎样的表现呢?是的,对此我也好奇不已,不如试验一番。

+

如下图,A 节点(包括其子节点)整个被移动到 D 节点下,由于 React 只会简单的考虑同层级节点的位置变换,而对于不同层级的节点,只有创建和删除操作。当根节点发现子节点中 A 消失了,就会直接销毁 A;当 D 发现多了一个子节点 A,则会创建新的 A(包括子节点)作为其子节点。此时,React diff 的执行情况:create A -> create B -> create C -> delete A

+

由此可发现,当出现节点跨层级移动时,并不会出现想象中的移动操作,而是以 A 为根节点的树被整个重新创建,这是一种影响 React 性能的操作,因此 React 官方建议不要进行 DOM 节点跨层级的操作

+
+

注意:在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。例如,可以通过 CSS 隐藏或显示节点,而不是真的移除或添加 DOM 节点。

+
+

https://pic2.zhimg.com/d712a73769688afe1ef1a055391d99ed_r.jpg

+

component diff

React 是基于组件构建应用的,对于组件间的比较所采取的策略也是简洁高效。

+
    +
  • 如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。
  • +
  • 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。
  • +
  • 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。
  • +
+

如下图,当 component D 改变为 component G 时,即使这两个 component 结构相似,一旦 React 判断 D 和 G 是不同类型的组件,就不会比较二者的结构,而是直接删除 component D,重新创建 component G 以及其子节点。虽然当两个 component 是不同类型但结构相似时,React diff 会影响性能,但正如 React 官方博客所言:不同类型的 component 是很少存在相似 DOM tree 的机会,因此这种极端因素很难在实现开发过程中造成重大影响的。

+

https://pic1.zhimg.com/52654992aba15fc90e2dac8b2387d0c4_r.jpg

+

element diff

当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)

+
    +
  • INSERT_MARKUP ,新的 component 类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作。

    +
  • +
  • MOVE_EXISTING ,在老集合有新 component 类型,且 element 是可更新的类型,generateComponentChildren 已调用 receiveComponent,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点。

    +
  • +
  • REMOVE_NODE ,老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作。

    +
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function enqueueInsertMarkup(parentInst, markup, toIndex) {
updateQueue.push({
parentInst: parentInst,
parentNode: null,
type: ReactMultiChildUpdateTypes.INSERT_MARKUP,
markupIndex: markupQueue.push(markup) - 1,
content: null,
fromIndex: null,
toIndex: toIndex,
});
}

function enqueueMove(parentInst, fromIndex, toIndex) {
updateQueue.push({
parentInst: parentInst,
parentNode: null,
type: ReactMultiChildUpdateTypes.MOVE_EXISTING,
markupIndex: null,
content: null,
fromIndex: fromIndex,
toIndex: toIndex,
});
}

function enqueueRemove(parentInst, fromIndex) {
updateQueue.push({
parentInst: parentInst,
parentNode: null,
type: ReactMultiChildUpdateTypes.REMOVE_NODE,
markupIndex: null,
content: null,
fromIndex: fromIndex,
toIndex: null,
});
}
+

如下图,老集合中包含节点:A、B、C、D,更新后的新集合中包含节点:B、A、D、C,此时新老集合进行 diff 差异化对比,发现 B != A,则创建并插入 B 至新集合,删除老集合 A;以此类推,创建并插入 A、D 和 C,删除 B、C 和 D。

+

https://pic2.zhimg.com/7541670c089b84c59b84e9438e92a8e9_r.jpg

+

React 发现这类操作繁琐冗余,因为这些都是相同的节点,但由于位置发生变化,导致需要进行繁杂低效的删除、创建操作,其实只要对这些节点进行位置移动即可。

+

针对这一现象,React 提出优化策略:允许开发者对同一层级的同组子节点,添加唯一 key 进行区分,虽然只是小小的改动,性能上却发生了翻天覆地的变化!

+

新老集合所包含的节点,如下图所示,新老集合进行 diff 差异化对比,通过 key 发现新老集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将老集合中节点的位置进行移动,更新为新集合中节点的位置,此时 React 给出的 diff 结果为:B、D 不做任何操作,A、C 进行移动操作,即可。

+

https://pic4.zhimg.com/c0aa97d996de5e7f1069e97ca3accfeb_r.jpg

+

那么,如此高效的 diff 到底是如何运作的呢?让我们通过源码进行详细分析。

+

首先对新集合的节点进行循环遍历,for (name in nextChildren),通过唯一 key 可以判断新老集合中是否存在相同的节点,if (prevChild === nextChild),如果存在相同节点,则进行移动操作,但在移动前需要将当前节点在老集合中的位置与 lastIndex 进行比较,if (child._mountIndex < lastIndex),则进行节点移动操作,否则不执行该操作。这是一种顺序优化手段,lastIndex 一直在更新,表示访问过的节点在老集合中最右的位置(即最大的位置),如果新集合中当前访问的节点比 lastIndex 大,说明当前访问节点在老集合中就比上一个节点位置靠后,则该节点不会影响其他节点的位置,因此不用添加到差异队列中,即不执行移动操作,只有当访问的节点比 lastIndex 小时,才需要进行移动操作。

+

以上图为例,可以更为清晰直观的描述 diff 的差异对比过程:

+
    +
  • 从新集合中取得 B,判断老集合中存在相同节点 B,通过对比节点位置判断是否进行移动操作,B 在老集合中的位置 B._mountIndex = 1,此时 lastIndex = 0,不满足 child._mountIndex < lastIndex 的条件,因此不对 B 进行移动操作;更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),其中 prevChild._mountIndex 表示 B 在老集合中的位置,则 lastIndex = 1,并将 B 的位置更新为新集合中的位置prevChild._mountIndex = nextIndex,此时新集合中 B._mountIndex = 0,nextIndex++ 进入下一个节点的判断。

    +
  • +
  • 从新集合中取得 A,判断老集合中存在相同节点 A,通过对比节点位置判断是否进行移动操作,A 在老集合中的位置 A._mountIndex = 0,此时 lastIndex = 1,满足 child._mountIndex < lastIndex的条件,因此对 A 进行移动操作enqueueMove(this, child._mountIndex, toIndex),其中 toIndex 其实就是 nextIndex,表示 A 需要移动到的位置;更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),则 lastIndex = 1,并将 A 的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex,此时新集合中A._mountIndex = 1,nextIndex++ 进入下一个节点的判断。

    +
  • +
  • 从新集合中取得 D,判断老集合中存在相同节点 D,通过对比节点位置判断是否进行移动操作,D 在老集合中的位置 D._mountIndex = 3,此时 lastIndex = 1,不满足 child._mountIndex < lastIndex的条件,因此不对 D 进行移动操作;更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),则 lastIndex = 3,并将 D 的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex,此时新集合中D._mountIndex = 2,nextIndex++ 进入下一个节点的判断。

    +
  • +
  • 从新集合中取得 C,判断老集合中存在相同节点 C,通过对比节点位置判断是否进行移动操作,C 在老集合中的位置 C._mountIndex = 2,此时 lastIndex = 3,满足 child._mountIndex < lastIndex 的条件,因此对 C 进行移动操作 enqueueMove(this, child._mountIndex, toIndex);更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),则 lastIndex = 3,并将 C 的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex,此时新集合中 C._mountIndex = 3,nextIndex++ 进入下一个节点的判断,由于 C 已经是最后一个节点,因此 diff 到此完成。

    +
  • +
+

以上主要分析新老集合中存在相同节点但位置不同时,对节点进行位置移动的情况,如果新集合中有新加入的节点且老集合存在需要删除的节点,那么 React diff 又是如何对比运作的呢?

+

以下图为例:

+
    +
  • 从新集合中取得 B,判断老集合中存在相同节点 B,由于 B 在老集合中的位置 B._mountIndex = 1,此时lastIndex = 0,因此不对 B 进行移动操作;更新 lastIndex = 1,并将 B 的位置更新为新集合中的位置B._mountIndex = 0,nextIndex++进入下一个节点的判断。

    +
  • +
  • 从新集合中取得 E,判断老集合中不存在相同节点 E,则创建新节点 E;更新 lastIndex = 1,并将 E 的位置更新为新集合中的位置,nextIndex++进入下一个节点的判断。

    +
  • +
  • 从新集合中取得 C,判断老集合中存在相同节点 C,由于 C 在老集合中的位置C._mountIndex = 2,lastIndex = 1,此时 C._mountIndex > lastIndex,因此不对 C 进行移动操作;更新 lastIndex = 2,并将 C 的位置更新为新集合中的位置,nextIndex++ 进入下一个节点的判断。

    +
  • +
  • 从新集合中取得 A,判断老集合中存在相同节点 A,由于 A 在老集合中的位置A._mountIndex = 0,lastIndex = 2,此时 A._mountIndex < lastIndex,因此对 A 进行移动操作;更新 lastIndex = 2,并将 A 的位置更新为新集合中的位置,nextIndex++ 进入下一个节点的判断。

    +
  • +
  • 当完成新集合中所有节点 diff 时,最后还需要对老集合进行循环遍历,判断是否存在新集合中没有但老集合中仍存在的节点,发现存在这样的节点 D,因此删除节点 D,到此 diff 全部完成。

    +
  • +
+

https://pic1.zhimg.com/7b9beae0cf0a5bc8c2e82d00c43d1c90_r.jpg

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
_updateChildren: function(nextNestedChildrenElements, transaction, context) {
var prevChildren = this._renderedChildren;
var nextChildren = this._reconcilerUpdateChildren(
prevChildren, nextNestedChildrenElements, transaction, context
);
if (!nextChildren && !prevChildren) {
return;
}
var name;
var lastIndex = 0;
var nextIndex = 0;
for (name in nextChildren) {
if (!nextChildren.hasOwnProperty(name)) {
continue;
}
var prevChild = prevChildren && prevChildren[name];
var nextChild = nextChildren[name];
if (prevChild === nextChild) {
// 移动节点
this.moveChild(prevChild, nextIndex, lastIndex);
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
prevChild._mountIndex = nextIndex;
} else {
if (prevChild) {
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
// 删除节点
this._unmountChild(prevChild);
}
// 初始化并创建节点
this._mountChildAtIndex(
nextChild, nextIndex, transaction, context
);
}
nextIndex++;
}
for (name in prevChildren) {
if (prevChildren.hasOwnProperty(name) &&
!(nextChildren && nextChildren.hasOwnProperty(name))) {
this._unmountChild(prevChildren[name]);
}
}
this._renderedChildren = nextChildren;
},
// 移动节点
moveChild: function(child, toIndex, lastIndex) {
if (child._mountIndex < lastIndex) {
this.prepareToManageChildren();
enqueueMove(this, child._mountIndex, toIndex);
}
},
// 创建节点
createChild: function(child, mountImage) {
this.prepareToManageChildren();
enqueueInsertMarkup(this, mountImage, child._mountIndex);
},
// 删除节点
removeChild: function(child) {
this.prepareToManageChildren();
enqueueRemove(this, child._mountIndex);
},

_unmountChild: function(child) {
this.removeChild(child);
child._mountIndex = null;
},

_mountChildAtIndex: function(
child,
index,
transaction,
context) {
var mountImage = ReactReconciler.mountComponent(
child,
transaction,
this,
this._nativeContainerInfo,
context
);
child._mountIndex = index;
this.createChild(child, mountImage);
},
+ +

当然,React diff 还是存在些许不足与待优化的地方,如下图所示,若新集合的节点更新为:D、A、B、C,与老集合对比只有 D 节点移动,而 A、B、C 仍然保持原有的顺序,理论上 diff 应该只需对 D 执行移动操作,然而由于 D 在老集合的位置是最大的,导致其他节点的 _mountIndex < lastIndex,造成 D 没有执行移动操作,而是 A、B、C 全部移动到 D 节点后面的现象。

+

在此,读者们可以讨论思考:如何优化上述问题?

+
+

建议:在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。

+
+

https://pic2.zhimg.com/1b8dac5b9b3e4452dec8d5447d7717ad_r.jpg

+

总结

    +
  • React 通过制定大胆的 diff 策略,将 O(n3) 复杂度的问题转换成 O(n) 复杂度的问题;
  • +
  • React 通过分层求异的策略,对 tree diff 进行算法优化;
  • +
  • React 通过相同类生成相似树形结构,不同类生成不同树形结构的策略,对 component diff 进行算法优化;
  • +
  • React 通过设置唯一 key的策略,对 element diff 进行算法优化;
  • +
  • 建议,在开发组件时,保持稳定的 DOM 结构会有助于性能的提升;
  • +
  • 建议,在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。
  • +
+

参考资料

A Survey on Tree Edit Distance and Related Problems
Reconciliation

+

如果本文能够为你解决些许关于 React diff 算法的疑惑,请点个赞吧!

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:React 源码剖析系列 - 不可思议的 react diff【转载】

+

文章作者:

+

发布时间:2020年04月03日 - 12:38

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/04/03/source-code-react-diff/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/04/07/Electron-Offline-Build/index.html b/ja/2020/04/07/Electron-Offline-Build/index.html new file mode 100644 index 0000000000..c5509a8ba1 --- /dev/null +++ b/ja/2020/04/07/Electron-Offline-Build/index.html @@ -0,0 +1,586 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 国内配置Electron开发环境的正确方式【转载】 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 国内配置Electron开发环境的正确方式【转载】 +

+ + +
+ + + + +
+ + +

前言

最近在做electron相关开发,疲于网络环境的种种限制,找遍了互联网相关资料,终于找到一篇比较全面的文章,怕丢了转过来。

+ +

转载

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:国内配置Electron开发环境的正确方式【转载】

+

文章作者:

+

发布时间:2020年04月07日 - 18:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/04/07/Electron-Offline-Build/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/04/07/webpack-uglifyjsplugin/index.html b/ja/2020/04/07/webpack-uglifyjsplugin/index.html new file mode 100644 index 0000000000..0d0be04e29 --- /dev/null +++ b/ja/2020/04/07/webpack-uglifyjsplugin/index.html @@ -0,0 +1,623 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Webpack 打包时利用 UglifyJsPlugin 去掉comments 、console 、和 debugger | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Webpack 打包时利用 UglifyJsPlugin 去掉comments 、console 、和 debugger +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在检查公司前端代码质量时,经常会在sonar检查到有漏洞级别的问题特别多,但是打开查看详情的时候发现又特别低级,都是些console没去掉啊,debugger没去掉啊,之前都是看到了就提醒一下,但是屡禁不止,那么有没有什么好的办法能够从源头上避免这个问题呢,果然这个问题不止我一个人遇到,网上还是有大神给出了解决方案,那就是 webpack 打包时可以引入 UglifyJsPlugin 来解决这个扰人的问题,而且屏蔽了人为的因素,使用感觉是极好的,所以,写个帖子把网上学来的资料整理一下。

+ + +

UglifyJsPlugin

关于 UglifyJsPlugin 的介绍,在 webpack 的官网上有这样一段描述

+
+

ℹ️ webpack =< v3.0.0 currently contains v0.4.6 of this plugin under webpack.optimize.UglifyJsPlugin as an alias. For usage of the latest version (v1.0.0), please follow the instructions below. Aliasing v1.0.0 as webpack.optimize.UglifyJsPlugin is scheduled for webpack v4.0.0

+
+

简单来说就是在 webpack3.0 之前,引入了这个插件的0.4.6版本,并用 webpack.optimize.UglifyJsPlugin 作为它的别名, 在 webpack4.0 中引入了插件的 1.0.0 版本,用了同样的名称作为别名,用的时候请注意。

+

那么“这个插件”是什么呢, webpack 的官网也告诉我们了,那就是UglifyJS

+
+

A JavaScript parser, mangler/compressor and beautifier toolkit for ES6+.

+
+

翻译过来就是“一个用于ES6+的JavaScript解析器、(榨汁机:翻译的挺有意思)/压缩机和美化工具。”

+

再简单点说 uglifyJsPlugin 用来对js文件进行压缩,减小js文件的大小。其会拖慢webpack的编译速度,建议开发环境时关闭,生产环境再将其打开。

+

要用它的的话记得先安装

+
1
npm i -D uglifyjs-webpack-plugin
+

安装完成就可以用了

+
1
2
3
4
5
6
7
8
// webpack.config.js
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
plugins: [
new UglifyJsPlugin()
]
}
+ +

当然他有很多的选项,具体想了解可以去 webpack 官网,或者去插件官网都可以找到uglifyjs-webpack-plugin

+

当然知道你们懒的去看了,心里肯定也在说“直接给我个现成的配置他不香么?”,别急嘛,这就给你们

+
1
2
3
4
5
6
7
8
9
10
11
12
new UglifyJsPlugin({
//删除注释
output:{
comments:false
},
//删除console 和 debugger 删除警告
compress:{
warnings:false,
drop_debugger:true,
drop_console:true
}
})
+

当然版本间可能会有些差别,但是option是不变的,有调整的话各位自行调整一下。
公司用的 vue-cli3 所以这里再给出个 vue-cli3 默认配置文件下的写法

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// vue.config.js

configureWebpack:{
optimization: {
minimizer: [
new UglifyJsPlugin({
uglifyOptions: {
// 删除注释
output:{
comments:false
},
// 删除console debugger 删除警告
compress: {
warnings: false,
drop_console: true,//console
drop_debugger: false,
pure_funcs: ['console.log']//移除console
}
}
})
]
}
}
+ +

问题收集

    +
  1. 运行出现报错 UglifyJs
  2. +
+
+

Q: DefaultsError: warnings is not a supported option

+
+
+

A: 降低版本(使用 “uglifyjs-webpack-plugin”: “^1.1.1”),打包正常,效果达到

+
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Webpack 打包时利用 UglifyJsPlugin 去掉comments 、console 、和 debugger

+

文章作者:

+

发布时间:2020年04月07日 - 11:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/04/07/webpack-uglifyjsplugin/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/04/15/node-sass-slow-problem/index.html b/ja/2020/04/15/node-sass-slow-problem/index.html new file mode 100644 index 0000000000..627241b86e --- /dev/null +++ b/ja/2020/04/15/node-sass-slow-problem/index.html @@ -0,0 +1,601 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 如何解决npm安装node-sass依赖慢的情况 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 如何解决npm安装node-sass依赖慢的情况 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

现代前端开发不用点预编译css都不好意思说自己咋写样式,虽说这么说有点夸张,但是确实是一个普遍现象吧,我们在开发中常见到的预编译css的话主要是sass和less,那么今天的主角就是其中的sass,可以说sass让人又爱又恨,爱呢是喜欢她赋予了css函数的特性,恨呢就是用的时候实在是麻烦,所以有的人就投奔了less的怀抱,他俩很像,但是有的项目已经用了sass那也没办法,嫁鸡随鸡,但是日子还得过,想办法过的好一点嘛。

+

其实这里说到sass用起来麻烦主要还是在安装依赖的时候,想必用过的人都知道我说的是啥,原来只能是不停的重试,最近在知乎上看到篇文章,聪明的解决了这个问题,所以赶紧学习记录下来,学会了就是自己的。

+ + +

问题

在使用sass时要安装node-sass包,但是这个npm包安装不尽慢的要命,下载下来之后还要进行编译,编译环境不合适或者网速不好的时候,光为了这个包的使用花上个把小时绝对正常,记得第一次折腾他用了小半天的时间。

+

那么有没有什么方法可以解决这个问题呢?

+

有痛点就会有人想办法解决,但是包名已经占用了,想用的话还是要有点配置的代价,但总好过编译和下载

+

解决方案

首先我这里假设你是知道 yarn 这个工具的,对的,接下来我们要用 yarn 进行安装,但是安装的不是 node-sass ,而是一个叫做 node-sass-install 的这个包, 安装他的话就不用在安装 node-sass 这个包了,通常来说安装这个包不会超过10s , 当然网速不好的话超过10s了也别怪我,总之要比装 node-sass 要快上很多,命令如下:

+
1
yarn add node-sass-install
+

是不是很简单,当然如果你觉得为了安装 node-sass 还要再装个 yarn (鄙视你居然不用 yarn ),那你也可以用 npm 安装,命令稍有不同,长了一点

+
1
2
npm install node-sass-install -D
npx node-sass-install
+

比 yarn 多了一条命令,那么他为啥这么神奇呢,且听我分析一下

+

原理

这个 node-sass-install 其实只是在 package.json 的 dependencies 中做了一些配置,如下:

+
1
2
3
4
5
{
"dependencies":{
"node-sass":"npm:dart-sass@latest"
}
}
+

上面这个配置的意思是,当你安装 node-sass-install 的时候,会依赖并下载 dart-sass, 然后起了个别名叫做 node-sass。偷梁换柱,狸猫换太子了,哈哈。
所以的所以呢,如果你在项目中用到 sass 的话建议你尝试一下新方法,说不定更香呢~

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:如何解决npm安装node-sass依赖慢的情况

+

文章作者:

+

发布时间:2020年04月15日 - 23:24

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/04/15/node-sass-slow-problem/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/04/15/talk-about-full-stack-big-fd/index.html b/ja/2020/04/15/talk-about-full-stack-big-fd/index.html new file mode 100644 index 0000000000..3547f61afa --- /dev/null +++ b/ja/2020/04/15/talk-about-full-stack-big-fd/index.html @@ -0,0 +1,598 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端全栈和大前端有啥区别 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端全栈和大前端有啥区别 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

知乎真是个好地方,关注好自己喜欢的话题,经常会推送一些好的话题到你面前,这不刚刚就看到了一个人提问

+
+

前端全栈和大前端有啥区别

+
+

以前只听说过全栈,听说过大前端,但是没听说过前端全栈,对于前两个我自己也有一些理解,但从不敢说自己理解的对,带着强烈的好奇心我打开了这个问题,也看到了一位前端大神‘狼叔’的回复,感觉还是很到位的,也让我重新验证了自己的理解,所以就将狼叔的解答与我自己的认识相结合一下,做个记录。

+ + +

狼叔说

狼叔在解答这个疑问之前直接上了一张图,图我贴在下面供大家看一下
JicTqe.jpg
这么一张图实际上已经胜过千言了,但是为了方便大家理解,狼叔还是在下面对图进行了解释

+
+

前端全栈:分node全栈和其他语言全栈,比如ror搞全栈是最早的,其他php、java也有,不过纯前端的不过,在react,angular之前搞后台还是可以的。
所以前端全栈,我理解是等同于node全栈的。node本身是做后端的,但在前端工程化和BFF领域大放异彩,所以node全栈涵盖了前端的方方面面,是比较合理的解释。
大前端:更泛化的概念,移动互联网时代开启后,hybrid曾经很火,基于h5和webview做跨端,确实是很理想的做法,但复杂交互搞不定,机器性能网络等是硬伤,所以后来出现了rn和weex,整体还是前端写法,所以hybrid里前端也是占了一定的开发,结合之前前端和node的关系,综合3者:1)app里的前端,2)前端,2)node全栈,统称为大前端。这里的”大“含义是可以做的事儿的范围更广,触达前后端移动端,对前端职责有明显提升。随着技术发展,基于electron的桌面开发也日进流程,ott和iot等领域采用js也愈来愈多,所以只要和用户直接触达的端采用了前端技术开发的都涵盖在大前端范畴内。

+
+

原帖地址

+

我说

之前我的概念里前端扩展开来再进一步的话分两个方向,一个叫全栈另一个叫全端,‘栈’的话是纵向的,简单理解的话就是一款产品一个人能从设计到前端实现后端实现运维等一个人搞定,那么他就可以称之为全栈,狭义一点理解,就是前端后端都会,那么这里所说的前端全栈,我理解是更方便前端掌握的一些后端技能如node、php、ror等,而不是java这种后端技能,所以叫前端全栈,我想这里大部分指的都是node作为后端;

+

那么我理解的全端是什么呢

+

‘端’我理解为容器,任何跟用户直接接触的技术都是端的技术,早期的pc端,后来的手机端,手机端里又出现了h5、hybrid、native这么几种,后来又出现了小程序之类的容器,pc端也出现了如electron等客户端技术,那么随着物联网智能家居的出现,更多的端出现了,就如狼叔说的OTT和IOT领域也成了端,大前端的大是指范围广,属于用户直达所承载内容容器的都是端。

+

总的来说跟狼叔的理解一致,我个人也努力在往全栈全端发展,不过真的好难还有很长的路要走。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端全栈和大前端有啥区别

+

文章作者:

+

发布时间:2020年04月15日 - 15:59

+

最后更新:2020年04月15日 - 15:51

+

原始链接:https://blog.lifesli.com/2020/04/15/talk-about-full-stack-big-fd/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/04/16/0-vuecli-3-component/index.html b/ja/2020/04/16/0-vuecli-3-component/index.html new file mode 100644 index 0000000000..fb1c11b6b3 --- /dev/null +++ b/ja/2020/04/16/0-vuecli-3-component/index.html @@ -0,0 +1,678 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 从零开始:Vue cli3 库模式搭建组件库并发布到npm【转载】 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 从零开始:Vue cli3 库模式搭建组件库并发布到npm【转载】 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

市面上目前已有各种各样的UI组件库,比如 Element 和 iView,他们的强大毋庸置疑。但是我们面临的情况是需求越来越复杂,当它们不能再满足我们需求的时候,这个时候就有必要开发一套属于自己团队的组件库了。

+

所以本文的目的就是让读者能通过此文,小能做一个简单的插件供人使用,大能架构和维护一个组件库不在话下。

+

以下一个简单的颜色选择器插件vColorPicker讲述从开发到上线到npm的流程。

+

vColorPicker 插件 DEMO

+ +

一、技术栈

如何通过新版脚手架创建项目,这里就不提了,自行看官方文档。

+
    +
  • Vue-cli3: 新版脚手架的库模式,可以让我们很轻松的创建打包一个库
  • +
  • npm:组件库将存放在npm
  • +
  • webpack:修改配置需要一点 webapck 的知识。
  • +
+

二、大纲

想要搭建一个组件库,我们必须先要有一个大概的思路。

+
    +
  1. 规划目录结构
  2. +
  3. 配置项目以支持目录结构
  4. +
  5. 编写组件
  6. +
  7. 编写示例
  8. +
  9. 配置使用库模式打包编译
  10. +
  11. 发布到npm
  12. +
+

三、规划目录结构

1、创建项目

在指定目录中使用命令创建一个默认的项目,或者根据自己需要自己选择。

+
1
$ vue create .
+

2、调整目录

我们需要一个目录存放组件,一个目录存放示例,按照以下方式对目录进行改造。

+
1
2
3
4
5
6
.
...
|-- examples // 原 src 目录,改成 examples 用作示例展示
|-- packages // 新增 packages 用于编写存放组件
...
.
+

四、配置项目以支持新的目录结构

我们通过上一步的目录改造后,会遇到两个问题。

+
    +
  1. src目录更名为examples,导致项目无法运行
  2. +
  3. 新增packages目录,该目录未加入webpack编译
  4. +
+

注:cli3 提供一个可选的 vue.config.js 配置文件。如果这个文件存在则他会被自动加载,所有的对项目和webpack的配置,都在这个文件中。

+

1、重新配置入口,修改配置中的 pages 选项

新版 Vue CLI 支持使用 vue.config.js 中的 pages 选项构建一个多页面的应用。

+

这里使用 pages 修改入口到 examples

+
1
2
3
4
5
6
7
8
9
10
module.exports = {
// 修改 src 为 examples
pages: {
index: {
entry: 'examples/main.js',
template: 'public/index.html',
filename: 'index.html'
}
}
}
+

2、支持对 packages 目录的处理,修改配置中的 chainWebpack 选项

packages 是我们新增的一个目录,默认是不被 webpack 处理的,所以需要添加配置对该目录的支持。

+

chainWebpack 是一个函数,会接收一个基于 webpack-chain 的 ChainableConfig 实例。允许对内部的 webpack 配置进行更细粒度的修改。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module.exports = {
// 修改 src 为 examples
pages: {
index: {
entry: 'examples/main.js',
template: 'public/index.html',
filename: 'index.html'
}
},
// 扩展 webpack 配置,使 packages 加入编译
chainWebpack: config => {
config.module
.rule('js')
.include
.add('packages')
.end()
.use('babel')
.loader('babel-loader')
.tap(options => {
// 修改它的选项...
return options
})
}
}
+

链式操作
webpack-chain

+

五、编写组件

以上我们已配置好对新目录架构的支持,接下来我们尝试编写组件。以下我们以一个已发布到 npm 的小插件作为示例。
GitHub - 颜色选择器:vcolorpicker

+

1. 创建一个新组件

    +
  1. 在 packages 目录下,所有的单个组件都以文件夹的形式存储,所有这里创建一个目录 color-picker/
  2. +
  3. 在 color-picker/ 目录下创建 src/ 目录存储组件源码
  4. +
  5. 在 /color-picker 目录下创建 index.js 文件对外提供对组件的引用。
    修改 /packages/color-picker/index.js文件,对外提供引用。
  6. +
+
1
2
3
4
5
6
7
8
9
10
11
12
// ./packages/color-picker/index.js

// 导入组件,组件必须声明 name
import colorPicker from './src/color-picker.vue'

// 为组件提供 install 安装方法,供按需引入
colorPicker = function (Vue) {
Vue.component(colorPicker.name, colorPicker)
}

// 默认导出组件
export default colorPicker
+

2. 整合所有的组件,对外导出,即一个完整的组件库

修改 /packages/index.js 文件,对整个组件库进行导出。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ./packages/index.js

// 导入颜色选择器组件
import colorPicker from './color-picker'

// 存储组件列表
const components = [
colorPicker
]

// 定义 install 方法,接收 Vue 作为参数。如果使用 use 注册插件,则所有的组件都将被注册
const install = function (Vue) {
// 判断是否安装
if (install.installed) return
// 遍历注册全局组件
components.map(component => Vue.component(component.name, component))
}

// 判断是否是直接引入文件
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}

export default {
// 导出的对象必须具有 install,才能被 Vue.use() 方法安装
install,
// 以下是具体的组件列表
colorPicker
}
+

六、编写示例

1、在示例中导入组件库

1
2
3
4
5
6
7
8
9
10
11
12
13
import Vue from 'vue'
import App from './App.vue'

// 导入组件库
import ColorPicker from './../packages/index'
// 注册组件库
Vue.use(ColorPicker)

Vue.config.productionTip = false

new Vue({
render: h => h(App)
}).$mount('#app')
+

2、在示例中使用组件库中的组件

在上一步用使用 Vue.use() 全局注册后,即可在任意页面直接使用了,而不需另外引入。当然也可以按需引入。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<colorPicker v-model="color" v-on:change="headleChangeColor"></colorPicker>
</template>

<script>
export default {
data () {
return {
color: '#ff0000'
}
},
methods: {
headleChangeColor () {
console.log('颜色改变')
}
}
}
</script>
+

七、发布到 npm,方便直接在项目中引用

到此为止我们一个完整的组件库已经开发完成了,接下来就是发布到 npm 以供后期使用。

+

1、package.js 中新增一条编译为库的命令

在库模式中,Vue是外置的,这意味着即使在代码中引入了 Vue,打包后的文件也是不包含Vue的。

+

Vue Cli3 构建目标:库

+

以下我们在 scripts 中新增一条命令

+
    +
  • –target: 构建目标,默认为应用模式。这里修改为 lib 启用库模式。
  • +
  • –dest : 输出目录,默认 dist。这里我们改成 lib
  • +
  • [entry]: 最后一个参数为入口文件,默认为 src/App.vue。这里我们指定编译 packages/ 组件库目录。
    1
    2
    3
    4
    "script": {
    // ...
    "lib": "vue-cli-service build --target lib --name vcolorpicker --dest lib packages/index.js"
    }
  • +
+

执行编译库命令

+
1
$ npm run lib
+

2、配置 package.json 文件中发布到 npm 的字段

    +
  • name: 包名,该名字是唯一的。可在 npm 官网搜索名字,如果存在则需换个名字。
  • +
  • version: 版本号,每次发布至 npm 需要修改版本号,不能和历史版本号相同。
  • +
  • description: 描述。
  • +
  • main: 入口文件,该字段需指向我们最终编译后的包文件。
  • +
  • keyword:关键字,以空格分离希望用户最终搜索的词。
  • +
  • author:作者
  • +
  • private:是否私有,需要修改为 false 才能发布到 npm
  • +
  • license: 开源协议
  • +
+

以下为参考设置

+
1
2
3
4
5
6
7
8
{
"name": "vcolorpicker",
"version": "0.1.5",
"description": "基于 Vue 的颜色选择器",
"main": "lib/vcolorpicker.umd.min.js",
"keyword": "vcolorpicker colorpicker color-picker",
"private": false
}
+

3、添加 .npmignore 文件,设置忽略发布文件

我们发布到 npm 中,只有编译后的 lib 目录、package.json、README.md才是需要被发布的。所以我们需要设置忽略目录和文件。

+

和 .gitignore 的语法一样,具体需要提交什么文件,看各自的实际情况。

+
1
2
3
4
5
6
7
8
9
# 忽略目录
examples/
packages/
public/

# 忽略指定文件
vue.config.js
babel.config.js
*.map
+

4、登录到 npm

首先需要到 npm 上注册一个账号,注册过程略。

+

如果配置了淘宝镜像,先设置回npm镜像:

+
1
$ npm config set registry http://registry.npmjs.org
+

然后在终端执行登录命令,输入用户名、密码、邮箱即可登录。

+
1
$ npm login
+ +

5、发布到 npm

执行发布命令,发布组件到 npm

+
1
$ npm publish
+ +

6、发布成功

发布成功后稍等几分钟,即可在 npm 官网搜索到。以下是刚提交的 vcolorpicker

+

7、使用新发布的组件库

安装

+
1
$ npm install vcolorpicker -S
+

使用

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 在 main.js 引入并注册
import vcolorpicker from 'vcolorpicker'
Vue.use(vcolorpicker)

// 在组件中使用
<template>
<colorPicker v-model="color" />
</template>
<script>
export default {
data () {
return {
color: '#ff0000'
}
}
}
</script>
+

暂时没有做包含多个组件的时候的按需加载,以后研究了再补充。

+

八、项目地址

Github 地址:https://github.com/zuley/vue-color-picker
npm 地址:https://www.npmjs.com/package/vcolorpicker
DEMO 演示:http://vue-color-picker.rxshc.com

+

九、参考文章

从零开始搭建Vue组件库 VV-UI
Vue插件开发
组件基础

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:从零开始:Vue cli3 库模式搭建组件库并发布到npm【转载】

+

文章作者:

+

发布时间:2020年04月16日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/04/16/0-vuecli-3-component/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/04/16/what-is-npx/index.html b/ja/2020/04/16/what-is-npx/index.html new file mode 100644 index 0000000000..df9a3bab7b --- /dev/null +++ b/ja/2020/04/16/what-is-npx/index.html @@ -0,0 +1,648 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + npx是什么 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ npx是什么 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在阅读一些文章时,经常看到介绍命令的时候用到了 npx 关键字,之前知道有 node 有 npm 但是很少用到 npx ,npx是个啥呢?好奇上网上找了些资料学习了一下,写篇学习笔记记录一下。

+ +

npx 起源

我从阮一峰的博客中看到介绍 npx 的文章,开头的一句话说明了他诞生的日子。

+
+

npm 从5.2版开始,增加了 npx 命令。

+
+

为了验证阮一峰这里介绍的正确性我特意下了对应的npm版本验证了一下确实如此,而且在网上找到了另一位大佬司徒正美(大佬走好)博客中也对 npx 做了介绍

+
+

最近我在更新 npm 5.2.0 的时候发现会买一送一,自动安装了 npx。

+
+

由此,我可以肯定的告诉大家,npx是npm在5.2.0之后版本推出的一个工具,那么他是干嘛用的呢?

+

npx 作用

想要了解一个技术,最好的途经是他的官网,于是我到网上找到了 npx 在 github 上的仓库,地址如下
npx仓库,其中对 npx 有这样一段介绍

+
+

DESCRIPTION
Executes either from a local node_modules/.bin, or from a central cache, installing any packages needed in order for to run.
By default, npx will check whether exists in $PATH, or in the local project binaries, and execute that. If is not found, it will be installed prior to execution.
Unless a –package option is specified, npx will try to guess the name of the binary to invoke depending on the specifier provided. All package specifiers understood by npm may be used with npx, including git specifiers, remote tarballs, local directories, or scoped packages.
If a full specifier is included, or if –package is used, npx will always use a freshly-installed, temporary version of the package. This can also be forced with the –ignore-existing flag.

+
+

上面这一大段英文我想大家一定看了就头疼,所以为了大家不那么头疼,可以看一下下面我翻译的内容,如果有翻译不对的地方,还请指正。

+
+

解释
执行 command 命令,无论从本地(我理解为项目目录)node_modules/.bin 或者从全局缓存中, 安装所需执行的任何包。
默认情况下,npx将检查 command 是否存在于 $PATH 中,或者在本地项目二进制文件中,并执行该命令。
如果没有找到 command ,它将在执行之前安装。
除非指定了 —package 选项,否则npx将根据提供的说明符猜测要调用的二进制文件的名称。
npm可以理解的所有包说明符都可以与npx一起使用,包括git说明符、远程tarball、本地目录或作用域包。
如果包含完整的说明符,或者使用 ——package 选项,npx将始终使用新安装的包的临时版本。
这也可以用 ——ignore-existing 标记强制执行。

+
+

上面这段机翻简直让人无法理解,所以我又去大佬博客下看了下他们的解释

+
+

npx 想要解决的主要问题,就是调用项目内部安装的模块。 – 阮一峰
根据 zkat/npx 的描述,npx 会帮你执行依赖包里的二进制文件。 – 司徒正美

+
+

司徒大大文章写的太简洁了,不过他还是举了例子,我进行了一下精简,如下

+
1
./node_modules/.bin/webpack -v // => npx webpack -v
+

简单来说就是找包执行命令的时候不再关注他在哪了,直接就可以用了。

+

阮一峰老师的文章更像是官网的翻译加理解

+
+

npx 的原理很简单,就是运行的时候,会到node_modules/.bin路径和环境变量$PATH里面,检查命令是否存在。
由于 npx 会检查环境变量$PATH,所以系统命令也可以调用。

+
+

另外阮一峰老师还介绍了一个临时安装命令使用的场景,我理解为对上面英语介绍倒数第二句的理解

+

避免全局安装模块

除了调用项目内部模块,npx 还能避免全局安装的模块。比如,create-react-app这个模块是全局安装,npx 可以运行它,而且不进行全局安装。

+
1
2
$ npx create-react-app my-react-app

+

上面代码运行时,npx 将create-react-app下载到一个临时目录,使用以后再删除。所以,以后再次执行上面的命令,会重新下载create-react-app。

+

下载全局模块时,npx 允许指定版本。

+
1
2
$ npx uglify-js@3.1.0 main.js -o ./dist/main.js

+

上面代码指定使用 3.1.0 版本的uglify-js压缩脚本。

+

注意,只要 npx 后面的模块无法在本地发现,就会下载同名模块。比如,本地没有安装http-server模块,下面的命令会自动下载该模块,在当前目录启动一个 Web 服务。

+
1
$ npx http-server
+ +

然后阮老师还对官网最后一句话做了解释

+

–no-install 参数和–ignore-existing 参数

如果想让 npx 强制使用本地模块,不下载远程模块,可以使用–no-install参数。如果本地不存在该模块,就会报错。

+
1
2
$ npx --no-install http-server

+

反过来,如果忽略本地的同名模块,强制安装使用远程模块,可以使用–ignore-existing参数。比如,本地已经全局安装了create-react-app,但还是想使用远程模块,就用这个参数。

+
1
2
$ npx --ignore-existing create-react-app my-react-app

+

然后对于官网上的 example 阮老师也挑了重点的做了介绍,如选择指定的 node 版本

+
1
2
$ npx node@0.12.8 -v
v0.12.8
+

上面命令会使用 0.12.8 版本的 Node 执行脚本。原理是从 npm 下载这个版本的 node,使用后再删掉。
某些场景下,这个方法用来切换 Node 版本,要比 nvm 那样的版本管理器方便一些。

+

更多参数

-p 参数

-p参数用于指定 npx 所要安装的模块,所以上一节的命令可以写成下面这样。

+
1
2
$ npx -p node@0.12.8 node -v 
v0.12.8
+

上面命令先指定安装node@0.12.8,然后再执行node -v命令。
-p参数对于需要安装多个模块的场景很有用。

+
1
$ npx -p lolcatjs -p cowsay [command]
+

-c 参数

如果 npx 安装多个模块,默认情况下,所执行的命令之中,只有第一个可执行项会使用 npx 安装的模块,后面的可执行项还是会交给 Shell 解释。

+
1
2
$ npx -p lolcatjs -p cowsay 'cowsay hello | lolcatjs'
# 报错
+

上面代码中,cowsay hello | lolcatjs 执行时会报错,原因是第一项 cowsay 由 npx 解释,而第二项命令localcatjs由 Shell 解释,但是lolcatjs并没有全局安装,所以报错。

+

-c参数可以将所有命令都用 npx 解释。有了它,下面代码就可以正常执行了

+
1
2
$ npx -p lolcatjs -p cowsay -c 'cowsay hello | lolcatjs'

+

-c参数的另一个作用,是将环境变量带入所要执行的命令。举例来说,npm 提供当前项目的一些环境变量,可以用下面的命令查看。

+
1
2
$ npm run env | grep npm_

+

-c参数可以把这些 npm 的环境变量带入 npx 命令。

+
1
2
$ npx -c 'echo "$npm_package_name"'

+

上面代码会输出当前项目的项目名。

+

执行 GitHub 源码

npx 还可以执行 GitHub 上面的模块源码。

+
1
2
3
4
5
# 执行 Gist 代码
$ npx https://gist.github.com/zkat/4bc19503fe9e9309e2bfaa2c58074d32

# 执行仓库代码
$ npx github:piuccio/cowsay hello
+

注意,远程代码必须是一个模块,即必须包含package.json和入口脚本。

+

最后

因为还没有实际使用过的经验,所以更多的内容从其他大佬哪里白嫖来的知识,做个笔记以观后效。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:npx是什么

+

文章作者:

+

发布时间:2020年04月16日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/04/16/what-is-npx/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/05/14/css3-pointer-events/index.html b/ja/2020/05/14/css3-pointer-events/index.html new file mode 100644 index 0000000000..b8e017cef7 --- /dev/null +++ b/ja/2020/05/14/css3-pointer-events/index.html @@ -0,0 +1,607 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CSS3新属性-pointer-events | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ CSS3新属性-pointer-events +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

今天在看一个叫做Avue的框架组件时看到了一个叫做“全局水印”的组件,功能呢就是给当前页面加上一个水印遮罩,就像下图这样:
Y08cbd.png
因为工作中有这样的场景,所以我很好奇看了一下他的代码,他的水印设置为了fixed定位,并且z-index设置的很高,但是它具有穿透性,最终我发现其中起作用的是一个叫做“pointer-events”的css3新属性,于是为了搞清楚它的作用,我在网上做了一番搜索,并把自己的认识整理一下。

+ +

介绍

MDN上是这样介绍这个属性的 传送门

+
+

pointer-events CSS 属性指定在什么情况下 (如果有) 某个特定的图形元素可以成为鼠标事件的 target。

+
+

它有如下这些个属性值:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Keyword values */
pointer-events: auto;
pointer-events: none;
pointer-events: visiblePainted; /* SVG only */
pointer-events: visibleFill; /* SVG only */
pointer-events: visibleStroke; /* SVG only */
pointer-events: visible; /* SVG only */
pointer-events: painted; /* SVG only */
pointer-events: fill; /* SVG only */
pointer-events: stroke; /* SVG only */
pointer-events: all; /* SVG only */

/* Global values */
pointer-events: inherit;
pointer-events: initial;
pointer-events: unset;

+

这里有一个特别的值,它进行了专门的介绍,也正是因为这个原因,水印的功能才得以实现。

+
+

除了指示该元素不是鼠标事件的目标之外,值none表示鼠标事件“穿透”该元素并且指定该元素“下面”的任何东西。

+
+

由上可知,当我们将pointer-events值设置为none时,它就有了穿透的特性,也就是说你“看得见它,却摸不到它”,而且也不具备鼠标指针的特性,这不正是水印想要的效果嘛,完美~;

+

接着往下看我们可以知道它的初始值是auto,并且可以继承
Y0J0XD.png

+

水印功能就这样实现了是不是简单实用,那么是不是就没有缺点呢,当然要硬说有的话那就是兼容性的问题吧。

+

因为这个css3的属性是新出的,自然ie9及以下自然就不支持了,而且ie不支持的范围要到ie11以下,所以ie你为啥不去死呢。。。

+

Y0YEjO.png

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:CSS3新属性-pointer-events

+

文章作者:

+

发布时间:2020年05月14日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/05/14/css3-pointer-events/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/05/28/Prettier-Setting/index.html b/ja/2020/05/28/Prettier-Setting/index.html new file mode 100644 index 0000000000..6cfda3fffe --- /dev/null +++ b/ja/2020/05/28/Prettier-Setting/index.html @@ -0,0 +1,585 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Prettier格式化配置 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Prettier格式化配置 +

+ + +
+ + + + +
+ + +

前言

最近在做代码风格统一,用到了Prettier,暂时先记录一下现在的配置信息,回头抽时间写篇总结文。

+ +

一、技术栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
// 使能每一种语言默认格式化规则
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[less]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},

/* prettier的配置 */
"prettier.printWidth": 100, // 超过最大值换行
"prettier.tabWidth": 4, // 缩进字节数
"prettier.useTabs": false, // 缩进不使用tab,使用空格
"prettier.semi": true, // 句尾添加分号
"prettier.singleQuote": true, // 使用单引号代替双引号
"prettier.proseWrap": "preserve", // 默认值。因为使用了一些折行敏感型的渲染器(如GitHub comment)而按照markdown文本样式进行折行
"prettier.arrowParens": "avoid", // (x) => {} 箭头函数参数只有一个时是否要有小括号。avoid:省略括号
"prettier.bracketSpacing": true, // 在对象,数组括号与文字之间加空格 "{ foo: bar }"
"prettier.disableLanguages": ["vue"], // 不格式化vue文件,vue文件的格式化单独设置
"prettier.endOfLine": "auto", // 结尾是 \n \r \n\r auto
"prettier.eslintIntegration": false, //不让prettier使用eslint的代码格式进行校验
"prettier.htmlWhitespaceSensitivity": "ignore",
"prettier.ignorePath": ".prettierignore", // 不使用prettier格式化的文件填写在项目的.prettierignore文件中
"prettier.jsxBracketSameLine": false, // 在jsx中把'>' 是否单独放一行
"prettier.jsxSingleQuote": false, // 在jsx中使用单引号代替双引号
"prettier.parser": "babylon", // 格式化的解析器,默认是babylon
"prettier.requireConfig": false, // Require a 'prettierconfig' to format prettier
"prettier.stylelintIntegration": false, //不让prettier使用stylelint的代码格式进行校验
"prettier.trailingComma": "es5", // 在对象或数组最后一个元素后面是否加逗号(在ES5中加尾逗号)
"prettier.tslintIntegration": false // 不让prettier使用tslint的代码格式进行校验
}
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Prettier格式化配置

+

文章作者:

+

发布时间:2020年05月28日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/05/28/Prettier-Setting/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2020/07/19/rpm\345\221\275\344\273\244/index.html" "b/ja/2020/07/19/rpm\345\221\275\344\273\244/index.html" new file mode 100644 index 0000000000..33211e3560 --- /dev/null +++ "b/ja/2020/07/19/rpm\345\221\275\344\273\244/index.html" @@ -0,0 +1,663 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + rpm命令 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ rpm命令 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

安装

sudo rpm -ivh [lz4-1.8.3-1.el7.x86_64.rpm]

+

卸载

sudo rpm -e [postgresql14-server-14.4-1PGDG.rhel7.x86_64]

+

查询功能

命令格式 rpm {-q|–query} [select-options] [query-options]

+

  RPM的查询功能是极为强大,是极为重要的功能之一;举几个常用的例子,更为详细的具体的,请参考#man rpm

+

1、对系统中已安装软件的查询

+

1)查询系统已安装的软件

+

  语法:rpm -q 软件名

+

  举例:[root@localhost beinan]# rpm -q gaim

+

  gaim-1.3.0-1.fc4   

+
   查看系统中所有已经安装的包,要加 -a 参数 ;
+
+

  [root@localhost RPMS]# rpm -qa

+

  如果分页查看,再加一个管道 |和more命令;

+

  [root@localhost RPMS]# rpm -qa |more

+

  在所有已经安装的软件包中查找某个软件,比如说 gaim ;可以用 grep 抽取出来;

+

  [root@localhost RPMS]# rpm -qa |grep gaim

+

  上面这条的功能和 rpm -q gaim 输出的结果是一样的;

+

2)查询一个已经安装的文件属于哪个软件包

+

  语法 rpm -qf 文件名

+

  注:文件名所在的绝对路径要指出

+

  举例:

+

  [root@localhost RPMS]# rpm -qf /usr/lib/libacl.la

+

  libacl-devel-2.2.23-8

+

3)查询已安装软件包都安装到何处

+

  语法:rpm -ql 软件名 或 rpm rpmquery -ql 软件名

+

  举例:

+

  [root@localhost RPMS]# rpm -ql lynx

+

  [root@localhost RPMS]# rpmquery -ql lynx

+

4)查询一个已安装软件包的信息

+

  语法格式: rpm -qi 软件名

+

  举例:

+

  [root@localhost RPMS]# rpm -qi lynx

+

5)查看一下已安装软件的配置文件

+

  语法格式:rpm -qc 软件名

+

  举例:

+

  [root@localhost RPMS]# rpm -qc lynx

+

6)查看一个已经安装软件的文档安装位置

+

  语法格式: rpm -qd 软件名

+

  举例:

+

  [root@localhost RPMS]# rpm -qd lynx

+

7)查看一下已安装软件所依赖的软件包及文件

+

  语法格式: rpm -qR 软件名

+

  举例:

+

  [root@localhost beinan]# rpm -qR rpm-python

+

  查询已安装软件的总结:对于一个软件包已经安装,我们可以把一系列的参数组合起来用;比如 rpm -qil ;比如:

+

  [root@localhost RPMS]# rpm -qil lynx

+

2、对于未安装的软件包的查看:

+

  查看的前提是您有一个.rpm 的文件,也就是说对既有软件file.rpm的查看等;

+

1)查看一个软件包的用途、版本等信息;

+

  语法: rpm -qpi file.rpm

+

  举例:

+

  [root@localhost RPMS]# rpm -qpi lynx-2.8.5-23.i386.rpm

+

2)查看一件软件包所包含的文件;

+

  语法: rpm -qpl file.rpm

+

  举例:

+

  [root@localhost RPMS]# rpm -qpl lynx-2.8.5-23.i386.rpm

+

3)查看软件包的文档所在的位置;

+

  语法: rpm -qpd file.rpm

+

  举例:

+

  [root@localhost RPMS]# rpm -qpd lynx-2.8.5-23.i386.rpm

+

4)查看一个软件包的配置文件;

+

  语法: rpm -qpc file.rpm

+

  举例:

+

  [root@localhost RPMS]# rpm -qpc lynx-2.8.5-23.i386.rpm

+

5)查看一个软件包的依赖关系

+

  语法: rpm -qpR file.rpm

+

  举例:

+

  [root@localhost archives]# rpm -qpR yumex_0.42-3.0.fc4_noarch.rpm

+

  /bin/bash

+

  /usr/bin/python

+

  config(yumex) = 0.42-3.0.fc4

+

  pygtk2

+

  pygtk2-libglade

+

  rpmlib(CompressedFileNames) <= 3.0.4-1

+

  rpmlib(PayloadFilesHavePrefix) <= 4.0-1

+

  usermode

+

  yum >= 2.3.2

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:rpm命令

+

文章作者:

+

发布时间:2020年07月19日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/07/19/rpm%E5%91%BD%E4%BB%A4/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2020/08/02/Linux\347\233\256\345\275\225\350\257\264\346\230\216/index.html" "b/ja/2020/08/02/Linux\347\233\256\345\275\225\350\257\264\346\230\216/index.html" new file mode 100644 index 0000000000..24cf92465e --- /dev/null +++ "b/ja/2020/08/02/Linux\347\233\256\345\275\225\350\257\264\346\230\216/index.html" @@ -0,0 +1,610 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Linux目录说明 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Linux目录说明 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

/bin:

bin是Binary的缩写, 这个目录存放着最经常使用的命令。

+

/boot:

这里存放的是启动Linux时使用的一些核心文件,包括一些连接文件以及镜像文件。

+

/dev :

dev是Device(设备)的缩写, 该目录下存放的是Linux的外部设备,在Linux中访问设备的方式和访问文件的方式是相同的。

+

/etc:

这个目录用来存放所有的系统管理所需要的配置文件和子目录。

+

/home:

用户的主目录,在Linux中,每个用户都有一个自己的目录,一般该目录名是以用户的账号命名的。

+

/lib:

这个目录里存放着系统最基本的动态连接共享库,其作用类似于Windows里的DLL文件。几乎所有的应用程序都需要用到这些共享库。

+

/lost+found:

这个目录一般情况下是空的,当系统非法关机后,这里就存放了一些文件。

+

/media linux

系统会自动识别一些设备,例如U盘、光驱等等,当识别后,linux会把识别的设备挂载到这个目录下。

+

/mnt:

系统提供该目录是为了让用户临时挂载别的文件系统的,我们可以将光驱挂载在/mnt/上,然后进入该目录就可以查看光驱里的内容了。

+

/opt:

这是给主机额外安装软件所摆放的目录。比如你安装一个ORACLE数据库则就可以放到这个目录下。默认是空的。

+

/proc:

这个目录是一个虚拟的目录,它是系统内存的映射,我们可以通过直接访问这个目录来获取系统信息。
这个目录的内容不在硬盘上而是在内存里,我们也可以直接修改里面的某些文件,比如可以通过下面的命令来屏蔽主机的ping命令,使别人无法ping你的机器:

+

/root:

该目录为系统管理员,也称作超级权限者的用户主目录。

+

/sbin:

s就是Super User的意思,这里存放的是系统管理员使用的系统管理程序。

+

/selinux:

这个目录是Redhat/CentOS所特有的目录,Selinux是一个安全机制,类似于windows的防火墙,但是这套机制比较复杂,这个目录就是存放selinux相关的文件的。

+

/srv:

该目录存放一些服务启动之后需要提取的数据。

+

/sys:

这是linux2.6内核的一个很大的变化。该目录下安装了2.6内核中新出现的一个文件系统 sysfs 。

+

sysfs文件系统集成了下面3种文件系统的信息:针对进程信息的proc文件系统、针对设备的devfs文件系统以及针对伪终端的devpts文件系统。
该文件系统是内核设备树的一个直观反映。

+

当一个内核对象被创建的时候,对应的文件和目录也在内核对象子系统中被创建。

+

/tmp:

这个目录是用来存放一些临时文件的。

+

/usr:

这是一个非常重要的目录,用户的很多应用程序和文件都放在这个目录下,类似与windows下的program files目录。

+

/usr/bin:

系统用户使用的应用程序。

+

/usr/sbin:

超级用户使用的比较高级的管理程序和系统守护程序。

+

/usr/src:

内核源代码默认的放置目录。

+

/var:

这个目录中存放着在不断扩充着的东西,我们习惯将那些经常被修改的目录放在这个目录下。包括各种日志文件。

+

在linux系统中,有几个目录是比较重要的,平时需要注意不要误删除或者随意更改内部文件。
系统上跑了很多程序,那么每个程序都会有相应的日志产生,而这些日志就被记录到这个目录下,具体在/var/log 目录下,另外mail的预设放置也是在这里。

+

/etc:

上边也提到了,这个是系统中的配置文件,如果你更改了该目录下的某个文件可能会导致系统不能启动。

+

/bin, /sbin, /usr/bin, /usr/sbin:

这是系统预设的执行文件的放置目录,比如 ls 就是在/bin/ls 目录下的。

+

值得提出的是,/bin, /usr/bin 是给系统用户使用的指令(除root外的普通用户),而/sbin, /usr/sbin 则是给root使用的指令。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Linux目录说明

+

文章作者:

+

发布时间:2020年08月02日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/08/02/Linux%E7%9B%AE%E5%BD%95%E8%AF%B4%E6%98%8E/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/11/10/learn-cordova-1/index.html b/ja/2020/11/10/learn-cordova-1/index.html new file mode 100644 index 0000000000..cf0829f55d --- /dev/null +++ b/ja/2020/11/10/learn-cordova-1/index.html @@ -0,0 +1,719 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 从零开始:学习Cordova(一) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 从零开始:学习Cordova(一) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

很长时间没有写博客了,因为最近换了工作,原来的技术栈基本上都换了,现在接触了很多新的技术栈,Cordova 就是其中之一。

+

接下来这篇文章就是我的学习笔记,以供回顾,写的不好请勿拍砖。

+ +

一、What is Cordova? Cordova是个啥?

Logo
移动端开发和 Web 开发有一些不一样,Web 端开发面向的是浏览器,而移动端开发面向的是各种移动设备,那么针对各种移动设备提供的 SDK 进行开发的话,我们通常称之为***原生开发***,原生开发虽然通常调用底层 Api ,性能更好,但是因为不同移动终端底层技术不一样,所以每种设备都要单独开发,这样开发成本和能力要求都比较高,所以通常都会有一些框架来兼容各种平台,就好比 Java 虚拟机可以运行在 Mac/Windows/Linux 一样,为了能够写一套移动端代码可以在各种设备中使用,于是乎就有了 Cordova

+

上面说了这么一堆废话,其实简言之,***Cordova 就是提供了跨设备调用底层Api的移动端开发框架(这种开发方式又叫做Hybird 模式)***

+

知道这一点应该就够了,当然这是我认为的。

+

Cordova 的官网地址如下

+
+

https://cordova.apache.org/

+
+

其实从这里就可以看出, CordovaApache 基金下的一个项目,其官网是这样介绍它的

+
+

Mobile apps with HTML, CSS & JS
Target multiple platforms with one code base
Free and open source

+
+

翻译成中文的话就是

+
+

使用 HtmlCSSJS 进行移动端应用开发
一份代码多平台使用
免费并且开源

+
+

三句话解释了 Cordova 的三个方面,

+
    +
  • 使用前端技术开发-对前端开发友好
  • +
  • 跨平台-节省开发工时,降低开发难度-从图标看支持 Android/IOS/WP三大平台
  • +
  • 免费不用解释开源的话意味着会有很多人贡献代码
  • +
+

以上奠定了 Cordova 强大及流行的基础。

+

二、快速上手

在使用 Cordova 做移动端开发之前需要做一些环境准备工作,首先要安装 Node 以及它携带的 Npm 包管理器,这个现代的前端开发应该都知道,我这里就不多说了,装它就完了。

+

装好之后我们就要用 Npm 全局安装一下 Cordova 项目的依赖,命令很简单;

+

方便的话还要装一下 Git ,因为一些命令要在 git-cmd 上执行,当然管理代码也是需要它的,所以装吧。

+
1
npm install -g cordova
+

就这么简单就可以开发 Cordova 应用了,接下来我们要创建一个项目,创建项目的命令也十分简单。在命令行工具中输入下面的命令你就创建了一个 Cordova 的应用

+
1
cordova create <path> // 这里的path是项目的路径
+

如果要查看创建项目时还能设置什么参数你可以执行下面的命令

+
1
cordova help create
+

怎么样是不是超级简单。
做了上面的工作你就创建了一个具备基本功能的 Cordova 项目,接下来我们要给项目配置一些东西。

+

首先刚才讲到了, Cordova 是一个跨平台的框架,那么你的项目要适配什么平台你需要进行一些配置, cd 到项目目录下执行下面的命令

+
1
cordova platform add <platform name> // platform name 为适配平台的名称
+

如果不知道名称是什么命令行工具也贴心的给了查询用的命令,通过此命令你可以看到你安装了什么平台的适配,有哪些可以用

+
1
2
cordova platform

+

执行了这个命令你将看到下面这样的 list

+
1
2
3
4
5
6
7
8
/***
Installed platforms:
Available platforms:
android ^9.0.0
browser ^6.0.0
electron ^1.0.0
windows ^7.0.0
*/
+

项目也创建了,简单的配置工作也完成了,我们可以启动项目看一下效果了,那么我们就该执行运行项目的命令了

+
1
cordova run <platform name> // platform name 适配平台的名称
+

到这我们就快速的创建并运行起了一个 Cordova 的工程。

+

小Tip

如上执行启动命令后,默认会启动8000端口,如果被占用的话可以通过增加命令行参数的形式改变端口号,参数如下设置

+
1
cordova run browser --target=chrome --port=9090
+

以上就可以自定义端口及浏览器启动应用了。

+

三、详细了解

1、概览

官网开头介绍了 Cordova 的身世和适用范围,前面开头已经讲过了不想再讲,直接看看 Cordova 的架构设计图。
此处是个架构图

+
Cordova 架构图
+ +

这个图理解起来不难,大的方面是两个部分,底层是 MobileOS ,也就是手机的操作系统本身,上层是由 Cordova 构建的框架;

+

Cordova 内部的话是两层结构:

+
    +
  • 上层的话是由前端代码构成的 WebApp 部分;
  • +
  • 下层是 Cordova 的视图渲染引擎,它为 WebApp 提供了 HtmlApiCordovaApi
  • +
+

也就是说我们可以调用 html 的原生 Api 也可以使用 Cordova 提供的 jsApi

+

Cordova 还提供了丰富的插件系统;通过 Cordova 的视图引擎调用 CordovaNativeApi 来调用一些 Cordova 提供好的调用设备的信息的 Api ,另外也可以调用一些用户自定义的 Api

+

Cordova 视图引擎和插件系统再将上层的需求通过 MobileOS 开放的 Api 能力实现具体功能,由此完成了整个 Cordova 框架的使用;

+

总的来说这个原理跟大多数跨平台的框架原理都类似,由框架提供统一的 Api 能力,再由框架处理不同平台的兼容,这里特别之处在于渲染引擎可以允许使用前端原生的 Api ,这将大大降低开发的难度。

+

WebView

说到移动端开发,不知道 WebView 应该是不可能的,在原生移动应用中,WebView 就是移动应用内部嵌入的一个‘浏览器’,它可以允许你使用前端技术展示内容。

+

WebApp

WebAppWebView 两个词有些像,事实上它们也确实有些关系,WebApp 从名字就可以看出是使用 Web 技术开发的 App ,那么它跟 WebView 是什么关系呢,打个比方,WebView 就是个快递盒子, WebApp 就是你买的商品,一个是容器,一个是内容;这样说应该就明白了吧,通过前端技术开发的 App 通过 WebView 嵌入到 App 中,就可以带给用户原生应用的体验,忘了说 WebApp 是只能用浏览器访问的 App

+

和纯 WebApp 不同,嵌入到 WebView 中的 WebApp 需要有一个配置文件,***config.xml*** ,它在项目的根目录下,说明了应用的一些信息,这个文件必不可少!!!

+

插件系统

刚刚在上面的架构图中我们介绍了 Cordova 的插件系统,它提供了我们通过 js 调用 MobileApi 的能力,如电源/相机/联系人等等;
官方维护了核心的功能,当然如果你愿意也可以调用一些第三方提供的插件,你可以方便的通过 npm 包管理器安装;

+

但是有一点必须要说:项目创建后默认是不带任何插件的,即使是官方的核心插件也是需要你自己导入进去,第三方的更不用说了,另外Cordova 只提供了功能 Api 并不包含任何的 UI 部件和 MV 框架(你可以根据自己喜欢使用 Angular 或者 Vue 或者其他什么都可以),这一点要牢记。

+

开发工作流

使用 Cordova 开发 MobileApp 的时候即可以开发多个平台的 App (一次开发多平台可用,降低开发成本),也可以专注开发某一平台的App (使用前端技术开发,降低开发难度),因此开发工作流也分为两种:

+
跨平台(CLI)的工作流

官网上说了很多,也不难理解,这里用我自己的话说就是, Cordova 提供了一个牛叉的 CLI 工具,它可以自动的完成一次编码多平台构建的事,你只需要按照它说的方式开发就行。:)

+
平台为中心的工作流

这里不得不说我没有看很明白,但是结合下面的注意事项我是这样理解的,因为只为某一平台服务,所以有一些针对此平台的特殊的功能或者插件你就可以使用了(虽然不太清楚为什么这么做,如果你知道告诉我,谢谢),但是一旦使用了这种开发方式,那么就回不到跨平台开发了,因为你使用了针对某一平台才能调用的代码,这也不难理解,从字里行间感觉就是不建议使用这种开发工作流(好的好的,知道了)。

+

好的,接下来开发 App 啦;

+

2、开发应用

快速上手中已经介绍了如何准备、创建和启动应用,如何添加平台,这里就不多说了。说些没讲过的。

+

上面说过检查目前平台安装情况的话可以使用下面的命令

+
1
cordova platforms
+

我们也可以使用另一个命令来看,我试验了一下,效果是一样的。。。(问号脸,一样的为什么搞两个命令)

+
1
cordova platform ls
+
+

事后我发现,无论我使用的命令是 platform 还是 platforms 加不加 ls 都可以查到当前安装的平台。。。 啊~兼容性好强。

+
+

每执行一次 add 命令,你就会发现工程目录下的 platforms 目录下就对应生成了一个对应平台的文件夹,切勿手贱删除,你虽然删除了文件夹,但是各处设置的 platform 并没有去掉,会导致报错,如果要删除请使用 remove 命令。 想知道还有哪些命令的话就看下面的链接吧。⇒ 命令大全

+

安装构建的先决条件

因为 Cordova 的底层还是要调用 MobileOS 的接口,所以各平台的 SDK 是必须要装的,当然这里指的是你要进行构建的平台,比如你要构建 Android 应用,那么你就要装 AndroidSDKIOS 同理。这里有一个例外broswer平台是不需要依赖其他 SDK 的(明明是开发 APP ,这里只是为了验证画面比较方便)
要看你是否满足了构建需要的依赖,可以执行下面的命令查看

+
1
cordova requirements
+

执行完后你会得到如下结果

+
1
2
3
4
5
6
7
8
9
10
Requirements check results for android:
Java JDK: installed .
Android SDK: installed
Android target: installed android-19,android-21,android-22,android-23,Google Inc.:Google Apis:19,Google Inc.:Google Apis (x86 System Image):19,Google Inc.:Google Apis:23
Gradle: installed

Requirements check results for ios:
Apple OS X: not installed
Cordova tooling for iOS requires Apple OS X
Error: Some of requirements check failed
+

这样你就可以清楚的知道自己需要安装哪些依赖才能完成对应平台的构建,简直方便的不要不要的。
具体想知道不同的平台都依赖些什么,你可以参见下面的链接;

+ +

构建 App

create 项目之后,项目的根目录下会生成一个 www 目录,这个目录包含了 webapp 的入口页面 index.html ,入口文件的话是 www/js/index.js 文件的 deviceready 事件中。

+

如果要构建代码的话,执行下面的命令

+
1
cordova build
+

这个命令会构建你安装的所有平台,如果只想构建某一平台的话,可以把平台的名字加在命令后面,如

+
1
cordova build ios
+

如果想了解更多的参数的话,可以看一下后面的链接。⇒ 更多参数

+

测试 App

构建完 App 之后我们就可以测试了,通常各平台的 SDK 中会提供模拟器,我们只需要执行下面的命令就可以启动模拟器。

+
1
cordova emulate android
+

当然如果你觉得用实机查看更自然一些的话,也可以把手机插上数据线与电脑建立连接,然后执行下面的命令就可以了。

+
1
cordova run android
+

上面演示的是 AndroidApp 测试过程,每个平台虽然基本类似,但还是有出入,所以,下面提供了不同平台的调试方法,供你预览。

+ +

添加插件

如果只是开发一个 Cordova 框架的 WebApp ,那么你不需要装任何插件,直接用前端技术开发就好了,但是应该没人会这么做吧,毕竟开发这个的目的就是为了跨平台开发原生级移动 App ,因此要调用移动设备的各种功能就必须安装插件。

+

插件

Cordova 插件是一些使用 Javascript 调用原生 SDK 功能的包;
你可以使用两种方式找到你想要的插件

+
    +
  1. 通过 Cordova 提供的包管理平台 ⇒ 插件搜索页
  2. +
  3. 通过命令行搜索 cordova plugin search camera
  4. +
+

选好你要安装的插件之后你需要执行下面的命令来安装它

+
1
cordova plugin add <plugin name> // 如 cordova plugin add cordova-plugin-chrome-apps-proxy
+

如果你的包没有发布在 Cordova 的平台上,也可以使用 git 地址来安装。
另外 Cordova 还非常贴心的提供了一个工具 Plugman 来帮助开发者更好的管理 Cordova 插件。⇒ Plugman参考

+

如果要查看你安装了哪些插件可以使用下面任意一种命令都可以。(咱也不知道为啥提供这么多方式,反正好用无脑)

+
1
2
3
4
5
6
7
8
plugin ls
plugin list
plugin

/**
$ cordova plugin ls
cordova-plugin-whitelist 1.2.1 "Whitelist"
*/
+

如果想知道更多关于 plugin 的命令参数,可以看右边的链接 ⇒ plugin参数

+

使用 merges 自定义每个平台

虽然可以用一套代码来构建多个平台,但一些平台会有自己的特点,就好像 Chrome 浏览器默认的字体大小与 IE 的不同, AndroidIOS 也有不一样的地方,这种情况下去改 www 目录下的文件显然是不合适的,所以 Cordova 提供了 merges 方式来适配不同平台各自的特别处理,你要做的就是在项目的根目录下创建一个 merges 目录,比如你要适配 Android ,你就可以在 merges 下创建一个 android 目录,然后在下面或覆盖或添加新的资源。(这里官网没有说 merges 在哪里创建,试验后发现是在根目录下)

+

更新 Cordova 和你的项目

如果你要更新你的 Cordova (通常不建议这么干,当然你知道后果并想要使用新的特性或者修正 bug 除外),那么你需要执行一下 update 命令

+
1
sudo npm update -g cordova
+

当然你也可以指定更新到什么版本(这都是 npm 的知识了)

+
1
sudo npm install -g cordova@3.1.0-0.2.0
+

你也可以使用下面的命令查看当前的版本(还是 npm 的知识)

+
1
npm info cordova version
+

你是不是以为这样就可以了,不好意思还不行,你还需要把各个平台进行一下升级

+
1
2
3
cordova platform update android --save
cordova platform update ios --save
// ...
+

其实这也好理解,你都已经轿车换 SUV 了,总不能还用原来的车轱辘吧,只是我希望能不能在换车的时候一块儿把车轱辘换了(这里的意思是希望直接自动化处理了)

+

到这里你的第一个 App 就已经开发好了,是不是还有点成就感。

+

一些参考资料

到这里一个简单的基于 Cordova 搭建的 App 就实现了,当然一个 App 绝不这么简单,各平台的 SDK 安装也没那么简单,但这不是本篇文章要说的事了,下一篇文章我会说说如何应对各种不同的开发平台,在这之前还请你看一下 Cordova 都支持哪些平台,具体看一下下面的链接。

+

好了,今天就先写到这里,下篇文章我们再见。

+

平台支持

+

(待续…)

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:从零开始:学习Cordova(一)

+

文章作者:

+

发布时间:2020年11月10日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/11/10/learn-cordova-1/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/11/10/learn-go/index.html b/ja/2020/11/10/learn-go/index.html new file mode 100644 index 0000000000..5d16488a6e --- /dev/null +++ b/ja/2020/11/10/learn-go/index.html @@ -0,0 +1,587 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 从零开始:学习Go(一) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 从零开始:学习Go(一) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:从零开始:学习Go(一)

+

文章作者:

+

发布时间:2020年11月10日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/11/10/learn-go/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2020/11/11/learn-ionic-1/index.html b/ja/2020/11/11/learn-ionic-1/index.html new file mode 100644 index 0000000000..da3c114679 --- /dev/null +++ b/ja/2020/11/11/learn-ionic-1/index.html @@ -0,0 +1,640 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 从零开始:学习Ionic(一) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 从零开始:学习Ionic(一) +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

在移动 App 开发领域,有一个框架非常知名,每每面试都会被问起,“你用过 Ionic 么?”,是的,就是 Ionic 框架。

+

之前已经学习过了 Cordova ,它和 Ionic 可以说是一对黄金搭档,一个面向 MobileOS ,一个面向 UI ,很多人搞不清楚它们之间的区别,没关系,回头我再抽时间讲一下,今天我们主要来认识一下 Ionic

+

好戏开始:)

+ +

Ionic 简介

学一门技术,我们首先要知道它是谁,能干啥,有啥好处。要了解一门新技术最好的方式就是去它的官网看看。
(一言不合丢给你一个官网 ⇒ 官网

+
+

One codebase.
Any platform.
Now in Vue.

+
+

翻译过来是这样的

+
+

一份代码
任何平台
现在支持Vue

+
+

看着是不是眼熟,嗯,跟 Cordova 的口号何其相似,目标都是跨平台,只是 Cordova 跨的是 MobileOSIonic 跨的是前端框架。
最下面一句 Now in Vue 像是在说,“嗯,我也支持Vue了(Vue发展太快了,不能掉队)”,要知道,最开始的 Ionic 是妥妥的 Angular 派,不过无所谓,管他黑猫白猫,能完成任务就是好猫。

+

再往下看你会看到,巴拉巴拉说了一堆好处,比如开发迅速啊,组件优美且丰富啊,社区强大啊,还有跨平台(支持原生 jsAngularVue ,nice~),另外还提供了 native 的组件来直接调用移动设备的功能,可以说是相当强大了。

+

好了,看完了介绍,我们赶紧快速上手吧。

+

快速上手

在使用Ionic做开发还是要做一些准备工作,nodegit都是必不可少的,所以请先行准备好。

+

目前,Ionic提供了两种创建工程的方式:

+
    +
  • 一种用官方的话说叫StepByStep,看过之后我理解就是通过命令的方式,一步步来;
  • +
  • 另一种的话是官方提供了一个向导工具,就跟安装软件一样填一些必要的信息,然后一路下一步就生成了一个工程;
  • +
+

接下来我将使用这两种方式分别创建工程,首先是第一种。

+

StepByStep 创建工程

    +
  1. 全局安装Ionic的脚手架工具
    1
    npm install -g @ionic/cli
  2. +
  3. 创建工程
    1
    ionic start myApp tabs // myApp 是项目目录名称也是工程名称 tabs是模板名称
  4. +
  5. 启动工程
    1
    2
    cd myApp
    ionic serve
  6. +
+

嗯,上面三个步骤之后,你的Ionic项目就启动起来了。(What !? 这么简单?! 嗯,是的。)

+

向导式(App Wizard)创建工程

这是一个在线创建工程的手段(感觉对于开发人员来说貌似还是命令行来的方便,这种可视化方案应该是给小白用户准备的吧),地址如下

+
+

https://ionicframework.com/start

+
+

此处是个图

+

点开上面的地址,你会看到如图所示的一个界面,我们可以看到它要求你输入的一些信息

+
    +
  1. App Name
  2. +
  3. Icon
  4. +
  5. Theme Color
  6. +
  7. Template
  8. +
  9. Framework
  10. +
+

可以看到这比命令行要多出几个信息,上面的命令行只要求了 App NameTemplate 难道强大的命令行不支持设置这些么?答案是否定的,当然可以设置这些信息,只是以参数的形式而已,先不说这个,填写好上面的信息以后我们点击[create App]按钮,这时我们进入了下一步,选择代码仓库(Choose a git host),你会看到下面有一个[Skip]按钮说明这步不是必须的,我们先选择一个git仓库,这里我选择github,其他的感兴趣的同学可以自己试试。点击[connect],这时会弹出一个窗口,申请对应git仓库的鉴权认证,认证通过后我们点击[choose],等待程序运行一会儿后工程就会被上传到git仓库中,并且会跳转到一个DashBoard,提供了可视化管理工程的页面(也太高大上了吧),右侧我们看到如何把代码下载到本地,并运行它,真的是很方便。

+

例:

+
1
2
3
npm install -g @ionic/cli cordova-res // cordova-res 指的是安装 cordova 的开发依赖,因为除 cordova 之外还可以选择如 phonegap 之类的MobileOS 框架
git clone https://github.com/lixuguang/li-app.git li-app
cd li-app && npm install && ionic serve
+ +

执行完上面的命令以后,我们的工程也就运行起来了,也是十分方便,即使不会写代码的人,跟着指南也可以很方便的搭建一个Ionic工程。

+

深入学习

Ionic 的体系当中,最重要的莫过于 Ionic CLI 和 UI Component 两个了,接下来我将分辨将这两部分中最重要的内容拿出来说说。

+

Ionic CLI

在上面的快速上手中,我们已经使用了几个命令,但这些还不够,Ionic CLI 提供了强大的命令,请与我一起看一下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
npm install -g @ionic/cli // 全局安装 Ionic CLI
ionic start myApp tabs // 用模板创建工程
ionic start --list // 查看可用模板
ionic serve // 启动工程
ionic serve --lab // 以android或ios方式打开
ionic lib update // 更新包
ionic serve --address 192.168.89.1 // 指定Ip,给外部用户访问
ionic platform add ios/android // 添加平台
ionic build ios/android // 构建平台
ionic emulate ios [options] // ionic run ios [options] 模拟器运行

ionic generate // 创建新特性


options
-l //livereload, 实时刷新变化。
-c //打印app里的console
-s //打印设备的console
-p //指定设备的端口
-i //指定livereload的重刷端口
--debug //debug
--release //release
--host=0.0.0.0 // IP
--port=8100 // 端口


ionic resources [--splash] [--icon] // 上传代码到官方平台
ionic upload // 登录
ionic info // 查看系统信息
ionic browser add crosswalk // 加壳预览(手机浏览器性能问题解决)
ionic browser list // 查看可用的browser
ionic browser revert android/ios // 删除安装的browser
ionic state reset // 先删除平台和插件,再安装package.json文件中的平台和插件。重置
ionic state save // 保存当前状态信息
ionic state clear // 先删除平台和插件,然后按照package.json文件中包含的平台和插件重新安装。

npm install @ionic/angular@latest --save // Ionic + Angular
ng add @ionic/angular // 使用Angular CLI 增加Ionic的组件库,可用于Angular项目改造Ionic

npm install @ionic/react // Ionic + React
npm install @ionic/react-router // 在既存的react项目中引入ionic特性

// 增加CSS文件为组件服务,加在根组件下
/* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css';

/* Basic CSS for apps built with Ionic */
import '@ionic/react/css/normalize.css';
import '@ionic/react/css/structure.css';
import '@ionic/react/css/typography.css';

/* Optional CSS utils that can be commented out */
import '@ionic/react/css/padding.css';
import '@ionic/react/css/float-elements.css';
import '@ionic/react/css/text-alignment.css';
import '@ionic/react/css/text-transformation.css';
import '@ionic/react/css/flex-utils.css';
import '@ionic/react/css/display.css';

npm install @ionic/vue @ionic/vue-router // Ionic + Vue ,在既存的Vue项目中引入ionic特性

// main.js 文件中加入引入
import { IonicVue } from '@ionic/vue';

import App from './App.vue'
import router from './router';

const app = createApp(App)
.use(IonicVue)
.use(router);

router.isReady().then(() => {
app.mount('#app');
});

// router/index.js 用Ionic提供的vue-router替换原本的vue-router
import { createRouter, createWebHistory } from '@ionic/vue-router';

const routes = [
// routes go here
]

const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})

export default router;

// 增加CSS文件为组件服务 main.js
/* Core CSS required for Ionic components to work properly */
import '@ionic/vue/css/core.css';

/* Basic CSS for apps built with Ionic */
import '@ionic/vue/css/normalize.css';
import '@ionic/vue/css/structure.css';
import '@ionic/vue/css/typography.css';

/* Optional CSS utils that can be commented out */
import '@ionic/vue/css/padding.css';
import '@ionic/vue/css/float-elements.css';
import '@ionic/vue/css/text-alignment.css';
import '@ionic/vue/css/text-transformation.css';
import '@ionic/vue/css/flex-utils.css';
import '@ionic/vue/css/display.css';

From here, you can learn about how to develop with Ionic Framework in our [Ionic Vue Quickstart Guide](https://ionicframework.com/docs/vue/quickstart).

## Ionicons CDN

Ionicons is packaged by default with the Ionic Framework, so no installation is necessary if you're using Ionic. To use Ionicons without Ionic Framework, place the following `<script>` near the end of your page, right before the closing `</body>` tag.


<script type="module" src="https://cdn.jsdelivr.net/npm/ionicons@4.7.4/dist/ionicons/ionicons.esm.js"></script>
<script nomodule src="https://cdn.jsdelivr.net/npm/ionicons@4.7.4/dist/ionicons/ionicons.js"></script>


+ + +

(待续…)

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:从零开始:学习Ionic(一)

+

文章作者:

+

发布时间:2020年11月11日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2020/11/11/learn-ionic-1/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2021/05/08/\345\205\263\344\272\216VSCode\344\270\255\345\210\253\345\220\215\357\274\210-\357\274\211\347\232\204\344\275\277\347\224\250/index.html" "b/ja/2021/05/08/\345\205\263\344\272\216VSCode\344\270\255\345\210\253\345\220\215\357\274\210-\357\274\211\347\232\204\344\275\277\347\224\250/index.html" new file mode 100644 index 0000000000..c19136ab01 --- /dev/null +++ "b/ja/2021/05/08/\345\205\263\344\272\216VSCode\344\270\255\345\210\253\345\220\215\357\274\210-\357\274\211\347\232\204\344\275\277\347\224\250/index.html" @@ -0,0 +1,611 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + VSCode中既想使用path-intellisense插件,又想使用Webpack别名(@)的方法 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ VSCode中既想使用path-intellisense插件,又想使用Webpack别名(@)的方法 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

+

场景:

前端开发我们通常使用VSCode,使用时常见有一个常见的场景:

+
+

引入一个文件、图片等

+
+

如果不利用VSCode的插件,我们通常会写相对路径或者绝对路径,但为了方便写代码,我们通常会利用工具来帮助我们减轻负担,这种情况就引入了今天的主角,VSCode下的插件path-intellisense,是的,有了它编辑器就会聪明的自动帮我们提示出文件在什么地方,这的确方便了我们。

+

但是,在Vue开发过程中,或者说是利用Webpack作为构建工具时我们通常会定义一个变量如:@ 来简化路径的书写,使代码变得简洁美观,这里的@,我们称之为 路径别名 ,这也是Vue官方十分推荐使用的,但因为路径别名的使用,原来的智能提示路径的插件就会无法正常工作,因为工作中确实遇到了这样的坑,所以简单的写个文记录一下。

+

问题一:因为使用了路径别名(@)导致path-intellisense不能自动提示

这个问题其实比较容易理解,因为路径别名是Webpack构建时使用的,并不是VSCode原生支持的功能,所以它并不认识@,这是个什么,所以解决思路就是告诉VSCode @ 是个什么就好了,专业一点说的话就是加上映射关系

+

解决步骤

    +
  1. 打开path-intellisense插件的setting
  2. +
  3. 找到Mapping配置项
  4. +
  5. 加以下代码
    1
    2
    3
    "path-intellisense.mappings": {
    "@": "${workspaceRoot}/src"
    }
  6. +
  7. 重启插件搞定!
  8. +
+

问题二:我们希望 Ctrl + 鼠标左键点击一个外部方法时,能够快速跳转到对应的外部文件。

第二个问题提出时,可能会有人有疑问,上面不是配置完映射关系了嘛,为什么还有下面的问题呢,其实你要是不问,我也有这个问题,上一步操作实际上是告诉了path-intellisense插件该怎么解析@这个东西,但是VSCode并不知道啊,但是Ctrl + 鼠标左键的动作又是VSCode负责的事,所以我们也要告诉一下编辑器该如何应对上面的情况,说到这里大家应该就理解了

+

解决步骤

    +
  1. 在项目package.json所在同级目录下创建文件jsconfig.json(这是VSCode的一个可选配置)
  2. +
  3. 在jsconfig.json增加如下配置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    {
    "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "allowSyntheticDefaultImports": true,
    "baseUrl": "./",
    "paths": {
    "@/*": ["src/*"] // 这里是关键代码
    }
    },
    "exclude": [
    "node_modules"
    ]
    }
  4. +
  5. 重启VSCode搞定!
  6. +
+

通过上面的解决方案,我们就可以愉快的使用VSCode进行前端开发了,你学废了吗?

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:VSCode中既想使用path-intellisense插件,又想使用Webpack别名(@)的方法

+

文章作者:

+

发布时间:2021年05月08日 - 08:03

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2021/05/08/%E5%85%B3%E4%BA%8EVSCode%E4%B8%AD%E5%88%AB%E5%90%8D%EF%BC%88-%EF%BC%89%E7%9A%84%E4%BD%BF%E7%94%A8/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2021/05/13/\345\205\263\344\272\216\347\211\210\346\234\254\347\256\241\347\220\206/index.html" "b/ja/2021/05/13/\345\205\263\344\272\216\347\211\210\346\234\254\347\256\241\347\220\206/index.html" new file mode 100644 index 0000000000..72762cfbf4 --- /dev/null +++ "b/ja/2021/05/13/\345\205\263\344\272\216\347\211\210\346\234\254\347\256\241\347\220\206/index.html" @@ -0,0 +1,600 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 关于版本管理 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 关于版本管理 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

还记得你的前端工程里有这样一个文件‘package.json’,里面管理者运行代码所需要的各种依赖,每种依赖都会有一个版本号,那么你知道版本号是怎么定义的么?不知道的话就看看下面的内容吧。

+ +

关于版本号的那些事

你会发现这些版本号通常是三部分构成的,像是‘X.Y.Z’的一种感觉,其实这是一种叫做SemVer的版本管理规范,下面我们就来讲讲SemVer

+

SemVer 的生平

语义化版本控制的规范是由 Gravatars 创办者兼 GitHub 共同创办者 Tom Preston-Werner 所建立。

+

它的许可证是 知识共享 署名 3.0 (CC BY 3.0) 所以你可以不用付费直接使用它。

+

关于它的更多描述你可以到下面的地址找到
https://semver.org/lang/zh-CN/

+

主要你要记住的是如下几句话:

+
+

版本格式:主版本号.次版本号.修订号,版本号递增规则如下:

+

主版本号:当你做了不兼容的 API 修改,
次版本号:当你做了向下兼容的功能性新增,
修订号:当你做了向下兼容的问题修正。
先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。

+
+

更多的内容的话,请到官网查看吧。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:关于版本管理

+

文章作者:

+

发布时间:2021年05月13日 - 07:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2021/05/13/%E5%85%B3%E4%BA%8E%E7%89%88%E6%9C%AC%E7%AE%A1%E7%90%86/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/03/11/\346\220\236\346\207\202EventLoop/index.html" "b/ja/2022/03/11/\346\220\236\346\207\202EventLoop/index.html" new file mode 100644 index 0000000000..5dd61ba462 --- /dev/null +++ "b/ja/2022/03/11/\346\220\236\346\207\202EventLoop/index.html" @@ -0,0 +1,702 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 搞懂EventLoop | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 搞懂EventLoop +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

EventLoop 翻译过来就是事件循环,那啥是事件循环啊,这个要从javascript是如何执行的说起。
要知道javascript是一个运行在浏览器或者Node环境的单线程执行的脚本语言,但是为了解决单线程阻塞的问题,这里就引入了事件循环的机制,也就是我们常说的异步特性。

+

但是虽然都是JavaScript,都是事件循环,都有异步特性,但浏览器环境和Node环境的实现方式是不一样的,这里要先说明一下。
浏览器的事件循环是HTML定义的规范,而Node环境是利用libuv库实现的,这里我们先从浏览器的事件循环开始说。

+ + +

浏览器的事件循环

首先我们要知道,浏览器的事件循环是有这么两个部分构成,一个叫做主线程(main thread),另一个叫做调用栈(call-stack),所有的任务(Task)都会被放到调用栈里,等待主线程调用,这个怎么理解呢,打个比方,主线程就像是物流配送中的传送带,如果没有快递往上放的时候,它是空的,当有快递包裹放上去的时候,它就开始运行,而你购买的产品要放倒包裹里,然后包裹在放到传送带上,最终被送到你家里,这里的传送带就是主线程,包裹就是调用栈,而购买的产品就是任务,不知道这样解释是否能好理解一些。

+

同步、异步任务

JavaScript中的任务有两种,一种是同步任务,另一种是异步任务。

+
    +
  • 同步任务会排着队,逐个执行
  • +
  • 异步任务在执行得到结果后,将回调函数添加到任务队列中,等着主线程空了再执行。
  • +
+

调用栈

栈,是一种数据结构,后进先出(LIFO),后入栈的先执行,执行完后出栈执行栈顶新的那个函数,直到栈空,像个瓶子,只有一头有口。

+

任务队列

队列,也是一种数据结构,先进先出(FIFO),队头出,队尾进,先到先得,就像排队买东西,或者汽车排队过隧道。

+

任务

任务分为两种,一种叫做宏任务(MacroTask),另一种叫做微任务(MicroTask),首先他们都是异步队列中的任务,那它们之间是什么关系呢?简单说来就是微任务是VIP优先执行,全部微任务执行完之后才轮到普通用户宏任务执行,而且执行完一个后马上要看有没有新的微任务执行,有的话宏任务还得等着,就这样往复,看着像不像是在银行等着排队办业务的你(其实是我)。
JavaScript的执行流程
宏任务与微任务
那么哪些是宏任务,哪些是微任务呢?看下表吧。

+ + + + + + + + + + + +
宏任务微任务
setTimeout、setInterval、js主代码、setImmediate(Node)、requestAnimationFrame(浏览器)process.nextTick、Promise的then方法
+

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
console.log('1');

setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})

setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
+ +

分析一下

JS 运行机制为从上而下,那么这里的执行顺序应该是什么样的呢?

+
第一轮循环:

1)、首先打印 1
2)、接下来是setTimeout是异步任务且是宏任务,加入宏任务暂且记为 setTimeout1
3)、接下来是 process 微任务 加入微任务队列 记为 process1
4)、接下来是 new Promise 里面直接 resolve(7) 所以打印 7 后面的then是微任务 记为 then1
5)、setTimeout 宏任务 记为 setTimeout2

+
+

第一轮循环打印出的是 1 7
当前宏任务队列:setTimeout1, setTimeout2
当前微任务队列:process1, then1,

+
+
第二轮循环:

1)、执行所有微任务
2)、执行process1,打印出 6
3)、执行then1 打印出8
4)、微任务都执行结束了,开始执行第一个宏任务
5)、执行 setTimeout1 也就是 第 3 - 14 行
6)、首先打印出 2
7)、遇到 process 微任务 记为 process2
8)、new Promise中resolve 打印出 4
9)、then 微任务 记为 then2

+
+

第二轮循环结束,当前打印出来的是 1 7 6 8 2 4
当前宏任务队列:setTimeout2
当前微任务队列:process2, then2

+
+
第三轮循环:

1)、执行所有的微任务
2)、执行 process2 打印出 3
3)、执行 then2 打印出 5
4)、执行第一个宏任务,也就是执行 setTimeout2 对应代码中的 25 - 36 行
5)、首先打印出 9
6)、process 微任务 记为 process3
7)、new Promise执行resolve 打印出 11
8)、then 微任务 记为 then3

+
+

第三轮循环结束,当前打印顺序为:1 7 6 8 2 4 3 5 9 11
当前宏任务队列为空
当前微任务队列:process3,then3

+
+
第四轮循环:

1)、执行所有的微任务
2)、执行process3 打印出 10
3)、执行then3 打印出 12

+

代码执行结束:
最终打印顺序为:1 7 6 8 2 4 3 5 9 11 10 12

+

Node环境的循环机制

Node的Event loop一共分为6个阶段,每个细节具体如下:

+
    +
  • timers: 执行setTimeout和setInterval中到期的callback。
  • +
  • pending callback: 上一轮循环中少数的callback会放在这一阶段执行。
  • +
  • idle, prepare: 仅在内部使用。
  • +
  • poll: 最重要的阶段,执行pending callback,在适当的情况下回阻塞在这个阶段。
  • +
  • check: 执行setImmediate(setImmediate()是将事件插入到事件队列尾部,主线程和事件队列的函数执行完成之后立即执行setImmediate指定的回调函数)的callback。
  • +
  • close callbacks: 执行close事件的callback,例如socket.on(‘close’[,fn])或者http.server.on(‘close, fn)。
  • +
+

Node EventLoop

+

具体细节如下:

+

timers

执行setTimeout和setInterval中到期的callback,执行这两者回调需要设置一个毫秒数,理论上来说,应该是时间一到就立即执行callback回调,但是由于system的调度可能会延时,达不到预期时间。
以下是官网文档解释的例子:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const fs = require('fs');

function someAsyncOperation(callback) {
// Assume this takes 95ms to complete
fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout(() => {
const delay = Date.now() - timeoutScheduled;

console.log(`${delay}ms have passed since I was scheduled`);
}, 100);


// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
const startCallback = Date.now();

// do something that will take 10ms...
while (Date.now() - startCallback < 10) {
// do nothing
}
});

+ +

当进入事件循环时,它有一个空队列(fs.readFile()尚未完成),因此定时器将等待剩余毫秒数,当到达95ms时,fs.readFile()完成读取文件并且其完成需要10毫秒的回调被添加到轮询队列并执行。
当回调结束时,队列中不再有回调,因此事件循环将看到已达到最快定时器的阈值,然后回到timers阶段以执行定时器的回调。
在此示例中,您将看到正在调度的计时器与正在执行的回调之间的总延迟将为105毫秒。

+

pending callbacks

此阶段执行某些系统操作(例如TCP错误类型)的回调。 例如,如果TCP socket ECONNREFUSED在尝试connect时receives,则某些* nix系统希望等待报告错误。 这将在pending callbacks阶段执行。

+

poll

该poll阶段有两个主要功能:

+
    +
  • 执行I/O回调。
  • +
  • 处理轮询队列中的事件。
  • +
+

当事件循环进入poll阶段并且在timers中没有可以执行定时器时,将发生以下两种情况之一
如果poll队列不为空,则事件循环将遍历其同步执行它们的callback队列,直到队列为空,或者达到system-dependent(系统相关限制)。

+

如果poll队列为空,则会发生以下两种情况之一

+
    +
  • 如果有setImmediate()回调需要执行,则会立即停止执行poll阶段并进入执行check阶段以执行回调。
  • +
  • 如果没有setImmediate()回到需要执行,poll阶段将等待callback被添加到队列中,然后立即执行。
  • +
+

当然设定了 timer 的话且 poll 队列为空,则会判断是否有 timer 超时,如果有的话会回到 timer 阶段执行回调。

+

check

此阶段允许人员在poll阶段完成后立即执行回调。
如果poll阶段闲置并且script已排队setImmediate(),则事件循环到达check阶段执行而不是继续等待。
setImmediate()实际上是一个特殊的计时器,它在事件循环的一个单独阶段运行。它使用libuv API来调度在poll阶段完成后执行的回调。
通常,当代码被执行时,事件循环最终将达到poll阶段,它将等待传入连接,请求等。
但是,如果已经调度了回调setImmediate(),并且轮询阶段变为空闲,则它将结束并且到达check阶段,而不是等待poll事件。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
console.log('start')
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
Promise.resolve().then(function() {
console.log('promise3')
})
console.log('end')

+ +

如果Node版本是v11.x,那么执行结果和浏览器一致,结果如下:

+
1
2
3
4
5
6
7
start
end
promise3
timer1
promise1
timer2
promise2
+ +

如果是v10版本上,那结果将会出现两种情况
如果time2定时器已经在执行队列中了

+
1
2
3
4
5
6
7
start
end
promise3
timer1
timer2
promise1
promise2
+ +

如果time2定时器没有在执行对列中,执行结果为

+
1
2
3
4
5
6
7
start
end
promise3
timer1
promise1
timer2
promise2
+ +

具体情况可以参考poll阶段的两种情况。

+

NodeEventLoop

+

setImmediate() 的setTimeout()的区别

setImmediate和setTimeout()是相似的,但根据它们被调用的时间以不同的方式表现。

+
    +
  • setImmediate()设计用于在当前poll阶段完成后check阶段执行脚本 。
  • +
  • setTimeout() 安排在经过最小(ms)后运行的脚本,在timers阶段执行。
  • +
+

举个例子

1
2
3
4
5
6
7
8
setTimeout(() => {
console.log('timeout');
}, 0);

setImmediate(() => {
console.log('immediate');
});

+ +

执行定时器的顺序将根据调用它们的上下文而有所不同。 如果从主模块中调用两者,那么时间将受到进程性能的限制。
其结果也不一致

+

如果在I / O周期内移动两个调用,则始终首先执行立即回调:

+
1
2
3
4
5
6
7
8
9
10
11
const fs = require('fs');

fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});

+ +

其结果可以确定一定是immediate => timeout。
主要原因是在I/O阶段读取文件后,事件循环会先进入poll阶段,发现有setImmediate需要执行,会立即进入check阶段执行setImmediate的回调。
然后再进入timers阶段,执行setTimeout,打印timeout。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
   ┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘

+ +

Process.nextTick()

process.nextTick()虽然它是异步API的一部分,但未在图中显示。这是因为process.nextTick()从技术上讲,它不是事件循环的一部分。

+

process.nextTick()方法将 callback 添加到next tick队列。 一旦当前事件轮询队列的任务全部完成,在next tick队列中的所有callbacks会被依次调用。

+

换种理解方式:

+

当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。

+

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let bar;

setTimeout(() => {
console.log('setTimeout');
}, 0)

setImmediate(() => {
console.log('setImmediate');
})
function someAsyncApiCall(callback) {
process.nextTick(callback);
}

someAsyncApiCall(() => {
console.log('bar', bar); // 1
});

bar = 1;

+

在NodeV10中上述代码执行可能有两种答案,一种为:

+
1
2
3
4
bar 1
setTimeout
setImmediate

+

另一种为

+
1
2
3
4
bar 1
setImmediate
setTimeout

+ +

无论哪种,始终都是先执行process.nextTick(callback),打印bar 1。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:搞懂EventLoop

+

文章作者:

+

发布时间:2022年03月11日 - 08:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/03/11/%E6%90%9E%E6%87%82EventLoop/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/03/11/\346\225\260\347\273\204\346\226\271\346\263\225\344\270\200\347\233\256\344\272\206\347\204\266/index.html" "b/ja/2022/03/11/\346\225\260\347\273\204\346\226\271\346\263\225\344\270\200\347\233\256\344\272\206\347\204\266/index.html" new file mode 100644 index 0000000000..779463dc38 --- /dev/null +++ "b/ja/2022/03/11/\346\225\260\347\273\204\346\226\271\346\263\225\344\270\200\347\233\256\344\272\206\347\204\266/index.html" @@ -0,0 +1,785 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 数组方法哪些有副作用,一目了然! | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 数组方法哪些有副作用,一目了然! +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

今天一个小兄弟微信给我发了个消息,说是今天面试,面试官问了个比较基础的问题,没答好,我就问他问了什么,他说,数组有哪些方法会改变原来的值,他说他就说了3、4个,还不确定对不对,我想了想说其实列个表就很容易记住了,于是便有了下面这个表格。

+ +

关于数组的那些方法

其实上面的问题:

+
+

数组有哪些方法会改变原来的值

+
+

现在有个词挺火的,叫做纯函数,什么是纯函数呢,就是没有副作用,什么是没有副作用呢,就是结果是幂等的,等等….怎么越来越绕,简单来说,就是这个函数干几次都是一样的结果,出了函数就不会有任何影响,他就像一台加工机器,输入原料,输出产品,没有任何其他输出材料,比如废气之类影响大气的东西。好吧,你不明白也没关系,反正我对上面这个问题的理解就变成了

+
+

数组的哪些方法是没有副作用的。

+
+

要说有哪些方法是有副作用,首先你得知道有哪些方法,

+

比如,push,pop,shift,unshift,splice 这些别跟我说你不知道,这些都是常用的数组操作,出栈入栈,
还有什么呢,数组的排序啊,比如sort,reverse,还有一个用的不多,fill是用一个固定值填充数组的起始元素到终止元素,这些呢都是在原数组上进行操作,所以肯定是影响数组的值的,也就是说有副作用的。

+

那么哪些没有副作用呢,简单说其他的都没有,
啥!?,是的,其他的都没有。。。所以记得上面这些就好了呀,怎么样,是不简单。

+

那还剩下啥?

+
    +
  • concat 返回的是心数组,
  • +
  • join 返回的是字符串,
  • +
  • indexOf和lastIndexOf返回的是索引,
  • +
  • slice 返回的是切割后的新数组,别把它和splice混淆了哈,
  • +
  • entries 返回一个新的Array Iterator对象,
  • +
  • keys 返回一个新的包含全部Array索引的 Iterator对象,
  • +
  • values 有keys就有values,返回一个新的包含全部Array值的 Iterator对象,
  • +
  • every、some、includes 返回 布尔值,
  • +
  • filter,map 返回新数组,
  • +
  • reducer 有点神奇 返回累加值,
  • +
  • find、findIndex 找元素 一个返回第一个符合的元素本身,一个返回他的索引,
  • +
  • 最后还有个flat,展平了数组,
  • +
  • forEach 遍历数组, 这么一数起来,方法还真不少呢!
  • +
+

总结

想要记住所有的方法呢,不下点功夫是不行的,但是想要记住有副作用的,然后排除掉他们的话,还是容易的,最后做个表,方便直观的来看。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
方法作用返回结果有无副作用
push数组尾部插入元素改变了的原数组
pop数组尾部删除元素改变了的原数组
shift数组头部插入元素改变了的原数组
unshift数组头部删除元素改变了的原数组
splice增删改数组改变了的原数组
sort数组排序改变了的原数组
reverse数组反序改变了的原数组
fill填充数组改变了的原数组
concat连结数组合并的新数组
join合并数组为字符串字符串
indexOf查索引第一个索引
lastIndexOf查索引最后一个索引
slice截取数组截取的新数组
entries迭代数组迭代器对象
keys迭代数组的key全部的key
values迭代数组的value全部的value
every每个都满足布尔值
some有一些满足布尔值
includes包含某元素布尔值
filter过滤数组新数组
map加工数组新数组
reducer迭代数组累加值
find查找元素第一个元素或undifined
findIndex查找元素第一个元素的index
flat展平数组新数组
forEach遍历数组void
+

就到这里,怎么样,清楚了吧。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:数组方法哪些有副作用,一目了然!

+

文章作者:

+

发布时间:2022年03月11日 - 07:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/03/11/%E6%95%B0%E7%BB%84%E6%96%B9%E6%B3%95%E4%B8%80%E7%9B%AE%E4%BA%86%E7%84%B6/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/06/01/CodeReview\344\273\243\347\240\201\345\256\241\346\237\245\346\214\207\345\215\227/index.html" "b/ja/2022/06/01/CodeReview\344\273\243\347\240\201\345\256\241\346\237\245\346\214\207\345\215\227/index.html" new file mode 100644 index 0000000000..a868557fe7 --- /dev/null +++ "b/ja/2022/06/01/CodeReview\344\273\243\347\240\201\345\256\241\346\237\245\346\214\207\345\215\227/index.html" @@ -0,0 +1,732 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CodeReview代码审查指南.md | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ CodeReview代码审查指南.md +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

1.关于Code Review

1.1 Code Review的目的

Code Review是一种用来确认方案设计和代码实现的质量保证机制,通过这个机制我们可以对代码、测试过程和注释进行检查。

+

Code Review主要用来在软件工程过程中改进代码质量,通过Code Review可以达到如下目的目的:

+
    +
  1. 在项目早期就能够发现代码中的BUG
  2. +
  3. 帮助初级开发人员学习高级开发人员的经验,达到知识共享
  4. +
  5. 避免开发人员犯一些很常见,很普通的错误
  6. +
  7. 保证项目组人员的良好沟通
  8. +
  9. 项目或产品的代码更容易维护
  10. +
+

1.2 Code Review的前提

进入Code Review需要检查的条件如下:

+
    +
  1. Code Review人员是否理解了Code Review的概念和Code Review将做什么
    如果做Code Review的人员不能理解Code Review对项目成败和代码质量的重要程度,他们的做法可能就会是应付了事。
  2. +
  3. 代码是否已经正确的build,build的目的使得代码已经不存在基本语法错误
    我们总不希望高级开发人员或是主管将时间浪费在检查连编译都通不过的代码上吧。
  4. +
  5. 代码执行时功能是否正确
    Code Review人员也不负责检查代码的功能是否正确,也就是说,需要复查的代码必须由开发人员或质量人员负责该代码的功能的正确性。
  6. +
  7. Review人员是否理解了代码
    做复查的人员需要对该代码有一个基本的了解,其功能是什么,是拿一方面的代码,涉及到数据库或是通讯,这样才能采取针对性的检查
  8. +
  9. 开发人员是否对代码做了单元测试
    这一点也是为了保证Code Review前一些语法和功能问题已经得到解决,Code Review人员可以将精力集中在代码的质量上。
  10. +
+

1.3 Code Review需要做什么

Code Review主要检查代码中是否存在以下方面问题:

+
    +
  • 代码的一致性
  • +
  • 编码风格
  • +
  • 代码的安全问题
  • +
  • 代码冗余
  • +
  • 是否正确设计以满足需求(性能、功能)
  • +
  • 等等
  • +
+

1.3.1 完整性检查(Completeness)

    +
  • 代码是否完全实现了设计文档中提出的功能需求
  • +
  • 代码是否已按照设计文档进行了集成和Debug
  • +
  • 代码是否已创建了需要的数据库,包括正确的初始化数据
  • +
  • 代码中是否存在任何没有定义或没有引用到的变量、常数或数据类型
  • +
+

1.3.2 一致性检查(Consistency)

    +
  • 代码的逻辑是否符合设计文档
  • +
  • 代码中使用的格式、符号、结构等风格是否保持一致
  • +
+

1.3.3 正确性检查(Correctness)

    +
  • 代码是否符合制定的标准
  • +
  • 所有的变量都被正确定义和使用
  • +
  • 所有的注释都是准确的
  • +
  • 所有的程序调用都使用了正确的参数个数
  • +
+

1.3.4 可修改性检查(Modifiability)

    +
  • 代码涉及到的常量是否易于修改(如使用配置、定义为类常量、使用专门的常量类等)
  • +
  • 代码中是否包含了交叉说明或数据字典,以描述程序是如何对变量和常量进行访问的
  • +
  • 代码是否只有一个出口和一个入口(严重的异常处理除外)
  • +
+

1.3.5 可预测性检查(Predictability)

    +
  • 代码所用的开发语言是否具有定义良好的语法和语义
  • +
  • 是否代码避免了依赖于开发语言缺省提供的功能
  • +
  • 代码是否无意中陷入了死循环
  • +
  • 代码是否是否避免了无穷递归
  • +
+

1.3.6 健壮性检查(Robustness)

    +
  • 代码是否采取措施避免运行时错误
      +
    • 数组边界溢出
    • +
    • 被零除
    • +
    • 值越界
    • +
    • 堆栈溢出
    • +
    • +
    +
  • +
+

1.3.7 结构性检查(Structuredness)

    +
  • 程序的每个功能是否都作为一个可辩识的代码块存在
    循环是否只有一个入口
  • +
+

1.3.8 可追溯性检查(Traceability)

    +
  • 代码是否对每个程序进行了唯一标识
  • +
  • 是否有一个交叉引用的框架可以用来在代码和开发文档之间相互对应
  • +
  • 代码是否包括一个修订历史记录,记录中对 代码的修改和原因都有记录
  • +
  • 是否所有的安全功能都有标识
  • +
+

1.3.9 可理解性检查(Understandability)

    +
  • 注释是否足够清晰的描述每个子程序
  • +
  • 是否使用到不明确或不必要的复杂代码,它们是否被清楚的注释
  • +
  • 使用一些统一的格式化技巧(如缩进、空白等)用来增强代码的清晰度
  • +
  • 是否在定义命名规则时采用了便于记忆,反映类型等方法
  • +
  • 每个变量都定义了合法的取值范围
  • +
  • 代码中的算法是否符合开发文档中描述的数学模型
  • +
+

1.3.10 可验证性检查(Verifiability)

代码中的实现技术是否便于测试

+

1.4 Code Review的步骤

这些是我在平时工作中的经验总结,目前也是按照这个步骤在做。

+
    +
  1. 代码编写者和代码审核者坐在一起,由代码编写者按照UC依次讲解自己负责的代码和相关逻辑,从Web层->DAO层;
  2. +
  3. 代码审核者在此过程中可以随时提出自己的疑问,同时积极发现隐藏的bug;对这些bug记录在案。
  4. +
  5. 代码讲解完毕后,代码审核者给自己安排几个小时再对代码审核一遍。
    代码需要一行一行静下心看。同时代码又要全面的看,以确保代码整体上设计优良。
  6. +
  7. 代码审核者根据审核的结果编写“代码审核报告”,“审核报告”中记录发现的问题及修改建议,然后把“审核报告”发送给相关人员。
  8. +
  9. 代码编写者根据“代码审核报告”给出的修改意见,修改好代码,有不清楚的地方可积极向代码审核者提出。
  10. +
  11. 代码编写者 bug fix完毕之后给出反馈。
  12. +
  13. 代码审核者把Code Review中发现的有价值的问题更新到”代码审核规范”的文档中,对于特别值得提醒的问题可群发email给所有技术人员。
  14. +
+

提示
Code Review必备的文档:

+
    +
  • “代码审核规范”文档:记录代码应该遵循的标准。
    +

    代码审核者根据这些标准来Code Review代码,同时在Code Review过程中不断完善该文档。

    +
    +
  • +
+

2.Code Reivew的执行

一个标准的Code Reivew活动应该分为三个阶段:

+

2.1.事前准备阶段

在一次CR前,对以下内容进行充分准备。

+

2.1.1.CR的对象

在准备CR代码对象时,我们要注意代码的数量,如果代码量比较大,要对代码进行必要的分解,确定其中的关键代码,对关键代码进行CR,可以达到举一反三的目的。

+

2.1.2.CR的内容

我们对代码的审查内容很多,如代码的编写是否规范(注释的书写格式、命名规范等)、技术处理规范(异常处理、日志处理、代码组织结构等)、业务实现等。我们不能希望通过一次CR活动,完成所有这些内容的审查,因此我们必须设定本次CR活动内容界限,确定审查重点;

+

2.1.3.评审规范和标准

在CR前设计确定评审规范和标准是必要,通过规范和标准我们在审查过程中可以有据可依,有理可循,而且还可以做到标准统一。

+

2.1.4.选择CR活动的参与者

在CR开始前,必须把本次CR活动的对象、审查内容以及审查的规范和标准通报给所有的参与者。

+

2.1.5.选择CR活动的实施方式。

CR活动有很多形式可供我们选择,我们可以根据实际情况选择桌面式CR、演示讲解式CR、一对一的座位CR等等。

+

2.2.实施阶段

充分的事前准备,只是做好CR活动的前提,在CR实施过程中,我们要做好以下工作。

+

2.2.1.准确记录

对于CR过程发现的问题,我们必须清晰准确的记录,可以使用问题点记录单,明确记录的项目和内容。

+

2.2.2.讲解与提问

CR过程中,要采用代码作者讲解和审查者提问方式。审查者不能只在发现问题时提问,同时也要根据本次审查的内容要求代码作者对某个特定问题的讲解。

+

2.2.3.逐项审查

对事前确定的审查内容,要逐项审查,不能因为时间不足等因素一扫而过。

+

2.2.4.注意气氛

实施审查时,要营造一个讨论问题、解决问题的氛围,不能把审查会搞成批判会,这样会影响相关人员的积极性。

+

2.3. 事后跟踪跟踪。

2.3.1. 确认发现的问题

CR结束后,对发现的问题,首先需要确定以下内容。

+
    +
  1. 问题点的难易程度以及影响的范围;
  2. +
  3. 解决问题的责任者和问题点修正结果的确认者;
  4. +
  5. 解决问题点的时限。
  6. +
+

2.3.2. 修正问题责任者

对于修正问题责任者,在问题点的修正过程中,要三方面内容的记录。

+
    +
  1. 问题点的原因;
  2. +
  3. 解决问题点的对策;
  4. +
  5. 修正的内容。
  6. +
+

2.3.3. 修正结果确认者

做为修正结果的确认者,必须按照事前约定的时限及时的对修正结果进行全面的确认

+

3.注意事项

3.1. 经常进行Code Review

    +
  1. 要Review的代码越多,那么要重构,重写的代码就会越多。而越不被程序作者接受的建议也会越多,唾沫口水战也会越多。
  2. +
  3. 程序员代码写得时候越长,程序员就会在代码中加入越来越多的个人的东西。
  4. +
  5. 越接近软件发布的最终期限,代码也就不能改得太多。
  6. +
+

3.2. Code Review不要太正式,而且要短

忘了那个代码评审的Checklist吧,走到你的同事座位跟前,像请师父一样请他坐到你的电脑面前,然后,花5分钟给他讲讲你的代码,给他另外一个5分钟让他给你的代码提提意见,这比什么都好。而如果你用了一个Checklist,让这个事情表现得很正式的话,下面两件事中必有一件事会发生:

+
    +
  1. 只有在Checklist上存在的东西才会被Review。
  2. +
  3. Code Reviews 变成了一种礼节性的东西,你的同事会装做很关心你的代码,但其实他心里想着尽快地离开你。
  4. +
+

只有不正式的Code Review才会让你和评审者放轻松,人只有放松了,才会表现得很真实,很真诚。记住Review只不过是一种形式,而只有在相互信任中通过相互的讨论得到了有意义和有建设性的建议和意见,那才是最实在的。不然,作者和评审者的关系就会变成小偷和警察的关系。

+

3.3. 尽可能的让不同的人Reivew你的代码

如果可能的话,不要总是只找一个人来Review你的代码,不同的人有不同的思考方式,有不同的见解,所以,不同的人可以全面的从各个方面评论你的代码。
但不要太多了,人多嘴杂反而适得其反,基本上来说,不要超过3个人,这是因为,这是一个可以围在一起讨论的最大人员尺寸。

+

下面是几个优点:

+
    +
  1. 从不同的方向评审代码总是好的。
  2. +
  3. 会有更多的人帮你在日后维护你的代码。
  4. +
  5. 这也是一个增加团队凝聚力的方法。
  6. +
+

3.4. 保持积极的正面的态度

程序员最大的问题就是“自负”,尤其当我们Reivew别人的代码的时候,我已经见过无数的场面,程序员在Code Review的时候,开始抨击别人的代码,质疑别人的能力。太可笑了,我分析了一下,这类的程序员其实并没有什么本事,因为他们指责对方的目的是想告诉大家自己有多么的牛,靠这种手段来表现自己的程序员,其实是就是传说中所说的“半瓶水”。

+

所以,无论是代码作者,还是评审者,都需要一种积极向上的正面的态度,作者需要能够虚心接受别人的建议,因为别人的建议是为了让你做得更好;评审者也需要以一种积极的正面的态度向作者提意见,因为那是和你在一个战壕里的战友。记住,你不是一段代码,你是一个人!

+

3.5. 学会享受Code Reivew

这可能是最重要的一个提示了,如果你到了一个人人都喜欢Code Reivew的团阿,那么,你会进入到一个生机勃勃的地方,在那里,每个人都能写出质量非常好的代码,在那里,你不需要经理的管理,团队会自适应一切变化,他们相互学习,相互帮助,不仅仅是写出好的代码,而且团队和其中的每个人都会自动进化,最关键的是,这个是一个团队。

+

资料来源

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:CodeReview代码审查指南.md

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/CodeReview%E4%BB%A3%E7%A0%81%E5%AE%A1%E6%9F%A5%E6%8C%87%E5%8D%97/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/06/01/Electron\347\274\226\347\240\201\350\247\204\350\214\203/index.html" "b/ja/2022/06/01/Electron\347\274\226\347\240\201\350\247\204\350\214\203/index.html" new file mode 100644 index 0000000000..ee1ee6e2cb --- /dev/null +++ "b/ja/2022/06/01/Electron\347\274\226\347\240\201\350\247\204\350\214\203/index.html" @@ -0,0 +1,634 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Electron编码规范.md | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Electron编码规范.md +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

目录
代码规范 2

+
    +
  1. 说明 2
  2. +
  3. 基本原则 2
  4. +
  5. C++与Python 2
  6. +
  7. 命名相关 2
  8. +
  9. 工程目录结构 3
  10. +
+

Electron代码规范

+
    +
  1. 说明
    Electron框架正如官网介绍是使用 JavaScript, HTML 和 CSS 构建跨平台的桌面应用的框架,框架分为前端和后端两个部分,根据前后端采用的技术不同分别遵循前后端所采用技术的代码规范
  2. +
  3. 基本原则
  4. +
+
    +
  1. 不要直接在渲染进程操作主进程,通过进程通信的方式进行数据及事件响应

    +
  2. +
  3. nodejs请使用require方式引入资源不使用import方式引入

    +
  4. +
  5. 多渲染进程传参请使用localStorage或sessionStorage

    +
  6. +
  7. 资源下载不下来时请直接到官网下载https://github.com/electron/electron/releases 将文件放到 用户目录\AppData\Local\electron\Cache 下 再进行 npm install -g electron

    +
  8. +
  9. 代码中包含native模块,必须进行 rebuild操作
    调试渲染进程时增加如下代码增加mainWindow.webContents.openDevTools();

    +
  10. +
  11. +
+
    +
  1. C++与Python
    对于 C++ 和 Python, 遵循 Chromium 的编码风格. 可以使用 clang-format 来自动格式化 C++ 代码. 可以使用 script/cpplint.py 来检验文件是否符合要求。
  2. +
+

现在使用的 Python 版本是 Python 2.7。

+

C++ 代码使用了大量 Chromium 的抽象和类型,因此建议使用者熟悉它们。 一个起步的好地方是 Chromium 的《重要的抽象概念和数据库结构》文档. 该文档提到一些特殊类型,范围类型(超出范围时自动释放其内存), 记录机制等。
4. 命名相关
Electron API 使用与 Node.js 相同的大小写方案:
当模块本身是class时, 比如 BrowserWindow, 使用 大驼峰.
当模块是一组 API 时, 比如 globalShortcut时,使用 小驼峰。
当 API 是对象的属性时, 并且它复杂到足以成为一个单独的块, 比如 win.webContents, 使用 小驼峰.
对于其他非模块API, 使用自然标题, 比如 Tag 或 Process Object.

+

当创建新的 API 时, 最好使用 getter 和 setter 而不是 jQuery 的一次性函数。 举个例子, .getText() 和 .setText(text) 优于 .text([text]).

+
    +
  1. 工程目录结构
    src - main
    // 必须

    +
      +
    • main.js // 入口
    • +
    • listen.js // 监听渲染进程通信
    • +
    • windows.js // 创建渲染进程
    • +
    • log.js // 日志log4js 插件
    • +
    +

    // 可选

    +
      +
    • utils.js
    • +
    • store.js
    • +
    +
  2. +
+
    +
  • api.js
  • +
  • render
  • +
+

main 目录下为主进程代码,render 目录下为渲染进程代码
渲染进程目录结构规范与前端项目目录结构规范一致

+

主进程中必须包含入口文件,监听进程文件和窗口创建文件
其他根据后端实际使用技术可选,nodejs作为后端的话 必须
6. 打包
Electron-packager 绿色可执行包
Electron-builder 压缩安装包

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Electron编码规范.md

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/Electron%E7%BC%96%E7%A0%81%E8%A7%84%E8%8C%83/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2022/06/01/GIT Standard/index.html b/ja/2022/06/01/GIT Standard/index.html new file mode 100644 index 0000000000..eddaded422 --- /dev/null +++ b/ja/2022/06/01/GIT Standard/index.html @@ -0,0 +1,654 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GIT Standard.md | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ GIT Standard.md +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

1、说明

项目是一个多人协同完成同一个目标的团队组织形式,在多人协作项目中,如果代码风格统一(前端代码规范和静态代码检查约束)、代码提交信息的说明准确规范(本章介绍),那么在项目开发过程及后期协作以及Bug处理时会更加方便。

+

在本文中,我将介绍大家如何利用工具及约定保证大家代码提交的统一性,从而提高大家的协同效率:

+
    +
  • commitlint: git 提交信息规范与验证
    +

    添加如ESLint的格式规范校验,规范comiit的格式,达到团队每个人的提交风格保持一直,保证提交信息的完整和准确性
    风格如下:

    +
    +
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
<type>(<scope>): <subject> // Header
// 空一行
<body> // 72
// 空一行
<footer> // 72

// 例
fix(登陆模块): 修复登录密码错误的提示

登录没有做友好型提示

修正后不存在此问题
+ +
    +
  • husky: 使git-hook更容易
    +

    husky继承了Git下所有的钩子,在触发钩子的时候,husky可以阻止不合法的commit,push等等。注意使用husky之前,必须先将代码放到git 仓库中,否则本地没有.git文件,就没有地方去继承钩子了。

    +
    +
  • +
  • standard-version: 自动生成CHANGELOG 并发布版本
  • +
+

2、git commit message 规范

commit message格式

1
2
3
4
5
类型(影响范围): 描述

问题描述

修复方式结果
+ + +

注意:
1.冒号后面有空格。
2.英文小括号。
3.正文和注脚前都要加空行。

+

type 类型

用于说明 commit 的类别,只允许使用下面13个标识。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
'feat', // feat:新增功能
'fix', // fix:bug 修复
'docs', // docs:文档更新
'style', // style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑)
'refactor', // refactor:重构代码(既没有新增功能,也没有修复 bug)
'test', // test:新增测试用例或是更新现有测试
'chore', // revert:回滚某个更早之前的提交
'revert', // build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交
'build', // build:打包生产环境代码
'ci', // ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交
'merge', // merge:分支合并 Merge branch ? of ?
'perf', // perf:性能, 体验优化
'chore' // chore:不属于以上类型的其他类型(构建过程或辅助工具的变动)
+ +

如果type为feat和fix,则该 commit 将肯定出现在 Change log 之中。

+

scope 影响范围

可根据项目组需要进行定制化设置,也可不做强制要求

+

subject 描述

subject是 commit 目的的简短描述,不超过50个字符,且结尾不加句号(.)。

+

body 详细描述

body 部分是对本次 commit 的详细描述,可以分成多行。下面是一个范例。

+
1
2
3
此次提交内容包括如下信息:
- 登录为空校验
- 密码错误提示
+

Footer 部分只用于两种情况。

+
    +
  • 不兼容变动(改变解决方案)
  • +
  • 关闭 Issue(回复bug)
  • +
+

3、使用工具校验commit是否符合规范

commitlint

3.1 commitlint安装

1
2
// npm 安装
npm install --save-dev @commitlint/{cli,config-conventional}
+ +

3.2 生成commitlint.config.js配置文件

执行echo命令创建配置文件,也可以手动创建配置文件,或者从已有的配置文件进行拷贝。

+
1
2
// 项目根目录创建commitlint.config.js
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
+ + +

3.3 在commitlint.config.js制定提交message规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// type(scope?): subject

// body?

// footer?
module.exports = {
// 继承自默认规则
extends: ['@commitlint/config-conventional'],
// 这里写自定义规则
rules: {
// 等级 [0 1 2]: 0 不使用规则 1 警告 2 错误
// 启用范围 always|never: 总是或者永不.
// 规则值: 规则对应的值

// 头部(包含type、scope、subject)
'header-case': [0, 'always', 'lower-case'],
'header-full-stop': [0, 'never', '.'],
'header-max-length': [0, 'always', 72],
'header-min-length': [0, 'always', 0],

// 提交类型
'type-case': [0, 'never'],
'type-empty': [2, 'never'],
'type-max-length': [0, 'always', Infinity],
'type-min-length': [0, 'always', 0],
'type-enum': [
2,
'always',
[
'feat', // feat:新增功能
'fix', // fix:bug 修复
'docs', // docs:文档更新
'style', // style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑)
'refactor', // refactor:重构代码(既没有新增功能,也没有修复 bug)
'test', // test:新增测试用例或是更新现有测试
'revert', // revert:回滚某个更早之前的提交
'build', // build:build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交,打包生产环境代码
'ci', // ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交
'merge', // merge:分支合并 Merge branch ? of ?
'perf', // perf:性能, 体验优化
'chore' // chore:不属于以上类型的其他类型(构建过程或辅助工具的变动)
]
],

// 影响范围
'scope-case': [0, 'always', 'lower-case'], // 书写格式
'scope-max-length': [0, 'always', Infinity], // 最长
'scope-min-length': [0, 'always', 0], // 最短
'scope-empty': [2, 'never'], // 影响范围:为空|永不
'scope-enum': [0, 'always', ['a', 'b']], // 影响范围:给出范围但是不在给定的列表内

// 简介
'subject-empty': [2, 'never'], // 简介不能为空
'subject-full-stop': [0, 'never', '.'], // 简介结尾以.结束
'subject-case': [0, 'never'],
'subject-max-length': [0, 'always', Infinity],
'subject-min-length': [0, 'always', 1],

// 正文
'body-leading-blank': [2, 'always'],
'body-max-length': [0, 'always', Infinity],
'body-max-line-length': [0, 'always', Infinity],
'body-min-length': [0, 'always', 1],

// 注脚
'footer-leading-blank': [2, 'always'],
'footer-max-length': [0, 'always', Infinity],
'footer-max-line-length': [0, 'always', Infinity],
'footer-min-length': [0, 'always', 1],

// 其他
'references-empty': [0, 'never'],
'signed-off-by': [0, 'always', 'Signed-off-by']
}
}
+ +

上面我们就完成了commitlint的安装与提交规范的制定。检验commit message的最佳方式是结合git hook,所以需要配合Husky

+

husky

3.4 husky安装

1
npm install husky --save-dev
+

3.5 husky 配置

安装成功后需要在项目下的package.json中配置

+
1
2
3
4
5
6
7
8
9
10
11
// package.json
{
"husky": {
"hooks": { // husky的钩子
"pre-commit": "npm run lint" // 提交前进行代码静态校验
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS" // 进行提交信息格式校验
}
},
"scripts": {}
...
}
+ +

3.5 husky 执行测试

最后我们可以正常的git操作

+
1
2
git add .
git commit -m ""
+ +

git commit的时候会触发commlint。下面演示下不符合规范提交示例:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
C:\lixg\git>git commit -m "thunisoft: abcde"

husky > npm run -s commitmsg (node v8.2.1)

⧗ input:
thunisoft(all): abcde

✖ type must be one of [feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert] [type-enum]
✖ found 1 problems, 0 warnings

husky > commit-msg hook failed (add --no-verify to bypass)

C:\lixg\git>
+ +

上面的提交看反馈消息得知是type格式没有符合限制的提交类型列表,所以提交失败,虾米啊我们把type改为feat再试一下

+
1
2
3
4
5
6
7
8
9
10
11
C:\lixg\git>git commit -m "feat(all): 新功能"

husky > npm run -s commitmsg (node v8.2.1)

⧗ input: feat: 新功能
✔ found 0 problems, 0 warnings

[develop 19dfhe] feat: 新功能
1 file changed, 1 insertion(+)

C:\lixg\git>
+ +

修改后格式符合规范,提交成功。

+

参考来源

+

版权声明

Copyright by lixuguang
未经授权,严禁转载。如需转载,请联系作者

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:GIT Standard.md

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/GIT%20Standard/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/06/01/Restful API \347\232\204\350\256\276\350\256\241\350\247\204\350\214\203/index.html" "b/ja/2022/06/01/Restful API \347\232\204\350\256\276\350\256\241\350\247\204\350\214\203/index.html" new file mode 100644 index 0000000000..b7d1442c8d --- /dev/null +++ "b/ja/2022/06/01/Restful API \347\232\204\350\256\276\350\256\241\350\247\204\350\214\203/index.html" @@ -0,0 +1,823 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Restful API 的设计规范.md | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Restful API 的设计规范.md +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

1. URI

URI 表示资源,资源一般对应服务器端领域模型中的实体类。

+

URI规范

    +
  1. 不用大写;
  2. +
  3. 用中杠-不用下杠_;
  4. +
  5. 参数列表要encode;
  6. +
  7. URI中的名词表示资源集合,使用复数形式。
  8. +
+

资源集合 vs 单个资源

URI表示资源的两种方式:资源集合、单个资源。

+

资源集合:

+
1
2
/zoos //所有动物园
/zoos/1/animals //id为1的动物园中的所有动物
+

单个资源:

+
1
2
/zoos/1 //id为1的动物园
/zoos/1;2;3 //id为1,2,3的动物园
+

避免层级过深的URI

/在url中表达层级,用于按实体关联关系进行对象导航,一般根据id导航。

+

过深的导航容易导致url膨胀,不易维护,如 GET /zoos/1/areas/3/animals/4,尽量使用查询参数代替路径中的实体导航,如GET /animals?zoo=1&area=3

+

对Composite资源的访问

服务器端的组合实体必须在uri中通过父实体的id导航访问。

+
+

组合实体不是first-class的实体,它的生命周期完全依赖父实体,无法独立存在,在实现上通常是对数据库表中某些列的抽象,不直接对应表,也无id。一个常见的例子是 User — Address,Address是对User表中zipCode/country/city三个字段的简单抽象,无法独立于User存在。必须通过User索引到Address:GET /user/1/addresses

+
+

2. Request

HTTP方法

通过标准HTTP方法对资源CRUD:

+

GET:查询

+
1
2
3
GET /zoos
GET /zoos/1
GET /zoos/1/employees
+ + +

POST:创建单个资源。POST一般向“资源集合”型uri发起

+
1
2
POST /animals  //新增动物
POST /zoos/1/employees //为id为1的动物园雇佣员工
+ +

PUT:更新单个资源(全量),客户端提供完整的更新后的资源。与之对应的是 PATCH,PATCH 负责部分更新,客户端提供要更新的那些字段。PUT/PATCH一般向“单个资源”型uri发

+
1
2
PUT /animals/1
PUT /zoos/1
+ +

DELETE:删除

+
1
2
3
DELETE /zoos/1/employees/2
DELETE /zoos/1/employees/2;4;5
DELETE /zoos/1/animals //删除id为1的动物园内的所有动物
+

HEAD / OPTION 用的不多,就不多解释了。

+

安全性和幂等性

    +
  1. 安全性:不会改变资源状态,可以理解为只读的;
  2. +
  3. 幂等性:执行1次和执行N次,对资源状态改变的效果是等价的。
  4. +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
.安全性幂等性
GET
POST××
PUT×
DELETE×
+

安全性和幂等性均不保证反复请求能拿到相同的response。以 DELETE 为例,第一次DELETE返回200表示删除成功,第二次返回404提示资源不存在,这是允许的。

+

复杂查询

查询可以捎带以下参数:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
.示例备注
过滤条件type=1&age=16允许一定的uri冗余,如/zoos/1与/zoos?id=1
排序?sort=age,desc
投影?whitelist=id,name,email
分页?limit=10&offset=3
+

Bookmarker

经常使用的、复杂的查询标签化,降低维护成本。

+

如:

+
1
2
GET /trades?status=closed&sort=created,desc

+

快捷方式:

+
1
2
3
GET /trades#recently-closed
// 或者
GET /trades/recently-closed
+ +

Format

只用以下常见的3种body format:

+
    +
  1. Content-Type: application/json
  2. +
+
1
2
3
4
5
6
7
8
9
10
POST /v1/animal HTTP/1.1
Host: api.example.org
Accept: application/json
Content-Type: application/json
Content-Length: 24

{
"name": "Gir",
"animalType": "12"
}
+ +
    +
  1. Content-Type: application/x-www-form-urlencoded (浏览器POST表单用的格式)
  2. +
+
1
2
3
4
5
6
7
POST /login HTTP/1.1
Host: example.com
Content-Length: 31
Accept: text/html
Content-Type: application/x-www-form-urlencoded

username=root&password=Zion0101
+ +
    +
  1. Content-Type: multipart/form-data; boundary=—-RANDOM_jDMUxq4Ot5 (表单有文件上传时的格式)
  2. +
+

6. Response

    +
  1. 不要包装:
    response 的 body 直接就是数据,不要做多余的包装。
  2. +
+

错误示例

+
1
2
3
4
{
"success":true,
"data":{"id":1,"name":"xiaotuan"},
}
+

各HTTP方法成功处理后的数据格式:

+ + + + + + + + + + + + + + + + + + + + + + + +
·response 格式
GET单个对象、集合
POST新增成功的对象
PUT/PATCH更新成功的对象
DELETE
+
    +
  1. json格式的约定:

    +
      +
    1. 时间用长整形(毫秒数),客户端自己按需解析(moment.js)
    2. +
    3. 不传null字段
    4. +
    +
  2. +
+

分页response

1
2
3
4
{
"paging":{"limit":10,"offset":0,"total":729},
"data":[{},{},{}...]
}
+

7. 错误处理

    +
  1. 不要发生了错误但给2xx响应,客户端可能会缓存成功的http请求;
  2. +
  3. 正确设置http状态码,不要自定义;
  4. +
  5. Response body 提供
      +
    1. 错误的代码(日志/问题追查);
    2. +
    3. 错误的描述文本(展示给用户)。
    4. +
    +
  6. +
+

对第三点的实现稍微多说一点:

+

Java 服务器端一般用异常表示 RESTful API 的错误。API 可能抛出两类异常:业务异常非业务异常

+

业务异常由自己的业务代码抛出,表示一个用例的前置条件不满足、业务规则冲突等,比如参数校验不通过、权限校验失败。

+

非业务类异常表示不在预期内的问题,通常由类库、框架抛出,或由于自己的代码逻辑错误导致,比如数据库连接失败、空指针异常、除0错误等等。

+

业务类异常必须提供2种信息:

+
    +
  1. 如果抛出该类异常,HTTP 响应状态码应该设成什么;
  2. +
  3. 异常的文本描述;
  4. +
+

在Controller层使用统一的异常拦截器:

+
    +
  1. 设置 HTTP 响应状态码:对业务类异常,用它指定的 HTTP code;对非业务类异常,统一500;
  2. +
  3. Response Body 的错误码:异常类名
  4. +
  5. Response Body 的错误描述:对业务类异常,用它指定的错误文本;对非业务类异常,线上可以统一文案如“服务器端错误,请稍后再试”,开发或测试环境中用异常的 stacktrace,服务器端提供该行为的开关。
  6. +
+

常用的http状态码及使用场景:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
状态码使用场景
400 bad request常用在参数校验
401 unauthorized未经验证的用户,常见于未登录。如果经过验证后依然没权限,应该 403(即 authentication 和 authorization 的区别)。
403 forbidden无权限
404 not found资源不存在
500 internal server error非业务类异常
503 service unavaliable由容器抛出,自己的代码不要抛这个异常
+

8. 服务型资源

除了资源简单的CRUD,服务器端经常还会提供其他服务,这些服务无法直接用上面提到的URI映射。如:

+
    +
  1. 按关键字搜索;
  2. +
  3. 计算地球上两点间的距离;
  4. +
  5. 批量向用户推送消息;
  6. +
+

可以把这些服务看成资源,计算的结果是资源的presentation,按服务属性选择合适的HTTP方法。

+

例:

+
1
2
3
4
GET /search?q=filter?category=file  搜索
GET /distance-calc?lats=47.480&lngs=-122.389&late=37.108&lnge=-122.448
POST /batch-publish-msg
[{"from":0,"to":1,"text":"abc"},{},{}...]
+

9. 异步任务

对耗时的异步任务,服务器端接受客户端传递的参数后,应返回创建成功的任务资源,其中包含了任务的执行状态。客户端可以轮训该任务获得最新的执行进度。

+
1
2
3
4
5
6
7
8
9
// 提交任务:
POST /batch-publish-msg
[{"from":0,"to":1,"text":"abc"},{},{}...]

// 返回:
{"taskId":3,"createBy":"Anonymous","status":"running"}

GET /task/3
{"taskId":3,"createBy":"Anonymous","status":"success"}
+ +

如果任务的执行状态包括较多信息,可以把“执行状态”抽象成组合资源,客户端查询该状态资源了解任务的执行情况。

+
1
2
3
4
5
6
7
8
9
// 提交任务:
POST /batch-publish-msg
[{"from":0,"to":1,"text":"abc"},{},{}...]

// 返回:
{"taskId":3,"createBy":"Anonymous"}

GET /task/3/status
{"progress":"50%","total":18,"success":8,"fail":1}
+ +

10. API的演进

版本

常见的三种方式:

+
    +
  1. 在uri中放版本信息:GET /v1/users/1
  2. +
  3. Accept Header:Accept: application/json+v1
  4. +
  5. 自定义 Header:X-Api-Version: 1
  6. +
+

用第一种,虽然没有那么优雅,但最明显最方便。

+

URI失效

随着系统发展,总有一些API失效或者迁移,对失效的API,返回404 not found 或 410 gone;对迁移的API,返回 301 重定向。

+

11. 安全

这个不熟,接触到的时候再说。

+

参考文档

    +
  • < RESTful Web Services Cookbook >
  • +
  • Consumer-Centric API Design
  • +
  • RESTful Best Practices
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Restful API 的设计规范.md

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/Restful%20API%20%E7%9A%84%E8%AE%BE%E8%AE%A1%E8%A7%84%E8%8C%83/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2022/06/01/squid/index.html b/ja/2022/06/01/squid/index.html new file mode 100644 index 0000000000..d67a0e26aa --- /dev/null +++ b/ja/2022/06/01/squid/index.html @@ -0,0 +1,606 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Squid 服务器学习笔记 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Squid 服务器学习笔记 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Squid 服务器学习笔记

Squid 服务器介绍

用来缓存前端资源的代理服务器,可以类比后端数据库缓存软件Redis。
当用户访问目标资源时由Squid服务器进行判断如果有数据缓存直接将缓存数据返回,如果没有缓存,代理客户访问资源,获取到后返回给用户,当有其他用户访问相同资源时直接返回资源。

+

代理方式

    +
  1. 普通代理
  2. +
  3. 透明代理
  4. +
  5. 反向代理
  6. +
+

安装

1
2
3
4
5
6
7
8
9
10
11
12
// 检查是否已经安装
rpm -qa | grep squid
// 安装软件
rpm install squid
// 设置开机启动
chkconfig squid on
// 启动服务
service squid start
// 查看服务状态(端口监听3128)
netstat -anput |grap squid
// 关闭服务
service squid stop
+ +

配置文件

/etc/squid/squid.config

+

10.0.0.0/8

+
+

扩展知识
CIDR - 无类域间路由CIDR(Classless InterDomain Routing)
ip每位占1个字节8位,00000000 ~ 11111111,也就是0 ~ 255,ip共有四个位数。
10.0.0.0/8 的意思是 8位不可变,剩下的可变,8位这里的意思是说第一个IP段,那么也就是说IP范围是10.0.0.0 ~ 10.255.255.255 这样的一个ip范围。

+
+

// 复制并重命名
mv /etc/squid/squid.config{,.bak}
// 删除文件中的注释和空行(只保留有效设定)
awk ‘if($0!~ /^#/ && $0~ !/^$/print $0)’ /etc/squid/squid.config.bak > /etc/squid/squid.config

+

squid常用命令:
/usr/local/squid/sbin/squid -z 初始化缓存空间
/usr/local/squid/sbin/squid 启动
/usr/local/squid/sbin/squid -k shutdown 停止
/usr/local/squid/sbin/squid -k reconfigure 重新载入配置文件
/usr/local/squid/sbin/squid -k rotate 轮循日志

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#acl all src 0.0.0.0/0.0.0.0 and http_access allow all选项定义了一个访问控制列表。详细情况参见和Squid软件
#携带的文档。这里的访问控制列表允许所有对代理服务的访问,因为这里该代理是加速web服务器。
acl all src 0.0.0.0/0.0.0.0
#允许所有IP访问
acl manager proto http #manager url协议为http
acl localhost src 127.0.0.1/255.255.255.255 #允午本机IP
acl to_localhost dst 127.0.0.1 #允午目的地址为本机IP
acl Safe_ports port 80 # 允许安全更新的端口为80
acl CONNECT method CONNECT #请求方法以CONNECT
http_access allow all #允许所有人使用该代理.因为这里是代理加速web服务器
http_reply_access allow all #允许所有客户端使用该代理

acl OverConnLimit maxconn 16 #限制每个IP最大允许16个连接,防止攻击
http_access deny OverConnLimit

icp_access deny all #禁止从邻居服务器缓冲内发送和接收ICP请求.
miss_access allow all #允许直接更新请求
ident_lookup_access deny all #禁止lookup检查DNS
http_port 8080 transparent #指定Squid监听浏览器客户请求的端口号。

hierarchy_stoplist cgi-bin ? #用来强制某些特定的对象不被缓存,主要是处于安全的目的。
acl QUERY urlpath_regex cgi-bin \?
cache deny QUERY

cache_mem 1 GB #这是一个优化选项,增加该内存值有利于缓存。应该注意的是:
#一般来说如果系统有内存,设置该值为(n/)3M。现在是3G 所以这里1G
fqdncache_size 1024 #FQDN 高速缓存大小
maximum_object_size_in_memory 2 MB #允许最大的文件载入内存

memory_replacement_policy heap LFUDA #动态使用最小的,移出内存cache
cache_replacement_policy heap LFUDA #动态使用最小的,移出硬盘cache

cache_dir ufs /home/cache 5000 32 512 #高速缓存目录 ufs 类型使用的缓冲值最大允午1000MB空间,
#32个一级目录,512个二级目录

max_open_disk_fds 0 #允许最大打开文件数量,0 无限制
minimum_object_size 1 KB #允午最小文件请求体大小
maximum_object_size 20 MB #允午最大文件请求体大小

cache_swap_low 90 #最小允许使用swap 90%
cache_swap_high 95 #最多允许使用swap 95%

ipcache_size 2048 # IP 地址高速缓存大小 2M
ipcache_low 90 #最小允许ipcache使用swap 90%
ipcache_high 95 #最大允许ipcache使用swap 90%


access_log /var/log/squid/access.log squid #定义日志存放记录
cache_log /var/log/squid/cache.log squid
cache_store_log none #禁止store日志

emulate_httpd_log on #将使Squid仿照Web服务器的格式创建访问记录。如果希望使用
#Web访问记录分析程序,就需要设置这个参数。

refresh_pattern . 0 20% 4320 override-expire override-lastmod reload-into-ims ignore-reload #更新cache规则

acl buggy_server url_regex ^http://.... http:// #只允许http的请求
broken_posts allow buggy_server

acl apache rep_header Server ^Apache #允许apache的编码
broken_vary_encoding allow apache

request_entities off #禁止非http的标分准请求,防止攻击
header_access header allow all #允许所有的http报头
relaxed_header_parser on #不严格分析http报头.
client_lifetime 120 minute #最大客户连接时间 120分钟

cache_mgr sky@test.com #指定当缓冲出现问题时向缓冲管理者发送告警信息的地址信息。

cache_effective_user squid #这里以用户squid的身份Squid服务器
cache_effective_group squid

icp_port 0 #指定Squid从邻居服务器缓冲内发送和接收ICP请求的端口号。
#这里设置为0是因为这里配置Squid为内部Web服务器的加速器,
#所以不需要使用邻居服务器的缓冲。0是禁用

cache_peer 127.0.0.1 parent 80 0 no-query default multicast-responder no-netdb-exchange #cache_peer 设置允许更新缓存的主机,因是本机所以127.0.0.1
cache_peer_domain 127.0.0.1
hostname_aliases 127.0.0.1

error_directory /usr/share/squid/errors/Simplify_Chinese #定义错误路径

always_direct allow all # cache丢失或不存在是允许所有请求直接转发到原始服务器
ignore_unknown_nameservers on #开反DNS查询,当域名地址不相同时候,禁止访问
coredump_dir /var/log/squid #定义dump的目录

max_filedesc 2048 #最大打开的文件描述

half_closed_clients off #使Squid在当read不再返回数据时立即关闭客户端的连接。
#有时read不再返回数据是由于某些客户关闭TCP的发送数据
#而仍然保持接收数据。而Squid分辨不出TCP半关闭和完全关闭。

buffered_logs on #若打开选项“buffered_logs”可以稍稍提高加速某些对日志文件的写入,该选项主要是实现优化特性。

acl tianya referer_regex -i tianya #防止天涯盗链,转嫁给百度
http_access deny tianya
deny_info tianya

acl baidu req_header User-Agent Baiduspider #阻止baidu蜘蛛
http_access deny baidu

acl OverConnLimit maxconn 128 #限制同一IP客户端的最大连接数
http_access deny OverConnLimit

acl myip dst 222.18.63.37 #防止被人利用为HTTP代理,设置允许访问的IP地址
http_access deny !myip

acl Manager proto cache_object #允许本地管理
acl Localhost src 127.0.0.1 222.18.63.37
http_access allow Manager Localhost
cachemgr_passwd 53034338 all
http_access deny Manager

acl all src 0.0.0.0/0.0.0.0 #仅仅允许80端口的代理
acl Safe_ports port 80 # http
http_access deny !Safe_ports
http_access allow all

visible_hostname happy.swjtu.edu.cn #Squid信息设置
cache_mgr ooopic2008@qq.com

cache_effective_user squid #基本设置
cache_effective_group squid
tcp_recv_bufsize 65535 bytes

cache_peer 127.0.0.1 parent 80 0 no-query originserver #2.6的反向代理加速配置

error_directory /usr/local/squid/share/errors/Simplify_Chinese #错误文档

icp_port 0 #单台使用,不使用该功能

hierarchy_stoplist cgi-bin ?

acl QUERY urlpath_regex cgi-bin \? .php .cgi .avi .wmv .rm .ram .mpg .mpeg .zip .exe
cache deny QUERY

acl apache rep_header Server ^Apache
broken_vary_encoding allow apache


refresh_pattern ^ftp: 1440 20% 10080
refresh_pattern ^gopher: 1440 0% 1440
refresh_pattern . 0 20% 4320

cache_store_log none
pid_filename /usr/local/squid/var/logs/squid.pid
emulate_httpd_log on
logformat combined %>a %ui %un [%tl] "%rm %ru HTTP/%rv" %Hs %<st "%{Referer}>h" "%{User-Agent}>h" %Ss:%Sh
cache_log /usr/local/squid/var/logs/cache.log
access_log /usr/local/squid/var/logs/access.log combined
coredump_dir /usr/local/squid/var/cache
cache_dir ufs /usr/local/squid/var/cache 10000 16 256

dns_children 32
hosts_file /etc/hosts

cache_mem 400 MB
cache_swap_low 90
cache_swap_high 95
maximum_object_size 32768 KB
maximum_object_size_in_memory 4096 KB
emulate_httpd_log on

acl picurl url_regex -i \.bmp$ \.png$ \.jpg$ \.gif$ \.jpeg$ #防止盗链
acl mystie1 referer_regex -i happy.swjtu.edu.cn
http_access allow mystie1 picurl
acl nullref referer_regex -i ^$
http_access allow nullref
acl hasref referer_regex -i .+
http_access deny hasref picurl
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Squid 服务器学习笔记

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/squid/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\346\214\207\345\215\227/index.html" "b/ja/2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\346\214\207\345\215\227/index.html" new file mode 100644 index 0000000000..deefaee420 --- /dev/null +++ "b/ja/2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\346\214\207\345\215\227/index.html" @@ -0,0 +1,728 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前后端分离开发指南 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前后端分离开发指南 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

背景

前后端分离狭义上来讲是以浏览器为分界,应用在浏览器内部的技术为前端技术,主要负责页面展示的部分,应用在浏览器外的技术为后端技术,主要负责处理业务逻辑和数据准备工作,但是随着近几年NodeJs的崛起,前端的工作范畴明显扩大,前后端分离也不仅仅作为一种开发模式,更是web应用架构层面的一种模式。

+

认识

    +
  1. 在开发阶段,前后端工程师要提前约定好数据交互的接口,从而实现并行开发和测试;
  2. +
  3. 在运行阶段,前后端分离模式需要对web应用进行分离部署,前后端之前使用HTTP或者其他协议进行交互请求。(NodeJs或Ngnix)
  4. +
+

由上述两条可以看出前后端分离分为开发阶段和部署阶段,通常来说我们做到的只是前后端分离开发,并没有做到分离部署。

+

分解

作为一种全新的架构模式,前后端分离需要从四个方面来比较和认识。

+
    +
  1. 交互形式
  2. +
  3. 开发模式/流程
  4. +
  5. 代码组织方式
  6. +
  7. 数据接口规范流程
  8. +
+

一、交互形式

在前后端分离架构中,后端只需要负责按照约定的数据格式向前端提供可调用的API服务即可。前后端之间通过HTTP请求进行交互,前端获取到数据后,进行页面的组装和渲染,最终返回给浏览器。

+

这里有一个经常引起激烈讨论的话题就是,“约定的数据格式”,是后端直接提供给前端可直接使用的数据格式,还是简单的数据库拉取数据不做加工直接推给前端,这个要视具体的项目情况而定,如果后端代码需要支持的不仅仅是一端,比如要同时适配pc端、手机端等多个终端,其多终端数据展现的形式如果各不相同,那后端只需要提供满足多端的基础数据格式即可,但如果只需要满足单个终端,那么建议直接将前后端数据格式进行统一处理,方便前后端协同,数据处理可以放在java端做也可以放在NodeJs端做,视人员配备情况而定,但绝不是后端不做任何处理就将数据返回给前端,完全由前端组织数据。

+

二、开发模式/流程

在前后端分离模式未诞生之前,前后端属于一个整体,那时使用的是MVC架构模式,前端对应的就是View层,主要通过html/css/js实现静态页面和动态效果,在有后端进行模板变量的嵌套和一些页面逻辑的处理,最终打包成一个整体,部署到同一服务器上,同时会进行简单的动静态分离部署。

+

此时开发的流程如下:
需求=》前后端并行开发=》前端开发静态页面=》后端套模板=》集成问题=》前后端调整=》再集成=》解决集成问题=》交付上线
// TODO 这里插图

+

出现前后端分离架构之后,前端工程师只需要编写前端页面+前端数据、业务逻辑处理,之后通过HTTP或其他请求方式调用后端提供的服务接口就可以了,而且除了在开发周期可以进行前后端分离,在部署阶段,前后端也可进行分离部署。
// TODO 这里插图

+

此时开发的流程如下:
需求=》设计接口、约定数据=》前后端并行开发=》集成=》调整=》集成成功=》交付上线

+

通过上面的描述及流程,不难发现,前后端分离的开发方式不仅仅从分工上进行了区分,更重要的是在并行开发的问题上解决了反复集成等前后端互相影响的问题,从而降低了开发的难度,简化了开发的流程。

+

三、代码组织方式

// TODO 这里插图
在传统的开发模式架构下,前端代码是作为项目的静态资源存在于项目工程下,页面中还夹着一些后端代码如jsp、php等技术,前后端开发时需要将整个项目代码完整的引入开发工具才能进行开发,前后端同时维护一份代码,这种开发方式导致前后端代码互相影响,因此前后端分离势在必行。

+

而前后端分离模式在代码的组织形式上由以下两种形式组成:

+
    +
  1. 半分离
    前后端仍共用一个代码库,但是代码分别存放在两个工程中。后端不关心或很少 关心前端元素的输出情况,前端不能独立进行开发和测试,项目中缺乏前后端 交互的测试用例。
  2. +
  3. 完全分离
    完全分离后,前端代码可以通过Mock来模拟后端请求,从此可以独立进行前端开发和测试。后端代码只需要按照跟前端约定好的接口格式写出完整的测试用例,确保接口的可用性。通过上述手段,降低开发集成风险。
  4. +
+

四、数据接口规范流程

通过上面三段的描述,我们可以看出前后端分离开发模式最重要也是最初的阶段就是数据接口的确定,因此在项目开发前必需先进行数据和接口的定义,数据接口的定义需要前后端开发共同商定,包括确定的数据格式,交互形式,并生成一份接口文档供前后端开发人员使用。之后才是并行开发。开发期间前后端双方需要严格按照确定的数据接口文档进行开发,前端开发完之后可以利用mock服务独自进行接口测试,后端也可以利用postman或其他接口测试工具进行测试,并提供完整的接口测试用例,然后前后端进行功能联调,最后再提交线上测试,也可进行自动化测试。

+

// TODO 这里插图

+

分离后的收益

到底分不分,如何分是个持续讨论的话题,通过上述的内容大家已经了解到了,什么是前后端分离,也知道如何进行前后端分离开发部署,那么前后端分离能带来哪些收益呢?

+

首先,就目前的软件开发应用趋势来看,越来越注重用户的体验性,而且架构越来越大,服务越来越小,而且终端设备越来越丰富,而原来不分离的方式已经不能支撑现在的发展趋势,因此前后端分离开发及部署将势在必行。

+

而且采用前后端分离的架构之后,我们将有如下几点提升:

+
    +
  1. 前后端分离后,前后端将不再互相纠缠而是各自在自己熟悉的领域进行开发工作,这将有利于前后端深化优化各自的代码,培养各自独特的技术特性,从而开发出更加优秀的应用,建立起专业精良的全栈开发团队。

    +
  2. +
  3. 通过前后端分离架构可以实现前后端开发从代码及开发流程上的完全解耦,只需要前后端共同商定好接口后,便可完全独立开发,只需要在联调阶段进行好协作,在此之前可以互不影响的进行并行开发,即是需求发生了变动,但只要不影响接口,后端既可以不用修改代码,只需前端进行变动即可,如此整体的开发效率将得到提升。

    +
  4. +
  5. 前后端分离后,能够更好的适应前端日益增多的的终端适配,代码解耦后复用率更高。

    +
  6. +
+

4.前后端分离后,前后端代码可以分别管理,代码不再混在一起,代码可维护性也增强了

+

前后端分离后收益不止以上四点,因为分离而带来的职责上、技术上、代码上、部署上的解耦让开发工作比以往任何时候都要更加专注和轻松。

+

注意事项

前后端分离误区

    +
  1. 前端人员不充足,不能进行前后端分离。
  2. +
+

此话说来是因为对前后端分离后职责区分不明确导致的问题,因为以往的前端只需要写静态页面就可以了,而前后端分离后前端也不仅仅需要写静态页面,而且还要为页面提供数据和页面逻辑的处理,但实际上可以根据团队情况来区别对待,如果团队前端人员充足,那么可以由前端人员负责多一些的工作,比如API请求后的业务逻辑的处理,页面逻辑的处理、页面数据的准备等,如果后端人员配备充足,那么上述几个环节仍然可由后端人员进行处理,前端开发仍然只是写静态页面,只是内容和逻辑不再写死而是通过js或其他手段如mvvm的框架进行处理。

+
    +
  1. 前后端分离后前端任务加重,职责也不清晰。
  2. +
+

如第一点描述可知,问题不在前后端分离的模式是否合适,而是任务分配和人员分配上的问题,如果前端能力强且人员比例较多,那么部分任务可以由前端承担,如果后端人员多,那么任务由后端承担。

+
    +
  1. 后端开发需要增加接口开发工作,增加任务量。
  2. +
+

无论如何后端开发都是需要写接口的,只是前后端分离后需要按照ResetFul风格写接口,或者采用最新的GraphQl的方式进行交互,如果说这个阶段需要前后端进行商量确定接口交互形式和数据格式花费了时间,但是在接下来前后端并行开发及问题解决上省掉的时间是更加可观的。

+
    +
  1. 分离后仍出现互相等待的问题,反而不如传统开发模式快。
  2. +
+

这个问题的产生其实也是由于对前后端分离后技术缺失导致的,常见情况是前端写完页面逻辑和假数据后后端开发还未完成接口开发导致无法进行联调,实际上前端通过mockserver等方式是可以解决一些问题的。

+

前后端分离适用场景

现代化的web应用适合用前后端分离的开发方式。
原因有以下几点:

+
    +
  1. web应用前端页面交互复杂。
      +
    • 页面渲染数据量大。
    • +
    • 页面包含复杂的业务逻辑。
    • +
    +
  2. +
  3. 终端适配情况多。
  4. +
  5. 分布式架构,微服务化应用场景。
  6. +
+

前后端分离具体方案

总体方向

后端专注于:后端控制层(Restful API) & 服务层 & 数据访问层;

+

前端专注于:前端控制层(Nodejs) & 视图层

+
    +
  1. 项目设计阶段,前后端架构负责人将项目整体进行分析,讨论并确定API风格、职责分配、开发协助模式,确定人员配备;设计确定后,前后端人员共同制定开发接口。

    +
  2. +
  3. 项目开发阶段,前后端分离是各自分工,协同敏捷开发,后端提供Restful API,并给出详细文档说明,前端人员进行页面渲染前台的任务是发送API请(GET,PUT,POST,DELETE等)获取数据(json,xml)后渲染页面。

    +
  4. +
  5. 项目测试阶段,API完成之前,前端人员会使用mock server进行模拟测试,后端人员采用junit进行API单元测试,不用互相等待;API完成之后,前后端再对接测试一下就可以了,当然并不是所有的接口都可以提前定义,有一些是在开发过程中进行调整的。

    +
  6. +
  7. 项目部署阶段,利用nginx 做反向代理,即Java + nodejs + nginx 方式进行。

    +
  8. +
+

技术手段

    +
  • 前端技术栈:前端代码 + mock服务
  • +
  • 后端技术栈:postman + 接口 + 后端业务逻辑 + 数据库
  • +
  • 公共依赖:接口文档/接口测试工具
  • +
+

常用的mock服务为jsonserver
常用的接口测试工具为postman、rap、swagger、doclever

+

部署方案

    +
  1. 第一阶段为前后端同一个代码库,同一个服务器,集中部署。
  2. +
  3. 第二阶段引入Ngnix服务作为中间件,前端向Ngnix发请求,Ngnix向后端服务发请求,由于Ngnix为静态服务器,所以在seo优化上和页面性能优化上效果不明显,因此前端仍需与后端进行配合才能达到整体的优化。
  4. +
+

浏览器 =》 Ngnix(前端机)=》Ngnix(后端机可没有)=》Server服务

+
    +
  1. 第三阶段引入nodejs作为中间层,将前端资源部署到Server层。同时实现数据代理服务,负责与提供数据的后端进行通信。
  2. +
+

浏览器=》Ngnix(前端机)=》NodeServer =》Server服务

+

浏览器向前端机发送请求,由Ngnix进行分发,url统一分发至NodeServer,在Node Server中根据请求类型从后端服务器上通过RPC服务请求页面的模板数据,然后进行页面的组装和渲染;API请求则直接转发到后端服务挖成相应。

+

前后端分离部署方案比较

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
属性字段传统模式Ngnix+ServerNode+ServerNgnix+Node+Server
SEOoknookok
浏览器渲染负担oknookok
前后端耦合nookokok
请求相应效率nooknook
+

结语

随着前端技术的快速发展和对用户体验日益增长的需求,前后端分离模式势必将会成为主流趋势。无论是从开发模式的角度上来说,还是对团队成长的角度上来说,前后端分离都会带来益处,让我们一同拥抱前后端分离,打造精良的开发团队,迎接日益复杂的web应用开发需求。

+

参考来源

+

版权声明

Copyright by lixuguang
未经授权,严禁转载。如需转载,请联系作者

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前后端分离开发指南

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\350\247\204\350\214\203/index.html" "b/ja/2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\350\247\204\350\214\203/index.html" new file mode 100644 index 0000000000..a088a91e75 --- /dev/null +++ "b/ja/2022/06/01/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\350\247\204\350\214\203/index.html" @@ -0,0 +1,660 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前后端分离开发规范 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前后端分离开发规范 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前后端分离开发规范

by lixg

+

(本文所说的前后端工作皆站在工作内容本身,不与职称相关,下面皆如此。)

+

一、为什么要前后端分离

    +
  1. 前后端专注于各自擅长的领域
  2. +
  3. 前端配置后端代码运行环境,节省搭建环境的时间
  4. +
  5. 明确前后端工作职责
  6. +
  7. 提高开发效率
  8. +
  9. 分离有助于前端、后端分别优化
  10. +
+

二、前后端分离存在的问题

    +
  1. 前后端分离必须接口先行,无接口导致前后端关于接口的工作重复出现。
  2. +
  3. 接口对接方式不一致,各项目团队没有采用统一的接口对接方式,导致每次都要熟悉一种新的对接方式。
  4. +
  5. 数据传输格式不一致,导致每次都要重新熟悉,另外由于接口格式不一致还会引起其他未知问题,影响系统健壮性。
  6. +
  7. 人员比例问题,目前公司前后端比例不完全符合前后端分离开发的要求。
  8. +
+

为解决上述问题,提高前后端分离开发效率,特制定如下规范。

+

三、如何做分离

    +
  1. 职责分离
      +
    • 前后端仅仅通过异步接口(AJAX/JSONP)来编程
    • +
    • 前后端都各自有自己的开发流程,构建工具,测试集合
    • +
    • 关注点分离,前后端变得相对独立并松耦合
      前后端职责
    • +
    +
  2. +
+ + + + + + + + + + + + + + + + + + + + + + + +
后端前端
提供数据接收数据,展示数据
处理业务逻辑处理渲染逻辑
Server-side MVC架构Client-sideMV*架构
代码运行在服务器上代码运行在浏览器上
+
    +
  1. 开发流程
      +
    • 前后端技术负责人约定好接口格式
    • +
    • 后端编写和维护接口文档,在 API 变化时更新接口文档 + MOCK平台
    • +
    • 后端根据接口文档进行接口开发
    • +
    • 前端根据接口文档 + MOCK平台进行开发
    • +
    • 开发完成后联调和提交测试
    • +
    +
  2. +
+

MOCK平台统一采用公司搭建的YAPI平台

+

YAPI平台可以对接SWAGGER工具进行自动构建MOCK服务
前后端开发流程
3. 规范原则
- 接口返回数据即显示:前端仅做渲染逻辑处理
- 渲染逻辑禁止跨多个接口调用
- 前端关注交互、渲染逻辑,尽量避免业务逻辑处理的出现
- 请求响应传输数据格式:JSON,JSON数据尽量简单轻量,避免多级JSON的出现

+

四、基本格式

接口定义参见《RESTFul API的设计规范》

请求格式

GET请求、POST请求==必须包含key为body的入参,所有请求数据包装为JSON格式,并存放到入参body中==,示例如下:

+

GET请求:

+
1
xxx/login?body={"username":"admin","password":"123456","captcha":"scfd","rememberMe":1}
+ +

响应格式

对于通用业务数据响应参照基本数据格式要求

+

响应基本数据格式

1
2
3
4
{
"code": 200,
"msg": "success"
}
+
响应实体格式
1
2
3
4
5
6
7
8
9
10
11
{
"code": 200,
"msg": "success",
"data": {
"entity": {
"id": 1,
"name": "XXX",
"phone": "XXX"
}
}
}
+

entity: 响应返回的实体数据

+
响应列表格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"code": 200,
"msg": "success",
"data": {
"list":[
{
"id": 1,
"name": "XXX",
"code": "XXX"
},
{
"id": 2,
"name": "XXX",
"code": "XXX"
},
{
"id": 3,
"name": "XXX",
"code": "XXX"
}
]
}
}
+

list: 响应返回的列表数据

+
响应分页格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"code": 200,
"msg":"success",
"data": {
"totalCount": 2, // 总记录数
"totalPage": 1 // 总页数
"pageNo": 1, // 当前页码
"pageSize": 10, // 每页大小
"list":[
{
"id": 1,
"name": "XXX",
"code": "XXX"
},
{
"id": 2,
"name": "XXX",
"code": "XXX"
},
{
"id": 3,
"name": "XXX",
"code": "XXX"
}
],
}
}
+

响应特殊数据格式

对于特定组件数据格式由后端统一处理后返回前端,如(echart、ztree等组件)

+

特殊内容规范

布尔类型

关于布尔类型,一律返回BOOLEN类型值

+
日期格式

关于日期类型,JSON数据传输中一律使用字符串格式时间戳,具体日期格式因业务而定

+

五、相关文章导读

前后端分离开发指南-理论篇
前后端分离开发指南-实践篇

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前后端分离开发规范

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB%E5%BC%80%E5%8F%91%E8%A7%84%E8%8C%83/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/06/01/\345\211\215\347\253\257\345\274\200\345\217\221\346\217\222\344\273\266/index.html" "b/ja/2022/06/01/\345\211\215\347\253\257\345\274\200\345\217\221\346\217\222\344\273\266/index.html" new file mode 100644 index 0000000000..713181df1c --- /dev/null +++ "b/ja/2022/06/01/\345\211\215\347\253\257\345\274\200\345\217\221\346\217\222\344\273\266/index.html" @@ -0,0 +1,588 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端开发插件 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端开发插件 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

插件安装

Prettier - Code formatter

格式化工具

+

ESLint

校验规则

+

Vetur

vue代码片段及代码美化

+

Vue 2 Snippets

vue2 代码片段

+

vscode-fileheader

文件注释

+

配置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// setting.json vscode配置文件
{
"workbench.startupEditor": "newUntitledFile",
"terminal.integrated.shell.windows": "C:\\Program Files\\Git\\bin\\bash.exe", \\ 自行配置自己的环境地址
"javascript.updateImportsOnFileMove.enabled": "always",
"editor.tabSize": 2,
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true
},
"sync.gist": "748b4cae5eb6e56d6997978ead096e8f",
"breadcrumbs.enabled": true,
"todohighlight.isEnable": false,
"liveServer.settings.donotShowInfoMsg": true,
"search.location": "sidebar",
"workbench.activityBar.visible": true,
"window.menuBarVisibility": "default",
"workbench.statusBar.visible": true,
"editor.snippetSuggestions": "top",
"editor.formatOnPaste": true,
"workbench.colorTheme": "Tiny Light",
"eslint.validate": [
"javascript",
"javascriptreact",
{
"language": "html",
"autoFix": true
},
{
"language": "vue",
"autoFix": true
}
],
"prettier.eslintIntegration": true,
"files.autoSave": "onWindowChange",
"code-runner.saveAllFilesBeforeRun": true,
"vetur.format.defaultFormatter.html": "js-beautify-html",
"vetur.format.defaultFormatter.js": "vscode-typescript",
"prettier.jsxSingleQuote": true,
"prettier.requireConfig": false,
"prettier.arrowParens": "always",
"typescript.format.insertSpaceAfterSemicolonInForStatements": false,
"prettier.stylelintIntegration": true,
"prettier.singleQuote": true,
"prettier.tslintIntegration": true,
"eslint.provideLintTask": true,
"eslint.autoFixOnSave": true,
"editor.mouseWheelZoom": true,
"editor.tabCompletion": "on",
"editor.formatOnType": true,
"eslint.alwaysShowStatus": true,
"eslint.options": {
"configFile": "E:/project/xxjs/fore-core/.eslintrc.js" // 自行配置自己的项目地址
},
"fileheader.Author": "Li.Xg", // 自行配置自己的名称
"fileheader.LastModifiedBy": "Li.Xg" // 自行配置自己的名称
}
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端开发插件

+

文章作者:

+

发布时间:2022年06月01日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/01/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91%E6%8F%92%E4%BB%B6/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2022/06/02/DockerFile/index.html b/ja/2022/06/02/DockerFile/index.html new file mode 100644 index 0000000000..837d483501 --- /dev/null +++ b/ja/2022/06/02/DockerFile/index.html @@ -0,0 +1,700 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DockerFile | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ DockerFile +

+ + +
+ + + + +
+ + +

DockerFile

原则与建议

    +
  • 容器轻量化。从镜像中产生的容器应该尽量轻量化,能在足够短的时间内停止、销毁、重新生成并替换原来的容器。
  • +
  • 使用 .gitignore。在大部分情况下,Dockerfile 会和构建所需的文件放在同一个目录中,为了提高构建的性能,应该使用 .gitignore 来过滤掉不需要的文件和目录。
  • +
  • 为了减少镜像的大小,减少依赖,仅安装需要的软件包。
  • +
  • 一个容器只做一件事。解耦复杂的应用,分成多个容器,而不是所有东西都放在一个容器内运行。如一个 Python Web 应用,可能需要 Server、DB、Cache、MQ、Log 等几个容器。一个更加极端的说法:One process per container。
  • +
  • 减少镜像的图层。不要多个 Label、ENV 等标签。
  • +
  • 对续行的参数按照字母表排序,特别是使用apt-get install -y安装包的时候。
  • +
  • 使用构建缓存。如果不想使用缓存,可以在构建的时候使用参数–no-cache=true来强制重新生成中间镜像。
  • +
+

常用命令

FROM

FROM 指令用于指定其后构建新镜像所使用的基础镜像

+
1
2
3
FROM <image>
FROM <image>:<tag>
FROM <image>:<digest>
+

FROM 必须 是 Dockerfile 中第一条非注释命令
在一个 Dockerfile 文件中创建多个镜像时,FROM 可以多次出现。只需在每个新命令 FROM 之前,记录提交上次的镜像 ID。
tag 或 digest 是可选的,如果不使用这两个值时,会使用 latest 版本的基础镜像

+

RUN

在镜像的构建过程中执行特定的命令,并生成一个中间镜像

+
1
2
3
4
#shell格式
RUN <command>
#exec格式
RUN ["executable", "param1", "param2"]
+
    +
  • RUN 命令将在当前 image 中执行任意合法命令并提交执行结果。命令执行提交后,就会自动执行 Dockerfile 中的下一个指令。
  • +
  • 层级 RUN 指令和生成提交是符合 Docker 核心理念的做法。它允许像版本控制那样,在任意一个点,对 image 镜像进行定制化构建。
  • +
  • RUN 指令创建的中间镜像会被缓存,并会在下次构建中使用。如果不想使用这些缓存镜像,可以在构建时指定 ######no###cache 参数,如:docker build ######no###cache。
  • +
+

FROM scratch

scratch 是一个空的虚拟的镜像源文件,因为需要的依赖都存在不需要基础镜像,从而减小体积。

+

COPY

复制文件

+
1
2
COPY <源路径>... <目标路径>
COPY ["<源路径1>",... "<目标路径>"]
+

和 RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的<目标路径>位置。比如:

+
1
COPY package.json /usr/src/app/
+ +

<源路径>可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,如:

+
1
2
COPY hom* /mydir/
COPY hom?.txt /mydir/
+ +

<目标路径>可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

+

此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。

+

ADD

更高级的复制文件
ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。比如<源路径>可以是一个 URL,这种情况下,Docker 引擎会试图去下载这个链接的文件放到<目标路径>去。

+

在构建镜像时,复制上下文中的文件到镜像内,格式:

+
1
2
ADD <源路径>... <目标路径>
ADD ["<源路径>",... "<目标路径>"]
+

注意
如果 docker 发现文件内容被改变,则接下来的指令都不会再使用缓存。关于复制文件时需要处理的/,基本跟正常的 copy 一致

+

ENV

设置环境变量
格式有两种:

+
1
2
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
+

这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。

+
1
2
ENV VERSION=1.0 DEBUG=on \
NAME="Happy Feet"
+

这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。

+

EXPOSE

为构建的镜像设置监听端口,使容器在运行时监听。
格式:

+
1
EXPOSE <port> [<port>...]
+

EXPOSE 指令并不会让容器监听 host 的端口,如果需要,需要在 docker run 时使用 -p、-P 参数来发布容器端口到 host 的某个端口上。

+

VOLUME

定义匿名卷
VOLUME用于创建挂载点,即向基于所构建镜像创始的容器添加卷:

+
1
VOLUME ["/data"]
+

一个卷可以存在于一个或多个容器的指定目录,该目录可以绕过联合文件系统,并具有以下功能:

+
    +
  • 卷可以容器间共享和重用
  • +
  • 容器并不一定要和其它容器共享卷
  • +
  • 修改卷后会立即生效
  • +
  • 对卷的修改不会对镜像产生影响
  • +
  • 卷会一直存在,直到没有任何容器在使用它
    VOLUME 让我们可以将源代码、数据或其它内容添加到镜像中,而又不并提交到镜像中,并使我们可以多个容器间共享这些内容。
  • +
+

WORKDIR

WORKDIR用于在容器内设置一个工作目录:

+
1
WORKDIR /path/to/workdir
+

通过WORKDIR设置工作目录后,Dockerfile 中其后的命令 RUN、CMD、ENTRYPOINT、ADD、COPY 等命令都会在该目录下执行。
如,使用WORKDIR设置工作目录:

+
1
2
3
4
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
+

在以上示例中,pwd 最终将会在 /a/b/c 目录中执行。在使用 docker run 运行容器时,可以通过-w参数覆盖构建时所设置的工作目录。

+

USER

指定当前用户
USER 用于指定运行镜像所使用的用户:

+
1
USER daemon
+

使用USER指定用户时,可以使用用户名、UID 或 GID,或是两者的组合。以下都是合法的指定试:

+
1
2
3
4
5
6
USER user
USER user:group
USER uid
USER uid:gid
USER user:gid
USER uid:group
+

使用USER指定用户后,Dockerfile 中其后的命令 RUN、CMD、ENTRYPOINT 都将使用该用户。镜像构建完成后,通过 docker run 运行容器时,可以通过 -u 参数来覆盖所指定的用户。

+

CMD

CMD用于指定在容器启动时所要执行的命令。CMD 有以下三种格式:

+
1
2
3
CMD ["executable","param1","param2"]
CMD ["param1","param2"]
CMD command param1 param2
+

省略可执行文件的 exec 格式,这种写法使 CMD 中的参数当做 ENTRYPOINT 的默认参数,此时 ENTRYPOINT 也应该是 exec 格式,具体与 ENTRYPOINT 的组合使用,参考 ENTRYPOINT。

+

注意
与 RUN 指令的区别:RUN 在构建的时候执行,并生成一个新的镜像,CMD 在容器运行的时候执行,在构建时不进行任何操作。

+

ENTRYPOINT

ENTRYPOINT 用于给容器配置一个可执行程序。也就是说,每次使用镜像创建容器时,通过 ENTRYPOINT 指定的程序都会被设置为默认程序。ENTRYPOINT 有以下两种形式:

+
1
2
3
ENTRYPOINT ["executable", "param1", "param2"]
ENTRYPOINT command param1 param2
ENTRYPOINT 与 CMD 非常类似,不同的是通过docker run执行的命令不会覆盖 ENTRYPOINT,而docker run命令中指定的任何参数,都会被当做参数再次传递给 ENTRYPOINT。Dockerfile 中只允许有一个 ENTRYPOINT 命令,多指定时会覆盖前面的设置,而只执行最后的 ENTRYPOINT 指令。
+

docker run运行容器时指定的参数都会被传递给 ENTRYPOINT ,且会覆盖 CMD 命令指定的参数。如,执行docker run -d时,-d 参数将被传递给入口点。

+

也可以通过docker run –entrypoint重写 ENTRYPOINT 入口点。如:可以像下面这样指定一个容器执行程序:

+
1
ENTRYPOINT ["/usr/bin/nginx"]
+

完整构建代码:

+
1
2
3
4
5
6
7
8
9
# Version: 0.0.3
FROM ubuntu:16.04
MAINTAINER 何民三 "cn.liuht@gmail.com"
RUN apt-get update
RUN apt-get install -y nginx
RUN echo 'Hello World, 我是个容器' \
> /var/www/html/index.html
ENTRYPOINT ["/usr/sbin/nginx"]
EXPOSE 80
+

使用docker build构建镜像,并将镜像指定为 itbilu/test:

+
1
docker build -t="itbilu/test" .
+

构建完成后,使用itbilu/test启动一个容器:

+
1
docker run -i -t  itbilu/test -g "daemon off;"
+

在运行容器时,我们使用了 -g “daemon off;”,这个参数将会被传递给 ENTRYPOINT,最终在容器中执行的命令为 /usr/sbin/nginx -g “daemon off;”。

+

LABEL

LABEL用于为镜像添加元数据,元数以键值对的形式指定:

+
1
LABEL <key>=<value> <key>=<value> <key>=<value> ...
+

使用LABEL指定元数据时,一条LABEL指定可以指定一或多条元数据,指定多条元数据时不同元数据之间通过空格分隔。推荐将所有的元数据通过一条LABEL指令指定,以免生成过多的中间镜像。
如,通过LABEL指定一些元数据:

+
1
LABEL version="1.0" description="这是一个Web服务器" by="IT笔录"
+

指定后可以通过docker inspect查看:

+
1
2
3
4
5
6
docker inspect itbilu/test
"Labels": {
"version": "1.0",
"description": "这是一个Web服务器",
"by": "IT笔录"
},
+ +

ARG

ARG用于指定传递给构建运行时的变量:

+
1
ARG <name>[=<default value>]
+

如,通过ARG指定两个变量:

+
1
2
ARG site
ARG build_user=IT笔录
+

以上我们指定了 site 和 build_user 两个变量,其中 build_user 指定了默认值。在使用 docker build 构建镜像时,可以通过 –build-arg = 参数来指定或重设置这些变量的值。

+
1
docker build --build-arg site=itiblu.com -t itbilu/test .
+

这样我们构建了 itbilu/test 镜像,其中site会被设置为 itbilu.com,由于没有指定 build_user,其值将是默认值 IT 笔录。

+

ONBUILD

ONBUILD用于设置镜像触发器:

+
1
ONBUILD [INSTRUCTION]
+

当所构建的镜像被用做其它镜像的基础镜像,该镜像中的触发器将会被钥触发。
如,当镜像被使用时,可能需要做一些处理:

+
1
2
3
4
[...]
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
[...]
+ +

STOPSIGNAL

STOPSIGNAL用于设置停止容器所要发送的系统调用信号:

+
1
STOPSIGNAL signal
+

所使用的信号必须是内核系统调用表中的合法的值,如:SIGKILL。

+

SHELL

SHELL用于设置执行命令(shell式)所使用的的默认 shell 类型:

+
1
SHELL ["executable", "parameters"]
+

SHELL在Windows环境下比较有用,Windows 下通常会有 cmd 和 powershell 两种 shell,可能还会有 sh。这时就可以通过 SHELL 来指定所使用的 shell 类型:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM microsoft/windowsservercore

# Executed as cmd /S /C echo default
RUN echo default

# Executed as cmd /S /C powershell -command Write-Host default
RUN powershell -command Write-Host default

# Executed as powershell -command Write-Host hello
SHELL ["powershell", "-command"]
RUN Write-Host hello

# Executed as cmd /S /C echo hello
SHELL ["cmd", "/S"", "/C"]
RUN echo hello
+ +

Dockerfile 示例

    +
  • 构建Nginx运行环境
  • +
  • 构建tomcat 环境
  • +
+

构建Nginx运行环境

Dockerfile文件

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#指定基础镜像
FROM sameersbn/ubuntu:14.04.20161014

#维护者信息
MAINTAINER sameer@damagehead.com

#设置环境
ENV RTMP_VERSION=1.1.10 \
NPS_VERSION=1.11.33.4 \
LIBAV_VERSION=11.8 \
NGINX_VERSION=1.10.1 \
NGINX_USER=www-data \
NGINX_SITECONF_DIR=/etc/nginx/sites-enabled \
NGINX_LOG_DIR=/var/log/nginx \
NGINX_TEMP_DIR=/var/lib/nginx \
NGINX_SETUP_DIR=/var/cache/nginx

#设置构建时变量,镜像建立完成后就失效
ARG BUILD_LIBAV=false
ARG WITH_DEBUG=false
ARG WITH_PAGESPEED=true
ARG WITH_RTMP=true

#复制本地文件到容器目录中
COPY setup/ ${NGINX_SETUP_DIR}/
RUN bash ${NGINX_SETUP_DIR}/install.sh

#复制本地配置文件到容器目录中
COPY nginx.conf /etc/nginx/nginx.conf
COPY entrypoint.sh /sbin/entrypoint.sh

#运行指令
RUN chmod 755 /sbin/entrypoint.sh

#允许指定的端口
EXPOSE 80/tcp 443/tcp 1935/tcp

#指定网站目录挂载点
VOLUME ["${NGINX_SITECONF_DIR}"]

ENTRYPOINT ["/sbin/entrypoint.sh"]
CMD ["/usr/sbin/nginx"]
+ +

构建Tomcat环境

Dockerfile文件

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 指定基于的基础镜像
FROM ubuntu:13.10

# 维护者信息
MAINTAINER zhangjiayang "zhangjiayang@sczq.com.cn"

# 镜像的指令操作
# 获取APT更新的资源列表
RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe"> /etc/apt/sources.list
# 更新软件
RUN apt-get update

# Install curl
RUN apt-get -y install curl

# Install JDK 7
RUN cd /tmp && curl -L 'http://download.oracle.com/otn-pub/java/jdk/7u65-b17/jdk-7u65-linux-x64.tar.gz' -H 'Cookie: oraclelicense=accept-securebackup-cookie; gpw_e24=Dockerfile' | tar -xz
RUN mkdir -p /usr/lib/jvm
RUN mv /tmp/jdk1.7.0_65/ /usr/lib/jvm/java-7-oracle/

# Set Oracle JDK 7 as default Java
RUN update-alternatives --install /usr/bin/java java /usr/lib/jvm/java-7-oracle/bin/java 300
RUN update-alternatives --install /usr/bin/javac javac /usr/lib/jvm/java-7-oracle/bin/javac 300

# 设置系统环境
ENV JAVA_HOME /usr/lib/jvm/java-7-oracle/

# Install tomcat7
RUN cd /tmp && curl -L 'http://archive.apache.org/dist/tomcat/tomcat-7/v7.0.8/bin/apache-tomcat-7.0.8.tar.gz' | tar -xz
RUN mv /tmp/apache-tomcat-7.0.8/ /opt/tomcat7/

ENV CATALINA_HOME /opt/tomcat7
ENV PATH $PATH:$CATALINA_HOME/bin

# 复件tomcat7.sh到容器中的目录
ADD tomcat7.sh /etc/init.d/tomcat7
RUN chmod 755 /etc/init.d/tomcat7

# Expose ports. 指定暴露的端口
EXPOSE 8080

# Define default command.
ENTRYPOINT service tomcat7 start && tail -f /opt/tomcat7/logs/catalina.out
+ +

tomcat7.sh命令文件

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export JAVA_HOME=/usr/lib/jvm/java-7-oracle/  
export TOMCAT_HOME=/opt/tomcat7

case $1 in
start)
sh $TOMCAT_HOME/bin/startup.sh
;;
stop)
sh $TOMCAT_HOME/bin/shutdown.sh
;;
restart)
sh $TOMCAT_HOME/bin/shutdown.sh
sh $TOMCAT_HOME/bin/startup.sh
;;
esac
exit 0
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:DockerFile

+

文章作者:

+

发布时间:2022年06月02日 - 11:20

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/02/DockerFile/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/06/02/Docker\345\221\275\344\273\244/index.html" "b/ja/2022/06/02/Docker\345\221\275\344\273\244/index.html" new file mode 100644 index 0000000000..ec4ccae731 --- /dev/null +++ "b/ja/2022/06/02/Docker\345\221\275\344\273\244/index.html" @@ -0,0 +1,632 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Docker 命令 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Docker 命令 +

+ + +
+ + + + +
+ + +

Docker 命令

    +
  • 帮助命令
    docker –help
  • +
+

镜像操作

    +
  • 搜索镜像
    docker search hello-world

    +
  • +
  • 下载镜像
    docker pull hello-world

    +
  • +
  • 查看本地已下载所有镜像
    docker images

    +
  • +
  • 查看镜像历史
    docker history hello-world

    +
  • +
  • 备份镜像
    docker tag hello-word:last hello-world:v2

    +
  • +
  • 删除镜像
    docker rmi hello-word:last

    +
  • +
  • 删除未使用过的镜像
    docker image prune

    +
  • +
  • 导出镜像
    docker save -o hello-world:last.tar hello-world:last

    +
  • +
  • 导入镜像
    docker load -i hello-world:last.tar

    +
  • +
  • 查看镜像信息
    docker image inspact nginx

    +
  • +
+

容器操作

    +
  • 查看所有容器 [-q 编号] [-a active 启动的容器]
    docker ps [-a] [-q]

    +
  • +
  • 启动容器 [-d 后台启动]
    docker run -d –name nginx1 nginx:last

    +
  • +
  • 停止容器
    docker stop nginx1

    +
  • +
  • 启动容器()
    docker start nginx1

    +
  • +
  • 删除容器
    docker rm nginx1

    +
  • +
  • 批量删除运行中容器
    docker rm $(docker ps -q) -f

    +
  • +
  • 创建容器并进入
    docker run -it –name nginx1 nginx:last /bin/bash

    +
  • +
  • 退出容器
    exit

    +
  • +
  • 进入容器
    docker exec -it nginx1 /bin/bash

    +
  • +
  • 通过容器创建镜像
    docker commit -m ‘laowang’ nginx1 nginx:v1

    +
  • +
  • 查看容器信息
    docker container inspact nginx1

    +
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Docker 命令

+

文章作者:

+

发布时间:2022年06月02日 - 16:20

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/02/Docker%E5%91%BD%E4%BB%A4/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/06/02/Docker\345\256\271\345\231\250\347\253\257\345\217\243\346\230\240\345\260\204/index.html" "b/ja/2022/06/02/Docker\345\256\271\345\231\250\347\253\257\345\217\243\346\230\240\345\260\204/index.html" new file mode 100644 index 0000000000..e86cdc35b6 --- /dev/null +++ "b/ja/2022/06/02/Docker\345\256\271\345\231\250\347\253\257\345\217\243\346\230\240\345\260\204/index.html" @@ -0,0 +1,585 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Docker容器端口映射 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Docker容器端口映射 +

+ + +
+ + + + +
+ + +

Docker容器端口映射

常见容器服务需要做端口映射,这里以nginx为例进行举例

+

启动一个nginx容器

docker run -itd –name nginx1 -P nginx:latest #随机端口
docker run -itd –name nginx2 -p 6001:80 nginx:latest #指定端口
docker run -itd –name nginx3 -p 172.16.10.1:6002:80 nginx:latest #指定IP和端口

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Docker容器端口映射

+

文章作者:

+

发布时间:2022年06月02日 - 11:22

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/02/Docker%E5%AE%B9%E5%99%A8%E7%AB%AF%E5%8F%A3%E6%98%A0%E5%B0%84/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/06/02/Docker\346\225\260\346\215\256\345\215\267/index.html" "b/ja/2022/06/02/Docker\346\225\260\346\215\256\345\215\267/index.html" new file mode 100644 index 0000000000..0bfcaba406 --- /dev/null +++ "b/ja/2022/06/02/Docker\346\225\260\346\215\256\345\215\267/index.html" @@ -0,0 +1,595 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Docker数据卷 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Docker数据卷 +

+ + +
+ + + + +
+ + +

Docker数据卷

宿主机与容器进行数据交互,共享宿主机与容器之间的数据

+

创建数据卷关联

docker run -it –name my_ubuntu1 -v ~ /Desktop/test:/test /bin/bash

+

利用系统方法操作数据卷

    +
  • 查 docker数据卷
    docker volume ls

    +
  • +
  • 创建数据卷
    docker volume create volname

    +
  • +
  • 共享
    docker run -it –name my_ubuntu2 -v volname:/home ubuntu:latest /bin/bash

    +
  • +
+

数据卷容器使用

可以通过数据卷容器,创建新的容器,并将多个容器绑定在一起

+

docker run -itd –name my_ubuntu3 –volumes-from my_ubuntu2 ubuntu:latest

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Docker数据卷

+

文章作者:

+

发布时间:2022年06月02日 - 11:21

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/02/Docker%E6%95%B0%E6%8D%AE%E5%8D%B7/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/06/02/Docker\347\275\221\347\273\234\346\250\241\345\274\217/index.html" "b/ja/2022/06/02/Docker\347\275\221\347\273\234\346\250\241\345\274\217/index.html" new file mode 100644 index 0000000000..460b4c053b --- /dev/null +++ "b/ja/2022/06/02/Docker\347\275\221\347\273\234\346\250\241\345\274\217/index.html" @@ -0,0 +1,595 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Docker网络模式 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Docker网络模式 +

+ + +
+ + + + +
+ + +

Docker网络模式

Docker常见的两种网络模式

+
    +
  • host网络模式:创建的容器和宿主机共享同一个网卡
  • +
  • bridge网络模式:桥接模式,只要使用桥接模式创建容器网段一样(类似于172.16.1.1,172.16.1.2)有点像是子局域网
  • +
+

利用network命令管理网络模式

    +
  • 查看网络模式
    docker network ls
  • +
  • 创建网络模式
    docker network create –drive bridge bridge_test
  • +
  • 通过network断网
    docker network disconnet bridge nginx5
  • +
  • 通过network联网
    docker network connect bridge nginx5
  • +
  • 删除网络模式
    docker network rm bridge_test
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Docker网络模式

+

文章作者:

+

发布时间:2022年06月02日 - 11:23

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/02/Docker%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%BC%8F/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/06/09/Hexo\345\234\250Github\345\217\221\345\270\203\344\271\213\345\220\216\350\207\252\345\256\232\344\271\211\345\237\237\345\220\215\347\232\204\351\205\215\347\275\256/index.html" "b/ja/2022/06/09/Hexo\345\234\250Github\345\217\221\345\270\203\344\271\213\345\220\216\350\207\252\345\256\232\344\271\211\345\237\237\345\220\215\347\232\204\351\205\215\347\275\256/index.html" new file mode 100644 index 0000000000..46eb1e7344 --- /dev/null +++ "b/ja/2022/06/09/Hexo\345\234\250Github\345\217\221\345\270\203\344\271\213\345\220\216\350\207\252\345\256\232\344\271\211\345\237\237\345\220\215\347\232\204\351\205\215\347\275\256/index.html" @@ -0,0 +1,593 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hexo在Github发布之后自定义域名的配置 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Hexo在Github发布之后自定义域名的配置 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

HexoGithub发布之后自定义域名的配置

进到你的博客发布所在仓库,如:

选择Settings,然后是pages,滚动页面找到Custom domain,将你自己的域名绑定进去保存一下就好了。

+

在你自己的域名商处用CNAME把你自己的域名指定到你自己的仓库page地址上比如我的就是lixuguang.github.io Public,这样你的GitHub Page就可以通过自己的域名进行访问了。

+

HexoGithub发布之后自定义域名会被清空的问题

使用 GitHub Page 功能将博客托管在了 GitHub 上,并配置 CNAME 将自己的域名解析了过去,但是发现一个问题,每次 hexo deploy 之后,custom domain 会被重置失效。

+

解决方案

Hexo 生成的博客的 source 目录下(注意这个是你的博客源码所在仓库,并不是发布的仓库)新建一个 CNAME 文件,然后在这个文件中填入你的域名,这样就不会每次发布之后,GitHub Page 里的 custom domain 都被重置掉啦。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Hexo在Github发布之后自定义域名的配置

+

文章作者:

+

发布时间:2022年06月09日 - 12:40

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/09/Hexo%E5%9C%A8Github%E5%8F%91%E5%B8%83%E4%B9%8B%E5%90%8E%E8%87%AA%E5%AE%9A%E4%B9%89%E5%9F%9F%E5%90%8D%E7%9A%84%E9%85%8D%E7%BD%AE/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/06/09/Nginx\346\214\207\344\273\244/index.html" "b/ja/2022/06/09/Nginx\346\214\207\344\273\244/index.html" new file mode 100644 index 0000000000..1995fa70d6 --- /dev/null +++ "b/ja/2022/06/09/Nginx\346\214\207\344\273\244/index.html" @@ -0,0 +1,591 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Nginx指令 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Nginx指令 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Nginx指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#启动Nginx
start nginx

#重启Nginx
nginx -s reopen

#重新加载Nginx配置文件,然后以优雅的方式重启Nginx
nginx -s reload

#强制停止Nginx服务
nginx -s stop

#优雅地停止Nginx服务(即处理完所有请求后再停止服务)
nginx -s quit

#检测配置文件是否有语法错误,然后退出
nginx -t

#显示版本信息并退出
nginx -v

#显示版本和配置选项信息,然后退出
nginx -V

#检测配置文件是否有语法错误,然后退出
nginx -t

#检测配置文件是否有语法错误,转储并退出
nginx -T

#在检测配置文件期间屏蔽非错误信息
nginx -q

#打开帮助信息
nginx -?,-h

#设置前缀路径(默认是:/usr/share/nginx/)
nginx -p prefix

#设置配置文件(默认是:/etc/nginx/nginx.conf)
nginx -c filename

#设置配置文件外的全局指令
nginx -g directives
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Nginx指令

+

文章作者:

+

发布时间:2022年06月09日 - 10:40

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/09/Nginx%E6%8C%87%E4%BB%A4/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2022/06/09/http_stub_status_module/index.html b/ja/2022/06/09/http_stub_status_module/index.html new file mode 100644 index 0000000000..a88f831c54 --- /dev/null +++ b/ja/2022/06/09/http_stub_status_module/index.html @@ -0,0 +1,610 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + http_stub_status_module Nginx的性能模块 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ http_stub_status_module Nginx的性能模块 +

+ + +
+ + + + +
+ + +

ngx_http_stub_status_module 是一个 Nginx 的内置 模块,可以提供 Nginx 的简单统计信息。默认情况下这个模块是不被编译进来的,所以在编译 Nginx 时要指定加载该模块:

+
1
--with-http_stub_status_module
+

查看是否安装

1
2
3
4
5
// 查看nginx版本及安装了那些模块
nginx -V

// 如果安装了的话会显示下面的信息
--with-http_stub_status_module
+ +

配置使用

nginx.conf配置文件中进行如下配置

+
1
2
3
location /ngx_status {
stub_status on; // 设置开启性能模块
}
+

通过浏览器访问 http://localhost/ngx_status 就能看到你的nginx服务性能监控画面了
image

+

解析

    +
  • 第一行:Active connections:活动连接数
  • +
  • 第二三行:Server accepts handled requests:(三个数字分别代表)总共处理的连接数,成功握手的连接数量,处理的请求数(正常情况下握手和连接数是相等的,表示没有丢失)
  • +
  • 第四行:
      +
    • Reading: Nginx 读取到客户端的Header信息数
    • +
    • Writing: Nginx 返回给客户端的Header信息数
    • +
    • Waiting: 开启keep-alive的情况下,这个值等于 active – (reading + writing),意思就是Nginx已经处理完成,正在等候下一次请求指令的驻留连接(在nginx开启了keep-alive,也就是长连接的情况下,客户端跟服务端建立了连接但是没有读写操作的空闲状态)
    • +
    +
  • +
+

使用场景

    +
  • 可以简单的用脚本做监控
  • +
  • 可以用开源工具,zabbix,prometheus等去采集nginx的统计信息,做监控和历史数据采集
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:http_stub_status_module Nginx的性能模块

+

文章作者:

+

发布时间:2022年06月09日 - 12:40

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/09/http_stub_status_module/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/06/15/\346\236\266\346\236\204\345\256\236\350\267\265/index.html" "b/ja/2022/06/15/\346\236\266\346\236\204\345\256\236\350\267\265/index.html" new file mode 100644 index 0000000000..a477472ab7 --- /dev/null +++ "b/ja/2022/06/15/\346\236\266\346\236\204\345\256\236\350\267\265/index.html" @@ -0,0 +1,618 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 架构实践 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 架构实践 +

+ + +
+ + + + +
+ + +

架构实践

架构设计内容一览

应用架构设计

    +
  • Transaction执行控制(在线/批处理路径和控制)
  • +
  • Session控制
  • +
  • 死锁控制
  • +
  • 接口处理流程(适配器设置/文件备份)
  • +
  • 命名规范(实例/SID等)
  • +
  • 中间件参数 (初始化参数等)
  • +
  • 认证(处理流程/错误处理)
  • +
  • 帐户(用户ID体系/权限管理/申请方式)
  • +
+

开发架构设计

    +
  • 系统景观(各种环境名称、利用方法等)
  • +
  • 转运路线(转运路线/转运工具/承认方式)
  • +
  • 开发账户(用户ID体系/权限管理/申请方式)
  • +
  • 开发终端设置(在线批处理开发工具/目录等)
  • +
  • 开发资源管理
  • +
  • 表单、协作工具的利用方法(表单/接口ID定义规则)
  • +
  • 开发和验证备份(处理流程/文件清除/周期)
  • +
  • 数据屏蔽(処理概要/屏蔽利用规则/対象/开发环境搭建方法)
  • +
+

运维架构设计

    +
  • 监测规范(方式/周期/対象)
  • +
  • 审计和错误日志(日志级别/日志保留时效/删除时效)
  • +
  • 生产环境和调研环境的备份(处理流程/删除时效/周期)
  • +
  • 加密处理(安装位置、加密规则、记录水平加密方法)
  • +
  • 云安全设定
  • +
  • 增加功能设计
  • +
+

基础设施

    +
  • 网络设计(虚拟网/子网/URL/负载均衡访问控制/白名单)
  • +
  • 云设计(实例/NFS/IAM)
  • +
  • 存储设计 (BlockVolume/FileStorage/ObjectStorage/数据配置/数据圧縮方法)
  • +
  • 时间DNS同步化(OCI設定)
  • +
  • OS参数设定
  • +
  • Docker参数设定
  • +
  • 高可用切换 (処理方式、设定参数考量)
  • +
  • 打补丁(打补丁/执行规范)
  • +
  • 虚拟桌面设定
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:架构实践

+

文章作者:

+

发布时间:2022年06月15日 - 14:40

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/15/%E6%9E%B6%E6%9E%84%E5%AE%9E%E8%B7%B5/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/06/16/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206 ---- \347\275\221\347\273\234\345\256\211\345\205\250\357\274\2102\357\274\211/index.html" "b/ja/2022/06/16/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206 ---- \347\275\221\347\273\234\345\256\211\345\205\250\357\274\2102\357\274\211/index.html" new file mode 100644 index 0000000000..6c3f906557 --- /dev/null +++ "b/ja/2022/06/16/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206 ---- \347\275\221\347\273\234\345\256\211\345\205\250\357\274\2102\357\274\211/index.html" @@ -0,0 +1,728 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 前端常见知识点整理 ---- 网络安全(2) | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 前端常见知识点整理 ---- 网络安全(2) +

+ + +
+ + + + +
+ + +

前端常见知识点整理 —- 网络安全(2)

SQL 注入

SQL 注入漏洞(SQL Injection)是 Web 开发中最常见的一种安全漏洞。可以用它来从数据库获取敏感信息,或者利用数据库的特性执行添加用户,导出文件等一系列恶意操作,甚至有可能获取数据库乃至系统用户最高权限。

+

而造成 SQL 注入的原因是因为程序没有有效的转义过滤用户的输入,使攻击者成功的向服务器提交恶意的 SQL 查询代码,程序在接收后错误的将攻击者的输入作为查询语句的一部分执行,导致原始的查询逻辑被改变,额外的执行了攻击者精心构造的恶意代码。

+

很多 Web 开发者没有意识到 SQL 查询是可以被篡改的,从而把 SQL 查询当作可信任的命令。殊不知,SQL 查询是可以绕开访问控制,从而绕过身份验证和权限检查的。更有甚者,有可能通过 SQL 查询去运行主机系统级的命令。

+

SQL 注入原理

下面将通过一些真实的例子来详细讲解 SQL 注入的方式的原理。

+

考虑以下简单的管理员登录表单:

+
1
2
3
4
5
<form action="/login" method="POST">
<p>Username: <input type="text" name="username" /></p>
<p>Password: <input type="password" name="password" /></p>
<p><input type="submit" value="登陆" /></p>
</form>
+ +

后端的 SQL 语句可能是如下这样的:

+
1
2
3
4
5
6
7
let querySQL = `
SELECT *
FROM user
WHERE username='${username}'
AND psw='${password}'
`;
// 接下来就是执行 sql 语句...
+ +

目的就是来验证用户名和密码是不是正确,按理说乍一看上面的 SQL 语句也没什么毛病,确实是能够达到我们的目的,可是你只是站在用户会老老实实按照你的设计来输入的角度来看问题,如果有一个恶意攻击者输入的用户名是 zoumiaojiang' OR 1 = 1 --,密码随意输入,就可以直接登入系统了。WFT!

+

冷静下来思考一下,我们之前预想的真实 SQL 语句是:

+
1
SELECT * FROM user WHERE username='zoumiaojiang' AND psw='mypassword'
+ +

可以恶意攻击者的奇怪用户名将你的 SQL 语句变成了如下形式:

+
1
2
SELECT * FROM user WHERE username='zoumiaojiang' OR 1 = 1 --' AND psw='xxxx'

+ +

在 SQL 中,-- 是注释后面的内容的意思,所以查询语句就变成了:

+
1
2
SELECT * FROM user WHERE username='zoumiaojiang' OR 1 = 1

+ +

这条 SQL 语句的查询条件永远为真,所以意思就是恶意攻击者不用我的密码,就可以登录进我的账号,然后可以在里面为所欲为,然而这还只是最简单的注入,牛逼的 SQL 注入高手甚至可以通过 SQL 查询去运行主机系统级的命令,将你主机里的内容一览无余,这里我也没有这个能力讲解的太深入,毕竟不是专业研究这类攻击的,但是通过以上的例子,已经了解了 SQL 注入的原理,我们基本已经能找到防御 SQL 注入的方案了。

+

如何预防 SQL 注入

防止 SQL 注入主要是不能允许用户输入的内容影响正常的 SQL 语句的逻辑,当用户的输入的信息将要用来拼接 SQL 语句的话,我们应该永远选择不相信,任何内容都必须进行转义过滤,当然做到这个还是不够的,下面列出防御 SQL 注入的几点注意事项:

+
    +
  • 严格限制Web应用的数据库的操作权限,给此用户提供仅仅能够满足其工作的最低权限,从而最大限度的减少注入攻击对数据库的危害

    +
  • +
  • 后端代码检查输入的数据是否符合预期,严格限制变量的类型,例如使用正则表达式进行一些匹配处理。

    +
  • +
  • 对进入数据库的特殊字符(',",\,<,>,&,*,; 等)进行转义处理,或编码转换。基本上所有的后端语言都有对字符串进行转义处理的方法,比如 lodash 的 lodash._escapehtmlchar 库。

    +
  • +
  • 所有的查询语句建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中,即不要直接拼接 SQL 语句。例如 Node.js 中的 mysqljs 库的 query 方法中的 ? 占位参数。

    +
  • +
+
1
mysql.query(`SELECT * FROM user WHERE username = ? AND psw = ?`, [username, psw]);
+ +
    +
  • 在应用发布之前建议使用专业的 SQL 注入检测工具进行检测,以及时修补被发现的 SQL 注入漏洞。网上有很多这方面的开源工具,例如 sqlmap、SQLninja 等。

    +
  • +
  • 避免网站打印出 SQL 错误信息,比如类型错误、字段不匹配等,把代码里的 SQL 语句暴露出来,以防止攻击者利用这些错误信息进行 SQL 注入。

    +
  • +
  • 不要过于细化返回的错误信息,如果目的是方便调试,就去使用后端日志,不要在接口上过多的暴露出错信息,毕竟真正的用户不关心太多的技术细节,只要话术合理就行。

    +
  • +
+

碰到要操作的数据库的代码,一定要慎重,小心使得万年船,多找几个人多来几次 code review,将问题都暴露出来,而且要善于利用工具,操作数据库相关的代码属于机密,没事不要去各种论坛晒自家站点的 SQL 语句,万一被人盯上了呢?

+

命令行注入

命令行注入漏洞,指的是攻击者能够通过 HTTP 请求直接侵入主机,执行攻击者预设的 shell 命令,听起来好像匪夷所思,这往往是 Web 开发者最容易忽视但是却是最危险的一个漏洞之一,看一个实例:

+

假如现在需要实现一个需求:用户提交一些内容到服务器,然后在服务器执行一些系统命令去产出一个结果返回给用户,接口的部分实现如下:

+
1
2
3
4
5
// 以 Node.js 为例,假如在接口中需要从 github 下载用户指定的 repo
const exec = require('mz/child_process').exec;
let params = {/* 用户输入的参数 */};

exec(`git clone ${params.repo} /some/path`);
+ +

这段代码确实能够满足业务需求,正常的用户也确实能从指定的 git repo 上下载到想要的代码,可是和 SQL 注入一样,这段代码在恶意攻击者眼中,简直就是香饽饽。

+

如果 params.repo 传入的是 https://github.com/zoumiaojiang/zoumiaojiang.github.io.git 当然没问题了。
可是如果 params.repo 传入的是 https://github.com/xx/xx.git && rm -rf /* && 恰好你的服务是用 root 权限起的就惨了。

+

具体恶意攻击者能用命令行注入干什么也像 SQL 注入一样,手法是千变万化的,比如「反弹 shell 注入」等,但原理都是一样的,我们绝对有能力防止命令行注入发生。防止命令行注入需要做到以下几件事情:

+
    +
  • 后端对前端提交内容需要完全选择不相信,并且对其进行规则限制(比如正则表达式)。
  • +
  • 在调用系统命令前对所有传入参数进行命令行参数转义过滤。
  • +
  • 不要直接拼接命令语句,借助一些工具做拼接、转义预处理,例如 Node.js 的 shell-escape npm 包。
  • +
+

还是前面的例子,我们可以做到如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const exec = require('mz/child_process').exec;

// 借助 shell-escape npm 包解决参数转义过滤问题
const shellescape = require('shell-escape');

let params = {/* 用户输入的参数 */};

// 先过滤一下参数,让参数符合预期
if (!/正确的表达式/.test(params.repo)) {
return;
}

let cmd = shellescape([
'git',
'clone',
params.repo,
'/some/path'
]);

// cmd 的值: git clone 'https://github.com/xx/xx.git && rm -rf / &&' /some/path
// 这样就不会被注入成功了。
exec(cmd);
+

无论是在何种后端语言环境中,凡是涉及到代码调用系统 shell 命令的时候都一定要谨慎。

+

DDoS 攻击

DDoS 又叫分布式拒绝服务,全称 Distributed Denial of Service,其原理就是利用大量的请求造成资源过载,导致服务不可用,这个攻击应该不能算是安全问题,这应该算是一个另类的存在,因为这种攻击根本就是耍流氓的存在,「伤敌一千,自损八百」的行为。出于保护 Web App 不受攻击的攻防角度,还是介绍一下 DDoS 攻击吧,毕竟也是挺常见的。

+

DDoS 攻击可以理解为:「你开了一家店,隔壁家点看不惯,就雇了一大堆黑社会人员进你店里干坐着,也不消费,其他客人也进不来,导致你营业惨淡」。为啥说 DDoS 是个「伤敌一千,自损八百」的行为呢?毕竟隔壁店还是花了不少钱雇黑社会但是啥也没得到不是?DDoS 攻击的目的基本上就以下几个:

+
    +
  • 深仇大恨,就是要干死你
  • +
  • 敲诈你,不给钱就干你
  • +
  • 忽悠你,不买我防火墙服务就会有“人”继续干你
  • +
+

也许你的站点遭受过 DDoS 攻击,具体什么原因怎么解读见仁见智。DDos 攻击从层次上可分为网络层攻击与应用层攻击,从攻击手法上可分为快型流量攻击与慢型流量攻击,但其原理都是造成资源过载,导致服务不可用。

+

网络层 DDoS

网络层 DDos 攻击包括 SYN FloodACK FloodUDP FloodICMP Flood 等。

+

SYN Flood 攻击

SYN flood 攻击主要利用了 TCP 三次握手过程中的 Bug,我们都知道 TCP 三次握手过程是要建立连接的双方发送 SYN,SYN + ACK,ACK 数据包,而当攻击方随意构造源 IP 去发送 SYN 包时,服务器返回的 SYN + ACK 就不能得到应答(因为 IP 是随意构造的),此时服务器就会尝试重新发送,并且会有至少 30s 的等待时间,导致资源饱和服务不可用,此攻击属于慢型 DDoS 攻击。

+

ACK Flood 攻击

ACK Flood 攻击是在 TCP 连接建立之后,所有的数据传输 TCP 报文都是带有 ACK 标志位的,主机在接收到一个带有 ACK 标志位的数据包的时候,需要检查该数据包所表示的连接四元组是否存在,如果存在则检查该数据包所表示的状态是否合法,然后再向应用层传递该数据包。如果在检查中发现该数据包不合法,例如该数据包所指向的目的端口在本机并未开放,则主机操作系统协议栈会回应 RST 包告诉对方此端口不存在。

+

UDP Flood 攻击

UDP flood 攻击是由于 UDP 是一种无连接的协议,因此攻击者可以伪造大量的源 IP 地址去发送 UDP 包,此种攻击属于大流量攻击。正常应用情况下,UDP 包双向流量会基本相等,因此发起这种攻击的攻击者在消耗对方资源的时候也在消耗自己的资源。

+

ICMP Flood 攻击

ICMP Flood 攻击属于大流量攻击,其原理就是不断发送不正常的 ICMP 包(所谓不正常就是 ICMP 包内容很大),导致目标带宽被占用,但其本身资源也会被消耗。目前很多服务器都是禁 ping 的(在防火墙在可以屏蔽 ICMP 包),因此这种攻击方式已经落伍。

+

网络层 DDoS 防御

网络层的 DDoS 攻击究其本质其实是无法防御的,我们能做得就是不断优化服务本身部署的网络架构,以及提升网络带宽。当然,还是做好以下几件事也是有助于缓解网络层 DDoS 攻击的冲击:

+
    +
  • 网络架构上做好优化,采用负载均衡分流。
  • +
  • 确保服务器的系统文件是最新的版本,并及时更新系统补丁。
  • +
  • 添加抗 DDos 设备,进行流量清洗。
  • +
  • 限制同时打开的 SYN 半连接数目,缩短 SYN 半连接的 Timeout 时间。
  • +
  • 限制单 IP 请求频率。
  • +
  • 防火墙等防护设置禁止 ICMP 包等。
  • +
  • 严格限制对外开放的服务器的向外访问。
  • +
  • 运行端口映射程序或端口扫描程序,要认真检查特权端口和非特权端口。
  • +
  • 关闭不必要的服务。
  • +
  • 认真检查网络设备和主机/服务器系统的日志。只要日志出现漏洞或是时间变更,那这台机器就可能遭到了攻击。
  • +
  • 限制在防火墙外与网络文件共享。这样会给黑客截取系统文件的机会,主机的信息暴露给黑客,无疑是给了对方入侵的机会。
  • +
  • 加钱堆机器。。
  • +
  • 报警。。-
  • +
+

应用层 DDoS

应用层 DDoS 攻击不是发生在网络层,是发生在 TCP 建立握手成功之后,应用程序处理请求的时候,现在很多常见的 DDoS 攻击都是应用层攻击。应用层攻击千变万化,目的就是在网络应用层耗尽你的带宽,下面列出集中典型的攻击类型。

+

CC 攻击

当时绿盟为了防御 DDoS 攻击研发了一款叫做 Collapasar 的产品,能够有效的防御 SYN Flood 攻击。黑客为了挑衅,研发了一款 Challenge Collapasar 攻击工具(简称 CC)。

+

CC 攻击的原理,就是针对消耗资源比较大的页面不断发起不正常的请求,导致资源耗尽。因此在发送 CC 攻击前,我们需要寻找加载比较慢,消耗资源比较多的网页,比如需要查询数据库的页面、读写硬盘文件的等。通过 CC 攻击,使用爬虫对某些加载需要消耗大量资源的页面发起 HTTP 请求。

+

DNS Flood

DNS Flood 攻击采用的方法是向被攻击的服务器发送大量的域名解析请求,通常请求解析的域名是随机生成或者是网络世界上根本不存在的域名,被攻击的DNS 服务器在接收到域名解析请求的时候首先会在服务器上查找是否有对应的缓存,如果查找不到并且该域名无法直接由服务器解析的时候,DNS 服务器会向其上层 DNS 服务器递归查询域名信息。域名解析的过程给服务器带来了很大的负载,每秒钟域名解析请求超过一定的数量就会造成 DNS 服务器解析域名超时。

+

根据微软的统计数据,一台 DNS 服务器所能承受的动态域名查询的上限是每秒钟 9000 个请求。而我们知道,在一台 P3 的 PC 机上可以轻易地构造出每秒钟几万个域名解析请求,足以使一台硬件配置极高的 DNS 服务器瘫痪,由此可见 DNS 服务器的脆弱性。

+

HTTP 慢速连接攻击

针对 HTTP 协议,先建立起 HTTP 连接,设置一个较大的 Conetnt-Length,每次只发送很少的字节,让服务器一直以为 HTTP 头部没有传输完成,这样连接一多就很快会出现连接耗尽。

+

应用层 DDoS 防御

    +
  • 判断 User-Agent 字段(不可靠,因为可以随意构造)
  • +
  • 针对 IP + cookie,限制访问频率(由于 cookie 可以更改,IP 可以使用代理,或者肉鸡,也不可靠)
  • +
  • 关闭服务器最大连接数等,合理配置中间件,缓解 DDoS 攻击。
  • +
  • 请求中添加验证码,比如请求中有数据库操作的时候。
  • +
  • 编写代码时,尽量实现优化,并合理使用缓存技术,减少数据库的读取操作。
  • +
  • 加钱堆机器。。
  • +
  • 报警。。
  • +
+

应用层的防御有时比网络层的更难,因为导致应用层被 DDoS 攻击的因素非常多,有时往往是因为程序员的失误,导致某个页面加载需要消耗大量资源,有时是因为中间件配置不当等等。而应用层 DDoS 防御的核心就是区分人与机器(爬虫),因为大量的请求不可能是人为的,肯定是机器构造的。因此如果能有效的区分人与爬虫行为,则可以很好地防御此攻击。

+

其他 DDoS 攻击

发起 DDoS 也是需要大量的带宽资源的,但是互联网就像森林,林子大了什么鸟都有,DDoS 攻击者也能找到其他的方式发起廉价并且极具杀伤力的 DDoS 攻击。

+

利用 XSS

举个例子,如果 12306 页面有一个 XSS 持久型漏洞被恶意攻击者发现,只需在春节抢票期间在这个漏洞中执行脚本使得往某一个小站点随便发点什么请求,然后随着用户访问的增多,感染用户增多,被攻击的站点自然就会迅速瘫痪了。这种 DDoS 简直就是无本万利,不用惊讶,现在大站有 XSS 漏洞的不要太多。

+

来自 P2P 网络攻击

大家都知道,互联网上的 P2P 用户和流量都是一个极为庞大的数字。如果他们都去一个指定的地方下载数据,成千上万的真实 IP 地址连接过来,没有哪个设备能够支撑住。拿 BT 下载来说,伪造一些热门视频的种子,发布到搜索引擎,就足以骗到许多用户和流量了,但是这只是基础攻击。
高级的 P2P 攻击,是直接欺骗资源管理服务器。如迅雷客户端会把自己发现的资源上传到资源管理服务器,然后推送给其它需要下载相同资源的用户,这样,一个链接就发布出去。通过协议逆向,攻击者伪造出大批量的热门资源信息通过资源管理中心分发出去,瞬间就可以传遍整个 P2P 网络。更为恐怖的是,这种攻击是无法停止的,即使是攻击者自身也无法停止,攻击一直持续到 P2P 官方发现问题更新服务器且下载用户重启下载软件为止。

+

DDoS 最后总结

DDoS 不可能防的住,就好比你的店只能容纳 50 人,黑社会有 100 人,你就换一家大店,能容纳 500 人,然后黑社会又找来了 1000 人,这种堆人头的做法就是 DDoS 本质上的攻防之道,「道高一尺,魔高一丈,魔高一尺,道高一丈」,讲真,必要的时候就答应勒索你的人的条件吧,实在不行就报警吧。

+

流量劫持

流量劫持应该算是黑产行业的一大经济支柱了吧?简直是让人恶心到吐,不吐槽了,还是继续谈干货吧,流量劫持基本分两种:DNS 劫持 和 HTTP 劫持,目的都是一样的,就是当用户访问 zoumiaojiang.com 的时候,给你展示的并不是或者不完全是 zoumiaojiang.com 提供的 “内容”。

+

DNS 劫持

DNS 劫持,也叫做域名劫持,可以这么理解,「你打了一辆车想去商场吃饭,结果你打的车是小作坊派来的,直接给你拉到小作坊去了」,DNS 的作用是把网络地址域名对应到真实的计算机能够识别的 IP 地址,以便计算机能够进一步通信,传递网址和内容等。如果当用户通过某一个域名访问一个站点的时候,被篡改的 DNS 服务器返回的是一个恶意的钓鱼站点的 IP,用户就被劫持到了恶意钓鱼站点,然后继而会被钓鱼输入各种账号密码信息,泄漏隐私。

+

这类劫持,要不就是网络运营商搞的鬼,一般小的网络运营商与黑产勾结会劫持 DNS,要不就是电脑中毒,被恶意篡改了路由器的 DNS 配置,基本上做为开发者或站长却是很难察觉的,除非有用户反馈,现在升级版的 DNS 劫持还可以对特定用户、特定区域等使用了用户画像进行筛选用户劫持的办法,另外这类广告显示更加随机更小,一般站长除非用户投诉否则很难觉察到,就算觉察到了取证举报更难。无论如何,如果接到有 DNS 劫持的反馈,一定要做好以下几件事:

+
    +
  • 取证很重要,时间、地点、IP、拨号账户、截屏、URL 地址等一定要有。
  • +
  • 可以跟劫持区域的电信运营商进行投诉反馈。
  • +
  • 如果投诉反馈无效,直接去工信部投诉,一般来说会加白你的域名。
  • +
+

HTTP 劫持

HTTP 劫持您可以这么理解,「你打了一辆车想去商场吃饭,结果司机跟你一路给你递小作坊的广告」,HTTP 劫持主要是当用户访问某个站点的时候会经过运营商网络,而不法运营商和黑产勾结能够截获 HTTP 请求返回内容,并且能够篡改内容,然后再返回给用户,从而实现劫持页面,轻则插入小广告,重则直接篡改成钓鱼网站页面骗用户隐私。能够实施流量劫持的根本原因,是 HTTP 协议没有办法对通信对方的身份进行校验以及对数据完整性进行校验。如果能解决这个问题,则流量劫持将无法轻易发生。所以防止 HTTP 劫持的方法只有将内容加密,让劫持者无法破解篡改,这样就可以防止 HTTP 劫持了。

+

HTTPS 协议就是一种基于 SSL 协议的安全加密网络应用层协议,可以很好的防止 HTTP 劫持。这里有篇 文章 讲的不错。HTTPS 在这就不深讲了,后面有机会我会单独好好讲讲 HTTPS。如果不想站点被 HTTP 劫持,赶紧将你的站点全站改造成 HTTPS 吧。

+

服务器漏洞

服务器除了以上提到的那些大名鼎鼎的漏洞和臭名昭著的攻击以外,其实还有很多其他的漏洞,往往也很容易被忽视,在这个小节也稍微介绍几种。

+

越权操作漏洞

如果你的系统是有登录控制的,那就要格外小心了,因为很有可能你的系统越权操作漏洞,越权操作漏洞可以简单的总结为 「A 用户能看到或者操作 B 用户的隐私内容」,如果你的系统中还有权限控制就更加需要小心了。所以每一个请求都需要做 userid 的判断

+

以下是一段有漏洞的后端示意代码:

+
1
2
3
4
5
6
7
// ctx 为请求的 context 上下文
let msgId = ctx.params.msgId;

mysql.query(
'SELECT * FROM msg_table WHERE msg_id = ?',
[msgId]
);
+ +

以上代码是任何人都可以查询到任何用户的消息,只要有 msg_id 就可以,这就是比较典型的越权漏洞

+

越权操作漏洞防御

如下这么改进一下:

+
1
2
3
4
5
6
7
8
// ctx 为请求的 context 上下文
let msgId = ctx.params.msgId;
let userId = ctx.session.userId; // 从会话中取出当前登陆的 userId

mysql.query(
'SELECT * FROM msg_table WHERE msg_id = ? AND user_id = ?',
[msgId, userId]
);
+

嗯,大概就是这个意思,如果有更严格的权限控制,那在每个请求中凡是涉及到数据库的操作都需要先进行严格的验证,并且在设计数据库表的时候需要考虑进 userId 的账号关联以及权限关联。

+

目录遍历漏洞

目录遍历漏洞指通过在 URL 或参数中构造 .././ 和类似的跨父目录字符串的 ASCII 编码、unicode 编码等,完成目录跳转,读取操作系统各个目录下的敏感文件,也可以称作「任意文件读取漏洞」。

+

目录遍历漏洞原理:程序没有充分过滤用户输入的 ../ 之类的目录跳转符,导致用户可以通过提交目录跳转来遍历服务器上的任意文件。使用多个.. 符号,不断向上跳转,最终停留在根 /,通过绝对路径去读取任意文件。

+

目录遍历漏洞几个示例和测试,一般构造 URL 然后使用浏览器直接访问,或者使用 Web 漏洞扫描工具检测,当然也可以自写程序测试。

+
1
2
3
4
5
6
7
8
9
http://somehost.com/../../../../../../../../../etc/passwd
http://somehost.com/some/path?file=../../Windows/system.ini

# 借助 %00 空字符截断是一个比较经典的攻击手法
http://somehost.com/some/path?file=../../Windows/system.ini%00.js

# 使用了 IIS 的脚本目录来移动目录并执行指令
http://somehost.com/scripts/..%5c../Windows/System32/cmd.exe?/c+dir+c:\

+

目录遍历漏洞防御

方法就是需要对 URL 或者参数进行 ../,./ 等字符的转义过滤。

+

物理路径泄漏

物理路径泄露属于低风险等级缺陷,它的危害一般被描述为「攻击者可以利用此漏洞得到信息,来对系统进一步地攻击」,通常都是系统报错 500 的错误信息直接返回到页面可见导致的漏洞。得到物理路径有些时候它能给攻击者带来一些有用的信息,比如说:可以大致了解系统的文件目录结构;可以看出系统所使用的第三方软件;也说不定会得到一个合法的用户名(因为很多人把自己的用户名作为网站的目录名)。

+

物理路径泄漏防御

防止这种泄漏的方法就是做好后端程序的出错处理,定制特殊的 500 报错页面。

+

源码暴露漏洞

和物理路径泄露类似,就是攻击者可以通过请求直接获取到你站点的后端源代码,然后就可以对系统进一步研究攻击。那么导致源代码暴露的原因是什么呢?基本上就是发生在服务器配置上了,服务器可以设置哪些路径的文件才可以被直接访问的,这里给一个 koa 服务起的例子,正常的 koa 服务器可以通过 koa-static 中间件去指定静态资源的目录,好让静态资源可以通过路径的路由访问。比如你的系统源代码目录是这样的:

+
1
2
3
4
5
6
|- project
|- src
|- static
|- ...
|- server.js

+ +

你想要将 static 的文件夹配成静态资源目录,你应该会在 server.js 做如下配置:

+
1
2
3
4
5
6
const Koa = require('koa');
const serve = require('koa-static');
const app = new Koa();

app.use(serve(__dirname + '/project/static'));

+ +

但是如果配错了静态资源的目录,可能就出大事了,比如:

+
1
2
3
// ...
app.use(serve(__dirname + '/project'));

+ +

这样所有的源代码都可以通过路由访问到了,所有的服务器都提供了静态资源机制,所以在通过服务器配置静态资源目录和路径的时候,一定要注意检验,不然很可能产生漏洞。

+

最后,希望 Web 开发者们能够管理好自己的代码隐私,注意代码安全问题,比如不要将产品的含有敏感信息的代码放到第三方外部站点或者暴露给外部用户,尤其是前端代码,私钥类似的保密性的东西不要直接输出在代码里或者页面中。也许还有很多值得注意的点,但是归根结底还是绷住安全那根弦,对待每一行代码都要多多推敲。

+

转载自:https://zoumiaojiang.com/article/common-web-security/

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:前端常见知识点整理 ---- 网络安全(2)

+

文章作者:

+

发布时间:2022年06月16日 - 14:40

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/16/%E5%89%8D%E7%AB%AF%E5%B8%B8%E8%A7%81%E7%9F%A5%E8%AF%86%E7%82%B9%E6%95%B4%E7%90%86%20----%20%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%EF%BC%882%EF%BC%89/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2022/06/30/nginx-timeout/index.html b/ja/2022/06/30/nginx-timeout/index.html new file mode 100644 index 0000000000..0fd9a47515 --- /dev/null +++ b/ja/2022/06/30/nginx-timeout/index.html @@ -0,0 +1,669 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nginx中的超时设置 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ nginx中的超时设置 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

nginx中的超时设置

Nginx主要有四类超时设置:客户端超时设置DNS解析超时设置代理超时设置,如果使用ngx_lua,则还有lua相关的超时设置。

+

客户端超时设置

对于客户端超时主要设置有读取请求头超时时间读取请求体超时时间发送响应超时时间长连接超时时间。通过客户端超时设置避免客户端恶意或者网络状况不佳造成连接长期占用,影响服务端的可处理的能力。

+

client_header_timeout 指定等待client发送一个请求头的超时时间

    +
  • 语法: client_header_timeout time
  • +
  • 默认值: 60s
  • +
  • 上下文: http server
  • +
  • 说明: 指定等待client发送一个请求头的超时时间(例如:GET / HTTP/1.1).仅当在一次read中,没有收到请求头,才会算成超时。如果在超时时间内,client没发送任何东西,nginx返回HTTP状态码408(“Request timed out”)
  • +
+

client_body_timeout 该指令设置请求体(request body)的读超时时间

    +
  • 语法: client_body_timeout time
  • +
  • 默认值: 60s
  • +
  • 上下文: http server location
  • +
  • 说明: 该指令设置请求体(request body)的读超时时间。仅当在一次readstep中,没有得到请求体,就会设为超时。超时后,nginx返回HTTP状态码408(“Request timed out”)
  • +
+

send_timeout time

设置发送响应到客户端的超时时间,默认为60s,此超时时间指的也是两次成功写操作间隔时间,而不是发送整个响应的超时时间。如果在此超时时间内客户端没有接收任何响应,则Nginx关闭此连接。

+

keepalive_timeout 第一个参数指定了与client的keep-alive连接超时时间

    +
  • 语法: keepalive_timeout timeout [ header_timeout ]

    +
  • +
  • 默认值: 75s

    +
  • +
  • 上下文: http server location

    +
  • +
  • 说明: 第一个参数指定了与client的keep-alive连接超时时间。服务器将会在这个时间后关闭连接。可选的第二个参数指定了在响应头Keep-Alive: timeout=time中的time值。这个头能够让一些浏览器主动关闭连接,这样服务器就不必要去关闭连接了。没有这个参数,nginx不会发送Keep-Alive响应头(尽管并不是由这个头来决定连接是否“keep-alive”)
    两个参数的值可并不相同

    +
      +
    • 注意不同浏览器怎么处理“keep-alive”头
    • +
    • MSIE和Opera忽略掉”Keep-Alive: timeout=“ header.
    • +
    • MSIE保持连接大约60-65秒,然后发送TCP RST
    • +
    • Opera永久保持长连接
    • +
    • Mozilla keeps the connection alive for N plus about 1-10 seconds.
    • +
    • Konqueror保持长连接N秒
    • +
    +
  • +
+

DNS解析超时设置

resolver_timeout 设置DNS解析超时时间

    +
  • 语法 resolver_timeout time
  • +
  • 默认值 30s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置DNS解析超时时间
  • +
+

代理超时设置

proxy_connect_timeout 设置与upstream server的连接超时时间,不能超过75秒

    +
  • 语法 proxy_connect_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置与upstream server的连接超时时间,有必要记住,这个超时不能超过75秒。
    这个不是等待后端返回页面的时间,那是由proxy_read_timeout声明的。如果你的upstream服务器起来了,但是hanging住了(例如,没有足够的线程处理请求,所以把你的请求放到请求池里稍后处理),那么这个声明是没有用的,由于与upstream服务器的连接已经建立了。
  • +
+

proxy_read_timeout 设置与代理服务器的读超时时间

    +
  • 语法 proxy_read_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置与代理服务器的读超时时间。它决定了nginx会等待多长时间来获得请求的响应。这个时间不是获得整个response的时间,而是两次reading操作的时间。
  • +
+

proxy_send_timeout 设置发送请求给upstream服务器的超时时间

    +
  • 语法 proxy_send_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 这个指定设置了发送请求给upstream服务器的超时时间。超时设置不是为了整个发送期间,而是在两次write操作期间。如果超时后,upstream没有收到新的数据,nginx会关闭连接
  • +
+

失败重试机制设置。

proxy_next_upstream error | timeout | invalid_header | http_500 | http_502 | http_503 | http_504 |http_403 | http_404 | non_idempotent | off …:配置什么情况下需要请求下一台上游服务器进行重试。默认为“errortimeout”。error表示与上游服务器建立连接、写请求或者读响应头出错。timeout表示与上游服务器建立连接、写请求或者读响应头超时。invalid_header表示上游服务器返回空的或错误的响应头。http_XXX表示上游服务器返回特定的状态码。non_idempotent表示RFC-2616定义的非幂等HTTP方法(POST、LOCK、PATCH),也可以在失败后重试下一台上游服务器(即默认幂等方法GET、HEAD、PUT、DELETE、OPTIONS、TRACE才可以重试)。off表示禁用重试。

+

重试不能无限制进行,因此,需要如下两个指令控制重试次数和重试超时时间。

+

proxy_next_upstream_tries number:设置重试次数,默认0表示不限制,注意此重试次数指的是所有请求次数(包括第一次和之后的重试次数之和)。

+

proxy_next_upstream_timeout time:设置重试最大超时时间,默认0表示不限制。

+

即在proxy_next_upstream_timeout时间内允许proxy_next_upstream_tries次重试。如果超过了其中一个设置,则Nginx也会结束重试并返回客户端响应(可能是错误码)。

+

如下配置表示当error/timeout时重试upstream中的下一台上游服务器,如果重试的总时间超出了6s或者重试了1次,则表示重试失败(因为之前已经请求一次了,所以还能重试一次),Nginx结束重试并返回客户端响应。

+

upstream存活超时设置

max_fails和fail_timeout:配置什么时候Nginx将上游服务器认定为不可用/不存活。当上游服务器在fail_timeout时间内失败了max_fails次,则认为该上游服务器不可用/不存活。并在接下来的fail_timeout时间内从upstream摘掉该节点(即请求不会转发到该上游服务器)。

+

什么情况下被认定为失败呢?其由 proxy_next_upstream定义,不过,不管 proxy_next_upstream如何配置,error, timeout and invalid_header 都将被认为是失败。

+

如server 192.168.61.1:9090max_fails=2 fail_timeout=10s;表示在10s内如果失败了2次,则在接下来的10s内认定该节点不可用/不存活。这种存活检测机制是只有当访问该上游服务器时,采取惰性检查,可以使用ngx_http_upstream_check_module配置主动检查。

+

max_fails设置为0表示不检查服务器是否可用(即认为一直可用),如果upstream中仅剩一台上游服务器时,则该服务器是不会被摘除的,将从不被认为不可用。

+

proxy_upstream_fail_timeout(fail_timeout)设置某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间

    +
  • 语法 server address [fail_timeout=30s]
  • +
  • 默认值 10s
  • +
  • 上下文 upstream
  • +
  • 说明 Upstream模块下 server指令的参数,设置了某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间,默认为10秒
  • +
+

lingering_timeout

    +
  • 语法: lingering_timeout time
  • +
  • 默认值: 5s
  • +
  • 上下文: http server location
  • +
  • 说明: lingering_close生效后,在关闭连接前,会检测是否有用户发送的数据到达服务器,如果超过lingering_timeout时间后还没有数据可读,就直接关闭连接;否则,必须在读取完连接缓冲区上的数据并丢弃掉后才会关闭连接。
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:nginx中的超时设置

+

文章作者:

+

发布时间:2022年06月30日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/06/30/nginx-timeout/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/08/02/Docker \351\225\234\345\203\217\345\256\211\345\205\250\346\234\200\344\275\263\345\256\236\350\267\265 /index.html" "b/ja/2022/08/02/Docker \351\225\234\345\203\217\345\256\211\345\205\250\346\234\200\344\275\263\345\256\236\350\267\265 /index.html" new file mode 100644 index 0000000000..2b9b54ad75 --- /dev/null +++ "b/ja/2022/08/02/Docker \351\225\234\345\203\217\345\256\211\345\205\250\346\234\200\344\275\263\345\256\236\350\267\265 /index.html" @@ -0,0 +1,609 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 在Docker Compose中配置网络 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 在Docker Compose中配置网络 +

+ + +
+ + + + +
+ + +

Docker 镜像安全最佳实践

Docker 和 宿主机 的设置

    +
  1. 保证宿主机和 Docker 的版本是最新的
  2. +
  3. 不要暴露 Docker 的 守护进程(daemon) 的套接字
  4. +
  5. 使用 rootless 模式启动 Docker
  6. +
  7. 避免使用特权容器
  8. +
  9. 限制容器资源
  10. +
  11. 隔离容器网络
  12. +
  13. 提高容器的隔离度
  14. +
  15. 将文件系统和卷设置为只读
  16. +
  17. 完整的生命周期管理
  18. +
  19. 限制来自容器内的系统调用
  20. +
+

确保镜像安全

    +
  1. 扫描和验证容器镜像
  2. +
  3. 使用最小基础镜像
  4. +
  5. 不要向 Docker 镜像泄露敏感信息
  6. +
  7. 使用多阶段构建
  8. +
  9. 确保容器注册
  10. +
  11. 使用固定标签以获得不变性
  12. +
+

监控容器

    +
  1. 监控容器活动
  2. +
  3. 确保容器在运行时的安全
  4. +
  5. 将故障排除数据与容器分开保存
  6. +
  7. 为镜像使用元数据标签
  8. +
+

参考

Top 20 Dockerfile best practices
Dockerセキュリティベストプラクティス トップ20

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:在Docker Compose中配置网络

+

文章作者:

+

发布时间:2022年08月02日 - 19:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/Docker%20%E9%95%9C%E5%83%8F%E5%AE%89%E5%85%A8%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%20/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/08/02/Network in Compose \345\234\250Docker Compose\344\270\255\351\205\215\347\275\256\347\275\221\347\273\234/index.html" "b/ja/2022/08/02/Network in Compose \345\234\250Docker Compose\344\270\255\351\205\215\347\275\256\347\275\221\347\273\234/index.html" new file mode 100644 index 0000000000..063bc1f17c --- /dev/null +++ "b/ja/2022/08/02/Network in Compose \345\234\250Docker Compose\344\270\255\351\205\215\347\275\256\347\275\221\347\273\234/index.html" @@ -0,0 +1,586 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 在Docker Compose中配置网络 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 在Docker Compose中配置网络 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Network in Compose 在Docker Compose中配置网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
version: "3.9"

services:
proxy:
build: ./proxy
networks:
- actsnetwork
app:
build: ./app
networks:
- actsnetwork
db:
image: postgres
networks:
- testnetwork

networks:
actsnetwork:
name: testnetwork
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:在Docker Compose中配置网络

+

文章作者:

+

发布时间:2022年08月02日 - 19:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/Network%20in%20Compose%20%E5%9C%A8Docker%20Compose%E4%B8%AD%E9%85%8D%E7%BD%AE%E7%BD%91%E7%BB%9C/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/08/02/Nginx\344\270\255\347\232\204\350\266\205\346\227\266\350\256\276\347\275\256/index.html" "b/ja/2022/08/02/Nginx\344\270\255\347\232\204\350\266\205\346\227\266\350\256\276\347\275\256/index.html" new file mode 100644 index 0000000000..b8fe3819fc --- /dev/null +++ "b/ja/2022/08/02/Nginx\344\270\255\347\232\204\350\266\205\346\227\266\350\256\276\347\275\256/index.html" @@ -0,0 +1,652 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Nginx中的超时设置 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Nginx中的超时设置 +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Nginx中的超时设置

client_header_timeout 指定等待client发送一个请求头的超时时间

    +
  • 语法: client_header_timeout time
  • +
  • 默认值: 60s
  • +
  • 上下文: http server
  • +
  • 说明: 指定等待client发送一个请求头的超时时间(例如:GET / HTTP/1.1).仅当在一次read中,没有收到请求头,才会算成超时。如果在超时时间内,client没发送任何东西,nginx返回HTTP状态码408(“Request timed out”)
  • +
+

client_body_timeout 该指令设置请求体(request body)的读超时时间

    +
  • 语法: client_body_timeout time
  • +
  • 默认值: 60s
  • +
  • 上下文: http server location
  • +
  • 说明: 该指令设置请求体(request body)的读超时时间。仅当在一次readstep中,没有得到请求体,就会设为超时。超时后,nginx返回HTTP状态码408(“Request timed out”)
  • +
+

keepalive_timeout 第一个参数指定了与client的keep-alive连接超时时间

    +
  • 语法: keepalive_timeout timeout [ header_timeout ]

    +
  • +
  • 默认值: 75s

    +
  • +
  • 上下文: http server location

    +
  • +
  • 说明: 第一个参数指定了与client的keep-alive连接超时时间。服务器将会在这个时间后关闭连接。可选的第二个参数指定了在响应头Keep-Alive: timeout=time中的time值。这个头能够让一些浏览器主动关闭连接,这样服务器就不必要去关闭连接了。没有这个参数,nginx不会发送Keep-Alive响应头(尽管并不是由这个头来决定连接是否“keep-alive”)
    两个参数的值可并不相同

    +
      +
    • 注意不同浏览器怎么处理“keep-alive”头
    • +
    • MSIE和Opera忽略掉”Keep-Alive: timeout=“ header.
    • +
    • MSIE保持连接大约60-65秒,然后发送TCP RST
    • +
    • Opera永久保持长连接
    • +
    • Mozilla keeps the connection alive for N plus about 1-10 seconds.
    • +
    • Konqueror保持长连接N秒
    • +
    +
  • +
+

lingering_timeout

    +
  • 语法: lingering_timeout time
  • +
  • 默认值: 5s
  • +
  • 上下文: http server location
  • +
  • 说明: lingering_close生效后,在关闭连接前,会检测是否有用户发送的数据到达服务器,如果超过lingering_timeout时间后还没有数据可读,就直接关闭连接;否则,必须在读取完连接缓冲区上的数据并丢弃掉后才会关闭连接。
  • +
+

resolver_timeout 设置DNS解析超时时间

    +
  • 语法 resolver_timeout time
  • +
  • 默认值 30s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置DNS解析超时时间
  • +
+

proxy_connect_timeout 设置与upstream server的连接超时时间,不能超过75秒

    +
  • 语法 proxy_connect_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置与upstream server的连接超时时间,有必要记住,这个超时不能超过75秒。
    这个不是等待后端返回页面的时间,那是由proxy_read_timeout声明的。如果你的upstream服务器起来了,但是hanging住了(例如,没有足够的线程处理请求,所以把你的请求放到请求池里稍后处理),那么这个声明是没有用的,由于与upstream服务器的连接已经建立了。
  • +
+

proxy_read_timeout 设置与代理服务器的读超时时间

    +
  • 语法 proxy_read_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置与代理服务器的读超时时间。它决定了nginx会等待多长时间来获得请求的响应。这个时间不是获得整个response的时间,而是两次reading操作的时间。
  • +
+

proxy_send_timeout 设置发送请求给upstream服务器的超时时间

    +
  • 语法 proxy_send_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 这个指定设置了发送请求给upstream服务器的超时时间。超时设置不是为了整个发送期间,而是在两次write操作期间。如果超时后,upstream没有收到新的数据,nginx会关闭连接
  • +
+

proxy_upstream_fail_timeout(fail_timeout)设置某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间

    +
  • 语法 server address [fail_timeout=30s]
  • +
  • 默认值 10s
  • +
  • 上下文 upstream
  • +
  • 说明 Upstream模块下 server指令的参数,设置了某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间,默认为10秒
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Nginx中的超时设置

+

文章作者:

+

发布时间:2022年08月02日 - 19:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/Nginx%E4%B8%AD%E7%9A%84%E8%B6%85%E6%97%B6%E8%AE%BE%E7%BD%AE/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2022/08/02/Oracle-Linux-install-Docker/index.html b/ja/2022/08/02/Oracle-Linux-install-Docker/index.html new file mode 100644 index 0000000000..137d4b3854 --- /dev/null +++ b/ja/2022/08/02/Oracle-Linux-install-Docker/index.html @@ -0,0 +1,590 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 在 Oracle Linux 中安装 Docker | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 在 Oracle Linux 中安装 Docker +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Oracle Linux install Docker(https://docs.docker.com/engine/install/centos/)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 查看
systemctl list-units

sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine

sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

sudo yum install -y yum-utils

sudo yum -y install docker-ce-20.10.17 docker-ce-cli-20.10.17 containerd.io-1.6.6

sudo vi /etc/yum.repos.d/docker-ce.repo

[centos-extras]
name=Centos extras - $basearch
baseurl=http://mirror.centos.org/centos/7/extras/x86_64
enabled=1
gpgcheck=1
gpgkey=http://centos.org/keys/RPM-GPG-KEY-CentOS-7

sudo yum -y install fuse-overlayfs slirp4netns

# sudo chmod 777 docker-ce.repo

# yum list docker-ce --showduplicates | sort -r

sudo systemctl start docker

sudo docker run hello-world

sudo yum install /path/to/package.rpm
sudo systemctl start docker
sudo docker run hello-world
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:在 Oracle Linux 中安装 Docker

+

文章作者:

+

发布时间:2022年08月02日 - 18:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/Oracle-Linux-install-Docker/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/08/02/OracleLinux\345\206\205\346\240\270\344\273\216uek\345\210\207\346\215\242\345\210\260uek/index.html" "b/ja/2022/08/02/OracleLinux\345\206\205\346\240\270\344\273\216uek\345\210\207\346\215\242\345\210\260uek/index.html" new file mode 100644 index 0000000000..86abfc4a54 --- /dev/null +++ "b/ja/2022/08/02/OracleLinux\345\206\205\346\240\270\344\273\216uek\345\210\207\346\215\242\345\210\260uek/index.html" @@ -0,0 +1,578 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OracleLinux内核从uek切换到uek | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ OracleLinux内核从uek切换到uek +

+ + +
+ + + + +
+ + +
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#/boot/grub/grub.conf 缺失:

yum install -y grub
grub-mkconfig -o /boot/grub/grub.conf

#/boot/grub2/grub.cfg 缺失:

yum install -y grub2
grub2-mkconfig -o /boot/grub2/grub.cfg

uname -a
sudo grep ^menuentry /boot/grub2/grub.cfg | awk -F\' '{print $2}' | nl -v0

sudo grub2-editenv list

sudo grep GRUB_DEFAULT /etc/default/grub

sudo vim /etc/default/grub

sudo grub2-set-default 1

sudo grub2-mkconfig -o /boot/grub2/grub.cfg

sudo dracut --force

sudo reboot

uname -a
+
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:OracleLinux内核从uek切换到uek

+

文章作者:

+

发布时间:2022年08月02日 - 18:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/OracleLinux%E5%86%85%E6%A0%B8%E4%BB%8Euek%E5%88%87%E6%8D%A2%E5%88%B0uek/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/08/02/Postgresql\347\273\277\350\211\262\347\211\210\344\275\277\347\224\250/index.html" "b/ja/2022/08/02/Postgresql\347\273\277\350\211\262\347\211\210\344\275\277\347\224\250/index.html" new file mode 100644 index 0000000000..6c13ed6919 --- /dev/null +++ "b/ja/2022/08/02/Postgresql\347\273\277\350\211\262\347\211\210\344\275\277\347\224\250/index.html" @@ -0,0 +1,586 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Postgresql绿色版使用 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Postgresql绿色版使用 +

+ + +
+ + + + +
+ + +

初始化数据库

c:\pgsql\bin\initdb.exe -D c:\pgsql\data -E UTF8

+

注册表

c:\pgsql\bin\pg_ctl register -N postgres -D c:\pgsql\data

+

启动服务

c:\pgsql\bin\postgres -D c:\pgsql\data

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:Postgresql绿色版使用

+

文章作者:

+

发布时间:2022年08月02日 - 19:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/Postgresql%E7%BB%BF%E8%89%B2%E7%89%88%E4%BD%BF%E7%94%A8/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2022/08/02/linux-scp/index.html b/ja/2022/08/02/linux-scp/index.html new file mode 100644 index 0000000000..49599ff69b --- /dev/null +++ b/ja/2022/08/02/linux-scp/index.html @@ -0,0 +1,599 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 在linux之间传输文件的命令scp | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 在linux之间传输文件的命令scp +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

这里假设要从Linux服务器A中把文件 file.txt 传输到Linux服务器B,该怎么做,这里就可以用scp实现
为了能够成功的传输文件,需要做到以下几点:

+
    +
  1. A需要知道要传输的的文件本地的路径
  2. +
  3. A为了往B传输文件,所以需要知道B的访问权限,因此需要B的访问地址,账号和私钥
  4. +
  5. 目标路径
  6. +
  7. 因为Linux权限管理的要求,私钥的访问权限需要设定为600
  8. +
+

当然还有一点前提是A和B两台服务器之间本身是可以通信的
知道以上信息便可以进行文件传输

+

实践

现在假设

+
    +
  1. A要往B传输的文件在A的如下目录:/tmp/transfer/file.txt
  2. +
  3. B的访问地址是 192.168.31.31 访问用户名是 testuser 用到的key上传到了A的如下目录: /tmp/ssh/test.key
  4. +
  5. 目标路径为B的如下目录: /tmp/test
    知道上面信息后我们来创建命令。
  6. +
+
1
2
chomd 600 /tmp/ssh/test.key
scp -i /tmp/ssh/test.key /tmp/transfer/file.txt testuser@192.168.31.31 /tmp/test
+ +

通过上面的命令即可实现从A服务器传输文件到B服务器了。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:在linux之间传输文件的命令scp

+

文章作者:

+

发布时间:2022年08月02日 - 00:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/linux-scp/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/08/02/systemctl \345\221\275\344\273\244/index.html" "b/ja/2022/08/02/systemctl \345\221\275\344\273\244/index.html" new file mode 100644 index 0000000000..5899a16280 --- /dev/null +++ "b/ja/2022/08/02/systemctl \345\221\275\344\273\244/index.html" @@ -0,0 +1,601 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + systemctl 命令 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ systemctl 命令 +

+ + +
+ + + + +
+ + +

systemctl 命令

systemctl

范列出系统上面有启动的unit

+

systemctl list-unit-files

列出所有已经安装的unit有哪些

+

systemctl list-units –type=service –all

列出类型为service的所有项目,不论启动与否

+

systemctl get-default

输入目前机器默认的模式,如图形界面模式或者文本模式

+

systemctl isolate multi-user.target

将目前的操作环境改为纯文本模式,关掉图形界面

+

systemctl isolate graphical.target

将目前的操作环境改为图形界面

+

systemctl poweroff

系统关机

+

systemctl reboot

重新开机

+

systemctl suspend

进入暂停模式

+

systemctl rescue

强制进入救援模式

+

systemctl hibernate

进入休眠模式

+

systemctl emergency

强制进入紧急救援模式

+

systemctl list-dependencies –reverse

查询当前默认的target关联了啥

+

systemctl list-dependencies graphical.target

查询图形界面模式的target关联了啥

+

systemctl list-sockets

查看当前的socket服务

+

systemctl show etcd.service

查看 unit 的详细配置情况

+

systemctl mask etcd.service

禁用某个服务

+

systemctl unmask etcd.service

解除禁用某个服务

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:systemctl 命令

+

文章作者:

+

发布时间:2022年08月02日 - 18:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/systemctl%20%E5%91%BD%E4%BB%A4/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/08/02/\346\220\255\345\273\272Docker\347\247\201\346\234\211\344\273\223\345\272\223/index.html" "b/ja/2022/08/02/\346\220\255\345\273\272Docker\347\247\201\346\234\211\344\273\223\345\272\223/index.html" new file mode 100644 index 0000000000..915fc22411 --- /dev/null +++ "b/ja/2022/08/02/\346\220\255\345\273\272Docker\347\247\201\346\234\211\344\273\223\345\272\223/index.html" @@ -0,0 +1,595 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 搭建Docker私有仓库 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 搭建Docker私有仓库 +

+ + +
+ + + + +
+ + +

搭建Docker私有仓库

利用registry搭建私有仓库

+

下载registry

1
docker pull registry
+ +

配置

1
2
/etc/docker/doemon.json
# 配置 'insecure-registry'
+

重启docker

1
systemctl restart docker
+ +

创建registry容器(关联私有仓库配置)

1
docker run -d -p 5000:5000 --name registry registry:latest
+ +

推送镜像到私有仓

    +
  • 备份镜像(172.16.12.134:5000 私有仓地址)

    +
    1
    docker tag ubuntu:latest 172.16.12.134:5000/my_ubuntu
  • +
  • 推送

    +
    1
    docker push 172.16.12.134:5000/my_ubuntu
  • +
  • 下载

    +
    1
    docker pull 172.16.12.134:5000/my_ubuntu
  • +
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:搭建Docker私有仓库

+

文章作者:

+

发布时间:2022年08月02日 - 18:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/%E6%90%AD%E5%BB%BADocker%E7%A7%81%E6%9C%89%E4%BB%93%E5%BA%93/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2022/08/02/\346\234\215\345\212\241\345\231\250\351\253\230\345\215\261\347\253\257\345\217\243\345\210\227\350\241\250/index.html" "b/ja/2022/08/02/\346\234\215\345\212\241\345\231\250\351\253\230\345\215\261\347\253\257\345\217\243\345\210\227\350\241\250/index.html" new file mode 100644 index 0000000000..128c772723 --- /dev/null +++ "b/ja/2022/08/02/\346\234\215\345\212\241\345\231\250\351\253\230\345\215\261\347\253\257\345\217\243\345\210\227\350\241\250/index.html" @@ -0,0 +1,865 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 服务器高危端口列表 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 服务器高危端口列表 +

+ + +
+ + + + +
+ + +

服务器高危端口列表

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
协议端口服务渗透测试
tcp20,21FTP(文件传输协议)允许匿名的上传下载,爆破,嗅探,win提权,远程执行(proftpd 1.3.5),各类后门(proftpd,vsftp 2.3.4)
tcp22SSH (安全外壳协议 )可根据已搜集到的信息尝试爆破,v1版本可中间人,ssh隧道及内网代理转发,文件传输等等
tcp23Telnet ( 远程终端协议)爆破,嗅探,一般常用于路由,交换登陆,可尝试弱口令
tcp25SMTP(简单邮件传输协议)邮件伪造,vrfy/expn查询邮件用户信息,可使用smtp-user-enum工具来自动跑
tcp/udp53DNS(域名系统)允许区域传送,dns劫持,缓存投毒,欺骗以及各种基于dns隧道的远控
tcp/udp69TFTP (简单文件传送协议 )尝试下载目标及其的各类重要配置文件
tcp80-89,443,8440-8450,8080-8089各种常用的Web服务端口,可尝试经典的topn,vpn,owa,webmail,目标oa,各类Java控制台,各类服务器Web管理面板,各类Web中间件漏洞利用,各类Web框架漏洞利用等等……
tcp110POP3(邮局协议版本3 )可尝试爆破,嗅探
tcp111,2049NFS(网络文件系统)权限配置不当
tcp137,139,445SMB(NETBIOS协议)可尝试爆破以及smb自身的各种远程执行类漏洞利用,如,ms08-067,ms17-010,嗅探等……
tcp143IMAP(邮件访问协议)可尝试爆破
udp161SNMP(简单网络管理协议)爆破默认团队字符串,搜集目标内网信息
tcp389LDAP( 轻量目录访问协议 )ldap注入,允许匿名访问,弱口令
tcp512,513,514Linux rexec (远程登录)可爆破,rlogin登陆
tcp873Rsync (数据镜像备份工具)匿名访问,文件上传
tcp1194OpenVPN(虚拟专用通道)想办法钓VPN账号,进内网
tcp1352Lotus(Lotus软件)弱口令,信息泄漏,爆破
tcp1433SQL Server(数据库管理系统)注入,提权,sa弱口令,爆破
tcp1521Oracle(甲骨文数据库)tns爆破,注入,弹shell…
tcp1500ISPmanager( 主机控制面板)弱口令
tcp1723PPTP(点对点隧道协议 )爆破,想办法钓VPN账号,进内网
tcp2082,2083cPanel (虚拟机控制系统 )弱口令
tcp2181ZooKeeper(分布式系统的可靠协调系统 )未授权访问
tcp2601,2604Zebra (zebra路由)默认密码zerbra
tcp3128Squid (代理缓存服务器)弱口令
tcp3312,3311kangle(web服务器)弱口令
tcp3306MySQL(数据库)注入,提权,爆破
tcp3389Windows rdp(桌面协议)shift后门[需要03以下的系统],爆破,ms12-020
tcp3690SVN(开放源代码的版本控制系统)svn泄露,未授权访问
tcp4848GlassFish(应用服务器)弱口令
tcp5000Sybase/DB2(数据库)爆破,注入
tcp5432PostgreSQL(数据库)爆破,注入,弱口令
tcp5900,5901,5902VNC(虚拟网络控制台,远控)弱口令爆破
tcp5984CouchDB(数据库)未授权导致的任意指令执行
tcp6379Redis(数据库)可尝试未授权访问,弱口令爆破
tcp7001,7002WebLogic(WEB应用系统)Java反序列化,弱口令
tcp7778Kloxo(虚拟主机管理系统)主机面板登录
tcp8000Ajenti(Linux服务器管理面板)弱口令
tcp8443Plesk(虚拟主机管理面板)弱口令
tcp8069Zabbix (系统网络监视)远程执行,SQL注入
tcp8080-8089Jenkins,JBoss (应用服务器)反序列化,控制台弱口令
tcp9080-9081,9090WebSphere(应用服务器)Java反序列化/弱口令
tcp9200,9300ElasticSearch (Lucene的搜索服务器)远程执行
tcp11211Memcached(缓存系统)未授权访问
tcp27017,27018MongoDB(数据库)爆破,未授权访问
tcp50070,50030Hadoop(分布式文件系统)默认端口未授权访问
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:服务器高危端口列表

+

文章作者:

+

发布时间:2022年08月02日 - 18:00

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2022/08/02/%E6%9C%8D%E5%8A%A1%E5%99%A8%E9%AB%98%E5%8D%B1%E7%AB%AF%E5%8F%A3%E5%88%97%E8%A1%A8/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/2023/09/20/DockerCompose\345\256\211\350\243\205/index.html" "b/ja/2023/09/20/DockerCompose\345\256\211\350\243\205/index.html" new file mode 100644 index 0000000000..53dc3fca01 --- /dev/null +++ "b/ja/2023/09/20/DockerCompose\345\256\211\350\243\205/index.html" @@ -0,0 +1,561 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ +

+ + +
+ + + + +
+ + +

DockerCompose安装

1
2
3
4
5
DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
mkdir -p $DOCKER_CONFIG/cli-plugins
sudo curl -SL https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-x86_64 -o /usr/bin/docker-compose
sudo chmod +x /usr/bin/docker-compose
sudo docker-compose version
+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:

+

文章作者:

+

发布时间:2023年09月20日 - 10:18

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2023/09/20/DockerCompose%E5%AE%89%E8%A3%85/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2023/09/20/test/index.html b/ja/2023/09/20/test/index.html new file mode 100644 index 0000000000..178b1fd783 --- /dev/null +++ b/ja/2023/09/20/test/index.html @@ -0,0 +1,590 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + test | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ test +

+ + +
+ + + + +
+ + +

前言

最近各种技术博客,各种技术文章,各种牛人前辈的学习心得都在看,最近看了一个不错的文章叫做《如何正确使用时间》
感觉还是很不错的,特别适合年轻的程序员看一下。下面将全文贴在下方,如果需要也可以去原文查看。

+

原文

+

你是否时常会焦虑时间过的很快,没时间学习,本文将会分享一些个人的见解。

+

花时间补基础,读文档

在工作中我们时常会花很多时间去 debug,但是你是否发现很多问题最终只是你基础不扎实或者文档没有仔细看。

+

基础是你技术的基石,一定要花时间打好基础,而不是追各种新的技术。一旦你的基础扎实,学习各种新的技术也肯定不在话下,因为新的技术,究其根本都是相通的。

+

文档同样也是一门技术的基础。一个优秀的库,开发人员肯定已经把如何使用这个库都写在文档中了,仔细阅读文档一定会是少写 bug 的最省事路子。

+

学会搜索

如果你还在使用百度搜索编程问题,请尽快抛弃这个垃圾搜索引擎。同样一个关键字,使用百度和谷歌,谷歌基本完胜的。即使你使用中文在谷歌中搜索,得到的结果也往往是谷歌占优,所以如果你想迅速的通过搜索引擎来解决问题,那一定是谷歌。

+

学点英语

说到英语,一定是大家所最不想听的。其实我一直认为程序员学习英语是简单的,因为我们工作中是一直接触着英语,并且看懂技术文章,文档所需要的单词量是极少的。我时常在群里看到大家发出一个问题的截图问什么原因,其实在截图中英语已经很明白的说明了问题的所在,如果你的英语过关,完全不需要浪费时间来提问和搜索。所以我认为学点英语也是节省时间中很重要的一点。

+

那么如何去学习呢,chrome 装个翻译插件,直接拿英文文档或文章读,不会的就直接划词翻译,然后记录下这个单词并背诵。每天花半小时看点英文文档和文章,坚持两个月,你的英语水平不说别的,看文档和文章绝对不会有难题了。这一定是一个很划算的个人时间投资,花点时间学习英语,能为你将来的技术之路铺平很多坎。

+

画个图,想一想再做

你是否遇到过这种问题,需求一下来,看一眼,然后马上就按照设计稿开始做了,可能中间出个问题导致你需要返工。

+

如果你存在这样的问题,我很推荐在看到设计稿和需求的时候花点时间想一想,画一画。考虑一下设计稿中是否可以找到可以拆分出来的复用组件,是否存在之前写过的组件。该如何组织这个界面,数据的流转是怎么样的。然后画一下这个页面的需求,最后再动手做。

+

利用好下班时间学习

说到下班时间,那可能就有人说了公司很迟下班,这其实是国内很普遍的情况。但是我认为正常的加班是可以的,但是强制的加班就是在损耗你的身体和前途。

+

可以这么说,大部分的 996 公司,加班的这些时间并不会增加你的技术,无非就是在写一些重复的业务逻辑。也许你可以拿到更多的钱,但是代价是身体还有前途。程序员是靠技术吃饭的,如果你长久呆在一个长时间加班的公司,不能增长你的技术还要吞噬你的下班学习时间,那么你一定会废掉的。如果你遇到了这种情况,只能推荐尽快跳槽到非 996 的公司。

+

那么如果你有足够的下班时间,一定要花上 1, 2 小时去学习,上班大家基本都一样,技术的精进就是看下班以后的那几个小时了。如果你能利用好下班时间来学习,坚持下去,时间一定会给你很好的答复。

+

列好 ToDo

我喜欢规划好一段时间内要做的事情,并且要把事情拆分为小点。给 ToDo 列好优先级,紧急的优先级最高。相同优先级的我喜欢先做简单的,因为这样一旦完成就能划掉一个,提高成就感。

+

反思和整理

每周末都会花上点时间整理下本周记录的笔记和看到的不错文章。然后考虑下本周完成的工作和下周准备要完成的工作。

+ +
+ + + + +
+
如果觉得不错请支持作者
+ + +
+ + +
+
+ ------ 版权声明 ------ + + +

本文标题:test

+

文章作者:

+

发布时间:2023年09月20日 - 18:33

+

最后更新:2023年09月20日 - 10:18

+

原始链接:https://blog.lifesli.com/2023/09/20/test/ + +

+

许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

+
+ +
+ + +
--> + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/@attachment/README.html b/ja/@attachment/README.html new file mode 100644 index 0000000000..77dfc07fbb --- /dev/null +++ b/ja/@attachment/README.html @@ -0,0 +1,485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | 李旭光的成长博客 + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + + + +
+ + + + + +
+
+ +

+

+ + + +
+ + + + +
+

这里追加图片

+
+ + + +
+ + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/about/index.html b/ja/about/index.html new file mode 100644 index 0000000000..a5f6b390cd --- /dev/null +++ b/ja/about/index.html @@ -0,0 +1,705 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 私の履歴書 | 李旭光的成长博客 + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + + + +
+ + + + + +
+
+ +

私の履歴書 +

+ + + +
+ + + + +
+

ZH | EN | JA

+

リ キョクコウ

+

フロントエンド開発 10年
スマホ: 15641181846
メール: lixuguang316@foxmail.com
ギットハブ: https://github.com/lixuguang
ブログ: https://lixuguang.github.io
アドレス: 大连沙河口区黄河路852号

+
+

持てる開発技術

    +
  • 熟练使用Html/Css/Javascript/Jq,响应式开发,移动端Web开发

    +
  • +
  • 熟练使用VUE/vuex/axios/vue-router/element-ui等vue全家桶完成项目/组件开发

    +
  • +
  • 熟练进行前后端分离开发,前后端接口联调,基于git的版本管理,CICD开发等。

    +
  • +
  • 熟练使用chrome-dev对项目进行前端代码调试/调优

    +
  • +
  • 掌握Nodejs/Electron/React/less/sass/bootstrap等开发

    +
  • +
  • 掌握webpack基本配置,npm包管理等前端工程化开发方式

    +
  • +
  • 掌握前端架构方法,对前端规范性要求非常熟悉,能够根据产品/项目需求给出最优技术选型方案。

    +
  • +
  • 熟练使用svn/git等代码版本管理工具及gitflow管理方式进行代码管理

    +
  • +
  • 8年前端开发经验,参与过100+的项目开发。

    +
  • +
  • 善于团队协作,自我驱动,持续学习,热爱工作,责任心强。

    +

    働いていた会社

    +

    華宇(大連)信息服务有限公司:主要面向政府、公检法机关及互联网市场大客户。
    2014.5-至今
    资深前端开发工程师 团队规模:30人 公司规模:600人
    主要职责:

    +
    +
  • +
  • 编写代码

    +
      +
    • 完成正常项目任务
        +
      • 累计完成项目超过150个
      • +
      • 参与项目类型包括pcWeb、padWeb、手机Web、微信Web、触摸屏、指挥控制大屏
      • +
      • 业务类型涉及政府、公检法各个领域
      • +
      • 使用技术包含HTML4/5、CSS2/3、ES5/6、bootstrap、framework7、Angular、Vue以及Artery框架等
      • +
      +
    • +
    • 极短时间内完成任务
        +
      • 竞标项目如:
          +
        • 5天60个页面带领一个初级前端和一个中级前端完成。
        • +
        • 3天完成一个问卷调查包含各种交互的特效。
        • +
        • 封闭1星期同3位开发共同完成canvas画板功能,支持检察官记录员实现档案卷宗实时编辑。
        • +
        +
      • +
      • 演示项目如:
          +
        • 1个月带领一个初级前端一个初级开发完成整个系统包含业务逻辑除后台的所有功能,实现超高保真原型。
        • +
        +
      • +
      • 救火任务如:
          +
        • 上线前2天解决其他人无法解决的问题,此类情况居多。
        • +
        • 项目工期提前,顶着压力加班完成任务。如1天13个页面。
        • +
        +
      • +
      +
    • +
    • 新技术框架研究应用
        +
      • 前后端分离技术
          +
        • 组织前端团队学习前后端分离思想,研究前后端分离所要学习的新技能知识,面向对象的思想以及面向接口的思想等。
        • +
        +
      • +
      • 前端自动化构建、自动检测
          +
        • 组织前端团队学习前端自动化构建如:npm包管理工具、各种cli工具使用、eslint、stylint等自动化检查工具,以及webpack和gulp等打包工具使用。
        • +
        +
      • +
      • MVVM框架等新技术
          +
        • 组织学习研究Angular2、Vue2等数据驱动的MVVM框架,并在实际项目中使用VUE框架。
        • +
        +
      • +
      +
    • +
    • 组织构建基础服务
        +
      • 前端通用组件库
      • +
      • Vue组件库
      • +
      • 前端题库
      • +
      • +
      +
    • +
    +
  • +
  • 制定规范

    +
      +
    • 前端编码规范
        +
      • 包括HTML规范、CSS规范、JS规范、Vue规范、通用规范等。
      • +
      +
    • +
    • 团队协作制度
        +
      • 包括代码管理制度、团队协作规范制度。
      • +
      +
    • +
    • 代码审查规范
        +
      • 人工审核
      • +
      • 自动化检测工具审核
      • +
      +
    • +
    • 知识考核规范
        +
      • 前端知识点地图
      • +
      • 前端技能栈思维导图
      • +
      • 前端考试平台
      • +
      +
    • +
    +
  • +
  • 团队建设

    +
      +
    • 面试官
        +
      • 从原来的2人前端团队发展至16人前端团队,4年流失率仅为3人。
      • +
      +
    • +
    • 组织师傅帮带
        +
      • 组织先进带后进,高级带初级,建立前端梯队,组织共同学习。
      • +
      +
    • +
    • 前端兴趣小组
        +
      • 每周二组织前端兴趣小组,了解当下最新的前端知识,组织分享活动。
      • +
      +
    • +
    • 前端培训
        +
      • 不定期组织学习公司将要使用的新技术以及开发过程中遇到的问题解决方案
      • +
      • 定期组织基础知识、中级知识、高级知识、实践应用四个层面的培训。
      • +
      +
    • +
    +
  • +
  • 人才培养

    +
      +
    • 人员考核
        +
      • 根据建立的知识点地图建立考点地图,建立试卷进行考评,关系到员工晋升。
      • +
      +
    • +
    • 建立人才档案,定期谈话
        +
      • 根据不同人的特点建立对应的人才档案,通过季度谈话的方式了解员工最新动态,根据员工特点设立学习方向和目标,引导员工成长。
      • +
      +
    • +
    • 参与人才晋升考官
        +
      • 参与员工晋升考核,给出努力方向。
      • +
      +
    • +
    +
  • +
+
+
+

大连新桥科技发展有限公司:面向教育系统提供整体服务。
2013.1-2014.1
用户体验团队负责人 团队规模:6人 公司规模:50人
主要职责:

+
+
    +
  • 接访客户
      +
    • 去客户现场或客户到工作现场,负责接待客户,了解客户需求。
    • +
    +
  • +
  • 设计、编写代码
      +
    • 根据客户或领导需求完成需求分析、视觉设计、代码编写等任务。涉及pcWeb、padWeb、手机Web等。
    • +
    +
  • +
  • 团队管理
      +
    • 根据任务量分配工作,协调团队资源。
    • +
    +
  • +
+
+
+

百维数元信息科技(北京)有限公司:一斑网在线调研平台。
2011.9-2012.12
开发工程师 团队规模:3人 公司规模:8人
主要职责:

+
+
    +
  • 设计、前后台代码编写、运营、客服
      +
    • 初创公司,开发运营团队3人,主要负责设计工作、前端开发、部分后端开发、运营、客服等工作。

      教育经历

      +

      大连外国语大学
      2008.9-2012.6
      信息系统与信息管理(日英双语强化)
      本科
      主要职务:

      +
      +
    • +
    • 计算机部部长
        +
      • 组织计算机竞赛、考级
      • +
      +
    • +
    • 团支书
        +
      • 组织班级党员发展、思想工作。
      • +
      +
    • +
    • 网络协会核心成员
        +
      • 为在校生提供vod服务,翻译日英影音资料供大家学习。
        +

        主要荣誉:

        +
        +
      • +
      +
    • +
    • 学习标兵
    • +
    • 优秀团支书
    • +
    • 优秀毕业生
    • +
    • 每一季度奖学金(一、二、三等均获得过)

      项目经历

    • +
    +
  • +
  • ERP系统
      +
    • 法院执行线索分析系统v2.1-v2.2
    • +
    • 数据质量检查系统v2.0-v2.3
    • +
    • 法官办案辅助系统
    • +
    • 量刑规范化服务系统
    • +
    • 裁判文书上网直报系统V2.0
    • +
    • 律师阅卷管理系统
    • +
    • … 100+
    • +
    +
  • +
  • CMS网站
      +
    • 诉讼服务网系列 50+
    • +
    • 法院官网 20+
    • +
    • … 30+
    • +
    +
  • +
  • pad、手机、触控屏系统
      +
    • 信息引导侦查系列产品
    • +
    • 远程视频会见系统2.1
    • +
    • 领导驾驶舱
    • +
    • 移动办案APP
    • +
    • … 30+
    • +
    +
  • +
  • 普通网站
      +
    • 企业官网、政府官网 10+
    • +
    +
  • +
+
+

涉及技术包含:
语言:
HTML4/5、CSS2/3、ES5/6、JSP…
框架:
JQ、bootstarp、mui、framework7、VUE…
理论:
前后端分离(面向对象OOB + 异步请求ajax + 面向接口api)

+
+

自我介绍

从2011年算起我已经从事前端开发8年、设计2年(重叠)、团队管理4年(重叠),参与各类大小项目200+,涉及各个业务领域包括公检法政、教育、调研,覆盖目前主流的前端技术(游戏向不包含)。基础能力扎实,能够解决绝大多数普通问题和部分棘手问题,从小在军营成长让我对团队纪律与制度有着深刻的认识,知道规范与制度的重要性,这为我建立高效优质的团队提供了良好土壤。谦逊使我可以与团队中以及团队之间有着良好的沟通,好学让我不断逃出舒适区,让自己不断的学习进步。有着个人荣誉感与集体荣誉感,让我对工作和团队认真负责。抗压能力强,让领导放心把最紧急最重要的工作交给我做,加班也毫无怨言。工作久了让我总结了一些工作中常见的问题,我会讲这些分享给他人,让大家一同进步,将问题扼杀在摇篮里。愿意组织活动参加活动,喜欢阅读,跑步,骑行,篮球。

+ +
+ + + +
+ + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2013/06/index.html b/ja/archives/2013/06/index.html new file mode 100644 index 0000000000..04e67b5090 --- /dev/null +++ b/ja/archives/2013/06/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2013 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2013/index.html b/ja/archives/2013/index.html new file mode 100644 index 0000000000..f99452b94a --- /dev/null +++ b/ja/archives/2013/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2013 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2015/12/index.html b/ja/archives/2015/12/index.html new file mode 100644 index 0000000000..3c88bec416 --- /dev/null +++ b/ja/archives/2015/12/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2015 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2015/index.html b/ja/archives/2015/index.html new file mode 100644 index 0000000000..71c6a9cf89 --- /dev/null +++ b/ja/archives/2015/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2015 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2016/10/index.html b/ja/archives/2016/10/index.html new file mode 100644 index 0000000000..659dd3c305 --- /dev/null +++ b/ja/archives/2016/10/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2016 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2016/index.html b/ja/archives/2016/index.html new file mode 100644 index 0000000000..8dcf17cd83 --- /dev/null +++ b/ja/archives/2016/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2016 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2017/08/index.html b/ja/archives/2017/08/index.html new file mode 100644 index 0000000000..cbb5323e0f --- /dev/null +++ b/ja/archives/2017/08/index.html @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2017 +
+ + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2017/10/index.html b/ja/archives/2017/10/index.html new file mode 100644 index 0000000000..7d850247c6 --- /dev/null +++ b/ja/archives/2017/10/index.html @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2017 +
+ + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2017/12/index.html b/ja/archives/2017/12/index.html new file mode 100644 index 0000000000..08278128cb --- /dev/null +++ b/ja/archives/2017/12/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2017 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2017/index.html b/ja/archives/2017/index.html new file mode 100644 index 0000000000..502132629a --- /dev/null +++ b/ja/archives/2017/index.html @@ -0,0 +1,618 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2017 +
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2019/12/index.html b/ja/archives/2019/12/index.html new file mode 100644 index 0000000000..a3806244cf --- /dev/null +++ b/ja/archives/2019/12/index.html @@ -0,0 +1,558 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2019 +
+ + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2019/index.html b/ja/archives/2019/index.html new file mode 100644 index 0000000000..0bd7522339 --- /dev/null +++ b/ja/archives/2019/index.html @@ -0,0 +1,558 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2019 +
+ + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2020/01/index.html b/ja/archives/2020/01/index.html new file mode 100644 index 0000000000..c641b4dd2f --- /dev/null +++ b/ja/archives/2020/01/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2020/01/page/2/index.html b/ja/archives/2020/01/page/2/index.html new file mode 100644 index 0000000000..431ab304d6 --- /dev/null +++ b/ja/archives/2020/01/page/2/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2020/01/page/3/index.html b/ja/archives/2020/01/page/3/index.html new file mode 100644 index 0000000000..f1c6b01f86 --- /dev/null +++ b/ja/archives/2020/01/page/3/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2020/01/page/4/index.html b/ja/archives/2020/01/page/4/index.html new file mode 100644 index 0000000000..3e1f8112f0 --- /dev/null +++ b/ja/archives/2020/01/page/4/index.html @@ -0,0 +1,601 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2020/02/index.html b/ja/archives/2020/02/index.html new file mode 100644 index 0000000000..e91cfb32e0 --- /dev/null +++ b/ja/archives/2020/02/index.html @@ -0,0 +1,598 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2020/03/index.html b/ja/archives/2020/03/index.html new file mode 100644 index 0000000000..9721fb3d43 --- /dev/null +++ b/ja/archives/2020/03/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2020/03/page/2/index.html b/ja/archives/2020/03/page/2/index.html new file mode 100644 index 0000000000..4338d76f5b --- /dev/null +++ b/ja/archives/2020/03/page/2/index.html @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2020/04/index.html b/ja/archives/2020/04/index.html new file mode 100644 index 0000000000..9461582bf8 --- /dev/null +++ b/ja/archives/2020/04/index.html @@ -0,0 +1,578 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2020/05/index.html b/ja/archives/2020/05/index.html new file mode 100644 index 0000000000..d5c3396835 --- /dev/null +++ b/ja/archives/2020/05/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2020/07/index.html b/ja/archives/2020/07/index.html new file mode 100644 index 0000000000..1a852a4f9f --- /dev/null +++ b/ja/archives/2020/07/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2020/08/index.html b/ja/archives/2020/08/index.html new file mode 100644 index 0000000000..50b41f0ed1 --- /dev/null +++ b/ja/archives/2020/08/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2020/11/index.html b/ja/archives/2020/11/index.html new file mode 100644 index 0000000000..10f754bddf --- /dev/null +++ b/ja/archives/2020/11/index.html @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2020/index.html b/ja/archives/2020/index.html new file mode 100644 index 0000000000..42953aa985 --- /dev/null +++ b/ja/archives/2020/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2020/page/2/index.html b/ja/archives/2020/page/2/index.html new file mode 100644 index 0000000000..5245576d23 --- /dev/null +++ b/ja/archives/2020/page/2/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2020/page/3/index.html b/ja/archives/2020/page/3/index.html new file mode 100644 index 0000000000..007fb3d57f --- /dev/null +++ b/ja/archives/2020/page/3/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2020/page/4/index.html b/ja/archives/2020/page/4/index.html new file mode 100644 index 0000000000..11f57cde04 --- /dev/null +++ b/ja/archives/2020/page/4/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2020/page/5/index.html b/ja/archives/2020/page/5/index.html new file mode 100644 index 0000000000..77874a40fe --- /dev/null +++ b/ja/archives/2020/page/5/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2020/page/6/index.html b/ja/archives/2020/page/6/index.html new file mode 100644 index 0000000000..528e1bd6f1 --- /dev/null +++ b/ja/archives/2020/page/6/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2020/page/7/index.html b/ja/archives/2020/page/7/index.html new file mode 100644 index 0000000000..f4b5f1a1de --- /dev/null +++ b/ja/archives/2020/page/7/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2020/page/8/index.html b/ja/archives/2020/page/8/index.html new file mode 100644 index 0000000000..c9b5077caf --- /dev/null +++ b/ja/archives/2020/page/8/index.html @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2021/05/index.html b/ja/archives/2021/05/index.html new file mode 100644 index 0000000000..e84b500a2b --- /dev/null +++ b/ja/archives/2021/05/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2021 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2021/index.html b/ja/archives/2021/index.html new file mode 100644 index 0000000000..308de079e3 --- /dev/null +++ b/ja/archives/2021/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2021 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2022/03/index.html b/ja/archives/2022/03/index.html new file mode 100644 index 0000000000..1354337371 --- /dev/null +++ b/ja/archives/2022/03/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2022 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2022/06/index.html b/ja/archives/2022/06/index.html new file mode 100644 index 0000000000..813e6e3217 --- /dev/null +++ b/ja/archives/2022/06/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2022/06/page/2/index.html b/ja/archives/2022/06/page/2/index.html new file mode 100644 index 0000000000..a10c2b3d23 --- /dev/null +++ b/ja/archives/2022/06/page/2/index.html @@ -0,0 +1,621 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2022/08/index.html b/ja/archives/2022/08/index.html new file mode 100644 index 0000000000..747fb09cf6 --- /dev/null +++ b/ja/archives/2022/08/index.html @@ -0,0 +1,638 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2022/index.html b/ja/archives/2022/index.html new file mode 100644 index 0000000000..8c40d77062 --- /dev/null +++ b/ja/archives/2022/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2022/page/2/index.html b/ja/archives/2022/page/2/index.html new file mode 100644 index 0000000000..56d9870c6b --- /dev/null +++ b/ja/archives/2022/page/2/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2022/page/3/index.html b/ja/archives/2022/page/3/index.html new file mode 100644 index 0000000000..04ec1c7588 --- /dev/null +++ b/ja/archives/2022/page/3/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2022/page/4/index.html b/ja/archives/2022/page/4/index.html new file mode 100644 index 0000000000..5f0da6f357 --- /dev/null +++ b/ja/archives/2022/page/4/index.html @@ -0,0 +1,461 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2022 +
+ + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2023/09/index.html b/ja/archives/2023/09/index.html new file mode 100644 index 0000000000..e76f0575ea --- /dev/null +++ b/ja/archives/2023/09/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2023 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/2023/index.html b/ja/archives/2023/index.html new file mode 100644 index 0000000000..2e72f323b8 --- /dev/null +++ b/ja/archives/2023/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2023 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/index.html b/ja/archives/index.html new file mode 100644 index 0000000000..4787f173e1 --- /dev/null +++ b/ja/archives/index.html @@ -0,0 +1,644 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2023 +
+ + + + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/page/10/index.html b/ja/archives/page/10/index.html new file mode 100644 index 0000000000..2f9f8370b3 --- /dev/null +++ b/ja/archives/page/10/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/page/11/index.html b/ja/archives/page/11/index.html new file mode 100644 index 0000000000..ce04f7d1b1 --- /dev/null +++ b/ja/archives/page/11/index.html @@ -0,0 +1,644 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + +
+ 2019 +
+ + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/page/12/index.html b/ja/archives/page/12/index.html new file mode 100644 index 0000000000..3babd6ec5f --- /dev/null +++ b/ja/archives/page/12/index.html @@ -0,0 +1,644 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2019 +
+ + + + + + + + + + +
+ 2017 +
+ + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/page/13/index.html b/ja/archives/page/13/index.html new file mode 100644 index 0000000000..abec2ef38e --- /dev/null +++ b/ja/archives/page/13/index.html @@ -0,0 +1,610 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2017 +
+ + + + + + + + +
+ 2016 +
+ + +
+ 2015 +
+ + +
+ 2013 +
+ + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/page/2/index.html b/ja/archives/page/2/index.html new file mode 100644 index 0000000000..de31084708 --- /dev/null +++ b/ja/archives/page/2/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/page/3/index.html b/ja/archives/page/3/index.html new file mode 100644 index 0000000000..dfa68cbdf9 --- /dev/null +++ b/ja/archives/page/3/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/page/4/index.html b/ja/archives/page/4/index.html new file mode 100644 index 0000000000..29d0ad8441 --- /dev/null +++ b/ja/archives/page/4/index.html @@ -0,0 +1,647 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2022 +
+ + + + + + +
+ 2021 +
+ + + + +
+ 2020 +
+ + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/page/5/index.html b/ja/archives/page/5/index.html new file mode 100644 index 0000000000..44ee5a1bb0 --- /dev/null +++ b/ja/archives/page/5/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/page/6/index.html b/ja/archives/page/6/index.html new file mode 100644 index 0000000000..fc56283b46 --- /dev/null +++ b/ja/archives/page/6/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/page/7/index.html b/ja/archives/page/7/index.html new file mode 100644 index 0000000000..604eb1e175 --- /dev/null +++ b/ja/archives/page/7/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/page/8/index.html b/ja/archives/page/8/index.html new file mode 100644 index 0000000000..19dcf5f134 --- /dev/null +++ b/ja/archives/page/8/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/archives/page/9/index.html b/ja/archives/page/9/index.html new file mode 100644 index 0000000000..fe685e9b4d --- /dev/null +++ b/ja/archives/page/9/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + アーカイブ | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+ + いいね! 全 128 ポスト もっと書こう! +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/categories/Database/Postgresql/index.html b/ja/categories/Database/Postgresql/index.html new file mode 100644 index 0000000000..48c87115fa --- /dev/null +++ b/ja/categories/Database/Postgresql/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: Postgresql | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Postgresql + カテゴリ +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/categories/Database/index.html b/ja/categories/Database/index.html new file mode 100644 index 0000000000..50138d4412 --- /dev/null +++ b/ja/categories/Database/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: Database | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Database + カテゴリ +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/categories/Docker/index.html b/ja/categories/Docker/index.html new file mode 100644 index 0000000000..20a9535706 --- /dev/null +++ b/ja/categories/Docker/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: Docker | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Docker + カテゴリ +

+
+ + +
+ 2022 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/categories/Infra/Docker/index.html b/ja/categories/Infra/Docker/index.html new file mode 100644 index 0000000000..951dfb5ad8 --- /dev/null +++ b/ja/categories/Infra/Docker/index.html @@ -0,0 +1,538 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: Docker | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Docker + カテゴリ +

+
+ + +
+ 2022 +
+ + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/Infra/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" "b/ja/categories/Infra/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" new file mode 100644 index 0000000000..83b544e8f3 --- /dev/null +++ "b/ja/categories/Infra/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: Nginx | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Nginx + カテゴリ +

+
+ + +
+ 2022 +
+ + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/Infra/Web\346\234\215\345\212\241\345\231\250/Squid/index.html" "b/ja/categories/Infra/Web\346\234\215\345\212\241\345\231\250/Squid/index.html" new file mode 100644 index 0000000000..750ce3a3d1 --- /dev/null +++ "b/ja/categories/Infra/Web\346\234\215\345\212\241\345\231\250/Squid/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: Squid | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Squid + カテゴリ +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/Infra/Web\346\234\215\345\212\241\345\231\250/index.html" "b/ja/categories/Infra/Web\346\234\215\345\212\241\345\231\250/index.html" new file mode 100644 index 0000000000..76357cba57 --- /dev/null +++ "b/ja/categories/Infra/Web\346\234\215\345\212\241\345\231\250/index.html" @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: Web服务器 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Web服务器 + カテゴリ +

+
+ + +
+ 2022 +
+ + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/categories/Infra/index.html b/ja/categories/Infra/index.html new file mode 100644 index 0000000000..8e8c479c9e --- /dev/null +++ b/ja/categories/Infra/index.html @@ -0,0 +1,618 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: Infra | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Infra + カテゴリ +

+
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/categories/Infrastructure/index.html b/ja/categories/Infrastructure/index.html new file mode 100644 index 0000000000..ba280b7949 --- /dev/null +++ b/ja/categories/Infrastructure/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: Infrastructure | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Infrastructure + カテゴリ +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/categories/Linux/Oracle/Docker/index.html b/ja/categories/Linux/Oracle/Docker/index.html new file mode 100644 index 0000000000..3b8bcf8524 --- /dev/null +++ b/ja/categories/Linux/Oracle/Docker/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: Docker | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Docker + カテゴリ +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/categories/Linux/Oracle/index.html b/ja/categories/Linux/Oracle/index.html new file mode 100644 index 0000000000..e85cc53519 --- /dev/null +++ b/ja/categories/Linux/Oracle/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: Oracle | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Oracle + カテゴリ +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/categories/Linux/RPM/index.html b/ja/categories/Linux/RPM/index.html new file mode 100644 index 0000000000..429249b80c --- /dev/null +++ b/ja/categories/Linux/RPM/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: RPM | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

RPM + カテゴリ +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/categories/Linux/index.html b/ja/categories/Linux/index.html new file mode 100644 index 0000000000..e9efb8c0d5 --- /dev/null +++ b/ja/categories/Linux/index.html @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: Linux | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Linux + カテゴリ +

+
+ + +
+ 2022 +
+ + + + +
+ 2020 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/categories/NodeJS/Npm/index.html b/ja/categories/NodeJS/Npm/index.html new file mode 100644 index 0000000000..07b37a60d3 --- /dev/null +++ b/ja/categories/NodeJS/Npm/index.html @@ -0,0 +1,481 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: Npm | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Npm + カテゴリ +

+
+ + +
+ 2020 +
+ + +
+ 2017 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/categories/NodeJS/index.html b/ja/categories/NodeJS/index.html new file mode 100644 index 0000000000..c85bf951d0 --- /dev/null +++ b/ja/categories/NodeJS/index.html @@ -0,0 +1,541 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: NodeJS | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

NodeJS + カテゴリ +

+
+ + +
+ 2020 +
+ + + + + + + + +
+ 2017 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/categories/OracleLinux/index.html b/ja/categories/OracleLinux/index.html new file mode 100644 index 0000000000..91226ad02c --- /dev/null +++ b/ja/categories/OracleLinux/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: OracleLinux | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

OracleLinux + カテゴリ +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" "b/ja/categories/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" new file mode 100644 index 0000000000..85812686cf --- /dev/null +++ "b/ja/categories/Web\346\234\215\345\212\241\345\231\250/Nginx/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: Nginx | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Nginx + カテゴリ +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/Web\346\234\215\345\212\241\345\231\250/index.html" "b/ja/categories/Web\346\234\215\345\212\241\345\231\250/index.html" new file mode 100644 index 0000000000..c30b9b6acc --- /dev/null +++ "b/ja/categories/Web\346\234\215\345\212\241\345\231\250/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: Web服务器 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Web服务器 + カテゴリ +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/categories/index.html b/ja/categories/index.html new file mode 100644 index 0000000000..96aaef0924 --- /dev/null +++ b/ja/categories/index.html @@ -0,0 +1,470 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分类 | 李旭光的成长博客 + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + + + + + + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/categories/linux/index.html b/ja/categories/linux/index.html new file mode 100644 index 0000000000..dc9f351c71 --- /dev/null +++ b/ja/categories/linux/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: linux | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

linux + カテゴリ +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/categories/linux/systemctl/index.html b/ja/categories/linux/systemctl/index.html new file mode 100644 index 0000000000..210f0b58b8 --- /dev/null +++ b/ja/categories/linux/systemctl/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: systemctl | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

systemctl + カテゴリ +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/Webpack/index.html" "b/ja/categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/Webpack/index.html" new file mode 100644 index 0000000000..4b8c5cb259 --- /dev/null +++ "b/ja/categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/Webpack/index.html" @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: Webpack | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Webpack + カテゴリ +

+
+ + +
+ 2020 +
+ + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/index.html" "b/ja/categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/index.html" new file mode 100644 index 0000000000..9170702792 --- /dev/null +++ "b/ja/categories/\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226/index.html" @@ -0,0 +1,541 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 前端工程化 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端工程化 + カテゴリ +

+
+ + +
+ 2020 +
+ + + + + + + + +
+ 2019 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\211\215\347\253\257\345\274\200\345\217\221\350\247\204\350\214\203/index.html" "b/ja/categories/\345\211\215\347\253\257\345\274\200\345\217\221\350\247\204\350\214\203/index.html" new file mode 100644 index 0000000000..5a036f0114 --- /dev/null +++ "b/ja/categories/\345\211\215\347\253\257\345\274\200\345\217\221\350\247\204\350\214\203/index.html" @@ -0,0 +1,578 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 前端开发规范 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端开发规范 + カテゴリ +

+
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS/index.html" "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS/index.html" new file mode 100644 index 0000000000..df89ce65ae --- /dev/null +++ "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: CSS | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

CSS + カテゴリ +

+
+ + +
+ 2013 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS3/index.html" "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS3/index.html" new file mode 100644 index 0000000000..db5daf0e1b --- /dev/null +++ "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/CSS3/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: CSS3 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

CSS3 + カテゴリ +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/index.html" "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/index.html" new file mode 100644 index 0000000000..eb64a6a3b7 --- /dev/null +++ "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/index.html" @@ -0,0 +1,647 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 前端技术 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端技术 + カテゴリ +

+
+ + +
+ 2022 +
+ + + + + + +
+ 2021 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/2/index.html" "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/2/index.html" new file mode 100644 index 0000000000..c2e9e3978a --- /dev/null +++ "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/2/index.html" @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 前端技术 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端技术 + カテゴリ +

+
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/3/index.html" "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/3/index.html" new file mode 100644 index 0000000000..dd11c80229 --- /dev/null +++ "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/3/index.html" @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 前端技术 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端技术 + カテゴリ +

+
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/4/index.html" "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/4/index.html" new file mode 100644 index 0000000000..a0cd8e4f46 --- /dev/null +++ "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/4/index.html" @@ -0,0 +1,644 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 前端技术 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端技术 + カテゴリ +

+
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + +
+ 2019 +
+ + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/5/index.html" "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/5/index.html" new file mode 100644 index 0000000000..705b7f3c1d --- /dev/null +++ "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/page/5/index.html" @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 前端技术 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端技术 + カテゴリ +

+
+ + +
+ 2017 +
+ + + + +
+ 2013 +
+ + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\205\274\345\256\271\346\200\247/index.html" "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\205\274\345\256\271\346\200\247/index.html" new file mode 100644 index 0000000000..6a4baa056e --- /dev/null +++ "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\205\274\345\256\271\346\200\247/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 兼容性 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

兼容性 + カテゴリ +

+
+ + +
+ 2017 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/index.html" "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/index.html" new file mode 100644 index 0000000000..471c245e89 --- /dev/null +++ "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/index.html" @@ -0,0 +1,644 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 前端常见知识点整理 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端常见知识点整理 + カテゴリ +

+
+ + +
+ 2022 +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/2/index.html" "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/2/index.html" new file mode 100644 index 0000000000..aa4a8c2e26 --- /dev/null +++ "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/2/index.html" @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 前端常见知识点整理 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端常见知识点整理 + カテゴリ +

+
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/3/index.html" "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/3/index.html" new file mode 100644 index 0000000000..94d2fd2dc6 --- /dev/null +++ "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\345\270\270\350\247\201\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/page/3/index.html" @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 前端常见知识点整理 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端常见知识点整理 + カテゴリ +

+
+ + +
+ 2020 +
+ + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\351\227\256\351\242\230/index.html" "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\351\227\256\351\242\230/index.html" new file mode 100644 index 0000000000..3aa690b583 --- /dev/null +++ "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\211\215\347\253\257\351\227\256\351\242\230/index.html" @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 前端问题 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端问题 + カテゴリ +

+
+ + +
+ 2020 +
+ + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\276\256\344\277\241/index.html" "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\276\256\344\277\241/index.html" new file mode 100644 index 0000000000..6b04654612 --- /dev/null +++ "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\345\276\256\344\277\241/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 微信 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

微信 + カテゴリ +

+
+ + +
+ 2019 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\350\256\241\347\256\227\346\234\272\351\200\232\350\257\206/index.html" "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\350\256\241\347\256\227\346\234\272\351\200\232\350\257\206/index.html" new file mode 100644 index 0000000000..7f0b336bb8 --- /dev/null +++ "b/ja/categories/\345\211\215\347\253\257\346\212\200\346\234\257/\350\256\241\347\256\227\346\234\272\351\200\232\350\257\206/index.html" @@ -0,0 +1,524 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 计算机通识 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

计算机通识 + カテゴリ +

+
+ + +
+ 2022 +
+ + + + +
+ 2021 +
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\211\215\347\253\257\346\241\206\346\236\266/React/index.html" "b/ja/categories/\345\211\215\347\253\257\346\241\206\346\236\266/React/index.html" new file mode 100644 index 0000000000..aa4a24be22 --- /dev/null +++ "b/ja/categories/\345\211\215\347\253\257\346\241\206\346\236\266/React/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: React | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

React + カテゴリ +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\211\215\347\253\257\346\241\206\346\236\266/Vue/index.html" "b/ja/categories/\345\211\215\347\253\257\346\241\206\346\236\266/Vue/index.html" new file mode 100644 index 0000000000..3b32580a67 --- /dev/null +++ "b/ja/categories/\345\211\215\347\253\257\346\241\206\346\236\266/Vue/index.html" @@ -0,0 +1,547 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: Vue | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Vue + カテゴリ +

+
+ + +
+ 2021 +
+ + +
+ 2020 +
+ + + + +
+ 2019 +
+ + +
+ 2017 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\211\215\347\253\257\346\241\206\346\236\266/index.html" "b/ja/categories/\345\211\215\347\253\257\346\241\206\346\236\266/index.html" new file mode 100644 index 0000000000..433a0fee57 --- /dev/null +++ "b/ja/categories/\345\211\215\347\253\257\346\241\206\346\236\266/index.html" @@ -0,0 +1,567 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 前端框架 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

前端框架 + カテゴリ +

+
+ + +
+ 2021 +
+ + +
+ 2020 +
+ + + + + + +
+ 2019 +
+ + +
+ 2017 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\220\216\347\253\257\345\274\200\345\217\221/Go\350\257\255\350\250\200/index.html" "b/ja/categories/\345\220\216\347\253\257\345\274\200\345\217\221/Go\350\257\255\350\250\200/index.html" new file mode 100644 index 0000000000..e892adb45a --- /dev/null +++ "b/ja/categories/\345\220\216\347\253\257\345\274\200\345\217\221/Go\350\257\255\350\250\200/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: Go语言 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Go语言 + カテゴリ +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\220\216\347\253\257\345\274\200\345\217\221/index.html" "b/ja/categories/\345\220\216\347\253\257\345\274\200\345\217\221/index.html" new file mode 100644 index 0000000000..a3478c9483 --- /dev/null +++ "b/ja/categories/\345\220\216\347\253\257\345\274\200\345\217\221/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 后端开发 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

后端开发 + カテゴリ +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\244\247\345\211\215\347\253\257/index.html" "b/ja/categories/\345\244\247\345\211\215\347\253\257/index.html" new file mode 100644 index 0000000000..c3c355cad2 --- /dev/null +++ "b/ja/categories/\345\244\247\345\211\215\347\253\257/index.html" @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 大前端 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

大前端 + カテゴリ +

+
+ + +
+ 2020 +
+ + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\244\247\345\211\215\347\253\257/\345\205\250\346\240\210/index.html" "b/ja/categories/\345\244\247\345\211\215\347\253\257/\345\205\250\346\240\210/index.html" new file mode 100644 index 0000000000..0db0855e4c --- /dev/null +++ "b/ja/categories/\345\244\247\345\211\215\347\253\257/\345\205\250\346\240\210/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 全栈 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

全栈 + カテゴリ +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\244\247\345\211\215\347\253\257/\345\256\242\346\210\267\347\253\257\346\212\200\346\234\257/index.html" "b/ja/categories/\345\244\247\345\211\215\347\253\257/\345\256\242\346\210\267\347\253\257\346\212\200\346\234\257/index.html" new file mode 100644 index 0000000000..611ac7cca6 --- /dev/null +++ "b/ja/categories/\345\244\247\345\211\215\347\253\257/\345\256\242\346\210\267\347\253\257\346\212\200\346\234\257/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 客户端技术 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

客户端技术 + カテゴリ +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\345\244\247\345\211\215\347\253\257/\347\247\273\345\212\250\345\274\200\345\217\221/index.html" "b/ja/categories/\345\244\247\345\211\215\347\253\257/\347\247\273\345\212\250\345\274\200\345\217\221/index.html" new file mode 100644 index 0000000000..a07e27d3dd --- /dev/null +++ "b/ja/categories/\345\244\247\345\211\215\347\253\257/\347\247\273\345\212\250\345\274\200\345\217\221/index.html" @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 移动开发 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

移动开发 + カテゴリ +

+
+ + +
+ 2020 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\346\225\210\347\216\207\345\267\245\345\205\267/Labrary/index.html" "b/ja/categories/\346\225\210\347\216\207\345\267\245\345\205\267/Labrary/index.html" new file mode 100644 index 0000000000..c0cc9480fb --- /dev/null +++ "b/ja/categories/\346\225\210\347\216\207\345\267\245\345\205\267/Labrary/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: Labrary | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Labrary + カテゴリ +

+
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\346\225\210\347\216\207\345\267\245\345\205\267/VSCode\346\217\222\344\273\266/index.html" "b/ja/categories/\346\225\210\347\216\207\345\267\245\345\205\267/VSCode\346\217\222\344\273\266/index.html" new file mode 100644 index 0000000000..684d11c893 --- /dev/null +++ "b/ja/categories/\346\225\210\347\216\207\345\267\245\345\205\267/VSCode\346\217\222\344\273\266/index.html" @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: VSCode插件 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

VSCode插件 + カテゴリ +

+
+ + +
+ 2020 +
+ + +
+ 2017 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\346\225\210\347\216\207\345\267\245\345\205\267/index.html" "b/ja/categories/\346\225\210\347\216\207\345\267\245\345\205\267/index.html" new file mode 100644 index 0000000000..bb1d16b2d9 --- /dev/null +++ "b/ja/categories/\346\225\210\347\216\207\345\267\245\345\205\267/index.html" @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 效率工具 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

效率工具 + カテゴリ +

+
+ + +
+ 2020 +
+ + + + +
+ 2017 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\346\235\202\344\270\203\346\235\202\345\205\253/index.html" "b/ja/categories/\346\235\202\344\270\203\346\235\202\345\205\253/index.html" new file mode 100644 index 0000000000..9703be5c45 --- /dev/null +++ "b/ja/categories/\346\235\202\344\270\203\346\235\202\345\205\253/index.html" @@ -0,0 +1,567 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 杂七杂八 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

杂七杂八 + カテゴリ +

+
+ + +
+ 2022 +
+ + +
+ 2020 +
+ + + + +
+ 2019 +
+ + +
+ 2017 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\346\235\202\344\270\203\346\235\202\345\205\253/\345\215\232\345\256\242\346\212\200\345\267\247/index.html" "b/ja/categories/\346\235\202\344\270\203\346\235\202\345\205\253/\345\215\232\345\256\242\346\212\200\345\267\247/index.html" new file mode 100644 index 0000000000..e5faa7c823 --- /dev/null +++ "b/ja/categories/\346\235\202\344\270\203\346\235\202\345\205\253/\345\215\232\345\256\242\346\212\200\345\267\247/index.html" @@ -0,0 +1,547 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 博客技巧 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

博客技巧 + カテゴリ +

+
+ + +
+ 2022 +
+ + +
+ 2020 +
+ + +
+ 2019 +
+ + +
+ 2017 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\346\236\266\346\236\204/index.html" "b/ja/categories/\346\236\266\346\236\204/index.html" new file mode 100644 index 0000000000..4228067540 --- /dev/null +++ "b/ja/categories/\346\236\266\346\236\204/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 架构 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

架构 + カテゴリ +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\346\272\220\347\240\201\345\216\237\347\220\206/index.html" "b/ja/categories/\346\272\220\347\240\201\345\216\237\347\220\206/index.html" new file mode 100644 index 0000000000..d0a16e3828 --- /dev/null +++ "b/ja/categories/\346\272\220\347\240\201\345\216\237\347\220\206/index.html" @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 源码原理 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

源码原理 + カテゴリ +

+
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\346\272\220\347\240\201\345\216\237\347\220\206/page/2/index.html" "b/ja/categories/\346\272\220\347\240\201\345\216\237\347\220\206/page/2/index.html" new file mode 100644 index 0000000000..66ad3141e1 --- /dev/null +++ "b/ja/categories/\346\272\220\347\240\201\345\216\237\347\220\206/page/2/index.html" @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 源码原理 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

源码原理 + カテゴリ +

+
+ + +
+ 2020 +
+ + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/index.html" "b/ja/categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/index.html" new file mode 100644 index 0000000000..0215505e87 --- /dev/null +++ "b/ja/categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/index.html" @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 自己动手实现系列 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

自己动手实现系列 + カテゴリ +

+
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/page/2/index.html" "b/ja/categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/page/2/index.html" new file mode 100644 index 0000000000..2d84bd8ea4 --- /dev/null +++ "b/ja/categories/\346\272\220\347\240\201\345\216\237\347\220\206/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/page/2/index.html" @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 自己动手实现系列 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

自己动手实现系列 + カテゴリ +

+
+ + +
+ 2020 +
+ + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\350\207\252\346\210\221\346\217\220\345\215\207/index.html" "b/ja/categories/\350\207\252\346\210\221\346\217\220\345\215\207/index.html" new file mode 100644 index 0000000000..8d7526df7f --- /dev/null +++ "b/ja/categories/\350\207\252\346\210\221\346\217\220\345\215\207/index.html" @@ -0,0 +1,656 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 自我提升 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

自我提升 + カテゴリ +

+
+ + +
+ 2023 +
+ + +
+ 2020 +
+ + + + + + +
+ 2019 +
+ + + + +
+ 2017 +
+ + +
+ 2016 +
+ + +
+ 2015 +
+ + +
+ 2013 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\344\272\272\347\224\237\346\200\235\350\200\203/index.html" "b/ja/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\344\272\272\347\224\237\346\200\235\350\200\203/index.html" new file mode 100644 index 0000000000..3387ba7f22 --- /dev/null +++ "b/ja/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\344\272\272\347\224\237\346\200\235\350\200\203/index.html" @@ -0,0 +1,481 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 人生思考 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

人生思考 + カテゴリ +

+
+ + +
+ 2023 +
+ + +
+ 2020 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\346\235\202\350\256\260\351\232\217\346\204\237/index.html" "b/ja/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\346\235\202\350\256\260\351\232\217\346\204\237/index.html" new file mode 100644 index 0000000000..e099730572 --- /dev/null +++ "b/ja/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\346\235\202\350\256\260\351\232\217\346\204\237/index.html" @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 杂记随感 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

杂记随感 + カテゴリ +

+
+ + +
+ 2020 +
+ + + + +
+ 2019 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\350\257\273\344\271\246\347\254\224\350\256\260/index.html" "b/ja/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\350\257\273\344\271\246\347\254\224\350\256\260/index.html" new file mode 100644 index 0000000000..ebf5a8c4a3 --- /dev/null +++ "b/ja/categories/\350\207\252\346\210\221\346\217\220\345\215\207/\350\257\273\344\271\246\347\254\224\350\256\260/index.html" @@ -0,0 +1,527 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 读书笔记 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

读书笔记 + カテゴリ +

+
+ + +
+ 2017 +
+ + +
+ 2016 +
+ + +
+ 2015 +
+ + +
+ 2013 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\350\231\232\346\213\237\345\214\226/Docker-Compose/index.html" "b/ja/categories/\350\231\232\346\213\237\345\214\226/Docker-Compose/index.html" new file mode 100644 index 0000000000..d5f1e7b793 --- /dev/null +++ "b/ja/categories/\350\231\232\346\213\237\345\214\226/Docker-Compose/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: Docker Compose | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

Docker Compose + カテゴリ +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/ja/categories/\350\231\232\346\213\237\345\214\226/index.html" "b/ja/categories/\350\231\232\346\213\237\345\214\226/index.html" new file mode 100644 index 0000000000..ea13d18faa --- /dev/null +++ "b/ja/categories/\350\231\232\346\213\237\345\214\226/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + カテゴリ: 虚拟化 | 李旭光的成长博客 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + +
+
+
+

虚拟化 + カテゴリ +

+
+ + +
+ 2022 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/index.html b/ja/index.html new file mode 100644 index 0000000000..afe3495a55 --- /dev/null +++ b/ja/index.html @@ -0,0 +1,506 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

前言

最近各种技术博客,各种技术文章,各种牛人前辈的学习心得都在看,最近看了一个不错的文章叫做《如何正确使用时间》
感觉还是很不错的,特别适合年轻的程序员看一下。下面将全文贴在下方,如果需要也可以去原文查看。

+

原文

+

你是否时常会焦虑时间过的很快,没时间学习,本文将会分享一些个人的见解。

+

花时间补基础,读文档

在工作中我们时常会花很多时间去 debug,但是你是否发现很多问题最终只是你基础不扎实或者文档没有仔细看。

+

基础是你技术的基石,一定要花时间打好基础,而不是追各种新的技术。一旦你的基础扎实,学习各种新的技术也肯定不在话下,因为新的技术,究其根本都是相通的。

+

文档同样也是一门技术的基础。一个优秀的库,开发人员肯定已经把如何使用这个库都写在文档中了,仔细阅读文档一定会是少写 bug 的最省事路子。

+

学会搜索

如果你还在使用百度搜索编程问题,请尽快抛弃这个垃圾搜索引擎。同样一个关键字,使用百度和谷歌,谷歌基本完胜的。即使你使用中文在谷歌中搜索,得到的结果也往往是谷歌占优,所以如果你想迅速的通过搜索引擎来解决问题,那一定是谷歌。

+

学点英语

说到英语,一定是大家所最不想听的。其实我一直认为程序员学习英语是简单的,因为我们工作中是一直接触着英语,并且看懂技术文章,文档所需要的单词量是极少的。我时常在群里看到大家发出一个问题的截图问什么原因,其实在截图中英语已经很明白的说明了问题的所在,如果你的英语过关,完全不需要浪费时间来提问和搜索。所以我认为学点英语也是节省时间中很重要的一点。

+

那么如何去学习呢,chrome 装个翻译插件,直接拿英文文档或文章读,不会的就直接划词翻译,然后记录下这个单词并背诵。每天花半小时看点英文文档和文章,坚持两个月,你的英语水平不说别的,看文档和文章绝对不会有难题了。这一定是一个很划算的个人时间投资,花点时间学习英语,能为你将来的技术之路铺平很多坎。

+

画个图,想一想再做

你是否遇到过这种问题,需求一下来,看一眼,然后马上就按照设计稿开始做了,可能中间出个问题导致你需要返工。

+

如果你存在这样的问题,我很推荐在看到设计稿和需求的时候花点时间想一想,画一画。考虑一下设计稿中是否可以找到可以拆分出来的复用组件,是否存在之前写过的组件。该如何组织这个界面,数据的流转是怎么样的。然后画一下这个页面的需求,最后再动手做。

+

利用好下班时间学习

说到下班时间,那可能就有人说了公司很迟下班,这其实是国内很普遍的情况。但是我认为正常的加班是可以的,但是强制的加班就是在损耗你的身体和前途。

+

可以这么说,大部分的 996 公司,加班的这些时间并不会增加你的技术,无非就是在写一些重复的业务逻辑。也许你可以拿到更多的钱,但是代价是身体还有前途。程序员是靠技术吃饭的,如果你长久呆在一个长时间加班的公司,不能增长你的技术还要吞噬你的下班学习时间,那么你一定会废掉的。如果你遇到了这种情况,只能推荐尽快跳槽到非 996 的公司。

+

那么如果你有足够的下班时间,一定要花上 1, 2 小时去学习,上班大家基本都一样,技术的精进就是看下班以后的那几个小时了。如果你能利用好下班时间来学习,坚持下去,时间一定会给你很好的答复。

+

列好 ToDo

我喜欢规划好一段时间内要做的事情,并且要把事情拆分为小点。给 ToDo 列好优先级,紧急的优先级最高。相同优先级的我喜欢先做简单的,因为这样一旦完成就能划掉一个,提高成就感。

+

反思和整理

每周末都会花上点时间整理下本周记录的笔记和看到的不错文章。然后考虑下本周完成的工作和下周准备要完成的工作。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/10/index.html b/ja/page/10/index.html new file mode 100644 index 0000000000..281b200ea9 --- /dev/null +++ b/ja/page/10/index.html @@ -0,0 +1,492 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#/boot/grub/grub.conf 缺失:

yum install -y grub
grub-mkconfig -o /boot/grub/grub.conf

#/boot/grub2/grub.cfg 缺失:

yum install -y grub2
grub2-mkconfig -o /boot/grub2/grub.cfg

uname -a
sudo grep ^menuentry /boot/grub2/grub.cfg | awk -F\' '{print $2}' | nl -v0

sudo grub2-editenv list

sudo grep GRUB_DEFAULT /etc/default/grub

sudo vim /etc/default/grub

sudo grub2-set-default 1

sudo grub2-mkconfig -o /boot/grub2/grub.cfg

sudo dracut --force

sudo reboot

uname -a
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/100/index.html b/ja/page/100/index.html new file mode 100644 index 0000000000..21632ad0ce --- /dev/null +++ b/ja/page/100/index.html @@ -0,0 +1,534 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

深浅拷贝

1
2
3
4
5
6
let a = {
age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2
+

从上述例子中我们可以发现,如果给一个变量赋值一个对象,那么两者的值会是同一个引用,其中一方改变,另一方也会相应改变。
通常在开发中我们不希望出现这样的问题,我们可以使用浅拷贝来解决这个问题。

+

浅拷贝

首先可以通过 Object.assign 来解决这个问题。

+
1
2
3
4
5
6
7
8
// js代码

let a = {
age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // => 1
+

当然我们也可以通过展开运算符(…)来解决

+
1
2
3
4
5
6
let a = {
age: 1
}
let b = {...a}
a.age = 2
console.log(b.age) // => 1
+

我们还可以用很多简单的方法都能实现浅拷贝:

+
1
2
arr.slice();
arr.concat();
+ +

通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就需要使用到深拷贝了

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = {...a}
a.jobs.first = 'native'
console.log(b.jobs.first) // native
+

浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到刚开始的话题了,两者享有相同的引用。要解决这个问题,我们需要引入深拷贝。

+

深拷贝

这个问题通常可以通过 JSON.parse(JSON.stringify(object)) 来解决,这也是最好用最简单的方法,俗称乞丐版。
乞丐版

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
+

但是该方法也是有局限性的:

+
    +
  • 会忽略 undefined
  • +
  • 会忽略 symbol
  • +
  • 不能序列化函数
  • +
  • 不能解决循环引用的对象
  • +
+

举个栗子:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

let obj = {
a: 1,
b: {
c: 2,
d: 3,
},
}

obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c

let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj) // => Uncaught TypeError: Converting circular structure to JSON
+

如果你有这么一个循环引用对象,你会发现你不能通过该方法深拷贝
在遇到函数、 undefined 或者 symbol 的时候,该对象也不能正常的序列化

+
1
2
3
4
5
6
7
8
9
10
// js代码

let a = {
age: undefined,
sex: Symbol('fmale'),
jobs: function() {},
name: 'lixuguang'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // => {name: "lixuang"}
+ +

你会发现在上述情况中,该方法会忽略掉函数和 undefined
但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题,并且该函数是内置函数中处理深拷贝性能最快的。

+

那么是否可以解决函数和循环引用的问题呢?答案是肯定可以解决,接下来是基础版本的改造
基础版

+
1
2
3
4
5
6
7
8
9
10
11
function myClone(target){
if(typeof target === 'object'){ // 判断传入目标是否是object类型
let cloneTarget = {}; // 创建克隆对象
for(const key in target){ // 遍历目标对象
cloneTarget[key] = myClone(target[key]) // 递归调用 clone 方法
}
return cloneTarget;
} else {
return target // 如果不是 object 返回
}
}
+

写到这里已经可以帮助你应付一些面试官考察你的递归解决问题的能力。但是显然,这个深拷贝函数还是有一些问题。
这里只考虑了对象,没有考虑数组。
下面我们来做一个强化版的深拷贝,同时考虑对象、数组还有循环引用的问题。
强化版

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function myClone(target, map = new WeakMap()) { // WeakMap => 键对象弱引用, 可被垃圾回收
if(typeof target === 'object'){ // 判断是否是对象
let cloneTarget = Array.isArray(target) ? [] : {}; // 判断是是数组还是对象
if(map.get(target)) {
return target;
}

map.set(target, cloneTarget);

for(const key in target) {
cloneTarget[key] = myClone(target[key], map)
}
return cloneTarget;
} else {
return target;
}
}
+

当然如果你的数据中含有以上三种情况下,可以使用 lodash 的深拷贝函数。
如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// js代码

function structuralClone(obj) {
return new Promise(resolve => {
const {port1, port2} = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
});
}

var obj = {
a: 1,
b: {
c: b
}
}
// 注意该方法是异步的
// 可以处理 undefined 和循环引用对象
(async () => {
const clone = await structuralClone(obj)
})()
+ +

深拷贝实现方式2,可以深拷贝 functionsymbol,等等,堪称终极版
终极版

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// js代码

const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const argsTag = '[object Arguments]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];
function forEach(array, iteratee) {
let index = -1;
const length = array.length;
while (++index < length) {
iteratee(array[index], index);
}
return array;
}

function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}

function getType(target) {
return Object.prototype.toString.call(target);
}

function getInit(target) {
const Ctor = target.constructor;
return new Ctor();
}

function cloneSymbol(targe) {
return Object(Symbol.prototype.valueOf.call(targe));
}

function cloneReg(targe) {
const reFlags = /\w*$/;
const result = new targe.constructor(targe.source, reFlags.exec(targe));
result.lastIndex = targe.lastIndex;
return result;
}

function cloneFunction(func) {
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
if (func.prototype) {
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if (body) {
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(funcString);
}
}

function cloneOtherType(targe, type) {
const Ctor = targe.constructor;
switch (type) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(targe);
case regexpTag:
return cloneReg(targe);
case symbolTag:
return cloneSymbol(targe);
case funcTag:
return cloneFunction(targe);
default:
return null;
}
}

function clone(target, map = new WeakMap()) {
// 克隆原始类型
if (!isObject(target)) {
return target;
}

// 初始化
const type = getType(target);
let cloneTarget;
if (deepTag.includes(type)) {
cloneTarget = getInit(target, type);
} else {
return cloneOtherType(target, type);
}

// 防止循环引用
if (map.get(target)) {
return target;
}
map.set(target, cloneTarget);

// 克隆set
if (type === setTag) {
target.forEach(value => {
cloneTarget.add(clone(value));
});
return cloneTarget;
}

// 克隆map
if (type === mapTag) {
target.forEach((value, key) => {
cloneTarget.set(key, clone(value));
});
return cloneTarget;
}

// 克隆对象和数组
const keys = type === arrayTag ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone(target[key], map);
});

return cloneTarget;
}

// 调用方法
clone(target);
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/101/index.html b/ja/page/101/index.html new file mode 100644 index 0000000000..2d3077f2bb --- /dev/null +++ b/ja/page/101/index.html @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

new 一个对象的过程

    +
  1. 新生成了一个对象
  2. +
  3. 链接到原型
  4. +
  5. 绑定 this
  6. +
  7. 返回新对象
  8. +
+

在调用 new 的过程中会发生以上四件事情,我们也可以试着来自己实现一个 new

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

function create() {
// 创建一个空的对象
let obj = new Object()
// 获得构造函数
let Con = [].shift.call(arguments)
// 链接到原型
obj.__proto__ = Con.prototype
// 绑定 this,执行构造函数
let result = Con.apply(obj, arguments)
// 确保 new 出来的是个对象
return typeof result === 'object' ? result : obj
}
+ +

对于实例对象来说,都是通过 new 产生的,无论是 function Foo() 还是 let a = { b : 1 }

+

对于创建一个对象来说,更推荐使用字面量的方式创建对象(无论性能上还是可读性)。因为你使用 new Object() 的方式创建对象需要通过作用域链一层层找到 Object,但是你使用字面量的方式就没这个问题。

+
1
2
3
4
// js代码

function Foo() {} // function 就是个语法糖,内部等同于 new Function()
let a = { b: 1 } // 这个字面量内部也是使用了 new Object()
+

对于 new 来说,还需要注意下运算符优先级。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

function Foo() {
return this;
}
Foo.getName = function () {
console.log('1');
};
Foo.prototype.getName = function () {
console.log('2');
};

new Foo.getName(); // -> 1
new Foo().getName(); // -> 2
+ +

从上图可以看出,new Foo() 的优先级大于 new Foo ,所以对于上述代码来说可以这样划分执行顺序

+
1
2
3
4
// js代码

new (Foo.getName());
(new Foo()).getName();
+ +
    +
  • 对于第一个函数来说,先执行了 Foo.getName() ,所以结果为 1;
  • +
  • 对于后者来说,先执行 new Foo() 产生了一个实例,然后通过原型链找到了 Foo 上的 getName 函数,所以结果为 2。
  • +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/102/index.html b/ja/page/102/index.html new file mode 100644 index 0000000000..8696ceadf7 --- /dev/null +++ b/ja/page/102/index.html @@ -0,0 +1,513 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

闭包Closure

闭包的定义很简单:函数 A 返回了一个函数 B ,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。

+
1
2
3
4
5
6
7
8
9
// js代码

function A() {
let a = 1
function B() {
console.log(a)
}
return B
}
+

你是否会疑惑,为什么函数 A 已经弹出调用栈了,为什么函数 B 还能引用到函数 A 中的变量。因为函数 A 中的变量这时候是存储在堆上的。现在的 JS 引擎可以通过逃逸分析辨别出哪些变量需要存储在堆上,哪些需要存储在栈上。

+

经典面试题,循环中使用闭包解决 var 定义函数的问题

+
1
2
3
4
5
6
7
// js代码

for ( var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
+

首先因为 setTimeout 是个异步函数,所有会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。

+

解决办法两种,第一种使用闭包

+
1
2
3
4
5
6
7
8
9
// js代码

for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
+

第二种就是使用 setTimeout 的第三个参数(附加参数,传给 setTimeout 中的函数)

+
1
2
3
4
5
6
7
// js代码

for ( var i=1; i<=5; i++) {
setTimeout( function timer(j) {
console.log( j );
}, i*1000, i);
}
+ +

第三种就是使用 let 定义 i

+
1
2
3
4
5
6
7
// js代码

for ( let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
+

因为对于 let 来说,他会创建一个块级作用域,相当于

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// js代码

{ // 形成块级作用域
let i = 0
{
let ii = i
setTimeout( function timer() {
console.log( ii );
}, i*1000 );
}
i++
{
let ii = i
}
i++
{
let ii = i
}
...
}
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/103/index.html b/ja/page/103/index.html new file mode 100644 index 0000000000..7afa84bb7e --- /dev/null +++ b/ja/page/103/index.html @@ -0,0 +1,523 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

执行上下文

当执行 JS 代码时,会产生三种执行上下文

+
    +
  • 全局执行上下文
  • +
  • 函数执行上下文
  • +
  • eval 执行上下文
  • +
+

每个执行上下文中都有三个重要的属性

+
    +
  • 变量对象( VO ),包含变量、函数声明和函数的形参,该属性只能在全局上下文中访问
  • +
  • 作用域链( JS 采用词法作用域,也就是说变量的作用域是在定义时就决定了)
  • +
  • this
    1
    2
    3
    4
    5
    6
    7
    // js代码

    var a = 10
    function foo(i) {
    var b = 20
    }
    foo()
    +对于上述代码,执行栈中有两个上下文:全局上下文和函数 foo 上下文。
    1
    2
    3
    4
    5
    6
    // js代码

    stack = [
    globalContext,
    fooContext
    ]
    +对于全局上下文来说, VO 大概是这样的
    1
    2
    3
    4
    5
    6
    7
    // js代码

    globalContext.VO === globe
    globalContext.VO = {
    a: undefined,
    foo: <Function>,
    }
    +对于函数 foo 来说, VO 不能访问,只能访问到活动对象( AO
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // js代码

    fooContext.VO === foo.AO
    fooContext.AO {
    i: undefined,
    b: undefined,
    arguments: <>
    }
    // arguments 是函数独有的对象(箭头函数没有)
    // 该对象是一个伪数组,有 `length` 属性且可以通过下标访问元素
    // 该对象中的 `callee` 属性代表函数本身
    // `caller` 属性代表函数的调用者
    +对于作用域链,可以把它理解成包含自身变量对象和上级变量对象的列表,通过 [[Scope]] 属性查找上级变量
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    fooContext.[[Scope]] = [
    globalContext.VO
    ]
    fooContext.Scope = fooContext.[[Scope]] + fooContext.VO
    fooContext.Scope = [
    fooContext.VO,
    globalContext.VO
    ]
    +接下来让我们看一个老生常谈的例子, var
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    b() // call b
    console.log(a) // undefined

    var a = 'Hello world'

    function b() {
    console.log('call b')
    }
    +想必以上的输出大家肯定都已经明白了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行上下文时,会有两个阶段。第一个阶段是创建的阶段(具体步骤是创建 VO ), JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined ,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用。
  • +
+

在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

b() // call b second

function b() {
console.log('call b fist')
}
function b() {
console.log('call b second')
}
var b = 'Hello world'
+

var 会产生很多错误,所以在 ES6 中引入了 letlet 不能在声明前使用,但是这并不是常说的 let 不会提升, let 提升了声明但没有赋值,因为临时死区导致了并不能在声明前使用。

+

对于非匿名的立即执行函数需要注意以下一点

+
1
2
3
4
5
6
7
// js代码

var foo = 1
(function foo() {
foo = 10
console.log(foo)
}()) // -> ƒ foo() { foo = 10 ; console.log(foo) }
+

因为当 JS 解释器在遇到非匿名的立即执行函数时,会创建一个辅助的特定对象,然后将函数名称作为这个对象的属性,因此函数内部才可以访问到 foo ,但是这个值又是只读的,所以对它的赋值并不生效,所以打印的结果还是这个函数,并且外部的值也没有发生更改。

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

specialObject = {};

Scope = specialObject + Scope;

foo = new FunctionExpression;
foo.[[Scope]] = Scope;
specialObject.foo = foo; // {DontDelete}, {ReadOnly}

delete Scope[0]; // remove specialObject from the front of scope chain
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/104/index.html b/ja/page/104/index.html new file mode 100644 index 0000000000..1e085417d5 --- /dev/null +++ b/ja/page/104/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

this

this 是很多人会混淆的概念,但是其实他一点都不难,你只需要记住几个规则就可以了。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// js代码

function foo() {
console.log(this.a)
}
var a = 1
foo()

var obj = {
a: 2,
foo: foo
}
obj.foo()

// 以上两者情况 `this` 只依赖于调用函数前的对象,优先级是第二个情况大于第一个情况

// 以下情况是优先级最高的,`this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向
var c = new foo()
c.a = 3
console.log(c.a)

// 还有种就是利用 `call` , `apply` , `bind` 改变 `this` ,这个优先级仅次于 `new`
+

以上几种情况明白了,很多代码中的 this 应该就没什么问题了,下面让我们看看箭头函数中的 this

+
1
2
3
4
5
6
7
8
9
10
// js代码

function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())
+

箭头函数其实是没有 this 的,这个函数中的 this 只取决于他外面的第一个不是箭头函数的函数的 this 。在这个例子中,因为调用 a 符合前面代码中的第一个情况,所以 thiswindow。并且 this 一旦绑定了上下文,就不会被任何代码改变。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/105/index.html b/ja/page/105/index.html new file mode 100644 index 0000000000..752df496ea --- /dev/null +++ b/ja/page/105/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

instanceof

instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype
举例:

+
1
a instanceof Object
+

判断 Objectprototype 是否在 a 的原型链上。

+

我们也可以试着实现一下 instanceof

+
1
2
3
4
5
6
7
8
9
10
11
12
function myInstanceof(left, right) { // left 表示左表达式,right 表示右表达式
let prototype = right.prototype // 获得类型的原型
left = left.__proto__ // 获得对象的原型

while (true) { // 判断对象的类型是否等于类型的原型
if (left === null)
return false
if (prototype === left)
return true
left = left.__proto__
}
}
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/106/index.html b/ja/page/106/index.html new file mode 100644 index 0000000000..9354a011e8 --- /dev/null +++ b/ja/page/106/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

还记得我19年总结最下面的那张照片么,那个同事送的的本子,对,就是下面这张图,这本子叫做《One In A Million》中文叫做极少数手册,是一个管理时间用的管理手册,英文如果直译的话应该是叫做万里挑一,实话说我并不是很自信,虽然我觉得自己还算可以,但远远达不到万里挑一的程度,所以,我打算好好利用这个本子,让自己尽可能成为百里挑一,千里挑一,最后万里挑一的人。

+

本子里有一个栏目叫做21天主题挑战,21-Day Challenge,每个月21天都会给你列出一些主题,越往后列出来的主题越少,更多的需要个人去补充,我想去挑战这些内容,让自己的生活变得有目标,让自己变得更好。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/107/index.html b/ja/page/107/index.html new file mode 100644 index 0000000000..28a0c6641f --- /dev/null +++ b/ja/page/107/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

原型

yuanxing.png
每个函数都有 prototype 属性,除了 Function.prototype.bind(),该属性指向原型对象,简称原型prototype原型对象里的constructor指向构造函数本身。
每个对象都有 __proto__ 属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 _proto_ 来访问。
对象可以通过 __proto__ 来寻找不属于该对象的属性,__proto__ 将对象连接起来组成了原型链

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/108/index.html b/ja/page/108/index.html new file mode 100644 index 0000000000..94ae0e7090 --- /dev/null +++ b/ja/page/108/index.html @@ -0,0 +1,510 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

+

温故而知新,可以为师矣。
———————— 论语

+
+

这篇文章主要是把我看过的书中的知识点,还有其他人整理的面试题进行整理,巩固自己的前端开发理论知识,希望其他看到这篇文章的人也能有所帮助。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/109/index.html b/ja/page/109/index.html new file mode 100644 index 0000000000..87fc0aeb0c --- /dev/null +++ b/ja/page/109/index.html @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

2020元旦伊始

时间过的真快,19年睡了一觉就成了过去的一年,今天是元旦,早上8:00起了床,简单洗漱后我出了门,新年新气象,去年没有坚持的晨跑,今年想要坚持一下,所以一早就出门跑了会儿步,太久没活动了,只是跑了半个小时就气喘吁吁,只好回家,但是即使是这半个小时,我感觉呼吸了新鲜的空气,整个人也精神了不少,回到家坐在阳台上,拿出小王宁送我的极少数手册开始研究了起来,生活还是应该有点仪式感何况是新年伊始,手册中有很多建议,全部看过一遍以后我郑重的拿起了笔写下了我今天的行动计划。但是没有写年度计划,明天上班后跟领导还要谈一谈,谈过之后再定一下年度的计划。今天我的计划就是陪家人过好元旦这一天。

+

元旦执行计划

    +
  1. 写一篇日志
  2. +
  3. 将家人这一天游玩的过程以Vlog的形式记录下来,上传到西瓜视频上。
  4. +
  5. 陪家人逛街,给桐桐买新衣裳。
  6. +
+

执行计划

吃过早饭后大概到了10:30,一家人收拾好以后终于可以出门了,今天有点风,一点点冷不是特别冷,空气还不错,天气听晴朗的,一家人开着车听着歌前往甘井子万达广场,20分钟左右我们就到了万达广场,首先我们先去看了桐桐心心念念的小猫咪,也就是猫咪咖啡馆里的那些小可爱,桐桐可能是受妈妈影响,很喜欢猫咪,但是又有一点点怕。大概看了10分钟的猫咪,我们继续下一站,上四楼给桐桐买新衣裳,桐桐看到满眼的漂亮衣裳很是兴奋,最终她挑选了一套带着向日葵花朵的淡蓝色衣裙,穿上漂亮的新衣裳,平时像个假小子似的桐桐也一下子害羞了起来,让妈妈和奶奶看完后还会让爸爸和爷爷看看,得到了大家的肯定以后,心满意足的买下了它,也不知道是不是试衣服试累了,桐桐嚷嚷着要吃饭,桐桐妈说有一家港式茶餐厅味道不错,之前和同事来过,于是我们就去了4楼的茶餐厅,说实话这是我头一次吃茶餐厅,我还跟我爸妈调侃说,这也算是我们来过一次香港了。去到餐厅时才11点多一点,客还没有上满,还有座位,我们找了个靠里的位置坐了下来,打开菜谱,桐桐妈挑了几样她觉得不错的菜,上菜时间稍微有点慢,期间桐桐有些不耐烦,坐也坐不住,终于等到了吃的上来,可是觉得大多数吃的都太偏甜,爸妈和我都吃不太惯,桐桐吃的还不错,大概吃了半个小时就吃完了,出门的时候门口已经排起了长队,还好来的早一点,不然吃饭都要等了。本打算带着桐桐回家睡个觉,下午要到姥爷家串门,可桐桐说没有逛够,正巧这时看到了一楼有小丑在折气球玩具,我们就下到一楼找小丑玩了,小丑给桐桐折了个贵宾犬,小丑是荣耀手机的员工,可能是元旦策划的活动吧,反正来了就来了,进去看看也好,看了新出的V30手机,感觉并不是很喜欢,店员说有旧机抵值的活动,我问了他我的7p128g还能值多少,他打了几个电话后回复我1400块,哎,三年前7000块买的手机,现在只值1400了,算了还是用着吧,出了荣耀手机店又去了旁边的华为,看了看新出的mate30pro,这个手机是真的不错,当然手机不错价格也美丽,6500+的价格真的是让我不舍得买,还是把钱留下来给桐桐吧。桐桐终于累了,嚷嚷着要走,于是一家人驱车去了姥姥家。

+

姥姥家在西南路,因为平时工作忙很少带桐桐去,这次去桐桐又不认识他们了,开始的时候很拘束,也不说话,一直到吃饭结束才终于愿意开口问好唱歌,因为家里没有电视机,到了姥姥家看到电视机很兴奋,一直在看,今天也算是过了瘾了,亲人就是即使很长时间不见面,但一见面就特别亲的人,饭从5:30一直吃到8:30,聊了工作,聊了生活,聊了困惑,聊了坦然,我终于知道了长辈们的智慧,了解了身体健康的重要性,20年一定要锻炼出一副好的身体,对家人负责。

+

9:30回到了家,心里挺高兴的,新年第一天陪家人一起度过了充实的一天,回到家把今天路上录下来的视频剪成了短视频上传到了西瓜视频,这也是我今年的一个想法,记录我们一家人的幸福生活,如果有粉丝有点播的话还能赚点钱,没有的话也记录了桐桐的成长,算是一点纪念吧。最后写下了这篇日记,写完之后我就要睡觉去了,明天一早就要起床,明天起我就要走路去上班了,为了有一副好身体,加油。

+

今天的目标都完成了,很开心~~

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/11/index.html b/ja/page/11/index.html new file mode 100644 index 0000000000..d97d4f4398 --- /dev/null +++ b/ja/page/11/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

搭建Docker私有仓库

利用registry搭建私有仓库

+

下载registry

1
docker pull registry
+ +

配置

1
2
/etc/docker/doemon.json
# 配置 'insecure-registry'
+

重启docker

1
systemctl restart docker
+ +

创建registry容器(关联私有仓库配置)

1
docker run -d -p 5000:5000 --name registry registry:latest
+ +

推送镜像到私有仓

    +
  • 备份镜像(172.16.12.134:5000 私有仓地址)

    +
    1
    docker tag ubuntu:latest 172.16.12.134:5000/my_ubuntu
  • +
  • 推送

    +
    1
    docker push 172.16.12.134:5000/my_ubuntu
  • +
  • 下载

    +
    1
    docker pull 172.16.12.134:5000/my_ubuntu
  • +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/110/index.html b/ja/page/110/index.html new file mode 100644 index 0000000000..b51a07ac0a --- /dev/null +++ b/ja/page/110/index.html @@ -0,0 +1,532 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

不知不觉又是一年,今天是2019年的最后一天,也是我30岁的最后一天,回顾这一年有收获有遗憾。翻出了18年的总结,看到了18年底对于自己19年的规划,现在回过头来看看哪些完成了。

+

18年底定的计划

学习技术

1. 深入学习客户端开发(全年)

18年客户端的项目也做了几个,Electron的开发能力经验更多了,之前只是用html+css+js做前端页面的开发,今年又增加了跟vue框架的结合,成功在多个项目中实践,只是一直没有形成规范感觉还是少了一环,原来一直在犹豫Electron到底算不算前端的技术栈之一,后来也算是想明白了,大前端范畴,只要是面向用户的都算是前端该做的范畴,所以Electron当然也算是前端应该掌握的技术栈,20年会继续学习客户端技术,把公司相关规范制定和落地。

+

2. 学习前端自动化测试相关知识(2019年3月前)

18年关注了前端自动化测试相关的知识,本打算19年好好学习一下,在公司项目中实践,但是今年前后端工作量大增,前端人员的精力有限,加之成本的考虑,并没有开展起来,不过这方面的技能储备已经具备了,年底两次关于前端自动化的测试也将前端自动化的思想普及到了公司前端的团队里,20年是公司强调质量的一年,相信20年前端自动化测试将有机会在公司一些项目中落地

+

3. 学习并掌握TS (2019年5月前)

18年就看到了TS将会成为一门比较活的开发语言,目前来说已经具备了做TS开发的技能,只是TS更适合大型的长期的项目,将更好的保障项目的代码质量,快速迭代和小型项目并不是很适合。前端团队里没有开花,没想到居然在开发团队里见到了曙光,感谢开发的小伙伴愿意接受这门前端新技术,相信有一个点采用后看到了优势,后续普及工作将会更好开展。

+

4. 学习并掌握React(2019年7月前)

18年Vue已经在公司前端全面普及开来,但是市场上React仍然是占据更多的份额,而且RN还能做App开发,所以还是一门必须要掌握的技术,今年看了一本React相关的书,也看了一些相关的视频教程,还参加了公司同事开展的React分享,虽然没有在项目中实际使用过,但是理论知识已经掌握了,可以进行小的Demo开发,20年要用React写一个开源的项目,做到学有所用,学以致用。

+

5. 学习前端持续集成的相关知识(2019年9月前)

19年公司强调了代码必须走线上构建的要求,前端工程化和自动构建持续集成的工作可以说是工作所迫,这方面的技能已经Get到了,另外令我感到高兴的是,通过对Github-Actions的配置,我的博客可以像提交代码一样提交文章了,而且也实践了自动构建的相关技术,很开心。

+

6. 学习Docker虚拟化技术( 2019年10月前)

这个任务没有达成,原本是希望能够通过Docker统一前端开发环境,不过Docker的适用场景还是以部署环境为主,Docker今年貌似热度也降下来了,今年K8s越来越火,不管怎样,持续关注新技术。

+

整理计划

1. 养成写博客的习惯,至少2天一篇,可以是技术博客,也可以是其他。

今年实在是太忙了,别说两天一篇文章,就连一个月一篇的量都没能达成,不过在年底我终于把博客又重新激活了,希望自己20年能够把博客坚持写下来,很简单,积累自己,打造个人品牌。

+

2. 将常用的方法和功能做成插件,开源给公司使用

今年4月开始,更多的精力都投在了部门的前端管理工作,说实话也迷茫也难过,这份迷茫终于在19年年底解开了,没能在技能层面有更高的提升,以及在代码层面给公司带来价值我觉得还是有些遗憾,20年希望自己能够在技术上有更大的进步,技术深度上/广度上/架构层面/后端技能都能有长足的进步。

+

读书计划

1. 每周读完一本书,并写一篇读后感

2019年读了25本书,每读完一本都会写一篇读后感,虽然没能保证每周读一本书,但是基本保证了每两周读完一本书,算是完成了50%的目标吧。20年继续努力,读书使人聪明,因为不聪明所以更要多读书。

+

部门前端计划

加强各设计组前端之间的交流

+

设计组的出现导致前端交流不畅,2019年加强与各设计组前端的交流,掌握各位前端的技术能力。

+
+

没想到一语成第,设计组居然在4月份将前端整个拿出来构成了一个前端组,由我来带,更没想到的是居然在19年年底又解散到项目团队了,这9个月的时间可以说有苦有泪也有甜,前端小伙伴们为了同一个目标,共同努力,尽最大的努力完成公司的业务,可以说为了这个目标,这一年我付出了很多很多,在这方面愧对了家人孩子,虽然最后前端还是拆分了,可以说我能力有限,但是我心里可以说是无愧,希望尽到项目团队里的小伙伴们也能开心工作,迅速成长,虽然不再在一个团队里,但是,我仍是你们的光妈。

+

前端俱乐部推动

+

继续每周2小时的兴趣交流,交流形式不限于ppt,要求各自建立技术博客做笔记。

+
+

俱乐部的活动最后还是没能坚持下来,大家工作比较忙也好,我自己的原因也好,确实没能坚持下来,20年还是希望能够将俱乐部活动组织起来,算是大家交流的机会也好,帮助大家开阔眼界也好,20年一定要再做起来!

+

进行梯队划分建设

+

前端今年人数明显增多,但各层级职责并没有区分,19年抽时间进行梯队建设。

+
+

19年前端从14人增加到了34人,可以说人数上翻翻了,随着负责人制的执行,梯队建设的工作算是完成了50%,但是能力提升的工作可以说还是做的不到位,应该有机会成长为高工的小伙伴没能升上去,这我有一定的责任,20年虽然团队不再,但是希望你们都坚持努力学习,迈向更高的台阶。

+

引入前端工程化工具和思想

+

目前前端工程化已经非常成熟,希望能将成熟的技术和工具引入。

+
+

19年,随着Vue的普及和熟练,模块化开发组件化开发的思想已经深入到每一个前端的心中,现在大家都知道该如何使用这些技术和工具,希望大家能不仅仅局限在用也能了解了解其中原理,至少对常用的配置有所了解。

+

提升整体前端开发的能力

+

目前整体前端开发能力还相对较弱,19年通过培训等方式提升整体前端开发的能力

+
+

19年能力提升的工作做的太少了,没什么好说的,希望20年这方面工作能够投入更多的精力,让更多的小伙伴成长起来,让我们一起加油吧。

+

生活目标

每天陪孩子读书一小时

跟上面的工作和个人目标比起来,这个任务可以说完成度0%,这也是我觉得愧对家人的地方,为了工作,对家人的关注少了很多很多,以至于桐桐晚上都不愿意跟我一起睡觉,20年我要好好对她,做个好的爸爸,孩子明年就三岁了,去到幼儿园会认识更多的朋友,希望她会跟她的小伙伴说,我有个爱我的爸爸。

+

减肥

减肥这件事可以说是真的对我来说太难了,看着自己马上破三位数(kg)的体重,心里无限难过,每次媳妇儿问我啥时候去健身,我都感到像被闪电击中了似的,办了健身卡被人嘲笑成了洗澡卡,20年,减不到150斤我就。。。。

+

写在最后

19年太多的遗憾,太多的后悔莫及,当然也有一些成绩和自我肯定,19年已经过去了,希望在20年,能够成为一个更优秀的人。
WechatIMG6.jpeg
感谢我可爱的同事,年底收到了礼物真的很开心。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/111/index.html b/ja/page/111/index.html new file mode 100644 index 0000000000..cc1445794f --- /dev/null +++ b/ja/page/111/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

公司于去年开始代码版本管理从SVN迁移到了Git,采用的GitLab程序做管理,但是自动构建采用的是Jekins,最近在给博客做自动构建的时候了解到的了Github的Actions,我就在想,是不是GitLab也有自己的自动构建能力,因为之前在参与项目的时候也看到GitLab上有CI/CD相关的内容,但是没有仔细的去看,趁这次机会就一起研究一下。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/112/index.html b/ja/page/112/index.html new file mode 100644 index 0000000000..53b3ac2db7 --- /dev/null +++ b/ja/page/112/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近被公司新来的前端问到了一些微信页面的兼容性问题,因为我做微信开发的经验比较少,被问到了真是感到很尴尬,所以在搜到了解决方案告诉了他之后,我又深入的再网上找了一些微信开发中遇到的兼容问题,特此整理,以留后用。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/113/index.html b/ja/page/113/index.html new file mode 100644 index 0000000000..d7406769fd --- /dev/null +++ b/ja/page/113/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近愈发觉得越想往上走,越不能局限在前端开发的领域,再往上走前后端都是通的,最近在知乎上看到一句话,说是到了架构师甚至专家的层面以后,就没有什么前端架构师或者前端专家和后端架构师、后端专家了,只有前端出身的技术专家,和后端出身的技术专家,越往上越注重人的综合能力,当一个人能够解决一个项目的技术选型、架构等工作后,对于公司或者团队来说,那就是个巨大的宝藏。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/114/index.html b/ja/page/114/index.html new file mode 100644 index 0000000000..47b4f54486 --- /dev/null +++ b/ja/page/114/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

忙活了很久很久,终于不那么忙了,也终于想起来我还有个博客没有运行,哎,虽然用Hexo可以享受Github提供的免费的空间和域名,但是命令行敲代码的方式实现博客编写还是有些不方便,尤其是需要编程环境的时候,最近公司在用CICD的方式对代码进行构建,我就在想有没有一种方式让博客也可以变成这样,于是我就在网上搜Github Hexo 自动构建 这些个词,还真让我搜到了一种方法而且还是Github自己提供的,下面隆重请出 GitHub Actions

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/115/index.html b/ja/page/115/index.html new file mode 100644 index 0000000000..6dd429f006 --- /dev/null +++ b/ja/page/115/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

公司这两年兴起了前后端分离的热潮,在选用三大技术框架时,vue凭借其学习曲线低深受公司程序员的喜爱,因此我们也就走上了vue学习的不归路,在招聘招揽小伙伴时有那么一些问题经常被问起,vue组件间通信就是其中之一,下面我们来聊一聊vue组件通信有哪些方式。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/116/index.html b/ja/page/116/index.html new file mode 100644 index 0000000000..ec7cbd4128 --- /dev/null +++ b/ja/page/116/index.html @@ -0,0 +1,554 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

指令表

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
指令含义备注
git add .提示增加文件.代表所有
git commit -m“说明内容” 提交到本地服务器
git status显示修改信息
git pull从网络服务器拉 更新最新版本
git push上传最新版本
git branch查看当前分支
git checkout develop切换到develop模式
git merge master从master合并过来
git push origin develop提交
git clone git@192.168.2.10:bat-web.git从服务器克隆
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/117/index.html b/ja/page/117/index.html new file mode 100644 index 0000000000..84f4789a50 --- /dev/null +++ b/ja/page/117/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

近日工作不是那么忙,所以有时间看看书,这本书之前就已经读过一遍,隔了一段时间已经有所忘记,所以再次拾起,重新回顾一下。.

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/118/index.html b/ja/page/118/index.html new file mode 100644 index 0000000000..45b198d31c --- /dev/null +++ b/ja/page/118/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

VSCode是新一代IDE,方便灵活的插件安装方式让它强大无比,对前端友好,让它成为了越来越多前端开发者的首选IDE,编辑器虽好,但是要自己安装符合自己需求的插件才行,下面介绍一款插件,名叫ESLint,是用来静态检测js代码的工具,让你的js代码在编辑过程中就能找到错误,提高代码质量,好了废话不多说下面介绍如何安装和配置这款插件。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/119/index.html b/ja/page/119/index.html new file mode 100644 index 0000000000..b5fbf4250b --- /dev/null +++ b/ja/page/119/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

上篇文章介绍了如何利用github+hexo+next制作一个博客系统,但是由于这个博客系统是基于静态页面的,所以无法完成评论功能,博客怎么可以没有评论功能呢,当然next主题提供了几个选项,但是国内你知道的,提供的几个评论功能都不是太好用,于是就上网找了好多资料,功夫不负有心人,让我找到了一个名曰gitment的基于github-issue的评论插件,下面就介绍大家如何使用这个插件。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/12/index.html b/ja/page/12/index.html new file mode 100644 index 0000000000..cb2eeaa3a0 --- /dev/null +++ b/ja/page/12/index.html @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

这里假设要从Linux服务器A中把文件 file.txt 传输到Linux服务器B,该怎么做,这里就可以用scp实现
为了能够成功的传输文件,需要做到以下几点:

+
    +
  1. A需要知道要传输的的文件本地的路径
  2. +
  3. A为了往B传输文件,所以需要知道B的访问权限,因此需要B的访问地址,账号和私钥
  4. +
  5. 目标路径
  6. +
  7. 因为Linux权限管理的要求,私钥的访问权限需要设定为600
  8. +
+

当然还有一点前提是A和B两台服务器之间本身是可以通信的
知道以上信息便可以进行文件传输

+

实践

现在假设

+
    +
  1. A要往B传输的文件在A的如下目录:/tmp/transfer/file.txt
  2. +
  3. B的访问地址是 192.168.31.31 访问用户名是 testuser 用到的key上传到了A的如下目录: /tmp/ssh/test.key
  4. +
  5. 目标路径为B的如下目录: /tmp/test
    知道上面信息后我们来创建命令。
  6. +
+
1
2
chomd 600 /tmp/ssh/test.key
scp -i /tmp/ssh/test.key /tmp/transfer/file.txt testuser@192.168.31.31 /tmp/test
+ +

通过上面的命令即可实现从A服务器传输文件到B服务器了。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/120/index.html b/ja/page/120/index.html new file mode 100644 index 0000000000..6e7b44709e --- /dev/null +++ b/ja/page/120/index.html @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

+

开博客貌似并不是一件难事,现在有很多的方式开博客,而且现在博客貌似也不再流行,但是作为一个开发者,维护一个技术博客对自己的发展是很有好处的,出名者如阮一峰等,所以还是想要做一个技术博客的。

+
+
+

开博客可以选择网上现有的博客系统如博客园以及其他各大公司提供的博客系统,也可以选择如wordpress等博客程序自行搭建博客系统,前者省事,但可自定义设置不足不符合一个爱折腾的人的性格,后者复杂些,爱折腾的可以学着搭建,但是服务器域名是个让人头疼的事,还有数据库等操作,着实麻烦,如今又有了另外一个选择,那就是利用GitHub和一些静态的博客系统搭建一个纯静态的博客系统,不花钱又能折腾,而且还是命令行下的,装B感觉棒棒的,另外搭配时下流行的MarkDown语法,做笔记正合我意。

+
+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/121/index.html b/ja/page/121/index.html new file mode 100644 index 0000000000..a627c70cdb --- /dev/null +++ b/ja/page/121/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近公司技术选型选择了VUE,下面就记录一下在学习Vue中遇到的一些知识点,有些在官方手册里已经讲的很详细了,这里就简单带过,主要对cli产生的以及自行添加的一些项目目录进行一下解释,希望对正在学习Vue的同学提供一点帮助,如果本文有谬误之处还请各位指出,谢谢各位支持。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/122/index.html b/ja/page/122/index.html new file mode 100644 index 0000000000..320af5c59b --- /dev/null +++ b/ja/page/122/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

现代化的IDE已经把开发者变得越来越懒,但是我觉得这样挺好的,让工具完成手工反复重复的工作,提高工作效率的同时降低开发者的疲劳感,Vue VSCode Snippets就是这样一个VSC插件,它可以用简单的几个字母就敲出一整块代码片段,在学习和做VUE项目时可以极大地提高工作效率,下面就介绍一下插件的常见命令。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/123/index.html b/ja/page/123/index.html new file mode 100644 index 0000000000..9e99eb585e --- /dev/null +++ b/ja/page/123/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

近两年前端突然呈爆发增长之势,node的出现起了重要的作用,而node之中有一个重要的功能就是npm包管理工具,下面就记录一下我学习npm之中遇到的一些问题及解决方案,仅供各位参考。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/124/index.html b/ja/page/124/index.html new file mode 100644 index 0000000000..be5ab0cea0 --- /dev/null +++ b/ja/page/124/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

作为前端,浏览器就是你的战场,而不同的浏览器就是不同的敌人,敌人有相同之处,也各有特点,这不同的特点就是不同浏览器的兼容性,下面就聊聊浏览器的兼容问题,如何解决不同浏览器的兼容性。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/125/index.html b/ja/page/125/index.html new file mode 100644 index 0000000000..94b90853c5 --- /dev/null +++ b/ja/page/125/index.html @@ -0,0 +1,604 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

昨天花了1天的时间读了《晨间日记的奇迹》,感觉整个人的认识发生了一些变化,下面把重要内容做一下摘抄总结。

+

早上写日记的好处 —— 提升效率

    +
  • 可以做好一天的准备 — 计划性
  • +
  • 可以正确的写出昨天发生的事 — 效率性&忠诚性
  • +
  • 可以中立的看待昨天 — 中立性
  • +
  • 相对自由的时间 — 持续性
  • +
  • 总结经验 — 活用性
  • +
+

注意事项

日记 不等于 日志
日记 要记录 时间 天气 心情 — 因为这些有助于回忆和记录
不要投入过长时间 — 3分钟 — 日记私密性
晨间日记前7天只写好事 — 反省固然重要,但是成长更重要,持续性很重要 — 巴普洛夫的狗

+

晨间日记2部分

Part1

客观记录已经发生的事(昨天)— 经验智慧

+

Part2

    +
  • 今天应做的事 — 具体行动(来自昨天的总结
  • +
  • 今天一定要做的事(紧急重要的事)— 变得更积极更主动
  • +
  • 未来要做的事 — 不紧急但重要的事
  • +
  • 连用日记 — 历史上的今天(过去一年同一天的事)
  • +
+

夜晚日记 VS 晨间日记

受当天情绪影响 — 更冷静

+

梦想成真表

+ + + + + + + + + + + + + + + + + +
过去未来
事实IQ 智慧指数NQ 人际关系指数
感情EQ 情感指数DQ 梦想指数
+
    +
  • IQ — 晨间日记冷静回忆分析 — 提高决策正确性 IQ up
  • +
  • EQ — 冷静记录昨日的情绪/自省 — 变积极 EQ up
  • +
  • NQ — 总结昨天给今天设立“对/为XXX做某事” 很重要 NQ up
  • +
  • DQ — 不能只有眼前的事 每天都要确定一下自己的梦想 脸皮厚点没关系 重复或只有一句话也没关系 铭记于心最重要
  • +
+

“忙碌”只是借口!“决心”才是问题! 对未来充满期待的人才有行动力

+

如何早起

    +
  • 设定起床音乐/用气味(精油)刺激起床/光线-电动窗帘早起自动拉开
  • +
  • 早起后淋浴 — 配合音乐/冥想/呐喊 — 让自己充满动力保持清醒
  • +
+

写日记的五大好处

    +
  • 提升写作能力
  • +
  • 谈话题材源源不断
  • +
  • 提高贵人运
  • +
  • 返现自我肉体和精神的状态与模式
  • +
  • 在自己身上挖宝,彻底改变人生
  • +
+

记录的日记要常拿出来看看

记录日记时 — 问问自己的灵魂 然后写下真心话 — 灵魂日记
六度空间理论

+

七种成功者的习惯

    +
  • 习惯之一:积极主动别指望谁能推你走 — 主动行动,迈出第一步
  • +
  • 习惯之二:以终为始忠于自己的人生计划。— 设立人生目标并坚持
  • +
  • 习惯之三:要事第一选择当前该做的事
  • +
  • 习惯之四:追求双赢远离角斗场
  • +
  • 习惯之五:善于沟通换位思考的原则
  • +
  • 习惯之六:统合综效 1+1可以大于2
  • +
  • 习惯之七:不断更新全方位平衡自我
  • +
+

早睡是为了身体,早起是为了我们的内心。— sugiponn

+

晨间日记的格式

晨间日记必须要设立的栏目 — 工作/金钱/健康/人际关系
要记下当日的日期/天气/温度/湿度

+

纬度标签
工作方面:

+
    +
  • 能力开发/经营/管理/进货/工作人员/学习/资格/活动/企划案/想法/推广
    金钱方面:
  • +
  • 收入/指出/购入/股票/资产/储蓄/家用
    健康方面:
  • +
  • 饮食/运动/性/减肥/身体状况/锻炼身体/体重/脂肪
    人际关系方面:
  • +
  • 父母/家人/小孩/亲戚/情人/工作伙伴/朋友/熟人/联络事项/人脉网络
    兴趣方面以及其他:
  • +
  • 义工活动/阅读/运动/打扮/电影/音乐/游戏/喜欢的物品/今天必须要注意的事/美容/开心的事/购物
  • +
+

劳动 — 职业 — 工作 — 乐趣

+

三大原则和七大作战守则

    +
  • 原则1:时间不超过3分钟 — 减少养成习惯的成本

    +
  • +
  • 原则2:决定好写晨间日记的地方 — 为了养成习惯

    +
  • +
  • 原则3:只写一个字也没关系 — 不要有压力

    +
  • +
  • 作战守则1:开心写日记 — 反省固然重要,鼓励更有作用 — 开始写晨间日记前七天只写好事

    +
  • +
  • 作战守则2:ONE-TWO作战 — 把动作分布容易养成习惯 — 形成固定动作

    +
  • +
  • 作战守则3:巴普洛夫的狗 — 开始写日记时候要有一个仪式 — 心理暗示

    +
  • +
  • 作战守则4:奖励自己 — 当习惯养成并坚持到一定时间,给自己一个奖励

    +
  • +
  • 作战守则5:宣告天下 — 借由他人的鼓励或压力形成动力/也可找到同志

    +
  • +
  • 作战守则6:一千日作战法 — 不管什么样的习惯坚持1000日就会成为永远的习惯 — 具体做法为记录 第XXX篇 晨间日记

    +
  • +
  • 作战守则7:为了别人 — 写晨间日记不只是为了自己,而且更是为了别人,身边人的幸福最终会影响到自己

    +
  • +
+

应该先肯定自己,给自己打100分

    +
  • “总之,先… 病” 没有目标/没有梦想 — 回顾时一定会后悔!
  • +
  • “假如…,应该 病” 无法满足/没有动力 — 焦躁不安
  • +
  • 共同点:失落感/空虚/
  • +
+

解决办法— 设立一个情境

例:我要建立一个幸福的家庭 —> 因此我要创造很多快乐的回忆 —> 为了制造很多快乐的回忆 —> 我必须要到很多的地方去看看 —> 为了去很多地方看看,我觉得有辆车会更方便 —> 买车 —> 考驾照

+

不要总想自己哪里欠缺,要想自己想要得到什么,并且为了目标努力,你就会变的更完美。
拥有一个敢于大声喊出的梦想,不要羞涩,要为了梦想一步步的前进,最终达到梦想。

+

提到的另外的书

《培育梦想种子》《日记的力量》《成功人士的七个习惯》

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/126/index.html b/ja/page/126/index.html new file mode 100644 index 0000000000..8668908e24 --- /dev/null +++ b/ja/page/126/index.html @@ -0,0 +1,696 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近读了《让老板提拔你》,下面将书中觉得有用的内容进行摘抄,放在下方。

+

定位自己

正确认识自己,确定社会定位、职业定位。 定位-决定-定价

+

要素

核心竞争力职位 契合度 是高薪关键所在

+

契合度

    +
  • 技能、专长、经历与职位要求的契合度
  • +
  • 专业资质和等级与职位要求的契合度
  • +
  • 综合素质与职位要求的契合度

    七大秘诀

  • +
  • 了解同行业薪酬的平均水平
  • +
  • 赢得未来单位的心
  • +
  • 先让对方开口
  • +
  • 勇敢地开口要求
  • +
  • 不要轻言放弃
  • +
  • 把握时机很重要
  • +
  • 说实话,别撒谎

    如何谈薪资

  • +
  • 商洽薪酬的关键在于把握好谈话时机,切勿开门见山。
    +

    只要有发展机会,我愿意接受贵单位的薪酬标准,不知道按规定这个工作岗位的薪酬标准是多少。

    +
    +
  • +
+

将知识卖个好价钱

推销自己既推销自己所掌握的知识即智慧,正确认识自己的价值是关键。

+

高薪是因为“物有所值”

    +
  • 用业绩、用能力说话,是人才坦然面对高薪的心态。
  • +
  • 高薪人才的求职方式 —- 圈内朋友引见、定向投简历、行业论坛、交流活动等。
  • +
  • 高级人才不能经常跳槽要保持相对的职业稳定。—- 在一家公司少则3年多则6年 —- 接受挑战,把握机会。
  • +
+

失败丰富走向成功经验

强调在失败中吸取的经验,在未来中可以避免的损失。

+

能为企业带来丰厚的利润才是人才

企业招聘员工的唯一目的:追求公司业绩最大化、利润最大化。

+

高质高效工作全攻略

    +
  • 进行正确的自我评价
  • +
  • 做最擅长做的事
      +
    • 三个经济原则 —- 发挥人才优势。
        +
      1. 比较利益原则—–自己的专长对自己才是最有利的,勿与他人作比较。
      2. +
      3. 机会成本原则—–选择一份工作放弃其他工作就是机会成本。
      4. +
      5. 效率原则——–工作的成功不在于时间有多长,而在于成效有多少,附加值有多少。
      6. +
      +
    • +
    +
  • +
  • 马上行动
  • +
  • 做事情要雷厉风行 ——— 成功属于踏实肯干的人
  • +
  • 有条不紊地开展工作 ——- 制定时间计划
  • +
  • 善于利用现代办公工具
  • +
  • 给自己最大的工作空间
  • +
  • 建立高效有序的办公环境
  • +
  • 不要忘记最初想去的方向
  • +
  • “聪明”的向上级提出建议
  • +
  • 专心做事,避免浮躁
  • +
  • 多而不专,一事难成
  • +
  • “专”才有高质工作——–人才主要指专才–某个领域的高精尖人才–勿做全才,一专多能
  • +
  • 做事要有条理
  • +
+

不要只把自己当成一个打工仔

+

要把工作当成事业

+
+
    +
  • 工作不仅仅是为了钱
  • +
  • 对工作要有明确的价值取向
      +
    1. 认清人生的方向
    2. +
    3. 开始学会醉卧探索和认知
    4. +
    5. 认清工作价值与成就的关系
    6. +
    7. 长期的工作规划
    8. +
    9. 在生命的天平上衡量自身的价值
    10. +
    +
  • +
  • 巧妙应对与上司看法向左时的三条准则
      +
    1. 遇事考虑全局
    2. +
    3. 辩证地看待问题
    4. +
    5. 切记感情用事
    6. +
    +
  • +
  • 把单位的事当成自家的事
  • +
  • 认真负责地用心工作
  • +
  • 珍惜岗位,热爱自己的职业
  • +
  • 永远是在为自己工作
  • +
  • 敬重自己的工作
  • +
  • 不要轻视薪水微薄的工作
  • +
  • 永远对工作充满激情
  • +
  • 以自己的工作为荣
  • +
  • 不要被他人的观点所束缚
  • +
  • 暂时的胜负并不会决定人生的最后走向
  • +
  • 将弱势转化为优势
  • +
  • 全力以赴做好每一天的工作
  • +
  • 和优秀的人士在一起—见贤思齐、借梯爬楼
      +
    • 如何争取跟优秀的人在一起
        +
      1. 不断的抛头露面
      2. +
      3. 帮助可以帮助自己成就事业的人做事
      4. +
      5. 与上司和比自己优秀的人士一起合作
      6. +
      +
    • +
    +
  • +
+
    +
  1. 尊重对方,严谨有致
  2. +
  3. 切记奉承,要不卑不亢
  4. +
  5. 态度自然,不必拘谨
  6. +
  7. 陪衬得当,不可狂妄
  8. +
  9. 主动真诚,做出姿态
  10. +
  11. 求助求教,接受呵护
  12. +
+
    +
  • 挑战自我,承担责任
      +
    • 三条忠告
        +
      1. 全心全意工作
      2. +
      3. 把自己视为合伙人
      4. +
      5. 迎接变革的需求
      6. +
      +
    • +
    +
  • +
  • 自信独立,不随波逐流
  • +
  • 敢于显示自己很重要
  • +
  • 千万不能只知道抱怨上司
  • +
  • 保持严谨认真的做事习惯
  • +
  • 自主地做好手中的工作
  • +
  • 踏踏实实地做好本职工作
  • +
  • 丢掉工作散漫的坏习惯
  • +
  • 不要让浮躁的性格困扰自己
  • +
  • 不推诿,勇于承担责任
  • +
  • 无论如何都不要拖延工作
  • +
  • 糊弄工作只能是在糊弄自己
  • +
  • 逊色的工作只会淘汰自己
  • +
  • 千万别丢掉“得宠”之资
  • +
  • “一步登天”只会摔疼自己
  • +
  • 别让“差不多”贻误了自己
  • +
  • 能完成100%,就决不做99%
  • +
+

与上司相处

    +
  • 不要做上司的“心腹”
  • +
  • 适时恰当的赞美上司
      +
    • 赞美上司,还要善于选择适当的场合
    • +
    • 赞美上司,要学会巧借公众语言称赞
    • +
    • 赞美上司,还要善于赞美不得志的上司
    • +
    +
  • +
  • 主动与领导沟通
  • +
  • 主动和上司保持联系
  • +
  • 用“心机”主动接近上司
      +
    • 尽可能详细的了解上司
    • +
    • 选择一个与领导尽可能近的位置
    • +
    • 赢得上司青睐的方法
    • +
    +
  • +
  • 更有效的和上司沟通
      +
    • 与上司沟通要简洁
    • +
    • 与上司沟通要大度大气大方
    • +
    • 与上司沟通,就要把自己先放一边 ——– 说话对事不对人
    • +
    +
  • +
  • 四种和上司进行沟通的方法
      +
    1. 开诚布公式———提出问题,说出自己的理解认识,给出解决方案,寻求领导意思,执行领导的命令
    2. +
    3. 先斩后奏式———因主客观原因导致的没有先汇报而是先解决的事,事后要将问题的分析方法,解决方法,实施方案以及结果做一份详尽的总结报告上交给领导
    4. +
    5. “含情脉脉”式——-通过交流一些工作上的问题来含蓄表达出个人的建议、需要
    6. +
    7. 巧设比喻式———通过典型案例来暗示自己的意见和要求
    8. +
    +
  • +
  • 把话说到上司的心坎上
  • +
  • 巧妙地为领导拾遗补缺
      +
    1. 诠释领导讲话的难点
    2. +
    3. 强调领导的才干
    4. +
    5. 化严肃为幽默
    6. +
    7. 稳定情绪,委婉暗示
    8. +
    +
  • +
  • 工作中勤于请示汇报
      +
    1. 听懂上司的意图
    2. +
    3. 探讨、磨合,达成共识
    4. +
    5. 制定尽可能详尽的工作计划
    6. +
    7. 随时向上司汇报任务的关键点
    8. +
    9. 总结汇报
    10. +
    +
  • +
  • 用成功赢得上司的信任
  • +
  • 工作中不要冲撞上司
  • +
  • 处理好同上司之间的分歧
      +
    1. 圆融协调——领导不懂,下达了错误的指令
        +
      1. 私下向上司陈述意见,帮助上司做出正确的决策
      2. +
      3. 如果上司采纳了下属的建议或意见,该下属有责任长期保守秘密
      4. +
      5. 如果上司固执己见,那么无条件服从
      6. +
      +
    2. +
    3. 装聋作哑——不涉及到原则问题
    4. +
    5. 棘手难题多权衡
        +
      1. 立刻插话纠正
      2. +
      3. 提醒上司
      4. +
      5. 暗示
      6. +
      7. 事后补救
      8. +
      9. 事后提醒
      10. +
      +
    6. +
    +
  • +
  • 正确对待上司的批评
  • +
  • 要善于服从自己的上司
  • +
  • 正确化解来自上司的压力
  • +
+

写在最后

博观约取,多读书读好书,丰富自己,变得睿智。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/127/index.html b/ja/page/127/index.html new file mode 100644 index 0000000000..1cf0ef9758 --- /dev/null +++ b/ja/page/127/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

利用CSS的border属性可以生成一些图形,例如三角或是圆角。纯粹的CSS2的内容,没有兼容性的问题,我之前在纯CSS实现各类气球泡泡对话框效果一文中算是比较详细的讲述了CSS border属性生成三角的原理,以及实例。我觉得此技术相当实用的,故本文再次简单叙述一下,另外,本文还将展示可能并不为众人所知的CSS border圆角生成技术。好了,裹脚布的话就不说了,直接进入正题。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/128/index.html b/ja/page/128/index.html new file mode 100644 index 0000000000..e5fa39a2b8 --- /dev/null +++ b/ja/page/128/index.html @@ -0,0 +1,526 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近读了《高性能JAVASCRIPT》,下面将书中觉得有用的内容进行摘抄,放在下方。

+

如何从小事提升JAVASCRIPT性能。

    +
  1. <script>标签写在</body>之前——将脚本放在底部。

    +
  2. +
  3. 尽量少的<script>标签,减少页面中的外链数量,减少请求次数。

    +
  4. +
  5. 使用打包工具如:Yahoo!combo handler

    +
  6. +
  7. 使用动态延迟加载技术如:LazyLoad类库,LABjs

    +
    1
    2
    3
    4
    5
    6
    7
    // js代码
    <script type="text/javascript" src="lazyload-min.js"></script>
    <script type="text/javascript">
    LazyLoad.js([],function(){
    Application.init();
    })
    </script>
  8. +
  9. 在javascript中存储位置十分重要,尽量用局部变量代替对象成员的访问。

    +
  10. +
  11. 集合变数组提高查询效率

    +
    1
    2
    3
    4
    5
    6
    7
    // js代码
    function toArray(coll){
    for(var i = 0, a=[], len=coll.length; i<len; i++){
    a[i]=col[i];
    }
    return a;
    }
  12. +
  13. 使用局部变量缓存访问多次的成员
    当便利一个集合时,首要优化原则是把集合存储在局部变量中,并把length缓存在循环外部,然后使用局部变量访问这些需要多次访问的元素。

    +
  14. +
  15. 使用原生DOM方法querySelectorAll()遍历查找元素。

    +
  16. +
  17. 让元素脱离动画流,页面重排次数越多,程序响应越慢,要尽量减少重拍
    方法:

    +
  18. +
    1. +
    2. 使用绝对定位使元素脱离文档流
    3. +
    +
  19. +
  20. IE:hover
    在大量元素使用:hover这个伪css选择器时,程序会降低响应速率,在大表格或列表的情况下

    +
  21. +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/13/index.html b/ja/page/13/index.html new file mode 100644 index 0000000000..032a94ef0d --- /dev/null +++ b/ja/page/13/index.html @@ -0,0 +1,582 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

nginx中的超时设置

Nginx主要有四类超时设置:客户端超时设置DNS解析超时设置代理超时设置,如果使用ngx_lua,则还有lua相关的超时设置。

+

客户端超时设置

对于客户端超时主要设置有读取请求头超时时间读取请求体超时时间发送响应超时时间长连接超时时间。通过客户端超时设置避免客户端恶意或者网络状况不佳造成连接长期占用,影响服务端的可处理的能力。

+

client_header_timeout 指定等待client发送一个请求头的超时时间

    +
  • 语法: client_header_timeout time
  • +
  • 默认值: 60s
  • +
  • 上下文: http server
  • +
  • 说明: 指定等待client发送一个请求头的超时时间(例如:GET / HTTP/1.1).仅当在一次read中,没有收到请求头,才会算成超时。如果在超时时间内,client没发送任何东西,nginx返回HTTP状态码408(“Request timed out”)
  • +
+

client_body_timeout 该指令设置请求体(request body)的读超时时间

    +
  • 语法: client_body_timeout time
  • +
  • 默认值: 60s
  • +
  • 上下文: http server location
  • +
  • 说明: 该指令设置请求体(request body)的读超时时间。仅当在一次readstep中,没有得到请求体,就会设为超时。超时后,nginx返回HTTP状态码408(“Request timed out”)
  • +
+

send_timeout time

设置发送响应到客户端的超时时间,默认为60s,此超时时间指的也是两次成功写操作间隔时间,而不是发送整个响应的超时时间。如果在此超时时间内客户端没有接收任何响应,则Nginx关闭此连接。

+

keepalive_timeout 第一个参数指定了与client的keep-alive连接超时时间

    +
  • 语法: keepalive_timeout timeout [ header_timeout ]

    +
  • +
  • 默认值: 75s

    +
  • +
  • 上下文: http server location

    +
  • +
  • 说明: 第一个参数指定了与client的keep-alive连接超时时间。服务器将会在这个时间后关闭连接。可选的第二个参数指定了在响应头Keep-Alive: timeout=time中的time值。这个头能够让一些浏览器主动关闭连接,这样服务器就不必要去关闭连接了。没有这个参数,nginx不会发送Keep-Alive响应头(尽管并不是由这个头来决定连接是否“keep-alive”)
    两个参数的值可并不相同

    +
      +
    • 注意不同浏览器怎么处理“keep-alive”头
    • +
    • MSIE和Opera忽略掉”Keep-Alive: timeout=“ header.
    • +
    • MSIE保持连接大约60-65秒,然后发送TCP RST
    • +
    • Opera永久保持长连接
    • +
    • Mozilla keeps the connection alive for N plus about 1-10 seconds.
    • +
    • Konqueror保持长连接N秒
    • +
    +
  • +
+

DNS解析超时设置

resolver_timeout 设置DNS解析超时时间

    +
  • 语法 resolver_timeout time
  • +
  • 默认值 30s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置DNS解析超时时间
  • +
+

代理超时设置

proxy_connect_timeout 设置与upstream server的连接超时时间,不能超过75秒

    +
  • 语法 proxy_connect_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置与upstream server的连接超时时间,有必要记住,这个超时不能超过75秒。
    这个不是等待后端返回页面的时间,那是由proxy_read_timeout声明的。如果你的upstream服务器起来了,但是hanging住了(例如,没有足够的线程处理请求,所以把你的请求放到请求池里稍后处理),那么这个声明是没有用的,由于与upstream服务器的连接已经建立了。
  • +
+

proxy_read_timeout 设置与代理服务器的读超时时间

    +
  • 语法 proxy_read_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置与代理服务器的读超时时间。它决定了nginx会等待多长时间来获得请求的响应。这个时间不是获得整个response的时间,而是两次reading操作的时间。
  • +
+

proxy_send_timeout 设置发送请求给upstream服务器的超时时间

    +
  • 语法 proxy_send_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 这个指定设置了发送请求给upstream服务器的超时时间。超时设置不是为了整个发送期间,而是在两次write操作期间。如果超时后,upstream没有收到新的数据,nginx会关闭连接
  • +
+

失败重试机制设置。

proxy_next_upstream error | timeout | invalid_header | http_500 | http_502 | http_503 | http_504 |http_403 | http_404 | non_idempotent | off …:配置什么情况下需要请求下一台上游服务器进行重试。默认为“errortimeout”。error表示与上游服务器建立连接、写请求或者读响应头出错。timeout表示与上游服务器建立连接、写请求或者读响应头超时。invalid_header表示上游服务器返回空的或错误的响应头。http_XXX表示上游服务器返回特定的状态码。non_idempotent表示RFC-2616定义的非幂等HTTP方法(POST、LOCK、PATCH),也可以在失败后重试下一台上游服务器(即默认幂等方法GET、HEAD、PUT、DELETE、OPTIONS、TRACE才可以重试)。off表示禁用重试。

+

重试不能无限制进行,因此,需要如下两个指令控制重试次数和重试超时时间。

+

proxy_next_upstream_tries number:设置重试次数,默认0表示不限制,注意此重试次数指的是所有请求次数(包括第一次和之后的重试次数之和)。

+

proxy_next_upstream_timeout time:设置重试最大超时时间,默认0表示不限制。

+

即在proxy_next_upstream_timeout时间内允许proxy_next_upstream_tries次重试。如果超过了其中一个设置,则Nginx也会结束重试并返回客户端响应(可能是错误码)。

+

如下配置表示当error/timeout时重试upstream中的下一台上游服务器,如果重试的总时间超出了6s或者重试了1次,则表示重试失败(因为之前已经请求一次了,所以还能重试一次),Nginx结束重试并返回客户端响应。

+

upstream存活超时设置

max_fails和fail_timeout:配置什么时候Nginx将上游服务器认定为不可用/不存活。当上游服务器在fail_timeout时间内失败了max_fails次,则认为该上游服务器不可用/不存活。并在接下来的fail_timeout时间内从upstream摘掉该节点(即请求不会转发到该上游服务器)。

+

什么情况下被认定为失败呢?其由 proxy_next_upstream定义,不过,不管 proxy_next_upstream如何配置,error, timeout and invalid_header 都将被认为是失败。

+

如server 192.168.61.1:9090max_fails=2 fail_timeout=10s;表示在10s内如果失败了2次,则在接下来的10s内认定该节点不可用/不存活。这种存活检测机制是只有当访问该上游服务器时,采取惰性检查,可以使用ngx_http_upstream_check_module配置主动检查。

+

max_fails设置为0表示不检查服务器是否可用(即认为一直可用),如果upstream中仅剩一台上游服务器时,则该服务器是不会被摘除的,将从不被认为不可用。

+

proxy_upstream_fail_timeout(fail_timeout)设置某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间

    +
  • 语法 server address [fail_timeout=30s]
  • +
  • 默认值 10s
  • +
  • 上下文 upstream
  • +
  • 说明 Upstream模块下 server指令的参数,设置了某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间,默认为10秒
  • +
+

lingering_timeout

    +
  • 语法: lingering_timeout time
  • +
  • 默认值: 5s
  • +
  • 上下文: http server location
  • +
  • 说明: lingering_close生效后,在关闭连接前,会检测是否有用户发送的数据到达服务器,如果超过lingering_timeout时间后还没有数据可读,就直接关闭连接;否则,必须在读取完连接缓冲区上的数据并丢弃掉后才会关闭连接。
  • +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/14/index.html b/ja/page/14/index.html new file mode 100644 index 0000000000..fea72bf13c --- /dev/null +++ b/ja/page/14/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

前端常见知识点整理 —- 网络安全(2)

SQL 注入

SQL 注入漏洞(SQL Injection)是 Web 开发中最常见的一种安全漏洞。可以用它来从数据库获取敏感信息,或者利用数据库的特性执行添加用户,导出文件等一系列恶意操作,甚至有可能获取数据库乃至系统用户最高权限。

+

而造成 SQL 注入的原因是因为程序没有有效的转义过滤用户的输入,使攻击者成功的向服务器提交恶意的 SQL 查询代码,程序在接收后错误的将攻击者的输入作为查询语句的一部分执行,导致原始的查询逻辑被改变,额外的执行了攻击者精心构造的恶意代码。

+

很多 Web 开发者没有意识到 SQL 查询是可以被篡改的,从而把 SQL 查询当作可信任的命令。殊不知,SQL 查询是可以绕开访问控制,从而绕过身份验证和权限检查的。更有甚者,有可能通过 SQL 查询去运行主机系统级的命令。

+

SQL 注入原理

下面将通过一些真实的例子来详细讲解 SQL 注入的方式的原理。

+

考虑以下简单的管理员登录表单:

+
1
2
3
4
5
<form action="/login" method="POST">
<p>Username: <input type="text" name="username" /></p>
<p>Password: <input type="password" name="password" /></p>
<p><input type="submit" value="登陆" /></p>
</form>
+ +

后端的 SQL 语句可能是如下这样的:

+
1
2
3
4
5
6
7
let querySQL = `
SELECT *
FROM user
WHERE username='${username}'
AND psw='${password}'
`;
// 接下来就是执行 sql 语句...
+ +

目的就是来验证用户名和密码是不是正确,按理说乍一看上面的 SQL 语句也没什么毛病,确实是能够达到我们的目的,可是你只是站在用户会老老实实按照你的设计来输入的角度来看问题,如果有一个恶意攻击者输入的用户名是 zoumiaojiang' OR 1 = 1 --,密码随意输入,就可以直接登入系统了。WFT!

+

冷静下来思考一下,我们之前预想的真实 SQL 语句是:

+
1
SELECT * FROM user WHERE username='zoumiaojiang' AND psw='mypassword'
+ +

可以恶意攻击者的奇怪用户名将你的 SQL 语句变成了如下形式:

+
1
2
SELECT * FROM user WHERE username='zoumiaojiang' OR 1 = 1 --' AND psw='xxxx'

+ +

在 SQL 中,-- 是注释后面的内容的意思,所以查询语句就变成了:

+
1
2
SELECT * FROM user WHERE username='zoumiaojiang' OR 1 = 1

+ +

这条 SQL 语句的查询条件永远为真,所以意思就是恶意攻击者不用我的密码,就可以登录进我的账号,然后可以在里面为所欲为,然而这还只是最简单的注入,牛逼的 SQL 注入高手甚至可以通过 SQL 查询去运行主机系统级的命令,将你主机里的内容一览无余,这里我也没有这个能力讲解的太深入,毕竟不是专业研究这类攻击的,但是通过以上的例子,已经了解了 SQL 注入的原理,我们基本已经能找到防御 SQL 注入的方案了。

+

如何预防 SQL 注入

防止 SQL 注入主要是不能允许用户输入的内容影响正常的 SQL 语句的逻辑,当用户的输入的信息将要用来拼接 SQL 语句的话,我们应该永远选择不相信,任何内容都必须进行转义过滤,当然做到这个还是不够的,下面列出防御 SQL 注入的几点注意事项:

+
    +
  • 严格限制Web应用的数据库的操作权限,给此用户提供仅仅能够满足其工作的最低权限,从而最大限度的减少注入攻击对数据库的危害

    +
  • +
  • 后端代码检查输入的数据是否符合预期,严格限制变量的类型,例如使用正则表达式进行一些匹配处理。

    +
  • +
  • 对进入数据库的特殊字符(',",\,<,>,&,*,; 等)进行转义处理,或编码转换。基本上所有的后端语言都有对字符串进行转义处理的方法,比如 lodash 的 lodash._escapehtmlchar 库。

    +
  • +
  • 所有的查询语句建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中,即不要直接拼接 SQL 语句。例如 Node.js 中的 mysqljs 库的 query 方法中的 ? 占位参数。

    +
  • +
+
1
mysql.query(`SELECT * FROM user WHERE username = ? AND psw = ?`, [username, psw]);
+ +
    +
  • 在应用发布之前建议使用专业的 SQL 注入检测工具进行检测,以及时修补被发现的 SQL 注入漏洞。网上有很多这方面的开源工具,例如 sqlmap、SQLninja 等。

    +
  • +
  • 避免网站打印出 SQL 错误信息,比如类型错误、字段不匹配等,把代码里的 SQL 语句暴露出来,以防止攻击者利用这些错误信息进行 SQL 注入。

    +
  • +
  • 不要过于细化返回的错误信息,如果目的是方便调试,就去使用后端日志,不要在接口上过多的暴露出错信息,毕竟真正的用户不关心太多的技术细节,只要话术合理就行。

    +
  • +
+

碰到要操作的数据库的代码,一定要慎重,小心使得万年船,多找几个人多来几次 code review,将问题都暴露出来,而且要善于利用工具,操作数据库相关的代码属于机密,没事不要去各种论坛晒自家站点的 SQL 语句,万一被人盯上了呢?

+

命令行注入

命令行注入漏洞,指的是攻击者能够通过 HTTP 请求直接侵入主机,执行攻击者预设的 shell 命令,听起来好像匪夷所思,这往往是 Web 开发者最容易忽视但是却是最危险的一个漏洞之一,看一个实例:

+

假如现在需要实现一个需求:用户提交一些内容到服务器,然后在服务器执行一些系统命令去产出一个结果返回给用户,接口的部分实现如下:

+
1
2
3
4
5
// 以 Node.js 为例,假如在接口中需要从 github 下载用户指定的 repo
const exec = require('mz/child_process').exec;
let params = {/* 用户输入的参数 */};

exec(`git clone ${params.repo} /some/path`);
+ +

这段代码确实能够满足业务需求,正常的用户也确实能从指定的 git repo 上下载到想要的代码,可是和 SQL 注入一样,这段代码在恶意攻击者眼中,简直就是香饽饽。

+

如果 params.repo 传入的是 https://github.com/zoumiaojiang/zoumiaojiang.github.io.git 当然没问题了。
可是如果 params.repo 传入的是 https://github.com/xx/xx.git && rm -rf /* && 恰好你的服务是用 root 权限起的就惨了。

+

具体恶意攻击者能用命令行注入干什么也像 SQL 注入一样,手法是千变万化的,比如「反弹 shell 注入」等,但原理都是一样的,我们绝对有能力防止命令行注入发生。防止命令行注入需要做到以下几件事情:

+
    +
  • 后端对前端提交内容需要完全选择不相信,并且对其进行规则限制(比如正则表达式)。
  • +
  • 在调用系统命令前对所有传入参数进行命令行参数转义过滤。
  • +
  • 不要直接拼接命令语句,借助一些工具做拼接、转义预处理,例如 Node.js 的 shell-escape npm 包。
  • +
+

还是前面的例子,我们可以做到如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const exec = require('mz/child_process').exec;

// 借助 shell-escape npm 包解决参数转义过滤问题
const shellescape = require('shell-escape');

let params = {/* 用户输入的参数 */};

// 先过滤一下参数,让参数符合预期
if (!/正确的表达式/.test(params.repo)) {
return;
}

let cmd = shellescape([
'git',
'clone',
params.repo,
'/some/path'
]);

// cmd 的值: git clone 'https://github.com/xx/xx.git && rm -rf / &&' /some/path
// 这样就不会被注入成功了。
exec(cmd);
+

无论是在何种后端语言环境中,凡是涉及到代码调用系统 shell 命令的时候都一定要谨慎。

+

DDoS 攻击

DDoS 又叫分布式拒绝服务,全称 Distributed Denial of Service,其原理就是利用大量的请求造成资源过载,导致服务不可用,这个攻击应该不能算是安全问题,这应该算是一个另类的存在,因为这种攻击根本就是耍流氓的存在,「伤敌一千,自损八百」的行为。出于保护 Web App 不受攻击的攻防角度,还是介绍一下 DDoS 攻击吧,毕竟也是挺常见的。

+

DDoS 攻击可以理解为:「你开了一家店,隔壁家点看不惯,就雇了一大堆黑社会人员进你店里干坐着,也不消费,其他客人也进不来,导致你营业惨淡」。为啥说 DDoS 是个「伤敌一千,自损八百」的行为呢?毕竟隔壁店还是花了不少钱雇黑社会但是啥也没得到不是?DDoS 攻击的目的基本上就以下几个:

+
    +
  • 深仇大恨,就是要干死你
  • +
  • 敲诈你,不给钱就干你
  • +
  • 忽悠你,不买我防火墙服务就会有“人”继续干你
  • +
+

也许你的站点遭受过 DDoS 攻击,具体什么原因怎么解读见仁见智。DDos 攻击从层次上可分为网络层攻击与应用层攻击,从攻击手法上可分为快型流量攻击与慢型流量攻击,但其原理都是造成资源过载,导致服务不可用。

+

网络层 DDoS

网络层 DDos 攻击包括 SYN FloodACK FloodUDP FloodICMP Flood 等。

+

SYN Flood 攻击

SYN flood 攻击主要利用了 TCP 三次握手过程中的 Bug,我们都知道 TCP 三次握手过程是要建立连接的双方发送 SYN,SYN + ACK,ACK 数据包,而当攻击方随意构造源 IP 去发送 SYN 包时,服务器返回的 SYN + ACK 就不能得到应答(因为 IP 是随意构造的),此时服务器就会尝试重新发送,并且会有至少 30s 的等待时间,导致资源饱和服务不可用,此攻击属于慢型 DDoS 攻击。

+

ACK Flood 攻击

ACK Flood 攻击是在 TCP 连接建立之后,所有的数据传输 TCP 报文都是带有 ACK 标志位的,主机在接收到一个带有 ACK 标志位的数据包的时候,需要检查该数据包所表示的连接四元组是否存在,如果存在则检查该数据包所表示的状态是否合法,然后再向应用层传递该数据包。如果在检查中发现该数据包不合法,例如该数据包所指向的目的端口在本机并未开放,则主机操作系统协议栈会回应 RST 包告诉对方此端口不存在。

+

UDP Flood 攻击

UDP flood 攻击是由于 UDP 是一种无连接的协议,因此攻击者可以伪造大量的源 IP 地址去发送 UDP 包,此种攻击属于大流量攻击。正常应用情况下,UDP 包双向流量会基本相等,因此发起这种攻击的攻击者在消耗对方资源的时候也在消耗自己的资源。

+

ICMP Flood 攻击

ICMP Flood 攻击属于大流量攻击,其原理就是不断发送不正常的 ICMP 包(所谓不正常就是 ICMP 包内容很大),导致目标带宽被占用,但其本身资源也会被消耗。目前很多服务器都是禁 ping 的(在防火墙在可以屏蔽 ICMP 包),因此这种攻击方式已经落伍。

+

网络层 DDoS 防御

网络层的 DDoS 攻击究其本质其实是无法防御的,我们能做得就是不断优化服务本身部署的网络架构,以及提升网络带宽。当然,还是做好以下几件事也是有助于缓解网络层 DDoS 攻击的冲击:

+
    +
  • 网络架构上做好优化,采用负载均衡分流。
  • +
  • 确保服务器的系统文件是最新的版本,并及时更新系统补丁。
  • +
  • 添加抗 DDos 设备,进行流量清洗。
  • +
  • 限制同时打开的 SYN 半连接数目,缩短 SYN 半连接的 Timeout 时间。
  • +
  • 限制单 IP 请求频率。
  • +
  • 防火墙等防护设置禁止 ICMP 包等。
  • +
  • 严格限制对外开放的服务器的向外访问。
  • +
  • 运行端口映射程序或端口扫描程序,要认真检查特权端口和非特权端口。
  • +
  • 关闭不必要的服务。
  • +
  • 认真检查网络设备和主机/服务器系统的日志。只要日志出现漏洞或是时间变更,那这台机器就可能遭到了攻击。
  • +
  • 限制在防火墙外与网络文件共享。这样会给黑客截取系统文件的机会,主机的信息暴露给黑客,无疑是给了对方入侵的机会。
  • +
  • 加钱堆机器。。
  • +
  • 报警。。-
  • +
+

应用层 DDoS

应用层 DDoS 攻击不是发生在网络层,是发生在 TCP 建立握手成功之后,应用程序处理请求的时候,现在很多常见的 DDoS 攻击都是应用层攻击。应用层攻击千变万化,目的就是在网络应用层耗尽你的带宽,下面列出集中典型的攻击类型。

+

CC 攻击

当时绿盟为了防御 DDoS 攻击研发了一款叫做 Collapasar 的产品,能够有效的防御 SYN Flood 攻击。黑客为了挑衅,研发了一款 Challenge Collapasar 攻击工具(简称 CC)。

+

CC 攻击的原理,就是针对消耗资源比较大的页面不断发起不正常的请求,导致资源耗尽。因此在发送 CC 攻击前,我们需要寻找加载比较慢,消耗资源比较多的网页,比如需要查询数据库的页面、读写硬盘文件的等。通过 CC 攻击,使用爬虫对某些加载需要消耗大量资源的页面发起 HTTP 请求。

+

DNS Flood

DNS Flood 攻击采用的方法是向被攻击的服务器发送大量的域名解析请求,通常请求解析的域名是随机生成或者是网络世界上根本不存在的域名,被攻击的DNS 服务器在接收到域名解析请求的时候首先会在服务器上查找是否有对应的缓存,如果查找不到并且该域名无法直接由服务器解析的时候,DNS 服务器会向其上层 DNS 服务器递归查询域名信息。域名解析的过程给服务器带来了很大的负载,每秒钟域名解析请求超过一定的数量就会造成 DNS 服务器解析域名超时。

+

根据微软的统计数据,一台 DNS 服务器所能承受的动态域名查询的上限是每秒钟 9000 个请求。而我们知道,在一台 P3 的 PC 机上可以轻易地构造出每秒钟几万个域名解析请求,足以使一台硬件配置极高的 DNS 服务器瘫痪,由此可见 DNS 服务器的脆弱性。

+

HTTP 慢速连接攻击

针对 HTTP 协议,先建立起 HTTP 连接,设置一个较大的 Conetnt-Length,每次只发送很少的字节,让服务器一直以为 HTTP 头部没有传输完成,这样连接一多就很快会出现连接耗尽。

+

应用层 DDoS 防御

    +
  • 判断 User-Agent 字段(不可靠,因为可以随意构造)
  • +
  • 针对 IP + cookie,限制访问频率(由于 cookie 可以更改,IP 可以使用代理,或者肉鸡,也不可靠)
  • +
  • 关闭服务器最大连接数等,合理配置中间件,缓解 DDoS 攻击。
  • +
  • 请求中添加验证码,比如请求中有数据库操作的时候。
  • +
  • 编写代码时,尽量实现优化,并合理使用缓存技术,减少数据库的读取操作。
  • +
  • 加钱堆机器。。
  • +
  • 报警。。
  • +
+

应用层的防御有时比网络层的更难,因为导致应用层被 DDoS 攻击的因素非常多,有时往往是因为程序员的失误,导致某个页面加载需要消耗大量资源,有时是因为中间件配置不当等等。而应用层 DDoS 防御的核心就是区分人与机器(爬虫),因为大量的请求不可能是人为的,肯定是机器构造的。因此如果能有效的区分人与爬虫行为,则可以很好地防御此攻击。

+

其他 DDoS 攻击

发起 DDoS 也是需要大量的带宽资源的,但是互联网就像森林,林子大了什么鸟都有,DDoS 攻击者也能找到其他的方式发起廉价并且极具杀伤力的 DDoS 攻击。

+

利用 XSS

举个例子,如果 12306 页面有一个 XSS 持久型漏洞被恶意攻击者发现,只需在春节抢票期间在这个漏洞中执行脚本使得往某一个小站点随便发点什么请求,然后随着用户访问的增多,感染用户增多,被攻击的站点自然就会迅速瘫痪了。这种 DDoS 简直就是无本万利,不用惊讶,现在大站有 XSS 漏洞的不要太多。

+

来自 P2P 网络攻击

大家都知道,互联网上的 P2P 用户和流量都是一个极为庞大的数字。如果他们都去一个指定的地方下载数据,成千上万的真实 IP 地址连接过来,没有哪个设备能够支撑住。拿 BT 下载来说,伪造一些热门视频的种子,发布到搜索引擎,就足以骗到许多用户和流量了,但是这只是基础攻击。
高级的 P2P 攻击,是直接欺骗资源管理服务器。如迅雷客户端会把自己发现的资源上传到资源管理服务器,然后推送给其它需要下载相同资源的用户,这样,一个链接就发布出去。通过协议逆向,攻击者伪造出大批量的热门资源信息通过资源管理中心分发出去,瞬间就可以传遍整个 P2P 网络。更为恐怖的是,这种攻击是无法停止的,即使是攻击者自身也无法停止,攻击一直持续到 P2P 官方发现问题更新服务器且下载用户重启下载软件为止。

+

DDoS 最后总结

DDoS 不可能防的住,就好比你的店只能容纳 50 人,黑社会有 100 人,你就换一家大店,能容纳 500 人,然后黑社会又找来了 1000 人,这种堆人头的做法就是 DDoS 本质上的攻防之道,「道高一尺,魔高一丈,魔高一尺,道高一丈」,讲真,必要的时候就答应勒索你的人的条件吧,实在不行就报警吧。

+

流量劫持

流量劫持应该算是黑产行业的一大经济支柱了吧?简直是让人恶心到吐,不吐槽了,还是继续谈干货吧,流量劫持基本分两种:DNS 劫持 和 HTTP 劫持,目的都是一样的,就是当用户访问 zoumiaojiang.com 的时候,给你展示的并不是或者不完全是 zoumiaojiang.com 提供的 “内容”。

+

DNS 劫持

DNS 劫持,也叫做域名劫持,可以这么理解,「你打了一辆车想去商场吃饭,结果你打的车是小作坊派来的,直接给你拉到小作坊去了」,DNS 的作用是把网络地址域名对应到真实的计算机能够识别的 IP 地址,以便计算机能够进一步通信,传递网址和内容等。如果当用户通过某一个域名访问一个站点的时候,被篡改的 DNS 服务器返回的是一个恶意的钓鱼站点的 IP,用户就被劫持到了恶意钓鱼站点,然后继而会被钓鱼输入各种账号密码信息,泄漏隐私。

+

这类劫持,要不就是网络运营商搞的鬼,一般小的网络运营商与黑产勾结会劫持 DNS,要不就是电脑中毒,被恶意篡改了路由器的 DNS 配置,基本上做为开发者或站长却是很难察觉的,除非有用户反馈,现在升级版的 DNS 劫持还可以对特定用户、特定区域等使用了用户画像进行筛选用户劫持的办法,另外这类广告显示更加随机更小,一般站长除非用户投诉否则很难觉察到,就算觉察到了取证举报更难。无论如何,如果接到有 DNS 劫持的反馈,一定要做好以下几件事:

+
    +
  • 取证很重要,时间、地点、IP、拨号账户、截屏、URL 地址等一定要有。
  • +
  • 可以跟劫持区域的电信运营商进行投诉反馈。
  • +
  • 如果投诉反馈无效,直接去工信部投诉,一般来说会加白你的域名。
  • +
+

HTTP 劫持

HTTP 劫持您可以这么理解,「你打了一辆车想去商场吃饭,结果司机跟你一路给你递小作坊的广告」,HTTP 劫持主要是当用户访问某个站点的时候会经过运营商网络,而不法运营商和黑产勾结能够截获 HTTP 请求返回内容,并且能够篡改内容,然后再返回给用户,从而实现劫持页面,轻则插入小广告,重则直接篡改成钓鱼网站页面骗用户隐私。能够实施流量劫持的根本原因,是 HTTP 协议没有办法对通信对方的身份进行校验以及对数据完整性进行校验。如果能解决这个问题,则流量劫持将无法轻易发生。所以防止 HTTP 劫持的方法只有将内容加密,让劫持者无法破解篡改,这样就可以防止 HTTP 劫持了。

+

HTTPS 协议就是一种基于 SSL 协议的安全加密网络应用层协议,可以很好的防止 HTTP 劫持。这里有篇 文章 讲的不错。HTTPS 在这就不深讲了,后面有机会我会单独好好讲讲 HTTPS。如果不想站点被 HTTP 劫持,赶紧将你的站点全站改造成 HTTPS 吧。

+

服务器漏洞

服务器除了以上提到的那些大名鼎鼎的漏洞和臭名昭著的攻击以外,其实还有很多其他的漏洞,往往也很容易被忽视,在这个小节也稍微介绍几种。

+

越权操作漏洞

如果你的系统是有登录控制的,那就要格外小心了,因为很有可能你的系统越权操作漏洞,越权操作漏洞可以简单的总结为 「A 用户能看到或者操作 B 用户的隐私内容」,如果你的系统中还有权限控制就更加需要小心了。所以每一个请求都需要做 userid 的判断

+

以下是一段有漏洞的后端示意代码:

+
1
2
3
4
5
6
7
// ctx 为请求的 context 上下文
let msgId = ctx.params.msgId;

mysql.query(
'SELECT * FROM msg_table WHERE msg_id = ?',
[msgId]
);
+ +

以上代码是任何人都可以查询到任何用户的消息,只要有 msg_id 就可以,这就是比较典型的越权漏洞

+

越权操作漏洞防御

如下这么改进一下:

+
1
2
3
4
5
6
7
8
// ctx 为请求的 context 上下文
let msgId = ctx.params.msgId;
let userId = ctx.session.userId; // 从会话中取出当前登陆的 userId

mysql.query(
'SELECT * FROM msg_table WHERE msg_id = ? AND user_id = ?',
[msgId, userId]
);
+

嗯,大概就是这个意思,如果有更严格的权限控制,那在每个请求中凡是涉及到数据库的操作都需要先进行严格的验证,并且在设计数据库表的时候需要考虑进 userId 的账号关联以及权限关联。

+

目录遍历漏洞

目录遍历漏洞指通过在 URL 或参数中构造 .././ 和类似的跨父目录字符串的 ASCII 编码、unicode 编码等,完成目录跳转,读取操作系统各个目录下的敏感文件,也可以称作「任意文件读取漏洞」。

+

目录遍历漏洞原理:程序没有充分过滤用户输入的 ../ 之类的目录跳转符,导致用户可以通过提交目录跳转来遍历服务器上的任意文件。使用多个.. 符号,不断向上跳转,最终停留在根 /,通过绝对路径去读取任意文件。

+

目录遍历漏洞几个示例和测试,一般构造 URL 然后使用浏览器直接访问,或者使用 Web 漏洞扫描工具检测,当然也可以自写程序测试。

+
1
2
3
4
5
6
7
8
9
http://somehost.com/../../../../../../../../../etc/passwd
http://somehost.com/some/path?file=../../Windows/system.ini

# 借助 %00 空字符截断是一个比较经典的攻击手法
http://somehost.com/some/path?file=../../Windows/system.ini%00.js

# 使用了 IIS 的脚本目录来移动目录并执行指令
http://somehost.com/scripts/..%5c../Windows/System32/cmd.exe?/c+dir+c:\

+

目录遍历漏洞防御

方法就是需要对 URL 或者参数进行 ../,./ 等字符的转义过滤。

+

物理路径泄漏

物理路径泄露属于低风险等级缺陷,它的危害一般被描述为「攻击者可以利用此漏洞得到信息,来对系统进一步地攻击」,通常都是系统报错 500 的错误信息直接返回到页面可见导致的漏洞。得到物理路径有些时候它能给攻击者带来一些有用的信息,比如说:可以大致了解系统的文件目录结构;可以看出系统所使用的第三方软件;也说不定会得到一个合法的用户名(因为很多人把自己的用户名作为网站的目录名)。

+

物理路径泄漏防御

防止这种泄漏的方法就是做好后端程序的出错处理,定制特殊的 500 报错页面。

+

源码暴露漏洞

和物理路径泄露类似,就是攻击者可以通过请求直接获取到你站点的后端源代码,然后就可以对系统进一步研究攻击。那么导致源代码暴露的原因是什么呢?基本上就是发生在服务器配置上了,服务器可以设置哪些路径的文件才可以被直接访问的,这里给一个 koa 服务起的例子,正常的 koa 服务器可以通过 koa-static 中间件去指定静态资源的目录,好让静态资源可以通过路径的路由访问。比如你的系统源代码目录是这样的:

+
1
2
3
4
5
6
|- project
|- src
|- static
|- ...
|- server.js

+ +

你想要将 static 的文件夹配成静态资源目录,你应该会在 server.js 做如下配置:

+
1
2
3
4
5
6
const Koa = require('koa');
const serve = require('koa-static');
const app = new Koa();

app.use(serve(__dirname + '/project/static'));

+ +

但是如果配错了静态资源的目录,可能就出大事了,比如:

+
1
2
3
// ...
app.use(serve(__dirname + '/project'));

+ +

这样所有的源代码都可以通过路由访问到了,所有的服务器都提供了静态资源机制,所以在通过服务器配置静态资源目录和路径的时候,一定要注意检验,不然很可能产生漏洞。

+

最后,希望 Web 开发者们能够管理好自己的代码隐私,注意代码安全问题,比如不要将产品的含有敏感信息的代码放到第三方外部站点或者暴露给外部用户,尤其是前端代码,私钥类似的保密性的东西不要直接输出在代码里或者页面中。也许还有很多值得注意的点,但是归根结底还是绷住安全那根弦,对待每一行代码都要多多推敲。

+

转载自:https://zoumiaojiang.com/article/common-web-security/

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/15/index.html b/ja/page/15/index.html new file mode 100644 index 0000000000..5e821a6315 --- /dev/null +++ b/ja/page/15/index.html @@ -0,0 +1,531 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

架构实践

架构设计内容一览

应用架构设计

    +
  • Transaction执行控制(在线/批处理路径和控制)
  • +
  • Session控制
  • +
  • 死锁控制
  • +
  • 接口处理流程(适配器设置/文件备份)
  • +
  • 命名规范(实例/SID等)
  • +
  • 中间件参数 (初始化参数等)
  • +
  • 认证(处理流程/错误处理)
  • +
  • 帐户(用户ID体系/权限管理/申请方式)
  • +
+

开发架构设计

    +
  • 系统景观(各种环境名称、利用方法等)
  • +
  • 转运路线(转运路线/转运工具/承认方式)
  • +
  • 开发账户(用户ID体系/权限管理/申请方式)
  • +
  • 开发终端设置(在线批处理开发工具/目录等)
  • +
  • 开发资源管理
  • +
  • 表单、协作工具的利用方法(表单/接口ID定义规则)
  • +
  • 开发和验证备份(处理流程/文件清除/周期)
  • +
  • 数据屏蔽(処理概要/屏蔽利用规则/対象/开发环境搭建方法)
  • +
+

运维架构设计

    +
  • 监测规范(方式/周期/対象)
  • +
  • 审计和错误日志(日志级别/日志保留时效/删除时效)
  • +
  • 生产环境和调研环境的备份(处理流程/删除时效/周期)
  • +
  • 加密处理(安装位置、加密规则、记录水平加密方法)
  • +
  • 云安全设定
  • +
  • 增加功能设计
  • +
+

基础设施

    +
  • 网络设计(虚拟网/子网/URL/负载均衡访问控制/白名单)
  • +
  • 云设计(实例/NFS/IAM)
  • +
  • 存储设计 (BlockVolume/FileStorage/ObjectStorage/数据配置/数据圧縮方法)
  • +
  • 时间DNS同步化(OCI設定)
  • +
  • OS参数设定
  • +
  • Docker参数设定
  • +
  • 高可用切换 (処理方式、设定参数考量)
  • +
  • 打补丁(打补丁/执行规范)
  • +
  • 虚拟桌面设定
  • +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/16/index.html b/ja/page/16/index.html new file mode 100644 index 0000000000..d301aafa20 --- /dev/null +++ b/ja/page/16/index.html @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

ngx_http_stub_status_module 是一个 Nginx 的内置 模块,可以提供 Nginx 的简单统计信息。默认情况下这个模块是不被编译进来的,所以在编译 Nginx 时要指定加载该模块:

+
1
--with-http_stub_status_module
+

查看是否安装

1
2
3
4
5
// 查看nginx版本及安装了那些模块
nginx -V

// 如果安装了的话会显示下面的信息
--with-http_stub_status_module
+ +

配置使用

nginx.conf配置文件中进行如下配置

+
1
2
3
location /ngx_status {
stub_status on; // 设置开启性能模块
}
+

通过浏览器访问 http://localhost/ngx_status 就能看到你的nginx服务性能监控画面了
image

+

解析

    +
  • 第一行:Active connections:活动连接数
  • +
  • 第二三行:Server accepts handled requests:(三个数字分别代表)总共处理的连接数,成功握手的连接数量,处理的请求数(正常情况下握手和连接数是相等的,表示没有丢失)
  • +
  • 第四行:
      +
    • Reading: Nginx 读取到客户端的Header信息数
    • +
    • Writing: Nginx 返回给客户端的Header信息数
    • +
    • Waiting: 开启keep-alive的情况下,这个值等于 active – (reading + writing),意思就是Nginx已经处理完成,正在等候下一次请求指令的驻留连接(在nginx开启了keep-alive,也就是长连接的情况下,客户端跟服务端建立了连接但是没有读写操作的空闲状态)
    • +
    +
  • +
+

使用场景

    +
  • 可以简单的用脚本做监控
  • +
  • 可以用开源工具,zabbix,prometheus等去采集nginx的统计信息,做监控和历史数据采集
  • +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/17/index.html b/ja/page/17/index.html new file mode 100644 index 0000000000..075acd64a0 --- /dev/null +++ b/ja/page/17/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

HexoGithub发布之后自定义域名的配置

进到你的博客发布所在仓库,如:

选择Settings,然后是pages,滚动页面找到Custom domain,将你自己的域名绑定进去保存一下就好了。

+

在你自己的域名商处用CNAME把你自己的域名指定到你自己的仓库page地址上比如我的就是lixuguang.github.io Public,这样你的GitHub Page就可以通过自己的域名进行访问了。

+

HexoGithub发布之后自定义域名会被清空的问题

使用 GitHub Page 功能将博客托管在了 GitHub 上,并配置 CNAME 将自己的域名解析了过去,但是发现一个问题,每次 hexo deploy 之后,custom domain 会被重置失效。

+

解决方案

Hexo 生成的博客的 source 目录下(注意这个是你的博客源码所在仓库,并不是发布的仓库)新建一个 CNAME 文件,然后在这个文件中填入你的域名,这样就不会每次发布之后,GitHub Page 里的 custom domain 都被重置掉啦。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/18/index.html b/ja/page/18/index.html new file mode 100644 index 0000000000..4d72844796 --- /dev/null +++ b/ja/page/18/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Nginx指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#启动Nginx
start nginx

#重启Nginx
nginx -s reopen

#重新加载Nginx配置文件,然后以优雅的方式重启Nginx
nginx -s reload

#强制停止Nginx服务
nginx -s stop

#优雅地停止Nginx服务(即处理完所有请求后再停止服务)
nginx -s quit

#检测配置文件是否有语法错误,然后退出
nginx -t

#显示版本信息并退出
nginx -v

#显示版本和配置选项信息,然后退出
nginx -V

#检测配置文件是否有语法错误,然后退出
nginx -t

#检测配置文件是否有语法错误,转储并退出
nginx -T

#在检测配置文件期间屏蔽非错误信息
nginx -q

#打开帮助信息
nginx -?,-h

#设置前缀路径(默认是:/usr/share/nginx/)
nginx -p prefix

#设置配置文件(默认是:/etc/nginx/nginx.conf)
nginx -c filename

#设置配置文件外的全局指令
nginx -g directives
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/19/index.html b/ja/page/19/index.html new file mode 100644 index 0000000000..070afe7bbb --- /dev/null +++ b/ja/page/19/index.html @@ -0,0 +1,545 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

Docker 命令

    +
  • 帮助命令
    docker –help
  • +
+

镜像操作

    +
  • 搜索镜像
    docker search hello-world

    +
  • +
  • 下载镜像
    docker pull hello-world

    +
  • +
  • 查看本地已下载所有镜像
    docker images

    +
  • +
  • 查看镜像历史
    docker history hello-world

    +
  • +
  • 备份镜像
    docker tag hello-word:last hello-world:v2

    +
  • +
  • 删除镜像
    docker rmi hello-word:last

    +
  • +
  • 删除未使用过的镜像
    docker image prune

    +
  • +
  • 导出镜像
    docker save -o hello-world:last.tar hello-world:last

    +
  • +
  • 导入镜像
    docker load -i hello-world:last.tar

    +
  • +
  • 查看镜像信息
    docker image inspact nginx

    +
  • +
+

容器操作

    +
  • 查看所有容器 [-q 编号] [-a active 启动的容器]
    docker ps [-a] [-q]

    +
  • +
  • 启动容器 [-d 后台启动]
    docker run -d –name nginx1 nginx:last

    +
  • +
  • 停止容器
    docker stop nginx1

    +
  • +
  • 启动容器()
    docker start nginx1

    +
  • +
  • 删除容器
    docker rm nginx1

    +
  • +
  • 批量删除运行中容器
    docker rm $(docker ps -q) -f

    +
  • +
  • 创建容器并进入
    docker run -it –name nginx1 nginx:last /bin/bash

    +
  • +
  • 退出容器
    exit

    +
  • +
  • 进入容器
    docker exec -it nginx1 /bin/bash

    +
  • +
  • 通过容器创建镜像
    docker commit -m ‘laowang’ nginx1 nginx:v1

    +
  • +
  • 查看容器信息
    docker container inspact nginx1

    +
  • +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/2/index.html b/ja/page/2/index.html new file mode 100644 index 0000000000..d078223394 --- /dev/null +++ b/ja/page/2/index.html @@ -0,0 +1,477 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

DockerCompose安装

1
2
3
4
5
DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
mkdir -p $DOCKER_CONFIG/cli-plugins
sudo curl -SL https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-x86_64 -o /usr/bin/docker-compose
sudo chmod +x /usr/bin/docker-compose
sudo docker-compose version
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/20/index.html b/ja/page/20/index.html new file mode 100644 index 0000000000..d6b58d3509 --- /dev/null +++ b/ja/page/20/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

Docker网络模式

Docker常见的两种网络模式

+
    +
  • host网络模式:创建的容器和宿主机共享同一个网卡
  • +
  • bridge网络模式:桥接模式,只要使用桥接模式创建容器网段一样(类似于172.16.1.1,172.16.1.2)有点像是子局域网
  • +
+

利用network命令管理网络模式

    +
  • 查看网络模式
    docker network ls
  • +
  • 创建网络模式
    docker network create –drive bridge bridge_test
  • +
  • 通过network断网
    docker network disconnet bridge nginx5
  • +
  • 通过network联网
    docker network connect bridge nginx5
  • +
  • 删除网络模式
    docker network rm bridge_test
  • +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/21/index.html b/ja/page/21/index.html new file mode 100644 index 0000000000..e784277f41 --- /dev/null +++ b/ja/page/21/index.html @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

Docker容器端口映射

常见容器服务需要做端口映射,这里以nginx为例进行举例

+

启动一个nginx容器

docker run -itd –name nginx1 -P nginx:latest #随机端口
docker run -itd –name nginx2 -p 6001:80 nginx:latest #指定端口
docker run -itd –name nginx3 -p 172.16.10.1:6002:80 nginx:latest #指定IP和端口

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/22/index.html b/ja/page/22/index.html new file mode 100644 index 0000000000..5e7153b1c2 --- /dev/null +++ b/ja/page/22/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

Docker数据卷

宿主机与容器进行数据交互,共享宿主机与容器之间的数据

+

创建数据卷关联

docker run -it –name my_ubuntu1 -v ~ /Desktop/test:/test /bin/bash

+

利用系统方法操作数据卷

    +
  • 查 docker数据卷
    docker volume ls

    +
  • +
  • 创建数据卷
    docker volume create volname

    +
  • +
  • 共享
    docker run -it –name my_ubuntu2 -v volname:/home ubuntu:latest /bin/bash

    +
  • +
+

数据卷容器使用

可以通过数据卷容器,创建新的容器,并将多个容器绑定在一起

+

docker run -itd –name my_ubuntu3 –volumes-from my_ubuntu2 ubuntu:latest

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/23/index.html b/ja/page/23/index.html new file mode 100644 index 0000000000..d3b7dece50 --- /dev/null +++ b/ja/page/23/index.html @@ -0,0 +1,613 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

DockerFile

原则与建议

    +
  • 容器轻量化。从镜像中产生的容器应该尽量轻量化,能在足够短的时间内停止、销毁、重新生成并替换原来的容器。
  • +
  • 使用 .gitignore。在大部分情况下,Dockerfile 会和构建所需的文件放在同一个目录中,为了提高构建的性能,应该使用 .gitignore 来过滤掉不需要的文件和目录。
  • +
  • 为了减少镜像的大小,减少依赖,仅安装需要的软件包。
  • +
  • 一个容器只做一件事。解耦复杂的应用,分成多个容器,而不是所有东西都放在一个容器内运行。如一个 Python Web 应用,可能需要 Server、DB、Cache、MQ、Log 等几个容器。一个更加极端的说法:One process per container。
  • +
  • 减少镜像的图层。不要多个 Label、ENV 等标签。
  • +
  • 对续行的参数按照字母表排序,特别是使用apt-get install -y安装包的时候。
  • +
  • 使用构建缓存。如果不想使用缓存,可以在构建的时候使用参数–no-cache=true来强制重新生成中间镜像。
  • +
+

常用命令

FROM

FROM 指令用于指定其后构建新镜像所使用的基础镜像

+
1
2
3
FROM <image>
FROM <image>:<tag>
FROM <image>:<digest>
+

FROM 必须 是 Dockerfile 中第一条非注释命令
在一个 Dockerfile 文件中创建多个镜像时,FROM 可以多次出现。只需在每个新命令 FROM 之前,记录提交上次的镜像 ID。
tag 或 digest 是可选的,如果不使用这两个值时,会使用 latest 版本的基础镜像

+

RUN

在镜像的构建过程中执行特定的命令,并生成一个中间镜像

+
1
2
3
4
#shell格式
RUN <command>
#exec格式
RUN ["executable", "param1", "param2"]
+
    +
  • RUN 命令将在当前 image 中执行任意合法命令并提交执行结果。命令执行提交后,就会自动执行 Dockerfile 中的下一个指令。
  • +
  • 层级 RUN 指令和生成提交是符合 Docker 核心理念的做法。它允许像版本控制那样,在任意一个点,对 image 镜像进行定制化构建。
  • +
  • RUN 指令创建的中间镜像会被缓存,并会在下次构建中使用。如果不想使用这些缓存镜像,可以在构建时指定 ######no###cache 参数,如:docker build ######no###cache。
  • +
+

FROM scratch

scratch 是一个空的虚拟的镜像源文件,因为需要的依赖都存在不需要基础镜像,从而减小体积。

+

COPY

复制文件

+
1
2
COPY <源路径>... <目标路径>
COPY ["<源路径1>",... "<目标路径>"]
+

和 RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的<目标路径>位置。比如:

+
1
COPY package.json /usr/src/app/
+ +

<源路径>可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,如:

+
1
2
COPY hom* /mydir/
COPY hom?.txt /mydir/
+ +

<目标路径>可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

+

此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。

+

ADD

更高级的复制文件
ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。比如<源路径>可以是一个 URL,这种情况下,Docker 引擎会试图去下载这个链接的文件放到<目标路径>去。

+

在构建镜像时,复制上下文中的文件到镜像内,格式:

+
1
2
ADD <源路径>... <目标路径>
ADD ["<源路径>",... "<目标路径>"]
+

注意
如果 docker 发现文件内容被改变,则接下来的指令都不会再使用缓存。关于复制文件时需要处理的/,基本跟正常的 copy 一致

+

ENV

设置环境变量
格式有两种:

+
1
2
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
+

这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。

+
1
2
ENV VERSION=1.0 DEBUG=on \
NAME="Happy Feet"
+

这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。

+

EXPOSE

为构建的镜像设置监听端口,使容器在运行时监听。
格式:

+
1
EXPOSE <port> [<port>...]
+

EXPOSE 指令并不会让容器监听 host 的端口,如果需要,需要在 docker run 时使用 -p、-P 参数来发布容器端口到 host 的某个端口上。

+

VOLUME

定义匿名卷
VOLUME用于创建挂载点,即向基于所构建镜像创始的容器添加卷:

+
1
VOLUME ["/data"]
+

一个卷可以存在于一个或多个容器的指定目录,该目录可以绕过联合文件系统,并具有以下功能:

+
    +
  • 卷可以容器间共享和重用
  • +
  • 容器并不一定要和其它容器共享卷
  • +
  • 修改卷后会立即生效
  • +
  • 对卷的修改不会对镜像产生影响
  • +
  • 卷会一直存在,直到没有任何容器在使用它
    VOLUME 让我们可以将源代码、数据或其它内容添加到镜像中,而又不并提交到镜像中,并使我们可以多个容器间共享这些内容。
  • +
+

WORKDIR

WORKDIR用于在容器内设置一个工作目录:

+
1
WORKDIR /path/to/workdir
+

通过WORKDIR设置工作目录后,Dockerfile 中其后的命令 RUN、CMD、ENTRYPOINT、ADD、COPY 等命令都会在该目录下执行。
如,使用WORKDIR设置工作目录:

+
1
2
3
4
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
+

在以上示例中,pwd 最终将会在 /a/b/c 目录中执行。在使用 docker run 运行容器时,可以通过-w参数覆盖构建时所设置的工作目录。

+

USER

指定当前用户
USER 用于指定运行镜像所使用的用户:

+
1
USER daemon
+

使用USER指定用户时,可以使用用户名、UID 或 GID,或是两者的组合。以下都是合法的指定试:

+
1
2
3
4
5
6
USER user
USER user:group
USER uid
USER uid:gid
USER user:gid
USER uid:group
+

使用USER指定用户后,Dockerfile 中其后的命令 RUN、CMD、ENTRYPOINT 都将使用该用户。镜像构建完成后,通过 docker run 运行容器时,可以通过 -u 参数来覆盖所指定的用户。

+

CMD

CMD用于指定在容器启动时所要执行的命令。CMD 有以下三种格式:

+
1
2
3
CMD ["executable","param1","param2"]
CMD ["param1","param2"]
CMD command param1 param2
+

省略可执行文件的 exec 格式,这种写法使 CMD 中的参数当做 ENTRYPOINT 的默认参数,此时 ENTRYPOINT 也应该是 exec 格式,具体与 ENTRYPOINT 的组合使用,参考 ENTRYPOINT。

+

注意
与 RUN 指令的区别:RUN 在构建的时候执行,并生成一个新的镜像,CMD 在容器运行的时候执行,在构建时不进行任何操作。

+

ENTRYPOINT

ENTRYPOINT 用于给容器配置一个可执行程序。也就是说,每次使用镜像创建容器时,通过 ENTRYPOINT 指定的程序都会被设置为默认程序。ENTRYPOINT 有以下两种形式:

+
1
2
3
ENTRYPOINT ["executable", "param1", "param2"]
ENTRYPOINT command param1 param2
ENTRYPOINT 与 CMD 非常类似,不同的是通过docker run执行的命令不会覆盖 ENTRYPOINT,而docker run命令中指定的任何参数,都会被当做参数再次传递给 ENTRYPOINT。Dockerfile 中只允许有一个 ENTRYPOINT 命令,多指定时会覆盖前面的设置,而只执行最后的 ENTRYPOINT 指令。
+

docker run运行容器时指定的参数都会被传递给 ENTRYPOINT ,且会覆盖 CMD 命令指定的参数。如,执行docker run -d时,-d 参数将被传递给入口点。

+

也可以通过docker run –entrypoint重写 ENTRYPOINT 入口点。如:可以像下面这样指定一个容器执行程序:

+
1
ENTRYPOINT ["/usr/bin/nginx"]
+

完整构建代码:

+
1
2
3
4
5
6
7
8
9
# Version: 0.0.3
FROM ubuntu:16.04
MAINTAINER 何民三 "cn.liuht@gmail.com"
RUN apt-get update
RUN apt-get install -y nginx
RUN echo 'Hello World, 我是个容器' \
> /var/www/html/index.html
ENTRYPOINT ["/usr/sbin/nginx"]
EXPOSE 80
+

使用docker build构建镜像,并将镜像指定为 itbilu/test:

+
1
docker build -t="itbilu/test" .
+

构建完成后,使用itbilu/test启动一个容器:

+
1
docker run -i -t  itbilu/test -g "daemon off;"
+

在运行容器时,我们使用了 -g “daemon off;”,这个参数将会被传递给 ENTRYPOINT,最终在容器中执行的命令为 /usr/sbin/nginx -g “daemon off;”。

+

LABEL

LABEL用于为镜像添加元数据,元数以键值对的形式指定:

+
1
LABEL <key>=<value> <key>=<value> <key>=<value> ...
+

使用LABEL指定元数据时,一条LABEL指定可以指定一或多条元数据,指定多条元数据时不同元数据之间通过空格分隔。推荐将所有的元数据通过一条LABEL指令指定,以免生成过多的中间镜像。
如,通过LABEL指定一些元数据:

+
1
LABEL version="1.0" description="这是一个Web服务器" by="IT笔录"
+

指定后可以通过docker inspect查看:

+
1
2
3
4
5
6
docker inspect itbilu/test
"Labels": {
"version": "1.0",
"description": "这是一个Web服务器",
"by": "IT笔录"
},
+ +

ARG

ARG用于指定传递给构建运行时的变量:

+
1
ARG <name>[=<default value>]
+

如,通过ARG指定两个变量:

+
1
2
ARG site
ARG build_user=IT笔录
+

以上我们指定了 site 和 build_user 两个变量,其中 build_user 指定了默认值。在使用 docker build 构建镜像时,可以通过 –build-arg = 参数来指定或重设置这些变量的值。

+
1
docker build --build-arg site=itiblu.com -t itbilu/test .
+

这样我们构建了 itbilu/test 镜像,其中site会被设置为 itbilu.com,由于没有指定 build_user,其值将是默认值 IT 笔录。

+

ONBUILD

ONBUILD用于设置镜像触发器:

+
1
ONBUILD [INSTRUCTION]
+

当所构建的镜像被用做其它镜像的基础镜像,该镜像中的触发器将会被钥触发。
如,当镜像被使用时,可能需要做一些处理:

+
1
2
3
4
[...]
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
[...]
+ +

STOPSIGNAL

STOPSIGNAL用于设置停止容器所要发送的系统调用信号:

+
1
STOPSIGNAL signal
+

所使用的信号必须是内核系统调用表中的合法的值,如:SIGKILL。

+

SHELL

SHELL用于设置执行命令(shell式)所使用的的默认 shell 类型:

+
1
SHELL ["executable", "parameters"]
+

SHELL在Windows环境下比较有用,Windows 下通常会有 cmd 和 powershell 两种 shell,可能还会有 sh。这时就可以通过 SHELL 来指定所使用的 shell 类型:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM microsoft/windowsservercore

# Executed as cmd /S /C echo default
RUN echo default

# Executed as cmd /S /C powershell -command Write-Host default
RUN powershell -command Write-Host default

# Executed as powershell -command Write-Host hello
SHELL ["powershell", "-command"]
RUN Write-Host hello

# Executed as cmd /S /C echo hello
SHELL ["cmd", "/S"", "/C"]
RUN echo hello
+ +

Dockerfile 示例

    +
  • 构建Nginx运行环境
  • +
  • 构建tomcat 环境
  • +
+

构建Nginx运行环境

Dockerfile文件

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#指定基础镜像
FROM sameersbn/ubuntu:14.04.20161014

#维护者信息
MAINTAINER sameer@damagehead.com

#设置环境
ENV RTMP_VERSION=1.1.10 \
NPS_VERSION=1.11.33.4 \
LIBAV_VERSION=11.8 \
NGINX_VERSION=1.10.1 \
NGINX_USER=www-data \
NGINX_SITECONF_DIR=/etc/nginx/sites-enabled \
NGINX_LOG_DIR=/var/log/nginx \
NGINX_TEMP_DIR=/var/lib/nginx \
NGINX_SETUP_DIR=/var/cache/nginx

#设置构建时变量,镜像建立完成后就失效
ARG BUILD_LIBAV=false
ARG WITH_DEBUG=false
ARG WITH_PAGESPEED=true
ARG WITH_RTMP=true

#复制本地文件到容器目录中
COPY setup/ ${NGINX_SETUP_DIR}/
RUN bash ${NGINX_SETUP_DIR}/install.sh

#复制本地配置文件到容器目录中
COPY nginx.conf /etc/nginx/nginx.conf
COPY entrypoint.sh /sbin/entrypoint.sh

#运行指令
RUN chmod 755 /sbin/entrypoint.sh

#允许指定的端口
EXPOSE 80/tcp 443/tcp 1935/tcp

#指定网站目录挂载点
VOLUME ["${NGINX_SITECONF_DIR}"]

ENTRYPOINT ["/sbin/entrypoint.sh"]
CMD ["/usr/sbin/nginx"]
+ +

构建Tomcat环境

Dockerfile文件

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 指定基于的基础镜像
FROM ubuntu:13.10

# 维护者信息
MAINTAINER zhangjiayang "zhangjiayang@sczq.com.cn"

# 镜像的指令操作
# 获取APT更新的资源列表
RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe"> /etc/apt/sources.list
# 更新软件
RUN apt-get update

# Install curl
RUN apt-get -y install curl

# Install JDK 7
RUN cd /tmp && curl -L 'http://download.oracle.com/otn-pub/java/jdk/7u65-b17/jdk-7u65-linux-x64.tar.gz' -H 'Cookie: oraclelicense=accept-securebackup-cookie; gpw_e24=Dockerfile' | tar -xz
RUN mkdir -p /usr/lib/jvm
RUN mv /tmp/jdk1.7.0_65/ /usr/lib/jvm/java-7-oracle/

# Set Oracle JDK 7 as default Java
RUN update-alternatives --install /usr/bin/java java /usr/lib/jvm/java-7-oracle/bin/java 300
RUN update-alternatives --install /usr/bin/javac javac /usr/lib/jvm/java-7-oracle/bin/javac 300

# 设置系统环境
ENV JAVA_HOME /usr/lib/jvm/java-7-oracle/

# Install tomcat7
RUN cd /tmp && curl -L 'http://archive.apache.org/dist/tomcat/tomcat-7/v7.0.8/bin/apache-tomcat-7.0.8.tar.gz' | tar -xz
RUN mv /tmp/apache-tomcat-7.0.8/ /opt/tomcat7/

ENV CATALINA_HOME /opt/tomcat7
ENV PATH $PATH:$CATALINA_HOME/bin

# 复件tomcat7.sh到容器中的目录
ADD tomcat7.sh /etc/init.d/tomcat7
RUN chmod 755 /etc/init.d/tomcat7

# Expose ports. 指定暴露的端口
EXPOSE 8080

# Define default command.
ENTRYPOINT service tomcat7 start && tail -f /opt/tomcat7/logs/catalina.out
+ +

tomcat7.sh命令文件

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export JAVA_HOME=/usr/lib/jvm/java-7-oracle/  
export TOMCAT_HOME=/opt/tomcat7

case $1 in
start)
sh $TOMCAT_HOME/bin/startup.sh
;;
stop)
sh $TOMCAT_HOME/bin/shutdown.sh
;;
restart)
sh $TOMCAT_HOME/bin/shutdown.sh
sh $TOMCAT_HOME/bin/startup.sh
;;
esac
exit 0
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/24/index.html b/ja/page/24/index.html new file mode 100644 index 0000000000..3a19673c96 --- /dev/null +++ b/ja/page/24/index.html @@ -0,0 +1,567 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

1、说明

项目是一个多人协同完成同一个目标的团队组织形式,在多人协作项目中,如果代码风格统一(前端代码规范和静态代码检查约束)、代码提交信息的说明准确规范(本章介绍),那么在项目开发过程及后期协作以及Bug处理时会更加方便。

+

在本文中,我将介绍大家如何利用工具及约定保证大家代码提交的统一性,从而提高大家的协同效率:

+
    +
  • commitlint: git 提交信息规范与验证
    +

    添加如ESLint的格式规范校验,规范comiit的格式,达到团队每个人的提交风格保持一直,保证提交信息的完整和准确性
    风格如下:

    +
    +
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
<type>(<scope>): <subject> // Header
// 空一行
<body> // 72
// 空一行
<footer> // 72

// 例
fix(登陆模块): 修复登录密码错误的提示

登录没有做友好型提示

修正后不存在此问题
+ +
    +
  • husky: 使git-hook更容易
    +

    husky继承了Git下所有的钩子,在触发钩子的时候,husky可以阻止不合法的commit,push等等。注意使用husky之前,必须先将代码放到git 仓库中,否则本地没有.git文件,就没有地方去继承钩子了。

    +
    +
  • +
  • standard-version: 自动生成CHANGELOG 并发布版本
  • +
+

2、git commit message 规范

commit message格式

1
2
3
4
5
类型(影响范围): 描述

问题描述

修复方式结果
+ + +

注意:
1.冒号后面有空格。
2.英文小括号。
3.正文和注脚前都要加空行。

+

type 类型

用于说明 commit 的类别,只允许使用下面13个标识。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
'feat', // feat:新增功能
'fix', // fix:bug 修复
'docs', // docs:文档更新
'style', // style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑)
'refactor', // refactor:重构代码(既没有新增功能,也没有修复 bug)
'test', // test:新增测试用例或是更新现有测试
'chore', // revert:回滚某个更早之前的提交
'revert', // build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交
'build', // build:打包生产环境代码
'ci', // ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交
'merge', // merge:分支合并 Merge branch ? of ?
'perf', // perf:性能, 体验优化
'chore' // chore:不属于以上类型的其他类型(构建过程或辅助工具的变动)
+ +

如果type为feat和fix,则该 commit 将肯定出现在 Change log 之中。

+

scope 影响范围

可根据项目组需要进行定制化设置,也可不做强制要求

+

subject 描述

subject是 commit 目的的简短描述,不超过50个字符,且结尾不加句号(.)。

+

body 详细描述

body 部分是对本次 commit 的详细描述,可以分成多行。下面是一个范例。

+
1
2
3
此次提交内容包括如下信息:
- 登录为空校验
- 密码错误提示
+

Footer 部分只用于两种情况。

+
    +
  • 不兼容变动(改变解决方案)
  • +
  • 关闭 Issue(回复bug)
  • +
+

3、使用工具校验commit是否符合规范

commitlint

3.1 commitlint安装

1
2
// npm 安装
npm install --save-dev @commitlint/{cli,config-conventional}
+ +

3.2 生成commitlint.config.js配置文件

执行echo命令创建配置文件,也可以手动创建配置文件,或者从已有的配置文件进行拷贝。

+
1
2
// 项目根目录创建commitlint.config.js
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
+ + +

3.3 在commitlint.config.js制定提交message规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// type(scope?): subject

// body?

// footer?
module.exports = {
// 继承自默认规则
extends: ['@commitlint/config-conventional'],
// 这里写自定义规则
rules: {
// 等级 [0 1 2]: 0 不使用规则 1 警告 2 错误
// 启用范围 always|never: 总是或者永不.
// 规则值: 规则对应的值

// 头部(包含type、scope、subject)
'header-case': [0, 'always', 'lower-case'],
'header-full-stop': [0, 'never', '.'],
'header-max-length': [0, 'always', 72],
'header-min-length': [0, 'always', 0],

// 提交类型
'type-case': [0, 'never'],
'type-empty': [2, 'never'],
'type-max-length': [0, 'always', Infinity],
'type-min-length': [0, 'always', 0],
'type-enum': [
2,
'always',
[
'feat', // feat:新增功能
'fix', // fix:bug 修复
'docs', // docs:文档更新
'style', // style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑)
'refactor', // refactor:重构代码(既没有新增功能,也没有修复 bug)
'test', // test:新增测试用例或是更新现有测试
'revert', // revert:回滚某个更早之前的提交
'build', // build:build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交,打包生产环境代码
'ci', // ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交
'merge', // merge:分支合并 Merge branch ? of ?
'perf', // perf:性能, 体验优化
'chore' // chore:不属于以上类型的其他类型(构建过程或辅助工具的变动)
]
],

// 影响范围
'scope-case': [0, 'always', 'lower-case'], // 书写格式
'scope-max-length': [0, 'always', Infinity], // 最长
'scope-min-length': [0, 'always', 0], // 最短
'scope-empty': [2, 'never'], // 影响范围:为空|永不
'scope-enum': [0, 'always', ['a', 'b']], // 影响范围:给出范围但是不在给定的列表内

// 简介
'subject-empty': [2, 'never'], // 简介不能为空
'subject-full-stop': [0, 'never', '.'], // 简介结尾以.结束
'subject-case': [0, 'never'],
'subject-max-length': [0, 'always', Infinity],
'subject-min-length': [0, 'always', 1],

// 正文
'body-leading-blank': [2, 'always'],
'body-max-length': [0, 'always', Infinity],
'body-max-line-length': [0, 'always', Infinity],
'body-min-length': [0, 'always', 1],

// 注脚
'footer-leading-blank': [2, 'always'],
'footer-max-length': [0, 'always', Infinity],
'footer-max-line-length': [0, 'always', Infinity],
'footer-min-length': [0, 'always', 1],

// 其他
'references-empty': [0, 'never'],
'signed-off-by': [0, 'always', 'Signed-off-by']
}
}
+ +

上面我们就完成了commitlint的安装与提交规范的制定。检验commit message的最佳方式是结合git hook,所以需要配合Husky

+

husky

3.4 husky安装

1
npm install husky --save-dev
+

3.5 husky 配置

安装成功后需要在项目下的package.json中配置

+
1
2
3
4
5
6
7
8
9
10
11
// package.json
{
"husky": {
"hooks": { // husky的钩子
"pre-commit": "npm run lint" // 提交前进行代码静态校验
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS" // 进行提交信息格式校验
}
},
"scripts": {}
...
}
+ +

3.5 husky 执行测试

最后我们可以正常的git操作

+
1
2
git add .
git commit -m ""
+ +

git commit的时候会触发commlint。下面演示下不符合规范提交示例:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
C:\lixg\git>git commit -m "thunisoft: abcde"

husky > npm run -s commitmsg (node v8.2.1)

⧗ input:
thunisoft(all): abcde

✖ type must be one of [feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert] [type-enum]
✖ found 1 problems, 0 warnings

husky > commit-msg hook failed (add --no-verify to bypass)

C:\lixg\git>
+ +

上面的提交看反馈消息得知是type格式没有符合限制的提交类型列表,所以提交失败,虾米啊我们把type改为feat再试一下

+
1
2
3
4
5
6
7
8
9
10
11
C:\lixg\git>git commit -m "feat(all): 新功能"

husky > npm run -s commitmsg (node v8.2.1)

⧗ input: feat: 新功能
✔ found 0 problems, 0 warnings

[develop 19dfhe] feat: 新功能
1 file changed, 1 insertion(+)

C:\lixg\git>
+ +

修改后格式符合规范,提交成功。

+

参考来源

+

版权声明

Copyright by lixuguang
未经授权,严禁转载。如需转载,请联系作者

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/25/index.html b/ja/page/25/index.html new file mode 100644 index 0000000000..5616624613 --- /dev/null +++ b/ja/page/25/index.html @@ -0,0 +1,736 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

1. URI

URI 表示资源,资源一般对应服务器端领域模型中的实体类。

+

URI规范

    +
  1. 不用大写;
  2. +
  3. 用中杠-不用下杠_;
  4. +
  5. 参数列表要encode;
  6. +
  7. URI中的名词表示资源集合,使用复数形式。
  8. +
+

资源集合 vs 单个资源

URI表示资源的两种方式:资源集合、单个资源。

+

资源集合:

+
1
2
/zoos //所有动物园
/zoos/1/animals //id为1的动物园中的所有动物
+

单个资源:

+
1
2
/zoos/1 //id为1的动物园
/zoos/1;2;3 //id为1,2,3的动物园
+

避免层级过深的URI

/在url中表达层级,用于按实体关联关系进行对象导航,一般根据id导航。

+

过深的导航容易导致url膨胀,不易维护,如 GET /zoos/1/areas/3/animals/4,尽量使用查询参数代替路径中的实体导航,如GET /animals?zoo=1&area=3

+

对Composite资源的访问

服务器端的组合实体必须在uri中通过父实体的id导航访问。

+
+

组合实体不是first-class的实体,它的生命周期完全依赖父实体,无法独立存在,在实现上通常是对数据库表中某些列的抽象,不直接对应表,也无id。一个常见的例子是 User — Address,Address是对User表中zipCode/country/city三个字段的简单抽象,无法独立于User存在。必须通过User索引到Address:GET /user/1/addresses

+
+

2. Request

HTTP方法

通过标准HTTP方法对资源CRUD:

+

GET:查询

+
1
2
3
GET /zoos
GET /zoos/1
GET /zoos/1/employees
+ + +

POST:创建单个资源。POST一般向“资源集合”型uri发起

+
1
2
POST /animals  //新增动物
POST /zoos/1/employees //为id为1的动物园雇佣员工
+ +

PUT:更新单个资源(全量),客户端提供完整的更新后的资源。与之对应的是 PATCH,PATCH 负责部分更新,客户端提供要更新的那些字段。PUT/PATCH一般向“单个资源”型uri发

+
1
2
PUT /animals/1
PUT /zoos/1
+ +

DELETE:删除

+
1
2
3
DELETE /zoos/1/employees/2
DELETE /zoos/1/employees/2;4;5
DELETE /zoos/1/animals //删除id为1的动物园内的所有动物
+

HEAD / OPTION 用的不多,就不多解释了。

+

安全性和幂等性

    +
  1. 安全性:不会改变资源状态,可以理解为只读的;
  2. +
  3. 幂等性:执行1次和执行N次,对资源状态改变的效果是等价的。
  4. +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
.安全性幂等性
GET
POST××
PUT×
DELETE×
+

安全性和幂等性均不保证反复请求能拿到相同的response。以 DELETE 为例,第一次DELETE返回200表示删除成功,第二次返回404提示资源不存在,这是允许的。

+

复杂查询

查询可以捎带以下参数:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
.示例备注
过滤条件type=1&age=16允许一定的uri冗余,如/zoos/1与/zoos?id=1
排序?sort=age,desc
投影?whitelist=id,name,email
分页?limit=10&offset=3
+

Bookmarker

经常使用的、复杂的查询标签化,降低维护成本。

+

如:

+
1
2
GET /trades?status=closed&sort=created,desc

+

快捷方式:

+
1
2
3
GET /trades#recently-closed
// 或者
GET /trades/recently-closed
+ +

Format

只用以下常见的3种body format:

+
    +
  1. Content-Type: application/json
  2. +
+
1
2
3
4
5
6
7
8
9
10
POST /v1/animal HTTP/1.1
Host: api.example.org
Accept: application/json
Content-Type: application/json
Content-Length: 24

{
"name": "Gir",
"animalType": "12"
}
+ +
    +
  1. Content-Type: application/x-www-form-urlencoded (浏览器POST表单用的格式)
  2. +
+
1
2
3
4
5
6
7
POST /login HTTP/1.1
Host: example.com
Content-Length: 31
Accept: text/html
Content-Type: application/x-www-form-urlencoded

username=root&password=Zion0101
+ +
    +
  1. Content-Type: multipart/form-data; boundary=—-RANDOM_jDMUxq4Ot5 (表单有文件上传时的格式)
  2. +
+

6. Response

    +
  1. 不要包装:
    response 的 body 直接就是数据,不要做多余的包装。
  2. +
+

错误示例

+
1
2
3
4
{
"success":true,
"data":{"id":1,"name":"xiaotuan"},
}
+

各HTTP方法成功处理后的数据格式:

+ + + + + + + + + + + + + + + + + + + + + + + +
·response 格式
GET单个对象、集合
POST新增成功的对象
PUT/PATCH更新成功的对象
DELETE
+
    +
  1. json格式的约定:

    +
      +
    1. 时间用长整形(毫秒数),客户端自己按需解析(moment.js)
    2. +
    3. 不传null字段
    4. +
    +
  2. +
+

分页response

1
2
3
4
{
"paging":{"limit":10,"offset":0,"total":729},
"data":[{},{},{}...]
}
+

7. 错误处理

    +
  1. 不要发生了错误但给2xx响应,客户端可能会缓存成功的http请求;
  2. +
  3. 正确设置http状态码,不要自定义;
  4. +
  5. Response body 提供
      +
    1. 错误的代码(日志/问题追查);
    2. +
    3. 错误的描述文本(展示给用户)。
    4. +
    +
  6. +
+

对第三点的实现稍微多说一点:

+

Java 服务器端一般用异常表示 RESTful API 的错误。API 可能抛出两类异常:业务异常非业务异常

+

业务异常由自己的业务代码抛出,表示一个用例的前置条件不满足、业务规则冲突等,比如参数校验不通过、权限校验失败。

+

非业务类异常表示不在预期内的问题,通常由类库、框架抛出,或由于自己的代码逻辑错误导致,比如数据库连接失败、空指针异常、除0错误等等。

+

业务类异常必须提供2种信息:

+
    +
  1. 如果抛出该类异常,HTTP 响应状态码应该设成什么;
  2. +
  3. 异常的文本描述;
  4. +
+

在Controller层使用统一的异常拦截器:

+
    +
  1. 设置 HTTP 响应状态码:对业务类异常,用它指定的 HTTP code;对非业务类异常,统一500;
  2. +
  3. Response Body 的错误码:异常类名
  4. +
  5. Response Body 的错误描述:对业务类异常,用它指定的错误文本;对非业务类异常,线上可以统一文案如“服务器端错误,请稍后再试”,开发或测试环境中用异常的 stacktrace,服务器端提供该行为的开关。
  6. +
+

常用的http状态码及使用场景:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
状态码使用场景
400 bad request常用在参数校验
401 unauthorized未经验证的用户,常见于未登录。如果经过验证后依然没权限,应该 403(即 authentication 和 authorization 的区别)。
403 forbidden无权限
404 not found资源不存在
500 internal server error非业务类异常
503 service unavaliable由容器抛出,自己的代码不要抛这个异常
+

8. 服务型资源

除了资源简单的CRUD,服务器端经常还会提供其他服务,这些服务无法直接用上面提到的URI映射。如:

+
    +
  1. 按关键字搜索;
  2. +
  3. 计算地球上两点间的距离;
  4. +
  5. 批量向用户推送消息;
  6. +
+

可以把这些服务看成资源,计算的结果是资源的presentation,按服务属性选择合适的HTTP方法。

+

例:

+
1
2
3
4
GET /search?q=filter?category=file  搜索
GET /distance-calc?lats=47.480&lngs=-122.389&late=37.108&lnge=-122.448
POST /batch-publish-msg
[{"from":0,"to":1,"text":"abc"},{},{}...]
+

9. 异步任务

对耗时的异步任务,服务器端接受客户端传递的参数后,应返回创建成功的任务资源,其中包含了任务的执行状态。客户端可以轮训该任务获得最新的执行进度。

+
1
2
3
4
5
6
7
8
9
// 提交任务:
POST /batch-publish-msg
[{"from":0,"to":1,"text":"abc"},{},{}...]

// 返回:
{"taskId":3,"createBy":"Anonymous","status":"running"}

GET /task/3
{"taskId":3,"createBy":"Anonymous","status":"success"}
+ +

如果任务的执行状态包括较多信息,可以把“执行状态”抽象成组合资源,客户端查询该状态资源了解任务的执行情况。

+
1
2
3
4
5
6
7
8
9
// 提交任务:
POST /batch-publish-msg
[{"from":0,"to":1,"text":"abc"},{},{}...]

// 返回:
{"taskId":3,"createBy":"Anonymous"}

GET /task/3/status
{"progress":"50%","total":18,"success":8,"fail":1}
+ +

10. API的演进

版本

常见的三种方式:

+
    +
  1. 在uri中放版本信息:GET /v1/users/1
  2. +
  3. Accept Header:Accept: application/json+v1
  4. +
  5. 自定义 Header:X-Api-Version: 1
  6. +
+

用第一种,虽然没有那么优雅,但最明显最方便。

+

URI失效

随着系统发展,总有一些API失效或者迁移,对失效的API,返回404 not found 或 410 gone;对迁移的API,返回 301 重定向。

+

11. 安全

这个不熟,接触到的时候再说。

+

参考文档

    +
  • < RESTful Web Services Cookbook >
  • +
  • Consumer-Centric API Design
  • +
  • RESTful Best Practices
  • +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/26/index.html b/ja/page/26/index.html new file mode 100644 index 0000000000..c26f7a6bdc --- /dev/null +++ b/ja/page/26/index.html @@ -0,0 +1,645 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

1.关于Code Review

1.1 Code Review的目的

Code Review是一种用来确认方案设计和代码实现的质量保证机制,通过这个机制我们可以对代码、测试过程和注释进行检查。

+

Code Review主要用来在软件工程过程中改进代码质量,通过Code Review可以达到如下目的目的:

+
    +
  1. 在项目早期就能够发现代码中的BUG
  2. +
  3. 帮助初级开发人员学习高级开发人员的经验,达到知识共享
  4. +
  5. 避免开发人员犯一些很常见,很普通的错误
  6. +
  7. 保证项目组人员的良好沟通
  8. +
  9. 项目或产品的代码更容易维护
  10. +
+

1.2 Code Review的前提

进入Code Review需要检查的条件如下:

+
    +
  1. Code Review人员是否理解了Code Review的概念和Code Review将做什么
    如果做Code Review的人员不能理解Code Review对项目成败和代码质量的重要程度,他们的做法可能就会是应付了事。
  2. +
  3. 代码是否已经正确的build,build的目的使得代码已经不存在基本语法错误
    我们总不希望高级开发人员或是主管将时间浪费在检查连编译都通不过的代码上吧。
  4. +
  5. 代码执行时功能是否正确
    Code Review人员也不负责检查代码的功能是否正确,也就是说,需要复查的代码必须由开发人员或质量人员负责该代码的功能的正确性。
  6. +
  7. Review人员是否理解了代码
    做复查的人员需要对该代码有一个基本的了解,其功能是什么,是拿一方面的代码,涉及到数据库或是通讯,这样才能采取针对性的检查
  8. +
  9. 开发人员是否对代码做了单元测试
    这一点也是为了保证Code Review前一些语法和功能问题已经得到解决,Code Review人员可以将精力集中在代码的质量上。
  10. +
+

1.3 Code Review需要做什么

Code Review主要检查代码中是否存在以下方面问题:

+
    +
  • 代码的一致性
  • +
  • 编码风格
  • +
  • 代码的安全问题
  • +
  • 代码冗余
  • +
  • 是否正确设计以满足需求(性能、功能)
  • +
  • 等等
  • +
+

1.3.1 完整性检查(Completeness)

    +
  • 代码是否完全实现了设计文档中提出的功能需求
  • +
  • 代码是否已按照设计文档进行了集成和Debug
  • +
  • 代码是否已创建了需要的数据库,包括正确的初始化数据
  • +
  • 代码中是否存在任何没有定义或没有引用到的变量、常数或数据类型
  • +
+

1.3.2 一致性检查(Consistency)

    +
  • 代码的逻辑是否符合设计文档
  • +
  • 代码中使用的格式、符号、结构等风格是否保持一致
  • +
+

1.3.3 正确性检查(Correctness)

    +
  • 代码是否符合制定的标准
  • +
  • 所有的变量都被正确定义和使用
  • +
  • 所有的注释都是准确的
  • +
  • 所有的程序调用都使用了正确的参数个数
  • +
+

1.3.4 可修改性检查(Modifiability)

    +
  • 代码涉及到的常量是否易于修改(如使用配置、定义为类常量、使用专门的常量类等)
  • +
  • 代码中是否包含了交叉说明或数据字典,以描述程序是如何对变量和常量进行访问的
  • +
  • 代码是否只有一个出口和一个入口(严重的异常处理除外)
  • +
+

1.3.5 可预测性检查(Predictability)

    +
  • 代码所用的开发语言是否具有定义良好的语法和语义
  • +
  • 是否代码避免了依赖于开发语言缺省提供的功能
  • +
  • 代码是否无意中陷入了死循环
  • +
  • 代码是否是否避免了无穷递归
  • +
+

1.3.6 健壮性检查(Robustness)

    +
  • 代码是否采取措施避免运行时错误
      +
    • 数组边界溢出
    • +
    • 被零除
    • +
    • 值越界
    • +
    • 堆栈溢出
    • +
    • +
    +
  • +
+

1.3.7 结构性检查(Structuredness)

    +
  • 程序的每个功能是否都作为一个可辩识的代码块存在
    循环是否只有一个入口
  • +
+

1.3.8 可追溯性检查(Traceability)

    +
  • 代码是否对每个程序进行了唯一标识
  • +
  • 是否有一个交叉引用的框架可以用来在代码和开发文档之间相互对应
  • +
  • 代码是否包括一个修订历史记录,记录中对 代码的修改和原因都有记录
  • +
  • 是否所有的安全功能都有标识
  • +
+

1.3.9 可理解性检查(Understandability)

    +
  • 注释是否足够清晰的描述每个子程序
  • +
  • 是否使用到不明确或不必要的复杂代码,它们是否被清楚的注释
  • +
  • 使用一些统一的格式化技巧(如缩进、空白等)用来增强代码的清晰度
  • +
  • 是否在定义命名规则时采用了便于记忆,反映类型等方法
  • +
  • 每个变量都定义了合法的取值范围
  • +
  • 代码中的算法是否符合开发文档中描述的数学模型
  • +
+

1.3.10 可验证性检查(Verifiability)

代码中的实现技术是否便于测试

+

1.4 Code Review的步骤

这些是我在平时工作中的经验总结,目前也是按照这个步骤在做。

+
    +
  1. 代码编写者和代码审核者坐在一起,由代码编写者按照UC依次讲解自己负责的代码和相关逻辑,从Web层->DAO层;
  2. +
  3. 代码审核者在此过程中可以随时提出自己的疑问,同时积极发现隐藏的bug;对这些bug记录在案。
  4. +
  5. 代码讲解完毕后,代码审核者给自己安排几个小时再对代码审核一遍。
    代码需要一行一行静下心看。同时代码又要全面的看,以确保代码整体上设计优良。
  6. +
  7. 代码审核者根据审核的结果编写“代码审核报告”,“审核报告”中记录发现的问题及修改建议,然后把“审核报告”发送给相关人员。
  8. +
  9. 代码编写者根据“代码审核报告”给出的修改意见,修改好代码,有不清楚的地方可积极向代码审核者提出。
  10. +
  11. 代码编写者 bug fix完毕之后给出反馈。
  12. +
  13. 代码审核者把Code Review中发现的有价值的问题更新到”代码审核规范”的文档中,对于特别值得提醒的问题可群发email给所有技术人员。
  14. +
+

提示
Code Review必备的文档:

+
    +
  • “代码审核规范”文档:记录代码应该遵循的标准。
    +

    代码审核者根据这些标准来Code Review代码,同时在Code Review过程中不断完善该文档。

    +
    +
  • +
+

2.Code Reivew的执行

一个标准的Code Reivew活动应该分为三个阶段:

+

2.1.事前准备阶段

在一次CR前,对以下内容进行充分准备。

+

2.1.1.CR的对象

在准备CR代码对象时,我们要注意代码的数量,如果代码量比较大,要对代码进行必要的分解,确定其中的关键代码,对关键代码进行CR,可以达到举一反三的目的。

+

2.1.2.CR的内容

我们对代码的审查内容很多,如代码的编写是否规范(注释的书写格式、命名规范等)、技术处理规范(异常处理、日志处理、代码组织结构等)、业务实现等。我们不能希望通过一次CR活动,完成所有这些内容的审查,因此我们必须设定本次CR活动内容界限,确定审查重点;

+

2.1.3.评审规范和标准

在CR前设计确定评审规范和标准是必要,通过规范和标准我们在审查过程中可以有据可依,有理可循,而且还可以做到标准统一。

+

2.1.4.选择CR活动的参与者

在CR开始前,必须把本次CR活动的对象、审查内容以及审查的规范和标准通报给所有的参与者。

+

2.1.5.选择CR活动的实施方式。

CR活动有很多形式可供我们选择,我们可以根据实际情况选择桌面式CR、演示讲解式CR、一对一的座位CR等等。

+

2.2.实施阶段

充分的事前准备,只是做好CR活动的前提,在CR实施过程中,我们要做好以下工作。

+

2.2.1.准确记录

对于CR过程发现的问题,我们必须清晰准确的记录,可以使用问题点记录单,明确记录的项目和内容。

+

2.2.2.讲解与提问

CR过程中,要采用代码作者讲解和审查者提问方式。审查者不能只在发现问题时提问,同时也要根据本次审查的内容要求代码作者对某个特定问题的讲解。

+

2.2.3.逐项审查

对事前确定的审查内容,要逐项审查,不能因为时间不足等因素一扫而过。

+

2.2.4.注意气氛

实施审查时,要营造一个讨论问题、解决问题的氛围,不能把审查会搞成批判会,这样会影响相关人员的积极性。

+

2.3. 事后跟踪跟踪。

2.3.1. 确认发现的问题

CR结束后,对发现的问题,首先需要确定以下内容。

+
    +
  1. 问题点的难易程度以及影响的范围;
  2. +
  3. 解决问题的责任者和问题点修正结果的确认者;
  4. +
  5. 解决问题点的时限。
  6. +
+

2.3.2. 修正问题责任者

对于修正问题责任者,在问题点的修正过程中,要三方面内容的记录。

+
    +
  1. 问题点的原因;
  2. +
  3. 解决问题点的对策;
  4. +
  5. 修正的内容。
  6. +
+

2.3.3. 修正结果确认者

做为修正结果的确认者,必须按照事前约定的时限及时的对修正结果进行全面的确认

+

3.注意事项

3.1. 经常进行Code Review

    +
  1. 要Review的代码越多,那么要重构,重写的代码就会越多。而越不被程序作者接受的建议也会越多,唾沫口水战也会越多。
  2. +
  3. 程序员代码写得时候越长,程序员就会在代码中加入越来越多的个人的东西。
  4. +
  5. 越接近软件发布的最终期限,代码也就不能改得太多。
  6. +
+

3.2. Code Review不要太正式,而且要短

忘了那个代码评审的Checklist吧,走到你的同事座位跟前,像请师父一样请他坐到你的电脑面前,然后,花5分钟给他讲讲你的代码,给他另外一个5分钟让他给你的代码提提意见,这比什么都好。而如果你用了一个Checklist,让这个事情表现得很正式的话,下面两件事中必有一件事会发生:

+
    +
  1. 只有在Checklist上存在的东西才会被Review。
  2. +
  3. Code Reviews 变成了一种礼节性的东西,你的同事会装做很关心你的代码,但其实他心里想着尽快地离开你。
  4. +
+

只有不正式的Code Review才会让你和评审者放轻松,人只有放松了,才会表现得很真实,很真诚。记住Review只不过是一种形式,而只有在相互信任中通过相互的讨论得到了有意义和有建设性的建议和意见,那才是最实在的。不然,作者和评审者的关系就会变成小偷和警察的关系。

+

3.3. 尽可能的让不同的人Reivew你的代码

如果可能的话,不要总是只找一个人来Review你的代码,不同的人有不同的思考方式,有不同的见解,所以,不同的人可以全面的从各个方面评论你的代码。
但不要太多了,人多嘴杂反而适得其反,基本上来说,不要超过3个人,这是因为,这是一个可以围在一起讨论的最大人员尺寸。

+

下面是几个优点:

+
    +
  1. 从不同的方向评审代码总是好的。
  2. +
  3. 会有更多的人帮你在日后维护你的代码。
  4. +
  5. 这也是一个增加团队凝聚力的方法。
  6. +
+

3.4. 保持积极的正面的态度

程序员最大的问题就是“自负”,尤其当我们Reivew别人的代码的时候,我已经见过无数的场面,程序员在Code Review的时候,开始抨击别人的代码,质疑别人的能力。太可笑了,我分析了一下,这类的程序员其实并没有什么本事,因为他们指责对方的目的是想告诉大家自己有多么的牛,靠这种手段来表现自己的程序员,其实是就是传说中所说的“半瓶水”。

+

所以,无论是代码作者,还是评审者,都需要一种积极向上的正面的态度,作者需要能够虚心接受别人的建议,因为别人的建议是为了让你做得更好;评审者也需要以一种积极的正面的态度向作者提意见,因为那是和你在一个战壕里的战友。记住,你不是一段代码,你是一个人!

+

3.5. 学会享受Code Reivew

这可能是最重要的一个提示了,如果你到了一个人人都喜欢Code Reivew的团阿,那么,你会进入到一个生机勃勃的地方,在那里,每个人都能写出质量非常好的代码,在那里,你不需要经理的管理,团队会自适应一切变化,他们相互学习,相互帮助,不仅仅是写出好的代码,而且团队和其中的每个人都会自动进化,最关键的是,这个是一个团队。

+

资料来源

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/27/index.html b/ja/page/27/index.html new file mode 100644 index 0000000000..bdbe5b97b8 --- /dev/null +++ b/ja/page/27/index.html @@ -0,0 +1,570 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前后端分离开发规范

by lixg

+

(本文所说的前后端工作皆站在工作内容本身,不与职称相关,下面皆如此。)

+

一、为什么要前后端分离

    +
  1. 前后端专注于各自擅长的领域
  2. +
  3. 前端配置后端代码运行环境,节省搭建环境的时间
  4. +
  5. 明确前后端工作职责
  6. +
  7. 提高开发效率
  8. +
  9. 分离有助于前端、后端分别优化
  10. +
+

二、前后端分离存在的问题

    +
  1. 前后端分离必须接口先行,无接口导致前后端关于接口的工作重复出现。
  2. +
  3. 接口对接方式不一致,各项目团队没有采用统一的接口对接方式,导致每次都要熟悉一种新的对接方式。
  4. +
  5. 数据传输格式不一致,导致每次都要重新熟悉,另外由于接口格式不一致还会引起其他未知问题,影响系统健壮性。
  6. +
  7. 人员比例问题,目前公司前后端比例不完全符合前后端分离开发的要求。
  8. +
+

为解决上述问题,提高前后端分离开发效率,特制定如下规范。

+

三、如何做分离

    +
  1. 职责分离
      +
    • 前后端仅仅通过异步接口(AJAX/JSONP)来编程
    • +
    • 前后端都各自有自己的开发流程,构建工具,测试集合
    • +
    • 关注点分离,前后端变得相对独立并松耦合
      前后端职责
    • +
    +
  2. +
+ + + + + + + + + + + + + + + + + + + + + + + +
后端前端
提供数据接收数据,展示数据
处理业务逻辑处理渲染逻辑
Server-side MVC架构Client-sideMV*架构
代码运行在服务器上代码运行在浏览器上
+
    +
  1. 开发流程
      +
    • 前后端技术负责人约定好接口格式
    • +
    • 后端编写和维护接口文档,在 API 变化时更新接口文档 + MOCK平台
    • +
    • 后端根据接口文档进行接口开发
    • +
    • 前端根据接口文档 + MOCK平台进行开发
    • +
    • 开发完成后联调和提交测试
    • +
    +
  2. +
+

MOCK平台统一采用公司搭建的YAPI平台

+

YAPI平台可以对接SWAGGER工具进行自动构建MOCK服务
前后端开发流程
3. 规范原则
- 接口返回数据即显示:前端仅做渲染逻辑处理
- 渲染逻辑禁止跨多个接口调用
- 前端关注交互、渲染逻辑,尽量避免业务逻辑处理的出现
- 请求响应传输数据格式:JSON,JSON数据尽量简单轻量,避免多级JSON的出现

+

四、基本格式

接口定义参见《RESTFul API的设计规范》

请求格式

GET请求、POST请求==必须包含key为body的入参,所有请求数据包装为JSON格式,并存放到入参body中==,示例如下:

+

GET请求:

+
1
xxx/login?body={"username":"admin","password":"123456","captcha":"scfd","rememberMe":1}
+ +

响应格式

对于通用业务数据响应参照基本数据格式要求

+

响应基本数据格式

1
2
3
4
{
"code": 200,
"msg": "success"
}
+
响应实体格式
1
2
3
4
5
6
7
8
9
10
11
{
"code": 200,
"msg": "success",
"data": {
"entity": {
"id": 1,
"name": "XXX",
"phone": "XXX"
}
}
}
+

entity: 响应返回的实体数据

+
响应列表格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"code": 200,
"msg": "success",
"data": {
"list":[
{
"id": 1,
"name": "XXX",
"code": "XXX"
},
{
"id": 2,
"name": "XXX",
"code": "XXX"
},
{
"id": 3,
"name": "XXX",
"code": "XXX"
}
]
}
}
+

list: 响应返回的列表数据

+
响应分页格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"code": 200,
"msg":"success",
"data": {
"totalCount": 2, // 总记录数
"totalPage": 1 // 总页数
"pageNo": 1, // 当前页码
"pageSize": 10, // 每页大小
"list":[
{
"id": 1,
"name": "XXX",
"code": "XXX"
},
{
"id": 2,
"name": "XXX",
"code": "XXX"
},
{
"id": 3,
"name": "XXX",
"code": "XXX"
}
],
}
}
+

响应特殊数据格式

对于特定组件数据格式由后端统一处理后返回前端,如(echart、ztree等组件)

+

特殊内容规范

布尔类型

关于布尔类型,一律返回BOOLEN类型值

+
日期格式

关于日期类型,JSON数据传输中一律使用字符串格式时间戳,具体日期格式因业务而定

+

五、相关文章导读

前后端分离开发指南-理论篇
前后端分离开发指南-实践篇

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/28/index.html b/ja/page/28/index.html new file mode 100644 index 0000000000..3f4e42f872 --- /dev/null +++ b/ja/page/28/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

背景

前后端分离狭义上来讲是以浏览器为分界,应用在浏览器内部的技术为前端技术,主要负责页面展示的部分,应用在浏览器外的技术为后端技术,主要负责处理业务逻辑和数据准备工作,但是随着近几年NodeJs的崛起,前端的工作范畴明显扩大,前后端分离也不仅仅作为一种开发模式,更是web应用架构层面的一种模式。

+

认识

    +
  1. 在开发阶段,前后端工程师要提前约定好数据交互的接口,从而实现并行开发和测试;
  2. +
  3. 在运行阶段,前后端分离模式需要对web应用进行分离部署,前后端之前使用HTTP或者其他协议进行交互请求。(NodeJs或Ngnix)
  4. +
+

由上述两条可以看出前后端分离分为开发阶段和部署阶段,通常来说我们做到的只是前后端分离开发,并没有做到分离部署。

+

分解

作为一种全新的架构模式,前后端分离需要从四个方面来比较和认识。

+
    +
  1. 交互形式
  2. +
  3. 开发模式/流程
  4. +
  5. 代码组织方式
  6. +
  7. 数据接口规范流程
  8. +
+

一、交互形式

在前后端分离架构中,后端只需要负责按照约定的数据格式向前端提供可调用的API服务即可。前后端之间通过HTTP请求进行交互,前端获取到数据后,进行页面的组装和渲染,最终返回给浏览器。

+

这里有一个经常引起激烈讨论的话题就是,“约定的数据格式”,是后端直接提供给前端可直接使用的数据格式,还是简单的数据库拉取数据不做加工直接推给前端,这个要视具体的项目情况而定,如果后端代码需要支持的不仅仅是一端,比如要同时适配pc端、手机端等多个终端,其多终端数据展现的形式如果各不相同,那后端只需要提供满足多端的基础数据格式即可,但如果只需要满足单个终端,那么建议直接将前后端数据格式进行统一处理,方便前后端协同,数据处理可以放在java端做也可以放在NodeJs端做,视人员配备情况而定,但绝不是后端不做任何处理就将数据返回给前端,完全由前端组织数据。

+

二、开发模式/流程

在前后端分离模式未诞生之前,前后端属于一个整体,那时使用的是MVC架构模式,前端对应的就是View层,主要通过html/css/js实现静态页面和动态效果,在有后端进行模板变量的嵌套和一些页面逻辑的处理,最终打包成一个整体,部署到同一服务器上,同时会进行简单的动静态分离部署。

+

此时开发的流程如下:
需求=》前后端并行开发=》前端开发静态页面=》后端套模板=》集成问题=》前后端调整=》再集成=》解决集成问题=》交付上线
// TODO 这里插图

+

出现前后端分离架构之后,前端工程师只需要编写前端页面+前端数据、业务逻辑处理,之后通过HTTP或其他请求方式调用后端提供的服务接口就可以了,而且除了在开发周期可以进行前后端分离,在部署阶段,前后端也可进行分离部署。
// TODO 这里插图

+

此时开发的流程如下:
需求=》设计接口、约定数据=》前后端并行开发=》集成=》调整=》集成成功=》交付上线

+

通过上面的描述及流程,不难发现,前后端分离的开发方式不仅仅从分工上进行了区分,更重要的是在并行开发的问题上解决了反复集成等前后端互相影响的问题,从而降低了开发的难度,简化了开发的流程。

+

三、代码组织方式

// TODO 这里插图
在传统的开发模式架构下,前端代码是作为项目的静态资源存在于项目工程下,页面中还夹着一些后端代码如jsp、php等技术,前后端开发时需要将整个项目代码完整的引入开发工具才能进行开发,前后端同时维护一份代码,这种开发方式导致前后端代码互相影响,因此前后端分离势在必行。

+

而前后端分离模式在代码的组织形式上由以下两种形式组成:

+
    +
  1. 半分离
    前后端仍共用一个代码库,但是代码分别存放在两个工程中。后端不关心或很少 关心前端元素的输出情况,前端不能独立进行开发和测试,项目中缺乏前后端 交互的测试用例。
  2. +
  3. 完全分离
    完全分离后,前端代码可以通过Mock来模拟后端请求,从此可以独立进行前端开发和测试。后端代码只需要按照跟前端约定好的接口格式写出完整的测试用例,确保接口的可用性。通过上述手段,降低开发集成风险。
  4. +
+

四、数据接口规范流程

通过上面三段的描述,我们可以看出前后端分离开发模式最重要也是最初的阶段就是数据接口的确定,因此在项目开发前必需先进行数据和接口的定义,数据接口的定义需要前后端开发共同商定,包括确定的数据格式,交互形式,并生成一份接口文档供前后端开发人员使用。之后才是并行开发。开发期间前后端双方需要严格按照确定的数据接口文档进行开发,前端开发完之后可以利用mock服务独自进行接口测试,后端也可以利用postman或其他接口测试工具进行测试,并提供完整的接口测试用例,然后前后端进行功能联调,最后再提交线上测试,也可进行自动化测试。

+

// TODO 这里插图

+

分离后的收益

到底分不分,如何分是个持续讨论的话题,通过上述的内容大家已经了解到了,什么是前后端分离,也知道如何进行前后端分离开发部署,那么前后端分离能带来哪些收益呢?

+

首先,就目前的软件开发应用趋势来看,越来越注重用户的体验性,而且架构越来越大,服务越来越小,而且终端设备越来越丰富,而原来不分离的方式已经不能支撑现在的发展趋势,因此前后端分离开发及部署将势在必行。

+

而且采用前后端分离的架构之后,我们将有如下几点提升:

+
    +
  1. 前后端分离后,前后端将不再互相纠缠而是各自在自己熟悉的领域进行开发工作,这将有利于前后端深化优化各自的代码,培养各自独特的技术特性,从而开发出更加优秀的应用,建立起专业精良的全栈开发团队。

    +
  2. +
  3. 通过前后端分离架构可以实现前后端开发从代码及开发流程上的完全解耦,只需要前后端共同商定好接口后,便可完全独立开发,只需要在联调阶段进行好协作,在此之前可以互不影响的进行并行开发,即是需求发生了变动,但只要不影响接口,后端既可以不用修改代码,只需前端进行变动即可,如此整体的开发效率将得到提升。

    +
  4. +
  5. 前后端分离后,能够更好的适应前端日益增多的的终端适配,代码解耦后复用率更高。

    +
  6. +
+

4.前后端分离后,前后端代码可以分别管理,代码不再混在一起,代码可维护性也增强了

+

前后端分离后收益不止以上四点,因为分离而带来的职责上、技术上、代码上、部署上的解耦让开发工作比以往任何时候都要更加专注和轻松。

+

注意事项

前后端分离误区

    +
  1. 前端人员不充足,不能进行前后端分离。
  2. +
+

此话说来是因为对前后端分离后职责区分不明确导致的问题,因为以往的前端只需要写静态页面就可以了,而前后端分离后前端也不仅仅需要写静态页面,而且还要为页面提供数据和页面逻辑的处理,但实际上可以根据团队情况来区别对待,如果团队前端人员充足,那么可以由前端人员负责多一些的工作,比如API请求后的业务逻辑的处理,页面逻辑的处理、页面数据的准备等,如果后端人员配备充足,那么上述几个环节仍然可由后端人员进行处理,前端开发仍然只是写静态页面,只是内容和逻辑不再写死而是通过js或其他手段如mvvm的框架进行处理。

+
    +
  1. 前后端分离后前端任务加重,职责也不清晰。
  2. +
+

如第一点描述可知,问题不在前后端分离的模式是否合适,而是任务分配和人员分配上的问题,如果前端能力强且人员比例较多,那么部分任务可以由前端承担,如果后端人员多,那么任务由后端承担。

+
    +
  1. 后端开发需要增加接口开发工作,增加任务量。
  2. +
+

无论如何后端开发都是需要写接口的,只是前后端分离后需要按照ResetFul风格写接口,或者采用最新的GraphQl的方式进行交互,如果说这个阶段需要前后端进行商量确定接口交互形式和数据格式花费了时间,但是在接下来前后端并行开发及问题解决上省掉的时间是更加可观的。

+
    +
  1. 分离后仍出现互相等待的问题,反而不如传统开发模式快。
  2. +
+

这个问题的产生其实也是由于对前后端分离后技术缺失导致的,常见情况是前端写完页面逻辑和假数据后后端开发还未完成接口开发导致无法进行联调,实际上前端通过mockserver等方式是可以解决一些问题的。

+

前后端分离适用场景

现代化的web应用适合用前后端分离的开发方式。
原因有以下几点:

+
    +
  1. web应用前端页面交互复杂。
      +
    • 页面渲染数据量大。
    • +
    • 页面包含复杂的业务逻辑。
    • +
    +
  2. +
  3. 终端适配情况多。
  4. +
  5. 分布式架构,微服务化应用场景。
  6. +
+

前后端分离具体方案

总体方向

后端专注于:后端控制层(Restful API) & 服务层 & 数据访问层;

+

前端专注于:前端控制层(Nodejs) & 视图层

+
    +
  1. 项目设计阶段,前后端架构负责人将项目整体进行分析,讨论并确定API风格、职责分配、开发协助模式,确定人员配备;设计确定后,前后端人员共同制定开发接口。

    +
  2. +
  3. 项目开发阶段,前后端分离是各自分工,协同敏捷开发,后端提供Restful API,并给出详细文档说明,前端人员进行页面渲染前台的任务是发送API请(GET,PUT,POST,DELETE等)获取数据(json,xml)后渲染页面。

    +
  4. +
  5. 项目测试阶段,API完成之前,前端人员会使用mock server进行模拟测试,后端人员采用junit进行API单元测试,不用互相等待;API完成之后,前后端再对接测试一下就可以了,当然并不是所有的接口都可以提前定义,有一些是在开发过程中进行调整的。

    +
  6. +
  7. 项目部署阶段,利用nginx 做反向代理,即Java + nodejs + nginx 方式进行。

    +
  8. +
+

技术手段

    +
  • 前端技术栈:前端代码 + mock服务
  • +
  • 后端技术栈:postman + 接口 + 后端业务逻辑 + 数据库
  • +
  • 公共依赖:接口文档/接口测试工具
  • +
+

常用的mock服务为jsonserver
常用的接口测试工具为postman、rap、swagger、doclever

+

部署方案

    +
  1. 第一阶段为前后端同一个代码库,同一个服务器,集中部署。
  2. +
  3. 第二阶段引入Ngnix服务作为中间件,前端向Ngnix发请求,Ngnix向后端服务发请求,由于Ngnix为静态服务器,所以在seo优化上和页面性能优化上效果不明显,因此前端仍需与后端进行配合才能达到整体的优化。
  4. +
+

浏览器 =》 Ngnix(前端机)=》Ngnix(后端机可没有)=》Server服务

+
    +
  1. 第三阶段引入nodejs作为中间层,将前端资源部署到Server层。同时实现数据代理服务,负责与提供数据的后端进行通信。
  2. +
+

浏览器=》Ngnix(前端机)=》NodeServer =》Server服务

+

浏览器向前端机发送请求,由Ngnix进行分发,url统一分发至NodeServer,在Node Server中根据请求类型从后端服务器上通过RPC服务请求页面的模板数据,然后进行页面的组装和渲染;API请求则直接转发到后端服务挖成相应。

+

前后端分离部署方案比较

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
属性字段传统模式Ngnix+ServerNode+ServerNgnix+Node+Server
SEOoknookok
浏览器渲染负担oknookok
前后端耦合nookokok
请求相应效率nooknook
+

结语

随着前端技术的快速发展和对用户体验日益增长的需求,前后端分离模式势必将会成为主流趋势。无论是从开发模式的角度上来说,还是对团队成长的角度上来说,前后端分离都会带来益处,让我们一同拥抱前后端分离,打造精良的开发团队,迎接日益复杂的web应用开发需求。

+

参考来源

+

版权声明

Copyright by lixuguang
未经授权,严禁转载。如需转载,请联系作者

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/29/index.html b/ja/page/29/index.html new file mode 100644 index 0000000000..86cc890e01 --- /dev/null +++ b/ja/page/29/index.html @@ -0,0 +1,548 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

目录
代码规范 2

+
    +
  1. 说明 2
  2. +
  3. 基本原则 2
  4. +
  5. C++与Python 2
  6. +
  7. 命名相关 2
  8. +
  9. 工程目录结构 3
  10. +
+

Electron代码规范

+
    +
  1. 说明
    Electron框架正如官网介绍是使用 JavaScript, HTML 和 CSS 构建跨平台的桌面应用的框架,框架分为前端和后端两个部分,根据前后端采用的技术不同分别遵循前后端所采用技术的代码规范
  2. +
  3. 基本原则
  4. +
+
    +
  1. 不要直接在渲染进程操作主进程,通过进程通信的方式进行数据及事件响应

    +
  2. +
  3. nodejs请使用require方式引入资源不使用import方式引入

    +
  4. +
  5. 多渲染进程传参请使用localStorage或sessionStorage

    +
  6. +
  7. 资源下载不下来时请直接到官网下载https://github.com/electron/electron/releases 将文件放到 用户目录\AppData\Local\electron\Cache 下 再进行 npm install -g electron

    +
  8. +
  9. 代码中包含native模块,必须进行 rebuild操作
    调试渲染进程时增加如下代码增加mainWindow.webContents.openDevTools();

    +
  10. +
  11. +
+
    +
  1. C++与Python
    对于 C++ 和 Python, 遵循 Chromium 的编码风格. 可以使用 clang-format 来自动格式化 C++ 代码. 可以使用 script/cpplint.py 来检验文件是否符合要求。
  2. +
+

现在使用的 Python 版本是 Python 2.7。

+

C++ 代码使用了大量 Chromium 的抽象和类型,因此建议使用者熟悉它们。 一个起步的好地方是 Chromium 的《重要的抽象概念和数据库结构》文档. 该文档提到一些特殊类型,范围类型(超出范围时自动释放其内存), 记录机制等。
4. 命名相关
Electron API 使用与 Node.js 相同的大小写方案:
当模块本身是class时, 比如 BrowserWindow, 使用 大驼峰.
当模块是一组 API 时, 比如 globalShortcut时,使用 小驼峰。
当 API 是对象的属性时, 并且它复杂到足以成为一个单独的块, 比如 win.webContents, 使用 小驼峰.
对于其他非模块API, 使用自然标题, 比如 Tag 或 Process Object.

+

当创建新的 API 时, 最好使用 getter 和 setter 而不是 jQuery 的一次性函数。 举个例子, .getText() 和 .setText(text) 优于 .text([text]).

+
    +
  1. 工程目录结构
    src - main
    // 必须

    +
      +
    • main.js // 入口
    • +
    • listen.js // 监听渲染进程通信
    • +
    • windows.js // 创建渲染进程
    • +
    • log.js // 日志log4js 插件
    • +
    +

    // 可选

    +
      +
    • utils.js
    • +
    • store.js
    • +
    +
  2. +
+
    +
  • api.js
  • +
  • render
  • +
+

main 目录下为主进程代码,render 目录下为渲染进程代码
渲染进程目录结构规范与前端项目目录结构规范一致

+

主进程中必须包含入口文件,监听进程文件和窗口创建文件
其他根据后端实际使用技术可选,nodejs作为后端的话 必须
6. 打包
Electron-packager 绿色可执行包
Electron-builder 压缩安装包

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/3/index.html b/ja/page/3/index.html new file mode 100644 index 0000000000..ee94aa1506 --- /dev/null +++ b/ja/page/3/index.html @@ -0,0 +1,499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Network in Compose 在Docker Compose中配置网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
version: "3.9"

services:
proxy:
build: ./proxy
networks:
- actsnetwork
app:
build: ./app
networks:
- actsnetwork
db:
image: postgres
networks:
- testnetwork

networks:
actsnetwork:
name: testnetwork
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/30/index.html b/ja/page/30/index.html new file mode 100644 index 0000000000..56041240ae --- /dev/null +++ b/ja/page/30/index.html @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Squid 服务器学习笔记

Squid 服务器介绍

用来缓存前端资源的代理服务器,可以类比后端数据库缓存软件Redis。
当用户访问目标资源时由Squid服务器进行判断如果有数据缓存直接将缓存数据返回,如果没有缓存,代理客户访问资源,获取到后返回给用户,当有其他用户访问相同资源时直接返回资源。

+

代理方式

    +
  1. 普通代理
  2. +
  3. 透明代理
  4. +
  5. 反向代理
  6. +
+

安装

1
2
3
4
5
6
7
8
9
10
11
12
// 检查是否已经安装
rpm -qa | grep squid
// 安装软件
rpm install squid
// 设置开机启动
chkconfig squid on
// 启动服务
service squid start
// 查看服务状态(端口监听3128)
netstat -anput |grap squid
// 关闭服务
service squid stop
+ +

配置文件

/etc/squid/squid.config

+

10.0.0.0/8

+
+

扩展知识
CIDR - 无类域间路由CIDR(Classless InterDomain Routing)
ip每位占1个字节8位,00000000 ~ 11111111,也就是0 ~ 255,ip共有四个位数。
10.0.0.0/8 的意思是 8位不可变,剩下的可变,8位这里的意思是说第一个IP段,那么也就是说IP范围是10.0.0.0 ~ 10.255.255.255 这样的一个ip范围。

+
+

// 复制并重命名
mv /etc/squid/squid.config{,.bak}
// 删除文件中的注释和空行(只保留有效设定)
awk ‘if($0!~ /^#/ && $0~ !/^$/print $0)’ /etc/squid/squid.config.bak > /etc/squid/squid.config

+

squid常用命令:
/usr/local/squid/sbin/squid -z 初始化缓存空间
/usr/local/squid/sbin/squid 启动
/usr/local/squid/sbin/squid -k shutdown 停止
/usr/local/squid/sbin/squid -k reconfigure 重新载入配置文件
/usr/local/squid/sbin/squid -k rotate 轮循日志

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#acl all src 0.0.0.0/0.0.0.0 and http_access allow all选项定义了一个访问控制列表。详细情况参见和Squid软件
#携带的文档。这里的访问控制列表允许所有对代理服务的访问,因为这里该代理是加速web服务器。
acl all src 0.0.0.0/0.0.0.0
#允许所有IP访问
acl manager proto http #manager url协议为http
acl localhost src 127.0.0.1/255.255.255.255 #允午本机IP
acl to_localhost dst 127.0.0.1 #允午目的地址为本机IP
acl Safe_ports port 80 # 允许安全更新的端口为80
acl CONNECT method CONNECT #请求方法以CONNECT
http_access allow all #允许所有人使用该代理.因为这里是代理加速web服务器
http_reply_access allow all #允许所有客户端使用该代理

acl OverConnLimit maxconn 16 #限制每个IP最大允许16个连接,防止攻击
http_access deny OverConnLimit

icp_access deny all #禁止从邻居服务器缓冲内发送和接收ICP请求.
miss_access allow all #允许直接更新请求
ident_lookup_access deny all #禁止lookup检查DNS
http_port 8080 transparent #指定Squid监听浏览器客户请求的端口号。

hierarchy_stoplist cgi-bin ? #用来强制某些特定的对象不被缓存,主要是处于安全的目的。
acl QUERY urlpath_regex cgi-bin \?
cache deny QUERY

cache_mem 1 GB #这是一个优化选项,增加该内存值有利于缓存。应该注意的是:
#一般来说如果系统有内存,设置该值为(n/)3M。现在是3G 所以这里1G
fqdncache_size 1024 #FQDN 高速缓存大小
maximum_object_size_in_memory 2 MB #允许最大的文件载入内存

memory_replacement_policy heap LFUDA #动态使用最小的,移出内存cache
cache_replacement_policy heap LFUDA #动态使用最小的,移出硬盘cache

cache_dir ufs /home/cache 5000 32 512 #高速缓存目录 ufs 类型使用的缓冲值最大允午1000MB空间,
#32个一级目录,512个二级目录

max_open_disk_fds 0 #允许最大打开文件数量,0 无限制
minimum_object_size 1 KB #允午最小文件请求体大小
maximum_object_size 20 MB #允午最大文件请求体大小

cache_swap_low 90 #最小允许使用swap 90%
cache_swap_high 95 #最多允许使用swap 95%

ipcache_size 2048 # IP 地址高速缓存大小 2M
ipcache_low 90 #最小允许ipcache使用swap 90%
ipcache_high 95 #最大允许ipcache使用swap 90%


access_log /var/log/squid/access.log squid #定义日志存放记录
cache_log /var/log/squid/cache.log squid
cache_store_log none #禁止store日志

emulate_httpd_log on #将使Squid仿照Web服务器的格式创建访问记录。如果希望使用
#Web访问记录分析程序,就需要设置这个参数。

refresh_pattern . 0 20% 4320 override-expire override-lastmod reload-into-ims ignore-reload #更新cache规则

acl buggy_server url_regex ^http://.... http:// #只允许http的请求
broken_posts allow buggy_server

acl apache rep_header Server ^Apache #允许apache的编码
broken_vary_encoding allow apache

request_entities off #禁止非http的标分准请求,防止攻击
header_access header allow all #允许所有的http报头
relaxed_header_parser on #不严格分析http报头.
client_lifetime 120 minute #最大客户连接时间 120分钟

cache_mgr sky@test.com #指定当缓冲出现问题时向缓冲管理者发送告警信息的地址信息。

cache_effective_user squid #这里以用户squid的身份Squid服务器
cache_effective_group squid

icp_port 0 #指定Squid从邻居服务器缓冲内发送和接收ICP请求的端口号。
#这里设置为0是因为这里配置Squid为内部Web服务器的加速器,
#所以不需要使用邻居服务器的缓冲。0是禁用

cache_peer 127.0.0.1 parent 80 0 no-query default multicast-responder no-netdb-exchange #cache_peer 设置允许更新缓存的主机,因是本机所以127.0.0.1
cache_peer_domain 127.0.0.1
hostname_aliases 127.0.0.1

error_directory /usr/share/squid/errors/Simplify_Chinese #定义错误路径

always_direct allow all # cache丢失或不存在是允许所有请求直接转发到原始服务器
ignore_unknown_nameservers on #开反DNS查询,当域名地址不相同时候,禁止访问
coredump_dir /var/log/squid #定义dump的目录

max_filedesc 2048 #最大打开的文件描述

half_closed_clients off #使Squid在当read不再返回数据时立即关闭客户端的连接。
#有时read不再返回数据是由于某些客户关闭TCP的发送数据
#而仍然保持接收数据。而Squid分辨不出TCP半关闭和完全关闭。

buffered_logs on #若打开选项“buffered_logs”可以稍稍提高加速某些对日志文件的写入,该选项主要是实现优化特性。

acl tianya referer_regex -i tianya #防止天涯盗链,转嫁给百度
http_access deny tianya
deny_info tianya

acl baidu req_header User-Agent Baiduspider #阻止baidu蜘蛛
http_access deny baidu

acl OverConnLimit maxconn 128 #限制同一IP客户端的最大连接数
http_access deny OverConnLimit

acl myip dst 222.18.63.37 #防止被人利用为HTTP代理,设置允许访问的IP地址
http_access deny !myip

acl Manager proto cache_object #允许本地管理
acl Localhost src 127.0.0.1 222.18.63.37
http_access allow Manager Localhost
cachemgr_passwd 53034338 all
http_access deny Manager

acl all src 0.0.0.0/0.0.0.0 #仅仅允许80端口的代理
acl Safe_ports port 80 # http
http_access deny !Safe_ports
http_access allow all

visible_hostname happy.swjtu.edu.cn #Squid信息设置
cache_mgr ooopic2008@qq.com

cache_effective_user squid #基本设置
cache_effective_group squid
tcp_recv_bufsize 65535 bytes

cache_peer 127.0.0.1 parent 80 0 no-query originserver #2.6的反向代理加速配置

error_directory /usr/local/squid/share/errors/Simplify_Chinese #错误文档

icp_port 0 #单台使用,不使用该功能

hierarchy_stoplist cgi-bin ?

acl QUERY urlpath_regex cgi-bin \? .php .cgi .avi .wmv .rm .ram .mpg .mpeg .zip .exe
cache deny QUERY

acl apache rep_header Server ^Apache
broken_vary_encoding allow apache


refresh_pattern ^ftp: 1440 20% 10080
refresh_pattern ^gopher: 1440 0% 1440
refresh_pattern . 0 20% 4320

cache_store_log none
pid_filename /usr/local/squid/var/logs/squid.pid
emulate_httpd_log on
logformat combined %>a %ui %un [%tl] "%rm %ru HTTP/%rv" %Hs %<st "%{Referer}>h" "%{User-Agent}>h" %Ss:%Sh
cache_log /usr/local/squid/var/logs/cache.log
access_log /usr/local/squid/var/logs/access.log combined
coredump_dir /usr/local/squid/var/cache
cache_dir ufs /usr/local/squid/var/cache 10000 16 256

dns_children 32
hosts_file /etc/hosts

cache_mem 400 MB
cache_swap_low 90
cache_swap_high 95
maximum_object_size 32768 KB
maximum_object_size_in_memory 4096 KB
emulate_httpd_log on

acl picurl url_regex -i \.bmp$ \.png$ \.jpg$ \.gif$ \.jpeg$ #防止盗链
acl mystie1 referer_regex -i happy.swjtu.edu.cn
http_access allow mystie1 picurl
acl nullref referer_regex -i ^$
http_access allow nullref
acl hasref referer_regex -i .+
http_access deny hasref picurl
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/31/index.html b/ja/page/31/index.html new file mode 100644 index 0000000000..a1d52bcb1d --- /dev/null +++ b/ja/page/31/index.html @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

插件安装

Prettier - Code formatter

格式化工具

+

ESLint

校验规则

+

Vetur

vue代码片段及代码美化

+

Vue 2 Snippets

vue2 代码片段

+

vscode-fileheader

文件注释

+

配置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// setting.json vscode配置文件
{
"workbench.startupEditor": "newUntitledFile",
"terminal.integrated.shell.windows": "C:\\Program Files\\Git\\bin\\bash.exe", \\ 自行配置自己的环境地址
"javascript.updateImportsOnFileMove.enabled": "always",
"editor.tabSize": 2,
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true
},
"sync.gist": "748b4cae5eb6e56d6997978ead096e8f",
"breadcrumbs.enabled": true,
"todohighlight.isEnable": false,
"liveServer.settings.donotShowInfoMsg": true,
"search.location": "sidebar",
"workbench.activityBar.visible": true,
"window.menuBarVisibility": "default",
"workbench.statusBar.visible": true,
"editor.snippetSuggestions": "top",
"editor.formatOnPaste": true,
"workbench.colorTheme": "Tiny Light",
"eslint.validate": [
"javascript",
"javascriptreact",
{
"language": "html",
"autoFix": true
},
{
"language": "vue",
"autoFix": true
}
],
"prettier.eslintIntegration": true,
"files.autoSave": "onWindowChange",
"code-runner.saveAllFilesBeforeRun": true,
"vetur.format.defaultFormatter.html": "js-beautify-html",
"vetur.format.defaultFormatter.js": "vscode-typescript",
"prettier.jsxSingleQuote": true,
"prettier.requireConfig": false,
"prettier.arrowParens": "always",
"typescript.format.insertSpaceAfterSemicolonInForStatements": false,
"prettier.stylelintIntegration": true,
"prettier.singleQuote": true,
"prettier.tslintIntegration": true,
"eslint.provideLintTask": true,
"eslint.autoFixOnSave": true,
"editor.mouseWheelZoom": true,
"editor.tabCompletion": "on",
"editor.formatOnType": true,
"eslint.alwaysShowStatus": true,
"eslint.options": {
"configFile": "E:/project/xxjs/fore-core/.eslintrc.js" // 自行配置自己的项目地址
},
"fileheader.Author": "Li.Xg", // 自行配置自己的名称
"fileheader.LastModifiedBy": "Li.Xg" // 自行配置自己的名称
}
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/32/index.html b/ja/page/32/index.html new file mode 100644 index 0000000000..977951ddfe --- /dev/null +++ b/ja/page/32/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

EventLoop 翻译过来就是事件循环,那啥是事件循环啊,这个要从javascript是如何执行的说起。
要知道javascript是一个运行在浏览器或者Node环境的单线程执行的脚本语言,但是为了解决单线程阻塞的问题,这里就引入了事件循环的机制,也就是我们常说的异步特性。

+

但是虽然都是JavaScript,都是事件循环,都有异步特性,但浏览器环境和Node环境的实现方式是不一样的,这里要先说明一下。
浏览器的事件循环是HTML定义的规范,而Node环境是利用libuv库实现的,这里我们先从浏览器的事件循环开始说。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/33/index.html b/ja/page/33/index.html new file mode 100644 index 0000000000..aff04b8173 --- /dev/null +++ b/ja/page/33/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

今天一个小兄弟微信给我发了个消息,说是今天面试,面试官问了个比较基础的问题,没答好,我就问他问了什么,他说,数组有哪些方法会改变原来的值,他说他就说了3、4个,还不确定对不对,我想了想说其实列个表就很容易记住了,于是便有了下面这个表格。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/34/index.html b/ja/page/34/index.html new file mode 100644 index 0000000000..7aad801757 --- /dev/null +++ b/ja/page/34/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

还记得你的前端工程里有这样一个文件‘package.json’,里面管理者运行代码所需要的各种依赖,每种依赖都会有一个版本号,那么你知道版本号是怎么定义的么?不知道的话就看看下面的内容吧。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/35/index.html b/ja/page/35/index.html new file mode 100644 index 0000000000..6705169f96 --- /dev/null +++ b/ja/page/35/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/36/index.html b/ja/page/36/index.html new file mode 100644 index 0000000000..f66b9a3c55 --- /dev/null +++ b/ja/page/36/index.html @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

在移动 App 开发领域,有一个框架非常知名,每每面试都会被问起,“你用过 Ionic 么?”,是的,就是 Ionic 框架。

+

之前已经学习过了 Cordova ,它和 Ionic 可以说是一对黄金搭档,一个面向 MobileOS ,一个面向 UI ,很多人搞不清楚它们之间的区别,没关系,回头我再抽时间讲一下,今天我们主要来认识一下 Ionic

+

好戏开始:)

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/37/index.html b/ja/page/37/index.html new file mode 100644 index 0000000000..d76a911105 --- /dev/null +++ b/ja/page/37/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

很长时间没有写博客了,因为最近换了工作,原来的技术栈基本上都换了,现在接触了很多新的技术栈,Cordova 就是其中之一。

+

接下来这篇文章就是我的学习笔记,以供回顾,写的不好请勿拍砖。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/38/index.html b/ja/page/38/index.html new file mode 100644 index 0000000000..dfc753a82a --- /dev/null +++ b/ja/page/38/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/39/index.html b/ja/page/39/index.html new file mode 100644 index 0000000000..a5e7392175 --- /dev/null +++ b/ja/page/39/index.html @@ -0,0 +1,523 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

/bin:

bin是Binary的缩写, 这个目录存放着最经常使用的命令。

+

/boot:

这里存放的是启动Linux时使用的一些核心文件,包括一些连接文件以及镜像文件。

+

/dev :

dev是Device(设备)的缩写, 该目录下存放的是Linux的外部设备,在Linux中访问设备的方式和访问文件的方式是相同的。

+

/etc:

这个目录用来存放所有的系统管理所需要的配置文件和子目录。

+

/home:

用户的主目录,在Linux中,每个用户都有一个自己的目录,一般该目录名是以用户的账号命名的。

+

/lib:

这个目录里存放着系统最基本的动态连接共享库,其作用类似于Windows里的DLL文件。几乎所有的应用程序都需要用到这些共享库。

+

/lost+found:

这个目录一般情况下是空的,当系统非法关机后,这里就存放了一些文件。

+

/media linux

系统会自动识别一些设备,例如U盘、光驱等等,当识别后,linux会把识别的设备挂载到这个目录下。

+

/mnt:

系统提供该目录是为了让用户临时挂载别的文件系统的,我们可以将光驱挂载在/mnt/上,然后进入该目录就可以查看光驱里的内容了。

+

/opt:

这是给主机额外安装软件所摆放的目录。比如你安装一个ORACLE数据库则就可以放到这个目录下。默认是空的。

+

/proc:

这个目录是一个虚拟的目录,它是系统内存的映射,我们可以通过直接访问这个目录来获取系统信息。
这个目录的内容不在硬盘上而是在内存里,我们也可以直接修改里面的某些文件,比如可以通过下面的命令来屏蔽主机的ping命令,使别人无法ping你的机器:

+

/root:

该目录为系统管理员,也称作超级权限者的用户主目录。

+

/sbin:

s就是Super User的意思,这里存放的是系统管理员使用的系统管理程序。

+

/selinux:

这个目录是Redhat/CentOS所特有的目录,Selinux是一个安全机制,类似于windows的防火墙,但是这套机制比较复杂,这个目录就是存放selinux相关的文件的。

+

/srv:

该目录存放一些服务启动之后需要提取的数据。

+

/sys:

这是linux2.6内核的一个很大的变化。该目录下安装了2.6内核中新出现的一个文件系统 sysfs 。

+

sysfs文件系统集成了下面3种文件系统的信息:针对进程信息的proc文件系统、针对设备的devfs文件系统以及针对伪终端的devpts文件系统。
该文件系统是内核设备树的一个直观反映。

+

当一个内核对象被创建的时候,对应的文件和目录也在内核对象子系统中被创建。

+

/tmp:

这个目录是用来存放一些临时文件的。

+

/usr:

这是一个非常重要的目录,用户的很多应用程序和文件都放在这个目录下,类似与windows下的program files目录。

+

/usr/bin:

系统用户使用的应用程序。

+

/usr/sbin:

超级用户使用的比较高级的管理程序和系统守护程序。

+

/usr/src:

内核源代码默认的放置目录。

+

/var:

这个目录中存放着在不断扩充着的东西,我们习惯将那些经常被修改的目录放在这个目录下。包括各种日志文件。

+

在linux系统中,有几个目录是比较重要的,平时需要注意不要误删除或者随意更改内部文件。
系统上跑了很多程序,那么每个程序都会有相应的日志产生,而这些日志就被记录到这个目录下,具体在/var/log 目录下,另外mail的预设放置也是在这里。

+

/etc:

上边也提到了,这个是系统中的配置文件,如果你更改了该目录下的某个文件可能会导致系统不能启动。

+

/bin, /sbin, /usr/bin, /usr/sbin:

这是系统预设的执行文件的放置目录,比如 ls 就是在/bin/ls 目录下的。

+

值得提出的是,/bin, /usr/bin 是给系统用户使用的指令(除root外的普通用户),而/sbin, /usr/sbin 则是给root使用的指令。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/4/index.html b/ja/page/4/index.html new file mode 100644 index 0000000000..c9542739dc --- /dev/null +++ b/ja/page/4/index.html @@ -0,0 +1,499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

初始化数据库

c:\pgsql\bin\initdb.exe -D c:\pgsql\data -E UTF8

+

注册表

c:\pgsql\bin\pg_ctl register -N postgres -D c:\pgsql\data

+

启动服务

c:\pgsql\bin\postgres -D c:\pgsql\data

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/40/index.html b/ja/page/40/index.html new file mode 100644 index 0000000000..5a6c9578a6 --- /dev/null +++ b/ja/page/40/index.html @@ -0,0 +1,576 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

安装

sudo rpm -ivh [lz4-1.8.3-1.el7.x86_64.rpm]

+

卸载

sudo rpm -e [postgresql14-server-14.4-1PGDG.rhel7.x86_64]

+

查询功能

命令格式 rpm {-q|–query} [select-options] [query-options]

+

  RPM的查询功能是极为强大,是极为重要的功能之一;举几个常用的例子,更为详细的具体的,请参考#man rpm

+

1、对系统中已安装软件的查询

+

1)查询系统已安装的软件

+

  语法:rpm -q 软件名

+

  举例:[root@localhost beinan]# rpm -q gaim

+

  gaim-1.3.0-1.fc4   

+
   查看系统中所有已经安装的包,要加 -a 参数 ;
+
+

  [root@localhost RPMS]# rpm -qa

+

  如果分页查看,再加一个管道 |和more命令;

+

  [root@localhost RPMS]# rpm -qa |more

+

  在所有已经安装的软件包中查找某个软件,比如说 gaim ;可以用 grep 抽取出来;

+

  [root@localhost RPMS]# rpm -qa |grep gaim

+

  上面这条的功能和 rpm -q gaim 输出的结果是一样的;

+

2)查询一个已经安装的文件属于哪个软件包

+

  语法 rpm -qf 文件名

+

  注:文件名所在的绝对路径要指出

+

  举例:

+

  [root@localhost RPMS]# rpm -qf /usr/lib/libacl.la

+

  libacl-devel-2.2.23-8

+

3)查询已安装软件包都安装到何处

+

  语法:rpm -ql 软件名 或 rpm rpmquery -ql 软件名

+

  举例:

+

  [root@localhost RPMS]# rpm -ql lynx

+

  [root@localhost RPMS]# rpmquery -ql lynx

+

4)查询一个已安装软件包的信息

+

  语法格式: rpm -qi 软件名

+

  举例:

+

  [root@localhost RPMS]# rpm -qi lynx

+

5)查看一下已安装软件的配置文件

+

  语法格式:rpm -qc 软件名

+

  举例:

+

  [root@localhost RPMS]# rpm -qc lynx

+

6)查看一个已经安装软件的文档安装位置

+

  语法格式: rpm -qd 软件名

+

  举例:

+

  [root@localhost RPMS]# rpm -qd lynx

+

7)查看一下已安装软件所依赖的软件包及文件

+

  语法格式: rpm -qR 软件名

+

  举例:

+

  [root@localhost beinan]# rpm -qR rpm-python

+

  查询已安装软件的总结:对于一个软件包已经安装,我们可以把一系列的参数组合起来用;比如 rpm -qil ;比如:

+

  [root@localhost RPMS]# rpm -qil lynx

+

2、对于未安装的软件包的查看:

+

  查看的前提是您有一个.rpm 的文件,也就是说对既有软件file.rpm的查看等;

+

1)查看一个软件包的用途、版本等信息;

+

  语法: rpm -qpi file.rpm

+

  举例:

+

  [root@localhost RPMS]# rpm -qpi lynx-2.8.5-23.i386.rpm

+

2)查看一件软件包所包含的文件;

+

  语法: rpm -qpl file.rpm

+

  举例:

+

  [root@localhost RPMS]# rpm -qpl lynx-2.8.5-23.i386.rpm

+

3)查看软件包的文档所在的位置;

+

  语法: rpm -qpd file.rpm

+

  举例:

+

  [root@localhost RPMS]# rpm -qpd lynx-2.8.5-23.i386.rpm

+

4)查看一个软件包的配置文件;

+

  语法: rpm -qpc file.rpm

+

  举例:

+

  [root@localhost RPMS]# rpm -qpc lynx-2.8.5-23.i386.rpm

+

5)查看一个软件包的依赖关系

+

  语法: rpm -qpR file.rpm

+

  举例:

+

  [root@localhost archives]# rpm -qpR yumex_0.42-3.0.fc4_noarch.rpm

+

  /bin/bash

+

  /usr/bin/python

+

  config(yumex) = 0.42-3.0.fc4

+

  pygtk2

+

  pygtk2-libglade

+

  rpmlib(CompressedFileNames) <= 3.0.4-1

+

  rpmlib(PayloadFilesHavePrefix) <= 4.0-1

+

  usermode

+

  yum >= 2.3.2

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/41/index.html b/ja/page/41/index.html new file mode 100644 index 0000000000..e836a4dd46 --- /dev/null +++ b/ja/page/41/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

前言

最近在做代码风格统一,用到了Prettier,暂时先记录一下现在的配置信息,回头抽时间写篇总结文。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/42/index.html b/ja/page/42/index.html new file mode 100644 index 0000000000..b312e5f0fd --- /dev/null +++ b/ja/page/42/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

今天在看一个叫做Avue的框架组件时看到了一个叫做“全局水印”的组件,功能呢就是给当前页面加上一个水印遮罩,就像下图这样:
Y08cbd.png
因为工作中有这样的场景,所以我很好奇看了一下他的代码,他的水印设置为了fixed定位,并且z-index设置的很高,但是它具有穿透性,最终我发现其中起作用的是一个叫做“pointer-events”的css3新属性,于是为了搞清楚它的作用,我在网上做了一番搜索,并把自己的认识整理一下。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/43/index.html b/ja/page/43/index.html new file mode 100644 index 0000000000..c0c8632bb4 --- /dev/null +++ b/ja/page/43/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在阅读一些文章时,经常看到介绍命令的时候用到了 npx 关键字,之前知道有 node 有 npm 但是很少用到 npx ,npx是个啥呢?好奇上网上找了些资料学习了一下,写篇学习笔记记录一下。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/44/index.html b/ja/page/44/index.html new file mode 100644 index 0000000000..86a87e265b --- /dev/null +++ b/ja/page/44/index.html @@ -0,0 +1,510 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

市面上目前已有各种各样的UI组件库,比如 Element 和 iView,他们的强大毋庸置疑。但是我们面临的情况是需求越来越复杂,当它们不能再满足我们需求的时候,这个时候就有必要开发一套属于自己团队的组件库了。

+

所以本文的目的就是让读者能通过此文,小能做一个简单的插件供人使用,大能架构和维护一个组件库不在话下。

+

以下一个简单的颜色选择器插件vColorPicker讲述从开发到上线到npm的流程。

+

vColorPicker 插件 DEMO

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/45/index.html b/ja/page/45/index.html new file mode 100644 index 0000000000..163a0eb343 --- /dev/null +++ b/ja/page/45/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

现代前端开发不用点预编译css都不好意思说自己咋写样式,虽说这么说有点夸张,但是确实是一个普遍现象吧,我们在开发中常见到的预编译css的话主要是sass和less,那么今天的主角就是其中的sass,可以说sass让人又爱又恨,爱呢是喜欢她赋予了css函数的特性,恨呢就是用的时候实在是麻烦,所以有的人就投奔了less的怀抱,他俩很像,但是有的项目已经用了sass那也没办法,嫁鸡随鸡,但是日子还得过,想办法过的好一点嘛。

+

其实这里说到sass用起来麻烦主要还是在安装依赖的时候,想必用过的人都知道我说的是啥,原来只能是不停的重试,最近在知乎上看到篇文章,聪明的解决了这个问题,所以赶紧学习记录下来,学会了就是自己的。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/46/index.html b/ja/page/46/index.html new file mode 100644 index 0000000000..fdc37a5413 --- /dev/null +++ b/ja/page/46/index.html @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

知乎真是个好地方,关注好自己喜欢的话题,经常会推送一些好的话题到你面前,这不刚刚就看到了一个人提问

+
+

前端全栈和大前端有啥区别

+
+

以前只听说过全栈,听说过大前端,但是没听说过前端全栈,对于前两个我自己也有一些理解,但从不敢说自己理解的对,带着强烈的好奇心我打开了这个问题,也看到了一位前端大神‘狼叔’的回复,感觉还是很到位的,也让我重新验证了自己的理解,所以就将狼叔的解答与我自己的认识相结合一下,做个记录。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/47/index.html b/ja/page/47/index.html new file mode 100644 index 0000000000..ee704f1bfe --- /dev/null +++ b/ja/page/47/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/48/index.html b/ja/page/48/index.html new file mode 100644 index 0000000000..9394f681e9 --- /dev/null +++ b/ja/page/48/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在检查公司前端代码质量时,经常会在sonar检查到有漏洞级别的问题特别多,但是打开查看详情的时候发现又特别低级,都是些console没去掉啊,debugger没去掉啊,之前都是看到了就提醒一下,但是屡禁不止,那么有没有什么好的办法能够从源头上避免这个问题呢,果然这个问题不止我一个人遇到,网上还是有大神给出了解决方案,那就是 webpack 打包时可以引入 UglifyJsPlugin 来解决这个扰人的问题,而且屏蔽了人为的因素,使用感觉是极好的,所以,写个帖子把网上学来的资料整理一下。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/49/index.html b/ja/page/49/index.html new file mode 100644 index 0000000000..8f516f8f3f --- /dev/null +++ b/ja/page/49/index.html @@ -0,0 +1,506 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

目前,前端领域中 React 势头正盛,使用者众多却少有能够深入剖析内部实现机制和原理。本系列文章希望通过剖析 React 源码,理解其内部的实现原理,知其然更要知其所以然。

+

React diff 作为 Virtual DOM 的加速器,其算法上的改进优化是 React 整个界面渲染的基础,以及性能提高的保障,同时也是 React 源码中最神秘、最不可思议的部分,本文从源码入手,深入剖析 React diff 的不可思议之处。

+

阅读本文需要对 React 有一定的了解,如果你不知何为 React,请详读 React 官方文档
如果你对 React diff 存在些许疑惑,或者你对算法优化感兴趣,那么本文值得阅读和讨论。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/5/index.html b/ja/page/5/index.html new file mode 100644 index 0000000000..7ca5745d30 --- /dev/null +++ b/ja/page/5/index.html @@ -0,0 +1,565 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Nginx中的超时设置

client_header_timeout 指定等待client发送一个请求头的超时时间

    +
  • 语法: client_header_timeout time
  • +
  • 默认值: 60s
  • +
  • 上下文: http server
  • +
  • 说明: 指定等待client发送一个请求头的超时时间(例如:GET / HTTP/1.1).仅当在一次read中,没有收到请求头,才会算成超时。如果在超时时间内,client没发送任何东西,nginx返回HTTP状态码408(“Request timed out”)
  • +
+

client_body_timeout 该指令设置请求体(request body)的读超时时间

    +
  • 语法: client_body_timeout time
  • +
  • 默认值: 60s
  • +
  • 上下文: http server location
  • +
  • 说明: 该指令设置请求体(request body)的读超时时间。仅当在一次readstep中,没有得到请求体,就会设为超时。超时后,nginx返回HTTP状态码408(“Request timed out”)
  • +
+

keepalive_timeout 第一个参数指定了与client的keep-alive连接超时时间

    +
  • 语法: keepalive_timeout timeout [ header_timeout ]

    +
  • +
  • 默认值: 75s

    +
  • +
  • 上下文: http server location

    +
  • +
  • 说明: 第一个参数指定了与client的keep-alive连接超时时间。服务器将会在这个时间后关闭连接。可选的第二个参数指定了在响应头Keep-Alive: timeout=time中的time值。这个头能够让一些浏览器主动关闭连接,这样服务器就不必要去关闭连接了。没有这个参数,nginx不会发送Keep-Alive响应头(尽管并不是由这个头来决定连接是否“keep-alive”)
    两个参数的值可并不相同

    +
      +
    • 注意不同浏览器怎么处理“keep-alive”头
    • +
    • MSIE和Opera忽略掉”Keep-Alive: timeout=“ header.
    • +
    • MSIE保持连接大约60-65秒,然后发送TCP RST
    • +
    • Opera永久保持长连接
    • +
    • Mozilla keeps the connection alive for N plus about 1-10 seconds.
    • +
    • Konqueror保持长连接N秒
    • +
    +
  • +
+

lingering_timeout

    +
  • 语法: lingering_timeout time
  • +
  • 默认值: 5s
  • +
  • 上下文: http server location
  • +
  • 说明: lingering_close生效后,在关闭连接前,会检测是否有用户发送的数据到达服务器,如果超过lingering_timeout时间后还没有数据可读,就直接关闭连接;否则,必须在读取完连接缓冲区上的数据并丢弃掉后才会关闭连接。
  • +
+

resolver_timeout 设置DNS解析超时时间

    +
  • 语法 resolver_timeout time
  • +
  • 默认值 30s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置DNS解析超时时间
  • +
+

proxy_connect_timeout 设置与upstream server的连接超时时间,不能超过75秒

    +
  • 语法 proxy_connect_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置与upstream server的连接超时时间,有必要记住,这个超时不能超过75秒。
    这个不是等待后端返回页面的时间,那是由proxy_read_timeout声明的。如果你的upstream服务器起来了,但是hanging住了(例如,没有足够的线程处理请求,所以把你的请求放到请求池里稍后处理),那么这个声明是没有用的,由于与upstream服务器的连接已经建立了。
  • +
+

proxy_read_timeout 设置与代理服务器的读超时时间

    +
  • 语法 proxy_read_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 该指令设置与代理服务器的读超时时间。它决定了nginx会等待多长时间来获得请求的响应。这个时间不是获得整个response的时间,而是两次reading操作的时间。
  • +
+

proxy_send_timeout 设置发送请求给upstream服务器的超时时间

    +
  • 语法 proxy_send_timeout time
  • +
  • 默认值 60s
  • +
  • 上下文 http server location
  • +
  • 说明 这个指定设置了发送请求给upstream服务器的超时时间。超时设置不是为了整个发送期间,而是在两次write操作期间。如果超时后,upstream没有收到新的数据,nginx会关闭连接
  • +
+

proxy_upstream_fail_timeout(fail_timeout)设置某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间

    +
  • 语法 server address [fail_timeout=30s]
  • +
  • 默认值 10s
  • +
  • 上下文 upstream
  • +
  • 说明 Upstream模块下 server指令的参数,设置了某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间,默认为10秒
  • +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/50/index.html b/ja/page/50/index.html new file mode 100644 index 0000000000..a51b22a4a8 --- /dev/null +++ b/ja/page/50/index.html @@ -0,0 +1,784 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

推荐序

这里是我自己写的,看了这篇文章把我零碎的 webpack 知识系统的整理了一下,感觉受益匪浅,推荐更多小伙伴看一看这篇文章,好文共享,建议稍微对 webpack 了解服用更佳。
2020年了,再不会webpack敲得代码就不香了(近万字实战)

+

前言

2020年即将到来,在众多前端的招聘要求里, webpack 、工程化这些字眼频率越来越高。日常开发者中,我们常常在用诸如 vue-clicreate-react-app 的脚手架来构建我们的项目。但是如果你想在团队脱颖而出(鹤立鸡群)、拿到更好的 offer (还房贷),那么你必须去深刻的认识下我们经常打交道的 webpack

+

入门(一起来用这些小例子让你熟悉webpack的配置)

webpack 是什么?

webpack 是一个现代 JavaScript 应用程序的静态模块打包器,当 webpack 处理应用程序时,会递归构建一个依赖关系图,其中包含应用程序需要的每个模块,然后将这些模块打包成一个或多个 bundle

+

webpack 的核心概念

    +
  • entry: 入口
  • +
  • output: 输出
  • +
  • loader: 模块转换器,用于把模块原内容按照需求转换成新内容
  • +
  • plugins: 扩展插件,在 webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要做的事情
  • +
+

初始化项目

新建一个目录,初始化 npm

+
1
npm init
+ +

webpack 是运行在 node 环境中的,我们需要安装以下两个 npm

+
1
npm i -D webpack webpack-cli
+ +
+
    +
  • npm i -Dnpm install --save-dev 的缩写
  • +
  • npm i -Snpm install --save 的缩写
  • +
+
+

新建一个文件夹 src ,然后新建一个文件 main.js ,写一点代码测试一下

+
1
console.log('call me 老yuan')
+ +

配置 package.json 命令

+
1
2
3
"script":{
"build":"webpack src/main.js"
}
+

执行

+
1
npm run build
+

此时如果生成了一个 dist 文件夹,并且内部含有 main.js 说明已经打包成功了

+

开始我们自己的配置

上面一个简单的例子只是 webpack 自己默认的配置,下面我们要实现更加丰富的自定义配置

+

新建一个 build 文件夹,里面新建一个 webpack.config.js

+
1
2
3
4
5
6
7
8
9
10
11
// webpack.config.js

const path = require('path');
module.exports = {
mode:'development', // 开发模式
entry: path.resolve(__dirname,'../src/main.js'), // 入口文件
output: {
filename: 'output.js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
}
}
+

更改我们的打包命令

+
1
2
3
"script":{
"build":"webpack build/webpack.config.js"
}
+

执行 npm run build
会发现生成了以下目录

+
1
2
3
4
project
dist
build
src
+

其中 dist 文件夹中的 main.js 就是我们需要在浏览器中实际运行的文件
当然实际运用中不会仅仅如此,下面让我们通过实际案例带你快速入手 webpack

+

配置html模板

js 文件打包好了,但是我们不可能每次在html文件中手动引入打包好的js

+
+

这里可能有的朋友会认为我们打包 js 文件名称不是一直是固定的嘛( output.js )?这样每次就不用改动引入文件名称了呀?实际上我们日常开发中往往会这样配置:

+
+
1
2
3
4
5
6
7
module.exports = {
// 省略其他配置
output: {
filename: '[name].[hash:8].js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
}
}
+ +

这时候生成的 dist 目录文件如下

+
1
2
dist/
app.fsafasf.js
+

为了缓存,你会发现打包好的 js 文件的名称每次都不一样。 webpack 打包出来的js文件我们需要引入到 html 中,但是每次我们都手动修改 js 文件名显得很麻烦,因此我们需要一个插件来帮我们完成这件事情,那就是 html-webpack-plugin

+
1
npm i -D html-webpack-plugin
+

新建一个 build 同级的文件夹 public ,里面新建一个 index.html
具体配置文件如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin') // **此插件用来解决html模板引入js文件
module.exports = {
mode:'development', // 开发模式
entry: path.resolve(__dirname,'../src/main.js'), // 入口文件
output: {
filename: '[name].[hash:8].js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
},
plugins:[
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html')
})
]
}
+

可以发现打包生成的js文件已经被自动引入 html 文件中

+

多入口文件如何开发

+

生成多个 html-webpack-plugin 实例来解决这个问题

+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode:'development', // 开发模式
entry: {
main:path.resolve(__dirname,'../src/main.js'),
header:path.resolve(__dirname,'../src/header.js')
},
output: {
filename: '[name].[hash:8].js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
},
plugins:[
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html'),
filename:'index.html',
chunks:['main'] // 与入口文件对应的模块名
}),
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/header.html'),
filename:'header.html',
chunks:['header'] // 与入口文件对应的模块名
}),
]
}
+ +

clean-webpack-plugin

+

每次执行 npm run build 会发现 dist 文件夹里会残留上次打包的文件,这里我们推荐一个 plugin 来帮我们在打包输出前清空文件夹 clean-webpack-plugin

+
+
1
2
3
4
5
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
module.exports = {
// ...省略其他配置
plugins:[new CleanWebpackPlugin()]
}
+
希望dist目录下某个文件夹不被清空

不过呢,有些时候,我们并不希望整个 dist 目录都被清空,比如,我们不希望,每次打包的时候,都删除 dll 目录,以及 dll 目录下的文件或子目录,该怎么办呢?

+

clean-webpack-plugin 为我们提供了参数 cleanOnceBeforeBuildPatterns

+
1
2
3
4
5
6
7
8
9
//webpack.config.js
module.exports = {
//...
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns:['**/*', '!dll', '!dll/**'] //不删除dll目录下的文件
})
]
}
+ +

引用CSS

我们的入口文件是 js ,所以我们在入口 js 中引入我们的 css 文件

+
1
import 'asset/style.css'
+

同时我们也需要一些 loader 来解析我们的 css 文件

+
1
npm i -D style-loader css-loader
+

如果我们使用 less 来构建样式,则需要多安装两个

+
1
npm i -D less less-loader
+

配置文件如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.config.js
module.exports = {
// ...省略其他配置
module:{
rules:[
{
test:/\.css$/,
use:['style-loader','css-loader'] // 从右向左解析原则
},
{
test:/\.less$/,
use:['style-loader','css-loader','less-loader'] // 从右向左解析原则
}
]
}
}
+ +

我们简单说一下上面的配置:

+
    +
  • style-loader 动态创建 style 标签,将 css 插入到 head 中.
  • +
  • css-loader 负责处理 @import 等语句。
  • +
  • postcss-loaderautoprefixer ,自动生成浏览器兼容性前缀 —— 2020了,应该没人去自己徒手去写浏览器前缀了吧
  • +
  • less-loader 负责处理编译 .less 文件,将其转为 css
  • +
+
+

注意:
loader 的执行顺序是从右向左执行的,也就是后面的 loader 先执行,上面 loader 的执行顺序为: less-loader —> postcss-loader —> css-loader —> style-loader
当然,loader 其实还有一个参数,可以修改优先级,enforce 参数,其值可以为: pre(优先执行) 或 post (滞后执行)。
现在,我们已经可以处理 .less 文件啦,.css 文件只需要修改匹配规则,删除 less-loader 即可。

+
+

为css添加浏览器前缀

1
2
npm i -D postcss-loader autoprefixer

+

配置如下

+
1
2
3
4
5
6
7
8
9
// webpack.config.js
module.exports = {
module:{
rules:[
test/\.less$/,
use:['style-loader','css-loader','postcss-loader','less-loader'] // 从右向左解析原则
]
}
}
+

接下来,我们还需要引入 autoprefixer 使其生效,这里有两种方式

+
在项目根目录下创建一个postcss.config.js文件,配置如下:
1
2
3
module.exports = {
plugins: [require('autoprefixer')] // 引用该插件即可了
}
+
直接在webpack.config.js里配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// webpack.config.js
module.exports = {
//...省略其他配置
module:{
rules:[{
test:/\.less$/,
use:['style-loader','css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
},'less-loader'] // 从右向左解析原则
}]
}
}
+

这时候我们发现 css 通过 style 标签的方式添加到了 html 文件中,但是如果样式文件很多,全部添加到 html 中,难免显得混乱。这时候我们想用把 css 拆分出来用外链的形式引入 css 文件怎么做呢?这时候我们就需要借助插件来帮助我们

+

拆分css

1
npm i -D mini-css-extract-plugin
+
+

webpack 4.0 以前,我们通过 extract-text-webpack-plugin 插件,把 css 样式从 js 文件中提取到单独的 css 文件中。 webpack4.0 以后,官方推荐使用 mini-css-extract-plugin 插件来打包 css 文件

+
+

配置文件如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
//...省略其他配置
module: {
rules: [
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader'
],
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].[hash].css",
chunkFilename: "[id].css",
})
]
}
+

拆分多个css

+

这里需要说的细一点,上面我们所用到的 mini-css-extract-plugin 将所有的 css 样式合并为一个 css 文件。如果你想拆分为一一对应的多个 css 文件,我们需要使用到 extract-text-webpack-plugin ,而目前 mini-css-extract-plugin 还不支持此功能。我们需要安装 @next 版本的 extract-text-webpack-plugin

+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
npm i -D extract-text-webpack-plugin@next
// webpack.config.js

const path = require('path');
const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')
let indexLess = new ExtractTextWebpackPlugin('index.less');
let indexCss = new ExtractTextWebpackPlugin('index.css');
module.exports = {
module:{
rules:[
{
test:/\.css$/,
use: indexCss.extract({
use: ['css-loader']
})
},
{
test:/\.less$/,
use: indexLess.extract({
use: ['css-loader','less-loader']
})
}
]
},
plugins:[
indexLess,
indexCss
]
}
+

打包 图片、字体、媒体、等文件

file-loader 就是将文件在进行一些处理后(主要是处理文件名和路径、解析文件 url ),并将文件移动到输出的目录中
url-loader 一般与 file-loader 搭配使用,功能与 file-loader 类似,如果文件小于限制的大小。则会返回 base64 编码,否则使用 file-loader 将文件移动到输出的目录中

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// webpack.config.js
module.exports = {
// 省略其它配置 ...
module: {
rules: [
// ...
{
test: /\.(jpe?g|png|gif)$/i, //图片文件
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]'
}
}
}
}
]
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, //媒体文件
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'media/[name].[hash:8].[ext]'
}
}
}
}
]
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i, // 字体
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'fonts/[name].[hash:8].[ext]'
}
}
}
}
]
},
]
}
}
+

用babel转义js文件

为了使我们的 js 代码兼容更多的环境我们需要安装依赖

+
1
2
npm i babel-loader @babel/preset-env @babel/core

+
+

注意
babel-loaderbabel-core 的版本对应关系

+
+
    +
  • babel-loader 8.x 对应 babel-core 7.x
  • +
  • babel-loader 7.x 对应 babel-core 6.x
  • +
+

配置如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// webpack.config.js
module.exports = {
// 省略其它配置 ...
module:{
rules:[
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:['@babel/preset-env']
}
},
exclude:/node_modules/
},
]
}
}
+

上面的 babel-loader 只会将 ES6/7/8 语法转换为 ES5 语法,但是对新 api 并不会转换 例如( promiseGeneratorSetMapsProxy 等)
此时我们需要借助 babel-polyfill 来帮助我们转换

+
1
2
3
4
5
6
npm i @babel/polyfill
// webpack.config.js
const path = require('path')
module.exports = {
entry: ["@babel/polyfill,path.resolve(__dirname,'../src/index.js')"], // 入口文件
}
+
+

手动把上面的 demo 敲一遍对阅读下面的文章更有益,建议入门的同学敲三遍以上

+
+

上面的实践是我们对 webpack 的功能有了一个初步的了解,但是要想熟练应用于开发中,我们需要一个系统的实战。让我们一起摆脱脚手架尝试自己搭建一个 vue 开发环境

+

搭建vue开发环境

上面的小例子已经帮助而我们实现了打包 css 、图片、 jshtml 等文件。
但是我们还需要以下几种配置

+

解析.vue文件

1
2
npm i -D vue-loader vue-template-compiler vue-style-loader
npm i -S vue
+
    +
  • vue-loader 用于解析 .vue 文件
  • +
  • vue-template-compiler 用于编译模板
  • +
+

配置如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const vueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
module:{
rules:[{
test:/\.vue$/,
use:['vue-loader']
},]
},
resolve:{
alias:{
'vue$':'vue/dist/vue.runtime.esm.js',
' @':path.resolve(__dirname,'../src')
},
extensions:['*','.js','.json','.vue']
},
plugins:[
new vueLoaderPlugin()
]
}
+

配置webpack-dev-server进行热更新

1
npm i -D webpack-dev-server
+

配置如下

+
1
2
3
4
5
6
7
8
9
10
11
12
const Webpack = require('webpack')
module.exports = {
// ...省略其他配置
devServer:{
port:3000,
hot:true,
contentBase:'../dist'
},
plugins:[
new Webpack.HotModuleReplacementPlugin()
]
}
+

完整配置如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// webpack.config.js
const path = require('path');
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')
const vueLoaderPlugin = require('vue-loader/lib/plugin')
const Webpack = require('webpack')
module.exports = {
mode:'development', // 开发模式
entry: {
main:path.resolve(__dirname,'../src/main.js'),
},
output: {
filename: '[name].[hash:8].js', // 打包后的文件名称
path: path.resolve(__dirname,'../dist') // 打包后的目录
},
module:{
rules:[
{
test:/\.vue$/,
use:['vue-loader']
},
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:[
['@babel/preset-env']
]
}
}
},
{
test:/\.css$/,
use: ['vue-style-loader','css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
}]
},
{
test:/\.less$/,
use: ['vue-style-loader','css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
},'less-loader']
}
]
},
resolve:{
alias:{
'vue$':'vue/dist/vue.runtime.esm.js',
' @':path.resolve(__dirname,'../src')
},
extensions:['*','.js','.json','.vue']
},
devServer:{
port:3000,
hot:true,
contentBase:'../dist'
},
plugins:[
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html'),
filename:'index.html'
}),
new vueLoaderPlugin(),
new Webpack.HotModuleReplacementPlugin()
]
}
+

配置打包命令

1
2
3
4
"script":{
"dev":"webpack-dev-server --config build/webpack.config.js --open",
"build":"webpack --config build/webpack.config.js"
}
+

打包文件已经配置完毕,接下来让我们测试一下
首先在 src 新建一个 main.js

+
1
2
3
4
5
6
// main.js
import Vue from 'vue'
import App from './app'
new Vue({
render:h=>h(App)
}).$mount('#app')
+

新建一个 App.vue

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// App.vue
<template>
<div id='container'></div>
</template>
<script>
export default {
data(){
return {
initData:''
}
}
}
</script>
<style scoped>
#container{
width:100%;
height:100%;
}
</style>
+

新建一个 public 文件夹,里面新建一个 index.html

+
1
2
3
4
5
6
7
8
9
10
11
12
13
// index.html
<!DOCTYPE html>
<html lang='en'>
<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'>
<title>lao li</title>
</head>
<body>
<div id='app'></div>
</body>
</html>
+

执行 npm run dev 这时候如果浏览器出现 Vue 开发环境运行成功,那么恭喜你,已经成功迈出了第一步

+

区分开发环境与生产环境

实际应用到项目中,我们需要区分开发环境与生产环境,我们在原来 webpack.config.js 的基础上再新增两个文件

+

webpack.dev.js 开发环境配置文件
开发环境主要实现的是热更新,不要压缩代码,完整的 sourceMap
webpack.prod.js生产环境配置文件
生产环境主要实现的是压缩代码、提取 css 文件、合理的 sourceMap 、分割代码
需要安装以下模块:

+
1
npm i -D  webpack-merge copy-webpack-plugin optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin
+
    +
  • webpack-merge 合并配置
  • +
  • copy-webpack-plugin 拷贝静态资源
  • +
  • optimize-css-assets-webpack-plugin 压缩 css
  • +
  • uglifyjs-webpack-plugin 压缩js
  • +
+
+

webpack mode 设置 production 的时候会自动压缩 js 代码。
原则上不需要引入 uglifyjs-webpack-plugin 进行重复工作。
但是 optimize-css-assets-webpack-plugin 压缩 css 的同时会破坏原有的 js 压缩,所以这里我们引入 uglifyjs 进行压缩

+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// webpack.config.js
const path = require('path')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const vueLoaderPlugin = require('vue-loader/lib/plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const devMode = process.argv.indexOf('--mode=production') === -1;
module.exports = {
entry:{
main:path.resolve(__dirname,'../src/main.js')
},
output:{
path:path.resolve(__dirname,'../dist'),
filename:'js/[name].[hash:8].js',
chunkFilename:'js/[name].[hash:8].js'
},
module:{
rules:[
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:['@babel/preset-env']
}
},
exclude:/node_modules/
},
{
test:/\.vue$/,
use:['cache-loader','thread-loader',{
loader:'vue-loader',
options:{
compilerOptions:{
preserveWhitespace:false
}
}
}]
},
{
test:/\.css$/,
use:[{
loader: devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
options:{
publicPath:"../dist/css/",
hmr:devMode
}
},'css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
}]
},
{
test:/\.less$/,
use:[{
loader:devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
options:{
publicPath:"../dist/css/",
hmr:devMode
}
},'css-loader','less-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')]
}
}]
},
{
test:/\.(jep?g|png|gif)$/,
use:{
loader:'url-loader',
options:{
limit:10240,
fallback:{
loader:'file-loader',
options:{
name:'img/[name].[hash:8].[ext]'
}
}
}
}
},
{
test:/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
use:{
loader:'url-loader',
options:{
limit:10240,
fallback:{
loader:'file-loader',
options:{
name:'media/[name].[hash:8].[ext]'
}
}
}
}
},
{
test:/\.(woff2?|eot|ttf|otf)(\?.*)?$/i,
use:{
loader:'url-loader',
options:{
limit:10240,
fallback:{
loader:'file-loader',
options:{
name:'media/[name].[hash:8].[ext]'
}
}
}
}
}
]
},
resolve:{
alias:{
'vue$':'vue/dist/vue.runtime.esm.js',
' @':path.resolve(__dirname,'../src')
},
extensions:['*','.js','.json','.vue']
},
plugins:[
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'../public/index.html')
}),
new vueLoaderPlugin(),
new MiniCssExtractPlugin({
filename: devMode ? '[name].css' : '[name].[hash].css',
chunkFilename: devMode ? '[id].css' : '[id].[hash].css'
})
]
}
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// webpack.dev.js
const Webpack = require('webpack')
const webpackConfig = require('./webpack.config.js')
const WebpackMerge = require('webpack-merge')

module.exports = WebpackMerge(webpackConfig,{
mode:'development',
devtool:'cheap-module-eval-source-map',
devServer:{
port:3000,
hot:true,
contentBase:'../dist'
},
plugins:[
new Webpack.HotModuleReplacementPlugin()
]
})
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// webpack.prod.js
const path = require('path')
const webpackConfig = require('./webpack.config.js')
const WebpackMerge = require('webpack-merge')

const CopyWebpackPlugin = require('copy-webpack-plugin')
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

module.exports = WebpackMerge(webpackConfig,{
mode:'production',
devtool:'cheap-module-source-map',
plugins:[
new CopyWebpackPlugin([{
from:path.resolve(__dirname,'../public'),
to:path.resolve(__dirname,'../dist')
}]),
],
optimization:{
minimizer:[
new UglifyJsPlugin({//压缩js
cache:true,
parallel:true,
sourceMap:true
}),
new OptimizeCssAssetsPlugin({})
],
splitChunks:{
chunks:'all',
cacheGroups:{
libs: {
name: "chunk-libs",
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: "initial" // 只打包初始时依赖的第三方
}
}
}
}
})
+ +

优化webpack配置

看到这里你或许有些累了,但是要想获取更好的offer,更高的薪水,下面必须继续深入

+

优化配置对我们来说非常有实际意义,这实际关系到你打包出来文件的大小,打包的速度等。
具体优化可以分为以下几点:

+

优化打包速度

+

构建速度指的是我们每次修改代码后热更新的速度以及发布前打包文件的速度。

+
+

合理的配置 mode 参数与 devtool 参数

devtool 可设置的值
mode 可设置 development production 两个参数

+

如果没有设置, webpack4 会将 mode 的默认值设置为 production

+
    +
  • production :将 process.env.NODE_ENV 的值设置为 production ,启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPluginUglifyJsPlugin,会进行 tree shaking (去除无用代码)和 uglifyjs (代码压缩混淆)
  • +
  • development :将 process.env.NODE_ENV 的值设置为 development ,启用 NamedChunksPluginNamedModulesPlugin
  • +
+

缩小文件的搜索范围(配置include exclude alias noParse extensions)

    +
  • alias 当我们代码中出现 import ‘vue’时, webpack会采用向上递归搜索的方式去node_modules 目录下找。为了减少搜索范围我们可以直接告诉webpack去哪个路径下查找。也就是别名(alias)的配置。
  • +
  • include exclude 同样配置include exclude也可以减少webpack loader的搜索转换时间。
  • +
  • noParse 当我们代码中使用到 import jq from 'jquery' 时, webpack 会去解析 jq 这个库是否有依赖其他的包。但是我们对类似 jquery 这类依赖库,一般会认为不会引用其他的包(特殊除外,自行判断)。增加 noParse 属性,告诉 webpack 不必解析,以此增加打包速度。
  • +
  • extensions webpack 会根据 extensions 定义的后缀查找文件(频率较高的文件类型优先写在前面)
  • +
+

配图

+

使用HappyPack开启多进程Loader转换

+

webpack 构建过程中,实际上耗费时间大多数用在 loader 解析转换以及代码的压缩中。日常开发中我们需要使用 Loaderjscss ,图片,字体等文件做转换操作,并且转换的文件数据量也是非常大。由于js单线程的特性使得这些转换操作不能并发处理文件,而是需要一个个文件进行处理。 HappyPack 的基本原理是将这部分任务分解到多个子进程中去并行处理,子进程处理完成后把结果发送到主进程中,从而减少总的构建时间

+
+
1
npm i -D happypack
+ +

happypack

+

使用 webpack-parallel-uglify-plugin 增强代码压缩

上面对于 loader 转换已经做优化,那么下面还有另一个难点就是优化代码的压缩时间。

+
1
npm i -D webpack-parallel-uglify-plugin
+

webpack-parallel-uglify-plugin

+

抽离第三方模块

+

对于开发项目中不经常会变更的静态依赖文件。类似于我们的 elementUivue 全家桶等等。因为很少会变更,所以我们不希望这些依赖要被集成到每一次的构建逻辑中去。 这样做的好处是每次更改我本地代码的文件的时候, webpack 只需要打包我项目本身的文件代码,而不会再去编译第三方库。以后只要我们不升级第三方包的时候,那么 webpack 就不会对这些库去打包,这样可以快速的提高打包的速度。

+
+

这里我们使用 webpack 内置的 DllPlugin DllReferencePlugin 进行抽离

+

在与 webpack 配置文件同级目录下新建 webpack.dll.config.js
代码如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// webpack.dll.config.js
const path = require("path");
const webpack = require("webpack");
module.exports = {
// 你想要打包的模块的数组
entry: {
vendor: ['vue','element-ui']
},
output: {
path: path.resolve(__dirname, 'static/js'), // 打包后文件输出的位置
filename: '[name].dll.js',
library: '[name]_library'
// 这里需要和webpack.DllPlugin中的`name: '[name]_library',`保持一致。
},
plugins: [
new webpack.DllPlugin({
path: path.resolve(__dirname, '[name]-manifest.json'),
name: '[name]_library',
context: __dirname
})
]
};
+

package.json 中配置如下命令

+
1
"dll": "webpack --config build/webpack.dll.config.js"
+ +

接下来在我们的 webpack.config.js 中增加以下代码

+
1
2
3
4
5
6
7
8
9
10
11
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./vendor-manifest.json')
}),
new CopyWebpackPlugin([ // 拷贝生成的文件到dist目录 这样每次不必手动去cv
{from: 'static', to:'static'}
]),
]
};
+

执行

+
1
npm run dll
+ +

会发现生成了我们需要的集合第三地方
代码的 vendor.dll.js
我们需要在html文件中手动引入这个js文件

+
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<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">
<title>老yuan</title>
<script src="static/js/vendor.dll.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
+

这样如果我们没有更新第三方依赖包,就不必 npm run dll 。直接执行 npm run dev npm run build 的时候会发现我们的打包速度明显有所提升。因为我们已经通过 dllPlugin 将第三方依赖包抽离出来了。

+

配置缓存

+

我们每次执行构建都会把所有的文件都重复编译一遍,这样的重复工作是否可以被缓存下来呢,答案是可以的,目前大部分 loader 都提供了 cache 配置项。比如在 babel-loader 中,可以通过设置cacheDirectory 来开启缓存,babel-loader?cacheDirectory=true 就会将每次的编译结果写进硬盘文件(默认是在项目根目录下的 node_modules/.cache/babel-loader 目录内,当然你也可以自定义)
但如果 loader 不支持缓存呢?我们也有方法,我们可以通过 cache-loader ,它所做的事情很简单,就是 babel-loader 开启 cache 后做的事情,将 loader 的编译结果写入硬盘缓存。再次构建会先比较一下,如果文件较之前的没有发生变化则会直接使用缓存。使用方法如官方 demo 所示,在一些性能开销较大的 loader 之前添加此 loader 即可

+
+
1
npm i -D cache-loader
+

cache-loader

+

优化打包文件体积

打包的速度我们是进行了优化,但是打包后的文件体积却是十分大,造成了页面加载缓慢,浪费流量等,接下来让我们从文件体积上继续优化

+

引入webpack-bundle-analyzer分析打包后的文件

webpack-bundle-analyzer 将打包后的内容束展示为方便交互的直观树状图,让我们知道我们所构建包中真正引入的内容

+
1
npm i -D webpack-bundle-analyzer
+

webpack-bundle-analyzer

+

接下来在 package.json 里配置启动命令

+
1
"analyz": "NODE_ENV=production npm_config_report=true npm run build"
+

windows 请安装 npm i -D cross-env

+
1
"analyz": "cross-env NODE_ENV=production npm_config_report=true npm run build"
+

接下来 npm run analyz 浏览器会自动打开文件依赖图的网页

+

externals

+

按照官方文档的解释,如果我们想引用一个库,但是又不想让 webpack 打包,并且又不影响我们在程序中以 CMDAMD 或者 window/global 全局等方式进行使用,那就可以通过配置 Externals 。这个功能主要是用在创建一个库的时候用的,但是也可以在我们项目开发中充分使用Externals 的方式,我们将这些不需要打包的静态资源从构建逻辑中剔除出去,而使用 CDN的方式,去引用它们。
有时我们希望我们通过 script 引入的库,如用 CDN 的方式引入的 jquery ,我们在使用时,依旧用 require 的方式来使用,但是却不希望 webpack 将它又编译进文件中。这里官网案例已经足够清晰明了,大家有兴趣可以点击了解
webpack
官网案例如下

+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
<script
src="https://code.jquery.com/jquery-3.1.0.js"
integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
crossorigin="anonymous">
</script>
module.exports = {
//...
externals: {
jquery: 'jQuery'
}
};
import $ from 'jquery';
$('.my-element').animate(/* ... */);
+

Tree-shaking

+

这里单独提一下 tree-shaking ,是因为这里有个坑。 tree-shaking 的主要作用是用来清除代码中无用的部分。目前在 webpack4 我们设置 modeproduction 的时候已经自动开启了tree-shaking 。但是要想使其生效,生成的代码必须是ES6模块。不能使用其它类型的模块如 CommonJS 之流。如果使用 Babel 的话,这里有一个小问题,因为 Babel 的预案( preset )默认会将任何模块类型都转译成 CommonJS 类型。修正这个问题也很简单,在 .babelrc 文件或在 webpack.config.js 文件中设置 modules:false 就好了

+
+
1
2
3
4
5
6
7
8
9
10
// .babelrc
{
"presets": [
["@babel/preset-env",
{
"modules": false
}
]
]
}
+

或者

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.config.js

module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', { modules: false }]
}
},
exclude: /(node_modules)/
}
]
}
+ +

经历过上面两个系列的洗礼,到现在我们成为了一名合格的 webpack 配置工程师。但是光拧螺丝,自身的可替代性还是很高,下面我们将深入 webpack 的原理中去

+

手写webpack系列

经历过上面两个部分,我们已经可以熟练的运用相关的 loaderplugin 对我们的代码进行转换、解析。接下来我们自己手动实现 loaderplugin ,使其在平时的开发中获得更多的乐趣。

+

手写 webpack loader

+

loader 从本质上来说其实就是一个 node 模块。相当于一台榨汁机( loader )将相关类型的文件代码( code )给它。根据我们设置的规则,经过它的一系列加工后还给我们加工好的果汁( code )。

+
+

loader 编写原则

    +
  • 单一原则: 每个 Loader 只做一件事;
  • +
  • 链式调用: Webpack 会按顺序链式调用每个 Loader
  • +
  • 统一原则: 遵循 Webpack 制定的设计规则和结构,输入与输出均为字符串,各个 Loader 完全独立,即插即用;
  • +
+

在日常开发环境中,为了方便调试我们往往会加入许多 console 打印。但是我们不希望在生产环境中存在打印的值。那么这里我们自己实现一个 loader 去除代码中的 console

+
+

知识点普及之 ASTAST 通俗的来说,假设我们有一个文件 a.js ,我们对 a.js 里面的1000行进行一些操作处理,比如为所有的 await 增加 try catch ,以及其他操作,但是 a.js 里面的代码本质上来说就是一堆字符串。那我们怎么办呢,那就是转换为带标记信息的对象(抽象语法树)我们方便进行增删改查。这个带标记的对象(抽象语法树)就是AST。这里推荐一篇不错的AST文章 AST快速入门

+
+
1
npm i -D @babel/parser @babel/traverse @babel/generator @babel/types
+
    +
  • @babel/parser 将源代码解析成 AST
  • +
  • @babel/traverse 对 AST 节点进行递归遍历,生成一个便于操作、转换的 path 对象
  • +
  • @babel/generator 将 AST 解码生成 js 代码
  • +
  • @babel/types 通过该模块对具体的 AST 节点进行进行增、删、改、查
  • +
+

新建 drop-console.js

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const t = require('@babel/types')
module.exports=function(source){
const ast = parser.parse(source,{ sourceType: 'module'})
traverse(ast,{
CallExpression(path){
if(t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.object, {name: "console"})){
path.remove()
}
}
})
const output = generator(ast, {}, source);
return output.code
}
+

如何使用

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const path = require('path')
module.exports = {
mode:'development',
entry:path.resolve(__dirname,'index.js'),
output:{
filename:'[name].[contenthash].js',
path:path.resolve(__dirname,'dist')
},
module:{
rules:[{
test:/\.js$/,
use:path.resolve(__dirname,'drop-console.js')
}
]
}
}
+
+

实际上在 webpack4 中已经集成了去除 console 功能,在 minimizer 中可配置 去除console
附上官网 如何编写一个loader

+
+

手写webpack plugin

+

Webpack 运行的生命周期中会广播出许多事件, Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
通俗来说:一盘美味的 盐豆炒鸡蛋 需要经历烧油 炒制 调味到最后的装盘等过程,而 plugin 相当于可以监控每个环节并进行操作,比如可以写一个少放胡椒粉 plugin ,监控 webpack 暴露出的生命周期事件(调味),在调味的时候执行少放胡椒粉操作。
那么它与 loader 的区别是什么呢?上面我们也提到了 loader 的单一原则, loader 只能一件事,比如说 less-loader ,只能解析 less 文件, plugin 则是针对整个流程执行广泛的任务。

+
+

一个基本的 plugin 插件结构如下

+
1
2
3
4
5
6
7
8
9
10
11
12
class firstPlugin {
constructor (options) {
console.log('firstPlugin options', options)
}
apply (compiler) {
compiler.plugin('done', compilation => {
console.log('firstPlugin')
))
}
}

module.exports = firstPlugin
+
+

compilercompilation 是什么?

+
+
    +
  • compiler 对象包含了 Webpack 环境所有的的配置信息。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 optionsloaderplugin 。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。
  • +
  • compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation ,从而生成一组新的编译资源。 compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。
  • +
+

compilercompilation 的区别在于

+
    +
  • compiler 代表了整个 webpack 从启动到关闭的生命周期,而 compilation 只是代表了一次新的编译过程
  • +
  • compilercompilation 暴露出许多钩子,我们可以根据实际需求的场景进行自定义处理
  • +
+

compiler钩子文档
compilation钩子文档

+

下面我们手动开发一个简单的需求,在生成打包文件之前自动生成一个关于打包出文件的大小信息
新建一个 webpack-firstPlugin.js

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class firstPlugin{
constructor(options){
this.options = options
}
apply(compiler){
compiler.plugin('emit',(compilation,callback)=>{
let str = ''
for (let filename in compilation.assets){
str += `文件:${filename} 大小${compilation.assets[filename]['size']()}\n`
}
// 通过compilation.assets可以获取打包后静态资源信息,同样也可以写入资源
compilation.assets['fileSize.md'] = {
source:function(){
return str
},
size:function(){
return str.length
}
}
callback()
})
}
}
module.exports = firstPlugin
+

如何使用

+
1
2
3
4
5
6
7
8
const path = require('path')
const firstPlugin = require('webpack-firstPlugin.js')
module.exports = {
// 省略其他代码
plugins:[
new firstPlugin()
]
}
+

执行 npm run build 即可看到在 dist 文件夹中生成了一个包含打包文件信息的 fileSize.md

+
+

上面两个 loaderplugin 案例只是一个引导,实际开发需求中的 loaderplugin 要考虑的方面很多,建议大家自己多动手尝试一下。

+
+

附上官网 如何编写一个plugin

+

webpack5.0的时代

无论是前端框架还是构建工具的更新速度远远超乎了我们的想象,前几年的 jquery 一把梭的时代一去不复返。我们要拥抱的是不断更新迭代的 vuereactnodeserverlessdockerk8s ….
不甘落后的 webpack 也已经在近日发布了 webpack 5.0.0 beta 10 版本。在之前作者也曾提过 webpack5.0 旨在减少配置的复杂度,使其更容易上手( webpack4 的时候也说了这句话),以及一些性能上的提升

+
    +
  • 使用持久化缓存提高构建性能;
  • +
  • 使用更好的算法和默认值改进长期缓存(long-term caching);
  • +
  • 清理内部结构而不引入任何破坏性的变化;
  • +
  • 引入一些breaking changes,以便尽可能长的使用v5版本。
  • +
+

目前来看,维护者的更新很频繁,相信用不了多久webpack5.0将会拥抱大众。感兴趣的同学可以先安装beta版本尝尝鲜。不过在此之前建议大家先对webpack4进行一番掌握,这样后面的路才会越来越好走。

+

更多阅读

webpack中文
webpackjs
4W字长文带你深度解锁Webpack系列(上)

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/51/index.html b/ja/page/51/index.html new file mode 100644 index 0000000000..a3eec84141 --- /dev/null +++ b/ja/page/51/index.html @@ -0,0 +1,531 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

原版

先来看一个call实例,看看call到底做了什么:

+
1
2
3
4
5
6
7
let foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call(foo); // 1
+

从代码的执行结果,我们可以看到,call首先改变了this的指向,使函数的this指向了foo,然后使bar函数执行了。
总结一下:

+
    +
  • call改变函数this指向
  • +
  • 调用函数
  • +
+

自己动手

    +
  1. 首先我们对参数 context 做了兼容处理,不传值, context 默认值为 window
  2. +
  3. 然后我们将函数挂载到 context 上面, context.fn = this
  4. +
  5. 处理参数,将传入 myCall 的参数截取,去除第一位,然后转为数组;
  6. +
  7. 调用 context.fn ,此时 fnthis 指向 context
  8. +
  9. 删除对象上的属性 delete context.fn
  10. +
  11. 将结果返回。
  12. +
+
1
2
3
4
5
6
7
8
Function.prototype.myCall = function(context) {
context = context || window;
context.fn = this; // 将函数挂载到对象的fn属性上
const args = [...arguments].slice(1); // 处理传入的参数
const result = context.fn(...args); // 通过对象的属性调用该方法
delete context.fn; // 删除该属性
return result // 返回结果
};
+

applycall 的区别在于参数, 其他没有差别,实现如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// myApply的参数形式为(obj,[arg1,arg2,arg3]);
// 所以myApply的第二个参数为[arg1,arg2,arg3]
// 这里我们用扩展运算符来处理一下参数的传入方式
Function.prototype.myApply = function(context) {
context = context || window
context.fn = this
let result
if (arguments[1]) { // 判断是否有第二个参数
result = context.fn(…arguments[1]) // 有的话传入执行
} else {
result = context.fn() // 没有的话空参执行
}
delete context.fn;
return result
};
+

bindcallapply 作用都是改变 this 的指向,区别在于 bind 改变后不会立即执行,而 callapply 会立即执行,我们看一下 bind 的用法

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(){
this.name="zs";
this.age=18;
this.gender="男"
}
let obj={
hobby:"看书"
}

let changePerson = Person.bind(obj); // 将构造函数的this绑定为obj
changePerson(); // 直接调用构造函数,函数会操作obj对象,给其添加三个属性;
console.log(obj); // => {hobby:"看书",name:"zs",age:18,gender:'男'}
let p = new changePerson(); // 用改变了this 指向的构造函数,new一个实例出来
console.log(p); // => Person {name:"zs",age:18,gender:'男'}
+

仔细观察上面的代码,再看输出结果。

+

我们对 Person 类使用了 bind 将其 this 指向 obj ,得到了 changePerson 函数,此处如果我们直接调用 changeperson 会改变 obj ,若用 new 调用 changeperson 会得到实例 p,并且其 __proto__ 指向 Person ,我们发现 bind 失效了。

+

我们得到结论:bind 改变了 this 指向的函数,如果用 new 操作符来调用, bind 将会失效

+

这个对象就是这个构造函数的实例,那么只要在函数内部执行 * this instanceof 构造函数 * 来判断其结果是否为 true ,就能判断函数是否是通过 new 操作符来调用了,若结果为 true 则是用 new 操作符调用的,总结如下:

+
    +
  1. 保存当前 this 指向
  2. +
  3. 保存环境上下文
  4. +
  5. 保存参数,去掉第一个对象参数
  6. +
  7. 返回待执行函数
      +
    1. 数组化剩余参数
    2. +
    3. 判断是否为构造函数
    4. +
    5. 若是执行构造函数,若不是改变 this 指向执行
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      // bind实现

      Function.prototype.myBind = function(context){
      let _this = this; // 1、保存函数
      context = context || window; // 2、保存目标对象
      let rest = [...arguments].slice(1); // 3、保存目标对象之外的参数,将其转化为数组;
      // 此处开始与 call 和 apply 不同,不是返回结果,而是返回一个函数
      return function F(){ // 4、返回一个待执行的函数
      let rest2 = Array.prototype.slice.call(arguments) // 5、这里的arguments是F函数的参数,转换为数组;
      if(this instanceof F){
      return new _this(...rest2) // 6、若是用new操作符调用,则直接用new 调用原函数,并用扩展运算符传递参数
      }else{
      _this.apply(context,rest.concat(rest2)); // 7、用apply调用第一步保存的函数,并绑定this,传递合并的参数数组,
      // 即context._this(rest.concat(rest2))
      }
      }
      };
    6. +
    +
  8. +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/52/index.html b/ja/page/52/index.html new file mode 100644 index 0000000000..e6e0438b1a --- /dev/null +++ b/ja/page/52/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

原理

就是利用 generator (生成器)分割代码片段。然后我们使用一个函数让其自迭代,每一个 yieldpromise 包裹起来。执行下一步的时机由 promise 来控制

+

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function _asyncToGenerator(fn) {
return function() {
var self = this, args = arguments;
return new Promise(function(resolve, reject) { // 将返回值 promise 化
var gen = fn.apply(self, args); // 获取迭代器实例
function _next(value) { // 执行下一步
asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
}
function _throw(err) { // 抛出异常
asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
}
_next(undefined); // 第一次触发
});
};
}
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/53/index.html b/ja/page/53/index.html new file mode 100644 index 0000000000..2f6161fa63 --- /dev/null +++ b/ja/page/53/index.html @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

实现一个 EventEmitter

    +
  1. 创建一个 Event 类,包含构造函数、绑定、解绑、触发方法
  2. +
  3. on 监听event事件,事件触发时调用fn函数。根据字典创建事件数组,如果事件处理不存在,那么就推入数组,反之返回原数组
  4. +
  5. off 从字典中获取当前事件数组,如果获取值是数组,那么判断解绑哪个事件,如果没填清空全部(数组长度置为零),否则从事件数组删除选择的事件
  6. +
  7. emit 触发event事件,并把参数arg1,arg2,arg3….传给事件处理函数,跟解绑外层判断一样,内层把删除改为执行即可
  8. +
  9. once 为指定事件注册一个单次监听器,单次监听器最多只触发一次,触发后立即解除监听器。
  10. +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class Event {
constructor () {
// 储存事件的数据结构
// 为查找迅速, 使用对象(字典)
this._cache = {}
}

// 绑定
on(event, callback) {
// 为了按类查找方便和节省空间
// 将同一类型事件放到一个数组中
// 这里的数组是队列, 遵循先进先出
// 即新绑定的事件先触发
let fns = (this._cache[event] = this._cache[event] || [])
if(fns.indexOf(callback) === -1) {
fns.push(callback)
}
return this
}

// 解绑
off (event, callback) {
let fns = this._cache[event]
if(Array.isArray(fns)) {
if(callback) {
let index = fns.indexOf(callback)
if(index !== -1) {
fns.splice(index, 1)
}
} else {
// 全部清空
fns.length = 0
}
}
return this
}
// 触发emit
emit(event, ...args) {
let fns = this._cache[event]
if(Array.isArray(fns)) {
fns.forEach((fn) => {
fn(...args)
})
}
return this
}

// 一次性绑定
once(event, callback) {
let onceCallback = () => { // 定义一个只执行一次就解绑的方法
callback.call(this); // 使用call改变this指向
this.off(event, onceCallback); // 解绑
};
this.on(event, onceCallback); // 绑定
return this;
}
}
+

好的接下来我们调用一下

+
1
2
3
4
5
6
7
8

let e = new Event()

e.on('click',function(){
console.log('on')
})
// e.trigger('click', '666')
console.log(e)
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/54/index.html b/ja/page/54/index.html new file mode 100644 index 0000000000..a29dca1a2f --- /dev/null +++ b/ja/page/54/index.html @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

Vue 2.x 的 Object.defineProperty 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 数据
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');

// 数据劫持
Object.defineProperty(data, 'text', {
// 数据变化 —> 修改视图
set(newVal) {
input.value = newVal;
span.innerHTML = newVal;
}
});

// 视图更改 --> 数据变化
input.addEventListener('keyup', function(e) {
data.text = e.target.value;
});
+ +

Vue 3.x 的 proxy 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 数据
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');

// 数据劫持
const handler = {
set(target, key, value) {
target[key] = value;
// 数据变化 —> 修改视图
input.value = value;
span.innerHTML = value;
return value;
}
};
const proxy = new Proxy(data, handler);

// 视图更改 --> 数据变化
input.addEventListener('keyup', function(e) {
proxy.text = e.target.value;
});
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/55/index.html b/ja/page/55/index.html new file mode 100644 index 0000000000..6be2a8988d --- /dev/null +++ b/ja/page/55/index.html @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

原理

先看看 reducemap 的使用方法

+
1
2
let new_array = arr.map(function callback(currentValue[, index[,array) {/* Return element for new_array */ }[, thisArg])
let result = arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
+ +

实现

第一种用 for 实现

+
1
2
3
4
5
6
7
Array.prototype.myMap = function(callback, thisArg) {
let arr = [];
for (let i = 0; i < this.length; i++) {
arr.push(callback.call(thisArg, this[i], i, this));
}
return arr;
};
+

第二种用 reduce 实现

+
1
2
3
4
5
6
7
Array.prototype.myMap = function(callback, thisArg) {
let result = this.reduce((accumulator, currentValue, index, array) => {
accumulator.push(callback.call(thisArg, currentValue, index, array));
return accumulator;
}, []);
return result;
};
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/56/index.html b/ja/page/56/index.html new file mode 100644 index 0000000000..5189673d1b --- /dev/null +++ b/ja/page/56/index.html @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

实现一个 Object.create() 方法

    +
  1. 创建一个空匿名函数
  2. +
  3. 函数原型对象指向传入对象实例
  4. +
  5. 返回构造函数创建的实例
  6. +
+
1
2
3
4
5
function create =  function (o) {
var F = function () {};
F.prototype = o;
return new F();
};
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/57/index.html b/ja/page/57/index.html new file mode 100644 index 0000000000..cb32356f60 --- /dev/null +++ b/ja/page/57/index.html @@ -0,0 +1,496 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<ul id="color-list">
<li>red</li>
<li>yellow</li>
<li>blue</li>
<li>green</li>
<li>black</li>
<li>white</li>
</ul>

<script>
(function () {
var color_list = document.getElementById('color-list');
color_list.addEventListener('click', showColor, true);
function showColor(e) {
var x = e.target;
if (x.nodeName.toLowerCase() === 'li') {
alert(x.innerHTML);
}
}
})();
</script>
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/58/index.html b/ja/page/58/index.html new file mode 100644 index 0000000000..b9c0809d4c --- /dev/null +++ b/ja/page/58/index.html @@ -0,0 +1,500 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

原理

将多层数组扁平化

+

实现

1
2
3
4
5
6
7
8
9
10
11
Array.prototype.myFlat = function() {
var arr = [];
this.forEach((item)=>{
if(Array.isArray(item)){
arr = arr.concat(item.myFlat()); // 如果是数组的话继续循环
}else{
arr.push(item)
}
})
return arr
};
+ +

还有另外一种实现方式,非常好用

+
1
2
3
4
5
Array.prototype.myFlat = function() {
return this.toString() // => "1,2,3,4"
.split(",") // => ["1", "2", "3", "4"]
.map(item => +item); // => [1, 2, 3, 4]
};
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/59/index.html b/ja/page/59/index.html new file mode 100644 index 0000000000..bfff706568 --- /dev/null +++ b/ja/page/59/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

实现一个Array.isArray

思路很简单,就是利用 Object.prototype.toString

+
1
2
3
Array.myIsArray = function(o) { 
return Object.prototype.toString.call(Object(o)) === '[object Array]';
};
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/6/index.html b/ja/page/6/index.html new file mode 100644 index 0000000000..6553956dc5 --- /dev/null +++ b/ja/page/6/index.html @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

Docker 镜像安全最佳实践

Docker 和 宿主机 的设置

    +
  1. 保证宿主机和 Docker 的版本是最新的
  2. +
  3. 不要暴露 Docker 的 守护进程(daemon) 的套接字
  4. +
  5. 使用 rootless 模式启动 Docker
  6. +
  7. 避免使用特权容器
  8. +
  9. 限制容器资源
  10. +
  11. 隔离容器网络
  12. +
  13. 提高容器的隔离度
  14. +
  15. 将文件系统和卷设置为只读
  16. +
  17. 完整的生命周期管理
  18. +
  19. 限制来自容器内的系统调用
  20. +
+

确保镜像安全

    +
  1. 扫描和验证容器镜像
  2. +
  3. 使用最小基础镜像
  4. +
  5. 不要向 Docker 镜像泄露敏感信息
  6. +
  7. 使用多阶段构建
  8. +
  9. 确保容器注册
  10. +
  11. 使用固定标签以获得不变性
  12. +
+

监控容器

    +
  1. 监控容器活动
  2. +
  3. 确保容器在运行时的安全
  4. +
  5. 将故障排除数据与容器分开保存
  6. +
  7. 为镜像使用元数据标签
  8. +
+

参考

Top 20 Dockerfile best practices
Dockerセキュリティベストプラクティス トップ20

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/60/index.html b/ja/page/60/index.html new file mode 100644 index 0000000000..86b03b0589 --- /dev/null +++ b/ja/page/60/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

实现一个new操作符

我们首先知道new做了什么:

+
    +
  1. 创建一个空的简单 JavaScript 对象(即{})
  2. +
  3. 链接该对象(即设置该对象的构造函数)到另一个对象
  4. +
  5. 将步骤(1)新创建的对象作为 this 的上下文
  6. +
  7. 如果该函数没有返回对象,则返回 this
  8. +
+

知道new做了什么,接下来我们就来实现它

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function create(){
// 创建一个空的对象
let obj = {};
// 获得构造函数
let Con = [].shift.call(arguments)
// 将空对象指向构造函数的原型链
Object.setPrototypeOf(obj, Con.prototype);
// obj.__proto__ = Con.prototype // 链接到原型
// obj绑定到构造函数上,便可以访问构造函数中的属性,即this.obj.Con(args)
let result = Con.apply(obj, arguments);
// 如果返回的result是一个对象则返回
// new方法失效,否则返回obj
return result instanceof Object ? result : this.obj;
// return typeof result === 'object' ? result : obj// 确保 new 出来的是个对象
}
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/61/index.html b/ja/page/61/index.html new file mode 100644 index 0000000000..ba5a3c9b63 --- /dev/null +++ b/ja/page/61/index.html @@ -0,0 +1,500 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

原版

1
Array.prototype.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
+ +

自己动手

1
2
3
4
5
6
7
8
Array.prototype.myReduce = function(callback, initialValue) {
let accumulator = initialValue ? initialValue : this[0]; // 第一次使用判断时候有initialValue参数,如果有用他,没有用this[0],这里this指的是传入的数组,this[0]就是数组第一项
for (let i = initialValue ? 0 : 1; i < this.length; i++) { // 如果有初始值从0开始循环,不然从1开始
let _this = this; // 保留当前this指向
accumulator = callback(accumulator, this[i], i, _this); //
}
return accumulator; // 返回迭代器的终值
};
+

试用一下

+
1
2
3
4
5
6
7
let arr = [1, 2, 3, 4];
let sum = arr.myReduce((acc, val) => {
acc += val;
return acc;
}, 5);

console.log(sum); // 15
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/62/index.html b/ja/page/62/index.html new file mode 100644 index 0000000000..a953162a55 --- /dev/null +++ b/ja/page/62/index.html @@ -0,0 +1,500 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

path.join 与 path.resolve 的区别

    +
  1. 对于以/开始的路径片段,path.join只是简单的将该路径片段进行拼接,而path.resolve将以/开始的路径片段作为根目录,在此之前的路径将会被丢弃,就像是在terminal中使用cd命令一样。
  2. +
+
1
2
path.join('/a', '/b') // 'a/b'
path.resolve('/a', '/b') // '/b'
+
    +
  1. path.resolve总是返回一个以相对于当前的工作目录(working directory)的绝对路径。
  2. +
+
1
2
path.join('./a', './b') // 'a/b'
path.resolve('./a', './b') // '/Users/username/Projects/webpack-demo/a/b'
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/63/index.html b/ja/page/63/index.html new file mode 100644 index 0000000000..14cd65b14b --- /dev/null +++ b/ja/page/63/index.html @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

使用 Alibaba 的 Homebrew 镜像源进行加速

平时我们执行 brew 命令安装软件的时候,跟以下 3 个仓库地址有关:

+
    +
  • brew.git
  • +
  • homebrew-core.git
  • +
  • homebrew-bottles
    通过以下操作将这 3 个仓库地址全部替换为 Alibaba 提供的地址
  • +
+

1. 替换 / 还原 brew.git 仓库地址

1
2
3
4
5
6
7
8
9
# 替换成阿里巴巴的 brew.git 仓库地址:
cd "$(brew --repo)"
git remote set-url origin https://mirrors.aliyun.com/homebrew/brew.git

#=======================================================

# 还原为官方提供的 brew.git 仓库地址
cd "$(brew --repo)"
git remote set-url origin https://github.com/Homebrew/brew.git
+ +

2. 替换 / 还原 homebrew-core.git 仓库地址

1
2
3
4
5
6
7
8
9
# 替换成阿里巴巴的 homebrew-core.git 仓库地址:
cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
git remote set-url origin https://mirrors.aliyun.com/homebrew/homebrew-core.git

#=======================================================

# 还原为官方提供的 homebrew-core.git 仓库地址
cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
git remote set-url origin https://github.com/Homebrew/homebrew-core.git
+ +

3. 替换 / 还原 homebrew-bottles 访问地址

这个步骤跟你的 macOS 系统使用的 shell 版本有关系

+

所以,先来查看当前使用的 shell 版本

+
1
2
3
4
echo $SHELL

# 如果你的输出结果是 /bin/zsh,参考下方的 zsh 终端操作方式
# 如果你的输出结果是 /bin/bash,参考下方的 bash 终端操作方式
+

3.1 zsh 终端操作方式

1
2
3
4
5
6
7
8
9
10
# 替换成阿里巴巴的 homebrew-bottles 访问地址:
echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles' >> ~/.zshrc
source ~/.zshrc

#=======================================================

# 还原为官方提供的 homebrew-bottles 访问地址
vi ~/.zshrc
# 然后,删除 HOMEBREW_BOTTLE_DOMAIN 这一行配置
source ~/.zshrc
+

3.2 bash 终端操作方式

1
2
3
4
5
6
7
8
9
10
# 替换 homebrew-bottles 访问 URL:
echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles' >> ~/.bash_profile
source ~/.bash_profile

#=======================================================

# 还原为官方提供的 homebrew-bottles 访问地址
vi ~/.bash_profile
# 然后,删除 HOMEBREW_BOTTLE_DOMAIN 这一行配置
source ~/.bash_profile
+ +

转载自:http://www.xiegangd.com/article/154055689187484

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/64/index.html b/ja/page/64/index.html new file mode 100644 index 0000000000..3d3faf193a --- /dev/null +++ b/ja/page/64/index.html @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

vue插件开发

+

Vue.use({install(Vues){}})

+
+

Vue.use

把给到的内容执行一下
举例

+
1
2
3
4
function a(){
console.log('a')
}
Vue.use(a) // a
+

有 install 就执行 install

+
1
2
3
4
5
6
7
function a(){
console.log('a')
}
a.install = function(){
console.log('b')
}
Vue.use(a) // b
+

再进一步

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function a(){
console.log('a')
}
a.install = function(){
// console.log('b')
vue.mixin({ // 抽离公共逻辑 , 缺点:命名冲突,难以阅读
data(){ // data数据少的时候可以不用vuex 用mixin
return {
c:'this is mixin'
}
},
methods:{
// 混入方法
// 提示性弹窗 原:import 控制 显隐 现在:在根节点引入,通过mixin在方法控制显隐
}
// 混入生命周期
create(){
// 所有组件的create生命周期都执行 mixin先执行
}
})
}
Vue.use(a) // b
+ +

vue.util.defineReactive()

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
vue.util.defineReactive()

var test = {
testa: 1
}
setTimeout(()=>{
test.testa = 2
},1000)
vue.mixin({
beforeCreate(){
this.test = test
}
})

+ +

vue.extend vue.util.extend

+

vue.util.extend ===> 简单做了个拷贝,拷贝到一起

+
1
vue.util.extend(a,b)
+

vue.extend ===> 获取到某个对象的实例

+
1
2
let Constrator = vue.extend(obj)
let vm = new Constrator()
+ +

手写vue-router

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// myVueRouter.js
class HistoryRoute(){
constructor(){
this.current = null;
}
}

class vueRouter{
constructor(options){
this.mode = options.mode || 'hash'
this.history = new HistoryRoute
this.routes = options.routes||[]
this.routesMap = this.createMap(this.routes)
this.init()
}
init(){
if(this.mode == 'hash'){
// 自动加上 #
location.hash?"":location.hash="/"
window.addEventListener('load',()=>{
this.history.current = location.hash.slice(1)
})
window.addEventListener('hashchange',()=>{
this.history.current = location.hash.slice(1)
})
}else{
location.pathname?"":location.pathname="/"
window.addEventListener('load',()=>{
this.history.current = location.hash.pathname
})
window.addEventListener('popstate',()=>{
this.history.current = location.hash.pathname
})
}
}
createMap(router){
return router.reduce((memo,current)=>{
memo[current.path] = current.component
})
}
}

vueRouter.install = function(Vue){
Vue.mixin({
beforeCreate(){ // 组件还未实例化好
if(this.$options && this.$options.router){ // 有配置而且引入路由
this._root = this
this._router = this.$option.router

Vue.util.defineReactive(this,'current',this._router.history)
}else{
this._root = this.$parent._root
}
// 增强健壮性
Object.defineProperty(this,'$route',{
get(){
return this._root._router
}
})
}
})
Vue.component('router-view',{
render(h){
// 如何根据当前的current,获取到对应的组件
let current = this._self._root._router.history.current
let routerMap = this._self._root._router.routeMap
return h(routeMap[current])
}
})
}
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/65/index.html b/ja/page/65/index.html new file mode 100644 index 0000000000..228fc8b96e --- /dev/null +++ b/ja/page/65/index.html @@ -0,0 +1,558 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

V8引擎如何回收垃圾

为什么我们要关注内存

    +
  • 防止页面占用内存过大,引起客户端卡顿,甚至无响应
  • +
  • Node使用的也是v8,内存对于后端服务的性能至关重要。因为服务的持久性,后端更容易造成内存溢出

    v8引擎的内存回收机制

    v8的内存分配

    新生代内存空间
  • +
  • from
  • +
  • to
    老生代内存空间

    内存大小

  • +
  • 和操作系统有关 — 64位(1.4G)32位(0.7G)
  • +
  • 64位下 新生代(64MB) 老生代(1400MB)
  • +
  • 32位下 新生代(16MB) 老生代(700MB)
  • +
+

为什么不占多一点内存

+
    +
  • js设计之初是为浏览器
      +
    • 前端特点 — 不持久化,执行一遍就回收了,所以1.4G够用了
    • +
    • js回收内存会暂停执行代码
    • +
    +
  • +
+

垃圾回收算法

新生代简单的说就是复制

+
    +
  • 新生代用来放新产生的变量,根据条件放到老生代,有用的放到to里(把有用的留一边,清空另一边,往复进行)
  • +
  • 算法复杂度(时间复杂度,空间复杂度)—- 牺牲空间换时间
  • +
+

老生代就是标记、删除、整理

+
    +
  • 为什么要整理
      +
    • 数组是需要连续的空间
    • +
    +
  • +
+

新生代如何晋升到老生代

+
    +
  • 变量是否经过回收,第二次回收放到老生代,第一次回收放到To
  • +
  • To空间使用了25%,放到老生代
  • +
+

V8是如何处理变量的

1
2
3
4
5
6
7
8
9
10
11
12
13
// 浏览器查看内存
window.performance
// nodejs查看内存 --- nodejs是c++的,可以拓宽内存
process.memoryUsage()

// 拿内存的方法
function getMem(){
var mem = process.memoryUsage();
var format = function(bytes){
return (bytes/1024/1024).toFixed(2)+'MB';
}
console.log('heapTotal:'+format(mem.heapTotal)+'heapUsed:'+format(mem.heapUsed))
}
+

变量处理

    +
  • 内存主要就是存储变量等数据的
  • +
  • 局部变量当程序执行结束,且没有引用的时候就会随着消失 — 可以被回收但不是说会马上回收
  • +
  • 全局对象会始终存活到程序运行结束
  • +
+

如何查看V8内存使用情况

如何注意内存使用

优化内存的技巧

    +
  • 尽量不要定义全局变量
  • +
  • 全局变量记得手动销毁掉
      +
    • 不推荐开发时写delete – 支持有问题,严格模式有bug
    • +
    • 赋值为 undefined/null undefined 是变量 null 是保留字
    • +
    +
  • +
  • 用匿名自执行函数变全局为局部
      +
    • (function(){})()
    • +
    +
  • +
  • 尽量避免使用闭包 —- 错误的观点,ie5时代的问题 —- 尽量避免使用闭包引用
  • +
+

闭包

+
1
2
3
4
5
6
7
function a(){
var size = 20*1024*1024;
var arr1 = new Array(size)
return arr1
}
a() // 这样就没问题
var b = a() // 因为引用所以无法销毁
+

防止内存泄漏

    +
  • 滥用缓存
  • +
  • 大内存量操作
  • +
+

所有的优化都可以用缓存来解决,缓存通常都在全局,缓存可以直接拿来用;

+
1
2
3
4
5
6
7
8
9
10
11
12
13
var 20*1024*1024;
var a = []
for(var i=0;i<13;i++){
a.push(new Array(size))
}

// 加缓存锁
for(var i=0;i<13;i++){
if(a.length>4){
a.shift();
}
a.push(new Array(size))
}
+
    +
  • 不要用v8来缓存
      +
    • 一定要用要的话加锁
    • +
    +
  • +
+

nodejs中读取大文件要用流的形式,不要用读文件到buffer
fs.readFile()
fs.createReadStream()

+

浏览器中,大文件上传记得切片
file.slice(0,1000)
file.slice(1000,2000)

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/66/index.html b/ja/page/66/index.html new file mode 100644 index 0000000000..d9c7ce134b --- /dev/null +++ b/ja/page/66/index.html @@ -0,0 +1,606 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近一直在追网课,说实话从业9年了,一直觉得前端发展非常快,而且一直充满着危机感,每天都要学习进步才有安稳的感觉,今天听了 vue 响应式原理实现的公开课,感觉还不错,做了如下笔记,帮助自己记忆,也希望能帮助大家。

+

Vue2 原理

什么是 defineProperty

defineProperty 其实是定义对象属性用的

+
+

defineProperty 其实并不是核心的为一个对象做数据双向绑定,而是去给对象做属性标签,只不过是属性里 get 和 set 实现了响应式。

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
属性名默认值
valueundefined
getundefined
setundefined
writablefalse
enumerablefalse
configurablefalse
+
1
2
3
4
5
6
7
8
9
10
11
12
13
var ob = {
a:1,
b:2
}
// 参数 1、对象 2、属性 3、配置
Object.defineProperty(ob,'a',{
writable:false,
enumerable:true,
configurable:true,
})
console.log(Object.getOwnPropertyDescriptor(ob,'a')) // 1
ob.a = 2
console.log(ob.a) // 1
+

下面我们实现一下双向绑定

+
1
2
3
4
5
6
7
8
9
10
11
12
13
Object.defineProperty(ob,'a',{
get:function(){
console.log('a is be get')
return 999;
},
set:function(){
console.log('a is be set')
return 999;
},
})

console.log(Object.getOwnPropertyDescriptor(ob,'a')) // 999

+

改造代码实现双向绑定(存取值)

+
1
2
3
4
5
6
7
8
9
10
11
12
var _val = obj.a; // 暂存
Object.defineProperty(ob,'a',{
get:function(){
console.log('a is be get')
return _val;
},
set:function(newVal){
_val = newVal // 新值替换旧值
console.log('a is be set')
return _val;
},
})
+

Vue 中从改变一个数据到发生改变的过程

    +
  1. 改变数据触发 Set
  2. +
  3. Set 部分触发 notify(更新)
      +
    1. Get 部分收集依赖
    2. +
    +
  4. +
  5. 更改对应的虚拟 Dom
  6. +
  7. 重新 Render
  8. +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// MyVue.js

// 简单版本 vue
function MyVue(){
this.$data = {
a: {
b:1
},
c:2
}
this.el = document.getElementById('app');
this.virtualDom = '';
this.observer(this.$data);
this.render();
}
vue.property.observer = function(obj){
var _val, self = this;
// var dep = new Dep() -> 源码中依赖收集对象
for(var key in obj){ // 属性有可能是对象,要递归绑定
_val = obj[key];
if(typeof _val === 'Object'){
this.observer(_val)
}else{
Object.defineProperty(this.$data,key,{ // 这里是实际绑定过程
get:function(){
// 依赖收集
// dep.depend(); -> vue 源码中收集依赖的方法
return _val
},
set:function(newVal){
_val = newVal
// dep.notify(); -> vue 源码中
self.render() // AST语法树
}
})
}
}
}
vue.property.render = function(){
this.virtualDom = 'i am '+this.$data.b;
this.el.innerHTML = this.virtualDom;
}
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// index.html

<!DOCTYPE html>
<html>
<head>
<title>自己实现Vue2数据双向绑定</title>
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src='myVue.js'></script>
<script type="text/javascript">
var mv = new MyVue();
setTimeout(function(){
console.log('changes');
console.log(mv.$data);
mv.$data.b = 222;
})
</script>
</body>
</html>
+
+

依赖收集:

+
    +
  1. 我们的data里面的数据并不是所有地方都用到
  2. +
  3. 如果我们直接更新整个视图,浪费资源
  4. +
  5. 先收集依赖改变的数据的组件,再更新依赖了数据的组件(Dep depend notify)
  6. +
+
+

格外注意的地方—数组怎么监听

definePropty 只能给对象进行 get set 绑定, 数组怎么办?

+

vue 中 使用了 装饰者模式

+
+

装饰者模式 Decorator模式(别名Wrapper):动态将职责附加到对象上,若要扩展功能,装饰者提供了比继承更具弹性的代替方案。

+
+
1
2
3
4
5
6
7
8
9
10
var arraypro = Array.property; // 创建一个数组的原型对象
var arrob = Object.create(arraypro); // 避免影响原型链
var arr = ['push','pop','shift'];
arr.forEach(function(method,index){
arrob[method]=function(){ // 装饰者模式
var ret = arraypro[method].apply(this,arguments)
dep.notify() // 扩展了功能
}
})

+

Vue3 实现双向绑定

Proxy 是什么?

+
+

Proxy 对象用于定义基本操作的自定义行为
和 definePropty 类似,功能几乎一样,只是用法上不同

+
    +
  1. 不会污染原对象
  2. +
  3. 直接给对象就可以了
  4. +
  5. 不需要借助外部变量 _val
  6. +
+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var ob = {
a:1,
b:2
}

var newOb = new Proxy(ob,{
get(target,key,receiver){ // target 对象,key 属性
console.log(target,key,receiver)
return target[key]
},
set(target,key,value,receiver){
return Reflect.set(target.key,value);
// 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
// return target[key] = value
}
})
+

为什么改用 Proxy

    +
  1. defineProperty 只能监听某个属性,不能全对象监听
  2. +
  3. 可以省去for in循环提升代码执行效率
  4. +
  5. 可以监听数组,不需要再为数组做特异性操作
  6. +
  7. 不污染原对象
  8. +
  9. 更优雅
  10. +
+

我们用 Proxy 实现一下 observe 方法

+
1
2
3
4
5
6
7
8
9
10
11
12
vue.property.observe = function(){
var self = this;
this.$data = new Proxy(this.$data,{
get(target,key, receiver){
return target[key]
},
set(target,key,newVal){
target[key] = newVal
self.render()
}
})
}
+ +

还能用 Proxy 做什么

    +
  1. 校验类型
  2. +
  3. 真正的私有变量
  4. +
+
校验类型

例子:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 数据类型验证
// 我们要创建一个对象,这个对象是个人,他有name和age两个属性
// name必须是中文,age必须是数字,大于18岁

// 这里用到了策略模式
var valid = {
name(value){
var reg=/^[\u4E00-\u9FAS]=$/
if(typeof value === 'string' && reg.test(value)){
return true;
}
return false;
},
age(value){
if(typeof value === 'number' && value > 18){
return true;
}
return false;
}
}
function Person(name,age){
this.name = name
this.age = age
return new Proxy(this,{
get(target,key){
return target[key]
},
set(target,key,value){
if(valid[key](value)){
return Reflect.set(target,key,value)
}else{
throw new Error(key+'is not valid')
}
}
})
}
new Person('name',19)
+
+

策略模式
定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。

+
+
真正的私有变量

vue-router 源码中,给 $router ,$route 用 defineProperty 定义 get 并返回本身,这样就不能修改属性了。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
Object.defineProperty(this,'$router',{ // Router 的实例
get(){
return this._root._router;
}
})
Object.defineProperty(this,'$route',{
get(){
return {
// 当前路由所在的状态
current: this._root._router.history.current;
}
}
})
+ +

虚拟Dom和diff算法

虚拟Dom是虚拟的,他只在概念里面存在,在AST语法树,下面进行解释

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 模板
<template>
<div>
<p>{{msg}}</p>
<p>2</p>
<p>3</p>
</div>
</template>

// diff 描述法
diff <div>
props:{
id:2
}
children:[
diff <p>
props:{
id:xxx
}
children:[
...
]
]

// 对象描述法
var virtual = {
dom:'div',
props:{
id:2
},
children:[
....
]
}
+

每层结构都是一样的,那么是如何进行 diff 比对的呢?

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** 
* diff 算法
*/
patchVnode(oldVnode,vnode){ // 接收新旧节点
const el = vnode.el = oldVnode.el; // 拿出真实dom
let i,oldCh = oldVnode.children ,ch = vnode.children // 拿出新旧节点子元素children数组
if(oldVnode === vnode) return; // 新旧节点一致,直接返回不进行后续操作
// 分情况操作 --- 只有文字节点,删除了子元素,增加了子元素,子元素发生变动
// 新旧节点都不为空,且不一样
if(oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text){ // 文字发生更新
api.setTextContent(el,vonde.text) // 文字发生了更新,更新文字,给Dom,更新文字
} else { // 不是单纯文字节点的话
updateEle(); // 更新元素
if(oldCh&&ch&&oldCh!==ch){ // 都有子元素,子元素变动
updateChildren() // 调用更新子元素方法
} else if(ch){ // 增加子元素
createEl(vnode) // 创建子元素
} else if(oldCh){ // 删除子元素
api.removeChildren(el) // 调用删除子元素方法
}
}
}
+

源码要多看,以下必看 Vue/react/axios/vue-router/Redux/Vuex
为什么要看源码??

+
    +
  • 初级前端就会用vue或react — 从差不多水平的60%中挑出更好的人
  • +
  • 提高思想–》看优秀的代码–》写优秀的代码
  • +
  • 看源码能力,对高级前端是必备的。— 解决疑难杂症,看源码了解原理。
  • +
+

vue 性能优化

因为是公开课,所以时间上没来的及说完,以后自己在听别的有关的内容时再补上这块。

+

最后

只有不断学习才能进步,充分利用网络的便利性,找各种优质的教学资源,我相信,努力会有回报,加油!

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/67/index.html b/ja/page/67/index.html new file mode 100644 index 0000000000..ebab709770 --- /dev/null +++ b/ja/page/67/index.html @@ -0,0 +1,564 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

这篇文章想写一写前端工程化相关的内容,原因一呢是是结合公司业务给自己定的业绩指标包含这些内容,另外一个原因是因为听了网易前端唐磊说过的一句话,前端leader干什么,很重要的工作就是前端工程化,高级以上不懂前端工程化很难。

+

今天听了公开课讲到了用node写一个自己的cli,说实话正是工作所需,课程讲的有点快,没有从头跟下来,自己上完了课又上网上找了些资料,终于把步骤弄明白了,下面就把如何自定义一个cli来帮助提升开发效率。同时也完成了一个业务指标,心里美滋滋。

+

准备

如果你看到这篇文章,也想跟着我的步骤写一下这个自定义cli,那么如下的知识还是有一些为好。

+
    +
  • 基础的nodejs相关知识
  • +
+

没错就只需要会一些node的基础知识就可以了,接下来正式开始

+

初始化

首先,我们要给我们的命令行工具起个名字,比如我们熟悉的 vue 命令行就是 vue-cli ,因为我写完了要给公司用,所以我起的名字是 tfd-cli ,你们喜欢叫什么你们随意

+

首先创建一个名字为 tfd-cli 的文件目录,然后在目录下执行 node 工程的初始化命令

+
1
npm init -y // 初始化项目 -y 默认全部yes的参数
+

命令执行完成后 tfd-cli 目录下会生成一个我们熟悉的 package.json 文件,我们打开 package.json 文件,增加一段代码,如下

+
1
2
3
4
// 追加的代码
"bin": {
"tfd": "index.js"
}
+

追加完成后,package.json 文件中的内容是这样的

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"name": "tfd-cli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"bin": {
"tfd": "index.js"
}
}
+

也就是说当我们执行 tfd 命令时它就会找同级目录下的 index.js 文件执行其中代码,我们目前还没有 index.js,文件,那么我们手动创建一个 index.js 的文件,然后在里面写下如下代码

+
1
2
3
4
#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的

console.log('hello world');
+

执行完这些还不够,因为我们是开发环境所以还有一步操作是要将 tfd 命令告知 npm,该如何处理,所以我们要执行如下命令

+
1
npm link
+

这里如果执行不成功,请用管理员权限执行,执行完成后我们会得到一个 package-lock.json 的文件,内容如下

+
1
2
3
4
5
{
"name": "tfd-cli",
"version": "1.0.0",
"lockfileVersion": 1
}
+

如此一来,我们就可以在任何目录下执行命令行 tfd 就会执行 index.js 文件了,这里我们会在控制面板中输出 hello world ,怎么样是不是小有成就感,我们接着往下来。

+

创建指令

我们写个命令行工具肯定不是为了输出个 hello world 这么简单,而是希望通过用户输入内容后根据条件输出一些东西,那么让我们想想一个命令行工具应该具备哪些指令呢?

+
    +
  • 查看命令行工具版本
  • +
  • 查看帮助文档
  • +
  • 初始化模板
  • +
  • 列出模板类型
  • +
  • 等等
  • +
+

那么用指令该如何描述呢

+
1
2
3
4
tfd -V|--version //查看工具版本号
tfd -h|--help //查看使用帮助
tfd init <template-name> <project-name> //基于指定模板进行项目初始化
tfd list //列出所有可用模板
+

为了执行命令,这里我们要引入一个 node 包叫做 commander,因此我们要先执行一下 install 命令

+
1
npm install commander
+

接着我们就可以在 index.js 里面写指令了。

+
1
2
3
4
5
6
7
8
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0
+

到这一步我们在控制台敲一下 tfd -V 你会发现什么也没输出,这是因为到这一步我们还无法解析 tfd -V 操作,在这之前我们要知道一个命令

+
1
process.argv // 使用process.argv获取命令行参数
+

当我们把这句话加到 console.log 中在 index.js 中输出时你会看到控制台打印出

+
1
2
3
4
5
6
7
8
9
10
11
12
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0
console.log(process.argv)

// 控制台
[ '/usr/local/bin/node', '/usr/local/bin/tfd', '-V' ]
+

接下来我们要让commander获取参数执行命令

+
1
2
3
4
5
6
7
8
9
10
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0
// console.log(process.argv)
cmd.parse(process.argv);
+

这个时候我们再在控制台输入 tfd -V 时,我们就会发现,控制台输出了 0.1.0,这样我们就完成了查版本的指令,接下来我们完成其他的指令

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0

// tfd init <template> <project>
cmd
.command('init <template> <project>') // 参数
.description('初始化项目模板')
.action((templateName, projectName) => {
console.log(templateName, projectName);
})

// tfd list
cmd
.command('list')
.description('查看所有可用模板')
.action(() => {
console.log(`
a a模板
b b模板
c c模板
`)
})
// console.log(process.argv)
cmd.parse(process.argv);
+

这个时候我们在控制台上输入 tfd -h 的时候,控制台会输出如下代码

+
1
2
3
4
5
6
7
8
9
Usage: tfd [options] [command]

Options:
-V, --version output the version number
-h, --help output usage information

Commands:
init <template> <project> 初始化项目模板
list 查看所有可用模板
+

这样我们就实现了自定义命令,我们执行一下 tfd init template1 project1,我们可以看到,控制台中输出了 template1 project1,也就是说command命令后尖括号中指向了action中的参数,我们就可以通过判断action中的参数做具体的操作了。

+

通常模板可以选择从本地拷贝一份,但更常用的是从线上拷贝一份,比如从github中,接下来我们就看看如何从github中拷贝一个模板作为项目的初始化工程

+

github上创建模板仓库

首先我们要在github上创建两个仓库 tpl-1 tpl-2,这里为了从github中下载仓库我们需要一个node包支持,让我们请出download-git-repo,别忘了执行安装命令

+
1
npm install download-git-repo
+

安装完依赖之后让我们再去index.js填点东西,首先引入下载依赖,然后是创建下载的 template 抽象对象

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');
const download = require('download-git-repo');
// 可用模板
const templates = {
'tpl-1': {
url: 'https://github.com/lixuguang/tpl-1',
downloadUrl: 'https://github.com:lixuguang/tpl-1#master',
description: 'tfd-cli脚手架测试模板1'
},
'tpl-2': {
url: 'https://github.com/lixuguang/tpl-2',
downloadUrl: 'https://github.com:lixuguang/tpl-2#master',
description: 'tfd-cli脚手架测试模板2'
}
}

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0

// tfd init <template> <project>
cmd
.command('init <template> <project>') // 参数
.description('初始化项目模板')
.action((templateName, projectName) => {
// console.log(templateName, projectName);
let {downloadUrl} = templates[templateName];
// 第一个参数是github仓库地址,第二个参数是创建的项目目录名,第三个参数是clone
download(downloadUrl, projectName, {clone: true}, err => {
if(err){
console.log('模板下载失败');
}else{
console.log('模板下载成功');
}
})
})

// tfd list
cmd
.command('list')
.description('查看所有可用模板')
.action(() => {
// console.log(`
// a a模板
// b b模板
// c c模板
// `)
// 通过获取templates里的key可以获取到模板名称
const templateName = Object.keys(templates)
console.log(templateName)
})
// console.log(process.argv)
cmd.parse(process.argv);
+

这样当我们执行 tfd list 就可以看到有哪些模板了,然后执行tfd init tpl-1 newproject 就可以依据 tpl-1 模板创建出 newproject 工程,这个过程实际上就是从github仓库克隆一份tpl-1作为模板创建工程newproject

+

这里需要注意的是download地址跟github仓库地址有点出入,比如github仓库地址是https://github.com/xxx/xxx而下载地址是https://github.com:xxx/xxx

+

虽然这样执行完成后就完成了基本的cli的雏形,但是毕竟不灵活,我们在使用vue-cli时,它的创建过程是问答式和选择式的,另外每个过程都会有进度显示什么的,那么要怎么添加这些功能呢,我们接着往下做。

+

进阶增加功能

使用inquirer进行命令行答询

inquirer 是一个进行命令行答询的库,通过它我们就可以创建问答式的内容,首先还是安装依赖

+
1
npm install inquirer
+

使用handlebars修改package.json

我们都知道在使用vue-cli的初始化命令后,会在项目目录下生成一个package.json文件,它就像是这个项目的基因序列一样,影响着项目的整个结构。模板是固定的,那要修改其中的package.json符合自己项目的需要,就要用到handlebars这个库来改写package.json文件,老规矩先安装它

+
1
npm install handlebars
+

使用ora在命令行中显示加载状态

我们在装任何依赖时都会有进度条显示进度,如果没有进度条又没有任何响应,会让用户迷茫,为了友好,我们就要加进度条,这里我们需要引入ora这个库来完成进度显示,我们接着安装。

+
1
npm install ora
+

使用chalk和log-symbols增加命令行输出样式

为了让命令行有红红绿绿的效果以及符号效果,我们需要使用chalklog-symbols来丰富样式,少废话,接着装

+
1
npm install chalk log-symbols
+ +

集大成

终于安装完一堆的依赖,别忘了在index.js中引入,让我们看看具体如何使用这些库吧。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// index.js

#!/usr/bin/env node
//使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
const cmd = require('commander');
const download = require('download-git-repo');

const iq = require('inquirer'); // 命令行答询
const hb = require('handlebars'); // 修改package.json文件
const ora = require('ora'); // 命令行中加载状态标识
const chalk = require('chalk'); // 命令行输出字符颜色
const ls = require('log-symbols'); // 命令行输出符号
const fs = require('fs'); // node fs原生模块

// 可用模板
const templates = {
'tpl-1': {
url: 'https://github.com/lixuguang/tpl-1',
downloadUrl: 'https://github.com:lixuguang/tpl-1#master',
description: 'tfd-cli脚手架测试模板1'
},
'tpl-2': {
url: 'https://github.com/lixuguang/tpl-2',
downloadUrl: 'https://github.com:lixuguang/tpl-2#master',
description: 'tfd-cli脚手架测试模板2'
}
}

// tfd -V|--version
cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0

// tfd init <template> <project>
cmd
.command('init <template> <project>') // 参数
.description('初始化项目模板')
.action((templateName, projectName) => {
// console.log(templateName, projectName);
let {downloadUrl} = templates[templateName];
//下载github项目,下载墙loading提示
const loading = ora('模板下载中...').start();
// 第一个参数是github仓库地址,第二个参数是创建的项目目录名,第三个参数是clone
download(downloadUrl, projectName, {clone: true}, err => {
if(err){
// console.log('模板下载失败');
loading.fail('模板下载失败');
}else{
// console.log('模板下载成功');
spinner.succeed('模板下载成功');
// 命令行答询
iq.prompt([
{
type: 'input', // 类型 输入框
name: 'name', // 字段 key
message: '请输入项目名称', // 描述
default: projectName // 默认值
},
{
type: 'input',
name: 'description',
message: '请输入项目简介',
default: ''
},
{
type: 'input',
name: 'author',
message: '请输入作者名称',
default: ''
}
]).then(answers => { // answers 是一个对象,对象的 key 为上面答询的 name 的值,value 为 用户输入的值,如果未输入,就取默认值
// 根据命令行答询结果修改 package.json 文件
let packageContent = fs.readFileSync(`${projectName}/package.json`, 'utf8'); // 同步方式以 utf-8 字符集获得下载好的项目目录下的 package.json 文件
let packageResult = hb.compile(packageContent)(answers); // 将用户输入项与原内容混合获得新内容
fs.writeFileSync(`${projectName}/package.json`, packageResult); // 重新同步方式写入到 package.json 文件中
// 用chalk和log-symbols改变命令行输出样式
console.log(ls.success, chalk.green('模板项目文件准备成功!'));
})
}
})
})

// tfd list
cmd
.command('list')
.description('查看所有可用模板')
.action(() => {
// console.log(`
// a a模板
// b b模板
// c c模板
// `)
// 通过获取templates里的key可以获取到模板名称
const templateName = Object.keys(templates)
console.log(templateName)
})
// console.log(process.argv)
cmd.parse(process.argv);
+ +

到这里你自己的命令行工具就创建完成了,当然还可以继续丰富,比如加上选择条件等,这个就更复杂了,不在本文中展开,后续会继续改进这个命令行工具,当然如果要将自己的命令行工具给其他人用当然要先发布出去,就像我们要用vue-cli,首先要在npm上下载下来,同样的我们要想让别人用,就得先上传到npm上。

+

发布到npm上

首先你需要有一个npm的账号,没有赶快去注册一个,在控制台中输入npm login,它会让你依次输入 Username Password Email ,当你都按照要求输入完之后,成功的话你会获得如下信息Logged in as XXX on https://registry.npm.org/.,再接下来执行 npm publish 命令,你的自定义脚手架就会发布到npm上,供他人下载使用,怎么样,学会了么?

+

后记

这篇文章会有后续持续进化跟进,多篇文章连续,只要我的cli工具还在进化,文章就会继续,欢迎跟进。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/68/index.html b/ja/page/68/index.html new file mode 100644 index 0000000000..8ac25004d4 --- /dev/null +++ b/ja/page/68/index.html @@ -0,0 +1,500 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

最近又有小伙伴问我,为啥我在写vue项目做代理转发时候报找不到代理服务的问题?
我看了一下他发给我的配置截图,仅一眼我就发现了问题所在,因为这个坑我曾经也踩过,而且好多小伙伴也都踩过,在写转发服务器的时候没写协议头,也就是在 target 那没写 http:// 这个部分,添加上以后重启一下,这个问题就解决了,小伙伴说就这个问题他解决了一天也没解决了,而且我说完了以后他想起来以前好像也遇到过这个问题,所以说在平时工作中遇到问题要常做笔记才对,所以为了避免以后自己也犯这种错误,暂且留一篇文章记录一下这个问题。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/69/index.html b/ja/page/69/index.html new file mode 100644 index 0000000000..2c5aac17ae --- /dev/null +++ b/ja/page/69/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

这两天复工,公司一个小伙伴在群里问了一个问题

+
+

如何在打印表格的时候,让超过一页的表格分割线不被截断

+
+

说着他有贴上来了一张图,一张因为跨页被截断了的表格,说实话打印的场景接触的不多,打印表格一般都是制式的样式所以也不会出现这问题,一时间也没有头绪,想着这个是不是没什么办法解决,隔天他发来一个消息,说是解决了,给表格里加上了 tr td 就好了,接着他又发来一张图,还真是,不过上下边距发生了一些变化,我问他是不是自己改了边距所以好了,他说没改,然后我问他那他是怎么解决的,他说因为实在找不到问题解决方案和产生原因,于是就把代码重构了一下,按照标准写法写了一下,结果就好了。。。怎么说呢,也是神奇,我也到网上去查了查相关的材料,并没有特别对的上的原因,凭感觉来说,应该是thead和tbody上有默认的css样式解决了上面的问题,暂且记下这个问题,避免以后遇到又忘了是怎么回事。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/7/index.html b/ja/page/7/index.html new file mode 100644 index 0000000000..df16f6e742 --- /dev/null +++ b/ja/page/7/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Oracle Linux install Docker(https://docs.docker.com/engine/install/centos/)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 查看
systemctl list-units

sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine

sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

sudo yum install -y yum-utils

sudo yum -y install docker-ce-20.10.17 docker-ce-cli-20.10.17 containerd.io-1.6.6

sudo vi /etc/yum.repos.d/docker-ce.repo

[centos-extras]
name=Centos extras - $basearch
baseurl=http://mirror.centos.org/centos/7/extras/x86_64
enabled=1
gpgcheck=1
gpgkey=http://centos.org/keys/RPM-GPG-KEY-CentOS-7

sudo yum -y install fuse-overlayfs slirp4netns

# sudo chmod 777 docker-ce.repo

# yum list docker-ce --showduplicates | sort -r

sudo systemctl start docker

sudo docker run hello-world

sudo yum install /path/to/package.rpm
sudo systemctl start docker
sudo docker run hello-world
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/70/index.html b/ja/page/70/index.html new file mode 100644 index 0000000000..3d4deae5e4 --- /dev/null +++ b/ja/page/70/index.html @@ -0,0 +1,513 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

什么是 RESTful

+

REST 指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是 RESTful。

+
+

实际上就是 url 风格/ 路由风格 , 给出一种写路由的原则 — 面向资源
tip:静态路由利于seo优化 api/news/1,动态路由不利于seo优化 api/news?id=1

+

原来的风格
| 路由 | 功能 | 描述 |
| —- | —- |——|
| http://127.0.0.1/user/query/1 | 查询 | 根据ID查询用户信息 |
| http://127.0.0.1/user/save | 保存 | 注册用户 |
| http://127.0.0.1/user/update | 更新 | 修改用户 |
| http://127.0.0.1/user/delete/{id} | 删除 | 删除用户 |

+

RESTful — 面向资源:对于同一个资源都在同一个 URL 进行,通过判断 HTTP 请求的类型来决定做不同的事
/user 一个资源
| 路由 | 请求类型 |
| ———————– | ——– |
| http://127.0.0.1/user/1 | GET |
| http://127.0.0.1/user | POST |
| http://127.0.0.1/user | PUT |
| http://127.0.0.1/user | DELETE |

+

URL 描述资源 , HTTP 描述请求。(HTTP协议无语义无状态 —- 降低复杂度,业务无关 — 架构代码业务无关)

+

RESTful 采用的是顶层路由

+
+

顶层路由设计:不需要有物理文件映射路由

+
+
1
2
3
4
5
6
7
8
// express
// app.js
const express = require('express')
const app = express()
app.get('/case.avi',(req, res)=>{
res.send('hello world'); // 不需要对应物理文件
})
app.listen(3000)
+

原生接口

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// index.js
const http = require('http');
const mysql = require('mysql'); // mysql
const co = require('co-mysql') // 异步同步化
const md5 = require('md5-node') // md5加密

// 连接数据库
let db = mysql.createPool({ // 连接池自己管理 不用关闭
host:'localhost',
user:'root',
password:'root',
database:'user'
})
let conn = co(db)

const app = http.createServer(async (req,res)=>{
if(req.method === 'POST'){
if(req.url === '/user'){
// res.end(JSON.stringify({'message':'对user发起post请求'}))
req.on('data', async (data)=>{
arr.push(data)
})
req.on('end',async ()=>{
let buffer = Buffer.concat(arr);
// json对象
let {username,pasword} = JSON.parse(buffer.toString())
// console.log(username,pasword)
let sql = `selct user from admin where user = ${username}`
let data = await conn.query(sql);
// console.log(data)
if(data.length >=1 ){
res.end(JSON.stringify({
'status':200,
'message':'用户名已经注册'
}))
}else{
// 写入数据库
password = md5(password);
let sql = `INSERT INTO admin (user,password) VALUES ('${username}','${password}')`
await conn.query(sql);
res.end(JSON.stringify({
'status':200,
'message':'注册成功'
}))
}
})
}
}if(req.method === 'GET'){
if(req.url === '/user'){
// res.end(JSON.stringify({'message':'对user发起get请求'}))
let sql = `SELECT id,user,password FROM admin`
let data = await conn.query(sql);
res.end(JSON.stringify(data))
}
}
}).listen(3000)

// .http 文件
@url = http://localhost:3000
@type = Content-Type: applications

GET {{url}}/user HTTP/1.1

POST {{url}}/user HTTP/1.1
{{type}}

{
username:'admin',
password:123456
}
+

使用express实现(express — generater yard ,koa — async await)

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

const express = require('express')
const app = express()
const mysql = require('mysql'); // mysql
const co = require('co-mysql') // 异步同步化
const md5 = require('md5-node') // md5加密
const bodyparse = require('body-parse')
// 连接数据库
let db = mysql.createPool({
host:'localhost',
user:'root',
password:'root',
database:'user'
})
let conn = co(db)

app.use(bodyparse.urlencoded({
extended:true // 返回对象是兼职对,false - string/array true - any
}))
app.use(bodyparse.json())

app.post('/user',async (req.res)=>{
let { username , password} = req.body
// console.log(username,pasword)
let sql = `selct user from admin where user = ${username}`
let data = await conn.query(sql);
// console.log(data)
if(data.length >=1 ){
res.send(JSON.stringify({
'status':200,
'message':'用户名已经注册'
}))
}else{
// 写入数据库
password = md5(password);
let sql = `INSERT INTO admin (user,password) VALUES ('${username}','${password}')`
await conn.query(sql);
res.send(JSON.stringify({
'status':200,
'message':'注册成功'
}))
}
})

app.get('/user/:id',(req,res)=>{
res.send(req.params.id)

let sql = `SELECT id,user,password FROM admin WHERE id = ${req.params.id}`
let data = await conn.query(sql);
res.end(JSON.stringify(data))
})

app.listen(3000)
+ +

使用koa实现

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53


// config.js
module.exports = {
host:'localhost',
user:'root',
password:'root',
database:'user'
}

// libs/database.js
const config = require('../config')
const mysql = require('mysql'); // mysql
const co = require('co-mysql') // 异步同步化
// 连接数据库
let db = mysql.createPool({
host:config.host,
user:config.user,
password:config.password,
database:config.database
})
let conn = co(db)

// router/user/index.js
const Router = require('koa-router')
const md5 = require('md5-node') // md5加密
const router = new Router();

router.get('/user',async ctx=>{
ctx.body = '主页'
})
router.post('/user',async ctx=>{
let {username,password} = ctx.request.body
// console.log(username,password)
ctx.body = {
username,password
}
})
module.exports = router.routes();

// app.js
const koa = require('koa')
const Router = require('koa-router')
const body = require('koa-bodyparse')
const config = require('config')
const app = new Koa()
const router = new Router()
app.context.db = require('./libs/database')
app.context.config = config
app.use(body())
router.use('/api',require('./router/user'))
app.use(router.routes())
app.listen(3000)
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/71/index.html b/ja/page/71/index.html new file mode 100644 index 0000000000..2f53d464dd --- /dev/null +++ b/ja/page/71/index.html @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

什么是SSR

传统浏览器的vue纯浏览器渲染

浏览器请求服务器,服务器返回静态资源 ,客户端负责渲染js

+

ssr

浏览器请求服务器,node服务端渲染js后,返回对应的页面返回给浏览器

+

SSR需要那些东西

手写SSR

特性:

+
    +
  • 每一次访问必须新建一个vue实例
  • +
  • 只会触发组件的 beforeCreate和created钩子
  • +
+

核心库

+
    +
  • vue
  • +
  • vue-server-renderer

    vue + next

    +

    作者:李旭光
    引用请标明出处

    +
    +
  • +
+

前言

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/72/index.html b/ja/page/72/index.html new file mode 100644 index 0000000000..72dc4da7f4 --- /dev/null +++ b/ja/page/72/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

网络安全是前端不可忽略的一个部分,可惜的是之前我都忽略了,最近发现网络安全已经变得日益重要,所以特别整理一篇文章说说网络安全相关的内容。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/73/index.html b/ja/page/73/index.html new file mode 100644 index 0000000000..4ce5b58c20 --- /dev/null +++ b/ja/page/73/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在学习Webpack相关的知识的时候对loader和plugin有点迷惑,两个特性都是用来做打包相关处理,那么他们有什么区别呢,为了弄清楚他们有什么区别,我开始了疯狂的查找资料,虽说每篇文章都说了一些自己的认识,但是并没有一个特别标准或者说容易理解的答案,我且先将它们记录下来,以便日后回顾之时可以有一些感触。那么接下来我将开始webpack的解迷之旅。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/74/index.html b/ja/page/74/index.html new file mode 100644 index 0000000000..3ded8da175 --- /dev/null +++ b/ja/page/74/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

es6中,arguments被取消掉了,但是函数还是会有参数不确定的时候,那么我们该怎么办呢,接下来将介绍一下如何在es6中完成arguments一样的效果。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/75/index.html b/ja/page/75/index.html new file mode 100644 index 0000000000..a5d7bd8ae4 --- /dev/null +++ b/ja/page/75/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

今年我下定决心一定要好好地写博客,完善博客的内容,所以最近把原来记在各种笔记中的文章内容都一一转移到了博客,内容一多想找一篇文章时就变得很麻烦,我得去归档中或者分类中一篇篇的找,所以我觉得是时候该给我的博客添加搜索的功能了,我看了博客的config文件,其中并没有搜索相关的配置,我又看了主题的配置文件,这下让我找到了,有个local_search的选项,于是我就开始了博客搜索的研究。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/76/index.html b/ja/page/76/index.html new file mode 100644 index 0000000000..e8fb3f03dc --- /dev/null +++ b/ja/page/76/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在整理自己的知识体系,一方面为了夯实基础,另一方面也为了查缺补漏、重新理解,设计模式之前看过大概三四次,从最开始一点都不懂,到后面看了几遍以后慢慢理解,今天再次整理一下,希望这一遍下来之后能更加透彻的理解什么是设计模式。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/77/index.html b/ja/page/77/index.html new file mode 100644 index 0000000000..ef7f796451 --- /dev/null +++ b/ja/page/77/index.html @@ -0,0 +1,580 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

继承的操作需要有一个父类,这里使用构造函数外加原型来创建一个:
有下面两个类,下面实现 Child 继承 Father:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

function Father() {
this.type = 'prople';
}

Father.prototype.eat = function() {
console.log('吃东西啦');
};

function Child(name) {
this.name = name;
this.color = 'black';
}
+ +

原型继承(认贼作父)

+

关键点:子类原型等于父类的实例 Child.prototype = new Person()(将父类指向子类的原型)。

+
+
1
2
// js代码
Child.prototype = new Father();
+ +

特点:
实例可继承的属性有:

+
    +
  • 实例的构造函数的属性
  • +
  • 父类构造函数的属性
  • +
  • 父类原型上的属性
    新实例不会继承父类实例的属性
  • +
+

缺点:

+
    +
  • 新实例无法向父类构造函数传参
  • +
  • 继承单一
  • +
  • 所有新实例都会共享父类实例的属性。— 原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改
  • +
+

构造继承(借腹生子)

+

在子类构造函数中调用父类构造函数

+
+
1
2
3
4
// js代码
function Child(name) {
Father.call(this);
}
+ +

关键点:用 call 或 apply 将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制))Person.call(this, ‘reng’)
特点:

+
    +
  • 只继承了父类构造函数的属性,没有继承父类原型的属性
  • +
  • 解决了原型链继承的注意事项(缺点)1,2,3
  • +
  • 可以继承多个构造函数的属性(call 可以多个)
  • +
  • 在子实例中可以向父实例传参
    缺点:
  • +
  • 只能继承父类构造函数的属性
  • +
  • 无法实现构造函数的复用。(每次用每次都要重新调用)
  • +
  • 每个新实例都有构造函数的副本,臃肿
    (不能继承父类原型,函数在构造函数中,每个子类实例不能共享函数,浪费内存。)
  • +
+

组合继承(原型继承+构造继承)

+

使用构造继承继承父类参数,使用原型继承继承父类函数

+
+
1
2
3
4
5
6
7
// js代码
function Child(name) {
// 构造继承
Father.call(this);
}

Child.prototype = Father.prototype; // Child.prototype = new Person(); // 原型继承
+ +

关键点:结合了两种模式的优点–向父类传参(call)和复用(prototype)
特点:

+
    +
  • 可以继承父类原型上的属性,可以传参,可复用
  • +
  • 每个新实例引入的构造函数属性是私有的
  • +
+

缺点:

+
    +
  • 父类原型和子类原型是同一个对象,无法区分子类真正是由谁构造。
  • +
  • 调用了两次父类的构造函数(耗内存)
  • +
  • 子类的构造函数会代替原型上的那个父类构造函数(call 相当于拿到了父类构造函数的副本)
  • +
+

原型式继承(复制降级)

1
2
3
4
5
6
7
8
9
10
11
// 先封装一个函数容器,用来承载继承的原型和输出对象
function create(obj) {
// 寄生
function F() {}
F.prototype = obj;
return new F();
}
var father = new Father();
var child = create(father);
console.log(child instanceof Father); // true
console.log(child.job); // frontend
+ +

关键点:用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了可以随意增添属性的实例或对象。Object.create()就是这个原理。

+

特点:

+
    +
  • 类似于复制一个对象,用函数来包装
  • +
+

注意事项:

+
    +
  • 所有的实例都会继承原型上的属性
  • +
  • 无法实现复用。(新实例属性都是后面添加的)
    Object.create()方法规范了原型式继承。这个方法接收两个参数,一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。
  • +
+
1
2
3
4
5
6
7
8
9
10
11
// 传一个参数的时候
var child = Object.create(new Father());
console.log(child.job); // frontend
console.log(child instanceof Father); // true
// 传两个参数的时候
var child = Object.create(new Father(), {
name: {
value: 'come on'
}
});
child.sayHello(); // Hello come on
+ +

寄生组合继承

它跟组合继承一样,都比较常用。
寄生:在函数内返回对象然后调用
组合

+
    +
  • 函数的原型等于另一个实例
  • +
  • 在函数中用 apply 或 call 引入另一个构造函数,可传参
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 寄生
function create(obj) {
function F() {}
F.prototype = obj;
return new F();
}
// object是F实例的另一种表示方法
var obj = create(Father.prototype);
// obj实例(F实例)的原型继承了父类函数的原型
// 上述更像是原型链继承,只不过只继承了原型属性

// 组合
function Child() {
// 构造
this.age = 100;
Father.call(this); // 这个继承了父类构造函数的属性
} // 解决了组合式两次调用构造函数属性的特点

// 重点
Child.prototype = obj; // 原型

console.log(Child.prototype.constructor); // Father
obj.constructor = Child; // 一定要修复实例
console.log(Child.prototype.constructor); // Child
var child = new Child();
// Child实例就继承了构造函数属性,父类实例,object的函数属性
console.log(child.job); // frontend
console.log(child instanceof Father); // true
+ +

重点:修复了组合继承的问题

+

在上面的问题中,你可能发现了这么一个注释obj.constructor = Sub; // 一定要修复实例。为什么要修正子类的构造函数的指向呢?

+

因为在不修正这个指向的时候,在获取构造函数返回的时候,在调用同名属性或方法取值上可能造成混乱。比如下面:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Car() {}
Car.prototype.orderOneLikeThis = function() {
// Clone producing function
return new this.constructor();
};
Car.prototype.advertise = function() {
console.log('I am a generic car.');
};

function BMW() {}
BMW.prototype = Object.create(Car.prototype);
BMW.prototype.constructor = BMW; // Resetting the constructor property
BMW.prototype.advertise = function() {
console.log('I am BMW with lots of uber features.');
};

var x5 = new BMW();

var myNewToy = x5.orderOneLikeThis();

myNewToy.advertise(); // => "I am BMW ..." if `BMW.prototype.constructor = BMW;` is not
// commented; "I am a generic car." otherwise.
+ +

object.create 在组合继承的基础上,子类继承一个由父类原型生成的空对象。

+
1
2
3
4
5
6
7
8
9
10
// js代码
function Child(name) {
Father.call(this);
}

Child.prototype = Object.create(Father.prototype, {
constructor: {
value: Child
}
});
+ +

inherits 函数 — Nodejs util.inherits 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// js代码

function inherits = function(ctor, superCtor) {
ctor.super_ = superCtor; // super_属性是子类继承父类时构造函数要写入的一个属性值.
ctor.prototype = Object.create(superCtor.prototype, {
constructor: {
value: ctor,
enumerable: false,
writable: true,
configurable: true
}
});
};
// 在上面的代码中 ctor 想要继承 superCtor ,我们姑且把 ctor 称作子类, superCtor 称作父类.

// 使用
function Child() {
Father.call(this);
//...
}
inherits(Child, Father);

Child.prototype.fun = ...
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/78/index.html b/ja/page/78/index.html new file mode 100644 index 0000000000..51e6d0b6e3 --- /dev/null +++ b/ja/page/78/index.html @@ -0,0 +1,536 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

前言

面试中时常会出现需要手动实现某个功能的要求,所以我准备整理一个自己动手实现系列文章,这一次呢就是自己动手实现 promise

+

实现 promise 思路

基础步骤

+
    +
  1. 设定三个状态 PENDINGFULFILLEDREJECTED ,只能由 PENDING 改变为 FULFILLEDREJECTED ,并且只能改变一次
  2. +
  3. MyPromise 接收一个函数 executorexecutor 有两个参数 resolve 方法和 reject 方法
  4. +
  5. resolvePENDING 改变为 FULFILLED
  6. +
  7. rejectPENDING 改变为 FULFILLED
  8. +
  9. promise 变为 FULFILLED 状态后具有一个唯一的 value
  10. +
  11. promise 变为 REJECTED 状态后具有一个唯一的 reason
  12. +
+

** then 方法**

+
    +
  1. then 方法接受两个参数 onFulfilledonRejected ,它们分别在状态由 PENDING 改变为 FULFILLEDREJECTED 后调用
  2. +
  3. 一个 promise 可绑定多个 then 方法
  4. +
  5. then 方法可以同步调用也可以异步调用
  6. +
  7. 同步调用:状态已经改变,直接调用 onFulfilled 方法
  8. +
  9. 异步调用:状态还是 PENDING ,将 onFulfilledonRejected 分别加入两个函数数组 onFulfilledCallbacksonRejectedCallbacks ,当异步调用 resolvereject 时,将两个数组中绑定的事件循环执行。
  10. +
+

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// js代码

// 定义状态常量
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

/**
* 定义MyPromise模拟Promise
* @param {func} executor 接收函数
*/
function MyPromise(executor) {
this.state = PENDING; // 默认状态为 pending
this.value = null;
this.reason = null;

// 定义成功失败的函数数组
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];

// 定义成功回调
const resolve = (value) => {
if (this.state === PENDING) {
this.state = FULFILLED;
this.value = value;

this.onFulfilledCallbacks.forEach(func => {
func();
});
}
}

// 定义失败回调
const reject = (reason) => {
if (this.state === PENDING) {
this.state = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach(func => {
func();
});
}
}

try {
executor(resolve, reject);
} catch (reason) {
reject(reason);
}
}

MyPromise.prototype.then = function (onFulfilled, onRejected) {
switch (this.state) {
case FULFILLED:
onFulfilled(this.value);
break;
case REJECTED:
onFulfilled(this.value);
break;
case PENDING:
this.onFulfilledCallbacks.push(() => {
onFulfilled(this.value);
})
this.onRejectedCallbacks.push(() => {
onRejected(this.reason);
})
break;
}
+

then方法异步调用

如下面的代码:输入顺序是:1、2、ConardLi

+
1
2
3
4
5
6
7
8
9
10
11
12
13
// js代码

console.log(1);

let promise = new Promise((resolve, reject) => {
resolve('ConardLi');
});

promise.then((value) => {
console.log(value);
});

console.log(2);
+

虽然 resolve 是同步执行的,我们必须保证 then 是异步调用的,我们用 setTimeout 来模拟异步调用(并不能实现微任务和宏任务的执行机制,只是保证异步调用)

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

// js代码
MyPromise.prototype.then = function (onFulfilled, onRejected) {
if (typeof onFulfilled != 'function') {
onFulfilled = function (value) {
return value;
}
}
if (typeof onRejected != 'function') {
onRejected = function (reason) {
throw reason;
}
}
switch (this.state) {
case FULFILLED:
setTimeout(() => {
onFulfilled(this.value);
}, 0);
break;
case REJECTED:
setTimeout(() => {
onRejected(this.reason);
}, 0);
break;
case PENDING:
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
onFulfilled(this.value);
}, 0);
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
onRejected(this.reason);
}, 0);
})
break;
}
}
+

then方法链式调用

保证链式调用,即 then 方法中要返回一个新的 promise ,并将 then 方法的返回值进行 resolve

+

注意:这种实现并不能保证 then 方法中返回一个新的 promise ,只能保证链式调用

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// js代码
MyPromise.prototype.then = function (onFulfilled, onRejected) {
if (typeof onFulfilled != 'function') {
onFulfilled = function (value) {
return value;
}
}
if (typeof onRejected != 'function') {
onRejected = function (reason) {
throw reason;
}
}

// 创建一个新的MyPromise对象
const promise2 = new MyPromise((resolve, reject) => {
switch (this.state) {
case FULFILLED:
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
break;
case REJECTED:
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
break;
case PENDING:
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
})
break;
}
})
return promise2;
}
+

catch方法

若上面没有定义 reject 方法,所有的异常会走向 catch 方法:

+
1
2
3
4
5
// js代码

MyPromise.prototype.catch = function(onRejected) {
return this.then(null, onRejected);
};
+

finally方法

不管是 resolve 还是 reject 都会调用 finally

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

MyPromise.prototype.finally = function(fn) {
return this.then(value => {
fn();
return value;
}, reason => {
fn();
throw reason;
});
};
+

Promise.resolve

Promise.resolve 用来生成一个直接处于 FULFILLED 状态的 Promise

+
1
2
3
4
5
6
// js代码
MyPromise.reject = function(value) {
return new MyPromise((resolve, reject) => {
resolve(value);
});
};
+

Promise.reject

Promise.reject 用来生成一个直接处于 REJECTED 状态的 Promise

+
1
2
3
4
5
6
// js代码
MyPromise.reject = function(reason) {
return new MyPromise((resolve, reject) => {
reject(reason);
});
};
+

all方法

接受一个 promise 数组,当所有 promise 状态 resolve 后,执行 resolve

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// js代码
MyPromise.all = function (promises) {
return new Promise((resolve, reject) => {
if (promises.length === 0) {
resolve([]);
} else {
let result = [];
let index = 0;
for (let i = 0; i < promises.length; i++) {
promises[i].then(data => {
result[i] = data;
if (++index === promises.length) {
resolve(result);
}
}, err => {
reject(err);
return;
});
}
}
});
}
+

race方法

接受一个 promise 数组,当有一个 promise 状态 resolve 后,执行 resolve

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码
MyPromise.race = function (promises) {
return new Promise((resolve, reject) => {
if (promises.length === 0) {
resolve();
} else {
let index = 0;
for (let i = 0; i < promises.length; i++) {
promises[i].then(data => {
resolve(data);
}, err => {
reject(err);
return;
});
}
}
});
}
+ +

最后

如此一个自定义的 promise 就实现了,怎么样学回来吗?

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/79/index.html b/ja/page/79/index.html new file mode 100644 index 0000000000..b8c7a3e4cf --- /dev/null +++ b/ja/page/79/index.html @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

前言

最近各种技术博客,各种技术文章,各种牛人前辈的学习心得都在看,最近看了一个不错的文章叫做《如何正确使用时间》
感觉还是很不错的,特别适合年轻的程序员看一下。下面将全文贴在下方,如果需要也可以去原文查看。

+

原文

+

你是否时常会焦虑时间过的很快,没时间学习,本文将会分享一些个人的见解。

+

花时间补基础,读文档

在工作中我们时常会花很多时间去 debug,但是你是否发现很多问题最终只是你基础不扎实或者文档没有仔细看。

+

基础是你技术的基石,一定要花时间打好基础,而不是追各种新的技术。一旦你的基础扎实,学习各种新的技术也肯定不在话下,因为新的技术,究其根本都是相通的。

+

文档同样也是一门技术的基础。一个优秀的库,开发人员肯定已经把如何使用这个库都写在文档中了,仔细阅读文档一定会是少写 bug 的最省事路子。

+

学会搜索

如果你还在使用百度搜索编程问题,请尽快抛弃这个垃圾搜索引擎。同样一个关键字,使用百度和谷歌,谷歌基本完胜的。即使你使用中文在谷歌中搜索,得到的结果也往往是谷歌占优,所以如果你想迅速的通过搜索引擎来解决问题,那一定是谷歌。

+

学点英语

说到英语,一定是大家所最不想听的。其实我一直认为程序员学习英语是简单的,因为我们工作中是一直接触着英语,并且看懂技术文章,文档所需要的单词量是极少的。我时常在群里看到大家发出一个问题的截图问什么原因,其实在截图中英语已经很明白的说明了问题的所在,如果你的英语过关,完全不需要浪费时间来提问和搜索。所以我认为学点英语也是节省时间中很重要的一点。

+

那么如何去学习呢,chrome 装个翻译插件,直接拿英文文档或文章读,不会的就直接划词翻译,然后记录下这个单词并背诵。每天花半小时看点英文文档和文章,坚持两个月,你的英语水平不说别的,看文档和文章绝对不会有难题了。这一定是一个很划算的个人时间投资,花点时间学习英语,能为你将来的技术之路铺平很多坎。

+

画个图,想一想再做

你是否遇到过这种问题,需求一下来,看一眼,然后马上就按照设计稿开始做了,可能中间出个问题导致你需要返工。

+

如果你存在这样的问题,我很推荐在看到设计稿和需求的时候花点时间想一想,画一画。考虑一下设计稿中是否可以找到可以拆分出来的复用组件,是否存在之前写过的组件。该如何组织这个界面,数据的流转是怎么样的。然后画一下这个页面的需求,最后再动手做。

+

利用好下班时间学习

说到下班时间,那可能就有人说了公司很迟下班,这其实是国内很普遍的情况。但是我认为正常的加班是可以的,但是强制的加班就是在损耗你的身体和前途。

+

可以这么说,大部分的 996 公司,加班的这些时间并不会增加你的技术,无非就是在写一些重复的业务逻辑。也许你可以拿到更多的钱,但是代价是身体还有前途。程序员是靠技术吃饭的,如果你长久呆在一个长时间加班的公司,不能增长你的技术还要吞噬你的下班学习时间,那么你一定会废掉的。如果你遇到了这种情况,只能推荐尽快跳槽到非 996 的公司。

+

那么如果你有足够的下班时间,一定要花上 1, 2 小时去学习,上班大家基本都一样,技术的精进就是看下班以后的那几个小时了。如果你能利用好下班时间来学习,坚持下去,时间一定会给你很好的答复。

+

列好 ToDo

我喜欢规划好一段时间内要做的事情,并且要把事情拆分为小点。给 ToDo 列好优先级,紧急的优先级最高。相同优先级的我喜欢先做简单的,因为这样一旦完成就能划掉一个,提高成就感。

+

反思和整理

每周末都会花上点时间整理下本周记录的笔记和看到的不错文章。然后考虑下本周完成的工作和下周准备要完成的工作。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/8/index.html b/ja/page/8/index.html new file mode 100644 index 0000000000..9228604c69 --- /dev/null +++ b/ja/page/8/index.html @@ -0,0 +1,778 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

服务器高危端口列表

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
协议端口服务渗透测试
tcp20,21FTP(文件传输协议)允许匿名的上传下载,爆破,嗅探,win提权,远程执行(proftpd 1.3.5),各类后门(proftpd,vsftp 2.3.4)
tcp22SSH (安全外壳协议 )可根据已搜集到的信息尝试爆破,v1版本可中间人,ssh隧道及内网代理转发,文件传输等等
tcp23Telnet ( 远程终端协议)爆破,嗅探,一般常用于路由,交换登陆,可尝试弱口令
tcp25SMTP(简单邮件传输协议)邮件伪造,vrfy/expn查询邮件用户信息,可使用smtp-user-enum工具来自动跑
tcp/udp53DNS(域名系统)允许区域传送,dns劫持,缓存投毒,欺骗以及各种基于dns隧道的远控
tcp/udp69TFTP (简单文件传送协议 )尝试下载目标及其的各类重要配置文件
tcp80-89,443,8440-8450,8080-8089各种常用的Web服务端口,可尝试经典的topn,vpn,owa,webmail,目标oa,各类Java控制台,各类服务器Web管理面板,各类Web中间件漏洞利用,各类Web框架漏洞利用等等……
tcp110POP3(邮局协议版本3 )可尝试爆破,嗅探
tcp111,2049NFS(网络文件系统)权限配置不当
tcp137,139,445SMB(NETBIOS协议)可尝试爆破以及smb自身的各种远程执行类漏洞利用,如,ms08-067,ms17-010,嗅探等……
tcp143IMAP(邮件访问协议)可尝试爆破
udp161SNMP(简单网络管理协议)爆破默认团队字符串,搜集目标内网信息
tcp389LDAP( 轻量目录访问协议 )ldap注入,允许匿名访问,弱口令
tcp512,513,514Linux rexec (远程登录)可爆破,rlogin登陆
tcp873Rsync (数据镜像备份工具)匿名访问,文件上传
tcp1194OpenVPN(虚拟专用通道)想办法钓VPN账号,进内网
tcp1352Lotus(Lotus软件)弱口令,信息泄漏,爆破
tcp1433SQL Server(数据库管理系统)注入,提权,sa弱口令,爆破
tcp1521Oracle(甲骨文数据库)tns爆破,注入,弹shell…
tcp1500ISPmanager( 主机控制面板)弱口令
tcp1723PPTP(点对点隧道协议 )爆破,想办法钓VPN账号,进内网
tcp2082,2083cPanel (虚拟机控制系统 )弱口令
tcp2181ZooKeeper(分布式系统的可靠协调系统 )未授权访问
tcp2601,2604Zebra (zebra路由)默认密码zerbra
tcp3128Squid (代理缓存服务器)弱口令
tcp3312,3311kangle(web服务器)弱口令
tcp3306MySQL(数据库)注入,提权,爆破
tcp3389Windows rdp(桌面协议)shift后门[需要03以下的系统],爆破,ms12-020
tcp3690SVN(开放源代码的版本控制系统)svn泄露,未授权访问
tcp4848GlassFish(应用服务器)弱口令
tcp5000Sybase/DB2(数据库)爆破,注入
tcp5432PostgreSQL(数据库)爆破,注入,弱口令
tcp5900,5901,5902VNC(虚拟网络控制台,远控)弱口令爆破
tcp5984CouchDB(数据库)未授权导致的任意指令执行
tcp6379Redis(数据库)可尝试未授权访问,弱口令爆破
tcp7001,7002WebLogic(WEB应用系统)Java反序列化,弱口令
tcp7778Kloxo(虚拟主机管理系统)主机面板登录
tcp8000Ajenti(Linux服务器管理面板)弱口令
tcp8443Plesk(虚拟主机管理面板)弱口令
tcp8069Zabbix (系统网络监视)远程执行,SQL注入
tcp8080-8089Jenkins,JBoss (应用服务器)反序列化,控制台弱口令
tcp9080-9081,9090WebSphere(应用服务器)Java反序列化/弱口令
tcp9200,9300ElasticSearch (Lucene的搜索服务器)远程执行
tcp11211Memcached(缓存系统)未授权访问
tcp27017,27018MongoDB(数据库)爆破,未授权访问
tcp50070,50030Hadoop(分布式文件系统)默认端口未授权访问
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/80/index.html b/ja/page/80/index.html new file mode 100644 index 0000000000..df49949ca8 --- /dev/null +++ b/ja/page/80/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

就在前天,公司里一位新入职的前端小伙伴找到我说遇到了一个问题,自己写的vue代码本地运行是好用的,但是打包后提给后台就访问不了接口了,刚开始我以为是代理配置的有问题,但是经过检查也没什么问题,因为是个get方法,所以我就直接在浏览器地址里敲了api地址,也能正常获取到数据,那么这么看就不是代理或者接口的问题,后来我又看了下调试工具,network中根本就没发出请求,我怀疑是直接前台就拦截了,最后我发现他这个api参数超级长,长的相当吓人,可以看一下下图。
lROUMt.png
我记得url是有长度限制的,于是我就把url变短再次请求,虽然因为参数不全,接口报了错,但是发出去了,由此我定位应该是url超长,因为vue中配置了什么导致的访问被拦截了。于是我就开始了搜索引擎查找解决方案之旅,接下来就把我了解到的信息一一整理。

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/81/index.html b/ja/page/81/index.html new file mode 100644 index 0000000000..2bad554f16 --- /dev/null +++ b/ja/page/81/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

前言

面试中时常会出现需要手动实现某个功能的要求,所以我准备整理一个自己动手实现系列文章,第一个呢就是自己动手实现 jsonp

+

实现 jsonp 思路

    +
  1. 将传入的data数据转化为url字符串形式
  2. +
  3. 处理url中的回调函数
  4. +
  5. 创建一个script标签并插入到页面中
  6. +
  7. 挂载回调函数
  8. +
+

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// js代码

(function (window,document) {
"use strict";
var jsonp = function (url,data,callback) {
// 1.将传入的data数据转化为url字符串形式
// {id:1,name:'jack'} => id=1&name=jack
var dataString = url.indexof('?') == -1? '?': '&';
for(var key in data){
dataString += key + '=' + data[key] + '&';
};

// 2 处理url中的回调函数
// cbFuncName回调函数的名字 :my_json_cb_名字的前缀 + 随机数(把小数点去掉)
var cbFuncName = 'my_json_cb_' + Math.random().toString().replace('.','');
dataString += 'callback=' + cbFuncName;

// 3.创建一个script标签并插入到页面中
var scriptEle = document.createElement('script');
scriptEle.src = url + dataString;

// 4.挂载回调函数
window[cbFuncName] = function (data) {
callback(data); // 处理完回调函数的数据之后,删除jsonp的script标签
document.body.removeChild(scriptEle);
}
document.body.appendChild(scriptEle);
}
window.$jsonp = jsonp;
})(window,document)
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/82/index.html b/ja/page/82/index.html new file mode 100644 index 0000000000..57fd055f04 --- /dev/null +++ b/ja/page/82/index.html @@ -0,0 +1,617 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

网络相关的知识虽然平时在编码阶段很少遇到,但是并不是说它不重要,尤其是前端优化层面,能从网络传输过程优化的点有很多,所以,我们需要好好掌握网络相关的知识,接下来,我将会将我了解到的网络相关的知识点进行一一整理

+

栈 Heap

+

栈是一个线性结构,在计算机中是一个相当常见的数据结构。
栈的特点是只能在某一端添加或删除数据,遵循先进后出(FILO)的原则

+
+

实现

每种数据结构都可以用很多种方式来实现,其实可以把栈看成是数组的一个子集,所以这里使用数组来实现

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// js代码
class Stack {
constructor() {
this.stack = []
}
push(item) {
this.stack.push(item)
}
pop() {
this.stack.pop()
}
peek() { // 取最后一项
return this.stack[this.getCount() - 1]
}
getCount() {
return this.stack.length
}
isEmpty() {
return this.getCount() === 0
}
}
+

应用

选取了 LeetCode 上序号为 20 的题目

+

题意是匹配括号,可以通过栈的特性来完成这道题目

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var isValid = function(str) {
let map = {
'(': -1,
')': 1,
'[': -2,
']': 2,
'{': -3,
'}': 3
}
let stack = [] // 空数组
for (let i = 0; i < str.length; i++) { // 遍历字符串每个字符
if (map[str[i]] < 0) { // 如果是左边括号,入栈
stack.push(str[i])
} else { // 否则出栈,判断左右括号加到一起是不是0
let last = stack.pop()
if (map[last] + map[str[i]] != 0) return false
}
}
if (stack.length > 0) return false // 循环完成后,判断数组中时候还有剩下的,有剩下的说明括号没闭合
return true // 否则没剩下的,都闭合了
}
+ +

队列

+

队列一个线性结构,特点是在某一端添加数据,在另一端删除数据,遵循先进先出(FIFO)的原则。

+
+

实现

这里会讲解两种实现队列的方式,分别是单链队列循环队列

+
    +
  • 链队列即队列的链式存储结构,结构上就是一个单链表,但数据只能是头进尾出。链式结构更加的灵活,特别是在存储空间上,基本不会出现溢出的情况,所以不用像循环队列一样判断队列是否已满,且空间的利用率相对较高。
      +
    • 链队列front指向头结点,头结点不存储数据,rear指向队尾结点。
    • +
    +
  • +
+
    +
  • 循环队列即为头尾相接的队列,它的最大存储空间和顺序队列一样由数组界定,但队列的长度并不一定等同于数组的长度;循环队列的队首和队尾分别由两个指针front、rear标识,于是这样就能做到首尾相接。

    +
  • +
  • 链队列:为操作方便,给链队列添加一个头结点

    +
  • +
  • 循环队列:附设两个指针front和rear分别指示队列头元素及尾元素的位置,每当插入新的队尾元素是,尾指针加1;每当删除队列头元素是,头指针加1

    +
      +
    • 如果用循环队列,则必须设定一个最大队列长度;若无法确定最大长度,则宜采用链队列。
    • +
    +
  • +
+

单链队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// js代码

class Queue {
constructor() {
this.queue = []
}
enQueue(item) {
this.queue.push(item)
}
deQueue() {
return this.queue.shift()
}
getHeader() {
return this.queue[0]
}
getLength() {
return this.queue.length
}
isEmpty() {
return this.getLength() === 0
}
}
+

因为单链队列在出队操作的时候需要 O(n) 的时间复杂度,所以引入了循环队列。
循环队列的出队操作平均是 O(1) 的时间复杂度。

+

循环队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// js代码

class SqQueue {
constructor(length) {
this.queue = new Array(length + 1)
// 队头
this.first = 0
// 队尾
this.last = 0
// 当前队列大小
this.size = 0
}
enQueue(item) {
// 判断队尾 + 1 是否为队头
// 如果是就代表需要扩容数组
// % this.queue.length 是为了防止数组越界
if (this.first === (this.last + 1) % this.queue.length) {
this.resize(this.getLength() * 2 + 1)
}
this.queue[this.last] = item
this.size++
this.last = (this.last + 1) % this.queue.length
}
deQueue() {
if (this.isEmpty()) {
throw Error('Queue is empty')
}
let r = this.queue[this.first]
this.queue[this.first] = null
this.first = (this.first + 1) % this.queue.length
this.size--
// 判断当前队列大小是否过小
// 为了保证不浪费空间,在队列空间等于总长度四分之一时
// 且不为 2 时缩小总长度为当前的一半
if (this.size === this.getLength() / 4 && this.getLength() / 2 !== 0) {
this.resize(this.getLength() / 2)
}
return r
}
getHeader() {
if (this.isEmpty()) {
throw Error('Queue is empty')
}
return this.queue[this.first]
}
getLength() {
return this.queue.length - 1
}
isEmpty() {
return this.first === this.last
}
resize(length) {
let q = new Array(length)
for (let i = 0; i < length; i++) {
q[i] = this.queue[(i + this.first) % this.queue.length]
}
this.queue = q
this.first = 0
this.last = this.size
}
}
+ +

链表

+

链表是一个线性结构,同时也是一个天然的递归结构。链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。

+
+

实现

单向链表

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// js代码

class Node {
constructor(v, next) {
this.value = v
this.next = next
}
}
class LinkList {
constructor() {
// 链表长度
this.size = 0
// 虚拟头部
this.dummyNode = new Node(null, null)
}
find(header, index, currentIndex) {
if (index === currentIndex) return header
return this.find(header.next, index, currentIndex + 1)
}
addNode(v, index) {
this.checkIndex(index)
// 当往链表末尾插入时,prev.next 为空
// 其他情况时,因为要插入节点,所以插入的节点
// 的 next 应该是 prev.next
// 然后设置 prev.next 为插入的节点
let prev = this.find(this.dummyNode, index, 0)
prev.next = new Node(v, prev.next)
this.size++
return prev.next
}
insertNode(v, index) {
return this.addNode(v, index)
}
addToFirst(v) {
return this.addNode(v, 0)
}
addToLast(v) {
return this.addNode(v, this.size)
}
removeNode(index, isLast) {
this.checkIndex(index)
index = isLast ? index - 1 : index
let prev = this.find(this.dummyNode, index, 0)
let node = prev.next
prev.next = node.next
node.next = null
this.size--
return node
}
removeFirstNode() {
return this.removeNode(0)
}
removeLastNode() {
return this.removeNode(this.size, true)
}
checkIndex(index) {
if (index < 0 || index > this.size) throw Error('Index error')
}
getNode(index) {
this.checkIndex(index)
if (this.isEmpty()) return
return this.find(this.dummyNode, index, 0).next
}
isEmpty() {
return this.size === 0
}
getSize() {
return this.size
}
}
+ +

二叉树

树拥有很多种结构,二叉树是树中最常用的结构,同时也是一个天然的递归结构。

+

二叉树拥有一个根节点,每个节点至多拥有两个子节点,分别为:左节点和右节点。树的最底部节点称之为叶节点,当一颗树的叶数量数量为满时,该树可以称之为满二叉树。

+

二分搜索树

二分搜索树也是二叉树,拥有二叉树的特性。但是区别在于二分搜索树每个节点的值都比他的左子树的值大,比右子树的值小。

+

这种存储方式很适合于数据搜索。如下图所示,当需要查找 6 的时候,因为需要查找的值比根节点的值大,所以只需要在根节点的右子树上寻找,大大提高了搜索效率。

+

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// js代码

class Node {
constructor(value) {
this.value = value
this.left = null
this.right = null
}
}
class BST {
constructor() {
this.root = null
this.size = 0
}
getSize() {
return this.size
}
isEmpty() {
return this.size === 0
}
addNode(v) {
this.root = this._addChild(this.root, v)
}
// 添加节点时,需要比较添加的节点值和当前
// 节点值的大小
_addChild(node, v) {
if (!node) {
this.size++
return new Node(v)
}
if (node.value > v) {
node.left = this._addChild(node.left, v)
} else if (node.value < v) {
node.right = this._addChild(node.right, v)
}
return node
}
}
+ +

以上是最基本的二分搜索树实现,接下来实现树的遍历。

+

对于树的遍历来说,有三种遍历方法,分别是先序遍历中序遍历后序遍历

+

三种遍历的区别在于何时访问节点。在遍历树的过程中,每个节点都会遍历三次,分别是遍历到自己,遍历左子树和遍历右子树。如果需要实现先序遍历,那么只需要第一次遍历到节点时进行操作即可。

+

以下都是递归实现.

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// js代码

// 先序遍历可用于打印树的结构
// 先序遍历先访问根节点,然后访问左节点,最后访问右节点。
preTraversal() {
this._pre(this.root)
}
_pre(node) {
if (node) {
console.log(node.value)
this._pre(node.left)
this._pre(node.right)
}
}
// 中序遍历可用于排序
// 对于 BST 来说,中序遍历可以实现一次遍历就
// 得到有序的值
// 中序遍历表示先访问左节点,然后访问根节点,最后访问右节点。
midTraversal() {
this._mid(this.root)
}
_mid(node) {
if (node) {
this._mid(node.left)
console.log(node.value)
this._mid(node.right)
}
}
// 后序遍历可用于先操作子节点
// 再操作父节点的场景
// 后序遍历表示先访问左节点,然后访问右节点,最后访问根节点。
backTraversal() {
this._back(this.root)
}
_back(node) {
if (node) {
this._back(node.left)
this._back(node.right)
console.log(node.value)
}
}
+ +

以上的这几种遍历都可以称之为深度遍历,对应的还有种遍历叫做广度遍历,也就是一层层地遍历树。对于广度遍历来说,我们需要利用之前讲过的队列结构来完成。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

breadthTraversal() {
if (!this.root) return null
let q = new Queue()
// 将根节点入队
q.enQueue(this.root)
// 循环判断队列是否为空,为空
// 代表树遍历完毕
while (!q.isEmpty()) {
// 将队首出队,判断是否有左右子树
// 有的话,就先左后右入队
let n = q.deQueue()
console.log(n.value)
if (n.left) q.enQueue(n.left)
if (n.right) q.enQueue(n.right)
}
}
+

接下来先介绍如何在树中寻找最小值或最大数。因为二分搜索树的特性,所以最小值一定在根节点的最左边,最大值相反

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码

getMin() {
return this._getMin(this.root).value
}
_getMin(node) {
if (!node.left) return node
return this._getMin(node.left)
}
getMax() {
return this._getMax(this.root).value
}
_getMax(node) {
if (!node.right) return node
return this._getMin(node.right)
}
+ +

向上取整和向下取整,这两个操作是相反的,所以代码也是类似的,这里只介绍如何向下取整。既然是向下取整,那么根据二分搜索树的特性,值一定在根节点的左侧。只需要一直遍历左子树直到当前节点的值不再大于等于需要的值,然后判断节点是否还拥有右子树。如果有的话,继续上面的递归判断。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

floor(v) {
let node = this._floor(this.root, v)
return node ? node.value : null
}
_floor(node, v) {
if (!node) return null
if (node.value === v) return v
// 如果当前节点值还比需要的值大,就继续递归
if (node.value > v) {
return this._floor(node.left, v)
}
// 判断当前节点是否拥有右子树
let right = this._floor(node.right, v)
if (right) return right
return node
}
+

排名,这是用于获取给定值的排名或者排名第几的节点的值,这两个操作也是相反的,所以这个只介绍如何获取排名第几的节点的值。对于这个操作而言,我们需要略微的改造点代码,让每个节点拥有一个 size 属性。该属性表示该节点下有多少子节点(包含自身)。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// js代码

class Node {
constructor(value) {
this.value = value
this.left = null
this.right = null
// 修改代码
this.size = 1
}
}
// 新增代码
_getSize(node) {
return node ? node.size : 0
}
_addChild(node, v) {
if (!node) {
return new Node(v)
}
if (node.value > v) {
// 修改代码
node.size++
node.left = this._addChild(node.left, v)
} else if (node.value < v) {
// 修改代码
node.size++
node.right = this._addChild(node.right, v)
}
return node
}
select(k) {
let node = this._select(this.root, k)
return node ? node.value : null
}
_select(node, k) {
if (!node) return null
// 先获取左子树下有几个节点
let size = node.left ? node.left.size : 0
// 判断 size 是否大于 k
// 如果大于 k,代表所需要的节点在左节点
if (size > k) return this._select(node.left, k)
// 如果小于 k,代表所需要的节点在右节点
// 注意这里需要重新计算 k,减去根节点除了右子树的节点数量
if (size < k) return this._select(node.right, k - size - 1)
return node
}
+ +

接下来讲解的是二分搜索树中最难实现的部分:删除节点。因为对于删除节点来说,会存在以下几种情况

+
    +
  • 需要删除的节点没有子树
  • +
  • 需要删除的节点只有一条子树
  • +
  • 需要删除的节点有左右两条树
  • +
+

对于前两种情况很好解决,但是第三种情况就有难度了,所以先来实现相对简单的操作:删除最小节点,对于删除最小节点来说,是不存在第三种情况的,删除最大节点操作是和删除最小节点相反的,所以这里也就不再赘述。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码

delectMin() {
this.root = this._delectMin(this.root)
console.log(this.root)
}
_delectMin(node) {
// 一直递归左子树
// 如果左子树为空,就判断节点是否拥有右子树
// 有右子树的话就把需要删除的节点替换为右子树
if ((node != null) & !node.left) return node.right
node.left = this._delectMin(node.left)
// 最后需要重新维护下节点的 `size`
node.size = this._getSize(node.left) + this._getSize(node.right) + 1
return node
}
+ +

最后讲解的就是如何删除任意节点了。对于这个操作,T.Hibbard 在 1962 年提出了解决这个难题的办法,也就是如何解决第三种情况。

+

当遇到这种情况时,需要取出当前节点的后继节点(也就是当前节点右子树的最小节点)来替换需要删除的节点。然后将需要删除节点的左子树赋值给后继结点,右子树删除后继结点后赋值给他。

+

你如果对于这个解决办法有疑问的话,可以这样考虑。因为二分搜索树的特性,父节点一定比所有左子节点大,比所有右子节点小。那么当需要删除父节点时,势必需要拿出一个比父节点大的节点来替换父节点。这个节点肯定不存在于左子树,必然存在于右子树。然后又需要保持父节点都是比右子节点小的,那么就可以取出右子树中最小的那个节点来替换父节点。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// js代码

delect(v) {
this.root = this._delect(this.root, v)
}
_delect(node, v) {
if (!node) return null
// 寻找的节点比当前节点小,去左子树找
if (node.value < v) {
node.right = this._delect(node.right, v)
} else if (node.value > v) {
// 寻找的节点比当前节点大,去右子树找
node.left = this._delect(node.left, v)
} else {
// 进入这个条件说明已经找到节点
// 先判断节点是否拥有拥有左右子树中的一个
// 是的话,将子树返回出去,这里和 `_delectMin` 的操作一样
if (!node.left) return node.right
if (!node.right) return node.left
// 进入这里,代表节点拥有左右子树
// 先取出当前节点的后继结点,也就是取当前节点右子树的最小值
let min = this._getMin(node.right)
// 取出最小值后,删除最小值
// 然后把删除节点后的子树赋值给最小值节点
min.right = this._delectMin(node.right)
// 左子树不动
min.left = node.left
node = min
}
// 维护 size
node.size = this._getSize(node.left) + this._getSize(node.right) + 1
return node
}
+ +

AVL 树

+

二分搜索树实际在业务中是受到限制的,因为并不是严格的 O(logN),在极端情况下会退化成链表,比如加入一组升序的数字就会造成这种情况。

+
+
+

AVL 树改进了二分搜索树,在 AVL 树中任意节点的左右子树的高度差都不大于 1,这样保证了时间复杂度是严格的 O(logN)。基于此,对 AVL 树增加或删除节点时可能需要旋转树来达到高度的平衡。

+
+

实现

因为 AVL 树是改进了二分搜索树,所以部分代码是于二分搜索树重复的,对于重复内容不作再次解析。

+

对于 AVL 树来说,添加节点会有四种情况
lWB0nf.png

+

对于左左情况来说,新增加的节点位于节点 2 的左侧,这时树已经不平衡,需要旋转。因为搜索树的特性,节点比左节点大,比右节点小,所以旋转以后也要实现这个特性。

+

旋转之前:new < 2 < C < 3 < B < 5 < A,右旋之后节点 3 为根节点,这时候需要将节点 3 的右节点加到节点 5 的左边,最后还需要更新节点的高度。

+

对于右右情况来说,相反于左左情况,所以不再赘述。

+

对于左右情况来说,新增加的节点位于节点 4 的右侧。对于这种情况,需要通过两次旋转来达到目的。

+

首先对节点的左节点左旋,这时树满足左左的情况,再对节点进行一次右旋就可以达到目的。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// js代码

class Node {
constructor(value) {
this.value = value
this.left = null
this.right = null
this.height = 1
}
}

class AVL {
constructor() {
this.root = null
}
addNode(v) {
this.root = this._addChild(this.root, v)
}
_addChild(node, v) {
if (!node) {
return new Node(v)
}
if (node.value > v) {
node.left = this._addChild(node.left, v)
} else if (node.value < v) {
node.right = this._addChild(node.right, v)
} else {
node.value = v
}
node.height =
1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
let factor = this._getBalanceFactor(node)
// 当需要右旋时,根节点的左树一定比右树高度高
if (factor > 1 && this._getBalanceFactor(node.left) >= 0) {
return this._rightRotate(node)
}
// 当需要左旋时,根节点的左树一定比右树高度矮
if (factor < -1 && this._getBalanceFactor(node.right) <= 0) {
return this._leftRotate(node)
}
// 左右情况
// 节点的左树比右树高,且节点的左树的右树比节点的左树的左树高
if (factor > 1 && this._getBalanceFactor(node.left) < 0) {
node.left = this._leftRotate(node.left)
return this._rightRotate(node)
}
// 右左情况
// 节点的左树比右树矮,且节点的右树的右树比节点的右树的左树矮
if (factor < -1 && this._getBalanceFactor(node.right) > 0) {
node.right = this._rightRotate(node.right)
return this._leftRotate(node)
}

return node
}
_getHeight(node) {
if (!node) return 0
return node.height
}
_getBalanceFactor(node) {
return this._getHeight(node.left) - this._getHeight(node.right)
}
// 节点右旋
// 5 2
// / \ / \
// 2 6 ==> 1 5
// / \ / / \
// 1 3 new 3 6
// /
// new
_rightRotate(node) {
// 旋转后新根节点
let newRoot = node.left
// 需要移动的节点
let moveNode = newRoot.right
// 节点 2 的右节点改为节点 5
newRoot.right = node
// 节点 5 左节点改为节点 3
node.left = moveNode
// 更新树的高度
node.height =
1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
newRoot.height =
1 +
Math.max(this._getHeight(newRoot.left), this._getHeight(newRoot.right))

return newRoot
}
// 节点左旋
// 4 6
// / \ / \
// 2 6 ==> 4 7
// / \ / \ \
// 5 7 2 5 new
// \
// new
_leftRotate(node) {
// 旋转后新根节点
let newRoot = node.right
// 需要移动的节点
let moveNode = newRoot.left
// 节点 6 的左节点改为节点 4
newRoot.left = node
// 节点 4 右节点改为节点 5
node.right = moveNode
// 更新树的高度
node.height =
1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
newRoot.height =
1 +
Math.max(this._getHeight(newRoot.left), this._getHeight(newRoot.right))

return newRoot
}
}
+ +

Trie

+

在计算机科学,trie,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。
简单点来说,这个结构的作用大多是为了方便搜索字符串,该树有以下几个特点

+
+
    +
  • 根节点代表空字符串,每个节点都有 N(假如搜索英文字符,就有 26 条) 条链接,每条链接代表一个字符
  • +
  • 节点不存储字符,只有路径才存储,这点和其他的树结构不同
  • +
  • 从根节点开始到任意一个节点,将沿途经过的字符连接起来就是该节点对应的字符串
  • +
+

实现

总得来说 Trie 的实现相比别的树结构来说简单的很多,实现就以搜索英文字符为例。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// js代码

class TrieNode {
constructor() {
// 代表每个字符经过节点的次数
this.path = 0
// 代表到该节点的字符串有几个
this.end = 0
// 链接
this.next = new Array(26).fill(null)
}
}
class Trie {
constructor() {
// 根节点,代表空字符
this.root = new TrieNode()
}
// 插入字符串
insert(str) {
if (!str) return
let node = this.root
for (let i = 0; i < str.length; i++) {
// 获得字符先对应的索引
let index = str[i].charCodeAt() - 'a'.charCodeAt()
// 如果索引对应没有值,就创建
if (!node.next[index]) {
node.next[index] = new TrieNode()
}
node.path += 1
node = node.next[index]
}
node.end += 1
}
// 搜索字符串出现的次数
search(str) {
if (!str) return
let node = this.root
for (let i = 0; i < str.length; i++) {
let index = str[i].charCodeAt() - 'a'.charCodeAt()
// 如果索引对应没有值,代表没有需要搜素的字符串
if (!node.next[index]) {
return 0
}
node = node.next[index]
}
return node.end
}
// 删除字符串
delete(str) {
if (!this.search(str)) return
let node = this.root
for (let i = 0; i < str.length; i++) {
let index = str[i].charCodeAt() - 'a'.charCodeAt()
// 如果索引对应的节点的 Path 为 0,代表经过该节点的字符串
// 已经一个,直接删除即可
if (--node.next[index].path == 0) {
node.next[index] = null
return
}
node = node.next[index]
}
node.end -= 1
}
}
+ +

并查集

+

并查集是一种特殊的树结构,用于处理一些不交集的合并及查询问题。该结构中每个节点都有一个父节点,如果只有当前一个节点,那么该节点的父节点指向自己。
这个结构中有两个重要的操作,分别是:

+
+
    +
  • Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
  • +
  • Union:将两个子集合并成同一个集合。
  • +
+

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// js代码

class DisjointSet {
// 初始化样本
constructor(count) {
// 初始化时,每个节点的父节点都是自己
this.parent = new Array(count)
// 用于记录树的深度,优化搜索复杂度
this.rank = new Array(count)
for (let i = 0; i < count; i++) {
this.parent[i] = i
this.rank[i] = 1
}
}
find(p) {
// 寻找当前节点的父节点是否为自己,不是的话表示还没找到
// 开始进行路径压缩优化
// 假设当前节点父节点为 A
// 将当前节点挂载到 A 节点的父节点上,达到压缩深度的目的
while (p != this.parent[p]) {
this.parent[p] = this.parent[this.parent[p]]
p = this.parent[p]
}
return p
}
isConnected(p, q) {
return this.find(p) === this.find(q)
}
// 合并
union(p, q) {
// 找到两个数字的父节点
let i = this.find(p)
let j = this.find(q)
if (i === j) return
// 判断两棵树的深度,深度小的加到深度大的树下面
// 如果两棵树深度相等,那就无所谓怎么加
if (this.rank[i] < this.rank[j]) {
this.parent[i] = j
} else if (this.rank[i] > this.rank[j]) {
this.parent[j] = i
} else {
this.parent[i] = j
this.rank[j] += 1
}
}
}
+ +

堆通常是一个可以被看做一棵树的数组对象。
堆的实现通过构造二叉堆,实为二叉树的一种。这种数据结构具有以下性质。

+
    +
  • 任意节点小于(或大于)它的所有子节点
  • +
  • 堆总是一棵完全树。即除了最底层,其他层的节点都被元素填满,且最底层从左到右填入。
    将根节点最大的堆叫做最大堆大根堆,根节点最小的堆叫做最小堆小根堆
    优先队列也完全可以用堆来实现,操作是一模一样的。
  • +
+

实现大根堆

堆的每个节点的左边子节点索引是 i * 2 + 1,右边是 i * 2 + 2,父节点是 (i - 1) /2。
堆有两个核心的操作,分别是 shiftUp 和 shiftDown 。前者用于添加元素,后者用于删除根节点。
shiftUp 的核心思路是一路将节点与父节点对比大小,如果比父节点大,就和父节点交换位置。
shiftDown 的核心思路是先将根节点和末尾交换位置,然后移除末尾元素。接下来循环判断父节点和两个子节点的大小,如果子节点大,就把最大的子节点和父节点交换。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// js代码

class MaxHeap {
constructor() {
this.heap = []
}
size() {
return this.heap.length
}
empty() {
return this.size() == 0
}
add(item) {
this.heap.push(item)
this._shiftUp(this.size() - 1)
}
removeMax() {
this._shiftDown(0)
}
getParentIndex(k) {
return parseInt((k - 1) / 2)
}
getLeftIndex(k) {
return k * 2 + 1
}
_shiftUp(k) {
// 如果当前节点比父节点大,就交换
while (this.heap[k] > this.heap[this.getParentIndex(k)]) {
this._swap(k, this.getParentIndex(k))
// 将索引变成父节点
k = this.getParentIndex(k)
}
}
_shiftDown(k) {
// 交换首位并删除末尾
this._swap(k, this.size() - 1)
this.heap.splice(this.size() - 1, 1)
// 判断节点是否有左孩子,因为二叉堆的特性,有右必有左
while (this.getLeftIndex(k) < this.size()) {
let j = this.getLeftIndex(k)
// 判断是否有右孩子,并且右孩子是否大于左孩子
if (j + 1 < this.size() && this.heap[j + 1] > this.heap[j]) j++
// 判断父节点是否已经比子节点都大
if (this.heap[k] >= this.heap[j]) break
this._swap(k, j)
k = j
}
}
_swap(left, right) {
let rightValue = this.heap[right]
this.heap[right] = this.heap[left]
this.heap[left] = rightValue
}
}
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/83/index.html b/ja/page/83/index.html new file mode 100644 index 0000000000..3c9363aafe --- /dev/null +++ b/ja/page/83/index.html @@ -0,0 +1,880 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

网络相关的知识虽然平时在编码阶段很少遇到,但是并不是说它不重要,尤其是前端优化层面,能从网络传输过程优化的点有很多,所以,我们需要好好掌握网络相关的知识,接下来,我将会将我了解到的网络相关的知识点进行一一整理

+

UDP - User Datagram Protocol - 用户数据报协议

面向报文

UDP 是一个面向报文(报文可以理解为一段段的数据)的协议。意思就是 UDP 只是报文的搬运工,不会对报文进行任何拆分和拼接操作。

+

具体来说

+
    +
  • 发送端,应用层将数据传递给传输层的 UDP 协议, UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了
  • +
  • 接收端,网络层将数据传递给传输层, UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作
  • +
+

不可靠性

    +
  1. UDP 是无连接的,也就是说通信不需要建立和断开连接。
  2. +
  3. UDP 也是不可靠的。协议收到什么数据就传递什么数据,并且也不会备份数据,对方能不能收到是不关心的
  4. +
  5. UDP 没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP
  6. +
+

高效

因为 UDP 没有 TCP 那么复杂,需要保证数据不丢失且有序到达。所以 UDP 的头部开销小,只有八字节,相比 TCP 的至少二十字节要少得多,在传输数据报文时是很高效的。

+

头部包含了以下几个数据

+
    +
  • 两个十六位的端口号,分别为源端口(可选字段)和目标端口
  • +
  • 整个数据报文的长度
  • +
  • 整个数据报文的检验和( IPv4 可选 字段),该字段用于发现头部信息和数据中的错误
  • +
+

传输方式

UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。

+

TCP

头部

TCP 头部比 UDP 头部复杂的多

+

对于 TCP 头部来说,以下几个字段是很重要的

+
    +
  • Sequence number(序号),它保证了 TCP 传输的报文都是有序的,对端可以通过序号顺序的拼接报文
  • +
  • Acknowledgement Number(确认编号),它表示数据接收端期望接收的下一个字节的编号是多少,同时也表示上一个序号的数据已经收到
  • +
  • Window Size(窗口大小),它表示还能接收多少字节的数据,用于流量控制
  • +
  • 标识符
      +
    • URG=1:该字段为一表示本数据报的数据部分包含紧急信息,是一个高优先级数据报文,此时紧急指针有效。紧急数据一定位于当前数据包数据部分的最前面,紧急指针标明了紧急数据的尾部。
    • +
    • ACK=1:该字段为一表示确认号字段有效。此外,TCP 还规定在连接建立后传送的所有报文段都必须把 ACK 置为一。
    • +
    • PSH=1:该字段为一表示接收端应该立即将数据 push 给应用层,而不是等到缓冲区满后再提交。
    • +
    • RST=1:该字段为一表示当前 TCP 连接出现严重问题,可能需要重新建立 TCP 连接,也可以用于拒绝非法的报文段和拒绝连接请求。
    • +
    • SYN=1:当SYN=1,ACK=0时,表示当前报文段是一个连接请求报文。当SYN=1,ACK=1时,表示当前报文段是一个同意建立连接的应答报文。
    • +
    • FIN=1:该字段为一表示此报文段是一个释放连接的请求报文。
    • +
    +
  • +
+

状态机

HTTP 是无连接的,所以作为下层的 TCP 协议也是无连接的,虽然看似 TCP 将两端连接了起来,但是其实只是两端共同维护了一个状态

+

TCP 的状态机是很复杂的,并且与建立断开连接时的握手息息相关,接下来就来详细描述下两种握手。
在这之前需要了解一个重要的性能指标 RTT 。该指标表示发送端发送数据到接收到对端数据所需的往返时间。

+

建立连接三次握手

在 TCP 协议中,主动发起请求的一端为客户端,被动连接的一端称为服务端。不管是客户端还是服务端,TCP 连接建立完后都能发送和接收数据,所以 TCP 也是一个全双工的协议。

+

起初,两端都为 CLOSED 状态。在通信开始前,双方都会创建 TCB。 服务器创建完 TCB 后遍进入 LISTEN 状态,此时开始等待客户端发送数据。

+

第一次握手

客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态,x 表示客户端的数据通信初始序号。

+

第二次握手

服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。

+

第三次握手

当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。

+

PS:第三次握手可以包含数据,通过 TCP 快速打开(TFO)技术。其实只要涉及到握手的协议,都可以使用类似 TFO 的方式,客户端和服务端存储相同 cookie,下次握手时发出 cookie 达到减少 RTT 的目的。

+

你是否有疑惑明明两次握手就可以建立起连接,为什么还需要第三次应答?

+

因为这是为了防止失效的连接请求报文段被服务端接收,从而产生错误。

+

可以想象如下场景。客户端发送了一个连接请求 A,但是因为网络原因造成了超时,这时 TCP 会启动超时重传的机制再次发送一个连接请求 B。此时请求顺利到达服务端,服务端应答完就建立了请求。如果连接请求 A 在两端关闭后终于抵达了服务端,那么这时服务端会认为客户端又需要建立 TCP 连接,从而应答了该请求并进入 ESTABLISHED 状态。此时客户端其实是 CLOSED 状态,那么就会导致服务端一直等待,造成资源的浪费。

+

PS:在建立连接中,任意一端掉线,TCP 都会重发 SYN 包,一般会重试五次,在建立连接中可能会遇到 SYN FLOOD 攻击。遇到这种情况你可以选择调低重试次数或者干脆在不能处理的情况下拒绝请求。

+

断开链接四次握手

TCP 是全双工的,在断开连接时两端都需要发送 FIN 和 ACK。

+

第一次握手

若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。

+

第二次握手

B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,表示 A 到 B 的连接已经释放,不接收 A 发的数据了。但是因为 TCP 连接时双向的,所以 B 仍旧可以发送数据给 A。

+

第三次握手

B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。

+

PS:通过延迟确认的技术(通常有时间限制,否则对方会误认为需要重传),可以将第二次和第三次握手合并,延迟 ACK 包的发送。

+

第四次握手

A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。

+

为什么 A 要进入 TIME-WAIT 状态,等待 2MSL 时间后才进入 CLOSED 状态?

+

为了保证 B 能收到 A 的确认应答。若 A 发完确认应答后直接进入 CLOSED 状态,如果确认应答因为网络问题一直没有到达,那么会造成 B 不能正常关闭。

+

ARQ 协议

ARQ 协议也就是超时重传机制。通过确认和超时机制保证了数据的正确送达,ARQ 协议包含停止等待 ARQ 和连续 ARQ

+

停止等待 ARQ

正常传输过程

只要 A 向 B 发送一段报文,都要停止发送并启动一个定时器,等待对端回应,在定时器时间内接收到对端应答就取消定时器并发送下一段报文。

+

报文丢失或出错

在报文传输的过程中可能会出现丢包。这时候超过定时器设定的时间就会再次发送丢包的数据直到对端响应,所以需要每次都备份发送的数据。

+

即使报文正常的传输到对端,也可能出现在传输过程中报文出错的问题。这时候对端会抛弃该报文并等待 A 端重传。

+

PS:一般定时器设定的时间都会大于一个 RTT 的平均时间。

+

ACK 超时或丢失

对端传输的应答也可能出现丢失或超时的情况。那么超过定时器时间 A 端照样会重传报文。这时候 B 端收到相同序号的报文会丢弃该报文并重传应答,直到 A 端发送下一个序号的报文。

+

在超时的情况下也可能出现应答很迟到达,这时 A 端会判断该序号是否已经接收过,如果接收过只需要丢弃应答即可。

+

这个协议的缺点就是传输效率低,在良好的网络环境下每次发送报文都得等待对端的 ACK 。

+

连续 ARQ

在连续 ARQ 中,发送端拥有一个发送窗口,可以在没有收到应答的情况下持续发送窗口内的数据,这样相比停止等待 ARQ 协议来说减少了等待时间,提高了效率。

+

累计确认

连续 ARQ 中,接收端会持续不断收到报文。如果和停止等待 ARQ 中接收一个报文就发送一个应答一样,就太浪费资源了。通过累计确认,可以在收到多个报文以后统一回复一个应答报文。报文中的 ACK 可以用来告诉发送端这个序号之前的数据已经全部接收到了,下次请发送这个序号 + 1的数据。

+

但是累计确认也有一个弊端。在连续接收报文时,可能会遇到接收到序号 5 的报文后,并未接到序号 6 的报文,然而序号 7 以后的报文已经接收。遇到这种情况时,ACK 只能回复 6,这样会造成发送端重复发送数据,这种情况下可以通过 Sack 来解决,这个会在下文说到。

+

滑动窗口

在上面小节中讲到了发送窗口。在 TCP 中,两端都维护着窗口:分别为发送端窗口和接收端窗口。

+

发送端窗口包含已发送但未收到应答的数据和可以发送但是未发送的数据。

+

发送端窗口是由接收窗口剩余大小决定的。接收方会把当前接收窗口的剩余大小写入应答报文,发送端收到应答后根据该值和当前网络拥塞情况设置发送窗口的大小,所以发送窗口的大小是不断变化的。

+

当发送端接收到应答报文后,会随之将窗口进行滑动

+

滑动窗口实现了流量控制。接收方通过报文告知发送方还可以发送多少数据,从而保证接收方能够来得及接收数据。

+

Zero 窗口

在发送报文的过程中,可能会遇到对端出现零窗口的情况。在该情况下,发送端会停止发送数据,并启动 persistent timer 。该定时器会定时发送请求给对端,让对端告知窗口大小。在重试次数超过一定次数后,可能会中断 TCP 链接。

+

拥塞处理

拥塞处理和流量控制不同,后者是作用于接收方,保证接收方来得及接受数据。而前者是作用于网络,防止过多的数据拥塞网络,避免出现网络负载过大的情况。

+

拥塞处理包括了四个算法,分别为:慢开始拥塞避免快速重传快速恢复

+

慢开始算法

慢开始算法,顾名思义,就是在传输开始时将发送窗口慢慢指数级扩大,从而避免一开始就传输大量数据导致网络拥塞。

+

慢开始算法步骤具体如下

+
    +
  1. 连接初始设置拥塞窗口(Congestion Window) 为 1 MSS(一个分段的最大数据量)
  2. +
  3. 每过一个 RTT 就将窗口大小乘二
  4. +
  5. 指数级增长肯定不能没有限制的,所以有一个阈值限制,当窗口大小大于阈值时就会启动拥塞避免算法。
  6. +
+

拥塞避免算法

拥塞避免算法相比简单点,每过一个 RTT 窗口大小只加一,这样能够避免指数级增长导致网络拥塞,慢慢将大小调整到最佳值。

+

在传输过程中可能定时器超时的情况,这时候 TCP 会认为网络拥塞了,会马上进行以下步骤:

+
    +
  • 将阈值设为当前拥塞窗口的一半
  • +
  • 将拥塞窗口设为 1 MSS
  • +
  • 启动拥塞避免算法
  • +
+

快速重传

快速重传一般和快恢复一起出现。一旦接收端收到的报文出现失序的情况,接收端只会回复最后一个顺序正确的报文序号(没有 Sack 的情况下)。如果收到三个重复的 ACK,无需等待定时器超时再重发而是启动快速重传。具体算法分为两种:

+

TCP Taho 实现如下

    +
  • 将阈值设为当前拥塞窗口的一半
  • +
  • 将拥塞窗口设为 1 MSS
  • +
  • 重新开始慢开始算法
  • +
+

TCP Reno 实现如下

    +
  • 拥塞窗口减半
  • +
  • 将阈值设为当前拥塞窗口
  • +
  • 进入快恢复阶段(重发对端需要的包,一旦收到一个新的 ACK 答复就退出该阶段)
  • +
  • 使用拥塞避免算法
  • +
+

TCP New Ren 改进后的快恢复

TCP New Reno 算法改进了之前 TCP Reno 算法的缺陷。在之前,快恢复中只要收到一个新的 ACK 包,就会退出快恢复。

+

在 TCP New Reno 中,TCP 发送方先记下三个重复 ACK 的分段的最大序号。

+

假如我有一个分段数据是 1 ~ 10 这十个序号的报文,其中丢失了序号为 3 和 7 的报文,那么该分段的最大序号就是 10。发送端只会收到 ACK 序号为 3 的应答。这时候重发序号为 3 的报文,接收方顺利接收并会发送 ACK 序号为 7 的应答。这时候 TCP 知道对端是有多个包未收到,会继续发送序号为 7 的报文,接收方顺利接收并会发送 ACK 序号为 11 的应答,这时发送端认为这个分段接收端已经顺利接收,接下来会退出快恢复阶段。

+

HTTP

HTTP 协议是个无状态协议,不会保存状态。

+

PostGet 的区别

先引入副作用幂等的概念。

+
+

副作用指对服务器上的资源做改变,搜索是无副作用的,注册是副作用的。

+
+
+

幂等指发送 M 和 N 次请求(两者不相同且都大于 1),服务器上资源的状态一致,比如注册 10 个和 11 个帐号是不幂等的,对文章进行更改 10 次和 11 次是幂等的。

+
+

在规范的应用场景上说, Get 多用于无副作用,幂等的场景,例如搜索关键字。 Post 多用于副作用,不幂等的场景,例如注册。

+

在技术上说:

+
    +
  • Get 请求能缓存,Post 不能
  • +
  • Post 相对 Get 安全一点点,因为Get 请求都包含在 URL 里,且会被浏览器保存历史纪录,Post 不会,但是在抓包的情况下都是一样的。
  • +
  • Post 可以通过 request body来传输比 Get 更多的数据,Get 没有这个技术
  • +
  • URL有长度限制,会影响 Get 请求,但是这个长度限制是浏览器规定的,不是 RFC 规定的
  • +
  • Post 支持更多的编码类型且不对数据类型限制
  • +
+

常见状态码

2XX 成功

200 OK,表示从客户端发来的请求在服务器端被正确处理
204 No content,表示请求成功,但响应报文不含实体的主体部分
205 Reset Content,表示请求成功,但响应报文不含实体的主体部分,但是与 204 响应不同在于要求请求方重置内容
206 Partial Content,进行范围请求

+

3XX 重定向

301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
302 found,临时性重定向,表示资源临时被分配了新的 URL
303 see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源
304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况
307 temporary redirect,临时重定向,和302含义类似,但是期望客户端保持请求方法不变向新的地址发出请求

+

4XX 客户端错误

400 bad request,请求报文存在语法错误
401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
403 forbidden,表示对请求资源的访问被服务器拒绝
404 not found,表示在服务器上没有找到请求的资源

+

5XX 服务器错误

500 internal sever error,表示服务器端在执行请求时发生了错误
501 Not Implemented,表示服务器不支持当前请求所需要的某个功能
503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求

+

HTTP 首部

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
通用字段作用
Cache-Control控制缓存的行为
Connection浏览器想要优先使用的连接类型,比如 keep-alive
Date创建报文时间
Pragma报文指令
Via代理服务器相关信息
Transfer-Encoding传输编码方式
Upgrade要求客户端升级协议
Warning在内容中可能存在错误
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
请求字段作用
Accept能正确接收的媒体类型
Accept-Charset能正确接收的字符集
Accept-Encoding能正确接收的编码格式列表
Accept-Language能正确接收的语言列表
Expect期待服务端的指定行为
From请求方邮箱地址
Host服务器的域名
If-Match两端资源标记比较
If-Modified-Since本地资源未修改返回 304(比较时间)
If-None-Match本地资源未修改返回 304(比较标记)
User-Agent客户端信息
Max-Forwards限制可被代理及网关转发的次数
Proxy-Authorization向代理服务器发送验证信息
Range请求某个内容的一部分
Referer表示浏览器所访问的前一个页面
TE传输编码方式
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
响应字段作用
Accept-Ranges是否支持某些种类的范围
Age资源在代理缓存中存在的时间
ETag资源标识
Location客户端重定向到某个 URL
Proxy-Authenticate向代理服务器发送验证信息
Server服务器名字
WWW-Authenticate获取资源需要的验证信息
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
实体字段作用
Allow资源的正确请求方式
Content-Encoding内容的编码格式
Content-Language内容使用的语言
Content-Lengthrequest body 长度
Content-Location返回数据的备用地址
Content-MD5Base64加密格式的内容 MD5检验值
Content-Range内容的位置范围
Content-Type内容的媒体类型
Expires内容的过期时间
Last_modified内容的最后修改时间
+

HTTPS

HTTPS 还是通过了 HTTP 来传输信息,但是信息通过 TLS 协议进行了加密。

+

TLS

TLS 协议位于传输层之上,应用层之下。首次进行 TLS 协议传输需要两个 RTT ,接下来可以通过 Session Resumption 减少到一个 RTT

+

TLS 中使用了两种加密技术,分别为:对称加密非对称加密

+

对称加密:
对称加密就是两边拥有相同的秘钥,两边都知道如何将密文加密解密。

+

非对称加密:
有公钥私钥之分,公钥所有人都可以知道,可以将数据用公钥加密,但是将数据解密必须使用私钥解密,私钥只有分发公钥的一方才知道。

+

TLS 握手过程如下图:

+
    +
  1. 客户端发送一个随机值,需要的协议和加密方式
  2. +
  3. 服务端收到客户端的随机值,自己也产生一个随机值,并根据客户端需求的协议和加密方式来使用对应的方式,发送自己的证书(如果需要验证客户端证书需要说明)
  4. +
  5. 客户端收到服务端的证书并验证是否有效,验证通过会再生成一个随机值,通过服务端证书的公钥去加密这个随机值并发送给服务端,如果服务端需要验证客户端证书的话会附带证书
  6. +
  7. 服务端收到加密过的随机值并使用私钥解密获得第三个随机值,这时候两端都拥有了三个随机值,可以通过这三个随机值按照之前约定的加密方式生成密钥,接下来的通信就可以通过该密钥来加密解密
  8. +
+

通过以上步骤可知,在 TLS 握手阶段,两端使用非对称加密的方式来通信,但是因为非对称加密损耗的性能比对称加密大,所以在正式传输数据时,两端使用对称加密的方式通信。

+

PS:以上说明的都是 TLS 1.2 协议的握手情况,在 1.3 协议中,首次建立连接只需要一个 RTT,后面恢复连接不需要 RTT 了。

+

HTTP 2.0

HTTP 2.0 相比于 HTTP 1.X,可以说是大幅度提高了 web 的性能。

+

在 HTTP 1.X 中,为了性能考虑,我们会引入雪碧图、将小图内联、使用多个域名等等的方式。这一切都是因为浏览器限制了同一个域名下的请求数量,当页面中需要请求很多资源的时候,队头阻塞(Head of line blocking)会导致在达到最大请求数量时,剩余的资源需要等待其他资源请求完成后才能发起请求。

+

你可以通过 该链接 感受下 HTTP 2.0 比 HTTP 1.X 到底快了多少。

+

在 HTTP 1.X 中,因为队头阻塞的原因,你会发现请求是这样的
lWJGkt.png
在 HTTP 2.0 中,因为引入了多路复用,你会发现请求是这样的
lWJa6g.png

+

二进制传输

HTTP 2.0 中所有加强性能的核心点在于此。在之前的 HTTP 版本中,我们是通过文本的方式传输数据。在 HTTP 2.0 中引入了新的编码机制,所有传输的数据都会被分割,并采用二进制格式编码。

+

多路复用

在 HTTP 2.0 中,有两个非常重要的概念,分别是帧(frame)流(stream)
帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流。

+

多路复用,就是在一个 TCP 连接中可以存在多条流。换句话说,也就是可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能。

+

Header 压缩

在 HTTP 1.X 中,我们使用文本的形式传输 header,在 header 携带 cookie 的情况下,可能每次都需要重复传输几百到几千的字节。

+

在 HTTP 2.0 中,使用了 HPACK 压缩格式对传输的 header 进行编码,减少了 header 的大小。并在两端维护了索引表,用于记录出现过的 header ,后面在传输过程中就可以传输已经记录过的 header 的键名,对端收到数据后就可以通过键名找到对应的值。

+

服务端 Push

在 HTTP 2.0 中,服务端可以在客户端某个请求后,主动推送其他资源。

+

可以想象以下情况,某些资源客户端是一定会请求的,这时就可以采取服务端 push 的技术,提前给客户端推送必要的资源,这样就可以相对减少一点延迟时间。当然在浏览器兼容的情况下你也可以使用 prefetch 。

+

QUIC

这是一个谷歌出品的基于 UDP 实现的同为传输层的协议,目标很远大,希望替代 TCP 协议。

+
    +
  • 该协议支持多路复用,虽然 HTTP 2.0 也支持多路复用,但是下层仍是 TCP,因为 TCP 的重传机制,只要一个包丢失就得判断丢失包并且重传,导致发生队头阻塞的问题,但是 UDP 没有这个机制
  • +
  • 实现了自己的加密协议,通过类似 TCP 的 TFO 机制可以实现 0-RTT,当然 TLS 1.3 已经实现了 0-RTT 了
  • +
  • 支持重传和纠错机制(向前恢复),在只丢失一个包的情况下不需要重传,使用纠错机制恢复丢失的包
      +
    • 纠错机制:通过异或的方式,算出发出去的数据的异或值并单独发出一个包,服务端在发现有一个包丢失的情况下,通过其他数据包和异或值包算出丢失包
    • +
    • 在丢失两个包或以上的情况就使用重传机制,因为算不出来了
    • +
    +
  • +
+

DNS

DNS 的作用就是通过域名查询到具体的 IP。

+

因为 IP 存在数字和英文的组合(IPv6),很不利于人类记忆,所以就出现了域名。你可以把域名看成是某个 IP 的别名,DNS 就是去查询这个别名的真正名称是什么。

+

在 TCP 握手之前就已经进行了 DNS 查询,这个查询是操作系统自己做的。当你在浏览器中想访问 www.google.com 时,会进行一下操作:

+
    +
  1. 操作系统会首先在本地缓存中查询
  2. +
  3. 没有的话会去系统配置的 DNS 服务器中查询
  4. +
  5. 如果这时候还没得话,会直接去 DNS 根服务器查询,这一步查询会找出负责 com 这个一级域名的服务器
  6. +
  7. 然后去该服务器查询 google 这个二级域名
  8. +
  9. 接下来三级域名的查询其实是我们配置的,你可以给 www 这个域名配置一个 IP,然后还可以给别的三级域名配置一个 IP
  10. +
+

以上介绍的是 DNS 迭代查询,还有种是递归查询,区别就是前者是由客户端去做请求,后者是由系统配置的 DNS 服务器做请求,得到结果后将数据返回给客户端。

+

PS:DNS 是基于 UDP 做的查询。

+

从输入 URL 到页面加载完成的过程

这是一个很经典的面试题,在这题中可以将本文讲得内容都串联起来。

+
    +
  1. 首先做 DNS 查询,如果这一步做了智能 DNS 解析的话,会提供访问速度最快的 IP 地址回来
  2. +
  3. 接下来是 TCP 握手,应用层会下发数据给传输层,这里 TCP 协议会指明两端的端口号,然后下发给网络层。网络层中的 IP 协议会确定 IP 地址,并且指示了数据传输中如何跳转路由器。然后包会再被封装到数据链路层的数据帧结构中,最后就是物理层面的传输了
  4. +
  5. TCP 握手结束后会进行 TLS 握手,然后就开始正式的传输数据
  6. +
  7. 数据在进入服务端之前,可能还会先经过负责负载均衡的服务器,它的作用就是将请求合理的分发到多台服务器上,这时假设服务端会响应一个 HTML 文件
  8. +
  9. 首先浏览器会判断状态码是什么,如果是 200 那就继续解析,如果 400500 的话就会报错,如果 300 的话会进行重定向,这里会有个重定向计数器,避免过多次的重定向,超过次数也会报错
  10. +
  11. 浏览器开始解析文件,如果是 gzip 格式的话会先解压一下,然后通过文件的编码格式知道该如何去解码文件
  12. +
  13. 文件解码成功后会正式开始渲染流程,先会根据 HTML 构建 DOM 树,有 CSS 的话会去构建 CSSOM 树。如果遇到 script 标签的话,会判断是否存在 async 或者 defer ,前者会并行进行下载并执行 JS ,后者会先下载文件,然后等待 HTML 解析完成后顺序执行,如果以上都没有,就会阻塞住渲染流程直到 JS 执行完毕。遇到文件下载的会去下载文件,这里如果使用 HTTP 2.0 协议的话会极大的提高多图的下载效率。
  14. +
  15. 初始的 HTML 被完全加载和解析后会触发 DOMContentLoaded 事件
  16. +
  17. CSSOM 树和 DOM 树构建完成后会开始生成 Render 树,这一步就是确定页面元素的布局、样式等等诸多方面的东西
  18. +
  19. 在生成 Render 树的过程中,浏览器就开始调用 GPU 绘制,合成图层,将内容显示在屏幕上了
  20. +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/84/index.html b/ja/page/84/index.html new file mode 100644 index 0000000000..87db48d78a --- /dev/null +++ b/ja/page/84/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近在做公司技术规范,需要能通过 B/S 方式进行分发,起初想法是做一个网站,用 html 的方式进行书写,但是后面感觉太麻烦了,所以就放弃了,偶然间看到有工具可以直接通过 md 文件编译生成网站,就像是 hexo 博客这样,我觉得这挺好,极大的节省了开发网站所需要的时间,只需要专注于内容就好了,所以我就研究了下来,整个了解过程中有这么两个工具映入了我的眼帘,一个是 doctify,另外一个就是今天的主角 vuepress

+ +
+ + 続きを読む » + +
+ + + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/85/index.html b/ja/page/85/index.html new file mode 100644 index 0000000000..fb766b9369 --- /dev/null +++ b/ja/page/85/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

定义

柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

+

柯里化,可以理解为提前接收部分参数,延迟执行,不立即输出结果,而是返回一个接受剩余参数的函数。因为这样的特性,也被称为部分计算函数。

+

通俗易懂的解释:用闭包把参数保存起来,当参数的数量足够执行函数了,就开始执行函数。柯里化,是一个逐步接收参数的过程。在接下来的剖析中,你会深刻体会到这一点。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码

function currying(fn){
var allArgs = [];

return function next(){
var args = [].slice.call(arguments); // 拆成数组元素

if(args.length > 0){
allArgs = allArgs.concat(args);
return next;
}else{
return fn.apply(null, allArgs);
}
}
}
+

我们来一个简单的实例验证一下:

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

var add = currying(function(){
var sum = 0;
for(var i = 0; i < arguments.length; i++){
sum += arguments[i];
}
return sum;
});

add(1)(2, 3)(4)() // => 10
+ +

应用场景

参数复用

1
2
3
4
5
6
7
8
// js代码

function getUrl(domain, protocol, path) {
return protocol + "://" + domain + "/" + path;
}

var page1 = getUrl('http', 'lixuguang.github.io', 'page1.html');
var page2 = getUrl('http', 'lixuguang.github.io', 'page2.html');
+

我们使用currying来简化它:

+
1
2
var conardliSite = currying(getUrl)
var page1 = conardliSite('page1.html')('http', 'lixuguang.github.io')();
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/86/index.html b/ja/page/86/index.html new file mode 100644 index 0000000000..51ccb1b2a5 --- /dev/null +++ b/ja/page/86/index.html @@ -0,0 +1,515 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

前言

最近看了好多技术文章,好多的代码片段,突然发现好多的代码里都有 reduce ,感觉以前都没怎么关注过这个函数,为了弥补这个过失,打算专门写一篇文章说说它。

+

reduce 函数在 MDN 中是这样介绍的

+
+

reduce() 方法对数组中的每个元素执行一个由 提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。

+
+

说实话看了一脸懵逼,这上面说的叫人话?后来耐着性子看了一些代码后感觉有所理解,如果理解不对,还请斧正。
首先看一下这里面几个关键词

+

* 每个元素: * 这就是遍历咯,没啥好说的
提供的 reducer 函数:我哪有什么 reducer 函数呢,后来理解了,就是回调,有的地方呢叫 handler ,这里跟 reduce 配合就叫了 reducer
升序执行:就是说是0,1,2下标这样的顺序执行啦。
将其结果汇总为单个返回值:最后返回的是一个值,当然没说必须是什么类型。

+

这样解释完了以后,上面这句话就比较好懂了,简单说就是 reduce 里传一个回调函数,执行回调函数方法后返回一个值,就是这样。

+
+

reducer 函数接收4个参数:

+
+
    +
  • Accumulator (acc) (累计器)
  • +
  • Current Value (cur) (当前值)
  • +
  • Current Index (idx) (当前索引)
  • +
  • Source Array (src) (源数组)
    您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
  • +
+

看上面的描述一定要注意,这些参数是 reducer 的参数,不是 reduce 的参数,一共有4个,通常用前两个情况比较多。

+

那么 reduce 函数呢实际上有两个参数,第二个还可以省略 reducerinitialValueinitialValue作为第一次调用 reducer 函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错,这点切记。

+

终于把前置知识讲清楚了,接下来,我们就用 reduce 搞点事情。

+

1. 使用 reduce 实现 map

map 的话我不想再讲一遍,一句话带过,就是对回调函数中的元素进行加工后返回一个长度一样的新数组。
那么该如何实现呢,我们来想想原理吧,刚才上面说过了,reduce执行过后的返回值会作为下一次执行的第一个参数放进去,那么就可以先用一个空数组作为初始值来接受每次需要处理的元素的集合,并返回回去,作为下一次传入的第一个参数,这样每次操作完之后就可以push到这个数组中,那么剩下要做的就是处理数组中每一项的函数了,我们叫他 handler

+
1
2
3
4
5
6
7
8
// js代码

Array.prototype.reduceToMap = function (handler) { // 自定义 `map` 函数 `reduceToMap`
return this.reduce((target, current, index) => { // this指向调用他的数组
target.push(handler.call(this, current, index)) // 这里用了call方法,handler将接受两个参数 current和index
return target; // 处理完成后返回新数组
}, []) // 初始化空的新数组
};
+

就这样我们就用 reduce,实现了 map 的功能,是不是很好用?

+

2. 使用 reduce 实现 filter

filter 也是数组常用的方法,同样传入一个回调函数,处理结果返回true或false,最终 filter 会返回一个过滤后的函数。
学会了上面的 map 的实现,实际上 filter 就会很简单

+
1
2
3
4
5
6
7
8
9
10
// js代码

Array.prototype.reduceToFilter = function (handler) { // 还是自定义方法名
return this.reduce((target, current, index) => {
if (handler.call(this, current, index)) { // 这里注意 handler 要返回的是布尔类型的值
target.push(current); // 符合条件就插入新数组
} // 不符合就什么都不做
return target; // 最后返回新数组
}, []) // 初始化一个空数组
};
+

日后在看到 reduce 的妙用之后还会来补充这篇文章,如果实在懒得写,我也会链接一下新文章,希望大家长期关注。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/87/index.html b/ja/page/87/index.html new file mode 100644 index 0000000000..b404b86064 --- /dev/null +++ b/ja/page/87/index.html @@ -0,0 +1,528 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

去重

1. 利用 ObjectKey 唯一特性

开辟一个外部存储空间用于标示元素是否出现过。

+
1
2
3
4
5
6
// js代码

const unique = (array)=> {
var container = {};
return array.filter((item, index) => container.hasOwnProperty(item) ? false : (container[item] = true));
}
+ +

2. 利用 indexOf 的返回值数值进行去重

原理是 indexOf 获取元素时如果返回值不等于下标说明已经有了,配合 filter 更美味

+
1
2
3
4
5
// js代码

const unique = arr => arr.filter((e,i) =>
arr.indexOf(e) === i // 如果元素找到的当前下标和当前索引相同说明是同一个,不同说明不是唯一
);
+ +

还有一种变形方法利用 lastIndexOf 方法

+
+

lastIndexOf() 方法返回指定元素(也即有效的 JavaScript 值或变量)在数组中的最后一个的索引,如果不存在则返回 -1。从数组的后面向前查找,从 fromIndex 处开始。

+
+
1
2
3
4
5
// js代码

const filterNonUnique = arr => arr.filter(e =>
arr.indexOf(e) === arr.lastIndexOf(e) // 判断一个元素出现时的第一次下标和最后一次下标是否相同,如果相同那么就唯一
)
+

3. 利用 Set 特性去重

SetES6 中新的数据类型,它的特点就是元素唯一性,且可以和数组进行转换

+
1
2
3
4
5
6
7
// js代码

const unique = arr => Array.from(new Set(arr)); // Array.from 方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。

// 优化

const unique = arr => [...new Set(arr)]; // 利用 `ES6` 中展开操作
+

4. 排序后判断前后两项是否相等去重

通过比较相邻数字是否重复,将排序后的数组进行去重。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

const unique = (array) => {
array.sort((a, b) => a - b);
let pre = 0;
const result = [];
for (let i = 0; i < array.length; i++) {
if (!i || array[i] != array[pre]) {
result.push(array[i]);
}
pre = i;
}
return result;
}
+ +

扁平

1. 普通方法

通过递归的方式判断数组中的项是否是数组,如果不是就加入到新的扁平数组,如果是就递归调用逐层判断,直到全部结束

+
1
2
3
4
5
6
7
8
9
10
11
12
13
// js代码

const flatten = (array) => { // array 原数组
let result = []; // 定义新的扁平数组
for (let i = 0; i < array.length; i++) {
if (Array.isArray(array[i])) { // 判断子元素是否是数组
result = result.concat(flatten(array[i])); // 递归判断
} else {
result.push(array[i]); // 加入新数组
}
}
return result;
}
+ +

2. 使用reduce简化上述方法

+

reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
reducer 函数接收4个参数:

+
+
    +
  • Accumulator (acc) (累计器)
  • +
  • Current Value (cur) (当前值)
  • +
  • Current Index (idx) (当前索引)
  • +
  • Source Array (src) (源数组)
  • +
  • 您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
  • +
+

先看一段 reduce 的示例函数

+
1
2
3
4
5
6
7
8
9
10
11
12
// js代码

const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue; // 定义一个累计器函数,作用是将数组前后累计值与当前值累加

// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer)); // 没有初始值用第一个元素
// expected output: 10

// 5 + 1 + 2 + 3 + 4
console.log(array1.reduce(reducer, 5)); // 有初始值从初始值开始
// expected output: 15
+

这下大家应该对 reduce 函数认识了,接下来看看怎么简化

+
1
2
3
4
5
6
7
8
9
// js代码

function flatten(array) {
return array.reduce((newArray, current) => // 新数组,当前项
Array.isArray(current) ? // 判断当前项是否为数组
newArray.concat(flatten(current)) : // 是的话 递归调用
newArray.concat(current) // 不是的话加进新数组
, []) // 初始化新数组为空
}
+

这里我们再变一个形,增加一个变量,变成可指定深度操作数组

+
1
2
3
4
5
6
7
8
9
10
// js代码

function flattenByDeep(array, deep = 1) { // 默认一层
return array.reduce(
(target, current) =>
Array.isArray(current) && deep > 1 ?
target.concat(flattenByDeep(current, deep - 1)) : // 下一次减一层
target.concat(current)
, [])
}
+

最值

利用 reduce

reduce 函数真的是超级好用,

+
1
2
3
// js代码

array.reduce((c,n) => Math.max(c,n))
+ +

Math.max

Math.max 参数原本是一组数字,只需要让他可以接收数组即可。

+
1
2
3
4
5
// js代码

const array = [3,2,1,4,5];
Math.max.apply(null,array);
Math.max(...array);
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/88/index.html b/ja/page/88/index.html new file mode 100644 index 0000000000..3ff2f4c6fb --- /dev/null +++ b/ja/page/88/index.html @@ -0,0 +1,634 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

基本概念

时间复杂度

一个算法的时间复杂度反映了程序运行从开始到结束所需要的时间。把算法中基本操作重复执行的次数(频度)作为算法的时间复杂度。
没有循环语句,记作O(1),也称为常数阶。只有一重循环,则算法的基本操作的执行频度与问题规模n呈线性增大关系,记作O(n),也叫线性阶。
常见的时间复杂度有:

+
    +
  • O(1): Constant Complexity: Constant 常数复杂度
  • +
  • O(log n): Logarithmic Complexity: 对数复杂度
  • +
  • O(n): Linear Complexity: 线性时间复杂度
  • +
  • O(n^2): N square Complexity 平⽅方
  • +
  • O(n^3): N square Complexity ⽴立⽅方
  • +
  • O(2^n): Exponential Growth 指数
  • +
  • O(n!): Factorial 阶乘
  • +
+

空间复杂度

一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。

+

一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。

+
    +
  • 稳定
  • +
  • 不稳定

    算法汇总

    十大经典排序.jpg

    关于时间复杂度:

    平方阶 (O(n2)) 排序 各类简单排序:直接插入、直接选择和冒泡排序。
    线性对数阶 (O(nlog2n)) 排序 快速排序、堆排序和归并排序;
    O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数。 希尔排序
    线性阶 (O(n)) 排序 基数排序,此外还有桶、箱排序。
  • +
+

关于稳定性:

稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。

+

名词解释:

    +
  • n:数据规模
  • +
  • k:“桶”的个数
  • +
  • In-place:占用常数内存,不占用额外内存
  • +
  • Out-place:占用额外内存
  • +
  • 稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同
  • +
+

1. 冒泡排序(Bubble Sort) — 前后两两比较 — 气泡

冒泡排序可谓是最经典的排序算法了,它是基于比较的排序算法,其优点是实现简单,排序数量较小时性能较好。

+

它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

+

1. 1 算法原理

相邻的数据进行两两比较,小数放在前面,大数放在后面,如果前面的数据比后面的数据大,就交换这两个数的位置。也可以实现大数放在前面,小数放在后面,如果前面的数据比后面的小,就交换两个的位置。要实现上述规则需要用到两层for循环。

+

1. 2 算法描述

    +
  1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  2. +
  3. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  4. +
  5. 针对所有的元素重复以上的步骤,除了最后一个;
  6. +
  7. 重复步骤1~3,直到排序完成。
  8. +
+

1. 3 动图演示

ldB5VS.gif

+

1. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// js代码

function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len; i++) {
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { // 相邻元素两两对比
// 元素交换
/** 1.使用中间变量 **/
var temp = arr[j + 1];
arr[j + 1] = arr[j]
arr[j] = temp
/** 2.适用纯数字的数组排序 **/
arr[j] = arr[j] + arr[j + 1]
arr[j + 1] = arr[j] - arr[j + 1]
arr[j] -= arr[j + 1]
/** 3.使用es6解构赋值 **/
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
}
}
}
return arr;
}
+

冒泡排序算法优化

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len; i++) {
var exchange=false; // 交换标志
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { // 相邻元素两两对比
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]] // 元素交换
exchange=true; //
}
}
if(!exchange){ // 若本趟排序未发生交换,提前终止算法
break;
}
}
return arr;
}
+

2. 选择排序(Selection Sort) — 面试挑简历,在剩下的里面挑最好的

表现最稳定的排序算法之一,因为无论什么数据进去都是O(n²)的时间复杂度。。。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。

+

2. 1 算法原理

先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

+

2. 2 算法描述

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

+
    +
  1. 初始状态:无序区为R[1..n],有序区为空;
  2. +
  3. 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中
  4. +
  5. 选出关键字最小的记录R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
  6. +
  7. n-1趟结束,数组有序化了。
  8. +
+

2. 3 动图演示

ldDWW9.gif

+

2. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

function selectionSort(arr) {
var len = arr.length;
var minIndex, temp;
for (var i = 0; i < len - 1; i++) {
minIndex = i;
for (var j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) { //寻找最小的数
minIndex = j; //将最小数的索引保存
}
}
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
return arr;
}
+ +

3. 插入排序(Insertion Sort)—– 麻将/扑克

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。

+

3. 1 算法原理

它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

+

3. 2 算法描述

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

+
    +
  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. +
  3. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  4. +
  5. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  6. +
  7. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  8. +
  9. 将新元素插入到该位置后;
  10. +
  11. 重复步骤2~5。
  12. +
+

3. 3 动图演示

ldDfzR.gif

+

3. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// js代码

function insertSort(arr) {
// 从1位置开始遍历arr中每元素,同时声明空变量temp
for (let i = 1; i < arr.length; i++) {
if (arr[i] < arr[i - 1]) { // 如果当前元素<前一个元素
let temp = arr[i] // 将当前元素值临时保存在temp中
let p = i - 1 // 定义变量 p = i- 1
// 循环 条件:
// 1. p>=0且temp小于p位置的元素
while (p >= 0 && temp < arr[p]) {
// 循环体: 将P位置的值赋值给p的后一个元素
arr[p + 1] = arr[p]
p-- // p向前移动一个
}
arr[p + 1] = temp // 将temp的值赋值给p+1位置的元素
}
}
}
+ +

4. 快速排序(Selection Sort)

快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

+

4. 1 算法原理

快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。

+

4. 2 算法描述

选基准:在数据结构中选择一个元素作为基准(pivot
划分区:参照基准元素值的大小,划分无序区,所有小于基准元素的数据放入一个区间,所有大于基准元素的数据放入另一区间,分区操作结束后,基准元素所处的位置就是最终排序后它应该所处的位置
递归:对初次划分出来的两个无序区间,递归调用第 1步和第 2步的算法,直到所有无序区间都只剩下一个元素为止。

+

简单理解就是,选择一个目标值,比目标值小的放左边,比目标值大的放右边,目标值的位置已排好,将左右两侧再进行快排。

+

4. 3 动图演示

快速排序.gif

+

4. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// js代码

function quickSort(arr){
//如果arr.length<=1,则直接返回arr
if(arr.length<=1){return arr}
// arr的元素个数/2,再下去整,将值保存在pivotIndex中
var pivotIndex=Math.floor(arr.length/2);
// 将arr中pivotIndex位置的元素,保存在变量pivot中
var pivot=arr[pivotIndex];
//声明空数组left和right
var left=[];
var right=[];
for(var i=0;i<arr.length;i++){ // 遍历arr中每个元素
if(i !== pivotIndex){ // 如果i !== pivotIndex
if(arr[i]<=pivot){ // 如果当前元素值<pivot
left.push(arr[i]); // 就将当前值压入left
}else{
right.push(arr[i]); // 就将当前值压入right
}
}
}
//递归
return quickSort(left).concat(pivot, quickSort(right)); // 链接多个数组到 left 从小到大
}
+

5. 希尔排序

5. 1 算法原理

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:

+
    +
  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
  • +
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
    希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
  • +
+

5. 2 算法描述

    +
  • 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
  • +
  • 按增量序列个数 k,对序列进行 k 趟排序;
  • +
  • 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
  • +
+

5.3 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// js代码

function shellSort(arr) {
var len = arr.length,
temp,
gap = 1;
while(gap < len/3) { //动态定义间隔序列
gap =gap*3+1;
}
for (gap; gap > 0; gap = Math.floor(gap/3)) {
for (var i = gap; i < len; i++) {
temp = arr[i];
for (var j = i-gap; j >= 0 && arr[j] > temp; j-=gap) {
arr[j+gap] = arr[j];
}
arr[j+gap] = temp;
}
}
return arr;
}
+ +

6. 归并排序

6. 1 算法原理

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

+
    +
  • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
  • +
  • 自下而上的迭代;
    +

    在《数据结构与算法 JavaScript 描述》中,作者给出了自下而上的迭代方法。但是对于递归法,作者却认为:
    However, it is not possible to do so in JavaScript, as the recursion goes too deep for the language to handle.
    然而,在 JavaScript 中这种方式不太可行,因为这个算法的递归深度对它来讲太深了。

    +
    +
  • +
+

说实话,我不太理解这句话。意思是 JavaScript 编译器内存太小,递归太深容易造成内存溢出吗?还望有大神能够指教。

+

和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。

+

6. 2 算法描述

    +
  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
  2. +
  3. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
  4. +
  5. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
  6. +
  7. 重复步骤 3 直到某一指针达到序列尾;
  8. +
  9. 将另一序列剩下的所有元素直接复制到合并序列尾。
  10. +
+

6. 3 动图演示

归并排序.gif

+

6. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// js代码

function mergeSort(arr) { // 采用自上而下的递归方法
var len = arr.length;
if(len < 2) {
return arr;
}
var middle = Math.floor(len / 2),
left = arr.slice(0, middle),
right = arr.slice(middle);
return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right){
var result = [];
while (left.length && right.length) {
if (left[0] <= right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}

while (left.length)
result.push(left.shift());

while (right.length)
result.push(right.shift());

return result;
}
+ +

7. 堆排序

7. 1 算法原理

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

+
    +
  • 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
  • +
  • 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
    堆排序的平均时间复杂度为 Ο(nlogn)。
  • +
+

7. 2 算法描述

    +
  1. 将待排序序列构建成一个堆 H[0……n-1],根据(升序降序需求)选择大顶堆或小顶堆;
  2. +
  3. 把堆首(最大值)和堆尾互换;
  4. +
  5. 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
  6. +
  7. 重复步骤 2,直到堆的尺寸为 1。
  8. +
+

7. 3 动图演示

堆排序.gif

+

7. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// js代码

var len; // 因为声明的多个函数都需要数据长度,所以把len设置成为全局变量

function buildMaxHeap(arr) { // 建立大顶堆
len = arr.length;
for (var i = Math.floor(len/2); i >= 0; i--) {
heapify(arr, i);
}
}

function heapify(arr, i) { // 堆调整
var left = 2 * i + 1,
right = 2 * i + 2,
largest = i;

if (left < len && arr[left] > arr[largest]) {
largest = left;
}

if (right < len && arr[right] > arr[largest]) {
largest = right;
}

if (largest != i) {
swap(arr, i, largest);
heapify(arr, largest);
}
}

function swap(arr, i, j) {
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

function heapSort(arr) {
buildMaxHeap(arr);

for (var i = arr.length-1; i > 0; i--) {
swap(arr, 0, i);
len--;
heapify(arr, 0);
}
return arr;
}
+ +

8. 计数排序

8. 1 算法原理

计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

+

8. 2 算法描述

8. 3 动图演示

计数排序.gif

+

8. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// js代码

function countingSort(arr, maxValue) {
var bucket = new Array(maxValue+1),
sortedIndex = 0;
arrLen = arr.length,
bucketLen = maxValue + 1;

for (var i = 0; i < arrLen; i++) {
if (!bucket[arr[i]]) {
bucket[arr[i]] = 0;
}
bucket[arr[i]]++;
}

for (var j = 0; j < bucketLen; j++) {
while(bucket[j] > 0) {
arr[sortedIndex++] = j;
bucket[j]--;
}
}

return arr;
}
+ +

9. 桶排序

9. 1 算法原理

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

+
    +
  • 在额外空间充足的情况下,尽量增大桶的数量
  • +
  • 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
    同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

    9. 2 算法描述

    1. 什么时候最快

    当输入的数据可以均匀的分配到每一个桶中。

    2. 什么时候最慢

    当输入的数据被分配到了同一个桶中。

    9. 3 动图演示

  • +
+

9. 4 js代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// js代码

function bucketSort(arr, bucketSize) {
if (arr.length === 0) {
return arr;
}

var i;
var minValue = arr[0];
var maxValue = arr[0];
for (i = 1; i < arr.length; i++) {
if (arr[i] < minValue) {
minValue = arr[i]; // 输入数据的最小值
} else if (arr[i] > maxValue) {
maxValue = arr[i]; // 输入数据的最大值
}
}

//桶的初始化
var DEFAULT_BUCKET_SIZE = 5; // 设置桶的默认数量为5
bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
var bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
var buckets = new Array(bucketCount);
for (i = 0; i < buckets.length; i++) {
buckets[i] = [];
}

//利用映射函数将数据分配到各个桶中
for (i = 0; i < arr.length; i++) {
buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i]);
}

arr.length = 0;
for (i = 0; i < buckets.length; i++) {
insertionSort(buckets[i]); // 对每个桶进行排序,这里使用了插入排序
for (var j = 0; j < buckets[i].length; j++) {
arr.push(buckets[i][j]);
}
}

return arr;
}
+ +

10. 基数排序

10. 1 算法原理

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

+

10. 2 算法描述

1. 基数排序 vs 计数排序 vs 桶排序

基数排序有三种方法:
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异案例看大家发的:

+
    +
  • 基数排序:根据键值的每位数字来分配桶;
  • +
  • 计数排序:每个桶只存储单一键值;
  • +
  • 桶排序:每个桶存储一定范围的数值;

    10. 3 动图演示

  • +
+
    +
  1. LSD 基数排序动图演示
    基数排序.gif

    10. 4 js代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // js代码

    // LSD Radix Sort
    var counter = [];
    function radixSort(arr, maxDigit) {
    var mod = 10;
    var dev = 1;
    for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
    for(var j = 0; j < arr.length; j++) {
    var bucket = parseInt((arr[j] % mod) / dev);
    if(counter[bucket]==null) {
    counter[bucket] = [];
    }
    counter[bucket].push(arr[j]);
    }
    var pos = 0;
    for(var j = 0; j < counter.length; j++) {
    var value = null;
    if(counter[j]!=null) {
    while ((value = counter[j].shift()) != null) {
    arr[pos++] = value;
    }
    }
    }
    }
    return arr;
    }
  2. +
+

总结

排序算法.png
以上就是十大经典算法,算法对于前端来说并不是一个十分熟悉的领域,但是排序算法算是算法里比较入门的,还是需要掌握的,毕竟即使是为了面试也是要准备的。

+

参考资料

https://github.com/hustcc/JS-Sorting-Algorithm
一本关于排序算法的 GitBook 在线书籍 《十大经典排序算法》,多语言实现。

+

http://www.sohu.com/a/136157205_671058
技术面试宝典: 很全面的算法和数据结构知识(含代码实现)下篇

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/89/index.html b/ja/page/89/index.html new file mode 100644 index 0000000000..f1a60b2afd --- /dev/null +++ b/ja/page/89/index.html @@ -0,0 +1,541 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

cookie,localStorage,sessionStorage,indexDB

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
特性cookielocalStoragesessionStorageindexDB
数据生命周期一般由服务器生成,可以设置过期时间除非被清理,否则一直存在页面关闭就清理除非被清理,否则一直存在
数据存储大小4K5M5M无限制
与服务端通信每次都会携带在 header 中,对于请求性能影响不参与不参与不参与
+

从上表可以看到, cookie 已经不建议用于存储。如果没有大量数据存储需求的话,可以使用 localStoragesessionStorage 。对于不怎么改变的数据尽量使用 localStorage 存储,否则可以用 sessionStorage 存储。

+

对于 cookie ,我们还需要注意安全性。
| 属性 | 作用 |
| ——— | ————————————————————– |
| value | 如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识 |
| http-only | 不能通过 JS 访问 Cookie,减少 XSS 攻击 |
| secure | 只能在协议为 HTTPS 的请求中携带 |
| same-site | 规定浏览器不能在跨域请求中携带 Cookie,减少 CSRF 攻击 |

+

Service Worker

+

Service workers 本质上充当 Web 应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步 API。

+
+

目前该技术通常用来做缓存文件,提高首屏速度,可以试着来实现这个功能。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// js代码

// index.js
if (navigator.serviceWorker) {
navigator.serviceWorker
.register('sw.js')
.then(function(registration) {
console.log('service worker 注册成功')
})
.catch(function(err) {
console.log('servcie worker 注册失败')
})
}
// sw.js
// 监听 `install` 事件,回调中缓存所需文件
self.addEventListener('install', e => {
e.waitUntil(
caches.open('my-cache').then(function(cache) {
return cache.addAll(['./index.html', './index.js'])
})
)
})

// 拦截所有请求事件
// 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据
self.addEventListener('fetch', e => {
e.respondWith(
caches.match(e.request).then(function(response) {
if (response) {
return response
}
console.log('fetch source')
})
)
})
+ +

打开页面,可以在开发者工具中的 Application 看到 Service Worker 已经启动了
Cache 中也可以发现我们所需的文件已被缓存

+

当我们重新刷新页面可以发现我们缓存的数据是从 Service Worker 中读取的

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/9/index.html b/ja/page/9/index.html new file mode 100644 index 0000000000..743b799eb1 --- /dev/null +++ b/ja/page/9/index.html @@ -0,0 +1,514 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +

systemctl 命令

systemctl

范列出系统上面有启动的unit

+

systemctl list-unit-files

列出所有已经安装的unit有哪些

+

systemctl list-units –type=service –all

列出类型为service的所有项目,不论启动与否

+

systemctl get-default

输入目前机器默认的模式,如图形界面模式或者文本模式

+

systemctl isolate multi-user.target

将目前的操作环境改为纯文本模式,关掉图形界面

+

systemctl isolate graphical.target

将目前的操作环境改为图形界面

+

systemctl poweroff

系统关机

+

systemctl reboot

重新开机

+

systemctl suspend

进入暂停模式

+

systemctl rescue

强制进入救援模式

+

systemctl hibernate

进入休眠模式

+

systemctl emergency

强制进入紧急救援模式

+

systemctl list-dependencies –reverse

查询当前默认的target关联了啥

+

systemctl list-dependencies graphical.target

查询图形界面模式的target关联了啥

+

systemctl list-sockets

查看当前的socket服务

+

systemctl show etcd.service

查看 unit 的详细配置情况

+

systemctl mask etcd.service

禁用某个服务

+

systemctl unmask etcd.service

解除禁用某个服务

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/90/index.html b/ja/page/90/index.html new file mode 100644 index 0000000000..5547249656 --- /dev/null +++ b/ja/page/90/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Proxy

因为 JS 采用 IEEE 754 双精度版本(64位),并且只要采用 IEEE 754 的语言都有该问题。

+

我们都知道计算机表示十进制是采用二进制表示的,所以 0.1 在二进制表示为

+
1
2
3
4
// js代码

// (0011) 表示循环
0.1 = 2^-4 * 1.10011(0011)
+

那么如何得到这个二进制的呢,我们可以来演算下

+

小数算二进制和整数不同。乘法计算时,只计算小数位,整数位用作每一位的二进制,并且得到的第一位为最高位。所以我们得出 0.1 = 2^-4 * 1.10011(0011),那么 0.2 的演算也基本如上所示,只需要去掉第一步乘法,所以得出 0.2 = 2^-3 * 1.10011(0011)

+

回来继续说 IEEE 754 双精度。六十四位中符号位占一位,整数位占十一位,其余五十二位都为小数位。因为 0.10.2 都是无限循环的二进制了,所以在小数位末尾处需要判断是否进位(就和十进制的四舍五入一样)。

+

所以 2^-4 * 1.10011...001 进位后就变成了 2^-4 * 1.10011(0011 * 12次)010 。那么把这两个二进制加起来会得出 2^-2 * 1.0011(0011 * 11次)0100 , 这个值算成十进制就是 0.30000000000000004

+

下面说一下原生解决办法,如下代码所示

+
1
2
3
// js代码

parseFloat((0.1 + 0.2).toFixed(10))
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/91/index.html b/ja/page/91/index.html new file mode 100644 index 0000000000..a28e98a1bf --- /dev/null +++ b/ja/page/91/index.html @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Proxy

ProxyES6 中新增的功能,可以用来自定义对象中的操作

+
1
2
3
4
5
// js代码

let p = new Proxy(target, handler);
// `target` 代表需要添加代理的对象
// `handler` 用来自定义对象中的操作
+

可以很方便的使用 Proxy 来实现一个数据绑定和监听

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(target, property)
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
setBind(value);
return Reflect.set(target, property, value);
}
};
return new Proxy(obj, handler);
};

let obj = { a: 1 }
let value
let p = onWatch(obj, (v) => {
value = v
}, (target, property) => {
console.log(`Get '${property}' = ${target[property]}`);
})
p.a = 2 // bind `value` to `2`
p.a // -> Get 'a' = 2
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/92/index.html b/ja/page/92/index.html new file mode 100644 index 0000000000..f93da92917 --- /dev/null +++ b/ja/page/92/index.html @@ -0,0 +1,516 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

async 和 await

一个函数如果加上 async ,那么该函数就会返回一个 Promise

+
1
2
3
4
async function test() {
return "1";
}
console.log(test()); // -> Promise {<resolved>: "1"}
+

可以把 async 看成将函数返回值使用 Promise.resolve() 包裹了下。
await 只能在 async 函数中使用

+
1
2
3
4
5
6
7
8
9
10
11
12
13
function sleep() {
return new Promise(resolve => {
setTimeout(() => {
console.log('finish')
resolve("sleep");
}, 2000);
});
}
async function test() {
let value = await sleep();
console.log("object");
}
test()
+ +

上面代码会先打印 finish 然后再打印 object 。因为 await 会等待 sleep 函数 resolve ,所以即使后面是同步代码,也不会先去执行同步代码再来执行异步代码。

+

asyncawait 相比直接使用 Promise 来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码。缺点在于滥用 await 可能会导致性能问题,因为 await 会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。

+

下面来看一个使用 await 的代码。

+
1
2
3
4
5
6
7
8
9
10
var a = 0
var b = async () => {
a = a + await 10
console.log('2', a) // -> '2' 10
a = (await 10) + a
console.log('3', a) // -> '3' 20
}
b()
a++
console.log('1', a) // -> '1' 1
+ +

对于以上代码你可能会有疑惑,这里说明下原理

+
    +
  • 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为在 await 内部实现了 generatorsgenerators 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
  • +
  • 因为 await 是异步操作,遇到 await 就会立即返回一个 pending 状态的 Promise 对象,暂时返回执行代码的控制权,使得函数外的代码得以继续执行,所以会先执行 console.log('1', a)
  • +
  • 这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 10
  • +
  • 然后后面就是常规执行代码了
  • +
+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/93/index.html b/ja/page/93/index.html new file mode 100644 index 0000000000..e90c534fe2 --- /dev/null +++ b/ja/page/93/index.html @@ -0,0 +1,515 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Map、FlatMap 和 Reduce

Map

Map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后 append 到新的数组中。

+
1
2
3
// js代码

[1, 2, 3].map((v) => v + 1) // -> [2, 3, 4]
+

Map 有三个参数,分别是当前索引元素索引原数组

+

FlatMap

FlatMapmap 的作用几乎是相同的,但是对于多维数组来说,会将原数组降维。可以将 FlatMap 看成是 map + flatten ,目前该函数在浏览器中还不支持。

+
1
2
3
// js代码

[1, [2], 3].flatMap((v) => v + 1) // -> [2, 3, 4]
+

如果想将一个多维数组彻底的降维,可以这样实现

+
1
2
3
4
5
const flattenDeep = (arr) => Array.isArray(arr)
? arr.reduce( (a, b) => [...a, ...flattenDeep(b)] , [])
: [arr]

flattenDeep([1, [[2], [3, [4]], 5]])
+ +

Reduce 升序执行

Reduce 作用是数组中的值组合起来,最终得到一个值
reduce() 方法对数组中的每个元素执行一个由您提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。

+

reducer 函数接收4个参数:

+
    +
  • Accumulator (acc) (累计器)
  • +
  • Current Value (cur) (当前值)
  • +
  • Current Index (idx) (当前索引)
  • +
  • Source Array (src) (源数组)
    您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
// js代码

function a() {
console.log(1);
}

function b() {
console.log(2);
}

[a, b].reduce((a, b) => a(b()))
// -> 2 1
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/94/index.html b/ja/page/94/index.html new file mode 100644 index 0000000000..ace4a1b696 --- /dev/null +++ b/ja/page/94/index.html @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Generator 实现

GeneratorES6 中新增的语法,和 Promise 一样,都可以用来异步编程

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js代码

// 使用 * 表示这是一个 Generator 函数
// 内部可以通过 yield 暂停代码
// 通过调用 next 恢复执行
function* test() {
let a = 1 + 2;
yield 2;
yield 3;
}
let b = test();
console.log(b.next()); // > { value: 2, done: false }
console.log(b.next()); // > { value: 3, done: false }
console.log(b.next()); // > { value: undefined, done: true }
+

从以上代码可以发现,加上 * 的函数执行后拥有了 next 函数,也就是说函数执行后返回了一个对象。每次调用 next 函数可以继续执行被暂停的代码。以下是 Generator 函数的简单实现

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// js代码

// cb 也就是编译过的 test 函数
function generator(cb) {
return (function() {
var object = {
next: 0,
stop: function() {}
};

return {
next: function() {
var ret = cb(object);
if (ret === undefined) return { value: undefined, done: true };
return {
value: ret,
done: false
};
}
};
})();
}
// 如果你使用 babel 编译后可以发现 test 函数变成了这样
function test() {
var a;
return generator(function(_context) {
while (1) {
switch ((_context.prev = _context.next)) {
// 可以发现通过 yield 将代码分割成几块
// 每次执行 next 函数就执行一块代码
// 并且表明下次需要执行哪块代码
case 0:
a = 1 + 2;
_context.next = 4;
return 2;
case 4:
_context.next = 6;
return 3;
// 执行完毕
case 6:
case "end":
return _context.stop();
}
}
});
}
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/95/index.html b/ja/page/95/index.html new file mode 100644 index 0000000000..756a8d2f33 --- /dev/null +++ b/ja/page/95/index.html @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

Promise 实现

PromiseES6 新增的语法,解决了回调地狱的问题。

+

可以把 Promise 看成一个状态机。初始是 pending 状态,可以通过函数 resolvereject ,将状态转变为 resolved 或者 rejected 状态,状态一旦改变就不能再次变化

+

then 函数会返回一个 Promise 实例,并且该返回值是一个新的实例而不是之前的实例。因为 Promise 规范规定除了 pending 状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个 then 调用就失去意义了。

+

对于 then 来说,本质上可以把它看成是 flatMap

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// js代码

// 三种状态
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
// promise 接收一个函数参数,该函数会立即执行
function MyPromise(fn) {
let _this = this;
_this.currentState = PENDING;
_this.value = undefined;
// 用于保存 then 中的回调,只有当 promise
// 状态为 pending 时才会缓存,并且每个实例至多缓存一个
_this.resolvedCallbacks = [];
_this.rejectedCallbacks = [];

_this.resolve = function (value) {
if (value instanceof MyPromise) {
// 如果 value 是个 Promise,递归执行
return value.then(_this.resolve, _this.reject)
}
setTimeout(() => { // 异步执行,保证执行顺序
if (_this.currentState === PENDING) {
_this.currentState = RESOLVED;
_this.value = value;
_this.resolvedCallbacks.forEach(cb => cb());
}
})
};

_this.reject = function (reason) {
setTimeout(() => { // 异步执行,保证执行顺序
if (_this.currentState === PENDING) {
_this.currentState = REJECTED;
_this.value = reason;
_this.rejectedCallbacks.forEach(cb => cb());
}
})
}
// 用于解决以下问题
// new Promise(() => throw Error('error))
try {
fn(_this.resolve, _this.reject);
} catch (e) {
_this.reject(e);
}
}

MyPromise.prototype.then = function (onResolved, onRejected) {
var self = this;
// 规范 2.2.7,then 必须返回一个新的 promise
var promise2;
// 规范 2.2.onResolved 和 onRejected 都为可选参数
// 如果类型不是函数需要忽略,同时也实现了透传
// Promise.resolve(4).then().then((value) => console.log(value))
onResolved = typeof onResolved === 'function' ? onResolved : v => v;
onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;

if (self.currentState === RESOLVED) {
return (promise2 = new MyPromise(function (resolve, reject) {
// 规范 2.2.4,保证 onFulfilled,onRjected 异步执行
// 所以用了 setTimeout 包裹下
setTimeout(function () {
try {
var x = onResolved(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}

if (self.currentState === REJECTED) {
return (promise2 = new MyPromise(function (resolve, reject) {
setTimeout(function () {
// 异步执行onRejected
try {
var x = onRejected(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}

if (self.currentState === PENDING) {
return (promise2 = new MyPromise(function (resolve, reject) {
self.resolvedCallbacks.push(function () {
// 考虑到可能会有报错,所以使用 try/catch 包裹
try {
var x = onResolved(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});

self.rejectedCallbacks.push(function () {
try {
var x = onRejected(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});
}));
}
};
// 规范 2.3
function resolutionProcedure(promise2, x, resolve, reject) {
// 规范 2.3.1,x 不能和 promise2 相同,避免循环引用
if (promise2 === x) {
return reject(new TypeError("Error"));
}
// 规范 2.3.2
// 如果 x 为 Promise,状态为 pending 需要继续等待否则执行
if (x instanceof MyPromise) {
if (x.currentState === PENDING) {
x.then(function (value) {
// 再次调用该函数是为了确认 x resolve 的
// 参数是什么类型,如果是基本类型就再次 resolve
// 把值传给下个 then
resolutionProcedure(promise2, value, resolve, reject);
}, reject);
} else {
x.then(resolve, reject);
}
return;
}
// 规范 2.3.3.3.3
// reject 或者 resolve 其中一个执行过得话,忽略其他的
let called = false;
// 规范 2.3.3,判断 x 是否为对象或者函数
if (x !== null && (typeof x === "object" || typeof x === "function")) {
// 规范 2.3.3.2,如果不能取出 then,就 reject
try {
// 规范 2.3.3.1
let then = x.then;
// 如果 then 是函数,调用 x.then
if (typeof then === "function") {
// 规范 2.3.3.3
then.call(
x,
y => {
if (called) return;
called = true;
// 规范 2.3.3.3.1
resolutionProcedure(promise2, y, resolve, reject);
},
e => {
if (called) return;
called = true;
reject(e);
}
);
} else {
// 规范 2.3.3.4
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
// 规范 2.3.4,x 为基本类型
resolve(x);
}
}
+

以上就是根据 Promise / A+ 规范来实现的代码,可以通过 promises-aplus-tests 的完整测试

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/96/index.html b/ja/page/96/index.html new file mode 100644 index 0000000000..ce55cd67f4 --- /dev/null +++ b/ja/page/96/index.html @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

call, apply, bind 区别

首先说下前两者的异同。
相同: callapply 都是为了解决改变 this 的指向。
不同:传参的方式不同,除了第一个参数外, call 可以接收一个参数列表apply 只接受一个参数数组

+
1
2
3
4
5
6
7
8
9
10
11
// js代码
let anObj = {
value: 1
}
function getValue(name, age) {
console.log(name)
console.log(age)
console.log(this.value)
}
getValue.call(anObj, 'lixuguang', '31')
getValue.apply(anObj, ['lixuguang', '31'])
+

模拟实现 callapply

可以从以下几点来考虑如何实现

+
    +
  • 不传入第一个参数,那么默认为 window
  • +
  • 改变了 this 指向,让新的对象可以执行该函数。那么思路是否可以变成给新的对象添加一个函数,然后在执行完以后删除?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    Function.prototype.myCall = function (context) {
    var context = context || window // 有入参用入参,没有给 window
    context.fn = this // 给 context 添加一个属性,getValue.call(anObj, 'lixuguang', '31') => anObj.fn = getValue
    var args = [...arguments].slice(1) // 将 context 后面的参数取出来
    var result = context.fn(...args) // getValue.call(anObj, 'lixuguang', '31') => anObj.fn('yck', '24')
    delete context.fn // 删除 fn
    return result
    }
    +以上就是 call 的思路,apply 的实现也类似
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    Function.prototype.Apply = function (context) {
    var context = context || window // 有入参用入参,没有给 window
    context.fn = this // 给 context 添加一个属性,getValue.call(anObj, 'lixuguang', '31') => anObj.fn = getValue
    var args = arguments[1] // 将 context 后面的参数取出来
    var result = args?context.fn(...args):context.fn() // getValue.call(anObj, ['lixuguang', '31']) => anObj.fn('yck', '24')
    delete context.fn // 删除 fn
    return result
    }
    +bind 和其他两个方法作用也是一致的,只是该方法会返回一个函数。并且我们可以通过 bind 实现柯里化。
  • +
+

同样的,也来模拟实现下 bind

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码

Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
var _this = this
var args = [...arguments].slice(1)

return function F() { // 返回一个函数,这是 `bind` 和 `call` , `apply` 的区别
if (this instanceof F) { // 因为返回了一个函数,我们可以 new F(),所以需要判断
return new _this(...args, ...arguments)
}
return _this.apply(context, args.concat(...arguments))
}
}
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/97/index.html b/ja/page/97/index.html new file mode 100644 index 0000000000..22faa02f17 --- /dev/null +++ b/ja/page/97/index.html @@ -0,0 +1,510 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

继承

在 ES5 中,我们可以使用如下方式解决继承的问题

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js代码
function Super() {}
Super.prototype.getNumber = function() {
return 1
}

function Sub() {}
let s = new Sub()
Sub.prototype = Object.create(Super.prototype, {
constructor: {
value: Sub,
enumerable: false,
writable: true,
configurable: true
}
})
+

以上继承实现思路就是将子类的原型设置为父类的原型
ES6 中,我们可以通过 class 语法轻松解决这个问题

+
1
2
3
4
5
6
7
8
9
// js代码

class MyDate extends Date {
test() {
return this.getTime()
}
}
let myDate = new MyDate()
myDate.test()
+

但是 ES6 不是所有浏览器都兼容,所以我们需要使用 Babel 来编译这段代码。

+

如果你使用编译过得代码调用 myDate.test() 你会惊奇地发现出现了报错

+

因为在 JS 底层有限制,如果不是由 Date 构造出来的实例的话,是不能调用 Date 里的函数的。所以这也侧面的说明了:ES6 中的 class 继承与 ES5 中的一般继承写法是不同的

+

既然底层限制了实例必须由 Date 构造出来,那么我们可以改变下思路实现继承

+
1
2
3
4
5
6
7
8
9
10
11
// js代码

function MyData() {

}
MyData.prototype.test = function () {
return this.getTime()
}
let d = new Date() // 父类实例
Object.setPrototypeOf(d, MyData.prototype)
Object.setPrototypeOf(MyData.prototype, Date.prototype)
+

以上继承实现思路:先创建父类实例 => 改变实例原先的 __proto__ 转而连接到子类的 prototype => 子类的 prototype__proto__ 改为父类的 prototype

+

通过以上方法实现的继承就可以完美解决 JS 底层的这个限制。

+ + +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/98/index.html b/ja/page/98/index.html new file mode 100644 index 0000000000..a156abda21 --- /dev/null +++ b/ja/page/98/index.html @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

函数防抖和节流

在前端开发的过程中,我们经常会需要绑定一些持续触发的事件,如 resize、scroll、mousemove 等等,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。

+

通常这种情况下我们怎么去解决的呢?一般来讲,防抖和节流是比较好的解决方案。

+

防抖

你是否在日常开发中遇到一个问题,在滚动事件中需要做个复杂计算或者实现一个按钮的防二次点击操作。

+

这些需求都可以通过函数防抖动来实现。尤其是第一个需求,如果在频繁的事件回调中做复杂计算,很有可能导致页面卡顿,不如将多次计算合并为一次计算,只在一个精确点做操作。

+

PS:防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的情况下只会调用一次,而节流的 情况会每隔一定时间(参数wait)调用函数。

+

我们先来看一个袖珍版的防抖理解一下防抖的实现:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
// 缓存一个定时器id
let timer = 0
// 这里返回的函数是每次用户实际调用的防抖函数
// 如果已经设定过定时器了就清空上一次的定时器
// 开始一个新的定时器,延迟执行用户传入的方法
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
// 不难看出如果用户调用该函数的间隔小于wait的情况下,上一次的时间还未到就被清除了,并不会执行函数
+

这是一个简单版的防抖,但是有缺陷,这个防抖只能在最后调用。一般的防抖会有immediate选项,表示是否立即调用。这两者的区别,举个栗子来说:

+
    +
  • 例如在搜索引擎搜索问题的时候,我们当然是希望用户输入完最后一个字才调用查询接口,这个时候适用延迟执行的防抖函数,它总是在一连串(间隔小于wait的)函数触发之后调用。
  • +
  • 例如用户给某系统表单点提交的时候,我们希望用户点第一下的时候就去调用接口,这个情况适用立即执行的防抖函数,它总是在第一次调用,并且下一次调用必须与前一次调用的时间间隔大于wait才会触发。
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// js代码

// 这个是用来获取当前时间戳的
function now() {
return +new Date()
}
/**
* 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {boolean} immediate 设置为ture时,是否立即调用函数
* @return {function} 返回客户调用函数
*/
function debounce (func, wait = 50, immediate = true) {
let timer, context, args

// 延迟执行函数
const later = () => setTimeout(() => {
// 延迟函数执行完毕,清空缓存的定时器序号
timer = null
// 延迟执行的情况下,函数会在延迟函数中执行
// 使用到之前缓存的参数和上下文
if (!immediate) {
func.apply(context, args)
context = args = null
}
}, wait)

// 这里返回的函数是每次实际调用的函数
return function(...params) {
// 如果没有创建延迟执行函数(later),就创建一个
if (!timer) {
timer = later()
// 如果是立即执行,调用函数
// 否则缓存参数和调用上下文
if (immediate) {
func.apply(this, params)
} else {
context = this
args = params
}
// 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
// 这样做延迟函数会重新计时
} else {
clearTimeout(timer)
timer = later()
}
}
}
+

整体函数实现的不难,总结一下。

+
    +
  • 对于按钮防点击来说的实现:如果函数是立即执行的,就立即调用,如果函数是延迟执行的,就缓存上下文和参数,放到延迟函数中去执行。一旦我开始一个定时器,只要我定时器还在,你每次点击我都重新计时。一旦你点累了,定时器时间到,定时器重置为 null,就可以再次点击了。
  • +
  • 对于延时执行函数来说的实现:清除定时器ID,如果是延迟调用就调用函数
  • +
+

节流

防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// js代码

/**
* underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {object} options 如果想忽略开始函数的的调用,传入{leading: false}。
* 如果想忽略结尾函数的调用,传入{trailing: false}
* 两者不能共存,否则函数不能执行
* @return {function} 返回客户调用函数
*/
_.throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
// 之前的时间戳
var previous = 0;
// 如果 options 没传则设为空对象
if (!options) options = {};
// 定时器回调函数
var later = function() {
// 如果设置了 leading,就将 previous 设为 0
// 用于下面函数的第一个 if 判断
previous = options.leading === false ? 0 : _.now();
// 置空一是为了防止内存泄漏,二是为了下面的定时器判断
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
// 获得当前时间戳
var now = _.now();
// 首次进入前者肯定为 true
// 如果需要第一次不执行函数
// 就将上次时间戳设为当前的
// 这样在接下来计算 remaining 的值时会大于0
if (!previous && options.leading === false) previous = now;
// 计算剩余时间
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果当前调用已经大于上次调用时间 + wait
// 或者用户手动调了时间
// 如果设置了 trailing,只会进入这个条件
// 如果没有设置 leading,那么第一次会进入这个条件
// 还有一点,你可能会觉得开启了定时器那么应该不会进入这个 if 条件了
// 其实还是会进入的,因为定时器的延时
// 并不是准确的时间,很可能你设置了2秒
// 但是他需要2.2秒才触发,这时候就会进入这个条件
if (remaining <= 0 || remaining > wait) {
// 如果存在定时器就清理掉否则会调用二次回调
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 判断是否设置了定时器和 trailing
// 没有的话就开启一个定时器
// 并且不能不能同时设置 leading 和 trailing
timeout = setTimeout(later, remaining);
}
return result;
};
};
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/page/99/index.html b/ja/page/99/index.html new file mode 100644 index 0000000000..d7efd8b375 --- /dev/null +++ b/ja/page/99/index.html @@ -0,0 +1,514 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + +
+ + + + +
+ + + + + +
+

+ + +

+ + +
+ + + + +
+ + +
+

作者:李旭光
引用请标明出处

+
+

模块化

在有 Babel 的情况下,我们可以直接使用 ES6 的模块化

+
1
2
3
4
5
6
7
8
9
10
// js代码

// file a.js
export function a() {}
export function b() {}
// file b.js
export default function() {}

import {a, b} from './a.js'
import XXX from './b.js'
+

CommonJS

CommonJsNode 独有的规范,浏览器中使用就需要用到 Browserify 解析了。

+
1
2
3
4
5
6
7
8
9
10
11
12
// js代码

// a.js
module.exports = {
a: 1
}
// or
exports.a = 1

// b.js
var module = require('./a.js')
module.a // -> log 1
+

在上述代码中, module.exportsexports 很容易混淆,让我们来看看大致内部实现

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// js代码

var module = require('./a.js')
module.a
// 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {
a: 1
}
// 基本实现
var module = {
exports: {} // exports 就是个空对象
}
// 这个是为什么 exports 和 module.exports 用法相似的原因
var exports = module.exports
var load = function (module) {
// 导出的东西
var a = 1
module.exports = a
return module.exports
};
+

再来说说 module.exports 和 exports,用法其实是相似的,但是不能对 exports 直接赋值,不会有任何效果。

+

对于 CommonJS 和 ES6 中的模块化的两者区别是:

+
    +
  • 前者支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
  • +
  • 前者是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
  • +
  • 前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是后者采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
  • +
  • 后者会编译成 require/exports 来执行的
  • +
+

AMD

AMD 是由 RequireJS 提出的

+
1
2
3
4
5
6
7
8
9
10
11
12
13
// js代码

// AMD
define(['./a', './b'], function(a, b) {
a.do()
b.do()
})
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
var b = require('./b')
b.doSomething()
})
+ +
+ + + + +
+
+
+
+ + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/tags/index.html b/ja/tags/index.html new file mode 100644 index 0000000000..bad5dbe6fe --- /dev/null +++ b/ja/tags/index.html @@ -0,0 +1,470 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 | 李旭光的成长博客 + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

李旭光的成长博客

+ +
+

求知若饥,虚心若愚。

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+
+ + + + +
+
+
+ + + + + + + + + +
+ + + + + + + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/local-search.js b/js/local-search.js new file mode 100644 index 0000000000..31f945fd7a --- /dev/null +++ b/js/local-search.js @@ -0,0 +1,278 @@ +/* global CONFIG */ + +document.addEventListener('DOMContentLoaded', () => { + // Popup Window + let isfetched = false; + let datas; + let isXml = true; + // Search DB path + let searchPath = CONFIG.path; + if (searchPath.length === 0) { + searchPath = 'search.xml'; + } else if (searchPath.endsWith('json')) { + isXml = false; + } + const input = document.querySelector('.search-input'); + const resultContent = document.getElementById('search-result'); + + const getIndexByWord = (word, text, caseSensitive) => { + if (CONFIG.localsearch.unescape) { + let div = document.createElement('div'); + div.innerText = word; + word = div.innerHTML; + } + let wordLen = word.length; + if (wordLen === 0) return []; + let startPosition = 0; + let position = []; + let index = []; + if (!caseSensitive) { + text = text.toLowerCase(); + word = word.toLowerCase(); + } + while ((position = text.indexOf(word, startPosition)) > -1) { + index.push({ position, word }); + startPosition = position + wordLen; + } + return index; + }; + + // Merge hits into slices + const mergeIntoSlice = (start, end, index, searchText) => { + let item = index[index.length - 1]; + let { position, word } = item; + let hits = []; + let searchTextCountInSlice = 0; + while (position + word.length <= end && index.length !== 0) { + if (word === searchText) { + searchTextCountInSlice++; + } + hits.push({ + position, + length: word.length + }); + let wordEnd = position + word.length; + + // Move to next position of hit + index.pop(); + while (index.length !== 0) { + item = index[index.length - 1]; + position = item.position; + word = item.word; + if (wordEnd > position) { + index.pop(); + } else { + break; + } + } + } + return { + hits, + start, + end, + searchTextCount: searchTextCountInSlice + }; + }; + + // Highlight title and content + const highlightKeyword = (text, slice) => { + let result = ''; + let prevEnd = slice.start; + slice.hits.forEach(hit => { + result += text.substring(prevEnd, hit.position); + let end = hit.position + hit.length; + result += `${text.substring(hit.position, end)}`; + prevEnd = end; + }); + result += text.substring(prevEnd, slice.end); + return result; + }; + + const inputEventFunction = () => { + if (!isfetched) return; + let searchText = input.value.trim().toLowerCase(); + let keywords = searchText.split(/[-\s]+/); + if (keywords.length > 1) { + keywords.push(searchText); + } + let resultItems = []; + if (searchText.length > 0) { + // Perform local searching + datas.forEach(({ title, content, url }) => { + let titleInLowerCase = title.toLowerCase(); + let contentInLowerCase = content.toLowerCase(); + let indexOfTitle = []; + let indexOfContent = []; + let searchTextCount = 0; + keywords.forEach(keyword => { + indexOfTitle = indexOfTitle.concat(getIndexByWord(keyword, titleInLowerCase, false)); + indexOfContent = indexOfContent.concat(getIndexByWord(keyword, contentInLowerCase, false)); + }); + + // Show search results + if (indexOfTitle.length > 0 || indexOfContent.length > 0) { + let hitCount = indexOfTitle.length + indexOfContent.length; + // Sort index by position of keyword + [indexOfTitle, indexOfContent].forEach(index => { + index.sort((itemLeft, itemRight) => { + if (itemRight.position !== itemLeft.position) { + return itemRight.position - itemLeft.position; + } + return itemLeft.word.length - itemRight.word.length; + }); + }); + + let slicesOfTitle = []; + if (indexOfTitle.length !== 0) { + let tmp = mergeIntoSlice(0, title.length, indexOfTitle, searchText); + searchTextCount += tmp.searchTextCountInSlice; + slicesOfTitle.push(tmp); + } + + let slicesOfContent = []; + while (indexOfContent.length !== 0) { + let item = indexOfContent[indexOfContent.length - 1]; + let { position, word } = item; + // Cut out 100 characters + let start = position - 20; + let end = position + 80; + if (start < 0) { + start = 0; + } + if (end < position + word.length) { + end = position + word.length; + } + if (end > content.length) { + end = content.length; + } + let tmp = mergeIntoSlice(start, end, indexOfContent, searchText); + searchTextCount += tmp.searchTextCountInSlice; + slicesOfContent.push(tmp); + } + + // Sort slices in content by search text's count and hits' count + slicesOfContent.sort((sliceLeft, sliceRight) => { + if (sliceLeft.searchTextCount !== sliceRight.searchTextCount) { + return sliceRight.searchTextCount - sliceLeft.searchTextCount; + } else if (sliceLeft.hits.length !== sliceRight.hits.length) { + return sliceRight.hits.length - sliceLeft.hits.length; + } + return sliceLeft.start - sliceRight.start; + }); + + // Select top N slices in content + let upperBound = parseInt(CONFIG.localsearch.top_n_per_article, 10); + if (upperBound >= 0) { + slicesOfContent = slicesOfContent.slice(0, upperBound); + } + + let resultItem = ''; + + if (slicesOfTitle.length !== 0) { + resultItem += `
  • ${highlightKeyword(title, slicesOfTitle[0])}`; + } else { + resultItem += `
  • ${title}`; + } + + slicesOfContent.forEach(slice => { + resultItem += `

    ${highlightKeyword(content, slice)}...

    `; + }); + + resultItem += '
  • '; + resultItems.push({ + item: resultItem, + id : resultItems.length, + hitCount, + searchTextCount + }); + } + }); + } + if (keywords.length === 1 && keywords[0] === '') { + resultContent.innerHTML = '
    '; + } else if (resultItems.length === 0) { + resultContent.innerHTML = '
    '; + } else { + resultItems.sort((resultLeft, resultRight) => { + if (resultLeft.searchTextCount !== resultRight.searchTextCount) { + return resultRight.searchTextCount - resultLeft.searchTextCount; + } else if (resultLeft.hitCount !== resultRight.hitCount) { + return resultRight.hitCount - resultLeft.hitCount; + } + return resultRight.id - resultLeft.id; + }); + resultContent.innerHTML = `
      ${resultItems.map(result => result.item).join('')}
    `; + window.pjax && window.pjax.refresh(resultContent); + } + }; + + const fetchData = () => { + fetch(CONFIG.root + searchPath) + .then(response => response.text()) + .then(res => { + // Get the contents from search data + isfetched = true; + datas = isXml ? [...new DOMParser().parseFromString(res, 'text/xml').querySelectorAll('entry')].map(element => { + return { + title : element.querySelector('title').textContent, + content: element.querySelector('content').textContent, + url : element.querySelector('url').textContent + }; + }) : JSON.parse(res); + // Only match articles with not empty titles + datas = datas.filter(data => data.title).map(data => { + data.title = data.title.trim(); + data.content = data.content ? data.content.trim().replace(/<[^>]+>/g, '') : ''; + data.url = decodeURIComponent(data.url).replace(/\/{2,}/g, '/'); + return data; + }); + // Remove loading animation + document.getElementById('no-result').innerHTML = ''; + inputEventFunction(); + }); + }; + + if (CONFIG.localsearch.preload) { + fetchData(); + } + + if (CONFIG.localsearch.trigger === 'auto') { + input.addEventListener('input', inputEventFunction); + } else { + document.querySelector('.search-icon').addEventListener('click', inputEventFunction); + input.addEventListener('keypress', event => { + if (event.key === 'Enter') { + inputEventFunction(); + } + }); + } + + // Handle and trigger popup window + document.querySelectorAll('.popup-trigger').forEach(element => { + element.addEventListener('click', () => { + document.body.style.overflow = 'hidden'; + document.querySelector('.search-pop-overlay').classList.add('search-active'); + input.focus(); + if (!isfetched) fetchData(); + }); + }); + + // Monitor main search box + const onPopupClose = () => { + document.body.style.overflow = ''; + document.querySelector('.search-pop-overlay').classList.remove('search-active'); + }; + + document.querySelector('.search-pop-overlay').addEventListener('click', event => { + if (event.target === document.querySelector('.search-pop-overlay')) { + onPopupClose(); + } + }); + document.querySelector('.popup-btn-close').addEventListener('click', onPopupClose); + window.addEventListener('pjax:success', onPopupClose); + window.addEventListener('keyup', event => { + if (event.key === 'Escape') { + onPopupClose(); + } + }); +}); diff --git a/js/motion.js b/js/motion.js new file mode 100644 index 0000000000..026199aabb --- /dev/null +++ b/js/motion.js @@ -0,0 +1,177 @@ +/* global NexT, CONFIG, Velocity */ + +if (window.$ && window.$.Velocity) window.Velocity = window.$.Velocity; + +NexT.motion = {}; + +NexT.motion.integrator = { + queue : [], + cursor: -1, + init : function() { + this.queue = []; + this.cursor = -1; + return this; + }, + add: function(fn) { + this.queue.push(fn); + return this; + }, + next: function() { + this.cursor++; + var fn = this.queue[this.cursor]; + typeof fn === 'function' && fn(NexT.motion.integrator); + }, + bootstrap: function() { + this.next(); + } +}; + +NexT.motion.middleWares = { + logo: function(integrator) { + var sequence = []; + var brand = document.querySelector('.brand'); + var image = document.querySelector('.custom-logo-image'); + var title = document.querySelector('.site-title'); + var subtitle = document.querySelector('.site-subtitle'); + var logoLineTop = document.querySelector('.logo-line-before i'); + var logoLineBottom = document.querySelector('.logo-line-after i'); + + brand && sequence.push({ + e: brand, + p: {opacity: 1}, + o: {duration: 200} + }); + + function getMistLineSettings(element, translateX) { + return { + e: element, + p: {translateX}, + o: { + duration : 500, + sequenceQueue: false + } + }; + } + + function pushImageToSequence() { + sequence.push({ + e: image, + p: {opacity: 1, top: 0}, + o: {duration: 200} + }); + } + + CONFIG.scheme === 'Mist' && logoLineTop && logoLineBottom + && sequence.push( + getMistLineSettings(logoLineTop, '100%'), + getMistLineSettings(logoLineBottom, '-100%') + ); + + CONFIG.scheme === 'Muse' && image && pushImageToSequence(); + + title && sequence.push({ + e: title, + p: {opacity: 1, top: 0}, + o: {duration: 200} + }); + + subtitle && sequence.push({ + e: subtitle, + p: {opacity: 1, top: 0}, + o: {duration: 200} + }); + + (CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') && image && pushImageToSequence(); + + if (sequence.length > 0) { + sequence[sequence.length - 1].o.complete = function() { + integrator.next(); + }; + Velocity.RunSequence(sequence); + } else { + integrator.next(); + } + + if (CONFIG.motion.async) { + integrator.next(); + } + }, + + menu: function(integrator) { + Velocity(document.querySelectorAll('.menu-item'), 'transition.slideDownIn', { + display : null, + duration: 200, + complete: function() { + integrator.next(); + } + }); + + if (CONFIG.motion.async) { + integrator.next(); + } + }, + + subMenu: function(integrator) { + var subMenuItem = document.querySelectorAll('.sub-menu .menu-item'); + if (subMenuItem.length > 0) { + subMenuItem.forEach(element => { + element.style.opacity = 1; + }); + } + integrator.next(); + }, + + postList: function(integrator) { + var postBlock = document.querySelectorAll('.post-block, .pagination, .comments'); + var postBlockTransition = CONFIG.motion.transition.post_block; + var postHeader = document.querySelectorAll('.post-header'); + var postHeaderTransition = CONFIG.motion.transition.post_header; + var postBody = document.querySelectorAll('.post-body'); + var postBodyTransition = CONFIG.motion.transition.post_body; + var collHeader = document.querySelectorAll('.collection-header'); + var collHeaderTransition = CONFIG.motion.transition.coll_header; + + if (postBlock.length > 0) { + var postMotionOptions = window.postMotionOptions || { + stagger : 100, + drag : true, + complete: function() { + integrator.next(); + } + }; + + if (CONFIG.motion.transition.post_block) { + Velocity(postBlock, 'transition.' + postBlockTransition, postMotionOptions); + } + if (CONFIG.motion.transition.post_header) { + Velocity(postHeader, 'transition.' + postHeaderTransition, postMotionOptions); + } + if (CONFIG.motion.transition.post_body) { + Velocity(postBody, 'transition.' + postBodyTransition, postMotionOptions); + } + if (CONFIG.motion.transition.coll_header) { + Velocity(collHeader, 'transition.' + collHeaderTransition, postMotionOptions); + } + } + if (CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') { + integrator.next(); + } + }, + + sidebar: function(integrator) { + var sidebarAffix = document.querySelector('.sidebar-inner'); + var sidebarAffixTransition = CONFIG.motion.transition.sidebar; + // Only for Pisces | Gemini. + if (sidebarAffixTransition && (CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini')) { + Velocity(sidebarAffix, 'transition.' + sidebarAffixTransition, { + display : null, + duration: 200, + complete: function() { + // After motion complete need to remove transform from sidebar to let affix work on Pisces | Gemini. + sidebarAffix.style.transform = 'initial'; + } + }); + } + integrator.next(); + } +}; diff --git a/js/next-boot.js b/js/next-boot.js new file mode 100644 index 0000000000..52ec9aec0f --- /dev/null +++ b/js/next-boot.js @@ -0,0 +1,114 @@ +/* global NexT, CONFIG, Velocity */ + +NexT.boot = {}; + +NexT.boot.registerEvents = function() { + + NexT.utils.registerScrollPercent(); + NexT.utils.registerCanIUseTag(); + + // Mobile top menu bar. + document.querySelector('.site-nav-toggle .toggle').addEventListener('click', () => { + event.currentTarget.classList.toggle('toggle-close'); + var siteNav = document.querySelector('.site-nav'); + var animateAction = siteNav.classList.contains('site-nav-on') ? 'slideUp' : 'slideDown'; + + if (typeof Velocity === 'function') { + Velocity(siteNav, animateAction, { + duration: 200, + complete: function() { + siteNav.classList.toggle('site-nav-on'); + } + }); + } else { + siteNav.classList.toggle('site-nav-on'); + } + }); + + var TAB_ANIMATE_DURATION = 200; + document.querySelectorAll('.sidebar-nav li').forEach((element, index) => { + element.addEventListener('click', event => { + var item = event.currentTarget; + var activeTabClassName = 'sidebar-nav-active'; + var activePanelClassName = 'sidebar-panel-active'; + if (item.classList.contains(activeTabClassName)) return; + + var targets = document.querySelectorAll('.sidebar-panel'); + var target = targets[index]; + var currentTarget = targets[1 - index]; + window.anime({ + targets : currentTarget, + duration: TAB_ANIMATE_DURATION, + easing : 'linear', + opacity : 0, + complete: () => { + // Prevent adding TOC to Overview if Overview was selected when close & open sidebar. + currentTarget.classList.remove(activePanelClassName); + target.style.opacity = 0; + target.classList.add(activePanelClassName); + window.anime({ + targets : target, + duration: TAB_ANIMATE_DURATION, + easing : 'linear', + opacity : 1 + }); + } + }); + + [...item.parentNode.children].forEach(element => { + element.classList.remove(activeTabClassName); + }); + item.classList.add(activeTabClassName); + }); + }); + + window.addEventListener('resize', NexT.utils.initSidebarDimension); + + window.addEventListener('hashchange', () => { + var tHash = location.hash; + if (tHash !== '' && !tHash.match(/%\S{2}/)) { + var target = document.querySelector(`.tabs ul.nav-tabs li a[href="${tHash}"]`); + target && target.click(); + } + }); +}; + +NexT.boot.refresh = function() { + + /** + * Register JS handlers by condition option. + * Need to add config option in Front-End at 'layout/_partials/head.swig' file. + */ + CONFIG.fancybox && NexT.utils.wrapImageWithFancyBox(); + CONFIG.mediumzoom && window.mediumZoom('.post-body :not(a) > img, .post-body > img'); + CONFIG.lazyload && window.lozad('.post-body img').observe(); + CONFIG.pangu && window.pangu.spacingPage(); + + CONFIG.exturl && NexT.utils.registerExtURL(); + CONFIG.copycode.enable && NexT.utils.registerCopyCode(); + NexT.utils.registerTabsTag(); + NexT.utils.registerActiveMenuItem(); + NexT.utils.registerLangSelect(); + NexT.utils.registerSidebarTOC(); + NexT.utils.wrapTableWithBox(); + NexT.utils.registerVideoIframe(); +}; + +NexT.boot.motion = function() { + // Define Motion Sequence & Bootstrap Motion. + if (CONFIG.motion.enable) { + NexT.motion.integrator + .add(NexT.motion.middleWares.logo) + .add(NexT.motion.middleWares.menu) + .add(NexT.motion.middleWares.postList) + .add(NexT.motion.middleWares.sidebar) + .bootstrap(); + } + NexT.utils.updateSidebarPosition(); +}; + +document.addEventListener('DOMContentLoaded', () => { + NexT.boot.registerEvents(); + NexT.boot.refresh(); + NexT.boot.motion(); +}); diff --git a/js/schemes/pisces.js b/js/schemes/pisces.js new file mode 100644 index 0000000000..41633eacbf --- /dev/null +++ b/js/schemes/pisces.js @@ -0,0 +1,86 @@ +/* global NexT, CONFIG */ + +var Affix = { + init: function(element, options) { + this.element = element; + this.offset = options || 0; + this.affixed = null; + this.unpin = null; + this.pinnedOffset = null; + this.checkPosition(); + window.addEventListener('scroll', this.checkPosition.bind(this)); + window.addEventListener('click', this.checkPositionWithEventLoop.bind(this)); + window.matchMedia('(min-width: 992px)').addListener(event => { + if (event.matches) { + this.offset = NexT.utils.getAffixParam(); + this.checkPosition(); + } + }); + }, + getState: function(scrollHeight, height, offsetTop, offsetBottom) { + let scrollTop = window.scrollY; + let targetHeight = window.innerHeight; + if (offsetTop != null && this.affixed === 'top') { + if (document.querySelector('.content-wrap').offsetHeight < offsetTop) return 'top'; + return scrollTop < offsetTop ? 'top' : false; + } + if (this.affixed === 'bottom') { + if (offsetTop != null) return this.unpin <= this.element.getBoundingClientRect().top ? false : 'bottom'; + return scrollTop + targetHeight <= scrollHeight - offsetBottom ? false : 'bottom'; + } + let initializing = this.affixed === null; + let colliderTop = initializing ? scrollTop : this.element.getBoundingClientRect().top + scrollTop; + let colliderHeight = initializing ? targetHeight : height; + if (offsetTop != null && scrollTop <= offsetTop) return 'top'; + if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom'; + return false; + }, + getPinnedOffset: function() { + if (this.pinnedOffset) return this.pinnedOffset; + this.element.classList.remove('affix-top', 'affix-bottom'); + this.element.classList.add('affix'); + return (this.pinnedOffset = this.element.getBoundingClientRect().top); + }, + checkPositionWithEventLoop() { + setTimeout(this.checkPosition.bind(this), 1); + }, + checkPosition: function() { + if (window.getComputedStyle(this.element).display === 'none') return; + let height = this.element.offsetHeight; + let { offset } = this; + let offsetTop = offset.top; + let offsetBottom = offset.bottom; + let { scrollHeight } = document.body; + let affix = this.getState(scrollHeight, height, offsetTop, offsetBottom); + if (this.affixed !== affix) { + if (this.unpin != null) this.element.style.top = ''; + let affixType = 'affix' + (affix ? '-' + affix : ''); + this.affixed = affix; + this.unpin = affix === 'bottom' ? this.getPinnedOffset() : null; + this.element.classList.remove('affix', 'affix-top', 'affix-bottom'); + this.element.classList.add(affixType); + } + if (affix === 'bottom') { + this.element.style.top = scrollHeight - height - offsetBottom + 'px'; + } + } +}; + +NexT.utils.getAffixParam = function() { + const sidebarOffset = CONFIG.sidebar.offset || 12; + + let headerOffset = document.querySelector('.header-inner').offsetHeight; + let footerOffset = document.querySelector('.footer').offsetHeight; + + document.querySelector('.sidebar').style.marginTop = headerOffset + sidebarOffset + 'px'; + + return { + top : headerOffset, + bottom: footerOffset + }; +}; + +document.addEventListener('DOMContentLoaded', () => { + + Affix.init(document.querySelector('.sidebar-inner'), NexT.utils.getAffixParam()); +}); diff --git a/js/utils.js b/js/utils.js new file mode 100644 index 0000000000..74a6dfd558 --- /dev/null +++ b/js/utils.js @@ -0,0 +1,415 @@ +/* global NexT, CONFIG */ + +HTMLElement.prototype.wrap = function(wrapper) { + this.parentNode.insertBefore(wrapper, this); + this.parentNode.removeChild(this); + wrapper.appendChild(this); +}; + +NexT.utils = { + + /** + * Wrap images with fancybox. + */ + wrapImageWithFancyBox: function() { + document.querySelectorAll('.post-body :not(a) > img, .post-body > img').forEach(element => { + var $image = $(element); + var imageLink = $image.attr('data-src') || $image.attr('src'); + var $imageWrapLink = $image.wrap(``).parent('a'); + if ($image.is('.post-gallery img')) { + $imageWrapLink.attr('data-fancybox', 'gallery').attr('rel', 'gallery'); + } else if ($image.is('.group-picture img')) { + $imageWrapLink.attr('data-fancybox', 'group').attr('rel', 'group'); + } else { + $imageWrapLink.attr('data-fancybox', 'default').attr('rel', 'default'); + } + + var imageTitle = $image.attr('title') || $image.attr('alt'); + if (imageTitle) { + $imageWrapLink.append(`

    ${imageTitle}

    `); + // Make sure img title tag will show correctly in fancybox + $imageWrapLink.attr('title', imageTitle).attr('data-caption', imageTitle); + } + }); + + $.fancybox.defaults.hash = false; + $('.fancybox').fancybox({ + loop : true, + helpers: { + overlay: { + locked: false + } + } + }); + }, + + registerExtURL: function() { + document.querySelectorAll('span.exturl').forEach(element => { + let link = document.createElement('a'); + // https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings + link.href = decodeURIComponent(atob(element.dataset.url).split('').map(c => { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join('')); + link.rel = 'noopener external nofollow noreferrer'; + link.target = '_blank'; + link.className = element.className; + link.title = element.title; + link.innerHTML = element.innerHTML; + element.parentNode.replaceChild(link, element); + }); + }, + + /** + * One-click copy code support. + */ + registerCopyCode: function() { + document.querySelectorAll('figure.highlight').forEach(element => { + const box = document.createElement('div'); + element.wrap(box); + box.classList.add('highlight-container'); + box.insertAdjacentHTML('beforeend', '
    '); + var button = element.parentNode.querySelector('.copy-btn'); + button.addEventListener('click', event => { + var target = event.currentTarget; + var code = [...target.parentNode.querySelectorAll('.code .line')].map(line => line.innerText).join('\n'); + var ta = document.createElement('textarea'); + ta.style.top = window.scrollY + 'px'; // Prevent page scrolling + ta.style.position = 'absolute'; + ta.style.opacity = '0'; + ta.readOnly = true; + ta.value = code; + document.body.append(ta); + const selection = document.getSelection(); + const selected = selection.rangeCount > 0 ? selection.getRangeAt(0) : false; + ta.select(); + ta.setSelectionRange(0, code.length); + ta.readOnly = false; + var result = document.execCommand('copy'); + if (CONFIG.copycode.show_result) { + target.querySelector('i').className = result ? 'fa fa-check fa-fw' : 'fa fa-times fa-fw'; + } + ta.blur(); // For iOS + target.blur(); + if (selected) { + selection.removeAllRanges(); + selection.addRange(selected); + } + document.body.removeChild(ta); + }); + button.addEventListener('mouseleave', event => { + setTimeout(() => { + event.target.querySelector('i').className = 'fa fa-clipboard fa-fw'; + }, 300); + }); + }); + }, + + wrapTableWithBox: function() { + document.querySelectorAll('table').forEach(element => { + const box = document.createElement('div'); + box.className = 'table-container'; + element.wrap(box); + }); + }, + + registerVideoIframe: function() { + document.querySelectorAll('iframe').forEach(element => { + const supported = [ + 'www.youtube.com', + 'player.vimeo.com', + 'player.youku.com', + 'player.bilibili.com', + 'www.tudou.com' + ].some(host => element.src.includes(host)); + if (supported && !element.parentNode.matches('.video-container')) { + const box = document.createElement('div'); + box.className = 'video-container'; + element.wrap(box); + let width = Number(element.width); + let height = Number(element.height); + if (width && height) { + element.parentNode.style.paddingTop = (height / width * 100) + '%'; + } + } + }); + }, + + registerScrollPercent: function() { + var THRESHOLD = 50; + var backToTop = document.querySelector('.back-to-top'); + var readingProgressBar = document.querySelector('.reading-progress-bar'); + // For init back to top in sidebar if page was scrolled after page refresh. + window.addEventListener('scroll', () => { + if (backToTop || readingProgressBar) { + var docHeight = document.querySelector('.container').offsetHeight; + var winHeight = window.innerHeight; + var contentVisibilityHeight = docHeight > winHeight ? docHeight - winHeight : document.body.scrollHeight - winHeight; + var scrollPercent = Math.min(100 * window.scrollY / contentVisibilityHeight, 100); + if (backToTop) { + backToTop.classList.toggle('back-to-top-on', window.scrollY > THRESHOLD); + backToTop.querySelector('span').innerText = Math.round(scrollPercent) + '%'; + } + if (readingProgressBar) { + readingProgressBar.style.width = scrollPercent.toFixed(2) + '%'; + } + } + }); + + backToTop && backToTop.addEventListener('click', () => { + window.anime({ + targets : document.scrollingElement, + duration : 500, + easing : 'linear', + scrollTop: 0 + }); + }); + }, + + /** + * Tabs tag listener (without twitter bootstrap). + */ + registerTabsTag: function() { + // Binding `nav-tabs` & `tab-content` by real time permalink changing. + document.querySelectorAll('.tabs ul.nav-tabs .tab').forEach(element => { + element.addEventListener('click', event => { + event.preventDefault(); + var target = event.currentTarget; + // Prevent selected tab to select again. + if (!target.classList.contains('active')) { + // Add & Remove active class on `nav-tabs` & `tab-content`. + [...target.parentNode.children].forEach(element => { + element.classList.remove('active'); + }); + target.classList.add('active'); + var tActive = document.getElementById(target.querySelector('a').getAttribute('href').replace('#', '')); + [...tActive.parentNode.children].forEach(element => { + element.classList.remove('active'); + }); + tActive.classList.add('active'); + // Trigger event + tActive.dispatchEvent(new Event('tabs:click', { + bubbles: true + })); + } + }); + }); + + window.dispatchEvent(new Event('tabs:register')); + }, + + registerCanIUseTag: function() { + // Get responsive height passed from iframe. + window.addEventListener('message', ({ data }) => { + if ((typeof data === 'string') && data.includes('ciu_embed')) { + var featureID = data.split(':')[1]; + var height = data.split(':')[2]; + document.querySelector(`iframe[data-feature=${featureID}]`).style.height = parseInt(height, 10) + 5 + 'px'; + } + }, false); + }, + + registerActiveMenuItem: function() { + document.querySelectorAll('.menu-item').forEach(element => { + var target = element.querySelector('a[href]'); + if (!target) return; + var isSamePath = target.pathname === location.pathname || target.pathname === location.pathname.replace('index.html', ''); + var isSubPath = !CONFIG.root.startsWith(target.pathname) && location.pathname.startsWith(target.pathname); + element.classList.toggle('menu-item-active', target.hostname === location.hostname && (isSamePath || isSubPath)); + }); + }, + + registerLangSelect: function() { + let selects = document.querySelectorAll('.lang-select'); + selects.forEach(sel => { + sel.value = CONFIG.page.lang; + sel.addEventListener('change', () => { + let target = sel.options[sel.selectedIndex]; + document.querySelectorAll('.lang-select-label span').forEach(span => span.innerText = target.text); + let url = target.dataset.href; + window.pjax ? window.pjax.loadUrl(url) : window.location.href = url; + }); + }); + }, + + registerSidebarTOC: function() { + const navItems = document.querySelectorAll('.post-toc li'); + const sections = [...navItems].map(element => { + var link = element.querySelector('a.nav-link'); + var target = document.getElementById(decodeURI(link.getAttribute('href')).replace('#', '')); + // TOC item animation navigate. + link.addEventListener('click', event => { + event.preventDefault(); + var offset = target.getBoundingClientRect().top + window.scrollY; + window.anime({ + targets : document.scrollingElement, + duration : 500, + easing : 'linear', + scrollTop: offset + 10 + }); + }); + return target; + }); + + var tocElement = document.querySelector('.post-toc-wrap'); + function activateNavByIndex(target) { + if (target.classList.contains('active-current')) return; + + document.querySelectorAll('.post-toc .active').forEach(element => { + element.classList.remove('active', 'active-current'); + }); + target.classList.add('active', 'active-current'); + var parent = target.parentNode; + while (!parent.matches('.post-toc')) { + if (parent.matches('li')) parent.classList.add('active'); + parent = parent.parentNode; + } + // Scrolling to center active TOC element if TOC content is taller then viewport. + window.anime({ + targets : tocElement, + duration : 200, + easing : 'linear', + scrollTop: tocElement.scrollTop - (tocElement.offsetHeight / 2) + target.getBoundingClientRect().top - tocElement.getBoundingClientRect().top + }); + } + + function findIndex(entries) { + let index = 0; + let entry = entries[index]; + if (entry.boundingClientRect.top > 0) { + index = sections.indexOf(entry.target); + return index === 0 ? 0 : index - 1; + } + for (; index < entries.length; index++) { + if (entries[index].boundingClientRect.top <= 0) { + entry = entries[index]; + } else { + return sections.indexOf(entry.target); + } + } + return sections.indexOf(entry.target); + } + + function createIntersectionObserver(marginTop) { + marginTop = Math.floor(marginTop + 10000); + let intersectionObserver = new IntersectionObserver((entries, observe) => { + let scrollHeight = document.documentElement.scrollHeight + 100; + if (scrollHeight > marginTop) { + observe.disconnect(); + createIntersectionObserver(scrollHeight); + return; + } + let index = findIndex(entries); + activateNavByIndex(navItems[index]); + }, { + rootMargin: marginTop + 'px 0px -100% 0px', + threshold : 0 + }); + sections.forEach(element => { + element && intersectionObserver.observe(element); + }); + } + createIntersectionObserver(document.documentElement.scrollHeight); + }, + + hasMobileUA: function() { + let ua = navigator.userAgent; + let pa = /iPad|iPhone|Android|Opera Mini|BlackBerry|webOS|UCWEB|Blazer|PSP|IEMobile|Symbian/g; + return pa.test(ua); + }, + + isTablet: function() { + return window.screen.width < 992 && window.screen.width > 767 && this.hasMobileUA(); + }, + + isMobile: function() { + return window.screen.width < 767 && this.hasMobileUA(); + }, + + isDesktop: function() { + return !this.isTablet() && !this.isMobile(); + }, + + supportsPDFs: function() { + let ua = navigator.userAgent; + let isFirefoxWithPDFJS = ua.includes('irefox') && parseInt(ua.split('rv:')[1].split('.')[0], 10) > 18; + let supportsPdfMimeType = typeof navigator.mimeTypes['application/pdf'] !== 'undefined'; + let isIOS = /iphone|ipad|ipod/i.test(ua.toLowerCase()); + return isFirefoxWithPDFJS || (supportsPdfMimeType && !isIOS); + }, + + /** + * Init Sidebar & TOC inner dimensions on all pages and for all schemes. + * Need for Sidebar/TOC inner scrolling if content taller then viewport. + */ + initSidebarDimension: function() { + var sidebarNav = document.querySelector('.sidebar-nav'); + var sidebarNavHeight = sidebarNav.style.display !== 'none' ? sidebarNav.offsetHeight : 0; + var sidebarOffset = CONFIG.sidebar.offset || 12; + var sidebarb2tHeight = CONFIG.back2top.enable && CONFIG.back2top.sidebar ? document.querySelector('.back-to-top').offsetHeight : 0; + var sidebarSchemePadding = (CONFIG.sidebar.padding * 2) + sidebarNavHeight + sidebarb2tHeight; + // Margin of sidebar b2t: -4px -10px -18px, brings a different of 22px. + if (CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') sidebarSchemePadding += (sidebarOffset * 2) - 22; + // Initialize Sidebar & TOC Height. + var sidebarWrapperHeight = document.body.offsetHeight - sidebarSchemePadding + 'px'; + document.querySelector('.site-overview-wrap').style.maxHeight = sidebarWrapperHeight; + document.querySelector('.post-toc-wrap').style.maxHeight = sidebarWrapperHeight; + }, + + updateSidebarPosition: function() { + var sidebarNav = document.querySelector('.sidebar-nav'); + var hasTOC = document.querySelector('.post-toc'); + if (hasTOC) { + sidebarNav.style.display = ''; + sidebarNav.classList.add('motion-element'); + document.querySelector('.sidebar-nav-toc').click(); + } else { + sidebarNav.style.display = 'none'; + sidebarNav.classList.remove('motion-element'); + document.querySelector('.sidebar-nav-overview').click(); + } + NexT.utils.initSidebarDimension(); + if (!this.isDesktop() || CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') return; + // Expand sidebar on post detail page by default, when post has a toc. + var display = CONFIG.page.sidebar; + if (typeof display !== 'boolean') { + // There's no definition sidebar in the page front-matter. + display = CONFIG.sidebar.display === 'always' || (CONFIG.sidebar.display === 'post' && hasTOC); + } + if (display) { + window.dispatchEvent(new Event('sidebar:show')); + } + }, + + getScript: function(url, callback, condition) { + if (condition) { + callback(); + } else { + var script = document.createElement('script'); + script.onload = script.onreadystatechange = function(_, isAbort) { + if (isAbort || !script.readyState || /loaded|complete/.test(script.readyState)) { + script.onload = script.onreadystatechange = null; + script = undefined; + if (!isAbort && callback) setTimeout(callback, 0); + } + }; + script.src = url; + document.head.appendChild(script); + } + }, + + loadComments: function(element, callback) { + if (!CONFIG.comments.lazyload || !element) { + callback(); + return; + } + let intersectionObserver = new IntersectionObserver((entries, observer) => { + let entry = entries[0]; + if (entry.isIntersecting) { + callback(); + observer.disconnect(); + } + }); + intersectionObserver.observe(element); + return intersectionObserver; + } +}; diff --git a/lib/anime.min.js b/lib/anime.min.js new file mode 100644 index 0000000000..99b263aaeb --- /dev/null +++ b/lib/anime.min.js @@ -0,0 +1,8 @@ +/* + * anime.js v3.1.0 + * (c) 2019 Julian Garnier + * Released under the MIT license + * animejs.com + */ + +!function(n,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):n.anime=e()}(this,function(){"use strict";var n={update:null,begin:null,loopBegin:null,changeBegin:null,change:null,changeComplete:null,loopComplete:null,complete:null,loop:1,direction:"normal",autoplay:!0,timelineOffset:0},e={duration:1e3,delay:0,endDelay:0,easing:"easeOutElastic(1, .5)",round:0},r=["translateX","translateY","translateZ","rotate","rotateX","rotateY","rotateZ","scale","scaleX","scaleY","scaleZ","skew","skewX","skewY","perspective"],t={CSS:{},springs:{}};function a(n,e,r){return Math.min(Math.max(n,e),r)}function o(n,e){return n.indexOf(e)>-1}function u(n,e){return n.apply(null,e)}var i={arr:function(n){return Array.isArray(n)},obj:function(n){return o(Object.prototype.toString.call(n),"Object")},pth:function(n){return i.obj(n)&&n.hasOwnProperty("totalLength")},svg:function(n){return n instanceof SVGElement},inp:function(n){return n instanceof HTMLInputElement},dom:function(n){return n.nodeType||i.svg(n)},str:function(n){return"string"==typeof n},fnc:function(n){return"function"==typeof n},und:function(n){return void 0===n},hex:function(n){return/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(n)},rgb:function(n){return/^rgb/.test(n)},hsl:function(n){return/^hsl/.test(n)},col:function(n){return i.hex(n)||i.rgb(n)||i.hsl(n)},key:function(r){return!n.hasOwnProperty(r)&&!e.hasOwnProperty(r)&&"targets"!==r&&"keyframes"!==r}};function c(n){var e=/\(([^)]+)\)/.exec(n);return e?e[1].split(",").map(function(n){return parseFloat(n)}):[]}function s(n,e){var r=c(n),o=a(i.und(r[0])?1:r[0],.1,100),u=a(i.und(r[1])?100:r[1],.1,100),s=a(i.und(r[2])?10:r[2],.1,100),f=a(i.und(r[3])?0:r[3],.1,100),l=Math.sqrt(u/o),d=s/(2*Math.sqrt(u*o)),p=d<1?l*Math.sqrt(1-d*d):0,h=1,v=d<1?(d*l-f)/p:-f+l;function g(n){var r=e?e*n/1e3:n;return r=d<1?Math.exp(-r*d*l)*(h*Math.cos(p*r)+v*Math.sin(p*r)):(h+v*r)*Math.exp(-r*l),0===n||1===n?n:1-r}return e?g:function(){var e=t.springs[n];if(e)return e;for(var r=0,a=0;;)if(1===g(r+=1/6)){if(++a>=16)break}else a=0;var o=r*(1/6)*1e3;return t.springs[n]=o,o}}function f(n){return void 0===n&&(n=10),function(e){return Math.round(e*n)*(1/n)}}var l,d,p=function(){var n=11,e=1/(n-1);function r(n,e){return 1-3*e+3*n}function t(n,e){return 3*e-6*n}function a(n){return 3*n}function o(n,e,o){return((r(e,o)*n+t(e,o))*n+a(e))*n}function u(n,e,o){return 3*r(e,o)*n*n+2*t(e,o)*n+a(e)}return function(r,t,a,i){if(0<=r&&r<=1&&0<=a&&a<=1){var c=new Float32Array(n);if(r!==t||a!==i)for(var s=0;s=.001?function(n,e,r,t){for(var a=0;a<4;++a){var i=u(e,r,t);if(0===i)return e;e-=(o(e,r,t)-n)/i}return e}(t,l,r,a):0===d?l:function(n,e,r,t,a){for(var u,i,c=0;(u=o(i=e+(r-e)/2,t,a)-n)>0?r=i:e=i,Math.abs(u)>1e-7&&++c<10;);return i}(t,i,i+e,r,a)}}}(),h=(l={linear:function(){return function(n){return n}}},d={Sine:function(){return function(n){return 1-Math.cos(n*Math.PI/2)}},Circ:function(){return function(n){return 1-Math.sqrt(1-n*n)}},Back:function(){return function(n){return n*n*(3*n-2)}},Bounce:function(){return function(n){for(var e,r=4;n<((e=Math.pow(2,--r))-1)/11;);return 1/Math.pow(4,3-r)-7.5625*Math.pow((3*e-2)/22-n,2)}},Elastic:function(n,e){void 0===n&&(n=1),void 0===e&&(e=.5);var r=a(n,1,10),t=a(e,.1,2);return function(n){return 0===n||1===n?n:-r*Math.pow(2,10*(n-1))*Math.sin((n-1-t/(2*Math.PI)*Math.asin(1/r))*(2*Math.PI)/t)}}},["Quad","Cubic","Quart","Quint","Expo"].forEach(function(n,e){d[n]=function(){return function(n){return Math.pow(n,e+2)}}}),Object.keys(d).forEach(function(n){var e=d[n];l["easeIn"+n]=e,l["easeOut"+n]=function(n,r){return function(t){return 1-e(n,r)(1-t)}},l["easeInOut"+n]=function(n,r){return function(t){return t<.5?e(n,r)(2*t)/2:1-e(n,r)(-2*t+2)/2}}}),l);function v(n,e){if(i.fnc(n))return n;var r=n.split("(")[0],t=h[r],a=c(n);switch(r){case"spring":return s(n,e);case"cubicBezier":return u(p,a);case"steps":return u(f,a);default:return u(t,a)}}function g(n){try{return document.querySelectorAll(n)}catch(n){return}}function m(n,e){for(var r=n.length,t=arguments.length>=2?arguments[1]:void 0,a=[],o=0;o1&&(r-=1),r<1/6?n+6*(e-n)*r:r<.5?e:r<2/3?n+(e-n)*(2/3-r)*6:n}if(0==u)e=r=t=i;else{var f=i<.5?i*(1+u):i+u-i*u,l=2*i-f;e=s(l,f,o+1/3),r=s(l,f,o),t=s(l,f,o-1/3)}return"rgba("+255*e+","+255*r+","+255*t+","+c+")"}(n):void 0;var e,r,t,a}function C(n){var e=/[+-]?\d*\.?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?(%|px|pt|em|rem|in|cm|mm|ex|ch|pc|vw|vh|vmin|vmax|deg|rad|turn)?$/.exec(n);if(e)return e[1]}function B(n,e){return i.fnc(n)?n(e.target,e.id,e.total):n}function P(n,e){return n.getAttribute(e)}function I(n,e,r){if(M([r,"deg","rad","turn"],C(e)))return e;var a=t.CSS[e+r];if(!i.und(a))return a;var o=document.createElement(n.tagName),u=n.parentNode&&n.parentNode!==document?n.parentNode:document.body;u.appendChild(o),o.style.position="absolute",o.style.width=100+r;var c=100/o.offsetWidth;u.removeChild(o);var s=c*parseFloat(e);return t.CSS[e+r]=s,s}function T(n,e,r){if(e in n.style){var t=e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase(),a=n.style[e]||getComputedStyle(n).getPropertyValue(t)||"0";return r?I(n,a,r):a}}function D(n,e){return i.dom(n)&&!i.inp(n)&&(P(n,e)||i.svg(n)&&n[e])?"attribute":i.dom(n)&&M(r,e)?"transform":i.dom(n)&&"transform"!==e&&T(n,e)?"css":null!=n[e]?"object":void 0}function E(n){if(i.dom(n)){for(var e,r=n.style.transform||"",t=/(\w+)\(([^)]*)\)/g,a=new Map;e=t.exec(r);)a.set(e[1],e[2]);return a}}function F(n,e,r,t){var a,u=o(e,"scale")?1:0+(o(a=e,"translate")||"perspective"===a?"px":o(a,"rotate")||o(a,"skew")?"deg":void 0),i=E(n).get(e)||u;return r&&(r.transforms.list.set(e,i),r.transforms.last=e),t?I(n,i,t):i}function N(n,e,r,t){switch(D(n,e)){case"transform":return F(n,e,t,r);case"css":return T(n,e,r);case"attribute":return P(n,e);default:return n[e]||0}}function A(n,e){var r=/^(\*=|\+=|-=)/.exec(n);if(!r)return n;var t=C(n)||0,a=parseFloat(e),o=parseFloat(n.replace(r[0],""));switch(r[0][0]){case"+":return a+o+t;case"-":return a-o+t;case"*":return a*o+t}}function L(n,e){if(i.col(n))return O(n);if(/\s/g.test(n))return n;var r=C(n),t=r?n.substr(0,n.length-r.length):n;return e?t+e:t}function j(n,e){return Math.sqrt(Math.pow(e.x-n.x,2)+Math.pow(e.y-n.y,2))}function S(n){for(var e,r=n.points,t=0,a=0;a0&&(t+=j(e,o)),e=o}return t}function q(n){if(n.getTotalLength)return n.getTotalLength();switch(n.tagName.toLowerCase()){case"circle":return o=n,2*Math.PI*P(o,"r");case"rect":return 2*P(a=n,"width")+2*P(a,"height");case"line":return j({x:P(t=n,"x1"),y:P(t,"y1")},{x:P(t,"x2"),y:P(t,"y2")});case"polyline":return S(n);case"polygon":return r=(e=n).points,S(e)+j(r.getItem(r.numberOfItems-1),r.getItem(0))}var e,r,t,a,o}function $(n,e){var r=e||{},t=r.el||function(n){for(var e=n.parentNode;i.svg(e)&&i.svg(e.parentNode);)e=e.parentNode;return e}(n),a=t.getBoundingClientRect(),o=P(t,"viewBox"),u=a.width,c=a.height,s=r.viewBox||(o?o.split(" "):[0,0,u,c]);return{el:t,viewBox:s,x:s[0]/1,y:s[1]/1,w:u/s[2],h:c/s[3]}}function X(n,e){function r(r){void 0===r&&(r=0);var t=e+r>=1?e+r:0;return n.el.getPointAtLength(t)}var t=$(n.el,n.svg),a=r(),o=r(-1),u=r(1);switch(n.property){case"x":return(a.x-t.x)*t.w;case"y":return(a.y-t.y)*t.h;case"angle":return 180*Math.atan2(u.y-o.y,u.x-o.x)/Math.PI}}function Y(n,e){var r=/[+-]?\d*\.?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g,t=L(i.pth(n)?n.totalLength:n,e)+"";return{original:t,numbers:t.match(r)?t.match(r).map(Number):[0],strings:i.str(n)||e?t.split(r):[]}}function Z(n){return m(n?y(i.arr(n)?n.map(b):b(n)):[],function(n,e,r){return r.indexOf(n)===e})}function Q(n){var e=Z(n);return e.map(function(n,r){return{target:n,id:r,total:e.length,transforms:{list:E(n)}}})}function V(n,e){var r=x(e);if(/^spring/.test(r.easing)&&(r.duration=s(r.easing)),i.arr(n)){var t=n.length;2===t&&!i.obj(n[0])?n={value:n}:i.fnc(e.duration)||(r.duration=e.duration/t)}var a=i.arr(n)?n:[n];return a.map(function(n,r){var t=i.obj(n)&&!i.pth(n)?n:{value:n};return i.und(t.delay)&&(t.delay=r?0:e.delay),i.und(t.endDelay)&&(t.endDelay=r===a.length-1?e.endDelay:0),t}).map(function(n){return k(n,r)})}function z(n,e){var r=[],t=e.keyframes;for(var a in t&&(e=k(function(n){for(var e=m(y(n.map(function(n){return Object.keys(n)})),function(n){return i.key(n)}).reduce(function(n,e){return n.indexOf(e)<0&&n.push(e),n},[]),r={},t=function(t){var a=e[t];r[a]=n.map(function(n){var e={};for(var r in n)i.key(r)?r==a&&(e.value=n[r]):e[r]=n[r];return e})},a=0;a-1&&(_.splice(o,1),r=_.length)}else a.tick(e);t++}n()}else U=cancelAnimationFrame(U)}return n}();function rn(r){void 0===r&&(r={});var t,o=0,u=0,i=0,c=0,s=null;function f(n){var e=window.Promise&&new Promise(function(n){return s=n});return n.finished=e,e}var l,d,p,h,v,g,y,b,M=(d=w(n,l=r),p=w(e,l),h=z(p,l),v=Q(l.targets),g=W(v,h),y=J(g,p),b=K,K++,k(d,{id:b,children:[],animatables:v,animations:g,duration:y.duration,delay:y.delay,endDelay:y.endDelay}));f(M);function x(){var n=M.direction;"alternate"!==n&&(M.direction="normal"!==n?"normal":"reverse"),M.reversed=!M.reversed,t.forEach(function(n){return n.reversed=M.reversed})}function O(n){return M.reversed?M.duration-n:n}function C(){o=0,u=O(M.currentTime)*(1/rn.speed)}function B(n,e){e&&e.seek(n-e.timelineOffset)}function P(n){for(var e=0,r=M.animations,t=r.length;e2||(b=Math.round(b*p)/p)),h.push(b)}var k=d.length;if(k){g=d[0];for(var O=0;O0&&(M.began=!0,I("begin")),!M.loopBegan&&M.currentTime>0&&(M.loopBegan=!0,I("loopBegin")),d<=r&&0!==M.currentTime&&P(0),(d>=l&&M.currentTime!==e||!e)&&P(e),d>r&&d=e&&(u=0,M.remaining&&!0!==M.remaining&&M.remaining--,M.remaining?(o=i,I("loopComplete"),M.loopBegan=!1,"alternate"===M.direction&&x()):(M.paused=!0,M.completed||(M.completed=!0,I("loopComplete"),I("complete"),!M.passThrough&&"Promise"in window&&(s(),f(M)))))}return M.reset=function(){var n=M.direction;M.passThrough=!1,M.currentTime=0,M.progress=0,M.paused=!0,M.began=!1,M.loopBegan=!1,M.changeBegan=!1,M.completed=!1,M.changeCompleted=!1,M.reversePlayback=!1,M.reversed="reverse"===n,M.remaining=M.loop,t=M.children;for(var e=c=t.length;e--;)M.children[e].reset();(M.reversed&&!0!==M.loop||"alternate"===n&&1===M.loop)&&M.remaining++,P(M.reversed?M.duration:0)},M.set=function(n,e){return R(n,e),M},M.tick=function(n){i=n,o||(o=i),T((i+(u-o))*rn.speed)},M.seek=function(n){T(O(n))},M.pause=function(){M.paused=!0,C()},M.play=function(){M.paused&&(M.completed&&M.reset(),M.paused=!1,_.push(M),C(),U||en())},M.reverse=function(){x(),C()},M.restart=function(){M.reset(),M.play()},M.reset(),M.autoplay&&M.play(),M}function tn(n,e){for(var r=e.length;r--;)M(n,e[r].animatable.target)&&e.splice(r,1)}return"undefined"!=typeof document&&document.addEventListener("visibilitychange",function(){document.hidden?(_.forEach(function(n){return n.pause()}),nn=_.slice(0),rn.running=_=[]):nn.forEach(function(n){return n.play()})}),rn.version="3.1.0",rn.speed=1,rn.running=_,rn.remove=function(n){for(var e=Z(n),r=_.length;r--;){var t=_[r],a=t.animations,o=t.children;tn(e,a);for(var u=o.length;u--;){var i=o[u],c=i.animations;tn(e,c),c.length||i.children.length||o.splice(u,1)}a.length||o.length||t.pause()}},rn.get=N,rn.set=R,rn.convertPx=I,rn.path=function(n,e){var r=i.str(n)?g(n)[0]:n,t=e||100;return function(n){return{property:n,el:r,svg:$(r),totalLength:q(r)*(t/100)}}},rn.setDashoffset=function(n){var e=q(n);return n.setAttribute("stroke-dasharray",e),e},rn.stagger=function(n,e){void 0===e&&(e={});var r=e.direction||"normal",t=e.easing?v(e.easing):null,a=e.grid,o=e.axis,u=e.from||0,c="first"===u,s="center"===u,f="last"===u,l=i.arr(n),d=l?parseFloat(n[0]):parseFloat(n),p=l?parseFloat(n[1]):0,h=C(l?n[1]:n)||0,g=e.start||0+(l?d:0),m=[],y=0;return function(n,e,i){if(c&&(u=0),s&&(u=(i-1)/2),f&&(u=i-1),!m.length){for(var v=0;v-1&&_.splice(o,1);for(var s=0;sli{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adobe:before{content:"\f778"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-bahai:before{content:"\f666"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-box-tissue:before{content:"\f95b"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buy-n-large:before{content:"\f8a6"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caravan:before{content:"\f8ff"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-alt:before{content:"\f422"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cotton-bureau:before{content:"\f89e"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dailymotion:before{content:"\f952"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-disease:before{content:"\f7fa"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-alt:before{content:"\f424"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-faucet:before{content:"\f905"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-firefox-browser:before{content:"\f907"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-medical:before{content:"\f95c"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-holding-water:before{content:"\f4c1"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-sparkles:before{content:"\f95d"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-hands-wash:before{content:"\f95e"}.fa-handshake:before{content:"\f2b5"}.fa-handshake-alt-slash:before{content:"\f95f"}.fa-handshake-slash:before{content:"\f960"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-cowboy:before{content:"\f8c0"}.fa-hat-cowboy-side:before{content:"\f8c1"}.fa-hat-wizard:before{content:"\f6e8"}.fa-hdd:before{content:"\f0a0"}.fa-head-side-cough:before{content:"\f961"}.fa-head-side-cough-slash:before{content:"\f962"}.fa-head-side-mask:before{content:"\f963"}.fa-head-side-virus:before{content:"\f964"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hospital-user:before{content:"\f80d"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-house-user:before{content:"\f965"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-ideal:before{content:"\f913"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-instagram-square:before{content:"\f955"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-house:before{content:"\f966"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lungs:before{content:"\f604"}.fa-lungs-virus:before{content:"\f967"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-mdb:before{content:"\f8ca"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microblog:before{content:"\f91a"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mixer:before{content:"\f956"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse:before{content:"\f8cc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-orcid:before{content:"\f8d2"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-arrows:before{content:"\f968"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-square:before{content:"\f91e"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-plane-slash:before{content:"\f969"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pump-medical:before{content:"\f96a"}.fa-pump-soap:before{content:"\f96b"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-record-vinyl:before{content:"\f8d9"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-shield-virus:before{content:"\f96c"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopify:before{content:"\f957"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-soap:before{content:"\f96e"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-stopwatch-20:before{content:"\f96f"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-store-alt-slash:before{content:"\f970"}.fa-store-slash:before{content:"\f971"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swift:before{content:"\f8e1"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toilet-paper-slash:before{content:"\f972"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-trailer:before{content:"\f941"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbraco:before{content:"\f8e8"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-unity:before{content:"\f949"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-virus:before{content:"\f974"}.fa-virus-slash:before{content:"\f975"}.fa-viruses:before{content:"\f976"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}@font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands"}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.fab,.far{font-weight:400}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.far,.fas{font-family:"Font Awesome 5 Free"}.fa,.fas{font-weight:900} \ No newline at end of file diff --git a/lib/font-awesome/webfonts/fa-brands-400.woff2 b/lib/font-awesome/webfonts/fa-brands-400.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..141a90a9e0a4b5a794557efa098a5c52320fb226 GIT binary patch literal 76612 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 literal 0 HcmV?d00001 diff --git a/lib/font-awesome/webfonts/fa-regular-400.woff2 b/lib/font-awesome/webfonts/fa-regular-400.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..7e0118e526eb53511cb57e7cfaf515784fee4345 GIT binary patch literal 13584 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 W8!VVrwhAGt$p&j$lnnbyiU9!e_&Ztv literal 0 HcmV?d00001 diff --git a/lib/font-awesome/webfonts/fa-solid-900.woff2 b/lib/font-awesome/webfonts/fa-solid-900.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..978a681a10ff0478581436eaca5c5695c97445d4 GIT binary patch literal 79444 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

    wQH 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 literal 0 HcmV?d00001 diff --git a/lib/velocity/velocity.min.js b/lib/velocity/velocity.min.js new file mode 100644 index 0000000000..58244c80e3 --- /dev/null +++ b/lib/velocity/velocity.min.js @@ -0,0 +1,4 @@ +/*! VelocityJS.org (1.2.2). (C) 2014 Julian Shapiro. MIT @license: en.wikipedia.org/wiki/MIT_License */ +/*! VelocityJS.org jQuery Shim (1.0.1). (C) 2014 The jQuery Foundation. MIT @license: en.wikipedia.org/wiki/MIT_License. */ +!function(e){function t(e){var t=e.length,r=$.type(e);return"function"===r||$.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===r||0===t||"number"==typeof t&&t>0&&t-1 in e}if(!e.jQuery){var $=function(e,t){return new $.fn.init(e,t)};$.isWindow=function(e){return null!=e&&e==e.window},$.type=function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?a[o.call(e)]||"object":typeof e},$.isArray=Array.isArray||function(e){return"array"===$.type(e)},$.isPlainObject=function(e){var t;if(!e||"object"!==$.type(e)||e.nodeType||$.isWindow(e))return!1;try{if(e.constructor&&!n.call(e,"constructor")&&!n.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(r){return!1}for(t in e);return void 0===t||n.call(e,t)},$.each=function(e,r,a){var n,o=0,i=e.length,s=t(e);if(a){if(s)for(;i>o&&(n=r.apply(e[o],a),n!==!1);o++);else for(o in e)if(n=r.apply(e[o],a),n===!1)break}else if(s)for(;i>o&&(n=r.call(e[o],o,e[o]),n!==!1);o++);else for(o in e)if(n=r.call(e[o],o,e[o]),n===!1)break;return e},$.data=function(e,t,a){if(void 0===a){var n=e[$.expando],o=n&&r[n];if(void 0===t)return o;if(o&&t in o)return o[t]}else if(void 0!==t){var n=e[$.expando]||(e[$.expando]=++$.uuid);return r[n]=r[n]||{},r[n][t]=a,a}},$.removeData=function(e,t){var a=e[$.expando],n=a&&r[a];n&&$.each(t,function(e,t){delete n[t]})},$.extend=function(){var e,t,r,a,n,o,i=arguments[0]||{},s=1,l=arguments.length,u=!1;for("boolean"==typeof i&&(u=i,i=arguments[s]||{},s++),"object"!=typeof i&&"function"!==$.type(i)&&(i={}),s===l&&(i=this,s--);l>s;s++)if(null!=(n=arguments[s]))for(a in n)e=i[a],r=n[a],i!==r&&(u&&r&&($.isPlainObject(r)||(t=$.isArray(r)))?(t?(t=!1,o=e&&$.isArray(e)?e:[]):o=e&&$.isPlainObject(e)?e:{},i[a]=$.extend(u,o,r)):void 0!==r&&(i[a]=r));return i},$.queue=function(e,r,a){function n(e,r){var a=r||[];return null!=e&&(t(Object(e))?!function(e,t){for(var r=+t.length,a=0,n=e.length;r>a;)e[n++]=t[a++];if(r!==r)for(;void 0!==t[a];)e[n++]=t[a++];return e.length=n,e}(a,"string"==typeof e?[e]:e):[].push.call(a,e)),a}if(e){r=(r||"fx")+"queue";var o=$.data(e,r);return a?(!o||$.isArray(a)?o=$.data(e,r,n(a)):o.push(a),o):o||[]}},$.dequeue=function(e,t){$.each(e.nodeType?[e]:e,function(e,r){t=t||"fx";var a=$.queue(r,t),n=a.shift();"inprogress"===n&&(n=a.shift()),n&&("fx"===t&&a.unshift("inprogress"),n.call(r,function(){$.dequeue(r,t)}))})},$.fn=$.prototype={init:function(e){if(e.nodeType)return this[0]=e,this;throw new Error("Not a DOM node.")},offset:function(){var t=this[0].getBoundingClientRect?this[0].getBoundingClientRect():{top:0,left:0};return{top:t.top+(e.pageYOffset||document.scrollTop||0)-(document.clientTop||0),left:t.left+(e.pageXOffset||document.scrollLeft||0)-(document.clientLeft||0)}},position:function(){function e(){for(var e=this.offsetParent||document;e&&"html"===!e.nodeType.toLowerCase&&"static"===e.style.position;)e=e.offsetParent;return e||document}var t=this[0],e=e.apply(t),r=this.offset(),a=/^(?:body|html)$/i.test(e.nodeName)?{top:0,left:0}:$(e).offset();return r.top-=parseFloat(t.style.marginTop)||0,r.left-=parseFloat(t.style.marginLeft)||0,e.style&&(a.top+=parseFloat(e.style.borderTopWidth)||0,a.left+=parseFloat(e.style.borderLeftWidth)||0),{top:r.top-a.top,left:r.left-a.left}}};var r={};$.expando="velocity"+(new Date).getTime(),$.uuid=0;for(var a={},n=a.hasOwnProperty,o=a.toString,i="Boolean Number String Function Array Date RegExp Object Error".split(" "),s=0;sn;++n){var o=u(r,e,a);if(0===o)return r;var i=l(r,e,a)-t;r-=i/o}return r}function p(){for(var t=0;b>t;++t)w[t]=l(t*x,e,a)}function f(t,r,n){var o,i,s=0;do i=r+(n-r)/2,o=l(i,e,a)-t,o>0?n=i:r=i;while(Math.abs(o)>h&&++s=y?c(t,s):0==l?s:f(t,r,r+x)}function g(){V=!0,(e!=r||a!=n)&&p()}var m=4,y=.001,h=1e-7,v=10,b=11,x=1/(b-1),S="Float32Array"in t;if(4!==arguments.length)return!1;for(var P=0;4>P;++P)if("number"!=typeof arguments[P]||isNaN(arguments[P])||!isFinite(arguments[P]))return!1;e=Math.min(e,1),a=Math.min(a,1),e=Math.max(e,0),a=Math.max(a,0);var w=S?new Float32Array(b):new Array(b),V=!1,C=function(t){return V||g(),e===r&&a===n?t:0===t?0:1===t?1:l(d(t),r,n)};C.getControlPoints=function(){return[{x:e,y:r},{x:a,y:n}]};var T="generateBezier("+[e,r,a,n]+")";return C.toString=function(){return T},C}function u(e,t){var r=e;return g.isString(e)?v.Easings[e]||(r=!1):r=g.isArray(e)&&1===e.length?s.apply(null,e):g.isArray(e)&&2===e.length?b.apply(null,e.concat([t])):g.isArray(e)&&4===e.length?l.apply(null,e):!1,r===!1&&(r=v.Easings[v.defaults.easing]?v.defaults.easing:h),r}function c(e){if(e){var t=(new Date).getTime(),r=v.State.calls.length;r>1e4&&(v.State.calls=n(v.State.calls));for(var o=0;r>o;o++)if(v.State.calls[o]){var s=v.State.calls[o],l=s[0],u=s[2],f=s[3],d=!!f,m=null;f||(f=v.State.calls[o][3]=t-16);for(var y=Math.min((t-f)/u.duration,1),h=0,b=l.length;b>h;h++){var S=l[h],w=S.element;if(i(w)){var V=!1;if(u.display!==a&&null!==u.display&&"none"!==u.display){if("flex"===u.display){var C=["-webkit-box","-moz-box","-ms-flexbox","-webkit-flex"];$.each(C,function(e,t){x.setPropertyValue(w,"display",t)})}x.setPropertyValue(w,"display",u.display)}u.visibility!==a&&"hidden"!==u.visibility&&x.setPropertyValue(w,"visibility",u.visibility);for(var T in S)if("element"!==T){var k=S[T],A,F=g.isString(k.easing)?v.Easings[k.easing]:k.easing;if(1===y)A=k.endValue;else{var E=k.endValue-k.startValue;if(A=k.startValue+E*F(y,u,E),!d&&A===k.currentValue)continue}if(k.currentValue=A,"tween"===T)m=A;else{if(x.Hooks.registered[T]){var j=x.Hooks.getRoot(T),H=i(w).rootPropertyValueCache[j];H&&(k.rootPropertyValue=H)}var N=x.setPropertyValue(w,T,k.currentValue+(0===parseFloat(A)?"":k.unitType),k.rootPropertyValue,k.scrollData);x.Hooks.registered[T]&&(i(w).rootPropertyValueCache[j]=x.Normalizations.registered[j]?x.Normalizations.registered[j]("extract",null,N[1]):N[1]),"transform"===N[0]&&(V=!0)}}u.mobileHA&&i(w).transformCache.translate3d===a&&(i(w).transformCache.translate3d="(0px, 0px, 0px)",V=!0),V&&x.flushTransformCache(w)}}u.display!==a&&"none"!==u.display&&(v.State.calls[o][2].display=!1),u.visibility!==a&&"hidden"!==u.visibility&&(v.State.calls[o][2].visibility=!1),u.progress&&u.progress.call(s[1],s[1],y,Math.max(0,f+u.duration-t),f,m),1===y&&p(o)}}v.State.isTicking&&P(c)}function p(e,t){if(!v.State.calls[e])return!1;for(var r=v.State.calls[e][0],n=v.State.calls[e][1],o=v.State.calls[e][2],s=v.State.calls[e][4],l=!1,u=0,c=r.length;c>u;u++){var p=r[u].element;if(t||o.loop||("none"===o.display&&x.setPropertyValue(p,"display",o.display),"hidden"===o.visibility&&x.setPropertyValue(p,"visibility",o.visibility)),o.loop!==!0&&($.queue(p)[1]===a||!/\.velocityQueueEntryFlag/i.test($.queue(p)[1]))&&i(p)){i(p).isAnimating=!1,i(p).rootPropertyValueCache={};var f=!1;$.each(x.Lists.transforms3D,function(e,t){var r=/^scale/.test(t)?1:0,n=i(p).transformCache[t];i(p).transformCache[t]!==a&&new RegExp("^\\("+r+"[^.]").test(n)&&(f=!0,delete i(p).transformCache[t])}),o.mobileHA&&(f=!0,delete i(p).transformCache.translate3d),f&&x.flushTransformCache(p),x.Values.removeClass(p,"velocity-animating")}if(!t&&o.complete&&!o.loop&&u===c-1)try{o.complete.call(n,n)}catch(d){setTimeout(function(){throw d},1)}s&&o.loop!==!0&&s(n),i(p)&&o.loop===!0&&!t&&($.each(i(p).tweensContainer,function(e,t){/^rotate/.test(e)&&360===parseFloat(t.endValue)&&(t.endValue=0,t.startValue=360),/^backgroundPosition/.test(e)&&100===parseFloat(t.endValue)&&"%"===t.unitType&&(t.endValue=0,t.startValue=100)}),v(p,"reverse",{loop:!0,delay:o.delay})),o.queue!==!1&&$.dequeue(p,o.queue)}v.State.calls[e]=!1;for(var g=0,m=v.State.calls.length;m>g;g++)if(v.State.calls[g]!==!1){l=!0;break}l===!1&&(v.State.isTicking=!1,delete v.State.calls,v.State.calls=[])}var f=function(){if(r.documentMode)return r.documentMode;for(var e=7;e>4;e--){var t=r.createElement("div");if(t.innerHTML="",t.getElementsByTagName("span").length)return t=null,e}return a}(),d=function(){var e=0;return t.webkitRequestAnimationFrame||t.mozRequestAnimationFrame||function(t){var r=(new Date).getTime(),a;return a=Math.max(0,16-(r-e)),e=r+a,setTimeout(function(){t(r+a)},a)}}(),g={isString:function(e){return"string"==typeof e},isArray:Array.isArray||function(e){return"[object Array]"===Object.prototype.toString.call(e)},isFunction:function(e){return"[object Function]"===Object.prototype.toString.call(e)},isNode:function(e){return e&&e.nodeType},isNodeList:function(e){return"object"==typeof e&&/^\[object (HTMLCollection|NodeList|Object)\]$/.test(Object.prototype.toString.call(e))&&e.length!==a&&(0===e.length||"object"==typeof e[0]&&e[0].nodeType>0)},isWrapped:function(e){return e&&(e.jquery||t.Zepto&&t.Zepto.zepto.isZ(e))},isSVG:function(e){return t.SVGElement&&e instanceof t.SVGElement},isEmptyObject:function(e){for(var t in e)return!1;return!0}},$,m=!1;if(e.fn&&e.fn.jquery?($=e,m=!0):$=t.Velocity.Utilities,8>=f&&!m)throw new Error("Velocity: IE8 and below require jQuery to be loaded before Velocity.");if(7>=f)return void(jQuery.fn.velocity=jQuery.fn.animate);var y=400,h="swing",v={State:{isMobile:/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),isAndroid:/Android/i.test(navigator.userAgent),isGingerbread:/Android 2\.3\.[3-7]/i.test(navigator.userAgent),isChrome:t.chrome,isFirefox:/Firefox/i.test(navigator.userAgent),prefixElement:r.createElement("div"),prefixMatches:{},scrollAnchor:null,scrollPropertyLeft:null,scrollPropertyTop:null,isTicking:!1,calls:[]},CSS:{},Utilities:$,Redirects:{},Easings:{},Promise:t.Promise,defaults:{queue:"",duration:y,easing:h,begin:a,complete:a,progress:a,display:a,visibility:a,loop:!1,delay:!1,mobileHA:!0,_cacheValues:!0},init:function(e){$.data(e,"velocity",{isSVG:g.isSVG(e),isAnimating:!1,computedStyle:null,tweensContainer:null,rootPropertyValueCache:{},transformCache:{}})},hook:null,mock:!1,version:{major:1,minor:2,patch:2},debug:!1};t.pageYOffset!==a?(v.State.scrollAnchor=t,v.State.scrollPropertyLeft="pageXOffset",v.State.scrollPropertyTop="pageYOffset"):(v.State.scrollAnchor=r.documentElement||r.body.parentNode||r.body,v.State.scrollPropertyLeft="scrollLeft",v.State.scrollPropertyTop="scrollTop");var b=function(){function e(e){return-e.tension*e.x-e.friction*e.v}function t(t,r,a){var n={x:t.x+a.dx*r,v:t.v+a.dv*r,tension:t.tension,friction:t.friction};return{dx:n.v,dv:e(n)}}function r(r,a){var n={dx:r.v,dv:e(r)},o=t(r,.5*a,n),i=t(r,.5*a,o),s=t(r,a,i),l=1/6*(n.dx+2*(o.dx+i.dx)+s.dx),u=1/6*(n.dv+2*(o.dv+i.dv)+s.dv);return r.x=r.x+l*a,r.v=r.v+u*a,r}return function a(e,t,n){var o={x:-1,v:0,tension:null,friction:null},i=[0],s=0,l=1e-4,u=.016,c,p,f;for(e=parseFloat(e)||500,t=parseFloat(t)||20,n=n||null,o.tension=e,o.friction=t,c=null!==n,c?(s=a(e,t),p=s/n*u):p=u;;)if(f=r(f||o,p),i.push(1+f.x),s+=16,!(Math.abs(f.x)>l&&Math.abs(f.v)>l))break;return c?function(e){return i[e*(i.length-1)|0]}:s}}();v.Easings={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},spring:function(e){return 1-Math.cos(4.5*e*Math.PI)*Math.exp(6*-e)}},$.each([["ease",[.25,.1,.25,1]],["ease-in",[.42,0,1,1]],["ease-out",[0,0,.58,1]],["ease-in-out",[.42,0,.58,1]],["easeInSine",[.47,0,.745,.715]],["easeOutSine",[.39,.575,.565,1]],["easeInOutSine",[.445,.05,.55,.95]],["easeInQuad",[.55,.085,.68,.53]],["easeOutQuad",[.25,.46,.45,.94]],["easeInOutQuad",[.455,.03,.515,.955]],["easeInCubic",[.55,.055,.675,.19]],["easeOutCubic",[.215,.61,.355,1]],["easeInOutCubic",[.645,.045,.355,1]],["easeInQuart",[.895,.03,.685,.22]],["easeOutQuart",[.165,.84,.44,1]],["easeInOutQuart",[.77,0,.175,1]],["easeInQuint",[.755,.05,.855,.06]],["easeOutQuint",[.23,1,.32,1]],["easeInOutQuint",[.86,0,.07,1]],["easeInExpo",[.95,.05,.795,.035]],["easeOutExpo",[.19,1,.22,1]],["easeInOutExpo",[1,0,0,1]],["easeInCirc",[.6,.04,.98,.335]],["easeOutCirc",[.075,.82,.165,1]],["easeInOutCirc",[.785,.135,.15,.86]]],function(e,t){v.Easings[t[0]]=l.apply(null,t[1])});var x=v.CSS={RegEx:{isHex:/^#([A-f\d]{3}){1,2}$/i,valueUnwrap:/^[A-z]+\((.*)\)$/i,wrappedValueAlreadyExtracted:/[0-9.]+ [0-9.]+ [0-9.]+( [0-9.]+)?/,valueSplit:/([A-z]+\(.+\))|(([A-z0-9#-.]+?)(?=\s|$))/gi},Lists:{colors:["fill","stroke","stopColor","color","backgroundColor","borderColor","borderTopColor","borderRightColor","borderBottomColor","borderLeftColor","outlineColor"],transformsBase:["translateX","translateY","scale","scaleX","scaleY","skewX","skewY","rotateZ"],transforms3D:["transformPerspective","translateZ","scaleZ","rotateX","rotateY"]},Hooks:{templates:{textShadow:["Color X Y Blur","black 0px 0px 0px"],boxShadow:["Color X Y Blur Spread","black 0px 0px 0px 0px"],clip:["Top Right Bottom Left","0px 0px 0px 0px"],backgroundPosition:["X Y","0% 0%"],transformOrigin:["X Y Z","50% 50% 0px"],perspectiveOrigin:["X Y","50% 50%"]},registered:{},register:function(){for(var e=0;e=f)switch(e){case"name":return"filter";case"extract":var a=r.toString().match(/alpha\(opacity=(.*)\)/i);return r=a?a[1]/100:1;case"inject":return t.style.zoom=1,parseFloat(r)>=1?"":"alpha(opacity="+parseInt(100*parseFloat(r),10)+")"}else switch(e){case"name":return"opacity";case"extract":return r;case"inject":return r}}},register:function(){9>=f||v.State.isGingerbread||(x.Lists.transformsBase=x.Lists.transformsBase.concat(x.Lists.transforms3D));for(var e=0;en&&(n=1),o=!/(\d)$/i.test(n);break;case"skew":o=!/(deg|\d)$/i.test(n);break;case"rotate":o=!/(deg|\d)$/i.test(n)}return o||(i(r).transformCache[t]="("+n+")"),i(r).transformCache[t]}}}();for(var e=0;e=f||3!==o.split(" ").length||(o+=" 1"),o;case"inject":return 8>=f?4===n.split(" ").length&&(n=n.split(/\s+/).slice(0,3).join(" ")):3===n.split(" ").length&&(n+=" 1"),(8>=f?"rgb":"rgba")+"("+n.replace(/\s+/g,",").replace(/\.(\d)+(?=,)/g,"")+")"}}}()}},Names:{camelCase:function(e){return e.replace(/-(\w)/g,function(e,t){return t.toUpperCase()})},SVGAttribute:function(e){var t="width|height|x|y|cx|cy|r|rx|ry|x1|x2|y1|y2";return(f||v.State.isAndroid&&!v.State.isChrome)&&(t+="|transform"),new RegExp("^("+t+")$","i").test(e)},prefixCheck:function(e){if(v.State.prefixMatches[e])return[v.State.prefixMatches[e],!0];for(var t=["","Webkit","Moz","ms","O"],r=0,a=t.length;a>r;r++){var n;if(n=0===r?e:t[r]+e.replace(/^\w/,function(e){return e.toUpperCase()}),g.isString(v.State.prefixElement.style[n]))return v.State.prefixMatches[e]=n,[n,!0]}return[e,!1]}},Values:{hexToRgb:function(e){var t=/^#?([a-f\d])([a-f\d])([a-f\d])$/i,r=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i,a;return e=e.replace(t,function(e,t,r,a){return t+t+r+r+a+a}),a=r.exec(e),a?[parseInt(a[1],16),parseInt(a[2],16),parseInt(a[3],16)]:[0,0,0]},isCSSNullValue:function(e){return 0==e||/^(none|auto|transparent|(rgba\(0, ?0, ?0, ?0\)))$/i.test(e)},getUnitType:function(e){return/^(rotate|skew)/i.test(e)?"deg":/(^(scale|scaleX|scaleY|scaleZ|alpha|flexGrow|flexHeight|zIndex|fontWeight)$)|((opacity|red|green|blue|alpha)$)/i.test(e)?"":"px"},getDisplayType:function(e){var t=e&&e.tagName.toString().toLowerCase();return/^(b|big|i|small|tt|abbr|acronym|cite|code|dfn|em|kbd|strong|samp|var|a|bdo|br|img|map|object|q|script|span|sub|sup|button|input|label|select|textarea)$/i.test(t)?"inline":/^(li)$/i.test(t)?"list-item":/^(tr)$/i.test(t)?"table-row":/^(table)$/i.test(t)?"table":/^(tbody)$/i.test(t)?"table-row-group":"block"},addClass:function(e,t){e.classList?e.classList.add(t):e.className+=(e.className.length?" ":"")+t},removeClass:function(e,t){e.classList?e.classList.remove(t):e.className=e.className.toString().replace(new RegExp("(^|\\s)"+t.split(" ").join("|")+"(\\s|$)","gi")," ")}},getPropertyValue:function(e,r,n,o){function s(e,r){function n(){u&&x.setPropertyValue(e,"display","none")}var l=0;if(8>=f)l=$.css(e,r);else{var u=!1;if(/^(width|height)$/.test(r)&&0===x.getPropertyValue(e,"display")&&(u=!0,x.setPropertyValue(e,"display",x.Values.getDisplayType(e))),!o){if("height"===r&&"border-box"!==x.getPropertyValue(e,"boxSizing").toString().toLowerCase()){var c=e.offsetHeight-(parseFloat(x.getPropertyValue(e,"borderTopWidth"))||0)-(parseFloat(x.getPropertyValue(e,"borderBottomWidth"))||0)-(parseFloat(x.getPropertyValue(e,"paddingTop"))||0)-(parseFloat(x.getPropertyValue(e,"paddingBottom"))||0);return n(),c}if("width"===r&&"border-box"!==x.getPropertyValue(e,"boxSizing").toString().toLowerCase()){var p=e.offsetWidth-(parseFloat(x.getPropertyValue(e,"borderLeftWidth"))||0)-(parseFloat(x.getPropertyValue(e,"borderRightWidth"))||0)-(parseFloat(x.getPropertyValue(e,"paddingLeft"))||0)-(parseFloat(x.getPropertyValue(e,"paddingRight"))||0);return n(),p}}var d;d=i(e)===a?t.getComputedStyle(e,null):i(e).computedStyle?i(e).computedStyle:i(e).computedStyle=t.getComputedStyle(e,null),"borderColor"===r&&(r="borderTopColor"),l=9===f&&"filter"===r?d.getPropertyValue(r):d[r],(""===l||null===l)&&(l=e.style[r]),n()}if("auto"===l&&/^(top|right|bottom|left)$/i.test(r)){var g=s(e,"position");("fixed"===g||"absolute"===g&&/top|left/i.test(r))&&(l=$(e).position()[r]+"px")}return l}var l;if(x.Hooks.registered[r]){var u=r,c=x.Hooks.getRoot(u);n===a&&(n=x.getPropertyValue(e,x.Names.prefixCheck(c)[0])),x.Normalizations.registered[c]&&(n=x.Normalizations.registered[c]("extract",e,n)),l=x.Hooks.extractValue(u,n)}else if(x.Normalizations.registered[r]){var p,d;p=x.Normalizations.registered[r]("name",e),"transform"!==p&&(d=s(e,x.Names.prefixCheck(p)[0]),x.Values.isCSSNullValue(d)&&x.Hooks.templates[r]&&(d=x.Hooks.templates[r][1])),l=x.Normalizations.registered[r]("extract",e,d)}if(!/^[\d-]/.test(l))if(i(e)&&i(e).isSVG&&x.Names.SVGAttribute(r))if(/^(height|width)$/i.test(r))try{l=e.getBBox()[r]}catch(g){l=0}else l=e.getAttribute(r);else l=s(e,x.Names.prefixCheck(r)[0]);return x.Values.isCSSNullValue(l)&&(l=0),v.debug>=2&&console.log("Get "+r+": "+l),l},setPropertyValue:function(e,r,a,n,o){var s=r;if("scroll"===r)o.container?o.container["scroll"+o.direction]=a:"Left"===o.direction?t.scrollTo(a,o.alternateValue):t.scrollTo(o.alternateValue,a);else if(x.Normalizations.registered[r]&&"transform"===x.Normalizations.registered[r]("name",e))x.Normalizations.registered[r]("inject",e,a),s="transform",a=i(e).transformCache[r];else{if(x.Hooks.registered[r]){var l=r,u=x.Hooks.getRoot(r);n=n||x.getPropertyValue(e,u),a=x.Hooks.injectValue(l,a,n),r=u}if(x.Normalizations.registered[r]&&(a=x.Normalizations.registered[r]("inject",e,a),r=x.Normalizations.registered[r]("name",e)),s=x.Names.prefixCheck(r)[0],8>=f)try{e.style[s]=a}catch(c){v.debug&&console.log("Browser does not support ["+a+"] for ["+s+"]")}else i(e)&&i(e).isSVG&&x.Names.SVGAttribute(r)?e.setAttribute(r,a):e.style[s]=a;v.debug>=2&&console.log("Set "+r+" ("+s+"): "+a)}return[s,a]},flushTransformCache:function(e){function t(t){return parseFloat(x.getPropertyValue(e,t))}var r="";if((f||v.State.isAndroid&&!v.State.isChrome)&&i(e).isSVG){var a={translate:[t("translateX"),t("translateY")],skewX:[t("skewX")],skewY:[t("skewY")],scale:1!==t("scale")?[t("scale"),t("scale")]:[t("scaleX"),t("scaleY")],rotate:[t("rotateZ"),0,0]};$.each(i(e).transformCache,function(e){/^translate/i.test(e)?e="translate":/^scale/i.test(e)?e="scale":/^rotate/i.test(e)&&(e="rotate"),a[e]&&(r+=e+"("+a[e].join(" ")+") ",delete a[e])})}else{var n,o;$.each(i(e).transformCache,function(t){return n=i(e).transformCache[t],"transformPerspective"===t?(o=n,!0):(9===f&&"rotateZ"===t&&(t="rotate"),void(r+=t+n+" "))}),o&&(r="perspective"+o+" "+r)}x.setPropertyValue(e,"transform",r)}};x.Hooks.register(),x.Normalizations.register(),v.hook=function(e,t,r){var n=a;return e=o(e),$.each(e,function(e,o){if(i(o)===a&&v.init(o),r===a)n===a&&(n=v.CSS.getPropertyValue(o,t));else{var s=v.CSS.setPropertyValue(o,t,r);"transform"===s[0]&&v.CSS.flushTransformCache(o),n=s}}),n};var S=function(){function e(){return l?T.promise||null:f}function n(){function e(e){function p(e,t){var r=a,i=a,s=a;return g.isArray(e)?(r=e[0],!g.isArray(e[1])&&/^[\d-]/.test(e[1])||g.isFunction(e[1])||x.RegEx.isHex.test(e[1])?s=e[1]:(g.isString(e[1])&&!x.RegEx.isHex.test(e[1])||g.isArray(e[1]))&&(i=t?e[1]:u(e[1],o.duration),e[2]!==a&&(s=e[2]))):r=e,t||(i=i||o.easing),g.isFunction(r)&&(r=r.call(n,w,P)),g.isFunction(s)&&(s=s.call(n,w,P)),[r||0,i,s]}function f(e,t){var r,a;return a=(t||"0").toString().toLowerCase().replace(/[%A-z]+$/,function(e){return r=e,""}),r||(r=x.Values.getUnitType(e)),[a,r]}function d(){var e={myParent:n.parentNode||r.body,position:x.getPropertyValue(n,"position"),fontSize:x.getPropertyValue(n,"fontSize")},a=e.position===N.lastPosition&&e.myParent===N.lastParent,o=e.fontSize===N.lastFontSize;N.lastParent=e.myParent,N.lastPosition=e.position,N.lastFontSize=e.fontSize;var s=100,l={};if(o&&a)l.emToPx=N.lastEmToPx,l.percentToPxWidth=N.lastPercentToPxWidth,l.percentToPxHeight=N.lastPercentToPxHeight;else{var u=i(n).isSVG?r.createElementNS("http://www.w3.org/2000/svg","rect"):r.createElement("div");v.init(u),e.myParent.appendChild(u),$.each(["overflow","overflowX","overflowY"],function(e,t){v.CSS.setPropertyValue(u,t,"hidden")}),v.CSS.setPropertyValue(u,"position",e.position),v.CSS.setPropertyValue(u,"fontSize",e.fontSize),v.CSS.setPropertyValue(u,"boxSizing","content-box"),$.each(["minWidth","maxWidth","width","minHeight","maxHeight","height"],function(e,t){v.CSS.setPropertyValue(u,t,s+"%")}),v.CSS.setPropertyValue(u,"paddingLeft",s+"em"),l.percentToPxWidth=N.lastPercentToPxWidth=(parseFloat(x.getPropertyValue(u,"width",null,!0))||1)/s,l.percentToPxHeight=N.lastPercentToPxHeight=(parseFloat(x.getPropertyValue(u,"height",null,!0))||1)/s,l.emToPx=N.lastEmToPx=(parseFloat(x.getPropertyValue(u,"paddingLeft"))||1)/s,e.myParent.removeChild(u)}return null===N.remToPx&&(N.remToPx=parseFloat(x.getPropertyValue(r.body,"fontSize"))||16),null===N.vwToPx&&(N.vwToPx=parseFloat(t.innerWidth)/100,N.vhToPx=parseFloat(t.innerHeight)/100),l.remToPx=N.remToPx,l.vwToPx=N.vwToPx,l.vhToPx=N.vhToPx,v.debug>=1&&console.log("Unit ratios: "+JSON.stringify(l),n),l}if(o.begin&&0===w)try{o.begin.call(m,m)}catch(y){setTimeout(function(){throw y},1)}if("scroll"===k){var S=/^x$/i.test(o.axis)?"Left":"Top",V=parseFloat(o.offset)||0,C,A,F;o.container?g.isWrapped(o.container)||g.isNode(o.container)?(o.container=o.container[0]||o.container,C=o.container["scroll"+S],F=C+$(n).position()[S.toLowerCase()]+V):o.container=null:(C=v.State.scrollAnchor[v.State["scrollProperty"+S]],A=v.State.scrollAnchor[v.State["scrollProperty"+("Left"===S?"Top":"Left")]],F=$(n).offset()[S.toLowerCase()]+V),s={scroll:{rootPropertyValue:!1,startValue:C,currentValue:C,endValue:F,unitType:"",easing:o.easing,scrollData:{container:o.container,direction:S,alternateValue:A}},element:n},v.debug&&console.log("tweensContainer (scroll): ",s.scroll,n)}else if("reverse"===k){if(!i(n).tweensContainer)return void $.dequeue(n,o.queue);"none"===i(n).opts.display&&(i(n).opts.display="auto"),"hidden"===i(n).opts.visibility&&(i(n).opts.visibility="visible"),i(n).opts.loop=!1,i(n).opts.begin=null,i(n).opts.complete=null,b.easing||delete o.easing,b.duration||delete o.duration,o=$.extend({},i(n).opts,o);var E=$.extend(!0,{},i(n).tweensContainer);for(var j in E)if("element"!==j){var H=E[j].startValue;E[j].startValue=E[j].currentValue=E[j].endValue,E[j].endValue=H,g.isEmptyObject(b)||(E[j].easing=o.easing),v.debug&&console.log("reverse tweensContainer ("+j+"): "+JSON.stringify(E[j]),n)}s=E}else if("start"===k){var E;i(n).tweensContainer&&i(n).isAnimating===!0&&(E=i(n).tweensContainer),$.each(h,function(e,t){if(RegExp("^"+x.Lists.colors.join("$|^")+"$").test(e)){var r=p(t,!0),n=r[0],o=r[1],i=r[2];if(x.RegEx.isHex.test(n)){for(var s=["Red","Green","Blue"],l=x.Values.hexToRgb(n),u=i?x.Values.hexToRgb(i):a,c=0;cO;O++){var z={delay:F.delay,progress:F.progress};O===R-1&&(z.display=F.display,z.visibility=F.visibility,z.complete=F.complete),S(m,"reverse",z)}return e()}};v=$.extend(S,v),v.animate=S;var P=t.requestAnimationFrame||d;return v.State.isMobile||r.hidden===a||r.addEventListener("visibilitychange",function(){r.hidden?(P=function(e){return setTimeout(function(){e(!0)},16)},c()):P=t.requestAnimationFrame||d}),e.Velocity=v,e!==t&&(e.fn.velocity=S,e.fn.velocity.defaults=v.defaults),$.each(["Down","Up"],function(e,t){v.Redirects["slide"+t]=function(e,r,n,o,i,s){var l=$.extend({},r),u=l.begin,c=l.complete,p={height:"",marginTop:"",marginBottom:"",paddingTop:"",paddingBottom:""},f={};l.display===a&&(l.display="Down"===t?"inline"===v.CSS.Values.getDisplayType(e)?"inline-block":"block":"none"),l.begin=function(){u&&u.call(i,i);for(var r in p){f[r]=e.style[r];var a=v.CSS.getPropertyValue(e,r);p[r]="Down"===t?[a,0]:[0,a]}f.overflow=e.style.overflow,e.style.overflow="hidden"},l.complete=function(){for(var t in f)e.style[t]=f[t];c&&c.call(i,i),s&&s.resolver(i)},v(e,p,l)}}),$.each(["In","Out"],function(e,t){v.Redirects["fade"+t]=function(e,r,n,o,i,s){var l=$.extend({},r),u={opacity:"In"===t?1:0},c=l.complete;l.complete=n!==o-1?l.begin=null:function(){c&&c.call(i,i),s&&s.resolver(i)},l.display===a&&(l.display="In"===t?"auto":"none"),v(this,u,l)}}),v}(window.jQuery||window.Zepto||window,window,document)}); \ No newline at end of file diff --git a/lib/velocity/velocity.ui.min.js b/lib/velocity/velocity.ui.min.js new file mode 100644 index 0000000000..870694530b --- /dev/null +++ b/lib/velocity/velocity.ui.min.js @@ -0,0 +1,2 @@ +/* VelocityJS.org UI Pack (5.0.4). (C) 2014 Julian Shapiro. MIT @license: en.wikipedia.org/wiki/MIT_License. Portions copyright Daniel Eden, Christian Pucci. */ +!function(t){"function"==typeof require&&"object"==typeof exports?module.exports=t():"function"==typeof define&&define.amd?define(["velocity"],t):t()}(function(){return function(t,a,e,r){function n(t,a){var e=[];return t&&a?($.each([t,a],function(t,a){var r=[];$.each(a,function(t,a){for(;a.toString().length<5;)a="0"+a;r.push(a)}),e.push(r.join(""))}),parseFloat(e[0])>parseFloat(e[1])):!1}if(!t.Velocity||!t.Velocity.Utilities)return void(a.console&&console.log("Velocity UI Pack: Velocity must be loaded first. Aborting."));var i=t.Velocity,$=i.Utilities,s=i.version,o={major:1,minor:1,patch:0};if(n(o,s)){var l="Velocity UI Pack: You need to update Velocity (jquery.velocity.js) to a newer version. Visit http://github.com/julianshapiro/velocity.";throw alert(l),new Error(l)}i.RegisterEffect=i.RegisterUI=function(t,a){function e(t,a,e,r){var n=0,s;$.each(t.nodeType?[t]:t,function(t,a){r&&(e+=t*r),s=a.parentNode,$.each(["height","paddingTop","paddingBottom","marginTop","marginBottom"],function(t,e){n+=parseFloat(i.CSS.getPropertyValue(a,e))})}),i.animate(s,{height:("In"===a?"+":"-")+"="+n},{queue:!1,easing:"ease-in-out",duration:e*("In"===a?.6:1)})}return i.Redirects[t]=function(n,s,o,l,c,u){function f(){s.display!==r&&"none"!==s.display||!/Out$/.test(t)||$.each(c.nodeType?[c]:c,function(t,a){i.CSS.setPropertyValue(a,"display","none")}),s.complete&&s.complete.call(c,c),u&&u.resolver(c||n)}var p=o===l-1;a.defaultDuration="function"==typeof a.defaultDuration?a.defaultDuration.call(c,c):parseFloat(a.defaultDuration);for(var d=0;d1&&($.each(a.reverse(),function(t,e){var r=a[t+1];if(r){var n=e.o||e.options,s=r.o||r.options,o=n&&n.sequenceQueue===!1?"begin":"complete",l=s&&s[o],c={};c[o]=function(){var t=r.e||r.elements,a=t.nodeType?[t]:t;l&&l.call(a,a),i(e)},r.o?r.o=$.extend({},s,c):r.options=$.extend({},s,c)}}),a.reverse()),i(a[0])}}(window.jQuery||window.Zepto||window,window,document)}); \ No newline at end of file diff --git a/live2dw/assets/moc/wanko.1024/texture_00.png b/live2dw/assets/moc/wanko.1024/texture_00.png new file mode 100644 index 0000000000000000000000000000000000000000..700840dd3b722ab4b73defbf6a3945ca5a3e9086 GIT binary patch literal 114061 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 XUz&GhOn8DZ3Y!+cGTTl265Rd;Zg+yf literal 0 HcmV?d00001 diff --git a/live2dw/assets/mtn/idle_01.mtn b/live2dw/assets/mtn/idle_01.mtn new file mode 100644 index 0000000000..6b7535cec0 --- /dev/null +++ b/live2dw/assets/mtn/idle_01.mtn @@ -0,0 +1,24 @@ +# Live2D Animator Motion Data +$fps=30 +PARAM_ANGLE_X=-1,-0.991,-0.97,-0.93,-0.88,-0.82,-0.76,-0.69,-0.62,-0.55,-0.47,-0.4,-0.33,-0.26,-0.2,-0.14,-0.09,-0.06,-0.03,-0.007,0,-0.007,-0.025,-0.06,-0.09,-0.14,-0.2,-0.26,-0.32,-0.39,-0.46,-0.53,-0.59,-0.66,-0.72,-0.78,-0.83,-0.88,-0.92,-0.96,-0.98,-0.995,-1,-0.991,-0.97,-0.93,-0.88,-0.82,-0.76,-0.69,-0.62,-0.55,-0.47,-0.4,-0.33,-0.26,-0.2,-0.14,-0.09,-0.06,-0.03,-0.007,0,-0.006,-0.025,-0.05,-0.09,-0.14,-0.19,-0.24,-0.3,-0.36,-0.43,-0.49,-0.56,-0.62,-0.68,-0.74,-0.79,-0.84,-0.89,-0.93,-0.96,-0.98,-0.995,-1,-0.993,-0.974,-0.94,-0.91,-0.86,-0.8,-0.74,-0.68,-0.61,-0.54,-0.46,-0.39,-0.32,-0.26,-0.2,-0.14,-0.09,-0.06,-0.03,-0.007,0,-0.004,-0.016,-0.034,-0.06,-0.09,-0.13,-0.17,-0.21,-0.26,-0.31,-0.36,-0.41,-0.47,-0.52,-0.57,-0.63,-0.68,-0.73,-0.77,-0.82,-0.86,-0.89,-0.92,-0.95,-0.97,-0.987,-0.997,-1,-0.987,-0.95,-0.9,-0.84,-0.76,-0.68,-0.6,-0.51,-0.42,-0.34,-0.26,-0.19,-0.13,-0.07,-0.03,-0.01,0,-0.004,-0.016,-0.034,-0.06,-0.09,-0.13,-0.17,-0.21,-0.26,-0.31,-0.36,-0.42,-0.47,-0.53,-0.58,-0.64,-0.69,-0.74,-0.79,-0.83,-0.87,-0.91,-0.94,-0.97,-0.984,-0.996,-1 +PARAM_ANGLE_Y=14,13.81,13.27,12.45,11.39,10.14,8.73,7.22,5.65,4.03,2.39,0.79,-0.77,-2.25,-3.6,-4.85,-5.91,-6.79,-7.43,-7.85,-8,-7.85,-7.45,-6.79,-5.91,-4.87,-3.68,-2.33,-0.95,0.54,2.05,3.58,5.05,6.52,7.9,9.2,10.37,11.41,12.31,13.03,13.56,13.89,14,13.77,13.1,12.1,10.8,9.27,7.53,5.68,3.75,1.76,-0.25,-2.21,-4.12,-5.94,-7.6,-9.13,-10.44,-11.51,-12.31,-12.82,-13,-12.83,-12.33,-11.55,-10.56,-9.33,-7.97,-6.46,-4.86,-3.18,-1.47,0.28,2.02,3.72,5.38,6.94,8.41,9.75,10.96,11.99,12.84,13.47,13.86,14,13.85,13.43,12.79,11.91,10.85,9.63,8.31,6.86,5.34,3.77,2.23,0.66,-0.86,-2.31,-3.63,-4.85,-5.91,-6.79,-7.43,-7.85,-8,-7.91,-7.65,-7.24,-6.7,-6.02,-5.24,-4.34,-3.37,-2.33,-1.25,-0.09,1.08,2.26,3.45,4.62,5.78,6.92,7.99,9.01,9.96,10.85,11.62,12.32,12.91,13.37,13.71,13.93,14,13.66,12.75,11.36,9.63,7.64,5.46,3.13,0.82,-1.55,-3.78,-5.91,-7.86,-9.58,-10.99,-12.07,-12.76,-13,-12.89,-12.58,-12.07,-11.39,-10.56,-9.59,-8.5,-7.29,-6.01,-4.64,-3.21,-1.75,-0.25,1.25,2.75,4.21,5.64,7.01,8.29,9.5,10.59,11.56,12.39,13.07,13.58,13.89,14 +PARAM_ANGLE_Z=-10,-9.96,-9.85,-9.67,-9.41,-9.1,-8.72,-8.3,-7.82,-7.28,-6.7,-6.1,-5.44,-4.75,-4.03,-3.3,-2.53,-1.75,-0.94,-0.15,0.68,1.5,2.32,3.15,3.94,4.75,5.53,6.3,7.03,7.75,8.44,9.1,9.7,10.28,10.82,11.3,11.72,12.1,12.41,12.67,12.85,12.96,13,12.96,12.85,12.67,12.41,12.1,11.73,11.3,10.82,10.29,9.72,9.1,8.46,7.78,7.06,6.33,5.56,4.79,4.01,3.21,2.41,1.6,0.79,-0.01,-0.79,-1.58,-2.36,-3.1,-3.82,-4.53,-5.21,-5.85,-6.46,-7.02,-7.55,-8.04,-8.47,-8.87,-9.2,-9.48,-9.71,-9.87,-9.97,-10,-9.97,-9.88,-9.74,-9.55,-9.31,-9.01,-8.68,-8.3,-7.88,-7.43,-6.93,-6.42,-5.86,-5.29,-4.68,-4.06,-3.42,-2.75,-2.08,-1.4,-0.7,0,0.71,1.42,2.12,2.83,3.52,4.21,4.89,5.56,6.22,6.85,7.47,8.07,8.63,9.19,9.7,10.19,10.65,11.07,11.46,11.8,12.11,12.37,12.6,12.77,12.9,12.97,13,12.96,12.85,12.67,12.42,12.11,11.75,11.33,10.87,10.35,9.8,9.21,8.58,7.92,7.24,6.53,5.82,5.07,4.3,3.55,2.76,1.99,1.21,0.42,-0.35,-1.11,-1.86,-2.6,-3.32,-4.02,-4.69,-5.33,-5.95,-6.54,-7.09,-7.6,-8.08,-8.5,-8.88,-9.21,-9.49,-9.71,-9.87,-9.97,-10 +PARAM_EYE_L_OPEN=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.82,0.54,0.27,0.08,0,0.03,0.13,0.26,0.42,0.58,0.74,0.87,0.97,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 +PARAM_EYE_R_OPEN=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.82,0.54,0.27,0.08,0,0.03,0.13,0.26,0.42,0.58,0.74,0.87,0.97,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 +PARAM_MOUTH_OPEN_Y=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.82,0.54,0.27,0.08,0,0.03,0.13,0.26,0.42,0.58,0.74,0.87,0.97,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.987,0.95,0.9,0.84,0.76,0.68,0.6,0.51,0.42,0.34,0.26,0.19,0.13,0.07,0.03,0.01,0,0.004,0.016,0.034,0.06,0.09,0.13,0.17,0.21,0.26,0.31,0.36,0.42,0.47,0.53,0.58,0.64,0.69,0.74,0.79,0.83,0.87,0.91,0.94,0.97,0.984,0.996,1 +PARAM_BODY_ANGLE_X=-2,-1.993,-1.973,-1.94,-1.9,-1.84,-1.78,-1.7,-1.62,-1.53,-1.43,-1.32,-1.21,-1.09,-0.96,-0.83,-0.7,-0.56,-0.42,-0.29,-0.14,0,0.14,0.29,0.42,0.56,0.7,0.83,0.96,1.09,1.21,1.32,1.43,1.53,1.62,1.7,1.78,1.84,1.9,1.94,1.97,1.993,2,1.993,1.973,1.94,1.9,1.84,1.78,1.7,1.62,1.53,1.43,1.32,1.21,1.09,0.97,0.84,0.71,0.57,0.44,0.3,0.16,0.02,-0.12,-0.26,-0.4,-0.54,-0.67,-0.8,-0.93,-1.05,-1.17,-1.28,-1.38,-1.48,-1.57,-1.66,-1.73,-1.8,-1.86,-1.91,-1.95,-1.98,-1.994,-2,-1.992,-1.97,-1.93,-1.88,-1.82,-1.74,-1.65,-1.56,-1.45,-1.33,-1.2,-1.07,-0.92,-0.77,-0.61,-0.45,-0.28,-0.11,0.06,0.24,0.43,0.61,0.79,0.98,1.16,1.35,1.53,1.71,1.88,2.06,2.23,2.4,2.56,2.71,2.86,3.01,3.14,3.27,3.39,3.5,3.6,3.69,3.77,3.84,3.89,3.94,3.97,3.99,4,3.99,3.96,3.91,3.85,3.77,3.67,3.56,3.44,3.31,3.17,3.01,2.85,2.68,2.5,2.31,2.13,1.93,1.73,1.53,1.33,1.13,0.93,0.72,0.52,0.32,0.12,-0.07,-0.26,-0.44,-0.61,-0.78,-0.94,-1.1,-1.24,-1.37,-1.5,-1.61,-1.71,-1.79,-1.87,-1.92,-1.97,-1.99,-2 +PARAM_BODY_ANGLE_Z=-5,-4.981,-4.93,-4.84,-4.72,-4.57,-4.39,-4.19,-3.96,-3.7,-3.42,-3.13,-2.82,-2.49,-2.15,-1.8,-1.43,-1.05,-0.67,-0.29,0.11,0.5,0.89,1.29,1.67,2.05,2.43,2.8,3.15,3.49,3.82,4.13,4.42,4.7,4.96,5.19,5.39,5.57,5.72,5.84,5.93,5.98,6,5.981,5.93,5.84,5.72,5.57,5.39,5.19,4.96,4.71,4.43,4.14,3.83,3.5,3.16,2.81,2.44,2.08,1.7,1.32,0.93,0.55,0.16,-0.22,-0.6,-0.97,-1.34,-1.7,-2.04,-2.39,-2.71,-3.01,-3.31,-3.58,-3.83,-4.06,-4.27,-4.46,-4.62,-4.75,-4.86,-4.94,-4.98,-5,-4.986,-4.94,-4.88,-4.78,-4.67,-4.53,-4.37,-4.19,-3.98,-3.77,-3.53,-3.29,-3.02,-2.75,-2.46,-2.16,-1.85,-1.53,-1.21,-0.89,-0.55,-0.22,0.12,0.46,0.8,1.14,1.47,1.79,2.12,2.44,2.76,3.06,3.35,3.64,3.91,4.18,4.42,4.66,4.87,5.08,5.26,5.43,5.57,5.7,5.81,5.89,5.95,5.99,6,5.981,5.93,5.84,5.72,5.58,5.4,5.2,4.98,4.73,4.47,4.19,3.89,3.57,3.25,2.91,2.56,2.21,1.84,1.48,1.1,0.73,0.36,-0.01,-0.38,-0.75,-1.11,-1.46,-1.8,-2.14,-2.46,-2.77,-3.07,-3.35,-3.61,-3.85,-4.08,-4.28,-4.47,-4.62,-4.75,-4.86,-4.94,-4.98,-5 +PARAM_BODY_ANGLE_Y=0 +PARAM_BREATH=0 +PARAM_BOWL_LID=50 +PARAM_YUGE_01=0 +PARAM_YUGE_02=0 +PARAM_EFFECT=0 +PARAM_EAR_L=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.06,0.21,0.41,0.61,0.76,0.82,0.54,0.1,-0.31,-0.6,-0.72,-0.55,-0.28,-0.02,0.16,0.23,0.06,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.15,0.38,0.6,0.76,0.82,0.54,0.1,-0.31,-0.6,-0.72,-0.55,-0.28,-0.02,0.16,0.23,0.17,0.06,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_EAR_R=0 +PARAM_HAND_L=0 +PARAM_HAND_R=0 +PARAM_SWING=0 +PARAM_BOWL_SWING=0 +PARAM_FACE_01=0 +PARAM_BASE_X=0 \ No newline at end of file diff --git a/live2dw/assets/mtn/idle_02.mtn b/live2dw/assets/mtn/idle_02.mtn new file mode 100644 index 0000000000..328d7a5680 --- /dev/null +++ b/live2dw/assets/mtn/idle_02.mtn @@ -0,0 +1,24 @@ +# Live2D Animator Motion Data +$fps=30 +PARAM_ANGLE_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.01,-0.04,-0.09,-0.15,-0.22,-0.31,-0.41,-0.52,-0.63,-0.75,-0.87,-1,-1.13,-1.25,-1.37,-1.48,-1.59,-1.69,-1.78,-1.85,-1.91,-1.96,-1.99,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-1.995,-1.979,-1.95,-1.92,-1.88,-1.83,-1.77,-1.7,-1.64,-1.56,-1.48,-1.4,-1.31,-1.23,-1.14,-1.05,-0.95,-0.86,-0.77,-0.69,-0.6,-0.52,-0.44,-0.36,-0.3,-0.23,-0.17,-0.12,-0.08,-0.05,-0.02,-0.005,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_ANGLE_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.08,0.29,0.64,1.12,1.68,2.34,3.09,3.88,4.75,5.65,6.56,7.5,8.44,9.35,10.25,11.12,11.91,12.66,13.32,13.88,14.36,14.71,14.92,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,14.96,14.84,14.65,14.39,14.07,13.7,13.26,12.79,12.27,11.71,11.11,10.49,9.85,9.19,8.52,7.84,7.16,6.48,5.81,5.15,4.51,3.89,3.29,2.73,2.21,1.74,1.3,0.93,0.61,0.35,0.16,0.04,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2.66,7.03,10.02,11,10.19,8.22,5.82,3.48,1.6,0.4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_ANGLE_Z=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.12,0.45,0.99,1.71,2.57,3.59,4.74,5.95,7.28,8.66,10.05,11.5,12.95,14.34,15.72,17.05,18.26,19.41,20.43,21.29,22.01,22.55,22.88,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,22.92,22.68,22.3,21.79,21.15,20.39,19.53,18.57,17.53,16.42,15.22,13.98,12.7,11.38,10.04,8.68,7.32,5.96,4.62,3.3,2.02,0.78,-0.42,-1.53,-2.57,-3.53,-4.39,-5.15,-5.79,-6.3,-6.68,-6.92,-7,-6.94,-6.76,-6.48,-6.11,-5.68,-5.19,-4.65,-4.08,-3.5,-2.92,-2.35,-1.81,-1.32,-0.89,-0.52,-0.24,-0.06,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_EYE_L_OPEN=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.74,0.26,0,0.08,0.26,0.5,0.74,0.92,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.74,0.26,0,0.08,0.26,0.5,0.74,0.92,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 +PARAM_EYE_R_OPEN=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.74,0.26,0,0.08,0.26,0.5,0.74,0.92,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.74,0.26,0,0.08,0.26,0.5,0.74,0.92,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 +PARAM_MOUTH_OPEN_Y=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.98,0.93,0.85,0.75,0.63,0.51,0.4,0.29,0.19,0.11,0.05,0.01,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.07,0.25,0.47,0.68,0.85,0.96,1,0.98,0.93,0.84,0.74,0.62,0.5,0.38,0.26,0.16,0.07,0.02,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.013,0.05,0.1,0.18,0.26,0.35,0.45,0.55,0.65,0.74,0.82,0.9,0.95,0.99,1 +PARAM_BODY_ANGLE_X=0 +PARAM_BODY_ANGLE_Z=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.05,0.2,0.43,0.74,1.12,1.56,2.06,2.59,3.16,3.77,4.37,5,5.63,6.23,6.84,7.41,7.94,8.44,8.88,9.26,9.57,9.8,9.95,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,9.989,9.96,9.9,9.82,9.73,9.62,9.49,9.34,9.18,9,8.8,8.6,8.38,8.15,7.92,7.66,7.41,7.14,6.87,6.59,6.31,6.02,5.73,5.44,5.15,4.85,4.56,4.27,3.98,3.69,3.41,3.13,2.86,2.59,2.34,2.08,1.85,1.62,1.4,1.2,1,0.82,0.66,0.51,0.38,0.27,0.18,0.1,0.04,0.01,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_BODY_ANGLE_Y=0,0.07,0.25,0.51,0.81,1.12,1.41,1.65,1.84,1.96,2,1.97,1.91,1.8,1.68,1.53,1.37,1.2,1.02,0.85,0.68,0.53,0.38,0.25,0.15,0.07,0.02,0,0.28,1,2.04,3.23,4.47,5.62,6.61,7.36,7.83,8,7.85,7.45,6.87,6.15,5.36,4.53,3.68,2.87,2.1,1.41,0.84,0.39,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.03,0.1,0.21,0.36,0.53,0.72,0.92,1.14,1.36,1.58,1.8,2.01,2.22,2.4,2.57,2.72,2.83,2.92,2.98,3,2.94,2.78,2.54,2.24,1.9,1.54,1.19,0.87,0.58,0.34,0.15,0.04,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.013,0.05,0.11,0.19,0.29,0.4,0.52,0.65,0.79,0.93,1.07,1.21,1.35,1.48,1.6,1.71,1.81,1.89,1.95,1.99,2,1.94,1.77,1.56,1.3,1.04,0.77,0.53,0.32,0.15,0.04,0,1.94,5.11,7.29,8,7.26,5.48,3.29,1.16,-0.55,-1.63,-2,-1.46,-0.61,0.2,0.77,1,0.74,0.26,0,0,0,0,0,0,0,0,0,0,0,0,0.1,0.34,0.67,1.05,1.45,1.84,2.21,2.52,2.78,2.94,3,2.96,2.86,2.69,2.47,2.22,1.94,1.65,1.35,1.06,0.78,0.53,0.31,0.14,0.04,0 +PARAM_BREATH=0 +PARAM_BOWL_LID=50 +PARAM_YUGE_01=0 +PARAM_YUGE_02=0 +PARAM_EFFECT=0 +PARAM_EAR_L=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.14,-0.36,-0.52,-0.57,-0.49,-0.28,-0.01,0.26,0.47,0.55,0.49,0.35,0.16,-0.04,-0.18,-0.24,-0.2,-0.13,-0.06,-0.02,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_EAR_R=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.12,0.32,0.46,0.5,0.41,0.21,-0.07,-0.35,-0.55,-0.64,-0.58,-0.43,-0.23,-0.03,0.12,0.18,0.15,0.1,0.05,0.01,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_HAND_L=0 +PARAM_HAND_R=0 +PARAM_SWING=0 +PARAM_BOWL_SWING=0 +PARAM_FACE_01=0 +PARAM_BASE_X=0 \ No newline at end of file diff --git a/live2dw/assets/mtn/idle_03.mtn b/live2dw/assets/mtn/idle_03.mtn new file mode 100644 index 0000000000..33a2daf9fb --- /dev/null +++ b/live2dw/assets/mtn/idle_03.mtn @@ -0,0 +1,24 @@ +# Live2D Animator Motion Data +$fps=30 +PARAM_ANGLE_X=0,-0.04,-0.15,-0.33,-0.57,-0.87,-1.22,-1.62,-2.07,-2.54,-3.06,-3.61,-4.18,-4.77,-5.37,-5.99,-6.61,-7.24,-7.85,-8.47,-9.06,-9.65,-10.21,-10.74,-11.26,-11.74,-12.18,-12.58,-12.94,-13.25,-13.51,-13.72,-13.87,-13.97,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-14,-13.95,-13.82,-13.61,-13.32,-12.96,-12.55,-12.08,-11.55,-11,-10.39,-9.76,-9.11,-8.45,-7.77,-7.08,-6.41,-5.73,-5.07,-4.43,-3.81,-3.22,-2.67,-2.15,-1.67,-1.25,-0.89,-0.58,-0.33,-0.15,-0.04,0,0,0,0,0,0,0,0,0,0 +PARAM_ANGLE_Y=0,0.04,0.17,0.37,0.65,0.99,1.39,1.85,2.36,2.91,3.5,4.12,4.78,5.45,6.14,6.84,7.55,8.27,8.97,9.68,10.36,11.03,11.67,12.28,12.87,13.41,13.92,14.38,14.78,15.14,15.44,15.68,15.86,15.96,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,15.95,15.8,15.55,15.22,14.82,14.34,13.8,13.2,12.57,11.88,11.16,10.42,9.65,8.88,8.09,7.32,6.55,5.8,5.06,4.35,3.68,3.05,2.46,1.91,1.43,1.01,0.66,0.38,0.17,0.04,0,0,0,0,0,0,0,0,0,0 +PARAM_ANGLE_Z=0 +PARAM_EYE_L_OPEN=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.97,0.88,0.75,0.6,0.44,0.3,0.17,0.08,0.02,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.03,0.12,0.25,0.4,0.56,0.7,0.83,0.92,0.98,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.74,0.26,0,0.08,0.26,0.5,0.74,0.92,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 +PARAM_EYE_R_OPEN=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.97,0.88,0.75,0.6,0.44,0.3,0.17,0.08,0.02,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.03,0.12,0.25,0.4,0.56,0.7,0.83,0.92,0.98,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.74,0.26,0,0.08,0.26,0.5,0.74,0.92,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 +PARAM_MOUTH_OPEN_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.021,0.07,0.15,0.24,0.34,0.42,0.5,0.55,0.59,0.6,0.588,0.56,0.51,0.44,0.37,0.3,0.23,0.16,0.09,0.04,0.01,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_BODY_ANGLE_X=0,-0.02,-0.09,-0.21,-0.36,-0.56,-0.78,-1.04,-1.33,-1.64,-1.97,-2.32,-2.69,-3.07,-3.45,-3.85,-4.25,-4.65,-5.05,-5.44,-5.83,-6.2,-6.56,-6.91,-7.24,-7.54,-7.83,-8.09,-8.32,-8.52,-8.68,-8.82,-8.92,-8.98,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-8.97,-8.89,-8.75,-8.56,-8.33,-8.07,-7.76,-7.43,-7.07,-6.68,-6.28,-5.86,-5.43,-4.99,-4.55,-4.12,-3.69,-3.26,-2.85,-2.45,-2.07,-1.71,-1.38,-1.08,-0.81,-0.57,-0.37,-0.21,-0.1,-0.02,0,0,0,0,0,0,0,0,0,0 +PARAM_BODY_ANGLE_Z=0,0,0,0,0,0,0,0,0,0,0,0,-0.013,-0.05,-0.11,-0.18,-0.27,-0.37,-0.48,-0.6,-0.73,-0.85,-0.98,-1.11,-1.24,-1.36,-1.48,-1.59,-1.69,-1.77,-1.85,-1.91,-1.96,-1.99,-2,-2,-1.999,-1.997,-1.994,-1.991,-1.987,-1.982,-1.977,-1.971,-1.964,-1.957,-1.949,-1.941,-1.931,-1.922,-1.911,-1.901,-1.889,-1.877,-1.865,-1.852,-1.838,-1.824,-1.81,-1.795,-1.78,-1.764,-1.748,-1.731,-1.714,-1.696,-1.679,-1.66,-1.642,-1.623,-1.604,-1.584,-1.564,-1.54,-1.523,-1.5,-1.48,-1.46,-1.44,-1.42,-1.39,-1.37,-1.35,-1.33,-1.31,-1.28,-1.26,-1.24,-1.21,-1.19,-1.17,-1.14,-1.12,-1.1,-1.07,-1.05,-1.02,-1,-0.98,-0.95,-0.93,-0.9,-0.88,-0.86,-0.83,-0.81,-0.79,-0.76,-0.74,-0.72,-0.69,-0.67,-0.65,-0.63,-0.61,-0.58,-0.56,-0.54,-0.52,-0.5,-0.48,-0.456,-0.44,-0.416,-0.396,-0.377,-0.358,-0.34,-0.321,-0.304,-0.286,-0.269,-0.252,-0.236,-0.22,-0.205,-0.19,-0.176,-0.162,-0.148,-0.135,-0.123,-0.111,-0.099,-0.089,-0.078,-0.069,-0.059,-0.051,-0.043,-0.036,-0.029,-0.023,-0.018,-0.013,-0.009,-0.006,-0.003,-0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_BODY_ANGLE_Y=0,0.13,0.45,0.89,1.4,1.93,2.45,2.95,3.37,3.7,3.92,4,3.9,3.63,3.23,2.73,2.17,1.57,0.99,0.45,-0.04,-0.44,-0.75,-0.93,-1,-0.64,-0.07,0.46,0.85,1,0.93,0.75,0.53,0.32,0.15,0.04,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.53,1.47,2,1.52,0.72,0.18,0,0.24,0.64,0.91,1,0.76,0.36,0.09,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.02,-0.07,-0.16,-0.26,-0.38,-0.5,-0.62,-0.74,-0.84,-0.93,-0.98,-1,-0.94,-0.78,-0.54,-0.24,0.1,0.46,0.81,1.13,1.42,1.66,1.85,1.96,2,1.999,1.993,1.983,1.965,1.94,1.9,1.85,1.79,1.72,1.63,1.52,1.4,1.25,1.09,0.91,0.72,0.5,0.26,0,-0.38,-0.81,-1.28,-1.77,-2.26,-2.73,-3.19,-3.61,-4,-4.34,-4.61,-4.82,-4.95,-5,-4.94,-4.77,-4.51,-4.19,-3.82,-3.42,-2.99,-2.56,-2.12,-1.71,-1.31,-0.95,-0.63,-0.37,-0.17,-0.04,0,0,0,0,0,0,0,0,0,0 +PARAM_BREATH=0 +PARAM_BOWL_LID=50 +PARAM_YUGE_01=0 +PARAM_YUGE_02=0 +PARAM_EFFECT=0 +PARAM_EAR_L=0 +PARAM_EAR_R=0 +PARAM_HAND_L=0 +PARAM_HAND_R=0 +PARAM_SWING=0 +PARAM_BOWL_SWING=0 +PARAM_FACE_01=0 +PARAM_BASE_X=0 \ No newline at end of file diff --git a/live2dw/assets/mtn/idle_04.mtn b/live2dw/assets/mtn/idle_04.mtn new file mode 100644 index 0000000000..dadfdf120b --- /dev/null +++ b/live2dw/assets/mtn/idle_04.mtn @@ -0,0 +1,24 @@ +# Live2D Animator Motion Data +$fps=30 +PARAM_ANGLE_X=0,0,0,0,0,-0.001,-0.002,0,-0.004,-0.005,-0.006,-0.007,-0.008,-0.009,-0.011,-0.012,-0.014,-0.016,-0.018,-0.02,-0.022,-0.024,-0.026,-0.028,-0.031,-0.033,-0.036,-0.039,-0.042,-0.044,-0.047,-0.05,-0.054,-0.057,-0.06,-0.063,-0.067,-0.07,-0.074,-0.078,-0.082,-0.085,-0.089,-0.093,-0.097,-0.102,-0.106,-0.11,-0.114,-0.119,-0.123,-0.128,-0.132,-0.137,-0.142,-0.146,-0.151,-0.156,-0.161,-0.166,-0.171,-0.176,-0.182,-0.187,-0.192,-0.197,-0.203,-0.208,-0.214,-0.219,-0.225,-0.231,-0.236,-0.242,-0.248,-0.253,-0.259,-0.265,-0.271,-0.277,-0.283,-0.289,-0.295,-0.301,-0.307,-0.313,-0.319,-0.326,-0.332,-0.338,-0.344,-0.351,-0.357,-0.363,-0.37,-0.376,-0.383,-0.389,-0.395,-0.402,-0.408,-0.415,-0.421,-0.428,-0.434,-0.441,-0.447,-0.454,-0.461,-0.467,-0.474,-0.48,-0.487,-0.493,-0.5,-0.507,-0.513,-0.52,-0.526,-0.533,-0.539,-0.546,-0.553,-0.559,-0.566,-0.572,-0.579,-0.585,-0.592,-0.598,-0.605,-0.611,-0.617,-0.624,-0.63,-0.637,-0.643,-0.649,-0.656,-0.662,-0.668,-0.674,-0.681,-0.687,-0.693,-0.699,-0.705,-0.711,-0.717,-0.723,-0.729,-0.735,-0.741,-0.747,-0.752,-0.758,-0.764,-0.769,-0.775,-0.781,-0.786,-0.792,-0.797,-0.803,-0.808,-0.813,-0.818,-0.824,-0.829,-0.834,-0.839,-0.844,-0.849,-0.854,-0.858,-0.863,-0.868,-0.872,-0.877,-0.881,-0.886,-0.89,-0.894,-0.898,-0.903,-0.907,-0.911,-0.915,-0.918,-0.922,-0.926,-0.93,-0.933,-0.937,-0.94,-0.943,-0.946,-0.95,-0.953,-0.956,-0.958,-0.961,-0.964,-0.967,-0.969,-0.972,-0.974,-0.976,-0.978,-0.98,-0.982,-0.984,-0.986,-0.988,-0.989,-0.991,-0.992,-0.993,-0.994,-0.995,-0.996,-0.997,-1,-0.999,-0.999,-1,-1,-1,-1,-1,-0.998,-0.996,-0.993,-0.989,-0.984,-0.979,-0.973,-0.966,-0.958,-0.95,-0.941,-0.931,-0.921,-0.91,-0.898,-0.887,-0.874,-0.861,-0.847,-0.833,-0.819,-0.804,-0.789,-0.773,-0.758,-0.741,-0.725,-0.708,-0.691,-0.674,-0.656,-0.639,-0.621,-0.603,-0.585,-0.566,-0.548,-0.53,-0.511,-0.493,-0.475,-0.456,-0.438,-0.42,-0.402,-0.384,-0.366,-0.349,-0.331,-0.314,-0.297,-0.281,-0.264,-0.248,-0.232,-0.217,-0.201,-0.187,-0.172,-0.159,-0.145,-0.132,-0.12,-0.108,-0.096,-0.085,-0.075,-0.065,-0.056,-0.047,-0.04,-0.032,-0.026,-0.02,-0.015,-0.01,-0.007,-0.004,-0.002,0,0 +PARAM_ANGLE_Y=0,0.04,0.15,0.34,0.58,0.89,1.25,1.66,2.1,2.59,3.11,3.66,4.22,4.81,5.4,6,6.6,7.19,7.78,8.34,8.89,9.41,9.9,10.34,10.75,11.11,11.42,11.66,11.85,11.96,12,11.986,11.95,11.88,11.79,11.67,11.53,11.37,11.19,11,10.78,10.54,10.29,10.02,9.73,9.44,9.13,8.81,8.47,8.14,7.79,7.43,7.07,6.71,6.34,5.96,5.58,5.21,4.83,4.45,4.08,3.7,3.34,2.98,2.62,2.27,1.94,1.61,1.28,0.97,0.68,0.39,0.13,-0.13,-0.37,-0.58,-0.78,-0.96,-1.12,-1.26,-1.38,-1.47,-1.53,-1.58,-1.589,-1.55,-1.45,-1.27,-1.04,-0.75,-0.41,-0.02,0.42,0.88,1.38,1.91,2.47,3.04,3.63,4.22,4.83,5.43,6.03,6.63,7.21,7.77,8.32,8.84,9.34,9.8,10.23,10.62,10.97,11.27,11.52,11.73,11.88,11.97,12,11.99,11.96,11.91,11.84,11.76,11.66,11.54,11.41,11.25,11.09,10.92,10.73,10.53,10.31,10.09,9.86,9.62,9.36,9.1,8.84,8.56,8.28,7.99,7.7,7.4,7.1,6.79,6.49,6.18,5.87,5.56,5.25,4.93,4.62,4.31,4,3.69,3.39,3.1,2.8,2.51,2.22,1.95,1.67,1.41,1.15,0.9,0.65,0.42,0.2,-0.01,-0.21,-0.4,-0.58,-0.75,-0.9,-1.04,-1.16,-1.27,-1.37,-1.45,-1.51,-1.55,-1.58,-1.589,-1.55,-1.43,-1.24,-0.98,-0.67,-0.29,0.13,0.59,1.09,1.64,2.2,2.8,3.41,4.04,4.68,5.32,5.98,6.63,7.28,7.9,8.53,9.14,9.72,10.29,10.81,11.31,11.77,12.19,12.57,12.9,13.18,13.41,13.57,13.68,13.71,12.45,9.14,4.35,-1.12,-6.79,-12.09,-16.61,-20.06,-22.23,-23,-21.87,-19.13,-15.79,-12.53,-9.91,-8.25,-7.68,-7.79,-8.08,-8.5,-9,-9.52,-10.02,-10.44,-10.73,-10.84,-10.83,-10.81,-10.77,-10.72,-10.66,-10.59,-10.5,-10.4,-10.29,-10.17,-10.03,-9.89,-9.74,-9.58,-9.41,-9.24,-9.05,-8.86,-8.66,-8.46,-8.24,-8.03,-7.8,-7.58,-7.35,-7.12,-6.88,-6.64,-6.4,-6.15,-5.91,-5.66,-5.42,-5.17,-4.93,-4.68,-4.44,-4.2,-3.96,-3.72,-3.49,-3.26,-3.03,-2.81,-2.59,-2.38,-2.18,-1.98,-1.79,-1.6,-1.43,-1.26,-1.1,-0.95,-0.8,-0.67,-0.55,-0.44,-0.34,-0.25,-0.18,-0.11,-0.07,-0.03,-0.01,0 +PARAM_ANGLE_Z=-8,-7.95,-7.79,-7.52,-7.17,-6.74,-6.23,-5.65,-5.02,-4.33,-3.6,-2.82,-2.02,-1.18,-0.35,0.5,1.35,2.18,3.02,3.82,4.6,5.33,6.02,6.65,7.23,7.74,8.17,8.52,8.79,8.95,9,8.98,8.92,8.82,8.69,8.52,8.31,8.08,7.81,7.52,7.2,6.85,6.48,6.08,5.66,5.23,4.77,4.3,3.81,3.32,2.8,2.28,1.75,1.21,0.66,0.11,-0.44,-1,-1.56,-2.11,-2.66,-3.21,-3.75,-4.28,-4.8,-5.32,-5.81,-6.3,-6.77,-7.23,-7.66,-8.08,-8.48,-8.85,-9.2,-9.52,-9.81,-10.08,-10.31,-10.52,-10.69,-10.82,-10.92,-10.98,-11,-10.95,-10.79,-10.54,-10.19,-9.76,-9.26,-8.69,-8.05,-7.37,-6.62,-5.85,-5.03,-4.19,-3.32,-2.45,-1.56,-0.66,0.21,1.1,1.95,2.78,3.59,4.35,5.08,5.76,6.4,6.97,7.48,7.92,8.3,8.6,8.82,8.95,9,8.985,8.94,8.87,8.77,8.64,8.5,8.32,8.12,7.9,7.66,7.41,7.13,6.83,6.52,6.19,5.85,5.49,5.12,4.73,4.34,3.94,3.52,3.1,2.67,2.23,1.79,1.34,0.89,0.43,-0.02,-0.48,-0.94,-1.4,-1.86,-2.31,-2.77,-3.22,-3.67,-4.1,-4.54,-4.97,-5.39,-5.8,-6.2,-6.59,-6.97,-7.34,-7.7,-8.04,-8.36,-8.68,-8.98,-9.26,-9.52,-9.76,-9.98,-10.19,-10.37,-10.54,-10.67,-10.79,-10.88,-10.95,-10.99,-11,-10.96,-10.83,-10.63,-10.36,-10.04,-9.64,-9.2,-8.72,-8.19,-7.63,-7.03,-6.42,-5.77,-5.11,-4.45,-3.77,-3.08,-2.4,-1.73,-1.07,-0.42,0.22,0.83,1.42,1.97,2.49,2.97,3.41,3.81,4.16,4.45,4.68,4.86,4.96,5,4.52,3.26,1.43,-0.65,-2.82,-4.84,-6.56,-7.88,-8.71,-9,-8.41,-6.98,-5.23,-3.53,-2.16,-1.29,-1,-1.21,-1.76,-2.56,-3.51,-4.49,-5.44,-6.24,-6.79,-7,-6.97,-6.89,-6.75,-6.56,-6.33,-6.07,-5.76,-5.43,-5.07,-4.68,-4.28,-3.86,-3.43,-2.99,-2.55,-2.12,-1.69,-1.26,-0.85,-0.45,-0.07,0.29,0.62,0.92,1.19,1.43,1.63,1.79,1.9,1.98,2,1.97,1.89,1.77,1.6,1.4,1.15,0.88,0.57,0.25,-0.11,-0.48,-0.87,-1.27,-1.68,-2.1,-2.52,-2.95,-3.37,-3.8,-4.21,-4.61,-5.01,-5.39,-5.76,-6.11,-6.43,-6.73,-7.01,-7.26,-7.47,-7.66,-7.8,-7.91,-7.98,-8 +PARAM_EYE_L_OPEN=0 +PARAM_EYE_R_OPEN=0 +PARAM_MOUTH_OPEN_Y=0,0.001,0.005,0.012,0.02,0.031,0.044,0.058,0.074,0.091,0.109,0.128,0.148,0.17,0.19,0.21,0.231,0.25,0.27,0.292,0.311,0.329,0.346,0.362,0.376,0.389,0.4,0.408,0.415,0.419,0.42,0.42,0.418,0.416,0.413,0.41,0.406,0.401,0.395,0.389,0.382,0.375,0.367,0.359,0.35,0.341,0.331,0.321,0.311,0.301,0.29,0.279,0.268,0.256,0.245,0.233,0.222,0.21,0.198,0.187,0.175,0.164,0.152,0.141,0.13,0.119,0.109,0.099,0.089,0.079,0.07,0.061,0.053,0.045,0.038,0.031,0.025,0.019,0.014,0.01,0.007,0.004,0.002,0,0,0.001,0.004,0.01,0.017,0.026,0.037,0.049,0.062,0.076,0.092,0.108,0.125,0.143,0.161,0.18,0.198,0.217,0.235,0.254,0.272,0.289,0.306,0.322,0.338,0.352,0.365,0.377,0.388,0.397,0.405,0.412,0.416,0.419,0.42,0.42,0.419,0.417,0.415,0.413,0.409,0.406,0.402,0.397,0.392,0.387,0.381,0.374,0.368,0.361,0.354,0.346,0.339,0.33,0.322,0.314,0.305,0.296,0.287,0.278,0.269,0.259,0.25,0.24,0.231,0.221,0.211,0.202,0.192,0.182,0.173,0.163,0.154,0.145,0.136,0.127,0.118,0.109,0.101,0.093,0.085,0.077,0.069,0.062,0.055,0.049,0.042,0.037,0.031,0.026,0.021,0.017,0.013,0.01,0.007,0.004,0.003,0.001,0,0,0,0.004,0.008,0.014,0.021,0.03,0.039,0.05,0.061,0.074,0.087,0.1,0.114,0.129,0.143,0.158,0.173,0.188,0.203,0.217,0.232,0.245,0.259,0.272,0.284,0.295,0.306,0.315,0.324,0.332,0.338,0.343,0.347,0.349,0.35,0.349,0.346,0.34,0.333,0.324,0.314,0.302,0.289,0.274,0.259,0.243,0.227,0.21,0.192,0.175,0.158,0.14,0.123,0.107,0.091,0.076,0.061,0.048,0.036,0.026,0.017,0.01,0.004,0.001,0,0.001,0.005,0.012,0.02,0.031,0.043,0.057,0.072,0.088,0.105,0.123,0.142,0.161,0.179,0.198,0.217,0.235,0.252,0.268,0.283,0.297,0.309,0.32,0.328,0.335,0.339,0.34,0.34,0.336,0.332,0.326,0.319,0.311,0.302,0.291,0.28,0.268,0.256,0.243,0.229,0.215,0.201,0.186,0.172,0.157,0.143,0.129,0.115,0.102,0.089,0.076,0.064,0.053,0.043,0.034,0.025,0.018,0.012,0.007,0.003,0.001,0 +PARAM_BODY_ANGLE_X=0 +PARAM_BODY_ANGLE_Z=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.21,-0.75,-1.53,-2.42,-3.35,-4.22,-4.95,-5.52,-5.87,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-5.996,-5.984,-5.964,-5.94,-5.9,-5.86,-5.81,-5.76,-5.7,-5.63,-5.56,-5.48,-5.39,-5.3,-5.21,-5.11,-5.01,-4.9,-4.79,-4.68,-4.56,-4.45,-4.32,-4.2,-4.07,-3.94,-3.81,-3.68,-3.54,-3.41,-3.27,-3.14,-3,-2.86,-2.73,-2.59,-2.46,-2.32,-2.19,-2.06,-1.93,-1.8,-1.68,-1.55,-1.44,-1.32,-1.21,-1.1,-0.99,-0.89,-0.79,-0.7,-0.61,-0.52,-0.44,-0.37,-0.3,-0.24,-0.19,-0.14,-0.1,-0.06,-0.04,-0.016,-0.004,0 +PARAM_BODY_ANGLE_Y=0 +PARAM_BREATH=0,0.002,0.008,0.018,0.031,0.048,0.067,0.09,0.11,0.14,0.17,0.2,0.23,0.26,0.29,0.32,0.35,0.38,0.41,0.44,0.47,0.5,0.53,0.55,0.57,0.592,0.609,0.622,0.632,0.638,0.64,0.639,0.637,0.634,0.63,0.624,0.618,0.611,0.602,0.593,0.582,0.571,0.559,0.547,0.533,0.519,0.505,0.49,0.474,0.458,0.442,0.425,0.408,0.391,0.373,0.356,0.338,0.32,0.302,0.284,0.267,0.249,0.232,0.215,0.198,0.182,0.166,0.15,0.135,0.121,0.107,0.093,0.081,0.069,0.058,0.047,0.038,0.029,0.022,0.016,0.01,0.006,0.003,0.001,0,0.002,0.008,0.017,0.03,0.045,0.064,0.08,0.11,0.13,0.16,0.19,0.22,0.25,0.28,0.31,0.34,0.38,0.41,0.44,0.47,0.5,0.53,0.56,0.59,0.61,0.63,0.66,0.675,0.691,0.704,0.715,0.723,0.728,0.73,0.729,0.728,0.725,0.722,0.717,0.712,0.705,0.698,0.69,0.681,0.672,0.662,0.651,0.639,0.628,0.615,0.602,0.588,0.574,0.56,0.545,0.53,0.515,0.499,0.483,0.467,0.45,0.434,0.417,0.401,0.384,0.367,0.35,0.333,0.317,0.3,0.284,0.268,0.252,0.236,0.22,0.205,0.19,0.175,0.161,0.147,0.133,0.121,0.108,0.096,0.085,0.074,0.064,0.054,0.045,0.037,0.03,0.023,0.017,0.012,0.008,0.004,0.002,0,0,0.002,0.008,0.017,0.029,0.044,0.062,0.082,0.1,0.13,0.15,0.18,0.21,0.24,0.27,0.3,0.33,0.36,0.39,0.42,0.45,0.48,0.51,0.54,0.57,0.59,0.62,0.64,0.658,0.676,0.692,0.705,0.716,0.723,0.728,0.73,0.726,0.716,0.7,0.68,0.65,0.62,0.59,0.55,0.51,0.47,0.43,0.39,0.35,0.31,0.27,0.23,0.19,0.16,0.12,0.09,0.07,0.04,0.025,0.011,0.003,0,0.001,0.005,0.011,0.019,0.029,0.04,0.054,0.068,0.084,0.1,0.118,0.136,0.155,0.174,0.193,0.212,0.23,0.249,0.267,0.284,0.3,0.316,0.33,0.343,0.355,0.365,0.374,0.381,0.386,0.389,0.39,0.389,0.386,0.381,0.374,0.366,0.357,0.346,0.334,0.322,0.308,0.293,0.278,0.263,0.246,0.23,0.214,0.197,0.18,0.164,0.148,0.132,0.116,0.102,0.087,0.074,0.061,0.049,0.039,0.029,0.021,0.013,0.008,0.003,0.001,0 +PARAM_BOWL_LID=50 +PARAM_YUGE_01=0 +PARAM_YUGE_02=0 +PARAM_EFFECT=0 +PARAM_EAR_L=0 +PARAM_EAR_R=0 +PARAM_HAND_L=0 +PARAM_HAND_R=0 +PARAM_SWING=0 +PARAM_BOWL_SWING=0 +PARAM_FACE_01=0 +PARAM_BASE_X=0 \ No newline at end of file diff --git a/live2dw/assets/mtn/shake_01.mtn b/live2dw/assets/mtn/shake_01.mtn new file mode 100644 index 0000000000..04b89e6add --- /dev/null +++ b/live2dw/assets/mtn/shake_01.mtn @@ -0,0 +1,24 @@ +# Live2D Animator Motion Data +$fps=30 +PARAM_ANGLE_X=-9,-8.38,-6.44,-3.52,0,5.39,10.1,14.18,17.31,19.28,20,18.78,15.71,11.57,6.71,1.67,-3.31,-8.02,-11.98,-15.17,-17.24,-18,-16.91,-14.17,-10.45,-6.11,-1.6,2.85,7.07,10.61,13.47,15.32,16,15.17,13.01,9.88,6.31,2.6,-0.86,-3.82,-6.08,-7.5,-8,-7.48,-6.34,-4.93,-3.46,-2.12,-1.01,-0.28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_ANGLE_Y=14,17.87,24.22,28.58,30,30,30,30,30,30,30,29.77,29.14,28.21,26.98,25.54,23.89,22.01,20.02,17.82,15.5,13,9.93,7.41,5.38,3.68,2.32,1.24,0.38,-0.24,-0.67,-0.92,-1,-0.86,-0.5,0.02,0.62,1.23,1.81,2.3,2.68,2.92,3,2.8,2.38,1.85,1.3,0.8,0.38,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_ANGLE_Z=0,0.6,2.21,4.69,7.8,11.26,15,18.74,22.2,25.31,27.79,29.4,30,28.23,23.8,17.79,10.76,3.47,-3.74,-10.55,-16.29,-20.91,-23.9,-25,-23.36,-19.25,-13.68,-7.16,-0.4,6.28,12.6,17.92,22.21,24.98,26,23.64,18.54,12.18,5.58,-0.45,-5.44,-8.76,-10,-9.8,-9.26,-8.46,-7.45,-6.33,-5.15,-3.98,-2.89,-1.93,-1.12,-0.51,-0.13,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_EYE_L_OPEN=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.013,0.05,0.1,0.17,0.26,0.35,0.44,0.54,0.63,0.72,0.8,0.87,0.92,0.96,0.99,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 +PARAM_EYE_R_OPEN=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.013,0.05,0.1,0.17,0.26,0.35,0.44,0.54,0.63,0.72,0.8,0.87,0.92,0.96,0.99,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 +PARAM_MOUTH_OPEN_Y=1 +PARAM_BODY_ANGLE_X=0,0.14,0.5,1.02,1.62,2.23,2.81,3.3,3.68,3.92,4,3.74,3.1,2.22,1.2,0.14,-0.91,-1.9,-2.73,-3.41,-3.84,-4,-3.78,-3.21,-2.45,-1.55,-0.62,0.29,1.16,1.89,2.48,2.86,3,2.86,2.5,1.98,1.38,0.77,0.19,-0.3,-0.68,-0.92,-1,-0.93,-0.79,-0.62,-0.43,-0.27,-0.13,-0.03,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_BODY_ANGLE_Z=0,0.2,0.74,1.56,2.6,3.75,5,6.25,7.4,8.44,9.26,9.8,10,9.36,7.74,5.56,3,0.35,-2.27,-4.75,-6.83,-8.51,-9.6,-10,-9.52,-8.31,-6.67,-4.75,-2.76,-0.8,1.06,2.62,3.88,4.7,5,4.48,3.34,1.93,0.46,-0.88,-1.99,-2.72,-3,-2.86,-2.5,-1.98,-1.38,-0.77,-0.19,0.3,0.68,0.92,1,0.993,0.975,0.94,0.91,0.86,0.8,0.74,0.68,0.61,0.54,0.47,0.41,0.34,0.28,0.22,0.17,0.12,0.08,0.04,0.02,0.005,0 +PARAM_BODY_ANGLE_Y=0 +PARAM_BREATH=0 +PARAM_BOWL_LID=50 +PARAM_YUGE_01=0 +PARAM_YUGE_02=0 +PARAM_EFFECT=0 +PARAM_EAR_L=0,-0.24,-0.64,-0.91,-1,-0.87,-0.59,-0.23,0.13,0.47,0.75,0.93,1,0.93,0.75,0.49,0.19,-0.12,-0.41,-0.65,-0.84,-0.96,-1,-0.85,-0.5,-0.06,0.37,0.71,0.93,1,0.93,0.75,0.48,0.16,-0.16,-0.48,-0.75,-0.93,-1,-0.93,-0.75,-0.48,-0.16,0.16,0.48,0.75,0.93,1,0.95,0.83,0.65,0.44,0.21,0,-0.18,-0.3,-0.35,-0.27,-0.15,-0.04,0.05,0.08,0.068,0.04,0,-0.04,-0.07,-0.08,-0.074,-0.06,-0.042,-0.025,-0.012,-0.003,0 +PARAM_EAR_R=0,-0.23,-0.61,-0.87,-0.96,-0.83,-0.55,-0.21,0.15,0.48,0.75,0.93,1,0.93,0.75,0.49,0.19,-0.12,-0.41,-0.65,-0.84,-0.96,-1,-0.85,-0.5,-0.06,0.37,0.71,0.93,1,0.93,0.75,0.48,0.16,-0.16,-0.48,-0.75,-0.93,-1,-0.93,-0.75,-0.48,-0.16,0.16,0.48,0.75,0.93,1,0.95,0.83,0.66,0.45,0.23,0.02,-0.15,-0.27,-0.32,-0.25,-0.14,-0.03,0.04,0.07,0.059,0.03,-0.01,-0.04,-0.07,-0.08,-0.074,-0.06,-0.042,-0.025,-0.012,-0.003,0 +PARAM_HAND_L=0 +PARAM_HAND_R=0 +PARAM_SWING=0 +PARAM_BOWL_SWING=0 +PARAM_FACE_01=0 +PARAM_BASE_X=0 \ No newline at end of file diff --git a/live2dw/assets/mtn/shake_02.mtn b/live2dw/assets/mtn/shake_02.mtn new file mode 100644 index 0000000000..92732b1c7a --- /dev/null +++ b/live2dw/assets/mtn/shake_02.mtn @@ -0,0 +1,24 @@ +# Live2D Animator Motion Data +$fps=30 +PARAM_ANGLE_X=0 +PARAM_ANGLE_Y=30 +PARAM_ANGLE_Z=0,-7.88,-22.12,-30,-27.93,-22.52,-14.7,-5.77,3.51,12.16,19.55,25.19,28.75,30,26.99,19.65,10.7,1.97,-5.04,-9.49,-11,-8.31,-2.5,4.73,12.26,19.12,24.81,28.59,30,26.76,18.9,9.29,-0.08,-7.6,-12.38,-14,-12.49,-9.23,-5.17,-0.95,2.9,6.09,8.21,9,8.15,6.31,4.01,1.63,-0.55,-2.35,-3.55,-4,-3.86,-3.49,-2.96,-2.33,-1.67,-1.04,-0.51,-0.14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_EYE_L_OPEN=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.18,0.46,0.73,0.92,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 +PARAM_EYE_R_OPEN=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.18,0.46,0.73,0.92,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 +PARAM_MOUTH_OPEN_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.26,0.74,1,0.76,0.36,0.09,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_BODY_ANGLE_X=0 +PARAM_BODY_ANGLE_Z=-10,-9.31,-7.51,-4.9,-1.92,1.17,4.05,6.52,8.4,9.58,10,8.53,4.95,0.59,-3.67,-7.09,-9.26,-10,-8.69,-5.86,-2.32,1.34,4.69,7.47,9.31,10,9.58,8.53,7.11,5.45,3.73,2.03,0.42,-0.94,-2.03,-2.74,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-2.9,-2.66,-2.33,-1.95,-1.55,-1.16,-0.79,-0.48,-0.22,-0.06,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_BODY_ANGLE_Y=10,9.1,6.91,4,1.09,-1.1,-2,-1.97,-1.66,-0.98,0,2.06,4.22,6.19,7.84,9.05,9.77,10,7.85,4.42,1.22,-1.1,-2,-1.92,-1.41,0,2.43,4.4,6.08,7.47,8.55,9.35,9.83,10,9.1,6.91,4,1.09,-1.1,-2,-0.95,0.95,2,1.52,0.72,0.18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_BREATH=0 +PARAM_BOWL_LID=50 +PARAM_YUGE_01=0 +PARAM_YUGE_02=0 +PARAM_EFFECT=0 +PARAM_EAR_L=0,0.16,0.42,0.59,0.65,0.55,0.29,-0.01,-0.31,-0.55,-0.71,-0.76,-0.7,-0.54,-0.3,-0.02,0.26,0.54,0.78,0.94,1,0.93,0.75,0.49,0.19,-0.12,-0.41,-0.65,-0.84,-0.96,-1,-0.87,-0.59,-0.23,0.13,0.47,0.75,0.93,1,0.92,0.74,0.5,0.26,0.08,0,0.18,0.46,0.73,0.92,1,0.86,0.51,0.08,-0.34,-0.67,-0.89,-0.96,-0.76,-0.43,-0.13,0.09,0.17,0.157,0.13,0.08,0.04,0.01,0,0,0,0,0,0,0,0,0 +PARAM_EAR_R=0,0.2,0.52,0.74,0.81,0.68,0.35,-0.04,-0.43,-0.74,-0.93,-1,-0.93,-0.75,-0.48,-0.16,0.16,0.48,0.75,0.93,1,0.93,0.75,0.49,0.19,-0.12,-0.41,-0.65,-0.84,-0.96,-1,-0.87,-0.59,-0.23,0.13,0.47,0.75,0.93,1,0.98,0.9,0.77,0.58,0.33,0,-0.44,-0.7,-0.87,-0.97,-1,-0.85,-0.5,-0.06,0.37,0.71,0.93,1,0.79,0.46,0.15,-0.07,-0.16,-0.148,-0.12,-0.08,-0.04,-0.01,0,0,0,0,0,0,0,0,0 +PARAM_HAND_L=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.24,0.64,0.91,1,0.74,0.26,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_HAND_R=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.24,0.64,0.91,1,0.74,0.26,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_SWING=0 +PARAM_BOWL_SWING=0 +PARAM_FACE_01=0 +PARAM_BASE_X=0 \ No newline at end of file diff --git a/live2dw/assets/mtn/touch_01.mtn b/live2dw/assets/mtn/touch_01.mtn new file mode 100644 index 0000000000..1baf260d98 --- /dev/null +++ b/live2dw/assets/mtn/touch_01.mtn @@ -0,0 +1,24 @@ +# Live2D Animator Motion Data +$fps=30 +PARAM_ANGLE_X=0 +PARAM_ANGLE_Y=0,0,0,0,0,0,0,0,0,0,0,0,5.36,13.95,21.96,27.75,30,28.97,26.2,22.2,17.45,12.55,7.8,3.8,1.03,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_ANGLE_Z=0,0,0,0,0,0,0,0,0,0,0,0,0.57,2.06,4.23,6.92,9.91,13.03,16.19,19.25,22.12,24.71,26.85,28.53,29.62,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30 +PARAM_EYE_L_OPEN=1 +PARAM_EYE_R_OPEN=1 +PARAM_MOUTH_OPEN_Y=1 +PARAM_BODY_ANGLE_X=0,0,0,0,0,0,0,0,0,0,0,0,0.04,0.14,0.28,0.46,0.66,0.87,1.08,1.28,1.47,1.65,1.79,1.9,1.97,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2 +PARAM_BODY_ANGLE_Z=0 +PARAM_BODY_ANGLE_Y=0,0,0,0,0,0,0,0,0,0,0,0,1.07,2.79,4.39,5.55,6,5.79,5.24,4.44,3.49,2.51,1.56,0.76,0.21,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_BREATH=0 +PARAM_BOWL_LID=50 +PARAM_YUGE_01=0 +PARAM_YUGE_02=0 +PARAM_EFFECT=0 +PARAM_EAR_L=0,0,0,0,0,0,0,0,0,0,0,0,0.07,0.25,0.47,0.68,0.85,0.96,1,0.85,0.48,0,-0.48,-0.85,-1,-0.77,-0.39,-0.04,0.21,0.31,0.22,0.07,-0.06,-0.16,-0.2,-0.185,-0.15,-0.1,-0.05,-0.02,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_EAR_R=0,0,0,0,0,0,0,0,0,0,0,0,-0.07,-0.25,-0.47,-0.68,-0.85,-0.96,-1,-0.85,-0.48,0,0.48,0.85,1,0.76,0.38,0.02,-0.24,-0.34,-0.25,-0.11,0.03,0.12,0.16,0.148,0.12,0.08,0.04,0.01,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_HAND_L=0 +PARAM_HAND_R=0 +PARAM_SWING=0 +PARAM_BOWL_SWING=0 +PARAM_FACE_01=0 +PARAM_BASE_X=0 \ No newline at end of file diff --git a/live2dw/assets/mtn/touch_02.mtn b/live2dw/assets/mtn/touch_02.mtn new file mode 100644 index 0000000000..156fb69bc4 --- /dev/null +++ b/live2dw/assets/mtn/touch_02.mtn @@ -0,0 +1,24 @@ +# Live2D Animator Motion Data +$fps=30 +PARAM_ANGLE_X=0 +PARAM_ANGLE_Y=0,0,0,0,0,0,0,0,0,0,0,4.47,11.62,18.3,23.12,25,23.36,19.82,15.41,10.82,6.63,3.17,0.86,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_ANGLE_Z=0,0,0,0,0,0,0,0,0,0,0,1.25,3.25,5.12,6.47,7,6.54,5.55,4.31,3.03,1.86,0.89,0.24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_EYE_L_OPEN=0 +PARAM_EYE_R_OPEN=0 +PARAM_MOUTH_OPEN_Y=0 +PARAM_BODY_ANGLE_X=0 +PARAM_BODY_ANGLE_Z=0,0,0,0,0,0,0,0,0,0,0,1.25,3.25,5.12,6.47,7,6.34,4.93,3.16,1.33,-0.35,-1.73,-2.66,-3,-2.7,-1.97,-1,-0.03,0.7,1,0.52,-0.28,-0.82,-1,-0.93,-0.75,-0.53,-0.32,-0.15,-0.04,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_BODY_ANGLE_Y=0,0,0,0,0,0,0,0,0,0,0,1.43,3.72,5.86,7.4,8,7.28,5.72,3.78,1.76,-0.08,-1.61,-2.62,-3,-2.7,-1.97,-1,-0.03,0.7,1,0.52,-0.28,-0.82,-1,-0.93,-0.75,-0.53,-0.32,-0.15,-0.04,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_BREATH=0 +PARAM_BOWL_LID=50 +PARAM_YUGE_01=0 +PARAM_YUGE_02=0 +PARAM_EFFECT=0 +PARAM_EAR_L=0,0,0,0,0,0,0,0,0,0,0,0.07,0.21,0.38,0.57,0.73,0.87,0.97,1,0.85,0.5,0.06,-0.37,-0.71,-0.93,-1,-0.88,-0.58,-0.21,0.14,0.43,0.61,0.67,0.46,0.14,-0.17,-0.39,-0.48,-0.44,-0.36,-0.24,-0.12,-0.04,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_EAR_R=0,0,0,0,0,0,0,0,0,0,0,-0.07,-0.21,-0.38,-0.57,-0.73,-0.87,-0.97,-1,-0.85,-0.5,-0.06,0.37,0.71,0.93,1,0.89,0.61,0.28,-0.05,-0.31,-0.47,-0.53,-0.35,-0.07,0.2,0.39,0.47,0.43,0.35,0.24,0.12,0.04,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_HAND_L=0 +PARAM_HAND_R=0 +PARAM_SWING=0 +PARAM_BOWL_SWING=0 +PARAM_FACE_01=0 +PARAM_BASE_X=0 \ No newline at end of file diff --git a/live2dw/assets/mtn/touch_03.mtn b/live2dw/assets/mtn/touch_03.mtn new file mode 100644 index 0000000000..4ab09858fb --- /dev/null +++ b/live2dw/assets/mtn/touch_03.mtn @@ -0,0 +1,24 @@ +# Live2D Animator Motion Data +$fps=30 +PARAM_ANGLE_X=-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-1.5,2.15,7,11.85,15.5,17,10.03,-1.13,-11.55,-19.07,-22,-19.67,-14.02,-6.5,1.02,6.67,9,8.32,6.68,4.5,2.32,0.68,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_ANGLE_Y=15,15,15,15,15,15,15,15,15,15,15,15,15,15.15,15.52,16,16.48,16.85,17,16.79,16.21,15.34,14.25,12.99,11.62,10.16,8.7,7.21,5.8,4.47,3.24,2.15,1.26,0.59,0.15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_ANGLE_Z=0 +PARAM_EYE_L_OPEN=0 +PARAM_EYE_R_OPEN=0 +PARAM_MOUTH_OPEN_Y=1 +PARAM_BODY_ANGLE_X=0,0,0,0,0,0,0,0,0,0,0,0,0,-0.23,-0.77,-1.5,-2.23,-2.77,-3,-2.11,-0.68,0.66,1.62,2,1.77,1.23,0.5,-0.23,-0.77,-1,-0.92,-0.74,-0.5,-0.26,-0.08,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_BODY_ANGLE_Z=0,0,0,0,0,0,0,0,0,0,0,0,0,0.53,1.8,3.5,5.2,6.47,7,5.39,2.82,0.41,-1.32,-2,-1.77,-1.23,-0.5,0.23,0.77,1,0.92,0.74,0.5,0.26,0.08,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_BODY_ANGLE_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,-0.019,-0.07,-0.16,-0.27,-0.41,-0.56,-0.73,-0.9,-1.09,-1.28,-1.48,-1.67,-1.86,-2.04,-2.22,-2.38,-2.53,-2.66,-2.78,-2.87,-2.94,-2.98,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3 +PARAM_BREATH=0 +PARAM_BOWL_LID=50 +PARAM_YUGE_01=0 +PARAM_YUGE_02=0 +PARAM_EFFECT=0 +PARAM_EAR_L=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.24,0.64,0.91,1,0.85,0.5,0.03,-0.44,-0.79,-0.94,-0.68,-0.28,0.11,0.38,0.49,0.42,0.27,0.07,-0.12,-0.27,-0.37,-0.4,-0.37,-0.3,-0.2,-0.1,-0.03,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_EAR_R=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.24,-0.64,-0.91,-1,-0.85,-0.48,0,0.48,0.85,1,0.74,0.34,-0.05,-0.32,-0.43,-0.37,-0.24,-0.07,0.1,0.23,0.31,0.34,0.31,0.25,0.17,0.09,0.03,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_HAND_L=0 +PARAM_HAND_R=0 +PARAM_SWING=0 +PARAM_BOWL_SWING=0 +PARAM_FACE_01=0 +PARAM_BASE_X=0 \ No newline at end of file diff --git a/live2dw/assets/mtn/touch_04.mtn b/live2dw/assets/mtn/touch_04.mtn new file mode 100644 index 0000000000..1e3c293704 --- /dev/null +++ b/live2dw/assets/mtn/touch_04.mtn @@ -0,0 +1,24 @@ +# Live2D Animator Motion Data +$fps=30 +PARAM_ANGLE_X=0 +PARAM_ANGLE_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.83,-2.83,-5.5,-8.17,-10.17,-11,-10.28,-8.34,-5.54,-2.22,1.22,4.54,7.34,9.28,10,9.66,8.75,7.45,5.96,4.42,2.97,1.74,0.8,0.21,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_ANGLE_Z=0 +PARAM_EYE_L_OPEN=0 +PARAM_EYE_R_OPEN=0 +PARAM_MOUTH_OPEN_Y=0 +PARAM_BODY_ANGLE_X=0 +PARAM_BODY_ANGLE_Z=0 +PARAM_BODY_ANGLE_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.45,-1.55,-3,-4.45,-5.55,-6,-5.66,-4.73,-3.4,-1.82,-0.18,1.4,2.73,3.66,4,3.86,3.5,2.98,2.38,1.77,1.19,0.7,0.32,0.08,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_BREATH=0 +PARAM_BOWL_LID=50 +PARAM_YUGE_01=0 +PARAM_YUGE_02=0 +PARAM_EFFECT=0 +PARAM_EAR_L=0,0,0,0,0,0,0,0,0,0,0,0,0.07,0.25,0.47,0.68,0.85,0.96,1,0.85,0.5,0.06,-0.37,-0.71,-0.93,-1,-0.87,-0.55,-0.15,0.23,0.54,0.73,0.8,0.63,0.37,0.12,-0.06,-0.13,-0.11,-0.07,-0.03,-0.01,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_EAR_R=0,0,0,0,0,0,0,0,0,0,0,0,-0.07,-0.25,-0.47,-0.68,-0.85,-0.96,-1,-0.85,-0.5,-0.06,0.37,0.71,0.93,1,0.87,0.54,0.15,-0.24,-0.55,-0.74,-0.81,-0.65,-0.38,-0.14,0.04,0.11,0.09,0.06,0.03,0.01,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_HAND_L=0 +PARAM_HAND_R=0 +PARAM_SWING=0 +PARAM_BOWL_SWING=0 +PARAM_FACE_01=0 +PARAM_BASE_X=0 \ No newline at end of file diff --git a/live2dw/assets/mtn/touch_05.mtn b/live2dw/assets/mtn/touch_05.mtn new file mode 100644 index 0000000000..4847895a2b --- /dev/null +++ b/live2dw/assets/mtn/touch_05.mtn @@ -0,0 +1,24 @@ +# Live2D Animator Motion Data +$fps=30 +PARAM_ANGLE_X=0 +PARAM_ANGLE_Y=-11,-11,-11,-11,-11,-11,-11,-10.28,-8.34,-5.54,-2.22,1.22,4.54,7.34,9.28,10,9.66,8.73,7.4,5.82,4.18,2.6,1.27,0.34,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_ANGLE_Z=0 +PARAM_EYE_L_OPEN=0 +PARAM_EYE_R_OPEN=0 +PARAM_MOUTH_OPEN_Y=0 +PARAM_BODY_ANGLE_X=0 +PARAM_BODY_ANGLE_Z=0 +PARAM_BODY_ANGLE_Y=0,0,0,0,0,0,0,0.14,0.51,1.04,1.67,2.33,2.96,3.49,3.86,4,3.86,3.49,2.96,2.33,1.67,1.04,0.51,0.14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_BREATH=0 +PARAM_BOWL_LID=50 +PARAM_YUGE_01=0 +PARAM_YUGE_02=0 +PARAM_EFFECT=0 +PARAM_EAR_L=0 +PARAM_EAR_R=0 +PARAM_HAND_L=0 +PARAM_HAND_R=0 +PARAM_SWING=0 +PARAM_BOWL_SWING=0 +PARAM_FACE_01=0 +PARAM_BASE_X=0 \ No newline at end of file diff --git a/live2dw/assets/mtn/touch_06.mtn b/live2dw/assets/mtn/touch_06.mtn new file mode 100644 index 0000000000..001765f4d4 --- /dev/null +++ b/live2dw/assets/mtn/touch_06.mtn @@ -0,0 +1,24 @@ +# Live2D Animator Motion Data +$fps=30 +PARAM_ANGLE_X=0,0,0,0,0,0,0,0,0,0,0,0.1,0.34,0.67,1.05,1.45,1.84,2.21,2.52,2.78,2.94,3,2.63,1.74,0.65,-0.42,-1.27,-1.82,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2 +PARAM_ANGLE_Y=0,0,0,0,0,0,0,0,0,0,0,4.36,11.5,16.4,18,14.47,5.89,-4.59,-14.81,-23.02,-28.23,-30,-28.75,-25.71,-22,-18.38,-15.47,-13.63,-13,-14.25,-17.29,-21,-24.62,-27.53,-29.37,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30 +PARAM_ANGLE_Z=0,0,0,0,0,0,0,0,0,0,0,-0.26,-0.95,-1.98,-3.29,-4.8,-6.51,-8.41,-10.38,-12.51,-14.7,-17,-19.56,-21.39,-22.59,-23.34,-23.76,-23.95,-24,-24,-24,-24,-24,-24,-24,-24,-24,-24,-24,-24,-24,-24,-24,-24,-24,-24,-24,-24,-24,-24,-24,-24,-24,-24,-24,-24 +PARAM_EYE_L_OPEN=0 +PARAM_EYE_R_OPEN=0 +PARAM_MOUTH_OPEN_Y=0 +PARAM_BODY_ANGLE_X=0,0,0,0,0,0,0,0,0,0,0,-0.03,-0.12,-0.26,-0.44,-0.67,-0.93,-1.23,-1.55,-1.88,-2.23,-2.59,-2.96,-3.32,-3.67,-4.02,-4.35,-4.66,-4.95,-5.21,-5.44,-5.63,-5.79,-5.9,-5.98,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6 +PARAM_BODY_ANGLE_Z=0,0,0,0,0,0,0,0,0,0,0,-0.06,-0.23,-0.44,-0.7,-0.96,-1.23,-1.47,-1.68,-1.85,-1.96,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2 +PARAM_BODY_ANGLE_Y=0,0,0,0,0,0,0,0,0,0,0,0.73,1.92,2.73,3,2.34,0.73,-1.24,-3.15,-4.69,-5.67,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6 +PARAM_BREATH=0 +PARAM_BOWL_LID=50 +PARAM_YUGE_01=0 +PARAM_YUGE_02=0 +PARAM_EFFECT=0 +PARAM_EAR_L=0,0,0,0,0,0,0,0,0,0,0,-0.18,-0.46,-0.73,-0.92,-1,-0.64,-0.07,0.46,0.85,1,0.85,0.5,0.03,-0.45,-0.8,-0.95,-0.7,-0.3,0.07,0.34,0.45,0.34,0.16,0,-0.12,-0.17,-0.157,-0.13,-0.08,-0.04,-0.01,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_EAR_R=0,0,0,0,0,0,0,0,0,0,0,0.18,0.46,0.73,0.92,1,0.64,0.07,-0.46,-0.85,-1,-0.85,-0.48,0,0.48,0.85,1,0.75,0.35,-0.02,-0.29,-0.39,-0.3,-0.15,-0.01,0.09,0.13,0.12,0.1,0.07,0.03,0.01,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PARAM_HAND_L=0 +PARAM_HAND_R=0 +PARAM_SWING=0 +PARAM_BOWL_SWING=0 +PARAM_FACE_01=0 +PARAM_BASE_X=0 \ No newline at end of file diff --git a/live2dw/assets/wanko.model.json b/live2dw/assets/wanko.model.json new file mode 100644 index 0000000000..9be37d0100 --- /dev/null +++ b/live2dw/assets/wanko.model.json @@ -0,0 +1 @@ +{"type":"Live2D Model Setting","name":"wanko","model":"moc/wanko.moc","textures":["moc/wanko.1024/texture_00.png"],"hit_areas":[{"name":"body","id":"D_REF.PT_SOBA_01"}],"layout":{"center_x":0,"y":1.8,"width":2.9},"motions":{"idle":[{"file":"mtn/idle_01.mtn","fade_in":500,"fade_out":200},{"file":"mtn/idle_02.mtn","fade_in":500,"fade_out":200},{"file":"mtn/idle_03.mtn","fade_in":500,"fade_out":200},{"file":"mtn/idle_04.mtn","fade_in":500,"fade_out":200}],"shake":[{"file":"mtn/shake_01.mtn","fade_in":500,"fade_out":0},{"file":"mtn/shake_02.mtn","fade_in":500,"fade_out":0}],"tap_body":[{"file":"mtn/touch_01.mtn","fade_in":500,"fade_out":0},{"file":"mtn/touch_02.mtn","fade_in":500,"fade_out":0},{"file":"mtn/touch_03.mtn","fade_in":500,"fade_out":0}]}} \ No newline at end of file diff --git a/live2dw/lib/L2Dwidget.0.min.js b/live2dw/lib/L2Dwidget.0.min.js new file mode 100644 index 0000000000..30411d3e22 --- /dev/null +++ b/live2dw/lib/L2Dwidget.0.min.js @@ -0,0 +1,3 @@ +/*! https://github.com/xiazeyu/live2d-widget.js built@2019-4-6 09:38:17 */ +webpackJsonpL2Dwidget([0],{76:function(t,e,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.captureFrame=e.theRealInit=void 0;var r,o=i(38),n=i(80),s=i(77),a=i(78),_=i(84),h=i(81),l=i(79),$=i(39),u=(r=$,r&&r.__esModule?r:{default:r}),p=i(37);var c=null,f=void 0,g=!1,d=null,y=null,m=null,T=null,P=!1,S=.5;function v(t,e,i){if(e.xi.left&&e.y>i.top)return e;var r=t.x-e.x,o=t.y-e.y;function n(t,e){return 180*Math.acos((i={x:0,y:1},r=function(t,e){var i=Math.sqrt(t*t+e*e);return{x:t/i,y:e/i}}(t,e),i.x*r.x+i.y*r.y))/Math.PI;var i,r}var s=n(r,o);e.xG._$T7){t._$NP|=r._$4s;throw new ht("_$gi _$C _$li , _$n0 _$_ version _$li ( SDK : "+G._$T7+" < _$f0 : "+i+" )@_$SS#loadModel()\n")}var h=o._$nP();if(i>=G._$s7){var l=o._$9T(),$=o._$9T();if(-30584!=l||-30584!=$)throw t._$NP|=r._$0s,new ht("_$gi _$C _$li , _$0 _$6 _$Ui.")}t._$KS(h);var u=t.getModelContext();u.setDrawParam(t.getDrawParam()),u.init()}catch(t){a._$Rb(t)}},r.prototype._$KS=function(t){this._$MT=t},r.prototype.getModelImpl=function(){return null==this._$MT&&(this._$MT=new $,this._$MT._$zP()),this._$MT},r.prototype.getCanvasWidth=function(){return null==this._$MT?0:this._$MT.getCanvasWidth()},r.prototype.getCanvasHeight=function(){return null==this._$MT?0:this._$MT.getCanvasHeight()},r.prototype.getParamFloat=function(t){return"number"!=typeof t&&(t=this._$5S.getParamIndex(l.getID(t))),this._$5S.getParamFloat(t)},r.prototype.setParamFloat=function(t,e,i){"number"!=typeof t&&(t=this._$5S.getParamIndex(l.getID(t))),arguments.length<3&&(i=1),this._$5S.setParamFloat(t,this._$5S.getParamFloat(t)*(1-i)+e*i)},r.prototype.addToParamFloat=function(t,e,i){"number"!=typeof t&&(t=this._$5S.getParamIndex(l.getID(t))),arguments.length<3&&(i=1),this._$5S.setParamFloat(t,this._$5S.getParamFloat(t)+e*i)},r.prototype.multParamFloat=function(t,e,i){"number"!=typeof t&&(t=this._$5S.getParamIndex(l.getID(t))),arguments.length<3&&(i=1),this._$5S.setParamFloat(t,this._$5S.getParamFloat(t)*(1+(e-1)*i))},r.prototype.getParamIndex=function(t){return this._$5S.getParamIndex(l.getID(t))},r.prototype.loadParam=function(){this._$5S.loadParam()},r.prototype.saveParam=function(){this._$5S.saveParam()},r.prototype.init=function(){this._$5S.init()},r.prototype.update=function(){this._$5S.update()},r.prototype._$Rs=function(){return a._$li("_$60 _$PT _$Rs()"),-1},r.prototype._$Ds=function(t){a._$li("_$60 _$PT _$SS#_$Ds() \n")},r.prototype._$K2=function(){},r.prototype.draw=function(){},r.prototype.getModelContext=function(){return this._$5S},r.prototype._$s2=function(){return this._$NP},r.prototype._$P7=function(t,e,i,r){var o=-1,n=0;if(0!=i)if(1==t.length){u=t[0];var s=0!=this.getParamFloat(u),a=(p=e[0],this.getPartsOpacity(p)),_=i/r;s?(a+=_)>1&&(a=1):(a-=_)<0&&(a=0),this.setPartsOpacity(p,a)}else{for($=0;$=0)break;o=$;p=e[$];n=this.getPartsOpacity(p),(n+=i/r)>1&&(n=1)}}o<0&&(console.log("No _$wi _$q0/ _$U default[%s]",t[0]),o=0,n=1,this.loadParam(),this.setParamFloat(t[o],n),this.saveParam());for($=0;$.15&&(h=1-.15/(1-n)),l>h&&(l=h),this.setPartsOpacity(p,l)}}}else for(var $=0;$=this._$5S._$aS.length)return null;var e=this._$5S._$aS[t];return null!=e&&e.getType()==W._$wb&&e instanceof lt?e.getIndexArray():null};function o(t){if(!i){this.clipContextList=new Array,this.glcontext=t.gl,this.dp_webgl=t,this.curFrameNo=0,this.firstError_clipInNotUpdate=!0,this.colorBuffer=0,this.isInitGLFBFunc=!1,this.tmpBoundsOnModel=new P,at.glContext.length>at.frameBuffers.length&&(this.curFrameNo=this.getMaskRenderTexture()),this.tmpModelToViewMatrix=new O,this.tmpMatrix2=new O,this.tmpMatrixForMask=new O,this.tmpMatrixForDraw=new O,this.CHANNEL_COLORS=new Array;var e=new E;(e=new E).r=0,e.g=0,e.b=0,e.a=1,this.CHANNEL_COLORS.push(e),(e=new E).r=1,e.g=0,e.b=0,e.a=0,this.CHANNEL_COLORS.push(e),(e=new E).r=0,e.g=1,e.b=0,e.a=0,this.CHANNEL_COLORS.push(e),(e=new E).r=0,e.g=0,e.b=1,e.a=0,this.CHANNEL_COLORS.push(e);for(var r=0;r=0;--t)this.CHANNEL_COLORS.splice(t,1);this.CHANNEL_COLORS=[]}this.releaseShader()},o.prototype.releaseShader=function(){for(var t=at.frameBuffers.length,e=0;e0){var n=e.gl.getParameter(e.gl.FRAMEBUFFER_BINDING),s=new Array(4);s[0]=0,s[1]=0,s[2]=e.gl.canvas.width,s[3]=e.gl.canvas.height,e.gl.viewport(0,0,at.clippingMaskBufferSize,at.clippingMaskBufferSize),this.setupLayoutBounds(i),e.gl.bindFramebuffer(e.gl.FRAMEBUFFER,at.frameBuffers[this.curFrameNo].framebuffer),e.gl.clearColor(0,0,0,0),e.gl.clear(e.gl.COLOR_BUFFER_BIT);for(r=0;rr?i:r,n=o,s=o,a=0,_=0,h=e.clippedDrawContextList.length,l=0;la&&(a=P),S>_&&(_=S)}}if(n==o)e.allClippedDrawRect.x=0,e.allClippedDrawRect.y=0,e.allClippedDrawRect.width=0,e.allClippedDrawRect.height=0,e.isUsing=!1;else{var v=a-n,L=_-s;e.allClippedDrawRect.x=n,e.allClippedDrawRect.y=s,e.allClippedDrawRect.width=v,e.allClippedDrawRect.height=L,e.isUsing=!0}},o.prototype.setupLayoutBounds=function(t){var e=t/o.CHANNEL_COUNT,i=t%o.CHANNEL_COUNT;e=~~e,i=~~i;for(var r=0,n=0;n=1)return 1;var u=r*r;return h*(r*u)+l*u+$*r+0},s.prototype._$a0=function(){},s.prototype.setFadeIn=function(t){this._$dP=t},s.prototype.setFadeOut=function(t){this._$eo=t},s.prototype._$pT=function(t){this._$V0=t},s.prototype.getFadeOut=function(){return this._$eo},s.prototype._$4T=function(){return this._$eo},s.prototype._$mT=function(){return this._$V0},s.prototype.getDurationMSec=function(){return-1},s.prototype.getLoopDurationMSec=function(){return-1},s.prototype.updateParam=function(t,e){if(e._$AT&&!e._$9L){var i=A.getUserTimeMSec();if(e._$z2<0){e._$z2=i,e._$bs=i;var r=this.getDurationMSec();e._$Do<0&&(e._$Do=r<=0?-1:e._$z2+r)}var o=this._$V0;0<=(o=o*(0==this._$dP?1:_t._$r2((i-e._$bs)/this._$dP))*(0==this._$eo||e._$Do<0?1:_t._$r2((e._$Do-i)/this._$eo)))&&o<=1||console.log("### assert!! ### "),this.updateParamExe(t,i,o,e),e._$Do>0&&e._$Do0?console.log("\n"):i%8==0&&i>0&&console.log(" "),console.log("%02X ",255&t[i]);console.log("\n")},a._$nr=function(t,e,i){console.log("%s\n",t);for(var r=e.length,o=0;o=0;--r){this._$lL[r]._$oP(t,this)}this._$oo(t,i),this._$M2=this._$Yb(),this._$9b=(this._$M2-this._$ks)/i,this._$ks=this._$M2}for(r=this._$qP.length-1;r>=0;--r){this._$qP[r]._$YS(t,this)}this._$iT=e},u.prototype._$oo=function(t,e){e<.033&&(e=.033);var i=1/e;this.p1.vx=(this.p1.x-this.p1._$s0)*i,this.p1.vy=(this.p1.y-this.p1._$70)*i,this.p1.ax=(this.p1.vx-this.p1._$7L)*i,this.p1.ay=(this.p1.vy-this.p1._$HL)*i,this.p1.fx=this.p1.ax*this.p1._$p,this.p1.fy=this.p1.ay*this.p1._$p,this.p1._$xT();var r,o,n=-Math.atan2(this.p1.y-this.p2.y,this.p1.x-this.p2.x),s=Math.cos(n),a=Math.sin(n),_=9.8*this.p2._$p,h=this._$Db*vt._$bS,l=_*Math.cos(n-h);r=l*a,o=l*s;var $=-this.p1.fx*a*a,u=-this.p1.fy*a*s,p=-this.p2.vx*this._$L2,c=-this.p2.vy*this._$L2;this.p2.fx=r+$+p,this.p2.fy=o+u+c,this.p2.ax=this.p2.fx/this.p2._$p,this.p2.ay=this.p2.fy/this.p2._$p,this.p2.vx+=this.p2.ax*e,this.p2.vy+=this.p2.ay*e,this.p2.x+=this.p2.vx*e,this.p2.y+=this.p2.vy*e;var f=Math.sqrt((this.p1.x-this.p2.x)*(this.p1.x-this.p2.x)+(this.p1.y-this.p2.y)*(this.p1.y-this.p2.y));this.p2.x=this.p1.x+this._$Fo*(this.p2.x-this.p1.x)/f,this.p2.y=this.p1.y+this._$Fo*(this.p2.y-this.p1.y)/f,this.p2.vx=(this.p2.x-this.p2._$s0)*i,this.p2.vy=(this.p2.y-this.p2._$70)*i,this.p2._$xT()};function p(){this._$p=1,this.x=0,this.y=0,this.vx=0,this.vy=0,this.ax=0,this.ay=0,this.fx=0,this.fy=0,this._$s0=0,this._$70=0,this._$7L=0,this._$HL=0}p.prototype._$xT=function(){this._$s0=this.x,this._$70=this.y,this._$7L=this.vx,this._$HL=this.vy};function c(t,e,i){this._$wL=null,this.scale=null,this._$V0=null,this._$wL=t,this.scale=e,this._$V0=i}c.prototype._$oP=function(t,e){};function f(t,e,i,r){c.prototype.constructor.call(this,e,i,r),this._$tL=null,this._$tL=t}f.prototype=new c,f.prototype._$oP=function(t,e){var i=this.scale*t.getParamFloat(this._$wL),r=e.getPhysicsPoint1();switch(this._$tL){default:case u.Src.SRC_TO_X:r.x=r.x+(i-r.x)*this._$V0;break;case u.Src.SRC_TO_Y:r.y=r.y+(i-r.y)*this._$V0;break;case u.Src.SRC_TO_G_ANGLE:var o=e._$qr();o+=(i-o)*this._$V0,e._$pr(o)}};function g(t,e,i){this._$wL=null,this.scale=null,this._$V0=null,this._$wL=t,this.scale=e,this._$V0=i}g.prototype._$YS=function(t,e){};function d(t,e,i,r){g.prototype.constructor.call(this,e,i,r),this._$YP=null,this._$YP=t}d.prototype=new g,d.prototype._$YS=function(t,e){switch(this._$YP){default:case u.Target.TARGET_FROM_ANGLE:t.setParamFloat(this._$wL,this.scale*e._$5r(),this._$V0);break;case u.Target.TARGET_FROM_ANGLE_V:t.setParamFloat(this._$wL,this.scale*e._$Cs(),this._$V0)}},u.Src=function(){},u.Src.SRC_TO_X="SRC_TO_X",u.Src.SRC_TO_Y="SRC_TO_Y",u.Src.SRC_TO_G_ANGLE="SRC_TO_G_ANGLE",u.Target=function(){},u.Target.TARGET_FROM_ANGLE="TARGET_FROM_ANGLE",u.Target.TARGET_FROM_ANGLE_V="TARGET_FROM_ANGLE_V";function y(){i||(this._$fL=0,this._$gL=0,this._$B0=1,this._$z0=1,this._$qT=0,this.reflectX=!1,this.reflectY=!1)}y.prototype.init=function(t){this._$fL=t._$fL,this._$gL=t._$gL,this._$B0=t._$B0,this._$z0=t._$z0,this._$qT=t._$qT,this.reflectX=t.reflectX,this.reflectY=t.reflectY},y.prototype._$F0=function(t){this._$fL=t._$_T(),this._$gL=t._$_T(),this._$B0=t._$_T(),this._$z0=t._$_T(),this._$qT=t._$_T(),t.getFormatVersion()>=G.LIVE2D_FORMAT_VERSION_V2_10_SDK2&&(this.reflectX=t._$po(),this.reflectY=t._$po())},y.prototype._$e=function(){};var T=function(){};T._$ni=function(t,e,i,r,o,n,s,a,_){var h=s*n-a*o;if(0==h)return null;var l,$=((t-i)*n-(e-r)*o)/h;return l=0!=o?(t-i-$*s)/o:(e-r-$*a)/n,isNaN(l)&&(l=(t-i-$*s)/o,isNaN(l)&&(l=(e-r-$*a)/n),isNaN(l)&&(console.log("a is NaN @UtVector#_$ni() "),console.log("v1x : "+o),console.log("v1x != 0 ? "+(0!=o)))),null==_?new Array(l,$):(_[0]=l,_[1]=$,_)};function P(){i||(this.x=null,this.y=null,this.width=null,this.height=null)}P.prototype._$8P=function(){return this.x+.5*this.width},P.prototype._$6P=function(){return this.y+.5*this.height},P.prototype._$EL=function(){return this.x+this.width},P.prototype._$5T=function(){return this.y+this.height},P.prototype._$jL=function(t,e,i,r){this.x=t,this.y=e,this.width=i,this.height=r},P.prototype._$jL=function(t){this.x=t.x,this.y=t.y,this.width=t.width,this.height=t.height},P.prototype.contains=function(t,e){return this.x<=this.x&&this.y<=this.y&&this.x<=this.x+this.width&&this.y<=this.y+this.height},P.prototype.expand=function(t,e){this.x-=t,this.y-=e,this.width+=2*t,this.height+=2*e};function S(){}S._$Z2=function(t,e,i,r){var o=e._$Q2(t,i),n=t._$vs(),s=t._$Tr();if(e._$zr(n,s,o),o<=0)return r[n[0]];if(1==o){return(a=r[n[0]])+((_=r[n[1]])-a)*($=s[0])|0}if(2==o){var a=r[n[0]],_=r[n[1]],h=r[n[2]],l=r[n[3]],$=s[0],u=s[1];return(S=a+(_-a)*$|0)+((h+(l-h)*$|0)-S)*u|0}if(3==o){var p=r[n[0]],c=r[n[1]],f=r[n[2]],g=r[n[3]],d=r[n[4]],y=r[n[5]],m=r[n[6]],T=r[n[7]],P=($=s[0],u=s[1],s[2]);return(S=(a=p+(c-p)*$|0)+((_=f+(g-f)*$|0)-a)*u|0)+(((h=d+(y-d)*$|0)+((l=m+(T-m)*$|0)-h)*u|0)-S)*P|0}if(4==o){var S,v=r[n[0]],L=r[n[1]],M=r[n[2]],E=r[n[3]],x=r[n[4]],A=r[n[5]],I=r[n[6]],w=r[n[7]],D=r[n[8]],O=r[n[9]],b=r[n[10]],R=r[n[11]],F=r[n[12]],C=r[n[13]],N=r[n[14]],B=r[n[15]],G=($=s[0],u=s[1],P=s[2],s[3]);return(S=(a=(p=v+(L-v)*$|0)+((c=M+(E-M)*$|0)-p)*u|0)+((_=(f=x+(A-x)*$|0)+((g=I+(w-I)*$|0)-f)*u|0)-a)*P|0)+(((h=(d=D+(O-D)*$|0)+((y=b+(R-b)*$|0)-d)*u|0)+((l=(m=F+(C-F)*$|0)+((T=N+(B-N)*$|0)-m)*u|0)-h)*P|0)-S)*G|0}for(var U=1<=G._$T7?(this.clipID=t._$nP(),this.clipIDList=this.convertClipIDForV2_11(this.clipID)):this.clipIDList=[],this._$MS(this._$Lb)},L.prototype.getClipIDList=function(){return this.clipIDList},L.prototype.init=function(t){},L.prototype._$Nr=function(t,e){if(e._$IS[0]=!1,e._$Us=S._$Z2(t,this._$GS,e._$IS,this._$Lb),at._$Zs);else if(e._$IS[0])return;e._$7s=S._$br(t,this._$GS,e._$IS,this._$mS)},L.prototype._$2b=function(t,e){},L.prototype.getDrawDataID=function(){return this._$gP},L.prototype._$j2=function(t){this._$gP=t},L.prototype.getOpacity=function(t,e){return e._$7s},L.prototype._$zS=function(t,e){return e._$Us},L.prototype._$MS=function(t){for(var e=t.length-1;e>=0;--e){var i=t[e];iL._$R2&&(L._$R2=i)}},L.prototype.getTargetBaseDataID=function(){return this._$dr},L.prototype._$gs=function(t){this._$dr=t},L.prototype._$32=function(){return null!=this._$dr&&this._$dr!=dt._$2o()},L.prototype.preDraw=function(t,e,i){},L.prototype.draw=function(t,e,i){},L.prototype.getType=function(){},L.prototype._$B2=function(t,e,i){};function M(){i||(this._$Eb=M._$ps,this._$lT=1,this._$C0=1,this._$tT=1,this._$WL=1,this.culling=!1,this.matrix4x4=new Float32Array(16),this.premultipliedAlpha=!1,this.anisotropy=0,this.clippingProcess=M.CLIPPING_PROCESS_NONE,this.clipBufPre_clipContextMask=null,this.clipBufPre_clipContextDraw=null,this.CHANNEL_COLORS=new Array)}M._$ps=32,M.CLIPPING_PROCESS_NONE=0,M.CLIPPING_PROCESS_OVERWRITE_ALPHA=1,M.CLIPPING_PROCESS_MULTIPLY_ALPHA=2,M.CLIPPING_PROCESS_DRAW=3,M.CLIPPING_PROCESS_CLEAR_ALPHA=4,M.prototype.setChannelFlagAsColor=function(t,e){this.CHANNEL_COLORS[t]=e},M.prototype.getChannelFlagAsColor=function(t){return this.CHANNEL_COLORS[t]},M.prototype._$ZT=function(){},M.prototype._$Uo=function(t,e,i,r,o,n,s){},M.prototype._$Rs=function(){return-1},M.prototype._$Ds=function(t){},M.prototype.setBaseColor=function(t,e,i,r){t<0?t=0:t>1&&(t=1),e<0?e=0:e>1&&(e=1),i<0?i=0:i>1&&(i=1),r<0?r=0:r>1&&(r=1),this._$lT=t,this._$C0=e,this._$tT=i,this._$WL=r},M.prototype._$WP=function(t){this.culling=t},M.prototype.setMatrix=function(t){for(var e=0;e<16;e++)this.matrix4x4[e]=t[e]},M.prototype._$IT=function(){return this.matrix4x4},M.prototype.setPremultipliedAlpha=function(t){this.premultipliedAlpha=t},M.prototype.isPremultipliedAlpha=function(){return this.premultipliedAlpha},M.prototype.setAnisotropy=function(t){this.anisotropy=t},M.prototype.getAnisotropy=function(){return this.anisotropy},M.prototype.getClippingProcess=function(){return this.clippingProcess},M.prototype.setClippingProcess=function(t){this.clippingProcess=t},M.prototype.setClipBufPre_clipContextForMask=function(t){this.clipBufPre_clipContextMask=t},M.prototype.getClipBufPre_clipContextMask=function(){return this.clipBufPre_clipContextMask},M.prototype.setClipBufPre_clipContextForDraw=function(t){this.clipBufPre_clipContextDraw=t},M.prototype.getClipBufPre_clipContextDraw=function(){return this.clipBufPre_clipContextDraw};function E(){i||(this.a=1,this.r=1,this.g=1,this.b=1,this.scale=1,this._$ho=1,this.blendMode=at.L2D_COLOR_BLEND_MODE_MULT)}function x(){i||(this._$kP=null,this._$dr=null,this._$Ai=!0,this._$mS=null)}x._$ur=-2,x._$c2=1,x._$_b=2,x.prototype._$F0=function(t){this._$kP=t._$nP(),this._$dr=t._$nP()},x.prototype.readV2_opacity=function(t){t.getFormatVersion()>=G.LIVE2D_FORMAT_VERSION_V2_10_SDK2&&(this._$mS=t._$Tb())},x.prototype.init=function(t){},x.prototype._$Nr=function(t,e){},x.prototype.interpolateOpacity=function(t,e,i,r){null==this._$mS?i.setInterpolatedOpacity(1):i.setInterpolatedOpacity(S._$br(t,e,r,this._$mS))},x.prototype._$2b=function(t,e){},x.prototype._$nb=function(t,e,i,r,o,n,s){},x.prototype.getType=function(){},x.prototype._$gs=function(t){this._$dr=t},x.prototype._$a2=function(t){this._$kP=t},x.prototype.getTargetBaseDataID=function(){return this._$dr},x.prototype.getBaseDataID=function(){return this._$kP},x.prototype._$32=function(){return null!=this._$dr&&this._$dr!=dt._$2o()};function A(){}A._$W2=0,A._$CS=A._$W2,A._$Mo=function(){return!0},A._$XP=function(t){try{for(var e=getTimeMSec();getTimeMSec()-e=t.length)return!1;for(var o=e;o=0;--i){var r=this._$Ob[i].getParamIndex(e);if(r==I._$ds&&(r=t.getParamIndex(this._$Ob[i].getParamID())),t._$Xb(r))return!0}return!1},D.prototype._$Q2=function(t,e){for(var i,r,o=this._$Ob.length,n=t._$v2(),s=0,a=0;aB._$Qb&&console.log("err 23245\n");for(var o=this._$Ob.length,n=1,s=1,a=0,_=0;_=0;--n)i[n]=o[n]}else this.mult_fast(t,e,i,r)},O.prototype.mult_fast=function(t,e,i,r){r?(i[0]=t[0]*e[0]+t[4]*e[1]+t[8]*e[2],i[4]=t[0]*e[4]+t[4]*e[5]+t[8]*e[6],i[8]=t[0]*e[8]+t[4]*e[9]+t[8]*e[10],i[12]=t[0]*e[12]+t[4]*e[13]+t[8]*e[14]+t[12],i[1]=t[1]*e[0]+t[5]*e[1]+t[9]*e[2],i[5]=t[1]*e[4]+t[5]*e[5]+t[9]*e[6],i[9]=t[1]*e[8]+t[5]*e[9]+t[9]*e[10],i[13]=t[1]*e[12]+t[5]*e[13]+t[9]*e[14]+t[13],i[2]=t[2]*e[0]+t[6]*e[1]+t[10]*e[2],i[6]=t[2]*e[4]+t[6]*e[5]+t[10]*e[6],i[10]=t[2]*e[8]+t[6]*e[9]+t[10]*e[10],i[14]=t[2]*e[12]+t[6]*e[13]+t[10]*e[14]+t[14],i[3]=i[7]=i[11]=0,i[15]=1):(i[0]=t[0]*e[0]+t[4]*e[1]+t[8]*e[2]+t[12]*e[3],i[4]=t[0]*e[4]+t[4]*e[5]+t[8]*e[6]+t[12]*e[7],i[8]=t[0]*e[8]+t[4]*e[9]+t[8]*e[10]+t[12]*e[11],i[12]=t[0]*e[12]+t[4]*e[13]+t[8]*e[14]+t[12]*e[15],i[1]=t[1]*e[0]+t[5]*e[1]+t[9]*e[2]+t[13]*e[3],i[5]=t[1]*e[4]+t[5]*e[5]+t[9]*e[6]+t[13]*e[7],i[9]=t[1]*e[8]+t[5]*e[9]+t[9]*e[10]+t[13]*e[11],i[13]=t[1]*e[12]+t[5]*e[13]+t[9]*e[14]+t[13]*e[15],i[2]=t[2]*e[0]+t[6]*e[1]+t[10]*e[2]+t[14]*e[3],i[6]=t[2]*e[4]+t[6]*e[5]+t[10]*e[6]+t[14]*e[7],i[10]=t[2]*e[8]+t[6]*e[9]+t[10]*e[10]+t[14]*e[11],i[14]=t[2]*e[12]+t[6]*e[13]+t[10]*e[14]+t[14]*e[15],i[3]=t[3]*e[0]+t[7]*e[1]+t[11]*e[2]+t[15]*e[3],i[7]=t[3]*e[4]+t[7]*e[5]+t[11]*e[6]+t[15]*e[7],i[11]=t[3]*e[8]+t[7]*e[9]+t[11]*e[10]+t[15]*e[11],i[15]=t[3]*e[12]+t[7]*e[13]+t[11]*e[14]+t[15]*e[15])},O.prototype.translate=function(t,e,i){this.m[12]=this.m[0]*t+this.m[4]*e+this.m[8]*i+this.m[12],this.m[13]=this.m[1]*t+this.m[5]*e+this.m[9]*i+this.m[13],this.m[14]=this.m[2]*t+this.m[6]*e+this.m[10]*i+this.m[14],this.m[15]=this.m[3]*t+this.m[7]*e+this.m[11]*i+this.m[15]},O.prototype.scale=function(t,e,i){this.m[0]*=t,this.m[4]*=e,this.m[8]*=i,this.m[1]*=t,this.m[5]*=e,this.m[9]*=i,this.m[2]*=t,this.m[6]*=e,this.m[10]*=i,this.m[3]*=t,this.m[7]*=e,this.m[11]*=i},O.prototype.rotateX=function(t){var e=vt.fcos(t),i=vt._$9(t),r=this.m[4];this.m[4]=r*e+this.m[8]*i,this.m[8]=r*-i+this.m[8]*e,r=this.m[5],this.m[5]=r*e+this.m[9]*i,this.m[9]=r*-i+this.m[9]*e,r=this.m[6],this.m[6]=r*e+this.m[10]*i,this.m[10]=r*-i+this.m[10]*e,r=this.m[7],this.m[7]=r*e+this.m[11]*i,this.m[11]=r*-i+this.m[11]*e},O.prototype.rotateY=function(t){var e=vt.fcos(t),i=vt._$9(t),r=this.m[0];this.m[0]=r*e+this.m[8]*-i,this.m[8]=r*i+this.m[8]*e,r=this.m[1],this.m[1]=r*e+this.m[9]*-i,this.m[9]=r*i+this.m[9]*e,r=m[2],this.m[2]=r*e+this.m[10]*-i,this.m[10]=r*i+this.m[10]*e,r=m[3],this.m[3]=r*e+this.m[11]*-i,this.m[11]=r*i+this.m[11]*e},O.prototype.rotateZ=function(t){var e=vt.fcos(t),i=vt._$9(t),r=this.m[0];this.m[0]=r*e+this.m[4]*i,this.m[4]=r*-i+this.m[4]*e,r=this.m[1],this.m[1]=r*e+this.m[5]*i,this.m[5]=r*-i+this.m[5]*e,r=this.m[2],this.m[2]=r*e+this.m[6]*i,this.m[6]=r*-i+this.m[6]*e,r=this.m[3],this.m[3]=r*e+this.m[7]*i,this.m[7]=r*-i+this.m[7]*e};function b(t){i||it.prototype.constructor.call(this,t)}b.prototype=new it,b._$tP=new Object,b._$27=function(){b._$tP.clear()},b.getID=function(t){var e=b._$tP[t];return null==e&&(e=new b(t),b._$tP[t]=e),e},b.prototype._$3s=function(){return new b};function R(){i||(this._$7=1,this._$f=0,this._$H=0,this._$g=1,this._$k=0,this._$w=0,this._$hi=STATE_IDENTITY,this._$Z=_$pS)}R._$kS=-1,R._$pS=0,R._$hb=1,R.STATE_IDENTITY=0,R._$gb=1,R._$fo=2,R._$go=4,R.prototype.transform=function(t,e,i){var r,o,n,s,a,_,h=0,l=0;switch(this._$hi){default:return;case R._$go|R._$fo|R._$gb:for(r=this._$7,o=this._$H,n=this._$k,s=this._$f,a=this._$g,_=this._$w;--i>=0;){var $=t[h++],u=t[h++];e[l++]=r*$+o*u+n,e[l++]=s*$+a*u+_}return;case R._$go|R._$fo:for(r=this._$7,o=this._$H,s=this._$f,a=this._$g;--i>=0;){$=t[h++],u=t[h++];e[l++]=r*$+o*u,e[l++]=s*$+a*u}return;case R._$go|R._$gb:for(o=this._$H,n=this._$k,s=this._$f,_=this._$w;--i>=0;){$=t[h++];e[l++]=o*t[h++]+n,e[l++]=s*$+_}return;case R._$go:for(o=this._$H,s=this._$f;--i>=0;){$=t[h++];e[l++]=o*t[h++],e[l++]=s*$}return;case R._$fo|R._$gb:for(r=this._$7,n=this._$k,a=this._$g,_=this._$w;--i>=0;)e[l++]=r*t[h++]+n,e[l++]=a*t[h++]+_;return;case R._$fo:for(r=this._$7,a=this._$g;--i>=0;)e[l++]=r*t[h++],e[l++]=a*t[h++];return;case R._$gb:for(n=this._$k,_=this._$w;--i>=0;)e[l++]=t[h++]+n,e[l++]=t[h++]+_;return;case R.STATE_IDENTITY:return void(t==e&&h==l||A._$jT(t,h,e,l,2*i))}},R.prototype.update=function(){0==this._$H&&0==this._$f?1==this._$7&&1==this._$g?0==this._$k&&0==this._$w?(this._$hi=R.STATE_IDENTITY,this._$Z=R._$pS):(this._$hi=R._$gb,this._$Z=R._$hb):0==this._$k&&0==this._$w?(this._$hi=R._$fo,this._$Z=R._$kS):(this._$hi=R._$fo|R._$gb,this._$Z=R._$kS):0==this._$7&&0==this._$g?0==this._$k&&0==this._$w?(this._$hi=R._$go,this._$Z=R._$kS):(this._$hi=R._$go|R._$gb,this._$Z=R._$kS):0==this._$k&&0==this._$w?(this._$hi=R._$go|R._$fo,this._$Z=R._$kS):(this._$hi=R._$go|R._$fo|R._$gb,this._$Z=R._$kS)},R.prototype._$RT=function(t){this._$IT(t);var e=t[0],i=t[2],r=t[1],o=t[3],n=Math.sqrt(e*e+r*r),s=e*o-i*r;0==n?at._$so&&console.log("affine._$RT() / rt==0"):(t[0]=n,t[1]=s/n,t[2]=(r*o+e*i)/s,t[3]=Math.atan2(r,e))},R.prototype._$ho=function(t,e,i,r){var o=new Float32Array(6),n=new Float32Array(6);t._$RT(o),e._$RT(n);var s=new Float32Array(6);s[0]=o[0]+(n[0]-o[0])*i,s[1]=o[1]+(n[1]-o[1])*i,s[2]=o[2]+(n[2]-o[2])*i,s[3]=o[3]+(n[3]-o[3])*i,s[4]=o[4]+(n[4]-o[4])*i,s[5]=o[5]+(n[5]-o[5])*i,r._$CT(s)},R.prototype._$CT=function(t){var e=Math.cos(t[3]),i=Math.sin(t[3]);this._$7=t[0]*e,this._$f=t[0]*i,this._$H=t[1]*(t[2]*e-i),this._$g=t[1]*(t[2]*i+e),this._$k=t[4],this._$w=t[5],this.update()},R.prototype._$IT=function(t){t[0]=this._$7,t[1]=this._$f,t[2]=this._$H,t[3]=this._$g,t[4]=this._$k,t[5]=this._$w};function F(){i||(s.prototype.constructor.call(this),this.motions=new Array,this._$7r=null,this._$7r=F._$Co++,this._$D0=30,this._$yT=0,this._$E=!0,this.loopFadeIn=!0,this._$AS=-1,_$a0())}F.prototype=new s,F._$cs="VISIBLE:",F._$ar="LAYOUT:",F._$Co=0,F._$D2=[],F._$1T=1,F.loadMotion=function(t){var e=new F,i=[0],r=t.length;e._$yT=0;for(var o=0;o=0){var s=new N;w.startsWith(t,h,F._$cs)?(s._$RP=N._$hs,s._$4P=new String(t,h,l-h)):w.startsWith(t,h,F._$ar)?(s._$4P=new String(t,h+7,l-h-7),w.startsWith(t,h+7,"ANCHOR_X")?s._$RP=N._$xs:w.startsWith(t,h+7,"ANCHOR_Y")?s._$RP=N._$us:w.startsWith(t,h+7,"SCALE_X")?s._$RP=N._$qs:w.startsWith(t,h+7,"SCALE_Y")?s._$RP=N._$Ys:w.startsWith(t,h+7,"X")?s._$RP=N._$ws:w.startsWith(t,h+7,"Y")&&(s._$RP=N._$Ns)):(s._$RP=N._$Fr,s._$4P=new String(t,h,l-h)),e.motions.push(s);var a=0;for(F._$D2.clear(),o=l+1;o0){F._$D2.push(u),a++;var _=i[0];if(_e._$yT&&(e._$yT=a)}}}else{for(var h=o,l=-1;o=0)for(l==h+4&&"f"==t[h+1]&&"p"==t[h+2]&&"s"==t[h+3]&&($=!0),o=l+1;o0&&$&&5=h?h-1:n];t.setParamFloat(l,$)}else if(N._$ws<=_._$RP&&_._$RP<=N._$Ys);else{var u=t.getParamFloat(l),p=_._$I0[n>=h?h-1:n],c=u+(p+(_._$I0[n+1>=h?h-1:n+1]-p)*s-u)*i;t.setParamFloat(l,c)}}n>=this._$yT&&(this._$E?(r._$z2=e,this.loopFadeIn&&(r._$bs=e)):r._$9L=!0)},F.prototype._$r0=function(){return this._$E},F.prototype._$aL=function(t){this._$E=t},F.prototype.isLoopFadeIn=function(){return this.loopFadeIn},F.prototype.setLoopFadeIn=function(t){this.loopFadeIn=t};function C(){this._$P=new Float32Array(100),this.size=0}C.prototype.clear=function(){this.size=0},C.prototype.add=function(t){if(this._$P.length<=this.size){var e=new Float32Array(2*this.size);A._$jT(this._$P,0,e,0,this.size),this._$P=e}this._$P[this.size++]=t},C.prototype._$BL=function(){var t=new Float32Array(this.size);return A._$jT(this._$P,0,t,0,this.size),t};function N(){this._$4P=null,this._$I0=null,this._$RP=null}N._$Fr=0,N._$hs=1,N._$ws=100,N._$Ns=101,N._$xs=102,N._$us=103,N._$qs=104,N._$Ys=105;function B(){}B._$Ms=1,B._$Qs=2,B._$i2=0,B._$No=2,B._$do=B._$Ms,B._$Ls=!0,B._$1r=5,B._$Qb=65,B._$J=1e-4,B._$FT=.001,B._$Ss=3;function G(){}G._$o7=6,G._$S7=7,G._$s7=8,G._$77=9,G.LIVE2D_FORMAT_VERSION_V2_10_SDK2=10,G.LIVE2D_FORMAT_VERSION_V2_11_SDK2_1=11,G._$T7=G.LIVE2D_FORMAT_VERSION_V2_11_SDK2_1,G._$Is=-2004318072,G._$h0=0,G._$4L=23,G._$7P=33,G._$uT=function(t){console.log("_$bo :: _$6 _$mo _$E0 : %d\n",t)},G._$9o=function(t){if(t<40)return G._$uT(t),null;if(t<50)return G._$uT(t),null;if(t<60)return G._$uT(t),null;if(t<100)switch(t){case 65:return new Z;case 66:return new D;case 67:return new I;case 68:return new z;case 69:return new y;case 70:return new lt;default:return G._$uT(t),null}else if(t<150)switch(t){case 131:return new nt;case 133:return new tt;case 136:return new $;case 137:return new rt;case 142:return new j}return G._$uT(t),null};function U(t){i||(this._$QT=!0,this._$co=-1,this._$qo=0,this._$pb=new Array(U._$is),this._$_2=new Float32Array(U._$is),this._$vr=new Float32Array(U._$is),this._$Rr=new Float32Array(U._$is),this._$Or=new Float32Array(U._$is),this._$fs=new Float32Array(U._$is),this._$Js=new Array(U._$is),this._$3S=new Array,this._$aS=new Array,this._$Bo=null,this._$F2=new Array,this._$db=new Array,this._$8b=new Array,this._$Hr=new Array,this._$Ws=null,this._$Vs=null,this._$Er=null,this._$Es=new Int16Array(B._$Qb),this._$ZP=new Float32Array(2*B._$1r),this._$Ri=t,this._$b0=U._$HP++,this.clipManager=null,this.dp_webgl=null)}U._$HP=0,U._$_0=!0,U._$V2=-1,U._$W0=-1,U._$jr=!1,U._$ZS=!0,U._$tr=-1e6,U._$lr=1e6,U._$is=32,U._$e=!1,U.prototype.getDrawDataIndex=function(t){for(var e=this._$aS.length-1;e>=0;--e)if(null!=this._$aS[e]&&this._$aS[e].getDrawDataID()==t)return e;return-1},U.prototype.getDrawData=function(t){if(t instanceof b){if(null==this._$Bo){this._$Bo=new Object;for(var e=this._$aS.length,i=0;i0&&this.release();for(var t=this._$Ri.getModelImpl(),e=t._$Xr(),i=e.length,r=new Array,n=new Array,s=0;s=0)&&(this._$3S.push(m),this._$db.push(n[s]),r[s]=null,y=!0)}}if(!y)break}var P=t._$E2();if(null!=P){var S=P._$1s();if(null!=S){var v=S.length;for(s=0;s=0;e--)this._$Js[e]=U._$jr;return this._$QT=!1,U._$e&&a.dump("_$eL"),!1},U.prototype.preDraw=function(t){null!=this.clipManager&&(t._$ZT(),this.clipManager.setupClip(this,t))},U.prototype.draw=function(t){if(null!=this._$Ws){var e=this._$Ws.length;t._$ZT();for(var i=0;i=0;--e)if(this._$pb[e]==t)return e;return this._$02(t,0,U._$tr,U._$lr)},U.prototype._$BS=function(t){return this.getBaseDataIndex(t)},U.prototype.getBaseDataIndex=function(t){for(var e=this._$3S.length-1;e>=0;--e)if(null!=this._$3S[e]&&this._$3S[e].getBaseDataID()==t)return e;return-1},U.prototype._$UT=function(t,e){var i=new Float32Array(e);return A._$jT(t,0,i,0,t.length),i},U.prototype._$02=function(t,e,i,r){if(this._$qo>=this._$pb.length){var o=this._$pb.length,n=new Array(2*o);A._$jT(this._$pb,0,n,0,o),this._$pb=n,this._$_2=this._$UT(this._$_2,2*o),this._$vr=this._$UT(this._$vr,2*o),this._$Rr=this._$UT(this._$Rr,2*o),this._$Or=this._$UT(this._$Or,2*o);var s=new Array;A._$jT(this._$Js,0,s,0,o),this._$Js=s}return this._$pb[this._$qo]=t,this._$_2[this._$qo]=e,this._$vr[this._$qo]=e,this._$Rr[this._$qo]=i,this._$Or[this._$qo]=r,this._$Js[this._$qo]=U._$ZS,this._$qo++},U.prototype._$Zo=function(t,e){this._$3S[t]=e},U.prototype.setParamFloat=function(t,e){ethis._$Or[t]&&(e=this._$Or[t]),this._$_2[t]=e},U.prototype.loadParam=function(){var t=this._$_2.length;t>this._$fs.length&&(t=this._$fs.length),A._$jT(this._$fs,0,this._$_2,0,t)},U.prototype.saveParam=function(){var t=this._$_2.length;t>this._$fs.length&&(this._$fs=new Float32Array(t)),A._$jT(this._$_2,0,this._$fs,0,t)},U.prototype._$v2=function(){return this._$co},U.prototype._$WS=function(){return this._$QT},U.prototype._$Xb=function(t){return this._$Js[t]==U._$ZS},U.prototype._$vs=function(){return this._$Es},U.prototype._$Tr=function(){return this._$ZP},U.prototype.getBaseData=function(t){return this._$3S[t]},U.prototype.getParamFloat=function(t){return this._$_2[t]},U.prototype.getParamMax=function(t){return this._$Or[t]},U.prototype.getParamMin=function(t){return this._$Rr[t]},U.prototype.setPartsOpacity=function(t,e){this._$Hr[t].setPartsOpacity(e)},U.prototype.getPartsOpacity=function(t){return this._$Hr[t].getPartsOpacity()},U.prototype.getPartsDataIndex=function(t){for(var e=this._$F2.length-1;e>=0;--e)if(null!=this._$F2[e]&&this._$F2[e]._$p2()==t)return e;return-1},U.prototype._$q2=function(t){return this._$db[t]},U.prototype._$C2=function(t){return this._$8b[t]},U.prototype._$Bb=function(t){return this._$Hr[t]},U.prototype._$5s=function(t,e){for(var i=this._$Ws.length,r=t,o=0;o0;)n+=e;return r},Y._$C=function(t){var e=null,i=null;try{e=t instanceof Array?t:new _$Xs(t,8192),i=new _$js;for(var r,o=new Int8Array(1e3);(r=e.read(o))>0;)i.write(o,0,r);return i._$TS()}finally{null!=t&&t.close(),null!=i&&(i.flush(),i.close())}};function k(){i||(this._$12=null,this._$bb=null,this._$_L=null,this._$jo=null,this._$iL=null,this._$0L=null,this._$Br=null,this._$Dr=null,this._$Cb=null,this._$mr=null,this._$_L=V.STATE_FIRST,this._$Br=4e3,this._$Dr=100,this._$Cb=50,this._$mr=150,this._$jo=!0,this._$iL="PARAM_EYE_L_OPEN",this._$0L="PARAM_EYE_R_OPEN")}k.prototype._$T2=function(){return A.getUserTimeMSec()+Math._$10()*(2*this._$Br-1)},k.prototype._$uo=function(t){this._$Br=t},k.prototype._$QS=function(t,e,i){this._$Dr=t,this._$Cb=e,this._$mr=i},k.prototype._$7T=function(t){var e,i=A.getUserTimeMSec(),r=0;switch(this._$_L){case STATE_CLOSING:(r=(i-this._$bb)/this._$Dr)>=1&&(r=1,this._$_L=V.STATE_CLOSED,this._$bb=i),e=1-r;break;case STATE_CLOSED:(r=(i-this._$bb)/this._$Cb)>=1&&(this._$_L=V.STATE_OPENING,this._$bb=i),e=0;break;case STATE_OPENING:(r=(i-this._$bb)/this._$mr)>=1&&(r=1,this._$_L=V.STATE_INTERVAL,this._$12=this._$T2()),e=r;break;case STATE_INTERVAL:this._$12.9?at.EXPAND_W:0;this.gl.drawElements(_,i,r,o,n,h,this.transform,a)}},X.prototype._$Rs=function(){throw new Error("_$Rs")},X.prototype._$Ds=function(t){throw new Error("_$Ds")},X.prototype._$K2=function(){for(var t=0;t=0;--e){var i=t[e];iW._$R2&&(W._$R2=i)}},W._$or=function(){return W._$52},W._$Pr=function(){return W._$R2},W.prototype._$F0=function(t){this._$gP=t._$nP(),this._$dr=t._$nP(),this._$GS=t._$nP(),this._$qb=t._$6L(),this._$Lb=t._$cS(),this._$mS=t._$Tb(),t.getFormatVersion()>=G._$T7?(this.clipID=t._$nP(),this.clipIDList=this.convertClipIDForV2_11(this.clipID)):this.clipIDList=null,W._$Sb(this._$Lb)},W.prototype.getClipIDList=function(){return this.clipIDList},W.prototype._$Nr=function(t,e){if(e._$IS[0]=!1,e._$Us=S._$Z2(t,this._$GS,e._$IS,this._$Lb),at._$Zs);else if(e._$IS[0])return;e._$7s=S._$br(t,this._$GS,e._$IS,this._$mS)},W.prototype._$2b=function(t){},W.prototype.getDrawDataID=function(){return this._$gP},W.prototype._$j2=function(t){this._$gP=t},W.prototype.getOpacity=function(t,e){return e._$7s},W.prototype._$zS=function(t,e){return e._$Us},W.prototype.getTargetBaseDataID=function(){return this._$dr},W.prototype._$gs=function(t){this._$dr=t},W.prototype._$32=function(){return null!=this._$dr&&this._$dr!=dt._$2o()},W.prototype.getType=function(){};function j(){i||(this._$NL=null,this._$3S=null,this._$aS=null,j._$42++)}j._$42=0,j.prototype._$1b=function(){return this._$3S},j.prototype.getDrawDataList=function(){return this._$aS},j.prototype._$F0=function(t){this._$NL=t._$nP(),this._$aS=t._$nP(),this._$3S=t._$nP()},j.prototype._$kr=function(t){t._$Zo(this._$3S),t._$xo(this._$aS),this._$3S=null,this._$aS=null};function q(){i||(r.prototype.constructor.call(this),this._$zo=new X)}q.prototype=new r,q.loadModel=function(t){var e=new q;return r._$62(e,t),e},q.loadModel=function(t){var e=new q;return r._$62(e,t),e},q._$to=function(){return new q},q._$er=function(t){var e=new _$5("../_$_r/_$t0/_$Ri/_$_P._$d");if(0==e.exists())throw new _$ls("_$t0 _$_ _$6 _$Ui :: "+e._$PL());for(var i=["../_$_r/_$t0/_$Ri/_$_P.512/_$CP._$1","../_$_r/_$t0/_$Ri/_$_P.512/_$vP._$1","../_$_r/_$t0/_$Ri/_$_P.512/_$EP._$1","../_$_r/_$t0/_$Ri/_$_P.512/_$pP._$1"],r=q.loadModel(e._$3b()),o=0;o=0){var a=new N;w.startsWith(t,$,J._$cs)?(a._$RP=N._$hs,a._$4P=w.createString(t,$,u-$)):w.startsWith(t,$,J._$ar)?(a._$4P=w.createString(t,$+7,u-$-7),w.startsWith(t,$+7,"ANCHOR_X")?a._$RP=N._$xs:w.startsWith(t,$+7,"ANCHOR_Y")?a._$RP=N._$us:w.startsWith(t,$+7,"SCALE_X")?a._$RP=N._$qs:w.startsWith(t,$+7,"SCALE_Y")?a._$RP=N._$Ys:w.startsWith(t,$+7,"X")?a._$RP=N._$ws:w.startsWith(t,$+7,"Y")&&(a._$RP=N._$Ns)):(a._$RP=N._$Fr,a._$4P=w.createString(t,$,u-$)),e.motions.push(a);var _=0,h=[];for(o=u+1;o0){h.push(c),_++;var l=i[0];if(le._$yT&&(e._$yT=_)}}}else{for(var $=o,u=-1;o=0)for(u==$+4&&"f"==Q(t,$+1)&&"p"==Q(t,$+2)&&"s"==Q(t,$+3)&&(p=!0),o=u+1;o0&&p&&5=h?h-1:n];t.setParamFloat(l,$)}else if(N._$ws<=_._$RP&&_._$RP<=N._$Ys);else{var u=t.getParamIndex(l),p=t.getModelContext(),c=.4*(p.getParamMax(u)-p.getParamMin(u)),f=p.getParamFloat(u),g=_._$I0[n>=h?h-1:n],d=_._$I0[n+1>=h?h-1:n+1],y=f+((gc||g>d&&g-d>c?g:g+(d-g)*s)-f)*i;t.setParamFloat(l,y)}}n>=this._$yT&&(this._$E?(r._$z2=e,this.loopFadeIn&&(r._$bs=e)):r._$9L=!0),this._$eP=i},J.prototype._$r0=function(){return this._$E},J.prototype._$aL=function(t){this._$E=t},J.prototype._$S0=function(){return this._$D0},J.prototype._$U0=function(t){this._$D0=t},J.prototype.isLoopFadeIn=function(){return this.loopFadeIn},J.prototype.setLoopFadeIn=function(t){this.loopFadeIn=t};function C(){this._$P=new Float32Array(100),this.size=0}C.prototype.clear=function(){this.size=0},C.prototype.add=function(t){if(this._$P.length<=this.size){var e=new Float32Array(2*this.size);A._$jT(this._$P,0,e,0,this.size),this._$P=e}this._$P[this.size++]=t},C.prototype._$BL=function(){var t=new Float32Array(this.size);return A._$jT(this._$P,0,t,0,this.size),t};function N(){this._$4P=null,this._$I0=null,this._$RP=null}N._$Fr=0,N._$hs=1,N._$ws=100,N._$Ns=101,N._$xs=102,N._$us=103,N._$qs=104,N._$Ys=105;function Z(){i||(x.prototype.constructor.call(this),this._$o=0,this._$A=0,this._$GS=null,this._$Eo=null)}Z.prototype=new x,Z._$gT=new Array,Z.prototype._$zP=function(){this._$GS=new D,this._$GS._$zP()},Z.prototype._$F0=function(t){x.prototype._$F0.call(this,t),this._$A=t._$6L(),this._$o=t._$6L(),this._$GS=t._$nP(),this._$Eo=t._$nP(),x.prototype.readV2_opacity.call(this,t)},Z.prototype.init=function(t){var e=new K(this),i=(this._$o+1)*(this._$A+1);return null!=e._$Cr&&(e._$Cr=null),e._$Cr=new Float32Array(2*i),null!=e._$hr&&(e._$hr=null),this._$32()?e._$hr=new Float32Array(2*i):e._$hr=null,e},Z.prototype._$Nr=function(t,e){var i=e;if(this._$GS._$Ur(t)){var r=this._$VT(),o=Z._$gT;o[0]=!1,S._$Vr(t,this._$GS,o,r,this._$Eo,i._$Cr,0,2),e._$Ib(o[0]),this.interpolateOpacity(t,this._$GS,e,o)}},Z.prototype._$2b=function(t,e){var i=e;if(i._$hS(!0),this._$32()){var r=this.getTargetBaseDataID();if(i._$8r==x._$ur&&(i._$8r=t.getBaseDataIndex(r)),i._$8r<0)at._$so&&a._$li("_$L _$0P _$G :: %s",r),i._$hS(!1);else{var o=t.getBaseData(i._$8r),n=t._$q2(i._$8r);if(null!=o&&n._$yo()){var s=n.getTotalScale();i.setTotalScale_notForClient(s);var _=n.getTotalOpacity();i.setTotalOpacity(_*i.getInterpolatedOpacity()),o._$nb(t,n,i._$Cr,i._$hr,this._$VT(),0,2),i._$hS(!0)}else i._$hS(!1)}}else i.setTotalOpacity(i.getInterpolatedOpacity())},Z.prototype._$nb=function(t,e,i,r,o,n,s){var a=e,_=null!=a._$hr?a._$hr:a._$Cr;Z.transformPoints_sdk2(i,r,o,n,s,_,this._$o,this._$A)},Z.transformPoints_sdk2=function(e,i,r,o,n,s,a,_){for(var h,l,$,u=r*n,p=0,c=0,f=0,g=0,d=0,y=0,m=!1,T=o;T=1){R=s[2*(0+_*M)],F=s[2*(0+_*M)+1],C=p-2*f+1*d,N=c-2*g+1*y,w=p+3*d,D=c+3*y,O=p-2*f+3*d,b=c-2*g+3*y;(B=.5*(v- -2))+(G=.5*(L-1))<=1?(i[T]=C+(R-C)*B+(O-C)*G,i[T+1]=N+(F-N)*B+(b-N)*G):(i[T]=w+(O-w)*(1-B)+(R-w)*(1-G),i[T+1]=D+(b-D)*(1-B)+(F-D)*(1-G))}else{(k=0|S)==_&&(k=_-1);var B=.5*(v- -2),G=S-k,U=k/_,Y=(k+1)/_;R=s[2*(0+k*M)],F=s[2*(0+k*M)+1],w=s[2*(0+(k+1)*M)],D=s[2*(0+(k+1)*M)+1],C=p-2*f+U*d,N=c-2*g+U*y,O=p-2*f+Y*d,b=c-2*g+Y*y;B+G<=1?(i[T]=C+(R-C)*B+(O-C)*G,i[T+1]=N+(F-N)*B+(b-N)*G):(i[T]=w+(O-w)*(1-B)+(R-w)*(1-G),i[T+1]=D+(b-D)*(1-B)+(F-D)*(1-G))}else if(1<=v)if(L<=0){O=s[2*(a+0*M)],b=s[2*(a+0*M)+1],w=p+3*f,D=c+3*g,C=p+1*f-2*d,N=c+1*g-2*y,R=p+3*f-2*d,F=c+3*g-2*y;(B=.5*(v-1))+(G=.5*(L- -2))<=1?(i[T]=C+(R-C)*B+(O-C)*G,i[T+1]=N+(F-N)*B+(b-N)*G):(i[T]=w+(O-w)*(1-B)+(R-w)*(1-G),i[T+1]=D+(b-D)*(1-B)+(F-D)*(1-G))}else if(L>=1){C=s[2*(a+_*M)],N=s[2*(a+_*M)+1],R=p+3*f+1*d,F=c+3*g+1*y,O=p+1*f+3*d,b=c+1*g+3*y,w=p+3*f+3*d,D=c+3*g+3*y;(B=.5*(v-1))+(G=.5*(L-1))<=1?(i[T]=C+(R-C)*B+(O-C)*G,i[T+1]=N+(F-N)*B+(b-N)*G):(i[T]=w+(O-w)*(1-B)+(R-w)*(1-G),i[T+1]=D+(b-D)*(1-B)+(F-D)*(1-G))}else{var k;(k=0|S)==_&&(k=_-1);B=.5*(v-1),G=S-k,U=k/_,Y=(k+1)/_,C=s[2*(a+k*M)],N=s[2*(a+k*M)+1],O=s[2*(a+(k+1)*M)],b=s[2*(a+(k+1)*M)+1],R=p+3*f+U*d,F=c+3*g+U*y,w=p+3*f+Y*d,D=c+3*g+Y*y;B+G<=1?(i[T]=C+(R-C)*B+(O-C)*G,i[T+1]=N+(F-N)*B+(b-N)*G):(i[T]=w+(O-w)*(1-B)+(R-w)*(1-G),i[T+1]=D+(b-D)*(1-B)+(F-D)*(1-G))}else if(L<=0){(z=0|P)==a&&(z=a-1);B=P-z,G=.5*(L- -2);var V=z/a,X=(z+1)/a;O=s[2*(z+0*M)],b=s[2*(z+0*M)+1],w=s[2*(z+1+0*M)],D=s[2*(z+1+0*M)+1],C=p+V*f-2*d,N=c+V*g-2*y,R=p+X*f-2*d,F=c+X*g-2*y;B+G<=1?(i[T]=C+(R-C)*B+(O-C)*G,i[T+1]=N+(F-N)*B+(b-N)*G):(i[T]=w+(O-w)*(1-B)+(R-w)*(1-G),i[T+1]=D+(b-D)*(1-B)+(F-D)*(1-G))}else if(L>=1){var z;(z=0|P)==a&&(z=a-1);B=P-z,G=.5*(L-1),V=z/a,X=(z+1)/a,C=s[2*(z+_*M)],N=s[2*(z+_*M)+1],R=s[2*(z+1+_*M)],F=s[2*(z+1+_*M)+1],O=p+V*f+3*d,b=c+V*g+3*y,w=p+X*f+3*d,D=c+X*g+3*y;B+G<=1?(i[T]=C+(R-C)*B+(O-C)*G,i[T+1]=N+(F-N)*B+(b-N)*G):(i[T]=w+(O-w)*(1-B)+(R-w)*(1-G),i[T+1]=D+(b-D)*(1-B)+(F-D)*(1-G))}else t.err.printf("_$li calc : %.4f , %.4f @@BDBoxGrid\n",v,L);else i[T]=p+v*f+L*d,i[T+1]=c+v*g+L*y}else h=2*((0|P)+(0|S)*(a+1)),(l=P-(0|P))+($=S-(0|S))<1?(i[T]=s[h]*(1-l-$)+s[h+2]*l+s[h+2*(a+1)]*$,i[T+1]=s[h+1]*(1-l-$)+s[h+3]*l+s[h+2*(a+1)+1]*$):(i[T]=s[h+2*(a+1)+2]*(l-1+$)+s[h+2*(a+1)]*(1-l)+s[h+2]*(1-$),i[T+1]=s[h+2*(a+1)+3]*(l-1+$)+s[h+2*(a+1)+1]*(1-l)+s[h+3]*(1-$))}},Z.prototype.transformPoints_sdk1=function(t,e,i,r,o,n,s){for(var a,_,h,l,$,u,p,c=e,f=this._$o,g=this._$A,d=o*s,y=null!=c._$hr?c._$hr:c._$Cr,m=n;m1&&(a=1),_<0?_=0:_>1&&(_=1),l=0|(_*=g),(h=0|(a*=f))>f-1&&(h=f-1),l>g-1&&(l=g-1),u=a-h,p=_-l,$=2*(h+l*(f+1))):(u=(a=i[m]*f)-(0|a),p=(_=i[m+1]*g)-(0|_),$=2*((0|a)+(0|_)*(f+1))),u+p<1?(r[m]=y[$]*(1-u-p)+y[$+2]*u+y[$+2*(f+1)]*p,r[m+1]=y[$+1]*(1-u-p)+y[$+3]*u+y[$+2*(f+1)+1]*p):(r[m]=y[$+2*(f+1)+2]*(u-1+p)+y[$+2*(f+1)]*(1-u)+y[$+2]*(1-p),r[m+1]=y[$+2*(f+1)+3]*(u-1+p)+y[$+2*(f+1)+1]*(1-u)+y[$+3]*(1-p))},Z.prototype._$VT=function(){return(this._$o+1)*(this._$A+1)},Z.prototype.getType=function(){return x._$_b};function K(t){st.prototype.constructor.call(this,t),this._$8r=x._$ur,this._$Cr=null,this._$hr=null}K.prototype=new st;function tt(){i||(this.visible=!0,this._$g0=!1,this._$NL=null,this._$3S=null,this._$aS=null,tt._$42++)}tt._$42=0,tt.prototype._$zP=function(){this._$3S=new Array,this._$aS=new Array},tt.prototype._$F0=function(t){this._$g0=t._$8L(),this.visible=t._$8L(),this._$NL=t._$nP(),this._$3S=t._$nP(),this._$aS=t._$nP()},tt.prototype.init=function(t){var e=new et(this);return e.setPartsOpacity(this.isVisible()?1:0),e},tt.prototype._$6o=function(t){if(null==this._$3S)throw new Error("_$3S _$6 _$Wo@_$6o");this._$3S.push(t)},tt.prototype._$3o=function(t){if(null==this._$aS)throw new Error("_$aS _$6 _$Wo@_$3o");this._$aS.push(t)},tt.prototype._$Zo=function(t){this._$3S=t},tt.prototype._$xo=function(t){this._$aS=t},tt.prototype.isVisible=function(){return this.visible},tt.prototype._$uL=function(){return this._$g0},tt.prototype._$KP=function(t){this.visible=t},tt.prototype._$ET=function(t){this._$g0=t},tt.prototype.getBaseData=function(){return this._$3S},tt.prototype.getDrawData=function(){return this._$aS},tt.prototype._$p2=function(){return this._$NL},tt.prototype._$ob=function(t){this._$NL=t},tt.prototype.getPartsID=function(){return this._$NL},tt.prototype._$MP=function(t){this._$NL=t};function et(t){this._$VS=null,this._$e0=null,this._$e0=t}et.prototype=new function(){},et.prototype.getPartsOpacity=function(){return this._$VS},et.prototype.setPartsOpacity=function(t){this._$VS=t};function it(t){i||(this.id=t)}it._$L7=function(){l._$27(),dt._$27(),b._$27(),h._$27()},it.prototype.toString=function(){return this.id};function rt(){i||(this._$4S=null)}rt.prototype._$1s=function(){return this._$4S},rt.prototype._$zP=function(){this._$4S=new Array},rt.prototype._$F0=function(t){this._$4S=t._$nP()},rt.prototype._$Ks=function(t){this._$4S.push(t)};function ot(t,e){this.canvas=t,this.context=e,this.viewport=new Array(0,0,t.width,t.height),this._$6r=1,this._$xP=0,this._$3r=1,this._$uP=0,this._$Qo=-1,this.cacheImages={}}ot.tr=new gt,ot._$50=new gt,ot._$Ti=new Array(0,0),ot._$Pi=new Array(0,0),ot._$B=new Array(0,0),ot.prototype._$lP=function(t,e,i,r){this.viewport=new Array(t,e,i,r)},ot.prototype._$bL=function(){this.context.save();var t=this.viewport;null!=t&&(this.context.beginPath(),this.context._$Li(t[0],t[1],t[2],t[3]),this.context.clip())},ot.prototype._$ei=function(){this.context.restore()},ot.prototype.drawElements=function(t,e,i,r,o,n,s,_){try{o!=this._$Qo&&(this._$Qo=o,this.context.globalAlpha=o);for(var h=e.length,l=t.width,$=t.height,u=this.context,p=this._$xP,c=this._$uP,f=this._$6r,g=this._$3r,d=ot.tr,y=ot._$Ti,m=ot._$Pi,P=ot._$B,S=0;S.02?ot.expandClip(t,e,i,r,l,$,u,p,c,f):ot.clipWithTransform(t,null,o,n,s,a,_,h)},ot.expandClip=function(t,e,i,r,o,n,s,a,_,h){var l=s-o,$=a-n,u=_-o,p=h-n,c=l*p-$*u>0?i:-i,f=-$,g=l,d=_-s,y=h-a,m=-y,T=d,P=Math.sqrt(d*d+y*y),S=-p,v=u,L=Math.sqrt(u*u+p*p),M=o-c*f/r,E=n-c*g/r,x=s-c*f/r,A=a-c*g/r,I=s-c*m/P,w=a-c*T/P,D=_-c*m/P,O=h-c*T/P,b=o+c*S/L,R=n+c*v/L,F=_+c*S/L,C=h+c*v/L,N=ot._$50;return null!=e._$P2(N)&&(ot.clipWithTransform(t,N,M,E,x,A,I,w,D,O,F,C,b,R),!0)},ot.clipWithTransform=function(t,e,i,r,o,n,s,_){if(arguments.length<7)a._$li("err : @LDGL.clip()");else if(arguments[1]instanceof gt){var h=ot._$B,l=e,$=arguments;if(t.beginPath(),l){l._$PS($[2],$[3],h),t.moveTo(h[0],h[1]);for(var u=4;u<$.length;u+=2)l._$PS($[u],$[u+1],h),t.lineTo(h[0],h[1])}else{t.moveTo($[2],$[3]);for(u=4;u<$.length;u+=2)t.lineTo($[u],$[u+1])}t.clip()}else a._$li("err : a[0] is _$6 LDTransform @LDGL.clip()")},ot.createCanvas=function(t,e){var i=document.createElement("canvas");return i.setAttribute("width",t),i.setAttribute("height",e),i||a._$li("err : "+i),i},ot.dumpValues=function(){for(var t="",e=0;e1?1:.5-.5*Math.cos(t*vt.PI_F)};function ht(t){i||(this._$ib=t)}ht._$fr=-1,ht.prototype.toString=function(){return this._$ib};function lt(){i||(W.prototype.constructor.call(this),this._$LP=-1,this._$d0=0,this._$Yo=0,this._$JP=null,this._$5P=null,this._$BP=null,this._$Eo=null,this._$Qi=null,this._$6s=lt._$ms,this.culling=!0,this.gl_cacheImage=null,this.instanceNo=lt._$42++)}lt.prototype=new W,lt._$42=0,lt._$Os=30,lt._$ms=0,lt._$ns=1,lt._$_s=2,lt._$gT=new Array,lt.prototype._$_S=function(t){this._$LP=t},lt.prototype.getTextureNo=function(){return this._$LP},lt.prototype._$ZL=function(){return this._$Qi},lt.prototype._$H2=function(){return this._$JP},lt.prototype.getNumPoints=function(){return this._$d0},lt.prototype.getType=function(){return W._$wb},lt.prototype._$B2=function(t,e,i){var r=e,o=null!=r._$hr?r._$hr:r._$Cr;switch(B._$do){default:case B._$Ms:throw new Error("_$L _$ro ");case B._$Qs:for(var n=this._$d0-1;n>=0;--n){o[n*B._$No+4]=i}}},lt.prototype._$zP=function(){this._$GS=new D,this._$GS._$zP()},lt.prototype._$F0=function(t){W.prototype._$F0.call(this,t),this._$LP=t._$6L(),this._$d0=t._$6L(),this._$Yo=t._$6L();var e=t._$nP();this._$BP=new Int16Array(3*this._$Yo);for(var i=3*this._$Yo-1;i>=0;--i)this._$BP[i]=e[i];if(this._$Eo=t._$nP(),this._$Qi=t._$nP(),t.getFormatVersion()>=G._$s7){if(this._$JP=t._$6L(),0!=this._$JP){if(0!=(1&this._$JP)){var r=t._$6L();null==this._$5P&&(this._$5P=new Object),this._$5P._$Hb=parseInt(r)}0!=(this._$JP<._$Os)?this._$6s=(this._$JP<._$Os)>>1:this._$6s=lt._$ms,0!=(32&this._$JP)&&(this.culling=!1)}}else this._$JP=0},lt.prototype.init=function(t){var e=new $t(this),i=this._$d0*B._$No,r=this._$32();null!=e._$Cr&&(e._$Cr=null),e._$Cr=new Float32Array(i),null!=e._$hr&&(e._$hr=null),e._$hr=r?new Float32Array(i):null;switch(B._$do){default:case B._$Ms:if(B._$Ls)for(var o=this._$d0-1;o>=0;--o){var n=o<<1;this._$Qi[n+1]=1-this._$Qi[n+1]}break;case B._$Qs:for(o=this._$d0-1;o>=0;--o){n=o<<1;var s=o*B._$No,a=this._$Qi[n],_=this._$Qi[n+1];e._$Cr[s]=a,e._$Cr[s+1]=_,e._$Cr[s+4]=0,r&&(e._$hr[s]=a,e._$hr[s+1]=_,e._$hr[s+4]=0)}}return e},lt.prototype._$Nr=function(t,e){var i=e;if(this!=i._$GT()&&console.log("### assert!! ### "),this._$GS._$Ur(t)&&(W.prototype._$Nr.call(this,t,i),!i._$IS[0])){var r=lt._$gT;r[0]=!1,S._$Vr(t,this._$GS,r,this._$d0,this._$Eo,i._$Cr,B._$i2,B._$No)}},lt.prototype._$2b=function(t,e){try{this!=e._$GT()&&console.log("### assert!! ### ");var i=!1;e._$IS[0]&&(i=!0);var r=e;if(!i&&(W.prototype._$2b.call(this,t),this._$32())){var o=this.getTargetBaseDataID();if(r._$8r==W._$ur&&(r._$8r=t.getBaseDataIndex(o)),r._$8r<0)at._$so&&a._$li("_$L _$0P _$G :: %s",o);else{var n=t.getBaseData(r._$8r),s=t._$q2(r._$8r);null==n||s._$x2()?r._$AT=!1:(n._$nb(t,s,r._$Cr,r._$hr,this._$d0,B._$i2,B._$No),r._$AT=!0),r.baseOpacity=s.getTotalOpacity()}}}catch(t){throw t}},lt.prototype.draw=function(t,e,i){if(this!=i._$GT()&&console.log("### assert!! ### "),!i._$IS[0]){var r=i,o=this._$LP;o<0&&(o=1);var n=this.getOpacity(e,r)*i._$VS*i.baseOpacity,s=null!=r._$hr?r._$hr:r._$Cr;t.setClipBufPre_clipContextForDraw(i.clipBufPre_clipContext),t._$WP(this.culling),t._$Uo(o,3*this._$Yo,this._$BP,s,this._$Qi,n,this._$6s,r)}},lt.prototype.dump=function(){console.log(" _$yi( %d ) , _$d0( %d ) , _$Yo( %d ) \n",this._$LP,this._$d0,this._$Yo),console.log(" _$Oi _$di = { ");for(var t=0;tstartMotion() / start _$K _$3 (m%d)\n",r,i._$sr));if(null==t)return-1;(i=new ft)._$w0=t,this.motions.push(i);var n=i._$sr;return this._$eb&&a._$Ji("MotionQueueManager[size:%2d]->startMotion() / new _$w0 (m%d)\n",r,n),n},ct.prototype.updateParam=function(t){try{for(var e=!1,i=0;iupdateParam() / _$T0 _$w0 (m%d)\n",this.motions.length-1,r._$sr),this.motions.splice(i,1),i--)):(this.motions=this.motions.splice(i,1),i--)}else this.motions.splice(i,1),i--}return e}catch(t){return a._$li(t),!0}},ct.prototype.isFinished=function(t){if(arguments.length>=1){for(var e=0;e.9&&at.EXPAND_W;var _=this.gl;if(null==this.gl)throw new Error("gl is null");var h=1*this._$C0*n,l=1*this._$tT*n,$=1*this._$WL*n,u=this._$lT*n;if(null!=this.clipBufPre_clipContextMask){_.frontFace(_.CCW),_.useProgram(this.shaderProgram),this._$vS=mt(_,this._$vS,r),this._$no=Tt(_,this._$no,i),_.enableVertexAttribArray(this.a_position_Loc),_.vertexAttribPointer(this.a_position_Loc,2,_.FLOAT,!1,0,0),this._$NT=mt(_,this._$NT,o),_.activeTexture(_.TEXTURE1),_.bindTexture(_.TEXTURE_2D,this.textures[t]),_.uniform1i(this.s_texture0_Loc,1),_.enableVertexAttribArray(this.a_texCoord_Loc),_.vertexAttribPointer(this.a_texCoord_Loc,2,_.FLOAT,!1,0,0),_.uniformMatrix4fv(this.u_matrix_Loc,!1,this.getClipBufPre_clipContextMask().matrixForMask);var p=this.getClipBufPre_clipContextMask().layoutChannelNo,c=this.getChannelFlagAsColor(p);_.uniform4f(this.u_channelFlag,c.r,c.g,c.b,c.a);var f=this.getClipBufPre_clipContextMask().layoutBounds;_.uniform4f(this.u_baseColor_Loc,2*f.x-1,2*f.y-1,2*f._$EL()-1,2*f._$5T()-1),_.uniform1i(this.u_maskFlag_Loc,!0)}else if(null!=this.getClipBufPre_clipContextDraw()){_.useProgram(this.shaderProgramOff),this._$vS=mt(_,this._$vS,r),this._$no=Tt(_,this._$no,i),_.enableVertexAttribArray(this.a_position_Loc_Off),_.vertexAttribPointer(this.a_position_Loc_Off,2,_.FLOAT,!1,0,0),this._$NT=mt(_,this._$NT,o),_.activeTexture(_.TEXTURE1),_.bindTexture(_.TEXTURE_2D,this.textures[t]),_.uniform1i(this.s_texture0_Loc_Off,1),_.enableVertexAttribArray(this.a_texCoord_Loc_Off),_.vertexAttribPointer(this.a_texCoord_Loc_Off,2,_.FLOAT,!1,0,0),_.uniformMatrix4fv(this.u_clipMatrix_Loc_Off,!1,this.getClipBufPre_clipContextDraw().matrixForDraw),_.uniformMatrix4fv(this.u_matrix_Loc_Off,!1,this.matrix4x4),_.activeTexture(_.TEXTURE2),_.bindTexture(_.TEXTURE_2D,at.fTexture[this.glno]),_.uniform1i(this.s_texture1_Loc_Off,2);p=this.getClipBufPre_clipContextDraw().layoutChannelNo,c=this.getChannelFlagAsColor(p);_.uniform4f(this.u_channelFlag_Loc_Off,c.r,c.g,c.b,c.a),_.uniform4f(this.u_baseColor_Loc_Off,h,l,$,u)}else _.useProgram(this.shaderProgram),this._$vS=mt(_,this._$vS,r),this._$no=Tt(_,this._$no,i),_.enableVertexAttribArray(this.a_position_Loc),_.vertexAttribPointer(this.a_position_Loc,2,_.FLOAT,!1,0,0),this._$NT=mt(_,this._$NT,o),_.activeTexture(_.TEXTURE1),_.bindTexture(_.TEXTURE_2D,this.textures[t]),_.uniform1i(this.s_texture0_Loc,1),_.enableVertexAttribArray(this.a_texCoord_Loc),_.vertexAttribPointer(this.a_texCoord_Loc,2,_.FLOAT,!1,0,0),_.uniformMatrix4fv(this.u_matrix_Loc,!1,this.matrix4x4),_.uniform4f(this.u_baseColor_Loc,h,l,$,u),_.uniform1i(this.u_maskFlag_Loc,!1);this.culling?this.gl.enable(_.CULL_FACE):this.gl.disable(_.CULL_FACE),this.gl.enable(_.BLEND);var g,d,y,m;if(null!=this.clipBufPre_clipContextMask)g=_.ONE,d=_.ONE_MINUS_SRC_ALPHA,y=_.ONE,m=_.ONE_MINUS_SRC_ALPHA;else switch(s){case lt._$ms:g=_.ONE,d=_.ONE_MINUS_SRC_ALPHA,y=_.ONE,m=_.ONE_MINUS_SRC_ALPHA;break;case lt._$ns:g=_.ONE,d=_.ONE,y=_.ZERO,m=_.ONE;break;case lt._$_s:g=_.DST_COLOR,d=_.ONE_MINUS_SRC_ALPHA,y=_.ZERO,m=_.ONE}_.blendEquationSeparate(_.FUNC_ADD,_.FUNC_ADD),_.blendFuncSeparate(g,d,y,m),this.anisotropyExt&&_.texParameteri(_.TEXTURE_2D,this.anisotropyExt.TEXTURE_MAX_ANISOTROPY_EXT,this.maxAnisotropy);var T=i.length;_.drawElements(_.TRIANGLES,T,_.UNSIGNED_SHORT,0),_.bindTexture(_.TEXTURE_2D,null)}};function mt(t,e,i){return null==e&&(e=t.createBuffer()),t.bindBuffer(t.ARRAY_BUFFER,e),t.bufferData(t.ARRAY_BUFFER,i,t.DYNAMIC_DRAW),e}function Tt(t,e,i){return null==e&&(e=t.createBuffer()),t.bindBuffer(t.ELEMENT_ARRAY_BUFFER,e),t.bufferData(t.ELEMENT_ARRAY_BUFFER,i,t.DYNAMIC_DRAW),e}yt.prototype._$Rs=function(){throw new Error("_$Rs")},yt.prototype._$Ds=function(t){throw new Error("_$Ds")},yt.prototype._$K2=function(){for(var t=0;t=48){var r=G._$9o(t);return null!=r?(r._$F0(this),r):null}switch(t){case 1:return this._$bT();case 10:return new function(){i||(this.color=null)}(this._$6L(),!0);case 11:return new P(this._$mP(),this._$mP(),this._$mP(),this._$mP());case 12:return new P(this._$_T(),this._$_T(),this._$_T(),this._$_T());case 13:return new v(this._$mP(),this._$mP());case 14:return new v(this._$_T(),this._$_T());case 15:for(var o=this._$3L(),n=new Array(o),s=0;s>7-this._$hL++&1)},Pt.prototype._$zT=function(){0!=this._$hL&&(this._$hL=0)};function vt(){}vt._$2S=Math.PI/180,vt._$bS=Math.PI/180,vt._$wS=180/Math.PI,vt._$NS=180/Math.PI,vt.PI_F=Math.PI,vt._$kT=[0,.012368,.024734,.037097,.049454,.061803,.074143,.086471,.098786,.111087,.12337,.135634,.147877,.160098,.172295,.184465,.196606,.208718,.220798,.232844,.244854,.256827,.268761,.280654,.292503,.304308,.316066,.327776,.339436,.351044,.362598,.374097,.385538,.396921,.408243,.419502,.430697,.441826,.452888,.463881,.474802,.485651,.496425,.507124,.517745,.528287,.538748,.549126,.559421,.56963,.579752,.589785,.599728,.609579,.619337,.629,.638567,.648036,.657406,.666676,.675843,.684908,.693867,.70272,.711466,.720103,.72863,.737045,.745348,.753536,.76161,.769566,.777405,.785125,.792725,.800204,.807561,.814793,.821901,.828884,.835739,.842467,.849066,.855535,.861873,.868079,.874153,.880093,.885898,.891567,.897101,.902497,.907754,.912873,.917853,.922692,.92739,.931946,.936359,.940629,.944755,.948737,.952574,.956265,.959809,.963207,.966457,.96956,.972514,.97532,.977976,.980482,.982839,.985045,.987101,.989006,.990759,.992361,.993811,.995109,.996254,.997248,.998088,.998776,.999312,.999694,.999924,1],vt._$92=function(t,e){var i=Math.atan2(t[1],t[0]),r=Math.atan2(e[1],e[0]);return vt._$tS(i,r)},vt._$tS=function(t,e){for(var i=t-e;i<-Math.PI;)i+=2*Math.PI;for(;i>Math.PI;)i-=2*Math.PI;return i},vt._$9=function(t){return Math.sin(t)},vt.fcos=function(t){return Math.cos(t)};function Lt(t){i||(this._$e0=null,this._$IP=null,this._$Us=null,this._$7s=null,this._$IS=[!1],this._$VS=null,this._$AT=!0,this.baseOpacity=1,this.clipBufPre_clipContext=null,this._$e0=t)}Lt.prototype._$u2=function(){return this._$IS[0]},Lt.prototype._$yo=function(){return this._$AT&&!this._$IS[0]},Lt.prototype._$GT=function(){return this._$e0};function Mt(){}Mt._$W2=0,Mt.SYSTEM_INFO=null,Mt.USER_AGENT=navigator.userAgent,Mt.isIPhone=function(){return Mt.SYSTEM_INFO||Mt.setup(),Mt.SYSTEM_INFO._isIPhone},Mt.isIOS=function(){return Mt.SYSTEM_INFO||Mt.setup(),Mt.SYSTEM_INFO._isIPhone||Mt.SYSTEM_INFO._isIPad},Mt.isAndroid=function(){return Mt.SYSTEM_INFO||Mt.setup(),Mt.SYSTEM_INFO._isAndroid},Mt.getOSVersion=function(){return Mt.SYSTEM_INFO||Mt.setup(),Mt.SYSTEM_INFO.version},Mt.getOS=function(){return Mt.SYSTEM_INFO||Mt.setup(),Mt.SYSTEM_INFO._isIPhone||Mt.SYSTEM_INFO._isIPad?"iOS":Mt.SYSTEM_INFO._isAndroid?"Android":"_$Q0 OS"},Mt.setup=function(){var t=Mt.USER_AGENT;function e(t,e){for(var i=t.substring(e).split(/[ _,;\.]/),r=0,o=0;o<=2&&!isNaN(i[o]);o++){var n=parseInt(i[o]);if(n<0||n>999){a._$li("err : "+n+" @UtHtml5.setup()"),r=0;break}r+=n*Math.pow(1e3,2-o)}return r}var i,r=Mt.SYSTEM_INFO={userAgent:t};if((i=t.indexOf("iPhone OS "))>=0)r.os="iPhone",r._isIPhone=!0,r.version=e(t,i+"iPhone OS ".length);else if((i=t.indexOf("iPad"))>=0){if((i=t.indexOf("CPU OS"))<0)return void a._$li(" err : "+t+" @UtHtml5.setup()");r.os="iPad",r._isIPad=!0,r.version=e(t,i+"CPU OS ".length)}else(i=t.indexOf("Android"))>=0?(r.os="Android",r._isAndroid=!0,r.version=e(t,i+"Android ".length)):(r.os="-",r.version=-1)},at.init();i=!1;e.UtSystem=A,e.UtDebug=a,e.LDTransform=gt,e.LDGL=ot,e.Live2D=at,e.Live2DModelWebGL=pt,e.Live2DModelJS=q,e.Live2DMotion=J,e.MotionQueueManager=ct,e.PhysicsHair=u,e.AMotion=s,e.PartsDataID=h,e.DrawDataID=b,e.BaseDataID=dt,e.ParamID=l}).call(e,i(83))},78:function(t,e,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.L2DBaseModel=e.L2DExpressionMotion=e.L2DExpressionParam=e.L2DEyeBlink=e.EYE_STATE=e.L2DMatrix44=e.L2DModelMatrix=e.L2DMotionManager=e.L2DPhysics=e.L2DPartsParam=e.L2DPose=e.L2DViewMatrix=e.Live2DFramework=e.L2DTargetPoint=void 0;var r=i(77);function o(){this.live2DModel=null,this.modelMatrix=null,this.eyeBlink=null,this.physics=null,this.pose=null,this.debugMode=!1,this.initialized=!1,this.updating=!1,this.alpha=1,this.accAlpha=0,this.lipSync=!1,this.lipSyncValue=0,this.accelX=0,this.accelY=0,this.accelZ=0,this.dragX=0,this.dragY=0,this.startTimeMSec=null,this.mainMotionManager=new u,this.expressionManager=new u,this.motions={},this.expressions={},this.isTexLoaded=!1}var n=0;o.prototype.getModelMatrix=function(){return this.modelMatrix},o.prototype.setAlpha=function(t){t>.999&&(t=1),t<.001&&(t=0),this.alpha=t},o.prototype.getAlpha=function(){return this.alpha},o.prototype.isInitialized=function(){return this.initialized},o.prototype.setInitialized=function(t){this.initialized=t},o.prototype.isUpdating=function(){return this.updating},o.prototype.setUpdating=function(t){this.updating=t},o.prototype.getLive2DModel=function(){return this.live2DModel},o.prototype.setLipSync=function(t){this.lipSync=t},o.prototype.setLipSyncValue=function(t){this.lipSyncValue=t},o.prototype.setAccel=function(t,e,i){this.accelX=t,this.accelY=e,this.accelZ=i},o.prototype.setDrag=function(t,e){this.dragX=t,this.dragY=e},o.prototype.getMainMotionManager=function(){return this.mainMotionManager},o.prototype.getExpressionManager=function(){return this.expressionManager},o.prototype.loadModelData=function(t,e){var i=y.getPlatformManager();this.debugMode&&i.log("Load model : "+t);var o=this;i.loadLive2DModel(t,function(t){o.live2DModel=t,o.live2DModel.saveParam();0==r.Live2D.getError()?(o.modelMatrix=new $(o.live2DModel.getCanvasWidth(),o.live2DModel.getCanvasHeight()),o.modelMatrix.setWidth(2),o.modelMatrix.setCenterPosition(0,0),e(o.live2DModel)):console.error("Error : Failed to loadModelData().")})},o.prototype.loadTexture=function(t,e,i){n++;var r=y.getPlatformManager();this.debugMode&&r.log("Load Texture : "+e);var o=this;r.loadTexture(this.live2DModel,t,e,function(){0==--n&&(o.isTexLoaded=!0),"function"==typeof i&&i()})},o.prototype.loadMotion=function(t,e,i){var o=y.getPlatformManager();this.debugMode&&o.log("Load Motion : "+e);var n=null,s=this;o.loadBytes(e,function(e){n=r.Live2DMotion.loadMotion(e),null!=t&&(s.motions[t]=n),i(n)})},o.prototype.loadExpression=function(t,e,i){var r=y.getPlatformManager();this.debugMode&&r.log("Load Expression : "+e);var o=this;r.loadBytes(e,function(e){null!=t&&(o.expressions[t]=s.loadJson(e)),"function"==typeof i&&i()})},o.prototype.loadPose=function(t,e){var i=y.getPlatformManager();this.debugMode&&i.log("Load Pose : "+t);var r=this;try{i.loadBytes(t,function(t){r.pose=c.load(t),"function"==typeof e&&e()})}catch(t){console.warn(t)}},o.prototype.loadPhysics=function(t){var e=y.getPlatformManager();this.debugMode&&e.log("Load Physics : "+t);var i=this;try{e.loadBytes(t,function(t){i.physics=p.load(t)})}catch(t){console.warn(t)}},o.prototype.hitTestSimple=function(t,e,i){if(null===this.live2DModel)return!1;var r=this.live2DModel.getDrawDataIndex(t);if(r<0)return!1;for(var o=this.live2DModel.getTransformedPoints(r),n=this.live2DModel.getCanvasWidth(),s=0,a=this.live2DModel.getCanvasHeight(),_=0,h=0;hs&&(s=l),$_&&(_=$)}var u=this.modelMatrix.invertTransformX(e),p=this.modelMatrix.invertTransformY(i);return n<=u&&u<=s&&a<=p&&p<=_};function s(){r.AMotion.prototype.constructor.call(this),this.paramList=new Array}s.prototype=new r.AMotion,s.EXPRESSION_DEFAULT="DEFAULT",s.TYPE_SET=0,s.TYPE_ADD=1,s.TYPE_MULT=2,s.loadJson=function(t){var e=new s,i=y.getPlatformManager().jsonParseFromBytes(t);if(e.setFadeIn(parseInt(i.fade_in)>0?parseInt(i.fade_in):1e3),e.setFadeOut(parseInt(i.fade_out)>0?parseInt(i.fade_out):1e3),null==i.params)return e;var r=i.params,o=r.length;e.paramList=[];for(var n=0;n=0;--o){var n=this.paramList[o];n.type==s.TYPE_ADD?t.addToParamFloat(n.id,n.value,i):n.type==s.TYPE_MULT?t.multParamFloat(n.id,n.value,i):n.type==s.TYPE_SET&&t.setParamFloat(n.id,n.value,i)}};function a(){this.id="",this.type=-1,this.value=null}function _(){this.nextBlinkTime=null,this.stateStartTime=null,this.blinkIntervalMsec=null,this.eyeState=h.STATE_FIRST,this.blinkIntervalMsec=4e3,this.closingMotionMsec=100,this.closedMotionMsec=50,this.openingMotionMsec=150,this.closeIfZero=!0,this.eyeID_L="PARAM_EYE_L_OPEN",this.eyeID_R="PARAM_EYE_R_OPEN"}_.prototype.calcNextBlink=function(){return r.UtSystem.getUserTimeMSec()+Math.random()*(2*this.blinkIntervalMsec-1)},_.prototype.setInterval=function(t){this.blinkIntervalMsec=t},_.prototype.setEyeMotion=function(t,e,i){this.closingMotionMsec=t,this.closedMotionMsec=e,this.openingMotionMsec=i},_.prototype.updateParam=function(t){var e,i=r.UtSystem.getUserTimeMSec(),o=0;switch(this.eyeState){case h.STATE_CLOSING:(o=(i-this.stateStartTime)/this.closingMotionMsec)>=1&&(o=1,this.eyeState=h.STATE_CLOSED,this.stateStartTime=i),e=1-o;break;case h.STATE_CLOSED:(o=(i-this.stateStartTime)/this.closedMotionMsec)>=1&&(this.eyeState=h.STATE_OPENING,this.stateStartTime=i),e=0;break;case h.STATE_OPENING:(o=(i-this.stateStartTime)/this.openingMotionMsec)>=1&&(o=1,this.eyeState=h.STATE_INTERVAL,this.nextBlinkTime=this.calcNextBlink()),e=o;break;case h.STATE_INTERVAL:this.nextBlinkTime=t)&&(!(this.currentPriority>=t)&&(this.reservePriority=t,!0))},u.prototype.setReservePriority=function(t){this.reservePriority=t},u.prototype.updateParam=function(t){var e=r.MotionQueueManager.prototype.updateParam.call(this,t);return this.isFinished()&&(this.currentPriority=0),e},u.prototype.startMotionPrio=function(t,e){return e==this.reservePriority&&(this.reservePriority=0),this.currentPriority=e,this.startMotion(t,!1)};function p(){this.physicsList=new Array,this.startTimeMSec=r.UtSystem.getUserTimeMSec()}p.load=function(t){for(var e=new p,i=y.getPlatformManager().jsonParseFromBytes(t).physics_hair,o=i.length,n=0;n=0)break;r=n,o=t.getPartsOpacity(s),(o+=i/.5)>1&&(o=1)}}r<0&&(r=0,o=1);for(n=0;n.15&&(_=1-.15/(1-o)),h>_&&(h=_),t.setPartsOpacity(s,h)}}},c.prototype.copyOpacityOtherParts=function(t,e){for(var i=0;io)&&(h*=o/$,l*=o/$,$=o),this.faceVX+=h,this.faceVY+=l;var u=.5*(Math.sqrt(o*o+16*o*a-8*o*a)-o),p=Math.sqrt(this.faceVX*this.faceVX+this.faceVY*this.faceVY);p>u&&(this.faceVX*=u/p,this.faceVY*=u/p),this.faceX+=this.faceVX,this.faceY+=this.faceVY}}else this.lastTimeSec=r.UtSystem.getUserTimeMSec()};function d(){l.prototype.constructor.call(this),this.screenLeft=null,this.screenRight=null,this.screenTop=null,this.screenBottom=null,this.maxLeft=null,this.maxRight=null,this.maxTop=null,this.maxBottom=null}d.prototype=new l,d.prototype.adjustTranslate=function(t,e){this.tr[0]*this.maxLeft+(this.tr[12]+t)>this.screenLeft&&(t=this.screenLeft-this.tr[0]*this.maxLeft-this.tr[12]),this.tr[0]*this.maxRight+(this.tr[12]+t)this.screenBottom&&(e=this.screenBottom-this.tr[5]*this.maxBottom-this.tr[13]);var i=[1,0,0,0,0,1,0,0,0,0,1,0,t,e,0,1];l.mul(i,this.tr,this.tr)},d.prototype.adjustScale=function(t,e,i){this.tr[0];var r=[1,0,0,0,0,1,0,0,0,0,1,0,t,e,0,1],o=[i,0,0,0,0,i,0,0,0,0,1,0,0,0,0,1],n=[1,0,0,0,0,1,0,0,0,0,1,0,-t,-e,0,1];l.mul(n,this.tr,this.tr),l.mul(o,this.tr,this.tr),l.mul(r,this.tr,this.tr)},d.prototype.setScreenRect=function(t,e,i,r){this.screenLeft=t,this.screenRight=e,this.screenTop=r,this.screenBottom=i},d.prototype.setMaxScreenRect=function(t,e,i,r){this.maxLeft=t,this.maxRight=e,this.maxTop=r,this.maxBottom=i},d.prototype.getScreenLeft=function(){return this.screenLeft},d.prototype.getScreenRight=function(){return this.screenRight},d.prototype.getScreenBottom=function(){return this.screenBottom},d.prototype.getScreenTop=function(){return this.screenTop},d.prototype.getMaxLeft=function(){return this.maxLeft},d.prototype.getMaxRight=function(){return this.maxRight},d.prototype.getMaxBottom=function(){return this.maxBottom},d.prototype.getMaxTop=function(){return this.maxTop};function y(){}y.platformManager=null,y.getPlatformManager=function(){return y.platformManager},y.setPlatformManager=function(t){y.platformManager=t},e.L2DTargetPoint=g,e.Live2DFramework=y,e.L2DViewMatrix=d,e.L2DPose=c,e.L2DPartsParam=f,e.L2DPhysics=p,e.L2DMotionManager=u,e.L2DModelMatrix=$,e.L2DMatrix44=l,e.EYE_STATE=h,e.L2DEyeBlink=_,e.L2DExpressionParam=a,e.L2DExpressionMotion=s,e.L2DBaseModel=o},79:function(t,e,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0});e.cDefine={VIEW_LOGICAL_LEFT:-1,VIEW_LOGICAL_RIGHT:1,VIEW_LOGICAL_MAX_LEFT:-2,VIEW_LOGICAL_MAX_RIGHT:2,VIEW_LOGICAL_MAX_BOTTOM:-2,VIEW_LOGICAL_MAX_TOP:2,PRIORITY_NONE:0,PRIORITY_IDLE:1,PRIORITY_NORMAL:2,PRIORITY_FORCE:3,MOTION_GROUP_IDLE:"idle",MOTION_GROUP_TAP_BODY:"tap_body",MOTION_GROUP_FLICK_HEAD:"flick_head",MOTION_GROUP_PINCH_IN:"pinch_in",MOTION_GROUP_PINCH_OUT:"pinch_out",MOTION_GROUP_SHAKE:"shake",HIT_AREA_HEAD:"head",HIT_AREA_BODY:"body"}},80:function(t,e,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.currCanvas=e.currWebGL=e.createElement=void 0;var r=i(38),o=i(37),n=i(82),s=void 0,a=void 0;e.createElement=function(){var t=document.getElementById(r.config.name.div);null!==t&&document.body.removeChild(t);var i=document.createElement("div");i.id=r.config.name.div,i.className="live2d-widget-container",i.style.setProperty("position","fixed"),i.style.setProperty(r.config.display.position,r.config.display.hOffset+"px"),i.style.setProperty("bottom",r.config.display.vOffset+"px"),i.style.setProperty("width",r.config.display.width+"px"),i.style.setProperty("height",r.config.display.height+"px"),i.style.setProperty("z-index",99999),i.style.setProperty("opacity",r.config.react.opacity),i.style.setProperty("pointer-events","none"),document.body.appendChild(i),o.L2Dwidget.emit("create-container",i),r.config.dialog.enable&&(0,n.createDialogElement)(i);var _=document.createElement("canvas");_.setAttribute("id",r.config.name.canvas),_.setAttribute("width",r.config.display.width*r.config.display.superSample),_.setAttribute("height",r.config.display.height*r.config.display.superSample),_.style.setProperty("position","absolute"),_.style.setProperty("left","0px"),_.style.setProperty("top","0px"),_.style.setProperty("width",r.config.display.width+"px"),_.style.setProperty("height",r.config.display.height+"px"),r.config.dev.border&&_.style.setProperty("border","dashed 1px #CCC"),i.appendChild(_),e.currCanvas=a=document.getElementById(r.config.name.canvas),o.L2Dwidget.emit("create-canvas",_),function(){for(var t=["webgl2","webgl","experimental-webgl2","experimental-webgl","webkit-3d","moz-webgl"],i=0;i\n .live2d-widget-dialog-container {\n width: 300px;\n height: 120px;\n position: absolute;\n bottom: 65%;\n right: 0px;\n transform-origin: right;\n padding: 12px;\n box-sizing: border-box;\n -webkit-font-smoothing: antialiased;\n }\n .live2d-widget-dialog {\n width: 100%;\n height: 100%;\n color: #917159;\n font-size: 16px;\n padding: 12px;\n border: 2px solid rgb(236, 203, 180);\n background: rgb(252, 248, 244);\n box-sizing: border-box;\n border-radius: 10px;\n transform: rotate(-2deg);\n opacity: 0;\n transition: 200ms opacity;\n box-shadow: rgba(0, 0, 0, 0.12) 0px 1px 6px, rgba(0, 0, 0, 0.12) 0px 1px 4px;\n animation: live2d-widget-dialog-tingle 4s ease-in-out 0s infinite alternate;\n }\n @keyframes live2d-widget-dialog-tingle {\n 0% { transform: translate(-1px, 1.5px) rotate(-2deg); }\n 100% { transform: translate(1px, -1.5px) rotate(2deg); }\n }\n\n";var n=void 0,s=void 0,a=void 0;function _(){s.style.opacity=1}function h(){s.style.opacity=0}function l(t){_(),s.innerText=t,clearTimeout(a),a=setTimeout(function(){h()},5e3)}function $(){var t=new XMLHttpRequest;t.open("get","https://v1.hitokoto.cn"),t.setRequestHeader("Cache-Control","no-cache"),t.onreadystatechange=function(){if(4===t.readyState){l(JSON.parse(t.responseText).hitokoto),setTimeout($,1e4)}},t.send()}t.exports={createDialogElement:function(t){(n=document.createElement("div")).className="live2d-widget-dialog-container",n.style.transform="scale("+r.config.display.width/250+")",(s=document.createElement("div")).className="live2d-widget-dialog",n.appendChild(s),t.appendChild(n),o.L2Dwidget.emit("create-dialog",n),r.config.dialog.hitokoto&&$()},displayDialog:_,hiddenDialog:h,alertText:l,showHitokotoLoop:$}},83:function(t,e){t.exports={import:function(){throw new Error("System.import cannot be used indirectly")}}},84:function(t,e,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.cManager=void 0;var r=i(78),o=i(85),n=i(86),s=i(79);function a(t){this.eventemitter=t,this.models=[],this.count=-1,this.reloadFlg=!1,r.Live2DFramework.setPlatformManager(new o.PlatformManager)}a.prototype.createModel=function(){var t=new n.cModel;return this.models.push(t),t},a.prototype.changeModel=function(t,e){this.reloadFlg&&(this.reloadFlg=!1,this.releaseModel(0,t),this.createModel(),this.models[0].load(t,e))},a.prototype.getModel=function(t){return t>=this.models.length?null:this.models[t]},a.prototype.releaseModel=function(t,e){this.models.length<=t||(this.models[t].release(e),delete this.models[t],this.models.splice(t,1))},a.prototype.numModels=function(){return this.models.length},a.prototype.setDrag=function(t,e){for(var i=0;i0){n.expressions={};for(var t=0;t range) {\n a = {\n x: a.x / r * range + center.x,\n y: a.y / r * range + center.y\n };\n return a;\n } else {\n return transform;\n }\n}\n*/\nfunction dot(A,B)\n{\n return A.x * B.x + A.y * B.y;\n}\n\nfunction normalize(x,y)\n{\n let length = Math.sqrt(x * x + y * y)\n return {\n x: x / length,\n y: y / length\n }\n}\n\nfunction transformRect(center, transform, rect)\n{\n if (transform.x < rect.left + rect.width && transform.y < rect.top + rect.height &&\n transform.x > rect.left && transform.y > rect.top) return transform;\n let Len_X = center.x - transform.x;\n let Len_Y = center.y - transform.y;\n\n function angle(Len_X, Len_Y)\n {\n return Math.acos(dot({\n x: 0,\n y: 1\n }, normalize(Len_X, Len_Y))) * 180 / Math.PI\n }\n\n let angleTarget = angle(Len_X, Len_Y);\n if (transform.x < center.x) angleTarget = 360 - angleTarget;\n let angleLeftTop = 360 - angle(rect.left - center.x, (rect.top - center.y) * -1);\n let angleLeftBottom = 360 - angle(rect.left - center.x, (rect.top + rect.height - center.y) * -1);\n let angleRightTop = angle(rect.left + rect.width - center.x, (rect.top - center.y) * -1);\n let angleRightBottom = angle(rect.left + rect.width - center.x, (rect.top + rect.height - center.y) * -1);\n let scale = Len_Y / Len_X;\n let res = {};\n\n if (angleTarget < angleRightTop) {\n let y3 = rect.top - center.y;\n let x3 = y3 / scale;\n res = {\n y: center.y + y3,\n x: center.x + x3\n }\n } else if(angleTarget < angleRightBottom) {\n let x3 = rect.left + rect.width - center.x;\n let y3 = x3 * scale;\n res = {\n y: center.y + y3,\n x: center.x + x3\n }\n } else if (angleTarget < angleLeftBottom) {\n let y3 = rect.top + rect.height - center.y;\n let x3 = y3 / scale;\n res = {\n y: center.y + y3,\n x: center.x + x3\n }\n } else if (angleTarget < angleLeftTop) {\n let x3 = center.x - rect.left;\n let y3 = x3 * scale;\n res = {\n y: center.y - y3,\n x: center.x - x3\n }\n } else {\n let y3 = rect.top - center.y;\n let x3 = y3 / scale;\n res = {\n y: center.y + y3,\n x: center.x + x3\n }\n }\n\n return res;\n}\n\nfunction modelTurnHead(event)\n{\n drag = true;\n\n let rect = currCanvas.getBoundingClientRect();\n\n let sx = transformScreenX(event.clientX - rect.left);\n let sy = transformScreenY(event.clientY - rect.top);\n let target = transformRect({\n x: rect.left + rect.width / 2,\n y: rect.top + rect.height * headPos\n }, {\n x: event.clientX,\n y: event.clientY\n }, rect)\n let vx = transformViewX(target.x - rect.left);\n let vy = transformViewY(target.y - rect.top);\n\n if (cDefine.DEBUG_MOUSE_LOG)\n console.log(\"modelTurnHead onMouseMove device( x:\" + event.clientX + \" y:\" + event.clientY + \" ) view( x:\" + vx + \" y:\" + vy + \")\");\n\n lastMouseX = sx;\n lastMouseY = sy;\n\n dragMgr.setPoint(vx, vy);\n}\n\nfunction modelTapEvent(event)\n{\n drag = true;\n\n let rect = currCanvas.getBoundingClientRect();\n\n let sx = transformScreenX(event.clientX - rect.left);\n let sy = transformScreenY(event.clientY - rect.top);\n let target = transformRect({\n x: rect.left + rect.width / 2,\n y: rect.top + rect.height * headPos\n }, {\n x: event.clientX,\n y: event.clientY\n }, rect)\n let vx = transformViewX(target.x - rect.left);\n let vy = transformViewY(target.y - rect.top);\n\n if (cDefine.DEBUG_MOUSE_LOG)\n console.log(\"modelTapEvent onMouseDown device( x:\" + event.clientX + \" y:\" + event.clientY + \" ) view( x:\" + vx + \" y:\" + vy + \")\");\n\n lastMouseX = sx;\n lastMouseY = sy;\n\n L2Dwidget.emit('tap', event);\n\n live2DMgr.tapEvent(vx, vy);\n}\n\nfunction followPointer(event)\n{\n let rect = currCanvas.getBoundingClientRect();\n\n let sx = transformScreenX(event.clientX - rect.left);\n let sy = transformScreenY(event.clientY - rect.top);\n\n // log but seems ok\n // console.log(\"ecx=\" + event.clientX + \" ecy=\" + event.clientY + \" sx=\" + sx + \" sy=\" + sy);\n\n let target = transformRect({// seems ok here\n x: rect.left + rect.width / 2,\n y: rect.top + rect.height * headPos\n }, {\n x: event.clientX,\n y: event.clientY\n }, rect)\n let vx = transformViewX(target.x - rect.left);\n let vy = transformViewY(target.y - rect.top);\n\n if (cDefine.DEBUG_MOUSE_LOG)\n console.log(\"followPointer onMouseMove device( x:\" + event.clientX + \" y:\" + event.clientY + \" ) view( x:\" + vx + \" y:\" + vy + \")\");\n\n if (drag)\n {\n lastMouseX = sx;\n lastMouseY = sy;\n dragMgr.setPoint(vx, vy);\n }\n}\n\nfunction lookFront()\n{\n if (drag) {\n drag = false;\n }\n dragMgr.setPoint(0, 0);\n}\n\nfunction mouseEvent(e)\n{\n //e.preventDefault();\n if (e.type == \"mousedown\") {\n modelTapEvent(e);\n } else if (e.type == \"mousemove\") {\n modelTurnHead(e);\n } else if (e.type == \"mouseup\") {\n if(\"button\" in e && e.button != 0) return;\n // lookFront();\n } else if (e.type == \"mouseleave\") {\n lookFront();\n }\n}\n\nfunction touchEvent(e)\n{\n var touch = e.touches[0];\n if (e.type == \"touchstart\") {\n if (e.touches.length == 1) modelTapEvent(touch);\n // onClick(touch);\n } else if (e.type == \"touchmove\") {\n followPointer(touch);\n } else if (e.type == \"touchend\") {\n lookFront();\n }\n}\n\nfunction transformViewX(deviceX)\n{\n var screenX = deviceToScreen.transformX(deviceX);\n return viewMatrix.invertTransformX(screenX);\n}\n\n\nfunction transformViewY(deviceY)\n{\n var screenY = deviceToScreen.transformY(deviceY);\n return viewMatrix.invertTransformY(screenY);\n}\n\n\nfunction transformScreenX(deviceX)\n{\n return deviceToScreen.transformX(deviceX);\n}\n\n\nfunction transformScreenY(deviceY)\n{\n return deviceToScreen.transformY(deviceY);\n}\n\nexport{\n theRealInit,\n captureFrame,\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/cLive2DApp.js","/**\n * ============================================================\n * Live2D Cubism SDK for WebGL Version 2.1.00_1\n *\n * (c) Live2D Inc.\n * ============================================================\n *\n * This is a Software Development Kit (SDK) for developing Live2D-Cubism-powered applications on WebGL.\n * The SDK contains proprietary libraries and sample projects.\n * Read this document when using the SDK.\n *\n * ------------------------------\n * License\n * ------------------------------\n * Read Live2D License Agreement\n * for business\n * http://live2d.com/en/sdk_license_cubism3\n *\n * for indie\n * http://live2d.com/en/sdk_license_cubism_indie\n *\n * After agree and accept Live2D SDK License Agreement, the content in the following folders may be placed in the server which you control.\n * SDK\n * ├─framework\n * │ Live2DFramework.js\n * │\n * ├─lib\n * │ live2d.min.js\n * │\n * └─sample\n */\n\n// Changes have been done and intention:\n// 1. Pretty the code using Chrome for easy editing.\n// 2. Use ES6's module system to prevent functions from exposing to 'window' and easy compatibility for ES6.\n\n\nvar j = true;\nfunction aa() {\n if (j) {\n return;\n }\n this._$MT = null;\n this._$5S = null;\n this._$NP = 0;\n aa._$42++;\n this._$5S = new y(this);\n}\naa._$0s = 1;\naa._$4s = 2;\naa._$42 = 0;\naa._$62 = function(aQ, aU) {\n try {\n if (aU instanceof ArrayBuffer) {\n aU = new DataView(aU);\n }\n if (!(aU instanceof DataView)) {\n throw new J(\"_$SS#loadModel(b) / b _$x be DataView or ArrayBuffer\");\n }\n var aS = new K(aU);\n var aM = aS._$ST();\n var aK = aS._$ST();\n var aJ = aS._$ST();\n var aN;\n if (aM == 109 && aK == 111 && aJ == 99) {\n aN = aS._$ST();\n } else {\n throw new J(\"_$gi _$C _$li , _$Q0 _$P0.\");\n }\n aS._$gr(aN);\n if (aN > ay._$T7) {\n aQ._$NP |= aa._$4s;\n var aR = ay._$T7;\n var aI = \"_$gi _$C _$li , _$n0 _$_ version _$li ( SDK : \" + aR + \" < _$f0 : \" + aN + \" )@_$SS#loadModel()\\n\";\n throw new J(aI);\n }\n var aL = aS._$nP();\n if (aN >= ay._$s7) {\n var aH = aS._$9T();\n var aT = aS._$9T();\n if (aH != -30584 || aT != -30584) {\n aQ._$NP |= aa._$0s;\n throw new J(\"_$gi _$C _$li , _$0 _$6 _$Ui.\");\n }\n }\n aQ._$KS(aL);\n var aP = aQ.getModelContext();\n aP.setDrawParam(aQ.getDrawParam());\n aP.init();\n } catch (aO) {\n q._$Rb(aO);\n }\n}\n;\naa.prototype._$KS = function(aH) {\n this._$MT = aH;\n}\n;\naa.prototype.getModelImpl = function() {\n if (this._$MT == null) {\n this._$MT = new w();\n this._$MT._$zP();\n }\n return this._$MT;\n}\n;\naa.prototype.getCanvasWidth = function() {\n if (this._$MT == null) {\n return 0;\n }\n return this._$MT.getCanvasWidth();\n}\n;\naa.prototype.getCanvasHeight = function() {\n if (this._$MT == null) {\n return 0;\n }\n return this._$MT.getCanvasHeight();\n}\n;\naa.prototype.getParamFloat = function(aH) {\n if (typeof aH != \"number\") {\n aH = this._$5S.getParamIndex(z.getID(aH));\n }\n return this._$5S.getParamFloat(aH);\n}\n;\naa.prototype.setParamFloat = function(aH, aJ, aI) {\n if (typeof aH != \"number\") {\n aH = this._$5S.getParamIndex(z.getID(aH));\n }\n if (arguments.length < 3) {\n aI = 1;\n }\n this._$5S.setParamFloat(aH, this._$5S.getParamFloat(aH) * (1 - aI) + aJ * aI);\n}\n;\naa.prototype.addToParamFloat = function(aH, aJ, aI) {\n if (typeof aH != \"number\") {\n aH = this._$5S.getParamIndex(z.getID(aH));\n }\n if (arguments.length < 3) {\n aI = 1;\n }\n this._$5S.setParamFloat(aH, this._$5S.getParamFloat(aH) + aJ * aI);\n}\n;\naa.prototype.multParamFloat = function(aH, aJ, aI) {\n if (typeof aH != \"number\") {\n aH = this._$5S.getParamIndex(z.getID(aH));\n }\n if (arguments.length < 3) {\n aI = 1;\n }\n this._$5S.setParamFloat(aH, this._$5S.getParamFloat(aH) * (1 + (aJ - 1) * aI));\n}\n;\naa.prototype.getParamIndex = function(aH) {\n return this._$5S.getParamIndex(z.getID(aH));\n}\n;\naa.prototype.loadParam = function() {\n this._$5S.loadParam();\n}\n;\naa.prototype.saveParam = function() {\n this._$5S.saveParam();\n}\n;\naa.prototype.init = function() {\n this._$5S.init();\n}\n;\naa.prototype.update = function() {\n this._$5S.update();\n}\n;\naa.prototype._$Rs = function() {\n q._$li(\"_$60 _$PT _$Rs()\");\n return -1;\n}\n;\naa.prototype._$Ds = function(aH) {\n q._$li(\"_$60 _$PT _$SS#_$Ds() \\n\");\n}\n;\naa.prototype._$K2 = function() {}\n;\naa.prototype.draw = function() {}\n;\naa.prototype.getModelContext = function() {\n return this._$5S;\n}\n;\naa.prototype._$s2 = function() {\n return this._$NP;\n}\n;\naa.prototype._$P7 = function(aK, aR, aH, a0) {\n var aU = -1;\n var aY = 0;\n var aM = this;\n var aJ = 0.5;\n var aI = 0.15;\n var aX = true;\n if (aH == 0) {\n for (var aV = 0; aV < aK.length; aV++) {\n var aP = aK[aV];\n var aO = aR[aV];\n var aS = (aM.getParamFloat(aP) != 0);\n aM.setPartsOpacity(aO, (aS ? 1 : 0));\n }\n return;\n } else {\n if (aK.length == 1) {\n var aP = aK[0];\n var aT = (aM.getParamFloat(aP) != 0);\n var aO = aR[0];\n var aQ = aM.getPartsOpacity(aO);\n var aW = aH / a0;\n if (aT) {\n aQ += aW;\n if (aQ > 1) {\n aQ = 1;\n }\n } else {\n aQ -= aW;\n if (aQ < 0) {\n aQ = 0;\n }\n }\n aM.setPartsOpacity(aO, aQ);\n } else {\n for (var aV = 0; aV < aK.length; aV++) {\n var aP = aK[aV];\n var aS = (aM.getParamFloat(aP) != 0);\n if (aS) {\n if (aU >= 0) {\n break;\n }\n aU = aV;\n var aO = aR[aV];\n aY = aM.getPartsOpacity(aO);\n aY += aH / a0;\n if (aY > 1) {\n aY = 1;\n }\n }\n }\n if (aU < 0) {\n console.log(\"No _$wi _$q0/ _$U default[%s]\", aK[0]);\n aU = 0;\n aY = 1;\n aM.loadParam();\n aM.setParamFloat(aK[aU], aY);\n aM.saveParam();\n }\n for (var aV = 0; aV < aK.length; aV++) {\n var aO = aR[aV];\n if (aU == aV) {\n aM.setPartsOpacity(aO, aY);\n } else {\n var aL = aM.getPartsOpacity(aO);\n var aZ;\n if (aY < aJ) {\n aZ = aY * (aJ - 1) / aJ + 1;\n } else {\n aZ = (1 - aY) * aJ / (1 - aJ);\n }\n if (aX) {\n var aN = (1 - aZ) * (1 - aY);\n if (aN > aI) {\n aZ = 1 - aI / (1 - aY);\n }\n }\n if (aL > aZ) {\n aL = aZ;\n }\n aM.setPartsOpacity(aO, aL);\n }\n }\n }\n }\n}\n;\naa.prototype.setPartsOpacity = function(aI, aH) {\n if (typeof aI != \"number\") {\n aI = this._$5S.getPartsDataIndex(i.getID(aI));\n }\n this._$5S.setPartsOpacity(aI, aH);\n}\n;\naa.prototype.getPartsDataIndex = function(aH) {\n if (!(aH instanceof i)) {\n aH = i.getID(aH);\n }\n return this._$5S.getPartsDataIndex(aH);\n}\n;\naa.prototype.getPartsOpacity = function(aH) {\n if (typeof aH != \"number\") {\n aH = this._$5S.getPartsDataIndex(i.getID(aH));\n }\n if (aH < 0) {\n return 0;\n }\n return this._$5S.getPartsOpacity(aH);\n}\n;\naa.prototype.getDrawParam = function() {}\n;\naa.prototype.getDrawDataIndex = function(aH) {\n return this._$5S.getDrawDataIndex(Z.getID(aH));\n}\n;\naa.prototype.getDrawData = function(aH) {\n return this._$5S.getDrawData(aH);\n}\n;\naa.prototype.getTransformedPoints = function(aH) {\n var aI = this._$5S._$C2(aH);\n if (aI instanceof ag) {\n return (aI).getTransformedPoints();\n }\n return null;\n}\n;\naa.prototype.getIndexArray = function(aI) {\n if (aI < 0 || aI >= this._$5S._$aS.length) {\n return null;\n }\n var aH = this._$5S._$aS[aI];\n if (aH != null && aH.getType() == a._$wb) {\n if (aH instanceof b) {\n return aH.getIndexArray();\n }\n }\n return null;\n}\n;\nfunction W(aJ) {\n if (j) {\n return;\n }\n this.clipContextList = new Array();\n this.glcontext = aJ.gl;\n this.dp_webgl = aJ;\n this.curFrameNo = 0;\n this.firstError_clipInNotUpdate = true;\n this.colorBuffer = 0;\n this.isInitGLFBFunc = false;\n this.tmpBoundsOnModel = new av();\n if (Q.glContext.length > Q.frameBuffers.length) {\n this.curFrameNo = this.getMaskRenderTexture();\n } else {}\n this.tmpModelToViewMatrix = new ac();\n this.tmpMatrix2 = new ac();\n this.tmpMatrixForMask = new ac();\n this.tmpMatrixForDraw = new ac();\n this.CHANNEL_COLORS = new Array();\n var aI = new o();\n aI = new o();\n aI.r = 0;\n aI.g = 0;\n aI.b = 0;\n aI.a = 1;\n this.CHANNEL_COLORS.push(aI);\n aI = new o();\n aI.r = 1;\n aI.g = 0;\n aI.b = 0;\n aI.a = 0;\n this.CHANNEL_COLORS.push(aI);\n aI = new o();\n aI.r = 0;\n aI.g = 1;\n aI.b = 0;\n aI.a = 0;\n this.CHANNEL_COLORS.push(aI);\n aI = new o();\n aI.r = 0;\n aI.g = 0;\n aI.b = 1;\n aI.a = 0;\n this.CHANNEL_COLORS.push(aI);\n for (var aH = 0; aH < this.CHANNEL_COLORS.length; aH++) {\n this.dp_webgl.setChannelFlagAsColor(aH, this.CHANNEL_COLORS[aH]);\n }\n}\nW.CHANNEL_COUNT = 4;\nW.RENDER_TEXTURE_USE_MIPMAP = false;\nW.NOT_USED_FRAME = -100;\nW.prototype._$L7 = function() {\n if (this.tmpModelToViewMatrix) {\n this.tmpModelToViewMatrix = null;\n }\n if (this.tmpMatrix2) {\n this.tmpMatrix2 = null;\n }\n if (this.tmpMatrixForMask) {\n this.tmpMatrixForMask = null;\n }\n if (this.tmpMatrixForDraw) {\n this.tmpMatrixForDraw = null;\n }\n if (this.tmpBoundsOnModel) {\n this.tmpBoundsOnModel = null;\n }\n if (this.CHANNEL_COLORS) {\n for (var aH = this.CHANNEL_COLORS.length - 1; aH >= 0; --aH) {\n this.CHANNEL_COLORS.splice(aH, 1);\n }\n this.CHANNEL_COLORS = [];\n }\n this.releaseShader();\n}\n;\nW.prototype.releaseShader = function() {\n var aI = Q.frameBuffers.length;\n for (var aH = 0; aH < aI; aH++) {\n this.gl.deleteFramebuffer(Q.frameBuffers[aH].framebuffer);\n }\n Q.frameBuffers = [];\n Q.glContext = [];\n}\n;\nW.prototype.init = function(aO, aN, aL) {\n for (var aM = 0; aM < aN.length; aM++) {\n var aH = aN[aM].getClipIDList();\n if (aH == null) {\n continue;\n }\n var aJ = this.findSameClip(aH);\n if (aJ == null) {\n aJ = new U(this,aO,aH);\n this.clipContextList.push(aJ);\n }\n var aI = aN[aM].getDrawDataID();\n var aK = aO.getDrawDataIndex(aI);\n aJ.addClippedDrawData(aI, aK);\n var aP = aL[aM];\n aP.clipBufPre_clipContext = aJ;\n }\n}\n;\nW.prototype.getMaskRenderTexture = function() {\n var aH = null;\n aH = this.dp_webgl.createFramebuffer();\n Q.frameBuffers[this.dp_webgl.glno] = aH;\n return this.dp_webgl.glno;\n}\n;\nW.prototype.setupClip = function(a1, aQ) {\n var aK = 0;\n for (var aO = 0; aO < this.clipContextList.length; aO++) {\n var aP = this.clipContextList[aO];\n this.calcClippedDrawTotalBounds(a1, aP);\n if (aP.isUsing) {\n aK++;\n }\n }\n if (aK > 0) {\n var aM = aQ.gl.getParameter(aQ.gl.FRAMEBUFFER_BINDING);\n var aW = new Array(4);\n aW[0] = 0;\n aW[1] = 0;\n aW[2] = aQ.gl.canvas.width;\n aW[3] = aQ.gl.canvas.height;\n aQ.gl.viewport(0, 0, Q.clippingMaskBufferSize, Q.clippingMaskBufferSize);\n this.setupLayoutBounds(aK);\n aQ.gl.bindFramebuffer(aQ.gl.FRAMEBUFFER, Q.frameBuffers[this.curFrameNo].framebuffer);\n aQ.gl.clearColor(0, 0, 0, 0);\n aQ.gl.clear(aQ.gl.COLOR_BUFFER_BIT);\n for (var aO = 0; aO < this.clipContextList.length; aO++) {\n var aP = this.clipContextList[aO];\n var aT = aP.allClippedDrawRect;\n var aN = aP.layoutChannelNo;\n var aV = aP.layoutBounds;\n var aJ = 0.05;\n this.tmpBoundsOnModel._$jL(aT);\n this.tmpBoundsOnModel.expand(aT.width * aJ, aT.height * aJ);\n var aZ = aV.width / this.tmpBoundsOnModel.width;\n var aY = aV.height / this.tmpBoundsOnModel.height;\n this.tmpMatrix2.identity();\n this.tmpMatrix2.translate(-1, -1, 0);\n this.tmpMatrix2.scale(2, 2, 1);\n this.tmpMatrix2.translate(aV.x, aV.y, 0);\n this.tmpMatrix2.scale(aZ, aY, 1);\n this.tmpMatrix2.translate(-this.tmpBoundsOnModel.x, -this.tmpBoundsOnModel.y, 0);\n this.tmpMatrixForMask.setMatrix(this.tmpMatrix2.m);\n this.tmpMatrix2.identity();\n this.tmpMatrix2.translate(aV.x, aV.y, 0);\n this.tmpMatrix2.scale(aZ, aY, 1);\n this.tmpMatrix2.translate(-this.tmpBoundsOnModel.x, -this.tmpBoundsOnModel.y, 0);\n this.tmpMatrixForDraw.setMatrix(this.tmpMatrix2.m);\n var aH = this.tmpMatrixForMask.getArray();\n for (var aX = 0; aX < 16; aX++) {\n aP.matrixForMask[aX] = aH[aX];\n }\n var a0 = this.tmpMatrixForDraw.getArray();\n for (var aX = 0; aX < 16; aX++) {\n aP.matrixForDraw[aX] = a0[aX];\n }\n var aS = aP.clippingMaskDrawIndexList.length;\n for (var aU = 0; aU < aS; aU++) {\n var aR = aP.clippingMaskDrawIndexList[aU];\n var aI = a1.getDrawData(aR);\n var aL = a1._$C2(aR);\n aQ.setClipBufPre_clipContextForMask(aP);\n aI.draw(aQ, a1, aL);\n }\n }\n aQ.gl.bindFramebuffer(aQ.gl.FRAMEBUFFER, aM);\n aQ.setClipBufPre_clipContextForMask(null);\n aQ.gl.viewport(aW[0], aW[1], aW[2], aW[3]);\n }\n}\n;\nW.prototype.getColorBuffer = function() {\n return this.colorBuffer;\n}\n;\nW.prototype.findSameClip = function(aK) {\n for (var aN = 0; aN < this.clipContextList.length; aN++) {\n var aO = this.clipContextList[aN];\n var aH = aO.clipIDList.length;\n if (aH != aK.length) {\n continue;\n }\n var aI = 0;\n for (var aM = 0; aM < aH; aM++) {\n var aL = aO.clipIDList[aM];\n for (var aJ = 0; aJ < aH; aJ++) {\n if (aK[aJ] == aL) {\n aI++;\n break;\n }\n }\n }\n if (aI == aH) {\n return aO;\n }\n }\n return null;\n}\n;\nW.prototype.calcClippedDrawTotalBounds = function(a6, aV) {\n var aU = a6._$Ri.getModelImpl().getCanvasWidth();\n var a5 = a6._$Ri.getModelImpl().getCanvasHeight();\n var aJ = aU > a5 ? aU : a5;\n var aT = aJ;\n var aR = aJ;\n var aS = 0;\n var aP = 0;\n var aL = aV.clippedDrawContextList.length;\n for (var aM = 0; aM < aL; aM++) {\n var aW = aV.clippedDrawContextList[aM];\n var aN = aW.drawDataIndex;\n var aK = a6._$C2(aN);\n if (aK._$yo()) {\n var aX = aK.getTransformedPoints();\n var a4 = aX.length;\n var aI = [];\n var aH = [];\n var aO = 0;\n for (var a3 = aw._$i2; a3 < a4; a3 += aw._$No) {\n aI[aO] = aX[a3];\n aH[aO] = aX[a3 + 1];\n aO++;\n }\n var a2 = Math.min.apply(null, aI);\n var a1 = Math.min.apply(null, aH);\n var a0 = Math.max.apply(null, aI);\n var aZ = Math.max.apply(null, aH);\n if (a2 < aT) {\n aT = a2;\n }\n if (a1 < aR) {\n aR = a1;\n }\n if (a0 > aS) {\n aS = a0;\n }\n if (aZ > aP) {\n aP = aZ;\n }\n }\n }\n if (aT == aJ) {\n aV.allClippedDrawRect.x = 0;\n aV.allClippedDrawRect.y = 0;\n aV.allClippedDrawRect.width = 0;\n aV.allClippedDrawRect.height = 0;\n aV.isUsing = false;\n } else {\n var aQ = aS - aT;\n var aY = aP - aR;\n aV.allClippedDrawRect.x = aT;\n aV.allClippedDrawRect.y = aR;\n aV.allClippedDrawRect.width = aQ;\n aV.allClippedDrawRect.height = aY;\n aV.isUsing = true;\n }\n}\n;\nW.prototype.setupLayoutBounds = function(aQ) {\n var aI = aQ / W.CHANNEL_COUNT;\n var aP = aQ % W.CHANNEL_COUNT;\n aI = ~~aI;\n aP = ~~aP;\n var aH = 0;\n for (var aJ = 0; aJ < W.CHANNEL_COUNT; aJ++) {\n var aM = aI + (aJ < aP ? 1 : 0);\n if (aM == 0) {} else {\n if (aM == 1) {\n var aL = this.clipContextList[aH++];\n aL.layoutChannelNo = aJ;\n aL.layoutBounds.x = 0;\n aL.layoutBounds.y = 0;\n aL.layoutBounds.width = 1;\n aL.layoutBounds.height = 1;\n } else {\n if (aM == 2) {\n for (var aO = 0; aO < aM; aO++) {\n var aN = aO % 2;\n var aK = 0;\n aN = ~~aN;\n var aL = this.clipContextList[aH++];\n aL.layoutChannelNo = aJ;\n aL.layoutBounds.x = aN * 0.5;\n aL.layoutBounds.y = 0;\n aL.layoutBounds.width = 0.5;\n aL.layoutBounds.height = 1;\n }\n } else {\n if (aM <= 4) {\n for (var aO = 0; aO < aM; aO++) {\n var aN = aO % 2;\n var aK = aO / 2;\n aN = ~~aN;\n aK = ~~aK;\n var aL = this.clipContextList[aH++];\n aL.layoutChannelNo = aJ;\n aL.layoutBounds.x = aN * 0.5;\n aL.layoutBounds.y = aK * 0.5;\n aL.layoutBounds.width = 0.5;\n aL.layoutBounds.height = 0.5;\n }\n } else {\n if (aM <= 9) {\n for (var aO = 0; aO < aM; aO++) {\n var aN = aO % 3;\n var aK = aO / 3;\n aN = ~~aN;\n aK = ~~aK;\n var aL = this.clipContextList[aH++];\n aL.layoutChannelNo = aJ;\n aL.layoutBounds.x = aN / 3;\n aL.layoutBounds.y = aK / 3;\n aL.layoutBounds.width = 1 / 3;\n aL.layoutBounds.height = 1 / 3;\n }\n } else {\n q._$li(\"_$6 _$0P mask count : %d\", aM);\n }\n }\n }\n }\n }\n }\n}\n;\nfunction U(aH, aK, aI) {\n this.clipIDList = new Array();\n this.clipIDList = aI;\n this.clippingMaskDrawIndexList = new Array();\n for (var aJ = 0; aJ < aI.length; aJ++) {\n this.clippingMaskDrawIndexList.push(aK.getDrawDataIndex(aI[aJ]));\n }\n this.clippedDrawContextList = new Array();\n this.isUsing = true;\n this.layoutChannelNo = 0;\n this.layoutBounds = new av();\n this.allClippedDrawRect = new av();\n this.matrixForMask = new Float32Array(16);\n this.matrixForDraw = new Float32Array(16);\n this.owner = aH;\n}\nU.prototype.addClippedDrawData = function(aJ, aI) {\n var aH = new R(aJ,aI);\n this.clippedDrawContextList.push(aH);\n}\n;\nfunction R(aI, aH) {\n this._$gP = aI;\n this.drawDataIndex = aH;\n}\nfunction I() {\n if (j) {\n return;\n }\n this.color = null;\n}\nfunction ah() {\n if (j) {\n return;\n }\n this._$dP = null;\n this._$eo = null;\n this._$V0 = null;\n this._$dP = 1000;\n this._$eo = 1000;\n this._$V0 = 1;\n this._$a0();\n}\nah._$JT = function(aP, aN, aO) {\n var aQ = aP / aN;\n var a1 = aO / aN;\n var aU = a1;\n var aZ = 1 / 3;\n var aR = 2 / 3;\n var a0 = 1 - (1 - a1) * (1 - a1);\n var a2 = 1 - (1 - aU) * (1 - aU);\n var aM = 0;\n var aL = ((1 - a1) * aZ) * a0 + (aU * aR + (1 - aU) * aZ) * (1 - a0);\n var aK = (aU + (1 - aU) * aR) * a2 + (a1 * aZ + (1 - a1) * aR) * (1 - a2);\n var aJ = 1;\n var aY = aJ - 3 * aK + 3 * aL - aM;\n var aX = 3 * aK - 6 * aL + 3 * aM;\n var aW = 3 * aL - 3 * aM;\n var aV = aM;\n if (aQ <= 0) {\n return 0;\n } else {\n if (aQ >= 1) {\n return 1;\n }\n }\n var aS = aQ;\n var aI = aS * aS;\n var aH = aS * aI;\n var aT = aY * aH + aX * aI + aW * aS + aV;\n return aT;\n}\n;\nah.prototype._$a0 = function() {}\n;\nah.prototype.setFadeIn = function(aH) {\n this._$dP = aH;\n}\n;\nah.prototype.setFadeOut = function(aH) {\n this._$eo = aH;\n}\n;\nah.prototype._$pT = function(aH) {\n this._$V0 = aH;\n}\n;\nah.prototype.getFadeOut = function() {\n return this._$eo;\n}\n;\nah.prototype._$4T = function() {\n return this._$eo;\n}\n;\nah.prototype._$mT = function() {\n return this._$V0;\n}\n;\nah.prototype.getDurationMSec = function() {\n return -1;\n}\n;\nah.prototype.getLoopDurationMSec = function() {\n return -1;\n}\n;\nah.prototype.updateParam = function(aJ, aN) {\n if (!aN._$AT || aN._$9L) {\n return;\n }\n var aL = P.getUserTimeMSec();\n if (aN._$z2 < 0) {\n aN._$z2 = aL;\n aN._$bs = aL;\n var aM = this.getDurationMSec();\n if (aN._$Do < 0) {\n aN._$Do = (aM <= 0) ? -1 : aN._$z2 + aM;\n }\n }\n var aI = this._$V0;\n var aH = (this._$dP == 0) ? 1 : A._$r2(((aL - aN._$bs) / (this._$dP)));\n var aK = (this._$eo == 0 || aN._$Do < 0) ? 1 : A._$r2(((aN._$Do - aL) / (this._$eo)));\n aI = aI * aH * aK;\n if (!((0 <= aI && aI <= 1))) {\n console.log(\"### assert!! ### \");\n }\n this.updateParamExe(aJ, aL, aI, aN);\n if (aN._$Do > 0 && aN._$Do < aL) {\n aN._$9L = true;\n }\n}\n;\nah.prototype.updateParamExe = function(aH, aI, aJ, aK) {}\n;\nfunction q() {}\nq._$8s = 0;\nq._$fT = new Object();\nq.start = function(aI) {\n var aH = q._$fT[aI];\n if (aH == null) {\n aH = new af();\n aH._$r = aI;\n q._$fT[aI] = aH;\n }\n aH._$0S = P.getSystemTimeMSec();\n}\n;\nq.dump = function(aJ) {\n var aH = q._$fT[aJ];\n if (aH != null) {\n var aI = P.getSystemTimeMSec();\n var aK = aI - aH._$0S;\n console.log(aJ + \" : \" + aK + \"ms\");\n return aK;\n } else {\n return -1;\n }\n}\n;\nq.end = function(aJ) {\n var aH = q._$fT[aJ];\n if (aH != null) {\n var aI = P.getSystemTimeMSec();\n return aI - aH._$0S;\n } else {\n return -1;\n }\n}\n;\nq._$li = function(aI, aH) {\n console.log(\"_$li : \" + aI + \"\\n\", aH);\n}\n;\nq._$Ji = function(aI, aH) {\n console.log(aI, aH);\n}\n;\nq._$dL = function(aI, aH) {\n console.log(aI, aH);\n console.log(\"\\n\");\n}\n;\nq._$KL = function(aJ, aI) {\n for (var aH = 0; aH < aI; aH++) {\n if (aH % 16 == 0 && aH > 0) {\n console.log(\"\\n\");\n } else {\n if (aH % 8 == 0 && aH > 0) {\n console.log(\" \");\n }\n }\n console.log(\"%02X \", (aJ[aH] & 255));\n }\n console.log(\"\\n\");\n}\n;\nq._$nr = function(aL, aI, aK) {\n console.log(\"%s\\n\", aL);\n var aH = aI.length;\n for (var aJ = 0; aJ < aH; ++aJ) {\n console.log(\"%5d\", aI[aJ]);\n console.log(\"%s\\n\", aK);\n console.log(\",\");\n }\n console.log(\"\\n\");\n}\n;\nq._$Rb = function(aH) {\n console.log(\"dump exception : \" + aH);\n console.log(\"stack :: \" + aH.stack);\n}\n;\nfunction af() {\n this._$r = null;\n this._$0S = null;\n}\nfunction F() {\n if (j) {\n return;\n }\n this.x = null;\n this.y = null;\n this.width = null;\n this.height = null;\n}\nF.prototype._$8P = function() {\n return 0.5 * (this.x + this.x + this.width);\n}\n;\nF.prototype._$6P = function() {\n return 0.5 * (this.y + this.y + this.height);\n}\n;\nF.prototype._$EL = function() {\n return this.x + this.width;\n}\n;\nF.prototype._$5T = function() {\n return this.y + this.height;\n}\n;\nF.prototype._$jL = function(aI, aK, aJ, aH) {\n this.x = aI;\n this.y = aK;\n this.width = aJ;\n this.height = aH;\n}\n;\nF.prototype._$jL = function(aH) {\n this.x = aH.x;\n this.y = aH.y;\n this.width = aH.width;\n this.height = aH.height;\n}\n;\nfunction i(aH) {\n if (j) {\n return;\n }\n ak.prototype.constructor.call(this, aH);\n}\ni.prototype = new ak();\ni._$tP = new Object();\ni._$27 = function() {\n i._$tP.clear();\n}\n;\ni.getID = function(aH) {\n var aI = i._$tP[aH];\n if (aI == null) {\n aI = new i(aH);\n i._$tP[aH] = aI;\n }\n return aI;\n}\n;\ni.prototype._$3s = function() {\n return new i();\n}\n;\nfunction S() {}\nfunction z(aH) {\n if (j) {\n return;\n }\n ak.prototype.constructor.call(this, aH);\n}\nz.prototype = new ak();\nz._$tP = new Object();\nz._$27 = function() {\n z._$tP.clear();\n}\n;\nz.getID = function(aH) {\n var aI = z._$tP[aH];\n if (aI == null) {\n aI = new z(aH);\n z._$tP[aH] = aI;\n }\n return aI;\n}\n;\nz.prototype._$3s = function() {\n return new z();\n}\n;\nfunction w() {\n if (j) {\n return;\n }\n this._$vo = null;\n this._$F2 = null;\n this._$ao = 400;\n this._$1S = 400;\n w._$42++;\n}\nw._$42 = 0;\nw.prototype._$zP = function() {\n if (this._$vo == null) {\n this._$vo = new an();\n }\n if (this._$F2 == null) {\n this._$F2 = new Array();\n }\n}\n;\nw.prototype.getCanvasWidth = function() {\n return this._$ao;\n}\n;\nw.prototype.getCanvasHeight = function() {\n return this._$1S;\n}\n;\nw.prototype._$F0 = function(aH) {\n this._$vo = aH._$nP();\n this._$F2 = aH._$nP();\n this._$ao = aH._$6L();\n this._$1S = aH._$6L();\n}\n;\nw.prototype._$6S = function(aH) {\n this._$F2.push(aH);\n}\n;\nw.prototype._$Xr = function() {\n return this._$F2;\n}\n;\nw.prototype._$E2 = function() {\n return this._$vo;\n}\n;\nfunction u() {\n if (j) {\n return;\n }\n this.p1 = new N();\n this.p2 = new N();\n this._$Fo = 0;\n this._$Db = 0;\n this._$L2 = 0;\n this._$M2 = 0;\n this._$ks = 0;\n this._$9b = 0;\n this._$iP = 0;\n this._$iT = 0;\n this._$lL = new Array();\n this._$qP = new Array();\n this.setup(0.3, 0.5, 0.1);\n}\nu.prototype.setup = function(aJ, aI, aH) {\n this._$ks = this._$Yb();\n this.p2._$xT();\n if (arguments.length == 3) {\n this._$Fo = aJ;\n this._$L2 = aI;\n this.p1._$p = aH;\n this.p2._$p = aH;\n this.p2.y = aJ;\n this.setup();\n }\n}\n;\nu.prototype.getPhysicsPoint1 = function() {\n return this.p1;\n}\n;\nu.prototype.getPhysicsPoint2 = function() {\n return this.p2;\n}\n;\nu.prototype._$qr = function() {\n return this._$Db;\n}\n;\nu.prototype._$pr = function(aH) {\n this._$Db = aH;\n}\n;\nu.prototype._$5r = function() {\n return this._$M2;\n}\n;\nu.prototype._$Cs = function() {\n return this._$9b;\n}\n;\nu.prototype._$Yb = function() {\n return (-180 * (Math.atan2(this.p1.x - this.p2.x, -(this.p1.y - this.p2.y))) / Math.PI);\n}\n;\nu.prototype.addSrcParam = function(aJ, aH, aL, aI) {\n var aK = new h(aJ,aH,aL,aI);\n this._$lL.push(aK);\n}\n;\nu.prototype.addTargetParam = function(aJ, aH, aK, aI) {\n var aL = new aF(aJ,aH,aK,aI);\n this._$qP.push(aL);\n}\n;\nu.prototype.update = function(aI, aL) {\n if (this._$iP == 0) {\n this._$iP = this._$iT = aL;\n this._$Fo = (Math.sqrt((this.p1.x - this.p2.x) * (this.p1.x - this.p2.x) + (this.p1.y - this.p2.y) * (this.p1.y - this.p2.y)));\n return;\n }\n var aK = (aL - this._$iT) / 1000;\n if (aK != 0) {\n for (var aJ = this._$lL.length - 1; aJ >= 0; --aJ) {\n var aM = this._$lL[aJ];\n aM._$oP(aI, this);\n }\n this._$oo(aI, aK);\n this._$M2 = this._$Yb();\n this._$9b = (this._$M2 - this._$ks) / aK;\n this._$ks = this._$M2;\n }\n for (var aJ = this._$qP.length - 1; aJ >= 0; --aJ) {\n var aH = this._$qP[aJ];\n aH._$YS(aI, this);\n }\n this._$iT = aL;\n}\n;\nu.prototype._$oo = function(aN, aI) {\n if (aI < 0.033) {\n aI = 0.033;\n }\n var aU = 1 / aI;\n this.p1.vx = (this.p1.x - this.p1._$s0) * aU;\n this.p1.vy = (this.p1.y - this.p1._$70) * aU;\n this.p1.ax = (this.p1.vx - this.p1._$7L) * aU;\n this.p1.ay = (this.p1.vy - this.p1._$HL) * aU;\n this.p1.fx = this.p1.ax * this.p1._$p;\n this.p1.fy = this.p1.ay * this.p1._$p;\n this.p1._$xT();\n var aM = -(Math.atan2((this.p1.y - this.p2.y), this.p1.x - this.p2.x));\n var aL;\n var aV;\n var aR = Math.cos(aM);\n var aH = Math.sin(aM);\n var aW = 9.8 * this.p2._$p;\n var aQ = (this._$Db * aC._$bS);\n var aP = (aW * Math.cos(aM - aQ));\n aL = (aP * aH);\n aV = (aP * aR);\n var aK = (-this.p1.fx * aH * aH);\n var aT = (-this.p1.fy * aH * aR);\n var aJ = ((-this.p2.vx * this._$L2));\n var aS = ((-this.p2.vy * this._$L2));\n this.p2.fx = ((aL + aK + aJ));\n this.p2.fy = ((aV + aT + aS));\n this.p2.ax = this.p2.fx / this.p2._$p;\n this.p2.ay = this.p2.fy / this.p2._$p;\n this.p2.vx += this.p2.ax * aI;\n this.p2.vy += this.p2.ay * aI;\n this.p2.x += this.p2.vx * aI;\n this.p2.y += this.p2.vy * aI;\n var aO = (Math.sqrt((this.p1.x - this.p2.x) * (this.p1.x - this.p2.x) + (this.p1.y - this.p2.y) * (this.p1.y - this.p2.y)));\n this.p2.x = this.p1.x + this._$Fo * (this.p2.x - this.p1.x) / aO;\n this.p2.y = this.p1.y + this._$Fo * (this.p2.y - this.p1.y) / aO;\n this.p2.vx = (this.p2.x - this.p2._$s0) * aU;\n this.p2.vy = (this.p2.y - this.p2._$70) * aU;\n this.p2._$xT();\n}\n;\nfunction N() {\n this._$p = 1;\n this.x = 0;\n this.y = 0;\n this.vx = 0;\n this.vy = 0;\n this.ax = 0;\n this.ay = 0;\n this.fx = 0;\n this.fy = 0;\n this._$s0 = 0;\n this._$70 = 0;\n this._$7L = 0;\n this._$HL = 0;\n}\nN.prototype._$xT = function() {\n this._$s0 = this.x;\n this._$70 = this.y;\n this._$7L = this.vx;\n this._$HL = this.vy;\n}\n;\nfunction at(aJ, aI, aH) {\n this._$wL = null;\n this.scale = null;\n this._$V0 = null;\n this._$wL = aJ;\n this.scale = aI;\n this._$V0 = aH;\n}\nat.prototype._$oP = function(aI, aH) {}\n;\nfunction h(aJ, aK, aI, aH) {\n at.prototype.constructor.call(this, aK, aI, aH);\n this._$tL = null;\n this._$tL = aJ;\n}\nh.prototype = new at();\nh.prototype._$oP = function(aJ, aH) {\n var aK = this.scale * aJ.getParamFloat(this._$wL);\n var aL = aH.getPhysicsPoint1();\n switch (this._$tL) {\n default:\n case u.Src.SRC_TO_X:\n aL.x = aL.x + (aK - aL.x) * this._$V0;\n break;\n case u.Src.SRC_TO_Y:\n aL.y = aL.y + (aK - aL.y) * this._$V0;\n break;\n case u.Src.SRC_TO_G_ANGLE:\n var aI = aH._$qr();\n aI = aI + (aK - aI) * this._$V0;\n aH._$pr(aI);\n break;\n }\n}\n;\nfunction d(aJ, aI, aH) {\n this._$wL = null;\n this.scale = null;\n this._$V0 = null;\n this._$wL = aJ;\n this.scale = aI;\n this._$V0 = aH;\n}\nd.prototype._$YS = function(aI, aH) {}\n;\nfunction aF(aI, aK, aJ, aH) {\n d.prototype.constructor.call(this, aK, aJ, aH);\n this._$YP = null;\n this._$YP = aI;\n}\naF.prototype = new d();\naF.prototype._$YS = function(aI, aH) {\n switch (this._$YP) {\n default:\n case u.Target.TARGET_FROM_ANGLE:\n aI.setParamFloat(this._$wL, this.scale * aH._$5r(), this._$V0);\n break;\n case u.Target.TARGET_FROM_ANGLE_V:\n aI.setParamFloat(this._$wL, this.scale * aH._$Cs(), this._$V0);\n break;\n }\n}\n;\nu.Src = function() {}\n;\nu.Src.SRC_TO_X = \"SRC_TO_X\";\nu.Src.SRC_TO_Y = \"SRC_TO_Y\";\nu.Src.SRC_TO_G_ANGLE = \"SRC_TO_G_ANGLE\";\nu.Target = function() {}\n;\nu.Target.TARGET_FROM_ANGLE = \"TARGET_FROM_ANGLE\";\nu.Target.TARGET_FROM_ANGLE_V = \"TARGET_FROM_ANGLE_V\";\nfunction X() {\n if (j) {\n return;\n }\n this._$fL = 0;\n this._$gL = 0;\n this._$B0 = 1;\n this._$z0 = 1;\n this._$qT = 0;\n this.reflectX = false;\n this.reflectY = false;\n}\nX.prototype.init = function(aH) {\n this._$fL = aH._$fL;\n this._$gL = aH._$gL;\n this._$B0 = aH._$B0;\n this._$z0 = aH._$z0;\n this._$qT = aH._$qT;\n this.reflectX = aH.reflectX;\n this.reflectY = aH.reflectY;\n}\n;\nX.prototype._$F0 = function(aH) {\n this._$fL = aH._$_T();\n this._$gL = aH._$_T();\n this._$B0 = aH._$_T();\n this._$z0 = aH._$_T();\n this._$qT = aH._$_T();\n if (aH.getFormatVersion() >= ay.LIVE2D_FORMAT_VERSION_V2_10_SDK2) {\n this.reflectX = aH._$po();\n this.reflectY = aH._$po();\n }\n}\n;\nX.prototype._$e = function() {}\n;\nvar ad = function() {};\nad._$ni = function(aL, aJ, aR, aQ, aK, aI, aH, aS, aN) {\n var aM = (aH * aI - aS * aK);\n if (aM == 0) {\n return null;\n } else {\n var aO = ((aL - aR) * aI - (aJ - aQ) * aK) / aM;\n var aP;\n if (aK != 0) {\n aP = (aL - aR - aO * aH) / aK;\n } else {\n aP = (aJ - aQ - aO * aS) / aI;\n }\n if (isNaN(aP)) {\n aP = (aL - aR - aO * aH) / aK;\n if (isNaN(aP)) {\n aP = (aJ - aQ - aO * aS) / aI;\n }\n if (isNaN(aP)) {\n console.log(\"a is NaN @UtVector#_$ni() \");\n console.log(\"v1x : \" + aK);\n console.log(\"v1x != 0 ? \" + (aK != 0));\n }\n }\n if (aN == null) {\n return new Array(aP,aO);\n } else {\n aN[0] = aP;\n aN[1] = aO;\n return aN;\n }\n }\n}\n;\nfunction av() {\n if (j) {\n return;\n }\n this.x = null;\n this.y = null;\n this.width = null;\n this.height = null;\n}\nav.prototype._$8P = function() {\n return this.x + 0.5 * this.width;\n}\n;\nav.prototype._$6P = function() {\n return this.y + 0.5 * this.height;\n}\n;\nav.prototype._$EL = function() {\n return this.x + this.width;\n}\n;\nav.prototype._$5T = function() {\n return this.y + this.height;\n}\n;\nav.prototype._$jL = function(aI, aK, aJ, aH) {\n this.x = aI;\n this.y = aK;\n this.width = aJ;\n this.height = aH;\n}\n;\nav.prototype._$jL = function(aH) {\n this.x = aH.x;\n this.y = aH.y;\n this.width = aH.width;\n this.height = aH.height;\n}\n;\nav.prototype.contains = function(aH, aI) {\n return this.x <= this.x && this.y <= this.y && (this.x <= this.x + this.width) && (this.y <= this.y + this.height);\n}\n;\nav.prototype.expand = function(aH, aI) {\n this.x -= aH;\n this.y -= aI;\n this.width += aH * 2;\n this.height += aI * 2;\n}\n;\nfunction aG() {}\naG._$Z2 = function(bb, bo, bp, a2) {\n var a1 = bo._$Q2(bb, bp);\n var a3 = bb._$vs();\n var ba = bb._$Tr();\n bo._$zr(a3, ba, a1);\n if (a1 <= 0) {\n return a2[a3[0]];\n } else {\n if (a1 == 1) {\n var bj = a2[a3[0]];\n var bi = a2[a3[1]];\n var a9 = ba[0];\n return (bj + (bi - bj) * a9) | 0;\n } else {\n if (a1 == 2) {\n var bj = a2[a3[0]];\n var bi = a2[a3[1]];\n var a0 = a2[a3[2]];\n var aZ = a2[a3[3]];\n var a9 = ba[0];\n var a8 = ba[1];\n var br = (bj + (bi - bj) * a9) | 0;\n var bq = (a0 + (aZ - a0) * a9) | 0;\n return (br + (bq - br) * a8) | 0;\n } else {\n if (a1 == 3) {\n var aP = a2[a3[0]];\n var aO = a2[a3[1]];\n var bn = a2[a3[2]];\n var bm = a2[a3[3]];\n var aK = a2[a3[4]];\n var aJ = a2[a3[5]];\n var bg = a2[a3[6]];\n var bf = a2[a3[7]];\n var a9 = ba[0];\n var a8 = ba[1];\n var a6 = ba[2];\n var bj = (aP + (aO - aP) * a9) | 0;\n var bi = (bn + (bm - bn) * a9) | 0;\n var a0 = (aK + (aJ - aK) * a9) | 0;\n var aZ = (bg + (bf - bg) * a9) | 0;\n var br = (bj + (bi - bj) * a8) | 0;\n var bq = (a0 + (aZ - a0) * a8) | 0;\n return (br + (bq - br) * a6) | 0;\n } else {\n if (a1 == 4) {\n var aT = a2[a3[0]];\n var aS = a2[a3[1]];\n var bu = a2[a3[2]];\n var bt = a2[a3[3]];\n var aN = a2[a3[4]];\n var aM = a2[a3[5]];\n var bl = a2[a3[6]];\n var bk = a2[a3[7]];\n var be = a2[a3[8]];\n var bc = a2[a3[9]];\n var aX = a2[a3[10]];\n var aW = a2[a3[11]];\n var a7 = a2[a3[12]];\n var a5 = a2[a3[13]];\n var aR = a2[a3[14]];\n var aQ = a2[a3[15]];\n var a9 = ba[0];\n var a8 = ba[1];\n var a6 = ba[2];\n var a4 = ba[3];\n var aP = (aT + (aS - aT) * a9) | 0;\n var aO = (bu + (bt - bu) * a9) | 0;\n var bn = (aN + (aM - aN) * a9) | 0;\n var bm = (bl + (bk - bl) * a9) | 0;\n var aK = (be + (bc - be) * a9) | 0;\n var aJ = (aX + (aW - aX) * a9) | 0;\n var bg = (a7 + (a5 - a7) * a9) | 0;\n var bf = (aR + (aQ - aR) * a9) | 0;\n var bj = (aP + (aO - aP) * a8) | 0;\n var bi = (bn + (bm - bn) * a8) | 0;\n var a0 = (aK + (aJ - aK) * a8) | 0;\n var aZ = (bg + (bf - bg) * a8) | 0;\n var br = (bj + (bi - bj) * a6) | 0;\n var bq = (a0 + (aZ - a0) * a6) | 0;\n return (br + (bq - br) * a4) | 0;\n } else {\n var aV = 1 << a1;\n var aY = new Float32Array(aV);\n for (var bh = 0; bh < aV; bh++) {\n var aI = bh;\n var aH = 1;\n for (var aL = 0; aL < a1; aL++) {\n aH *= (aI % 2 == 0) ? (1 - ba[aL]) : ba[aL];\n aI /= 2;\n }\n aY[bh] = aH;\n }\n var bs = new Float32Array(aV);\n for (var aU = 0; aU < aV; aU++) {\n bs[aU] = a2[a3[aU]];\n }\n var bd = 0;\n for (var aU = 0; aU < aV; aU++) {\n bd += aY[aU] * bs[aU];\n }\n return (bd + 0.5) | 0;\n }\n }\n }\n }\n }\n}\n;\naG._$br = function(ba, bo, bp, bg) {\n var a1 = bo._$Q2(ba, bp);\n var a2 = ba._$vs();\n var a9 = ba._$Tr();\n bo._$zr(a2, a9, a1);\n if (a1 <= 0) {\n return bg[a2[0]];\n } else {\n if (a1 == 1) {\n var bj = bg[a2[0]];\n var bi = bg[a2[1]];\n var a8 = a9[0];\n return bj + (bi - bj) * a8;\n } else {\n if (a1 == 2) {\n var bj = bg[a2[0]];\n var bi = bg[a2[1]];\n var a0 = bg[a2[2]];\n var aZ = bg[a2[3]];\n var a8 = a9[0];\n var a7 = a9[1];\n return (1 - a7) * (bj + (bi - bj) * a8) + a7 * (a0 + (aZ - a0) * a8);\n } else {\n if (a1 == 3) {\n var aP = bg[a2[0]];\n var aO = bg[a2[1]];\n var bn = bg[a2[2]];\n var bm = bg[a2[3]];\n var aK = bg[a2[4]];\n var aJ = bg[a2[5]];\n var bf = bg[a2[6]];\n var be = bg[a2[7]];\n var a8 = a9[0];\n var a7 = a9[1];\n var a5 = a9[2];\n return (1 - a5) * ((1 - a7) * (aP + (aO - aP) * a8) + a7 * (bn + (bm - bn) * a8)) + a5 * ((1 - a7) * (aK + (aJ - aK) * a8) + a7 * (bf + (be - bf) * a8));\n } else {\n if (a1 == 4) {\n var aT = bg[a2[0]];\n var aS = bg[a2[1]];\n var bs = bg[a2[2]];\n var br = bg[a2[3]];\n var aN = bg[a2[4]];\n var aM = bg[a2[5]];\n var bl = bg[a2[6]];\n var bk = bg[a2[7]];\n var bd = bg[a2[8]];\n var bb = bg[a2[9]];\n var aX = bg[a2[10]];\n var aW = bg[a2[11]];\n var a6 = bg[a2[12]];\n var a4 = bg[a2[13]];\n var aR = bg[a2[14]];\n var aQ = bg[a2[15]];\n var a8 = a9[0];\n var a7 = a9[1];\n var a5 = a9[2];\n var a3 = a9[3];\n return (1 - a3) * ((1 - a5) * ((1 - a7) * (aT + (aS - aT) * a8) + a7 * (bs + (br - bs) * a8)) + a5 * ((1 - a7) * (aN + (aM - aN) * a8) + a7 * (bl + (bk - bl) * a8))) + a3 * ((1 - a5) * ((1 - a7) * (bd + (bb - bd) * a8) + a7 * (aX + (aW - aX) * a8)) + a5 * ((1 - a7) * (a6 + (a4 - a6) * a8) + a7 * (aR + (aQ - aR) * a8)));\n } else {\n var aV = 1 << a1;\n var aY = new Float32Array(aV);\n for (var bh = 0; bh < aV; bh++) {\n var aI = bh;\n var aH = 1;\n for (var aL = 0; aL < a1; aL++) {\n aH *= (aI % 2 == 0) ? (1 - a9[aL]) : a9[aL];\n aI /= 2;\n }\n aY[bh] = aH;\n }\n var bq = new Float32Array(aV);\n for (var aU = 0; aU < aV; aU++) {\n bq[aU] = bg[a2[aU]];\n }\n var bc = 0;\n for (var aU = 0; aU < aV; aU++) {\n bc += aY[aU] * bq[aU];\n }\n return bc;\n }\n }\n }\n }\n }\n}\n;\naG._$Vr = function(bV, bW, a5, aI, bC, a3, bX, bH) {\n var aN = bW._$Q2(bV, a5);\n var bw = bV._$vs();\n var a2 = bV._$Tr();\n bW._$zr(bw, a2, aN);\n var aJ = aI * 2;\n var aQ = bX;\n if (aN <= 0) {\n var bI = bw[0];\n var bq = bC[bI];\n if (bH == 2 && bX == 0) {\n P._$jT(bq, 0, a3, 0, aJ);\n } else {\n for (var bt = 0; bt < aJ; ) {\n a3[aQ] = bq[bt++];\n a3[aQ + 1] = bq[bt++];\n aQ += bH;\n }\n }\n } else {\n if (aN == 1) {\n var bq = bC[bw[0]];\n var bp = bC[bw[1]];\n var b3 = a2[0];\n var bT = 1 - b3;\n for (var bt = 0; bt < aJ; ) {\n a3[aQ] = bq[bt] * bT + bp[bt] * b3;\n ++bt;\n a3[aQ + 1] = bq[bt] * bT + bp[bt] * b3;\n ++bt;\n aQ += bH;\n }\n } else {\n if (aN == 2) {\n var bq = bC[bw[0]];\n var bp = bC[bw[1]];\n var aZ = bC[bw[2]];\n var aY = bC[bw[3]];\n var b3 = a2[0];\n var b1 = a2[1];\n var bT = 1 - b3;\n var bP = 1 - b1;\n var b2 = bP * bT;\n var b0 = bP * b3;\n var bM = b1 * bT;\n var bL = b1 * b3;\n for (var bt = 0; bt < aJ; ) {\n a3[aQ] = b2 * bq[bt] + b0 * bp[bt] + bM * aZ[bt] + bL * aY[bt];\n ++bt;\n a3[aQ + 1] = b2 * bq[bt] + b0 * bp[bt] + bM * aZ[bt] + bL * aY[bt];\n ++bt;\n aQ += bH;\n }\n } else {\n if (aN == 3) {\n var ba = bC[bw[0]];\n var a9 = bC[bw[1]];\n var aP = bC[bw[2]];\n var aO = bC[bw[3]];\n var a6 = bC[bw[4]];\n var a4 = bC[bw[5]];\n var aL = bC[bw[6]];\n var aK = bC[bw[7]];\n var b3 = a2[0];\n var b1 = a2[1];\n var bZ = a2[2];\n var bT = 1 - b3;\n var bP = 1 - b1;\n var bN = 1 - bZ;\n var b8 = bN * bP * bT;\n var b7 = bN * bP * b3;\n var bU = bN * b1 * bT;\n var bS = bN * b1 * b3;\n var b6 = bZ * bP * bT;\n var b5 = bZ * bP * b3;\n var bQ = bZ * b1 * bT;\n var bO = bZ * b1 * b3;\n for (var bt = 0; bt < aJ; ) {\n a3[aQ] = b8 * ba[bt] + b7 * a9[bt] + bU * aP[bt] + bS * aO[bt] + b6 * a6[bt] + b5 * a4[bt] + bQ * aL[bt] + bO * aK[bt];\n ++bt;\n a3[aQ + 1] = b8 * ba[bt] + b7 * a9[bt] + bU * aP[bt] + bS * aO[bt] + b6 * a6[bt] + b5 * a4[bt] + bQ * aL[bt] + bO * aK[bt];\n ++bt;\n aQ += bH;\n }\n } else {\n if (aN == 4) {\n var bD = bC[bw[0]];\n var bB = bC[bw[1]];\n var bo = bC[bw[2]];\n var bm = bC[bw[3]];\n var by = bC[bw[4]];\n var bx = bC[bw[5]];\n var be = bC[bw[6]];\n var bd = bC[bw[7]];\n var bG = bC[bw[8]];\n var bE = bC[bw[9]];\n var bv = bC[bw[10]];\n var bu = bC[bw[11]];\n var bA = bC[bw[12]];\n var bz = bC[bw[13]];\n var bn = bC[bw[14]];\n var bl = bC[bw[15]];\n var b3 = a2[0];\n var b1 = a2[1];\n var bZ = a2[2];\n var bY = a2[3];\n var bT = 1 - b3;\n var bP = 1 - b1;\n var bN = 1 - bZ;\n var bK = 1 - bY;\n var bk = bK * bN * bP * bT;\n var bi = bK * bN * bP * b3;\n var aW = bK * bN * b1 * bT;\n var aV = bK * bN * b1 * b3;\n var bc = bK * bZ * bP * bT;\n var bb = bK * bZ * bP * b3;\n var aS = bK * bZ * b1 * bT;\n var aR = bK * bZ * b1 * b3;\n var bs = bY * bN * bP * bT;\n var br = bY * bN * bP * b3;\n var a1 = bY * bN * b1 * bT;\n var a0 = bY * bN * b1 * b3;\n var bh = bY * bZ * bP * bT;\n var bf = bY * bZ * bP * b3;\n var aU = bY * bZ * b1 * bT;\n var aT = bY * bZ * b1 * b3;\n for (var bt = 0; bt < aJ; ) {\n a3[aQ] = bk * bD[bt] + bi * bB[bt] + aW * bo[bt] + aV * bm[bt] + bc * by[bt] + bb * bx[bt] + aS * be[bt] + aR * bd[bt] + bs * bG[bt] + br * bE[bt] + a1 * bv[bt] + a0 * bu[bt] + bh * bA[bt] + bf * bz[bt] + aU * bn[bt] + aT * bl[bt];\n ++bt;\n a3[aQ + 1] = bk * bD[bt] + bi * bB[bt] + aW * bo[bt] + aV * bm[bt] + bc * by[bt] + bb * bx[bt] + aS * be[bt] + aR * bd[bt] + bs * bG[bt] + br * bE[bt] + a1 * bv[bt] + a0 * bu[bt] + bh * bA[bt] + bf * bz[bt] + aU * bn[bt] + aT * bl[bt];\n ++bt;\n aQ += bH;\n }\n } else {\n var b4 = 1 << aN;\n var bJ = new Float32Array(b4);\n for (var bj = 0; bj < b4; bj++) {\n var aH = bj;\n var aM = 1;\n for (var bF = 0; bF < aN; bF++) {\n aM *= (aH % 2 == 0) ? (1 - a2[bF]) : a2[bF];\n aH /= 2;\n }\n bJ[bj] = aM;\n }\n var bg = new Float32Array(b4);\n for (var aX = 0; aX < b4; aX++) {\n bg[aX] = bC[bw[aX]];\n }\n for (var bt = 0; bt < aJ; ) {\n var a8 = 0\n , a7 = 0;\n var bR = bt + 1;\n for (var aX = 0; aX < b4; aX++) {\n a8 += bJ[aX] * bg[aX][bt];\n a7 += bJ[aX] * bg[aX][bR];\n }\n bt += 2;\n a3[aQ] = a8;\n a3[aQ + 1] = a7;\n aQ += bH;\n }\n }\n }\n }\n }\n }\n}\n;\nfunction e() {\n if (j) {\n return;\n }\n this.x = null;\n this.y = null;\n}\ne.prototype._$HT = function(aH, aI) {\n this.x = aH;\n this.y = aI;\n}\n;\ne.prototype._$HT = function(aH) {\n this.x = aH.x;\n this.y = aH.y;\n}\n;\nfunction ae() {\n if (j) {\n return;\n }\n this._$gP = null;\n this._$dr = null;\n this._$GS = null;\n this._$qb = null;\n this._$Lb = null;\n this._$mS = null;\n this.clipID = null;\n this.clipIDList = new Array();\n}\nae._$ur = -2;\nae._$ES = 500;\nae._$wb = 2;\nae._$8S = 3;\nae._$52 = ae._$ES;\nae._$R2 = ae._$ES;\nae._$or = function() {\n return ae._$52;\n}\n;\nae._$Pr = function() {\n return ae._$R2;\n}\n;\nae.prototype.convertClipIDForV2_11 = function(aI) {\n var aH = [];\n if (aI == null) {\n return null;\n }\n if (aI.length == 0) {\n return null;\n }\n if (!/,/.test(aI)) {\n aH.push(aI.id);\n return aH;\n }\n aH = aI.id.split(\",\");\n return aH;\n}\n;\nae.prototype._$F0 = function(aH) {\n this._$gP = aH._$nP();\n this._$dr = aH._$nP();\n this._$GS = aH._$nP();\n this._$qb = aH._$6L();\n this._$Lb = aH._$cS();\n this._$mS = aH._$Tb();\n if (aH.getFormatVersion() >= ay._$T7) {\n this.clipID = aH._$nP();\n this.clipIDList = this.convertClipIDForV2_11(this.clipID);\n } else {\n this.clipIDList = [];\n }\n this._$MS(this._$Lb);\n}\n;\nae.prototype.getClipIDList = function() {\n return this.clipIDList;\n}\n;\nae.prototype.init = function(aH) {}\n;\nae.prototype._$Nr = function(aH, aI) {\n aI._$IS[0] = false;\n aI._$Us = aG._$Z2(aH, this._$GS, aI._$IS, this._$Lb);\n if (Q._$Zs) {} else {\n if (aI._$IS[0]) {\n return;\n }\n }\n aI._$7s = aG._$br(aH, this._$GS, aI._$IS, this._$mS);\n}\n;\nae.prototype._$2b = function(aH, aI) {}\n;\nae.prototype.getDrawDataID = function() {\n return this._$gP;\n}\n;\nae.prototype._$j2 = function(aH) {\n this._$gP = aH;\n}\n;\nae.prototype.getOpacity = function(aH, aI) {\n return aI._$7s;\n}\n;\nae.prototype._$zS = function(aH, aI) {\n return aI._$Us;\n}\n;\nae.prototype._$MS = function(aJ) {\n for (var aI = aJ.length - 1; aI >= 0; --aI) {\n var aH = aJ[aI];\n if (aH < ae._$52) {\n ae._$52 = aH;\n } else {\n if (aH > ae._$R2) {\n ae._$R2 = aH;\n }\n }\n }\n}\n;\nae.prototype.getTargetBaseDataID = function() {\n return this._$dr;\n}\n;\nae.prototype._$gs = function(aH) {\n this._$dr = aH;\n}\n;\nae.prototype._$32 = function() {\n return (this._$dr != null && (this._$dr != n._$2o()));\n}\n;\nae.prototype.preDraw = function(aJ, aH, aI) {}\n;\nae.prototype.draw = function(aJ, aH, aI) {}\n;\nae.prototype.getType = function() {}\n;\nae.prototype._$B2 = function(aI, aH, aJ) {}\n;\nfunction ax() {\n if (j) {\n return;\n }\n this._$Eb = ax._$ps;\n this._$lT = 1;\n this._$C0 = 1;\n this._$tT = 1;\n this._$WL = 1;\n this.culling = false;\n this.matrix4x4 = new Float32Array(16);\n this.premultipliedAlpha = false;\n this.anisotropy = 0;\n this.clippingProcess = ax.CLIPPING_PROCESS_NONE;\n this.clipBufPre_clipContextMask = null;\n this.clipBufPre_clipContextDraw = null;\n this.CHANNEL_COLORS = new Array();\n}\nax._$ps = 32;\nax.CLIPPING_PROCESS_NONE = 0;\nax.CLIPPING_PROCESS_OVERWRITE_ALPHA = 1;\nax.CLIPPING_PROCESS_MULTIPLY_ALPHA = 2;\nax.CLIPPING_PROCESS_DRAW = 3;\nax.CLIPPING_PROCESS_CLEAR_ALPHA = 4;\nax.prototype.setChannelFlagAsColor = function(aH, aI) {\n this.CHANNEL_COLORS[aH] = aI;\n}\n;\nax.prototype.getChannelFlagAsColor = function(aH) {\n return this.CHANNEL_COLORS[aH];\n}\n;\nax.prototype._$ZT = function() {}\n;\nax.prototype._$Uo = function(aM, aK, aJ, aL, aN, aI, aH) {}\n;\nax.prototype._$Rs = function() {\n return -1;\n}\n;\nax.prototype._$Ds = function(aH) {}\n;\nax.prototype.setBaseColor = function(aK, aJ, aI, aH) {\n if (aK < 0) {\n aK = 0;\n } else {\n if (aK > 1) {\n aK = 1;\n }\n }\n if (aJ < 0) {\n aJ = 0;\n } else {\n if (aJ > 1) {\n aJ = 1;\n }\n }\n if (aI < 0) {\n aI = 0;\n } else {\n if (aI > 1) {\n aI = 1;\n }\n }\n if (aH < 0) {\n aH = 0;\n } else {\n if (aH > 1) {\n aH = 1;\n }\n }\n this._$lT = aK;\n this._$C0 = aJ;\n this._$tT = aI;\n this._$WL = aH;\n}\n;\nax.prototype._$WP = function(aH) {\n this.culling = aH;\n}\n;\nax.prototype.setMatrix = function(aH) {\n for (var aI = 0; aI < 16; aI++) {\n this.matrix4x4[aI] = aH[aI];\n }\n}\n;\nax.prototype._$IT = function() {\n return this.matrix4x4;\n}\n;\nax.prototype.setPremultipliedAlpha = function(aH) {\n this.premultipliedAlpha = aH;\n}\n;\nax.prototype.isPremultipliedAlpha = function() {\n return this.premultipliedAlpha;\n}\n;\nax.prototype.setAnisotropy = function(aH) {\n this.anisotropy = aH;\n}\n;\nax.prototype.getAnisotropy = function() {\n return this.anisotropy;\n}\n;\nax.prototype.getClippingProcess = function() {\n return this.clippingProcess;\n}\n;\nax.prototype.setClippingProcess = function(aH) {\n this.clippingProcess = aH;\n}\n;\nax.prototype.setClipBufPre_clipContextForMask = function(aH) {\n this.clipBufPre_clipContextMask = aH;\n}\n;\nax.prototype.getClipBufPre_clipContextMask = function() {\n return this.clipBufPre_clipContextMask;\n}\n;\nax.prototype.setClipBufPre_clipContextForDraw = function(aH) {\n this.clipBufPre_clipContextDraw = aH;\n}\n;\nax.prototype.getClipBufPre_clipContextDraw = function() {\n return this.clipBufPre_clipContextDraw;\n}\n;\nfunction o() {\n if (j) {\n return;\n }\n this.a = 1;\n this.r = 1;\n this.g = 1;\n this.b = 1;\n this.scale = 1;\n this._$ho = 1;\n this.blendMode = Q.L2D_COLOR_BLEND_MODE_MULT;\n}\nfunction c() {\n if (j) {\n return;\n }\n this._$kP = null;\n this._$dr = null;\n this._$Ai = true;\n this._$mS = null;\n}\nc._$ur = -2;\nc._$c2 = 1;\nc._$_b = 2;\nc.prototype._$F0 = function(aH) {\n this._$kP = aH._$nP();\n this._$dr = aH._$nP();\n}\n;\nc.prototype.readV2_opacity = function(aH) {\n if (aH.getFormatVersion() >= ay.LIVE2D_FORMAT_VERSION_V2_10_SDK2) {\n this._$mS = aH._$Tb();\n }\n}\n;\nc.prototype.init = function(aH) {}\n;\nc.prototype._$Nr = function(aI, aH) {}\n;\nc.prototype.interpolateOpacity = function(aJ, aK, aI, aH) {\n if (this._$mS == null) {\n aI.setInterpolatedOpacity(1);\n } else {\n aI.setInterpolatedOpacity(aG._$br(aJ, aK, aH, this._$mS));\n }\n}\n;\nc.prototype._$2b = function(aI, aH) {}\n;\nc.prototype._$nb = function(aL, aK, aM, aH, aI, aJ, aN) {}\n;\nc.prototype.getType = function() {}\n;\nc.prototype._$gs = function(aH) {\n this._$dr = aH;\n}\n;\nc.prototype._$a2 = function(aH) {\n this._$kP = aH;\n}\n;\nc.prototype.getTargetBaseDataID = function() {\n return this._$dr;\n}\n;\nc.prototype.getBaseDataID = function() {\n return this._$kP;\n}\n;\nc.prototype._$32 = function() {\n return (this._$dr != null && (this._$dr != n._$2o()));\n}\n;\nfunction P() {}\nP._$W2 = 0;\nP._$CS = P._$W2;\nP._$Mo = function() {\n return true;\n}\n;\nP._$XP = function(aI) {\n try {\n var aJ = getTimeMSec();\n while (getTimeMSec() - aJ < aI) {}\n } catch (aH) {\n aH._$Rb();\n }\n}\n;\nP.getUserTimeMSec = function() {\n return (P._$CS == P._$W2) ? P.getSystemTimeMSec() : P._$CS;\n}\n;\nP.setUserTimeMSec = function(aH) {\n P._$CS = aH;\n}\n;\nP.updateUserTimeMSec = function() {\n return (P._$CS = P.getSystemTimeMSec());\n}\n;\nP.getTimeMSec = function() {\n return new Date().getTime();\n}\n;\nP.getSystemTimeMSec = function() {\n return new Date().getTime();\n}\n;\nP._$Q = function(aH) {}\n;\nP._$jT = function(aM, aJ, aI, aL, aH) {\n for (var aK = 0; aK < aH; aK++) {\n aI[aL + aK] = aM[aJ + aK];\n }\n}\n;\nfunction aA() {\n if (j) {\n return;\n }\n this._$VP = 0;\n this._$wL = null;\n this._$GP = null;\n this._$8o = aA._$ds;\n this._$2r = -1;\n this._$O2 = 0;\n this._$ri = 0;\n}\naA._$ds = -2;\naA.prototype._$F0 = function(aH) {\n this._$wL = aH._$nP();\n this._$VP = aH._$6L();\n this._$GP = aH._$nP();\n}\n;\naA.prototype.getParamIndex = function(aH) {\n if (this._$2r != aH) {\n this._$8o = aA._$ds;\n }\n return this._$8o;\n}\n;\naA.prototype._$Pb = function(aI, aH) {\n this._$8o = aI;\n this._$2r = aH;\n}\n;\naA.prototype.getParamID = function() {\n return this._$wL;\n}\n;\naA.prototype._$yP = function(aH) {\n this._$wL = aH;\n}\n;\naA.prototype._$N2 = function() {\n return this._$VP;\n}\n;\naA.prototype._$d2 = function() {\n return this._$GP;\n}\n;\naA.prototype._$t2 = function(aI, aH) {\n this._$VP = aI;\n this._$GP = aH;\n}\n;\naA.prototype._$Lr = function() {\n return this._$O2;\n}\n;\naA.prototype._$wr = function(aH) {\n this._$O2 = aH;\n}\n;\naA.prototype._$SL = function() {\n return this._$ri;\n}\n;\naA.prototype._$AL = function(aH) {\n this._$ri = aH;\n}\n;\nfunction G() {}\nG.startsWith = function(aJ, aL, aK) {\n var aH = aL + aK.length;\n if (aH >= aJ.length) {\n return false;\n }\n for (var aI = aL; aI < aH; aI++) {\n if (G.getChar(aJ, aI) != aK.charAt(aI - aL)) {\n return false;\n }\n }\n return true;\n}\n;\nG.getChar = function(aI, aH) {\n return String.fromCharCode(aI.getUint8(aH));\n}\n;\nG.createString = function(aM, aL, aJ) {\n var aH = new ArrayBuffer(aJ * 2);\n var aK = new Uint16Array(aH);\n for (var aI = 0; aI < aJ; aI++) {\n aK[aI] = aM.getUint8(aL + aI);\n }\n return String.fromCharCode.apply(null, aK);\n}\n;\nG._$LS = function(aP, aM, aR, aK) {\n if (aP instanceof ArrayBuffer) {\n aP = new DataView(aP);\n }\n var aL = aR;\n var aJ = false;\n var aQ = false;\n var aS = 0;\n var aO = G.getChar(aP, aL);\n if (aO == \"-\") {\n aJ = true;\n aL++;\n }\n var aN = false;\n for (; aL < aM; aL++) {\n aO = G.getChar(aP, aL);\n switch (aO) {\n case \"0\":\n aS = aS * 10;\n break;\n case \"1\":\n aS = aS * 10 + 1;\n break;\n case \"2\":\n aS = aS * 10 + 2;\n break;\n case \"3\":\n aS = aS * 10 + 3;\n break;\n case \"4\":\n aS = aS * 10 + 4;\n break;\n case \"5\":\n aS = aS * 10 + 5;\n break;\n case \"6\":\n aS = aS * 10 + 6;\n break;\n case \"7\":\n aS = aS * 10 + 7;\n break;\n case \"8\":\n aS = aS * 10 + 8;\n break;\n case \"9\":\n aS = aS * 10 + 9;\n break;\n case \".\":\n aQ = true;\n aL++;\n aN = true;\n break;\n default:\n aN = true;\n break;\n }\n if (aN) {\n break;\n }\n }\n if (aQ) {\n var aI = 0.1;\n var aH = false;\n for (; aL < aM; aL++) {\n aO = G.getChar(aP, aL);\n switch (aO) {\n case \"0\":\n break;\n case \"1\":\n aS += aI * 1;\n break;\n case \"2\":\n aS += aI * 2;\n break;\n case \"3\":\n aS += aI * 3;\n break;\n case \"4\":\n aS += aI * 4;\n break;\n case \"5\":\n aS += aI * 5;\n break;\n case \"6\":\n aS += aI * 6;\n break;\n case \"7\":\n aS += aI * 7;\n break;\n case \"8\":\n aS += aI * 8;\n break;\n case \"9\":\n aS += aI * 9;\n break;\n default:\n aH = true;\n break;\n }\n aI *= 0.1;\n if (aH) {\n break;\n }\n }\n }\n if (aJ) {\n aS = -aS;\n }\n aK[0] = aL;\n return aS;\n}\n;\nfunction g() {\n if (j) {\n return;\n }\n this._$Ob = null;\n}\ng.prototype._$zP = function() {\n this._$Ob = new Array();\n}\n;\ng.prototype._$F0 = function(aH) {\n this._$Ob = aH._$nP();\n}\n;\ng.prototype._$Ur = function(aK) {\n if (aK._$WS()) {\n return true;\n }\n var aH = aK._$v2();\n for (var aJ = this._$Ob.length - 1; aJ >= 0; --aJ) {\n var aI = this._$Ob[aJ].getParamIndex(aH);\n if (aI == aA._$ds) {\n aI = aK.getParamIndex(this._$Ob[aJ].getParamID());\n }\n if (aK._$Xb(aI)) {\n return true;\n }\n }\n return false;\n}\n;\ng.prototype._$Q2 = function(aL, aV) {\n var aX = this._$Ob.length;\n var aJ = aL._$v2();\n var aN = 0;\n var aI;\n var aQ;\n for (var aK = 0; aK < aX; aK++) {\n var aH = this._$Ob[aK];\n aI = aH.getParamIndex(aJ);\n if (aI == aA._$ds) {\n aI = aL.getParamIndex(aH.getParamID());\n aH._$Pb(aI, aJ);\n }\n if (aI < 0) {\n throw new Exception(\"err 23242 : \" + aH.getParamID());\n }\n var aU = aI < 0 ? 0 : aL.getParamFloat(aI);\n aQ = aH._$N2();\n var aM = aH._$d2();\n var aP = -1;\n var aT = 0;\n var aS;\n var aR;\n if (aQ < 1) {} else {\n if (aQ == 1) {\n aS = aM[0];\n if (aS - aw._$J < aU && aU < aS + aw._$J) {\n aP = 0;\n aT = 0;\n } else {\n aP = 0;\n aV[0] = true;\n }\n } else {\n aS = aM[0];\n if (aU < aS - aw._$J) {\n aP = 0;\n aV[0] = true;\n } else {\n if (aU < aS + aw._$J) {\n aP = 0;\n } else {\n var aW = false;\n for (var aO = 1; aO < aQ; ++aO) {\n aR = aM[aO];\n if (aU < aR + aw._$J) {\n if (aR - aw._$J < aU) {\n aP = aO;\n } else {\n aP = aO - 1;\n aT = (aU - aS) / (aR - aS);\n aN++;\n }\n aW = true;\n break;\n }\n aS = aR;\n }\n if (!aW) {\n aP = aQ - 1;\n aT = 0;\n aV[0] = true;\n }\n }\n }\n }\n }\n aH._$wr(aP);\n aH._$AL(aT);\n }\n return aN;\n}\n;\ng.prototype._$zr = function(aN, aT, aP) {\n var aR = 1 << aP;\n if (aR + 1 > aw._$Qb) {\n console.log(\"err 23245\\n\");\n }\n var aS = this._$Ob.length;\n var aK = 1;\n var aH = 1;\n var aJ = 0;\n for (var aQ = 0; aQ < aR; ++aQ) {\n aN[aQ] = 0;\n }\n for (var aL = 0; aL < aS; ++aL) {\n var aI = this._$Ob[aL];\n if (aI._$SL() == 0) {\n var aO = aI._$Lr() * aK;\n if (aO < 0 && Q._$3T) {\n throw new Exception(\"err 23246\");\n }\n for (var aQ = 0; aQ < aR; ++aQ) {\n aN[aQ] += aO;\n }\n } else {\n var aO = aK * aI._$Lr();\n var aM = aK * (aI._$Lr() + 1);\n for (var aQ = 0; aQ < aR; ++aQ) {\n aN[aQ] += ((aQ / aH | 0) % 2 == 0) ? aO : aM;\n }\n aT[aJ++] = aI._$SL();\n aH *= 2;\n }\n aK *= aI._$N2();\n }\n aN[aR] = 65535;\n aT[aJ] = -1;\n}\n;\ng.prototype._$h2 = function(aJ, aH, aK) {\n var aM = new Float32Array(aH);\n for (var aL = 0; aL < aH; ++aL) {\n aM[aL] = aK[aL];\n }\n var aI = new aA();\n aI._$yP(aJ);\n aI._$t2(aH, aM);\n this._$Ob.push(aI);\n}\n;\ng.prototype._$J2 = function(aO) {\n var aN = aO;\n var aM = this._$Ob.length;\n for (var aK = 0; aK < aM; ++aK) {\n var aI = this._$Ob[aK];\n var aH = aI._$N2();\n var aJ = aN % aI._$N2();\n var aL = aI._$d2()[aJ];\n console.log(\"%s[%d]=%7.2f / \", aI.getParamID(), aJ, aL);\n aN /= aH;\n }\n console.log(\"\\n\");\n}\n;\ng.prototype.getParamCount = function() {\n return this._$Ob.length;\n}\n;\ng.prototype._$zs = function() {\n return this._$Ob;\n}\n;\nfunction ac() {\n this.m = new Float32Array(16);\n this.identity();\n}\nac.prototype.identity = function() {\n for (var aH = 0; aH < 16; aH++) {\n this.m[aH] = ((aH % 5) == 0) ? 1 : 0;\n }\n}\n;\nac.prototype.getArray = function() {\n return this.m;\n}\n;\nac.prototype.getCopyMatrix = function() {\n return new Float32Array(this.m);\n}\n;\nac.prototype.setMatrix = function(aI) {\n if (aI == null || aI.length != 16) {\n return;\n }\n for (var aH = 0; aH < 16; aH++) {\n this.m[aH] = aI[aH];\n }\n}\n;\nac.prototype.mult = function(aH, aJ, aI) {\n if (aJ == null) {\n return null;\n }\n if (this == aJ) {\n this.mult_safe(this.m, aH.m, aJ.m, aI);\n } else {\n this.mult_fast(this.m, aH.m, aJ.m, aI);\n }\n return aJ;\n}\n;\nac.prototype.mult_safe = function(aI, aH, aM, aJ) {\n if (aI == aM) {\n var aL = new Array(16);\n this.mult_fast(aI, aH, aL, aJ);\n for (var aK = 15; aK >= 0; --aK) {\n aM[aK] = aL[aK];\n }\n } else {\n this.mult_fast(aI, aH, aM, aJ);\n }\n}\n;\nac.prototype.mult_fast = function(aI, aH, aK, aJ) {\n if (aJ) {\n aK[0] = aI[0] * aH[0] + aI[4] * aH[1] + aI[8] * aH[2];\n aK[4] = aI[0] * aH[4] + aI[4] * aH[5] + aI[8] * aH[6];\n aK[8] = aI[0] * aH[8] + aI[4] * aH[9] + aI[8] * aH[10];\n aK[12] = aI[0] * aH[12] + aI[4] * aH[13] + aI[8] * aH[14] + aI[12];\n aK[1] = aI[1] * aH[0] + aI[5] * aH[1] + aI[9] * aH[2];\n aK[5] = aI[1] * aH[4] + aI[5] * aH[5] + aI[9] * aH[6];\n aK[9] = aI[1] * aH[8] + aI[5] * aH[9] + aI[9] * aH[10];\n aK[13] = aI[1] * aH[12] + aI[5] * aH[13] + aI[9] * aH[14] + aI[13];\n aK[2] = aI[2] * aH[0] + aI[6] * aH[1] + aI[10] * aH[2];\n aK[6] = aI[2] * aH[4] + aI[6] * aH[5] + aI[10] * aH[6];\n aK[10] = aI[2] * aH[8] + aI[6] * aH[9] + aI[10] * aH[10];\n aK[14] = aI[2] * aH[12] + aI[6] * aH[13] + aI[10] * aH[14] + aI[14];\n aK[3] = aK[7] = aK[11] = 0;\n aK[15] = 1;\n } else {\n aK[0] = aI[0] * aH[0] + aI[4] * aH[1] + aI[8] * aH[2] + aI[12] * aH[3];\n aK[4] = aI[0] * aH[4] + aI[4] * aH[5] + aI[8] * aH[6] + aI[12] * aH[7];\n aK[8] = aI[0] * aH[8] + aI[4] * aH[9] + aI[8] * aH[10] + aI[12] * aH[11];\n aK[12] = aI[0] * aH[12] + aI[4] * aH[13] + aI[8] * aH[14] + aI[12] * aH[15];\n aK[1] = aI[1] * aH[0] + aI[5] * aH[1] + aI[9] * aH[2] + aI[13] * aH[3];\n aK[5] = aI[1] * aH[4] + aI[5] * aH[5] + aI[9] * aH[6] + aI[13] * aH[7];\n aK[9] = aI[1] * aH[8] + aI[5] * aH[9] + aI[9] * aH[10] + aI[13] * aH[11];\n aK[13] = aI[1] * aH[12] + aI[5] * aH[13] + aI[9] * aH[14] + aI[13] * aH[15];\n aK[2] = aI[2] * aH[0] + aI[6] * aH[1] + aI[10] * aH[2] + aI[14] * aH[3];\n aK[6] = aI[2] * aH[4] + aI[6] * aH[5] + aI[10] * aH[6] + aI[14] * aH[7];\n aK[10] = aI[2] * aH[8] + aI[6] * aH[9] + aI[10] * aH[10] + aI[14] * aH[11];\n aK[14] = aI[2] * aH[12] + aI[6] * aH[13] + aI[10] * aH[14] + aI[14] * aH[15];\n aK[3] = aI[3] * aH[0] + aI[7] * aH[1] + aI[11] * aH[2] + aI[15] * aH[3];\n aK[7] = aI[3] * aH[4] + aI[7] * aH[5] + aI[11] * aH[6] + aI[15] * aH[7];\n aK[11] = aI[3] * aH[8] + aI[7] * aH[9] + aI[11] * aH[10] + aI[15] * aH[11];\n aK[15] = aI[3] * aH[12] + aI[7] * aH[13] + aI[11] * aH[14] + aI[15] * aH[15];\n }\n}\n;\nac.prototype.translate = function(aH, aJ, aI) {\n this.m[12] = this.m[0] * aH + this.m[4] * aJ + this.m[8] * aI + this.m[12];\n this.m[13] = this.m[1] * aH + this.m[5] * aJ + this.m[9] * aI + this.m[13];\n this.m[14] = this.m[2] * aH + this.m[6] * aJ + this.m[10] * aI + this.m[14];\n this.m[15] = this.m[3] * aH + this.m[7] * aJ + this.m[11] * aI + this.m[15];\n}\n;\nac.prototype.scale = function(aJ, aI, aH) {\n this.m[0] *= aJ;\n this.m[4] *= aI;\n this.m[8] *= aH;\n this.m[1] *= aJ;\n this.m[5] *= aI;\n this.m[9] *= aH;\n this.m[2] *= aJ;\n this.m[6] *= aI;\n this.m[10] *= aH;\n this.m[3] *= aJ;\n this.m[7] *= aI;\n this.m[11] *= aH;\n}\n;\nac.prototype.rotateX = function(aH) {\n var aK = aC.fcos(aH);\n var aJ = aC._$9(aH);\n var aI = this.m[4];\n this.m[4] = aI * aK + this.m[8] * aJ;\n this.m[8] = aI * -aJ + this.m[8] * aK;\n aI = this.m[5];\n this.m[5] = aI * aK + this.m[9] * aJ;\n this.m[9] = aI * -aJ + this.m[9] * aK;\n aI = this.m[6];\n this.m[6] = aI * aK + this.m[10] * aJ;\n this.m[10] = aI * -aJ + this.m[10] * aK;\n aI = this.m[7];\n this.m[7] = aI * aK + this.m[11] * aJ;\n this.m[11] = aI * -aJ + this.m[11] * aK;\n}\n;\nac.prototype.rotateY = function(aH) {\n var aK = aC.fcos(aH);\n var aJ = aC._$9(aH);\n var aI = this.m[0];\n this.m[0] = aI * aK + this.m[8] * -aJ;\n this.m[8] = aI * aJ + this.m[8] * aK;\n aI = this.m[1];\n this.m[1] = aI * aK + this.m[9] * -aJ;\n this.m[9] = aI * aJ + this.m[9] * aK;\n aI = m[2];\n this.m[2] = aI * aK + this.m[10] * -aJ;\n this.m[10] = aI * aJ + this.m[10] * aK;\n aI = m[3];\n this.m[3] = aI * aK + this.m[11] * -aJ;\n this.m[11] = aI * aJ + this.m[11] * aK;\n}\n;\nac.prototype.rotateZ = function(aH) {\n var aK = aC.fcos(aH);\n var aJ = aC._$9(aH);\n var aI = this.m[0];\n this.m[0] = aI * aK + this.m[4] * aJ;\n this.m[4] = aI * -aJ + this.m[4] * aK;\n aI = this.m[1];\n this.m[1] = aI * aK + this.m[5] * aJ;\n this.m[5] = aI * -aJ + this.m[5] * aK;\n aI = this.m[2];\n this.m[2] = aI * aK + this.m[6] * aJ;\n this.m[6] = aI * -aJ + this.m[6] * aK;\n aI = this.m[3];\n this.m[3] = aI * aK + this.m[7] * aJ;\n this.m[7] = aI * -aJ + this.m[7] * aK;\n}\n;\nfunction Z(aH) {\n if (j) {\n return;\n }\n ak.prototype.constructor.call(this, aH);\n}\nZ.prototype = new ak();\nZ._$tP = new Object();\nZ._$27 = function() {\n Z._$tP.clear();\n}\n;\nZ.getID = function(aH) {\n var aI = Z._$tP[aH];\n if (aI == null) {\n aI = new Z(aH);\n Z._$tP[aH] = aI;\n }\n return aI;\n}\n;\nZ.prototype._$3s = function() {\n return new Z();\n}\n;\nfunction aD() {\n if (j) {\n return;\n }\n this._$7 = 1;\n this._$f = 0;\n this._$H = 0;\n this._$g = 1;\n this._$k = 0;\n this._$w = 0;\n this._$hi = STATE_IDENTITY;\n this._$Z = _$pS;\n}\naD._$kS = -1;\naD._$pS = 0;\naD._$hb = 1;\naD.STATE_IDENTITY = 0;\naD._$gb = 1;\naD._$fo = 2;\naD._$go = 4;\naD.prototype.transform = function(aK, aI, aH) {\n var aT, aS, aR, aM, aL, aJ;\n var aQ = 0;\n var aN = 0;\n switch (this._$hi) {\n default:\n return;\n case (aD._$go | aD._$fo | aD._$gb):\n aT = this._$7;\n aS = this._$H;\n aR = this._$k;\n aM = this._$f;\n aL = this._$g;\n aJ = this._$w;\n while (--aH >= 0) {\n var aP = aK[aQ++];\n var aO = aK[aQ++];\n aI[aN++] = (aT * aP + aS * aO + aR);\n aI[aN++] = (aM * aP + aL * aO + aJ);\n }\n return;\n case (aD._$go | aD._$fo):\n aT = this._$7;\n aS = this._$H;\n aM = this._$f;\n aL = this._$g;\n while (--aH >= 0) {\n var aP = aK[aQ++];\n var aO = aK[aQ++];\n aI[aN++] = (aT * aP + aS * aO);\n aI[aN++] = (aM * aP + aL * aO);\n }\n return;\n case (aD._$go | aD._$gb):\n aS = this._$H;\n aR = this._$k;\n aM = this._$f;\n aJ = this._$w;\n while (--aH >= 0) {\n var aP = aK[aQ++];\n aI[aN++] = (aS * aK[aQ++] + aR);\n aI[aN++] = (aM * aP + aJ);\n }\n return;\n case (aD._$go):\n aS = this._$H;\n aM = this._$f;\n while (--aH >= 0) {\n var aP = aK[aQ++];\n aI[aN++] = (aS * aK[aQ++]);\n aI[aN++] = (aM * aP);\n }\n return;\n case (aD._$fo | aD._$gb):\n aT = this._$7;\n aR = this._$k;\n aL = this._$g;\n aJ = this._$w;\n while (--aH >= 0) {\n aI[aN++] = (aT * aK[aQ++] + aR);\n aI[aN++] = (aL * aK[aQ++] + aJ);\n }\n return;\n case (aD._$fo):\n aT = this._$7;\n aL = this._$g;\n while (--aH >= 0) {\n aI[aN++] = (aT * aK[aQ++]);\n aI[aN++] = (aL * aK[aQ++]);\n }\n return;\n case (aD._$gb):\n aR = this._$k;\n aJ = this._$w;\n while (--aH >= 0) {\n aI[aN++] = (aK[aQ++] + aR);\n aI[aN++] = (aK[aQ++] + aJ);\n }\n return;\n case (aD.STATE_IDENTITY):\n if (aK != aI || aQ != aN) {\n P._$jT(aK, aQ, aI, aN, aH * 2);\n }\n return;\n }\n}\n;\naD.prototype.update = function() {\n if (this._$H == 0 && this._$f == 0) {\n if (this._$7 == 1 && this._$g == 1) {\n if (this._$k == 0 && this._$w == 0) {\n this._$hi = aD.STATE_IDENTITY;\n this._$Z = aD._$pS;\n } else {\n this._$hi = aD._$gb;\n this._$Z = aD._$hb;\n }\n } else {\n if (this._$k == 0 && this._$w == 0) {\n this._$hi = aD._$fo;\n this._$Z = aD._$kS;\n } else {\n this._$hi = (aD._$fo | aD._$gb);\n this._$Z = aD._$kS;\n }\n }\n } else {\n if (this._$7 == 0 && this._$g == 0) {\n if (this._$k == 0 && this._$w == 0) {\n this._$hi = aD._$go;\n this._$Z = aD._$kS;\n } else {\n this._$hi = (aD._$go | aD._$gb);\n this._$Z = aD._$kS;\n }\n } else {\n if (this._$k == 0 && this._$w == 0) {\n this._$hi = (aD._$go | aD._$fo);\n this._$Z = aD._$kS;\n } else {\n this._$hi = (aD._$go | aD._$fo | aD._$gb);\n this._$Z = aD._$kS;\n }\n }\n }\n}\n;\naD.prototype._$RT = function(aK) {\n this._$IT(aK);\n var aJ = aK[0];\n var aH = aK[2];\n var aN = aK[1];\n var aM = aK[3];\n var aI = Math.sqrt(aJ * aJ + aN * aN);\n var aL = aJ * aM - aH * aN;\n if (aI == 0) {\n if (Q._$so) {\n console.log(\"affine._$RT() / rt==0\");\n }\n } else {\n aK[0] = aI;\n aK[1] = aL / aI;\n aK[2] = (aN * aM + aJ * aH) / aL;\n aK[3] = Math.atan2(aN, aJ);\n }\n}\n;\naD.prototype._$ho = function(aN, aM, aI, aH) {\n var aL = new Float32Array(6);\n var aK = new Float32Array(6);\n aN._$RT(aL);\n aM._$RT(aK);\n var aJ = new Float32Array(6);\n aJ[0] = aL[0] + (aK[0] - aL[0]) * aI;\n aJ[1] = aL[1] + (aK[1] - aL[1]) * aI;\n aJ[2] = aL[2] + (aK[2] - aL[2]) * aI;\n aJ[3] = aL[3] + (aK[3] - aL[3]) * aI;\n aJ[4] = aL[4] + (aK[4] - aL[4]) * aI;\n aJ[5] = aL[5] + (aK[5] - aL[5]) * aI;\n aH._$CT(aJ);\n}\n;\naD.prototype._$CT = function(aJ) {\n var aI = Math.cos(aJ[3]);\n var aH = Math.sin(aJ[3]);\n this._$7 = aJ[0] * aI;\n this._$f = aJ[0] * aH;\n this._$H = aJ[1] * (aJ[2] * aI - aH);\n this._$g = aJ[1] * (aJ[2] * aH + aI);\n this._$k = aJ[4];\n this._$w = aJ[5];\n this.update();\n}\n;\naD.prototype._$IT = function(aH) {\n aH[0] = this._$7;\n aH[1] = this._$f;\n aH[2] = this._$H;\n aH[3] = this._$g;\n aH[4] = this._$k;\n aH[5] = this._$w;\n}\n;\nfunction Y() {\n if (j) {\n return;\n }\n ah.prototype.constructor.call(this);\n this.motions = new Array();\n this._$7r = null;\n this._$7r = Y._$Co++;\n this._$D0 = 30;\n this._$yT = 0;\n this._$E = true;\n this.loopFadeIn = true;\n this._$AS = -1;\n _$a0();\n}\nY.prototype = new ah();\nY._$cs = \"VISIBLE:\";\nY._$ar = \"LAYOUT:\";\nY._$Co = 0;\nY._$D2 = [];\nY._$1T = 1;\nY.loadMotion = function(aR) {\n var aM = new Y();\n var aI = [0];\n var aP = aR.length;\n aM._$yT = 0;\n for (var aJ = 0; aJ < aP; ++aJ) {\n var aQ = (aR[aJ] & 255);\n if (aQ == \"\\n\" || aQ == \"\\r\") {\n continue;\n }\n if (aQ == \"#\") {\n for (; aJ < aP; ++aJ) {\n if (aR[aJ] == \"\\n\" || aR[aJ] == \"\\r\") {\n break;\n }\n }\n continue;\n }\n if (aQ == \"$\") {\n var aT = aJ;\n var aK = -1;\n for (; aJ < aP; ++aJ) {\n aQ = (aR[aJ] & 255);\n if (aQ == \"\\r\" || aQ == \"\\n\") {\n break;\n }\n if (aQ == \"=\") {\n aK = aJ;\n break;\n }\n }\n var aO = false;\n if (aK >= 0) {\n if (aK == aT + 4 && aR[aT + 1] == \"f\" && aR[aT + 2] == \"p\" && aR[aT + 3] == \"s\") {\n aO = true;\n }\n for (aJ = aK + 1; aJ < aP; ++aJ) {\n aQ = (aR[aJ] & 255);\n if (aQ == \"\\r\" || aQ == \"\\n\") {\n break;\n }\n if (aQ == \",\" || aQ == \" \" || aQ == \"\\t\") {\n continue;\n }\n var aL = G._$LS(aR, aP, aJ, aI);\n if (aI[0] > 0) {\n if (aO && 5 < aL && aL < 121) {\n aM._$D0 = aL;\n }\n }\n aJ = aI[0];\n }\n }\n for (; aJ < aP; ++aJ) {\n if (aR[aJ] == \"\\n\" || aR[aJ] == \"\\r\") {\n break;\n }\n }\n continue;\n }\n if ((\"a\" <= aQ && aQ <= \"z\") || (\"A\" <= aQ && aQ <= \"Z\") || aQ == \"_\") {\n var aT = aJ;\n var aK = -1;\n for (; aJ < aP; ++aJ) {\n aQ = (aR[aJ] & 255);\n if (aQ == \"\\r\" || aQ == \"\\n\") {\n break;\n }\n if (aQ == \"=\") {\n aK = aJ;\n break;\n }\n }\n if (aK >= 0) {\n var aN = new t();\n if (G.startsWith(aR, aT, Y._$cs)) {\n aN._$RP = t._$hs;\n aN._$4P = new String(aR,aT,aK - aT);\n } else {\n if (G.startsWith(aR, aT, Y._$ar)) {\n aN._$4P = new String(aR,aT + 7,aK - aT - 7);\n if (G.startsWith(aR, aT + 7, \"ANCHOR_X\")) {\n aN._$RP = t._$xs;\n } else {\n if (G.startsWith(aR, aT + 7, \"ANCHOR_Y\")) {\n aN._$RP = t._$us;\n } else {\n if (G.startsWith(aR, aT + 7, \"SCALE_X\")) {\n aN._$RP = t._$qs;\n } else {\n if (G.startsWith(aR, aT + 7, \"SCALE_Y\")) {\n aN._$RP = t._$Ys;\n } else {\n if (G.startsWith(aR, aT + 7, \"X\")) {\n aN._$RP = t._$ws;\n } else {\n if (G.startsWith(aR, aT + 7, \"Y\")) {\n aN._$RP = t._$Ns;\n }\n }\n }\n }\n }\n }\n } else {\n aN._$RP = t._$Fr;\n aN._$4P = new String(aR,aT,aK - aT);\n }\n }\n aM.motions.push(aN);\n var aS = 0;\n Y._$D2.clear();\n for (aJ = aK + 1; aJ < aP; ++aJ) {\n aQ = (aR[aJ] & 255);\n if (aQ == \"\\r\" || aQ == \"\\n\") {\n break;\n }\n if (aQ == \",\" || aQ == \" \" || aQ == \"\\t\") {\n continue;\n }\n var aL = G._$LS(aR, aP, aJ, aI);\n if (aI[0] > 0) {\n Y._$D2.push(aL);\n aS++;\n var aH = aI[0];\n if (aH < aJ) {\n console.log(\"_$n0 _$hi . @Live2DMotion loadMotion()\\n\");\n break;\n }\n aJ = aH;\n }\n }\n aN._$I0 = Y._$D2._$BL();\n if (aS > aM._$yT) {\n aM._$yT = aS;\n }\n }\n }\n }\n aM._$AS = ((1000 * aM._$yT) / aM._$D0) | 0;\n return aM;\n}\n;\nY.prototype.getDurationMSec = function() {\n return this._$AS;\n}\n;\nY.prototype.dump = function() {\n for (var aJ = 0; aJ < this.motions.length; aJ++) {\n var aH = this.motions[aJ];\n console.log(\"_$wL[%s] [%d]. \", aH._$4P, aH._$I0.length);\n for (var aI = 0; aI < aH._$I0.length && aI < 10; aI++) {\n console.log(\"%5.2f ,\", aH._$I0[aI]);\n }\n console.log(\"\\n\");\n }\n}\n;\nY.prototype.updateParamExe = function(aH, aL, aO, aX) {\n var aM = aL - aX._$z2;\n var aV = aM * this._$D0 / 1000;\n var aJ = aV | 0;\n var aP = aV - aJ;\n for (var aU = 0; aU < this.motions.length; aU++) {\n var aS = this.motions[aU];\n var aK = aS._$I0.length;\n var aQ = aS._$4P;\n if (aS._$RP == t._$hs) {\n var aT = aS._$I0[(aJ >= aK ? aK - 1 : aJ)];\n aH.setParamFloat(aQ, aT);\n } else {\n if (t._$ws <= aS._$RP && aS._$RP <= t._$Ys) {} else {\n var aR = aH.getParamFloat(aQ);\n var aY = aS._$I0[(aJ >= aK ? aK - 1 : aJ)];\n var aW = aS._$I0[(aJ + 1 >= aK ? aK - 1 : aJ + 1)];\n var aI = aY + (aW - aY) * aP;\n var aN = aR + (aI - aR) * aO;\n aH.setParamFloat(aQ, aN);\n }\n }\n }\n if (aJ >= this._$yT) {\n if (this._$E) {\n aX._$z2 = aL;\n if (this.loopFadeIn) {\n aX._$bs = aL;\n }\n } else {\n aX._$9L = true;\n }\n }\n}\n;\nY.prototype._$r0 = function() {\n return this._$E;\n}\n;\nY.prototype._$aL = function(aH) {\n this._$E = aH;\n}\n;\nY.prototype.isLoopFadeIn = function() {\n return this.loopFadeIn;\n}\n;\nY.prototype.setLoopFadeIn = function(aH) {\n this.loopFadeIn = aH;\n}\n;\nfunction aE() {\n this._$P = new Float32Array(100);\n this.size = 0;\n}\naE.prototype.clear = function() {\n this.size = 0;\n}\n;\naE.prototype.add = function(aI) {\n if (this._$P.length <= this.size) {\n var aH = new Float32Array(this.size * 2);\n P._$jT(this._$P, 0, aH, 0, this.size);\n this._$P = aH;\n }\n this._$P[this.size++] = aI;\n}\n;\naE.prototype._$BL = function() {\n var aH = new Float32Array(this.size);\n P._$jT(this._$P, 0, aH, 0, this.size);\n return aH;\n}\n;\nfunction t() {\n this._$4P = null;\n this._$I0 = null;\n this._$RP = null;\n}\nt._$Fr = 0;\nt._$hs = 1;\nt._$ws = 100;\nt._$Ns = 101;\nt._$xs = 102;\nt._$us = 103;\nt._$qs = 104;\nt._$Ys = 105;\nfunction aw() {}\naw._$Ms = 1;\naw._$Qs = 2;\naw._$i2 = 0;\naw._$No = 2;\naw._$do = aw._$Ms;\naw._$Ls = true;\naw._$1r = 5;\naw._$Qb = 65;\naw._$J = 0.0001;\naw._$FT = 0.001;\naw._$Ss = 3;\nfunction ay() {}\nay._$o7 = 6;\nay._$S7 = 7;\nay._$s7 = 8;\nay._$77 = 9;\nay.LIVE2D_FORMAT_VERSION_V2_10_SDK2 = 10;\nay.LIVE2D_FORMAT_VERSION_V2_11_SDK2_1 = 11;\nay._$T7 = ay.LIVE2D_FORMAT_VERSION_V2_11_SDK2_1;\nay._$Is = -2004318072;\nay._$h0 = 0;\nay._$4L = 23;\nay._$7P = 33;\nay._$uT = function(aH) {\n console.log(\"_$bo :: _$6 _$mo _$E0 : %d\\n\", aH);\n}\n;\nay._$9o = function(aH) {\n if (aH < 40) {\n ay._$uT(aH);\n return null;\n } else {\n if (aH < 50) {\n ay._$uT(aH);\n return null;\n } else {\n if (aH < 60) {\n ay._$uT(aH);\n return null;\n } else {\n if (aH < 100) {\n switch (aH) {\n case 65:\n return new E();\n case 66:\n return new g();\n case 67:\n return new aA();\n case 68:\n return new ab();\n case 69:\n return new X();\n case 70:\n return new b();\n default:\n ay._$uT(aH);\n return null;\n }\n } else {\n if (aH < 150) {\n switch (aH) {\n case 131:\n return new f();\n case 133:\n return new s();\n case 136:\n return new w();\n case 137:\n return new an();\n case 142:\n return new aq();\n }\n }\n }\n }\n }\n }\n ay._$uT(aH);\n return null;\n}\n;\nfunction y(aH) {\n if (j) {\n return;\n }\n this._$QT = true;\n this._$co = -1;\n this._$qo = 0;\n this._$pb = new Array(y._$is);\n this._$_2 = new Float32Array(y._$is);\n this._$vr = new Float32Array(y._$is);\n this._$Rr = new Float32Array(y._$is);\n this._$Or = new Float32Array(y._$is);\n this._$fs = new Float32Array(y._$is);\n this._$Js = new Array(y._$is);\n this._$3S = new Array();\n this._$aS = new Array();\n this._$Bo = null;\n this._$F2 = new Array();\n this._$db = new Array();\n this._$8b = new Array();\n this._$Hr = new Array();\n this._$Ws = null;\n this._$Vs = null;\n this._$Er = null;\n this._$Es = new Int16Array(aw._$Qb);\n this._$ZP = new Float32Array(aw._$1r * 2);\n this._$Ri = aH;\n this._$b0 = y._$HP++;\n this.clipManager = null;\n this.dp_webgl = null;\n}\ny._$HP = 0;\ny._$_0 = true;\ny._$V2 = -1;\ny._$W0 = -1;\ny._$jr = false;\ny._$ZS = true;\ny._$tr = (-1000000);\ny._$lr = (1000000);\ny._$is = 32;\ny._$e = false;\ny.prototype.getDrawDataIndex = function(aI) {\n for (var aH = this._$aS.length - 1; aH >= 0; --aH) {\n if (this._$aS[aH] != null && this._$aS[aH].getDrawDataID() == aI) {\n return aH;\n }\n }\n return -1;\n}\n;\ny.prototype.getDrawData = function(aH) {\n if (aH instanceof Z) {\n if (this._$Bo == null) {\n this._$Bo = new Object();\n var aJ = this._$aS.length;\n for (var aI = 0; aI < aJ; aI++) {\n var aL = this._$aS[aI];\n var aK = aL.getDrawDataID();\n if (aK == null) {\n continue;\n }\n this._$Bo[aK] = aL;\n }\n }\n return this._$Bo[id];\n } else {\n if (aH < this._$aS.length) {\n return this._$aS[aH];\n } else {\n return null;\n }\n }\n}\n;\ny.prototype.release = function() {\n this._$3S.clear();\n this._$aS.clear();\n this._$F2.clear();\n if (this._$Bo != null) {\n this._$Bo.clear();\n }\n this._$db.clear();\n this._$8b.clear();\n this._$Hr.clear();\n}\n;\ny.prototype.init = function() {\n this._$co++;\n if (this._$F2.length > 0) {\n this.release();\n }\n var aO = this._$Ri.getModelImpl();\n var aT = aO._$Xr();\n var aS = aT.length;\n var aH = new Array();\n var a3 = new Array();\n for (var aV = 0; aV < aS; ++aV) {\n var a4 = aT[aV];\n this._$F2.push(a4);\n this._$Hr.push(a4.init(this));\n var aK = a4.getBaseData();\n var aR = aK.length;\n for (var aU = 0; aU < aR; ++aU) {\n aH.push(aK[aU]);\n }\n for (var aU = 0; aU < aR; ++aU) {\n var aM = aK[aU].init(this);\n aM._$l2(aV);\n a3.push(aM);\n }\n var a1 = a4.getDrawData();\n var aP = a1.length;\n for (var aU = 0; aU < aP; ++aU) {\n var aZ = a1[aU];\n var a0 = aZ.init(this);\n a0._$IP = aV;\n this._$aS.push(aZ);\n this._$8b.push(a0);\n }\n }\n var aY = aH.length;\n var aN = n._$2o();\n while (true) {\n var aX = false;\n for (var aV = 0; aV < aY; ++aV) {\n var aL = aH[aV];\n if (aL == null) {\n continue;\n }\n var a2 = aL.getTargetBaseDataID();\n if (a2 == null || a2 == aN || this.getBaseDataIndex(a2) >= 0) {\n this._$3S.push(aL);\n this._$db.push(a3[aV]);\n aH[aV] = null;\n aX = true;\n }\n }\n if (!aX) {\n break;\n }\n }\n var aI = aO._$E2();\n if (aI != null) {\n var aJ = aI._$1s();\n if (aJ != null) {\n var aW = aJ.length;\n for (var aV = 0; aV < aW; ++aV) {\n var aQ = aJ[aV];\n if (aQ == null) {\n continue;\n }\n this._$02(aQ.getParamID(), aQ.getDefaultValue(), aQ.getMinValue(), aQ.getMaxValue());\n }\n }\n }\n this.clipManager = new W(this.dp_webgl);\n this.clipManager.init(this, this._$aS, this._$8b);\n this._$QT = true;\n}\n;\ny.prototype.update = function() {\n if (y._$e) {\n q.start(\"_$zL\");\n }\n var aK = this._$_2.length;\n for (var aW = 0; aW < aK; aW++) {\n if (this._$_2[aW] != this._$vr[aW]) {\n this._$Js[aW] = y._$ZS;\n this._$vr[aW] = this._$_2[aW];\n }\n }\n var aX = false;\n var aQ = this._$3S.length;\n var aN = this._$aS.length;\n var aS = a._$or();\n var aZ = a._$Pr();\n var aU = aZ - aS + 1;\n if (this._$Ws == null || this._$Ws.length < aU) {\n this._$Ws = new Int16Array(aU);\n this._$Vs = new Int16Array(aU);\n }\n for (var aW = 0; aW < aU; aW++) {\n this._$Ws[aW] = y._$V2;\n this._$Vs[aW] = y._$V2;\n }\n if (this._$Er == null || this._$Er.length < aN) {\n this._$Er = new Int16Array(aN);\n }\n for (var aW = 0; aW < aN; aW++) {\n this._$Er[aW] = y._$W0;\n }\n if (y._$e) {\n q.dump(\"_$zL\");\n }\n if (y._$e) {\n q.start(\"_$UL\");\n }\n var aL = null;\n for (var aV = 0; aV < aQ; ++aV) {\n var aJ = this._$3S[aV];\n var aH = this._$db[aV];\n try {\n aJ._$Nr(this, aH);\n aJ._$2b(this, aH);\n } catch (aY) {\n if (aL == null) {\n aL = aY;\n }\n }\n }\n if (aL != null) {\n if (y._$_0) {\n q._$Rb(aL);\n }\n }\n if (y._$e) {\n q.dump(\"_$UL\");\n }\n if (y._$e) {\n q.start(\"_$DL\");\n }\n var aR = null;\n for (var aO = 0; aO < aN; ++aO) {\n var aM = this._$aS[aO];\n var aI = this._$8b[aO];\n try {\n aM._$Nr(this, aI);\n if (aI._$u2()) {\n continue;\n }\n aM._$2b(this, aI);\n var aT = Math.floor(aM._$zS(this, aI) - aS);\n var aP;\n try {\n aP = this._$Vs[aT];\n } catch (aY) {\n console.log(\"_$li :: %s / %s @@_$fS\\n\", aY.toString(), aM.getDrawDataID().toString());\n aT = Math.floor(aM._$zS(this, aI) - aS);\n continue;\n }\n if (aP == y._$V2) {\n this._$Ws[aT] = aO;\n } else {\n this._$Er[aP] = aO;\n }\n this._$Vs[aT] = aO;\n } catch (aY) {\n if (aR == null) {\n aR = aY;\n Q._$sT(Q._$H7);\n }\n }\n }\n if (aR != null) {\n if (y._$_0) {\n q._$Rb(aR);\n }\n }\n if (y._$e) {\n q.dump(\"_$DL\");\n }\n if (y._$e) {\n q.start(\"_$eL\");\n }\n for (var aW = this._$Js.length - 1; aW >= 0; aW--) {\n this._$Js[aW] = y._$jr;\n }\n this._$QT = false;\n if (y._$e) {\n q.dump(\"_$eL\");\n }\n return aX;\n}\n;\ny.prototype.preDraw = function(aH) {\n if (this.clipManager != null) {\n aH._$ZT();\n this.clipManager.setupClip(this, aH);\n }\n}\n;\ny.prototype.draw = function(aM) {\n if (this._$Ws == null) {\n q._$li(\"call _$Ri.update() before _$Ri.draw() \");\n return;\n }\n var aP = this._$Ws.length;\n aM._$ZT();\n for (var aK = 0; aK < aP; ++aK) {\n var aN = this._$Ws[aK];\n if (aN == y._$V2) {\n continue;\n }\n do {\n var aH = this._$aS[aN];\n var aI = this._$8b[aN];\n if (aI._$yo()) {\n var aJ = aI._$IP;\n var aL = this._$Hr[aJ];\n aI._$VS = aL.getPartsOpacity();\n aH.draw(aM, this, aI);\n }\n var aO = this._$Er[aN];\n if (aO <= aN || aO == y._$W0) {\n break;\n }\n aN = aO;\n } while (true);\n }\n}\n;\ny.prototype.getParamIndex = function(aH) {\n for (var aI = this._$pb.length - 1; aI >= 0; --aI) {\n if (this._$pb[aI] == aH) {\n return aI;\n }\n }\n return this._$02(aH, 0, y._$tr, y._$lr);\n}\n;\ny.prototype._$BS = function(aH) {\n return this.getBaseDataIndex(aH);\n}\n;\ny.prototype.getBaseDataIndex = function(aH) {\n for (var aI = this._$3S.length - 1; aI >= 0; --aI) {\n if (this._$3S[aI] != null && this._$3S[aI].getBaseDataID() == aH) {\n return aI;\n }\n }\n return -1;\n}\n;\ny.prototype._$UT = function(aJ, aH) {\n var aI = new Float32Array(aH);\n P._$jT(aJ, 0, aI, 0, aJ.length);\n return aI;\n}\n;\ny.prototype._$02 = function(aN, aM, aL, aH) {\n if (this._$qo >= this._$pb.length) {\n var aK = this._$pb.length;\n var aJ = new Array(aK * 2);\n P._$jT(this._$pb, 0, aJ, 0, aK);\n this._$pb = aJ;\n this._$_2 = this._$UT(this._$_2, aK * 2);\n this._$vr = this._$UT(this._$vr, aK * 2);\n this._$Rr = this._$UT(this._$Rr, aK * 2);\n this._$Or = this._$UT(this._$Or, aK * 2);\n var aI = new Array();\n P._$jT(this._$Js, 0, aI, 0, aK);\n this._$Js = aI;\n }\n this._$pb[this._$qo] = aN;\n this._$_2[this._$qo] = aM;\n this._$vr[this._$qo] = aM;\n this._$Rr[this._$qo] = aL;\n this._$Or[this._$qo] = aH;\n this._$Js[this._$qo] = y._$ZS;\n return this._$qo++;\n}\n;\ny.prototype._$Zo = function(aI, aH) {\n this._$3S[aI] = aH;\n}\n;\ny.prototype.setParamFloat = function(aH, aI) {\n if (aI < this._$Rr[aH]) {\n aI = this._$Rr[aH];\n }\n if (aI > this._$Or[aH]) {\n aI = this._$Or[aH];\n }\n this._$_2[aH] = aI;\n}\n;\ny.prototype.loadParam = function() {\n var aH = this._$_2.length;\n if (aH > this._$fs.length) {\n aH = this._$fs.length;\n }\n P._$jT(this._$fs, 0, this._$_2, 0, aH);\n}\n;\ny.prototype.saveParam = function() {\n var aH = this._$_2.length;\n if (aH > this._$fs.length) {\n this._$fs = new Float32Array(aH);\n }\n P._$jT(this._$_2, 0, this._$fs, 0, aH);\n}\n;\ny.prototype._$v2 = function() {\n return this._$co;\n}\n;\ny.prototype._$WS = function() {\n return this._$QT;\n}\n;\ny.prototype._$Xb = function(aH) {\n return this._$Js[aH] == y._$ZS;\n}\n;\ny.prototype._$vs = function() {\n return this._$Es;\n}\n;\ny.prototype._$Tr = function() {\n return this._$ZP;\n}\n;\ny.prototype.getBaseData = function(aH) {\n return this._$3S[aH];\n}\n;\ny.prototype.getParamFloat = function(aH) {\n return this._$_2[aH];\n}\n;\ny.prototype.getParamMax = function(aH) {\n return this._$Or[aH];\n}\n;\ny.prototype.getParamMin = function(aH) {\n return this._$Rr[aH];\n}\n;\ny.prototype.setPartsOpacity = function(aJ, aH) {\n var aI = this._$Hr[aJ];\n aI.setPartsOpacity(aH);\n}\n;\ny.prototype.getPartsOpacity = function(aI) {\n var aH = this._$Hr[aI];\n return aH.getPartsOpacity();\n}\n;\ny.prototype.getPartsDataIndex = function(aI) {\n for (var aH = this._$F2.length - 1; aH >= 0; --aH) {\n if (this._$F2[aH] != null && this._$F2[aH]._$p2() == aI) {\n return aH;\n }\n }\n return -1;\n}\n;\ny.prototype._$q2 = function(aH) {\n return this._$db[aH];\n}\n;\ny.prototype._$C2 = function(aH) {\n return this._$8b[aH];\n}\n;\ny.prototype._$Bb = function(aH) {\n return this._$Hr[aH];\n}\n;\ny.prototype._$5s = function(aO, aK) {\n var aJ = this._$Ws.length;\n var aN = aO;\n for (var aL = 0; aL < aJ; ++aL) {\n var aI = this._$Ws[aL];\n if (aI == y._$V2) {\n continue;\n }\n do {\n var aM = this._$8b[aI];\n if (aM._$yo()) {\n aM._$GT()._$B2(this, aM, aN);\n aN += aK;\n }\n var aH = this._$Er[aI];\n if (aH <= aI || aH == y._$W0) {\n break;\n }\n aI = aH;\n } while (true);\n }\n}\n;\ny.prototype.setDrawParam = function(aH) {\n this.dp_webgl = aH;\n}\n;\ny.prototype.getDrawParam = function() {\n return this.dp_webgl;\n}\n;\nfunction ap() {}\nap._$0T = function(aH) {\n return ap._$0T(new _$5(aH));\n}\n;\nap._$0T = function(aJ) {\n if (!aJ.exists()) {\n throw new _$ls(aJ._$3b());\n }\n var aH = aJ.length();\n var aI = new Int8Array(aH);\n var aM = new _$Xs(new _$kb(aJ),8192);\n var aK;\n var aL = 0;\n while ((aK = aM.read(aI, aL, aH - aL)) > 0) {\n aL += aK;\n }\n return aI;\n}\n;\nap._$C = function(aJ) {\n var aI = null;\n var aL = null;\n try {\n aI = (aJ instanceof Array) ? aJ : new _$Xs(aJ,8192);\n aL = new _$js();\n var aM = 1000;\n var aK;\n var aH = new Int8Array(aM);\n while ((aK = aI.read(aH)) > 0) {\n aL.write(aH, 0, aK);\n }\n return aL._$TS();\n } finally {\n if (aJ != null) {\n aJ.close();\n }\n if (aL != null) {\n aL.flush();\n aL.close();\n }\n }\n}\n;\nfunction ar() {\n if (j) {\n return;\n }\n this._$12 = null;\n this._$bb = null;\n this._$_L = null;\n this._$jo = null;\n this._$iL = null;\n this._$0L = null;\n this._$Br = null;\n this._$Dr = null;\n this._$Cb = null;\n this._$mr = null;\n this._$_L = az.STATE_FIRST;\n this._$Br = 4000;\n this._$Dr = 100;\n this._$Cb = 50;\n this._$mr = 150;\n this._$jo = true;\n this._$iL = \"PARAM_EYE_L_OPEN\";\n this._$0L = \"PARAM_EYE_R_OPEN\";\n}\nar.prototype._$T2 = function() {\n var aI = P.getUserTimeMSec();\n var aH = Math._$10();\n return (aI + aH * (2 * this._$Br - 1));\n}\n;\nar.prototype._$uo = function(aH) {\n this._$Br = aH;\n}\n;\nar.prototype._$QS = function(aI, aH, aJ) {\n this._$Dr = aI;\n this._$Cb = aH;\n this._$mr = aJ;\n}\n;\nar.prototype._$7T = function(aI) {\n var aK = P.getUserTimeMSec();\n var aH;\n var aJ = 0;\n switch (this._$_L) {\n case STATE_CLOSING:\n aJ = (aK - this._$bb) / this._$Dr;\n if (aJ >= 1) {\n aJ = 1;\n this._$_L = az.STATE_CLOSED;\n this._$bb = aK;\n }\n aH = 1 - aJ;\n break;\n case STATE_CLOSED:\n aJ = (aK - this._$bb) / this._$Cb;\n if (aJ >= 1) {\n this._$_L = az.STATE_OPENING;\n this._$bb = aK;\n }\n aH = 0;\n break;\n case STATE_OPENING:\n aJ = (aK - this._$bb) / this._$mr;\n if (aJ >= 1) {\n aJ = 1;\n this._$_L = az.STATE_INTERVAL;\n this._$12 = this._$T2();\n }\n aH = aJ;\n break;\n case STATE_INTERVAL:\n if (this._$12 < aK) {\n this._$_L = az.STATE_CLOSING;\n this._$bb = aK;\n }\n aH = 1;\n break;\n case STATE_FIRST:\n default:\n this._$_L = az.STATE_INTERVAL;\n this._$12 = this._$T2();\n aH = 1;\n break;\n }\n if (!this._$jo) {\n aH = -aH;\n }\n aI.setParamFloat(this._$iL, aH);\n aI.setParamFloat(this._$0L, aH);\n}\n;\nvar az = function() {};\naz.STATE_FIRST = \"STATE_FIRST\";\naz.STATE_INTERVAL = \"STATE_INTERVAL\";\naz.STATE_CLOSING = \"STATE_CLOSING\";\naz.STATE_CLOSED = \"STATE_CLOSED\";\naz.STATE_OPENING = \"STATE_OPENING\";\nfunction x() {\n if (j) {\n return;\n }\n ax.prototype.constructor.call(this);\n this._$sb = new Int32Array(x._$As);\n this._$U2 = new Array();\n this.transform = null;\n this.gl = null;\n if (x._$NT == null) {\n x._$NT = x._$9r(256);\n x._$vS = x._$9r(256);\n x._$no = x._$vb(256);\n }\n}\nx.prototype = new ax();\nx._$As = 32;\nx._$Gr = false;\nx._$NT = null;\nx._$vS = null;\nx._$no = null;\nx._$9r = function(aH) {\n var aI = new Float32Array(aH);\n return aI;\n}\n;\nx._$vb = function(aH) {\n var aI = new Int16Array(aH);\n return aI;\n}\n;\nx._$cr = function(aI, aH) {\n if (aI == null || aI._$yL() < aH.length) {\n aI = x._$9r(aH.length * 2);\n aI.put(aH);\n aI._$oT(0);\n } else {\n aI.clear();\n aI.put(aH);\n aI._$oT(0);\n }\n return aI;\n}\n;\nx._$mb = function(aI, aH) {\n if (aI == null || aI._$yL() < aH.length) {\n aI = x._$vb(aH.length * 2);\n aI.put(aH);\n aI._$oT(0);\n } else {\n aI.clear();\n aI.put(aH);\n aI._$oT(0);\n }\n return aI;\n}\n;\nx._$Hs = function() {\n return x._$Gr;\n}\n;\nx._$as = function(aH) {\n x._$Gr = aH;\n}\n;\nx.prototype.setGL = function(aH) {\n this.gl = aH;\n}\n;\nx.prototype.setTransform = function(aH) {\n this.transform = aH;\n}\n;\nx.prototype._$ZT = function() {}\n;\nx.prototype._$Uo = function(aO, aH, aP, aI, aQ, aM, aK, aJ) {\n if (aM < 0.01) {\n return;\n }\n var aL = this._$U2[aO];\n var aN = aM > 0.9 ? Q.EXPAND_W : 0;\n this.gl.drawElements(aL, aP, aI, aQ, aM, aN, this.transform, aJ);\n}\n;\nx.prototype._$Rs = function() {\n throw new Error(\"_$Rs\");\n}\n;\nx.prototype._$Ds = function(aH) {\n throw new Error(\"_$Ds\");\n}\n;\nx.prototype._$K2 = function() {\n for (var aH = 0; aH < this._$sb.length; aH++) {\n var aI = this._$sb[aH];\n if (aI != 0) {\n this.gl._$Sr(1, this._$sb, aH);\n this._$sb[aH] = 0;\n }\n }\n}\n;\nx.prototype.setTexture = function(aI, aH) {\n if (this._$sb.length < aI + 1) {\n this._$nS(aI);\n }\n this._$sb[aI] = aH;\n}\n;\nx.prototype.setTexture = function(aH, aI) {\n if (this._$sb.length < aH + 1) {\n this._$nS(aH);\n }\n this._$U2[aH] = aI;\n}\n;\nx.prototype._$nS = function(aH) {\n var aK = Math.max(this._$sb.length * 2, aH + 1 + 10);\n var aI = new Int32Array(aK);\n P._$jT(this._$sb, 0, aI, 0, this._$sb.length);\n this._$sb = aI;\n var aJ = new Array();\n P._$jT(this._$U2, 0, aJ, 0, this._$U2.length);\n this._$U2 = aJ;\n}\n;\nfunction ab() {\n if (j) {\n return;\n }\n c.prototype.constructor.call(this);\n this._$GS = null;\n this._$Y0 = null;\n}\nab.prototype = new c();\nab._$Xo = new Float32Array(2);\nab._$io = new Float32Array(2);\nab._$0o = new Float32Array(2);\nab._$Lo = new Float32Array(2);\nab._$To = new Float32Array(2);\nab._$Po = new Float32Array(2);\nab._$gT = new Array();\nab.prototype._$zP = function() {\n this._$GS = new g();\n this._$GS._$zP();\n this._$Y0 = new Array();\n}\n;\nab.prototype.getType = function() {\n return c._$c2;\n}\n;\nab.prototype._$F0 = function(aH) {\n c.prototype._$F0.call(this, aH);\n this._$GS = aH._$nP();\n this._$Y0 = aH._$nP();\n c.prototype.readV2_opacity.call(this, aH);\n}\n;\nab.prototype.init = function(aH) {\n var aI = new al(this);\n aI._$Yr = new X();\n if (this._$32()) {\n aI._$Wr = new X();\n }\n return aI;\n}\n;\nab.prototype._$Nr = function(bf, bx) {\n if (!((this == bx._$GT()))) {\n console.log(\"### assert!! ### \");\n }\n var bm = bx;\n if (!this._$GS._$Ur(bf)) {\n return;\n }\n var bw = ab._$gT;\n bw[0] = false;\n var a2 = this._$GS._$Q2(bf, bw);\n bx._$Ib(bw[0]);\n this.interpolateOpacity(bf, this._$GS, bx, bw);\n var a3 = bf._$vs();\n var ba = bf._$Tr();\n this._$GS._$zr(a3, ba, a2);\n if (a2 <= 0) {\n var bn = this._$Y0[a3[0]];\n bm._$Yr.init(bn);\n } else {\n if (a2 == 1) {\n var bn = this._$Y0[a3[0]];\n var bl = this._$Y0[a3[1]];\n var a9 = ba[0];\n bm._$Yr._$fL = bn._$fL + (bl._$fL - bn._$fL) * a9;\n bm._$Yr._$gL = bn._$gL + (bl._$gL - bn._$gL) * a9;\n bm._$Yr._$B0 = bn._$B0 + (bl._$B0 - bn._$B0) * a9;\n bm._$Yr._$z0 = bn._$z0 + (bl._$z0 - bn._$z0) * a9;\n bm._$Yr._$qT = bn._$qT + (bl._$qT - bn._$qT) * a9;\n } else {\n if (a2 == 2) {\n var bn = this._$Y0[a3[0]];\n var bl = this._$Y0[a3[1]];\n var a1 = this._$Y0[a3[2]];\n var a0 = this._$Y0[a3[3]];\n var a9 = ba[0];\n var a8 = ba[1];\n var bC = bn._$fL + (bl._$fL - bn._$fL) * a9;\n var bB = a1._$fL + (a0._$fL - a1._$fL) * a9;\n bm._$Yr._$fL = bC + (bB - bC) * a8;\n bC = bn._$gL + (bl._$gL - bn._$gL) * a9;\n bB = a1._$gL + (a0._$gL - a1._$gL) * a9;\n bm._$Yr._$gL = bC + (bB - bC) * a8;\n bC = bn._$B0 + (bl._$B0 - bn._$B0) * a9;\n bB = a1._$B0 + (a0._$B0 - a1._$B0) * a9;\n bm._$Yr._$B0 = bC + (bB - bC) * a8;\n bC = bn._$z0 + (bl._$z0 - bn._$z0) * a9;\n bB = a1._$z0 + (a0._$z0 - a1._$z0) * a9;\n bm._$Yr._$z0 = bC + (bB - bC) * a8;\n bC = bn._$qT + (bl._$qT - bn._$qT) * a9;\n bB = a1._$qT + (a0._$qT - a1._$qT) * a9;\n bm._$Yr._$qT = bC + (bB - bC) * a8;\n } else {\n if (a2 == 3) {\n var aP = this._$Y0[a3[0]];\n var aO = this._$Y0[a3[1]];\n var bu = this._$Y0[a3[2]];\n var bs = this._$Y0[a3[3]];\n var aK = this._$Y0[a3[4]];\n var aJ = this._$Y0[a3[5]];\n var bj = this._$Y0[a3[6]];\n var bi = this._$Y0[a3[7]];\n var a9 = ba[0];\n var a8 = ba[1];\n var a6 = ba[2];\n var bC = aP._$fL + (aO._$fL - aP._$fL) * a9;\n var bB = bu._$fL + (bs._$fL - bu._$fL) * a9;\n var bz = aK._$fL + (aJ._$fL - aK._$fL) * a9;\n var by = bj._$fL + (bi._$fL - bj._$fL) * a9;\n bm._$Yr._$fL = (1 - a6) * (bC + (bB - bC) * a8) + a6 * (bz + (by - bz) * a8);\n bC = aP._$gL + (aO._$gL - aP._$gL) * a9;\n bB = bu._$gL + (bs._$gL - bu._$gL) * a9;\n bz = aK._$gL + (aJ._$gL - aK._$gL) * a9;\n by = bj._$gL + (bi._$gL - bj._$gL) * a9;\n bm._$Yr._$gL = (1 - a6) * (bC + (bB - bC) * a8) + a6 * (bz + (by - bz) * a8);\n bC = aP._$B0 + (aO._$B0 - aP._$B0) * a9;\n bB = bu._$B0 + (bs._$B0 - bu._$B0) * a9;\n bz = aK._$B0 + (aJ._$B0 - aK._$B0) * a9;\n by = bj._$B0 + (bi._$B0 - bj._$B0) * a9;\n bm._$Yr._$B0 = (1 - a6) * (bC + (bB - bC) * a8) + a6 * (bz + (by - bz) * a8);\n bC = aP._$z0 + (aO._$z0 - aP._$z0) * a9;\n bB = bu._$z0 + (bs._$z0 - bu._$z0) * a9;\n bz = aK._$z0 + (aJ._$z0 - aK._$z0) * a9;\n by = bj._$z0 + (bi._$z0 - bj._$z0) * a9;\n bm._$Yr._$z0 = (1 - a6) * (bC + (bB - bC) * a8) + a6 * (bz + (by - bz) * a8);\n bC = aP._$qT + (aO._$qT - aP._$qT) * a9;\n bB = bu._$qT + (bs._$qT - bu._$qT) * a9;\n bz = aK._$qT + (aJ._$qT - aK._$qT) * a9;\n by = bj._$qT + (bi._$qT - bj._$qT) * a9;\n bm._$Yr._$qT = (1 - a6) * (bC + (bB - bC) * a8) + a6 * (bz + (by - bz) * a8);\n } else {\n if (a2 == 4) {\n var aT = this._$Y0[a3[0]];\n var aS = this._$Y0[a3[1]];\n var bE = this._$Y0[a3[2]];\n var bD = this._$Y0[a3[3]];\n var aN = this._$Y0[a3[4]];\n var aM = this._$Y0[a3[5]];\n var bp = this._$Y0[a3[6]];\n var bo = this._$Y0[a3[7]];\n var bh = this._$Y0[a3[8]];\n var bg = this._$Y0[a3[9]];\n var aY = this._$Y0[a3[10]];\n var aW = this._$Y0[a3[11]];\n var a7 = this._$Y0[a3[12]];\n var a5 = this._$Y0[a3[13]];\n var aR = this._$Y0[a3[14]];\n var aQ = this._$Y0[a3[15]];\n var a9 = ba[0];\n var a8 = ba[1];\n var a6 = ba[2];\n var a4 = ba[3];\n var bC = aT._$fL + (aS._$fL - aT._$fL) * a9;\n var bB = bE._$fL + (bD._$fL - bE._$fL) * a9;\n var bz = aN._$fL + (aM._$fL - aN._$fL) * a9;\n var by = bp._$fL + (bo._$fL - bp._$fL) * a9;\n var bv = bh._$fL + (bg._$fL - bh._$fL) * a9;\n var bt = aY._$fL + (aW._$fL - aY._$fL) * a9;\n var br = a7._$fL + (a5._$fL - a7._$fL) * a9;\n var bq = aR._$fL + (aQ._$fL - aR._$fL) * a9;\n bm._$Yr._$fL = (1 - a4) * ((1 - a6) * (bC + (bB - bC) * a8) + a6 * (bz + (by - bz) * a8)) + a4 * ((1 - a6) * (bv + (bt - bv) * a8) + a6 * (br + (bq - br) * a8));\n bC = aT._$gL + (aS._$gL - aT._$gL) * a9;\n bB = bE._$gL + (bD._$gL - bE._$gL) * a9;\n bz = aN._$gL + (aM._$gL - aN._$gL) * a9;\n by = bp._$gL + (bo._$gL - bp._$gL) * a9;\n bv = bh._$gL + (bg._$gL - bh._$gL) * a9;\n bt = aY._$gL + (aW._$gL - aY._$gL) * a9;\n br = a7._$gL + (a5._$gL - a7._$gL) * a9;\n bq = aR._$gL + (aQ._$gL - aR._$gL) * a9;\n bm._$Yr._$gL = (1 - a4) * ((1 - a6) * (bC + (bB - bC) * a8) + a6 * (bz + (by - bz) * a8)) + a4 * ((1 - a6) * (bv + (bt - bv) * a8) + a6 * (br + (bq - br) * a8));\n bC = aT._$B0 + (aS._$B0 - aT._$B0) * a9;\n bB = bE._$B0 + (bD._$B0 - bE._$B0) * a9;\n bz = aN._$B0 + (aM._$B0 - aN._$B0) * a9;\n by = bp._$B0 + (bo._$B0 - bp._$B0) * a9;\n bv = bh._$B0 + (bg._$B0 - bh._$B0) * a9;\n bt = aY._$B0 + (aW._$B0 - aY._$B0) * a9;\n br = a7._$B0 + (a5._$B0 - a7._$B0) * a9;\n bq = aR._$B0 + (aQ._$B0 - aR._$B0) * a9;\n bm._$Yr._$B0 = (1 - a4) * ((1 - a6) * (bC + (bB - bC) * a8) + a6 * (bz + (by - bz) * a8)) + a4 * ((1 - a6) * (bv + (bt - bv) * a8) + a6 * (br + (bq - br) * a8));\n bC = aT._$z0 + (aS._$z0 - aT._$z0) * a9;\n bB = bE._$z0 + (bD._$z0 - bE._$z0) * a9;\n bz = aN._$z0 + (aM._$z0 - aN._$z0) * a9;\n by = bp._$z0 + (bo._$z0 - bp._$z0) * a9;\n bv = bh._$z0 + (bg._$z0 - bh._$z0) * a9;\n bt = aY._$z0 + (aW._$z0 - aY._$z0) * a9;\n br = a7._$z0 + (a5._$z0 - a7._$z0) * a9;\n bq = aR._$z0 + (aQ._$z0 - aR._$z0) * a9;\n bm._$Yr._$z0 = (1 - a4) * ((1 - a6) * (bC + (bB - bC) * a8) + a6 * (bz + (by - bz) * a8)) + a4 * ((1 - a6) * (bv + (bt - bv) * a8) + a6 * (br + (bq - br) * a8));\n bC = aT._$qT + (aS._$qT - aT._$qT) * a9;\n bB = bE._$qT + (bD._$qT - bE._$qT) * a9;\n bz = aN._$qT + (aM._$qT - aN._$qT) * a9;\n by = bp._$qT + (bo._$qT - bp._$qT) * a9;\n bv = bh._$qT + (bg._$qT - bh._$qT) * a9;\n bt = aY._$qT + (aW._$qT - aY._$qT) * a9;\n br = a7._$qT + (a5._$qT - a7._$qT) * a9;\n bq = aR._$qT + (aQ._$qT - aR._$qT) * a9;\n bm._$Yr._$qT = (1 - a4) * ((1 - a6) * (bC + (bB - bC) * a8) + a6 * (bz + (by - bz) * a8)) + a4 * ((1 - a6) * (bv + (bt - bv) * a8) + a6 * (br + (bq - br) * a8));\n } else {\n var aV = Math.pow(2, a2) | 0;\n var aZ = new Float32Array(aV);\n for (var bk = 0; bk < aV; bk++) {\n var aI = bk;\n var aH = 1;\n for (var aL = 0; aL < a2; aL++) {\n aH *= (aI % 2 == 0) ? (1 - ba[aL]) : ba[aL];\n aI /= 2;\n }\n aZ[bk] = aH;\n }\n var bA = new Array();\n for (var aU = 0; aU < aV; aU++) {\n bA[aU] = this._$Y0[a3[aU]];\n }\n var be = 0\n , bc = 0\n , bd = 0\n , bb = 0\n , aX = 0;\n for (var aU = 0; aU < aV; aU++) {\n be += aZ[aU] * bA[aU]._$fL;\n bc += aZ[aU] * bA[aU]._$gL;\n bd += aZ[aU] * bA[aU]._$B0;\n bb += aZ[aU] * bA[aU]._$z0;\n aX += aZ[aU] * bA[aU]._$qT;\n }\n bm._$Yr._$fL = be;\n bm._$Yr._$gL = bc;\n bm._$Yr._$B0 = bd;\n bm._$Yr._$z0 = bb;\n bm._$Yr._$qT = aX;\n }\n }\n }\n }\n }\n var bn = this._$Y0[a3[0]];\n bm._$Yr.reflectX = bn.reflectX;\n bm._$Yr.reflectY = bn.reflectY;\n}\n;\nab.prototype._$2b = function(aM, aH) {\n if (!((this == aH._$GT()))) {\n console.log(\"### assert!! ### \");\n }\n var aR = aH;\n aR._$hS(true);\n if (!this._$32()) {\n aR.setTotalScale_notForClient(aR._$Yr._$B0);\n aR.setTotalOpacity(aR.getInterpolatedOpacity());\n } else {\n var aT = this.getTargetBaseDataID();\n if (aR._$8r == c._$ur) {\n aR._$8r = aM.getBaseDataIndex(aT);\n }\n if (aR._$8r < 0) {\n if (Q._$so) {\n q._$li(\"_$L _$0P _$G :: %s\", aT);\n }\n aR._$hS(false);\n } else {\n var aI = aM.getBaseData(aR._$8r);\n if (aI != null) {\n var aL = aM._$q2(aR._$8r);\n var aS = ab._$Xo;\n aS[0] = aR._$Yr._$fL;\n aS[1] = aR._$Yr._$gL;\n var aJ = ab._$io;\n aJ[0] = 0;\n aJ[1] = -0.1;\n var aO = aL._$GT().getType();\n if (aO == c._$c2) {\n aJ[1] = -10;\n } else {\n aJ[1] = -0.1;\n }\n var aQ = ab._$0o;\n this._$Jr(aM, aI, aL, aS, aJ, aQ);\n var aP = aC._$92(aJ, aQ);\n aI._$nb(aM, aL, aS, aS, 1, 0, 2);\n aR._$Wr._$fL = aS[0];\n aR._$Wr._$gL = aS[1];\n aR._$Wr._$B0 = aR._$Yr._$B0;\n aR._$Wr._$z0 = aR._$Yr._$z0;\n aR._$Wr._$qT = aR._$Yr._$qT - aP * aC._$NS;\n var aK = aL.getTotalScale();\n aR.setTotalScale_notForClient(aK * aR._$Wr._$B0);\n var aN = aL.getTotalOpacity();\n aR.setTotalOpacity(aN * aR.getInterpolatedOpacity());\n aR._$Wr.reflectX = aR._$Yr.reflectX;\n aR._$Wr.reflectY = aR._$Yr.reflectY;\n aR._$hS(aL._$yo());\n } else {\n aR._$hS(false);\n }\n }\n }\n}\n;\nab.prototype._$nb = function(aJ, aR, aL, a4, aT, aO, a2) {\n if (!((this == aR._$GT()))) {\n console.log(\"### assert!! ### \");\n }\n var aH = aR;\n var aU = aH._$Wr != null ? aH._$Wr : aH._$Yr;\n var a0 = Math.sin(aC._$bS * aU._$qT);\n var aP = Math.cos(aC._$bS * aU._$qT);\n var a3 = aH.getTotalScale();\n var aW = aU.reflectX ? -1 : 1;\n var aV = aU.reflectY ? -1 : 1;\n var aS = aP * a3 * aW;\n var aQ = -a0 * a3 * aV;\n var a1 = a0 * a3 * aW;\n var aZ = aP * a3 * aV;\n var aY = aU._$fL;\n var aX = aU._$gL;\n var aN, aM;\n var aI = aT * a2;\n for (var aK = aO; aK < aI; aK += a2) {\n aN = aL[aK];\n aM = aL[aK + 1];\n a4[aK] = aS * aN + aQ * aM + aY;\n a4[aK + 1] = a1 * aN + aZ * aM + aX;\n }\n}\n;\nab.prototype._$Jr = function(aP, aK, aI, aR, aQ, aH) {\n if (!((aK == aI._$GT()))) {\n console.log(\"### assert!! ### \");\n }\n var aO = ab._$Lo;\n ab._$Lo[0] = aR[0];\n ab._$Lo[1] = aR[1];\n aK._$nb(aP, aI, aO, aO, 1, 0, 2);\n var aL = ab._$To;\n var aS = ab._$Po;\n var aN = 10;\n var aJ = 1;\n for (var aM = 0; aM < aN; aM++) {\n aS[0] = aR[0] + aJ * aQ[0];\n aS[1] = aR[1] + aJ * aQ[1];\n aK._$nb(aP, aI, aS, aL, 1, 0, 2);\n aL[0] -= aO[0];\n aL[1] -= aO[1];\n if (aL[0] != 0 || aL[1] != 0) {\n aH[0] = aL[0];\n aH[1] = aL[1];\n return;\n }\n aS[0] = aR[0] - aJ * aQ[0];\n aS[1] = aR[1] - aJ * aQ[1];\n aK._$nb(aP, aI, aS, aL, 1, 0, 2);\n aL[0] -= aO[0];\n aL[1] -= aO[1];\n if (aL[0] != 0 || aL[1] != 0) {\n aL[0] = -aL[0];\n aL[0] = -aL[0];\n aH[0] = aL[0];\n aH[1] = aL[1];\n return;\n }\n aJ *= 0.1;\n }\n if (Q._$so) {\n console.log(\"_$L0 to transform _$SP\\n\");\n }\n}\n;\nfunction al(aH) {\n B.prototype.constructor.call(this, aH);\n this._$8r = c._$ur;\n this._$Yr = null;\n this._$Wr = null;\n}\nal.prototype = new B();\nfunction a() {\n if (j) {\n return;\n }\n ae.prototype.constructor.call(this);\n this._$gP = null;\n this._$dr = null;\n this._$GS = null;\n this._$qb = null;\n this._$Lb = null;\n this._$mS = null;\n}\na.prototype = new ae();\na._$ur = -2;\na._$ES = 500;\na._$wb = 2;\na._$8S = 3;\na._$os = 4;\na._$52 = a._$ES;\na._$R2 = a._$ES;\na._$Sb = function(aJ) {\n for (var aI = aJ.length - 1; aI >= 0; --aI) {\n var aH = aJ[aI];\n if (aH < a._$52) {\n a._$52 = aH;\n } else {\n if (aH > a._$R2) {\n a._$R2 = aH;\n }\n }\n }\n}\n;\na._$or = function() {\n return a._$52;\n}\n;\na._$Pr = function() {\n return a._$R2;\n}\n;\na.prototype._$F0 = function(aH) {\n this._$gP = aH._$nP();\n this._$dr = aH._$nP();\n this._$GS = aH._$nP();\n this._$qb = aH._$6L();\n this._$Lb = aH._$cS();\n this._$mS = aH._$Tb();\n if (aH.getFormatVersion() >= ay._$T7) {\n this.clipID = aH._$nP();\n this.clipIDList = this.convertClipIDForV2_11(this.clipID);\n } else {\n this.clipIDList = null;\n }\n a._$Sb(this._$Lb);\n}\n;\na.prototype.getClipIDList = function() {\n return this.clipIDList;\n}\n;\na.prototype._$Nr = function(aI, aH) {\n aH._$IS[0] = false;\n aH._$Us = aG._$Z2(aI, this._$GS, aH._$IS, this._$Lb);\n if (Q._$Zs) {} else {\n if (aH._$IS[0]) {\n return;\n }\n }\n aH._$7s = aG._$br(aI, this._$GS, aH._$IS, this._$mS);\n}\n;\na.prototype._$2b = function(aH) {}\n;\na.prototype.getDrawDataID = function() {\n return this._$gP;\n}\n;\na.prototype._$j2 = function(aH) {\n this._$gP = aH;\n}\n;\na.prototype.getOpacity = function(aH, aI) {\n return aI._$7s;\n}\n;\na.prototype._$zS = function(aH, aI) {\n return aI._$Us;\n}\n;\na.prototype.getTargetBaseDataID = function() {\n return this._$dr;\n}\n;\na.prototype._$gs = function(aH) {\n this._$dr = aH;\n}\n;\na.prototype._$32 = function() {\n return (this._$dr != null && (this._$dr != n._$2o()));\n}\n;\na.prototype.getType = function() {}\n;\nfunction aq() {\n if (j) {\n return;\n }\n this._$NL = null;\n this._$3S = null;\n this._$aS = null;\n aq._$42++;\n}\naq._$42 = 0;\naq.prototype._$1b = function() {\n return this._$3S;\n}\n;\naq.prototype.getDrawDataList = function() {\n return this._$aS;\n}\n;\naq.prototype._$F0 = function(aH) {\n this._$NL = aH._$nP();\n this._$aS = aH._$nP();\n this._$3S = aH._$nP();\n}\n;\naq.prototype._$kr = function(aH) {\n aH._$Zo(this._$3S);\n aH._$xo(this._$aS);\n this._$3S = null;\n this._$aS = null;\n}\n;\nfunction v() {\n if (j) {\n return;\n }\n aa.prototype.constructor.call(this);\n this._$zo = new x();\n}\nv.prototype = new aa();\nv.loadModel = function(aI) {\n var aH = new v();\n aa._$62(aH, aI);\n return aH;\n}\n;\nv.loadModel = function(aI) {\n var aH = new v();\n aa._$62(aH, aI);\n return aH;\n}\n;\nv._$to = function() {\n var aH = new v();\n return aH;\n}\n;\nv._$er = function(aM) {\n var aJ = new _$5(\"../_$_r/_$t0/_$Ri/_$_P._$d\");\n if (aJ.exists() == false) {\n throw new _$ls(\"_$t0 _$_ _$6 _$Ui :: \" + aJ._$PL());\n }\n var aH = [\"../_$_r/_$t0/_$Ri/_$_P.512/_$CP._$1\", \"../_$_r/_$t0/_$Ri/_$_P.512/_$vP._$1\", \"../_$_r/_$t0/_$Ri/_$_P.512/_$EP._$1\", \"../_$_r/_$t0/_$Ri/_$_P.512/_$pP._$1\"];\n var aK = v.loadModel(aJ._$3b());\n for (var aI = 0; aI < aH.length; aI++) {\n var aL = new _$5(aH[aI]);\n if (aL.exists() == false) {\n throw new _$ls(\"_$t0 _$_ _$6 _$Ui :: \" + aL._$PL());\n }\n aK.setTexture(aI, _$nL._$_o(aM, aL._$3b()));\n }\n return aK;\n}\n;\nv.prototype.setGL = function(aH) {\n this._$zo.setGL(aH);\n}\n;\nv.prototype.setTransform = function(aH) {\n this._$zo.setTransform(aH);\n}\n;\nv.prototype.draw = function() {\n this._$5S.draw(this._$zo);\n}\n;\nv.prototype._$K2 = function() {\n this._$zo._$K2();\n}\n;\nv.prototype.setTexture = function(aI, aH) {\n if (this._$zo == null) {\n q._$li(\"_$Yi for QT _$ki / _$XS() is _$6 _$ui!!\");\n }\n this._$zo.setTexture(aI, aH);\n}\n;\nv.prototype.setTexture = function(aI, aH) {\n if (this._$zo == null) {\n q._$li(\"_$Yi for QT _$ki / _$XS() is _$6 _$ui!!\");\n }\n this._$zo.setTexture(aI, aH);\n}\n;\nv.prototype._$Rs = function() {\n return this._$zo._$Rs();\n}\n;\nv.prototype._$Ds = function(aH) {\n this._$zo._$Ds(aH);\n}\n;\nv.prototype.getDrawParam = function() {\n return this._$zo;\n}\n;\nfunction ao() {\n if (j) {\n return;\n }\n ah.prototype.constructor.call(this);\n this.motions = new Array();\n this._$o2 = null;\n this._$7r = ao._$Co++;\n this._$D0 = 30;\n this._$yT = 0;\n this._$E = false;\n this.loopFadeIn = true;\n this._$rr = -1;\n this._$eP = 0;\n}\nao.prototype = new ah();\nao._$cs = \"VISIBLE:\";\nao._$ar = \"LAYOUT:\";\nao.MTN_PREFIX_FADEIN = \"FADEIN:\";\nao.MTN_PREFIX_FADEOUT = \"FADEOUT:\";\nao._$Co = 0;\nao._$1T = 1;\nao.loadMotion = function(aJ) {\n var aI = ap._$C(aJ);\n var aH = ao.loadMotion(aI);\n return aH;\n}\n;\nfunction p(aI, aH) {\n return String.fromCharCode(aI.getUint8(aH));\n}\nao.loadMotion = function(aT) {\n if (aT instanceof ArrayBuffer) {\n aT = new DataView(aT);\n }\n var aN = new ao();\n var aI = [0];\n var aQ = aT.byteLength;\n aN._$yT = 0;\n for (var aJ = 0; aJ < aQ; ++aJ) {\n var aS = p(aT, aJ);\n var aL = aS.charCodeAt(0);\n if (aS == \"\\n\" || aS == \"\\r\") {\n continue;\n }\n if (aS == \"#\") {\n for (; aJ < aQ; ++aJ) {\n if (p(aT, aJ) == \"\\n\" || p(aT, aJ) == \"\\r\") {\n break;\n }\n }\n continue;\n }\n if (aS == \"$\") {\n var aV = aJ;\n var aK = -1;\n for (; aJ < aQ; ++aJ) {\n aS = p(aT, aJ);\n if (aS == \"\\r\" || aS == \"\\n\") {\n break;\n }\n if (aS == \"=\") {\n aK = aJ;\n break;\n }\n }\n var aP = false;\n if (aK >= 0) {\n if (aK == aV + 4 && p(aT, aV + 1) == \"f\" && p(aT, aV + 2) == \"p\" && p(aT, aV + 3) == \"s\") {\n aP = true;\n }\n for (aJ = aK + 1; aJ < aQ; ++aJ) {\n aS = p(aT, aJ);\n if (aS == \"\\r\" || aS == \"\\n\") {\n break;\n }\n if (aS == \",\" || aS == \" \" || aS == \"\\t\") {\n continue;\n }\n var aM = G._$LS(aT, aQ, aJ, aI);\n if (aI[0] > 0) {\n if (aP && 5 < aM && aM < 121) {\n aN._$D0 = aM;\n }\n }\n aJ = aI[0];\n }\n }\n for (; aJ < aQ; ++aJ) {\n if (p(aT, aJ) == \"\\n\" || p(aT, aJ) == \"\\r\") {\n break;\n }\n }\n continue;\n }\n if ((97 <= aL && aL <= 122) || (65 <= aL && aL <= 90) || aS == \"_\") {\n var aV = aJ;\n var aK = -1;\n for (; aJ < aQ; ++aJ) {\n aS = p(aT, aJ);\n if (aS == \"\\r\" || aS == \"\\n\") {\n break;\n }\n if (aS == \"=\") {\n aK = aJ;\n break;\n }\n }\n if (aK >= 0) {\n var aO = new t();\n if (G.startsWith(aT, aV, ao._$cs)) {\n aO._$RP = t._$hs;\n aO._$4P = G.createString(aT, aV, aK - aV);\n } else {\n if (G.startsWith(aT, aV, ao._$ar)) {\n aO._$4P = G.createString(aT, aV + 7, aK - aV - 7);\n if (G.startsWith(aT, aV + 7, \"ANCHOR_X\")) {\n aO._$RP = t._$xs;\n } else {\n if (G.startsWith(aT, aV + 7, \"ANCHOR_Y\")) {\n aO._$RP = t._$us;\n } else {\n if (G.startsWith(aT, aV + 7, \"SCALE_X\")) {\n aO._$RP = t._$qs;\n } else {\n if (G.startsWith(aT, aV + 7, \"SCALE_Y\")) {\n aO._$RP = t._$Ys;\n } else {\n if (G.startsWith(aT, aV + 7, \"X\")) {\n aO._$RP = t._$ws;\n } else {\n if (G.startsWith(aT, aV + 7, \"Y\")) {\n aO._$RP = t._$Ns;\n }\n }\n }\n }\n }\n }\n } else {\n aO._$RP = t._$Fr;\n aO._$4P = G.createString(aT, aV, aK - aV);\n }\n }\n aN.motions.push(aO);\n var aU = 0;\n var aR = [];\n for (aJ = aK + 1; aJ < aQ; ++aJ) {\n aS = p(aT, aJ);\n if (aS == \"\\r\" || aS == \"\\n\") {\n break;\n }\n if (aS == \",\" || aS == \" \" || aS == \"\\t\") {\n continue;\n }\n var aM = G._$LS(aT, aQ, aJ, aI);\n if (aI[0] > 0) {\n aR.push(aM);\n aU++;\n var aH = aI[0];\n if (aH < aJ) {\n console.log(\"_$n0 _$hi . @Live2DMotion loadMotion()\\n\");\n break;\n }\n aJ = aH - 1;\n }\n }\n aO._$I0 = new Float32Array(aR);\n if (aU > aN._$yT) {\n aN._$yT = aU;\n }\n }\n }\n }\n aN._$rr = ((1000 * aN._$yT) / aN._$D0) | 0;\n return aN;\n}\n;\nao.prototype.getDurationMSec = function() {\n return this._$E ? -1 : this._$rr;\n}\n;\nao.prototype.getLoopDurationMSec = function() {\n return this._$rr;\n}\n;\nao.prototype.dump = function() {\n for (var aJ = 0; aJ < this.motions.length; aJ++) {\n var aH = this.motions[aJ];\n console.log(\"_$wL[%s] [%d]. \", aH._$4P, aH._$I0.length);\n for (var aI = 0; aI < aH._$I0.length && aI < 10; aI++) {\n console.log(\"%5.2f ,\", aH._$I0[aI]);\n }\n console.log(\"\\n\");\n }\n}\n;\nao.prototype.updateParamExe = function(aJ, aN, aQ, a3) {\n var aO = aN - a3._$z2;\n var a0 = aO * this._$D0 / 1000;\n var aK = a0 | 0;\n var aR = a0 - aK;\n for (var aZ = 0; aZ < this.motions.length; aZ++) {\n var aV = this.motions[aZ];\n var aL = aV._$I0.length;\n var aT = aV._$4P;\n if (aV._$RP == t._$hs) {\n var aX = aV._$I0[(aK >= aL ? aL - 1 : aK)];\n aJ.setParamFloat(aT, aX);\n } else {\n if (t._$ws <= aV._$RP && aV._$RP <= t._$Ys) {} else {\n var aH = aJ.getParamIndex(aT);\n var a4 = aJ.getModelContext();\n var aY = a4.getParamMax(aH);\n var aW = a4.getParamMin(aH);\n var aM = 0.4;\n var aS = aM * (aY - aW);\n var aU = a4.getParamFloat(aH);\n var a2 = aV._$I0[(aK >= aL ? aL - 1 : aK)];\n var a1 = aV._$I0[(aK + 1 >= aL ? aL - 1 : aK + 1)];\n var aI;\n if ((a2 < a1 && a1 - a2 > aS) || (a2 > a1 && a2 - a1 > aS)) {\n aI = a2;\n } else {\n aI = a2 + (a1 - a2) * aR;\n }\n var aP = aU + (aI - aU) * aQ;\n aJ.setParamFloat(aT, aP);\n }\n }\n }\n if (aK >= this._$yT) {\n if (this._$E) {\n a3._$z2 = aN;\n if (this.loopFadeIn) {\n a3._$bs = aN;\n }\n } else {\n a3._$9L = true;\n }\n }\n this._$eP = aQ;\n}\n;\nao.prototype._$r0 = function() {\n return this._$E;\n}\n;\nao.prototype._$aL = function(aH) {\n this._$E = aH;\n}\n;\nao.prototype._$S0 = function() {\n return this._$D0;\n}\n;\nao.prototype._$U0 = function(aH) {\n this._$D0 = aH;\n}\n;\nao.prototype.isLoopFadeIn = function() {\n return this.loopFadeIn;\n}\n;\nao.prototype.setLoopFadeIn = function(aH) {\n this.loopFadeIn = aH;\n}\n;\nfunction aE() {\n this._$P = new Float32Array(100);\n this.size = 0;\n}\naE.prototype.clear = function() {\n this.size = 0;\n}\n;\naE.prototype.add = function(aI) {\n if (this._$P.length <= this.size) {\n var aH = new Float32Array(this.size * 2);\n P._$jT(this._$P, 0, aH, 0, this.size);\n this._$P = aH;\n }\n this._$P[this.size++] = aI;\n}\n;\naE.prototype._$BL = function() {\n var aH = new Float32Array(this.size);\n P._$jT(this._$P, 0, aH, 0, this.size);\n return aH;\n}\n;\nfunction t() {\n this._$4P = null;\n this._$I0 = null;\n this._$RP = null;\n}\nt._$Fr = 0;\nt._$hs = 1;\nt._$ws = 100;\nt._$Ns = 101;\nt._$xs = 102;\nt._$us = 103;\nt._$qs = 104;\nt._$Ys = 105;\nfunction E() {\n if (j) {\n return;\n }\n c.prototype.constructor.call(this);\n this._$o = 0;\n this._$A = 0;\n this._$GS = null;\n this._$Eo = null;\n}\nE.prototype = new c();\nE._$gT = new Array();\nE.prototype._$zP = function() {\n this._$GS = new g();\n this._$GS._$zP();\n}\n;\nE.prototype._$F0 = function(aH) {\n c.prototype._$F0.call(this, aH);\n this._$A = aH._$6L();\n this._$o = aH._$6L();\n this._$GS = aH._$nP();\n this._$Eo = aH._$nP();\n c.prototype.readV2_opacity.call(this, aH);\n}\n;\nE.prototype.init = function(aH) {\n var aI = new H(this);\n var aJ = (this._$o + 1) * (this._$A + 1);\n if (aI._$Cr != null) {\n aI._$Cr = null;\n }\n aI._$Cr = new Float32Array(aJ * 2);\n if (aI._$hr != null) {\n aI._$hr = null;\n }\n if (this._$32()) {\n aI._$hr = new Float32Array(aJ * 2);\n } else {\n aI._$hr = null;\n }\n return aI;\n}\n;\nE.prototype._$Nr = function(aJ, aI) {\n var aK = aI;\n if (!this._$GS._$Ur(aJ)) {\n return;\n }\n var aL = this._$VT();\n var aH = E._$gT;\n aH[0] = false;\n aG._$Vr(aJ, this._$GS, aH, aL, this._$Eo, aK._$Cr, 0, 2);\n aI._$Ib(aH[0]);\n this.interpolateOpacity(aJ, this._$GS, aI, aH);\n}\n;\nE.prototype._$2b = function(aK, aJ) {\n var aL = aJ;\n aL._$hS(true);\n if (!this._$32()) {\n aL.setTotalOpacity(aL.getInterpolatedOpacity());\n } else {\n var aH = this.getTargetBaseDataID();\n if (aL._$8r == c._$ur) {\n aL._$8r = aK.getBaseDataIndex(aH);\n }\n if (aL._$8r < 0) {\n if (Q._$so) {\n q._$li(\"_$L _$0P _$G :: %s\", aH);\n }\n aL._$hS(false);\n } else {\n var aN = aK.getBaseData(aL._$8r);\n var aI = aK._$q2(aL._$8r);\n if (aN != null && aI._$yo()) {\n var aM = aI.getTotalScale();\n aL.setTotalScale_notForClient(aM);\n var aO = aI.getTotalOpacity();\n aL.setTotalOpacity(aO * aL.getInterpolatedOpacity());\n aN._$nb(aK, aI, aL._$Cr, aL._$hr, this._$VT(), 0, 2);\n aL._$hS(true);\n } else {\n aL._$hS(false);\n }\n }\n }\n}\n;\nE.prototype._$nb = function(aL, aI, aH, aM, aO, aK, aJ) {\n if (true) {\n var aN = aI;\n var aP = (aN._$hr != null) ? aN._$hr : aN._$Cr;\n E.transformPoints_sdk2(aH, aM, aO, aK, aJ, aP, this._$o, this._$A);\n } else {\n this.transformPoints_sdk1(aL, aI, aH, aM, aO, aK, aJ);\n }\n}\n;\nE.transformPoints_sdk2 = function(a0, bc, a5, aP, aI, aR, aQ, aU) {\n var aW = a5 * aI;\n var aV;\n var bn, bm;\n var aT = 0;\n var aS = 0;\n var bl = 0;\n var bk = 0;\n var bf = 0;\n var be = 0;\n var aZ = false;\n for (var ba = aP; ba < aW; ba += aI) {\n var bd, a7, a4, aX;\n a4 = a0[ba];\n aX = a0[ba + 1];\n bd = a4 * aQ;\n a7 = aX * aU;\n if (bd < 0 || a7 < 0 || aQ <= bd || aU <= a7) {\n var a1 = aQ + 1;\n if (!aZ) {\n aZ = true;\n aT = 0.25 * (aR[((0) + (0) * a1) * 2] + aR[((aQ) + (0) * a1) * 2] + aR[((0) + (aU) * a1) * 2] + aR[((aQ) + (aU) * a1) * 2]);\n aS = 0.25 * (aR[((0) + (0) * a1) * 2 + 1] + aR[((aQ) + (0) * a1) * 2 + 1] + aR[((0) + (aU) * a1) * 2 + 1] + aR[((aQ) + (aU) * a1) * 2 + 1]);\n var aM = aR[((aQ) + (aU) * a1) * 2] - aR[((0) + (0) * a1) * 2];\n var aL = aR[((aQ) + (aU) * a1) * 2 + 1] - aR[((0) + (0) * a1) * 2 + 1];\n var bh = aR[((aQ) + (0) * a1) * 2] - aR[((0) + (aU) * a1) * 2];\n var bg = aR[((aQ) + (0) * a1) * 2 + 1] - aR[((0) + (aU) * a1) * 2 + 1];\n bl = (aM + bh) * 0.5;\n bk = (aL + bg) * 0.5;\n bf = (aM - bh) * 0.5;\n be = (aL - bg) * 0.5;\n if (bl == 0 && bk == 0) {}\n if (bf == 0 && be == 0) {}\n aT -= 0.5 * (bl + bf);\n aS -= 0.5 * (bk + be);\n }\n if ((-2 < a4 && a4 < 3) && (-2 < aX && aX < 3)) {\n if (a4 <= 0) {\n if (aX <= 0) {\n var a3 = aR[((0) + (0) * a1) * 2];\n var a2 = aR[((0) + (0) * a1) * 2 + 1];\n var a8 = aT - 2 * bl;\n var a6 = aS - 2 * bk;\n var aK = aT - 2 * bf;\n var aJ = aS - 2 * be;\n var aO = aT - 2 * bl - 2 * bf;\n var aN = aS - 2 * bk - 2 * be;\n var bj = 0.5 * (a4 - (-2));\n var bi = 0.5 * (aX - (-2));\n if (bj + bi <= 1) {\n bc[ba] = aO + (aK - aO) * bj + (a8 - aO) * bi;\n bc[ba + 1] = aN + (aJ - aN) * bj + (a6 - aN) * bi;\n } else {\n bc[ba] = a3 + (a8 - a3) * (1 - bj) + (aK - a3) * (1 - bi);\n bc[ba + 1] = a2 + (a6 - a2) * (1 - bj) + (aJ - a2) * (1 - bi);\n }\n } else {\n if (aX >= 1) {\n var aK = aR[((0) + (aU) * a1) * 2];\n var aJ = aR[((0) + (aU) * a1) * 2 + 1];\n var aO = aT - 2 * bl + 1 * bf;\n var aN = aS - 2 * bk + 1 * be;\n var a3 = aT + 3 * bf;\n var a2 = aS + 3 * be;\n var a8 = aT - 2 * bl + 3 * bf;\n var a6 = aS - 2 * bk + 3 * be;\n var bj = 0.5 * (a4 - (-2));\n var bi = 0.5 * (aX - (1));\n if (bj + bi <= 1) {\n bc[ba] = aO + (aK - aO) * bj + (a8 - aO) * bi;\n bc[ba + 1] = aN + (aJ - aN) * bj + (a6 - aN) * bi;\n } else {\n bc[ba] = a3 + (a8 - a3) * (1 - bj) + (aK - a3) * (1 - bi);\n bc[ba + 1] = a2 + (a6 - a2) * (1 - bj) + (aJ - a2) * (1 - bi);\n }\n } else {\n var aH = (a7 | 0);\n if (aH == aU) {\n aH = aU - 1;\n }\n var bj = 0.5 * (a4 - (-2));\n var bi = a7 - aH;\n var bb = aH / aU;\n var a9 = (aH + 1) / aU;\n var aK = aR[((0) + (aH) * a1) * 2];\n var aJ = aR[((0) + (aH) * a1) * 2 + 1];\n var a3 = aR[((0) + (aH + 1) * a1) * 2];\n var a2 = aR[((0) + (aH + 1) * a1) * 2 + 1];\n var aO = aT - 2 * bl + bb * bf;\n var aN = aS - 2 * bk + bb * be;\n var a8 = aT - 2 * bl + a9 * bf;\n var a6 = aS - 2 * bk + a9 * be;\n if (bj + bi <= 1) {\n bc[ba] = aO + (aK - aO) * bj + (a8 - aO) * bi;\n bc[ba + 1] = aN + (aJ - aN) * bj + (a6 - aN) * bi;\n } else {\n bc[ba] = a3 + (a8 - a3) * (1 - bj) + (aK - a3) * (1 - bi);\n bc[ba + 1] = a2 + (a6 - a2) * (1 - bj) + (aJ - a2) * (1 - bi);\n }\n }\n }\n } else {\n if (1 <= a4) {\n if (aX <= 0) {\n var a8 = aR[((aQ) + (0) * a1) * 2];\n var a6 = aR[((aQ) + (0) * a1) * 2 + 1];\n var a3 = aT + 3 * bl;\n var a2 = aS + 3 * bk;\n var aO = aT + 1 * bl - 2 * bf;\n var aN = aS + 1 * bk - 2 * be;\n var aK = aT + 3 * bl - 2 * bf;\n var aJ = aS + 3 * bk - 2 * be;\n var bj = 0.5 * (a4 - (1));\n var bi = 0.5 * (aX - (-2));\n if (bj + bi <= 1) {\n bc[ba] = aO + (aK - aO) * bj + (a8 - aO) * bi;\n bc[ba + 1] = aN + (aJ - aN) * bj + (a6 - aN) * bi;\n } else {\n bc[ba] = a3 + (a8 - a3) * (1 - bj) + (aK - a3) * (1 - bi);\n bc[ba + 1] = a2 + (a6 - a2) * (1 - bj) + (aJ - a2) * (1 - bi);\n }\n } else {\n if (aX >= 1) {\n var aO = aR[((aQ) + (aU) * a1) * 2];\n var aN = aR[((aQ) + (aU) * a1) * 2 + 1];\n var aK = aT + 3 * bl + 1 * bf;\n var aJ = aS + 3 * bk + 1 * be;\n var a8 = aT + 1 * bl + 3 * bf;\n var a6 = aS + 1 * bk + 3 * be;\n var a3 = aT + 3 * bl + 3 * bf;\n var a2 = aS + 3 * bk + 3 * be;\n var bj = 0.5 * (a4 - (1));\n var bi = 0.5 * (aX - (1));\n if (bj + bi <= 1) {\n bc[ba] = aO + (aK - aO) * bj + (a8 - aO) * bi;\n bc[ba + 1] = aN + (aJ - aN) * bj + (a6 - aN) * bi;\n } else {\n bc[ba] = a3 + (a8 - a3) * (1 - bj) + (aK - a3) * (1 - bi);\n bc[ba + 1] = a2 + (a6 - a2) * (1 - bj) + (aJ - a2) * (1 - bi);\n }\n } else {\n var aH = (a7 | 0);\n if (aH == aU) {\n aH = aU - 1;\n }\n var bj = 0.5 * (a4 - (1));\n var bi = a7 - aH;\n var bb = aH / aU;\n var a9 = (aH + 1) / aU;\n var aO = aR[((aQ) + (aH) * a1) * 2];\n var aN = aR[((aQ) + (aH) * a1) * 2 + 1];\n var a8 = aR[((aQ) + (aH + 1) * a1) * 2];\n var a6 = aR[((aQ) + (aH + 1) * a1) * 2 + 1];\n var aK = aT + 3 * bl + bb * bf;\n var aJ = aS + 3 * bk + bb * be;\n var a3 = aT + 3 * bl + a9 * bf;\n var a2 = aS + 3 * bk + a9 * be;\n if (bj + bi <= 1) {\n bc[ba] = aO + (aK - aO) * bj + (a8 - aO) * bi;\n bc[ba + 1] = aN + (aJ - aN) * bj + (a6 - aN) * bi;\n } else {\n bc[ba] = a3 + (a8 - a3) * (1 - bj) + (aK - a3) * (1 - bi);\n bc[ba + 1] = a2 + (a6 - a2) * (1 - bj) + (aJ - a2) * (1 - bi);\n }\n }\n }\n } else {\n if (aX <= 0) {\n var aY = (bd | 0);\n if (aY == aQ) {\n aY = aQ - 1;\n }\n var bj = bd - aY;\n var bi = 0.5 * (aX - (-2));\n var bp = aY / aQ;\n var bo = (aY + 1) / aQ;\n var a8 = aR[((aY) + (0) * a1) * 2];\n var a6 = aR[((aY) + (0) * a1) * 2 + 1];\n var a3 = aR[((aY + 1) + (0) * a1) * 2];\n var a2 = aR[((aY + 1) + (0) * a1) * 2 + 1];\n var aO = aT + bp * bl - 2 * bf;\n var aN = aS + bp * bk - 2 * be;\n var aK = aT + bo * bl - 2 * bf;\n var aJ = aS + bo * bk - 2 * be;\n if (bj + bi <= 1) {\n bc[ba] = aO + (aK - aO) * bj + (a8 - aO) * bi;\n bc[ba + 1] = aN + (aJ - aN) * bj + (a6 - aN) * bi;\n } else {\n bc[ba] = a3 + (a8 - a3) * (1 - bj) + (aK - a3) * (1 - bi);\n bc[ba + 1] = a2 + (a6 - a2) * (1 - bj) + (aJ - a2) * (1 - bi);\n }\n } else {\n if (aX >= 1) {\n var aY = (bd | 0);\n if (aY == aQ) {\n aY = aQ - 1;\n }\n var bj = bd - aY;\n var bi = 0.5 * (aX - (1));\n var bp = aY / aQ;\n var bo = (aY + 1) / aQ;\n var aO = aR[((aY) + (aU) * a1) * 2];\n var aN = aR[((aY) + (aU) * a1) * 2 + 1];\n var aK = aR[((aY + 1) + (aU) * a1) * 2];\n var aJ = aR[((aY + 1) + (aU) * a1) * 2 + 1];\n var a8 = aT + bp * bl + 3 * bf;\n var a6 = aS + bp * bk + 3 * be;\n var a3 = aT + bo * bl + 3 * bf;\n var a2 = aS + bo * bk + 3 * be;\n if (bj + bi <= 1) {\n bc[ba] = aO + (aK - aO) * bj + (a8 - aO) * bi;\n bc[ba + 1] = aN + (aJ - aN) * bj + (a6 - aN) * bi;\n } else {\n bc[ba] = a3 + (a8 - a3) * (1 - bj) + (aK - a3) * (1 - bi);\n bc[ba + 1] = a2 + (a6 - a2) * (1 - bj) + (aJ - a2) * (1 - bi);\n }\n } else {\n System.err.printf(\"_$li calc : %.4f , %.4f @@BDBoxGrid\\n\", a4, aX);\n }\n }\n }\n }\n } else {\n bc[ba] = aT + a4 * bl + aX * bf;\n bc[ba + 1] = aS + a4 * bk + aX * be;\n }\n } else {\n bn = bd - (bd | 0);\n bm = a7 - (a7 | 0);\n aV = 2 * ((bd | 0) + ((a7 | 0)) * (aQ + 1));\n if (bn + bm < 1) {\n bc[ba] = aR[aV] * (1 - bn - bm) + aR[aV + 2] * bn + aR[aV + 2 * (aQ + 1)] * bm;\n bc[ba + 1] = aR[aV + 1] * (1 - bn - bm) + aR[aV + 3] * bn + aR[aV + 2 * (aQ + 1) + 1] * bm;\n } else {\n bc[ba] = aR[aV + 2 * (aQ + 1) + 2] * (bn - 1 + bm) + aR[aV + 2 * (aQ + 1)] * (1 - bn) + aR[aV + 2] * (1 - bm);\n bc[ba + 1] = aR[aV + 2 * (aQ + 1) + 3] * (bn - 1 + bm) + aR[aV + 2 * (aQ + 1) + 1] * (1 - bn) + aR[aV + 3] * (1 - bm);\n }\n }\n }\n}\n;\nE.prototype.transformPoints_sdk1 = function(aJ, aR, aL, a0, aU, aP, aZ) {\n var aH = aR;\n var aO, aN;\n var aM = this._$o;\n var aQ = this._$A;\n var aI = aU * aZ;\n var aS, aY;\n var aV;\n var aX, aW;\n var aT = (aH._$hr != null) ? aH._$hr : aH._$Cr;\n for (var aK = aP; aK < aI; aK += aZ) {\n if (Q._$ts) {\n aO = aL[aK];\n aN = aL[aK + 1];\n if (aO < 0) {\n aO = 0;\n } else {\n if (aO > 1) {\n aO = 1;\n }\n }\n if (aN < 0) {\n aN = 0;\n } else {\n if (aN > 1) {\n aN = 1;\n }\n }\n aO *= aM;\n aN *= aQ;\n aS = (aO | 0);\n aY = (aN | 0);\n if (aS > aM - 1) {\n aS = aM - 1;\n }\n if (aY > aQ - 1) {\n aY = aQ - 1;\n }\n aX = aO - aS;\n aW = aN - aY;\n aV = 2 * (aS + aY * (aM + 1));\n } else {\n aO = aL[aK] * aM;\n aN = aL[aK + 1] * aQ;\n aX = aO - (aO | 0);\n aW = aN - (aN | 0);\n aV = 2 * ((aO | 0) + (aN | 0) * (aM + 1));\n }\n if (aX + aW < 1) {\n a0[aK] = aT[aV] * (1 - aX - aW) + aT[aV + 2] * aX + aT[aV + 2 * (aM + 1)] * aW;\n a0[aK + 1] = aT[aV + 1] * (1 - aX - aW) + aT[aV + 3] * aX + aT[aV + 2 * (aM + 1) + 1] * aW;\n } else {\n a0[aK] = aT[aV + 2 * (aM + 1) + 2] * (aX - 1 + aW) + aT[aV + 2 * (aM + 1)] * (1 - aX) + aT[aV + 2] * (1 - aW);\n a0[aK + 1] = aT[aV + 2 * (aM + 1) + 3] * (aX - 1 + aW) + aT[aV + 2 * (aM + 1) + 1] * (1 - aX) + aT[aV + 3] * (1 - aW);\n }\n }\n}\n;\nE.prototype._$VT = function() {\n return (this._$o + 1) * (this._$A + 1);\n}\n;\nE.prototype.getType = function() {\n return c._$_b;\n}\n;\nfunction H(aH) {\n B.prototype.constructor.call(this, aH);\n this._$8r = c._$ur;\n this._$Cr = null;\n this._$hr = null;\n}\nH.prototype = new B();\nfunction s() {\n if (j) {\n return;\n }\n this.visible = true;\n this._$g0 = false;\n this._$NL = null;\n this._$3S = null;\n this._$aS = null;\n s._$42++;\n}\ns._$42 = 0;\ns.prototype._$zP = function() {\n this._$3S = new Array();\n this._$aS = new Array();\n}\n;\ns.prototype._$F0 = function(aH) {\n this._$g0 = aH._$8L();\n this.visible = aH._$8L();\n this._$NL = aH._$nP();\n this._$3S = aH._$nP();\n this._$aS = aH._$nP();\n}\n;\ns.prototype.init = function(aI) {\n var aH = new aj(this);\n aH.setPartsOpacity(this.isVisible() ? 1 : 0);\n return aH;\n}\n;\ns.prototype._$6o = function(aH) {\n if (this._$3S == null) {\n throw new Error(\"_$3S _$6 _$Wo@_$6o\");\n }\n this._$3S.push(aH);\n}\n;\ns.prototype._$3o = function(aH) {\n if (this._$aS == null) {\n throw new Error(\"_$aS _$6 _$Wo@_$3o\");\n }\n this._$aS.push(aH);\n}\n;\ns.prototype._$Zo = function(aH) {\n this._$3S = aH;\n}\n;\ns.prototype._$xo = function(aH) {\n this._$aS = aH;\n}\n;\ns.prototype.isVisible = function() {\n return this.visible;\n}\n;\ns.prototype._$uL = function() {\n return this._$g0;\n}\n;\ns.prototype._$KP = function(aH) {\n this.visible = aH;\n}\n;\ns.prototype._$ET = function(aH) {\n this._$g0 = aH;\n}\n;\ns.prototype.getBaseData = function() {\n return this._$3S;\n}\n;\ns.prototype.getDrawData = function() {\n return this._$aS;\n}\n;\ns.prototype._$p2 = function() {\n return this._$NL;\n}\n;\ns.prototype._$ob = function(aH) {\n this._$NL = aH;\n}\n;\ns.prototype.getPartsID = function() {\n return this._$NL;\n}\n;\ns.prototype._$MP = function(aH) {\n this._$NL = aH;\n}\n;\nfunction aj(aH) {\n this._$VS = null;\n this._$e0 = null;\n this._$e0 = aH;\n}\naj.prototype = new S();\naj.prototype.getPartsOpacity = function() {\n return this._$VS;\n}\n;\naj.prototype.setPartsOpacity = function(aH) {\n this._$VS = aH;\n}\n;\nfunction ak(aH) {\n if (j) {\n return;\n }\n this.id = aH;\n}\nak._$L7 = function() {\n z._$27();\n n._$27();\n Z._$27();\n i._$27();\n}\n;\nak.prototype.toString = function() {\n return this.id;\n}\n;\nfunction D() {}\nD.prototype._$F0 = function(aH) {}\n;\nfunction an() {\n if (j) {\n return;\n }\n this._$4S = null;\n}\nan.prototype._$1s = function() {\n return this._$4S;\n}\n;\nan.prototype._$zP = function() {\n this._$4S = new Array();\n}\n;\nan.prototype._$F0 = function(aH) {\n this._$4S = aH._$nP();\n}\n;\nan.prototype._$Ks = function(aH) {\n this._$4S.push(aH);\n}\n;\nfunction au(aH, aI) {\n this.canvas = aH;\n this.context = aI;\n this.viewport = new Array(0,0,aH.width,aH.height);\n this._$6r = 1;\n this._$xP = 0;\n this._$3r = 1;\n this._$uP = 0;\n this._$Qo = -1;\n this.cacheImages = {};\n}\nau.tr = new am();\nau._$50 = new am();\nau._$Ti = new Array(0,0);\nau._$Pi = new Array(0,0);\nau._$B = new Array(0,0);\nau.prototype._$lP = function(aI, aK, aJ, aH) {\n this.viewport = new Array(aI,aK,aJ,aH);\n}\n;\nau.prototype._$bL = function() {\n this.context.save();\n var aH = this.viewport;\n if (aH != null) {\n this.context.beginPath();\n this.context._$Li(aH[0], aH[1], aH[2], aH[3]);\n this.context.clip();\n }\n}\n;\nau.prototype._$ei = function() {\n this.context.restore();\n}\n;\nau.prototype.drawElements = function(bc, bm, aX, aJ, bA, aM, bl, bz) {\n try {\n if (bA != this._$Qo) {\n this._$Qo = bA;\n this.context.globalAlpha = bA;\n }\n var a2 = bm.length;\n var aP = bc.width;\n var a5 = bc.height;\n var bE = this.context;\n var a7 = this._$xP;\n var a6 = this._$uP;\n var a1 = this._$6r;\n var aZ = this._$3r;\n var bD = au.tr;\n var aI = au._$Ti;\n var aH = au._$Pi;\n var bu = au._$B;\n for (var by = 0; by < a2; by += 3) {\n bE.save();\n var aW = bm[by];\n var aV = bm[by + 1];\n var aT = bm[by + 2];\n var aL = a7 + a1 * aX[aW * 2];\n var aK = a6 + aZ * aX[aW * 2 + 1];\n var br = a7 + a1 * aX[aV * 2];\n var bp = a6 + aZ * aX[aV * 2 + 1];\n var bh = a7 + a1 * aX[aT * 2];\n var bf = a6 + aZ * aX[aT * 2 + 1];\n if (bl) {\n bl._$PS(aL, aK, bu);\n aL = bu[0];\n aK = bu[1];\n bl._$PS(br, bp, bu);\n br = bu[0];\n bp = bu[1];\n bl._$PS(bh, bf, bu);\n bh = bu[0];\n bf = bu[1];\n }\n var aS = aP * aJ[aW * 2];\n var aQ = a5 - a5 * aJ[aW * 2 + 1];\n var bx = aP * aJ[aV * 2];\n var bw = a5 - a5 * aJ[aV * 2 + 1];\n var bk = aP * aJ[aT * 2];\n var bj = a5 - a5 * aJ[aT * 2 + 1];\n var a3 = Math.atan2(bw - aQ, bx - aS);\n var a0 = Math.atan2(bp - aK, br - aL);\n var aO = br - aL;\n var aN = bp - aK;\n var bi = Math.sqrt(aO * aO + aN * aN);\n var aU = bx - aS;\n var aR = bw - aQ;\n var bt = Math.sqrt(aU * aU + aR * aR);\n var bv = bi / bt;\n ad._$ni(bk, bj, aS, aQ, (bx - aS), (bw - aQ), -(bw - aQ), (bx - aS), aI);\n ad._$ni(bh, bf, aL, aK, (br - aL), (bp - aK), -(bp - aK), (br - aL), aH);\n var aY = (aH[0] - aI[0]) / aI[1];\n var bs = Math.min(aS, bx, bk);\n var bg = Math.max(aS, bx, bk);\n var bq = Math.min(aQ, bw, bj);\n var be = Math.max(aQ, bw, bj);\n var bo = Math.floor(bs);\n var bb = Math.floor(bq);\n var a4 = Math.ceil(bg);\n var bC = Math.ceil(be);\n bD.identity();\n bD.translate(aL, aK);\n bD.rotate(a0);\n bD.scale(1, aH[1] / aI[1]);\n bD.shear(aY, 0);\n bD.scale(bv, bv);\n bD.rotate(-a3);\n bD.translate(-aS, -aQ);\n bD.setContext(bE);\n var a8 = true;\n var a9 = 1.2;\n if (!aM) {\n aM = a8 ? a9 : 0;\n }\n if (Q.IGNORE_EXPAND) {\n aM = 0;\n }\n if (Q.USE_CACHED_POLYGON_IMAGE) {\n var bd = bz._$e0;\n bd.gl_cacheImage = bd.gl_cacheImage || {};\n if (!bd.gl_cacheImage[by]) {\n var bn = au.createCanvas(a4 - bo, bC - bb);\n Q.DEBUG_DATA.LDGL_CANVAS_MB = Q.DEBUG_DATA.LDGL_CANVAS_MB || 0;\n Q.DEBUG_DATA.LDGL_CANVAS_MB += (a4 - bo) * (bC - bb) * 4;\n var ba = bn.getContext(\"2d\");\n ba.translate(-bo, -bb);\n au.clip(ba, bD, aM, bi, aS, aQ, bx, bw, bk, bj, aL, aK, br, bp, bh, bf);\n ba.drawImage(bc, 0, 0);\n bd.gl_cacheImage[by] = {\n cacheCanvas: bn,\n cacheContext: ba\n };\n }\n bE.drawImage(bd.gl_cacheImage[by][\"cacheCanvas\"], bo, bb);\n } else {\n if (!Q.IGNORE_CLIP) {\n au.clip(bE, bD, aM, bi, aS, aQ, bx, bw, bk, bj, aL, aK, br, bp, bh, bf);\n }\n if (Q.USE_ADJUST_TRANSLATION) {\n bs = 0;\n bg = aP;\n bq = 0;\n be = a5;\n }\n bE.drawImage(bc, bs, bq, bg - bs, be - bq, bs, bq, bg - bs, be - bq);\n }\n bE.restore();\n }\n } catch (bB) {\n q._$Rb(bB);\n }\n}\n;\nau.clip = function(aK, aJ, aV, aI, aM, aL, aU, aT, aQ, aP, aO, aN, aH, aW, aS, aR) {\n if (aV > 0.02) {\n au.expandClip(aK, aJ, aV, aI, aO, aN, aH, aW, aS, aR);\n } else {\n au.clipWithTransform(aK, null, aM, aL, aU, aT, aQ, aP);\n }\n}\n;\nau.expandClip = function(aV, bg, aK, a3, aJ, aI, be, ba, aZ, aX) {\n var aP = be - aJ;\n var aO = ba - aI;\n var bi = aZ - aJ;\n var bh = aX - aI;\n var bj = aP * bh - aO * bi > 0 ? aK : -aK;\n var aL = -aO;\n var aH = aP;\n var bc = aZ - be;\n var a8 = aX - ba;\n var a7 = -a8;\n var a6 = bc;\n var aQ = Math.sqrt(bc * bc + a8 * a8);\n var bf = -bh;\n var bb = bi;\n var a2 = Math.sqrt(bi * bi + bh * bh);\n var bd = aJ - bj * aL / a3;\n var a9 = aI - bj * aH / a3;\n var aY = be - bj * aL / a3;\n var aW = ba - bj * aH / a3;\n var a5 = be - bj * a7 / aQ;\n var a4 = ba - bj * a6 / aQ;\n var aS = aZ - bj * a7 / aQ;\n var aR = aX - bj * a6 / aQ;\n var aN = aJ + bj * bf / a2;\n var aM = aI + bj * bb / a2;\n var a1 = aZ + bj * bf / a2;\n var a0 = aX + bj * bb / a2;\n var aU = au._$50;\n var aT = bg._$P2(aU);\n if (aT == null) {\n return false;\n }\n au.clipWithTransform(aV, aU, bd, a9, aY, aW, a5, a4, aS, aR, a1, a0, aN, aM);\n return true;\n}\n;\nau.clipWithTransform = function(aH, aI, aS, aN, aQ, aK, aP, aJ) {\n if (arguments.length < (1 + 3 * 2)) {\n q._$li(\"err : @LDGL.clip()\");\n return;\n }\n if (!(arguments[1]instanceof am)) {\n q._$li(\"err : a[0] is _$6 LDTransform @LDGL.clip()\");\n return;\n }\n var aM = au._$B;\n var aO = aI;\n var aR = arguments;\n aH.beginPath();\n if (aO) {\n aO._$PS(aR[2], aR[3], aM);\n aH.moveTo(aM[0], aM[1]);\n for (var aL = 4; aL < aR.length; aL += 2) {\n aO._$PS(aR[aL], aR[aL + 1], aM);\n aH.lineTo(aM[0], aM[1]);\n }\n } else {\n aH.moveTo(aR[2], aR[3]);\n for (var aL = 4; aL < aR.length; aL += 2) {\n aH.lineTo(aR[aL], aR[aL + 1]);\n }\n }\n aH.clip();\n}\n;\nau.createCanvas = function(aH, aJ) {\n var aI = document.createElement(\"canvas\");\n aI.setAttribute(\"width\", aH);\n aI.setAttribute(\"height\", aJ);\n if (!aI) {\n q._$li(\"err : \" + aI);\n }\n return aI;\n}\n;\nau.dumpValues = function() {\n var aI = \"\";\n for (var aH = 0; aH < arguments.length; aH++) {\n aI += \"[\" + aH + \"]= \" + arguments[aH].toFixed(3) + \" , \";\n }\n console.log(aI);\n}\n;\nfunction f() {\n if (j) {\n return;\n }\n this._$TT = null;\n this._$LT = null;\n this._$FS = null;\n this._$wL = null;\n}\nf.prototype._$F0 = function(aH) {\n this._$TT = aH._$_T();\n this._$LT = aH._$_T();\n this._$FS = aH._$_T();\n this._$wL = aH._$nP();\n}\n;\nf.prototype.getMinValue = function() {\n return this._$TT;\n}\n;\nf.prototype.getMaxValue = function() {\n return this._$LT;\n}\n;\nf.prototype.getDefaultValue = function() {\n return this._$FS;\n}\n;\nf.prototype.getParamID = function() {\n return this._$wL;\n}\n;\nfunction B(aH) {\n if (j) {\n return;\n }\n this._$e0 = null;\n this._$IP = null;\n this._$JS = false;\n this._$AT = true;\n this._$e0 = aH;\n this.totalScale = 1;\n this._$7s = 1;\n this.totalOpacity = 1;\n}\nB.prototype._$yo = function() {\n return this._$AT && !this._$JS;\n}\n;\nB.prototype._$hS = function(aH) {\n this._$AT = aH;\n}\n;\nB.prototype._$GT = function() {\n return this._$e0;\n}\n;\nB.prototype._$l2 = function(aH) {\n this._$IP = aH;\n}\n;\nB.prototype.getPartsIndex = function() {\n return this._$IP;\n}\n;\nB.prototype._$x2 = function() {\n return this._$JS;\n}\n;\nB.prototype._$Ib = function(aH) {\n this._$JS = aH;\n}\n;\nB.prototype.getTotalScale = function() {\n return this.totalScale;\n}\n;\nB.prototype.setTotalScale_notForClient = function(aH) {\n this.totalScale = aH;\n}\n;\nB.prototype.getInterpolatedOpacity = function() {\n return this._$7s;\n}\n;\nB.prototype.setInterpolatedOpacity = function(aH) {\n this._$7s = aH;\n}\n;\nB.prototype.getTotalOpacity = function(aH) {\n return this.totalOpacity;\n}\n;\nB.prototype.setTotalOpacity = function(aH) {\n this.totalOpacity = aH;\n}\n;\nfunction Q() {}\nQ._$2s = \"2.1.00_1\";\nQ._$Kr = 201001000;\nQ._$sP = true;\nQ._$so = true;\nQ._$cb = false;\nQ._$3T = true;\nQ._$Ts = true;\nQ._$fb = true;\nQ._$ts = true;\nQ.L2D_DEFORMER_EXTEND = true;\nQ._$Wb = false;\nQ._$yr = false;\nQ._$Zs = false;\nQ.L2D_NO_ERROR = 0;\nQ._$i7 = 1000;\nQ._$9s = 1001;\nQ._$es = 1100;\nQ._$r7 = 2000;\nQ._$07 = 2001;\nQ._$b7 = 2002;\nQ._$H7 = 4000;\nQ.L2D_COLOR_BLEND_MODE_MULT = 0;\nQ.L2D_COLOR_BLEND_MODE_ADD = 1;\nQ.L2D_COLOR_BLEND_MODE_INTERPOLATE = 2;\nQ._$6b = true;\nQ._$cT = 0;\nQ.clippingMaskBufferSize = 256;\nQ.glContext = new Array();\nQ.frameBuffers = new Array();\nQ.fTexture = new Array();\nQ.IGNORE_CLIP = false;\nQ.IGNORE_EXPAND = false;\nQ.EXPAND_W = 2;\nQ.USE_ADJUST_TRANSLATION = true;\nQ.USE_CANVAS_TRANSFORM = true;\nQ.USE_CACHED_POLYGON_IMAGE = false;\nQ.DEBUG_DATA = {};\nQ.PROFILE_IOS_SPEED = {\n PROFILE_NAME: \"iOS Speed\",\n USE_ADJUST_TRANSLATION: true,\n USE_CACHED_POLYGON_IMAGE: true,\n EXPAND_W: 4\n};\nQ.PROFILE_IOS_QUALITY = {\n PROFILE_NAME: \"iOS HiQ\",\n USE_ADJUST_TRANSLATION: true,\n USE_CACHED_POLYGON_IMAGE: false,\n EXPAND_W: 2\n};\nQ.PROFILE_IOS_DEFAULT = Q.PROFILE_IOS_QUALITY;\nQ.PROFILE_ANDROID = {\n PROFILE_NAME: \"Android\",\n USE_ADJUST_TRANSLATION: false,\n USE_CACHED_POLYGON_IMAGE: false,\n EXPAND_W: 2\n};\nQ.PROFILE_DESKTOP = {\n PROFILE_NAME: \"Desktop\",\n USE_ADJUST_TRANSLATION: false,\n USE_CACHED_POLYGON_IMAGE: false,\n EXPAND_W: 2\n};\nQ.initProfile = function() {\n if (r.isIOS()) {\n Q.setupProfile(Q.PROFILE_IOS_DEFAULT);\n } else {\n if (r.isAndroid()) {\n Q.setupProfile(Q.PROFILE_ANDROID);\n } else {\n Q.setupProfile(Q.PROFILE_DESKTOP);\n }\n }\n}\n;\nQ.setupProfile = function(aI, aJ) {\n if (typeof aI == \"number\") {\n switch (aI) {\n case 9901:\n aI = Q.PROFILE_IOS_SPEED;\n break;\n case 9902:\n aI = Q.PROFILE_IOS_QUALITY;\n break;\n case 9903:\n aI = Q.PROFILE_IOS_DEFAULT;\n break;\n case 9904:\n aI = Q.PROFILE_ANDROID;\n break;\n case 9905:\n aI = Q.PROFILE_DESKTOP;\n break;\n default:\n alert(\"profile _$6 _$Ui : \" + aI);\n break;\n }\n }\n if (arguments.length < 2) {\n aJ = true;\n }\n if (aJ) {\n console.log(\"profile : \" + aI.PROFILE_NAME);\n }\n for (var aH in aI) {\n Q[aH] = aI[aH];\n if (aJ) {\n console.log(\" [\" + aH + \"] = \" + aI[aH]);\n }\n }\n}\n;\nQ.init = function() {\n if (Q._$6b) {\n console.log(\"Live2D %s\", Q._$2s);\n Q._$6b = false;\n var aH = false;\n aH = true;\n Q.initProfile();\n }\n}\n;\nQ.getVersionStr = function() {\n return Q._$2s;\n}\n;\nQ.getVersionNo = function() {\n return Q._$Kr;\n}\n;\nQ._$sT = function(aH) {\n Q._$cT = aH;\n}\n;\nQ.getError = function() {\n var aH = Q._$cT;\n Q._$cT = 0;\n return aH;\n}\n;\nQ.dispose = function() {\n Q.glContext = [];\n Q.frameBuffers = [];\n Q.fTexture = [];\n}\n;\nQ.setGL = function(aJ, aI) {\n var aH = aI || 0;\n Q.glContext[aH] = aJ;\n}\n;\nQ.getGL = function(aH) {\n return Q.glContext[aH];\n}\n;\nQ.setClippingMaskBufferSize = function(aH) {\n Q.clippingMaskBufferSize = aH;\n}\n;\nQ.getClippingMaskBufferSize = function() {\n return Q.clippingMaskBufferSize;\n}\n;\nQ.deleteBuffer = function(aI) {\n var aH = Q.getGL(aI);\n aH.deleteFramebuffer(Q.frameBuffers[aI].framebuffer);\n delete Q.frameBuffers[aI];\n delete Q.glContext[aI];\n}\n;\nfunction A() {}\nA._$r2 = function(aH) {\n if (aH < 0) {\n return 0;\n } else {\n if (aH > 1) {\n return 1;\n }\n }\n return (0.5 - 0.5 * Math.cos(aH * aC.PI_F));\n}\n;\nfunction J(aH) {\n if (j) {\n return;\n }\n this._$ib = aH;\n}\nJ._$fr = -1;\nJ.prototype.toString = function() {\n return this._$ib;\n}\n;\nfunction b() {\n if (j) {\n return;\n }\n a.prototype.constructor.call(this);\n this._$LP = -1;\n this._$d0 = 0;\n this._$Yo = 0;\n this._$JP = null;\n this._$5P = null;\n this._$BP = null;\n this._$Eo = null;\n this._$Qi = null;\n this._$6s = b._$ms;\n this.culling = true;\n this.gl_cacheImage = null;\n this.instanceNo = b._$42++;\n}\nb.prototype = new a();\nb._$42 = 0;\nb._$Os = 30;\nb._$ms = 0;\nb._$ns = 1;\nb._$_s = 2;\nb._$gT = new Array();\nb.prototype._$_S = function(aH) {\n this._$LP = aH;\n}\n;\nb.prototype.getTextureNo = function() {\n return this._$LP;\n}\n;\nb.prototype._$ZL = function() {\n return this._$Qi;\n}\n;\nb.prototype._$H2 = function() {\n return this._$JP;\n}\n;\nb.prototype.getNumPoints = function() {\n return this._$d0;\n}\n;\nb.prototype.getType = function() {\n return a._$wb;\n}\n;\nb.prototype._$B2 = function(aL, aH, aO) {\n var aM = aH;\n var aN = (aM._$hr != null) ? aM._$hr : aM._$Cr;\n var aK = aw._$do;\n switch (aK) {\n default:\n case aw._$Ms:\n throw new Error(\"_$L _$ro \");\n case aw._$Qs:\n for (var aJ = this._$d0 - 1; aJ >= 0; --aJ) {\n var aI = aJ * aw._$No;\n aN[aI + 4] = aO;\n }\n break;\n }\n}\n;\nb.prototype._$zP = function() {\n this._$GS = new g();\n this._$GS._$zP();\n}\n;\nb.prototype._$F0 = function(aK) {\n a.prototype._$F0.call(this, aK);\n this._$LP = aK._$6L();\n this._$d0 = aK._$6L();\n this._$Yo = aK._$6L();\n var aH = aK._$nP();\n this._$BP = new Int16Array(this._$Yo * 3);\n for (var aJ = this._$Yo * 3 - 1; aJ >= 0; --aJ) {\n this._$BP[aJ] = aH[aJ];\n }\n this._$Eo = aK._$nP();\n this._$Qi = aK._$nP();\n if (aK.getFormatVersion() >= ay._$s7) {\n this._$JP = aK._$6L();\n if (this._$JP != 0) {\n if ((this._$JP & 1) != 0) {\n var aI = aK._$6L();\n if (this._$5P == null) {\n this._$5P = new Object();\n }\n this._$5P._$Hb = parseInt(aI);\n }\n if ((this._$JP & b._$Os) != 0) {\n this._$6s = (this._$JP & b._$Os) >> 1;\n } else {\n this._$6s = b._$ms;\n }\n if ((this._$JP & 32) != 0) {\n this.culling = false;\n }\n }\n } else {\n this._$JP = 0;\n }\n}\n;\nb.prototype.init = function(aL) {\n var aN = new ag(this);\n var aI = this._$d0 * aw._$No;\n var aH = this._$32();\n if (aN._$Cr != null) {\n aN._$Cr = null;\n }\n aN._$Cr = new Float32Array(aI);\n if (aN._$hr != null) {\n aN._$hr = null;\n }\n aN._$hr = aH ? new Float32Array(aI) : null;\n var aM = aw._$do;\n switch (aM) {\n default:\n case aw._$Ms:\n if (aw._$Ls) {\n for (var aJ = this._$d0 - 1; aJ >= 0; --aJ) {\n var aO = aJ << 1;\n this._$Qi[aO + 1] = 1 - this._$Qi[aO + 1];\n }\n }\n break;\n case aw._$Qs:\n for (var aJ = this._$d0 - 1; aJ >= 0; --aJ) {\n var aO = aJ << 1;\n var aK = aJ * aw._$No;\n var aQ = this._$Qi[aO];\n var aP = this._$Qi[aO + 1];\n aN._$Cr[aK] = aQ;\n aN._$Cr[aK + 1] = aP;\n aN._$Cr[aK + 4] = 0;\n if (aH) {\n aN._$hr[aK] = aQ;\n aN._$hr[aK + 1] = aP;\n aN._$hr[aK + 4] = 0;\n }\n }\n break;\n }\n return aN;\n}\n;\nb.prototype._$Nr = function(aJ, aH) {\n var aK = aH;\n if (!((this == aK._$GT()))) {\n console.log(\"### assert!! ### \");\n }\n if (!this._$GS._$Ur(aJ)) {\n return;\n }\n a.prototype._$Nr.call(this, aJ, aK);\n if (aK._$IS[0]) {\n return;\n }\n var aI = b._$gT;\n aI[0] = false;\n aG._$Vr(aJ, this._$GS, aI, this._$d0, this._$Eo, aK._$Cr, aw._$i2, aw._$No);\n}\n;\nb.prototype._$2b = function(aK, aI) {\n try {\n if (!((this == aI._$GT()))) {\n console.log(\"### assert!! ### \");\n }\n var aL = false;\n if (aI._$IS[0]) {\n aL = true;\n }\n var aM = aI;\n if (!aL) {\n a.prototype._$2b.call(this, aK);\n if (this._$32()) {\n var aH = this.getTargetBaseDataID();\n if (aM._$8r == a._$ur) {\n aM._$8r = aK.getBaseDataIndex(aH);\n }\n if (aM._$8r < 0) {\n if (Q._$so) {\n q._$li(\"_$L _$0P _$G :: %s\", aH);\n }\n } else {\n var aO = aK.getBaseData(aM._$8r);\n var aJ = aK._$q2(aM._$8r);\n if (aO != null && !aJ._$x2()) {\n aO._$nb(aK, aJ, aM._$Cr, aM._$hr, this._$d0, aw._$i2, aw._$No);\n aM._$AT = true;\n } else {\n aM._$AT = false;\n }\n aM.baseOpacity = aJ.getTotalOpacity();\n }\n }\n }\n } catch (aN) {\n throw aN;\n }\n}\n;\nb.prototype.draw = function(aN, aK, aI) {\n if (!((this == aI._$GT()))) {\n console.log(\"### assert!! ### \");\n }\n if (aI._$IS[0]) {\n return;\n }\n var aL = aI;\n var aJ = this._$LP;\n if (aJ < 0) {\n aJ = 1;\n }\n var aH = this.getOpacity(aK, aL) * aI._$VS * aI.baseOpacity;\n var aM = (aL._$hr != null) ? aL._$hr : aL._$Cr;\n aN.setClipBufPre_clipContextForDraw(aI.clipBufPre_clipContext);\n aN._$WP(this.culling);\n aN._$Uo(aJ, 3 * this._$Yo, this._$BP, aM, this._$Qi, aH, this._$6s, aL);\n}\n;\nb.prototype.dump = function() {\n console.log(\" _$yi( %d ) , _$d0( %d ) , _$Yo( %d ) \\n\", this._$LP, this._$d0, this._$Yo);\n console.log(\" _$Oi _$di = { \");\n for (var aJ = 0; aJ < this._$BP.length; aJ++) {\n console.log(\"%5d ,\", this._$BP[aJ]);\n }\n console.log(\"\\n _$5i _$30\");\n for (var aJ = 0; aJ < this._$Eo.length; aJ++) {\n console.log(\"\\n _$30[%d] = \", aJ);\n var aH = this._$Eo[aJ];\n for (var aI = 0; aI < aH.length; aI++) {\n console.log(\"%6.2f, \", aH[aI]);\n }\n }\n console.log(\"\\n\");\n}\n;\nb.prototype._$72 = function(aH) {\n if (this._$5P == null) {\n return null;\n }\n return this._$5P[aH];\n}\n;\nb.prototype.getIndexArray = function() {\n return this._$BP;\n}\n;\nfunction ag(aH) {\n aB.prototype.constructor.call(this, aH);\n this._$8r = a._$ur;\n this._$Cr = null;\n this._$hr = null;\n}\nag.prototype = new aB();\nag.prototype.getTransformedPoints = function() {\n return (this._$hr != null) ? this._$hr : this._$Cr;\n}\n;\nfunction k() {\n if (j) {\n return;\n }\n this.x = null;\n this.y = null;\n}\nk.prototype._$HT = function(aH) {\n this.x = aH.x;\n this.y = aH.y;\n}\n;\nk.prototype._$HT = function(aH, aI) {\n this.x = aH;\n this.y = aI;\n}\n;\nfunction l(aH) {\n if (j) {\n return;\n }\n aa.prototype.constructor.call(this);\n this.drawParamWebGL = new C(aH);\n this.drawParamWebGL.setGL(Q.getGL(aH));\n}\nl.prototype = new aa();\nl.loadModel = function(aI) {\n var aH = new l();\n aa._$62(aH, aI);\n return aH;\n}\n;\nl.loadModel = function(aI, aK) {\n var aJ = aK || 0;\n var aH = new l(aJ);\n aa._$62(aH, aI);\n return aH;\n}\n;\nl._$to = function() {\n var aH = new l();\n return aH;\n}\n;\nl._$er = function(aM) {\n var aJ = new _$5(\"../_$_r/_$t0/_$Ri/_$_P._$d\");\n if (aJ.exists() == false) {\n throw new _$ls(\"_$t0 _$_ _$6 _$Ui :: \" + aJ._$PL());\n }\n var aH = [\"../_$_r/_$t0/_$Ri/_$_P.512/_$CP._$1\", \"../_$_r/_$t0/_$Ri/_$_P.512/_$vP._$1\", \"../_$_r/_$t0/_$Ri/_$_P.512/_$EP._$1\", \"../_$_r/_$t0/_$Ri/_$_P.512/_$pP._$1\"];\n var aK = l.loadModel(aJ._$3b());\n for (var aI = 0; aI < aH.length; aI++) {\n var aL = new _$5(aH[aI]);\n if (aL.exists() == false) {\n throw new _$ls(\"_$t0 _$_ _$6 _$Ui :: \" + aL._$PL());\n }\n aK.setTexture(aI, _$nL._$_o(aM, aL._$3b()));\n }\n return aK;\n}\n;\nl.prototype.setGL = function(aH) {\n Q.setGL(aH);\n}\n;\nl.prototype.setTransform = function(aH) {\n this.drawParamWebGL.setTransform(aH);\n}\n;\nl.prototype.update = function() {\n this._$5S.update();\n this._$5S.preDraw(this.drawParamWebGL);\n}\n;\nl.prototype.draw = function() {\n this._$5S.draw(this.drawParamWebGL);\n}\n;\nl.prototype._$K2 = function() {\n this.drawParamWebGL._$K2();\n}\n;\nl.prototype.setTexture = function(aI, aH) {\n if (this.drawParamWebGL == null) {\n q._$li(\"_$Yi for QT _$ki / _$XS() is _$6 _$ui!!\");\n }\n this.drawParamWebGL.setTexture(aI, aH);\n}\n;\nl.prototype.setTexture = function(aI, aH) {\n if (this.drawParamWebGL == null) {\n q._$li(\"_$Yi for QT _$ki / _$XS() is _$6 _$ui!!\");\n }\n this.drawParamWebGL.setTexture(aI, aH);\n}\n;\nl.prototype._$Rs = function() {\n return this.drawParamWebGL._$Rs();\n}\n;\nl.prototype._$Ds = function(aH) {\n this.drawParamWebGL._$Ds(aH);\n}\n;\nl.prototype.getDrawParam = function() {\n return this.drawParamWebGL;\n}\n;\nl.prototype.setMatrix = function(aH) {\n this.drawParamWebGL.setMatrix(aH);\n}\n;\nl.prototype.setPremultipliedAlpha = function(aH) {\n this.drawParamWebGL.setPremultipliedAlpha(aH);\n}\n;\nl.prototype.isPremultipliedAlpha = function() {\n return this.drawParamWebGL.isPremultipliedAlpha();\n}\n;\nl.prototype.setAnisotropy = function(aH) {\n this.drawParamWebGL.setAnisotropy(aH);\n}\n;\nl.prototype.getAnisotropy = function() {\n return this.drawParamWebGL.getAnisotropy();\n}\n;\nfunction V() {\n if (j) {\n return;\n }\n this.motions = null;\n this._$eb = false;\n this.motions = new Array();\n}\nV.prototype._$tb = function() {\n return this.motions;\n}\n;\nV.prototype.startMotion = function(aJ, aI) {\n var aM = null;\n var aL = null;\n var aH = this.motions.length;\n for (var aK = 0; aK < aH; ++aK) {\n aL = this.motions[aK];\n if (aL == null) {\n continue;\n }\n aL._$qS(aL._$w0.getFadeOut());\n if (this._$eb) {\n q._$Ji(\"MotionQueueManager[size:%2d]->startMotion() / start _$K _$3 (m%d)\\n\", aH, aL._$sr);\n }\n }\n if (aJ == null) {\n return -1;\n }\n aL = new M();\n aL._$w0 = aJ;\n this.motions.push(aL);\n var aN = aL._$sr;\n if (this._$eb) {\n q._$Ji(\"MotionQueueManager[size:%2d]->startMotion() / new _$w0 (m%d)\\n\", aH, aN);\n }\n return aN;\n}\n;\nV.prototype.updateParam = function(aJ) {\n try {\n var aI = false;\n for (var aK = 0; aK < this.motions.length; aK++) {\n var aL = this.motions[aK];\n if (aL == null) {\n this.motions.splice(aK, 1);\n aK--;\n continue;\n }\n var aH = aL._$w0;\n if (aH == null) {\n this.motions = this.motions.splice(aK, 1);\n aK--;\n continue;\n }\n aH.updateParam(aJ, aL);\n aI = true;\n if (aL.isFinished()) {\n if (this._$eb) {\n q._$Ji(\"MotionQueueManager[size:%2d]->updateParam() / _$T0 _$w0 (m%d)\\n\", this.motions.length - 1, aL._$sr);\n }\n this.motions.splice(aK, 1);\n aK--;\n } else {}\n }\n return aI;\n } catch (aM) {\n q._$li(aM);\n return true;\n }\n}\n;\nV.prototype.isFinished = function(aK) {\n if (arguments.length >= 1) {\n for (var aI = 0; aI < this.motions.length; aI++) {\n var aJ = this.motions[aI];\n if (aJ == null) {\n continue;\n }\n if (aJ._$sr == aK && !aJ.isFinished()) {\n return false;\n }\n }\n return true;\n } else {\n for (var aI = 0; aI < this.motions.length; aI++) {\n var aJ = this.motions[aI];\n if (aJ == null) {\n this.motions.splice(aI, 1);\n aI--;\n continue;\n }\n var aH = aJ._$w0;\n if (aH == null) {\n this.motions.splice(aI, 1);\n aI--;\n continue;\n }\n if (!aJ.isFinished()) {\n return false;\n }\n }\n return true;\n }\n}\n;\nV.prototype.stopAllMotions = function() {\n for (var aI = 0; aI < this.motions.length; aI++) {\n var aJ = this.motions[aI];\n if (aJ == null) {\n this.motions.splice(aI, 1);\n aI--;\n continue;\n }\n var aH = aJ._$w0;\n if (aH == null) {\n this.motions.splice(aI, 1);\n aI--;\n continue;\n }\n if (true) {\n this.motions.splice(aI, 1);\n aI--;\n }\n }\n}\n;\nV.prototype._$Zr = function(aH) {\n this._$eb = aH;\n}\n;\nV.prototype._$e = function() {\n console.log(\"-- _$R --\\n\");\n for (var aH = 0; aH < this.motions.length; aH++) {\n var aI = this.motions[aH];\n var aJ = aI._$w0;\n console.log(\"MotionQueueEnt[%d] :: %s\\n\", this.motions.length, aJ.toString());\n }\n}\n;\nfunction M() {\n this._$w0 = null;\n this._$AT = true;\n this._$9L = false;\n this._$z2 = -1;\n this._$bs = -1;\n this._$Do = -1;\n this._$sr = null;\n this._$sr = M._$Gs++;\n}\nM._$Gs = 0;\nM.prototype.isFinished = function() {\n return this._$9L;\n}\n;\nM.prototype._$qS = function(aJ) {\n var aI = P.getUserTimeMSec();\n var aH = aI + aJ;\n if (this._$Do < 0 || aH < this._$Do) {\n this._$Do = aH;\n }\n}\n;\nM.prototype._$Bs = function() {\n return this._$sr;\n}\n;\nfunction am() {\n this.m = new Array(1,0,0,0,1,0,0,0,1);\n}\nam.prototype.setContext = function(aI) {\n var aH = this.m;\n aI.transform(aH[0], aH[1], aH[3], aH[4], aH[6], aH[7]);\n}\n;\nam.prototype.toString = function() {\n var aI = \"LDTransform { \";\n for (var aH = 0; aH < 9; aH++) {\n aI += this.m[aH].toFixed(2) + \" ,\";\n }\n aI += \" }\";\n return aI;\n}\n;\nam.prototype.identity = function() {\n var aH = this.m;\n aH[0] = aH[4] = aH[8] = 1;\n aH[1] = aH[2] = aH[3] = aH[5] = aH[6] = aH[7] = 0;\n}\n;\nam.prototype._$PS = function(aI, aK, aJ) {\n if (aJ == null) {\n aJ = new Array(0,0);\n }\n var aH = this.m;\n aJ[0] = aH[0] * aI + aH[3] * aK + aH[6];\n aJ[1] = aH[1] * aI + aH[4] * aK + aH[7];\n return aJ;\n}\n;\nam.prototype._$P2 = function(aK) {\n if (!aK) {\n aK = new am();\n }\n var aI = this.m;\n var aT = aI[0];\n var aS = aI[1];\n var aR = aI[2];\n var aQ = aI[3];\n var aP = aI[4];\n var aO = aI[5];\n var aN = aI[6];\n var aM = aI[7];\n var aL = aI[8];\n var aJ = aT * aP * aL + aS * aO * aN + aR * aQ * aM - aT * aO * aM - aR * aP * aN - aS * aQ * aL;\n if (aJ == 0) {\n return null;\n } else {\n var aH = 1 / aJ;\n aK.m[0] = aH * (aP * aL - aM * aO);\n aK.m[1] = aH * (aM * aR - aS * aL);\n aK.m[2] = aH * (aS * aO - aP * aR);\n aK.m[3] = aH * (aN * aO - aQ * aL);\n aK.m[4] = aH * (aT * aL - aN * aR);\n aK.m[5] = aH * (aQ * aR - aT * aO);\n aK.m[6] = aH * (aQ * aM - aN * aP);\n aK.m[7] = aH * (aN * aS - aT * aM);\n aK.m[8] = aH * (aT * aP - aQ * aS);\n return aK;\n }\n}\n;\nam.prototype.transform = function(aI, aK, aJ) {\n if (aJ == null) {\n aJ = new Array(0,0);\n }\n var aH = this.m;\n aJ[0] = aH[0] * aI + aH[3] * aK + aH[6];\n aJ[1] = aH[1] * aI + aH[4] * aK + aH[7];\n return aJ;\n}\n;\nam.prototype.translate = function(aI, aJ) {\n var aH = this.m;\n aH[6] = aH[0] * aI + aH[3] * aJ + aH[6];\n aH[7] = aH[1] * aI + aH[4] * aJ + aH[7];\n aH[8] = aH[2] * aI + aH[5] * aJ + aH[8];\n}\n;\nam.prototype.scale = function(aJ, aI) {\n var aH = this.m;\n aH[0] *= aJ;\n aH[1] *= aJ;\n aH[2] *= aJ;\n aH[3] *= aI;\n aH[4] *= aI;\n aH[5] *= aI;\n}\n;\nam.prototype.shear = function(aM, aL) {\n var aH = this.m;\n var aK = aH[0] + aH[3] * aL;\n var aJ = aH[1] + aH[4] * aL;\n var aI = aH[2] + aH[5] * aL;\n aH[3] = aH[0] * aM + aH[3];\n aH[4] = aH[1] * aM + aH[4];\n aH[5] = aH[2] * aM + aH[5];\n aH[0] = aK;\n aH[1] = aJ;\n aH[2] = aI;\n}\n;\nam.prototype.rotate = function(aM) {\n var aH = this.m;\n var aN = Math.cos(aM);\n var aL = Math.sin(aM);\n var aK = aH[0] * aN + aH[3] * aL;\n var aJ = aH[1] * aN + aH[4] * aL;\n var aI = aH[2] * aN + aH[5] * aL;\n aH[3] = -aH[0] * aL + aH[3] * aN;\n aH[4] = -aH[1] * aL + aH[4] * aN;\n aH[5] = -aH[2] * aL + aH[5] * aN;\n aH[0] = aK;\n aH[1] = aJ;\n aH[2] = aI;\n}\n;\nam.prototype.concatenate = function(aL) {\n var aO = this.m;\n var aM = aL.m;\n var aS = aO[0] * aM[0] + aO[3] * aM[1] + aO[6] * aM[2];\n var aR = aO[1] * aM[0] + aO[4] * aM[1] + aO[7] * aM[2];\n var aQ = aO[2] * aM[0] + aO[5] * aM[1] + aO[8] * aM[2];\n var aP = aO[0] * aM[3] + aO[3] * aM[4] + aO[6] * aM[5];\n var aN = aO[1] * aM[3] + aO[4] * aM[4] + aO[7] * aM[5];\n var aK = aO[2] * aM[3] + aO[5] * aM[4] + aO[8] * aM[5];\n var aJ = aO[0] * aM[6] + aO[3] * aM[7] + aO[6] * aM[8];\n var aI = aO[1] * aM[6] + aO[4] * aM[7] + aO[7] * aM[8];\n var aH = aO[2] * aM[6] + aO[5] * aM[7] + aO[8] * aM[8];\n m[0] = aS;\n m[1] = aR;\n m[2] = aQ;\n m[3] = aP;\n m[4] = aN;\n m[5] = aK;\n m[6] = aJ;\n m[7] = aI;\n m[8] = aH;\n}\n;\nfunction n(aH) {\n if (j) {\n return;\n }\n ak.prototype.constructor.call(this, aH);\n}\nn.prototype = new ak();\nn._$eT = null;\nn._$tP = new Object();\nn._$2o = function() {\n if (n._$eT == null) {\n n._$eT = n.getID(\"DST_BASE\");\n }\n return n._$eT;\n}\n;\nn._$27 = function() {\n n._$tP.clear();\n n._$eT = null;\n}\n;\nn.getID = function(aH) {\n var aI = n._$tP[aH];\n if (aI == null) {\n aI = new n(aH);\n n._$tP[aH] = aI;\n }\n return aI;\n}\n;\nn.prototype._$3s = function() {\n return new n();\n}\n;\nfunction C(aH) {\n if (j) {\n return;\n }\n ax.prototype.constructor.call(this);\n this.textures = new Array();\n this.transform = null;\n this.gl = null;\n this.glno = aH;\n this.firstDraw = true;\n this.anisotropyExt = null;\n this.maxAnisotropy = 0;\n this._$As = 32;\n this._$Gr = false;\n this._$NT = null;\n this._$vS = null;\n this._$no = null;\n this.vertShader = null;\n this.fragShader = null;\n this.vertShaderOff = null;\n this.fragShaderOff = null;\n}\nC.prototype = new ax();\nC._$9r = function(aH) {\n var aI = new Float32Array(aH);\n return aI;\n}\n;\nC._$vb = function(aH) {\n var aI = new Int16Array(aH);\n return aI;\n}\n;\nC._$cr = function(aI, aH) {\n if (aI == null || aI._$yL() < aH.length) {\n aI = C._$9r(aH.length * 2);\n aI.put(aH);\n aI._$oT(0);\n } else {\n aI.clear();\n aI.put(aH);\n aI._$oT(0);\n }\n return aI;\n}\n;\nC._$mb = function(aI, aH) {\n if (aI == null || aI._$yL() < aH.length) {\n aI = C._$vb(aH.length * 2);\n aI.put(aH);\n aI._$oT(0);\n } else {\n aI.clear();\n aI.put(aH);\n aI._$oT(0);\n }\n return aI;\n}\n;\nC._$Hs = function() {\n return this._$Gr;\n}\n;\nC._$as = function(aH) {\n this._$Gr = aH;\n}\n;\nC.prototype.getGL = function() {\n return this.gl;\n}\n;\nC.prototype.setGL = function(aH) {\n this.gl = aH;\n}\n;\nC.prototype.setTransform = function(aH) {\n this.transform = aH;\n}\n;\nC.prototype._$ZT = function() {\n var aH = this.gl;\n if (this.firstDraw) {\n this.initShader();\n this.firstDraw = false;\n this.anisotropyExt = aH.getExtension(\"EXT_texture_filter_anisotropic\") || aH.getExtension(\"WEBKIT_EXT_texture_filter_anisotropic\") || aH.getExtension(\"MOZ_EXT_texture_filter_anisotropic\");\n if (this.anisotropyExt) {\n this.maxAnisotropy = aH.getParameter(this.anisotropyExt.MAX_TEXTURE_MAX_ANISOTROPY_EXT);\n }\n }\n aH.disable(aH.SCISSOR_TEST);\n aH.disable(aH.STENCIL_TEST);\n aH.disable(aH.DEPTH_TEST);\n aH.frontFace(aH.CW);\n aH.enable(aH.BLEND);\n aH.colorMask(1, 1, 1, 1);\n aH.bindBuffer(aH.ARRAY_BUFFER, null);\n aH.bindBuffer(aH.ELEMENT_ARRAY_BUFFER, null);\n}\n;\nC.prototype._$Uo = function(aS, aT, aL, aU, aV, aN, aM, aO) {\n if (aN < 0.01 && this.clipBufPre_clipContextMask == null) {\n return;\n }\n var aH = aN > 0.9 ? Q.EXPAND_W : 0;\n var a0 = this.gl;\n if (this.gl == null) {\n throw new Error(\"gl is null\");\n }\n var a1 = false;\n var aQ = 1;\n var aP = 1;\n var a3 = 1;\n var aZ = 1;\n var aW = this._$C0 * aP * aN;\n var a2 = this._$tT * a3 * aN;\n var a5 = this._$WL * aZ * aN;\n var a7 = this._$lT * aN;\n if (this.clipBufPre_clipContextMask != null) {\n a0.frontFace(a0.CCW);\n a0.useProgram(this.shaderProgram);\n this._$vS = T(a0, this._$vS, aU);\n this._$no = L(a0, this._$no, aL);\n a0.enableVertexAttribArray(this.a_position_Loc);\n a0.vertexAttribPointer(this.a_position_Loc, 2, a0.FLOAT, false, 0, 0);\n this._$NT = T(a0, this._$NT, aV);\n a0.activeTexture(a0.TEXTURE1);\n a0.bindTexture(a0.TEXTURE_2D, this.textures[aS]);\n a0.uniform1i(this.s_texture0_Loc, 1);\n a0.enableVertexAttribArray(this.a_texCoord_Loc);\n a0.vertexAttribPointer(this.a_texCoord_Loc, 2, a0.FLOAT, false, 0, 0);\n a0.uniformMatrix4fv(this.u_matrix_Loc, false, this.getClipBufPre_clipContextMask().matrixForMask);\n var aY = this.getClipBufPre_clipContextMask().layoutChannelNo;\n var a4 = this.getChannelFlagAsColor(aY);\n a0.uniform4f(this.u_channelFlag, a4.r, a4.g, a4.b, a4.a);\n var aI = this.getClipBufPre_clipContextMask().layoutBounds;\n a0.uniform4f(this.u_baseColor_Loc, aI.x * 2 - 1, aI.y * 2 - 1, aI._$EL() * 2 - 1, aI._$5T() * 2 - 1);\n a0.uniform1i(this.u_maskFlag_Loc, true);\n } else {\n a1 = this.getClipBufPre_clipContextDraw() != null;\n if (a1) {\n a0.useProgram(this.shaderProgramOff);\n this._$vS = T(a0, this._$vS, aU);\n this._$no = L(a0, this._$no, aL);\n a0.enableVertexAttribArray(this.a_position_Loc_Off);\n a0.vertexAttribPointer(this.a_position_Loc_Off, 2, a0.FLOAT, false, 0, 0);\n this._$NT = T(a0, this._$NT, aV);\n a0.activeTexture(a0.TEXTURE1);\n a0.bindTexture(a0.TEXTURE_2D, this.textures[aS]);\n a0.uniform1i(this.s_texture0_Loc_Off, 1);\n a0.enableVertexAttribArray(this.a_texCoord_Loc_Off);\n a0.vertexAttribPointer(this.a_texCoord_Loc_Off, 2, a0.FLOAT, false, 0, 0);\n a0.uniformMatrix4fv(this.u_clipMatrix_Loc_Off, false, this.getClipBufPre_clipContextDraw().matrixForDraw);\n a0.uniformMatrix4fv(this.u_matrix_Loc_Off, false, this.matrix4x4);\n a0.activeTexture(a0.TEXTURE2);\n a0.bindTexture(a0.TEXTURE_2D, Q.fTexture[this.glno]);\n a0.uniform1i(this.s_texture1_Loc_Off, 2);\n var aY = this.getClipBufPre_clipContextDraw().layoutChannelNo;\n var a4 = this.getChannelFlagAsColor(aY);\n a0.uniform4f(this.u_channelFlag_Loc_Off, a4.r, a4.g, a4.b, a4.a);\n a0.uniform4f(this.u_baseColor_Loc_Off, aW, a2, a5, a7);\n } else {\n a0.useProgram(this.shaderProgram);\n this._$vS = T(a0, this._$vS, aU);\n this._$no = L(a0, this._$no, aL);\n a0.enableVertexAttribArray(this.a_position_Loc);\n a0.vertexAttribPointer(this.a_position_Loc, 2, a0.FLOAT, false, 0, 0);\n this._$NT = T(a0, this._$NT, aV);\n a0.activeTexture(a0.TEXTURE1);\n a0.bindTexture(a0.TEXTURE_2D, this.textures[aS]);\n a0.uniform1i(this.s_texture0_Loc, 1);\n a0.enableVertexAttribArray(this.a_texCoord_Loc);\n a0.vertexAttribPointer(this.a_texCoord_Loc, 2, a0.FLOAT, false, 0, 0);\n a0.uniformMatrix4fv(this.u_matrix_Loc, false, this.matrix4x4);\n a0.uniform4f(this.u_baseColor_Loc, aW, a2, a5, a7);\n a0.uniform1i(this.u_maskFlag_Loc, false);\n }\n }\n if (this.culling) {\n this.gl.enable(a0.CULL_FACE);\n } else {\n this.gl.disable(a0.CULL_FACE);\n }\n this.gl.enable(a0.BLEND);\n var a6;\n var aX;\n var aR;\n var aK;\n if (this.clipBufPre_clipContextMask != null) {\n a6 = a0.ONE;\n aX = a0.ONE_MINUS_SRC_ALPHA;\n aR = a0.ONE;\n aK = a0.ONE_MINUS_SRC_ALPHA;\n } else {\n switch (aM) {\n case b._$ms:\n a6 = a0.ONE;\n aX = a0.ONE_MINUS_SRC_ALPHA;\n aR = a0.ONE;\n aK = a0.ONE_MINUS_SRC_ALPHA;\n break;\n case b._$ns:\n a6 = a0.ONE;\n aX = a0.ONE;\n aR = a0.ZERO;\n aK = a0.ONE;\n break;\n case b._$_s:\n a6 = a0.DST_COLOR;\n aX = a0.ONE_MINUS_SRC_ALPHA;\n aR = a0.ZERO;\n aK = a0.ONE;\n break;\n }\n }\n a0.blendEquationSeparate(a0.FUNC_ADD, a0.FUNC_ADD);\n a0.blendFuncSeparate(a6, aX, aR, aK);\n if (this.anisotropyExt) {\n a0.texParameteri(a0.TEXTURE_2D, this.anisotropyExt.TEXTURE_MAX_ANISOTROPY_EXT, this.maxAnisotropy);\n }\n var aJ = aL.length;\n a0.drawElements(a0.TRIANGLES, aJ, a0.UNSIGNED_SHORT, 0);\n a0.bindTexture(a0.TEXTURE_2D, null);\n}\n;\nfunction T(aJ, aH, aI) {\n if (aH == null) {\n aH = aJ.createBuffer();\n }\n aJ.bindBuffer(aJ.ARRAY_BUFFER, aH);\n aJ.bufferData(aJ.ARRAY_BUFFER, aI, aJ.DYNAMIC_DRAW);\n return aH;\n}\nfunction L(aJ, aH, aI) {\n if (aH == null) {\n aH = aJ.createBuffer();\n }\n aJ.bindBuffer(aJ.ELEMENT_ARRAY_BUFFER, aH);\n aJ.bufferData(aJ.ELEMENT_ARRAY_BUFFER, aI, aJ.DYNAMIC_DRAW);\n return aH;\n}\nC.prototype._$Rs = function() {\n throw new Error(\"_$Rs\");\n}\n;\nC.prototype._$Ds = function(aH) {\n throw new Error(\"_$Ds\");\n}\n;\nC.prototype._$K2 = function() {\n for (var aH = 0; aH < this.textures.length; aH++) {\n var aI = this.textures[aH];\n if (aI != 0) {\n this.gl._$K2(1, this.textures, aH);\n this.textures[aH] = null;\n }\n }\n}\n;\nC.prototype.setTexture = function(aH, aI) {\n this.textures[aH] = aI;\n}\n;\nC.prototype.initShader = function() {\n var aH = this.gl;\n this.loadShaders2();\n this.a_position_Loc = aH.getAttribLocation(this.shaderProgram, \"a_position\");\n this.a_texCoord_Loc = aH.getAttribLocation(this.shaderProgram, \"a_texCoord\");\n this.u_matrix_Loc = aH.getUniformLocation(this.shaderProgram, \"u_mvpMatrix\");\n this.s_texture0_Loc = aH.getUniformLocation(this.shaderProgram, \"s_texture0\");\n this.u_channelFlag = aH.getUniformLocation(this.shaderProgram, \"u_channelFlag\");\n this.u_baseColor_Loc = aH.getUniformLocation(this.shaderProgram, \"u_baseColor\");\n this.u_maskFlag_Loc = aH.getUniformLocation(this.shaderProgram, \"u_maskFlag\");\n this.a_position_Loc_Off = aH.getAttribLocation(this.shaderProgramOff, \"a_position\");\n this.a_texCoord_Loc_Off = aH.getAttribLocation(this.shaderProgramOff, \"a_texCoord\");\n this.u_matrix_Loc_Off = aH.getUniformLocation(this.shaderProgramOff, \"u_mvpMatrix\");\n this.u_clipMatrix_Loc_Off = aH.getUniformLocation(this.shaderProgramOff, \"u_ClipMatrix\");\n this.s_texture0_Loc_Off = aH.getUniformLocation(this.shaderProgramOff, \"s_texture0\");\n this.s_texture1_Loc_Off = aH.getUniformLocation(this.shaderProgramOff, \"s_texture1\");\n this.u_channelFlag_Loc_Off = aH.getUniformLocation(this.shaderProgramOff, \"u_channelFlag\");\n this.u_baseColor_Loc_Off = aH.getUniformLocation(this.shaderProgramOff, \"u_baseColor\");\n}\n;\nC.prototype.disposeShader = function() {\n var aH = this.gl;\n if (this.shaderProgram) {\n aH.deleteProgram(this.shaderProgram);\n this.shaderProgram = null;\n }\n if (this.shaderProgramOff) {\n aH.deleteProgram(this.shaderProgramOff);\n this.shaderProgramOff = null;\n }\n}\n;\nC.prototype.compileShader = function(aJ, aN) {\n var aM = this.gl;\n var aH;\n var aL = aN;\n var aK = aM.createShader(aJ);\n if (aK == null) {\n q._$Ji(\"_$L0 to create shader\");\n return null;\n }\n aM.shaderSource(aK, aL);\n aM.compileShader(aK);\n var aH = aM.getShaderParameter(aK, aM.COMPILE_STATUS);\n if (!aH) {\n var aI = aM.getShaderInfoLog(aK);\n q._$Ji(\"_$L0 to compile shader : \" + aI);\n aM.deleteShader(aK);\n return null;\n }\n return aK;\n}\n;\nC.prototype.loadShaders2 = function() {\n var aN = this.gl;\n this.shaderProgram = aN.createProgram();\n if (!this.shaderProgram) {\n return false;\n }\n this.shaderProgramOff = aN.createProgram();\n if (!this.shaderProgramOff) {\n return false;\n }\n var aK = \"attribute vec4 a_position;attribute vec2 a_texCoord;varying vec2 v_texCoord;varying vec4 v_ClipPos;uniform mat4 u_mvpMatrix;void main(){ gl_Position = u_mvpMatrix * a_position; v_ClipPos = u_mvpMatrix * a_position; v_texCoord = a_texCoord;}\";\n var aM = \"precision mediump float;varying vec2 v_texCoord;varying vec4 v_ClipPos;uniform sampler2D s_texture0;uniform vec4 u_channelFlag;uniform vec4 u_baseColor;uniform bool u_maskFlag;void main(){ vec4 smpColor; if(u_maskFlag){ float isInside = step(u_baseColor.x, v_ClipPos.x/v_ClipPos.w) * step(u_baseColor.y, v_ClipPos.y/v_ClipPos.w) * step(v_ClipPos.x/v_ClipPos.w, u_baseColor.z) * step(v_ClipPos.y/v_ClipPos.w, u_baseColor.w); smpColor = u_channelFlag * texture2D(s_texture0 , v_texCoord).a * isInside; }else{ smpColor = texture2D(s_texture0 , v_texCoord) * u_baseColor; } gl_FragColor = smpColor;}\";\n var aL = \"attribute vec4 a_position;attribute vec2 a_texCoord;varying vec2 v_texCoord;varying vec4 v_ClipPos;uniform mat4 u_mvpMatrix;uniform mat4 u_ClipMatrix;void main(){ gl_Position = u_mvpMatrix * a_position; v_ClipPos = u_ClipMatrix * a_position; v_texCoord = a_texCoord ;}\";\n var aJ = \"precision mediump float ;varying vec2 v_texCoord;varying vec4 v_ClipPos;uniform sampler2D s_texture0;uniform sampler2D s_texture1;uniform vec4 u_channelFlag;uniform vec4 u_baseColor ;void main(){ vec4 col_formask = texture2D(s_texture0, v_texCoord) * u_baseColor; vec4 clipMask = texture2D(s_texture1, v_ClipPos.xy / v_ClipPos.w) * u_channelFlag; float maskVal = clipMask.r + clipMask.g + clipMask.b + clipMask.a; col_formask = col_formask * maskVal; gl_FragColor = col_formask;}\";\n this.vertShader = this.compileShader(aN.VERTEX_SHADER, aK);\n if (!this.vertShader) {\n q._$Ji(\"Vertex shader compile _$li!\");\n return false;\n }\n this.vertShaderOff = this.compileShader(aN.VERTEX_SHADER, aL);\n if (!this.vertShaderOff) {\n q._$Ji(\"OffVertex shader compile _$li!\");\n return false;\n }\n this.fragShader = this.compileShader(aN.FRAGMENT_SHADER, aM);\n if (!this.fragShader) {\n q._$Ji(\"Fragment shader compile _$li!\");\n return false;\n }\n this.fragShaderOff = this.compileShader(aN.FRAGMENT_SHADER, aJ);\n if (!this.fragShaderOff) {\n q._$Ji(\"OffFragment shader compile _$li!\");\n return false;\n }\n aN.attachShader(this.shaderProgram, this.vertShader);\n aN.attachShader(this.shaderProgram, this.fragShader);\n aN.attachShader(this.shaderProgramOff, this.vertShaderOff);\n aN.attachShader(this.shaderProgramOff, this.fragShaderOff);\n aN.linkProgram(this.shaderProgram);\n aN.linkProgram(this.shaderProgramOff);\n var aH = aN.getProgramParameter(this.shaderProgram, aN.LINK_STATUS);\n if (!aH) {\n var aI = aN.getProgramInfoLog(this.shaderProgram);\n q._$Ji(\"_$L0 to link program: \" + aI);\n if (this.vertShader) {\n aN.deleteShader(this.vertShader);\n this.vertShader = 0;\n }\n if (this.fragShader) {\n aN.deleteShader(this.fragShader);\n this.fragShader = 0;\n }\n if (this.shaderProgram) {\n aN.deleteProgram(this.shaderProgram);\n this.shaderProgram = 0;\n }\n if (this.vertShaderOff) {\n aN.deleteShader(this.vertShaderOff);\n this.vertShaderOff = 0;\n }\n if (this.fragShaderOff) {\n aN.deleteShader(this.fragShaderOff);\n this.fragShaderOff = 0;\n }\n if (this.shaderProgramOff) {\n aN.deleteProgram(this.shaderProgramOff);\n this.shaderProgramOff = 0;\n }\n return false;\n }\n return true;\n}\n;\nC.prototype.createFramebuffer = function() {\n var aL = this.gl;\n var aK = Q.clippingMaskBufferSize;\n var aJ = aL.createFramebuffer();\n aL.bindFramebuffer(aL.FRAMEBUFFER, aJ);\n var aH = aL.createRenderbuffer();\n aL.bindRenderbuffer(aL.RENDERBUFFER, aH);\n aL.renderbufferStorage(aL.RENDERBUFFER, aL.RGBA4, aK, aK);\n aL.framebufferRenderbuffer(aL.FRAMEBUFFER, aL.COLOR_ATTACHMENT0, aL.RENDERBUFFER, aH);\n var aI = aL.createTexture();\n aL.bindTexture(aL.TEXTURE_2D, aI);\n aL.texImage2D(aL.TEXTURE_2D, 0, aL.RGBA, aK, aK, 0, aL.RGBA, aL.UNSIGNED_BYTE, null);\n aL.texParameteri(aL.TEXTURE_2D, aL.TEXTURE_MIN_FILTER, aL.LINEAR);\n aL.texParameteri(aL.TEXTURE_2D, aL.TEXTURE_MAG_FILTER, aL.LINEAR);\n aL.texParameteri(aL.TEXTURE_2D, aL.TEXTURE_WRAP_S, aL.CLAMP_TO_EDGE);\n aL.texParameteri(aL.TEXTURE_2D, aL.TEXTURE_WRAP_T, aL.CLAMP_TO_EDGE);\n aL.framebufferTexture2D(aL.FRAMEBUFFER, aL.COLOR_ATTACHMENT0, aL.TEXTURE_2D, aI, 0);\n aL.bindTexture(aL.TEXTURE_2D, null);\n aL.bindRenderbuffer(aL.RENDERBUFFER, null);\n aL.bindFramebuffer(aL.FRAMEBUFFER, null);\n Q.fTexture[this.glno] = aI;\n return {\n framebuffer: aJ,\n renderbuffer: aH,\n texture: Q.fTexture[this.glno]\n };\n}\n;\nfunction K(aH) {\n if (j) {\n return;\n }\n this._$P = new Int8Array(8);\n this._$R0 = new DataView(this._$P.buffer);\n this._$3i = new Int8Array(1000);\n this._$hL = 0;\n this._$v0 = 0;\n this._$S2 = 0;\n this._$Ko = new Array();\n this._$T = aH;\n this._$F = 0;\n}\nK.prototype._$fP = function() {\n var aK = this._$ST();\n var aJ, aI, aH;\n if ((aK & 128) == 0) {\n return aK & 255;\n } else {\n if (((aJ = this._$ST()) & 128) == 0) {\n return ((aK & 127) << 7) | (aJ & 127);\n } else {\n if (((aI = this._$ST()) & 128) == 0) {\n return ((aK & 127) << 14) | ((aJ & 127) << 7) | (aI & 255);\n } else {\n if (((aH = this._$ST()) & 128) == 0) {\n return ((aK & 127) << 21) | ((aJ & 127) << 14) | ((aI & 127) << 7) | (aH & 255);\n } else {\n throw new J(\"_$L _$0P _\");\n }\n }\n }\n }\n}\n;\nK.prototype.getFormatVersion = function() {\n return this._$S2;\n}\n;\nK.prototype._$gr = function(aH) {\n this._$S2 = aH;\n}\n;\nK.prototype._$3L = function() {\n return this._$fP();\n}\n;\nK.prototype._$mP = function() {\n this._$zT();\n this._$F += 8;\n return this._$T.getFloat64(this._$F - 8);\n}\n;\nK.prototype._$_T = function() {\n this._$zT();\n this._$F += 4;\n return this._$T.getFloat32(this._$F - 4);\n}\n;\nK.prototype._$6L = function() {\n this._$zT();\n this._$F += 4;\n return this._$T.getInt32(this._$F - 4);\n}\n;\nK.prototype._$ST = function() {\n this._$zT();\n return this._$T.getInt8(this._$F++);\n}\n;\nK.prototype._$9T = function() {\n this._$zT();\n this._$F += 2;\n return this._$T.getInt16(this._$F - 2);\n}\n;\nK.prototype._$2T = function() {\n this._$zT();\n this._$F += 8;\n throw new J(\"_$L _$q read long\");\n}\n;\nK.prototype._$po = function() {\n this._$zT();\n return this._$T.getInt8(this._$F++) != 0;\n}\n;\nvar O = true;\nK.prototype._$bT = function() {\n this._$zT();\n var aH = this._$3L();\n var aK = null;\n if (O) {\n try {\n var aM = new ArrayBuffer(aH * 2);\n aK = new Uint16Array(aM);\n for (var aJ = 0; aJ < aH; ++aJ) {\n aK[aJ] = this._$T.getUint8(this._$F++);\n }\n return String.fromCharCode.apply(null, aK);\n } catch (aL) {\n O = false;\n }\n }\n try {\n var aI = new Array();\n if (aK == null) {\n for (var aJ = 0; aJ < aH; ++aJ) {\n aI[aJ] = this._$T.getUint8(this._$F++);\n }\n } else {\n for (var aJ = 0; aJ < aH; ++aJ) {\n aI[aJ] = aK[aJ];\n }\n }\n return String.fromCharCode.apply(null, aI);\n } catch (aL) {\n console.log(\"read utf8 / _$rT _$L0 !! : \" + aL);\n }\n}\n;\nK.prototype._$cS = function() {\n this._$zT();\n var aI = this._$3L();\n var aH = new Int32Array(aI);\n for (var aJ = 0; aJ < aI; aJ++) {\n aH[aJ] = this._$T.getInt32(this._$F);\n this._$F += 4;\n }\n return aH;\n}\n;\nK.prototype._$Tb = function() {\n this._$zT();\n var aI = this._$3L();\n var aH = new Float32Array(aI);\n for (var aJ = 0; aJ < aI; aJ++) {\n aH[aJ] = this._$T.getFloat32(this._$F);\n this._$F += 4;\n }\n return aH;\n}\n;\nK.prototype._$5b = function() {\n this._$zT();\n var aI = this._$3L();\n var aH = new Float64Array(aI);\n for (var aJ = 0; aJ < aI; aJ++) {\n aH[aJ] = this._$T.getFloat64(this._$F);\n this._$F += 8;\n }\n return aH;\n}\n;\nK.prototype._$nP = function() {\n return this._$Jb(-1);\n}\n;\nK.prototype._$Jb = function(aJ) {\n this._$zT();\n if (aJ < 0) {\n aJ = this._$3L();\n }\n if (aJ == ay._$7P) {\n var aH = this._$6L();\n if (0 <= aH && aH < this._$Ko.length) {\n return this._$Ko[aH];\n } else {\n throw new J(\"_$sL _$4i @_$m0\");\n }\n } else {\n var aI = this._$4b(aJ);\n this._$Ko.push(aI);\n return aI;\n }\n}\n;\nK.prototype._$4b = function(aN) {\n if (aN == 0) {\n return null;\n }\n if (aN == 50) {\n var aK = this._$bT();\n var aI = Z.getID(aK);\n return aI;\n } else {\n if (aN == 51) {\n var aK = this._$bT();\n var aI = n.getID(aK);\n return aI;\n } else {\n if (aN == 134) {\n var aK = this._$bT();\n var aI = i.getID(aK);\n return aI;\n } else {\n if (aN == 60) {\n var aK = this._$bT();\n var aI = z.getID(aK);\n return aI;\n }\n }\n }\n }\n if (aN >= 48) {\n var aL = ay._$9o(aN);\n if (aL != null) {\n aL._$F0(this);\n return aL;\n } else {\n return null;\n }\n }\n switch (aN) {\n case 1:\n return this._$bT();\n case 10:\n var aM = this._$6L();\n return new I(aM,true);\n case 11:\n return new av(this._$mP(),this._$mP(),this._$mP(),this._$mP());\n case 12:\n return new av(this._$_T(),this._$_T(),this._$_T(),this._$_T());\n case 13:\n return new e(this._$mP(),this._$mP());\n case 14:\n return new e(this._$_T(),this._$_T());\n case 15:\n var aH = this._$3L();\n var aI = new Array(aH);\n for (var aJ = 0; aJ < aH; aJ++) {\n aI[aJ] = this._$nP();\n }\n return aI;\n case 17:\n var aI = new aD(this._$mP(),this._$mP(),this._$mP(),this._$mP(),this._$mP(),this._$mP());\n return aI;\n case 21:\n return new F(this._$6L(),this._$6L(),this._$6L(),this._$6L());\n case 22:\n return new k(this._$6L(),this._$6L());\n case 23:\n throw new Error(\"_$L _$ro \");\n case 16:\n case 25:\n return this._$cS();\n case 26:\n return this._$5b();\n case 27:\n return this._$Tb();\n case 2:\n case 3:\n case 4:\n case 5:\n case 6:\n case 7:\n case 8:\n case 9:\n case 18:\n case 19:\n case 20:\n case 24:\n case 28:\n throw new J(\"_$6 _$q : _$nP() of 2-9 ,18,19,20,24,28 : \" + aN);\n default:\n throw new J(\"_$6 _$q : _$nP() NO _$i : \" + aN);\n }\n}\n;\nK.prototype._$8L = function() {\n if (this._$hL == 0) {\n this._$v0 = this._$ST();\n } else {\n if (this._$hL == 8) {\n this._$v0 = this._$ST();\n this._$hL = 0;\n }\n }\n return ((this._$v0 >> (7 - this._$hL++)) & 1) == 1;\n}\n;\nK.prototype._$zT = function() {\n if (this._$hL != 0) {\n this._$hL = 0;\n }\n}\n;\nfunction ai() {}\nai.prototype._$wP = function(aM, aI, aK) {\n for (var aL = 0; aL < aK; aL++) {\n for (var aH = 0; aH < aI; aH++) {\n var aJ = 2 * (aH + aL * aI);\n console.log(\"(% 7.3f , % 7.3f) , \", aM[aJ], aM[aJ + 1]);\n }\n console.log(\"\\n\");\n }\n console.log(\"\\n\");\n}\n;\nfunction aC() {}\naC._$2S = Math.PI / 180;\naC._$bS = (Math.PI / 180);\naC._$wS = 180 / Math.PI;\naC._$NS = (180 / Math.PI);\naC.PI_F = Math.PI;\naC._$kT = [0, 0.012368, 0.024734, 0.037097, 0.049454, 0.061803, 0.074143, 0.086471, 0.098786, 0.111087, 0.12337, 0.135634, 0.147877, 0.160098, 0.172295, 0.184465, 0.196606, 0.208718, 0.220798, 0.232844, 0.244854, 0.256827, 0.268761, 0.280654, 0.292503, 0.304308, 0.316066, 0.327776, 0.339436, 0.351044, 0.362598, 0.374097, 0.385538, 0.396921, 0.408243, 0.419502, 0.430697, 0.441826, 0.452888, 0.463881, 0.474802, 0.485651, 0.496425, 0.507124, 0.517745, 0.528287, 0.538748, 0.549126, 0.559421, 0.56963, 0.579752, 0.589785, 0.599728, 0.609579, 0.619337, 0.629, 0.638567, 0.648036, 0.657406, 0.666676, 0.675843, 0.684908, 0.693867, 0.70272, 0.711466, 0.720103, 0.72863, 0.737045, 0.745348, 0.753536, 0.76161, 0.769566, 0.777405, 0.785125, 0.792725, 0.800204, 0.807561, 0.814793, 0.821901, 0.828884, 0.835739, 0.842467, 0.849066, 0.855535, 0.861873, 0.868079, 0.874153, 0.880093, 0.885898, 0.891567, 0.897101, 0.902497, 0.907754, 0.912873, 0.917853, 0.922692, 0.92739, 0.931946, 0.936359, 0.940629, 0.944755, 0.948737, 0.952574, 0.956265, 0.959809, 0.963207, 0.966457, 0.96956, 0.972514, 0.97532, 0.977976, 0.980482, 0.982839, 0.985045, 0.987101, 0.989006, 0.990759, 0.992361, 0.993811, 0.995109, 0.996254, 0.997248, 0.998088, 0.998776, 0.999312, 0.999694, 0.999924, 1];\naC._$92 = function(aK, aI) {\n var aH = Math.atan2(aK[1], aK[0]);\n var aJ = Math.atan2(aI[1], aI[0]);\n return aC._$tS(aH, aJ);\n}\n;\naC._$tS = function(aI, aH) {\n var aJ = aI - aH;\n while (aJ < -Math.PI) {\n aJ += 2 * Math.PI;\n }\n while (aJ > Math.PI) {\n aJ -= 2 * Math.PI;\n }\n return aJ;\n}\n;\naC._$9 = function(aH) {\n return Math.sin(aH);\n}\n;\naC.fcos = function(aH) {\n return Math.cos(aH);\n}\n;\nfunction aB(aH) {\n if (j) {\n return;\n }\n this._$e0 = null;\n this._$IP = null;\n this._$Us = null;\n this._$7s = null;\n this._$IS = [false];\n this._$VS = null;\n this._$AT = true;\n this.baseOpacity = 1;\n this.clipBufPre_clipContext = null;\n this._$e0 = aH;\n}\naB.prototype._$u2 = function() {\n return this._$IS[0];\n}\n;\naB.prototype._$yo = function() {\n return this._$AT && !this._$IS[0];\n}\n;\naB.prototype._$GT = function() {\n return this._$e0;\n}\n;\nfunction r() {}\nr._$W2 = 0;\nr.SYSTEM_INFO = null;\nr.USER_AGENT = navigator.userAgent;\nr.isIPhone = function() {\n if (!r.SYSTEM_INFO) {\n r.setup();\n }\n return r.SYSTEM_INFO._isIPhone;\n}\n;\nr.isIOS = function() {\n if (!r.SYSTEM_INFO) {\n r.setup();\n }\n return r.SYSTEM_INFO._isIPhone || r.SYSTEM_INFO._isIPad;\n}\n;\nr.isAndroid = function() {\n if (!r.SYSTEM_INFO) {\n r.setup();\n }\n return r.SYSTEM_INFO._isAndroid;\n}\n;\nr.getOSVersion = function() {\n if (!r.SYSTEM_INFO) {\n r.setup();\n }\n return r.SYSTEM_INFO.version;\n}\n;\nr.getOS = function() {\n if (!r.SYSTEM_INFO) {\n r.setup();\n }\n if (r.SYSTEM_INFO._isIPhone || r.SYSTEM_INFO._isIPad) {\n return \"iOS\";\n }\n if (r.SYSTEM_INFO._isAndroid) {\n return \"Android\";\n } else {\n return \"_$Q0 OS\";\n }\n}\n;\nr.setup = function() {\n var aK = r.USER_AGENT;\n function aI(aO, aR) {\n var aN = aO.substring(aR).split(/[ _,;\\.]/);\n var aQ = 0;\n for (var aM = 0; aM <= 2; aM++) {\n if (isNaN(aN[aM])) {\n break;\n }\n var aP = parseInt(aN[aM]);\n if (aP < 0 || aP > 999) {\n q._$li(\"err : \" + aP + \" @UtHtml5.setup()\");\n aQ = 0;\n break;\n }\n aQ += aP * Math.pow(1000, (2 - aM));\n }\n return aQ;\n }\n var aL;\n var aH;\n var aJ = r.SYSTEM_INFO = {\n userAgent: aK\n };\n if ((aL = aK.indexOf(\"iPhone OS \")) >= 0) {\n aJ.os = \"iPhone\";\n aJ._isIPhone = true;\n aJ.version = aI(aK, aL + \"iPhone OS \".length);\n } else {\n if ((aL = aK.indexOf(\"iPad\")) >= 0) {\n aL = aK.indexOf(\"CPU OS\");\n if (aL < 0) {\n q._$li(\" err : \" + aK + \" @UtHtml5.setup()\");\n return;\n }\n aJ.os = \"iPad\";\n aJ._isIPad = true;\n aJ.version = aI(aK, aL + \"CPU OS \".length);\n } else {\n if ((aL = aK.indexOf(\"Android\")) >= 0) {\n aJ.os = \"Android\";\n aJ._isAndroid = true;\n aJ.version = aI(aK, aL + \"Android \".length);\n } else {\n aJ.os = \"-\";\n aJ.version = -1;\n }\n }\n }\n}\n;\nQ.init();\nvar j = false;\n\nexport{\n P as UtSystem,\n q as UtDebug,\n am as LDTransform,\n au as LDGL,\n Q as Live2D,\n l as Live2DModelWebGL,\n v as Live2DModelJS,\n ao as Live2DMotion,\n V as MotionQueueManager,\n u as PhysicsHair,\n ah as AMotion,\n i as PartsDataID,\n Z as DrawDataID,\n n as BaseDataID,\n z as ParamID,\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/lib/live2d.core.js","/**\n *\n * You can modify and use this source freely\n * only for the development of application related Live2D.\n *\n * (c) Live2D Inc. All rights reserved.\n */\n\n/**\n * EYHN 基于 live2d 官方 Live2DFramework.js 修改\n *\n * Copyright © 2016 - 2017 EYHN\n */\n\n// Modified by xiazeyu.\n\n/**\n* @desc Basic functions releated to model react\n*/\n\nimport { UtSystem,\n UtDebug,\n LDTransform,\n LDGL,\n Live2D,\n Live2DModelWebGL,\n Live2DModelJS,\n Live2DMotion,\n MotionQueueManager,\n PhysicsHair,\n AMotion,\n PartsDataID,\n DrawDataID,\n BaseDataID,\n ParamID } from './live2d.core';\n\n//============================================================\n//============================================================\n// class L2DBaseModel\n//============================================================\n//============================================================\nfunction L2DBaseModel() {\n this.live2DModel = null; // ALive2DModel\n this.modelMatrix = null; // L2DModelMatrix\n this.eyeBlink = null; // L2DEyeBlink\n this.physics = null; // L2DPhysics\n this.pose = null; // L2DPose\n this.debugMode = false;\n this.initialized = false;\n this.updating = false;\n this.alpha = 1;\n this.accAlpha = 0;\n this.lipSync = false;\n this.lipSyncValue = 0;\n this.accelX = 0;\n this.accelY = 0;\n this.accelZ = 0;\n this.dragX = 0;\n this.dragY = 0;\n this.startTimeMSec = null;\n this.mainMotionManager = new L2DMotionManager(); //L2DMotionManager\n this.expressionManager = new L2DMotionManager(); //L2DMotionManager\n this.motions = {};\n this.expressions = {};\n this.isTexLoaded = false;\n}\n\nvar texCounter = 0;\n\n//============================================================\n// L2DBaseModel # getModelMatrix()\n//============================================================\nL2DBaseModel.prototype.getModelMatrix = function () {\n return this.modelMatrix;\n}\n\n//============================================================\n// L2DBaseModel # setAlpha()\n//============================================================\nL2DBaseModel.prototype.setAlpha = function (a/*float*/) {\n if (a > 0.999) a = 1;\n if (a < 0.001) a = 0;\n this.alpha = a;\n}\n\n//============================================================\n// L2DBaseModel # getAlpha()\n//============================================================\nL2DBaseModel.prototype.getAlpha = function () {\n return this.alpha;\n}\n\n//============================================================\n// L2DBaseModel # isInitialized()\n//============================================================\nL2DBaseModel.prototype.isInitialized = function () {\n return this.initialized;\n}\n\n//============================================================\n// L2DBaseModel # setInitialized()\n//============================================================\nL2DBaseModel.prototype.setInitialized = function (v/*boolean*/) {\n this.initialized = v;\n}\n\n//============================================================\n// L2DBaseModel # isUpdating()\n//============================================================\nL2DBaseModel.prototype.isUpdating = function () {\n return this.updating;\n}\n\n//============================================================\n// L2DBaseModel # setUpdating()\n//============================================================\nL2DBaseModel.prototype.setUpdating = function (v/*boolean*/) {\n this.updating = v;\n}\n\n//============================================================\n// L2DBaseModel # getLive2DModel()\n//============================================================\nL2DBaseModel.prototype.getLive2DModel = function () {\n return this.live2DModel;\n}\n\n//============================================================\n// L2DBaseModel # setLipSync()\n//============================================================\nL2DBaseModel.prototype.setLipSync = function (v/*boolean*/) {\n this.lipSync = v;\n}\n\n//============================================================\n// L2DBaseModel # setLipSyncValue()\n//============================================================\nL2DBaseModel.prototype.setLipSyncValue = function (v/*float*/) {\n this.lipSyncValue = v;\n}\n\n//============================================================\n// L2DBaseModel # setAccel()\n//============================================================\nL2DBaseModel.prototype.setAccel = function (x/*float*/, y/*float*/, z/*float*/) {\n this.accelX = x;\n this.accelY = y;\n this.accelZ = z;\n}\n\n//============================================================\n// L2DBaseModel # setDrag()\n//============================================================\nL2DBaseModel.prototype.setDrag = function (x/*float*/, y/*float*/) {\n this.dragX = x;\n this.dragY = y;\n}\n\n//============================================================\n// L2DBaseModel # getMainMotionManager()\n//============================================================\nL2DBaseModel.prototype.getMainMotionManager = function () {\n return this.mainMotionManager;\n}\n\n//============================================================\n// L2DBaseModel # getExpressionManager()\n//============================================================\nL2DBaseModel.prototype.getExpressionManager = function () {\n return this.expressionManager;\n}\n\n//============================================================\n// L2DBaseModel # loadModelData()\n//============================================================\nL2DBaseModel.prototype.loadModelData = function (path/*String*/, callback) {\n /*\n if( this.live2DModel != null ) {\n this.live2DModel.deleteTextures();\n }\n */\n var pm = Live2DFramework.getPlatformManager(); //IPlatformManager\n if (this.debugMode) pm.log(\"Load model : \" + path);\n\n var thisRef = this;\n pm.loadLive2DModel(path, function (l2dModel) {\n thisRef.live2DModel = l2dModel;\n thisRef.live2DModel.saveParam();\n\n var _err = Live2D.getError();\n\n if (_err != 0) {\n console.error(\"Error : Failed to loadModelData().\");\n return;\n }\n\n thisRef.modelMatrix = new L2DModelMatrix(\n thisRef.live2DModel.getCanvasWidth(),\n thisRef.live2DModel.getCanvasHeight()); //L2DModelMatrix\n thisRef.modelMatrix.setWidth(2);\n thisRef.modelMatrix.setCenterPosition(0, 0);\n\n callback(thisRef.live2DModel);\n });\n}\n\n\n//============================================================\n// L2DBaseModel # loadTexture()\n//============================================================\nL2DBaseModel.prototype.loadTexture = function (no/*int*/, path/*String*/, callback) {\n texCounter++;\n\n var pm = Live2DFramework.getPlatformManager(); //IPlatformManager\n\n if (this.debugMode) pm.log(\"Load Texture : \" + path);\n\n var thisRef = this;\n pm.loadTexture(this.live2DModel, no, path, function () {\n texCounter--;\n if (texCounter == 0) thisRef.isTexLoaded = true;\n if (typeof callback == \"function\") callback();\n });\n\n}\n\n//============================================================\n// L2DBaseModel # loadMotion()\n//============================================================\nL2DBaseModel.prototype.loadMotion = function (name/*String*/, path /*String*/, callback) {\n var pm = Live2DFramework.getPlatformManager(); //IPlatformManager\n\n if (this.debugMode) pm.log(\"Load Motion : \" + path);\n\n var motion = null; //Live2DMotion\n\n var thisRef = this;\n pm.loadBytes(path, function (buf) {\n motion = Live2DMotion.loadMotion(buf);\n if (name != null) {\n thisRef.motions[name] = motion;\n }\n callback(motion);\n });\n\n}\n\n//============================================================\n// L2DBaseModel # loadExpression()\n//============================================================\nL2DBaseModel.prototype.loadExpression = function (name/*String*/, path /*String*/, callback) {\n var pm = Live2DFramework.getPlatformManager(); //IPlatformManager\n\n if (this.debugMode) pm.log(\"Load Expression : \" + path);\n\n var thisRef = this;\n pm.loadBytes(path, function (buf) {\n if (name != null) {\n thisRef.expressions[name] = L2DExpressionMotion.loadJson(buf);\n }\n if (typeof callback == \"function\") callback();\n });\n}\n\n//============================================================\n// L2DBaseModel # loadPose()\n//============================================================\nL2DBaseModel.prototype.loadPose = function (path /*String*/, callback) {\n var pm = Live2DFramework.getPlatformManager(); //IPlatformManager\n if (this.debugMode) pm.log(\"Load Pose : \" + path);\n var thisRef = this;\n try {\n pm.loadBytes(path, function (buf) {\n thisRef.pose = L2DPose.load(buf);\n if (typeof callback == \"function\") callback();\n });\n }\n catch (e) {\n console.warn(e);\n }\n}\n\n//============================================================\n// L2DBaseModel # loadPhysics()\n//============================================================\nL2DBaseModel.prototype.loadPhysics = function (path/*String*/) {\n var pm = Live2DFramework.getPlatformManager(); //IPlatformManager\n if (this.debugMode) pm.log(\"Load Physics : \" + path);\n var thisRef = this;\n try {\n pm.loadBytes(path, function (buf) {\n thisRef.physics = L2DPhysics.load(buf);\n });\n }\n catch (e) {\n console.warn(e);\n }\n}\n\n//============================================================\n// L2DBaseModel # hitTestSimple()\n//============================================================\nL2DBaseModel.prototype.hitTestSimple = function (drawID, testX, testY) {\n\n\tif(this.live2DModel === null) return !1;\n\n var drawIndex = this.live2DModel.getDrawDataIndex(drawID);\n\n if (drawIndex < 0) return false;\n\n var points = this.live2DModel.getTransformedPoints(drawIndex);\n var left = this.live2DModel.getCanvasWidth();\n var right = 0;\n var top = this.live2DModel.getCanvasHeight();\n var bottom = 0;\n\n for (var j = 0; j < points.length; j = j + 2) {\n var x = points[j];\n var y = points[j + 1];\n\n if (x < left) left = x;\n if (x > right) right = x;\n if (y < top) top = y;\n if (y > bottom) bottom = y;\n }\n var tx = this.modelMatrix.invertTransformX(testX);\n var ty = this.modelMatrix.invertTransformY(testY);\n\n return (left <= tx && tx <= right && top <= ty && ty <= bottom);\n}\n\n//============================================================\n//============================================================\n// class L2DExpressionMotion extends AMotion\n//============================================================\n//============================================================\nfunction L2DExpressionMotion() {\n AMotion.prototype.constructor.call(this);\n this.paramList = new Array(); //ArrayList\n}\n\nL2DExpressionMotion.prototype = new AMotion(); // L2DExpressionMotion extends AMotion\n\n//============================================================\nL2DExpressionMotion.EXPRESSION_DEFAULT = \"DEFAULT\";\nL2DExpressionMotion.TYPE_SET = 0;\nL2DExpressionMotion.TYPE_ADD = 1;\nL2DExpressionMotion.TYPE_MULT = 2;\n\n//============================================================\n// static L2DExpressionMotion.loadJson()\n//============================================================\nL2DExpressionMotion.loadJson = function (buf) {\n var ret = new L2DExpressionMotion();\n\n var pm = Live2DFramework.getPlatformManager();\n var json = pm.jsonParseFromBytes(buf);\n\n ret.setFadeIn(parseInt(json.fade_in) > 0 ? parseInt(json.fade_in) : 1000);\n ret.setFadeOut(parseInt(json.fade_out) > 0 ? parseInt(json.fade_out) : 1000);\n\n if (json.params == null) {\n return ret;\n }\n\n var params = json.params;\n var paramNum = params.length;\n ret.paramList = []; //ArrayList\n for (var i = 0; i < paramNum; i++) {\n var param = params[i];\n var paramID = param.id.toString();\n var value = parseFloat(param.val);\n var calcTypeInt = L2DExpressionMotion.TYPE_ADD;\n var calc = param.calc != null ? param.calc.toString() : \"add\";\n if (calc === \"add\") {\n calcTypeInt = L2DExpressionMotion.TYPE_ADD;\n }\n else if (calc === \"mult\") {\n calcTypeInt = L2DExpressionMotion.TYPE_MULT;\n }\n else if (calc === \"set\") {\n calcTypeInt = L2DExpressionMotion.TYPE_SET;\n }\n else {\n calcTypeInt = L2DExpressionMotion.TYPE_ADD;\n }\n if (calcTypeInt == L2DExpressionMotion.TYPE_ADD) {\n var defaultValue = param.def == null ? 0 : parseFloat(param.def);\n value = value - defaultValue;\n }\n else if (calcTypeInt == L2DExpressionMotion.TYPE_MULT) {\n var defaultValue = param.def == null ? 1 : parseFloat(param.def);\n if (defaultValue == 0) defaultValue = 1;\n value = value / defaultValue;\n }\n\n var item = new L2DExpressionParam();\n item.id = paramID;\n item.type = calcTypeInt;\n item.value = value;\n\n ret.paramList.push(item);\n }\n\n return ret;\n}\n\n\n//============================================================\n// L2DExpressionMotion # updateParamExe()\n//============================================================\nL2DExpressionMotion.prototype.updateParamExe = function (model /*ALive2DModel*/, timeMSec/*long*/, weight /*float*/, motionQueueEnt /*MotionQueueEnt*/) {\n for (var i = this.paramList.length - 1; i >= 0; --i) {\n var param = this.paramList[i]; //L2DExpressionParam\n // if (!param || !param.type) continue;\n if (param.type == L2DExpressionMotion.TYPE_ADD) {\n model.addToParamFloat(param.id, param.value, weight);\n }\n else if (param.type == L2DExpressionMotion.TYPE_MULT) {\n model.multParamFloat(param.id, param.value, weight);\n }\n else if (param.type == L2DExpressionMotion.TYPE_SET) {\n model.setParamFloat(param.id, param.value, weight);\n }\n }\n}\n\n//============================================================\n//============================================================\n// class L2DExpressionParam\n//============================================================\n//============================================================\nfunction L2DExpressionParam() {\n this.id = \"\";\n this.type = -1;\n this.value = null;\n}\n\n//============================================================\n//============================================================\n// class L2DEyeBlink\n//============================================================\n//============================================================\nfunction L2DEyeBlink() {\n this.nextBlinkTime = null /* TODO NOT INIT */; //\n this.stateStartTime = null /* TODO NOT INIT */; //\n this.blinkIntervalMsec = null /* TODO NOT INIT */; //\n this.eyeState = EYE_STATE.STATE_FIRST;\n this.blinkIntervalMsec = 4000;\n this.closingMotionMsec = 100;\n this.closedMotionMsec = 50;\n this.openingMotionMsec = 150;\n this.closeIfZero = true;\n this.eyeID_L = \"PARAM_EYE_L_OPEN\";\n this.eyeID_R = \"PARAM_EYE_R_OPEN\";\n}\n\n//============================================================\n// L2DEyeBlink # calcNextBlink()\n//============================================================\nL2DEyeBlink.prototype.calcNextBlink = function () {\n var time /*long*/ = UtSystem.getUserTimeMSec();\n var r /*Number*/ = Math.random();\n return /*(long)*/ (time + r * (2 * this.blinkIntervalMsec - 1));\n}\n\n//============================================================\n// L2DEyeBlink # setInterval()\n//============================================================\nL2DEyeBlink.prototype.setInterval = function (blinkIntervalMsec /*int*/) {\n this.blinkIntervalMsec = blinkIntervalMsec;\n}\n\n//============================================================\n// L2DEyeBlink # setEyeMotion()\n//============================================================\nL2DEyeBlink.prototype.setEyeMotion = function (closingMotionMsec/*int*/, closedMotionMsec/*int*/, openingMotionMsec/*int*/) {\n this.closingMotionMsec = closingMotionMsec;\n this.closedMotionMsec = closedMotionMsec;\n this.openingMotionMsec = openingMotionMsec;\n}\n\n//============================================================\n// L2DEyeBlink # updateParam()\n//============================================================\nL2DEyeBlink.prototype.updateParam = function (model/*ALive2DModel*/) {\n var time /*:long*/ = UtSystem.getUserTimeMSec();\n var eyeParamValue /*:Number*/;\n var t /*:Number*/ = 0;\n switch (this.eyeState) {\n case EYE_STATE.STATE_CLOSING:\n t = (time - this.stateStartTime) / this.closingMotionMsec;\n if (t >= 1) {\n t = 1;\n this.eyeState = EYE_STATE.STATE_CLOSED;\n this.stateStartTime = time;\n }\n eyeParamValue = 1 - t;\n break;\n case EYE_STATE.STATE_CLOSED:\n t = (time - this.stateStartTime) / this.closedMotionMsec;\n if (t >= 1) {\n this.eyeState = EYE_STATE.STATE_OPENING;\n this.stateStartTime = time;\n }\n eyeParamValue = 0;\n break;\n case EYE_STATE.STATE_OPENING:\n t = (time - this.stateStartTime) / this.openingMotionMsec;\n if (t >= 1) {\n t = 1;\n this.eyeState = EYE_STATE.STATE_INTERVAL;\n this.nextBlinkTime = this.calcNextBlink();\n }\n eyeParamValue = t;\n break;\n case EYE_STATE.STATE_INTERVAL:\n if (this.nextBlinkTime < time) {\n this.eyeState = EYE_STATE.STATE_CLOSING;\n this.stateStartTime = time;\n }\n eyeParamValue = 1;\n break;\n case EYE_STATE.STATE_FIRST:\n default:\n this.eyeState = EYE_STATE.STATE_INTERVAL;\n this.nextBlinkTime = this.calcNextBlink();\n eyeParamValue = 1;\n break;\n }\n if (!this.closeIfZero) eyeParamValue = -eyeParamValue;\n model.setParamFloat(this.eyeID_L, eyeParamValue);\n model.setParamFloat(this.eyeID_R, eyeParamValue);\n}\n\n//== enum EYE_STATE ==\nvar EYE_STATE = function () { };\n\nEYE_STATE.STATE_FIRST = \"STATE_FIRST\"\nEYE_STATE.STATE_INTERVAL = \"STATE_INTERVAL\"\nEYE_STATE.STATE_CLOSING = \"STATE_CLOSING\"\nEYE_STATE.STATE_CLOSED = \"STATE_CLOSED\"\nEYE_STATE.STATE_OPENING = \"STATE_OPENING\"\n\n//============================================================\n//============================================================\n// class L2DMatrix44\n//============================================================\n//============================================================\nfunction L2DMatrix44() {\n this.tr = new Float32Array(16); //\n this.identity();\n}\n\n//============================================================\n// static L2DMatrix44.mul()\n//============================================================\n// matrix multiplication\nL2DMatrix44.mul = function (a/*float[]*/, b/*float[]*/, dst/*float[]*/) {\n var c = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];\n var n = 4;\n var i, j, k;\n for (i = 0; i < n; i++) {\n for (j = 0; j < n; j++) {\n for (k = 0; k < n; k++) {\n c[i + j * 4] += a[i + k * 4] * b[k + j * 4];\n }\n }\n }\n for (i = 0; i < 16; i++) {\n dst[i] = c[i];\n }\n}\n\n//============================================================\n// L2DMatrix44 # identity()\n//============================================================\nL2DMatrix44.prototype.identity = function () {\n for (var i/*:int*/ = 0; i < 16; i++)\n this.tr[i] = ((i % 5) == 0) ? 1 : 0;\n}\n\n//============================================================\n// L2DMatrix44 # getArray()\n//============================================================\nL2DMatrix44.prototype.getArray = function () {\n return this.tr;\n}\n\n//============================================================\n// L2DMatrix44 # getCopyMatrix()\n//============================================================\nL2DMatrix44.prototype.getCopyMatrix = function () {\n return new Float32Array(this.tr); // this.tr.clone();\n}\n\n//============================================================\n// L2DMatrix44 # setMatrix()\n//============================================================\nL2DMatrix44.prototype.setMatrix = function (tr/*float[]*/) {\n if (this.tr == null || this.tr.length != this.tr.length) return;\n for (var i/*:int*/ = 0; i < 16; i++) this.tr[i] = tr[i];\n}\n\n//============================================================\n// L2DMatrix44 # getScaleX()\n//============================================================\nL2DMatrix44.prototype.getScaleX = function () {\n return this.tr[0];\n}\n\n//============================================================\n// L2DMatrix44 # getScaleY()\n//============================================================\nL2DMatrix44.prototype.getScaleY = function () {\n return this.tr[5];\n}\n\n//============================================================\n// L2DMatrix44 # transformX()\n//============================================================\nL2DMatrix44.prototype.transformX = function (src/*float*/) {\n return this.tr[0] * src + this.tr[12];\n}\n\n//============================================================\n// L2DMatrix44 # transformY()\n//============================================================\nL2DMatrix44.prototype.transformY = function (src/*float*/) {\n return this.tr[5] * src + this.tr[13];\n}\n\n//============================================================\n// L2DMatrix44 # invertTransformX()\n//============================================================\nL2DMatrix44.prototype.invertTransformX = function (src/*float*/) {\n return (src - this.tr[12]) / this.tr[0];\n}\n\n//============================================================\n// L2DMatrix44 # invertTransformY()\n//============================================================\nL2DMatrix44.prototype.invertTransformY = function (src/*float*/) {\n return (src - this.tr[13]) / this.tr[5];\n}\n\n//============================================================\n// L2DMatrix44 # multTranslate()\n//============================================================\nL2DMatrix44.prototype.multTranslate = function (shiftX/*float*/, shiftY/*float*/) {\n var tr1 = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, shiftX, shiftY, 0, 1];\n L2DMatrix44.mul(tr1, this.tr, this.tr);\n}\n\n//============================================================\n// L2DMatrix44 # translate()\n//============================================================\nL2DMatrix44.prototype.translate = function (x/*float*/, y/*float*/) {\n this.tr[12] = x;\n this.tr[13] = y;\n}\n\n//============================================================\n// L2DMatrix44 # translateX()\n//============================================================\nL2DMatrix44.prototype.translateX = function (x/*float*/) {\n this.tr[12] = x;\n}\n\n//============================================================\n// L2DMatrix44 # translateY()\n//============================================================\nL2DMatrix44.prototype.translateY = function (y/*float*/) {\n this.tr[13] = y;\n}\n\n//============================================================\n// L2DMatrix44 # multScale()\n//============================================================\nL2DMatrix44.prototype.multScale = function (scaleX/*float*/, scaleY/*float*/) {\n var tr1 = [scaleX, 0, 0, 0, 0, scaleY, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];\n L2DMatrix44.mul(tr1, this.tr, this.tr);\n}\n\n//============================================================\n// L2DMatrix44 # scale()\n//============================================================\nL2DMatrix44.prototype.scale = function (scaleX/*float*/, scaleY/*float*/) {\n this.tr[0] = scaleX;\n this.tr[5] = scaleY;\n}\n\n//============================================================\n//============================================================\n// class L2DModelMatrix extends L2DMatrix44\n//============================================================\n//============================================================\nfunction L2DModelMatrix(w/*float*/, h/*float*/) {\n L2DMatrix44.prototype.constructor.call(this);\n this.width = w;\n this.height = h;\n}\n\n//L2DModelMatrix extends L2DMatrix44\nL2DModelMatrix.prototype = new L2DMatrix44();\n\n//============================================================\n// L2DModelMatrix # setPosition()\n//============================================================\nL2DModelMatrix.prototype.setPosition = function (x/*float*/, y/*float*/) {\n this.translate(x, y);\n}\n\n//============================================================\n// L2DModelMatrix # setCenterPosition()\n//============================================================\nL2DModelMatrix.prototype.setCenterPosition = function (x/*float*/, y/*float*/) {\n var w = this.width * this.getScaleX();\n var h = this.height * this.getScaleY();\n this.translate(x - w / 2, y - h / 2);\n}\n\n//============================================================\n// L2DModelMatrix # top()\n//============================================================\nL2DModelMatrix.prototype.top = function (y/*float*/) {\n this.setY(y);\n}\n\n//============================================================\n// L2DModelMatrix # bottom()\n//============================================================\nL2DModelMatrix.prototype.bottom = function (y/*float*/) {\n var h = this.height * this.getScaleY();\n this.translateY(y - h);\n}\n\n//============================================================\n// L2DModelMatrix # left()\n//============================================================\nL2DModelMatrix.prototype.left = function (x/*float*/) {\n this.setX(x);\n}\n\n//============================================================\n// L2DModelMatrix # right()\n//============================================================\nL2DModelMatrix.prototype.right = function (x/*float*/) {\n var w = this.width * this.getScaleX();\n this.translateX(x - w);\n}\n\n//============================================================\n// L2DModelMatrix # centerX()\n//============================================================\nL2DModelMatrix.prototype.centerX = function (x/*float*/) {\n var w = this.width * this.getScaleX();\n this.translateX(x - w / 2);\n}\n\n//============================================================\n// L2DModelMatrix # centerY()\n//============================================================\nL2DModelMatrix.prototype.centerY = function (y/*float*/) {\n var h = this.height * this.getScaleY();\n this.translateY(y - h / 2);\n}\n\n//============================================================\n// L2DModelMatrix # setX()\n//============================================================\nL2DModelMatrix.prototype.setX = function (x/*float*/) {\n this.translateX(x);\n}\n\n//============================================================\n// L2DModelMatrix # setY()\n//============================================================\nL2DModelMatrix.prototype.setY = function (y/*float*/) {\n this.translateY(y);\n}\n\n//============================================================\n// L2DModelMatrix # setHeight()\n//============================================================\nL2DModelMatrix.prototype.setHeight = function (h/*float*/) {\n var scaleX = h / this.height;\n var scaleY = -scaleX;\n this.scale(scaleX, scaleY);\n}\n\n//============================================================\n// L2DModelMatrix # setWidth()\n//============================================================\nL2DModelMatrix.prototype.setWidth = function (w/*float*/) {\n var scaleX = w / this.width;\n var scaleY = -scaleX;\n this.scale(scaleX, scaleY);\n}\n\n//============================================================\n//============================================================\n// class L2DMotionManager extends MotionQueueManager\n//============================================================\n//============================================================\nfunction L2DMotionManager() {\n MotionQueueManager.prototype.constructor.call(this);\n this.currentPriority = null;\n this.reservePriority = null;\n\n this.super = MotionQueueManager.prototype;\n}\n\n\nL2DMotionManager.prototype = new MotionQueueManager();\n\n//============================================================\n// L2DMotionManager # getCurrentPriority()\n//============================================================\nL2DMotionManager.prototype.getCurrentPriority = function () {\n return this.currentPriority;\n}\n\n//============================================================\n// L2DMotionManager # getReservePriority()\n//============================================================\nL2DMotionManager.prototype.getReservePriority = function () {\n return this.reservePriority;\n}\n\n//============================================================\n// L2DMotionManager # reserveMotion()\n//============================================================\nL2DMotionManager.prototype.reserveMotion = function (priority/*int*/) {\n if (this.reservePriority >= priority) {\n return false;\n }\n if (this.currentPriority >= priority) {\n return false;\n }\n\n this.reservePriority = priority;\n\n return true;\n}\n\n//============================================================\n// L2DMotionManager # setReservePriority()\n//============================================================\nL2DMotionManager.prototype.setReservePriority = function (val/*int*/) {\n this.reservePriority = val;\n}\n\n//============================================================\n// L2DMotionManager # updateParam()\n//============================================================\nL2DMotionManager.prototype.updateParam = function (model/*ALive2DModel*/) {\n var updated = MotionQueueManager.prototype.updateParam.call(this, model);\n\n if (this.isFinished()) {\n this.currentPriority = 0;\n }\n\n return updated;\n}\n\n//============================================================\n// L2DMotionManager # startMotionPrio()\n//============================================================\nL2DMotionManager.prototype.startMotionPrio = function (motion/*AMotion*/, priority/*int*/) {\n if (priority == this.reservePriority) {\n this.reservePriority = 0;\n }\n this.currentPriority = priority;\n return this.startMotion(motion, false);\n}\n\n//============================================================\n//============================================================\n// class L2DPhysics\n//============================================================\n//============================================================\nfunction L2DPhysics() {\n this.physicsList = new Array(); //ArrayList\n this.startTimeMSec = UtSystem.getUserTimeMSec();\n}\n\n//============================================================\n// static L2DPhysics.load()\n//============================================================\nL2DPhysics.load = function (buf /*byte[]*/) {\n var ret = new L2DPhysics(); //L2DPhysicsL2DPhysics\n var pm = Live2DFramework.getPlatformManager();\n var json = pm.jsonParseFromBytes(buf);\n var params = json.physics_hair;\n var paramNum = params.length;\n for (var i = 0; i < paramNum; i++) {\n var param = params[i]; //Value\n var physics = new PhysicsHair(); //PhysicsHairPhysicsHair\n var setup = param.setup; //Value\n var length = parseFloat(setup.length);\n var resist = parseFloat(setup.regist);\n var mass = parseFloat(setup.mass);\n physics.setup(length, resist, mass);\n var srcList = param.src; //Value\n var srcNum = srcList.length;\n for (var j = 0; j < srcNum; j++) {\n var src = srcList[j]; //Value\n var id = src.id; //String\n var type = PhysicsHair.Src.SRC_TO_X;\n var typeStr = src.ptype; //String\n if (typeStr === \"x\") {\n type = PhysicsHair.Src.SRC_TO_X;\n }\n else if (typeStr === \"y\") {\n type = PhysicsHair.Src.SRC_TO_Y;\n }\n else if (typeStr === \"angle\") {\n type = PhysicsHair.Src.SRC_TO_G_ANGLE;\n }\n else {\n UtDebug.error(\"live2d\", \"Invalid parameter:PhysicsHair.Src\");\n }\n var scale = parseFloat(src.scale);\n var weight = parseFloat(src.weight);\n physics.addSrcParam(type, id, scale, weight);\n }\n var targetList = param.targets; //Value\n var targetNum = targetList.length;\n for (var j = 0; j < targetNum; j++) {\n var target = targetList[j]; //Value\n var id = target.id; //String\n var type = PhysicsHair.Target.TARGET_FROM_ANGLE;\n var typeStr = target.ptype; //String\n if (typeStr === \"angle\") {\n type = PhysicsHair.Target.TARGET_FROM_ANGLE;\n }\n else if (typeStr === \"angle_v\") {\n type = PhysicsHair.Target.TARGET_FROM_ANGLE_V;\n }\n else {\n UtDebug.error(\"live2d\", \"Invalid parameter:PhysicsHair.Target\");\n }\n var scale = parseFloat(target.scale);\n var weight = parseFloat(target.weight);\n physics.addTargetParam(type, id, scale, weight);\n }\n ret.physicsList.push(physics);\n }\n return ret;\n}\n\n//============================================================\n// L2DPhysics # updateParam()\n//============================================================\nL2DPhysics.prototype.updateParam = function (model/*ALive2DModel*/) {\n var timeMSec = UtSystem.getUserTimeMSec() - this.startTimeMSec;\n for (var i = 0; i < this.physicsList.length; i++) {\n this.physicsList[i].update(model, timeMSec);\n }\n}\n\n//============================================================\n//============================================================\n// class L2DPose\n//============================================================\n//============================================================\nfunction L2DPose() {\n this.lastTime = 0;\n this.lastModel = null; //ALive2DModel\n this.partsGroups = new Array(); //ArrayList\n}\n\n\n//============================================================\n// static L2DPose.load()\n//============================================================\nL2DPose.load = function (buf/*byte[]*/) {\n var ret = new L2DPose(); //L2DPose\n var pm = Live2DFramework.getPlatformManager();\n var json = pm.jsonParseFromBytes(buf);\n var poseListInfo = json.parts_visible; //Value\n var poseNum = poseListInfo.length;\n for (var i_pose = 0; i_pose < poseNum; i_pose++) {\n var poseInfo = poseListInfo[i_pose]; //Value\n var idListInfo = poseInfo.group; //Value\n var idNum = idListInfo.length;\n var partsGroup/*L2DPartsParam*/ = new Array();\n for (var i_group = 0; i_group < idNum; i_group++) {\n var partsInfo = idListInfo[i_group]; //Value\n var parts = new L2DPartsParam(partsInfo.id); //L2DPartsParamL2DPartsParam\n partsGroup[i_group] = parts;\n if (partsInfo.link == null) continue;\n var linkListInfo = partsInfo.link; //Value\n var linkNum = linkListInfo.length;\n parts.link = new Array(); //ArrayList\n for (var i_link = 0; i_link < linkNum; i_link++) {\n var linkParts = new L2DPartsParam(linkListInfo[i_link]); //L2DPartsParamL2DPartsParam\n parts.link.push(linkParts);\n }\n }\n ret.partsGroups.push(partsGroup);\n }\n\n return ret;\n}\n\n//============================================================\n// L2DPose # updateParam()\n//============================================================\nL2DPose.prototype.updateParam = function (model/*ALive2DModel*/) {\n if (model == null) return;\n\n if (!(model == this.lastModel)) {\n this.initParam(model);\n }\n this.lastModel = model;\n\n var curTime = UtSystem.getUserTimeMSec();\n var deltaTimeSec = ((this.lastTime == 0) ? 0 : (curTime - this.lastTime) / 1000.0);\n this.lastTime = curTime;\n if (deltaTimeSec < 0) deltaTimeSec = 0;\n for (var i = 0; i < this.partsGroups.length; i++) {\n this.normalizePartsOpacityGroup(model, this.partsGroups[i], deltaTimeSec);\n this.copyOpacityOtherParts(model, this.partsGroups[i]);\n }\n}\n\n//============================================================\n// L2DPose # initParam()\n//============================================================\nL2DPose.prototype.initParam = function (model/*ALive2DModel*/) {\n if (model == null) return;\n for (var i = 0; i < this.partsGroups.length; i++) {\n var partsGroup = this.partsGroups[i]; //L2DPartsParam\n for (var j = 0; j < partsGroup.length; j++) {\n partsGroup[j].initIndex(model);\n var partsIndex = partsGroup[j].partsIndex;\n var paramIndex = partsGroup[j].paramIndex;\n if (partsIndex < 0) continue;\n var v/*:Boolean*/ = (model.getParamFloat(paramIndex) != 0);\n model.setPartsOpacity(partsIndex, (v ? 1.0 : 0.0));\n model.setParamFloat(paramIndex, (v ? 1.0 : 0.0));\n if (partsGroup[j].link == null) continue;\n for (var k = 0; k < partsGroup[j].link.length; k++) {\n partsGroup[j].link[k].initIndex(model);\n }\n }\n }\n}\n\n//============================================================\n// L2DPose # normalizePartsOpacityGroup()\n//============================================================\nL2DPose.prototype.normalizePartsOpacityGroup = function (model/*ALive2DModel*/, partsGroup/*L2DPartsParam[]*/, deltaTimeSec/*float*/) {\n var visibleParts = -1;\n var visibleOpacity = 1.0;\n var CLEAR_TIME_SEC = 0.5;\n var phi = 0.5;\n var maxBackOpacity = 0.15;\n for (var i = 0; i < partsGroup.length; i++) {\n var partsIndex = partsGroup[i].partsIndex;\n var paramIndex = partsGroup[i].paramIndex;\n if (partsIndex < 0) continue; if (model.getParamFloat(paramIndex) != 0) {\n if (visibleParts >= 0) {\n break;\n }\n visibleParts = i;\n visibleOpacity = model.getPartsOpacity(partsIndex);\n visibleOpacity += deltaTimeSec / CLEAR_TIME_SEC;\n if (visibleOpacity > 1) {\n visibleOpacity = 1;\n }\n }\n }\n if (visibleParts < 0) {\n visibleParts = 0;\n visibleOpacity = 1;\n }\n for (var i = 0; i < partsGroup.length; i++) {\n var partsIndex = partsGroup[i].partsIndex;\n if (partsIndex < 0) continue; if (visibleParts == i) {\n model.setPartsOpacity(partsIndex, visibleOpacity);\n }\n else {\n var opacity = model.getPartsOpacity(partsIndex);\n var a1;\n if (visibleOpacity < phi) {\n a1 = visibleOpacity * (phi - 1) / phi + 1;\n }\n else {\n a1 = (1 - visibleOpacity) * phi / (1 - phi);\n }\n var backOp = (1 - a1) * (1 - visibleOpacity);\n if (backOp > maxBackOpacity) {\n a1 = 1 - maxBackOpacity / (1 - visibleOpacity);\n }\n if (opacity > a1) {\n opacity = a1;\n }\n model.setPartsOpacity(partsIndex, opacity);\n }\n }\n}\n\n//============================================================\n// L2DPose # copyOpacityOtherParts()\n//============================================================\nL2DPose.prototype.copyOpacityOtherParts = function (model/*ALive2DModel*/, partsGroup/*L2DPartsParam[]*/) {\n for (var i_group = 0; i_group < partsGroup.length; i_group++) {\n var partsParam = partsGroup[i_group]; //L2DPartsParam\n if (partsParam.link == null) continue;\n if (partsParam.partsIndex < 0) continue;\n var opacity = model.getPartsOpacity(partsParam.partsIndex);\n for (var i_link = 0; i_link < partsParam.link.length; i_link++) {\n var linkParts = partsParam.link[i_link]; //L2DPartsParam\n if (linkParts.partsIndex < 0) continue;\n model.setPartsOpacity(linkParts.partsIndex, opacity);\n }\n }\n}\n\n//============================================================\n//============================================================\n// class L2DPartsParam\n//============================================================\n//============================================================\nfunction L2DPartsParam(id/*String*/) {\n this.paramIndex = -1;\n this.partsIndex = -1;\n this.link = null; // ArrayList\n this.id = id;\n}\n\n//============================================================\n// L2DPartsParam # initIndex()\n//============================================================\nL2DPartsParam.prototype.initIndex = function (model/*ALive2DModel*/) {\n this.paramIndex = model.getParamIndex(\"VISIBLE:\" + this.id);\n this.partsIndex = model.getPartsDataIndex(PartsDataID.getID(this.id));\n model.setParamFloat(this.paramIndex, 1);\n}\n\n//============================================================\n//============================================================\n// class L2DTargetPoint\n//============================================================\n//============================================================\nfunction L2DTargetPoint() {\n this.EPSILON = 0.01; // 変化の最小値(この値以下は無視される)\n this.faceTargetX = 0;\n this.faceTargetY = 0;\n this.faceX = 0;\n this.faceY = 0;\n this.faceVX = 0;\n this.faceVY = 0;\n this.lastTimeSec = 0;\n}\n\n//============================================================\nL2DTargetPoint.FRAME_RATE = 60;\n\n//============================================================\n// L2DTargetPoint # set()\n//============================================================\nL2DTargetPoint.prototype.setPoint = function (x/*float*/, y/*float*/) {\n this.faceTargetX = x;\n this.faceTargetY = y;\n}\n\n//============================================================\n// L2DTargetPoint # getX()\n//============================================================\nL2DTargetPoint.prototype.getX = function () {\n return this.faceX;\n}\n\n//============================================================\n// L2DTargetPoint # getY()\n//============================================================\nL2DTargetPoint.prototype.getY = function () {\n return this.faceY;\n}\n\n//============================================================\n// L2DTargetPoint # update()\n//============================================================\nL2DTargetPoint.prototype.update = function () {\n var TIME_TO_MAX_SPEED = 0.15;\n var FACE_PARAM_MAX_V = 40.0 / 7.5;\n var MAX_V = FACE_PARAM_MAX_V / L2DTargetPoint.FRAME_RATE;\n if (this.lastTimeSec == 0) {\n this.lastTimeSec = UtSystem.getUserTimeMSec();\n return;\n }\n var curTimeSec = UtSystem.getUserTimeMSec();\n var deltaTimeWeight = (curTimeSec - this.lastTimeSec) * L2DTargetPoint.FRAME_RATE / 1000.0;\n this.lastTimeSec = curTimeSec;\n var FRAME_TO_MAX_SPEED = TIME_TO_MAX_SPEED * L2DTargetPoint.FRAME_RATE;\n var MAX_A = deltaTimeWeight * MAX_V / FRAME_TO_MAX_SPEED;\n var dx = (this.faceTargetX - this.faceX);\n var dy = (this.faceTargetY - this.faceY);\n // if(dx == 0 && dy == 0) return;\n if (Math.abs(dx) <= this.EPSILON && Math.abs(dy) <= this.EPSILON) return;\n var d = Math.sqrt(dx * dx + dy * dy);\n var vx = MAX_V * dx / d;\n var vy = MAX_V * dy / d;\n var ax = vx - this.faceVX;\n var ay = vy - this.faceVY;\n var a = Math.sqrt(ax * ax + ay * ay);\n if (a < -MAX_A || a > MAX_A) {\n ax *= MAX_A / a;\n ay *= MAX_A / a;\n a = MAX_A;\n }\n this.faceVX += ax;\n this.faceVY += ay;\n {\n var max_v = 0.5 * (Math.sqrt(MAX_A * MAX_A + 16 * MAX_A * d - 8 * MAX_A * d) - MAX_A);\n var cur_v = Math.sqrt(this.faceVX * this.faceVX + this.faceVY * this.faceVY);\n if (cur_v > max_v) {\n this.faceVX *= max_v / cur_v;\n this.faceVY *= max_v / cur_v;\n }\n }\n this.faceX += this.faceVX;\n this.faceY += this.faceVY;\n}\n\n//============================================================\n//============================================================\n// class L2DViewMatrix extends L2DMatrix44\n//============================================================\n//============================================================\nfunction L2DViewMatrix() {\n L2DMatrix44.prototype.constructor.call(this);\n this.screenLeft = null;\n this.screenRight = null;\n this.screenTop = null;\n this.screenBottom = null;\n this.maxLeft = null;\n this.maxRight = null;\n this.maxTop = null;\n this.maxBottom = null;\n}\n\nL2DViewMatrix.prototype = new L2DMatrix44(); //L2DViewMatrix extends L2DMatrix44\n\n//============================================================\n// L2DViewMatrix # adjustTranslate()\n//============================================================\nL2DViewMatrix.prototype.adjustTranslate = function (shiftX/*float*/, shiftY/*float*/) {\n if (this.tr[0] * this.maxLeft + (this.tr[12] + shiftX) > this.screenLeft)\n shiftX = this.screenLeft - this.tr[0] * this.maxLeft - this.tr[12];\n if (this.tr[0] * this.maxRight + (this.tr[12] + shiftX) < this.screenRight)\n shiftX = this.screenRight - this.tr[0] * this.maxRight - this.tr[12];\n if (this.tr[5] * this.maxTop + (this.tr[13] + shiftY) < this.screenTop)\n shiftY = this.screenTop - this.tr[5] * this.maxTop - this.tr[13];\n if (this.tr[5] * this.maxBottom + (this.tr[13] + shiftY) > this.screenBottom)\n shiftY = this.screenBottom - this.tr[5] * this.maxBottom - this.tr[13];\n\n var tr1 = [1, 0, 0, 0,\n 0, 1, 0, 0,\n 0, 0, 1, 0,\n shiftX, shiftY, 0, 1];\n L2DMatrix44.mul(tr1, this.tr, this.tr);\n}\n\n//============================================================\n// L2DViewMatrix # adjustScale()\n//============================================================\nL2DViewMatrix.prototype.adjustScale = function (cx/*float*/, cy/*float*/, scale/*float*/) {\n var targetScale = scale * this.tr[0];\n var tr1 = [1, 0, 0, 0,\n 0, 1, 0, 0,\n 0, 0, 1, 0,\n cx, cy, 0, 1];\n var tr2 = [scale, 0, 0, 0,\n 0, scale, 0, 0,\n 0, 0, 1, 0,\n 0, 0, 0, 1];\n var tr3 = [1, 0, 0, 0,\n 0, 1, 0, 0,\n 0, 0, 1, 0,\n -cx, -cy, 0, 1];\n L2DMatrix44.mul(tr3, this.tr, this.tr);\n L2DMatrix44.mul(tr2, this.tr, this.tr);\n L2DMatrix44.mul(tr1, this.tr, this.tr);\n}\n\n//============================================================\n// L2DViewMatrix # setScreenRect()\n//============================================================\nL2DViewMatrix.prototype.setScreenRect = function (left/*float*/, right/*float*/, bottom/*float*/, top/*float*/) {\n this.screenLeft = left;\n this.screenRight = right;\n this.screenTop = top;\n this.screenBottom = bottom;\n}\n\n//============================================================\n// L2DViewMatrix # setMaxScreenRect()\n//============================================================\nL2DViewMatrix.prototype.setMaxScreenRect = function (left/*float*/, right/*float*/, bottom/*float*/, top/*float*/) {\n this.maxLeft = left;\n this.maxRight = right;\n this.maxTop = top;\n this.maxBottom = bottom;\n}\n\n//============================================================\n// L2DViewMatrix # getScreenLeft()\n//============================================================\nL2DViewMatrix.prototype.getScreenLeft = function () {\n return this.screenLeft;\n}\n\n//============================================================\n// L2DViewMatrix # getScreenRight()\n//============================================================\nL2DViewMatrix.prototype.getScreenRight = function () {\n return this.screenRight;\n}\n\n//============================================================\n// L2DViewMatrix # getScreenBottom()\n//============================================================\nL2DViewMatrix.prototype.getScreenBottom = function () {\n return this.screenBottom;\n}\n\n//============================================================\n// L2DViewMatrix # getScreenTop()\n//============================================================\nL2DViewMatrix.prototype.getScreenTop = function () {\n return this.screenTop;\n}\n\n//============================================================\n// L2DViewMatrix # getMaxLeft()\n//============================================================\nL2DViewMatrix.prototype.getMaxLeft = function () {\n return this.maxLeft;\n}\n\n//============================================================\n// L2DViewMatrix # getMaxRight()\n//============================================================\nL2DViewMatrix.prototype.getMaxRight = function () {\n return this.maxRight;\n}\n\n//============================================================\n// L2DViewMatrix # getMaxBottom()\n//============================================================\nL2DViewMatrix.prototype.getMaxBottom = function () {\n return this.maxBottom;\n}\n\n//============================================================\n// L2DViewMatrix # getMaxTop()\n//============================================================\nL2DViewMatrix.prototype.getMaxTop = function () {\n return this.maxTop;\n}\n\n//============================================================\n//============================================================\n// class Live2DFramework\n//============================================================\n//============================================================\nfunction Live2DFramework() {\n}\n\n//============================================================\nLive2DFramework.platformManager = null;\n\n//============================================================\n// static Live2DFramework.getPlatformManager()\n//============================================================\nLive2DFramework.getPlatformManager = function () {\n return Live2DFramework.platformManager;\n}\n\n//============================================================\n// static Live2DFramework.setPlatformManager()\n//============================================================\nLive2DFramework.setPlatformManager = function (platformManager /*IPlatformManager*/) {\n Live2DFramework.platformManager = platformManager;\n}\n\nexport{\n L2DTargetPoint,\n Live2DFramework,\n L2DViewMatrix,\n L2DPose,\n L2DPartsParam,\n L2DPhysics,\n L2DMotionManager,\n L2DModelMatrix,\n L2DMatrix44,\n EYE_STATE,\n L2DEyeBlink,\n L2DExpressionParam,\n L2DExpressionMotion,\n L2DBaseModel,\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/lib/Live2DFramework.js","// Modified by xiazeyu.\n\n/**\n* @desc The definitions of values releated to model react\n*/\n\nexport const cDefine = {\n // above are viewMatrix value settings\n VIEW_LOGICAL_LEFT : -1, // -1, the left abscissa of viewMatrix\n VIEW_LOGICAL_RIGHT : 1, // 1, the right abscissa of viewMatrix\n VIEW_LOGICAL_MAX_LEFT : -2, // -2, the max left abscissa of viewMatrix\n VIEW_LOGICAL_MAX_RIGHT : 2, // 2, the max right abscissa of viewMatrix\n VIEW_LOGICAL_MAX_BOTTOM : -2, // -2, the max bottom abscissa of viewMatrix\n VIEW_LOGICAL_MAX_TOP : 2, // 2, the max top abscissa of viewMatrix\n\n // above are the motions priority settings.\n PRIORITY_NONE : 0, // 0,do nothing\n PRIORITY_IDLE : 1, // 1, idle motions\n PRIORITY_NORMAL : 2, // 2, normal motions\n PRIORITY_FORCE : 3, // 3, force to show motion\n\n // above are the index to the motions in model.json\n // #43\n MOTION_GROUP_IDLE : \"idle\",\n MOTION_GROUP_TAP_BODY : \"tap_body\",\n MOTION_GROUP_FLICK_HEAD : \"flick_head\", // unused\n MOTION_GROUP_PINCH_IN : \"pinch_in\", // unused\n MOTION_GROUP_PINCH_OUT : \"pinch_out\", // unused\n MOTION_GROUP_SHAKE : \"shake\", // unused\n\n // above are the index to the hit areas in model.json\n // #43\n HIT_AREA_HEAD : \"head\",\n HIT_AREA_BODY : \"body\"\n};\n\n\n\n// WEBPACK FOOTER //\n// ./src/cDefine.js","/**\n * @description The container and manager for all the DOM and WebGL emelents.\n */\n\n\nimport { config } from './config/configMgr';\nimport { L2Dwidget } from './index';\nimport { createDialogElement } from './dialog';\n\n/**\n * The current WebGL element\n * @type {RenderingContext}\n */\n\nlet currWebGL = undefined;\n\n/**\n * The current canvas element\n * @type {HTMLElement}\n */\n\nlet currCanvas;\n\n\n/**\n * Create the canvas and styles using DOM\n * @return {null}\n */\n\nfunction createElement() {\n\n let e = document.getElementById(config.name.div)\n if (e !== null) {\n document.body.removeChild(e);\n }\n\n let newElem = document.createElement('div');\n newElem.id = config.name.div;\n newElem.className = 'live2d-widget-container';\n newElem.style.setProperty('position', 'fixed');\n newElem.style.setProperty(config.display.position, config.display.hOffset + 'px');\n newElem.style.setProperty('bottom', config.display.vOffset + 'px');\n newElem.style.setProperty('width', config.display.width + 'px');\n newElem.style.setProperty('height', config.display.height + 'px');\n newElem.style.setProperty('z-index', 99999);\n newElem.style.setProperty('opacity', config.react.opacity);\n newElem.style.setProperty('pointer-events', 'none');\n document.body.appendChild(newElem);\n L2Dwidget.emit('create-container', newElem);\n\n if (config.dialog.enable)\n createDialogElement(newElem);\n\n let newCanvasElem = document.createElement('canvas');\n newCanvasElem.setAttribute('id', config.name.canvas);\n newCanvasElem.setAttribute('width', config.display.width * config.display.superSample);\n newCanvasElem.setAttribute('height', config.display.height * config.display.superSample);\n newCanvasElem.style.setProperty('position', 'absolute');\n newCanvasElem.style.setProperty('left', '0px');\n newCanvasElem.style.setProperty('top', '0px');\n newCanvasElem.style.setProperty('width', config.display.width + 'px');\n newCanvasElem.style.setProperty('height', config.display.height + 'px');\n if (config.dev.border) newCanvasElem.style.setProperty('border', 'dashed 1px #CCC');\n newElem.appendChild(newCanvasElem);\n\n currCanvas = document.getElementById(config.name.canvas);\n L2Dwidget.emit('create-canvas', newCanvasElem);\n\n initWebGL();\n\n}\n\n/**\n * Find and set the current WebGL element to the container\n * @return {null}\n */\n\nfunction initWebGL() {\n\n var NAMES = ['webgl2', 'webgl', 'experimental-webgl2', 'experimental-webgl', 'webkit-3d', 'moz-webgl'];\n for (let i = 0; i < NAMES.length; i++) {\n try {\n let ctx = currCanvas.getContext(NAMES[i], {\n alpha: true,\n antialias: true,\n premultipliedAlpha: true,\n failIfMajorPerformanceCaveat: false,\n });\n if (ctx) currWebGL = ctx;\n } catch (e) { }\n }\n if (!currWebGL) {\n console.error('Live2D widgets: Failed to create WebGL context.');\n if (!window.WebGLRenderingContext) {\n console.error('Your browser may not support WebGL, check https://get.webgl.org/ for futher information.');\n }\n return;\n }\n};\n\n\nexport {\n createElement,\n currWebGL,\n currCanvas,\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/elementMgr.js","/**\n *\n * You can modify and use this source freely\n * only for the development of application related Live2D.\n *\n * (c) Live2D Inc. All rights reserved.\n */\n\n/**\n * EYHN 修改\n *\n * Copyright © 2016 - 2017 EYHN\n */\n\n// Modified by xiazeyu.\n\n/**\n* @desc A matrix stack releated to draw the model\n*/\n\nexport function MatrixStack() {}\n\nMatrixStack.matrixStack = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];\nMatrixStack.depth = 0;\nMatrixStack.currentMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];\nMatrixStack.tmp = new Array(16);\n\n/**\n* @name reset\n* @desc reset the stack\n* @param null\n* @returns null\n* @memberOf MatrixStack\n*/\nMatrixStack.reset = function(){\n this.depth = 0;\n}\n\n/**\n* @name loadIdentity\n* @desc reset values in the stack to whether it can be divisible by 5\n* @param null\n* @returns null\n* @memberOf MatrixStack\n*/\nMatrixStack.loadIdentity = function(){\n var thisRef = this;\n for (var i = 0; i < 16; i++){\n thisRef.currentMatrix[i] = (i % 5 == 0) ? 1 : 0;\n }\n}\n\n/**\n* @name push\n* @desc push a new element into the stack\n* @param null\n* @returns null\n* @memberOf MatrixStack\n*/\nMatrixStack.push = function(){\n var thisRef = this;\n // var offset = thisRef.depth * 16;\n var nextOffset = (thisRef.depth + 1) * 16;\n\n if (thisRef.matrixStack.length < nextOffset + 16){\n thisRef.matrixStack.length = nextOffset + 16;\n }\n\n for (var i = 0; i < 16; i++){\n thisRef.matrixStack[nextOffset + i] = thisRef.currentMatrix[i];\n }\n\n thisRef.depth++;\n}\n\n/**\n* @name pop\n* @desc pop an element from the stack\n* @param null\n* @returns null\n* @memberOf MatrixStack\n*/\nMatrixStack.pop = function(){\n var thisRef = this;\n thisRef.depth--;\n if (thisRef.depth < 0){ // stack is underflow?????\n myError(\"Invalid matrix stack.\");\n thisRef.depth = 0;\n }\n\n var offset = thisRef.depth * 16;\n for (var i = 0; i < 16; i++){\n thisRef.currentMatrix[i] = thisRef.matrixStack[offset + i];\n }\n}\n\n/**\n* @name getMatrix\n* @desc return the current matrix stack\n* @param null\n* @returns {Array} current matrix stack\n* @memberOf MatrixStack\n*/\nMatrixStack.getMatrix = function(){\n return this.currentMatrix;\n}\n\n/**\n* @name multMatrix\n* @desc matrix multiplication, save to the currentMatrix\n* @param null\n* @returns null\n* @memberOf MatrixStack\n*/\nMatrixStack.multMatrix = function(matNew)\n{\n var thisRef = this;\n var i, j, k;\n\n for (i = 0; i < 16; i++){\n thisRef.tmp[i] = 0;\n }\n\n for (i = 0; i < 4; i++){\n for (j = 0; j < 4; j++){\n for (k = 0; k < 4; k++){\n thisRef.tmp[i + j * 4] += thisRef.currentMatrix[i + k * 4] * matNew[k + j * 4];\n }\n }\n }\n for (i = 0; i < 16; i++){\n thisRef.currentMatrix[i] = thisRef.tmp[i];\n }\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/utils/MatrixStack.js","import { config } from '../config/configMgr';\nimport { L2Dwidget } from '../index';\n\ndocument.head.innerHTML += `\n\n`;\n\nlet containerElement,dialogElement,closeTimer;\n\n/**\n * 创建对话框元素\n * @param {HTMLElement} root 位置\n */\nfunction createDialogElement(root) {\n containerElement = document.createElement('div');\n containerElement.className = 'live2d-widget-dialog-container';\n containerElement.style.transform = `scale(${config.display.width / 250})`\n dialogElement = document.createElement('div');\n dialogElement.className = 'live2d-widget-dialog';\n containerElement.appendChild(dialogElement);\n root.appendChild(containerElement);\n\n L2Dwidget.emit('create-dialog', containerElement);\n\n if (config.dialog.hitokoto)\n showHitokotoLoop()\n}\n\nfunction displayDialog() {\n dialogElement.style.opacity = 1;\n}\n\nfunction hiddenDialog() {\n dialogElement.style.opacity = 0;\n}\n\nfunction alertText(text) {\n displayDialog();\n dialogElement.innerText = text;\n clearTimeout(closeTimer);\n closeTimer = setTimeout(function () {\n hiddenDialog();\n }, 5000);\n}\n\nfunction showHitokotoLoop() {\n var xhr = new XMLHttpRequest();\n xhr.open('get', 'https://v1.hitokoto.cn');\n xhr.setRequestHeader(\"Cache-Control\", \"no-cache\");\n xhr.onreadystatechange = function () {\n if (xhr.readyState === 4) {\n var data = JSON.parse(xhr.responseText);\n alertText(data.hitokoto);\n setTimeout(showHitokotoLoop, 10000)\n }\n }\n xhr.send();\n}\n\n\nmodule.exports = {\n createDialogElement, displayDialog, hiddenDialog, alertText, showHitokotoLoop\n}\n\n\n// WEBPACK FOOTER //\n// ./src/dialog/index.js","// Provide a \"System\" global.\r\nmodule.exports = {\r\n\t// Make sure import is only used as \"System.import\"\r\n\timport: function() {\r\n\t\tthrow new Error(\"System.import cannot be used indirectly\");\r\n\t}\r\n};\r\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// (webpack)/buildin/system.js\n// module id = 83\n// module chunks = 0","import { Live2DFramework } from \"./lib/Live2DFramework\";\nimport { PlatformManager } from \"./PlatformManager\";\nimport { cModel } from \"./cModel\";\nimport { cDefine } from \"./cDefine\";\n\nfunction cManager(eventemitter) {\n // console.log(\"--> cManager()\");\n\n this.eventemitter = eventemitter;\n\n this.models = [];\n this.count = -1;\n this.reloadFlg = false;\n\n Live2DFramework.setPlatformManager(new PlatformManager());\n\n}\n\ncManager.prototype.createModel = function () {\n\n var model = new cModel();\n this.models.push(model);\n\n return model;\n\n}\n\n\ncManager.prototype.changeModel = function (gl, modelurl) {\n // console.log(\"--> cManager.update(gl)\");\n\n if (this.reloadFlg) {\n this.reloadFlg = false;\n this.releaseModel(0, gl);\n this.createModel();\n this.models[0].load(gl, modelurl);\n }\n\n};\n\n\ncManager.prototype.getModel = function (no) {\n // console.log(\"--> cManager.getModel(\" + no + \")\");\n\n if (no >= this.models.length) return null;\n\n return this.models[no];\n};\n\n\n\ncManager.prototype.releaseModel = function (no, gl) {\n // console.log(\"--> cManager.releaseModel(\" + no + \")\");\n\n if (this.models.length <= no) return;\n\n this.models[no].release(gl);\n\n delete this.models[no];\n this.models.splice(no, 1);\n};\n\n\n\ncManager.prototype.numModels = function () {\n return this.models.length;\n};\n\n\n\ncManager.prototype.setDrag = function (x, y) {\n for (var i = 0; i < this.models.length; i++) {\n this.models[i].setDrag(x, y);\n }\n}\n\ncManager.prototype.tapEvent = function (x, y) {\n if (cDefine.DEBUG_LOG)\n console.log(\"tapEvent view x:\" + x + \" y:\" + y);\n\n for (var i = 0; i < this.models.length; i++) {\n\n if (this.models[i].hitTest(cDefine.HIT_AREA_HEAD, x, y)) {\n this.eventemitter.emit('tapface');\n \n if (cDefine.DEBUG_LOG)\n console.log(\"Tap face.\");\n\n this.models[i].setRandomExpression();\n }\n else if (this.models[i].hitTest(cDefine.HIT_AREA_BODY, x, y)) {\n this.eventemitter.emit('tapbody');\n if (cDefine.DEBUG_LOG)\n console.log(\"Tap body.\" + \" models[\" + i + \"]\");\n\n this.models[i].startRandomMotion(cDefine.MOTION_GROUP_TAP_BODY,\n cDefine.PRIORITY_NORMAL);\n }\n }\n\n return true;\n};\n\nexport{\n cManager,\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/cManager.js","\n/**\n *\n * You can modify and use this source freely\n * only for the development of application related Live2D.\n *\n * (c) Live2D Inc. All rights reserved.\n */\n\n// Modified by xiazeyu.\n\n/**\n* @desc A library that provide basic IO and json function\n*/\n\nimport { currWebGL } from './elementMgr';\nimport { Live2DModelWebGL } from \"./lib/live2d.core\";\n\n\n//============================================================\n//============================================================\n// class PlatformManager extend IPlatformManager\n//============================================================\n//============================================================\n\n/**\n* @name PlatformManager\n* @desc Define the variable type of PlatformManager\n* @param null\n* @returns {Structure} PlatformManager\n*/\nexport function PlatformManager()\n{\n\n}\n\n\n//============================================================\n// PlatformManager # loadBytes()\n//============================================================\n\n/**\n* @name loadBytes\n* @desc load bytes from the path and callback\n* @param {String} path, {Function} callback\n* @returns callback {raw} context\n* @memberOf PlatformManager\n*/\n\nPlatformManager.prototype.loadBytes = function(path/*String*/, callback)\n{\n var request = new XMLHttpRequest();\n request.open(\"GET\", path, true);\n request.responseType = \"arraybuffer\";\n request.onload = function(){\n switch(request.status){\n case 200:\n callback(request.response);\n break;\n default:\n console.error(\"Failed to load (\" + request.status + \") : \" + path);\n break;\n }\n }\n request.send(null);\n // return request;\n}\n\n\n//============================================================\n// PlatformManager # loadString()\n//============================================================\n\n/**\n* @name loadString\n* @desc load bytes from the path and put it into buffer\n* @param {String} path\n* @returns buffer {raw} context\n* @memberOf PlatformManager\n*/\nPlatformManager.prototype.loadString = function(path/*String*/)\n{\n\n this.loadBytes(path, function(buf) {\n return buf;\n });\n\n}\n\n\n//============================================================\n// PlatformManager # loadLive2DModel()\n//============================================================\n\n/**\n* @name loadLive2DModel\n* @desc load Live2DModel from the path and put it into buffer\n* @param {String} path, {function} callback\n* @returns callback loaded model\n* @memberOf PlatformManager\n*/\nPlatformManager.prototype.loadLive2DModel = function(path/*String*/, callback)\n{\n var model = null;\n\n // load moc\n this.loadBytes(path, function(buf){\n model = Live2DModelWebGL.loadModel(buf);\n callback(model);\n });\n\n}\n\n\n//============================================================\n// PlatformManager # loadTexture()\n//============================================================\n\n/**\n* @name loadTexture\n* @desc load Live2DModel's Texture and callback\n* @param {Live2DModelWebGL}model, {int}no, {string}path, {function}callback\n* @returns callback\n* @memberOf PlatformManager\n*/\nPlatformManager.prototype.loadTexture = function(model/*ALive2DModel*/, no/*int*/, path/*String*/, callback)\n{\n // load textures\n var loadedImage = new Image();\n // Thanks to @mashirozx & @fghrsh\n // Issues:\n // @https://github.com/journey-ad/live2d_src/issues/1\n // @https://github.com/journey-ad/live2d_src/issues/3\n loadedImage.crossOrigin = 'Anonymous';\n loadedImage.src = path;\n loadedImage.onload = onload;\n loadedImage.onerror = onerror;\n\n // var thisRef = this;\n loadedImage.onload = function() {\n // create texture\n var gl = currWebGL;\n var texture = gl.createTexture();\n if (!texture){ console.error(\"Failed to generate gl texture name.\"); return -1; }\n\n if(!model.isPremultipliedAlpha()){\n // 乗算済アルファテクスチャ以外の場合\n // emmmm, maybe do something for textures with alpha layer.\n gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1);\n }\n gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, texture);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,\n gl.UNSIGNED_BYTE, loadedImage);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);\n gl.generateMipmap(gl.TEXTURE_2D);\n\n\n\n model.setTexture(no, texture);\n\n // テクスチャオブジェクトを解放\n // Release the texture object to prevent buffer overruns.\n texture = null;\n\n if (typeof callback == \"function\") callback();\n };\n\n loadedImage.onerror = function() {\n console.error(\"Failed to load image : \" + path);\n }\n}\n\n\n//============================================================\n// PlatformManager # parseFromBytes(buf)\n\n//============================================================\n\n/**\n* @name jsonParseFromBytes\n* @desc parse json file into arrays\n* @param {raw} buf\n* @returns {Array}jsonObj\n* @memberOf PlatformManager\n*/\nPlatformManager.prototype.jsonParseFromBytes = function(buf){\n\n var jsonStr;\n var bomCode = new Uint8Array(buf, 0, 3);\n if (bomCode[0] == 239 && bomCode[1] == 187 && bomCode[2] == 191) {\n jsonStr = String.fromCharCode.apply(null, new Uint8Array(buf, 3));\n } else {\n jsonStr = String.fromCharCode.apply(null, new Uint8Array(buf));\n }\n\n var jsonObj = JSON.parse(jsonStr);\n\n return jsonObj;\n};\n\n\n\n//============================================================\n// PlatformManager # log()\n//============================================================\n\n/**\n* @name log\n* @desc output log in console\n* @param {string} txt\n* @returns null\n* @memberOf PlatformManager\n*/\nPlatformManager.prototype.log = function(txt/*String*/)\n{\n console.log(txt);\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/PlatformManager.js","import { Live2DFramework, L2DBaseModel, L2DEyeBlink } from \"./lib/Live2DFramework\";\nimport { ModelSettingJson } from \"./utils/ModelSettingJson\";\nimport { MatrixStack } from \"./utils/MatrixStack\";\nimport { cDefine } from \"./cDefine\";\nimport { UtSystem,/*\n UtDebug,\n LDTransform,\n LDGL,\n Live2D,\n Live2DModelWebGL,\n Live2DModelJS,\n Live2DMotion,\n MotionQueueManager,\n PhysicsHair,\n AMotion,\n PartsDataID,\n DrawDataID,\n BaseDataID,\n ParamID*/ } from './lib/live2d.core';\n//============================================================\n//============================================================\n// class cModel extends L2DBaseModel\n//============================================================\n//============================================================\nexport function cModel()\n{\n //L2DBaseModel.apply(this, arguments);\n L2DBaseModel.prototype.constructor.call(this);\n\n this.modelHomeDir = \"\";\n this.modelSetting = null;\n this.tmpMatrix = [];\n}\n\ncModel.prototype = new L2DBaseModel();\n\n\ncModel.prototype.load = function(gl, modelSettingPath, callback)\n{\n this.setUpdating(true);\n this.setInitialized(false);\n\n this.modelHomeDir = modelSettingPath.substring(0, modelSettingPath.lastIndexOf(\"/\") + 1);\n\n this.modelSetting = new ModelSettingJson();\n\n var thisRef = this;\n\n this.modelSetting.loadModelSetting(modelSettingPath, function(){\n\n var path = thisRef.modelHomeDir + thisRef.modelSetting.getModelFile();\n thisRef.loadModelData(path, function(model){\n\n for (var i = 0; i < thisRef.modelSetting.getTextureNum(); i++)\n {\n if( /^https?:\\/\\/|^\\/\\//i.test(thisRef.modelSetting.getTextureFile(i)) ){\n\n var texPaths = thisRef.modelSetting.getTextureFile(i);\n\n }else{\n var texPaths = thisRef.modelHomeDir + thisRef.modelSetting.getTextureFile(i);\n }\n thisRef.loadTexture(i, texPaths, function() {\n\n if( thisRef.isTexLoaded ) {\n\n if (thisRef.modelSetting.getExpressionNum() > 0)\n {\n\n thisRef.expressions = {};\n\n for (var j = 0; j < thisRef.modelSetting.getExpressionNum(); j++)\n {\n var expName = thisRef.modelSetting.getExpressionName(j);\n var expFilePath = thisRef.modelHomeDir +\n thisRef.modelSetting.getExpressionFile(j);\n\n thisRef.loadExpression(expName, expFilePath);\n }\n }\n else\n {\n thisRef.expressionManager = null;\n thisRef.expressions = {};\n }\n\n\n\n if (thisRef.eyeBlink == null)\n {\n thisRef.eyeBlink = new L2DEyeBlink();\n }\n\n\n if (thisRef.modelSetting.getPhysicsFile() != null)\n {\n thisRef.loadPhysics(thisRef.modelHomeDir +\n thisRef.modelSetting.getPhysicsFile());\n }\n else\n {\n thisRef.physics = null;\n }\n\n\n\n if (thisRef.modelSetting.getPoseFile() != null)\n {\n thisRef.loadPose(\n thisRef.modelHomeDir +\n thisRef.modelSetting.getPoseFile(),\n function() {\n thisRef.pose.updateParam(thisRef.live2DModel);\n }\n );\n }\n else\n {\n thisRef.pose = null;\n }\n\n\n\n if (thisRef.modelSetting.getLayout() != null)\n {\n var layout = thisRef.modelSetting.getLayout();\n if (layout[\"width\"] != null)\n thisRef.modelMatrix.setWidth(layout[\"width\"]);\n if (layout[\"height\"] != null)\n thisRef.modelMatrix.setHeight(layout[\"height\"]);\n\n if (layout[\"x\"] != null)\n thisRef.modelMatrix.setX(layout[\"x\"]);\n if (layout[\"y\"] != null)\n thisRef.modelMatrix.setY(layout[\"y\"]);\n if (layout[\"center_x\"] != null)\n thisRef.modelMatrix.centerX(layout[\"center_x\"]);\n if (layout[\"center_y\"] != null)\n thisRef.modelMatrix.centerY(layout[\"center_y\"]);\n if (layout[\"top\"] != null)\n thisRef.modelMatrix.top(layout[\"top\"]);\n if (layout[\"bottom\"] != null)\n thisRef.modelMatrix.bottom(layout[\"bottom\"]);\n if (layout[\"left\"] != null)\n thisRef.modelMatrix.left(layout[\"left\"]);\n if (layout[\"right\"] != null)\n thisRef.modelMatrix.right(layout[\"right\"]);\n }\n\n for (var j = 0; j < thisRef.modelSetting.getInitParamNum(); j++)\n {\n\n thisRef.live2DModel.setParamFloat(\n thisRef.modelSetting.getInitParamID(j),\n thisRef.modelSetting.getInitParamValue(j)\n );\n }\n\n for (var j = 0; j < thisRef.modelSetting.getInitPartsVisibleNum(); j++)\n {\n\n thisRef.live2DModel.setPartsOpacity(\n thisRef.modelSetting.getInitPartsVisibleID(j),\n thisRef.modelSetting.getInitPartsVisibleValue(j)\n );\n }\n\n\n\n thisRef.live2DModel.saveParam();\n // thisRef.live2DModel.setGL(gl);\n\n\n thisRef.preloadMotionGroup(cDefine.MOTION_GROUP_IDLE);\n thisRef.mainMotionManager.stopAllMotions();\n\n thisRef.setUpdating(false);\n thisRef.setInitialized(true);\n\n if (typeof callback == \"function\") callback();\n\n }\n });\n }\n });\n });\n};\n\n\n\ncModel.prototype.release = function(gl)\n{\n // this.live2DModel.deleteTextures();\n var pm = Live2DFramework.getPlatformManager();\n\n gl.deleteTexture(pm.texture);\n}\n\n\n\ncModel.prototype.preloadMotionGroup = function(name)\n{\n var thisRef = this;\n\n for (var i = 0; i < this.modelSetting.getMotionNum(name); i++)\n {\n var file = this.modelSetting.getMotionFile(name, i);\n this.loadMotion(file, this.modelHomeDir + file, function(motion) {\n motion.setFadeIn(thisRef.modelSetting.getMotionFadeIn(name, i));\n motion.setFadeOut(thisRef.modelSetting.getMotionFadeOut(name, i));\n });\n\n }\n}\n\n\ncModel.prototype.update = function()\n{\n // console.log(\"--> cModel.update()\");\n\n if(this.live2DModel == null)\n {\n if (cDefine.DEBUG_LOG) console.error(\"Failed to update.\");\n\n return;\n }\n\n var timeMSec = UtSystem.getUserTimeMSec() - this.startTimeMSec;\n var timeSec = timeMSec / 1000.0;\n var t = timeSec * 2 * Math.PI;\n\n\n if (this.mainMotionManager.isFinished())\n {\n\n this.startRandomMotion(cDefine.MOTION_GROUP_IDLE, cDefine.PRIORITY_IDLE);\n }\n\n //-----------------------------------------------------------------\n\n\n this.live2DModel.loadParam();\n\n\n\n var update = this.mainMotionManager.updateParam(this.live2DModel);\n if (!update) {\n\n if(this.eyeBlink != null) {\n this.eyeBlink.updateParam(this.live2DModel);\n }\n }\n\n\n this.live2DModel.saveParam();\n\n //-----------------------------------------------------------------\n\n\n if (this.expressionManager != null &&\n this.expressions != null &&\n !this.expressionManager.isFinished())\n {\n this.expressionManager.updateParam(this.live2DModel);\n }\n\n\n\n this.live2DModel.addToParamFloat(\"PARAM_ANGLE_X\", this.dragX * 30, 1);\n this.live2DModel.addToParamFloat(\"PARAM_ANGLE_Y\", this.dragY * 30, 1);\n this.live2DModel.addToParamFloat(\"PARAM_ANGLE_Z\", (this.dragX * this.dragY) * -30, 1);\n\n\n\n this.live2DModel.addToParamFloat(\"PARAM_BODY_ANGLE_X\", this.dragX*10, 1);\n\n\n\n this.live2DModel.addToParamFloat(\"PARAM_EYE_BALL_X\", this.dragX, 1);\n this.live2DModel.addToParamFloat(\"PARAM_EYE_BALL_Y\", this.dragY, 1);\n\n\n\n this.live2DModel.addToParamFloat(\"PARAM_ANGLE_X\",\n Number((15 * Math.sin(t / 6.5345))), 0.5);\n this.live2DModel.addToParamFloat(\"PARAM_ANGLE_Y\",\n Number((8 * Math.sin(t / 3.5345))), 0.5);\n this.live2DModel.addToParamFloat(\"PARAM_ANGLE_Z\",\n Number((10 * Math.sin(t / 5.5345))), 0.5);\n this.live2DModel.addToParamFloat(\"PARAM_BODY_ANGLE_X\",\n Number((4 * Math.sin(t / 15.5345))), 0.5);\n this.live2DModel.setParamFloat(\"PARAM_BREATH\",\n Number((0.5 + 0.5 * Math.sin(t / 3.2345))), 1);\n\n\n if (this.physics != null)\n {\n this.physics.updateParam(this.live2DModel);\n }\n\n\n if (this.lipSync == null)\n {\n this.live2DModel.setParamFloat(\"PARAM_MOUTH_OPEN_Y\",\n this.lipSyncValue);\n }\n\n\n if( this.pose != null ) {\n this.pose.updateParam(this.live2DModel);\n }\n\n this.live2DModel.update();\n};\n\n\n\ncModel.prototype.setRandomExpression = function()\n{\n var tmp = [];\n for (var name in this.expressions)\n {\n tmp.push(name);\n }\n\n var no = parseInt(Math.random() * tmp.length);\n\n this.setExpression(tmp[no]);\n}\n\n\n\ncModel.prototype.startRandomMotion = function(name, priority)\n{\n var max = this.modelSetting.getMotionNum(name);\n var no = parseInt(Math.random() * max);\n this.startMotion(name, no, priority);\n}\n\n\n\ncModel.prototype.startMotion = function(name, no, priority)\n{\n // console.log(\"startMotion : \" + name + \" \" + no + \" \" + priority);\n\n var motionName = this.modelSetting.getMotionFile(name, no);\n\n if (motionName == null || motionName == \"\")\n {\n if (cDefine.DEBUG_LOG)\n console.error(\"Failed to motion.\");\n return;\n }\n\n if (priority == cDefine.PRIORITY_FORCE)\n {\n this.mainMotionManager.setReservePriority(priority);\n }\n else if (!this.mainMotionManager.reserveMotion(priority))\n {\n if (cDefine.DEBUG_LOG)\n console.log(\"Motion is running.\")\n return;\n }\n\n var thisRef = this;\n var motion;\n\n if (this.motions[name] == null)\n {\n this.loadMotion(name, this.modelHomeDir + motionName, function(mtn) {\n motion = mtn;\n\n\n thisRef.setFadeInFadeOut(name, no, priority, motion);\n \n });\n }\n else\n {\n motion = this.motions[name];\n\n\n thisRef.setFadeInFadeOut(name, no, priority, motion);\n }\n}\n\n\ncModel.prototype.setFadeInFadeOut = function(name, no, priority, motion)\n{\n var motionName = this.modelSetting.getMotionFile(name, no);\n\n motion.setFadeIn(this.modelSetting.getMotionFadeIn(name, no));\n motion.setFadeOut(this.modelSetting.getMotionFadeOut(name, no));\n\n\n if (cDefine.DEBUG_LOG)\n console.log(\"Start motion : \" + motionName);\n\n if (this.modelSetting.getMotionSound(name, no) == null)\n {\n this.mainMotionManager.startMotionPrio(motion, priority);\n }\n else\n {\n var soundName = this.modelSetting.getMotionSound(name, no);\n // var player = new Sound(this.modelHomeDir + soundName);\n\n var snd = document.createElement(\"audio\");\n snd.src = this.modelHomeDir + soundName;\n\n if (cDefine.DEBUG_LOG)\n console.log(\"Start sound : \" + soundName);\n\n snd.play();\n this.mainMotionManager.startMotionPrio(motion, priority);\n }\n}\n\n\n\ncModel.prototype.setExpression = function(name)\n{\n var motion = this.expressions[name];\n\n if (cDefine.DEBUG_LOG)\n console.log(\"Expression : \" + name);\n\n this.expressionManager.startMotion(motion, false);\n}\n\n\n\ncModel.prototype.draw = function(gl)\n{\n //console.log(\"--> cModel.draw()\");\n\n // if(this.live2DModel == null) return;\n\n\n MatrixStack.push();\n\n MatrixStack.multMatrix(this.modelMatrix.getArray());\n\n this.tmpMatrix = MatrixStack.getMatrix()\n this.live2DModel.setMatrix(this.tmpMatrix);\n this.live2DModel.draw();\n\n MatrixStack.pop();\n\n};\n\n\n\ncModel.prototype.hitTest = function(id, testX, testY)\n{\n var len = this.modelSetting.getHitAreaNum();\n for (var i = 0; i < len; i++)\n {\n if (id == this.modelSetting.getHitAreaName(i))\n {\n var drawID = this.modelSetting.getHitAreaID(i);\n\n return this.hitTestSimple(drawID, testX, testY);\n }\n }\n\n return false;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/cModel.js","// Modified by xiazeyu.\n\n/**\n* @desc To get the model settings from given json file\n*/\n\nimport { Live2DFramework } from \"../lib/Live2DFramework\"\n\n/**\n* @name ModelSettingJson\n* @desc return the struct of ModelSettingJson\n* @param null\n* @returns {Structure} ModelSettingJson\n*/\nexport function ModelSettingJson()\n{ // Define the index in the json file.\n this.NAME = \"name\";\n this.ID = \"id\";\n this.MODEL = \"model\";\n this.TEXTURES = \"textures\";\n this.HIT_AREAS = \"hit_areas\";\n this.PHYSICS = \"physics\";\n this.POSE = \"pose\";\n this.EXPRESSIONS = \"expressions\";\n this.MOTION_GROUPS = \"motions\";\n this.SOUND = \"sound\";\n this.FADE_IN = \"fade_in\";\n this.FADE_OUT = \"fade_out\";\n this.LAYOUT = \"layout\";\n this.INIT_PARAM = \"init_param\";\n this.INIT_PARTS_VISIBLE = \"init_parts_visible\";\n this.VALUE = \"val\";\n this.FILE = \"file\";\n this.json = {};\n}\n\n/**\n* @name loadModelSetting\n* @desc load model settings from json\n* @param {string} jsonPath, {function} callback\n* @returns null\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.loadModelSetting = function(path, callback)\n{\n var thisRef = this;\n var pm = Live2DFramework.getPlatformManager();\n pm.loadBytes(path, function(buf) {\n var str = String.fromCharCode.apply(null,new Uint8Array(buf));\n thisRef.json = JSON.parse(str);\n callback();\n });\n};\n\n/**\n* @name getTextureFile\n* @desc get texture file from json\n* @param {int} order number of texture\n* @returns {string} file path\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.getTextureFile = function(n)\n{\n if (this.json[this.TEXTURES] == null || this.json[this.TEXTURES][n] == null)\n return null;\n\n return this.json[this.TEXTURES][n];\n}\n\n/**\n* @name getModelFile\n* @desc get model file from json\n* @param null\n* @returns {string} file path\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.getModelFile = function()\n{\n return this.json[this.MODEL];\n};\n\n/**\n* @name getTextureNum\n* @desc get the amount of textures from json\n* @param null\n* @returns {int} amout\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.getTextureNum = function()\n{\n if (this.json[this.TEXTURES] == null) return 0;\n\n return this.json[this.TEXTURES].length;\n}\n\n/**\n* @name getHitAreaNum\n* @desc get the amount of hit area from json\n* @param null\n* @returns {int} amout\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.getHitAreaNum = function()\n{\n if (this.json[this.HIT_AREAS] == null)\n return 0;\n\n return this.json[this.HIT_AREAS].length;\n}\n\n/**\n* @name getHitAreaID\n* @desc get the hit area ID of given index from json\n* @param {int} index\n* @returns {int} ID\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.getHitAreaID = function(n)\n{\n if (this.json[this.HIT_AREAS] == null ||\n this.json[this.HIT_AREAS][n] == null)\n return null;\n\n return this.json[this.HIT_AREAS][n][this.ID];\n}\n\n/**\n* @name getHitAreaName\n* @desc get the hit area name of given index from json\n* @param {int} index\n* @returns {string} name\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.getHitAreaName = function(n)\n{\n if (this.json[this.HIT_AREAS] == null ||\n this.json[this.HIT_AREAS][n] == null)\n return null;\n\n return this.json[this.HIT_AREAS][n][this.NAME];\n}\n\n/**\n* @name getPhysicsFile\n* @desc get physics file from json\n* @param null\n* @returns {string} file path\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.getPhysicsFile = function()\n{\n return this.json[this.PHYSICS];\n}\n\n/**\n* @name getPoseFile\n* @desc get pose file from json\n* @param null\n* @returns {string} file path\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.getPoseFile = function()\n{\n return this.json[this.POSE];\n}\n\n/**\n* @name getExpressionNum\n* @desc get the amount of expressions from json\n* @param null\n* @returns {int} amout\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.getExpressionNum = function()\n{\n return (this.json[this.EXPRESSIONS] == null) ? 0 : this.json[this.EXPRESSIONS].length;\n}\n\n/**\n* @name getExpressionFile\n* @desc get expression file from json\n* @param null\n* @returns {string} file path\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.getExpressionFile = function(n)\n{\n if (this.json[this.EXPRESSIONS] == null)\n return null;\n return this.json[this.EXPRESSIONS][n][this.FILE];\n}\n\n/**\n* @name getExpressionName\n* @desc get the hit expression name of given index from json\n* @param {int} index\n* @returns {string} name\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.getExpressionName = function(n)\n{\n if (this.json[this.EXPRESSIONS] == null)\n return null;\n return this.json[this.EXPRESSIONS][n][this.NAME];\n}\n\n/**\n* @name getLayout\n* @desc get the layout from json\n* @param null\n* @returns {string} layout\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.getLayout = function()\n{\n return this.json[this.LAYOUT];\n}\n\n/**\n* @name getInitParamNum\n* @desc get the amount of init parameter from json\n* @param null\n* @returns {int} amount\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.getInitParamNum = function()\n{\n return (this.json[this.INIT_PARAM] == null) ? 0 : this.json[this.INIT_PARAM].length;\n}\n\n/**\n* @name getMotionNum\n* @desc get the amount of motions from json\n* @param null\n* @returns {int} amout\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.getMotionNum = function(name)\n{\n if (this.json[this.MOTION_GROUPS] == null ||\n this.json[this.MOTION_GROUPS][name] == null)\n return 0;\n\n return this.json[this.MOTION_GROUPS][name].length;\n}\n\n/**\n* @name getMotionFile\n* @desc get motion file from json\n* @param null\n* @returns {string} file path\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.getMotionFile = function(name, n)\n{\n if (this.json[this.MOTION_GROUPS] == null ||\n this.json[this.MOTION_GROUPS][name] == null ||\n this.json[this.MOTION_GROUPS][name][n] == null)\n return null;\n\n return this.json[this.MOTION_GROUPS][name][n][this.FILE];\n}\n\n/**\n* @name getMotionSound\n* @desc get motion's sound file from json\n* @param null\n* @returns {string} file path\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.getMotionSound = function(name, n)\n{\n if (this.json[this.MOTION_GROUPS] == null ||\n this.json[this.MOTION_GROUPS][name] == null ||\n this.json[this.MOTION_GROUPS][name][n] == null ||\n this.json[this.MOTION_GROUPS][name][n][this.SOUND] == null)\n return null;\n\n return this.json[this.MOTION_GROUPS][name][n][this.SOUND];\n}\n\n/**\n* @name getMotionFadeIn\n* @desc get the motion's fade in setting from json\n* @param {string} name, {int} index\n* @returns {int} time (1000 if not found)\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.getMotionFadeIn = function(name, n)\n{\n if (this.json[this.MOTION_GROUPS] == null ||\n this.json[this.MOTION_GROUPS][name] == null ||\n this.json[this.MOTION_GROUPS][name][n] == null ||\n this.json[this.MOTION_GROUPS][name][n][this.FADE_IN] == null)\n return 1000;\n\n return this.json[this.MOTION_GROUPS][name][n][this.FADE_IN];\n}\n\n/**\n* @name getMotionFadeOut\n* @desc get the motion's fade out setting from json\n* @param {string} name, {int} index\n* @returns {int} time (1000 if not found)\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.getMotionFadeOut = function(name, n)\n{\n if (this.json[this.MOTION_GROUPS] == null ||\n this.json[this.MOTION_GROUPS][name] == null ||\n this.json[this.MOTION_GROUPS][name][n] == null ||\n this.json[this.MOTION_GROUPS][name][n][this.FADE_OUT] == null)\n return 1000;\n\n return this.json[this.MOTION_GROUPS][name][n][this.FADE_OUT];\n}\n\n/**\n* @name getInitParamID\n* @desc get the visible ID of init parameter from json\n* @param {(int)} index\n* @returns {int} ID\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.getInitParamID = function(n)\n{\n if (this.json[this.INIT_PARAM] == null ||\n this.json[this.INIT_PARAM][n] == null)\n return null;\n\n return this.json[this.INIT_PARAM][n][this.ID];\n}\n\n/**\n* @name getInitParamValue\n* @desc get the visible value of init parameter from json\n* @param {(int)} index\n* @returns {int} value\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.getInitParamValue = function(n)\n{\n if (this.json[this.INIT_PARAM] == null || this.json[this.INIT_PARAM][n] == null)\n return NaN;\n\n return this.json[this.INIT_PARAM][n][this.VALUE];\n}\n\n/**\n* @name getInitPartsVisibleNum\n* @desc get the amount of init parts visible from json\n* @param null\n* @returns {int} amout\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.getInitPartsVisibleNum = function()\n{\n return (this.json[this.INIT_PARTS_VISIBLE] == null) ? 0 : this.json[this.INIT_PARTS_VISIBLE].length;\n}\n\n/**\n* @name getInitPartsVisibleID\n* @desc get the visible ID of init parts from json\n* @param {(int)} index\n* @returns {int} ID\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.getInitPartsVisibleID = function(n)\n{\n if (this.json[this.INIT_PARTS_VISIBLE] == null || this.json[this.INIT_PARTS_VISIBLE][n] == null)\n return null;\n return this.json[this.INIT_PARTS_VISIBLE][n][this.ID];\n}\n\n/**\n* @name getInitPartsVisibleValue\n* @desc get the visible value of init parts from json\n* @param {(int)} index\n* @returns {int} value\n* @memberOf ModelSettingJson\n*/\nModelSettingJson.prototype.getInitPartsVisibleValue = function(n)\n{\n if (this.json[this.INIT_PARTS_VISIBLE] == null || this.json[this.INIT_PARTS_VISIBLE][n] == null)\n return NaN;\n\n return this.json[this.INIT_PARTS_VISIBLE][n][this.VALUE];\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/utils/ModelSettingJson.js"],"sourceRoot":""} \ No newline at end of file diff --git a/live2dw/lib/L2Dwidget.min.js b/live2dw/lib/L2Dwidget.min.js new file mode 100644 index 0000000000..c40355679e --- /dev/null +++ b/live2dw/lib/L2Dwidget.min.js @@ -0,0 +1,3 @@ +/*! https://github.com/xiazeyu/live2d-widget.js built@2019-4-6 09:38:17 */ +var L2Dwidget=function(t){var n=window.webpackJsonpL2Dwidget;window.webpackJsonpL2Dwidget=function(e,o,i){for(var c,u,a=0,s=[];a0?r:e)(t)}},function(t,n){t.exports=function(t){if(void 0==t)throw TypeError("Can't call method on "+t);return t}},function(t,n,e){var r=e(51),o=e(19);t.exports=function(t){return r(o(t))}},function(t,n,e){var r=e(24)("keys"),o=e(16);t.exports=function(t){return r[t]||(r[t]=o(t))}},function(t,n,e){var r=e(11).f,o=e(8),i=e(0)("toStringTag");t.exports=function(t,n,e){t&&!o(t=e?t:t.prototype,i)&&r(t,i,{configurable:!0,value:n})}},function(t,n,e){"use strict";var r=e(14);t.exports.f=function(t){return new function(t){var n,e;this.promise=new t(function(t,r){if(void 0!==n||void 0!==e)throw TypeError("Bad Promise constructor");n=t,e=r}),this.resolve=r(n),this.reject=r(e)}(t)}},function(t,n,e){var r=e(1),o="__core-js_shared__",i=r[o]||(r[o]={});t.exports=function(t){return i[t]||(i[t]={})}},function(t,n){t.exports=function(t){try{return!!t()}catch(t){return!0}}},function(t,n){t.exports=function(t,n){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:n}}},function(t,n,e){"use strict";var r=e(28),o=e(12),i=e(5),c=e(3),u=e(8),a=e(9),s=e(47),f=e(22),l=e(54),p=e(0)("iterator"),d=!([].keys&&"next"in[].keys()),v="values",h=function(){return this};t.exports=function(t,n,e,y,m,b,w){s(e,n,y);var g,x,_,S=function(t){if(!d&&t in O)return O[t];switch(t){case"keys":case v:return function(){return new e(this,t)}}return function(){return new e(this,t)}},k=n+" Iterator",P=m==v,j=!1,O=t.prototype,T=O[p]||O["@@iterator"]||m&&O[m],L=!d&&T||S(m),E=m?P?S("entries"):L:void 0,M="Array"==n?O.entries||T:T;if(M&&(_=l(M.call(new t)))!==Object.prototype&&_.next&&(f(_,k,!0),r||u(_,p)||c(_,p,h)),P&&T&&T.name!==v&&(j=!0,L=function(){return T.call(this)}),r&&!w||!d&&!j&&O[p]||c(O,p,L),a[n]=L,a[k]=h,m)if(g={values:P?L:S(v),keys:b?L:S("keys"),entries:E},w)for(x in g)x in O||i(O,x,g[x]);else o(o.P+o.F*(d||j),n,g);return g}},function(t,n){t.exports=!1},function(t,n,e){var r=e(50),o=e(31);t.exports=Object.keys||function(t){return r(t,o)}},function(t,n,e){var r=e(18),o=Math.min;t.exports=function(t){return t>0?o(r(t),9007199254740991):0}},function(t,n){t.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},function(t,n,e){var r=e(1).document;t.exports=r&&r.documentElement},function(t,n,e){var r=e(2),o=e(14),i=e(0)("species");t.exports=function(t,n){var e,c=r(t).constructor;return void 0===c||void 0==(e=r(c)[i])?n:o(e)}},function(t,n,e){var r,o,i,c=e(13),u=e(66),a=e(32),s=e(17),f=e(1),l=f.process,p=f.setImmediate,d=f.clearImmediate,v=f.MessageChannel,h=f.Dispatch,y=0,m={},b="onreadystatechange",w=function(){var t=+this;if(m.hasOwnProperty(t)){var n=m[t];delete m[t],n()}},g=function(t){w.call(t.data)};p&&d||(p=function(t){for(var n=[],e=1;arguments.length>e;)n.push(arguments[e++]);return m[++y]=function(){u("function"==typeof t?t:Function(t),n)},r(y),y},d=function(t){delete m[t]},"process"==e(10)(l)?r=function(t){l.nextTick(c(w,t,1))}:h&&h.now?r=function(t){h.now(c(w,t,1))}:v?(i=(o=new v).port2,o.port1.onmessage=g,r=c(i.postMessage,i,1)):f.addEventListener&&"function"==typeof postMessage&&!f.importScripts?(r=function(t){f.postMessage(t+"","*")},f.addEventListener("message",g,!1)):r=b in s("script")?function(t){a.appendChild(s("script"))[b]=function(){a.removeChild(this),w.call(t)}}:function(t){setTimeout(c(w,t,1),0)}),t.exports={set:p,clear:d}},function(t,n){t.exports=function(t){try{return{e:!1,v:t()}}catch(t){return{e:!0,v:t}}}},function(t,n,e){var r=e(2),o=e(6),i=e(23);t.exports=function(t,n){if(r(t),o(n)&&n.constructor===t)return n;var e=i.f(t);return(0,e.resolve)(n),e.promise}},function(t,n,e){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.L2Dwidget=void 0;var r,o=function(){function t(t,n){for(var e=0;e1?n-1:0),r=1;r0&&void 0!==arguments[0]?arguments[0]:{};(0,u.configApplyer)(n),this.emit("config",this.config),!u.config.mobile.show&&c.default.mobile()||e.e(0).then(e.bind(null,76)).then(function(n){(a=n).theRealInit(t)}).catch(function(t){console.error(t)})}},{key:"captureFrame",value:function(t){return a.captureFrame(t)}},{key:"downloadFrame",value:function(){this.captureFrame(function(t){var n=document.createElement("a");document.body.appendChild(n),n.setAttribute("type","hidden"),n.href=t,n.download="live2d.png",n.click()})}}]),t}());n.L2Dwidget=s},function(t,n,e){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.config=n.configApplyer=void 0;var r=i(e(74)),o=i(e(75));function i(t){return t&&t.__esModule?t:{default:t}}var c={};n.configApplyer=function(t){(0,o.default)(c,t,r.default)},n.config=c},function(t,n,e){"use strict";Object.defineProperty(n,"__esModule",{value:!0});var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},o=window.device,i={},c=[];window.device=i;var u=window.document.documentElement,a=window.navigator.userAgent.toLowerCase(),s=["googletv","viera","smarttv","internet.tv","netcast","nettv","appletv","boxee","kylo","roku","dlnadoc","roku","pov_tv","hbbtv","ce-html"];i.macos=function(){return f("mac")},i.ios=function(){return i.iphone()||i.ipod()||i.ipad()},i.iphone=function(){return!i.windows()&&f("iphone")},i.ipod=function(){return f("ipod")},i.ipad=function(){return f("ipad")},i.android=function(){return!i.windows()&&f("android")},i.androidPhone=function(){return i.android()&&f("mobile")},i.androidTablet=function(){return i.android()&&!f("mobile")},i.blackberry=function(){return f("blackberry")||f("bb10")||f("rim")},i.blackberryPhone=function(){return i.blackberry()&&!f("tablet")},i.blackberryTablet=function(){return i.blackberry()&&f("tablet")},i.windows=function(){return f("windows")},i.windowsPhone=function(){return i.windows()&&f("phone")},i.windowsTablet=function(){return i.windows()&&f("touch")&&!i.windowsPhone()},i.fxos=function(){return(f("(mobile")||f("(tablet"))&&f(" rv:")},i.fxosPhone=function(){return i.fxos()&&f("mobile")},i.fxosTablet=function(){return i.fxos()&&f("tablet")},i.meego=function(){return f("meego")},i.cordova=function(){return window.cordova&&"file:"===location.protocol},i.nodeWebkit=function(){return"object"===r(window.process)},i.mobile=function(){return i.androidPhone()||i.iphone()||i.ipod()||i.windowsPhone()||i.blackberryPhone()||i.fxosPhone()||i.meego()},i.tablet=function(){return i.ipad()||i.androidTablet()||i.blackberryTablet()||i.windowsTablet()||i.fxosTablet()},i.desktop=function(){return!i.tablet()&&!i.mobile()},i.television=function(){for(var t=0;t1},i.landscape=function(){return window.innerHeight/window.innerWidth<1},i.noConflict=function(){return window.device=o,this};function f(t){return-1!==a.indexOf(t)}function l(t){return u.className.match(new RegExp(t,"i"))}function p(t){var n=null;l(t)||(n=u.className.replace(/^\s+|\s+$/g,""),u.className=n+" "+t)}function d(t){l(t)&&(u.className=u.className.replace(" "+t,""))}i.ios()?i.ipad()?p("ios ipad tablet"):i.iphone()?p("ios iphone mobile"):i.ipod()&&p("ios ipod mobile"):i.macos()?p("macos desktop"):i.android()?i.androidTablet()?p("android tablet"):p("android mobile"):i.blackberry()?i.blackberryTablet()?p("blackberry tablet"):p("blackberry mobile"):i.windows()?i.windowsTablet()?p("windows tablet"):i.windowsPhone()?p("windows mobile"):p("windows desktop"):i.fxos()?i.fxosTablet()?p("fxos tablet"):p("fxos mobile"):i.meego()?p("meego mobile"):i.nodeWebkit()?p("node-webkit"):i.television()?p("television"):i.desktop()&&p("desktop"),i.cordova()&&p("cordova");function v(){i.landscape()?(d("portrait"),p("landscape"),h("landscape")):(d("landscape"),p("portrait"),h("portrait")),b()}function h(t){for(var n in c)c[n](t)}i.onChangeOrientation=function(t){"function"==typeof t&&c.push(t)};var y="resize";Object.prototype.hasOwnProperty.call(window,"onorientationchange")&&(y="onorientationchange"),window.addEventListener?window.addEventListener(y,v,!1):window.attachEvent?window.attachEvent(y,v):window[y]=v,v();function m(t){for(var n=0;n=n.length?{value:void 0,done:!0}:(t=r(n,e),this._i+=t.length,{value:t,done:!1})})},function(t,n,e){var r=e(18),o=e(19);t.exports=function(t){return function(n,e){var i,c,u=String(o(n)),a=r(e),s=u.length;return a<0||a>=s?t?"":void 0:(i=u.charCodeAt(a))<55296||i>56319||a+1===s||(c=u.charCodeAt(a+1))<56320||c>57343?t?u.charAt(a):i:t?u.slice(a,a+2):c-56320+(i-55296<<10)+65536}}},function(t,n,e){"use strict";var r=e(48),o=e(26),i=e(22),c={};e(3)(c,e(0)("iterator"),function(){return this}),t.exports=function(t,n,e){t.prototype=r(c,{next:o(1,e)}),i(t,n+" Iterator")}},function(t,n,e){var r=e(2),o=e(49),i=e(31),c=e(21)("IE_PROTO"),u=function(){},a="prototype",s=function(){var t,n=e(17)("iframe"),r=i.length;for(n.style.display="none",e(32).appendChild(n),n.src="javascript:",(t=n.contentWindow.document).open(),t.write(" + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +

    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    #/boot/grub/grub.conf 缺失:

    yum install -y grub
    grub-mkconfig -o /boot/grub/grub.conf

    #/boot/grub2/grub.cfg 缺失:

    yum install -y grub2
    grub2-mkconfig -o /boot/grub2/grub.cfg

    uname -a
    sudo grep ^menuentry /boot/grub2/grub.cfg | awk -F\' '{print $2}' | nl -v0

    sudo grub2-editenv list

    sudo grep GRUB_DEFAULT /etc/default/grub

    sudo vim /etc/default/grub

    sudo grub2-set-default 1

    sudo grub2-mkconfig -o /boot/grub2/grub.cfg

    sudo dracut --force

    sudo reboot

    uname -a
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/100/index.html b/page/100/index.html new file mode 100644 index 0000000000..adc2ecb5b0 --- /dev/null +++ b/page/100/index.html @@ -0,0 +1,534 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    深浅拷贝

    1
    2
    3
    4
    5
    6
    let a = {
    age: 1
    }
    let b = a
    a.age = 2
    console.log(b.age) // 2
    +

    从上述例子中我们可以发现,如果给一个变量赋值一个对象,那么两者的值会是同一个引用,其中一方改变,另一方也会相应改变。
    通常在开发中我们不希望出现这样的问题,我们可以使用浅拷贝来解决这个问题。

    +

    浅拷贝

    首先可以通过 Object.assign 来解决这个问题。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    // js代码

    let a = {
    age: 1
    }
    let b = Object.assign({}, a)
    a.age = 2
    console.log(b.age) // => 1
    +

    当然我们也可以通过展开运算符(…)来解决

    +
    1
    2
    3
    4
    5
    6
    let a = {
    age: 1
    }
    let b = {...a}
    a.age = 2
    console.log(b.age) // => 1
    +

    我们还可以用很多简单的方法都能实现浅拷贝:

    +
    1
    2
    arr.slice();
    arr.concat();
    + +

    通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就需要使用到深拷贝了

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // js代码

    let a = {
    age: 1,
    jobs: {
    first: 'FE'
    }
    }
    let b = {...a}
    a.jobs.first = 'native'
    console.log(b.jobs.first) // native
    +

    浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到刚开始的话题了,两者享有相同的引用。要解决这个问题,我们需要引入深拷贝。

    +

    深拷贝

    这个问题通常可以通过 JSON.parse(JSON.stringify(object)) 来解决,这也是最好用最简单的方法,俗称乞丐版。
    乞丐版

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // js代码

    let a = {
    age: 1,
    jobs: {
    first: 'FE'
    }
    }
    let b = JSON.parse(JSON.stringify(a))
    a.jobs.first = 'native'
    console.log(b.jobs.first) // FE
    +

    但是该方法也是有局限性的:

    +
      +
    • 会忽略 undefined
    • +
    • 会忽略 symbol
    • +
    • 不能序列化函数
    • +
    • 不能解决循环引用的对象
    • +
    +

    举个栗子:

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // js代码

    let obj = {
    a: 1,
    b: {
    c: 2,
    d: 3,
    },
    }

    obj.c = obj.b
    obj.e = obj.a
    obj.b.c = obj.c
    obj.b.d = obj.b
    obj.b.e = obj.b.c

    let newObj = JSON.parse(JSON.stringify(obj))
    console.log(newObj) // => Uncaught TypeError: Converting circular structure to JSON
    +

    如果你有这么一个循环引用对象,你会发现你不能通过该方法深拷贝
    在遇到函数、 undefined 或者 symbol 的时候,该对象也不能正常的序列化

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    let a = {
    age: undefined,
    sex: Symbol('fmale'),
    jobs: function() {},
    name: 'lixuguang'
    }
    let b = JSON.parse(JSON.stringify(a))
    console.log(b) // => {name: "lixuang"}
    + +

    你会发现在上述情况中,该方法会忽略掉函数和 undefined
    但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题,并且该函数是内置函数中处理深拷贝性能最快的。

    +

    那么是否可以解决函数和循环引用的问题呢?答案是肯定可以解决,接下来是基础版本的改造
    基础版

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function myClone(target){
    if(typeof target === 'object'){ // 判断传入目标是否是object类型
    let cloneTarget = {}; // 创建克隆对象
    for(const key in target){ // 遍历目标对象
    cloneTarget[key] = myClone(target[key]) // 递归调用 clone 方法
    }
    return cloneTarget;
    } else {
    return target // 如果不是 object 返回
    }
    }
    +

    写到这里已经可以帮助你应付一些面试官考察你的递归解决问题的能力。但是显然,这个深拷贝函数还是有一些问题。
    这里只考虑了对象,没有考虑数组。
    下面我们来做一个强化版的深拷贝,同时考虑对象、数组还有循环引用的问题。
    强化版

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function myClone(target, map = new WeakMap()) { // WeakMap => 键对象弱引用, 可被垃圾回收
    if(typeof target === 'object'){ // 判断是否是对象
    let cloneTarget = Array.isArray(target) ? [] : {}; // 判断是是数组还是对象
    if(map.get(target)) {
    return target;
    }

    map.set(target, cloneTarget);

    for(const key in target) {
    cloneTarget[key] = myClone(target[key], map)
    }
    return cloneTarget;
    } else {
    return target;
    }
    }
    +

    当然如果你的数据中含有以上三种情况下,可以使用 lodash 的深拷贝函数。
    如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // js代码

    function structuralClone(obj) {
    return new Promise(resolve => {
    const {port1, port2} = new MessageChannel();
    port2.onmessage = ev => resolve(ev.data);
    port1.postMessage(obj);
    });
    }

    var obj = {
    a: 1,
    b: {
    c: b
    }
    }
    // 注意该方法是异步的
    // 可以处理 undefined 和循环引用对象
    (async () => {
    const clone = await structuralClone(obj)
    })()
    + +

    深拷贝实现方式2,可以深拷贝 functionsymbol,等等,堪称终极版
    终极版

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    // js代码

    const mapTag = '[object Map]';
    const setTag = '[object Set]';
    const arrayTag = '[object Array]';
    const objectTag = '[object Object]';
    const argsTag = '[object Arguments]';

    const boolTag = '[object Boolean]';
    const dateTag = '[object Date]';
    const numberTag = '[object Number]';
    const stringTag = '[object String]';
    const symbolTag = '[object Symbol]';
    const errorTag = '[object Error]';
    const regexpTag = '[object RegExp]';
    const funcTag = '[object Function]';

    const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];
    function forEach(array, iteratee) {
    let index = -1;
    const length = array.length;
    while (++index < length) {
    iteratee(array[index], index);
    }
    return array;
    }

    function isObject(target) {
    const type = typeof target;
    return target !== null && (type === 'object' || type === 'function');
    }

    function getType(target) {
    return Object.prototype.toString.call(target);
    }

    function getInit(target) {
    const Ctor = target.constructor;
    return new Ctor();
    }

    function cloneSymbol(targe) {
    return Object(Symbol.prototype.valueOf.call(targe));
    }

    function cloneReg(targe) {
    const reFlags = /\w*$/;
    const result = new targe.constructor(targe.source, reFlags.exec(targe));
    result.lastIndex = targe.lastIndex;
    return result;
    }

    function cloneFunction(func) {
    const bodyReg = /(?<={)(.|\n)+(?=})/m;
    const paramReg = /(?<=\().+(?=\)\s+{)/;
    const funcString = func.toString();
    if (func.prototype) {
    const param = paramReg.exec(funcString);
    const body = bodyReg.exec(funcString);
    if (body) {
    if (param) {
    const paramArr = param[0].split(',');
    return new Function(...paramArr, body[0]);
    } else {
    return new Function(body[0]);
    }
    } else {
    return null;
    }
    } else {
    return eval(funcString);
    }
    }

    function cloneOtherType(targe, type) {
    const Ctor = targe.constructor;
    switch (type) {
    case boolTag:
    case numberTag:
    case stringTag:
    case errorTag:
    case dateTag:
    return new Ctor(targe);
    case regexpTag:
    return cloneReg(targe);
    case symbolTag:
    return cloneSymbol(targe);
    case funcTag:
    return cloneFunction(targe);
    default:
    return null;
    }
    }

    function clone(target, map = new WeakMap()) {
    // 克隆原始类型
    if (!isObject(target)) {
    return target;
    }

    // 初始化
    const type = getType(target);
    let cloneTarget;
    if (deepTag.includes(type)) {
    cloneTarget = getInit(target, type);
    } else {
    return cloneOtherType(target, type);
    }

    // 防止循环引用
    if (map.get(target)) {
    return target;
    }
    map.set(target, cloneTarget);

    // 克隆set
    if (type === setTag) {
    target.forEach(value => {
    cloneTarget.add(clone(value));
    });
    return cloneTarget;
    }

    // 克隆map
    if (type === mapTag) {
    target.forEach((value, key) => {
    cloneTarget.set(key, clone(value));
    });
    return cloneTarget;
    }

    // 克隆对象和数组
    const keys = type === arrayTag ? undefined : Object.keys(target);
    forEach(keys || target, (value, key) => {
    if (keys) {
    key = value;
    }
    cloneTarget[key] = clone(target[key], map);
    });

    return cloneTarget;
    }

    // 调用方法
    clone(target);
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/101/index.html b/page/101/index.html new file mode 100644 index 0000000000..4d9b7bf4ff --- /dev/null +++ b/page/101/index.html @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    new 一个对象的过程

      +
    1. 新生成了一个对象
    2. +
    3. 链接到原型
    4. +
    5. 绑定 this
    6. +
    7. 返回新对象
    8. +
    +

    在调用 new 的过程中会发生以上四件事情,我们也可以试着来自己实现一个 new

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // js代码

    function create() {
    // 创建一个空的对象
    let obj = new Object()
    // 获得构造函数
    let Con = [].shift.call(arguments)
    // 链接到原型
    obj.__proto__ = Con.prototype
    // 绑定 this,执行构造函数
    let result = Con.apply(obj, arguments)
    // 确保 new 出来的是个对象
    return typeof result === 'object' ? result : obj
    }
    + +

    对于实例对象来说,都是通过 new 产生的,无论是 function Foo() 还是 let a = { b : 1 }

    +

    对于创建一个对象来说,更推荐使用字面量的方式创建对象(无论性能上还是可读性)。因为你使用 new Object() 的方式创建对象需要通过作用域链一层层找到 Object,但是你使用字面量的方式就没这个问题。

    +
    1
    2
    3
    4
    // js代码

    function Foo() {} // function 就是个语法糖,内部等同于 new Function()
    let a = { b: 1 } // 这个字面量内部也是使用了 new Object()
    +

    对于 new 来说,还需要注意下运算符优先级。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // js代码

    function Foo() {
    return this;
    }
    Foo.getName = function () {
    console.log('1');
    };
    Foo.prototype.getName = function () {
    console.log('2');
    };

    new Foo.getName(); // -> 1
    new Foo().getName(); // -> 2
    + +

    从上图可以看出,new Foo() 的优先级大于 new Foo ,所以对于上述代码来说可以这样划分执行顺序

    +
    1
    2
    3
    4
    // js代码

    new (Foo.getName());
    (new Foo()).getName();
    + +
      +
    • 对于第一个函数来说,先执行了 Foo.getName() ,所以结果为 1;
    • +
    • 对于后者来说,先执行 new Foo() 产生了一个实例,然后通过原型链找到了 Foo 上的 getName 函数,所以结果为 2。
    • +
    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/102/index.html b/page/102/index.html new file mode 100644 index 0000000000..8fb0933ee6 --- /dev/null +++ b/page/102/index.html @@ -0,0 +1,513 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    闭包Closure

    闭包的定义很简单:函数 A 返回了一个函数 B ,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // js代码

    function A() {
    let a = 1
    function B() {
    console.log(a)
    }
    return B
    }
    +

    你是否会疑惑,为什么函数 A 已经弹出调用栈了,为什么函数 B 还能引用到函数 A 中的变量。因为函数 A 中的变量这时候是存储在堆上的。现在的 JS 引擎可以通过逃逸分析辨别出哪些变量需要存储在堆上,哪些需要存储在栈上。

    +

    经典面试题,循环中使用闭包解决 var 定义函数的问题

    +
    1
    2
    3
    4
    5
    6
    7
    // js代码

    for ( var i=1; i<=5; i++) {
    setTimeout( function timer() {
    console.log( i );
    }, i*1000 );
    }
    +

    首先因为 setTimeout 是个异步函数,所有会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。

    +

    解决办法两种,第一种使用闭包

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // js代码

    for (var i = 1; i <= 5; i++) {
    (function(j) {
    setTimeout(function timer() {
    console.log(j);
    }, j * 1000);
    })(i);
    }
    +

    第二种就是使用 setTimeout 的第三个参数(附加参数,传给 setTimeout 中的函数)

    +
    1
    2
    3
    4
    5
    6
    7
    // js代码

    for ( var i=1; i<=5; i++) {
    setTimeout( function timer(j) {
    console.log( j );
    }, i*1000, i);
    }
    + +

    第三种就是使用 let 定义 i

    +
    1
    2
    3
    4
    5
    6
    7
    // js代码

    for ( let i=1; i<=5; i++) {
    setTimeout( function timer() {
    console.log( i );
    }, i*1000 );
    }
    +

    因为对于 let 来说,他会创建一个块级作用域,相当于

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // js代码

    { // 形成块级作用域
    let i = 0
    {
    let ii = i
    setTimeout( function timer() {
    console.log( ii );
    }, i*1000 );
    }
    i++
    {
    let ii = i
    }
    i++
    {
    let ii = i
    }
    ...
    }
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/103/index.html b/page/103/index.html new file mode 100644 index 0000000000..259fc1dd4a --- /dev/null +++ b/page/103/index.html @@ -0,0 +1,523 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    执行上下文

    当执行 JS 代码时,会产生三种执行上下文

    +
      +
    • 全局执行上下文
    • +
    • 函数执行上下文
    • +
    • eval 执行上下文
    • +
    +

    每个执行上下文中都有三个重要的属性

    +
      +
    • 变量对象( VO ),包含变量、函数声明和函数的形参,该属性只能在全局上下文中访问
    • +
    • 作用域链( JS 采用词法作用域,也就是说变量的作用域是在定义时就决定了)
    • +
    • this
      1
      2
      3
      4
      5
      6
      7
      // js代码

      var a = 10
      function foo(i) {
      var b = 20
      }
      foo()
      +对于上述代码,执行栈中有两个上下文:全局上下文和函数 foo 上下文。
      1
      2
      3
      4
      5
      6
      // js代码

      stack = [
      globalContext,
      fooContext
      ]
      +对于全局上下文来说, VO 大概是这样的
      1
      2
      3
      4
      5
      6
      7
      // js代码

      globalContext.VO === globe
      globalContext.VO = {
      a: undefined,
      foo: <Function>,
      }
      +对于函数 foo 来说, VO 不能访问,只能访问到活动对象( AO
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      // js代码

      fooContext.VO === foo.AO
      fooContext.AO {
      i: undefined,
      b: undefined,
      arguments: <>
      }
      // arguments 是函数独有的对象(箭头函数没有)
      // 该对象是一个伪数组,有 `length` 属性且可以通过下标访问元素
      // 该对象中的 `callee` 属性代表函数本身
      // `caller` 属性代表函数的调用者
      +对于作用域链,可以把它理解成包含自身变量对象和上级变量对象的列表,通过 [[Scope]] 属性查找上级变量
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // js代码

      fooContext.[[Scope]] = [
      globalContext.VO
      ]
      fooContext.Scope = fooContext.[[Scope]] + fooContext.VO
      fooContext.Scope = [
      fooContext.VO,
      globalContext.VO
      ]
      +接下来让我们看一个老生常谈的例子, var
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // js代码

      b() // call b
      console.log(a) // undefined

      var a = 'Hello world'

      function b() {
      console.log('call b')
      }
      +想必以上的输出大家肯定都已经明白了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行上下文时,会有两个阶段。第一个阶段是创建的阶段(具体步骤是创建 VO ), JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined ,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用。
    • +
    +

    在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // js代码

    b() // call b second

    function b() {
    console.log('call b fist')
    }
    function b() {
    console.log('call b second')
    }
    var b = 'Hello world'
    +

    var 会产生很多错误,所以在 ES6 中引入了 letlet 不能在声明前使用,但是这并不是常说的 let 不会提升, let 提升了声明但没有赋值,因为临时死区导致了并不能在声明前使用。

    +

    对于非匿名的立即执行函数需要注意以下一点

    +
    1
    2
    3
    4
    5
    6
    7
    // js代码

    var foo = 1
    (function foo() {
    foo = 10
    console.log(foo)
    }()) // -> ƒ foo() { foo = 10 ; console.log(foo) }
    +

    因为当 JS 解释器在遇到非匿名的立即执行函数时,会创建一个辅助的特定对象,然后将函数名称作为这个对象的属性,因此函数内部才可以访问到 foo ,但是这个值又是只读的,所以对它的赋值并不生效,所以打印的结果还是这个函数,并且外部的值也没有发生更改。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // js代码

    specialObject = {};

    Scope = specialObject + Scope;

    foo = new FunctionExpression;
    foo.[[Scope]] = Scope;
    specialObject.foo = foo; // {DontDelete}, {ReadOnly}

    delete Scope[0]; // remove specialObject from the front of scope chain
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/104/index.html b/page/104/index.html new file mode 100644 index 0000000000..08cad86edd --- /dev/null +++ b/page/104/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    this

    this 是很多人会混淆的概念,但是其实他一点都不难,你只需要记住几个规则就可以了。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // js代码

    function foo() {
    console.log(this.a)
    }
    var a = 1
    foo()

    var obj = {
    a: 2,
    foo: foo
    }
    obj.foo()

    // 以上两者情况 `this` 只依赖于调用函数前的对象,优先级是第二个情况大于第一个情况

    // 以下情况是优先级最高的,`this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向
    var c = new foo()
    c.a = 3
    console.log(c.a)

    // 还有种就是利用 `call` , `apply` , `bind` 改变 `this` ,这个优先级仅次于 `new`
    +

    以上几种情况明白了,很多代码中的 this 应该就没什么问题了,下面让我们看看箭头函数中的 this

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    function a() {
    return () => {
    return () => {
    console.log(this)
    }
    }
    }
    console.log(a()()())
    +

    箭头函数其实是没有 this 的,这个函数中的 this 只取决于他外面的第一个不是箭头函数的函数的 this 。在这个例子中,因为调用 a 符合前面代码中的第一个情况,所以 thiswindow。并且 this 一旦绑定了上下文,就不会被任何代码改变。

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/105/index.html b/page/105/index.html new file mode 100644 index 0000000000..99bd0604ec --- /dev/null +++ b/page/105/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    instanceof

    instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype
    举例:

    +
    1
    a instanceof Object
    +

    判断 Objectprototype 是否在 a 的原型链上。

    +

    我们也可以试着实现一下 instanceof

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function myInstanceof(left, right) { // left 表示左表达式,right 表示右表达式
    let prototype = right.prototype // 获得类型的原型
    left = left.__proto__ // 获得对象的原型

    while (true) { // 判断对象的类型是否等于类型的原型
    if (left === null)
    return false
    if (prototype === left)
    return true
    left = left.__proto__
    }
    }
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/106/index.html b/page/106/index.html new file mode 100644 index 0000000000..532e96c18d --- /dev/null +++ b/page/106/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    还记得我19年总结最下面的那张照片么,那个同事送的的本子,对,就是下面这张图,这本子叫做《One In A Million》中文叫做极少数手册,是一个管理时间用的管理手册,英文如果直译的话应该是叫做万里挑一,实话说我并不是很自信,虽然我觉得自己还算可以,但远远达不到万里挑一的程度,所以,我打算好好利用这个本子,让自己尽可能成为百里挑一,千里挑一,最后万里挑一的人。

    +

    本子里有一个栏目叫做21天主题挑战,21-Day Challenge,每个月21天都会给你列出一些主题,越往后列出来的主题越少,更多的需要个人去补充,我想去挑战这些内容,让自己的生活变得有目标,让自己变得更好。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/107/index.html b/page/107/index.html new file mode 100644 index 0000000000..79ddf55c46 --- /dev/null +++ b/page/107/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    原型

    yuanxing.png
    每个函数都有 prototype 属性,除了 Function.prototype.bind(),该属性指向原型对象,简称原型prototype原型对象里的constructor指向构造函数本身。
    每个对象都有 __proto__ 属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 _proto_ 来访问。
    对象可以通过 __proto__ 来寻找不属于该对象的属性,__proto__ 将对象连接起来组成了原型链

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/108/index.html b/page/108/index.html new file mode 100644 index 0000000000..499cac39dc --- /dev/null +++ b/page/108/index.html @@ -0,0 +1,510 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    +

    温故而知新,可以为师矣。
    ———————— 论语

    +
    +

    这篇文章主要是把我看过的书中的知识点,还有其他人整理的面试题进行整理,巩固自己的前端开发理论知识,希望其他看到这篇文章的人也能有所帮助。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/109/index.html b/page/109/index.html new file mode 100644 index 0000000000..f0bbdd4736 --- /dev/null +++ b/page/109/index.html @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    2020元旦伊始

    时间过的真快,19年睡了一觉就成了过去的一年,今天是元旦,早上8:00起了床,简单洗漱后我出了门,新年新气象,去年没有坚持的晨跑,今年想要坚持一下,所以一早就出门跑了会儿步,太久没活动了,只是跑了半个小时就气喘吁吁,只好回家,但是即使是这半个小时,我感觉呼吸了新鲜的空气,整个人也精神了不少,回到家坐在阳台上,拿出小王宁送我的极少数手册开始研究了起来,生活还是应该有点仪式感何况是新年伊始,手册中有很多建议,全部看过一遍以后我郑重的拿起了笔写下了我今天的行动计划。但是没有写年度计划,明天上班后跟领导还要谈一谈,谈过之后再定一下年度的计划。今天我的计划就是陪家人过好元旦这一天。

    +

    元旦执行计划

      +
    1. 写一篇日志
    2. +
    3. 将家人这一天游玩的过程以Vlog的形式记录下来,上传到西瓜视频上。
    4. +
    5. 陪家人逛街,给桐桐买新衣裳。
    6. +
    +

    执行计划

    吃过早饭后大概到了10:30,一家人收拾好以后终于可以出门了,今天有点风,一点点冷不是特别冷,空气还不错,天气听晴朗的,一家人开着车听着歌前往甘井子万达广场,20分钟左右我们就到了万达广场,首先我们先去看了桐桐心心念念的小猫咪,也就是猫咪咖啡馆里的那些小可爱,桐桐可能是受妈妈影响,很喜欢猫咪,但是又有一点点怕。大概看了10分钟的猫咪,我们继续下一站,上四楼给桐桐买新衣裳,桐桐看到满眼的漂亮衣裳很是兴奋,最终她挑选了一套带着向日葵花朵的淡蓝色衣裙,穿上漂亮的新衣裳,平时像个假小子似的桐桐也一下子害羞了起来,让妈妈和奶奶看完后还会让爸爸和爷爷看看,得到了大家的肯定以后,心满意足的买下了它,也不知道是不是试衣服试累了,桐桐嚷嚷着要吃饭,桐桐妈说有一家港式茶餐厅味道不错,之前和同事来过,于是我们就去了4楼的茶餐厅,说实话这是我头一次吃茶餐厅,我还跟我爸妈调侃说,这也算是我们来过一次香港了。去到餐厅时才11点多一点,客还没有上满,还有座位,我们找了个靠里的位置坐了下来,打开菜谱,桐桐妈挑了几样她觉得不错的菜,上菜时间稍微有点慢,期间桐桐有些不耐烦,坐也坐不住,终于等到了吃的上来,可是觉得大多数吃的都太偏甜,爸妈和我都吃不太惯,桐桐吃的还不错,大概吃了半个小时就吃完了,出门的时候门口已经排起了长队,还好来的早一点,不然吃饭都要等了。本打算带着桐桐回家睡个觉,下午要到姥爷家串门,可桐桐说没有逛够,正巧这时看到了一楼有小丑在折气球玩具,我们就下到一楼找小丑玩了,小丑给桐桐折了个贵宾犬,小丑是荣耀手机的员工,可能是元旦策划的活动吧,反正来了就来了,进去看看也好,看了新出的V30手机,感觉并不是很喜欢,店员说有旧机抵值的活动,我问了他我的7p128g还能值多少,他打了几个电话后回复我1400块,哎,三年前7000块买的手机,现在只值1400了,算了还是用着吧,出了荣耀手机店又去了旁边的华为,看了看新出的mate30pro,这个手机是真的不错,当然手机不错价格也美丽,6500+的价格真的是让我不舍得买,还是把钱留下来给桐桐吧。桐桐终于累了,嚷嚷着要走,于是一家人驱车去了姥姥家。

    +

    姥姥家在西南路,因为平时工作忙很少带桐桐去,这次去桐桐又不认识他们了,开始的时候很拘束,也不说话,一直到吃饭结束才终于愿意开口问好唱歌,因为家里没有电视机,到了姥姥家看到电视机很兴奋,一直在看,今天也算是过了瘾了,亲人就是即使很长时间不见面,但一见面就特别亲的人,饭从5:30一直吃到8:30,聊了工作,聊了生活,聊了困惑,聊了坦然,我终于知道了长辈们的智慧,了解了身体健康的重要性,20年一定要锻炼出一副好的身体,对家人负责。

    +

    9:30回到了家,心里挺高兴的,新年第一天陪家人一起度过了充实的一天,回到家把今天路上录下来的视频剪成了短视频上传到了西瓜视频,这也是我今年的一个想法,记录我们一家人的幸福生活,如果有粉丝有点播的话还能赚点钱,没有的话也记录了桐桐的成长,算是一点纪念吧。最后写下了这篇日记,写完之后我就要睡觉去了,明天一早就要起床,明天起我就要走路去上班了,为了有一副好身体,加油。

    +

    今天的目标都完成了,很开心~~

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/11/index.html b/page/11/index.html new file mode 100644 index 0000000000..f944ddd702 --- /dev/null +++ b/page/11/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    搭建Docker私有仓库

    利用registry搭建私有仓库

    +

    下载registry

    1
    docker pull registry
    + +

    配置

    1
    2
    /etc/docker/doemon.json
    # 配置 'insecure-registry'
    +

    重启docker

    1
    systemctl restart docker
    + +

    创建registry容器(关联私有仓库配置)

    1
    docker run -d -p 5000:5000 --name registry registry:latest
    + +

    推送镜像到私有仓

      +
    • 备份镜像(172.16.12.134:5000 私有仓地址)

      +
      1
      docker tag ubuntu:latest 172.16.12.134:5000/my_ubuntu
    • +
    • 推送

      +
      1
      docker push 172.16.12.134:5000/my_ubuntu
    • +
    • 下载

      +
      1
      docker pull 172.16.12.134:5000/my_ubuntu
    • +
    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/110/index.html b/page/110/index.html new file mode 100644 index 0000000000..b6b58ea04d --- /dev/null +++ b/page/110/index.html @@ -0,0 +1,532 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    不知不觉又是一年,今天是2019年的最后一天,也是我30岁的最后一天,回顾这一年有收获有遗憾。翻出了18年的总结,看到了18年底对于自己19年的规划,现在回过头来看看哪些完成了。

    +

    18年底定的计划

    学习技术

    1. 深入学习客户端开发(全年)

    18年客户端的项目也做了几个,Electron的开发能力经验更多了,之前只是用html+css+js做前端页面的开发,今年又增加了跟vue框架的结合,成功在多个项目中实践,只是一直没有形成规范感觉还是少了一环,原来一直在犹豫Electron到底算不算前端的技术栈之一,后来也算是想明白了,大前端范畴,只要是面向用户的都算是前端该做的范畴,所以Electron当然也算是前端应该掌握的技术栈,20年会继续学习客户端技术,把公司相关规范制定和落地。

    +

    2. 学习前端自动化测试相关知识(2019年3月前)

    18年关注了前端自动化测试相关的知识,本打算19年好好学习一下,在公司项目中实践,但是今年前后端工作量大增,前端人员的精力有限,加之成本的考虑,并没有开展起来,不过这方面的技能储备已经具备了,年底两次关于前端自动化的测试也将前端自动化的思想普及到了公司前端的团队里,20年是公司强调质量的一年,相信20年前端自动化测试将有机会在公司一些项目中落地

    +

    3. 学习并掌握TS (2019年5月前)

    18年就看到了TS将会成为一门比较活的开发语言,目前来说已经具备了做TS开发的技能,只是TS更适合大型的长期的项目,将更好的保障项目的代码质量,快速迭代和小型项目并不是很适合。前端团队里没有开花,没想到居然在开发团队里见到了曙光,感谢开发的小伙伴愿意接受这门前端新技术,相信有一个点采用后看到了优势,后续普及工作将会更好开展。

    +

    4. 学习并掌握React(2019年7月前)

    18年Vue已经在公司前端全面普及开来,但是市场上React仍然是占据更多的份额,而且RN还能做App开发,所以还是一门必须要掌握的技术,今年看了一本React相关的书,也看了一些相关的视频教程,还参加了公司同事开展的React分享,虽然没有在项目中实际使用过,但是理论知识已经掌握了,可以进行小的Demo开发,20年要用React写一个开源的项目,做到学有所用,学以致用。

    +

    5. 学习前端持续集成的相关知识(2019年9月前)

    19年公司强调了代码必须走线上构建的要求,前端工程化和自动构建持续集成的工作可以说是工作所迫,这方面的技能已经Get到了,另外令我感到高兴的是,通过对Github-Actions的配置,我的博客可以像提交代码一样提交文章了,而且也实践了自动构建的相关技术,很开心。

    +

    6. 学习Docker虚拟化技术( 2019年10月前)

    这个任务没有达成,原本是希望能够通过Docker统一前端开发环境,不过Docker的适用场景还是以部署环境为主,Docker今年貌似热度也降下来了,今年K8s越来越火,不管怎样,持续关注新技术。

    +

    整理计划

    1. 养成写博客的习惯,至少2天一篇,可以是技术博客,也可以是其他。

    今年实在是太忙了,别说两天一篇文章,就连一个月一篇的量都没能达成,不过在年底我终于把博客又重新激活了,希望自己20年能够把博客坚持写下来,很简单,积累自己,打造个人品牌。

    +

    2. 将常用的方法和功能做成插件,开源给公司使用

    今年4月开始,更多的精力都投在了部门的前端管理工作,说实话也迷茫也难过,这份迷茫终于在19年年底解开了,没能在技能层面有更高的提升,以及在代码层面给公司带来价值我觉得还是有些遗憾,20年希望自己能够在技术上有更大的进步,技术深度上/广度上/架构层面/后端技能都能有长足的进步。

    +

    读书计划

    1. 每周读完一本书,并写一篇读后感

    2019年读了25本书,每读完一本都会写一篇读后感,虽然没能保证每周读一本书,但是基本保证了每两周读完一本书,算是完成了50%的目标吧。20年继续努力,读书使人聪明,因为不聪明所以更要多读书。

    +

    部门前端计划

    加强各设计组前端之间的交流

    +

    设计组的出现导致前端交流不畅,2019年加强与各设计组前端的交流,掌握各位前端的技术能力。

    +
    +

    没想到一语成第,设计组居然在4月份将前端整个拿出来构成了一个前端组,由我来带,更没想到的是居然在19年年底又解散到项目团队了,这9个月的时间可以说有苦有泪也有甜,前端小伙伴们为了同一个目标,共同努力,尽最大的努力完成公司的业务,可以说为了这个目标,这一年我付出了很多很多,在这方面愧对了家人孩子,虽然最后前端还是拆分了,可以说我能力有限,但是我心里可以说是无愧,希望尽到项目团队里的小伙伴们也能开心工作,迅速成长,虽然不再在一个团队里,但是,我仍是你们的光妈。

    +

    前端俱乐部推动

    +

    继续每周2小时的兴趣交流,交流形式不限于ppt,要求各自建立技术博客做笔记。

    +
    +

    俱乐部的活动最后还是没能坚持下来,大家工作比较忙也好,我自己的原因也好,确实没能坚持下来,20年还是希望能够将俱乐部活动组织起来,算是大家交流的机会也好,帮助大家开阔眼界也好,20年一定要再做起来!

    +

    进行梯队划分建设

    +

    前端今年人数明显增多,但各层级职责并没有区分,19年抽时间进行梯队建设。

    +
    +

    19年前端从14人增加到了34人,可以说人数上翻翻了,随着负责人制的执行,梯队建设的工作算是完成了50%,但是能力提升的工作可以说还是做的不到位,应该有机会成长为高工的小伙伴没能升上去,这我有一定的责任,20年虽然团队不再,但是希望你们都坚持努力学习,迈向更高的台阶。

    +

    引入前端工程化工具和思想

    +

    目前前端工程化已经非常成熟,希望能将成熟的技术和工具引入。

    +
    +

    19年,随着Vue的普及和熟练,模块化开发组件化开发的思想已经深入到每一个前端的心中,现在大家都知道该如何使用这些技术和工具,希望大家能不仅仅局限在用也能了解了解其中原理,至少对常用的配置有所了解。

    +

    提升整体前端开发的能力

    +

    目前整体前端开发能力还相对较弱,19年通过培训等方式提升整体前端开发的能力

    +
    +

    19年能力提升的工作做的太少了,没什么好说的,希望20年这方面工作能够投入更多的精力,让更多的小伙伴成长起来,让我们一起加油吧。

    +

    生活目标

    每天陪孩子读书一小时

    跟上面的工作和个人目标比起来,这个任务可以说完成度0%,这也是我觉得愧对家人的地方,为了工作,对家人的关注少了很多很多,以至于桐桐晚上都不愿意跟我一起睡觉,20年我要好好对她,做个好的爸爸,孩子明年就三岁了,去到幼儿园会认识更多的朋友,希望她会跟她的小伙伴说,我有个爱我的爸爸。

    +

    减肥

    减肥这件事可以说是真的对我来说太难了,看着自己马上破三位数(kg)的体重,心里无限难过,每次媳妇儿问我啥时候去健身,我都感到像被闪电击中了似的,办了健身卡被人嘲笑成了洗澡卡,20年,减不到150斤我就。。。。

    +

    写在最后

    19年太多的遗憾,太多的后悔莫及,当然也有一些成绩和自我肯定,19年已经过去了,希望在20年,能够成为一个更优秀的人。
    WechatIMG6.jpeg
    感谢我可爱的同事,年底收到了礼物真的很开心。

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/111/index.html b/page/111/index.html new file mode 100644 index 0000000000..faa22c334d --- /dev/null +++ b/page/111/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    公司于去年开始代码版本管理从SVN迁移到了Git,采用的GitLab程序做管理,但是自动构建采用的是Jekins,最近在给博客做自动构建的时候了解到的了Github的Actions,我就在想,是不是GitLab也有自己的自动构建能力,因为之前在参与项目的时候也看到GitLab上有CI/CD相关的内容,但是没有仔细的去看,趁这次机会就一起研究一下。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/112/index.html b/page/112/index.html new file mode 100644 index 0000000000..95f9a5702a --- /dev/null +++ b/page/112/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    最近被公司新来的前端问到了一些微信页面的兼容性问题,因为我做微信开发的经验比较少,被问到了真是感到很尴尬,所以在搜到了解决方案告诉了他之后,我又深入的再网上找了一些微信开发中遇到的兼容问题,特此整理,以留后用。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/113/index.html b/page/113/index.html new file mode 100644 index 0000000000..7ead95aca0 --- /dev/null +++ b/page/113/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    最近愈发觉得越想往上走,越不能局限在前端开发的领域,再往上走前后端都是通的,最近在知乎上看到一句话,说是到了架构师甚至专家的层面以后,就没有什么前端架构师或者前端专家和后端架构师、后端专家了,只有前端出身的技术专家,和后端出身的技术专家,越往上越注重人的综合能力,当一个人能够解决一个项目的技术选型、架构等工作后,对于公司或者团队来说,那就是个巨大的宝藏。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/114/index.html b/page/114/index.html new file mode 100644 index 0000000000..84c0bfc9a4 --- /dev/null +++ b/page/114/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    忙活了很久很久,终于不那么忙了,也终于想起来我还有个博客没有运行,哎,虽然用Hexo可以享受Github提供的免费的空间和域名,但是命令行敲代码的方式实现博客编写还是有些不方便,尤其是需要编程环境的时候,最近公司在用CICD的方式对代码进行构建,我就在想有没有一种方式让博客也可以变成这样,于是我就在网上搜Github Hexo 自动构建 这些个词,还真让我搜到了一种方法而且还是Github自己提供的,下面隆重请出 GitHub Actions

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/115/index.html b/page/115/index.html new file mode 100644 index 0000000000..835aea1bd3 --- /dev/null +++ b/page/115/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    公司这两年兴起了前后端分离的热潮,在选用三大技术框架时,vue凭借其学习曲线低深受公司程序员的喜爱,因此我们也就走上了vue学习的不归路,在招聘招揽小伙伴时有那么一些问题经常被问起,vue组件间通信就是其中之一,下面我们来聊一聊vue组件通信有哪些方式。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/116/index.html b/page/116/index.html new file mode 100644 index 0000000000..71dd7882c7 --- /dev/null +++ b/page/116/index.html @@ -0,0 +1,554 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    指令表

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    指令含义备注
    git add .提示增加文件.代表所有
    git commit -m“说明内容” 提交到本地服务器
    git status显示修改信息
    git pull从网络服务器拉 更新最新版本
    git push上传最新版本
    git branch查看当前分支
    git checkout develop切换到develop模式
    git merge master从master合并过来
    git push origin develop提交
    git clone git@192.168.2.10:bat-web.git从服务器克隆
    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/117/index.html b/page/117/index.html new file mode 100644 index 0000000000..0f7f7af36b --- /dev/null +++ b/page/117/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    近日工作不是那么忙,所以有时间看看书,这本书之前就已经读过一遍,隔了一段时间已经有所忘记,所以再次拾起,重新回顾一下。.

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/118/index.html b/page/118/index.html new file mode 100644 index 0000000000..44c32dfda1 --- /dev/null +++ b/page/118/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    VSCode是新一代IDE,方便灵活的插件安装方式让它强大无比,对前端友好,让它成为了越来越多前端开发者的首选IDE,编辑器虽好,但是要自己安装符合自己需求的插件才行,下面介绍一款插件,名叫ESLint,是用来静态检测js代码的工具,让你的js代码在编辑过程中就能找到错误,提高代码质量,好了废话不多说下面介绍如何安装和配置这款插件。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/119/index.html b/page/119/index.html new file mode 100644 index 0000000000..8eb76889e2 --- /dev/null +++ b/page/119/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    上篇文章介绍了如何利用github+hexo+next制作一个博客系统,但是由于这个博客系统是基于静态页面的,所以无法完成评论功能,博客怎么可以没有评论功能呢,当然next主题提供了几个选项,但是国内你知道的,提供的几个评论功能都不是太好用,于是就上网找了好多资料,功夫不负有心人,让我找到了一个名曰gitment的基于github-issue的评论插件,下面就介绍大家如何使用这个插件。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/12/index.html b/page/12/index.html new file mode 100644 index 0000000000..5105959717 --- /dev/null +++ b/page/12/index.html @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    这里假设要从Linux服务器A中把文件 file.txt 传输到Linux服务器B,该怎么做,这里就可以用scp实现
    为了能够成功的传输文件,需要做到以下几点:

    +
      +
    1. A需要知道要传输的的文件本地的路径
    2. +
    3. A为了往B传输文件,所以需要知道B的访问权限,因此需要B的访问地址,账号和私钥
    4. +
    5. 目标路径
    6. +
    7. 因为Linux权限管理的要求,私钥的访问权限需要设定为600
    8. +
    +

    当然还有一点前提是A和B两台服务器之间本身是可以通信的
    知道以上信息便可以进行文件传输

    +

    实践

    现在假设

    +
      +
    1. A要往B传输的文件在A的如下目录:/tmp/transfer/file.txt
    2. +
    3. B的访问地址是 192.168.31.31 访问用户名是 testuser 用到的key上传到了A的如下目录: /tmp/ssh/test.key
    4. +
    5. 目标路径为B的如下目录: /tmp/test
      知道上面信息后我们来创建命令。
    6. +
    +
    1
    2
    chomd 600 /tmp/ssh/test.key
    scp -i /tmp/ssh/test.key /tmp/transfer/file.txt testuser@192.168.31.31 /tmp/test
    + +

    通过上面的命令即可实现从A服务器传输文件到B服务器了。

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/120/index.html b/page/120/index.html new file mode 100644 index 0000000000..3bb1cf3e70 --- /dev/null +++ b/page/120/index.html @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    +

    开博客貌似并不是一件难事,现在有很多的方式开博客,而且现在博客貌似也不再流行,但是作为一个开发者,维护一个技术博客对自己的发展是很有好处的,出名者如阮一峰等,所以还是想要做一个技术博客的。

    +
    +
    +

    开博客可以选择网上现有的博客系统如博客园以及其他各大公司提供的博客系统,也可以选择如wordpress等博客程序自行搭建博客系统,前者省事,但可自定义设置不足不符合一个爱折腾的人的性格,后者复杂些,爱折腾的可以学着搭建,但是服务器域名是个让人头疼的事,还有数据库等操作,着实麻烦,如今又有了另外一个选择,那就是利用GitHub和一些静态的博客系统搭建一个纯静态的博客系统,不花钱又能折腾,而且还是命令行下的,装B感觉棒棒的,另外搭配时下流行的MarkDown语法,做笔记正合我意。

    +
    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/121/index.html b/page/121/index.html new file mode 100644 index 0000000000..10d766cba1 --- /dev/null +++ b/page/121/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    最近公司技术选型选择了VUE,下面就记录一下在学习Vue中遇到的一些知识点,有些在官方手册里已经讲的很详细了,这里就简单带过,主要对cli产生的以及自行添加的一些项目目录进行一下解释,希望对正在学习Vue的同学提供一点帮助,如果本文有谬误之处还请各位指出,谢谢各位支持。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/122/index.html b/page/122/index.html new file mode 100644 index 0000000000..328d36a5b5 --- /dev/null +++ b/page/122/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    现代化的IDE已经把开发者变得越来越懒,但是我觉得这样挺好的,让工具完成手工反复重复的工作,提高工作效率的同时降低开发者的疲劳感,Vue VSCode Snippets就是这样一个VSC插件,它可以用简单的几个字母就敲出一整块代码片段,在学习和做VUE项目时可以极大地提高工作效率,下面就介绍一下插件的常见命令。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/123/index.html b/page/123/index.html new file mode 100644 index 0000000000..44a1429c8d --- /dev/null +++ b/page/123/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    近两年前端突然呈爆发增长之势,node的出现起了重要的作用,而node之中有一个重要的功能就是npm包管理工具,下面就记录一下我学习npm之中遇到的一些问题及解决方案,仅供各位参考。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/124/index.html b/page/124/index.html new file mode 100644 index 0000000000..b08ab3cc73 --- /dev/null +++ b/page/124/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    作为前端,浏览器就是你的战场,而不同的浏览器就是不同的敌人,敌人有相同之处,也各有特点,这不同的特点就是不同浏览器的兼容性,下面就聊聊浏览器的兼容问题,如何解决不同浏览器的兼容性。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/125/index.html b/page/125/index.html new file mode 100644 index 0000000000..9f02225294 --- /dev/null +++ b/page/125/index.html @@ -0,0 +1,604 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    昨天花了1天的时间读了《晨间日记的奇迹》,感觉整个人的认识发生了一些变化,下面把重要内容做一下摘抄总结。

    +

    早上写日记的好处 —— 提升效率

      +
    • 可以做好一天的准备 — 计划性
    • +
    • 可以正确的写出昨天发生的事 — 效率性&忠诚性
    • +
    • 可以中立的看待昨天 — 中立性
    • +
    • 相对自由的时间 — 持续性
    • +
    • 总结经验 — 活用性
    • +
    +

    注意事项

    日记 不等于 日志
    日记 要记录 时间 天气 心情 — 因为这些有助于回忆和记录
    不要投入过长时间 — 3分钟 — 日记私密性
    晨间日记前7天只写好事 — 反省固然重要,但是成长更重要,持续性很重要 — 巴普洛夫的狗

    +

    晨间日记2部分

    Part1

    客观记录已经发生的事(昨天)— 经验智慧

    +

    Part2

      +
    • 今天应做的事 — 具体行动(来自昨天的总结
    • +
    • 今天一定要做的事(紧急重要的事)— 变得更积极更主动
    • +
    • 未来要做的事 — 不紧急但重要的事
    • +
    • 连用日记 — 历史上的今天(过去一年同一天的事)
    • +
    +

    夜晚日记 VS 晨间日记

    受当天情绪影响 — 更冷静

    +

    梦想成真表

    + + + + + + + + + + + + + + + + + +
    过去未来
    事实IQ 智慧指数NQ 人际关系指数
    感情EQ 情感指数DQ 梦想指数
    +
      +
    • IQ — 晨间日记冷静回忆分析 — 提高决策正确性 IQ up
    • +
    • EQ — 冷静记录昨日的情绪/自省 — 变积极 EQ up
    • +
    • NQ — 总结昨天给今天设立“对/为XXX做某事” 很重要 NQ up
    • +
    • DQ — 不能只有眼前的事 每天都要确定一下自己的梦想 脸皮厚点没关系 重复或只有一句话也没关系 铭记于心最重要
    • +
    +

    “忙碌”只是借口!“决心”才是问题! 对未来充满期待的人才有行动力

    +

    如何早起

      +
    • 设定起床音乐/用气味(精油)刺激起床/光线-电动窗帘早起自动拉开
    • +
    • 早起后淋浴 — 配合音乐/冥想/呐喊 — 让自己充满动力保持清醒
    • +
    +

    写日记的五大好处

      +
    • 提升写作能力
    • +
    • 谈话题材源源不断
    • +
    • 提高贵人运
    • +
    • 返现自我肉体和精神的状态与模式
    • +
    • 在自己身上挖宝,彻底改变人生
    • +
    +

    记录的日记要常拿出来看看

    记录日记时 — 问问自己的灵魂 然后写下真心话 — 灵魂日记
    六度空间理论

    +

    七种成功者的习惯

      +
    • 习惯之一:积极主动别指望谁能推你走 — 主动行动,迈出第一步
    • +
    • 习惯之二:以终为始忠于自己的人生计划。— 设立人生目标并坚持
    • +
    • 习惯之三:要事第一选择当前该做的事
    • +
    • 习惯之四:追求双赢远离角斗场
    • +
    • 习惯之五:善于沟通换位思考的原则
    • +
    • 习惯之六:统合综效 1+1可以大于2
    • +
    • 习惯之七:不断更新全方位平衡自我
    • +
    +

    早睡是为了身体,早起是为了我们的内心。— sugiponn

    +

    晨间日记的格式

    晨间日记必须要设立的栏目 — 工作/金钱/健康/人际关系
    要记下当日的日期/天气/温度/湿度

    +

    纬度标签
    工作方面:

    +
      +
    • 能力开发/经营/管理/进货/工作人员/学习/资格/活动/企划案/想法/推广
      金钱方面:
    • +
    • 收入/指出/购入/股票/资产/储蓄/家用
      健康方面:
    • +
    • 饮食/运动/性/减肥/身体状况/锻炼身体/体重/脂肪
      人际关系方面:
    • +
    • 父母/家人/小孩/亲戚/情人/工作伙伴/朋友/熟人/联络事项/人脉网络
      兴趣方面以及其他:
    • +
    • 义工活动/阅读/运动/打扮/电影/音乐/游戏/喜欢的物品/今天必须要注意的事/美容/开心的事/购物
    • +
    +

    劳动 — 职业 — 工作 — 乐趣

    +

    三大原则和七大作战守则

      +
    • 原则1:时间不超过3分钟 — 减少养成习惯的成本

      +
    • +
    • 原则2:决定好写晨间日记的地方 — 为了养成习惯

      +
    • +
    • 原则3:只写一个字也没关系 — 不要有压力

      +
    • +
    • 作战守则1:开心写日记 — 反省固然重要,鼓励更有作用 — 开始写晨间日记前七天只写好事

      +
    • +
    • 作战守则2:ONE-TWO作战 — 把动作分布容易养成习惯 — 形成固定动作

      +
    • +
    • 作战守则3:巴普洛夫的狗 — 开始写日记时候要有一个仪式 — 心理暗示

      +
    • +
    • 作战守则4:奖励自己 — 当习惯养成并坚持到一定时间,给自己一个奖励

      +
    • +
    • 作战守则5:宣告天下 — 借由他人的鼓励或压力形成动力/也可找到同志

      +
    • +
    • 作战守则6:一千日作战法 — 不管什么样的习惯坚持1000日就会成为永远的习惯 — 具体做法为记录 第XXX篇 晨间日记

      +
    • +
    • 作战守则7:为了别人 — 写晨间日记不只是为了自己,而且更是为了别人,身边人的幸福最终会影响到自己

      +
    • +
    +

    应该先肯定自己,给自己打100分

      +
    • “总之,先… 病” 没有目标/没有梦想 — 回顾时一定会后悔!
    • +
    • “假如…,应该 病” 无法满足/没有动力 — 焦躁不安
    • +
    • 共同点:失落感/空虚/
    • +
    +

    解决办法— 设立一个情境

    例:我要建立一个幸福的家庭 —> 因此我要创造很多快乐的回忆 —> 为了制造很多快乐的回忆 —> 我必须要到很多的地方去看看 —> 为了去很多地方看看,我觉得有辆车会更方便 —> 买车 —> 考驾照

    +

    不要总想自己哪里欠缺,要想自己想要得到什么,并且为了目标努力,你就会变的更完美。
    拥有一个敢于大声喊出的梦想,不要羞涩,要为了梦想一步步的前进,最终达到梦想。

    +

    提到的另外的书

    《培育梦想种子》《日记的力量》《成功人士的七个习惯》

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/126/index.html b/page/126/index.html new file mode 100644 index 0000000000..25df5040e6 --- /dev/null +++ b/page/126/index.html @@ -0,0 +1,696 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    最近读了《让老板提拔你》,下面将书中觉得有用的内容进行摘抄,放在下方。

    +

    定位自己

    正确认识自己,确定社会定位、职业定位。 定位-决定-定价

    +

    要素

    核心竞争力职位 契合度 是高薪关键所在

    +

    契合度

      +
    • 技能、专长、经历与职位要求的契合度
    • +
    • 专业资质和等级与职位要求的契合度
    • +
    • 综合素质与职位要求的契合度

      七大秘诀

    • +
    • 了解同行业薪酬的平均水平
    • +
    • 赢得未来单位的心
    • +
    • 先让对方开口
    • +
    • 勇敢地开口要求
    • +
    • 不要轻言放弃
    • +
    • 把握时机很重要
    • +
    • 说实话,别撒谎

      如何谈薪资

    • +
    • 商洽薪酬的关键在于把握好谈话时机,切勿开门见山。
      +

      只要有发展机会,我愿意接受贵单位的薪酬标准,不知道按规定这个工作岗位的薪酬标准是多少。

      +
      +
    • +
    +

    将知识卖个好价钱

    推销自己既推销自己所掌握的知识即智慧,正确认识自己的价值是关键。

    +

    高薪是因为“物有所值”

      +
    • 用业绩、用能力说话,是人才坦然面对高薪的心态。
    • +
    • 高薪人才的求职方式 —- 圈内朋友引见、定向投简历、行业论坛、交流活动等。
    • +
    • 高级人才不能经常跳槽要保持相对的职业稳定。—- 在一家公司少则3年多则6年 —- 接受挑战,把握机会。
    • +
    +

    失败丰富走向成功经验

    强调在失败中吸取的经验,在未来中可以避免的损失。

    +

    能为企业带来丰厚的利润才是人才

    企业招聘员工的唯一目的:追求公司业绩最大化、利润最大化。

    +

    高质高效工作全攻略

      +
    • 进行正确的自我评价
    • +
    • 做最擅长做的事
        +
      • 三个经济原则 —- 发挥人才优势。
          +
        1. 比较利益原则—–自己的专长对自己才是最有利的,勿与他人作比较。
        2. +
        3. 机会成本原则—–选择一份工作放弃其他工作就是机会成本。
        4. +
        5. 效率原则——–工作的成功不在于时间有多长,而在于成效有多少,附加值有多少。
        6. +
        +
      • +
      +
    • +
    • 马上行动
    • +
    • 做事情要雷厉风行 ——— 成功属于踏实肯干的人
    • +
    • 有条不紊地开展工作 ——- 制定时间计划
    • +
    • 善于利用现代办公工具
    • +
    • 给自己最大的工作空间
    • +
    • 建立高效有序的办公环境
    • +
    • 不要忘记最初想去的方向
    • +
    • “聪明”的向上级提出建议
    • +
    • 专心做事,避免浮躁
    • +
    • 多而不专,一事难成
    • +
    • “专”才有高质工作——–人才主要指专才–某个领域的高精尖人才–勿做全才,一专多能
    • +
    • 做事要有条理
    • +
    +

    不要只把自己当成一个打工仔

    +

    要把工作当成事业

    +
    +
      +
    • 工作不仅仅是为了钱
    • +
    • 对工作要有明确的价值取向
        +
      1. 认清人生的方向
      2. +
      3. 开始学会醉卧探索和认知
      4. +
      5. 认清工作价值与成就的关系
      6. +
      7. 长期的工作规划
      8. +
      9. 在生命的天平上衡量自身的价值
      10. +
      +
    • +
    • 巧妙应对与上司看法向左时的三条准则
        +
      1. 遇事考虑全局
      2. +
      3. 辩证地看待问题
      4. +
      5. 切记感情用事
      6. +
      +
    • +
    • 把单位的事当成自家的事
    • +
    • 认真负责地用心工作
    • +
    • 珍惜岗位,热爱自己的职业
    • +
    • 永远是在为自己工作
    • +
    • 敬重自己的工作
    • +
    • 不要轻视薪水微薄的工作
    • +
    • 永远对工作充满激情
    • +
    • 以自己的工作为荣
    • +
    • 不要被他人的观点所束缚
    • +
    • 暂时的胜负并不会决定人生的最后走向
    • +
    • 将弱势转化为优势
    • +
    • 全力以赴做好每一天的工作
    • +
    • 和优秀的人士在一起—见贤思齐、借梯爬楼
        +
      • 如何争取跟优秀的人在一起
          +
        1. 不断的抛头露面
        2. +
        3. 帮助可以帮助自己成就事业的人做事
        4. +
        5. 与上司和比自己优秀的人士一起合作
        6. +
        +
      • +
      +
    • +
    +
      +
    1. 尊重对方,严谨有致
    2. +
    3. 切记奉承,要不卑不亢
    4. +
    5. 态度自然,不必拘谨
    6. +
    7. 陪衬得当,不可狂妄
    8. +
    9. 主动真诚,做出姿态
    10. +
    11. 求助求教,接受呵护
    12. +
    +
      +
    • 挑战自我,承担责任
        +
      • 三条忠告
          +
        1. 全心全意工作
        2. +
        3. 把自己视为合伙人
        4. +
        5. 迎接变革的需求
        6. +
        +
      • +
      +
    • +
    • 自信独立,不随波逐流
    • +
    • 敢于显示自己很重要
    • +
    • 千万不能只知道抱怨上司
    • +
    • 保持严谨认真的做事习惯
    • +
    • 自主地做好手中的工作
    • +
    • 踏踏实实地做好本职工作
    • +
    • 丢掉工作散漫的坏习惯
    • +
    • 不要让浮躁的性格困扰自己
    • +
    • 不推诿,勇于承担责任
    • +
    • 无论如何都不要拖延工作
    • +
    • 糊弄工作只能是在糊弄自己
    • +
    • 逊色的工作只会淘汰自己
    • +
    • 千万别丢掉“得宠”之资
    • +
    • “一步登天”只会摔疼自己
    • +
    • 别让“差不多”贻误了自己
    • +
    • 能完成100%,就决不做99%
    • +
    +

    与上司相处

      +
    • 不要做上司的“心腹”
    • +
    • 适时恰当的赞美上司
        +
      • 赞美上司,还要善于选择适当的场合
      • +
      • 赞美上司,要学会巧借公众语言称赞
      • +
      • 赞美上司,还要善于赞美不得志的上司
      • +
      +
    • +
    • 主动与领导沟通
    • +
    • 主动和上司保持联系
    • +
    • 用“心机”主动接近上司
        +
      • 尽可能详细的了解上司
      • +
      • 选择一个与领导尽可能近的位置
      • +
      • 赢得上司青睐的方法
      • +
      +
    • +
    • 更有效的和上司沟通
        +
      • 与上司沟通要简洁
      • +
      • 与上司沟通要大度大气大方
      • +
      • 与上司沟通,就要把自己先放一边 ——– 说话对事不对人
      • +
      +
    • +
    • 四种和上司进行沟通的方法
        +
      1. 开诚布公式———提出问题,说出自己的理解认识,给出解决方案,寻求领导意思,执行领导的命令
      2. +
      3. 先斩后奏式———因主客观原因导致的没有先汇报而是先解决的事,事后要将问题的分析方法,解决方法,实施方案以及结果做一份详尽的总结报告上交给领导
      4. +
      5. “含情脉脉”式——-通过交流一些工作上的问题来含蓄表达出个人的建议、需要
      6. +
      7. 巧设比喻式———通过典型案例来暗示自己的意见和要求
      8. +
      +
    • +
    • 把话说到上司的心坎上
    • +
    • 巧妙地为领导拾遗补缺
        +
      1. 诠释领导讲话的难点
      2. +
      3. 强调领导的才干
      4. +
      5. 化严肃为幽默
      6. +
      7. 稳定情绪,委婉暗示
      8. +
      +
    • +
    • 工作中勤于请示汇报
        +
      1. 听懂上司的意图
      2. +
      3. 探讨、磨合,达成共识
      4. +
      5. 制定尽可能详尽的工作计划
      6. +
      7. 随时向上司汇报任务的关键点
      8. +
      9. 总结汇报
      10. +
      +
    • +
    • 用成功赢得上司的信任
    • +
    • 工作中不要冲撞上司
    • +
    • 处理好同上司之间的分歧
        +
      1. 圆融协调——领导不懂,下达了错误的指令
          +
        1. 私下向上司陈述意见,帮助上司做出正确的决策
        2. +
        3. 如果上司采纳了下属的建议或意见,该下属有责任长期保守秘密
        4. +
        5. 如果上司固执己见,那么无条件服从
        6. +
        +
      2. +
      3. 装聋作哑——不涉及到原则问题
      4. +
      5. 棘手难题多权衡
          +
        1. 立刻插话纠正
        2. +
        3. 提醒上司
        4. +
        5. 暗示
        6. +
        7. 事后补救
        8. +
        9. 事后提醒
        10. +
        +
      6. +
      +
    • +
    • 正确对待上司的批评
    • +
    • 要善于服从自己的上司
    • +
    • 正确化解来自上司的压力
    • +
    +

    写在最后

    博观约取,多读书读好书,丰富自己,变得睿智。

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/127/index.html b/page/127/index.html new file mode 100644 index 0000000000..cce2be5d62 --- /dev/null +++ b/page/127/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    利用CSS的border属性可以生成一些图形,例如三角或是圆角。纯粹的CSS2的内容,没有兼容性的问题,我之前在纯CSS实现各类气球泡泡对话框效果一文中算是比较详细的讲述了CSS border属性生成三角的原理,以及实例。我觉得此技术相当实用的,故本文再次简单叙述一下,另外,本文还将展示可能并不为众人所知的CSS border圆角生成技术。好了,裹脚布的话就不说了,直接进入正题。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/128/index.html b/page/128/index.html new file mode 100644 index 0000000000..a924838305 --- /dev/null +++ b/page/128/index.html @@ -0,0 +1,526 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    最近读了《高性能JAVASCRIPT》,下面将书中觉得有用的内容进行摘抄,放在下方。

    +

    如何从小事提升JAVASCRIPT性能。

      +
    1. <script>标签写在</body>之前——将脚本放在底部。

      +
    2. +
    3. 尽量少的<script>标签,减少页面中的外链数量,减少请求次数。

      +
    4. +
    5. 使用打包工具如:Yahoo!combo handler

      +
    6. +
    7. 使用动态延迟加载技术如:LazyLoad类库,LABjs

      +
      1
      2
      3
      4
      5
      6
      7
      // js代码
      <script type="text/javascript" src="lazyload-min.js"></script>
      <script type="text/javascript">
      LazyLoad.js([],function(){
      Application.init();
      })
      </script>
    8. +
    9. 在javascript中存储位置十分重要,尽量用局部变量代替对象成员的访问。

      +
    10. +
    11. 集合变数组提高查询效率

      +
      1
      2
      3
      4
      5
      6
      7
      // js代码
      function toArray(coll){
      for(var i = 0, a=[], len=coll.length; i<len; i++){
      a[i]=col[i];
      }
      return a;
      }
    12. +
    13. 使用局部变量缓存访问多次的成员
      当便利一个集合时,首要优化原则是把集合存储在局部变量中,并把length缓存在循环外部,然后使用局部变量访问这些需要多次访问的元素。

      +
    14. +
    15. 使用原生DOM方法querySelectorAll()遍历查找元素。

      +
    16. +
    17. 让元素脱离动画流,页面重排次数越多,程序响应越慢,要尽量减少重拍
      方法:

      +
    18. +
      1. +
      2. 使用绝对定位使元素脱离文档流
      3. +
      +
    19. +
    20. IE:hover
      在大量元素使用:hover这个伪css选择器时,程序会降低响应速率,在大表格或列表的情况下

      +
    21. +
    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/13/index.html b/page/13/index.html new file mode 100644 index 0000000000..2e0677f9aa --- /dev/null +++ b/page/13/index.html @@ -0,0 +1,582 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    nginx中的超时设置

    Nginx主要有四类超时设置:客户端超时设置DNS解析超时设置代理超时设置,如果使用ngx_lua,则还有lua相关的超时设置。

    +

    客户端超时设置

    对于客户端超时主要设置有读取请求头超时时间读取请求体超时时间发送响应超时时间长连接超时时间。通过客户端超时设置避免客户端恶意或者网络状况不佳造成连接长期占用,影响服务端的可处理的能力。

    +

    client_header_timeout 指定等待client发送一个请求头的超时时间

      +
    • 语法: client_header_timeout time
    • +
    • 默认值: 60s
    • +
    • 上下文: http server
    • +
    • 说明: 指定等待client发送一个请求头的超时时间(例如:GET / HTTP/1.1).仅当在一次read中,没有收到请求头,才会算成超时。如果在超时时间内,client没发送任何东西,nginx返回HTTP状态码408(“Request timed out”)
    • +
    +

    client_body_timeout 该指令设置请求体(request body)的读超时时间

      +
    • 语法: client_body_timeout time
    • +
    • 默认值: 60s
    • +
    • 上下文: http server location
    • +
    • 说明: 该指令设置请求体(request body)的读超时时间。仅当在一次readstep中,没有得到请求体,就会设为超时。超时后,nginx返回HTTP状态码408(“Request timed out”)
    • +
    +

    send_timeout time

    设置发送响应到客户端的超时时间,默认为60s,此超时时间指的也是两次成功写操作间隔时间,而不是发送整个响应的超时时间。如果在此超时时间内客户端没有接收任何响应,则Nginx关闭此连接。

    +

    keepalive_timeout 第一个参数指定了与client的keep-alive连接超时时间

      +
    • 语法: keepalive_timeout timeout [ header_timeout ]

      +
    • +
    • 默认值: 75s

      +
    • +
    • 上下文: http server location

      +
    • +
    • 说明: 第一个参数指定了与client的keep-alive连接超时时间。服务器将会在这个时间后关闭连接。可选的第二个参数指定了在响应头Keep-Alive: timeout=time中的time值。这个头能够让一些浏览器主动关闭连接,这样服务器就不必要去关闭连接了。没有这个参数,nginx不会发送Keep-Alive响应头(尽管并不是由这个头来决定连接是否“keep-alive”)
      两个参数的值可并不相同

      +
        +
      • 注意不同浏览器怎么处理“keep-alive”头
      • +
      • MSIE和Opera忽略掉”Keep-Alive: timeout=“ header.
      • +
      • MSIE保持连接大约60-65秒,然后发送TCP RST
      • +
      • Opera永久保持长连接
      • +
      • Mozilla keeps the connection alive for N plus about 1-10 seconds.
      • +
      • Konqueror保持长连接N秒
      • +
      +
    • +
    +

    DNS解析超时设置

    resolver_timeout 设置DNS解析超时时间

      +
    • 语法 resolver_timeout time
    • +
    • 默认值 30s
    • +
    • 上下文 http server location
    • +
    • 说明 该指令设置DNS解析超时时间
    • +
    +

    代理超时设置

    proxy_connect_timeout 设置与upstream server的连接超时时间,不能超过75秒

      +
    • 语法 proxy_connect_timeout time
    • +
    • 默认值 60s
    • +
    • 上下文 http server location
    • +
    • 说明 该指令设置与upstream server的连接超时时间,有必要记住,这个超时不能超过75秒。
      这个不是等待后端返回页面的时间,那是由proxy_read_timeout声明的。如果你的upstream服务器起来了,但是hanging住了(例如,没有足够的线程处理请求,所以把你的请求放到请求池里稍后处理),那么这个声明是没有用的,由于与upstream服务器的连接已经建立了。
    • +
    +

    proxy_read_timeout 设置与代理服务器的读超时时间

      +
    • 语法 proxy_read_timeout time
    • +
    • 默认值 60s
    • +
    • 上下文 http server location
    • +
    • 说明 该指令设置与代理服务器的读超时时间。它决定了nginx会等待多长时间来获得请求的响应。这个时间不是获得整个response的时间,而是两次reading操作的时间。
    • +
    +

    proxy_send_timeout 设置发送请求给upstream服务器的超时时间

      +
    • 语法 proxy_send_timeout time
    • +
    • 默认值 60s
    • +
    • 上下文 http server location
    • +
    • 说明 这个指定设置了发送请求给upstream服务器的超时时间。超时设置不是为了整个发送期间,而是在两次write操作期间。如果超时后,upstream没有收到新的数据,nginx会关闭连接
    • +
    +

    失败重试机制设置。

    proxy_next_upstream error | timeout | invalid_header | http_500 | http_502 | http_503 | http_504 |http_403 | http_404 | non_idempotent | off …:配置什么情况下需要请求下一台上游服务器进行重试。默认为“errortimeout”。error表示与上游服务器建立连接、写请求或者读响应头出错。timeout表示与上游服务器建立连接、写请求或者读响应头超时。invalid_header表示上游服务器返回空的或错误的响应头。http_XXX表示上游服务器返回特定的状态码。non_idempotent表示RFC-2616定义的非幂等HTTP方法(POST、LOCK、PATCH),也可以在失败后重试下一台上游服务器(即默认幂等方法GET、HEAD、PUT、DELETE、OPTIONS、TRACE才可以重试)。off表示禁用重试。

    +

    重试不能无限制进行,因此,需要如下两个指令控制重试次数和重试超时时间。

    +

    proxy_next_upstream_tries number:设置重试次数,默认0表示不限制,注意此重试次数指的是所有请求次数(包括第一次和之后的重试次数之和)。

    +

    proxy_next_upstream_timeout time:设置重试最大超时时间,默认0表示不限制。

    +

    即在proxy_next_upstream_timeout时间内允许proxy_next_upstream_tries次重试。如果超过了其中一个设置,则Nginx也会结束重试并返回客户端响应(可能是错误码)。

    +

    如下配置表示当error/timeout时重试upstream中的下一台上游服务器,如果重试的总时间超出了6s或者重试了1次,则表示重试失败(因为之前已经请求一次了,所以还能重试一次),Nginx结束重试并返回客户端响应。

    +

    upstream存活超时设置

    max_fails和fail_timeout:配置什么时候Nginx将上游服务器认定为不可用/不存活。当上游服务器在fail_timeout时间内失败了max_fails次,则认为该上游服务器不可用/不存活。并在接下来的fail_timeout时间内从upstream摘掉该节点(即请求不会转发到该上游服务器)。

    +

    什么情况下被认定为失败呢?其由 proxy_next_upstream定义,不过,不管 proxy_next_upstream如何配置,error, timeout and invalid_header 都将被认为是失败。

    +

    如server 192.168.61.1:9090max_fails=2 fail_timeout=10s;表示在10s内如果失败了2次,则在接下来的10s内认定该节点不可用/不存活。这种存活检测机制是只有当访问该上游服务器时,采取惰性检查,可以使用ngx_http_upstream_check_module配置主动检查。

    +

    max_fails设置为0表示不检查服务器是否可用(即认为一直可用),如果upstream中仅剩一台上游服务器时,则该服务器是不会被摘除的,将从不被认为不可用。

    +

    proxy_upstream_fail_timeout(fail_timeout)设置某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间

      +
    • 语法 server address [fail_timeout=30s]
    • +
    • 默认值 10s
    • +
    • 上下文 upstream
    • +
    • 说明 Upstream模块下 server指令的参数,设置了某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间,默认为10秒
    • +
    +

    lingering_timeout

      +
    • 语法: lingering_timeout time
    • +
    • 默认值: 5s
    • +
    • 上下文: http server location
    • +
    • 说明: lingering_close生效后,在关闭连接前,会检测是否有用户发送的数据到达服务器,如果超过lingering_timeout时间后还没有数据可读,就直接关闭连接;否则,必须在读取完连接缓冲区上的数据并丢弃掉后才会关闭连接。
    • +
    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/14/index.html b/page/14/index.html new file mode 100644 index 0000000000..8267cc873f --- /dev/null +++ b/page/14/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    前端常见知识点整理 —- 网络安全(2)

    SQL 注入

    SQL 注入漏洞(SQL Injection)是 Web 开发中最常见的一种安全漏洞。可以用它来从数据库获取敏感信息,或者利用数据库的特性执行添加用户,导出文件等一系列恶意操作,甚至有可能获取数据库乃至系统用户最高权限。

    +

    而造成 SQL 注入的原因是因为程序没有有效的转义过滤用户的输入,使攻击者成功的向服务器提交恶意的 SQL 查询代码,程序在接收后错误的将攻击者的输入作为查询语句的一部分执行,导致原始的查询逻辑被改变,额外的执行了攻击者精心构造的恶意代码。

    +

    很多 Web 开发者没有意识到 SQL 查询是可以被篡改的,从而把 SQL 查询当作可信任的命令。殊不知,SQL 查询是可以绕开访问控制,从而绕过身份验证和权限检查的。更有甚者,有可能通过 SQL 查询去运行主机系统级的命令。

    +

    SQL 注入原理

    下面将通过一些真实的例子来详细讲解 SQL 注入的方式的原理。

    +

    考虑以下简单的管理员登录表单:

    +
    1
    2
    3
    4
    5
    <form action="/login" method="POST">
    <p>Username: <input type="text" name="username" /></p>
    <p>Password: <input type="password" name="password" /></p>
    <p><input type="submit" value="登陆" /></p>
    </form>
    + +

    后端的 SQL 语句可能是如下这样的:

    +
    1
    2
    3
    4
    5
    6
    7
    let querySQL = `
    SELECT *
    FROM user
    WHERE username='${username}'
    AND psw='${password}'
    `;
    // 接下来就是执行 sql 语句...
    + +

    目的就是来验证用户名和密码是不是正确,按理说乍一看上面的 SQL 语句也没什么毛病,确实是能够达到我们的目的,可是你只是站在用户会老老实实按照你的设计来输入的角度来看问题,如果有一个恶意攻击者输入的用户名是 zoumiaojiang' OR 1 = 1 --,密码随意输入,就可以直接登入系统了。WFT!

    +

    冷静下来思考一下,我们之前预想的真实 SQL 语句是:

    +
    1
    SELECT * FROM user WHERE username='zoumiaojiang' AND psw='mypassword'
    + +

    可以恶意攻击者的奇怪用户名将你的 SQL 语句变成了如下形式:

    +
    1
    2
    SELECT * FROM user WHERE username='zoumiaojiang' OR 1 = 1 --' AND psw='xxxx'

    + +

    在 SQL 中,-- 是注释后面的内容的意思,所以查询语句就变成了:

    +
    1
    2
    SELECT * FROM user WHERE username='zoumiaojiang' OR 1 = 1

    + +

    这条 SQL 语句的查询条件永远为真,所以意思就是恶意攻击者不用我的密码,就可以登录进我的账号,然后可以在里面为所欲为,然而这还只是最简单的注入,牛逼的 SQL 注入高手甚至可以通过 SQL 查询去运行主机系统级的命令,将你主机里的内容一览无余,这里我也没有这个能力讲解的太深入,毕竟不是专业研究这类攻击的,但是通过以上的例子,已经了解了 SQL 注入的原理,我们基本已经能找到防御 SQL 注入的方案了。

    +

    如何预防 SQL 注入

    防止 SQL 注入主要是不能允许用户输入的内容影响正常的 SQL 语句的逻辑,当用户的输入的信息将要用来拼接 SQL 语句的话,我们应该永远选择不相信,任何内容都必须进行转义过滤,当然做到这个还是不够的,下面列出防御 SQL 注入的几点注意事项:

    +
      +
    • 严格限制Web应用的数据库的操作权限,给此用户提供仅仅能够满足其工作的最低权限,从而最大限度的减少注入攻击对数据库的危害

      +
    • +
    • 后端代码检查输入的数据是否符合预期,严格限制变量的类型,例如使用正则表达式进行一些匹配处理。

      +
    • +
    • 对进入数据库的特殊字符(',",\,<,>,&,*,; 等)进行转义处理,或编码转换。基本上所有的后端语言都有对字符串进行转义处理的方法,比如 lodash 的 lodash._escapehtmlchar 库。

      +
    • +
    • 所有的查询语句建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中,即不要直接拼接 SQL 语句。例如 Node.js 中的 mysqljs 库的 query 方法中的 ? 占位参数。

      +
    • +
    +
    1
    mysql.query(`SELECT * FROM user WHERE username = ? AND psw = ?`, [username, psw]);
    + +
      +
    • 在应用发布之前建议使用专业的 SQL 注入检测工具进行检测,以及时修补被发现的 SQL 注入漏洞。网上有很多这方面的开源工具,例如 sqlmap、SQLninja 等。

      +
    • +
    • 避免网站打印出 SQL 错误信息,比如类型错误、字段不匹配等,把代码里的 SQL 语句暴露出来,以防止攻击者利用这些错误信息进行 SQL 注入。

      +
    • +
    • 不要过于细化返回的错误信息,如果目的是方便调试,就去使用后端日志,不要在接口上过多的暴露出错信息,毕竟真正的用户不关心太多的技术细节,只要话术合理就行。

      +
    • +
    +

    碰到要操作的数据库的代码,一定要慎重,小心使得万年船,多找几个人多来几次 code review,将问题都暴露出来,而且要善于利用工具,操作数据库相关的代码属于机密,没事不要去各种论坛晒自家站点的 SQL 语句,万一被人盯上了呢?

    +

    命令行注入

    命令行注入漏洞,指的是攻击者能够通过 HTTP 请求直接侵入主机,执行攻击者预设的 shell 命令,听起来好像匪夷所思,这往往是 Web 开发者最容易忽视但是却是最危险的一个漏洞之一,看一个实例:

    +

    假如现在需要实现一个需求:用户提交一些内容到服务器,然后在服务器执行一些系统命令去产出一个结果返回给用户,接口的部分实现如下:

    +
    1
    2
    3
    4
    5
    // 以 Node.js 为例,假如在接口中需要从 github 下载用户指定的 repo
    const exec = require('mz/child_process').exec;
    let params = {/* 用户输入的参数 */};

    exec(`git clone ${params.repo} /some/path`);
    + +

    这段代码确实能够满足业务需求,正常的用户也确实能从指定的 git repo 上下载到想要的代码,可是和 SQL 注入一样,这段代码在恶意攻击者眼中,简直就是香饽饽。

    +

    如果 params.repo 传入的是 https://github.com/zoumiaojiang/zoumiaojiang.github.io.git 当然没问题了。
    可是如果 params.repo 传入的是 https://github.com/xx/xx.git && rm -rf /* && 恰好你的服务是用 root 权限起的就惨了。

    +

    具体恶意攻击者能用命令行注入干什么也像 SQL 注入一样,手法是千变万化的,比如「反弹 shell 注入」等,但原理都是一样的,我们绝对有能力防止命令行注入发生。防止命令行注入需要做到以下几件事情:

    +
      +
    • 后端对前端提交内容需要完全选择不相信,并且对其进行规则限制(比如正则表达式)。
    • +
    • 在调用系统命令前对所有传入参数进行命令行参数转义过滤。
    • +
    • 不要直接拼接命令语句,借助一些工具做拼接、转义预处理,例如 Node.js 的 shell-escape npm 包。
    • +
    +

    还是前面的例子,我们可以做到如下:

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    const exec = require('mz/child_process').exec;

    // 借助 shell-escape npm 包解决参数转义过滤问题
    const shellescape = require('shell-escape');

    let params = {/* 用户输入的参数 */};

    // 先过滤一下参数,让参数符合预期
    if (!/正确的表达式/.test(params.repo)) {
    return;
    }

    let cmd = shellescape([
    'git',
    'clone',
    params.repo,
    '/some/path'
    ]);

    // cmd 的值: git clone 'https://github.com/xx/xx.git && rm -rf / &&' /some/path
    // 这样就不会被注入成功了。
    exec(cmd);
    +

    无论是在何种后端语言环境中,凡是涉及到代码调用系统 shell 命令的时候都一定要谨慎。

    +

    DDoS 攻击

    DDoS 又叫分布式拒绝服务,全称 Distributed Denial of Service,其原理就是利用大量的请求造成资源过载,导致服务不可用,这个攻击应该不能算是安全问题,这应该算是一个另类的存在,因为这种攻击根本就是耍流氓的存在,「伤敌一千,自损八百」的行为。出于保护 Web App 不受攻击的攻防角度,还是介绍一下 DDoS 攻击吧,毕竟也是挺常见的。

    +

    DDoS 攻击可以理解为:「你开了一家店,隔壁家点看不惯,就雇了一大堆黑社会人员进你店里干坐着,也不消费,其他客人也进不来,导致你营业惨淡」。为啥说 DDoS 是个「伤敌一千,自损八百」的行为呢?毕竟隔壁店还是花了不少钱雇黑社会但是啥也没得到不是?DDoS 攻击的目的基本上就以下几个:

    +
      +
    • 深仇大恨,就是要干死你
    • +
    • 敲诈你,不给钱就干你
    • +
    • 忽悠你,不买我防火墙服务就会有“人”继续干你
    • +
    +

    也许你的站点遭受过 DDoS 攻击,具体什么原因怎么解读见仁见智。DDos 攻击从层次上可分为网络层攻击与应用层攻击,从攻击手法上可分为快型流量攻击与慢型流量攻击,但其原理都是造成资源过载,导致服务不可用。

    +

    网络层 DDoS

    网络层 DDos 攻击包括 SYN FloodACK FloodUDP FloodICMP Flood 等。

    +

    SYN Flood 攻击

    SYN flood 攻击主要利用了 TCP 三次握手过程中的 Bug,我们都知道 TCP 三次握手过程是要建立连接的双方发送 SYN,SYN + ACK,ACK 数据包,而当攻击方随意构造源 IP 去发送 SYN 包时,服务器返回的 SYN + ACK 就不能得到应答(因为 IP 是随意构造的),此时服务器就会尝试重新发送,并且会有至少 30s 的等待时间,导致资源饱和服务不可用,此攻击属于慢型 DDoS 攻击。

    +

    ACK Flood 攻击

    ACK Flood 攻击是在 TCP 连接建立之后,所有的数据传输 TCP 报文都是带有 ACK 标志位的,主机在接收到一个带有 ACK 标志位的数据包的时候,需要检查该数据包所表示的连接四元组是否存在,如果存在则检查该数据包所表示的状态是否合法,然后再向应用层传递该数据包。如果在检查中发现该数据包不合法,例如该数据包所指向的目的端口在本机并未开放,则主机操作系统协议栈会回应 RST 包告诉对方此端口不存在。

    +

    UDP Flood 攻击

    UDP flood 攻击是由于 UDP 是一种无连接的协议,因此攻击者可以伪造大量的源 IP 地址去发送 UDP 包,此种攻击属于大流量攻击。正常应用情况下,UDP 包双向流量会基本相等,因此发起这种攻击的攻击者在消耗对方资源的时候也在消耗自己的资源。

    +

    ICMP Flood 攻击

    ICMP Flood 攻击属于大流量攻击,其原理就是不断发送不正常的 ICMP 包(所谓不正常就是 ICMP 包内容很大),导致目标带宽被占用,但其本身资源也会被消耗。目前很多服务器都是禁 ping 的(在防火墙在可以屏蔽 ICMP 包),因此这种攻击方式已经落伍。

    +

    网络层 DDoS 防御

    网络层的 DDoS 攻击究其本质其实是无法防御的,我们能做得就是不断优化服务本身部署的网络架构,以及提升网络带宽。当然,还是做好以下几件事也是有助于缓解网络层 DDoS 攻击的冲击:

    +
      +
    • 网络架构上做好优化,采用负载均衡分流。
    • +
    • 确保服务器的系统文件是最新的版本,并及时更新系统补丁。
    • +
    • 添加抗 DDos 设备,进行流量清洗。
    • +
    • 限制同时打开的 SYN 半连接数目,缩短 SYN 半连接的 Timeout 时间。
    • +
    • 限制单 IP 请求频率。
    • +
    • 防火墙等防护设置禁止 ICMP 包等。
    • +
    • 严格限制对外开放的服务器的向外访问。
    • +
    • 运行端口映射程序或端口扫描程序,要认真检查特权端口和非特权端口。
    • +
    • 关闭不必要的服务。
    • +
    • 认真检查网络设备和主机/服务器系统的日志。只要日志出现漏洞或是时间变更,那这台机器就可能遭到了攻击。
    • +
    • 限制在防火墙外与网络文件共享。这样会给黑客截取系统文件的机会,主机的信息暴露给黑客,无疑是给了对方入侵的机会。
    • +
    • 加钱堆机器。。
    • +
    • 报警。。-
    • +
    +

    应用层 DDoS

    应用层 DDoS 攻击不是发生在网络层,是发生在 TCP 建立握手成功之后,应用程序处理请求的时候,现在很多常见的 DDoS 攻击都是应用层攻击。应用层攻击千变万化,目的就是在网络应用层耗尽你的带宽,下面列出集中典型的攻击类型。

    +

    CC 攻击

    当时绿盟为了防御 DDoS 攻击研发了一款叫做 Collapasar 的产品,能够有效的防御 SYN Flood 攻击。黑客为了挑衅,研发了一款 Challenge Collapasar 攻击工具(简称 CC)。

    +

    CC 攻击的原理,就是针对消耗资源比较大的页面不断发起不正常的请求,导致资源耗尽。因此在发送 CC 攻击前,我们需要寻找加载比较慢,消耗资源比较多的网页,比如需要查询数据库的页面、读写硬盘文件的等。通过 CC 攻击,使用爬虫对某些加载需要消耗大量资源的页面发起 HTTP 请求。

    +

    DNS Flood

    DNS Flood 攻击采用的方法是向被攻击的服务器发送大量的域名解析请求,通常请求解析的域名是随机生成或者是网络世界上根本不存在的域名,被攻击的DNS 服务器在接收到域名解析请求的时候首先会在服务器上查找是否有对应的缓存,如果查找不到并且该域名无法直接由服务器解析的时候,DNS 服务器会向其上层 DNS 服务器递归查询域名信息。域名解析的过程给服务器带来了很大的负载,每秒钟域名解析请求超过一定的数量就会造成 DNS 服务器解析域名超时。

    +

    根据微软的统计数据,一台 DNS 服务器所能承受的动态域名查询的上限是每秒钟 9000 个请求。而我们知道,在一台 P3 的 PC 机上可以轻易地构造出每秒钟几万个域名解析请求,足以使一台硬件配置极高的 DNS 服务器瘫痪,由此可见 DNS 服务器的脆弱性。

    +

    HTTP 慢速连接攻击

    针对 HTTP 协议,先建立起 HTTP 连接,设置一个较大的 Conetnt-Length,每次只发送很少的字节,让服务器一直以为 HTTP 头部没有传输完成,这样连接一多就很快会出现连接耗尽。

    +

    应用层 DDoS 防御

      +
    • 判断 User-Agent 字段(不可靠,因为可以随意构造)
    • +
    • 针对 IP + cookie,限制访问频率(由于 cookie 可以更改,IP 可以使用代理,或者肉鸡,也不可靠)
    • +
    • 关闭服务器最大连接数等,合理配置中间件,缓解 DDoS 攻击。
    • +
    • 请求中添加验证码,比如请求中有数据库操作的时候。
    • +
    • 编写代码时,尽量实现优化,并合理使用缓存技术,减少数据库的读取操作。
    • +
    • 加钱堆机器。。
    • +
    • 报警。。
    • +
    +

    应用层的防御有时比网络层的更难,因为导致应用层被 DDoS 攻击的因素非常多,有时往往是因为程序员的失误,导致某个页面加载需要消耗大量资源,有时是因为中间件配置不当等等。而应用层 DDoS 防御的核心就是区分人与机器(爬虫),因为大量的请求不可能是人为的,肯定是机器构造的。因此如果能有效的区分人与爬虫行为,则可以很好地防御此攻击。

    +

    其他 DDoS 攻击

    发起 DDoS 也是需要大量的带宽资源的,但是互联网就像森林,林子大了什么鸟都有,DDoS 攻击者也能找到其他的方式发起廉价并且极具杀伤力的 DDoS 攻击。

    +

    利用 XSS

    举个例子,如果 12306 页面有一个 XSS 持久型漏洞被恶意攻击者发现,只需在春节抢票期间在这个漏洞中执行脚本使得往某一个小站点随便发点什么请求,然后随着用户访问的增多,感染用户增多,被攻击的站点自然就会迅速瘫痪了。这种 DDoS 简直就是无本万利,不用惊讶,现在大站有 XSS 漏洞的不要太多。

    +

    来自 P2P 网络攻击

    大家都知道,互联网上的 P2P 用户和流量都是一个极为庞大的数字。如果他们都去一个指定的地方下载数据,成千上万的真实 IP 地址连接过来,没有哪个设备能够支撑住。拿 BT 下载来说,伪造一些热门视频的种子,发布到搜索引擎,就足以骗到许多用户和流量了,但是这只是基础攻击。
    高级的 P2P 攻击,是直接欺骗资源管理服务器。如迅雷客户端会把自己发现的资源上传到资源管理服务器,然后推送给其它需要下载相同资源的用户,这样,一个链接就发布出去。通过协议逆向,攻击者伪造出大批量的热门资源信息通过资源管理中心分发出去,瞬间就可以传遍整个 P2P 网络。更为恐怖的是,这种攻击是无法停止的,即使是攻击者自身也无法停止,攻击一直持续到 P2P 官方发现问题更新服务器且下载用户重启下载软件为止。

    +

    DDoS 最后总结

    DDoS 不可能防的住,就好比你的店只能容纳 50 人,黑社会有 100 人,你就换一家大店,能容纳 500 人,然后黑社会又找来了 1000 人,这种堆人头的做法就是 DDoS 本质上的攻防之道,「道高一尺,魔高一丈,魔高一尺,道高一丈」,讲真,必要的时候就答应勒索你的人的条件吧,实在不行就报警吧。

    +

    流量劫持

    流量劫持应该算是黑产行业的一大经济支柱了吧?简直是让人恶心到吐,不吐槽了,还是继续谈干货吧,流量劫持基本分两种:DNS 劫持 和 HTTP 劫持,目的都是一样的,就是当用户访问 zoumiaojiang.com 的时候,给你展示的并不是或者不完全是 zoumiaojiang.com 提供的 “内容”。

    +

    DNS 劫持

    DNS 劫持,也叫做域名劫持,可以这么理解,「你打了一辆车想去商场吃饭,结果你打的车是小作坊派来的,直接给你拉到小作坊去了」,DNS 的作用是把网络地址域名对应到真实的计算机能够识别的 IP 地址,以便计算机能够进一步通信,传递网址和内容等。如果当用户通过某一个域名访问一个站点的时候,被篡改的 DNS 服务器返回的是一个恶意的钓鱼站点的 IP,用户就被劫持到了恶意钓鱼站点,然后继而会被钓鱼输入各种账号密码信息,泄漏隐私。

    +

    这类劫持,要不就是网络运营商搞的鬼,一般小的网络运营商与黑产勾结会劫持 DNS,要不就是电脑中毒,被恶意篡改了路由器的 DNS 配置,基本上做为开发者或站长却是很难察觉的,除非有用户反馈,现在升级版的 DNS 劫持还可以对特定用户、特定区域等使用了用户画像进行筛选用户劫持的办法,另外这类广告显示更加随机更小,一般站长除非用户投诉否则很难觉察到,就算觉察到了取证举报更难。无论如何,如果接到有 DNS 劫持的反馈,一定要做好以下几件事:

    +
      +
    • 取证很重要,时间、地点、IP、拨号账户、截屏、URL 地址等一定要有。
    • +
    • 可以跟劫持区域的电信运营商进行投诉反馈。
    • +
    • 如果投诉反馈无效,直接去工信部投诉,一般来说会加白你的域名。
    • +
    +

    HTTP 劫持

    HTTP 劫持您可以这么理解,「你打了一辆车想去商场吃饭,结果司机跟你一路给你递小作坊的广告」,HTTP 劫持主要是当用户访问某个站点的时候会经过运营商网络,而不法运营商和黑产勾结能够截获 HTTP 请求返回内容,并且能够篡改内容,然后再返回给用户,从而实现劫持页面,轻则插入小广告,重则直接篡改成钓鱼网站页面骗用户隐私。能够实施流量劫持的根本原因,是 HTTP 协议没有办法对通信对方的身份进行校验以及对数据完整性进行校验。如果能解决这个问题,则流量劫持将无法轻易发生。所以防止 HTTP 劫持的方法只有将内容加密,让劫持者无法破解篡改,这样就可以防止 HTTP 劫持了。

    +

    HTTPS 协议就是一种基于 SSL 协议的安全加密网络应用层协议,可以很好的防止 HTTP 劫持。这里有篇 文章 讲的不错。HTTPS 在这就不深讲了,后面有机会我会单独好好讲讲 HTTPS。如果不想站点被 HTTP 劫持,赶紧将你的站点全站改造成 HTTPS 吧。

    +

    服务器漏洞

    服务器除了以上提到的那些大名鼎鼎的漏洞和臭名昭著的攻击以外,其实还有很多其他的漏洞,往往也很容易被忽视,在这个小节也稍微介绍几种。

    +

    越权操作漏洞

    如果你的系统是有登录控制的,那就要格外小心了,因为很有可能你的系统越权操作漏洞,越权操作漏洞可以简单的总结为 「A 用户能看到或者操作 B 用户的隐私内容」,如果你的系统中还有权限控制就更加需要小心了。所以每一个请求都需要做 userid 的判断

    +

    以下是一段有漏洞的后端示意代码:

    +
    1
    2
    3
    4
    5
    6
    7
    // ctx 为请求的 context 上下文
    let msgId = ctx.params.msgId;

    mysql.query(
    'SELECT * FROM msg_table WHERE msg_id = ?',
    [msgId]
    );
    + +

    以上代码是任何人都可以查询到任何用户的消息,只要有 msg_id 就可以,这就是比较典型的越权漏洞

    +

    越权操作漏洞防御

    如下这么改进一下:

    +
    1
    2
    3
    4
    5
    6
    7
    8
    // ctx 为请求的 context 上下文
    let msgId = ctx.params.msgId;
    let userId = ctx.session.userId; // 从会话中取出当前登陆的 userId

    mysql.query(
    'SELECT * FROM msg_table WHERE msg_id = ? AND user_id = ?',
    [msgId, userId]
    );
    +

    嗯,大概就是这个意思,如果有更严格的权限控制,那在每个请求中凡是涉及到数据库的操作都需要先进行严格的验证,并且在设计数据库表的时候需要考虑进 userId 的账号关联以及权限关联。

    +

    目录遍历漏洞

    目录遍历漏洞指通过在 URL 或参数中构造 .././ 和类似的跨父目录字符串的 ASCII 编码、unicode 编码等,完成目录跳转,读取操作系统各个目录下的敏感文件,也可以称作「任意文件读取漏洞」。

    +

    目录遍历漏洞原理:程序没有充分过滤用户输入的 ../ 之类的目录跳转符,导致用户可以通过提交目录跳转来遍历服务器上的任意文件。使用多个.. 符号,不断向上跳转,最终停留在根 /,通过绝对路径去读取任意文件。

    +

    目录遍历漏洞几个示例和测试,一般构造 URL 然后使用浏览器直接访问,或者使用 Web 漏洞扫描工具检测,当然也可以自写程序测试。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    http://somehost.com/../../../../../../../../../etc/passwd
    http://somehost.com/some/path?file=../../Windows/system.ini

    # 借助 %00 空字符截断是一个比较经典的攻击手法
    http://somehost.com/some/path?file=../../Windows/system.ini%00.js

    # 使用了 IIS 的脚本目录来移动目录并执行指令
    http://somehost.com/scripts/..%5c../Windows/System32/cmd.exe?/c+dir+c:\

    +

    目录遍历漏洞防御

    方法就是需要对 URL 或者参数进行 ../,./ 等字符的转义过滤。

    +

    物理路径泄漏

    物理路径泄露属于低风险等级缺陷,它的危害一般被描述为「攻击者可以利用此漏洞得到信息,来对系统进一步地攻击」,通常都是系统报错 500 的错误信息直接返回到页面可见导致的漏洞。得到物理路径有些时候它能给攻击者带来一些有用的信息,比如说:可以大致了解系统的文件目录结构;可以看出系统所使用的第三方软件;也说不定会得到一个合法的用户名(因为很多人把自己的用户名作为网站的目录名)。

    +

    物理路径泄漏防御

    防止这种泄漏的方法就是做好后端程序的出错处理,定制特殊的 500 报错页面。

    +

    源码暴露漏洞

    和物理路径泄露类似,就是攻击者可以通过请求直接获取到你站点的后端源代码,然后就可以对系统进一步研究攻击。那么导致源代码暴露的原因是什么呢?基本上就是发生在服务器配置上了,服务器可以设置哪些路径的文件才可以被直接访问的,这里给一个 koa 服务起的例子,正常的 koa 服务器可以通过 koa-static 中间件去指定静态资源的目录,好让静态资源可以通过路径的路由访问。比如你的系统源代码目录是这样的:

    +
    1
    2
    3
    4
    5
    6
    |- project
    |- src
    |- static
    |- ...
    |- server.js

    + +

    你想要将 static 的文件夹配成静态资源目录,你应该会在 server.js 做如下配置:

    +
    1
    2
    3
    4
    5
    6
    const Koa = require('koa');
    const serve = require('koa-static');
    const app = new Koa();

    app.use(serve(__dirname + '/project/static'));

    + +

    但是如果配错了静态资源的目录,可能就出大事了,比如:

    +
    1
    2
    3
    // ...
    app.use(serve(__dirname + '/project'));

    + +

    这样所有的源代码都可以通过路由访问到了,所有的服务器都提供了静态资源机制,所以在通过服务器配置静态资源目录和路径的时候,一定要注意检验,不然很可能产生漏洞。

    +

    最后,希望 Web 开发者们能够管理好自己的代码隐私,注意代码安全问题,比如不要将产品的含有敏感信息的代码放到第三方外部站点或者暴露给外部用户,尤其是前端代码,私钥类似的保密性的东西不要直接输出在代码里或者页面中。也许还有很多值得注意的点,但是归根结底还是绷住安全那根弦,对待每一行代码都要多多推敲。

    +

    转载自:https://zoumiaojiang.com/article/common-web-security/

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/15/index.html b/page/15/index.html new file mode 100644 index 0000000000..3e56860e1f --- /dev/null +++ b/page/15/index.html @@ -0,0 +1,531 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    架构实践

    架构设计内容一览

    应用架构设计

      +
    • Transaction执行控制(在线/批处理路径和控制)
    • +
    • Session控制
    • +
    • 死锁控制
    • +
    • 接口处理流程(适配器设置/文件备份)
    • +
    • 命名规范(实例/SID等)
    • +
    • 中间件参数 (初始化参数等)
    • +
    • 认证(处理流程/错误处理)
    • +
    • 帐户(用户ID体系/权限管理/申请方式)
    • +
    +

    开发架构设计

      +
    • 系统景观(各种环境名称、利用方法等)
    • +
    • 转运路线(转运路线/转运工具/承认方式)
    • +
    • 开发账户(用户ID体系/权限管理/申请方式)
    • +
    • 开发终端设置(在线批处理开发工具/目录等)
    • +
    • 开发资源管理
    • +
    • 表单、协作工具的利用方法(表单/接口ID定义规则)
    • +
    • 开发和验证备份(处理流程/文件清除/周期)
    • +
    • 数据屏蔽(処理概要/屏蔽利用规则/対象/开发环境搭建方法)
    • +
    +

    运维架构设计

      +
    • 监测规范(方式/周期/対象)
    • +
    • 审计和错误日志(日志级别/日志保留时效/删除时效)
    • +
    • 生产环境和调研环境的备份(处理流程/删除时效/周期)
    • +
    • 加密处理(安装位置、加密规则、记录水平加密方法)
    • +
    • 云安全设定
    • +
    • 增加功能设计
    • +
    +

    基础设施

      +
    • 网络设计(虚拟网/子网/URL/负载均衡访问控制/白名单)
    • +
    • 云设计(实例/NFS/IAM)
    • +
    • 存储设计 (BlockVolume/FileStorage/ObjectStorage/数据配置/数据圧縮方法)
    • +
    • 时间DNS同步化(OCI設定)
    • +
    • OS参数设定
    • +
    • Docker参数设定
    • +
    • 高可用切换 (処理方式、设定参数考量)
    • +
    • 打补丁(打补丁/执行规范)
    • +
    • 虚拟桌面设定
    • +
    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/16/index.html b/page/16/index.html new file mode 100644 index 0000000000..04db3b9db5 --- /dev/null +++ b/page/16/index.html @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    ngx_http_stub_status_module 是一个 Nginx 的内置 模块,可以提供 Nginx 的简单统计信息。默认情况下这个模块是不被编译进来的,所以在编译 Nginx 时要指定加载该模块:

    +
    1
    --with-http_stub_status_module
    +

    查看是否安装

    1
    2
    3
    4
    5
    // 查看nginx版本及安装了那些模块
    nginx -V

    // 如果安装了的话会显示下面的信息
    --with-http_stub_status_module
    + +

    配置使用

    nginx.conf配置文件中进行如下配置

    +
    1
    2
    3
    location /ngx_status {
    stub_status on; // 设置开启性能模块
    }
    +

    通过浏览器访问 http://localhost/ngx_status 就能看到你的nginx服务性能监控画面了
    image

    +

    解析

      +
    • 第一行:Active connections:活动连接数
    • +
    • 第二三行:Server accepts handled requests:(三个数字分别代表)总共处理的连接数,成功握手的连接数量,处理的请求数(正常情况下握手和连接数是相等的,表示没有丢失)
    • +
    • 第四行:
        +
      • Reading: Nginx 读取到客户端的Header信息数
      • +
      • Writing: Nginx 返回给客户端的Header信息数
      • +
      • Waiting: 开启keep-alive的情况下,这个值等于 active – (reading + writing),意思就是Nginx已经处理完成,正在等候下一次请求指令的驻留连接(在nginx开启了keep-alive,也就是长连接的情况下,客户端跟服务端建立了连接但是没有读写操作的空闲状态)
      • +
      +
    • +
    +

    使用场景

      +
    • 可以简单的用脚本做监控
    • +
    • 可以用开源工具,zabbix,prometheus等去采集nginx的统计信息,做监控和历史数据采集
    • +
    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/17/index.html b/page/17/index.html new file mode 100644 index 0000000000..8d1fab6756 --- /dev/null +++ b/page/17/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    HexoGithub发布之后自定义域名的配置

    进到你的博客发布所在仓库,如:

    选择Settings,然后是pages,滚动页面找到Custom domain,将你自己的域名绑定进去保存一下就好了。

    +

    在你自己的域名商处用CNAME把你自己的域名指定到你自己的仓库page地址上比如我的就是lixuguang.github.io Public,这样你的GitHub Page就可以通过自己的域名进行访问了。

    +

    HexoGithub发布之后自定义域名会被清空的问题

    使用 GitHub Page 功能将博客托管在了 GitHub 上,并配置 CNAME 将自己的域名解析了过去,但是发现一个问题,每次 hexo deploy 之后,custom domain 会被重置失效。

    +

    解决方案

    Hexo 生成的博客的 source 目录下(注意这个是你的博客源码所在仓库,并不是发布的仓库)新建一个 CNAME 文件,然后在这个文件中填入你的域名,这样就不会每次发布之后,GitHub Page 里的 custom domain 都被重置掉啦。

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/18/index.html b/page/18/index.html new file mode 100644 index 0000000000..d0d17b4cb6 --- /dev/null +++ b/page/18/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    Nginx指令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    #启动Nginx
    start nginx

    #重启Nginx
    nginx -s reopen

    #重新加载Nginx配置文件,然后以优雅的方式重启Nginx
    nginx -s reload

    #强制停止Nginx服务
    nginx -s stop

    #优雅地停止Nginx服务(即处理完所有请求后再停止服务)
    nginx -s quit

    #检测配置文件是否有语法错误,然后退出
    nginx -t

    #显示版本信息并退出
    nginx -v

    #显示版本和配置选项信息,然后退出
    nginx -V

    #检测配置文件是否有语法错误,然后退出
    nginx -t

    #检测配置文件是否有语法错误,转储并退出
    nginx -T

    #在检测配置文件期间屏蔽非错误信息
    nginx -q

    #打开帮助信息
    nginx -?,-h

    #设置前缀路径(默认是:/usr/share/nginx/)
    nginx -p prefix

    #设置配置文件(默认是:/etc/nginx/nginx.conf)
    nginx -c filename

    #设置配置文件外的全局指令
    nginx -g directives
    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/19/index.html b/page/19/index.html new file mode 100644 index 0000000000..57e9714c82 --- /dev/null +++ b/page/19/index.html @@ -0,0 +1,545 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    Docker 命令

      +
    • 帮助命令
      docker –help
    • +
    +

    镜像操作

      +
    • 搜索镜像
      docker search hello-world

      +
    • +
    • 下载镜像
      docker pull hello-world

      +
    • +
    • 查看本地已下载所有镜像
      docker images

      +
    • +
    • 查看镜像历史
      docker history hello-world

      +
    • +
    • 备份镜像
      docker tag hello-word:last hello-world:v2

      +
    • +
    • 删除镜像
      docker rmi hello-word:last

      +
    • +
    • 删除未使用过的镜像
      docker image prune

      +
    • +
    • 导出镜像
      docker save -o hello-world:last.tar hello-world:last

      +
    • +
    • 导入镜像
      docker load -i hello-world:last.tar

      +
    • +
    • 查看镜像信息
      docker image inspact nginx

      +
    • +
    +

    容器操作

      +
    • 查看所有容器 [-q 编号] [-a active 启动的容器]
      docker ps [-a] [-q]

      +
    • +
    • 启动容器 [-d 后台启动]
      docker run -d –name nginx1 nginx:last

      +
    • +
    • 停止容器
      docker stop nginx1

      +
    • +
    • 启动容器()
      docker start nginx1

      +
    • +
    • 删除容器
      docker rm nginx1

      +
    • +
    • 批量删除运行中容器
      docker rm $(docker ps -q) -f

      +
    • +
    • 创建容器并进入
      docker run -it –name nginx1 nginx:last /bin/bash

      +
    • +
    • 退出容器
      exit

      +
    • +
    • 进入容器
      docker exec -it nginx1 /bin/bash

      +
    • +
    • 通过容器创建镜像
      docker commit -m ‘laowang’ nginx1 nginx:v1

      +
    • +
    • 查看容器信息
      docker container inspact nginx1

      +
    • +
    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/2/index.html b/page/2/index.html new file mode 100644 index 0000000000..8523ad585a --- /dev/null +++ b/page/2/index.html @@ -0,0 +1,477 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    DockerCompose安装

    1
    2
    3
    4
    5
    DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
    mkdir -p $DOCKER_CONFIG/cli-plugins
    sudo curl -SL https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-x86_64 -o /usr/bin/docker-compose
    sudo chmod +x /usr/bin/docker-compose
    sudo docker-compose version
    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/20/index.html b/page/20/index.html new file mode 100644 index 0000000000..47b20d047e --- /dev/null +++ b/page/20/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    Docker网络模式

    Docker常见的两种网络模式

    +
      +
    • host网络模式:创建的容器和宿主机共享同一个网卡
    • +
    • bridge网络模式:桥接模式,只要使用桥接模式创建容器网段一样(类似于172.16.1.1,172.16.1.2)有点像是子局域网
    • +
    +

    利用network命令管理网络模式

      +
    • 查看网络模式
      docker network ls
    • +
    • 创建网络模式
      docker network create –drive bridge bridge_test
    • +
    • 通过network断网
      docker network disconnet bridge nginx5
    • +
    • 通过network联网
      docker network connect bridge nginx5
    • +
    • 删除网络模式
      docker network rm bridge_test
    • +
    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/21/index.html b/page/21/index.html new file mode 100644 index 0000000000..73bc426fe2 --- /dev/null +++ b/page/21/index.html @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    Docker容器端口映射

    常见容器服务需要做端口映射,这里以nginx为例进行举例

    +

    启动一个nginx容器

    docker run -itd –name nginx1 -P nginx:latest #随机端口
    docker run -itd –name nginx2 -p 6001:80 nginx:latest #指定端口
    docker run -itd –name nginx3 -p 172.16.10.1:6002:80 nginx:latest #指定IP和端口

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/22/index.html b/page/22/index.html new file mode 100644 index 0000000000..0d60f73f40 --- /dev/null +++ b/page/22/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    Docker数据卷

    宿主机与容器进行数据交互,共享宿主机与容器之间的数据

    +

    创建数据卷关联

    docker run -it –name my_ubuntu1 -v ~ /Desktop/test:/test /bin/bash

    +

    利用系统方法操作数据卷

      +
    • 查 docker数据卷
      docker volume ls

      +
    • +
    • 创建数据卷
      docker volume create volname

      +
    • +
    • 共享
      docker run -it –name my_ubuntu2 -v volname:/home ubuntu:latest /bin/bash

      +
    • +
    +

    数据卷容器使用

    可以通过数据卷容器,创建新的容器,并将多个容器绑定在一起

    +

    docker run -itd –name my_ubuntu3 –volumes-from my_ubuntu2 ubuntu:latest

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/23/index.html b/page/23/index.html new file mode 100644 index 0000000000..9619554164 --- /dev/null +++ b/page/23/index.html @@ -0,0 +1,613 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    DockerFile

    原则与建议

      +
    • 容器轻量化。从镜像中产生的容器应该尽量轻量化,能在足够短的时间内停止、销毁、重新生成并替换原来的容器。
    • +
    • 使用 .gitignore。在大部分情况下,Dockerfile 会和构建所需的文件放在同一个目录中,为了提高构建的性能,应该使用 .gitignore 来过滤掉不需要的文件和目录。
    • +
    • 为了减少镜像的大小,减少依赖,仅安装需要的软件包。
    • +
    • 一个容器只做一件事。解耦复杂的应用,分成多个容器,而不是所有东西都放在一个容器内运行。如一个 Python Web 应用,可能需要 Server、DB、Cache、MQ、Log 等几个容器。一个更加极端的说法:One process per container。
    • +
    • 减少镜像的图层。不要多个 Label、ENV 等标签。
    • +
    • 对续行的参数按照字母表排序,特别是使用apt-get install -y安装包的时候。
    • +
    • 使用构建缓存。如果不想使用缓存,可以在构建的时候使用参数–no-cache=true来强制重新生成中间镜像。
    • +
    +

    常用命令

    FROM

    FROM 指令用于指定其后构建新镜像所使用的基础镜像

    +
    1
    2
    3
    FROM <image>
    FROM <image>:<tag>
    FROM <image>:<digest>
    +

    FROM 必须 是 Dockerfile 中第一条非注释命令
    在一个 Dockerfile 文件中创建多个镜像时,FROM 可以多次出现。只需在每个新命令 FROM 之前,记录提交上次的镜像 ID。
    tag 或 digest 是可选的,如果不使用这两个值时,会使用 latest 版本的基础镜像

    +

    RUN

    在镜像的构建过程中执行特定的命令,并生成一个中间镜像

    +
    1
    2
    3
    4
    #shell格式
    RUN <command>
    #exec格式
    RUN ["executable", "param1", "param2"]
    +
      +
    • RUN 命令将在当前 image 中执行任意合法命令并提交执行结果。命令执行提交后,就会自动执行 Dockerfile 中的下一个指令。
    • +
    • 层级 RUN 指令和生成提交是符合 Docker 核心理念的做法。它允许像版本控制那样,在任意一个点,对 image 镜像进行定制化构建。
    • +
    • RUN 指令创建的中间镜像会被缓存,并会在下次构建中使用。如果不想使用这些缓存镜像,可以在构建时指定 ######no###cache 参数,如:docker build ######no###cache。
    • +
    +

    FROM scratch

    scratch 是一个空的虚拟的镜像源文件,因为需要的依赖都存在不需要基础镜像,从而减小体积。

    +

    COPY

    复制文件

    +
    1
    2
    COPY <源路径>... <目标路径>
    COPY ["<源路径1>",... "<目标路径>"]
    +

    和 RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的<目标路径>位置。比如:

    +
    1
    COPY package.json /usr/src/app/
    + +

    <源路径>可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,如:

    +
    1
    2
    COPY hom* /mydir/
    COPY hom?.txt /mydir/
    + +

    <目标路径>可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

    +

    此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。

    +

    ADD

    更高级的复制文件
    ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。比如<源路径>可以是一个 URL,这种情况下,Docker 引擎会试图去下载这个链接的文件放到<目标路径>去。

    +

    在构建镜像时,复制上下文中的文件到镜像内,格式:

    +
    1
    2
    ADD <源路径>... <目标路径>
    ADD ["<源路径>",... "<目标路径>"]
    +

    注意
    如果 docker 发现文件内容被改变,则接下来的指令都不会再使用缓存。关于复制文件时需要处理的/,基本跟正常的 copy 一致

    +

    ENV

    设置环境变量
    格式有两种:

    +
    1
    2
    ENV <key> <value>
    ENV <key1>=<value1> <key2>=<value2>...
    +

    这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。

    +
    1
    2
    ENV VERSION=1.0 DEBUG=on \
    NAME="Happy Feet"
    +

    这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。

    +

    EXPOSE

    为构建的镜像设置监听端口,使容器在运行时监听。
    格式:

    +
    1
    EXPOSE <port> [<port>...]
    +

    EXPOSE 指令并不会让容器监听 host 的端口,如果需要,需要在 docker run 时使用 -p、-P 参数来发布容器端口到 host 的某个端口上。

    +

    VOLUME

    定义匿名卷
    VOLUME用于创建挂载点,即向基于所构建镜像创始的容器添加卷:

    +
    1
    VOLUME ["/data"]
    +

    一个卷可以存在于一个或多个容器的指定目录,该目录可以绕过联合文件系统,并具有以下功能:

    +
      +
    • 卷可以容器间共享和重用
    • +
    • 容器并不一定要和其它容器共享卷
    • +
    • 修改卷后会立即生效
    • +
    • 对卷的修改不会对镜像产生影响
    • +
    • 卷会一直存在,直到没有任何容器在使用它
      VOLUME 让我们可以将源代码、数据或其它内容添加到镜像中,而又不并提交到镜像中,并使我们可以多个容器间共享这些内容。
    • +
    +

    WORKDIR

    WORKDIR用于在容器内设置一个工作目录:

    +
    1
    WORKDIR /path/to/workdir
    +

    通过WORKDIR设置工作目录后,Dockerfile 中其后的命令 RUN、CMD、ENTRYPOINT、ADD、COPY 等命令都会在该目录下执行。
    如,使用WORKDIR设置工作目录:

    +
    1
    2
    3
    4
    WORKDIR /a
    WORKDIR b
    WORKDIR c
    RUN pwd
    +

    在以上示例中,pwd 最终将会在 /a/b/c 目录中执行。在使用 docker run 运行容器时,可以通过-w参数覆盖构建时所设置的工作目录。

    +

    USER

    指定当前用户
    USER 用于指定运行镜像所使用的用户:

    +
    1
    USER daemon
    +

    使用USER指定用户时,可以使用用户名、UID 或 GID,或是两者的组合。以下都是合法的指定试:

    +
    1
    2
    3
    4
    5
    6
    USER user
    USER user:group
    USER uid
    USER uid:gid
    USER user:gid
    USER uid:group
    +

    使用USER指定用户后,Dockerfile 中其后的命令 RUN、CMD、ENTRYPOINT 都将使用该用户。镜像构建完成后,通过 docker run 运行容器时,可以通过 -u 参数来覆盖所指定的用户。

    +

    CMD

    CMD用于指定在容器启动时所要执行的命令。CMD 有以下三种格式:

    +
    1
    2
    3
    CMD ["executable","param1","param2"]
    CMD ["param1","param2"]
    CMD command param1 param2
    +

    省略可执行文件的 exec 格式,这种写法使 CMD 中的参数当做 ENTRYPOINT 的默认参数,此时 ENTRYPOINT 也应该是 exec 格式,具体与 ENTRYPOINT 的组合使用,参考 ENTRYPOINT。

    +

    注意
    与 RUN 指令的区别:RUN 在构建的时候执行,并生成一个新的镜像,CMD 在容器运行的时候执行,在构建时不进行任何操作。

    +

    ENTRYPOINT

    ENTRYPOINT 用于给容器配置一个可执行程序。也就是说,每次使用镜像创建容器时,通过 ENTRYPOINT 指定的程序都会被设置为默认程序。ENTRYPOINT 有以下两种形式:

    +
    1
    2
    3
    ENTRYPOINT ["executable", "param1", "param2"]
    ENTRYPOINT command param1 param2
    ENTRYPOINT 与 CMD 非常类似,不同的是通过docker run执行的命令不会覆盖 ENTRYPOINT,而docker run命令中指定的任何参数,都会被当做参数再次传递给 ENTRYPOINT。Dockerfile 中只允许有一个 ENTRYPOINT 命令,多指定时会覆盖前面的设置,而只执行最后的 ENTRYPOINT 指令。
    +

    docker run运行容器时指定的参数都会被传递给 ENTRYPOINT ,且会覆盖 CMD 命令指定的参数。如,执行docker run -d时,-d 参数将被传递给入口点。

    +

    也可以通过docker run –entrypoint重写 ENTRYPOINT 入口点。如:可以像下面这样指定一个容器执行程序:

    +
    1
    ENTRYPOINT ["/usr/bin/nginx"]
    +

    完整构建代码:

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # Version: 0.0.3
    FROM ubuntu:16.04
    MAINTAINER 何民三 "cn.liuht@gmail.com"
    RUN apt-get update
    RUN apt-get install -y nginx
    RUN echo 'Hello World, 我是个容器' \
    > /var/www/html/index.html
    ENTRYPOINT ["/usr/sbin/nginx"]
    EXPOSE 80
    +

    使用docker build构建镜像,并将镜像指定为 itbilu/test:

    +
    1
    docker build -t="itbilu/test" .
    +

    构建完成后,使用itbilu/test启动一个容器:

    +
    1
    docker run -i -t  itbilu/test -g "daemon off;"
    +

    在运行容器时,我们使用了 -g “daemon off;”,这个参数将会被传递给 ENTRYPOINT,最终在容器中执行的命令为 /usr/sbin/nginx -g “daemon off;”。

    +

    LABEL

    LABEL用于为镜像添加元数据,元数以键值对的形式指定:

    +
    1
    LABEL <key>=<value> <key>=<value> <key>=<value> ...
    +

    使用LABEL指定元数据时,一条LABEL指定可以指定一或多条元数据,指定多条元数据时不同元数据之间通过空格分隔。推荐将所有的元数据通过一条LABEL指令指定,以免生成过多的中间镜像。
    如,通过LABEL指定一些元数据:

    +
    1
    LABEL version="1.0" description="这是一个Web服务器" by="IT笔录"
    +

    指定后可以通过docker inspect查看:

    +
    1
    2
    3
    4
    5
    6
    docker inspect itbilu/test
    "Labels": {
    "version": "1.0",
    "description": "这是一个Web服务器",
    "by": "IT笔录"
    },
    + +

    ARG

    ARG用于指定传递给构建运行时的变量:

    +
    1
    ARG <name>[=<default value>]
    +

    如,通过ARG指定两个变量:

    +
    1
    2
    ARG site
    ARG build_user=IT笔录
    +

    以上我们指定了 site 和 build_user 两个变量,其中 build_user 指定了默认值。在使用 docker build 构建镜像时,可以通过 –build-arg = 参数来指定或重设置这些变量的值。

    +
    1
    docker build --build-arg site=itiblu.com -t itbilu/test .
    +

    这样我们构建了 itbilu/test 镜像,其中site会被设置为 itbilu.com,由于没有指定 build_user,其值将是默认值 IT 笔录。

    +

    ONBUILD

    ONBUILD用于设置镜像触发器:

    +
    1
    ONBUILD [INSTRUCTION]
    +

    当所构建的镜像被用做其它镜像的基础镜像,该镜像中的触发器将会被钥触发。
    如,当镜像被使用时,可能需要做一些处理:

    +
    1
    2
    3
    4
    [...]
    ONBUILD ADD . /app/src
    ONBUILD RUN /usr/local/bin/python-build --dir /app/src
    [...]
    + +

    STOPSIGNAL

    STOPSIGNAL用于设置停止容器所要发送的系统调用信号:

    +
    1
    STOPSIGNAL signal
    +

    所使用的信号必须是内核系统调用表中的合法的值,如:SIGKILL。

    +

    SHELL

    SHELL用于设置执行命令(shell式)所使用的的默认 shell 类型:

    +
    1
    SHELL ["executable", "parameters"]
    +

    SHELL在Windows环境下比较有用,Windows 下通常会有 cmd 和 powershell 两种 shell,可能还会有 sh。这时就可以通过 SHELL 来指定所使用的 shell 类型:

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    FROM microsoft/windowsservercore

    # Executed as cmd /S /C echo default
    RUN echo default

    # Executed as cmd /S /C powershell -command Write-Host default
    RUN powershell -command Write-Host default

    # Executed as powershell -command Write-Host hello
    SHELL ["powershell", "-command"]
    RUN Write-Host hello

    # Executed as cmd /S /C echo hello
    SHELL ["cmd", "/S"", "/C"]
    RUN echo hello
    + +

    Dockerfile 示例

      +
    • 构建Nginx运行环境
    • +
    • 构建tomcat 环境
    • +
    +

    构建Nginx运行环境

    Dockerfile文件

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    #指定基础镜像
    FROM sameersbn/ubuntu:14.04.20161014

    #维护者信息
    MAINTAINER sameer@damagehead.com

    #设置环境
    ENV RTMP_VERSION=1.1.10 \
    NPS_VERSION=1.11.33.4 \
    LIBAV_VERSION=11.8 \
    NGINX_VERSION=1.10.1 \
    NGINX_USER=www-data \
    NGINX_SITECONF_DIR=/etc/nginx/sites-enabled \
    NGINX_LOG_DIR=/var/log/nginx \
    NGINX_TEMP_DIR=/var/lib/nginx \
    NGINX_SETUP_DIR=/var/cache/nginx

    #设置构建时变量,镜像建立完成后就失效
    ARG BUILD_LIBAV=false
    ARG WITH_DEBUG=false
    ARG WITH_PAGESPEED=true
    ARG WITH_RTMP=true

    #复制本地文件到容器目录中
    COPY setup/ ${NGINX_SETUP_DIR}/
    RUN bash ${NGINX_SETUP_DIR}/install.sh

    #复制本地配置文件到容器目录中
    COPY nginx.conf /etc/nginx/nginx.conf
    COPY entrypoint.sh /sbin/entrypoint.sh

    #运行指令
    RUN chmod 755 /sbin/entrypoint.sh

    #允许指定的端口
    EXPOSE 80/tcp 443/tcp 1935/tcp

    #指定网站目录挂载点
    VOLUME ["${NGINX_SITECONF_DIR}"]

    ENTRYPOINT ["/sbin/entrypoint.sh"]
    CMD ["/usr/sbin/nginx"]
    + +

    构建Tomcat环境

    Dockerfile文件

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    # 指定基于的基础镜像
    FROM ubuntu:13.10

    # 维护者信息
    MAINTAINER zhangjiayang "zhangjiayang@sczq.com.cn"

    # 镜像的指令操作
    # 获取APT更新的资源列表
    RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe"> /etc/apt/sources.list
    # 更新软件
    RUN apt-get update

    # Install curl
    RUN apt-get -y install curl

    # Install JDK 7
    RUN cd /tmp && curl -L 'http://download.oracle.com/otn-pub/java/jdk/7u65-b17/jdk-7u65-linux-x64.tar.gz' -H 'Cookie: oraclelicense=accept-securebackup-cookie; gpw_e24=Dockerfile' | tar -xz
    RUN mkdir -p /usr/lib/jvm
    RUN mv /tmp/jdk1.7.0_65/ /usr/lib/jvm/java-7-oracle/

    # Set Oracle JDK 7 as default Java
    RUN update-alternatives --install /usr/bin/java java /usr/lib/jvm/java-7-oracle/bin/java 300
    RUN update-alternatives --install /usr/bin/javac javac /usr/lib/jvm/java-7-oracle/bin/javac 300

    # 设置系统环境
    ENV JAVA_HOME /usr/lib/jvm/java-7-oracle/

    # Install tomcat7
    RUN cd /tmp && curl -L 'http://archive.apache.org/dist/tomcat/tomcat-7/v7.0.8/bin/apache-tomcat-7.0.8.tar.gz' | tar -xz
    RUN mv /tmp/apache-tomcat-7.0.8/ /opt/tomcat7/

    ENV CATALINA_HOME /opt/tomcat7
    ENV PATH $PATH:$CATALINA_HOME/bin

    # 复件tomcat7.sh到容器中的目录
    ADD tomcat7.sh /etc/init.d/tomcat7
    RUN chmod 755 /etc/init.d/tomcat7

    # Expose ports. 指定暴露的端口
    EXPOSE 8080

    # Define default command.
    ENTRYPOINT service tomcat7 start && tail -f /opt/tomcat7/logs/catalina.out
    + +

    tomcat7.sh命令文件

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    export JAVA_HOME=/usr/lib/jvm/java-7-oracle/  
    export TOMCAT_HOME=/opt/tomcat7

    case $1 in
    start)
    sh $TOMCAT_HOME/bin/startup.sh
    ;;
    stop)
    sh $TOMCAT_HOME/bin/shutdown.sh
    ;;
    restart)
    sh $TOMCAT_HOME/bin/shutdown.sh
    sh $TOMCAT_HOME/bin/startup.sh
    ;;
    esac
    exit 0
    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/24/index.html b/page/24/index.html new file mode 100644 index 0000000000..65ebd49ac4 --- /dev/null +++ b/page/24/index.html @@ -0,0 +1,567 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    1、说明

    项目是一个多人协同完成同一个目标的团队组织形式,在多人协作项目中,如果代码风格统一(前端代码规范和静态代码检查约束)、代码提交信息的说明准确规范(本章介绍),那么在项目开发过程及后期协作以及Bug处理时会更加方便。

    +

    在本文中,我将介绍大家如何利用工具及约定保证大家代码提交的统一性,从而提高大家的协同效率:

    +
      +
    • commitlint: git 提交信息规范与验证
      +

      添加如ESLint的格式规范校验,规范comiit的格式,达到团队每个人的提交风格保持一直,保证提交信息的完整和准确性
      风格如下:

      +
      +
    • +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <type>(<scope>): <subject> // Header
    // 空一行
    <body> // 72
    // 空一行
    <footer> // 72

    // 例
    fix(登陆模块): 修复登录密码错误的提示

    登录没有做友好型提示

    修正后不存在此问题
    + +
      +
    • husky: 使git-hook更容易
      +

      husky继承了Git下所有的钩子,在触发钩子的时候,husky可以阻止不合法的commit,push等等。注意使用husky之前,必须先将代码放到git 仓库中,否则本地没有.git文件,就没有地方去继承钩子了。

      +
      +
    • +
    • standard-version: 自动生成CHANGELOG 并发布版本
    • +
    +

    2、git commit message 规范

    commit message格式

    1
    2
    3
    4
    5
    类型(影响范围): 描述

    问题描述

    修复方式结果
    + + +

    注意:
    1.冒号后面有空格。
    2.英文小括号。
    3.正文和注脚前都要加空行。

    +

    type 类型

    用于说明 commit 的类别,只允许使用下面13个标识。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    'feat', // feat:新增功能
    'fix', // fix:bug 修复
    'docs', // docs:文档更新
    'style', // style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑)
    'refactor', // refactor:重构代码(既没有新增功能,也没有修复 bug)
    'test', // test:新增测试用例或是更新现有测试
    'chore', // revert:回滚某个更早之前的提交
    'revert', // build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交
    'build', // build:打包生产环境代码
    'ci', // ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交
    'merge', // merge:分支合并 Merge branch ? of ?
    'perf', // perf:性能, 体验优化
    'chore' // chore:不属于以上类型的其他类型(构建过程或辅助工具的变动)
    + +

    如果type为feat和fix,则该 commit 将肯定出现在 Change log 之中。

    +

    scope 影响范围

    可根据项目组需要进行定制化设置,也可不做强制要求

    +

    subject 描述

    subject是 commit 目的的简短描述,不超过50个字符,且结尾不加句号(.)。

    +

    body 详细描述

    body 部分是对本次 commit 的详细描述,可以分成多行。下面是一个范例。

    +
    1
    2
    3
    此次提交内容包括如下信息:
    - 登录为空校验
    - 密码错误提示
    +

    Footer 部分只用于两种情况。

    +
      +
    • 不兼容变动(改变解决方案)
    • +
    • 关闭 Issue(回复bug)
    • +
    +

    3、使用工具校验commit是否符合规范

    commitlint

    3.1 commitlint安装

    1
    2
    // npm 安装
    npm install --save-dev @commitlint/{cli,config-conventional}
    + +

    3.2 生成commitlint.config.js配置文件

    执行echo命令创建配置文件,也可以手动创建配置文件,或者从已有的配置文件进行拷贝。

    +
    1
    2
    // 项目根目录创建commitlint.config.js
    echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
    + + +

    3.3 在commitlint.config.js制定提交message规范

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    // type(scope?): subject

    // body?

    // footer?
    module.exports = {
    // 继承自默认规则
    extends: ['@commitlint/config-conventional'],
    // 这里写自定义规则
    rules: {
    // 等级 [0 1 2]: 0 不使用规则 1 警告 2 错误
    // 启用范围 always|never: 总是或者永不.
    // 规则值: 规则对应的值

    // 头部(包含type、scope、subject)
    'header-case': [0, 'always', 'lower-case'],
    'header-full-stop': [0, 'never', '.'],
    'header-max-length': [0, 'always', 72],
    'header-min-length': [0, 'always', 0],

    // 提交类型
    'type-case': [0, 'never'],
    'type-empty': [2, 'never'],
    'type-max-length': [0, 'always', Infinity],
    'type-min-length': [0, 'always', 0],
    'type-enum': [
    2,
    'always',
    [
    'feat', // feat:新增功能
    'fix', // fix:bug 修复
    'docs', // docs:文档更新
    'style', // style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑)
    'refactor', // refactor:重构代码(既没有新增功能,也没有修复 bug)
    'test', // test:新增测试用例或是更新现有测试
    'revert', // revert:回滚某个更早之前的提交
    'build', // build:build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交,打包生产环境代码
    'ci', // ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交
    'merge', // merge:分支合并 Merge branch ? of ?
    'perf', // perf:性能, 体验优化
    'chore' // chore:不属于以上类型的其他类型(构建过程或辅助工具的变动)
    ]
    ],

    // 影响范围
    'scope-case': [0, 'always', 'lower-case'], // 书写格式
    'scope-max-length': [0, 'always', Infinity], // 最长
    'scope-min-length': [0, 'always', 0], // 最短
    'scope-empty': [2, 'never'], // 影响范围:为空|永不
    'scope-enum': [0, 'always', ['a', 'b']], // 影响范围:给出范围但是不在给定的列表内

    // 简介
    'subject-empty': [2, 'never'], // 简介不能为空
    'subject-full-stop': [0, 'never', '.'], // 简介结尾以.结束
    'subject-case': [0, 'never'],
    'subject-max-length': [0, 'always', Infinity],
    'subject-min-length': [0, 'always', 1],

    // 正文
    'body-leading-blank': [2, 'always'],
    'body-max-length': [0, 'always', Infinity],
    'body-max-line-length': [0, 'always', Infinity],
    'body-min-length': [0, 'always', 1],

    // 注脚
    'footer-leading-blank': [2, 'always'],
    'footer-max-length': [0, 'always', Infinity],
    'footer-max-line-length': [0, 'always', Infinity],
    'footer-min-length': [0, 'always', 1],

    // 其他
    'references-empty': [0, 'never'],
    'signed-off-by': [0, 'always', 'Signed-off-by']
    }
    }
    + +

    上面我们就完成了commitlint的安装与提交规范的制定。检验commit message的最佳方式是结合git hook,所以需要配合Husky

    +

    husky

    3.4 husky安装

    1
    npm install husky --save-dev
    +

    3.5 husky 配置

    安装成功后需要在项目下的package.json中配置

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // package.json
    {
    "husky": {
    "hooks": { // husky的钩子
    "pre-commit": "npm run lint" // 提交前进行代码静态校验
    "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" // 进行提交信息格式校验
    }
    },
    "scripts": {}
    ...
    }
    + +

    3.5 husky 执行测试

    最后我们可以正常的git操作

    +
    1
    2
    git add .
    git commit -m ""
    + +

    git commit的时候会触发commlint。下面演示下不符合规范提交示例:

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    C:\lixg\git>git commit -m "thunisoft: abcde"

    husky > npm run -s commitmsg (node v8.2.1)

    ⧗ input:
    thunisoft(all): abcde

    ✖ type must be one of [feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert] [type-enum]
    ✖ found 1 problems, 0 warnings

    husky > commit-msg hook failed (add --no-verify to bypass)

    C:\lixg\git>
    + +

    上面的提交看反馈消息得知是type格式没有符合限制的提交类型列表,所以提交失败,虾米啊我们把type改为feat再试一下

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    C:\lixg\git>git commit -m "feat(all): 新功能"

    husky > npm run -s commitmsg (node v8.2.1)

    ⧗ input: feat: 新功能
    ✔ found 0 problems, 0 warnings

    [develop 19dfhe] feat: 新功能
    1 file changed, 1 insertion(+)

    C:\lixg\git>
    + +

    修改后格式符合规范,提交成功。

    +

    参考来源

    +

    版权声明

    Copyright by lixuguang
    未经授权,严禁转载。如需转载,请联系作者

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/25/index.html b/page/25/index.html new file mode 100644 index 0000000000..d287d6717d --- /dev/null +++ b/page/25/index.html @@ -0,0 +1,736 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    1. URI

    URI 表示资源,资源一般对应服务器端领域模型中的实体类。

    +

    URI规范

      +
    1. 不用大写;
    2. +
    3. 用中杠-不用下杠_;
    4. +
    5. 参数列表要encode;
    6. +
    7. URI中的名词表示资源集合,使用复数形式。
    8. +
    +

    资源集合 vs 单个资源

    URI表示资源的两种方式:资源集合、单个资源。

    +

    资源集合:

    +
    1
    2
    /zoos //所有动物园
    /zoos/1/animals //id为1的动物园中的所有动物
    +

    单个资源:

    +
    1
    2
    /zoos/1 //id为1的动物园
    /zoos/1;2;3 //id为1,2,3的动物园
    +

    避免层级过深的URI

    /在url中表达层级,用于按实体关联关系进行对象导航,一般根据id导航。

    +

    过深的导航容易导致url膨胀,不易维护,如 GET /zoos/1/areas/3/animals/4,尽量使用查询参数代替路径中的实体导航,如GET /animals?zoo=1&area=3

    +

    对Composite资源的访问

    服务器端的组合实体必须在uri中通过父实体的id导航访问。

    +
    +

    组合实体不是first-class的实体,它的生命周期完全依赖父实体,无法独立存在,在实现上通常是对数据库表中某些列的抽象,不直接对应表,也无id。一个常见的例子是 User — Address,Address是对User表中zipCode/country/city三个字段的简单抽象,无法独立于User存在。必须通过User索引到Address:GET /user/1/addresses

    +
    +

    2. Request

    HTTP方法

    通过标准HTTP方法对资源CRUD:

    +

    GET:查询

    +
    1
    2
    3
    GET /zoos
    GET /zoos/1
    GET /zoos/1/employees
    + + +

    POST:创建单个资源。POST一般向“资源集合”型uri发起

    +
    1
    2
    POST /animals  //新增动物
    POST /zoos/1/employees //为id为1的动物园雇佣员工
    + +

    PUT:更新单个资源(全量),客户端提供完整的更新后的资源。与之对应的是 PATCH,PATCH 负责部分更新,客户端提供要更新的那些字段。PUT/PATCH一般向“单个资源”型uri发

    +
    1
    2
    PUT /animals/1
    PUT /zoos/1
    + +

    DELETE:删除

    +
    1
    2
    3
    DELETE /zoos/1/employees/2
    DELETE /zoos/1/employees/2;4;5
    DELETE /zoos/1/animals //删除id为1的动物园内的所有动物
    +

    HEAD / OPTION 用的不多,就不多解释了。

    +

    安全性和幂等性

      +
    1. 安全性:不会改变资源状态,可以理解为只读的;
    2. +
    3. 幂等性:执行1次和执行N次,对资源状态改变的效果是等价的。
    4. +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    .安全性幂等性
    GET
    POST××
    PUT×
    DELETE×
    +

    安全性和幂等性均不保证反复请求能拿到相同的response。以 DELETE 为例,第一次DELETE返回200表示删除成功,第二次返回404提示资源不存在,这是允许的。

    +

    复杂查询

    查询可以捎带以下参数:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    .示例备注
    过滤条件type=1&age=16允许一定的uri冗余,如/zoos/1与/zoos?id=1
    排序?sort=age,desc
    投影?whitelist=id,name,email
    分页?limit=10&offset=3
    +

    Bookmarker

    经常使用的、复杂的查询标签化,降低维护成本。

    +

    如:

    +
    1
    2
    GET /trades?status=closed&sort=created,desc

    +

    快捷方式:

    +
    1
    2
    3
    GET /trades#recently-closed
    // 或者
    GET /trades/recently-closed
    + +

    Format

    只用以下常见的3种body format:

    +
      +
    1. Content-Type: application/json
    2. +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    POST /v1/animal HTTP/1.1
    Host: api.example.org
    Accept: application/json
    Content-Type: application/json
    Content-Length: 24

    {
    "name": "Gir",
    "animalType": "12"
    }
    + +
      +
    1. Content-Type: application/x-www-form-urlencoded (浏览器POST表单用的格式)
    2. +
    +
    1
    2
    3
    4
    5
    6
    7
    POST /login HTTP/1.1
    Host: example.com
    Content-Length: 31
    Accept: text/html
    Content-Type: application/x-www-form-urlencoded

    username=root&password=Zion0101
    + +
      +
    1. Content-Type: multipart/form-data; boundary=—-RANDOM_jDMUxq4Ot5 (表单有文件上传时的格式)
    2. +
    +

    6. Response

      +
    1. 不要包装:
      response 的 body 直接就是数据,不要做多余的包装。
    2. +
    +

    错误示例

    +
    1
    2
    3
    4
    {
    "success":true,
    "data":{"id":1,"name":"xiaotuan"},
    }
    +

    各HTTP方法成功处理后的数据格式:

    + + + + + + + + + + + + + + + + + + + + + + + +
    ·response 格式
    GET单个对象、集合
    POST新增成功的对象
    PUT/PATCH更新成功的对象
    DELETE
    +
      +
    1. json格式的约定:

      +
        +
      1. 时间用长整形(毫秒数),客户端自己按需解析(moment.js)
      2. +
      3. 不传null字段
      4. +
      +
    2. +
    +

    分页response

    1
    2
    3
    4
    {
    "paging":{"limit":10,"offset":0,"total":729},
    "data":[{},{},{}...]
    }
    +

    7. 错误处理

      +
    1. 不要发生了错误但给2xx响应,客户端可能会缓存成功的http请求;
    2. +
    3. 正确设置http状态码,不要自定义;
    4. +
    5. Response body 提供
        +
      1. 错误的代码(日志/问题追查);
      2. +
      3. 错误的描述文本(展示给用户)。
      4. +
      +
    6. +
    +

    对第三点的实现稍微多说一点:

    +

    Java 服务器端一般用异常表示 RESTful API 的错误。API 可能抛出两类异常:业务异常非业务异常

    +

    业务异常由自己的业务代码抛出,表示一个用例的前置条件不满足、业务规则冲突等,比如参数校验不通过、权限校验失败。

    +

    非业务类异常表示不在预期内的问题,通常由类库、框架抛出,或由于自己的代码逻辑错误导致,比如数据库连接失败、空指针异常、除0错误等等。

    +

    业务类异常必须提供2种信息:

    +
      +
    1. 如果抛出该类异常,HTTP 响应状态码应该设成什么;
    2. +
    3. 异常的文本描述;
    4. +
    +

    在Controller层使用统一的异常拦截器:

    +
      +
    1. 设置 HTTP 响应状态码:对业务类异常,用它指定的 HTTP code;对非业务类异常,统一500;
    2. +
    3. Response Body 的错误码:异常类名
    4. +
    5. Response Body 的错误描述:对业务类异常,用它指定的错误文本;对非业务类异常,线上可以统一文案如“服务器端错误,请稍后再试”,开发或测试环境中用异常的 stacktrace,服务器端提供该行为的开关。
    6. +
    +

    常用的http状态码及使用场景:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    状态码使用场景
    400 bad request常用在参数校验
    401 unauthorized未经验证的用户,常见于未登录。如果经过验证后依然没权限,应该 403(即 authentication 和 authorization 的区别)。
    403 forbidden无权限
    404 not found资源不存在
    500 internal server error非业务类异常
    503 service unavaliable由容器抛出,自己的代码不要抛这个异常
    +

    8. 服务型资源

    除了资源简单的CRUD,服务器端经常还会提供其他服务,这些服务无法直接用上面提到的URI映射。如:

    +
      +
    1. 按关键字搜索;
    2. +
    3. 计算地球上两点间的距离;
    4. +
    5. 批量向用户推送消息;
    6. +
    +

    可以把这些服务看成资源,计算的结果是资源的presentation,按服务属性选择合适的HTTP方法。

    +

    例:

    +
    1
    2
    3
    4
    GET /search?q=filter?category=file  搜索
    GET /distance-calc?lats=47.480&lngs=-122.389&late=37.108&lnge=-122.448
    POST /batch-publish-msg
    [{"from":0,"to":1,"text":"abc"},{},{}...]
    +

    9. 异步任务

    对耗时的异步任务,服务器端接受客户端传递的参数后,应返回创建成功的任务资源,其中包含了任务的执行状态。客户端可以轮训该任务获得最新的执行进度。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 提交任务:
    POST /batch-publish-msg
    [{"from":0,"to":1,"text":"abc"},{},{}...]

    // 返回:
    {"taskId":3,"createBy":"Anonymous","status":"running"}

    GET /task/3
    {"taskId":3,"createBy":"Anonymous","status":"success"}
    + +

    如果任务的执行状态包括较多信息,可以把“执行状态”抽象成组合资源,客户端查询该状态资源了解任务的执行情况。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 提交任务:
    POST /batch-publish-msg
    [{"from":0,"to":1,"text":"abc"},{},{}...]

    // 返回:
    {"taskId":3,"createBy":"Anonymous"}

    GET /task/3/status
    {"progress":"50%","total":18,"success":8,"fail":1}
    + +

    10. API的演进

    版本

    常见的三种方式:

    +
      +
    1. 在uri中放版本信息:GET /v1/users/1
    2. +
    3. Accept Header:Accept: application/json+v1
    4. +
    5. 自定义 Header:X-Api-Version: 1
    6. +
    +

    用第一种,虽然没有那么优雅,但最明显最方便。

    +

    URI失效

    随着系统发展,总有一些API失效或者迁移,对失效的API,返回404 not found 或 410 gone;对迁移的API,返回 301 重定向。

    +

    11. 安全

    这个不熟,接触到的时候再说。

    +

    参考文档

      +
    • < RESTful Web Services Cookbook >
    • +
    • Consumer-Centric API Design
    • +
    • RESTful Best Practices
    • +
    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/26/index.html b/page/26/index.html new file mode 100644 index 0000000000..464594c7cc --- /dev/null +++ b/page/26/index.html @@ -0,0 +1,645 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    1.关于Code Review

    1.1 Code Review的目的

    Code Review是一种用来确认方案设计和代码实现的质量保证机制,通过这个机制我们可以对代码、测试过程和注释进行检查。

    +

    Code Review主要用来在软件工程过程中改进代码质量,通过Code Review可以达到如下目的目的:

    +
      +
    1. 在项目早期就能够发现代码中的BUG
    2. +
    3. 帮助初级开发人员学习高级开发人员的经验,达到知识共享
    4. +
    5. 避免开发人员犯一些很常见,很普通的错误
    6. +
    7. 保证项目组人员的良好沟通
    8. +
    9. 项目或产品的代码更容易维护
    10. +
    +

    1.2 Code Review的前提

    进入Code Review需要检查的条件如下:

    +
      +
    1. Code Review人员是否理解了Code Review的概念和Code Review将做什么
      如果做Code Review的人员不能理解Code Review对项目成败和代码质量的重要程度,他们的做法可能就会是应付了事。
    2. +
    3. 代码是否已经正确的build,build的目的使得代码已经不存在基本语法错误
      我们总不希望高级开发人员或是主管将时间浪费在检查连编译都通不过的代码上吧。
    4. +
    5. 代码执行时功能是否正确
      Code Review人员也不负责检查代码的功能是否正确,也就是说,需要复查的代码必须由开发人员或质量人员负责该代码的功能的正确性。
    6. +
    7. Review人员是否理解了代码
      做复查的人员需要对该代码有一个基本的了解,其功能是什么,是拿一方面的代码,涉及到数据库或是通讯,这样才能采取针对性的检查
    8. +
    9. 开发人员是否对代码做了单元测试
      这一点也是为了保证Code Review前一些语法和功能问题已经得到解决,Code Review人员可以将精力集中在代码的质量上。
    10. +
    +

    1.3 Code Review需要做什么

    Code Review主要检查代码中是否存在以下方面问题:

    +
      +
    • 代码的一致性
    • +
    • 编码风格
    • +
    • 代码的安全问题
    • +
    • 代码冗余
    • +
    • 是否正确设计以满足需求(性能、功能)
    • +
    • 等等
    • +
    +

    1.3.1 完整性检查(Completeness)

      +
    • 代码是否完全实现了设计文档中提出的功能需求
    • +
    • 代码是否已按照设计文档进行了集成和Debug
    • +
    • 代码是否已创建了需要的数据库,包括正确的初始化数据
    • +
    • 代码中是否存在任何没有定义或没有引用到的变量、常数或数据类型
    • +
    +

    1.3.2 一致性检查(Consistency)

      +
    • 代码的逻辑是否符合设计文档
    • +
    • 代码中使用的格式、符号、结构等风格是否保持一致
    • +
    +

    1.3.3 正确性检查(Correctness)

      +
    • 代码是否符合制定的标准
    • +
    • 所有的变量都被正确定义和使用
    • +
    • 所有的注释都是准确的
    • +
    • 所有的程序调用都使用了正确的参数个数
    • +
    +

    1.3.4 可修改性检查(Modifiability)

      +
    • 代码涉及到的常量是否易于修改(如使用配置、定义为类常量、使用专门的常量类等)
    • +
    • 代码中是否包含了交叉说明或数据字典,以描述程序是如何对变量和常量进行访问的
    • +
    • 代码是否只有一个出口和一个入口(严重的异常处理除外)
    • +
    +

    1.3.5 可预测性检查(Predictability)

      +
    • 代码所用的开发语言是否具有定义良好的语法和语义
    • +
    • 是否代码避免了依赖于开发语言缺省提供的功能
    • +
    • 代码是否无意中陷入了死循环
    • +
    • 代码是否是否避免了无穷递归
    • +
    +

    1.3.6 健壮性检查(Robustness)

      +
    • 代码是否采取措施避免运行时错误
        +
      • 数组边界溢出
      • +
      • 被零除
      • +
      • 值越界
      • +
      • 堆栈溢出
      • +
      • +
      +
    • +
    +

    1.3.7 结构性检查(Structuredness)

      +
    • 程序的每个功能是否都作为一个可辩识的代码块存在
      循环是否只有一个入口
    • +
    +

    1.3.8 可追溯性检查(Traceability)

      +
    • 代码是否对每个程序进行了唯一标识
    • +
    • 是否有一个交叉引用的框架可以用来在代码和开发文档之间相互对应
    • +
    • 代码是否包括一个修订历史记录,记录中对 代码的修改和原因都有记录
    • +
    • 是否所有的安全功能都有标识
    • +
    +

    1.3.9 可理解性检查(Understandability)

      +
    • 注释是否足够清晰的描述每个子程序
    • +
    • 是否使用到不明确或不必要的复杂代码,它们是否被清楚的注释
    • +
    • 使用一些统一的格式化技巧(如缩进、空白等)用来增强代码的清晰度
    • +
    • 是否在定义命名规则时采用了便于记忆,反映类型等方法
    • +
    • 每个变量都定义了合法的取值范围
    • +
    • 代码中的算法是否符合开发文档中描述的数学模型
    • +
    +

    1.3.10 可验证性检查(Verifiability)

    代码中的实现技术是否便于测试

    +

    1.4 Code Review的步骤

    这些是我在平时工作中的经验总结,目前也是按照这个步骤在做。

    +
      +
    1. 代码编写者和代码审核者坐在一起,由代码编写者按照UC依次讲解自己负责的代码和相关逻辑,从Web层->DAO层;
    2. +
    3. 代码审核者在此过程中可以随时提出自己的疑问,同时积极发现隐藏的bug;对这些bug记录在案。
    4. +
    5. 代码讲解完毕后,代码审核者给自己安排几个小时再对代码审核一遍。
      代码需要一行一行静下心看。同时代码又要全面的看,以确保代码整体上设计优良。
    6. +
    7. 代码审核者根据审核的结果编写“代码审核报告”,“审核报告”中记录发现的问题及修改建议,然后把“审核报告”发送给相关人员。
    8. +
    9. 代码编写者根据“代码审核报告”给出的修改意见,修改好代码,有不清楚的地方可积极向代码审核者提出。
    10. +
    11. 代码编写者 bug fix完毕之后给出反馈。
    12. +
    13. 代码审核者把Code Review中发现的有价值的问题更新到”代码审核规范”的文档中,对于特别值得提醒的问题可群发email给所有技术人员。
    14. +
    +

    提示
    Code Review必备的文档:

    +
      +
    • “代码审核规范”文档:记录代码应该遵循的标准。
      +

      代码审核者根据这些标准来Code Review代码,同时在Code Review过程中不断完善该文档。

      +
      +
    • +
    +

    2.Code Reivew的执行

    一个标准的Code Reivew活动应该分为三个阶段:

    +

    2.1.事前准备阶段

    在一次CR前,对以下内容进行充分准备。

    +

    2.1.1.CR的对象

    在准备CR代码对象时,我们要注意代码的数量,如果代码量比较大,要对代码进行必要的分解,确定其中的关键代码,对关键代码进行CR,可以达到举一反三的目的。

    +

    2.1.2.CR的内容

    我们对代码的审查内容很多,如代码的编写是否规范(注释的书写格式、命名规范等)、技术处理规范(异常处理、日志处理、代码组织结构等)、业务实现等。我们不能希望通过一次CR活动,完成所有这些内容的审查,因此我们必须设定本次CR活动内容界限,确定审查重点;

    +

    2.1.3.评审规范和标准

    在CR前设计确定评审规范和标准是必要,通过规范和标准我们在审查过程中可以有据可依,有理可循,而且还可以做到标准统一。

    +

    2.1.4.选择CR活动的参与者

    在CR开始前,必须把本次CR活动的对象、审查内容以及审查的规范和标准通报给所有的参与者。

    +

    2.1.5.选择CR活动的实施方式。

    CR活动有很多形式可供我们选择,我们可以根据实际情况选择桌面式CR、演示讲解式CR、一对一的座位CR等等。

    +

    2.2.实施阶段

    充分的事前准备,只是做好CR活动的前提,在CR实施过程中,我们要做好以下工作。

    +

    2.2.1.准确记录

    对于CR过程发现的问题,我们必须清晰准确的记录,可以使用问题点记录单,明确记录的项目和内容。

    +

    2.2.2.讲解与提问

    CR过程中,要采用代码作者讲解和审查者提问方式。审查者不能只在发现问题时提问,同时也要根据本次审查的内容要求代码作者对某个特定问题的讲解。

    +

    2.2.3.逐项审查

    对事前确定的审查内容,要逐项审查,不能因为时间不足等因素一扫而过。

    +

    2.2.4.注意气氛

    实施审查时,要营造一个讨论问题、解决问题的氛围,不能把审查会搞成批判会,这样会影响相关人员的积极性。

    +

    2.3. 事后跟踪跟踪。

    2.3.1. 确认发现的问题

    CR结束后,对发现的问题,首先需要确定以下内容。

    +
      +
    1. 问题点的难易程度以及影响的范围;
    2. +
    3. 解决问题的责任者和问题点修正结果的确认者;
    4. +
    5. 解决问题点的时限。
    6. +
    +

    2.3.2. 修正问题责任者

    对于修正问题责任者,在问题点的修正过程中,要三方面内容的记录。

    +
      +
    1. 问题点的原因;
    2. +
    3. 解决问题点的对策;
    4. +
    5. 修正的内容。
    6. +
    +

    2.3.3. 修正结果确认者

    做为修正结果的确认者,必须按照事前约定的时限及时的对修正结果进行全面的确认

    +

    3.注意事项

    3.1. 经常进行Code Review

      +
    1. 要Review的代码越多,那么要重构,重写的代码就会越多。而越不被程序作者接受的建议也会越多,唾沫口水战也会越多。
    2. +
    3. 程序员代码写得时候越长,程序员就会在代码中加入越来越多的个人的东西。
    4. +
    5. 越接近软件发布的最终期限,代码也就不能改得太多。
    6. +
    +

    3.2. Code Review不要太正式,而且要短

    忘了那个代码评审的Checklist吧,走到你的同事座位跟前,像请师父一样请他坐到你的电脑面前,然后,花5分钟给他讲讲你的代码,给他另外一个5分钟让他给你的代码提提意见,这比什么都好。而如果你用了一个Checklist,让这个事情表现得很正式的话,下面两件事中必有一件事会发生:

    +
      +
    1. 只有在Checklist上存在的东西才会被Review。
    2. +
    3. Code Reviews 变成了一种礼节性的东西,你的同事会装做很关心你的代码,但其实他心里想着尽快地离开你。
    4. +
    +

    只有不正式的Code Review才会让你和评审者放轻松,人只有放松了,才会表现得很真实,很真诚。记住Review只不过是一种形式,而只有在相互信任中通过相互的讨论得到了有意义和有建设性的建议和意见,那才是最实在的。不然,作者和评审者的关系就会变成小偷和警察的关系。

    +

    3.3. 尽可能的让不同的人Reivew你的代码

    如果可能的话,不要总是只找一个人来Review你的代码,不同的人有不同的思考方式,有不同的见解,所以,不同的人可以全面的从各个方面评论你的代码。
    但不要太多了,人多嘴杂反而适得其反,基本上来说,不要超过3个人,这是因为,这是一个可以围在一起讨论的最大人员尺寸。

    +

    下面是几个优点:

    +
      +
    1. 从不同的方向评审代码总是好的。
    2. +
    3. 会有更多的人帮你在日后维护你的代码。
    4. +
    5. 这也是一个增加团队凝聚力的方法。
    6. +
    +

    3.4. 保持积极的正面的态度

    程序员最大的问题就是“自负”,尤其当我们Reivew别人的代码的时候,我已经见过无数的场面,程序员在Code Review的时候,开始抨击别人的代码,质疑别人的能力。太可笑了,我分析了一下,这类的程序员其实并没有什么本事,因为他们指责对方的目的是想告诉大家自己有多么的牛,靠这种手段来表现自己的程序员,其实是就是传说中所说的“半瓶水”。

    +

    所以,无论是代码作者,还是评审者,都需要一种积极向上的正面的态度,作者需要能够虚心接受别人的建议,因为别人的建议是为了让你做得更好;评审者也需要以一种积极的正面的态度向作者提意见,因为那是和你在一个战壕里的战友。记住,你不是一段代码,你是一个人!

    +

    3.5. 学会享受Code Reivew

    这可能是最重要的一个提示了,如果你到了一个人人都喜欢Code Reivew的团阿,那么,你会进入到一个生机勃勃的地方,在那里,每个人都能写出质量非常好的代码,在那里,你不需要经理的管理,团队会自适应一切变化,他们相互学习,相互帮助,不仅仅是写出好的代码,而且团队和其中的每个人都会自动进化,最关键的是,这个是一个团队。

    +

    资料来源

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/27/index.html b/page/27/index.html new file mode 100644 index 0000000000..f06f9124b3 --- /dev/null +++ b/page/27/index.html @@ -0,0 +1,570 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前后端分离开发规范

    by lixg

    +

    (本文所说的前后端工作皆站在工作内容本身,不与职称相关,下面皆如此。)

    +

    一、为什么要前后端分离

      +
    1. 前后端专注于各自擅长的领域
    2. +
    3. 前端配置后端代码运行环境,节省搭建环境的时间
    4. +
    5. 明确前后端工作职责
    6. +
    7. 提高开发效率
    8. +
    9. 分离有助于前端、后端分别优化
    10. +
    +

    二、前后端分离存在的问题

      +
    1. 前后端分离必须接口先行,无接口导致前后端关于接口的工作重复出现。
    2. +
    3. 接口对接方式不一致,各项目团队没有采用统一的接口对接方式,导致每次都要熟悉一种新的对接方式。
    4. +
    5. 数据传输格式不一致,导致每次都要重新熟悉,另外由于接口格式不一致还会引起其他未知问题,影响系统健壮性。
    6. +
    7. 人员比例问题,目前公司前后端比例不完全符合前后端分离开发的要求。
    8. +
    +

    为解决上述问题,提高前后端分离开发效率,特制定如下规范。

    +

    三、如何做分离

      +
    1. 职责分离
        +
      • 前后端仅仅通过异步接口(AJAX/JSONP)来编程
      • +
      • 前后端都各自有自己的开发流程,构建工具,测试集合
      • +
      • 关注点分离,前后端变得相对独立并松耦合
        前后端职责
      • +
      +
    2. +
    + + + + + + + + + + + + + + + + + + + + + + + +
    后端前端
    提供数据接收数据,展示数据
    处理业务逻辑处理渲染逻辑
    Server-side MVC架构Client-sideMV*架构
    代码运行在服务器上代码运行在浏览器上
    +
      +
    1. 开发流程
        +
      • 前后端技术负责人约定好接口格式
      • +
      • 后端编写和维护接口文档,在 API 变化时更新接口文档 + MOCK平台
      • +
      • 后端根据接口文档进行接口开发
      • +
      • 前端根据接口文档 + MOCK平台进行开发
      • +
      • 开发完成后联调和提交测试
      • +
      +
    2. +
    +

    MOCK平台统一采用公司搭建的YAPI平台

    +

    YAPI平台可以对接SWAGGER工具进行自动构建MOCK服务
    前后端开发流程
    3. 规范原则
    - 接口返回数据即显示:前端仅做渲染逻辑处理
    - 渲染逻辑禁止跨多个接口调用
    - 前端关注交互、渲染逻辑,尽量避免业务逻辑处理的出现
    - 请求响应传输数据格式:JSON,JSON数据尽量简单轻量,避免多级JSON的出现

    +

    四、基本格式

    接口定义参见《RESTFul API的设计规范》

    请求格式

    GET请求、POST请求==必须包含key为body的入参,所有请求数据包装为JSON格式,并存放到入参body中==,示例如下:

    +

    GET请求:

    +
    1
    xxx/login?body={"username":"admin","password":"123456","captcha":"scfd","rememberMe":1}
    + +

    响应格式

    对于通用业务数据响应参照基本数据格式要求

    +

    响应基本数据格式

    1
    2
    3
    4
    {
    "code": 200,
    "msg": "success"
    }
    +
    响应实体格式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    "code": 200,
    "msg": "success",
    "data": {
    "entity": {
    "id": 1,
    "name": "XXX",
    "phone": "XXX"
    }
    }
    }
    +

    entity: 响应返回的实体数据

    +
    响应列表格式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    {
    "code": 200,
    "msg": "success",
    "data": {
    "list":[
    {
    "id": 1,
    "name": "XXX",
    "code": "XXX"
    },
    {
    "id": 2,
    "name": "XXX",
    "code": "XXX"
    },
    {
    "id": 3,
    "name": "XXX",
    "code": "XXX"
    }
    ]
    }
    }
    +

    list: 响应返回的列表数据

    +
    响应分页格式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    {
    "code": 200,
    "msg":"success",
    "data": {
    "totalCount": 2, // 总记录数
    "totalPage": 1 // 总页数
    "pageNo": 1, // 当前页码
    "pageSize": 10, // 每页大小
    "list":[
    {
    "id": 1,
    "name": "XXX",
    "code": "XXX"
    },
    {
    "id": 2,
    "name": "XXX",
    "code": "XXX"
    },
    {
    "id": 3,
    "name": "XXX",
    "code": "XXX"
    }
    ],
    }
    }
    +

    响应特殊数据格式

    对于特定组件数据格式由后端统一处理后返回前端,如(echart、ztree等组件)

    +

    特殊内容规范

    布尔类型

    关于布尔类型,一律返回BOOLEN类型值

    +
    日期格式

    关于日期类型,JSON数据传输中一律使用字符串格式时间戳,具体日期格式因业务而定

    +

    五、相关文章导读

    前后端分离开发指南-理论篇
    前后端分离开发指南-实践篇

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/28/index.html b/page/28/index.html new file mode 100644 index 0000000000..8ad4d81262 --- /dev/null +++ b/page/28/index.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    背景

    前后端分离狭义上来讲是以浏览器为分界,应用在浏览器内部的技术为前端技术,主要负责页面展示的部分,应用在浏览器外的技术为后端技术,主要负责处理业务逻辑和数据准备工作,但是随着近几年NodeJs的崛起,前端的工作范畴明显扩大,前后端分离也不仅仅作为一种开发模式,更是web应用架构层面的一种模式。

    +

    认识

      +
    1. 在开发阶段,前后端工程师要提前约定好数据交互的接口,从而实现并行开发和测试;
    2. +
    3. 在运行阶段,前后端分离模式需要对web应用进行分离部署,前后端之前使用HTTP或者其他协议进行交互请求。(NodeJs或Ngnix)
    4. +
    +

    由上述两条可以看出前后端分离分为开发阶段和部署阶段,通常来说我们做到的只是前后端分离开发,并没有做到分离部署。

    +

    分解

    作为一种全新的架构模式,前后端分离需要从四个方面来比较和认识。

    +
      +
    1. 交互形式
    2. +
    3. 开发模式/流程
    4. +
    5. 代码组织方式
    6. +
    7. 数据接口规范流程
    8. +
    +

    一、交互形式

    在前后端分离架构中,后端只需要负责按照约定的数据格式向前端提供可调用的API服务即可。前后端之间通过HTTP请求进行交互,前端获取到数据后,进行页面的组装和渲染,最终返回给浏览器。

    +

    这里有一个经常引起激烈讨论的话题就是,“约定的数据格式”,是后端直接提供给前端可直接使用的数据格式,还是简单的数据库拉取数据不做加工直接推给前端,这个要视具体的项目情况而定,如果后端代码需要支持的不仅仅是一端,比如要同时适配pc端、手机端等多个终端,其多终端数据展现的形式如果各不相同,那后端只需要提供满足多端的基础数据格式即可,但如果只需要满足单个终端,那么建议直接将前后端数据格式进行统一处理,方便前后端协同,数据处理可以放在java端做也可以放在NodeJs端做,视人员配备情况而定,但绝不是后端不做任何处理就将数据返回给前端,完全由前端组织数据。

    +

    二、开发模式/流程

    在前后端分离模式未诞生之前,前后端属于一个整体,那时使用的是MVC架构模式,前端对应的就是View层,主要通过html/css/js实现静态页面和动态效果,在有后端进行模板变量的嵌套和一些页面逻辑的处理,最终打包成一个整体,部署到同一服务器上,同时会进行简单的动静态分离部署。

    +

    此时开发的流程如下:
    需求=》前后端并行开发=》前端开发静态页面=》后端套模板=》集成问题=》前后端调整=》再集成=》解决集成问题=》交付上线
    // TODO 这里插图

    +

    出现前后端分离架构之后,前端工程师只需要编写前端页面+前端数据、业务逻辑处理,之后通过HTTP或其他请求方式调用后端提供的服务接口就可以了,而且除了在开发周期可以进行前后端分离,在部署阶段,前后端也可进行分离部署。
    // TODO 这里插图

    +

    此时开发的流程如下:
    需求=》设计接口、约定数据=》前后端并行开发=》集成=》调整=》集成成功=》交付上线

    +

    通过上面的描述及流程,不难发现,前后端分离的开发方式不仅仅从分工上进行了区分,更重要的是在并行开发的问题上解决了反复集成等前后端互相影响的问题,从而降低了开发的难度,简化了开发的流程。

    +

    三、代码组织方式

    // TODO 这里插图
    在传统的开发模式架构下,前端代码是作为项目的静态资源存在于项目工程下,页面中还夹着一些后端代码如jsp、php等技术,前后端开发时需要将整个项目代码完整的引入开发工具才能进行开发,前后端同时维护一份代码,这种开发方式导致前后端代码互相影响,因此前后端分离势在必行。

    +

    而前后端分离模式在代码的组织形式上由以下两种形式组成:

    +
      +
    1. 半分离
      前后端仍共用一个代码库,但是代码分别存放在两个工程中。后端不关心或很少 关心前端元素的输出情况,前端不能独立进行开发和测试,项目中缺乏前后端 交互的测试用例。
    2. +
    3. 完全分离
      完全分离后,前端代码可以通过Mock来模拟后端请求,从此可以独立进行前端开发和测试。后端代码只需要按照跟前端约定好的接口格式写出完整的测试用例,确保接口的可用性。通过上述手段,降低开发集成风险。
    4. +
    +

    四、数据接口规范流程

    通过上面三段的描述,我们可以看出前后端分离开发模式最重要也是最初的阶段就是数据接口的确定,因此在项目开发前必需先进行数据和接口的定义,数据接口的定义需要前后端开发共同商定,包括确定的数据格式,交互形式,并生成一份接口文档供前后端开发人员使用。之后才是并行开发。开发期间前后端双方需要严格按照确定的数据接口文档进行开发,前端开发完之后可以利用mock服务独自进行接口测试,后端也可以利用postman或其他接口测试工具进行测试,并提供完整的接口测试用例,然后前后端进行功能联调,最后再提交线上测试,也可进行自动化测试。

    +

    // TODO 这里插图

    +

    分离后的收益

    到底分不分,如何分是个持续讨论的话题,通过上述的内容大家已经了解到了,什么是前后端分离,也知道如何进行前后端分离开发部署,那么前后端分离能带来哪些收益呢?

    +

    首先,就目前的软件开发应用趋势来看,越来越注重用户的体验性,而且架构越来越大,服务越来越小,而且终端设备越来越丰富,而原来不分离的方式已经不能支撑现在的发展趋势,因此前后端分离开发及部署将势在必行。

    +

    而且采用前后端分离的架构之后,我们将有如下几点提升:

    +
      +
    1. 前后端分离后,前后端将不再互相纠缠而是各自在自己熟悉的领域进行开发工作,这将有利于前后端深化优化各自的代码,培养各自独特的技术特性,从而开发出更加优秀的应用,建立起专业精良的全栈开发团队。

      +
    2. +
    3. 通过前后端分离架构可以实现前后端开发从代码及开发流程上的完全解耦,只需要前后端共同商定好接口后,便可完全独立开发,只需要在联调阶段进行好协作,在此之前可以互不影响的进行并行开发,即是需求发生了变动,但只要不影响接口,后端既可以不用修改代码,只需前端进行变动即可,如此整体的开发效率将得到提升。

      +
    4. +
    5. 前后端分离后,能够更好的适应前端日益增多的的终端适配,代码解耦后复用率更高。

      +
    6. +
    +

    4.前后端分离后,前后端代码可以分别管理,代码不再混在一起,代码可维护性也增强了

    +

    前后端分离后收益不止以上四点,因为分离而带来的职责上、技术上、代码上、部署上的解耦让开发工作比以往任何时候都要更加专注和轻松。

    +

    注意事项

    前后端分离误区

      +
    1. 前端人员不充足,不能进行前后端分离。
    2. +
    +

    此话说来是因为对前后端分离后职责区分不明确导致的问题,因为以往的前端只需要写静态页面就可以了,而前后端分离后前端也不仅仅需要写静态页面,而且还要为页面提供数据和页面逻辑的处理,但实际上可以根据团队情况来区别对待,如果团队前端人员充足,那么可以由前端人员负责多一些的工作,比如API请求后的业务逻辑的处理,页面逻辑的处理、页面数据的准备等,如果后端人员配备充足,那么上述几个环节仍然可由后端人员进行处理,前端开发仍然只是写静态页面,只是内容和逻辑不再写死而是通过js或其他手段如mvvm的框架进行处理。

    +
      +
    1. 前后端分离后前端任务加重,职责也不清晰。
    2. +
    +

    如第一点描述可知,问题不在前后端分离的模式是否合适,而是任务分配和人员分配上的问题,如果前端能力强且人员比例较多,那么部分任务可以由前端承担,如果后端人员多,那么任务由后端承担。

    +
      +
    1. 后端开发需要增加接口开发工作,增加任务量。
    2. +
    +

    无论如何后端开发都是需要写接口的,只是前后端分离后需要按照ResetFul风格写接口,或者采用最新的GraphQl的方式进行交互,如果说这个阶段需要前后端进行商量确定接口交互形式和数据格式花费了时间,但是在接下来前后端并行开发及问题解决上省掉的时间是更加可观的。

    +
      +
    1. 分离后仍出现互相等待的问题,反而不如传统开发模式快。
    2. +
    +

    这个问题的产生其实也是由于对前后端分离后技术缺失导致的,常见情况是前端写完页面逻辑和假数据后后端开发还未完成接口开发导致无法进行联调,实际上前端通过mockserver等方式是可以解决一些问题的。

    +

    前后端分离适用场景

    现代化的web应用适合用前后端分离的开发方式。
    原因有以下几点:

    +
      +
    1. web应用前端页面交互复杂。
        +
      • 页面渲染数据量大。
      • +
      • 页面包含复杂的业务逻辑。
      • +
      +
    2. +
    3. 终端适配情况多。
    4. +
    5. 分布式架构,微服务化应用场景。
    6. +
    +

    前后端分离具体方案

    总体方向

    后端专注于:后端控制层(Restful API) & 服务层 & 数据访问层;

    +

    前端专注于:前端控制层(Nodejs) & 视图层

    +
      +
    1. 项目设计阶段,前后端架构负责人将项目整体进行分析,讨论并确定API风格、职责分配、开发协助模式,确定人员配备;设计确定后,前后端人员共同制定开发接口。

      +
    2. +
    3. 项目开发阶段,前后端分离是各自分工,协同敏捷开发,后端提供Restful API,并给出详细文档说明,前端人员进行页面渲染前台的任务是发送API请(GET,PUT,POST,DELETE等)获取数据(json,xml)后渲染页面。

      +
    4. +
    5. 项目测试阶段,API完成之前,前端人员会使用mock server进行模拟测试,后端人员采用junit进行API单元测试,不用互相等待;API完成之后,前后端再对接测试一下就可以了,当然并不是所有的接口都可以提前定义,有一些是在开发过程中进行调整的。

      +
    6. +
    7. 项目部署阶段,利用nginx 做反向代理,即Java + nodejs + nginx 方式进行。

      +
    8. +
    +

    技术手段

      +
    • 前端技术栈:前端代码 + mock服务
    • +
    • 后端技术栈:postman + 接口 + 后端业务逻辑 + 数据库
    • +
    • 公共依赖:接口文档/接口测试工具
    • +
    +

    常用的mock服务为jsonserver
    常用的接口测试工具为postman、rap、swagger、doclever

    +

    部署方案

      +
    1. 第一阶段为前后端同一个代码库,同一个服务器,集中部署。
    2. +
    3. 第二阶段引入Ngnix服务作为中间件,前端向Ngnix发请求,Ngnix向后端服务发请求,由于Ngnix为静态服务器,所以在seo优化上和页面性能优化上效果不明显,因此前端仍需与后端进行配合才能达到整体的优化。
    4. +
    +

    浏览器 =》 Ngnix(前端机)=》Ngnix(后端机可没有)=》Server服务

    +
      +
    1. 第三阶段引入nodejs作为中间层,将前端资源部署到Server层。同时实现数据代理服务,负责与提供数据的后端进行通信。
    2. +
    +

    浏览器=》Ngnix(前端机)=》NodeServer =》Server服务

    +

    浏览器向前端机发送请求,由Ngnix进行分发,url统一分发至NodeServer,在Node Server中根据请求类型从后端服务器上通过RPC服务请求页面的模板数据,然后进行页面的组装和渲染;API请求则直接转发到后端服务挖成相应。

    +

    前后端分离部署方案比较

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    属性字段传统模式Ngnix+ServerNode+ServerNgnix+Node+Server
    SEOoknookok
    浏览器渲染负担oknookok
    前后端耦合nookokok
    请求相应效率nooknook
    +

    结语

    随着前端技术的快速发展和对用户体验日益增长的需求,前后端分离模式势必将会成为主流趋势。无论是从开发模式的角度上来说,还是对团队成长的角度上来说,前后端分离都会带来益处,让我们一同拥抱前后端分离,打造精良的开发团队,迎接日益复杂的web应用开发需求。

    +

    参考来源

    +

    版权声明

    Copyright by lixuguang
    未经授权,严禁转载。如需转载,请联系作者

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/29/index.html b/page/29/index.html new file mode 100644 index 0000000000..d6ce494d15 --- /dev/null +++ b/page/29/index.html @@ -0,0 +1,548 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    目录
    代码规范 2

    +
      +
    1. 说明 2
    2. +
    3. 基本原则 2
    4. +
    5. C++与Python 2
    6. +
    7. 命名相关 2
    8. +
    9. 工程目录结构 3
    10. +
    +

    Electron代码规范

    +
      +
    1. 说明
      Electron框架正如官网介绍是使用 JavaScript, HTML 和 CSS 构建跨平台的桌面应用的框架,框架分为前端和后端两个部分,根据前后端采用的技术不同分别遵循前后端所采用技术的代码规范
    2. +
    3. 基本原则
    4. +
    +
      +
    1. 不要直接在渲染进程操作主进程,通过进程通信的方式进行数据及事件响应

      +
    2. +
    3. nodejs请使用require方式引入资源不使用import方式引入

      +
    4. +
    5. 多渲染进程传参请使用localStorage或sessionStorage

      +
    6. +
    7. 资源下载不下来时请直接到官网下载https://github.com/electron/electron/releases 将文件放到 用户目录\AppData\Local\electron\Cache 下 再进行 npm install -g electron

      +
    8. +
    9. 代码中包含native模块,必须进行 rebuild操作
      调试渲染进程时增加如下代码增加mainWindow.webContents.openDevTools();

      +
    10. +
    11. +
    +
      +
    1. C++与Python
      对于 C++ 和 Python, 遵循 Chromium 的编码风格. 可以使用 clang-format 来自动格式化 C++ 代码. 可以使用 script/cpplint.py 来检验文件是否符合要求。
    2. +
    +

    现在使用的 Python 版本是 Python 2.7。

    +

    C++ 代码使用了大量 Chromium 的抽象和类型,因此建议使用者熟悉它们。 一个起步的好地方是 Chromium 的《重要的抽象概念和数据库结构》文档. 该文档提到一些特殊类型,范围类型(超出范围时自动释放其内存), 记录机制等。
    4. 命名相关
    Electron API 使用与 Node.js 相同的大小写方案:
    当模块本身是class时, 比如 BrowserWindow, 使用 大驼峰.
    当模块是一组 API 时, 比如 globalShortcut时,使用 小驼峰。
    当 API 是对象的属性时, 并且它复杂到足以成为一个单独的块, 比如 win.webContents, 使用 小驼峰.
    对于其他非模块API, 使用自然标题, 比如 Tag 或 Process Object.

    +

    当创建新的 API 时, 最好使用 getter 和 setter 而不是 jQuery 的一次性函数。 举个例子, .getText() 和 .setText(text) 优于 .text([text]).

    +
      +
    1. 工程目录结构
      src - main
      // 必须

      +
        +
      • main.js // 入口
      • +
      • listen.js // 监听渲染进程通信
      • +
      • windows.js // 创建渲染进程
      • +
      • log.js // 日志log4js 插件
      • +
      +

      // 可选

      +
        +
      • utils.js
      • +
      • store.js
      • +
      +
    2. +
    +
      +
    • api.js
    • +
    • render
    • +
    +

    main 目录下为主进程代码,render 目录下为渲染进程代码
    渲染进程目录结构规范与前端项目目录结构规范一致

    +

    主进程中必须包含入口文件,监听进程文件和窗口创建文件
    其他根据后端实际使用技术可选,nodejs作为后端的话 必须
    6. 打包
    Electron-packager 绿色可执行包
    Electron-builder 压缩安装包

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/3/index.html b/page/3/index.html new file mode 100644 index 0000000000..65fb4f7d5e --- /dev/null +++ b/page/3/index.html @@ -0,0 +1,499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    Network in Compose 在Docker Compose中配置网络

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    version: "3.9"

    services:
    proxy:
    build: ./proxy
    networks:
    - actsnetwork
    app:
    build: ./app
    networks:
    - actsnetwork
    db:
    image: postgres
    networks:
    - testnetwork

    networks:
    actsnetwork:
    name: testnetwork
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/30/index.html b/page/30/index.html new file mode 100644 index 0000000000..b094df3958 --- /dev/null +++ b/page/30/index.html @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    Squid 服务器学习笔记

    Squid 服务器介绍

    用来缓存前端资源的代理服务器,可以类比后端数据库缓存软件Redis。
    当用户访问目标资源时由Squid服务器进行判断如果有数据缓存直接将缓存数据返回,如果没有缓存,代理客户访问资源,获取到后返回给用户,当有其他用户访问相同资源时直接返回资源。

    +

    代理方式

      +
    1. 普通代理
    2. +
    3. 透明代理
    4. +
    5. 反向代理
    6. +
    +

    安装

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 检查是否已经安装
    rpm -qa | grep squid
    // 安装软件
    rpm install squid
    // 设置开机启动
    chkconfig squid on
    // 启动服务
    service squid start
    // 查看服务状态(端口监听3128)
    netstat -anput |grap squid
    // 关闭服务
    service squid stop
    + +

    配置文件

    /etc/squid/squid.config

    +

    10.0.0.0/8

    +
    +

    扩展知识
    CIDR - 无类域间路由CIDR(Classless InterDomain Routing)
    ip每位占1个字节8位,00000000 ~ 11111111,也就是0 ~ 255,ip共有四个位数。
    10.0.0.0/8 的意思是 8位不可变,剩下的可变,8位这里的意思是说第一个IP段,那么也就是说IP范围是10.0.0.0 ~ 10.255.255.255 这样的一个ip范围。

    +
    +

    // 复制并重命名
    mv /etc/squid/squid.config{,.bak}
    // 删除文件中的注释和空行(只保留有效设定)
    awk ‘if($0!~ /^#/ && $0~ !/^$/print $0)’ /etc/squid/squid.config.bak > /etc/squid/squid.config

    +

    squid常用命令:
    /usr/local/squid/sbin/squid -z 初始化缓存空间
    /usr/local/squid/sbin/squid 启动
    /usr/local/squid/sbin/squid -k shutdown 停止
    /usr/local/squid/sbin/squid -k reconfigure 重新载入配置文件
    /usr/local/squid/sbin/squid -k rotate 轮循日志

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    #acl all src 0.0.0.0/0.0.0.0 and http_access allow all选项定义了一个访问控制列表。详细情况参见和Squid软件
    #携带的文档。这里的访问控制列表允许所有对代理服务的访问,因为这里该代理是加速web服务器。
    acl all src 0.0.0.0/0.0.0.0
    #允许所有IP访问
    acl manager proto http #manager url协议为http
    acl localhost src 127.0.0.1/255.255.255.255 #允午本机IP
    acl to_localhost dst 127.0.0.1 #允午目的地址为本机IP
    acl Safe_ports port 80 # 允许安全更新的端口为80
    acl CONNECT method CONNECT #请求方法以CONNECT
    http_access allow all #允许所有人使用该代理.因为这里是代理加速web服务器
    http_reply_access allow all #允许所有客户端使用该代理

    acl OverConnLimit maxconn 16 #限制每个IP最大允许16个连接,防止攻击
    http_access deny OverConnLimit

    icp_access deny all #禁止从邻居服务器缓冲内发送和接收ICP请求.
    miss_access allow all #允许直接更新请求
    ident_lookup_access deny all #禁止lookup检查DNS
    http_port 8080 transparent #指定Squid监听浏览器客户请求的端口号。

    hierarchy_stoplist cgi-bin ? #用来强制某些特定的对象不被缓存,主要是处于安全的目的。
    acl QUERY urlpath_regex cgi-bin \?
    cache deny QUERY

    cache_mem 1 GB #这是一个优化选项,增加该内存值有利于缓存。应该注意的是:
    #一般来说如果系统有内存,设置该值为(n/)3M。现在是3G 所以这里1G
    fqdncache_size 1024 #FQDN 高速缓存大小
    maximum_object_size_in_memory 2 MB #允许最大的文件载入内存

    memory_replacement_policy heap LFUDA #动态使用最小的,移出内存cache
    cache_replacement_policy heap LFUDA #动态使用最小的,移出硬盘cache

    cache_dir ufs /home/cache 5000 32 512 #高速缓存目录 ufs 类型使用的缓冲值最大允午1000MB空间,
    #32个一级目录,512个二级目录

    max_open_disk_fds 0 #允许最大打开文件数量,0 无限制
    minimum_object_size 1 KB #允午最小文件请求体大小
    maximum_object_size 20 MB #允午最大文件请求体大小

    cache_swap_low 90 #最小允许使用swap 90%
    cache_swap_high 95 #最多允许使用swap 95%

    ipcache_size 2048 # IP 地址高速缓存大小 2M
    ipcache_low 90 #最小允许ipcache使用swap 90%
    ipcache_high 95 #最大允许ipcache使用swap 90%


    access_log /var/log/squid/access.log squid #定义日志存放记录
    cache_log /var/log/squid/cache.log squid
    cache_store_log none #禁止store日志

    emulate_httpd_log on #将使Squid仿照Web服务器的格式创建访问记录。如果希望使用
    #Web访问记录分析程序,就需要设置这个参数。

    refresh_pattern . 0 20% 4320 override-expire override-lastmod reload-into-ims ignore-reload #更新cache规则

    acl buggy_server url_regex ^http://.... http:// #只允许http的请求
    broken_posts allow buggy_server

    acl apache rep_header Server ^Apache #允许apache的编码
    broken_vary_encoding allow apache

    request_entities off #禁止非http的标分准请求,防止攻击
    header_access header allow all #允许所有的http报头
    relaxed_header_parser on #不严格分析http报头.
    client_lifetime 120 minute #最大客户连接时间 120分钟

    cache_mgr sky@test.com #指定当缓冲出现问题时向缓冲管理者发送告警信息的地址信息。

    cache_effective_user squid #这里以用户squid的身份Squid服务器
    cache_effective_group squid

    icp_port 0 #指定Squid从邻居服务器缓冲内发送和接收ICP请求的端口号。
    #这里设置为0是因为这里配置Squid为内部Web服务器的加速器,
    #所以不需要使用邻居服务器的缓冲。0是禁用

    cache_peer 127.0.0.1 parent 80 0 no-query default multicast-responder no-netdb-exchange #cache_peer 设置允许更新缓存的主机,因是本机所以127.0.0.1
    cache_peer_domain 127.0.0.1
    hostname_aliases 127.0.0.1

    error_directory /usr/share/squid/errors/Simplify_Chinese #定义错误路径

    always_direct allow all # cache丢失或不存在是允许所有请求直接转发到原始服务器
    ignore_unknown_nameservers on #开反DNS查询,当域名地址不相同时候,禁止访问
    coredump_dir /var/log/squid #定义dump的目录

    max_filedesc 2048 #最大打开的文件描述

    half_closed_clients off #使Squid在当read不再返回数据时立即关闭客户端的连接。
    #有时read不再返回数据是由于某些客户关闭TCP的发送数据
    #而仍然保持接收数据。而Squid分辨不出TCP半关闭和完全关闭。

    buffered_logs on #若打开选项“buffered_logs”可以稍稍提高加速某些对日志文件的写入,该选项主要是实现优化特性。

    acl tianya referer_regex -i tianya #防止天涯盗链,转嫁给百度
    http_access deny tianya
    deny_info tianya

    acl baidu req_header User-Agent Baiduspider #阻止baidu蜘蛛
    http_access deny baidu

    acl OverConnLimit maxconn 128 #限制同一IP客户端的最大连接数
    http_access deny OverConnLimit

    acl myip dst 222.18.63.37 #防止被人利用为HTTP代理,设置允许访问的IP地址
    http_access deny !myip

    acl Manager proto cache_object #允许本地管理
    acl Localhost src 127.0.0.1 222.18.63.37
    http_access allow Manager Localhost
    cachemgr_passwd 53034338 all
    http_access deny Manager

    acl all src 0.0.0.0/0.0.0.0 #仅仅允许80端口的代理
    acl Safe_ports port 80 # http
    http_access deny !Safe_ports
    http_access allow all

    visible_hostname happy.swjtu.edu.cn #Squid信息设置
    cache_mgr ooopic2008@qq.com

    cache_effective_user squid #基本设置
    cache_effective_group squid
    tcp_recv_bufsize 65535 bytes

    cache_peer 127.0.0.1 parent 80 0 no-query originserver #2.6的反向代理加速配置

    error_directory /usr/local/squid/share/errors/Simplify_Chinese #错误文档

    icp_port 0 #单台使用,不使用该功能

    hierarchy_stoplist cgi-bin ?

    acl QUERY urlpath_regex cgi-bin \? .php .cgi .avi .wmv .rm .ram .mpg .mpeg .zip .exe
    cache deny QUERY

    acl apache rep_header Server ^Apache
    broken_vary_encoding allow apache


    refresh_pattern ^ftp: 1440 20% 10080
    refresh_pattern ^gopher: 1440 0% 1440
    refresh_pattern . 0 20% 4320

    cache_store_log none
    pid_filename /usr/local/squid/var/logs/squid.pid
    emulate_httpd_log on
    logformat combined %>a %ui %un [%tl] "%rm %ru HTTP/%rv" %Hs %<st "%{Referer}>h" "%{User-Agent}>h" %Ss:%Sh
    cache_log /usr/local/squid/var/logs/cache.log
    access_log /usr/local/squid/var/logs/access.log combined
    coredump_dir /usr/local/squid/var/cache
    cache_dir ufs /usr/local/squid/var/cache 10000 16 256

    dns_children 32
    hosts_file /etc/hosts

    cache_mem 400 MB
    cache_swap_low 90
    cache_swap_high 95
    maximum_object_size 32768 KB
    maximum_object_size_in_memory 4096 KB
    emulate_httpd_log on

    acl picurl url_regex -i \.bmp$ \.png$ \.jpg$ \.gif$ \.jpeg$ #防止盗链
    acl mystie1 referer_regex -i happy.swjtu.edu.cn
    http_access allow mystie1 picurl
    acl nullref referer_regex -i ^$
    http_access allow nullref
    acl hasref referer_regex -i .+
    http_access deny hasref picurl
    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/31/index.html b/page/31/index.html new file mode 100644 index 0000000000..90d19f6957 --- /dev/null +++ b/page/31/index.html @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    插件安装

    Prettier - Code formatter

    格式化工具

    +

    ESLint

    校验规则

    +

    Vetur

    vue代码片段及代码美化

    +

    Vue 2 Snippets

    vue2 代码片段

    +

    vscode-fileheader

    文件注释

    +

    配置项

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    // setting.json vscode配置文件
    {
    "workbench.startupEditor": "newUntitledFile",
    "terminal.integrated.shell.windows": "C:\\Program Files\\Git\\bin\\bash.exe", \\ 自行配置自己的环境地址
    "javascript.updateImportsOnFileMove.enabled": "always",
    "editor.tabSize": 2,
    "search.exclude": {
    "**/node_modules": true,
    "**/bower_components": true
    },
    "sync.gist": "748b4cae5eb6e56d6997978ead096e8f",
    "breadcrumbs.enabled": true,
    "todohighlight.isEnable": false,
    "liveServer.settings.donotShowInfoMsg": true,
    "search.location": "sidebar",
    "workbench.activityBar.visible": true,
    "window.menuBarVisibility": "default",
    "workbench.statusBar.visible": true,
    "editor.snippetSuggestions": "top",
    "editor.formatOnPaste": true,
    "workbench.colorTheme": "Tiny Light",
    "eslint.validate": [
    "javascript",
    "javascriptreact",
    {
    "language": "html",
    "autoFix": true
    },
    {
    "language": "vue",
    "autoFix": true
    }
    ],
    "prettier.eslintIntegration": true,
    "files.autoSave": "onWindowChange",
    "code-runner.saveAllFilesBeforeRun": true,
    "vetur.format.defaultFormatter.html": "js-beautify-html",
    "vetur.format.defaultFormatter.js": "vscode-typescript",
    "prettier.jsxSingleQuote": true,
    "prettier.requireConfig": false,
    "prettier.arrowParens": "always",
    "typescript.format.insertSpaceAfterSemicolonInForStatements": false,
    "prettier.stylelintIntegration": true,
    "prettier.singleQuote": true,
    "prettier.tslintIntegration": true,
    "eslint.provideLintTask": true,
    "eslint.autoFixOnSave": true,
    "editor.mouseWheelZoom": true,
    "editor.tabCompletion": "on",
    "editor.formatOnType": true,
    "eslint.alwaysShowStatus": true,
    "eslint.options": {
    "configFile": "E:/project/xxjs/fore-core/.eslintrc.js" // 自行配置自己的项目地址
    },
    "fileheader.Author": "Li.Xg", // 自行配置自己的名称
    "fileheader.LastModifiedBy": "Li.Xg" // 自行配置自己的名称
    }
    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/32/index.html b/page/32/index.html new file mode 100644 index 0000000000..5c7b7d10e3 --- /dev/null +++ b/page/32/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    EventLoop 翻译过来就是事件循环,那啥是事件循环啊,这个要从javascript是如何执行的说起。
    要知道javascript是一个运行在浏览器或者Node环境的单线程执行的脚本语言,但是为了解决单线程阻塞的问题,这里就引入了事件循环的机制,也就是我们常说的异步特性。

    +

    但是虽然都是JavaScript,都是事件循环,都有异步特性,但浏览器环境和Node环境的实现方式是不一样的,这里要先说明一下。
    浏览器的事件循环是HTML定义的规范,而Node环境是利用libuv库实现的,这里我们先从浏览器的事件循环开始说。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/33/index.html b/page/33/index.html new file mode 100644 index 0000000000..37efe5fb1b --- /dev/null +++ b/page/33/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    今天一个小兄弟微信给我发了个消息,说是今天面试,面试官问了个比较基础的问题,没答好,我就问他问了什么,他说,数组有哪些方法会改变原来的值,他说他就说了3、4个,还不确定对不对,我想了想说其实列个表就很容易记住了,于是便有了下面这个表格。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/34/index.html b/page/34/index.html new file mode 100644 index 0000000000..c50a6c4750 --- /dev/null +++ b/page/34/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    还记得你的前端工程里有这样一个文件‘package.json’,里面管理者运行代码所需要的各种依赖,每种依赖都会有一个版本号,那么你知道版本号是怎么定义的么?不知道的话就看看下面的内容吧。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/35/index.html b/page/35/index.html new file mode 100644 index 0000000000..ea371624da --- /dev/null +++ b/page/35/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/36/index.html b/page/36/index.html new file mode 100644 index 0000000000..d403ad5b50 --- /dev/null +++ b/page/36/index.html @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    在移动 App 开发领域,有一个框架非常知名,每每面试都会被问起,“你用过 Ionic 么?”,是的,就是 Ionic 框架。

    +

    之前已经学习过了 Cordova ,它和 Ionic 可以说是一对黄金搭档,一个面向 MobileOS ,一个面向 UI ,很多人搞不清楚它们之间的区别,没关系,回头我再抽时间讲一下,今天我们主要来认识一下 Ionic

    +

    好戏开始:)

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/37/index.html b/page/37/index.html new file mode 100644 index 0000000000..bd2ac5b3a9 --- /dev/null +++ b/page/37/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    很长时间没有写博客了,因为最近换了工作,原来的技术栈基本上都换了,现在接触了很多新的技术栈,Cordova 就是其中之一。

    +

    接下来这篇文章就是我的学习笔记,以供回顾,写的不好请勿拍砖。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/38/index.html b/page/38/index.html new file mode 100644 index 0000000000..f96f391520 --- /dev/null +++ b/page/38/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/39/index.html b/page/39/index.html new file mode 100644 index 0000000000..a67bd4f70a --- /dev/null +++ b/page/39/index.html @@ -0,0 +1,523 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    /bin:

    bin是Binary的缩写, 这个目录存放着最经常使用的命令。

    +

    /boot:

    这里存放的是启动Linux时使用的一些核心文件,包括一些连接文件以及镜像文件。

    +

    /dev :

    dev是Device(设备)的缩写, 该目录下存放的是Linux的外部设备,在Linux中访问设备的方式和访问文件的方式是相同的。

    +

    /etc:

    这个目录用来存放所有的系统管理所需要的配置文件和子目录。

    +

    /home:

    用户的主目录,在Linux中,每个用户都有一个自己的目录,一般该目录名是以用户的账号命名的。

    +

    /lib:

    这个目录里存放着系统最基本的动态连接共享库,其作用类似于Windows里的DLL文件。几乎所有的应用程序都需要用到这些共享库。

    +

    /lost+found:

    这个目录一般情况下是空的,当系统非法关机后,这里就存放了一些文件。

    +

    /media linux

    系统会自动识别一些设备,例如U盘、光驱等等,当识别后,linux会把识别的设备挂载到这个目录下。

    +

    /mnt:

    系统提供该目录是为了让用户临时挂载别的文件系统的,我们可以将光驱挂载在/mnt/上,然后进入该目录就可以查看光驱里的内容了。

    +

    /opt:

    这是给主机额外安装软件所摆放的目录。比如你安装一个ORACLE数据库则就可以放到这个目录下。默认是空的。

    +

    /proc:

    这个目录是一个虚拟的目录,它是系统内存的映射,我们可以通过直接访问这个目录来获取系统信息。
    这个目录的内容不在硬盘上而是在内存里,我们也可以直接修改里面的某些文件,比如可以通过下面的命令来屏蔽主机的ping命令,使别人无法ping你的机器:

    +

    /root:

    该目录为系统管理员,也称作超级权限者的用户主目录。

    +

    /sbin:

    s就是Super User的意思,这里存放的是系统管理员使用的系统管理程序。

    +

    /selinux:

    这个目录是Redhat/CentOS所特有的目录,Selinux是一个安全机制,类似于windows的防火墙,但是这套机制比较复杂,这个目录就是存放selinux相关的文件的。

    +

    /srv:

    该目录存放一些服务启动之后需要提取的数据。

    +

    /sys:

    这是linux2.6内核的一个很大的变化。该目录下安装了2.6内核中新出现的一个文件系统 sysfs 。

    +

    sysfs文件系统集成了下面3种文件系统的信息:针对进程信息的proc文件系统、针对设备的devfs文件系统以及针对伪终端的devpts文件系统。
    该文件系统是内核设备树的一个直观反映。

    +

    当一个内核对象被创建的时候,对应的文件和目录也在内核对象子系统中被创建。

    +

    /tmp:

    这个目录是用来存放一些临时文件的。

    +

    /usr:

    这是一个非常重要的目录,用户的很多应用程序和文件都放在这个目录下,类似与windows下的program files目录。

    +

    /usr/bin:

    系统用户使用的应用程序。

    +

    /usr/sbin:

    超级用户使用的比较高级的管理程序和系统守护程序。

    +

    /usr/src:

    内核源代码默认的放置目录。

    +

    /var:

    这个目录中存放着在不断扩充着的东西,我们习惯将那些经常被修改的目录放在这个目录下。包括各种日志文件。

    +

    在linux系统中,有几个目录是比较重要的,平时需要注意不要误删除或者随意更改内部文件。
    系统上跑了很多程序,那么每个程序都会有相应的日志产生,而这些日志就被记录到这个目录下,具体在/var/log 目录下,另外mail的预设放置也是在这里。

    +

    /etc:

    上边也提到了,这个是系统中的配置文件,如果你更改了该目录下的某个文件可能会导致系统不能启动。

    +

    /bin, /sbin, /usr/bin, /usr/sbin:

    这是系统预设的执行文件的放置目录,比如 ls 就是在/bin/ls 目录下的。

    +

    值得提出的是,/bin, /usr/bin 是给系统用户使用的指令(除root外的普通用户),而/sbin, /usr/sbin 则是给root使用的指令。

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/4/index.html b/page/4/index.html new file mode 100644 index 0000000000..175df02f85 --- /dev/null +++ b/page/4/index.html @@ -0,0 +1,499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    初始化数据库

    c:\pgsql\bin\initdb.exe -D c:\pgsql\data -E UTF8

    +

    注册表

    c:\pgsql\bin\pg_ctl register -N postgres -D c:\pgsql\data

    +

    启动服务

    c:\pgsql\bin\postgres -D c:\pgsql\data

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/40/index.html b/page/40/index.html new file mode 100644 index 0000000000..2b325b09ff --- /dev/null +++ b/page/40/index.html @@ -0,0 +1,576 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    安装

    sudo rpm -ivh [lz4-1.8.3-1.el7.x86_64.rpm]

    +

    卸载

    sudo rpm -e [postgresql14-server-14.4-1PGDG.rhel7.x86_64]

    +

    查询功能

    命令格式 rpm {-q|–query} [select-options] [query-options]

    +

      RPM的查询功能是极为强大,是极为重要的功能之一;举几个常用的例子,更为详细的具体的,请参考#man rpm

    +

    1、对系统中已安装软件的查询

    +

    1)查询系统已安装的软件

    +

      语法:rpm -q 软件名

    +

      举例:[root@localhost beinan]# rpm -q gaim

    +

      gaim-1.3.0-1.fc4   

    +
       查看系统中所有已经安装的包,要加 -a 参数 ;
    +
    +

      [root@localhost RPMS]# rpm -qa

    +

      如果分页查看,再加一个管道 |和more命令;

    +

      [root@localhost RPMS]# rpm -qa |more

    +

      在所有已经安装的软件包中查找某个软件,比如说 gaim ;可以用 grep 抽取出来;

    +

      [root@localhost RPMS]# rpm -qa |grep gaim

    +

      上面这条的功能和 rpm -q gaim 输出的结果是一样的;

    +

    2)查询一个已经安装的文件属于哪个软件包

    +

      语法 rpm -qf 文件名

    +

      注:文件名所在的绝对路径要指出

    +

      举例:

    +

      [root@localhost RPMS]# rpm -qf /usr/lib/libacl.la

    +

      libacl-devel-2.2.23-8

    +

    3)查询已安装软件包都安装到何处

    +

      语法:rpm -ql 软件名 或 rpm rpmquery -ql 软件名

    +

      举例:

    +

      [root@localhost RPMS]# rpm -ql lynx

    +

      [root@localhost RPMS]# rpmquery -ql lynx

    +

    4)查询一个已安装软件包的信息

    +

      语法格式: rpm -qi 软件名

    +

      举例:

    +

      [root@localhost RPMS]# rpm -qi lynx

    +

    5)查看一下已安装软件的配置文件

    +

      语法格式:rpm -qc 软件名

    +

      举例:

    +

      [root@localhost RPMS]# rpm -qc lynx

    +

    6)查看一个已经安装软件的文档安装位置

    +

      语法格式: rpm -qd 软件名

    +

      举例:

    +

      [root@localhost RPMS]# rpm -qd lynx

    +

    7)查看一下已安装软件所依赖的软件包及文件

    +

      语法格式: rpm -qR 软件名

    +

      举例:

    +

      [root@localhost beinan]# rpm -qR rpm-python

    +

      查询已安装软件的总结:对于一个软件包已经安装,我们可以把一系列的参数组合起来用;比如 rpm -qil ;比如:

    +

      [root@localhost RPMS]# rpm -qil lynx

    +

    2、对于未安装的软件包的查看:

    +

      查看的前提是您有一个.rpm 的文件,也就是说对既有软件file.rpm的查看等;

    +

    1)查看一个软件包的用途、版本等信息;

    +

      语法: rpm -qpi file.rpm

    +

      举例:

    +

      [root@localhost RPMS]# rpm -qpi lynx-2.8.5-23.i386.rpm

    +

    2)查看一件软件包所包含的文件;

    +

      语法: rpm -qpl file.rpm

    +

      举例:

    +

      [root@localhost RPMS]# rpm -qpl lynx-2.8.5-23.i386.rpm

    +

    3)查看软件包的文档所在的位置;

    +

      语法: rpm -qpd file.rpm

    +

      举例:

    +

      [root@localhost RPMS]# rpm -qpd lynx-2.8.5-23.i386.rpm

    +

    4)查看一个软件包的配置文件;

    +

      语法: rpm -qpc file.rpm

    +

      举例:

    +

      [root@localhost RPMS]# rpm -qpc lynx-2.8.5-23.i386.rpm

    +

    5)查看一个软件包的依赖关系

    +

      语法: rpm -qpR file.rpm

    +

      举例:

    +

      [root@localhost archives]# rpm -qpR yumex_0.42-3.0.fc4_noarch.rpm

    +

      /bin/bash

    +

      /usr/bin/python

    +

      config(yumex) = 0.42-3.0.fc4

    +

      pygtk2

    +

      pygtk2-libglade

    +

      rpmlib(CompressedFileNames) <= 3.0.4-1

    +

      rpmlib(PayloadFilesHavePrefix) <= 4.0-1

    +

      usermode

    +

      yum >= 2.3.2

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/41/index.html b/page/41/index.html new file mode 100644 index 0000000000..00c7b9fb4f --- /dev/null +++ b/page/41/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    前言

    最近在做代码风格统一,用到了Prettier,暂时先记录一下现在的配置信息,回头抽时间写篇总结文。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/42/index.html b/page/42/index.html new file mode 100644 index 0000000000..7da790174b --- /dev/null +++ b/page/42/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    今天在看一个叫做Avue的框架组件时看到了一个叫做“全局水印”的组件,功能呢就是给当前页面加上一个水印遮罩,就像下图这样:
    Y08cbd.png
    因为工作中有这样的场景,所以我很好奇看了一下他的代码,他的水印设置为了fixed定位,并且z-index设置的很高,但是它具有穿透性,最终我发现其中起作用的是一个叫做“pointer-events”的css3新属性,于是为了搞清楚它的作用,我在网上做了一番搜索,并把自己的认识整理一下。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/43/index.html b/page/43/index.html new file mode 100644 index 0000000000..39c0cbe6f6 --- /dev/null +++ b/page/43/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    最近在阅读一些文章时,经常看到介绍命令的时候用到了 npx 关键字,之前知道有 node 有 npm 但是很少用到 npx ,npx是个啥呢?好奇上网上找了些资料学习了一下,写篇学习笔记记录一下。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/44/index.html b/page/44/index.html new file mode 100644 index 0000000000..599f22b905 --- /dev/null +++ b/page/44/index.html @@ -0,0 +1,510 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    市面上目前已有各种各样的UI组件库,比如 Element 和 iView,他们的强大毋庸置疑。但是我们面临的情况是需求越来越复杂,当它们不能再满足我们需求的时候,这个时候就有必要开发一套属于自己团队的组件库了。

    +

    所以本文的目的就是让读者能通过此文,小能做一个简单的插件供人使用,大能架构和维护一个组件库不在话下。

    +

    以下一个简单的颜色选择器插件vColorPicker讲述从开发到上线到npm的流程。

    +

    vColorPicker 插件 DEMO

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/45/index.html b/page/45/index.html new file mode 100644 index 0000000000..ec11d213e7 --- /dev/null +++ b/page/45/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    现代前端开发不用点预编译css都不好意思说自己咋写样式,虽说这么说有点夸张,但是确实是一个普遍现象吧,我们在开发中常见到的预编译css的话主要是sass和less,那么今天的主角就是其中的sass,可以说sass让人又爱又恨,爱呢是喜欢她赋予了css函数的特性,恨呢就是用的时候实在是麻烦,所以有的人就投奔了less的怀抱,他俩很像,但是有的项目已经用了sass那也没办法,嫁鸡随鸡,但是日子还得过,想办法过的好一点嘛。

    +

    其实这里说到sass用起来麻烦主要还是在安装依赖的时候,想必用过的人都知道我说的是啥,原来只能是不停的重试,最近在知乎上看到篇文章,聪明的解决了这个问题,所以赶紧学习记录下来,学会了就是自己的。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/46/index.html b/page/46/index.html new file mode 100644 index 0000000000..3ce539ad0c --- /dev/null +++ b/page/46/index.html @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    知乎真是个好地方,关注好自己喜欢的话题,经常会推送一些好的话题到你面前,这不刚刚就看到了一个人提问

    +
    +

    前端全栈和大前端有啥区别

    +
    +

    以前只听说过全栈,听说过大前端,但是没听说过前端全栈,对于前两个我自己也有一些理解,但从不敢说自己理解的对,带着强烈的好奇心我打开了这个问题,也看到了一位前端大神‘狼叔’的回复,感觉还是很到位的,也让我重新验证了自己的理解,所以就将狼叔的解答与我自己的认识相结合一下,做个记录。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/47/index.html b/page/47/index.html new file mode 100644 index 0000000000..16945fa3e7 --- /dev/null +++ b/page/47/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/48/index.html b/page/48/index.html new file mode 100644 index 0000000000..a58f852348 --- /dev/null +++ b/page/48/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    最近在检查公司前端代码质量时,经常会在sonar检查到有漏洞级别的问题特别多,但是打开查看详情的时候发现又特别低级,都是些console没去掉啊,debugger没去掉啊,之前都是看到了就提醒一下,但是屡禁不止,那么有没有什么好的办法能够从源头上避免这个问题呢,果然这个问题不止我一个人遇到,网上还是有大神给出了解决方案,那就是 webpack 打包时可以引入 UglifyJsPlugin 来解决这个扰人的问题,而且屏蔽了人为的因素,使用感觉是极好的,所以,写个帖子把网上学来的资料整理一下。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/49/index.html b/page/49/index.html new file mode 100644 index 0000000000..d9b446f0fc --- /dev/null +++ b/page/49/index.html @@ -0,0 +1,506 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    目前,前端领域中 React 势头正盛,使用者众多却少有能够深入剖析内部实现机制和原理。本系列文章希望通过剖析 React 源码,理解其内部的实现原理,知其然更要知其所以然。

    +

    React diff 作为 Virtual DOM 的加速器,其算法上的改进优化是 React 整个界面渲染的基础,以及性能提高的保障,同时也是 React 源码中最神秘、最不可思议的部分,本文从源码入手,深入剖析 React diff 的不可思议之处。

    +

    阅读本文需要对 React 有一定的了解,如果你不知何为 React,请详读 React 官方文档
    如果你对 React diff 存在些许疑惑,或者你对算法优化感兴趣,那么本文值得阅读和讨论。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/5/index.html b/page/5/index.html new file mode 100644 index 0000000000..436957bc1e --- /dev/null +++ b/page/5/index.html @@ -0,0 +1,565 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    Nginx中的超时设置

    client_header_timeout 指定等待client发送一个请求头的超时时间

      +
    • 语法: client_header_timeout time
    • +
    • 默认值: 60s
    • +
    • 上下文: http server
    • +
    • 说明: 指定等待client发送一个请求头的超时时间(例如:GET / HTTP/1.1).仅当在一次read中,没有收到请求头,才会算成超时。如果在超时时间内,client没发送任何东西,nginx返回HTTP状态码408(“Request timed out”)
    • +
    +

    client_body_timeout 该指令设置请求体(request body)的读超时时间

      +
    • 语法: client_body_timeout time
    • +
    • 默认值: 60s
    • +
    • 上下文: http server location
    • +
    • 说明: 该指令设置请求体(request body)的读超时时间。仅当在一次readstep中,没有得到请求体,就会设为超时。超时后,nginx返回HTTP状态码408(“Request timed out”)
    • +
    +

    keepalive_timeout 第一个参数指定了与client的keep-alive连接超时时间

      +
    • 语法: keepalive_timeout timeout [ header_timeout ]

      +
    • +
    • 默认值: 75s

      +
    • +
    • 上下文: http server location

      +
    • +
    • 说明: 第一个参数指定了与client的keep-alive连接超时时间。服务器将会在这个时间后关闭连接。可选的第二个参数指定了在响应头Keep-Alive: timeout=time中的time值。这个头能够让一些浏览器主动关闭连接,这样服务器就不必要去关闭连接了。没有这个参数,nginx不会发送Keep-Alive响应头(尽管并不是由这个头来决定连接是否“keep-alive”)
      两个参数的值可并不相同

      +
        +
      • 注意不同浏览器怎么处理“keep-alive”头
      • +
      • MSIE和Opera忽略掉”Keep-Alive: timeout=“ header.
      • +
      • MSIE保持连接大约60-65秒,然后发送TCP RST
      • +
      • Opera永久保持长连接
      • +
      • Mozilla keeps the connection alive for N plus about 1-10 seconds.
      • +
      • Konqueror保持长连接N秒
      • +
      +
    • +
    +

    lingering_timeout

      +
    • 语法: lingering_timeout time
    • +
    • 默认值: 5s
    • +
    • 上下文: http server location
    • +
    • 说明: lingering_close生效后,在关闭连接前,会检测是否有用户发送的数据到达服务器,如果超过lingering_timeout时间后还没有数据可读,就直接关闭连接;否则,必须在读取完连接缓冲区上的数据并丢弃掉后才会关闭连接。
    • +
    +

    resolver_timeout 设置DNS解析超时时间

      +
    • 语法 resolver_timeout time
    • +
    • 默认值 30s
    • +
    • 上下文 http server location
    • +
    • 说明 该指令设置DNS解析超时时间
    • +
    +

    proxy_connect_timeout 设置与upstream server的连接超时时间,不能超过75秒

      +
    • 语法 proxy_connect_timeout time
    • +
    • 默认值 60s
    • +
    • 上下文 http server location
    • +
    • 说明 该指令设置与upstream server的连接超时时间,有必要记住,这个超时不能超过75秒。
      这个不是等待后端返回页面的时间,那是由proxy_read_timeout声明的。如果你的upstream服务器起来了,但是hanging住了(例如,没有足够的线程处理请求,所以把你的请求放到请求池里稍后处理),那么这个声明是没有用的,由于与upstream服务器的连接已经建立了。
    • +
    +

    proxy_read_timeout 设置与代理服务器的读超时时间

      +
    • 语法 proxy_read_timeout time
    • +
    • 默认值 60s
    • +
    • 上下文 http server location
    • +
    • 说明 该指令设置与代理服务器的读超时时间。它决定了nginx会等待多长时间来获得请求的响应。这个时间不是获得整个response的时间,而是两次reading操作的时间。
    • +
    +

    proxy_send_timeout 设置发送请求给upstream服务器的超时时间

      +
    • 语法 proxy_send_timeout time
    • +
    • 默认值 60s
    • +
    • 上下文 http server location
    • +
    • 说明 这个指定设置了发送请求给upstream服务器的超时时间。超时设置不是为了整个发送期间,而是在两次write操作期间。如果超时后,upstream没有收到新的数据,nginx会关闭连接
    • +
    +

    proxy_upstream_fail_timeout(fail_timeout)设置某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间

      +
    • 语法 server address [fail_timeout=30s]
    • +
    • 默认值 10s
    • +
    • 上下文 upstream
    • +
    • 说明 Upstream模块下 server指令的参数,设置了某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间,默认为10秒
    • +
    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/50/index.html b/page/50/index.html new file mode 100644 index 0000000000..3146e8ba92 --- /dev/null +++ b/page/50/index.html @@ -0,0 +1,784 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    推荐序

    这里是我自己写的,看了这篇文章把我零碎的 webpack 知识系统的整理了一下,感觉受益匪浅,推荐更多小伙伴看一看这篇文章,好文共享,建议稍微对 webpack 了解服用更佳。
    2020年了,再不会webpack敲得代码就不香了(近万字实战)

    +

    前言

    2020年即将到来,在众多前端的招聘要求里, webpack 、工程化这些字眼频率越来越高。日常开发者中,我们常常在用诸如 vue-clicreate-react-app 的脚手架来构建我们的项目。但是如果你想在团队脱颖而出(鹤立鸡群)、拿到更好的 offer (还房贷),那么你必须去深刻的认识下我们经常打交道的 webpack

    +

    入门(一起来用这些小例子让你熟悉webpack的配置)

    webpack 是什么?

    webpack 是一个现代 JavaScript 应用程序的静态模块打包器,当 webpack 处理应用程序时,会递归构建一个依赖关系图,其中包含应用程序需要的每个模块,然后将这些模块打包成一个或多个 bundle

    +

    webpack 的核心概念

      +
    • entry: 入口
    • +
    • output: 输出
    • +
    • loader: 模块转换器,用于把模块原内容按照需求转换成新内容
    • +
    • plugins: 扩展插件,在 webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要做的事情
    • +
    +

    初始化项目

    新建一个目录,初始化 npm

    +
    1
    npm init
    + +

    webpack 是运行在 node 环境中的,我们需要安装以下两个 npm

    +
    1
    npm i -D webpack webpack-cli
    + +
    +
      +
    • npm i -Dnpm install --save-dev 的缩写
    • +
    • npm i -Snpm install --save 的缩写
    • +
    +
    +

    新建一个文件夹 src ,然后新建一个文件 main.js ,写一点代码测试一下

    +
    1
    console.log('call me 老yuan')
    + +

    配置 package.json 命令

    +
    1
    2
    3
    "script":{
    "build":"webpack src/main.js"
    }
    +

    执行

    +
    1
    npm run build
    +

    此时如果生成了一个 dist 文件夹,并且内部含有 main.js 说明已经打包成功了

    +

    开始我们自己的配置

    上面一个简单的例子只是 webpack 自己默认的配置,下面我们要实现更加丰富的自定义配置

    +

    新建一个 build 文件夹,里面新建一个 webpack.config.js

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // webpack.config.js

    const path = require('path');
    module.exports = {
    mode:'development', // 开发模式
    entry: path.resolve(__dirname,'../src/main.js'), // 入口文件
    output: {
    filename: 'output.js', // 打包后的文件名称
    path: path.resolve(__dirname,'../dist') // 打包后的目录
    }
    }
    +

    更改我们的打包命令

    +
    1
    2
    3
    "script":{
    "build":"webpack build/webpack.config.js"
    }
    +

    执行 npm run build
    会发现生成了以下目录

    +
    1
    2
    3
    4
    project
    dist
    build
    src
    +

    其中 dist 文件夹中的 main.js 就是我们需要在浏览器中实际运行的文件
    当然实际运用中不会仅仅如此,下面让我们通过实际案例带你快速入手 webpack

    +

    配置html模板

    js 文件打包好了,但是我们不可能每次在html文件中手动引入打包好的js

    +
    +

    这里可能有的朋友会认为我们打包 js 文件名称不是一直是固定的嘛( output.js )?这样每次就不用改动引入文件名称了呀?实际上我们日常开发中往往会这样配置:

    +
    +
    1
    2
    3
    4
    5
    6
    7
    module.exports = {
    // 省略其他配置
    output: {
    filename: '[name].[hash:8].js', // 打包后的文件名称
    path: path.resolve(__dirname,'../dist') // 打包后的目录
    }
    }
    + +

    这时候生成的 dist 目录文件如下

    +
    1
    2
    dist/
    app.fsafasf.js
    +

    为了缓存,你会发现打包好的 js 文件的名称每次都不一样。 webpack 打包出来的js文件我们需要引入到 html 中,但是每次我们都手动修改 js 文件名显得很麻烦,因此我们需要一个插件来帮我们完成这件事情,那就是 html-webpack-plugin

    +
    1
    npm i -D html-webpack-plugin
    +

    新建一个 build 同级的文件夹 public ,里面新建一个 index.html
    具体配置文件如下

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // webpack.config.js
    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-plugin') // **此插件用来解决html模板引入js文件
    module.exports = {
    mode:'development', // 开发模式
    entry: path.resolve(__dirname,'../src/main.js'), // 入口文件
    output: {
    filename: '[name].[hash:8].js', // 打包后的文件名称
    path: path.resolve(__dirname,'../dist') // 打包后的目录
    },
    plugins:[
    new HtmlWebpackPlugin({
    template:path.resolve(__dirname,'../public/index.html')
    })
    ]
    }
    +

    可以发现打包生成的js文件已经被自动引入 html 文件中

    +

    多入口文件如何开发

    +

    生成多个 html-webpack-plugin 实例来解决这个问题

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    module.exports = {
    mode:'development', // 开发模式
    entry: {
    main:path.resolve(__dirname,'../src/main.js'),
    header:path.resolve(__dirname,'../src/header.js')
    },
    output: {
    filename: '[name].[hash:8].js', // 打包后的文件名称
    path: path.resolve(__dirname,'../dist') // 打包后的目录
    },
    plugins:[
    new HtmlWebpackPlugin({
    template:path.resolve(__dirname,'../public/index.html'),
    filename:'index.html',
    chunks:['main'] // 与入口文件对应的模块名
    }),
    new HtmlWebpackPlugin({
    template:path.resolve(__dirname,'../public/header.html'),
    filename:'header.html',
    chunks:['header'] // 与入口文件对应的模块名
    }),
    ]
    }
    + +

    clean-webpack-plugin

    +

    每次执行 npm run build 会发现 dist 文件夹里会残留上次打包的文件,这里我们推荐一个 plugin 来帮我们在打包输出前清空文件夹 clean-webpack-plugin

    +
    +
    1
    2
    3
    4
    5
    const {CleanWebpackPlugin} = require('clean-webpack-plugin')
    module.exports = {
    // ...省略其他配置
    plugins:[new CleanWebpackPlugin()]
    }
    +
    希望dist目录下某个文件夹不被清空

    不过呢,有些时候,我们并不希望整个 dist 目录都被清空,比如,我们不希望,每次打包的时候,都删除 dll 目录,以及 dll 目录下的文件或子目录,该怎么办呢?

    +

    clean-webpack-plugin 为我们提供了参数 cleanOnceBeforeBuildPatterns

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    //webpack.config.js
    module.exports = {
    //...
    plugins: [
    new CleanWebpackPlugin({
    cleanOnceBeforeBuildPatterns:['**/*', '!dll', '!dll/**'] //不删除dll目录下的文件
    })
    ]
    }
    + +

    引用CSS

    我们的入口文件是 js ,所以我们在入口 js 中引入我们的 css 文件

    +
    1
    import 'asset/style.css'
    +

    同时我们也需要一些 loader 来解析我们的 css 文件

    +
    1
    npm i -D style-loader css-loader
    +

    如果我们使用 less 来构建样式,则需要多安装两个

    +
    1
    npm i -D less less-loader
    +

    配置文件如下

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // webpack.config.js
    module.exports = {
    // ...省略其他配置
    module:{
    rules:[
    {
    test:/\.css$/,
    use:['style-loader','css-loader'] // 从右向左解析原则
    },
    {
    test:/\.less$/,
    use:['style-loader','css-loader','less-loader'] // 从右向左解析原则
    }
    ]
    }
    }
    + +

    我们简单说一下上面的配置:

    +
      +
    • style-loader 动态创建 style 标签,将 css 插入到 head 中.
    • +
    • css-loader 负责处理 @import 等语句。
    • +
    • postcss-loaderautoprefixer ,自动生成浏览器兼容性前缀 —— 2020了,应该没人去自己徒手去写浏览器前缀了吧
    • +
    • less-loader 负责处理编译 .less 文件,将其转为 css
    • +
    +
    +

    注意:
    loader 的执行顺序是从右向左执行的,也就是后面的 loader 先执行,上面 loader 的执行顺序为: less-loader —> postcss-loader —> css-loader —> style-loader
    当然,loader 其实还有一个参数,可以修改优先级,enforce 参数,其值可以为: pre(优先执行) 或 post (滞后执行)。
    现在,我们已经可以处理 .less 文件啦,.css 文件只需要修改匹配规则,删除 less-loader 即可。

    +
    +

    为css添加浏览器前缀

    1
    2
    npm i -D postcss-loader autoprefixer

    +

    配置如下

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // webpack.config.js
    module.exports = {
    module:{
    rules:[
    test/\.less$/,
    use:['style-loader','css-loader','postcss-loader','less-loader'] // 从右向左解析原则
    ]
    }
    }
    +

    接下来,我们还需要引入 autoprefixer 使其生效,这里有两种方式

    +
    在项目根目录下创建一个postcss.config.js文件,配置如下:
    1
    2
    3
    module.exports = {
    plugins: [require('autoprefixer')] // 引用该插件即可了
    }
    +
    直接在webpack.config.js里配置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // webpack.config.js
    module.exports = {
    //...省略其他配置
    module:{
    rules:[{
    test:/\.less$/,
    use:['style-loader','css-loader',{
    loader:'postcss-loader',
    options:{
    plugins:[require('autoprefixer')]
    }
    },'less-loader'] // 从右向左解析原则
    }]
    }
    }
    +

    这时候我们发现 css 通过 style 标签的方式添加到了 html 文件中,但是如果样式文件很多,全部添加到 html 中,难免显得混乱。这时候我们想用把 css 拆分出来用外链的形式引入 css 文件怎么做呢?这时候我们就需要借助插件来帮助我们

    +

    拆分css

    1
    npm i -D mini-css-extract-plugin
    +
    +

    webpack 4.0 以前,我们通过 extract-text-webpack-plugin 插件,把 css 样式从 js 文件中提取到单独的 css 文件中。 webpack4.0 以后,官方推荐使用 mini-css-extract-plugin 插件来打包 css 文件

    +
    +

    配置文件如下

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    const MiniCssExtractPlugin = require("mini-css-extract-plugin");
    module.exports = {
    //...省略其他配置
    module: {
    rules: [
    {
    test: /\.less$/,
    use: [
    MiniCssExtractPlugin.loader,
    'css-loader',
    'less-loader'
    ],
    }
    ]
    },
    plugins: [
    new MiniCssExtractPlugin({
    filename: "[name].[hash].css",
    chunkFilename: "[id].css",
    })
    ]
    }
    +

    拆分多个css

    +

    这里需要说的细一点,上面我们所用到的 mini-css-extract-plugin 将所有的 css 样式合并为一个 css 文件。如果你想拆分为一一对应的多个 css 文件,我们需要使用到 extract-text-webpack-plugin ,而目前 mini-css-extract-plugin 还不支持此功能。我们需要安装 @next 版本的 extract-text-webpack-plugin

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    npm i -D extract-text-webpack-plugin@next
    // webpack.config.js

    const path = require('path');
    const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')
    let indexLess = new ExtractTextWebpackPlugin('index.less');
    let indexCss = new ExtractTextWebpackPlugin('index.css');
    module.exports = {
    module:{
    rules:[
    {
    test:/\.css$/,
    use: indexCss.extract({
    use: ['css-loader']
    })
    },
    {
    test:/\.less$/,
    use: indexLess.extract({
    use: ['css-loader','less-loader']
    })
    }
    ]
    },
    plugins:[
    indexLess,
    indexCss
    ]
    }
    +

    打包 图片、字体、媒体、等文件

    file-loader 就是将文件在进行一些处理后(主要是处理文件名和路径、解析文件 url ),并将文件移动到输出的目录中
    url-loader 一般与 file-loader 搭配使用,功能与 file-loader 类似,如果文件小于限制的大小。则会返回 base64 编码,否则使用 file-loader 将文件移动到输出的目录中

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    // webpack.config.js
    module.exports = {
    // 省略其它配置 ...
    module: {
    rules: [
    // ...
    {
    test: /\.(jpe?g|png|gif)$/i, //图片文件
    use: [
    {
    loader: 'url-loader',
    options: {
    limit: 10240,
    fallback: {
    loader: 'file-loader',
    options: {
    name: 'img/[name].[hash:8].[ext]'
    }
    }
    }
    }
    ]
    },
    {
    test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, //媒体文件
    use: [
    {
    loader: 'url-loader',
    options: {
    limit: 10240,
    fallback: {
    loader: 'file-loader',
    options: {
    name: 'media/[name].[hash:8].[ext]'
    }
    }
    }
    }
    ]
    },
    {
    test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i, // 字体
    use: [
    {
    loader: 'url-loader',
    options: {
    limit: 10240,
    fallback: {
    loader: 'file-loader',
    options: {
    name: 'fonts/[name].[hash:8].[ext]'
    }
    }
    }
    }
    ]
    },
    ]
    }
    }
    +

    用babel转义js文件

    为了使我们的 js 代码兼容更多的环境我们需要安装依赖

    +
    1
    2
    npm i babel-loader @babel/preset-env @babel/core

    +
    +

    注意
    babel-loaderbabel-core 的版本对应关系

    +
    +
      +
    • babel-loader 8.x 对应 babel-core 7.x
    • +
    • babel-loader 7.x 对应 babel-core 6.x
    • +
    +

    配置如下

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // webpack.config.js
    module.exports = {
    // 省略其它配置 ...
    module:{
    rules:[
    {
    test:/\.js$/,
    use:{
    loader:'babel-loader',
    options:{
    presets:['@babel/preset-env']
    }
    },
    exclude:/node_modules/
    },
    ]
    }
    }
    +

    上面的 babel-loader 只会将 ES6/7/8 语法转换为 ES5 语法,但是对新 api 并不会转换 例如( promiseGeneratorSetMapsProxy 等)
    此时我们需要借助 babel-polyfill 来帮助我们转换

    +
    1
    2
    3
    4
    5
    6
    npm i @babel/polyfill
    // webpack.config.js
    const path = require('path')
    module.exports = {
    entry: ["@babel/polyfill,path.resolve(__dirname,'../src/index.js')"], // 入口文件
    }
    +
    +

    手动把上面的 demo 敲一遍对阅读下面的文章更有益,建议入门的同学敲三遍以上

    +
    +

    上面的实践是我们对 webpack 的功能有了一个初步的了解,但是要想熟练应用于开发中,我们需要一个系统的实战。让我们一起摆脱脚手架尝试自己搭建一个 vue 开发环境

    +

    搭建vue开发环境

    上面的小例子已经帮助而我们实现了打包 css 、图片、 jshtml 等文件。
    但是我们还需要以下几种配置

    +

    解析.vue文件

    1
    2
    npm i -D vue-loader vue-template-compiler vue-style-loader
    npm i -S vue
    +
      +
    • vue-loader 用于解析 .vue 文件
    • +
    • vue-template-compiler 用于编译模板
    • +
    +

    配置如下

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    const vueLoaderPlugin = require('vue-loader/lib/plugin')
    module.exports = {
    module:{
    rules:[{
    test:/\.vue$/,
    use:['vue-loader']
    },]
    },
    resolve:{
    alias:{
    'vue$':'vue/dist/vue.runtime.esm.js',
    ' @':path.resolve(__dirname,'../src')
    },
    extensions:['*','.js','.json','.vue']
    },
    plugins:[
    new vueLoaderPlugin()
    ]
    }
    +

    配置webpack-dev-server进行热更新

    1
    npm i -D webpack-dev-server
    +

    配置如下

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const Webpack = require('webpack')
    module.exports = {
    // ...省略其他配置
    devServer:{
    port:3000,
    hot:true,
    contentBase:'../dist'
    },
    plugins:[
    new Webpack.HotModuleReplacementPlugin()
    ]
    }
    +

    完整配置如下

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    // webpack.config.js
    const path = require('path');
    const {CleanWebpackPlugin} = require('clean-webpack-plugin')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    const MiniCssExtractPlugin = require("mini-css-extract-plugin");
    const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')
    const vueLoaderPlugin = require('vue-loader/lib/plugin')
    const Webpack = require('webpack')
    module.exports = {
    mode:'development', // 开发模式
    entry: {
    main:path.resolve(__dirname,'../src/main.js'),
    },
    output: {
    filename: '[name].[hash:8].js', // 打包后的文件名称
    path: path.resolve(__dirname,'../dist') // 打包后的目录
    },
    module:{
    rules:[
    {
    test:/\.vue$/,
    use:['vue-loader']
    },
    {
    test:/\.js$/,
    use:{
    loader:'babel-loader',
    options:{
    presets:[
    ['@babel/preset-env']
    ]
    }
    }
    },
    {
    test:/\.css$/,
    use: ['vue-style-loader','css-loader',{
    loader:'postcss-loader',
    options:{
    plugins:[require('autoprefixer')]
    }
    }]
    },
    {
    test:/\.less$/,
    use: ['vue-style-loader','css-loader',{
    loader:'postcss-loader',
    options:{
    plugins:[require('autoprefixer')]
    }
    },'less-loader']
    }
    ]
    },
    resolve:{
    alias:{
    'vue$':'vue/dist/vue.runtime.esm.js',
    ' @':path.resolve(__dirname,'../src')
    },
    extensions:['*','.js','.json','.vue']
    },
    devServer:{
    port:3000,
    hot:true,
    contentBase:'../dist'
    },
    plugins:[
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
    template:path.resolve(__dirname,'../public/index.html'),
    filename:'index.html'
    }),
    new vueLoaderPlugin(),
    new Webpack.HotModuleReplacementPlugin()
    ]
    }
    +

    配置打包命令

    1
    2
    3
    4
    "script":{
    "dev":"webpack-dev-server --config build/webpack.config.js --open",
    "build":"webpack --config build/webpack.config.js"
    }
    +

    打包文件已经配置完毕,接下来让我们测试一下
    首先在 src 新建一个 main.js

    +
    1
    2
    3
    4
    5
    6
    // main.js
    import Vue from 'vue'
    import App from './app'
    new Vue({
    render:h=>h(App)
    }).$mount('#app')
    +

    新建一个 App.vue

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // App.vue
    <template>
    <div id='container'></div>
    </template>
    <script>
    export default {
    data(){
    return {
    initData:''
    }
    }
    }
    </script>
    <style scoped>
    #container{
    width:100%;
    height:100%;
    }
    </style>
    +

    新建一个 public 文件夹,里面新建一个 index.html

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // index.html
    <!DOCTYPE html>
    <html lang='en'>
    <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'>
    <title>lao li</title>
    </head>
    <body>
    <div id='app'></div>
    </body>
    </html>
    +

    执行 npm run dev 这时候如果浏览器出现 Vue 开发环境运行成功,那么恭喜你,已经成功迈出了第一步

    +

    区分开发环境与生产环境

    实际应用到项目中,我们需要区分开发环境与生产环境,我们在原来 webpack.config.js 的基础上再新增两个文件

    +

    webpack.dev.js 开发环境配置文件
    开发环境主要实现的是热更新,不要压缩代码,完整的 sourceMap
    webpack.prod.js生产环境配置文件
    生产环境主要实现的是压缩代码、提取 css 文件、合理的 sourceMap 、分割代码
    需要安装以下模块:

    +
    1
    npm i -D  webpack-merge copy-webpack-plugin optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin
    +
      +
    • webpack-merge 合并配置
    • +
    • copy-webpack-plugin 拷贝静态资源
    • +
    • optimize-css-assets-webpack-plugin 压缩 css
    • +
    • uglifyjs-webpack-plugin 压缩js
    • +
    +
    +

    webpack mode 设置 production 的时候会自动压缩 js 代码。
    原则上不需要引入 uglifyjs-webpack-plugin 进行重复工作。
    但是 optimize-css-assets-webpack-plugin 压缩 css 的同时会破坏原有的 js 压缩,所以这里我们引入 uglifyjs 进行压缩

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    // webpack.config.js
    const path = require('path')
    const {CleanWebpackPlugin} = require('clean-webpack-plugin')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    const vueLoaderPlugin = require('vue-loader/lib/plugin')
    const MiniCssExtractPlugin = require("mini-css-extract-plugin")
    const devMode = process.argv.indexOf('--mode=production') === -1;
    module.exports = {
    entry:{
    main:path.resolve(__dirname,'../src/main.js')
    },
    output:{
    path:path.resolve(__dirname,'../dist'),
    filename:'js/[name].[hash:8].js',
    chunkFilename:'js/[name].[hash:8].js'
    },
    module:{
    rules:[
    {
    test:/\.js$/,
    use:{
    loader:'babel-loader',
    options:{
    presets:['@babel/preset-env']
    }
    },
    exclude:/node_modules/
    },
    {
    test:/\.vue$/,
    use:['cache-loader','thread-loader',{
    loader:'vue-loader',
    options:{
    compilerOptions:{
    preserveWhitespace:false
    }
    }
    }]
    },
    {
    test:/\.css$/,
    use:[{
    loader: devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
    options:{
    publicPath:"../dist/css/",
    hmr:devMode
    }
    },'css-loader',{
    loader:'postcss-loader',
    options:{
    plugins:[require('autoprefixer')]
    }
    }]
    },
    {
    test:/\.less$/,
    use:[{
    loader:devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
    options:{
    publicPath:"../dist/css/",
    hmr:devMode
    }
    },'css-loader','less-loader',{
    loader:'postcss-loader',
    options:{
    plugins:[require('autoprefixer')]
    }
    }]
    },
    {
    test:/\.(jep?g|png|gif)$/,
    use:{
    loader:'url-loader',
    options:{
    limit:10240,
    fallback:{
    loader:'file-loader',
    options:{
    name:'img/[name].[hash:8].[ext]'
    }
    }
    }
    }
    },
    {
    test:/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
    use:{
    loader:'url-loader',
    options:{
    limit:10240,
    fallback:{
    loader:'file-loader',
    options:{
    name:'media/[name].[hash:8].[ext]'
    }
    }
    }
    }
    },
    {
    test:/\.(woff2?|eot|ttf|otf)(\?.*)?$/i,
    use:{
    loader:'url-loader',
    options:{
    limit:10240,
    fallback:{
    loader:'file-loader',
    options:{
    name:'media/[name].[hash:8].[ext]'
    }
    }
    }
    }
    }
    ]
    },
    resolve:{
    alias:{
    'vue$':'vue/dist/vue.runtime.esm.js',
    ' @':path.resolve(__dirname,'../src')
    },
    extensions:['*','.js','.json','.vue']
    },
    plugins:[
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
    template:path.resolve(__dirname,'../public/index.html')
    }),
    new vueLoaderPlugin(),
    new MiniCssExtractPlugin({
    filename: devMode ? '[name].css' : '[name].[hash].css',
    chunkFilename: devMode ? '[id].css' : '[id].[hash].css'
    })
    ]
    }
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // webpack.dev.js
    const Webpack = require('webpack')
    const webpackConfig = require('./webpack.config.js')
    const WebpackMerge = require('webpack-merge')

    module.exports = WebpackMerge(webpackConfig,{
    mode:'development',
    devtool:'cheap-module-eval-source-map',
    devServer:{
    port:3000,
    hot:true,
    contentBase:'../dist'
    },
    plugins:[
    new Webpack.HotModuleReplacementPlugin()
    ]
    })
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    // webpack.prod.js
    const path = require('path')
    const webpackConfig = require('./webpack.config.js')
    const WebpackMerge = require('webpack-merge')

    const CopyWebpackPlugin = require('copy-webpack-plugin')
    const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
    const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

    module.exports = WebpackMerge(webpackConfig,{
    mode:'production',
    devtool:'cheap-module-source-map',
    plugins:[
    new CopyWebpackPlugin([{
    from:path.resolve(__dirname,'../public'),
    to:path.resolve(__dirname,'../dist')
    }]),
    ],
    optimization:{
    minimizer:[
    new UglifyJsPlugin({//压缩js
    cache:true,
    parallel:true,
    sourceMap:true
    }),
    new OptimizeCssAssetsPlugin({})
    ],
    splitChunks:{
    chunks:'all',
    cacheGroups:{
    libs: {
    name: "chunk-libs",
    test: /[\\/]node_modules[\\/]/,
    priority: 10,
    chunks: "initial" // 只打包初始时依赖的第三方
    }
    }
    }
    }
    })
    + +

    优化webpack配置

    看到这里你或许有些累了,但是要想获取更好的offer,更高的薪水,下面必须继续深入

    +

    优化配置对我们来说非常有实际意义,这实际关系到你打包出来文件的大小,打包的速度等。
    具体优化可以分为以下几点:

    +

    优化打包速度

    +

    构建速度指的是我们每次修改代码后热更新的速度以及发布前打包文件的速度。

    +
    +

    合理的配置 mode 参数与 devtool 参数

    devtool 可设置的值
    mode 可设置 development production 两个参数

    +

    如果没有设置, webpack4 会将 mode 的默认值设置为 production

    +
      +
    • production :将 process.env.NODE_ENV 的值设置为 production ,启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPluginUglifyJsPlugin,会进行 tree shaking (去除无用代码)和 uglifyjs (代码压缩混淆)
    • +
    • development :将 process.env.NODE_ENV 的值设置为 development ,启用 NamedChunksPluginNamedModulesPlugin
    • +
    +

    缩小文件的搜索范围(配置include exclude alias noParse extensions)

      +
    • alias 当我们代码中出现 import ‘vue’时, webpack会采用向上递归搜索的方式去node_modules 目录下找。为了减少搜索范围我们可以直接告诉webpack去哪个路径下查找。也就是别名(alias)的配置。
    • +
    • include exclude 同样配置include exclude也可以减少webpack loader的搜索转换时间。
    • +
    • noParse 当我们代码中使用到 import jq from 'jquery' 时, webpack 会去解析 jq 这个库是否有依赖其他的包。但是我们对类似 jquery 这类依赖库,一般会认为不会引用其他的包(特殊除外,自行判断)。增加 noParse 属性,告诉 webpack 不必解析,以此增加打包速度。
    • +
    • extensions webpack 会根据 extensions 定义的后缀查找文件(频率较高的文件类型优先写在前面)
    • +
    +

    配图

    +

    使用HappyPack开启多进程Loader转换

    +

    webpack 构建过程中,实际上耗费时间大多数用在 loader 解析转换以及代码的压缩中。日常开发中我们需要使用 Loaderjscss ,图片,字体等文件做转换操作,并且转换的文件数据量也是非常大。由于js单线程的特性使得这些转换操作不能并发处理文件,而是需要一个个文件进行处理。 HappyPack 的基本原理是将这部分任务分解到多个子进程中去并行处理,子进程处理完成后把结果发送到主进程中,从而减少总的构建时间

    +
    +
    1
    npm i -D happypack
    + +

    happypack

    +

    使用 webpack-parallel-uglify-plugin 增强代码压缩

    上面对于 loader 转换已经做优化,那么下面还有另一个难点就是优化代码的压缩时间。

    +
    1
    npm i -D webpack-parallel-uglify-plugin
    +

    webpack-parallel-uglify-plugin

    +

    抽离第三方模块

    +

    对于开发项目中不经常会变更的静态依赖文件。类似于我们的 elementUivue 全家桶等等。因为很少会变更,所以我们不希望这些依赖要被集成到每一次的构建逻辑中去。 这样做的好处是每次更改我本地代码的文件的时候, webpack 只需要打包我项目本身的文件代码,而不会再去编译第三方库。以后只要我们不升级第三方包的时候,那么 webpack 就不会对这些库去打包,这样可以快速的提高打包的速度。

    +
    +

    这里我们使用 webpack 内置的 DllPlugin DllReferencePlugin 进行抽离

    +

    在与 webpack 配置文件同级目录下新建 webpack.dll.config.js
    代码如下

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // webpack.dll.config.js
    const path = require("path");
    const webpack = require("webpack");
    module.exports = {
    // 你想要打包的模块的数组
    entry: {
    vendor: ['vue','element-ui']
    },
    output: {
    path: path.resolve(__dirname, 'static/js'), // 打包后文件输出的位置
    filename: '[name].dll.js',
    library: '[name]_library'
    // 这里需要和webpack.DllPlugin中的`name: '[name]_library',`保持一致。
    },
    plugins: [
    new webpack.DllPlugin({
    path: path.resolve(__dirname, '[name]-manifest.json'),
    name: '[name]_library',
    context: __dirname
    })
    ]
    };
    +

    package.json 中配置如下命令

    +
    1
    "dll": "webpack --config build/webpack.dll.config.js"
    + +

    接下来在我们的 webpack.config.js 中增加以下代码

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    module.exports = {
    plugins: [
    new webpack.DllReferencePlugin({
    context: __dirname,
    manifest: require('./vendor-manifest.json')
    }),
    new CopyWebpackPlugin([ // 拷贝生成的文件到dist目录 这样每次不必手动去cv
    {from: 'static', to:'static'}
    ]),
    ]
    };
    +

    执行

    +
    1
    npm run dll
    + +

    会发现生成了我们需要的集合第三地方
    代码的 vendor.dll.js
    我们需要在html文件中手动引入这个js文件

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <!DOCTYPE html>
    <html lang="en">
    <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">
    <title>老yuan</title>
    <script src="static/js/vendor.dll.js"></script>
    </head>
    <body>
    <div id="app"></div>
    </body>
    </html>
    +

    这样如果我们没有更新第三方依赖包,就不必 npm run dll 。直接执行 npm run dev npm run build 的时候会发现我们的打包速度明显有所提升。因为我们已经通过 dllPlugin 将第三方依赖包抽离出来了。

    +

    配置缓存

    +

    我们每次执行构建都会把所有的文件都重复编译一遍,这样的重复工作是否可以被缓存下来呢,答案是可以的,目前大部分 loader 都提供了 cache 配置项。比如在 babel-loader 中,可以通过设置cacheDirectory 来开启缓存,babel-loader?cacheDirectory=true 就会将每次的编译结果写进硬盘文件(默认是在项目根目录下的 node_modules/.cache/babel-loader 目录内,当然你也可以自定义)
    但如果 loader 不支持缓存呢?我们也有方法,我们可以通过 cache-loader ,它所做的事情很简单,就是 babel-loader 开启 cache 后做的事情,将 loader 的编译结果写入硬盘缓存。再次构建会先比较一下,如果文件较之前的没有发生变化则会直接使用缓存。使用方法如官方 demo 所示,在一些性能开销较大的 loader 之前添加此 loader 即可

    +
    +
    1
    npm i -D cache-loader
    +

    cache-loader

    +

    优化打包文件体积

    打包的速度我们是进行了优化,但是打包后的文件体积却是十分大,造成了页面加载缓慢,浪费流量等,接下来让我们从文件体积上继续优化

    +

    引入webpack-bundle-analyzer分析打包后的文件

    webpack-bundle-analyzer 将打包后的内容束展示为方便交互的直观树状图,让我们知道我们所构建包中真正引入的内容

    +
    1
    npm i -D webpack-bundle-analyzer
    +

    webpack-bundle-analyzer

    +

    接下来在 package.json 里配置启动命令

    +
    1
    "analyz": "NODE_ENV=production npm_config_report=true npm run build"
    +

    windows 请安装 npm i -D cross-env

    +
    1
    "analyz": "cross-env NODE_ENV=production npm_config_report=true npm run build"
    +

    接下来 npm run analyz 浏览器会自动打开文件依赖图的网页

    +

    externals

    +

    按照官方文档的解释,如果我们想引用一个库,但是又不想让 webpack 打包,并且又不影响我们在程序中以 CMDAMD 或者 window/global 全局等方式进行使用,那就可以通过配置 Externals 。这个功能主要是用在创建一个库的时候用的,但是也可以在我们项目开发中充分使用Externals 的方式,我们将这些不需要打包的静态资源从构建逻辑中剔除出去,而使用 CDN的方式,去引用它们。
    有时我们希望我们通过 script 引入的库,如用 CDN 的方式引入的 jquery ,我们在使用时,依旧用 require 的方式来使用,但是却不希望 webpack 将它又编译进文件中。这里官网案例已经足够清晰明了,大家有兴趣可以点击了解
    webpack
    官网案例如下

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <script
    src="https://code.jquery.com/jquery-3.1.0.js"
    integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
    crossorigin="anonymous">
    </script>
    module.exports = {
    //...
    externals: {
    jquery: 'jQuery'
    }
    };
    import $ from 'jquery';
    $('.my-element').animate(/* ... */);
    +

    Tree-shaking

    +

    这里单独提一下 tree-shaking ,是因为这里有个坑。 tree-shaking 的主要作用是用来清除代码中无用的部分。目前在 webpack4 我们设置 modeproduction 的时候已经自动开启了tree-shaking 。但是要想使其生效,生成的代码必须是ES6模块。不能使用其它类型的模块如 CommonJS 之流。如果使用 Babel 的话,这里有一个小问题,因为 Babel 的预案( preset )默认会将任何模块类型都转译成 CommonJS 类型。修正这个问题也很简单,在 .babelrc 文件或在 webpack.config.js 文件中设置 modules:false 就好了

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // .babelrc
    {
    "presets": [
    ["@babel/preset-env",
    {
    "modules": false
    }
    ]
    ]
    }
    +

    或者

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // webpack.config.js

    module: {
    rules: [
    {
    test: /\.js$/,
    use: {
    loader: 'babel-loader',
    options: {
    presets: ['@babel/preset-env', { modules: false }]
    }
    },
    exclude: /(node_modules)/
    }
    ]
    }
    + +

    经历过上面两个系列的洗礼,到现在我们成为了一名合格的 webpack 配置工程师。但是光拧螺丝,自身的可替代性还是很高,下面我们将深入 webpack 的原理中去

    +

    手写webpack系列

    经历过上面两个部分,我们已经可以熟练的运用相关的 loaderplugin 对我们的代码进行转换、解析。接下来我们自己手动实现 loaderplugin ,使其在平时的开发中获得更多的乐趣。

    +

    手写 webpack loader

    +

    loader 从本质上来说其实就是一个 node 模块。相当于一台榨汁机( loader )将相关类型的文件代码( code )给它。根据我们设置的规则,经过它的一系列加工后还给我们加工好的果汁( code )。

    +
    +

    loader 编写原则

      +
    • 单一原则: 每个 Loader 只做一件事;
    • +
    • 链式调用: Webpack 会按顺序链式调用每个 Loader
    • +
    • 统一原则: 遵循 Webpack 制定的设计规则和结构,输入与输出均为字符串,各个 Loader 完全独立,即插即用;
    • +
    +

    在日常开发环境中,为了方便调试我们往往会加入许多 console 打印。但是我们不希望在生产环境中存在打印的值。那么这里我们自己实现一个 loader 去除代码中的 console

    +
    +

    知识点普及之 ASTAST 通俗的来说,假设我们有一个文件 a.js ,我们对 a.js 里面的1000行进行一些操作处理,比如为所有的 await 增加 try catch ,以及其他操作,但是 a.js 里面的代码本质上来说就是一堆字符串。那我们怎么办呢,那就是转换为带标记信息的对象(抽象语法树)我们方便进行增删改查。这个带标记的对象(抽象语法树)就是AST。这里推荐一篇不错的AST文章 AST快速入门

    +
    +
    1
    npm i -D @babel/parser @babel/traverse @babel/generator @babel/types
    +
      +
    • @babel/parser 将源代码解析成 AST
    • +
    • @babel/traverse 对 AST 节点进行递归遍历,生成一个便于操作、转换的 path 对象
    • +
    • @babel/generator 将 AST 解码生成 js 代码
    • +
    • @babel/types 通过该模块对具体的 AST 节点进行进行增、删、改、查
    • +
    +

    新建 drop-console.js

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const parser = require('@babel/parser')
    const traverse = require('@babel/traverse').default
    const generator = require('@babel/generator').default
    const t = require('@babel/types')
    module.exports=function(source){
    const ast = parser.parse(source,{ sourceType: 'module'})
    traverse(ast,{
    CallExpression(path){
    if(t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.object, {name: "console"})){
    path.remove()
    }
    }
    })
    const output = generator(ast, {}, source);
    return output.code
    }
    +

    如何使用

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const path = require('path')
    module.exports = {
    mode:'development',
    entry:path.resolve(__dirname,'index.js'),
    output:{
    filename:'[name].[contenthash].js',
    path:path.resolve(__dirname,'dist')
    },
    module:{
    rules:[{
    test:/\.js$/,
    use:path.resolve(__dirname,'drop-console.js')
    }
    ]
    }
    }
    +
    +

    实际上在 webpack4 中已经集成了去除 console 功能,在 minimizer 中可配置 去除console
    附上官网 如何编写一个loader

    +
    +

    手写webpack plugin

    +

    Webpack 运行的生命周期中会广播出许多事件, Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
    通俗来说:一盘美味的 盐豆炒鸡蛋 需要经历烧油 炒制 调味到最后的装盘等过程,而 plugin 相当于可以监控每个环节并进行操作,比如可以写一个少放胡椒粉 plugin ,监控 webpack 暴露出的生命周期事件(调味),在调味的时候执行少放胡椒粉操作。
    那么它与 loader 的区别是什么呢?上面我们也提到了 loader 的单一原则, loader 只能一件事,比如说 less-loader ,只能解析 less 文件, plugin 则是针对整个流程执行广泛的任务。

    +
    +

    一个基本的 plugin 插件结构如下

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class firstPlugin {
    constructor (options) {
    console.log('firstPlugin options', options)
    }
    apply (compiler) {
    compiler.plugin('done', compilation => {
    console.log('firstPlugin')
    ))
    }
    }

    module.exports = firstPlugin
    +
    +

    compilercompilation 是什么?

    +
    +
      +
    • compiler 对象包含了 Webpack 环境所有的的配置信息。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 optionsloaderplugin 。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。
    • +
    • compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation ,从而生成一组新的编译资源。 compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。
    • +
    +

    compilercompilation 的区别在于

    +
      +
    • compiler 代表了整个 webpack 从启动到关闭的生命周期,而 compilation 只是代表了一次新的编译过程
    • +
    • compilercompilation 暴露出许多钩子,我们可以根据实际需求的场景进行自定义处理
    • +
    +

    compiler钩子文档
    compilation钩子文档

    +

    下面我们手动开发一个简单的需求,在生成打包文件之前自动生成一个关于打包出文件的大小信息
    新建一个 webpack-firstPlugin.js

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    class firstPlugin{
    constructor(options){
    this.options = options
    }
    apply(compiler){
    compiler.plugin('emit',(compilation,callback)=>{
    let str = ''
    for (let filename in compilation.assets){
    str += `文件:${filename} 大小${compilation.assets[filename]['size']()}\n`
    }
    // 通过compilation.assets可以获取打包后静态资源信息,同样也可以写入资源
    compilation.assets['fileSize.md'] = {
    source:function(){
    return str
    },
    size:function(){
    return str.length
    }
    }
    callback()
    })
    }
    }
    module.exports = firstPlugin
    +

    如何使用

    +
    1
    2
    3
    4
    5
    6
    7
    8
    const path = require('path')
    const firstPlugin = require('webpack-firstPlugin.js')
    module.exports = {
    // 省略其他代码
    plugins:[
    new firstPlugin()
    ]
    }
    +

    执行 npm run build 即可看到在 dist 文件夹中生成了一个包含打包文件信息的 fileSize.md

    +
    +

    上面两个 loaderplugin 案例只是一个引导,实际开发需求中的 loaderplugin 要考虑的方面很多,建议大家自己多动手尝试一下。

    +
    +

    附上官网 如何编写一个plugin

    +

    webpack5.0的时代

    无论是前端框架还是构建工具的更新速度远远超乎了我们的想象,前几年的 jquery 一把梭的时代一去不复返。我们要拥抱的是不断更新迭代的 vuereactnodeserverlessdockerk8s ….
    不甘落后的 webpack 也已经在近日发布了 webpack 5.0.0 beta 10 版本。在之前作者也曾提过 webpack5.0 旨在减少配置的复杂度,使其更容易上手( webpack4 的时候也说了这句话),以及一些性能上的提升

    +
      +
    • 使用持久化缓存提高构建性能;
    • +
    • 使用更好的算法和默认值改进长期缓存(long-term caching);
    • +
    • 清理内部结构而不引入任何破坏性的变化;
    • +
    • 引入一些breaking changes,以便尽可能长的使用v5版本。
    • +
    +

    目前来看,维护者的更新很频繁,相信用不了多久webpack5.0将会拥抱大众。感兴趣的同学可以先安装beta版本尝尝鲜。不过在此之前建议大家先对webpack4进行一番掌握,这样后面的路才会越来越好走。

    +

    更多阅读

    webpack中文
    webpackjs
    4W字长文带你深度解锁Webpack系列(上)

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/51/index.html b/page/51/index.html new file mode 100644 index 0000000000..b95b97e168 --- /dev/null +++ b/page/51/index.html @@ -0,0 +1,531 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    原版

    先来看一个call实例,看看call到底做了什么:

    +
    1
    2
    3
    4
    5
    6
    7
    let foo = {
    value: 1
    };
    function bar() {
    console.log(this.value);
    }
    bar.call(foo); // 1
    +

    从代码的执行结果,我们可以看到,call首先改变了this的指向,使函数的this指向了foo,然后使bar函数执行了。
    总结一下:

    +
      +
    • call改变函数this指向
    • +
    • 调用函数
    • +
    +

    自己动手

      +
    1. 首先我们对参数 context 做了兼容处理,不传值, context 默认值为 window
    2. +
    3. 然后我们将函数挂载到 context 上面, context.fn = this
    4. +
    5. 处理参数,将传入 myCall 的参数截取,去除第一位,然后转为数组;
    6. +
    7. 调用 context.fn ,此时 fnthis 指向 context
    8. +
    9. 删除对象上的属性 delete context.fn
    10. +
    11. 将结果返回。
    12. +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    Function.prototype.myCall = function(context) {
    context = context || window;
    context.fn = this; // 将函数挂载到对象的fn属性上
    const args = [...arguments].slice(1); // 处理传入的参数
    const result = context.fn(...args); // 通过对象的属性调用该方法
    delete context.fn; // 删除该属性
    return result // 返回结果
    };
    +

    applycall 的区别在于参数, 其他没有差别,实现如下

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // myApply的参数形式为(obj,[arg1,arg2,arg3]);
    // 所以myApply的第二个参数为[arg1,arg2,arg3]
    // 这里我们用扩展运算符来处理一下参数的传入方式
    Function.prototype.myApply = function(context) {
    context = context || window
    context.fn = this
    let result
    if (arguments[1]) { // 判断是否有第二个参数
    result = context.fn(…arguments[1]) // 有的话传入执行
    } else {
    result = context.fn() // 没有的话空参执行
    }
    delete context.fn;
    return result
    };
    +

    bindcallapply 作用都是改变 this 的指向,区别在于 bind 改变后不会立即执行,而 callapply 会立即执行,我们看一下 bind 的用法

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function Person(){
    this.name="zs";
    this.age=18;
    this.gender="男"
    }
    let obj={
    hobby:"看书"
    }

    let changePerson = Person.bind(obj); // 将构造函数的this绑定为obj
    changePerson(); // 直接调用构造函数,函数会操作obj对象,给其添加三个属性;
    console.log(obj); // => {hobby:"看书",name:"zs",age:18,gender:'男'}
    let p = new changePerson(); // 用改变了this 指向的构造函数,new一个实例出来
    console.log(p); // => Person {name:"zs",age:18,gender:'男'}
    +

    仔细观察上面的代码,再看输出结果。

    +

    我们对 Person 类使用了 bind 将其 this 指向 obj ,得到了 changePerson 函数,此处如果我们直接调用 changeperson 会改变 obj ,若用 new 调用 changeperson 会得到实例 p,并且其 __proto__ 指向 Person ,我们发现 bind 失效了。

    +

    我们得到结论:bind 改变了 this 指向的函数,如果用 new 操作符来调用, bind 将会失效

    +

    这个对象就是这个构造函数的实例,那么只要在函数内部执行 * this instanceof 构造函数 * 来判断其结果是否为 true ,就能判断函数是否是通过 new 操作符来调用了,若结果为 true 则是用 new 操作符调用的,总结如下:

    +
      +
    1. 保存当前 this 指向
    2. +
    3. 保存环境上下文
    4. +
    5. 保存参数,去掉第一个对象参数
    6. +
    7. 返回待执行函数
        +
      1. 数组化剩余参数
      2. +
      3. 判断是否为构造函数
      4. +
      5. 若是执行构造函数,若不是改变 this 指向执行
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        // bind实现

        Function.prototype.myBind = function(context){
        let _this = this; // 1、保存函数
        context = context || window; // 2、保存目标对象
        let rest = [...arguments].slice(1); // 3、保存目标对象之外的参数,将其转化为数组;
        // 此处开始与 call 和 apply 不同,不是返回结果,而是返回一个函数
        return function F(){ // 4、返回一个待执行的函数
        let rest2 = Array.prototype.slice.call(arguments) // 5、这里的arguments是F函数的参数,转换为数组;
        if(this instanceof F){
        return new _this(...rest2) // 6、若是用new操作符调用,则直接用new 调用原函数,并用扩展运算符传递参数
        }else{
        _this.apply(context,rest.concat(rest2)); // 7、用apply调用第一步保存的函数,并绑定this,传递合并的参数数组,
        // 即context._this(rest.concat(rest2))
        }
        }
        };
      6. +
      +
    8. +
    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/52/index.html b/page/52/index.html new file mode 100644 index 0000000000..47f0b497bd --- /dev/null +++ b/page/52/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    原理

    就是利用 generator (生成器)分割代码片段。然后我们使用一个函数让其自迭代,每一个 yieldpromise 包裹起来。执行下一步的时机由 promise 来控制

    +

    实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function _asyncToGenerator(fn) {
    return function() {
    var self = this, args = arguments;
    return new Promise(function(resolve, reject) { // 将返回值 promise 化
    var gen = fn.apply(self, args); // 获取迭代器实例
    function _next(value) { // 执行下一步
    asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
    }
    function _throw(err) { // 抛出异常
    asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
    }
    _next(undefined); // 第一次触发
    });
    };
    }
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/53/index.html b/page/53/index.html new file mode 100644 index 0000000000..1406ecbba5 --- /dev/null +++ b/page/53/index.html @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    实现一个 EventEmitter

      +
    1. 创建一个 Event 类,包含构造函数、绑定、解绑、触发方法
    2. +
    3. on 监听event事件,事件触发时调用fn函数。根据字典创建事件数组,如果事件处理不存在,那么就推入数组,反之返回原数组
    4. +
    5. off 从字典中获取当前事件数组,如果获取值是数组,那么判断解绑哪个事件,如果没填清空全部(数组长度置为零),否则从事件数组删除选择的事件
    6. +
    7. emit 触发event事件,并把参数arg1,arg2,arg3….传给事件处理函数,跟解绑外层判断一样,内层把删除改为执行即可
    8. +
    9. once 为指定事件注册一个单次监听器,单次监听器最多只触发一次,触发后立即解除监听器。
    10. +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    class Event {
    constructor () {
    // 储存事件的数据结构
    // 为查找迅速, 使用对象(字典)
    this._cache = {}
    }

    // 绑定
    on(event, callback) {
    // 为了按类查找方便和节省空间
    // 将同一类型事件放到一个数组中
    // 这里的数组是队列, 遵循先进先出
    // 即新绑定的事件先触发
    let fns = (this._cache[event] = this._cache[event] || [])
    if(fns.indexOf(callback) === -1) {
    fns.push(callback)
    }
    return this
    }

    // 解绑
    off (event, callback) {
    let fns = this._cache[event]
    if(Array.isArray(fns)) {
    if(callback) {
    let index = fns.indexOf(callback)
    if(index !== -1) {
    fns.splice(index, 1)
    }
    } else {
    // 全部清空
    fns.length = 0
    }
    }
    return this
    }
    // 触发emit
    emit(event, ...args) {
    let fns = this._cache[event]
    if(Array.isArray(fns)) {
    fns.forEach((fn) => {
    fn(...args)
    })
    }
    return this
    }

    // 一次性绑定
    once(event, callback) {
    let onceCallback = () => { // 定义一个只执行一次就解绑的方法
    callback.call(this); // 使用call改变this指向
    this.off(event, onceCallback); // 解绑
    };
    this.on(event, onceCallback); // 绑定
    return this;
    }
    }
    +

    好的接下来我们调用一下

    +
    1
    2
    3
    4
    5
    6
    7
    8

    let e = new Event()

    e.on('click',function(){
    console.log('on')
    })
    // e.trigger('click', '666')
    console.log(e)
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/54/index.html b/page/54/index.html new file mode 100644 index 0000000000..78c91921fe --- /dev/null +++ b/page/54/index.html @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    Vue 2.x 的 Object.defineProperty 版本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 数据
    const data = {
    text: 'default'
    };
    const input = document.getElementById('input');
    const span = document.getElementById('span');

    // 数据劫持
    Object.defineProperty(data, 'text', {
    // 数据变化 —> 修改视图
    set(newVal) {
    input.value = newVal;
    span.innerHTML = newVal;
    }
    });

    // 视图更改 --> 数据变化
    input.addEventListener('keyup', function(e) {
    data.text = e.target.value;
    });
    + +

    Vue 3.x 的 proxy 版本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 数据
    const data = {
    text: 'default'
    };
    const input = document.getElementById('input');
    const span = document.getElementById('span');

    // 数据劫持
    const handler = {
    set(target, key, value) {
    target[key] = value;
    // 数据变化 —> 修改视图
    input.value = value;
    span.innerHTML = value;
    return value;
    }
    };
    const proxy = new Proxy(data, handler);

    // 视图更改 --> 数据变化
    input.addEventListener('keyup', function(e) {
    proxy.text = e.target.value;
    });
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/55/index.html b/page/55/index.html new file mode 100644 index 0000000000..c211a6009d --- /dev/null +++ b/page/55/index.html @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    原理

    先看看 reducemap 的使用方法

    +
    1
    2
    let new_array = arr.map(function callback(currentValue[, index[,array) {/* Return element for new_array */ }[, thisArg])
    let result = arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
    + +

    实现

    第一种用 for 实现

    +
    1
    2
    3
    4
    5
    6
    7
    Array.prototype.myMap = function(callback, thisArg) {
    let arr = [];
    for (let i = 0; i < this.length; i++) {
    arr.push(callback.call(thisArg, this[i], i, this));
    }
    return arr;
    };
    +

    第二种用 reduce 实现

    +
    1
    2
    3
    4
    5
    6
    7
    Array.prototype.myMap = function(callback, thisArg) {
    let result = this.reduce((accumulator, currentValue, index, array) => {
    accumulator.push(callback.call(thisArg, currentValue, index, array));
    return accumulator;
    }, []);
    return result;
    };
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/56/index.html b/page/56/index.html new file mode 100644 index 0000000000..28548a00e6 --- /dev/null +++ b/page/56/index.html @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    实现一个 Object.create() 方法

      +
    1. 创建一个空匿名函数
    2. +
    3. 函数原型对象指向传入对象实例
    4. +
    5. 返回构造函数创建的实例
    6. +
    +
    1
    2
    3
    4
    5
    function create =  function (o) {
    var F = function () {};
    F.prototype = o;
    return new F();
    };
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/57/index.html b/page/57/index.html new file mode 100644 index 0000000000..60ad9ffa15 --- /dev/null +++ b/page/57/index.html @@ -0,0 +1,496 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <ul id="color-list">
    <li>red</li>
    <li>yellow</li>
    <li>blue</li>
    <li>green</li>
    <li>black</li>
    <li>white</li>
    </ul>

    <script>
    (function () {
    var color_list = document.getElementById('color-list');
    color_list.addEventListener('click', showColor, true);
    function showColor(e) {
    var x = e.target;
    if (x.nodeName.toLowerCase() === 'li') {
    alert(x.innerHTML);
    }
    }
    })();
    </script>
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/58/index.html b/page/58/index.html new file mode 100644 index 0000000000..b6b5d51973 --- /dev/null +++ b/page/58/index.html @@ -0,0 +1,500 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    原理

    将多层数组扁平化

    +

    实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Array.prototype.myFlat = function() {
    var arr = [];
    this.forEach((item)=>{
    if(Array.isArray(item)){
    arr = arr.concat(item.myFlat()); // 如果是数组的话继续循环
    }else{
    arr.push(item)
    }
    })
    return arr
    };
    + +

    还有另外一种实现方式,非常好用

    +
    1
    2
    3
    4
    5
    Array.prototype.myFlat = function() {
    return this.toString() // => "1,2,3,4"
    .split(",") // => ["1", "2", "3", "4"]
    .map(item => +item); // => [1, 2, 3, 4]
    };
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/59/index.html b/page/59/index.html new file mode 100644 index 0000000000..9d741cdcd3 --- /dev/null +++ b/page/59/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    实现一个Array.isArray

    思路很简单,就是利用 Object.prototype.toString

    +
    1
    2
    3
    Array.myIsArray = function(o) { 
    return Object.prototype.toString.call(Object(o)) === '[object Array]';
    };
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/6/index.html b/page/6/index.html new file mode 100644 index 0000000000..5c9fb59457 --- /dev/null +++ b/page/6/index.html @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    Docker 镜像安全最佳实践

    Docker 和 宿主机 的设置

      +
    1. 保证宿主机和 Docker 的版本是最新的
    2. +
    3. 不要暴露 Docker 的 守护进程(daemon) 的套接字
    4. +
    5. 使用 rootless 模式启动 Docker
    6. +
    7. 避免使用特权容器
    8. +
    9. 限制容器资源
    10. +
    11. 隔离容器网络
    12. +
    13. 提高容器的隔离度
    14. +
    15. 将文件系统和卷设置为只读
    16. +
    17. 完整的生命周期管理
    18. +
    19. 限制来自容器内的系统调用
    20. +
    +

    确保镜像安全

      +
    1. 扫描和验证容器镜像
    2. +
    3. 使用最小基础镜像
    4. +
    5. 不要向 Docker 镜像泄露敏感信息
    6. +
    7. 使用多阶段构建
    8. +
    9. 确保容器注册
    10. +
    11. 使用固定标签以获得不变性
    12. +
    +

    监控容器

      +
    1. 监控容器活动
    2. +
    3. 确保容器在运行时的安全
    4. +
    5. 将故障排除数据与容器分开保存
    6. +
    7. 为镜像使用元数据标签
    8. +
    +

    参考

    Top 20 Dockerfile best practices
    Dockerセキュリティベストプラクティス トップ20

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/60/index.html b/page/60/index.html new file mode 100644 index 0000000000..a4b7c9d690 --- /dev/null +++ b/page/60/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    实现一个new操作符

    我们首先知道new做了什么:

    +
      +
    1. 创建一个空的简单 JavaScript 对象(即{})
    2. +
    3. 链接该对象(即设置该对象的构造函数)到另一个对象
    4. +
    5. 将步骤(1)新创建的对象作为 this 的上下文
    6. +
    7. 如果该函数没有返回对象,则返回 this
    8. +
    +

    知道new做了什么,接下来我们就来实现它

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function create(){
    // 创建一个空的对象
    let obj = {};
    // 获得构造函数
    let Con = [].shift.call(arguments)
    // 将空对象指向构造函数的原型链
    Object.setPrototypeOf(obj, Con.prototype);
    // obj.__proto__ = Con.prototype // 链接到原型
    // obj绑定到构造函数上,便可以访问构造函数中的属性,即this.obj.Con(args)
    let result = Con.apply(obj, arguments);
    // 如果返回的result是一个对象则返回
    // new方法失效,否则返回obj
    return result instanceof Object ? result : this.obj;
    // return typeof result === 'object' ? result : obj// 确保 new 出来的是个对象
    }
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/61/index.html b/page/61/index.html new file mode 100644 index 0000000000..a633b82c33 --- /dev/null +++ b/page/61/index.html @@ -0,0 +1,500 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    原版

    1
    Array.prototype.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
    + +

    自己动手

    1
    2
    3
    4
    5
    6
    7
    8
    Array.prototype.myReduce = function(callback, initialValue) {
    let accumulator = initialValue ? initialValue : this[0]; // 第一次使用判断时候有initialValue参数,如果有用他,没有用this[0],这里this指的是传入的数组,this[0]就是数组第一项
    for (let i = initialValue ? 0 : 1; i < this.length; i++) { // 如果有初始值从0开始循环,不然从1开始
    let _this = this; // 保留当前this指向
    accumulator = callback(accumulator, this[i], i, _this); //
    }
    return accumulator; // 返回迭代器的终值
    };
    +

    试用一下

    +
    1
    2
    3
    4
    5
    6
    7
    let arr = [1, 2, 3, 4];
    let sum = arr.myReduce((acc, val) => {
    acc += val;
    return acc;
    }, 5);

    console.log(sum); // 15
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/62/index.html b/page/62/index.html new file mode 100644 index 0000000000..78457c562b --- /dev/null +++ b/page/62/index.html @@ -0,0 +1,500 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    path.join 与 path.resolve 的区别

      +
    1. 对于以/开始的路径片段,path.join只是简单的将该路径片段进行拼接,而path.resolve将以/开始的路径片段作为根目录,在此之前的路径将会被丢弃,就像是在terminal中使用cd命令一样。
    2. +
    +
    1
    2
    path.join('/a', '/b') // 'a/b'
    path.resolve('/a', '/b') // '/b'
    +
      +
    1. path.resolve总是返回一个以相对于当前的工作目录(working directory)的绝对路径。
    2. +
    +
    1
    2
    path.join('./a', './b') // 'a/b'
    path.resolve('./a', './b') // '/Users/username/Projects/webpack-demo/a/b'
    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/63/index.html b/page/63/index.html new file mode 100644 index 0000000000..edc9bfc3da --- /dev/null +++ b/page/63/index.html @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    使用 Alibaba 的 Homebrew 镜像源进行加速

    平时我们执行 brew 命令安装软件的时候,跟以下 3 个仓库地址有关:

    +
      +
    • brew.git
    • +
    • homebrew-core.git
    • +
    • homebrew-bottles
      通过以下操作将这 3 个仓库地址全部替换为 Alibaba 提供的地址
    • +
    +

    1. 替换 / 还原 brew.git 仓库地址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 替换成阿里巴巴的 brew.git 仓库地址:
    cd "$(brew --repo)"
    git remote set-url origin https://mirrors.aliyun.com/homebrew/brew.git

    #=======================================================

    # 还原为官方提供的 brew.git 仓库地址
    cd "$(brew --repo)"
    git remote set-url origin https://github.com/Homebrew/brew.git
    + +

    2. 替换 / 还原 homebrew-core.git 仓库地址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 替换成阿里巴巴的 homebrew-core.git 仓库地址:
    cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
    git remote set-url origin https://mirrors.aliyun.com/homebrew/homebrew-core.git

    #=======================================================

    # 还原为官方提供的 homebrew-core.git 仓库地址
    cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
    git remote set-url origin https://github.com/Homebrew/homebrew-core.git
    + +

    3. 替换 / 还原 homebrew-bottles 访问地址

    这个步骤跟你的 macOS 系统使用的 shell 版本有关系

    +

    所以,先来查看当前使用的 shell 版本

    +
    1
    2
    3
    4
    echo $SHELL

    # 如果你的输出结果是 /bin/zsh,参考下方的 zsh 终端操作方式
    # 如果你的输出结果是 /bin/bash,参考下方的 bash 终端操作方式
    +

    3.1 zsh 终端操作方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 替换成阿里巴巴的 homebrew-bottles 访问地址:
    echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles' >> ~/.zshrc
    source ~/.zshrc

    #=======================================================

    # 还原为官方提供的 homebrew-bottles 访问地址
    vi ~/.zshrc
    # 然后,删除 HOMEBREW_BOTTLE_DOMAIN 这一行配置
    source ~/.zshrc
    +

    3.2 bash 终端操作方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 替换 homebrew-bottles 访问 URL:
    echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles' >> ~/.bash_profile
    source ~/.bash_profile

    #=======================================================

    # 还原为官方提供的 homebrew-bottles 访问地址
    vi ~/.bash_profile
    # 然后,删除 HOMEBREW_BOTTLE_DOMAIN 这一行配置
    source ~/.bash_profile
    + +

    转载自:http://www.xiegangd.com/article/154055689187484

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/64/index.html b/page/64/index.html new file mode 100644 index 0000000000..d4d709f24d --- /dev/null +++ b/page/64/index.html @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    vue插件开发

    +

    Vue.use({install(Vues){}})

    +
    +

    Vue.use

    把给到的内容执行一下
    举例

    +
    1
    2
    3
    4
    function a(){
    console.log('a')
    }
    Vue.use(a) // a
    +

    有 install 就执行 install

    +
    1
    2
    3
    4
    5
    6
    7
    function a(){
    console.log('a')
    }
    a.install = function(){
    console.log('b')
    }
    Vue.use(a) // b
    +

    再进一步

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    function a(){
    console.log('a')
    }
    a.install = function(){
    // console.log('b')
    vue.mixin({ // 抽离公共逻辑 , 缺点:命名冲突,难以阅读
    data(){ // data数据少的时候可以不用vuex 用mixin
    return {
    c:'this is mixin'
    }
    },
    methods:{
    // 混入方法
    // 提示性弹窗 原:import 控制 显隐 现在:在根节点引入,通过mixin在方法控制显隐
    }
    // 混入生命周期
    create(){
    // 所有组件的create生命周期都执行 mixin先执行
    }
    })
    }
    Vue.use(a) // b
    + +

    vue.util.defineReactive()

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    vue.util.defineReactive()

    var test = {
    testa: 1
    }
    setTimeout(()=>{
    test.testa = 2
    },1000)
    vue.mixin({
    beforeCreate(){
    this.test = test
    }
    })

    + +

    vue.extend vue.util.extend

    +

    vue.util.extend ===> 简单做了个拷贝,拷贝到一起

    +
    1
    vue.util.extend(a,b)
    +

    vue.extend ===> 获取到某个对象的实例

    +
    1
    2
    let Constrator = vue.extend(obj)
    let vm = new Constrator()
    + +

    手写vue-router

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    // myVueRouter.js
    class HistoryRoute(){
    constructor(){
    this.current = null;
    }
    }

    class vueRouter{
    constructor(options){
    this.mode = options.mode || 'hash'
    this.history = new HistoryRoute
    this.routes = options.routes||[]
    this.routesMap = this.createMap(this.routes)
    this.init()
    }
    init(){
    if(this.mode == 'hash'){
    // 自动加上 #
    location.hash?"":location.hash="/"
    window.addEventListener('load',()=>{
    this.history.current = location.hash.slice(1)
    })
    window.addEventListener('hashchange',()=>{
    this.history.current = location.hash.slice(1)
    })
    }else{
    location.pathname?"":location.pathname="/"
    window.addEventListener('load',()=>{
    this.history.current = location.hash.pathname
    })
    window.addEventListener('popstate',()=>{
    this.history.current = location.hash.pathname
    })
    }
    }
    createMap(router){
    return router.reduce((memo,current)=>{
    memo[current.path] = current.component
    })
    }
    }

    vueRouter.install = function(Vue){
    Vue.mixin({
    beforeCreate(){ // 组件还未实例化好
    if(this.$options && this.$options.router){ // 有配置而且引入路由
    this._root = this
    this._router = this.$option.router

    Vue.util.defineReactive(this,'current',this._router.history)
    }else{
    this._root = this.$parent._root
    }
    // 增强健壮性
    Object.defineProperty(this,'$route',{
    get(){
    return this._root._router
    }
    })
    }
    })
    Vue.component('router-view',{
    render(h){
    // 如何根据当前的current,获取到对应的组件
    let current = this._self._root._router.history.current
    let routerMap = this._self._root._router.routeMap
    return h(routeMap[current])
    }
    })
    }
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/65/index.html b/page/65/index.html new file mode 100644 index 0000000000..059eb2f0a4 --- /dev/null +++ b/page/65/index.html @@ -0,0 +1,558 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    V8引擎如何回收垃圾

    为什么我们要关注内存

      +
    • 防止页面占用内存过大,引起客户端卡顿,甚至无响应
    • +
    • Node使用的也是v8,内存对于后端服务的性能至关重要。因为服务的持久性,后端更容易造成内存溢出

      v8引擎的内存回收机制

      v8的内存分配

      新生代内存空间
    • +
    • from
    • +
    • to
      老生代内存空间

      内存大小

    • +
    • 和操作系统有关 — 64位(1.4G)32位(0.7G)
    • +
    • 64位下 新生代(64MB) 老生代(1400MB)
    • +
    • 32位下 新生代(16MB) 老生代(700MB)
    • +
    +

    为什么不占多一点内存

    +
      +
    • js设计之初是为浏览器
        +
      • 前端特点 — 不持久化,执行一遍就回收了,所以1.4G够用了
      • +
      • js回收内存会暂停执行代码
      • +
      +
    • +
    +

    垃圾回收算法

    新生代简单的说就是复制

    +
      +
    • 新生代用来放新产生的变量,根据条件放到老生代,有用的放到to里(把有用的留一边,清空另一边,往复进行)
    • +
    • 算法复杂度(时间复杂度,空间复杂度)—- 牺牲空间换时间
    • +
    +

    老生代就是标记、删除、整理

    +
      +
    • 为什么要整理
        +
      • 数组是需要连续的空间
      • +
      +
    • +
    +

    新生代如何晋升到老生代

    +
      +
    • 变量是否经过回收,第二次回收放到老生代,第一次回收放到To
    • +
    • To空间使用了25%,放到老生代
    • +
    +

    V8是如何处理变量的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 浏览器查看内存
    window.performance
    // nodejs查看内存 --- nodejs是c++的,可以拓宽内存
    process.memoryUsage()

    // 拿内存的方法
    function getMem(){
    var mem = process.memoryUsage();
    var format = function(bytes){
    return (bytes/1024/1024).toFixed(2)+'MB';
    }
    console.log('heapTotal:'+format(mem.heapTotal)+'heapUsed:'+format(mem.heapUsed))
    }
    +

    变量处理

      +
    • 内存主要就是存储变量等数据的
    • +
    • 局部变量当程序执行结束,且没有引用的时候就会随着消失 — 可以被回收但不是说会马上回收
    • +
    • 全局对象会始终存活到程序运行结束
    • +
    +

    如何查看V8内存使用情况

    如何注意内存使用

    优化内存的技巧

      +
    • 尽量不要定义全局变量
    • +
    • 全局变量记得手动销毁掉
        +
      • 不推荐开发时写delete – 支持有问题,严格模式有bug
      • +
      • 赋值为 undefined/null undefined 是变量 null 是保留字
      • +
      +
    • +
    • 用匿名自执行函数变全局为局部
        +
      • (function(){})()
      • +
      +
    • +
    • 尽量避免使用闭包 —- 错误的观点,ie5时代的问题 —- 尽量避免使用闭包引用
    • +
    +

    闭包

    +
    1
    2
    3
    4
    5
    6
    7
    function a(){
    var size = 20*1024*1024;
    var arr1 = new Array(size)
    return arr1
    }
    a() // 这样就没问题
    var b = a() // 因为引用所以无法销毁
    +

    防止内存泄漏

      +
    • 滥用缓存
    • +
    • 大内存量操作
    • +
    +

    所有的优化都可以用缓存来解决,缓存通常都在全局,缓存可以直接拿来用;

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var 20*1024*1024;
    var a = []
    for(var i=0;i<13;i++){
    a.push(new Array(size))
    }

    // 加缓存锁
    for(var i=0;i<13;i++){
    if(a.length>4){
    a.shift();
    }
    a.push(new Array(size))
    }
    +
      +
    • 不要用v8来缓存
        +
      • 一定要用要的话加锁
      • +
      +
    • +
    +

    nodejs中读取大文件要用流的形式,不要用读文件到buffer
    fs.readFile()
    fs.createReadStream()

    +

    浏览器中,大文件上传记得切片
    file.slice(0,1000)
    file.slice(1000,2000)

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/66/index.html b/page/66/index.html new file mode 100644 index 0000000000..9a60689c47 --- /dev/null +++ b/page/66/index.html @@ -0,0 +1,606 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    最近一直在追网课,说实话从业9年了,一直觉得前端发展非常快,而且一直充满着危机感,每天都要学习进步才有安稳的感觉,今天听了 vue 响应式原理实现的公开课,感觉还不错,做了如下笔记,帮助自己记忆,也希望能帮助大家。

    +

    Vue2 原理

    什么是 defineProperty

    defineProperty 其实是定义对象属性用的

    +
    +

    defineProperty 其实并不是核心的为一个对象做数据双向绑定,而是去给对象做属性标签,只不过是属性里 get 和 set 实现了响应式。

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    属性名默认值
    valueundefined
    getundefined
    setundefined
    writablefalse
    enumerablefalse
    configurablefalse
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var ob = {
    a:1,
    b:2
    }
    // 参数 1、对象 2、属性 3、配置
    Object.defineProperty(ob,'a',{
    writable:false,
    enumerable:true,
    configurable:true,
    })
    console.log(Object.getOwnPropertyDescriptor(ob,'a')) // 1
    ob.a = 2
    console.log(ob.a) // 1
    +

    下面我们实现一下双向绑定

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Object.defineProperty(ob,'a',{
    get:function(){
    console.log('a is be get')
    return 999;
    },
    set:function(){
    console.log('a is be set')
    return 999;
    },
    })

    console.log(Object.getOwnPropertyDescriptor(ob,'a')) // 999

    +

    改造代码实现双向绑定(存取值)

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var _val = obj.a; // 暂存
    Object.defineProperty(ob,'a',{
    get:function(){
    console.log('a is be get')
    return _val;
    },
    set:function(newVal){
    _val = newVal // 新值替换旧值
    console.log('a is be set')
    return _val;
    },
    })
    +

    Vue 中从改变一个数据到发生改变的过程

      +
    1. 改变数据触发 Set
    2. +
    3. Set 部分触发 notify(更新)
        +
      1. Get 部分收集依赖
      2. +
      +
    4. +
    5. 更改对应的虚拟 Dom
    6. +
    7. 重新 Render
    8. +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    // MyVue.js

    // 简单版本 vue
    function MyVue(){
    this.$data = {
    a: {
    b:1
    },
    c:2
    }
    this.el = document.getElementById('app');
    this.virtualDom = '';
    this.observer(this.$data);
    this.render();
    }
    vue.property.observer = function(obj){
    var _val, self = this;
    // var dep = new Dep() -> 源码中依赖收集对象
    for(var key in obj){ // 属性有可能是对象,要递归绑定
    _val = obj[key];
    if(typeof _val === 'Object'){
    this.observer(_val)
    }else{
    Object.defineProperty(this.$data,key,{ // 这里是实际绑定过程
    get:function(){
    // 依赖收集
    // dep.depend(); -> vue 源码中收集依赖的方法
    return _val
    },
    set:function(newVal){
    _val = newVal
    // dep.notify(); -> vue 源码中
    self.render() // AST语法树
    }
    })
    }
    }
    }
    vue.property.render = function(){
    this.virtualDom = 'i am '+this.$data.b;
    this.el.innerHTML = this.virtualDom;
    }
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // index.html

    <!DOCTYPE html>
    <html>
    <head>
    <title>自己实现Vue2数据双向绑定</title>
    </head>
    <body>
    <div id="app"></div>
    <script type="text/javascript" src='myVue.js'></script>
    <script type="text/javascript">
    var mv = new MyVue();
    setTimeout(function(){
    console.log('changes');
    console.log(mv.$data);
    mv.$data.b = 222;
    })
    </script>
    </body>
    </html>
    +
    +

    依赖收集:

    +
      +
    1. 我们的data里面的数据并不是所有地方都用到
    2. +
    3. 如果我们直接更新整个视图,浪费资源
    4. +
    5. 先收集依赖改变的数据的组件,再更新依赖了数据的组件(Dep depend notify)
    6. +
    +
    +

    格外注意的地方—数组怎么监听

    definePropty 只能给对象进行 get set 绑定, 数组怎么办?

    +

    vue 中 使用了 装饰者模式

    +
    +

    装饰者模式 Decorator模式(别名Wrapper):动态将职责附加到对象上,若要扩展功能,装饰者提供了比继承更具弹性的代替方案。

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var arraypro = Array.property; // 创建一个数组的原型对象
    var arrob = Object.create(arraypro); // 避免影响原型链
    var arr = ['push','pop','shift'];
    arr.forEach(function(method,index){
    arrob[method]=function(){ // 装饰者模式
    var ret = arraypro[method].apply(this,arguments)
    dep.notify() // 扩展了功能
    }
    })

    +

    Vue3 实现双向绑定

    Proxy 是什么?

    +
    +

    Proxy 对象用于定义基本操作的自定义行为
    和 definePropty 类似,功能几乎一样,只是用法上不同

    +
      +
    1. 不会污染原对象
    2. +
    3. 直接给对象就可以了
    4. +
    5. 不需要借助外部变量 _val
    6. +
    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    var ob = {
    a:1,
    b:2
    }

    var newOb = new Proxy(ob,{
    get(target,key,receiver){ // target 对象,key 属性
    console.log(target,key,receiver)
    return target[key]
    },
    set(target,key,value,receiver){
    return Reflect.set(target.key,value);
    // 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
    // return target[key] = value
    }
    })
    +

    为什么改用 Proxy

      +
    1. defineProperty 只能监听某个属性,不能全对象监听
    2. +
    3. 可以省去for in循环提升代码执行效率
    4. +
    5. 可以监听数组,不需要再为数组做特异性操作
    6. +
    7. 不污染原对象
    8. +
    9. 更优雅
    10. +
    +

    我们用 Proxy 实现一下 observe 方法

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    vue.property.observe = function(){
    var self = this;
    this.$data = new Proxy(this.$data,{
    get(target,key, receiver){
    return target[key]
    },
    set(target,key,newVal){
    target[key] = newVal
    self.render()
    }
    })
    }
    + +

    还能用 Proxy 做什么

      +
    1. 校验类型
    2. +
    3. 真正的私有变量
    4. +
    +
    校验类型

    例子:

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    // 数据类型验证
    // 我们要创建一个对象,这个对象是个人,他有name和age两个属性
    // name必须是中文,age必须是数字,大于18岁

    // 这里用到了策略模式
    var valid = {
    name(value){
    var reg=/^[\u4E00-\u9FAS]=$/
    if(typeof value === 'string' && reg.test(value)){
    return true;
    }
    return false;
    },
    age(value){
    if(typeof value === 'number' && value > 18){
    return true;
    }
    return false;
    }
    }
    function Person(name,age){
    this.name = name
    this.age = age
    return new Proxy(this,{
    get(target,key){
    return target[key]
    },
    set(target,key,value){
    if(valid[key](value)){
    return Reflect.set(target,key,value)
    }else{
    throw new Error(key+'is not valid')
    }
    }
    })
    }
    new Person('name',19)
    +
    +

    策略模式
    定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。

    +
    +
    真正的私有变量

    vue-router 源码中,给 $router ,$route 用 defineProperty 定义 get 并返回本身,这样就不能修改属性了。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Object.defineProperty(this,'$router',{ // Router 的实例
    get(){
    return this._root._router;
    }
    })
    Object.defineProperty(this,'$route',{
    get(){
    return {
    // 当前路由所在的状态
    current: this._root._router.history.current;
    }
    }
    })
    + +

    虚拟Dom和diff算法

    虚拟Dom是虚拟的,他只在概念里面存在,在AST语法树,下面进行解释

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    // 模板
    <template>
    <div>
    <p>{{msg}}</p>
    <p>2</p>
    <p>3</p>
    </div>
    </template>

    // diff 描述法
    diff <div>
    props:{
    id:2
    }
    children:[
    diff <p>
    props:{
    id:xxx
    }
    children:[
    ...
    ]
    ]

    // 对象描述法
    var virtual = {
    dom:'div',
    props:{
    id:2
    },
    children:[
    ....
    ]
    }
    +

    每层结构都是一样的,那么是如何进行 diff 比对的呢?

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    /** 
    * diff 算法
    */
    patchVnode(oldVnode,vnode){ // 接收新旧节点
    const el = vnode.el = oldVnode.el; // 拿出真实dom
    let i,oldCh = oldVnode.children ,ch = vnode.children // 拿出新旧节点子元素children数组
    if(oldVnode === vnode) return; // 新旧节点一致,直接返回不进行后续操作
    // 分情况操作 --- 只有文字节点,删除了子元素,增加了子元素,子元素发生变动
    // 新旧节点都不为空,且不一样
    if(oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text){ // 文字发生更新
    api.setTextContent(el,vonde.text) // 文字发生了更新,更新文字,给Dom,更新文字
    } else { // 不是单纯文字节点的话
    updateEle(); // 更新元素
    if(oldCh&&ch&&oldCh!==ch){ // 都有子元素,子元素变动
    updateChildren() // 调用更新子元素方法
    } else if(ch){ // 增加子元素
    createEl(vnode) // 创建子元素
    } else if(oldCh){ // 删除子元素
    api.removeChildren(el) // 调用删除子元素方法
    }
    }
    }
    +

    源码要多看,以下必看 Vue/react/axios/vue-router/Redux/Vuex
    为什么要看源码??

    +
      +
    • 初级前端就会用vue或react — 从差不多水平的60%中挑出更好的人
    • +
    • 提高思想–》看优秀的代码–》写优秀的代码
    • +
    • 看源码能力,对高级前端是必备的。— 解决疑难杂症,看源码了解原理。
    • +
    +

    vue 性能优化

    因为是公开课,所以时间上没来的及说完,以后自己在听别的有关的内容时再补上这块。

    +

    最后

    只有不断学习才能进步,充分利用网络的便利性,找各种优质的教学资源,我相信,努力会有回报,加油!

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/67/index.html b/page/67/index.html new file mode 100644 index 0000000000..8a22604d68 --- /dev/null +++ b/page/67/index.html @@ -0,0 +1,564 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    这篇文章想写一写前端工程化相关的内容,原因一呢是是结合公司业务给自己定的业绩指标包含这些内容,另外一个原因是因为听了网易前端唐磊说过的一句话,前端leader干什么,很重要的工作就是前端工程化,高级以上不懂前端工程化很难。

    +

    今天听了公开课讲到了用node写一个自己的cli,说实话正是工作所需,课程讲的有点快,没有从头跟下来,自己上完了课又上网上找了些资料,终于把步骤弄明白了,下面就把如何自定义一个cli来帮助提升开发效率。同时也完成了一个业务指标,心里美滋滋。

    +

    准备

    如果你看到这篇文章,也想跟着我的步骤写一下这个自定义cli,那么如下的知识还是有一些为好。

    +
      +
    • 基础的nodejs相关知识
    • +
    +

    没错就只需要会一些node的基础知识就可以了,接下来正式开始

    +

    初始化

    首先,我们要给我们的命令行工具起个名字,比如我们熟悉的 vue 命令行就是 vue-cli ,因为我写完了要给公司用,所以我起的名字是 tfd-cli ,你们喜欢叫什么你们随意

    +

    首先创建一个名字为 tfd-cli 的文件目录,然后在目录下执行 node 工程的初始化命令

    +
    1
    npm init -y // 初始化项目 -y 默认全部yes的参数
    +

    命令执行完成后 tfd-cli 目录下会生成一个我们熟悉的 package.json 文件,我们打开 package.json 文件,增加一段代码,如下

    +
    1
    2
    3
    4
    // 追加的代码
    "bin": {
    "tfd": "index.js"
    }
    +

    追加完成后,package.json 文件中的内容是这样的

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    {
    "name": "tfd-cli",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "bin": {
    "tfd": "index.js"
    }
    }
    +

    也就是说当我们执行 tfd 命令时它就会找同级目录下的 index.js 文件执行其中代码,我们目前还没有 index.js,文件,那么我们手动创建一个 index.js 的文件,然后在里面写下如下代码

    +
    1
    2
    3
    4
    #!/usr/bin/env node
    //使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的

    console.log('hello world');
    +

    执行完这些还不够,因为我们是开发环境所以还有一步操作是要将 tfd 命令告知 npm,该如何处理,所以我们要执行如下命令

    +
    1
    npm link
    +

    这里如果执行不成功,请用管理员权限执行,执行完成后我们会得到一个 package-lock.json 的文件,内容如下

    +
    1
    2
    3
    4
    5
    {
    "name": "tfd-cli",
    "version": "1.0.0",
    "lockfileVersion": 1
    }
    +

    如此一来,我们就可以在任何目录下执行命令行 tfd 就会执行 index.js 文件了,这里我们会在控制面板中输出 hello world ,怎么样是不是小有成就感,我们接着往下来。

    +

    创建指令

    我们写个命令行工具肯定不是为了输出个 hello world 这么简单,而是希望通过用户输入内容后根据条件输出一些东西,那么让我们想想一个命令行工具应该具备哪些指令呢?

    +
      +
    • 查看命令行工具版本
    • +
    • 查看帮助文档
    • +
    • 初始化模板
    • +
    • 列出模板类型
    • +
    • 等等
    • +
    +

    那么用指令该如何描述呢

    +
    1
    2
    3
    4
    tfd -V|--version //查看工具版本号
    tfd -h|--help //查看使用帮助
    tfd init <template-name> <project-name> //基于指定模板进行项目初始化
    tfd list //列出所有可用模板
    +

    为了执行命令,这里我们要引入一个 node 包叫做 commander,因此我们要先执行一下 install 命令

    +
    1
    npm install commander
    +

    接着我们就可以在 index.js 里面写指令了。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    // index.js

    #!/usr/bin/env node
    //使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
    const cmd = require('commander');

    // tfd -V|--version
    cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0
    +

    到这一步我们在控制台敲一下 tfd -V 你会发现什么也没输出,这是因为到这一步我们还无法解析 tfd -V 操作,在这之前我们要知道一个命令

    +
    1
    process.argv // 使用process.argv获取命令行参数
    +

    当我们把这句话加到 console.log 中在 index.js 中输出时你会看到控制台打印出

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // index.js

    #!/usr/bin/env node
    //使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
    const cmd = require('commander');

    // tfd -V|--version
    cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0
    console.log(process.argv)

    // 控制台
    [ '/usr/local/bin/node', '/usr/local/bin/tfd', '-V' ]
    +

    接下来我们要让commander获取参数执行命令

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // index.js

    #!/usr/bin/env node
    //使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
    const cmd = require('commander');

    // tfd -V|--version
    cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0
    // console.log(process.argv)
    cmd.parse(process.argv);
    +

    这个时候我们再在控制台输入 tfd -V 时,我们就会发现,控制台输出了 0.1.0,这样我们就完成了查版本的指令,接下来我们完成其他的指令

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    // index.js

    #!/usr/bin/env node
    //使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
    const cmd = require('commander');

    // tfd -V|--version
    cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0

    // tfd init <template> <project>
    cmd
    .command('init <template> <project>') // 参数
    .description('初始化项目模板')
    .action((templateName, projectName) => {
    console.log(templateName, projectName);
    })

    // tfd list
    cmd
    .command('list')
    .description('查看所有可用模板')
    .action(() => {
    console.log(`
    a a模板
    b b模板
    c c模板
    `)
    })
    // console.log(process.argv)
    cmd.parse(process.argv);
    +

    这个时候我们在控制台上输入 tfd -h 的时候,控制台会输出如下代码

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Usage: tfd [options] [command]

    Options:
    -V, --version output the version number
    -h, --help output usage information

    Commands:
    init <template> <project> 初始化项目模板
    list 查看所有可用模板
    +

    这样我们就实现了自定义命令,我们执行一下 tfd init template1 project1,我们可以看到,控制台中输出了 template1 project1,也就是说command命令后尖括号中指向了action中的参数,我们就可以通过判断action中的参数做具体的操作了。

    +

    通常模板可以选择从本地拷贝一份,但更常用的是从线上拷贝一份,比如从github中,接下来我们就看看如何从github中拷贝一个模板作为项目的初始化工程

    +

    github上创建模板仓库

    首先我们要在github上创建两个仓库 tpl-1 tpl-2,这里为了从github中下载仓库我们需要一个node包支持,让我们请出download-git-repo,别忘了执行安装命令

    +
    1
    npm install download-git-repo
    +

    安装完依赖之后让我们再去index.js填点东西,首先引入下载依赖,然后是创建下载的 template 抽象对象

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    #!/usr/bin/env node
    //使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
    const cmd = require('commander');
    const download = require('download-git-repo');
    // 可用模板
    const templates = {
    'tpl-1': {
    url: 'https://github.com/lixuguang/tpl-1',
    downloadUrl: 'https://github.com:lixuguang/tpl-1#master',
    description: 'tfd-cli脚手架测试模板1'
    },
    'tpl-2': {
    url: 'https://github.com/lixuguang/tpl-2',
    downloadUrl: 'https://github.com:lixuguang/tpl-2#master',
    description: 'tfd-cli脚手架测试模板2'
    }
    }

    // tfd -V|--version
    cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0

    // tfd init <template> <project>
    cmd
    .command('init <template> <project>') // 参数
    .description('初始化项目模板')
    .action((templateName, projectName) => {
    // console.log(templateName, projectName);
    let {downloadUrl} = templates[templateName];
    // 第一个参数是github仓库地址,第二个参数是创建的项目目录名,第三个参数是clone
    download(downloadUrl, projectName, {clone: true}, err => {
    if(err){
    console.log('模板下载失败');
    }else{
    console.log('模板下载成功');
    }
    })
    })

    // tfd list
    cmd
    .command('list')
    .description('查看所有可用模板')
    .action(() => {
    // console.log(`
    // a a模板
    // b b模板
    // c c模板
    // `)
    // 通过获取templates里的key可以获取到模板名称
    const templateName = Object.keys(templates)
    console.log(templateName)
    })
    // console.log(process.argv)
    cmd.parse(process.argv);
    +

    这样当我们执行 tfd list 就可以看到有哪些模板了,然后执行tfd init tpl-1 newproject 就可以依据 tpl-1 模板创建出 newproject 工程,这个过程实际上就是从github仓库克隆一份tpl-1作为模板创建工程newproject

    +

    这里需要注意的是download地址跟github仓库地址有点出入,比如github仓库地址是https://github.com/xxx/xxx而下载地址是https://github.com:xxx/xxx

    +

    虽然这样执行完成后就完成了基本的cli的雏形,但是毕竟不灵活,我们在使用vue-cli时,它的创建过程是问答式和选择式的,另外每个过程都会有进度显示什么的,那么要怎么添加这些功能呢,我们接着往下做。

    +

    进阶增加功能

    使用inquirer进行命令行答询

    inquirer 是一个进行命令行答询的库,通过它我们就可以创建问答式的内容,首先还是安装依赖

    +
    1
    npm install inquirer
    +

    使用handlebars修改package.json

    我们都知道在使用vue-cli的初始化命令后,会在项目目录下生成一个package.json文件,它就像是这个项目的基因序列一样,影响着项目的整个结构。模板是固定的,那要修改其中的package.json符合自己项目的需要,就要用到handlebars这个库来改写package.json文件,老规矩先安装它

    +
    1
    npm install handlebars
    +

    使用ora在命令行中显示加载状态

    我们在装任何依赖时都会有进度条显示进度,如果没有进度条又没有任何响应,会让用户迷茫,为了友好,我们就要加进度条,这里我们需要引入ora这个库来完成进度显示,我们接着安装。

    +
    1
    npm install ora
    +

    使用chalk和log-symbols增加命令行输出样式

    为了让命令行有红红绿绿的效果以及符号效果,我们需要使用chalklog-symbols来丰富样式,少废话,接着装

    +
    1
    npm install chalk log-symbols
    + +

    集大成

    终于安装完一堆的依赖,别忘了在index.js中引入,让我们看看具体如何使用这些库吧。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    // index.js

    #!/usr/bin/env node
    //使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
    const cmd = require('commander');
    const download = require('download-git-repo');

    const iq = require('inquirer'); // 命令行答询
    const hb = require('handlebars'); // 修改package.json文件
    const ora = require('ora'); // 命令行中加载状态标识
    const chalk = require('chalk'); // 命令行输出字符颜色
    const ls = require('log-symbols'); // 命令行输出符号
    const fs = require('fs'); // node fs原生模块

    // 可用模板
    const templates = {
    'tpl-1': {
    url: 'https://github.com/lixuguang/tpl-1',
    downloadUrl: 'https://github.com:lixuguang/tpl-1#master',
    description: 'tfd-cli脚手架测试模板1'
    },
    'tpl-2': {
    url: 'https://github.com/lixuguang/tpl-2',
    downloadUrl: 'https://github.com:lixuguang/tpl-2#master',
    description: 'tfd-cli脚手架测试模板2'
    }
    }

    // tfd -V|--version
    cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0

    // tfd init <template> <project>
    cmd
    .command('init <template> <project>') // 参数
    .description('初始化项目模板')
    .action((templateName, projectName) => {
    // console.log(templateName, projectName);
    let {downloadUrl} = templates[templateName];
    //下载github项目,下载墙loading提示
    const loading = ora('模板下载中...').start();
    // 第一个参数是github仓库地址,第二个参数是创建的项目目录名,第三个参数是clone
    download(downloadUrl, projectName, {clone: true}, err => {
    if(err){
    // console.log('模板下载失败');
    loading.fail('模板下载失败');
    }else{
    // console.log('模板下载成功');
    spinner.succeed('模板下载成功');
    // 命令行答询
    iq.prompt([
    {
    type: 'input', // 类型 输入框
    name: 'name', // 字段 key
    message: '请输入项目名称', // 描述
    default: projectName // 默认值
    },
    {
    type: 'input',
    name: 'description',
    message: '请输入项目简介',
    default: ''
    },
    {
    type: 'input',
    name: 'author',
    message: '请输入作者名称',
    default: ''
    }
    ]).then(answers => { // answers 是一个对象,对象的 key 为上面答询的 name 的值,value 为 用户输入的值,如果未输入,就取默认值
    // 根据命令行答询结果修改 package.json 文件
    let packageContent = fs.readFileSync(`${projectName}/package.json`, 'utf8'); // 同步方式以 utf-8 字符集获得下载好的项目目录下的 package.json 文件
    let packageResult = hb.compile(packageContent)(answers); // 将用户输入项与原内容混合获得新内容
    fs.writeFileSync(`${projectName}/package.json`, packageResult); // 重新同步方式写入到 package.json 文件中
    // 用chalk和log-symbols改变命令行输出样式
    console.log(ls.success, chalk.green('模板项目文件准备成功!'));
    })
    }
    })
    })

    // tfd list
    cmd
    .command('list')
    .description('查看所有可用模板')
    .action(() => {
    // console.log(`
    // a a模板
    // b b模板
    // c c模板
    // `)
    // 通过获取templates里的key可以获取到模板名称
    const templateName = Object.keys(templates)
    console.log(templateName)
    })
    // console.log(process.argv)
    cmd.parse(process.argv);
    + +

    到这里你自己的命令行工具就创建完成了,当然还可以继续丰富,比如加上选择条件等,这个就更复杂了,不在本文中展开,后续会继续改进这个命令行工具,当然如果要将自己的命令行工具给其他人用当然要先发布出去,就像我们要用vue-cli,首先要在npm上下载下来,同样的我们要想让别人用,就得先上传到npm上。

    +

    发布到npm上

    首先你需要有一个npm的账号,没有赶快去注册一个,在控制台中输入npm login,它会让你依次输入 Username Password Email ,当你都按照要求输入完之后,成功的话你会获得如下信息Logged in as XXX on https://registry.npm.org/.,再接下来执行 npm publish 命令,你的自定义脚手架就会发布到npm上,供他人下载使用,怎么样,学会了么?

    +

    后记

    这篇文章会有后续持续进化跟进,多篇文章连续,只要我的cli工具还在进化,文章就会继续,欢迎跟进。

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/68/index.html b/page/68/index.html new file mode 100644 index 0000000000..5620d9bfac --- /dev/null +++ b/page/68/index.html @@ -0,0 +1,500 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    最近又有小伙伴问我,为啥我在写vue项目做代理转发时候报找不到代理服务的问题?
    我看了一下他发给我的配置截图,仅一眼我就发现了问题所在,因为这个坑我曾经也踩过,而且好多小伙伴也都踩过,在写转发服务器的时候没写协议头,也就是在 target 那没写 http:// 这个部分,添加上以后重启一下,这个问题就解决了,小伙伴说就这个问题他解决了一天也没解决了,而且我说完了以后他想起来以前好像也遇到过这个问题,所以说在平时工作中遇到问题要常做笔记才对,所以为了避免以后自己也犯这种错误,暂且留一篇文章记录一下这个问题。

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/69/index.html b/page/69/index.html new file mode 100644 index 0000000000..db7faf45f9 --- /dev/null +++ b/page/69/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    这两天复工,公司一个小伙伴在群里问了一个问题

    +
    +

    如何在打印表格的时候,让超过一页的表格分割线不被截断

    +
    +

    说着他有贴上来了一张图,一张因为跨页被截断了的表格,说实话打印的场景接触的不多,打印表格一般都是制式的样式所以也不会出现这问题,一时间也没有头绪,想着这个是不是没什么办法解决,隔天他发来一个消息,说是解决了,给表格里加上了 tr td 就好了,接着他又发来一张图,还真是,不过上下边距发生了一些变化,我问他是不是自己改了边距所以好了,他说没改,然后我问他那他是怎么解决的,他说因为实在找不到问题解决方案和产生原因,于是就把代码重构了一下,按照标准写法写了一下,结果就好了。。。怎么说呢,也是神奇,我也到网上去查了查相关的材料,并没有特别对的上的原因,凭感觉来说,应该是thead和tbody上有默认的css样式解决了上面的问题,暂且记下这个问题,避免以后遇到又忘了是怎么回事。

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/7/index.html b/page/7/index.html new file mode 100644 index 0000000000..a4dd5fceee --- /dev/null +++ b/page/7/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    Oracle Linux install Docker(https://docs.docker.com/engine/install/centos/)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    # 查看
    systemctl list-units

    sudo yum remove docker \
    docker-client \
    docker-client-latest \
    docker-common \
    docker-latest \
    docker-latest-logrotate \
    docker-logrotate \
    docker-engine

    sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

    sudo yum install -y yum-utils

    sudo yum -y install docker-ce-20.10.17 docker-ce-cli-20.10.17 containerd.io-1.6.6

    sudo vi /etc/yum.repos.d/docker-ce.repo

    [centos-extras]
    name=Centos extras - $basearch
    baseurl=http://mirror.centos.org/centos/7/extras/x86_64
    enabled=1
    gpgcheck=1
    gpgkey=http://centos.org/keys/RPM-GPG-KEY-CentOS-7

    sudo yum -y install fuse-overlayfs slirp4netns

    # sudo chmod 777 docker-ce.repo

    # yum list docker-ce --showduplicates | sort -r

    sudo systemctl start docker

    sudo docker run hello-world

    sudo yum install /path/to/package.rpm
    sudo systemctl start docker
    sudo docker run hello-world
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/70/index.html b/page/70/index.html new file mode 100644 index 0000000000..8e444c1920 --- /dev/null +++ b/page/70/index.html @@ -0,0 +1,513 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    什么是 RESTful

    +

    REST 指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是 RESTful。

    +
    +

    实际上就是 url 风格/ 路由风格 , 给出一种写路由的原则 — 面向资源
    tip:静态路由利于seo优化 api/news/1,动态路由不利于seo优化 api/news?id=1

    +

    原来的风格
    | 路由 | 功能 | 描述 |
    | —- | —- |——|
    | http://127.0.0.1/user/query/1 | 查询 | 根据ID查询用户信息 |
    | http://127.0.0.1/user/save | 保存 | 注册用户 |
    | http://127.0.0.1/user/update | 更新 | 修改用户 |
    | http://127.0.0.1/user/delete/{id} | 删除 | 删除用户 |

    +

    RESTful — 面向资源:对于同一个资源都在同一个 URL 进行,通过判断 HTTP 请求的类型来决定做不同的事
    /user 一个资源
    | 路由 | 请求类型 |
    | ———————– | ——– |
    | http://127.0.0.1/user/1 | GET |
    | http://127.0.0.1/user | POST |
    | http://127.0.0.1/user | PUT |
    | http://127.0.0.1/user | DELETE |

    +

    URL 描述资源 , HTTP 描述请求。(HTTP协议无语义无状态 —- 降低复杂度,业务无关 — 架构代码业务无关)

    +

    RESTful 采用的是顶层路由

    +
    +

    顶层路由设计:不需要有物理文件映射路由

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    // express
    // app.js
    const express = require('express')
    const app = express()
    app.get('/case.avi',(req, res)=>{
    res.send('hello world'); // 不需要对应物理文件
    })
    app.listen(3000)
    +

    原生接口

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    // index.js
    const http = require('http');
    const mysql = require('mysql'); // mysql
    const co = require('co-mysql') // 异步同步化
    const md5 = require('md5-node') // md5加密

    // 连接数据库
    let db = mysql.createPool({ // 连接池自己管理 不用关闭
    host:'localhost',
    user:'root',
    password:'root',
    database:'user'
    })
    let conn = co(db)

    const app = http.createServer(async (req,res)=>{
    if(req.method === 'POST'){
    if(req.url === '/user'){
    // res.end(JSON.stringify({'message':'对user发起post请求'}))
    req.on('data', async (data)=>{
    arr.push(data)
    })
    req.on('end',async ()=>{
    let buffer = Buffer.concat(arr);
    // json对象
    let {username,pasword} = JSON.parse(buffer.toString())
    // console.log(username,pasword)
    let sql = `selct user from admin where user = ${username}`
    let data = await conn.query(sql);
    // console.log(data)
    if(data.length >=1 ){
    res.end(JSON.stringify({
    'status':200,
    'message':'用户名已经注册'
    }))
    }else{
    // 写入数据库
    password = md5(password);
    let sql = `INSERT INTO admin (user,password) VALUES ('${username}','${password}')`
    await conn.query(sql);
    res.end(JSON.stringify({
    'status':200,
    'message':'注册成功'
    }))
    }
    })
    }
    }if(req.method === 'GET'){
    if(req.url === '/user'){
    // res.end(JSON.stringify({'message':'对user发起get请求'}))
    let sql = `SELECT id,user,password FROM admin`
    let data = await conn.query(sql);
    res.end(JSON.stringify(data))
    }
    }
    }).listen(3000)

    // .http 文件
    @url = http://localhost:3000
    @type = Content-Type: applications

    GET {{url}}/user HTTP/1.1

    POST {{url}}/user HTTP/1.1
    {{type}}

    {
    username:'admin',
    password:123456
    }
    +

    使用express实现(express — generater yard ,koa — async await)

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53

    const express = require('express')
    const app = express()
    const mysql = require('mysql'); // mysql
    const co = require('co-mysql') // 异步同步化
    const md5 = require('md5-node') // md5加密
    const bodyparse = require('body-parse')
    // 连接数据库
    let db = mysql.createPool({
    host:'localhost',
    user:'root',
    password:'root',
    database:'user'
    })
    let conn = co(db)

    app.use(bodyparse.urlencoded({
    extended:true // 返回对象是兼职对,false - string/array true - any
    }))
    app.use(bodyparse.json())

    app.post('/user',async (req.res)=>{
    let { username , password} = req.body
    // console.log(username,pasword)
    let sql = `selct user from admin where user = ${username}`
    let data = await conn.query(sql);
    // console.log(data)
    if(data.length >=1 ){
    res.send(JSON.stringify({
    'status':200,
    'message':'用户名已经注册'
    }))
    }else{
    // 写入数据库
    password = md5(password);
    let sql = `INSERT INTO admin (user,password) VALUES ('${username}','${password}')`
    await conn.query(sql);
    res.send(JSON.stringify({
    'status':200,
    'message':'注册成功'
    }))
    }
    })

    app.get('/user/:id',(req,res)=>{
    res.send(req.params.id)

    let sql = `SELECT id,user,password FROM admin WHERE id = ${req.params.id}`
    let data = await conn.query(sql);
    res.end(JSON.stringify(data))
    })

    app.listen(3000)
    + +

    使用koa实现

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53


    // config.js
    module.exports = {
    host:'localhost',
    user:'root',
    password:'root',
    database:'user'
    }

    // libs/database.js
    const config = require('../config')
    const mysql = require('mysql'); // mysql
    const co = require('co-mysql') // 异步同步化
    // 连接数据库
    let db = mysql.createPool({
    host:config.host,
    user:config.user,
    password:config.password,
    database:config.database
    })
    let conn = co(db)

    // router/user/index.js
    const Router = require('koa-router')
    const md5 = require('md5-node') // md5加密
    const router = new Router();

    router.get('/user',async ctx=>{
    ctx.body = '主页'
    })
    router.post('/user',async ctx=>{
    let {username,password} = ctx.request.body
    // console.log(username,password)
    ctx.body = {
    username,password
    }
    })
    module.exports = router.routes();

    // app.js
    const koa = require('koa')
    const Router = require('koa-router')
    const body = require('koa-bodyparse')
    const config = require('config')
    const app = new Koa()
    const router = new Router()
    app.context.db = require('./libs/database')
    app.context.config = config
    app.use(body())
    router.use('/api',require('./router/user'))
    app.use(router.routes())
    app.listen(3000)
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/71/index.html b/page/71/index.html new file mode 100644 index 0000000000..b0fc9c9338 --- /dev/null +++ b/page/71/index.html @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    什么是SSR

    传统浏览器的vue纯浏览器渲染

    浏览器请求服务器,服务器返回静态资源 ,客户端负责渲染js

    +

    ssr

    浏览器请求服务器,node服务端渲染js后,返回对应的页面返回给浏览器

    +

    SSR需要那些东西

    手写SSR

    特性:

    +
      +
    • 每一次访问必须新建一个vue实例
    • +
    • 只会触发组件的 beforeCreate和created钩子
    • +
    +

    核心库

    +
      +
    • vue
    • +
    • vue-server-renderer

      vue + next

      +

      作者:李旭光
      引用请标明出处

      +
      +
    • +
    +

    前言

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/72/index.html b/page/72/index.html new file mode 100644 index 0000000000..591c67b8ef --- /dev/null +++ b/page/72/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    网络安全是前端不可忽略的一个部分,可惜的是之前我都忽略了,最近发现网络安全已经变得日益重要,所以特别整理一篇文章说说网络安全相关的内容。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/73/index.html b/page/73/index.html new file mode 100644 index 0000000000..7dd971363d --- /dev/null +++ b/page/73/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    最近在学习Webpack相关的知识的时候对loader和plugin有点迷惑,两个特性都是用来做打包相关处理,那么他们有什么区别呢,为了弄清楚他们有什么区别,我开始了疯狂的查找资料,虽说每篇文章都说了一些自己的认识,但是并没有一个特别标准或者说容易理解的答案,我且先将它们记录下来,以便日后回顾之时可以有一些感触。那么接下来我将开始webpack的解迷之旅。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/74/index.html b/page/74/index.html new file mode 100644 index 0000000000..085608741c --- /dev/null +++ b/page/74/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    es6中,arguments被取消掉了,但是函数还是会有参数不确定的时候,那么我们该怎么办呢,接下来将介绍一下如何在es6中完成arguments一样的效果。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/75/index.html b/page/75/index.html new file mode 100644 index 0000000000..f223ddebd9 --- /dev/null +++ b/page/75/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    今年我下定决心一定要好好地写博客,完善博客的内容,所以最近把原来记在各种笔记中的文章内容都一一转移到了博客,内容一多想找一篇文章时就变得很麻烦,我得去归档中或者分类中一篇篇的找,所以我觉得是时候该给我的博客添加搜索的功能了,我看了博客的config文件,其中并没有搜索相关的配置,我又看了主题的配置文件,这下让我找到了,有个local_search的选项,于是我就开始了博客搜索的研究。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/76/index.html b/page/76/index.html new file mode 100644 index 0000000000..08c68a6d21 --- /dev/null +++ b/page/76/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    最近在整理自己的知识体系,一方面为了夯实基础,另一方面也为了查缺补漏、重新理解,设计模式之前看过大概三四次,从最开始一点都不懂,到后面看了几遍以后慢慢理解,今天再次整理一下,希望这一遍下来之后能更加透彻的理解什么是设计模式。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/77/index.html b/page/77/index.html new file mode 100644 index 0000000000..2db298178c --- /dev/null +++ b/page/77/index.html @@ -0,0 +1,580 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    继承的操作需要有一个父类,这里使用构造函数外加原型来创建一个:
    有下面两个类,下面实现 Child 继承 Father:

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // js代码

    function Father() {
    this.type = 'prople';
    }

    Father.prototype.eat = function() {
    console.log('吃东西啦');
    };

    function Child(name) {
    this.name = name;
    this.color = 'black';
    }
    + +

    原型继承(认贼作父)

    +

    关键点:子类原型等于父类的实例 Child.prototype = new Person()(将父类指向子类的原型)。

    +
    +
    1
    2
    // js代码
    Child.prototype = new Father();
    + +

    特点:
    实例可继承的属性有:

    +
      +
    • 实例的构造函数的属性
    • +
    • 父类构造函数的属性
    • +
    • 父类原型上的属性
      新实例不会继承父类实例的属性
    • +
    +

    缺点:

    +
      +
    • 新实例无法向父类构造函数传参
    • +
    • 继承单一
    • +
    • 所有新实例都会共享父类实例的属性。— 原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改
    • +
    +

    构造继承(借腹生子)

    +

    在子类构造函数中调用父类构造函数

    +
    +
    1
    2
    3
    4
    // js代码
    function Child(name) {
    Father.call(this);
    }
    + +

    关键点:用 call 或 apply 将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制))Person.call(this, ‘reng’)
    特点:

    +
      +
    • 只继承了父类构造函数的属性,没有继承父类原型的属性
    • +
    • 解决了原型链继承的注意事项(缺点)1,2,3
    • +
    • 可以继承多个构造函数的属性(call 可以多个)
    • +
    • 在子实例中可以向父实例传参
      缺点:
    • +
    • 只能继承父类构造函数的属性
    • +
    • 无法实现构造函数的复用。(每次用每次都要重新调用)
    • +
    • 每个新实例都有构造函数的副本,臃肿
      (不能继承父类原型,函数在构造函数中,每个子类实例不能共享函数,浪费内存。)
    • +
    +

    组合继承(原型继承+构造继承)

    +

    使用构造继承继承父类参数,使用原型继承继承父类函数

    +
    +
    1
    2
    3
    4
    5
    6
    7
    // js代码
    function Child(name) {
    // 构造继承
    Father.call(this);
    }

    Child.prototype = Father.prototype; // Child.prototype = new Person(); // 原型继承
    + +

    关键点:结合了两种模式的优点–向父类传参(call)和复用(prototype)
    特点:

    +
      +
    • 可以继承父类原型上的属性,可以传参,可复用
    • +
    • 每个新实例引入的构造函数属性是私有的
    • +
    +

    缺点:

    +
      +
    • 父类原型和子类原型是同一个对象,无法区分子类真正是由谁构造。
    • +
    • 调用了两次父类的构造函数(耗内存)
    • +
    • 子类的构造函数会代替原型上的那个父类构造函数(call 相当于拿到了父类构造函数的副本)
    • +
    +

    原型式继承(复制降级)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 先封装一个函数容器,用来承载继承的原型和输出对象
    function create(obj) {
    // 寄生
    function F() {}
    F.prototype = obj;
    return new F();
    }
    var father = new Father();
    var child = create(father);
    console.log(child instanceof Father); // true
    console.log(child.job); // frontend
    + +

    关键点:用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了可以随意增添属性的实例或对象。Object.create()就是这个原理。

    +

    特点:

    +
      +
    • 类似于复制一个对象,用函数来包装
    • +
    +

    注意事项:

    +
      +
    • 所有的实例都会继承原型上的属性
    • +
    • 无法实现复用。(新实例属性都是后面添加的)
      Object.create()方法规范了原型式继承。这个方法接收两个参数,一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。
    • +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 传一个参数的时候
    var child = Object.create(new Father());
    console.log(child.job); // frontend
    console.log(child instanceof Father); // true
    // 传两个参数的时候
    var child = Object.create(new Father(), {
    name: {
    value: 'come on'
    }
    });
    child.sayHello(); // Hello come on
    + +

    寄生组合继承

    它跟组合继承一样,都比较常用。
    寄生:在函数内返回对象然后调用
    组合

    +
      +
    • 函数的原型等于另一个实例
    • +
    • 在函数中用 apply 或 call 引入另一个构造函数,可传参
    • +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // 寄生
    function create(obj) {
    function F() {}
    F.prototype = obj;
    return new F();
    }
    // object是F实例的另一种表示方法
    var obj = create(Father.prototype);
    // obj实例(F实例)的原型继承了父类函数的原型
    // 上述更像是原型链继承,只不过只继承了原型属性

    // 组合
    function Child() {
    // 构造
    this.age = 100;
    Father.call(this); // 这个继承了父类构造函数的属性
    } // 解决了组合式两次调用构造函数属性的特点

    // 重点
    Child.prototype = obj; // 原型

    console.log(Child.prototype.constructor); // Father
    obj.constructor = Child; // 一定要修复实例
    console.log(Child.prototype.constructor); // Child
    var child = new Child();
    // Child实例就继承了构造函数属性,父类实例,object的函数属性
    console.log(child.job); // frontend
    console.log(child instanceof Father); // true
    + +

    重点:修复了组合继承的问题

    +

    在上面的问题中,你可能发现了这么一个注释obj.constructor = Sub; // 一定要修复实例。为什么要修正子类的构造函数的指向呢?

    +

    因为在不修正这个指向的时候,在获取构造函数返回的时候,在调用同名属性或方法取值上可能造成混乱。比如下面:

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    function Car() {}
    Car.prototype.orderOneLikeThis = function() {
    // Clone producing function
    return new this.constructor();
    };
    Car.prototype.advertise = function() {
    console.log('I am a generic car.');
    };

    function BMW() {}
    BMW.prototype = Object.create(Car.prototype);
    BMW.prototype.constructor = BMW; // Resetting the constructor property
    BMW.prototype.advertise = function() {
    console.log('I am BMW with lots of uber features.');
    };

    var x5 = new BMW();

    var myNewToy = x5.orderOneLikeThis();

    myNewToy.advertise(); // => "I am BMW ..." if `BMW.prototype.constructor = BMW;` is not
    // commented; "I am a generic car." otherwise.
    + +

    object.create 在组合继承的基础上,子类继承一个由父类原型生成的空对象。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码
    function Child(name) {
    Father.call(this);
    }

    Child.prototype = Object.create(Father.prototype, {
    constructor: {
    value: Child
    }
    });
    + +

    inherits 函数 — Nodejs util.inherits 函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // js代码

    function inherits = function(ctor, superCtor) {
    ctor.super_ = superCtor; // super_属性是子类继承父类时构造函数要写入的一个属性值.
    ctor.prototype = Object.create(superCtor.prototype, {
    constructor: {
    value: ctor,
    enumerable: false,
    writable: true,
    configurable: true
    }
    });
    };
    // 在上面的代码中 ctor 想要继承 superCtor ,我们姑且把 ctor 称作子类, superCtor 称作父类.

    // 使用
    function Child() {
    Father.call(this);
    //...
    }
    inherits(Child, Father);

    Child.prototype.fun = ...
    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/78/index.html b/page/78/index.html new file mode 100644 index 0000000000..ffd6addba7 --- /dev/null +++ b/page/78/index.html @@ -0,0 +1,536 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    前言

    面试中时常会出现需要手动实现某个功能的要求,所以我准备整理一个自己动手实现系列文章,这一次呢就是自己动手实现 promise

    +

    实现 promise 思路

    基础步骤

    +
      +
    1. 设定三个状态 PENDINGFULFILLEDREJECTED ,只能由 PENDING 改变为 FULFILLEDREJECTED ,并且只能改变一次
    2. +
    3. MyPromise 接收一个函数 executorexecutor 有两个参数 resolve 方法和 reject 方法
    4. +
    5. resolvePENDING 改变为 FULFILLED
    6. +
    7. rejectPENDING 改变为 FULFILLED
    8. +
    9. promise 变为 FULFILLED 状态后具有一个唯一的 value
    10. +
    11. promise 变为 REJECTED 状态后具有一个唯一的 reason
    12. +
    +

    ** then 方法**

    +
      +
    1. then 方法接受两个参数 onFulfilledonRejected ,它们分别在状态由 PENDING 改变为 FULFILLEDREJECTED 后调用
    2. +
    3. 一个 promise 可绑定多个 then 方法
    4. +
    5. then 方法可以同步调用也可以异步调用
    6. +
    7. 同步调用:状态已经改变,直接调用 onFulfilled 方法
    8. +
    9. 异步调用:状态还是 PENDING ,将 onFulfilledonRejected 分别加入两个函数数组 onFulfilledCallbacksonRejectedCallbacks ,当异步调用 resolvereject 时,将两个数组中绑定的事件循环执行。
    10. +
    +

    代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    // js代码

    // 定义状态常量
    const PENDING = 'pending';
    const FULFILLED = 'fulfilled';
    const REJECTED = 'rejected';

    /**
    * 定义MyPromise模拟Promise
    * @param {func} executor 接收函数
    */
    function MyPromise(executor) {
    this.state = PENDING; // 默认状态为 pending
    this.value = null;
    this.reason = null;

    // 定义成功失败的函数数组
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    // 定义成功回调
    const resolve = (value) => {
    if (this.state === PENDING) {
    this.state = FULFILLED;
    this.value = value;

    this.onFulfilledCallbacks.forEach(func => {
    func();
    });
    }
    }

    // 定义失败回调
    const reject = (reason) => {
    if (this.state === PENDING) {
    this.state = REJECTED;
    this.reason = reason;
    this.onRejectedCallbacks.forEach(func => {
    func();
    });
    }
    }

    try {
    executor(resolve, reject);
    } catch (reason) {
    reject(reason);
    }
    }

    MyPromise.prototype.then = function (onFulfilled, onRejected) {
    switch (this.state) {
    case FULFILLED:
    onFulfilled(this.value);
    break;
    case REJECTED:
    onFulfilled(this.value);
    break;
    case PENDING:
    this.onFulfilledCallbacks.push(() => {
    onFulfilled(this.value);
    })
    this.onRejectedCallbacks.push(() => {
    onRejected(this.reason);
    })
    break;
    }
    +

    then方法异步调用

    如下面的代码:输入顺序是:1、2、ConardLi

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // js代码

    console.log(1);

    let promise = new Promise((resolve, reject) => {
    resolve('ConardLi');
    });

    promise.then((value) => {
    console.log(value);
    });

    console.log(2);
    +

    虽然 resolve 是同步执行的,我们必须保证 then 是异步调用的,我们用 setTimeout 来模拟异步调用(并不能实现微任务和宏任务的执行机制,只是保证异步调用)

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38

    // js代码
    MyPromise.prototype.then = function (onFulfilled, onRejected) {
    if (typeof onFulfilled != 'function') {
    onFulfilled = function (value) {
    return value;
    }
    }
    if (typeof onRejected != 'function') {
    onRejected = function (reason) {
    throw reason;
    }
    }
    switch (this.state) {
    case FULFILLED:
    setTimeout(() => {
    onFulfilled(this.value);
    }, 0);
    break;
    case REJECTED:
    setTimeout(() => {
    onRejected(this.reason);
    }, 0);
    break;
    case PENDING:
    this.onFulfilledCallbacks.push(() => {
    setTimeout(() => {
    onFulfilled(this.value);
    }, 0);
    })
    this.onRejectedCallbacks.push(() => {
    setTimeout(() => {
    onRejected(this.reason);
    }, 0);
    })
    break;
    }
    }
    +

    then方法链式调用

    保证链式调用,即 then 方法中要返回一个新的 promise ,并将 then 方法的返回值进行 resolve

    +

    注意:这种实现并不能保证 then 方法中返回一个新的 promise ,只能保证链式调用

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    // js代码
    MyPromise.prototype.then = function (onFulfilled, onRejected) {
    if (typeof onFulfilled != 'function') {
    onFulfilled = function (value) {
    return value;
    }
    }
    if (typeof onRejected != 'function') {
    onRejected = function (reason) {
    throw reason;
    }
    }

    // 创建一个新的MyPromise对象
    const promise2 = new MyPromise((resolve, reject) => {
    switch (this.state) {
    case FULFILLED:
    setTimeout(() => {
    try {
    const x = onFulfilled(this.value);
    resolve(x);
    } catch (reason) {
    reject(reason);
    }
    }, 0);
    break;
    case REJECTED:
    setTimeout(() => {
    try {
    const x = onRejected(this.reason);
    resolve(x);
    } catch (reason) {
    reject(reason);
    }
    }, 0);
    break;
    case PENDING:
    this.onFulfilledCallbacks.push(() => {
    setTimeout(() => {
    try {
    const x = onFulfilled(this.value);
    resolve(x);
    } catch (reason) {
    reject(reason);
    }
    }, 0);
    })
    this.onRejectedCallbacks.push(() => {
    setTimeout(() => {
    try {
    const x = onRejected(this.reason);
    resolve(x);
    } catch (reason) {
    reject(reason);
    }
    }, 0);
    })
    break;
    }
    })
    return promise2;
    }
    +

    catch方法

    若上面没有定义 reject 方法,所有的异常会走向 catch 方法:

    +
    1
    2
    3
    4
    5
    // js代码

    MyPromise.prototype.catch = function(onRejected) {
    return this.then(null, onRejected);
    };
    +

    finally方法

    不管是 resolve 还是 reject 都会调用 finally

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // js代码

    MyPromise.prototype.finally = function(fn) {
    return this.then(value => {
    fn();
    return value;
    }, reason => {
    fn();
    throw reason;
    });
    };
    +

    Promise.resolve

    Promise.resolve 用来生成一个直接处于 FULFILLED 状态的 Promise

    +
    1
    2
    3
    4
    5
    6
    // js代码
    MyPromise.reject = function(value) {
    return new MyPromise((resolve, reject) => {
    resolve(value);
    });
    };
    +

    Promise.reject

    Promise.reject 用来生成一个直接处于 REJECTED 状态的 Promise

    +
    1
    2
    3
    4
    5
    6
    // js代码
    MyPromise.reject = function(reason) {
    return new MyPromise((resolve, reject) => {
    reject(reason);
    });
    };
    +

    all方法

    接受一个 promise 数组,当所有 promise 状态 resolve 后,执行 resolve

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // js代码
    MyPromise.all = function (promises) {
    return new Promise((resolve, reject) => {
    if (promises.length === 0) {
    resolve([]);
    } else {
    let result = [];
    let index = 0;
    for (let i = 0; i < promises.length; i++) {
    promises[i].then(data => {
    result[i] = data;
    if (++index === promises.length) {
    resolve(result);
    }
    }, err => {
    reject(err);
    return;
    });
    }
    }
    });
    }
    +

    race方法

    接受一个 promise 数组,当有一个 promise 状态 resolve 后,执行 resolve

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // js代码
    MyPromise.race = function (promises) {
    return new Promise((resolve, reject) => {
    if (promises.length === 0) {
    resolve();
    } else {
    let index = 0;
    for (let i = 0; i < promises.length; i++) {
    promises[i].then(data => {
    resolve(data);
    }, err => {
    reject(err);
    return;
    });
    }
    }
    });
    }
    + +

    最后

    如此一个自定义的 promise 就实现了,怎么样学回来吗?

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/79/index.html b/page/79/index.html new file mode 100644 index 0000000000..f792390ede --- /dev/null +++ b/page/79/index.html @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    前言

    最近各种技术博客,各种技术文章,各种牛人前辈的学习心得都在看,最近看了一个不错的文章叫做《如何正确使用时间》
    感觉还是很不错的,特别适合年轻的程序员看一下。下面将全文贴在下方,如果需要也可以去原文查看。

    +

    原文

    +

    你是否时常会焦虑时间过的很快,没时间学习,本文将会分享一些个人的见解。

    +

    花时间补基础,读文档

    在工作中我们时常会花很多时间去 debug,但是你是否发现很多问题最终只是你基础不扎实或者文档没有仔细看。

    +

    基础是你技术的基石,一定要花时间打好基础,而不是追各种新的技术。一旦你的基础扎实,学习各种新的技术也肯定不在话下,因为新的技术,究其根本都是相通的。

    +

    文档同样也是一门技术的基础。一个优秀的库,开发人员肯定已经把如何使用这个库都写在文档中了,仔细阅读文档一定会是少写 bug 的最省事路子。

    +

    学会搜索

    如果你还在使用百度搜索编程问题,请尽快抛弃这个垃圾搜索引擎。同样一个关键字,使用百度和谷歌,谷歌基本完胜的。即使你使用中文在谷歌中搜索,得到的结果也往往是谷歌占优,所以如果你想迅速的通过搜索引擎来解决问题,那一定是谷歌。

    +

    学点英语

    说到英语,一定是大家所最不想听的。其实我一直认为程序员学习英语是简单的,因为我们工作中是一直接触着英语,并且看懂技术文章,文档所需要的单词量是极少的。我时常在群里看到大家发出一个问题的截图问什么原因,其实在截图中英语已经很明白的说明了问题的所在,如果你的英语过关,完全不需要浪费时间来提问和搜索。所以我认为学点英语也是节省时间中很重要的一点。

    +

    那么如何去学习呢,chrome 装个翻译插件,直接拿英文文档或文章读,不会的就直接划词翻译,然后记录下这个单词并背诵。每天花半小时看点英文文档和文章,坚持两个月,你的英语水平不说别的,看文档和文章绝对不会有难题了。这一定是一个很划算的个人时间投资,花点时间学习英语,能为你将来的技术之路铺平很多坎。

    +

    画个图,想一想再做

    你是否遇到过这种问题,需求一下来,看一眼,然后马上就按照设计稿开始做了,可能中间出个问题导致你需要返工。

    +

    如果你存在这样的问题,我很推荐在看到设计稿和需求的时候花点时间想一想,画一画。考虑一下设计稿中是否可以找到可以拆分出来的复用组件,是否存在之前写过的组件。该如何组织这个界面,数据的流转是怎么样的。然后画一下这个页面的需求,最后再动手做。

    +

    利用好下班时间学习

    说到下班时间,那可能就有人说了公司很迟下班,这其实是国内很普遍的情况。但是我认为正常的加班是可以的,但是强制的加班就是在损耗你的身体和前途。

    +

    可以这么说,大部分的 996 公司,加班的这些时间并不会增加你的技术,无非就是在写一些重复的业务逻辑。也许你可以拿到更多的钱,但是代价是身体还有前途。程序员是靠技术吃饭的,如果你长久呆在一个长时间加班的公司,不能增长你的技术还要吞噬你的下班学习时间,那么你一定会废掉的。如果你遇到了这种情况,只能推荐尽快跳槽到非 996 的公司。

    +

    那么如果你有足够的下班时间,一定要花上 1, 2 小时去学习,上班大家基本都一样,技术的精进就是看下班以后的那几个小时了。如果你能利用好下班时间来学习,坚持下去,时间一定会给你很好的答复。

    +

    列好 ToDo

    我喜欢规划好一段时间内要做的事情,并且要把事情拆分为小点。给 ToDo 列好优先级,紧急的优先级最高。相同优先级的我喜欢先做简单的,因为这样一旦完成就能划掉一个,提高成就感。

    +

    反思和整理

    每周末都会花上点时间整理下本周记录的笔记和看到的不错文章。然后考虑下本周完成的工作和下周准备要完成的工作。

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/8/index.html b/page/8/index.html new file mode 100644 index 0000000000..7cbe6f2f5a --- /dev/null +++ b/page/8/index.html @@ -0,0 +1,778 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    服务器高危端口列表

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    协议端口服务渗透测试
    tcp20,21FTP(文件传输协议)允许匿名的上传下载,爆破,嗅探,win提权,远程执行(proftpd 1.3.5),各类后门(proftpd,vsftp 2.3.4)
    tcp22SSH (安全外壳协议 )可根据已搜集到的信息尝试爆破,v1版本可中间人,ssh隧道及内网代理转发,文件传输等等
    tcp23Telnet ( 远程终端协议)爆破,嗅探,一般常用于路由,交换登陆,可尝试弱口令
    tcp25SMTP(简单邮件传输协议)邮件伪造,vrfy/expn查询邮件用户信息,可使用smtp-user-enum工具来自动跑
    tcp/udp53DNS(域名系统)允许区域传送,dns劫持,缓存投毒,欺骗以及各种基于dns隧道的远控
    tcp/udp69TFTP (简单文件传送协议 )尝试下载目标及其的各类重要配置文件
    tcp80-89,443,8440-8450,8080-8089各种常用的Web服务端口,可尝试经典的topn,vpn,owa,webmail,目标oa,各类Java控制台,各类服务器Web管理面板,各类Web中间件漏洞利用,各类Web框架漏洞利用等等……
    tcp110POP3(邮局协议版本3 )可尝试爆破,嗅探
    tcp111,2049NFS(网络文件系统)权限配置不当
    tcp137,139,445SMB(NETBIOS协议)可尝试爆破以及smb自身的各种远程执行类漏洞利用,如,ms08-067,ms17-010,嗅探等……
    tcp143IMAP(邮件访问协议)可尝试爆破
    udp161SNMP(简单网络管理协议)爆破默认团队字符串,搜集目标内网信息
    tcp389LDAP( 轻量目录访问协议 )ldap注入,允许匿名访问,弱口令
    tcp512,513,514Linux rexec (远程登录)可爆破,rlogin登陆
    tcp873Rsync (数据镜像备份工具)匿名访问,文件上传
    tcp1194OpenVPN(虚拟专用通道)想办法钓VPN账号,进内网
    tcp1352Lotus(Lotus软件)弱口令,信息泄漏,爆破
    tcp1433SQL Server(数据库管理系统)注入,提权,sa弱口令,爆破
    tcp1521Oracle(甲骨文数据库)tns爆破,注入,弹shell…
    tcp1500ISPmanager( 主机控制面板)弱口令
    tcp1723PPTP(点对点隧道协议 )爆破,想办法钓VPN账号,进内网
    tcp2082,2083cPanel (虚拟机控制系统 )弱口令
    tcp2181ZooKeeper(分布式系统的可靠协调系统 )未授权访问
    tcp2601,2604Zebra (zebra路由)默认密码zerbra
    tcp3128Squid (代理缓存服务器)弱口令
    tcp3312,3311kangle(web服务器)弱口令
    tcp3306MySQL(数据库)注入,提权,爆破
    tcp3389Windows rdp(桌面协议)shift后门[需要03以下的系统],爆破,ms12-020
    tcp3690SVN(开放源代码的版本控制系统)svn泄露,未授权访问
    tcp4848GlassFish(应用服务器)弱口令
    tcp5000Sybase/DB2(数据库)爆破,注入
    tcp5432PostgreSQL(数据库)爆破,注入,弱口令
    tcp5900,5901,5902VNC(虚拟网络控制台,远控)弱口令爆破
    tcp5984CouchDB(数据库)未授权导致的任意指令执行
    tcp6379Redis(数据库)可尝试未授权访问,弱口令爆破
    tcp7001,7002WebLogic(WEB应用系统)Java反序列化,弱口令
    tcp7778Kloxo(虚拟主机管理系统)主机面板登录
    tcp8000Ajenti(Linux服务器管理面板)弱口令
    tcp8443Plesk(虚拟主机管理面板)弱口令
    tcp8069Zabbix (系统网络监视)远程执行,SQL注入
    tcp8080-8089Jenkins,JBoss (应用服务器)反序列化,控制台弱口令
    tcp9080-9081,9090WebSphere(应用服务器)Java反序列化/弱口令
    tcp9200,9300ElasticSearch (Lucene的搜索服务器)远程执行
    tcp11211Memcached(缓存系统)未授权访问
    tcp27017,27018MongoDB(数据库)爆破,未授权访问
    tcp50070,50030Hadoop(分布式文件系统)默认端口未授权访问
    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/80/index.html b/page/80/index.html new file mode 100644 index 0000000000..a14620465d --- /dev/null +++ b/page/80/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    就在前天,公司里一位新入职的前端小伙伴找到我说遇到了一个问题,自己写的vue代码本地运行是好用的,但是打包后提给后台就访问不了接口了,刚开始我以为是代理配置的有问题,但是经过检查也没什么问题,因为是个get方法,所以我就直接在浏览器地址里敲了api地址,也能正常获取到数据,那么这么看就不是代理或者接口的问题,后来我又看了下调试工具,network中根本就没发出请求,我怀疑是直接前台就拦截了,最后我发现他这个api参数超级长,长的相当吓人,可以看一下下图。
    lROUMt.png
    我记得url是有长度限制的,于是我就把url变短再次请求,虽然因为参数不全,接口报了错,但是发出去了,由此我定位应该是url超长,因为vue中配置了什么导致的访问被拦截了。于是我就开始了搜索引擎查找解决方案之旅,接下来就把我了解到的信息一一整理。

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/81/index.html b/page/81/index.html new file mode 100644 index 0000000000..0fbf02d1b8 --- /dev/null +++ b/page/81/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    前言

    面试中时常会出现需要手动实现某个功能的要求,所以我准备整理一个自己动手实现系列文章,第一个呢就是自己动手实现 jsonp

    +

    实现 jsonp 思路

      +
    1. 将传入的data数据转化为url字符串形式
    2. +
    3. 处理url中的回调函数
    4. +
    5. 创建一个script标签并插入到页面中
    6. +
    7. 挂载回调函数
    8. +
    +

    代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    // js代码

    (function (window,document) {
    "use strict";
    var jsonp = function (url,data,callback) {
    // 1.将传入的data数据转化为url字符串形式
    // {id:1,name:'jack'} => id=1&name=jack
    var dataString = url.indexof('?') == -1? '?': '&';
    for(var key in data){
    dataString += key + '=' + data[key] + '&';
    };

    // 2 处理url中的回调函数
    // cbFuncName回调函数的名字 :my_json_cb_名字的前缀 + 随机数(把小数点去掉)
    var cbFuncName = 'my_json_cb_' + Math.random().toString().replace('.','');
    dataString += 'callback=' + cbFuncName;

    // 3.创建一个script标签并插入到页面中
    var scriptEle = document.createElement('script');
    scriptEle.src = url + dataString;

    // 4.挂载回调函数
    window[cbFuncName] = function (data) {
    callback(data); // 处理完回调函数的数据之后,删除jsonp的script标签
    document.body.removeChild(scriptEle);
    }
    document.body.appendChild(scriptEle);
    }
    window.$jsonp = jsonp;
    })(window,document)
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/82/index.html b/page/82/index.html new file mode 100644 index 0000000000..e07b500e24 --- /dev/null +++ b/page/82/index.html @@ -0,0 +1,617 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    网络相关的知识虽然平时在编码阶段很少遇到,但是并不是说它不重要,尤其是前端优化层面,能从网络传输过程优化的点有很多,所以,我们需要好好掌握网络相关的知识,接下来,我将会将我了解到的网络相关的知识点进行一一整理

    +

    栈 Heap

    +

    栈是一个线性结构,在计算机中是一个相当常见的数据结构。
    栈的特点是只能在某一端添加或删除数据,遵循先进后出(FILO)的原则

    +
    +

    实现

    每种数据结构都可以用很多种方式来实现,其实可以把栈看成是数组的一个子集,所以这里使用数组来实现

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // js代码
    class Stack {
    constructor() {
    this.stack = []
    }
    push(item) {
    this.stack.push(item)
    }
    pop() {
    this.stack.pop()
    }
    peek() { // 取最后一项
    return this.stack[this.getCount() - 1]
    }
    getCount() {
    return this.stack.length
    }
    isEmpty() {
    return this.getCount() === 0
    }
    }
    +

    应用

    选取了 LeetCode 上序号为 20 的题目

    +

    题意是匹配括号,可以通过栈的特性来完成这道题目

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    var isValid = function(str) {
    let map = {
    '(': -1,
    ')': 1,
    '[': -2,
    ']': 2,
    '{': -3,
    '}': 3
    }
    let stack = [] // 空数组
    for (let i = 0; i < str.length; i++) { // 遍历字符串每个字符
    if (map[str[i]] < 0) { // 如果是左边括号,入栈
    stack.push(str[i])
    } else { // 否则出栈,判断左右括号加到一起是不是0
    let last = stack.pop()
    if (map[last] + map[str[i]] != 0) return false
    }
    }
    if (stack.length > 0) return false // 循环完成后,判断数组中时候还有剩下的,有剩下的说明括号没闭合
    return true // 否则没剩下的,都闭合了
    }
    + +

    队列

    +

    队列一个线性结构,特点是在某一端添加数据,在另一端删除数据,遵循先进先出(FIFO)的原则。

    +
    +

    实现

    这里会讲解两种实现队列的方式,分别是单链队列循环队列

    +
      +
    • 链队列即队列的链式存储结构,结构上就是一个单链表,但数据只能是头进尾出。链式结构更加的灵活,特别是在存储空间上,基本不会出现溢出的情况,所以不用像循环队列一样判断队列是否已满,且空间的利用率相对较高。
        +
      • 链队列front指向头结点,头结点不存储数据,rear指向队尾结点。
      • +
      +
    • +
    +
      +
    • 循环队列即为头尾相接的队列,它的最大存储空间和顺序队列一样由数组界定,但队列的长度并不一定等同于数组的长度;循环队列的队首和队尾分别由两个指针front、rear标识,于是这样就能做到首尾相接。

      +
    • +
    • 链队列:为操作方便,给链队列添加一个头结点

      +
    • +
    • 循环队列:附设两个指针front和rear分别指示队列头元素及尾元素的位置,每当插入新的队尾元素是,尾指针加1;每当删除队列头元素是,头指针加1

      +
        +
      • 如果用循环队列,则必须设定一个最大队列长度;若无法确定最大长度,则宜采用链队列。
      • +
      +
    • +
    +

    单链队列

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // js代码

    class Queue {
    constructor() {
    this.queue = []
    }
    enQueue(item) {
    this.queue.push(item)
    }
    deQueue() {
    return this.queue.shift()
    }
    getHeader() {
    return this.queue[0]
    }
    getLength() {
    return this.queue.length
    }
    isEmpty() {
    return this.getLength() === 0
    }
    }
    +

    因为单链队列在出队操作的时候需要 O(n) 的时间复杂度,所以引入了循环队列。
    循环队列的出队操作平均是 O(1) 的时间复杂度。

    +

    循环队列

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    // js代码

    class SqQueue {
    constructor(length) {
    this.queue = new Array(length + 1)
    // 队头
    this.first = 0
    // 队尾
    this.last = 0
    // 当前队列大小
    this.size = 0
    }
    enQueue(item) {
    // 判断队尾 + 1 是否为队头
    // 如果是就代表需要扩容数组
    // % this.queue.length 是为了防止数组越界
    if (this.first === (this.last + 1) % this.queue.length) {
    this.resize(this.getLength() * 2 + 1)
    }
    this.queue[this.last] = item
    this.size++
    this.last = (this.last + 1) % this.queue.length
    }
    deQueue() {
    if (this.isEmpty()) {
    throw Error('Queue is empty')
    }
    let r = this.queue[this.first]
    this.queue[this.first] = null
    this.first = (this.first + 1) % this.queue.length
    this.size--
    // 判断当前队列大小是否过小
    // 为了保证不浪费空间,在队列空间等于总长度四分之一时
    // 且不为 2 时缩小总长度为当前的一半
    if (this.size === this.getLength() / 4 && this.getLength() / 2 !== 0) {
    this.resize(this.getLength() / 2)
    }
    return r
    }
    getHeader() {
    if (this.isEmpty()) {
    throw Error('Queue is empty')
    }
    return this.queue[this.first]
    }
    getLength() {
    return this.queue.length - 1
    }
    isEmpty() {
    return this.first === this.last
    }
    resize(length) {
    let q = new Array(length)
    for (let i = 0; i < length; i++) {
    q[i] = this.queue[(i + this.first) % this.queue.length]
    }
    this.queue = q
    this.first = 0
    this.last = this.size
    }
    }
    + +

    链表

    +

    链表是一个线性结构,同时也是一个天然的递归结构。链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。

    +
    +

    实现

    单向链表

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    // js代码

    class Node {
    constructor(v, next) {
    this.value = v
    this.next = next
    }
    }
    class LinkList {
    constructor() {
    // 链表长度
    this.size = 0
    // 虚拟头部
    this.dummyNode = new Node(null, null)
    }
    find(header, index, currentIndex) {
    if (index === currentIndex) return header
    return this.find(header.next, index, currentIndex + 1)
    }
    addNode(v, index) {
    this.checkIndex(index)
    // 当往链表末尾插入时,prev.next 为空
    // 其他情况时,因为要插入节点,所以插入的节点
    // 的 next 应该是 prev.next
    // 然后设置 prev.next 为插入的节点
    let prev = this.find(this.dummyNode, index, 0)
    prev.next = new Node(v, prev.next)
    this.size++
    return prev.next
    }
    insertNode(v, index) {
    return this.addNode(v, index)
    }
    addToFirst(v) {
    return this.addNode(v, 0)
    }
    addToLast(v) {
    return this.addNode(v, this.size)
    }
    removeNode(index, isLast) {
    this.checkIndex(index)
    index = isLast ? index - 1 : index
    let prev = this.find(this.dummyNode, index, 0)
    let node = prev.next
    prev.next = node.next
    node.next = null
    this.size--
    return node
    }
    removeFirstNode() {
    return this.removeNode(0)
    }
    removeLastNode() {
    return this.removeNode(this.size, true)
    }
    checkIndex(index) {
    if (index < 0 || index > this.size) throw Error('Index error')
    }
    getNode(index) {
    this.checkIndex(index)
    if (this.isEmpty()) return
    return this.find(this.dummyNode, index, 0).next
    }
    isEmpty() {
    return this.size === 0
    }
    getSize() {
    return this.size
    }
    }
    + +

    二叉树

    树拥有很多种结构,二叉树是树中最常用的结构,同时也是一个天然的递归结构。

    +

    二叉树拥有一个根节点,每个节点至多拥有两个子节点,分别为:左节点和右节点。树的最底部节点称之为叶节点,当一颗树的叶数量数量为满时,该树可以称之为满二叉树。

    +

    二分搜索树

    二分搜索树也是二叉树,拥有二叉树的特性。但是区别在于二分搜索树每个节点的值都比他的左子树的值大,比右子树的值小。

    +

    这种存储方式很适合于数据搜索。如下图所示,当需要查找 6 的时候,因为需要查找的值比根节点的值大,所以只需要在根节点的右子树上寻找,大大提高了搜索效率。

    +

    实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    // js代码

    class Node {
    constructor(value) {
    this.value = value
    this.left = null
    this.right = null
    }
    }
    class BST {
    constructor() {
    this.root = null
    this.size = 0
    }
    getSize() {
    return this.size
    }
    isEmpty() {
    return this.size === 0
    }
    addNode(v) {
    this.root = this._addChild(this.root, v)
    }
    // 添加节点时,需要比较添加的节点值和当前
    // 节点值的大小
    _addChild(node, v) {
    if (!node) {
    this.size++
    return new Node(v)
    }
    if (node.value > v) {
    node.left = this._addChild(node.left, v)
    } else if (node.value < v) {
    node.right = this._addChild(node.right, v)
    }
    return node
    }
    }
    + +

    以上是最基本的二分搜索树实现,接下来实现树的遍历。

    +

    对于树的遍历来说,有三种遍历方法,分别是先序遍历中序遍历后序遍历

    +

    三种遍历的区别在于何时访问节点。在遍历树的过程中,每个节点都会遍历三次,分别是遍历到自己,遍历左子树和遍历右子树。如果需要实现先序遍历,那么只需要第一次遍历到节点时进行操作即可。

    +

    以下都是递归实现.

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    // js代码

    // 先序遍历可用于打印树的结构
    // 先序遍历先访问根节点,然后访问左节点,最后访问右节点。
    preTraversal() {
    this._pre(this.root)
    }
    _pre(node) {
    if (node) {
    console.log(node.value)
    this._pre(node.left)
    this._pre(node.right)
    }
    }
    // 中序遍历可用于排序
    // 对于 BST 来说,中序遍历可以实现一次遍历就
    // 得到有序的值
    // 中序遍历表示先访问左节点,然后访问根节点,最后访问右节点。
    midTraversal() {
    this._mid(this.root)
    }
    _mid(node) {
    if (node) {
    this._mid(node.left)
    console.log(node.value)
    this._mid(node.right)
    }
    }
    // 后序遍历可用于先操作子节点
    // 再操作父节点的场景
    // 后序遍历表示先访问左节点,然后访问右节点,最后访问根节点。
    backTraversal() {
    this._back(this.root)
    }
    _back(node) {
    if (node) {
    this._back(node.left)
    this._back(node.right)
    console.log(node.value)
    }
    }
    + +

    以上的这几种遍历都可以称之为深度遍历,对应的还有种遍历叫做广度遍历,也就是一层层地遍历树。对于广度遍历来说,我们需要利用之前讲过的队列结构来完成。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // js代码

    breadthTraversal() {
    if (!this.root) return null
    let q = new Queue()
    // 将根节点入队
    q.enQueue(this.root)
    // 循环判断队列是否为空,为空
    // 代表树遍历完毕
    while (!q.isEmpty()) {
    // 将队首出队,判断是否有左右子树
    // 有的话,就先左后右入队
    let n = q.deQueue()
    console.log(n.value)
    if (n.left) q.enQueue(n.left)
    if (n.right) q.enQueue(n.right)
    }
    }
    +

    接下来先介绍如何在树中寻找最小值或最大数。因为二分搜索树的特性,所以最小值一定在根节点的最左边,最大值相反

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // js代码

    getMin() {
    return this._getMin(this.root).value
    }
    _getMin(node) {
    if (!node.left) return node
    return this._getMin(node.left)
    }
    getMax() {
    return this._getMax(this.root).value
    }
    _getMax(node) {
    if (!node.right) return node
    return this._getMin(node.right)
    }
    + +

    向上取整和向下取整,这两个操作是相反的,所以代码也是类似的,这里只介绍如何向下取整。既然是向下取整,那么根据二分搜索树的特性,值一定在根节点的左侧。只需要一直遍历左子树直到当前节点的值不再大于等于需要的值,然后判断节点是否还拥有右子树。如果有的话,继续上面的递归判断。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // js代码

    floor(v) {
    let node = this._floor(this.root, v)
    return node ? node.value : null
    }
    _floor(node, v) {
    if (!node) return null
    if (node.value === v) return v
    // 如果当前节点值还比需要的值大,就继续递归
    if (node.value > v) {
    return this._floor(node.left, v)
    }
    // 判断当前节点是否拥有右子树
    let right = this._floor(node.right, v)
    if (right) return right
    return node
    }
    +

    排名,这是用于获取给定值的排名或者排名第几的节点的值,这两个操作也是相反的,所以这个只介绍如何获取排名第几的节点的值。对于这个操作而言,我们需要略微的改造点代码,让每个节点拥有一个 size 属性。该属性表示该节点下有多少子节点(包含自身)。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    // js代码

    class Node {
    constructor(value) {
    this.value = value
    this.left = null
    this.right = null
    // 修改代码
    this.size = 1
    }
    }
    // 新增代码
    _getSize(node) {
    return node ? node.size : 0
    }
    _addChild(node, v) {
    if (!node) {
    return new Node(v)
    }
    if (node.value > v) {
    // 修改代码
    node.size++
    node.left = this._addChild(node.left, v)
    } else if (node.value < v) {
    // 修改代码
    node.size++
    node.right = this._addChild(node.right, v)
    }
    return node
    }
    select(k) {
    let node = this._select(this.root, k)
    return node ? node.value : null
    }
    _select(node, k) {
    if (!node) return null
    // 先获取左子树下有几个节点
    let size = node.left ? node.left.size : 0
    // 判断 size 是否大于 k
    // 如果大于 k,代表所需要的节点在左节点
    if (size > k) return this._select(node.left, k)
    // 如果小于 k,代表所需要的节点在右节点
    // 注意这里需要重新计算 k,减去根节点除了右子树的节点数量
    if (size < k) return this._select(node.right, k - size - 1)
    return node
    }
    + +

    接下来讲解的是二分搜索树中最难实现的部分:删除节点。因为对于删除节点来说,会存在以下几种情况

    +
      +
    • 需要删除的节点没有子树
    • +
    • 需要删除的节点只有一条子树
    • +
    • 需要删除的节点有左右两条树
    • +
    +

    对于前两种情况很好解决,但是第三种情况就有难度了,所以先来实现相对简单的操作:删除最小节点,对于删除最小节点来说,是不存在第三种情况的,删除最大节点操作是和删除最小节点相反的,所以这里也就不再赘述。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // js代码

    delectMin() {
    this.root = this._delectMin(this.root)
    console.log(this.root)
    }
    _delectMin(node) {
    // 一直递归左子树
    // 如果左子树为空,就判断节点是否拥有右子树
    // 有右子树的话就把需要删除的节点替换为右子树
    if ((node != null) & !node.left) return node.right
    node.left = this._delectMin(node.left)
    // 最后需要重新维护下节点的 `size`
    node.size = this._getSize(node.left) + this._getSize(node.right) + 1
    return node
    }
    + +

    最后讲解的就是如何删除任意节点了。对于这个操作,T.Hibbard 在 1962 年提出了解决这个难题的办法,也就是如何解决第三种情况。

    +

    当遇到这种情况时,需要取出当前节点的后继节点(也就是当前节点右子树的最小节点)来替换需要删除的节点。然后将需要删除节点的左子树赋值给后继结点,右子树删除后继结点后赋值给他。

    +

    你如果对于这个解决办法有疑问的话,可以这样考虑。因为二分搜索树的特性,父节点一定比所有左子节点大,比所有右子节点小。那么当需要删除父节点时,势必需要拿出一个比父节点大的节点来替换父节点。这个节点肯定不存在于左子树,必然存在于右子树。然后又需要保持父节点都是比右子节点小的,那么就可以取出右子树中最小的那个节点来替换父节点。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    // js代码

    delect(v) {
    this.root = this._delect(this.root, v)
    }
    _delect(node, v) {
    if (!node) return null
    // 寻找的节点比当前节点小,去左子树找
    if (node.value < v) {
    node.right = this._delect(node.right, v)
    } else if (node.value > v) {
    // 寻找的节点比当前节点大,去右子树找
    node.left = this._delect(node.left, v)
    } else {
    // 进入这个条件说明已经找到节点
    // 先判断节点是否拥有拥有左右子树中的一个
    // 是的话,将子树返回出去,这里和 `_delectMin` 的操作一样
    if (!node.left) return node.right
    if (!node.right) return node.left
    // 进入这里,代表节点拥有左右子树
    // 先取出当前节点的后继结点,也就是取当前节点右子树的最小值
    let min = this._getMin(node.right)
    // 取出最小值后,删除最小值
    // 然后把删除节点后的子树赋值给最小值节点
    min.right = this._delectMin(node.right)
    // 左子树不动
    min.left = node.left
    node = min
    }
    // 维护 size
    node.size = this._getSize(node.left) + this._getSize(node.right) + 1
    return node
    }
    + +

    AVL 树

    +

    二分搜索树实际在业务中是受到限制的,因为并不是严格的 O(logN),在极端情况下会退化成链表,比如加入一组升序的数字就会造成这种情况。

    +
    +
    +

    AVL 树改进了二分搜索树,在 AVL 树中任意节点的左右子树的高度差都不大于 1,这样保证了时间复杂度是严格的 O(logN)。基于此,对 AVL 树增加或删除节点时可能需要旋转树来达到高度的平衡。

    +
    +

    实现

    因为 AVL 树是改进了二分搜索树,所以部分代码是于二分搜索树重复的,对于重复内容不作再次解析。

    +

    对于 AVL 树来说,添加节点会有四种情况
    lWB0nf.png

    +

    对于左左情况来说,新增加的节点位于节点 2 的左侧,这时树已经不平衡,需要旋转。因为搜索树的特性,节点比左节点大,比右节点小,所以旋转以后也要实现这个特性。

    +

    旋转之前:new < 2 < C < 3 < B < 5 < A,右旋之后节点 3 为根节点,这时候需要将节点 3 的右节点加到节点 5 的左边,最后还需要更新节点的高度。

    +

    对于右右情况来说,相反于左左情况,所以不再赘述。

    +

    对于左右情况来说,新增加的节点位于节点 4 的右侧。对于这种情况,需要通过两次旋转来达到目的。

    +

    首先对节点的左节点左旋,这时树满足左左的情况,再对节点进行一次右旋就可以达到目的。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    // js代码

    class Node {
    constructor(value) {
    this.value = value
    this.left = null
    this.right = null
    this.height = 1
    }
    }

    class AVL {
    constructor() {
    this.root = null
    }
    addNode(v) {
    this.root = this._addChild(this.root, v)
    }
    _addChild(node, v) {
    if (!node) {
    return new Node(v)
    }
    if (node.value > v) {
    node.left = this._addChild(node.left, v)
    } else if (node.value < v) {
    node.right = this._addChild(node.right, v)
    } else {
    node.value = v
    }
    node.height =
    1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
    let factor = this._getBalanceFactor(node)
    // 当需要右旋时,根节点的左树一定比右树高度高
    if (factor > 1 && this._getBalanceFactor(node.left) >= 0) {
    return this._rightRotate(node)
    }
    // 当需要左旋时,根节点的左树一定比右树高度矮
    if (factor < -1 && this._getBalanceFactor(node.right) <= 0) {
    return this._leftRotate(node)
    }
    // 左右情况
    // 节点的左树比右树高,且节点的左树的右树比节点的左树的左树高
    if (factor > 1 && this._getBalanceFactor(node.left) < 0) {
    node.left = this._leftRotate(node.left)
    return this._rightRotate(node)
    }
    // 右左情况
    // 节点的左树比右树矮,且节点的右树的右树比节点的右树的左树矮
    if (factor < -1 && this._getBalanceFactor(node.right) > 0) {
    node.right = this._rightRotate(node.right)
    return this._leftRotate(node)
    }

    return node
    }
    _getHeight(node) {
    if (!node) return 0
    return node.height
    }
    _getBalanceFactor(node) {
    return this._getHeight(node.left) - this._getHeight(node.right)
    }
    // 节点右旋
    // 5 2
    // / \ / \
    // 2 6 ==> 1 5
    // / \ / / \
    // 1 3 new 3 6
    // /
    // new
    _rightRotate(node) {
    // 旋转后新根节点
    let newRoot = node.left
    // 需要移动的节点
    let moveNode = newRoot.right
    // 节点 2 的右节点改为节点 5
    newRoot.right = node
    // 节点 5 左节点改为节点 3
    node.left = moveNode
    // 更新树的高度
    node.height =
    1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
    newRoot.height =
    1 +
    Math.max(this._getHeight(newRoot.left), this._getHeight(newRoot.right))

    return newRoot
    }
    // 节点左旋
    // 4 6
    // / \ / \
    // 2 6 ==> 4 7
    // / \ / \ \
    // 5 7 2 5 new
    // \
    // new
    _leftRotate(node) {
    // 旋转后新根节点
    let newRoot = node.right
    // 需要移动的节点
    let moveNode = newRoot.left
    // 节点 6 的左节点改为节点 4
    newRoot.left = node
    // 节点 4 右节点改为节点 5
    node.right = moveNode
    // 更新树的高度
    node.height =
    1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
    newRoot.height =
    1 +
    Math.max(this._getHeight(newRoot.left), this._getHeight(newRoot.right))

    return newRoot
    }
    }
    + +

    Trie

    +

    在计算机科学,trie,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。
    简单点来说,这个结构的作用大多是为了方便搜索字符串,该树有以下几个特点

    +
    +
      +
    • 根节点代表空字符串,每个节点都有 N(假如搜索英文字符,就有 26 条) 条链接,每条链接代表一个字符
    • +
    • 节点不存储字符,只有路径才存储,这点和其他的树结构不同
    • +
    • 从根节点开始到任意一个节点,将沿途经过的字符连接起来就是该节点对应的字符串
    • +
    +

    实现

    总得来说 Trie 的实现相比别的树结构来说简单的很多,实现就以搜索英文字符为例。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    // js代码

    class TrieNode {
    constructor() {
    // 代表每个字符经过节点的次数
    this.path = 0
    // 代表到该节点的字符串有几个
    this.end = 0
    // 链接
    this.next = new Array(26).fill(null)
    }
    }
    class Trie {
    constructor() {
    // 根节点,代表空字符
    this.root = new TrieNode()
    }
    // 插入字符串
    insert(str) {
    if (!str) return
    let node = this.root
    for (let i = 0; i < str.length; i++) {
    // 获得字符先对应的索引
    let index = str[i].charCodeAt() - 'a'.charCodeAt()
    // 如果索引对应没有值,就创建
    if (!node.next[index]) {
    node.next[index] = new TrieNode()
    }
    node.path += 1
    node = node.next[index]
    }
    node.end += 1
    }
    // 搜索字符串出现的次数
    search(str) {
    if (!str) return
    let node = this.root
    for (let i = 0; i < str.length; i++) {
    let index = str[i].charCodeAt() - 'a'.charCodeAt()
    // 如果索引对应没有值,代表没有需要搜素的字符串
    if (!node.next[index]) {
    return 0
    }
    node = node.next[index]
    }
    return node.end
    }
    // 删除字符串
    delete(str) {
    if (!this.search(str)) return
    let node = this.root
    for (let i = 0; i < str.length; i++) {
    let index = str[i].charCodeAt() - 'a'.charCodeAt()
    // 如果索引对应的节点的 Path 为 0,代表经过该节点的字符串
    // 已经一个,直接删除即可
    if (--node.next[index].path == 0) {
    node.next[index] = null
    return
    }
    node = node.next[index]
    }
    node.end -= 1
    }
    }
    + +

    并查集

    +

    并查集是一种特殊的树结构,用于处理一些不交集的合并及查询问题。该结构中每个节点都有一个父节点,如果只有当前一个节点,那么该节点的父节点指向自己。
    这个结构中有两个重要的操作,分别是:

    +
    +
      +
    • Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
    • +
    • Union:将两个子集合并成同一个集合。
    • +
    +

    实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    // js代码

    class DisjointSet {
    // 初始化样本
    constructor(count) {
    // 初始化时,每个节点的父节点都是自己
    this.parent = new Array(count)
    // 用于记录树的深度,优化搜索复杂度
    this.rank = new Array(count)
    for (let i = 0; i < count; i++) {
    this.parent[i] = i
    this.rank[i] = 1
    }
    }
    find(p) {
    // 寻找当前节点的父节点是否为自己,不是的话表示还没找到
    // 开始进行路径压缩优化
    // 假设当前节点父节点为 A
    // 将当前节点挂载到 A 节点的父节点上,达到压缩深度的目的
    while (p != this.parent[p]) {
    this.parent[p] = this.parent[this.parent[p]]
    p = this.parent[p]
    }
    return p
    }
    isConnected(p, q) {
    return this.find(p) === this.find(q)
    }
    // 合并
    union(p, q) {
    // 找到两个数字的父节点
    let i = this.find(p)
    let j = this.find(q)
    if (i === j) return
    // 判断两棵树的深度,深度小的加到深度大的树下面
    // 如果两棵树深度相等,那就无所谓怎么加
    if (this.rank[i] < this.rank[j]) {
    this.parent[i] = j
    } else if (this.rank[i] > this.rank[j]) {
    this.parent[j] = i
    } else {
    this.parent[i] = j
    this.rank[j] += 1
    }
    }
    }
    + +

    堆通常是一个可以被看做一棵树的数组对象。
    堆的实现通过构造二叉堆,实为二叉树的一种。这种数据结构具有以下性质。

    +
      +
    • 任意节点小于(或大于)它的所有子节点
    • +
    • 堆总是一棵完全树。即除了最底层,其他层的节点都被元素填满,且最底层从左到右填入。
      将根节点最大的堆叫做最大堆大根堆,根节点最小的堆叫做最小堆小根堆
      优先队列也完全可以用堆来实现,操作是一模一样的。
    • +
    +

    实现大根堆

    堆的每个节点的左边子节点索引是 i * 2 + 1,右边是 i * 2 + 2,父节点是 (i - 1) /2。
    堆有两个核心的操作,分别是 shiftUp 和 shiftDown 。前者用于添加元素,后者用于删除根节点。
    shiftUp 的核心思路是一路将节点与父节点对比大小,如果比父节点大,就和父节点交换位置。
    shiftDown 的核心思路是先将根节点和末尾交换位置,然后移除末尾元素。接下来循环判断父节点和两个子节点的大小,如果子节点大,就把最大的子节点和父节点交换。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    // js代码

    class MaxHeap {
    constructor() {
    this.heap = []
    }
    size() {
    return this.heap.length
    }
    empty() {
    return this.size() == 0
    }
    add(item) {
    this.heap.push(item)
    this._shiftUp(this.size() - 1)
    }
    removeMax() {
    this._shiftDown(0)
    }
    getParentIndex(k) {
    return parseInt((k - 1) / 2)
    }
    getLeftIndex(k) {
    return k * 2 + 1
    }
    _shiftUp(k) {
    // 如果当前节点比父节点大,就交换
    while (this.heap[k] > this.heap[this.getParentIndex(k)]) {
    this._swap(k, this.getParentIndex(k))
    // 将索引变成父节点
    k = this.getParentIndex(k)
    }
    }
    _shiftDown(k) {
    // 交换首位并删除末尾
    this._swap(k, this.size() - 1)
    this.heap.splice(this.size() - 1, 1)
    // 判断节点是否有左孩子,因为二叉堆的特性,有右必有左
    while (this.getLeftIndex(k) < this.size()) {
    let j = this.getLeftIndex(k)
    // 判断是否有右孩子,并且右孩子是否大于左孩子
    if (j + 1 < this.size() && this.heap[j + 1] > this.heap[j]) j++
    // 判断父节点是否已经比子节点都大
    if (this.heap[k] >= this.heap[j]) break
    this._swap(k, j)
    k = j
    }
    }
    _swap(left, right) {
    let rightValue = this.heap[right]
    this.heap[right] = this.heap[left]
    this.heap[left] = rightValue
    }
    }
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/83/index.html b/page/83/index.html new file mode 100644 index 0000000000..b91e05d286 --- /dev/null +++ b/page/83/index.html @@ -0,0 +1,880 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    网络相关的知识虽然平时在编码阶段很少遇到,但是并不是说它不重要,尤其是前端优化层面,能从网络传输过程优化的点有很多,所以,我们需要好好掌握网络相关的知识,接下来,我将会将我了解到的网络相关的知识点进行一一整理

    +

    UDP - User Datagram Protocol - 用户数据报协议

    面向报文

    UDP 是一个面向报文(报文可以理解为一段段的数据)的协议。意思就是 UDP 只是报文的搬运工,不会对报文进行任何拆分和拼接操作。

    +

    具体来说

    +
      +
    • 发送端,应用层将数据传递给传输层的 UDP 协议, UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了
    • +
    • 接收端,网络层将数据传递给传输层, UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作
    • +
    +

    不可靠性

      +
    1. UDP 是无连接的,也就是说通信不需要建立和断开连接。
    2. +
    3. UDP 也是不可靠的。协议收到什么数据就传递什么数据,并且也不会备份数据,对方能不能收到是不关心的
    4. +
    5. UDP 没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP
    6. +
    +

    高效

    因为 UDP 没有 TCP 那么复杂,需要保证数据不丢失且有序到达。所以 UDP 的头部开销小,只有八字节,相比 TCP 的至少二十字节要少得多,在传输数据报文时是很高效的。

    +

    头部包含了以下几个数据

    +
      +
    • 两个十六位的端口号,分别为源端口(可选字段)和目标端口
    • +
    • 整个数据报文的长度
    • +
    • 整个数据报文的检验和( IPv4 可选 字段),该字段用于发现头部信息和数据中的错误
    • +
    +

    传输方式

    UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。

    +

    TCP

    头部

    TCP 头部比 UDP 头部复杂的多

    +

    对于 TCP 头部来说,以下几个字段是很重要的

    +
      +
    • Sequence number(序号),它保证了 TCP 传输的报文都是有序的,对端可以通过序号顺序的拼接报文
    • +
    • Acknowledgement Number(确认编号),它表示数据接收端期望接收的下一个字节的编号是多少,同时也表示上一个序号的数据已经收到
    • +
    • Window Size(窗口大小),它表示还能接收多少字节的数据,用于流量控制
    • +
    • 标识符
        +
      • URG=1:该字段为一表示本数据报的数据部分包含紧急信息,是一个高优先级数据报文,此时紧急指针有效。紧急数据一定位于当前数据包数据部分的最前面,紧急指针标明了紧急数据的尾部。
      • +
      • ACK=1:该字段为一表示确认号字段有效。此外,TCP 还规定在连接建立后传送的所有报文段都必须把 ACK 置为一。
      • +
      • PSH=1:该字段为一表示接收端应该立即将数据 push 给应用层,而不是等到缓冲区满后再提交。
      • +
      • RST=1:该字段为一表示当前 TCP 连接出现严重问题,可能需要重新建立 TCP 连接,也可以用于拒绝非法的报文段和拒绝连接请求。
      • +
      • SYN=1:当SYN=1,ACK=0时,表示当前报文段是一个连接请求报文。当SYN=1,ACK=1时,表示当前报文段是一个同意建立连接的应答报文。
      • +
      • FIN=1:该字段为一表示此报文段是一个释放连接的请求报文。
      • +
      +
    • +
    +

    状态机

    HTTP 是无连接的,所以作为下层的 TCP 协议也是无连接的,虽然看似 TCP 将两端连接了起来,但是其实只是两端共同维护了一个状态

    +

    TCP 的状态机是很复杂的,并且与建立断开连接时的握手息息相关,接下来就来详细描述下两种握手。
    在这之前需要了解一个重要的性能指标 RTT 。该指标表示发送端发送数据到接收到对端数据所需的往返时间。

    +

    建立连接三次握手

    在 TCP 协议中,主动发起请求的一端为客户端,被动连接的一端称为服务端。不管是客户端还是服务端,TCP 连接建立完后都能发送和接收数据,所以 TCP 也是一个全双工的协议。

    +

    起初,两端都为 CLOSED 状态。在通信开始前,双方都会创建 TCB。 服务器创建完 TCB 后遍进入 LISTEN 状态,此时开始等待客户端发送数据。

    +

    第一次握手

    客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态,x 表示客户端的数据通信初始序号。

    +

    第二次握手

    服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。

    +

    第三次握手

    当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。

    +

    PS:第三次握手可以包含数据,通过 TCP 快速打开(TFO)技术。其实只要涉及到握手的协议,都可以使用类似 TFO 的方式,客户端和服务端存储相同 cookie,下次握手时发出 cookie 达到减少 RTT 的目的。

    +

    你是否有疑惑明明两次握手就可以建立起连接,为什么还需要第三次应答?

    +

    因为这是为了防止失效的连接请求报文段被服务端接收,从而产生错误。

    +

    可以想象如下场景。客户端发送了一个连接请求 A,但是因为网络原因造成了超时,这时 TCP 会启动超时重传的机制再次发送一个连接请求 B。此时请求顺利到达服务端,服务端应答完就建立了请求。如果连接请求 A 在两端关闭后终于抵达了服务端,那么这时服务端会认为客户端又需要建立 TCP 连接,从而应答了该请求并进入 ESTABLISHED 状态。此时客户端其实是 CLOSED 状态,那么就会导致服务端一直等待,造成资源的浪费。

    +

    PS:在建立连接中,任意一端掉线,TCP 都会重发 SYN 包,一般会重试五次,在建立连接中可能会遇到 SYN FLOOD 攻击。遇到这种情况你可以选择调低重试次数或者干脆在不能处理的情况下拒绝请求。

    +

    断开链接四次握手

    TCP 是全双工的,在断开连接时两端都需要发送 FIN 和 ACK。

    +

    第一次握手

    若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。

    +

    第二次握手

    B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,表示 A 到 B 的连接已经释放,不接收 A 发的数据了。但是因为 TCP 连接时双向的,所以 B 仍旧可以发送数据给 A。

    +

    第三次握手

    B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。

    +

    PS:通过延迟确认的技术(通常有时间限制,否则对方会误认为需要重传),可以将第二次和第三次握手合并,延迟 ACK 包的发送。

    +

    第四次握手

    A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。

    +

    为什么 A 要进入 TIME-WAIT 状态,等待 2MSL 时间后才进入 CLOSED 状态?

    +

    为了保证 B 能收到 A 的确认应答。若 A 发完确认应答后直接进入 CLOSED 状态,如果确认应答因为网络问题一直没有到达,那么会造成 B 不能正常关闭。

    +

    ARQ 协议

    ARQ 协议也就是超时重传机制。通过确认和超时机制保证了数据的正确送达,ARQ 协议包含停止等待 ARQ 和连续 ARQ

    +

    停止等待 ARQ

    正常传输过程

    只要 A 向 B 发送一段报文,都要停止发送并启动一个定时器,等待对端回应,在定时器时间内接收到对端应答就取消定时器并发送下一段报文。

    +

    报文丢失或出错

    在报文传输的过程中可能会出现丢包。这时候超过定时器设定的时间就会再次发送丢包的数据直到对端响应,所以需要每次都备份发送的数据。

    +

    即使报文正常的传输到对端,也可能出现在传输过程中报文出错的问题。这时候对端会抛弃该报文并等待 A 端重传。

    +

    PS:一般定时器设定的时间都会大于一个 RTT 的平均时间。

    +

    ACK 超时或丢失

    对端传输的应答也可能出现丢失或超时的情况。那么超过定时器时间 A 端照样会重传报文。这时候 B 端收到相同序号的报文会丢弃该报文并重传应答,直到 A 端发送下一个序号的报文。

    +

    在超时的情况下也可能出现应答很迟到达,这时 A 端会判断该序号是否已经接收过,如果接收过只需要丢弃应答即可。

    +

    这个协议的缺点就是传输效率低,在良好的网络环境下每次发送报文都得等待对端的 ACK 。

    +

    连续 ARQ

    在连续 ARQ 中,发送端拥有一个发送窗口,可以在没有收到应答的情况下持续发送窗口内的数据,这样相比停止等待 ARQ 协议来说减少了等待时间,提高了效率。

    +

    累计确认

    连续 ARQ 中,接收端会持续不断收到报文。如果和停止等待 ARQ 中接收一个报文就发送一个应答一样,就太浪费资源了。通过累计确认,可以在收到多个报文以后统一回复一个应答报文。报文中的 ACK 可以用来告诉发送端这个序号之前的数据已经全部接收到了,下次请发送这个序号 + 1的数据。

    +

    但是累计确认也有一个弊端。在连续接收报文时,可能会遇到接收到序号 5 的报文后,并未接到序号 6 的报文,然而序号 7 以后的报文已经接收。遇到这种情况时,ACK 只能回复 6,这样会造成发送端重复发送数据,这种情况下可以通过 Sack 来解决,这个会在下文说到。

    +

    滑动窗口

    在上面小节中讲到了发送窗口。在 TCP 中,两端都维护着窗口:分别为发送端窗口和接收端窗口。

    +

    发送端窗口包含已发送但未收到应答的数据和可以发送但是未发送的数据。

    +

    发送端窗口是由接收窗口剩余大小决定的。接收方会把当前接收窗口的剩余大小写入应答报文,发送端收到应答后根据该值和当前网络拥塞情况设置发送窗口的大小,所以发送窗口的大小是不断变化的。

    +

    当发送端接收到应答报文后,会随之将窗口进行滑动

    +

    滑动窗口实现了流量控制。接收方通过报文告知发送方还可以发送多少数据,从而保证接收方能够来得及接收数据。

    +

    Zero 窗口

    在发送报文的过程中,可能会遇到对端出现零窗口的情况。在该情况下,发送端会停止发送数据,并启动 persistent timer 。该定时器会定时发送请求给对端,让对端告知窗口大小。在重试次数超过一定次数后,可能会中断 TCP 链接。

    +

    拥塞处理

    拥塞处理和流量控制不同,后者是作用于接收方,保证接收方来得及接受数据。而前者是作用于网络,防止过多的数据拥塞网络,避免出现网络负载过大的情况。

    +

    拥塞处理包括了四个算法,分别为:慢开始拥塞避免快速重传快速恢复

    +

    慢开始算法

    慢开始算法,顾名思义,就是在传输开始时将发送窗口慢慢指数级扩大,从而避免一开始就传输大量数据导致网络拥塞。

    +

    慢开始算法步骤具体如下

    +
      +
    1. 连接初始设置拥塞窗口(Congestion Window) 为 1 MSS(一个分段的最大数据量)
    2. +
    3. 每过一个 RTT 就将窗口大小乘二
    4. +
    5. 指数级增长肯定不能没有限制的,所以有一个阈值限制,当窗口大小大于阈值时就会启动拥塞避免算法。
    6. +
    +

    拥塞避免算法

    拥塞避免算法相比简单点,每过一个 RTT 窗口大小只加一,这样能够避免指数级增长导致网络拥塞,慢慢将大小调整到最佳值。

    +

    在传输过程中可能定时器超时的情况,这时候 TCP 会认为网络拥塞了,会马上进行以下步骤:

    +
      +
    • 将阈值设为当前拥塞窗口的一半
    • +
    • 将拥塞窗口设为 1 MSS
    • +
    • 启动拥塞避免算法
    • +
    +

    快速重传

    快速重传一般和快恢复一起出现。一旦接收端收到的报文出现失序的情况,接收端只会回复最后一个顺序正确的报文序号(没有 Sack 的情况下)。如果收到三个重复的 ACK,无需等待定时器超时再重发而是启动快速重传。具体算法分为两种:

    +

    TCP Taho 实现如下

      +
    • 将阈值设为当前拥塞窗口的一半
    • +
    • 将拥塞窗口设为 1 MSS
    • +
    • 重新开始慢开始算法
    • +
    +

    TCP Reno 实现如下

      +
    • 拥塞窗口减半
    • +
    • 将阈值设为当前拥塞窗口
    • +
    • 进入快恢复阶段(重发对端需要的包,一旦收到一个新的 ACK 答复就退出该阶段)
    • +
    • 使用拥塞避免算法
    • +
    +

    TCP New Ren 改进后的快恢复

    TCP New Reno 算法改进了之前 TCP Reno 算法的缺陷。在之前,快恢复中只要收到一个新的 ACK 包,就会退出快恢复。

    +

    在 TCP New Reno 中,TCP 发送方先记下三个重复 ACK 的分段的最大序号。

    +

    假如我有一个分段数据是 1 ~ 10 这十个序号的报文,其中丢失了序号为 3 和 7 的报文,那么该分段的最大序号就是 10。发送端只会收到 ACK 序号为 3 的应答。这时候重发序号为 3 的报文,接收方顺利接收并会发送 ACK 序号为 7 的应答。这时候 TCP 知道对端是有多个包未收到,会继续发送序号为 7 的报文,接收方顺利接收并会发送 ACK 序号为 11 的应答,这时发送端认为这个分段接收端已经顺利接收,接下来会退出快恢复阶段。

    +

    HTTP

    HTTP 协议是个无状态协议,不会保存状态。

    +

    PostGet 的区别

    先引入副作用幂等的概念。

    +
    +

    副作用指对服务器上的资源做改变,搜索是无副作用的,注册是副作用的。

    +
    +
    +

    幂等指发送 M 和 N 次请求(两者不相同且都大于 1),服务器上资源的状态一致,比如注册 10 个和 11 个帐号是不幂等的,对文章进行更改 10 次和 11 次是幂等的。

    +
    +

    在规范的应用场景上说, Get 多用于无副作用,幂等的场景,例如搜索关键字。 Post 多用于副作用,不幂等的场景,例如注册。

    +

    在技术上说:

    +
      +
    • Get 请求能缓存,Post 不能
    • +
    • Post 相对 Get 安全一点点,因为Get 请求都包含在 URL 里,且会被浏览器保存历史纪录,Post 不会,但是在抓包的情况下都是一样的。
    • +
    • Post 可以通过 request body来传输比 Get 更多的数据,Get 没有这个技术
    • +
    • URL有长度限制,会影响 Get 请求,但是这个长度限制是浏览器规定的,不是 RFC 规定的
    • +
    • Post 支持更多的编码类型且不对数据类型限制
    • +
    +

    常见状态码

    2XX 成功

    200 OK,表示从客户端发来的请求在服务器端被正确处理
    204 No content,表示请求成功,但响应报文不含实体的主体部分
    205 Reset Content,表示请求成功,但响应报文不含实体的主体部分,但是与 204 响应不同在于要求请求方重置内容
    206 Partial Content,进行范围请求

    +

    3XX 重定向

    301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
    302 found,临时性重定向,表示资源临时被分配了新的 URL
    303 see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源
    304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况
    307 temporary redirect,临时重定向,和302含义类似,但是期望客户端保持请求方法不变向新的地址发出请求

    +

    4XX 客户端错误

    400 bad request,请求报文存在语法错误
    401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
    403 forbidden,表示对请求资源的访问被服务器拒绝
    404 not found,表示在服务器上没有找到请求的资源

    +

    5XX 服务器错误

    500 internal sever error,表示服务器端在执行请求时发生了错误
    501 Not Implemented,表示服务器不支持当前请求所需要的某个功能
    503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求

    +

    HTTP 首部

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    通用字段作用
    Cache-Control控制缓存的行为
    Connection浏览器想要优先使用的连接类型,比如 keep-alive
    Date创建报文时间
    Pragma报文指令
    Via代理服务器相关信息
    Transfer-Encoding传输编码方式
    Upgrade要求客户端升级协议
    Warning在内容中可能存在错误
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    请求字段作用
    Accept能正确接收的媒体类型
    Accept-Charset能正确接收的字符集
    Accept-Encoding能正确接收的编码格式列表
    Accept-Language能正确接收的语言列表
    Expect期待服务端的指定行为
    From请求方邮箱地址
    Host服务器的域名
    If-Match两端资源标记比较
    If-Modified-Since本地资源未修改返回 304(比较时间)
    If-None-Match本地资源未修改返回 304(比较标记)
    User-Agent客户端信息
    Max-Forwards限制可被代理及网关转发的次数
    Proxy-Authorization向代理服务器发送验证信息
    Range请求某个内容的一部分
    Referer表示浏览器所访问的前一个页面
    TE传输编码方式
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    响应字段作用
    Accept-Ranges是否支持某些种类的范围
    Age资源在代理缓存中存在的时间
    ETag资源标识
    Location客户端重定向到某个 URL
    Proxy-Authenticate向代理服务器发送验证信息
    Server服务器名字
    WWW-Authenticate获取资源需要的验证信息
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    实体字段作用
    Allow资源的正确请求方式
    Content-Encoding内容的编码格式
    Content-Language内容使用的语言
    Content-Lengthrequest body 长度
    Content-Location返回数据的备用地址
    Content-MD5Base64加密格式的内容 MD5检验值
    Content-Range内容的位置范围
    Content-Type内容的媒体类型
    Expires内容的过期时间
    Last_modified内容的最后修改时间
    +

    HTTPS

    HTTPS 还是通过了 HTTP 来传输信息,但是信息通过 TLS 协议进行了加密。

    +

    TLS

    TLS 协议位于传输层之上,应用层之下。首次进行 TLS 协议传输需要两个 RTT ,接下来可以通过 Session Resumption 减少到一个 RTT

    +

    TLS 中使用了两种加密技术,分别为:对称加密非对称加密

    +

    对称加密:
    对称加密就是两边拥有相同的秘钥,两边都知道如何将密文加密解密。

    +

    非对称加密:
    有公钥私钥之分,公钥所有人都可以知道,可以将数据用公钥加密,但是将数据解密必须使用私钥解密,私钥只有分发公钥的一方才知道。

    +

    TLS 握手过程如下图:

    +
      +
    1. 客户端发送一个随机值,需要的协议和加密方式
    2. +
    3. 服务端收到客户端的随机值,自己也产生一个随机值,并根据客户端需求的协议和加密方式来使用对应的方式,发送自己的证书(如果需要验证客户端证书需要说明)
    4. +
    5. 客户端收到服务端的证书并验证是否有效,验证通过会再生成一个随机值,通过服务端证书的公钥去加密这个随机值并发送给服务端,如果服务端需要验证客户端证书的话会附带证书
    6. +
    7. 服务端收到加密过的随机值并使用私钥解密获得第三个随机值,这时候两端都拥有了三个随机值,可以通过这三个随机值按照之前约定的加密方式生成密钥,接下来的通信就可以通过该密钥来加密解密
    8. +
    +

    通过以上步骤可知,在 TLS 握手阶段,两端使用非对称加密的方式来通信,但是因为非对称加密损耗的性能比对称加密大,所以在正式传输数据时,两端使用对称加密的方式通信。

    +

    PS:以上说明的都是 TLS 1.2 协议的握手情况,在 1.3 协议中,首次建立连接只需要一个 RTT,后面恢复连接不需要 RTT 了。

    +

    HTTP 2.0

    HTTP 2.0 相比于 HTTP 1.X,可以说是大幅度提高了 web 的性能。

    +

    在 HTTP 1.X 中,为了性能考虑,我们会引入雪碧图、将小图内联、使用多个域名等等的方式。这一切都是因为浏览器限制了同一个域名下的请求数量,当页面中需要请求很多资源的时候,队头阻塞(Head of line blocking)会导致在达到最大请求数量时,剩余的资源需要等待其他资源请求完成后才能发起请求。

    +

    你可以通过 该链接 感受下 HTTP 2.0 比 HTTP 1.X 到底快了多少。

    +

    在 HTTP 1.X 中,因为队头阻塞的原因,你会发现请求是这样的
    lWJGkt.png
    在 HTTP 2.0 中,因为引入了多路复用,你会发现请求是这样的
    lWJa6g.png

    +

    二进制传输

    HTTP 2.0 中所有加强性能的核心点在于此。在之前的 HTTP 版本中,我们是通过文本的方式传输数据。在 HTTP 2.0 中引入了新的编码机制,所有传输的数据都会被分割,并采用二进制格式编码。

    +

    多路复用

    在 HTTP 2.0 中,有两个非常重要的概念,分别是帧(frame)流(stream)
    帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流。

    +

    多路复用,就是在一个 TCP 连接中可以存在多条流。换句话说,也就是可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能。

    +

    Header 压缩

    在 HTTP 1.X 中,我们使用文本的形式传输 header,在 header 携带 cookie 的情况下,可能每次都需要重复传输几百到几千的字节。

    +

    在 HTTP 2.0 中,使用了 HPACK 压缩格式对传输的 header 进行编码,减少了 header 的大小。并在两端维护了索引表,用于记录出现过的 header ,后面在传输过程中就可以传输已经记录过的 header 的键名,对端收到数据后就可以通过键名找到对应的值。

    +

    服务端 Push

    在 HTTP 2.0 中,服务端可以在客户端某个请求后,主动推送其他资源。

    +

    可以想象以下情况,某些资源客户端是一定会请求的,这时就可以采取服务端 push 的技术,提前给客户端推送必要的资源,这样就可以相对减少一点延迟时间。当然在浏览器兼容的情况下你也可以使用 prefetch 。

    +

    QUIC

    这是一个谷歌出品的基于 UDP 实现的同为传输层的协议,目标很远大,希望替代 TCP 协议。

    +
      +
    • 该协议支持多路复用,虽然 HTTP 2.0 也支持多路复用,但是下层仍是 TCP,因为 TCP 的重传机制,只要一个包丢失就得判断丢失包并且重传,导致发生队头阻塞的问题,但是 UDP 没有这个机制
    • +
    • 实现了自己的加密协议,通过类似 TCP 的 TFO 机制可以实现 0-RTT,当然 TLS 1.3 已经实现了 0-RTT 了
    • +
    • 支持重传和纠错机制(向前恢复),在只丢失一个包的情况下不需要重传,使用纠错机制恢复丢失的包
        +
      • 纠错机制:通过异或的方式,算出发出去的数据的异或值并单独发出一个包,服务端在发现有一个包丢失的情况下,通过其他数据包和异或值包算出丢失包
      • +
      • 在丢失两个包或以上的情况就使用重传机制,因为算不出来了
      • +
      +
    • +
    +

    DNS

    DNS 的作用就是通过域名查询到具体的 IP。

    +

    因为 IP 存在数字和英文的组合(IPv6),很不利于人类记忆,所以就出现了域名。你可以把域名看成是某个 IP 的别名,DNS 就是去查询这个别名的真正名称是什么。

    +

    在 TCP 握手之前就已经进行了 DNS 查询,这个查询是操作系统自己做的。当你在浏览器中想访问 www.google.com 时,会进行一下操作:

    +
      +
    1. 操作系统会首先在本地缓存中查询
    2. +
    3. 没有的话会去系统配置的 DNS 服务器中查询
    4. +
    5. 如果这时候还没得话,会直接去 DNS 根服务器查询,这一步查询会找出负责 com 这个一级域名的服务器
    6. +
    7. 然后去该服务器查询 google 这个二级域名
    8. +
    9. 接下来三级域名的查询其实是我们配置的,你可以给 www 这个域名配置一个 IP,然后还可以给别的三级域名配置一个 IP
    10. +
    +

    以上介绍的是 DNS 迭代查询,还有种是递归查询,区别就是前者是由客户端去做请求,后者是由系统配置的 DNS 服务器做请求,得到结果后将数据返回给客户端。

    +

    PS:DNS 是基于 UDP 做的查询。

    +

    从输入 URL 到页面加载完成的过程

    这是一个很经典的面试题,在这题中可以将本文讲得内容都串联起来。

    +
      +
    1. 首先做 DNS 查询,如果这一步做了智能 DNS 解析的话,会提供访问速度最快的 IP 地址回来
    2. +
    3. 接下来是 TCP 握手,应用层会下发数据给传输层,这里 TCP 协议会指明两端的端口号,然后下发给网络层。网络层中的 IP 协议会确定 IP 地址,并且指示了数据传输中如何跳转路由器。然后包会再被封装到数据链路层的数据帧结构中,最后就是物理层面的传输了
    4. +
    5. TCP 握手结束后会进行 TLS 握手,然后就开始正式的传输数据
    6. +
    7. 数据在进入服务端之前,可能还会先经过负责负载均衡的服务器,它的作用就是将请求合理的分发到多台服务器上,这时假设服务端会响应一个 HTML 文件
    8. +
    9. 首先浏览器会判断状态码是什么,如果是 200 那就继续解析,如果 400500 的话就会报错,如果 300 的话会进行重定向,这里会有个重定向计数器,避免过多次的重定向,超过次数也会报错
    10. +
    11. 浏览器开始解析文件,如果是 gzip 格式的话会先解压一下,然后通过文件的编码格式知道该如何去解码文件
    12. +
    13. 文件解码成功后会正式开始渲染流程,先会根据 HTML 构建 DOM 树,有 CSS 的话会去构建 CSSOM 树。如果遇到 script 标签的话,会判断是否存在 async 或者 defer ,前者会并行进行下载并执行 JS ,后者会先下载文件,然后等待 HTML 解析完成后顺序执行,如果以上都没有,就会阻塞住渲染流程直到 JS 执行完毕。遇到文件下载的会去下载文件,这里如果使用 HTTP 2.0 协议的话会极大的提高多图的下载效率。
    14. +
    15. 初始的 HTML 被完全加载和解析后会触发 DOMContentLoaded 事件
    16. +
    17. CSSOM 树和 DOM 树构建完成后会开始生成 Render 树,这一步就是确定页面元素的布局、样式等等诸多方面的东西
    18. +
    19. 在生成 Render 树的过程中,浏览器就开始调用 GPU 绘制,合成图层,将内容显示在屏幕上了
    20. +
    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/84/index.html b/page/84/index.html new file mode 100644 index 0000000000..c2d56c9c5d --- /dev/null +++ b/page/84/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    最近在做公司技术规范,需要能通过 B/S 方式进行分发,起初想法是做一个网站,用 html 的方式进行书写,但是后面感觉太麻烦了,所以就放弃了,偶然间看到有工具可以直接通过 md 文件编译生成网站,就像是 hexo 博客这样,我觉得这挺好,极大的节省了开发网站所需要的时间,只需要专注于内容就好了,所以我就研究了下来,整个了解过程中有这么两个工具映入了我的眼帘,一个是 doctify,另外一个就是今天的主角 vuepress

    + +
    + + 阅读全文 » + +
    + + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/85/index.html b/page/85/index.html new file mode 100644 index 0000000000..6390652736 --- /dev/null +++ b/page/85/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    定义

    柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

    +

    柯里化,可以理解为提前接收部分参数,延迟执行,不立即输出结果,而是返回一个接受剩余参数的函数。因为这样的特性,也被称为部分计算函数。

    +

    通俗易懂的解释:用闭包把参数保存起来,当参数的数量足够执行函数了,就开始执行函数。柯里化,是一个逐步接收参数的过程。在接下来的剖析中,你会深刻体会到这一点。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // js代码

    function currying(fn){
    var allArgs = [];

    return function next(){
    var args = [].slice.call(arguments); // 拆成数组元素

    if(args.length > 0){
    allArgs = allArgs.concat(args);
    return next;
    }else{
    return fn.apply(null, allArgs);
    }
    }
    }
    +

    我们来一个简单的实例验证一下:

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // js代码

    var add = currying(function(){
    var sum = 0;
    for(var i = 0; i < arguments.length; i++){
    sum += arguments[i];
    }
    return sum;
    });

    add(1)(2, 3)(4)() // => 10
    + +

    应用场景

    参数复用

    1
    2
    3
    4
    5
    6
    7
    8
    // js代码

    function getUrl(domain, protocol, path) {
    return protocol + "://" + domain + "/" + path;
    }

    var page1 = getUrl('http', 'lixuguang.github.io', 'page1.html');
    var page2 = getUrl('http', 'lixuguang.github.io', 'page2.html');
    +

    我们使用currying来简化它:

    +
    1
    2
    var conardliSite = currying(getUrl)
    var page1 = conardliSite('page1.html')('http', 'lixuguang.github.io')();
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/86/index.html b/page/86/index.html new file mode 100644 index 0000000000..db102b1d48 --- /dev/null +++ b/page/86/index.html @@ -0,0 +1,515 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    最近看了好多技术文章,好多的代码片段,突然发现好多的代码里都有 reduce ,感觉以前都没怎么关注过这个函数,为了弥补这个过失,打算专门写一篇文章说说它。

    +

    reduce 函数在 MDN 中是这样介绍的

    +
    +

    reduce() 方法对数组中的每个元素执行一个由 提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。

    +
    +

    说实话看了一脸懵逼,这上面说的叫人话?后来耐着性子看了一些代码后感觉有所理解,如果理解不对,还请斧正。
    首先看一下这里面几个关键词

    +

    * 每个元素: * 这就是遍历咯,没啥好说的
    提供的 reducer 函数:我哪有什么 reducer 函数呢,后来理解了,就是回调,有的地方呢叫 handler ,这里跟 reduce 配合就叫了 reducer
    升序执行:就是说是0,1,2下标这样的顺序执行啦。
    将其结果汇总为单个返回值:最后返回的是一个值,当然没说必须是什么类型。

    +

    这样解释完了以后,上面这句话就比较好懂了,简单说就是 reduce 里传一个回调函数,执行回调函数方法后返回一个值,就是这样。

    +
    +

    reducer 函数接收4个参数:

    +
    +
      +
    • Accumulator (acc) (累计器)
    • +
    • Current Value (cur) (当前值)
    • +
    • Current Index (idx) (当前索引)
    • +
    • Source Array (src) (源数组)
      您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
    • +
    +

    看上面的描述一定要注意,这些参数是 reducer 的参数,不是 reduce 的参数,一共有4个,通常用前两个情况比较多。

    +

    那么 reduce 函数呢实际上有两个参数,第二个还可以省略 reducerinitialValueinitialValue作为第一次调用 reducer 函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错,这点切记。

    +

    终于把前置知识讲清楚了,接下来,我们就用 reduce 搞点事情。

    +

    1. 使用 reduce 实现 map

    map 的话我不想再讲一遍,一句话带过,就是对回调函数中的元素进行加工后返回一个长度一样的新数组。
    那么该如何实现呢,我们来想想原理吧,刚才上面说过了,reduce执行过后的返回值会作为下一次执行的第一个参数放进去,那么就可以先用一个空数组作为初始值来接受每次需要处理的元素的集合,并返回回去,作为下一次传入的第一个参数,这样每次操作完之后就可以push到这个数组中,那么剩下要做的就是处理数组中每一项的函数了,我们叫他 handler

    +
    1
    2
    3
    4
    5
    6
    7
    8
    // js代码

    Array.prototype.reduceToMap = function (handler) { // 自定义 `map` 函数 `reduceToMap`
    return this.reduce((target, current, index) => { // this指向调用他的数组
    target.push(handler.call(this, current, index)) // 这里用了call方法,handler将接受两个参数 current和index
    return target; // 处理完成后返回新数组
    }, []) // 初始化空的新数组
    };
    +

    就这样我们就用 reduce,实现了 map 的功能,是不是很好用?

    +

    2. 使用 reduce 实现 filter

    filter 也是数组常用的方法,同样传入一个回调函数,处理结果返回true或false,最终 filter 会返回一个过滤后的函数。
    学会了上面的 map 的实现,实际上 filter 就会很简单

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    Array.prototype.reduceToFilter = function (handler) { // 还是自定义方法名
    return this.reduce((target, current, index) => {
    if (handler.call(this, current, index)) { // 这里注意 handler 要返回的是布尔类型的值
    target.push(current); // 符合条件就插入新数组
    } // 不符合就什么都不做
    return target; // 最后返回新数组
    }, []) // 初始化一个空数组
    };
    +

    日后在看到 reduce 的妙用之后还会来补充这篇文章,如果实在懒得写,我也会链接一下新文章,希望大家长期关注。

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/87/index.html b/page/87/index.html new file mode 100644 index 0000000000..f366fb57d8 --- /dev/null +++ b/page/87/index.html @@ -0,0 +1,528 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    去重

    1. 利用 ObjectKey 唯一特性

    开辟一个外部存储空间用于标示元素是否出现过。

    +
    1
    2
    3
    4
    5
    6
    // js代码

    const unique = (array)=> {
    var container = {};
    return array.filter((item, index) => container.hasOwnProperty(item) ? false : (container[item] = true));
    }
    + +

    2. 利用 indexOf 的返回值数值进行去重

    原理是 indexOf 获取元素时如果返回值不等于下标说明已经有了,配合 filter 更美味

    +
    1
    2
    3
    4
    5
    // js代码

    const unique = arr => arr.filter((e,i) =>
    arr.indexOf(e) === i // 如果元素找到的当前下标和当前索引相同说明是同一个,不同说明不是唯一
    );
    + +

    还有一种变形方法利用 lastIndexOf 方法

    +
    +

    lastIndexOf() 方法返回指定元素(也即有效的 JavaScript 值或变量)在数组中的最后一个的索引,如果不存在则返回 -1。从数组的后面向前查找,从 fromIndex 处开始。

    +
    +
    1
    2
    3
    4
    5
    // js代码

    const filterNonUnique = arr => arr.filter(e =>
    arr.indexOf(e) === arr.lastIndexOf(e) // 判断一个元素出现时的第一次下标和最后一次下标是否相同,如果相同那么就唯一
    )
    +

    3. 利用 Set 特性去重

    SetES6 中新的数据类型,它的特点就是元素唯一性,且可以和数组进行转换

    +
    1
    2
    3
    4
    5
    6
    7
    // js代码

    const unique = arr => Array.from(new Set(arr)); // Array.from 方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。

    // 优化

    const unique = arr => [...new Set(arr)]; // 利用 `ES6` 中展开操作
    +

    4. 排序后判断前后两项是否相等去重

    通过比较相邻数字是否重复,将排序后的数组进行去重。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // js代码

    const unique = (array) => {
    array.sort((a, b) => a - b);
    let pre = 0;
    const result = [];
    for (let i = 0; i < array.length; i++) {
    if (!i || array[i] != array[pre]) {
    result.push(array[i]);
    }
    pre = i;
    }
    return result;
    }
    + +

    扁平

    1. 普通方法

    通过递归的方式判断数组中的项是否是数组,如果不是就加入到新的扁平数组,如果是就递归调用逐层判断,直到全部结束

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // js代码

    const flatten = (array) => { // array 原数组
    let result = []; // 定义新的扁平数组
    for (let i = 0; i < array.length; i++) {
    if (Array.isArray(array[i])) { // 判断子元素是否是数组
    result = result.concat(flatten(array[i])); // 递归判断
    } else {
    result.push(array[i]); // 加入新数组
    }
    }
    return result;
    }
    + +

    2. 使用reduce简化上述方法

    +

    reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
    reducer 函数接收4个参数:

    +
    +
      +
    • Accumulator (acc) (累计器)
    • +
    • Current Value (cur) (当前值)
    • +
    • Current Index (idx) (当前索引)
    • +
    • Source Array (src) (源数组)
    • +
    • 您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
    • +
    +

    先看一段 reduce 的示例函数

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // js代码

    const array1 = [1, 2, 3, 4];
    const reducer = (accumulator, currentValue) => accumulator + currentValue; // 定义一个累计器函数,作用是将数组前后累计值与当前值累加

    // 1 + 2 + 3 + 4
    console.log(array1.reduce(reducer)); // 没有初始值用第一个元素
    // expected output: 10

    // 5 + 1 + 2 + 3 + 4
    console.log(array1.reduce(reducer, 5)); // 有初始值从初始值开始
    // expected output: 15
    +

    这下大家应该对 reduce 函数认识了,接下来看看怎么简化

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // js代码

    function flatten(array) {
    return array.reduce((newArray, current) => // 新数组,当前项
    Array.isArray(current) ? // 判断当前项是否为数组
    newArray.concat(flatten(current)) : // 是的话 递归调用
    newArray.concat(current) // 不是的话加进新数组
    , []) // 初始化新数组为空
    }
    +

    这里我们再变一个形,增加一个变量,变成可指定深度操作数组

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    function flattenByDeep(array, deep = 1) { // 默认一层
    return array.reduce(
    (target, current) =>
    Array.isArray(current) && deep > 1 ?
    target.concat(flattenByDeep(current, deep - 1)) : // 下一次减一层
    target.concat(current)
    , [])
    }
    +

    最值

    利用 reduce

    reduce 函数真的是超级好用,

    +
    1
    2
    3
    // js代码

    array.reduce((c,n) => Math.max(c,n))
    + +

    Math.max

    Math.max 参数原本是一组数字,只需要让他可以接收数组即可。

    +
    1
    2
    3
    4
    5
    // js代码

    const array = [3,2,1,4,5];
    Math.max.apply(null,array);
    Math.max(...array);
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/88/index.html b/page/88/index.html new file mode 100644 index 0000000000..625d12fc0c --- /dev/null +++ b/page/88/index.html @@ -0,0 +1,634 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    基本概念

    时间复杂度

    一个算法的时间复杂度反映了程序运行从开始到结束所需要的时间。把算法中基本操作重复执行的次数(频度)作为算法的时间复杂度。
    没有循环语句,记作O(1),也称为常数阶。只有一重循环,则算法的基本操作的执行频度与问题规模n呈线性增大关系,记作O(n),也叫线性阶。
    常见的时间复杂度有:

    +
      +
    • O(1): Constant Complexity: Constant 常数复杂度
    • +
    • O(log n): Logarithmic Complexity: 对数复杂度
    • +
    • O(n): Linear Complexity: 线性时间复杂度
    • +
    • O(n^2): N square Complexity 平⽅方
    • +
    • O(n^3): N square Complexity ⽴立⽅方
    • +
    • O(2^n): Exponential Growth 指数
    • +
    • O(n!): Factorial 阶乘
    • +
    +

    空间复杂度

    一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。

    +

    一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。

    +
      +
    • 稳定
    • +
    • 不稳定

      算法汇总

      十大经典排序.jpg

      关于时间复杂度:

      平方阶 (O(n2)) 排序 各类简单排序:直接插入、直接选择和冒泡排序。
      线性对数阶 (O(nlog2n)) 排序 快速排序、堆排序和归并排序;
      O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数。 希尔排序
      线性阶 (O(n)) 排序 基数排序,此外还有桶、箱排序。
    • +
    +

    关于稳定性:

    稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
    不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。

    +

    名词解释:

      +
    • n:数据规模
    • +
    • k:“桶”的个数
    • +
    • In-place:占用常数内存,不占用额外内存
    • +
    • Out-place:占用额外内存
    • +
    • 稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同
    • +
    +

    1. 冒泡排序(Bubble Sort) — 前后两两比较 — 气泡

    冒泡排序可谓是最经典的排序算法了,它是基于比较的排序算法,其优点是实现简单,排序数量较小时性能较好。

    +

    它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

    +

    1. 1 算法原理

    相邻的数据进行两两比较,小数放在前面,大数放在后面,如果前面的数据比后面的数据大,就交换这两个数的位置。也可以实现大数放在前面,小数放在后面,如果前面的数据比后面的小,就交换两个的位置。要实现上述规则需要用到两层for循环。

    +

    1. 2 算法描述

      +
    1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
    2. +
    3. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
    4. +
    5. 针对所有的元素重复以上的步骤,除了最后一个;
    6. +
    7. 重复步骤1~3,直到排序完成。
    8. +
    +

    1. 3 动图演示

    ldB5VS.gif

    +

    1. 4 js代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // js代码

    function bubbleSort(arr) {
    var len = arr.length;
    for (var i = 0; i < len; i++) {
    for (var j = 0; j < len - 1 - i; j++) {
    if (arr[j] > arr[j + 1]) { // 相邻元素两两对比
    // 元素交换
    /** 1.使用中间变量 **/
    var temp = arr[j + 1];
    arr[j + 1] = arr[j]
    arr[j] = temp
    /** 2.适用纯数字的数组排序 **/
    arr[j] = arr[j] + arr[j + 1]
    arr[j + 1] = arr[j] - arr[j + 1]
    arr[j] -= arr[j + 1]
    /** 3.使用es6解构赋值 **/
    [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
    }
    }
    }
    return arr;
    }
    +

    冒泡排序算法优化

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // js代码

    function bubbleSort(arr) {
    var len = arr.length;
    for (var i = 0; i < len; i++) {
    var exchange=false; // 交换标志
    for (var j = 0; j < len - 1 - i; j++) {
    if (arr[j] > arr[j + 1]) { // 相邻元素两两对比
    [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]] // 元素交换
    exchange=true; //
    }
    }
    if(!exchange){ // 若本趟排序未发生交换,提前终止算法
    break;
    }
    }
    return arr;
    }
    +

    2. 选择排序(Selection Sort) — 面试挑简历,在剩下的里面挑最好的

    表现最稳定的排序算法之一,因为无论什么数据进去都是O(n²)的时间复杂度。。。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。

    +

    2. 1 算法原理

    先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

    +

    2. 2 算法描述

    n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

    +
      +
    1. 初始状态:无序区为R[1..n],有序区为空;
    2. +
    3. 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中
    4. +
    5. 选出关键字最小的记录R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
    6. +
    7. n-1趟结束,数组有序化了。
    8. +
    +

    2. 3 动图演示

    ldDWW9.gif

    +

    2. 4 js代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // js代码

    function selectionSort(arr) {
    var len = arr.length;
    var minIndex, temp;
    for (var i = 0; i < len - 1; i++) {
    minIndex = i;
    for (var j = i + 1; j < len; j++) {
    if (arr[j] < arr[minIndex]) { //寻找最小的数
    minIndex = j; //将最小数的索引保存
    }
    }
    temp = arr[i];
    arr[i] = arr[minIndex];
    arr[minIndex] = temp;
    }
    return arr;
    }
    + +

    3. 插入排序(Insertion Sort)—– 麻将/扑克

    插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。

    +

    3. 1 算法原理

    它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

    +

    3. 2 算法描述

    一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

    +
      +
    1. 从第一个元素开始,该元素可以认为已经被排序;
    2. +
    3. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
    4. +
    5. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
    6. +
    7. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
    8. +
    9. 将新元素插入到该位置后;
    10. +
    11. 重复步骤2~5。
    12. +
    +

    3. 3 动图演示

    ldDfzR.gif

    +

    3. 4 js代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // js代码

    function insertSort(arr) {
    // 从1位置开始遍历arr中每元素,同时声明空变量temp
    for (let i = 1; i < arr.length; i++) {
    if (arr[i] < arr[i - 1]) { // 如果当前元素<前一个元素
    let temp = arr[i] // 将当前元素值临时保存在temp中
    let p = i - 1 // 定义变量 p = i- 1
    // 循环 条件:
    // 1. p>=0且temp小于p位置的元素
    while (p >= 0 && temp < arr[p]) {
    // 循环体: 将P位置的值赋值给p的后一个元素
    arr[p + 1] = arr[p]
    p-- // p向前移动一个
    }
    arr[p + 1] = temp // 将temp的值赋值给p+1位置的元素
    }
    }
    }
    + +

    4. 快速排序(Selection Sort)

    快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

    +

    4. 1 算法原理

    快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。

    +

    4. 2 算法描述

    选基准:在数据结构中选择一个元素作为基准(pivot
    划分区:参照基准元素值的大小,划分无序区,所有小于基准元素的数据放入一个区间,所有大于基准元素的数据放入另一区间,分区操作结束后,基准元素所处的位置就是最终排序后它应该所处的位置
    递归:对初次划分出来的两个无序区间,递归调用第 1步和第 2步的算法,直到所有无序区间都只剩下一个元素为止。

    +

    简单理解就是,选择一个目标值,比目标值小的放左边,比目标值大的放右边,目标值的位置已排好,将左右两侧再进行快排。

    +

    4. 3 动图演示

    快速排序.gif

    +

    4. 4 js代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // js代码

    function quickSort(arr){
    //如果arr.length<=1,则直接返回arr
    if(arr.length<=1){return arr}
    // arr的元素个数/2,再下去整,将值保存在pivotIndex中
    var pivotIndex=Math.floor(arr.length/2);
    // 将arr中pivotIndex位置的元素,保存在变量pivot中
    var pivot=arr[pivotIndex];
    //声明空数组left和right
    var left=[];
    var right=[];
    for(var i=0;i<arr.length;i++){ // 遍历arr中每个元素
    if(i !== pivotIndex){ // 如果i !== pivotIndex
    if(arr[i]<=pivot){ // 如果当前元素值<pivot
    left.push(arr[i]); // 就将当前值压入left
    }else{
    right.push(arr[i]); // 就将当前值压入right
    }
    }
    }
    //递归
    return quickSort(left).concat(pivot, quickSort(right)); // 链接多个数组到 left 从小到大
    }
    +

    5. 希尔排序

    5. 1 算法原理

    希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
    希尔排序是基于插入排序的以下两点性质而提出改进方法的:

    +
      +
    • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
    • +
    • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
      希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
    • +
    +

    5. 2 算法描述

      +
    • 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
    • +
    • 按增量序列个数 k,对序列进行 k 趟排序;
    • +
    • 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
    • +
    +

    5.3 js代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // js代码

    function shellSort(arr) {
    var len = arr.length,
    temp,
    gap = 1;
    while(gap < len/3) { //动态定义间隔序列
    gap =gap*3+1;
    }
    for (gap; gap > 0; gap = Math.floor(gap/3)) {
    for (var i = gap; i < len; i++) {
    temp = arr[i];
    for (var j = i-gap; j >= 0 && arr[j] > temp; j-=gap) {
    arr[j+gap] = arr[j];
    }
    arr[j+gap] = temp;
    }
    }
    return arr;
    }
    + +

    6. 归并排序

    6. 1 算法原理

    归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
    作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

    +
      +
    • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
    • +
    • 自下而上的迭代;
      +

      在《数据结构与算法 JavaScript 描述》中,作者给出了自下而上的迭代方法。但是对于递归法,作者却认为:
      However, it is not possible to do so in JavaScript, as the recursion goes too deep for the language to handle.
      然而,在 JavaScript 中这种方式不太可行,因为这个算法的递归深度对它来讲太深了。

      +
      +
    • +
    +

    说实话,我不太理解这句话。意思是 JavaScript 编译器内存太小,递归太深容易造成内存溢出吗?还望有大神能够指教。

    +

    和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。

    +

    6. 2 算法描述

      +
    1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
    2. +
    3. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
    4. +
    5. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
    6. +
    7. 重复步骤 3 直到某一指针达到序列尾;
    8. +
    9. 将另一序列剩下的所有元素直接复制到合并序列尾。
    10. +
    +

    6. 3 动图演示

    归并排序.gif

    +

    6. 4 js代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    // js代码

    function mergeSort(arr) { // 采用自上而下的递归方法
    var len = arr.length;
    if(len < 2) {
    return arr;
    }
    var middle = Math.floor(len / 2),
    left = arr.slice(0, middle),
    right = arr.slice(middle);
    return merge(mergeSort(left), mergeSort(right));
    }

    function merge(left, right){
    var result = [];
    while (left.length && right.length) {
    if (left[0] <= right[0]) {
    result.push(left.shift());
    } else {
    result.push(right.shift());
    }
    }

    while (left.length)
    result.push(left.shift());

    while (right.length)
    result.push(right.shift());

    return result;
    }
    + +

    7. 堆排序

    7. 1 算法原理

    堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

    +
      +
    • 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
    • +
    • 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
      堆排序的平均时间复杂度为 Ο(nlogn)。
    • +
    +

    7. 2 算法描述

      +
    1. 将待排序序列构建成一个堆 H[0……n-1],根据(升序降序需求)选择大顶堆或小顶堆;
    2. +
    3. 把堆首(最大值)和堆尾互换;
    4. +
    5. 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
    6. +
    7. 重复步骤 2,直到堆的尺寸为 1。
    8. +
    +

    7. 3 动图演示

    堆排序.gif

    +

    7. 4 js代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    // js代码

    var len; // 因为声明的多个函数都需要数据长度,所以把len设置成为全局变量

    function buildMaxHeap(arr) { // 建立大顶堆
    len = arr.length;
    for (var i = Math.floor(len/2); i >= 0; i--) {
    heapify(arr, i);
    }
    }

    function heapify(arr, i) { // 堆调整
    var left = 2 * i + 1,
    right = 2 * i + 2,
    largest = i;

    if (left < len && arr[left] > arr[largest]) {
    largest = left;
    }

    if (right < len && arr[right] > arr[largest]) {
    largest = right;
    }

    if (largest != i) {
    swap(arr, i, largest);
    heapify(arr, largest);
    }
    }

    function swap(arr, i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
    }

    function heapSort(arr) {
    buildMaxHeap(arr);

    for (var i = arr.length-1; i > 0; i--) {
    swap(arr, 0, i);
    len--;
    heapify(arr, 0);
    }
    return arr;
    }
    + +

    8. 计数排序

    8. 1 算法原理

    计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

    +

    8. 2 算法描述

    8. 3 动图演示

    计数排序.gif

    +

    8. 4 js代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // js代码

    function countingSort(arr, maxValue) {
    var bucket = new Array(maxValue+1),
    sortedIndex = 0;
    arrLen = arr.length,
    bucketLen = maxValue + 1;

    for (var i = 0; i < arrLen; i++) {
    if (!bucket[arr[i]]) {
    bucket[arr[i]] = 0;
    }
    bucket[arr[i]]++;
    }

    for (var j = 0; j < bucketLen; j++) {
    while(bucket[j] > 0) {
    arr[sortedIndex++] = j;
    bucket[j]--;
    }
    }

    return arr;
    }
    + +

    9. 桶排序

    9. 1 算法原理

    桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

    +
      +
    • 在额外空间充足的情况下,尽量增大桶的数量
    • +
    • 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
      同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

      9. 2 算法描述

      1. 什么时候最快

      当输入的数据可以均匀的分配到每一个桶中。

      2. 什么时候最慢

      当输入的数据被分配到了同一个桶中。

      9. 3 动图演示

    • +
    +

    9. 4 js代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    // js代码

    function bucketSort(arr, bucketSize) {
    if (arr.length === 0) {
    return arr;
    }

    var i;
    var minValue = arr[0];
    var maxValue = arr[0];
    for (i = 1; i < arr.length; i++) {
    if (arr[i] < minValue) {
    minValue = arr[i]; // 输入数据的最小值
    } else if (arr[i] > maxValue) {
    maxValue = arr[i]; // 输入数据的最大值
    }
    }

    //桶的初始化
    var DEFAULT_BUCKET_SIZE = 5; // 设置桶的默认数量为5
    bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
    var bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
    var buckets = new Array(bucketCount);
    for (i = 0; i < buckets.length; i++) {
    buckets[i] = [];
    }

    //利用映射函数将数据分配到各个桶中
    for (i = 0; i < arr.length; i++) {
    buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i]);
    }

    arr.length = 0;
    for (i = 0; i < buckets.length; i++) {
    insertionSort(buckets[i]); // 对每个桶进行排序,这里使用了插入排序
    for (var j = 0; j < buckets[i].length; j++) {
    arr.push(buckets[i][j]);
    }
    }

    return arr;
    }
    + +

    10. 基数排序

    10. 1 算法原理

    基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

    +

    10. 2 算法描述

    1. 基数排序 vs 计数排序 vs 桶排序

    基数排序有三种方法:
    这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异案例看大家发的:

    +
      +
    • 基数排序:根据键值的每位数字来分配桶;
    • +
    • 计数排序:每个桶只存储单一键值;
    • +
    • 桶排序:每个桶存储一定范围的数值;

      10. 3 动图演示

    • +
    +
      +
    1. LSD 基数排序动图演示
      基数排序.gif

      10. 4 js代码实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      // js代码

      // LSD Radix Sort
      var counter = [];
      function radixSort(arr, maxDigit) {
      var mod = 10;
      var dev = 1;
      for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
      for(var j = 0; j < arr.length; j++) {
      var bucket = parseInt((arr[j] % mod) / dev);
      if(counter[bucket]==null) {
      counter[bucket] = [];
      }
      counter[bucket].push(arr[j]);
      }
      var pos = 0;
      for(var j = 0; j < counter.length; j++) {
      var value = null;
      if(counter[j]!=null) {
      while ((value = counter[j].shift()) != null) {
      arr[pos++] = value;
      }
      }
      }
      }
      return arr;
      }
    2. +
    +

    总结

    排序算法.png
    以上就是十大经典算法,算法对于前端来说并不是一个十分熟悉的领域,但是排序算法算是算法里比较入门的,还是需要掌握的,毕竟即使是为了面试也是要准备的。

    +

    参考资料

    https://github.com/hustcc/JS-Sorting-Algorithm
    一本关于排序算法的 GitBook 在线书籍 《十大经典排序算法》,多语言实现。

    +

    http://www.sohu.com/a/136157205_671058
    技术面试宝典: 很全面的算法和数据结构知识(含代码实现)下篇

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/89/index.html b/page/89/index.html new file mode 100644 index 0000000000..fdbc4644fe --- /dev/null +++ b/page/89/index.html @@ -0,0 +1,541 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    cookie,localStorage,sessionStorage,indexDB

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    特性cookielocalStoragesessionStorageindexDB
    数据生命周期一般由服务器生成,可以设置过期时间除非被清理,否则一直存在页面关闭就清理除非被清理,否则一直存在
    数据存储大小4K5M5M无限制
    与服务端通信每次都会携带在 header 中,对于请求性能影响不参与不参与不参与
    +

    从上表可以看到, cookie 已经不建议用于存储。如果没有大量数据存储需求的话,可以使用 localStoragesessionStorage 。对于不怎么改变的数据尽量使用 localStorage 存储,否则可以用 sessionStorage 存储。

    +

    对于 cookie ,我们还需要注意安全性。
    | 属性 | 作用 |
    | ——— | ————————————————————– |
    | value | 如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识 |
    | http-only | 不能通过 JS 访问 Cookie,减少 XSS 攻击 |
    | secure | 只能在协议为 HTTPS 的请求中携带 |
    | same-site | 规定浏览器不能在跨域请求中携带 Cookie,减少 CSRF 攻击 |

    +

    Service Worker

    +

    Service workers 本质上充当 Web 应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步 API。

    +
    +

    目前该技术通常用来做缓存文件,提高首屏速度,可以试着来实现这个功能。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    // js代码

    // index.js
    if (navigator.serviceWorker) {
    navigator.serviceWorker
    .register('sw.js')
    .then(function(registration) {
    console.log('service worker 注册成功')
    })
    .catch(function(err) {
    console.log('servcie worker 注册失败')
    })
    }
    // sw.js
    // 监听 `install` 事件,回调中缓存所需文件
    self.addEventListener('install', e => {
    e.waitUntil(
    caches.open('my-cache').then(function(cache) {
    return cache.addAll(['./index.html', './index.js'])
    })
    )
    })

    // 拦截所有请求事件
    // 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据
    self.addEventListener('fetch', e => {
    e.respondWith(
    caches.match(e.request).then(function(response) {
    if (response) {
    return response
    }
    console.log('fetch source')
    })
    )
    })
    + +

    打开页面,可以在开发者工具中的 Application 看到 Service Worker 已经启动了
    Cache 中也可以发现我们所需的文件已被缓存

    +

    当我们重新刷新页面可以发现我们缓存的数据是从 Service Worker 中读取的

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/9/index.html b/page/9/index.html new file mode 100644 index 0000000000..22a7cafdbe --- /dev/null +++ b/page/9/index.html @@ -0,0 +1,514 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    systemctl 命令

    systemctl

    范列出系统上面有启动的unit

    +

    systemctl list-unit-files

    列出所有已经安装的unit有哪些

    +

    systemctl list-units –type=service –all

    列出类型为service的所有项目,不论启动与否

    +

    systemctl get-default

    输入目前机器默认的模式,如图形界面模式或者文本模式

    +

    systemctl isolate multi-user.target

    将目前的操作环境改为纯文本模式,关掉图形界面

    +

    systemctl isolate graphical.target

    将目前的操作环境改为图形界面

    +

    systemctl poweroff

    系统关机

    +

    systemctl reboot

    重新开机

    +

    systemctl suspend

    进入暂停模式

    +

    systemctl rescue

    强制进入救援模式

    +

    systemctl hibernate

    进入休眠模式

    +

    systemctl emergency

    强制进入紧急救援模式

    +

    systemctl list-dependencies –reverse

    查询当前默认的target关联了啥

    +

    systemctl list-dependencies graphical.target

    查询图形界面模式的target关联了啥

    +

    systemctl list-sockets

    查看当前的socket服务

    +

    systemctl show etcd.service

    查看 unit 的详细配置情况

    +

    systemctl mask etcd.service

    禁用某个服务

    +

    systemctl unmask etcd.service

    解除禁用某个服务

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/90/index.html b/page/90/index.html new file mode 100644 index 0000000000..d4745862d1 --- /dev/null +++ b/page/90/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    Proxy

    因为 JS 采用 IEEE 754 双精度版本(64位),并且只要采用 IEEE 754 的语言都有该问题。

    +

    我们都知道计算机表示十进制是采用二进制表示的,所以 0.1 在二进制表示为

    +
    1
    2
    3
    4
    // js代码

    // (0011) 表示循环
    0.1 = 2^-4 * 1.10011(0011)
    +

    那么如何得到这个二进制的呢,我们可以来演算下

    +

    小数算二进制和整数不同。乘法计算时,只计算小数位,整数位用作每一位的二进制,并且得到的第一位为最高位。所以我们得出 0.1 = 2^-4 * 1.10011(0011),那么 0.2 的演算也基本如上所示,只需要去掉第一步乘法,所以得出 0.2 = 2^-3 * 1.10011(0011)

    +

    回来继续说 IEEE 754 双精度。六十四位中符号位占一位,整数位占十一位,其余五十二位都为小数位。因为 0.10.2 都是无限循环的二进制了,所以在小数位末尾处需要判断是否进位(就和十进制的四舍五入一样)。

    +

    所以 2^-4 * 1.10011...001 进位后就变成了 2^-4 * 1.10011(0011 * 12次)010 。那么把这两个二进制加起来会得出 2^-2 * 1.0011(0011 * 11次)0100 , 这个值算成十进制就是 0.30000000000000004

    +

    下面说一下原生解决办法,如下代码所示

    +
    1
    2
    3
    // js代码

    parseFloat((0.1 + 0.2).toFixed(10))
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/91/index.html b/page/91/index.html new file mode 100644 index 0000000000..ab60c81111 --- /dev/null +++ b/page/91/index.html @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    Proxy

    ProxyES6 中新增的功能,可以用来自定义对象中的操作

    +
    1
    2
    3
    4
    5
    // js代码

    let p = new Proxy(target, handler);
    // `target` 代表需要添加代理的对象
    // `handler` 用来自定义对象中的操作
    +

    可以很方便的使用 Proxy 来实现一个数据绑定和监听

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    let onWatch = (obj, setBind, getLogger) => {
    let handler = {
    get(target, property, receiver) {
    getLogger(target, property)
    return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
    setBind(value);
    return Reflect.set(target, property, value);
    }
    };
    return new Proxy(obj, handler);
    };

    let obj = { a: 1 }
    let value
    let p = onWatch(obj, (v) => {
    value = v
    }, (target, property) => {
    console.log(`Get '${property}' = ${target[property]}`);
    })
    p.a = 2 // bind `value` to `2`
    p.a // -> Get 'a' = 2
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/92/index.html b/page/92/index.html new file mode 100644 index 0000000000..b0ecee174a --- /dev/null +++ b/page/92/index.html @@ -0,0 +1,516 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    async 和 await

    一个函数如果加上 async ,那么该函数就会返回一个 Promise

    +
    1
    2
    3
    4
    async function test() {
    return "1";
    }
    console.log(test()); // -> Promise {<resolved>: "1"}
    +

    可以把 async 看成将函数返回值使用 Promise.resolve() 包裹了下。
    await 只能在 async 函数中使用

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function sleep() {
    return new Promise(resolve => {
    setTimeout(() => {
    console.log('finish')
    resolve("sleep");
    }, 2000);
    });
    }
    async function test() {
    let value = await sleep();
    console.log("object");
    }
    test()
    + +

    上面代码会先打印 finish 然后再打印 object 。因为 await 会等待 sleep 函数 resolve ,所以即使后面是同步代码,也不会先去执行同步代码再来执行异步代码。

    +

    asyncawait 相比直接使用 Promise 来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码。缺点在于滥用 await 可能会导致性能问题,因为 await 会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。

    +

    下面来看一个使用 await 的代码。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var a = 0
    var b = async () => {
    a = a + await 10
    console.log('2', a) // -> '2' 10
    a = (await 10) + a
    console.log('3', a) // -> '3' 20
    }
    b()
    a++
    console.log('1', a) // -> '1' 1
    + +

    对于以上代码你可能会有疑惑,这里说明下原理

    +
      +
    • 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为在 await 内部实现了 generatorsgenerators 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
    • +
    • 因为 await 是异步操作,遇到 await 就会立即返回一个 pending 状态的 Promise 对象,暂时返回执行代码的控制权,使得函数外的代码得以继续执行,所以会先执行 console.log('1', a)
    • +
    • 这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 10
    • +
    • 然后后面就是常规执行代码了
    • +
    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/93/index.html b/page/93/index.html new file mode 100644 index 0000000000..f7ce26cdcf --- /dev/null +++ b/page/93/index.html @@ -0,0 +1,515 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    Map、FlatMap 和 Reduce

    Map

    Map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后 append 到新的数组中。

    +
    1
    2
    3
    // js代码

    [1, 2, 3].map((v) => v + 1) // -> [2, 3, 4]
    +

    Map 有三个参数,分别是当前索引元素索引原数组

    +

    FlatMap

    FlatMapmap 的作用几乎是相同的,但是对于多维数组来说,会将原数组降维。可以将 FlatMap 看成是 map + flatten ,目前该函数在浏览器中还不支持。

    +
    1
    2
    3
    // js代码

    [1, [2], 3].flatMap((v) => v + 1) // -> [2, 3, 4]
    +

    如果想将一个多维数组彻底的降维,可以这样实现

    +
    1
    2
    3
    4
    5
    const flattenDeep = (arr) => Array.isArray(arr)
    ? arr.reduce( (a, b) => [...a, ...flattenDeep(b)] , [])
    : [arr]

    flattenDeep([1, [[2], [3, [4]], 5]])
    + +

    Reduce 升序执行

    Reduce 作用是数组中的值组合起来,最终得到一个值
    reduce() 方法对数组中的每个元素执行一个由您提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。

    +

    reducer 函数接收4个参数:

    +
      +
    • Accumulator (acc) (累计器)
    • +
    • Current Value (cur) (当前值)
    • +
    • Current Index (idx) (当前索引)
    • +
    • Source Array (src) (源数组)
      您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
    • +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // js代码

    function a() {
    console.log(1);
    }

    function b() {
    console.log(2);
    }

    [a, b].reduce((a, b) => a(b()))
    // -> 2 1
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/94/index.html b/page/94/index.html new file mode 100644 index 0000000000..0decd4c4b9 --- /dev/null +++ b/page/94/index.html @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    Generator 实现

    GeneratorES6 中新增的语法,和 Promise 一样,都可以用来异步编程

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // js代码

    // 使用 * 表示这是一个 Generator 函数
    // 内部可以通过 yield 暂停代码
    // 通过调用 next 恢复执行
    function* test() {
    let a = 1 + 2;
    yield 2;
    yield 3;
    }
    let b = test();
    console.log(b.next()); // > { value: 2, done: false }
    console.log(b.next()); // > { value: 3, done: false }
    console.log(b.next()); // > { value: undefined, done: true }
    +

    从以上代码可以发现,加上 * 的函数执行后拥有了 next 函数,也就是说函数执行后返回了一个对象。每次调用 next 函数可以继续执行被暂停的代码。以下是 Generator 函数的简单实现

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    // js代码

    // cb 也就是编译过的 test 函数
    function generator(cb) {
    return (function() {
    var object = {
    next: 0,
    stop: function() {}
    };

    return {
    next: function() {
    var ret = cb(object);
    if (ret === undefined) return { value: undefined, done: true };
    return {
    value: ret,
    done: false
    };
    }
    };
    })();
    }
    // 如果你使用 babel 编译后可以发现 test 函数变成了这样
    function test() {
    var a;
    return generator(function(_context) {
    while (1) {
    switch ((_context.prev = _context.next)) {
    // 可以发现通过 yield 将代码分割成几块
    // 每次执行 next 函数就执行一块代码
    // 并且表明下次需要执行哪块代码
    case 0:
    a = 1 + 2;
    _context.next = 4;
    return 2;
    case 4:
    _context.next = 6;
    return 3;
    // 执行完毕
    case 6:
    case "end":
    return _context.stop();
    }
    }
    });
    }
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/95/index.html b/page/95/index.html new file mode 100644 index 0000000000..81fb708bc8 --- /dev/null +++ b/page/95/index.html @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    Promise 实现

    PromiseES6 新增的语法,解决了回调地狱的问题。

    +

    可以把 Promise 看成一个状态机。初始是 pending 状态,可以通过函数 resolvereject ,将状态转变为 resolved 或者 rejected 状态,状态一旦改变就不能再次变化

    +

    then 函数会返回一个 Promise 实例,并且该返回值是一个新的实例而不是之前的实例。因为 Promise 规范规定除了 pending 状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个 then 调用就失去意义了。

    +

    对于 then 来说,本质上可以把它看成是 flatMap

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    // js代码

    // 三种状态
    const PENDING = "pending";
    const RESOLVED = "resolved";
    const REJECTED = "rejected";
    // promise 接收一个函数参数,该函数会立即执行
    function MyPromise(fn) {
    let _this = this;
    _this.currentState = PENDING;
    _this.value = undefined;
    // 用于保存 then 中的回调,只有当 promise
    // 状态为 pending 时才会缓存,并且每个实例至多缓存一个
    _this.resolvedCallbacks = [];
    _this.rejectedCallbacks = [];

    _this.resolve = function (value) {
    if (value instanceof MyPromise) {
    // 如果 value 是个 Promise,递归执行
    return value.then(_this.resolve, _this.reject)
    }
    setTimeout(() => { // 异步执行,保证执行顺序
    if (_this.currentState === PENDING) {
    _this.currentState = RESOLVED;
    _this.value = value;
    _this.resolvedCallbacks.forEach(cb => cb());
    }
    })
    };

    _this.reject = function (reason) {
    setTimeout(() => { // 异步执行,保证执行顺序
    if (_this.currentState === PENDING) {
    _this.currentState = REJECTED;
    _this.value = reason;
    _this.rejectedCallbacks.forEach(cb => cb());
    }
    })
    }
    // 用于解决以下问题
    // new Promise(() => throw Error('error))
    try {
    fn(_this.resolve, _this.reject);
    } catch (e) {
    _this.reject(e);
    }
    }

    MyPromise.prototype.then = function (onResolved, onRejected) {
    var self = this;
    // 规范 2.2.7,then 必须返回一个新的 promise
    var promise2;
    // 规范 2.2.onResolved 和 onRejected 都为可选参数
    // 如果类型不是函数需要忽略,同时也实现了透传
    // Promise.resolve(4).then().then((value) => console.log(value))
    onResolved = typeof onResolved === 'function' ? onResolved : v => v;
    onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;

    if (self.currentState === RESOLVED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
    // 规范 2.2.4,保证 onFulfilled,onRjected 异步执行
    // 所以用了 setTimeout 包裹下
    setTimeout(function () {
    try {
    var x = onResolved(self.value);
    resolutionProcedure(promise2, x, resolve, reject);
    } catch (reason) {
    reject(reason);
    }
    });
    }));
    }

    if (self.currentState === REJECTED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
    setTimeout(function () {
    // 异步执行onRejected
    try {
    var x = onRejected(self.value);
    resolutionProcedure(promise2, x, resolve, reject);
    } catch (reason) {
    reject(reason);
    }
    });
    }));
    }

    if (self.currentState === PENDING) {
    return (promise2 = new MyPromise(function (resolve, reject) {
    self.resolvedCallbacks.push(function () {
    // 考虑到可能会有报错,所以使用 try/catch 包裹
    try {
    var x = onResolved(self.value);
    resolutionProcedure(promise2, x, resolve, reject);
    } catch (r) {
    reject(r);
    }
    });

    self.rejectedCallbacks.push(function () {
    try {
    var x = onRejected(self.value);
    resolutionProcedure(promise2, x, resolve, reject);
    } catch (r) {
    reject(r);
    }
    });
    }));
    }
    };
    // 规范 2.3
    function resolutionProcedure(promise2, x, resolve, reject) {
    // 规范 2.3.1,x 不能和 promise2 相同,避免循环引用
    if (promise2 === x) {
    return reject(new TypeError("Error"));
    }
    // 规范 2.3.2
    // 如果 x 为 Promise,状态为 pending 需要继续等待否则执行
    if (x instanceof MyPromise) {
    if (x.currentState === PENDING) {
    x.then(function (value) {
    // 再次调用该函数是为了确认 x resolve 的
    // 参数是什么类型,如果是基本类型就再次 resolve
    // 把值传给下个 then
    resolutionProcedure(promise2, value, resolve, reject);
    }, reject);
    } else {
    x.then(resolve, reject);
    }
    return;
    }
    // 规范 2.3.3.3.3
    // reject 或者 resolve 其中一个执行过得话,忽略其他的
    let called = false;
    // 规范 2.3.3,判断 x 是否为对象或者函数
    if (x !== null && (typeof x === "object" || typeof x === "function")) {
    // 规范 2.3.3.2,如果不能取出 then,就 reject
    try {
    // 规范 2.3.3.1
    let then = x.then;
    // 如果 then 是函数,调用 x.then
    if (typeof then === "function") {
    // 规范 2.3.3.3
    then.call(
    x,
    y => {
    if (called) return;
    called = true;
    // 规范 2.3.3.3.1
    resolutionProcedure(promise2, y, resolve, reject);
    },
    e => {
    if (called) return;
    called = true;
    reject(e);
    }
    );
    } else {
    // 规范 2.3.3.4
    resolve(x);
    }
    } catch (e) {
    if (called) return;
    called = true;
    reject(e);
    }
    } else {
    // 规范 2.3.4,x 为基本类型
    resolve(x);
    }
    }
    +

    以上就是根据 Promise / A+ 规范来实现的代码,可以通过 promises-aplus-tests 的完整测试

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/96/index.html b/page/96/index.html new file mode 100644 index 0000000000..57399e49cb --- /dev/null +++ b/page/96/index.html @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    call, apply, bind 区别

    首先说下前两者的异同。
    相同: callapply 都是为了解决改变 this 的指向。
    不同:传参的方式不同,除了第一个参数外, call 可以接收一个参数列表apply 只接受一个参数数组

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // js代码
    let anObj = {
    value: 1
    }
    function getValue(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value)
    }
    getValue.call(anObj, 'lixuguang', '31')
    getValue.apply(anObj, ['lixuguang', '31'])
    +

    模拟实现 callapply

    可以从以下几点来考虑如何实现

    +
      +
    • 不传入第一个参数,那么默认为 window
    • +
    • 改变了 this 指向,让新的对象可以执行该函数。那么思路是否可以变成给新的对象添加一个函数,然后在执行完以后删除?
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // js代码

      Function.prototype.myCall = function (context) {
      var context = context || window // 有入参用入参,没有给 window
      context.fn = this // 给 context 添加一个属性,getValue.call(anObj, 'lixuguang', '31') => anObj.fn = getValue
      var args = [...arguments].slice(1) // 将 context 后面的参数取出来
      var result = context.fn(...args) // getValue.call(anObj, 'lixuguang', '31') => anObj.fn('yck', '24')
      delete context.fn // 删除 fn
      return result
      }
      +以上就是 call 的思路,apply 的实现也类似
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // js代码

      Function.prototype.Apply = function (context) {
      var context = context || window // 有入参用入参,没有给 window
      context.fn = this // 给 context 添加一个属性,getValue.call(anObj, 'lixuguang', '31') => anObj.fn = getValue
      var args = arguments[1] // 将 context 后面的参数取出来
      var result = args?context.fn(...args):context.fn() // getValue.call(anObj, ['lixuguang', '31']) => anObj.fn('yck', '24')
      delete context.fn // 删除 fn
      return result
      }
      +bind 和其他两个方法作用也是一致的,只是该方法会返回一个函数。并且我们可以通过 bind 实现柯里化。
    • +
    +

    同样的,也来模拟实现下 bind

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // js代码

    Function.prototype.myBind = function (context) {
    if (typeof this !== 'function') {
    throw new TypeError('Error')
    }
    var _this = this
    var args = [...arguments].slice(1)

    return function F() { // 返回一个函数,这是 `bind` 和 `call` , `apply` 的区别
    if (this instanceof F) { // 因为返回了一个函数,我们可以 new F(),所以需要判断
    return new _this(...args, ...arguments)
    }
    return _this.apply(context, args.concat(...arguments))
    }
    }
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/97/index.html b/page/97/index.html new file mode 100644 index 0000000000..c364221885 --- /dev/null +++ b/page/97/index.html @@ -0,0 +1,510 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    继承

    在 ES5 中,我们可以使用如下方式解决继承的问题

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // js代码
    function Super() {}
    Super.prototype.getNumber = function() {
    return 1
    }

    function Sub() {}
    let s = new Sub()
    Sub.prototype = Object.create(Super.prototype, {
    constructor: {
    value: Sub,
    enumerable: false,
    writable: true,
    configurable: true
    }
    })
    +

    以上继承实现思路就是将子类的原型设置为父类的原型
    ES6 中,我们可以通过 class 语法轻松解决这个问题

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // js代码

    class MyDate extends Date {
    test() {
    return this.getTime()
    }
    }
    let myDate = new MyDate()
    myDate.test()
    +

    但是 ES6 不是所有浏览器都兼容,所以我们需要使用 Babel 来编译这段代码。

    +

    如果你使用编译过得代码调用 myDate.test() 你会惊奇地发现出现了报错

    +

    因为在 JS 底层有限制,如果不是由 Date 构造出来的实例的话,是不能调用 Date 里的函数的。所以这也侧面的说明了:ES6 中的 class 继承与 ES5 中的一般继承写法是不同的

    +

    既然底层限制了实例必须由 Date 构造出来,那么我们可以改变下思路实现继承

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // js代码

    function MyData() {

    }
    MyData.prototype.test = function () {
    return this.getTime()
    }
    let d = new Date() // 父类实例
    Object.setPrototypeOf(d, MyData.prototype)
    Object.setPrototypeOf(MyData.prototype, Date.prototype)
    +

    以上继承实现思路:先创建父类实例 => 改变实例原先的 __proto__ 转而连接到子类的 prototype => 子类的 prototype__proto__ 改为父类的 prototype

    +

    通过以上方法实现的继承就可以完美解决 JS 底层的这个限制。

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/98/index.html b/page/98/index.html new file mode 100644 index 0000000000..c9c5a2b905 --- /dev/null +++ b/page/98/index.html @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    函数防抖和节流

    在前端开发的过程中,我们经常会需要绑定一些持续触发的事件,如 resize、scroll、mousemove 等等,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。

    +

    通常这种情况下我们怎么去解决的呢?一般来讲,防抖和节流是比较好的解决方案。

    +

    防抖

    你是否在日常开发中遇到一个问题,在滚动事件中需要做个复杂计算或者实现一个按钮的防二次点击操作。

    +

    这些需求都可以通过函数防抖动来实现。尤其是第一个需求,如果在频繁的事件回调中做复杂计算,很有可能导致页面卡顿,不如将多次计算合并为一次计算,只在一个精确点做操作。

    +

    PS:防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的情况下只会调用一次,而节流的 情况会每隔一定时间(参数wait)调用函数。

    +

    我们先来看一个袖珍版的防抖理解一下防抖的实现:

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // js代码

    // func是用户传入需要防抖的函数
    // wait是等待时间
    const debounce = (func, wait = 50) => {
    // 缓存一个定时器id
    let timer = 0
    // 这里返回的函数是每次用户实际调用的防抖函数
    // 如果已经设定过定时器了就清空上一次的定时器
    // 开始一个新的定时器,延迟执行用户传入的方法
    return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
    func.apply(this, args)
    }, wait)
    }
    }
    // 不难看出如果用户调用该函数的间隔小于wait的情况下,上一次的时间还未到就被清除了,并不会执行函数
    +

    这是一个简单版的防抖,但是有缺陷,这个防抖只能在最后调用。一般的防抖会有immediate选项,表示是否立即调用。这两者的区别,举个栗子来说:

    +
      +
    • 例如在搜索引擎搜索问题的时候,我们当然是希望用户输入完最后一个字才调用查询接口,这个时候适用延迟执行的防抖函数,它总是在一连串(间隔小于wait的)函数触发之后调用。
    • +
    • 例如用户给某系统表单点提交的时候,我们希望用户点第一下的时候就去调用接口,这个情况适用立即执行的防抖函数,它总是在第一次调用,并且下一次调用必须与前一次调用的时间间隔大于wait才会触发。
    • +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    // js代码

    // 这个是用来获取当前时间戳的
    function now() {
    return +new Date()
    }
    /**
    * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
    *
    * @param {function} func 回调函数
    * @param {number} wait 表示时间窗口的间隔
    * @param {boolean} immediate 设置为ture时,是否立即调用函数
    * @return {function} 返回客户调用函数
    */
    function debounce (func, wait = 50, immediate = true) {
    let timer, context, args

    // 延迟执行函数
    const later = () => setTimeout(() => {
    // 延迟函数执行完毕,清空缓存的定时器序号
    timer = null
    // 延迟执行的情况下,函数会在延迟函数中执行
    // 使用到之前缓存的参数和上下文
    if (!immediate) {
    func.apply(context, args)
    context = args = null
    }
    }, wait)

    // 这里返回的函数是每次实际调用的函数
    return function(...params) {
    // 如果没有创建延迟执行函数(later),就创建一个
    if (!timer) {
    timer = later()
    // 如果是立即执行,调用函数
    // 否则缓存参数和调用上下文
    if (immediate) {
    func.apply(this, params)
    } else {
    context = this
    args = params
    }
    // 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
    // 这样做延迟函数会重新计时
    } else {
    clearTimeout(timer)
    timer = later()
    }
    }
    }
    +

    整体函数实现的不难,总结一下。

    +
      +
    • 对于按钮防点击来说的实现:如果函数是立即执行的,就立即调用,如果函数是延迟执行的,就缓存上下文和参数,放到延迟函数中去执行。一旦我开始一个定时器,只要我定时器还在,你每次点击我都重新计时。一旦你点累了,定时器时间到,定时器重置为 null,就可以再次点击了。
    • +
    • 对于延时执行函数来说的实现:清除定时器ID,如果是延迟调用就调用函数
    • +
    +

    节流

    防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    // js代码

    /**
    * underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait
    *
    * @param {function} func 回调函数
    * @param {number} wait 表示时间窗口的间隔
    * @param {object} options 如果想忽略开始函数的的调用,传入{leading: false}。
    * 如果想忽略结尾函数的调用,传入{trailing: false}
    * 两者不能共存,否则函数不能执行
    * @return {function} 返回客户调用函数
    */
    _.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;
    // 之前的时间戳
    var previous = 0;
    // 如果 options 没传则设为空对象
    if (!options) options = {};
    // 定时器回调函数
    var later = function() {
    // 如果设置了 leading,就将 previous 设为 0
    // 用于下面函数的第一个 if 判断
    previous = options.leading === false ? 0 : _.now();
    // 置空一是为了防止内存泄漏,二是为了下面的定时器判断
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
    };
    return function() {
    // 获得当前时间戳
    var now = _.now();
    // 首次进入前者肯定为 true
    // 如果需要第一次不执行函数
    // 就将上次时间戳设为当前的
    // 这样在接下来计算 remaining 的值时会大于0
    if (!previous && options.leading === false) previous = now;
    // 计算剩余时间
    var remaining = wait - (now - previous);
    context = this;
    args = arguments;
    // 如果当前调用已经大于上次调用时间 + wait
    // 或者用户手动调了时间
    // 如果设置了 trailing,只会进入这个条件
    // 如果没有设置 leading,那么第一次会进入这个条件
    // 还有一点,你可能会觉得开启了定时器那么应该不会进入这个 if 条件了
    // 其实还是会进入的,因为定时器的延时
    // 并不是准确的时间,很可能你设置了2秒
    // 但是他需要2.2秒才触发,这时候就会进入这个条件
    if (remaining <= 0 || remaining > wait) {
    // 如果存在定时器就清理掉否则会调用二次回调
    if (timeout) {
    clearTimeout(timeout);
    timeout = null;
    }
    previous = now;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {
    // 判断是否设置了定时器和 trailing
    // 没有的话就开启一个定时器
    // 并且不能不能同时设置 leading 和 trailing
    timeout = setTimeout(later, remaining);
    }
    return result;
    };
    };
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/99/index.html b/page/99/index.html new file mode 100644 index 0000000000..fcc40f5f4c --- /dev/null +++ b/page/99/index.html @@ -0,0 +1,514 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    模块化

    在有 Babel 的情况下,我们可以直接使用 ES6 的模块化

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // js代码

    // file a.js
    export function a() {}
    export function b() {}
    // file b.js
    export default function() {}

    import {a, b} from './a.js'
    import XXX from './b.js'
    +

    CommonJS

    CommonJsNode 独有的规范,浏览器中使用就需要用到 Browserify 解析了。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // js代码

    // a.js
    module.exports = {
    a: 1
    }
    // or
    exports.a = 1

    // b.js
    var module = require('./a.js')
    module.a // -> log 1
    +

    在上述代码中, module.exportsexports 很容易混淆,让我们来看看大致内部实现

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // js代码

    var module = require('./a.js')
    module.a
    // 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
    // 重要的是 module 这里,module 是 Node 独有的一个变量
    module.exports = {
    a: 1
    }
    // 基本实现
    var module = {
    exports: {} // exports 就是个空对象
    }
    // 这个是为什么 exports 和 module.exports 用法相似的原因
    var exports = module.exports
    var load = function (module) {
    // 导出的东西
    var a = 1
    module.exports = a
    return module.exports
    };
    +

    再来说说 module.exports 和 exports,用法其实是相似的,但是不能对 exports 直接赋值,不会有任何效果。

    +

    对于 CommonJS 和 ES6 中的模块化的两者区别是:

    +
      +
    • 前者支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
    • +
    • 前者是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
    • +
    • 前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是后者采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
    • +
    • 后者会编译成 require/exports 来执行的
    • +
    +

    AMD

    AMD 是由 RequireJS 提出的

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // js代码

    // AMD
    define(['./a', './b'], function(a, b) {
    a.do()
    b.do()
    })
    define(function(require, exports, module) {
    var a = require('./a')
    a.doSomething()
    var b = require('./b')
    b.doSomething()
    })
    + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/placeholder b/placeholder deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/search.xml b/search.xml new file mode 100644 index 0000000000..7d140d38ac --- /dev/null +++ b/search.xml @@ -0,0 +1,7826 @@ + + + + 2020年21天主题挑战之灵感篇-Day 1-写下自己期待中的生平 + /2020/01/02/21-Day-Challenge-01/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    还记得我19年总结最下面的那张照片么,那个同事送的的本子,对,就是下面这张图,这本子叫做《One In A Million》中文叫做极少数手册,是一个管理时间用的管理手册,英文如果直译的话应该是叫做万里挑一,实话说我并不是很自信,虽然我觉得自己还算可以,但远远达不到万里挑一的程度,所以,我打算好好利用这个本子,让自己尽可能成为百里挑一,千里挑一,最后万里挑一的人。

    +

    本子里有一个栏目叫做21天主题挑战,21-Day Challenge,每个月21天都会给你列出一些主题,越往后列出来的主题越少,更多的需要个人去补充,我想去挑战这些内容,让自己的生活变得有目标,让自己变得更好。

    + + +

    1月份21天主题挑战之灵感Inspiration

    +

    设定一个挑战主题,让自己更富有创造力,连续21天挑战,让灵感乍现,唤醒天赋。

    +
    +

    第一天 Day 1 写下自己期待中的生平

    看到这个题目之后,我闭上了眼睛,努力的回想我自己曾经的梦想是什么,什么时候丢掉了梦想,儿时的梦想,上学时的梦想,长大以后的梦想,现在的梦想,我努力回想了好久好久,儿时的梦想我想起来了,上学时的梦想我想起来了,长大后的梦想我也想起来了,但是我没能一下子找到我现在的梦想,也许现在说梦想有些奢侈,也许也有点贴不上今天的主题期待中的人生,我不太会写文章,但是想到哪里写哪里吧,我先写下我之前的那些个梦想。

    +

    小时候的梦想-成为科学家

    小时候的梦想说出来很简单,但是是那么真诚,真诚就容易打动人,说实话我记性不是太好,已经忘掉了小时候的梦想,所以我问了我妈,我妈笑话我,说我咋不记得小时候那伟大的梦想了,妈妈说我儿时的梦想是当科学家,那会应该是上小学吧,妈妈说学校老师让写作文,标题就是我的梦想,我的作文里写的是成为科学家,这个科学家不像是别人写的,比如造火箭,造大炮又或者是造卫星造原子弹,我的梦想现在说起来与其说是科学家,不如说是药剂师更贴切,妈妈笑着跟我说我当时的梦想是做一个科学家,要研究出来一种长生不老的药,然后让爸爸妈妈吃下以后就可以永远健康年轻,妈妈说那个时候她总逗我,看到路上漂亮的阿姨就会问我,那个阿姨漂亮么,起初我会说漂亮,然后妈妈就会说那让她给你当妈妈好不好,我呢会说不好,妈妈说后来她再问的时候,我就再也不会说我要漂亮阿姨当妈妈的话了,我现在想想也许就是那时,就是因为发现妈妈越来越胖,辛苦操劳后年轻也渐渐不在我才有了这个想法吧,说实话上小学期间我一直认为自己长大能够成为一个科学家,能够造出那长生不老的药让爸妈吃下,这样我就再也不会需要一个漂亮阿姨当妈妈了。我那时希望快快长大,长大了以后我就能做科学家了。

    +

    上了初中高中以后,也许是进了大城市,也许是长大了,我知道了原来的梦想可能有些遥远,学业的压力让我有些透不过气,从海岛到城市,落后了3年的时间,我通过补习班慢慢追赶,终于能够追到了班级还算靠前的位置,那个时候我的梦想很小,也很简单,那就是考个好大学找个好工作,让爸爸妈妈早点不用那么操劳,早点享福。

    +

    18岁的梦想 — 找个好工作,让爸妈早点享福

    努力的学习,懵懂的感情,初中4年+高中3年的生活,最终我并没能特别出色的考上985/211大学,而是上了离家只有1小时车程的大外,学了计算机加日语英语,学费不便宜,每年16000,说实话没自己工作的时候不知道这16000对于一个下岗自己在家开小卖店的父母是多大的负担,工作后我才终于知道这笔钱有多少。好在原来在爸爸单位的时候接触电脑还算比较早,而我对这个新鲜玩意也算是感兴趣,大学学业上并没给我带来太大的压力,但是我也确实不是个聪明的孩子,日语仅仅过了N2,而英语则一直只是CET-4,现在回想起来,原来应该多努努力,也许现在的生活就会更好了,多想回到过去跟自己说,你要努力啊!一晃4年的大学生活就结束了,这时我终于离开了父母身边,只身去往了大城市北京,开启了我的工作生涯。

    +

    20岁的人生 — 多挣钱,快速成长

    工作了以后,我就像海绵一样不断的吸收着周围的水分,学习工作中需要的技能,学习如何才能让领导器重,学习如何才能快速积累人生的财富,因为我想着,想着能快快独立反哺我的爸妈。20岁,我跟媳妇儿谈了场异地的恋爱,后来她到北京找我,再后来我们就一起回了大连。北京是个大城市,大的有时候让人迷茫,虽然工作机会比较多,但是租房的压力,环境的恶劣,家里的呼唤,最终让我选择了回到我熟悉的城市,另外找了份工资不高的工作,我不满足,我想能成为顶天立地的男子汉,后来我就来到了现在的公司华宇,而且一待就是5年。

    +

    华宇的职业生涯 — 5年工作,9年经验

    网上有个段子我记得,一个人面试拿出简历,工作时间是2年却写着3年工作经验,面试官问他是不是写错了,他答不是,因为加班加出来的。这算是对前公司的吐槽吧。

    +

    来到大连华宇时,公司还不足120人,我所在的团队还是个交互组,只有三个人,前端的话只有我和另外一个刚毕业的大学生,记得刚来公司的第一年,我参与了140个项目的开发工作,现在想想这个数字有些惊人,但是因为只是些前端切图仔的工作,对我来说感觉难度并不大,不过还是要感谢刚开始这2年,让我的基础非常扎实,再后来公司引入了前后端分离,引入了Vue框架,越来越多的业务要写,数据处理要写,加班成了家常便饭,后面这三年,我几乎没有休过除了元旦和春节的任何法定节假日,每年5天的年假也几乎都没休成,说实话每次加班加到要崩溃时候,我都会想我到底为了啥这么拼命。要不我还是换一家比较轻松一点的工作吧,工资还能涨点。说实话这段时间工作就是生活的全部,每天到家都10点以后,到家老人孩子都睡了,有时候我都睡不着,想着我的生活难道就这样了么,我不甘心,不服输。很多人劝我别那么拼命,别把公司当成自己家的,只是个打工的而已,但是有时候想着下面还有那么多新人信任着我依赖着我,我就没法撒手不管。终于时间到了19年年底,5年来培养的前端团队最后还是没守护住,要拆到各项目团队了,一开始真的难以接受,不过公司领导层已经决定了,作为一线员工只能服从,我希望大家能够把心中的担忧都能消除掉,在新的团队里开启新的篇章,也许会有更好的发展,如果有一天离开了华宇,也要江湖相望,常聚聚。

    +

    2020年31岁踏上新的旅途

    还是感谢小可爱的这个本子,让我能够有主题想想我未来到底想要什么样的生活,说句实在话,我向往不为钱发愁工作的日子,可以在做好自己工作之外的时间里多陪陪家人,带着孩子常出去走走,见见外面的世界,说实话已经31岁了,除了谈恋爱时去找媳妇儿去过江浙苏杭,工作原因去过北京/青海,好像这些年也就去了趟南京,现在的我已经有些不知道什么叫做生活了,我希望自己的空闲时间是可以让兴趣填满的,可以和朋友同事同学多聚聚,但是怕打扰大家我又从来不会主动去约别人,小时候没有听从爸妈的话培养自己的兴趣,现在有些后悔,真的空闲下来都不知道该用什么填满这时间,都是躺在床上刷手机,看电视,我不喜欢这样的生活,但是我不知道该过怎么样的生活。看到那些会做饭的视频,做顿丰盛的饭菜给家人吃,看到他们的幸福笑脸,我想做那样的事;看到带着家人过着一路向前增长见闻的旅途生活,我想也尝试一下那样的人生,我感觉为了钱我被束缚在了工作上,2020年我想过一种不为工作所累,不为钱所累,能够享受生活,陪伴家人的生活,保重身体,每天快快乐乐的,多发现生活中的美好,感恩,努力,成长。期待自己成为更好的人。

    +

    写在最后

    我所期待中的生平,成为一个不被工作强迫,不被金钱所累,爱家顾家,孝敬父母,人缘好朋友多,兴趣广泛,感恩的人。

    +]]>
    + + 自我提升 + 杂记随感 + +
    + + 从零开始:Vue cli3 库模式搭建组件库并发布到npm【转载】 + /2020/04/16/0-vuecli-3-component/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    市面上目前已有各种各样的UI组件库,比如 Element 和 iView,他们的强大毋庸置疑。但是我们面临的情况是需求越来越复杂,当它们不能再满足我们需求的时候,这个时候就有必要开发一套属于自己团队的组件库了。

    +

    所以本文的目的就是让读者能通过此文,小能做一个简单的插件供人使用,大能架构和维护一个组件库不在话下。

    +

    以下一个简单的颜色选择器插件vColorPicker讲述从开发到上线到npm的流程。

    +

    vColorPicker 插件 DEMO

    + +

    一、技术栈

    如何通过新版脚手架创建项目,这里就不提了,自行看官方文档。

    +
      +
    • Vue-cli3: 新版脚手架的库模式,可以让我们很轻松的创建打包一个库
    • +
    • npm:组件库将存放在npm
    • +
    • webpack:修改配置需要一点 webapck 的知识。
    • +
    +

    二、大纲

    想要搭建一个组件库,我们必须先要有一个大概的思路。

    +
      +
    1. 规划目录结构
    2. +
    3. 配置项目以支持目录结构
    4. +
    5. 编写组件
    6. +
    7. 编写示例
    8. +
    9. 配置使用库模式打包编译
    10. +
    11. 发布到npm
    12. +
    +

    三、规划目录结构

    1、创建项目

    在指定目录中使用命令创建一个默认的项目,或者根据自己需要自己选择。

    +
    $ vue create .
    +

    2、调整目录

    我们需要一个目录存放组件,一个目录存放示例,按照以下方式对目录进行改造。

    +
    .
    ...
    |-- examples // 原 src 目录,改成 examples 用作示例展示
    |-- packages // 新增 packages 用于编写存放组件
    ...
    .
    +

    四、配置项目以支持新的目录结构

    我们通过上一步的目录改造后,会遇到两个问题。

    +
      +
    1. src目录更名为examples,导致项目无法运行
    2. +
    3. 新增packages目录,该目录未加入webpack编译
    4. +
    +

    注:cli3 提供一个可选的 vue.config.js 配置文件。如果这个文件存在则他会被自动加载,所有的对项目和webpack的配置,都在这个文件中。

    +

    1、重新配置入口,修改配置中的 pages 选项

    新版 Vue CLI 支持使用 vue.config.js 中的 pages 选项构建一个多页面的应用。

    +

    这里使用 pages 修改入口到 examples

    +
    module.exports = {
    // 修改 src 为 examples
    pages: {
    index: {
    entry: 'examples/main.js',
    template: 'public/index.html',
    filename: 'index.html'
    }
    }
    }
    +

    2、支持对 packages 目录的处理,修改配置中的 chainWebpack 选项

    packages 是我们新增的一个目录,默认是不被 webpack 处理的,所以需要添加配置对该目录的支持。

    +

    chainWebpack 是一个函数,会接收一个基于 webpack-chain 的 ChainableConfig 实例。允许对内部的 webpack 配置进行更细粒度的修改。

    +
    module.exports = {
    // 修改 src 为 examples
    pages: {
    index: {
    entry: 'examples/main.js',
    template: 'public/index.html',
    filename: 'index.html'
    }
    },
    // 扩展 webpack 配置,使 packages 加入编译
    chainWebpack: config => {
    config.module
    .rule('js')
    .include
    .add('packages')
    .end()
    .use('babel')
    .loader('babel-loader')
    .tap(options => {
    // 修改它的选项...
    return options
    })
    }
    }
    +

    链式操作
    webpack-chain

    +

    五、编写组件

    以上我们已配置好对新目录架构的支持,接下来我们尝试编写组件。以下我们以一个已发布到 npm 的小插件作为示例。
    GitHub - 颜色选择器:vcolorpicker

    +

    1. 创建一个新组件

      +
    1. 在 packages 目录下,所有的单个组件都以文件夹的形式存储,所有这里创建一个目录 color-picker/
    2. +
    3. 在 color-picker/ 目录下创建 src/ 目录存储组件源码
    4. +
    5. 在 /color-picker 目录下创建 index.js 文件对外提供对组件的引用。
      修改 /packages/color-picker/index.js文件,对外提供引用。
    6. +
    +
    // ./packages/color-picker/index.js

    // 导入组件,组件必须声明 name
    import colorPicker from './src/color-picker.vue'

    // 为组件提供 install 安装方法,供按需引入
    colorPicker = function (Vue) {
    Vue.component(colorPicker.name, colorPicker)
    }

    // 默认导出组件
    export default colorPicker
    +

    2. 整合所有的组件,对外导出,即一个完整的组件库

    修改 /packages/index.js 文件,对整个组件库进行导出。

    +
    // ./packages/index.js

    // 导入颜色选择器组件
    import colorPicker from './color-picker'

    // 存储组件列表
    const components = [
    colorPicker
    ]

    // 定义 install 方法,接收 Vue 作为参数。如果使用 use 注册插件,则所有的组件都将被注册
    const install = function (Vue) {
    // 判断是否安装
    if (install.installed) return
    // 遍历注册全局组件
    components.map(component => Vue.component(component.name, component))
    }

    // 判断是否是直接引入文件
    if (typeof window !== 'undefined' && window.Vue) {
    install(window.Vue)
    }

    export default {
    // 导出的对象必须具有 install,才能被 Vue.use() 方法安装
    install,
    // 以下是具体的组件列表
    colorPicker
    }
    +

    六、编写示例

    1、在示例中导入组件库

    import Vue from 'vue'
    import App from './App.vue'

    // 导入组件库
    import ColorPicker from './../packages/index'
    // 注册组件库
    Vue.use(ColorPicker)

    Vue.config.productionTip = false

    new Vue({
    render: h => h(App)
    }).$mount('#app')
    +

    2、在示例中使用组件库中的组件

    在上一步用使用 Vue.use() 全局注册后,即可在任意页面直接使用了,而不需另外引入。当然也可以按需引入。

    +
    <template>
    <colorPicker v-model="color" v-on:change="headleChangeColor"></colorPicker>
    </template>

    <script>
    export default {
    data () {
    return {
    color: '#ff0000'
    }
    },
    methods: {
    headleChangeColor () {
    console.log('颜色改变')
    }
    }
    }
    </script>
    +

    七、发布到 npm,方便直接在项目中引用

    到此为止我们一个完整的组件库已经开发完成了,接下来就是发布到 npm 以供后期使用。

    +

    1、package.js 中新增一条编译为库的命令

    在库模式中,Vue是外置的,这意味着即使在代码中引入了 Vue,打包后的文件也是不包含Vue的。

    +

    Vue Cli3 构建目标:库

    +

    以下我们在 scripts 中新增一条命令

    +
      +
    • –target: 构建目标,默认为应用模式。这里修改为 lib 启用库模式。
    • +
    • –dest : 输出目录,默认 dist。这里我们改成 lib
    • +
    • [entry]: 最后一个参数为入口文件,默认为 src/App.vue。这里我们指定编译 packages/ 组件库目录。
      "script": {
      // ...
      "lib": "vue-cli-service build --target lib --name vcolorpicker --dest lib packages/index.js"
      }
    • +
    +

    执行编译库命令

    +
    $ npm run lib
    +

    2、配置 package.json 文件中发布到 npm 的字段

      +
    • name: 包名,该名字是唯一的。可在 npm 官网搜索名字,如果存在则需换个名字。
    • +
    • version: 版本号,每次发布至 npm 需要修改版本号,不能和历史版本号相同。
    • +
    • description: 描述。
    • +
    • main: 入口文件,该字段需指向我们最终编译后的包文件。
    • +
    • keyword:关键字,以空格分离希望用户最终搜索的词。
    • +
    • author:作者
    • +
    • private:是否私有,需要修改为 false 才能发布到 npm
    • +
    • license: 开源协议
    • +
    +

    以下为参考设置

    +
    {
    "name": "vcolorpicker",
    "version": "0.1.5",
    "description": "基于 Vue 的颜色选择器",
    "main": "lib/vcolorpicker.umd.min.js",
    "keyword": "vcolorpicker colorpicker color-picker",
    "private": false
    }
    +

    3、添加 .npmignore 文件,设置忽略发布文件

    我们发布到 npm 中,只有编译后的 lib 目录、package.json、README.md才是需要被发布的。所以我们需要设置忽略目录和文件。

    +

    和 .gitignore 的语法一样,具体需要提交什么文件,看各自的实际情况。

    +
    # 忽略目录
    examples/
    packages/
    public/

    # 忽略指定文件
    vue.config.js
    babel.config.js
    *.map
    +

    4、登录到 npm

    首先需要到 npm 上注册一个账号,注册过程略。

    +

    如果配置了淘宝镜像,先设置回npm镜像:

    +
    $ npm config set registry http://registry.npmjs.org
    +

    然后在终端执行登录命令,输入用户名、密码、邮箱即可登录。

    +
    $ npm login
    + +

    5、发布到 npm

    执行发布命令,发布组件到 npm

    +
    $ npm publish
    + +

    6、发布成功

    发布成功后稍等几分钟,即可在 npm 官网搜索到。以下是刚提交的 vcolorpicker

    +

    7、使用新发布的组件库

    安装

    +
    $ npm install vcolorpicker -S
    +

    使用

    +
    // 在 main.js 引入并注册
    import vcolorpicker from 'vcolorpicker'
    Vue.use(vcolorpicker)

    // 在组件中使用
    <template>
    <colorPicker v-model="color" />
    </template>
    <script>
    export default {
    data () {
    return {
    color: '#ff0000'
    }
    }
    }
    </script>
    +

    暂时没有做包含多个组件的时候的按需加载,以后研究了再补充。

    +

    八、项目地址

    Github 地址:https://github.com/zuley/vue-color-picker
    npm 地址:https://www.npmjs.com/package/vcolorpicker
    DEMO 演示:http://vue-color-picker.rxshc.com

    +

    九、参考文章

    从零开始搭建Vue组件库 VV-UI
    Vue插件开发
    组件基础

    +]]>
    + + 前端框架 + Vue + + + 打包 + +
    + + CSS border三角、圆角图形生成技术详解 + /2013/06/26/CSS-Triangle-Circle/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    利用CSS的border属性可以生成一些图形,例如三角或是圆角。纯粹的CSS2的内容,没有兼容性的问题,我之前在纯CSS实现各类气球泡泡对话框效果一文中算是比较详细的讲述了CSS border属性生成三角的原理,以及实例。我觉得此技术相当实用的,故本文再次简单叙述一下,另外,本文还将展示可能并不为众人所知的CSS border圆角生成技术。好了,裹脚布的话就不说了,直接进入正题。

    + + +

    CSS border生成三角技术简介

    效果抢鲜

    下图为使用CSS的border属性实现的三角效果:

    +
    // css 代码
    .test{
    width: 0;
    height: 0;
    border-width: 20px 10px;
    border-style: solid;
    border-color: #ff3300 #ff3300 #ffffff #ffffff;
    }
    +

    如何实现的,为何会有这样的效果,不急,take it easy!

    +

    梯形图案

    看下面这段样式:

    +
    .test{
    width: 10px;
    height: 10px;
    border: 10px solid;
    border-color: #ff3300 #0000ff #339966 #00ff00
    }
    +

    当某个div应用了上面这个样式后,结果会如何?见下图(截自Firefox3.5,IE浏览器有细节上的差异):

    +

    更进一步 – 部分边框透明

    现在,设想一下,如果我们现在只保留一个一个上边框,其余边框均transparent透明(或与背景色同色),那么是不是就只显示一个上面红色的边框了,我们测试下,与上面类似的代码,只是修改下其余三个边框的颜色。

    +
    .test{
    width:10px;
    height:10px;
    border:10px solid;
    border-color:#ff3300 #ffffff #ffffff #ffffff;
    }
    +

    结果如下图(截自Firefox3.5):

    +

    从梯形到三角

    上面的是梯形,我要想得到一个三角图案该怎么办呢?显然,很简单,把div的高宽都变成0,只留一边,不就是三角了吗?如下代码:

    +
    .test{
    width: 0;
    height: 0;
    border: 10px solid;
    border-color: #ff3300 #ffffff #ffffff #ffffff;}
    +

    结果如下(依旧截图自Firefox3.5):

    +

    从等腰直角三角形到普通等腰三角

    上图为等腰直角三角形,之所以为等腰直角,是因为所有的边框宽度是一样的,如果我们将边框宽度设置为不同,那会怎样?则会形成等腰三角形。如下代码:

    +
    .test{
    width: 0;
    height: 0;
    border-width: 20px 10px;
    border-style: solid;
    border-color: #ff3300 #ffffff #ffffff #ffffff;}
    +

    得到的结果如下图:

    +

    从等腰到不等腰

    我们可以不局限于保留一条边框,我们可以保留两条,于是我们可以告别等腰,得到更加锐利的三角,正如一开始所展示的那个三角:

    +
    .test{
    width: 0;
    height: 0;
    border-width: 20px 10px;
    border-style: solid;
    border-color: #ff3300 #ff3300 #ffffff #ffffff;}
    +

    实际的应用

    关于应用,不多说,直接看图:
    说明:
    以上的测试代码纯粹为了说明原理,所以使用#ffffff白色边框,通过于背景融合来隐藏边框。在实际的操作中,应该使用transparent透明属性,例如border-color:#ff3300 #ff3300 transparent transparent;,这同样会有问题,IE6浏览器不支持transparent透明属性,不过没有关系,就border生成三角技术而言,直接设置对应的透明边框的border-style属性为dotted或是dashed即可解决这一问题,为什么使用dotted和dashed可以修复此问题呢?您有兴趣可以参见默尘的这篇文章Dotted&Dashed终极分析及IE6透明边框。

    +

    CSS border圆角生成技术简介

    我看圆角

    一提到圆角,我脑中闪过的词就是“定位”,“嵌套”,“模拟”,“渐进增强”,“滥用”。

    +
      +
    • 定位,也就是切四个角上下左右定位,这是淘宝首页的做法,但是面对IE6的奇偶bug只能当作看客;
    • +
    • 使用“嵌套”则不会有此问题,“嵌套”分图片背景嵌套和CSS边框嵌套,使用图片嵌套则图片的重用性,大小优化有待加强,边框嵌套则技术实现上有些难度;
    • +
    • 或使用“渐进增强”,CSS3 border-radius属性,而不要去鸟IE这类自我感觉良好的浏览器;
    • +
    • 或是学习Google使用CSS模拟,而一般的CSS模拟都是使用左右边框+背景色的方式1像素1像素的拼合成的。这类方法各有优缺点,需根据实际情况采用。对于满眼圆角的设计图我是很不喜欢的,该用则用,切勿为了圆角而圆角。
    • +
    +

    border圆角图案生成法

    这里介绍的实现圆角的得到与上面提到的都是不一样的,虽然也属于CSS模拟的范畴,但是其高效的程度确实相当惊人的,可谓最佳实践之一。
    我们先看看效果,见下图,截自Firefox3.6:
    上述效果的实现仅仅使用了三个标签,如下代码:

    +
    // html 代码
    <div class="box">
    <div class="top"></div>
    <div class="center">我是一只小小鸟、小小鸟!</div>
    <div class="bot"></div>
    </div>

    // css 代码
    .box{
    width:500px;
    }
    .top{
    border-bottom:3px solid;
    border-top-color:#cc0000;
    border-bottom-color:#cc0000;
    border-left:3px dotted transparent;
    border-right:3px dotted transparent;
    }
    .center{
    padding:10px 20px;
    color:white;
    font-size:14px;
    background:#cc0000;
    }
    .bot{
    border-top:3px solid;
    border-top-color:#cc0000;
    border-bottom-color:#cccccc;
    border-left:3px dotted transparent;
    border-right:3px dotted transparent;
    }
    +

    我们看看这段代码在IE6下的效果:

    +

    这里的高效在于,仅仅使用了一层标签就模拟了3像素的圆角,按照曾经我对CSS圆角模拟的理解,模拟1像素的圆角需要一层标签(background+borderLeft+borderRight),两像素的需要两层标签,三像素的需要三层标签。

    +

    有点神奇,但是就像看刘谦的魔术一样,说穿了也就那么回事,其实这里的圆角模拟在本文的上面已经展示了,就是这样图片:

    +

    您可能会疑问,是不是搞错图片啦,这显然不是一个模样的,非也非也,就本质上而言,圆角的实现与上面的梯形图就是同样的东西。现在,盯着上面这张图,我们想象一下,用力的想象,用想花姑娘的那番劲头想象——上面的梯形宽度越来越宽(不是拉伸),一直宽到500像素,是不是与上面实现的圆角的下边缘一致啊?

    +

    也就是说,那个含有“我是一只小小鸟……”文字的圆角图形是有一个上梯形+矩形+下梯形组成的。参见下面的分离效果图:
    您可以狠狠地点击这里:CSS border圆角生成demo

    +

    局限性

    人无完人,金无足赤,此方法虽然简洁高效,兼容性上佳,但是依然有局限性,在实现实色背景的圆角效果时,此方法可谓首选;如果是纯粹的圆角边框,此方法也可以实现,需要用到边框重叠,但是标签数几乎要翻倍,其权衡效用将大打折扣,反不如其他圆角方法来的实在。

    +

    结语

    如果在web制作中,需要用的一些直接可以使用CSS+单标签模拟的图片,我的建议是“毫不犹豫使用CSS模拟”,例如实色的三角,或是实现实色的圆角效果,这可以说是最高效,最利于扩展维护的前端实现方法了。我们需要开阔的思维,而不要仅仅局限于眼前的技术,武侠中所谓的“无招胜有招”还是有着一定的哲学道理的,长远来看,意识与海纳百川的心态比当下的一点技术更来得重要。

    +]]>
    + + 前端技术 + CSS + + + CSS技巧 + +
    + + 国内配置Electron开发环境的正确方式【转载】 + /2020/04/07/Electron-Offline-Build/ + 前言

    最近在做electron相关开发,疲于网络环境的种种限制,找遍了互联网相关资料,终于找到一篇比较全面的文章,怕丢了转过来。

    + +

    转载

    +]]>
    + + 大前端 + 客户端技术 + + + Electron + +
    + + es6中箭头函数没了arguments怎么办? + /2020/01/22/Es6-arrowFunc-arguments/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    es6中,arguments被取消掉了,但是函数还是会有参数不确定的时候,那么我们该怎么办呢,接下来将介绍一下如何在es6中完成arguments一样的效果。

    + + +

    剩余运算符

    // js代码

    let func = (...rest) => {
    console.log(rest)
    //[1,2,3]
    }

    func(1,2,3)
    + +

    看上面的代码,有的朋友会问,这...的操作不应该是展开运算符么?是的,扩展运算符与剩余操作符都是以三点开头的操作符,二者长的很像,只是在用法上有些差别。它们已经被 ES6 数组支持,能解决很多之前 arguments 解决起来很麻烦的问题。

    +

    简单来说剩余运算是在参数上使用的。

    +]]>
    + + 前端技术 + + + es6 + +
    + + 数组常见操作 ---- 去重、扁平、取最大最小值 + /2020/01/05/FE-guide-ArrayOprs/ + +

    作者:李旭光
    引用请标明出处

    + +

    去重

    1. 利用 ObjectKey 唯一特性

    开辟一个外部存储空间用于标示元素是否出现过。

    +
    // js代码

    const unique = (array)=> {
    var container = {};
    return array.filter((item, index) => container.hasOwnProperty(item) ? false : (container[item] = true));
    }
    + +

    2. 利用 indexOf 的返回值数值进行去重

    原理是 indexOf 获取元素时如果返回值不等于下标说明已经有了,配合 filter 更美味

    +
    // js代码

    const unique = arr => arr.filter((e,i) =>
    arr.indexOf(e) === i // 如果元素找到的当前下标和当前索引相同说明是同一个,不同说明不是唯一
    );
    + +

    还有一种变形方法利用 lastIndexOf 方法

    +
    +

    lastIndexOf() 方法返回指定元素(也即有效的 JavaScript 值或变量)在数组中的最后一个的索引,如果不存在则返回 -1。从数组的后面向前查找,从 fromIndex 处开始。

    +
    +
    // js代码

    const filterNonUnique = arr => arr.filter(e =>
    arr.indexOf(e) === arr.lastIndexOf(e) // 判断一个元素出现时的第一次下标和最后一次下标是否相同,如果相同那么就唯一
    )
    +

    3. 利用 Set 特性去重

    SetES6 中新的数据类型,它的特点就是元素唯一性,且可以和数组进行转换

    +
    // js代码

    const unique = arr => Array.from(new Set(arr)); // Array.from 方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。

    // 优化

    const unique = arr => [...new Set(arr)]; // 利用 `ES6` 中展开操作
    +

    4. 排序后判断前后两项是否相等去重

    通过比较相邻数字是否重复,将排序后的数组进行去重。

    +
    // js代码

    const unique = (array) => {
    array.sort((a, b) => a - b);
    let pre = 0;
    const result = [];
    for (let i = 0; i < array.length; i++) {
    if (!i || array[i] != array[pre]) {
    result.push(array[i]);
    }
    pre = i;
    }
    return result;
    }
    + +

    扁平

    1. 普通方法

    通过递归的方式判断数组中的项是否是数组,如果不是就加入到新的扁平数组,如果是就递归调用逐层判断,直到全部结束

    +
    // js代码

    const flatten = (array) => { // array 原数组
    let result = []; // 定义新的扁平数组
    for (let i = 0; i < array.length; i++) {
    if (Array.isArray(array[i])) { // 判断子元素是否是数组
    result = result.concat(flatten(array[i])); // 递归判断
    } else {
    result.push(array[i]); // 加入新数组
    }
    }
    return result;
    }
    + +

    2. 使用reduce简化上述方法

    +

    reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
    reducer 函数接收4个参数:

    +
    +
      +
    • Accumulator (acc) (累计器)
    • +
    • Current Value (cur) (当前值)
    • +
    • Current Index (idx) (当前索引)
    • +
    • Source Array (src) (源数组)
    • +
    • 您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
    • +
    +

    先看一段 reduce 的示例函数

    +
    // js代码

    const array1 = [1, 2, 3, 4];
    const reducer = (accumulator, currentValue) => accumulator + currentValue; // 定义一个累计器函数,作用是将数组前后累计值与当前值累加

    // 1 + 2 + 3 + 4
    console.log(array1.reduce(reducer)); // 没有初始值用第一个元素
    // expected output: 10

    // 5 + 1 + 2 + 3 + 4
    console.log(array1.reduce(reducer, 5)); // 有初始值从初始值开始
    // expected output: 15
    +

    这下大家应该对 reduce 函数认识了,接下来看看怎么简化

    +
    // js代码

    function flatten(array) {
    return array.reduce((newArray, current) => // 新数组,当前项
    Array.isArray(current) ? // 判断当前项是否为数组
    newArray.concat(flatten(current)) : // 是的话 递归调用
    newArray.concat(current) // 不是的话加进新数组
    , []) // 初始化新数组为空
    }
    +

    这里我们再变一个形,增加一个变量,变成可指定深度操作数组

    +
    // js代码

    function flattenByDeep(array, deep = 1) { // 默认一层
    return array.reduce(
    (target, current) =>
    Array.isArray(current) && deep > 1 ?
    target.concat(flattenByDeep(current, deep - 1)) : // 下一次减一层
    target.concat(current)
    , [])
    }
    +

    最值

    利用 reduce

    reduce 函数真的是超级好用,

    +
    // js代码

    array.reduce((c,n) => Math.max(c,n))
    + +

    Math.max

    Math.max 参数原本是一组数字,只需要让他可以接收数组即可。

    +
    // js代码

    const array = [3,2,1,4,5];
    Math.max.apply(null,array);
    Math.max(...array);
    ]]>
    + + 前端技术 + + + 面试 + 知识点 + +
    + + 前端常见知识点整理 ---- Generator 生成器 + /2020/01/03/FE-guide-Generator/ + +

    作者:李旭光
    引用请标明出处

    + +

    Generator 实现

    GeneratorES6 中新增的语法,和 Promise 一样,都可以用来异步编程

    +
    // js代码

    // 使用 * 表示这是一个 Generator 函数
    // 内部可以通过 yield 暂停代码
    // 通过调用 next 恢复执行
    function* test() {
    let a = 1 + 2;
    yield 2;
    yield 3;
    }
    let b = test();
    console.log(b.next()); // > { value: 2, done: false }
    console.log(b.next()); // > { value: 3, done: false }
    console.log(b.next()); // > { value: undefined, done: true }
    +

    从以上代码可以发现,加上 * 的函数执行后拥有了 next 函数,也就是说函数执行后返回了一个对象。每次调用 next 函数可以继续执行被暂停的代码。以下是 Generator 函数的简单实现

    +
    // js代码

    // cb 也就是编译过的 test 函数
    function generator(cb) {
    return (function() {
    var object = {
    next: 0,
    stop: function() {}
    };

    return {
    next: function() {
    var ret = cb(object);
    if (ret === undefined) return { value: undefined, done: true };
    return {
    value: ret,
    done: false
    };
    }
    };
    })();
    }
    // 如果你使用 babel 编译后可以发现 test 函数变成了这样
    function test() {
    var a;
    return generator(function(_context) {
    while (1) {
    switch ((_context.prev = _context.next)) {
    // 可以发现通过 yield 将代码分割成几块
    // 每次执行 next 函数就执行一块代码
    // 并且表明下次需要执行哪块代码
    case 0:
    a = 1 + 2;
    _context.next = 4;
    return 2;
    case 4:
    _context.next = 6;
    return 3;
    // 执行完毕
    case 6:
    case "end":
    return _context.stop();
    }
    }
    });
    }
    ]]>
    + + 前端技术 + 前端常见知识点整理 + + + 面试 + +
    + + 前端常见知识点整理 ---- Map、FlatMap 和 Reduce + /2020/01/03/FE-guide-Map-FlatMap-Reduce/ + +

    作者:李旭光
    引用请标明出处

    + +

    Map、FlatMap 和 Reduce

    Map

    Map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后 append 到新的数组中。

    +
    // js代码

    [1, 2, 3].map((v) => v + 1) // -> [2, 3, 4]
    +

    Map 有三个参数,分别是当前索引元素索引原数组

    +

    FlatMap

    FlatMapmap 的作用几乎是相同的,但是对于多维数组来说,会将原数组降维。可以将 FlatMap 看成是 map + flatten ,目前该函数在浏览器中还不支持。

    +
    // js代码

    [1, [2], 3].flatMap((v) => v + 1) // -> [2, 3, 4]
    +

    如果想将一个多维数组彻底的降维,可以这样实现

    +
    const flattenDeep = (arr) => Array.isArray(arr)
    ? arr.reduce( (a, b) => [...a, ...flattenDeep(b)] , [])
    : [arr]

    flattenDeep([1, [[2], [3, [4]], 5]])
    + +

    Reduce 升序执行

    Reduce 作用是数组中的值组合起来,最终得到一个值
    reduce() 方法对数组中的每个元素执行一个由您提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。

    +

    reducer 函数接收4个参数:

    +
      +
    • Accumulator (acc) (累计器)
    • +
    • Current Value (cur) (当前值)
    • +
    • Current Index (idx) (当前索引)
    • +
    • Source Array (src) (源数组)
      您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
    • +
    +
    // js代码

    function a() {
    console.log(1);
    }

    function b() {
    console.log(2);
    }

    [a, b].reduce((a, b) => a(b()))
    // -> 2 1
    ]]>
    + + 前端技术 + 前端常见知识点整理 + + + 面试 + +
    + + 前端常见知识点整理 ---- 模块化 + /2020/01/03/FE-guide-Module/ + +

    作者:李旭光
    引用请标明出处

    + +

    模块化

    在有 Babel 的情况下,我们可以直接使用 ES6 的模块化

    +
    // js代码

    // file a.js
    export function a() {}
    export function b() {}
    // file b.js
    export default function() {}

    import {a, b} from './a.js'
    import XXX from './b.js'
    +

    CommonJS

    CommonJsNode 独有的规范,浏览器中使用就需要用到 Browserify 解析了。

    +
    // js代码

    // a.js
    module.exports = {
    a: 1
    }
    // or
    exports.a = 1

    // b.js
    var module = require('./a.js')
    module.a // -> log 1
    +

    在上述代码中, module.exportsexports 很容易混淆,让我们来看看大致内部实现

    +
    // js代码

    var module = require('./a.js')
    module.a
    // 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
    // 重要的是 module 这里,module 是 Node 独有的一个变量
    module.exports = {
    a: 1
    }
    // 基本实现
    var module = {
    exports: {} // exports 就是个空对象
    }
    // 这个是为什么 exports 和 module.exports 用法相似的原因
    var exports = module.exports
    var load = function (module) {
    // 导出的东西
    var a = 1
    module.exports = a
    return module.exports
    };
    +

    再来说说 module.exports 和 exports,用法其实是相似的,但是不能对 exports 直接赋值,不会有任何效果。

    +

    对于 CommonJS 和 ES6 中的模块化的两者区别是:

    +
      +
    • 前者支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
    • +
    • 前者是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
    • +
    • 前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是后者采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
    • +
    • 后者会编译成 require/exports 来执行的
    • +
    +

    AMD

    AMD 是由 RequireJS 提出的

    +
    // js代码

    // AMD
    define(['./a', './b'], function(a, b) {
    a.do()
    b.do()
    })
    define(function(require, exports, module) {
    var a = require('./a')
    a.doSomething()
    var b = require('./b')
    b.doSomething()
    })
    ]]>
    + + 前端技术 + 前端常见知识点整理 + + + 面试 + +
    + + 前端常见知识点整理 ---- Promise + /2020/01/03/FE-guide-Promise/ + +

    作者:李旭光
    引用请标明出处

    + +

    Promise 实现

    PromiseES6 新增的语法,解决了回调地狱的问题。

    +

    可以把 Promise 看成一个状态机。初始是 pending 状态,可以通过函数 resolvereject ,将状态转变为 resolved 或者 rejected 状态,状态一旦改变就不能再次变化

    +

    then 函数会返回一个 Promise 实例,并且该返回值是一个新的实例而不是之前的实例。因为 Promise 规范规定除了 pending 状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个 then 调用就失去意义了。

    +

    对于 then 来说,本质上可以把它看成是 flatMap

    +
    // js代码

    // 三种状态
    const PENDING = "pending";
    const RESOLVED = "resolved";
    const REJECTED = "rejected";
    // promise 接收一个函数参数,该函数会立即执行
    function MyPromise(fn) {
    let _this = this;
    _this.currentState = PENDING;
    _this.value = undefined;
    // 用于保存 then 中的回调,只有当 promise
    // 状态为 pending 时才会缓存,并且每个实例至多缓存一个
    _this.resolvedCallbacks = [];
    _this.rejectedCallbacks = [];

    _this.resolve = function (value) {
    if (value instanceof MyPromise) {
    // 如果 value 是个 Promise,递归执行
    return value.then(_this.resolve, _this.reject)
    }
    setTimeout(() => { // 异步执行,保证执行顺序
    if (_this.currentState === PENDING) {
    _this.currentState = RESOLVED;
    _this.value = value;
    _this.resolvedCallbacks.forEach(cb => cb());
    }
    })
    };

    _this.reject = function (reason) {
    setTimeout(() => { // 异步执行,保证执行顺序
    if (_this.currentState === PENDING) {
    _this.currentState = REJECTED;
    _this.value = reason;
    _this.rejectedCallbacks.forEach(cb => cb());
    }
    })
    }
    // 用于解决以下问题
    // new Promise(() => throw Error('error))
    try {
    fn(_this.resolve, _this.reject);
    } catch (e) {
    _this.reject(e);
    }
    }

    MyPromise.prototype.then = function (onResolved, onRejected) {
    var self = this;
    // 规范 2.2.7,then 必须返回一个新的 promise
    var promise2;
    // 规范 2.2.onResolved 和 onRejected 都为可选参数
    // 如果类型不是函数需要忽略,同时也实现了透传
    // Promise.resolve(4).then().then((value) => console.log(value))
    onResolved = typeof onResolved === 'function' ? onResolved : v => v;
    onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;

    if (self.currentState === RESOLVED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
    // 规范 2.2.4,保证 onFulfilled,onRjected 异步执行
    // 所以用了 setTimeout 包裹下
    setTimeout(function () {
    try {
    var x = onResolved(self.value);
    resolutionProcedure(promise2, x, resolve, reject);
    } catch (reason) {
    reject(reason);
    }
    });
    }));
    }

    if (self.currentState === REJECTED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
    setTimeout(function () {
    // 异步执行onRejected
    try {
    var x = onRejected(self.value);
    resolutionProcedure(promise2, x, resolve, reject);
    } catch (reason) {
    reject(reason);
    }
    });
    }));
    }

    if (self.currentState === PENDING) {
    return (promise2 = new MyPromise(function (resolve, reject) {
    self.resolvedCallbacks.push(function () {
    // 考虑到可能会有报错,所以使用 try/catch 包裹
    try {
    var x = onResolved(self.value);
    resolutionProcedure(promise2, x, resolve, reject);
    } catch (r) {
    reject(r);
    }
    });

    self.rejectedCallbacks.push(function () {
    try {
    var x = onRejected(self.value);
    resolutionProcedure(promise2, x, resolve, reject);
    } catch (r) {
    reject(r);
    }
    });
    }));
    }
    };
    // 规范 2.3
    function resolutionProcedure(promise2, x, resolve, reject) {
    // 规范 2.3.1,x 不能和 promise2 相同,避免循环引用
    if (promise2 === x) {
    return reject(new TypeError("Error"));
    }
    // 规范 2.3.2
    // 如果 x 为 Promise,状态为 pending 需要继续等待否则执行
    if (x instanceof MyPromise) {
    if (x.currentState === PENDING) {
    x.then(function (value) {
    // 再次调用该函数是为了确认 x resolve 的
    // 参数是什么类型,如果是基本类型就再次 resolve
    // 把值传给下个 then
    resolutionProcedure(promise2, value, resolve, reject);
    }, reject);
    } else {
    x.then(resolve, reject);
    }
    return;
    }
    // 规范 2.3.3.3.3
    // reject 或者 resolve 其中一个执行过得话,忽略其他的
    let called = false;
    // 规范 2.3.3,判断 x 是否为对象或者函数
    if (x !== null && (typeof x === "object" || typeof x === "function")) {
    // 规范 2.3.3.2,如果不能取出 then,就 reject
    try {
    // 规范 2.3.3.1
    let then = x.then;
    // 如果 then 是函数,调用 x.then
    if (typeof then === "function") {
    // 规范 2.3.3.3
    then.call(
    x,
    y => {
    if (called) return;
    called = true;
    // 规范 2.3.3.3.1
    resolutionProcedure(promise2, y, resolve, reject);
    },
    e => {
    if (called) return;
    called = true;
    reject(e);
    }
    );
    } else {
    // 规范 2.3.3.4
    resolve(x);
    }
    } catch (e) {
    if (called) return;
    called = true;
    reject(e);
    }
    } else {
    // 规范 2.3.4,x 为基本类型
    resolve(x);
    }
    }
    +

    以上就是根据 Promise / A+ 规范来实现的代码,可以通过 promises-aplus-tests 的完整测试

    +]]>
    + + 前端技术 + 前端常见知识点整理 + + + 面试 + +
    + + 前端常见知识点整理 ---- 网络 + /2020/01/09/FE-guide-Net/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    网络相关的知识虽然平时在编码阶段很少遇到,但是并不是说它不重要,尤其是前端优化层面,能从网络传输过程优化的点有很多,所以,我们需要好好掌握网络相关的知识,接下来,我将会将我了解到的网络相关的知识点进行一一整理

    +

    UDP - User Datagram Protocol - 用户数据报协议

    面向报文

    UDP 是一个面向报文(报文可以理解为一段段的数据)的协议。意思就是 UDP 只是报文的搬运工,不会对报文进行任何拆分和拼接操作。

    +

    具体来说

    +
      +
    • 发送端,应用层将数据传递给传输层的 UDP 协议, UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了
    • +
    • 接收端,网络层将数据传递给传输层, UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作
    • +
    +

    不可靠性

      +
    1. UDP 是无连接的,也就是说通信不需要建立和断开连接。
    2. +
    3. UDP 也是不可靠的。协议收到什么数据就传递什么数据,并且也不会备份数据,对方能不能收到是不关心的
    4. +
    5. UDP 没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP
    6. +
    +

    高效

    因为 UDP 没有 TCP 那么复杂,需要保证数据不丢失且有序到达。所以 UDP 的头部开销小,只有八字节,相比 TCP 的至少二十字节要少得多,在传输数据报文时是很高效的。

    +

    头部包含了以下几个数据

    +
      +
    • 两个十六位的端口号,分别为源端口(可选字段)和目标端口
    • +
    • 整个数据报文的长度
    • +
    • 整个数据报文的检验和( IPv4 可选 字段),该字段用于发现头部信息和数据中的错误
    • +
    +

    传输方式

    UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。

    +

    TCP

    头部

    TCP 头部比 UDP 头部复杂的多

    +

    对于 TCP 头部来说,以下几个字段是很重要的

    +
      +
    • Sequence number(序号),它保证了 TCP 传输的报文都是有序的,对端可以通过序号顺序的拼接报文
    • +
    • Acknowledgement Number(确认编号),它表示数据接收端期望接收的下一个字节的编号是多少,同时也表示上一个序号的数据已经收到
    • +
    • Window Size(窗口大小),它表示还能接收多少字节的数据,用于流量控制
    • +
    • 标识符
        +
      • URG=1:该字段为一表示本数据报的数据部分包含紧急信息,是一个高优先级数据报文,此时紧急指针有效。紧急数据一定位于当前数据包数据部分的最前面,紧急指针标明了紧急数据的尾部。
      • +
      • ACK=1:该字段为一表示确认号字段有效。此外,TCP 还规定在连接建立后传送的所有报文段都必须把 ACK 置为一。
      • +
      • PSH=1:该字段为一表示接收端应该立即将数据 push 给应用层,而不是等到缓冲区满后再提交。
      • +
      • RST=1:该字段为一表示当前 TCP 连接出现严重问题,可能需要重新建立 TCP 连接,也可以用于拒绝非法的报文段和拒绝连接请求。
      • +
      • SYN=1:当SYN=1,ACK=0时,表示当前报文段是一个连接请求报文。当SYN=1,ACK=1时,表示当前报文段是一个同意建立连接的应答报文。
      • +
      • FIN=1:该字段为一表示此报文段是一个释放连接的请求报文。
      • +
      +
    • +
    +

    状态机

    HTTP 是无连接的,所以作为下层的 TCP 协议也是无连接的,虽然看似 TCP 将两端连接了起来,但是其实只是两端共同维护了一个状态

    +

    TCP 的状态机是很复杂的,并且与建立断开连接时的握手息息相关,接下来就来详细描述下两种握手。
    在这之前需要了解一个重要的性能指标 RTT 。该指标表示发送端发送数据到接收到对端数据所需的往返时间。

    +

    建立连接三次握手

    在 TCP 协议中,主动发起请求的一端为客户端,被动连接的一端称为服务端。不管是客户端还是服务端,TCP 连接建立完后都能发送和接收数据,所以 TCP 也是一个全双工的协议。

    +

    起初,两端都为 CLOSED 状态。在通信开始前,双方都会创建 TCB。 服务器创建完 TCB 后遍进入 LISTEN 状态,此时开始等待客户端发送数据。

    +

    第一次握手

    客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态,x 表示客户端的数据通信初始序号。

    +

    第二次握手

    服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。

    +

    第三次握手

    当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。

    +

    PS:第三次握手可以包含数据,通过 TCP 快速打开(TFO)技术。其实只要涉及到握手的协议,都可以使用类似 TFO 的方式,客户端和服务端存储相同 cookie,下次握手时发出 cookie 达到减少 RTT 的目的。

    +

    你是否有疑惑明明两次握手就可以建立起连接,为什么还需要第三次应答?

    +

    因为这是为了防止失效的连接请求报文段被服务端接收,从而产生错误。

    +

    可以想象如下场景。客户端发送了一个连接请求 A,但是因为网络原因造成了超时,这时 TCP 会启动超时重传的机制再次发送一个连接请求 B。此时请求顺利到达服务端,服务端应答完就建立了请求。如果连接请求 A 在两端关闭后终于抵达了服务端,那么这时服务端会认为客户端又需要建立 TCP 连接,从而应答了该请求并进入 ESTABLISHED 状态。此时客户端其实是 CLOSED 状态,那么就会导致服务端一直等待,造成资源的浪费。

    +

    PS:在建立连接中,任意一端掉线,TCP 都会重发 SYN 包,一般会重试五次,在建立连接中可能会遇到 SYN FLOOD 攻击。遇到这种情况你可以选择调低重试次数或者干脆在不能处理的情况下拒绝请求。

    +

    断开链接四次握手

    TCP 是全双工的,在断开连接时两端都需要发送 FIN 和 ACK。

    +

    第一次握手

    若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。

    +

    第二次握手

    B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,表示 A 到 B 的连接已经释放,不接收 A 发的数据了。但是因为 TCP 连接时双向的,所以 B 仍旧可以发送数据给 A。

    +

    第三次握手

    B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。

    +

    PS:通过延迟确认的技术(通常有时间限制,否则对方会误认为需要重传),可以将第二次和第三次握手合并,延迟 ACK 包的发送。

    +

    第四次握手

    A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。

    +

    为什么 A 要进入 TIME-WAIT 状态,等待 2MSL 时间后才进入 CLOSED 状态?

    +

    为了保证 B 能收到 A 的确认应答。若 A 发完确认应答后直接进入 CLOSED 状态,如果确认应答因为网络问题一直没有到达,那么会造成 B 不能正常关闭。

    +

    ARQ 协议

    ARQ 协议也就是超时重传机制。通过确认和超时机制保证了数据的正确送达,ARQ 协议包含停止等待 ARQ 和连续 ARQ

    +

    停止等待 ARQ

    正常传输过程

    只要 A 向 B 发送一段报文,都要停止发送并启动一个定时器,等待对端回应,在定时器时间内接收到对端应答就取消定时器并发送下一段报文。

    +

    报文丢失或出错

    在报文传输的过程中可能会出现丢包。这时候超过定时器设定的时间就会再次发送丢包的数据直到对端响应,所以需要每次都备份发送的数据。

    +

    即使报文正常的传输到对端,也可能出现在传输过程中报文出错的问题。这时候对端会抛弃该报文并等待 A 端重传。

    +

    PS:一般定时器设定的时间都会大于一个 RTT 的平均时间。

    +

    ACK 超时或丢失

    对端传输的应答也可能出现丢失或超时的情况。那么超过定时器时间 A 端照样会重传报文。这时候 B 端收到相同序号的报文会丢弃该报文并重传应答,直到 A 端发送下一个序号的报文。

    +

    在超时的情况下也可能出现应答很迟到达,这时 A 端会判断该序号是否已经接收过,如果接收过只需要丢弃应答即可。

    +

    这个协议的缺点就是传输效率低,在良好的网络环境下每次发送报文都得等待对端的 ACK 。

    +

    连续 ARQ

    在连续 ARQ 中,发送端拥有一个发送窗口,可以在没有收到应答的情况下持续发送窗口内的数据,这样相比停止等待 ARQ 协议来说减少了等待时间,提高了效率。

    +

    累计确认

    连续 ARQ 中,接收端会持续不断收到报文。如果和停止等待 ARQ 中接收一个报文就发送一个应答一样,就太浪费资源了。通过累计确认,可以在收到多个报文以后统一回复一个应答报文。报文中的 ACK 可以用来告诉发送端这个序号之前的数据已经全部接收到了,下次请发送这个序号 + 1的数据。

    +

    但是累计确认也有一个弊端。在连续接收报文时,可能会遇到接收到序号 5 的报文后,并未接到序号 6 的报文,然而序号 7 以后的报文已经接收。遇到这种情况时,ACK 只能回复 6,这样会造成发送端重复发送数据,这种情况下可以通过 Sack 来解决,这个会在下文说到。

    +

    滑动窗口

    在上面小节中讲到了发送窗口。在 TCP 中,两端都维护着窗口:分别为发送端窗口和接收端窗口。

    +

    发送端窗口包含已发送但未收到应答的数据和可以发送但是未发送的数据。

    +

    发送端窗口是由接收窗口剩余大小决定的。接收方会把当前接收窗口的剩余大小写入应答报文,发送端收到应答后根据该值和当前网络拥塞情况设置发送窗口的大小,所以发送窗口的大小是不断变化的。

    +

    当发送端接收到应答报文后,会随之将窗口进行滑动

    +

    滑动窗口实现了流量控制。接收方通过报文告知发送方还可以发送多少数据,从而保证接收方能够来得及接收数据。

    +

    Zero 窗口

    在发送报文的过程中,可能会遇到对端出现零窗口的情况。在该情况下,发送端会停止发送数据,并启动 persistent timer 。该定时器会定时发送请求给对端,让对端告知窗口大小。在重试次数超过一定次数后,可能会中断 TCP 链接。

    +

    拥塞处理

    拥塞处理和流量控制不同,后者是作用于接收方,保证接收方来得及接受数据。而前者是作用于网络,防止过多的数据拥塞网络,避免出现网络负载过大的情况。

    +

    拥塞处理包括了四个算法,分别为:慢开始拥塞避免快速重传快速恢复

    +

    慢开始算法

    慢开始算法,顾名思义,就是在传输开始时将发送窗口慢慢指数级扩大,从而避免一开始就传输大量数据导致网络拥塞。

    +

    慢开始算法步骤具体如下

    +
      +
    1. 连接初始设置拥塞窗口(Congestion Window) 为 1 MSS(一个分段的最大数据量)
    2. +
    3. 每过一个 RTT 就将窗口大小乘二
    4. +
    5. 指数级增长肯定不能没有限制的,所以有一个阈值限制,当窗口大小大于阈值时就会启动拥塞避免算法。
    6. +
    +

    拥塞避免算法

    拥塞避免算法相比简单点,每过一个 RTT 窗口大小只加一,这样能够避免指数级增长导致网络拥塞,慢慢将大小调整到最佳值。

    +

    在传输过程中可能定时器超时的情况,这时候 TCP 会认为网络拥塞了,会马上进行以下步骤:

    +
      +
    • 将阈值设为当前拥塞窗口的一半
    • +
    • 将拥塞窗口设为 1 MSS
    • +
    • 启动拥塞避免算法
    • +
    +

    快速重传

    快速重传一般和快恢复一起出现。一旦接收端收到的报文出现失序的情况,接收端只会回复最后一个顺序正确的报文序号(没有 Sack 的情况下)。如果收到三个重复的 ACK,无需等待定时器超时再重发而是启动快速重传。具体算法分为两种:

    +

    TCP Taho 实现如下

      +
    • 将阈值设为当前拥塞窗口的一半
    • +
    • 将拥塞窗口设为 1 MSS
    • +
    • 重新开始慢开始算法
    • +
    +

    TCP Reno 实现如下

      +
    • 拥塞窗口减半
    • +
    • 将阈值设为当前拥塞窗口
    • +
    • 进入快恢复阶段(重发对端需要的包,一旦收到一个新的 ACK 答复就退出该阶段)
    • +
    • 使用拥塞避免算法
    • +
    +

    TCP New Ren 改进后的快恢复

    TCP New Reno 算法改进了之前 TCP Reno 算法的缺陷。在之前,快恢复中只要收到一个新的 ACK 包,就会退出快恢复。

    +

    在 TCP New Reno 中,TCP 发送方先记下三个重复 ACK 的分段的最大序号。

    +

    假如我有一个分段数据是 1 ~ 10 这十个序号的报文,其中丢失了序号为 3 和 7 的报文,那么该分段的最大序号就是 10。发送端只会收到 ACK 序号为 3 的应答。这时候重发序号为 3 的报文,接收方顺利接收并会发送 ACK 序号为 7 的应答。这时候 TCP 知道对端是有多个包未收到,会继续发送序号为 7 的报文,接收方顺利接收并会发送 ACK 序号为 11 的应答,这时发送端认为这个分段接收端已经顺利接收,接下来会退出快恢复阶段。

    +

    HTTP

    HTTP 协议是个无状态协议,不会保存状态。

    +

    PostGet 的区别

    先引入副作用幂等的概念。

    +
    +

    副作用指对服务器上的资源做改变,搜索是无副作用的,注册是副作用的。

    +
    +
    +

    幂等指发送 M 和 N 次请求(两者不相同且都大于 1),服务器上资源的状态一致,比如注册 10 个和 11 个帐号是不幂等的,对文章进行更改 10 次和 11 次是幂等的。

    +
    +

    在规范的应用场景上说, Get 多用于无副作用,幂等的场景,例如搜索关键字。 Post 多用于副作用,不幂等的场景,例如注册。

    +

    在技术上说:

    +
      +
    • Get 请求能缓存,Post 不能
    • +
    • Post 相对 Get 安全一点点,因为Get 请求都包含在 URL 里,且会被浏览器保存历史纪录,Post 不会,但是在抓包的情况下都是一样的。
    • +
    • Post 可以通过 request body来传输比 Get 更多的数据,Get 没有这个技术
    • +
    • URL有长度限制,会影响 Get 请求,但是这个长度限制是浏览器规定的,不是 RFC 规定的
    • +
    • Post 支持更多的编码类型且不对数据类型限制
    • +
    +

    常见状态码

    2XX 成功

    200 OK,表示从客户端发来的请求在服务器端被正确处理
    204 No content,表示请求成功,但响应报文不含实体的主体部分
    205 Reset Content,表示请求成功,但响应报文不含实体的主体部分,但是与 204 响应不同在于要求请求方重置内容
    206 Partial Content,进行范围请求

    +

    3XX 重定向

    301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
    302 found,临时性重定向,表示资源临时被分配了新的 URL
    303 see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源
    304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况
    307 temporary redirect,临时重定向,和302含义类似,但是期望客户端保持请求方法不变向新的地址发出请求

    +

    4XX 客户端错误

    400 bad request,请求报文存在语法错误
    401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
    403 forbidden,表示对请求资源的访问被服务器拒绝
    404 not found,表示在服务器上没有找到请求的资源

    +

    5XX 服务器错误

    500 internal sever error,表示服务器端在执行请求时发生了错误
    501 Not Implemented,表示服务器不支持当前请求所需要的某个功能
    503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求

    +

    HTTP 首部

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    通用字段作用
    Cache-Control控制缓存的行为
    Connection浏览器想要优先使用的连接类型,比如 keep-alive
    Date创建报文时间
    Pragma报文指令
    Via代理服务器相关信息
    Transfer-Encoding传输编码方式
    Upgrade要求客户端升级协议
    Warning在内容中可能存在错误
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    请求字段作用
    Accept能正确接收的媒体类型
    Accept-Charset能正确接收的字符集
    Accept-Encoding能正确接收的编码格式列表
    Accept-Language能正确接收的语言列表
    Expect期待服务端的指定行为
    From请求方邮箱地址
    Host服务器的域名
    If-Match两端资源标记比较
    If-Modified-Since本地资源未修改返回 304(比较时间)
    If-None-Match本地资源未修改返回 304(比较标记)
    User-Agent客户端信息
    Max-Forwards限制可被代理及网关转发的次数
    Proxy-Authorization向代理服务器发送验证信息
    Range请求某个内容的一部分
    Referer表示浏览器所访问的前一个页面
    TE传输编码方式
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    响应字段作用
    Accept-Ranges是否支持某些种类的范围
    Age资源在代理缓存中存在的时间
    ETag资源标识
    Location客户端重定向到某个 URL
    Proxy-Authenticate向代理服务器发送验证信息
    Server服务器名字
    WWW-Authenticate获取资源需要的验证信息
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    实体字段作用
    Allow资源的正确请求方式
    Content-Encoding内容的编码格式
    Content-Language内容使用的语言
    Content-Lengthrequest body 长度
    Content-Location返回数据的备用地址
    Content-MD5Base64加密格式的内容 MD5检验值
    Content-Range内容的位置范围
    Content-Type内容的媒体类型
    Expires内容的过期时间
    Last_modified内容的最后修改时间
    +

    HTTPS

    HTTPS 还是通过了 HTTP 来传输信息,但是信息通过 TLS 协议进行了加密。

    +

    TLS

    TLS 协议位于传输层之上,应用层之下。首次进行 TLS 协议传输需要两个 RTT ,接下来可以通过 Session Resumption 减少到一个 RTT

    +

    TLS 中使用了两种加密技术,分别为:对称加密非对称加密

    +

    对称加密:
    对称加密就是两边拥有相同的秘钥,两边都知道如何将密文加密解密。

    +

    非对称加密:
    有公钥私钥之分,公钥所有人都可以知道,可以将数据用公钥加密,但是将数据解密必须使用私钥解密,私钥只有分发公钥的一方才知道。

    +

    TLS 握手过程如下图:

    +
      +
    1. 客户端发送一个随机值,需要的协议和加密方式
    2. +
    3. 服务端收到客户端的随机值,自己也产生一个随机值,并根据客户端需求的协议和加密方式来使用对应的方式,发送自己的证书(如果需要验证客户端证书需要说明)
    4. +
    5. 客户端收到服务端的证书并验证是否有效,验证通过会再生成一个随机值,通过服务端证书的公钥去加密这个随机值并发送给服务端,如果服务端需要验证客户端证书的话会附带证书
    6. +
    7. 服务端收到加密过的随机值并使用私钥解密获得第三个随机值,这时候两端都拥有了三个随机值,可以通过这三个随机值按照之前约定的加密方式生成密钥,接下来的通信就可以通过该密钥来加密解密
    8. +
    +

    通过以上步骤可知,在 TLS 握手阶段,两端使用非对称加密的方式来通信,但是因为非对称加密损耗的性能比对称加密大,所以在正式传输数据时,两端使用对称加密的方式通信。

    +

    PS:以上说明的都是 TLS 1.2 协议的握手情况,在 1.3 协议中,首次建立连接只需要一个 RTT,后面恢复连接不需要 RTT 了。

    +

    HTTP 2.0

    HTTP 2.0 相比于 HTTP 1.X,可以说是大幅度提高了 web 的性能。

    +

    在 HTTP 1.X 中,为了性能考虑,我们会引入雪碧图、将小图内联、使用多个域名等等的方式。这一切都是因为浏览器限制了同一个域名下的请求数量,当页面中需要请求很多资源的时候,队头阻塞(Head of line blocking)会导致在达到最大请求数量时,剩余的资源需要等待其他资源请求完成后才能发起请求。

    +

    你可以通过 该链接 感受下 HTTP 2.0 比 HTTP 1.X 到底快了多少。

    +

    在 HTTP 1.X 中,因为队头阻塞的原因,你会发现请求是这样的
    lWJGkt.png
    在 HTTP 2.0 中,因为引入了多路复用,你会发现请求是这样的
    lWJa6g.png

    +

    二进制传输

    HTTP 2.0 中所有加强性能的核心点在于此。在之前的 HTTP 版本中,我们是通过文本的方式传输数据。在 HTTP 2.0 中引入了新的编码机制,所有传输的数据都会被分割,并采用二进制格式编码。

    +

    多路复用

    在 HTTP 2.0 中,有两个非常重要的概念,分别是帧(frame)流(stream)
    帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流。

    +

    多路复用,就是在一个 TCP 连接中可以存在多条流。换句话说,也就是可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能。

    +

    Header 压缩

    在 HTTP 1.X 中,我们使用文本的形式传输 header,在 header 携带 cookie 的情况下,可能每次都需要重复传输几百到几千的字节。

    +

    在 HTTP 2.0 中,使用了 HPACK 压缩格式对传输的 header 进行编码,减少了 header 的大小。并在两端维护了索引表,用于记录出现过的 header ,后面在传输过程中就可以传输已经记录过的 header 的键名,对端收到数据后就可以通过键名找到对应的值。

    +

    服务端 Push

    在 HTTP 2.0 中,服务端可以在客户端某个请求后,主动推送其他资源。

    +

    可以想象以下情况,某些资源客户端是一定会请求的,这时就可以采取服务端 push 的技术,提前给客户端推送必要的资源,这样就可以相对减少一点延迟时间。当然在浏览器兼容的情况下你也可以使用 prefetch 。

    +

    QUIC

    这是一个谷歌出品的基于 UDP 实现的同为传输层的协议,目标很远大,希望替代 TCP 协议。

    +
      +
    • 该协议支持多路复用,虽然 HTTP 2.0 也支持多路复用,但是下层仍是 TCP,因为 TCP 的重传机制,只要一个包丢失就得判断丢失包并且重传,导致发生队头阻塞的问题,但是 UDP 没有这个机制
    • +
    • 实现了自己的加密协议,通过类似 TCP 的 TFO 机制可以实现 0-RTT,当然 TLS 1.3 已经实现了 0-RTT 了
    • +
    • 支持重传和纠错机制(向前恢复),在只丢失一个包的情况下不需要重传,使用纠错机制恢复丢失的包
        +
      • 纠错机制:通过异或的方式,算出发出去的数据的异或值并单独发出一个包,服务端在发现有一个包丢失的情况下,通过其他数据包和异或值包算出丢失包
      • +
      • 在丢失两个包或以上的情况就使用重传机制,因为算不出来了
      • +
      +
    • +
    +

    DNS

    DNS 的作用就是通过域名查询到具体的 IP。

    +

    因为 IP 存在数字和英文的组合(IPv6),很不利于人类记忆,所以就出现了域名。你可以把域名看成是某个 IP 的别名,DNS 就是去查询这个别名的真正名称是什么。

    +

    在 TCP 握手之前就已经进行了 DNS 查询,这个查询是操作系统自己做的。当你在浏览器中想访问 www.google.com 时,会进行一下操作:

    +
      +
    1. 操作系统会首先在本地缓存中查询
    2. +
    3. 没有的话会去系统配置的 DNS 服务器中查询
    4. +
    5. 如果这时候还没得话,会直接去 DNS 根服务器查询,这一步查询会找出负责 com 这个一级域名的服务器
    6. +
    7. 然后去该服务器查询 google 这个二级域名
    8. +
    9. 接下来三级域名的查询其实是我们配置的,你可以给 www 这个域名配置一个 IP,然后还可以给别的三级域名配置一个 IP
    10. +
    +

    以上介绍的是 DNS 迭代查询,还有种是递归查询,区别就是前者是由客户端去做请求,后者是由系统配置的 DNS 服务器做请求,得到结果后将数据返回给客户端。

    +

    PS:DNS 是基于 UDP 做的查询。

    +

    从输入 URL 到页面加载完成的过程

    这是一个很经典的面试题,在这题中可以将本文讲得内容都串联起来。

    +
      +
    1. 首先做 DNS 查询,如果这一步做了智能 DNS 解析的话,会提供访问速度最快的 IP 地址回来
    2. +
    3. 接下来是 TCP 握手,应用层会下发数据给传输层,这里 TCP 协议会指明两端的端口号,然后下发给网络层。网络层中的 IP 协议会确定 IP 地址,并且指示了数据传输中如何跳转路由器。然后包会再被封装到数据链路层的数据帧结构中,最后就是物理层面的传输了
    4. +
    5. TCP 握手结束后会进行 TLS 握手,然后就开始正式的传输数据
    6. +
    7. 数据在进入服务端之前,可能还会先经过负责负载均衡的服务器,它的作用就是将请求合理的分发到多台服务器上,这时假设服务端会响应一个 HTML 文件
    8. +
    9. 首先浏览器会判断状态码是什么,如果是 200 那就继续解析,如果 400500 的话就会报错,如果 300 的话会进行重定向,这里会有个重定向计数器,避免过多次的重定向,超过次数也会报错
    10. +
    11. 浏览器开始解析文件,如果是 gzip 格式的话会先解压一下,然后通过文件的编码格式知道该如何去解码文件
    12. +
    13. 文件解码成功后会正式开始渲染流程,先会根据 HTML 构建 DOM 树,有 CSS 的话会去构建 CSSOM 树。如果遇到 script 标签的话,会判断是否存在 async 或者 defer ,前者会并行进行下载并执行 JS ,后者会先下载文件,然后等待 HTML 解析完成后顺序执行,如果以上都没有,就会阻塞住渲染流程直到 JS 执行完毕。遇到文件下载的会去下载文件,这里如果使用 HTTP 2.0 协议的话会极大的提高多图的下载效率。
    14. +
    15. 初始的 HTML 被完全加载和解析后会触发 DOMContentLoaded 事件
    16. +
    17. CSSOM 树和 DOM 树构建完成后会开始生成 Render 树,这一步就是确定页面元素的布局、样式等等诸多方面的东西
    18. +
    19. 在生成 Render 树的过程中,浏览器就开始调用 GPU 绘制,合成图层,将内容显示在屏幕上了
    20. +
    +]]>
    + + 前端技术 + 前端常见知识点整理 + + + 面试 + +
    + + 前端常见知识点整理 ---- Proxy + /2020/01/03/FE-guide-async-Proxy/ + +

    作者:李旭光
    引用请标明出处

    + +

    Proxy

    ProxyES6 中新增的功能,可以用来自定义对象中的操作

    +
    // js代码

    let p = new Proxy(target, handler);
    // `target` 代表需要添加代理的对象
    // `handler` 用来自定义对象中的操作
    +

    可以很方便的使用 Proxy 来实现一个数据绑定和监听

    +
    let onWatch = (obj, setBind, getLogger) => {
    let handler = {
    get(target, property, receiver) {
    getLogger(target, property)
    return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
    setBind(value);
    return Reflect.set(target, property, value);
    }
    };
    return new Proxy(obj, handler);
    };

    let obj = { a: 1 }
    let value
    let p = onWatch(obj, (v) => {
    value = v
    }, (target, property) => {
    console.log(`Get '${property}' = ${target[property]}`);
    })
    p.a = 2 // bind `value` to `2`
    p.a // -> Get 'a' = 2
    ]]>
    + + 前端技术 + 前端常见知识点整理 + + + 面试 + +
    + + reduce函数的妙用 ---- 实现map和filter + /2020/01/05/FE-guide-about-reduce/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    最近看了好多技术文章,好多的代码片段,突然发现好多的代码里都有 reduce ,感觉以前都没怎么关注过这个函数,为了弥补这个过失,打算专门写一篇文章说说它。

    +

    reduce 函数在 MDN 中是这样介绍的

    +
    +

    reduce() 方法对数组中的每个元素执行一个由 提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。

    +
    +

    说实话看了一脸懵逼,这上面说的叫人话?后来耐着性子看了一些代码后感觉有所理解,如果理解不对,还请斧正。
    首先看一下这里面几个关键词

    +

    * 每个元素: * 这就是遍历咯,没啥好说的
    提供的 reducer 函数:我哪有什么 reducer 函数呢,后来理解了,就是回调,有的地方呢叫 handler ,这里跟 reduce 配合就叫了 reducer
    升序执行:就是说是0,1,2下标这样的顺序执行啦。
    将其结果汇总为单个返回值:最后返回的是一个值,当然没说必须是什么类型。

    +

    这样解释完了以后,上面这句话就比较好懂了,简单说就是 reduce 里传一个回调函数,执行回调函数方法后返回一个值,就是这样。

    +
    +

    reducer 函数接收4个参数:

    +
    +
      +
    • Accumulator (acc) (累计器)
    • +
    • Current Value (cur) (当前值)
    • +
    • Current Index (idx) (当前索引)
    • +
    • Source Array (src) (源数组)
      您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
    • +
    +

    看上面的描述一定要注意,这些参数是 reducer 的参数,不是 reduce 的参数,一共有4个,通常用前两个情况比较多。

    +

    那么 reduce 函数呢实际上有两个参数,第二个还可以省略 reducerinitialValueinitialValue作为第一次调用 reducer 函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错,这点切记。

    +

    终于把前置知识讲清楚了,接下来,我们就用 reduce 搞点事情。

    +

    1. 使用 reduce 实现 map

    map 的话我不想再讲一遍,一句话带过,就是对回调函数中的元素进行加工后返回一个长度一样的新数组。
    那么该如何实现呢,我们来想想原理吧,刚才上面说过了,reduce执行过后的返回值会作为下一次执行的第一个参数放进去,那么就可以先用一个空数组作为初始值来接受每次需要处理的元素的集合,并返回回去,作为下一次传入的第一个参数,这样每次操作完之后就可以push到这个数组中,那么剩下要做的就是处理数组中每一项的函数了,我们叫他 handler

    +
    // js代码

    Array.prototype.reduceToMap = function (handler) { // 自定义 `map` 函数 `reduceToMap`
    return this.reduce((target, current, index) => { // this指向调用他的数组
    target.push(handler.call(this, current, index)) // 这里用了call方法,handler将接受两个参数 current和index
    return target; // 处理完成后返回新数组
    }, []) // 初始化空的新数组
    };
    +

    就这样我们就用 reduce,实现了 map 的功能,是不是很好用?

    +

    2. 使用 reduce 实现 filter

    filter 也是数组常用的方法,同样传入一个回调函数,处理结果返回true或false,最终 filter 会返回一个过滤后的函数。
    学会了上面的 map 的实现,实际上 filter 就会很简单

    +
    // js代码

    Array.prototype.reduceToFilter = function (handler) { // 还是自定义方法名
    return this.reduce((target, current, index) => {
    if (handler.call(this, current, index)) { // 这里注意 handler 要返回的是布尔类型的值
    target.push(current); // 符合条件就插入新数组
    } // 不符合就什么都不做
    return target; // 最后返回新数组
    }, []) // 初始化一个空数组
    };
    +

    日后在看到 reduce 的妙用之后还会来补充这篇文章,如果实在懒得写,我也会链接一下新文章,希望大家长期关注。

    +]]>
    + + 前端技术 + + + 面试 + 知识点 + +
    + + 前端常见知识点整理 ---- async 和 await + /2020/01/03/FE-guide-async-await/ + +

    作者:李旭光
    引用请标明出处

    + +

    async 和 await

    一个函数如果加上 async ,那么该函数就会返回一个 Promise

    +
    async function test() {
    return "1";
    }
    console.log(test()); // -> Promise {<resolved>: "1"}
    +

    可以把 async 看成将函数返回值使用 Promise.resolve() 包裹了下。
    await 只能在 async 函数中使用

    +
    function sleep() {
    return new Promise(resolve => {
    setTimeout(() => {
    console.log('finish')
    resolve("sleep");
    }, 2000);
    });
    }
    async function test() {
    let value = await sleep();
    console.log("object");
    }
    test()
    + +

    上面代码会先打印 finish 然后再打印 object 。因为 await 会等待 sleep 函数 resolve ,所以即使后面是同步代码,也不会先去执行同步代码再来执行异步代码。

    +

    asyncawait 相比直接使用 Promise 来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码。缺点在于滥用 await 可能会导致性能问题,因为 await 会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。

    +

    下面来看一个使用 await 的代码。

    +
    var a = 0
    var b = async () => {
    a = a + await 10
    console.log('2', a) // -> '2' 10
    a = (await 10) + a
    console.log('3', a) // -> '3' 20
    }
    b()
    a++
    console.log('1', a) // -> '1' 1
    + +

    对于以上代码你可能会有疑惑,这里说明下原理

    +
      +
    • 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为在 await 内部实现了 generatorsgenerators 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
    • +
    • 因为 await 是异步操作,遇到 await 就会立即返回一个 pending 状态的 Promise 对象,暂时返回执行代码的控制权,使得函数外的代码得以继续执行,所以会先执行 console.log('1', a)
    • +
    • 这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 10
    • +
    • 然后后面就是常规执行代码了
    • +
    +]]>
    + + 前端技术 + 前端常见知识点整理 + + + 面试 + +
    + + 前端常见知识点整理 ---- call, apply, bind 区别 + /2020/01/03/FE-guide-call-apply-bind/ + +

    作者:李旭光
    引用请标明出处

    + +

    call, apply, bind 区别

    首先说下前两者的异同。
    相同: callapply 都是为了解决改变 this 的指向。
    不同:传参的方式不同,除了第一个参数外, call 可以接收一个参数列表apply 只接受一个参数数组

    +
    // js代码
    let anObj = {
    value: 1
    }
    function getValue(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value)
    }
    getValue.call(anObj, 'lixuguang', '31')
    getValue.apply(anObj, ['lixuguang', '31'])
    +

    模拟实现 callapply

    可以从以下几点来考虑如何实现

    +
      +
    • 不传入第一个参数,那么默认为 window
    • +
    • 改变了 this 指向,让新的对象可以执行该函数。那么思路是否可以变成给新的对象添加一个函数,然后在执行完以后删除?
      // js代码

      Function.prototype.myCall = function (context) {
      var context = context || window // 有入参用入参,没有给 window
      context.fn = this // 给 context 添加一个属性,getValue.call(anObj, 'lixuguang', '31') => anObj.fn = getValue
      var args = [...arguments].slice(1) // 将 context 后面的参数取出来
      var result = context.fn(...args) // getValue.call(anObj, 'lixuguang', '31') => anObj.fn('yck', '24')
      delete context.fn // 删除 fn
      return result
      }
      +以上就是 call 的思路,apply 的实现也类似
      // js代码

      Function.prototype.Apply = function (context) {
      var context = context || window // 有入参用入参,没有给 window
      context.fn = this // 给 context 添加一个属性,getValue.call(anObj, 'lixuguang', '31') => anObj.fn = getValue
      var args = arguments[1] // 将 context 后面的参数取出来
      var result = args?context.fn(...args):context.fn() // getValue.call(anObj, ['lixuguang', '31']) => anObj.fn('yck', '24')
      delete context.fn // 删除 fn
      return result
      }
      +bind 和其他两个方法作用也是一致的,只是该方法会返回一个函数。并且我们可以通过 bind 实现柯里化。
    • +
    +

    同样的,也来模拟实现下 bind

    +
    // js代码

    Function.prototype.myBind = function (context) {
    if (typeof this !== 'function') {
    throw new TypeError('Error')
    }
    var _this = this
    var args = [...arguments].slice(1)

    return function F() { // 返回一个函数,这是 `bind` 和 `call` , `apply` 的区别
    if (this instanceof F) { // 因为返回了一个函数,我们可以 new F(),所以需要判断
    return new _this(...args, ...arguments)
    }
    return _this.apply(context, args.concat(...arguments))
    }
    }
    ]]>
    + + 前端技术 + 前端常见知识点整理 + + + 面试 + +
    + + 前端常见知识点整理 ---- 深浅拷贝 + /2020/01/03/FE-guide-copy/ + +

    作者:李旭光
    引用请标明出处

    + +

    深浅拷贝

    let a = {
    age: 1
    }
    let b = a
    a.age = 2
    console.log(b.age) // 2
    +

    从上述例子中我们可以发现,如果给一个变量赋值一个对象,那么两者的值会是同一个引用,其中一方改变,另一方也会相应改变。
    通常在开发中我们不希望出现这样的问题,我们可以使用浅拷贝来解决这个问题。

    +

    浅拷贝

    首先可以通过 Object.assign 来解决这个问题。

    +
    // js代码

    let a = {
    age: 1
    }
    let b = Object.assign({}, a)
    a.age = 2
    console.log(b.age) // => 1
    +

    当然我们也可以通过展开运算符(…)来解决

    +
    let a = {
    age: 1
    }
    let b = {...a}
    a.age = 2
    console.log(b.age) // => 1
    +

    我们还可以用很多简单的方法都能实现浅拷贝:

    +
    arr.slice();
    arr.concat();
    + +

    通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就需要使用到深拷贝了

    +
    // js代码

    let a = {
    age: 1,
    jobs: {
    first: 'FE'
    }
    }
    let b = {...a}
    a.jobs.first = 'native'
    console.log(b.jobs.first) // native
    +

    浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到刚开始的话题了,两者享有相同的引用。要解决这个问题,我们需要引入深拷贝。

    +

    深拷贝

    这个问题通常可以通过 JSON.parse(JSON.stringify(object)) 来解决,这也是最好用最简单的方法,俗称乞丐版。
    乞丐版

    +
    // js代码

    let a = {
    age: 1,
    jobs: {
    first: 'FE'
    }
    }
    let b = JSON.parse(JSON.stringify(a))
    a.jobs.first = 'native'
    console.log(b.jobs.first) // FE
    +

    但是该方法也是有局限性的:

    +
      +
    • 会忽略 undefined
    • +
    • 会忽略 symbol
    • +
    • 不能序列化函数
    • +
    • 不能解决循环引用的对象
    • +
    +

    举个栗子:

    +
    // js代码

    let obj = {
    a: 1,
    b: {
    c: 2,
    d: 3,
    },
    }

    obj.c = obj.b
    obj.e = obj.a
    obj.b.c = obj.c
    obj.b.d = obj.b
    obj.b.e = obj.b.c

    let newObj = JSON.parse(JSON.stringify(obj))
    console.log(newObj) // => Uncaught TypeError: Converting circular structure to JSON
    +

    如果你有这么一个循环引用对象,你会发现你不能通过该方法深拷贝
    在遇到函数、 undefined 或者 symbol 的时候,该对象也不能正常的序列化

    +
    // js代码

    let a = {
    age: undefined,
    sex: Symbol('fmale'),
    jobs: function() {},
    name: 'lixuguang'
    }
    let b = JSON.parse(JSON.stringify(a))
    console.log(b) // => {name: "lixuang"}
    + +

    你会发现在上述情况中,该方法会忽略掉函数和 undefined
    但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题,并且该函数是内置函数中处理深拷贝性能最快的。

    +

    那么是否可以解决函数和循环引用的问题呢?答案是肯定可以解决,接下来是基础版本的改造
    基础版

    +
    function myClone(target){
    if(typeof target === 'object'){ // 判断传入目标是否是object类型
    let cloneTarget = {}; // 创建克隆对象
    for(const key in target){ // 遍历目标对象
    cloneTarget[key] = myClone(target[key]) // 递归调用 clone 方法
    }
    return cloneTarget;
    } else {
    return target // 如果不是 object 返回
    }
    }
    +

    写到这里已经可以帮助你应付一些面试官考察你的递归解决问题的能力。但是显然,这个深拷贝函数还是有一些问题。
    这里只考虑了对象,没有考虑数组。
    下面我们来做一个强化版的深拷贝,同时考虑对象、数组还有循环引用的问题。
    强化版

    +
    function myClone(target, map = new WeakMap()) { // WeakMap => 键对象弱引用, 可被垃圾回收
    if(typeof target === 'object'){ // 判断是否是对象
    let cloneTarget = Array.isArray(target) ? [] : {}; // 判断是是数组还是对象
    if(map.get(target)) {
    return target;
    }

    map.set(target, cloneTarget);

    for(const key in target) {
    cloneTarget[key] = myClone(target[key], map)
    }
    return cloneTarget;
    } else {
    return target;
    }
    }
    +

    当然如果你的数据中含有以上三种情况下,可以使用 lodash 的深拷贝函数。
    如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel

    +
    // js代码

    function structuralClone(obj) {
    return new Promise(resolve => {
    const {port1, port2} = new MessageChannel();
    port2.onmessage = ev => resolve(ev.data);
    port1.postMessage(obj);
    });
    }

    var obj = {
    a: 1,
    b: {
    c: b
    }
    }
    // 注意该方法是异步的
    // 可以处理 undefined 和循环引用对象
    (async () => {
    const clone = await structuralClone(obj)
    })()
    + +

    深拷贝实现方式2,可以深拷贝 functionsymbol,等等,堪称终极版
    终极版

    +
    // js代码

    const mapTag = '[object Map]';
    const setTag = '[object Set]';
    const arrayTag = '[object Array]';
    const objectTag = '[object Object]';
    const argsTag = '[object Arguments]';

    const boolTag = '[object Boolean]';
    const dateTag = '[object Date]';
    const numberTag = '[object Number]';
    const stringTag = '[object String]';
    const symbolTag = '[object Symbol]';
    const errorTag = '[object Error]';
    const regexpTag = '[object RegExp]';
    const funcTag = '[object Function]';

    const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];
    function forEach(array, iteratee) {
    let index = -1;
    const length = array.length;
    while (++index < length) {
    iteratee(array[index], index);
    }
    return array;
    }

    function isObject(target) {
    const type = typeof target;
    return target !== null && (type === 'object' || type === 'function');
    }

    function getType(target) {
    return Object.prototype.toString.call(target);
    }

    function getInit(target) {
    const Ctor = target.constructor;
    return new Ctor();
    }

    function cloneSymbol(targe) {
    return Object(Symbol.prototype.valueOf.call(targe));
    }

    function cloneReg(targe) {
    const reFlags = /\w*$/;
    const result = new targe.constructor(targe.source, reFlags.exec(targe));
    result.lastIndex = targe.lastIndex;
    return result;
    }

    function cloneFunction(func) {
    const bodyReg = /(?<={)(.|\n)+(?=})/m;
    const paramReg = /(?<=\().+(?=\)\s+{)/;
    const funcString = func.toString();
    if (func.prototype) {
    const param = paramReg.exec(funcString);
    const body = bodyReg.exec(funcString);
    if (body) {
    if (param) {
    const paramArr = param[0].split(',');
    return new Function(...paramArr, body[0]);
    } else {
    return new Function(body[0]);
    }
    } else {
    return null;
    }
    } else {
    return eval(funcString);
    }
    }

    function cloneOtherType(targe, type) {
    const Ctor = targe.constructor;
    switch (type) {
    case boolTag:
    case numberTag:
    case stringTag:
    case errorTag:
    case dateTag:
    return new Ctor(targe);
    case regexpTag:
    return cloneReg(targe);
    case symbolTag:
    return cloneSymbol(targe);
    case funcTag:
    return cloneFunction(targe);
    default:
    return null;
    }
    }

    function clone(target, map = new WeakMap()) {
    // 克隆原始类型
    if (!isObject(target)) {
    return target;
    }

    // 初始化
    const type = getType(target);
    let cloneTarget;
    if (deepTag.includes(type)) {
    cloneTarget = getInit(target, type);
    } else {
    return cloneOtherType(target, type);
    }

    // 防止循环引用
    if (map.get(target)) {
    return target;
    }
    map.set(target, cloneTarget);

    // 克隆set
    if (type === setTag) {
    target.forEach(value => {
    cloneTarget.add(clone(value));
    });
    return cloneTarget;
    }

    // 克隆map
    if (type === mapTag) {
    target.forEach((value, key) => {
    cloneTarget.set(key, clone(value));
    });
    return cloneTarget;
    }

    // 克隆对象和数组
    const keys = type === arrayTag ? undefined : Object.keys(target);
    forEach(keys || target, (value, key) => {
    if (keys) {
    key = value;
    }
    cloneTarget[key] = clone(target[key], map);
    });

    return cloneTarget;
    }

    // 调用方法
    clone(target);
    ]]>
    + + 前端技术 + 前端常见知识点整理 + + + 面试 + +
    + + 前端常见知识点整理 ---- 柯里化 currying + /2020/01/05/FE-guide-currying/ + +

    作者:李旭光
    引用请标明出处

    + +

    定义

    柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

    +

    柯里化,可以理解为提前接收部分参数,延迟执行,不立即输出结果,而是返回一个接受剩余参数的函数。因为这样的特性,也被称为部分计算函数。

    +

    通俗易懂的解释:用闭包把参数保存起来,当参数的数量足够执行函数了,就开始执行函数。柯里化,是一个逐步接收参数的过程。在接下来的剖析中,你会深刻体会到这一点。

    +
    // js代码

    function currying(fn){
    var allArgs = [];

    return function next(){
    var args = [].slice.call(arguments); // 拆成数组元素

    if(args.length > 0){
    allArgs = allArgs.concat(args);
    return next;
    }else{
    return fn.apply(null, allArgs);
    }
    }
    }
    +

    我们来一个简单的实例验证一下:

    +
    // js代码

    var add = currying(function(){
    var sum = 0;
    for(var i = 0; i < arguments.length; i++){
    sum += arguments[i];
    }
    return sum;
    });

    add(1)(2, 3)(4)() // => 10
    + +

    应用场景

    参数复用

    // js代码

    function getUrl(domain, protocol, path) {
    return protocol + "://" + domain + "/" + path;
    }

    var page1 = getUrl('http', 'lixuguang.github.io', 'page1.html');
    var page2 = getUrl('http', 'lixuguang.github.io', 'page2.html');
    +

    我们使用currying来简化它:

    +
    var conardliSite = currying(getUrl)
    var page1 = conardliSite('page1.html')('http', 'lixuguang.github.io')();
    ]]>
    + + 前端技术 + 前端常见知识点整理 + + + 面试 + +
    + + 前端常见知识点整理 ---- 闭包 + /2020/01/02/FE-guide-Closure/ + +

    作者:李旭光
    引用请标明出处

    + +

    闭包Closure

    闭包的定义很简单:函数 A 返回了一个函数 B ,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。

    +
    // js代码

    function A() {
    let a = 1
    function B() {
    console.log(a)
    }
    return B
    }
    +

    你是否会疑惑,为什么函数 A 已经弹出调用栈了,为什么函数 B 还能引用到函数 A 中的变量。因为函数 A 中的变量这时候是存储在堆上的。现在的 JS 引擎可以通过逃逸分析辨别出哪些变量需要存储在堆上,哪些需要存储在栈上。

    +

    经典面试题,循环中使用闭包解决 var 定义函数的问题

    +
    // js代码

    for ( var i=1; i<=5; i++) {
    setTimeout( function timer() {
    console.log( i );
    }, i*1000 );
    }
    +

    首先因为 setTimeout 是个异步函数,所有会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。

    +

    解决办法两种,第一种使用闭包

    +
    // js代码

    for (var i = 1; i <= 5; i++) {
    (function(j) {
    setTimeout(function timer() {
    console.log(j);
    }, j * 1000);
    })(i);
    }
    +

    第二种就是使用 setTimeout 的第三个参数(附加参数,传给 setTimeout 中的函数)

    +
    // js代码

    for ( var i=1; i<=5; i++) {
    setTimeout( function timer(j) {
    console.log( j );
    }, i*1000, i);
    }
    + +

    第三种就是使用 let 定义 i

    +
    // js代码

    for ( let i=1; i<=5; i++) {
    setTimeout( function timer() {
    console.log( i );
    }, i*1000 );
    }
    +

    因为对于 let 来说,他会创建一个块级作用域,相当于

    +
    // js代码

    { // 形成块级作用域
    let i = 0
    {
    let ii = i
    setTimeout( function timer() {
    console.log( ii );
    }, i*1000 );
    }
    i++
    {
    let ii = i
    }
    i++
    {
    let ii = i
    }
    ...
    }
    ]]>
    + + 前端技术 + 前端常见知识点整理 + + + 面试 + 知识点 + +
    + + 前端常见知识点整理 ---- 函数防抖和节流 + /2020/01/03/FE-guide-debounce-throttle/ + +

    作者:李旭光
    引用请标明出处

    + +

    函数防抖和节流

    在前端开发的过程中,我们经常会需要绑定一些持续触发的事件,如 resize、scroll、mousemove 等等,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。

    +

    通常这种情况下我们怎么去解决的呢?一般来讲,防抖和节流是比较好的解决方案。

    +

    防抖

    你是否在日常开发中遇到一个问题,在滚动事件中需要做个复杂计算或者实现一个按钮的防二次点击操作。

    +

    这些需求都可以通过函数防抖动来实现。尤其是第一个需求,如果在频繁的事件回调中做复杂计算,很有可能导致页面卡顿,不如将多次计算合并为一次计算,只在一个精确点做操作。

    +

    PS:防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的情况下只会调用一次,而节流的 情况会每隔一定时间(参数wait)调用函数。

    +

    我们先来看一个袖珍版的防抖理解一下防抖的实现:

    +
    // js代码

    // func是用户传入需要防抖的函数
    // wait是等待时间
    const debounce = (func, wait = 50) => {
    // 缓存一个定时器id
    let timer = 0
    // 这里返回的函数是每次用户实际调用的防抖函数
    // 如果已经设定过定时器了就清空上一次的定时器
    // 开始一个新的定时器,延迟执行用户传入的方法
    return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
    func.apply(this, args)
    }, wait)
    }
    }
    // 不难看出如果用户调用该函数的间隔小于wait的情况下,上一次的时间还未到就被清除了,并不会执行函数
    +

    这是一个简单版的防抖,但是有缺陷,这个防抖只能在最后调用。一般的防抖会有immediate选项,表示是否立即调用。这两者的区别,举个栗子来说:

    +
      +
    • 例如在搜索引擎搜索问题的时候,我们当然是希望用户输入完最后一个字才调用查询接口,这个时候适用延迟执行的防抖函数,它总是在一连串(间隔小于wait的)函数触发之后调用。
    • +
    • 例如用户给某系统表单点提交的时候,我们希望用户点第一下的时候就去调用接口,这个情况适用立即执行的防抖函数,它总是在第一次调用,并且下一次调用必须与前一次调用的时间间隔大于wait才会触发。
    • +
    +
    // js代码

    // 这个是用来获取当前时间戳的
    function now() {
    return +new Date()
    }
    /**
    * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
    *
    * @param {function} func 回调函数
    * @param {number} wait 表示时间窗口的间隔
    * @param {boolean} immediate 设置为ture时,是否立即调用函数
    * @return {function} 返回客户调用函数
    */
    function debounce (func, wait = 50, immediate = true) {
    let timer, context, args

    // 延迟执行函数
    const later = () => setTimeout(() => {
    // 延迟函数执行完毕,清空缓存的定时器序号
    timer = null
    // 延迟执行的情况下,函数会在延迟函数中执行
    // 使用到之前缓存的参数和上下文
    if (!immediate) {
    func.apply(context, args)
    context = args = null
    }
    }, wait)

    // 这里返回的函数是每次实际调用的函数
    return function(...params) {
    // 如果没有创建延迟执行函数(later),就创建一个
    if (!timer) {
    timer = later()
    // 如果是立即执行,调用函数
    // 否则缓存参数和调用上下文
    if (immediate) {
    func.apply(this, params)
    } else {
    context = this
    args = params
    }
    // 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
    // 这样做延迟函数会重新计时
    } else {
    clearTimeout(timer)
    timer = later()
    }
    }
    }
    +

    整体函数实现的不难,总结一下。

    +
      +
    • 对于按钮防点击来说的实现:如果函数是立即执行的,就立即调用,如果函数是延迟执行的,就缓存上下文和参数,放到延迟函数中去执行。一旦我开始一个定时器,只要我定时器还在,你每次点击我都重新计时。一旦你点累了,定时器时间到,定时器重置为 null,就可以再次点击了。
    • +
    • 对于延时执行函数来说的实现:清除定时器ID,如果是延迟调用就调用函数
    • +
    +

    节流

    防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。

    +
    // js代码

    /**
    * underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait
    *
    * @param {function} func 回调函数
    * @param {number} wait 表示时间窗口的间隔
    * @param {object} options 如果想忽略开始函数的的调用,传入{leading: false}。
    * 如果想忽略结尾函数的调用,传入{trailing: false}
    * 两者不能共存,否则函数不能执行
    * @return {function} 返回客户调用函数
    */
    _.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;
    // 之前的时间戳
    var previous = 0;
    // 如果 options 没传则设为空对象
    if (!options) options = {};
    // 定时器回调函数
    var later = function() {
    // 如果设置了 leading,就将 previous 设为 0
    // 用于下面函数的第一个 if 判断
    previous = options.leading === false ? 0 : _.now();
    // 置空一是为了防止内存泄漏,二是为了下面的定时器判断
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
    };
    return function() {
    // 获得当前时间戳
    var now = _.now();
    // 首次进入前者肯定为 true
    // 如果需要第一次不执行函数
    // 就将上次时间戳设为当前的
    // 这样在接下来计算 remaining 的值时会大于0
    if (!previous && options.leading === false) previous = now;
    // 计算剩余时间
    var remaining = wait - (now - previous);
    context = this;
    args = arguments;
    // 如果当前调用已经大于上次调用时间 + wait
    // 或者用户手动调了时间
    // 如果设置了 trailing,只会进入这个条件
    // 如果没有设置 leading,那么第一次会进入这个条件
    // 还有一点,你可能会觉得开启了定时器那么应该不会进入这个 if 条件了
    // 其实还是会进入的,因为定时器的延时
    // 并不是准确的时间,很可能你设置了2秒
    // 但是他需要2.2秒才触发,这时候就会进入这个条件
    if (remaining <= 0 || remaining > wait) {
    // 如果存在定时器就清理掉否则会调用二次回调
    if (timeout) {
    clearTimeout(timeout);
    timeout = null;
    }
    previous = now;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {
    // 判断是否设置了定时器和 trailing
    // 没有的话就开启一个定时器
    // 并且不能不能同时设置 leading 和 trailing
    timeout = setTimeout(later, remaining);
    }
    return result;
    };
    };
    ]]>
    + + 前端技术 + 前端常见知识点整理 + + + 面试 + +
    + + 计算机通识 ---- 数据结构 + /2020/01/09/FE-guide-data-structure/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    网络相关的知识虽然平时在编码阶段很少遇到,但是并不是说它不重要,尤其是前端优化层面,能从网络传输过程优化的点有很多,所以,我们需要好好掌握网络相关的知识,接下来,我将会将我了解到的网络相关的知识点进行一一整理

    +

    栈 Heap

    +

    栈是一个线性结构,在计算机中是一个相当常见的数据结构。
    栈的特点是只能在某一端添加或删除数据,遵循先进后出(FILO)的原则

    +
    +

    实现

    每种数据结构都可以用很多种方式来实现,其实可以把栈看成是数组的一个子集,所以这里使用数组来实现

    +
    // js代码
    class Stack {
    constructor() {
    this.stack = []
    }
    push(item) {
    this.stack.push(item)
    }
    pop() {
    this.stack.pop()
    }
    peek() { // 取最后一项
    return this.stack[this.getCount() - 1]
    }
    getCount() {
    return this.stack.length
    }
    isEmpty() {
    return this.getCount() === 0
    }
    }
    +

    应用

    选取了 LeetCode 上序号为 20 的题目

    +

    题意是匹配括号,可以通过栈的特性来完成这道题目

    +
    var isValid = function(str) {
    let map = {
    '(': -1,
    ')': 1,
    '[': -2,
    ']': 2,
    '{': -3,
    '}': 3
    }
    let stack = [] // 空数组
    for (let i = 0; i < str.length; i++) { // 遍历字符串每个字符
    if (map[str[i]] < 0) { // 如果是左边括号,入栈
    stack.push(str[i])
    } else { // 否则出栈,判断左右括号加到一起是不是0
    let last = stack.pop()
    if (map[last] + map[str[i]] != 0) return false
    }
    }
    if (stack.length > 0) return false // 循环完成后,判断数组中时候还有剩下的,有剩下的说明括号没闭合
    return true // 否则没剩下的,都闭合了
    }
    + +

    队列

    +

    队列一个线性结构,特点是在某一端添加数据,在另一端删除数据,遵循先进先出(FIFO)的原则。

    +
    +

    实现

    这里会讲解两种实现队列的方式,分别是单链队列循环队列

    +
      +
    • 链队列即队列的链式存储结构,结构上就是一个单链表,但数据只能是头进尾出。链式结构更加的灵活,特别是在存储空间上,基本不会出现溢出的情况,所以不用像循环队列一样判断队列是否已满,且空间的利用率相对较高。
        +
      • 链队列front指向头结点,头结点不存储数据,rear指向队尾结点。
      • +
      +
    • +
    +
      +
    • 循环队列即为头尾相接的队列,它的最大存储空间和顺序队列一样由数组界定,但队列的长度并不一定等同于数组的长度;循环队列的队首和队尾分别由两个指针front、rear标识,于是这样就能做到首尾相接。

      +
    • +
    • 链队列:为操作方便,给链队列添加一个头结点

      +
    • +
    • 循环队列:附设两个指针front和rear分别指示队列头元素及尾元素的位置,每当插入新的队尾元素是,尾指针加1;每当删除队列头元素是,头指针加1

      +
        +
      • 如果用循环队列,则必须设定一个最大队列长度;若无法确定最大长度,则宜采用链队列。
      • +
      +
    • +
    +

    单链队列

    // js代码

    class Queue {
    constructor() {
    this.queue = []
    }
    enQueue(item) {
    this.queue.push(item)
    }
    deQueue() {
    return this.queue.shift()
    }
    getHeader() {
    return this.queue[0]
    }
    getLength() {
    return this.queue.length
    }
    isEmpty() {
    return this.getLength() === 0
    }
    }
    +

    因为单链队列在出队操作的时候需要 O(n) 的时间复杂度,所以引入了循环队列。
    循环队列的出队操作平均是 O(1) 的时间复杂度。

    +

    循环队列

    // js代码

    class SqQueue {
    constructor(length) {
    this.queue = new Array(length + 1)
    // 队头
    this.first = 0
    // 队尾
    this.last = 0
    // 当前队列大小
    this.size = 0
    }
    enQueue(item) {
    // 判断队尾 + 1 是否为队头
    // 如果是就代表需要扩容数组
    // % this.queue.length 是为了防止数组越界
    if (this.first === (this.last + 1) % this.queue.length) {
    this.resize(this.getLength() * 2 + 1)
    }
    this.queue[this.last] = item
    this.size++
    this.last = (this.last + 1) % this.queue.length
    }
    deQueue() {
    if (this.isEmpty()) {
    throw Error('Queue is empty')
    }
    let r = this.queue[this.first]
    this.queue[this.first] = null
    this.first = (this.first + 1) % this.queue.length
    this.size--
    // 判断当前队列大小是否过小
    // 为了保证不浪费空间,在队列空间等于总长度四分之一时
    // 且不为 2 时缩小总长度为当前的一半
    if (this.size === this.getLength() / 4 && this.getLength() / 2 !== 0) {
    this.resize(this.getLength() / 2)
    }
    return r
    }
    getHeader() {
    if (this.isEmpty()) {
    throw Error('Queue is empty')
    }
    return this.queue[this.first]
    }
    getLength() {
    return this.queue.length - 1
    }
    isEmpty() {
    return this.first === this.last
    }
    resize(length) {
    let q = new Array(length)
    for (let i = 0; i < length; i++) {
    q[i] = this.queue[(i + this.first) % this.queue.length]
    }
    this.queue = q
    this.first = 0
    this.last = this.size
    }
    }
    + +

    链表

    +

    链表是一个线性结构,同时也是一个天然的递归结构。链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。

    +
    +

    实现

    单向链表

    +
    // js代码

    class Node {
    constructor(v, next) {
    this.value = v
    this.next = next
    }
    }
    class LinkList {
    constructor() {
    // 链表长度
    this.size = 0
    // 虚拟头部
    this.dummyNode = new Node(null, null)
    }
    find(header, index, currentIndex) {
    if (index === currentIndex) return header
    return this.find(header.next, index, currentIndex + 1)
    }
    addNode(v, index) {
    this.checkIndex(index)
    // 当往链表末尾插入时,prev.next 为空
    // 其他情况时,因为要插入节点,所以插入的节点
    // 的 next 应该是 prev.next
    // 然后设置 prev.next 为插入的节点
    let prev = this.find(this.dummyNode, index, 0)
    prev.next = new Node(v, prev.next)
    this.size++
    return prev.next
    }
    insertNode(v, index) {
    return this.addNode(v, index)
    }
    addToFirst(v) {
    return this.addNode(v, 0)
    }
    addToLast(v) {
    return this.addNode(v, this.size)
    }
    removeNode(index, isLast) {
    this.checkIndex(index)
    index = isLast ? index - 1 : index
    let prev = this.find(this.dummyNode, index, 0)
    let node = prev.next
    prev.next = node.next
    node.next = null
    this.size--
    return node
    }
    removeFirstNode() {
    return this.removeNode(0)
    }
    removeLastNode() {
    return this.removeNode(this.size, true)
    }
    checkIndex(index) {
    if (index < 0 || index > this.size) throw Error('Index error')
    }
    getNode(index) {
    this.checkIndex(index)
    if (this.isEmpty()) return
    return this.find(this.dummyNode, index, 0).next
    }
    isEmpty() {
    return this.size === 0
    }
    getSize() {
    return this.size
    }
    }
    + +

    二叉树

    树拥有很多种结构,二叉树是树中最常用的结构,同时也是一个天然的递归结构。

    +

    二叉树拥有一个根节点,每个节点至多拥有两个子节点,分别为:左节点和右节点。树的最底部节点称之为叶节点,当一颗树的叶数量数量为满时,该树可以称之为满二叉树。

    +

    二分搜索树

    二分搜索树也是二叉树,拥有二叉树的特性。但是区别在于二分搜索树每个节点的值都比他的左子树的值大,比右子树的值小。

    +

    这种存储方式很适合于数据搜索。如下图所示,当需要查找 6 的时候,因为需要查找的值比根节点的值大,所以只需要在根节点的右子树上寻找,大大提高了搜索效率。

    +

    实现

    // js代码

    class Node {
    constructor(value) {
    this.value = value
    this.left = null
    this.right = null
    }
    }
    class BST {
    constructor() {
    this.root = null
    this.size = 0
    }
    getSize() {
    return this.size
    }
    isEmpty() {
    return this.size === 0
    }
    addNode(v) {
    this.root = this._addChild(this.root, v)
    }
    // 添加节点时,需要比较添加的节点值和当前
    // 节点值的大小
    _addChild(node, v) {
    if (!node) {
    this.size++
    return new Node(v)
    }
    if (node.value > v) {
    node.left = this._addChild(node.left, v)
    } else if (node.value < v) {
    node.right = this._addChild(node.right, v)
    }
    return node
    }
    }
    + +

    以上是最基本的二分搜索树实现,接下来实现树的遍历。

    +

    对于树的遍历来说,有三种遍历方法,分别是先序遍历中序遍历后序遍历

    +

    三种遍历的区别在于何时访问节点。在遍历树的过程中,每个节点都会遍历三次,分别是遍历到自己,遍历左子树和遍历右子树。如果需要实现先序遍历,那么只需要第一次遍历到节点时进行操作即可。

    +

    以下都是递归实现.

    +
    // js代码

    // 先序遍历可用于打印树的结构
    // 先序遍历先访问根节点,然后访问左节点,最后访问右节点。
    preTraversal() {
    this._pre(this.root)
    }
    _pre(node) {
    if (node) {
    console.log(node.value)
    this._pre(node.left)
    this._pre(node.right)
    }
    }
    // 中序遍历可用于排序
    // 对于 BST 来说,中序遍历可以实现一次遍历就
    // 得到有序的值
    // 中序遍历表示先访问左节点,然后访问根节点,最后访问右节点。
    midTraversal() {
    this._mid(this.root)
    }
    _mid(node) {
    if (node) {
    this._mid(node.left)
    console.log(node.value)
    this._mid(node.right)
    }
    }
    // 后序遍历可用于先操作子节点
    // 再操作父节点的场景
    // 后序遍历表示先访问左节点,然后访问右节点,最后访问根节点。
    backTraversal() {
    this._back(this.root)
    }
    _back(node) {
    if (node) {
    this._back(node.left)
    this._back(node.right)
    console.log(node.value)
    }
    }
    + +

    以上的这几种遍历都可以称之为深度遍历,对应的还有种遍历叫做广度遍历,也就是一层层地遍历树。对于广度遍历来说,我们需要利用之前讲过的队列结构来完成。

    +
    // js代码

    breadthTraversal() {
    if (!this.root) return null
    let q = new Queue()
    // 将根节点入队
    q.enQueue(this.root)
    // 循环判断队列是否为空,为空
    // 代表树遍历完毕
    while (!q.isEmpty()) {
    // 将队首出队,判断是否有左右子树
    // 有的话,就先左后右入队
    let n = q.deQueue()
    console.log(n.value)
    if (n.left) q.enQueue(n.left)
    if (n.right) q.enQueue(n.right)
    }
    }
    +

    接下来先介绍如何在树中寻找最小值或最大数。因为二分搜索树的特性,所以最小值一定在根节点的最左边,最大值相反

    +
    // js代码

    getMin() {
    return this._getMin(this.root).value
    }
    _getMin(node) {
    if (!node.left) return node
    return this._getMin(node.left)
    }
    getMax() {
    return this._getMax(this.root).value
    }
    _getMax(node) {
    if (!node.right) return node
    return this._getMin(node.right)
    }
    + +

    向上取整和向下取整,这两个操作是相反的,所以代码也是类似的,这里只介绍如何向下取整。既然是向下取整,那么根据二分搜索树的特性,值一定在根节点的左侧。只需要一直遍历左子树直到当前节点的值不再大于等于需要的值,然后判断节点是否还拥有右子树。如果有的话,继续上面的递归判断。

    +
    // js代码

    floor(v) {
    let node = this._floor(this.root, v)
    return node ? node.value : null
    }
    _floor(node, v) {
    if (!node) return null
    if (node.value === v) return v
    // 如果当前节点值还比需要的值大,就继续递归
    if (node.value > v) {
    return this._floor(node.left, v)
    }
    // 判断当前节点是否拥有右子树
    let right = this._floor(node.right, v)
    if (right) return right
    return node
    }
    +

    排名,这是用于获取给定值的排名或者排名第几的节点的值,这两个操作也是相反的,所以这个只介绍如何获取排名第几的节点的值。对于这个操作而言,我们需要略微的改造点代码,让每个节点拥有一个 size 属性。该属性表示该节点下有多少子节点(包含自身)。

    +
    // js代码

    class Node {
    constructor(value) {
    this.value = value
    this.left = null
    this.right = null
    // 修改代码
    this.size = 1
    }
    }
    // 新增代码
    _getSize(node) {
    return node ? node.size : 0
    }
    _addChild(node, v) {
    if (!node) {
    return new Node(v)
    }
    if (node.value > v) {
    // 修改代码
    node.size++
    node.left = this._addChild(node.left, v)
    } else if (node.value < v) {
    // 修改代码
    node.size++
    node.right = this._addChild(node.right, v)
    }
    return node
    }
    select(k) {
    let node = this._select(this.root, k)
    return node ? node.value : null
    }
    _select(node, k) {
    if (!node) return null
    // 先获取左子树下有几个节点
    let size = node.left ? node.left.size : 0
    // 判断 size 是否大于 k
    // 如果大于 k,代表所需要的节点在左节点
    if (size > k) return this._select(node.left, k)
    // 如果小于 k,代表所需要的节点在右节点
    // 注意这里需要重新计算 k,减去根节点除了右子树的节点数量
    if (size < k) return this._select(node.right, k - size - 1)
    return node
    }
    + +

    接下来讲解的是二分搜索树中最难实现的部分:删除节点。因为对于删除节点来说,会存在以下几种情况

    +
      +
    • 需要删除的节点没有子树
    • +
    • 需要删除的节点只有一条子树
    • +
    • 需要删除的节点有左右两条树
    • +
    +

    对于前两种情况很好解决,但是第三种情况就有难度了,所以先来实现相对简单的操作:删除最小节点,对于删除最小节点来说,是不存在第三种情况的,删除最大节点操作是和删除最小节点相反的,所以这里也就不再赘述。

    +
    // js代码

    delectMin() {
    this.root = this._delectMin(this.root)
    console.log(this.root)
    }
    _delectMin(node) {
    // 一直递归左子树
    // 如果左子树为空,就判断节点是否拥有右子树
    // 有右子树的话就把需要删除的节点替换为右子树
    if ((node != null) & !node.left) return node.right
    node.left = this._delectMin(node.left)
    // 最后需要重新维护下节点的 `size`
    node.size = this._getSize(node.left) + this._getSize(node.right) + 1
    return node
    }
    + +

    最后讲解的就是如何删除任意节点了。对于这个操作,T.Hibbard 在 1962 年提出了解决这个难题的办法,也就是如何解决第三种情况。

    +

    当遇到这种情况时,需要取出当前节点的后继节点(也就是当前节点右子树的最小节点)来替换需要删除的节点。然后将需要删除节点的左子树赋值给后继结点,右子树删除后继结点后赋值给他。

    +

    你如果对于这个解决办法有疑问的话,可以这样考虑。因为二分搜索树的特性,父节点一定比所有左子节点大,比所有右子节点小。那么当需要删除父节点时,势必需要拿出一个比父节点大的节点来替换父节点。这个节点肯定不存在于左子树,必然存在于右子树。然后又需要保持父节点都是比右子节点小的,那么就可以取出右子树中最小的那个节点来替换父节点。

    +
    // js代码

    delect(v) {
    this.root = this._delect(this.root, v)
    }
    _delect(node, v) {
    if (!node) return null
    // 寻找的节点比当前节点小,去左子树找
    if (node.value < v) {
    node.right = this._delect(node.right, v)
    } else if (node.value > v) {
    // 寻找的节点比当前节点大,去右子树找
    node.left = this._delect(node.left, v)
    } else {
    // 进入这个条件说明已经找到节点
    // 先判断节点是否拥有拥有左右子树中的一个
    // 是的话,将子树返回出去,这里和 `_delectMin` 的操作一样
    if (!node.left) return node.right
    if (!node.right) return node.left
    // 进入这里,代表节点拥有左右子树
    // 先取出当前节点的后继结点,也就是取当前节点右子树的最小值
    let min = this._getMin(node.right)
    // 取出最小值后,删除最小值
    // 然后把删除节点后的子树赋值给最小值节点
    min.right = this._delectMin(node.right)
    // 左子树不动
    min.left = node.left
    node = min
    }
    // 维护 size
    node.size = this._getSize(node.left) + this._getSize(node.right) + 1
    return node
    }
    + +

    AVL 树

    +

    二分搜索树实际在业务中是受到限制的,因为并不是严格的 O(logN),在极端情况下会退化成链表,比如加入一组升序的数字就会造成这种情况。

    +
    +
    +

    AVL 树改进了二分搜索树,在 AVL 树中任意节点的左右子树的高度差都不大于 1,这样保证了时间复杂度是严格的 O(logN)。基于此,对 AVL 树增加或删除节点时可能需要旋转树来达到高度的平衡。

    +
    +

    实现

    因为 AVL 树是改进了二分搜索树,所以部分代码是于二分搜索树重复的,对于重复内容不作再次解析。

    +

    对于 AVL 树来说,添加节点会有四种情况
    lWB0nf.png

    +

    对于左左情况来说,新增加的节点位于节点 2 的左侧,这时树已经不平衡,需要旋转。因为搜索树的特性,节点比左节点大,比右节点小,所以旋转以后也要实现这个特性。

    +

    旋转之前:new < 2 < C < 3 < B < 5 < A,右旋之后节点 3 为根节点,这时候需要将节点 3 的右节点加到节点 5 的左边,最后还需要更新节点的高度。

    +

    对于右右情况来说,相反于左左情况,所以不再赘述。

    +

    对于左右情况来说,新增加的节点位于节点 4 的右侧。对于这种情况,需要通过两次旋转来达到目的。

    +

    首先对节点的左节点左旋,这时树满足左左的情况,再对节点进行一次右旋就可以达到目的。

    +
    // js代码

    class Node {
    constructor(value) {
    this.value = value
    this.left = null
    this.right = null
    this.height = 1
    }
    }

    class AVL {
    constructor() {
    this.root = null
    }
    addNode(v) {
    this.root = this._addChild(this.root, v)
    }
    _addChild(node, v) {
    if (!node) {
    return new Node(v)
    }
    if (node.value > v) {
    node.left = this._addChild(node.left, v)
    } else if (node.value < v) {
    node.right = this._addChild(node.right, v)
    } else {
    node.value = v
    }
    node.height =
    1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
    let factor = this._getBalanceFactor(node)
    // 当需要右旋时,根节点的左树一定比右树高度高
    if (factor > 1 && this._getBalanceFactor(node.left) >= 0) {
    return this._rightRotate(node)
    }
    // 当需要左旋时,根节点的左树一定比右树高度矮
    if (factor < -1 && this._getBalanceFactor(node.right) <= 0) {
    return this._leftRotate(node)
    }
    // 左右情况
    // 节点的左树比右树高,且节点的左树的右树比节点的左树的左树高
    if (factor > 1 && this._getBalanceFactor(node.left) < 0) {
    node.left = this._leftRotate(node.left)
    return this._rightRotate(node)
    }
    // 右左情况
    // 节点的左树比右树矮,且节点的右树的右树比节点的右树的左树矮
    if (factor < -1 && this._getBalanceFactor(node.right) > 0) {
    node.right = this._rightRotate(node.right)
    return this._leftRotate(node)
    }

    return node
    }
    _getHeight(node) {
    if (!node) return 0
    return node.height
    }
    _getBalanceFactor(node) {
    return this._getHeight(node.left) - this._getHeight(node.right)
    }
    // 节点右旋
    // 5 2
    // / \ / \
    // 2 6 ==> 1 5
    // / \ / / \
    // 1 3 new 3 6
    // /
    // new
    _rightRotate(node) {
    // 旋转后新根节点
    let newRoot = node.left
    // 需要移动的节点
    let moveNode = newRoot.right
    // 节点 2 的右节点改为节点 5
    newRoot.right = node
    // 节点 5 左节点改为节点 3
    node.left = moveNode
    // 更新树的高度
    node.height =
    1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
    newRoot.height =
    1 +
    Math.max(this._getHeight(newRoot.left), this._getHeight(newRoot.right))

    return newRoot
    }
    // 节点左旋
    // 4 6
    // / \ / \
    // 2 6 ==> 4 7
    // / \ / \ \
    // 5 7 2 5 new
    // \
    // new
    _leftRotate(node) {
    // 旋转后新根节点
    let newRoot = node.right
    // 需要移动的节点
    let moveNode = newRoot.left
    // 节点 6 的左节点改为节点 4
    newRoot.left = node
    // 节点 4 右节点改为节点 5
    node.right = moveNode
    // 更新树的高度
    node.height =
    1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
    newRoot.height =
    1 +
    Math.max(this._getHeight(newRoot.left), this._getHeight(newRoot.right))

    return newRoot
    }
    }
    + +

    Trie

    +

    在计算机科学,trie,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。
    简单点来说,这个结构的作用大多是为了方便搜索字符串,该树有以下几个特点

    +
    +
      +
    • 根节点代表空字符串,每个节点都有 N(假如搜索英文字符,就有 26 条) 条链接,每条链接代表一个字符
    • +
    • 节点不存储字符,只有路径才存储,这点和其他的树结构不同
    • +
    • 从根节点开始到任意一个节点,将沿途经过的字符连接起来就是该节点对应的字符串
    • +
    +

    实现

    总得来说 Trie 的实现相比别的树结构来说简单的很多,实现就以搜索英文字符为例。

    +
    // js代码

    class TrieNode {
    constructor() {
    // 代表每个字符经过节点的次数
    this.path = 0
    // 代表到该节点的字符串有几个
    this.end = 0
    // 链接
    this.next = new Array(26).fill(null)
    }
    }
    class Trie {
    constructor() {
    // 根节点,代表空字符
    this.root = new TrieNode()
    }
    // 插入字符串
    insert(str) {
    if (!str) return
    let node = this.root
    for (let i = 0; i < str.length; i++) {
    // 获得字符先对应的索引
    let index = str[i].charCodeAt() - 'a'.charCodeAt()
    // 如果索引对应没有值,就创建
    if (!node.next[index]) {
    node.next[index] = new TrieNode()
    }
    node.path += 1
    node = node.next[index]
    }
    node.end += 1
    }
    // 搜索字符串出现的次数
    search(str) {
    if (!str) return
    let node = this.root
    for (let i = 0; i < str.length; i++) {
    let index = str[i].charCodeAt() - 'a'.charCodeAt()
    // 如果索引对应没有值,代表没有需要搜素的字符串
    if (!node.next[index]) {
    return 0
    }
    node = node.next[index]
    }
    return node.end
    }
    // 删除字符串
    delete(str) {
    if (!this.search(str)) return
    let node = this.root
    for (let i = 0; i < str.length; i++) {
    let index = str[i].charCodeAt() - 'a'.charCodeAt()
    // 如果索引对应的节点的 Path 为 0,代表经过该节点的字符串
    // 已经一个,直接删除即可
    if (--node.next[index].path == 0) {
    node.next[index] = null
    return
    }
    node = node.next[index]
    }
    node.end -= 1
    }
    }
    + +

    并查集

    +

    并查集是一种特殊的树结构,用于处理一些不交集的合并及查询问题。该结构中每个节点都有一个父节点,如果只有当前一个节点,那么该节点的父节点指向自己。
    这个结构中有两个重要的操作,分别是:

    +
    +
      +
    • Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
    • +
    • Union:将两个子集合并成同一个集合。
    • +
    +

    实现

    // js代码

    class DisjointSet {
    // 初始化样本
    constructor(count) {
    // 初始化时,每个节点的父节点都是自己
    this.parent = new Array(count)
    // 用于记录树的深度,优化搜索复杂度
    this.rank = new Array(count)
    for (let i = 0; i < count; i++) {
    this.parent[i] = i
    this.rank[i] = 1
    }
    }
    find(p) {
    // 寻找当前节点的父节点是否为自己,不是的话表示还没找到
    // 开始进行路径压缩优化
    // 假设当前节点父节点为 A
    // 将当前节点挂载到 A 节点的父节点上,达到压缩深度的目的
    while (p != this.parent[p]) {
    this.parent[p] = this.parent[this.parent[p]]
    p = this.parent[p]
    }
    return p
    }
    isConnected(p, q) {
    return this.find(p) === this.find(q)
    }
    // 合并
    union(p, q) {
    // 找到两个数字的父节点
    let i = this.find(p)
    let j = this.find(q)
    if (i === j) return
    // 判断两棵树的深度,深度小的加到深度大的树下面
    // 如果两棵树深度相等,那就无所谓怎么加
    if (this.rank[i] < this.rank[j]) {
    this.parent[i] = j
    } else if (this.rank[i] > this.rank[j]) {
    this.parent[j] = i
    } else {
    this.parent[i] = j
    this.rank[j] += 1
    }
    }
    }
    + +

    堆通常是一个可以被看做一棵树的数组对象。
    堆的实现通过构造二叉堆,实为二叉树的一种。这种数据结构具有以下性质。

    +
      +
    • 任意节点小于(或大于)它的所有子节点
    • +
    • 堆总是一棵完全树。即除了最底层,其他层的节点都被元素填满,且最底层从左到右填入。
      将根节点最大的堆叫做最大堆大根堆,根节点最小的堆叫做最小堆小根堆
      优先队列也完全可以用堆来实现,操作是一模一样的。
    • +
    +

    实现大根堆

    堆的每个节点的左边子节点索引是 i * 2 + 1,右边是 i * 2 + 2,父节点是 (i - 1) /2。
    堆有两个核心的操作,分别是 shiftUp 和 shiftDown 。前者用于添加元素,后者用于删除根节点。
    shiftUp 的核心思路是一路将节点与父节点对比大小,如果比父节点大,就和父节点交换位置。
    shiftDown 的核心思路是先将根节点和末尾交换位置,然后移除末尾元素。接下来循环判断父节点和两个子节点的大小,如果子节点大,就把最大的子节点和父节点交换。

    +
    // js代码

    class MaxHeap {
    constructor() {
    this.heap = []
    }
    size() {
    return this.heap.length
    }
    empty() {
    return this.size() == 0
    }
    add(item) {
    this.heap.push(item)
    this._shiftUp(this.size() - 1)
    }
    removeMax() {
    this._shiftDown(0)
    }
    getParentIndex(k) {
    return parseInt((k - 1) / 2)
    }
    getLeftIndex(k) {
    return k * 2 + 1
    }
    _shiftUp(k) {
    // 如果当前节点比父节点大,就交换
    while (this.heap[k] > this.heap[this.getParentIndex(k)]) {
    this._swap(k, this.getParentIndex(k))
    // 将索引变成父节点
    k = this.getParentIndex(k)
    }
    }
    _shiftDown(k) {
    // 交换首位并删除末尾
    this._swap(k, this.size() - 1)
    this.heap.splice(this.size() - 1, 1)
    // 判断节点是否有左孩子,因为二叉堆的特性,有右必有左
    while (this.getLeftIndex(k) < this.size()) {
    let j = this.getLeftIndex(k)
    // 判断是否有右孩子,并且右孩子是否大于左孩子
    if (j + 1 < this.size() && this.heap[j + 1] > this.heap[j]) j++
    // 判断父节点是否已经比子节点都大
    if (this.heap[k] >= this.heap[j]) break
    this._swap(k, j)
    k = j
    }
    }
    _swap(left, right) {
    let rightValue = this.heap[right]
    this.heap[right] = this.heap[left]
    this.heap[left] = rightValue
    }
    }
    ]]>
    + + 前端技术 + 计算机通识 + + + 面试 + +
    + + 前端常见知识点整理 ---- 继承 + /2020/01/03/FE-guide-inherit/ + +

    作者:李旭光
    引用请标明出处

    + +

    继承

    在 ES5 中,我们可以使用如下方式解决继承的问题

    +
    // js代码
    function Super() {}
    Super.prototype.getNumber = function() {
    return 1
    }

    function Sub() {}
    let s = new Sub()
    Sub.prototype = Object.create(Super.prototype, {
    constructor: {
    value: Sub,
    enumerable: false,
    writable: true,
    configurable: true
    }
    })
    +

    以上继承实现思路就是将子类的原型设置为父类的原型
    ES6 中,我们可以通过 class 语法轻松解决这个问题

    +
    // js代码

    class MyDate extends Date {
    test() {
    return this.getTime()
    }
    }
    let myDate = new MyDate()
    myDate.test()
    +

    但是 ES6 不是所有浏览器都兼容,所以我们需要使用 Babel 来编译这段代码。

    +

    如果你使用编译过得代码调用 myDate.test() 你会惊奇地发现出现了报错

    +

    因为在 JS 底层有限制,如果不是由 Date 构造出来的实例的话,是不能调用 Date 里的函数的。所以这也侧面的说明了:ES6 中的 class 继承与 ES5 中的一般继承写法是不同的

    +

    既然底层限制了实例必须由 Date 构造出来,那么我们可以改变下思路实现继承

    +
    // js代码

    function MyData() {

    }
    MyData.prototype.test = function () {
    return this.getTime()
    }
    let d = new Date() // 父类实例
    Object.setPrototypeOf(d, MyData.prototype)
    Object.setPrototypeOf(MyData.prototype, Date.prototype)
    +

    以上继承实现思路:先创建父类实例 => 改变实例原先的 __proto__ 转而连接到子类的 prototype => 子类的 prototype__proto__ 改为父类的 prototype

    +

    通过以上方法实现的继承就可以完美解决 JS 底层的这个限制。

    +]]>
    + + 前端技术 + 前端常见知识点整理 + + + 面试 + +
    + + 前端常见知识点整理 ---- 数据类型、类型判断、类型转换、类型比较 + /2020/01/02/FE-guide-dataType/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    +

    温故而知新,可以为师矣。
    ———————— 论语

    +
    +

    这篇文章主要是把我看过的书中的知识点,还有其他人整理的面试题进行整理,巩固自己的前端开发理论知识,希望其他看到这篇文章的人也能有所帮助。

    + + +

    JS知识点

    +

    当前市场中,如何区分一个好一点的前端开发和一般的前端开发,主要看的就是js能力的差距,好的前端开发,JS玩的转,不仅仅是框架玩的好,还要JS的基础扎实,只有基础与新技术都玩的6的前端开发才是好前端。

    +
    +

    上面这句话不是什么名人或者某位知名前端大拿说的,这是我在公司这几年面试招聘的过程中真真切切总结感受的。
    所以可以说得JS者得高级前端,所以下面也主要是写JS的相关知识点。

    +

    JavaScript内置数据类型 — 数据类型

    无论学什么语言最重要最基础的就是数据类型,《JavaScript权威指南》这本书中详细的介绍了JS中的数据类型,下面总结一下。

    +

    JavaScript中数据类型分为两大类共七种内置数据类型,其一是6种基本类型,其二是1种引用类型,我发现在面试过程中好多面试者都不会先说有两大类,而是会直接蹦出数字类型、字符串类型。。。对象、数组也会被并排放在一起,这其实不是个掌握知识点的好方法,应该先把数据类型分成上面说的基本类型引用类型这样两个大类之后,再看看这两大类中有什么其他的子类型,在记忆其他子类型之前我觉得应该先了解一下什么是基本类型引用类型,实际上基本类型和引用类型的主要区别是存储的区别,基本类型在栈中,而引用类型的话,引用数据的地址存储在栈中,而对象本身是存储在堆中,引用的数据地址是个16进制的数据值,它就像一把钥匙让你能够找到宝藏在什么地方。这就是基本数据类型和引用类型的区别了。

    +

    那么如何记住有哪些基本数据类型和引用数据类型呢,实际上只要记住了6个基本数据类型,其他的都是引用数据类型,而所有的引用数据类型的祖宗都是Object,所以引用数据类型实际上只有Object一个,那么像是Array等其他子类型,都是Object的孩子,不跟Object在一个级别上。

    +

    基本数据类型有哪些呢?其实挺好记的,数字,字符串,这两个一个像温柔的文学少女(string),一个像有点精于算计的男生(number),还有一个布尔类型(boolen)他像是班级里正义感爆棚的人,只论对错;另外还有个差生,没头脑似的未定义(Undefined)还有一个失了忆记不起来自己是谁的空(Null),最后还有一个新加入的插班生,总是带着口罩的标志符号(Symbol),这些人构成了这个班级的所有学生,也就是全部的基本类型,那么引用类型的对象Object呢就像及了漂亮爱化妆的班主任老师,有好多副面孔。不知道大家有没有看过一个动漫叫做《黑塔利亚》,他就是把国家都拟人化了,有了各自的性格,我很喜欢看,我觉得这些数据类型也各有各的特点,像这些国家一样,好了脑洞有点挖深了,有人会说我不就是这么几个简单的数据类型嘛,硬记下来不就好了,但是知识总有你硬记不下来的时候,最好的方法也是速记领域最为常用的方法,就是把你不熟悉的知识与你感兴趣的画面或者既往的知识串联起来,这样就能达到很好的记忆,如果你不喜欢动漫(怎么会有不喜欢动漫的人!!!),可以试试用其他的方法记,当然你如果硬要死记硬背那我也没办法,我继续开我的脑洞。
    如果你是学过Java开发的同学(如果是计算机专业出来的,应该都或多或少学过,非计算机专业的我也不知道说啥了。。),数字在Java中是分成 byte/short/int/long 的,但是在Js中没有那么多,就一个Number,它是浮点类型,基于 IEEE 754标准实现,刚才我不是说了Number是个精于算计的男生,精于算计就是说他分毫不差,这样浮点型就很好的记了下来,这个754的标准可以不记,如果非要记的话,你可以记成他是自称IEEE 754团体的成员。最后Number身后还跟着一个小弟,叫做NaN,他虽然是Number的小弟,但他总是说话不算数,自己说过什么都不承认。所以NaN!=NaN。
    老师是个爱化妆的老师,而这些学生也不是普通的学生,在学校他们是老老实实的基本类型,放了学之后一打扮,他们就各有了其他的能力,这个过程叫做装箱,具体的后面再说。(好了快回到现实吧你!)

    +

    如果基础数据是字面量类型,那么他们就像是在上课的学生,只是学生而已,而当他们调用方法时,他们就成了下课后各种技能都有的新新人类,这个过程有时候是显示的,就像是有些学生喜欢大声嚷嚷,而更多的是你不自觉中就用到了装箱操作,是Js引擎提供的能力,就像是有些闷骚的学生一样。

    +
    // js代码
    let aNumber = 111 // 这只是字面量,不是 number 类型
    aNumber.toString() // 装箱操作自动转化成数字对象,使用时候才会转换为对象类型
    +

    对象有个深浅拷贝的知识点必须要会,对象因为是引用数据类型,在栈中存储的是地址,当用另一个变量接收了之前的变量,那么就好像把钥匙复制了一把,两把钥匙开的还是同一个门,而深拷贝呢就像是照着原来的样板间又造了一个一模一样的房间,这两个房间长得一样,但就是两个房间,钥匙自然也是不一样的,所以呢,当往房间里搬家具的时候,浅拷贝搬进去的是一个房间,所以两把钥匙打开之后看到的都是多了家具,而深拷贝的话,我只是往样板间搬了家具,所以照着装修的房间里是不可能有的,这就是浅拷贝原数据会受影响,而深拷贝不会。

    +
    // js代码

    let a = { name: 'FE' }
    let b = a
    b.name = 'EF'
    console.log(a.name) // EF 浅拷贝原数据会受影响
    +

    内置数据类型检测 Typeof — 类型判断

    +

    typeof 对于基本类型,除了 null 都可以显示正确的类型

    +
    +

    typeof 就像是学校的教导主任一样,他有着一双火眼金睛,不管是哪个同学,穿了什么样的衣服,他一问就能问出来这个学生是谁,大家都怕他,但是Null因为失忆了,他也不知道教导主任是谁,所以typeof就拿他也没办法,因为他不怕教导主任,教导主任甚至会以为他是老师呢。

    +
    // js代码

    typeof 1 // 'number'
    typeof '1' // 'string'
    typeof undefined // 'undefined'
    typeof true // 'boolean'
    typeof Symbol() // 'symbol'
    typeof b // b 没有声明,但是还会显示 undefined

    typeof null // 'object' 这是JS中的bug
    + +
    +

    typeof 对于对象,除了函数都会显示 object

    +
    +
    // js代码

    typeof [] // 'object'
    typeof {} // 'object'
    typeof console.log // 'function'
    + +
    +

    知识扩展:为什么会出现这种情况呢?因为在 JS 的最初版本中,使用的是 32 位系统,为了性能考虑使用低位存储了变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。

    +
    +

    如果我们想获得一个变量的正确类型,可以通过 Object.prototype.toString.call(xx)。这样我们就可以获得类似 [object Type] 的字符串。

    +
    +

    小知识扩展

    +
    +
    let a
    // 我们也可以这样判断 undefined
    a === undefined
    // 但是 undefined 不是保留字,能够在低版本浏览器被赋值
    let undefined = 1
    // 这样判断就会出错
    // 所以可以用下面的方式来判断,并且代码量更少
    // 因为 void 后面随便跟上一个组成表达式
    // 返回就是 undefined
    a === void 0
    + +

    类型转换

    转Boolean

    一句话可以概括

    +
    +

    在条件判断时,除了 undefinednullfalseNaN''0-0,其他所有值都转为 true,包括所有对象。

    +
    +

    从上面这段话可以看出来,undefinednull 是基本类型之二,而false是布尔类型的假值,NaN是数字类型的无效值,''是字符串类型的空值,而0-0都是数字类型的零值,可以看到,除了0-0有些特殊,除了插班生Symbol,剩下的都是基本类型的假值,由此实际上就很好记了,有时候数字这个容易忘,但是记住“非0既真”这句话就好了。

    +

    对象转基本类型

    对象在转换基本类型时,首先是先会调用ToPrimitive(原始类型),如果有hint参数调用对应的的类型方法,如果没有那默认先会调用 valueOf 然后调用 toString。如果返回了基本类型,结束。如果都没返回,那么Error但是注意这两个方法你是可以重写的。

    +
    +

    Symbol.toPrimitive 是一个内置的 Symbol 值,它是作为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数。(来自MDN的解释)
    需要解释的关键机制是ToPrimitive函数。该函数是将任意值转换为相应的基本类型值。如果输入的就是一个基本类型值,那么将不做修改,被直接返回。如果值是非基本类型值,它将调用内部方法 [[DefaultValue]] 为对象找到一个默认值。
    [[DefaultValue]]是每一个对象的内部属性。该方法需要一个可选的参数hint,值是Number或String。如果没有提供hint,则默认为Number(除非该对象是Date,在这种情况下默认为String)。然后将调用toString和valueOf去寻找基本类型值。在这里hint就起到作用了。如果hint参数值为Number,valueOf将先被调用,如果hint是String的话,则toString被先调用。
    [[DefaultValue]] 返回的值一定是基本类型值。如果不是,一个TypeError 将会被抛出。这就意味着为了在这种情况下有意义,toString和valueOf应该返回基本类型值。

    +
    +

    四则运算符

    +

    只有当加法运算时,其中一方是字符串类型,就会把另一个也转为字符串类型。其他运算只要其中一方是数字,那么另一方就转为数字。并且加法运算会触发三种类型转换:将值转换为原始值,转换为数字,转换为字符串。

    +
    +
    // js代码

    1 + '1' // '11' 这里会是面试陷阱
    2 * '2' // 4
    [1, 2] + [2, 1] // '1,22,1'
    // [1, 2].toString() -> '1,2'
    // [2, 1].toString() -> '2,1'
    // '1,2' + '2,1' = '1,22,1'
    +

    *面试陷阱题之 ++ *

    +
    // 问表达式 'a' + + 'b' 返回结果是什么?
    // 答案是 'aNaN'
    'a' + + 'b' -> // 一元运算符优先级高
    'a' + (+ 'b') -> // +'b'转数字类型,非有效结果是NaN
    'a' + NaN.toString() -> // NaN调用toString()成字符串'NaN'
    'aNaN' // 字符串接到一起后'aNaN'

    // 类似题 '1' + + '4' 返回结果是什么
    // 其实就是'4' -> 4 -> '4' 最后还是'14'
    + +

    == 操作符 — 类型比较

    +

    相等运算符的运算规则如下:
    1、如果两个值类型相同,进行 === 比较。(这个非常好理解,就不多说了)
    (1)数字比大小
    (2)字符串就通过 unicode 字符索引来比较
    2、如果两个值类型不同,他们可能相等。根据下面规则进行类型转换再比较:
    (1)如果一个是null、一个是undefined,那么[相等]。 // 这个有点特殊需要单独记
    (2)如果任一值是字符串,另一个值是数值,在比较相等性之前先将字符串转换为数值;即是调用Number()函数。
    (3)如果任一值时布尔值,则在比较相等性之前先将其转换为数值,即是调用Number()函数。
    (4)如果一个是对象,另一个是数值或字符串,把对象转换成基础类型的值再比较。对象转换成基础类型,利用它的toString或者valueOf方法。 (js核心内置类ToPrimitive,会尝试valueOf先于toString;例外的是Date,Date利用的是toString转换。)

    +
    +

    1不说,2(1)的话单独记,其他基本类型转数字比较,引用性数据类型调用ToPrimitive转换成基本数据类型

    +

    首先还是一道面试题

    +
    // [] == ![] 结果是什么
    [] == ![] // true
    +

    为什么呢?我们来解析一下,

    +
      +
    1. 首先我们看一下右侧,![]肯定是要先转换成boolen类型了吧,那么[]的布尔类型是什么呢,上面转换布尔的时候我们说过,除了那五个基本类型的假值以及正负0之外,都是真值,所以[] -> true ![] -> false
    2. +
    3. ToPrimitive(false)->0右边的值得出了数值类型的原始值
    4. +
    5. 看左边ToPrimitive([])->[].toString()->''
    6. +
    7. Number('')->0
    8. +
    9. 比较左右 0 == 0 -> true
    10. +
    +

    最后

    到这里JS的内置数据类型及类型的转换和比较就讲完了,相信大家看过以后一定会记得住的
    PS:突然好像学画漫画,《JS数据结构们》,一定大火,哈哈哈😂

    +]]>
    + + 前端技术 + 前端常见知识点整理 + + + 面试 + +
    + + 前端常见知识点整理 ---- 继承类型 + /2020/01/13/FE-guide-inherit2/ + +

    作者:李旭光
    引用请标明出处

    + +

    继承的操作需要有一个父类,这里使用构造函数外加原型来创建一个:
    有下面两个类,下面实现 Child 继承 Father:

    +
    // js代码

    function Father() {
    this.type = 'prople';
    }

    Father.prototype.eat = function() {
    console.log('吃东西啦');
    };

    function Child(name) {
    this.name = name;
    this.color = 'black';
    }
    + +

    原型继承(认贼作父)

    +

    关键点:子类原型等于父类的实例 Child.prototype = new Person()(将父类指向子类的原型)。

    +
    +
    // js代码
    Child.prototype = new Father();
    + +

    特点:
    实例可继承的属性有:

    +
      +
    • 实例的构造函数的属性
    • +
    • 父类构造函数的属性
    • +
    • 父类原型上的属性
      新实例不会继承父类实例的属性
    • +
    +

    缺点:

    +
      +
    • 新实例无法向父类构造函数传参
    • +
    • 继承单一
    • +
    • 所有新实例都会共享父类实例的属性。— 原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改
    • +
    +

    构造继承(借腹生子)

    +

    在子类构造函数中调用父类构造函数

    +
    +
    // js代码
    function Child(name) {
    Father.call(this);
    }
    + +

    关键点:用 call 或 apply 将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制))Person.call(this, ‘reng’)
    特点:

    +
      +
    • 只继承了父类构造函数的属性,没有继承父类原型的属性
    • +
    • 解决了原型链继承的注意事项(缺点)1,2,3
    • +
    • 可以继承多个构造函数的属性(call 可以多个)
    • +
    • 在子实例中可以向父实例传参
      缺点:
    • +
    • 只能继承父类构造函数的属性
    • +
    • 无法实现构造函数的复用。(每次用每次都要重新调用)
    • +
    • 每个新实例都有构造函数的副本,臃肿
      (不能继承父类原型,函数在构造函数中,每个子类实例不能共享函数,浪费内存。)
    • +
    +

    组合继承(原型继承+构造继承)

    +

    使用构造继承继承父类参数,使用原型继承继承父类函数

    +
    +
    // js代码
    function Child(name) {
    // 构造继承
    Father.call(this);
    }

    Child.prototype = Father.prototype; // Child.prototype = new Person(); // 原型继承
    + +

    关键点:结合了两种模式的优点–向父类传参(call)和复用(prototype)
    特点:

    +
      +
    • 可以继承父类原型上的属性,可以传参,可复用
    • +
    • 每个新实例引入的构造函数属性是私有的
    • +
    +

    缺点:

    +
      +
    • 父类原型和子类原型是同一个对象,无法区分子类真正是由谁构造。
    • +
    • 调用了两次父类的构造函数(耗内存)
    • +
    • 子类的构造函数会代替原型上的那个父类构造函数(call 相当于拿到了父类构造函数的副本)
    • +
    +

    原型式继承(复制降级)

    // 先封装一个函数容器,用来承载继承的原型和输出对象
    function create(obj) {
    // 寄生
    function F() {}
    F.prototype = obj;
    return new F();
    }
    var father = new Father();
    var child = create(father);
    console.log(child instanceof Father); // true
    console.log(child.job); // frontend
    + +

    关键点:用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了可以随意增添属性的实例或对象。Object.create()就是这个原理。

    +

    特点:

    +
      +
    • 类似于复制一个对象,用函数来包装
    • +
    +

    注意事项:

    +
      +
    • 所有的实例都会继承原型上的属性
    • +
    • 无法实现复用。(新实例属性都是后面添加的)
      Object.create()方法规范了原型式继承。这个方法接收两个参数,一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。
    • +
    +
    // 传一个参数的时候
    var child = Object.create(new Father());
    console.log(child.job); // frontend
    console.log(child instanceof Father); // true
    // 传两个参数的时候
    var child = Object.create(new Father(), {
    name: {
    value: 'come on'
    }
    });
    child.sayHello(); // Hello come on
    + +

    寄生组合继承

    它跟组合继承一样,都比较常用。
    寄生:在函数内返回对象然后调用
    组合

    +
      +
    • 函数的原型等于另一个实例
    • +
    • 在函数中用 apply 或 call 引入另一个构造函数,可传参
    • +
    +
    // 寄生
    function create(obj) {
    function F() {}
    F.prototype = obj;
    return new F();
    }
    // object是F实例的另一种表示方法
    var obj = create(Father.prototype);
    // obj实例(F实例)的原型继承了父类函数的原型
    // 上述更像是原型链继承,只不过只继承了原型属性

    // 组合
    function Child() {
    // 构造
    this.age = 100;
    Father.call(this); // 这个继承了父类构造函数的属性
    } // 解决了组合式两次调用构造函数属性的特点

    // 重点
    Child.prototype = obj; // 原型

    console.log(Child.prototype.constructor); // Father
    obj.constructor = Child; // 一定要修复实例
    console.log(Child.prototype.constructor); // Child
    var child = new Child();
    // Child实例就继承了构造函数属性,父类实例,object的函数属性
    console.log(child.job); // frontend
    console.log(child instanceof Father); // true
    + +

    重点:修复了组合继承的问题

    +

    在上面的问题中,你可能发现了这么一个注释obj.constructor = Sub; // 一定要修复实例。为什么要修正子类的构造函数的指向呢?

    +

    因为在不修正这个指向的时候,在获取构造函数返回的时候,在调用同名属性或方法取值上可能造成混乱。比如下面:

    +
    function Car() {}
    Car.prototype.orderOneLikeThis = function() {
    // Clone producing function
    return new this.constructor();
    };
    Car.prototype.advertise = function() {
    console.log('I am a generic car.');
    };

    function BMW() {}
    BMW.prototype = Object.create(Car.prototype);
    BMW.prototype.constructor = BMW; // Resetting the constructor property
    BMW.prototype.advertise = function() {
    console.log('I am BMW with lots of uber features.');
    };

    var x5 = new BMW();

    var myNewToy = x5.orderOneLikeThis();

    myNewToy.advertise(); // => "I am BMW ..." if `BMW.prototype.constructor = BMW;` is not
    // commented; "I am a generic car." otherwise.
    + +

    object.create 在组合继承的基础上,子类继承一个由父类原型生成的空对象。

    +
    // js代码
    function Child(name) {
    Father.call(this);
    }

    Child.prototype = Object.create(Father.prototype, {
    constructor: {
    value: Child
    }
    });
    + +

    inherits 函数 — Nodejs util.inherits 函数

    // js代码

    function inherits = function(ctor, superCtor) {
    ctor.super_ = superCtor; // super_属性是子类继承父类时构造函数要写入的一个属性值.
    ctor.prototype = Object.create(superCtor.prototype, {
    constructor: {
    value: ctor,
    enumerable: false,
    writable: true,
    configurable: true
    }
    });
    };
    // 在上面的代码中 ctor 想要继承 superCtor ,我们姑且把 ctor 称作子类, superCtor 称作父类.

    // 使用
    function Child() {
    Father.call(this);
    //...
    }
    inherits(Child, Father);

    Child.prototype.fun = ...
    +]]>
    + + 前端技术 + 前端常见知识点整理 + + + 面试 + +
    + + 前端常见知识点整理 ---- instanceof + /2020/01/02/FE-guide-instanceof/ + +

    作者:李旭光
    引用请标明出处

    + +

    instanceof

    instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype
    举例:

    +
    a instanceof Object
    +

    判断 Objectprototype 是否在 a 的原型链上。

    +

    我们也可以试着实现一下 instanceof

    +
    function myInstanceof(left, right) { // left 表示左表达式,right 表示右表达式
    let prototype = right.prototype // 获得类型的原型
    left = left.__proto__ // 获得对象的原型

    while (true) { // 判断对象的类型是否等于类型的原型
    if (left === null)
    return false
    if (prototype === left)
    return true
    left = left.__proto__
    }
    }
    ]]>
    + + 前端技术 + 前端常见知识点整理 + + + 面试 + +
    + + 前端常见知识点整理 ---- new + /2020/01/02/FE-guide-new/ + +

    作者:李旭光
    引用请标明出处

    + +

    new 一个对象的过程

      +
    1. 新生成了一个对象
    2. +
    3. 链接到原型
    4. +
    5. 绑定 this
    6. +
    7. 返回新对象
    8. +
    +

    在调用 new 的过程中会发生以上四件事情,我们也可以试着来自己实现一个 new

    +
    // js代码

    function create() {
    // 创建一个空的对象
    let obj = new Object()
    // 获得构造函数
    let Con = [].shift.call(arguments)
    // 链接到原型
    obj.__proto__ = Con.prototype
    // 绑定 this,执行构造函数
    let result = Con.apply(obj, arguments)
    // 确保 new 出来的是个对象
    return typeof result === 'object' ? result : obj
    }
    + +

    对于实例对象来说,都是通过 new 产生的,无论是 function Foo() 还是 let a = { b : 1 }

    +

    对于创建一个对象来说,更推荐使用字面量的方式创建对象(无论性能上还是可读性)。因为你使用 new Object() 的方式创建对象需要通过作用域链一层层找到 Object,但是你使用字面量的方式就没这个问题。

    +
    // js代码

    function Foo() {} // function 就是个语法糖,内部等同于 new Function()
    let a = { b: 1 } // 这个字面量内部也是使用了 new Object()
    +

    对于 new 来说,还需要注意下运算符优先级。

    +
    // js代码

    function Foo() {
    return this;
    }
    Foo.getName = function () {
    console.log('1');
    };
    Foo.prototype.getName = function () {
    console.log('2');
    };

    new Foo.getName(); // -> 1
    new Foo().getName(); // -> 2
    + +

    从上图可以看出,new Foo() 的优先级大于 new Foo ,所以对于上述代码来说可以这样划分执行顺序

    +
    // js代码

    new (Foo.getName());
    (new Foo()).getName();
    + +
      +
    • 对于第一个函数来说,先执行了 Foo.getName() ,所以结果为 1;
    • +
    • 对于后者来说,先执行 new Foo() 产生了一个实例,然后通过原型链找到了 Foo 上的 getName 函数,所以结果为 2。
    • +
    +]]>
    + + 前端技术 + 前端常见知识点整理 + + + 面试 + +
    + + 前端常见知识点整理 ---- 原型 + /2020/01/02/FE-guide-prototype/ + +

    作者:李旭光
    引用请标明出处

    + +

    原型

    yuanxing.png
    每个函数都有 prototype 属性,除了 Function.prototype.bind(),该属性指向原型对象,简称原型prototype原型对象里的constructor指向构造函数本身。
    每个对象都有 __proto__ 属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 _proto_ 来访问。
    对象可以通过 __proto__ 来寻找不属于该对象的属性,__proto__ 将对象连接起来组成了原型链

    + +

    总结

    +

    prototype 原型对象

    什么是原型?

    每个函数都具有 prototype 属性,它被默认成一个对象,即原型对象
    首先来介绍下 prototype 属性。这是一个显式原型属性,只有函数才拥有该属性。基本上所有函数都有这个属性,但是也有一个例外

    +
    // js代码
    let fun = Function.prototype.bind()
    +

    如果你以上述方法创建一个函数,那么可以发现这个函数是不具有 prototype 属性的。

    +

    prototype 如何产生的

    当我们声明一个函数时,这个属性就被自动创建了。

    +

    function Foo() {}
    +

    并且这个属性的值是一个对象(也就是原型),只有一个属性 constructor
    constructor 对应着构造函数,也就是 Foo

    +

    什么是原型链?

    当对象使用属性时,先在自身找,有就直接用,没有就沿着proto这条链往上找,直到 Object 原型的位置,有就返回相应的值,没有就返回 underfined。

    +

    constructor 构造函数

    什么是构造函数?

    任何一个函数,只要被 new 操作符使用,就可以是一个构造函数(构造函数建议以大写开头)
    另外,在 JavaScript 的内置对象中,所有的函数对象都是 Function 构造函数的实例,比如:Object、Array等

    +

    constructor 是一个公有且不可枚举的属性。一旦我们改变了函数的 prototype ,那么新对象就没有这个属性了(当然可以通过原型链取到 constructor)。

    +

    那么你肯定也有一个疑问,这个属性到底有什么用呢?其实这个属性可以说是一个历史遗留问题,在大部分情况下是没用的,在我的理解里,我认为他有两个作用:

    +
      +
    1. 让实例对象知道是什么函数构造了它
    2. +
    3. 如果想给某些类库中的构造函数增加一些自定义的方法,就可以通过 xx.constructor.method 来扩展
    4. +
    +

    _proto_

    这是每个对象都有的隐式原型属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 _proto_ 来访问。

    +

    因为在 JS 中是没有类的概念的,为了实现类似继承的方式,通过 _proto_ 将对象和原型联系起来组成原型链,得以让对象可以访问到不属于自己的属性。

    +

    实例对象的 _proto_ 如何产生的

    从上图可知,当我们使用 new 操作符时,生成的实例对象拥有了 _proto_ 属性。

    +
    function Foo() {}
    // 这个函数是 Function 的实例对象
    // function 就是一个语法糖
    // 内部调用了 new Function(...)
    +

    所以可以说,在 new 的过程中,新对象被添加了 _proto_ 并且链接到构造函数的原型上。

    +

    new 的过程

      +
    1. 新生成了一个对象
    2. +
    3. 链接到原型
    4. +
    5. 绑定 this
    6. +
    7. 返回新对象
    8. +
    +

    在调用 new 的过程中会发生以上四件事情,我们也可以试着来自己实现一个 new

    +
    function Person( name ){ 
    this.name = name;
    };
    Person.prototype.getName = function(){
    return this.name;
    };

    function create() {
    let obj = new Object() // 从 Object.prototype 上克隆一个空的对象
    let Con = [].shift.call(arguments) // 获取外部传入的构造器,此例是 Person
    obj.__proto__ = Con.prototype // 指向正确的原型,链接到原型
    let result = Con.apply(obj, arguments) // 绑定 this,执行构造函数,借用外部传入的构造器给 obj 设置属性
    return typeof result === 'object' ? result : obj // 确保 new 出来的是个对象
    }

    create(Person,'lixg')
    +

    对于实例对象来说,都是通过 new 产生的,无论是 function Foo() 还是 let a = { b : 1 }

    +

    对于创建一个对象来说,更推荐使用字面量的方式创建对象。因为你使用 new Object() 的方式创建对象需要通过作用域链一层层找到 Object ,但是你使用字面量的方式就没这个问题。

    +
    function Foo() {} // function 就是个语法糖,内部等同于 new Function()
    let a = { b: 1 } // 这个字面量内部也是使用了 new Object()
    + +

    Function.proto === Function.prototype

    对于对象来说,xx.__proto__.contrcutor 是该对象的构造函数,但是在图中我们可以发现 Function.__proto__ === Function.prototype,难道这代表着 Function 自己产生了自己?

    +

    答案肯定是否认的,要说明这个问题我们先从 Object 说起。

    +

    从图中我们可以发现,所有对象都可以通过原型链最终找到 Object.prototype ,虽然 Object.prototype 也是一个对象,但是这个对象却不是 Object 创造的,而是引擎自己创建了 Object.prototype 。所以可以这样说,所有实例都是对象,但是对象不一定都是实例。

    +

    接下来我们来看 Function.prototype 这个特殊的对象,如果你在浏览器将这个对象打印出来,会发现这个对象其实是一个函数。

    +

    我们知道函数都是通过 new Function() 生成的,难道 Function.prototype 也是通过 new Function() 产生的吗?答案也是否定的,这个函数也是引擎自己创建的。首先引擎创建了 Object.prototype ,然后创建了 Function.prototype ,并且通过 __proto__ 将两者联系了起来。这里也很好的解释了上面的一个问题,为什么 let fun = Function.prototype.bind() 没有 prototype 属性。因为 Function.prototype 是引擎创建出来的对象,引擎认为不需要给这个对象添加 prototype 属性。

    +

    所以我们又可以得出一个结论,不是所有函数都是 new Function() 产生的。
    有了 Function.prototype 以后才有了 function Function() ,然后其他的构造函数都是 function Function() 生成的。

    +

    现在可以来解释 Function.__proto__ === Function.prototype 这个问题了。因为先有的 Function.prototype 以后才有的 function Function() ,所以也就不存在鸡生蛋蛋生鸡的悖论问题了。对于为什么 Function.__proto__ 会等于 Function.prototype ,个人的理解是:其他所有的构造函数都可以通过原型链找到 Function.prototype ,并且 function Function() 本质也是一个函数,为了不产生混乱就将 function Function()__proto__ 联系到了 Function.prototype 上。

    +

    总结

      +
    • Object 是所有对象的爸爸,所有对象都可以通过 __proto__ 找到它
    • +
    • Function 是所有函数的爸爸,所有函数都可以通过 __proto__ 找到它
    • +
    • Function.prototypeObject.prototype 是两个特殊的对象,他们由引擎来创建
    • +
    • 除了以上两个特殊对象,其他对象都是通过构造器 new 出来的
    • +
    • 函数的 prototype 是一个对象,也就是原型
    • +
    • 对象的 __proto__ 指向原型, __proto__ 将对象和原型连接起来组成了原型链
      总结
    • +
    +

    归纳

    ES 把对象定义为:“无序属性的集合,其属性可以包含基本值,对象和函数”。
    严格来讲,这就相当于说对象是一组没有特定顺序的值。ES 中的构造函数可以用来创建特定类型的对象,用来在创建对象时初始化对象。它的特点是,一般为大写字母开头,使用 new 操作符来实例化对象,比如:

    +
    function Person() {}
    var person = new Person();
    person.name = "Kevin";
    console.log(person.name); // Kevin
    + +

    Person 就是构造函数, person 就是对象。对于对象而言,每个 JS 对象一定对应一个原型对象,并从原型对象继承属性和方法。对象 __proto__ 属性的值就是它所对应的原型对象。对象的 __proto__ 指向自己构造函数的 prototype 。所以对象的原型链就是 obj.__proto__.proto__.... 。对于函数而言,只有函数才有 prototype 属性, Person.prototype 是一个对象,并且有两个属性, 一个是 constructor 指向其构造函数 Person , 一个是 __proto__ 属性:是一个对象,指向上一层的原型。原型链的尽头是 Object.prototype 。所有对象均从 Object.prototype 继承属性。Function.prototypeFunction.__proto__ 为同一对象。Object/Array/String 等等构造函数本质上和 Function 一样,均继承于 Function.prototypeFunction.prototype 直接继承 Object.prototype 。这里的 ObjectFunction 有点鸡和蛋的问题,总结:先有 Object.prototype(原型链顶端),Function.prototype 继承 Object.prototype 而产生,最后,FunctionObject 和其它构造函数继承 Function.prototype 而产生。属性查找时,先在对象自己上找,找不到才会一步步根据原型链往上找。
    继承

    +

    关联阅读

    https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain
    说说原型(prototype)、原型链和原型继承

    +

    扩展阅读

    继承

    +]]>
    + + 前端技术 + 前端常见知识点整理 + + + 面试 + +
    + + 前端常见知识点整理 ---- 浏览器存储 + /2020/01/03/FE-guide-store/ + +

    作者:李旭光
    引用请标明出处

    + +

    cookie,localStorage,sessionStorage,indexDB

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    特性cookielocalStoragesessionStorageindexDB
    数据生命周期一般由服务器生成,可以设置过期时间除非被清理,否则一直存在页面关闭就清理除非被清理,否则一直存在
    数据存储大小4K5M5M无限制
    与服务端通信每次都会携带在 header 中,对于请求性能影响不参与不参与不参与
    +

    从上表可以看到, cookie 已经不建议用于存储。如果没有大量数据存储需求的话,可以使用 localStoragesessionStorage 。对于不怎么改变的数据尽量使用 localStorage 存储,否则可以用 sessionStorage 存储。

    +

    对于 cookie ,我们还需要注意安全性。
    | 属性 | 作用 |
    | ——— | ————————————————————– |
    | value | 如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识 |
    | http-only | 不能通过 JS 访问 Cookie,减少 XSS 攻击 |
    | secure | 只能在协议为 HTTPS 的请求中携带 |
    | same-site | 规定浏览器不能在跨域请求中携带 Cookie,减少 CSRF 攻击 |

    +

    Service Worker

    +

    Service workers 本质上充当 Web 应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步 API。

    +
    +

    目前该技术通常用来做缓存文件,提高首屏速度,可以试着来实现这个功能。

    +
    // js代码

    // index.js
    if (navigator.serviceWorker) {
    navigator.serviceWorker
    .register('sw.js')
    .then(function(registration) {
    console.log('service worker 注册成功')
    })
    .catch(function(err) {
    console.log('servcie worker 注册失败')
    })
    }
    // sw.js
    // 监听 `install` 事件,回调中缓存所需文件
    self.addEventListener('install', e => {
    e.waitUntil(
    caches.open('my-cache').then(function(cache) {
    return cache.addAll(['./index.html', './index.js'])
    })
    )
    })

    // 拦截所有请求事件
    // 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据
    self.addEventListener('fetch', e => {
    e.respondWith(
    caches.match(e.request).then(function(response) {
    if (response) {
    return response
    }
    console.log('fetch source')
    })
    )
    })
    + +

    打开页面,可以在开发者工具中的 Application 看到 Service Worker 已经启动了
    Cache 中也可以发现我们所需的文件已被缓存

    +

    当我们重新刷新页面可以发现我们缓存的数据是从 Service Worker 中读取的

    +]]>
    + + 前端技术 + 前端常见知识点整理 + + + 面试 + +
    + + 前端常见知识点整理 ---- 网络安全 + /2020/01/22/FE-guide-safe/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    网络安全是前端不可忽略的一个部分,可惜的是之前我都忽略了,最近发现网络安全已经变得日益重要,所以特别整理一篇文章说说网络安全相关的内容。

    + +

    XSS(Cross-site scripting)跨站脚本攻击

    跨站脚本攻击是一种网站应用程序的安全漏洞攻击,是代码注入的一种。它允许恶意用户将代码注入到网页上,其他用户在观看网页时就会受到影响。这类攻击通常包含了HTML以及用户端脚本语言。

    +

    XSS攻击通常指的是通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。这些恶意网页程序通常是JavaScript,但实际上也可以包括Java,VBScript,ActiveX,Flash或者甚至是普通的HTML。攻击成功后,攻击者可能得到更高的权限(如执行一些操作)、私密网页内容、会话和cookie等各种内容。

    +

    XSS攻击分成两类:

    +
      +
    • 来自内部的攻击
        +
      • 主要指的是利用程序自身的漏洞,构造跨站语句
      • +
      +
    • +
    • 来自外部的攻击
        +
      • 主要指自己构造XSS跨站漏洞网页或者寻找非目标机以外的有跨站漏洞的网页。如当我们要渗透一个站点,我们自己构造一个有跨站漏洞的网页,然后构造跨站语句,通过结合其他技术,如社会工程学等,欺骗目标服务器的管理员打开。
      • +
      +
    • +
    +

    XSS分为:存储型反射型

    +
      +
    • 存储型XSS:存储型XSS,持久化,代码是存储在服务其中的,如在个人信息或发表文章等地方,加入代码,如果没有过滤或过滤不严,那么这些代码将储存到服务器中,用户访问该页面的时候触发代码执行。这种XSS比较危险,容易造成蠕虫,盗窃cookie(虽然还有种DOM型XSS,但是也还是包括在存储型XSS内)。
    • +
    • 反射型XSS:非持久化,需要欺骗用户自己去点击链接才能触发XSS代码(服务器中没有这样的页面和内容),一般容易出现在搜索页面。
    • +
    +

    攻击手段和目的

    攻击者使被攻击者在浏览器中执行脚本后,如果需要收集来自被攻击者的数据(如cookie或其他敏感信息),可以自行架设一个网站,让被攻击者通过JavaScript等方式把收集好的数据作为参数提交,随后以数据库等形式记录在攻击者自己的服务器上。

    +

    常用的XSS攻击手段和目的有:

    +
      +
    • 盗用cookie,获取敏感信息。
    • +
    • 利用植入Flash,通过crossdomain权限设置进一步获取更高权限;或者利用Java等得到类似的操作。
    • +
    • 利用iframe、frame、XMLHttpRequest或上述Flash等方式,以(被攻击)用户的身份执行一些管理动作,或执行一些一般的如发微博、加好友、发私信等操作。
    • +
    • 利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动。
    • +
    • 在访问量极大的一些页面上的XSS可以攻击一些小型网站,实现DDoS攻击的效果。
    • +
    +

    漏洞的防御和利用

    过滤特殊字符

    避免XSS的方法之一主要是将用户所提供的内容进行过滤,许多语言都有提供对HTML的过滤:

    +
      +
    • PHP的htmlentities()或是htmlspecialchars()。
    • +
    • Python的cgi.escape()。
    • +
    • ASP的Server.HTMLEncode()。
    • +
    • ASP.NET的Server.HtmlEncode()或功能更强的Microsoft Anti-Cross Site Scripting Library
    • +
    • Java的xssprotect (Open Source Library)。
    • +
    • NodeJS的node-validator。
    • +
    +

    使用HTTP头指定类型

    很多时候可以使用HTTP头指定内容的类型,使得输出的内容避免被作为HTML解析。如在PHP语言中使用以下代码:
    <?php header('Content-Type: text/javascript; charset=utf-8'); ?>
    即可强行指定输出内容为文本/JavaScript脚本(顺便指定了内容编码),而非可以引发攻击的HTML。

    +

    用户方面

    包括Internet Explorer、Mozilla Firefox在内的大多数浏览器皆有关闭JavaScript的选项,但关闭功能并非是最好的方法,因为许多网站都需要使用JavaScript语言才能正常运作。通常来说,一个经常有安全更新推出的浏览器,在使用上会比很久都没有更新的浏览器更为安全。

    +

    CRSF(Cross-site request forgery)跨站请求伪造

    跨站请求伪造是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

    +
      +
    • XSS 利用的是用户对指定网站的信任
    • +
    • CSRF 利用的是网站对用户网页浏览器的信任
    • +
    +

    跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。

    +

    例子

    例子
    假如一家银行用以运行转账操作的URL地址如下:
    http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName
    那么,一个恶意攻击者可以在另一个网站上放置如下代码:
    <img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">
    如果有账户名为Alice的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失1000资金。
    这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险
    透过例子能够看出,攻击者并不能通过CSRF攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义运行操作。

    +

    防御措施

    检查Referer(参照)字段

    HTTP头中有一个Referer字段,这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer字段应和请求的地址位于同一域名下。以上文银行操作为例,Referer字段地址通常应该是转账按钮所在的网页地址,应该也位于www.examplebank.com之下。而如果是CSRF攻击传来的请求,Referer字段会是包含恶意网址的地址,不会位于www.examplebank.com之下,这时候服务器就能识别出恶意的访问。
    这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的Referer字段。虽然http协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其Referer字段的可能。

    +

    添加校验token

    由于CSRF的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在cookie中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再运行CSRF攻击。这种数据通常是窗体中的一个数据项。服务器将其生成并附加在窗体中,其内容是一个伪随机数。当客户端通过窗体提交请求时,这个伪随机数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪随机数,而通过CSRF传来的欺骗性攻击中,攻击者无从事先得知这个伪随机数的值,服务端就会因为校验token的值为空或者错误,拒绝这个可疑请求。

    +]]>
    + + 前端技术 + 前端常见知识点整理 + + + 网络安全 + +
    + + 前端常见知识点整理 ---- this + /2020/01/02/FE-guide-this/ + +

    作者:李旭光
    引用请标明出处

    + +

    this

    this 是很多人会混淆的概念,但是其实他一点都不难,你只需要记住几个规则就可以了。

    +
    // js代码

    function foo() {
    console.log(this.a)
    }
    var a = 1
    foo()

    var obj = {
    a: 2,
    foo: foo
    }
    obj.foo()

    // 以上两者情况 `this` 只依赖于调用函数前的对象,优先级是第二个情况大于第一个情况

    // 以下情况是优先级最高的,`this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向
    var c = new foo()
    c.a = 3
    console.log(c.a)

    // 还有种就是利用 `call` , `apply` , `bind` 改变 `this` ,这个优先级仅次于 `new`
    +

    以上几种情况明白了,很多代码中的 this 应该就没什么问题了,下面让我们看看箭头函数中的 this

    +
    // js代码

    function a() {
    return () => {
    return () => {
    console.log(this)
    }
    }
    }
    console.log(a()()())
    +

    箭头函数其实是没有 this 的,这个函数中的 this 只取决于他外面的第一个不是箭头函数的函数的 this 。在这个例子中,因为调用 a 符合前面代码中的第一个情况,所以 thiswindow。并且 this 一旦绑定了上下文,就不会被任何代码改变。

    +]]>
    + + 前端技术 + 前端常见知识点整理 + + + 面试 + +
    + + 介绍一个好用的doc展示库 ---- vuepress + /2020/01/05/FE-guide-vuepress/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    最近在做公司技术规范,需要能通过 B/S 方式进行分发,起初想法是做一个网站,用 html 的方式进行书写,但是后面感觉太麻烦了,所以就放弃了,偶然间看到有工具可以直接通过 md 文件编译生成网站,就像是 hexo 博客这样,我觉得这挺好,极大的节省了开发网站所需要的时间,只需要专注于内容就好了,所以我就研究了下来,整个了解过程中有这么两个工具映入了我的眼帘,一个是 doctify,另外一个就是今天的主角 vuepress

    + + +

    vuepress 何许

    ]]>
    + + 效率工具 + Labrary + + + vuepress + +
    + + Git的常用命令 + /2017/12/12/Git-Shell/ + +

    作者:李旭光
    引用请标明出处

    + +

    指令表

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    指令含义备注
    git add .提示增加文件.代表所有
    git commit -m“说明内容” 提交到本地服务器
    git status显示修改信息
    git pull从网络服务器拉 更新最新版本
    git push上传最新版本
    git branch查看当前分支
    git checkout develop切换到develop模式
    git merge master从master合并过来
    git push origin develop提交
    git clone git@192.168.2.10:bat-web.git从服务器克隆
    +]]>
    + + 前端技术 + + + Git + +
    + + 前端常见知识点整理 ---- 执行上下文 + /2020/01/02/FE-guide-%E6%89%A7%E8%A1%8C%E4%B8%8A%E4%B8%8B%E6%96%87/ + +

    作者:李旭光
    引用请标明出处

    + +

    执行上下文

    当执行 JS 代码时,会产生三种执行上下文

    +
      +
    • 全局执行上下文
    • +
    • 函数执行上下文
    • +
    • eval 执行上下文
    • +
    +

    每个执行上下文中都有三个重要的属性

    +
      +
    • 变量对象( VO ),包含变量、函数声明和函数的形参,该属性只能在全局上下文中访问
    • +
    • 作用域链( JS 采用词法作用域,也就是说变量的作用域是在定义时就决定了)
    • +
    • this
      // js代码

      var a = 10
      function foo(i) {
      var b = 20
      }
      foo()
      +对于上述代码,执行栈中有两个上下文:全局上下文和函数 foo 上下文。
      // js代码

      stack = [
      globalContext,
      fooContext
      ]
      +对于全局上下文来说, VO 大概是这样的
      // js代码

      globalContext.VO === globe
      globalContext.VO = {
      a: undefined,
      foo: <Function>,
      }
      +对于函数 foo 来说, VO 不能访问,只能访问到活动对象( AO
      // js代码

      fooContext.VO === foo.AO
      fooContext.AO {
      i: undefined,
      b: undefined,
      arguments: <>
      }
      // arguments 是函数独有的对象(箭头函数没有)
      // 该对象是一个伪数组,有 `length` 属性且可以通过下标访问元素
      // 该对象中的 `callee` 属性代表函数本身
      // `caller` 属性代表函数的调用者
      +对于作用域链,可以把它理解成包含自身变量对象和上级变量对象的列表,通过 [[Scope]] 属性查找上级变量
      // js代码

      fooContext.[[Scope]] = [
      globalContext.VO
      ]
      fooContext.Scope = fooContext.[[Scope]] + fooContext.VO
      fooContext.Scope = [
      fooContext.VO,
      globalContext.VO
      ]
      +接下来让我们看一个老生常谈的例子, var
      // js代码

      b() // call b
      console.log(a) // undefined

      var a = 'Hello world'

      function b() {
      console.log('call b')
      }
      +想必以上的输出大家肯定都已经明白了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行上下文时,会有两个阶段。第一个阶段是创建的阶段(具体步骤是创建 VO ), JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined ,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用。
    • +
    +

    在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升

    +
    // js代码

    b() // call b second

    function b() {
    console.log('call b fist')
    }
    function b() {
    console.log('call b second')
    }
    var b = 'Hello world'
    +

    var 会产生很多错误,所以在 ES6 中引入了 letlet 不能在声明前使用,但是这并不是常说的 let 不会提升, let 提升了声明但没有赋值,因为临时死区导致了并不能在声明前使用。

    +

    对于非匿名的立即执行函数需要注意以下一点

    +
    // js代码

    var foo = 1
    (function foo() {
    foo = 10
    console.log(foo)
    }()) // -> ƒ foo() { foo = 10 ; console.log(foo) }
    +

    因为当 JS 解释器在遇到非匿名的立即执行函数时,会创建一个辅助的特定对象,然后将函数名称作为这个对象的属性,因此函数内部才可以访问到 foo ,但是这个值又是只读的,所以对它的赋值并不生效,所以打印的结果还是这个函数,并且外部的值也没有发生更改。

    +
    // js代码

    specialObject = {};

    Scope = specialObject + Scope;

    foo = new FunctionExpression;
    foo.[[Scope]] = Scope;
    specialObject.foo = foo; // {DontDelete}, {ReadOnly}

    delete Scope[0]; // remove specialObject from the front of scope chain
    ]]>
    + + 前端技术 + 前端常见知识点整理 + + + 面试 + +
    + + 再见2019 + /2019/12/31/GoodBye-2019/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    不知不觉又是一年,今天是2019年的最后一天,也是我30岁的最后一天,回顾这一年有收获有遗憾。翻出了18年的总结,看到了18年底对于自己19年的规划,现在回过头来看看哪些完成了。

    +

    18年底定的计划

    学习技术

    1. 深入学习客户端开发(全年)

    18年客户端的项目也做了几个,Electron的开发能力经验更多了,之前只是用html+css+js做前端页面的开发,今年又增加了跟vue框架的结合,成功在多个项目中实践,只是一直没有形成规范感觉还是少了一环,原来一直在犹豫Electron到底算不算前端的技术栈之一,后来也算是想明白了,大前端范畴,只要是面向用户的都算是前端该做的范畴,所以Electron当然也算是前端应该掌握的技术栈,20年会继续学习客户端技术,把公司相关规范制定和落地。

    +

    2. 学习前端自动化测试相关知识(2019年3月前)

    18年关注了前端自动化测试相关的知识,本打算19年好好学习一下,在公司项目中实践,但是今年前后端工作量大增,前端人员的精力有限,加之成本的考虑,并没有开展起来,不过这方面的技能储备已经具备了,年底两次关于前端自动化的测试也将前端自动化的思想普及到了公司前端的团队里,20年是公司强调质量的一年,相信20年前端自动化测试将有机会在公司一些项目中落地

    +

    3. 学习并掌握TS (2019年5月前)

    18年就看到了TS将会成为一门比较活的开发语言,目前来说已经具备了做TS开发的技能,只是TS更适合大型的长期的项目,将更好的保障项目的代码质量,快速迭代和小型项目并不是很适合。前端团队里没有开花,没想到居然在开发团队里见到了曙光,感谢开发的小伙伴愿意接受这门前端新技术,相信有一个点采用后看到了优势,后续普及工作将会更好开展。

    +

    4. 学习并掌握React(2019年7月前)

    18年Vue已经在公司前端全面普及开来,但是市场上React仍然是占据更多的份额,而且RN还能做App开发,所以还是一门必须要掌握的技术,今年看了一本React相关的书,也看了一些相关的视频教程,还参加了公司同事开展的React分享,虽然没有在项目中实际使用过,但是理论知识已经掌握了,可以进行小的Demo开发,20年要用React写一个开源的项目,做到学有所用,学以致用。

    +

    5. 学习前端持续集成的相关知识(2019年9月前)

    19年公司强调了代码必须走线上构建的要求,前端工程化和自动构建持续集成的工作可以说是工作所迫,这方面的技能已经Get到了,另外令我感到高兴的是,通过对Github-Actions的配置,我的博客可以像提交代码一样提交文章了,而且也实践了自动构建的相关技术,很开心。

    +

    6. 学习Docker虚拟化技术( 2019年10月前)

    这个任务没有达成,原本是希望能够通过Docker统一前端开发环境,不过Docker的适用场景还是以部署环境为主,Docker今年貌似热度也降下来了,今年K8s越来越火,不管怎样,持续关注新技术。

    +

    整理计划

    1. 养成写博客的习惯,至少2天一篇,可以是技术博客,也可以是其他。

    今年实在是太忙了,别说两天一篇文章,就连一个月一篇的量都没能达成,不过在年底我终于把博客又重新激活了,希望自己20年能够把博客坚持写下来,很简单,积累自己,打造个人品牌。

    +

    2. 将常用的方法和功能做成插件,开源给公司使用

    今年4月开始,更多的精力都投在了部门的前端管理工作,说实话也迷茫也难过,这份迷茫终于在19年年底解开了,没能在技能层面有更高的提升,以及在代码层面给公司带来价值我觉得还是有些遗憾,20年希望自己能够在技术上有更大的进步,技术深度上/广度上/架构层面/后端技能都能有长足的进步。

    +

    读书计划

    1. 每周读完一本书,并写一篇读后感

    2019年读了25本书,每读完一本都会写一篇读后感,虽然没能保证每周读一本书,但是基本保证了每两周读完一本书,算是完成了50%的目标吧。20年继续努力,读书使人聪明,因为不聪明所以更要多读书。

    +

    部门前端计划

    加强各设计组前端之间的交流

    +

    设计组的出现导致前端交流不畅,2019年加强与各设计组前端的交流,掌握各位前端的技术能力。

    +
    +

    没想到一语成第,设计组居然在4月份将前端整个拿出来构成了一个前端组,由我来带,更没想到的是居然在19年年底又解散到项目团队了,这9个月的时间可以说有苦有泪也有甜,前端小伙伴们为了同一个目标,共同努力,尽最大的努力完成公司的业务,可以说为了这个目标,这一年我付出了很多很多,在这方面愧对了家人孩子,虽然最后前端还是拆分了,可以说我能力有限,但是我心里可以说是无愧,希望尽到项目团队里的小伙伴们也能开心工作,迅速成长,虽然不再在一个团队里,但是,我仍是你们的光妈。

    +

    前端俱乐部推动

    +

    继续每周2小时的兴趣交流,交流形式不限于ppt,要求各自建立技术博客做笔记。

    +
    +

    俱乐部的活动最后还是没能坚持下来,大家工作比较忙也好,我自己的原因也好,确实没能坚持下来,20年还是希望能够将俱乐部活动组织起来,算是大家交流的机会也好,帮助大家开阔眼界也好,20年一定要再做起来!

    +

    进行梯队划分建设

    +

    前端今年人数明显增多,但各层级职责并没有区分,19年抽时间进行梯队建设。

    +
    +

    19年前端从14人增加到了34人,可以说人数上翻翻了,随着负责人制的执行,梯队建设的工作算是完成了50%,但是能力提升的工作可以说还是做的不到位,应该有机会成长为高工的小伙伴没能升上去,这我有一定的责任,20年虽然团队不再,但是希望你们都坚持努力学习,迈向更高的台阶。

    +

    引入前端工程化工具和思想

    +

    目前前端工程化已经非常成熟,希望能将成熟的技术和工具引入。

    +
    +

    19年,随着Vue的普及和熟练,模块化开发组件化开发的思想已经深入到每一个前端的心中,现在大家都知道该如何使用这些技术和工具,希望大家能不仅仅局限在用也能了解了解其中原理,至少对常用的配置有所了解。

    +

    提升整体前端开发的能力

    +

    目前整体前端开发能力还相对较弱,19年通过培训等方式提升整体前端开发的能力

    +
    +

    19年能力提升的工作做的太少了,没什么好说的,希望20年这方面工作能够投入更多的精力,让更多的小伙伴成长起来,让我们一起加油吧。

    +

    生活目标

    每天陪孩子读书一小时

    跟上面的工作和个人目标比起来,这个任务可以说完成度0%,这也是我觉得愧对家人的地方,为了工作,对家人的关注少了很多很多,以至于桐桐晚上都不愿意跟我一起睡觉,20年我要好好对她,做个好的爸爸,孩子明年就三岁了,去到幼儿园会认识更多的朋友,希望她会跟她的小伙伴说,我有个爱我的爸爸。

    +

    减肥

    减肥这件事可以说是真的对我来说太难了,看着自己马上破三位数(kg)的体重,心里无限难过,每次媳妇儿问我啥时候去健身,我都感到像被闪电击中了似的,办了健身卡被人嘲笑成了洗澡卡,20年,减不到150斤我就。。。。

    +

    写在最后

    19年太多的遗憾,太多的后悔莫及,当然也有一些成绩和自我肯定,19年已经过去了,希望在20年,能够成为一个更优秀的人。
    WechatIMG6.jpeg
    感谢我可爱的同事,年底收到了礼物真的很开心。

    +]]>
    + + 自我提升 + 杂记随感 + + + 年终总结 + +
    + + 你好2020 + /2020/01/01/Hello-2020/ + +

    作者:李旭光
    引用请标明出处

    + +

    2020元旦伊始

    时间过的真快,19年睡了一觉就成了过去的一年,今天是元旦,早上8:00起了床,简单洗漱后我出了门,新年新气象,去年没有坚持的晨跑,今年想要坚持一下,所以一早就出门跑了会儿步,太久没活动了,只是跑了半个小时就气喘吁吁,只好回家,但是即使是这半个小时,我感觉呼吸了新鲜的空气,整个人也精神了不少,回到家坐在阳台上,拿出小王宁送我的极少数手册开始研究了起来,生活还是应该有点仪式感何况是新年伊始,手册中有很多建议,全部看过一遍以后我郑重的拿起了笔写下了我今天的行动计划。但是没有写年度计划,明天上班后跟领导还要谈一谈,谈过之后再定一下年度的计划。今天我的计划就是陪家人过好元旦这一天。

    +

    元旦执行计划

      +
    1. 写一篇日志
    2. +
    3. 将家人这一天游玩的过程以Vlog的形式记录下来,上传到西瓜视频上。
    4. +
    5. 陪家人逛街,给桐桐买新衣裳。
    6. +
    +

    执行计划

    吃过早饭后大概到了10:30,一家人收拾好以后终于可以出门了,今天有点风,一点点冷不是特别冷,空气还不错,天气听晴朗的,一家人开着车听着歌前往甘井子万达广场,20分钟左右我们就到了万达广场,首先我们先去看了桐桐心心念念的小猫咪,也就是猫咪咖啡馆里的那些小可爱,桐桐可能是受妈妈影响,很喜欢猫咪,但是又有一点点怕。大概看了10分钟的猫咪,我们继续下一站,上四楼给桐桐买新衣裳,桐桐看到满眼的漂亮衣裳很是兴奋,最终她挑选了一套带着向日葵花朵的淡蓝色衣裙,穿上漂亮的新衣裳,平时像个假小子似的桐桐也一下子害羞了起来,让妈妈和奶奶看完后还会让爸爸和爷爷看看,得到了大家的肯定以后,心满意足的买下了它,也不知道是不是试衣服试累了,桐桐嚷嚷着要吃饭,桐桐妈说有一家港式茶餐厅味道不错,之前和同事来过,于是我们就去了4楼的茶餐厅,说实话这是我头一次吃茶餐厅,我还跟我爸妈调侃说,这也算是我们来过一次香港了。去到餐厅时才11点多一点,客还没有上满,还有座位,我们找了个靠里的位置坐了下来,打开菜谱,桐桐妈挑了几样她觉得不错的菜,上菜时间稍微有点慢,期间桐桐有些不耐烦,坐也坐不住,终于等到了吃的上来,可是觉得大多数吃的都太偏甜,爸妈和我都吃不太惯,桐桐吃的还不错,大概吃了半个小时就吃完了,出门的时候门口已经排起了长队,还好来的早一点,不然吃饭都要等了。本打算带着桐桐回家睡个觉,下午要到姥爷家串门,可桐桐说没有逛够,正巧这时看到了一楼有小丑在折气球玩具,我们就下到一楼找小丑玩了,小丑给桐桐折了个贵宾犬,小丑是荣耀手机的员工,可能是元旦策划的活动吧,反正来了就来了,进去看看也好,看了新出的V30手机,感觉并不是很喜欢,店员说有旧机抵值的活动,我问了他我的7p128g还能值多少,他打了几个电话后回复我1400块,哎,三年前7000块买的手机,现在只值1400了,算了还是用着吧,出了荣耀手机店又去了旁边的华为,看了看新出的mate30pro,这个手机是真的不错,当然手机不错价格也美丽,6500+的价格真的是让我不舍得买,还是把钱留下来给桐桐吧。桐桐终于累了,嚷嚷着要走,于是一家人驱车去了姥姥家。

    +

    姥姥家在西南路,因为平时工作忙很少带桐桐去,这次去桐桐又不认识他们了,开始的时候很拘束,也不说话,一直到吃饭结束才终于愿意开口问好唱歌,因为家里没有电视机,到了姥姥家看到电视机很兴奋,一直在看,今天也算是过了瘾了,亲人就是即使很长时间不见面,但一见面就特别亲的人,饭从5:30一直吃到8:30,聊了工作,聊了生活,聊了困惑,聊了坦然,我终于知道了长辈们的智慧,了解了身体健康的重要性,20年一定要锻炼出一副好的身体,对家人负责。

    +

    9:30回到了家,心里挺高兴的,新年第一天陪家人一起度过了充实的一天,回到家把今天路上录下来的视频剪成了短视频上传到了西瓜视频,这也是我今年的一个想法,记录我们一家人的幸福生活,如果有粉丝有点播的话还能赚点钱,没有的话也记录了桐桐的成长,算是一点纪念吧。最后写下了这篇日记,写完之后我就要睡觉去了,明天一早就要起床,明天起我就要走路去上班了,为了有一副好身体,加油。

    +

    今天的目标都完成了,很开心~~

    +]]>
    + + 自我提升 + 杂记随感 + +
    + + Vue 响应式原理的实现(课程笔记) + /2020/02/21/Implementation-of-the-vue-response-principle/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    最近一直在追网课,说实话从业9年了,一直觉得前端发展非常快,而且一直充满着危机感,每天都要学习进步才有安稳的感觉,今天听了 vue 响应式原理实现的公开课,感觉还不错,做了如下笔记,帮助自己记忆,也希望能帮助大家。

    +

    Vue2 原理

    什么是 defineProperty

    defineProperty 其实是定义对象属性用的

    +
    +

    defineProperty 其实并不是核心的为一个对象做数据双向绑定,而是去给对象做属性标签,只不过是属性里 get 和 set 实现了响应式。

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    属性名默认值
    valueundefined
    getundefined
    setundefined
    writablefalse
    enumerablefalse
    configurablefalse
    +
    var ob = {
    a:1,
    b:2
    }
    // 参数 1、对象 2、属性 3、配置
    Object.defineProperty(ob,'a',{
    writable:false,
    enumerable:true,
    configurable:true,
    })
    console.log(Object.getOwnPropertyDescriptor(ob,'a')) // 1
    ob.a = 2
    console.log(ob.a) // 1
    +

    下面我们实现一下双向绑定

    +
    Object.defineProperty(ob,'a',{
    get:function(){
    console.log('a is be get')
    return 999;
    },
    set:function(){
    console.log('a is be set')
    return 999;
    },
    })

    console.log(Object.getOwnPropertyDescriptor(ob,'a')) // 999

    +

    改造代码实现双向绑定(存取值)

    +
    var _val = obj.a; // 暂存
    Object.defineProperty(ob,'a',{
    get:function(){
    console.log('a is be get')
    return _val;
    },
    set:function(newVal){
    _val = newVal // 新值替换旧值
    console.log('a is be set')
    return _val;
    },
    })
    +

    Vue 中从改变一个数据到发生改变的过程

      +
    1. 改变数据触发 Set
    2. +
    3. Set 部分触发 notify(更新)
        +
      1. Get 部分收集依赖
      2. +
      +
    4. +
    5. 更改对应的虚拟 Dom
    6. +
    7. 重新 Render
    8. +
    +
    // MyVue.js

    // 简单版本 vue
    function MyVue(){
    this.$data = {
    a: {
    b:1
    },
    c:2
    }
    this.el = document.getElementById('app');
    this.virtualDom = '';
    this.observer(this.$data);
    this.render();
    }
    vue.property.observer = function(obj){
    var _val, self = this;
    // var dep = new Dep() -> 源码中依赖收集对象
    for(var key in obj){ // 属性有可能是对象,要递归绑定
    _val = obj[key];
    if(typeof _val === 'Object'){
    this.observer(_val)
    }else{
    Object.defineProperty(this.$data,key,{ // 这里是实际绑定过程
    get:function(){
    // 依赖收集
    // dep.depend(); -> vue 源码中收集依赖的方法
    return _val
    },
    set:function(newVal){
    _val = newVal
    // dep.notify(); -> vue 源码中
    self.render() // AST语法树
    }
    })
    }
    }
    }
    vue.property.render = function(){
    this.virtualDom = 'i am '+this.$data.b;
    this.el.innerHTML = this.virtualDom;
    }
    +
    // index.html

    <!DOCTYPE html>
    <html>
    <head>
    <title>自己实现Vue2数据双向绑定</title>
    </head>
    <body>
    <div id="app"></div>
    <script type="text/javascript" src='myVue.js'></script>
    <script type="text/javascript">
    var mv = new MyVue();
    setTimeout(function(){
    console.log('changes');
    console.log(mv.$data);
    mv.$data.b = 222;
    })
    </script>
    </body>
    </html>
    +
    +

    依赖收集:

    +
      +
    1. 我们的data里面的数据并不是所有地方都用到
    2. +
    3. 如果我们直接更新整个视图,浪费资源
    4. +
    5. 先收集依赖改变的数据的组件,再更新依赖了数据的组件(Dep depend notify)
    6. +
    +
    +

    格外注意的地方—数组怎么监听

    definePropty 只能给对象进行 get set 绑定, 数组怎么办?

    +

    vue 中 使用了 装饰者模式

    +
    +

    装饰者模式 Decorator模式(别名Wrapper):动态将职责附加到对象上,若要扩展功能,装饰者提供了比继承更具弹性的代替方案。

    +
    +
    var arraypro = Array.property; // 创建一个数组的原型对象
    var arrob = Object.create(arraypro); // 避免影响原型链
    var arr = ['push','pop','shift'];
    arr.forEach(function(method,index){
    arrob[method]=function(){ // 装饰者模式
    var ret = arraypro[method].apply(this,arguments)
    dep.notify() // 扩展了功能
    }
    })

    +

    Vue3 实现双向绑定

    Proxy 是什么?

    +
    +

    Proxy 对象用于定义基本操作的自定义行为
    和 definePropty 类似,功能几乎一样,只是用法上不同

    +
      +
    1. 不会污染原对象
    2. +
    3. 直接给对象就可以了
    4. +
    5. 不需要借助外部变量 _val
    6. +
    +
    +
    var ob = {
    a:1,
    b:2
    }

    var newOb = new Proxy(ob,{
    get(target,key,receiver){ // target 对象,key 属性
    console.log(target,key,receiver)
    return target[key]
    },
    set(target,key,value,receiver){
    return Reflect.set(target.key,value);
    // 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
    // return target[key] = value
    }
    })
    +

    为什么改用 Proxy

      +
    1. defineProperty 只能监听某个属性,不能全对象监听
    2. +
    3. 可以省去for in循环提升代码执行效率
    4. +
    5. 可以监听数组,不需要再为数组做特异性操作
    6. +
    7. 不污染原对象
    8. +
    9. 更优雅
    10. +
    +

    我们用 Proxy 实现一下 observe 方法

    +
    vue.property.observe = function(){
    var self = this;
    this.$data = new Proxy(this.$data,{
    get(target,key, receiver){
    return target[key]
    },
    set(target,key,newVal){
    target[key] = newVal
    self.render()
    }
    })
    }
    + +

    还能用 Proxy 做什么

      +
    1. 校验类型
    2. +
    3. 真正的私有变量
    4. +
    +
    校验类型

    例子:

    +
    // 数据类型验证
    // 我们要创建一个对象,这个对象是个人,他有name和age两个属性
    // name必须是中文,age必须是数字,大于18岁

    // 这里用到了策略模式
    var valid = {
    name(value){
    var reg=/^[\u4E00-\u9FAS]=$/
    if(typeof value === 'string' && reg.test(value)){
    return true;
    }
    return false;
    },
    age(value){
    if(typeof value === 'number' && value > 18){
    return true;
    }
    return false;
    }
    }
    function Person(name,age){
    this.name = name
    this.age = age
    return new Proxy(this,{
    get(target,key){
    return target[key]
    },
    set(target,key,value){
    if(valid[key](value)){
    return Reflect.set(target,key,value)
    }else{
    throw new Error(key+'is not valid')
    }
    }
    })
    }
    new Person('name',19)
    +
    +

    策略模式
    定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。

    +
    +
    真正的私有变量

    vue-router 源码中,给 $router ,$route 用 defineProperty 定义 get 并返回本身,这样就不能修改属性了。

    +
    Object.defineProperty(this,'$router',{ // Router 的实例
    get(){
    return this._root._router;
    }
    })
    Object.defineProperty(this,'$route',{
    get(){
    return {
    // 当前路由所在的状态
    current: this._root._router.history.current;
    }
    }
    })
    + +

    虚拟Dom和diff算法

    虚拟Dom是虚拟的,他只在概念里面存在,在AST语法树,下面进行解释

    +
    // 模板
    <template>
    <div>
    <p>{{msg}}</p>
    <p>2</p>
    <p>3</p>
    </div>
    </template>

    // diff 描述法
    diff <div>
    props:{
    id:2
    }
    children:[
    diff <p>
    props:{
    id:xxx
    }
    children:[
    ...
    ]
    ]

    // 对象描述法
    var virtual = {
    dom:'div',
    props:{
    id:2
    },
    children:[
    ....
    ]
    }
    +

    每层结构都是一样的,那么是如何进行 diff 比对的呢?

    +
    /** 
    * diff 算法
    */
    patchVnode(oldVnode,vnode){ // 接收新旧节点
    const el = vnode.el = oldVnode.el; // 拿出真实dom
    let i,oldCh = oldVnode.children ,ch = vnode.children // 拿出新旧节点子元素children数组
    if(oldVnode === vnode) return; // 新旧节点一致,直接返回不进行后续操作
    // 分情况操作 --- 只有文字节点,删除了子元素,增加了子元素,子元素发生变动
    // 新旧节点都不为空,且不一样
    if(oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text){ // 文字发生更新
    api.setTextContent(el,vonde.text) // 文字发生了更新,更新文字,给Dom,更新文字
    } else { // 不是单纯文字节点的话
    updateEle(); // 更新元素
    if(oldCh&&ch&&oldCh!==ch){ // 都有子元素,子元素变动
    updateChildren() // 调用更新子元素方法
    } else if(ch){ // 增加子元素
    createEl(vnode) // 创建子元素
    } else if(oldCh){ // 删除子元素
    api.removeChildren(el) // 调用删除子元素方法
    }
    }
    }
    +

    源码要多看,以下必看 Vue/react/axios/vue-router/Redux/Vuex
    为什么要看源码??

    +
      +
    • 初级前端就会用vue或react — 从差不多水平的60%中挑出更好的人
    • +
    • 提高思想–》看优秀的代码–》写优秀的代码
    • +
    • 看源码能力,对高级前端是必备的。— 解决疑难杂症,看源码了解原理。
    • +
    +

    vue 性能优化

    因为是公开课,所以时间上没来的及说完,以后自己在听别的有关的内容时再补上这块。

    +

    最后

    只有不断学习才能进步,充分利用网络的便利性,找各种优质的教学资源,我相信,努力会有回报,加油!

    +]]>
    + + 源码原理 + + + Vue + +
    + + JavaScript 设计模式 Design Pattern + /2020/01/13/Javascript-Design-Pattern/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    最近在整理自己的知识体系,一方面为了夯实基础,另一方面也为了查缺补漏、重新理解,设计模式之前看过大概三四次,从最开始一点都不懂,到后面看了几遍以后慢慢理解,今天再次整理一下,希望这一遍下来之后能更加透彻的理解什么是设计模式。

    + + +

    什么是设计模式

    +

    设计模式的定义:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案
    设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。

    +
    +
    +

      使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的;设计模式使代码编制真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。 —- 百度百科

    +
    +

    不知道大家看了上面的定义以后是什么感受,说实话我第一次看到这句话并没有什么深刻的认识,什么面向对象的软件设计,什么针对特定问题,什么优雅的解决方案,这都说的是什么,后来我看了几遍之后,上面这句话用我的理解翻译如下:

    +
    +

    软件开发过程中,解决某一类问题用到的一系列套路

    +
    +

    这就是我对设计模式的认识。当然这也不仅仅是我自己的认识,在跟其他的一些开发人员交流时,很多人都是这么认为的。

    +

    这些解决问题的方案实在是太好用了,所以大神就把它们抽象出来,然后起了个名字-就叫做设计模式了。

    +

    这么说大家可能还是不太明白,举个开发过程中可能遇到的实际例子吧。

    +
    +

    当系统中某个接口的结构已经无法满足我们现在的业务需求,但又不能改动这个接口,因为可能原来的系统很多功能都依赖于这个接口,改动接口会牵扯到太多文件。那么这种场景下我们该如何解决这个问题呢?通常我们需要新增一个接口,兼容原来的接口和新的业务需求参数。
    因此应对这种场景,我们可以很快地想到可以用适配器模式来解决这个问题。

    +
    +

    这就是设计模式的应用,实际上也许你还不知道设计模式这个词,但是你已经在工作中频繁的用到了设计模式,下面我们就来看看到底有哪些设计模式。

    +

    哦,对了,设计模式并不依赖于语言,它本身更像是一种软件的设计思想,因为我是一个前端,所以接下来具体实现的时候我会使用js来实现设计模式的用法。

    +

    学习设计模式

    目前被普遍接受的经典的设计模式共有 23 种,而这23种设计模式又分为了 3大类 ,看过一张图这里拿过来镇贴。
    lHgD4H.jpg
    他们分别是

    +
      +
    • 创建型模式
    • +
    • 结构型模式
    • +
    • 行为型模式
    • +
    +

    接下来,我将会将这23种,3大类设计模式一个个的拆解开来,跟大家一起学习一下,设计模式有哪些内容。

    +

    创建型模式 6个

    这类模式用于对象的生成生命周期的管理
    创建型模式可以决定生成哪些对象,提高了程序的灵活性。具体属于此类的模式清单如下,共有 5 个:

    +
      +
    • 单例模式(Singleton)
    • +
    • 工厂方法模式(Factory Method)
    • +
    • 抽象工厂模式(Abstract Factory)
    • +
    • 建造者模式(Builder)
    • +
    • 原型模式(Prototype)
    • +
    • 迭代器模式(Iterator)
    • +
    +

    单例模式(Singleton)

    描述:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

    +

    工厂方法模式(Factory Method)

    描述:定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。

    +

    抽象工厂模式(Abstract Factory)

    描述:为创建一组相关或相互依赖的对象提供一个接口,而且无需指定它们的具体类

    +

    建造者模式(Builder)

    描述:将一个复杂对象的构建与他的表示分离,使得同样的构建过程可以创建不同的表示。

    +

    原型模式(Prototype)

    描述:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

    +

    迭代器模式(Iterator)

    描述:提供一种方法访问一个容器对象中的各个元素,而又不需暴露该对象的内部细节。

    +

    结构型模式 7个

      +
    • 适配器模式(Adapter)
    • +
    • 组合模式(Compositor)
    • +
    • 代理模式(Proxy)
    • +
    • 桥梁模式(Bridge)
    • +
    • 装饰模式(Decorator)
    • +
    • 门面模式(Facade)
    • +
    • 享元模式(Flyweight)
    • +
    +

    适配器模式(Adapter)

    描述:将一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。

    +

    组合模式(Compositor)

    描述:将对象组合成树形结构

    +

    代理模式(Proxy)

    描述:

    +

    桥梁模式(Bridge)

    描述:

    +

    装饰模式(Decorator)

    描述:

    +

    门面模式(Facade)

    描述:

    +

    享元模式(Flyweight)

    描述:

    +

    行为型模式 10个

      +
    • 命名模式(Command)
    • +
    • 解释器模式(Interpreter)
    • +
    • 责任链模式(Chian of Responsibility)
    • +
    • 观察者模式(Observer)
    • +
    • 中介者模式(Mediator)
    • +
    • 备忘录模式(Memento)
    • +
    • 状态模式(State)
    • +
    • 策略模式(Strategy)
    • +
    • 模板方法模式(Template Method)
    • +
    • 访问者模式(Visitor)
    • +
    +

    命名模式(Command)

    描述:

    +

    解释器模式(Interpreter)

    描述:

    +

    责任链模式(Chian of Responsibility)

    描述:

    +

    观察者模式(Observer)

    描述:

    +

    中介者模式(Mediator)

    描述:

    +

    备忘录模式(Memento)

    描述:

    +

    状态模式(State)

    描述:

    +

    策略模式(Strategy)

    描述:

    +

    模板方法模式(Template Method)

    描述:

    +

    访问者模式(Visitor)

    描述:

    +]]>
    + + 前端技术 + + + 设计模式 + +
    + + 《高性能JAVASCRIPT》读书笔记 + /2013/06/17/Read-High-Performance-JavaScript/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    最近读了《高性能JAVASCRIPT》,下面将书中觉得有用的内容进行摘抄,放在下方。

    +

    如何从小事提升JAVASCRIPT性能。

      +
    1. <script>标签写在</body>之前——将脚本放在底部。

      +
    2. +
    3. 尽量少的<script>标签,减少页面中的外链数量,减少请求次数。

      +
    4. +
    5. 使用打包工具如:Yahoo!combo handler

      +
    6. +
    7. 使用动态延迟加载技术如:LazyLoad类库,LABjs

      +
      // js代码
      <script type="text/javascript" src="lazyload-min.js"></script>
      <script type="text/javascript">
      LazyLoad.js([],function(){
      Application.init();
      })
      </script>
    8. +
    9. 在javascript中存储位置十分重要,尽量用局部变量代替对象成员的访问。

      +
    10. +
    11. 集合变数组提高查询效率

      +
      // js代码
      function toArray(coll){
      for(var i = 0, a=[], len=coll.length; i<len; i++){
      a[i]=col[i];
      }
      return a;
      }
    12. +
    13. 使用局部变量缓存访问多次的成员
      当便利一个集合时,首要优化原则是把集合存储在局部变量中,并把length缓存在循环外部,然后使用局部变量访问这些需要多次访问的元素。

      +
    14. +
    15. 使用原生DOM方法querySelectorAll()遍历查找元素。

      +
    16. +
    17. 让元素脱离动画流,页面重排次数越多,程序响应越慢,要尽量减少重拍
      方法:

      +
    18. +
      1. +
      2. 使用绝对定位使元素脱离文档流
      3. +
      +
    19. +
    20. IE:hover
      在大量元素使用:hover这个伪css选择器时,程序会降低响应速率,在大表格或列表的情况下

      +
    21. +
    +]]>
    + + 自我提升 + 读书笔记 + +
    + + 《让老板提拔你》读书笔记 + /2015/12/31/Read-Let-your-boss-promote-you/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    最近读了《让老板提拔你》,下面将书中觉得有用的内容进行摘抄,放在下方。

    +

    定位自己

    正确认识自己,确定社会定位、职业定位。 定位-决定-定价

    +

    要素

    核心竞争力职位 契合度 是高薪关键所在

    +

    契合度

      +
    • 技能、专长、经历与职位要求的契合度
    • +
    • 专业资质和等级与职位要求的契合度
    • +
    • 综合素质与职位要求的契合度

      七大秘诀

    • +
    • 了解同行业薪酬的平均水平
    • +
    • 赢得未来单位的心
    • +
    • 先让对方开口
    • +
    • 勇敢地开口要求
    • +
    • 不要轻言放弃
    • +
    • 把握时机很重要
    • +
    • 说实话,别撒谎

      如何谈薪资

    • +
    • 商洽薪酬的关键在于把握好谈话时机,切勿开门见山。
      +

      只要有发展机会,我愿意接受贵单位的薪酬标准,不知道按规定这个工作岗位的薪酬标准是多少。

      +
      +
    • +
    +

    将知识卖个好价钱

    推销自己既推销自己所掌握的知识即智慧,正确认识自己的价值是关键。

    +

    高薪是因为“物有所值”

      +
    • 用业绩、用能力说话,是人才坦然面对高薪的心态。
    • +
    • 高薪人才的求职方式 —- 圈内朋友引见、定向投简历、行业论坛、交流活动等。
    • +
    • 高级人才不能经常跳槽要保持相对的职业稳定。—- 在一家公司少则3年多则6年 —- 接受挑战,把握机会。
    • +
    +

    失败丰富走向成功经验

    强调在失败中吸取的经验,在未来中可以避免的损失。

    +

    能为企业带来丰厚的利润才是人才

    企业招聘员工的唯一目的:追求公司业绩最大化、利润最大化。

    +

    高质高效工作全攻略

      +
    • 进行正确的自我评价
    • +
    • 做最擅长做的事
        +
      • 三个经济原则 —- 发挥人才优势。
          +
        1. 比较利益原则—–自己的专长对自己才是最有利的,勿与他人作比较。
        2. +
        3. 机会成本原则—–选择一份工作放弃其他工作就是机会成本。
        4. +
        5. 效率原则——–工作的成功不在于时间有多长,而在于成效有多少,附加值有多少。
        6. +
        +
      • +
      +
    • +
    • 马上行动
    • +
    • 做事情要雷厉风行 ——— 成功属于踏实肯干的人
    • +
    • 有条不紊地开展工作 ——- 制定时间计划
    • +
    • 善于利用现代办公工具
    • +
    • 给自己最大的工作空间
    • +
    • 建立高效有序的办公环境
    • +
    • 不要忘记最初想去的方向
    • +
    • “聪明”的向上级提出建议
    • +
    • 专心做事,避免浮躁
    • +
    • 多而不专,一事难成
    • +
    • “专”才有高质工作——–人才主要指专才–某个领域的高精尖人才–勿做全才,一专多能
    • +
    • 做事要有条理
    • +
    +

    不要只把自己当成一个打工仔

    +

    要把工作当成事业

    +
    +
      +
    • 工作不仅仅是为了钱
    • +
    • 对工作要有明确的价值取向
        +
      1. 认清人生的方向
      2. +
      3. 开始学会醉卧探索和认知
      4. +
      5. 认清工作价值与成就的关系
      6. +
      7. 长期的工作规划
      8. +
      9. 在生命的天平上衡量自身的价值
      10. +
      +
    • +
    • 巧妙应对与上司看法向左时的三条准则
        +
      1. 遇事考虑全局
      2. +
      3. 辩证地看待问题
      4. +
      5. 切记感情用事
      6. +
      +
    • +
    • 把单位的事当成自家的事
    • +
    • 认真负责地用心工作
    • +
    • 珍惜岗位,热爱自己的职业
    • +
    • 永远是在为自己工作
    • +
    • 敬重自己的工作
    • +
    • 不要轻视薪水微薄的工作
    • +
    • 永远对工作充满激情
    • +
    • 以自己的工作为荣
    • +
    • 不要被他人的观点所束缚
    • +
    • 暂时的胜负并不会决定人生的最后走向
    • +
    • 将弱势转化为优势
    • +
    • 全力以赴做好每一天的工作
    • +
    • 和优秀的人士在一起—见贤思齐、借梯爬楼
        +
      • 如何争取跟优秀的人在一起
          +
        1. 不断的抛头露面
        2. +
        3. 帮助可以帮助自己成就事业的人做事
        4. +
        5. 与上司和比自己优秀的人士一起合作
        6. +
        +
      • +
      +
    • +
    +
      +
    1. 尊重对方,严谨有致
    2. +
    3. 切记奉承,要不卑不亢
    4. +
    5. 态度自然,不必拘谨
    6. +
    7. 陪衬得当,不可狂妄
    8. +
    9. 主动真诚,做出姿态
    10. +
    11. 求助求教,接受呵护
    12. +
    +
      +
    • 挑战自我,承担责任
        +
      • 三条忠告
          +
        1. 全心全意工作
        2. +
        3. 把自己视为合伙人
        4. +
        5. 迎接变革的需求
        6. +
        +
      • +
      +
    • +
    • 自信独立,不随波逐流
    • +
    • 敢于显示自己很重要
    • +
    • 千万不能只知道抱怨上司
    • +
    • 保持严谨认真的做事习惯
    • +
    • 自主地做好手中的工作
    • +
    • 踏踏实实地做好本职工作
    • +
    • 丢掉工作散漫的坏习惯
    • +
    • 不要让浮躁的性格困扰自己
    • +
    • 不推诿,勇于承担责任
    • +
    • 无论如何都不要拖延工作
    • +
    • 糊弄工作只能是在糊弄自己
    • +
    • 逊色的工作只会淘汰自己
    • +
    • 千万别丢掉“得宠”之资
    • +
    • “一步登天”只会摔疼自己
    • +
    • 别让“差不多”贻误了自己
    • +
    • 能完成100%,就决不做99%
    • +
    +

    与上司相处

      +
    • 不要做上司的“心腹”
    • +
    • 适时恰当的赞美上司
        +
      • 赞美上司,还要善于选择适当的场合
      • +
      • 赞美上司,要学会巧借公众语言称赞
      • +
      • 赞美上司,还要善于赞美不得志的上司
      • +
      +
    • +
    • 主动与领导沟通
    • +
    • 主动和上司保持联系
    • +
    • 用“心机”主动接近上司
        +
      • 尽可能详细的了解上司
      • +
      • 选择一个与领导尽可能近的位置
      • +
      • 赢得上司青睐的方法
      • +
      +
    • +
    • 更有效的和上司沟通
        +
      • 与上司沟通要简洁
      • +
      • 与上司沟通要大度大气大方
      • +
      • 与上司沟通,就要把自己先放一边 ——– 说话对事不对人
      • +
      +
    • +
    • 四种和上司进行沟通的方法
        +
      1. 开诚布公式———提出问题,说出自己的理解认识,给出解决方案,寻求领导意思,执行领导的命令
      2. +
      3. 先斩后奏式———因主客观原因导致的没有先汇报而是先解决的事,事后要将问题的分析方法,解决方法,实施方案以及结果做一份详尽的总结报告上交给领导
      4. +
      5. “含情脉脉”式——-通过交流一些工作上的问题来含蓄表达出个人的建议、需要
      6. +
      7. 巧设比喻式———通过典型案例来暗示自己的意见和要求
      8. +
      +
    • +
    • 把话说到上司的心坎上
    • +
    • 巧妙地为领导拾遗补缺
        +
      1. 诠释领导讲话的难点
      2. +
      3. 强调领导的才干
      4. +
      5. 化严肃为幽默
      6. +
      7. 稳定情绪,委婉暗示
      8. +
      +
    • +
    • 工作中勤于请示汇报
        +
      1. 听懂上司的意图
      2. +
      3. 探讨、磨合,达成共识
      4. +
      5. 制定尽可能详尽的工作计划
      6. +
      7. 随时向上司汇报任务的关键点
      8. +
      9. 总结汇报
      10. +
      +
    • +
    • 用成功赢得上司的信任
    • +
    • 工作中不要冲撞上司
    • +
    • 处理好同上司之间的分歧
        +
      1. 圆融协调——领导不懂,下达了错误的指令
          +
        1. 私下向上司陈述意见,帮助上司做出正确的决策
        2. +
        3. 如果上司采纳了下属的建议或意见,该下属有责任长期保守秘密
        4. +
        5. 如果上司固执己见,那么无条件服从
        6. +
        +
      2. +
      3. 装聋作哑——不涉及到原则问题
      4. +
      5. 棘手难题多权衡
          +
        1. 立刻插话纠正
        2. +
        3. 提醒上司
        4. +
        5. 暗示
        6. +
        7. 事后补救
        8. +
        9. 事后提醒
        10. +
        +
      6. +
      +
    • +
    • 正确对待上司的批评
    • +
    • 要善于服从自己的上司
    • +
    • 正确化解来自上司的压力
    • +
    +

    写在最后

    博观约取,多读书读好书,丰富自己,变得睿智。

    +]]>
    + + 自我提升 + 读书笔记 + +
    + + 《晨间日记的奇迹》读书笔记 + /2016/10/27/Read-The-miracle-of-the-morning-journal/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    昨天花了1天的时间读了《晨间日记的奇迹》,感觉整个人的认识发生了一些变化,下面把重要内容做一下摘抄总结。

    +

    早上写日记的好处 —— 提升效率

      +
    • 可以做好一天的准备 — 计划性
    • +
    • 可以正确的写出昨天发生的事 — 效率性&忠诚性
    • +
    • 可以中立的看待昨天 — 中立性
    • +
    • 相对自由的时间 — 持续性
    • +
    • 总结经验 — 活用性
    • +
    +

    注意事项

    日记 不等于 日志
    日记 要记录 时间 天气 心情 — 因为这些有助于回忆和记录
    不要投入过长时间 — 3分钟 — 日记私密性
    晨间日记前7天只写好事 — 反省固然重要,但是成长更重要,持续性很重要 — 巴普洛夫的狗

    +

    晨间日记2部分

    Part1

    客观记录已经发生的事(昨天)— 经验智慧

    +

    Part2

      +
    • 今天应做的事 — 具体行动(来自昨天的总结
    • +
    • 今天一定要做的事(紧急重要的事)— 变得更积极更主动
    • +
    • 未来要做的事 — 不紧急但重要的事
    • +
    • 连用日记 — 历史上的今天(过去一年同一天的事)
    • +
    +

    夜晚日记 VS 晨间日记

    受当天情绪影响 — 更冷静

    +

    梦想成真表

    + + + + + + + + + + + + + + + + + +
    过去未来
    事实IQ 智慧指数NQ 人际关系指数
    感情EQ 情感指数DQ 梦想指数
    +
      +
    • IQ — 晨间日记冷静回忆分析 — 提高决策正确性 IQ up
    • +
    • EQ — 冷静记录昨日的情绪/自省 — 变积极 EQ up
    • +
    • NQ — 总结昨天给今天设立“对/为XXX做某事” 很重要 NQ up
    • +
    • DQ — 不能只有眼前的事 每天都要确定一下自己的梦想 脸皮厚点没关系 重复或只有一句话也没关系 铭记于心最重要
    • +
    +

    “忙碌”只是借口!“决心”才是问题! 对未来充满期待的人才有行动力

    +

    如何早起

      +
    • 设定起床音乐/用气味(精油)刺激起床/光线-电动窗帘早起自动拉开
    • +
    • 早起后淋浴 — 配合音乐/冥想/呐喊 — 让自己充满动力保持清醒
    • +
    +

    写日记的五大好处

      +
    • 提升写作能力
    • +
    • 谈话题材源源不断
    • +
    • 提高贵人运
    • +
    • 返现自我肉体和精神的状态与模式
    • +
    • 在自己身上挖宝,彻底改变人生
    • +
    +

    记录的日记要常拿出来看看

    记录日记时 — 问问自己的灵魂 然后写下真心话 — 灵魂日记
    六度空间理论

    +

    七种成功者的习惯

      +
    • 习惯之一:积极主动别指望谁能推你走 — 主动行动,迈出第一步
    • +
    • 习惯之二:以终为始忠于自己的人生计划。— 设立人生目标并坚持
    • +
    • 习惯之三:要事第一选择当前该做的事
    • +
    • 习惯之四:追求双赢远离角斗场
    • +
    • 习惯之五:善于沟通换位思考的原则
    • +
    • 习惯之六:统合综效 1+1可以大于2
    • +
    • 习惯之七:不断更新全方位平衡自我
    • +
    +

    早睡是为了身体,早起是为了我们的内心。— sugiponn

    +

    晨间日记的格式

    晨间日记必须要设立的栏目 — 工作/金钱/健康/人际关系
    要记下当日的日期/天气/温度/湿度

    +

    纬度标签
    工作方面:

    +
      +
    • 能力开发/经营/管理/进货/工作人员/学习/资格/活动/企划案/想法/推广
      金钱方面:
    • +
    • 收入/指出/购入/股票/资产/储蓄/家用
      健康方面:
    • +
    • 饮食/运动/性/减肥/身体状况/锻炼身体/体重/脂肪
      人际关系方面:
    • +
    • 父母/家人/小孩/亲戚/情人/工作伙伴/朋友/熟人/联络事项/人脉网络
      兴趣方面以及其他:
    • +
    • 义工活动/阅读/运动/打扮/电影/音乐/游戏/喜欢的物品/今天必须要注意的事/美容/开心的事/购物
    • +
    +

    劳动 — 职业 — 工作 — 乐趣

    +

    三大原则和七大作战守则

      +
    • 原则1:时间不超过3分钟 — 减少养成习惯的成本

      +
    • +
    • 原则2:决定好写晨间日记的地方 — 为了养成习惯

      +
    • +
    • 原则3:只写一个字也没关系 — 不要有压力

      +
    • +
    • 作战守则1:开心写日记 — 反省固然重要,鼓励更有作用 — 开始写晨间日记前七天只写好事

      +
    • +
    • 作战守则2:ONE-TWO作战 — 把动作分布容易养成习惯 — 形成固定动作

      +
    • +
    • 作战守则3:巴普洛夫的狗 — 开始写日记时候要有一个仪式 — 心理暗示

      +
    • +
    • 作战守则4:奖励自己 — 当习惯养成并坚持到一定时间,给自己一个奖励

      +
    • +
    • 作战守则5:宣告天下 — 借由他人的鼓励或压力形成动力/也可找到同志

      +
    • +
    • 作战守则6:一千日作战法 — 不管什么样的习惯坚持1000日就会成为永远的习惯 — 具体做法为记录 第XXX篇 晨间日记

      +
    • +
    • 作战守则7:为了别人 — 写晨间日记不只是为了自己,而且更是为了别人,身边人的幸福最终会影响到自己

      +
    • +
    +

    应该先肯定自己,给自己打100分

      +
    • “总之,先… 病” 没有目标/没有梦想 — 回顾时一定会后悔!
    • +
    • “假如…,应该 病” 无法满足/没有动力 — 焦躁不安
    • +
    • 共同点:失落感/空虚/
    • +
    +

    解决办法— 设立一个情境

    例:我要建立一个幸福的家庭 —> 因此我要创造很多快乐的回忆 —> 为了制造很多快乐的回忆 —> 我必须要到很多的地方去看看 —> 为了去很多地方看看,我觉得有辆车会更方便 —> 买车 —> 考驾照

    +

    不要总想自己哪里欠缺,要想自己想要得到什么,并且为了目标努力,你就会变的更完美。
    拥有一个敢于大声喊出的梦想,不要羞涩,要为了梦想一步步的前进,最终达到梦想。

    +

    提到的另外的书

    《培育梦想种子》《日记的力量》《成功人士的七个习惯》

    +]]>
    + + 自我提升 + 读书笔记 + +
    + + SSR + /2020/02/11/SSR/ + +

    作者:李旭光
    引用请标明出处

    + +

    什么是SSR

    传统浏览器的vue纯浏览器渲染

    浏览器请求服务器,服务器返回静态资源 ,客户端负责渲染js

    +

    ssr

    浏览器请求服务器,node服务端渲染js后,返回对应的页面返回给浏览器

    +

    SSR需要那些东西

    手写SSR

    特性:

    +
      +
    • 每一次访问必须新建一个vue实例
    • +
    • 只会触发组件的 beforeCreate和created钩子
    • +
    +

    核心库

    +
      +
    • vue
    • +
    • vue-server-renderer

      vue + next

      +

      作者:李旭光
      引用请标明出处

      +
      +
    • +
    +

    前言

    +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    ]]>
    + + 前端技术 + + + 服务端渲染 + 同构技术 + SSR + +
    + + 使用阿里镜像加速brew(转载) + /2020/03/03/Speeding-up-brew-with-Ali-mirroring/ + +

    作者:李旭光
    引用请标明出处

    + +

    使用 Alibaba 的 Homebrew 镜像源进行加速

    平时我们执行 brew 命令安装软件的时候,跟以下 3 个仓库地址有关:

    +
      +
    • brew.git
    • +
    • homebrew-core.git
    • +
    • homebrew-bottles
      通过以下操作将这 3 个仓库地址全部替换为 Alibaba 提供的地址
    • +
    +

    1. 替换 / 还原 brew.git 仓库地址

    # 替换成阿里巴巴的 brew.git 仓库地址:
    cd "$(brew --repo)"
    git remote set-url origin https://mirrors.aliyun.com/homebrew/brew.git

    #=======================================================

    # 还原为官方提供的 brew.git 仓库地址
    cd "$(brew --repo)"
    git remote set-url origin https://github.com/Homebrew/brew.git
    + +

    2. 替换 / 还原 homebrew-core.git 仓库地址

    # 替换成阿里巴巴的 homebrew-core.git 仓库地址:
    cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
    git remote set-url origin https://mirrors.aliyun.com/homebrew/homebrew-core.git

    #=======================================================

    # 还原为官方提供的 homebrew-core.git 仓库地址
    cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
    git remote set-url origin https://github.com/Homebrew/homebrew-core.git
    + +

    3. 替换 / 还原 homebrew-bottles 访问地址

    这个步骤跟你的 macOS 系统使用的 shell 版本有关系

    +

    所以,先来查看当前使用的 shell 版本

    +
    echo $SHELL

    # 如果你的输出结果是 /bin/zsh,参考下方的 zsh 终端操作方式
    # 如果你的输出结果是 /bin/bash,参考下方的 bash 终端操作方式
    +

    3.1 zsh 终端操作方式

    # 替换成阿里巴巴的 homebrew-bottles 访问地址:
    echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles' >> ~/.zshrc
    source ~/.zshrc

    #=======================================================

    # 还原为官方提供的 homebrew-bottles 访问地址
    vi ~/.zshrc
    # 然后,删除 HOMEBREW_BOTTLE_DOMAIN 这一行配置
    source ~/.zshrc
    +

    3.2 bash 终端操作方式

    # 替换 homebrew-bottles 访问 URL:
    echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles' >> ~/.bash_profile
    source ~/.bash_profile

    #=======================================================

    # 还原为官方提供的 homebrew-bottles 访问地址
    vi ~/.bash_profile
    # 然后,删除 HOMEBREW_BOTTLE_DOMAIN 这一行配置
    source ~/.bash_profile
    + +

    转载自:http://www.xiegangd.com/article/154055689187484

    +]]>
    + + 杂七杂八 + + + HomeBrew + Mac + 海外提速 + +
    + + v8引擎如何回收内存(笔记) + /2020/02/23/V8-engine-memory-management-and-optimization/ + +

    作者:李旭光
    引用请标明出处

    + +

    V8引擎如何回收垃圾

    为什么我们要关注内存

      +
    • 防止页面占用内存过大,引起客户端卡顿,甚至无响应
    • +
    • Node使用的也是v8,内存对于后端服务的性能至关重要。因为服务的持久性,后端更容易造成内存溢出

      v8引擎的内存回收机制

      v8的内存分配

      新生代内存空间
    • +
    • from
    • +
    • to
      老生代内存空间

      内存大小

    • +
    • 和操作系统有关 — 64位(1.4G)32位(0.7G)
    • +
    • 64位下 新生代(64MB) 老生代(1400MB)
    • +
    • 32位下 新生代(16MB) 老生代(700MB)
    • +
    +

    为什么不占多一点内存

    +
      +
    • js设计之初是为浏览器
        +
      • 前端特点 — 不持久化,执行一遍就回收了,所以1.4G够用了
      • +
      • js回收内存会暂停执行代码
      • +
      +
    • +
    +

    垃圾回收算法

    新生代简单的说就是复制

    +
      +
    • 新生代用来放新产生的变量,根据条件放到老生代,有用的放到to里(把有用的留一边,清空另一边,往复进行)
    • +
    • 算法复杂度(时间复杂度,空间复杂度)—- 牺牲空间换时间
    • +
    +

    老生代就是标记、删除、整理

    +
      +
    • 为什么要整理
        +
      • 数组是需要连续的空间
      • +
      +
    • +
    +

    新生代如何晋升到老生代

    +
      +
    • 变量是否经过回收,第二次回收放到老生代,第一次回收放到To
    • +
    • To空间使用了25%,放到老生代
    • +
    +

    V8是如何处理变量的

    // 浏览器查看内存
    window.performance
    // nodejs查看内存 --- nodejs是c++的,可以拓宽内存
    process.memoryUsage()

    // 拿内存的方法
    function getMem(){
    var mem = process.memoryUsage();
    var format = function(bytes){
    return (bytes/1024/1024).toFixed(2)+'MB';
    }
    console.log('heapTotal:'+format(mem.heapTotal)+'heapUsed:'+format(mem.heapUsed))
    }
    +

    变量处理

      +
    • 内存主要就是存储变量等数据的
    • +
    • 局部变量当程序执行结束,且没有引用的时候就会随着消失 — 可以被回收但不是说会马上回收
    • +
    • 全局对象会始终存活到程序运行结束
    • +
    +

    如何查看V8内存使用情况

    如何注意内存使用

    优化内存的技巧

      +
    • 尽量不要定义全局变量
    • +
    • 全局变量记得手动销毁掉
        +
      • 不推荐开发时写delete – 支持有问题,严格模式有bug
      • +
      • 赋值为 undefined/null undefined 是变量 null 是保留字
      • +
      +
    • +
    • 用匿名自执行函数变全局为局部
        +
      • (function(){})()
      • +
      +
    • +
    • 尽量避免使用闭包 —- 错误的观点,ie5时代的问题 —- 尽量避免使用闭包引用
    • +
    +

    闭包

    +
    function a(){
    var size = 20*1024*1024;
    var arr1 = new Array(size)
    return arr1
    }
    a() // 这样就没问题
    var b = a() // 因为引用所以无法销毁
    +

    防止内存泄漏

      +
    • 滥用缓存
    • +
    • 大内存量操作
    • +
    +

    所有的优化都可以用缓存来解决,缓存通常都在全局,缓存可以直接拿来用;

    +
    var 20*1024*1024;
    var a = []
    for(var i=0;i<13;i++){
    a.push(new Array(size))
    }

    // 加缓存锁
    for(var i=0;i<13;i++){
    if(a.length>4){
    a.shift();
    }
    a.push(new Array(size))
    }
    +
      +
    • 不要用v8来缓存
        +
      • 一定要用要的话加锁
      • +
      +
    • +
    +

    nodejs中读取大文件要用流的形式,不要用读文件到buffer
    fs.readFile()
    fs.createReadStream()

    +

    浏览器中,大文件上传记得切片
    file.slice(0,1000)
    file.slice(1000,2000)

    +]]>
    + + 前端技术 + + + 内存回收 + +
    + + VSCode ESLint JS代码静态检测工具 + /2017/10/12/VSCode-ESLint/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    VSCode是新一代IDE,方便灵活的插件安装方式让它强大无比,对前端友好,让它成为了越来越多前端开发者的首选IDE,编辑器虽好,但是要自己安装符合自己需求的插件才行,下面介绍一款插件,名叫ESLint,是用来静态检测js代码的工具,让你的js代码在编辑过程中就能找到错误,提高代码质量,好了废话不多说下面介绍如何安装和配置这款插件。

    + +

    安装ESLint插件

    打开VSCode编辑器,在左侧右下角有一个安装插件的图标,点击后就可以打开插件市场,输入ESLint,就会有个黄色的图标出现在你面前,不用犹豫双击它,稍等一会它就安装完了,是不是超简单。

    +

    安装NPM依赖

    ESLint插件运行需要一些依赖,对于用过npm包管理工具的人来讲小意思啦,我把代码放到下面,需要的直接粘贴运行就好。

    +
    //全局安装eslint
    npm i eslint -g

    //如果用到html中的js校验
    npm i eslint-plugin-html -g

    //如果用到es2015语法
    npm i babel-eslint -g
    +

    配置eslint配置文件到项目根目录

    配置文件名称如下:
    eslintrc.json
    内容为:

    +
    {
    "plugins": [
    // "react",
    "html"
    ],
    "env": {
    "node": true,
    "jquery": true,
    "es6": true,
    "browser": true
    },
    "globals": {
    "angular": false
    },
    "parser": "babel-eslint",
    "rules": {
    //官方文档 http://eslint.org/docs/rules/
    //参数:0 关闭,1 警告,2 错误
    // "quotes": [0, "single"], //建议使用单引号
    // "no-inner-declarations": [0, "both"], //不建议在{}代码块内部声明变量或函数
    "no-extra-boolean-cast": 1, //多余的感叹号转布尔型
    "no-extra-semi": 1, //多余的分号
    "no-extra-parens": 0, //多余的括号
    "no-empty": 1, //空代码块

    //使用前未定义
    "no-use-before-define": [
    0,
    "nofunc"
    ],

    "complexity": [0, 10], //圈复杂度大于*

    //定义数组或对象最后多余的逗号
    "comma-dangle": [
    0,
    "never"
    ],

    // 不允许对全局变量赋值,如 window = 'abc'
    "no-global-assign": ["error", {
    // 定义例外
    // "exceptions": ["Object"]
    }],
    "no-var": 0, //用let或const替代var
    "no-const-assign": 2, //不允许const重新赋值
    "no-class-assign": 2, //不允许对class重新赋值
    "no-debugger": 1, //debugger 调试代码未删除
    "no-console": 0, //console 未删除
    "no-constant-condition": 2, //常量作为条件
    "no-dupe-args": 2, //参数重复
    "no-dupe-keys": 2, //对象属性重复
    "no-duplicate-case": 2, //case重复
    "no-empty-character-class": 2, //正则无法匹配任何值
    "no-invalid-regexp": 2, //无效的正则
    "no-func-assign": 2, //函数被赋值
    "valid-typeof": 1, //无效的类型判断
    "no-unreachable": 2, //不可能执行到的代码
    "no-unexpected-multiline": 2, //行尾缺少分号可能导致一些意外情况
    "no-sparse-arrays": 1, //数组中多出逗号
    "no-shadow-restricted-names": 2, //关键词与命名冲突
    "no-undef": 1, //变量未定义
    "no-unused-vars": 1, //变量定义后未使用
    "no-cond-assign": 2, //条件语句中禁止赋值操作
    "no-native-reassign": 2, //禁止覆盖原生对象
    "no-mixed-spaces-and-tabs": 0,



    //代码风格优化 --------------------------------------
    "no-irregular-whitespace": 0,
    "no-else-return": 0, //在else代码块中return,else是多余的
    "no-multi-spaces": 0, //不允许多个空格

    //object直接量建议写法 : 后一个空格前面不留空格
    "key-spacing": [
    0,
    {
    "beforeColon": false,
    "afterColon": true
    }
    ],

    "block-scoped-var": 1, //变量应在外部上下文中声明,不应在{}代码块中
    "consistent-return": 1, //函数返回值可能是不同类型
    "accessor-pairs": 1, //object getter/setter方法需要成对出现

    //换行调用对象方法 点操作符应写在行首
    "dot-location": [
    1,
    "property"
    ],
    "no-lone-blocks": 1, //多余的{}嵌套
    "no-labels": 1, //无用的标记
    "no-extend-native": 1, //禁止扩展原生对象
    "no-floating-decimal": 1, //浮点型需要写全 禁止.1 或 2.写法
    "no-loop-func": 1, //禁止在循环体中定义函数
    "no-new-func": 1, //禁止new Function(...) 写法
    "no-self-compare": 1, //不允与自己比较作为条件
    "no-sequences": 1, //禁止可能导致结果不明确的逗号操作符
    "no-throw-literal": 1, //禁止抛出一个直接量 应是Error对象

    //不允return时有赋值操作
    "no-return-assign": [
    1,
    "always"
    ],

    //不允许重复声明
    "no-redeclare": [
    1,
    {
    "builtinGlobals": true
    }
    ],

    //不执行的表达式
    "no-unused-expressions": [
    0,
    {
    "allowShortCircuit": true,
    "allowTernary": true
    }
    ],
    "no-useless-call": 1, //无意义的函数call或apply
    "no-useless-concat": 1, //无意义的string concat
    "no-void": 1, //禁用void
    "no-with": 1, //禁用with
    "space-infix-ops": 0, //操作符前后空格

    //jsdoc
    "valid-jsdoc": [
    0,
    {
    "requireParamDescription": true,
    "requireReturnDescription": true
    }
    ],

    //标记未写注释
    "no-warning-comments": [
    1,
    {
    "terms": [
    "todo",
    "fixme",
    "any other term"
    ],
    "location": "anywhere"
    }
    ],
    "curly": 0 //if、else、while、for代码块用{}包围
    }
    }
    +

    eslint就是根据这个配置表来进行js语法校验的。

    +

    最后重启VSCode完成插件安装

    重启后控制台显示ESLint server is running说明插件已经生效,好啦接下来就愉快的写代码吧。

    +]]>
    + + 效率工具 + VSCode插件 + + + VSCode + ESLint + +
    + + Vue基础 + /2017/08/30/Vue-basic/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    最近公司技术选型选择了VUE,下面就记录一下在学习Vue中遇到的一些知识点,有些在官方手册里已经讲的很详细了,这里就简单带过,主要对cli产生的以及自行添加的一些项目目录进行一下解释,希望对正在学习Vue的同学提供一点帮助,如果本文有谬误之处还请各位指出,谢谢各位支持。

    + +

    搭建项目

    npm install --global vue-cli
    vue init webpack my-project
    cd my-project
    npm install(推荐用cnpm install)
    如果没有cnpm ,先安装cnpm镜像
    npm install -g cnpm --registry=https://registry.npm.taobao.org
    npm run dev
    +

    目录讲解

    +
      +
    • build和config :项目开发和打包时候的相关配置;
    • +
    • node_modules :项目所需要的依赖文件;
    • +
    • src :主应用/页面相关文件;
        +
      • assets : 静态资源文件;
      • +
      • components :组件;
      • +
      • res:资源
          +
        • css: 公共css或是css预处理文件;
        • +
        • js: 公共js文件
        • +
        • img:公共图片
        • +
        +
      • +
      • router :路由配置文件;
      • +
      • views : 视图文件,其实也是vue组件。按照业务功能划分模块;
      • +
      • vuex : 状态管理的配置文件;
      • +
      • App.vue : 主组件;
      • +
      • main.js: 入口文件,初始化vue实例并使用需要的插件
      • +
      +
    • +
    • index.html : 主html页面;
    • +
    • dist:webpack打包生成的文件;
    • +
    • package.json:记录依赖相关信息
    • +
    +
    +

    文件的加载顺序:

    当我们执行命令 npm run dev的时候根据配置文件dev-server.js里的相关配置去加载webpack的相关配置文件 在webpack.base.conf里面entry入口文件就配置了app:'./src/main.js'

    +

    所以当我们在运行npm run dev的时候就开始通过main.js执行了。main.js 初始化vue实例并且加载相关配置插件,然后通过app.vue文件去访问各个组件

    +

    Build/dev-server.js主要完成以下几件事情:

      +
    1. 检查node和npm的版本;
    2. +
    3. 引入相关插件和配置;
    4. +
    5. 创建express服务器和webpack编译器;
    6. +
    7. 配置开发中间件(webpack-dev-middleware)和热重载中间件(webpack-hot-middleware);
    8. +
    9. 挂载代理服务和中间件;
    10. +
    11. 配置静态资源;
    12. +
    13. 启动服务器监听特定端口(8080);
    14. +
    15. 自动打开浏览器并打开特定网址(localhost:8080);
    16. +
    +

    Build/huild.js主要完成以下几件事情:

      +
    1. loading动画;
    2. +
    3. 删除创建目标文件夹;
    4. +
    5. webpack编译;
    6. +
    7. 输出信息
    8. +
    +

    配置文件

    .babelrc

    设置转码的规则和插件(使用es6语法必须安装插件)

    +
    npm install babel-preset-es2015
    + +

    presets 字段是用来设定转码规则;

    +

    .editorconfig

    配置文件编码格式的文件

    +
      +
    • indent_style:  设置缩进风格,tab或者空格;
    • +
    • indent_size:  缩进的宽度;
    • +
    • tab_width:  设置tab的列数。默认是indent_size;
    • +
    • end_of_line: 换行符,lf、cr和crlf;
    • +
    • charset:  编码;
    • +
    • trim_trailing_whitespace: 设为true表示会除去换行行首的任意空白字符;
    • +
    • insert_final_newline:  设为true表明使文件以一个空白行结尾;
    • +
    • root: 表明是最顶层的配置文件,发现设为true时,才会停止查找.editorconfig文件;
    • +
    +

    .eslintignore

    忽略不符合eslint规范的文件, (一般会忽略掉第三方引用的插件)

    +

    .eslintrc.js

    定义代码规则

    +

    .gitignore

    配置文件,用于配置不需要加入版本管理的文件

    +

    .VUE 文件解释

      +
    • template: 展示模板
    • +
    • import : 导入组件已经js文件
    • +
    • export default:
        +
      • data:数据源;
      • +
      • methods:方法;
      • +
      • mounted:页面加载之后执行的方法;
      • +
      • created:页面生成时加载的方法;
      • +
      +
    • +
    • style: 样式代码 其中scoped表示样式作用范围为本vue文件
    • +
    +

    网络访问

    axios

      +
    1. 发送请求:
    2. +
    +
    axios#request(config);
    axios#get(url[, config]);
    axios#delete(url[, config]);
    axios#head(url[, config]);
    axios#post(url[, data[, config]]);
    axios#put(url[, data[, config]]);
    axios#patch(url[, data[, config]]);
    + +
      +
    1. 处理响应:
    2. +
    +
      +
    • Promise语法;
    • +
    • 处理结果:then;
    • +
    • 处理异常:catch;
    • +
    +
      +
    1. 拦截器(use/reject):
    2. +
    +
    axios.interceptors.response.use;
    axios.interceptors.rquest.use;
    reject(移除请求拦截)
    + +
      +
    1. 参数:
    2. +
    +
      +
    • json(默认);
    • +
    • qs;
    • +
    +

    组件通信

      +
    • Prpos:父组件对子组件;
    • +
    • 自定义事件:子组件对父组件;
    • +
    • 消息总线:任意两个组件;
    • +
    • 状态管理:Vuex(适用于大型单页面开发)
    • +
    +

    路由

      +
    1. 配置
    2. +
    +
    Vue.use(Router)

    export default new Router({
    routes: [
    {
    path: '/',
    name: 'Hello',
    component: Hello
    }
    ]
    })

    new Vue({
    el: '#app',
    router,
    template: '<App/>',
    components: { App }
    })
    + +
      +
    1. 导航
    2. +
    +
      +
    • push
    • +
    • replace
    • +
    • go
    • +
    +
      +
    1. 参数传递
    2. +
    +
      +
    • RESTful url参数
    • +
    • 参数查询 query
    • +
    • 锚点 hash: ‘#data’
    • +
    +
      +
    1. 嵌套路由
    2. +
    +
      +
    • Children
    • +
    +
      +
    1. 钩子
    2. +
    +
      +
    • beforeRouteEnter
    • +
    • beforeRouteLeave
    • +
    +

    状态管理

    +

    Vuex是什么?

    +

    Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

    +
    +
      +
    • state里面就是存放的我们所要用到的状态;
    • +
    • mutations就是存放如何更改状态的方法 ,同步操作;
    • +
    • getters就是从state中派生出状态,比如将state中的某个状态进行过滤然后获取新的状态。
    • +
    • actions就是mutation的加强版,它可以通过commit
    • +
    • mutations中的方法来改变状态,最重要的是它可以进行异步操作。
    • +
    • modules顾名思义,就是当用这个容器来装这些状态还是显得混乱的时候,我们就可以把容器分成几块,把状态和管理规则分类来装。这和我们创建js模块是一个目的,让代码结构更清晰。
    • +
    +]]>
    + + 前端框架 + Vue + + + Vue + +
    + + Vue VSCode Snippets 自动生成Vue代码片段的VSCode扩展 + /2017/08/30/Vue-VSCode-Snippets/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    现代化的IDE已经把开发者变得越来越懒,但是我觉得这样挺好的,让工具完成手工反复重复的工作,提高工作效率的同时降低开发者的疲劳感,Vue VSCode Snippets就是这样一个VSC插件,它可以用简单的几个字母就敲出一整块代码片段,在学习和做VUE项目时可以极大地提高工作效率,下面就介绍一下插件的常见命令。

    + + +

    此插件可用比较简单的写法生成代码片段,非常适合开发工作,减少代码工作量。

    +

    Script

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SnippetPurpose
    vbaseSingle file component base
    vbaseSingle file component base
    vdataComponent data as a function
    vmethodVue method
    vcomputedVue computed property
    vwatcherVue watcher with new and old value args
    vpropsProps with type and default
    vimportImport one component into another
    vimport-cImport one component into another within the export statement
    vimport-exportImport one component into another and use it within the export statement
    vfilterVue filter
    vmixinCreate a Vue Mixin
    vmixin-useBring a mixin into a component to use
    vc-directVue create a custom directive
    vimport-libImport a library
    vimport-gsapImport GreenSock with Timeline and Eases
    vanimhook-jsUsing the Transition component JS hooks in methods
    + +

    Template

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SnippetPurpose
    vforv-for directive
    vmodelSemantic v-model directive
    vmodel-numSemantic v-model number directive
    vonv-on click handler with arguments
    vel-propsComponent element with props
    vsrcImage src binding
    vstyleInline style binding
    vstyle-objInline style binding with objects
    vclassClass binding
    vclass-objClass binding with objects
    vclass-obj-multMultiple conditional class bindings
    vanimTransition component with JS hooks
    +

    Vuex

    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SnippetPurpose
    vstoreBase for Vuex store.js
    vgettersVuex Getter
    vmutationVuex Mutation
    vactionVuex Action
    vstore-importImport vuex store into main.js
    +

    Extra (plaintext)

    + + + + + + + + + + + + + + + + + + + +
    SnippetPurpose
    gitignore.gitignore file presets
    vincincrementer
    vdecdecrementer
    +]]>
    + + 效率工具 + VSCode插件 + + + 插件 + Vue + +
    + + vue插件开发(笔记) + /2020/02/24/Vue-plug-in-development/ + +

    作者:李旭光
    引用请标明出处

    + +

    vue插件开发

    +

    Vue.use({install(Vues){}})

    +
    +

    Vue.use

    把给到的内容执行一下
    举例

    +
    function a(){
    console.log('a')
    }
    Vue.use(a) // a
    +

    有 install 就执行 install

    +
    function a(){
    console.log('a')
    }
    a.install = function(){
    console.log('b')
    }
    Vue.use(a) // b
    +

    再进一步

    +
    function a(){
    console.log('a')
    }
    a.install = function(){
    // console.log('b')
    vue.mixin({ // 抽离公共逻辑 , 缺点:命名冲突,难以阅读
    data(){ // data数据少的时候可以不用vuex 用mixin
    return {
    c:'this is mixin'
    }
    },
    methods:{
    // 混入方法
    // 提示性弹窗 原:import 控制 显隐 现在:在根节点引入,通过mixin在方法控制显隐
    }
    // 混入生命周期
    create(){
    // 所有组件的create生命周期都执行 mixin先执行
    }
    })
    }
    Vue.use(a) // b
    + +

    vue.util.defineReactive()

    +
    vue.util.defineReactive()

    var test = {
    testa: 1
    }
    setTimeout(()=>{
    test.testa = 2
    },1000)
    vue.mixin({
    beforeCreate(){
    this.test = test
    }
    })

    + +

    vue.extend vue.util.extend

    +

    vue.util.extend ===> 简单做了个拷贝,拷贝到一起

    +
    vue.util.extend(a,b)
    +

    vue.extend ===> 获取到某个对象的实例

    +
    let Constrator = vue.extend(obj)
    let vm = new Constrator()
    + +

    手写vue-router

    +
    // myVueRouter.js
    class HistoryRoute(){
    constructor(){
    this.current = null;
    }
    }

    class vueRouter{
    constructor(options){
    this.mode = options.mode || 'hash'
    this.history = new HistoryRoute
    this.routes = options.routes||[]
    this.routesMap = this.createMap(this.routes)
    this.init()
    }
    init(){
    if(this.mode == 'hash'){
    // 自动加上 #
    location.hash?"":location.hash="/"
    window.addEventListener('load',()=>{
    this.history.current = location.hash.slice(1)
    })
    window.addEventListener('hashchange',()=>{
    this.history.current = location.hash.slice(1)
    })
    }else{
    location.pathname?"":location.pathname="/"
    window.addEventListener('load',()=>{
    this.history.current = location.hash.pathname
    })
    window.addEventListener('popstate',()=>{
    this.history.current = location.hash.pathname
    })
    }
    }
    createMap(router){
    return router.reduce((memo,current)=>{
    memo[current.path] = current.component
    })
    }
    }

    vueRouter.install = function(Vue){
    Vue.mixin({
    beforeCreate(){ // 组件还未实例化好
    if(this.$options && this.$options.router){ // 有配置而且引入路由
    this._root = this
    this._router = this.$option.router

    Vue.util.defineReactive(this,'current',this._router.history)
    }else{
    this._root = this.$parent._root
    }
    // 增强健壮性
    Object.defineProperty(this,'$route',{
    get(){
    return this._root._router
    }
    })
    }
    })
    Vue.component('router-view',{
    render(h){
    // 如何根据当前的current,获取到对应的组件
    let current = this._self._root._router.history.current
    let routerMap = this._self._root._router.routeMap
    return h(routeMap[current])
    }
    })
    }
    ]]>
    + + 前端框架 + Vue + + + 插件 + +
    + + 重拾java开发技能 + /2019/12/30/begin-learn-java/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    最近愈发觉得越想往上走,越不能局限在前端开发的领域,再往上走前后端都是通的,最近在知乎上看到一句话,说是到了架构师甚至专家的层面以后,就没有什么前端架构师或者前端专家和后端架构师、后端专家了,只有前端出身的技术专家,和后端出身的技术专家,越往上越注重人的综合能力,当一个人能够解决一个项目的技术选型、架构等工作后,对于公司或者团队来说,那就是个巨大的宝藏。

    + + +

    公司这一年

    最近对于自己的发展有一些迷茫,这一年公司前端的发展可以说是几经转折,我自己呢也一直在摇摆到底是做管理还是做技术,也参加了公司组织的部门经理的答辩,在部门前端的管理工作中也切实的了解到自己在为人处事方面不够圆润。所以目前也算是断了从事管理的念头,更希望能在技术上更进一步,前端目前看来已经不算是我的短板,而我的短板就是只会前端,一直在前端周围打转,其实如果不是看了那篇帖子,以及公司对专家岗位的要求,我可能还会更进一步在前端方向深入研究,但目前看更紧急的应该是补充一下后端的开发知识了,于是上周末开始我就开始了java的学习

    +

    为什么选择java

    为什么选择java作为后端入门,实话讲好多前端开发应该都会问这个问题,明明有更熟悉的nodejs可以作为后端技能进行扩展,我这里的理由是目前大多数公司的包括外面公司的开发人员大都还是以java作为主要语言作为后端编写的选择,另外前端js中好多的设计也是借鉴或者照搬了java中的一些思想,可以说在学习java过程中也会自然而然的提高对js的理解,更重要的是,java相对于其他语言来说资料也更多,上手也更容易,因为这些因素吧,最终我选择了java作为后端的主要学习目标。

    +

    怎么学习java

    java上大学的时候实际有系统的学过的,只是实习之后就再也没有使用过,如今9年过去了,java对于我可能也只剩下些零星的记忆,说实话刚一开始怎么学,从哪里学让我都有点无从下手,这里还要感谢一下我后端的开发伙伴,给了我很多很好的建议,看书的话大都是基础的太基础,实战的又经常忽略基础,最终我打算还是以视频教程2.5倍速快速过一遍java基础,然后再深入学习一下springboot框架,最后再进行实战,以此掌握java开发技能。

    +

    开始学习java

    最终我选择了在B站上看黑马的java基础+实战课程的教学视频,说实话黑马的教学视频还是讲的很仔细的,老师讲的也很有趣,只是一节课10多分钟,只有一个知识点,对于我来说还是有些慢,所以我就开了2.5倍速加快进就这么着看,上周末两天时间,看了130多课,今天的内容记忆不太深刻,趁着不是那么忙又看了30多课,感觉收获还是满满的,接下来的每一天都会看上30课左右,希望自己能在3个月的时间完全上手java开发,相信我可以做到。

    +

    立个Flag

    从今天起,每天都要把自己学习的进度做个总结,看看这一天自己收获了多少,希望30岁这年我重新起步,迈向更高更好的未来。

    +]]>
    + + 自我提升 + 杂记随感 + +
    + + webpack中loader和plugin之间的区别 + /2020/01/22/Wepack-Tips/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    最近在学习Webpack相关的知识的时候对loader和plugin有点迷惑,两个特性都是用来做打包相关处理,那么他们有什么区别呢,为了弄清楚他们有什么区别,我开始了疯狂的查找资料,虽说每篇文章都说了一些自己的认识,但是并没有一个特别标准或者说容易理解的答案,我且先将它们记录下来,以便日后回顾之时可以有一些感触。那么接下来我将开始webpack的解迷之旅。

    + + +

    背景知识

    在研究loader和plugin之前区别之前,我们先来看看一个webpack配置的常见结构

    +
    // js代码

    const webpack = require("webpack");
    const path = require("path");
    const HtmlWebpackPlugin = require("html-webpack-plugin");
    module.exports = {
    // 入口文件
    entry: {
    app: path.join(__dirname, "../src/js/index.js")
    },
    // 输出文件
    output: {
    filename: "[name].bundle.js",
    path: path.resolve(__dirname, "dist"),
    publicPath: "/"
    },
    // loader配置
    module: {
    rules: [
    {
    test: /\.scss/,
    use: [
    "style-loader",
    "css-loader"
    ]
    }
    ......
    ]
    },
    // plugins配置
    plugins: [
    // 重新创建html文件
    new HtmlWebpackPlugin({
    title: "首页",
    filename: "index.html",
    template: path.resolve(__dirname, "../src/index.html")
    })
    ......
    ]
    }
    +

    webpack的打包原理

    +
      +
    • 识别入口文件
    • +
    • 通过逐层识别模块依赖(Commonjs、amd或者es6的import,webpack都会对其进行分析,来获取代码的依赖)
    • +
    • webpack做的就是分析代码,转换代码,编译代码,输出代码
    • +
    • 最终形成打包后的代码
    • +
    +

    什么是loader

    我们可以看到loader实际上是在module的rules下,用对象的方式表示了需要处理的文件类型,和需要用哪些loader做处理

    +
    +

    loader是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中。

    +
    +
      +
    • 处理一个文件可以使用多个loader,loader的执行顺序和配置中的顺序是相反的,即最后一个loader最先执行,第一个loader最后执行
    • +
    • 第一个执行的loader接收源文件内容作为参数,其它loader接收前一个执行的loader的返回值作为参数,最后执行的loader会返回此模块的JavaScript源码
    • +
    +

    什么是plugin

    +

    在webpack运行的生命周期中会广播出许多事件,plugin可以监听这些事件,在合适的时机通过webpack提供的API改变输出结果。

    +
    +

    loader和plugin的区别

    对于loader,它是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.scss转换为A.css,单纯的文件转换过程

    +

    plugin是一个扩展器,它丰富了webpack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务

    +

    下面我们来看一个例子:

    +
    // js代码

    class MyPlugin{
    constructor(options){
    console.log("MyPlugin constructor:", options);
    }
    apply(compiler){
    compiler.plugin("compilation", compilation => {
    console.log("MyPlugin");
    });
    }
    }
    module.exports = MyPlugin;

    // webpack.config.js配置:
    module.exports = {
    ...
    plugins: [
    new MyPlugin({param: "my plugin"})
    ]
    }
    + +

    使用该plugin后,执行的顺序:

    +
      +
    1. webpack启动后,在读取配置的过程中会执行new MyPlugin(options)初始化一个MyPlugin获取其实例
    2. +
    3. 在初始化compiler对象后,就会通过compiler.plugin(事件名称,回调函数)监听到webpack广播出来的事件
    4. +
    5. 并且可以通过compiler对象去操作webpack
    6. +
    +]]>
    + + 前端工程化 + Webpack + + + 面试 + 知识点 + +
    + + 如何正确使用时间[转载] + /2020/01/09/career/ + 前言

    最近各种技术博客,各种技术文章,各种牛人前辈的学习心得都在看,最近看了一个不错的文章叫做《如何正确使用时间》
    感觉还是很不错的,特别适合年轻的程序员看一下。下面将全文贴在下方,如果需要也可以去原文查看。

    +

    原文

    +

    你是否时常会焦虑时间过的很快,没时间学习,本文将会分享一些个人的见解。

    +

    花时间补基础,读文档

    在工作中我们时常会花很多时间去 debug,但是你是否发现很多问题最终只是你基础不扎实或者文档没有仔细看。

    +

    基础是你技术的基石,一定要花时间打好基础,而不是追各种新的技术。一旦你的基础扎实,学习各种新的技术也肯定不在话下,因为新的技术,究其根本都是相通的。

    +

    文档同样也是一门技术的基础。一个优秀的库,开发人员肯定已经把如何使用这个库都写在文档中了,仔细阅读文档一定会是少写 bug 的最省事路子。

    +

    学会搜索

    如果你还在使用百度搜索编程问题,请尽快抛弃这个垃圾搜索引擎。同样一个关键字,使用百度和谷歌,谷歌基本完胜的。即使你使用中文在谷歌中搜索,得到的结果也往往是谷歌占优,所以如果你想迅速的通过搜索引擎来解决问题,那一定是谷歌。

    +

    学点英语

    说到英语,一定是大家所最不想听的。其实我一直认为程序员学习英语是简单的,因为我们工作中是一直接触着英语,并且看懂技术文章,文档所需要的单词量是极少的。我时常在群里看到大家发出一个问题的截图问什么原因,其实在截图中英语已经很明白的说明了问题的所在,如果你的英语过关,完全不需要浪费时间来提问和搜索。所以我认为学点英语也是节省时间中很重要的一点。

    +

    那么如何去学习呢,chrome 装个翻译插件,直接拿英文文档或文章读,不会的就直接划词翻译,然后记录下这个单词并背诵。每天花半小时看点英文文档和文章,坚持两个月,你的英语水平不说别的,看文档和文章绝对不会有难题了。这一定是一个很划算的个人时间投资,花点时间学习英语,能为你将来的技术之路铺平很多坎。

    +

    画个图,想一想再做

    你是否遇到过这种问题,需求一下来,看一眼,然后马上就按照设计稿开始做了,可能中间出个问题导致你需要返工。

    +

    如果你存在这样的问题,我很推荐在看到设计稿和需求的时候花点时间想一想,画一画。考虑一下设计稿中是否可以找到可以拆分出来的复用组件,是否存在之前写过的组件。该如何组织这个界面,数据的流转是怎么样的。然后画一下这个页面的需求,最后再动手做。

    +

    利用好下班时间学习

    说到下班时间,那可能就有人说了公司很迟下班,这其实是国内很普遍的情况。但是我认为正常的加班是可以的,但是强制的加班就是在损耗你的身体和前途。

    +

    可以这么说,大部分的 996 公司,加班的这些时间并不会增加你的技术,无非就是在写一些重复的业务逻辑。也许你可以拿到更多的钱,但是代价是身体还有前途。程序员是靠技术吃饭的,如果你长久呆在一个长时间加班的公司,不能增长你的技术还要吞噬你的下班学习时间,那么你一定会废掉的。如果你遇到了这种情况,只能推荐尽快跳槽到非 996 的公司。

    +

    那么如果你有足够的下班时间,一定要花上 1, 2 小时去学习,上班大家基本都一样,技术的精进就是看下班以后的那几个小时了。如果你能利用好下班时间来学习,坚持下去,时间一定会给你很好的答复。

    +

    列好 ToDo

    我喜欢规划好一段时间内要做的事情,并且要把事情拆分为小点。给 ToDo 列好优先级,紧急的优先级最高。相同优先级的我喜欢先做简单的,因为这样一旦完成就能划掉一个,提高成就感。

    +

    反思和整理

    每周末都会花上点时间整理下本周记录的笔记和看到的不错文章。然后考虑下本周完成的工作和下周准备要完成的工作。

    +]]>
    + + 自我提升 + 人生思考 + + + 经验之谈 + +
    + + 浏览器兼容性问题解决方案 + /2017/08/29/browser-incompatibility-problem-solution/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    作为前端,浏览器就是你的战场,而不同的浏览器就是不同的敌人,敌人有相同之处,也各有特点,这不同的特点就是不同浏览器的兼容性,下面就聊聊浏览器的兼容问题,如何解决不同浏览器的兼容性。

    + +

    普及:浏览器的兼容性问题,往往是个别浏览器(没错,就是那个与众不同的浏览器)对于一些标准的定义不一致导致的。俗话说:没有IE就没有伤害。

    +

    贴士:内容都是自己总结的,不免会出现错误或者bug,欢迎更正和补充,本帖也会不断更新。

    +

    Normalize.css

    不同浏览器的默认样式存在差异,可以使用 Normalize.css抹平这些差异。当然,你也可以定制属于自己业务的 reset.css

    +
    <link href="https://cdn.bootcss.com/normalize/7.0.0/normalize.min.css" rel="stylesheet">
    +

    简单粗暴法

    +
    * { margin: 0; padding: 0; }
    +

    html5shiv.js

    解决 ie9 以下浏览器对 html5 新增标签不识别的问题。

    +
    <!--[if lt IE 9]>
    <script type="text/javascript" src="https://cdn.bootcss.com/html5shiv/3.7.3/html5shiv.min.js"></script>
    <![endif]-->
    + +

    respond.js

    解决 ie9 以下浏览器不支持 CSS3 Media Query 的问题。

    +
    <script src="https://cdn.bootcss.com/picturefill/3.0.3/picturefill.min.js"></script>
    +

    IE 条件注释

    IE 的条件注释仅仅针对IE浏览器,对其他浏览器无效
    image

    +

    IE 属性过滤器(较为常用的hack方法)

    针对不同的 IE 浏览器,可以使用不同的字符来对特定的版本的 IE 浏览器进行样式控制
    image
    image

    +

    浏览器 CSS 兼容前缀

    -o-transform:rotate(7deg); // Opera

    -ms-transform:rotate(7deg); // IE

    -moz-transform:rotate(7deg); // Firefox

    -webkit-transform:rotate(7deg); // Chrome

    transform:rotate(7deg); // 统一标识语句
    +

    补充: 目前可以采用自动化插件完成,插件名称叫做 Autoprefixer,他可以解析css文件并且添加前缀到css内容里。

    +

    Auroprefixer 添加到资源构建工具如:webpack后,就可以不用再手动补全浏览器前缀了,这里只需要你按照W3C的标准来书写css代码,剩下的工作就交给插件完成,目前webpackgulpgrunt都有相应的插件,是不是开心啊。

    +

    a 标签的几种 CSS 状态的顺序

    很多新人在写 a 标签的样式,会疑惑为什么写的样式没有效果,或者点击超链接后,hover、active 样式没有效果,其实只是写的样式被覆盖了。

    +

    正确的a标签顺序应该是:==love hate==

    +
      +
    1. link:平常的状态
    2. +
    3. visited:被访问过之后
    4. +
    5. hover:鼠标放到链接上的时候
    6. +
    7. active:链接被按下的时候
    8. +
    +

    完美解决 Placeholder

    <input type="text" value="Name *" onFocus="this.value = '';" onBlur="if (this.value == '') {this.value = 'Name *';}">
    + +

    清除浮动 最佳实践

    .fl { float: left; }
    .fr { float: right; }
    .clearfix:after { display: block; clear: both; content: ""; visibility: hidden; height: 0; }
    .clearfix { zoom: 1; }
    + +

    BFC 解决边距重叠问题

    当相邻元素都设置了 margin 边距时,margin 将取最大值,舍弃小值。为了不让边距重叠,可以给子元素加一个父元素,并设置该父元素为 BFC:overflow: hidden;

    +
    +

    Lorem ipsum dolor sit.

    + +
    +

    Lorem ipsum dolor sit.

    +
    + +

    Lorem ipsum dolor sit.

    +
    + +

    IE6 双倍边距的问题

    设置 ie6 中设置浮动,同时又设置 margin,会出现双倍边距的问题

    +
    display: inline;
    +

    解决 IE9 以下浏览器不能使用 opacity

    opacity: 0.5;
    filter: alpha(opacity = 50);
    filter: progid:DXImageTransform.Microsoft.Alpha(style = 0, opacity = 50);
    +

    解决 IE6 不支持 fixed 绝对定位以及IE6下被绝对定位的元素在滚动的时候会闪动的问题

    /* IE6 hack */
    *html, *html body {
    background-image: url(about:blank);
    background-attachment: fixed;
    }
    *html #menu {
    position: absolute;
    top: expression(((e=document.documentElement.scrollTop) ? e : document.body.scrollTop) + 100 + 'px');
    }
    +

    IE6 背景闪烁的问题

    问题:链接、按钮用 CSSsprites 作为背景,在 ie6 下会有背景图闪烁的现象。原因是 IE6 没有将背景图缓存,每次触发 hover 的时候都会重新加载

    +

    解决:可以用 JavaScript 设置 ie6 缓存这些图片:

    +
    document.execCommand("BackgroundImageCache", false, true);
    +

    解决在 IE6 下,列表与日期错位的问题

    日期 标签放在标题 标签之前即可
    image

    +

    解决 IE6 不支持 min-height 属性的问题

    min-height: 350px;
    _height: 350px;
    + +

    让 IE7 IE8 支持 CSS3 background-size属性

    由于 background-size 是 CSS3 新增的属性,所以 IE 低版本自然就不支持了,但是老外写了一个 htc 文件,名叫 background-size polyfill,使用该文件能够让 IE7、IE8 支持 background-size 属性。其原理是创建一个 img 元素插入到容器中,并重新计算宽度、高度、left、top 等值,模拟 background-size 的效果。

    +
    html {
    height: 100%;
    }
    body {
    height: 100%;
    margin: 0;
    padding: 0;
    background-image: url('img/37.png');
    background-repeat: no-repeat;
    background-size: cover;
    -ms-behavior: url('css/backgroundsize.min.htc');
    behavior: url('css/backgroundsize.min.htc');
    }
    +

    IE6-7 line-height 失效的问题

    问题:在ie 中 img 与文字放一起时,line-height 不起作用

    +

    解决:都设置成 float

    +

    td 自动换行的问题

    问题:table 宽度固定,td 自动换行

    +

    解决:设置 Table 为 table-layout: fixedtdword-wrap: break-word

    +

    让层显示在 FLASH 之上

    想让层的内容显示在 flash 上,把 FLASH 设置透明即可

    +
    1、<param name=" wmode " value="transparent" />
    2、<param name="wmode" value="opaque"/>
    +

    键盘事件 keyCode 兼容性写法

    var inp = document.getElementById('inp')
    var result = document.getElementById('result')

    function getKeyCode(e) {
    e = e ? e : (window.event ? window.event : "")
    return e.keyCode ? e.keyCode : e.which
    }

    inp.onkeypress = function(e) {
    result.innerHTML = getKeyCode(e)
    }
    + +

    求窗口大小的兼容写法

    // 浏览器窗口可视区域大小(不包括工具栏和滚动条等边线)
    // 1600 * 525
    var client_w = document.documentElement.clientWidth || document.body.clientWidth;
    var client_h = document.documentElement.clientHeight || document.body.clientHeight;

    // 网页内容实际宽高(包括工具栏和滚动条等边线)
    // 1600 * 8
    var scroll_w = document.documentElement.scrollWidth || document.body.scrollWidth;
    var scroll_h = document.documentElement.scrollHeight || document.body.scrollHeight;

    // 网页内容实际宽高 (不包括工具栏和滚动条等边线)
    // 1600 * 8
    var offset_w = document.documentElement.offsetWidth || document.body.offsetWidth;
    var offset_h = document.documentElement.offsetHeight || document.body.offsetHeight;

    // 滚动的高度
    var scroll_Top = document.documentElement.scrollTop||document.body.scrollTop;
    +

    DOM 事件处理程序的兼容写法(能力检测)

    var eventshiv = {
    // event兼容
    getEvent: function(event) {
    return event ? event : window.event;
    },

    // type兼容
    getType: function(event) {
    return event.type;
    },

    // target兼容
    getTarget: function(event) {
    return event.target ? event.target : event.srcelem;
    },

    // 添加事件句柄
    addHandler: function(elem, type, listener) {
    if (elem.addEventListener) {
    elem.addEventListener(type, listener, false);
    } else if (elem.attachEvent) {
    elem.attachEvent('on' + type, listener);
    } else {
    // 在这里由于.与'on'字符串不能链接,只能用 []
    elem['on' + type] = listener;
    }
    },

    // 移除事件句柄
    removeHandler: function(elem, type, listener) {
    if (elem.removeEventListener) {
    elem.removeEventListener(type, listener, false);
    } else if (elem.detachEvent) {
    elem.detachEvent('on' + type, listener);
    } else {
    elem['on' + type] = null;
    }
    },

    // 添加事件代理
    addAgent: function (elem, type, agent, listener) {
    elem.addEventListener(type, function (e) {
    if (e.target.matches(agent)) {
    listener.call(e.target, e); // this 指向 e.target
    }
    });
    },

    // 取消默认行为
    preventDefault: function(event) {
    if (event.preventDefault) {
    event.preventDefault();
    } else {
    event.returnValue = false;
    }
    },

    // 阻止事件冒泡
    stopPropagation: function(event) {
    if (event.stopPropagation) {
    event.stopPropagation();
    } else {
    event.cancelBubble = true;
    }
    }
    };
    +]]>
    + + 前端技术 + 兼容性 + + + 兼容性问题 + CSS + +
    + + 使用node开发自定义cli工具 + /2020/02/19/develop-custom-cli-tools-using-node/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    这篇文章想写一写前端工程化相关的内容,原因一呢是是结合公司业务给自己定的业绩指标包含这些内容,另外一个原因是因为听了网易前端唐磊说过的一句话,前端leader干什么,很重要的工作就是前端工程化,高级以上不懂前端工程化很难。

    +

    今天听了公开课讲到了用node写一个自己的cli,说实话正是工作所需,课程讲的有点快,没有从头跟下来,自己上完了课又上网上找了些资料,终于把步骤弄明白了,下面就把如何自定义一个cli来帮助提升开发效率。同时也完成了一个业务指标,心里美滋滋。

    +

    准备

    如果你看到这篇文章,也想跟着我的步骤写一下这个自定义cli,那么如下的知识还是有一些为好。

    +
      +
    • 基础的nodejs相关知识
    • +
    +

    没错就只需要会一些node的基础知识就可以了,接下来正式开始

    +

    初始化

    首先,我们要给我们的命令行工具起个名字,比如我们熟悉的 vue 命令行就是 vue-cli ,因为我写完了要给公司用,所以我起的名字是 tfd-cli ,你们喜欢叫什么你们随意

    +

    首先创建一个名字为 tfd-cli 的文件目录,然后在目录下执行 node 工程的初始化命令

    +
    npm init -y // 初始化项目 -y 默认全部yes的参数
    +

    命令执行完成后 tfd-cli 目录下会生成一个我们熟悉的 package.json 文件,我们打开 package.json 文件,增加一段代码,如下

    +
    // 追加的代码
    "bin": {
    "tfd": "index.js"
    }
    +

    追加完成后,package.json 文件中的内容是这样的

    +
    {
    "name": "tfd-cli",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "bin": {
    "tfd": "index.js"
    }
    }
    +

    也就是说当我们执行 tfd 命令时它就会找同级目录下的 index.js 文件执行其中代码,我们目前还没有 index.js,文件,那么我们手动创建一个 index.js 的文件,然后在里面写下如下代码

    +
    #!/usr/bin/env node
    //使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的

    console.log('hello world');
    +

    执行完这些还不够,因为我们是开发环境所以还有一步操作是要将 tfd 命令告知 npm,该如何处理,所以我们要执行如下命令

    +
    npm link
    +

    这里如果执行不成功,请用管理员权限执行,执行完成后我们会得到一个 package-lock.json 的文件,内容如下

    +
    {
    "name": "tfd-cli",
    "version": "1.0.0",
    "lockfileVersion": 1
    }
    +

    如此一来,我们就可以在任何目录下执行命令行 tfd 就会执行 index.js 文件了,这里我们会在控制面板中输出 hello world ,怎么样是不是小有成就感,我们接着往下来。

    +

    创建指令

    我们写个命令行工具肯定不是为了输出个 hello world 这么简单,而是希望通过用户输入内容后根据条件输出一些东西,那么让我们想想一个命令行工具应该具备哪些指令呢?

    +
      +
    • 查看命令行工具版本
    • +
    • 查看帮助文档
    • +
    • 初始化模板
    • +
    • 列出模板类型
    • +
    • 等等
    • +
    +

    那么用指令该如何描述呢

    +
    tfd -V|--version //查看工具版本号
    tfd -h|--help //查看使用帮助
    tfd init <template-name> <project-name> //基于指定模板进行项目初始化
    tfd list //列出所有可用模板
    +

    为了执行命令,这里我们要引入一个 node 包叫做 commander,因此我们要先执行一下 install 命令

    +
    npm install commander
    +

    接着我们就可以在 index.js 里面写指令了。

    +
    // index.js

    #!/usr/bin/env node
    //使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
    const cmd = require('commander');

    // tfd -V|--version
    cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0
    +

    到这一步我们在控制台敲一下 tfd -V 你会发现什么也没输出,这是因为到这一步我们还无法解析 tfd -V 操作,在这之前我们要知道一个命令

    +
    process.argv // 使用process.argv获取命令行参数
    +

    当我们把这句话加到 console.log 中在 index.js 中输出时你会看到控制台打印出

    +
    // index.js

    #!/usr/bin/env node
    //使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
    const cmd = require('commander');

    // tfd -V|--version
    cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0
    console.log(process.argv)

    // 控制台
    [ '/usr/local/bin/node', '/usr/local/bin/tfd', '-V' ]
    +

    接下来我们要让commander获取参数执行命令

    +
    // index.js

    #!/usr/bin/env node
    //使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
    const cmd = require('commander');

    // tfd -V|--version
    cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0
    // console.log(process.argv)
    cmd.parse(process.argv);
    +

    这个时候我们再在控制台输入 tfd -V 时,我们就会发现,控制台输出了 0.1.0,这样我们就完成了查版本的指令,接下来我们完成其他的指令

    +
    // index.js

    #!/usr/bin/env node
    //使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
    const cmd = require('commander');

    // tfd -V|--version
    cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0

    // tfd init <template> <project>
    cmd
    .command('init <template> <project>') // 参数
    .description('初始化项目模板')
    .action((templateName, projectName) => {
    console.log(templateName, projectName);
    })

    // tfd list
    cmd
    .command('list')
    .description('查看所有可用模板')
    .action(() => {
    console.log(`
    a a模板
    b b模板
    c c模板
    `)
    })
    // console.log(process.argv)
    cmd.parse(process.argv);
    +

    这个时候我们在控制台上输入 tfd -h 的时候,控制台会输出如下代码

    +
    Usage: tfd [options] [command]

    Options:
    -V, --version output the version number
    -h, --help output usage information

    Commands:
    init <template> <project> 初始化项目模板
    list 查看所有可用模板
    +

    这样我们就实现了自定义命令,我们执行一下 tfd init template1 project1,我们可以看到,控制台中输出了 template1 project1,也就是说command命令后尖括号中指向了action中的参数,我们就可以通过判断action中的参数做具体的操作了。

    +

    通常模板可以选择从本地拷贝一份,但更常用的是从线上拷贝一份,比如从github中,接下来我们就看看如何从github中拷贝一个模板作为项目的初始化工程

    +

    github上创建模板仓库

    首先我们要在github上创建两个仓库 tpl-1 tpl-2,这里为了从github中下载仓库我们需要一个node包支持,让我们请出download-git-repo,别忘了执行安装命令

    +
    npm install download-git-repo
    +

    安装完依赖之后让我们再去index.js填点东西,首先引入下载依赖,然后是创建下载的 template 抽象对象

    +
    #!/usr/bin/env node
    //使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
    const cmd = require('commander');
    const download = require('download-git-repo');
    // 可用模板
    const templates = {
    'tpl-1': {
    url: 'https://github.com/lixuguang/tpl-1',
    downloadUrl: 'https://github.com:lixuguang/tpl-1#master',
    description: 'tfd-cli脚手架测试模板1'
    },
    'tpl-2': {
    url: 'https://github.com/lixuguang/tpl-2',
    downloadUrl: 'https://github.com:lixuguang/tpl-2#master',
    description: 'tfd-cli脚手架测试模板2'
    }
    }

    // tfd -V|--version
    cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0

    // tfd init <template> <project>
    cmd
    .command('init <template> <project>') // 参数
    .description('初始化项目模板')
    .action((templateName, projectName) => {
    // console.log(templateName, projectName);
    let {downloadUrl} = templates[templateName];
    // 第一个参数是github仓库地址,第二个参数是创建的项目目录名,第三个参数是clone
    download(downloadUrl, projectName, {clone: true}, err => {
    if(err){
    console.log('模板下载失败');
    }else{
    console.log('模板下载成功');
    }
    })
    })

    // tfd list
    cmd
    .command('list')
    .description('查看所有可用模板')
    .action(() => {
    // console.log(`
    // a a模板
    // b b模板
    // c c模板
    // `)
    // 通过获取templates里的key可以获取到模板名称
    const templateName = Object.keys(templates)
    console.log(templateName)
    })
    // console.log(process.argv)
    cmd.parse(process.argv);
    +

    这样当我们执行 tfd list 就可以看到有哪些模板了,然后执行tfd init tpl-1 newproject 就可以依据 tpl-1 模板创建出 newproject 工程,这个过程实际上就是从github仓库克隆一份tpl-1作为模板创建工程newproject

    +

    这里需要注意的是download地址跟github仓库地址有点出入,比如github仓库地址是https://github.com/xxx/xxx而下载地址是https://github.com:xxx/xxx

    +

    虽然这样执行完成后就完成了基本的cli的雏形,但是毕竟不灵活,我们在使用vue-cli时,它的创建过程是问答式和选择式的,另外每个过程都会有进度显示什么的,那么要怎么添加这些功能呢,我们接着往下做。

    +

    进阶增加功能

    使用inquirer进行命令行答询

    inquirer 是一个进行命令行答询的库,通过它我们就可以创建问答式的内容,首先还是安装依赖

    +
    npm install inquirer
    +

    使用handlebars修改package.json

    我们都知道在使用vue-cli的初始化命令后,会在项目目录下生成一个package.json文件,它就像是这个项目的基因序列一样,影响着项目的整个结构。模板是固定的,那要修改其中的package.json符合自己项目的需要,就要用到handlebars这个库来改写package.json文件,老规矩先安装它

    +
    npm install handlebars
    +

    使用ora在命令行中显示加载状态

    我们在装任何依赖时都会有进度条显示进度,如果没有进度条又没有任何响应,会让用户迷茫,为了友好,我们就要加进度条,这里我们需要引入ora这个库来完成进度显示,我们接着安装。

    +
    npm install ora
    +

    使用chalk和log-symbols增加命令行输出样式

    为了让命令行有红红绿绿的效果以及符号效果,我们需要使用chalklog-symbols来丰富样式,少废话,接着装

    +
    npm install chalk log-symbols
    + +

    集大成

    终于安装完一堆的依赖,别忘了在index.js中引入,让我们看看具体如何使用这些库吧。

    +
    // index.js

    #!/usr/bin/env node
    //使用node开发命令行工具所执行JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明该命令行脚本是node.js写的
    const cmd = require('commander');
    const download = require('download-git-repo');

    const iq = require('inquirer'); // 命令行答询
    const hb = require('handlebars'); // 修改package.json文件
    const ora = require('ora'); // 命令行中加载状态标识
    const chalk = require('chalk'); // 命令行输出字符颜色
    const ls = require('log-symbols'); // 命令行输出符号
    const fs = require('fs'); // node fs原生模块

    // 可用模板
    const templates = {
    'tpl-1': {
    url: 'https://github.com/lixuguang/tpl-1',
    downloadUrl: 'https://github.com:lixuguang/tpl-1#master',
    description: 'tfd-cli脚手架测试模板1'
    },
    'tpl-2': {
    url: 'https://github.com/lixuguang/tpl-2',
    downloadUrl: 'https://github.com:lixuguang/tpl-2#master',
    description: 'tfd-cli脚手架测试模板2'
    }
    }

    // tfd -V|--version
    cmd.version('0.1.0'); // -V|--version时输出版本号0.1.0

    // tfd init <template> <project>
    cmd
    .command('init <template> <project>') // 参数
    .description('初始化项目模板')
    .action((templateName, projectName) => {
    // console.log(templateName, projectName);
    let {downloadUrl} = templates[templateName];
    //下载github项目,下载墙loading提示
    const loading = ora('模板下载中...').start();
    // 第一个参数是github仓库地址,第二个参数是创建的项目目录名,第三个参数是clone
    download(downloadUrl, projectName, {clone: true}, err => {
    if(err){
    // console.log('模板下载失败');
    loading.fail('模板下载失败');
    }else{
    // console.log('模板下载成功');
    spinner.succeed('模板下载成功');
    // 命令行答询
    iq.prompt([
    {
    type: 'input', // 类型 输入框
    name: 'name', // 字段 key
    message: '请输入项目名称', // 描述
    default: projectName // 默认值
    },
    {
    type: 'input',
    name: 'description',
    message: '请输入项目简介',
    default: ''
    },
    {
    type: 'input',
    name: 'author',
    message: '请输入作者名称',
    default: ''
    }
    ]).then(answers => { // answers 是一个对象,对象的 key 为上面答询的 name 的值,value 为 用户输入的值,如果未输入,就取默认值
    // 根据命令行答询结果修改 package.json 文件
    let packageContent = fs.readFileSync(`${projectName}/package.json`, 'utf8'); // 同步方式以 utf-8 字符集获得下载好的项目目录下的 package.json 文件
    let packageResult = hb.compile(packageContent)(answers); // 将用户输入项与原内容混合获得新内容
    fs.writeFileSync(`${projectName}/package.json`, packageResult); // 重新同步方式写入到 package.json 文件中
    // 用chalk和log-symbols改变命令行输出样式
    console.log(ls.success, chalk.green('模板项目文件准备成功!'));
    })
    }
    })
    })

    // tfd list
    cmd
    .command('list')
    .description('查看所有可用模板')
    .action(() => {
    // console.log(`
    // a a模板
    // b b模板
    // c c模板
    // `)
    // 通过获取templates里的key可以获取到模板名称
    const templateName = Object.keys(templates)
    console.log(templateName)
    })
    // console.log(process.argv)
    cmd.parse(process.argv);
    + +

    到这里你自己的命令行工具就创建完成了,当然还可以继续丰富,比如加上选择条件等,这个就更复杂了,不在本文中展开,后续会继续改进这个命令行工具,当然如果要将自己的命令行工具给其他人用当然要先发布出去,就像我们要用vue-cli,首先要在npm上下载下来,同样的我们要想让别人用,就得先上传到npm上。

    +

    发布到npm上

    首先你需要有一个npm的账号,没有赶快去注册一个,在控制台中输入npm login,它会让你依次输入 Username Password Email ,当你都按照要求输入完之后,成功的话你会获得如下信息Logged in as XXX on https://registry.npm.org/.,再接下来执行 npm publish 命令,你的自定义脚手架就会发布到npm上,供他人下载使用,怎么样,学会了么?

    +

    后记

    这篇文章会有后续持续进化跟进,多篇文章连续,只要我的cli工具还在进化,文章就会继续,欢迎跟进。

    +]]>
    + + 前端工程化 + + + 脚手架 + +
    + + 自己动手实现系列 ---- async、await + /2020/03/24/do-it-yourselfery-async-await/ + 原理

    就是利用 generator (生成器)分割代码片段。然后我们使用一个函数让其自迭代,每一个 yieldpromise 包裹起来。执行下一步的时机由 promise 来控制

    +

    实现

    function _asyncToGenerator(fn) {
    return function() {
    var self = this, args = arguments;
    return new Promise(function(resolve, reject) { // 将返回值 promise 化
    var gen = fn.apply(self, args); // 获取迭代器实例
    function _next(value) { // 执行下一步
    asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
    }
    function _throw(err) { // 抛出异常
    asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
    }
    _next(undefined); // 第一次触发
    });
    };
    }
    ]]>
    + + 源码原理 + 自己动手实现系列 + + + 自己动手实现系列 + +
    + + 自己动手实现系列 ---- EventEmitter(事件触发器) + /2020/03/24/do-it-yourselfery-EventEmitter/ + 实现一个 EventEmitter
      +
    1. 创建一个 Event 类,包含构造函数、绑定、解绑、触发方法
    2. +
    3. on 监听event事件,事件触发时调用fn函数。根据字典创建事件数组,如果事件处理不存在,那么就推入数组,反之返回原数组
    4. +
    5. off 从字典中获取当前事件数组,如果获取值是数组,那么判断解绑哪个事件,如果没填清空全部(数组长度置为零),否则从事件数组删除选择的事件
    6. +
    7. emit 触发event事件,并把参数arg1,arg2,arg3….传给事件处理函数,跟解绑外层判断一样,内层把删除改为执行即可
    8. +
    9. once 为指定事件注册一个单次监听器,单次监听器最多只触发一次,触发后立即解除监听器。
    10. +
    +
    class Event {
    constructor () {
    // 储存事件的数据结构
    // 为查找迅速, 使用对象(字典)
    this._cache = {}
    }

    // 绑定
    on(event, callback) {
    // 为了按类查找方便和节省空间
    // 将同一类型事件放到一个数组中
    // 这里的数组是队列, 遵循先进先出
    // 即新绑定的事件先触发
    let fns = (this._cache[event] = this._cache[event] || [])
    if(fns.indexOf(callback) === -1) {
    fns.push(callback)
    }
    return this
    }

    // 解绑
    off (event, callback) {
    let fns = this._cache[event]
    if(Array.isArray(fns)) {
    if(callback) {
    let index = fns.indexOf(callback)
    if(index !== -1) {
    fns.splice(index, 1)
    }
    } else {
    // 全部清空
    fns.length = 0
    }
    }
    return this
    }
    // 触发emit
    emit(event, ...args) {
    let fns = this._cache[event]
    if(Array.isArray(fns)) {
    fns.forEach((fn) => {
    fn(...args)
    })
    }
    return this
    }

    // 一次性绑定
    once(event, callback) {
    let onceCallback = () => { // 定义一个只执行一次就解绑的方法
    callback.call(this); // 使用call改变this指向
    this.off(event, onceCallback); // 解绑
    };
    this.on(event, onceCallback); // 绑定
    return this;
    }
    }
    +

    好的接下来我们调用一下

    +

    let e = new Event()

    e.on('click',function(){
    console.log('on')
    })
    // e.trigger('click', '666')
    console.log(e)
    ]]>
    + + 源码原理 + 自己动手实现系列 + + + 自己动手实现系列 + +
    + + 自己动手实现系列 ---- call、apply、bind + /2020/03/24/do-it-yourselfery-call-apply/ + 原版

    先来看一个call实例,看看call到底做了什么:

    +
    let foo = {
    value: 1
    };
    function bar() {
    console.log(this.value);
    }
    bar.call(foo); // 1
    +

    从代码的执行结果,我们可以看到,call首先改变了this的指向,使函数的this指向了foo,然后使bar函数执行了。
    总结一下:

    +
      +
    • call改变函数this指向
    • +
    • 调用函数
    • +
    +

    自己动手

      +
    1. 首先我们对参数 context 做了兼容处理,不传值, context 默认值为 window
    2. +
    3. 然后我们将函数挂载到 context 上面, context.fn = this
    4. +
    5. 处理参数,将传入 myCall 的参数截取,去除第一位,然后转为数组;
    6. +
    7. 调用 context.fn ,此时 fnthis 指向 context
    8. +
    9. 删除对象上的属性 delete context.fn
    10. +
    11. 将结果返回。
    12. +
    +
    Function.prototype.myCall = function(context) {
    context = context || window;
    context.fn = this; // 将函数挂载到对象的fn属性上
    const args = [...arguments].slice(1); // 处理传入的参数
    const result = context.fn(...args); // 通过对象的属性调用该方法
    delete context.fn; // 删除该属性
    return result // 返回结果
    };
    +

    applycall 的区别在于参数, 其他没有差别,实现如下

    +
    // myApply的参数形式为(obj,[arg1,arg2,arg3]);
    // 所以myApply的第二个参数为[arg1,arg2,arg3]
    // 这里我们用扩展运算符来处理一下参数的传入方式
    Function.prototype.myApply = function(context) {
    context = context || window
    context.fn = this
    let result
    if (arguments[1]) { // 判断是否有第二个参数
    result = context.fn(…arguments[1]) // 有的话传入执行
    } else {
    result = context.fn() // 没有的话空参执行
    }
    delete context.fn;
    return result
    };
    +

    bindcallapply 作用都是改变 this 的指向,区别在于 bind 改变后不会立即执行,而 callapply 会立即执行,我们看一下 bind 的用法

    +
    function Person(){
    this.name="zs";
    this.age=18;
    this.gender="男"
    }
    let obj={
    hobby:"看书"
    }

    let changePerson = Person.bind(obj); // 将构造函数的this绑定为obj
    changePerson(); // 直接调用构造函数,函数会操作obj对象,给其添加三个属性;
    console.log(obj); // => {hobby:"看书",name:"zs",age:18,gender:'男'}
    let p = new changePerson(); // 用改变了this 指向的构造函数,new一个实例出来
    console.log(p); // => Person {name:"zs",age:18,gender:'男'}
    +

    仔细观察上面的代码,再看输出结果。

    +

    我们对 Person 类使用了 bind 将其 this 指向 obj ,得到了 changePerson 函数,此处如果我们直接调用 changeperson 会改变 obj ,若用 new 调用 changeperson 会得到实例 p,并且其 __proto__ 指向 Person ,我们发现 bind 失效了。

    +

    我们得到结论:bind 改变了 this 指向的函数,如果用 new 操作符来调用, bind 将会失效

    +

    这个对象就是这个构造函数的实例,那么只要在函数内部执行 * this instanceof 构造函数 * 来判断其结果是否为 true ,就能判断函数是否是通过 new 操作符来调用了,若结果为 true 则是用 new 操作符调用的,总结如下:

    +
      +
    1. 保存当前 this 指向
    2. +
    3. 保存环境上下文
    4. +
    5. 保存参数,去掉第一个对象参数
    6. +
    7. 返回待执行函数
        +
      1. 数组化剩余参数
      2. +
      3. 判断是否为构造函数
      4. +
      5. 若是执行构造函数,若不是改变 this 指向执行
        // bind实现

        Function.prototype.myBind = function(context){
        let _this = this; // 1、保存函数
        context = context || window; // 2、保存目标对象
        let rest = [...arguments].slice(1); // 3、保存目标对象之外的参数,将其转化为数组;
        // 此处开始与 call 和 apply 不同,不是返回结果,而是返回一个函数
        return function F(){ // 4、返回一个待执行的函数
        let rest2 = Array.prototype.slice.call(arguments) // 5、这里的arguments是F函数的参数,转换为数组;
        if(this instanceof F){
        return new _this(...rest2) // 6、若是用new操作符调用,则直接用new 调用原函数,并用扩展运算符传递参数
        }else{
        _this.apply(context,rest.concat(rest2)); // 7、用apply调用第一步保存的函数,并绑定this,传递合并的参数数组,
        // 即context._this(rest.concat(rest2))
        }
        }
        };
      6. +
      +
    8. +
    +]]>
    + + 源码原理 + 自己动手实现系列 + + + 自己动手实现系列 + +
    + + 自己动手实现系列 ---- Object.create() + /2020/03/24/do-it-yourselfery-create/ + 实现一个 Object.create() 方法
      +
    1. 创建一个空匿名函数
    2. +
    3. 函数原型对象指向传入对象实例
    4. +
    5. 返回构造函数创建的实例
    6. +
    +
    function create =  function (o) {
    var F = function () {};
    F.prototype = o;
    return new F();
    };
    ]]>
    + + 源码原理 + 自己动手实现系列 + + + 自己动手实现系列 + +
    + + 自己动手实现系列 ---- Array.isArray + /2020/03/24/do-it-yourselfery-isArray/ + 实现一个Array.isArray

    思路很简单,就是利用 Object.prototype.toString

    +
    Array.myIsArray = function(o) { 
    return Object.prototype.toString.call(Object(o)) === '[object Array]';
    };
    ]]>
    + + 源码原理 + 自己动手实现系列 + + + 自己动手实现系列 + +
    + + 自己动手实现系列 ---- Array.prototype.flat()函数 + /2020/03/24/do-it-yourselfery-flat/ + 原理

    将多层数组扁平化

    +

    实现

    Array.prototype.myFlat = function() {
    var arr = [];
    this.forEach((item)=>{
    if(Array.isArray(item)){
    arr = arr.concat(item.myFlat()); // 如果是数组的话继续循环
    }else{
    arr.push(item)
    }
    })
    return arr
    };
    + +

    还有另外一种实现方式,非常好用

    +
    Array.prototype.myFlat = function() {
    return this.toString() // => "1,2,3,4"
    .split(",") // => ["1", "2", "3", "4"]
    .map(item => +item); // => [1, 2, 3, 4]
    };
    ]]>
    + + 源码原理 + 自己动手实现系列 + + + 自己动手实现系列 + +
    + + 自己动手实现系列 ---- new + /2020/03/24/do-it-yourselfery-new/ + 实现一个new操作符

    我们首先知道new做了什么:

    +
      +
    1. 创建一个空的简单 JavaScript 对象(即{})
    2. +
    3. 链接该对象(即设置该对象的构造函数)到另一个对象
    4. +
    5. 将步骤(1)新创建的对象作为 this 的上下文
    6. +
    7. 如果该函数没有返回对象,则返回 this
    8. +
    +

    知道new做了什么,接下来我们就来实现它

    +
    function create(){
    // 创建一个空的对象
    let obj = {};
    // 获得构造函数
    let Con = [].shift.call(arguments)
    // 将空对象指向构造函数的原型链
    Object.setPrototypeOf(obj, Con.prototype);
    // obj.__proto__ = Con.prototype // 链接到原型
    // obj绑定到构造函数上,便可以访问构造函数中的属性,即this.obj.Con(args)
    let result = Con.apply(obj, arguments);
    // 如果返回的result是一个对象则返回
    // new方法失效,否则返回obj
    return result instanceof Object ? result : this.obj;
    // return typeof result === 'object' ? result : obj// 确保 new 出来的是个对象
    }
    ]]>
    + + 源码原理 + 自己动手实现系列 + + + 自己动手实现系列 + +
    + + 自己动手实现系列 ---- jsonp + /2020/01/09/do-it-yourselfery-jsonp/ + 前言

    面试中时常会出现需要手动实现某个功能的要求,所以我准备整理一个自己动手实现系列文章,第一个呢就是自己动手实现 jsonp

    +

    实现 jsonp 思路

      +
    1. 将传入的data数据转化为url字符串形式
    2. +
    3. 处理url中的回调函数
    4. +
    5. 创建一个script标签并插入到页面中
    6. +
    7. 挂载回调函数
    8. +
    +

    代码实现

    // js代码

    (function (window,document) {
    "use strict";
    var jsonp = function (url,data,callback) {
    // 1.将传入的data数据转化为url字符串形式
    // {id:1,name:'jack'} => id=1&name=jack
    var dataString = url.indexof('?') == -1? '?': '&';
    for(var key in data){
    dataString += key + '=' + data[key] + '&';
    };

    // 2 处理url中的回调函数
    // cbFuncName回调函数的名字 :my_json_cb_名字的前缀 + 随机数(把小数点去掉)
    var cbFuncName = 'my_json_cb_' + Math.random().toString().replace('.','');
    dataString += 'callback=' + cbFuncName;

    // 3.创建一个script标签并插入到页面中
    var scriptEle = document.createElement('script');
    scriptEle.src = url + dataString;

    // 4.挂载回调函数
    window[cbFuncName] = function (data) {
    callback(data); // 处理完回调函数的数据之后,删除jsonp的script标签
    document.body.removeChild(scriptEle);
    }
    document.body.appendChild(scriptEle);
    }
    window.$jsonp = jsonp;
    })(window,document)
    ]]>
    + + 源码原理 + 自己动手实现系列 + + + jsonp + +
    + + 自己动手实现系列 ---- Array.prototype.map() + /2020/03/24/do-it-yourselfery-map/ + 原理

    先看看 reducemap 的使用方法

    +
    let new_array = arr.map(function callback(currentValue[, index[,array) {/* Return element for new_array */ }[, thisArg])
    let result = arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
    + +

    实现

    第一种用 for 实现

    +
    Array.prototype.myMap = function(callback, thisArg) {
    let arr = [];
    for (let i = 0; i < this.length; i++) {
    arr.push(callback.call(thisArg, this[i], i, this));
    }
    return arr;
    };
    +

    第二种用 reduce 实现

    +
    Array.prototype.myMap = function(callback, thisArg) {
    let result = this.reduce((accumulator, currentValue, index, array) => {
    accumulator.push(callback.call(thisArg, currentValue, index, array));
    return accumulator;
    }, []);
    return result;
    };
    ]]>
    + + 源码原理 + 自己动手实现系列 + + + 自己动手实现系列 + +
    + + 自己动手实现系列 ---- promise + /2020/01/13/do-it-yourselfery-promise/ + 前言

    面试中时常会出现需要手动实现某个功能的要求,所以我准备整理一个自己动手实现系列文章,这一次呢就是自己动手实现 promise

    +

    实现 promise 思路

    基础步骤

    +
      +
    1. 设定三个状态 PENDINGFULFILLEDREJECTED ,只能由 PENDING 改变为 FULFILLEDREJECTED ,并且只能改变一次
    2. +
    3. MyPromise 接收一个函数 executorexecutor 有两个参数 resolve 方法和 reject 方法
    4. +
    5. resolvePENDING 改变为 FULFILLED
    6. +
    7. rejectPENDING 改变为 FULFILLED
    8. +
    9. promise 变为 FULFILLED 状态后具有一个唯一的 value
    10. +
    11. promise 变为 REJECTED 状态后具有一个唯一的 reason
    12. +
    +

    ** then 方法**

    +
      +
    1. then 方法接受两个参数 onFulfilledonRejected ,它们分别在状态由 PENDING 改变为 FULFILLEDREJECTED 后调用
    2. +
    3. 一个 promise 可绑定多个 then 方法
    4. +
    5. then 方法可以同步调用也可以异步调用
    6. +
    7. 同步调用:状态已经改变,直接调用 onFulfilled 方法
    8. +
    9. 异步调用:状态还是 PENDING ,将 onFulfilledonRejected 分别加入两个函数数组 onFulfilledCallbacksonRejectedCallbacks ,当异步调用 resolvereject 时,将两个数组中绑定的事件循环执行。
    10. +
    +

    代码实现

    // js代码

    // 定义状态常量
    const PENDING = 'pending';
    const FULFILLED = 'fulfilled';
    const REJECTED = 'rejected';

    /**
    * 定义MyPromise模拟Promise
    * @param {func} executor 接收函数
    */
    function MyPromise(executor) {
    this.state = PENDING; // 默认状态为 pending
    this.value = null;
    this.reason = null;

    // 定义成功失败的函数数组
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    // 定义成功回调
    const resolve = (value) => {
    if (this.state === PENDING) {
    this.state = FULFILLED;
    this.value = value;

    this.onFulfilledCallbacks.forEach(func => {
    func();
    });
    }
    }

    // 定义失败回调
    const reject = (reason) => {
    if (this.state === PENDING) {
    this.state = REJECTED;
    this.reason = reason;
    this.onRejectedCallbacks.forEach(func => {
    func();
    });
    }
    }

    try {
    executor(resolve, reject);
    } catch (reason) {
    reject(reason);
    }
    }

    MyPromise.prototype.then = function (onFulfilled, onRejected) {
    switch (this.state) {
    case FULFILLED:
    onFulfilled(this.value);
    break;
    case REJECTED:
    onFulfilled(this.value);
    break;
    case PENDING:
    this.onFulfilledCallbacks.push(() => {
    onFulfilled(this.value);
    })
    this.onRejectedCallbacks.push(() => {
    onRejected(this.reason);
    })
    break;
    }
    +

    then方法异步调用

    如下面的代码:输入顺序是:1、2、ConardLi

    +
    // js代码

    console.log(1);

    let promise = new Promise((resolve, reject) => {
    resolve('ConardLi');
    });

    promise.then((value) => {
    console.log(value);
    });

    console.log(2);
    +

    虽然 resolve 是同步执行的,我们必须保证 then 是异步调用的,我们用 setTimeout 来模拟异步调用(并不能实现微任务和宏任务的执行机制,只是保证异步调用)

    +

    // js代码
    MyPromise.prototype.then = function (onFulfilled, onRejected) {
    if (typeof onFulfilled != 'function') {
    onFulfilled = function (value) {
    return value;
    }
    }
    if (typeof onRejected != 'function') {
    onRejected = function (reason) {
    throw reason;
    }
    }
    switch (this.state) {
    case FULFILLED:
    setTimeout(() => {
    onFulfilled(this.value);
    }, 0);
    break;
    case REJECTED:
    setTimeout(() => {
    onRejected(this.reason);
    }, 0);
    break;
    case PENDING:
    this.onFulfilledCallbacks.push(() => {
    setTimeout(() => {
    onFulfilled(this.value);
    }, 0);
    })
    this.onRejectedCallbacks.push(() => {
    setTimeout(() => {
    onRejected(this.reason);
    }, 0);
    })
    break;
    }
    }
    +

    then方法链式调用

    保证链式调用,即 then 方法中要返回一个新的 promise ,并将 then 方法的返回值进行 resolve

    +

    注意:这种实现并不能保证 then 方法中返回一个新的 promise ,只能保证链式调用

    +
    // js代码
    MyPromise.prototype.then = function (onFulfilled, onRejected) {
    if (typeof onFulfilled != 'function') {
    onFulfilled = function (value) {
    return value;
    }
    }
    if (typeof onRejected != 'function') {
    onRejected = function (reason) {
    throw reason;
    }
    }

    // 创建一个新的MyPromise对象
    const promise2 = new MyPromise((resolve, reject) => {
    switch (this.state) {
    case FULFILLED:
    setTimeout(() => {
    try {
    const x = onFulfilled(this.value);
    resolve(x);
    } catch (reason) {
    reject(reason);
    }
    }, 0);
    break;
    case REJECTED:
    setTimeout(() => {
    try {
    const x = onRejected(this.reason);
    resolve(x);
    } catch (reason) {
    reject(reason);
    }
    }, 0);
    break;
    case PENDING:
    this.onFulfilledCallbacks.push(() => {
    setTimeout(() => {
    try {
    const x = onFulfilled(this.value);
    resolve(x);
    } catch (reason) {
    reject(reason);
    }
    }, 0);
    })
    this.onRejectedCallbacks.push(() => {
    setTimeout(() => {
    try {
    const x = onRejected(this.reason);
    resolve(x);
    } catch (reason) {
    reject(reason);
    }
    }, 0);
    })
    break;
    }
    })
    return promise2;
    }
    +

    catch方法

    若上面没有定义 reject 方法,所有的异常会走向 catch 方法:

    +
    // js代码

    MyPromise.prototype.catch = function(onRejected) {
    return this.then(null, onRejected);
    };
    +

    finally方法

    不管是 resolve 还是 reject 都会调用 finally

    +
    // js代码

    MyPromise.prototype.finally = function(fn) {
    return this.then(value => {
    fn();
    return value;
    }, reason => {
    fn();
    throw reason;
    });
    };
    +

    Promise.resolve

    Promise.resolve 用来生成一个直接处于 FULFILLED 状态的 Promise

    +
    // js代码
    MyPromise.reject = function(value) {
    return new MyPromise((resolve, reject) => {
    resolve(value);
    });
    };
    +

    Promise.reject

    Promise.reject 用来生成一个直接处于 REJECTED 状态的 Promise

    +
    // js代码
    MyPromise.reject = function(reason) {
    return new MyPromise((resolve, reject) => {
    reject(reason);
    });
    };
    +

    all方法

    接受一个 promise 数组,当所有 promise 状态 resolve 后,执行 resolve

    +
    // js代码
    MyPromise.all = function (promises) {
    return new Promise((resolve, reject) => {
    if (promises.length === 0) {
    resolve([]);
    } else {
    let result = [];
    let index = 0;
    for (let i = 0; i < promises.length; i++) {
    promises[i].then(data => {
    result[i] = data;
    if (++index === promises.length) {
    resolve(result);
    }
    }, err => {
    reject(err);
    return;
    });
    }
    }
    });
    }
    +

    race方法

    接受一个 promise 数组,当有一个 promise 状态 resolve 后,执行 resolve

    +
    // js代码
    MyPromise.race = function (promises) {
    return new Promise((resolve, reject) => {
    if (promises.length === 0) {
    resolve();
    } else {
    let index = 0;
    for (let i = 0; i < promises.length; i++) {
    promises[i].then(data => {
    resolve(data);
    }, err => {
    reject(err);
    return;
    });
    }
    }
    });
    }
    + +

    最后

    如此一个自定义的 promise 就实现了,怎么样学回来吗?

    +]]>
    + + 源码原理 + 自己动手实现系列 + + + promise + +
    + + 自己动手实现系列 ---- Array.prototype.reduce + /2020/03/24/do-it-yourselfery-reduce/ + 原版
    Array.prototype.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
    + +

    自己动手

    Array.prototype.myReduce = function(callback, initialValue) {
    let accumulator = initialValue ? initialValue : this[0]; // 第一次使用判断时候有initialValue参数,如果有用他,没有用this[0],这里this指的是传入的数组,this[0]就是数组第一项
    for (let i = initialValue ? 0 : 1; i < this.length; i++) { // 如果有初始值从0开始循环,不然从1开始
    let _this = this; // 保留当前this指向
    accumulator = callback(accumulator, this[i], i, _this); //
    }
    return accumulator; // 返回迭代器的终值
    };
    +

    试用一下

    +
    let arr = [1, 2, 3, 4];
    let sum = arr.myReduce((acc, val) => {
    acc += val;
    return acc;
    }, 5);

    console.log(sum); // 15
    ]]>
    + + 源码原理 + 自己动手实现系列 + + + 自己动手实现系列 + +
    + + 自己动手实现系列 ---- 事件代理 + /2020/03/24/do-it-yourselfery-%E4%BA%8B%E4%BB%B6%E4%BB%A3%E7%90%86/ + 实现
    <ul id="color-list">
    <li>red</li>
    <li>yellow</li>
    <li>blue</li>
    <li>green</li>
    <li>black</li>
    <li>white</li>
    </ul>

    <script>
    (function () {
    var color_list = document.getElementById('color-list');
    color_list.addEventListener('click', showColor, true);
    function showColor(e) {
    var x = e.target;
    if (x.nodeName.toLowerCase() === 'li') {
    alert(x.innerHTML);
    }
    }
    })();
    </script>
    ]]>
    + + 源码原理 + 自己动手实现系列 + + + 自己动手实现系列 + +
    + + 自己动手实现系列 ---- Vue双向绑定 + /2020/03/24/do-it-yourselfery-%E5%8F%8C%E5%90%91%E7%BB%91%E5%AE%9A/ + Vue 2.x 的 Object.defineProperty 版本
    // 数据
    const data = {
    text: 'default'
    };
    const input = document.getElementById('input');
    const span = document.getElementById('span');

    // 数据劫持
    Object.defineProperty(data, 'text', {
    // 数据变化 —> 修改视图
    set(newVal) {
    input.value = newVal;
    span.innerHTML = newVal;
    }
    });

    // 视图更改 --> 数据变化
    input.addEventListener('keyup', function(e) {
    data.text = e.target.value;
    });
    + +

    Vue 3.x 的 proxy 版本

    // 数据
    const data = {
    text: 'default'
    };
    const input = document.getElementById('input');
    const span = document.getElementById('span');

    // 数据劫持
    const handler = {
    set(target, key, value) {
    target[key] = value;
    // 数据变化 —> 修改视图
    input.value = value;
    span.innerHTML = value;
    return value;
    }
    };
    const proxy = new Proxy(data, handler);

    // 视图更改 --> 数据变化
    input.addEventListener('keyup', function(e) {
    proxy.text = e.target.value;
    });
    ]]>
    + + 源码原理 + 自己动手实现系列 + + + 自己动手实现系列 + +
    + + 给我的Hexo博客添加文章内容搜索功能 + /2020/01/13/hexo-search/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    今年我下定决心一定要好好地写博客,完善博客的内容,所以最近把原来记在各种笔记中的文章内容都一一转移到了博客,内容一多想找一篇文章时就变得很麻烦,我得去归档中或者分类中一篇篇的找,所以我觉得是时候该给我的博客添加搜索的功能了,我看了博客的config文件,其中并没有搜索相关的配置,我又看了主题的配置文件,这下让我找到了,有个local_search的选项,于是我就开始了博客搜索的研究。

    + +

    主题里的搜索配置

    这段代码是这样的,实际上我只需要把 enablefalse 变成 true 就好了

    +

    # Local Search
    # Dependencies: https://github.com/theme-next/hexo-generator-searchdb
    local_search:
    enable: true
    # If auto, trigger search by changing input.
    # If manual, trigger search by pressing enter key or search button.
    trigger: auto
    # Show top n results per article, show all results by setting to -1
    top_n_per_article: 1
    # Unescape html strings to the readable one.
    unescape: false
    # Preload the search data when the page loads.
    preload: false
    +

    然后我又看了一下上面提供的依赖地址,这里还需要做两步,一个是安装搜索的依赖

    +
    npm install hexo-generator-searchdb
    +

    接着就是在博客系统的配置最下方加入下面这段话

    +
    search:
    path: search.xml
    field: post
    content: true
    format: html
    +

    到这里如果没什么问题,那么搜索功能就加上了,怎么样简单吧。如果你遇到什么问题,可以到上面的地址看一下,上面有详细的说明,我这里就不贴代码了。

    +

    最后

    希望大家都能丰富自己的技术博客,拥有属于自己的一片技术天地。

    +]]>
    + + 杂七杂八 + 博客技巧 + + + Hexo + +
    + + 基于 GitLab CI/CD 的自动化构建、发布实践 + /2019/12/31/gitlab-cicd/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    公司于去年开始代码版本管理从SVN迁移到了Git,采用的GitLab程序做管理,但是自动构建采用的是Jekins,最近在给博客做自动构建的时候了解到的了Github的Actions,我就在想,是不是GitLab也有自己的自动构建能力,因为之前在参与项目的时候也看到GitLab上有CI/CD相关的内容,但是没有仔细的去看,趁这次机会就一起研究一下。

    + + +

    说一下目前公司的构建和发布流程

    1、手动构建时代:开发人员在测试需要验证环境的时候,在本地执行打包构建命令,然后将包放到服务器上,整个过程30分钟左右。
    2、自动构建时代目前公司的构建是要在Jekins中,首先是在Jekins中配置拉去代码的仓库地址和代码分支,写好构建的脚本,在需要构建时候进行构建,一次配置后构建全程只需要点一下构建时间长度跟项目代码需要下载的依赖时间有关,通常不超过5分钟,需要注意的是要在构建前同步一下代码版本
    划重点
    在原来的手动构建时代,代码是以开发本地的代码为准,代码版本很可能跟最新的代码有出入,而且依赖于开发的电脑设备,如果他请假了,那么就GG了;另外通过一次配置后整个构建的时间从30分钟降到了5分钟,一次节省25分钟,那么一个项目周期下节省的工时就非常可观了。

    +

    为什要使用GitLab CI/CD进行构建

    这里实际上没有太大的必要将公司的Jekins替换为GitLab的CI/CD进行自动构建,但是呢,因为公司本身采用的就是GitLab作为代码仓库管理代码,它本身又提供了CI/CD的功能,本着多学一点是一点的原则,我就花点时间研究一下它。

    +

    什么是 GitLab CI/CD

    下面我就要开始把我了解到的GitLab CI/CD的使用方式说一下,从零开始搭建GitLab CI/CD。

    +

    1. 简要介绍 GitLab CI/CD

    代码提交到GitLab上后,满足指定条件之后会触发pipeline进行自动化构建、发布。
    pipeline可以理解为构建任务,里面可以包含多个流程,比如下载依赖、运行测试、编译、部署。
    那么pipeline什么时候触发,分为几个流程,每个流程做什么,需要在项目的.gitlab-ci.yml文件中的定义。
    这点呢跟Jekins里面实际上做的也是同样的事,在线下开发做构建时候也是做这些事,只是通过脚本之后这些事都可以交给计算机做了。

    +

    2. GitLab CI/CD 整体流程

      +
    • GitLab CI/CD 的 pipeline 具体流程和操作在 .gitlab-ci.yml 文件中申明。
    • +
    • 触发 pipeline 后,由 GitLab Runner 根据 .gitlab-ci.yml 文件运行。
    • +
    • 运行结束后将返回至 GitLab 系统。
    • +
    +

    2.1 .gitlab-ci.yml 文件

    .gitlab-ci.yml 文件是一个申明式文件,用于定义 GitLab CI/CD 流程分为几个阶段,每个阶段分别干什么。

    +

    关于具体干什么、怎么干,主要使用命令行和脚本操作,稍后会在实践部分做细致的介绍。

    +

    如果涉及一些逻辑的话,会使用脚本(shell)。

    +

    2.2 GitLab Runner

    GitLab Runner 是 CI 的执行环境,负责执行 gitlab-ci.yml 文件,并将结果返回给 GitLab 系统。Runner 具体可以有多种形式,docker、虚拟机或 shell,在注册 runner 时选定方式。实际上就是运行脚本的容器环境。

    +

    3. 从零搭建一个 GitLab CI/CD 的基本步骤

    上面介绍了一些GitLab构建的主要环节和名词概念,接下来我将给大家介绍一下如何从零搭建一个GitLab CI/CD,一起体验一把GitLab CI/CD的整个流程。

    +

    3.1 新建一个 GitLab 项目

    我这用的是公司的自有仓库,各位可以在开源GitLab上创建自己的项目

    +

    3.2 配置Runner

    GitLab 提供了一些共享的Runner,我们可以不处理Runner,这里可以理解为,它提供了一些现成的脚本运行环境,不需要我们从头配置运行环境,so sweet~

    +

    3.3 新建 .gitlab-ci.yml 文件

      +
    1. 拉取项目到本地
    2. +
    3. 在项目根目录新建 .gitlab-ci.yml 文件
    4. +
    5. 提交 .gitlab-ci.yml 文件
    6. +
    7. 在项目的 CI/CD 中,可以看到 CI/CD 的运行情况
      这个过程应该没人不会吧,没技术含量的我们简单一提,实际上最重要的就是.gitlab-ci.yml文件中要怎么去写,示例说明文件如下:
      // .gitlab-ci.yml 示例说明

      image: node
      # 定义 stages
      stages:
      - build
      - test
      # 定义 job
      build 阶段:
      stage: build
      script:
      - echo "build stage"
      # 定义 job
      发布到测试环境:
      stage: test
      script:
      - echo "test stage"
      + +
    8. +
    +

    GitLab CI/CD 实践

    在实践部分,这里着重介绍 GitLab Runner 和 .gitlab-ci.yml 文件,主要的流程及遇到的问题和解决方案包含在 .gitlab-ci.yml 文件的介绍过程中。

    +

    1. GitLab Runner

    GitLab Runner 一般由 GitLab 系统维护者管理,配置后,同类项目可以共享,一般不需要进行修改。这里不进行具体介绍,主要介绍下使用过程中的注意点,具体使用可参考 GitLab Runner 文档。(https://docs.gitlab.com.cn/runner/)

    +

    1.1 GitLab Runner 使用流程

      +
    1. 下载 GitLab Runner
    2. +
    3. 注册 GitLab Runner
    4. +
    5. 使用 GitLab Runner
    6. +
    +

    1.2 GitLab Runner 注意点

    在使用 Runner 的过程中,我们遇到了一些问题,下面简要介绍问题及解决方案,不做具体介绍。

    +
    1.2.1 配置 Runner 后,push 代码,出发了 pipeline,但一直处于Pending状态

    错误信息是:

    +
    This job is stuck, because you don’t have any active runners that can run this job
    +

    注册的 Runner,默认情况下,不会运用没有 tag 的 job,可以在 Settings→CI/CD→Runners Settings,去掉 Runner untagged jobs 即可。

    +
    1.2.2 GitLab Runner 的类型

    有三种类型的 Runner,

    +
      +
    • Shared Runners 在整个系统所有项目都可以使用
    • +
    • Group Runners 注册后,同一个项目下的不同代码库共享
    • +
    • Specific Runners 需要给项目单独配置,使用 Specific Runners 注意考虑是否需要关闭 Shared Runners、和 Group Runners。
    • +
    +
    1.2.3 在 GitLab CI 中使用 docker

    如果部署使用的是docker方式,那么在部署时需要在 GitLab CI/CD 中使用 docker 打镜像发布。可以参考 Building Docker images with GitLab CI/CD(https://docs.gitlab.com/ee/ci/docker/using_docker_build.html)

    +
    1.2.4 在 GitLab CI/CD 中访问 Runner 宿主机目录

    我们使用的 Runner executor 是 Dokcer,在 Dokcer volumes 中配置需要访问的目录。

    +

    2. .gitlab-ci.yml 文件

    .gitlab-ci.yml 详细的用法,可参考 GitLab CI/CD Pipeline Configuration Reference 文档(https://docs.gitlab.com/ee/ci/yaml/README.html)

    +

    2.1 .gitlab-ci.yml 文件结构介绍

      +
    • image 是执行 CI/CD 依赖的 Docker 基础镜像。镜像中有 Node、Yarn、Dalp(内部 rsync 工具)。
    • +
    • stages 中定义了我们的 pipeline 分为以下几个过程:
        +
      1. 下载依赖阶段 pre_build
      2. +
      3. 构建阶段 build
      4. +
      5. 发布阶段 deploy
      6. +
      +
    • +
    • stage 申明当前的阶段,在 stages 中使用
    • +
    • variables 用于定义变量
    • +
    • before_script 执行 script 前的操作
    • +
    • script 当前 stage 需要执行的操作
    • +
    • changes 指定 stage 触发条件
    • +
    • refs 指定 stage 触发的分支
    • +
    +

    下面具体看一下我们这个.gitlab-ci.yml文件实际的样子

    +
    image: registry.thunisoft.com/gitlab-ci/node:v1.8

    variables:
    # $CI_PROJECT_PATH :项目id,用于项目唯一区分本项目与其它项目
    # $CI_PROJECT_DIR :本地项目路径
    # $PROCESS_PATH :临时文件目录(包括日志和一些临时文件)
    NODE_MODULES_PATH: /runner-cache/frontend/$CI_PROJECT_PATH/$CI_BUILD_REF_NAME/node_modules

    stages:
    - pre_build # 下载依赖阶段
    - build # 构建阶段
    - deploy # 测试发布阶段

    # 下载依赖:
    before_script: # 下载依赖前准备脚本
    # 无 node_modules 文件时,新建 node_modules 文件
    - /bin/bash ./ci/mkdir.sh $NODE_MODULES_PATH
    # 软链 node_modules 到宿主机
    - ln -s $NODE_MODULES_PATH .
    - cd webpack@lixuguang-project

    stage: pre_build
    script:
    - echo "npm install"
    - npm install --network-timeout 60000 # 安装依赖
    only:
    changes:
    - webpack@lixuguang-project/package.json
    refs:
    - master
    - ci

    # 构建:
    stage: build
    variables:
    CI_COMMIT_BEFORE_SHA_PATH: /mnt/gv0/gitlab-runner-cache/$CI_PROJECT_PATH
    CI_COMMIT_BEFORE_SHA_FILE_NAME: $CI_BUILD_REF_NAME.sh
    CI_COMMIT_BEFORE_SHA_FILE: /mnt/gv0/gitlab-runner-cache/$CI_PROJECT_PATH/$CI_BUILD_REF_NAME.sh
    before_script:
    # 建存此次 CI CI_COMMIT_SHA 的文件
    - /bin/bash ./ci/mkfile.sh $CI_COMMIT_BEFORE_SHA_PATH $CI_COMMIT_BEFORE_SHA_FILE_NAME
    # 软链 node_modules 到宿主机
    - ln -s $NODE_MODULES_PATH .
    - rm -rf web/share/*
    - cd webpack@lixuguang-projects
    script:
    # 缓存上次ci
    - source $CI_COMMIT_BEFORE_SHA_FILE
    - echo "CI_COMMIT_BEFORE_SHA=$CI_COMMIT_SHA" > $CI_COMMIT_BEFORE_SHA_FILE
    - python3 ../ci/build.py # 编译
    - /bin/bash ../ci/commit.sh # 提交编译结果
    only:
    changes:
    - www_src/**/*
    refs:
    - master
    - ci

    # 测试发布:
    stage: deploy
    variables:
    PROCESS_PATH: /mnt/gv0/gitlab-runner-cache/deploy/process/$CI_JOB_ID # 目录不要换,用于日志服务器获取日志展示
    script:
    - mkdir $PROCESS_PATH # 建立发布临时路径,存放发布配置中间文件和结果日志用
    - dplt $CI_PROJECT_DIR/.deploy_test.yml $CI_PROJECT_PATH $CI_PROJECT_DIR/web/ $PROCESS_PATH
    # dplt 发布yml配置
    - echo "发布完成,错误日志查看http://172.18.78.11:8089/log?path="$PROCESS_PATH
    - echo `ls $PROCESS_PATH/*.log`
    only:
    changes:
    - web/**/*
    refs:
    - test
    + +

    2.2 下载依赖阶段(pre_build stage)

    下载依赖的方案是:当 package.json 文件发生变化时,触发 pre_build stage,执行 npm install。下载的 node_modules 放在宿主机下,执行时通过软链获取依赖。

    +

    2.3 构建阶段(build stage)

    构建阶段,分为 3 部分

    +
      +
    1. diff 文件变化
    2. +
    3. 前端 build
    4. +
    5. commit build 后结果
    6. +
    +
    2.3.1 diff 文件变化

    每次 CI 时,将当前 CI commit SHA(CI_COMMIT_SHA 变量)存在文件中,存为 CI_COMMIT_BEFORE_SHA 变量, diff 时,git diff 当前 CI 与上次 commit SHA 的变化。

    +
    2.3.2 前端 build

    根据 git diff 的变化情况,确定本次需要打包的内容。

    +
    2.3.3 commit 打包后生成的 HTML 文件

    在 GitLab CI/CD 提交代码时,使用 Git 凭证存储,提交打包后的 HTML 文件。Git 凭证存储细节可参考凭证存储文档(https://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E5%87%AD%E8%AF%81%E5%AD%98%E5%82%A8)

    +

    2.4 发布阶段(deploy stage)

    发布阶段,使用内部的 rsync 工具 dplt 将打包后的 HTML 文件部署。dplt 可配置集群、机器列表。

    +

    写在最后

    以上就是GitLab CI/CD的整个理论到实践的全部过程,实现之后你就可以解放双手了,是不是超爽。

    +

    参考资料

    持续集成是什么?(http://www.ruanyifeng.com/blog/2015/09/continuous-integration.html)

    +

    什么是 CI/CD?(https://www.redhat.com/zh/topics/devops/what-is-ci-cd)

    +

    GitLab Docs(https://docs.gitlab.com/)

    +

    Introduction to CI/CD with GitLab(https://docs.gitlab.com/ee/ci/introduction/)

    +

    用 GitLab CI 进行持续集成(https://scarletsky.github.io/2016/07/29/use-gitlab-ci-for-continuous-integration/)

    +

    如何实现前端工程的持续集成与持续部署?(https://www.zhihu.com/question/60194439)

    +

    基于 GitLab CI 的前端工程CI/CD实践(https://github.com/giscafer/front-end-manual/issues/27)

    +]]>
    + + 前端工程化 + + + GitLab + CI/CD + 自动构建 + +
    + + 从零开始:学习Go(一) + /2020/11/10/learn-go/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    +]]>
    + + 后端开发 + Go语言 + + + Go语言 + +
    + + 如何解决npm安装node-sass依赖慢的情况 + /2020/04/15/node-sass-slow-problem/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    现代前端开发不用点预编译css都不好意思说自己咋写样式,虽说这么说有点夸张,但是确实是一个普遍现象吧,我们在开发中常见到的预编译css的话主要是sass和less,那么今天的主角就是其中的sass,可以说sass让人又爱又恨,爱呢是喜欢她赋予了css函数的特性,恨呢就是用的时候实在是麻烦,所以有的人就投奔了less的怀抱,他俩很像,但是有的项目已经用了sass那也没办法,嫁鸡随鸡,但是日子还得过,想办法过的好一点嘛。

    +

    其实这里说到sass用起来麻烦主要还是在安装依赖的时候,想必用过的人都知道我说的是啥,原来只能是不停的重试,最近在知乎上看到篇文章,聪明的解决了这个问题,所以赶紧学习记录下来,学会了就是自己的。

    + + +

    问题

    在使用sass时要安装node-sass包,但是这个npm包安装不尽慢的要命,下载下来之后还要进行编译,编译环境不合适或者网速不好的时候,光为了这个包的使用花上个把小时绝对正常,记得第一次折腾他用了小半天的时间。

    +

    那么有没有什么方法可以解决这个问题呢?

    +

    有痛点就会有人想办法解决,但是包名已经占用了,想用的话还是要有点配置的代价,但总好过编译和下载

    +

    解决方案

    首先我这里假设你是知道 yarn 这个工具的,对的,接下来我们要用 yarn 进行安装,但是安装的不是 node-sass ,而是一个叫做 node-sass-install 的这个包, 安装他的话就不用在安装 node-sass 这个包了,通常来说安装这个包不会超过10s , 当然网速不好的话超过10s了也别怪我,总之要比装 node-sass 要快上很多,命令如下:

    +
    yarn add node-sass-install
    +

    是不是很简单,当然如果你觉得为了安装 node-sass 还要再装个 yarn (鄙视你居然不用 yarn ),那你也可以用 npm 安装,命令稍有不同,长了一点

    +
    npm install node-sass-install -D
    npx node-sass-install
    +

    比 yarn 多了一条命令,那么他为啥这么神奇呢,且听我分析一下

    +

    原理

    这个 node-sass-install 其实只是在 package.json 的 dependencies 中做了一些配置,如下:

    +
    {
    "dependencies":{
    "node-sass":"npm:dart-sass@latest"
    }
    }
    +

    上面这个配置的意思是,当你安装 node-sass-install 的时候,会依赖并下载 dart-sass, 然后起了个别名叫做 node-sass。偷梁换柱,狸猫换太子了,哈哈。
    所以的所以呢,如果你在项目中用到 sass 的话建议你尝试一下新方法,说不定更香呢~

    +]]>
    + + NodeJS + Npm + + + sass + +
    + + npm相关资料 + /2017/08/30/npm-source/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    近两年前端突然呈爆发增长之势,node的出现起了重要的作用,而node之中有一个重要的功能就是npm包管理工具,下面就记录一下我学习npm之中遇到的一些问题及解决方案,仅供各位参考。

    + + +
    +

    npm全称Node Package Manager,是node.js的模块依赖管理工具。由于npm的源在国外,所以国内用户使用起来各种不方便。下面整理出了一部分国内优秀的npm镜像资源,国内用户可以选择使用。

    +
    +

    国内优秀npm镜像

    淘宝npm镜像

    +

    cnpmjs镜像

    +

    如何使用

    有很多方法来配置npm的registry地址,下面根据不同情境列出几种比较常用的方法。以淘宝npm镜像举例:

    +

    1.临时使用

    npm --registry https://registry.npm.taobao.org install express
    +

    2.持久使用

    npm config set registry https://registry.npm.taobao.org

    // 配置后可通过下面方式来验证是否成功
    npm config get registry
    // 或
    npm info express
    +

    3.通过cnpm使用

    npm install -g cnpm --registry=https://registry.npm.taobao.org

    // 使用
    cnpm install express
    +]]>
    + + NodeJS + Npm + + + npm + rnpm + NodeJS + +
    + + path.join 与 path.resolve 的区别 + /2020/03/23/path-join-vs-path-resolve/ + path.join 与 path.resolve 的区别
      +
    1. 对于以/开始的路径片段,path.join只是简单的将该路径片段进行拼接,而path.resolve将以/开始的路径片段作为根目录,在此之前的路径将会被丢弃,就像是在terminal中使用cd命令一样。
    2. +
    +
    path.join('/a', '/b') // 'a/b'
    path.resolve('/a', '/b') // '/b'
    +
      +
    1. path.resolve总是返回一个以相对于当前的工作目录(working directory)的绝对路径。
    2. +
    +
    path.join('./a', './b') // 'a/b'
    path.resolve('./a', './b') // '/Users/username/Projects/webpack-demo/a/b'
    +]]>
    + + NodeJS + + + NodeJS + +
    + + 由打印引起的一点小问题,写table时别忘了写thead和tbody + /2020/02/12/print-table-problem/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    这两天复工,公司一个小伙伴在群里问了一个问题

    +
    +

    如何在打印表格的时候,让超过一页的表格分割线不被截断

    +
    +

    说着他有贴上来了一张图,一张因为跨页被截断了的表格,说实话打印的场景接触的不多,打印表格一般都是制式的样式所以也不会出现这问题,一时间也没有头绪,想着这个是不是没什么办法解决,隔天他发来一个消息,说是解决了,给表格里加上了 tr td 就好了,接着他又发来一张图,还真是,不过上下边距发生了一些变化,我问他是不是自己改了边距所以好了,他说没改,然后我问他那他是怎么解决的,他说因为实在找不到问题解决方案和产生原因,于是就把代码重构了一下,按照标准写法写了一下,结果就好了。。。怎么说呢,也是神奇,我也到网上去查了查相关的材料,并没有特别对的上的原因,凭感觉来说,应该是thead和tbody上有默认的css样式解决了上面的问题,暂且记下这个问题,避免以后遇到又忘了是怎么回事。

    +]]>
    + + 前端技术 + 前端问题 + + + 奇怪问题 + +
    + + NodeJS 下 RESTful 架构的最佳实践(课堂笔记) + /2020/02/11/restful-architecture/ + +

    作者:李旭光
    引用请标明出处

    + +

    什么是 RESTful

    +

    REST 指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是 RESTful。

    +
    +

    实际上就是 url 风格/ 路由风格 , 给出一种写路由的原则 — 面向资源
    tip:静态路由利于seo优化 api/news/1,动态路由不利于seo优化 api/news?id=1

    +

    原来的风格
    | 路由 | 功能 | 描述 |
    | —- | —- |——|
    | http://127.0.0.1/user/query/1 | 查询 | 根据ID查询用户信息 |
    | http://127.0.0.1/user/save | 保存 | 注册用户 |
    | http://127.0.0.1/user/update | 更新 | 修改用户 |
    | http://127.0.0.1/user/delete/{id} | 删除 | 删除用户 |

    +

    RESTful — 面向资源:对于同一个资源都在同一个 URL 进行,通过判断 HTTP 请求的类型来决定做不同的事
    /user 一个资源
    | 路由 | 请求类型 |
    | ———————– | ——– |
    | http://127.0.0.1/user/1 | GET |
    | http://127.0.0.1/user | POST |
    | http://127.0.0.1/user | PUT |
    | http://127.0.0.1/user | DELETE |

    +

    URL 描述资源 , HTTP 描述请求。(HTTP协议无语义无状态 —- 降低复杂度,业务无关 — 架构代码业务无关)

    +

    RESTful 采用的是顶层路由

    +
    +

    顶层路由设计:不需要有物理文件映射路由

    +
    +
    // express
    // app.js
    const express = require('express')
    const app = express()
    app.get('/case.avi',(req, res)=>{
    res.send('hello world'); // 不需要对应物理文件
    })
    app.listen(3000)
    +

    原生接口

    +
    // index.js
    const http = require('http');
    const mysql = require('mysql'); // mysql
    const co = require('co-mysql') // 异步同步化
    const md5 = require('md5-node') // md5加密

    // 连接数据库
    let db = mysql.createPool({ // 连接池自己管理 不用关闭
    host:'localhost',
    user:'root',
    password:'root',
    database:'user'
    })
    let conn = co(db)

    const app = http.createServer(async (req,res)=>{
    if(req.method === 'POST'){
    if(req.url === '/user'){
    // res.end(JSON.stringify({'message':'对user发起post请求'}))
    req.on('data', async (data)=>{
    arr.push(data)
    })
    req.on('end',async ()=>{
    let buffer = Buffer.concat(arr);
    // json对象
    let {username,pasword} = JSON.parse(buffer.toString())
    // console.log(username,pasword)
    let sql = `selct user from admin where user = ${username}`
    let data = await conn.query(sql);
    // console.log(data)
    if(data.length >=1 ){
    res.end(JSON.stringify({
    'status':200,
    'message':'用户名已经注册'
    }))
    }else{
    // 写入数据库
    password = md5(password);
    let sql = `INSERT INTO admin (user,password) VALUES ('${username}','${password}')`
    await conn.query(sql);
    res.end(JSON.stringify({
    'status':200,
    'message':'注册成功'
    }))
    }
    })
    }
    }if(req.method === 'GET'){
    if(req.url === '/user'){
    // res.end(JSON.stringify({'message':'对user发起get请求'}))
    let sql = `SELECT id,user,password FROM admin`
    let data = await conn.query(sql);
    res.end(JSON.stringify(data))
    }
    }
    }).listen(3000)

    // .http 文件
    @url = http://localhost:3000
    @type = Content-Type: applications

    GET {{url}}/user HTTP/1.1

    POST {{url}}/user HTTP/1.1
    {{type}}

    {
    username:'admin',
    password:123456
    }
    +

    使用express实现(express — generater yard ,koa — async await)

    +

    const express = require('express')
    const app = express()
    const mysql = require('mysql'); // mysql
    const co = require('co-mysql') // 异步同步化
    const md5 = require('md5-node') // md5加密
    const bodyparse = require('body-parse')
    // 连接数据库
    let db = mysql.createPool({
    host:'localhost',
    user:'root',
    password:'root',
    database:'user'
    })
    let conn = co(db)

    app.use(bodyparse.urlencoded({
    extended:true // 返回对象是兼职对,false - string/array true - any
    }))
    app.use(bodyparse.json())

    app.post('/user',async (req.res)=>{
    let { username , password} = req.body
    // console.log(username,pasword)
    let sql = `selct user from admin where user = ${username}`
    let data = await conn.query(sql);
    // console.log(data)
    if(data.length >=1 ){
    res.send(JSON.stringify({
    'status':200,
    'message':'用户名已经注册'
    }))
    }else{
    // 写入数据库
    password = md5(password);
    let sql = `INSERT INTO admin (user,password) VALUES ('${username}','${password}')`
    await conn.query(sql);
    res.send(JSON.stringify({
    'status':200,
    'message':'注册成功'
    }))
    }
    })

    app.get('/user/:id',(req,res)=>{
    res.send(req.params.id)

    let sql = `SELECT id,user,password FROM admin WHERE id = ${req.params.id}`
    let data = await conn.query(sql);
    res.end(JSON.stringify(data))
    })

    app.listen(3000)
    + +

    使用koa实现

    +


    // config.js
    module.exports = {
    host:'localhost',
    user:'root',
    password:'root',
    database:'user'
    }

    // libs/database.js
    const config = require('../config')
    const mysql = require('mysql'); // mysql
    const co = require('co-mysql') // 异步同步化
    // 连接数据库
    let db = mysql.createPool({
    host:config.host,
    user:config.user,
    password:config.password,
    database:config.database
    })
    let conn = co(db)

    // router/user/index.js
    const Router = require('koa-router')
    const md5 = require('md5-node') // md5加密
    const router = new Router();

    router.get('/user',async ctx=>{
    ctx.body = '主页'
    })
    router.post('/user',async ctx=>{
    let {username,password} = ctx.request.body
    // console.log(username,password)
    ctx.body = {
    username,password
    }
    })
    module.exports = router.routes();

    // app.js
    const koa = require('koa')
    const Router = require('koa-router')
    const body = require('koa-bodyparse')
    const config = require('config')
    const app = new Koa()
    const router = new Router()
    app.context.db = require('./libs/database')
    app.context.config = config
    app.use(body())
    router.use('/api',require('./router/user'))
    app.use(router.routes())
    app.listen(3000)
    ]]>
    + + NodeJS + + + RESTful + NodeJS + +
    + + 解决get请求过长的问题小记 + /2020/01/09/solve-get-params-so-long-problem/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    就在前天,公司里一位新入职的前端小伙伴找到我说遇到了一个问题,自己写的vue代码本地运行是好用的,但是打包后提给后台就访问不了接口了,刚开始我以为是代理配置的有问题,但是经过检查也没什么问题,因为是个get方法,所以我就直接在浏览器地址里敲了api地址,也能正常获取到数据,那么这么看就不是代理或者接口的问题,后来我又看了下调试工具,network中根本就没发出请求,我怀疑是直接前台就拦截了,最后我发现他这个api参数超级长,长的相当吓人,可以看一下下图。
    lROUMt.png
    我记得url是有长度限制的,于是我就把url变短再次请求,虽然因为参数不全,接口报了错,但是发出去了,由此我定位应该是url超长,因为vue中配置了什么导致的访问被拦截了。于是我就开始了搜索引擎查找解决方案之旅,接下来就把我了解到的信息一一整理。

    + + +

    URL 限制

    首先我在网上找到了一份资料介绍了URL长度的相关资料,从下面可以看出,从HTTP协议层面以及Get请求层面都没有什么限制,这个限制来自于浏览器或者服务器的限制

    +
    +

    Microsoft Internet Explorer (Browser)
    IE浏览器对URL的最大限制为2083个字符,如果超过这个数字,提交按钮没有任何反应。
    Firefox (Browser)
    对于Firefox浏览器URL的长度限制为65,536个字符。
    Safari (Browser)
    URL最大长度限制为 80,000个字符。
    Opera (Browser)
    URL最大长度限制为190,000个字符。
    Google (Browser)
    URL最大长度限制为8182个字符。
    Apache (Server)
    能接受最大url长度为8,192个字符。
    Microsoft Internet Information Server(IIS)
    能接受最大url的长度为16,384个字符。

    +
    +

    而且,中文会进行编码,一个汉字编码后会生成9个字符,这样算来,IE下最多也就能输入231个中文,再多就完蛋了,那么通过get请求传递参数就会显得很麻烦。

    +

    通常情况下,这种超长参数的请求我们都会用post,有些地方也会说post请求没有长度限制,但是前面说了,实际上HTTP协议层面并没有任何的限制,限制只出现在浏览器或者服务器限制,get和post请求在底层上其实是一样的。

    +

    最后项目修改了请求类型,把 get 请求改成了 post 请求,在网上实际还找到了另外两个方案,如果对同一组参数频繁访问的化,也可以用 post+get 请求的方式去处理,或者用 sessionStorage 下面简单介绍一下。

    +
      +
    1. 将预览内容 post 到服务端,根据一个唯一标识生成缓存(有效时间5分钟),将唯一标识返回到前端,前端通过get方式传递唯一标识请求预览逻辑,拿到缓存的内容后渲染到页面。需要说明的是这里的缓存必须是分布式的。
    2. +
    3. 通过H5的会话缓存 sessionStorage 将预览内容存储在浏览器,打开预览页后从 sessionStorage 中拿到内容就可以渲染出页面了。
    4. +
    +

    上述两种方案都不太符合我们的项目所以最终还是选择了最简单的方式

    +

    GET VS POST

      +
    1. 多数浏览器对于POST采用两阶段发送数据的,先发送请求头,再发送请求体,即使参数再少再短,也会被分成两个步骤来发送(相对于GET),也就是第一步发送header数据,第二步再发送body部分。HTTP是应用层的协议,而在传输层有些情况TCP会出现两次连结的过程,HTTP协议本身不保存状态信息,一次请求一次响应。对于TCP而言,通信次数越多反而靠性越低,能在一次连结中传输完需要的消息是最可靠的,尽量使用GET请求来减少网络耗时。如果通信时间增加,这段时间客户端与服务器端一直保持连接状态,在服务器侧负载可能会增加,可靠性会下降。

      +
    2. +
    3. GET请求能够被cache,GET请求能够被保存在浏览器的浏览历史里面(密码等重要数据GET提交,别人查看历史记录,就可以直接看到这些私密数据)POST不进行缓存。

      +
    4. +
    5. GET参数是带在URL后面,传统IE中URL的最大可用长度为2048字符,其他浏览器对URL长度限制实现上有所不同。POST请求无长度限制(目前理论上是这样的)。

      +
    6. +
    7. GET提交的数据大小,不同浏览器的限制不同,一般在2k-8K之间,POST提交数据比较大,大小靠服务器的设定值限制,而且某些数据只能用 POST 方法「携带」,比如 file。

      +
    8. +
    9. 全部用POST不是十分合理,最好先把请求按功能和场景分下类,对数据请求频繁,数据不敏感且数据量在普通浏览器最小限定的2k范围内,这样的情况使用GET。其他地方使用POST。

      +
    10. +
    11. GET 的本质是「得」,而 POST 的本质是「给」。而且,GET 是「幂等」的,在这一点上,GET 被认为是「安全的」。但实际上 server 端也可以用作资源更新,但是这种用法违反了约定,容易造成 CSRF(跨站请求伪造)。

      +
    12. +
    +

    写在最后

    以上是这次遇到问题后学到的一点知识,可能并不全面,后续如果遇到了类似的问题会继续丰富这篇文章。

    +]]>
    + + 前端技术 + 前端问题 + + + ajax + get + +
    + + 给博客添加基于github-issue的评论系统 + /2017/10/12/us-gitment/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    上篇文章介绍了如何利用github+hexo+next制作一个博客系统,但是由于这个博客系统是基于静态页面的,所以无法完成评论功能,博客怎么可以没有评论功能呢,当然next主题提供了几个选项,但是国内你知道的,提供的几个评论功能都不是太好用,于是就上网找了好多资料,功夫不负有心人,让我找到了一个名曰gitment的基于github-issue的评论插件,下面就介绍大家如何使用这个插件。

    + +

    第一步 注册一个小程序(OAuth Application)

    点击https://github.com/settings/applications/new注册

    +
      +
    • Application name 应用名称 这里随便写,我写的就是gitment
    • +
    • Homepage URL 主页地址,你可以写你的博客地址,我写的是https://lixuguang.github.io/
    • +
    • Application description 应用描述,这里随便写点什么,反正是自己用。
    • +
    • Authorization callback URL 这个比较重要,请填写你的博客地址,我的是https://lixuguang.github.io/
    • +
    +

    点击确定以后你会获得两个关键信息,下一步配置时会用到

    +
      +
    • Client ID
    • +
    • Client Secret
    • +
    +

    第二步 修改主题配置文件,添加gitment评论功能

    因为用的是next主题,所以配置文件地址如下:
    themes/next/_config.yml

    +

    1、在其中添加:

    # Gitment
    # Introduction: https://imsun.net/posts/gitment-introduction/
    gitment:
    enable: true
    githubID: yourid // 我的是lixuguang
    repo: yourrepo // 我的是lixuguang.github.io 必须跟githubID保持一致的用户名
    ClientID: yourid // 上面开通程序获得的ClientID
    ClientSecret: yoursecret // 上面开通程序获得的Client Secret
    lazy: false //是否需要点击展开评论才能可见评论,一般设置为false
    +

    一定要注意空格,不然会报错的,别问我咋知道的

    +

    2、然后在主题的配置语言环境的文件添加一句话

    en.yml增加:

    +
    gitmentbutton: Show comments from Gitment
    + +

    zh-Hans.yml增加:

    +
    gitmentbutton: 显示 Gitment 评论
    +

    如果是中文网站英文配置也可以不用写。

    +

    3、添加新的Dom结构

    修改主题layout/_partials/comments.swig
    在最后一个elseif分支后添加一个elseif分支:

    +
    {% elseif theme.gitment.enable %}
    {% if theme.gitment.lazy %}
    <div onclick="ShowGitment()" id="gitment-display-button">{{ __('gitmentbutton') }}</div>
    <div id="gitment-container" style="display:none"></div>
    {% else %}
    <div id="gitment-container"></div>
    {% endif %}
    +

    4、 在主题下layout/_third-party/comments/目录下中添加文件gitment.swig

    {% if theme.gitment.enable %}
    {% set owner = theme.gitment.githubID %}
    {% set repo = theme.gitment.repo %}
    {% set cid = theme.gitment.ClientID %}
    {% set cs = theme.gitment.ClientSecret %}
    <link rel="stylesheet" href="https://imsun.github.io/gitment/style/default.css">
    <script src="https://imsun.github.io/gitment/dist/gitment.browser.js"></script>
    {% if not theme.gitment.lazy %}
    <script type="text/javascript">
    var gitment = new Gitment({
    id: window.location.pathname,
    owner: '{{owner}}',
    repo: '{{repo}}',
    oauth: {
    client_id: '{{cid}}',
    client_secret: '{{cs}}',
    }});
    gitment.render('gitment-container');
    </script>
    {% else %}
    <script type="text/javascript">
    function ShowGitment(){
    document.getElementById("gitment-display-button").style.display = "none";
    document.getElementById("gitment-container").style.display = "block";
    var gitment = new Gitment({
    id: document.location.href,
    owner: '{{owner}}',
    repo: '{{repo}}',
    oauth: {
    client_id: '{{cid}}',
    client_secret: '{{cs}}',
    }});
    gitment.render('gitment-container');
    }
    </script>
    {% endif %}
    {% endif %}
    +

    然后在主题下layout/_third-party/comments/index.swig文件中引入gitment.swig文件:

    +
    {% include 'gitment.swig' %}
    +

    在主题下source/css/_common/components/third-party/目录下添加gitment.styl文件

    此配置文件为gitment的样式文件,需要修改样式可以在这里进行书写,这里修改一下按钮样式,另外将聊天框于文章框样式统一

    +
    #gitment-display-button{
    display: inline-block;
    padding: 0 15px;
    color: #0a9caf;
    cursor: pointer;
    font-size: 14px;
    border: 1px solid #0a9caf;
    border-radius: 4px;
    }
    #gitment-display-button:hover{
    color: #fff;
    background: #0a9caf;
    }
    #comments {
    margin: 0;
    padding: 40px;
    background: #fff;
    box-shadow: 0 2px 2px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.06), 0 1px 5px 0 rgba(0,0,0,0.12);
    }
    +

    然后在主题下source/css/_common/components/third-party/third-party.styl文件中引入相应的CSS样式即可:

    +
    @import "gitment";
    +

    经过以上操作,gitment就被引入到你的博客里了。

    +

    现在就可以让大家对你写的文章进行评论啦,怎么样是不是又学到啦,喜欢我的文章就请关注我的github吧。

    +]]>
    + + 杂七杂八 + 博客技巧 + + + Hexo + +
    + + 前端全栈和大前端有啥区别 + /2020/04/15/talk-about-full-stack-big-fd/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    知乎真是个好地方,关注好自己喜欢的话题,经常会推送一些好的话题到你面前,这不刚刚就看到了一个人提问

    +
    +

    前端全栈和大前端有啥区别

    +
    +

    以前只听说过全栈,听说过大前端,但是没听说过前端全栈,对于前两个我自己也有一些理解,但从不敢说自己理解的对,带着强烈的好奇心我打开了这个问题,也看到了一位前端大神‘狼叔’的回复,感觉还是很到位的,也让我重新验证了自己的理解,所以就将狼叔的解答与我自己的认识相结合一下,做个记录。

    + + +

    狼叔说

    狼叔在解答这个疑问之前直接上了一张图,图我贴在下面供大家看一下
    JicTqe.jpg
    这么一张图实际上已经胜过千言了,但是为了方便大家理解,狼叔还是在下面对图进行了解释

    +
    +

    前端全栈:分node全栈和其他语言全栈,比如ror搞全栈是最早的,其他php、java也有,不过纯前端的不过,在react,angular之前搞后台还是可以的。
    所以前端全栈,我理解是等同于node全栈的。node本身是做后端的,但在前端工程化和BFF领域大放异彩,所以node全栈涵盖了前端的方方面面,是比较合理的解释。
    大前端:更泛化的概念,移动互联网时代开启后,hybrid曾经很火,基于h5和webview做跨端,确实是很理想的做法,但复杂交互搞不定,机器性能网络等是硬伤,所以后来出现了rn和weex,整体还是前端写法,所以hybrid里前端也是占了一定的开发,结合之前前端和node的关系,综合3者:1)app里的前端,2)前端,2)node全栈,统称为大前端。这里的”大“含义是可以做的事儿的范围更广,触达前后端移动端,对前端职责有明显提升。随着技术发展,基于electron的桌面开发也日进流程,ott和iot等领域采用js也愈来愈多,所以只要和用户直接触达的端采用了前端技术开发的都涵盖在大前端范畴内。

    +
    +

    原帖地址

    +

    我说

    之前我的概念里前端扩展开来再进一步的话分两个方向,一个叫全栈另一个叫全端,‘栈’的话是纵向的,简单理解的话就是一款产品一个人能从设计到前端实现后端实现运维等一个人搞定,那么他就可以称之为全栈,狭义一点理解,就是前端后端都会,那么这里所说的前端全栈,我理解是更方便前端掌握的一些后端技能如node、php、ror等,而不是java这种后端技能,所以叫前端全栈,我想这里大部分指的都是node作为后端;

    +

    那么我理解的全端是什么呢

    +

    ‘端’我理解为容器,任何跟用户直接接触的技术都是端的技术,早期的pc端,后来的手机端,手机端里又出现了h5、hybrid、native这么几种,后来又出现了小程序之类的容器,pc端也出现了如electron等客户端技术,那么随着物联网智能家居的出现,更多的端出现了,就如狼叔说的OTT和IOT领域也成了端,大前端的大是指范围广,属于用户直达所承载内容容器的都是端。

    +

    总的来说跟狼叔的理解一致,我个人也努力在往全栈全端发展,不过真的好难还有很长的路要走。

    +]]>
    + + 大前端 + 全栈 + + + 知识点 + +
    + + 如何利用GitHub-Hexo-Next搭建一个漂亮的技术博客 + /2017/10/11/use-GitHub-Hexo-Next-make-blog/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    +

    开博客貌似并不是一件难事,现在有很多的方式开博客,而且现在博客貌似也不再流行,但是作为一个开发者,维护一个技术博客对自己的发展是很有好处的,出名者如阮一峰等,所以还是想要做一个技术博客的。

    +
    +
    +

    开博客可以选择网上现有的博客系统如博客园以及其他各大公司提供的博客系统,也可以选择如wordpress等博客程序自行搭建博客系统,前者省事,但可自定义设置不足不符合一个爱折腾的人的性格,后者复杂些,爱折腾的可以学着搭建,但是服务器域名是个让人头疼的事,还有数据库等操作,着实麻烦,如今又有了另外一个选择,那就是利用GitHub和一些静态的博客系统搭建一个纯静态的博客系统,不花钱又能折腾,而且还是命令行下的,装B感觉棒棒的,另外搭配时下流行的MarkDown语法,做笔记正合我意。

    +
    + + +

    技术栈选型

    +

    这里说是技术栈选型可能并不是很恰当,但又找不到合适的描述,就是把需要的技术介绍一下,如果还不会的,可以自行学习,或者看看我的其他文章。

    +
    +
      +
    • node(npm),现在node这么火,没用过都不好意思出门,但是如果你还不回的话,就先自行学习安装一下吧。
    • +
    • Hexo 静态博客程序,其实还有很多,只不过这个比较新,而且搭配Next非常漂亮,就选了它。
    • +
    • Next 可以说是Hexo的定制系统,不仅仅是做了个皮肤,简洁美观的配置项和官网说明深得我心。
    • +
    +

    搭建步骤(安装步骤)

    安装Hexo Hexo官网

    $ npm install hexo-cli -g // 安装hexo的脚手架工具
    $ hexo init blog // 初始化博客
    $ cd blog // 返回博客目录
    $ npm install // 安装依赖
    $ hexo server // 启动博客
    +
    +

    怎么样五行代码就生成并运行了一个博客是不是超简单。
    下面我们看一下生成的博客的目录

    +
    +
    .
    ├── _config.yml
    ├── package.json
    ├── scaffolds
    ├── source
    | ├── _drafts
    | └── _posts
    └── themes
    +

    _config.yml这是博客的配置文件,比如博客名称,副标题,作者等信息都在这个文件里设置。

    +

    package.json这是博客的依赖文件可以忽略

    +

    scaffolds这是博客的模板目录,当你要写一篇文章时,这里会有文章的默认类型。

    +

    source这是博客的网站资源,包括发布的文章(_posts)、关于、分类还有上传文件等。

    +

    themes这是博客的皮肤。

    +

    更多配置信息请查阅官网手册

    +

    安装Next主题 Next官网

    $ cd your-hexo-site
    $ git clone https://github.com/iissnan/hexo-theme-next themes/next
    + +

    然后到_config.yml配置文件将主题配置改成next就可以使用next的皮肤了

    +
    theme: next
    +

    皮肤也有配置文件,为跟Hexo进行区分Hexo的配置文件称为站点配置文件, 皮肤配置文件称为主题配置文件

    +

    对两个配置文件进行简单配置后,符合需求的博客就搭建而成了,这里有个友好的建议,配置文件如果配置不正确将不能正确运行博客,所以在配置前务必保留好原始配置文件,注意配置时不要缺了空格,不要问我为什么知道这个。

    +

    更新博客主题

    https://github.com/theme-next/hexo-theme-next/blob/master/docs/zh-CN/UPDATE-FROM-5.1.X.md

    +

    写博客

    博客已经搭建好了,接下来就是写博客了,那么如何开始写博客呢,超级简单一行命令足以:

    +
    hexo new [layout] <title>  
    // layout 为模板类型可以省略,title为文章标题,通常可以简写为如下
    hexo new title

    +

    新建命令执行后在_posts的目录下就会生成一个你刚才命名的md后缀的文件,这就是一个MarkDown语法的文件,(如果不了解MarkDown语法的可以去学一下,很简单的符号语言,或者像我一样用支持MarkDown语法的编辑器来写文章。新建的文章打开内容如下

    +
    ---
    title: new-post
    date: 2017-10-11 15:01:09
    tags:
    ---

    +

    非常好理解,title就是标题,date为创建时间,tags是标签方便分类,但是这些并不全,还有些常用的分类没有写上,下面我将常用的进行补充

    +
    ---
    title: new-post
    date: 2017-10-11 15:01:09
    categories:
    - NodeJS
    - npm
    tags:
    - npm
    - NodeJS
    - rnpm
    ---
    +

    这样补充后就有了常见博客的分类和标签的功能,是不是很简单。

    +

    写完文章以后还要执行下面命令,生成静态页面

    +
    $ hexo generate // 将md后缀文件生成成静态html文件

    + + +

    这样我们就完成了博客的搭建和博客的书写,到现在我们就已经有了一个本地的博客,那么如何将博客上传到GitHub上呢?

    +

    将Blog上传至GitHub

    github是一个代码托管的平台,为了方便描述代码功能,它提供了README.md文件进行说明,但是为了更好的展现,也提供了gitpage的功能,博客是基于这个功能进行的扩展,那么如何用gitpage的功能来实现博客系统呢?

    +

    创建仓库

    创建一个以你的GitHub账号为开头命名的仓库,格式如下

    +
    GitHub账号名称.github.io
    // 如
    lixuguang.github.io
    +

    然后到blog系统的配置文件_config.yml里配置一下上传路径

    +
    deploy:
    type: git
    repo: <repository url>

    // 我的实例
    deploy:
    type: git
    repo: git@github.com:lixuguang/lixuguang.github.io.git
    branch: master

    +

    配置好就可以进行部署了,部署也很简单,只需要执行一下下面的命令。

    +
    $ npm install hexo-deployer-git --save // 安装上传工具

    $ hexo deploy

    +

    稍等一会,如果没有出现什么错误信息,那么你的部署就成功了。之后你就可以访问你的博客了,博客地址如下:
    https://你的github账号.github.io/
    我的如下:
    https://lixuguang.github.io/

    +

    现在你是不是已经学会如何利用github搭建一个静态的博客系统了呢,如果你还没有一个自己的技术博客,快来试试吧。

    +

    技巧

    是不是觉得命令行还是挺麻烦的,要敲那么一大串字母,哈哈实际上这些常用命令是有缩写方式的,下面给大家介绍一下缩写方式。

    +
    $ hexo server
    // 简写
    $ hexo s

    $ hexo generate
    // 简写
    $ hexo g

    $ hexo deploy
    // 简写
    $ hexo d

    $ hexo new
    // 简写
    $ hexo n
    + +

    另外每次发布之前最好执行以下命令,清理当前内容

    +
    $ hexo clean

    +

    以防出现冲突的情况,具体动作如下

    +
    $ hexo clean
    $ hexo g -d // 文件生成后立即部署网站
    $ hexo d -g // 部署之前预先生成静态文件

    + +

    常见问题

      +
    1. SSH问题
    2. +
    +
    $ ssh-keygen -t rsa -C "lixuguang316@gmail.com"
    // 填写你自己的github邮箱

    +

    敲三下回车,之后会在

    +
    C:\Users\Administrator\.ssh // windows下
    open ~/.ssh // Mac下打开ssh文件
    +

    文件夹下生成两个文件id_rsa(私钥)、id_rsa.pub(公钥),在github上的SSH处添加新的ssh,然后将公钥内容贴到上面起个名字可以叫hexo,保存,然后在git bash下敲击

    +
    $ ssh git@github.com
    +

    然后敲yes就可以上传blog代码了
    怎么样会了么?更多高阶玩法请阅读官方说明文档,文章如有谬误之处请各位指出,如果觉得文章对你有所帮助我将十分开心,如果你喜欢我的文章可以到我的github上点个fork,谢谢你的阅读。

    +]]>
    + + 杂七杂八 + 博客技巧 + + + Hexo + +
    + + React 源码剖析系列 - 不可思议的 react diff【转载】 + /2020/04/03/source-code-react-diff/ + 目前,前端领域中 React 势头正盛,使用者众多却少有能够深入剖析内部实现机制和原理。本系列文章希望通过剖析 React 源码,理解其内部的实现原理,知其然更要知其所以然。

    +

    React diff 作为 Virtual DOM 的加速器,其算法上的改进优化是 React 整个界面渲染的基础,以及性能提高的保障,同时也是 React 源码中最神秘、最不可思议的部分,本文从源码入手,深入剖析 React diff 的不可思议之处。

    +

    阅读本文需要对 React 有一定的了解,如果你不知何为 React,请详读 React 官方文档
    如果你对 React diff 存在些许疑惑,或者你对算法优化感兴趣,那么本文值得阅读和讨论。

    + +

    前言

    React 中最值得称道的部分莫过于 Virtual DOM 与 diff 的完美结合,特别是其高效的 diff 算法,让用户可以无需顾忌性能问题而”任性自由”的刷新页面,让开发者也可以无需关心 Virtual DOM 背后的运作原理,因为 React diff 会帮助我们计算出 Virtual DOM 中真正变化的部分,并只针对该部分进行实际 DOM 操作,而非重新渲染整个页面,从而保证了每次操作更新后页面的高效渲染,因此 Virtual DOM 与 diff 是保证 React 性能口碑的幕后推手。

    +

    行文至此,可能会有读者质疑:React 无非就是引入 diff 这一概念,且 diff 算法也并非其首创,何必吹嘘的如此天花乱坠呢?

    +

    其实,正是因为 diff 算法的普识度高,就更应该认可 React 针对 diff 算法优化所做的努力与贡献,更能体现 React 开发者们的魅力与智慧!

    +

    传统 diff 算法

    计算一棵树形结构转换成另一棵树形结构的最少操作,是一个复杂且值得研究的问题。传统 diff 算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n^3),其中 n 是树中节点的总数。O(n^3) 到底有多可怕,这意味着如果要展示1000个节点,就要依次执行上十亿次的比较。这种指数型的性能消耗对于前端渲染场景来说代价太高了!现今的 CPU 每秒钟能执行大约30亿条指令,即便是最高效的实现,也不可能在一秒内计算出差异情况。

    +

    如果 React 只是单纯的引入 diff 算法而没有任何的优化改进,那么其效率是远远无法满足前端渲染所要求的性能。

    +

    因此,想要将 diff 思想引入 Virtual DOM,就需要设计一种稳定高效的 diff 算法,而 React 做到了!

    +

    那么,React diff 到底是如何实现的呢?

    +

    详解 React diff

    传统 diff 算法的复杂度为 O(n^3),显然这是无法满足性能要求的。React 通过制定大胆的策略,将 O(n^3) 复杂度的问题转换成 O(n) 复杂度的问题。

    +

    diff 策略

      +
    1. Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
    2. +
    3. 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
    4. +
    5. 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。
    6. +
    +

    基于以上三个前提策略,React 分别对 tree diff、component diff 以及 element diff 进行算法优化,事实也证明这三个前提策略是合理且准确的,它保证了整体界面构建的性能。

    +
      +
    • tree diff
    • +
    • component diff
    • +
    • element diff
    • +
    +
    +

    本文中源码 ReactMultiChild.js

    +
    +

    tree diff

    基于策略一,React 对树的算法进行了简洁明了的优化,即对树进行分层比较,两棵树只会对同一层次的节点进行比较。

    +

    既然 DOM 节点跨层级的移动操作少到可以忽略不计,针对这一现象,React 通过 updateDepth 对 Virtual DOM 树进行层级控制,只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。

    +

    https://pic1.zhimg.com/0c08dbb6b1e0745780de4d208ad51d34_r.jpg

    +
    updateChildren: function(nextNestedChildrenElements, transaction, context) {
    updateDepth++;
    var errorThrown = true;
    try {
    this._updateChildren(nextNestedChildrenElements, transaction, context);
    errorThrown = false;
    } finally {
    updateDepth--;
    if (!updateDepth) {
    if (errorThrown) {
    clearQueue();
    } else {
    processQueue();
    }
    }
    }
    }
    +

    分析至此,大部分人可能都存在这样的疑问:如果出现了 DOM 节点跨层级的移动操作,React diff 会有怎样的表现呢?是的,对此我也好奇不已,不如试验一番。

    +

    如下图,A 节点(包括其子节点)整个被移动到 D 节点下,由于 React 只会简单的考虑同层级节点的位置变换,而对于不同层级的节点,只有创建和删除操作。当根节点发现子节点中 A 消失了,就会直接销毁 A;当 D 发现多了一个子节点 A,则会创建新的 A(包括子节点)作为其子节点。此时,React diff 的执行情况:create A -> create B -> create C -> delete A

    +

    由此可发现,当出现节点跨层级移动时,并不会出现想象中的移动操作,而是以 A 为根节点的树被整个重新创建,这是一种影响 React 性能的操作,因此 React 官方建议不要进行 DOM 节点跨层级的操作

    +
    +

    注意:在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。例如,可以通过 CSS 隐藏或显示节点,而不是真的移除或添加 DOM 节点。

    +
    +

    https://pic2.zhimg.com/d712a73769688afe1ef1a055391d99ed_r.jpg

    +

    component diff

    React 是基于组件构建应用的,对于组件间的比较所采取的策略也是简洁高效。

    +
      +
    • 如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。
    • +
    • 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。
    • +
    • 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。
    • +
    +

    如下图,当 component D 改变为 component G 时,即使这两个 component 结构相似,一旦 React 判断 D 和 G 是不同类型的组件,就不会比较二者的结构,而是直接删除 component D,重新创建 component G 以及其子节点。虽然当两个 component 是不同类型但结构相似时,React diff 会影响性能,但正如 React 官方博客所言:不同类型的 component 是很少存在相似 DOM tree 的机会,因此这种极端因素很难在实现开发过程中造成重大影响的。

    +

    https://pic1.zhimg.com/52654992aba15fc90e2dac8b2387d0c4_r.jpg

    +

    element diff

    当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)

    +
      +
    • INSERT_MARKUP ,新的 component 类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作。

      +
    • +
    • MOVE_EXISTING ,在老集合有新 component 类型,且 element 是可更新的类型,generateComponentChildren 已调用 receiveComponent,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点。

      +
    • +
    • REMOVE_NODE ,老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作。

      +
    • +
    +
    function enqueueInsertMarkup(parentInst, markup, toIndex) {
    updateQueue.push({
    parentInst: parentInst,
    parentNode: null,
    type: ReactMultiChildUpdateTypes.INSERT_MARKUP,
    markupIndex: markupQueue.push(markup) - 1,
    content: null,
    fromIndex: null,
    toIndex: toIndex,
    });
    }

    function enqueueMove(parentInst, fromIndex, toIndex) {
    updateQueue.push({
    parentInst: parentInst,
    parentNode: null,
    type: ReactMultiChildUpdateTypes.MOVE_EXISTING,
    markupIndex: null,
    content: null,
    fromIndex: fromIndex,
    toIndex: toIndex,
    });
    }

    function enqueueRemove(parentInst, fromIndex) {
    updateQueue.push({
    parentInst: parentInst,
    parentNode: null,
    type: ReactMultiChildUpdateTypes.REMOVE_NODE,
    markupIndex: null,
    content: null,
    fromIndex: fromIndex,
    toIndex: null,
    });
    }
    +

    如下图,老集合中包含节点:A、B、C、D,更新后的新集合中包含节点:B、A、D、C,此时新老集合进行 diff 差异化对比,发现 B != A,则创建并插入 B 至新集合,删除老集合 A;以此类推,创建并插入 A、D 和 C,删除 B、C 和 D。

    +

    https://pic2.zhimg.com/7541670c089b84c59b84e9438e92a8e9_r.jpg

    +

    React 发现这类操作繁琐冗余,因为这些都是相同的节点,但由于位置发生变化,导致需要进行繁杂低效的删除、创建操作,其实只要对这些节点进行位置移动即可。

    +

    针对这一现象,React 提出优化策略:允许开发者对同一层级的同组子节点,添加唯一 key 进行区分,虽然只是小小的改动,性能上却发生了翻天覆地的变化!

    +

    新老集合所包含的节点,如下图所示,新老集合进行 diff 差异化对比,通过 key 发现新老集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将老集合中节点的位置进行移动,更新为新集合中节点的位置,此时 React 给出的 diff 结果为:B、D 不做任何操作,A、C 进行移动操作,即可。

    +

    https://pic4.zhimg.com/c0aa97d996de5e7f1069e97ca3accfeb_r.jpg

    +

    那么,如此高效的 diff 到底是如何运作的呢?让我们通过源码进行详细分析。

    +

    首先对新集合的节点进行循环遍历,for (name in nextChildren),通过唯一 key 可以判断新老集合中是否存在相同的节点,if (prevChild === nextChild),如果存在相同节点,则进行移动操作,但在移动前需要将当前节点在老集合中的位置与 lastIndex 进行比较,if (child._mountIndex < lastIndex),则进行节点移动操作,否则不执行该操作。这是一种顺序优化手段,lastIndex 一直在更新,表示访问过的节点在老集合中最右的位置(即最大的位置),如果新集合中当前访问的节点比 lastIndex 大,说明当前访问节点在老集合中就比上一个节点位置靠后,则该节点不会影响其他节点的位置,因此不用添加到差异队列中,即不执行移动操作,只有当访问的节点比 lastIndex 小时,才需要进行移动操作。

    +

    以上图为例,可以更为清晰直观的描述 diff 的差异对比过程:

    +
      +
    • 从新集合中取得 B,判断老集合中存在相同节点 B,通过对比节点位置判断是否进行移动操作,B 在老集合中的位置 B._mountIndex = 1,此时 lastIndex = 0,不满足 child._mountIndex < lastIndex 的条件,因此不对 B 进行移动操作;更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),其中 prevChild._mountIndex 表示 B 在老集合中的位置,则 lastIndex = 1,并将 B 的位置更新为新集合中的位置prevChild._mountIndex = nextIndex,此时新集合中 B._mountIndex = 0,nextIndex++ 进入下一个节点的判断。

      +
    • +
    • 从新集合中取得 A,判断老集合中存在相同节点 A,通过对比节点位置判断是否进行移动操作,A 在老集合中的位置 A._mountIndex = 0,此时 lastIndex = 1,满足 child._mountIndex < lastIndex的条件,因此对 A 进行移动操作enqueueMove(this, child._mountIndex, toIndex),其中 toIndex 其实就是 nextIndex,表示 A 需要移动到的位置;更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),则 lastIndex = 1,并将 A 的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex,此时新集合中A._mountIndex = 1,nextIndex++ 进入下一个节点的判断。

      +
    • +
    • 从新集合中取得 D,判断老集合中存在相同节点 D,通过对比节点位置判断是否进行移动操作,D 在老集合中的位置 D._mountIndex = 3,此时 lastIndex = 1,不满足 child._mountIndex < lastIndex的条件,因此不对 D 进行移动操作;更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),则 lastIndex = 3,并将 D 的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex,此时新集合中D._mountIndex = 2,nextIndex++ 进入下一个节点的判断。

      +
    • +
    • 从新集合中取得 C,判断老集合中存在相同节点 C,通过对比节点位置判断是否进行移动操作,C 在老集合中的位置 C._mountIndex = 2,此时 lastIndex = 3,满足 child._mountIndex < lastIndex 的条件,因此对 C 进行移动操作 enqueueMove(this, child._mountIndex, toIndex);更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),则 lastIndex = 3,并将 C 的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex,此时新集合中 C._mountIndex = 3,nextIndex++ 进入下一个节点的判断,由于 C 已经是最后一个节点,因此 diff 到此完成。

      +
    • +
    +

    以上主要分析新老集合中存在相同节点但位置不同时,对节点进行位置移动的情况,如果新集合中有新加入的节点且老集合存在需要删除的节点,那么 React diff 又是如何对比运作的呢?

    +

    以下图为例:

    +
      +
    • 从新集合中取得 B,判断老集合中存在相同节点 B,由于 B 在老集合中的位置 B._mountIndex = 1,此时lastIndex = 0,因此不对 B 进行移动操作;更新 lastIndex = 1,并将 B 的位置更新为新集合中的位置B._mountIndex = 0,nextIndex++进入下一个节点的判断。

      +
    • +
    • 从新集合中取得 E,判断老集合中不存在相同节点 E,则创建新节点 E;更新 lastIndex = 1,并将 E 的位置更新为新集合中的位置,nextIndex++进入下一个节点的判断。

      +
    • +
    • 从新集合中取得 C,判断老集合中存在相同节点 C,由于 C 在老集合中的位置C._mountIndex = 2,lastIndex = 1,此时 C._mountIndex > lastIndex,因此不对 C 进行移动操作;更新 lastIndex = 2,并将 C 的位置更新为新集合中的位置,nextIndex++ 进入下一个节点的判断。

      +
    • +
    • 从新集合中取得 A,判断老集合中存在相同节点 A,由于 A 在老集合中的位置A._mountIndex = 0,lastIndex = 2,此时 A._mountIndex < lastIndex,因此对 A 进行移动操作;更新 lastIndex = 2,并将 A 的位置更新为新集合中的位置,nextIndex++ 进入下一个节点的判断。

      +
    • +
    • 当完成新集合中所有节点 diff 时,最后还需要对老集合进行循环遍历,判断是否存在新集合中没有但老集合中仍存在的节点,发现存在这样的节点 D,因此删除节点 D,到此 diff 全部完成。

      +
    • +
    +

    https://pic1.zhimg.com/7b9beae0cf0a5bc8c2e82d00c43d1c90_r.jpg

    +
    _updateChildren: function(nextNestedChildrenElements, transaction, context) {
    var prevChildren = this._renderedChildren;
    var nextChildren = this._reconcilerUpdateChildren(
    prevChildren, nextNestedChildrenElements, transaction, context
    );
    if (!nextChildren && !prevChildren) {
    return;
    }
    var name;
    var lastIndex = 0;
    var nextIndex = 0;
    for (name in nextChildren) {
    if (!nextChildren.hasOwnProperty(name)) {
    continue;
    }
    var prevChild = prevChildren && prevChildren[name];
    var nextChild = nextChildren[name];
    if (prevChild === nextChild) {
    // 移动节点
    this.moveChild(prevChild, nextIndex, lastIndex);
    lastIndex = Math.max(prevChild._mountIndex, lastIndex);
    prevChild._mountIndex = nextIndex;
    } else {
    if (prevChild) {
    lastIndex = Math.max(prevChild._mountIndex, lastIndex);
    // 删除节点
    this._unmountChild(prevChild);
    }
    // 初始化并创建节点
    this._mountChildAtIndex(
    nextChild, nextIndex, transaction, context
    );
    }
    nextIndex++;
    }
    for (name in prevChildren) {
    if (prevChildren.hasOwnProperty(name) &&
    !(nextChildren && nextChildren.hasOwnProperty(name))) {
    this._unmountChild(prevChildren[name]);
    }
    }
    this._renderedChildren = nextChildren;
    },
    // 移动节点
    moveChild: function(child, toIndex, lastIndex) {
    if (child._mountIndex < lastIndex) {
    this.prepareToManageChildren();
    enqueueMove(this, child._mountIndex, toIndex);
    }
    },
    // 创建节点
    createChild: function(child, mountImage) {
    this.prepareToManageChildren();
    enqueueInsertMarkup(this, mountImage, child._mountIndex);
    },
    // 删除节点
    removeChild: function(child) {
    this.prepareToManageChildren();
    enqueueRemove(this, child._mountIndex);
    },

    _unmountChild: function(child) {
    this.removeChild(child);
    child._mountIndex = null;
    },

    _mountChildAtIndex: function(
    child,
    index,
    transaction,
    context) {
    var mountImage = ReactReconciler.mountComponent(
    child,
    transaction,
    this,
    this._nativeContainerInfo,
    context
    );
    child._mountIndex = index;
    this.createChild(child, mountImage);
    },
    + +

    当然,React diff 还是存在些许不足与待优化的地方,如下图所示,若新集合的节点更新为:D、A、B、C,与老集合对比只有 D 节点移动,而 A、B、C 仍然保持原有的顺序,理论上 diff 应该只需对 D 执行移动操作,然而由于 D 在老集合的位置是最大的,导致其他节点的 _mountIndex < lastIndex,造成 D 没有执行移动操作,而是 A、B、C 全部移动到 D 节点后面的现象。

    +

    在此,读者们可以讨论思考:如何优化上述问题?

    +
    +

    建议:在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。

    +
    +

    https://pic2.zhimg.com/1b8dac5b9b3e4452dec8d5447d7717ad_r.jpg

    +

    总结

      +
    • React 通过制定大胆的 diff 策略,将 O(n3) 复杂度的问题转换成 O(n) 复杂度的问题;
    • +
    • React 通过分层求异的策略,对 tree diff 进行算法优化;
    • +
    • React 通过相同类生成相似树形结构,不同类生成不同树形结构的策略,对 component diff 进行算法优化;
    • +
    • React 通过设置唯一 key的策略,对 element diff 进行算法优化;
    • +
    • 建议,在开发组件时,保持稳定的 DOM 结构会有助于性能的提升;
    • +
    • 建议,在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。
    • +
    +

    参考资料

    A Survey on Tree Edit Distance and Related Problems
    Reconciliation

    +

    如果本文能够为你解决些许关于 React diff 算法的疑惑,请点个赞吧!

    +]]>
    + + 前端框架 + React + + + 源码解析 + React + +
    + + Could not proxy request XXX from localhost:8080 to localhost:8081 + /2020/02/12/vue-proxyTable-problem/ + +

    作者:李旭光
    引用请标明出处

    + +

    最近又有小伙伴问我,为啥我在写vue项目做代理转发时候报找不到代理服务的问题?
    我看了一下他发给我的配置截图,仅一眼我就发现了问题所在,因为这个坑我曾经也踩过,而且好多小伙伴也都踩过,在写转发服务器的时候没写协议头,也就是在 target 那没写 http:// 这个部分,添加上以后重启一下,这个问题就解决了,小伙伴说就这个问题他解决了一天也没解决了,而且我说完了以后他想起来以前好像也遇到过这个问题,所以说在平时工作中遇到问题要常做笔记才对,所以为了避免以后自己也犯这种错误,暂且留一篇文章记录一下这个问题。

    +]]>
    + + 前端技术 + 前端问题 + + + 奇怪问题 + +
    + + Webpack 打包时利用 UglifyJsPlugin 去掉comments 、console 、和 debugger + /2020/04/07/webpack-uglifyjsplugin/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    最近在检查公司前端代码质量时,经常会在sonar检查到有漏洞级别的问题特别多,但是打开查看详情的时候发现又特别低级,都是些console没去掉啊,debugger没去掉啊,之前都是看到了就提醒一下,但是屡禁不止,那么有没有什么好的办法能够从源头上避免这个问题呢,果然这个问题不止我一个人遇到,网上还是有大神给出了解决方案,那就是 webpack 打包时可以引入 UglifyJsPlugin 来解决这个扰人的问题,而且屏蔽了人为的因素,使用感觉是极好的,所以,写个帖子把网上学来的资料整理一下。

    + + +

    UglifyJsPlugin

    关于 UglifyJsPlugin 的介绍,在 webpack 的官网上有这样一段描述

    +
    +

    ℹ️ webpack =< v3.0.0 currently contains v0.4.6 of this plugin under webpack.optimize.UglifyJsPlugin as an alias. For usage of the latest version (v1.0.0), please follow the instructions below. Aliasing v1.0.0 as webpack.optimize.UglifyJsPlugin is scheduled for webpack v4.0.0

    +
    +

    简单来说就是在 webpack3.0 之前,引入了这个插件的0.4.6版本,并用 webpack.optimize.UglifyJsPlugin 作为它的别名, 在 webpack4.0 中引入了插件的 1.0.0 版本,用了同样的名称作为别名,用的时候请注意。

    +

    那么“这个插件”是什么呢, webpack 的官网也告诉我们了,那就是UglifyJS

    +
    +

    A JavaScript parser, mangler/compressor and beautifier toolkit for ES6+.

    +
    +

    翻译过来就是“一个用于ES6+的JavaScript解析器、(榨汁机:翻译的挺有意思)/压缩机和美化工具。”

    +

    再简单点说 uglifyJsPlugin 用来对js文件进行压缩,减小js文件的大小。其会拖慢webpack的编译速度,建议开发环境时关闭,生产环境再将其打开。

    +

    要用它的的话记得先安装

    +
    npm i -D uglifyjs-webpack-plugin
    +

    安装完成就可以用了

    +
    // webpack.config.js
    const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

    module.exports = {
    plugins: [
    new UglifyJsPlugin()
    ]
    }
    + +

    当然他有很多的选项,具体想了解可以去 webpack 官网,或者去插件官网都可以找到uglifyjs-webpack-plugin

    +

    当然知道你们懒的去看了,心里肯定也在说“直接给我个现成的配置他不香么?”,别急嘛,这就给你们

    +
    new UglifyJsPlugin({
    //删除注释
    output:{
    comments:false
    },
    //删除console 和 debugger 删除警告
    compress:{
    warnings:false,
    drop_debugger:true,
    drop_console:true
    }
    })
    +

    当然版本间可能会有些差别,但是option是不变的,有调整的话各位自行调整一下。
    公司用的 vue-cli3 所以这里再给出个 vue-cli3 默认配置文件下的写法

    +
    // vue.config.js

    configureWebpack:{
    optimization: {
    minimizer: [
    new UglifyJsPlugin({
    uglifyOptions: {
    // 删除注释
    output:{
    comments:false
    },
    // 删除console debugger 删除警告
    compress: {
    warnings: false,
    drop_console: true,//console
    drop_debugger: false,
    pure_funcs: ['console.log']//移除console
    }
    }
    })
    ]
    }
    }
    + +

    问题收集

      +
    1. 运行出现报错 UglifyJs
    2. +
    +
    +

    Q: DefaultsError: warnings is not a supported option

    +
    +
    +

    A: 降低版本(使用 “uglifyjs-webpack-plugin”: “^1.1.1”),打包正常,效果达到

    +
    +]]>
    + + 前端工程化 + Webpack + + + UglifyJsPlugin + +
    + + 《Web全栈工程师的自我修养》读书笔记 + /2017/10/12/web-quanzhan/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    近日工作不是那么忙,所以有时间看看书,这本书之前就已经读过一遍,隔了一段时间已经有所忘记,所以再次拾起,重新回顾一下。.

    + + +

    什么是全栈

    “全栈”是个外来词,翻译自英文full-stack,此处的栈指的是为了完成项目而使用的一系列技术的合集,不是堆栈概念中的栈。

    +

    “全端”工程师是指能够完成pc端、移动端等多终端设备适配的情况

    +

    什么是全栈工程师

    +

    全栈工程师是指一个能够处理数据库、服务器、系统工程、客户端等所有工作的的工程师,根据项目不同,可能是移动栈、Web栈,或者原生应用程序栈。

    +
    +

    简单来说全栈工程师就是一个人能搞定一个项目,全能大神一样的人物。

    +

    一个Web产品典型的技术栈

    +

    服务器+数据库+服务器端编程语言+前端编程语言

    +
    +
    +

    全栈工程师技术的兴起有两个重要原因:技术的发展和PaaS(Platform as a Service,平台即服务)服务的平台越来越多。

    +
    +

    全栈框架———MEAN

    +

    MongoDB-Express-AngularJs-Node.js
    前后端采用一种编程语言JavaScript

    +
    +

    全栈工程师的要求

    一专多长

    在一个领域里至少达到高级的级别,然后再去向上游或者下游延伸

    +

    关注商业目标

    公司聘请你是为了让你产生利润,并不关心你会什么,所以选择技术栈时要考虑的是如何降低公司的成本或者提高收入。

    +

    关注用户体验

    产品的最终目标是满足客户的需求,所以作为全栈工程师必须要关注用户体验。

    +
    +

    这是一些作为全栈工程师我整理出来的干货,这本书本身并不是一本技术性很强的书,倒像是一位过来人介绍些经验,适合刚入职场或者进入职场不久的人,在前端领域比较迷茫时看一看,书中介绍了作者读过的一些书,很有参考性,推荐大家阅读。

    +]]>
    + + 自我提升 + 读书笔记 + +
    + + 2020年了,再不会webpack敲得代码就不香了(近万字实战)【转载】 + /2020/03/24/webpack-learning-1/ + 推荐序

    这里是我自己写的,看了这篇文章把我零碎的 webpack 知识系统的整理了一下,感觉受益匪浅,推荐更多小伙伴看一看这篇文章,好文共享,建议稍微对 webpack 了解服用更佳。
    2020年了,再不会webpack敲得代码就不香了(近万字实战)

    +

    前言

    2020年即将到来,在众多前端的招聘要求里, webpack 、工程化这些字眼频率越来越高。日常开发者中,我们常常在用诸如 vue-clicreate-react-app 的脚手架来构建我们的项目。但是如果你想在团队脱颖而出(鹤立鸡群)、拿到更好的 offer (还房贷),那么你必须去深刻的认识下我们经常打交道的 webpack

    +

    入门(一起来用这些小例子让你熟悉webpack的配置)

    webpack 是什么?

    webpack 是一个现代 JavaScript 应用程序的静态模块打包器,当 webpack 处理应用程序时,会递归构建一个依赖关系图,其中包含应用程序需要的每个模块,然后将这些模块打包成一个或多个 bundle

    +

    webpack 的核心概念

      +
    • entry: 入口
    • +
    • output: 输出
    • +
    • loader: 模块转换器,用于把模块原内容按照需求转换成新内容
    • +
    • plugins: 扩展插件,在 webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要做的事情
    • +
    +

    初始化项目

    新建一个目录,初始化 npm

    +
    npm init
    + +

    webpack 是运行在 node 环境中的,我们需要安装以下两个 npm

    +
    npm i -D webpack webpack-cli
    + +
    +
      +
    • npm i -Dnpm install --save-dev 的缩写
    • +
    • npm i -Snpm install --save 的缩写
    • +
    +
    +

    新建一个文件夹 src ,然后新建一个文件 main.js ,写一点代码测试一下

    +
    console.log('call me 老yuan')
    + +

    配置 package.json 命令

    +
    "script":{
    "build":"webpack src/main.js"
    }
    +

    执行

    +
    npm run build
    +

    此时如果生成了一个 dist 文件夹,并且内部含有 main.js 说明已经打包成功了

    +

    开始我们自己的配置

    上面一个简单的例子只是 webpack 自己默认的配置,下面我们要实现更加丰富的自定义配置

    +

    新建一个 build 文件夹,里面新建一个 webpack.config.js

    +
    // webpack.config.js

    const path = require('path');
    module.exports = {
    mode:'development', // 开发模式
    entry: path.resolve(__dirname,'../src/main.js'), // 入口文件
    output: {
    filename: 'output.js', // 打包后的文件名称
    path: path.resolve(__dirname,'../dist') // 打包后的目录
    }
    }
    +

    更改我们的打包命令

    +
    "script":{
    "build":"webpack build/webpack.config.js"
    }
    +

    执行 npm run build
    会发现生成了以下目录

    +
    project
    dist
    build
    src
    +

    其中 dist 文件夹中的 main.js 就是我们需要在浏览器中实际运行的文件
    当然实际运用中不会仅仅如此,下面让我们通过实际案例带你快速入手 webpack

    +

    配置html模板

    js 文件打包好了,但是我们不可能每次在html文件中手动引入打包好的js

    +
    +

    这里可能有的朋友会认为我们打包 js 文件名称不是一直是固定的嘛( output.js )?这样每次就不用改动引入文件名称了呀?实际上我们日常开发中往往会这样配置:

    +
    +
    module.exports = {
    // 省略其他配置
    output: {
    filename: '[name].[hash:8].js', // 打包后的文件名称
    path: path.resolve(__dirname,'../dist') // 打包后的目录
    }
    }
    + +

    这时候生成的 dist 目录文件如下

    +
    dist/
    app.fsafasf.js
    +

    为了缓存,你会发现打包好的 js 文件的名称每次都不一样。 webpack 打包出来的js文件我们需要引入到 html 中,但是每次我们都手动修改 js 文件名显得很麻烦,因此我们需要一个插件来帮我们完成这件事情,那就是 html-webpack-plugin

    +
    npm i -D html-webpack-plugin
    +

    新建一个 build 同级的文件夹 public ,里面新建一个 index.html
    具体配置文件如下

    +
    // webpack.config.js
    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-plugin') // **此插件用来解决html模板引入js文件
    module.exports = {
    mode:'development', // 开发模式
    entry: path.resolve(__dirname,'../src/main.js'), // 入口文件
    output: {
    filename: '[name].[hash:8].js', // 打包后的文件名称
    path: path.resolve(__dirname,'../dist') // 打包后的目录
    },
    plugins:[
    new HtmlWebpackPlugin({
    template:path.resolve(__dirname,'../public/index.html')
    })
    ]
    }
    +

    可以发现打包生成的js文件已经被自动引入 html 文件中

    +

    多入口文件如何开发

    +

    生成多个 html-webpack-plugin 实例来解决这个问题

    +
    +
    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    module.exports = {
    mode:'development', // 开发模式
    entry: {
    main:path.resolve(__dirname,'../src/main.js'),
    header:path.resolve(__dirname,'../src/header.js')
    },
    output: {
    filename: '[name].[hash:8].js', // 打包后的文件名称
    path: path.resolve(__dirname,'../dist') // 打包后的目录
    },
    plugins:[
    new HtmlWebpackPlugin({
    template:path.resolve(__dirname,'../public/index.html'),
    filename:'index.html',
    chunks:['main'] // 与入口文件对应的模块名
    }),
    new HtmlWebpackPlugin({
    template:path.resolve(__dirname,'../public/header.html'),
    filename:'header.html',
    chunks:['header'] // 与入口文件对应的模块名
    }),
    ]
    }
    + +

    clean-webpack-plugin

    +

    每次执行 npm run build 会发现 dist 文件夹里会残留上次打包的文件,这里我们推荐一个 plugin 来帮我们在打包输出前清空文件夹 clean-webpack-plugin

    +
    +
    const {CleanWebpackPlugin} = require('clean-webpack-plugin')
    module.exports = {
    // ...省略其他配置
    plugins:[new CleanWebpackPlugin()]
    }
    +
    希望dist目录下某个文件夹不被清空

    不过呢,有些时候,我们并不希望整个 dist 目录都被清空,比如,我们不希望,每次打包的时候,都删除 dll 目录,以及 dll 目录下的文件或子目录,该怎么办呢?

    +

    clean-webpack-plugin 为我们提供了参数 cleanOnceBeforeBuildPatterns

    +
    //webpack.config.js
    module.exports = {
    //...
    plugins: [
    new CleanWebpackPlugin({
    cleanOnceBeforeBuildPatterns:['**/*', '!dll', '!dll/**'] //不删除dll目录下的文件
    })
    ]
    }
    + +

    引用CSS

    我们的入口文件是 js ,所以我们在入口 js 中引入我们的 css 文件

    +
    import 'asset/style.css'
    +

    同时我们也需要一些 loader 来解析我们的 css 文件

    +
    npm i -D style-loader css-loader
    +

    如果我们使用 less 来构建样式,则需要多安装两个

    +
    npm i -D less less-loader
    +

    配置文件如下

    +
    // webpack.config.js
    module.exports = {
    // ...省略其他配置
    module:{
    rules:[
    {
    test:/\.css$/,
    use:['style-loader','css-loader'] // 从右向左解析原则
    },
    {
    test:/\.less$/,
    use:['style-loader','css-loader','less-loader'] // 从右向左解析原则
    }
    ]
    }
    }
    + +

    我们简单说一下上面的配置:

    +
      +
    • style-loader 动态创建 style 标签,将 css 插入到 head 中.
    • +
    • css-loader 负责处理 @import 等语句。
    • +
    • postcss-loaderautoprefixer ,自动生成浏览器兼容性前缀 —— 2020了,应该没人去自己徒手去写浏览器前缀了吧
    • +
    • less-loader 负责处理编译 .less 文件,将其转为 css
    • +
    +
    +

    注意:
    loader 的执行顺序是从右向左执行的,也就是后面的 loader 先执行,上面 loader 的执行顺序为: less-loader —> postcss-loader —> css-loader —> style-loader
    当然,loader 其实还有一个参数,可以修改优先级,enforce 参数,其值可以为: pre(优先执行) 或 post (滞后执行)。
    现在,我们已经可以处理 .less 文件啦,.css 文件只需要修改匹配规则,删除 less-loader 即可。

    +
    +

    为css添加浏览器前缀

    npm i -D postcss-loader autoprefixer

    +

    配置如下

    +
    // webpack.config.js
    module.exports = {
    module:{
    rules:[
    test/\.less$/,
    use:['style-loader','css-loader','postcss-loader','less-loader'] // 从右向左解析原则
    ]
    }
    }
    +

    接下来,我们还需要引入 autoprefixer 使其生效,这里有两种方式

    +
    在项目根目录下创建一个postcss.config.js文件,配置如下:
    module.exports = {
    plugins: [require('autoprefixer')] // 引用该插件即可了
    }
    +
    直接在webpack.config.js里配置
    // webpack.config.js
    module.exports = {
    //...省略其他配置
    module:{
    rules:[{
    test:/\.less$/,
    use:['style-loader','css-loader',{
    loader:'postcss-loader',
    options:{
    plugins:[require('autoprefixer')]
    }
    },'less-loader'] // 从右向左解析原则
    }]
    }
    }
    +

    这时候我们发现 css 通过 style 标签的方式添加到了 html 文件中,但是如果样式文件很多,全部添加到 html 中,难免显得混乱。这时候我们想用把 css 拆分出来用外链的形式引入 css 文件怎么做呢?这时候我们就需要借助插件来帮助我们

    +

    拆分css

    npm i -D mini-css-extract-plugin
    +
    +

    webpack 4.0 以前,我们通过 extract-text-webpack-plugin 插件,把 css 样式从 js 文件中提取到单独的 css 文件中。 webpack4.0 以后,官方推荐使用 mini-css-extract-plugin 插件来打包 css 文件

    +
    +

    配置文件如下

    +
    const MiniCssExtractPlugin = require("mini-css-extract-plugin");
    module.exports = {
    //...省略其他配置
    module: {
    rules: [
    {
    test: /\.less$/,
    use: [
    MiniCssExtractPlugin.loader,
    'css-loader',
    'less-loader'
    ],
    }
    ]
    },
    plugins: [
    new MiniCssExtractPlugin({
    filename: "[name].[hash].css",
    chunkFilename: "[id].css",
    })
    ]
    }
    +

    拆分多个css

    +

    这里需要说的细一点,上面我们所用到的 mini-css-extract-plugin 将所有的 css 样式合并为一个 css 文件。如果你想拆分为一一对应的多个 css 文件,我们需要使用到 extract-text-webpack-plugin ,而目前 mini-css-extract-plugin 还不支持此功能。我们需要安装 @next 版本的 extract-text-webpack-plugin

    +
    +
    npm i -D extract-text-webpack-plugin@next
    // webpack.config.js

    const path = require('path');
    const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')
    let indexLess = new ExtractTextWebpackPlugin('index.less');
    let indexCss = new ExtractTextWebpackPlugin('index.css');
    module.exports = {
    module:{
    rules:[
    {
    test:/\.css$/,
    use: indexCss.extract({
    use: ['css-loader']
    })
    },
    {
    test:/\.less$/,
    use: indexLess.extract({
    use: ['css-loader','less-loader']
    })
    }
    ]
    },
    plugins:[
    indexLess,
    indexCss
    ]
    }
    +

    打包 图片、字体、媒体、等文件

    file-loader 就是将文件在进行一些处理后(主要是处理文件名和路径、解析文件 url ),并将文件移动到输出的目录中
    url-loader 一般与 file-loader 搭配使用,功能与 file-loader 类似,如果文件小于限制的大小。则会返回 base64 编码,否则使用 file-loader 将文件移动到输出的目录中

    +
    // webpack.config.js
    module.exports = {
    // 省略其它配置 ...
    module: {
    rules: [
    // ...
    {
    test: /\.(jpe?g|png|gif)$/i, //图片文件
    use: [
    {
    loader: 'url-loader',
    options: {
    limit: 10240,
    fallback: {
    loader: 'file-loader',
    options: {
    name: 'img/[name].[hash:8].[ext]'
    }
    }
    }
    }
    ]
    },
    {
    test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, //媒体文件
    use: [
    {
    loader: 'url-loader',
    options: {
    limit: 10240,
    fallback: {
    loader: 'file-loader',
    options: {
    name: 'media/[name].[hash:8].[ext]'
    }
    }
    }
    }
    ]
    },
    {
    test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i, // 字体
    use: [
    {
    loader: 'url-loader',
    options: {
    limit: 10240,
    fallback: {
    loader: 'file-loader',
    options: {
    name: 'fonts/[name].[hash:8].[ext]'
    }
    }
    }
    }
    ]
    },
    ]
    }
    }
    +

    用babel转义js文件

    为了使我们的 js 代码兼容更多的环境我们需要安装依赖

    +
    npm i babel-loader @babel/preset-env @babel/core

    +
    +

    注意
    babel-loaderbabel-core 的版本对应关系

    +
    +
      +
    • babel-loader 8.x 对应 babel-core 7.x
    • +
    • babel-loader 7.x 对应 babel-core 6.x
    • +
    +

    配置如下

    +
    // webpack.config.js
    module.exports = {
    // 省略其它配置 ...
    module:{
    rules:[
    {
    test:/\.js$/,
    use:{
    loader:'babel-loader',
    options:{
    presets:['@babel/preset-env']
    }
    },
    exclude:/node_modules/
    },
    ]
    }
    }
    +

    上面的 babel-loader 只会将 ES6/7/8 语法转换为 ES5 语法,但是对新 api 并不会转换 例如( promiseGeneratorSetMapsProxy 等)
    此时我们需要借助 babel-polyfill 来帮助我们转换

    +
    npm i @babel/polyfill
    // webpack.config.js
    const path = require('path')
    module.exports = {
    entry: ["@babel/polyfill,path.resolve(__dirname,'../src/index.js')"], // 入口文件
    }
    +
    +

    手动把上面的 demo 敲一遍对阅读下面的文章更有益,建议入门的同学敲三遍以上

    +
    +

    上面的实践是我们对 webpack 的功能有了一个初步的了解,但是要想熟练应用于开发中,我们需要一个系统的实战。让我们一起摆脱脚手架尝试自己搭建一个 vue 开发环境

    +

    搭建vue开发环境

    上面的小例子已经帮助而我们实现了打包 css 、图片、 jshtml 等文件。
    但是我们还需要以下几种配置

    +

    解析.vue文件

    npm i -D vue-loader vue-template-compiler vue-style-loader
    npm i -S vue
    +
      +
    • vue-loader 用于解析 .vue 文件
    • +
    • vue-template-compiler 用于编译模板
    • +
    +

    配置如下

    +
    const vueLoaderPlugin = require('vue-loader/lib/plugin')
    module.exports = {
    module:{
    rules:[{
    test:/\.vue$/,
    use:['vue-loader']
    },]
    },
    resolve:{
    alias:{
    'vue$':'vue/dist/vue.runtime.esm.js',
    ' @':path.resolve(__dirname,'../src')
    },
    extensions:['*','.js','.json','.vue']
    },
    plugins:[
    new vueLoaderPlugin()
    ]
    }
    +

    配置webpack-dev-server进行热更新

    npm i -D webpack-dev-server
    +

    配置如下

    +
    const Webpack = require('webpack')
    module.exports = {
    // ...省略其他配置
    devServer:{
    port:3000,
    hot:true,
    contentBase:'../dist'
    },
    plugins:[
    new Webpack.HotModuleReplacementPlugin()
    ]
    }
    +

    完整配置如下

    +
    // webpack.config.js
    const path = require('path');
    const {CleanWebpackPlugin} = require('clean-webpack-plugin')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    const MiniCssExtractPlugin = require("mini-css-extract-plugin");
    const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')
    const vueLoaderPlugin = require('vue-loader/lib/plugin')
    const Webpack = require('webpack')
    module.exports = {
    mode:'development', // 开发模式
    entry: {
    main:path.resolve(__dirname,'../src/main.js'),
    },
    output: {
    filename: '[name].[hash:8].js', // 打包后的文件名称
    path: path.resolve(__dirname,'../dist') // 打包后的目录
    },
    module:{
    rules:[
    {
    test:/\.vue$/,
    use:['vue-loader']
    },
    {
    test:/\.js$/,
    use:{
    loader:'babel-loader',
    options:{
    presets:[
    ['@babel/preset-env']
    ]
    }
    }
    },
    {
    test:/\.css$/,
    use: ['vue-style-loader','css-loader',{
    loader:'postcss-loader',
    options:{
    plugins:[require('autoprefixer')]
    }
    }]
    },
    {
    test:/\.less$/,
    use: ['vue-style-loader','css-loader',{
    loader:'postcss-loader',
    options:{
    plugins:[require('autoprefixer')]
    }
    },'less-loader']
    }
    ]
    },
    resolve:{
    alias:{
    'vue$':'vue/dist/vue.runtime.esm.js',
    ' @':path.resolve(__dirname,'../src')
    },
    extensions:['*','.js','.json','.vue']
    },
    devServer:{
    port:3000,
    hot:true,
    contentBase:'../dist'
    },
    plugins:[
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
    template:path.resolve(__dirname,'../public/index.html'),
    filename:'index.html'
    }),
    new vueLoaderPlugin(),
    new Webpack.HotModuleReplacementPlugin()
    ]
    }
    +

    配置打包命令

    "script":{
    "dev":"webpack-dev-server --config build/webpack.config.js --open",
    "build":"webpack --config build/webpack.config.js"
    }
    +

    打包文件已经配置完毕,接下来让我们测试一下
    首先在 src 新建一个 main.js

    +
    // main.js
    import Vue from 'vue'
    import App from './app'
    new Vue({
    render:h=>h(App)
    }).$mount('#app')
    +

    新建一个 App.vue

    +
    // App.vue
    <template>
    <div id='container'></div>
    </template>
    <script>
    export default {
    data(){
    return {
    initData:''
    }
    }
    }
    </script>
    <style scoped>
    #container{
    width:100%;
    height:100%;
    }
    </style>
    +

    新建一个 public 文件夹,里面新建一个 index.html

    +
    // index.html
    <!DOCTYPE html>
    <html lang='en'>
    <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'>
    <title>lao li</title>
    </head>
    <body>
    <div id='app'></div>
    </body>
    </html>
    +

    执行 npm run dev 这时候如果浏览器出现 Vue 开发环境运行成功,那么恭喜你,已经成功迈出了第一步

    +

    区分开发环境与生产环境

    实际应用到项目中,我们需要区分开发环境与生产环境,我们在原来 webpack.config.js 的基础上再新增两个文件

    +

    webpack.dev.js 开发环境配置文件
    开发环境主要实现的是热更新,不要压缩代码,完整的 sourceMap
    webpack.prod.js生产环境配置文件
    生产环境主要实现的是压缩代码、提取 css 文件、合理的 sourceMap 、分割代码
    需要安装以下模块:

    +
    npm i -D  webpack-merge copy-webpack-plugin optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin
    +
      +
    • webpack-merge 合并配置
    • +
    • copy-webpack-plugin 拷贝静态资源
    • +
    • optimize-css-assets-webpack-plugin 压缩 css
    • +
    • uglifyjs-webpack-plugin 压缩js
    • +
    +
    +

    webpack mode 设置 production 的时候会自动压缩 js 代码。
    原则上不需要引入 uglifyjs-webpack-plugin 进行重复工作。
    但是 optimize-css-assets-webpack-plugin 压缩 css 的同时会破坏原有的 js 压缩,所以这里我们引入 uglifyjs 进行压缩

    +
    +
    // webpack.config.js
    const path = require('path')
    const {CleanWebpackPlugin} = require('clean-webpack-plugin')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    const vueLoaderPlugin = require('vue-loader/lib/plugin')
    const MiniCssExtractPlugin = require("mini-css-extract-plugin")
    const devMode = process.argv.indexOf('--mode=production') === -1;
    module.exports = {
    entry:{
    main:path.resolve(__dirname,'../src/main.js')
    },
    output:{
    path:path.resolve(__dirname,'../dist'),
    filename:'js/[name].[hash:8].js',
    chunkFilename:'js/[name].[hash:8].js'
    },
    module:{
    rules:[
    {
    test:/\.js$/,
    use:{
    loader:'babel-loader',
    options:{
    presets:['@babel/preset-env']
    }
    },
    exclude:/node_modules/
    },
    {
    test:/\.vue$/,
    use:['cache-loader','thread-loader',{
    loader:'vue-loader',
    options:{
    compilerOptions:{
    preserveWhitespace:false
    }
    }
    }]
    },
    {
    test:/\.css$/,
    use:[{
    loader: devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
    options:{
    publicPath:"../dist/css/",
    hmr:devMode
    }
    },'css-loader',{
    loader:'postcss-loader',
    options:{
    plugins:[require('autoprefixer')]
    }
    }]
    },
    {
    test:/\.less$/,
    use:[{
    loader:devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
    options:{
    publicPath:"../dist/css/",
    hmr:devMode
    }
    },'css-loader','less-loader',{
    loader:'postcss-loader',
    options:{
    plugins:[require('autoprefixer')]
    }
    }]
    },
    {
    test:/\.(jep?g|png|gif)$/,
    use:{
    loader:'url-loader',
    options:{
    limit:10240,
    fallback:{
    loader:'file-loader',
    options:{
    name:'img/[name].[hash:8].[ext]'
    }
    }
    }
    }
    },
    {
    test:/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
    use:{
    loader:'url-loader',
    options:{
    limit:10240,
    fallback:{
    loader:'file-loader',
    options:{
    name:'media/[name].[hash:8].[ext]'
    }
    }
    }
    }
    },
    {
    test:/\.(woff2?|eot|ttf|otf)(\?.*)?$/i,
    use:{
    loader:'url-loader',
    options:{
    limit:10240,
    fallback:{
    loader:'file-loader',
    options:{
    name:'media/[name].[hash:8].[ext]'
    }
    }
    }
    }
    }
    ]
    },
    resolve:{
    alias:{
    'vue$':'vue/dist/vue.runtime.esm.js',
    ' @':path.resolve(__dirname,'../src')
    },
    extensions:['*','.js','.json','.vue']
    },
    plugins:[
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
    template:path.resolve(__dirname,'../public/index.html')
    }),
    new vueLoaderPlugin(),
    new MiniCssExtractPlugin({
    filename: devMode ? '[name].css' : '[name].[hash].css',
    chunkFilename: devMode ? '[id].css' : '[id].[hash].css'
    })
    ]
    }
    +
    // webpack.dev.js
    const Webpack = require('webpack')
    const webpackConfig = require('./webpack.config.js')
    const WebpackMerge = require('webpack-merge')

    module.exports = WebpackMerge(webpackConfig,{
    mode:'development',
    devtool:'cheap-module-eval-source-map',
    devServer:{
    port:3000,
    hot:true,
    contentBase:'../dist'
    },
    plugins:[
    new Webpack.HotModuleReplacementPlugin()
    ]
    })
    +
    // webpack.prod.js
    const path = require('path')
    const webpackConfig = require('./webpack.config.js')
    const WebpackMerge = require('webpack-merge')

    const CopyWebpackPlugin = require('copy-webpack-plugin')
    const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
    const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

    module.exports = WebpackMerge(webpackConfig,{
    mode:'production',
    devtool:'cheap-module-source-map',
    plugins:[
    new CopyWebpackPlugin([{
    from:path.resolve(__dirname,'../public'),
    to:path.resolve(__dirname,'../dist')
    }]),
    ],
    optimization:{
    minimizer:[
    new UglifyJsPlugin({//压缩js
    cache:true,
    parallel:true,
    sourceMap:true
    }),
    new OptimizeCssAssetsPlugin({})
    ],
    splitChunks:{
    chunks:'all',
    cacheGroups:{
    libs: {
    name: "chunk-libs",
    test: /[\\/]node_modules[\\/]/,
    priority: 10,
    chunks: "initial" // 只打包初始时依赖的第三方
    }
    }
    }
    }
    })
    + +

    优化webpack配置

    看到这里你或许有些累了,但是要想获取更好的offer,更高的薪水,下面必须继续深入

    +

    优化配置对我们来说非常有实际意义,这实际关系到你打包出来文件的大小,打包的速度等。
    具体优化可以分为以下几点:

    +

    优化打包速度

    +

    构建速度指的是我们每次修改代码后热更新的速度以及发布前打包文件的速度。

    +
    +

    合理的配置 mode 参数与 devtool 参数

    devtool 可设置的值
    mode 可设置 development production 两个参数

    +

    如果没有设置, webpack4 会将 mode 的默认值设置为 production

    +
      +
    • production :将 process.env.NODE_ENV 的值设置为 production ,启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPluginUglifyJsPlugin,会进行 tree shaking (去除无用代码)和 uglifyjs (代码压缩混淆)
    • +
    • development :将 process.env.NODE_ENV 的值设置为 development ,启用 NamedChunksPluginNamedModulesPlugin
    • +
    +

    缩小文件的搜索范围(配置include exclude alias noParse extensions)

      +
    • alias 当我们代码中出现 import ‘vue’时, webpack会采用向上递归搜索的方式去node_modules 目录下找。为了减少搜索范围我们可以直接告诉webpack去哪个路径下查找。也就是别名(alias)的配置。
    • +
    • include exclude 同样配置include exclude也可以减少webpack loader的搜索转换时间。
    • +
    • noParse 当我们代码中使用到 import jq from 'jquery' 时, webpack 会去解析 jq 这个库是否有依赖其他的包。但是我们对类似 jquery 这类依赖库,一般会认为不会引用其他的包(特殊除外,自行判断)。增加 noParse 属性,告诉 webpack 不必解析,以此增加打包速度。
    • +
    • extensions webpack 会根据 extensions 定义的后缀查找文件(频率较高的文件类型优先写在前面)
    • +
    +

    配图

    +

    使用HappyPack开启多进程Loader转换

    +

    webpack 构建过程中,实际上耗费时间大多数用在 loader 解析转换以及代码的压缩中。日常开发中我们需要使用 Loaderjscss ,图片,字体等文件做转换操作,并且转换的文件数据量也是非常大。由于js单线程的特性使得这些转换操作不能并发处理文件,而是需要一个个文件进行处理。 HappyPack 的基本原理是将这部分任务分解到多个子进程中去并行处理,子进程处理完成后把结果发送到主进程中,从而减少总的构建时间

    +
    +
    npm i -D happypack
    + +

    happypack

    +

    使用 webpack-parallel-uglify-plugin 增强代码压缩

    上面对于 loader 转换已经做优化,那么下面还有另一个难点就是优化代码的压缩时间。

    +
    npm i -D webpack-parallel-uglify-plugin
    +

    webpack-parallel-uglify-plugin

    +

    抽离第三方模块

    +

    对于开发项目中不经常会变更的静态依赖文件。类似于我们的 elementUivue 全家桶等等。因为很少会变更,所以我们不希望这些依赖要被集成到每一次的构建逻辑中去。 这样做的好处是每次更改我本地代码的文件的时候, webpack 只需要打包我项目本身的文件代码,而不会再去编译第三方库。以后只要我们不升级第三方包的时候,那么 webpack 就不会对这些库去打包,这样可以快速的提高打包的速度。

    +
    +

    这里我们使用 webpack 内置的 DllPlugin DllReferencePlugin 进行抽离

    +

    在与 webpack 配置文件同级目录下新建 webpack.dll.config.js
    代码如下

    +
    // webpack.dll.config.js
    const path = require("path");
    const webpack = require("webpack");
    module.exports = {
    // 你想要打包的模块的数组
    entry: {
    vendor: ['vue','element-ui']
    },
    output: {
    path: path.resolve(__dirname, 'static/js'), // 打包后文件输出的位置
    filename: '[name].dll.js',
    library: '[name]_library'
    // 这里需要和webpack.DllPlugin中的`name: '[name]_library',`保持一致。
    },
    plugins: [
    new webpack.DllPlugin({
    path: path.resolve(__dirname, '[name]-manifest.json'),
    name: '[name]_library',
    context: __dirname
    })
    ]
    };
    +

    package.json 中配置如下命令

    +
    "dll": "webpack --config build/webpack.dll.config.js"
    + +

    接下来在我们的 webpack.config.js 中增加以下代码

    +
    module.exports = {
    plugins: [
    new webpack.DllReferencePlugin({
    context: __dirname,
    manifest: require('./vendor-manifest.json')
    }),
    new CopyWebpackPlugin([ // 拷贝生成的文件到dist目录 这样每次不必手动去cv
    {from: 'static', to:'static'}
    ]),
    ]
    };
    +

    执行

    +
    npm run dll
    + +

    会发现生成了我们需要的集合第三地方
    代码的 vendor.dll.js
    我们需要在html文件中手动引入这个js文件

    +
    <!DOCTYPE html>
    <html lang="en">
    <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">
    <title>老yuan</title>
    <script src="static/js/vendor.dll.js"></script>
    </head>
    <body>
    <div id="app"></div>
    </body>
    </html>
    +

    这样如果我们没有更新第三方依赖包,就不必 npm run dll 。直接执行 npm run dev npm run build 的时候会发现我们的打包速度明显有所提升。因为我们已经通过 dllPlugin 将第三方依赖包抽离出来了。

    +

    配置缓存

    +

    我们每次执行构建都会把所有的文件都重复编译一遍,这样的重复工作是否可以被缓存下来呢,答案是可以的,目前大部分 loader 都提供了 cache 配置项。比如在 babel-loader 中,可以通过设置cacheDirectory 来开启缓存,babel-loader?cacheDirectory=true 就会将每次的编译结果写进硬盘文件(默认是在项目根目录下的 node_modules/.cache/babel-loader 目录内,当然你也可以自定义)
    但如果 loader 不支持缓存呢?我们也有方法,我们可以通过 cache-loader ,它所做的事情很简单,就是 babel-loader 开启 cache 后做的事情,将 loader 的编译结果写入硬盘缓存。再次构建会先比较一下,如果文件较之前的没有发生变化则会直接使用缓存。使用方法如官方 demo 所示,在一些性能开销较大的 loader 之前添加此 loader 即可

    +
    +
    npm i -D cache-loader
    +

    cache-loader

    +

    优化打包文件体积

    打包的速度我们是进行了优化,但是打包后的文件体积却是十分大,造成了页面加载缓慢,浪费流量等,接下来让我们从文件体积上继续优化

    +

    引入webpack-bundle-analyzer分析打包后的文件

    webpack-bundle-analyzer 将打包后的内容束展示为方便交互的直观树状图,让我们知道我们所构建包中真正引入的内容

    +
    npm i -D webpack-bundle-analyzer
    +

    webpack-bundle-analyzer

    +

    接下来在 package.json 里配置启动命令

    +
    "analyz": "NODE_ENV=production npm_config_report=true npm run build"
    +

    windows 请安装 npm i -D cross-env

    +
    "analyz": "cross-env NODE_ENV=production npm_config_report=true npm run build"
    +

    接下来 npm run analyz 浏览器会自动打开文件依赖图的网页

    +

    externals

    +

    按照官方文档的解释,如果我们想引用一个库,但是又不想让 webpack 打包,并且又不影响我们在程序中以 CMDAMD 或者 window/global 全局等方式进行使用,那就可以通过配置 Externals 。这个功能主要是用在创建一个库的时候用的,但是也可以在我们项目开发中充分使用Externals 的方式,我们将这些不需要打包的静态资源从构建逻辑中剔除出去,而使用 CDN的方式,去引用它们。
    有时我们希望我们通过 script 引入的库,如用 CDN 的方式引入的 jquery ,我们在使用时,依旧用 require 的方式来使用,但是却不希望 webpack 将它又编译进文件中。这里官网案例已经足够清晰明了,大家有兴趣可以点击了解
    webpack
    官网案例如下

    +
    +
    <script
    src="https://code.jquery.com/jquery-3.1.0.js"
    integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
    crossorigin="anonymous">
    </script>
    module.exports = {
    //...
    externals: {
    jquery: 'jQuery'
    }
    };
    import $ from 'jquery';
    $('.my-element').animate(/* ... */);
    +

    Tree-shaking

    +

    这里单独提一下 tree-shaking ,是因为这里有个坑。 tree-shaking 的主要作用是用来清除代码中无用的部分。目前在 webpack4 我们设置 modeproduction 的时候已经自动开启了tree-shaking 。但是要想使其生效,生成的代码必须是ES6模块。不能使用其它类型的模块如 CommonJS 之流。如果使用 Babel 的话,这里有一个小问题,因为 Babel 的预案( preset )默认会将任何模块类型都转译成 CommonJS 类型。修正这个问题也很简单,在 .babelrc 文件或在 webpack.config.js 文件中设置 modules:false 就好了

    +
    +
    // .babelrc
    {
    "presets": [
    ["@babel/preset-env",
    {
    "modules": false
    }
    ]
    ]
    }
    +

    或者

    +
    // webpack.config.js

    module: {
    rules: [
    {
    test: /\.js$/,
    use: {
    loader: 'babel-loader',
    options: {
    presets: ['@babel/preset-env', { modules: false }]
    }
    },
    exclude: /(node_modules)/
    }
    ]
    }
    + +

    经历过上面两个系列的洗礼,到现在我们成为了一名合格的 webpack 配置工程师。但是光拧螺丝,自身的可替代性还是很高,下面我们将深入 webpack 的原理中去

    +

    手写webpack系列

    经历过上面两个部分,我们已经可以熟练的运用相关的 loaderplugin 对我们的代码进行转换、解析。接下来我们自己手动实现 loaderplugin ,使其在平时的开发中获得更多的乐趣。

    +

    手写 webpack loader

    +

    loader 从本质上来说其实就是一个 node 模块。相当于一台榨汁机( loader )将相关类型的文件代码( code )给它。根据我们设置的规则,经过它的一系列加工后还给我们加工好的果汁( code )。

    +
    +

    loader 编写原则

      +
    • 单一原则: 每个 Loader 只做一件事;
    • +
    • 链式调用: Webpack 会按顺序链式调用每个 Loader
    • +
    • 统一原则: 遵循 Webpack 制定的设计规则和结构,输入与输出均为字符串,各个 Loader 完全独立,即插即用;
    • +
    +

    在日常开发环境中,为了方便调试我们往往会加入许多 console 打印。但是我们不希望在生产环境中存在打印的值。那么这里我们自己实现一个 loader 去除代码中的 console

    +
    +

    知识点普及之 ASTAST 通俗的来说,假设我们有一个文件 a.js ,我们对 a.js 里面的1000行进行一些操作处理,比如为所有的 await 增加 try catch ,以及其他操作,但是 a.js 里面的代码本质上来说就是一堆字符串。那我们怎么办呢,那就是转换为带标记信息的对象(抽象语法树)我们方便进行增删改查。这个带标记的对象(抽象语法树)就是AST。这里推荐一篇不错的AST文章 AST快速入门

    +
    +
    npm i -D @babel/parser @babel/traverse @babel/generator @babel/types
    +
      +
    • @babel/parser 将源代码解析成 AST
    • +
    • @babel/traverse 对 AST 节点进行递归遍历,生成一个便于操作、转换的 path 对象
    • +
    • @babel/generator 将 AST 解码生成 js 代码
    • +
    • @babel/types 通过该模块对具体的 AST 节点进行进行增、删、改、查
    • +
    +

    新建 drop-console.js

    +
    const parser = require('@babel/parser')
    const traverse = require('@babel/traverse').default
    const generator = require('@babel/generator').default
    const t = require('@babel/types')
    module.exports=function(source){
    const ast = parser.parse(source,{ sourceType: 'module'})
    traverse(ast,{
    CallExpression(path){
    if(t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.object, {name: "console"})){
    path.remove()
    }
    }
    })
    const output = generator(ast, {}, source);
    return output.code
    }
    +

    如何使用

    +
    const path = require('path')
    module.exports = {
    mode:'development',
    entry:path.resolve(__dirname,'index.js'),
    output:{
    filename:'[name].[contenthash].js',
    path:path.resolve(__dirname,'dist')
    },
    module:{
    rules:[{
    test:/\.js$/,
    use:path.resolve(__dirname,'drop-console.js')
    }
    ]
    }
    }
    +
    +

    实际上在 webpack4 中已经集成了去除 console 功能,在 minimizer 中可配置 去除console
    附上官网 如何编写一个loader

    +
    +

    手写webpack plugin

    +

    Webpack 运行的生命周期中会广播出许多事件, Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
    通俗来说:一盘美味的 盐豆炒鸡蛋 需要经历烧油 炒制 调味到最后的装盘等过程,而 plugin 相当于可以监控每个环节并进行操作,比如可以写一个少放胡椒粉 plugin ,监控 webpack 暴露出的生命周期事件(调味),在调味的时候执行少放胡椒粉操作。
    那么它与 loader 的区别是什么呢?上面我们也提到了 loader 的单一原则, loader 只能一件事,比如说 less-loader ,只能解析 less 文件, plugin 则是针对整个流程执行广泛的任务。

    +
    +

    一个基本的 plugin 插件结构如下

    +
    class firstPlugin {
    constructor (options) {
    console.log('firstPlugin options', options)
    }
    apply (compiler) {
    compiler.plugin('done', compilation => {
    console.log('firstPlugin')
    ))
    }
    }

    module.exports = firstPlugin
    +
    +

    compilercompilation 是什么?

    +
    +
      +
    • compiler 对象包含了 Webpack 环境所有的的配置信息。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 optionsloaderplugin 。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。
    • +
    • compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation ,从而生成一组新的编译资源。 compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。
    • +
    +

    compilercompilation 的区别在于

    +
      +
    • compiler 代表了整个 webpack 从启动到关闭的生命周期,而 compilation 只是代表了一次新的编译过程
    • +
    • compilercompilation 暴露出许多钩子,我们可以根据实际需求的场景进行自定义处理
    • +
    +

    compiler钩子文档
    compilation钩子文档

    +

    下面我们手动开发一个简单的需求,在生成打包文件之前自动生成一个关于打包出文件的大小信息
    新建一个 webpack-firstPlugin.js

    +
    class firstPlugin{
    constructor(options){
    this.options = options
    }
    apply(compiler){
    compiler.plugin('emit',(compilation,callback)=>{
    let str = ''
    for (let filename in compilation.assets){
    str += `文件:${filename} 大小${compilation.assets[filename]['size']()}\n`
    }
    // 通过compilation.assets可以获取打包后静态资源信息,同样也可以写入资源
    compilation.assets['fileSize.md'] = {
    source:function(){
    return str
    },
    size:function(){
    return str.length
    }
    }
    callback()
    })
    }
    }
    module.exports = firstPlugin
    +

    如何使用

    +
    const path = require('path')
    const firstPlugin = require('webpack-firstPlugin.js')
    module.exports = {
    // 省略其他代码
    plugins:[
    new firstPlugin()
    ]
    }
    +

    执行 npm run build 即可看到在 dist 文件夹中生成了一个包含打包文件信息的 fileSize.md

    +
    +

    上面两个 loaderplugin 案例只是一个引导,实际开发需求中的 loaderplugin 要考虑的方面很多,建议大家自己多动手尝试一下。

    +
    +

    附上官网 如何编写一个plugin

    +

    webpack5.0的时代

    无论是前端框架还是构建工具的更新速度远远超乎了我们的想象,前几年的 jquery 一把梭的时代一去不复返。我们要拥抱的是不断更新迭代的 vuereactnodeserverlessdockerk8s ….
    不甘落后的 webpack 也已经在近日发布了 webpack 5.0.0 beta 10 版本。在之前作者也曾提过 webpack5.0 旨在减少配置的复杂度,使其更容易上手( webpack4 的时候也说了这句话),以及一些性能上的提升

    +
      +
    • 使用持久化缓存提高构建性能;
    • +
    • 使用更好的算法和默认值改进长期缓存(long-term caching);
    • +
    • 清理内部结构而不引入任何破坏性的变化;
    • +
    • 引入一些breaking changes,以便尽可能长的使用v5版本。
    • +
    +

    目前来看,维护者的更新很频繁,相信用不了多久webpack5.0将会拥抱大众。感兴趣的同学可以先安装beta版本尝尝鲜。不过在此之前建议大家先对webpack4进行一番掌握,这样后面的路才会越来越好走。

    +

    更多阅读

    webpack中文
    webpackjs
    4W字长文带你深度解锁Webpack系列(上)

    +]]>
    + + 前端工程化 + Webpack + + + Webpack + +
    + + 微信的H5兼容方案 + /2019/12/31/weichat-h5-compatibility/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    最近被公司新来的前端问到了一些微信页面的兼容性问题,因为我做微信开发的经验比较少,被问到了真是感到很尴尬,所以在搜到了解决方案告诉了他之后,我又深入的再网上找了一些微信开发中遇到的兼容问题,特此整理,以留后用。

    + + +

    1、ios端兼容input光标的高度

    bug描述:
    这个问题只出现在苹果手机上,在安卓手机上显示没有问题,可以说是非常诡异,简单描述一下就是在input输入框聚焦时,光标大小应该跟字号一直,但是在苹果手机上当点击输入的时候,光标的高度和父盒子的高度一样。
    分析:
    说来主要是习惯导致的问题,通常我们习惯将height和line-height设置成一样的值,这个时候input光标就会整个变得很大。
    解决:
    实际上解决方案也很简单,就是不设置行高,通过padding来控制输入内容与外框的距离。

    +
    // less代码
    .input-x{
    height:40px;
    // line-height:40px; // 此行注释掉
    .input-inline{
    padding: 10px 0;
    }
    }
    +

    这样做问题就解决了。

    +

    2、ios端微信h5页面上下滑动会卡顿,页面会有缺失

    bug描述:
    没错又是ios端,当页面高度超过一屏,那么上下滑动时就会出现页面卡顿的情况,而且时有伴随内容不能全部显示的情况。
    分析:
    这里实际上是浏览器内核解析不同导致的问题,在Andriod设备上,微信调用的是Webkit内核,而ios中是使用了Safari的内核,Safari对于滚动事件(overflow-scrolling)会使用原生的控件。而webkit内核则会创建一个UIScrollView来提供给子layer用以渲染。
    解决:
    在做样式重置时,加上下面这句话就能解决这个问题。

    +
    // css代码
    *{
    -webkit-overflow-scrolling: touch;
    }
    +

    但是这个方案也有缺陷,就是页面中不能有使用absolute定位的元素,不然布局就错乱了。
    延伸:
    -webkit-overflow-scrolling 属性控制元素在移动设备上是否使用滚动回弹效果.

    +
      +
    • auto: 使用普通滚动, 当手指从触摸屏上移开,滚动会立即停止。
    • +
    • touch: 使用具有回弹效果的滚动, 当手指从触摸屏上移开,内容会继续保持一段时间的滚动效果。继续滚动的速度和持续的时间和滚动手势的强烈程度成正比。同时也会创建一个新的堆栈上下文。
    • +
    +

    3、ios键盘唤起再收起,页面不会恢复原位

    bug描述:
    哎,对的还是ios,问题标题描述的比较清晰了,就是键盘弹出时,页面内容会整体上移,但是收起键盘时本应回归原位的不回去了。—_—|||
    分析:
    固定定位的元素,如果元素内input框聚焦的时候会弹出软键盘,软键盘会占用屏幕面积,失去焦点时软键盘消失,但是仍会占用,页面就会不能恢复原状,也就导致input框不能再次输入了。
    解决:
    在input失去焦点键盘收起时,写一个监听事件,事例代码如下:

    +
    // vue代码
    <input @blur="changeBlur()"/>

    // js代码
    changeBlur(){
    let ua = navigator.userAgent; // 获取用户代理
    let app = navigator.appVersion; // 获取客户端版本信息
    let isIos = ua.match(/i[^;]+;( U;)? CPU.+Mac OS X/); // 判断是否是Ios设备
    if(isIos){
    setTimeout(()=>{
    const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop || 0
    window.scrollTop(0,Math.max(scrollHeight - 1), 0)
    },100)
    }
    }
    +

    延伸:
    在iso的微信开发中,页面元素如果用到了position: fixed进行定位,那么键盘收起时,就会被顶上去,第三方输入法也不例外。

    +

    4、Android弹出键盘遮挡文本输入框

    bug描述:
    刚才说的问题都是Ios端的,实际上Android上也有挺多坑,上面讲到Ios上输入框弹出键盘的问题后,Android中实际也有,只是现象不同;Andriod中弹出键盘后页面不会向上滑动,但是如果输入框在底部的话会直接被挡住。。。
    分析:
    很坑,因为Andriod中输入框focus后,并不会向上滑动,如果靠下就会被挡住。。
    解决:
    实际上跟Ios上处理差不多的方案,代码如下:

    +
    // vue代码
    <input @blur="changeBlur()"/>

    // js代码
    changeFocus(){
    let ua = navigator.userAgent;
    let app = navigator.appVersion;
    let isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1;
    if(isAndroid){
    setTimeout(function() {
    document.activeElement.scrollIntoViewIfNeeded();
    document.activeElement.scrollIntoView();
    }, 500);
    }
    }
    +

    扩展
    Element.scrollIntoView()方法让当前的元素滚动到浏览器窗口的可视区域内。而Element.scrollIntoViewIfNeeded()方法也是用来将不在浏览器窗口的可见区域内的元素滚动到浏览器窗口的可见区域。但如果该元素已经在浏览器窗口的可见区域内,则不会发生滚动

    +

    5、Vue中路由使用hash模式,分享时Android可分享成功,Ios端分享失败

    bug描述:
    Ios的问题真的挺多的。。。

    +
      +
    • 在分享页面给A时,没问题,A把链接分享给B的时候就跳转到首页了;
    • +
    • 使用Vue-router跳转到第二个页面在分享时候,分享失败;
      以上两个问题在Android上均没有问题。
    • +
    +

    分析:
    jssdk是后端进行签署,前端校验,但是有时跨域,ios是分享以后会自动带上 from=singlemessage&isappinstalled=0 以及其他参数,分享朋友圈参数还不一样,貌似系统不一样参数也不一样,但是每次获取url并不能获取后面这些参数
    解决:

    +
      +
    • 可以使用改页面this.$router.push跳转,为window.location.href去跳转,而不使用路由跳转,这样可以使地址栏的地址与当前页的地址一样,可以分享成功
    • +
    • 把入口地址保存在本地,等需要获取签名的时候再取出来,注意:sessionStorage.setItem(‘href’,href); 只在刚进入单应用的时候保存!(还没测试,有点low)
    • +
    +

    写在最后

    虽然微信H5方式开发想对来说成本比较低,但是有时候坑开始挺多的,但是微信原生开发又增加了成本,很矛盾,目前能做的就是尽量把踩过的坑都记下来,下次别再跳进去了。

    +]]>
    + + 前端技术 + 微信 + + + 兼容性问题 + 微信 + +
    + + npx是什么 + /2020/04/16/what-is-npx/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    最近在阅读一些文章时,经常看到介绍命令的时候用到了 npx 关键字,之前知道有 node 有 npm 但是很少用到 npx ,npx是个啥呢?好奇上网上找了些资料学习了一下,写篇学习笔记记录一下。

    + +

    npx 起源

    我从阮一峰的博客中看到介绍 npx 的文章,开头的一句话说明了他诞生的日子。

    +
    +

    npm 从5.2版开始,增加了 npx 命令。

    +
    +

    为了验证阮一峰这里介绍的正确性我特意下了对应的npm版本验证了一下确实如此,而且在网上找到了另一位大佬司徒正美(大佬走好)博客中也对 npx 做了介绍

    +
    +

    最近我在更新 npm 5.2.0 的时候发现会买一送一,自动安装了 npx。

    +
    +

    由此,我可以肯定的告诉大家,npx是npm在5.2.0之后版本推出的一个工具,那么他是干嘛用的呢?

    +

    npx 作用

    想要了解一个技术,最好的途经是他的官网,于是我到网上找到了 npx 在 github 上的仓库,地址如下
    npx仓库,其中对 npx 有这样一段介绍

    +
    +

    DESCRIPTION
    Executes either from a local node_modules/.bin, or from a central cache, installing any packages needed in order for to run.
    By default, npx will check whether exists in $PATH, or in the local project binaries, and execute that. If is not found, it will be installed prior to execution.
    Unless a –package option is specified, npx will try to guess the name of the binary to invoke depending on the specifier provided. All package specifiers understood by npm may be used with npx, including git specifiers, remote tarballs, local directories, or scoped packages.
    If a full specifier is included, or if –package is used, npx will always use a freshly-installed, temporary version of the package. This can also be forced with the –ignore-existing flag.

    +
    +

    上面这一大段英文我想大家一定看了就头疼,所以为了大家不那么头疼,可以看一下下面我翻译的内容,如果有翻译不对的地方,还请指正。

    +
    +

    解释
    执行 command 命令,无论从本地(我理解为项目目录)node_modules/.bin 或者从全局缓存中, 安装所需执行的任何包。
    默认情况下,npx将检查 command 是否存在于 $PATH 中,或者在本地项目二进制文件中,并执行该命令。
    如果没有找到 command ,它将在执行之前安装。
    除非指定了 —package 选项,否则npx将根据提供的说明符猜测要调用的二进制文件的名称。
    npm可以理解的所有包说明符都可以与npx一起使用,包括git说明符、远程tarball、本地目录或作用域包。
    如果包含完整的说明符,或者使用 ——package 选项,npx将始终使用新安装的包的临时版本。
    这也可以用 ——ignore-existing 标记强制执行。

    +
    +

    上面这段机翻简直让人无法理解,所以我又去大佬博客下看了下他们的解释

    +
    +

    npx 想要解决的主要问题,就是调用项目内部安装的模块。 – 阮一峰
    根据 zkat/npx 的描述,npx 会帮你执行依赖包里的二进制文件。 – 司徒正美

    +
    +

    司徒大大文章写的太简洁了,不过他还是举了例子,我进行了一下精简,如下

    +
    ./node_modules/.bin/webpack -v // => npx webpack -v
    +

    简单来说就是找包执行命令的时候不再关注他在哪了,直接就可以用了。

    +

    阮一峰老师的文章更像是官网的翻译加理解

    +
    +

    npx 的原理很简单,就是运行的时候,会到node_modules/.bin路径和环境变量$PATH里面,检查命令是否存在。
    由于 npx 会检查环境变量$PATH,所以系统命令也可以调用。

    +
    +

    另外阮一峰老师还介绍了一个临时安装命令使用的场景,我理解为对上面英语介绍倒数第二句的理解

    +

    避免全局安装模块

    除了调用项目内部模块,npx 还能避免全局安装的模块。比如,create-react-app这个模块是全局安装,npx 可以运行它,而且不进行全局安装。

    +
    $ npx create-react-app my-react-app

    +

    上面代码运行时,npx 将create-react-app下载到一个临时目录,使用以后再删除。所以,以后再次执行上面的命令,会重新下载create-react-app。

    +

    下载全局模块时,npx 允许指定版本。

    +
    $ npx uglify-js@3.1.0 main.js -o ./dist/main.js

    +

    上面代码指定使用 3.1.0 版本的uglify-js压缩脚本。

    +

    注意,只要 npx 后面的模块无法在本地发现,就会下载同名模块。比如,本地没有安装http-server模块,下面的命令会自动下载该模块,在当前目录启动一个 Web 服务。

    +
    $ npx http-server
    + +

    然后阮老师还对官网最后一句话做了解释

    +

    –no-install 参数和–ignore-existing 参数

    如果想让 npx 强制使用本地模块,不下载远程模块,可以使用–no-install参数。如果本地不存在该模块,就会报错。

    +
    $ npx --no-install http-server

    +

    反过来,如果忽略本地的同名模块,强制安装使用远程模块,可以使用–ignore-existing参数。比如,本地已经全局安装了create-react-app,但还是想使用远程模块,就用这个参数。

    +
    $ npx --ignore-existing create-react-app my-react-app

    +

    然后对于官网上的 example 阮老师也挑了重点的做了介绍,如选择指定的 node 版本

    +
    $ npx node@0.12.8 -v
    v0.12.8
    +

    上面命令会使用 0.12.8 版本的 Node 执行脚本。原理是从 npm 下载这个版本的 node,使用后再删掉。
    某些场景下,这个方法用来切换 Node 版本,要比 nvm 那样的版本管理器方便一些。

    +

    更多参数

    -p 参数

    -p参数用于指定 npx 所要安装的模块,所以上一节的命令可以写成下面这样。

    +
    $ npx -p node@0.12.8 node -v 
    v0.12.8
    +

    上面命令先指定安装node@0.12.8,然后再执行node -v命令。
    -p参数对于需要安装多个模块的场景很有用。

    +
    $ npx -p lolcatjs -p cowsay [command]
    +

    -c 参数

    如果 npx 安装多个模块,默认情况下,所执行的命令之中,只有第一个可执行项会使用 npx 安装的模块,后面的可执行项还是会交给 Shell 解释。

    +
    $ npx -p lolcatjs -p cowsay 'cowsay hello | lolcatjs'
    # 报错
    +

    上面代码中,cowsay hello | lolcatjs 执行时会报错,原因是第一项 cowsay 由 npx 解释,而第二项命令localcatjs由 Shell 解释,但是lolcatjs并没有全局安装,所以报错。

    +

    -c参数可以将所有命令都用 npx 解释。有了它,下面代码就可以正常执行了

    +
    $ npx -p lolcatjs -p cowsay -c 'cowsay hello | lolcatjs'

    +

    -c参数的另一个作用,是将环境变量带入所要执行的命令。举例来说,npm 提供当前项目的一些环境变量,可以用下面的命令查看。

    +
    $ npm run env | grep npm_

    +

    -c参数可以把这些 npm 的环境变量带入 npx 命令。

    +
    $ npx -c 'echo "$npm_package_name"'

    +

    上面代码会输出当前项目的项目名。

    +

    执行 GitHub 源码

    npx 还可以执行 GitHub 上面的模块源码。

    +
    # 执行 Gist 代码
    $ npx https://gist.github.com/zkat/4bc19503fe9e9309e2bfaa2c58074d32

    # 执行仓库代码
    $ npx github:piuccio/cowsay hello
    +

    注意,远程代码必须是一个模块,即必须包含package.json和入口脚本。

    +

    最后

    因为还没有实际使用过的经验,所以更多的内容从其他大佬哪里白嫖来的知识,做个笔记以观后效。

    +]]>
    + + NodeJS + + + npx + +
    + + 为什么 0.1 + 0.2 != 0.3 + /2020/01/03/why-0.1-plus-0.2-not-equals-0.3/ + +

    作者:李旭光
    引用请标明出处

    + +

    Proxy

    因为 JS 采用 IEEE 754 双精度版本(64位),并且只要采用 IEEE 754 的语言都有该问题。

    +

    我们都知道计算机表示十进制是采用二进制表示的,所以 0.1 在二进制表示为

    +
    // js代码

    // (0011) 表示循环
    0.1 = 2^-4 * 1.10011(0011)
    +

    那么如何得到这个二进制的呢,我们可以来演算下

    +

    小数算二进制和整数不同。乘法计算时,只计算小数位,整数位用作每一位的二进制,并且得到的第一位为最高位。所以我们得出 0.1 = 2^-4 * 1.10011(0011),那么 0.2 的演算也基本如上所示,只需要去掉第一步乘法,所以得出 0.2 = 2^-3 * 1.10011(0011)

    +

    回来继续说 IEEE 754 双精度。六十四位中符号位占一位,整数位占十一位,其余五十二位都为小数位。因为 0.10.2 都是无限循环的二进制了,所以在小数位末尾处需要判断是否进位(就和十进制的四舍五入一样)。

    +

    所以 2^-4 * 1.10011...001 进位后就变成了 2^-4 * 1.10011(0011 * 12次)010 。那么把这两个二进制加起来会得出 2^-2 * 1.0011(0011 * 11次)0100 , 这个值算成十进制就是 0.30000000000000004

    +

    下面说一下原生解决办法,如下代码所示

    +
    // js代码

    parseFloat((0.1 + 0.2).toFixed(10))
    ]]>
    + + 前端技术 + + + 面试 + 知识点 + +
    + + 巧妙利用Acitons进行博客的自动构建 + /2019/12/27/Actions/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    忙活了很久很久,终于不那么忙了,也终于想起来我还有个博客没有运行,哎,虽然用Hexo可以享受Github提供的免费的空间和域名,但是命令行敲代码的方式实现博客编写还是有些不方便,尤其是需要编程环境的时候,最近公司在用CICD的方式对代码进行构建,我就在想有没有一种方式让博客也可以变成这样,于是我就在网上搜Github Hexo 自动构建 这些个词,还真让我搜到了一种方法而且还是Github自己提供的,下面隆重请出 GitHub Actions

    + + +

    GitHub Actions 是什么

    GitHub Actions 由 GitHub 官方推出的工作流工具。典型的应用场景应该是 CI/CD,类似 Travis 的用法。如果不知道 CI/CD&Travis 感兴趣的建议去了解一下,下面不展开说明,直接说怎么用就好。

    +

    前期准备

    在使用 GitHub Actions 之前我们先来看看我们有什么;
    首先我们有一个放博客程序的地方,我这里是叫做 blog-source ,另外呢有一个通过 hexo g 创建出来的静态网站,为了存放它而建的另一个仓库,我这里是叫做lixuguang.github.io,也就是说我们现在是有这样两个仓库。
    |仓库|作用|
    |-|-|
    |blog-source|放博客源代码|
    |lixuguang.github.io|放博客生成代码|

    +

    生成密钥

    因为 GitHub Actions 它需要访问我的 blog-source 仓库的代码所以必须要有密钥,密钥大家应该熟悉了,创建博客的时候也是创建了一个公钥和私钥用来在本地往 lixuguang.github.io 这个仓库提交代码
    这里呢我们用下面的命令生成密钥。

    +
    ssh-keygen -t ed25519 -f ~/.ssh/github-actions-deploy # 连按三次回车即刻
    +

    命令执行完成后,我们会得到两个文件 github-actions-deploygithub-actions-deploy.pub 两个文件,第一个是私钥,第二个是公钥。
    |名称|解释|
    |-|-|
    |github-actions-deploy|私钥|
    |github-actions-deploy.pub|公钥|

    +

    接下来的步骤一定要好好看,因为我在这个地方被卡住好多次,就是因为有的文章说的并不正确,或者至少是讲的不够仔细,这里我会仔细地说明一下。

    +

    配置 GitHub 仓库

    配置博客源代码仓库

    我这里的源代码是放在 blog-source 中,所以我现在要给源代码仓库配置私钥,配置过程如下:
    打开 blog-source 仓库,选择 settings,然后选中 secrets , 再点击 Add new secrets,照着下面填写内容
    |字段|值|
    |-|-|
    |Name|HEXO_DEPLOY_PRI(名称自动构建时有用)|
    |Value|github-actions-deploy|

    +

    配置博客源代码仓库

    我这里生成的博客静态代码是放在 lixuguang.github.io 中,所以我现在要给静态代码仓库配置公钥,配置过程如下:
    打开 lixuguang.github.io,选择 settings,然后选中 keys,再点击 Add deploy key,照着下面填写内容
    |字段|值|
    |-|-|
    |Title|HEXO_DEPLOY_PUB|
    |Key|github-actions-deploy.pub|

    +

    编写 Actions 脚本

    经过上面一系列的准备操作,终于来到了编写自动构建脚本的环节,构建脚本如下,如果按照上面我做的操作一步步来的话,那么这一步你可以直接copy啦

    +
    name: Deploy Blog

    on: [push] # 当有新push时运行

    jobs:
    build: # 一项叫做build的任务

    runs-on: ubuntu-latest # 在最新版的Ubuntu系统下运行

    steps:
    - name: Checkout # 将仓库内master分支的内容下载到工作目录
    uses: actions/checkout@v1 # 脚本来自 https://github.com/actions/checkout

    - name: Use Node.js 10.x # 配置Node环境
    uses: actions/setup-node@v1 # 配置脚本来自 https://github.com/actions/setup-node
    with:
    node-version: "10.x"

    - name: Setup Hexo env
    env:
    HEXO_DEPLOY_PRI: ${{ secrets.HEXO_DEPLOY_PRI }} # 这里是上面配置的私钥名称
    run: |
    # set up private key for deploy
    mkdir -p ~/.ssh/
    echo "$HEXO_DEPLOY_PRI" | tr -d '\r' > ~/.ssh/id_rsa # 配置秘钥
    chmod 600 ~/.ssh/id_rsa
    ssh-keyscan github.com >> ~/.ssh/known_hosts
    # set git infomation
    git config --global user.name 'lixuguang' # 换成你自己的名字
    git config --global user.email 'lixuguang@gmail.com' # 换成你自己的邮箱
    # install dependencies
    npm i -g hexo-cli # 安装hexo
    npm i

    - name: Deploy
    run: |
    # publish
    rm -rf .deploy_git # 如果上次构建失败这句命令会清除上次失败的代码
    hexo generate && hexo deploy # 执行部署程序


    + +
    +

    通过以上这些步骤的操作,如果没什么意外的话,博客的自动构建就完成了,之后只要你提交新的文章到博客源代码仓库,它将自动帮你生成并发送到博客的静态代码仓库,再也不用执行hexo g -d啦,如果这篇文章对你有用,欢迎follow我或打赏一下这篇文章,感谢阅读。

    +

    ps:这里有个小坑需要注意一下,因为博客的皮肤也是另外一个git仓库,如果你在本地构建好用但是线上构建博客不显示了,需要注意下是不是皮肤没有上传到博客源码仓库,这里我遇到了,希望你不会因此困扰,拜拜~

    +]]>
    + + 杂七杂八 + 博客技巧 + + + Hexo + Github + Actions + +
    + + 从零开始:学习Cordova(一) + /2020/11/10/learn-cordova-1/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    很长时间没有写博客了,因为最近换了工作,原来的技术栈基本上都换了,现在接触了很多新的技术栈,Cordova 就是其中之一。

    +

    接下来这篇文章就是我的学习笔记,以供回顾,写的不好请勿拍砖。

    + +

    一、What is Cordova? Cordova是个啥?

    Logo
    移动端开发和 Web 开发有一些不一样,Web 端开发面向的是浏览器,而移动端开发面向的是各种移动设备,那么针对各种移动设备提供的 SDK 进行开发的话,我们通常称之为***原生开发***,原生开发虽然通常调用底层 Api ,性能更好,但是因为不同移动终端底层技术不一样,所以每种设备都要单独开发,这样开发成本和能力要求都比较高,所以通常都会有一些框架来兼容各种平台,就好比 Java 虚拟机可以运行在 Mac/Windows/Linux 一样,为了能够写一套移动端代码可以在各种设备中使用,于是乎就有了 Cordova

    +

    上面说了这么一堆废话,其实简言之,***Cordova 就是提供了跨设备调用底层Api的移动端开发框架(这种开发方式又叫做Hybird 模式)***

    +

    知道这一点应该就够了,当然这是我认为的。

    +

    Cordova 的官网地址如下

    +
    +

    https://cordova.apache.org/

    +
    +

    其实从这里就可以看出, CordovaApache 基金下的一个项目,其官网是这样介绍它的

    +
    +

    Mobile apps with HTML, CSS & JS
    Target multiple platforms with one code base
    Free and open source

    +
    +

    翻译成中文的话就是

    +
    +

    使用 HtmlCSSJS 进行移动端应用开发
    一份代码多平台使用
    免费并且开源

    +
    +

    三句话解释了 Cordova 的三个方面,

    +
      +
    • 使用前端技术开发-对前端开发友好
    • +
    • 跨平台-节省开发工时,降低开发难度-从图标看支持 Android/IOS/WP三大平台
    • +
    • 免费不用解释开源的话意味着会有很多人贡献代码
    • +
    +

    以上奠定了 Cordova 强大及流行的基础。

    +

    二、快速上手

    在使用 Cordova 做移动端开发之前需要做一些环境准备工作,首先要安装 Node 以及它携带的 Npm 包管理器,这个现代的前端开发应该都知道,我这里就不多说了,装它就完了。

    +

    装好之后我们就要用 Npm 全局安装一下 Cordova 项目的依赖,命令很简单;

    +

    方便的话还要装一下 Git ,因为一些命令要在 git-cmd 上执行,当然管理代码也是需要它的,所以装吧。

    +
    npm install -g cordova
    +

    就这么简单就可以开发 Cordova 应用了,接下来我们要创建一个项目,创建项目的命令也十分简单。在命令行工具中输入下面的命令你就创建了一个 Cordova 的应用

    +
    cordova create <path> // 这里的path是项目的路径
    +

    如果要查看创建项目时还能设置什么参数你可以执行下面的命令

    +
    cordova help create
    +

    怎么样是不是超级简单。
    做了上面的工作你就创建了一个具备基本功能的 Cordova 项目,接下来我们要给项目配置一些东西。

    +

    首先刚才讲到了, Cordova 是一个跨平台的框架,那么你的项目要适配什么平台你需要进行一些配置, cd 到项目目录下执行下面的命令

    +
    cordova platform add <platform name> // platform name 为适配平台的名称
    +

    如果不知道名称是什么命令行工具也贴心的给了查询用的命令,通过此命令你可以看到你安装了什么平台的适配,有哪些可以用

    +
    cordova platform

    +

    执行了这个命令你将看到下面这样的 list

    +
    /***
    Installed platforms:
    Available platforms:
    android ^9.0.0
    browser ^6.0.0
    electron ^1.0.0
    windows ^7.0.0
    */
    +

    项目也创建了,简单的配置工作也完成了,我们可以启动项目看一下效果了,那么我们就该执行运行项目的命令了

    +
    cordova run <platform name> // platform name 适配平台的名称
    +

    到这我们就快速的创建并运行起了一个 Cordova 的工程。

    +

    小Tip

    如上执行启动命令后,默认会启动8000端口,如果被占用的话可以通过增加命令行参数的形式改变端口号,参数如下设置

    +
    cordova run browser --target=chrome --port=9090
    +

    以上就可以自定义端口及浏览器启动应用了。

    +

    三、详细了解

    1、概览

    官网开头介绍了 Cordova 的身世和适用范围,前面开头已经讲过了不想再讲,直接看看 Cordova 的架构设计图。
    此处是个架构图

    +
    Cordova 架构图
    + +

    这个图理解起来不难,大的方面是两个部分,底层是 MobileOS ,也就是手机的操作系统本身,上层是由 Cordova 构建的框架;

    +

    Cordova 内部的话是两层结构:

    +
      +
    • 上层的话是由前端代码构成的 WebApp 部分;
    • +
    • 下层是 Cordova 的视图渲染引擎,它为 WebApp 提供了 HtmlApiCordovaApi
    • +
    +

    也就是说我们可以调用 html 的原生 Api 也可以使用 Cordova 提供的 jsApi

    +

    Cordova 还提供了丰富的插件系统;通过 Cordova 的视图引擎调用 CordovaNativeApi 来调用一些 Cordova 提供好的调用设备的信息的 Api ,另外也可以调用一些用户自定义的 Api

    +

    Cordova 视图引擎和插件系统再将上层的需求通过 MobileOS 开放的 Api 能力实现具体功能,由此完成了整个 Cordova 框架的使用;

    +

    总的来说这个原理跟大多数跨平台的框架原理都类似,由框架提供统一的 Api 能力,再由框架处理不同平台的兼容,这里特别之处在于渲染引擎可以允许使用前端原生的 Api ,这将大大降低开发的难度。

    +

    WebView

    说到移动端开发,不知道 WebView 应该是不可能的,在原生移动应用中,WebView 就是移动应用内部嵌入的一个‘浏览器’,它可以允许你使用前端技术展示内容。

    +

    WebApp

    WebAppWebView 两个词有些像,事实上它们也确实有些关系,WebApp 从名字就可以看出是使用 Web 技术开发的 App ,那么它跟 WebView 是什么关系呢,打个比方,WebView 就是个快递盒子, WebApp 就是你买的商品,一个是容器,一个是内容;这样说应该就明白了吧,通过前端技术开发的 App 通过 WebView 嵌入到 App 中,就可以带给用户原生应用的体验,忘了说 WebApp 是只能用浏览器访问的 App

    +

    和纯 WebApp 不同,嵌入到 WebView 中的 WebApp 需要有一个配置文件,***config.xml*** ,它在项目的根目录下,说明了应用的一些信息,这个文件必不可少!!!

    +

    插件系统

    刚刚在上面的架构图中我们介绍了 Cordova 的插件系统,它提供了我们通过 js 调用 MobileApi 的能力,如电源/相机/联系人等等;
    官方维护了核心的功能,当然如果你愿意也可以调用一些第三方提供的插件,你可以方便的通过 npm 包管理器安装;

    +

    但是有一点必须要说:项目创建后默认是不带任何插件的,即使是官方的核心插件也是需要你自己导入进去,第三方的更不用说了,另外Cordova 只提供了功能 Api 并不包含任何的 UI 部件和 MV 框架(你可以根据自己喜欢使用 Angular 或者 Vue 或者其他什么都可以),这一点要牢记。

    +

    开发工作流

    使用 Cordova 开发 MobileApp 的时候即可以开发多个平台的 App (一次开发多平台可用,降低开发成本),也可以专注开发某一平台的App (使用前端技术开发,降低开发难度),因此开发工作流也分为两种:

    +
    跨平台(CLI)的工作流

    官网上说了很多,也不难理解,这里用我自己的话说就是, Cordova 提供了一个牛叉的 CLI 工具,它可以自动的完成一次编码多平台构建的事,你只需要按照它说的方式开发就行。:)

    +
    平台为中心的工作流

    这里不得不说我没有看很明白,但是结合下面的注意事项我是这样理解的,因为只为某一平台服务,所以有一些针对此平台的特殊的功能或者插件你就可以使用了(虽然不太清楚为什么这么做,如果你知道告诉我,谢谢),但是一旦使用了这种开发方式,那么就回不到跨平台开发了,因为你使用了针对某一平台才能调用的代码,这也不难理解,从字里行间感觉就是不建议使用这种开发工作流(好的好的,知道了)。

    +

    好的,接下来开发 App 啦;

    +

    2、开发应用

    快速上手中已经介绍了如何准备、创建和启动应用,如何添加平台,这里就不多说了。说些没讲过的。

    +

    上面说过检查目前平台安装情况的话可以使用下面的命令

    +
    cordova platforms
    +

    我们也可以使用另一个命令来看,我试验了一下,效果是一样的。。。(问号脸,一样的为什么搞两个命令)

    +
    cordova platform ls
    +
    +

    事后我发现,无论我使用的命令是 platform 还是 platforms 加不加 ls 都可以查到当前安装的平台。。。 啊~兼容性好强。

    +
    +

    每执行一次 add 命令,你就会发现工程目录下的 platforms 目录下就对应生成了一个对应平台的文件夹,切勿手贱删除,你虽然删除了文件夹,但是各处设置的 platform 并没有去掉,会导致报错,如果要删除请使用 remove 命令。 想知道还有哪些命令的话就看下面的链接吧。⇒ 命令大全

    +

    安装构建的先决条件

    因为 Cordova 的底层还是要调用 MobileOS 的接口,所以各平台的 SDK 是必须要装的,当然这里指的是你要进行构建的平台,比如你要构建 Android 应用,那么你就要装 AndroidSDKIOS 同理。这里有一个例外broswer平台是不需要依赖其他 SDK 的(明明是开发 APP ,这里只是为了验证画面比较方便)
    要看你是否满足了构建需要的依赖,可以执行下面的命令查看

    +
    cordova requirements
    +

    执行完后你会得到如下结果

    +
    Requirements check results for android:
    Java JDK: installed .
    Android SDK: installed
    Android target: installed android-19,android-21,android-22,android-23,Google Inc.:Google Apis:19,Google Inc.:Google Apis (x86 System Image):19,Google Inc.:Google Apis:23
    Gradle: installed

    Requirements check results for ios:
    Apple OS X: not installed
    Cordova tooling for iOS requires Apple OS X
    Error: Some of requirements check failed
    +

    这样你就可以清楚的知道自己需要安装哪些依赖才能完成对应平台的构建,简直方便的不要不要的。
    具体想知道不同的平台都依赖些什么,你可以参见下面的链接;

    + +

    构建 App

    create 项目之后,项目的根目录下会生成一个 www 目录,这个目录包含了 webapp 的入口页面 index.html ,入口文件的话是 www/js/index.js 文件的 deviceready 事件中。

    +

    如果要构建代码的话,执行下面的命令

    +
    cordova build
    +

    这个命令会构建你安装的所有平台,如果只想构建某一平台的话,可以把平台的名字加在命令后面,如

    +
    cordova build ios
    +

    如果想了解更多的参数的话,可以看一下后面的链接。⇒ 更多参数

    +

    测试 App

    构建完 App 之后我们就可以测试了,通常各平台的 SDK 中会提供模拟器,我们只需要执行下面的命令就可以启动模拟器。

    +
    cordova emulate android
    +

    当然如果你觉得用实机查看更自然一些的话,也可以把手机插上数据线与电脑建立连接,然后执行下面的命令就可以了。

    +
    cordova run android
    +

    上面演示的是 AndroidApp 测试过程,每个平台虽然基本类似,但还是有出入,所以,下面提供了不同平台的调试方法,供你预览。

    + +

    添加插件

    如果只是开发一个 Cordova 框架的 WebApp ,那么你不需要装任何插件,直接用前端技术开发就好了,但是应该没人会这么做吧,毕竟开发这个的目的就是为了跨平台开发原生级移动 App ,因此要调用移动设备的各种功能就必须安装插件。

    +

    插件

    Cordova 插件是一些使用 Javascript 调用原生 SDK 功能的包;
    你可以使用两种方式找到你想要的插件

    +
      +
    1. 通过 Cordova 提供的包管理平台 ⇒ 插件搜索页
    2. +
    3. 通过命令行搜索 cordova plugin search camera
    4. +
    +

    选好你要安装的插件之后你需要执行下面的命令来安装它

    +
    cordova plugin add <plugin name> // 如 cordova plugin add cordova-plugin-chrome-apps-proxy
    +

    如果你的包没有发布在 Cordova 的平台上,也可以使用 git 地址来安装。
    另外 Cordova 还非常贴心的提供了一个工具 Plugman 来帮助开发者更好的管理 Cordova 插件。⇒ Plugman参考

    +

    如果要查看你安装了哪些插件可以使用下面任意一种命令都可以。(咱也不知道为啥提供这么多方式,反正好用无脑)

    +
    plugin ls
    plugin list
    plugin

    /**
    $ cordova plugin ls
    cordova-plugin-whitelist 1.2.1 "Whitelist"
    */
    +

    如果想知道更多关于 plugin 的命令参数,可以看右边的链接 ⇒ plugin参数

    +

    使用 merges 自定义每个平台

    虽然可以用一套代码来构建多个平台,但一些平台会有自己的特点,就好像 Chrome 浏览器默认的字体大小与 IE 的不同, AndroidIOS 也有不一样的地方,这种情况下去改 www 目录下的文件显然是不合适的,所以 Cordova 提供了 merges 方式来适配不同平台各自的特别处理,你要做的就是在项目的根目录下创建一个 merges 目录,比如你要适配 Android ,你就可以在 merges 下创建一个 android 目录,然后在下面或覆盖或添加新的资源。(这里官网没有说 merges 在哪里创建,试验后发现是在根目录下)

    +

    更新 Cordova 和你的项目

    如果你要更新你的 Cordova (通常不建议这么干,当然你知道后果并想要使用新的特性或者修正 bug 除外),那么你需要执行一下 update 命令

    +
    sudo npm update -g cordova
    +

    当然你也可以指定更新到什么版本(这都是 npm 的知识了)

    +
    sudo npm install -g cordova@3.1.0-0.2.0
    +

    你也可以使用下面的命令查看当前的版本(还是 npm 的知识)

    +
    npm info cordova version
    +

    你是不是以为这样就可以了,不好意思还不行,你还需要把各个平台进行一下升级

    +
    cordova platform update android --save
    cordova platform update ios --save
    // ...
    +

    其实这也好理解,你都已经轿车换 SUV 了,总不能还用原来的车轱辘吧,只是我希望能不能在换车的时候一块儿把车轱辘换了(这里的意思是希望直接自动化处理了)

    +

    到这里你的第一个 App 就已经开发好了,是不是还有点成就感。

    +

    一些参考资料

    到这里一个简单的基于 Cordova 搭建的 App 就实现了,当然一个 App 绝不这么简单,各平台的 SDK 安装也没那么简单,但这不是本篇文章要说的事了,下一篇文章我会说说如何应对各种不同的开发平台,在这之前还请你看一下 Cordova 都支持哪些平台,具体看一下下面的链接。

    +

    好了,今天就先写到这里,下篇文章我们再见。

    +

    平台支持

    +

    (待续…)

    +]]>
    + + 大前端 + 移动开发 + + + Cordova + +
    + + 从零开始:学习Ionic(一) + /2020/11/11/learn-ionic-1/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    在移动 App 开发领域,有一个框架非常知名,每每面试都会被问起,“你用过 Ionic 么?”,是的,就是 Ionic 框架。

    +

    之前已经学习过了 Cordova ,它和 Ionic 可以说是一对黄金搭档,一个面向 MobileOS ,一个面向 UI ,很多人搞不清楚它们之间的区别,没关系,回头我再抽时间讲一下,今天我们主要来认识一下 Ionic

    +

    好戏开始:)

    + +

    Ionic 简介

    学一门技术,我们首先要知道它是谁,能干啥,有啥好处。要了解一门新技术最好的方式就是去它的官网看看。
    (一言不合丢给你一个官网 ⇒ 官网

    +
    +

    One codebase.
    Any platform.
    Now in Vue.

    +
    +

    翻译过来是这样的

    +
    +

    一份代码
    任何平台
    现在支持Vue

    +
    +

    看着是不是眼熟,嗯,跟 Cordova 的口号何其相似,目标都是跨平台,只是 Cordova 跨的是 MobileOSIonic 跨的是前端框架。
    最下面一句 Now in Vue 像是在说,“嗯,我也支持Vue了(Vue发展太快了,不能掉队)”,要知道,最开始的 Ionic 是妥妥的 Angular 派,不过无所谓,管他黑猫白猫,能完成任务就是好猫。

    +

    再往下看你会看到,巴拉巴拉说了一堆好处,比如开发迅速啊,组件优美且丰富啊,社区强大啊,还有跨平台(支持原生 jsAngularVue ,nice~),另外还提供了 native 的组件来直接调用移动设备的功能,可以说是相当强大了。

    +

    好了,看完了介绍,我们赶紧快速上手吧。

    +

    快速上手

    在使用Ionic做开发还是要做一些准备工作,nodegit都是必不可少的,所以请先行准备好。

    +

    目前,Ionic提供了两种创建工程的方式:

    +
      +
    • 一种用官方的话说叫StepByStep,看过之后我理解就是通过命令的方式,一步步来;
    • +
    • 另一种的话是官方提供了一个向导工具,就跟安装软件一样填一些必要的信息,然后一路下一步就生成了一个工程;
    • +
    +

    接下来我将使用这两种方式分别创建工程,首先是第一种。

    +

    StepByStep 创建工程

      +
    1. 全局安装Ionic的脚手架工具
      npm install -g @ionic/cli
    2. +
    3. 创建工程
      ionic start myApp tabs // myApp 是项目目录名称也是工程名称 tabs是模板名称
    4. +
    5. 启动工程
      cd myApp
      ionic serve
    6. +
    +

    嗯,上面三个步骤之后,你的Ionic项目就启动起来了。(What !? 这么简单?! 嗯,是的。)

    +

    向导式(App Wizard)创建工程

    这是一个在线创建工程的手段(感觉对于开发人员来说貌似还是命令行来的方便,这种可视化方案应该是给小白用户准备的吧),地址如下

    +
    +

    https://ionicframework.com/start

    +
    +

    此处是个图

    +

    点开上面的地址,你会看到如图所示的一个界面,我们可以看到它要求你输入的一些信息

    +
      +
    1. App Name
    2. +
    3. Icon
    4. +
    5. Theme Color
    6. +
    7. Template
    8. +
    9. Framework
    10. +
    +

    可以看到这比命令行要多出几个信息,上面的命令行只要求了 App NameTemplate 难道强大的命令行不支持设置这些么?答案是否定的,当然可以设置这些信息,只是以参数的形式而已,先不说这个,填写好上面的信息以后我们点击[create App]按钮,这时我们进入了下一步,选择代码仓库(Choose a git host),你会看到下面有一个[Skip]按钮说明这步不是必须的,我们先选择一个git仓库,这里我选择github,其他的感兴趣的同学可以自己试试。点击[connect],这时会弹出一个窗口,申请对应git仓库的鉴权认证,认证通过后我们点击[choose],等待程序运行一会儿后工程就会被上传到git仓库中,并且会跳转到一个DashBoard,提供了可视化管理工程的页面(也太高大上了吧),右侧我们看到如何把代码下载到本地,并运行它,真的是很方便。

    +

    例:

    +
    npm install -g @ionic/cli cordova-res // cordova-res 指的是安装 cordova 的开发依赖,因为除 cordova 之外还可以选择如 phonegap 之类的MobileOS 框架
    git clone https://github.com/lixuguang/li-app.git li-app
    cd li-app && npm install && ionic serve
    + +

    执行完上面的命令以后,我们的工程也就运行起来了,也是十分方便,即使不会写代码的人,跟着指南也可以很方便的搭建一个Ionic工程。

    +

    深入学习

    Ionic 的体系当中,最重要的莫过于 Ionic CLI 和 UI Component 两个了,接下来我将分辨将这两部分中最重要的内容拿出来说说。

    +

    Ionic CLI

    在上面的快速上手中,我们已经使用了几个命令,但这些还不够,Ionic CLI 提供了强大的命令,请与我一起看一下

    +
    npm install -g @ionic/cli // 全局安装 Ionic CLI
    ionic start myApp tabs // 用模板创建工程
    ionic start --list // 查看可用模板
    ionic serve // 启动工程
    ionic serve --lab // 以android或ios方式打开
    ionic lib update // 更新包
    ionic serve --address 192.168.89.1 // 指定Ip,给外部用户访问
    ionic platform add ios/android // 添加平台
    ionic build ios/android // 构建平台
    ionic emulate ios [options] // ionic run ios [options] 模拟器运行

    ionic generate // 创建新特性


    options
    -l //livereload, 实时刷新变化。
    -c //打印app里的console
    -s //打印设备的console
    -p //指定设备的端口
    -i //指定livereload的重刷端口
    --debug //debug
    --release //release
    --host=0.0.0.0 // IP
    --port=8100 // 端口


    ionic resources [--splash] [--icon] // 上传代码到官方平台
    ionic upload // 登录
    ionic info // 查看系统信息
    ionic browser add crosswalk // 加壳预览(手机浏览器性能问题解决)
    ionic browser list // 查看可用的browser
    ionic browser revert android/ios // 删除安装的browser
    ionic state reset // 先删除平台和插件,再安装package.json文件中的平台和插件。重置
    ionic state save // 保存当前状态信息
    ionic state clear // 先删除平台和插件,然后按照package.json文件中包含的平台和插件重新安装。

    npm install @ionic/angular@latest --save // Ionic + Angular
    ng add @ionic/angular // 使用Angular CLI 增加Ionic的组件库,可用于Angular项目改造Ionic

    npm install @ionic/react // Ionic + React
    npm install @ionic/react-router // 在既存的react项目中引入ionic特性

    // 增加CSS文件为组件服务,加在根组件下
    /* Core CSS required for Ionic components to work properly */
    import '@ionic/react/css/core.css';

    /* Basic CSS for apps built with Ionic */
    import '@ionic/react/css/normalize.css';
    import '@ionic/react/css/structure.css';
    import '@ionic/react/css/typography.css';

    /* Optional CSS utils that can be commented out */
    import '@ionic/react/css/padding.css';
    import '@ionic/react/css/float-elements.css';
    import '@ionic/react/css/text-alignment.css';
    import '@ionic/react/css/text-transformation.css';
    import '@ionic/react/css/flex-utils.css';
    import '@ionic/react/css/display.css';

    npm install @ionic/vue @ionic/vue-router // Ionic + Vue ,在既存的Vue项目中引入ionic特性

    // main.js 文件中加入引入
    import { IonicVue } from '@ionic/vue';

    import App from './App.vue'
    import router from './router';

    const app = createApp(App)
    .use(IonicVue)
    .use(router);

    router.isReady().then(() => {
    app.mount('#app');
    });

    // router/index.js 用Ionic提供的vue-router替换原本的vue-router
    import { createRouter, createWebHistory } from '@ionic/vue-router';

    const routes = [
    // routes go here
    ]

    const router = createRouter({
    history: createWebHistory(process.env.BASE_URL),
    routes
    })

    export default router;

    // 增加CSS文件为组件服务 main.js
    /* Core CSS required for Ionic components to work properly */
    import '@ionic/vue/css/core.css';

    /* Basic CSS for apps built with Ionic */
    import '@ionic/vue/css/normalize.css';
    import '@ionic/vue/css/structure.css';
    import '@ionic/vue/css/typography.css';

    /* Optional CSS utils that can be commented out */
    import '@ionic/vue/css/padding.css';
    import '@ionic/vue/css/float-elements.css';
    import '@ionic/vue/css/text-alignment.css';
    import '@ionic/vue/css/text-transformation.css';
    import '@ionic/vue/css/flex-utils.css';
    import '@ionic/vue/css/display.css';

    From here, you can learn about how to develop with Ionic Framework in our [Ionic Vue Quickstart Guide](https://ionicframework.com/docs/vue/quickstart).

    ## Ionicons CDN

    Ionicons is packaged by default with the Ionic Framework, so no installation is necessary if you're using Ionic. To use Ionicons without Ionic Framework, place the following `<script>` near the end of your page, right before the closing `</body>` tag.


    <script type="module" src="https://cdn.jsdelivr.net/npm/ionicons@4.7.4/dist/ionicons/ionicons.esm.js"></script>
    <script nomodule src="https://cdn.jsdelivr.net/npm/ionicons@4.7.4/dist/ionicons/ionicons.js"></script>


    + + +

    (待续…)

    +]]>
    + + 大前端 + 移动开发 + + + Ionic + +
    + + CSS3新属性-pointer-events + /2020/05/14/css3-pointer-events/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    今天在看一个叫做Avue的框架组件时看到了一个叫做“全局水印”的组件,功能呢就是给当前页面加上一个水印遮罩,就像下图这样:
    Y08cbd.png
    因为工作中有这样的场景,所以我很好奇看了一下他的代码,他的水印设置为了fixed定位,并且z-index设置的很高,但是它具有穿透性,最终我发现其中起作用的是一个叫做“pointer-events”的css3新属性,于是为了搞清楚它的作用,我在网上做了一番搜索,并把自己的认识整理一下。

    + +

    介绍

    MDN上是这样介绍这个属性的 传送门

    +
    +

    pointer-events CSS 属性指定在什么情况下 (如果有) 某个特定的图形元素可以成为鼠标事件的 target。

    +
    +

    它有如下这些个属性值:

    +
    /* Keyword values */
    pointer-events: auto;
    pointer-events: none;
    pointer-events: visiblePainted; /* SVG only */
    pointer-events: visibleFill; /* SVG only */
    pointer-events: visibleStroke; /* SVG only */
    pointer-events: visible; /* SVG only */
    pointer-events: painted; /* SVG only */
    pointer-events: fill; /* SVG only */
    pointer-events: stroke; /* SVG only */
    pointer-events: all; /* SVG only */

    /* Global values */
    pointer-events: inherit;
    pointer-events: initial;
    pointer-events: unset;

    +

    这里有一个特别的值,它进行了专门的介绍,也正是因为这个原因,水印的功能才得以实现。

    +
    +

    除了指示该元素不是鼠标事件的目标之外,值none表示鼠标事件“穿透”该元素并且指定该元素“下面”的任何东西。

    +
    +

    由上可知,当我们将pointer-events值设置为none时,它就有了穿透的特性,也就是说你“看得见它,却摸不到它”,而且也不具备鼠标指针的特性,这不正是水印想要的效果嘛,完美~;

    +

    接着往下看我们可以知道它的初始值是auto,并且可以继承
    Y0J0XD.png

    +

    水印功能就这样实现了是不是简单实用,那么是不是就没有缺点呢,当然要硬说有的话那就是兼容性的问题吧。

    +

    因为这个css3的属性是新出的,自然ie9及以下自然就不支持了,而且ie不支持的范围要到ie11以下,所以ie你为啥不去死呢。。。

    +

    Y0YEjO.png

    +]]>
    + + 前端技术 + CSS3 + + + CSS3 + +
    + + Prettier格式化配置 + /2020/05/28/Prettier-Setting/ + 前言

    最近在做代码风格统一,用到了Prettier,暂时先记录一下现在的配置信息,回头抽时间写篇总结文。

    + +

    一、技术栈

    {
    // 使能每一种语言默认格式化规则
    "[html]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[css]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[less]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
    },

    /* prettier的配置 */
    "prettier.printWidth": 100, // 超过最大值换行
    "prettier.tabWidth": 4, // 缩进字节数
    "prettier.useTabs": false, // 缩进不使用tab,使用空格
    "prettier.semi": true, // 句尾添加分号
    "prettier.singleQuote": true, // 使用单引号代替双引号
    "prettier.proseWrap": "preserve", // 默认值。因为使用了一些折行敏感型的渲染器(如GitHub comment)而按照markdown文本样式进行折行
    "prettier.arrowParens": "avoid", // (x) => {} 箭头函数参数只有一个时是否要有小括号。avoid:省略括号
    "prettier.bracketSpacing": true, // 在对象,数组括号与文字之间加空格 "{ foo: bar }"
    "prettier.disableLanguages": ["vue"], // 不格式化vue文件,vue文件的格式化单独设置
    "prettier.endOfLine": "auto", // 结尾是 \n \r \n\r auto
    "prettier.eslintIntegration": false, //不让prettier使用eslint的代码格式进行校验
    "prettier.htmlWhitespaceSensitivity": "ignore",
    "prettier.ignorePath": ".prettierignore", // 不使用prettier格式化的文件填写在项目的.prettierignore文件中
    "prettier.jsxBracketSameLine": false, // 在jsx中把'>' 是否单独放一行
    "prettier.jsxSingleQuote": false, // 在jsx中使用单引号代替双引号
    "prettier.parser": "babylon", // 格式化的解析器,默认是babylon
    "prettier.requireConfig": false, // Require a 'prettierconfig' to format prettier
    "prettier.stylelintIntegration": false, //不让prettier使用stylelint的代码格式进行校验
    "prettier.trailingComma": "es5", // 在对象或数组最后一个元素后面是否加逗号(在ES5中加尾逗号)
    "prettier.tslintIntegration": false // 不让prettier使用tslint的代码格式进行校验
    }
    ]]>
    + + 效率工具 + VSCode插件 + + + Prettier + +
    + + 前端面试Vue篇:Vue组件通信的几种方式 + /2019/12/27/Vue-Component-Communication/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    公司这两年兴起了前后端分离的热潮,在选用三大技术框架时,vue凭借其学习曲线低深受公司程序员的喜爱,因此我们也就走上了vue学习的不归路,在招聘招揽小伙伴时有那么一些问题经常被问起,vue组件间通信就是其中之一,下面我们来聊一聊vue组件通信有哪些方式。

    + + +

    props$emit

    啥也不了解的小伙伴应该也知道这种方式吧,这是最最基础的通信方式了,父子组件通信基本都用它。父组件向子组件传递数据的时候通过prop传参,子组件中通过$emit传递给父组件,父组件在触发子组件$emit方法时得到子组件数据。实例如下:

    +
    // 父组件 parent.vue

    Vue.compinent('parent', {
    template: `
    <div>
    <p>父组件</p>
    <child :message="message" @getChildrenData="getChildrenData"></child>
    </div>`,
    data() {
    return {
    message:'Hello lixuguang'
    }
    },
    methods: {
    /**
    * 执行子组件触发的事件方法
    */
    getChildrenData(data){
    console.log(data)
    }
    }
    })

    // 子组件 child.vue

    Vue.component('child', {
    props:['message'], // 得到父组件传过来的数据
    data() {
    return {
    childMessage: this.message
    }
    },
    template:`
    <div>
    <input type="text" v-model="childMessage" @input="emitParentData(childMessage)">
    </div>
    `,
    methods: {
    emitParentData(data) {
    this.$emit('getChildrenData', data) // 父组件触发时给父组件传值
    }
    }
    })

    // App.vue
    var app = new Vue({
    el: "#app",
    template: `
    <div>
    <parent></parent>
    </div>
    `
    })
    +

    解析代码:

    +
      +
    1. 父组件通过message属性将数据传递给子组件,并且通过getChildrenData事件来监听子组件出发的事件;
    2. +
    3. 子组件通过props获得父组件传过来的数据,并且通过this.$emit触发了getChildrenData事件;
    4. +
    +

    $attrs$listeners

    前一种方法我们完成了父子组件的数据通信,那你有没有想过如果有多层嵌套的数据最上层要往最下层传值怎么办,前一种方法只能一层一层的往下传,可是这样太麻烦了,那么有没有什么方法能够一次传到你想要传到的位置呢,当然是有的,那就是接下来要说到的$attrs$listeners
    假设我们现在有三层包含关系的组件,分别是level1/level2/level3,level1 > level2 > level3, > 表示包含关系。

    +
    // level3.vue

    Vue.component('level3', {
    template:`
    <div>
    <input type="text" v-model="$attrs.level3Message" @input="emitLevel3Data($attrs.level3Message)">
    </div>
    `,
    methods: {
    emitLevel3Data(data) {
    this.$emit('getLevel3Data', data)
    }
    }
    })

    // level2.vue

    Vue.component('level2', {
    props:['level2Message'],
    data(){
    return {
    level2Message:this.level2Message
    }
    },
    template:`
    <div>
    <input type="text" v-model="level2Message" @input="emitLevel2Data(level2Message)">
    <level3 v-bind='$attrs' v-on='$listenrs'></level3>
    </div>
    `,
    methods: {
    emitLevel2Data(data) {
    this.$emit('getLevel2Data', data)
    }
    }
    })

    // level1.vue

    Vue.component('level1', {
    data(){
    return {
    level3Message:"I am Level3",
    level2Message:"I am Level2"
    }
    },
    template:`
    <div>
    <h1>这是level1中的内容</h1>
    <level2 :level2Message="level2Message"
    :level3Message="level3Message"
    @getLevel2Data="getLevel2Data()"
    @getLevel3Data="getLevel3Data()"
    ></level2>
    </div>
    `,
    methods: {
    getLevel2Data(data) {
    console.log('这是来自level2的数据', data)
    },
    getLevel3Data(data) {
    console.log('这是来自level3的数据', data)
    }
    }
    })

    // App.vue

    var app = new Vue({
    el: "#app",
    template: `
    <div>
    <level1></level1>
    </div>
    `
    })
    +

    解析代码:

    +
      +
    1. level3组件能直接触发getLevel3Data是因为level2组件在调用level3组件时使用v-on绑定了$listeners属性;
    2. +
    3. 通过v-bind绑定了$attrs属性,level3组件可以直接获取到从level1传下来的props;
    4. +
    +

    v-model

    父组件通过v-model传递值给子组件时,会自动传递一个value的props户型,在子组件中可以通过this.$emit(‘input’,val)自动修改v-model绑定的值。

    +

    ```

    +

    未完待续~

    +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    ]]>
    + + 前端框架 + Vue + + + 面试 + 组件通信 + +
    + + JavaScript实现经典排序算法 + /2020/01/04/Algorithm/ + +

    作者:李旭光
    引用请标明出处

    + +

    基本概念

    时间复杂度

    一个算法的时间复杂度反映了程序运行从开始到结束所需要的时间。把算法中基本操作重复执行的次数(频度)作为算法的时间复杂度。
    没有循环语句,记作O(1),也称为常数阶。只有一重循环,则算法的基本操作的执行频度与问题规模n呈线性增大关系,记作O(n),也叫线性阶。
    常见的时间复杂度有:

    +
      +
    • O(1): Constant Complexity: Constant 常数复杂度
    • +
    • O(log n): Logarithmic Complexity: 对数复杂度
    • +
    • O(n): Linear Complexity: 线性时间复杂度
    • +
    • O(n^2): N square Complexity 平⽅方
    • +
    • O(n^3): N square Complexity ⽴立⽅方
    • +
    • O(2^n): Exponential Growth 指数
    • +
    • O(n!): Factorial 阶乘
    • +
    +

    空间复杂度

    一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。

    +

    一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。

    +
      +
    • 稳定
    • +
    • 不稳定

      算法汇总

      十大经典排序.jpg

      关于时间复杂度:

      平方阶 (O(n2)) 排序 各类简单排序:直接插入、直接选择和冒泡排序。
      线性对数阶 (O(nlog2n)) 排序 快速排序、堆排序和归并排序;
      O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数。 希尔排序
      线性阶 (O(n)) 排序 基数排序,此外还有桶、箱排序。
    • +
    +

    关于稳定性:

    稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
    不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。

    +

    名词解释:

      +
    • n:数据规模
    • +
    • k:“桶”的个数
    • +
    • In-place:占用常数内存,不占用额外内存
    • +
    • Out-place:占用额外内存
    • +
    • 稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同
    • +
    +

    1. 冒泡排序(Bubble Sort) — 前后两两比较 — 气泡

    冒泡排序可谓是最经典的排序算法了,它是基于比较的排序算法,其优点是实现简单,排序数量较小时性能较好。

    +

    它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

    +

    1. 1 算法原理

    相邻的数据进行两两比较,小数放在前面,大数放在后面,如果前面的数据比后面的数据大,就交换这两个数的位置。也可以实现大数放在前面,小数放在后面,如果前面的数据比后面的小,就交换两个的位置。要实现上述规则需要用到两层for循环。

    +

    1. 2 算法描述

      +
    1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
    2. +
    3. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
    4. +
    5. 针对所有的元素重复以上的步骤,除了最后一个;
    6. +
    7. 重复步骤1~3,直到排序完成。
    8. +
    +

    1. 3 动图演示

    ldB5VS.gif

    +

    1. 4 js代码实现

    // js代码

    function bubbleSort(arr) {
    var len = arr.length;
    for (var i = 0; i < len; i++) {
    for (var j = 0; j < len - 1 - i; j++) {
    if (arr[j] > arr[j + 1]) { // 相邻元素两两对比
    // 元素交换
    /** 1.使用中间变量 **/
    var temp = arr[j + 1];
    arr[j + 1] = arr[j]
    arr[j] = temp
    /** 2.适用纯数字的数组排序 **/
    arr[j] = arr[j] + arr[j + 1]
    arr[j + 1] = arr[j] - arr[j + 1]
    arr[j] -= arr[j + 1]
    /** 3.使用es6解构赋值 **/
    [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
    }
    }
    }
    return arr;
    }
    +

    冒泡排序算法优化

    +
    // js代码

    function bubbleSort(arr) {
    var len = arr.length;
    for (var i = 0; i < len; i++) {
    var exchange=false; // 交换标志
    for (var j = 0; j < len - 1 - i; j++) {
    if (arr[j] > arr[j + 1]) { // 相邻元素两两对比
    [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]] // 元素交换
    exchange=true; //
    }
    }
    if(!exchange){ // 若本趟排序未发生交换,提前终止算法
    break;
    }
    }
    return arr;
    }
    +

    2. 选择排序(Selection Sort) — 面试挑简历,在剩下的里面挑最好的

    表现最稳定的排序算法之一,因为无论什么数据进去都是O(n²)的时间复杂度。。。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。

    +

    2. 1 算法原理

    先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

    +

    2. 2 算法描述

    n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

    +
      +
    1. 初始状态:无序区为R[1..n],有序区为空;
    2. +
    3. 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中
    4. +
    5. 选出关键字最小的记录R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
    6. +
    7. n-1趟结束,数组有序化了。
    8. +
    +

    2. 3 动图演示

    ldDWW9.gif

    +

    2. 4 js代码实现

    // js代码

    function selectionSort(arr) {
    var len = arr.length;
    var minIndex, temp;
    for (var i = 0; i < len - 1; i++) {
    minIndex = i;
    for (var j = i + 1; j < len; j++) {
    if (arr[j] < arr[minIndex]) { //寻找最小的数
    minIndex = j; //将最小数的索引保存
    }
    }
    temp = arr[i];
    arr[i] = arr[minIndex];
    arr[minIndex] = temp;
    }
    return arr;
    }
    + +

    3. 插入排序(Insertion Sort)—– 麻将/扑克

    插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。

    +

    3. 1 算法原理

    它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

    +

    3. 2 算法描述

    一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

    +
      +
    1. 从第一个元素开始,该元素可以认为已经被排序;
    2. +
    3. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
    4. +
    5. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
    6. +
    7. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
    8. +
    9. 将新元素插入到该位置后;
    10. +
    11. 重复步骤2~5。
    12. +
    +

    3. 3 动图演示

    ldDfzR.gif

    +

    3. 4 js代码实现

    // js代码

    function insertSort(arr) {
    // 从1位置开始遍历arr中每元素,同时声明空变量temp
    for (let i = 1; i < arr.length; i++) {
    if (arr[i] < arr[i - 1]) { // 如果当前元素<前一个元素
    let temp = arr[i] // 将当前元素值临时保存在temp中
    let p = i - 1 // 定义变量 p = i- 1
    // 循环 条件:
    // 1. p>=0且temp小于p位置的元素
    while (p >= 0 && temp < arr[p]) {
    // 循环体: 将P位置的值赋值给p的后一个元素
    arr[p + 1] = arr[p]
    p-- // p向前移动一个
    }
    arr[p + 1] = temp // 将temp的值赋值给p+1位置的元素
    }
    }
    }
    + +

    4. 快速排序(Selection Sort)

    快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

    +

    4. 1 算法原理

    快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。

    +

    4. 2 算法描述

    选基准:在数据结构中选择一个元素作为基准(pivot
    划分区:参照基准元素值的大小,划分无序区,所有小于基准元素的数据放入一个区间,所有大于基准元素的数据放入另一区间,分区操作结束后,基准元素所处的位置就是最终排序后它应该所处的位置
    递归:对初次划分出来的两个无序区间,递归调用第 1步和第 2步的算法,直到所有无序区间都只剩下一个元素为止。

    +

    简单理解就是,选择一个目标值,比目标值小的放左边,比目标值大的放右边,目标值的位置已排好,将左右两侧再进行快排。

    +

    4. 3 动图演示

    快速排序.gif

    +

    4. 4 js代码实现

    // js代码

    function quickSort(arr){
    //如果arr.length<=1,则直接返回arr
    if(arr.length<=1){return arr}
    // arr的元素个数/2,再下去整,将值保存在pivotIndex中
    var pivotIndex=Math.floor(arr.length/2);
    // 将arr中pivotIndex位置的元素,保存在变量pivot中
    var pivot=arr[pivotIndex];
    //声明空数组left和right
    var left=[];
    var right=[];
    for(var i=0;i<arr.length;i++){ // 遍历arr中每个元素
    if(i !== pivotIndex){ // 如果i !== pivotIndex
    if(arr[i]<=pivot){ // 如果当前元素值<pivot
    left.push(arr[i]); // 就将当前值压入left
    }else{
    right.push(arr[i]); // 就将当前值压入right
    }
    }
    }
    //递归
    return quickSort(left).concat(pivot, quickSort(right)); // 链接多个数组到 left 从小到大
    }
    +

    5. 希尔排序

    5. 1 算法原理

    希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
    希尔排序是基于插入排序的以下两点性质而提出改进方法的:

    +
      +
    • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
    • +
    • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
      希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
    • +
    +

    5. 2 算法描述

      +
    • 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
    • +
    • 按增量序列个数 k,对序列进行 k 趟排序;
    • +
    • 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
    • +
    +

    5.3 js代码实现

    // js代码

    function shellSort(arr) {
    var len = arr.length,
    temp,
    gap = 1;
    while(gap < len/3) { //动态定义间隔序列
    gap =gap*3+1;
    }
    for (gap; gap > 0; gap = Math.floor(gap/3)) {
    for (var i = gap; i < len; i++) {
    temp = arr[i];
    for (var j = i-gap; j >= 0 && arr[j] > temp; j-=gap) {
    arr[j+gap] = arr[j];
    }
    arr[j+gap] = temp;
    }
    }
    return arr;
    }
    + +

    6. 归并排序

    6. 1 算法原理

    归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
    作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

    +
      +
    • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
    • +
    • 自下而上的迭代;
      +

      在《数据结构与算法 JavaScript 描述》中,作者给出了自下而上的迭代方法。但是对于递归法,作者却认为:
      However, it is not possible to do so in JavaScript, as the recursion goes too deep for the language to handle.
      然而,在 JavaScript 中这种方式不太可行,因为这个算法的递归深度对它来讲太深了。

      +
      +
    • +
    +

    说实话,我不太理解这句话。意思是 JavaScript 编译器内存太小,递归太深容易造成内存溢出吗?还望有大神能够指教。

    +

    和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。

    +

    6. 2 算法描述

      +
    1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
    2. +
    3. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
    4. +
    5. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
    6. +
    7. 重复步骤 3 直到某一指针达到序列尾;
    8. +
    9. 将另一序列剩下的所有元素直接复制到合并序列尾。
    10. +
    +

    6. 3 动图演示

    归并排序.gif

    +

    6. 4 js代码实现

    // js代码

    function mergeSort(arr) { // 采用自上而下的递归方法
    var len = arr.length;
    if(len < 2) {
    return arr;
    }
    var middle = Math.floor(len / 2),
    left = arr.slice(0, middle),
    right = arr.slice(middle);
    return merge(mergeSort(left), mergeSort(right));
    }

    function merge(left, right){
    var result = [];
    while (left.length && right.length) {
    if (left[0] <= right[0]) {
    result.push(left.shift());
    } else {
    result.push(right.shift());
    }
    }

    while (left.length)
    result.push(left.shift());

    while (right.length)
    result.push(right.shift());

    return result;
    }
    + +

    7. 堆排序

    7. 1 算法原理

    堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

    +
      +
    • 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
    • +
    • 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
      堆排序的平均时间复杂度为 Ο(nlogn)。
    • +
    +

    7. 2 算法描述

      +
    1. 将待排序序列构建成一个堆 H[0……n-1],根据(升序降序需求)选择大顶堆或小顶堆;
    2. +
    3. 把堆首(最大值)和堆尾互换;
    4. +
    5. 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
    6. +
    7. 重复步骤 2,直到堆的尺寸为 1。
    8. +
    +

    7. 3 动图演示

    堆排序.gif

    +

    7. 4 js代码实现

    // js代码

    var len; // 因为声明的多个函数都需要数据长度,所以把len设置成为全局变量

    function buildMaxHeap(arr) { // 建立大顶堆
    len = arr.length;
    for (var i = Math.floor(len/2); i >= 0; i--) {
    heapify(arr, i);
    }
    }

    function heapify(arr, i) { // 堆调整
    var left = 2 * i + 1,
    right = 2 * i + 2,
    largest = i;

    if (left < len && arr[left] > arr[largest]) {
    largest = left;
    }

    if (right < len && arr[right] > arr[largest]) {
    largest = right;
    }

    if (largest != i) {
    swap(arr, i, largest);
    heapify(arr, largest);
    }
    }

    function swap(arr, i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
    }

    function heapSort(arr) {
    buildMaxHeap(arr);

    for (var i = arr.length-1; i > 0; i--) {
    swap(arr, 0, i);
    len--;
    heapify(arr, 0);
    }
    return arr;
    }
    + +

    8. 计数排序

    8. 1 算法原理

    计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

    +

    8. 2 算法描述

    8. 3 动图演示

    计数排序.gif

    +

    8. 4 js代码实现

    // js代码

    function countingSort(arr, maxValue) {
    var bucket = new Array(maxValue+1),
    sortedIndex = 0;
    arrLen = arr.length,
    bucketLen = maxValue + 1;

    for (var i = 0; i < arrLen; i++) {
    if (!bucket[arr[i]]) {
    bucket[arr[i]] = 0;
    }
    bucket[arr[i]]++;
    }

    for (var j = 0; j < bucketLen; j++) {
    while(bucket[j] > 0) {
    arr[sortedIndex++] = j;
    bucket[j]--;
    }
    }

    return arr;
    }
    + +

    9. 桶排序

    9. 1 算法原理

    桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

    +
      +
    • 在额外空间充足的情况下,尽量增大桶的数量
    • +
    • 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
      同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

      9. 2 算法描述

      1. 什么时候最快

      当输入的数据可以均匀的分配到每一个桶中。

      2. 什么时候最慢

      当输入的数据被分配到了同一个桶中。

      9. 3 动图演示

    • +
    +

    9. 4 js代码实现

    // js代码

    function bucketSort(arr, bucketSize) {
    if (arr.length === 0) {
    return arr;
    }

    var i;
    var minValue = arr[0];
    var maxValue = arr[0];
    for (i = 1; i < arr.length; i++) {
    if (arr[i] < minValue) {
    minValue = arr[i]; // 输入数据的最小值
    } else if (arr[i] > maxValue) {
    maxValue = arr[i]; // 输入数据的最大值
    }
    }

    //桶的初始化
    var DEFAULT_BUCKET_SIZE = 5; // 设置桶的默认数量为5
    bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
    var bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
    var buckets = new Array(bucketCount);
    for (i = 0; i < buckets.length; i++) {
    buckets[i] = [];
    }

    //利用映射函数将数据分配到各个桶中
    for (i = 0; i < arr.length; i++) {
    buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i]);
    }

    arr.length = 0;
    for (i = 0; i < buckets.length; i++) {
    insertionSort(buckets[i]); // 对每个桶进行排序,这里使用了插入排序
    for (var j = 0; j < buckets[i].length; j++) {
    arr.push(buckets[i][j]);
    }
    }

    return arr;
    }
    + +

    10. 基数排序

    10. 1 算法原理

    基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

    +

    10. 2 算法描述

    1. 基数排序 vs 计数排序 vs 桶排序

    基数排序有三种方法:
    这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异案例看大家发的:

    +
      +
    • 基数排序:根据键值的每位数字来分配桶;
    • +
    • 计数排序:每个桶只存储单一键值;
    • +
    • 桶排序:每个桶存储一定范围的数值;

      10. 3 动图演示

    • +
    +
      +
    1. LSD 基数排序动图演示
      基数排序.gif

      10. 4 js代码实现

      // js代码

      // LSD Radix Sort
      var counter = [];
      function radixSort(arr, maxDigit) {
      var mod = 10;
      var dev = 1;
      for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
      for(var j = 0; j < arr.length; j++) {
      var bucket = parseInt((arr[j] % mod) / dev);
      if(counter[bucket]==null) {
      counter[bucket] = [];
      }
      counter[bucket].push(arr[j]);
      }
      var pos = 0;
      for(var j = 0; j < counter.length; j++) {
      var value = null;
      if(counter[j]!=null) {
      while ((value = counter[j].shift()) != null) {
      arr[pos++] = value;
      }
      }
      }
      }
      return arr;
      }
    2. +
    +

    总结

    排序算法.png
    以上就是十大经典算法,算法对于前端来说并不是一个十分熟悉的领域,但是排序算法算是算法里比较入门的,还是需要掌握的,毕竟即使是为了面试也是要准备的。

    +

    参考资料

    https://github.com/hustcc/JS-Sorting-Algorithm
    一本关于排序算法的 GitBook 在线书籍 《十大经典排序算法》,多语言实现。

    +

    http://www.sohu.com/a/136157205_671058
    技术面试宝典: 很全面的算法和数据结构知识(含代码实现)下篇

    +]]>
    + + 前端技术 + + + 面试 + 排序算法 + +
    + + CodeReview代码审查指南.md + /2022/06/01/CodeReview%E4%BB%A3%E7%A0%81%E5%AE%A1%E6%9F%A5%E6%8C%87%E5%8D%97/ + +

    作者:李旭光
    引用请标明出处

    + +

    1.关于Code Review

    1.1 Code Review的目的

    Code Review是一种用来确认方案设计和代码实现的质量保证机制,通过这个机制我们可以对代码、测试过程和注释进行检查。

    +

    Code Review主要用来在软件工程过程中改进代码质量,通过Code Review可以达到如下目的目的:

    +
      +
    1. 在项目早期就能够发现代码中的BUG
    2. +
    3. 帮助初级开发人员学习高级开发人员的经验,达到知识共享
    4. +
    5. 避免开发人员犯一些很常见,很普通的错误
    6. +
    7. 保证项目组人员的良好沟通
    8. +
    9. 项目或产品的代码更容易维护
    10. +
    +

    1.2 Code Review的前提

    进入Code Review需要检查的条件如下:

    +
      +
    1. Code Review人员是否理解了Code Review的概念和Code Review将做什么
      如果做Code Review的人员不能理解Code Review对项目成败和代码质量的重要程度,他们的做法可能就会是应付了事。
    2. +
    3. 代码是否已经正确的build,build的目的使得代码已经不存在基本语法错误
      我们总不希望高级开发人员或是主管将时间浪费在检查连编译都通不过的代码上吧。
    4. +
    5. 代码执行时功能是否正确
      Code Review人员也不负责检查代码的功能是否正确,也就是说,需要复查的代码必须由开发人员或质量人员负责该代码的功能的正确性。
    6. +
    7. Review人员是否理解了代码
      做复查的人员需要对该代码有一个基本的了解,其功能是什么,是拿一方面的代码,涉及到数据库或是通讯,这样才能采取针对性的检查
    8. +
    9. 开发人员是否对代码做了单元测试
      这一点也是为了保证Code Review前一些语法和功能问题已经得到解决,Code Review人员可以将精力集中在代码的质量上。
    10. +
    +

    1.3 Code Review需要做什么

    Code Review主要检查代码中是否存在以下方面问题:

    +
      +
    • 代码的一致性
    • +
    • 编码风格
    • +
    • 代码的安全问题
    • +
    • 代码冗余
    • +
    • 是否正确设计以满足需求(性能、功能)
    • +
    • 等等
    • +
    +

    1.3.1 完整性检查(Completeness)

      +
    • 代码是否完全实现了设计文档中提出的功能需求
    • +
    • 代码是否已按照设计文档进行了集成和Debug
    • +
    • 代码是否已创建了需要的数据库,包括正确的初始化数据
    • +
    • 代码中是否存在任何没有定义或没有引用到的变量、常数或数据类型
    • +
    +

    1.3.2 一致性检查(Consistency)

      +
    • 代码的逻辑是否符合设计文档
    • +
    • 代码中使用的格式、符号、结构等风格是否保持一致
    • +
    +

    1.3.3 正确性检查(Correctness)

      +
    • 代码是否符合制定的标准
    • +
    • 所有的变量都被正确定义和使用
    • +
    • 所有的注释都是准确的
    • +
    • 所有的程序调用都使用了正确的参数个数
    • +
    +

    1.3.4 可修改性检查(Modifiability)

      +
    • 代码涉及到的常量是否易于修改(如使用配置、定义为类常量、使用专门的常量类等)
    • +
    • 代码中是否包含了交叉说明或数据字典,以描述程序是如何对变量和常量进行访问的
    • +
    • 代码是否只有一个出口和一个入口(严重的异常处理除外)
    • +
    +

    1.3.5 可预测性检查(Predictability)

      +
    • 代码所用的开发语言是否具有定义良好的语法和语义
    • +
    • 是否代码避免了依赖于开发语言缺省提供的功能
    • +
    • 代码是否无意中陷入了死循环
    • +
    • 代码是否是否避免了无穷递归
    • +
    +

    1.3.6 健壮性检查(Robustness)

      +
    • 代码是否采取措施避免运行时错误
        +
      • 数组边界溢出
      • +
      • 被零除
      • +
      • 值越界
      • +
      • 堆栈溢出
      • +
      • +
      +
    • +
    +

    1.3.7 结构性检查(Structuredness)

      +
    • 程序的每个功能是否都作为一个可辩识的代码块存在
      循环是否只有一个入口
    • +
    +

    1.3.8 可追溯性检查(Traceability)

      +
    • 代码是否对每个程序进行了唯一标识
    • +
    • 是否有一个交叉引用的框架可以用来在代码和开发文档之间相互对应
    • +
    • 代码是否包括一个修订历史记录,记录中对 代码的修改和原因都有记录
    • +
    • 是否所有的安全功能都有标识
    • +
    +

    1.3.9 可理解性检查(Understandability)

      +
    • 注释是否足够清晰的描述每个子程序
    • +
    • 是否使用到不明确或不必要的复杂代码,它们是否被清楚的注释
    • +
    • 使用一些统一的格式化技巧(如缩进、空白等)用来增强代码的清晰度
    • +
    • 是否在定义命名规则时采用了便于记忆,反映类型等方法
    • +
    • 每个变量都定义了合法的取值范围
    • +
    • 代码中的算法是否符合开发文档中描述的数学模型
    • +
    +

    1.3.10 可验证性检查(Verifiability)

    代码中的实现技术是否便于测试

    +

    1.4 Code Review的步骤

    这些是我在平时工作中的经验总结,目前也是按照这个步骤在做。

    +
      +
    1. 代码编写者和代码审核者坐在一起,由代码编写者按照UC依次讲解自己负责的代码和相关逻辑,从Web层->DAO层;
    2. +
    3. 代码审核者在此过程中可以随时提出自己的疑问,同时积极发现隐藏的bug;对这些bug记录在案。
    4. +
    5. 代码讲解完毕后,代码审核者给自己安排几个小时再对代码审核一遍。
      代码需要一行一行静下心看。同时代码又要全面的看,以确保代码整体上设计优良。
    6. +
    7. 代码审核者根据审核的结果编写“代码审核报告”,“审核报告”中记录发现的问题及修改建议,然后把“审核报告”发送给相关人员。
    8. +
    9. 代码编写者根据“代码审核报告”给出的修改意见,修改好代码,有不清楚的地方可积极向代码审核者提出。
    10. +
    11. 代码编写者 bug fix完毕之后给出反馈。
    12. +
    13. 代码审核者把Code Review中发现的有价值的问题更新到”代码审核规范”的文档中,对于特别值得提醒的问题可群发email给所有技术人员。
    14. +
    +

    提示
    Code Review必备的文档:

    +
      +
    • “代码审核规范”文档:记录代码应该遵循的标准。
      +

      代码审核者根据这些标准来Code Review代码,同时在Code Review过程中不断完善该文档。

      +
      +
    • +
    +

    2.Code Reivew的执行

    一个标准的Code Reivew活动应该分为三个阶段:

    +

    2.1.事前准备阶段

    在一次CR前,对以下内容进行充分准备。

    +

    2.1.1.CR的对象

    在准备CR代码对象时,我们要注意代码的数量,如果代码量比较大,要对代码进行必要的分解,确定其中的关键代码,对关键代码进行CR,可以达到举一反三的目的。

    +

    2.1.2.CR的内容

    我们对代码的审查内容很多,如代码的编写是否规范(注释的书写格式、命名规范等)、技术处理规范(异常处理、日志处理、代码组织结构等)、业务实现等。我们不能希望通过一次CR活动,完成所有这些内容的审查,因此我们必须设定本次CR活动内容界限,确定审查重点;

    +

    2.1.3.评审规范和标准

    在CR前设计确定评审规范和标准是必要,通过规范和标准我们在审查过程中可以有据可依,有理可循,而且还可以做到标准统一。

    +

    2.1.4.选择CR活动的参与者

    在CR开始前,必须把本次CR活动的对象、审查内容以及审查的规范和标准通报给所有的参与者。

    +

    2.1.5.选择CR活动的实施方式。

    CR活动有很多形式可供我们选择,我们可以根据实际情况选择桌面式CR、演示讲解式CR、一对一的座位CR等等。

    +

    2.2.实施阶段

    充分的事前准备,只是做好CR活动的前提,在CR实施过程中,我们要做好以下工作。

    +

    2.2.1.准确记录

    对于CR过程发现的问题,我们必须清晰准确的记录,可以使用问题点记录单,明确记录的项目和内容。

    +

    2.2.2.讲解与提问

    CR过程中,要采用代码作者讲解和审查者提问方式。审查者不能只在发现问题时提问,同时也要根据本次审查的内容要求代码作者对某个特定问题的讲解。

    +

    2.2.3.逐项审查

    对事前确定的审查内容,要逐项审查,不能因为时间不足等因素一扫而过。

    +

    2.2.4.注意气氛

    实施审查时,要营造一个讨论问题、解决问题的氛围,不能把审查会搞成批判会,这样会影响相关人员的积极性。

    +

    2.3. 事后跟踪跟踪。

    2.3.1. 确认发现的问题

    CR结束后,对发现的问题,首先需要确定以下内容。

    +
      +
    1. 问题点的难易程度以及影响的范围;
    2. +
    3. 解决问题的责任者和问题点修正结果的确认者;
    4. +
    5. 解决问题点的时限。
    6. +
    +

    2.3.2. 修正问题责任者

    对于修正问题责任者,在问题点的修正过程中,要三方面内容的记录。

    +
      +
    1. 问题点的原因;
    2. +
    3. 解决问题点的对策;
    4. +
    5. 修正的内容。
    6. +
    +

    2.3.3. 修正结果确认者

    做为修正结果的确认者,必须按照事前约定的时限及时的对修正结果进行全面的确认

    +

    3.注意事项

    3.1. 经常进行Code Review

      +
    1. 要Review的代码越多,那么要重构,重写的代码就会越多。而越不被程序作者接受的建议也会越多,唾沫口水战也会越多。
    2. +
    3. 程序员代码写得时候越长,程序员就会在代码中加入越来越多的个人的东西。
    4. +
    5. 越接近软件发布的最终期限,代码也就不能改得太多。
    6. +
    +

    3.2. Code Review不要太正式,而且要短

    忘了那个代码评审的Checklist吧,走到你的同事座位跟前,像请师父一样请他坐到你的电脑面前,然后,花5分钟给他讲讲你的代码,给他另外一个5分钟让他给你的代码提提意见,这比什么都好。而如果你用了一个Checklist,让这个事情表现得很正式的话,下面两件事中必有一件事会发生:

    +
      +
    1. 只有在Checklist上存在的东西才会被Review。
    2. +
    3. Code Reviews 变成了一种礼节性的东西,你的同事会装做很关心你的代码,但其实他心里想着尽快地离开你。
    4. +
    +

    只有不正式的Code Review才会让你和评审者放轻松,人只有放松了,才会表现得很真实,很真诚。记住Review只不过是一种形式,而只有在相互信任中通过相互的讨论得到了有意义和有建设性的建议和意见,那才是最实在的。不然,作者和评审者的关系就会变成小偷和警察的关系。

    +

    3.3. 尽可能的让不同的人Reivew你的代码

    如果可能的话,不要总是只找一个人来Review你的代码,不同的人有不同的思考方式,有不同的见解,所以,不同的人可以全面的从各个方面评论你的代码。
    但不要太多了,人多嘴杂反而适得其反,基本上来说,不要超过3个人,这是因为,这是一个可以围在一起讨论的最大人员尺寸。

    +

    下面是几个优点:

    +
      +
    1. 从不同的方向评审代码总是好的。
    2. +
    3. 会有更多的人帮你在日后维护你的代码。
    4. +
    5. 这也是一个增加团队凝聚力的方法。
    6. +
    +

    3.4. 保持积极的正面的态度

    程序员最大的问题就是“自负”,尤其当我们Reivew别人的代码的时候,我已经见过无数的场面,程序员在Code Review的时候,开始抨击别人的代码,质疑别人的能力。太可笑了,我分析了一下,这类的程序员其实并没有什么本事,因为他们指责对方的目的是想告诉大家自己有多么的牛,靠这种手段来表现自己的程序员,其实是就是传说中所说的“半瓶水”。

    +

    所以,无论是代码作者,还是评审者,都需要一种积极向上的正面的态度,作者需要能够虚心接受别人的建议,因为别人的建议是为了让你做得更好;评审者也需要以一种积极的正面的态度向作者提意见,因为那是和你在一个战壕里的战友。记住,你不是一段代码,你是一个人!

    +

    3.5. 学会享受Code Reivew

    这可能是最重要的一个提示了,如果你到了一个人人都喜欢Code Reivew的团阿,那么,你会进入到一个生机勃勃的地方,在那里,每个人都能写出质量非常好的代码,在那里,你不需要经理的管理,团队会自适应一切变化,他们相互学习,相互帮助,不仅仅是写出好的代码,而且团队和其中的每个人都会自动进化,最关键的是,这个是一个团队。

    +

    资料来源

    +]]>
    + + 前端开发规范 + + + CodeReview + +
    + + 在Docker Compose中配置网络 + /2022/08/02/Docker%20%E9%95%9C%E5%83%8F%E5%AE%89%E5%85%A8%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%20/ + Docker 镜像安全最佳实践

    Docker 和 宿主机 的设置

      +
    1. 保证宿主机和 Docker 的版本是最新的
    2. +
    3. 不要暴露 Docker 的 守护进程(daemon) 的套接字
    4. +
    5. 使用 rootless 模式启动 Docker
    6. +
    7. 避免使用特权容器
    8. +
    9. 限制容器资源
    10. +
    11. 隔离容器网络
    12. +
    13. 提高容器的隔离度
    14. +
    15. 将文件系统和卷设置为只读
    16. +
    17. 完整的生命周期管理
    18. +
    19. 限制来自容器内的系统调用
    20. +
    +

    确保镜像安全

      +
    1. 扫描和验证容器镜像
    2. +
    3. 使用最小基础镜像
    4. +
    5. 不要向 Docker 镜像泄露敏感信息
    6. +
    7. 使用多阶段构建
    8. +
    9. 确保容器注册
    10. +
    11. 使用固定标签以获得不变性
    12. +
    +

    监控容器

      +
    1. 监控容器活动
    2. +
    3. 确保容器在运行时的安全
    4. +
    5. 将故障排除数据与容器分开保存
    6. +
    7. 为镜像使用元数据标签
    8. +
    +

    参考

    Top 20 Dockerfile best practices
    Dockerセキュリティベストプラクティス トップ20

    +]]>
    + + Docker + + + 镜像安全 + +
    + + + /2023/09/20/DockerCompose%E5%AE%89%E8%A3%85/ + DockerCompose安装
    DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
    mkdir -p $DOCKER_CONFIG/cli-plugins
    sudo curl -SL https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-x86_64 -o /usr/bin/docker-compose
    sudo chmod +x /usr/bin/docker-compose
    sudo docker-compose version
    +]]>
    +
    + + DockerFile + /2022/06/02/DockerFile/ + DockerFile

    原则与建议

      +
    • 容器轻量化。从镜像中产生的容器应该尽量轻量化,能在足够短的时间内停止、销毁、重新生成并替换原来的容器。
    • +
    • 使用 .gitignore。在大部分情况下,Dockerfile 会和构建所需的文件放在同一个目录中,为了提高构建的性能,应该使用 .gitignore 来过滤掉不需要的文件和目录。
    • +
    • 为了减少镜像的大小,减少依赖,仅安装需要的软件包。
    • +
    • 一个容器只做一件事。解耦复杂的应用,分成多个容器,而不是所有东西都放在一个容器内运行。如一个 Python Web 应用,可能需要 Server、DB、Cache、MQ、Log 等几个容器。一个更加极端的说法:One process per container。
    • +
    • 减少镜像的图层。不要多个 Label、ENV 等标签。
    • +
    • 对续行的参数按照字母表排序,特别是使用apt-get install -y安装包的时候。
    • +
    • 使用构建缓存。如果不想使用缓存,可以在构建的时候使用参数–no-cache=true来强制重新生成中间镜像。
    • +
    +

    常用命令

    FROM

    FROM 指令用于指定其后构建新镜像所使用的基础镜像

    +
    FROM <image>
    FROM <image>:<tag>
    FROM <image>:<digest>
    +

    FROM 必须 是 Dockerfile 中第一条非注释命令
    在一个 Dockerfile 文件中创建多个镜像时,FROM 可以多次出现。只需在每个新命令 FROM 之前,记录提交上次的镜像 ID。
    tag 或 digest 是可选的,如果不使用这两个值时,会使用 latest 版本的基础镜像

    +

    RUN

    在镜像的构建过程中执行特定的命令,并生成一个中间镜像

    +
    #shell格式
    RUN <command>
    #exec格式
    RUN ["executable", "param1", "param2"]
    +
      +
    • RUN 命令将在当前 image 中执行任意合法命令并提交执行结果。命令执行提交后,就会自动执行 Dockerfile 中的下一个指令。
    • +
    • 层级 RUN 指令和生成提交是符合 Docker 核心理念的做法。它允许像版本控制那样,在任意一个点,对 image 镜像进行定制化构建。
    • +
    • RUN 指令创建的中间镜像会被缓存,并会在下次构建中使用。如果不想使用这些缓存镜像,可以在构建时指定 ######no###cache 参数,如:docker build ######no###cache。
    • +
    +

    FROM scratch

    scratch 是一个空的虚拟的镜像源文件,因为需要的依赖都存在不需要基础镜像,从而减小体积。

    +

    COPY

    复制文件

    +
    COPY <源路径>... <目标路径>
    COPY ["<源路径1>",... "<目标路径>"]
    +

    和 RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的<目标路径>位置。比如:

    +
    COPY package.json /usr/src/app/
    + +

    <源路径>可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,如:

    +
    COPY hom* /mydir/
    COPY hom?.txt /mydir/
    + +

    <目标路径>可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

    +

    此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。

    +

    ADD

    更高级的复制文件
    ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。比如<源路径>可以是一个 URL,这种情况下,Docker 引擎会试图去下载这个链接的文件放到<目标路径>去。

    +

    在构建镜像时,复制上下文中的文件到镜像内,格式:

    +
    ADD <源路径>... <目标路径>
    ADD ["<源路径>",... "<目标路径>"]
    +

    注意
    如果 docker 发现文件内容被改变,则接下来的指令都不会再使用缓存。关于复制文件时需要处理的/,基本跟正常的 copy 一致

    +

    ENV

    设置环境变量
    格式有两种:

    +
    ENV <key> <value>
    ENV <key1>=<value1> <key2>=<value2>...
    +

    这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。

    +
    ENV VERSION=1.0 DEBUG=on \
    NAME="Happy Feet"
    +

    这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。

    +

    EXPOSE

    为构建的镜像设置监听端口,使容器在运行时监听。
    格式:

    +
    EXPOSE <port> [<port>...]
    +

    EXPOSE 指令并不会让容器监听 host 的端口,如果需要,需要在 docker run 时使用 -p、-P 参数来发布容器端口到 host 的某个端口上。

    +

    VOLUME

    定义匿名卷
    VOLUME用于创建挂载点,即向基于所构建镜像创始的容器添加卷:

    +
    VOLUME ["/data"]
    +

    一个卷可以存在于一个或多个容器的指定目录,该目录可以绕过联合文件系统,并具有以下功能:

    +
      +
    • 卷可以容器间共享和重用
    • +
    • 容器并不一定要和其它容器共享卷
    • +
    • 修改卷后会立即生效
    • +
    • 对卷的修改不会对镜像产生影响
    • +
    • 卷会一直存在,直到没有任何容器在使用它
      VOLUME 让我们可以将源代码、数据或其它内容添加到镜像中,而又不并提交到镜像中,并使我们可以多个容器间共享这些内容。
    • +
    +

    WORKDIR

    WORKDIR用于在容器内设置一个工作目录:

    +
    WORKDIR /path/to/workdir
    +

    通过WORKDIR设置工作目录后,Dockerfile 中其后的命令 RUN、CMD、ENTRYPOINT、ADD、COPY 等命令都会在该目录下执行。
    如,使用WORKDIR设置工作目录:

    +
    WORKDIR /a
    WORKDIR b
    WORKDIR c
    RUN pwd
    +

    在以上示例中,pwd 最终将会在 /a/b/c 目录中执行。在使用 docker run 运行容器时,可以通过-w参数覆盖构建时所设置的工作目录。

    +

    USER

    指定当前用户
    USER 用于指定运行镜像所使用的用户:

    +
    USER daemon
    +

    使用USER指定用户时,可以使用用户名、UID 或 GID,或是两者的组合。以下都是合法的指定试:

    +
    USER user
    USER user:group
    USER uid
    USER uid:gid
    USER user:gid
    USER uid:group
    +

    使用USER指定用户后,Dockerfile 中其后的命令 RUN、CMD、ENTRYPOINT 都将使用该用户。镜像构建完成后,通过 docker run 运行容器时,可以通过 -u 参数来覆盖所指定的用户。

    +

    CMD

    CMD用于指定在容器启动时所要执行的命令。CMD 有以下三种格式:

    +
    CMD ["executable","param1","param2"]
    CMD ["param1","param2"]
    CMD command param1 param2
    +

    省略可执行文件的 exec 格式,这种写法使 CMD 中的参数当做 ENTRYPOINT 的默认参数,此时 ENTRYPOINT 也应该是 exec 格式,具体与 ENTRYPOINT 的组合使用,参考 ENTRYPOINT。

    +

    注意
    与 RUN 指令的区别:RUN 在构建的时候执行,并生成一个新的镜像,CMD 在容器运行的时候执行,在构建时不进行任何操作。

    +

    ENTRYPOINT

    ENTRYPOINT 用于给容器配置一个可执行程序。也就是说,每次使用镜像创建容器时,通过 ENTRYPOINT 指定的程序都会被设置为默认程序。ENTRYPOINT 有以下两种形式:

    +
    ENTRYPOINT ["executable", "param1", "param2"]
    ENTRYPOINT command param1 param2
    ENTRYPOINT 与 CMD 非常类似,不同的是通过docker run执行的命令不会覆盖 ENTRYPOINT,而docker run命令中指定的任何参数,都会被当做参数再次传递给 ENTRYPOINT。Dockerfile 中只允许有一个 ENTRYPOINT 命令,多指定时会覆盖前面的设置,而只执行最后的 ENTRYPOINT 指令。
    +

    docker run运行容器时指定的参数都会被传递给 ENTRYPOINT ,且会覆盖 CMD 命令指定的参数。如,执行docker run -d时,-d 参数将被传递给入口点。

    +

    也可以通过docker run –entrypoint重写 ENTRYPOINT 入口点。如:可以像下面这样指定一个容器执行程序:

    +
    ENTRYPOINT ["/usr/bin/nginx"]
    +

    完整构建代码:

    +
    # Version: 0.0.3
    FROM ubuntu:16.04
    MAINTAINER 何民三 "cn.liuht@gmail.com"
    RUN apt-get update
    RUN apt-get install -y nginx
    RUN echo 'Hello World, 我是个容器' \
    > /var/www/html/index.html
    ENTRYPOINT ["/usr/sbin/nginx"]
    EXPOSE 80
    +

    使用docker build构建镜像,并将镜像指定为 itbilu/test:

    +
    docker build -t="itbilu/test" .
    +

    构建完成后,使用itbilu/test启动一个容器:

    +
    docker run -i -t  itbilu/test -g "daemon off;"
    +

    在运行容器时,我们使用了 -g “daemon off;”,这个参数将会被传递给 ENTRYPOINT,最终在容器中执行的命令为 /usr/sbin/nginx -g “daemon off;”。

    +

    LABEL

    LABEL用于为镜像添加元数据,元数以键值对的形式指定:

    +
    LABEL <key>=<value> <key>=<value> <key>=<value> ...
    +

    使用LABEL指定元数据时,一条LABEL指定可以指定一或多条元数据,指定多条元数据时不同元数据之间通过空格分隔。推荐将所有的元数据通过一条LABEL指令指定,以免生成过多的中间镜像。
    如,通过LABEL指定一些元数据:

    +
    LABEL version="1.0" description="这是一个Web服务器" by="IT笔录"
    +

    指定后可以通过docker inspect查看:

    +
    docker inspect itbilu/test
    "Labels": {
    "version": "1.0",
    "description": "这是一个Web服务器",
    "by": "IT笔录"
    },
    + +

    ARG

    ARG用于指定传递给构建运行时的变量:

    +
    ARG <name>[=<default value>]
    +

    如,通过ARG指定两个变量:

    +
    ARG site
    ARG build_user=IT笔录
    +

    以上我们指定了 site 和 build_user 两个变量,其中 build_user 指定了默认值。在使用 docker build 构建镜像时,可以通过 –build-arg = 参数来指定或重设置这些变量的值。

    +
    docker build --build-arg site=itiblu.com -t itbilu/test .
    +

    这样我们构建了 itbilu/test 镜像,其中site会被设置为 itbilu.com,由于没有指定 build_user,其值将是默认值 IT 笔录。

    +

    ONBUILD

    ONBUILD用于设置镜像触发器:

    +
    ONBUILD [INSTRUCTION]
    +

    当所构建的镜像被用做其它镜像的基础镜像,该镜像中的触发器将会被钥触发。
    如,当镜像被使用时,可能需要做一些处理:

    +
    [...]
    ONBUILD ADD . /app/src
    ONBUILD RUN /usr/local/bin/python-build --dir /app/src
    [...]
    + +

    STOPSIGNAL

    STOPSIGNAL用于设置停止容器所要发送的系统调用信号:

    +
    STOPSIGNAL signal
    +

    所使用的信号必须是内核系统调用表中的合法的值,如:SIGKILL。

    +

    SHELL

    SHELL用于设置执行命令(shell式)所使用的的默认 shell 类型:

    +
    SHELL ["executable", "parameters"]
    +

    SHELL在Windows环境下比较有用,Windows 下通常会有 cmd 和 powershell 两种 shell,可能还会有 sh。这时就可以通过 SHELL 来指定所使用的 shell 类型:

    +
    FROM microsoft/windowsservercore

    # Executed as cmd /S /C echo default
    RUN echo default

    # Executed as cmd /S /C powershell -command Write-Host default
    RUN powershell -command Write-Host default

    # Executed as powershell -command Write-Host hello
    SHELL ["powershell", "-command"]
    RUN Write-Host hello

    # Executed as cmd /S /C echo hello
    SHELL ["cmd", "/S"", "/C"]
    RUN echo hello
    + +

    Dockerfile 示例

      +
    • 构建Nginx运行环境
    • +
    • 构建tomcat 环境
    • +
    +

    构建Nginx运行环境

    Dockerfile文件

    +
    #指定基础镜像
    FROM sameersbn/ubuntu:14.04.20161014

    #维护者信息
    MAINTAINER sameer@damagehead.com

    #设置环境
    ENV RTMP_VERSION=1.1.10 \
    NPS_VERSION=1.11.33.4 \
    LIBAV_VERSION=11.8 \
    NGINX_VERSION=1.10.1 \
    NGINX_USER=www-data \
    NGINX_SITECONF_DIR=/etc/nginx/sites-enabled \
    NGINX_LOG_DIR=/var/log/nginx \
    NGINX_TEMP_DIR=/var/lib/nginx \
    NGINX_SETUP_DIR=/var/cache/nginx

    #设置构建时变量,镜像建立完成后就失效
    ARG BUILD_LIBAV=false
    ARG WITH_DEBUG=false
    ARG WITH_PAGESPEED=true
    ARG WITH_RTMP=true

    #复制本地文件到容器目录中
    COPY setup/ ${NGINX_SETUP_DIR}/
    RUN bash ${NGINX_SETUP_DIR}/install.sh

    #复制本地配置文件到容器目录中
    COPY nginx.conf /etc/nginx/nginx.conf
    COPY entrypoint.sh /sbin/entrypoint.sh

    #运行指令
    RUN chmod 755 /sbin/entrypoint.sh

    #允许指定的端口
    EXPOSE 80/tcp 443/tcp 1935/tcp

    #指定网站目录挂载点
    VOLUME ["${NGINX_SITECONF_DIR}"]

    ENTRYPOINT ["/sbin/entrypoint.sh"]
    CMD ["/usr/sbin/nginx"]
    + +

    构建Tomcat环境

    Dockerfile文件

    +
    # 指定基于的基础镜像
    FROM ubuntu:13.10

    # 维护者信息
    MAINTAINER zhangjiayang "zhangjiayang@sczq.com.cn"

    # 镜像的指令操作
    # 获取APT更新的资源列表
    RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe"> /etc/apt/sources.list
    # 更新软件
    RUN apt-get update

    # Install curl
    RUN apt-get -y install curl

    # Install JDK 7
    RUN cd /tmp && curl -L 'http://download.oracle.com/otn-pub/java/jdk/7u65-b17/jdk-7u65-linux-x64.tar.gz' -H 'Cookie: oraclelicense=accept-securebackup-cookie; gpw_e24=Dockerfile' | tar -xz
    RUN mkdir -p /usr/lib/jvm
    RUN mv /tmp/jdk1.7.0_65/ /usr/lib/jvm/java-7-oracle/

    # Set Oracle JDK 7 as default Java
    RUN update-alternatives --install /usr/bin/java java /usr/lib/jvm/java-7-oracle/bin/java 300
    RUN update-alternatives --install /usr/bin/javac javac /usr/lib/jvm/java-7-oracle/bin/javac 300

    # 设置系统环境
    ENV JAVA_HOME /usr/lib/jvm/java-7-oracle/

    # Install tomcat7
    RUN cd /tmp && curl -L 'http://archive.apache.org/dist/tomcat/tomcat-7/v7.0.8/bin/apache-tomcat-7.0.8.tar.gz' | tar -xz
    RUN mv /tmp/apache-tomcat-7.0.8/ /opt/tomcat7/

    ENV CATALINA_HOME /opt/tomcat7
    ENV PATH $PATH:$CATALINA_HOME/bin

    # 复件tomcat7.sh到容器中的目录
    ADD tomcat7.sh /etc/init.d/tomcat7
    RUN chmod 755 /etc/init.d/tomcat7

    # Expose ports. 指定暴露的端口
    EXPOSE 8080

    # Define default command.
    ENTRYPOINT service tomcat7 start && tail -f /opt/tomcat7/logs/catalina.out
    + +

    tomcat7.sh命令文件

    +
    export JAVA_HOME=/usr/lib/jvm/java-7-oracle/  
    export TOMCAT_HOME=/opt/tomcat7

    case $1 in
    start)
    sh $TOMCAT_HOME/bin/startup.sh
    ;;
    stop)
    sh $TOMCAT_HOME/bin/shutdown.sh
    ;;
    restart)
    sh $TOMCAT_HOME/bin/shutdown.sh
    sh $TOMCAT_HOME/bin/startup.sh
    ;;
    esac
    exit 0
    +]]>
    + + Infra + Docker + + + DockerFile + +
    + + Docker 命令 + /2022/06/02/Docker%E5%91%BD%E4%BB%A4/ + Docker 命令
      +
    • 帮助命令
      docker –help
    • +
    +

    镜像操作

      +
    • 搜索镜像
      docker search hello-world

      +
    • +
    • 下载镜像
      docker pull hello-world

      +
    • +
    • 查看本地已下载所有镜像
      docker images

      +
    • +
    • 查看镜像历史
      docker history hello-world

      +
    • +
    • 备份镜像
      docker tag hello-word:last hello-world:v2

      +
    • +
    • 删除镜像
      docker rmi hello-word:last

      +
    • +
    • 删除未使用过的镜像
      docker image prune

      +
    • +
    • 导出镜像
      docker save -o hello-world:last.tar hello-world:last

      +
    • +
    • 导入镜像
      docker load -i hello-world:last.tar

      +
    • +
    • 查看镜像信息
      docker image inspact nginx

      +
    • +
    +

    容器操作

      +
    • 查看所有容器 [-q 编号] [-a active 启动的容器]
      docker ps [-a] [-q]

      +
    • +
    • 启动容器 [-d 后台启动]
      docker run -d –name nginx1 nginx:last

      +
    • +
    • 停止容器
      docker stop nginx1

      +
    • +
    • 启动容器()
      docker start nginx1

      +
    • +
    • 删除容器
      docker rm nginx1

      +
    • +
    • 批量删除运行中容器
      docker rm $(docker ps -q) -f

      +
    • +
    • 创建容器并进入
      docker run -it –name nginx1 nginx:last /bin/bash

      +
    • +
    • 退出容器
      exit

      +
    • +
    • 进入容器
      docker exec -it nginx1 /bin/bash

      +
    • +
    • 通过容器创建镜像
      docker commit -m ‘laowang’ nginx1 nginx:v1

      +
    • +
    • 查看容器信息
      docker container inspact nginx1

      +
    • +
    +]]>
    + + Infra + Docker + + + Docker + +
    + + Docker容器端口映射 + /2022/06/02/Docker%E5%AE%B9%E5%99%A8%E7%AB%AF%E5%8F%A3%E6%98%A0%E5%B0%84/ + Docker容器端口映射

    常见容器服务需要做端口映射,这里以nginx为例进行举例

    +

    启动一个nginx容器

    docker run -itd –name nginx1 -P nginx:latest #随机端口
    docker run -itd –name nginx2 -p 6001:80 nginx:latest #指定端口
    docker run -itd –name nginx3 -p 172.16.10.1:6002:80 nginx:latest #指定IP和端口

    +]]>
    + + Infra + Docker + + + 容器端口映射 + +
    + + Docker数据卷 + /2022/06/02/Docker%E6%95%B0%E6%8D%AE%E5%8D%B7/ + Docker数据卷

    宿主机与容器进行数据交互,共享宿主机与容器之间的数据

    +

    创建数据卷关联

    docker run -it –name my_ubuntu1 -v ~ /Desktop/test:/test /bin/bash

    +

    利用系统方法操作数据卷

      +
    • 查 docker数据卷
      docker volume ls

      +
    • +
    • 创建数据卷
      docker volume create volname

      +
    • +
    • 共享
      docker run -it –name my_ubuntu2 -v volname:/home ubuntu:latest /bin/bash

      +
    • +
    +

    数据卷容器使用

    可以通过数据卷容器,创建新的容器,并将多个容器绑定在一起

    +

    docker run -itd –name my_ubuntu3 –volumes-from my_ubuntu2 ubuntu:latest

    +]]>
    + + Infra + Docker + + + 数据卷 + +
    + + Docker网络模式 + /2022/06/02/Docker%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%BC%8F/ + Docker网络模式

    Docker常见的两种网络模式

    +
      +
    • host网络模式:创建的容器和宿主机共享同一个网卡
    • +
    • bridge网络模式:桥接模式,只要使用桥接模式创建容器网段一样(类似于172.16.1.1,172.16.1.2)有点像是子局域网
    • +
    +

    利用network命令管理网络模式

      +
    • 查看网络模式
      docker network ls
    • +
    • 创建网络模式
      docker network create –drive bridge bridge_test
    • +
    • 通过network断网
      docker network disconnet bridge nginx5
    • +
    • 通过network联网
      docker network connect bridge nginx5
    • +
    • 删除网络模式
      docker network rm bridge_test
    • +
    +]]>
    + + Infra + Docker + + + 网络模式 + +
    + + Electron编码规范.md + /2022/06/01/Electron%E7%BC%96%E7%A0%81%E8%A7%84%E8%8C%83/ + +

    作者:李旭光
    引用请标明出处

    + +

    目录
    代码规范 2

    +
      +
    1. 说明 2
    2. +
    3. 基本原则 2
    4. +
    5. C++与Python 2
    6. +
    7. 命名相关 2
    8. +
    9. 工程目录结构 3
    10. +
    +

    Electron代码规范

    +
      +
    1. 说明
      Electron框架正如官网介绍是使用 JavaScript, HTML 和 CSS 构建跨平台的桌面应用的框架,框架分为前端和后端两个部分,根据前后端采用的技术不同分别遵循前后端所采用技术的代码规范
    2. +
    3. 基本原则
    4. +
    +
      +
    1. 不要直接在渲染进程操作主进程,通过进程通信的方式进行数据及事件响应

      +
    2. +
    3. nodejs请使用require方式引入资源不使用import方式引入

      +
    4. +
    5. 多渲染进程传参请使用localStorage或sessionStorage

      +
    6. +
    7. 资源下载不下来时请直接到官网下载https://github.com/electron/electron/releases 将文件放到 用户目录\AppData\Local\electron\Cache 下 再进行 npm install -g electron

      +
    8. +
    9. 代码中包含native模块,必须进行 rebuild操作
      调试渲染进程时增加如下代码增加mainWindow.webContents.openDevTools();

      +
    10. +
    11. +
    +
      +
    1. C++与Python
      对于 C++ 和 Python, 遵循 Chromium 的编码风格. 可以使用 clang-format 来自动格式化 C++ 代码. 可以使用 script/cpplint.py 来检验文件是否符合要求。
    2. +
    +

    现在使用的 Python 版本是 Python 2.7。

    +

    C++ 代码使用了大量 Chromium 的抽象和类型,因此建议使用者熟悉它们。 一个起步的好地方是 Chromium 的《重要的抽象概念和数据库结构》文档. 该文档提到一些特殊类型,范围类型(超出范围时自动释放其内存), 记录机制等。
    4. 命名相关
    Electron API 使用与 Node.js 相同的大小写方案:
    当模块本身是class时, 比如 BrowserWindow, 使用 大驼峰.
    当模块是一组 API 时, 比如 globalShortcut时,使用 小驼峰。
    当 API 是对象的属性时, 并且它复杂到足以成为一个单独的块, 比如 win.webContents, 使用 小驼峰.
    对于其他非模块API, 使用自然标题, 比如 Tag 或 Process Object.

    +

    当创建新的 API 时, 最好使用 getter 和 setter 而不是 jQuery 的一次性函数。 举个例子, .getText() 和 .setText(text) 优于 .text([text]).

    +
      +
    1. 工程目录结构
      src - main
      // 必须

      +
        +
      • main.js // 入口
      • +
      • listen.js // 监听渲染进程通信
      • +
      • windows.js // 创建渲染进程
      • +
      • log.js // 日志log4js 插件
      • +
      +

      // 可选

      +
        +
      • utils.js
      • +
      • store.js
      • +
      +
    2. +
    +
      +
    • api.js
    • +
    • render
    • +
    +

    main 目录下为主进程代码,render 目录下为渲染进程代码
    渲染进程目录结构规范与前端项目目录结构规范一致

    +

    主进程中必须包含入口文件,监听进程文件和窗口创建文件
    其他根据后端实际使用技术可选,nodejs作为后端的话 必须
    6. 打包
    Electron-packager 绿色可执行包
    Electron-builder 压缩安装包

    +]]>
    + + 前端开发规范 + + + Electron + +
    + + GIT Standard.md + /2022/06/01/GIT%20Standard/ + +

    作者:李旭光
    引用请标明出处

    + +

    1、说明

    项目是一个多人协同完成同一个目标的团队组织形式,在多人协作项目中,如果代码风格统一(前端代码规范和静态代码检查约束)、代码提交信息的说明准确规范(本章介绍),那么在项目开发过程及后期协作以及Bug处理时会更加方便。

    +

    在本文中,我将介绍大家如何利用工具及约定保证大家代码提交的统一性,从而提高大家的协同效率:

    +
      +
    • commitlint: git 提交信息规范与验证
      +

      添加如ESLint的格式规范校验,规范comiit的格式,达到团队每个人的提交风格保持一直,保证提交信息的完整和准确性
      风格如下:

      +
      +
    • +
    +
    <type>(<scope>): <subject> // Header
    // 空一行
    <body> // 72
    // 空一行
    <footer> // 72

    // 例
    fix(登陆模块): 修复登录密码错误的提示

    登录没有做友好型提示

    修正后不存在此问题
    + +
      +
    • husky: 使git-hook更容易
      +

      husky继承了Git下所有的钩子,在触发钩子的时候,husky可以阻止不合法的commit,push等等。注意使用husky之前,必须先将代码放到git 仓库中,否则本地没有.git文件,就没有地方去继承钩子了。

      +
      +
    • +
    • standard-version: 自动生成CHANGELOG 并发布版本
    • +
    +

    2、git commit message 规范

    commit message格式

    类型(影响范围): 描述

    问题描述

    修复方式结果
    + + +

    注意:
    1.冒号后面有空格。
    2.英文小括号。
    3.正文和注脚前都要加空行。

    +

    type 类型

    用于说明 commit 的类别,只允许使用下面13个标识。

    +
    'feat', // feat:新增功能
    'fix', // fix:bug 修复
    'docs', // docs:文档更新
    'style', // style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑)
    'refactor', // refactor:重构代码(既没有新增功能,也没有修复 bug)
    'test', // test:新增测试用例或是更新现有测试
    'chore', // revert:回滚某个更早之前的提交
    'revert', // build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交
    'build', // build:打包生产环境代码
    'ci', // ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交
    'merge', // merge:分支合并 Merge branch ? of ?
    'perf', // perf:性能, 体验优化
    'chore' // chore:不属于以上类型的其他类型(构建过程或辅助工具的变动)
    + +

    如果type为feat和fix,则该 commit 将肯定出现在 Change log 之中。

    +

    scope 影响范围

    可根据项目组需要进行定制化设置,也可不做强制要求

    +

    subject 描述

    subject是 commit 目的的简短描述,不超过50个字符,且结尾不加句号(.)。

    +

    body 详细描述

    body 部分是对本次 commit 的详细描述,可以分成多行。下面是一个范例。

    +
    此次提交内容包括如下信息:
    - 登录为空校验
    - 密码错误提示
    +

    Footer 部分只用于两种情况。

    +
      +
    • 不兼容变动(改变解决方案)
    • +
    • 关闭 Issue(回复bug)
    • +
    +

    3、使用工具校验commit是否符合规范

    commitlint

    3.1 commitlint安装

    // npm 安装
    npm install --save-dev @commitlint/{cli,config-conventional}
    + +

    3.2 生成commitlint.config.js配置文件

    执行echo命令创建配置文件,也可以手动创建配置文件,或者从已有的配置文件进行拷贝。

    +
    // 项目根目录创建commitlint.config.js
    echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
    + + +

    3.3 在commitlint.config.js制定提交message规范

    // type(scope?): subject

    // body?

    // footer?
    module.exports = {
    // 继承自默认规则
    extends: ['@commitlint/config-conventional'],
    // 这里写自定义规则
    rules: {
    // 等级 [0 1 2]: 0 不使用规则 1 警告 2 错误
    // 启用范围 always|never: 总是或者永不.
    // 规则值: 规则对应的值

    // 头部(包含type、scope、subject)
    'header-case': [0, 'always', 'lower-case'],
    'header-full-stop': [0, 'never', '.'],
    'header-max-length': [0, 'always', 72],
    'header-min-length': [0, 'always', 0],

    // 提交类型
    'type-case': [0, 'never'],
    'type-empty': [2, 'never'],
    'type-max-length': [0, 'always', Infinity],
    'type-min-length': [0, 'always', 0],
    'type-enum': [
    2,
    'always',
    [
    'feat', // feat:新增功能
    'fix', // fix:bug 修复
    'docs', // docs:文档更新
    'style', // style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑)
    'refactor', // refactor:重构代码(既没有新增功能,也没有修复 bug)
    'test', // test:新增测试用例或是更新现有测试
    'revert', // revert:回滚某个更早之前的提交
    'build', // build:build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交,打包生产环境代码
    'ci', // ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交
    'merge', // merge:分支合并 Merge branch ? of ?
    'perf', // perf:性能, 体验优化
    'chore' // chore:不属于以上类型的其他类型(构建过程或辅助工具的变动)
    ]
    ],

    // 影响范围
    'scope-case': [0, 'always', 'lower-case'], // 书写格式
    'scope-max-length': [0, 'always', Infinity], // 最长
    'scope-min-length': [0, 'always', 0], // 最短
    'scope-empty': [2, 'never'], // 影响范围:为空|永不
    'scope-enum': [0, 'always', ['a', 'b']], // 影响范围:给出范围但是不在给定的列表内

    // 简介
    'subject-empty': [2, 'never'], // 简介不能为空
    'subject-full-stop': [0, 'never', '.'], // 简介结尾以.结束
    'subject-case': [0, 'never'],
    'subject-max-length': [0, 'always', Infinity],
    'subject-min-length': [0, 'always', 1],

    // 正文
    'body-leading-blank': [2, 'always'],
    'body-max-length': [0, 'always', Infinity],
    'body-max-line-length': [0, 'always', Infinity],
    'body-min-length': [0, 'always', 1],

    // 注脚
    'footer-leading-blank': [2, 'always'],
    'footer-max-length': [0, 'always', Infinity],
    'footer-max-line-length': [0, 'always', Infinity],
    'footer-min-length': [0, 'always', 1],

    // 其他
    'references-empty': [0, 'never'],
    'signed-off-by': [0, 'always', 'Signed-off-by']
    }
    }
    + +

    上面我们就完成了commitlint的安装与提交规范的制定。检验commit message的最佳方式是结合git hook,所以需要配合Husky

    +

    husky

    3.4 husky安装

    npm install husky --save-dev
    +

    3.5 husky 配置

    安装成功后需要在项目下的package.json中配置

    +
    // package.json
    {
    "husky": {
    "hooks": { // husky的钩子
    "pre-commit": "npm run lint" // 提交前进行代码静态校验
    "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" // 进行提交信息格式校验
    }
    },
    "scripts": {}
    ...
    }
    + +

    3.5 husky 执行测试

    最后我们可以正常的git操作

    +
    git add .
    git commit -m ""
    + +

    git commit的时候会触发commlint。下面演示下不符合规范提交示例:

    +
    C:\lixg\git>git commit -m "thunisoft: abcde"

    husky > npm run -s commitmsg (node v8.2.1)

    ⧗ input:
    thunisoft(all): abcde

    ✖ type must be one of [feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert] [type-enum]
    ✖ found 1 problems, 0 warnings

    husky > commit-msg hook failed (add --no-verify to bypass)

    C:\lixg\git>
    + +

    上面的提交看反馈消息得知是type格式没有符合限制的提交类型列表,所以提交失败,虾米啊我们把type改为feat再试一下

    +
    C:\lixg\git>git commit -m "feat(all): 新功能"

    husky > npm run -s commitmsg (node v8.2.1)

    ⧗ input: feat: 新功能
    ✔ found 0 problems, 0 warnings

    [develop 19dfhe] feat: 新功能
    1 file changed, 1 insertion(+)

    C:\lixg\git>
    + +

    修改后格式符合规范,提交成功。

    +

    参考来源

    +

    版权声明

    Copyright by lixuguang
    未经授权,严禁转载。如需转载,请联系作者

    +]]>
    + + 前端开发规范 + + + GIT + +
    + + Hexo在Github发布之后自定义域名的配置 + /2022/06/09/Hexo%E5%9C%A8Github%E5%8F%91%E5%B8%83%E4%B9%8B%E5%90%8E%E8%87%AA%E5%AE%9A%E4%B9%89%E5%9F%9F%E5%90%8D%E7%9A%84%E9%85%8D%E7%BD%AE/ + +

    作者:李旭光
    引用请标明出处

    + +

    HexoGithub发布之后自定义域名的配置

    进到你的博客发布所在仓库,如:

    选择Settings,然后是pages,滚动页面找到Custom domain,将你自己的域名绑定进去保存一下就好了。

    +

    在你自己的域名商处用CNAME把你自己的域名指定到你自己的仓库page地址上比如我的就是lixuguang.github.io Public,这样你的GitHub Page就可以通过自己的域名进行访问了。

    +

    HexoGithub发布之后自定义域名会被清空的问题

    使用 GitHub Page 功能将博客托管在了 GitHub 上,并配置 CNAME 将自己的域名解析了过去,但是发现一个问题,每次 hexo deploy 之后,custom domain 会被重置失效。

    +

    解决方案

    Hexo 生成的博客的 source 目录下(注意这个是你的博客源码所在仓库,并不是发布的仓库)新建一个 CNAME 文件,然后在这个文件中填入你的域名,这样就不会每次发布之后,GitHub Page 里的 custom domain 都被重置掉啦。

    +]]>
    + + 杂七杂八 + 博客技巧 + + + Hexo + +
    + + Linux目录说明 + /2020/08/02/Linux%E7%9B%AE%E5%BD%95%E8%AF%B4%E6%98%8E/ + +

    作者:李旭光
    引用请标明出处

    + +

    /bin:

    bin是Binary的缩写, 这个目录存放着最经常使用的命令。

    +

    /boot:

    这里存放的是启动Linux时使用的一些核心文件,包括一些连接文件以及镜像文件。

    +

    /dev :

    dev是Device(设备)的缩写, 该目录下存放的是Linux的外部设备,在Linux中访问设备的方式和访问文件的方式是相同的。

    +

    /etc:

    这个目录用来存放所有的系统管理所需要的配置文件和子目录。

    +

    /home:

    用户的主目录,在Linux中,每个用户都有一个自己的目录,一般该目录名是以用户的账号命名的。

    +

    /lib:

    这个目录里存放着系统最基本的动态连接共享库,其作用类似于Windows里的DLL文件。几乎所有的应用程序都需要用到这些共享库。

    +

    /lost+found:

    这个目录一般情况下是空的,当系统非法关机后,这里就存放了一些文件。

    +

    /media linux

    系统会自动识别一些设备,例如U盘、光驱等等,当识别后,linux会把识别的设备挂载到这个目录下。

    +

    /mnt:

    系统提供该目录是为了让用户临时挂载别的文件系统的,我们可以将光驱挂载在/mnt/上,然后进入该目录就可以查看光驱里的内容了。

    +

    /opt:

    这是给主机额外安装软件所摆放的目录。比如你安装一个ORACLE数据库则就可以放到这个目录下。默认是空的。

    +

    /proc:

    这个目录是一个虚拟的目录,它是系统内存的映射,我们可以通过直接访问这个目录来获取系统信息。
    这个目录的内容不在硬盘上而是在内存里,我们也可以直接修改里面的某些文件,比如可以通过下面的命令来屏蔽主机的ping命令,使别人无法ping你的机器:

    +

    /root:

    该目录为系统管理员,也称作超级权限者的用户主目录。

    +

    /sbin:

    s就是Super User的意思,这里存放的是系统管理员使用的系统管理程序。

    +

    /selinux:

    这个目录是Redhat/CentOS所特有的目录,Selinux是一个安全机制,类似于windows的防火墙,但是这套机制比较复杂,这个目录就是存放selinux相关的文件的。

    +

    /srv:

    该目录存放一些服务启动之后需要提取的数据。

    +

    /sys:

    这是linux2.6内核的一个很大的变化。该目录下安装了2.6内核中新出现的一个文件系统 sysfs 。

    +

    sysfs文件系统集成了下面3种文件系统的信息:针对进程信息的proc文件系统、针对设备的devfs文件系统以及针对伪终端的devpts文件系统。
    该文件系统是内核设备树的一个直观反映。

    +

    当一个内核对象被创建的时候,对应的文件和目录也在内核对象子系统中被创建。

    +

    /tmp:

    这个目录是用来存放一些临时文件的。

    +

    /usr:

    这是一个非常重要的目录,用户的很多应用程序和文件都放在这个目录下,类似与windows下的program files目录。

    +

    /usr/bin:

    系统用户使用的应用程序。

    +

    /usr/sbin:

    超级用户使用的比较高级的管理程序和系统守护程序。

    +

    /usr/src:

    内核源代码默认的放置目录。

    +

    /var:

    这个目录中存放着在不断扩充着的东西,我们习惯将那些经常被修改的目录放在这个目录下。包括各种日志文件。

    +

    在linux系统中,有几个目录是比较重要的,平时需要注意不要误删除或者随意更改内部文件。
    系统上跑了很多程序,那么每个程序都会有相应的日志产生,而这些日志就被记录到这个目录下,具体在/var/log 目录下,另外mail的预设放置也是在这里。

    +

    /etc:

    上边也提到了,这个是系统中的配置文件,如果你更改了该目录下的某个文件可能会导致系统不能启动。

    +

    /bin, /sbin, /usr/bin, /usr/sbin:

    这是系统预设的执行文件的放置目录,比如 ls 就是在/bin/ls 目录下的。

    +

    值得提出的是,/bin, /usr/bin 是给系统用户使用的指令(除root外的普通用户),而/sbin, /usr/sbin 则是给root使用的指令。

    +]]>
    + + Linux + + + Linux + +
    + + 在Docker Compose中配置网络 + /2022/08/02/Network%20in%20Compose%20%E5%9C%A8Docker%20Compose%E4%B8%AD%E9%85%8D%E7%BD%AE%E7%BD%91%E7%BB%9C/ + +

    作者:李旭光
    引用请标明出处

    + +

    Network in Compose 在Docker Compose中配置网络

    version: "3.9"

    services:
    proxy:
    build: ./proxy
    networks:
    - actsnetwork
    app:
    build: ./app
    networks:
    - actsnetwork
    db:
    image: postgres
    networks:
    - testnetwork

    networks:
    actsnetwork:
    name: testnetwork
    ]]>
    + + 虚拟化 + Docker Compose + + + Docker Compose + +
    + + Nginx中的超时设置 + /2022/08/02/Nginx%E4%B8%AD%E7%9A%84%E8%B6%85%E6%97%B6%E8%AE%BE%E7%BD%AE/ + +

    作者:李旭光
    引用请标明出处

    + +

    Nginx中的超时设置

    client_header_timeout 指定等待client发送一个请求头的超时时间

      +
    • 语法: client_header_timeout time
    • +
    • 默认值: 60s
    • +
    • 上下文: http server
    • +
    • 说明: 指定等待client发送一个请求头的超时时间(例如:GET / HTTP/1.1).仅当在一次read中,没有收到请求头,才会算成超时。如果在超时时间内,client没发送任何东西,nginx返回HTTP状态码408(“Request timed out”)
    • +
    +

    client_body_timeout 该指令设置请求体(request body)的读超时时间

      +
    • 语法: client_body_timeout time
    • +
    • 默认值: 60s
    • +
    • 上下文: http server location
    • +
    • 说明: 该指令设置请求体(request body)的读超时时间。仅当在一次readstep中,没有得到请求体,就会设为超时。超时后,nginx返回HTTP状态码408(“Request timed out”)
    • +
    +

    keepalive_timeout 第一个参数指定了与client的keep-alive连接超时时间

      +
    • 语法: keepalive_timeout timeout [ header_timeout ]

      +
    • +
    • 默认值: 75s

      +
    • +
    • 上下文: http server location

      +
    • +
    • 说明: 第一个参数指定了与client的keep-alive连接超时时间。服务器将会在这个时间后关闭连接。可选的第二个参数指定了在响应头Keep-Alive: timeout=time中的time值。这个头能够让一些浏览器主动关闭连接,这样服务器就不必要去关闭连接了。没有这个参数,nginx不会发送Keep-Alive响应头(尽管并不是由这个头来决定连接是否“keep-alive”)
      两个参数的值可并不相同

      +
        +
      • 注意不同浏览器怎么处理“keep-alive”头
      • +
      • MSIE和Opera忽略掉”Keep-Alive: timeout=“ header.
      • +
      • MSIE保持连接大约60-65秒,然后发送TCP RST
      • +
      • Opera永久保持长连接
      • +
      • Mozilla keeps the connection alive for N plus about 1-10 seconds.
      • +
      • Konqueror保持长连接N秒
      • +
      +
    • +
    +

    lingering_timeout

      +
    • 语法: lingering_timeout time
    • +
    • 默认值: 5s
    • +
    • 上下文: http server location
    • +
    • 说明: lingering_close生效后,在关闭连接前,会检测是否有用户发送的数据到达服务器,如果超过lingering_timeout时间后还没有数据可读,就直接关闭连接;否则,必须在读取完连接缓冲区上的数据并丢弃掉后才会关闭连接。
    • +
    +

    resolver_timeout 设置DNS解析超时时间

      +
    • 语法 resolver_timeout time
    • +
    • 默认值 30s
    • +
    • 上下文 http server location
    • +
    • 说明 该指令设置DNS解析超时时间
    • +
    +

    proxy_connect_timeout 设置与upstream server的连接超时时间,不能超过75秒

      +
    • 语法 proxy_connect_timeout time
    • +
    • 默认值 60s
    • +
    • 上下文 http server location
    • +
    • 说明 该指令设置与upstream server的连接超时时间,有必要记住,这个超时不能超过75秒。
      这个不是等待后端返回页面的时间,那是由proxy_read_timeout声明的。如果你的upstream服务器起来了,但是hanging住了(例如,没有足够的线程处理请求,所以把你的请求放到请求池里稍后处理),那么这个声明是没有用的,由于与upstream服务器的连接已经建立了。
    • +
    +

    proxy_read_timeout 设置与代理服务器的读超时时间

      +
    • 语法 proxy_read_timeout time
    • +
    • 默认值 60s
    • +
    • 上下文 http server location
    • +
    • 说明 该指令设置与代理服务器的读超时时间。它决定了nginx会等待多长时间来获得请求的响应。这个时间不是获得整个response的时间,而是两次reading操作的时间。
    • +
    +

    proxy_send_timeout 设置发送请求给upstream服务器的超时时间

      +
    • 语法 proxy_send_timeout time
    • +
    • 默认值 60s
    • +
    • 上下文 http server location
    • +
    • 说明 这个指定设置了发送请求给upstream服务器的超时时间。超时设置不是为了整个发送期间,而是在两次write操作期间。如果超时后,upstream没有收到新的数据,nginx会关闭连接
    • +
    +

    proxy_upstream_fail_timeout(fail_timeout)设置某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间

      +
    • 语法 server address [fail_timeout=30s]
    • +
    • 默认值 10s
    • +
    • 上下文 upstream
    • +
    • 说明 Upstream模块下 server指令的参数,设置了某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间,默认为10秒
    • +
    +]]>
    + + Web服务器 + Nginx + + + 超时设置 + +
    + + Nginx指令 + /2022/06/09/Nginx%E6%8C%87%E4%BB%A4/ + +

    作者:李旭光
    引用请标明出处

    + +

    Nginx指令

    #启动Nginx
    start nginx

    #重启Nginx
    nginx -s reopen

    #重新加载Nginx配置文件,然后以优雅的方式重启Nginx
    nginx -s reload

    #强制停止Nginx服务
    nginx -s stop

    #优雅地停止Nginx服务(即处理完所有请求后再停止服务)
    nginx -s quit

    #检测配置文件是否有语法错误,然后退出
    nginx -t

    #显示版本信息并退出
    nginx -v

    #显示版本和配置选项信息,然后退出
    nginx -V

    #检测配置文件是否有语法错误,然后退出
    nginx -t

    #检测配置文件是否有语法错误,转储并退出
    nginx -T

    #在检测配置文件期间屏蔽非错误信息
    nginx -q

    #打开帮助信息
    nginx -?,-h

    #设置前缀路径(默认是:/usr/share/nginx/)
    nginx -p prefix

    #设置配置文件(默认是:/etc/nginx/nginx.conf)
    nginx -c filename

    #设置配置文件外的全局指令
    nginx -g directives
    +]]>
    + + Infra + Web服务器 + Nginx + + + Nginx + +
    + + 在 Oracle Linux 中安装 Docker + /2022/08/02/Oracle-Linux-install-Docker/ + +

    作者:李旭光
    引用请标明出处

    + +

    Oracle Linux install Docker(https://docs.docker.com/engine/install/centos/)

    # 查看
    systemctl list-units

    sudo yum remove docker \
    docker-client \
    docker-client-latest \
    docker-common \
    docker-latest \
    docker-latest-logrotate \
    docker-logrotate \
    docker-engine

    sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

    sudo yum install -y yum-utils

    sudo yum -y install docker-ce-20.10.17 docker-ce-cli-20.10.17 containerd.io-1.6.6

    sudo vi /etc/yum.repos.d/docker-ce.repo

    [centos-extras]
    name=Centos extras - $basearch
    baseurl=http://mirror.centos.org/centos/7/extras/x86_64
    enabled=1
    gpgcheck=1
    gpgkey=http://centos.org/keys/RPM-GPG-KEY-CentOS-7

    sudo yum -y install fuse-overlayfs slirp4netns

    # sudo chmod 777 docker-ce.repo

    # yum list docker-ce --showduplicates | sort -r

    sudo systemctl start docker

    sudo docker run hello-world

    sudo yum install /path/to/package.rpm
    sudo systemctl start docker
    sudo docker run hello-world
    ]]>
    + + Linux + Oracle + Docker + + + Docker + +
    + + OracleLinux内核从uek切换到uek + /2022/08/02/OracleLinux%E5%86%85%E6%A0%B8%E4%BB%8Euek%E5%88%87%E6%8D%A2%E5%88%B0uek/ +
    #/boot/grub/grub.conf 缺失:

    yum install -y grub
    grub-mkconfig -o /boot/grub/grub.conf

    #/boot/grub2/grub.cfg 缺失:

    yum install -y grub2
    grub2-mkconfig -o /boot/grub2/grub.cfg

    uname -a
    sudo grep ^menuentry /boot/grub2/grub.cfg | awk -F\' '{print $2}' | nl -v0

    sudo grub2-editenv list

    sudo grep GRUB_DEFAULT /etc/default/grub

    sudo vim /etc/default/grub

    sudo grub2-set-default 1

    sudo grub2-mkconfig -o /boot/grub2/grub.cfg

    sudo dracut --force

    sudo reboot

    uname -a
    ]]>
    + + OracleLinux + + + kernel + +
    + + Postgresql绿色版使用 + /2022/08/02/Postgresql%E7%BB%BF%E8%89%B2%E7%89%88%E4%BD%BF%E7%94%A8/ + 初始化数据库

    c:\pgsql\bin\initdb.exe -D c:\pgsql\data -E UTF8

    +

    注册表

    c:\pgsql\bin\pg_ctl register -N postgres -D c:\pgsql\data

    +

    启动服务

    c:\pgsql\bin\postgres -D c:\pgsql\data

    +]]>
    + + Database + Postgresql + + + Postgresql + +
    + + Restful API 的设计规范.md + /2022/06/01/Restful%20API%20%E7%9A%84%E8%AE%BE%E8%AE%A1%E8%A7%84%E8%8C%83/ + +

    作者:李旭光
    引用请标明出处

    + +

    1. URI

    URI 表示资源,资源一般对应服务器端领域模型中的实体类。

    +

    URI规范

      +
    1. 不用大写;
    2. +
    3. 用中杠-不用下杠_;
    4. +
    5. 参数列表要encode;
    6. +
    7. URI中的名词表示资源集合,使用复数形式。
    8. +
    +

    资源集合 vs 单个资源

    URI表示资源的两种方式:资源集合、单个资源。

    +

    资源集合:

    +
    /zoos //所有动物园
    /zoos/1/animals //id为1的动物园中的所有动物
    +

    单个资源:

    +
    /zoos/1 //id为1的动物园
    /zoos/1;2;3 //id为1,2,3的动物园
    +

    避免层级过深的URI

    /在url中表达层级,用于按实体关联关系进行对象导航,一般根据id导航。

    +

    过深的导航容易导致url膨胀,不易维护,如 GET /zoos/1/areas/3/animals/4,尽量使用查询参数代替路径中的实体导航,如GET /animals?zoo=1&area=3

    +

    对Composite资源的访问

    服务器端的组合实体必须在uri中通过父实体的id导航访问。

    +
    +

    组合实体不是first-class的实体,它的生命周期完全依赖父实体,无法独立存在,在实现上通常是对数据库表中某些列的抽象,不直接对应表,也无id。一个常见的例子是 User — Address,Address是对User表中zipCode/country/city三个字段的简单抽象,无法独立于User存在。必须通过User索引到Address:GET /user/1/addresses

    +
    +

    2. Request

    HTTP方法

    通过标准HTTP方法对资源CRUD:

    +

    GET:查询

    +
    GET /zoos
    GET /zoos/1
    GET /zoos/1/employees
    + + +

    POST:创建单个资源。POST一般向“资源集合”型uri发起

    +
    POST /animals  //新增动物
    POST /zoos/1/employees //为id为1的动物园雇佣员工
    + +

    PUT:更新单个资源(全量),客户端提供完整的更新后的资源。与之对应的是 PATCH,PATCH 负责部分更新,客户端提供要更新的那些字段。PUT/PATCH一般向“单个资源”型uri发

    +
    PUT /animals/1
    PUT /zoos/1
    + +

    DELETE:删除

    +
    DELETE /zoos/1/employees/2
    DELETE /zoos/1/employees/2;4;5
    DELETE /zoos/1/animals //删除id为1的动物园内的所有动物
    +

    HEAD / OPTION 用的不多,就不多解释了。

    +

    安全性和幂等性

      +
    1. 安全性:不会改变资源状态,可以理解为只读的;
    2. +
    3. 幂等性:执行1次和执行N次,对资源状态改变的效果是等价的。
    4. +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    .安全性幂等性
    GET
    POST××
    PUT×
    DELETE×
    +

    安全性和幂等性均不保证反复请求能拿到相同的response。以 DELETE 为例,第一次DELETE返回200表示删除成功,第二次返回404提示资源不存在,这是允许的。

    +

    复杂查询

    查询可以捎带以下参数:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    .示例备注
    过滤条件type=1&age=16允许一定的uri冗余,如/zoos/1与/zoos?id=1
    排序?sort=age,desc
    投影?whitelist=id,name,email
    分页?limit=10&offset=3
    +

    Bookmarker

    经常使用的、复杂的查询标签化,降低维护成本。

    +

    如:

    +
    GET /trades?status=closed&sort=created,desc

    +

    快捷方式:

    +
    GET /trades#recently-closed
    // 或者
    GET /trades/recently-closed
    + +

    Format

    只用以下常见的3种body format:

    +
      +
    1. Content-Type: application/json
    2. +
    +
    POST /v1/animal HTTP/1.1
    Host: api.example.org
    Accept: application/json
    Content-Type: application/json
    Content-Length: 24

    {
    "name": "Gir",
    "animalType": "12"
    }
    + +
      +
    1. Content-Type: application/x-www-form-urlencoded (浏览器POST表单用的格式)
    2. +
    +
    POST /login HTTP/1.1
    Host: example.com
    Content-Length: 31
    Accept: text/html
    Content-Type: application/x-www-form-urlencoded

    username=root&password=Zion0101
    + +
      +
    1. Content-Type: multipart/form-data; boundary=—-RANDOM_jDMUxq4Ot5 (表单有文件上传时的格式)
    2. +
    +

    6. Response

      +
    1. 不要包装:
      response 的 body 直接就是数据,不要做多余的包装。
    2. +
    +

    错误示例

    +
    {
    "success":true,
    "data":{"id":1,"name":"xiaotuan"},
    }
    +

    各HTTP方法成功处理后的数据格式:

    + + + + + + + + + + + + + + + + + + + + + + + +
    ·response 格式
    GET单个对象、集合
    POST新增成功的对象
    PUT/PATCH更新成功的对象
    DELETE
    +
      +
    1. json格式的约定:

      +
        +
      1. 时间用长整形(毫秒数),客户端自己按需解析(moment.js)
      2. +
      3. 不传null字段
      4. +
      +
    2. +
    +

    分页response

    {
    "paging":{"limit":10,"offset":0,"total":729},
    "data":[{},{},{}...]
    }
    +

    7. 错误处理

      +
    1. 不要发生了错误但给2xx响应,客户端可能会缓存成功的http请求;
    2. +
    3. 正确设置http状态码,不要自定义;
    4. +
    5. Response body 提供
        +
      1. 错误的代码(日志/问题追查);
      2. +
      3. 错误的描述文本(展示给用户)。
      4. +
      +
    6. +
    +

    对第三点的实现稍微多说一点:

    +

    Java 服务器端一般用异常表示 RESTful API 的错误。API 可能抛出两类异常:业务异常非业务异常

    +

    业务异常由自己的业务代码抛出,表示一个用例的前置条件不满足、业务规则冲突等,比如参数校验不通过、权限校验失败。

    +

    非业务类异常表示不在预期内的问题,通常由类库、框架抛出,或由于自己的代码逻辑错误导致,比如数据库连接失败、空指针异常、除0错误等等。

    +

    业务类异常必须提供2种信息:

    +
      +
    1. 如果抛出该类异常,HTTP 响应状态码应该设成什么;
    2. +
    3. 异常的文本描述;
    4. +
    +

    在Controller层使用统一的异常拦截器:

    +
      +
    1. 设置 HTTP 响应状态码:对业务类异常,用它指定的 HTTP code;对非业务类异常,统一500;
    2. +
    3. Response Body 的错误码:异常类名
    4. +
    5. Response Body 的错误描述:对业务类异常,用它指定的错误文本;对非业务类异常,线上可以统一文案如“服务器端错误,请稍后再试”,开发或测试环境中用异常的 stacktrace,服务器端提供该行为的开关。
    6. +
    +

    常用的http状态码及使用场景:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    状态码使用场景
    400 bad request常用在参数校验
    401 unauthorized未经验证的用户,常见于未登录。如果经过验证后依然没权限,应该 403(即 authentication 和 authorization 的区别)。
    403 forbidden无权限
    404 not found资源不存在
    500 internal server error非业务类异常
    503 service unavaliable由容器抛出,自己的代码不要抛这个异常
    +

    8. 服务型资源

    除了资源简单的CRUD,服务器端经常还会提供其他服务,这些服务无法直接用上面提到的URI映射。如:

    +
      +
    1. 按关键字搜索;
    2. +
    3. 计算地球上两点间的距离;
    4. +
    5. 批量向用户推送消息;
    6. +
    +

    可以把这些服务看成资源,计算的结果是资源的presentation,按服务属性选择合适的HTTP方法。

    +

    例:

    +
    GET /search?q=filter?category=file  搜索
    GET /distance-calc?lats=47.480&lngs=-122.389&late=37.108&lnge=-122.448
    POST /batch-publish-msg
    [{"from":0,"to":1,"text":"abc"},{},{}...]
    +

    9. 异步任务

    对耗时的异步任务,服务器端接受客户端传递的参数后,应返回创建成功的任务资源,其中包含了任务的执行状态。客户端可以轮训该任务获得最新的执行进度。

    +
    // 提交任务:
    POST /batch-publish-msg
    [{"from":0,"to":1,"text":"abc"},{},{}...]

    // 返回:
    {"taskId":3,"createBy":"Anonymous","status":"running"}

    GET /task/3
    {"taskId":3,"createBy":"Anonymous","status":"success"}
    + +

    如果任务的执行状态包括较多信息,可以把“执行状态”抽象成组合资源,客户端查询该状态资源了解任务的执行情况。

    +
    // 提交任务:
    POST /batch-publish-msg
    [{"from":0,"to":1,"text":"abc"},{},{}...]

    // 返回:
    {"taskId":3,"createBy":"Anonymous"}

    GET /task/3/status
    {"progress":"50%","total":18,"success":8,"fail":1}
    + +

    10. API的演进

    版本

    常见的三种方式:

    +
      +
    1. 在uri中放版本信息:GET /v1/users/1
    2. +
    3. Accept Header:Accept: application/json+v1
    4. +
    5. 自定义 Header:X-Api-Version: 1
    6. +
    +

    用第一种,虽然没有那么优雅,但最明显最方便。

    +

    URI失效

    随着系统发展,总有一些API失效或者迁移,对失效的API,返回404 not found 或 410 gone;对迁移的API,返回 301 重定向。

    +

    11. 安全

    这个不熟,接触到的时候再说。

    +

    参考文档

      +
    • < RESTful Web Services Cookbook >
    • +
    • Consumer-Centric API Design
    • +
    • RESTful Best Practices
    • +
    +]]>
    + + 前端开发规范 + + + Restful API + +
    + + http_stub_status_module Nginx的性能模块 + /2022/06/09/http_stub_status_module/ + ngx_http_stub_status_module 是一个 Nginx 的内置 模块,可以提供 Nginx 的简单统计信息。默认情况下这个模块是不被编译进来的,所以在编译 Nginx 时要指定加载该模块:

    +
    --with-http_stub_status_module
    +

    查看是否安装

    // 查看nginx版本及安装了那些模块
    nginx -V

    // 如果安装了的话会显示下面的信息
    --with-http_stub_status_module
    + +

    配置使用

    nginx.conf配置文件中进行如下配置

    +
    location /ngx_status {
    stub_status on; // 设置开启性能模块
    }
    +

    通过浏览器访问 http://localhost/ngx_status 就能看到你的nginx服务性能监控画面了
    image

    +

    解析

      +
    • 第一行:Active connections:活动连接数
    • +
    • 第二三行:Server accepts handled requests:(三个数字分别代表)总共处理的连接数,成功握手的连接数量,处理的请求数(正常情况下握手和连接数是相等的,表示没有丢失)
    • +
    • 第四行:
        +
      • Reading: Nginx 读取到客户端的Header信息数
      • +
      • Writing: Nginx 返回给客户端的Header信息数
      • +
      • Waiting: 开启keep-alive的情况下,这个值等于 active – (reading + writing),意思就是Nginx已经处理完成,正在等候下一次请求指令的驻留连接(在nginx开启了keep-alive,也就是长连接的情况下,客户端跟服务端建立了连接但是没有读写操作的空闲状态)
      • +
      +
    • +
    +

    使用场景

      +
    • 可以简单的用脚本做监控
    • +
    • 可以用开源工具,zabbix,prometheus等去采集nginx的统计信息,做监控和历史数据采集
    • +
    +]]>
    + + Infra + Web服务器 + Nginx + + + Nginx + +
    + + 在linux之间传输文件的命令scp + /2022/08/02/linux-scp/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    这里假设要从Linux服务器A中把文件 file.txt 传输到Linux服务器B,该怎么做,这里就可以用scp实现
    为了能够成功的传输文件,需要做到以下几点:

    +
      +
    1. A需要知道要传输的的文件本地的路径
    2. +
    3. A为了往B传输文件,所以需要知道B的访问权限,因此需要B的访问地址,账号和私钥
    4. +
    5. 目标路径
    6. +
    7. 因为Linux权限管理的要求,私钥的访问权限需要设定为600
    8. +
    +

    当然还有一点前提是A和B两台服务器之间本身是可以通信的
    知道以上信息便可以进行文件传输

    +

    实践

    现在假设

    +
      +
    1. A要往B传输的文件在A的如下目录:/tmp/transfer/file.txt
    2. +
    3. B的访问地址是 192.168.31.31 访问用户名是 testuser 用到的key上传到了A的如下目录: /tmp/ssh/test.key
    4. +
    5. 目标路径为B的如下目录: /tmp/test
      知道上面信息后我们来创建命令。
    6. +
    +
    chomd 600 /tmp/ssh/test.key
    scp -i /tmp/ssh/test.key /tmp/transfer/file.txt testuser@192.168.31.31 /tmp/test
    + +

    通过上面的命令即可实现从A服务器传输文件到B服务器了。

    +]]>
    + + Linux + + + scp + +
    + + nginx中的超时设置 + /2022/06/30/nginx-timeout/ + +

    作者:李旭光
    引用请标明出处

    + +

    nginx中的超时设置

    Nginx主要有四类超时设置:客户端超时设置DNS解析超时设置代理超时设置,如果使用ngx_lua,则还有lua相关的超时设置。

    +

    客户端超时设置

    对于客户端超时主要设置有读取请求头超时时间读取请求体超时时间发送响应超时时间长连接超时时间。通过客户端超时设置避免客户端恶意或者网络状况不佳造成连接长期占用,影响服务端的可处理的能力。

    +

    client_header_timeout 指定等待client发送一个请求头的超时时间

      +
    • 语法: client_header_timeout time
    • +
    • 默认值: 60s
    • +
    • 上下文: http server
    • +
    • 说明: 指定等待client发送一个请求头的超时时间(例如:GET / HTTP/1.1).仅当在一次read中,没有收到请求头,才会算成超时。如果在超时时间内,client没发送任何东西,nginx返回HTTP状态码408(“Request timed out”)
    • +
    +

    client_body_timeout 该指令设置请求体(request body)的读超时时间

      +
    • 语法: client_body_timeout time
    • +
    • 默认值: 60s
    • +
    • 上下文: http server location
    • +
    • 说明: 该指令设置请求体(request body)的读超时时间。仅当在一次readstep中,没有得到请求体,就会设为超时。超时后,nginx返回HTTP状态码408(“Request timed out”)
    • +
    +

    send_timeout time

    设置发送响应到客户端的超时时间,默认为60s,此超时时间指的也是两次成功写操作间隔时间,而不是发送整个响应的超时时间。如果在此超时时间内客户端没有接收任何响应,则Nginx关闭此连接。

    +

    keepalive_timeout 第一个参数指定了与client的keep-alive连接超时时间

      +
    • 语法: keepalive_timeout timeout [ header_timeout ]

      +
    • +
    • 默认值: 75s

      +
    • +
    • 上下文: http server location

      +
    • +
    • 说明: 第一个参数指定了与client的keep-alive连接超时时间。服务器将会在这个时间后关闭连接。可选的第二个参数指定了在响应头Keep-Alive: timeout=time中的time值。这个头能够让一些浏览器主动关闭连接,这样服务器就不必要去关闭连接了。没有这个参数,nginx不会发送Keep-Alive响应头(尽管并不是由这个头来决定连接是否“keep-alive”)
      两个参数的值可并不相同

      +
        +
      • 注意不同浏览器怎么处理“keep-alive”头
      • +
      • MSIE和Opera忽略掉”Keep-Alive: timeout=“ header.
      • +
      • MSIE保持连接大约60-65秒,然后发送TCP RST
      • +
      • Opera永久保持长连接
      • +
      • Mozilla keeps the connection alive for N plus about 1-10 seconds.
      • +
      • Konqueror保持长连接N秒
      • +
      +
    • +
    +

    DNS解析超时设置

    resolver_timeout 设置DNS解析超时时间

      +
    • 语法 resolver_timeout time
    • +
    • 默认值 30s
    • +
    • 上下文 http server location
    • +
    • 说明 该指令设置DNS解析超时时间
    • +
    +

    代理超时设置

    proxy_connect_timeout 设置与upstream server的连接超时时间,不能超过75秒

      +
    • 语法 proxy_connect_timeout time
    • +
    • 默认值 60s
    • +
    • 上下文 http server location
    • +
    • 说明 该指令设置与upstream server的连接超时时间,有必要记住,这个超时不能超过75秒。
      这个不是等待后端返回页面的时间,那是由proxy_read_timeout声明的。如果你的upstream服务器起来了,但是hanging住了(例如,没有足够的线程处理请求,所以把你的请求放到请求池里稍后处理),那么这个声明是没有用的,由于与upstream服务器的连接已经建立了。
    • +
    +

    proxy_read_timeout 设置与代理服务器的读超时时间

      +
    • 语法 proxy_read_timeout time
    • +
    • 默认值 60s
    • +
    • 上下文 http server location
    • +
    • 说明 该指令设置与代理服务器的读超时时间。它决定了nginx会等待多长时间来获得请求的响应。这个时间不是获得整个response的时间,而是两次reading操作的时间。
    • +
    +

    proxy_send_timeout 设置发送请求给upstream服务器的超时时间

      +
    • 语法 proxy_send_timeout time
    • +
    • 默认值 60s
    • +
    • 上下文 http server location
    • +
    • 说明 这个指定设置了发送请求给upstream服务器的超时时间。超时设置不是为了整个发送期间,而是在两次write操作期间。如果超时后,upstream没有收到新的数据,nginx会关闭连接
    • +
    +

    失败重试机制设置。

    proxy_next_upstream error | timeout | invalid_header | http_500 | http_502 | http_503 | http_504 |http_403 | http_404 | non_idempotent | off …:配置什么情况下需要请求下一台上游服务器进行重试。默认为“errortimeout”。error表示与上游服务器建立连接、写请求或者读响应头出错。timeout表示与上游服务器建立连接、写请求或者读响应头超时。invalid_header表示上游服务器返回空的或错误的响应头。http_XXX表示上游服务器返回特定的状态码。non_idempotent表示RFC-2616定义的非幂等HTTP方法(POST、LOCK、PATCH),也可以在失败后重试下一台上游服务器(即默认幂等方法GET、HEAD、PUT、DELETE、OPTIONS、TRACE才可以重试)。off表示禁用重试。

    +

    重试不能无限制进行,因此,需要如下两个指令控制重试次数和重试超时时间。

    +

    proxy_next_upstream_tries number:设置重试次数,默认0表示不限制,注意此重试次数指的是所有请求次数(包括第一次和之后的重试次数之和)。

    +

    proxy_next_upstream_timeout time:设置重试最大超时时间,默认0表示不限制。

    +

    即在proxy_next_upstream_timeout时间内允许proxy_next_upstream_tries次重试。如果超过了其中一个设置,则Nginx也会结束重试并返回客户端响应(可能是错误码)。

    +

    如下配置表示当error/timeout时重试upstream中的下一台上游服务器,如果重试的总时间超出了6s或者重试了1次,则表示重试失败(因为之前已经请求一次了,所以还能重试一次),Nginx结束重试并返回客户端响应。

    +

    upstream存活超时设置

    max_fails和fail_timeout:配置什么时候Nginx将上游服务器认定为不可用/不存活。当上游服务器在fail_timeout时间内失败了max_fails次,则认为该上游服务器不可用/不存活。并在接下来的fail_timeout时间内从upstream摘掉该节点(即请求不会转发到该上游服务器)。

    +

    什么情况下被认定为失败呢?其由 proxy_next_upstream定义,不过,不管 proxy_next_upstream如何配置,error, timeout and invalid_header 都将被认为是失败。

    +

    如server 192.168.61.1:9090max_fails=2 fail_timeout=10s;表示在10s内如果失败了2次,则在接下来的10s内认定该节点不可用/不存活。这种存活检测机制是只有当访问该上游服务器时,采取惰性检查,可以使用ngx_http_upstream_check_module配置主动检查。

    +

    max_fails设置为0表示不检查服务器是否可用(即认为一直可用),如果upstream中仅剩一台上游服务器时,则该服务器是不会被摘除的,将从不被认为不可用。

    +

    proxy_upstream_fail_timeout(fail_timeout)设置某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间

      +
    • 语法 server address [fail_timeout=30s]
    • +
    • 默认值 10s
    • +
    • 上下文 upstream
    • +
    • 说明 Upstream模块下 server指令的参数,设置了某一个upstream后端失败了指定次数(max_fails)后,该后端不可操作的时间,默认为10秒
    • +
    +

    lingering_timeout

      +
    • 语法: lingering_timeout time
    • +
    • 默认值: 5s
    • +
    • 上下文: http server location
    • +
    • 说明: lingering_close生效后,在关闭连接前,会检测是否有用户发送的数据到达服务器,如果超过lingering_timeout时间后还没有数据可读,就直接关闭连接;否则,必须在读取完连接缓冲区上的数据并丢弃掉后才会关闭连接。
    • +
    +]]>
    + + Infra + Web服务器 + Nginx + + + Timeout + +
    + + rpm命令 + /2020/07/19/rpm%E5%91%BD%E4%BB%A4/ + +

    作者:李旭光
    引用请标明出处

    + +

    安装

    sudo rpm -ivh [lz4-1.8.3-1.el7.x86_64.rpm]

    +

    卸载

    sudo rpm -e [postgresql14-server-14.4-1PGDG.rhel7.x86_64]

    +

    查询功能

    命令格式 rpm {-q|–query} [select-options] [query-options]

    +

      RPM的查询功能是极为强大,是极为重要的功能之一;举几个常用的例子,更为详细的具体的,请参考#man rpm

    +

    1、对系统中已安装软件的查询

    +

    1)查询系统已安装的软件

    +

      语法:rpm -q 软件名

    +

      举例:[root@localhost beinan]# rpm -q gaim

    +

      gaim-1.3.0-1.fc4   

    +
       查看系统中所有已经安装的包,要加 -a 参数 ;
    +
    +

      [root@localhost RPMS]# rpm -qa

    +

      如果分页查看,再加一个管道 |和more命令;

    +

      [root@localhost RPMS]# rpm -qa |more

    +

      在所有已经安装的软件包中查找某个软件,比如说 gaim ;可以用 grep 抽取出来;

    +

      [root@localhost RPMS]# rpm -qa |grep gaim

    +

      上面这条的功能和 rpm -q gaim 输出的结果是一样的;

    +

    2)查询一个已经安装的文件属于哪个软件包

    +

      语法 rpm -qf 文件名

    +

      注:文件名所在的绝对路径要指出

    +

      举例:

    +

      [root@localhost RPMS]# rpm -qf /usr/lib/libacl.la

    +

      libacl-devel-2.2.23-8

    +

    3)查询已安装软件包都安装到何处

    +

      语法:rpm -ql 软件名 或 rpm rpmquery -ql 软件名

    +

      举例:

    +

      [root@localhost RPMS]# rpm -ql lynx

    +

      [root@localhost RPMS]# rpmquery -ql lynx

    +

    4)查询一个已安装软件包的信息

    +

      语法格式: rpm -qi 软件名

    +

      举例:

    +

      [root@localhost RPMS]# rpm -qi lynx

    +

    5)查看一下已安装软件的配置文件

    +

      语法格式:rpm -qc 软件名

    +

      举例:

    +

      [root@localhost RPMS]# rpm -qc lynx

    +

    6)查看一个已经安装软件的文档安装位置

    +

      语法格式: rpm -qd 软件名

    +

      举例:

    +

      [root@localhost RPMS]# rpm -qd lynx

    +

    7)查看一下已安装软件所依赖的软件包及文件

    +

      语法格式: rpm -qR 软件名

    +

      举例:

    +

      [root@localhost beinan]# rpm -qR rpm-python

    +

      查询已安装软件的总结:对于一个软件包已经安装,我们可以把一系列的参数组合起来用;比如 rpm -qil ;比如:

    +

      [root@localhost RPMS]# rpm -qil lynx

    +

    2、对于未安装的软件包的查看:

    +

      查看的前提是您有一个.rpm 的文件,也就是说对既有软件file.rpm的查看等;

    +

    1)查看一个软件包的用途、版本等信息;

    +

      语法: rpm -qpi file.rpm

    +

      举例:

    +

      [root@localhost RPMS]# rpm -qpi lynx-2.8.5-23.i386.rpm

    +

    2)查看一件软件包所包含的文件;

    +

      语法: rpm -qpl file.rpm

    +

      举例:

    +

      [root@localhost RPMS]# rpm -qpl lynx-2.8.5-23.i386.rpm

    +

    3)查看软件包的文档所在的位置;

    +

      语法: rpm -qpd file.rpm

    +

      举例:

    +

      [root@localhost RPMS]# rpm -qpd lynx-2.8.5-23.i386.rpm

    +

    4)查看一个软件包的配置文件;

    +

      语法: rpm -qpc file.rpm

    +

      举例:

    +

      [root@localhost RPMS]# rpm -qpc lynx-2.8.5-23.i386.rpm

    +

    5)查看一个软件包的依赖关系

    +

      语法: rpm -qpR file.rpm

    +

      举例:

    +

      [root@localhost archives]# rpm -qpR yumex_0.42-3.0.fc4_noarch.rpm

    +

      /bin/bash

    +

      /usr/bin/python

    +

      config(yumex) = 0.42-3.0.fc4

    +

      pygtk2

    +

      pygtk2-libglade

    +

      rpmlib(CompressedFileNames) <= 3.0.4-1

    +

      rpmlib(PayloadFilesHavePrefix) <= 4.0-1

    +

      usermode

    +

      yum >= 2.3.2

    +]]>
    + + Linux + RPM + + + RPM + +
    + + Squid 服务器学习笔记 + /2022/06/01/squid/ + +

    作者:李旭光
    引用请标明出处

    + +

    Squid 服务器学习笔记

    Squid 服务器介绍

    用来缓存前端资源的代理服务器,可以类比后端数据库缓存软件Redis。
    当用户访问目标资源时由Squid服务器进行判断如果有数据缓存直接将缓存数据返回,如果没有缓存,代理客户访问资源,获取到后返回给用户,当有其他用户访问相同资源时直接返回资源。

    +

    代理方式

      +
    1. 普通代理
    2. +
    3. 透明代理
    4. +
    5. 反向代理
    6. +
    +

    安装

    // 检查是否已经安装
    rpm -qa | grep squid
    // 安装软件
    rpm install squid
    // 设置开机启动
    chkconfig squid on
    // 启动服务
    service squid start
    // 查看服务状态(端口监听3128)
    netstat -anput |grap squid
    // 关闭服务
    service squid stop
    + +

    配置文件

    /etc/squid/squid.config

    +

    10.0.0.0/8

    +
    +

    扩展知识
    CIDR - 无类域间路由CIDR(Classless InterDomain Routing)
    ip每位占1个字节8位,00000000 ~ 11111111,也就是0 ~ 255,ip共有四个位数。
    10.0.0.0/8 的意思是 8位不可变,剩下的可变,8位这里的意思是说第一个IP段,那么也就是说IP范围是10.0.0.0 ~ 10.255.255.255 这样的一个ip范围。

    +
    +

    // 复制并重命名
    mv /etc/squid/squid.config{,.bak}
    // 删除文件中的注释和空行(只保留有效设定)
    awk ‘if($0!~ /^#/ && $0~ !/^$/print $0)’ /etc/squid/squid.config.bak > /etc/squid/squid.config

    +

    squid常用命令:
    /usr/local/squid/sbin/squid -z 初始化缓存空间
    /usr/local/squid/sbin/squid 启动
    /usr/local/squid/sbin/squid -k shutdown 停止
    /usr/local/squid/sbin/squid -k reconfigure 重新载入配置文件
    /usr/local/squid/sbin/squid -k rotate 轮循日志

    +
    #acl all src 0.0.0.0/0.0.0.0 and http_access allow all选项定义了一个访问控制列表。详细情况参见和Squid软件
    #携带的文档。这里的访问控制列表允许所有对代理服务的访问,因为这里该代理是加速web服务器。
    acl all src 0.0.0.0/0.0.0.0
    #允许所有IP访问
    acl manager proto http #manager url协议为http
    acl localhost src 127.0.0.1/255.255.255.255 #允午本机IP
    acl to_localhost dst 127.0.0.1 #允午目的地址为本机IP
    acl Safe_ports port 80 # 允许安全更新的端口为80
    acl CONNECT method CONNECT #请求方法以CONNECT
    http_access allow all #允许所有人使用该代理.因为这里是代理加速web服务器
    http_reply_access allow all #允许所有客户端使用该代理

    acl OverConnLimit maxconn 16 #限制每个IP最大允许16个连接,防止攻击
    http_access deny OverConnLimit

    icp_access deny all #禁止从邻居服务器缓冲内发送和接收ICP请求.
    miss_access allow all #允许直接更新请求
    ident_lookup_access deny all #禁止lookup检查DNS
    http_port 8080 transparent #指定Squid监听浏览器客户请求的端口号。

    hierarchy_stoplist cgi-bin ? #用来强制某些特定的对象不被缓存,主要是处于安全的目的。
    acl QUERY urlpath_regex cgi-bin \?
    cache deny QUERY

    cache_mem 1 GB #这是一个优化选项,增加该内存值有利于缓存。应该注意的是:
    #一般来说如果系统有内存,设置该值为(n/)3M。现在是3G 所以这里1G
    fqdncache_size 1024 #FQDN 高速缓存大小
    maximum_object_size_in_memory 2 MB #允许最大的文件载入内存

    memory_replacement_policy heap LFUDA #动态使用最小的,移出内存cache
    cache_replacement_policy heap LFUDA #动态使用最小的,移出硬盘cache

    cache_dir ufs /home/cache 5000 32 512 #高速缓存目录 ufs 类型使用的缓冲值最大允午1000MB空间,
    #32个一级目录,512个二级目录

    max_open_disk_fds 0 #允许最大打开文件数量,0 无限制
    minimum_object_size 1 KB #允午最小文件请求体大小
    maximum_object_size 20 MB #允午最大文件请求体大小

    cache_swap_low 90 #最小允许使用swap 90%
    cache_swap_high 95 #最多允许使用swap 95%

    ipcache_size 2048 # IP 地址高速缓存大小 2M
    ipcache_low 90 #最小允许ipcache使用swap 90%
    ipcache_high 95 #最大允许ipcache使用swap 90%


    access_log /var/log/squid/access.log squid #定义日志存放记录
    cache_log /var/log/squid/cache.log squid
    cache_store_log none #禁止store日志

    emulate_httpd_log on #将使Squid仿照Web服务器的格式创建访问记录。如果希望使用
    #Web访问记录分析程序,就需要设置这个参数。

    refresh_pattern . 0 20% 4320 override-expire override-lastmod reload-into-ims ignore-reload #更新cache规则

    acl buggy_server url_regex ^http://.... http:// #只允许http的请求
    broken_posts allow buggy_server

    acl apache rep_header Server ^Apache #允许apache的编码
    broken_vary_encoding allow apache

    request_entities off #禁止非http的标分准请求,防止攻击
    header_access header allow all #允许所有的http报头
    relaxed_header_parser on #不严格分析http报头.
    client_lifetime 120 minute #最大客户连接时间 120分钟

    cache_mgr sky@test.com #指定当缓冲出现问题时向缓冲管理者发送告警信息的地址信息。

    cache_effective_user squid #这里以用户squid的身份Squid服务器
    cache_effective_group squid

    icp_port 0 #指定Squid从邻居服务器缓冲内发送和接收ICP请求的端口号。
    #这里设置为0是因为这里配置Squid为内部Web服务器的加速器,
    #所以不需要使用邻居服务器的缓冲。0是禁用

    cache_peer 127.0.0.1 parent 80 0 no-query default multicast-responder no-netdb-exchange #cache_peer 设置允许更新缓存的主机,因是本机所以127.0.0.1
    cache_peer_domain 127.0.0.1
    hostname_aliases 127.0.0.1

    error_directory /usr/share/squid/errors/Simplify_Chinese #定义错误路径

    always_direct allow all # cache丢失或不存在是允许所有请求直接转发到原始服务器
    ignore_unknown_nameservers on #开反DNS查询,当域名地址不相同时候,禁止访问
    coredump_dir /var/log/squid #定义dump的目录

    max_filedesc 2048 #最大打开的文件描述

    half_closed_clients off #使Squid在当read不再返回数据时立即关闭客户端的连接。
    #有时read不再返回数据是由于某些客户关闭TCP的发送数据
    #而仍然保持接收数据。而Squid分辨不出TCP半关闭和完全关闭。

    buffered_logs on #若打开选项“buffered_logs”可以稍稍提高加速某些对日志文件的写入,该选项主要是实现优化特性。

    acl tianya referer_regex -i tianya #防止天涯盗链,转嫁给百度
    http_access deny tianya
    deny_info tianya

    acl baidu req_header User-Agent Baiduspider #阻止baidu蜘蛛
    http_access deny baidu

    acl OverConnLimit maxconn 128 #限制同一IP客户端的最大连接数
    http_access deny OverConnLimit

    acl myip dst 222.18.63.37 #防止被人利用为HTTP代理,设置允许访问的IP地址
    http_access deny !myip

    acl Manager proto cache_object #允许本地管理
    acl Localhost src 127.0.0.1 222.18.63.37
    http_access allow Manager Localhost
    cachemgr_passwd 53034338 all
    http_access deny Manager

    acl all src 0.0.0.0/0.0.0.0 #仅仅允许80端口的代理
    acl Safe_ports port 80 # http
    http_access deny !Safe_ports
    http_access allow all

    visible_hostname happy.swjtu.edu.cn #Squid信息设置
    cache_mgr ooopic2008@qq.com

    cache_effective_user squid #基本设置
    cache_effective_group squid
    tcp_recv_bufsize 65535 bytes

    cache_peer 127.0.0.1 parent 80 0 no-query originserver #2.6的反向代理加速配置

    error_directory /usr/local/squid/share/errors/Simplify_Chinese #错误文档

    icp_port 0 #单台使用,不使用该功能

    hierarchy_stoplist cgi-bin ?

    acl QUERY urlpath_regex cgi-bin \? .php .cgi .avi .wmv .rm .ram .mpg .mpeg .zip .exe
    cache deny QUERY

    acl apache rep_header Server ^Apache
    broken_vary_encoding allow apache


    refresh_pattern ^ftp: 1440 20% 10080
    refresh_pattern ^gopher: 1440 0% 1440
    refresh_pattern . 0 20% 4320

    cache_store_log none
    pid_filename /usr/local/squid/var/logs/squid.pid
    emulate_httpd_log on
    logformat combined %>a %ui %un [%tl] "%rm %ru HTTP/%rv" %Hs %<st "%{Referer}>h" "%{User-Agent}>h" %Ss:%Sh
    cache_log /usr/local/squid/var/logs/cache.log
    access_log /usr/local/squid/var/logs/access.log combined
    coredump_dir /usr/local/squid/var/cache
    cache_dir ufs /usr/local/squid/var/cache 10000 16 256

    dns_children 32
    hosts_file /etc/hosts

    cache_mem 400 MB
    cache_swap_low 90
    cache_swap_high 95
    maximum_object_size 32768 KB
    maximum_object_size_in_memory 4096 KB
    emulate_httpd_log on

    acl picurl url_regex -i \.bmp$ \.png$ \.jpg$ \.gif$ \.jpeg$ #防止盗链
    acl mystie1 referer_regex -i happy.swjtu.edu.cn
    http_access allow mystie1 picurl
    acl nullref referer_regex -i ^$
    http_access allow nullref
    acl hasref referer_regex -i .+
    http_access deny hasref picurl
    +]]>
    + + Infra + Web服务器 + Squid + + + Squid + +
    + + systemctl 命令 + /2022/08/02/systemctl%20%E5%91%BD%E4%BB%A4/ + systemctl 命令

    systemctl

    范列出系统上面有启动的unit

    +

    systemctl list-unit-files

    列出所有已经安装的unit有哪些

    +

    systemctl list-units –type=service –all

    列出类型为service的所有项目,不论启动与否

    +

    systemctl get-default

    输入目前机器默认的模式,如图形界面模式或者文本模式

    +

    systemctl isolate multi-user.target

    将目前的操作环境改为纯文本模式,关掉图形界面

    +

    systemctl isolate graphical.target

    将目前的操作环境改为图形界面

    +

    systemctl poweroff

    系统关机

    +

    systemctl reboot

    重新开机

    +

    systemctl suspend

    进入暂停模式

    +

    systemctl rescue

    强制进入救援模式

    +

    systemctl hibernate

    进入休眠模式

    +

    systemctl emergency

    强制进入紧急救援模式

    +

    systemctl list-dependencies –reverse

    查询当前默认的target关联了啥

    +

    systemctl list-dependencies graphical.target

    查询图形界面模式的target关联了啥

    +

    systemctl list-sockets

    查看当前的socket服务

    +

    systemctl show etcd.service

    查看 unit 的详细配置情况

    +

    systemctl mask etcd.service

    禁用某个服务

    +

    systemctl unmask etcd.service

    解除禁用某个服务

    +]]>
    + + linux + systemctl + + + systemctl + +
    + + test + /2023/09/20/test/ + 前言

    最近各种技术博客,各种技术文章,各种牛人前辈的学习心得都在看,最近看了一个不错的文章叫做《如何正确使用时间》
    感觉还是很不错的,特别适合年轻的程序员看一下。下面将全文贴在下方,如果需要也可以去原文查看。

    +

    原文

    +

    你是否时常会焦虑时间过的很快,没时间学习,本文将会分享一些个人的见解。

    +

    花时间补基础,读文档

    在工作中我们时常会花很多时间去 debug,但是你是否发现很多问题最终只是你基础不扎实或者文档没有仔细看。

    +

    基础是你技术的基石,一定要花时间打好基础,而不是追各种新的技术。一旦你的基础扎实,学习各种新的技术也肯定不在话下,因为新的技术,究其根本都是相通的。

    +

    文档同样也是一门技术的基础。一个优秀的库,开发人员肯定已经把如何使用这个库都写在文档中了,仔细阅读文档一定会是少写 bug 的最省事路子。

    +

    学会搜索

    如果你还在使用百度搜索编程问题,请尽快抛弃这个垃圾搜索引擎。同样一个关键字,使用百度和谷歌,谷歌基本完胜的。即使你使用中文在谷歌中搜索,得到的结果也往往是谷歌占优,所以如果你想迅速的通过搜索引擎来解决问题,那一定是谷歌。

    +

    学点英语

    说到英语,一定是大家所最不想听的。其实我一直认为程序员学习英语是简单的,因为我们工作中是一直接触着英语,并且看懂技术文章,文档所需要的单词量是极少的。我时常在群里看到大家发出一个问题的截图问什么原因,其实在截图中英语已经很明白的说明了问题的所在,如果你的英语过关,完全不需要浪费时间来提问和搜索。所以我认为学点英语也是节省时间中很重要的一点。

    +

    那么如何去学习呢,chrome 装个翻译插件,直接拿英文文档或文章读,不会的就直接划词翻译,然后记录下这个单词并背诵。每天花半小时看点英文文档和文章,坚持两个月,你的英语水平不说别的,看文档和文章绝对不会有难题了。这一定是一个很划算的个人时间投资,花点时间学习英语,能为你将来的技术之路铺平很多坎。

    +

    画个图,想一想再做

    你是否遇到过这种问题,需求一下来,看一眼,然后马上就按照设计稿开始做了,可能中间出个问题导致你需要返工。

    +

    如果你存在这样的问题,我很推荐在看到设计稿和需求的时候花点时间想一想,画一画。考虑一下设计稿中是否可以找到可以拆分出来的复用组件,是否存在之前写过的组件。该如何组织这个界面,数据的流转是怎么样的。然后画一下这个页面的需求,最后再动手做。

    +

    利用好下班时间学习

    说到下班时间,那可能就有人说了公司很迟下班,这其实是国内很普遍的情况。但是我认为正常的加班是可以的,但是强制的加班就是在损耗你的身体和前途。

    +

    可以这么说,大部分的 996 公司,加班的这些时间并不会增加你的技术,无非就是在写一些重复的业务逻辑。也许你可以拿到更多的钱,但是代价是身体还有前途。程序员是靠技术吃饭的,如果你长久呆在一个长时间加班的公司,不能增长你的技术还要吞噬你的下班学习时间,那么你一定会废掉的。如果你遇到了这种情况,只能推荐尽快跳槽到非 996 的公司。

    +

    那么如果你有足够的下班时间,一定要花上 1, 2 小时去学习,上班大家基本都一样,技术的精进就是看下班以后的那几个小时了。如果你能利用好下班时间来学习,坚持下去,时间一定会给你很好的答复。

    +

    列好 ToDo

    我喜欢规划好一段时间内要做的事情,并且要把事情拆分为小点。给 ToDo 列好优先级,紧急的优先级最高。相同优先级的我喜欢先做简单的,因为这样一旦完成就能划掉一个,提高成就感。

    +

    反思和整理

    每周末都会花上点时间整理下本周记录的笔记和看到的不错文章。然后考虑下本周完成的工作和下周准备要完成的工作。

    +]]>
    + + 自我提升 + 人生思考 + + + 经验之谈 + +
    + + VSCode中既想使用path-intellisense插件,又想使用Webpack别名(@)的方法 + /2021/05/08/%E5%85%B3%E4%BA%8EVSCode%E4%B8%AD%E5%88%AB%E5%90%8D%EF%BC%88-%EF%BC%89%E7%9A%84%E4%BD%BF%E7%94%A8/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    +

    场景:

    前端开发我们通常使用VSCode,使用时常见有一个常见的场景:

    +
    +

    引入一个文件、图片等

    +
    +

    如果不利用VSCode的插件,我们通常会写相对路径或者绝对路径,但为了方便写代码,我们通常会利用工具来帮助我们减轻负担,这种情况就引入了今天的主角,VSCode下的插件path-intellisense,是的,有了它编辑器就会聪明的自动帮我们提示出文件在什么地方,这的确方便了我们。

    +

    但是,在Vue开发过程中,或者说是利用Webpack作为构建工具时我们通常会定义一个变量如:@ 来简化路径的书写,使代码变得简洁美观,这里的@,我们称之为 路径别名 ,这也是Vue官方十分推荐使用的,但因为路径别名的使用,原来的智能提示路径的插件就会无法正常工作,因为工作中确实遇到了这样的坑,所以简单的写个文记录一下。

    +

    问题一:因为使用了路径别名(@)导致path-intellisense不能自动提示

    这个问题其实比较容易理解,因为路径别名是Webpack构建时使用的,并不是VSCode原生支持的功能,所以它并不认识@,这是个什么,所以解决思路就是告诉VSCode @ 是个什么就好了,专业一点说的话就是加上映射关系

    +

    解决步骤

      +
    1. 打开path-intellisense插件的setting
    2. +
    3. 找到Mapping配置项
    4. +
    5. 加以下代码
      "path-intellisense.mappings": {
      "@": "${workspaceRoot}/src"
      }
    6. +
    7. 重启插件搞定!
    8. +
    +

    问题二:我们希望 Ctrl + 鼠标左键点击一个外部方法时,能够快速跳转到对应的外部文件。

    第二个问题提出时,可能会有人有疑问,上面不是配置完映射关系了嘛,为什么还有下面的问题呢,其实你要是不问,我也有这个问题,上一步操作实际上是告诉了path-intellisense插件该怎么解析@这个东西,但是VSCode并不知道啊,但是Ctrl + 鼠标左键的动作又是VSCode负责的事,所以我们也要告诉一下编辑器该如何应对上面的情况,说到这里大家应该就理解了

    +

    解决步骤

      +
    1. 在项目package.json所在同级目录下创建文件jsconfig.json(这是VSCode的一个可选配置)
    2. +
    3. 在jsconfig.json增加如下配置
      {
      "compilerOptions": {
      "target": "ES6",
      "module": "commonjs",
      "allowSyntheticDefaultImports": true,
      "baseUrl": "./",
      "paths": {
      "@/*": ["src/*"] // 这里是关键代码
      }
      },
      "exclude": [
      "node_modules"
      ]
      }
    4. +
    5. 重启VSCode搞定!
    6. +
    +

    通过上面的解决方案,我们就可以愉快的使用VSCode进行前端开发了,你学废了吗?

    +]]>
    + + 前端框架 + Vue + + + VSCode插件 + path-intellisense + 路径别名 + +
    + + 关于版本管理 + /2021/05/13/%E5%85%B3%E4%BA%8E%E7%89%88%E6%9C%AC%E7%AE%A1%E7%90%86/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    还记得你的前端工程里有这样一个文件‘package.json’,里面管理者运行代码所需要的各种依赖,每种依赖都会有一个版本号,那么你知道版本号是怎么定义的么?不知道的话就看看下面的内容吧。

    + +

    关于版本号的那些事

    你会发现这些版本号通常是三部分构成的,像是‘X.Y.Z’的一种感觉,其实这是一种叫做SemVer的版本管理规范,下面我们就来讲讲SemVer

    +

    SemVer 的生平

    语义化版本控制的规范是由 Gravatars 创办者兼 GitHub 共同创办者 Tom Preston-Werner 所建立。

    +

    它的许可证是 知识共享 署名 3.0 (CC BY 3.0) 所以你可以不用付费直接使用它。

    +

    关于它的更多描述你可以到下面的地址找到
    https://semver.org/lang/zh-CN/

    +

    主要你要记住的是如下几句话:

    +
    +

    版本格式:主版本号.次版本号.修订号,版本号递增规则如下:

    +

    主版本号:当你做了不兼容的 API 修改,
    次版本号:当你做了向下兼容的功能性新增,
    修订号:当你做了向下兼容的问题修正。
    先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。

    +
    +

    更多的内容的话,请到官网查看吧。

    +]]>
    + + 前端技术 + 计算机通识 + + + SemVer + 版本管理 + +
    + + 前后端分离开发指南 + /2022/06/01/%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/ + +

    作者:李旭光
    引用请标明出处

    + +

    背景

    前后端分离狭义上来讲是以浏览器为分界,应用在浏览器内部的技术为前端技术,主要负责页面展示的部分,应用在浏览器外的技术为后端技术,主要负责处理业务逻辑和数据准备工作,但是随着近几年NodeJs的崛起,前端的工作范畴明显扩大,前后端分离也不仅仅作为一种开发模式,更是web应用架构层面的一种模式。

    +

    认识

      +
    1. 在开发阶段,前后端工程师要提前约定好数据交互的接口,从而实现并行开发和测试;
    2. +
    3. 在运行阶段,前后端分离模式需要对web应用进行分离部署,前后端之前使用HTTP或者其他协议进行交互请求。(NodeJs或Ngnix)
    4. +
    +

    由上述两条可以看出前后端分离分为开发阶段和部署阶段,通常来说我们做到的只是前后端分离开发,并没有做到分离部署。

    +

    分解

    作为一种全新的架构模式,前后端分离需要从四个方面来比较和认识。

    +
      +
    1. 交互形式
    2. +
    3. 开发模式/流程
    4. +
    5. 代码组织方式
    6. +
    7. 数据接口规范流程
    8. +
    +

    一、交互形式

    在前后端分离架构中,后端只需要负责按照约定的数据格式向前端提供可调用的API服务即可。前后端之间通过HTTP请求进行交互,前端获取到数据后,进行页面的组装和渲染,最终返回给浏览器。

    +

    这里有一个经常引起激烈讨论的话题就是,“约定的数据格式”,是后端直接提供给前端可直接使用的数据格式,还是简单的数据库拉取数据不做加工直接推给前端,这个要视具体的项目情况而定,如果后端代码需要支持的不仅仅是一端,比如要同时适配pc端、手机端等多个终端,其多终端数据展现的形式如果各不相同,那后端只需要提供满足多端的基础数据格式即可,但如果只需要满足单个终端,那么建议直接将前后端数据格式进行统一处理,方便前后端协同,数据处理可以放在java端做也可以放在NodeJs端做,视人员配备情况而定,但绝不是后端不做任何处理就将数据返回给前端,完全由前端组织数据。

    +

    二、开发模式/流程

    在前后端分离模式未诞生之前,前后端属于一个整体,那时使用的是MVC架构模式,前端对应的就是View层,主要通过html/css/js实现静态页面和动态效果,在有后端进行模板变量的嵌套和一些页面逻辑的处理,最终打包成一个整体,部署到同一服务器上,同时会进行简单的动静态分离部署。

    +

    此时开发的流程如下:
    需求=》前后端并行开发=》前端开发静态页面=》后端套模板=》集成问题=》前后端调整=》再集成=》解决集成问题=》交付上线
    // TODO 这里插图

    +

    出现前后端分离架构之后,前端工程师只需要编写前端页面+前端数据、业务逻辑处理,之后通过HTTP或其他请求方式调用后端提供的服务接口就可以了,而且除了在开发周期可以进行前后端分离,在部署阶段,前后端也可进行分离部署。
    // TODO 这里插图

    +

    此时开发的流程如下:
    需求=》设计接口、约定数据=》前后端并行开发=》集成=》调整=》集成成功=》交付上线

    +

    通过上面的描述及流程,不难发现,前后端分离的开发方式不仅仅从分工上进行了区分,更重要的是在并行开发的问题上解决了反复集成等前后端互相影响的问题,从而降低了开发的难度,简化了开发的流程。

    +

    三、代码组织方式

    // TODO 这里插图
    在传统的开发模式架构下,前端代码是作为项目的静态资源存在于项目工程下,页面中还夹着一些后端代码如jsp、php等技术,前后端开发时需要将整个项目代码完整的引入开发工具才能进行开发,前后端同时维护一份代码,这种开发方式导致前后端代码互相影响,因此前后端分离势在必行。

    +

    而前后端分离模式在代码的组织形式上由以下两种形式组成:

    +
      +
    1. 半分离
      前后端仍共用一个代码库,但是代码分别存放在两个工程中。后端不关心或很少 关心前端元素的输出情况,前端不能独立进行开发和测试,项目中缺乏前后端 交互的测试用例。
    2. +
    3. 完全分离
      完全分离后,前端代码可以通过Mock来模拟后端请求,从此可以独立进行前端开发和测试。后端代码只需要按照跟前端约定好的接口格式写出完整的测试用例,确保接口的可用性。通过上述手段,降低开发集成风险。
    4. +
    +

    四、数据接口规范流程

    通过上面三段的描述,我们可以看出前后端分离开发模式最重要也是最初的阶段就是数据接口的确定,因此在项目开发前必需先进行数据和接口的定义,数据接口的定义需要前后端开发共同商定,包括确定的数据格式,交互形式,并生成一份接口文档供前后端开发人员使用。之后才是并行开发。开发期间前后端双方需要严格按照确定的数据接口文档进行开发,前端开发完之后可以利用mock服务独自进行接口测试,后端也可以利用postman或其他接口测试工具进行测试,并提供完整的接口测试用例,然后前后端进行功能联调,最后再提交线上测试,也可进行自动化测试。

    +

    // TODO 这里插图

    +

    分离后的收益

    到底分不分,如何分是个持续讨论的话题,通过上述的内容大家已经了解到了,什么是前后端分离,也知道如何进行前后端分离开发部署,那么前后端分离能带来哪些收益呢?

    +

    首先,就目前的软件开发应用趋势来看,越来越注重用户的体验性,而且架构越来越大,服务越来越小,而且终端设备越来越丰富,而原来不分离的方式已经不能支撑现在的发展趋势,因此前后端分离开发及部署将势在必行。

    +

    而且采用前后端分离的架构之后,我们将有如下几点提升:

    +
      +
    1. 前后端分离后,前后端将不再互相纠缠而是各自在自己熟悉的领域进行开发工作,这将有利于前后端深化优化各自的代码,培养各自独特的技术特性,从而开发出更加优秀的应用,建立起专业精良的全栈开发团队。

      +
    2. +
    3. 通过前后端分离架构可以实现前后端开发从代码及开发流程上的完全解耦,只需要前后端共同商定好接口后,便可完全独立开发,只需要在联调阶段进行好协作,在此之前可以互不影响的进行并行开发,即是需求发生了变动,但只要不影响接口,后端既可以不用修改代码,只需前端进行变动即可,如此整体的开发效率将得到提升。

      +
    4. +
    5. 前后端分离后,能够更好的适应前端日益增多的的终端适配,代码解耦后复用率更高。

      +
    6. +
    +

    4.前后端分离后,前后端代码可以分别管理,代码不再混在一起,代码可维护性也增强了

    +

    前后端分离后收益不止以上四点,因为分离而带来的职责上、技术上、代码上、部署上的解耦让开发工作比以往任何时候都要更加专注和轻松。

    +

    注意事项

    前后端分离误区

      +
    1. 前端人员不充足,不能进行前后端分离。
    2. +
    +

    此话说来是因为对前后端分离后职责区分不明确导致的问题,因为以往的前端只需要写静态页面就可以了,而前后端分离后前端也不仅仅需要写静态页面,而且还要为页面提供数据和页面逻辑的处理,但实际上可以根据团队情况来区别对待,如果团队前端人员充足,那么可以由前端人员负责多一些的工作,比如API请求后的业务逻辑的处理,页面逻辑的处理、页面数据的准备等,如果后端人员配备充足,那么上述几个环节仍然可由后端人员进行处理,前端开发仍然只是写静态页面,只是内容和逻辑不再写死而是通过js或其他手段如mvvm的框架进行处理。

    +
      +
    1. 前后端分离后前端任务加重,职责也不清晰。
    2. +
    +

    如第一点描述可知,问题不在前后端分离的模式是否合适,而是任务分配和人员分配上的问题,如果前端能力强且人员比例较多,那么部分任务可以由前端承担,如果后端人员多,那么任务由后端承担。

    +
      +
    1. 后端开发需要增加接口开发工作,增加任务量。
    2. +
    +

    无论如何后端开发都是需要写接口的,只是前后端分离后需要按照ResetFul风格写接口,或者采用最新的GraphQl的方式进行交互,如果说这个阶段需要前后端进行商量确定接口交互形式和数据格式花费了时间,但是在接下来前后端并行开发及问题解决上省掉的时间是更加可观的。

    +
      +
    1. 分离后仍出现互相等待的问题,反而不如传统开发模式快。
    2. +
    +

    这个问题的产生其实也是由于对前后端分离后技术缺失导致的,常见情况是前端写完页面逻辑和假数据后后端开发还未完成接口开发导致无法进行联调,实际上前端通过mockserver等方式是可以解决一些问题的。

    +

    前后端分离适用场景

    现代化的web应用适合用前后端分离的开发方式。
    原因有以下几点:

    +
      +
    1. web应用前端页面交互复杂。
        +
      • 页面渲染数据量大。
      • +
      • 页面包含复杂的业务逻辑。
      • +
      +
    2. +
    3. 终端适配情况多。
    4. +
    5. 分布式架构,微服务化应用场景。
    6. +
    +

    前后端分离具体方案

    总体方向

    后端专注于:后端控制层(Restful API) & 服务层 & 数据访问层;

    +

    前端专注于:前端控制层(Nodejs) & 视图层

    +
      +
    1. 项目设计阶段,前后端架构负责人将项目整体进行分析,讨论并确定API风格、职责分配、开发协助模式,确定人员配备;设计确定后,前后端人员共同制定开发接口。

      +
    2. +
    3. 项目开发阶段,前后端分离是各自分工,协同敏捷开发,后端提供Restful API,并给出详细文档说明,前端人员进行页面渲染前台的任务是发送API请(GET,PUT,POST,DELETE等)获取数据(json,xml)后渲染页面。

      +
    4. +
    5. 项目测试阶段,API完成之前,前端人员会使用mock server进行模拟测试,后端人员采用junit进行API单元测试,不用互相等待;API完成之后,前后端再对接测试一下就可以了,当然并不是所有的接口都可以提前定义,有一些是在开发过程中进行调整的。

      +
    6. +
    7. 项目部署阶段,利用nginx 做反向代理,即Java + nodejs + nginx 方式进行。

      +
    8. +
    +

    技术手段

      +
    • 前端技术栈:前端代码 + mock服务
    • +
    • 后端技术栈:postman + 接口 + 后端业务逻辑 + 数据库
    • +
    • 公共依赖:接口文档/接口测试工具
    • +
    +

    常用的mock服务为jsonserver
    常用的接口测试工具为postman、rap、swagger、doclever

    +

    部署方案

      +
    1. 第一阶段为前后端同一个代码库,同一个服务器,集中部署。
    2. +
    3. 第二阶段引入Ngnix服务作为中间件,前端向Ngnix发请求,Ngnix向后端服务发请求,由于Ngnix为静态服务器,所以在seo优化上和页面性能优化上效果不明显,因此前端仍需与后端进行配合才能达到整体的优化。
    4. +
    +

    浏览器 =》 Ngnix(前端机)=》Ngnix(后端机可没有)=》Server服务

    +
      +
    1. 第三阶段引入nodejs作为中间层,将前端资源部署到Server层。同时实现数据代理服务,负责与提供数据的后端进行通信。
    2. +
    +

    浏览器=》Ngnix(前端机)=》NodeServer =》Server服务

    +

    浏览器向前端机发送请求,由Ngnix进行分发,url统一分发至NodeServer,在Node Server中根据请求类型从后端服务器上通过RPC服务请求页面的模板数据,然后进行页面的组装和渲染;API请求则直接转发到后端服务挖成相应。

    +

    前后端分离部署方案比较

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    属性字段传统模式Ngnix+ServerNode+ServerNgnix+Node+Server
    SEOoknookok
    浏览器渲染负担oknookok
    前后端耦合nookokok
    请求相应效率nooknook
    +

    结语

    随着前端技术的快速发展和对用户体验日益增长的需求,前后端分离模式势必将会成为主流趋势。无论是从开发模式的角度上来说,还是对团队成长的角度上来说,前后端分离都会带来益处,让我们一同拥抱前后端分离,打造精良的开发团队,迎接日益复杂的web应用开发需求。

    +

    参考来源

    +

    版权声明

    Copyright by lixuguang
    未经授权,严禁转载。如需转载,请联系作者

    +]]>
    + + 前端开发规范 + + + 前后端分离开发指南 + +
    + + 前后端分离开发规范 + /2022/06/01/%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB%E5%BC%80%E5%8F%91%E8%A7%84%E8%8C%83/ + +

    作者:李旭光
    引用请标明出处

    + +

    前后端分离开发规范

    by lixg

    +

    (本文所说的前后端工作皆站在工作内容本身,不与职称相关,下面皆如此。)

    +

    一、为什么要前后端分离

      +
    1. 前后端专注于各自擅长的领域
    2. +
    3. 前端配置后端代码运行环境,节省搭建环境的时间
    4. +
    5. 明确前后端工作职责
    6. +
    7. 提高开发效率
    8. +
    9. 分离有助于前端、后端分别优化
    10. +
    +

    二、前后端分离存在的问题

      +
    1. 前后端分离必须接口先行,无接口导致前后端关于接口的工作重复出现。
    2. +
    3. 接口对接方式不一致,各项目团队没有采用统一的接口对接方式,导致每次都要熟悉一种新的对接方式。
    4. +
    5. 数据传输格式不一致,导致每次都要重新熟悉,另外由于接口格式不一致还会引起其他未知问题,影响系统健壮性。
    6. +
    7. 人员比例问题,目前公司前后端比例不完全符合前后端分离开发的要求。
    8. +
    +

    为解决上述问题,提高前后端分离开发效率,特制定如下规范。

    +

    三、如何做分离

      +
    1. 职责分离
        +
      • 前后端仅仅通过异步接口(AJAX/JSONP)来编程
      • +
      • 前后端都各自有自己的开发流程,构建工具,测试集合
      • +
      • 关注点分离,前后端变得相对独立并松耦合
        前后端职责
      • +
      +
    2. +
    + + + + + + + + + + + + + + + + + + + + + + + +
    后端前端
    提供数据接收数据,展示数据
    处理业务逻辑处理渲染逻辑
    Server-side MVC架构Client-sideMV*架构
    代码运行在服务器上代码运行在浏览器上
    +
      +
    1. 开发流程
        +
      • 前后端技术负责人约定好接口格式
      • +
      • 后端编写和维护接口文档,在 API 变化时更新接口文档 + MOCK平台
      • +
      • 后端根据接口文档进行接口开发
      • +
      • 前端根据接口文档 + MOCK平台进行开发
      • +
      • 开发完成后联调和提交测试
      • +
      +
    2. +
    +

    MOCK平台统一采用公司搭建的YAPI平台

    +

    YAPI平台可以对接SWAGGER工具进行自动构建MOCK服务
    前后端开发流程
    3. 规范原则
    - 接口返回数据即显示:前端仅做渲染逻辑处理
    - 渲染逻辑禁止跨多个接口调用
    - 前端关注交互、渲染逻辑,尽量避免业务逻辑处理的出现
    - 请求响应传输数据格式:JSON,JSON数据尽量简单轻量,避免多级JSON的出现

    +

    四、基本格式

    接口定义参见《RESTFul API的设计规范》

    请求格式

    GET请求、POST请求==必须包含key为body的入参,所有请求数据包装为JSON格式,并存放到入参body中==,示例如下:

    +

    GET请求:

    +
    xxx/login?body={"username":"admin","password":"123456","captcha":"scfd","rememberMe":1}
    + +

    响应格式

    对于通用业务数据响应参照基本数据格式要求

    +

    响应基本数据格式

    {
    "code": 200,
    "msg": "success"
    }
    +
    响应实体格式
    {
    "code": 200,
    "msg": "success",
    "data": {
    "entity": {
    "id": 1,
    "name": "XXX",
    "phone": "XXX"
    }
    }
    }
    +

    entity: 响应返回的实体数据

    +
    响应列表格式
    {
    "code": 200,
    "msg": "success",
    "data": {
    "list":[
    {
    "id": 1,
    "name": "XXX",
    "code": "XXX"
    },
    {
    "id": 2,
    "name": "XXX",
    "code": "XXX"
    },
    {
    "id": 3,
    "name": "XXX",
    "code": "XXX"
    }
    ]
    }
    }
    +

    list: 响应返回的列表数据

    +
    响应分页格式
    {
    "code": 200,
    "msg":"success",
    "data": {
    "totalCount": 2, // 总记录数
    "totalPage": 1 // 总页数
    "pageNo": 1, // 当前页码
    "pageSize": 10, // 每页大小
    "list":[
    {
    "id": 1,
    "name": "XXX",
    "code": "XXX"
    },
    {
    "id": 2,
    "name": "XXX",
    "code": "XXX"
    },
    {
    "id": 3,
    "name": "XXX",
    "code": "XXX"
    }
    ],
    }
    }
    +

    响应特殊数据格式

    对于特定组件数据格式由后端统一处理后返回前端,如(echart、ztree等组件)

    +

    特殊内容规范

    布尔类型

    关于布尔类型,一律返回BOOLEN类型值

    +
    日期格式

    关于日期类型,JSON数据传输中一律使用字符串格式时间戳,具体日期格式因业务而定

    +

    五、相关文章导读

    前后端分离开发指南-理论篇
    前后端分离开发指南-实践篇

    +]]>
    + + 前端开发规范 + + + 前后端分离开发规范 + +
    + + 前端开发插件 + /2022/06/01/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91%E6%8F%92%E4%BB%B6/ + +

    作者:李旭光
    引用请标明出处

    + +

    插件安装

    Prettier - Code formatter

    格式化工具

    +

    ESLint

    校验规则

    +

    Vetur

    vue代码片段及代码美化

    +

    Vue 2 Snippets

    vue2 代码片段

    +

    vscode-fileheader

    文件注释

    +

    配置项

    // setting.json vscode配置文件
    {
    "workbench.startupEditor": "newUntitledFile",
    "terminal.integrated.shell.windows": "C:\\Program Files\\Git\\bin\\bash.exe", \\ 自行配置自己的环境地址
    "javascript.updateImportsOnFileMove.enabled": "always",
    "editor.tabSize": 2,
    "search.exclude": {
    "**/node_modules": true,
    "**/bower_components": true
    },
    "sync.gist": "748b4cae5eb6e56d6997978ead096e8f",
    "breadcrumbs.enabled": true,
    "todohighlight.isEnable": false,
    "liveServer.settings.donotShowInfoMsg": true,
    "search.location": "sidebar",
    "workbench.activityBar.visible": true,
    "window.menuBarVisibility": "default",
    "workbench.statusBar.visible": true,
    "editor.snippetSuggestions": "top",
    "editor.formatOnPaste": true,
    "workbench.colorTheme": "Tiny Light",
    "eslint.validate": [
    "javascript",
    "javascriptreact",
    {
    "language": "html",
    "autoFix": true
    },
    {
    "language": "vue",
    "autoFix": true
    }
    ],
    "prettier.eslintIntegration": true,
    "files.autoSave": "onWindowChange",
    "code-runner.saveAllFilesBeforeRun": true,
    "vetur.format.defaultFormatter.html": "js-beautify-html",
    "vetur.format.defaultFormatter.js": "vscode-typescript",
    "prettier.jsxSingleQuote": true,
    "prettier.requireConfig": false,
    "prettier.arrowParens": "always",
    "typescript.format.insertSpaceAfterSemicolonInForStatements": false,
    "prettier.stylelintIntegration": true,
    "prettier.singleQuote": true,
    "prettier.tslintIntegration": true,
    "eslint.provideLintTask": true,
    "eslint.autoFixOnSave": true,
    "editor.mouseWheelZoom": true,
    "editor.tabCompletion": "on",
    "editor.formatOnType": true,
    "eslint.alwaysShowStatus": true,
    "eslint.options": {
    "configFile": "E:/project/xxjs/fore-core/.eslintrc.js" // 自行配置自己的项目地址
    },
    "fileheader.Author": "Li.Xg", // 自行配置自己的名称
    "fileheader.LastModifiedBy": "Li.Xg" // 自行配置自己的名称
    }
    +]]>
    + + 前端开发规范 + + + 前端开发插件 + +
    + + 搞懂EventLoop + /2022/03/11/%E6%90%9E%E6%87%82EventLoop/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    EventLoop 翻译过来就是事件循环,那啥是事件循环啊,这个要从javascript是如何执行的说起。
    要知道javascript是一个运行在浏览器或者Node环境的单线程执行的脚本语言,但是为了解决单线程阻塞的问题,这里就引入了事件循环的机制,也就是我们常说的异步特性。

    +

    但是虽然都是JavaScript,都是事件循环,都有异步特性,但浏览器环境和Node环境的实现方式是不一样的,这里要先说明一下。
    浏览器的事件循环是HTML定义的规范,而Node环境是利用libuv库实现的,这里我们先从浏览器的事件循环开始说。

    + + +

    浏览器的事件循环

    首先我们要知道,浏览器的事件循环是有这么两个部分构成,一个叫做主线程(main thread),另一个叫做调用栈(call-stack),所有的任务(Task)都会被放到调用栈里,等待主线程调用,这个怎么理解呢,打个比方,主线程就像是物流配送中的传送带,如果没有快递往上放的时候,它是空的,当有快递包裹放上去的时候,它就开始运行,而你购买的产品要放倒包裹里,然后包裹在放到传送带上,最终被送到你家里,这里的传送带就是主线程,包裹就是调用栈,而购买的产品就是任务,不知道这样解释是否能好理解一些。

    +

    同步、异步任务

    JavaScript中的任务有两种,一种是同步任务,另一种是异步任务。

    +
      +
    • 同步任务会排着队,逐个执行
    • +
    • 异步任务在执行得到结果后,将回调函数添加到任务队列中,等着主线程空了再执行。
    • +
    +

    调用栈

    栈,是一种数据结构,后进先出(LIFO),后入栈的先执行,执行完后出栈执行栈顶新的那个函数,直到栈空,像个瓶子,只有一头有口。

    +

    任务队列

    队列,也是一种数据结构,先进先出(FIFO),队头出,队尾进,先到先得,就像排队买东西,或者汽车排队过隧道。

    +

    任务

    任务分为两种,一种叫做宏任务(MacroTask),另一种叫做微任务(MicroTask),首先他们都是异步队列中的任务,那它们之间是什么关系呢?简单说来就是微任务是VIP优先执行,全部微任务执行完之后才轮到普通用户宏任务执行,而且执行完一个后马上要看有没有新的微任务执行,有的话宏任务还得等着,就这样往复,看着像不像是在银行等着排队办业务的你(其实是我)。
    JavaScript的执行流程
    宏任务与微任务
    那么哪些是宏任务,哪些是微任务呢?看下表吧。

    + + + + + + + + + + + +
    宏任务微任务
    setTimeout、setInterval、js主代码、setImmediate(Node)、requestAnimationFrame(浏览器)process.nextTick、Promise的then方法
    +

    举个例子

    console.log('1');

    setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
    console.log('3');
    })
    new Promise(function(resolve) {
    console.log('4');
    resolve();
    }).then(function() {
    console.log('5')
    })
    })
    process.nextTick(function() {
    console.log('6');
    })
    new Promise(function(resolve) {
    console.log('7');
    resolve();
    }).then(function() {
    console.log('8')
    })

    setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
    console.log('10');
    })
    new Promise(function(resolve) {
    console.log('11');
    resolve();
    }).then(function() {
    console.log('12')
    })
    })
    + +

    分析一下

    JS 运行机制为从上而下,那么这里的执行顺序应该是什么样的呢?

    +
    第一轮循环:

    1)、首先打印 1
    2)、接下来是setTimeout是异步任务且是宏任务,加入宏任务暂且记为 setTimeout1
    3)、接下来是 process 微任务 加入微任务队列 记为 process1
    4)、接下来是 new Promise 里面直接 resolve(7) 所以打印 7 后面的then是微任务 记为 then1
    5)、setTimeout 宏任务 记为 setTimeout2

    +
    +

    第一轮循环打印出的是 1 7
    当前宏任务队列:setTimeout1, setTimeout2
    当前微任务队列:process1, then1,

    +
    +
    第二轮循环:

    1)、执行所有微任务
    2)、执行process1,打印出 6
    3)、执行then1 打印出8
    4)、微任务都执行结束了,开始执行第一个宏任务
    5)、执行 setTimeout1 也就是 第 3 - 14 行
    6)、首先打印出 2
    7)、遇到 process 微任务 记为 process2
    8)、new Promise中resolve 打印出 4
    9)、then 微任务 记为 then2

    +
    +

    第二轮循环结束,当前打印出来的是 1 7 6 8 2 4
    当前宏任务队列:setTimeout2
    当前微任务队列:process2, then2

    +
    +
    第三轮循环:

    1)、执行所有的微任务
    2)、执行 process2 打印出 3
    3)、执行 then2 打印出 5
    4)、执行第一个宏任务,也就是执行 setTimeout2 对应代码中的 25 - 36 行
    5)、首先打印出 9
    6)、process 微任务 记为 process3
    7)、new Promise执行resolve 打印出 11
    8)、then 微任务 记为 then3

    +
    +

    第三轮循环结束,当前打印顺序为:1 7 6 8 2 4 3 5 9 11
    当前宏任务队列为空
    当前微任务队列:process3,then3

    +
    +
    第四轮循环:

    1)、执行所有的微任务
    2)、执行process3 打印出 10
    3)、执行then3 打印出 12

    +

    代码执行结束:
    最终打印顺序为:1 7 6 8 2 4 3 5 9 11 10 12

    +

    Node环境的循环机制

    Node的Event loop一共分为6个阶段,每个细节具体如下:

    +
      +
    • timers: 执行setTimeout和setInterval中到期的callback。
    • +
    • pending callback: 上一轮循环中少数的callback会放在这一阶段执行。
    • +
    • idle, prepare: 仅在内部使用。
    • +
    • poll: 最重要的阶段,执行pending callback,在适当的情况下回阻塞在这个阶段。
    • +
    • check: 执行setImmediate(setImmediate()是将事件插入到事件队列尾部,主线程和事件队列的函数执行完成之后立即执行setImmediate指定的回调函数)的callback。
    • +
    • close callbacks: 执行close事件的callback,例如socket.on(‘close’[,fn])或者http.server.on(‘close, fn)。
    • +
    +

    Node EventLoop

    +

    具体细节如下:

    +

    timers

    执行setTimeout和setInterval中到期的callback,执行这两者回调需要设置一个毫秒数,理论上来说,应该是时间一到就立即执行callback回调,但是由于system的调度可能会延时,达不到预期时间。
    以下是官网文档解释的例子:

    +
    const fs = require('fs');

    function someAsyncOperation(callback) {
    // Assume this takes 95ms to complete
    fs.readFile('/path/to/file', callback);
    }

    const timeoutScheduled = Date.now();

    setTimeout(() => {
    const delay = Date.now() - timeoutScheduled;

    console.log(`${delay}ms have passed since I was scheduled`);
    }, 100);


    // do someAsyncOperation which takes 95 ms to complete
    someAsyncOperation(() => {
    const startCallback = Date.now();

    // do something that will take 10ms...
    while (Date.now() - startCallback < 10) {
    // do nothing
    }
    });

    + +

    当进入事件循环时,它有一个空队列(fs.readFile()尚未完成),因此定时器将等待剩余毫秒数,当到达95ms时,fs.readFile()完成读取文件并且其完成需要10毫秒的回调被添加到轮询队列并执行。
    当回调结束时,队列中不再有回调,因此事件循环将看到已达到最快定时器的阈值,然后回到timers阶段以执行定时器的回调。
    在此示例中,您将看到正在调度的计时器与正在执行的回调之间的总延迟将为105毫秒。

    +

    pending callbacks

    此阶段执行某些系统操作(例如TCP错误类型)的回调。 例如,如果TCP socket ECONNREFUSED在尝试connect时receives,则某些* nix系统希望等待报告错误。 这将在pending callbacks阶段执行。

    +

    poll

    该poll阶段有两个主要功能:

    +
      +
    • 执行I/O回调。
    • +
    • 处理轮询队列中的事件。
    • +
    +

    当事件循环进入poll阶段并且在timers中没有可以执行定时器时,将发生以下两种情况之一
    如果poll队列不为空,则事件循环将遍历其同步执行它们的callback队列,直到队列为空,或者达到system-dependent(系统相关限制)。

    +

    如果poll队列为空,则会发生以下两种情况之一

    +
      +
    • 如果有setImmediate()回调需要执行,则会立即停止执行poll阶段并进入执行check阶段以执行回调。
    • +
    • 如果没有setImmediate()回到需要执行,poll阶段将等待callback被添加到队列中,然后立即执行。
    • +
    +

    当然设定了 timer 的话且 poll 队列为空,则会判断是否有 timer 超时,如果有的话会回到 timer 阶段执行回调。

    +

    check

    此阶段允许人员在poll阶段完成后立即执行回调。
    如果poll阶段闲置并且script已排队setImmediate(),则事件循环到达check阶段执行而不是继续等待。
    setImmediate()实际上是一个特殊的计时器,它在事件循环的一个单独阶段运行。它使用libuv API来调度在poll阶段完成后执行的回调。
    通常,当代码被执行时,事件循环最终将达到poll阶段,它将等待传入连接,请求等。
    但是,如果已经调度了回调setImmediate(),并且轮询阶段变为空闲,则它将结束并且到达check阶段,而不是等待poll事件。

    +
    console.log('start')
    setTimeout(() => {
    console.log('timer1')
    Promise.resolve().then(function() {
    console.log('promise1')
    })
    }, 0)
    setTimeout(() => {
    console.log('timer2')
    Promise.resolve().then(function() {
    console.log('promise2')
    })
    }, 0)
    Promise.resolve().then(function() {
    console.log('promise3')
    })
    console.log('end')

    + +

    如果Node版本是v11.x,那么执行结果和浏览器一致,结果如下:

    +
    start
    end
    promise3
    timer1
    promise1
    timer2
    promise2
    + +

    如果是v10版本上,那结果将会出现两种情况
    如果time2定时器已经在执行队列中了

    +
    start
    end
    promise3
    timer1
    timer2
    promise1
    promise2
    + +

    如果time2定时器没有在执行对列中,执行结果为

    +
    start
    end
    promise3
    timer1
    promise1
    timer2
    promise2
    + +

    具体情况可以参考poll阶段的两种情况。

    +

    NodeEventLoop

    +

    setImmediate() 的setTimeout()的区别

    setImmediate和setTimeout()是相似的,但根据它们被调用的时间以不同的方式表现。

    +
      +
    • setImmediate()设计用于在当前poll阶段完成后check阶段执行脚本 。
    • +
    • setTimeout() 安排在经过最小(ms)后运行的脚本,在timers阶段执行。
    • +
    +

    举个例子

    setTimeout(() => {
    console.log('timeout');
    }, 0);

    setImmediate(() => {
    console.log('immediate');
    });

    + +

    执行定时器的顺序将根据调用它们的上下文而有所不同。 如果从主模块中调用两者,那么时间将受到进程性能的限制。
    其结果也不一致

    +

    如果在I / O周期内移动两个调用,则始终首先执行立即回调:

    +
    const fs = require('fs');

    fs.readFile(__filename, () => {
    setTimeout(() => {
    console.log('timeout');
    }, 0);
    setImmediate(() => {
    console.log('immediate');
    });
    });

    + +

    其结果可以确定一定是immediate => timeout。
    主要原因是在I/O阶段读取文件后,事件循环会先进入poll阶段,发现有setImmediate需要执行,会立即进入check阶段执行setImmediate的回调。
    然后再进入timers阶段,执行setTimeout,打印timeout。

    +
       ┌───────────────────────────┐
    ┌─>│ timers │
    │ └─────────────┬─────────────┘
    │ ┌─────────────┴─────────────┐
    │ │ pending callbacks │
    │ └─────────────┬─────────────┘
    │ ┌─────────────┴─────────────┐
    │ │ idle, prepare │
    │ └─────────────┬─────────────┘ ┌───────────────┐
    │ ┌─────────────┴─────────────┐ │ incoming: │
    │ │ poll │<─────┤ connections, │
    │ └─────────────┬─────────────┘ │ data, etc. │
    │ ┌─────────────┴─────────────┐ └───────────────┘
    │ │ check │
    │ └─────────────┬─────────────┘
    │ ┌─────────────┴─────────────┐
    └──┤ close callbacks │
    └───────────────────────────┘

    + +

    Process.nextTick()

    process.nextTick()虽然它是异步API的一部分,但未在图中显示。这是因为process.nextTick()从技术上讲,它不是事件循环的一部分。

    +

    process.nextTick()方法将 callback 添加到next tick队列。 一旦当前事件轮询队列的任务全部完成,在next tick队列中的所有callbacks会被依次调用。

    +

    换种理解方式:

    +

    当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。

    +

    举个例子

    let bar;

    setTimeout(() => {
    console.log('setTimeout');
    }, 0)

    setImmediate(() => {
    console.log('setImmediate');
    })
    function someAsyncApiCall(callback) {
    process.nextTick(callback);
    }

    someAsyncApiCall(() => {
    console.log('bar', bar); // 1
    });

    bar = 1;

    +

    在NodeV10中上述代码执行可能有两种答案,一种为:

    +
    bar 1
    setTimeout
    setImmediate

    +

    另一种为

    +
    bar 1
    setImmediate
    setTimeout

    + +

    无论哪种,始终都是先执行process.nextTick(callback),打印bar 1。

    +]]>
    + + 前端技术 + 计算机通识 + + + EventLoop + 事件驱动 + +
    + + 搭建Docker私有仓库 + /2022/08/02/%E6%90%AD%E5%BB%BADocker%E7%A7%81%E6%9C%89%E4%BB%93%E5%BA%93/ + 搭建Docker私有仓库

    利用registry搭建私有仓库

    +

    下载registry

    docker pull registry
    + +

    配置

    /etc/docker/doemon.json
    # 配置 'insecure-registry'
    +

    重启docker

    systemctl restart docker
    + +

    创建registry容器(关联私有仓库配置)

    docker run -d -p 5000:5000 --name registry registry:latest
    + +

    推送镜像到私有仓

      +
    • 备份镜像(172.16.12.134:5000 私有仓地址)

      +
      docker tag ubuntu:latest 172.16.12.134:5000/my_ubuntu
    • +
    • 推送

      +
      docker push 172.16.12.134:5000/my_ubuntu
    • +
    • 下载

      +
      docker pull 172.16.12.134:5000/my_ubuntu
    • +
    +]]>
    + + Docker + + + registry + +
    + + 数组方法哪些有副作用,一目了然! + /2022/03/11/%E6%95%B0%E7%BB%84%E6%96%B9%E6%B3%95%E4%B8%80%E7%9B%AE%E4%BA%86%E7%84%B6/ + +

    作者:李旭光
    引用请标明出处

    + +

    前言

    今天一个小兄弟微信给我发了个消息,说是今天面试,面试官问了个比较基础的问题,没答好,我就问他问了什么,他说,数组有哪些方法会改变原来的值,他说他就说了3、4个,还不确定对不对,我想了想说其实列个表就很容易记住了,于是便有了下面这个表格。

    + +

    关于数组的那些方法

    其实上面的问题:

    +
    +

    数组有哪些方法会改变原来的值

    +
    +

    现在有个词挺火的,叫做纯函数,什么是纯函数呢,就是没有副作用,什么是没有副作用呢,就是结果是幂等的,等等….怎么越来越绕,简单来说,就是这个函数干几次都是一样的结果,出了函数就不会有任何影响,他就像一台加工机器,输入原料,输出产品,没有任何其他输出材料,比如废气之类影响大气的东西。好吧,你不明白也没关系,反正我对上面这个问题的理解就变成了

    +
    +

    数组的哪些方法是没有副作用的。

    +
    +

    要说有哪些方法是有副作用,首先你得知道有哪些方法,

    +

    比如,push,pop,shift,unshift,splice 这些别跟我说你不知道,这些都是常用的数组操作,出栈入栈,
    还有什么呢,数组的排序啊,比如sort,reverse,还有一个用的不多,fill是用一个固定值填充数组的起始元素到终止元素,这些呢都是在原数组上进行操作,所以肯定是影响数组的值的,也就是说有副作用的。

    +

    那么哪些没有副作用呢,简单说其他的都没有,
    啥!?,是的,其他的都没有。。。所以记得上面这些就好了呀,怎么样,是不简单。

    +

    那还剩下啥?

    +
      +
    • concat 返回的是心数组,
    • +
    • join 返回的是字符串,
    • +
    • indexOf和lastIndexOf返回的是索引,
    • +
    • slice 返回的是切割后的新数组,别把它和splice混淆了哈,
    • +
    • entries 返回一个新的Array Iterator对象,
    • +
    • keys 返回一个新的包含全部Array索引的 Iterator对象,
    • +
    • values 有keys就有values,返回一个新的包含全部Array值的 Iterator对象,
    • +
    • every、some、includes 返回 布尔值,
    • +
    • filter,map 返回新数组,
    • +
    • reducer 有点神奇 返回累加值,
    • +
    • find、findIndex 找元素 一个返回第一个符合的元素本身,一个返回他的索引,
    • +
    • 最后还有个flat,展平了数组,
    • +
    • forEach 遍历数组, 这么一数起来,方法还真不少呢!
    • +
    +

    总结

    想要记住所有的方法呢,不下点功夫是不行的,但是想要记住有副作用的,然后排除掉他们的话,还是容易的,最后做个表,方便直观的来看。

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    方法作用返回结果有无副作用
    push数组尾部插入元素改变了的原数组
    pop数组尾部删除元素改变了的原数组
    shift数组头部插入元素改变了的原数组
    unshift数组头部删除元素改变了的原数组
    splice增删改数组改变了的原数组
    sort数组排序改变了的原数组
    reverse数组反序改变了的原数组
    fill填充数组改变了的原数组
    concat连结数组合并的新数组
    join合并数组为字符串字符串
    indexOf查索引第一个索引
    lastIndexOf查索引最后一个索引
    slice截取数组截取的新数组
    entries迭代数组迭代器对象
    keys迭代数组的key全部的key
    values迭代数组的value全部的value
    every每个都满足布尔值
    some有一些满足布尔值
    includes包含某元素布尔值
    filter过滤数组新数组
    map加工数组新数组
    reducer迭代数组累加值
    find查找元素第一个元素或undifined
    findIndex查找元素第一个元素的index
    flat展平数组新数组
    forEach遍历数组void
    +

    就到这里,怎么样,清楚了吧。

    +]]>
    + + 前端技术 + 计算机通识 + + + 面试 + 数组 + +
    + + 服务器高危端口列表 + /2022/08/02/%E6%9C%8D%E5%8A%A1%E5%99%A8%E9%AB%98%E5%8D%B1%E7%AB%AF%E5%8F%A3%E5%88%97%E8%A1%A8/ + 服务器高危端口列表 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    协议端口服务渗透测试
    tcp20,21FTP(文件传输协议)允许匿名的上传下载,爆破,嗅探,win提权,远程执行(proftpd 1.3.5),各类后门(proftpd,vsftp 2.3.4)
    tcp22SSH (安全外壳协议 )可根据已搜集到的信息尝试爆破,v1版本可中间人,ssh隧道及内网代理转发,文件传输等等
    tcp23Telnet ( 远程终端协议)爆破,嗅探,一般常用于路由,交换登陆,可尝试弱口令
    tcp25SMTP(简单邮件传输协议)邮件伪造,vrfy/expn查询邮件用户信息,可使用smtp-user-enum工具来自动跑
    tcp/udp53DNS(域名系统)允许区域传送,dns劫持,缓存投毒,欺骗以及各种基于dns隧道的远控
    tcp/udp69TFTP (简单文件传送协议 )尝试下载目标及其的各类重要配置文件
    tcp80-89,443,8440-8450,8080-8089各种常用的Web服务端口,可尝试经典的topn,vpn,owa,webmail,目标oa,各类Java控制台,各类服务器Web管理面板,各类Web中间件漏洞利用,各类Web框架漏洞利用等等……
    tcp110POP3(邮局协议版本3 )可尝试爆破,嗅探
    tcp111,2049NFS(网络文件系统)权限配置不当
    tcp137,139,445SMB(NETBIOS协议)可尝试爆破以及smb自身的各种远程执行类漏洞利用,如,ms08-067,ms17-010,嗅探等……
    tcp143IMAP(邮件访问协议)可尝试爆破
    udp161SNMP(简单网络管理协议)爆破默认团队字符串,搜集目标内网信息
    tcp389LDAP( 轻量目录访问协议 )ldap注入,允许匿名访问,弱口令
    tcp512,513,514Linux rexec (远程登录)可爆破,rlogin登陆
    tcp873Rsync (数据镜像备份工具)匿名访问,文件上传
    tcp1194OpenVPN(虚拟专用通道)想办法钓VPN账号,进内网
    tcp1352Lotus(Lotus软件)弱口令,信息泄漏,爆破
    tcp1433SQL Server(数据库管理系统)注入,提权,sa弱口令,爆破
    tcp1521Oracle(甲骨文数据库)tns爆破,注入,弹shell…
    tcp1500ISPmanager( 主机控制面板)弱口令
    tcp1723PPTP(点对点隧道协议 )爆破,想办法钓VPN账号,进内网
    tcp2082,2083cPanel (虚拟机控制系统 )弱口令
    tcp2181ZooKeeper(分布式系统的可靠协调系统 )未授权访问
    tcp2601,2604Zebra (zebra路由)默认密码zerbra
    tcp3128Squid (代理缓存服务器)弱口令
    tcp3312,3311kangle(web服务器)弱口令
    tcp3306MySQL(数据库)注入,提权,爆破
    tcp3389Windows rdp(桌面协议)shift后门[需要03以下的系统],爆破,ms12-020
    tcp3690SVN(开放源代码的版本控制系统)svn泄露,未授权访问
    tcp4848GlassFish(应用服务器)弱口令
    tcp5000Sybase/DB2(数据库)爆破,注入
    tcp5432PostgreSQL(数据库)爆破,注入,弱口令
    tcp5900,5901,5902VNC(虚拟网络控制台,远控)弱口令爆破
    tcp5984CouchDB(数据库)未授权导致的任意指令执行
    tcp6379Redis(数据库)可尝试未授权访问,弱口令爆破
    tcp7001,7002WebLogic(WEB应用系统)Java反序列化,弱口令
    tcp7778Kloxo(虚拟主机管理系统)主机面板登录
    tcp8000Ajenti(Linux服务器管理面板)弱口令
    tcp8443Plesk(虚拟主机管理面板)弱口令
    tcp8069Zabbix (系统网络监视)远程执行,SQL注入
    tcp8080-8089Jenkins,JBoss (应用服务器)反序列化,控制台弱口令
    tcp9080-9081,9090WebSphere(应用服务器)Java反序列化/弱口令
    tcp9200,9300ElasticSearch (Lucene的搜索服务器)远程执行
    tcp11211Memcached(缓存系统)未授权访问
    tcp27017,27018MongoDB(数据库)爆破,未授权访问
    tcp50070,50030Hadoop(分布式文件系统)默认端口未授权访问
    +]]>
    + + Infrastructure + + + port + +
    + + 架构实践 + /2022/06/15/%E6%9E%B6%E6%9E%84%E5%AE%9E%E8%B7%B5/ + 架构实践

    架构设计内容一览

    应用架构设计

      +
    • Transaction执行控制(在线/批处理路径和控制)
    • +
    • Session控制
    • +
    • 死锁控制
    • +
    • 接口处理流程(适配器设置/文件备份)
    • +
    • 命名规范(实例/SID等)
    • +
    • 中间件参数 (初始化参数等)
    • +
    • 认证(处理流程/错误处理)
    • +
    • 帐户(用户ID体系/权限管理/申请方式)
    • +
    +

    开发架构设计

      +
    • 系统景观(各种环境名称、利用方法等)
    • +
    • 转运路线(转运路线/转运工具/承认方式)
    • +
    • 开发账户(用户ID体系/权限管理/申请方式)
    • +
    • 开发终端设置(在线批处理开发工具/目录等)
    • +
    • 开发资源管理
    • +
    • 表单、协作工具的利用方法(表单/接口ID定义规则)
    • +
    • 开发和验证备份(处理流程/文件清除/周期)
    • +
    • 数据屏蔽(処理概要/屏蔽利用规则/対象/开发环境搭建方法)
    • +
    +

    运维架构设计

      +
    • 监测规范(方式/周期/対象)
    • +
    • 审计和错误日志(日志级别/日志保留时效/删除时效)
    • +
    • 生产环境和调研环境的备份(处理流程/删除时效/周期)
    • +
    • 加密处理(安装位置、加密规则、记录水平加密方法)
    • +
    • 云安全设定
    • +
    • 增加功能设计
    • +
    +

    基础设施

      +
    • 网络设计(虚拟网/子网/URL/负载均衡访问控制/白名单)
    • +
    • 云设计(实例/NFS/IAM)
    • +
    • 存储设计 (BlockVolume/FileStorage/ObjectStorage/数据配置/数据圧縮方法)
    • +
    • 时间DNS同步化(OCI設定)
    • +
    • OS参数设定
    • +
    • Docker参数设定
    • +
    • 高可用切换 (処理方式、设定参数考量)
    • +
    • 打补丁(打补丁/执行规范)
    • +
    • 虚拟桌面设定
    • +
    +]]>
    + + 架构 + + + 架构 + +
    + + 前端常见知识点整理 ---- 网络安全(2) + /2022/06/16/%E5%89%8D%E7%AB%AF%E5%B8%B8%E8%A7%81%E7%9F%A5%E8%AF%86%E7%82%B9%E6%95%B4%E7%90%86%20----%20%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%EF%BC%882%EF%BC%89/ + 前端常见知识点整理 —- 网络安全(2)

    SQL 注入

    SQL 注入漏洞(SQL Injection)是 Web 开发中最常见的一种安全漏洞。可以用它来从数据库获取敏感信息,或者利用数据库的特性执行添加用户,导出文件等一系列恶意操作,甚至有可能获取数据库乃至系统用户最高权限。

    +

    而造成 SQL 注入的原因是因为程序没有有效的转义过滤用户的输入,使攻击者成功的向服务器提交恶意的 SQL 查询代码,程序在接收后错误的将攻击者的输入作为查询语句的一部分执行,导致原始的查询逻辑被改变,额外的执行了攻击者精心构造的恶意代码。

    +

    很多 Web 开发者没有意识到 SQL 查询是可以被篡改的,从而把 SQL 查询当作可信任的命令。殊不知,SQL 查询是可以绕开访问控制,从而绕过身份验证和权限检查的。更有甚者,有可能通过 SQL 查询去运行主机系统级的命令。

    +

    SQL 注入原理

    下面将通过一些真实的例子来详细讲解 SQL 注入的方式的原理。

    +

    考虑以下简单的管理员登录表单:

    +
    <form action="/login" method="POST">
    <p>Username: <input type="text" name="username" /></p>
    <p>Password: <input type="password" name="password" /></p>
    <p><input type="submit" value="登陆" /></p>
    </form>
    + +

    后端的 SQL 语句可能是如下这样的:

    +
    let querySQL = `
    SELECT *
    FROM user
    WHERE username='${username}'
    AND psw='${password}'
    `;
    // 接下来就是执行 sql 语句...
    + +

    目的就是来验证用户名和密码是不是正确,按理说乍一看上面的 SQL 语句也没什么毛病,确实是能够达到我们的目的,可是你只是站在用户会老老实实按照你的设计来输入的角度来看问题,如果有一个恶意攻击者输入的用户名是 zoumiaojiang' OR 1 = 1 --,密码随意输入,就可以直接登入系统了。WFT!

    +

    冷静下来思考一下,我们之前预想的真实 SQL 语句是:

    +
    SELECT * FROM user WHERE username='zoumiaojiang' AND psw='mypassword'
    + +

    可以恶意攻击者的奇怪用户名将你的 SQL 语句变成了如下形式:

    +
    SELECT * FROM user WHERE username='zoumiaojiang' OR 1 = 1 --' AND psw='xxxx'

    + +

    在 SQL 中,-- 是注释后面的内容的意思,所以查询语句就变成了:

    +
    SELECT * FROM user WHERE username='zoumiaojiang' OR 1 = 1

    + +

    这条 SQL 语句的查询条件永远为真,所以意思就是恶意攻击者不用我的密码,就可以登录进我的账号,然后可以在里面为所欲为,然而这还只是最简单的注入,牛逼的 SQL 注入高手甚至可以通过 SQL 查询去运行主机系统级的命令,将你主机里的内容一览无余,这里我也没有这个能力讲解的太深入,毕竟不是专业研究这类攻击的,但是通过以上的例子,已经了解了 SQL 注入的原理,我们基本已经能找到防御 SQL 注入的方案了。

    +

    如何预防 SQL 注入

    防止 SQL 注入主要是不能允许用户输入的内容影响正常的 SQL 语句的逻辑,当用户的输入的信息将要用来拼接 SQL 语句的话,我们应该永远选择不相信,任何内容都必须进行转义过滤,当然做到这个还是不够的,下面列出防御 SQL 注入的几点注意事项:

    +
      +
    • 严格限制Web应用的数据库的操作权限,给此用户提供仅仅能够满足其工作的最低权限,从而最大限度的减少注入攻击对数据库的危害

      +
    • +
    • 后端代码检查输入的数据是否符合预期,严格限制变量的类型,例如使用正则表达式进行一些匹配处理。

      +
    • +
    • 对进入数据库的特殊字符(',",\,<,>,&,*,; 等)进行转义处理,或编码转换。基本上所有的后端语言都有对字符串进行转义处理的方法,比如 lodash 的 lodash._escapehtmlchar 库。

      +
    • +
    • 所有的查询语句建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中,即不要直接拼接 SQL 语句。例如 Node.js 中的 mysqljs 库的 query 方法中的 ? 占位参数。

      +
    • +
    +
    mysql.query(`SELECT * FROM user WHERE username = ? AND psw = ?`, [username, psw]);
    + +
      +
    • 在应用发布之前建议使用专业的 SQL 注入检测工具进行检测,以及时修补被发现的 SQL 注入漏洞。网上有很多这方面的开源工具,例如 sqlmap、SQLninja 等。

      +
    • +
    • 避免网站打印出 SQL 错误信息,比如类型错误、字段不匹配等,把代码里的 SQL 语句暴露出来,以防止攻击者利用这些错误信息进行 SQL 注入。

      +
    • +
    • 不要过于细化返回的错误信息,如果目的是方便调试,就去使用后端日志,不要在接口上过多的暴露出错信息,毕竟真正的用户不关心太多的技术细节,只要话术合理就行。

      +
    • +
    +

    碰到要操作的数据库的代码,一定要慎重,小心使得万年船,多找几个人多来几次 code review,将问题都暴露出来,而且要善于利用工具,操作数据库相关的代码属于机密,没事不要去各种论坛晒自家站点的 SQL 语句,万一被人盯上了呢?

    +

    命令行注入

    命令行注入漏洞,指的是攻击者能够通过 HTTP 请求直接侵入主机,执行攻击者预设的 shell 命令,听起来好像匪夷所思,这往往是 Web 开发者最容易忽视但是却是最危险的一个漏洞之一,看一个实例:

    +

    假如现在需要实现一个需求:用户提交一些内容到服务器,然后在服务器执行一些系统命令去产出一个结果返回给用户,接口的部分实现如下:

    +
    // 以 Node.js 为例,假如在接口中需要从 github 下载用户指定的 repo
    const exec = require('mz/child_process').exec;
    let params = {/* 用户输入的参数 */};

    exec(`git clone ${params.repo} /some/path`);
    + +

    这段代码确实能够满足业务需求,正常的用户也确实能从指定的 git repo 上下载到想要的代码,可是和 SQL 注入一样,这段代码在恶意攻击者眼中,简直就是香饽饽。

    +

    如果 params.repo 传入的是 https://github.com/zoumiaojiang/zoumiaojiang.github.io.git 当然没问题了。
    可是如果 params.repo 传入的是 https://github.com/xx/xx.git && rm -rf /* && 恰好你的服务是用 root 权限起的就惨了。

    +

    具体恶意攻击者能用命令行注入干什么也像 SQL 注入一样,手法是千变万化的,比如「反弹 shell 注入」等,但原理都是一样的,我们绝对有能力防止命令行注入发生。防止命令行注入需要做到以下几件事情:

    +
      +
    • 后端对前端提交内容需要完全选择不相信,并且对其进行规则限制(比如正则表达式)。
    • +
    • 在调用系统命令前对所有传入参数进行命令行参数转义过滤。
    • +
    • 不要直接拼接命令语句,借助一些工具做拼接、转义预处理,例如 Node.js 的 shell-escape npm 包。
    • +
    +

    还是前面的例子,我们可以做到如下:

    +
    const exec = require('mz/child_process').exec;

    // 借助 shell-escape npm 包解决参数转义过滤问题
    const shellescape = require('shell-escape');

    let params = {/* 用户输入的参数 */};

    // 先过滤一下参数,让参数符合预期
    if (!/正确的表达式/.test(params.repo)) {
    return;
    }

    let cmd = shellescape([
    'git',
    'clone',
    params.repo,
    '/some/path'
    ]);

    // cmd 的值: git clone 'https://github.com/xx/xx.git && rm -rf / &&' /some/path
    // 这样就不会被注入成功了。
    exec(cmd);
    +

    无论是在何种后端语言环境中,凡是涉及到代码调用系统 shell 命令的时候都一定要谨慎。

    +

    DDoS 攻击

    DDoS 又叫分布式拒绝服务,全称 Distributed Denial of Service,其原理就是利用大量的请求造成资源过载,导致服务不可用,这个攻击应该不能算是安全问题,这应该算是一个另类的存在,因为这种攻击根本就是耍流氓的存在,「伤敌一千,自损八百」的行为。出于保护 Web App 不受攻击的攻防角度,还是介绍一下 DDoS 攻击吧,毕竟也是挺常见的。

    +

    DDoS 攻击可以理解为:「你开了一家店,隔壁家点看不惯,就雇了一大堆黑社会人员进你店里干坐着,也不消费,其他客人也进不来,导致你营业惨淡」。为啥说 DDoS 是个「伤敌一千,自损八百」的行为呢?毕竟隔壁店还是花了不少钱雇黑社会但是啥也没得到不是?DDoS 攻击的目的基本上就以下几个:

    +
      +
    • 深仇大恨,就是要干死你
    • +
    • 敲诈你,不给钱就干你
    • +
    • 忽悠你,不买我防火墙服务就会有“人”继续干你
    • +
    +

    也许你的站点遭受过 DDoS 攻击,具体什么原因怎么解读见仁见智。DDos 攻击从层次上可分为网络层攻击与应用层攻击,从攻击手法上可分为快型流量攻击与慢型流量攻击,但其原理都是造成资源过载,导致服务不可用。

    +

    网络层 DDoS

    网络层 DDos 攻击包括 SYN FloodACK FloodUDP FloodICMP Flood 等。

    +

    SYN Flood 攻击

    SYN flood 攻击主要利用了 TCP 三次握手过程中的 Bug,我们都知道 TCP 三次握手过程是要建立连接的双方发送 SYN,SYN + ACK,ACK 数据包,而当攻击方随意构造源 IP 去发送 SYN 包时,服务器返回的 SYN + ACK 就不能得到应答(因为 IP 是随意构造的),此时服务器就会尝试重新发送,并且会有至少 30s 的等待时间,导致资源饱和服务不可用,此攻击属于慢型 DDoS 攻击。

    +

    ACK Flood 攻击

    ACK Flood 攻击是在 TCP 连接建立之后,所有的数据传输 TCP 报文都是带有 ACK 标志位的,主机在接收到一个带有 ACK 标志位的数据包的时候,需要检查该数据包所表示的连接四元组是否存在,如果存在则检查该数据包所表示的状态是否合法,然后再向应用层传递该数据包。如果在检查中发现该数据包不合法,例如该数据包所指向的目的端口在本机并未开放,则主机操作系统协议栈会回应 RST 包告诉对方此端口不存在。

    +

    UDP Flood 攻击

    UDP flood 攻击是由于 UDP 是一种无连接的协议,因此攻击者可以伪造大量的源 IP 地址去发送 UDP 包,此种攻击属于大流量攻击。正常应用情况下,UDP 包双向流量会基本相等,因此发起这种攻击的攻击者在消耗对方资源的时候也在消耗自己的资源。

    +

    ICMP Flood 攻击

    ICMP Flood 攻击属于大流量攻击,其原理就是不断发送不正常的 ICMP 包(所谓不正常就是 ICMP 包内容很大),导致目标带宽被占用,但其本身资源也会被消耗。目前很多服务器都是禁 ping 的(在防火墙在可以屏蔽 ICMP 包),因此这种攻击方式已经落伍。

    +

    网络层 DDoS 防御

    网络层的 DDoS 攻击究其本质其实是无法防御的,我们能做得就是不断优化服务本身部署的网络架构,以及提升网络带宽。当然,还是做好以下几件事也是有助于缓解网络层 DDoS 攻击的冲击:

    +
      +
    • 网络架构上做好优化,采用负载均衡分流。
    • +
    • 确保服务器的系统文件是最新的版本,并及时更新系统补丁。
    • +
    • 添加抗 DDos 设备,进行流量清洗。
    • +
    • 限制同时打开的 SYN 半连接数目,缩短 SYN 半连接的 Timeout 时间。
    • +
    • 限制单 IP 请求频率。
    • +
    • 防火墙等防护设置禁止 ICMP 包等。
    • +
    • 严格限制对外开放的服务器的向外访问。
    • +
    • 运行端口映射程序或端口扫描程序,要认真检查特权端口和非特权端口。
    • +
    • 关闭不必要的服务。
    • +
    • 认真检查网络设备和主机/服务器系统的日志。只要日志出现漏洞或是时间变更,那这台机器就可能遭到了攻击。
    • +
    • 限制在防火墙外与网络文件共享。这样会给黑客截取系统文件的机会,主机的信息暴露给黑客,无疑是给了对方入侵的机会。
    • +
    • 加钱堆机器。。
    • +
    • 报警。。-
    • +
    +

    应用层 DDoS

    应用层 DDoS 攻击不是发生在网络层,是发生在 TCP 建立握手成功之后,应用程序处理请求的时候,现在很多常见的 DDoS 攻击都是应用层攻击。应用层攻击千变万化,目的就是在网络应用层耗尽你的带宽,下面列出集中典型的攻击类型。

    +

    CC 攻击

    当时绿盟为了防御 DDoS 攻击研发了一款叫做 Collapasar 的产品,能够有效的防御 SYN Flood 攻击。黑客为了挑衅,研发了一款 Challenge Collapasar 攻击工具(简称 CC)。

    +

    CC 攻击的原理,就是针对消耗资源比较大的页面不断发起不正常的请求,导致资源耗尽。因此在发送 CC 攻击前,我们需要寻找加载比较慢,消耗资源比较多的网页,比如需要查询数据库的页面、读写硬盘文件的等。通过 CC 攻击,使用爬虫对某些加载需要消耗大量资源的页面发起 HTTP 请求。

    +

    DNS Flood

    DNS Flood 攻击采用的方法是向被攻击的服务器发送大量的域名解析请求,通常请求解析的域名是随机生成或者是网络世界上根本不存在的域名,被攻击的DNS 服务器在接收到域名解析请求的时候首先会在服务器上查找是否有对应的缓存,如果查找不到并且该域名无法直接由服务器解析的时候,DNS 服务器会向其上层 DNS 服务器递归查询域名信息。域名解析的过程给服务器带来了很大的负载,每秒钟域名解析请求超过一定的数量就会造成 DNS 服务器解析域名超时。

    +

    根据微软的统计数据,一台 DNS 服务器所能承受的动态域名查询的上限是每秒钟 9000 个请求。而我们知道,在一台 P3 的 PC 机上可以轻易地构造出每秒钟几万个域名解析请求,足以使一台硬件配置极高的 DNS 服务器瘫痪,由此可见 DNS 服务器的脆弱性。

    +

    HTTP 慢速连接攻击

    针对 HTTP 协议,先建立起 HTTP 连接,设置一个较大的 Conetnt-Length,每次只发送很少的字节,让服务器一直以为 HTTP 头部没有传输完成,这样连接一多就很快会出现连接耗尽。

    +

    应用层 DDoS 防御

      +
    • 判断 User-Agent 字段(不可靠,因为可以随意构造)
    • +
    • 针对 IP + cookie,限制访问频率(由于 cookie 可以更改,IP 可以使用代理,或者肉鸡,也不可靠)
    • +
    • 关闭服务器最大连接数等,合理配置中间件,缓解 DDoS 攻击。
    • +
    • 请求中添加验证码,比如请求中有数据库操作的时候。
    • +
    • 编写代码时,尽量实现优化,并合理使用缓存技术,减少数据库的读取操作。
    • +
    • 加钱堆机器。。
    • +
    • 报警。。
    • +
    +

    应用层的防御有时比网络层的更难,因为导致应用层被 DDoS 攻击的因素非常多,有时往往是因为程序员的失误,导致某个页面加载需要消耗大量资源,有时是因为中间件配置不当等等。而应用层 DDoS 防御的核心就是区分人与机器(爬虫),因为大量的请求不可能是人为的,肯定是机器构造的。因此如果能有效的区分人与爬虫行为,则可以很好地防御此攻击。

    +

    其他 DDoS 攻击

    发起 DDoS 也是需要大量的带宽资源的,但是互联网就像森林,林子大了什么鸟都有,DDoS 攻击者也能找到其他的方式发起廉价并且极具杀伤力的 DDoS 攻击。

    +

    利用 XSS

    举个例子,如果 12306 页面有一个 XSS 持久型漏洞被恶意攻击者发现,只需在春节抢票期间在这个漏洞中执行脚本使得往某一个小站点随便发点什么请求,然后随着用户访问的增多,感染用户增多,被攻击的站点自然就会迅速瘫痪了。这种 DDoS 简直就是无本万利,不用惊讶,现在大站有 XSS 漏洞的不要太多。

    +

    来自 P2P 网络攻击

    大家都知道,互联网上的 P2P 用户和流量都是一个极为庞大的数字。如果他们都去一个指定的地方下载数据,成千上万的真实 IP 地址连接过来,没有哪个设备能够支撑住。拿 BT 下载来说,伪造一些热门视频的种子,发布到搜索引擎,就足以骗到许多用户和流量了,但是这只是基础攻击。
    高级的 P2P 攻击,是直接欺骗资源管理服务器。如迅雷客户端会把自己发现的资源上传到资源管理服务器,然后推送给其它需要下载相同资源的用户,这样,一个链接就发布出去。通过协议逆向,攻击者伪造出大批量的热门资源信息通过资源管理中心分发出去,瞬间就可以传遍整个 P2P 网络。更为恐怖的是,这种攻击是无法停止的,即使是攻击者自身也无法停止,攻击一直持续到 P2P 官方发现问题更新服务器且下载用户重启下载软件为止。

    +

    DDoS 最后总结

    DDoS 不可能防的住,就好比你的店只能容纳 50 人,黑社会有 100 人,你就换一家大店,能容纳 500 人,然后黑社会又找来了 1000 人,这种堆人头的做法就是 DDoS 本质上的攻防之道,「道高一尺,魔高一丈,魔高一尺,道高一丈」,讲真,必要的时候就答应勒索你的人的条件吧,实在不行就报警吧。

    +

    流量劫持

    流量劫持应该算是黑产行业的一大经济支柱了吧?简直是让人恶心到吐,不吐槽了,还是继续谈干货吧,流量劫持基本分两种:DNS 劫持 和 HTTP 劫持,目的都是一样的,就是当用户访问 zoumiaojiang.com 的时候,给你展示的并不是或者不完全是 zoumiaojiang.com 提供的 “内容”。

    +

    DNS 劫持

    DNS 劫持,也叫做域名劫持,可以这么理解,「你打了一辆车想去商场吃饭,结果你打的车是小作坊派来的,直接给你拉到小作坊去了」,DNS 的作用是把网络地址域名对应到真实的计算机能够识别的 IP 地址,以便计算机能够进一步通信,传递网址和内容等。如果当用户通过某一个域名访问一个站点的时候,被篡改的 DNS 服务器返回的是一个恶意的钓鱼站点的 IP,用户就被劫持到了恶意钓鱼站点,然后继而会被钓鱼输入各种账号密码信息,泄漏隐私。

    +

    这类劫持,要不就是网络运营商搞的鬼,一般小的网络运营商与黑产勾结会劫持 DNS,要不就是电脑中毒,被恶意篡改了路由器的 DNS 配置,基本上做为开发者或站长却是很难察觉的,除非有用户反馈,现在升级版的 DNS 劫持还可以对特定用户、特定区域等使用了用户画像进行筛选用户劫持的办法,另外这类广告显示更加随机更小,一般站长除非用户投诉否则很难觉察到,就算觉察到了取证举报更难。无论如何,如果接到有 DNS 劫持的反馈,一定要做好以下几件事:

    +
      +
    • 取证很重要,时间、地点、IP、拨号账户、截屏、URL 地址等一定要有。
    • +
    • 可以跟劫持区域的电信运营商进行投诉反馈。
    • +
    • 如果投诉反馈无效,直接去工信部投诉,一般来说会加白你的域名。
    • +
    +

    HTTP 劫持

    HTTP 劫持您可以这么理解,「你打了一辆车想去商场吃饭,结果司机跟你一路给你递小作坊的广告」,HTTP 劫持主要是当用户访问某个站点的时候会经过运营商网络,而不法运营商和黑产勾结能够截获 HTTP 请求返回内容,并且能够篡改内容,然后再返回给用户,从而实现劫持页面,轻则插入小广告,重则直接篡改成钓鱼网站页面骗用户隐私。能够实施流量劫持的根本原因,是 HTTP 协议没有办法对通信对方的身份进行校验以及对数据完整性进行校验。如果能解决这个问题,则流量劫持将无法轻易发生。所以防止 HTTP 劫持的方法只有将内容加密,让劫持者无法破解篡改,这样就可以防止 HTTP 劫持了。

    +

    HTTPS 协议就是一种基于 SSL 协议的安全加密网络应用层协议,可以很好的防止 HTTP 劫持。这里有篇 文章 讲的不错。HTTPS 在这就不深讲了,后面有机会我会单独好好讲讲 HTTPS。如果不想站点被 HTTP 劫持,赶紧将你的站点全站改造成 HTTPS 吧。

    +

    服务器漏洞

    服务器除了以上提到的那些大名鼎鼎的漏洞和臭名昭著的攻击以外,其实还有很多其他的漏洞,往往也很容易被忽视,在这个小节也稍微介绍几种。

    +

    越权操作漏洞

    如果你的系统是有登录控制的,那就要格外小心了,因为很有可能你的系统越权操作漏洞,越权操作漏洞可以简单的总结为 「A 用户能看到或者操作 B 用户的隐私内容」,如果你的系统中还有权限控制就更加需要小心了。所以每一个请求都需要做 userid 的判断

    +

    以下是一段有漏洞的后端示意代码:

    +
    // ctx 为请求的 context 上下文
    let msgId = ctx.params.msgId;

    mysql.query(
    'SELECT * FROM msg_table WHERE msg_id = ?',
    [msgId]
    );
    + +

    以上代码是任何人都可以查询到任何用户的消息,只要有 msg_id 就可以,这就是比较典型的越权漏洞

    +

    越权操作漏洞防御

    如下这么改进一下:

    +
    // ctx 为请求的 context 上下文
    let msgId = ctx.params.msgId;
    let userId = ctx.session.userId; // 从会话中取出当前登陆的 userId

    mysql.query(
    'SELECT * FROM msg_table WHERE msg_id = ? AND user_id = ?',
    [msgId, userId]
    );
    +

    嗯,大概就是这个意思,如果有更严格的权限控制,那在每个请求中凡是涉及到数据库的操作都需要先进行严格的验证,并且在设计数据库表的时候需要考虑进 userId 的账号关联以及权限关联。

    +

    目录遍历漏洞

    目录遍历漏洞指通过在 URL 或参数中构造 .././ 和类似的跨父目录字符串的 ASCII 编码、unicode 编码等,完成目录跳转,读取操作系统各个目录下的敏感文件,也可以称作「任意文件读取漏洞」。

    +

    目录遍历漏洞原理:程序没有充分过滤用户输入的 ../ 之类的目录跳转符,导致用户可以通过提交目录跳转来遍历服务器上的任意文件。使用多个.. 符号,不断向上跳转,最终停留在根 /,通过绝对路径去读取任意文件。

    +

    目录遍历漏洞几个示例和测试,一般构造 URL 然后使用浏览器直接访问,或者使用 Web 漏洞扫描工具检测,当然也可以自写程序测试。

    +
    http://somehost.com/../../../../../../../../../etc/passwd
    http://somehost.com/some/path?file=../../Windows/system.ini

    # 借助 %00 空字符截断是一个比较经典的攻击手法
    http://somehost.com/some/path?file=../../Windows/system.ini%00.js

    # 使用了 IIS 的脚本目录来移动目录并执行指令
    http://somehost.com/scripts/..%5c../Windows/System32/cmd.exe?/c+dir+c:\

    +

    目录遍历漏洞防御

    方法就是需要对 URL 或者参数进行 ../,./ 等字符的转义过滤。

    +

    物理路径泄漏

    物理路径泄露属于低风险等级缺陷,它的危害一般被描述为「攻击者可以利用此漏洞得到信息,来对系统进一步地攻击」,通常都是系统报错 500 的错误信息直接返回到页面可见导致的漏洞。得到物理路径有些时候它能给攻击者带来一些有用的信息,比如说:可以大致了解系统的文件目录结构;可以看出系统所使用的第三方软件;也说不定会得到一个合法的用户名(因为很多人把自己的用户名作为网站的目录名)。

    +

    物理路径泄漏防御

    防止这种泄漏的方法就是做好后端程序的出错处理,定制特殊的 500 报错页面。

    +

    源码暴露漏洞

    和物理路径泄露类似,就是攻击者可以通过请求直接获取到你站点的后端源代码,然后就可以对系统进一步研究攻击。那么导致源代码暴露的原因是什么呢?基本上就是发生在服务器配置上了,服务器可以设置哪些路径的文件才可以被直接访问的,这里给一个 koa 服务起的例子,正常的 koa 服务器可以通过 koa-static 中间件去指定静态资源的目录,好让静态资源可以通过路径的路由访问。比如你的系统源代码目录是这样的:

    +
    |- project
    |- src
    |- static
    |- ...
    |- server.js

    + +

    你想要将 static 的文件夹配成静态资源目录,你应该会在 server.js 做如下配置:

    +
    const Koa = require('koa');
    const serve = require('koa-static');
    const app = new Koa();

    app.use(serve(__dirname + '/project/static'));

    + +

    但是如果配错了静态资源的目录,可能就出大事了,比如:

    +
    // ...
    app.use(serve(__dirname + '/project'));

    + +

    这样所有的源代码都可以通过路由访问到了,所有的服务器都提供了静态资源机制,所以在通过服务器配置静态资源目录和路径的时候,一定要注意检验,不然很可能产生漏洞。

    +

    最后,希望 Web 开发者们能够管理好自己的代码隐私,注意代码安全问题,比如不要将产品的含有敏感信息的代码放到第三方外部站点或者暴露给外部用户,尤其是前端代码,私钥类似的保密性的东西不要直接输出在代码里或者页面中。也许还有很多值得注意的点,但是归根结底还是绷住安全那根弦,对待每一行代码都要多多推敲。

    +

    转载自:https://zoumiaojiang.com/article/common-web-security/

    +]]>
    + + 前端技术 + 前端常见知识点整理 + + + 网络安全 + +
    +
    diff --git a/tags/Actions/index.html b/tags/Actions/index.html new file mode 100644 index 0000000000..2143684f81 --- /dev/null +++ b/tags/Actions/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: Actions | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Actions + 标签 +

    +
    + + +
    + 2019 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/CI-CD/index.html b/tags/CI-CD/index.html new file mode 100644 index 0000000000..3262cbdcef --- /dev/null +++ b/tags/CI-CD/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: CI/CD | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    CI/CD + 标签 +

    +
    + + +
    + 2019 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/CSS/index.html b/tags/CSS/index.html new file mode 100644 index 0000000000..1a0455f019 --- /dev/null +++ b/tags/CSS/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: CSS | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    CSS + 标签 +

    +
    + + +
    + 2017 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/CSS3/index.html b/tags/CSS3/index.html new file mode 100644 index 0000000000..f82b8df1b5 --- /dev/null +++ b/tags/CSS3/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: CSS3 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    CSS3 + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/CSS\346\212\200\345\267\247/index.html" "b/tags/CSS\346\212\200\345\267\247/index.html" new file mode 100644 index 0000000000..31bd3bb19f --- /dev/null +++ "b/tags/CSS\346\212\200\345\267\247/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: CSS技巧 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    CSS技巧 + 标签 +

    +
    + + +
    + 2013 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/CodeReview/index.html b/tags/CodeReview/index.html new file mode 100644 index 0000000000..966c3fe9c6 --- /dev/null +++ b/tags/CodeReview/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: CodeReview | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    CodeReview + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Cordova/index.html b/tags/Cordova/index.html new file mode 100644 index 0000000000..6711ee42db --- /dev/null +++ b/tags/Cordova/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: Cordova | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Cordova + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Docker-Compose/index.html b/tags/Docker-Compose/index.html new file mode 100644 index 0000000000..10851d985c --- /dev/null +++ b/tags/Docker-Compose/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: Docker Compose | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Docker Compose + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Docker/index.html b/tags/Docker/index.html new file mode 100644 index 0000000000..4af93b6f1c --- /dev/null +++ b/tags/Docker/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: Docker | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Docker + 标签 +

    +
    + + +
    + 2022 +
    + + + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/DockerFile/index.html b/tags/DockerFile/index.html new file mode 100644 index 0000000000..926447fa28 --- /dev/null +++ b/tags/DockerFile/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: DockerFile | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    DockerFile + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/ESLint/index.html b/tags/ESLint/index.html new file mode 100644 index 0000000000..8dc6e60e16 --- /dev/null +++ b/tags/ESLint/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: ESLint | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    ESLint + 标签 +

    +
    + + +
    + 2017 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Electron/index.html b/tags/Electron/index.html new file mode 100644 index 0000000000..e49d529461 --- /dev/null +++ b/tags/Electron/index.html @@ -0,0 +1,481 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: Electron | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Electron + 标签 +

    +
    + + +
    + 2022 +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/EventLoop/index.html b/tags/EventLoop/index.html new file mode 100644 index 0000000000..544bff5f5e --- /dev/null +++ b/tags/EventLoop/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: EventLoop | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    EventLoop + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/GIT/index.html b/tags/GIT/index.html new file mode 100644 index 0000000000..ea49be51f9 --- /dev/null +++ b/tags/GIT/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: GIT | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    GIT + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Git/index.html b/tags/Git/index.html new file mode 100644 index 0000000000..775e02f4f0 --- /dev/null +++ b/tags/Git/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: Git | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Git + 标签 +

    +
    + + +
    + 2017 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/GitLab/index.html b/tags/GitLab/index.html new file mode 100644 index 0000000000..e6776ab7c0 --- /dev/null +++ b/tags/GitLab/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: GitLab | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    GitLab + 标签 +

    +
    + + +
    + 2019 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Github/index.html b/tags/Github/index.html new file mode 100644 index 0000000000..5dc47e5488 --- /dev/null +++ b/tags/Github/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: Github | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Github + 标签 +

    +
    + + +
    + 2019 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/Go\350\257\255\350\250\200/index.html" "b/tags/Go\350\257\255\350\250\200/index.html" new file mode 100644 index 0000000000..1b31a05175 --- /dev/null +++ "b/tags/Go\350\257\255\350\250\200/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: Go语言 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Go语言 + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Hexo/index.html b/tags/Hexo/index.html new file mode 100644 index 0000000000..7a607e226a --- /dev/null +++ b/tags/Hexo/index.html @@ -0,0 +1,547 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: Hexo | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Hexo + 标签 +

    +
    + + +
    + 2022 +
    + + +
    + 2020 +
    + + +
    + 2019 +
    + + +
    + 2017 +
    + + + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/HomeBrew/index.html b/tags/HomeBrew/index.html new file mode 100644 index 0000000000..6a9e7e6ce6 --- /dev/null +++ b/tags/HomeBrew/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: HomeBrew | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    HomeBrew + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Ionic/index.html b/tags/Ionic/index.html new file mode 100644 index 0000000000..39e6213812 --- /dev/null +++ b/tags/Ionic/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: Ionic | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Ionic + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Linux/index.html b/tags/Linux/index.html new file mode 100644 index 0000000000..f09d58ee58 --- /dev/null +++ b/tags/Linux/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: Linux | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Linux + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Mac/index.html b/tags/Mac/index.html new file mode 100644 index 0000000000..00daf4d053 --- /dev/null +++ b/tags/Mac/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: Mac | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Mac + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Nginx/index.html b/tags/Nginx/index.html new file mode 100644 index 0000000000..a572fa679d --- /dev/null +++ b/tags/Nginx/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: Nginx | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Nginx + 标签 +

    +
    + + +
    + 2022 +
    + + + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/NodeJS/index.html b/tags/NodeJS/index.html new file mode 100644 index 0000000000..434ab90869 --- /dev/null +++ b/tags/NodeJS/index.html @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: NodeJS | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    NodeJS + 标签 +

    +
    + + +
    + 2020 +
    + + + + +
    + 2017 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Postgresql/index.html b/tags/Postgresql/index.html new file mode 100644 index 0000000000..546ffb8795 --- /dev/null +++ b/tags/Postgresql/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: Postgresql | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Postgresql + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Prettier/index.html b/tags/Prettier/index.html new file mode 100644 index 0000000000..a4c9c90f94 --- /dev/null +++ b/tags/Prettier/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: Prettier | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Prettier + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/RESTful/index.html b/tags/RESTful/index.html new file mode 100644 index 0000000000..6d1e5e032c --- /dev/null +++ b/tags/RESTful/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: RESTful | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    RESTful + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/RPM/index.html b/tags/RPM/index.html new file mode 100644 index 0000000000..d2a21262fc --- /dev/null +++ b/tags/RPM/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: RPM | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    RPM + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/React/index.html b/tags/React/index.html new file mode 100644 index 0000000000..31b3728694 --- /dev/null +++ b/tags/React/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: React | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    React + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Restful-API/index.html b/tags/Restful-API/index.html new file mode 100644 index 0000000000..3bef2c16bb --- /dev/null +++ b/tags/Restful-API/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: Restful API | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Restful API + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/SSR/index.html b/tags/SSR/index.html new file mode 100644 index 0000000000..6f9d18a06c --- /dev/null +++ b/tags/SSR/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: SSR | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    SSR + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/SemVer/index.html b/tags/SemVer/index.html new file mode 100644 index 0000000000..91df1f49ed --- /dev/null +++ b/tags/SemVer/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: SemVer | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    SemVer + 标签 +

    +
    + + +
    + 2021 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Squid/index.html b/tags/Squid/index.html new file mode 100644 index 0000000000..8be963bbb7 --- /dev/null +++ b/tags/Squid/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: Squid | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Squid + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Timeout/index.html b/tags/Timeout/index.html new file mode 100644 index 0000000000..4b5b61d5fb --- /dev/null +++ b/tags/Timeout/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: Timeout | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Timeout + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/UglifyJsPlugin/index.html b/tags/UglifyJsPlugin/index.html new file mode 100644 index 0000000000..fd8a833bb7 --- /dev/null +++ b/tags/UglifyJsPlugin/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: UglifyJsPlugin | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    UglifyJsPlugin + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/VSCode/index.html b/tags/VSCode/index.html new file mode 100644 index 0000000000..0aca16f695 --- /dev/null +++ b/tags/VSCode/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: VSCode | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    VSCode + 标签 +

    +
    + + +
    + 2017 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/VSCode\346\217\222\344\273\266/index.html" "b/tags/VSCode\346\217\222\344\273\266/index.html" new file mode 100644 index 0000000000..298ef479ae --- /dev/null +++ "b/tags/VSCode\346\217\222\344\273\266/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: VSCode插件 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    VSCode插件 + 标签 +

    +
    + + +
    + 2021 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Vue/index.html b/tags/Vue/index.html new file mode 100644 index 0000000000..ae11921b46 --- /dev/null +++ b/tags/Vue/index.html @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: Vue | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Vue + 标签 +

    +
    + + +
    + 2020 +
    + + +
    + 2017 +
    + + + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Webpack/index.html b/tags/Webpack/index.html new file mode 100644 index 0000000000..9821cd8560 --- /dev/null +++ b/tags/Webpack/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: Webpack | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Webpack + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/ajax/index.html b/tags/ajax/index.html new file mode 100644 index 0000000000..cf70dfbe08 --- /dev/null +++ b/tags/ajax/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: ajax | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    ajax + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/es6/index.html b/tags/es6/index.html new file mode 100644 index 0000000000..954f076efd --- /dev/null +++ b/tags/es6/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: es6 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    es6 + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/get/index.html b/tags/get/index.html new file mode 100644 index 0000000000..8ad1e4fd73 --- /dev/null +++ b/tags/get/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: get | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    get + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/index.html b/tags/index.html new file mode 100644 index 0000000000..ad1f14d478 --- /dev/null +++ b/tags/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签 | 李旭光的成长博客 + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + + + + + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/jsonp/index.html b/tags/jsonp/index.html new file mode 100644 index 0000000000..a3cc2639e9 --- /dev/null +++ b/tags/jsonp/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: jsonp | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    jsonp + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/kernel/index.html b/tags/kernel/index.html new file mode 100644 index 0000000000..ef4ee9f733 --- /dev/null +++ b/tags/kernel/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: kernel | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    kernel + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/npm/index.html b/tags/npm/index.html new file mode 100644 index 0000000000..70c407cca1 --- /dev/null +++ b/tags/npm/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: npm | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    npm + 标签 +

    +
    + + +
    + 2017 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/npx/index.html b/tags/npx/index.html new file mode 100644 index 0000000000..bdb263895a --- /dev/null +++ b/tags/npx/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: npx | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    npx + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/path-intellisense/index.html b/tags/path-intellisense/index.html new file mode 100644 index 0000000000..edb522af8c --- /dev/null +++ b/tags/path-intellisense/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: path-intellisense | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    path-intellisense + 标签 +

    +
    + + +
    + 2021 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/port/index.html b/tags/port/index.html new file mode 100644 index 0000000000..897c0009bf --- /dev/null +++ b/tags/port/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: port | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    port + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/promise/index.html b/tags/promise/index.html new file mode 100644 index 0000000000..a41d20b37e --- /dev/null +++ b/tags/promise/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: promise | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    promise + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/registry/index.html b/tags/registry/index.html new file mode 100644 index 0000000000..a5d6860be9 --- /dev/null +++ b/tags/registry/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: registry | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    registry + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/rnpm/index.html b/tags/rnpm/index.html new file mode 100644 index 0000000000..471e948fc6 --- /dev/null +++ b/tags/rnpm/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: rnpm | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    rnpm + 标签 +

    +
    + + +
    + 2017 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/sass/index.html b/tags/sass/index.html new file mode 100644 index 0000000000..68983bf851 --- /dev/null +++ b/tags/sass/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: sass | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    sass + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/scp/index.html b/tags/scp/index.html new file mode 100644 index 0000000000..f2f6f2868a --- /dev/null +++ b/tags/scp/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: scp | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    scp + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/systemctl/index.html b/tags/systemctl/index.html new file mode 100644 index 0000000000..56c17bf6e0 --- /dev/null +++ b/tags/systemctl/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: systemctl | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    systemctl + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/vuepress/index.html b/tags/vuepress/index.html new file mode 100644 index 0000000000..32c5543056 --- /dev/null +++ b/tags/vuepress/index.html @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: vuepress | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    vuepress + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\344\272\213\344\273\266\351\251\261\345\212\250/index.html" "b/tags/\344\272\213\344\273\266\351\251\261\345\212\250/index.html" new file mode 100644 index 0000000000..577a5d0c7e --- /dev/null +++ "b/tags/\344\272\213\344\273\266\351\251\261\345\212\250/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 事件驱动 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    事件驱动 + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\345\205\274\345\256\271\346\200\247\351\227\256\351\242\230/index.html" "b/tags/\345\205\274\345\256\271\346\200\247\351\227\256\351\242\230/index.html" new file mode 100644 index 0000000000..85279fb64d --- /dev/null +++ "b/tags/\345\205\274\345\256\271\346\200\247\351\227\256\351\242\230/index.html" @@ -0,0 +1,481 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 兼容性问题 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    兼容性问题 + 标签 +

    +
    + + +
    + 2019 +
    + + +
    + 2017 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\345\206\205\345\255\230\345\233\236\346\224\266/index.html" "b/tags/\345\206\205\345\255\230\345\233\236\346\224\266/index.html" new file mode 100644 index 0000000000..378f67b3da --- /dev/null +++ "b/tags/\345\206\205\345\255\230\345\233\236\346\224\266/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 内存回收 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    内存回收 + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\346\214\207\345\215\227/index.html" "b/tags/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\346\214\207\345\215\227/index.html" new file mode 100644 index 0000000000..8a1cfed69a --- /dev/null +++ "b/tags/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\346\214\207\345\215\227/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 前后端分离开发指南 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    前后端分离开发指南 + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\350\247\204\350\214\203/index.html" "b/tags/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\350\247\204\350\214\203/index.html" new file mode 100644 index 0000000000..e5673016be --- /dev/null +++ "b/tags/\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\350\247\204\350\214\203/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 前后端分离开发规范 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    前后端分离开发规范 + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\345\211\215\347\253\257\345\274\200\345\217\221\346\217\222\344\273\266/index.html" "b/tags/\345\211\215\347\253\257\345\274\200\345\217\221\346\217\222\344\273\266/index.html" new file mode 100644 index 0000000000..c511e08db4 --- /dev/null +++ "b/tags/\345\211\215\347\253\257\345\274\200\345\217\221\346\217\222\344\273\266/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 前端开发插件 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    前端开发插件 + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\345\220\214\346\236\204\346\212\200\346\234\257/index.html" "b/tags/\345\220\214\346\236\204\346\212\200\346\234\257/index.html" new file mode 100644 index 0000000000..ee9f0b43f2 --- /dev/null +++ "b/tags/\345\220\214\346\236\204\346\212\200\346\234\257/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 同构技术 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    同构技术 + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\345\245\207\346\200\252\351\227\256\351\242\230/index.html" "b/tags/\345\245\207\346\200\252\351\227\256\351\242\230/index.html" new file mode 100644 index 0000000000..7ddbaef9ba --- /dev/null +++ "b/tags/\345\245\207\346\200\252\351\227\256\351\242\230/index.html" @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 奇怪问题 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    奇怪问题 + 标签 +

    +
    + + +
    + 2020 +
    + + + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\345\256\271\345\231\250\347\253\257\345\217\243\346\230\240\345\260\204/index.html" "b/tags/\345\256\271\345\231\250\347\253\257\345\217\243\346\230\240\345\260\204/index.html" new file mode 100644 index 0000000000..0e89bee32e --- /dev/null +++ "b/tags/\345\256\271\345\231\250\347\253\257\345\217\243\346\230\240\345\260\204/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 容器端口映射 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    容器端口映射 + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\345\271\264\347\273\210\346\200\273\347\273\223/index.html" "b/tags/\345\271\264\347\273\210\346\200\273\347\273\223/index.html" new file mode 100644 index 0000000000..83f118a3ff --- /dev/null +++ "b/tags/\345\271\264\347\273\210\346\200\273\347\273\223/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 年终总结 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    年终总结 + 标签 +

    +
    + + +
    + 2019 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\345\276\256\344\277\241/index.html" "b/tags/\345\276\256\344\277\241/index.html" new file mode 100644 index 0000000000..456528257e --- /dev/null +++ "b/tags/\345\276\256\344\277\241/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 微信 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    微信 + 标签 +

    +
    + + +
    + 2019 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\346\211\223\345\214\205/index.html" "b/tags/\346\211\223\345\214\205/index.html" new file mode 100644 index 0000000000..d407e734f7 --- /dev/null +++ "b/tags/\346\211\223\345\214\205/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 打包 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    打包 + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\346\216\222\345\272\217\347\256\227\346\263\225/index.html" "b/tags/\346\216\222\345\272\217\347\256\227\346\263\225/index.html" new file mode 100644 index 0000000000..76e858b182 --- /dev/null +++ "b/tags/\346\216\222\345\272\217\347\256\227\346\263\225/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 排序算法 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    排序算法 + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\346\217\222\344\273\266/index.html" "b/tags/\346\217\222\344\273\266/index.html" new file mode 100644 index 0000000000..11a4b4fe24 --- /dev/null +++ "b/tags/\346\217\222\344\273\266/index.html" @@ -0,0 +1,481 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 插件 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    插件 + 标签 +

    +
    + + +
    + 2020 +
    + + +
    + 2017 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\346\225\260\346\215\256\345\215\267/index.html" "b/tags/\346\225\260\346\215\256\345\215\267/index.html" new file mode 100644 index 0000000000..aa4b0871a7 --- /dev/null +++ "b/tags/\346\225\260\346\215\256\345\215\267/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 数据卷 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    数据卷 + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\346\225\260\347\273\204/index.html" "b/tags/\346\225\260\347\273\204/index.html" new file mode 100644 index 0000000000..e90e613179 --- /dev/null +++ "b/tags/\346\225\260\347\273\204/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 数组 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    数组 + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\346\234\215\345\212\241\347\253\257\346\270\262\346\237\223/index.html" "b/tags/\346\234\215\345\212\241\347\253\257\346\270\262\346\237\223/index.html" new file mode 100644 index 0000000000..a00a03bcc1 --- /dev/null +++ "b/tags/\346\234\215\345\212\241\347\253\257\346\270\262\346\237\223/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 服务端渲染 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    服务端渲染 + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\346\236\266\346\236\204/index.html" "b/tags/\346\236\266\346\236\204/index.html" new file mode 100644 index 0000000000..ea1c8ad1b0 --- /dev/null +++ "b/tags/\346\236\266\346\236\204/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 架构 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    架构 + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\346\265\267\345\244\226\346\217\220\351\200\237/index.html" "b/tags/\346\265\267\345\244\226\346\217\220\351\200\237/index.html" new file mode 100644 index 0000000000..9f94f9d5b8 --- /dev/null +++ "b/tags/\346\265\267\345\244\226\346\217\220\351\200\237/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 海外提速 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    海外提速 + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\346\272\220\347\240\201\350\247\243\346\236\220/index.html" "b/tags/\346\272\220\347\240\201\350\247\243\346\236\220/index.html" new file mode 100644 index 0000000000..38bfc46143 --- /dev/null +++ "b/tags/\346\272\220\347\240\201\350\247\243\346\236\220/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 源码解析 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    源码解析 + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\347\211\210\346\234\254\347\256\241\347\220\206/index.html" "b/tags/\347\211\210\346\234\254\347\256\241\347\220\206/index.html" new file mode 100644 index 0000000000..c7a14a03ab --- /dev/null +++ "b/tags/\347\211\210\346\234\254\347\256\241\347\220\206/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 版本管理 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    版本管理 + 标签 +

    +
    + + +
    + 2021 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\347\237\245\350\257\206\347\202\271/index.html" "b/tags/\347\237\245\350\257\206\347\202\271/index.html" new file mode 100644 index 0000000000..ec6a0f63dd --- /dev/null +++ "b/tags/\347\237\245\350\257\206\347\202\271/index.html" @@ -0,0 +1,558 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 知识点 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    知识点 + 标签 +

    +
    + + +
    + 2020 +
    + + + + + + + + + + + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\347\273\204\344\273\266\351\200\232\344\277\241/index.html" "b/tags/\347\273\204\344\273\266\351\200\232\344\277\241/index.html" new file mode 100644 index 0000000000..f1d4f32fb0 --- /dev/null +++ "b/tags/\347\273\204\344\273\266\351\200\232\344\277\241/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 组件通信 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    组件通信 + 标签 +

    +
    + + +
    + 2019 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\347\273\217\351\252\214\344\271\213\350\260\210/index.html" "b/tags/\347\273\217\351\252\214\344\271\213\350\260\210/index.html" new file mode 100644 index 0000000000..939801a2bc --- /dev/null +++ "b/tags/\347\273\217\351\252\214\344\271\213\350\260\210/index.html" @@ -0,0 +1,481 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 经验之谈 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    经验之谈 + 标签 +

    +
    + + +
    + 2023 +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\347\275\221\347\273\234\345\256\211\345\205\250/index.html" "b/tags/\347\275\221\347\273\234\345\256\211\345\205\250/index.html" new file mode 100644 index 0000000000..fbecf69f6d --- /dev/null +++ "b/tags/\347\275\221\347\273\234\345\256\211\345\205\250/index.html" @@ -0,0 +1,481 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 网络安全 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    网络安全 + 标签 +

    +
    + + +
    + 2022 +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\347\275\221\347\273\234\346\250\241\345\274\217/index.html" "b/tags/\347\275\221\347\273\234\346\250\241\345\274\217/index.html" new file mode 100644 index 0000000000..22160dc233 --- /dev/null +++ "b/tags/\347\275\221\347\273\234\346\250\241\345\274\217/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 网络模式 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    网络模式 + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\350\204\232\346\211\213\346\236\266/index.html" "b/tags/\350\204\232\346\211\213\346\236\266/index.html" new file mode 100644 index 0000000000..64328db2cc --- /dev/null +++ "b/tags/\350\204\232\346\211\213\346\236\266/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 脚手架 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    脚手架 + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\350\207\252\345\212\250\346\236\204\345\273\272/index.html" "b/tags/\350\207\252\345\212\250\346\236\204\345\273\272/index.html" new file mode 100644 index 0000000000..bad7e407ae --- /dev/null +++ "b/tags/\350\207\252\345\212\250\346\236\204\345\273\272/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 自动构建 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    自动构建 + 标签 +

    +
    + + +
    + 2019 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/index.html" "b/tags/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/index.html" new file mode 100644 index 0000000000..b74a0a44bf --- /dev/null +++ "b/tags/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/index.html" @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 自己动手实现系列 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    自己动手实现系列 + 标签 +

    +
    + + +
    + 2020 +
    + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/page/2/index.html" "b/tags/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/page/2/index.html" new file mode 100644 index 0000000000..61ae935297 --- /dev/null +++ "b/tags/\350\207\252\345\267\261\345\212\250\346\211\213\345\256\236\347\216\260\347\263\273\345\210\227/page/2/index.html" @@ -0,0 +1,461 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 自己动手实现系列 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    自己动手实现系列 + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\350\256\276\350\256\241\346\250\241\345\274\217/index.html" "b/tags/\350\256\276\350\256\241\346\250\241\345\274\217/index.html" new file mode 100644 index 0000000000..01e8a52ee9 --- /dev/null +++ "b/tags/\350\256\276\350\256\241\346\250\241\345\274\217/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 设计模式 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    设计模式 + 标签 +

    +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\350\266\205\346\227\266\350\256\276\347\275\256/index.html" "b/tags/\350\266\205\346\227\266\350\256\276\347\275\256/index.html" new file mode 100644 index 0000000000..88d5eb538a --- /dev/null +++ "b/tags/\350\266\205\346\227\266\350\256\276\347\275\256/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 超时设置 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    超时设置 + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\350\267\257\345\276\204\345\210\253\345\220\215/index.html" "b/tags/\350\267\257\345\276\204\345\210\253\345\220\215/index.html" new file mode 100644 index 0000000000..3a2f0ed8c0 --- /dev/null +++ "b/tags/\350\267\257\345\276\204\345\210\253\345\220\215/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 路径别名 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    路径别名 + 标签 +

    +
    + + +
    + 2021 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\351\225\234\345\203\217\345\256\211\345\205\250/index.html" "b/tags/\351\225\234\345\203\217\345\256\211\345\205\250/index.html" new file mode 100644 index 0000000000..b3bd30ae1f --- /dev/null +++ "b/tags/\351\225\234\345\203\217\345\256\211\345\205\250/index.html" @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 镜像安全 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    镜像安全 + 标签 +

    +
    + + +
    + 2022 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\351\235\242\350\257\225/index.html" "b/tags/\351\235\242\350\257\225/index.html" new file mode 100644 index 0000000000..15c3e81fd0 --- /dev/null +++ "b/tags/\351\235\242\350\257\225/index.html" @@ -0,0 +1,644 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 面试 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    面试 + 标签 +

    +
    + + +
    + 2022 +
    + + +
    + 2020 +
    + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\351\235\242\350\257\225/page/2/index.html" "b/tags/\351\235\242\350\257\225/page/2/index.html" new file mode 100644 index 0000000000..2e0f2b6c95 --- /dev/null +++ "b/tags/\351\235\242\350\257\225/page/2/index.html" @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 面试 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    面试 + 标签 +

    +
    + + +
    + 2020 +
    + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\351\235\242\350\257\225/page/3/index.html" "b/tags/\351\235\242\350\257\225/page/3/index.html" new file mode 100644 index 0000000000..e8c74e1be4 --- /dev/null +++ "b/tags/\351\235\242\350\257\225/page/3/index.html" @@ -0,0 +1,624 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签: 面试 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    面试 + 标签 +

    +
    + + +
    + 2020 +
    + + + + + + + + + + + + + + + + +
    + 2019 +
    + + + +
    +
    + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  • )fmpd@vCHhjn)bJ z=)ro+4da&EI^w$5rO4&TNeo#_E{Ik)Gd<6U>?ex%x%3f8n>|-pS7IL*1cV#E7YN@v zORR03DOO_GN?J#SNfr-d-j!3~6P|ncarHrBv~@;yPM-IrkF*2Xew0~SX>-VzJBNZ7 z?rw`KP2K1^xTDeHwdJ}N!<{=e7cAR@%f;pq>cShe&FtH6EcF!+!XO@5%So$WMa^+m z%{wtfIIB~&Y3}kfI+6aG`)^B@?Rc@S9t#tGS))wD-Thg-awBz6r%XXz9D0mpeCgRrr#_RU|?%j**kh4sXzJ5&Bw_l_Ginx z`V?Ln7h@KvyTq|NUaD#;Y8JNKLnUu-sQIva_7Nx9@q;(7F~?Ho4nEF}=_oX~wO8aB4TLa_7&TNzQ;UMYi>xzAhgd}I8Rp{mQ`$;)J# zRaZ!QZ9DO}MN?eu{iz#0-Q%T*v}4?8oMwRV?$!6a$4^Awx=c=_ICQ_rusnD$D|AQ0 z@ToW9Z=?Jxg^wPlAFiRh_v*V3n=P@8yHq#4%%v`;s`X{ZBu}uABlQrtRrP=F~|A+MvYjK6F>bCQIl+&y*#z=LCyx(B1!jh{` z?HZ$ADWs=WE-PAT^?q@b@xKV4l6z2m*vr~p5GoI2c_hwX${MlrI2M&%+^*N7}tNM)>T~eCWZY0>P zCI}kbxFvCkBXl`N;{M+5H|g3!=$zrFmVLNIsv)uuBR?w2-)+ubU70#-6s`GAtkv_< zW3TX%tj!fkbRenKZDDOV*@$$0F2VRc4!ChC>?PmId$YACwdbfZ2QSxWzUle&wb;a2 zAS8SDr_g7Z7~8)M_KW?;B+2iDIAW8c_*UDFSr7d9*_u9(4=xpE2~*ARZ4VJX9d-9= zW&xx624@v8RK$j{B+5$qt ze2Od7e~W&fkTQFAvRgCA{ArA2yGvDL$XgqoVlzOp&g562oLKon3VX>#GI!0*d>;y# zS3dWv)ErRegp#17FosE~?p~Hu)kBT}O0s&5{l~hy;gfcz zKZs&DU(DijR*903!k(@bwk85g@T2s|#>9a2HuE=KU8$SYKk> z*OD0>|2_Kh*DSXMqTaNfmw`FKVbh%V)#izo5`gWvyGjsffMA zYfqHekuOfk=iXyvxErNA^6Gfb8e$4?*2KS@^`s`Vla)cwf?9xqtWL5&QCN%k$i1TE zSn-nYHjjEIYKXMYuPjPr<~X&vg}r|ewdzpYEMBx?{ErFW;{CiI^~7_>99to(oZ$i? ztLw~+gzr4qLE(r3E5~?_VPG@9{N+zK_rz?^G~{8k#y3cjBKnaww#N%%8DDc6s%pSd zrVT1WY9XQ@@aXVPD!lXX&+h~>=a+YiuQu9nbma4s%WWNb)x67q zi-XG`|8*JonHL+6UC2J%$YCPv-o`OtRt4};M=xRnQS0cEs_5W*@WME6w(xZCMCu2J zznsK_XwDD3jDtFs0z{jaH(na!)Oa8*rsl&U`Ol_%lXA}LyVR}r!8<YpKoqP2kz6}X<mIo- zA9WlLV`AfZa}w76@snHK5n5{E2VC_*@W?Xyv?#Q_rAGPh5!(yRSOS9G=}S#;;3-!50+)kGk<|K$3pVO3cGsUMW_;f| z75GKj6u=t=GGq|PCgQ0HV&uTwMhldwh3i~F+cvmd9BpNB^nTd&Do_@Zy|xu#QrddtTT z>f$mZISELC71A~0s>J5EN2U~$V4V4~QJym8gVcuztsKt^aemk#rJcoOg|X+NjWJopcrk|(0YzC@10kY^EQ=z( zBo6x9dpf&bpeG%@x%QO7rOi*o5BVBSvW3x@7l^Y-)5yp7UmJQlQbLx$EWOcZwKr)k z=IRJ@3m?;19CBD{Jp)bDY3wdNlXK71W5z#kagXP~o9%yXx=NO)} zXYR)wk4PO7T_3r|(Bh97N*^`Fzd_>D(mO!LPr78J9S7)n=ZgyE2? zT!z6=^*}9qKUzgh$HEX%&YDnO<(3wj0w&fw?->MgvfhrMAxomnwvxV8+AUh z7vQat<~-Y&F87@i(aYS(gCgjq-~~FhSFU^~eo*5qwnk0N+Y`ComdJP6Pu#$IPl-!< z?4xcvCxV!&+}IPZWBQclI7OWc>|yT!W=HxGgBk1+v5(k9V>+8h=Md+dv=`qW%lo&; z1G^>I`kFD!&>Of#UW@2Sg%dietbiBW<9jbTNmaU?-yI@lHpl1I+;2B#CqQDD6Mg9rUUop3#}E1QUR5<*W`w6_96aawJDdZtuRnq_cbjC*%@q|?>O4f zi3;0 z9lFM|mV;pFIQtT6{ZC1Mw5dWcqUk5p>5sgt79wKjh%jC>rKs9ZOiXr%Cq^=`nV0;WRjG7M2CD} zdaU;10jaJS*T;{em`4UXGky2xb;;&Lg}r*awiL6!iu9fGR*AJn4P^bWZCP)f7qJ{W z5<|er%y)<;t&Zzc@XWMvyOQj5OT8u0_gxW`o)5E}tD9QUz|xQ(7(^C~u)+=#N-yQs zj0K&(;+QEkaFT!etJ8li9@{N6O*a!JqK?s+_qliS@m^jmb|(^y2x%>P>P{)J+E6b& zvFFp7$YWoJzC<1}lsql)Y~@Ye?^f99rWXpet!79yQ#H}Kq(*9*K_U6-S7DYdV`;~d zyYZ~V!ffrEcc0r&Z0v9}YTa$NF5%YfmC72C>e_mjp_hG{rQzdT@dCmvJh4-WJ+BWx zb#RT#6oKQn;RmKa9hq84$>2D%7oiQ(3XCVz4)Ovo@)wI~cUW<&TF}0X!0j0y zWe;Faip^R-h!G#94qku;@(hMET4D*7)lio5K%oa|`Fk38b{QIOsEaGMC$uOh#Ph}XwUCw@1Ii1>;66)B24a}Fxpf}w2#5X6z4MZBm z+IwHM(;7U02xG?HD|BIJY?204{Ld3n`P7^v$8e1f==x~3HQQYO^rjb&t|`98<{s+& zt}9)xC*FyOBX%afGy%D~L39HvyP`>yNsHp4V^Zw_S`?=yn zFqaSib6;?O|HBiz{#6rC0~PKwkiapeSuAq`BQan|r|e;`S(?>X;|y|z?-!AGt)@%V zP4{8sKl*5_SAf=%%h&-U@Vr7lFzXs=oRc}^>(s2Wfnu0-}tzRCcEdM@S_nujCaT%b@t+2R7sLLN14UfzMJFBAQI4$+Q)B<^vLB=UX?3P zA8r-SIWIOEY$}vYm_A07xECp^K~55<*5W7kMWix?p#&!V4hrs?)CRa za|4dIYFK>UO*r!|m_#^uYcpJ~Q{%WNoL=a@h}c?39Z-Fa^p;S*2~(43ELkT%`)O9dlT2c zrQ7ij2><4Z=2z2&LbVhb_=0ScP|jixZbE|Nt)L*IkWk#+MY4h|?MKdI1%xpJYais} z7%+&|mZ?iO2pA^{xks1bPkl-^dUj^0ojtpA^1|B^S<8yK2d@j95?#wb^t}r6+WeyQ8?=@sGV6O*j^Y#IvDGmOLtJxoZX)}%-Q~kW z8P8ZE8;T^VU%`4550Y*8;_Zq$xXd@K`>jUlnoL_(E!)7tp@AxtjvZ{is24C)*0SEJFYv;KAgVHc9)^>V9YM-k?ea{ZaOyo z_RGe8V1j6Z=xVRIjN5S-*2Bjj5YYd!NW92=GPM#^81%m^))*1m+nrnJBg~tK4pHk% zWYCZiU7&#!6aB3gp!GGXCMp9HIH&X=BvJ;Dzic>+4G4Qj(S;@fBHBtEGog+=1V32T z%Ri;Vg}A4yG9M0QlN6pSF+R>EnR@YYUx7YYvwvITH59`+!vGN$bQ@hS{QFd%fAhw( zI@RZ2d^lL?s}{mK@jg|0d;s_B;l&`E@P7(PnfxUrl{+gBVfr%k13H~=lJE@w>nS>o zkNPhUJ@l~O+F+tQcW6bY(+mE2&x8c{t+wmpzVsA2Lt*TExVMdPa39q+#uK{zi)gHM z&I2|HEdC;mjftJra-$}784#0dAYXCAi%Lfq<2;tEFs3?0@6}OS*vP@n{NG!7jT|CJR&}uXY1$0 zEqu5^w|wP-F+NT@0SbWV#-Wqi&ezG~;}F3=iXaq;Pd_&GeB8iVJ&)=V=OAf*mV8@m z^$kSSRcYYVWBFtx2{}W>)alUdwu~cSQm{Ft~CJZ^FjkKa~IJr2oHa7U}P(IWGp=0w|Vb zFLNkkG(Q4urG`Mm1^$MHqg+?`KHLNk>stO?(npdAOhbAm7qqjcC(bC!S7pnUQ(57g zGX&>O-Z2$o=IN1+BSl{kh-T;X6IlufuV~e?nnzJ=U*QPW-5E+WO@+R5Nqdn38+=zi z1YB&10c33Nc>T}67Xj6+6kmMX16o$%0GqvsGbf#e(DR4)lL`JuGV^S*UpcyS?mUb< zGAuh9z1&o&y^@D&XF?a!yv7+Ar$%T@kyvfQ8xx|>%h~tya`xv4@M1WQe$ct8ztd#U zB*}fjXuE1gWHAZnJEcj<_@bCP|6HwD>+2P?bI>^#yYzKYUJ!oj}t22XUI2TSrMv2z!%GCksResc+i)cU(|r&MQZ`!GH=i!0zt;fPat z6Ar3F2j4I}3#coo_RdQ<`{@D^MhT9%Q*2F(T_J5~Zxi4`bj?FeE=Os8k3iK)5*z!cF5f z$Q)Ph0hT{19Kn|7?5I)2V_4^ZV9dPDi!;;gqa5Y#^GPWhMJR5_#_ zrch*>aQQDfbEFKQ=DS9yd$#!0x=spfP4%E9eYoIq4~#lG@#slJd+WO4p%z6wS(n+b zBxTnp=yzu96S~al|7niP1l9+iMk_{I2&Er6couPf`c&@LaI05e_9=FboU}31e5duT zOq_{uG;ll?JN_&X2UmAPc}xt3^Y%eLm7734K?}vFVf|n)Nq8Mjf0yu(jB(`PkP#3368wOwJn9oUGTU2-ZN|~3SC`OQ z6w0Wgxl$wL4_N*OJY&sbRK%_;KNsxUw*I^Zq53BXs^?&LS$p_~YcgK9&Nt)hV;AxC zRX0B=?`>Zy|FT?i3tzMz3DTQI0EU5v7VM?5w!kR}fZ3zdtOy218vF)NfS&raU!^2c ztxZ^RJ-}fZF(c?i2=`VT`R-zOdHHDh`5NM*kF&$S?k=qt{=+tp;fbKldKj5hCQBfV z4R>J{iA!T|9_b=da2NpMd#@7!kB-@*k!8BLcb`aUNP!f*+@FDEFIA$BT@;^GA$lSJ zLG4WZ@o9vzAnFaAnB1GIyJHRByfVx1`E1x@B)j+Fahc#%U>{#Pa3Ev(tAJQi1Ae-3 zx(BG~0MZ)T1`?liauH0%I_!$*JbdCfi+RHBbVKi|g@)2QMbEo!>hQrYNt)60-cLVG zJxGN>Cxcu7Dh$?UjBh<9u|XB?ob9uNFHuirI))$n@>nUyewn)q6f`D1rF_87@J59C zKP<039bqPBGz_7OEvzgI+9$4q(Qj1+qc8E~OYsofe+%DO$qG=?hO)r_{BWLdVJCMb z+vA!1SD^&_tmF7;>N@IKWK!Xn3D*ZU;m6pRNuG4!dD2r(YY8$bX2jXAv*|&g!pP#u zbARTh{?1YTfB*BxziNu-?@)#Zq0B_Ufhvm5bMI;Y>c7FXPiIZ7^H{&zy~qHY>?;p; zeA~G&{$&-P`XFTaOT?$RF#gMu`ve@aHN6@Y_=1e6f<6JQ#TmVhp9e9mF$8ZiAapYS zfA_6kqijGRS~|+43OqJ}725g5TtC0gHZQQm`els$no&7|jjZ83>$3}c$b~*SLJtC` zteGGYqvg#$;$C);PAKZ{7ZLfg{vB=4)U|K}$>80o?R#C`x!IKMugk-DRTFdnm>?$c zhe~7$WPlQ?t(0con`iagLFG<#?lPAoM`F66B?%WWS3EyXOM_HXn|bf(Hx=*1fwoz4 zmN{-0x0PTJ-E=~i5JxAJ2??YG=VBUbK0vDSEg((-ldOnSOL&<^p9U%?LKjCF`{}J0 z{-57EJ|izU2BLTdY}mFnSsl(C!3@~rF&slKRD#*zh0r<#mx8WiNqrotFe00zWgHm} z6Ln(t?G%-9vslg;H5Dr3wXyvcPD}}M??9|T#^j4|_~-_O)r;+pajTxS=V?2gmCf}1 zl3;6QeOpl@b@>DgX3GD>*-rXI9l1ah#}Ax_d0Wr-a8a<`wNRKbrpStgYT{&MQjU5b z7ZSInPDh4woIwd4JMs-`7o}6ZpPD~v+mIywp>nm5@a}p17ku?SYlQBA@^_lMCzRT- zce5gN>$LQ&JFxQ~=O;PNuirDFwyJQSuim`bZzM6JKsfEKB~q5+==%7UlPzD(qZv!D z?^}&CIa-taD7ccpr#P|+Ga=(I73>}J($ybp9 zl|LKOZ1fBMZ}E%u{kSQ^HOQiif07$kwQM|R zK7%kQ$=GSkGHJ_+HP<#jWY~4nsPUqYhTfN(Q**SkL zRGh7?i<_=&gs2?mB1pkb#3mF+YQTx);@c?xjSWkdW}CfGdhET!P$sx^Ww_S5o7)x) zyzbL*qm%(Mn0aw6_}>5+4&+*W&!fd;G31of!O{upB3H$YTrI=-w=Z?>i#jSCd+B!O z4~+6!#{Eu-N>UBH>#4&*I&`W#2VR^aQd&I(GWm_!IKPBjtl$+kaqQyP>xVh!_G~R;EEIc^+CrPB#c7DZ1;l z^wk+QpcP#u?oT5qVNS~xU(0B4yVQ%M6}pB0s#M^4fhSphDQ9J}_6kNu3SPFkxptP+ z81@>7ULE29+B$Jx6FkiU>OMuVhrvjC3Dm5w>rk5K7kRk!?Af&Il0PtHhyL6}K$W_2 zA9<;D8uJ$u@>kJYTfblSORj$&t+wX<_Vw=ywrBFIFTbFR`?fP4?RcadImkX`3L1{Vf_ z&lJ8n8uhDQjI^mzfRL&Y>gWCjTJVhE^xySLU^^lOdv%0Rz=u_NbaKJvjc;s|93qaE zFOPot_7Z~C(s>Q8(#d3XWpNVaMJ=pbH zBRITN!eXgNTE)%1ohPb*m6rXDhDkqYcTeAwN#l?jt6(qV?BRx`S1)Vk=+$3ronXi| z95BDHVR%YdG0L=+^RBbhE!Y|w{Z1HMUOW6(mgui+(f@EpOJ9Kw`OL#-ZW4XeW81uq^=mDD?L7=amB*`bY0s)5!2Wf`ep}{ z!Wrbjxet+oBU_+QTu1u53TyGf)G*gkKaNC@?1L zkn#fe3bdpY@Yqwwr{F?XaSlPzOY{RcLTf6$E7*cy6125!KpEGtQOl(Rzof~L?$C1d zX-M0bsf zA}iT6`l$P9^Kw}=7sIu;EnXa-Ju+o&yA85;U1ov2!G^zH0gm2<2bJOjV?>U?n|N%o zlmf`oF82pn+CZxy@afUu<8PrLBxwXflA?gG7ur!kNb)=mge2EpRzhc4)B?EJ$rEPM zQvx#Ltr3Kn+aK`gfOiNeaL!Z*mpG7v%z78 zJ%thT6h=1TgmiB6b%^aJCKCmaws6`a9W^g`dRa+`LRq$1tbK>0Ue3)HNBgh4Bk!a% z*tErI0vJmc=T+;#*FWQZz1|;R&r_WJ=Zkf5H&nS_z2tb&J1Eu80;O8x6(k7if4(Vd z0!6U*Jj=RJ3U|FH@^pk@YG%5<*us>WU8zx+QD4GpA5~T7|4Qh@k^T9<&X&Y4fZ;rx zNIX2`Cfj|m2k!Km;@Bc@H>Tf^n%FQM<;zt~bOMn;olQc$G|IuT_lT?^4 z&x5{koP@y17xq_qzq=-ymx1Sb_>^OR+ILC>;#dvUIJ8&tSH{T8-9xu$H@1DEl=$wn zU%jihR6g28*(F#`VYC`li9y&h;7J;^t6i$+V|(k7R>Fb}>ns5|(FTwcZ2<+c9uQNO zi>XFux0>==$o|-u$9XN;R@oHPe>RW(qTSr`4BwWje9RG;^Y*8*mXO-| zm%Y|oe_$@khhK<$BatHV>xKilS3Pe6+miG?et&GA(mx>;6)XKHtwo=) z`cZhr!X2_bD~tDe_kY{-*ZtKGHv@ZM(+QJyRX_xmf#@+x?>=nqk0Vu>2YJLD;RM7j zeiLyu{4R(@!!i2x9AHQ*Yl&Gj-f2MKG&sX)09nGH&jIhUfCl(^{{ux=@ED0+9KxRZBA7wO1;%2m@!+LCSjnHjR$SG;Ael=55Yb9=hXZ0>b5jEF=n zyLLd~mgeBLcTf=&FA1C+pW?XV30`6f!&4c-`!SaXZ>fg?#^BtiMgi|NCO)8*?f6wzxQD zZkQJK5v#M9r1u#eatL!lbvAjs=!2V*qd6xI97Ziv*uzvxT*D?bbo&8^_ z3z7B-GRRZMIO17Lorn*RS4xA2+hGIsv@;VKB1JLc4Og6>^c}EIp8YvMA+1d8K(wIF zrd{#YEj~I)|3L#VU;BTW_5W9mt-Gq{K`Wt2BZbMVqaCyWyfq5v;TmyIZhd|wwPG~C ze6dj)Y1!Gq*HsI&*Yek{rGWMYZy?4411N?IMY}-_5#}g_LB`hl2jgH^3-TGX zmaSi%w)a(M#mHjK%+BF?;5wXs%QL;8(eYP!Z_Q+X_p z!sIOQ3BLvije{gzQaz5ohsv@W4#{81D*AyDS;`h+yA6QdLYniD=q}w`KQJ#w>$WwM zvo(MNyR#n3h>znZRh%IlU<$%@>TB~7E;mED;?=%8~JH*_#o2T^%9Vly&ImH7Haqef_WT%5%}ZC#L}|11cG^aHb>=!5@dT?~F* z5mTYzOF;atOrlM7%A>xHESXP8*E)|M7|TaMAcT;Z;I;;<0Gf!>Sckq6;UQkgVG}jI zs-xA}YU-7MeXasFVd7qHedmKs{5;fCHWBYyez z$1}S}Z9jsPyob9>;3n`!esGM@N^a!0N$B$Zf!RGW!A-~xFB=rs-xbF{ah^<+i}nNGpCOT!1la*8!FqnV*Cq%v+2 z8q_nXv%@2fcsJ+lc-7{xhG=y8MWi z_Mouw*iU?PBz}hqIDmq#D}!v_FsoviwTIfS184LgcNtq|fZU8C%+RTg!}t%eXRB2% zZC&2E-1YJFn?WB?74g8da$0O-0&;!q$;-2 z?#-cFwR0|c_NY!l!Sq@cGwl(RCC!Kl1E@RU`P1oF5o9*Zj}iq_bZVhkCTylsM|WaF{K?B;W!1 z)CjfB7iaTGM~xqt=Wl*sZX@V-aH2my*kL$JYbH2%m5Sgo-hctM7sn^KBn(pILT1S? z<(wGitj5#tBZVSAFq=a&y{``9oqu4Ap`R65?-Z*wNcQTH>{S2FW6MR_u+Si5g%iw` zXV|he*)pi8=b9Y0{M{Z8&AZWOxk)*RmJjlL;UO^iJE)u`TcA=bwu9o2(1FfK$G1z? z4~-3{BMy@!w`Lo=i2=5z)0Zsu^0MS})6Lio{4WhsRtS+wrT54%@aRSYM-dCl#WKQo zI5V9GC@SyR$do}ebxOej~0DDhZC^G_)S6S zrA~FQYI*M`X-ruBh-0iQ7=V!i`{q$Nq08h~Gefw~fh+MXCI=Elw9M&^s5ruO1XPqyT*R@1D7MY<>1`&K%QfW_bn%J^l1S z{s`BfdJj`mM!0sJDi?=Rbte-HOn?1xEsM*B9%t(1WToN z;Rj|dh^^ESZ69xaCt1N1esN zi0@Zo>|zxLuBE&)`S$i_*!KV9@BBah^#jVzA!1&0?HN1UsewAnXkWA=vjVrYCT}(D z;ER*Z33#wE;q{uGr}%-@7aAjsdH)eVALYf*@SnFv4!rmoAo1gVJZr9#F0N|6I2Xr@ zdLa5Ug6K~aqQCPvi2l}Hu105>0|$DQI2|>4!#RSW{HG6{y1<0+*VmaP&op7(#TQp& z-OKhjrr5rWdE5GB`#cxFyR|_!+uk)#)d+nTCv8Cr9I0KjTuIpL5Yc|$@QJNNltUZI zuiX5pj#U2dIzkNTJlFW`k7z+-F(0=?ez|6{?aTjyL5E~*EeMiH(;{CraZ#VYgqX@9%Um9|Pvi}bA2^qi$?YKsk4IYw?EIJjK7 zxt5=~F%Uh+3s8W~h_R!xmR=xc&|VQrxddh~=)O>crW)=p2=z$uPV!F9Nh`sHxyZ^m z;^kK=I&A#I_=`KX-NS~^l} zG<-uNBeE>uVyw3QZkdcL;quvjr`A3i(N*+;pi*GQ@+D1vW8Cy#dfkhd;(m7SF}tzY z{#d;*pLzWP$r2}pBv+lc<5w~Scl?;ly#x+=hd6i09zP=d3IgTL0615JV46I-BMKRB z!E@&jCf)#gd$+y55IqbwP({WOsJ`%_+6=BA zW!|dsa(>o-Il5>Lxj3ppZ7aQOc72N}`@l|_#1BuqH@zU-!ism`>FjEt%Z#@WIU+!p z^-cwHc5R5^IHDV zho~?GD$ovgVyC9+@M9?&sD9GaH#i&MTFY;nTu7Z}r2Yo!6G#L|*ompQ;`Ln!kTGbuA z3Pk3Ns=I<1w3vE^*koRaWGYTvcg{8H@T6hYu3Nvkx!Hc+`2fB09Y!AaH@~!&&VbtF zUyd0+c-d(khE11IuUtpQLtw3Z_4`~%)#L>io4mXeSy8Jx43%67j`j~?E^WOoenjWM z6{oASJSuPwoz$NXg)XjkT%6PP2Sc?E5Y`qbY}(k(qo{zIk&976XMe5{dWWMQ!cJ;a zZr~>tz&c1Mv47=g_;m|Iarl~Q=<>6p>6JQbdor&M%sbO~DxA}E zzEGm)jcUywoZ&x4&RPb?&gJ3NC4=G?7JB2w1??& z@^yBI4x+p(w98DBais)MEzoK}u5JNxbt90goA3^_$!(NCGE)Vl9}cf^pN;5cn{Y%c z-s02qqwU!{`{UH>$D9m04jFbV3tHyre(HLQ3AGWxjQ%ZWcywI5zPqp6 zJZjnAlq%i9*WFoOD-udNoR{{Jmc^M}*FHAYQv1kMWOeoW(j)w(x@Q70-My4v9^L$t zdH%)K_~7qJDNMmq|7x9ir1d}M9aOdi1JO_(-~5l;B5s1wf^?&5KD3g^H%<-8#)V`k zo*vy3^HDYPP1bew&lY=AMUQT4+X}L)N{#($oJACLUydmtk@M^y7y}q$r8-p!F@z)7 z%9MKifJHuRtQu3Ham5*;Or(m&=`9kvxDTKHcMAe%R0*02wN)bfK(^C)aw}OJX{vJp zta>SuQ~ZB}=4->hNmK#YDU&kdvOd~;9T8SfhxFCu%6(Q+wa(+qPQ5-*liu)B>*zz7W*!4Ng92-3EK22Hv(LgVxG z5G6c)3fmHm`bY!ve5*-CjeyPC&OQKy1V2pT5pn~24xMp3_Y*6sIXYLF5TC^h`BCW$ zh9O4*5u$$$sh&E9fq7PZUw|*N#2}{igppBTO5(ZCkX}BR#K4~em^K^VK8DLs;$pNE z0M@u6oM>HKz3MO5YCiK?g#q3CpSsw7CL}Dsu}$3}fJuQ7iL8kZ#W=FfQ~Q?s?2Gn^ zdb0Z^4t$-v{ltdw`>l_(dL+uk_$Eq_wM06BJgIfx2z}c^M1veqisR%Cu>G*cKUM9S zLUkm>igcslNoArxuuA~EK7Q|?oJFY(y@ID73=Jix^%Qb(?P|VbA+!1oYQ2W#j9MK7 z`Ryv;>}G9G*jcT+bDyhJYklIQ>?=fFIGjs8BA?t2oV85Vqs~M z$?2g*q5O8zn3^$@`4x%o=SyiOSssHMW6T0uHcKs4xEP-*QjVF&jK}shP_N;GaFAIC zCw=0+0br6-lse}byQ-1a0%Ky0n9i)QN6lWl8da&HJD<5fnUi7u$am<|iKD}}O;!f0 zCM~>jZ*X=1|1)i^zW2Ve!f1yxYXf<3`;+%m7p1U%A1=K(d7#1VROHTdTdq2%|gO z5Y`50U+plFaz~1?T9#fHZ9P=4sA#jf`HRq8t>?jqVqftUbgpb1%hSbaqW@&D4*${i zfUfRmU!2K<)d+o_*~q|DAeIqkbXwO4^%%@-;Tf+0Tn)!FVjaVu>upb-BCJ4U4)OiQ zjuXI)SD4JO`Sp+#m>GF+DhXq}PfmePCedxV!H)3E(+H!gy11E@R>YbNL>o`vSq;@6 za4Z}DtiOU+{}`=}Y{t`%kteqya0;7}oS_Ud%at`sY)Ty52EGPmsjvy|UyI6Ui(IuG zDopEMEZ!Hg<$GJ8jIfbx{L84h`vyNsUkJ7ioGe0@=dmF)uR(ytk+cV}e4CGxx1V?l z{X$C{zKTCh44ptkyk@(4dORvfZ2qz#dPFv2`1OG!OG0EU=DCbCmO?uS34)s$6|EqB zx)|#cw%*0`*_h%%YFZ zZeEi4Kk9i3UBY9|LFC05(0T<#Z0>f3QOTy(f+Y9wZz(C(8D_i1oA|B~Y>%rCmF5Bh z%Blt}MdOHe0yjv*1Pw1E>JmmG={$w|X7+B5I(_Svw)xmy^v~ID{u`Xe*luwjzdyE1 zez+*%eAXV3g}Jvd)eikIR^~6$2#fTWX}mjwh^l)M3acK@jYVbcw-OhrRyr5o>a$ws zt7!IpWlqKsGtX~}-^t8(C|+r+x&5SWpUJT}Hae$*y|MMEg+&>K^(cLRPVm=b*6Ziu zM!ti`UvpWgaGKoSVsGJAtIy^MYfgsM*rteuS!PXpM~%>gho1O1P^WiJ@={?bc2aJ{ z%sW^4%-vCsTJIFzZnWcnvG?YIQ15-;_-Ioo*_UD}O14xIvP=?^5JJ|egpi%G&Q!97 z3855ImXMgT?_(E|-DEc?vd&n>Fw6OUbY171w(Gg?>v^vGzR&Y}u0PH>otb9l`~7@B z@Av1uyxy-@muc1FK9@92{Y%%kF$&cG!_%lTnid5c&4A}KHlq5W)qv?z@oEYgF>ns| zIYE9Rub?E-ldW1f#Y(9qL;4ku#d)ik%a20bhwms0{$gARQ=LHj49=AA28$r{z^q?LSAti^vHed;2gwE$ zb4xG8>X^=ptjpM-@#4rqyFAt1S6wa^{suY8=N=Kj+vo$K16kQyzb7$S^oFM>noxcg z@LZo3`kd(jbdZ5mwdm5%ZcMY-!1@BJpt-1fGOU2;qr6+ngQiYf;e+bM$y;+M>eZcj zoD$+Vz&pbH>G|M?=%+*95|Uhlx<3)pyf ze&&qA+g>V)R;zu%qHN6=a2hJ12ggZKPo->#)W1*$eq-BhG}R47_SBd)0A&h)9GztW zM*ai==ZNvf@vE7&#k zI>$VgVCxO0AwhvO_!0v207ZZb_6HEnMP6iFlu<*PV+qr!kLmRL6bzLQ#CybM7`_Mk zR+qM86;750G0`}1?GeAcOspVw6oehQlhx=xv@v08dd)j1;SGGg zH8c#H=UfYDL=ZBB`ufi$7`sgO+s8)+>oEIoZMm<(B)1t5F0G{{V{_m?t}*nRbr?nx zn<1bqLMbQf0zhA0HI}rk3%i_fiu}WZW9jnn&`Pkj_hRWd#1`U_>`~-SubknLbA*+_ zb82%-{hjJT2PNK_gs5#6o>)1-Kr^2nS!AjJwWs6AsY9UjWJW&|k6!85rESMe zIfCIODN-A@B&=bwp47Ut_O%l%&vBdKhwblB?YQ}^fRe0@bQD_zdJbzCWTXWZqH8 zF8NyaewNJ{azczRFm(%LpQ5>JfTWEmqBDcS7kLPR)WZIO+Aa7yYL|A2Y(nv;cVVH* z2ffTjgXSzgyGS>QvnGZQi?xqL(pda zk;(=K%JFkMDf_8Rv`7T2*8|c+GGlVf3-VI++xnAFyk)v^b{uVDW+g{IwC?#V&+AvU z(q>9c0N>VLs9pH zsFgFb^qH}K>@4nIbMc*Nl$>Pb8m&LcP~ZTx7TvaiWZMHu`JFdr-h7XHJ3RbP8KO*^Hi$I!X!(BS~c3J7f;e|ffVbE%*XHG?FIra?&p`9mzv!ckJ%_sOyIn~ zGdqx74}XVy`f4+kAvq2W2)1^rKVH(diY{`0@C!dJl_cvbp38F&&*FC2dSA|^sE2M~ z6!q}0Mo~u=esozNlV`?`CXM8_RksOzfwL=a#%IoY&hZV+z4WC#G34t=xtUU8u=sXw z7=&`|SAi9#!-qNroEHcmzN!4)fknMKjA;kcRk$i-5WsIY&R{qB8Frx!I>tJjFx-bH zqX$EiqYoMlu^EjX8#3733c*+xRn<$~jItMnhw57VcZ}6X7)$Opa3e-JUOO zPefa^x$tv1OFX^RJj?h#B?`w+T4RN@?I}-ZP_jBl)m){vu;rarHDy zB~xuC-!1va2GpG2+L~xhHXD989|=soUXAniHzSURpKtnUo{47i6V9sAI2=2Oeg!*8 zgwxg70d7J!90XF-`*ic~#-+X;o%&Dy8TQ*>iX-M%Gfw{?2Kv?Z4HBRKQ3oJnGldI~ zm7wWN97K&qBqPik%}%D^D_^ajtgv#u@^CO&C|3e^IP&DX*X5Z)fZXn;-bOaAh$i)J zG|GV2xl@Ry+MvkZu#E#t?Fa~<7;b_S3&`_a%Qq(WfSQO1tpGcf24Wsb;OY2M=z_Wc zao?c@BB&}e7#*N&K4x=KJ(0Cdzd;te0DV;sBvM5QKH^ZvZy=~Z*Zx;8LjW{&8C>ZP zI`i=NhDaUk->a*@$MtNKSB)MqP*~y3JXEepx~>Q);k`K-4{p65o7E3k{YEjcMekFf zE0vF>ad$JU-QA$pf%h%UGJ^h&BXCJo9iX|@h#6;Mp%8^An*;}AcVP!yeca-_G+^KsaXnK|>+DKyV z?$N!`x||HuTi-!Vu@$W%bQL4`N&}3x#kk%TSuG2@_Rd-}ne1>A!^Z;Tt^L^E0S4us z)h&9$$ApjV%;aTCY2URK;)486hXP(Bfb{Qw`NTTgF5X8ueRt9aI^Vv8KE;`%98{Pm4iZ3BN&HzV0gW(@GBLOnB>8F zZ@W{2L&w6r_9{AUKCjYWwRg9gf6Qqp3$pq=Y@!))5EM=dF!b13c%vcm8kV4sT04#o zMXt;_fQpF=j@a{c>;d{?(;2DXLlyHACbto#MvX%!i19VqcFO|Sv~CwTgp5a?x%x2c z_?V&fM8;f__!sB@-94-O>pcsKZ4Ebo2achG_(vZE{oEk_2?t(LTn#AzCoCd?R}8bj z(Kq!0uL#gt6JS24h{jolp>E9w$1(^?!#dyzP=c0oB#aX<6EPD3iLAs-uETmZI^aM9WZS^Ey>q1!YxqRYpCxOS0{o(;x2Zg~{}5 z)M|1ZYdNNvv@-Oajefn~4fj#$Ktm}ykD)>VgI+CGz^iwyK*`;tv(vXM&QLw>%q;Wn zyO&%TPm6!~8%X;{vGH;`Hdo1jzN3e-$H6pl_-!UtzA#~N-A*Jy)$i!D zMZ)U3k)u!1Z?B*7@9yxEDKw50v4+`~jRp!MTkIiprI~hRP_}9^AUM;4JQir8k;N72 z!oe3yg7y~(#r9}CXE~j2=My_0k=gISfBhwM3tvhSA7o2v)Y0oV_eO4K1V9@~24+0# zNAINZy{|)3ZSKI{nu?RBe|plu(7$nY>A=-JXPq_r<(Utlu0Q!pkRjGB449Fgk{d@U zSO|!T%hx{y_;r-`wrwpH&p#rtYiisGe@BrYvUcrPl^8v;CJ{XbuflLNjY|cZc1@kC zi(9bAu{G|!U%TJPy6#m=liq+LkhL-XxqWpSeh>ZX`}VdC)2MlQ+*Z!g;5{yP1Y-(~VVNS{S8SWvU;2kkdq8i$rM zm1!gK?O7|J_>Nd0tCb6+7{6e1IfL@`m;I_LDB-gO0ivaPDP@0iEpC&1y&vIH3<&;7sE}u>lKiaNc0dxryyB zqK7qxfQqJ-EZ{yl4IPl=qG}^+t?6J4+DK6Y6uS7ZK)SDm&NnJh z4$plzDY2-y{r!7gea)s532TFtvnjCm|@6e0}Cw`r(6d(XKC6aL!I zbz?B=fA{*7cY_Q4ofRCEy8$aW*!vE!(KLhsLai9zeVEZ9kY=g_-7{ z@0cUB;aBG*De@-Ni?rv8O4dZi`mSl?-dy|AZgFp~cC$BZ>eexm1q}Puy)eZ;nG#@J z9pVB+b3ZIL>ibo>$asvx00M`n8|8&JCy?vMW@grF3Ai9X*xwDPYqZlN;^Z-|g6!Pw ze={E!My0Jz7a2BUeUtS+u{7CCZQqqcAm6bjzFu$tZZg4RWT!;7j8w=lR~Kf??14kJ zX@jVhYpmC>hmjFu2H4(!!0G3`!-YrYug)e?PIx(x+yEdO%1V`P|8vXl4A+#9R=YVH z8NN+b6mm}i)FYv4TqKd%9e5~<*8-`KjKu5m+1*-Pwg0T!#45U|BkJb zj&eT3G}o_^K9CE3hVG^_f%Kq2^3e!~pU_KklD6Ebfs-$8#ymdz_y~uW+|XTg8U)E? zNK-)m4fSaVP@mcW_1SMcH1fSIVk`)Zwa{QRQ~r}>Xwa}gE@0(#Jysk&T*Gxw_Q@kr zw^tAe!%Md9Opy7hzZ7)&FVEWYe_qk}t40&{zm6upI@FD5Vk79Jp9U=*=pxXGK+X$* z`omG<6^fRvD_?67vq&)&3^=wIXJiE*+(4*oZ~OvKJV=`um4 z`NLQGaupO*B<364ijp{b`oUh;=S&%4M_)a6f$U{C&E@$&G&;R%?_-mgZHGIhj&b0c ziKJGs2~a(VPEY?XWG@J{vkIb< z93FS-=GVxIy_?;9B`T>HGX-{=I8mI=EKX!?9A;>z^EQn-vrQ{QeOnUG>3C2w9m8L+ z?|kw8LfPAx@a0wnE65Vn>EB7w|1;OO4vG$6d70xkc$q=nXdUn}qhz|sm<=iWA*v51 zqM&66LE^0wRif3!AD6fix1C=d(6Nc|HE8Alm}%BeOv^Ob2Bp?_^vmj!2z2|`lld#j z^?%}o#{9u+ob%J5J_?-Yf6fvb7M2v}6^G4t8U=X?Sjz8?z3wS*q%Xfk5D0D1DBrak z{zj~Wk|N(2>1U*e>2Ac*4xn1vC>EJcoEZ-)NH_n&-&nWPhtxni8RT`Sd;WUZ;n_4GVSqDX+$#didP)fY6$#yYpnjIXixrYngo~+7_wNh0=*t_^pbwvklix|(3FD$3cqZlvb=okodNji z_}zuaqYrz^efiozzXS~D~bF$R(_%W(G zZXurfoqR8%@m!}z?xc3^z4oSv;R_CevmPJXrBCzA{Da8xS1JV~drQ;0k7>Mq$HjizP!Gt+10rU6104sd-byj_buPL6 zcdX?ZUvN>`ku@e@s+eUMB<*L>R72!VXl{PzAfgqBQpRe;7oC=Bp%(pQv!zkW{tKRzb@ zgeRrx?B{z7A^b_kZ3pzkdt#Iyjb- zKuF`4KwtSm9r_3DXn;W!+>x>;HR6%aLp3#cHzSo>=uF3nkv_O?aaTUY*F-nub-bV6 zfs-uK#8}7}HhvvK1BP->tYt4epEW!@d^dELtO07xCkXozS^%n@Z6NEf3`B0`HjRAo z@?Do5PQU+Mt=)~phf!1Kqw4Lzfd6;T8G;~hbh!?NSuo-?9!--=dZh2M5>()Ao;Tq3 z>xD0O+Z*Q%g)9|)h)NwuDjw7kSH2^BE-hZ!RC+vzK}MwS$nQ3VM2?1i�prq1 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 李旭光的成长博客 - 求知若饥,虚心若愚。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2013/06/17/Read-High-Performance-JavaScript/index.html b/ja/2013/06/17/Read-High-Performance-JavaScript/index.html new file mode 100644 index 0000000000..5fcdeec3ce --- /dev/null +++ b/ja/2013/06/17/Read-High-Performance-JavaScript/index.html @@ -0,0 +1,607 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《高性能JAVASCRIPT》读书笔记 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    + + + + + +
    +

    + 《高性能JAVASCRIPT》读书笔记 +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    最近读了《高性能JAVASCRIPT》,下面将书中觉得有用的内容进行摘抄,放在下方。

    +

    如何从小事提升JAVASCRIPT性能。

      +
    1. <script>标签写在</body>之前——将脚本放在底部。

      +
    2. +
    3. 尽量少的<script>标签,减少页面中的外链数量,减少请求次数。

      +
    4. +
    5. 使用打包工具如:Yahoo!combo handler

      +
    6. +
    7. 使用动态延迟加载技术如:LazyLoad类库,LABjs

      +
      1
      2
      3
      4
      5
      6
      7
      // js代码
      <script type="text/javascript" src="lazyload-min.js"></script>
      <script type="text/javascript">
      LazyLoad.js([],function(){
      Application.init();
      })
      </script>
    8. +
    9. 在javascript中存储位置十分重要,尽量用局部变量代替对象成员的访问。

      +
    10. +
    11. 集合变数组提高查询效率

      +
      1
      2
      3
      4
      5
      6
      7
      // js代码
      function toArray(coll){
      for(var i = 0, a=[], len=coll.length; i<len; i++){
      a[i]=col[i];
      }
      return a;
      }
    12. +
    13. 使用局部变量缓存访问多次的成员
      当便利一个集合时,首要优化原则是把集合存储在局部变量中,并把length缓存在循环外部,然后使用局部变量访问这些需要多次访问的元素。

      +
    14. +
    15. 使用原生DOM方法querySelectorAll()遍历查找元素。

      +
    16. +
    17. 让元素脱离动画流,页面重排次数越多,程序响应越慢,要尽量减少重拍
      方法:

      +
    18. +
      1. +
      2. 使用绝对定位使元素脱离文档流
      3. +
      +
    19. +
    20. IE:hover
      在大量元素使用:hover这个伪css选择器时,程序会降低响应速率,在大表格或列表的情况下

      +
    21. +
    + +
    + + + + +
    +
    如果觉得不错请支持作者
    + + +
    + + +
    +
    + ------ 版权声明 ------ + + +

    本文标题:《高性能JAVASCRIPT》读书笔记

    +

    文章作者:

    +

    发布时间:2013年06月17日 - 22:22

    +

    最后更新:2023年09月20日 - 10:18

    +

    原始链接:https://blog.lifesli.com/2013/06/17/Read-High-Performance-JavaScript/ + +

    +

    许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

    +
    + +
    + + +
    --> + + + + + + + + + +
    + +
    + + + +
    + + + + + + + + +
    + + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2013/06/26/CSS-Triangle-Circle/index.html b/ja/2013/06/26/CSS-Triangle-Circle/index.html new file mode 100644 index 0000000000..8531bbbf40 --- /dev/null +++ b/ja/2013/06/26/CSS-Triangle-Circle/index.html @@ -0,0 +1,623 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CSS border三角、圆角图形生成技术详解 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    + + + + + +
    +

    + CSS border三角、圆角图形生成技术详解 +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    利用CSS的border属性可以生成一些图形,例如三角或是圆角。纯粹的CSS2的内容,没有兼容性的问题,我之前在纯CSS实现各类气球泡泡对话框效果一文中算是比较详细的讲述了CSS border属性生成三角的原理,以及实例。我觉得此技术相当实用的,故本文再次简单叙述一下,另外,本文还将展示可能并不为众人所知的CSS border圆角生成技术。好了,裹脚布的话就不说了,直接进入正题。

    + + +

    CSS border生成三角技术简介

    效果抢鲜

    下图为使用CSS的border属性实现的三角效果:

    +
    1
    2
    3
    4
    5
    6
    7
    8
    // css 代码
    .test{
    width: 0;
    height: 0;
    border-width: 20px 10px;
    border-style: solid;
    border-color: #ff3300 #ff3300 #ffffff #ffffff;
    }
    +

    如何实现的,为何会有这样的效果,不急,take it easy!

    +

    梯形图案

    看下面这段样式:

    +
    1
    2
    3
    4
    5
    6
    .test{
    width: 10px;
    height: 10px;
    border: 10px solid;
    border-color: #ff3300 #0000ff #339966 #00ff00
    }
    +

    当某个div应用了上面这个样式后,结果会如何?见下图(截自Firefox3.5,IE浏览器有细节上的差异):

    +

    更进一步 – 部分边框透明

    现在,设想一下,如果我们现在只保留一个一个上边框,其余边框均transparent透明(或与背景色同色),那么是不是就只显示一个上面红色的边框了,我们测试下,与上面类似的代码,只是修改下其余三个边框的颜色。

    +
    1
    2
    3
    4
    5
    6
    .test{
    width:10px;
    height:10px;
    border:10px solid;
    border-color:#ff3300 #ffffff #ffffff #ffffff;
    }
    +

    结果如下图(截自Firefox3.5):

    +

    从梯形到三角

    上面的是梯形,我要想得到一个三角图案该怎么办呢?显然,很简单,把div的高宽都变成0,只留一边,不就是三角了吗?如下代码:

    +
    1
    2
    3
    4
    5
    .test{
    width: 0;
    height: 0;
    border: 10px solid;
    border-color: #ff3300 #ffffff #ffffff #ffffff;}
    +

    结果如下(依旧截图自Firefox3.5):

    +

    从等腰直角三角形到普通等腰三角

    上图为等腰直角三角形,之所以为等腰直角,是因为所有的边框宽度是一样的,如果我们将边框宽度设置为不同,那会怎样?则会形成等腰三角形。如下代码:

    +
    1
    2
    3
    4
    5
    6
    .test{
    width: 0;
    height: 0;
    border-width: 20px 10px;
    border-style: solid;
    border-color: #ff3300 #ffffff #ffffff #ffffff;}
    +

    得到的结果如下图:

    +

    从等腰到不等腰

    我们可以不局限于保留一条边框,我们可以保留两条,于是我们可以告别等腰,得到更加锐利的三角,正如一开始所展示的那个三角:

    +
    1
    2
    3
    4
    5
    6
    .test{
    width: 0;
    height: 0;
    border-width: 20px 10px;
    border-style: solid;
    border-color: #ff3300 #ff3300 #ffffff #ffffff;}
    +

    实际的应用

    关于应用,不多说,直接看图:
    说明:
    以上的测试代码纯粹为了说明原理,所以使用#ffffff白色边框,通过于背景融合来隐藏边框。在实际的操作中,应该使用transparent透明属性,例如border-color:#ff3300 #ff3300 transparent transparent;,这同样会有问题,IE6浏览器不支持transparent透明属性,不过没有关系,就border生成三角技术而言,直接设置对应的透明边框的border-style属性为dotted或是dashed即可解决这一问题,为什么使用dotted和dashed可以修复此问题呢?您有兴趣可以参见默尘的这篇文章Dotted&Dashed终极分析及IE6透明边框。

    +

    CSS border圆角生成技术简介

    我看圆角

    一提到圆角,我脑中闪过的词就是“定位”,“嵌套”,“模拟”,“渐进增强”,“滥用”。

    +
      +
    • 定位,也就是切四个角上下左右定位,这是淘宝首页的做法,但是面对IE6的奇偶bug只能当作看客;
    • +
    • 使用“嵌套”则不会有此问题,“嵌套”分图片背景嵌套和CSS边框嵌套,使用图片嵌套则图片的重用性,大小优化有待加强,边框嵌套则技术实现上有些难度;
    • +
    • 或使用“渐进增强”,CSS3 border-radius属性,而不要去鸟IE这类自我感觉良好的浏览器;
    • +
    • 或是学习Google使用CSS模拟,而一般的CSS模拟都是使用左右边框+背景色的方式1像素1像素的拼合成的。这类方法各有优缺点,需根据实际情况采用。对于满眼圆角的设计图我是很不喜欢的,该用则用,切勿为了圆角而圆角。
    • +
    +

    border圆角图案生成法

    这里介绍的实现圆角的得到与上面提到的都是不一样的,虽然也属于CSS模拟的范畴,但是其高效的程度确实相当惊人的,可谓最佳实践之一。
    我们先看看效果,见下图,截自Firefox3.6:
    上述效果的实现仅仅使用了三个标签,如下代码:

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    // html 代码
    <div class="box">
    <div class="top"></div>
    <div class="center">我是一只小小鸟、小小鸟!</div>
    <div class="bot"></div>
    </div>

    // css 代码
    .box{
    width:500px;
    }
    .top{
    border-bottom:3px solid;
    border-top-color:#cc0000;
    border-bottom-color:#cc0000;
    border-left:3px dotted transparent;
    border-right:3px dotted transparent;
    }
    .center{
    padding:10px 20px;
    color:white;
    font-size:14px;
    background:#cc0000;
    }
    .bot{
    border-top:3px solid;
    border-top-color:#cc0000;
    border-bottom-color:#cccccc;
    border-left:3px dotted transparent;
    border-right:3px dotted transparent;
    }
    +

    我们看看这段代码在IE6下的效果:

    +

    这里的高效在于,仅仅使用了一层标签就模拟了3像素的圆角,按照曾经我对CSS圆角模拟的理解,模拟1像素的圆角需要一层标签(background+borderLeft+borderRight),两像素的需要两层标签,三像素的需要三层标签。

    +

    有点神奇,但是就像看刘谦的魔术一样,说穿了也就那么回事,其实这里的圆角模拟在本文的上面已经展示了,就是这样图片:

    +

    您可能会疑问,是不是搞错图片啦,这显然不是一个模样的,非也非也,就本质上而言,圆角的实现与上面的梯形图就是同样的东西。现在,盯着上面这张图,我们想象一下,用力的想象,用想花姑娘的那番劲头想象——上面的梯形宽度越来越宽(不是拉伸),一直宽到500像素,是不是与上面实现的圆角的下边缘一致啊?

    +

    也就是说,那个含有“我是一只小小鸟……”文字的圆角图形是有一个上梯形+矩形+下梯形组成的。参见下面的分离效果图:
    您可以狠狠地点击这里:CSS border圆角生成demo

    +

    局限性

    人无完人,金无足赤,此方法虽然简洁高效,兼容性上佳,但是依然有局限性,在实现实色背景的圆角效果时,此方法可谓首选;如果是纯粹的圆角边框,此方法也可以实现,需要用到边框重叠,但是标签数几乎要翻倍,其权衡效用将大打折扣,反不如其他圆角方法来的实在。

    +

    结语

    如果在web制作中,需要用的一些直接可以使用CSS+单标签模拟的图片,我的建议是“毫不犹豫使用CSS模拟”,例如实色的三角,或是实现实色的圆角效果,这可以说是最高效,最利于扩展维护的前端实现方法了。我们需要开阔的思维,而不要仅仅局限于眼前的技术,武侠中所谓的“无招胜有招”还是有着一定的哲学道理的,长远来看,意识与海纳百川的心态比当下的一点技术更来得重要。

    + +
    + + + + +
    +
    如果觉得不错请支持作者
    + + +
    + + +
    +
    + ------ 版权声明 ------ + + +

    本文标题:CSS border三角、圆角图形生成技术详解

    +

    文章作者:

    +

    发布时间:2013年06月26日 - 22:22

    +

    最后更新:2023年09月20日 - 10:18

    +

    原始链接:https://blog.lifesli.com/2013/06/26/CSS-Triangle-Circle/ + +

    +

    许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

    +
    + +
    + + +
    --> + + + + + + + + + +
    + +
    + + + +
    + + + + + + + + +
    + + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2015/12/31/Read-Let-your-boss-promote-you/index.html b/ja/2015/12/31/Read-Let-your-boss-promote-you/index.html new file mode 100644 index 0000000000..8d132b03ab --- /dev/null +++ b/ja/2015/12/31/Read-Let-your-boss-promote-you/index.html @@ -0,0 +1,780 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《让老板提拔你》读书笔记 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    + + + + + +
    +

    + 《让老板提拔你》读书笔记 +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    最近读了《让老板提拔你》,下面将书中觉得有用的内容进行摘抄,放在下方。

    +

    定位自己

    正确认识自己,确定社会定位、职业定位。 定位-决定-定价

    +

    要素

    核心竞争力职位 契合度 是高薪关键所在

    +

    契合度

      +
    • 技能、专长、经历与职位要求的契合度
    • +
    • 专业资质和等级与职位要求的契合度
    • +
    • 综合素质与职位要求的契合度

      七大秘诀

    • +
    • 了解同行业薪酬的平均水平
    • +
    • 赢得未来单位的心
    • +
    • 先让对方开口
    • +
    • 勇敢地开口要求
    • +
    • 不要轻言放弃
    • +
    • 把握时机很重要
    • +
    • 说实话,别撒谎

      如何谈薪资

    • +
    • 商洽薪酬的关键在于把握好谈话时机,切勿开门见山。
      +

      只要有发展机会,我愿意接受贵单位的薪酬标准,不知道按规定这个工作岗位的薪酬标准是多少。

      +
      +
    • +
    +

    将知识卖个好价钱

    推销自己既推销自己所掌握的知识即智慧,正确认识自己的价值是关键。

    +

    高薪是因为“物有所值”

      +
    • 用业绩、用能力说话,是人才坦然面对高薪的心态。
    • +
    • 高薪人才的求职方式 —- 圈内朋友引见、定向投简历、行业论坛、交流活动等。
    • +
    • 高级人才不能经常跳槽要保持相对的职业稳定。—- 在一家公司少则3年多则6年 —- 接受挑战,把握机会。
    • +
    +

    失败丰富走向成功经验

    强调在失败中吸取的经验,在未来中可以避免的损失。

    +

    能为企业带来丰厚的利润才是人才

    企业招聘员工的唯一目的:追求公司业绩最大化、利润最大化。

    +

    高质高效工作全攻略

      +
    • 进行正确的自我评价
    • +
    • 做最擅长做的事
        +
      • 三个经济原则 —- 发挥人才优势。
          +
        1. 比较利益原则—–自己的专长对自己才是最有利的,勿与他人作比较。
        2. +
        3. 机会成本原则—–选择一份工作放弃其他工作就是机会成本。
        4. +
        5. 效率原则——–工作的成功不在于时间有多长,而在于成效有多少,附加值有多少。
        6. +
        +
      • +
      +
    • +
    • 马上行动
    • +
    • 做事情要雷厉风行 ——— 成功属于踏实肯干的人
    • +
    • 有条不紊地开展工作 ——- 制定时间计划
    • +
    • 善于利用现代办公工具
    • +
    • 给自己最大的工作空间
    • +
    • 建立高效有序的办公环境
    • +
    • 不要忘记最初想去的方向
    • +
    • “聪明”的向上级提出建议
    • +
    • 专心做事,避免浮躁
    • +
    • 多而不专,一事难成
    • +
    • “专”才有高质工作——–人才主要指专才–某个领域的高精尖人才–勿做全才,一专多能
    • +
    • 做事要有条理
    • +
    +

    不要只把自己当成一个打工仔

    +

    要把工作当成事业

    +
    +
      +
    • 工作不仅仅是为了钱
    • +
    • 对工作要有明确的价值取向
        +
      1. 认清人生的方向
      2. +
      3. 开始学会醉卧探索和认知
      4. +
      5. 认清工作价值与成就的关系
      6. +
      7. 长期的工作规划
      8. +
      9. 在生命的天平上衡量自身的价值
      10. +
      +
    • +
    • 巧妙应对与上司看法向左时的三条准则
        +
      1. 遇事考虑全局
      2. +
      3. 辩证地看待问题
      4. +
      5. 切记感情用事
      6. +
      +
    • +
    • 把单位的事当成自家的事
    • +
    • 认真负责地用心工作
    • +
    • 珍惜岗位,热爱自己的职业
    • +
    • 永远是在为自己工作
    • +
    • 敬重自己的工作
    • +
    • 不要轻视薪水微薄的工作
    • +
    • 永远对工作充满激情
    • +
    • 以自己的工作为荣
    • +
    • 不要被他人的观点所束缚
    • +
    • 暂时的胜负并不会决定人生的最后走向
    • +
    • 将弱势转化为优势
    • +
    • 全力以赴做好每一天的工作
    • +
    • 和优秀的人士在一起—见贤思齐、借梯爬楼
        +
      • 如何争取跟优秀的人在一起
          +
        1. 不断的抛头露面
        2. +
        3. 帮助可以帮助自己成就事业的人做事
        4. +
        5. 与上司和比自己优秀的人士一起合作
        6. +
        +
      • +
      +
    • +
    +
      +
    1. 尊重对方,严谨有致
    2. +
    3. 切记奉承,要不卑不亢
    4. +
    5. 态度自然,不必拘谨
    6. +
    7. 陪衬得当,不可狂妄
    8. +
    9. 主动真诚,做出姿态
    10. +
    11. 求助求教,接受呵护
    12. +
    +
      +
    • 挑战自我,承担责任
        +
      • 三条忠告
          +
        1. 全心全意工作
        2. +
        3. 把自己视为合伙人
        4. +
        5. 迎接变革的需求
        6. +
        +
      • +
      +
    • +
    • 自信独立,不随波逐流
    • +
    • 敢于显示自己很重要
    • +
    • 千万不能只知道抱怨上司
    • +
    • 保持严谨认真的做事习惯
    • +
    • 自主地做好手中的工作
    • +
    • 踏踏实实地做好本职工作
    • +
    • 丢掉工作散漫的坏习惯
    • +
    • 不要让浮躁的性格困扰自己
    • +
    • 不推诿,勇于承担责任
    • +
    • 无论如何都不要拖延工作
    • +
    • 糊弄工作只能是在糊弄自己
    • +
    • 逊色的工作只会淘汰自己
    • +
    • 千万别丢掉“得宠”之资
    • +
    • “一步登天”只会摔疼自己
    • +
    • 别让“差不多”贻误了自己
    • +
    • 能完成100%,就决不做99%
    • +
    +

    与上司相处

      +
    • 不要做上司的“心腹”
    • +
    • 适时恰当的赞美上司
        +
      • 赞美上司,还要善于选择适当的场合
      • +
      • 赞美上司,要学会巧借公众语言称赞
      • +
      • 赞美上司,还要善于赞美不得志的上司
      • +
      +
    • +
    • 主动与领导沟通
    • +
    • 主动和上司保持联系
    • +
    • 用“心机”主动接近上司
        +
      • 尽可能详细的了解上司
      • +
      • 选择一个与领导尽可能近的位置
      • +
      • 赢得上司青睐的方法
      • +
      +
    • +
    • 更有效的和上司沟通
        +
      • 与上司沟通要简洁
      • +
      • 与上司沟通要大度大气大方
      • +
      • 与上司沟通,就要把自己先放一边 ——– 说话对事不对人
      • +
      +
    • +
    • 四种和上司进行沟通的方法
        +
      1. 开诚布公式———提出问题,说出自己的理解认识,给出解决方案,寻求领导意思,执行领导的命令
      2. +
      3. 先斩后奏式———因主客观原因导致的没有先汇报而是先解决的事,事后要将问题的分析方法,解决方法,实施方案以及结果做一份详尽的总结报告上交给领导
      4. +
      5. “含情脉脉”式——-通过交流一些工作上的问题来含蓄表达出个人的建议、需要
      6. +
      7. 巧设比喻式———通过典型案例来暗示自己的意见和要求
      8. +
      +
    • +
    • 把话说到上司的心坎上
    • +
    • 巧妙地为领导拾遗补缺
        +
      1. 诠释领导讲话的难点
      2. +
      3. 强调领导的才干
      4. +
      5. 化严肃为幽默
      6. +
      7. 稳定情绪,委婉暗示
      8. +
      +
    • +
    • 工作中勤于请示汇报
        +
      1. 听懂上司的意图
      2. +
      3. 探讨、磨合,达成共识
      4. +
      5. 制定尽可能详尽的工作计划
      6. +
      7. 随时向上司汇报任务的关键点
      8. +
      9. 总结汇报
      10. +
      +
    • +
    • 用成功赢得上司的信任
    • +
    • 工作中不要冲撞上司
    • +
    • 处理好同上司之间的分歧
        +
      1. 圆融协调——领导不懂,下达了错误的指令
          +
        1. 私下向上司陈述意见,帮助上司做出正确的决策
        2. +
        3. 如果上司采纳了下属的建议或意见,该下属有责任长期保守秘密
        4. +
        5. 如果上司固执己见,那么无条件服从
        6. +
        +
      2. +
      3. 装聋作哑——不涉及到原则问题
      4. +
      5. 棘手难题多权衡
          +
        1. 立刻插话纠正
        2. +
        3. 提醒上司
        4. +
        5. 暗示
        6. +
        7. 事后补救
        8. +
        9. 事后提醒
        10. +
        +
      6. +
      +
    • +
    • 正确对待上司的批评
    • +
    • 要善于服从自己的上司
    • +
    • 正确化解来自上司的压力
    • +
    +

    写在最后

    博观约取,多读书读好书,丰富自己,变得睿智。

    + +
    + + + + +
    +
    如果觉得不错请支持作者
    + + +
    + + +
    +
    + ------ 版权声明 ------ + + +

    本文标题:《让老板提拔你》读书笔记

    +

    文章作者:

    +

    发布时间:2015年12月31日 - 22:22

    +

    最后更新:2023年09月20日 - 10:18

    +

    原始链接:https://blog.lifesli.com/2015/12/31/Read-Let-your-boss-promote-you/ + +

    +

    许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

    +
    + +
    + + +
    --> + + + + + + + + + +
    + +
    + + + +
    + + + + + + + + +
    + + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2016/10/27/Read-The-miracle-of-the-morning-journal/index.html b/ja/2016/10/27/Read-The-miracle-of-the-morning-journal/index.html new file mode 100644 index 0000000000..5cf7675036 --- /dev/null +++ b/ja/2016/10/27/Read-The-miracle-of-the-morning-journal/index.html @@ -0,0 +1,688 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《晨间日记的奇迹》读书笔记 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    + + + + + +
    +

    + 《晨间日记的奇迹》读书笔记 +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    昨天花了1天的时间读了《晨间日记的奇迹》,感觉整个人的认识发生了一些变化,下面把重要内容做一下摘抄总结。

    +

    早上写日记的好处 —— 提升效率

      +
    • 可以做好一天的准备 — 计划性
    • +
    • 可以正确的写出昨天发生的事 — 效率性&忠诚性
    • +
    • 可以中立的看待昨天 — 中立性
    • +
    • 相对自由的时间 — 持续性
    • +
    • 总结经验 — 活用性
    • +
    +

    注意事项

    日记 不等于 日志
    日记 要记录 时间 天气 心情 — 因为这些有助于回忆和记录
    不要投入过长时间 — 3分钟 — 日记私密性
    晨间日记前7天只写好事 — 反省固然重要,但是成长更重要,持续性很重要 — 巴普洛夫的狗

    +

    晨间日记2部分

    Part1

    客观记录已经发生的事(昨天)— 经验智慧

    +

    Part2

      +
    • 今天应做的事 — 具体行动(来自昨天的总结
    • +
    • 今天一定要做的事(紧急重要的事)— 变得更积极更主动
    • +
    • 未来要做的事 — 不紧急但重要的事
    • +
    • 连用日记 — 历史上的今天(过去一年同一天的事)
    • +
    +

    夜晚日记 VS 晨间日记

    受当天情绪影响 — 更冷静

    +

    梦想成真表

    + + + + + + + + + + + + + + + + + +
    过去未来
    事实IQ 智慧指数NQ 人际关系指数
    感情EQ 情感指数DQ 梦想指数
    +
      +
    • IQ — 晨间日记冷静回忆分析 — 提高决策正确性 IQ up
    • +
    • EQ — 冷静记录昨日的情绪/自省 — 变积极 EQ up
    • +
    • NQ — 总结昨天给今天设立“对/为XXX做某事” 很重要 NQ up
    • +
    • DQ — 不能只有眼前的事 每天都要确定一下自己的梦想 脸皮厚点没关系 重复或只有一句话也没关系 铭记于心最重要
    • +
    +

    “忙碌”只是借口!“决心”才是问题! 对未来充满期待的人才有行动力

    +

    如何早起

      +
    • 设定起床音乐/用气味(精油)刺激起床/光线-电动窗帘早起自动拉开
    • +
    • 早起后淋浴 — 配合音乐/冥想/呐喊 — 让自己充满动力保持清醒
    • +
    +

    写日记的五大好处

      +
    • 提升写作能力
    • +
    • 谈话题材源源不断
    • +
    • 提高贵人运
    • +
    • 返现自我肉体和精神的状态与模式
    • +
    • 在自己身上挖宝,彻底改变人生
    • +
    +

    记录的日记要常拿出来看看

    记录日记时 — 问问自己的灵魂 然后写下真心话 — 灵魂日记
    六度空间理论

    +

    七种成功者的习惯

      +
    • 习惯之一:积极主动别指望谁能推你走 — 主动行动,迈出第一步
    • +
    • 习惯之二:以终为始忠于自己的人生计划。— 设立人生目标并坚持
    • +
    • 习惯之三:要事第一选择当前该做的事
    • +
    • 习惯之四:追求双赢远离角斗场
    • +
    • 习惯之五:善于沟通换位思考的原则
    • +
    • 习惯之六:统合综效 1+1可以大于2
    • +
    • 习惯之七:不断更新全方位平衡自我
    • +
    +

    早睡是为了身体,早起是为了我们的内心。— sugiponn

    +

    晨间日记的格式

    晨间日记必须要设立的栏目 — 工作/金钱/健康/人际关系
    要记下当日的日期/天气/温度/湿度

    +

    纬度标签
    工作方面:

    +
      +
    • 能力开发/经营/管理/进货/工作人员/学习/资格/活动/企划案/想法/推广
      金钱方面:
    • +
    • 收入/指出/购入/股票/资产/储蓄/家用
      健康方面:
    • +
    • 饮食/运动/性/减肥/身体状况/锻炼身体/体重/脂肪
      人际关系方面:
    • +
    • 父母/家人/小孩/亲戚/情人/工作伙伴/朋友/熟人/联络事项/人脉网络
      兴趣方面以及其他:
    • +
    • 义工活动/阅读/运动/打扮/电影/音乐/游戏/喜欢的物品/今天必须要注意的事/美容/开心的事/购物
    • +
    +

    劳动 — 职业 — 工作 — 乐趣

    +

    三大原则和七大作战守则

      +
    • 原则1:时间不超过3分钟 — 减少养成习惯的成本

      +
    • +
    • 原则2:决定好写晨间日记的地方 — 为了养成习惯

      +
    • +
    • 原则3:只写一个字也没关系 — 不要有压力

      +
    • +
    • 作战守则1:开心写日记 — 反省固然重要,鼓励更有作用 — 开始写晨间日记前七天只写好事

      +
    • +
    • 作战守则2:ONE-TWO作战 — 把动作分布容易养成习惯 — 形成固定动作

      +
    • +
    • 作战守则3:巴普洛夫的狗 — 开始写日记时候要有一个仪式 — 心理暗示

      +
    • +
    • 作战守则4:奖励自己 — 当习惯养成并坚持到一定时间,给自己一个奖励

      +
    • +
    • 作战守则5:宣告天下 — 借由他人的鼓励或压力形成动力/也可找到同志

      +
    • +
    • 作战守则6:一千日作战法 — 不管什么样的习惯坚持1000日就会成为永远的习惯 — 具体做法为记录 第XXX篇 晨间日记

      +
    • +
    • 作战守则7:为了别人 — 写晨间日记不只是为了自己,而且更是为了别人,身边人的幸福最终会影响到自己

      +
    • +
    +

    应该先肯定自己,给自己打100分

      +
    • “总之,先… 病” 没有目标/没有梦想 — 回顾时一定会后悔!
    • +
    • “假如…,应该 病” 无法满足/没有动力 — 焦躁不安
    • +
    • 共同点:失落感/空虚/
    • +
    +

    解决办法— 设立一个情境

    例:我要建立一个幸福的家庭 —> 因此我要创造很多快乐的回忆 —> 为了制造很多快乐的回忆 —> 我必须要到很多的地方去看看 —> 为了去很多地方看看,我觉得有辆车会更方便 —> 买车 —> 考驾照

    +

    不要总想自己哪里欠缺,要想自己想要得到什么,并且为了目标努力,你就会变的更完美。
    拥有一个敢于大声喊出的梦想,不要羞涩,要为了梦想一步步的前进,最终达到梦想。

    +

    提到的另外的书

    《培育梦想种子》《日记的力量》《成功人士的七个习惯》

    + +
    + + + + +
    +
    如果觉得不错请支持作者
    + + +
    + + +
    +
    + ------ 版权声明 ------ + + +

    本文标题:《晨间日记的奇迹》读书笔记

    +

    文章作者:

    +

    发布时间:2016年10月27日 - 22:22

    +

    最后更新:2023年09月20日 - 10:18

    +

    原始链接:https://blog.lifesli.com/2016/10/27/Read-The-miracle-of-the-morning-journal/ + +

    +

    许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

    +
    + +
    + + +
    --> + + + + + + + + + +
    + +
    + + + +
    + + + + + + + + +
    + + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ja/2017/08/29/browser-incompatibility-problem-solution/index.html b/ja/2017/08/29/browser-incompatibility-problem-solution/index.html new file mode 100644 index 0000000000..f9af3cf681 --- /dev/null +++ b/ja/2017/08/29/browser-incompatibility-problem-solution/index.html @@ -0,0 +1,656 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 浏览器兼容性问题解决方案 | 李旭光的成长博客 + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + + +
    + + + + + +
    +

    + 浏览器兼容性问题解决方案 +

    + + +
    + + + + +
    + + +
    +

    作者:李旭光
    引用请标明出处

    +
    +

    前言

    作为前端,浏览器就是你的战场,而不同的浏览器就是不同的敌人,敌人有相同之处,也各有特点,这不同的特点就是不同浏览器的兼容性,下面就聊聊浏览器的兼容问题,如何解决不同浏览器的兼容性。

    + +

    普及:浏览器的兼容性问题,往往是个别浏览器(没错,就是那个与众不同的浏览器)对于一些标准的定义不一致导致的。俗话说:没有IE就没有伤害。

    +

    贴士:内容都是自己总结的,不免会出现错误或者bug,欢迎更正和补充,本帖也会不断更新。

    +

    Normalize.css

    不同浏览器的默认样式存在差异,可以使用 Normalize.css抹平这些差异。当然,你也可以定制属于自己业务的 reset.css

    +
    1
    <link href="https://cdn.bootcss.com/normalize/7.0.0/normalize.min.css" rel="stylesheet">
    +

    简单粗暴法

    +
    1
    * { margin: 0; padding: 0; }
    +

    html5shiv.js

    解决 ie9 以下浏览器对 html5 新增标签不识别的问题。

    +
    1
    2
    3
    <!--[if lt IE 9]>
    <script type="text/javascript" src="https://cdn.bootcss.com/html5shiv/3.7.3/html5shiv.min.js"></script>
    <![endif]-->
    + +

    respond.js

    解决 ie9 以下浏览器不支持 CSS3 Media Query 的问题。

    +
    1
    <script src="https://cdn.bootcss.com/picturefill/3.0.3/picturefill.min.js"></script>
    +

    IE 条件注释

    IE 的条件注释仅仅针对IE浏览器,对其他浏览器无效
    image

    +

    IE 属性过滤器(较为常用的hack方法)

    针对不同的 IE 浏览器,可以使用不同的字符来对特定的版本的 IE 浏览器进行样式控制
    image
    image

    +

    浏览器 CSS 兼容前缀

    1
    2
    3
    4
    5
    6
    7
    8
    9
    -o-transform:rotate(7deg); // Opera

    -ms-transform:rotate(7deg); // IE

    -moz-transform:rotate(7deg); // Firefox

    -webkit-transform:rotate(7deg); // Chrome

    transform:rotate(7deg); // 统一标识语句
    +

    补充: 目前可以采用自动化插件完成,插件名称叫做 Autoprefixer,他可以解析css文件并且添加前缀到css内容里。

    +

    Auroprefixer 添加到资源构建工具如:webpack后,就可以不用再手动补全浏览器前缀了,这里只需要你按照W3C的标准来书写css代码,剩下的工作就交给插件完成,目前webpackgulpgrunt都有相应的插件,是不是开心啊。

    +

    a 标签的几种 CSS 状态的顺序

    很多新人在写 a 标签的样式,会疑惑为什么写的样式没有效果,或者点击超链接后,hover、active 样式没有效果,其实只是写的样式被覆盖了。

    +

    正确的a标签顺序应该是:==love hate==

    +
      +
    1. link:平常的状态
    2. +
    3. visited:被访问过之后
    4. +
    5. hover:鼠标放到链接上的时候
    6. +
    7. active:链接被按下的时候
    8. +
    +

    完美解决 Placeholder

    1
    <input type="text" value="Name *" onFocus="this.value = '';" onBlur="if (this.value == '') {this.value = 'Name *';}">
    + +

    清除浮动 最佳实践

    1
    2
    3
    4
    .fl { float: left; }
    .fr { float: right; }
    .clearfix:after { display: block; clear: both; content: ""; visibility: hidden; height: 0; }
    .clearfix { zoom: 1; }
    + +

    BFC 解决边距重叠问题

    当相邻元素都设置了 margin 边距时,margin 将取最大值,舍弃小值。为了不让边距重叠,可以给子元素加一个父元素,并设置该父元素为 BFC:overflow: hidden;

    +
    +

    Lorem ipsum dolor sit.

    + +
    +

    Lorem ipsum dolor sit.

    +
    + +

    Lorem ipsum dolor sit.

    +
    + +

    IE6 双倍边距的问题

    设置 ie6 中设置浮动,同时又设置 margin,会出现双倍边距的问题

    +
    1
    display: inline;
    +

    解决 IE9 以下浏览器不能使用 opacity

    1
    2
    3
    opacity: 0.5;
    filter: alpha(opacity = 50);
    filter: progid:DXImageTransform.Microsoft.Alpha(style = 0, opacity = 50);
    +

    解决 IE6 不支持 fixed 绝对定位以及IE6下被绝对定位的元素在滚动的时候会闪动的问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* IE6 hack */
    *html, *html body {
    background-image: url(about:blank);
    background-attachment: fixed;
    }
    *html #menu {
    position: absolute;
    top: expression(((e=document.documentElement.scrollTop) ? e : document.body.scrollTop) + 100 + 'px');
    }
    +

    IE6 背景闪烁的问题

    问题:链接、按钮用 CSSsprites 作为背景,在 ie6 下会有背景图闪烁的现象。原因是 IE6 没有将背景图缓存,每次触发 hover 的时候都会重新加载

    +

    解决:可以用 JavaScript 设置 ie6 缓存这些图片:

    +
    1
    document.execCommand("BackgroundImageCache", false, true);
    +

    解决在 IE6 下,列表与日期错位的问题

    日期 标签放在标题 标签之前即可
    image

    +

    解决 IE6 不支持 min-height 属性的问题

    1
    2
    min-height: 350px;
    _height: 350px;
    + +

    让 IE7 IE8 支持 CSS3 background-size属性

    由于 background-size 是 CSS3 新增的属性,所以 IE 低版本自然就不支持了,但是老外写了一个 htc 文件,名叫 background-size polyfill,使用该文件能够让 IE7、IE8 支持 background-size 属性。其原理是创建一个 img 元素插入到容器中,并重新计算宽度、高度、left、top 等值,模拟 background-size 的效果。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    html {
    height: 100%;
    }
    body {
    height: 100%;
    margin: 0;
    padding: 0;
    background-image: url('img/37.png');
    background-repeat: no-repeat;
    background-size: cover;
    -ms-behavior: url('css/backgroundsize.min.htc');
    behavior: url('css/backgroundsize.min.htc');
    }
    +

    IE6-7 line-height 失效的问题

    问题:在ie 中 img 与文字放一起时,line-height 不起作用

    +

    解决:都设置成 float

    +

    td 自动换行的问题

    问题:table 宽度固定,td 自动换行

    +

    解决:设置 Table 为 table-layout: fixedtdword-wrap: break-word

    +

    让层显示在 FLASH 之上

    想让层的内容显示在 flash 上,把 FLASH 设置透明即可

    +
    1
    2
    1、<param name=" wmode " value="transparent" />
    2、<param name="wmode" value="opaque"/>
    +

    键盘事件 keyCode 兼容性写法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var inp = document.getElementById('inp')
    var result = document.getElementById('result')

    function getKeyCode(e) {
    e = e ? e : (window.event ? window.event : "")
    return e.keyCode ? e.keyCode : e.which
    }

    inp.onkeypress = function(e) {
    result.innerHTML = getKeyCode(e)
    }
    + +

    求窗口大小的兼容写法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 浏览器窗口可视区域大小(不包括工具栏和滚动条等边线)
    // 1600 * 525
    var client_w = document.documentElement.clientWidth || document.body.clientWidth;
    var client_h = document.documentElement.clientHeight || document.body.clientHeight;

    // 网页内容实际宽高(包括工具栏和滚动条等边线)
    // 1600 * 8
    var scroll_w = document.documentElement.scrollWidth || document.body.scrollWidth;
    var scroll_h = document.documentElement.scrollHeight || document.body.scrollHeight;

    // 网页内容实际宽高 (不包括工具栏和滚动条等边线)
    // 1600 * 8
    var offset_w = document.documentElement.offsetWidth || document.body.offsetWidth;
    var offset_h = document.documentElement.offsetHeight || document.body.offsetHeight;

    // 滚动的高度
    var scroll_Top = document.documentElement.scrollTop||document.body.scrollTop;
    +

    DOM 事件处理程序的兼容写法(能力检测)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    var eventshiv = {
    // event兼容
    getEvent: function(event) {
    return event ? event : window.event;
    },

    // type兼容
    getType: function(event) {
    return event.type;
    },

    // target兼容
    getTarget: function(event) {
    return event.target ? event.target : event.srcelem;
    },

    // 添加事件句柄
    addHandler: function(elem, type, listener) {
    if (elem.addEventListener) {
    elem.addEventListener(type, listener, false);
    } else if (elem.attachEvent) {
    elem.attachEvent('on' + type, listener);
    } else {
    // 在这里由于.与'on'字符串不能链接,只能用 []
    elem['on' + type] = listener;
    }
    },

    // 移除事件句柄
    removeHandler: function(elem, type, listener) {
    if (elem.removeEventListener) {
    elem.removeEventListener(type, listener, false);
    } else if (elem.detachEvent) {
    elem.detachEvent('on' + type, listener);
    } else {
    elem['on' + type] = null;
    }
    },

    // 添加事件代理
    addAgent: function (elem, type, agent, listener) {
    elem.addEventListener(type, function (e) {
    if (e.target.matches(agent)) {
    listener.call(e.target, e); // this 指向 e.target
    }
    });
    },

    // 取消默认行为
    preventDefault: function(event) {
    if (event.preventDefault) {
    event.preventDefault();
    } else {
    event.returnValue = false;
    }
    },

    // 阻止事件冒泡
    stopPropagation: function(event) {
    if (event.stopPropagation) {
    event.stopPropagation();
    } else {
    event.cancelBubble = true;
    }
    }
    };
    + +
    + + + + +
    +
    如果觉得不错请支持作者
    + + +
    + + +
    +
    + ------ 版权声明 ------ + + +

    本文标题:浏览器兼容性问题解决方案

    +

    文章作者:

    +

    发布时间:2017年08月29日 - 17:04

    +

    最后更新:2023年09月20日 - 10:18

    +

    原始链接:https://blog.lifesli.com/2017/08/29/browser-incompatibility-problem-solution/ + +

    +

    许可协议: 署名-非商业性使用-禁止演绎 4.0 + 国际 转载请保留原文链接及作者。

    +
    + +
    + + +
    --> + + + + + + + + + +
    + +
    + + + +
    + + + + +
    +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + +
    + + + + +
    + + + + + +
    +

    + + +

    + + +
    + + + + +
    + + +

    前言

    最近各种技术博客,各种技术文章,各种牛人前辈的学习心得都在看,最近看了一个不错的文章叫做《如何正确使用时间》
    感觉还是很不错的,特别适合年轻的程序员看一下。下面将全文贴在下方,如果需要也可以去原文查看。

    +

    原文

    +

    你是否时常会焦虑时间过的很快,没时间学习,本文将会分享一些个人的见解。

    +

    花时间补基础,读文档

    在工作中我们时常会花很多时间去 debug,但是你是否发现很多问题最终只是你基础不扎实或者文档没有仔细看。

    +

    基础是你技术的基石,一定要花时间打好基础,而不是追各种新的技术。一旦你的基础扎实,学习各种新的技术也肯定不在话下,因为新的技术,究其根本都是相通的。

    +

    文档同样也是一门技术的基础。一个优秀的库,开发人员肯定已经把如何使用这个库都写在文档中了,仔细阅读文档一定会是少写 bug 的最省事路子。

    +

    学会搜索

    如果你还在使用百度搜索编程问题,请尽快抛弃这个垃圾搜索引擎。同样一个关键字,使用百度和谷歌,谷歌基本完胜的。即使你使用中文在谷歌中搜索,得到的结果也往往是谷歌占优,所以如果你想迅速的通过搜索引擎来解决问题,那一定是谷歌。

    +

    学点英语

    说到英语,一定是大家所最不想听的。其实我一直认为程序员学习英语是简单的,因为我们工作中是一直接触着英语,并且看懂技术文章,文档所需要的单词量是极少的。我时常在群里看到大家发出一个问题的截图问什么原因,其实在截图中英语已经很明白的说明了问题的所在,如果你的英语过关,完全不需要浪费时间来提问和搜索。所以我认为学点英语也是节省时间中很重要的一点。

    +

    那么如何去学习呢,chrome 装个翻译插件,直接拿英文文档或文章读,不会的就直接划词翻译,然后记录下这个单词并背诵。每天花半小时看点英文文档和文章,坚持两个月,你的英语水平不说别的,看文档和文章绝对不会有难题了。这一定是一个很划算的个人时间投资,花点时间学习英语,能为你将来的技术之路铺平很多坎。

    +

    画个图,想一想再做

    你是否遇到过这种问题,需求一下来,看一眼,然后马上就按照设计稿开始做了,可能中间出个问题导致你需要返工。

    +

    如果你存在这样的问题,我很推荐在看到设计稿和需求的时候花点时间想一想,画一画。考虑一下设计稿中是否可以找到可以拆分出来的复用组件,是否存在之前写过的组件。该如何组织这个界面,数据的流转是怎么样的。然后画一下这个页面的需求,最后再动手做。

    +

    利用好下班时间学习

    说到下班时间,那可能就有人说了公司很迟下班,这其实是国内很普遍的情况。但是我认为正常的加班是可以的,但是强制的加班就是在损耗你的身体和前途。

    +

    可以这么说,大部分的 996 公司,加班的这些时间并不会增加你的技术,无非就是在写一些重复的业务逻辑。也许你可以拿到更多的钱,但是代价是身体还有前途。程序员是靠技术吃饭的,如果你长久呆在一个长时间加班的公司,不能增长你的技术还要吞噬你的下班学习时间,那么你一定会废掉的。如果你遇到了这种情况,只能推荐尽快跳槽到非 996 的公司。

    +

    那么如果你有足够的下班时间,一定要花上 1, 2 小时去学习,上班大家基本都一样,技术的精进就是看下班以后的那几个小时了。如果你能利用好下班时间来学习,坚持下去,时间一定会给你很好的答复。

    +

    列好 ToDo

    我喜欢规划好一段时间内要做的事情,并且要把事情拆分为小点。给 ToDo 列好优先级,紧急的优先级最高。相同优先级的我喜欢先做简单的,因为这样一旦完成就能划掉一个,提高成就感。

    +

    反思和整理

    每周末都会花上点时间整理下本周记录的笔记和看到的不错文章。然后考虑下本周完成的工作和下周准备要完成的工作。

    + + +
    + + + + +
    +
    +
    +
    + + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
  • +
    + +
    +
    + + +
    + + + +

    李旭光的成长博客

    + +
    +

    求知若饥,虚心若愚。

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    +
    + + + + +
    +
    +
    + + + + +
    + + + + + +
    +
    + +

    +

    + + + +
    + + + + +
    +

    这里追加图片

    +
    + + + +
    + + + + + + + +
    + +
    + + + +
    + + + + + + + + +
    +
    + + +