当前位置:网站首页>我希望按照我的思路尽可能将canvas基础讲明白
我希望按照我的思路尽可能将canvas基础讲明白
2022-06-25 09:39:00 【请打码】
写在前面
canvas很多人写过,我之前的博客里面也写过关于canvas的教程,但是后面我觉得其实不太好,因为很多东西都是很模糊的,没有非常直观清晰的将canvas讲解明白,究其原因,还是这个属性使用的不够多,导致很多属性不够熟练,但是我希望这篇文章可以将这个属性彻底的讲明白,毕竟只是一个标签而已,怎么讲都不会太复杂,他之所以不太好学原因就在于他自带的方法太多,加上很多的效果都是需要方法之间的相互配合使用,所以难度和复杂度就直接升高了很多,它不像html的其他标签一样,比如p、span等都只是自带了一些样式罢了,但是没有自带那么多的方法和属性,但是我们学的时候其实完全可以不用全部搞明白,而且canvas其实也就只有两个属性,width和height,他的复杂度来源于它的API,我们其实只需要将他的方法的基本用法进行一个记录和认识,到我们真实需要这个业务的时候再进行查阅文档也是可以的,毕竟精力有限,当然如果你的时间很多,你完全可以进行视频文档等全部看完,将所有方法都进行一个尝试,包括一些常见的动画组合等等都是可以的!博主我也是肝了几个夜晚,写了很多例子,也尝试了很多技巧展示给你们才将这篇文章全部写完的, 看完点个赞也不过分吧!文章篇幅有点长,慢慢阅读!
canvas介绍
canvas是H5新特性里面的一个属性,说白了,就是一个html的新标签,和div、p、span都是一样的,可以直接被浏览器解析的html语言,所以我们从心里上不要排斥它,也不要害怕它,我们就把它做当一个div来学就可以了!(其实这段是我自己安慰自己的),为什么要有canvas呢?其实在他出现之前,我们的动画都是逗比(Adobe)公司的Flash技术支持的,他的一个很大的问题就在于比较重,安装的时候就发现了,安装的文件其实很大,所以慢慢就被淘汰了,可以说canvas其实是给了Flash一记重拳,落后就要挨打嘛,正常!但是也不是说canvas就完全是好的,他也有一些弊端,本章内容会大概的介绍一下canvas的一些问题和特性!
canvasAPI
怎么学?
这个问题其实我在没有学canvas之前,思考了很久,虽然我直到这篇文章完结的时候我都没有完全掌握canvas的使用,但是我已经不惧怕这个技术点了,因为知道了他是怎么回事,这种感觉可能很多人都体会过,就是一门技术,你突然觉得他非常的简单,可能只是某一些效果做起来很复杂,但是不至于没有任何的思路,只是代码编写的时候需要点时间罢了,这里我说一下我当时怎么看明白的
- 学习的第一点:他仅仅只是一个HTML标签
学习一个新的知识点,搞明白他的本质很重要,所以这一点不是废话,可能有人看到之后就说,我当然知道他是一个标签,但是你从心里没有接受他是一个标签,因为它很重,这个重是相对于别的html标签来说的,正常的标签就只是一个简单的字带样式的功能块而已,所以学习的时候本身就不会太繁重!所以我们学习canvas的时候也仅仅把它当做一个简单的标签进行学习即可!
- 学习的第二点:他的绘图功能和他本身没有任何关系
这句话不是抬杠,也不是错的,它本身只是提供了和别的htm标签一样的功能,提供了一块区域而已,至于它强大的绘图功能,是他的API,和他本身属性没有任何关系,有人就说了,那我直接使用div为什么不行呢?所以说我要你理解我第一句话,也就是我说他只是提供了一块区域而已,这块区域就是提供给API使用的,所以这里不要抬杠,它本身就只有width和height两个属性!所以,他的难点也只是在他的API上!我们要学的是他的方法,而不是它本身!那么使用的过程中我们大部分的场景使用的都是基于2d场景开发,也就是说,不管你是不是很熟悉canvas的使用,开始的都应该明白怎么写,
//因为都是基于canvasAPI进行开发的,所以我们首先要将标签的上下文获取到,后面具体怎么实现,是根据实际业务进行的,所以你最多可以说你不会他的API,不可以说你不会canvas let canvas = document.getElementById('canvas') let cas = canvas.getContext('2d')
- 学习的第三点:他的绘图功能基于他的API
这句话看起来有些废话,但是我们尝试理解,就好像我们看到这些:
其实就是将这些或者这里面的某一些关键方法学会即可,他的方法看起来很多,但是其实很多都是一些属性很简单的方法,比如beginPath(),fill()等都只是一些方法,甚至没有任何属性参数值的!所以一个很关键的点在于你有没有耐心将文档上面出现的方法属性进行尝试,很多东西你只要尝试了一下,就会非常的确定他的用法,人们对未知的事物才会恐惧,我们知道了他的本质,自然就不会恐惧!
- 学习的第四点:通过写简单的Demo,拼合成复杂的应用的过程
这是一种学习的方法,当一个应用或者动画被你看起来很复杂的时候,你只需要将它的动画拆分开,举个例子,运动的小球是canvas里比较经典的绘制例子,初次看到的时候我也觉得怎么怎么复杂,后来我慢慢的研究了一下他的实现过程,发现其实并不复杂,这是代码量比较大,拆开看,绘制一个小球、让他运动、生成随机数提供给运动轨迹、做一个计时器进行重复绘制和运动、这个看起来复杂的功能应用就实现了,前面的四步哪一步是复杂的?所以下面我会写几个Demo,都是非常简单的,入门级别的canvas的Demo,但是你可以使用这几个Demo进行拼合成你自己想要的一些看起来比较复杂的应用!下面demo将canvas中使用频率比较高的几个API进行了演示,可以直接运行到你的本地html文件中进行查看效果!
- 学习的第五点:参数的特殊说明
我在第二点的时候说我们大部分的场景都是基于2d上下文进行开发的,但是不代表他只有2d这一个参数,canvas为我们提供了’webgl’或’experimental-webgl’、webgl2、bitmaprenderer,本篇文章不会对这三个参数做详细的介绍,这虽然是四个参数,但是前面两个是同一个,只是第二个是IE针对第一个参数的做的兼容,感兴趣的可以自己去看详细的关于这三个参数的介绍!参数介绍
canvas需要明确的特性
canvas兼容性不太好,需要做浏览器的兼容性处理
我们都知道canvas是H5的新特性,也就意味着,不支持H5的浏览器版本自然也就不支持canvas,所以我们需要在写canvas的时候,写上一句兼容性的提示语句,比如如下代码:
<canvas> 该浏览器版本不支持canvas,请升级浏览器或者使用Chrome浏览器打开 </canvas>
canvas不具备将画布内容重新获取的能力
canvas不具备将画布内容重新获取的能力,解释一下这句话,我们在画布上绘制了一个图形之后,想要获取到这个图形,是不可以的,canvas不具备获取该图形的能力,那么canvas是怎么实现动画的呢?他的一个实现思路比较暴力,通过不停的清屏-》重绘的操作进行动画的实现!说白了就是不停的将之前已经画上去的图形删除,重新再绘制一次,只是下一次和上一次的位置不一样,连续不停的清除显示的过程就是动画的过程,每一个静态图形都是一帧,写个demo,小试一下,后面会详细的说明绘制的过程!该demo只是为了演示canvas的动画是怎么生成的。这里说一下,逗比公司的Flash技术是可以进行获取到动画本身的,这也是为什么他比较重的一个原因!
<!-- * @use: 直接浏览器运行即可 * @description: 画布基本动画展示 * @SpecialInstructions: 下方的Demo中出现的cas均为该Html中的canvas * @Author: clearlove * @Date: 2022-05-28 02:26:50 * @LastEditTime: 2022-05-28 02:39:51 * @FilePath: /universal-background-template/Users/leimingwei/Desktop/开源代码/canvas.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style> canvas {
border: 1px solid rebeccapurple; } </style>
</head>
<body>
<canvas id="cas" width="600" height="600">
该浏览器版本不支持Canvas,请升级浏览器或者使用Chrome浏览器打开
</canvas>
</body>
<script> // 下面的例子只是演示说明js部分应该如何写,实际开发中不建议这样写,尽可能的将每一个需要的功能进行封装 let canvas = document.getElementById('cas') let ctx = canvas.getContext('2d') //设置一个背景颜色 ctx.fillStyle = 'grep' //设置一个单位量 运动的偏移量 let offsetLeft = 50 //开始绘制动画 setInterval(() => {
//清空画布 clearCanvas(ctx,0, 0, 600, 600) //设置移动偏移量 offsetLeft++ //开始绘制 在X轴运动 绘制一个在Y轴300位置,50*50的矩形 ctx.fillRect(offsetLeft, 300, 50, 50) }, 10) /** * @function clearCanvas 清空画布 * @params O {String} canvas内容对象 * @params startX {Number} 清空的X坐标开始位置 * @params startY {Number} 清空的Y坐标开始位置 * @params W {Number} 清空的宽度 * @params H {Number} 清空的高度 * */ function clearCanvas(O, startX, startY, W, H) {
O.clearRect(startX, startY, W, H) } </script>
</html>
- 运行结果
- 类的思想进行修改上述代码 实现一个运动的矩形 Demo-1 fillRect、strokeRect
let canvas = document.getElementById('cas')
let ctx = canvas.getContext('2d')
console.trace(ctx.canvas.clientWidth)
//获取到canvas的宽度
let casWidth = ctx.canvas.clientWidth
//声明一个绘制的类(方法)X:x轴开始位置、Y:y轴开始位置、W:图形宽度、H:图形高度、C:图形的背景颜色
function Drawgraphics(X, Y, W, H, C) {
this.X = X
this.Y = Y
this.W = W
this.H = H
this.C = C
}
//更新X轴位置
Drawgraphics.prototype.updateLocation = function () {
this.X++
}
//开始绘制画布
Drawgraphics.prototype.startFillRect = function () {
ctx.fillStyle = this.C
ctx.fillRect(this.X, this.Y, this.W, this.H)
}
//绘制矩形边框
Drawgraphics.prototype.startStroke = function () {
ctx.strokeStyle = this.C
ctx.strokeRect(this.X, this.Y, this.W, this.H)
}
//声明一个绘制矩形的实例
let R = new Drawgraphics(50, 50, 50, 50, '#cccccc')
//声明一个绘制矩形边框的实例
let S = new Drawgraphics(80, 80, 50, 50, '#cccccc')
//开始绘制动画
let stopFlag = setInterval(() => {
//清空画布
clearCanvas(ctx, 0, 0, 600, 600)
//设置移动偏移量
R.updateLocation()
//开始绘制 在X轴运动 绘制一个在Y轴300位置,50*50的矩形
R.startFillRect()
//设置移动偏移量
S.updateLocation()
//绘制一个矩形边框
S.startStroke()
//触及边缘停止
if(S.X >= casWidth - 50){
clearInterval(stopFlag)
}
}, 10)
// 清空画布 {注释如上代码}
function clearCanvas(O, startX, startY, W, H) {
O.clearRect(startX, startY, W, H)
}
- 运行效果
- 绘制不规则图形 Demo-2 moveTo、lineTo
let canvas = document.getElementById('cas')
let ctx = canvas.getContext('2d')
//创建一个路径
ctx.beginPath()
//起点 移动绘制点
ctx.moveTo(50, 50)
//第一个点的位置 描述绘制路径
ctx.lineTo(400, 50)
//第二个点的位置
ctx.lineTo(400, 450)
//第三个点的位置
ctx.lineTo(50, 150)
//关闭图形 不关闭的话,绘制线条最后是不会封闭的
ctx.closePath()
//填充颜色
ctx.strokeStyle = 'red'
//绘制线条 描边
ctx.stroke()
//填充 颜色
ctx.fillStyle = 'orange'
//填充
ctx.fill()
- 绘制一个圆弧 Demo-3 stroke
let canvas = document.getElementById('cas')
let ctx = canvas.getContext('2d')
ctx.beginPath()
/** * @params1 圆心的X坐标 * @params2 圆心的Y坐标 * @params3 圆弧的半径 * @params4 开始的角度 * @params5 结束的角度 * @params6 绘制方向 {true 逆时针绘制 false 顺时针绘制} * @desc {params4,params5} 角度的计算方式:Math.PI 一个圆进行分为6.28份(3.14 * 2) 也就是6.28个弧度 0 是水平位置 Math.PI * 2 是一个圆 此时有一个技巧,如果params5 - params4 > 6.28 并且是顺时针 那么绘制出来的一定是一个圆 * */
ctx.arc(300, 300, 100, 0, 3.18, false)
ctx.strokeStyle = 'red'
ctx.stroke()
- 实现小球跟随鼠标 Demo-4 arc
let canvas = document.getElementById('cas')
let ctx = canvas.getContext('2d')
//清除当前的画布
function clearCanvas() {
ctx.clearRect(0, 0, 600, 600)
}
//绘制一个小球
function drawRound(startX, startY, len, startAngle, endAngle, Directtion, C) {
ctx.beginPath()
ctx.arc(startX, startY, len, startAngle, endAngle, Directtion)
ctx.strokeStyle = C
ctx.stroke()
ctx.fillStyle = C
ctx.fill()
}
//获取到当前的鼠标坐标 以当前的canvas的左上角作为顶点
function getMousePosition(event) {
var e = event || window.event;
console.log(e.offsetX, e.offsetY)
clearCanvas()
drawRound(e.offsetX, e.offsetY, 30, 0, Math.PI * 2, false, getRadomColor())
}
//生成一个随机颜色值
function getRadomColor() {
let types = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 'a', 'b', 'c', 'd', 'e', 'f']
let color = '#'
for (let i = 0; i < 6; i++) {
let radom = Math.floor(Math.random() * types.length)
color += types[radom]
}
return color
}
//当前的画布添加一个鼠标移动的事件,进行更新小球的位置
canvas.addEventListener('mousemove', function (event) {
getMousePosition(event)
})
- 在画布上进行书写文字 Demo-5 fillText
let canvas = document.getElementById('cas')
let ctx = canvas.getContext('2d')
//这里需要注意的事:{如果这里没有该字体的话,那么这里是不会进行展示的,也就是他的大小也不会生效}
ctx.font = '48px SimSun, Songti SC'
//添加一个透明度 {大部分是用来处理图片 这里只是举个例子进行处理一段文字}
ctx.filter = 'opacity(25%)'
//400是字体占据的宽度
ctx.fillText("this is canvas",24,66,400)
- 在画布上绘制图片 Demo-6 drawImage
let canvas = document.getElementById('cas')
let ctx = canvas.getContext('2d')
let imgUrl = new Image()
imgUrl.src = 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.jj20.com%2Fup%2Fallimg%2F4k%2Fs%2F02%2F2109242342133248-0-lp.jpg&refer=http%3A%2F%2Fimg.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1656381685&t=0729cacb65159f78af95f6d9c78bed44'
console.trace(imgUrl)
//图片的绘制 必须在onload之后进行绘制,否则图片是不会进行渲染的
imgUrl.onload = function (e) {
drawImg(this);
};
// 绘制函数简单封装
function drawImg(img){
/** * @param1 {String:img} 图片资源地址 * @param2 {Number:0} 相对于图片的X点位置 * @param3 {Number:0} 相对于图片的Y点位置 * @param4 {Number:400} 切片的切出来的宽度 * @param5 {Number:267} 切片的切出来的高度 * @param6 {Number:100} 相对于画布的X点位置 * @param7 {Number:100} 相对于画布的Y点位置 * @param8 {Number:140} 切片保存到画布的宽度 * @param9 {Number:110} 切片保存到画布的高度 * @desc drawImage绘制的过程中 参数可以是3个 也可以是5个 也可以是9个 但是最少是3个 * @params 3个参数的情况:{当三个参数的时候,说明将图片直接存放到画布的某一个位置- img\x\y} * @params 5个参数的情况:{当五个参数的时候,说明将图片直接存放到画布的某一个位置同时指定图片的宽高- img\x\y\w\h} */
ctx.drawImage(img,0,0,400,267,100,100,140,110)
}
- 画布移动和状态的保存 translate、save、restore
let canvas = document.getElementById('cas')
let ctx = canvas.getContext('2d')
//使ctx本身右下方移动50
ctx.translate(50,50)
ctx.fillStyle = "red"
ctx.fillRect(0,0,50,50)
ctx.fillStyle = "blue"
ctx.fillRect(50,50,50,50)
let canvas = document.getElementById('cas')
let ctx = canvas.getContext('2d')
//保存当前没有移动之前的ctx位置(状态)
ctx.save()
ctx.translate(50,50)
ctx.fillStyle = "red"
ctx.fillRect(0,0,50,50)
//恢复之前保存下来的ctx的状态
ctx.restore()
ctx.fillStyle = "blue"
ctx.fillRect(50,50,50,50)
- 说明
save 我想了但是并不知道怎么描述给你们他的特性,官网是这么说的,“保存当前Canvas画布状态并放在栈的最上面,可以使用restore()方法依次取出”restore 一般是和save配对使用的,目的是将save保存的状态提取出来,当然save和restore本身的作用不止是这些,官网给的解释里面还有一句话的是值得注意的,保存当前Canvas画布状态并放在栈的最上面,注意这里用的是栈,也就是说他符合栈的数据结构特性,也就是先进后出、后进先出,所以这里我不知道怎么具体演示,所以给大家画了一个还相对容易理解的图下方栈底其实是栈顶,因为不想重复画图了,这里说明一下 save不是所有的属性都可以保存,他可以保存的属性有:
- 画布属性的缩放scale
这里进行特殊说明有两个原因,第一个是他和css3里面的scale不同,canvas里面是两个参数分别是XY,但是css3里面只有一个参数,就是整体的缩小,另一个原因在下面Demo里面
let canvas = document.getElementById('cas')
let ctx = canvas.getContext('2d')
ctx.fillStyle = "red"
ctx.fillRect(50, 50, 50, 50)
ctx.fillStyle = "blue"
ctx.scale(0.5, 0.5)
ctx.fillRect(150, 150, 50, 50)
- canvas实现导出
let canvas = document.getElementById('cas')
let ctx = canvas.getContext('2d')
ctx.beginPath()
ctx.fillStyle = 'red'
ctx.arc(100, 100, 50, 0, 2 * Math.PI, false)
ctx.fill()
//导出base64文件
// let data = canvas.toDataURL('image/jpeg', 1)
// console.log(data)
//导出Blob文件
let data = canvas.toBlob((v) => {
console.log(v)
}, 'image/jpeg', 0.7)
自己直接运行一下吧!没有什么特殊说明的!
- 实现一个缩放的圆圈
let canvas = document.getElementById('cas')
let ctx = canvas.getContext('2d')
let r = 10
let stopFlag = setInterval(() => {
r++
drawArc(r)
if (r > 50) {
let f = setInterval(() => {
r--
drawArc(r)
if (r < 10) {
clearInterval(f)
}
}, 10)
}
}, 20)
//绘制一个圆
function drawArc(r) {
ctx.beginPath()
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.strokeStyle = 'red'
ctx.filter = 'drop-shadow(4px 4px 8px red)'
ctx.arc(canvas.width / 2, canvas.height / 2, r, 0, 2 * Math.PI, false)
ctx.stroke()
ctx.closePath()
}
- 实现一个小游戏
<!-- * @use: 直接运行到html即可 * @description: 产生一个随机数 进行判断是否中奖 * @SpecialInstructions: 无 * @Author: clearlove * @Date: 2022-05-28 17:55:04 * @LastEditTime: 2022-05-30 00:04:05 * @FilePath: /universal-background-template/Users/leimingwei/Desktop/开源代码/canvas/鼠标点击出现一个炫彩小球.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>点击抽奖</title>
<style> button {
margin-bottom: 40px; width: 150px; height: 50px; border-radius: 5px; background-color: burlywood; font-size: 24px; color: #fff; border: none; cursor: pointer; } .result {
width: 200px; height: 60px; position: relative; user-select: none; } input {
width: 200px; height: 60px; font-size: 30px; line-height: 60px; text-align: center; position: absolute; user-select: none; border: none; top: 0; left: 0; } canvas {
position: absolute; top: 0; left: 0; } </style>
</head>
<body>
<button onclick="beginGame()">开始摇奖</button>
<div class="result">
<input disabled id="result" value="中奖了">
<canvas id="cas" width="200" height="60"></canvas>
</div>
</body>
<script> function beginGame() {
//刷新页面 重置游戏 location.reload() } // 开始游戏 function startGame() {
//将随机结果存起来 let result = document.getElementById('result') result.value = readomRasult() let canvas = document.getElementById('cas') let ctx = canvas.getContext('2d') // 绘制一个矩形 ctx.fillStyle = '#ccc' ctx.fillRect(0, 0, 200, 60) //将后置产生的元素清除 ctx.globalCompositeOperation = 'destination-out' //添加一个事件,鼠标按下去并滑动 canvas.onmousedown = () => {
canvas.onmousemove = (event) => {
ctx.beginPath() ctx.arc(event.offsetX, event.offsetY, 10, 0, 2 * Math.PI, false) ctx.fill() } } } // 产生随机中奖结果 这里的条件自己进行更改,提高中奖率 function readomRasult() {
let res = "" let n = Math.floor(Math.random() * 100) if (n % 3 === 0) {
res = "中奖了" } else {
res = "很遗憾" } return res } startGame() </script>
</html>
写到最后
这篇文章有点长,我自己也感觉到了,但是canvas我原本是准备每一个属性都写一遍,或者写一个demo进行演示,不过我想了一下,这个方式并没有什么实际意义,因为其实官网给的例子已经很详细了,所以我想到的方式就是按照我们一些常用的属性方法进行实现一些比较简单的demo效果,这样第一可以练习到canvas的属性部分,也可以提高我们对canvas的乐趣,上文中的例子很多都是B站出现过我自己写了一遍的,因为B站上面的例子是比较有代表性的,希望上面的这些例子可以帮助我们对canvas更加的了解,篇幅很长,看到这里的相信都是对canvas想学会的,我自己也不是完全对canvas非常的了解,我也是学习的过程中,所以上面的例子或者解释不保证过完全都是对的,只能说我自己运行的时候效果就是上面的效果,另外就是该文章只是将canvas的基础用法展示给大家,一些比较复杂的应用,需要大家按照基础的方法进行组合,希望有不对的地方大家及时指正!
边栏推荐
- Unique Wulin, architecture selection manual (including PDF)
- js工具函数,自己封装一个节流函数
- 如何自制一个安装程序,将程序打包生成安装程序的办法
- 测试开发工程师
- How to "transform" small and micro businesses (I)?
- ShardingSphere-Proxy 4.1 分庫分錶
- Jetpack compose layout (II) - material components and layout
- Redis (II) distributed locks and redis cluster construction
- Jetpack compose layout (IV) - constraintlayout
- Encoding format for x86
猜你喜欢
什么是 CRA
字符串 最长公共前缀
Flask博客实战 - 实现侧边栏文章归档及标签
How do wechat applets make their own programs? How to make small programs on wechat?
2台三菱PLC走BCNetTCP协议,能否实现网口无线通讯?
[MySQL learning notes 22] index
Unique Wulin, architecture selection manual (including PDF)
Tiktok brand goes to sea: both exposure and transformation are required. What are the skills of information flow advertising?
Solution to the problem of repeated startup of esp8266
Minio基本使用与原理
随机推荐
String implementation strstr()
汇付国际为跨境电商赋能:做合规的跨境支付平台!
puzzle(019.2)六边锁
MongoDB的原理、基本使用、集群和分片集群
Ruiji takeout project (II)
PHP obtains the IP address, and the apache2 server runs without error
Mongodb's principle, basic use, clustering and partitioned clustering
如何在Microsoft Exchange 2010中安装SSL证书
Gradle download warehouse is slow
MySQL source code reading (II) login connection debugging
[shared farm] smart agriculture applet, customized development and secondary development of Kaiyuan source code, which is more appropriate?
(forwarding articles) after skipping multiple pages, shuttle returns to the first page and passes parameters
Neat Syntax Design of an ETL Language (Part 2)
Learning notes of rxjs takeuntil operator
Computational Thinking and economic thinking
Redis (I) principle and basic use
Repo sync will automatically switch the correspondence between the local branch and the remote branch - how to customize this behavior
How to apply for a widget on wechat how to get a widget on wechat
Rxjs TakeUntil 操作符的学习笔记
Neat Syntax Design of an ETL Language (Part 2)