├── .gitignore ├── README.md ├── _config.yml ├── config ├── deploy.sh └── nginx │ ├── conf.d │ └── blog.conf │ └── nginx.conf ├── package-lock.json ├── package.json ├── scaffolds ├── draft.md ├── page.md └── post.md ├── source ├── _posts │ ├── a-robust-equality-operation.md │ ├── a-strategic-guide-to-the-network-economy.md │ ├── active-handle-and-timer-in-nodejs-1.md │ ├── active-handle-and-timer-in-nodejs-2.md │ ├── analysis-on-the-lookup-dns-cache-and-nodejs.md │ ├── async-programming-in-js.md │ ├── broadcast-channel.md │ ├── code-split-in-react-router4.md │ ├── commonjs-without-build-and-server.md │ ├── cross-origin-in-browsers.md │ ├── cross-tab-communication-in-frontend.md │ ├── css-layout-guide.md │ ├── css-modular-guide-1.md │ ├── css-modular-guide-2.md │ ├── css-modular-guide-3.md │ ├── css-only-chat.md │ ├── css-theming-methods.md │ ├── fe-performance-journey.md │ ├── frontend-tech-list.md │ ├── how-snowpack-works.md │ ├── how-to-improve-json-stringify-performance.md │ ├── how-to-make-a-tool-for-printing-banner-in-console.md │ ├── how-to-make-your-own-scaffold.md │ ├── implement-a-react-like-lib.md │ ├── implement-promise-a-plus.md │ ├── improvement-in-webpack.md │ ├── migration-from-vue-cli-to-vite2.md │ ├── mongodb-replica-set.md │ ├── mpa-based-on-gulp-in-action.md │ ├── optimize-packing-to-speed-up.md │ ├── pwa-1.md │ ├── pwa-10.md │ ├── pwa-2.md │ ├── pwa-3.md │ ├── pwa-4.md │ ├── pwa-5.md │ ├── pwa-6.md │ ├── pwa-7.md │ ├── pwa-8.md │ ├── pwa-9.md │ ├── quick-start-a-react-spa.md │ ├── quicklink-implementation-and-gotcha.md │ ├── request-timeout-after-finishing-in-nodejs.md │ ├── server-push-methods.md │ ├── set-selection-range.md │ ├── the-phoenix-project.md │ ├── troubleshooting-grpc-static-codegen.md │ ├── troubleshooting-nodejs-dns-mem.md │ ├── troubleshooting-npm-script-root-auth.md │ ├── web-note-highlight-in-js.md │ ├── webpack-babel-transform.md │ ├── webpack-loader-in-deep.md │ ├── webpack-module-runtime.md │ ├── webpack-plugin-hooks-visualization.md │ ├── why-use-static-types-in-js-1.md │ ├── why-use-static-types-in-js-2&3.md │ └── why-use-static-types-in-js-4.md └── img │ ├── 1294017096.jpg │ ├── 1629fca5313ecb88.png │ ├── 1629fd2a527121d2.png │ ├── 1629fd2a529adc58.png │ ├── 1629fd2a529dbe50.png │ ├── 1629fd2a52bb59a0.png │ ├── 162a560d0b6f194f.png │ ├── 162a560d0b7dfb84.gif │ ├── 162a560d0ba6b18b.png │ ├── 162a560d0bdaf33b.png │ ├── 162a560d0bdb6ed1.png │ ├── 162a560d2d6b1798.png │ ├── 162a560d30b47136.png │ ├── 162a560d35c15a67.png │ ├── 162a560d5f48ee00.png │ ├── 162a560d5f79572c.png │ ├── 162a560d65ba5c20.png │ ├── 162a560d66590ff7.png │ ├── 162afdd08f6cd205.png │ ├── 162afdd08f75977a.png │ ├── 162afdd08f8759e8.png │ ├── 162afdd08f8a3d0a.png │ ├── 162afdd08f96d4ed.png │ ├── 162afdd08f994938.png │ ├── 162afdd0b8f77b15.png │ ├── 162ba587f2a42eca.png │ ├── 162ba59261481104.png │ ├── 162ba68f0d4a5861.png │ ├── 162ba85304cba1f5.png │ ├── 162ba99a6b69af03.png │ ├── 162badb18824ff12.png │ ├── 162bc954b09d78cf.gif │ ├── 162bc97ba69e1679.gif │ ├── 162bfa0147d2b40d.png │ ├── 162bfd2d499ba656.png │ ├── 162bfe83b578881f.gif │ ├── 16310656fb567e8f.png │ ├── 163106e9284cbc26.png │ ├── 163107b914043f63.png │ ├── 163108561231f344.png │ ├── 163108efbea4b61d.png │ ├── 1631097d137fb789.png │ ├── 1631a562ba773ddd.gif │ ├── 1631a6c6007ffec9.png │ ├── 1631a6f12cc17f0c.png │ ├── 1631afa349ff2f0c.png │ ├── 1631b1ef1e592c36.gif │ ├── 1631b52052cccb59.gif │ ├── 163266184cbe4acd.png │ ├── 163266184cd3012c.png │ ├── 1632dfee442ac44d.png │ ├── 1632dff994a5f163.png │ ├── 1632dfffc7d10f7c.png │ ├── 1632e14e43c51ffb.png │ ├── 1632e19f6b41592b.png │ ├── 1632e19f6b55068a.png │ ├── 1632e66b1ad70b03.png │ ├── 1632e66b1b179577.png │ ├── 1632e66b434d09f4.png │ ├── 1632e8c6138105fa.png │ ├── 1632ea590a6e0088.png │ ├── 1632ea869bad3777.png │ ├── 1632eaf0bcf7c7b5.png │ ├── 1635905056b125a7.png │ ├── 1635975104e68836.gif │ ├── 163598ca174364ed.gif │ ├── 16359f504955c8b8.png │ ├── 16359f671e79ba6b.png │ ├── 1635a5723bed476c.gif │ ├── 1635a579ba845ce7.gif │ ├── 16397f563295ef0e.png │ ├── 1639b2f3954721b0.png │ ├── 1639b385aac7ce3c.png │ ├── 163dabc8440e4ece.png │ ├── 163daf0d02eb1e6c.png │ ├── 163db06dae676193.gif │ ├── 163db08447932e5e.gif │ ├── 163db0aba564b278.gif │ ├── 163db0c9519f6a53.gif │ ├── 163db3d3dd408fcb.png │ ├── 163db4bae3a5ec95.gif │ ├── 163db4e9e1cf80b1.gif │ ├── 16401f75d0bed9f5.png │ ├── 16401f75d1061a6b.png │ ├── 16401f75d1374bb6.png │ ├── 16401f75d1469309.png │ ├── 16401f75d150a951.png │ ├── 16401f7659bb8714.png │ ├── 16401f7659c340eb.gif │ ├── 16401f765fcc94dc.png │ ├── 16401f766fa6d723.png │ ├── 16401f76734dc323.png │ ├── 16401f767ed1bf08.png │ ├── 16401f76848f1d65.png │ ├── 16401f76a42d5ecd.png │ ├── 1645fd10965531f9.png │ ├── 1645fd10966db934.png │ ├── 1645fd10966e8ee5.png │ ├── 1645fd10967f2208.png │ ├── 1645fd10968de015.png │ ├── 1645fd1096a5574e.png │ ├── 1645fd10c4022eb6.png │ ├── 1645fd10c4105b96.png │ ├── 1645fd10c42778cd.png │ ├── 1645fd10ce900086.png │ ├── 1645fd10d55eab80.png │ ├── 1645fd10d760b18f.png │ ├── 1645fdc7e2d360d1.png │ ├── 1645fdcc9dec47b3.png │ ├── 1645fdd0fdacbfb7.png │ ├── 1645fe43135e298f.png │ ├── 1645ff2d0ec568f3.png │ ├── 1645ffbcc1e79e99.png │ ├── 1646049946695d35.png │ ├── 164653c5e7a035d4.png │ ├── 1646547a0ff1dacb.png │ ├── 1646549cd671d30a.png │ ├── 1646550a7f945c10.png │ ├── 164723a815c7d001.png │ ├── 16472418608ef0d4.png │ ├── 164725a874c0a4bd.png │ ├── 164c675aabb374df.png │ ├── 164c687371151554.gif │ ├── 1654852a672a4d8e.png │ ├── 1654c154b3778163.png │ ├── 1657be864c854472.png │ ├── 16625513129661c2.png │ ├── 166255f64585604f.png │ ├── 16625b6e741edc52.gif │ ├── 16625b730faf4a62.gif │ ├── 16628a3932c5b50b.gif │ ├── 166713f3741bc389.png │ ├── 166715d115c84870.png │ ├── 166726229bc58100.png │ ├── 1679e2f8c6f75eea.png │ ├── 1679e2fac347b63b.png │ ├── 1679e2fd21633517.png │ ├── 1679e300d854a05e.png │ ├── 167e4b2371f81ea0.png │ ├── 167e4b291f960c09.png │ ├── 167e54a6cc93bde4.png │ ├── 169d468988a6ba8f.png │ ├── 169d767c01990c37.gif │ ├── 169d80b1620cdac9.png │ ├── 169d80efd65b5401.png │ ├── 169d8452cef80241.gif │ ├── 169d8701f85f9f33.gif │ ├── 16a3b62dd95bac60.gif │ ├── 16a3b748f692f336.gif │ ├── 16a3b78be942a10d.png │ ├── 16a3f9a22e744782.png │ ├── 16f6c376da73aced.png │ ├── 16f6e12e3b3ac91a.png │ ├── 16f6f67316cf3a36.png │ ├── 16f8389a295640a6.jpg │ ├── 1_dQA3VhfjIQc1DYua6KoLFQ.png │ ├── 68747470733a2f2f692e696d6775722e636f6d2f4e56525a4c48762e706e67.png │ ├── a-plus.jpg │ ├── a-robust-equality-operation │ ├── 16f6f132c0bec8ec.png │ ├── 16f6f174c669e5f3.png │ ├── 16f6f2ddd2f954bc.png │ └── 16f6f35688449594.png │ ├── a-strategic-guide-to-the-network-economy │ ├── cover.jpg │ └── mindmap.svg │ ├── a1c34f42fd6a13ea1b3c.jpg │ ├── active-handle-and-timer-in-nodejs │ ├── 01.png │ ├── 02.png │ ├── 03.png │ ├── 04.png │ ├── 05.png │ ├── 06.png │ ├── 07.png │ ├── 08.png │ ├── 09.png │ ├── 10.png │ ├── 11.png │ └── nodejs-libuv.jpeg │ ├── analysis-on-the-lookup-dns-cache-and-nodejs │ └── 15886736971147.jpg │ ├── automatic-tool-flow.jpg │ ├── babel.jpg │ ├── bem-css.png │ ├── broadcast.jpg │ ├── chrome-dev.jpg │ ├── code-split-in-react-router4 │ ├── 6476654-c7d1c77bcbade8ee.png │ └── 6476654-edac2f3e0b68de1b.png │ ├── commonjs-without-build-and-server │ ├── 16f6bdd32223feba.png │ ├── 1_43_420BE-rnsY75fgjoydQ.jpg │ └── qj8120981169.jpg │ ├── communicating_message.jpg │ ├── css-chat.jpg │ ├── css-is-awesome.jpg │ ├── css-only-chat │ ├── 16ad543f357344b1.png │ ├── 16ad547da08e7d73.gif │ └── 16ad56d29a8af1c6.gif │ ├── fe-performance-journey-1.jpg │ ├── fe-performance-journey.png │ ├── firebase.jpg │ ├── frontend-tech-list.png │ ├── how-snowpack-works │ ├── 68747470733a2f2f696d6775722e636f6d2f755848466d35792e6.png │ ├── 68747470733a2f2f696d6775722e636f6d2f755848466d35792e6a7067.png │ └── bundling-webpack-graph.jpeg │ ├── how-to-improve-json-stringify-performance │ ├── 16b21d1399c2e90b.png │ ├── 16b21db215453a98.png │ ├── 16b223db88e935e6.png │ ├── 16b25784d49d825a.png │ ├── 16b25793da834834.png │ └── 16f6c376da73aced.png │ ├── how-to-make-a-tool-for-printing-banner-in-console │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ └── 5.png │ ├── how-to-make-your-own-scaffold │ ├── 16ac080688a52242.png │ ├── 16ac081750971790.gif │ └── 16ac137b8509c628.png │ ├── http-protocol.jpg │ ├── improvement-in-webpack │ ├── 1711a6ee306dd462.jpg │ ├── QJ6289726503.jpg │ ├── QJ6308900872.jpg │ ├── QJ7116776506.jpg │ ├── QJ8308459591.jpg │ ├── QJ8644641844.jpg │ └── QJ9127834588.jpg │ ├── input.jpg │ ├── json.jpg │ ├── migration-from-vue-cli-to-vite2 │ ├── 1.png │ ├── 2.png │ ├── 3.jpeg │ ├── 4.jpeg │ └── 5.jpeg │ ├── mongodb.svg │ ├── mpa-based-on-gulp-in-action │ ├── 6476654-071fe32a56dc9b70.png │ ├── 6476654-826361fea7518d59.png │ ├── 6476654-a512d598d147d565.png │ ├── 6476654-c78145e9df14fa47.png │ └── 6476654-ff7268e61e149d78.png │ ├── optimization.jpg │ ├── pic-plugins.jpg │ ├── pwa-2.jpg │ ├── pwa-3.jpg │ ├── pwa-5.jpg │ ├── pwa-8.jpg │ ├── pwa-logo.svg │ ├── pwa-pass-3.svg │ ├── qj6119915943.jpg │ ├── quick-start-a-react-spa │ ├── 6476654-218811ebec26b62a.png │ ├── 6476654-2a910fb0b5ca7659.png │ ├── 6476654-2dc20497ac24afce.png │ ├── 6476654-2dd97c02d08631d5.png │ ├── 6476654-31f02cf83192276e.png │ ├── 6476654-48a1dac919bf9f16.png │ ├── 6476654-a19220d421015fe2.png │ ├── 6476654-c1e748c23f8d83cd.png │ ├── 6476654-c63f5cdf69659c53.png │ └── 6476654-ceeeda7ba166a8fe.png │ ├── quote-what-i-cannot-create-i-do-not-understand-richard-feynman-228644.jpg │ ├── react-pic.jpg │ ├── react-router.jpg │ ├── request-timeout-after-finishing-in-nodejs │ ├── 01.png │ ├── 02.png │ ├── 03.png │ ├── 04.png │ ├── 05.png │ ├── 06.png │ ├── 07.png │ ├── 08.png │ └── request-timeout.jpeg │ ├── resource-hint.jpg │ ├── scaffold.jpg │ ├── set-selection-range │ ├── 6476654-444ef081d4722b1d.png │ └── 6476654-5c5b883ece246b51.png │ ├── styled-components.jpg │ ├── the-phoenix-project │ ├── chart.jpg │ └── cover.jpg │ ├── tips.jpg │ ├── troubleshooting-grpc-static-codegen │ ├── 0.jpeg │ ├── 1.png │ ├── 2.png │ ├── 3.png │ └── 4.png │ ├── troubleshooting-nodejs-dns-mem │ ├── 00.jpeg │ ├── 00.png │ ├── 01.png │ ├── 02.png │ ├── 03.png │ ├── 04.jpeg │ ├── 05.jpeg │ ├── 06.jpeg │ ├── 07.jpeg │ ├── 08.jpeg │ ├── 09.jpeg │ ├── 10.png │ ├── 11.jpeg │ ├── 12.jpeg │ └── 13.jpeg │ ├── troubleshooting-npm-script-root-auth │ ├── 0.png │ ├── 1.png │ └── 2.png │ ├── troubleshooting.jpg │ ├── web-highlighter.jpg │ ├── why-use-static-types-in-js │ ├── 1_IDG2HHn55BhiZk8KMADLsQ.png │ ├── 1_PFxhb9gct7GYWBlBY0lofg.png │ ├── 1_Z79CcJO6h4DO_xKJMdK9zg.png │ └── 1_iemrVKr16FMed25x6-bfBA.png │ └── xiaoyuanchai.png ├── themes └── minos │ ├── README.md │ ├── _config.yml │ ├── _config.yml.example │ ├── languages │ ├── en.yml │ └── zh-cn.yml │ ├── layout │ ├── archive.ejs │ ├── categories.ejs │ ├── category.ejs │ ├── comment │ │ ├── changyan.ejs │ │ ├── disqus.ejs │ │ ├── facebook.ejs │ │ ├── gitment.ejs │ │ ├── isso.ejs │ │ ├── livere.ejs │ │ ├── valine.ejs │ │ └── youyan.ejs │ ├── common │ │ ├── article.ejs │ │ ├── footer.ejs │ │ ├── head.ejs │ │ ├── languages.ejs │ │ ├── navbar.ejs │ │ ├── paginator.ejs │ │ └── scripts.ejs │ ├── index.ejs │ ├── layout.ejs │ ├── plugins │ │ ├── clipboard.ejs │ │ ├── gallery.ejs │ │ ├── google-analytics.ejs │ │ ├── katex.ejs │ │ └── mathjax.ejs │ ├── post.ejs │ ├── search │ │ ├── google-cse.ejs │ │ └── insight.ejs │ ├── share │ │ ├── addthis.ejs │ │ └── sharethis.ejs │ ├── tag.ejs │ └── tags.ejs │ ├── lib │ ├── i18n.js │ └── rfc5646.js │ ├── scripts │ ├── 01_check.js │ ├── 10_i18n.js │ ├── 99_config.js │ ├── 99_content.js │ └── 99_tags.js │ └── source │ ├── css │ ├── insight.scss │ └── style.scss │ ├── images │ ├── check.svg │ ├── exclamation.svg │ ├── info.svg │ ├── logo.png │ ├── question.svg │ └── quote-left.svg │ └── js │ ├── insight.js │ └── script.js └── tools ├── image_convert.js └── match.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public 3 | db.json 4 | **/.DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 个人博客 >> https://www.alienzhou.com/ 2 | 3 | 不定期发布一些技术学习的收获,主要聚焦于大前端领域,也会有一些其他内容。欢迎star 🌟 4 | 5 | # 目录 6 | 7 | 8 | 1. [最新更新](#) 9 | 2. [综合列表](#-1) 10 | 3. [开源项目学习](#-1) 11 | 4. [自动化工具](#-1) 12 | 5. [CSS 学习](#CSS) 13 | 6. [性能](#-1) 14 | 7. [Webpack进阶](#Webpack) 15 | 8. [CSS模块化](#CSS-1) 16 | 9. [PWA](#PWA) 17 | 10. [知识速览](#-1) 18 | 11. [阅读笔记](#reading) 19 | 12. [排障系列](#troubleshooting) 20 | 13. [Node.js](#nodejs) 21 | 22 | 26 | 27 | 28 | ## 1. 最新更新 29 | 30 | - [Node.js 中 HTTP 请求结束后,误报超时的问题排查](https://github.com/alienzhou/blog/issues/58)(2021.10.22) 31 | 32 | ## 2. 综合列表 33 | 34 | - [年终回顾,为你汇总一份「前端技术清单」](https://www.alienzhou.com/2018/11/13/frontend-tech-list/) 35 | - [✨如何实现一个通用的“划词高亮”在线笔记功能?✨🖍️](https://www.alienzhou.com/2019/04/21/web-note-highlight-in-js/) 36 | - [前端跨页面通信,你知道哪些方法?](https://www.alienzhou.com/2019/04/01/cross-tab-communication-in-frontend/) 37 | - [MongoDB高可用__使用Replica Set](https://www.alienzhou.com/2018/05/03/mongodb-replica-set/) 38 | - [各类“服务器推”技术原理与实例(Polling/COMET/SSE/WebSocket)](https://www.alienzhou.com/2018/06/08/server-push-methods/) 39 | - [如何在零JS代码情况下实现一个实时聊天功能❓](https://www.alienzhou.com/2019/05/20/css-only-chat/) 40 | 41 | ## 3. 开源项目学习 42 | 43 | - [【漫游Github】quicklink:实现原理与给前端的启发](https://www.alienzhou.com/2018/12/25/quicklink-implementation-and-gotcha/) 44 | - [【漫游Github】如何提升JSON.stringify()的性能?](https://www.alienzhou.com/2019/06/05/how-to-improve-json-stringify-performance/) 45 | - [【漫游Github】无编译/无服务器,实现浏览器的 CommonJS 模块化](https://www.alienzhou.com/2020/01/10/commonjs-without-build-and-server/) 46 | 47 | ## 4. 自动化工具 48 | 49 | - [vue-cli 迁移 vite2 实践小结](https://github.com/alienzhou/blog/issues/45) 50 | - [替代 webpack?带你了解 snowpack 原理](https://www.alienzhou.com/2020/06/18/how-snowpack-works/) 51 | - [Gulp.js实践详解__基于Gulp的多页面应用实践指南](https://www.alienzhou.com/2017/10/15/mpa-based-on-gulp-in-action/) 52 | - [🛠如何快速开发一个自己的项目脚手架?](https://www.alienzhou.com/2019/05/17/how-to-make-your-own-scaffold/) 53 | 54 | ## 5. CSS 学习 55 | 56 | - [一篇全面的CSS布局学习指南 [译]](https://www.alienzhou.com/2018/07/07/css-layout-guide/) 57 | - [(S)CSS中实现主题样式的4½种方式 [译]](https://www.alienzhou.com/2018/12/12/css-theming-methods/) 58 | 59 | ## 6. 性能 60 | 61 | - [【性能优化指南】带你全面掌握前端性能优化 🚀](https://www.alienzhou.com/2019/08/08/fe-performance-journey/) 62 | - [【性能优化实践】优化打包策略提升页面加载速度](https://www.alienzhou.com/2018/05/05/optimize-packing-to-speed-up/) 63 | 64 | ## 7. Webpack进阶 65 | 66 | - [聊一聊 webpack 的打包优化实践](https://www.alienzhou.com/2020/03/28/improvement-in-webpack/) 67 | - [【webpack进阶】使用babel避免webpack编译运行时模块依赖](https://www.alienzhou.com/2018/08/19/webpack-babel-transform/) 68 | - [【webpack进阶】前端运行时的模块化设计与实现](https://www.alienzhou.com/2018/08/27/webpack-module-runtime/) 69 | - [【webpack进阶】可视化展示webpack内部插件与钩子关系📈](https://www.alienzhou.com/2018/09/30/webpack-plugin-hooks-visualization/) 70 | - [【webpack进阶】你真的掌握了loader么?- loader十问](https://www.alienzhou.com/2018/10/14/webpack-loader-in-deep/) 71 | 72 | ## 8. CSS模块化 73 | 74 | - [【CSS模块化之路1】使用BEM与命名空间来规范CSS](https://www.alienzhou.com/2018/06/13/css-modular-guide-1/) 75 | - [【CSS模块化之路2】webpack中的Local Scope](https://www.alienzhou.com/2018/06/15/css-modular-guide-2/) 76 | - [【CSS模块化之路3】 使用💅styled-components来进行react开发](https://www.alienzhou.com/2018/06/15/css-modular-guide-3/) 77 | 78 | ## 9. PWA 79 | 80 | - [【PWA学习与实践】(1) 2018,开始你的PWA学习之旅](https://www.alienzhou.com/2018/04/07/pwa-1/) 81 | - [【PWA学习与实践】(2) 使用Manifest,让你的WebApp更“Native”](https://www.alienzhou.com/2018/04/07/pwa-2/) 82 | - [【PWA学习与实践】(3) 让你的WebApp离线可用](https://www.alienzhou.com/2018/04/08/pwa-3/) 83 | - [【PWA学习与实践】(4) 解决FireBase login验证失败问题](https://www.alienzhou.com/2018/04/10/pwa-4/) 84 | - [【PWA学习与实践】(5)在Web中进行服务端消息推送](https://www.alienzhou.com/2018/04/14/pwa-5/) 85 | - [【PWA学习与实践】(6) 在Chrome中调试你的PWA](https://www.alienzhou.com/2018/05/01/pwa-6/) 86 | - [【PWA学习与实践】(7)使用Notification API来进行消息提醒](https://www.alienzhou.com/2018/05/01/pwa-7/) 87 | - [【PWA学习与实践】(8)使用Service Worker进行后台同步 - Background Sync](https://www.alienzhou.com/2018/05/14/pwa-8/) 88 | - [【PWA学习与实践】(9)生产环境中PWA实践的问题与解决方案](https://www.alienzhou.com/2018/05/26/pwa-9/) 89 | - [【PWA学习与实践】(10)使用Resource Hint提升页面加载性能与体验](https://www.alienzhou.com/2018/07/23/pwa-10/) 90 | 91 | ## 10. 知识速览 92 | 93 | - [【3分钟速览】前端广播式通信:Broadcast Channel ](https://www.alienzhou.com/2019/04/01/broadcast-channel/) 94 | - [【3分钟速览】如何“严谨地”判断两个变量是否相同](https://www.alienzhou.com/2020/01/08/a-robust-equality-operation/) 95 | 96 | ## 11. 阅读笔记 97 | 98 | - [读《凤凰项目:一个 IT 运维的传奇故事》](https://www.alienzhou.com/2020/02/23/the-phoenix-project/) 99 | - [读《信息规则:网络经济的策略指导》](https://www.alienzhou.com/2020/03/02/a-strategic-guide-to-the-network-economy/) 100 | 101 | ## 12. 排障系列 102 | 103 | - [DNS 查询导致的 Nodejs 服务疑似“内存泄漏”问题](https://github.com/alienzhou/blog/issues/49) 104 | - [npm script 执行”丢失“ root 权限的问题](https://github.com/alienzhou/blog/issues/48) 105 | - [记一次 Node gRPC 静态生成文件引发的问题](https://github.com/alienzhou/blog/issues/47) 106 | - [Node.js 中 HTTP 请求结束后,误报超时的问题排查](https://github.com/alienzhou/blog/issues/58) 107 | 108 | ## 13. Node.js 109 | 110 | - [Nodejs 中的 Active Handle 与 Timer 优化(上)](https://github.com/alienzhou/blog/issues/54) 111 | - [Nodejs 中的 Active Handle 与 Timer 优化(下)](https://github.com/alienzhou/blog/issues/55) 112 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | # Hexo Configuration 2 | ## Docs: https://hexo.io/docs/configuration.html 3 | ## Source: https://github.com/hexojs/hexo/ 4 | 5 | # Site 6 | title: AlienZHOU 的个人站点 7 | subtitle: '' 8 | description: '个人博客,分享JavaScript、CSS、构建工具、性能优化等一系列工程技术,分享代码,分享技术,分享经验' 9 | keywords: '博客,前端,代码,技术' 10 | author: AlienZhou 11 | language: zh-cn 12 | timezone: 'Asia/Shanghai' 13 | 14 | # URL 15 | ## If your site is put in a subdirectory, set url as 'http://yoursite.com/child' and root as '/child/' 16 | url: https://www.alienzhou.com 17 | root: / 18 | permalink: :year/:month/:day/:title/ 19 | permalink_defaults: 20 | pretty_urls: 21 | trailing_index: true # Set to false to remove trailing 'index.html' from permalinks 22 | trailing_html: true # Set to false to remove trailing '.html' from permalinks 23 | 24 | # Directory 25 | source_dir: source 26 | public_dir: public 27 | tag_dir: tags 28 | archive_dir: archives 29 | category_dir: categories 30 | code_dir: downloads/code 31 | i18n_dir: :lang 32 | skip_render: 33 | 34 | # Writing 35 | new_post_name: :title.md # File name of new posts 36 | default_layout: post 37 | titlecase: false # Transform title into titlecase 38 | external_link: 39 | enable: true # Open external links in new tab 40 | field: site # Apply to the whole site 41 | exclude: '' 42 | filename_case: 0 43 | render_drafts: false 44 | post_asset_folder: false 45 | relative_link: false 46 | future: true 47 | highlight: 48 | enable: true 49 | line_number: true 50 | auto_detect: false 51 | tab_replace: '' 52 | wrap: true 53 | hljs: false 54 | 55 | # Home page setting 56 | # path: Root path for your blogs index page. (default = '') 57 | # per_page: Posts displayed per page. (0 = disable pagination) 58 | # order_by: Posts order. (Order by date descending by default) 59 | index_generator: 60 | path: '' 61 | per_page: 10 62 | order_by: -date 63 | 64 | # Category & Tag 65 | default_category: uncategorized 66 | category_map: 67 | tag_map: 68 | 69 | # Metadata elements 70 | ## https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta 71 | meta_generator: true 72 | 73 | # Date / Time format 74 | ## Hexo uses Moment.js to parse and display date 75 | ## You can customize the date format as defined in 76 | ## http://momentjs.com/docs/#/displaying/format/ 77 | date_format: YYYY-MM-DD 78 | time_format: HH:mm:ss 79 | ## Use post's date for updated date unless set in front-matter 80 | use_date_for_updated: false 81 | 82 | # Pagination 83 | ## Set per_page to 0 to disable pagination 84 | per_page: 10 85 | pagination_dir: page 86 | 87 | # Include / Exclude file(s) 88 | ## include:/exclude: options only apply to the 'source/' folder 89 | include: 90 | exclude: 91 | ignore: 92 | 93 | # Extensions 94 | ## Plugins: https://hexo.io/plugins/ 95 | ## Themes: https://hexo.io/themes/ 96 | theme: minos 97 | 98 | # Deployment 99 | ## Docs: https://hexo.io/docs/deployment.html 100 | deploy: 101 | type: '' 102 | 103 | # Navigation bar menu links. 104 | menu: 105 | 归档: /archives 106 | 分类: /tags 107 | 项目示例: /projects 108 | 关于我: /about 109 | 110 | baidu_tongji: 111 | hm: 64136edb1907ceabdc133e8d5d4566af 112 | 113 | # RSS Feed 114 | feed: 115 | type: rss2 116 | path: rss.xml 117 | limit: 20 -------------------------------------------------------------------------------- /config/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd blog 4 | git pull origin master 5 | npm run build 6 | 7 | cd .. 8 | cp -rf blog/public/* public 9 | -------------------------------------------------------------------------------- /config/nginx/conf.d/blog.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; 3 | server_name alienzhou.com *.alienzhou.com; 4 | keepalive_timeout 70; 5 | 6 | # HSTS策略 7 | add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; 8 | 9 | # ssl_certificate /home/work/SHA256withRSA_alienzhou/alienzhou.com.crt; 10 | # ssl_certificate_key /home/work/SHA256withRSA_alienzhou/alienzhou.com.key; 11 | 12 | ssl_certificate /home/work/SHA256withRSA_alienzhou/5041517_www.alienzhou.com.pem; 13 | ssl_certificate_key /home/work/SHA256withRSA_alienzhou/5041517_www.alienzhou.com.key; 14 | 15 | add_header X-Frame-Options SAMEORIGIN; 16 | add_header X-Content-Type-Options nosniff; 17 | add_header X-Xss-Protection 1; 18 | 19 | ssl on; 20 | etag on; 21 | default_type 'text/html'; 22 | charset utf-8; 23 | 24 | location / { 25 | # expires 1d; 26 | root /home/work/blog-source/public; 27 | } 28 | 29 | error_page 497 https://$host$uri?$args; 30 | } 31 | 32 | server { 33 | listen 80; 34 | server_name alienzhou.com *.alienzhou.com; 35 | rewrite ^(.*)$ https://$host$1 permanent; 36 | } -------------------------------------------------------------------------------- /config/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | user www-data; 2 | worker_processes auto; 3 | pid /run/nginx.pid; 4 | 5 | events { 6 | worker_connections 768; 7 | # multi_accept on; 8 | } 9 | 10 | http { 11 | 12 | ## 13 | # Basic Settings 14 | ## 15 | 16 | sendfile on; 17 | tcp_nopush on; 18 | tcp_nodelay on; 19 | keepalive_timeout 65; 20 | types_hash_max_size 2048; 21 | # server_tokens off; 22 | 23 | etag on; 24 | 25 | # server_names_hash_bucket_size 64; 26 | # server_name_in_redirect off; 27 | 28 | include /etc/nginx/mime.types; 29 | default_type application/octet-stream; 30 | 31 | # 配置共享会话缓存大小 32 | ssl_session_cache shared:SSL:10m; 33 | # 配置会话超时时间 34 | ssl_session_timeout 10m; 35 | 36 | ## 37 | # SSL Settings 38 | ## 39 | 40 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE 41 | ssl_prefer_server_ciphers on; 42 | 43 | ## 44 | # Logging Settings 45 | ## 46 | 47 | access_log /var/log/nginx/access.log; 48 | error_log /var/log/nginx/error.log; 49 | 50 | ## 51 | # Gzip Settings 52 | ## 53 | 54 | gzip on; 55 | gzip_disable "msie6"; 56 | 57 | # gzip_vary on; 58 | # gzip_proxied any; 59 | # gzip_comp_level 6; 60 | # gzip_buffers 16 8k; 61 | # gzip_http_version 1.1; 62 | # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; 63 | 64 | ## 65 | # Virtual Host Configs 66 | ## 67 | 68 | include /etc/nginx/conf.d/*.conf; 69 | include /etc/nginx/sites-enabled/*; 70 | } 71 | 72 | 73 | #mail { 74 | # # See sample authentication script at: 75 | # # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript 76 | # 77 | # # auth_http localhost/auth.php; 78 | # # pop3_capabilities "TOP" "USER"; 79 | # # imap_capabilities "IMAP4rev1" "UIDPLUS"; 80 | # 81 | # server { 82 | # listen localhost:110; 83 | # protocol pop3; 84 | # proxy on; 85 | # } 86 | # 87 | # server { 88 | # listen localhost:143; 89 | # protocol imap; 90 | # proxy on; 91 | # } 92 | #} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hexo-site", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "hexo generate", 7 | "watch": "hexo generate -w", 8 | "clean": "hexo clean", 9 | "deploy": "hexo deploy", 10 | "server": "hexo server" 11 | }, 12 | "hexo": { 13 | "version": "4.2.0" 14 | }, 15 | "dependencies": { 16 | "cheerio": "^1.0.0-rc.3", 17 | "file-type": "^14.0.0", 18 | "hexo": "^4.0.0", 19 | "hexo-generator-archive": "^1.0.0", 20 | "hexo-generator-category": "^1.0.0", 21 | "hexo-generator-feed": "^2.2.0", 22 | "hexo-generator-index": "^1.0.0", 23 | "hexo-generator-tag": "^1.0.0", 24 | "hexo-renderer-ejs": "^1.0.0", 25 | "hexo-renderer-marked": "^2.0.0", 26 | "hexo-renderer-sass": "^0.4.0", 27 | "hexo-renderer-stylus": "^1.1.0", 28 | "hexo-server": "^1.0.0", 29 | "sharp": "^0.24.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /scaffolds/draft.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: {{ title }} 3 | tags: 4 | --- 5 | -------------------------------------------------------------------------------- /scaffolds/page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: {{ title }} 3 | date: {{ date }} 4 | --- 5 | -------------------------------------------------------------------------------- /scaffolds/post.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: {{ title }} 3 | date: {{ date }} 4 | tags: 5 | --- 6 | -------------------------------------------------------------------------------- /source/_posts/a-robust-equality-operation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 如何“严谨地”判断两个变量是否相同 3 | date: 2020-01-08 12:00:00 4 | tags: 5 | - 3分钟速览 6 | - JavaScript 7 | --- 8 | 9 | ![](/img/16f8389a295640a6.jpg) 10 | 11 | 你知道如何“严谨地”判断两个变量相同么?仅仅使用 `===` 就可以了么? 12 | 13 | 14 | 15 | ## 严格相等 16 | 17 | 我们可以非常快的写一个 `is` 方法来判断变量 x 是否就是 y: 18 | 19 | ```JavaScript 20 | // 第一版 21 | function is(x, y) { 22 | return x == y; 23 | } 24 | ``` 25 | 26 | 当然,你会很快发现,方法里用了 `==`,由于[隐式转换](https://www.w3schools.com/js/js_type_conversion.asp)的问题,这并不严谨。所以我们自然会使用如下的方法: 27 | 28 | ```JavaScript 29 | // 第二版 30 | function is(x, y) { 31 | return x === y; 32 | } 33 | ``` 34 | 35 | 那么这是否完美了呢? 36 | 37 | ## 一个“更严谨”的方法 38 | 39 | ```JavaScript 40 | // 第三版 41 | function is(x, y) { 42 | if (x === y) { 43 | return x !== 0 || y !== 0 || 1 / x === 1 / y; 44 | } else { 45 | return x !== x && y !== y; 46 | } 47 | } 48 | ``` 49 | 50 | 上面方法相较于我们常用的第二版更复杂了。那么为什么多了这么多判断呢? 51 | 52 | 下面让我们来详细看看。 53 | 54 | ### 1. Infinity 55 | 56 | 了解 JavaScript 的同学应该会记得,在全局中有一个叫做 [`Infinity`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Infinity) 的属性,表示数值上的无穷大。 57 | 58 | | Infinity 属性的属性特性 | | 59 | |---|---| 60 | | writable | false | 61 | | enumerable | false | 62 | | configurable | false | 63 | 64 | 同时,你用 `Number.POSITIVE_INFINITY` 也能获取到该值。 65 | 66 | ![](/img/a-robust-equality-operation/16f6f132c0bec8ec.png) 67 | 68 | 于此对应的,也有个 `Number.NEGATIVE_INFINITY` 的值,实际就是 `-Infinity`。 69 | 70 | 而 `Infinity` 比较特殊的一点在于,在 JavaScript 中 `1 / Infinity` 与 `-1 / Infinity`。 被认为是相等的(由于 `+0` 和 `-0`,下一节会进一步介绍) 71 | 72 | ![](/img/a-robust-equality-operation/16f6f174c669e5f3.png) 73 | 74 | 而在很多场景中,包括像一些 deepEqual 之类的方法中,我们不希望将其判定为相等。学过统计的同学都知道[假设检验中有两类错误](https://en.wikipedia.org/wiki/False_positives_and_false_negatives): 75 | 76 | - I类错误:弃真错误(false positive) 77 | - II类错误:取伪错误(false negative) 78 | 79 | 结合我们上面提到的,第一个条件判断可能就会犯II类错误 —— `1 / Infinity` 与 `-1 / Infinity` 不相同,却判断为相同了。所以需要进一步判断: 80 | 81 | ```JavaScript 82 | x !== 0 || y !== 0 || 1 / x === 1 / y 83 | ``` 84 | 85 | `1 / Infinity` 与 `-1 / Infinity` 在与 `0` 的相等判断中都会为 `true` 86 | 87 | 88 | ![](/img/a-robust-equality-operation/16f6f2ddd2f954bc.png) 89 | 90 | 而其倒数 `Infinity` 与 `-Infinity` 是不相等的,所以避免了 `1 / Infinity` 与 `-1 / Infinity` 的判断问题。 91 | 92 | ### 2. `+0` 与 `-0` 93 | 94 | 其实,上面 `Infinity` 问题的核心原因在于于 JavaScript 中存在 `+0` 与 `-0`。 95 | 96 | 我们知道每个数字都有其对应的二进制编码形式,因此 `+0` 与 `-0` 编码是有区别的,平时我们不主动声明的话,所使用的其实都是 `+0`,而 JavaScript 为了我们的运算能更加方便,也做了很多额外工作。 97 | 98 | > 想要更进一步了解 `+0` 与 `-0` 可以读一下 [JavaScript’s two zeros](https://2ality.com/2012/03/signedzero.html) 这篇文章。 99 | 100 | 但在很多判断相等的工作上,我们还是会把 `+0` 与 `-0` 区分开。 101 | 102 | ```JavaScript 103 | x !== 0 || y !== 0 || 1 / x === 1 / y 104 | ``` 105 | 106 | 上面这个式子也就起到了这个作用。 107 | 108 | ### 3. `NaN` 109 | 110 | JavaScript 中还有一个叫 [`NaN`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/NaN) 全局属性,用来表示不是一个数字(Not-A-Number) 111 | 112 | | NaN 属性的属性特性 | | 113 | |---|---| 114 | | writable | false | 115 | | enumerable | false | 116 | | configurable | false | 117 | 118 | 它有一个特点 —— 自己不等于自己: 119 | 120 | ![](/img/a-robust-equality-operation/16f6f35688449594.png) 121 | 122 | 这可能会导致判断出现 I 类错误(弃真错误):原本是相同的,却被我们判断为不相同。 123 | 124 | 解决的方法也很简单,JavaScript 中只有 `NaN` 会有“自己不等于自己”的特点。所以只需要判断两个变量是否都“自己不等于自己”即可,即都为 `NaN` : 125 | 126 | ```JavaScript 127 | x !== x && y !== y 128 | ``` 129 | 130 | 如果两个变量都为 `NaN`,那么他们其实就还是相同的。 131 | 132 | ## 总结 133 | 134 | 总的来说,我们的加强版就是额外处理了 `+0`/`-0` 与 `NaN` 的情况。 135 | 136 | 实际项目中,很多时候由于并不会碰这样的业务值,或者这些边界情况的判断并不影响业务逻辑,所以使用 `===` 就足够了。 137 | 138 | 而在一些开源库中,由于需要更加严谨,所以很多时候就会考虑使用第三版的这类方法。例如在 [react-redux 中对 props 和 state 前后相等性判断](https://github.com/reduxjs/react-redux/blob/58ae5edee510a2f2f3bc577f55057fe9142f2976/src/utils/shallowEqual.js#L1-L7),[underscore 中的相等判断方法](https://github.com/jashkenas/underscore/blob/master/underscore.js#L1191-L1198)等。而 underscore 中更进一步还对 `null` 与 `undefined` 做了特殊处理。 139 | -------------------------------------------------------------------------------- /source/_posts/a-strategic-guide-to-the-network-economy.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 信息规则:网络经济的策略指导 3 | date: 2020-03-02 12:00:00 4 | tags: 5 | - 阅读笔记 6 | - 经济 7 | - IT 8 | --- 9 | 10 | ![](/img/a-strategic-guide-to-the-network-economy/cover.jpg) 11 | 12 | 今天的许多管理者“只见树木不见森林”,仅仅关注技术变革,而忽视决定生死的经济规律。如果你无法理解网络经济(network economy),那么如何能够理解信息时代(例如互联网)的商业运作模式呢? 13 | 14 | 15 | 16 | 记得读书时老师曾经推荐过一篇 Hal Varian 的微观经济学论文,文章很短,没有那种论文里面常见的“弯弯绕绕”的措辞,数学模型计算与推理简洁明了,看着就像是一份随堂作业一般的随笔之作。然而文章发在了经济学的顶刊上,不得不让人佩服,大师就是大师。 17 | 18 | 而这本[《信息规则》](https://book.douban.com/subject/27179558/)不是网上那种充斥着博人眼球的小知识点的“伪学术”书,也不是那种在学校图书馆里的枯燥的教课书。书中没有晦涩的专业知识,艰深的模型公式,而是通过一个个现实案例来对书中提到的论点进行解释和印证。让即使不具备经济学基础的读者也能很好理解其中的内容。 19 | 20 | --- 21 | 22 | 网络经济和以前的经济规律有区别么? 23 | 24 | **有也没有。** 25 | 26 | 之所一说有是在于,书中提到,作者们经常听到别人抱怨说经济学在现今的经济活动中已没什么用了。之后他们了解到:“抱怨针对的是大多数在校学习的古典经济学,其核心是供给需求曲线和完全竞争市场,比如农产品市场”。显然,针对当今信息经济的一些经济学研究是与传统古典经济学存在差异的。而正是有这些差异,才需要这么一本书来进行分析与归纳。 27 | 28 | 而说它没有,也是因为信息经济并非是一种横空出世、超脱于所有经济规律的“神物”。如果你仔细分析,会发现仍然有许多经济规律在其中运行,只是现在的限制条件、前提变了。书中经常会指出一些经济活动或形式背后的本质逻辑,并提供一些历史上的先例来告诉你 —— 它并不是什么新鲜事。例如数字化拷贝带来的更便宜的生产和分销机制,在历史上的图书馆、打印机、复印机也一样导致了这些变革。又例如在阐述“锁定”的章节,作者说:“摩擦并没有消失,它们只是变换了形式”。 29 | 30 | 所以我们更应该辨证地看待网络经济:既要认识到它的独特之处,学习新时代的“玩法”;也要避免“神话”这种现象,因为它也是符合经济学规律,很多历史先例更我们提供了一些蓝本。 31 | 32 | 书中信息还是比较丰富的,整理了如下的思维导图以供参考: 33 | 34 | ![](/img/a-strategic-guide-to-the-network-economy/mindmap.svg) 35 | -------------------------------------------------------------------------------- /source/_posts/broadcast-channel.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 前端广播式通信:Broadcast Channel 3 | date: 2019-04-01 12:00:00 4 | tags: 5 | - 3分钟速览 6 | - 浏览器 7 | - 特性 8 | --- 9 | 10 | ![](/img/broadcast.jpg) 11 | 12 | 前端如何实现广播式通信呢?Broadcast Channel 就是一个好方法。它会创建一个所有同源页面都可以共享的(广播)频道,因此其中某一个页面发送的消息可以被其他页面监听到。 13 | 14 | 15 | 16 | ## Broadcast Channel 是什么? 17 | 18 | 在前端,我们经常会用 `postMessage` 来实现页面间的通信,但这种方式更像是点对点的通信。对于一些需要广播(让所有页面知道)的消息,用 `postMessage` 不是非常自然。Broadcast Channel 就是用来弥补这个缺陷的。 19 | 20 | 顾名思义,Broadcast Channel 会创建一个所有同源页面都可以共享的(广播)频道,因此其中某一个页面发送的消息可以被其他页面监听到。 21 | 22 | 下面就来速览一下它的使用方法。 23 | 24 | ## 如何使用? 25 | 26 | Broadcast Channel 的 API 非常简单易用。 27 | 28 | ### 创建 29 | 30 | 首先我们会使用构造函数创建一个实例: 31 | 32 | ```JavaScript 33 | const bc = new BroadcastChannel('alienzhou'); 34 | ``` 35 | 36 | 可以接受一个 `DOMString` 作为 name,用以标识这个 channel 。在其他页面,可以通过传入相同的 name 来使用同一个广播频道。用 MDN 上的话来解释就是: 37 | 38 | > There is one single channel with this name for all browsing contexts with the same origin. 39 | 40 | 该 name 值可以通过实例的 `.name` 属性获得 41 | 42 | ```JavaScript 43 | console.log(bc.name); 44 | // alienzhou 45 | ``` 46 | 47 | ### 监听消息 48 | 49 | Broadcast Channel 创建完成后,就可以在页面监听广播的消息: 50 | 51 | ```JavaScript 52 | bc.onmessage = function(e) { 53 | console.log('receive:', e.data); 54 | }; 55 | ``` 56 | 57 | 对于错误也可以绑定监听: 58 | 59 | ```JavaScript 60 | bc.onmessageerror = function(e) { 61 | console.warn('error:', e); 62 | }; 63 | ``` 64 | 65 | > 除了为 `.onmessage` 赋值这种方式,也可以使用 `addEventListener` 来添加 `'message'` 监听。 66 | 67 | ### 发送消息 68 | 69 | Broadcast Channel 实例也有一个对应的 `postMessage` 用于发送消息: 70 | 71 | ```JavaScript 72 | bc.postMessage('hello'); 73 | ``` 74 | 75 | ### 关闭 76 | 77 | 可以看到,上述短短几行代码就可以实现多个页面间的广播通信,非常方便。而有时我们希望取消当前页面的广播监听: 78 | 79 | - 一种方式是取消或者修改相应的 `'message'` 事件监听 80 | - 另一种简单的方式就是使用 Broadcast Channel 实例为我们提供的 `close` 方法。 81 | 82 | ```JavaScript 83 | bc.close(); 84 | ``` 85 | 86 | 两者是有区别的: 87 | 88 | 取消 `'message'` 监听只是让页面不对广播消息进行响应,Broadcast Channel 仍然存在;而调用 `close` 方法这会切断与 Broadcast Channel 的连接,浏览器才能够尝试回收该对象,因为此时浏览器才会知道用户已经不需要使用广播频道了。 89 | 90 | 在关闭后调用 `postMessage` 会出现如下报错 91 | 92 | ![](/img/169d80b1620cdac9.png) 93 | 94 | 如果之后又再需要广播,则可以重新创建一个相同 name 的 Broadcast Channel。 95 | 96 | ## Demo 效果 97 | 98 | [可以戳这里查看在线 Demo >>](/projects/broadcast-channel) 99 | 100 | 下面是 Broadcast Channel Demo 的演示效果: 101 | 102 | ![](/img/169d8452cef80241.gif) 103 | 104 | ## 兼容性如何? 105 | 106 | Broadcast Channel 是一个非常好用的多页面消息同步 API,然而兼容性却**不是很乐观**。 107 | 108 | ![](/img/169d80efd65b5401.png) 109 | 110 | 好在我们还有些其他方案可以作为补充(或者作为 polyfill ),其他的前端跨页面通信可以参考我的另一篇文章[《前端跨页面通信的方法》](/2019/04/01/cross-tab-communication-in-frontend/)。 111 | -------------------------------------------------------------------------------- /source/_posts/css-only-chat.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 如何在零JS代码情况下实现实时聊天功能? 3 | date: 2019-05-20 12:00:00 4 | tags: 5 | - CSS 6 | - 综合 7 | --- 8 | 9 | ![](./img/css-chat.jpg) 10 | 11 | 在我们的印象里,实现一个简单的聊天应用(消息发送与多页面同步)并不困难 —— 这是在我们有 JavaScript 的帮助下。而如果让你只能使用 CSS,不能有前端的 JavaScript 代码,那你能够实现么? 12 | 13 | 14 | 15 | ## 引言 16 | 17 | 前段时间在 github 上看到了一个很“trick”的项目:用纯 CSS(即不使用 JavaScript)实现一个聊天应用 —— [css-only-chat](https://github.com/kkuchta/css-only-chat)。即下图所示效果。 18 | 19 | ![](/img/css-only-chat/16ad56d29a8af1c6.gif) 20 | 21 | > 原版是用 Ruby 写的后端。可能大家对 Ruby 不太了解,所以我按照原作者思路,用 NodeJS 实现了一版 [css-only-chat-node](https://github.com/alienzhou/css-only-chat-node),对大家来说可能会更易读些。 22 | 23 | ## 1. 我们要解决什么问题 24 | 25 | 首先强调一下,服务端的代码肯定还是需要写的,而且这部分显然不能是 CSS。所以这里的“纯 CSS”主要指在浏览器端只使用 CSS。 26 | 27 | 回忆一下,如果使用 JavaScript 来实现上图中展示的聊天功能,有哪些问题需要处理呢? 28 | 29 | - 首先,需要添加按钮的`click`事件监听,包括字符按钮的点击与发送按钮的点击; 30 | - 其次,点击相应按钮后,要将信息通过 Ajax 的方式发送到后端服务; 31 | - 再者,要实现实时的消息展示,一般会建立一个 WebSocket 连接; 32 | - 最后,对于后端同步来的消息,我们会在浏览器端操作 DOM API 来改变 DOM 内容,展示消息记录。 33 | 34 | 涉及到 JavaScript 的操作主要就是上面四个了。但是,现在我们只能使用 CSS,那对于上面这几个操作,可以用什么方式实现呢? 35 | 36 | ## 2. Trick Time 37 | 38 | ### 2.1. 解决“点击监听”的问题 39 | 40 | 使用 JavaScript 的话一行代码可以搞定: 41 | 42 | ```JavaScript 43 | document.getElementById('btn').addEventListener('click', function () { 44 | // …… 45 | }); 46 | ``` 47 | 48 | 使用 CSS 的话,其实有个伪类可以帮我们,即`:active`。它可以选择激活的元素,而当我们点击某个元素时,它就会处于激活状态。 49 | 50 | 所以,对于上面动图中的26个字母(再加上 send 按钮),可以分配不同的`classname`,然后设置伪类选择器,这样就可以在点击该字母对应的按钮时触发命中某个 CSS 规则。例如可以对字符“a”设置如下规则用于“捕获”点击: 51 | 52 | ```CSS 53 | .btn_a:active { 54 | /* …… */ 55 | } 56 | ``` 57 | 58 | ### 2.2. 发送请求 59 | 60 | 如果有 JavaScript 的帮助,发送请求只需要用个 XHR 即可,很方便。而对于 CSS,如果要想发一个请求的话有什么办法么? 61 | 62 | 可以使用`background-image`属性,将它指定为某个 URL,这样前端就会向服务器发起一个背景图片的请求。之所以可以使用`background-image`属性还因为:浏览器只有在该 CSS 选择器规则被实际应用到 DOM 元素后才会实际发起`background-image`的请求。例如下面这个规则: 63 | 64 | ```CSS 65 | .btn_a:active { 66 | background-image: url('/keys/a'); 67 | } 68 | ``` 69 | 70 | 只有在字符“a”被点击后,浏览器才会向服务器请求`/keys/a`这张“图片”。而在服务器端,通过判断 URL 可以知道前端点击了哪个字符。例如,对于按钮“b”会有如下规则: 71 | 72 | ```CSS 73 | .btn_b:active { 74 | background-image: url('/keys/b'); 75 | } 76 | ``` 77 | 78 | 这样就相当于实现了在 URL(`/keys/a`与`/keys/b`) 中“传参”。 79 | 80 | ### 2.3. 实时消息展示 81 | 82 | 实时的消息展示,核心会用到一种叫“服务器推”的技术。其中比较常见方式有: 83 | 84 | - 使用 JavaScript 来和服务端建立 WebSocket 连接 85 | - 使用 JavaScript 创建定时器,定时发送请求轮询 86 | - 使用 JavaScript 和服务端配合来实现长轮询 87 | 88 | 但这些方法都无法规避 JavaScript,显然不符合咱们的要求。其实还有一种方式,我在[《各类“服务器推”技术原理与实例》](/2018/06/08/server-push-methods/)中也有提到,那就是基于 iframe 的长连接流(stream)模式。 89 | 90 | 这里我们主要是借鉴了“长连接流”这种模式。让我们的页面永远处于一个未加载完成的状态。但是,由于请求头中包含`Transfer-Encoding: chunked`,它会告诉浏览器,虽然页面没有返回结束,但你可以开始渲染页面了。正是由于该请求的响应永远不会结束,所以我们可以不断向其中写入新的内容,来更新页面展示。 91 | 92 | 实现起来也非常简单。`http.ServerResponse`类本身就是继承自`Stream`的,所以只要在需要更新页面内容时调用`.write()`方法即可。例如下面这段代码,可以每隔2s在页面上动态添加 "hello" 字符串而不需要任何浏览器端的配合(也就不需要写 JavaScript 代码了): 93 | 94 | ```JavaScript 95 | const http = require('http'); 96 | http.createServer((req, res) => { 97 | res.setHeader('connection', 'keep-alive'); 98 | res.setHeader('content-type', 'text/html; charset=utf-8'); 99 | res.statusCode = 200; 100 | res.write('I will update by myself'); 101 | 102 | setInterval(() => res.write('
hello'), 2000); 103 | }).listen(8085); 104 | ``` 105 | 106 | ![](/img/css-only-chat/16ad547da08e7d73.gif) 107 | 108 | ### 2.4. 改变页面信息 109 | 110 | 在上一节我们已经可以通过 Stream 的方式,不借助 JavaScript 即可动态改变页面内容了。但是如果你细心会发现,这种方式只能不断“append”内容。而在我们的例子中,看起来更像是能够动态改变某个 DOM 中的文本,例如随着点击不同按钮,“Current Message”后面的文本会不断变化。 111 | 112 | 这里其实也有个很“trick”的方式。下图这个部分(我们姑且叫它 ChatPanel 吧) 113 | 114 | ![](/img/css-only-chat/16ad543f357344b1.png) 115 | 116 | 其实我们每次调用`res.write()`时都会返回一个全新的 ChatPanel 的 HTML 片段。于此同时,还会附带一个` 35 | 64 | <% } %> -------------------------------------------------------------------------------- /themes/minos/layout/plugins/gallery.ejs: -------------------------------------------------------------------------------- 1 | <% if (!head && !(has_config('plugins.gallery') && get_config('plugins.gallery') === false)) { %> 2 | 3 | 4 | 16 | <% } %> -------------------------------------------------------------------------------- /themes/minos/layout/plugins/google-analytics.ejs: -------------------------------------------------------------------------------- 1 | <% if (head && has_config('plugins.google-analytics.tracking_id')) { %> 2 | 3 | 10 | <% } %> 11 | -------------------------------------------------------------------------------- /themes/minos/layout/plugins/katex.ejs: -------------------------------------------------------------------------------- 1 | <% if (!head && !(has_config('plugins.katex') && get_config('plugins.katex') === false)) { %> 2 | 3 | 4 | 6 | 7 | 14 | <% } %> -------------------------------------------------------------------------------- /themes/minos/layout/plugins/mathjax.ejs: -------------------------------------------------------------------------------- 1 | <% if (!head && !(has_config('plugins.mathjax') && get_config('plugins.mathjax') === false)) { %> 2 | 3 | 22 | <% } %> -------------------------------------------------------------------------------- /themes/minos/layout/post.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%- partial('common/article', { post: page, index: false }) %> 4 |
5 |
-------------------------------------------------------------------------------- /themes/minos/layout/search/google-cse.ejs: -------------------------------------------------------------------------------- 1 | 32 | -------------------------------------------------------------------------------- /themes/minos/layout/search/insight.ejs: -------------------------------------------------------------------------------- 1 | 13 | 28 | <%- js('js/insight') %> -------------------------------------------------------------------------------- /themes/minos/layout/share/addthis.ejs: -------------------------------------------------------------------------------- 1 | <% if (!has_config('share.install_url')) { %> 2 |
3 | You need to set install_url to use AddThis. Please set it in _config.yml. 4 |
5 | <% } else { %> 6 |
7 | 8 | <% } %> -------------------------------------------------------------------------------- /themes/minos/layout/share/sharethis.ejs: -------------------------------------------------------------------------------- 1 | <% if (!has_config('share.install_url')) { %> 2 |
3 | You need to set install_url to use ShareThis. Please set it in _config.yml. 4 |
5 | <% } else { %> 6 |
7 | 8 | <% } %> -------------------------------------------------------------------------------- /themes/minos/layout/tag.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
#<%= page.tag %>
5 |
6 |
7 |
8 | <%- partial('index', { page }) %> -------------------------------------------------------------------------------- /themes/minos/layout/tags.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
#<%= __('common.tags') %>
5 |
6 |
7 |
8 |
9 |
10 |
11 | <% (page._tags || page.tags).forEach(tag => {%> 12 | 13 | 14 | 15 | <% }) %> 16 |
17 |
18 |
-------------------------------------------------------------------------------- /themes/minos/lib/i18n.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const util = require('hexo-util'); 3 | const RFC5646_TAGS = require('./rfc5646'); 4 | 5 | const Pattern = util.Pattern; 6 | 7 | module.exports = function (hexo) { 8 | function pathJoin(...paths) { 9 | return paths.join('/'); 10 | } 11 | 12 | function formatRfc5646(language) { 13 | if (!language) { 14 | return ''; 15 | } 16 | return language.split(/[-_]/).map((l, i) => i === 0 ? l.toLowerCase() : l.toUpperCase()).join('-'); 17 | } 18 | 19 | function formatIso639(language) { 20 | if (!language) { 21 | return ''; 22 | } 23 | return language.split(/[-_]/)[0].toLowerCase(); 24 | } 25 | 26 | function getClosestRfc5646WithCountryCode(language) { 27 | if (!language) { 28 | return ''; 29 | } 30 | const iso639 = formatRfc5646(language).split('-')[0]; 31 | const result = Object.keys(RFC5646_TAGS).find(tag => tag.startsWith(iso639 + '-')); 32 | return result ? result : iso639; 33 | } 34 | 35 | function getUsedLanguages() { 36 | return hexo.theme.i18n.list(); 37 | } 38 | 39 | function getDisplayLanguages() { 40 | let languages = hexo.config.language; 41 | if (!languages) { 42 | return ['default']; 43 | } 44 | languages = [].concat(hexo.config.language); 45 | if (!Array.isArray(languages)) { 46 | languages = [languages]; 47 | } 48 | if (languages.indexOf('default') > -1) { 49 | languages.splice(languages.indexOf('default'), 1); 50 | } 51 | return languages; 52 | } 53 | 54 | function isLanguageValid(language) { 55 | const variants = [language, formatRfc5646(language)]; 56 | return variants.some(variant => RFC5646_TAGS.hasOwnProperty(variant)); 57 | } 58 | 59 | function injectLanguages(func) { 60 | return function(locals) { 61 | return func.call(this, getDisplayLanguages(), locals); 62 | } 63 | } 64 | 65 | function getPageLanguage(post) { 66 | const languages = getUsedLanguages(); 67 | let lang = post.lang || post.language; 68 | if (!lang && post.source) { 69 | const path = post.source.startsWith('_posts/') ? post.source.slice('_posts/'.length) : post.source; 70 | const pattern = new Pattern(`${hexo.config.i18n_dir}/*path`); 71 | const data = pattern.match(path); 72 | 73 | if (data && data.lang && ~languages.indexOf(data.lang)) { 74 | lang = data.lang; 75 | } 76 | } 77 | return lang; 78 | } 79 | 80 | function isDefaultLanguage(language) { 81 | return !language || getDisplayLanguages().indexOf(language) === 0; 82 | } 83 | 84 | function postFilter(language) { 85 | return function (post) { 86 | let lang = getPageLanguage(post); 87 | return (lang === language || (isDefaultLanguage(language) && !lang)) && (post.indexing !== false); 88 | } 89 | } 90 | 91 | function url_for(path) { 92 | return hexo.extend.helper.get('url_for').call(hexo, path); 93 | } 94 | 95 | return { 96 | pathJoin, 97 | isDefaultLanguage, 98 | url_for, 99 | postFilter, 100 | injectLanguages, 101 | getUsedLanguages, 102 | getDisplayLanguages, 103 | getPageLanguage, 104 | isLanguageValid, 105 | formatRfc5646, 106 | formatIso639, 107 | getClosestRfc5646WithCountryCode 108 | }; 109 | }; -------------------------------------------------------------------------------- /themes/minos/scripts/01_check.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const logger = require('hexo-log')(); 4 | 5 | logger.info(`======================================= 6 | ███╗ ███╗ ██╗ ███╗ ██╗ ██████╗ ███████╗ 7 | ████╗ ████║ ██║ ████╗ ██║ ██╔═══██╗ ██╔════╝ 8 | ██╔████╔██║ ██║ ██╔██╗ ██║ ██║ ██║ ███████╗ 9 | ██║╚██╔╝██║ ██║ ██║╚██╗██║ ██║ ██║ ╚════██║ 10 | ██║ ╚═╝ ██║ ██║ ██║ ╚████║ ╚██████╔╝ ███████║ 11 | ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ 12 | =============================================`); 13 | 14 | function checkDependency(name) { 15 | try { 16 | require.resolve(name); 17 | return true; 18 | } catch(e) { 19 | logger.error(`Package ${name} is not installed.`) 20 | } 21 | return false; 22 | } 23 | 24 | logger.info('Checking dependencies'); 25 | const missingDeps = [ 26 | 'moment', 27 | 'lodash', 28 | 'cheerio', 29 | 'js-yaml', 30 | 'highlight.js', 31 | 'hexo-util', 32 | 'hexo-generator-archive', 33 | 'hexo-generator-category', 34 | 'hexo-generator-index', 35 | 'hexo-generator-tag', 36 | 'hexo-renderer-ejs', 37 | 'hexo-renderer-marked', 38 | 'hexo-renderer-sass', 39 | ].map(checkDependency).some(installed => !installed); 40 | if (missingDeps) { 41 | logger.error('Please install the missing dependencies in the root directory of your Hexo site.'); 42 | process.exit(-1); 43 | } 44 | 45 | const themeRoot = path.join(__dirname, '..'); 46 | const mainConfigPath = path.join(themeRoot, '_config.yml'); 47 | 48 | logger.info('Checking if the configuration file exists'); 49 | if (!fs.existsSync(mainConfigPath)) { 50 | logger.warn(`${mainConfigPath} is not found. Please create one from the template _config.yml.example.`) 51 | } 52 | 53 | const { getUsedLanguages, getDisplayLanguages, isLanguageValid } = require('../lib/i18n')(hexo); 54 | 55 | logger.info('Checking language names against RFC5646 specs'); 56 | const invalidLanguages = getUsedLanguages().filter(language => !isLanguageValid(language)); 57 | if (invalidLanguages.length > 0) { 58 | logger.warn(`Language ${invalidLanguages} indicated by some posts is not a valid RFC5646 language.`) 59 | } 60 | const invalidDisplayLanguages = getDisplayLanguages().filter(language => !isLanguageValid(language)); 61 | if (invalidDisplayLanguages.length > 0) { 62 | logger.warn(`Language ${invalidDisplayLanguages} set in the configuration file is not a valid RFC5646 language.`) 63 | } 64 | -------------------------------------------------------------------------------- /themes/minos/scripts/99_config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const yaml = require('js-yaml'); 4 | 5 | const altConfigs = {}; 6 | const themeRoot = path.join(__dirname, '..'); 7 | 8 | /** 9 | * Theme configuration helper. 10 | */ 11 | function getConfig(config, path) { 12 | const paths = path.split('.'); 13 | for (let path of paths) { 14 | if (typeof(config) === 'undefined' || config === null || !config.hasOwnProperty(path)) { 15 | return null; 16 | } 17 | config = config[path]; 18 | } 19 | return config; 20 | } 21 | 22 | /** 23 | * Get alternative theme config file by page language 24 | * 25 | * @param lang page language 26 | * @returns Object merged theme config 27 | */ 28 | function getThemeConfig(lang = null) { 29 | if (lang) { 30 | if (!altConfigs.hasOwnProperty(lang)) { 31 | const configPath = path.join(themeRoot, '_config.' + lang + '.yml'); 32 | if (fs.existsSync(configPath)) { 33 | const config = yaml.safeLoad(fs.readFileSync(configPath)); 34 | if (config != null) { 35 | altConfigs[lang] = config; 36 | } 37 | } 38 | } 39 | if (altConfigs.hasOwnProperty(lang) && altConfigs[lang]) { 40 | return Object.assign({}, hexo.theme.config, altConfigs[lang]); 41 | } 42 | } 43 | return hexo.theme.config; 44 | } 45 | 46 | hexo.extend.helper.register('has_config', function (configName, excludePage = false) { 47 | return getConfig(Object.assign({}, 48 | this.config, 49 | getThemeConfig(this.page.lang), 50 | !excludePage ? this.page : {}), configName) !== null; 51 | }); 52 | 53 | hexo.extend.helper.register('get_config', function (configName, defaultValue = null, excludePage = false) { 54 | let config = getConfig(Object.assign({}, 55 | this.config, 56 | getThemeConfig(this.page.lang), 57 | !excludePage ? this.page : {}), configName); 58 | return config === null ? defaultValue : config; 59 | }); -------------------------------------------------------------------------------- /themes/minos/scripts/99_content.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const moment = require('moment'); 3 | const cheerio = require('cheerio'); 4 | const { formatRfc5646, formatIso639, getClosestRfc5646WithCountryCode, getPageLanguage } = require('../lib/i18n')(hexo); 5 | 6 | const MOMENTJS_SUPPORTED_LANGUAGES = ['af', 'ar-dz', 'ar-kw', 'ar-ly', 'ar-ma', 'ar-sa', 7 | 'ar-tn', 'ar', 'az', 'be', 'bg', 'bm', 'bn', 'bo', 'br', 'bs', 'ca', 'cs', 'cv', 'cy', 8 | 'da', 'de-at', 'de-ch', 'de', 'dv', 'el', 'en-au', 'en-ca', 'en-gb', 'en-ie', 'en-il', 9 | 'en-nz', 'eo', 'es-do', 'es-us', 'es', 'et', 'eu', 'fa', 'fi', 'fo', 'fr-ca', 'fr-ch', 10 | 'fr', 'fy', 'gd', 'gl', 'gom-latn', 'gu', 'he', 'hi', 'hr', 'hu', 'hy-am', 'id', 'is', 11 | 'it', 'ja', 'jv', 'ka', 'kk', 'km', 'kn', 'ko', 'ky', 'lb', 'lo', 'lt', 'lv', 'me', 12 | 'mi', 'mk', 'ml', 'mn', 'mr', 'ms-my', 'ms', 'mt', 'my', 'nb', 'ne', 'nl-be', 'nl', 13 | 'nn', 'pa-in', 'pl', 'pt-br', 'pt', 'ro', 'ru', 'sd', 'se', 'si', 'sk', 'sl', 'sq', 14 | 'sr-cyrl', 'sr', 'ss', 'sv', 'sw', 'ta', 'te', 'tet', 'tg', 'th', 'tl-ph', 'tlh', 'tr', 15 | 'tzl', 'tzm-latn', 'tzm', 'ug-cn', 'uk', 'ur', 'uz-latn', 'uz', 'vi', 'x-pseudo', 'yo', 16 | 'zh-cn', 'zh-hk', 'zh-tw']; 17 | 18 | function getMomentLocale(language) { 19 | let locale = formatRfc5646(language); 20 | if (MOMENTJS_SUPPORTED_LANGUAGES.indexOf(locale) === -1) { 21 | if (MOMENTJS_SUPPORTED_LANGUAGES.indexOf(formatIso639(locale)) > -1) { 22 | locale = formatIso639(locale); 23 | } else if (MOMENTJS_SUPPORTED_LANGUAGES.indexOf(getClosestRfc5646WithCountryCode(locale).toLowerCase()) > -1) { 24 | locale = getClosestRfc5646WithCountryCode(locale); 25 | } 26 | } 27 | return locale; 28 | } 29 | 30 | function injectMomentLocale(func) { 31 | return function () { 32 | let language = getMomentLocale(getPageLanguage(this.page)); 33 | moment.locale(language); 34 | const args = Array.prototype.slice.call(arguments).map(arg => { 35 | if (arg instanceof moment) { 36 | return moment(arg).locale(language); 37 | } 38 | return arg; 39 | }); 40 | return func.apply(this, args); 41 | } 42 | } 43 | 44 | hexo.extend.helper.register('is_categories', function () { 45 | return this.page.__categories; 46 | }); 47 | 48 | hexo.extend.helper.register('is_tags', function () { 49 | return this.page.__tags; 50 | }); 51 | 52 | /** 53 | * Generate html head title based on page type 54 | */ 55 | hexo.extend.helper.register('page_title', function () { 56 | const page = this.page; 57 | let title = page.title; 58 | 59 | if (this.is_archive()) { 60 | title = this.__('common.archives'); 61 | if (this.is_month()) { 62 | title += ': ' + page.year + '/' + page.month; 63 | } else if (this.is_year()) { 64 | title += ': ' + page.year; 65 | } 66 | } else if (this.is_category()) { 67 | title = this.__('common.category') + ': ' + page.category; 68 | } else if (this.is_tag()) { 69 | title = this.__('common.tag') + ': ' + page.tag; 70 | } else if (this.is_categories()) { 71 | title = this.__('common.categories'); 72 | } else if (this.is_tags()) { 73 | title = this.__('common.tags'); 74 | } 75 | 76 | const getConfig = hexo.extend.helper.get('get_config').bind(this); 77 | 78 | return [title, getConfig('title', '', true)].filter(str => typeof (str) !== 'undefined' && str.trim() !== '').join(' - '); 79 | }); 80 | 81 | /** 82 | * Format date to string without year. 83 | */ 84 | hexo.extend.helper.register('format_date', injectMomentLocale(function (date) { 85 | return moment(date).format('MMM D'); 86 | })); 87 | 88 | /** 89 | * Format date to string with year. 90 | */ 91 | hexo.extend.helper.register('format_date_full', injectMomentLocale(function (date) { 92 | return moment(date).format('MMM D YYYY'); 93 | })); 94 | 95 | /** 96 | * Get moment.js supported page locale 97 | */ 98 | hexo.extend.helper.register('momentjs_locale', function () { 99 | return getMomentLocale(getPageLanguage(this.page)); 100 | }); 101 | 102 | /** 103 | * Export moment.duration 104 | */ 105 | hexo.extend.helper.register('duration', injectMomentLocale(function () { 106 | return moment.duration.apply(null, arguments); 107 | })); 108 | 109 | /** 110 | * Get the word count of a paragraph. 111 | */ 112 | hexo.extend.helper.register('word_count', (content) => { 113 | content = content.replace(/<\/?[a-z][^>]*>/gi, ''); 114 | content = content.trim(); 115 | return content ? (content.match(/[\u00ff-\uffff]|[a-zA-Z]+/g) || []).length : 0; 116 | }); 117 | 118 | /** 119 | * Export a list of headings of an article 120 | * [ 121 | * ['1', 'heading-anchor-1', 'Title of the heading 1', 1], 122 | * ['1.1', 'heading-anchor-1-1', 'Title of the heading 1.1', 2], 123 | * ] 124 | */ 125 | hexo.extend.helper.register('toc_list', (content) => { 126 | const $ = cheerio.load(content, { decodeEntities: false }); 127 | const levels = [0, 0, 0]; 128 | const levelTags = []; 129 | // Get top 3 headings 130 | for (let i = 1; i <= 6; i++) { 131 | if ($('h' + i).length > 0) { 132 | levelTags.push('h' + i); 133 | } 134 | if (levelTags.length === 3) { 135 | break; 136 | } 137 | } 138 | const tocList = []; 139 | if (levelTags.length === 0) { 140 | return tocList; 141 | } 142 | const headings = $(levelTags.join(',')); 143 | headings.each(function () { 144 | const level = levelTags.indexOf(this.name); 145 | const id = $(this).attr('id'); 146 | const text = _.escape($(this).text()); 147 | 148 | for (let i = 0; i < levels.length; i++) { 149 | if (i > level) { 150 | levels[i] = 0; 151 | } else if (i < level) { 152 | // if headings start with a lower level heading, set the former heading index to 1 153 | // e.g. h3, h2, h1, h2, h3 => 1.1.1, 1.2, 2, 2.1, 2.1.1 154 | if (levels[i] === 0) { 155 | levels[i] = 1; 156 | } 157 | } else { 158 | levels[i] += 1; 159 | } 160 | } 161 | tocList.push([levels.slice(0, level + 1).join('.'), id, text, level + 1]); 162 | }); 163 | return tocList; 164 | }); 165 | -------------------------------------------------------------------------------- /themes/minos/scripts/99_tags.js: -------------------------------------------------------------------------------- 1 | const cheerio = require('cheerio'); 2 | 3 | /** 4 | * Colored quote block 5 | */ 6 | hexo.extend.tag.register('colorquote', function (args, content) { 7 | var type = args[0]; 8 | return '
' + hexo.render.renderSync({ text: content, engine: 'markdown' }) + '
'; 9 | }, { ends: true }); 10 | 11 | const rEscapeContent = /]*)>([\s\S]*?)<\/escape>/g; 12 | const rPlaceholder = /(?:<|<)epacse(?:[^>]*)(?:>|>)(\d+)(?:<|<)\/epacse(?:[^>]*)(?:>|>)/g; 13 | const cache = []; 14 | function escapeContent(str) { 15 | return ''; 16 | } 17 | 18 | hexo.extend.filter.register('before_post_render', function (data) { 19 | data.content = data.content.replace(rEscapeContent, function (match, content) { 20 | return escapeContent(content); 21 | }); 22 | return data; 23 | }); 24 | 25 | hexo.extend.filter.register('after_post_render', function (data) { 26 | data.content = data.content.replace(rPlaceholder, function () { 27 | return cache[arguments[1]]; 28 | }); 29 | return data; 30 | }); 31 | 32 | function patchCodeHighlight(content) { 33 | content = content.replace(/</g, '%&-l-t%'); 34 | content = content.replace(/>/g, '%&-g-t%'); 35 | const $ = cheerio.load(content, { decodeEntities: false }); 36 | $('figure.highlight').addClass('hljs'); 37 | $('figure.highlight .code .line span').each(function () { 38 | const classes = $(this).attr('class').split(' '); 39 | if (classes.length === 1) { 40 | $(this).addClass('hljs-' + classes[0]); 41 | $(this).removeClass(classes[0]); 42 | } 43 | }); 44 | content = $.html(); 45 | content = content.replace(/%&-l-t%/g, '<'); 46 | content = content.replace(/%&-g-t%/g, '>'); 47 | 48 | return content; 49 | } 50 | 51 | /** 52 | * Add .hljs class name to the code blocks and code elements. 53 | * Note: must be put after the above escape patch (hexojs/hexo#2400) 54 | */ 55 | hexo.extend.filter.register('after_post_render', function (data) { 56 | data.content = data.content ? patchCodeHighlight(data.content) : data.content; 57 | data.excerpt = data.excerpt ? patchCodeHighlight(data.excerpt) : data.excerpt; 58 | return data; 59 | }); 60 | -------------------------------------------------------------------------------- /themes/minos/source/css/insight.scss: -------------------------------------------------------------------------------- 1 | .ins-section-container { 2 | position: relative; 3 | background: #f7f7f7; 4 | } 5 | 6 | .ins-section { 7 | font-size: 14px; 8 | line-height: 16px; 9 | } 10 | 11 | .ins-section .ins-section-header, 12 | .ins-section .ins-search-item { 13 | padding: 8px 15px; 14 | } 15 | 16 | .ins-section .ins-section-header { 17 | color: #9a9a9a; 18 | border-bottom: 1px solid #e2e2e2; 19 | } 20 | 21 | .ins-section .ins-slug { 22 | margin-left: 5px; 23 | color: #9a9a9a; 24 | } 25 | 26 | .ins-section .ins-slug:before { 27 | content: '('; 28 | } 29 | 30 | .ins-section .ins-slug:after { 31 | content: ')'; 32 | } 33 | 34 | .ins-section .ins-search-item header, 35 | .ins-section .ins-search-item .ins-search-preview { 36 | overflow: hidden; 37 | white-space: nowrap; 38 | text-overflow: ellipsis; 39 | } 40 | 41 | .ins-section .ins-search-item header .ins-title { 42 | margin-left: 8px; 43 | } 44 | 45 | .ins-section .ins-search-item .ins-search-preview { 46 | height: 15px; 47 | font-size: 12px; 48 | color: #9a9a9a; 49 | margin: 5px 0 0 20px; 50 | } 51 | 52 | .ins-section .ins-search-item:hover, 53 | .ins-section .ins-search-item.active { 54 | color: #fff; 55 | background: #3273dc; 56 | } 57 | 58 | .ins-section .ins-search-item:hover .ins-slug, 59 | .ins-section .ins-search-item.active .ins-slug, 60 | .ins-section .ins-search-item:hover .ins-search-preview, 61 | .ins-section .ins-search-item.active .ins-search-preview { 62 | color: #fff; 63 | } -------------------------------------------------------------------------------- /themes/minos/source/images/check.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /themes/minos/source/images/exclamation.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /themes/minos/source/images/info.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /themes/minos/source/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienzhou/blog/19401b865b09e1cca59fcf3a5f55a1103f13aaab/themes/minos/source/images/logo.png -------------------------------------------------------------------------------- /themes/minos/source/images/question.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /themes/minos/source/images/quote-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /themes/minos/source/js/script.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | $('.navbar-burger').click(function () { 3 | $(this).toggleClass('is-active'); 4 | $('.navbar-main .navbar-start').toggleClass('is-active'); 5 | $('.navbar-main .navbar-end').toggleClass('is-active'); 6 | }); 7 | 8 | // Hide Header on on scroll down 9 | var didScroll; 10 | var lastScrollTop = 0; 11 | var delta = 5; 12 | var navbarHeight = $('.navbar-main').outerHeight(); 13 | 14 | $(window).scroll(function(event){ 15 | didScroll = true; 16 | }); 17 | 18 | setInterval(function() { 19 | if (didScroll) { 20 | hasScrolled(); 21 | didScroll = false; 22 | } 23 | }, 250); 24 | 25 | function hasScrolled() { 26 | var st = $(this).scrollTop(); 27 | 28 | // Make sure they scroll more than delta 29 | if(Math.abs(lastScrollTop - st) <= delta) { 30 | return; 31 | } 32 | 33 | // If they scrolled down and are past the navbar, add class .navbar-down. 34 | // This is necessary so you never see what is "behind" the navbar. 35 | if (st > lastScrollTop && st > navbarHeight) { 36 | var posY = Math.min(st, navbarHeight); 37 | // Scroll Down 38 | $('.navbar-main').css({ 39 | '-webkit-transform' : 'translateY(-' + posY + 'px)', 40 | '-moz-transform' : 'translateY(-' + posY + 'px)', 41 | '-ms-transform' : 'translateY(-' + posY + 'px)', 42 | '-o-transform' : 'translateY(-' + posY + 'px)', 43 | 'transform' : 'translateY(-' + posY + 'px)' 44 | }); 45 | } else { 46 | // Scroll Up 47 | if(st + $(window).height() < $(document).height()) { 48 | $('.navbar-main').css({ 49 | '-webkit-transform' : 'translateY(0px)', 50 | '-moz-transform' : 'translateY(0px)', 51 | '-ms-transform' : 'translateY(0px)', 52 | '-o-transform' : 'translateY(0px)', 53 | 'transform' : 'translateY(0px)' 54 | }); 55 | } 56 | } 57 | 58 | lastScrollTop = st; 59 | } 60 | 61 | $('.article.gallery img:not(".not-gallery-item")').each(function () { 62 | // wrap images with link and add caption if possible 63 | if ($(this).parent('a').length === 0) { 64 | $(this).wrap(''); 65 | if (this.alt) { 66 | $(this).after('
' + this.alt + '
'); 67 | } 68 | } 69 | }); 70 | 71 | $('.article-entry').find('h1, h2, h3, h4, h5, h6').on('click', function () { 72 | if ($(this).get(0).id) { 73 | window.location.hash = $(this).get(0).id; 74 | } 75 | }); 76 | 77 | if (typeof(moment) === 'function') { 78 | $('.article-meta time').each(function () { 79 | $(this).text(moment($(this).attr('datetime')).fromNow()); 80 | }); 81 | } 82 | })(jQuery); -------------------------------------------------------------------------------- /tools/image_convert.js: -------------------------------------------------------------------------------- 1 | const FileType = require('file-type'); 2 | const sharp = require('sharp'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | (async () => { 7 | const imgDir = path.resolve(__dirname, '..', 'source', 'img', 'why-use-static-types-in-js'); 8 | const fileList = fs.readdirSync(imgDir); 9 | fileList.forEach(async (file) => { 10 | if (/.*\.png/.test(file)) { 11 | const filePath = path.resolve(imgDir, file); 12 | const meta = await FileType.fromFile(filePath); 13 | console.log(file, meta); 14 | if (meta.mime === 'image/webp') { 15 | await sharp(filePath).toFile(path.resolve(__dirname, 'output', file)); 16 | } 17 | } 18 | }) 19 | })(); -------------------------------------------------------------------------------- /tools/match.js: -------------------------------------------------------------------------------- 1 | // (?<=(?