-
Notifications
You must be signed in to change notification settings - Fork 0
/
content.json
1 lines (1 loc) · 125 KB
/
content.json
1
{"meta":{"title":"Kaviilee's blog","subtitle":null,"description":"唯有知识不离不弃","author":"kaviilee","url":"https://kaviilee.github.io/blog","root":"/blog/"},"pages":[{"title":"404 Not Found","date":"2023-01-02T18:31:29.271Z","updated":"2023-01-02T18:31:29.271Z","comments":true,"path":"404.html","permalink":"https://kaviilee.github.io/blog/404.html","excerpt":"","text":"404 很抱歉,您访问的页面不存在 可能是输入地址有误或该地址已被删除"},{"title":"关于","date":"2023-01-02T18:31:29.275Z","updated":"2023-01-02T18:31:29.275Z","comments":true,"path":"about/index.html","permalink":"https://kaviilee.github.io/blog/about/index.html","excerpt":"","text":"95后 在职码农 想回老家种田 _(:зゝ∠) _ 持续更新… 一点计划 小程序开发 全栈之路 Rust Go Css"},{"title":"所有分类","date":"2023-01-02T18:31:29.275Z","updated":"2023-01-02T18:31:29.275Z","comments":true,"path":"categories/index.html","permalink":"https://kaviilee.github.io/blog/categories/index.html","excerpt":"","text":""},{"title":"essays","date":"2019-09-07T15:08:39.000Z","updated":"2023-01-02T18:31:29.275Z","comments":false,"path":"essays/index.html","permalink":"https://kaviilee.github.io/blog/essays/index.html","excerpt":"","text":""},{"title":"我的朋友们","date":"2023-01-02T18:31:29.275Z","updated":"2023-01-02T18:31:29.275Z","comments":true,"path":"friends/index.html","permalink":"https://kaviilee.github.io/blog/friends/index.html","excerpt":"📢 友情链接","text":"📢 友情链接 📢 友链格式 博客名:Kaviilee 地址:https://kaviilee.github.io/blog/ 头像:https://cdn.jsdelivr.net/gh/Kaviilee/[email protected]/blog/images/custom/papalymo.jpg 简介:唯有知识不离不弃 友链不然分先后顺序 在下方留言板留言,格式如上 申请友链时记得先把本站挂为友链哦~"},{"title":"说说","date":"2023-01-02T18:31:29.275Z","updated":"2023-01-02T18:31:29.275Z","comments":true,"path":"shuoshuo/index.html","permalink":"https://kaviilee.github.io/blog/shuoshuo/index.html","excerpt":"","text":"new Artitalk({ appId: 'BreaVvx97UMIMsnq4bAaUDuG-MdYXbMMI', appKey: '2F8UD2LPgYvYQM9OOD5X4CJV' })"},{"title":"","date":"2023-01-02T18:31:29.275Z","updated":"2023-01-02T18:31:29.275Z","comments":true,"path":"mylist/index.html","permalink":"https://kaviilee.github.io/blog/mylist/index.html","excerpt":"","text":""},{"title":"所有标签","date":"2023-01-02T18:31:29.275Z","updated":"2023-01-02T18:31:29.275Z","comments":true,"path":"tags/index.html","permalink":"https://kaviilee.github.io/blog/tags/index.html","excerpt":"","text":""}],"posts":[{"title":"一些js小技巧","slug":"一些js小技巧","date":"2022-04-23T11:42:07.000Z","updated":"2023-01-02T18:31:29.271Z","comments":true,"path":"2022/04/23/一些js小技巧/","link":"","permalink":"https://kaviilee.github.io/blog/2022/04/23/%E4%B8%80%E4%BA%9Bjs%E5%B0%8F%E6%8A%80%E5%B7%A7/","excerpt":"记录一些js的小技巧。","text":"记录一些js的小技巧。 一些js小技巧操作URL查询参数const params = new URLSearchParams(location.search); // location.search = '?key=dd&value=kk'params.has('key'); // trueparams.get('value'); // kk 取整 代替正数的 Math.floor(),代替负数的 Math.ceil() const num1 = ~~2.3;const num2 = 2.3 | 0;const num3 = 2.3 >> 0; 短路运算const a = d && 1; // 满足条件赋值:取假运算,从左到右依次判断,遇到假值返回假值,后面不再执行,否则返回最后一个真值const b = d || 1; // 默认赋值:取真运算,从左到右依次判断,遇到真值返回真值,后面不再执行,否则返回最后一个假值const c = !d; // 取假赋值:单个表达式转换为true则返回false,否则返回true 判断空对象const obj = {};const flag = !Object.keys(obj).length; // true 满足条件时执行const flagA = true; // 条件Aconst flagB = false; // 条件B(flagA || flagB) && Func(); // 满足A或B时执行(flagA || !flagB) && Func(); // 满足A或不满足B时执行flagA && flagB && Func(); // 同时满足A和B时执行flagA && !flagB && Func(); // 满足A且不满足B时执行 克隆数组const arr = [1, 2, 3];const _arr = [...arr]; // [1, 2, 3] 合并数组const arr1 = [1, 2, 3];const arr2 = [4, 5, 6];const arr = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6] 数组去重const arr = [...new Set([1, 2, 1, 4, 6, null, null])]; // [1, 2 ,4, 6, null] 过滤空值const arr = [undefined, null, \"\", 0, false, NaN, 1, 2].filter(Boolean); // [1, 2] 统计成员个数const arr = [0, 1, 1, 2, 2, 2];const count = arr.reduce((t, v) => { t[v] = t[v] ? ++t[v] : 1; return t;}, {});// count => { 0: 1, 1: 2, 2: 3 } 按属性对Object分类const people = [ { name: 'Alice', age: 20 }, { name: 'Max', age: 21 }, { name: 'Tom', age: 20 }]const groupBy = (objectArr, property) => { return objectArr.reduce((acc, obj) => { const key = obj[property]; if (!acc[key]) { acc[key] = [] } acc[key].push(obj); return acc; }, {})}const groupedPeople = groupBy(people, 'age');/* { 20: [ { name: 'Max', age: 20 }, { name: 'Jane', age: 20 } ], 21: [{ name: 'Alice', age: 21 }]} */ 删除对象无用属性const obj = { a: 0, b: 1, c: 2 }; // 只想拿b和cconst { a, ...rest } = obj; // { b: 1, c: 2 }","categories":[],"tags":[{"name":"javascript","slug":"javascript","permalink":"https://kaviilee.github.io/blog/tags/javascript/"}]},{"title":"刷刷算法","slug":"刷刷算法","date":"2021-03-30T01:52:01.000Z","updated":"2023-01-02T18:31:29.271Z","comments":true,"path":"2021/03/30/刷刷算法/","link":"","permalink":"https://kaviilee.github.io/blog/2021/03/30/%E5%88%B7%E5%88%B7%E7%AE%97%E6%B3%95/","excerpt":"是算法耶!","text":"是算法耶! 实现 Trie (前缀树)Trie(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。 请你实现 Trie 类: Trie() 初始化前缀树对象。 void insert(String word) 向前缀树中插入字符串 word 。 boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。 boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。 class Trie { root: { [key: string]: any } constructor() { this.root = Object.create(null) } insert(word: string): void { let node = this.root; for (const w of word) { if (!node[w]) { node[w] = Object.create(null); } node = node[w] } node.isEnd = true } searchPrefix(word: string) { let node = this.root; for (const w of word) { node = node[w]; if (!node) return null } return node } search(word: string): boolean { let node = this.searchPrefix(word) return !!node && !!node.isEnd; } startWith(prefix: string): boolean { return !!this.searchPrefix(prefix) }} 实现 LRU 算法 class LRUCache { cache: Map<number, number>; capacity: number; constructor(capacity: number) { this.cache = new Map<number, number>() this.capacity = capcity; } put(key: number, value: number): void { if (this.cache.has(key)) { this.cache.delete(key); } else if (this.cache.size >= this.capacity) { this.cache.delete(this.cache.keys().next().value) } this.cache.set(key, value) } get(key: number): number { if (this.cache.has(key)) { const val = this.cache.get(key); this.cache.delete(key); this.cache.set(key, val); return val } return -1 }}","categories":[],"tags":[{"name":"算法","slug":"算法","permalink":"https://kaviilee.github.io/blog/tags/%E7%AE%97%E6%B3%95/"}]},{"title":"凉经","slug":"凉经","date":"2021-03-24T15:31:09.000Z","updated":"2023-01-02T18:31:29.271Z","comments":true,"path":"2021/03/24/凉经/","link":"","permalink":"https://kaviilee.github.io/blog/2021/03/24/%E5%87%89%E7%BB%8F/","excerpt":"记录一下自己 “被打” 的过程😂","text":"记录一下自己 “被打” 的过程😂 面试记录2021.2.28 从前公司离职,开始了复习知识和面试的日子。记录一下自己面试的经历以及题目,常看常新,补全知识。 富途一面 Vue 组件间通信 如果是父子组件,那么可以直接通过 props 进行通信,也可以使用 provide/inject Event Bus 通过新建一个 Vue 实例使两个组件能够进行通信 Vuex 拓展:Vuex 和 Vue Router 在 install 的时候发生了什么?(其实是 Vue 插件机制)其实 Vue 通过 use() 安装插件,就是通过 mixin 混入了方法。Vuex 和 Router 都是通过在 beforeCreate 这个生命周期里把 $router 和 store 挂载到了 Vue 的实例上。 Vue2 和 Vue3 的生命周期和区别 Vue 2.x Vue 3.x Details beforeCreate beforeCreate 在实例初始化之后,数据观测和 event/watcher 事件配置之前被调用 created created 在实例创建完成之后立即被调用。$el property 不可用 beforeMount beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用 mounted mounted 实例被挂载后调用,此时可以使用 nextTick 和进行 DOM 操作 beforeUpdate beforeUpdate 数据更新时调用,发生在虚拟 DOM 打补丁之前。这里适合在更新前访问现有 DOM,手动移除事件监听器 updated updated 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。 activated activated 被 keep-alive 缓存的组件激活时调用 deactivated deactivated 被 keep-alive 缓存的组件停用时停用 beforeDestroy beforeUmount 在销毁(卸载)组件实例之前调用。此时实例还是完全正常的 destroyed unmounted 实例销毁(卸载)后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。 errorCaptured errorCaptured 当捕获一个来自子孙组件的错误时被调用。 - renderTracked 跟踪虚拟 DOM 重新渲染时调用。钩子接收 debugger event 作为参数。此事件告诉你哪个操作跟踪了组件以及该操作的目标对象和键。 - renderTriggered 当虚拟 DOM 重新渲染为 triggered.Similarly 为 renderTracked,接收 debugger event 作为参数。此事件告诉你是什么操作触发了重新渲染,以及该操作的目标对象和键。 拓展1: Vue 父子组件的渲染顺序 父beforeCreate => 父created => 父beforeMount => 子beforeCreate => 子created => 子beforeMount => 子mounted => 父mounted 拓展2: 子组件更新过程 父beforeUpdate => 子beforeUpdate => 子updated => 父updated 拓展3: 销毁过程 父beforeDestroy => 子beforeDestroy => 子destroyed => 父destroyed 浏览器渲染 浏览器渲染进程主要有 GUI 渲染线程,JS 引擎线程,事件触发线程,定时触发器线程,异步 HTTP 请求线程。 其中 GUI 渲染线程和 JS 引擎线程是互斥的。GUI 渲染线程负责渲染浏览器界面,解析 HTML 和 CSS,在负责处理 JavaScript 脚本程序的 JS 引擎线程执行时,GUI 渲染线程会暂时挂起,直到 JS 引擎空闲。 延伸1: 浏览器工作流程: 构建 DOM => 构建 CSSOM => 构建渲染树 => 布局 => 绘制。 CSSOM 会阻塞渲染,只有当 CSSOM 构建完毕之后才会进入下一阶段的构建树。 通常情况下 DOM 和 CSSOM 是并行构建的,但是当浏览器遇到 script 标签时,DOM 构建将暂停,直到脚本执行完成。但由于 JavaScript 可以修改 CSSOM,所以需要等 CSSOM 构建完毕后再执行 JS。 所以如果想要首屏渲染时间缩短,就需要把 script 标签放在 body 标签底部,或者直接不要在首屏加载 JS 文件。 浏览器缓存 基本的网络请求就是三个步骤:请求,处理,响应。后端主要就是”处理”这个步骤,所以其实前端缓存就在“请求”和“响应”中进行。 当浏览器要请求资源时: 调用 Service Worker 的 fetch 事件 查看 memory cache 查看 disk cache: 如果有强制缓存且未失效,则使用强制缓存,不请求服务器。状态码全部是200 如果有强制缓存但已失效,使用对比缓存,比较后确定是304还是200 发送网络请求,等待响应 把响应内容存入 disk cache(如果 HTTP 头信息配置可以缓存) 把响应内容的引用存入 memory cache(无视 HTTP 头信息的配置) 把响应内容存入 Service Worker 的 Cache Storage this 指向问题 可以从 Reference 类型来判断 this 的指向。 首先判断出 MemberExpression 赋值给 ref,然后判断 ref 是不是一个 Reference 类型。 Reference = {// 属性所在的对象或 EnvironmentRecord,它的值只可能是 undefined, Object, Boolean, String, Number, environment recordbase,// 属性名name,// 是否为 strict 模式strict}// 举个栗子var value = 1;var foo = { value: 2, bar: function () { return this.value; }}console.log(foo.bar()); // 2/* MemberExpression 为 foo.bar Renference = { base: foo, name: 'bar', strict: false } 所以 this = foo,该表达式输出 2.*/// 有操作符,逻辑运算符或逗号运算符,ref 的就不是 Reference类型,this 就是 undefined,最终的 this 会隐式转换为全局对象,所以该表达式输出 1.console.log((foo.bar = foo.bar)()); // 1console.log((foo.bar || foo.bar)()); // 1console.log((foo.bar, foo.bar)()); // 1 拓展1: 将 foo.bar 改成 箭头函数会怎样? 拓展2: 将 var 换成 let 或 const 又会怎样? 箭头函数和普通函数的区别 箭头函数不会创建自己的 this,它只会从自己的作用域链的上一层继承 this。 箭头函数没有自己的 this,它会捕获定义时自己所处的外层执行环境的 this,并继承这个 this。 箭头函数继承来的 this 指向永远不变 call,apply,bind 无法改变箭头函数中 this 的指向 箭头函数不能作为构造函数使用 箭头函数没有自己的 arguments 对象 箭头函数没有原型 prototype 箭头函数不能用作 Generator 函数,不能使用 yeild 关键字 斐波那契函数,以及缓存优化 // 动态规划function fib1(n: number): number { let a = 0, b = 1, sum; for (let i = 0; i < n; i++) { sum = (a + b) % 1000000007; a = b; b = sum; } return a; // 花里胡哨 let a = 0, b= 1; while(n--) { [a, b] = [b, (a + b) % 1000000007]; } return a}// 缓存 闭包function fib2(n: number): number { const map = new Map<number, number>(); function sub(n: number): number { if (n < 2) { return n } if (map.get(n)) { return map.get(n) } let sum = sub(n - 1) + sub(n - 2); map.set(n, sum); return sum % 1000000007; } return sub(n)} Vue 数据量大的时候,如何优化 按需加载局部数据,虚拟列表,无限下拉刷新(可视区域) 大量纯展示的数据,可以使用 Object.freeze 冻结 还可以进行分页 图片网站 优化 首先我们分析一下图片加载存在的问题和原因: 启动页面时加载过多图片 首屏图片优先加载,等首屏图片加载完成再去加载非首屏图片 可以进行域名切分,来提升并发的请求数量,或者使用 HTTP/2 协议 部分图片体积过大 单位像素优化 - 改变图片格式,使用 webp 格式 图片像素总数优化 - 裁剪大图片,减少图片体积,减少网络开销,加快下载速率 防抖节流 // 防抖 触发高频事件 n 秒内只会执行一次函数,n 秒内事件再次触发,则重新计算时间 如果你在期间一直触发,则不会触发function debounce(fn, ms) { let timeout = null; return function() { clearTimeout(timeout); timeout = setTimeout(() => { fn.apply(this, arguments) }, ms) }}// 节流 触发高频事件,但在 n 秒内只会执行一次,所以节流会稀释函数的执行效率 一定会触发function throttle(fn, ms) { let timeout; return function() { if (!timeout) { timeout = setTimeout(() => { timeout = null fn.apply(this, arguments) }, ms) } }} [事件循环机制](详解JavaScript中的Event Loop(事件循环)机制 - 知乎 (zhihu.com) var,let,const 的区别 let/const 是使用块级作用域;var 使用的是函数作用域,且存在变量提升。 在函数外部使用 var 进行声明都会自动成为 window 对象上的一个属性。 暂时性死区(TDZ) 只要块级作用域内存在 let 命令,它所声明的变量就”绑定”这个区域,不再受外部的影响。 ES6 中规定,如果区块中存在 let 和 const 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明前使用这些变量,就会报错。 闭包 能够访问自由变量的函数被称为闭包。 自由变量:在函数中使用的既不是函数参数也不是局部变量的变量。 HTTPS 和 HTTP 的区别 HTTPS 是 HTTP 协议的安全版本,HTTP 协议的数据传输是明文的,是不安全的,HTTPS 使用了 SSL/TLS 协议进行加密处理。 HTTP 和 HTTPS 使用的链接方式不同,默认端口也不一样,前者是80,后者是443. 引申: 对称加密和非对称加密 使用不同密钥进行加密和解密就是非对称加密,相对的加密解密使用同一密钥的就是对称加密。 HTTP2.0 和 HTTP1.1 的区别?多个请求在这两个协议之间的区别?如果请求的是不同主域,那么区别在哪? HTTP2.0 多路复用 header 压缩 服务端推送 HTTP1.1 keepalive 可以让 HTTP 重用 TCP 链接,就是所谓的长链接。 HTTP2.0 会采用多路复用,一次 TCP 连接即可,采用 HTTP 的话,如果没有设置 keepalive 那么一次请求就要建立一个 TCP 连接。 探迹一面 事件机制 事件循环 Vue 响应式原理 react 优化 setState 数组去重 深浅拷贝 get post 区别 前端性能优化 call的实现 node require node 怎么读文件","categories":[],"tags":[{"name":"面经","slug":"面经","permalink":"https://kaviilee.github.io/blog/tags/%E9%9D%A2%E7%BB%8F/"}]},{"title":"修改 console.log 的颜色","slug":"修改console的颜色","date":"2021-03-22T16:49:05.000Z","updated":"2023-01-02T18:31:29.271Z","comments":true,"path":"2021/03/22/修改console的颜色/","link":"","permalink":"https://kaviilee.github.io/blog/2021/03/22/%E4%BF%AE%E6%94%B9console%E7%9A%84%E9%A2%9C%E8%89%B2/","excerpt":"今日用 node 写一个小工具的时候,想要 console 输出不一样的颜色,又不想而外引入工具库,遂查找资料,记录一下。","text":"今日用 node 写一个小工具的时候,想要 console 输出不一样的颜色,又不想而外引入工具库,遂查找资料,记录一下。 不使用库console.log(fg, bg, content, \"\\x1b[0m\"); 这里 fg 其实就是字体颜色,bg 对应背景颜色,content 就是要输出的内容 以下列出其他的颜色 Reset = "\\x1b[0m"Bright = "\\x1b[1m"Dim = "\\x1b[2m"Underscore = "\\x1b[4m"Blink = "\\x1b[5m"Reverse = "\\x1b[7m"Hidden = "\\x1b[8m"FgBlack = "\\x1b[30m"FgRed = "\\x1b[31m"FgGreen = "\\x1b[32m"FgYellow = "\\x1b[33m"FgBlue = "\\x1b[34m"FgMagenta = "\\x1b[35m"FgCyan = "\\x1b[36m"FgWhite = "\\x1b[37m"BgBlack = "\\x1b[40m"BgRed = "\\x1b[41m"BgGreen = "\\x1b[42m"BgYellow = "\\x1b[43m"BgBlue = "\\x1b[44m"BgMagenta = "\\x1b[45m"BgCyan = "\\x1b[46m"BgWhite = "\\x1b[47m" Fgxx 对应的是字体颜色,Bgxx 对应的是背景颜色。 当然我们可以封装一下,使调用更加便捷一点 const colors = { reset: \"\\x1b[0m\", bright: \"\\x1b[1m\", dim: \"\\x1b[2m\", underscore: \"\\x1b[4m\", blink: \"\\x1b[5m\", reverse: \"\\x1b[7m\", hidden: \"\\x1b[8m\", fg: { black: \"\\x1b[30m\", red: \"\\x1b[31m\", green: \"\\x1b[32m\", yellow: \"\\x1b[33m\", blue: \"\\x1b[34m\", magenta: \"\\x1b[35m\", cyan: \"\\x1b[36m\", white: \"\\x1b[37m\", }, bg: { black: \"\\x1b[40m\", red: \"\\x1b[41m\", green: \"\\x1b[42m\", yellow: \"\\x1b[43m\", blue: \"\\x1b[44m\", magenta: \"\\x1b[45m\", cyan: \"\\x1b[46m\", white: \"\\x1b[47m\", }} 使用库修改 console 颜色的库有 chalk colors cli-color 碎碎念好看的 console.log 可以让写代码的我快乐(๑•̀ㅂ•́)و✧","categories":[],"tags":[{"name":"其他","slug":"其他","permalink":"https://kaviilee.github.io/blog/tags/%E5%85%B6%E4%BB%96/"}]},{"title":"webpack 常用 loaders 和 plugins","slug":"webpack常用loaders和plugins","date":"2021-03-20T21:27:36.000Z","updated":"2023-01-02T18:31:29.271Z","comments":true,"path":"2021/03/20/webpack常用loaders和plugins/","link":"","permalink":"https://kaviilee.github.io/blog/2021/03/20/webpack%E5%B8%B8%E7%94%A8loaders%E5%92%8Cplugins/","excerpt":"记录一下 webpack 常用 loaders 和 plugins","text":"记录一下 webpack 常用 loaders 和 plugins 常用 Loaders加载文件 raw-loader:把文本文件的内容加载到代码中去。 file-loader:把文本输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件。 url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去。 source-map-loader:加载额外的 Source Map 文件,以方便断点调试。 svg-inline-loader:把压缩后的 SVG 内容注入到代码中。 node-loader:加载 Node.js 原生模块 .node 文件。 image-loader:加载并且压缩图片文件。 json-loader:加载 JSON 文件。 yaml-loader:加载 YAML 文件。 编译模板 pug-loader:把 Pug 模版转换成 JavaScript 函数返回。 ejs-loader:把 EJS 模版编译成函数返回。 markdown-loader:把 Markdown 文件转换成 HTML。 转换脚本语言 babel-loader:把 ES6 转换成 ES5。 awesome-typescript-loader:把 TypeScript 转换成 JavaScript,性能要比 ts-loader 好。 coffee-loader:把 CoffeeScript 转换成 JavaScript。 转换样式文件 css-loader:加载 CSS,支持模块化、压缩、文件导入等特性。 style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。 sass-loader:把 SCSS/SASS 代码转换成 CSS。 postcss-loader:扩展 CSS 语法,使用下一代 CSS。 less-loader:把 Less 代码转换成 CSS 代码。 stylus-loader:把 Stylus 代码转换成 CSS 代码。 检查代码 eslint-loader:通过 ESLint 检查 JavaScript 代码。 tslint-loader:通过 TSLint 检查 TypeScript 代码。 mocha-loader:加载 Mocha 测试用例代码。 其他 vue-loader:加载 Vue.js 单文件组件。 常用 Plugins commons-chunk-plugin:提取公共代码,在4-11提取公共代码中有介绍。 extract-text-webpack-plugin:提取 JavaScript 中的 CSS 代码到单独的文件中。 prepack-webpack-plugin:通过 Facebook 的 Prepack 优化输出的 JavaScript 代码性能。 uglifyjs-webpack-plugin:通过 UglifyES 压缩 ES6 代码。 webpack-parallel-uglify-plugin:多进程执行 UglifyJS 代码压缩,提升构建速度。 happypack:多进程处理任务。 ModuleConcatenationPlugin:开启 Webpack Scope Hoisting 功能。","categories":[{"name":"前端工程化","slug":"前端工程化","permalink":"https://kaviilee.github.io/blog/categories/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/"}],"tags":[{"name":"webpack","slug":"webpack","permalink":"https://kaviilee.github.io/blog/tags/webpack/"}]},{"title":"webpack.config 整体结构","slug":"webpack-config整体结构","date":"2021-03-20T17:26:19.000Z","updated":"2023-01-02T18:31:29.271Z","comments":true,"path":"2021/03/20/webpack-config整体结构/","link":"","permalink":"https://kaviilee.github.io/blog/2021/03/20/webpack-config%E6%95%B4%E4%BD%93%E7%BB%93%E6%9E%84/","excerpt":"记录一下 webpack 配置对应的意义,便于自己查阅~","text":"记录一下 webpack 配置对应的意义,便于自己查阅~ const path = require('path')module.exports = { // entry 表示入口,webpack 执行构建的第一步从 entry 开始 // 类型可以是 String | Object | Array entry: 'index.js', // 只有一个入口, 入口只有一个文件 entry: ['index1.js', 'index2.js'], // 只有一个入口,入口有2个文件 entry: { // 有两个入口 a: 'indexA.js', b: ['indexB1.js', 'indexB2.js'] }, // 如何输出结果:在 webpack 的一系列处理之后,如何输出最终的代码 output: { // 输出文件的存放目录,必须是 String 类型的绝对路径 path: path.resolve(__dirname, 'dist'), // 输出文件的名称 filename: 'bundle.js', // 完整的名称 // 当配置了多个 entry 时,通过名称模板为不同的 entry 生成不同的文件名称 filename: '[name].js', // 根据文件内容 hash 值生成文件名称,用于浏览器长时间缓存文件 filename: '[chunkhash].js', // 发布到线上的所有资源的 URL 前缀,String 类型 publicPath: '/public', // 放到指定目录下 publicPath: '', // 放到当前目录下 publicPath: 'https://cdn.jsdelivr.net/', // 放到 CDN 上 // 导出库的名称 String 类型 // 缺省时,默认输出格式是匿名的立即执行函数 library: 'MyLibrary', // 导出库的类型,枚举类型 默认是 var // 可以是 umd | umd2 | commonjs2 | commonjs | amd | this | var | assign // | global | jsonp libraryTarget: 'umd', // 是否包含有用的文件路径信息到生成的代码中 Boolean 类型 pathinfo: true, // 附加 Chunk 的文件名称 chunkFilename: '[id].js', chunkFilename: '[chunkhash].js', // JSONP 异步加载资源时的回调函数名称,需要和服务端配合使用 jsonpFunction: 'webpackJsonp', // 生成的 Source Map 文件名称 sourceMapFilename: '[file].map', // 浏览器开发者工具里显示的源码模块名称 devtoolModuleFilenameTemplate: 'webpack:///[resource-path]', // 异步加载跨域的资源时使用的方式 crossOriginLoading: 'use-credentials', crossOriginLoading: 'anonymous', crossOriginLoading: false, }, // 配置模块相关 module: { rules: [ // loader 配置 { test: /\\.jsx?$/, // 正则匹配命中要使用 loader 的文件 include: [ // 只会名字这里的文件 path.resolve(__dirname, 'src'), ], exclude: [ // 忽略这里的文件 /node_modules/ ], use: [ // 使用哪些 loader,从右到左,从后到前执行 'style-loader', { loader: 'css-loader', options: {} } ] } ], noParse: [ // 不用解析和处理的模块 /special-library\\.js$/ // 正则匹配 ] }, // 配置插件 plugins: {}, // 配置寻找模块的规则 resolve: { modules: [ // 寻找模块的根目录,Array 类型,默认根目录为 node_modules 'node_modules', path.resolve(__dirname, 'src') ], extensions: ['.js', 'json', 'jsx', '.css'], // 模块的后缀名 alias: { // 模块别名配置,用于映射模块 // 把 '@' 映射为 'src','@/index'会被映射为 'src/index' '@': path.resolve(__dirname, 'src'), // 使用结尾符号 $ 后,把 'only-module' 映射为 'new-module' // 但不会把 'only-module/index' 映射为 'new-module/index' 'only-module$': 'new-module' }, alias: [ // 支持数组来配置 { name: 'module', // 老的模块 alias: 'new-module', // 新的模块 // 是否只映射模块,对应上面是否有 $ onlyModule: true } ], symlinks: true, // 是否跟随文件软链接去搜寻模块的路径 descriptionFiles: ['package.json'], // 模块的描述文件 mainFields: ['main'], // 模块的描述文件里的描述入口的文件的字段 enforceExtension: false, // 是否强制导入语句必须要写明文件后缀 }, // 输出文件性能检查配置 performance: { hints: 'warning', // 有性能问题时输出 'warning' 警告 'error' 错误 false 关闭 maxAssetSize: 200000, // 最大文件大小 bytes maxEntrypointSize: 400000, // 最大入口文件大小 bytes assetFilter: function (assetFilename) { return assetFilename.endWith('.css') || assetFilename.endWith('.js') } }, devtool: 'source-map', // 配置 source-map 类型 context: __dirname, // webpack 使用的根目录,String 类型 必须是绝对路径 // 配置输出代码的运行环境 target: 'web', // 浏览器,默认 target: 'webworker', // WebWorker target: 'node', // Node.js,使用 `require` 语句加载 Chunk 代码 target: 'async-node', // Node.js,异步加载 Chunk 代码 target: 'node-webkit', // nw.js target: 'electron-main', // electron, 主线程 target: 'electron-renderer', // electron, 渲染线程 // 使用来自 JavaScript 运行环境提供的全局编写 externals: { jquery: 'jQuery' }, // 控制台输出日志控制 stats: { assets: true, colors: true, errors: true, errorDetails: true, hash: true, }, // DevServer 相关配置 devServer: { proxy: { // 代理到后端服务接口 '/api': 'http://localhost:3000' }, // 配置 DevServer HTTP 服务器的文件目录 contentBase: path.join(__dirname, 'public'), compress: true, // 是否开启 gzip 压缩 historyApiFallback: true, // 是否开发 HTML5 History API 网页 hot: true, // 是否开启模块热替换功能 https: false, // 是否开启 HTTPS 模式 }, profile: true, // 是否捕捉 Webpack 构建的性能信息,用于分析什么原因导致构建性能不佳 cache: false, // 是否开启缓存提升构建速度 watch: true, // 是否开启监听模式 watchOptions: { // 监听模式选项 // 不监听的文件或文件夹,支持正则,默认为空 ignored: /node_modules/, // 监听到变化发生后会等300ms再去执行动作,防止文件更新太快导致重新编译频率太高 // 默认为300ms aggregateTimeout: 300, // 判断文件是否发生变化是不停的去询问系统指定文件有没有变化,默认每隔1000毫秒询问一次 poll: 1000 }}","categories":[{"name":"前端工程化","slug":"前端工程化","permalink":"https://kaviilee.github.io/blog/categories/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/"}],"tags":[{"name":"webpack","slug":"webpack","permalink":"https://kaviilee.github.io/blog/tags/webpack/"}]},{"title":"一些TS中的运算符","slug":"一些ts中的运算符","date":"2021-01-28T17:59:30.000Z","updated":"2023-01-02T18:31:29.271Z","comments":true,"path":"2021/01/28/一些ts中的运算符/","link":"","permalink":"https://kaviilee.github.io/blog/2021/01/28/%E4%B8%80%E4%BA%9Bts%E4%B8%AD%E7%9A%84%E8%BF%90%E7%AE%97%E7%AC%A6/","excerpt":"近来用到了不少的 TypeScript 的运算符,但是也有很多是没用过的,本着学到就是赚到的想法,记录一下。","text":"近来用到了不少的 TypeScript 的运算符,但是也有很多是没用过的,本着学到就是赚到的想法,记录一下。 非空断言操作符 !! 是一个后缀表达式操作符,可以用于断言操作对象是非 null 和非 undefined 类型。 const func = (str: string | null | undefined) => { // Type 'string | null | undefined' is not assignable to type 'string'. // Type 'undefined' is not assignable to type 'string'.(2322) let onlyStr: string = str; // error let ignoredNullOrUndefined: string = str!; // ok} ?. 运算符?. 可选链,在我们遇到可能是 null 或 undefined 时就可以停止表达式的运行。 const a = b?.c 上述 TypeScript 代码在编译之后 const a = b === null || b === void 0 ? void 0 : b.c; 所以使用 ?. 运算符就可以自动检查对象 b 是否为 null 或 undefined,如果是就返回 undefined。使用 ?. 可以代替 && 执行空检查。 const a = b && b.c// equal toconst a = b?.c 值得注意的是,?. 只检查 null 和 undefined,对于空字串和 0,它是不会检查的。 可选链还可以和函数调用一起使用 const res = obj.func?.() 编译之后 const res = (_a = obj.func) === null || _a === void 0 ? void 0 : _a.call(obj); 如果 obj 不存在 func 这个方法,就回返回 undefined. 注意事项: 如果 func 不存在于 obj,上面代码会产生一个 TypeError 异常 可选链的运算行为被局限在属性的访问,调用和元素的访问,不会延伸到后续的表达式,也就是说可选调用不会阻止 a?.b / someMethod() 表达式中的除法运算或 someMethod 的方法调用 ?? 空值合并运算符 当左侧操作数为 null 或 undefined时,返回右侧的操作数,否则返回左边的操作符 与 || 运算符不同,?? 也是只判断 null 和 undefined,其他的 falsy 它是不会返回右侧的。 ?: 可选属性这个运算符一般用在定义类型的时候 interface Person { age?: number; // equal to // age: number | undefined} Partial把某个接口的属性变成可选属性 interface Person { name: string; age: number;}/* type CopyPerson = { name?: string; age?: number;} */type CopyPerson = Partial<Person> Required使用 Required 可以把可选属性转为必选属性。 & 运算符在 TypeScript 中使用 & 运算符可以将多个类型叠加成为一种类型。 同名基础类型合并,如果存在相同的成员,那么该成员的类型会变成 never 同名非基础类型合并,是可以合并的 | 分隔符 在 TypeScript 中联合类型(Union Types)表示取值可以为多种类型中的一种,联合类型使用 | 分隔每个类型. const test: string | undefined; 表示 test 可以是 string 或 undefined. RecordRecord<K extends keyof any, T> 将 K 中所有的属性的值转化为 T 类型。 type Record<K extends keyof any, T> = { [P in K]: T}// exampletype Route = 'home' | 'demo' | 'help';const route: Record<Route, string> = { home: string; demo: string; gelp: string;} PickPick<T, K extends keyof T> 将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型。 type Pick<T, K extends keyof T> = { [P in K]: T[P]}// exampleinterface Todo { title: string; description: string; status: boolean;}type TodoPreview = Pick<Todo, 'title' | 'status'>;const todo: TodoPreview = { title: 'todo1', status: false} EXcludeExclude<T, U> 将某个类型中的另一个类型移除。 // 如果 T 能够赋值给 U,那么就返回 never 类型,否则返回 T 类型,其实就是将 T 中属于 U 的类型移除。type Exclude<T, U> = T extends U ? : never : T// exampletype Demo = Exclude<'a' | 'b' | 'c', 'c'>; // 'a' | 'b' ReturnTypeReturnType<T> 用于获取函数 T 的返回类型。 type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;// exampletype Demo = ReturnType<() => string>; // string","categories":[],"tags":[{"name":"TypeScript","slug":"typescript","permalink":"https://kaviilee.github.io/blog/tags/typescript/"}]},{"title":"浏览器后退事件踩坑","slug":"浏览器后退事件踩坑","date":"2021-01-20T10:43:29.000Z","updated":"2023-01-02T18:31:29.271Z","comments":true,"path":"2021/01/20/浏览器后退事件踩坑/","link":"","permalink":"https://kaviilee.github.io/blog/2021/01/20/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%90%8E%E9%80%80%E4%BA%8B%E4%BB%B6%E8%B8%A9%E5%9D%91/","excerpt":"这是一次关于浏览器后退事件的踩坑记录。","text":"这是一次关于浏览器后退事件的踩坑记录。 背景页面需要在打开一个 Drawer 的情况下,用户点击浏览器的后退按钮之后,将 Drawer 关闭,而不是页面发生了后退。 解决方案思路并不是要阻止浏览器触发后退事件,而是在浏览器触发后退事件前,将预定的 history 信息加入到浏览器的路由信息中,后退时,是后退到了指定的页面,然后在后退事件中加入自己的处理。 一点点代码利用 history.pushState() 方法在浏览器的历史堆栈中添加一个 state。然后再监听 popstate 事件,加入自己的处理。 const handleOnpopState = () => { // your code Drawer.close()}window.history.pushState(null, '', document.URL);// mountedwindow.addEventListener('popstate', handleOnpopState, false)// unmountwindow.removeEventListener('popstate', handleOnpopState, false) 注意点当网页加载时,各浏览器对popstate事件是否触发有不同的表现,Chrome 和 Safari会触发popstate事件, 而Firefox不会.","categories":[],"tags":[]},{"title":"关于Antd框架下Cascader动态加载选项的问题","slug":"关于antd框架下cascader动态加载选项的问题","date":"2021-01-13T18:07:43.000Z","updated":"2023-01-02T18:31:29.271Z","comments":true,"path":"2021/01/13/关于antd框架下cascader动态加载选项的问题/","link":"","permalink":"https://kaviilee.github.io/blog/2021/01/13/%E5%85%B3%E4%BA%8Eantd%E6%A1%86%E6%9E%B6%E4%B8%8Bcascader%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD%E9%80%89%E9%A1%B9%E7%9A%84%E9%97%AE%E9%A2%98/","excerpt":"近日在使用 Ant Design 的 Cascader 初始化传值遇到的问题,记录一下。","text":"近日在使用 Ant Design 的 Cascader 初始化传值遇到的问题,记录一下。 问题描述在使用 Cascader 组件的动态加载数据时,由于需要传入初始值来初始化当前的选择,但是此时组件内并不存在对应的数据。假设我们使用一个 Number 类型的字段来作为 Cascader 的 value 值,那么如果贸然传入数字类型的数组,就会出现初始化不正常的问题。 解决方案既然不能简单的传入数字类型的数组来初始化组件,那么我们可以传入对象数组或者是字符串数组,利用 Cascader 的 displayRender 属性来渲染。 并利用 fetched 来控制是否已经进行异步加载,当进行异步加载之后,渲染就变为选择的值。 具体可以查看 动态加载选项初始化组件","categories":[],"tags":[{"name":"Antd v4","slug":"antd-v4","permalink":"https://kaviilee.github.io/blog/tags/antd-v4/"}]},{"title":"浅尝 deno","slug":"浅尝deno","date":"2021-01-05T00:01:36.000Z","updated":"2023-01-02T18:31:29.271Z","comments":true,"path":"2021/01/05/浅尝deno/","link":"","permalink":"https://kaviilee.github.io/blog/2021/01/05/%E6%B5%85%E5%B0%9Ddeno/","excerpt":"新的一年新的开始!新的东西,学起!(扶朕起来,朕还可以再学","text":"新的一年新的开始!新的东西,学起!(扶朕起来,朕还可以再学 它是 Node.js 的替代品。有了它,将来可能就不需要 Node.js 了。 ———— 阮一峰《Deno 运行时入门教程:Node.js 的替代品》 Deno 简介Deno 是一个跨平台的运行时,即基于 Google V8 引擎的运行时环境,该运行时环境是使用 Rust 语言开发的,并使用 Tokio 库来构建事件循环系统。Deno 有以下优点: 默认安全。除非显式启用,否则不能访问文件,网络或环境 开箱即用的 TypeScript 支持 只分发一个可执行文件 具有内置的工具箱,比如依赖项检查工具(deno info)和 代码格式化工具(deno fmt) 拥有一组保证能够与 Deno 一起使用的经过审查的标准模块:deno.land/std 脚本可以打包到一个 JavaScript 文件中 一些命令执行 deno -h 或 deno --help 就能够显示 Deno 支持的子命令,以下列举一些主要使用的命令 deno bundle:deno bundle [options] [out_file] 将脚本和依赖打包 deno fmt:代码格式化 deno info:显示本地的依赖缓存 deno install:将脚本安装为可执行文件 deno repl:进入 REPL 环境 deno run:运行脚本 与 Node.js 的区别Deno 既然是 Node.js 的 ”替代品“,究竟解决了 Node.js 的哪些痛点呢? 模块引入的方式Node 模块引入Node 内置 API 通过模块引入的方式引入。 const path = require('fs')fs.readFileSync('demo.txt') Deno 全局对象在 Deno 中则是一个全局对象 Deno 的属性和方法 Deno.readFileSync('demo.txt') 模块系统在模块系统方面,这是 Deno 和 Node.js 差别最大的地方。 Node.js CommonJS 规范Node.js 采用的是 CommonJS 模块规范。具体规范见 CommonJS 规范 const fs = require('fs')module.exports = function() { console.log('demo')} Deno 的模块规范不同于 Node.js 的规范,Deno 所采用的模块规范是 ES Module 规范 import as fs from \"https//deno.land/std/fs/mod.ts\"import { Application } from \"./initApp.js\"import creatApp from \"./creatApp.ts\" 在 Deno 中,可以直接 import url 来引用线上的资源,并且资源的扩展名和文件名不可以省略。 安全这个安全主要体现在 Deno 的操作很多都需要提供权限,比如: // demo.tsconst file = await Deno.creat(\"./bar.txt\")console.log(`create file: bar.txt`) 我们执行这段代码 $ deno run .\\demo.tsCheck file:///C:/Users/lee/Desktop/demo.tserror: Uncaught (in promise) PermissionDenied: read access to \"bar.txt\", run again with the --allow-read flag at processResponse (deno:core/core.js:223:11) at Object.jsonOpAsync (deno:core/core.js:240:12) at async open (deno:runtime/js/30_files.js:44:17) at async file:///C:/Users/lee/Desktop/demo.ts:1:14 这会告诉我们没有给予它 --allow-read 的权限,当我们给予它权限后,就能成功创建文件 $ deno run --allow-read --allow-write .\\demo.tscreate file: bar.txt Deno 的权限列表 -A, --allow-all 允许所有权限,这将禁用所有安全限制。 --allow-env 允许环境访问,例如读取和设置环境变量。 --allow-hrtime 允许高精度时间测量,高精度时间能够在计时攻击和特征识别中使用。 --allow-net=<allow-net> 允许网络访问。您可以指定一系列用逗号分隔的域名,来提供域名白名单。 --allow-plugin 允许加载插件。请注意:这是一个不稳定功能。 --allow-read=<allow-read> 允许读取文件系统。您可以指定一系列用逗号分隔的目录或文件,来提供文件系统白名单。 --allow-run 允许运行子进程。请注意,子进程不在沙箱中运行,因此没有与 deno 进程相同的安全限制,请谨慎使用。 --allow-write=<allow-write> 允许写入文件系统。您可以指定一系列用逗号分隔的目录或文件,来提供文件系统白名单。 这里插一句不属于权限但是属于 run 的时候可以传入的选项 --unstable,传递这个选项将允许在运行时使用不稳定的 API。 白名单Deno 还可以设置白名单来控制权限的粒度。 deno run --allow-read=/etc https://deno.land/std@$STD_VERSION/examples/main.ts /etc/passwd 可以使得文件系统能够访问 /etc 目录。 支持 TypeScriptDeno 原生支持 TS,这点对于一个 TS 使用者来说实在是太赞了。Node.js 中要使用 TypeScript 的话还需要 ts-node 之类的工具的支持。 无 node_modules不同于 Node.js,Deno 并没有 node_modules 来进行包管理。但是 Deno 在每次初执行时,会进行依赖的下载和编译,我们可以通过 deno info 来查看 $ deno info [file_name]DENO_DIR location: \"C:\\\\Users\\\\lee\\\\AppData\\\\Local\\\\deno\"Remote modules cache: \"C:\\\\Users\\\\lee\\\\AppData\\\\Local\\\\deno\\\\deps\"TypeScript compiler cache: \"C:\\\\Users\\\\lee\\\\AppData\\\\Local\\\\deno\\\\gen\" 其实这就相当于是进行了包管理,编译后的文件,远程模块缓存都会放在相应的位置。 异步操作Node 用回调的方式处理异步操作,而 Deno 则使用的是 Promise // nodeconst fs = require('fs')fs.readFile('./demo.txt', (err, data) => { if (err) { throw(err) } console.log(data)})// denoconst data = await Deno.readFile('./demo.txt')console.log(data) 这是实战 项目采用的框架是 oak,应该是对应了 Node.js 的 Koa,如果之前使用过 Koa 的话,使用 oak 这个框架应该会很熟悉。 准备工作因为笔者是用的 VSCode 在开发,所以在开始写 Deno 项目前我们先做好相关设置。 在 .vscode/settings.json 中写入设置 { \"deno.enable\": true, \"deno.import_intellisense_origins\": { \"https://deno.land\": true }, \"deno.lint\": true, \"[typescript]\": { \"editor.defaultFormatter\": \"denoland.vscode-deno\", }, \"[typescriptreact]\": { \"editor.defaultFormatter\": \"denoland.vscode-deno\", },} 使得编辑器在写 deno 时能够进行代码提示。 虽然 Deno 说自己没有像 npmjs 那样的包管理中心,但其实他还是有属于自己的 Deno标准库,目前版本是 0.83.0。当我们要使用一些标准模块,比如 path,我们就要去引入标准库。 项目结构└── deno-demo ├── configs.ts # 配置信息文件 ├── db # 数据库目录 ├── controllers # 控制器目录 ├── app.ts # 入口文件 ├── middlewares # 中间件目录 ├── models # 模型定义目录 ├── routes.ts # 路由文件 └── services # 服务层程序目录 入口文件app.ts import { Application } from \"https://deno.land/x/oak/mod.ts\";import { HOST, PORT } from \"./configs.ts\";import router from \"./routes.ts\";import notFound from \"./controllers/notFound.ts\";import errorMiddleware from \"./middlewares/error.ts\";const app = new Application();app.use(errorMiddleware);app.use(router.routes());app.use(router.allowedMethods());app.use(notFound);console.log(`Listening on ${PORT}...`);try { await app.listen(`${HOST}:${PORT}`);} catch(e) { console.log(e)} 从以上的代码来看,整个流程跟 Node.js 使用 Koa 开发几乎一样。 配置文件configs.ts const env = Deno.env.toObject();export const HOST = env.HOST || \"127.0.0.1\";export const PORT = env.PORT || 3000;export const DB = env.DB || \"./db/todos.json\"; 在配置文件中配置 HOST、PORT、DB 等参数。 路由配置routes.ts import { Router } from \"https://deno.land/x/oak/mod.ts\";import getTodos from \"./controllers/getTodos.ts\";import createTodo from \"./controllers/createTodo.ts\";import updateTodo from \"./controllers/updateTodo.ts\";import deleteTodo from \"./controllers/deleteTodo.ts\";const router = new Router();router .get(\"/todos\", getTodos) .post(\"/todos\", createTodo) .put(\"/todos/:id\", updateTodo) .delete(\"/todos/:id\", deleteTodo);export default router; 路由文件也和使用 Koa-router 时的代码差不多。 定义模型(model)定义模型在本项目中体现为定义了一个接口 model/todo.ts export interface Todo { id: string; userId: number; title: string; status: boolean;} 数据操作service/db.ts import { DB } from \"../configs.ts\";import { Todo } from \"../models/todo.ts\";// 获取 Todo 数据export const fetchTodo = async (): Promise<Todo[]> => { const data = await Deno.readFile(DB); const decoder = new TextDecoder(); const decodedData = decoder.decode(data); return JSON.parse(decodedData);};// 写入 Todo 数据export const persistTodo = async (data: Todo[]): Promise<void> => { const encoder = new TextEncoder(); await Deno.writeFile(DB, encoder.encode(JSON.stringify(data)));}; 数据的增删改查详情见项目的 controllers 下的文件 Not Found当 api 不能匹配到对应的路由时,返回 Not Found 信息 controllers/notFound.ts import { Response } from \"https://deno.land/x/oak/mod.ts\";export default ({ response }: { response: Response }) => { response.status = 404; response.body = { message: \"Not Found\" };}; 中间件 Middlewares我们定义了一个中间件 error.ts 来处理读不到数据的错误 moddlewares/error.ts import { Response } from \"https://deno.land/x/oak/mod.ts\";export default async ( { response }: { response: Response }, next: () => Promise<void>,) => { try { await next(); } catch (err) { response.status = 500; response.body = { message: err.message }; }}; 总结在进行以上代码的写入后,我们就可以进行 run 整个项目了 注意 run 的时候要带上 -A 或 --allow-all,意思是给予全部权限给当前项目。 deno run -A ./app.tsCheck file:///D:/DenoDemo/app.tsListening on 3000... ok!整个项目就跑起来啦。这次 deno 的初体验也就完成了。完整代码的地址是 deno-demo。 注意事项 不能开代理。否则无法运行","categories":[],"tags":[]},{"title":"记一次文字拷贝","slug":"记一次文字拷贝","date":"2020-12-29T14:54:50.000Z","updated":"2023-01-02T18:31:29.271Z","comments":true,"path":"2020/12/29/记一次文字拷贝/","link":"","permalink":"https://kaviilee.github.io/blog/2020/12/29/%E8%AE%B0%E4%B8%80%E6%AC%A1%E6%96%87%E5%AD%97%E6%8B%B7%E8%B4%9D/","excerpt":"Clipboard API,“船新”的浏览器与系统剪贴板交互的异步 API。","text":"Clipboard API,“船新”的浏览器与系统剪贴板交互的异步 API。 小小的碎碎念恰巧前段时间要做 复制到剪贴板 这个功能,就去查了一下实现方式,大致都是 document.execCommand 来实现。使用的时候没多注意这个 API 已经废弃,之后发现了,觉得既然废弃就应该有替代的。于是再继续查了查,原来确实是有替代,就是今天的主角 Clipboard API。 接下来就是通过比对 document.execCommand 与 Clipboard API 的使用方法来介绍这两种方式的区别,来认识 Clipboard API。 写入系统剪贴板有两种方式向系统剪贴板写入数据。document.execCommand 触发 cut 和 copy 来写入;另一种方式就是使用 Clipboard API 的 writeText() 或 write() 写入数据。 使用 document.execCommand bool = document.execCommand(aCommandName, aShowDefaultUI, aValueArgument) 返回值为布尔值,如果是 false 则表示操作不被支持或未被启用。 document.execCommand 是一个非常强大的 API,但我们这里只用到了它的 cut,copy,paste const copy = () => { const copyEle = document.querySelector('#copy') as HTMLInputElement copyEle.select() const success = document.execCommand('copy') if (success) { alert('已将文本复制到剪贴板') } else { alert('复制失败') }}<input value="这是一个copy测试" id="copy" /><button onClick={copy}>点击复制</button> 使用 Clipboard API 相较 document.execCommand,Clipboard API 更加灵活,它不仅能够将当前选择复制到剪贴板,还能够直接指定要放入剪贴板的信息。 不同于 document.execCommand,Clipboard API 需要申请 clipboardRead 与 clipboardWrite 权限。 我们可以通过 navigator.permissions.query() 来查询权限: navigator.permissions.query({ name: 'clipboard-write' }).then(res => { if (res.state === 'granted' || res.state === 'prompt') { // write your code }}) 更新剪贴板使用方法 const updateClipboard = (newText) => { navigator.clipboard.writeText(newText).then(() => { // successfully set }, () => { // failed })} 不同浏览器的差异 Chrome 可以在所有执行上下中写入系统剪贴板 不需要权限 Firefox version 51 以上才开始支持 “clipboardWrite” 权限 具体的兼容性 write writeText 读取系统剪贴板document.execCommand 提供了 paste 指令,让用户能够粘贴内容。也可以使用 Clipboard API 的 read() 和 readText() 使用 document.execCommandconst paste = () => { const pasteText = document.querySelector('#output') as HTMLInputElement pasteText.focus() const success = document.execCommand('paste')}<input id="output" type="text"/><button onClick={paste}>粘贴</button> 使用 Clipboard API在获得了 “clipboard-read” 权限之后,剪贴板的数据读取 navigator.clipboard.readText().then(text => { document.querySelector('#output').innerText = text}) 这个代码片段从剪贴板提取文本并且用该文本替换 ID 为 "outbox" 的元素的当前内容。 具体的兼容性 read readText Clipboard EventClipboard API 还可以让我们响应 复制,剪切,粘贴 等事件。只需要给元素增加对 copy,cut 和 paste 事件的监听,就能够拦截到事件之后进行自定义的处理。 Clipboard API 中定义了一个 ClipboardEvent,而这个 ClipboardEvent 有一个属性 ClipboardData (目前还是一个实验性的属性),顾名思义这是剪贴板数据。 ClipboardData 属性是一个 DataTransfer 对象,有两个方法: setData(format, data):当用户进行复制和剪贴时,可以通过这个方法来写入。参数 format: DOMString 表示数据的类型,参数 data: DOMString 表示数据。 getData(format):可以在用户进行粘贴时处理数据。 拦截 copy,cut 事件通过拦截用户的 copy 和 cut 事件,可以做到更改用户的剪贴板数据 const handleCopy = e => { const selection = document.getSelection() e.clipboardData?.setData('text/plain', `你的剪贴板数据被我更改了哦~~${selection}`) e.preventDefault()}document.querySelector('#copy').addEventListener('copy', proxyClipboardCopy) 利用这个方法,我们可以做到在用户复制特定元素的内容时,在复制内容中加入你想加入的信息,比如: 版权信息。 拦截 paste 事件利用拦截用户的 paste 事件,我们可以做到更多的事情,比如富文本编辑中的粘贴图片之后自动上传或者进行 base64 转换之类的操作。 <textarea id="input" placeholder="请开始你的表演" />const handlePaste = (e) => { const input = document.querySelector('#input') input.addEventListener('paste', () => { const text = e.clipboardData.getData('text/plain') console.log(text) e.preventDefault() })} 总结说一个小知识 document.execCommand 这个 API 本身也不是标准 API,这是 IE 的私有 API,只是被其他浏览器做了兼容支持。而这个 API 被废弃的原因主要是安全问题,它不需要任何权限就能执行一些敏感操作,还有就是这是一个同步方法,而且是操作 DOM 对象的,会阻塞页面渲染和脚本执行。 可能就是要解决以上的两个问题,Clipboard API 被设计成了需要相对应的权限才能使用的异步方法。 目前各个浏览器对 Clipboard API 的支持不相同,尤其是在能够复制更复杂内容的 write 和 read 上,Chrome 实现了而 Firefox 却还没有实现。而且具体的规范和标准也还处在工作草案阶段。相较于已经被废弃的 execCommand ,Clipboard API 之后会发展成啥样也是蛮值得期待的。","categories":[{"name":"JavaScript","slug":"javascript","permalink":"https://kaviilee.github.io/blog/categories/javascript/"}],"tags":[]},{"title":"linear-gradient笔记","slug":"linear-gradient笔记","date":"2020-12-21T15:15:25.000Z","updated":"2023-01-02T18:31:29.271Z","comments":true,"path":"2020/12/21/linear-gradient笔记/","link":"","permalink":"https://kaviilee.github.io/blog/2020/12/21/linear-gradient%E7%AC%94%E8%AE%B0/","excerpt":"之前用 background-image 的 linear-gradient 画了下划线,觉得很神奇,就去认真学习了一下。做了些小笔记,备忘。","text":"之前用 background-image 的 linear-gradient 画了下划线,觉得很神奇,就去认真学习了一下。做了些小笔记,备忘。 渐变线的构成 CSS linear-gradient() 函数用于创建一个表示两种或多种颜色线性渐变的图片。其结果属于 <gradient> 数据类型,是一种特别的 <image> 数据类型。 以上是 mdn 关于 linear-gradient 的定义。语法如下 linear-gradient( [ <angle> | to <side-or-corner> ,]? <color-stop-list> ) \\---------------------------------/ \\----------------------------/ Definition of the gradient line List of color stopswhere <side-or-corner> = [ left | right ] || [ top | bottom ] and <color-stop-list> = [ <linear-color-stop> [, <color-hint>? ]? ]#, <linear-color-stop> and <linear-color-stop> = <color> [ <color-stop-length> ]? and <color-stop-length> = [ <percentage> | <length> ]{1,2} and <color-hint> = [ <percentage> | <length> ] // 颜色中转点 这个函数表示接受第一个参数为是渐变的角度,它可以是单位为 deg,rad,grad 或 turn 的具体的角度值(该角度是顺时针增加的);还可以是 to 和表示方向的关键词(top,right,bottom,left 这些值会被转换成 0°,90°,180°,270°,其余值会被转换成一个以顶部中央方向为起点顺时针旋转的角度),关键词先后顺序无影响,都是可选的。第二个参数是一系列的颜色节点,由一个 <color> 值组成,并且伴随一个可选的终点位置,这个终点位置可以是一个百分比值或者是沿着渐变轴的 <length>。 渐变容器渐变图像是没有内在尺寸的,是无限大的。那么 linear-gradient 函数的具体尺寸由渐变容器的大小决定。 一般来说,如果给一个元素的 background-image 使用 linear-gradient,那么渐变的区域就是该元素的区域。而如果该元素设置了 background-size 渐变容器就会变成设置的 background-size 大小。 渐变线 渐变线(Gradient line)由包含渐变图形的容器的中心点和一个角度来定义的。 这里借用一下 mdn 的图片 渐变角度linear-gradient 是通过渐变角度来控制渐变的方向的。 如上图所示,红线就是渐变角度。上文中有说到,定义这个角度有两种方法: 使用方向关键词:to <top | right | bottom | left | top right | top left | bottom right | bottom left> 使用具体的角度值:比如 45deg 如果缺省角度值,默认值就是 to bottom (180deg) 这里方向关键词有其对应的具体角度值: top(0deg),right(90deg),bottom(180deg),left(270deg)。 而如果是是使用 to top left 这种复合的顶角关键词,就没有对应的固定角度。因为它依赖的是渐变容器的尺寸,如果容器刚好是一个正方形,那么 to top right 和 45deg 的效果是一样。 渐变色节点 (linear color stop)渐变色节点可以这样定义: <linear-color-stop> = <color> [ <percentage> | <length> ]? 这意味着颜色在渐变线上的位置并不需要强制提供。 如果没有提供颜色在渐变线上的位置,颜色的位置就会沿着渐变线平均分布。如果只有 2 个颜色,那么颜色 1 将被放在 0% 的位置,颜色 2 就被放在 100% 的位置;如果有 3 个颜色,那么颜色 1 在 0%,颜色 2 在 50%,颜色 3 在 100%,以此类推。 运用好 linear-gradient 可以让你的页面更好看(是真的","categories":[{"name":"CSS","slug":"css","permalink":"https://kaviilee.github.io/blog/categories/css/"}],"tags":[{"name":"CSS","slug":"css","permalink":"https://kaviilee.github.io/blog/tags/css/"}]},{"title":"使用background-image画虚线","slug":"使用background-image画虚线","date":"2020-12-14T14:54:50.000Z","updated":"2023-01-02T18:31:29.271Z","comments":true,"path":"2020/12/14/使用background-image画虚线/","link":"","permalink":"https://kaviilee.github.io/blog/2020/12/14/%E4%BD%BF%E7%94%A8background-image%E7%94%BB%E8%99%9A%E7%BA%BF/","excerpt":"在知道可以用 background-image 的渐变来定义虚线之前我都是用 border-bottom: 1px dashed #ccc 的。是新的知识,耶!","text":"在知道可以用 background-image 的渐变来定义虚线之前我都是用 border-bottom: 1px dashed #ccc 的。是新的知识,耶! <div class=\"container\"> <div class=\"item\">background-image</div></div> 传统的 border.container { position: relative; .item { padding: 16px; border-bottom: 1px solid #ccc; }} 使用 border 的效果如下 使用 background-image.container { position: relative; .item { padding: 16px; &::after { position: absolute; content: ''; width: 100%; height: 1px; left: 0; bottom: 0; // important background-image: linear-gradient(to right, #ccc 0%, #ccc 50%, transparent 50%); // 设置图片宽度与高度 background-size: 20px 1px; background-repeat: repeat-x; } }} 使用 background-image 就可以通过改变 background-size 来改变虚线的间距。 如果要改变虚线的方向,需要修改 width,height,background-image, background-repeat 和 background-size。 &::after { width: 1px; height: 100%; background-image: linear-gradient(to bottom, #ccc 0%, #ccc 50%, transparent 50%); background-repeat: repeat-y; background-size: 1px 10px;}","categories":[{"name":"CSS","slug":"css","permalink":"https://kaviilee.github.io/blog/categories/css/"}],"tags":[]},{"title":"antd form 自定义校验相关错误问题","slug":"antd-form-自定义校验相关错误问题","date":"2020-12-09T14:17:35.000Z","updated":"2023-01-02T18:31:29.271Z","comments":true,"path":"2020/12/09/antd-form-自定义校验相关错误问题/","link":"","permalink":"https://kaviilee.github.io/blog/2020/12/09/antd-form-%E8%87%AA%E5%AE%9A%E4%B9%89%E6%A0%A1%E9%AA%8C%E7%9B%B8%E5%85%B3%E9%94%99%E8%AF%AF%E9%97%AE%E9%A2%98/","excerpt":"在使用 ant design form 组件自定义校验,进行远程校验数据唯一性的时候,遇到的一些问题以及解决方法。","text":"在使用 ant design form 组件自定义校验,进行远程校验数据唯一性的时候,遇到的一些问题以及解决方法。 问题背景使用 ant design form 设计表单时,某些字段需要进行远程校验,比如:用户名(username),手机号(mobile),就需要自定义校验规则。 错误示例在需要进行校验的地方,之前错误的写法 /** 验证字段唯一性* @param key 字段key* @param value 字段value*/const validateUnique = (key: string, value: string): any => { if (value) { remoteValidate({ key, value }).then((res: any) => { if (res.success) { return res.data.isUnique; } }).catch(e => { message.error(e) return false }) }}<Form.Item label="用户名" name="username" required rules={[{ required: true, message: '请输入用户名' }, () => ({ validator(_, value) { if (!value || validateUnique('username', value)) { return Promise.resolve() } return Promise.reject('已存在相同的用户名') }})]}> <Input /></Form.Item> 以上的写法能够校验字段值为空时的错误,但是在进行远程校验字段唯一性的时候,就无法验证。validateUnique 返回的值一直为 undefined,使得错误一直是 已存在相同的用户名。 解决方法为了解决校验一直返回 undefined,我们修改写法为 <Form.Item label="用户名" name="username" required rules={[{ required: true, message: '请输入用户名' }, () => ({ validator(_, value) { return new Promise((resolve, reject) => { if (value) { remoteValidate({ key: 'username', value }).then(async (res) => { if (res.data.isUnique) { return resolve() } return reject('已存在相同的手机号') }) } else { return reject() } }) }})]}> <Input /></Form.Item>","categories":[{"name":"框架","slug":"框架","permalink":"https://kaviilee.github.io/blog/categories/%E6%A1%86%E6%9E%B6/"}],"tags":[{"name":"Antd v4","slug":"antd-v4","permalink":"https://kaviilee.github.io/blog/tags/antd-v4/"},{"name":"react","slug":"react","permalink":"https://kaviilee.github.io/blog/tags/react/"}]},{"title":"RESTful API 简介与实践","slug":"restful","date":"2020-10-21T17:02:37.000Z","updated":"2023-01-02T18:31:29.271Z","comments":true,"path":"2020/10/21/restful/","link":"","permalink":"https://kaviilee.github.io/blog/2020/10/21/restful/","excerpt":"RESTful API 相关","text":"RESTful API 相关 REST 全称 Representational State Transfer,即「表现层状态转化」。符合 REST 原则的架构,就被称为 RESTful 架构。 简介一、资源 (Resources)REST 的名称「表现层状态转化」的主语其实是指的是「资源」(Resources),即 “资源表现层状态转化”。 资源,就是网络上的一个实体,或者说是网络上的一个具体信息。可以是文本,图片,歌曲,服务等等。我们可以用一个 URI 指向它,每种资源对应一个特定的 URI。要获得这个资源,访问这个 URI就可以,因此 URI 也就成了每个资源的地址或者独一无二的标识符。 二、表现层 (Representation)「资源」是一种信息实体,它可以有多种外在表现形式。我们把「资源」具体呈现出来的形式,叫做它的「表现层」(Representation)。 比如,文本可以用 txt 格式表现,也可以用 HTML,XML,JSON等格式表现,图片可以用 JPG 格式表现,也可以用 PNG 格式表现。 URI 只代表资源的实体,不代表表现形式。具体的表现格式,就属于「表现层」的范畴,而URI应该只代表「资源」的位置。它的具体表现形式,应该在HTTP请求的头信息中用 Accept 和 Content-Type 字段指定,这两个字段才是对「表现层」的描述。 三、状态转化 (State Transfer)访问一个状态,就代表了客户端和服务端的一个互动过程。在这个过程中,必然涉及到数据和状态的变化。 互联网通讯协议 HTTP 协议,是一个无状态协议。也就是说,所有的状态都保存在服务器端。因此,客户端想要操作服务器,就必须通过某种手段,使服务器发生「状态转化」(State Transfer)。而这种转化是建立在表现层上的,也就是「表现层状态转化」。 在 HTTP 协议中,四个表示操作方式的动词:GET,POST,PUT,DELETE。他们分别对应四种基本操作:GET 获取资源,POST 新建资源(也可以更新资源),PUT 更新资源,DELETE 删除资源。 四、总结总结一下 RESTful 架构: 每一个 URI 代表一种资源; 客户端和服务器之间,传递这种资源的表现层; 客户端通过四个 HTTP 动词,对服务端资源进行操作,实现「表现层状态转化」。 实践一、URL设计1.1 动词 + 宾语RESTful 的核心思想就是,客户端发出的数据操作指令都是「动词 + 宾语」的结构。比如,GET /todos 这个命令,GET 是动词,/todos 是宾语。 动词通常就是五种 HTTP 方法,对应 CRUD 操作。 GET:读取(Read) POST:新建(Create) PUT:更新(Update) PATCH:更新(Update),通常是部分更新 DELETE:删除(Delete) 1.2 动词的覆盖有些客户端只能使用 GET 和 POST 方法。服务器必须接受 POST 模拟其他三个方法(PUT,PATCH,DELETE) 。 此时,客户端发出的 HTTP 请求,要加上 X-HTTP-Method-Override 属性,告诉服务器应该使用哪个动词来覆盖 POST 方法。 POST /api/todos/4 HTTP/1.1X-HTTP-Method-Override: PUT 1.3 宾语必须是名词宾语就是 API 的 URL,是 HTTP 动词作用的对象。它应该是名词,不能是动词。 1.4 复数 URL既然 URL 是名词,那么应该使用复数,还是单数? 这没有统一的规定,但是常见的操作是读取一个集合,比如 GET /articles(读取所有文章),这里明显应该是复数。 为了统一起见,建议都使用复数 URL,比如 GET /articles/2 要好于 GET /article/2。 1.5 避免多级 URL常见的情况是,资源需要多级分类,因此很容易写出多级的 URL,比如获取某个作者的某一类文章。 GET /authors/12/categories/2 改成查询字符串更好 GET /authors/12?categories=2 二、状态码2.1 状态码必须精确客户端的每一次请求,服务器都必须给出回应。回应包含 HTTP 状态码和数据两部分。 HTTP 状态码就是一个三位数,分成五个类别。 1xx:相关信息 2xx:操作成功 3xx:重定向 4xx:客户端错误 5xx:服务器错误 API 不需要 1xx 状态码 2.2 状态码具体意义服务器向用户返回的状态码和提示信息,常见的有以下一些(方括号中是该状态码对应的 HTTP 动词)。 200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。 201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。 202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务) 204 NO CONTENT - [DELETE]:用户删除数据成功。 400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。 401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。 403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。 404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。 406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。 410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。 422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。 500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。 503 Service Unavailable: 服务器无法处理请求,一般用于网站维护状态 本文摘自阮一峰老师的 理解RESTful架构 和 RESTful API 最佳实践","categories":[{"name":"技术","slug":"技术","permalink":"https://kaviilee.github.io/blog/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"规范","slug":"规范","permalink":"https://kaviilee.github.io/blog/tags/%E8%A7%84%E8%8C%83/"}]},{"title":"约定式提交 (Conventional Commits)","slug":"conventional-commits","date":"2020-10-13T00:19:25.000Z","updated":"2023-01-02T18:31:29.271Z","comments":true,"path":"2020/10/13/conventional-commits/","link":"","permalink":"https://kaviilee.github.io/blog/2020/10/13/conventional-commits/","excerpt":"关于约定式提交的规范内容。","text":"关于约定式提交的规范内容。 约定式提交 一种用于给提交信息增加人机可读含义的规范。 概述约定式提交规范是一种基于提交消息的轻量级约定。 它提供了一组用于创建清晰的提交历史的简单规则; 这使得编写基于规范的自动化工具变得更容易。在提交信息中描述新特性、bug 修复和破坏性变更。 提交说明的结构如下: <类型>[可选的作用域]: <描述>[可选的正文][可选的脚注] 提交说明包含了下面的结构化元素,以向类库使用者表明其意图: fix: 类型为 fix 的提交表示在代码库中修复了一个 bug。 feat: 类型为 feat 的提交表示在代码库中新增了一个功能。 BREAKING CHANGE: 在可选的正文或脚注的起始位置带有 BREAKING CHANGE: 的提交,表示引入了破坏性 API 变更。破坏性变更可以是任意 类型 提交的一部分。 其它情况: 除 fix: 和 feat: 之外的提交 类型 也是被允许的,例如 @commitlint/config-conventional(基于 Angular 约定)中推荐的 chore:、docs:、style:、refactor:、perf:、test: 及其他标签。 我们也推荐使用improvement,用于对当前实现进行改进而没有添加新功能或修复错误的提交。 请注意,这些标签在约定式提交规范中并不是强制性的。并且在语义化版本中没有隐式的影响(除非他们包含 BREAKING CHANGE)。 可以为提交类型添加一个围在圆括号内的作用域,以为其提供额外的上下文信息。例如 feat(parser): adds ability to parse arrays.。 示例包含了描述以及正文内有破坏性变更的提交说明feat: allow provided config object to extend other configsBREAKING CHANGE: `extends` key in config file is now used for extending other config files 不包含正文的提交说明docs: correct spelling of CHANGELOG 包含作用域的提交说明feat(lang): add polish language 为 fix 编写的提交说明,包含(可选的) issue 编号fix: correct minor typos in codesee the issue for details on the typos fixedcloses issue #12 约定式提交规范 每个提交都必须使用类型字段前缀,它由一个名词组成,诸如 feat 或 fix ,其后接一个可选的作用域字段,以及一个必要的冒号(英文半角)和空格。 当一个提交为应用或类库实现了新特性时,必须使用 feat 类型。 当一个提交为应用修复了 bug 时,必须使用 fix 类型。 作用域字段可以跟随在类型字段后面。作用域必须是一个描述某部分代码的名词,并用圆括号包围,例如: fix(parser): 描述字段必须紧接在类型/作用域前缀的空格之后。描述指的是对代码变更的简短总结,例如: fix: array parsing issue when multiple spaces were contained in string. 在简短描述之后,可以编写更长的提交正文,为代码变更提供额外的上下文信息。正文必须起始于描述字段结束的一个空行后。 在正文结束的一个空行之后,可以编写一行或多行脚注。脚注必须包含关于提交的元信息,例如:关联的合并请求、Reviewer、破坏性变更,每条元信息一行。 破坏性变更必须标示在正文区域最开始处,或脚注区域中某一行的开始。一个破坏性变更必须包含大写的文本 BREAKING CHANGE,后面紧跟冒号和空格。 在 BREAKING CHANGE:之后必须提供描述,以描述对 API 的变更。例如: BREAKING CHANGE: environment variables now take precedence over config files. 在提交说明中,可以使用 feat 和 fix 之外的类型。 工具的实现必须不区分大小写地解析构成约定式提交的信息单元,只有 BREAKING CHANGE 必须是大写的。 可以在类型/作用域前缀之后,: 之前,附加 ! 字符,以进一步提醒注意破坏性变更。当有 ! 前缀时,正文或脚注内必须包含 BREAKING CHANGE: description 具体类型含义如下: 类型 说明 feat: 新增功能 fix: 修复 bug docs: 仅修改文档 style: 样式不会影响代码含义的更改,如 空白符、格式、分号补全、错别字修改等 refactor: 既不修复错误也不增加功能的代码修改 perf: 本次代码的更改可提高性能 test: 添加或修改测试内容 build: 影响构建系统或外部依赖的更改 (Example scopes: webpack, npm) ci: 对 CI 配置文件和脚本的更改 (Example scopes: travis) chore 其他不会修改 src 或测试文件的更改,如 .gitignore,package.json、yarn.json 等 revert 回退旧版本 更多内容参见:约定式提交","categories":[{"name":"技术","slug":"技术","permalink":"https://kaviilee.github.io/blog/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"git","slug":"git","permalink":"https://kaviilee.github.io/blog/tags/git/"},{"name":"规范","slug":"规范","permalink":"https://kaviilee.github.io/blog/tags/%E8%A7%84%E8%8C%83/"}]},{"title":"JavaScript类型转换","slug":"javascript类型转换","date":"2020-09-09T23:23:10.000Z","updated":"2023-01-02T18:31:29.271Z","comments":true,"path":"2020/09/09/javascript类型转换/","link":"","permalink":"https://kaviilee.github.io/blog/2020/09/09/javascript%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2/","excerpt":"关于 JavaScript 类型转换的一些笔记。","text":"关于 JavaScript 类型转换的一些笔记。 前言将值从一种类型转换到另一种类型通常称为类型转换。 原始值转布尔值JavaScript中,只有 6 种值可以被转换为false,其余都会被转换成 true。 console.log(Boolean()) // falseconsole.log(Boolean(false)) // falseconsole.log(Boolean(undefined)) // falseconsole.log(Boolean(null)) // falseconsole.log(Boolean(+0)) // falseconsole.log(Boolean(-0)) // falseconsole.log(Boolean(NaN)) // falseconsole.log(Boolean(\"\")) // false 原始值转数字使用 Number 函数将值的类型转换成数字类型。 根据规范,如果有参数,则调用 ToNumber(value) 计算出值返回,否则返回 +0。 此处的 ToNumber 表示的是一个底层规范上实现的方法,并没有暴露出来。 值类型 结果 Undefined NaN Null +0 Boolean 如果是 true,返回1;如果是 false,返回0 Number 返回与之相等的值 String 如下 console.log(Number()) // +0console.log(Number(undefined)) // NaNconsole.log(Number(null)) // +0console.log(Number(false)) // +0console.log(Number(true)) // 1console.log(Number(\"123\")) // 123console.log(Number(\"-123\")) // -123console.log(Number(\"1.2\")) // 1.2console.log(Number(\"000123\")) // 123console.log(Number(\"-000123\")) // -123console.log(Number(\"0x11\")) // 17console.log(Number(\"\")) // 0console.log(Number(\" \")) // 0console.log(Number(\"123 123\")) // NaNconsole.log(Number(\"foo\")) // NaNconsole.log(Number(\"100a\")) // NaN 原始值转字符使用 String 函数将值的类型转换成字符类型。 根据规范,如果函数有参数,则调用 ToString(value) 计算出值返回,否则返回空字符串。 值类型 结果 Undefined ‘undefined’ Null ‘null’ Boolean 如果是 true,返回 “true”;如果是 false,返回 “false” String 返回与之相等的值 Number 如下 console.log(String()) // 空字符串console.log(String(undefined)) // undefinedconsole.log(String(null)) // nullconsole.log(String(false)) // falseconsole.log(String(true)) // trueconsole.log(String(0)) // 0console.log(String(-0)) // 0console.log(String(NaN)) // NaNconsole.log(String(Infinity)) // Infinityconsole.log(String(-Infinity)) // -Infinityconsole.log(String(1)) // 1 原始值转对象原始值通过调用 String()、Number() 或者 Boolean() 构造函数就能转化为各自的包装对象。 null 和 undefined,当将它们用在期望是一个对象的地方都会造成一个类型错误 (TypeError) 异常,而不会执行正常的转换。 对象转布尔值所有对象转为布尔值都为 true。 对象转字符串和数字对象转换到字符串或者数字都是调用待转换对象的一个方法来实现的。主要有两个方法 toString() 和 valueOf()。 所有对象除了 null 和 undefined 之外的任何值都有 toString 方法,通常,它和 String 方法的返回值是一致的。 JavaScript 根据不同的类各自的特点,定义了更多版本的 toString 方法。 console.log(({}).toString()) // [object Object]console.log([].toString()) // \"\"console.log([0].toString()) // 0console.log([1, 2, 3].toString()) // 1,2,3console.log((function(){var a = 1;}).toString()) // function (){var a = 1;}console.log((/\\d+/g).toString()) // /\\d+/gconsole.log((new Date(2010, 0, 1)).toString()) // Fri Jan 01 2010 00:00:00 GMT+0800 (CST) 另一个转换对象的方法是 valueOf,表示对象的原始值。默认的 valueOf 方法返回这个对象本身。日期是个例外,它会返回它的一个内容表示:1970 年 1 月 1日以来的毫秒数。 var date = new Date(2020, 9, 6);console.log(date.valueOf()) // 1601913600000 对象转字符串 值类型 结果 Object 1. primValue = ToPrimitive(input, Number)2. 返回 ToString(primValue)。 对象转数字 参数类型 结果 Object 1. primValue = ToPrimitive(input, Number)2. 返回 ToNumber(primValue)。 console.log(Number({})) // NaNconsole.log(Number({a : 1})) // NaNconsole.log(Number([])) // 0console.log(Number([0])) // 0console.log(Number([1, 2, 3])) // NaNconsole.log(Number(function(){var a = 1;})) // NaNconsole.log(Number(/\\d+/g)) // NaNconsole.log(Number(new Date(2010, 0, 1))) // 1262275200000console.log(Number(new Error('a'))) // NaN Number([]) 时,先调用了 [] 的 valueOf 方法返回了 [] ,因为不是原始值,继续调用 toString 方法,返回空字符串,继续调用 ToNumber,空字符串返回 0。 Number([1,2,3]) 时,先调用了 [1, 2, 3] 的 valueOf 方法返回了 [1, 2, 3],再调用 toString 方法,返回 1,2,3,调用 ToNumber,返回NaN。 ToPrimitive语法如下: ToPrimitive(input[, PreferredType]) 第一个参数是 input,表示要处理的输入值。 第二个参数是 PreferredType,非必填,表示希望转换成的类型,有两个值可以选,Number 或者 String。 当不传入 PreferredType 时,如果 input 是日期类型,相当于传入 String,否则,都相当于传入 Number。 如果传入的 input 是 Undefined、Null、Boolean、Number、String 类型,直接返回该值。 如果是 ToPrimitive(obj, Number),处理步骤如下: 如果 obj 为 基本类型,直接返回 否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。 否则,调用 toString 方法,如果返回一个原始值,则 JavaScript 将其返回。 否则,JavaScript 抛出一个类型错误异常。 如果是 ToPrimitive(obj, String),处理步骤如下: 如果 obj为 基本类型,直接返回 否则,调用 toString 方法,如果返回一个原始值,则 JavaScript 将其返回。 否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。 否则,JavaScript 抛出一个类型错误异常。 隐式转换场景 一元操作符 +当 + 运算符作为一元操作符时,会调用 ToNumber 处理该值,相当于 Number(value)。 如果 value 是对象,会先调用 ToPrimitive(obj, Number) 方法。 console.log(+['1']); // 1console.log(+['1', '2', '3']); // NaNconsole.log(+{}); // NaN 二元操作符 +当计算 value1 + value2 时: lprim = ToPrimitive(value1) rprim = ToPrimitive(value2) 如果 lprim 是字符串或者 rprim 是字符串,那么返回 ToString(lprim) 和 ToString(rprim) 的拼接结果 返回 ToNumber(lprim) 和 ToNumber(rprim) 的运算结果 == 相等 类型 (x) 类型 (y) 结果 null undefined true undefined null true 数值 字符串 x == ToNumber(y) 字符串 数值 ToNumber(x) == y 布尔值 任何类型 ToNumber(x) == y 任何类型 布尔值 x = ToNumber(y) 字符串或数 对象 x = ToPrimitive(y) 对象 字符串或数 ToPrimitive(x) == y","categories":[{"name":"JavaScript","slug":"javascript","permalink":"https://kaviilee.github.io/blog/categories/javascript/"}],"tags":[]},{"title":"Git Flow","slug":"git-flow","date":"2020-09-03T09:56:17.000Z","updated":"2023-01-02T18:31:29.271Z","comments":true,"path":"2020/09/03/git-flow/","link":"","permalink":"https://kaviilee.github.io/blog/2020/09/03/git-flow/","excerpt":"之前偶尔看到的一篇文章,是讲 Git Flow 的,谈了分支相关的一些知识。","text":"之前偶尔看到的一篇文章,是讲 Git Flow 的,谈了分支相关的一些知识。 Git Flow分支应用场景根据 Git Flow 的建议,主要的分支有 master、develop、hotfix、release 以及 feature 这五种分支,各种分支负责不同的功能。其中 Master 以及 Develop 这两个分支又被称作长期分支,因为他们会一直存活在整个 Git Flow 里,而其它的分支大多会因任务结束而被刪除。 Master 分支主要是用来放稳定、随时可上线的版本。这个分支的来源只能从别的分支合并过来,开发者不会直接 Commit 到这个分支。因为是稳定版本,所以通常也会在这个分支上的 Commit 上打上版本号 tag。 每个版本发布完,develop 会合并到 master,并打tag。 Develop 分支这个分支主要是所有开发的基础分支,当要新增功能的时候,所有的 Feature 分支都是从这个分支切出去的。而 Feature 分支的功能完成后,也都会合并回来这个分支。 开发过程中的稳定分支,develop分支应该保证每次最新的commit都是可以被run的。 Hotfix 分支当线上产品发生紧急问题的时候,会从 Master 分支开一个 Hotfix 分支出来进行修复,Hotfix 分支修复完成之后,会合并回 Master 分支,也同时会合并一份到 Develop 分支。 Release 分支当认为 Develop 分支够成熟了,就可以把 Develop 分支合并到 Release 分支,在这边进行算是上线前的最后测试。测试完成后,Release 分支将会同时合并到 Master 以及 Develop 这两个分支上。 Master 分支是上线版本,而合并回 Develop 分支的目的,是因为可能在 Release 分支上还会测到并修正一些问题,所以需要跟 Develop 分支同步,免得之后的版本又再度出现同样的问题。 其实正常的做法应该是提测,或测试到某个阶段以后使用。一般用在多版本并行开发的时候,充当特定版本的develop 分支使用 Feature 分支当要开始新增功能的时候,就是使用 Feature 分支的时候了。 Feature 分支都是从 Develop 分支来的,完成之后会再并回 Develop 分支。 目前feature分支起名规则以 dev_ 开头,后面跟当前开发功能 分支提交与合并如果 feature 需要 develop 的功能,不要将 develop 分支 merge 到 feature 分支,应该使用 feature rebase develop 分支提交使用 rebase 或 merge 方式都可以,一般来说commit 少的场景我会比较喜欢 rebase,但如果commit攒的多了,rebase 解决冲突会很累。 rebase 操作例如,开始开发时,分支是这样的 * -- * -- A (develop) \\ * (dev_xxx) 开发完成,准备提交时: * -- * -- A -- B -- C -- D -- E (develop) \\ * -- X -- Y -- Z (dev_xxx) 命令行步骤如下(GUI参考命令行执行对应操作) git add .git commit -m \"xxx\"git pull //用于更新远端分支(git fetch 也可以)git rebase origin/develop //将当前分支base变为develop 的最新 commit 这一步的实际原理是,git会从develop新开一个分支,将你当前的dev_xxx 分支的 commit 按照提交顺序挨个 cherry-pick 到新开分支上 这一步执行过程中如果 develop 的 B C D E等commit 与 X Y Z 冲突,则需要依次解决冲突 解决冲突以后,执行 git rebase --continue 或者 git rebase --skip 继续执行接下来的 rebase rebase 结束后,分支结构将变为: * -- * -- A -- B -- C -- D -- E (develop) \\ -- * -- X -- Y -- Z (dev_xxx) 然后需要执行 git push -f // 必须加 -f 强制推送,否则由于本地分支的base与远端不一致,会报需要 git pull 无法提交 提交完成后去 gitlab 创建 merge request,走正常 review 流程,合并代码。 本文转载自 再聊 Git Flow","categories":[{"name":"技术","slug":"技术","permalink":"https://kaviilee.github.io/blog/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"git","slug":"git","permalink":"https://kaviilee.github.io/blog/tags/git/"},{"name":"规范","slug":"规范","permalink":"https://kaviilee.github.io/blog/tags/%E8%A7%84%E8%8C%83/"}]},{"title":"中文文案排版指北","slug":"中文文案排版指北","date":"2020-08-18T11:31:47.000Z","updated":"2023-01-02T18:31:29.271Z","comments":true,"path":"2020/08/18/中文文案排版指北/","link":"","permalink":"https://kaviilee.github.io/blog/2020/08/18/%E4%B8%AD%E6%96%87%E6%96%87%E6%A1%88%E6%8E%92%E7%89%88%E6%8C%87%E5%8C%97/","excerpt":"今天在看文档的时候,偶然看到这篇关于中文文案排版建议的文章,觉得可以分享出来~","text":"今天在看文档的时候,偶然看到这篇关于中文文案排版建议的文章,觉得可以分享出来~ 空格「有研究显示,打字的时候不喜欢在中文和英文之间加空格的人,感情路都走得很辛苦,有七成的比例会在 34 岁的时候跟自己不爱的人结婚,而其余三成的人最后只能把遗产留给自己的猫。毕竟爱情跟书写都需要适时地留白。 与大家共勉之。」——vinta/paranoid-auto-spacing 中英文之间需要增加空格正确: 在 LeanCloud 上,数据存储是围绕 AVObject 进行的。 错误: 在LeanCloud上,数据存储是围绕AVObject进行的。 在 LeanCloud上,数据存储是围绕AVObject 进行的。 完整的正确用法: 在 LeanCloud 上,数据存储是围绕 AVObject 进行的。每个 AVObject 都包含了与 JSON 兼容的 key-value 对应的数据。数据是 schema-free 的,你不需要在每个 AVObject 上提前指定存在哪些键,只要直接设定对应的 key-value 即可。 例外:「豆瓣FM」等产品名词,按照官方所定义的格式书写。 中文与数字之间需要增加空格正确: 今天出去买菜花了 5000 元。 错误: 今天出去买菜花了 5000元。 今天出去买菜花了5000元。 数字与单位之间无需增加空格正确: 我家的光纤入户宽带有 10Gbps,SSD 一共有 10TB。 错误: 我家的光纤入户宽带有 10 Gbps,SSD 一共有 20 TB。 另外,度/百分比与数字之间不需要增加空格: 正确: 今天是 233° 的高温。 新 MacBook Pro 有 15% 的 CPU 性能提升。 错误: 今天是 233 ° 的高温。 新 MacBook Pro 有 15 % 的 CPU 性能提升。 全角标点与其他字符之间不加空格正确: 刚刚买了一部 iPhone,好开心! 错误: 刚刚买了一部 iPhone ,好开心! -ms-text-autospace to the rescue?Microsoft 有个 -ms-text-autospace 的 CSS 属性可以实现自动为中英文之间增加空白。不过目前并未普及,另外在其他应用场景,例如 OS X、iOS 的用户界面目前并不存在这个特性,所以请继续保持随手加空格的习惯。 标点符号不重复使用标点符号正确: 德国队竟然战胜了巴西队! 她竟然对你说「喵」?! 错误: 德国队竟然战胜了巴西队!! 德国队竟然战胜了巴西队!!!!!!!! 她竟然对你说「喵」??!! 她竟然对你说「喵」?!?!??!! 全角和半角不明白什么是全角(全形)与半角(半形)符号?请查看维基百科词条『全角和半角』。 使用全角中文标点正确: 嗨!你知道嘛?今天前台的小妹跟我说「喵」了哎! 核磁共振成像(NMRI)是什么原理都不知道?JFGI! 错误: 嗨! 你知道嘛? 今天前台的小妹跟我说 “喵” 了哎! 嗨!你知道嘛?今天前台的小妹跟我说”喵”了哎! 核磁共振成像 (NMRI) 是什么原理都不知道? JFGI! 核磁共振成像(NMRI)是什么原理都不知道?JFGI! 数字使用半角字符正确: 这件蛋糕只卖 1000 元。 错误: 这件蛋糕只卖 1000 元。 例外:在设计稿、宣传海报中如出现极少量数字的情形时,为方便文字对齐,是可以使用全角数字的。 遇到完整的英文整句、特殊名词,其內容使用半角标点正确: 乔布斯那句话是怎么说的?「Stay hungry, stay foolish.」 推荐你阅读《Hackers & Painters: Big Ideas from the Computer Age》,非常的有趣。 错误: 乔布斯那句话是怎么说的?「Stay hungry,stay foolish。」 推荐你阅读《Hackers&Painters:Big Ideas from the Computer Age》,非常的有趣。 名词专有名词使用正确的大小写大小写相关用法原属于英文书写范畴,不属于本 wiki 讨论內容,在这里只对部分易错用法进行简述。 正确: 使用 GitHub 登录 我们的客户有 GitHub、Foursquare、Microsoft Corporation、Google、Facebook, Inc.。 错误: 使用 github 登录 使用 GITHUB 登录 使用 Github 登录 使用 gitHub 登录 使用 gイんĤЦ8 登录 我们的客户有 github、foursquare、microsoft corporation、google、facebook, inc.。 我们的客户有 GITHUB、FOURSQUARE、MICROSOFT CORPORATION、GOOGLE、FACEBOOK, INC.。 我们的客户有 Github、FourSquare、MicroSoft Corporation、Google、FaceBook, Inc.。 我们的客户有 gitHub、fourSquare、microSoft Corporation、google、faceBook, Inc.。 我们的客户有 gイんĤЦ8、キouЯƧquムгє、๓เςг๏ร๏Ŧt ς๏гק๏гคtเ๏ภn、900913、ƒ4ᄃëв๏๏к, IПᄃ.。 注意:当网页中需要配合整体视觉风格而出现全部大写/小写的情形,HTML 中请使用标准的大小写规范进行书写;并通过 text-transform: uppercase;/text-transform: lowercase; 对表现形式进行定义。 不要使用不地道的缩写正确: 我们需要一位熟悉 JavaScript、HTML5,至少理解一种框架(如 Backbone.js、AngularJS、React 等)的前端开发者。 错误: 我们需要一位熟悉 Js、h5,至少理解一种框架(如 backbone、angular、RJS 等)的 FED。 争议以下用法略带有个人色彩,即:无论是否遵循下述规则,从语法的角度来讲都是正确的。 链接之间增加空格用法: 请 提交一个 issue 并分配给相关同事。 访问我们网站的最新动态,请 点击这里 进行订阅! 对比用法: 请提交一个 issue 并分配给相关同事。 访问我们网站的最新动态,请点击这里进行订阅! 简体中文使用直角引号用法: 「老师,『有条不紊』的『紊』是什么意思?」 对比用法: “老师,‘有条不紊’的‘紊’是什么意思?” 本文属于摘录,更多内容在 chinese-copywriting-guidelines ~","categories":[{"name":"技术","slug":"技术","permalink":"https://kaviilee.github.io/blog/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"规范","slug":"规范","permalink":"https://kaviilee.github.io/blog/tags/%E8%A7%84%E8%8C%83/"}]},{"title":"Docker 学习笔记","slug":"docker","date":"2020-07-21T00:03:15.000Z","updated":"2023-01-02T18:31:29.271Z","comments":true,"path":"2020/07/21/docker/","link":"","permalink":"https://kaviilee.github.io/blog/2020/07/21/docker/","excerpt":"这是在看完狂神的 Docker 视频之后做的笔记~存档用","text":"这是在看完狂神的 Docker 视频之后做的笔记~存档用 Docker 概述Docekr 为什么会出现一款产品有开发和生产两套环境,也就对应两套应用配置,一般就会存在两个问题。 开发和运维问题:这个项目在我的电脑上是可以运行的!但是伴随版本更新迭代,不同版本环境的兼容,导致服务不可用。 环境配置问题:环境配置是非常麻烦的,每个机器都要部署环境,费时费力。 思考:项目是否可以带上环境一起安装打包?把原始环境一模一样地复制过来? 在服务器上部署十分麻烦,不能跨平台。 传统开发:开发人员做项目。运维人员做部署 现在:开发打包部署上线,一起做 Docker 能干什么? 之前的虚拟机技术,所有的项目都在同一个环境下运行。 虚拟机的缺点 1.占用资源非常多2.冗余技术多3.启动很慢 容器化技术 容器化技术不是一个完整的操作系统 比较 Docker 和虚拟机技术的不同 传统的虚拟机,虚拟出一套硬件后,在其上运行一个完整的操作系统,在该操作系统上安装和运行软件; 容器内的应用直接运行在宿主机的内核,容器是没有自己的内核的,也没有进行硬件虚拟,更加轻便; 每个容器间都是相互隔离的,每个容器都有自己的文件系统,互不影响,能区分计算资源。 DevOps (开发,运维) 更快速的交付和部署 传统:一堆帮助文档,安装程序 Docker:一键运行打包镜像发布测试 更便捷的升级和扩缩容 使用了 Docker 之后,我们部署应用就像搭积木一样! 项目打包为一个镜像,扩展,可以在多个服务器上部署。 更简单的系统运维 在容器化之后,我们开发,测试环境是高度一致的。 更高效的计算机资源利用 Docker 是内核级别的虚拟化,可以在一个物理机上运行很多的容器实例,服务器性能可以被压榨到极致。 Docker 安装Docker 的基本组成 镜像 (image) Docker 镜像好比一个模板,可以通过这个模板来创建容器服务 nginx 镜像 => run => nginx01 (提供服务器)。 通过这个镜像可以创建多个容器(最终服务运行或者项目运行就是在容器中的)。 容器 (container) Docker 利用容器技术,独立运行一个或一组应用,通过镜像来创建 容器和镜像关系类似于面向对象编程中的对象和类。 仓库(repository) 仓库就是存放镜像的地方,仓库分为共有仓库和私有仓库。 Docker hub(默认国外镜像),阿里云,网易有国内镜像加速服务。 安装 DockerCentOS Docker 安装 环境准备 会一点 Linux 基础 使用的操作系统 CentOS7 使用 XShell 连接远程服务器 环境查看 # 查看系统内核# uname -r3.10.0-1062.18.1.el7.x86_64 #系统版本# cat /etc/os-releaseNAME=\"CentOS Linux\"VERSION=\"7 (Core)\"ID=\"centos\"ID_LIKE=\"rhel fedora\"VERSION_ID=\"7\"PRETTY_NAME=\"CentOS Linux 7 (Core)\"ANSI_COLOR=\"0;31\"CPE_NAME=\"cpe:/o:centos:centos:7\"HOME_URL=\"https://www.centos.org/\"BUG_REPORT_URL=\"https://bugs.centos.org/\"CENTOS_MANTISBT_PROJECT=\"CentOS-7\"CENTOS_MANTISBT_PROJECT_VERSION=\"7\"REDHAT_SUPPORT_PRODUCT=\"centos\"REDHAT_SUPPORT_PRODUCT_VERSION=\"7\" 安装 Docker # 一, 卸载旧的版本$ sudo yum remove docker \\ docker-client \\ docker-client-latest \\ docker-common \\ docker-latest \\ docker-latest-logrotate \\ docker-logrotate \\ docker-engine # 2, 需要安装的包yum install -y yum-utils \\ device-mapper-persistent-data \\ lvm2#3. 设置镜像仓库yum-config-manager \\ --add-repo \\ https://download.docker.com/linux/centos/docker-ce.repo #默认是国外的yum-config-manager \\ --add-repo \\ https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo #推荐使用#更新yum软件包索引yum makecache fast#安装 docker sudo yum install docker-ce docker-ce-cli containerd.io #启动 dockersystemctl start docker#查看 docker 版本docker version 下载镜像 docker pull [要下载的镜像] 查看下载的镜像 docker images (docker image ls) 卸载 docker yum remove docker-ce docker-ce-cli containerd.iorm -rf /var/lib/docker #docker 默认工作路径 Windows Docker 安装(win10)下载官方 Docker-desktop 安装程序 https://www.docker.com/products/docker-desktop 开启 Hyper-V # 开启搜索win + s# 输入启用或关闭windows功能 选中 Hyper-V 配置镜像加速 底层原理Docker 是怎么工作的 Docker 是一个 C/S 结构的系统,Docker 的守护进程运行在主机上,通过 Socket 从客户端访问 DockerServer 接收到 Docker-Client 的指令,就会执行这个命令 Docker 为什么比 VM 快? Docker 有着比虚拟机更少的抽象层 Dcoker 利用的是宿主机的内核,VM 需要的是 Guest OS 新建一个容器的时候,Docker 不需要像虚拟机一样重新安装一个操作系统内核,虚拟机是加载 Guest OS,分钟级别的,而 Docker 是利用宿主机的操作系统,省略了这个复杂的过程 Docker 命令帮助命令docker version #docker版本docker info #显示docker的系统信息,包括镜像和容器数量docker [命令] --help #查看某个具体的命令 镜像命令docker images 查看下载的所有镜像 # docker imagesREPOSITORY TAG IMAGE ID CREATED SIZEmysql 5.6 8de95e6026c3 20 hours ago 302MBredis latest 36304d3b4540 12 days ago 104MBmysql latest 30f937e841c8 2 weeks ago 541MBcentos/mysql-57-centos7 latest f83a2938370c 8 months ago 452MB# 解释REPOSITORY 镜像的仓库名TAG 镜像的标签IMAGE ID 镜像IDCREATED 镜像创建时间SIZE 镜像的大小#可选项Options: -a, --all #列出所有镜像 -q, --quiet #只显示镜像ID docker search 搜索镜像 docker search mysqlNAME DESCRIPTION STARS OFFICIAL AUTOMATED mysql MySQL is a widely used, open-source relation… 9604 [OK] #可选项,通过收藏来过滤--filter=stars=3000 #搜索出来的镜像收藏就是大于3000的 docker pull 拉取镜像 docker pull nginx [:tag]Using default tag: latest #如果不写tag 默认使用最新版本latest: Pulling from library/nginx8559a31e96f4: Pull complete #分层下载,docker image核心 联合文件系统8d69e59170f7: Pull complete 3f9f1ec1d262: Pull complete d1f5ff4f210d: Pull complete 1e22bfa8652e: Pull complete Digest: sha256:21f32f6c08406306d822a0e6e8b7dc81f53f336570e852e25fbe1e3e3d0d0133 #签名Status: Downloaded newer image for nginx:latestdocker.io/library/nginx:latest #真实地址# docker pull nginx 等价于 dicker pull docker.io/library/nginx:latest docker rmi 删除镜像 # 删除指定的镜像$ docker rmi -f 8de95e6026c3 # 删除全部的镜像$ docker rmi -f $(docker images -ap)# 清空临时镜像$ docker rmi $(docker images -q -f dangling=true) docker build 使用dockerfile创建镜像 # 使用当前目录的 dockerfile 创建镜像 当 dockerfile 的命名为 dockerfile 就不需要制定文件名 -f$ docker build -t node:10.15-alpine .$ docker build -f /path/to/a/dockerfile . # /path/to/a 容器命令新建容器并启动 docker run [options] image# options# 若image本地没有则会去 docker镜像库拉取--name=\"\" 容器名字 用于区分容器-d 后台方式运行-it 使用交互方式运行,进入容器查看内容-p 指定容器的端口 -p 80:8080 主机端口:容器端口-P(大写) 随机指定端口 列出所有运行的容器 # docker ps 命令 列出当前正在运行的容器# options-a # 列出当前正在运行的容器+历史运行过的容器-n=? # 显示最近创建的容器-q # 只显示容器的编号$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES$ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES919e58ff5521 redis \"Docker-entrypoint.s…\" 20 hours ago Exited (0) 16 hours ago redis 退出容器 exit #直接容器停止并退出 删除容器 $ docker rm 容器id$ docker rm -f $(docker ps -aq) #删除所有的容器 启动和停止容器 $ docker start 容器id or 容器name # 启动一个或多个已经被停止的容器$ docker restart 容器id or 容器name # 重启容器$ docker stop 容器id or 容器name # 停止运行中的容器$ docker kill 容器id or 容器name # 杀掉运行中的容器 其他常用命令后台启动容器 $ docker run -d 镜像名 查看容器中进程信息 $ docker top 容器id 查看镜像元数据 $ docker inspect 容器id or 容器name 进入当前正在运行的容器 #我们通常容器都是使用后台方式运行的,需要进入容器,修改一些配置#命令# 进入容器后开启一个新的终端,可以在里面操作(常用) 退出 shell 不会导致容器停止运行$ docker exec -it 容器id or name bashshell 默认命令行# 进入容器正在执行的终端,不会启动新的进程 如果退出 shell,容器会停止运行$ docker attach 容器id or 容器name 从容器内拷贝文件到主机上 $ docker cp 容器id: 容器内路径 目的主机路径 docker system命令 # 查看docker磁盘占用情况$ docker system df# 命令可以用于清理磁盘,删除关闭的容器、无用的数据卷和网络$ docker system prune-a # 没有容器使用的 docker 容器都删除 手动清除 docker 镜像/容器/数据卷 # 删除所有 dangling 镜像(即无 tag 的镜像)$ docker rmi $(docker images | grep \"^<none>\" | awk \"{print $3}\")# 删除所有 dangling 数据卷(即无用的 volume)$ docker volume rm $(docker volume ls -qf dangling=true) Docker 镜像镜像是什么镜像就是一个轻量级的,可执行的独立软件包,用来打包软件运行环境和基于运行环境开发的软件,它包含运行某个软件所需的所有内容,包括代码,运行时,库,环境变量和配置文件。 如何得到镜像 从远处仓库下载 拷贝 自己制作一个镜像 dockerfile docker 镜像加载原理 UnionFs (联合文件系统查询) 我们下载的时候看到的一层一层就是这个 UnionFs (联合文件系统): Union 文件系统(UnionFS)是一种分层,轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下, Union 文件系统是 Docker 镜像的基础,镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像 特性: 一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统,联合加载会把各层文件系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录结构 Docker 镜像加载原理 Docker 的镜像实际上由一层一层的文件系统组成,这种层级的文件系统UnionFS bootfs(boot file system)主要包含 bootlloader 和 kernel, bootfs 主要是引导加载 kernel, Linux 刚启动时会加载 bootfs 文件系统,在 Docker 镜像的最底层是 bootfs, 这一层与我们典型的 Linux/Unix 系统是一样的,包含 boot 加载器和内核,当 boot 加载完成之后整个内核就在内存中了,此时内存的使用权已由 bootfa 转交给内核,此时系统也会卸载 bootfs rootfs(root file system),在 bootfs 之上,包含的就是典型 Linux 系统中的 /dev, /proc,/bin, /etc 等标准目录和文件, rootfs 就是各种不同的操作系统发行版,比如 Ubuntu, CentOS 等等 分层理解镜像下载的时候是一层一层的在下载 思考: 为什么 Docker 镜像要采用这种分层的结构呢? 最大好处,我觉得莫过于资源共享了!比如有多个镜像都从相同的 Base 镜像构建而来,那么宿主机 只需在磁盘上保留一份 Base 镜像,同时内存中也只需要加载一份 Base 镜像,这样就可以为所有的容器服务了,而且镜像的每一层都可以被共享 查看镜像分层的方式可以通过 Docker image inspect 命令! commit 镜像$ docker commit 提交容器成为一个新的镜像#命令和git原理类似$ docker commit -m=\"提交的描述信息\" -a=\"作者\" 容器ID 目标镜像名:[tag] 容器数据卷什么是容器数据卷Docker 的理念回顾 将应用和环境都打包成一个镜像! 如果数据都在容器中,那么我们容器删除,数据就会丢失! 需求: 数据可以持久化 MySQL,容器删了,数据丢失. 需求:MySQL 数据可以存储到本地 容器之间可以有一个数据共享的技术!Docker 容器中产生的数据,同步到本地 目录的挂载,将容器内的目录挂载到 Linux 上面 总结一句话: 容器的持久化和同步操作! 容器间也可以数据共享的! 使用数据卷 方法一:直接使用命令来挂载 -v $ docker run -it -v 主机目录:容器内目录 -p 主机端口:容器端口# 启动起来我们可以使用 docker inspect 容器id 实战:安装 MySQL# 获取镜像$ docker pull mysql:5.7# 运行容器,需要做数据挂载! # 安装启动mysql,需要配置密码,这是官方的# 官方测试: docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=密码 -d mysql:tag#启mysql-d 后台运行-p 端口映射-v 端口映射-e 环境配置--name 容器名$ docker run -d -p 3310:3306 -v /home/mysql/conf:/etc/mysql/conf.d -v /home/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=root --name mysql01 mysql:5.7# 启动成功之后,我们在本地使用navicat来测试连接#navicat-连接到服务器的3310 --- 3310和容器内的3306映射,这个时候我们就可以连接上了 假设我们将容器删除,挂载到本地 的数据卷依旧没有丢失,这就实现了容器数据持久化功能 具名和匿名挂载# 匿名挂载-v 容器内路径$ docker run -d -p 8080:80 --name nginx01 -v /etc/nginx nginx:alpine# 查看所有的volume情况$ docker volume lsDRIVER VOLUME NAMElocal b448950f96ca2daed2a90cd21e687431653dc9a2f40ccf51e0ce38432f6564a4# 这个就是匿名挂载,-v时只写了容器内路径,没有写# 通过 -v 卷名:容器内路径 所有的 Docker 容器内的卷,没有指定目录的情况下都是在 /var/lib/Docker/volumes/ 卷名 /_data 我们通过具名挂载可以方便的找到一个卷,大多数情况在使用的具名挂载 # 如何确定是具名挂载还是匿名挂载还是指定路径挂载-v 容器内路径 # 匿名挂载-v 卷名:容器内路径 # 具名挂载 拓展: #通过 -v 容器内路径: ro rw 改变读写权限# ro read only# read and write#一旦设置了容器权限,容器对挂载出来的内容就有限定了!docker -run -P -name nginx01 -v /etc/nginx:ro nginxdocker -run -P -name nginx01 -v /etc/nginx:rw nginxro : 只要看到ro就说明这个路径只能通过宿主机来改变,容器内部无法操作 dockerfiledockerfile 是用来构建 docker 镜像的文件,命令参数脚本。 构建步骤: 编写一个 dockerfile 文件 docker build 构建成为一个镜像 docker run 镜像 docker push 发布镜像(dockerHub,阿里云镜像仓库) 很多官方镜像都是基础包,很多功能都是没有的,我们通常自己创建自己的镜像。 Dockerfile的构建过程基础知识: 每个保留关键字(指令)都必须是大写字母 执行从上到下顺序执行 #表示注释 每一条命令都会创建提交一个镜像层,并提交 Dockerfile 是面向开发的,我们以后要发布项目,做镜像,就需要编写 Dockerfile 文件,这个文件十分简单。 Docker 镜像逐渐成为企业交付的标准,必须要掌握 步骤: 开发,部署,运维 Dockerfile:构建文件,定义了一切步骤,源代码 DockerImages:通过 Dockerfile 构建生成的镜像,最终发布和运行的产品 Docker容器:容器就是镜像运行起来提供服务器 Dockerfile的指令FROM # 基础镜像, 一切从这里开始构建MANTAINER # 镜像是谁写的, 姓名+邮箱RUN # 镜像构建的时候需要运行的命令ADD # 步骤, tomcat镜像,压缩包! 添加内容WORKDIR # 镜像的工作目录VOLUME # 挂载的目录EXPOSE # 暴露端口配置CMD # 指定这个容器启动的时候要运行的命令,只有最后一个会生效,可被替代ENTRYPOINT # 指定这个容器启动的时候要运行的命令,可以追加命令ONBUILD # 当构建一个被继承 DockerFile 这个时候就会运行ONBUILD的指令,触发指令COPY # 类似ADD,将我们文件拷贝到镜像中ENV # 构建的时候设置环境变量 实战测试docker Hub 中99%镜像都是从 CentOS 基础镜像过来的,然后配置需要的软件 创建一个自己的 CentOS # 1. 编写 dockerfile 文件FROM centosMAINTAINER jiawei<[email protected]>ENV MYPATH /usr/localWORKDIR ${MYPATH}RUN yum -y install vim && yum -y install net-toolsEXPOSE 80CMD echo ${MYPATH} && echo \"--end--\" && /bin/sh# 2. 通过这个文件构建镜像docker build -f <dockerfile文件目录> -t <镜像名:[tag]> .# 3. 测试 CMD 和 ENTRYPOINT 的区别 CMD # 指定这个容器启动的时候要运行的命令,只有最后一个会生效,可被替代ENTRYPOINT # 指定这个容器启动的时候要运行的命令,可以追加命令 测试 CMD # 编写 dockerfile 文件$ vim dockerfile-cmd-testFROM centosCMD [\"ls\", \"-a\"]# 构建镜像$ docker build -f dockerfile-cmd-test -t centos .# run 运行,发现ls -a生效$ docker run 963149b1ac5d....dockerenvbindevetchomeliblib64lost+foundmediamntoptprocrootrunsbinsrvsystmpusrvar# 想要追加一个命令 -l ls -al$ docker run 963149b1ac5d -ldocker: Error response from daemon: OCI runtime create failed: container_linux.go:349: starting container process caused \"exec: \\\"-l\\\": executable file not found in $PATH\": unknown.ERRO[0000] error waiting for container: context canceled # cmd的情况下 替换了CMD[\"ls\",\"-a\"]命令,-不是命令追加 ENTRYPOINT 是往命令之后追加 # 编写dockerfile文件$ vim dockerfile-cmd-testFROM centosENTRYPOINT [\"ls\", \"-a\"]# 构建镜像$ docker build -f dockerfile-cmd-test -t centos .# run 运行,发现ls -a生效$ docker run 963149b1ac5d....dockerenvbindevetchomeliblib64lost+foundmediamntoptprocrootrunsbinsrvsystmpusrvar# 想要追加一个命令 -l ls -al$ docker run 963149b1ac5d -l# 这里是生效的 实战:Tomcat镜像 准备镜像文件 tomcat 压缩包,jdk 压缩包 编写 Dockerfile 文件,官方命名 Dockerfile,build 会自动寻找这个文件,就不需要 -f 指定文件了 FROM centosMAINTAINER czp<[email protected]>COPY readme.txt /usr/local/readme.txtADD apache-tomcat-9.0.33.tar.gz /usr/local/ADD jdk-8u221-linux-x64.rpm /usr/local/RUN yum -y install vim ENV MYPATH /usr/local WORKDIR $MYPATH ENV JAVA_HOME /usr/local/jdk1.8.0_11ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jarENV CATALINA_HOME /usr/local/apache-tomcat-9.0.33ENV CATALINA_BASH /usr/local/apache-tomcat-9.0.33# 配置环境变量ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/lib:/CATALINA_HOME/binEXPOSE 8080CMD /usr/local/apache-tomcat-9.0.33/bin/startup.sh && tail -F /usr/local/apache-tomcat-9.0.33/bin/logs/catalina.out 构建镜像 # docker build -t diytomcat . 本地测试 curl localhost:9090 发布镜像 Dockerhub 地址 hub.docker.com 注册自己的账号! 确定这个账号可以登录 在服务器上提交自己的镜像 $ docker login --helpUsage: docker login [OPTIONS] [SERVER]Log in to a docker registry.If no server is specified, the default is defined by the daemon.Options: -p, --password string Password --password-stdin Take the password from stdin -u, --username string Username 登录完毕就可以提交镜像了,就是一步 docker push 提交到阿里云镜像仓库 登录阿里云 找到容器镜像服务 创建命名空间 创建容器镜像 浏览阿里云 小结 Docker 网络原理理解 Docker0清空所有环境 测试 三个网络 # docker 是如何处理容器网络访问的?$ docker run -d -P --name tomcat01 tomcat# 查看容器内部网络地址 ip addr 发现容器启动的时候会得到一个eth0@if8 ip地址,docker分配的$ docker exec -it tomcat01 ip addr1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever2: sit0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000 link/sit 0.0.0.0 brd 0.0.0.07: eth0@if8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0 valid_lft forever preferred_lft forever# linux能ping通容器内部 原理 我们每启动一个 docker 容器,docker 就会给 docker 容器分配一个 ip,我们只要安装了 docker,就会有一个网卡 Docker0 桥接模式,使用的是 veth-pair 技术 再次测试 ip addr 再启动一个容器,发现又多了一对网卡 # 我们发现这个容器带来网卡, 都是一对对的# veth-pair 就是一对虚拟机设备接口,他们都是成对出现的,一端连着协议,一端彼此相连# 正因为有这个特性,veth-pair 充当桥梁,连接各种虚拟网络设备的# openStac,Docker容器之间的连接,OVS的连接,都是使用 veth-pair 技术 测试 tomcat01 和 tomcat02 是否能 ping 通 # 结论:容器和容器之间是可以互相ping通的 结论: tomcat01 和 tomcat02 是共用的一个路由器, Docker0 所有的容器不指定网络的情况下,都是 Docker0 路由的, Docker 会给我们的容器分配一个默认的可用IP 小结 Docker 使用的是 Linux 的桥接,宿主机是一个 Docker 容器的网桥 Docker0 Docker 中所有的网络接口都是虚拟的,虚拟的转发效率高(内网传递文件) 只要容器删除,对应网桥的一对就没了 –link 思考一个场景,我们编写了一个微服务,database url = ip;项目不重启,数据库 ip 改变了,我们希望可以处理这个问题,可以通过名字来访问容器吗? $ docker exec -it tomcat02 ping tomcat01ping: tomcat01: Name or service not known# 通过--link可以解决网络连接问题$ docker run -d -P --name tomcat03 --link tomcat02 tomcat6aedb0ba2e798b184f42f98e4a38ce2a54cb97d47b985d17065b064a7f73d404$ docker exec -it tomcat03 ping tomcat02PING tomcat02 (172.17.0.3) 56(84) bytes of data.64 bytes from tomcat02 (172.17.0.3): icmp_seq=1 ttl=64 time=0.061 ms64 bytes from tomcat02 (172.17.0.3): icmp_seq=2 ttl=64 time=0.040 ms64 bytes from tomcat02 (172.17.0.3): icmp_seq=3 ttl=64 time=0.040 ms64 bytes from tomcat02 (172.17.0.3): icmp_seq=4 ttl=64 time=0.082 ms64 bytes from tomcat02 (172.17.0.3): icmp_seq=5 ttl=64 time=0.039 ms^C--- tomcat02 ping statistics ---5 packets transmitted, 5 received, 0% packet loss, time 134msrtt min/avg/max/mdev = 0.039/0.052/0.082/0.018 ms# 反向是否可以ping通吗[root@CZP ~]# docker exec -it tomcat02 ping tomcat03 -link 本质就是在 hosts 中添加映射 我们现在玩 docker 已经不建议使用 –link 了! 自定义网络,不使用 Docker0! Docker0 问题: 它不支持容器名连接访问! 自定义网络 查看所有的 Docker 网络 $ docker network lsNETWORK ID NAME DRIVER SCOPE86c70406cec4 bridge bridge locale2cd35c81ffb host host localc6fe6b78ab62 none null local 网络模式 bridge: 桥接模式 docker 搭桥(默认) none: 不配置网络 host:和宿主机共享网络 container: 容器内网络连通(用得少,局限很大) # 直接启动的命令 --net brodge,默认docker0docker run -d -P --name tomcat01 --net bridge tomcat# docker0的特点: 默认的,域名是不能访问的, --link可以打通连接# 自定义网络# --driver bridgedocker network create --driver bridge --subnet 192.168.0.0/16 --gateway 192.168.0.1 mynet87d0f163b3a0c857d281bf4e97675d03555486c530969d1cb04950f203133b55$ docker network lsNETWORK ID NAME DRIVER SCOPE86c70406cec4 bridge bridge locale2cd35c81ffb host host local87d0f163b3a0 mynet bridge localc6fe6b78ab62 none null local 自己的网络创建好了 $ Docker network inspect mynet[ { \"Name\": \"mynet\", \"Id\": \"87d0f163b3a0c857d281bf4e97675d03555486c530969d1cb04950f203133b55\", \"Created\": \"2020-07-08T01:56:39.0611734Z\", \"Scope\": \"local\", \"Driver\": \"bridge\", \"EnableIPv6\": false, \"IPAM\": { \"Driver\": \"default\", \"Options\": {}, \"Config\": [ { \"Subnet\": \"192.168.0.0/16\", \"Gateway\": \"192.168.0.1\" } ] }, \"Internal\": false, \"Attachable\": false, \"Ingress\": false, \"ConfigFrom\": { \"Network\": \"\" }, \"ConfigOnly\": false, \"Containers\": {}, \"Options\": {}, \"Labels\": {} }]$ docker run -d -P --name tomcat-net-01 --net mynet tomcatf8acd6bd8a21c27ca293d4c2d150448299192bd1f58b41d273d61d24cfe7d9a8$ docker run -d -P --name tomcat-net-02 --net mynet tomcat84b8b3a4a45c579eb479dfa036bc6e88f2c4ea5a0e8edd0c8f225bddebb2747c$ docker network inspect mynet[ { \"Name\": \"mynet\", \"Id\": \"87d0f163b3a0c857d281bf4e97675d03555486c530969d1cb04950f203133b55\", \"Created\": \"2020-07-08T01:56:39.0611734Z\", \"Scope\": \"local\", \"Driver\": \"bridge\", \"EnableIPv6\": false, \"IPAM\": { \"Driver\": \"default\", \"Options\": {}, \"Config\": [ { \"Subnet\": \"192.168.0.0/16\", \"Gateway\": \"192.168.0.1\" } ] }, \"Internal\": false, \"Attachable\": false, \"Ingress\": false, \"ConfigFrom\": { \"Network\": \"\" }, \"ConfigOnly\": false, \"Containers\": { \"84b8b3a4a45c579eb479dfa036bc6e88f2c4ea5a0e8edd0c8f225bddebb2747c\": { \"Name\": \"tomcat-net-02\", \"EndpointID\": \"889a15d10cf311193a18033af3a75eefa6a074291e84aab65e9d88f4b9889bf2\", \"MacAddress\": \"02:42:c0:a8:00:03\", \"IPv4Address\": \"192.168.0.3/16\", \"IPv6Address\": \"\" }, \"f8acd6bd8a21c27ca293d4c2d150448299192bd1f58b41d273d61d24cfe7d9a8\": { \"Name\": \"tomcat-net-01\", \"EndpointID\": \"810c98a4ee532167410f1bc28acbc1d3aac11390e7c5a0c0864c20832bf06fb6\", \"MacAddress\": \"02:42:c0:a8:00:02\", \"IPv4Address\": \"192.168.0.2/16\", \"IPv6Address\": \"\" } }, \"Options\": {}, \"Labels\": {} }]# 再次测试ping连接$ docker exec -it tomcat-net-01 ping 192.168.0.3PING 192.168.0.3 (192.168.0.3) 56(84) bytes of data.64 bytes from 192.168.0.3: icmp_seq=1 ttl=64 time=0.056 ms64 bytes from 192.168.0.3: icmp_seq=2 ttl=64 time=0.156 ms64 bytes from 192.168.0.3: icmp_seq=3 ttl=64 time=0.086 ms64 bytes from 192.168.0.3: icmp_seq=4 ttl=64 time=0.037 ms^C--- 192.168.0.3 ping statistics ---4 packets transmitted, 4 received, 0% packet loss, time 162msrtt min/avg/max/mdev = 0.037/0.083/0.156/0.046 ms# 现在不使用--link也可以ping容器名字$ docker exec -it tomcat-net-02 ping 192.168.0.2PING 192.168.0.2 (192.168.0.2) 56(84) bytes of data.64 bytes from 192.168.0.2: icmp_seq=1 ttl=64 time=0.039 ms64 bytes from 192.168.0.2: icmp_seq=2 ttl=64 time=0.066 ms^C--- 192.168.0.2 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 47msrtt min/avg/max/mdev = 0.039/0.052/0.066/0.015 ms 自定义网络 docker 都帮我们维护好了对应关系,推荐平时这样使用网络! 好处: 不同的集群使用不同的集群,保证集群之间是安全和健康的 网络连通 #测试打通 tomcat01到tomcat-net-01$ docker network connect mynet tomcat01# 连通之后就是将 tomcat01 放到了mynet网络下# 一个容器两个ip 阿里云: 公网ip 私网ip # 01 连通ok$ docker exec -it tomcat01 ping tomcat-net-01PING tomcat-net-01 (192.168.0.2) 56(84) bytes of data.64 bytes from tomcat-net-01.mynet (192.168.0.2): icmp_seq=1 ttl=64 time=0.087 ms64 bytes from tomcat-net-01.mynet (192.168.0.2): icmp_seq=2 ttl=64 time=0.065 ms^C--- tomcat-net-01 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 71msrtt min/avg/max/mdev = 0.065/0.076/0.087/0.011 ms# 02 依旧是连不通的$ docker exec -it tomcat02 ping tomcat-net-01ping: tomcat-net-01: Name or service not known 结论:要跨网络操作别人,就需要使用 docker network connect 连通","categories":[{"name":"技术","slug":"技术","permalink":"https://kaviilee.github.io/blog/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"Docker","slug":"docker","permalink":"https://kaviilee.github.io/blog/tags/docker/"}],"author":{"name":"Kaviilee","avatar":"https://cdn.jsdelivr.net/gh/Kaviilee/[email protected]/blog/images/custom/papalymo.jpg"}},{"title":"js获取视口和文档高度","slug":"js获取视口和文档高度","date":"2019-09-07T17:25:18.000Z","updated":"2023-01-02T18:31:29.271Z","comments":true,"path":"2019/09/07/js获取视口和文档高度/","link":"","permalink":"https://kaviilee.github.io/blog/2019/09/07/js%E8%8E%B7%E5%8F%96%E8%A7%86%E5%8F%A3%E5%92%8C%E6%96%87%E6%A1%A3%E9%AB%98%E5%BA%A6/","excerpt":"js获取视口和文档高度","text":"js获取视口和文档高度 项目 内容 clientHeight content area + padding offsetHeight border + content area + padding scrollHeight 没有滚动条(内容展开时)的高度 body 和 html 分别表示 document.body 和 document.documentElement html.clientHeight 返回视口高度html.offsetHeight 返回 <html> 元素的高度,在没有给 <html> 元素设置height时,可以理解为文档高度。ie6-8返回视口高度html.scrollHeight 总是返回文档高度。在 firefox,opera,ie8 中,返回文档高度和视口高度中较大的那个。body.clientHeight 和 body.offsetHeight 返回 <body> 元素的高度(近似于文档高度),如果 <body> 设置 height,则返回设定的值(ie6 仍然返回 <body> 内元素的总高度)。body.scrollHeight 总是返回文档高度。在 webkit 中,返回文档高度和视口高度中较大的那个。 总之,获取视口高度用 html.clientHeight ,IE6-8 还可以用 html.offsetHeight。获取文档高度可以用 html.scrollHeight 或 body.scrollHeight。 另外,现代浏览器中还有一个属性 window.innerHeight 可以用来获取视口高度,IE9+ 才开始支持。得到的高度有时候会多十几个像素,innerHeight 把滚动条的高度也计算在内。 // 获取视口高度const viewportH = window.innerHeight || document.documentElement.clientHeight;// 获取文档高度const docH = document.body.scrollHeight;// 或者const docH = document.documentElement.scrollHeight","categories":[{"name":"JavaScript","slug":"javascript","permalink":"https://kaviilee.github.io/blog/categories/javascript/"}],"tags":[],"author":{"name":"Kaviilee","avatar":"https://cdn.jsdelivr.net/gh/Kaviilee/[email protected]/blog/images/custom/papalymo.jpg"}}],"categories":[{"name":"前端工程化","slug":"前端工程化","permalink":"https://kaviilee.github.io/blog/categories/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/"},{"name":"JavaScript","slug":"javascript","permalink":"https://kaviilee.github.io/blog/categories/javascript/"},{"name":"CSS","slug":"css","permalink":"https://kaviilee.github.io/blog/categories/css/"},{"name":"框架","slug":"框架","permalink":"https://kaviilee.github.io/blog/categories/%E6%A1%86%E6%9E%B6/"},{"name":"技术","slug":"技术","permalink":"https://kaviilee.github.io/blog/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"javascript","slug":"javascript","permalink":"https://kaviilee.github.io/blog/tags/javascript/"},{"name":"算法","slug":"算法","permalink":"https://kaviilee.github.io/blog/tags/%E7%AE%97%E6%B3%95/"},{"name":"面经","slug":"面经","permalink":"https://kaviilee.github.io/blog/tags/%E9%9D%A2%E7%BB%8F/"},{"name":"其他","slug":"其他","permalink":"https://kaviilee.github.io/blog/tags/%E5%85%B6%E4%BB%96/"},{"name":"webpack","slug":"webpack","permalink":"https://kaviilee.github.io/blog/tags/webpack/"},{"name":"TypeScript","slug":"typescript","permalink":"https://kaviilee.github.io/blog/tags/typescript/"},{"name":"Antd v4","slug":"antd-v4","permalink":"https://kaviilee.github.io/blog/tags/antd-v4/"},{"name":"CSS","slug":"css","permalink":"https://kaviilee.github.io/blog/tags/css/"},{"name":"react","slug":"react","permalink":"https://kaviilee.github.io/blog/tags/react/"},{"name":"规范","slug":"规范","permalink":"https://kaviilee.github.io/blog/tags/%E8%A7%84%E8%8C%83/"},{"name":"git","slug":"git","permalink":"https://kaviilee.github.io/blog/tags/git/"},{"name":"Docker","slug":"docker","permalink":"https://kaviilee.github.io/blog/tags/docker/"}]}