canvas由Apple首先提出,现在已经有非常好的浏览器支持度。它和<img>标签很像,但是只有widthheight两个属性,在未设置时,为300px和150px。canvas类似<video><audio>可以设置替换内容,应对标签本身不被支持的情况。另外,canvas不同于<img>,它必须要有一个</canvas>作为闭合标志。

canvas是一个2D渲染上下文环境(就像webGL是3D渲染上下文环境),在获取到<canvas>元素后,可以通过其getContext方法得到渲染上下文和相关功能,通常传入2d,用来绘制2D图案。

形状

canvas.getContext('2d')得到一个CanvasRenderingContext2D对象。剩下的绘制操作都通过调用对象上的API实现。

canvas的坐标系系统和svg一样,从左上角开始,向右和向下为正,坐标轴单位为像素。下面列出最常见的绘制图形API。

  • fillRect(x, y, width, height) 绘制矩形
  • strokeRect(x, y, width, height) 绘制矩形边框
  • clearRect(x, y, width, height) 擦除矩形范围

path被用来绘制通用曲线,注意path都是封闭的。绘制路径有4步:

  • beginPath() 新建一条路径,生成之后,图形绘制命令被指向到路径上生成路径。
  • 使用画图命令去画出路径
  • closePath() 闭合路径
  • 可选 fill() 通过填充路径的内容区域生成实心的图形,使用fill()时可以不手动闭合路径
  • stroke(),为路径添加描边
1
2
3
4
5
6
// 样例
ctx.beginPath();
ctx.moveTo(75,50);
ctx.lineTo(100,75);
ctx.lineTo(100,25);
ctx.fill();
所有的路径通过`stroke()`和`fill()`才能体现效果。

里面包含了常见的moveTolineTo,表示移动画笔/画直线到(x,y)处。画曲线时可以选择:

  • arc(x, y, radius, startAngle, endAngle, anticlockwise)
    画一个以(x,y)为圆心的以radius为半径的圆弧(圆),从startAngle开始到endAngle结束,按照anticlockwise给定的方向(默认为顺时针)来生成。
  • arcTo(x1, y1, x2, y2, radius) 根据给定的控制点和半径画一段圆弧,再以直线连接两个控制点。
  • quadraticCurveTo(cp1x, cp1y, x, y) 绘制二次贝塞尔曲线,cp1x,cp1y为一个控制点,x,y为结束点。
  • bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) 绘制三次贝塞尔曲线,cp1x,cp1y为控制点一,cp2x,cp2y为控制点二,x,y为结束点。

path2D

通过new path2D()用path2D声明子路径,允许你保留和重用路径对象。除了CanvasRenderingContext2D对象的API外,还可以用addPath新增路径到path2D对象中,它还支持通过SVG格式的字符串导入为路径。

样式

通过修改fillStylestrokeStyle改变当前填充和描边的默认颜色。支持的颜色格式有

  • 颜色名
  • hex
  • rgb
  • rgba

通过globalAlpha修改画布里的所有图形的透明度,取值在0到1之间。

线型样式选择有:

  • lineWidth = value 设置线条宽度。
  • lineCap = type 设置线条末端样式。默认为butt,还有round、square可选。
  • lineJoin = type 设定线条与线条间接合处的样式。round, bevel 和 miter三种可选,miter时,miterLimit可以限制尖角的高度。
  • miterLimit = value 限制当两条线相交时交接处最大长度;所谓交接处长度(斜接长度)是指线条交接处内角顶点到外角顶点的长度

设置虚线时,setLineDash(segments)设置当前虚线样式。lineDashOffset = value设置虚线样式的起始偏移量。getLineDash()返回一个包含当前虚线样式,长度为非负偶数的数组。

渐变

渐变需要先指定类型和覆盖范围:

  • createLinearGradient(x1, y1, x2, y2) 方法接受4个参数,表示渐变的起点 (x1,y1) 与终点 (x2,y2)。
  • createRadialGradient(x1, y1, r1, x2, y2, r2) 方法接受6个参数,前三个定义一个以 (x1,y1) 为原点,半径为r1的圆,后三个参数则定义另一个以 (x2,y2) 为原点,半径为r2的圆。

创建的渐变通过addColorStop(position, color)添加多个color stop。position参数是一个0.0与1.0之间的数值,表示渐变中颜色所在的相对位置。渐变是一种样式,通过指定给fillStyle或是strokeStyle发挥作用

模式

通过createPattern(image, type)创建pattern,Image可以是一个Image对象的引用,或者另一个canvas对象。Type描述重复的格式,是下面的字符串值之一:repeat,repeat-x,repeat-y 和 no-repeat。

同样,通过指定给fillStylestrokeStyle使用。

阴影

  • shadowOffsetX = float
  • shadowOffsetY = float
    shadowOffsetX和shadowOffsetY用来设定阴影在X和Y轴的延伸距离,它们是不受变换矩阵所影响的。负值表示阴影会往上或左延伸,正值则表示会往下或右延伸,它们默认都为0。
  • shadowBlur = float shadowBlur 用于设定阴影的模糊程度,默认为 0。
  • shadowColor = color,默认为黑色

canvas有两种填充规则non-zeroeven-odd,默认为前者。

文字

  • fillText(text, x, y [, maxWidth]) 在指定的(x,y)位置填充指定的文本,绘制的最大宽度是可选的
  • strokeText(text, x, y [, maxWidth]) 在指定的(x,y)位置绘制文本边框,绘制的最大宽度是可选的
1
2
ctx.font = "48px PingFangSC";
ctx.strokeText("Hello world", 10, 50);

除了font(语法和CSS的font相同)外,还有下面的选择:

  • textAlign = value 文本对齐选项. 可选的值包括:start, end, left, right or center. 默认值是 start。
  • textBaseline = value 基线对齐选项. 可选的值包括:top, hanging, middle, alphabetic, ideographic, bottom。默认值是 alphabetic。
  • direction = value 文本方向

另外,可以通过measureText(),得到文本绘制的宽度。

使用图片

canvas里可以引入Image对象或其他canvas元素,或者通过URL方式使用图片。

  • 使用相同页面内的图片,使用正常地获取元素的方式获取即可
  • 使用其它域名下的图片,在HTMLImageElement上使用crossOrigin属性,可以请求加载其它域名上的图片。若服务器不允许跨域加载,则会污染canvas,即不能导出数据
  • 使用canvas,按获取元素的方式获取即可

image可以指定src属性为URL或data:url的形式。甚至引入<video>使用视频帧作为image。绘制图片时,使用:

  • drawImage(image, x, y) 其中image是image或者canvas对象,x和y是其在目标canvas里的起始坐标。
  • drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)是包含了缩放和切片后完整的drawImage用法

变形和裁剪

saverestore用来保存和恢复canvas状态。每当save()方法被调用后,当前的状态就被推送到栈中保存。状态包括所有变形、样式信息。每次restore时会弹出栈顶的状态。建议在做变形和裁剪前保存状态

变形用到的属性和CSS很像:

  • translate(x,y)改变当前原点位置,
  • rotate(angle)以当前原点为圆心旋转画布,angle为弧度值。
  • scale(x, y)缩放当前canvas中的图形大小,x和y分别表示两轴上的缩放因子
  • transform(m11, m12, m21, m22, dx, dy)通过变形矩阵进行变换
  • setTransform(m11, m12, m21, m22, dx, dy)先还原为单位矩阵,再按入参的矩阵变换
  • resetTransform重置变形为单位矩阵

globalCompositeOperation定义了图形相互重叠时的处理策略,类似PS中图层的混合模式,默认为darker,还有很多别的选项

clip()即裁剪方法,和fill以及stroke类似,不过clip将路径对应的部分裁剪出指定区域。

动画

canvas只是一个画布,画出的东西都会保持原样。制作动画只能采取重绘,逐帧绘制,而每一帧包括下面几步:

  • 清空canvas,可以使用clearRect方法
  • 可选,保存当前状态
  • 使用上面提过的种种方法绘制下一帧
  • 可选,恢复状态

绘制动画通常要结合用户交互以及setTimeoutsetIntervalrequestAnimationFrame

MDN给个一个小球的组合动画可以参考。

像素级操作

ImageData接口描述<canvas>元素的一个包含像素数据的区域。它包含width, height, data单个只读属性。ImageData可以通过ctx.createImageData(width, height)或者从已有对象中创建,除此创建的所有像素都是透明黑。

可以用getImageData(left, top, width, height)方法获取指定范围的ImageData信息,当widthheight都为1时,取得当前像素信息。

使用putImageData(imageData, dx, dy)可以在当前画布(dx, dy)处绘制imageData像素数据。imageSmoothingEnabled默认开启,关闭后可以在图片缩放时看到清楚颗粒化的细节。

导出

主要有三种用法:

  • canvas.toDataURL('image/png'),默认将canvas导出成png文件
  • canvas.toDataURL('image/jpeg', quality),quality指定在0到1之间,默认为0.92。
  • canvas.toBlob(callback, type, encoderOptions),这个创建了一个在画布中的代表图片的Blob对像

toDataURL除了上面两种导出格式还有别的选择。

交互

<canvas>标签只是一个位图,它并不提供任何已经绘制在上面的对象的信息。canvas的内容不能像语义化的HTML一样暴露给一些协助工具。一般来说,你应该避免在交互型的网站或者App上使用canvas。

addHitRegion(options)可以将添加一个点击区域,options可以参考MDN文档,鼠标事件如果触发在点击区域中,会带上region用于定位点击区域。

另外drawFocusIfNeeded()scrollPathIntoView()方法还可以绘制焦点圈。

性能问题

  • 预渲染相似或重复对象
  • 为了避免抗锯齿带来的额外运算,减少使用浮点数
  • 在离屏canvas中缓存图片的不同尺寸,不要用drawImage()去缩放它们
  • 使用多层画布去画一个复杂的场景,比如使用多层画布,描绘不同层级的内容。
    1
    2
    3
    4
    5
    <div id="stage">
    <canvas id="ui-layer" width="480" height="320"></canvas>
    <canvas id="game-layer" width="480" height="320"></canvas>
    <canvas id="background-layer" width="480" height="320"></canvas>
    </div>
  • 用CSS画大的背景图
  • 用CSS transforms特性缩放画布,建议不要将小画布放大,而是去将大画布缩小
  • 尽可能避免text rendering
  • 尽可能避免 shadowBlur
  • window.requestAnimationFrame()性能优于window.setInterval()

本文是《计算机程序的构造和解释》的笔记

序中其实也包含了很多睿智的观点,值得细细体会。

  • “每一个计算机程序都是现实中的或者精神中的某个过程的一个模型”
  • “我们很少能通过自己的程序将这种过程模拟到永远令人满意的程度”
  • “不幸的是,随着程序变得更大更复杂(实际上它们几乎总是如此),这种描述本身的适宜性,一致性和正确性也都变得非常值得怀疑了”
  • “如何利用一些已经证明和有价值的组织技术,将这些结构组合成更大的结构,这些都是至关重要的”
  • “将我们的Lisp程序变换到‘机器’程序的过程本身也是抽象模型,是通过程序设计做出来的。研究和构造它们,能使人更加深刻地理解与任何模型的程序设计有关的程序组织问题”
  • “计算机永远都不够大也不够快。硬件技术的每一次突破都带来了更大规模的程序设计事业,新的组织原理,以及更加丰富的抽象模型。每个读者都应该反复问自己‘到哪里才是头儿,到哪里才是头儿’——但是不要问的过于频繁,以免忽略了程序设计的乐趣,使自己陷入一种喜忧参半的呆滞状态中”
  • “Pascal是为了建造金字塔——壮丽辉煌,令人震撼,是由各就其位的沉重巨石筑起的静态结构,而Lisp则是为了构造有机体——同样壮丽辉煌并令人震撼,由各就其位但却永不静止的无数简单的有机体片段构成的动态结构”
  • “Lisp程序大大抬高了函数库的地位,使其可用性超越了催生它们的那些具体应用”
  • “采用100个函数在一种数据结构上操作,远远优于用10个函数在10个数据结构上操作。作为这些情况的必然后果,金字塔矗立在那里千年不变,而有机体则必须演化,否则会死亡”
  • “在任何非常大的程序设计工作中,一条有用的组织原则就是通过发明新语言,去控制和隔离作业模块之间的信息流动”

过程抽象

  • 应用序和正则序
  • 递归和迭代在展开式上的区分,以及尾递归
  • 过程(函数)作为入参、返回值
  • 匿名函数和高阶函数

数据抽象

  • 构造函数和方法函数
  • conscarcdr
  • 序对和list(层次化数据)
  • 表操作和表映射
  • 序列化操作
  • 符号数据(类似字符串)
  • 数据的多种表示(类型)与通用操作

模块化、对象和状态

  • 面向对象和面向流
  • 从时间角度理解赋值和局部状态
  • 赋值的利与弊
  • 赋值带来的环境模型解释(作用域、作用域链)
    • 局部状态
    • 作用域模型的解释
  • 变动的表
    • 区分共享和相等(相同的指针、相同的值)
    • 队列与键值对
  • 描述约束系统
  • 并发(交错进行的读写操作)
    • 串行化和串行化组
    • mutex(mutual exclusion)和实现
    • 死锁(多共享资源)
      • 按顺序获取资源列表
      • 死锁恢复
      • 屏障同步
    • 延时求值的表序列
    • 延时求值的原理
    • 无穷流的构造
    • 流操作和组合

元语言设计

  • 求值器(解释器)的工作与意义
    • 在基本过程上提供组合与抽象构建一个语言
      • 表达式的嵌套
      • 变量维护
      • 过程复合
  • 求值器内核
    • eval 过程体解释
    • apply 过程求值解释
    • 表达式规范化和实现 / 派生表达式
    • 环境模型的数据结构
    • 求值器程序初始化
  • 数据即程序
    • 图灵机和停机问题
  • 内部定义
    • 内部定义是否应该具有时序
    • Y结合子与lambda演算
  • 语法分析与执行分离
  • 惰性求值
    • thunk化,关联表达式和环境
    • 惰性的表
  • 非确定性求值(满足约束的所有可行解)
    • amb和自动回溯
    • amb实现,成功与失败继续过程
  • 逻辑语言设计
    • 类SQL语言基于amb的实现

解释与编译

  • 机器描述
    • 基本指令与子程序(label)
    • 堆栈实现递归
  • 基本指令的实现
    • 类汇编语言
  • 内存管理
    • 表与堆栈的实现
    • garbage collection机制
  • 解释
    • 基础操作实现
    • 尾递归优化解释
  • 编译
    • 与解释有何区别,各自优势
    • env/argl/proc/val/continue寄存器
    • 编译器结构
      • 语法分派
      • 入参:target(存储表达式值的寄存器)与linkage(continue寄存器)
      • 指令序列的结构与构造,分析指令序列,preserving机制避免无谓的堆栈操作
    • 表达式的编译
      • linkage的编译,检查nextreturn的情况
      • 简单、条件表达式、表达式序列的编译
      • lambda表达式的编译
    • 过程的编译
      • 入参的处理
      • 尾递归
    • 指令序列的组合
    • 代码编译的实例
    • 优化变量查找
      • 词法地址
    • 解释与编译
      • 解释:机器语言 -> 用户程序
      • 编译:用户程序 -> 机器语言

最后吐槽下,书是本好书,就是翻译的不太给力,在有些地方强行提高了理解难度。

必读:WebRTC API - Web API接口 | MDN
必读:WebRTC的前世今生

Web端用户视频推流使用webRTC方案,使用已有JS SDK(和这个兼容)。

WebRTC由Google主推,全称Web Browser Real Time Communication。目标是希望在Web端进行点对点音视频通信。

整个P2P过程很复杂,但是对于浏览器而言,WebRTC实际上只是提供了三个API:

  • MediaStream, 即getUserMedia(navigator.getUserMedia),获取媒体数据,如来自用户摄像头和麦克风的音视频流
  • RTCPeerConnection,用于peer跟peer之间呼叫和建立连接以便传输音视频数据流;这个不同浏览器的实现不同,官网推荐使用adapter.js进行适配
  • RTCDataChannel,用于peer跟peer之间传输音视频之外的一般数据。

MediaStream

参考MDN。来自navigator.getUserMedia(),这个方法接收三个参数:

  • 一个约束对象,如{ audio: false, video: true },除了这两种,其他外设也可以作为输入
  • 一个成功回调
  • 一个失败回调

返回的MediaStream对象有addTrack, getAudioTracks, getVideoTracks等方法。通过这些方法取出的MediaStreamTrack数组代表对应类型的流,可以把取出的这些流导入到<video>等标签输出。在Chrome或Opera中,URL.createObjectURL()方法可以转换一个MediaStream到一个Blob URL,可以被设置作为视频的源。除了这种方法,还可以使用AudioContextAPI,对音频做处理后输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
function gotStream(stream) {
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var audioContext = new AudioContext();

// Create an AudioNode from the stream
var mediaStreamSource = audioContext.createMediaStreamSource(stream);

// Connect it to destination to hear yourself
// or any other node for processing!
mediaStreamSource.connect(audioContext.destination);
}

navigator.getUserMedia({audio:true}, gotStream);

使用HTTPS请求getUserMedia会向用户给出一次提示。不建议在HTTP环境下使用。

这里是一个demo,打开console,查看全局变量stream就明白MediaStream结构了。

约束对象中可以商议视频分辨率等信息。它会影响获取到的视频流。

RTCPeerConnection

WebRTC使用RTCPeerConnection在浏览器间传递数据流,但在之前需要有一个交换信令的过程。这个过程不在WebRTC中定义,开发者可以使用任何想用的消息协议,比如WebSocket或XHR轮询什么的。信令过程需要传递三种信息:

  • 连接控制信息:初始化或者关闭连接报告错误。
  • 网络配置:对于外网,我们电脑的 IP 地址和端口?
  • 多媒体数据:使用什么编码解码器,浏览器可以处理什么信息

点对点的连接需要ICE(Interactive Connectivity Establishment)的帮助,ICE靠STUN和TURN服务器处理NAT穿透等复杂问题。起初连接建立在UDP之上,STUN服务器让位于NAT中的client获知自己的公网IP和端口。如果UDP建立失败,考虑TCP连接,再考虑HTTP和HTTPS连接。否则使用TURN服务器做中转工作。

W3C给了RTCPeerConnection的样例,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
var signalingChannel = new SignalingChannel();
var configuration = { "iceServers": [{ "url": "stun:stun.example.org" }] };
var pc;

// 调用start()建立连接
function start() {
pc = new RTCPeerConnection(configuration);

// 有任何 ICE candidates 可用,
// 通过signalingChannel传递candidate给对方
pc.onicecandidate = function (evt) {
if (evt.candidate)
signalingChannel.send(JSON.stringify({ "candidate": evt.candidate }));
};

// let the "negotiationneeded" event trigger offer generation
pc.onnegotiationneeded = function () {
pc.createOffer(localDescCreated, logError);
}

// 远端流到达时,在remoteView中做展示
pc.onaddstream = function (evt) {
remoteView.src = URL.createObjectURL(evt.stream);
};

// 获取本地流,展示并传递
navigator.getUserMedia({ "audio": true, "video": true }, function (stream) {
selfView.src = URL.createObjectURL(stream);
pc.addStream(stream);
}, logError);
}

function localDescCreated(desc) {
pc.setLocalDescription(desc, function () {
signalingChannel.send(JSON.stringify({ "sdp": pc.localDescription }));
}, logError);
}

signalingChannel.onmessage = function (evt) {
if (!pc)
start();

var message = JSON.parse(evt.data);
if (message.sdp) {
pc.setRemoteDescription(new RTCSessionDescription(message.sdp), function () {
// 接收到offer时,回应一个answer
if (pc.remoteDescription.type == "offer")
pc.createAnswer(localDescCreated, logError);
}, logError);
} else {
// 接收对方candidate并加入自己的RTCPeerConnection
pc.addIceCandidate(new RTCIceCandidate(message.candidate));
}
};

function logError(error) {
log(error.name + ": " + error.message);
}

在开始建立连接时,调用start(),创建RTCPeerConnection对象,接着完成下面步骤:

  • 交换网络信息onicecandidate回调在有任何candidate出现时,将通过SignalChannel(使用额外方法创建,如WebSocket)传递给对方。同样地,在通过SignalChannel接收到对方发来的该信息时,加入这个candidate到RTCPeerConnection中。
  • 交换多媒体信息,使用SDP(Session Description Protocol)与对端交换多媒体资讯,在onnegotiationneeded中,调用createOffer通过setLocalDescription创建RTCSessionDecription对象进行本地存储,并传给对方。接收方通过setRemoteDescription方法设定remote description。

上述过程称为JavaScript Session Establishment Protocol(JSEP)。一旦这个signaling完成了,数据可以直接的在端到端之间进行数据传输。如果失败了,通过中介服务器relay服务进行转发。

通常RTCPeerConnection的API太复杂,所以有很多在此基础上的库封装出了更加友善的API。

JS-SDK接入流程

参考:Agora:视频通话API

  1. 准备工作,包括界面绘制等
  2. 向远端注册当前用户,获取token,为后面做准备
  3. 使用createClient()方法创建客户端
  4. 指定回调,包括已订阅流、已添加流、移除、失败等生命周期事件的回调
  5. 初始化客户端,传入appId和成功回调
  6. 初始化成功后,调用join方法根据获取到的token加入指定房间(原理是WebRTC的stream有id)
  7. 指定配置创建本地流(getUserMedia),发布本地流,播放本地流

RTMP相关

必读:RTMP H5 直播流技术解析

一次RTMP握手的模拟。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 握手协议模拟
class C {
constructor(time, random) {
this.time = time || 0;
this.random = random || Buffer.alloc(0); // Buffer类型
}

get C0() {
const buf = Buffer.alloc(1, 3);
return buf;
}

get C1() {
const buf = Buffer.alloc(1536);
return buf;
}

/**
* write C2
*/
get C2() {
let buf = Buffer.alloc(1536);
buf.writeUInt32BE(this.time, 4);
this.random.copy(buf, 8, 0, 1528);
}
}

// 客户端
const client = new net.socket();
const RTMP_C = new C();

client.connect({
port: 1935,
host: 'example.live.com',
() => {
console.log('connected');
}
});

client.on('data', (data) => {
if (!data) {
console.warn('Empty Buffer.');
}
if (!RTMP_C.S0 && data.length > 0) {
RTMP_C.S0 = data.readUInt8(0);
data = data.slice(1);
}

if (!RTMP_C.S1 && data.length >= 1536) {
RTMP_C.time = data.readUInt32BE(0);
RTMP_C.random = data.slice(8, 1536);
RTMP_C.S1 = true;
data = data.slice(1536)
console.log('send C2');
client.write(RTMP_C.C2);
}

if (!RTMP_C.S2 && data.length >= 1536) {
RTMP_C.S2 = true;
data = data.slice(1536);
}
});

持续更新…

免密ssh步骤

一句命令代替繁琐的ssh远程登录开发机。

Step 1:免密

  1. ssh-keygen生成公钥。ssh-keygen
  2. 拷贝公钥。ssh-copy-id -i ~/.ssh/id_rsa.pub <your-remote-host>
  3. 免密登录。ssh <your-remote-host>

Step 2:简化命令

使用alias,比如:alias timetowork="ssh <your-remote-host>"

Step 3:get back to work

输入timetowork

参考:

fis-receiver

简写为fisrcv。使用fis进行项目构建时,若需要release到远端开发机,可以通过配置fis-conf.js里的deploy项目实现,fis会通过HTTP的方式上传压缩过的代码到远端指定位置,这需要远端有receiver接收上传的文件。

fis-receiver是在远端接收上传文件的服务端脚本,node、python、PHP等都可以。fisrcv实际上是使用node服务在远端接收deploy文件的服务端脚本而已。

参考:

webpack-release

等同于webpack版的fis release,不过原先写在fis-conf.js中的部署设置,现在写在webpack.config.js中。receiveUrlremotePath即远端开发机位置。实现上也采用HTTP POST的方式。

参考:

tmux

tmux是终端复用工具,允许在单个终端下相互隔离地运行多个后台程序。甚至在关闭终端时可以让程序在后台运行。使用tmux attachtmux detach进入和离开各个session。attach后还可以接-t指定连接的session。

参考:

HtmlWebpackPlugin

把html和js或css文件对应组织起来,可以指定filenametemplatechunks等。

参考:

encodeURI和encodeURIComponent

前者用于对整段URI转码,后者用于对URI中被分割符隔开的部分进行边编码。因此,

  • encodeURI会忽略允许出现在URI的符号,包括特殊符号。对空格、中文等进行转码
  • encodeURIComponent也会转码特殊符号,如/,$,@,.等

origami

origami是sublime中的一个拆分窗口的插件,用快捷键可以像在vim中一样方便地创建和转移到各个窗口编码。通过command + K开启快捷键。

  • +up/down/left/right 转移到其他窗口
  • +command+up/down/left/right 在该方向上打开新的工作窗口
  • +shift+command+up/down/left/right 销毁该方向上的新窗口

nrm与n

npm registry管理工具nrm,能够查看和切换当前使用的registry,在切换和查看registry时非常有用。常用命令:

  • nrm ls
  • nrm use
  • nrm help
  • nrm home
  • nrm add/delete 增加和删除registry
  • nrm test 测速

n是类似nvm的node.js版本管理工具。

Promise then的链式调用

then()方法返回一个Promise 。它最多需要有两个参数:Promise的成功和失败情况的回调函数。

then方法会返回一个Promise,它的行为与then中指定的回调函数返回值有关:

  • 如果then中的回调函数返回一个值,那么then返回的Promise将会成为接受状态(即使原Promise始Rejected状态),并且将返回的值作为接受状态的回调函数的参数值。
  • 如果then中的回调函数抛出一个错误,那么then返回的Promise将会成为拒绝状态,并且将抛出的错误作为拒绝状态的回调函数的参数值。
  • 如果then中的回调函数返回一个已经是接受状态的Promise,那么then返回的Promise也会成为接受状态,并且将那个Promise的接受状态的回调函数的参数值作为该被返回的Promise的接受状态回调函数的参数值。
  • 如果then中的回调函数返回一个已经是拒绝状态的Promise,那么then返回的Promise也会成为拒绝状态,并且将那个Promise的拒绝状态的回调函数的参数值作为该被返回的Promise的拒绝状态回调函数的参数值。
  • 如果then中的回调函数返回一个未定状态(pending)的Promise,那么then返回Promise的状态也是未定的,并且它的终态与那个Promise的终态相同;同时,它变为终态时调用的回调函数参数与那个Promise变为终态时的回调函数的参数是相同的。

下面是几个官网上的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Promise.resolve("foo")
// 1. 接收 "foo" 并与 "bar" 拼接,并将其结果做为下一个resolve返回。
.then(function(string) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
string += 'bar';
resolve(string);
}, 1);
});
})
// 2. 接收 "foobar", 放入一个异步函数中处理该字符串
// 并将其打印到控制台中, 但是不将处理后的字符串返回到下一个。
.then(function(string) {
setTimeout(function() {
string += 'baz';
console.log(string);
}, 1)
return string;
})
// 3. 打印本节中代码将如何运行的帮助消息,
// 字符串实际上是由上一个回调函数之前的那块异步代码处理的。
.then(function(string) {
console.log("Last Then: oops... didn't bother to instantiate and return " +
"a promise in the prior then so the sequence may be a bit " +
"surprising");

// 注意 `string` 这时不会存在 'baz'。
// 因为这是发生在我们通过setTimeout模拟的异步函数中。
console.log(string);
});
1
2
3
4
5
6
7
8
9
10
11
Promise.resolve()
.then( () => {
// 使 .then() 返回一个 rejected promise
throw 'Oh no!';
})
.catch( reason => {
console.error( 'onRejected function called: ', reason );
})
.then( () => {
console.log( "I am always called even if the prior then's promise rejects" );
});

SOLID原则

程序设计领域,尤其是面向对象编程的优秀实践里,有着一些实现原则,如SOLID(单一功能、开闭原则、里氏替换、接口隔离、依赖翻转)。这些设计模式原则可以有助于编写可维护、可拓展、清晰可读的代码。

  • S,Single Responsibility Principle,每个类都应有单一的功能,且被类封装起来。
  • O,Open-Closed Principle,对象(类、接口、函数等)对于拓展是开放的,对于修改是封闭的。即易拓展、保证可靠。
  • L,Liskov Substitution Principle,子类可以在不改变正确性的情况下替换父类
  • I,Interface-segregation Principle,多个特定功能的接口好于单个宽泛功能的接口
  • D,Dependency Inversion Principle,方法应该依赖于一个抽象(接口)而不是一个实例(类)

axios-mock-adpter

使用axios获取数据时,通过axios-mock-adaptermock数据。MockAdapter可以绑定在axios上,拦截通过绑定的axios发送的请求。使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var axios = require('axios');
var MockAdapter = require('axios-mock-adapter');

// This sets the mock adapter on the default instance
var mock = new MockAdapter(axios);

// Mock any GET request to /users
// arguments for reply are (status, data, headers)
mock.onGet('/users').reply(200, {
users: [
{ id: 1, name: 'John Smith' }
]
});

axios.get('/users')
.then(function(response) {
console.log(response.data);
});

另外,可以用mock.restore()撤销所有mocking行为,或通过mock.reset()除去所有mocking的handler。通过mock.on<方法名>还可以链式调用其他方法:

  • onAny() 绑定任何方法
  • networkError() 返回网络错误
  • timeout() 返回请求超时
  • passThrough() 跳过mocking直接请求

在reply中可以使用函数进行更复杂的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var normalAxios = axios.create();
var mockAxios = axios.create();
var mock = MockAdapter(mockAxios);

mock
.onGet('/orders')
.reply(() => Promise.all([
normalAxios
.get('/api/v1/orders')
.then(resp => resp.data),
normalAxios
.get('/api/v2/orders')
.then(resp => resp.data),
{ id: '-1', content: 'extra row 1' },
{ id: '-2', content: 'extra row 2' }
]).then(
sources => [200, sources.reduce((agg, source) => agg.concat(source))]
)
);

移动端Charles调试经验

  1. 设置http代理为8888端口
  2. 设置SSL代理,填写对应的域名,设置端口为443,安装根证书
  3. 手机打开无线设置,设置代理IP和端口8888
  4. 打开chls.pro/ssl,安装根证书并信任
  5. 使用Map remote访问开发机位置
  6. 使用Map local可以劫持WebView中请求的JS等资源到本地,通过alert的方式打印调试信息,进行临时的线上Webview环境debug

官网:https://quilljs.com

quill可以是一个文本编辑器JS库或是文本编辑器构建库。它提供了结构化数据方式用脱离语言的方式描述编辑器内容,同时预置了内置插件,支持自定义插件,有助于在此基础上进行和业务相关编辑器开发。

准备工作

关于contenteditable属性,selection对象和range对象的介绍,可以参考这篇文章

配置

1
2
3
const editor = new Quill("#container");
// 或是直接传入DOM对象
const editor = new Quill(document.body);

quill通常使用上面的方式初始化。在初始化时,支持用丰富的配置项定义生成的编辑器。下面是一个例子。

1
2
3
4
5
6
7
8
9
10
const options = {
debug: 'info',
modules: {
toolbar: '#toolbar'
},
placeholder: 'Tell a story...',
readOnly: false,
them: 'snow'
}
const editor = new Quill('#container', options);

Quill支持的配置项有:

  • bounds,Quill UI元素的限制范围,默认document.body
  • debug,是Quill.debug的快捷方式,用于打印调试信息,默认级别为warn
  • formats,Quill中允许出现的格式,默认为所有格式,它和toolbar是解耦的
  • modules,注册在Quill中的功能模块和与之对应的配置信息,Quill会有默认配置
  • placeholder,提示信息
  • readOnly,是否可写
  • scrollContainer,编辑器滚动条的父级,默认为编辑器本身
  • strict,版本更新配置,默认为true
  • theme,整体外观,默认为snowbubble可选

支持的文本格式

分为行内块级嵌入式三大类。

  • 行内:加粗/背景色/字体颜色/字体/行内代码/斜体/下划线/删除线/链接/字体大小/上、下标
  • 块级:引用/标题/行首缩进/有序、无序列表/对齐/文本方向/代码块
  • 嵌入式:音频/视频/公式

API

按照由浅入深,分为修改内容修改格式选取编辑器本身事件数据模型操作拓展几大类。涉及到内容修改的都会返回代表更改的delta。

修改内容

涉及到修改时,最后一个参数都可以选择userapisilentuser类型下,disabled时会没有效果。

  • deleteText,输入起始点和长度,删除特定范围的内容,返回delta类型数据。如quill.deleteText(6, 4)
  • getContents,获取delta格式的编辑器内容,如quill.getContents()
  • getLength,获取文本长度,quill默认会有一个空行,所以默认返回1
  • getText,获取文本内容,跳过非文本如音视频元素,如quill.getText(0, 10)
  • insertEmbed,输入位置,类型,值,插入嵌入式内容,如quill.insertEmbed(10, 'image', 'https://quilljs.com/images/cloud.png')
  • insertText,插入文本,可带格式。如下
    1
    2
    3
    4
    5
    6
    quill.insertText(0, 'Hello', 'bold', true);

    quill.insertText(5, 'Quill', {
    'color': '#ffff00',
    'italic': true
    });
  • setContent,输入delta,重设编辑器内容,需以\n结尾。如下:
    1
    2
    3
    4
    5
    quill.setContents([
    { insert: 'Hello ' },
    { insert: 'World!', attributes: { bold: true } },
    { insert: '\n' }
    ]);
  • setText,设置文本,返回代表改变的delta
  • updateContent,输入delta,更新内容,返回代表更新的delta

修改格式

  • format,设置用户当前所选的文本格式,如quill.format('color', 'red');
  • formatLine,设置给定选择当前整行样式,使用类似format的方法设置样式,也支持直接传入格式对象。类似quill.formatLine(1, 2, { 'align': 'right'})
  • formatText,设置给定范围内文本格式,类似formatLine
  • getFormat,获取给定范围内的格式,没有输入时,返回当前选择的格式
  • removeFormat,移除范围内样式

选取

  • getBounds(index, length = 0),返回的top、width、height、left相对于编辑器容器而言
  • getSelection(focus = false),返回用户的选取范围,由index、length组成
  • setSelection(index, length = 0),设置选区范围,会自动focus,输入null会自动blur

编辑器本身

  • blur,失焦
  • disable,禁用
  • enable(enabled = false),启用
  • focus,聚焦
  • hasFocus,是否聚焦
  • update,同步用户改动,协同工作时常用

事件

事件通过on方法绑定在quill对象上。

  • text-change,quill内的内容改变时触发,回调函数可以获取delta、oldContent,source。通常来自’user’,source为’silent’时,该事件不会触发。
  • selection-change,回调函数可以获取range,oldRange,source
  • editor-change,上述两个事件触发时触发,即使source为silent

除了on方法,还有once用于绑定一次和off方法解绑。

数据模型操作

  • find,寻找DOM节点对应的quill或Blot对象
  • getIndex,返回文档开头到给定Blot的偏移量
  • getLeafBlot,返回给定位置的Blot
  • getLine,返回给定位置整行的Blot
  • getLines,返回给定范围的Blot

拓展

  • debug,设置调试信息级别,info | log | warn | error
  • import,导出quill相关库,输入相对于quill的路径
    1
    2
    3
    4
    5
    var Parchment = Quill.import('parchment');
    var Delta = Quill.import('delta');

    var Toolbar = Quill.import('modules/toolbar');
    var Link = Quill.import('formats/link');
  • register,注册module到quill中,有下面几种用法
    1
    2
    3
    Quill.register(format: Attributor | BlotDefinintion, supressWarning: Boolean = false)
    Quill.register(path: String, def: any, supressWarning: Boolean = false)
    Quill.register(defs: { [String]: any }, supressWarning: Boolean = false)
  • addContainer,新增容器并返回
  • enable/disable,启用、禁用编辑器

delta

delta是quill中最重要的概念。据介绍所说,quill是“第一个”使用delta(结构化数据)这个概念的。不同于其他大多数文本编辑器需要反复执行修改编辑器中的HTML文档。quill维护一个delta数组,使用JSON数据的方式描述了文档的内容。

使用delta一词,并没有问题,因为可以理解成文档本身是由空内容 + delta一点点得到的。delta主要有两个特性:

  • 权威性,delta和对应的生成结果是一一对应的,没有歧义
  • 压缩性,delta中描述的操作是经过压缩后的

delta中的操作可以分为增、删、修改格式,分别对应insertdeleteretain操作。对文本编辑器的一次改动(真实世界中的改动行为)只可能涉及上述三种行为的一种(Quill并不允许Ctrl多处选中)。其中retain的意义类似于光标的移动,它使得这三种操作并不需要使用index描述,便于Quill做优化和压缩。

delta的操作实际上是对parchment进行的,它类似于vdom,使用JS的数据结构对文本编辑器中可能出现的各元素进行了抽象,称作Blot。Blot有scroll,inline、block、text,break几种。父Blot下必须包含至少一个子Blot,而所有的Blot都包含在一个scroll Blot下。文本编辑器中特定格式的文本块都用特定的Blot表示,每个这样的Blot都必须继承自上面的一种Blot类型。就像通过下面的方式继承了Blot,就可以使对应的行内元素得到对应的编辑器样式元素对应起来,并使用在后面的编辑器里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let Inline = Quill.import('blots/inline');

class BoldBlot extends Inline { }
BoldBlot.blotName = 'bold';
BoldBlot.tagName = 'strong';

class ItalicBlot extends Inline { }
ItalicBlot.blotName = 'italic';
ItalicBlot.tagName = 'em';

Quill.register(BoldBlot);
Quill.register(ItalicBlot);

// in your editor

quill.insertText(0, 'Test', { bold: true });
quill.formatText(0, 4, 'italic', true);

类似地,我们定义一个Link Blot。它相比bold,italic不同的是,它需要一个string而不是boolean初始化。因此需要定义createformat两个函数。其中create在构造Blot时使用,value即输入的href,formats将用户的format字段和真实DOM的字段相关联。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class LinkBlot extends Inline {
static create(value) {
let node = super.create();
// Sanitize url value if desired
node.setAttribute('href', value);
// Okay to set other non-format related attributes
// These are invisible to Parchment so must be static
node.setAttribute('target', '_blank');
return node;
}

static formats(node) {
// We will only be called with a node already
// determined to be a Link blot, so we do
// not need to check ourselves
return node.getAttribute('href');
}
}
LinkBlot.blotName = 'link';
LinkBlot.tagName = 'a';

Quill.register(LinkBlot);

定义引用这样的块级元素时,对应地继承Block Blot即可。和inline Blot不同的是,Block Blot无法嵌套,在对已有的块级元素应用时会替换而不是嵌套绑定在元素上。以Header元素为例,可以指定tagName为一个数组,可以在format时使用1、2的方式指定具体哪种tag。

1
2
3
4
5
6
7
8
9
class HeaderBlot extends Block {
static formats(node) {
return HeaderBlot.tagName.indexOf(node.tagName) + 1;
}
}
HeaderBlot.blotName = 'header';
// Medium only supports two header sizes, so we will only demonstrate two,
// but we could easily just add more tags into this array
HeaderBlot.tagName = ['H1', 'H2'];

类似的,可以在插入embed Blot,这种类型效果是插入在元素中间的新的tag。如Image。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
let BlockEmbed = Quill.import('blots/block/embed');

class ImageBlot extends BlockEmbed {
static create(value) {
let node = super.create();
node.setAttribute('alt', value.alt);
node.setAttribute('src', value.url);
return node;
}

static value(node) {
return {
alt: node.getAttribute('alt'),
url: node.getAttribute('src')
};
}
}
ImageBlot.blotName = 'image';
ImageBlot.tagName = 'img';

// 使用时
let range = quill.getSelection(true);
quill.insertText(range.index, '\n', Quill.sources.USER);
quill.insertEmbed(range.index + 1, 'image', {
alt: 'Quill Cloud',
url: 'https://quilljs.com/0.20/assets/images/cloud.png'
}, Quill.sources.USER);
quill.setSelection(range.index + 2, Quill.sources.SILENT);

modules

quill中的module位于quill的应用层。可以通过定制modules,利用quill的功能;或是更改quill内置module,修改quill本身的行为和功能。clipboard、keyboard、history三个module是quill默认加载的。用户完全可以根据业务需求定义自己的module。官网给了简单的例子展示了module的大致骨架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Quill.register('modules/counter', function(quill, options) {
var container = document.querySelector(options.container);
quill.on('text-change', function() {
var text = quill.getText();
if (options.unit === 'word') {
container.innerText = text.split(/\s+/).length + ' words';
} else {
container.innerText = text.length + ' characters';
}
});
});

var quill = new Quill('#editor', {
modules: {
counter: {
container: '#counter',
unit: 'word'
}
}
});

只需要定义一个可以接收quill对象的函数即可,在函数内部利用quill事件监听即可完成应用层的建设。

Toolbar和ClipBoard

ToolbarClipboard是Quill内置的两个module,对你构建自己的文本编辑器有很大的借鉴意义。

Toolbar用来定制工具栏上的按钮,是自定义编辑器(尤其是业务相关的编辑器)逃不开的一部分。它有几个基本配置:

  • container,放置工具栏的DOM容器
  • handler,点击ToolBar图标时注册的函数,传入Blot的value,通过调用Quill的API完成功能。也可以通过下面方式注册。
1
2
3
// Handlers can also be added post initialization
var toolbar = quill.getModule('toolbar');
toolbar.addHandler('image', showImageUI);

在遇到从别的文本编辑器拷贝内容过来的情况时,需要修改ClipBoard Module中addMatcher的定义。这个方法向ClipBoard中注册了新的Matcher匹配拷贝过来的HTML文本,将之转换为对应的Blot。如:

1
2
3
4
5
6
7
8
quill.clipboard.addMatcher(Node.TEXT_NODE, function(node, delta) {
return new Delta().insert(node.data);
});

// Interpret a <b> tag as bold
quill.clipboard.addMatcher('.custom-class', function(node, delta) {
return delta.compose(new Delta().retain(delta.length(), { bold: true }));
});

或者在configuration中,注入新定义的matcher即可。

1
2
3
4
5
6
7
8
9
10
var quill = new Quill('#editor', {
modules: {
clipboard: {
matchers: [
['B', customMatcherA],
[Node.TEXT_NODE, customMatcherB]
]
}
}
});

参考

0%