├── .gitignore ├── .gitmodules ├── .prettierignore ├── .prettierrc.js ├── .travis.yml ├── .vuepress ├── config.js ├── config │ ├── head.js │ ├── index.js │ ├── locales.js │ ├── markdown.js │ └── themeConf.js ├── plugins │ ├── my-loader │ │ ├── Loader.vue │ │ ├── index.js │ │ └── loader.js │ └── my-router │ │ └── index.js ├── public │ ├── edit-tools.png │ ├── favicon.ico │ └── scripts │ │ └── baidu-analytics.js ├── styles │ └── index.styl └── theme │ ├── components │ ├── Page.vue │ ├── RightSidebar.vue │ ├── SidebarGroup.js │ └── SidebarGroup.vue │ └── index.js ├── LICENSE ├── README.md ├── bin ├── cronjob.js ├── cronjob.sh ├── deploy.sh └── sidebar.js ├── deploy.sh ├── docs ├── Git实战手册 │ ├── 01.标签应用和版本管理.md │ ├── 02.批量修改log中的提交信息.md │ └── 03.stash解惑与妙用.md ├── NodeJS │ ├── 01.Koa │ │ ├── 01.koa源码:核心库原理.md │ │ ├── 02.koa源码:架构设计.md │ │ └── 03.koa源码:手动实现.md │ ├── 02.命令行 │ │ └── 01.玩转Nodejs命令行.md │ ├── 03.测试 │ │ ├── 01.Jest实战:单元测试与服务测试.md │ │ └── 02.Jest进阶:接入ts、集成测试与覆盖率统计.md │ ├── 04.Serverless │ │ ├── 01.基于实时数据库-在线对战五子棋小游戏.md │ │ └── 02.基于ServerLess的极简网页计数器:源码分析与最佳实践.md │ └── 05.更多 │ │ ├── 01.NodeJS实现简易区块链.md │ │ ├── 02.负载均衡:轮询调度算法实现.md │ │ ├── 03.VemoJS源码拆解.md │ │ ├── 04.NodeJS是如何监听文件的变化.md │ │ └── 05.日志库的实现机制与优化方法.md ├── ReactJS │ ├── 01.React组件通信解决方案.md │ ├── 02.ReactRouter进阶技巧.md │ ├── 03.一文彻底搞懂ReactHooks的原理和实现.md │ └── 04.一文说清VirtualDOM的含义与实现.md ├── UI设计 │ ├── 01.CSS3 │ │ ├── 01.border-sizing属性详解和应用.md │ │ ├── 02.Flex上手与实战.md │ │ └── 03.你了解css3的nth-child吗.md │ ├── 02.SCSS │ │ ├── 01.基础:配置与运行.md │ │ ├── 02.进阶:继承、占位符和混合宏.md │ │ └── 03.译文:逐步替换Sass.md │ ├── 03.动画设计 │ │ ├── 01.字体特效.md │ │ ├── 02.输入框特效.md │ │ ├── 03.按钮特效.md │ │ ├── 04.Loader特效-基础篇.md │ │ └── 05.Loader特效-进阶篇.md │ └── 04.项目实战 │ │ └── 01.个人博客UI设计.md ├── webpack4系列教程 │ ├── 00.零:前言.md │ ├── 01.一:打包JS.md │ ├── 02.二:编译ES6.md │ ├── 03.三:多页面解决方案--提取公共代码.md │ ├── 04.四:单页面解决方案--代码分割和懒加载.md │ ├── 05.五:处理CSS.md │ ├── 06.六:处理SCSS.md │ ├── 07.七:SCSS提取和懒加载.md │ ├── 08.八:JS-Tree-Shaking.md │ ├── 09.九:CSS-Tree-Shaking.md │ ├── 10.十:图片处理汇总.md │ ├── 11.十一:字体文件处理.md │ ├── 12.十二:处理第三方JavaScript库.md │ ├── 13.十三:自动生成HTML文件.md │ ├── 14.十四:Clean-Plugin-and-Watch-Mode.md │ ├── 15.十五:开发模式与webpack-dev-server.md │ └── 16.十六:开发模式和生产模式·实战.md ├── 前端知识体系 │ ├── 01.JavaScript │ │ ├── 01.JavaScript基础知识梳理-上.md │ │ ├── 02.JavaScript基础知识梳理-下.md │ │ ├── 03.正则表达式.md │ │ ├── 04.前端面试中常考的源码实现.md │ │ └── 05.JavaScript高级程序设计-读书笔记.md │ ├── 02.TypeScript │ │ ├── 01.基础篇.md │ │ └── 02.实战篇.md │ ├── 03.ES6 │ │ ├── 01.Promise专题 │ │ │ ├── 01.Promise概述.md │ │ │ ├── 02.手写Promise的相关方法.md │ │ │ └── 03.手动实现Promise.md │ │ ├── 02.ES6重难点整理.md │ │ └── 03.谈谈promise-async-await的执行顺序与V8引擎的BUG.md │ ├── 04.HTML5 │ │ ├── 01.Canvas │ │ │ ├── 01.canvas学习和滤镜实现.md │ │ │ └── 02.canvas离屏技术与放大镜实现.md │ │ ├── 02.HTML5原生拖放事件的学习与实践.md │ │ ├── 03.FileAPI文件操作实战.md │ │ ├── 04.WebSocket学习和群聊实现.md │ │ └── 05.ServiceWorker离线缓存实战.md │ ├── 05.浏览器与安全 │ │ ├── 01.SSL连接并非完全安全问题解决.md │ │ ├── 02.浏览器常见考点.md │ │ └── 03.Web安全与防护.md │ └── 06.开发实战 │ │ ├── 01.MathJax-让前端支持数学公式.md │ │ ├── 02.momentjs使用详解.md │ │ ├── 03.axios全局代理实战.md │ │ └── 05.微信网页登录逻辑与实现.md ├── 剑指offer刷题笔记 │ ├── 0.README.md │ ├── 01.字符串 │ │ ├── 01.替换空格.md │ │ ├── 02.字符串的全排列.md │ │ ├── 03.翻转单词顺序.md │ │ └── 04.实现atoi.md │ ├── 02.查找 │ │ ├── 01.旋转数组最小的数字.md │ │ └── 02.数字在排序数组中出现的次数.md │ ├── 03.链表 │ │ ├── 01.从尾到头打印链表.md │ │ ├── 02.快速删除链表节点.md │ │ ├── 03.链表倒数第k节点.md │ │ ├── 04.反转链表.md │ │ ├── 05.合并两个有序链表.md │ │ ├── 06.复杂链表的复制.md │ │ └── 07.两个链表中的第一个公共节点.md │ ├── 04.数组 │ │ ├── 01.二维数组中的查找.md │ │ ├── 02.数组顺序调整.md │ │ ├── 03.把数组排成最小的数.md │ │ └── 04.数组中的逆序对.md │ ├── 05.栈和队列 │ │ ├── 01.用两个栈实现队列.md │ │ ├── 02.包含min函数的栈.md │ │ └── 03.栈的压入弹出序列.md │ ├── 06.递归与循环 │ │ ├── 01.青蛙跳台阶.md │ │ ├── 02.数值的整次方.md │ │ ├── 03.打印从1到最大的n位数.md │ │ ├── 04.顺时针打印矩阵.md │ │ ├── 05.数组中出现次数超过一半的数字.md │ │ ├── 06.最小的k个数.md │ │ ├── 07.和为s的两个数字.md │ │ ├── 08.和为s的连续正数序列.md │ │ ├── 09.n个骰子的点数.md │ │ ├── 10.扑克牌的顺子.md │ │ └── 11.圆圈中最后剩下的数字.md │ ├── 07.树 │ │ ├── 01.重建二叉树.md │ │ ├── 02.判断是否子树.md │ │ ├── 03.二叉树的镜像.md │ │ ├── 04.二叉搜索树的后序遍历序列.md │ │ ├── 05.二叉树中和为某一值的路径.md │ │ ├── 06.二叉树层序遍历.md │ │ ├── 07.二叉树转双向链表.md │ │ └── 08.判断是否是平衡二叉树.md │ ├── 08.位运算 │ │ ├── 01.二进制中1的个数.md │ │ ├── 02.二进制中1的个数进阶版.md │ │ └── 03.数组中只出现一次的数字.md │ ├── 09.哈希表 │ │ ├── 01.丑数.md │ │ └── 02.第一次只出现一次的字符.md │ └── 10.堆 │ │ └── 01.最小的k个数.md ├── 思考与成长 │ └── 01.如何保持高效学习.md ├── 每周分享 │ ├── 00.介绍.md │ ├── 01.2019 │ │ ├── 01.新年初刊.md │ │ ├── 02.如何缩小学习反馈周期.md │ │ ├── 03.无声半年.md │ │ └── 04.redis热key等问题研究.md │ └── 02.2018 │ │ ├── 01.第一期.md │ │ └── 02.第二期.md └── 设计模式手册 │ ├── 01.创建型模式 │ ├── 01.单例模式.md │ ├── 02.工厂模式.md │ └── 03.抽象工厂模式.md │ ├── 02.结构型模式 │ ├── 01.享元模式.md │ ├── 02.代理模式.md │ ├── 03.桥接模式.md │ ├── 04.组合模式.md │ ├── 05.装饰者模式.md │ └── 06.适配器模式.md │ ├── 03.行为型模式 │ ├── 01.命令模式.md │ ├── 02.备忘录模式.md │ ├── 03.模板模式.md │ ├── 04.状态模式.md │ ├── 05.策略模式.md │ ├── 06.解释器模式.md │ ├── 07.订阅-发布模式.md │ ├── 08.责任链模式.md │ └── 09.迭代器模式.md │ └── README.md ├── huskyrc ├── nginx.conf ├── package.json └── pages ├── changelog.md └── friends.md /.gitignore: -------------------------------------------------------------------------------- 1 | # npm 2 | package-lock.json 3 | node_modules 4 | 5 | # vscode 6 | .vscode 7 | 8 | # vuepress 9 | dist 10 | .vuepress/config/sidebar-auto.js 11 | 12 | # 撰写中 13 | todo/ 14 | 15 | # 读书笔记 16 | notes/ 17 | 18 | # 剑指offer刷题笔记 19 | 剑指offer刷题笔记/index.js 20 | 21 | # 前端知识体系 22 | 前端知识体系/js/index.js -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongyuanxin/blog/da5e224c3ca8c3abb7301a3e460773ccf8d5e48c/.gitmodules -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 4, 3 | useTabs: false, 4 | semi: true, 5 | endOfLine: "lf", 6 | }; 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: true 2 | branches: 3 | only: 4 | - master 5 | language: node_js 6 | node_js: 7 | - "10" 8 | install: 9 | - npm install 10 | before_script: 11 | - git clone "https://godbmw:$GITHUB_TOKEN@github.com/dongyuanxin/notes.git" notes 12 | script: 13 | - npm run check 14 | - ./deploy.sh 15 | -------------------------------------------------------------------------------- /.vuepress/config.js: -------------------------------------------------------------------------------- 1 | const { mdConf, themeConf, localesConf, headConf } = require("./config/"); 2 | 3 | module.exports = { 4 | locales: localesConf, 5 | markdown: mdConf, 6 | themeConfig: themeConf, 7 | head: headConf, 8 | plugins: [ 9 | "vuepress-plugin-table-of-contents", 10 | require("./plugins/my-router"), 11 | require("./plugins/my-loader"), 12 | require("vuepress-plugin-viewer"), 13 | "@vuepress/back-to-top", 14 | ["@vuepress/google-analytics", { ga: "UA-124601890-1" }], 15 | [ 16 | "@vuepress/pwa", 17 | { 18 | serviceWorker: true, 19 | updatePopup: { 20 | message: "发现页面有新内容", 21 | buttonText: "刷新" 22 | } 23 | } 24 | ], 25 | [ 26 | "vuepress-plugin-comment", 27 | { 28 | choosen: "gitalk", 29 | options: { 30 | clientID: "6558fdf298fa596c5d8c", 31 | clientSecret: process.env.clientSecret || "", 32 | repo: "blog", 33 | owner: "dongyuanxin", 34 | admin: ["dongyuanxin"], 35 | id: "<%- frontmatter.commentid || frontmatter.permalink %>", // Ensure uniqueness and length less than 50 36 | distractionFreeMode: false, // Facebook-like distraction free mode 37 | labels: ["Gitalk", "Comment"], 38 | title: "「评论」<%- frontmatter.title %>", 39 | body: 40 | "<%- frontmatter.title %>:<%- window.location.origin %><%- frontmatter.to.path || window.location.pathname %>" 41 | } 42 | } 43 | ] 44 | ] 45 | }; 46 | -------------------------------------------------------------------------------- /.vuepress/config/head.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | [ 3 | "script", 4 | { 5 | type: "text/javascript", 6 | src: "/scripts/baidu-analytics.js", 7 | async: "" 8 | } 9 | ], 10 | ["link", { rel: "icon", href: "/favicon.ico" }] 11 | ]; 12 | -------------------------------------------------------------------------------- /.vuepress/config/index.js: -------------------------------------------------------------------------------- 1 | const mdConf = require("./markdown"); 2 | const themeConf = require("./themeConf"); 3 | const localesConf = require("./locales"); 4 | const headConf = require("./head"); 5 | 6 | module.exports = { 7 | mdConf, 8 | themeConf, 9 | localesConf, 10 | headConf 11 | }; 12 | -------------------------------------------------------------------------------- /.vuepress/config/locales.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "/": { 3 | lang: "zh-CN", 4 | title: "心谭博客", 5 | description: "专注前端与算法的博客" 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /.vuepress/config/markdown.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | // markdown-it-anchor 的选项 4 | anchor: { 5 | permalink: true 6 | }, 7 | // markdown-it-toc 的选项 8 | toc: { 9 | includeLevel: [1, 2, 3] 10 | }, 11 | lineNumbers: true 12 | } 13 | -------------------------------------------------------------------------------- /.vuepress/config/themeConf.js: -------------------------------------------------------------------------------- 1 | const { getSidebar } = require("./../../bin/sidebar"); 2 | 3 | module.exports = { 4 | repo: "dongyuanxin/blog", 5 | navbar: true, 6 | editLinks: true, 7 | editLinkText: "在 GitHub 上编辑此页", 8 | lastUpdated: "更新于", 9 | nav: [ 10 | { 11 | text: '更新日志', 12 | link: '/changelog/' 13 | }, 14 | { 15 | text: "笔记归档", 16 | link: "/notes/" 17 | }, 18 | { 19 | text: "前端图谱", 20 | link: "/frontend/" 21 | }, 22 | { 23 | text: "算法题解", 24 | link: "/algorithm/" 25 | } 26 | ], 27 | sidebar: getSidebar(), 28 | searchMaxSuggestions: 15 29 | }; 30 | -------------------------------------------------------------------------------- /.vuepress/plugins/my-loader/Loader.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 29 | 30 | 64 | -------------------------------------------------------------------------------- /.vuepress/plugins/my-loader/index.js: -------------------------------------------------------------------------------- 1 | const path= require('path') 2 | 3 | module.exports = { 4 | enhanceAppFiles: path.resolve(__dirname, 'loader.js'), 5 | globalUIComponents: 'Loader' 6 | } -------------------------------------------------------------------------------- /.vuepress/plugins/my-loader/loader.js: -------------------------------------------------------------------------------- 1 | import Loader from './Loader.vue' 2 | 3 | export default ({ Vue }) => { 4 | Vue.component('Loader', Loader) 5 | } -------------------------------------------------------------------------------- /.vuepress/plugins/my-router/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 文章路径增加前缀 和 评论标识 3 | extendPageData($page) { 4 | const { frontmatter, regularPath } = $page; 5 | 6 | frontmatter.comment = false; 7 | 8 | if (/^\/?docs\//.test(regularPath)) { 9 | frontmatter.commentid = frontmatter.permalink; 10 | frontmatter.permalink = `/passages/${frontmatter.permalink}`; 11 | } 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /.vuepress/public/edit-tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongyuanxin/blog/da5e224c3ca8c3abb7301a3e460773ccf8d5e48c/.vuepress/public/edit-tools.png -------------------------------------------------------------------------------- /.vuepress/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongyuanxin/blog/da5e224c3ca8c3abb7301a3e460773ccf8d5e48c/.vuepress/public/favicon.ico -------------------------------------------------------------------------------- /.vuepress/public/scripts/baidu-analytics.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | var _hmt = _hmt || []; 3 | 4 | var hm = document.createElement("script"); 5 | hm.src = "https://hm.baidu.com/hm.js?d0e7f39d671748aaf903f2f2878da387"; 6 | var s = document.getElementsByTagName("script")[0]; 7 | s.parentNode.insertBefore(hm, s); 8 | 9 | _hmt.push("_requirePlugin", "UrlChangeTracker", { 10 | shouldTrackUrlChange: function(newPath, oldPath) { 11 | return newPath && oldPath; 12 | } 13 | }); 14 | })(); 15 | -------------------------------------------------------------------------------- /.vuepress/styles/index.styl: -------------------------------------------------------------------------------- 1 | #vuepress-plugin-comment 2 | max-width $contentWidth 3 | margin 0 auto 4 | padding 2rem 2.5rem 5 | @media (max-width: $MQNarrow) 6 | padding 2rem 7 | @media (max-width: $MQMobileNarrow) 8 | padding 1.5rem 9 | 10 | #main-title 11 | display none !important 12 | 13 | .hero 14 | .description 15 | display none !important 16 | 17 | main.home 18 | ul 19 | line-height: 2.5 20 | 21 | // $sidebarWidth: 20rem 22 | // define at: vuepress/packages/@vuepress/core/lib/client/style/config.styl 23 | .page 24 | @media (min-width: $MQNarrow + 1px) { 25 | padding-right $sidebarWidth 26 | } 27 | 28 | // 侧边栏 29 | .sidebar 30 | span, a 31 | font-size 14px !important 32 | font-weight 500 !important 33 | 34 | .sidebar > .sidebar-links 35 | padding 0 !important 36 | 37 | .sidebar-group.is-sub-group > .sidebar-heading:not(.clickable) 38 | opacity 0.6 !important 39 | 40 | .sidebar span.arrow 41 | cursor pointer 42 | 43 | .sidebar-links > li 44 | margin-top 0 !important -------------------------------------------------------------------------------- /.vuepress/theme/components/Page.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 38 | 39 | -------------------------------------------------------------------------------- /.vuepress/theme/components/SidebarGroup.js: -------------------------------------------------------------------------------- 1 | export const hashRE = /#.*$/; 2 | export const extRE = /\.(md|html)$/; 3 | export const endingSlashRE = /\/$/; 4 | export const outboundRE = /^[a-z]+:/i; 5 | 6 | export function normalize(path) { 7 | return decodeURI(path) 8 | .replace(hashRE, "") 9 | .replace(extRE, ""); 10 | } 11 | 12 | export function getHash(path) { 13 | const match = path.match(hashRE); 14 | if (match) { 15 | return match[0]; 16 | } 17 | } 18 | 19 | export function isActive(route, path) { 20 | const routeHash = decodeURIComponent(route.hash); 21 | const linkHash = getHash(path); 22 | if (linkHash && routeHash !== linkHash) { 23 | return false; 24 | } 25 | const routePath = normalize(route.path); 26 | const pagePath = normalize(path); 27 | return routePath === pagePath; 28 | } 29 | -------------------------------------------------------------------------------- /.vuepress/theme/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extend: "@vuepress/theme-default" 3 | }; 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 董沅鑫 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /bin/cronjob.js: -------------------------------------------------------------------------------- 1 | const cron = require("node-cron"); 2 | const execa = require("execa"); 3 | const path = require("path"); 4 | 5 | cron.schedule("30 23 * * *", () => { 6 | execa(path.join(__dirname, "cronjob.sh")).stdout.pipe(process.stdout); 7 | }); 8 | -------------------------------------------------------------------------------- /bin/cronjob.sh: -------------------------------------------------------------------------------- 1 | cd /home/ubuntu/self/blog 2 | 3 | # download noetes 4 | rm -rf notes 5 | git clone git@github.com:dongyuanxin/notes.git 6 | 7 | # build vuepress 8 | npm run build 9 | 10 | # move static files 11 | cp -dpr .vuepress/dist/* /home/ubuntu/data/blog-static/ 12 | cd - 13 | -------------------------------------------------------------------------------- /bin/deploy.sh: -------------------------------------------------------------------------------- 1 | # download noetes 2 | rm -rf notes 3 | git clone git@github.com:dongyuanxin/notes.git 4 | 5 | # build vuepress 6 | npm run build 7 | 8 | # upload files 9 | scp -r .vuepress/dist/* ubuntu@101.32.188.38:~/data/blog-static/ 10 | -------------------------------------------------------------------------------- /bin/sidebar.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const yaml = require("js-yaml"); 4 | 5 | module.exports.getSidebar = getSidebar; 6 | 7 | const ROOT_PATH = path.resolve(process.cwd(), "notes"); 8 | 9 | function getSidebar() { 10 | const toc = []; 11 | scan(ROOT_PATH, toc); 12 | return { 13 | "/notes/": toc 14 | }; 15 | } 16 | 17 | function scan(parentPath, toc = []) { 18 | const folders = fs.readdirSync(parentPath); 19 | folders.sort((a, b) => { 20 | if (a < b) return -1; 21 | else if (a > b) return 1; 22 | return 0; 23 | }); 24 | 25 | for (const folderName of folders) { 26 | if (!checkValid(folderName)) { 27 | continue; 28 | } 29 | 30 | const folderPath = path.resolve(parentPath, folderName); 31 | const fileStat = fs.statSync(folderPath); 32 | if (fileStat.isFile() && folderName.endsWith(".md")) { 33 | toc.push({ 34 | title: folderName.replace(/^\d+\./, "").replace(/\.md$/, ""), 35 | collapsable: true, 36 | sidebarDepth: 0, 37 | path: `/notes/${path.relative(ROOT_PATH, folderPath)}`, 38 | children: [] 39 | }); 40 | } else if (fileStat.isDirectory()) { 41 | const subToc = { 42 | title: folderName.replace(/^\d+\./, ""), 43 | collapsable: true, 44 | sidebarDepth: 0, 45 | children: [] 46 | }; 47 | const existReadme = fs.existsSync( 48 | path.join(folderPath, "readme.md") 49 | ); 50 | if (existReadme) { 51 | subToc.path = `/notes/${path.relative(ROOT_PATH, folderPath)}/`; 52 | } 53 | scan(folderPath, subToc.children); 54 | if (subToc.children.length > 0 && existReadme) { 55 | subToc.path = getPermalink(path.join(folderPath, "readme.md")); 56 | } 57 | toc.push(subToc); 58 | } 59 | } 60 | } 61 | 62 | function checkValid(name = "") { 63 | return /^\d+\./.test(name); 64 | } 65 | 66 | function getPermalink(filepath) { 67 | if (!filepath) { 68 | return; 69 | } 70 | 71 | const content = fs.readFileSync(filepath).toString("utf8"); 72 | try { 73 | const yamlContent = /---\n((.|\n)*?)\n---\n/.exec(content)[1]; 74 | const doc = yaml.safeLoad(yamlContent); 75 | if (doc) { 76 | return "/" + doc.permalink + "/"; 77 | } 78 | } catch (error) { 79 | console.log("getPermalink error", filepath, error.message); 80 | } 81 | return; 82 | } 83 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # throw error 4 | set -e 5 | 6 | if [[ -z "$CODING_TOKEN" || -z "$GITHUB_TOKEN" ]]; then 7 | exit 0 8 | fi 9 | 10 | # build static 11 | npm run build 12 | cd .vuepress/dist 13 | echo "xin-tan.com" > CNAME 14 | 15 | git init 16 | git add -A 17 | git commit -m 'deploy' 18 | 19 | # xxoo521.com 20 | # git push -f "https://godbmw:$CODING_TOKEN@git.dev.tencent.com/godbmw/godbmw.coding.me.git" master:master 21 | 22 | git push -f "https://godbmw:$GITHUB_TOKEN@github.com/dongyuanxin/blog.git" master:gh-pages 23 | 24 | cd - 25 | 26 | rm -rf .vuepress/dist -------------------------------------------------------------------------------- /docs/Git实战手册/01.标签应用和版本管理.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "标签应用和版本管理" 3 | date: "2018-09-06" 4 | permalink: "2018-09-06-git-tag-and-version" 5 | --- 6 | 7 | > 当一个代码仓库进过长时间的迭代,针对不同的时期和需求,必定会有不同的版本。而借助 Git 提供的标签功能,可以快捷方便地记录代码版本。无论什么时候,想取回某个版本,不再需要查找冗长的`commit_id`,只需要取出打标签的历史版本即可。 8 | 9 | 可以这么理解:**标签是版本库的一个快照**。在主流的 Git 平台上,版本可以直接下载的,节省了开发者的不少精力。 10 | 11 | 下面记录下如何借助 标签 进行版本管理。 12 | 13 | ### 1. 环境仿真 14 | 15 | 首先我在 github 上建立了一个仓库。如你所见,这是一个全新仓库:[>>> Star now](https://github.com/dongyuanxin/git-demos) 16 | 17 | ![](https://static.godbmw.com/images/Git/标签应用和版本管理/1.png) 18 | 19 | 执行`git clone git@github.com:dongyuanxin/git-demos.git`, 将代码库 clone 到本地后。 20 | 21 | 在根目录下创建一个`hello.v1.py`文件:`touch hello.v1.py`。代码如下: 22 | 23 | ```python 24 | # hello.v1.py 25 | print("This is version 1.0") 26 | ``` 27 | 28 | ### 2. 本地标签操作 29 | 30 | 下面记录了本地标签操作的常用命令: 31 | 32 | | 命令 | 作用 | 33 | | ----------------------------------- | ------------------------------- | 34 | | `git tag` | 查看所有标签 | 35 | | `git tag ` | 创建标签:tagname | 36 | | `git tag -a -m ` | 为标签 tagname 增加说明 comment | 37 | | `git show ` | 查看标签:tagname 的内容 | 38 | | `git tag -d ` | 删除标签:tagname | 39 | 40 | 其中,创建标签 tagname 并且增加说明 comment,可以简写成:`git tag -m ` 41 | 42 | Now, 为代码库在本地打上标签:`git tag v1.0.0 -m 'v1.0.0正式版本'`。 43 | 44 | ### 3. 标签的远程推送和拉取 45 | 46 | 在本地打好标签后,需要将标签推送到 Git 平台(比如 GitHub)上,才能留下版本快照,供别人下载使用。 47 | 48 | 除此之外,还有可能删除已经发布的标签(比如标签命名不合规范)。 49 | 50 | 下面记录了远程标签操作的常用命令: 51 | 52 | | 命令 | 作用 | 53 | | --------------------------- | ------------ | 54 | | `git push origin --tags` | 推送所有标签 | 55 | | `git push origin ` | 推送指定标签 | 56 | 57 | 我们首先将`v1.0.0`版本推送到 Github:`git push origin v1.0.0` 58 | 59 | 然后,打开仓库,我们会发现`releases`标签已经变成了 1。 60 | ![](https://static.godbmw.com/images/Git/标签应用和版本管理/2.png) 61 | 62 | 点进去,会发现我们可以直接下载`v1.0.0`版本。所以,标签是版本的快照。 63 | ![](https://static.godbmw.com/images/Git/标签应用和版本管理/3.png) 64 | 65 | 值得一提的是:**标签会随着`git clone`仓库时候,自动拉取到本地**。不信的话可以 clone 一下这个代码仓库,查看是不是已经有了`v1.0.0`这个标签。 66 | 67 | ### 4. 标签的删除 68 | 69 | 标签的删除请慎重,尤其是删除远程标签(_一般来说,没有人会这么做_)。命令如下: 70 | 71 | | 命令 | 作用 | 72 | | -------------------------------------- | ------------ | 73 | | `git tag -d ` | 删除指定标签 | 74 | | `git push origin :refs/tags/` | 删除远程标签 | 75 | 76 | ### 5. 拓展阅读 77 | 78 | 版本管理可以借助 git 的 tag 来实现。但是,在开发过程中,不可能每次修改都打标签然后发布。 79 | 80 | 实际开发中,一般会有一个`dev`分支,作为开发版本分支。在没有完成此次开发任务前,所有的代码都是合并到`dev`分支。 81 | 82 | 当完成了开发任务,管理者会将`dev`分支合并到`master`分支,然后删除`dev`分支,并且通过打标签来发布版本。 83 | 84 | 这就是为什么很多放弃维护的大型开源项目只有`master`分支的原因。 85 | -------------------------------------------------------------------------------- /docs/Git实战手册/02.批量修改log中的提交信息.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "批量修改log中的提交信息" 3 | date: "2018-06-19" 4 | permalink: "2018-06-19-batch-edit-log" 5 | --- 6 | 7 | > 事情的起源是这样的:迷恋的谷歌的我最近申请了一个新的 google 邮箱。然后果断在 github 上更新了邮箱地址,并且删除了之前的 163 等国内邮箱。 8 | 9 | **回到 github 首页,我发现之前的项目提交记录都没了。**到底什么情况(O_o)?? 10 | 11 | 看一张灾难现场图: 12 | 13 | ![](https://static.godbmw.com/images/Git/Git修改log中的提交信息/1.png) 14 | 15 | 仔细查找了相关资料,发现了是因为之前的提交记录中`Author`字段的信息是 163 等国内邮箱的。如下: 16 | 17 | ![](https://static.godbmw.com/images/Git/Git修改log中的提交信息/2.png) 18 | 19 | 所以,解决方法就是:**更改`log`中的提交信息**。 20 | 21 | ### 简单尝试: 22 | 23 | 通过百度,发现通过如下命令可以修改: 24 | 25 | ```sh 26 | git commit --amend --author='名称 <邮箱>' 27 | ``` 28 | 29 | 但是,这只能修改最近一条。**如何批量修改 log 信息呢?** 30 | 31 | ### 解决方法 32 | 33 | > 通过 google,发现`stackoverflow`上早有人提问,[请移步墙外](https://stackoverflow.com/questions/750172/how-to-change-the-author-and-committer-name-and-e-mail-of-multiple-commits-in-gi?rq=1)。 34 | > 同时,`Github`官方也提供了脚本,虽然仓库没了,但是复制脚本,然后执行即可,[移步这里](https://help.github.com/articles/changing-author-info/) 35 | 36 | **首先,我们创建`change.sh`脚本,并根据个人信息复制以下脚本**。 37 | 38 | ```sh 39 | #!/bin/sh 40 | 41 | git filter-branch --env-filter ' 42 | OLD_EMAIL="填写原来的邮箱" 43 | CORRECT_NAME="填写现在的名称" 44 | CORRECT_EMAIL="填写现在的邮箱" 45 | if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ] 46 | then 47 | export GIT_COMMITTER_NAME="$CORRECT_NAME" 48 | export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL" 49 | fi 50 | if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ] 51 | then 52 | export GIT_AUTHOR_NAME="$CORRECT_NAME" 53 | export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL" 54 | fi 55 | ' --tag-name-filter cat -- --branches --tags 56 | ``` 57 | 58 | **将脚本移入要修改的`git`仓库,并执行脚本**。修改后的 log 信息如下。 59 | 60 | ![](https://static.godbmw.com/images/Git/Git修改log中的提交信息/3.png) 61 | 62 | **通过`git push --force`强行推送修改后的 log 信息。** 63 | 64 | 哈哈,[我的 github](https://github.com/dongyuanxin/markdown-static)又重新绿了起来。 65 | -------------------------------------------------------------------------------- /docs/Git实战手册/03.stash解惑与妙用.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "stash解惑与妙用" 3 | date: "2018-09-07" 4 | permalink: "2018-09-07-git-stash" 5 | --- 6 | 7 | > 在实际项目开发中,总会遇到代码写到一半(没法去打`commit`),去开启新的分支 修复 Bug 或者 增加功能 的情况。如果不处理,未修改的代码就会被带入临时创建的新的分支,没写完的代码 和 要修复的代码混合在一起,绝对苦逼。而 Git 中的`stash`就是用来对付这种情况。 8 | 9 | ### 1. 初识`git stash` 10 | 11 | `stash`在英文中的意思是:隐藏。在 Git 代码管理的过程中,它的作用也是隐藏没完成的代码,防止它干扰 别人 或者 新分支的工作。 12 | 13 | 关于`git stash`,常用命令如下: 14 | 15 | | 命令 | 作用 | 16 | | ---------------------- | ---------------------------------------------------------- | 17 | | `git stash` | 隐藏当前的工作现场, 此时, `git status`的结果是 `clean` | 18 | | `git stash list` | 查看所有隐藏, 每一行的冒号前面的字符串就是标识此隐藏的`id` | 19 | | `git stash apply ` | 重新显示标识为 id 的隐藏 | 20 | | `git stash drop ` | `git apply`恢复隐藏后, 需要手动删除 list 列表中的记录 | 21 | | `git stash pop` | Apply last stash and remove it from the list | 22 | 23 | ### 2. 应用与实战 24 | 25 | 假设:正当我在 master 分支上写着文档时候(_没有完成、没有提交_),同事发现 `hello.py` 这个脚本有问题,紧急报告给我进行修复。 26 | 27 | 收到报告,下意识就是开一个 debug 分支来处理 bug。但是,现在的文档没完成,自然无法提交,又不能把没提交的东西带入到新创建的 debug 分支(_执意如此,我也没办法 : \)_)。目前,status 如下: 28 | 29 | ![](https://static.godbmw.com/images/Git/stash解惑与妙用/1.png) 30 | 31 | 为了达到目的,分为以下几步: 32 | 33 | 1. 隐藏修改:`git stash` : ![](https://static.godbmw.com/images/Git/stash解惑与妙用/2.png) 34 | 2. 创建新分支:`git branch debug` : ![](https://static.godbmw.com/images/Git/stash解惑与妙用/3.png) 35 | 3. 在`debug`分支上修复 bug, 并且将修改添加到 log 中 : ![](https://static.godbmw.com/images/Git/stash解惑与妙用/4.png) 36 | 4. 回到`master`分支, 合并`debug`分支的修改, 并且删除 `debug` 分支 : ![](https://static.godbmw.com/images/Git/stash解惑与妙用/5.png) 37 | 5. 重新显示隐藏的`stash`,并且将其从`stash list`列表中删除 : ![](https://static.godbmw.com/images/Git/stash解惑与妙用/6.png) 38 | 6. **然后就可以愉快地继续做自己的事情啦!** 39 | 40 | 感谢[chucklu](https://www.cnblogs.com/chucklu/)的提醒:上面第五步的两条命令,可以用`git stash pop`命令代替。 41 | 42 | ### 3. 拓展阅读 43 | 44 | 在实际生产过程中,难免会遇到多个 stash 的情况。此时,他们的 id 默认都是:`stash{0}`, `stash{1}`, `stash{2}` ... ... 45 | 46 | 当我们恢复一个 stash ,并且将它从 stash list 中移除的时候,在其下方的 stash 记录的 id 会自动变小,以保证 id 是从 0 到 n 的连续自然数列。**所以,当从 stash list 移除一个 stash 后,一些 stash 的 id 就会发生改变**。此处是个坑。 47 | -------------------------------------------------------------------------------- /docs/NodeJS/01.Koa/02.koa源码:架构设计.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "深入koa源码:架构设计" 3 | date: "2019-06-18" 4 | permalink: "2019-06-18-deep-in-koa" 5 | --- 6 | 7 | > 最近读了 koa 的源码,理清楚了架构设计与用到的第三方库。本系列将分为 3 篇,分别介绍 koa 的架构设计和 3 个核心库,最终会手动实现一个简易的 koa。 8 | 9 | koa 的实现都在仓库的`lib`目录下,如下图所示,只有 4 个文件: 10 | 11 | ![](https://static.godbmw.com/img/2019-06-18-deep-in-koa/1.png) 12 | 13 | 对于这四个文件,根据用途和封装逻辑,可以分为 3 类:req 和 res,上下文以及 application。 14 | 15 | ## req 和 res 16 | 17 | 对应的文件是:`request.js` 和 `response.js`。分别代表着客户端请求信息和服务端返回信息。 18 | 19 | 这两个文件在实现逻辑上完全一致。对外暴露都是一个对象,对象上的属性都使用了`getter`或`setter`来实现读写控制。 20 | 21 | ## 上下文 22 | 23 | 对应的文件是:`context.js`。存了运行环境的上下文信息,例如`cookies`。 24 | 25 | 除此之外,因为`request`和`response`都属于上下文信息,所以通过`delegate.js`库来实现了对`request.js`和`response.js`上所有属性的代理。例如以下代码: 26 | 27 | ```javascript 28 | /** 29 | * Response delegation. 30 | */ 31 | delegate(proto, "response") 32 | .method("attachment") 33 | .method("redirect"); 34 | 35 | /** 36 | * Request delegation. 37 | */ 38 | 39 | delegate(proto, "request") 40 | .method("acceptsLanguages") 41 | .method("acceptsEncodings"); 42 | ``` 43 | 44 | 使用代理的另外一个好处就是:更方便的访问 req 和 res 上的属性。比如在开发 koa 应用的时候,可以通过`ctx.headers`来读取客户端请求的头部信息,不需要写成`ctx.res.headers`了(这样写没错)。 45 | 46 | **注意**:req 和 res 并不是在`context.js`中被绑定到上下文的,而是在`application`被绑定到上下文变量`ctx`中的。原因是因为每个请求的 req/res 都不是相同的。 47 | 48 | ## Application 49 | 50 | 对应的文件是: `application.js`。这个文件的逻辑是最重要的,它的作用主要是: 51 | 52 | - 给用户暴露服务启动接口 53 | - 针对每个请求,生成新的上下文 54 | - 处理中间件,将其串联 55 | 56 | ### 对外暴露接口 57 | 58 | 使用 koa 时候,我们常通过`listen`或者`callback`来启动服务器: 59 | 60 | ```javascript 61 | const app = new Koa(); 62 | app.listen(3000); // listen启动 63 | http.createServer(app.callback()).listen(3000); // callback启动 64 | ``` 65 | 66 | 这两种启动方法是完全等价的。因为`listen`方法内部,就调用了`callback`,并且将它传给`http.createServer`。接着看一下`callback`这个方法主要做了什么: 67 | 68 | 1. 调用`koa-compose`将中间件串联起来(下文再讲)。 69 | 2. 生成传给`http.createServer()`的函数,并且返回。 70 | 71 | - `http.createServer`传给函数参数的请求信息和返回信息,都被这个函数拿到了。并且传给`createContext`方法,生成本次请求的上下文。 72 | - 将生成的上下文传给第 1 步生成的中间件调用链,**这就是为什么我们在中间件处理逻辑的时候能够访问`ctx`** 73 | 74 | ### 生成新的上下文 75 | 76 | 这里上下文的方法对应的是`createContext`方法。这里我觉得更像语法糖,是为了让 koa 使用者使用更方便。比如以下这段代码: 77 | 78 | ```javascript 79 | // this.request 是 request.js 暴露出来的对象,将其引用保存在context.request中 80 | // 用户可以直接通过 ctx.属性名 来访问对应属性 81 | const request = (context.request = Object.create(this.request)); 82 | 83 | // 这个req是本次请求信息,是由 http.createServer 传递给回调函数的 84 | context.req = request.req = response.req = req; 85 | ``` 86 | 87 | 读到这里,虽然可以解释 `context.headers` 是 `context.request.headers` 的语法糖这类问题。但是感觉怪怪的。就以这个例子,context.headers 访问的是 context.request 上的 headers,而不是本次请求信息上的`headers`。本次请求信息挂在了`context.req`上。 88 | 89 | 让我们再回到`reqeust.js`的源码,看到了`headers`的 getter 实现: 90 | 91 | ```javascript 92 | get headers() { 93 | return this.req.headers; 94 | } 95 | ``` 96 | 97 | 所以,`context.request.headers` 就是 `context.request.req.headers`。而前面提及的`createContext`方法中的逻辑,`context.reqest`上的`req`属性就是由`http`模块函数传来的真实请求信息。 **感谢 [@theniceangel](https://github.com/theniceangel) 的评论指正**。 98 | 99 | 可以看到,koa 为了让开发者使用方便,在上下文上做了很多工作。 100 | 101 | ### 中间件机制 102 | 103 | 中间件的设计是 koa 最重要的部分,实现上用到了`koa-compose`库来串联中间件,形成“洋葱模型”。关于这个库,放在第二篇关于 koa 核心库的介绍中说明。 104 | 105 | application 中处理中间件的函数是`use`和`handleRequest`: 106 | 107 | - `use`函数:传入`async/await`函数,并将其放入 application 实例上的`middleware`数组中。如果传入是 generator,会调用`koa-conver`库将其转化为`async/await`函数。 108 | - `handleRequest(ctx, fnMiddleware)`函数:传入的`fnMiddleware`是已经串联好的中间件,函数所做的工作就是再其后再添加一个返回给客户端的函数和错误处理函数。返回给客户端的函数其实就是`respond`函数,里面通过调用`res.end()`来向客户端返回信息,整个流程就走完了。 109 | -------------------------------------------------------------------------------- /docs/NodeJS/03.测试/02.Jest进阶:接入ts、集成测试与覆盖率统计.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Jest进阶:接入ts、集成测试与覆盖率统计" 3 | date: "2019-05-04" 4 | permalink: "2019-05-04-deep-in-jest" 5 | --- 6 | 7 | ## 接入 TypeScript 8 | 9 | 在给 vemojs 做完各种测试之后,导师很快提出了新的要求,给 [clousebase-cli](https://github.com/TencentCloudBase/cloud-base-cli)  编写测试用例。有个问题摆在眼前:它是用 typescript 编写,所以需要配置相关环境。 10 | 11 | 好吧,不说废话了,直接上干货。 12 | 13 | `jest.config.js`  配置内容如下,解释在注释里面: 14 | 15 | ```javascript 16 | module.exports = { 17 | roots: [ 18 | "/test" // 测试目录 19 | ], 20 | transform: { 21 | "^.+\\.tsx?$": "ts-jest" // 匹配 .ts 或者 .tsx 结尾的文件 22 | }, 23 | collectCoverage: true, // 统计覆盖率 24 | testEnvironment: "node", // 测试环境 25 | setupFilesAfterEnv: [ 26 | "/jest.setup.js" // 之后再说 27 | ], 28 | // 不算入覆盖率的文件夹 29 | coveragePathIgnorePatterns: [ 30 | "/node_modules/", 31 | "/test/", 32 | "/deps/" 33 | ] 34 | }; 35 | ``` 36 | 37 | 上面有个 `jest.setup.js` ,它的内容是: `jest.setTimeout(60000)` 。因为有时候网速很慢,api 请求延时会很高,所以这个就是设置请求超时时间为 1 分钟。 38 | 39 | 最坑的一点是,除了 `jest`  的配置文件,还要修改 typescript 对应的文件, `tsconfig.json`  内容如下。types 中必须添加 `jest` ,否则找不到 `expect` 、 `describe`  等变量的定义。 40 | 41 | ```json 42 | { 43 | "compilerOptions": { 44 | "types": ["node", "jest"] 45 | } 46 | } 47 | ``` 48 | 49 | **总之,cloudbase-cli 的测试用例写的比 vemo 好,哈哈** 50 | 51 | ## 集成测试 52 | 53 | 持续继承测试我们借助  [https://travis-ci.org/](https://travis-ci.org/)  这个平台,它的工作流程非常简单: 54 | 55 | 1. 在它平台上授权 github 仓库的权限,github 仓库下配置  .travis.yml 文件 56 | 1. 每次 commit 推上新代码的时候,travis-ci 平台都会接收到通知 57 | 1. 读取 .travis.yml 文件,**然后创建一个虚拟环境**,来跑配置好的脚本(比如启动测试脚本) 58 | 59 | 它的优点在于,测试代码推上去后,直接在账号下的控制台就能看到测试结果,非常方便;而且可以在配置文件中,指明多个测试环境,比如 node 有 6、8、10,让测试更具有信服力。 60 | 61 | 我把样例代码放在了 [try-travis-ci](https://github.com/dongyuanxin/try-travis-ci)  仓库下,可以跑一下看看。下面是 .travis.yml 文件内容。 62 | 63 | ```yaml 64 | sudo: false 65 | language: "node_js" 66 | node_js: 67 | - "8" 68 | - "10" 69 | install: 70 | - npm install 71 | script: npm run test 72 | ``` 73 | 74 | 看见了吗,就是下面贼酷炫的界面,登陆用户就能看到了:
![image.png](https://cdn.nlark.com/yuque/0/2019/png/233327/1556960049220-d204d334-21fb-4963-9095-e37d1600ac4b.png#align=left&display=inline&height=784&name=image.png&originHeight=980&originWidth=1908&size=147774&status=done&width=1526.4) 75 | 76 | ## 覆盖率统计 77 | 78 | 覆盖率统计也很简单(本来以为会很难),但是要安装 `coveralls`  这个库。除此之外,还要修改一下 package.json 中的 scripts 的指令。通过管道,将结果交给 coveralls。 79 | 80 | ```json 81 | { 82 | "scripts": { 83 | "test": "jest --passWithNoTests --coverage --env=node --detectOpenHandles --coverageReporters=text-lcov | coveralls" 84 | } 85 | } 86 | ``` 87 | 88 | 后来发现,在统计覆盖率的时候,会把覆盖的信息放在根目录下的 `coverage`  文件夹下,这些信息都是多个平台约定好的数据格式。所以各个工具间可以共同使用。 89 | 90 | 剩下要做的就是,登陆 coveralls.io 平台,授权 github 仓库权限。当你在 travis 平台运行上述 scripts 脚本时候,它就自动把结果扔到了 coveralls.io 平台。登陆账号,就能看到覆盖率了。 91 | 92 | ## 参考资料 93 | 94 | - 《持续集成服务 Travis CI 教程》:[http://www.ruanyifeng.com/blog/2017/12/travis_ci_tutorial.html?20190430165111](http://www.ruanyifeng.com/blog/2017/12/travis_ci_tutorial.html?20190430165111) 95 | - Travis CI Document:[https://docs.travis-ci.com/user/languages/javascript-with-nodejs/#stq=&stp=0](https://docs.travis-ci.com/user/languages/javascript-with-nodejs/#stq=&stp=0) 96 | - Coveralls IO JavaScript Document:[https://docs.coveralls.io/javascript](https://docs.coveralls.io/javascript) 97 | - 第三方库 node-coveralls:[https://github.com/nickmerwin/node-coveralls](https://github.com/nickmerwin/node-coveralls) 98 | -------------------------------------------------------------------------------- /docs/NodeJS/05.更多/03.VemoJS源码拆解.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "VemoJS源码拆解" 3 | date: "2019-04-23" 4 | permalink: "2019-04-23-vemojs" 5 | --- 6 | 7 | 主要分析下 src 文件夹下的代码。从简单到难吧: 8 | 9 | ## utils.js 10 | 11 | 按照命名,肯定是封装一些常用的方法。这里只提供了一个 cpuNum 的 getter 接口。 12 | 13 | 这个是计算当前计算机的 cpu 核心数目,用于决定开启多大的集群进程。为了保证效率,最小的返回值是 4。 14 | 15 | ## error.js 16 | 17 | 主要从 `Error`  对象上继承并且创建了新对象: `VemoError` 。对象构造函数上, **设计了一个有关此框架的错误状态码的信息** 。 18 | 19 | 除了 `VemoError` ,还暴露了一些错误的代号定义。 20 | 21 | ## cluster.js 22 | 23 | 对外暴露了自定义的高并发集群代码,它引用了同级的 `./process.js`  封装的管理类,并且初始化了它。它的具体含义请见"process.js"中的讲解。 24 | 25 | ## cloudbase.js 26 | 27 | `getLocalSecret`  方法是读取本地用户的根目录中的 `.tcbrc.json`  配置文件,并且将 Id 和 Key 放入 `process.env`  变量中。 28 | 29 | `getTempSecret`  方法是获取云主机的临时密钥: 30 | 31 | 1. 正常情况下,访问远程地址,获取 id、key、token 和过期时间,并且放入 `process.env`  变量中。 **有个不错的设计点:过期时间比远程动态减少 600s,主要是为了提前拉取** 32 | 1. 异常情况下, **如果没有到达最大失败次数,则在异常捕获中,调用自身,进行重试** 。这里也是一个不错的设计点,包括参数的传递。(不需要用闭包) 33 | 1. **改进意见** : 34 | 1. 考虑到网络情况造成的异常情况处理,其实可以使用定时器调用。 35 | 1. 检测位置可以提到函数入口,考虑参数为 (0, -1) 的情况 36 | 37 |
对外暴露:
async 函数,就是拿到有效的临时密钥,并且挂载上下文,继续执行后面的程序(next 参数)。 38 | 39 | ## process.js 40 | 41 | 整体的设计思路是:一个主进程,多个工作进程。为了保证工作进程有效,**又采用了"心跳机制" + "生命周期" + "定时检测"3 种机制**来保证有效以及意外情况下的重启机制。 42 | 43 | 除此之外,在内存不足的时候,会自动降级服务! 44 | 45 | ### 内置方法 46 | 47 | `timer`  函数,就是一个类似 setInterval 的函数。与 setInterval 不同的地方有 2 点: 48 | 49 | 1. 回调函数的参数:当前的时间戳 50 | 1. 第一次是立即执行的 51 | 52 | `restartWorker`  函数,专门用来重启工作进程的函数。
它首先会检测距离上次批量重启的时间是否太短,太短,则跳过。 **根据全局环境设置,这里的每次自动重启检测的时间是 60s。** 
否则,循环遍历所有子进程, **检查当前子进程的存活时间是否超过规定(60min)** ,超过,就调用 killWorker 先新建再删除。
这个函数的精彩之处,在于时间上的处理。一言:每分钟自动检测进程更新,每个进程都会在 1 小时后被依次自动更新。 53 | 54 | `killWorker`  函数,先启动新进程占位,再删除原进程。 55 | 56 | 1. 对于新建的进程:监听 killSelf 新号,重建自己;监听 hearBeat 心跳信号。 57 | 1. 对于被替换的老进程:先断开连接,再 2s 后 kill 掉。( *problem:为什么先断链,再关闭呢* ?如果有大运算,调用 disconnect 会阻塞并不会关闭 IPC 管道。需要等待 2s 自定义缓冲时间, **再强制 kill** ,不再调用 disconnect,因为还会阻塞)。 58 | 59 | ### 对外暴露方法 60 | 61 | `init`  函数,初始化进程管理器: 62 | 63 | 1. 对于主进程 64 | 1. 根据 cpu 核心创建工作进程 65 | 1. 每个工作进程监听 3 种信号并且响应 66 | 1. 开启 restartWorker,对于超过生命周期的进程,自动重启 67 | 1. 再定义一个 timer,检测每个工作进程存活状态,以及心跳是否正常 68 | 1. 对于工作进程 69 | 1. 加载 index.js 中的端口逻辑,若有出错,则告知主进程,并且关闭主进程( *problem:主进程会自动关闭其他工作进程吗?* cluster 已经帮忙做了,linux 下通过 ps -aux | grep "..." 可以查看) 70 | 1. 定义一个 timer,向主进程发送心跳包 71 | 1. 定义一个 timer,检测内存占用过高,主进程关闭工作进程( *problem:process.memoryUsage()是全部的吗?是当前进程的信息* ) 72 | 73 | `reload`  函数,重启全部进程:
这个设计的很巧妙,因为全部工作进程的创建时间都放在 `workerCreateTime`  对象中,主进程中又开启了自动重启子进程的 timer(在 120 行)。所以这里直接一个循环,将其所有属性置 0。
**当然,这并不是同时重启,每个子进程的重启有个间隔,这个间隔可以改进,因为这个间隔期间就是服务器响应能力比较弱的时候** 74 | 75 | ## index.js 76 | 77 | 在工作进程启动端口相关的服务,主要分为 3 个部分:普通 http 服务、websocket 服务以及静态服务器服务。 78 | 79 | 请分别调研使用它们的服务对应的库的用法。 80 | -------------------------------------------------------------------------------- /docs/NodeJS/05.更多/04.NodeJS是如何监听文件的变化.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "NodeJS是如何监听文件的变化?" 3 | date: "2019-09-03" 4 | permalink: "2019-09-03-nodejs-watch-file" 5 | --- 6 | 7 | Keywords: 操作系统差异、识别用户/编辑器操作、连续触发的优化、工程级 API。 8 | 9 | ## 1. 概述 10 | 11 | NodeJS 提供了 fs.watch / fs.watchFile 两种 API: 12 | 13 | - fs.watch: 推荐,可以监听文件夹。基于操作系统。 14 | - fs.watchFile: 只能监听指定文件。并且通过轮询检测文件变化,不能响应实时反馈。 15 | 16 | 一个监听指定文件夹的代码如下: 17 | 18 | ```javascript 19 | fs.watch(dir, { recursive: true }, (eventType, file) => { 20 | if (file && eventType === "change") { 21 | console.log(`${file} 已经改变`); 22 | } 23 | }); 24 | ``` 25 | 26 | ## 2. 跨平台优化 27 | 28 | > 对于不同系统内核,比如 maxos,fs.watch 回调函数中的第一个参数,不会监听到 rename、delete 事件。因此,**这不是一个工程级别的可用 api**。 29 | 30 | ### 2.1 文件 md5 31 | 32 | 某些开源软件,会将文件内容都清空后,再添加内容。而且保存过程中,可能会出现多个中间态。 33 | 34 | 对于文件更改的情况,检测内容的 md5 值,是个不错的方法。 35 | 36 | ```javascript 37 | let previousMD5 = ""; 38 | fs.watch("./whatever", (type, filename) => { 39 | if (!filename) { 40 | return; 41 | } 42 | 43 | const md5 = crypto.createHash("md5"); 44 | const currentMD5 = md5 45 | .update(fs.readFileSync(filename).toString()) 46 | .digest("hex"); 47 | if (currentMD5 === previousMD5) { 48 | return; 49 | } 50 | 51 | previousMD5 = currentMD5; 52 | console.log(`${filename} is changed`); 53 | }); 54 | ``` 55 | 56 | ### 2.2 事件频率控制 57 | 58 | 对于文件变更,不同的系统可能会触发多个不同的中间态。因此,借助 debounce 函数的思想,控制和修正回调事件的触发频率。 59 | 60 | 前面的代码修正为: 61 | 62 | ```javascript 63 | let previousMD5 = ""; 64 | let watchWait = false; // 65 | 66 | fs.watch("./whatever", (type, filename) => { 67 | if (!filename || watchWait) { 68 | return; 69 | } 70 | 71 | // 72 | watchWait = setTimeout(() => { 73 | watchWait = false; 74 | }, 100); 75 | 76 | const md5 = crypto.createHash("md5"); 77 | const currentMD5 = md5 78 | .update(fs.readFileSync(filename).toString()) 79 | .digest("hex"); 80 | if (currentMD5 === previousMD5) { 81 | return; 82 | } 83 | 84 | previousMD5 = currentMD5; 85 | console.log(`${filename} is changed`); 86 | }); 87 | ``` 88 | 89 | ### 2.3 文件信息 90 | 91 | 对于常见的库来说,除了不信任原生 API、使用上述技巧外,很重要的是,**都根据 fs.Stats 类的信息,自定义逻辑来判断文件状态,以此保证不同平台兼容性**。 92 | 93 | 下面是在 Node10 中,打印的文件状态信息: 94 | 95 | ```sh 96 | Stats { 97 | dev: 16777222, 98 | mode: 33188, 99 | nlink: 1, 100 | uid: 501, 101 | gid: 20, 102 | rdev: 0, 103 | blksize: 4096, 104 | ino: 6493141, 105 | size: 7, 106 | blocks: 8, 107 | atimeMs: 1567516873292.676, 108 | mtimeMs: 1567516873293.3867, 109 | ctimeMs: 1567516873293.3867, 110 | birthtimeMs: 1566547653640.1763, 111 | atime: 2019-09-03T13:21:13.293Z, 112 | mtime: 2019-09-03T13:21:13.293Z, 113 | ctime: 2019-09-03T13:21:13.293Z, 114 | birthtime: 2019-08-23T08:07:33.640Z } 115 | ``` 116 | 117 | 通过文件信息的思路,就是在`fs.stat()`的回调函数中,进行逻辑处理: 118 | 119 | ```javascript 120 | // 判断文件是否写入完毕的操作 121 | function awaitWriteFinish() { 122 | // ...省略 123 | fs.stat( 124 | fullPath, 125 | function(err, curStat) { 126 | // ...省略 127 | 128 | if (prevStat && curStat.size != prevStat.size) { 129 | this._pendingWrites[path].lastChange = now; 130 | } 131 | 132 | if (now - this._pendingWrites[path].lastChange >= threshold) { 133 | delete this._pendingWrites[path]; 134 | awfEmit(null, curStat); 135 | } else { 136 | timeoutHandler = setTimeout( 137 | awaitWriteFinish.bind(this, curStat), 138 | this.options.awaitWriteFinish.pollInterval 139 | ); 140 | } 141 | }.bind(this) 142 | ); 143 | // ...省略 144 | } 145 | ``` 146 | 147 | ## 3. 成熟的库 148 | 149 | - [nodemon](https://github.com/remy/nodemon/) 150 | 151 | ## 参考链接 152 | 153 | - [精读《如何利用 Nodejs 监听文件夹》](https://segmentfault.com/a/1190000015159683) 154 | -------------------------------------------------------------------------------- /docs/NodeJS/05.更多/05.日志库的实现机制与优化方法.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "日志库的实现机制与优化方法" 3 | date: "2019-09-04" 4 | permalink: "2019-09-04-log-module" 5 | --- 6 | 7 | Keywords:堆栈、容器存储、Lazy Log、异步日志、缓存周期 8 | 9 | ## 概述 10 | 11 | 规范化的日志输出和存留,可以用来:**开发调试、行为留存、程序状态记录**。 12 | 13 | 对于日志,一般需要 4 个要素:**时间、级别、位置、内容、上下文信息**。对于集群或者多台机器来说,日志还需要区分不同机器的**唯一标识**。 14 | 15 | ## 基本原理:堆栈信息 16 | 17 | 自己封了个包,日志报错信息的格式为:`